Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add utility to rr.components.Color to generate colors from any string (and use it in the air traffic data example) #8458

Merged
merged 2 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions examples/python/air_traffic_data/air_traffic_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ def process_measurement(self, measurement: Measurement) -> None:
)

entity_path = f"aircraft/{measurement.icao_id}"
color = rr.components.Color.from_string(entity_path)

if (
measurement.latitude is not None
Expand All @@ -247,13 +248,16 @@ def process_measurement(self, measurement: Measurement) -> None:
):
rr.log(
entity_path,
rr.Points3D([
self._proj.transform(
measurement.longitude,
measurement.latitude,
measurement.barometric_altitude,
)
]),
rr.Points3D(
[
self._proj.transform(
measurement.longitude,
measurement.latitude,
measurement.barometric_altitude,
),
],
colors=color,
),
rr.GeoPoints(lat_lon=[measurement.latitude, measurement.longitude]),
)

Expand All @@ -264,6 +268,7 @@ def process_measurement(self, measurement: Measurement) -> None:
rr.log(
entity_path + "/barometric_altitude",
rr.Scalar(measurement.barometric_altitude),
rr.SeriesLine(color=color),
)

def flush(self) -> None:
Expand Down Expand Up @@ -310,7 +315,13 @@ def log_position_and_altitude(self, df: polars.DataFrame, icao_id: str) -> None:
return

if icao_id not in self._position_indicators:
rr.log(entity_path, [rr.archetypes.Points3D.indicator(), rr.archetypes.GeoPoints.indicator()], static=True)
color = rr.components.Color.from_string(entity_path)
rr.log(
entity_path,
[rr.archetypes.Points3D.indicator(), rr.archetypes.GeoPoints.indicator(), color],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good lord I didnt realize how much that code needs tagged components 🙈

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good example of a component which is intended to be shared across two archetypes, btw. Hopefully we don't make that use case too contrived, aka if that Color is tagged with the Points3D archetype, it should be implicitly(?) used by the GeoPoints archetype as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha yeah I didn't even realize that was what's happening, interesting.

Yeah we'll have to figure out how we want to handle those. I do think heuristics to implicitly pick up the same component from unrelated tags if on the same entity makes sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vaguely related is one realisation we've had when discussing the archetype vs. visualiser relationship the other day: indicator components are additive, while tags are exclusive. With the former, you could have 2 archetypes with 1 components (e.g. hypothetical ScalarSeriesLine + ScalarSeriesPoint archetypes based on a single scalar component + 2 indicators), but with the latter you are forced to make a choice.

We don't think this constraint is going to be much of problem though. More in #8368

static=True,
)
rr.log(entity_path + "/barometric_altitude", [rr.archetypes.SeriesLine.indicator(), color], static=True)
self._position_indicators.add(icao_id)

timestamps = rr.TimeSecondsColumn("unix_time", df["timestamp"].to_numpy())
Expand Down
3 changes: 2 additions & 1 deletion rerun_py/rerun_sdk/rerun/components/color.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions rerun_py/rerun_sdk/rerun/components/color_ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import annotations

import colorsys
import math
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from . import Color

_GOLDEN_RATIO = (math.sqrt(5.0) - 1.0) / 2.0


class ColorExt:
"""Extension for [Color][rerun.components.Color]."""

@staticmethod
def from_string(s: str) -> Color:
"""
Generate a random yet deterministic color based on a string.

The color is guaranteed to be identical for the same input string.
"""

from . import Color

# adapted from egui::PlotUi
hue = (hash(s) & 0xFFFF) / 2**16 * _GOLDEN_RATIO
return Color([round(comp * 255) for comp in colorsys.hsv_to_rgb(hue, 0.85, 0.5)])
Loading