Skip to main content

euv_chart/series/
impl.rs

1use super::*;
2
3/// Inherent implementation of [`SeriesRenderer`].
4impl SeriesRenderer {
5    /// Returns a renderer.
6    pub fn new() -> Self {
7        Self {}
8    }
9
10    /// Draws every series of the chart.
11    ///
12    /// # Arguments
13    ///
14    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
15    /// - `&ChartOption` - Shared reference to a `ChartOption`.
16    /// - `&ChartLayout` - Shared reference to a `ChartLayout`.
17    pub fn draw(&mut self, list: &mut DisplayList, option: &ChartOption, layout: &ChartLayout) {
18        for (index, series) in option.series.iter().enumerate() {
19            if !series.show {
20                continue;
21            }
22            let color: Color = palette_color(option, index);
23            let series_layout: Option<&SeriesLayout> = layout
24                .series
25                .iter()
26                .find(|entry: &&SeriesLayout| entry.index == index);
27            let Some(series_layout) = series_layout else {
28                continue;
29            };
30            let z: i32 = Z_SERIES + index as i32 * 10;
31            match series.kind {
32                SeriesType::Line => {
33                    self.draw_line(list, series, series_layout, color, z);
34                }
35                SeriesType::ThemeRiver => {
36                    draw_theme_river(list, series, series_layout, color, z);
37                }
38                SeriesType::Bar => {
39                    self.draw_bar(list, series, series_layout, color, z);
40                }
41                SeriesType::PictorialBar => {
42                    draw_pictorial_bar(list, series, series_layout, color, z);
43                }
44                SeriesType::Pie => {
45                    self.draw_pie(list, series, series_layout, color, z);
46                }
47                SeriesType::Funnel => {
48                    draw_funnel(list, series, series_layout, color, z);
49                }
50                SeriesType::Radar => {
51                    draw_radar(list, option, series, series_layout, color, z);
52                }
53                SeriesType::Gauge => {
54                    draw_gauge(list, series, series_layout, color, z);
55                }
56                SeriesType::Scatter | SeriesType::EffectScatter => {
57                    self.draw_scatter(list, series, series_layout, color, z);
58                }
59                SeriesType::Candlestick => {
60                    self.draw_candlestick(list, series, series_layout, color, z);
61                }
62                SeriesType::Boxplot => {
63                    self.draw_boxplot(list, series, series_layout, color, z);
64                }
65                SeriesType::Heatmap => {
66                    self.draw_heatmap(list, series, series_layout, color, z);
67                }
68                SeriesType::Graph => {
69                    draw_graph(list, series, series_layout, color, z);
70                }
71                SeriesType::Treemap => {
72                    draw_treemap(list, series, series_layout, color, z);
73                }
74                SeriesType::Sunburst => {
75                    draw_sunburst(list, series, series_layout, color, z);
76                }
77                SeriesType::Sankey => {
78                    draw_sankey(list, series, series_layout, color, z);
79                }
80                SeriesType::Parallel => {
81                    draw_parallel(list, series, series_layout, color, z);
82                }
83                SeriesType::Lines => {
84                    draw_lines(list, series, series_layout, color, z);
85                }
86                SeriesType::Map => {
87                    draw_map(list, option, series, series_layout, layout, color, z);
88                }
89                SeriesType::Custom => {
90                    draw_custom(list, series, series_layout, layout, color, z);
91                }
92            }
93        }
94    }
95
96    /// Draws the labels of a series' data points.
97    ///
98    /// A label shows the point's name when it has one, and its value
99    /// otherwise — which is what a reader expects from a chart whose
100    /// points are not named.
101    ///
102    /// # Arguments
103    ///
104    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
105    /// - `&SeriesOption` - Shared reference to a `SeriesOption`.
106    /// - `&SeriesLayout` - Shared reference to a `SeriesLayout`.
107    /// - `Color` - A `Color` parameter.
108    /// - `i32` - A 32-bit signed integer (`i32`).
109    fn draw_labels(
110        &mut self,
111        list: &mut DisplayList,
112        series: &SeriesOption,
113        layout: &SeriesLayout,
114        color: Color,
115        z: i32,
116    ) {
117        let Some(label) = &series.label else {
118            return;
119        };
120        if !label.show {
121            return;
122        }
123        let style: TextStyle = series_label_style(series, color);
124        let distance: f64 = label.distance.unwrap_or(0.0);
125        for point in &layout.points {
126            let text: String = if point.name.is_empty() {
127                format_number(point.value)
128            } else {
129                point.name.clone()
130            };
131            if text.is_empty() {
132                continue;
133            }
134            let mut label_style: TextStyle = style.clone();
135            if let Some(align) = label.align {
136                label_style.align = align;
137            }
138            if let Some(vertical_align) = label.vertical_align {
139                label_style.vertical_align = vertical_align;
140            }
141            list.text(text, point.x, point.y - distance, label_style, z);
142        }
143    }
144
145    /// Draws a line series, with its area fill and its symbols.
146    ///
147    /// # Arguments
148    ///
149    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
150    /// - `&SeriesOption` - Shared reference to a `SeriesOption`.
151    /// - `&SeriesLayout` - Shared reference to a `SeriesLayout`.
152    /// - `Color` - A `Color` parameter.
153    /// - `i32` - A 32-bit signed integer (`i32`).
154    fn draw_line(
155        &mut self,
156        list: &mut DisplayList,
157        series: &SeriesOption,
158        layout: &SeriesLayout,
159        color: Color,
160        z: i32,
161    ) {
162        if layout.points.is_empty() {
163            return;
164        }
165        let points: Vec<Point> = layout
166            .points
167            .iter()
168            .map(|point: &SeriesPoint| Point::new(point.x as f32, point.y as f32))
169            .collect();
170
171        // The area fill comes first: it sits under the line.
172        if let Some(line_options) = &series.line
173            && let Some(area_style) = &line_options.area_style
174        {
175            let mut area: SeriesOption = series.clone();
176            area.item_style = Some(area_style.clone());
177            let area_color: Color = area_style
178                .color
179                .as_ref()
180                .and_then(|value: &String| Color::parse(value))
181                .unwrap_or(color);
182            let fill: Fill = series_fill(&area, area_color);
183            let baseline: f64 = baseline_of(&layout.points);
184            list.strip(points.clone(), baseline as f32, fill, 1.0, z);
185        }
186
187        let path: Path = match series
188            .line
189            .as_ref()
190            .and_then(|line: &LineOptions| line.smooth)
191        {
192            Some(LineSmooth::Smooth) => smooth_path(&points),
193            Some(LineSmooth::Step) => step_path(&points, StepPosition::Middle),
194            _ => polyline_path(&points),
195        };
196        let stroke: Stroke = series_stroke(series, color);
197        let style: LineStyle = series_line_style(series, color);
198        list.stroke(path, stroke, style, 1.0, z + 1);
199
200        // The symbols.
201        let show_symbol: bool = series
202            .line
203            .as_ref()
204            .and_then(|line: &LineOptions| line.show_symbol)
205            .unwrap_or(true)
206            && series
207                .label
208                .as_ref()
209                .map(|label: &LabelOption| label.show)
210                .unwrap_or(false);
211        let symbol: SymbolType = series
212            .line
213            .as_ref()
214            .and_then(|line: &LineOptions| line.symbol)
215            .unwrap_or(SymbolType::Circle);
216        let size: f64 = series
217            .line
218            .as_ref()
219            .and_then(|line: &LineOptions| line.symbol_size)
220            .map(|size: SymbolSize| match size {
221                SymbolSize::Uniform(value) => value,
222                SymbolSize::NonUniform(width, _) => width,
223            })
224            .unwrap_or(DEFAULT_SYMBOL_SIZE);
225        // The symbols are only drawn when the points are far enough
226        // apart for them to be told apart.
227        let dense: bool = average_spacing(&layout.points) < size;
228        let show_all: bool = series
229            .line
230            .as_ref()
231            .and_then(|line: &LineOptions| line.show_all_symbol)
232            .unwrap_or(false);
233        if show_symbol && (!dense || show_all) {
234            let fill: Fill = series_fill(series, color);
235            for point in &layout.points {
236                let path: Path = symbol_path(symbol, point.x, point.y, size);
237                list.fill(path, fill.clone(), 1.0, z + 2);
238            }
239        }
240
241        self.draw_labels(list, series, layout, color, z + 3);
242    }
243
244    /// Draws a bar series.
245    ///
246    /// # Arguments
247    ///
248    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
249    /// - `&SeriesOption` - Shared reference to a `SeriesOption`.
250    /// - `&SeriesLayout` - Shared reference to a `SeriesLayout`.
251    /// - `Color` - A `Color` parameter.
252    /// - `i32` - A 32-bit signed integer (`i32`).
253    fn draw_bar(
254        &mut self,
255        list: &mut DisplayList,
256        series: &SeriesOption,
257        layout: &SeriesLayout,
258        color: Color,
259        z: i32,
260    ) {
261        if layout.points.is_empty() {
262            return;
263        }
264        let fill: Fill = series_fill(series, color);
265        // A bar is bordered by its *item* style, and not by its line
266        // style: ECharts draws no border at all unless the option asks
267        // for one. A border on every one of a million bars would double
268        // the work of the frame for a line nobody asked for.
269        let item_style: ItemStyleOption = series.item_style.clone().unwrap_or_default();
270        let border_width: f64 = item_style.border_width.unwrap_or(0.0);
271        let border_color: Color = item_style
272            .border_color
273            .as_deref()
274            .and_then(Color::parse)
275            .unwrap_or(color);
276
277        let bar_options: BarOptions = series.bar.clone().unwrap_or_default();
278        let horizontal: bool = bar_options.horizontal;
279        let category_gap: f64 = bar_options
280            .bar_category_gap
281            .as_deref()
282            .and_then(parse_percent)
283            .unwrap_or(DEFAULT_BAR_CATEGORY_GAP);
284        let group_gap: f64 = bar_options
285            .bar_gap
286            .as_deref()
287            .and_then(parse_percent)
288            .unwrap_or(DEFAULT_BAR_GAP);
289
290        // The width of one bar: the band, minus the gaps.
291        let group_size: f64 = layout.stack_size.max(1) as f64;
292        let available: f64 = (layout.band_width * (1.0 - category_gap)).max(1.0);
293        let bar_width: f64 = match bar_options.bar_width {
294            Some(width) => width,
295            None => (available / group_size * (1.0 - group_gap)).max(1.0),
296        };
297        let clamped: f64 = bar_options.bar_max_width.unwrap_or(f64::INFINITY);
298        let minimum: f64 = bar_options.bar_min_width.unwrap_or(0.0);
299        let bar_width: f64 = bar_width.clamp(minimum, clamped);
300
301        // The baseline the bars grow from.
302        let baseline: f64 = baseline_of(&layout.points);
303
304        for (index, point) in layout.points.iter().enumerate() {
305            let offset: f64 = (index % group_size as usize) as f64
306                * (bar_width / (1.0 - group_gap).max(f64::EPSILON));
307            let center: f64 = point.x;
308            let value: f64 = point.value;
309            let path: Path = if horizontal {
310                let left: f64 = baseline.min(point.y);
311                let right: f64 = baseline.max(point.y);
312                Path::builder()
313                    .rect(
314                        (left + offset) as f32,
315                        (center - bar_width * 0.5) as f32,
316                        (right - left).max(0.0) as f32,
317                        bar_width as f32,
318                    )
319                    .build()
320            } else {
321                let top: f64 = baseline.min(point.y);
322                let height: f64 = (baseline - point.y).abs();
323                Path::builder()
324                    .rect(
325                        (center - bar_width * 0.5 + offset) as f32,
326                        top as f32,
327                        bar_width as f32,
328                        height.max(0.0) as f32,
329                    )
330                    .build()
331            };
332            let _: f64 = value;
333            list.fill(path.clone(), fill.clone(), 1.0, z);
334            if border_width > 0.0 {
335                let mut style: LineStyle = LineStyle::new();
336                style.width = border_width;
337                style.color = border_color;
338                list.stroke(path, Stroke::Solid(border_color), style, 1.0, z + 1);
339            }
340        }
341    }
342
343    /// Draws a pie or a funnel series.
344    ///
345    /// # Arguments
346    ///
347    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
348    /// - `&SeriesOption` - Shared reference to a `SeriesOption`.
349    /// - `&SeriesLayout` - Shared reference to a `SeriesLayout`.
350    /// - `Color` - A `Color` parameter.
351    /// - `i32` - A 32-bit signed integer (`i32`).
352    fn draw_pie(
353        &mut self,
354        list: &mut DisplayList,
355        series: &SeriesOption,
356        layout: &SeriesLayout,
357        color: Color,
358        z: i32,
359    ) {
360        if layout.points.is_empty() {
361            return;
362        }
363        let fill: Fill = series_fill(series, color);
364        let total: f64 = layout
365            .points
366            .iter()
367            .map(|point: &SeriesPoint| point.value.max(0.0))
368            .sum();
369        if total <= 0.0 {
370            return;
371        }
372        let (center_x, center_y, radius) = pie_geometry(series, layout);
373        let inner_radius: f64 = series
374            .pie
375            .as_ref()
376            .and_then(|pie: &PieOptions| pie.inner_radius.as_deref())
377            .and_then(parse_percent)
378            .map(|value: f64| value * radius)
379            .unwrap_or(0.0);
380        let start_angle: f64 = series
381            .pie
382            .as_ref()
383            .and_then(|pie: &PieOptions| pie.start_angle)
384            .unwrap_or(90.0)
385            .to_radians();
386        let mut angle: f64 = start_angle;
387        for (index, point) in layout.points.iter().enumerate() {
388            let share: f64 = point.value.max(0.0) / total;
389            let sweep: f64 = share * core::f64::consts::TAU;
390            let path: Path = sector_path(
391                center_x,
392                center_y,
393                radius,
394                inner_radius,
395                angle,
396                angle + sweep,
397            );
398            list.fill(path, fill.clone(), 1.0, z + index as i32);
399            angle += sweep;
400        }
401    }
402
403    /// Draws a scatter series.
404    ///
405    /// # Arguments
406    ///
407    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
408    /// - `&SeriesOption` - Shared reference to a `SeriesOption`.
409    /// - `&SeriesLayout` - Shared reference to a `SeriesLayout`.
410    /// - `Color` - A `Color` parameter.
411    /// - `i32` - A 32-bit signed integer (`i32`).
412    fn draw_scatter(
413        &mut self,
414        list: &mut DisplayList,
415        series: &SeriesOption,
416        layout: &SeriesLayout,
417        color: Color,
418        z: i32,
419    ) {
420        if layout.points.is_empty() {
421            return;
422        }
423        let fill: Fill = series_fill(series, color);
424        let symbol: SymbolType = series
425            .scatter
426            .as_ref()
427            .and_then(|scatter: &ScatterOptions| scatter.symbol)
428            .unwrap_or(SymbolType::Circle);
429        let size: f64 = series
430            .scatter
431            .as_ref()
432            .and_then(|scatter: &ScatterOptions| scatter.symbol_size)
433            .map(|size: SymbolSize| match size {
434                SymbolSize::Uniform(value) => value,
435                SymbolSize::NonUniform(width, _) => width,
436            })
437            .unwrap_or(DEFAULT_SYMBOL_SIZE);
438        // The symbols are dropped when the points are too close to be
439        // told apart; a dense scatter is better read as a cloud.
440        let dense: bool = average_spacing(&layout.points) < size;
441        if dense {
442            return;
443        }
444        for point in &layout.points {
445            let path: Path = symbol_path(symbol, point.x, point.y, size);
446            list.fill(path, fill.clone(), 1.0, z);
447        }
448        self.draw_labels(list, series, layout, color, z + 1);
449    }
450
451    /// Draws a candlestick series.
452    ///
453    /// # Arguments
454    ///
455    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
456    /// - `&SeriesOption` - Shared reference to a `SeriesOption`.
457    /// - `&SeriesLayout` - Shared reference to a `SeriesLayout`.
458    /// - `Color` - A `Color` parameter.
459    /// - `i32` - A 32-bit signed integer (`i32`).
460    fn draw_candlestick(
461        &mut self,
462        list: &mut DisplayList,
463        series: &SeriesOption,
464        layout: &SeriesLayout,
465        color: Color,
466        z: i32,
467    ) {
468        if layout.points.is_empty() {
469            return;
470        }
471        let rising: Color = series
472            .candlestick
473            .as_ref()
474            .and_then(|candle: &CandlestickOptions| candle.color.as_deref())
475            .and_then(Color::parse)
476            .unwrap_or(color);
477        let falling: Color = series
478            .candlestick
479            .as_ref()
480            .and_then(|candle: &CandlestickOptions| candle.color0.as_deref())
481            .and_then(Color::parse)
482            .unwrap_or(Color::rgb(30, 130, 220));
483        let width: f64 = series
484            .candlestick
485            .as_ref()
486            .and_then(|candle: &CandlestickOptions| candle.bar_width)
487            .unwrap_or(DEFAULT_BAR_WIDTH * 0.5);
488        let baseline: f64 = baseline_of(&layout.points);
489
490        for point in &layout.points {
491            let body: f64 = series
492                .candlestick
493                .as_ref()
494                .and_then(|candle: &CandlestickOptions| candle.bar_max_width)
495                .unwrap_or(width);
496            let fill: Color = if point.value >= 0.0 { rising } else { falling };
497            let top: f64 = baseline.min(point.y);
498            let height: f64 = (baseline - point.y).abs();
499            let body_path: Path = Path::builder()
500                .rect(
501                    (point.x - body * 0.5) as f32,
502                    top as f32,
503                    body as f32,
504                    height.max(1.0) as f32,
505                )
506                .build();
507            list.fill(body_path, Fill::Solid(fill), 1.0, z);
508        }
509    }
510
511    /// Draws a boxplot series.
512    ///
513    /// # Arguments
514    ///
515    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
516    /// - `&SeriesOption` - Shared reference to a `SeriesOption`.
517    /// - `&SeriesLayout` - Shared reference to a `SeriesLayout`.
518    /// - `Color` - A `Color` parameter.
519    /// - `i32` - A 32-bit signed integer (`i32`).
520    fn draw_boxplot(
521        &mut self,
522        list: &mut DisplayList,
523        series: &SeriesOption,
524        layout: &SeriesLayout,
525        color: Color,
526        z: i32,
527    ) {
528        if layout.points.is_empty() {
529            return;
530        }
531        let fill: Fill = series_fill(series, color);
532        let width: f64 = series
533            .boxplot
534            .as_ref()
535            .and_then(|boxplot: &BoxplotOptions| boxplot.box_width)
536            .unwrap_or(DEFAULT_BAR_WIDTH);
537        for point in &layout.points {
538            let path: Path = Path::builder()
539                .rect(
540                    (point.x - width * 0.5) as f32,
541                    (point.y - width * 0.5) as f32,
542                    width as f32,
543                    width as f32,
544                )
545                .build();
546            list.fill(path, fill.clone(), 1.0, z);
547        }
548    }
549
550    /// Draws a heatmap series.
551    ///
552    /// # Arguments
553    ///
554    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
555    /// - `&SeriesOption` - Shared reference to a `SeriesOption`.
556    /// - `&SeriesLayout` - Shared reference to a `SeriesLayout`.
557    /// - `Color` - A `Color` parameter.
558    /// - `i32` - A 32-bit signed integer (`i32`).
559    fn draw_heatmap(
560        &mut self,
561        list: &mut DisplayList,
562        series: &SeriesOption,
563        layout: &SeriesLayout,
564        color: Color,
565        z: i32,
566    ) {
567        if layout.points.is_empty() {
568            return;
569        }
570        let fill: Fill = series_fill(series, color);
571        let size: f64 = series
572            .heatmap
573            .as_ref()
574            .and_then(|heatmap: &HeatmapOptions| heatmap.min_size)
575            .unwrap_or(1.0);
576        for point in &layout.points {
577            let path: Path = Path::builder()
578                .rect(
579                    (point.x - size * 0.5) as f32,
580                    (point.y - size * 0.5) as f32,
581                    size as f32,
582                    size as f32,
583                )
584                .build();
585            list.fill(path, fill.clone(), 1.0, z);
586        }
587    }
588}
589
590/// Default construction for [`SeriesRenderer`].
591impl Default for SeriesRenderer {
592    /// Constructs a default [`SeriesRenderer`] value.
593    fn default() -> Self {
594        Self::new()
595    }
596}
597
598/// Inherent implementation of [`RadarGeometry`].
599impl RadarGeometry {
600    /// Returns the position of a point on the radar.
601    ///
602    /// `index` is the index of the side, `ratio` is how far along that
603    /// side the point is, from `0.0` at the centre to `1.0` at the edge.
604    ///
605    /// # Arguments
606    ///
607    /// - `usize` - A non-negative integer (`usize`).
608    /// - `f64` - A 64-bit float (`f64`).
609    ///
610    /// # Returns
611    ///
612    /// - `(f64, f64)` - A `(f64, f64)` value.
613    pub fn point(&self, index: usize, ratio: f64) -> (f64, f64) {
614        let angle: f64 = self.get_start_angle()
615            + index as f64 / self.get_sides().max(1) as f64 * core::f64::consts::TAU;
616        (
617            self.get_center_x() + self.get_radius() * ratio * angle.cos(),
618            self.get_center_y() + self.get_radius() * ratio * angle.sin(),
619        )
620    }
621}