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