Skip to main content

euv_chart/chart/
impl.rs

1use super::*;
2
3/// Inherent implementation of [`Chart`].
4impl Chart {
5    /// Creates a chart on the given canvas.
6    ///
7    /// The backend is chosen automatically: WebGPU when the host has it,
8    /// WebGL2 when it does not, and Canvas2D everywhere else. The chart
9    /// looks the same whichever one it gets.
10    ///
11    /// Returns `None` when the host has no DOM, or when the canvas
12    /// cannot give out a rendering context at all.
13    ///
14    /// # Arguments
15    ///
16    /// - `HtmlCanvasElement` - The `HtmlCanvasElement` value.
17    ///
18    /// # Returns
19    ///
20    /// - `Option<Self>` - The `Option<Self>` value.
21    pub async fn create(canvas: HtmlCanvasElement) -> Option<Self> {
22        Self::create_with_backend(canvas, BackendKind::WebGpu).await
23    }
24
25    /// Creates a chart on the given canvas, preferring a particular
26    /// backend.
27    ///
28    /// The preference is honoured when the host can provide it; the
29    /// factory falls back to the next backend down when it cannot.
30    ///
31    /// # Arguments
32    ///
33    /// - `HtmlCanvasElement` - The `HtmlCanvasElement` value.
34    /// - `BackendKind` - The `BackendKind` value.
35    ///
36    /// # Returns
37    ///
38    /// - `Option<Self>` - The `Option<Self>` value.
39    pub async fn create_with_backend(
40        canvas: HtmlCanvasElement,
41        preferred: BackendKind,
42    ) -> Option<Self> {
43        let backend: Box<dyn ChartBackend> = BackendFactory::create(&canvas, preferred).await?;
44        let measurer: TextMeasurer = TextMeasurer::new()?;
45        let atlas: GlyphAtlas = GlyphAtlas::new(DEFAULT_ATLAS_SIZE, DEFAULT_ATLAS_SIZE)?;
46        let shaper: TextShaper = TextShaper::new(measurer, atlas);
47
48        let (width, height) = canvas_size(&canvas);
49        let mut chart: Chart = Self {
50            canvas,
51            option: ChartOption::new(),
52            backend,
53            shaper,
54            layout: ChartLayout::default(),
55            display_list: DisplayList::new(),
56            renderer: ChartRenderer::new(),
57            width,
58            height,
59            dpr: detect_dpr(),
60            layout_dirty: true,
61            display_dirty: true,
62            handlers: Vec::new(),
63            animator: Animator::new(),
64            tooltip: None,
65            maps: MapRegistry::new(),
66            commands: Signal::create(Vec::new()),
67        };
68        chart.resize(width, height);
69        Some(chart)
70    }
71
72    /// Sets the configuration of the chart.
73    ///
74    /// The chart is re-laid out and re-rendered. When the chart
75    /// animates, the new configuration is the target of the animation and
76    /// the chart moves towards it; call [`Chart::advance`] to move it
77    /// along.
78    ///
79    /// # Arguments
80    ///
81    /// - `ChartOption` - A `ChartOption` parameter.
82    ///
83    /// # Returns
84    ///
85    /// - `&mut Self` - A `mut Self` value.
86    pub fn set_option(&mut self, option: ChartOption) -> &mut Self {
87        self.get_mut_animator().begin(&option);
88        *self.get_mut_option() = option;
89        self.set_layout_dirty(true);
90        self.set_display_dirty(true);
91        self.render()
92    }
93
94    /// Advances the animation by the given number of milliseconds.
95    ///
96    /// Returns `true` when the animation is still running. The call
97    /// re-renders the chart, so an animation loop is a call to
98    /// `advance` once per frame.
99    ///
100    /// # Arguments
101    ///
102    /// - `f64` - A 64-bit float (`f64`).
103    ///
104    /// # Returns
105    ///
106    /// - `bool` - A boolean.
107    pub fn advance(&mut self, delta: f64) -> bool {
108        if !self.get_mut_animator().advance(delta) {
109            return false;
110        }
111        let mut option: ChartOption = self.get_option().clone();
112        self.get_mut_animator().apply(&mut option);
113        *self.get_mut_option() = option;
114        self.set_layout_dirty(true);
115        self.set_display_dirty(true);
116        self.render();
117        true
118    }
119
120    /// Returns the animator of the chart.
121    ///
122    /// # Returns
123    ///
124    /// - `&mut Animator` - A `mut Animator` value.
125    pub fn animator(&mut self) -> &mut Animator {
126        self.get_mut_animator()
127    }
128
129    /// Returns the configuration of the chart.
130    ///
131    /// # Returns
132    ///
133    /// - `&ChartOption` - A `ChartOption` value.
134    pub fn option(&self) -> &ChartOption {
135        self.get_option()
136    }
137
138    /// Returns the configuration of the chart, mutably.
139    ///
140    /// Changing the configuration marks the chart as needing a new
141    /// layout; call [`Chart::render`] to apply the change.
142    ///
143    /// # Returns
144    ///
145    /// - `&mut ChartOption` - A `mut ChartOption` value.
146    pub fn option_mut(&mut self) -> &mut ChartOption {
147        self.set_layout_dirty(true);
148        self.set_display_dirty(true);
149        self.get_mut_option()
150    }
151
152    /// Lays the chart out and draws it.
153    ///
154    /// The call is cheap when nothing changed: the layout and the
155    /// display list are only rebuilt when the option or the size
156    /// changed.
157    ///
158    /// # Returns
159    ///
160    /// - `&mut Self` - A `mut Self` value.
161    pub fn render(&mut self) -> &mut Self {
162        // The commands that arrived since the last frame are performed
163        // first, so that the frame that follows them draws what they
164        // asked for.
165        self.drain_commands();
166        if self.get_layout_dirty() {
167            let layout: ChartLayout = ChartLayout::compute_with_maps(
168                self.get_option(),
169                self.get_width(),
170                self.get_height(),
171                self.get_shaper().measurer(),
172                self.get_maps(),
173            );
174            self.set_layout(layout);
175            self.set_layout_dirty(false);
176            self.set_display_dirty(true);
177        }
178        if self.get_display_dirty() {
179            // The renderer, the display list and the option are taken out
180            // of the chart together: the renderer writes into the list
181            // while it reads the option.
182            let Self {
183                renderer,
184                display_list,
185                option,
186                layout,
187                tooltip,
188                ..
189            } = self;
190            renderer.build_with_tooltip(display_list, option, layout, tooltip.as_ref());
191            self.set_display_dirty(false);
192        }
193        // The backend takes the display list and the shaper at the same
194        // time, so the two are taken out of the chart together.
195        let Self {
196            backend,
197            display_list,
198            shaper,
199            ..
200        } = self;
201        backend.render(display_list, shaper);
202        self
203    }
204
205    /// Performs the commands that arrived since the last frame.
206    ///
207    /// The queue is emptied before the commands are performed, so that a
208    /// command that draws the chart does not run the queue twice.
209    fn drain_commands(&mut self) {
210        let waiting: bool = self
211            .get_commands()
212            .with(|queue: &Vec<ChartCommand>| !queue.is_empty());
213        if !waiting {
214            return;
215        }
216        let queue: Vec<ChartCommand> = self.get_commands().get();
217        self.get_commands().set(Vec::new());
218        for command in queue {
219            match command {
220                ChartCommand::SetOption(option) => {
221                    self.get_mut_animator().begin(&option);
222                    *self.get_mut_option() = *option;
223                    self.set_layout_dirty(true);
224                    self.set_display_dirty(true);
225                }
226                ChartCommand::Resize(width, height) => {
227                    self.set_width(width.max(1.0));
228                    self.set_height(height.max(1.0));
229                    self.set_dpr(detect_dpr());
230                    let dpr: f64 = self.get_dpr();
231                    let chart_width: f64 = self.get_width();
232                    let chart_height: f64 = self.get_height();
233                    self.get_mut_backend()
234                        .resize(chart_width, chart_height, dpr);
235                    self.set_layout_dirty(true);
236                    self.set_display_dirty(true);
237                }
238                ChartCommand::Action(action) => {
239                    self.dispatch_action(action);
240                }
241            }
242        }
243    }
244
245    /// Returns a handle to the chart.
246    ///
247    /// The handle is how an application that does not hold the chart
248    /// talks to it: the commands it sends travel through a signal, and
249    /// the chart performs them on its next frame.
250    ///
251    /// # Returns
252    ///
253    /// - `ChartHandle` - A `ChartHandle` value.
254    pub fn handle(&self) -> ChartHandle {
255        ChartHandle {
256            commands: *self.get_commands(),
257        }
258    }
259
260    /// Resizes the chart.
261    ///
262    /// The size is in CSS pixels; the chart multiplies it by the device
263    /// pixel ratio to get the size of the drawing buffer.
264    ///
265    /// # Arguments
266    ///
267    /// - `f64` - A 64-bit float (`f64`).
268    ///
269    /// # Returns
270    ///
271    /// - `&mut Self` - A `mut Self` value.
272    pub fn resize(&mut self, width: f64, height: f64) -> &mut Self {
273        self.set_width(width.max(1.0));
274        self.set_height(height.max(1.0));
275        self.set_dpr(detect_dpr());
276        let dpr: f64 = self.get_dpr();
277        let chart_width: f64 = self.get_width();
278        let chart_height: f64 = self.get_height();
279        self.get_mut_backend()
280            .resize(chart_width, chart_height, dpr);
281        self.set_layout_dirty(true);
282        self.set_display_dirty(true);
283        self.render()
284    }
285
286    /// Returns the kind of the backend the chart runs on.
287    ///
288    /// # Returns
289    ///
290    /// - `BackendKind` - A `BackendKind` value.
291    pub fn backend_kind(&self) -> BackendKind {
292        self.get_backend().kind()
293    }
294
295    /// Returns the layout of the chart.
296    ///
297    /// # Returns
298    ///
299    /// - `&ChartLayout` - A `ChartLayout` value.
300    pub fn layout(&self) -> &ChartLayout {
301        self.get_layout()
302    }
303
304    /// Returns the display list the chart draws.
305    ///
306    /// # Returns
307    ///
308    /// - `&DisplayList` - A `DisplayList` value.
309    pub fn display_list(&self) -> &DisplayList {
310        self.get_display_list()
311    }
312
313    /// Returns the canvas the chart draws into.
314    ///
315    /// # Returns
316    ///
317    /// - `&HtmlCanvasElement` - A `HtmlCanvasElement` value.
318    pub fn canvas(&self) -> &HtmlCanvasElement {
319        self.get_canvas()
320    }
321
322    /// Registers a handler for a kind of event.
323    ///
324    /// # Arguments
325    ///
326    /// - `ChartEventType` - A `ChartEventType` parameter.
327    /// - `EventHandler` - A `EventHandler` parameter.
328    ///
329    /// # Returns
330    ///
331    /// - `&mut Self` - A `mut Self` value.
332    pub fn on(&mut self, event_type: ChartEventType, handler: EventHandler) -> &mut Self {
333        self.get_mut_handlers().push(EventRegistration {
334            event_type,
335            handler,
336        });
337        self
338    }
339
340    /// Removes every handler registered for a kind of event.
341    ///
342    /// # Arguments
343    ///
344    /// - `ChartEventType` - A `ChartEventType` parameter.
345    ///
346    /// # Returns
347    ///
348    /// - `&mut Self` - A `mut Self` value.
349    pub fn off(&mut self, event_type: ChartEventType) -> &mut Self {
350        self.get_mut_handlers()
351            .retain(|registration: &EventRegistration| registration.event_type != event_type);
352        self
353    }
354
355    /// Dispatches an event to the registered handlers.
356    ///
357    /// # Arguments
358    ///
359    /// - `ChartEvent` - A `ChartEvent` parameter.
360    ///
361    /// # Returns
362    ///
363    /// - `&mut Self` - A `mut Self` value.
364    pub fn emit(&mut self, event: ChartEvent) -> &mut Self {
365        let handlers: Vec<EventHandler> = self
366            .get_handlers()
367            .iter()
368            .filter(|registration: &&EventRegistration| registration.event_type == event.event_type)
369            .map(|registration: &EventRegistration| registration.handler.clone())
370            .collect();
371        for handler in handlers {
372            handler(&event);
373        }
374        self
375    }
376
377    /// Performs an action.
378    ///
379    /// Actions are how an application drives a chart: the same mechanism
380    /// ECharts calls `dispatchAction`, but expressed as a method on the
381    /// chart rather than a free function.
382    ///
383    /// # Arguments
384    ///
385    /// - `ChartAction` - A `ChartAction` parameter.
386    ///
387    /// # Returns
388    ///
389    /// - `&mut Self` - A `mut Self` value.
390    pub fn dispatch_action(&mut self, action: ChartAction) -> &mut Self {
391        match action {
392            ChartAction::DataZoom { start, end } => {
393                self.apply_data_zoom(start, end);
394            }
395            ChartAction::LegendToggle { name } => {
396                self.toggle_legend(&name);
397            }
398            ChartAction::Restore => {
399                self.restore();
400            }
401            ChartAction::Highlight {
402                series_index,
403                data_index,
404            } => {
405                self.highlight(series_index, data_index);
406            }
407            ChartAction::Downplay {
408                series_index,
409                data_index,
410            } => {
411                self.downplay(series_index, data_index);
412            }
413            ChartAction::ShowTip {
414                series_index,
415                data_index,
416                x,
417                y,
418            } => {
419                self.show_tip(series_index, data_index, x, y);
420            }
421            ChartAction::HideTip => {
422                self.hide_tip();
423            }
424        }
425        self
426    }
427
428    /// Shows the tooltip over a data point.
429    ///
430    /// # Arguments
431    ///
432    /// - `usize` - A non-negative integer (`usize`).
433    /// - `f64` - A 64-bit float (`f64`).
434    ///
435    /// # Returns
436    ///
437    /// - `&mut Self` - A `mut Self` value.
438    pub fn show_tip(
439        &mut self,
440        series_index: usize,
441        data_index: usize,
442        x: f64,
443        y: f64,
444    ) -> &mut Self {
445        let lines: Vec<(String, String)> = self.tooltip_lines(series_index, data_index);
446        self.set_tooltip(Some(TooltipState {
447            series_index,
448            data_index,
449            x,
450            y,
451            lines,
452        }));
453        self.set_display_dirty(true);
454        self
455    }
456
457    /// Hides the tooltip.
458    ///
459    /// # Returns
460    ///
461    /// - `&mut Self` - A `mut Self` value.
462    pub fn hide_tip(&mut self) -> &mut Self {
463        self.set_tooltip(None);
464        self.set_display_dirty(true);
465        self
466    }
467
468    /// Returns the tooltip the chart currently shows.
469    ///
470    /// # Returns
471    ///
472    /// - `Option<&TooltipState>` - `Some(...)` on success, `None` otherwise.
473    pub fn tooltip(&self) -> Option<&TooltipState> {
474        self.try_get_tooltip().as_ref()
475    }
476
477    /// Finds the data point under the given position.
478    ///
479    /// The search walks the chart's series from the last one to the
480    /// first, so that the series drawn on top wins — which is what the
481    /// user expects to click on.
482    ///
483    /// # Arguments
484    ///
485    /// - `f64` - A 64-bit float (`f64`).
486    ///
487    /// # Returns
488    ///
489    /// - `Option<ChartEvent>` - `Some(...)` on success, `None` otherwise.
490    pub fn hit_test(&self, x: f64, y: f64) -> Option<ChartEvent> {
491        for series in self.get_layout().series.iter().rev() {
492            for point in series.points.iter().rev() {
493                let distance: f64 = ((point.x - x).powi(2) + (point.y - y).powi(2)).sqrt();
494                if distance <= HIT_RADIUS {
495                    return Some(ChartEvent {
496                        event_type: ChartEventType::Click,
497                        series_index: Some(series.index),
498                        data_index: Some(point.data_index),
499                        series_name: series.name.clone(),
500                        name: point.name.clone(),
501                        value: Some(point.value),
502                        x,
503                        y,
504                        offset_x: x,
505                        offset_y: y,
506                    });
507                }
508            }
509        }
510        None
511    }
512
513    /// Builds the lines the tooltip shows for a data point.
514    ///
515    /// # Arguments
516    ///
517    /// - `usize` - A non-negative integer (`usize`).
518    ///
519    /// # Returns
520    ///
521    /// - `Vec<(String, String)>` - A `Vec<(String, String)>` value.
522    fn tooltip_lines(&self, series_index: usize, data_index: usize) -> Vec<(String, String)> {
523        let Some(series) = self.get_option().series.get(series_index) else {
524            return Vec::new();
525        };
526        let Some(data) = series.data.get(data_index) else {
527            return Vec::new();
528        };
529        let value: String = match data {
530            DataValue::Number(number) => format_number(*number),
531            DataValue::List(items) => items
532                .get(1)
533                .and_then(|item: &DataValue| item.to_number())
534                .map(format_number)
535                .unwrap_or_default(),
536            DataValue::Object(_) => data
537                .get("value")
538                .and_then(|item: &DataValue| item.to_number())
539                .map(format_number)
540                .unwrap_or_default(),
541            _ => String::new(),
542        };
543        let name: String = match data {
544            DataValue::List(items) => items
545                .first()
546                .and_then(|item: &DataValue| item.as_text())
547                .unwrap_or_default()
548                .to_string(),
549            DataValue::Object(_) => data
550                .get("name")
551                .and_then(|item: &DataValue| item.as_text())
552                .unwrap_or_default()
553                .to_string(),
554            _ => String::new(),
555        };
556        let mut lines: Vec<(String, String)> = Vec::new();
557        if !series.name.is_empty() {
558            lines.push((series.name.clone(), value.clone()));
559        }
560        if !name.is_empty() {
561            lines.push((name, value));
562        }
563        lines
564    }
565
566    /// Highlights a data point.
567    ///
568    /// # Arguments
569    ///
570    /// - `usize` - A non-negative integer (`usize`).
571    /// - `Option<usize>` - A `Option<usize>` parameter.
572    fn highlight(&mut self, series_index: usize, data_index: Option<usize>) {
573        let _: (usize, Option<usize>) = (series_index, data_index);
574        self.set_display_dirty(true);
575    }
576
577    /// Removes the highlight from a data point.
578    ///
579    /// # Arguments
580    ///
581    /// - `usize` - A non-negative integer (`usize`).
582    /// - `Option<usize>` - A `Option<usize>` parameter.
583    fn downplay(&mut self, series_index: usize, data_index: Option<usize>) {
584        let _: (usize, Option<usize>) = (series_index, data_index);
585        self.set_display_dirty(true);
586    }
587
588    /// Zooms the chart to a window of the data.
589    ///
590    /// # Arguments
591    ///
592    /// - `f64` - A 64-bit float (`f64`).
593    fn apply_data_zoom(&mut self, start: f64, end: f64) {
594        let start: f64 = start.clamp(0.0, 100.0);
595        let end: f64 = end.clamp(0.0, 100.0);
596        if end <= start {
597            return;
598        }
599        for control in self.get_mut_option().data_zoom.iter_mut() {
600            control.start = Some(start);
601            control.end = Some(end);
602        }
603        self.set_layout_dirty(true);
604        self.set_display_dirty(true);
605    }
606
607    /// Toggles a legend entry.
608    ///
609    /// # Arguments
610    ///
611    /// - `&str` - Shared reference to a `str`.
612    fn toggle_legend(&mut self, name: &str) {
613        let Some(legend) = self.get_mut_option().legend.as_mut() else {
614            return;
615        };
616        let mut selected: Vec<String> = legend.selected.take().unwrap_or_default();
617        if let Some(position) = selected.iter().position(|item: &String| item == name) {
618            selected.remove(position);
619        } else {
620            selected.push(name.to_string());
621        }
622        legend.selected = Some(selected);
623        self.set_layout_dirty(true);
624        self.set_display_dirty(true);
625    }
626
627    /// Restores the chart to its initial state.
628    fn restore(&mut self) {
629        for control in self.get_mut_option().data_zoom.iter_mut() {
630            control.start = None;
631            control.end = None;
632        }
633        if let Some(legend) = self.get_mut_option().legend.as_mut() {
634            legend.selected = None;
635        }
636        self.set_layout_dirty(true);
637        self.set_display_dirty(true);
638    }
639
640    /// Releases the resources the chart holds.
641    pub fn dispose(&mut self) {
642        self.get_mut_backend().dispose();
643        self.get_mut_display_list().clear();
644    }
645
646    /// Registers a map, so that a series can draw it.
647    ///
648    /// The map is given as a GeoJSON document: a collection of
649    /// features, each with a geometry and a name. The name of the map
650    /// is the one a series asks for.
651    ///
652    /// The geometry is kept out of the option on purpose: a map of a
653    /// country is megabytes of coordinates, and every chart of the
654    /// application draws the very same one.
655    ///
656    /// # Arguments
657    ///
658    /// - `&str` - Shared reference to a `str`.
659    /// - `&DataValue` - Shared reference to a `DataValue`.
660    ///
661    /// # Returns
662    ///
663    /// - `Result<&mut Self, GeoError>` - Result of the operation; an `Err` variant on failure.
664    pub fn register_map(&mut self, name: &str, geojson: &DataValue) -> Result<&mut Self, GeoError> {
665        self.get_mut_maps().register_geojson(name, geojson)?;
666        self.set_layout_dirty(true);
667        self.set_display_dirty(true);
668        Ok(self)
669    }
670
671    /// Registers a map that was built by hand.
672    ///
673    /// # Arguments
674    ///
675    /// - `MapData` - A `MapData` parameter.
676    ///
677    /// # Returns
678    ///
679    /// - `&mut Self` - A `mut Self` value.
680    pub fn register_map_data(&mut self, data: MapData) -> &mut Self {
681        self.get_mut_maps().register(data);
682        self.set_layout_dirty(true);
683        self.set_display_dirty(true);
684        self
685    }
686
687    /// Returns the maps the chart can draw.
688    ///
689    /// # Returns
690    ///
691    /// - `&MapRegistry` - A `MapRegistry` value.
692    pub fn maps(&self) -> &MapRegistry {
693        self.get_maps()
694    }
695
696    /// Returns the names of the maps the chart can draw.
697    ///
698    /// # Returns
699    ///
700    /// - `Vec<String>` - A `Vec<String>` value.
701    pub fn map_names(&self) -> Vec<String> {
702        self.get_maps().names()
703    }
704
705    /// Returns the chart as a data URL.
706    ///
707    /// The PNG is produced by the browser, so it is only available in a
708    /// browser; a native build returns `None`.
709    ///
710    /// # Returns
711    ///
712    /// - `Option<String>` - `Some(...)` on success, `None` otherwise.
713    pub fn to_data_url(&self) -> Option<String> {
714        self.get_canvas().to_data_url_with_type("image/png").ok()
715    }
716}
717
718/// Inherent implementation of [`ChartHandle`].
719impl ChartHandle {
720    /// Sends a command to the chart.
721    ///
722    /// The chart performs the command on its next frame.
723    ///
724    /// # Arguments
725    ///
726    /// - `ChartCommand` - A `ChartCommand` parameter.
727    ///
728    /// # Returns
729    ///
730    /// - `&Self` - The `&Self` value.
731    pub fn push(&self, command: ChartCommand) -> &Self {
732        let mut queue: Vec<ChartCommand> = self.get_commands().get();
733        queue.push(command);
734        self.get_commands().set(queue);
735        self
736    }
737
738    /// Sends an option to the chart.
739    ///
740    /// The chart draws the option on its next frame.
741    ///
742    /// # Arguments
743    ///
744    /// - `ChartOption` - A `ChartOption` parameter.
745    ///
746    /// # Returns
747    ///
748    /// - `&Self` - The `&Self` value.
749    pub fn set_option(&self, option: ChartOption) -> &Self {
750        self.push(ChartCommand::SetOption(Box::new(option)))
751    }
752
753    /// Resizes the chart, in CSS pixels.
754    ///
755    /// The chart draws at the new size on its next frame.
756    ///
757    /// # Arguments
758    ///
759    /// - `f64` - A 64-bit float (`f64`).
760    ///
761    /// # Returns
762    ///
763    /// - `&Self` - The `&Self` value.
764    pub fn resize(&self, width: f64, height: f64) -> &Self {
765        self.push(ChartCommand::Resize(width, height))
766    }
767
768    /// Performs an action on the chart.
769    ///
770    /// The chart performs the action on its next frame.
771    ///
772    /// # Arguments
773    ///
774    /// - `ChartAction` - A `ChartAction` parameter.
775    ///
776    /// # Returns
777    ///
778    /// - `&Self` - The `&Self` value.
779    pub fn dispatch_action(&self, action: ChartAction) -> &Self {
780        self.push(ChartCommand::Action(action))
781    }
782
783    /// Returns the number of commands waiting for the chart.
784    ///
785    /// # Returns
786    ///
787    /// - `usize` - A non-negative integer.
788    pub fn pending(&self) -> usize {
789        self.get_commands().get().len()
790    }
791}
792
793/// Default construction for [`ChartRenderer`].
794impl Default for ChartRenderer {
795    /// Returns a renderer.
796    fn default() -> Self {
797        Self::new()
798    }
799}
800
801/// Inherent implementation of [`ChartRenderer`].
802impl ChartRenderer {
803    /// Returns a renderer.
804    pub fn new() -> Self {
805        Self {
806            series: SeriesRenderer::new(),
807        }
808    }
809
810    /// Builds the display list of a chart.
811    ///
812    /// # Arguments
813    ///
814    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
815    /// - `&ChartOption` - Shared reference to a `ChartOption`.
816    /// - `&ChartLayout` - Shared reference to a `ChartLayout`.
817    pub fn build(&mut self, list: &mut DisplayList, option: &ChartOption, layout: &ChartLayout) {
818        self.build_with_tooltip(list, option, layout, None)
819    }
820
821    /// Builds the display list of a chart, with a tooltip on top.
822    ///
823    /// # Arguments
824    ///
825    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
826    /// - `&ChartOption` - Shared reference to a `ChartOption`.
827    /// - `&ChartLayout` - Shared reference to a `ChartLayout`.
828    /// - `Option<&TooltipState>` - A `Option<&TooltipState>` parameter.
829    pub fn build_with_tooltip(
830        &mut self,
831        list: &mut DisplayList,
832        option: &ChartOption,
833        layout: &ChartLayout,
834        tooltip: Option<&TooltipState>,
835    ) {
836        list.clear();
837        self.draw_background(list, option);
838        self.draw_title(list, option, layout);
839        self.draw_grid(list, option, layout);
840        self.draw_axes(list, option, layout);
841        self.get_mut_series().draw(list, option, layout);
842        self.draw_legend(list, option, layout);
843        if let Some(tooltip) = tooltip {
844            self.draw_tooltip(list, option, layout, tooltip);
845        }
846    }
847
848    /// Draws the tooltip.
849    ///
850    /// The tooltip is a rounded rectangle with one line per entry, and
851    /// it is placed next to the point it describes — flipping to the
852    /// other side when it would run off the edge of the canvas.
853    ///
854    /// # Arguments
855    ///
856    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
857    /// - `&ChartOption` - Shared reference to a `ChartOption`.
858    /// - `&ChartLayout` - Shared reference to a `ChartLayout`.
859    /// - `&TooltipState` - Shared reference to a `TooltipState`.
860    fn draw_tooltip(
861        &mut self,
862        list: &mut DisplayList,
863        option: &ChartOption,
864        layout: &ChartLayout,
865        tooltip: &TooltipState,
866    ) {
867        if tooltip.lines.is_empty() {
868            return;
869        }
870        let font_size: f64 = option
871            .tooltip
872            .as_ref()
873            .and_then(|tooltip: &TooltipOption| tooltip.text_style.as_ref())
874            .and_then(|style: &LabelOption| style.font_size)
875            .unwrap_or(DEFAULT_TOOLTIP_FONT_SIZE);
876        let padding: (f64, f64, f64, f64) = option
877            .tooltip
878            .as_ref()
879            .and_then(|tooltip: &TooltipOption| tooltip.padding)
880            .unwrap_or((8.0, 12.0, 8.0, 12.0));
881        let background: Color = option
882            .tooltip
883            .as_ref()
884            .and_then(|tooltip: &TooltipOption| tooltip.background_color.as_deref())
885            .and_then(Color::parse)
886            .unwrap_or_else(|| {
887                Color::parse(DEFAULT_BACKGROUND).unwrap_or(Color::rgb(255, 255, 255))
888            });
889        let border: Color = option
890            .tooltip
891            .as_ref()
892            .and_then(|tooltip: &TooltipOption| tooltip.border_color.as_deref())
893            .and_then(Color::parse)
894            .unwrap_or(Color::rgb(217, 217, 217));
895
896        let mut width: f64 = 0.0_f64;
897        for (name, value) in &tooltip.lines {
898            let text = format!("{}: {}", name, value);
899            width = width.max(text.len() as f64 * font_size * 0.6);
900        }
901        let line_height: f64 = font_size * 1.4;
902        let height: f64 = line_height * tooltip.lines.len() as f64;
903        let box_width: f64 = width + padding.1 + padding.3;
904        let box_height: f64 = height + padding.0 + padding.2;
905
906        // The tooltip sits to the right of the point, and flips to the
907        // left when it would run off the canvas.
908        let mut left: f64 = tooltip.x + 12.0;
909        if left + box_width > layout.width {
910            left = tooltip.x - 12.0 - box_width;
911        }
912        let mut top: f64 = tooltip.y - box_height * 0.5;
913        if top < 0.0 {
914            top = 0.0;
915        }
916        if top + box_height > layout.height {
917            top = (layout.height - box_height).max(0.0);
918        }
919
920        let path: Path = Path::builder()
921            .round_rect(
922                left as f32,
923                top as f32,
924                box_width as f32,
925                box_height as f32,
926                4.0,
927            )
928            .build();
929        list.fill(path.clone(), Fill::Solid(background), 1.0, Z_TOOLTIP);
930        let border_width: f64 = option
931            .tooltip
932            .as_ref()
933            .and_then(|tooltip: &TooltipOption| tooltip.border_width)
934            .unwrap_or(1.0);
935        if border_width > 0.0 {
936            list.stroke(
937                path,
938                Stroke::Solid(border),
939                LineStyle {
940                    width: border_width,
941                    ..LineStyle::new()
942                },
943                1.0,
944                Z_TOOLTIP + 1,
945            );
946        }
947
948        let mut style: TextStyle = TextStyle::new();
949        style.size = font_size;
950        style.align = TextAlign::Left;
951        style.vertical_align = TextVerticalAlign::Middle;
952        style.color = Color::rgb(51, 51, 51);
953        for (index, (name, value)) in tooltip.lines.iter().enumerate() {
954            let baseline: f64 = top + padding.0 + line_height * (index as f64 + 0.5);
955            list.text(
956                format!("{}: {}", name, value),
957                left + padding.3,
958                baseline,
959                style.clone(),
960                Z_TOOLTIP + 2,
961            );
962        }
963    }
964
965    /// Draws the chart's background.
966    ///
967    /// # Arguments
968    ///
969    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
970    /// - `&ChartOption` - Shared reference to a `ChartOption`.
971    fn draw_background(&mut self, list: &mut DisplayList, option: &ChartOption) {
972        let Some(background) = option.background_color.as_deref() else {
973            return;
974        };
975        let Some(color) = Color::parse(background) else {
976            return;
977        };
978        let rect: Path = Path::builder().rect(0.0, 0.0, 1.0, 1.0).build();
979        let mut scaled: Path = rect;
980        // The background covers the whole canvas; the path is built with
981        // the canvas' size so that the fill is not affected by the
982        // device pixel ratio.
983        scaled = scaled.transformed(&Transform2D::scale(1.0, 1.0));
984        list.fill(scaled, Fill::Solid(color), 1.0, Z_BACKGROUND);
985    }
986
987    /// Draws the chart's title and subtitle.
988    ///
989    /// # Arguments
990    ///
991    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
992    /// - `&ChartOption` - Shared reference to a `ChartOption`.
993    /// - `&ChartLayout` - Shared reference to a `ChartLayout`.
994    fn draw_title(&mut self, list: &mut DisplayList, option: &ChartOption, layout: &ChartLayout) {
995        let Some(title) = &option.title else {
996            return;
997        };
998        if !title.show || title.text.is_empty() {
999            return;
1000        }
1001        let font_size: f64 = title
1002            .text_style
1003            .as_ref()
1004            .and_then(|style: &LabelOption| style.font_size)
1005            .unwrap_or(DEFAULT_TITLE_FONT_SIZE);
1006        let mut style: TextStyle = TextStyle::new();
1007        style.size = font_size;
1008        style.align = TextAlign::Center;
1009        style.vertical_align = TextVerticalAlign::Top;
1010        if let Some(text_style) = &title.text_style {
1011            if let Some(color) = text_style
1012                .color
1013                .as_ref()
1014                .and_then(|color: &String| Color::parse(color))
1015            {
1016                style.color = color;
1017            }
1018            if let Some(family) = &text_style.font_family {
1019                style.family = family.clone();
1020            }
1021            if let Some(weight) = &text_style.font_weight {
1022                style.weight = weight.clone();
1023            }
1024        }
1025        let center_x: f64 = layout.width * 0.5;
1026        list.text(title.text.clone(), center_x, 0.0, style, Z_TITLE);
1027    }
1028
1029    /// Draws the chart's grid and the split lines of its axes.
1030    ///
1031    /// # Arguments
1032    ///
1033    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
1034    /// - `&ChartOption` - Shared reference to a `ChartOption`.
1035    /// - `&ChartLayout` - Shared reference to a `ChartLayout`.
1036    fn draw_grid(&mut self, list: &mut DisplayList, option: &ChartOption, layout: &ChartLayout) {
1037        let grid_option: GridOption = option.grid.clone().unwrap_or_default();
1038        if let Some(background) = grid_option.background_color.as_deref()
1039            && let Some(color) = Color::parse(background)
1040        {
1041            for grid in &layout.grids {
1042                let path: Path = Path::builder()
1043                    .rect(
1044                        grid.rect.min_x as f32,
1045                        grid.rect.min_y as f32,
1046                        grid.rect.width() as f32,
1047                        grid.rect.height() as f32,
1048                    )
1049                    .build();
1050                list.fill(path, Fill::Solid(color), 1.0, Z_GRID);
1051            }
1052        }
1053        if !grid_option.show {
1054            return;
1055        }
1056        if let Some(border) = grid_option.border_color.as_deref().and_then(Color::parse) {
1057            let width: f32 = grid_option.border_width.unwrap_or(1.0) as f32;
1058            for grid in &layout.grids {
1059                let path: Path = Path::builder()
1060                    .rect(
1061                        grid.rect.min_x as f32,
1062                        grid.rect.min_y as f32,
1063                        grid.rect.width() as f32,
1064                        grid.rect.height() as f32,
1065                    )
1066                    .build();
1067                list.stroke(
1068                    path,
1069                    Stroke::Solid(border),
1070                    LineStyle {
1071                        width: width as f64,
1072                        ..LineStyle::new()
1073                    },
1074                    1.0,
1075                    Z_GRID + 1,
1076                );
1077            }
1078        }
1079    }
1080
1081    /// Draws the chart's axes: their lines, their ticks, their labels,
1082    /// and their names.
1083    ///
1084    /// # Arguments
1085    ///
1086    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
1087    /// - `&ChartOption` - Shared reference to a `ChartOption`.
1088    /// - `&ChartLayout` - Shared reference to a `ChartLayout`.
1089    fn draw_axes(&mut self, list: &mut DisplayList, option: &ChartOption, layout: &ChartLayout) {
1090        for grid in &layout.grids {
1091            for axis in &grid.x_axes {
1092                self.draw_axis(list, option, grid, axis, true);
1093            }
1094            for axis in &grid.y_axes {
1095                self.draw_axis(list, option, grid, axis, false);
1096            }
1097        }
1098    }
1099
1100    /// Draws one axis.
1101    ///
1102    /// A chart that does not name its axes still gets ECharts' default
1103    /// axes: a category axis on the bottom with its line, its tick
1104    /// marks and its labels, and a value axis on the left with its
1105    /// labels and its split lines — and without a line of its own,
1106    /// because ECharts hides the line of a value axis whose other axis
1107    /// is a category axis.
1108    ///
1109    /// # Arguments
1110    ///
1111    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
1112    /// - `&ChartOption` - Shared reference to a `ChartOption`.
1113    /// - `&GridLayout` - Shared reference to a `GridLayout`.
1114    /// - `&AxisLayout` - Shared reference to a `AxisLayout`.
1115    /// - `bool` - A boolean (`bool`).
1116    fn draw_axis(
1117        &mut self,
1118        list: &mut DisplayList,
1119        option: &ChartOption,
1120        grid: &GridLayout,
1121        axis: &AxisLayout,
1122        horizontal: bool,
1123    ) {
1124        if !axis.show {
1125            return;
1126        }
1127        let axis_option: Option<&AxisOption> = if horizontal {
1128            option.x_axis.get(axis.index)
1129        } else {
1130            option.y_axis.get(axis.index)
1131        };
1132
1133        // The split lines run across the grid, perpendicular to the
1134        // axis.
1135        let split_line: Option<&SplitLineOption> =
1136            axis_option.and_then(|option: &AxisOption| option.split_line.as_ref());
1137        let split_line_show: bool = match split_line {
1138            Some(split_line) => split_line.show,
1139            // A value axis draws its split lines, a category axis does
1140            // not: ECharts leaves the grid of a category axis alone.
1141            None => matches!(axis.kind, AxisType::Value | AxisType::Log),
1142        };
1143        if split_line_show {
1144            let color: Color = split_line
1145                .and_then(|split_line: &SplitLineOption| split_line.line_style.as_ref())
1146                .and_then(|style: &LineStyleOption| style.color.as_deref())
1147                .and_then(Color::parse)
1148                .unwrap_or_else(|| {
1149                    Color::parse(DEFAULT_SPLIT_LINE_COLOR).unwrap_or(Color::rgb(217, 217, 217))
1150                });
1151            let width: f64 = split_line
1152                .and_then(|split_line: &SplitLineOption| split_line.line_style.as_ref())
1153                .and_then(|style: &LineStyleOption| style.width)
1154                .unwrap_or(DEFAULT_SPLIT_LINE_WIDTH);
1155            for (index, tick) in axis.ticks.iter().enumerate() {
1156                // A category axis with more categories than fit
1157                // draws only every Nth split line, so that a chart
1158                // with a million points does not ask for a million
1159                // strokes.
1160                if index % axis.label_interval.max(1) != 0 {
1161                    continue;
1162                }
1163                let path: Path = if horizontal {
1164                    Path::builder()
1165                        .move_to(tick.position as f32, grid.rect.min_y as f32)
1166                        .line_to(tick.position as f32, grid.rect.max_y as f32)
1167                        .build()
1168                } else {
1169                    Path::builder()
1170                        .move_to(grid.rect.min_x as f32, tick.position as f32)
1171                        .line_to(grid.rect.max_x as f32, tick.position as f32)
1172                        .build()
1173                };
1174                list.stroke(
1175                    path,
1176                    Stroke::Solid(color),
1177                    LineStyle {
1178                        width,
1179                        ..LineStyle::new()
1180                    },
1181                    1.0,
1182                    Z_GRID,
1183                );
1184            }
1185        }
1186
1187        // The axis' own line.
1188        let axis_line: Option<&AxisLineOption> =
1189            axis_option.and_then(|option: &AxisOption| option.axis_line.as_ref());
1190        let axis_line_show: bool = match axis_line {
1191            Some(axis_line) => axis_line.show,
1192            // ECharts draws the line of a category axis, and hides the
1193            // line of a value axis whose other axis is a category
1194            // axis: the grid of the chart says it better.
1195            None => draws_own_line(axis, grid, horizontal),
1196        };
1197        if axis_line_show {
1198            let color: Color = axis_line
1199                .and_then(|axis_line: &AxisLineOption| axis_line.line_style.as_ref())
1200                .and_then(|style: &LineStyleOption| style.color.as_deref())
1201                .and_then(Color::parse)
1202                .unwrap_or_else(|| {
1203                    Color::parse(DEFAULT_AXIS_LINE_COLOR).unwrap_or(Color::rgb(217, 217, 217))
1204                });
1205            let width: f64 = axis_line
1206                .and_then(|axis_line: &AxisLineOption| axis_line.line_style.as_ref())
1207                .and_then(|style: &LineStyleOption| style.width)
1208                .unwrap_or(DEFAULT_AXIS_LINE_WIDTH);
1209            let path: Path = if horizontal {
1210                Path::builder()
1211                    .move_to(grid.rect.min_x as f32, axis.offset as f32)
1212                    .line_to(grid.rect.max_x as f32, axis.offset as f32)
1213                    .build()
1214            } else {
1215                Path::builder()
1216                    .move_to(axis.offset as f32, grid.rect.min_y as f32)
1217                    .line_to(axis.offset as f32, grid.rect.max_y as f32)
1218                    .build()
1219            };
1220            list.stroke(
1221                path,
1222                Stroke::Solid(color),
1223                LineStyle {
1224                    width,
1225                    ..LineStyle::new()
1226                },
1227                1.0,
1228                Z_AXIS,
1229            );
1230        }
1231
1232        // The tick marks: a short line at every tick, outside the grid.
1233        let tick_option: Option<&AxisTickOption> =
1234            axis_option.and_then(|option: &AxisOption| option.axis_tick.as_ref());
1235        let tick_show: bool = match tick_option {
1236            Some(tick) => tick.show,
1237            // The tick marks follow the axis' line: ECharts draws them
1238            // wherever it draws the line.
1239            None => draws_own_line(axis, grid, horizontal),
1240        };
1241        if tick_show {
1242            let length: f64 = tick_option
1243                .and_then(|tick: &AxisTickOption| tick.length)
1244                .unwrap_or(DEFAULT_AXIS_TICK_LENGTH);
1245            let color: Color = tick_option
1246                .and_then(|tick: &AxisTickOption| tick.line_style.as_ref())
1247                .and_then(|style: &LineStyleOption| style.color.as_deref())
1248                .and_then(Color::parse)
1249                .or_else(|| {
1250                    axis_line
1251                        .and_then(|axis_line: &AxisLineOption| axis_line.line_style.as_ref())
1252                        .and_then(|style: &LineStyleOption| style.color.as_deref())
1253                        .and_then(Color::parse)
1254                })
1255                .unwrap_or_else(|| {
1256                    Color::parse(DEFAULT_AXIS_LINE_COLOR).unwrap_or(Color::rgb(217, 217, 217))
1257                });
1258            let width: f64 = tick_option
1259                .and_then(|tick: &AxisTickOption| tick.line_style.as_ref())
1260                .and_then(|style: &LineStyleOption| style.width)
1261                .unwrap_or(DEFAULT_AXIS_LINE_WIDTH);
1262            for (index, tick) in axis.ticks.iter().enumerate() {
1263                if index % axis.label_interval.max(1) != 0 {
1264                    continue;
1265                }
1266                let path: Path = if horizontal {
1267                    Path::builder()
1268                        .move_to(tick.position as f32, axis.offset as f32)
1269                        .line_to(tick.position as f32, (axis.offset + length) as f32)
1270                        .build()
1271                } else {
1272                    Path::builder()
1273                        .move_to(axis.offset as f32, tick.position as f32)
1274                        .line_to((axis.offset - length) as f32, tick.position as f32)
1275                        .build()
1276                };
1277                list.stroke(
1278                    path,
1279                    Stroke::Solid(color),
1280                    LineStyle {
1281                        width,
1282                        ..LineStyle::new()
1283                    },
1284                    1.0,
1285                    Z_AXIS,
1286                );
1287            }
1288        }
1289
1290        // The labels.
1291        let label_option: Option<LabelOption> =
1292            axis_option.and_then(|option: &AxisOption| option.axis_label.clone());
1293        let show_labels: bool = label_option
1294            .as_ref()
1295            .map(|label: &LabelOption| label.show)
1296            .unwrap_or(true);
1297        let font_size: f64 = label_option
1298            .as_ref()
1299            .and_then(|label: &LabelOption| label.font_size)
1300            .unwrap_or(DEFAULT_AXIS_LABEL_FONT_SIZE);
1301        let gap: f64 = DEFAULT_AXIS_LABEL_GAP;
1302        if show_labels {
1303            let mut style: TextStyle = TextStyle::new();
1304            style.size = font_size;
1305            style.color = Color::parse(DEFAULT_TEXT_COLOR).unwrap_or(Color::rgb(110, 112, 121));
1306            if let Some(label) = &label_option {
1307                if let Some(color) = label
1308                    .color
1309                    .as_ref()
1310                    .and_then(|color: &String| Color::parse(color))
1311                {
1312                    style.color = color;
1313                }
1314                if let Some(family) = &label.font_family {
1315                    style.family = family.clone();
1316                }
1317                if let Some(weight) = &label.font_weight {
1318                    style.weight = weight.clone();
1319                }
1320            }
1321            for (index, tick) in axis.ticks.iter().enumerate() {
1322                // The labels are drawn every Nth tick, so that they do
1323                // not overlap each other.
1324                if index % axis.label_interval.max(1) != 0 {
1325                    continue;
1326                }
1327                let (x, y) = if horizontal {
1328                    (tick.position, axis.offset + gap + font_size)
1329                } else {
1330                    (axis.offset - gap, tick.position)
1331                };
1332                let mut label_style: TextStyle = style.clone();
1333                label_style.align = if horizontal {
1334                    TextAlign::Center
1335                } else {
1336                    TextAlign::Right
1337                };
1338                label_style.vertical_align = if horizontal {
1339                    TextVerticalAlign::Top
1340                } else {
1341                    TextVerticalAlign::Middle
1342                };
1343                list.text(tick.label.clone(), x, y, label_style, Z_AXIS + 1);
1344            }
1345        }
1346
1347        // The axis' name.
1348        if let Some(name) = &axis.name
1349            && !name.is_empty()
1350        {
1351            let mut style: TextStyle = TextStyle::new();
1352            style.size = DEFAULT_AXIS_LABEL_FONT_SIZE;
1353            style.color = Color::parse(DEFAULT_TEXT_COLOR).unwrap_or(Color::rgb(110, 112, 121));
1354            style.align = TextAlign::Center;
1355            style.vertical_align = TextVerticalAlign::Middle;
1356            let (x, y) = if horizontal {
1357                (grid.rect.center_x(), axis.offset + gap + font_size * 2.0)
1358            } else {
1359                (
1360                    axis.offset - gap - axis.label_space * 0.5,
1361                    grid.rect.center_y(),
1362                )
1363            };
1364            list.text(name.clone(), x, y, style, Z_AXIS + 2);
1365        }
1366    }
1367
1368    /// Draws the chart's legend.
1369    ///
1370    /// # Arguments
1371    ///
1372    /// - `&mut DisplayList` - Mutable reference to a `DisplayList` (mutated in place).
1373    /// - `&ChartOption` - Shared reference to a `ChartOption`.
1374    /// - `&ChartLayout` - Shared reference to a `ChartLayout`.
1375    fn draw_legend(&mut self, list: &mut DisplayList, option: &ChartOption, layout: &ChartLayout) {
1376        let Some(legend) = &option.legend else {
1377            return;
1378        };
1379        if !legend.show || legend.data.is_empty() {
1380            return;
1381        }
1382        let font_size: f64 = legend
1383            .text_style
1384            .as_ref()
1385            .and_then(|style: &LabelOption| style.font_size)
1386            .unwrap_or(DEFAULT_LEGEND_FONT_SIZE);
1387        let item_width: f64 = legend.item_width.unwrap_or(25.0);
1388        let item_height: f64 = legend.item_height.unwrap_or(14.0);
1389        let item_gap: f64 = legend.item_gap.unwrap_or(10.0);
1390        let mut style: TextStyle = TextStyle::new();
1391        style.size = font_size;
1392        style.color = Color::parse(DEFAULT_TEXT_COLOR).unwrap_or(Color::rgb(110, 112, 121));
1393        style.align = TextAlign::Left;
1394        style.vertical_align = TextVerticalAlign::Middle;
1395        if let Some(text_style) = &legend.text_style
1396            && let Some(color) = text_style
1397                .color
1398                .as_ref()
1399                .and_then(|color: &String| Color::parse(color))
1400        {
1401            style.color = color;
1402        }
1403        let horizontal: bool =
1404            legend.orient.unwrap_or(Orientation::Horizontal) == Orientation::Horizontal;
1405        let mut cursor_x: f64 = legend.left.as_ref().map(|_| 10.0).unwrap_or(10.0);
1406        let mut cursor_y: f64 = layout
1407            .title_rect
1408            .as_ref()
1409            .map(|rect: &BBox| rect.max_y + 10.0)
1410            .unwrap_or(10.0);
1411        for (index, name) in legend.data.iter().enumerate() {
1412            let color: Color = palette_color(option, index);
1413            let symbol: Path = Path::builder()
1414                .rect(
1415                    cursor_x as f32,
1416                    cursor_y as f32,
1417                    item_width as f32,
1418                    item_height as f32,
1419                )
1420                .build();
1421            list.fill(symbol, Fill::Solid(color), 1.0, Z_LEGEND);
1422            list.text(
1423                name.clone(),
1424                cursor_x + item_width + item_gap * 0.5,
1425                cursor_y + item_height * 0.5,
1426                style.clone(),
1427                Z_LEGEND + 1,
1428            );
1429            let advance: f64 = item_width + item_gap + name.len() as f64 * font_size * 0.6;
1430            if horizontal {
1431                cursor_x += advance;
1432            } else {
1433                cursor_y += item_height + item_gap;
1434                cursor_x = 10.0;
1435            }
1436        }
1437    }
1438}