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(
77 backend: &mut dyn DrawBackend,
78 scales: &ScaleSet,
79 data: &DataFrame,
80 i: Option<usize>,
81 what: &str,
82 value: &Value,
83 axis: Option<String>,
84) {
85 let series = i.and_then(|i| super::series_key(data, i));
86 let raw = super::raw_value(value);
87 let aes = if what == "x" {
88 Aesthetic::X
89 } else {
90 Aesthetic::Y
91 };
92 let formatted = if backend.wants_formatted_values() {
93 scales.get(&aes).and_then(|s| s.format_value(value))
94 } else {
95 None
96 };
97 let shown = formatted.clone().unwrap_or_else(|| super::tip_value(value));
98 let tip = raw.as_ref().map(|_| match &series {
99 Some(s) => format!("{s}: {what} = {shown}"),
100 None => format!("{what} = {shown}"),
101 });
102 let axis = axis.map(|_| formatted.unwrap_or(shown));
103 super::set_mark(backend, tip, axis, series, raw);
104 super::set_mark_formatted(
105 backend,
106 scales,
107 (what == "x").then_some(value),
108 &Aesthetic::Y,
109 (what == "y").then_some(value),
110 );
111}
112
113fn line_rows(data: &DataFrame, col: &str, constant: f64) -> Vec<(Option<usize>, Value)> {
116 match data.column(col) {
117 Some(c) => c
118 .iter()
119 .enumerate()
120 .filter(|(_, v)| v.as_f64().is_some_and(f64::is_finite))
121 .map(|(i, v)| (Some(i), v.clone()))
122 .collect(),
123 None if constant.is_finite() => vec![(None, Value::Float(constant))],
124 None => vec![],
125 }
126}
127
128pub struct GeomHline {
130 pub yintercept: f64,
131 pub color: (u8, u8, u8),
132 pub width: f64,
133 pub linetype: Linetype,
134 pub alpha: f64,
135}
136
137impl GeomHline {
138 pub fn new(yintercept: f64) -> Self {
139 GeomHline {
140 yintercept,
141 color: (0, 0, 0),
142 width: 1.0,
143 linetype: Linetype::Dashed,
144 alpha: 1.0,
145 }
146 }
147
148 pub fn mapped() -> Self {
151 Self::new(f64::NAN)
152 }
153}
154
155impl Geom for GeomHline {
156 fn draw(
157 &self,
158 data: &DataFrame,
159 coord: &dyn Coord,
160 scales: &ScaleSet,
161 _theme: &Theme,
162 backend: &mut dyn DrawBackend,
163 ) -> Result<(), RenderError> {
164 let plot_area = backend.plot_area();
165 let y_scale = scales.get(&Aesthetic::Y);
166 for (row, value) in line_rows(data, "yintercept", self.yintercept) {
167 let ny = map_position(y_scale, &value).unwrap_or(0.5);
168 if !ny.is_finite() {
169 continue;
170 }
171 let style = match row {
172 Some(i) => row_style(
173 data,
174 i,
175 scales,
176 self.color,
177 self.width,
178 self.linetype,
179 self.alpha,
180 ),
181 None => LineStyle {
182 color: self.color,
183 alpha: self.alpha,
184 width: self.width,
185 linetype: self.linetype,
186 ..Default::default()
187 },
188 };
189 mark_line(backend, scales, data, row, "y", &value, None);
190 let pts = sample_segment(coord, &plot_area, (0.0, ny), (1.0, ny));
191 backend.draw_line(&pts, &style)?;
192 }
193 super::clear_mark(backend);
194 Ok(())
195 }
196
197 fn required_aes(&self) -> Vec<Aesthetic> {
198 vec![]
199 }
200 fn default_stat(&self) -> Box<dyn Stat> {
201 Box::new(StatIdentity)
202 }
203 fn default_position(&self) -> Box<dyn Position> {
204 Box::new(PositionIdentity)
205 }
206 fn default_params(&self) -> GeomParams {
207 GeomParams::default()
208 }
209 fn name(&self) -> &str {
210 "hline"
211 }
212 fn inherit_aes(&self) -> bool {
213 false
214 }
215 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
216 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
217 self.color = ink;
218 }
219 }
220}
221
222pub struct GeomVline {
224 pub xintercept: f64,
225 pub color: (u8, u8, u8),
226 pub width: f64,
227 pub linetype: Linetype,
228 pub alpha: f64,
229}
230
231impl GeomVline {
232 pub fn new(xintercept: f64) -> Self {
233 GeomVline {
234 xintercept,
235 color: (0, 0, 0),
236 width: 1.0,
237 linetype: Linetype::Dashed,
238 alpha: 1.0,
239 }
240 }
241
242 pub fn mapped() -> Self {
244 Self::new(f64::NAN)
245 }
246}
247
248impl Geom for GeomVline {
249 fn draw(
250 &self,
251 data: &DataFrame,
252 coord: &dyn Coord,
253 scales: &ScaleSet,
254 _theme: &Theme,
255 backend: &mut dyn DrawBackend,
256 ) -> Result<(), RenderError> {
257 let plot_area = backend.plot_area();
258 let x_scale = scales.get(&Aesthetic::X);
259 for (row, value) in line_rows(data, "xintercept", self.xintercept) {
260 let nx = map_position(x_scale, &value).unwrap_or(0.5);
261 if !nx.is_finite() {
262 continue;
263 }
264 let style = match row {
265 Some(i) => row_style(
266 data,
267 i,
268 scales,
269 self.color,
270 self.width,
271 self.linetype,
272 self.alpha,
273 ),
274 None => LineStyle {
275 color: self.color,
276 alpha: self.alpha,
277 width: self.width,
278 linetype: self.linetype,
279 ..Default::default()
280 },
281 };
282 let axis = Some(super::tip_value(&value));
283 mark_line(backend, scales, data, row, "x", &value, axis);
284 let pts = sample_segment(coord, &plot_area, (nx, 0.0), (nx, 1.0));
285 backend.draw_line(&pts, &style)?;
286 }
287 super::clear_mark(backend);
288 Ok(())
289 }
290
291 fn required_aes(&self) -> Vec<Aesthetic> {
292 vec![]
293 }
294 fn default_stat(&self) -> Box<dyn Stat> {
295 Box::new(StatIdentity)
296 }
297 fn default_position(&self) -> Box<dyn Position> {
298 Box::new(PositionIdentity)
299 }
300 fn default_params(&self) -> GeomParams {
301 GeomParams::default()
302 }
303 fn name(&self) -> &str {
304 "vline"
305 }
306 fn inherit_aes(&self) -> bool {
307 false
308 }
309 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
310 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
311 self.color = ink;
312 }
313 }
314}
315
316pub struct GeomAbline {
319 pub slope: f64,
320 pub intercept: f64,
321 pub color: (u8, u8, u8),
322 pub width: f64,
323 pub linetype: Linetype,
324 pub alpha: f64,
325}
326
327impl GeomAbline {
328 pub fn new(slope: f64, intercept: f64) -> Self {
329 GeomAbline {
330 slope,
331 intercept,
332 color: (0, 0, 0),
333 width: 1.0,
334 linetype: Linetype::Dashed,
335 alpha: 1.0,
336 }
337 }
338
339 pub fn mapped() -> Self {
342 Self::new(1.0, 0.0)
343 }
344}
345
346fn panel_x_range(scales: &ScaleSet) -> (f64, f64) {
348 match scales.get(&Aesthetic::X) {
349 Some(s) if s.is_discrete() => (0.5, s.breaks().len().max(1) as f64 + 0.5),
350 Some(s) => s.expanded_domain().unwrap_or((0.0, 1.0)),
351 None => (0.0, 1.0),
352 }
353}
354
355pub(crate) fn abline_runs(scales: &ScaleSet, slope: f64, intercept: f64) -> Vec<Vec<(f64, f64)>> {
358 if !(slope.is_finite() && intercept.is_finite()) {
359 return vec![];
360 }
361 let (x0, x1) = panel_x_range(scales);
362 let y_scale = scales.get(&Aesthetic::Y);
363 let ny = |x: f64| {
364 let y = intercept + slope * x;
365 match y_scale {
366 Some(_) => map_position(y_scale, &Value::Float(y)).unwrap_or(f64::NAN),
367 None => y,
368 }
369 };
370 clip_polyline_unit(&[(0.0, ny(x0)), (1.0, ny(x1))])
371}
372
373fn sample_segment(
377 coord: &dyn Coord,
378 plot_area: &crate::render::Rect,
379 a: (f64, f64),
380 b: (f64, f64),
381) -> Vec<(f64, f64)> {
382 let n = if coord.is_polar() || coord.is_radar() {
383 64
384 } else {
385 1
386 };
387 (0..=n)
388 .map(|k| {
389 let t = k as f64 / n as f64;
390 coord.transform((a.0 + t * (b.0 - a.0), a.1 + t * (b.1 - a.1)), plot_area)
391 })
392 .collect()
393}
394
395impl Geom for GeomAbline {
396 fn draw(
397 &self,
398 data: &DataFrame,
399 coord: &dyn Coord,
400 scales: &ScaleSet,
401 _theme: &Theme,
402 backend: &mut dyn DrawBackend,
403 ) -> Result<(), RenderError> {
404 let plot_area = backend.plot_area();
405 let slope_col = data.column("slope");
406 let icpt_col = data.column("intercept");
407 let rows: Vec<(Option<usize>, f64, f64)> = if slope_col.is_some() || icpt_col.is_some() {
408 (0..data.nrows())
409 .map(|i| {
410 let get = |c: Option<&[Value]>, d: f64| {
411 c.map_or(Some(d), |c| c.get(i).and_then(|v| v.as_f64()))
412 };
413 (i, get(slope_col, 1.0), get(icpt_col, 0.0))
414 })
415 .filter_map(|(i, s, a)| Some((Some(i), s?, a?)))
416 .collect()
417 } else {
418 vec![(None, self.slope, self.intercept)]
419 };
420 for (row, slope, intercept) in rows {
421 let style = match row {
422 Some(i) => row_style(
423 data,
424 i,
425 scales,
426 self.color,
427 self.width,
428 self.linetype,
429 self.alpha,
430 ),
431 None => LineStyle {
432 color: self.color,
433 alpha: self.alpha,
434 width: self.width,
435 linetype: self.linetype,
436 ..Default::default()
437 },
438 };
439 let series = row.and_then(|i| super::series_key(data, i));
440 let label = format!(
441 "y = {} + {}·x",
442 super::tip_value(&Value::Float(intercept)),
443 super::tip_value(&Value::Float(slope))
444 );
445 let tip = Some(match &series {
446 Some(s) => format!("{s}: {label}"),
447 None => label,
448 });
449 super::set_mark(
450 backend,
451 tip,
452 None,
453 series,
454 super::raw_value(&Value::Float(slope)),
455 );
456 super::set_mark_formatted(backend, scales, None, &Aesthetic::Y, None);
458 for run in abline_runs(scales, slope, intercept) {
459 let mut pts = Vec::new();
460 for w in run.windows(2) {
461 let seg = sample_segment(coord, &plot_area, w[0], w[1]);
462 if !pts.is_empty() {
463 pts.pop();
464 }
465 pts.extend(seg);
466 }
467 backend.draw_line(&pts, &style)?;
468 }
469 }
470 super::clear_mark(backend);
471 Ok(())
472 }
473
474 fn required_aes(&self) -> Vec<Aesthetic> {
475 vec![]
476 }
477 fn default_stat(&self) -> Box<dyn Stat> {
478 Box::new(StatIdentity)
479 }
480 fn default_position(&self) -> Box<dyn Position> {
481 Box::new(PositionIdentity)
482 }
483 fn default_params(&self) -> GeomParams {
484 GeomParams::default()
485 }
486 fn name(&self) -> &str {
487 "abline"
488 }
489 fn inherit_aes(&self) -> bool {
490 false
491 }
492 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
493 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
494 self.color = ink;
495 }
496 }
497}
498
499#[cfg(test)]
500mod fixed_value_tests {
501 use crate::data::Value;
502 use crate::prelude::*;
503
504 #[test]
509 fn fixed_value_reflines_render_without_a_data_mapping() {
510 let svg = GGPlot::new(vec![
511 ("x".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
512 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
513 ])
514 .aes(Aes::new().x("x").y("y"))
515 .geom_point()
516 .geom_hline(0.5)
517 .geom_vline(0.5)
518 .geom_abline(1.0, 0.0)
519 .render_svg_native_with_size(200, 200)
520 .unwrap();
521
522 assert!(!svg.contains("NaN"));
523 assert!(svg.matches("<polyline").count() >= 3);
526 }
527}