1mod delegate;
26
27mod chromaticity;
28pub use chromaticity::XYChromaticity;
29
30mod range;
31pub use range::{ScaleRange, ScaleRangeIterator, ScaleRangeWithStep, ScaleValue};
32
33use std::{collections::HashMap, ops::RangeBounds, rc::Rc};
34
35use svg::{
36 node::element::{path::Data, ClipPath, Definitions, Group, Image, Line, Path, Text, SVG},
37 Node,
38};
39
40use crate::{
41 layer::Layer, new_id, rendable::Rendable, round_to_default_precision, view::ViewParameters,
42 StyleAttr,
43};
44
45pub type CoordinateTransform = Rc<dyn Fn((f64, f64)) -> (f64, f64)>;
46
47#[derive(Clone)]
55pub struct XYChart {
56 pub id: Option<String>, pub x_range: ScaleRange, pub y_range: ScaleRange, pub plot_width: u32, pub plot_height: u32, pub to_plot: CoordinateTransform,
65
66 pub to_world: CoordinateTransform,
68
69 pub view_parameters: ViewParameters,
71
72 pub layers: HashMap<&'static str, Layer>,
74
75 pub clip_paths: Vec<ClipPath>,
77
78 pub margins: [i32; 4],
80}
81
82impl XYChart {
83 pub const ANNOTATE_SEP: i32 = 2;
85
86 pub const LABEL_HEIGHT: i32 = 16;
88
89 pub const DESCRIPTION_HEIGHT: i32 = 20;
91
92 pub const DESCRIPTION_SEP: i32 = 20;
94
95 pub const DESCRIPTION_OFFSET: i32 = Self::LABEL_HEIGHT + Self::DESCRIPTION_SEP;
97
98 pub fn new(
104 plot_width_and_height: (u32, u32),
105 ranges: (impl RangeBounds<f64>, impl RangeBounds<f64>),
106 ) -> XYChart {
107 let (plot_width, plot_height) = plot_width_and_height;
108 let (x_range, y_range) = (ScaleRange::new(ranges.0), ScaleRange::new(ranges.1));
109
110 let to_plot = Rc::new(move |xy: (f64, f64)| {
112 let w = plot_width as f64;
113 let h = plot_height as f64;
114 world_to_plot_coordinates(xy.0, xy.1, &x_range, &y_range, w, h)
115 });
116
117 let to_world = Rc::new(move |xy: (f64, f64)| {
118 let w = plot_width as f64;
119 let h = plot_height as f64;
120 plot_to_world_coordinates(xy.0, xy.1, &x_range, &y_range, w, h)
121 });
122
123 let path = to_path(
125 [
126 (0f64, 0f64),
127 (plot_width as f64, 0f64),
128 (plot_width as f64, plot_height as f64),
129 (0f64, plot_height as f64),
130 ],
131 true,
132 );
133
134 let mut layers = HashMap::new();
136
137 let mut axes_layer = Layer::new();
140 axes_layer.assign("class", "axes");
141
142 let plot_area = path.clone().set("class", "plot-area");
143 axes_layer.append(plot_area);
144 layers.insert("axes", axes_layer);
145
146 let mut clip_paths = Vec::new();
148
149 let clip_id = new_id();
151 clip_paths.push(
152 ClipPath::new()
153 .set("id", format!("clip-{clip_id}"))
154 .add(path.clone()),
155 );
156
157 let mut plot_layer = Layer::new();
160 plot_layer.assign("clip-path", format!("url(#clip-{clip_id})"));
161 layers.insert("plot", plot_layer);
164
165 let mut annotations_layer = Layer::new();
166 annotations_layer.assign("class", "annotations");
167 layers.insert("annotations", annotations_layer);
168
169 let view_box = ViewParameters::new(0, 0, plot_width, plot_height, plot_width, plot_height);
170 XYChart {
171 id: None,
172 view_parameters: view_box,
173 plot_height,
174 plot_width,
175 x_range,
176 y_range,
177 layers,
178 clip_paths,
179 margins: [0i32; 4], to_plot,
181 to_world,
182 }
183 }
184
185 pub fn set_id(mut self, id: impl Into<String>) -> Self {
186 self.id = Some(id.into());
187 self
188 }
189
190 pub fn ticks(
204 mut self,
205 x_step: f64,
206 y_step: f64,
207 length: i32,
208 style_attr: Option<StyleAttr>,
209 ) -> Self {
210 let mut data = Data::new();
211 let to_plot = self.to_plot.clone();
212 for x in self.x_range.iter_with_step(x_step) {
213 let (px, py) = to_plot((x, self.y_range.start));
214 data = data.move_to((px, py)).line_to((px, py + length as f64));
215
216 let (px, py) = to_plot((x, self.y_range.end));
217 data = data.move_to((px, py)).line_to((px, py - length as f64));
218 }
219 for y in self.y_range.iter_with_step(y_step) {
220 let (px, py) = to_plot((self.x_range.start, y));
221 data = data.move_to((px, py)).line_to((px - length as f64, py));
222
223 let (px, py) = to_plot((self.x_range.end, y));
224 data = data.move_to((px, py)).line_to((px + length as f64, py));
225 }
226 if length > 0 {
228 self.margins.iter_mut().for_each(|v| {
229 if *v < length {
230 *v = length;
231 }
232 });
233 self.update_view();
234 }
235 self.draw_data("axes", data, style_attr)
236 }
237
238 pub fn x_labels(mut self, step: f64, offset: usize, style_attr: Option<StyleAttr>) -> Self {
248 let range_with_step = ScaleRangeWithStep::new(self.x_range, step);
249 let y = self.y_range.start;
250 let to_plot = self.to_plot.clone();
251 let mut x_labels = Group::new();
252 for x in range_with_step.iter() {
253 let display_value = format!("{}", ScaleValue(x, step));
254 let (px, py) = to_plot((x, y));
255 let txt = Text::new(display_value)
256 .set("x", px)
257 .set("y", py + offset as f64)
258 .set("text-anchor", "middle")
259 .set("dominant-baseline", "text-before-edge");
260 x_labels.append(txt);
261 }
262 style_attr.unwrap_or_default().assign(&mut x_labels);
263 self.layers.get_mut("axes").unwrap().append(x_labels);
264 self.margins[2] = self.margins[2].max(Self::LABEL_HEIGHT + offset as i32);
265 self.update_view();
266 self
267 }
268
269 pub fn y_labels(mut self, step: f64, offset: usize, style_attr: Option<StyleAttr>) -> Self {
278 let range_with_step = ScaleRangeWithStep::new(self.y_range, step);
279 let x = self.x_range.start;
280 let to_plot = self.to_plot.clone();
281 let mut y_labels = Group::new();
282 for y in range_with_step.iter() {
283 let display_value = format!("{}", ScaleValue(y, step));
284 let (px, py) = to_plot((x, y));
285 let txt = Text::new(display_value)
286 .set("x", px - offset as f64)
287 .set("y", py)
288 .set("text-anchor", "middle")
289 .set("dominant-baseline", "text-after-edge")
290 .set(
291 "transform",
292 format!("rotate(-90, {}, {})", px - offset as f64, py),
293 );
294 y_labels.append(txt);
295 }
296 style_attr.unwrap_or_default().assign(&mut y_labels);
297 self.layers.get_mut("axes").unwrap().append(y_labels);
298 self.margins[3] = self.margins[3].max(Self::LABEL_HEIGHT + offset as i32);
299 self.update_view();
300 self
301 }
302
303 pub fn x_axis_description(mut self, description: &str, style_attr: Option<StyleAttr>) -> Self {
305 let x_middle = (self.x_range.start + self.x_range.end) / 2.0;
306 let y = self.y_range.start;
307 let (px, py) = (self.to_plot)((x_middle, y));
308 let mut text = Text::new(description)
309 .set("x", px)
310 .set("y", py + Self::DESCRIPTION_OFFSET as f64)
311 .set("text-anchor", "middle")
312 .set("dominant-baseline", "text-before-edge");
313 style_attr.unwrap_or_default().assign(&mut text);
314 self.layers.get_mut("axes").unwrap().append(text);
315 self.margins[0] = self.margins[0].max(Self::DESCRIPTION_HEIGHT + Self::DESCRIPTION_OFFSET);
316 self.update_view();
317 self
318 }
319
320 pub fn y_axis_description(mut self, description: &str, style_attr: Option<StyleAttr>) -> Self {
323 let y_middle = (self.y_range.start + self.y_range.end) / 2.0;
324 let x = self.x_range.start;
325 let (px, py) = (self.to_plot)((x, y_middle));
326 let mut text = Text::new(description)
327 .set("x", px - Self::DESCRIPTION_OFFSET as f64)
328 .set("y", py)
329 .set("text-anchor", "middle")
330 .set("dominant-baseline", "text-after-edge")
331 .set(
332 "transform",
333 format!(
334 "rotate(-90, {}, {})",
335 px - Self::DESCRIPTION_OFFSET as f64,
336 py
337 ),
338 );
339 style_attr.unwrap_or_default().assign(&mut text);
340 self.layers.get_mut("axes").unwrap().append(text);
341 self.margins[3] = self.margins[3].max(Self::DESCRIPTION_HEIGHT + Self::DESCRIPTION_OFFSET);
342 self.update_view();
343 self
344 }
345
346 pub fn plot_grid(self, x_step: f64, y_step: f64, style_attr: Option<StyleAttr>) -> Self {
350 let mut data = Data::new();
351 let on_canvas = self.to_plot.clone();
352 for x in self.x_range.iter_with_step(x_step) {
353 data = data
354 .move_to(on_canvas((x, self.y_range.start)))
355 .line_to(on_canvas((x, self.y_range.end)));
356 }
357 for y in self.y_range.iter_with_step(y_step) {
358 data = data
359 .move_to(on_canvas((self.x_range.start, y)))
360 .line_to(on_canvas((self.x_range.end, y)));
361 }
362 self.draw_data("plot", data, style_attr)
363 }
364
365 pub fn plot_image(mut self, image: impl Into<Image>, style_attr: Option<StyleAttr>) -> Self {
374 let mut image: Image = image.into();
375 style_attr.unwrap_or_default().assign(&mut image);
376 self.layers.get_mut("plot").unwrap().append(image);
377 self
378 }
379
380 pub fn label_pin(
383 mut self,
384 cxy: (f64, f64),
385 r: f64,
386 angle_and_length: (i32, i32),
387 text: impl AsRef<str>,
388 style_attr: Option<StyleAttr>,
389 ) -> Self {
390 let (angle, len) = angle_and_length;
391 let angle = 360 - ((angle + 360) % 360);
392 let (cx, cy) = cxy;
393 let (cx, cy) = (self.to_plot)((cx, cy));
394 let dx = len as f64 * (angle as f64).to_radians().cos();
395 let dy = len as f64 * (angle as f64).to_radians().sin();
396 let circle = svg::node::element::Circle::new()
397 .set("cx", cx)
398 .set("cy", cy)
399 .set("r", r);
400 let line = Line::new()
401 .set("x1", cx)
402 .set("y1", cy)
403 .set("x2", cx + dx)
404 .set("y2", cy + dy);
405
406 let dxt = (len + Self::ANNOTATE_SEP) as f64 * (angle as f64).to_radians().cos();
407 let dyt = (len + Self::ANNOTATE_SEP) as f64 * (angle as f64).to_radians().sin();
408 let mut text = Text::new(text.as_ref())
409 .set("x", cx + dxt)
410 .set("y", cy + dyt);
411
412 match angle {
413 0..55 | 305..=360 => {
414 text.assign("text-anchor", "start");
416 text.assign("dominant-baseline", "middle");
417 }
418 55..125 => {
419 text.assign("text-anchor", "middle");
421 text.assign("dominant-baseline", "text-before-edge");
422 }
423 125..235 => {
424 text.assign("text-anchor", "end");
426 text.assign("dominant-baseline", "middle");
427 }
428 _ => {
429 text.assign("text-anchor", "middle");
431 text.assign("dominant-baseline", "text-after-edge");
432 }
433 }
434
435 let mut group = Group::new().add(circle).add(line).add(text);
436 style_attr.unwrap_or_default().assign(&mut group);
437 self.layers.get_mut("annotations").unwrap().append(group);
438 self
439 }
440
441 pub fn draw_path(mut self, layer: &str, mut path: Path, style_attr: Option<StyleAttr>) -> Self {
444 if let Some(layer) = self.layers.get_mut(layer) {
445 style_attr.unwrap_or_default().assign(&mut path);
446 layer.append(path);
447 } else {
448 panic!("unknown layer");
449 }
450 self
451 }
452
453 pub fn draw_data(self, layer: &str, data: Data, style_attr: Option<StyleAttr>) -> Self {
456 let path = Path::new().set("d", data);
457 self.draw_path(layer, path, style_attr)
458 }
459
460 pub fn plot_poly_line(
463 self,
464 data: impl IntoIterator<Item = (f64, f64)>,
465 style_attr: Option<StyleAttr>,
466 ) -> Self {
467 let on_canvas = self.to_plot.clone();
468 let iter_canvas = data.into_iter().map(|xy| (on_canvas)(xy));
469 self.draw_path("plot", to_path(iter_canvas, false), style_attr)
470 }
471
472 pub fn plot_shape(
475 self,
476 data: impl IntoIterator<Item = (f64, f64)>,
477 style_attr: Option<StyleAttr>,
478 ) -> Self {
479 let on_canvas = self.to_plot.clone();
480 let iter_canvas = data.into_iter().map(|xy| (on_canvas)(xy));
481 self.draw_path("plot", to_path(iter_canvas, true), style_attr)
482 }
483
484 pub fn update_view(&mut self) {
486 let vx = -(self.margins[3]);
487 let vy = -(self.margins[0]);
488 let vw = self.plot_width + self.margins[1] as u32 + 2 * self.margins[3] as u32;
490 let vh = self.plot_height + 2 * self.margins[0] as u32 + self.margins[2] as u32;
492
493 self.view_parameters.set_view_box(vx, vy, vw, vh);
494 self.set_width(vw);
495 self.set_height(vh);
496 }
497}
498
499pub(super) fn to_path(data: impl IntoIterator<Item = (f64, f64)>, close: bool) -> Path {
500 let mut path_data = Data::new();
501 for xy in data {
502 if path_data.is_empty() {
503 path_data = path_data.move_to(xy);
504 } else {
505 path_data = path_data.line_to(xy);
506 }
507 }
508 if close {
509 path_data = path_data.close();
510 }
511 Path::new()
512 .set("d", path_data.clone())
514}
515
516fn world_to_plot_coordinates(
524 x: f64,
525 y: f64,
526 x_range: &ScaleRange,
527 y_range: &ScaleRange,
528 w: f64,
529 h: f64,
530) -> (f64, f64) {
531 let x_canvas = x_range.scale(x) * w;
532 let y_canvas = h - (y_range.scale(y) * h);
533 (
534 round_to_default_precision(x_canvas),
535 round_to_default_precision(y_canvas),
536 )
537}
538
539pub fn plot_to_world_coordinates(
547 x: f64,
548 y: f64,
549 x_range: &ScaleRange,
550 y_range: &ScaleRange,
551 w: f64,
552 h: f64,
553) -> (f64, f64) {
554 let x_world = x_range.unscale(x / w);
555 let y_world = y_range.unscale_descent(y / h);
556 (x_world, y_world)
557}
558#[test]
559fn test_plot_to_world_coordinates() {
560 use approx::assert_abs_diff_eq;
561 let x_range = ScaleRange::new(0.0..=1.0);
562 let y_range = ScaleRange::new(0.0..=1.0);
563 let (x, y) = plot_to_world_coordinates(100.0, 200.0, &x_range, &y_range, 400.0, 300.0);
564 assert_abs_diff_eq!(x, 0.25, epsilon = 1e-10);
565 assert_abs_diff_eq!(y, 1.0 / 3.0, epsilon = 1e-10);
566}
567
568impl From<XYChart> for SVG {
569 fn from(chart: XYChart) -> Self {
570 chart.render()
571 }
572}
573
574impl Rendable for XYChart {
575 fn view_parameters(&self) -> ViewParameters {
577 self.view_parameters.clone()
578 }
579
580 fn set_view_parameters(&mut self, view_box: ViewParameters) {
581 self.view_parameters = view_box;
582 }
583
584 fn render(&self) -> SVG {
585 let mut defs = Definitions::new();
586 for clip in self.clip_paths.iter() {
587 defs.append(clip.clone());
588 }
589
590 let mut svg = SVG::new();
591 svg = svg
592 .set("xmlns", "http://www.w3.org/2000/svg")
593 .set("xmlns:xlink", "http://www.w3.org/1999/xlink")
594 .set("version", "1.1")
595 .set("class", "chart"); if let Some(id) = &self.id {
597 svg = svg.set("id", id.as_str());
598 }
599
600 svg.set("width", self.width())
601 .set("height", self.height())
602 .set("viewBox", self.view_parameters().view_box_str())
603 .add(defs)
604 .add(self.layers.get("axes").unwrap().clone())
605 .add(self.layers.get("plot").unwrap().clone())
606 .add(self.layers.get("annotations").unwrap().clone())
607 }
608}