Skip to main content

plotkit_core/charts/
waterfall.rs

1//! Waterfall chart builder methods.
2//!
3//! Provides a fluent builder API for configuring [`WaterfallArtist`] instances.
4//! Each method returns `&mut Self`, allowing calls to be chained together
5//! for concise, readable chart construction.
6
7use crate::artist::WaterfallArtist;
8use crate::primitives::Color;
9
10impl WaterfallArtist {
11    /// Sets the color for bars representing positive (increasing) changes.
12    ///
13    /// # Arguments
14    ///
15    /// * `color` - The [`Color`] to fill increase bars with.
16    pub fn increase_color(&mut self, color: Color) -> &mut Self {
17        self.increase_color = color;
18        self
19    }
20
21    /// Sets the color for bars representing negative (decreasing) changes.
22    ///
23    /// # Arguments
24    ///
25    /// * `color` - The [`Color`] to fill decrease bars with.
26    pub fn decrease_color(&mut self, color: Color) -> &mut Self {
27        self.decrease_color = color;
28        self
29    }
30
31    /// Sets the color for total bars (bars drawn from zero).
32    ///
33    /// # Arguments
34    ///
35    /// * `color` - The [`Color`] to fill total bars with.
36    pub fn total_color(&mut self, color: Color) -> &mut Self {
37        self.total_color = color;
38        self
39    }
40
41    /// Enables or disables connector lines between consecutive bar tops.
42    ///
43    /// When enabled (the default), thin horizontal lines are drawn from the
44    /// end of each bar to the start of the next, visually connecting the
45    /// running total across the chart.
46    ///
47    /// # Arguments
48    ///
49    /// * `enabled` - `true` to draw connector lines, `false` to hide them.
50    pub fn connector_lines(&mut self, enabled: bool) -> &mut Self {
51        self.connector_lines = enabled;
52        self
53    }
54
55    /// Enables or disables value labels displayed on each bar.
56    ///
57    /// When enabled, each bar shows its change value (or total value for
58    /// total bars) as a text label positioned at the bar's top edge.
59    ///
60    /// # Arguments
61    ///
62    /// * `enabled` - `true` to show value labels, `false` to hide them.
63    pub fn show_values(&mut self, enabled: bool) -> &mut Self {
64        self.show_values = enabled;
65        self
66    }
67
68    /// Sets the bar width as a fraction of the category spacing (0.0 to 1.0).
69    ///
70    /// Smaller values produce thinner bars with more whitespace between them,
71    /// while larger values make the bars wider. The value is clamped to the
72    /// range `[0.1, 1.0]` so that bars are never invisibly thin nor overlap
73    /// their neighbours.
74    ///
75    /// # Arguments
76    ///
77    /// * `width` - The fraction of available category space each bar should occupy.
78    pub fn bar_width(&mut self, width: f64) -> &mut Self {
79        self.bar_width = width.clamp(0.1, 1.0);
80        self
81    }
82
83    /// Sets the legend label.
84    ///
85    /// When a legend is displayed on the figure, this label will appear
86    /// next to the color swatch for this waterfall series. Passing a new
87    /// value overwrites any previously set label.
88    ///
89    /// # Arguments
90    ///
91    /// * `label` - A string slice that will be stored as the legend entry.
92    pub fn label(&mut self, label: &str) -> &mut Self {
93        self.label = Some(label.to_string());
94        self
95    }
96
97    /// Marks the given bar indices as "total" bars.
98    ///
99    /// Total bars are drawn from zero to the current cumulative sum, rather
100    /// than showing the incremental change. Typically the first and/or last
101    /// bar in a waterfall chart are marked as totals.
102    ///
103    /// # Arguments
104    ///
105    /// * `indices` - A slice of zero-based category indices to treat as totals.
106    pub fn total(&mut self, indices: &[usize]) -> &mut Self {
107        self.total_indices = indices.to_vec();
108        self
109    }
110
111    /// Sets the opacity.
112    ///
113    /// The value is clamped to the range `[0.0, 1.0]`, where `0.0` is fully
114    /// transparent and `1.0` is fully opaque.
115    ///
116    /// # Arguments
117    ///
118    /// * `alpha` - The desired opacity level.
119    pub fn alpha(&mut self, alpha: f64) -> &mut Self {
120        self.alpha = alpha.clamp(0.0, 1.0);
121        self
122    }
123
124    /// Computes the running cumulative sum from the change values.
125    ///
126    /// For total bars, the cumulative sum equals the bar's value directly.
127    /// For non-total bars, each value is added to the running total.
128    ///
129    /// Returns a vector of cumulative sums, one per category.
130    pub fn cumulative_sums(&self) -> Vec<f64> {
131        let n = self.values.len();
132        let mut cumsum = Vec::with_capacity(n);
133        let mut running = 0.0;
134        for i in 0..n {
135            if self.total_indices.contains(&i) {
136                running = self.values.data[i];
137            } else {
138                running += self.values.data[i];
139            }
140            cumsum.push(running);
141        }
142        cumsum
143    }
144
145    /// Returns the bar base and top positions for each category.
146    ///
147    /// For non-total bars: base is the previous cumulative sum, top is the
148    /// current cumulative sum (base + change).
149    ///
150    /// For total bars: base is 0, top is the cumulative sum value.
151    ///
152    /// Returns a vector of `(base, top)` tuples.
153    pub fn bar_positions(&self) -> Vec<(f64, f64)> {
154        let cumsum = self.cumulative_sums();
155        let n = cumsum.len();
156        let mut positions = Vec::with_capacity(n);
157
158        for i in 0..n {
159            if self.total_indices.contains(&i) {
160                positions.push((0.0, cumsum[i]));
161            } else {
162                let base = if i == 0 { 0.0 } else { cumsum[i - 1] };
163                positions.push((base, cumsum[i]));
164            }
165        }
166        positions
167    }
168
169    /// Returns the connector line positions as pairs of y-values.
170    ///
171    /// Each connector runs from the top of bar `i` to the start of bar `i+1`.
172    /// Returns a vector of `(from_y, to_x_index)` tuples where `from_y` is
173    /// the y-value the connector line runs at.
174    pub fn connector_positions(&self) -> Vec<(usize, f64)> {
175        if !self.connector_lines {
176            return Vec::new();
177        }
178        let cumsum = self.cumulative_sums();
179        let n = cumsum.len();
180        if n < 2 {
181            return Vec::new();
182        }
183        let mut connectors = Vec::with_capacity(n - 1);
184        for (i, &val) in cumsum.iter().enumerate().take(n - 1) {
185            connectors.push((i, val));
186        }
187        connectors
188    }
189}