1use crate::aes::Aesthetic;
17use crate::coord::Coord;
18use crate::data::{DataFrame, Value};
19use crate::position::identity::PositionIdentity;
20use crate::position::Position;
21use crate::render::backend::{DrawBackend, LineStyle, Linetype};
22use crate::render::RenderError;
23use crate::scale::ScaleSet;
24use crate::stat::identity::StatIdentity;
25use crate::stat::Stat;
26use crate::theme::Theme;
27
28use super::support::{clip_polyline_unit, map_position};
29use super::{Geom, GeomParams};
30
31fn row_style(
33 data: &DataFrame,
34 i: usize,
35 scales: &ScaleSet,
36 color: (u8, u8, u8),
37 width: f64,
38 linetype: Linetype,
39 alpha: f64,
40) -> LineStyle {
41 let color = data
42 .column("color")
43 .and_then(|c| c.get(i))
44 .and_then(|v| scales.map_color(&Aesthetic::Color, v))
45 .unwrap_or(color);
46 let linetype = data
47 .column("linetype")
48 .and_then(|c| c.get(i))
49 .and_then(|v| scales.map_linetype(v))
50 .unwrap_or(linetype);
51 let alpha = data
52 .column("alpha")
53 .and_then(|c| c.get(i))
54 .and_then(|v| scales.map_alpha(v).or_else(|| v.as_f64()))
55 .filter(|a| a.is_finite())
56 .unwrap_or(alpha);
57 LineStyle {
58 color,
59 alpha,
60 width,
61 linetype,
62 ..Default::default()
63 }
64}
65
66fn mark_line(
70 backend: &mut dyn DrawBackend,
71 data: &DataFrame,
72 i: Option<usize>,
73 what: &str,
74 value: &Value,
75 axis: Option<String>,
76) {
77 let series = i.and_then(|i| super::series_key(data, i));
78 let raw = super::raw_value(value);
79 let tip = raw.as_ref().map(|v| match &series {
80 Some(s) => format!("{s}: {what} = {}", super::tip_value(value)),
81 None => format!("{what} = {v}"),
82 });
83 super::set_mark(backend, tip, axis, series, raw);
84}
85
86fn line_rows(data: &DataFrame, col: &str, constant: f64) -> Vec<(Option<usize>, Value)> {
89 match data.column(col) {
90 Some(c) => c
91 .iter()
92 .enumerate()
93 .filter(|(_, v)| v.as_f64().is_some_and(f64::is_finite))
94 .map(|(i, v)| (Some(i), v.clone()))
95 .collect(),
96 None if constant.is_finite() => vec![(None, Value::Float(constant))],
97 None => vec![],
98 }
99}
100
101pub struct GeomHline {
103 pub yintercept: f64,
104 pub color: (u8, u8, u8),
105 pub width: f64,
106 pub linetype: Linetype,
107 pub alpha: f64,
108}
109
110impl GeomHline {
111 pub fn new(yintercept: f64) -> Self {
112 GeomHline {
113 yintercept,
114 color: (0, 0, 0),
115 width: 1.0,
116 linetype: Linetype::Dashed,
117 alpha: 1.0,
118 }
119 }
120
121 pub fn mapped() -> Self {
124 Self::new(f64::NAN)
125 }
126}
127
128impl Geom for GeomHline {
129 fn draw(
130 &self,
131 data: &DataFrame,
132 coord: &dyn Coord,
133 scales: &ScaleSet,
134 _theme: &Theme,
135 backend: &mut dyn DrawBackend,
136 ) -> Result<(), RenderError> {
137 let plot_area = backend.plot_area();
138 let y_scale = scales.get(&Aesthetic::Y);
139 for (row, value) in line_rows(data, "yintercept", self.yintercept) {
140 let ny = map_position(y_scale, &value).unwrap_or(0.5);
141 if !ny.is_finite() {
142 continue;
143 }
144 let style = match row {
145 Some(i) => row_style(
146 data,
147 i,
148 scales,
149 self.color,
150 self.width,
151 self.linetype,
152 self.alpha,
153 ),
154 None => LineStyle {
155 color: self.color,
156 alpha: self.alpha,
157 width: self.width,
158 linetype: self.linetype,
159 ..Default::default()
160 },
161 };
162 mark_line(backend, data, row, "y", &value, None);
163 let pts = sample_segment(coord, &plot_area, (0.0, ny), (1.0, ny));
164 backend.draw_line(&pts, &style)?;
165 }
166 super::clear_mark(backend);
167 Ok(())
168 }
169
170 fn required_aes(&self) -> Vec<Aesthetic> {
171 vec![]
172 }
173 fn default_stat(&self) -> Box<dyn Stat> {
174 Box::new(StatIdentity)
175 }
176 fn default_position(&self) -> Box<dyn Position> {
177 Box::new(PositionIdentity)
178 }
179 fn default_params(&self) -> GeomParams {
180 GeomParams::default()
181 }
182 fn name(&self) -> &str {
183 "hline"
184 }
185 fn inherit_aes(&self) -> bool {
186 false
187 }
188}
189
190pub struct GeomVline {
192 pub xintercept: f64,
193 pub color: (u8, u8, u8),
194 pub width: f64,
195 pub linetype: Linetype,
196 pub alpha: f64,
197}
198
199impl GeomVline {
200 pub fn new(xintercept: f64) -> Self {
201 GeomVline {
202 xintercept,
203 color: (0, 0, 0),
204 width: 1.0,
205 linetype: Linetype::Dashed,
206 alpha: 1.0,
207 }
208 }
209
210 pub fn mapped() -> Self {
212 Self::new(f64::NAN)
213 }
214}
215
216impl Geom for GeomVline {
217 fn draw(
218 &self,
219 data: &DataFrame,
220 coord: &dyn Coord,
221 scales: &ScaleSet,
222 _theme: &Theme,
223 backend: &mut dyn DrawBackend,
224 ) -> Result<(), RenderError> {
225 let plot_area = backend.plot_area();
226 let x_scale = scales.get(&Aesthetic::X);
227 for (row, value) in line_rows(data, "xintercept", self.xintercept) {
228 let nx = map_position(x_scale, &value).unwrap_or(0.5);
229 if !nx.is_finite() {
230 continue;
231 }
232 let style = match row {
233 Some(i) => row_style(
234 data,
235 i,
236 scales,
237 self.color,
238 self.width,
239 self.linetype,
240 self.alpha,
241 ),
242 None => LineStyle {
243 color: self.color,
244 alpha: self.alpha,
245 width: self.width,
246 linetype: self.linetype,
247 ..Default::default()
248 },
249 };
250 let axis = Some(super::tip_value(&value));
251 mark_line(backend, data, row, "x", &value, axis);
252 let pts = sample_segment(coord, &plot_area, (nx, 0.0), (nx, 1.0));
253 backend.draw_line(&pts, &style)?;
254 }
255 super::clear_mark(backend);
256 Ok(())
257 }
258
259 fn required_aes(&self) -> Vec<Aesthetic> {
260 vec![]
261 }
262 fn default_stat(&self) -> Box<dyn Stat> {
263 Box::new(StatIdentity)
264 }
265 fn default_position(&self) -> Box<dyn Position> {
266 Box::new(PositionIdentity)
267 }
268 fn default_params(&self) -> GeomParams {
269 GeomParams::default()
270 }
271 fn name(&self) -> &str {
272 "vline"
273 }
274 fn inherit_aes(&self) -> bool {
275 false
276 }
277}
278
279pub struct GeomAbline {
282 pub slope: f64,
283 pub intercept: f64,
284 pub color: (u8, u8, u8),
285 pub width: f64,
286 pub linetype: Linetype,
287 pub alpha: f64,
288}
289
290impl GeomAbline {
291 pub fn new(slope: f64, intercept: f64) -> Self {
292 GeomAbline {
293 slope,
294 intercept,
295 color: (0, 0, 0),
296 width: 1.0,
297 linetype: Linetype::Dashed,
298 alpha: 1.0,
299 }
300 }
301
302 pub fn mapped() -> Self {
305 Self::new(1.0, 0.0)
306 }
307}
308
309fn panel_x_range(scales: &ScaleSet) -> (f64, f64) {
311 match scales.get(&Aesthetic::X) {
312 Some(s) if s.is_discrete() => (0.5, s.breaks().len().max(1) as f64 + 0.5),
313 Some(s) => s.expanded_domain().unwrap_or((0.0, 1.0)),
314 None => (0.0, 1.0),
315 }
316}
317
318pub(crate) fn abline_runs(scales: &ScaleSet, slope: f64, intercept: f64) -> Vec<Vec<(f64, f64)>> {
321 if !(slope.is_finite() && intercept.is_finite()) {
322 return vec![];
323 }
324 let (x0, x1) = panel_x_range(scales);
325 let y_scale = scales.get(&Aesthetic::Y);
326 let ny = |x: f64| {
327 let y = intercept + slope * x;
328 match y_scale {
329 Some(_) => map_position(y_scale, &Value::Float(y)).unwrap_or(f64::NAN),
330 None => y,
331 }
332 };
333 clip_polyline_unit(&[(0.0, ny(x0)), (1.0, ny(x1))])
334}
335
336fn sample_segment(
340 coord: &dyn Coord,
341 plot_area: &crate::render::Rect,
342 a: (f64, f64),
343 b: (f64, f64),
344) -> Vec<(f64, f64)> {
345 let n = if coord.is_polar() || coord.is_radar() {
346 64
347 } else {
348 1
349 };
350 (0..=n)
351 .map(|k| {
352 let t = k as f64 / n as f64;
353 coord.transform((a.0 + t * (b.0 - a.0), a.1 + t * (b.1 - a.1)), plot_area)
354 })
355 .collect()
356}
357
358impl Geom for GeomAbline {
359 fn draw(
360 &self,
361 data: &DataFrame,
362 coord: &dyn Coord,
363 scales: &ScaleSet,
364 _theme: &Theme,
365 backend: &mut dyn DrawBackend,
366 ) -> Result<(), RenderError> {
367 let plot_area = backend.plot_area();
368 let slope_col = data.column("slope");
369 let icpt_col = data.column("intercept");
370 let rows: Vec<(Option<usize>, f64, f64)> = if slope_col.is_some() || icpt_col.is_some() {
371 (0..data.nrows())
372 .map(|i| {
373 let get = |c: Option<&[Value]>, d: f64| {
374 c.map_or(Some(d), |c| c.get(i).and_then(|v| v.as_f64()))
375 };
376 (i, get(slope_col, 1.0), get(icpt_col, 0.0))
377 })
378 .filter_map(|(i, s, a)| Some((Some(i), s?, a?)))
379 .collect()
380 } else {
381 vec![(None, self.slope, self.intercept)]
382 };
383 for (row, slope, intercept) in rows {
384 let style = match row {
385 Some(i) => row_style(
386 data,
387 i,
388 scales,
389 self.color,
390 self.width,
391 self.linetype,
392 self.alpha,
393 ),
394 None => LineStyle {
395 color: self.color,
396 alpha: self.alpha,
397 width: self.width,
398 linetype: self.linetype,
399 ..Default::default()
400 },
401 };
402 let series = row.and_then(|i| super::series_key(data, i));
403 let label = format!(
404 "y = {} + {}·x",
405 super::tip_value(&Value::Float(intercept)),
406 super::tip_value(&Value::Float(slope))
407 );
408 let tip = Some(match &series {
409 Some(s) => format!("{s}: {label}"),
410 None => label,
411 });
412 super::set_mark(
413 backend,
414 tip,
415 None,
416 series,
417 super::raw_value(&Value::Float(slope)),
418 );
419 for run in abline_runs(scales, slope, intercept) {
420 let mut pts = Vec::new();
421 for w in run.windows(2) {
422 let seg = sample_segment(coord, &plot_area, w[0], w[1]);
423 if !pts.is_empty() {
424 pts.pop();
425 }
426 pts.extend(seg);
427 }
428 backend.draw_line(&pts, &style)?;
429 }
430 }
431 super::clear_mark(backend);
432 Ok(())
433 }
434
435 fn required_aes(&self) -> Vec<Aesthetic> {
436 vec![]
437 }
438 fn default_stat(&self) -> Box<dyn Stat> {
439 Box::new(StatIdentity)
440 }
441 fn default_position(&self) -> Box<dyn Position> {
442 Box::new(PositionIdentity)
443 }
444 fn default_params(&self) -> GeomParams {
445 GeomParams::default()
446 }
447 fn name(&self) -> &str {
448 "abline"
449 }
450 fn inherit_aes(&self) -> bool {
451 false
452 }
453}
454
455#[cfg(test)]
456mod fixed_value_tests {
457 use crate::data::Value;
458 use crate::prelude::*;
459
460 #[test]
465 fn fixed_value_reflines_render_without_a_data_mapping() {
466 let svg = GGPlot::new(vec![
467 ("x".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
468 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
469 ])
470 .aes(Aes::new().x("x").y("y"))
471 .geom_point()
472 .geom_hline(0.5)
473 .geom_vline(0.5)
474 .geom_abline(1.0, 0.0)
475 .render_svg_native_with_size(200, 200)
476 .unwrap();
477
478 assert!(!svg.contains("NaN"));
479 assert!(svg.matches("<polyline").count() >= 3);
482 }
483}