Skip to main content

rustial_engine/visualization/
legend.rs

1//! Legend metadata for client-side UI rendering.
2
3use super::ColorRamp;
4
5/// Normalization mode for legend value display.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum NormalizationMode {
8    /// Values are displayed as-is.
9    Absolute,
10    /// Values are normalized to `[0, 1]` using the field min/max.
11    ZeroToOne,
12    /// Values are displayed as percentages `[0, 100]`.
13    Percentage,
14}
15
16/// A labeled stop in a legend.
17#[derive(Debug, Clone)]
18pub struct LabeledStop {
19    /// Normalized position in `[0.0, 1.0]`.
20    pub position: f32,
21    /// Human-readable label for this stop.
22    pub label: String,
23}
24
25/// Legend metadata sufficient for client-side UI rendering.
26///
27/// Rustial does **not** render the legend itself. This struct carries
28/// enough information for a client application to build a legend widget
29/// in Bevy UI, egui, HTML, or any other framework.
30#[derive(Debug, Clone)]
31pub struct LegendSpec {
32    /// Human-readable title (e.g. `"Agent Density"`).
33    pub title: String,
34    /// Units string (e.g. `"agents/m2"`, `"dBm"`, `"%"`).
35    pub units: String,
36    /// The colour ramp driving both renderer output and legend display.
37    pub ramp: ColorRamp,
38    /// Minimum data value (maps to ramp position 0.0).
39    pub min_value: f64,
40    /// Maximum data value (maps to ramp position 1.0).
41    pub max_value: f64,
42    /// Optional labeled tick marks for the legend.
43    pub labeled_stops: Vec<LabeledStop>,
44    /// How values should be presented.
45    pub normalization: NormalizationMode,
46}
47
48impl LegendSpec {
49    /// Create a minimal legend spec.
50    pub fn new(title: impl Into<String>, ramp: ColorRamp, min: f64, max: f64) -> Self {
51        Self {
52            title: title.into(),
53            units: String::new(),
54            ramp,
55            min_value: min,
56            max_value: max,
57            labeled_stops: Vec::new(),
58            normalization: NormalizationMode::Absolute,
59        }
60    }
61
62    /// Set the units string.
63    pub fn with_units(mut self, units: impl Into<String>) -> Self {
64        self.units = units.into();
65        self
66    }
67
68    /// Add a labeled stop.
69    pub fn with_stop(mut self, position: f32, label: impl Into<String>) -> Self {
70        self.labeled_stops.push(LabeledStop {
71            position,
72            label: label.into(),
73        });
74        self
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::visualization::{ColorRamp, ColorStop};
82
83    #[test]
84    fn legend_spec_builder() {
85        let ramp = ColorRamp::new(vec![
86            ColorStop {
87                value: 0.0,
88                color: [0.0, 0.0, 1.0, 1.0],
89            },
90            ColorStop {
91                value: 1.0,
92                color: [1.0, 0.0, 0.0, 1.0],
93            },
94        ]);
95        let legend = LegendSpec::new("Test", ramp, 0.0, 100.0)
96            .with_units("m/s")
97            .with_stop(0.0, "Low")
98            .with_stop(1.0, "High");
99
100        assert_eq!(legend.title, "Test");
101        assert_eq!(legend.units, "m/s");
102        assert_eq!(legend.labeled_stops.len(), 2);
103        assert!((legend.min_value - 0.0).abs() < 1e-9);
104        assert!((legend.max_value - 100.0).abs() < 1e-9);
105    }
106}