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 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 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 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}