widgetry/tools/
colors.rs

1use geom::{Circle, Distance, Line, Polygon, Pt2D, Tessellation};
2
3use crate::{Color, EventCtx, Fill, GeomBatch, Line, LinearGradient, Text, Widget};
4
5pub struct ColorLegend {}
6
7impl ColorLegend {
8    pub fn row(ctx: &EventCtx, color: Color, label: impl AsRef<str>) -> Widget {
9        let radius = 15.0;
10        Widget::row(vec![
11            GeomBatch::from(vec![(
12                color,
13                Circle::new(Pt2D::new(radius, radius), Distance::meters(radius)).to_polygon(),
14            )])
15            .into_widget(ctx)
16            .centered_vert(),
17            Text::from(label).wrap_to_pct(ctx, 35).into_widget(ctx),
18        ])
19    }
20
21    pub fn gradient_with_width<I: Into<String>>(
22        ctx: &mut EventCtx,
23        scale: &ColorScale,
24        labels: Vec<I>,
25        width: f64,
26    ) -> Widget {
27        assert!(scale.0.len() >= 2);
28        let n = scale.0.len();
29        let mut batch = GeomBatch::new();
30        let width_each = width / ((n - 1) as f64);
31        batch.push(
32            Fill::LinearGradient(LinearGradient {
33                line: Line::must_new(Pt2D::new(0.0, 0.0), Pt2D::new(width, 0.0)),
34                stops: scale
35                    .0
36                    .iter()
37                    .enumerate()
38                    .map(|(idx, color)| ((idx as f64) / ((n - 1) as f64), *color))
39                    .collect(),
40            }),
41            Tessellation::union_all(
42                (0..n - 1)
43                    .map(|i| {
44                        Tessellation::from(
45                            Polygon::rectangle(width_each, 32.0)
46                                .translate((i as f64) * width_each, 0.0),
47                        )
48                    })
49                    .collect(),
50            ),
51        );
52        // Extra wrapping to make the labels stretch against just the scale, not everything else
53        // TODO Long labels aren't nicely lined up with the boundaries between buckets
54        Widget::col(vec![
55            batch.into_widget(ctx),
56            Widget::custom_row(
57                labels
58                    .into_iter()
59                    .map(|lbl| Line(lbl).small().into_widget(ctx))
60                    .collect(),
61            )
62            .evenly_spaced(),
63        ])
64        .container()
65    }
66
67    pub fn gradient<I: Into<String>>(
68        ctx: &mut EventCtx,
69        scale: &ColorScale,
70        labels: Vec<I>,
71    ) -> Widget {
72        Self::gradient_with_width(ctx, scale, labels, 300.0)
73    }
74
75    pub fn categories(ctx: &mut EventCtx, pairs: Vec<(Color, &str)>, max: &str) -> Widget {
76        assert!(pairs.len() >= 2);
77        let width = 300.0;
78        let n = pairs.len();
79        let mut batch = GeomBatch::new();
80        let width_each = width / ((n - 1) as f64);
81        for (idx, (color, _)) in pairs.iter().enumerate() {
82            batch.push(
83                *color,
84                Polygon::rectangle(width_each, 32.0).translate((idx as f64) * width_each, 0.0),
85            );
86        }
87        // Extra wrapping to make the labels stretch against just the scale, not everything else
88        // TODO Long labels aren't nicely lined up with the boundaries between buckets
89        let mut labels = pairs
90            .into_iter()
91            .map(|(_, lbl)| Line(lbl).small().into_widget(ctx))
92            .collect::<Vec<_>>();
93        labels.push(Line(max).small().into_widget(ctx));
94        Widget::col(vec![
95            batch.into_widget(ctx),
96            Widget::custom_row(labels).evenly_spaced(),
97        ])
98        .container()
99    }
100}
101
102pub struct DivergingScale {
103    low_color: Color,
104    mid_color: Color,
105    high_color: Color,
106    min: f64,
107    avg: f64,
108    max: f64,
109    ignore: Option<(f64, f64)>,
110}
111
112impl DivergingScale {
113    pub fn new(low_color: Color, mid_color: Color, high_color: Color) -> DivergingScale {
114        DivergingScale {
115            low_color,
116            mid_color,
117            high_color,
118            min: 0.0,
119            avg: 0.5,
120            max: 1.0,
121            ignore: None,
122        }
123    }
124
125    pub fn range(mut self, min: f64, max: f64) -> DivergingScale {
126        assert!(min < max);
127        self.min = min;
128        self.avg = (min + max) / 2.0;
129        self.max = max;
130        self
131    }
132
133    pub fn ignore(mut self, from: f64, to: f64) -> DivergingScale {
134        assert!(from < to);
135        self.ignore = Some((from, to));
136        self
137    }
138
139    pub fn eval(&self, value: f64) -> Option<Color> {
140        let value = value.clamp(self.min, self.max);
141        if let Some((from, to)) = self.ignore {
142            if value >= from && value <= to {
143                return None;
144            }
145        }
146        if value <= self.avg {
147            Some(
148                self.low_color
149                    .lerp(self.mid_color, (value - self.min) / (self.avg - self.min)),
150            )
151        } else {
152            Some(
153                self.mid_color
154                    .lerp(self.high_color, (value - self.avg) / (self.max - self.avg)),
155            )
156        }
157    }
158
159    pub fn make_legend<I: Into<String>>(self, ctx: &mut EventCtx, labels: Vec<I>) -> Widget {
160        ColorLegend::gradient(
161            ctx,
162            &ColorScale(vec![self.low_color, self.mid_color, self.high_color]),
163            labels,
164        )
165    }
166}
167
168pub struct ColorScale(pub Vec<Color>);
169
170impl ColorScale {
171    pub fn eval(&self, pct: f64) -> Color {
172        let (low, pct) = self.inner_eval(pct);
173        self.0[low].lerp(self.0[low + 1], pct)
174    }
175
176    #[allow(unused)]
177    pub fn from_colorous(gradient: colorous::Gradient) -> ColorScale {
178        let n = 7;
179        ColorScale(
180            (0..n)
181                .map(|i| {
182                    let c = gradient.eval_rational(i, n);
183                    Color::rgb(c.r as usize, c.g as usize, c.b as usize)
184                })
185                .collect(),
186        )
187    }
188
189    fn inner_eval(&self, pct: f64) -> (usize, f64) {
190        assert!((0.0..=1.0).contains(&pct));
191        // What's the interval between each pair of colors?
192        let width = 1.0 / (self.0.len() - 1) as f64;
193        let low = (pct / width).floor() as usize;
194        if low == self.0.len() - 1 {
195            return (low - 1, 1.0);
196        }
197        (low, (pct % width) / width)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    #[test]
204    fn test_scale() {
205        use super::{Color, ColorScale};
206
207        let two = ColorScale(vec![Color::BLACK, Color::WHITE]);
208        assert_same((0, 0.0), two.inner_eval(0.0));
209        assert_same((0, 0.5), two.inner_eval(0.5));
210        assert_same((0, 1.0), two.inner_eval(1.0));
211
212        let three = ColorScale(vec![Color::BLACK, Color::RED, Color::WHITE]);
213        assert_same((0, 0.0), three.inner_eval(0.0));
214        assert_same((0, 0.4), three.inner_eval(0.2));
215        assert_same((1, 0.0), three.inner_eval(0.5));
216        assert_same((1, 0.4), three.inner_eval(0.7));
217        assert_same((1, 1.0), three.inner_eval(1.0));
218    }
219
220    fn assert_same(expected: (usize, f64), actual: (usize, f64)) {
221        assert_eq!(expected.0, actual.0);
222        if (expected.1 - actual.1).abs() > 0.0001 {
223            panic!("{:?} != {:?}", expected, actual);
224        }
225    }
226}