1use crate::aes::Aesthetic;
6use crate::coord::Coord;
7use crate::data::{DataFrame, Value};
8use crate::position::identity::PositionIdentity;
9use crate::position::Position;
10use crate::render::backend::{DrawBackend, LineStyle, Linetype, RectStyle};
11use crate::render::RenderError;
12use crate::scale::ScaleSet;
13use crate::stat::identity::StatIdentity;
14use crate::stat::Stat;
15use crate::theme::Theme;
16
17use super::{Geom, GeomParams};
18
19pub struct GeomBullet {
23 pub target: Option<String>,
25 pub ranges: Vec<f64>,
28 pub range_columns: Vec<String>,
30 pub band_colors: Vec<(u8, u8, u8)>,
32 pub bar_color: (u8, u8, u8),
33 pub target_color: (u8, u8, u8),
34 pub band_width: f64,
36 pub bar_width: f64,
37 pub target_width: f64,
38}
39
40impl Default for GeomBullet {
41 fn default() -> Self {
42 GeomBullet {
43 target: None,
44 ranges: Vec::new(),
45 range_columns: Vec::new(),
46 band_colors: vec![(150, 150, 150), (190, 190, 190), (225, 225, 225)],
47 bar_color: (40, 40, 40),
48 target_color: (200, 30, 30),
49 band_width: 0.75,
50 bar_width: 0.28,
51 target_width: 0.55,
52 }
53 }
54}
55
56impl GeomBullet {
57 pub fn with_target(mut self, target_column: impl Into<String>) -> Self {
59 self.target = Some(target_column.into());
60 self
61 }
62
63 pub fn with_ranges(mut self, ranges: impl IntoIterator<Item = f64>) -> Self {
65 self.ranges = ranges.into_iter().collect();
66 self
67 }
68
69 pub fn with_range_columns<S: Into<String>>(
71 mut self,
72 cols: impl IntoIterator<Item = S>,
73 ) -> Self {
74 self.range_columns = cols.into_iter().map(Into::into).collect();
75 self
76 }
77
78 fn row_ranges(&self, data: &DataFrame, i: usize) -> Vec<f64> {
79 let mut r: Vec<f64> = if self.range_columns.is_empty() {
80 self.ranges.clone()
81 } else {
82 self.range_columns
83 .iter()
84 .filter_map(|c| data.column(c).and_then(|col| col[i].as_f64()))
85 .collect()
86 };
87 r.retain(|v| v.is_finite());
88 r.sort_by(|a, b| a.total_cmp(b));
89 r
90 }
91}
92
93impl Geom for GeomBullet {
94 fn draw(
95 &self,
96 data: &DataFrame,
97 coord: &dyn Coord,
98 scales: &ScaleSet,
99 _theme: &Theme,
100 backend: &mut dyn DrawBackend,
101 ) -> Result<(), RenderError> {
102 let x_col = data
103 .column("x")
104 .ok_or(RenderError::MissingAesthetic("x".into()))?;
105 let y_col = data
106 .column("y")
107 .ok_or(RenderError::MissingAesthetic("y".into()))?;
108 let target_col = self.target.as_deref().and_then(|c| data.column(c));
109 let area = backend.plot_area();
110 let x_scale = scales.get(&Aesthetic::X);
111 let my = |v: f64| {
112 scales
113 .get(&Aesthetic::Y)
114 .map(|s| s.map(&Value::Float(v)))
115 .unwrap_or(0.0)
116 };
117 let slot = if x_scale.map(|s| s.is_discrete()).unwrap_or(false) {
118 let n = x_scale.map(|s| s.breaks().len()).unwrap_or(1).max(1) as f64;
119 1.0 / (n * if coord.discrete_expand() { 1.1 } else { 1.0 })
120 } else {
121 2.0 * super::continuous_bar_half_width(
122 x_col.iter().filter_map(|v| x_scale.map(|s| s.map(v))),
123 1.0,
124 0.05,
125 )
126 };
127 let rect = |b: &mut dyn DrawBackend, nx: f64, frac: f64, lo: f64, hi: f64, fill| {
128 let h = slot * frac / 2.0;
129 let (a, c) = (
130 coord.transform((nx - h, my(hi)), &area),
131 coord.transform((nx + h, my(lo)), &area),
132 );
133 b.draw_rect(
134 (a.0.min(c.0), a.1.min(c.1)),
135 (a.0.max(c.0), a.1.max(c.1)),
136 &RectStyle {
137 fill: Some(fill),
138 stroke: None,
139 stroke_width: 0.0,
140 alpha: 1.0,
141 clip: true,
142 corner_radius: 0.0,
143 },
144 )
145 };
146
147 for i in 0..data.nrows() {
148 let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.5);
149 let Some(actual) = y_col[i].as_f64() else {
150 continue;
151 };
152 let x_txt = super::tip_value(&x_col[i]);
153 super::clear_mark(backend);
154 let mut lo = 0.0;
155 for (k, hi) in self.row_ranges(data, i).into_iter().enumerate() {
156 let color = self.band_colors[k % self.band_colors.len().max(1)];
157 rect(backend, nx, self.band_width, lo, hi, color)?;
158 lo = hi;
159 }
160 let target = target_col.and_then(|c| c[i].as_f64());
161 let tip = match target {
162 Some(t) => format!(
163 "{x_txt}: {} (target {})",
164 super::tip_value(&y_col[i]),
165 super::tip_value(&Value::Float(t))
166 ),
167 None => format!("{x_txt}: {}", super::tip_value(&y_col[i])),
168 };
169 super::set_mark(
170 backend,
171 Some(tip),
172 Some(x_txt),
173 super::series_key(data, i),
174 super::raw_value(&y_col[i]),
175 );
176 super::set_row_extras(backend, data, i);
177 rect(backend, nx, self.bar_width, 0.0, actual, self.bar_color)?;
178 if let Some(t) = target {
179 let h = slot * self.target_width / 2.0;
180 backend.draw_line(
181 &[
182 coord.transform((nx - h, my(t)), &area),
183 coord.transform((nx + h, my(t)), &area),
184 ],
185 &LineStyle {
186 color: self.target_color,
187 alpha: 1.0,
188 width: 3.0,
189 linetype: Linetype::Solid,
190 ..Default::default()
191 },
192 )?;
193 }
194 }
195 super::clear_mark(backend);
196 super::clear_row_extras(backend);
197 Ok(())
198 }
199
200 fn required_aes(&self) -> Vec<Aesthetic> {
201 vec![Aesthetic::X, Aesthetic::Y]
202 }
203 fn default_stat(&self) -> Box<dyn Stat> {
204 Box::new(StatIdentity)
205 }
206 fn default_position(&self) -> Box<dyn Position> {
207 Box::new(PositionIdentity)
208 }
209 fn default_params(&self) -> GeomParams {
210 GeomParams::default()
211 }
212 fn name(&self) -> &str {
213 "bullet"
214 }
215 fn include_zero_baseline(&self) -> bool {
216 true
217 }
218 fn consumes_tooltips(&self) -> bool {
219 true
220 }
221 fn set_series_color(&mut self, color: (u8, u8, u8)) {
222 self.bar_color = color;
223 }
224
225 fn setup_data(&self, data: &mut DataFrame, _warnings: &mut Vec<String>) {
227 let n = data.nrows();
228 let target_col = self.target.as_deref().and_then(|c| data.column(c));
229 let y_col = data.column("y");
230 let ymax: Vec<Value> = (0..n)
231 .map(|i| {
232 let mut m = y_col.and_then(|c| c[i].as_f64()).unwrap_or(f64::NAN);
233 if let Some(t) = target_col.and_then(|c| c[i].as_f64()) {
234 m = m.max(t);
235 }
236 for r in self.row_ranges(data, i) {
237 m = m.max(r);
238 }
239 Value::Float(m)
240 })
241 .collect();
242 if !data.has_column("ymax") {
243 data.add_column("ymax".into(), ymax);
244 }
245 }
246}