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}