1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/// Controls where slice labels are placed relative to each slice.
///
/// The default is [`Auto`](PieLabelPosition::Auto), which places labels
/// inside large slices and moves small ones outside with leader lines.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PieLabelPosition {
/// Labels centered between the inner and outer radius (or at mid-radius for
/// a full pie). Works well when all slices are large enough to fit text.
Inside,
/// Labels placed outside the pie with leader lines connecting them to their
/// slice. Small slices are automatically spaced to avoid label overlap.
Outside,
/// Inside for large slices; outside with a leader line for small ones.
/// This is the default. The threshold is controlled by
/// [`with_min_label_fraction`](PiePlot::with_min_label_fraction).
Auto,
/// No slice labels. Combine with [`with_legend`](PiePlot::with_legend)
/// to identify slices via a legend instead.
None,
}
/// Builder for a pie or donut chart.
///
/// Each slice has its own explicit color. Slice labels can be positioned
/// automatically, forced inside or outside, or suppressed entirely in favor
/// of a legend. Percentage values can be appended to labels with
/// [`with_percent`](Self::with_percent).
///
/// Render with [`render_pie`](crate::render::render::render_pie) for most
/// cases, or [`render_multiple`](crate::render::render::render_multiple) when
/// a legend is attached.
///
/// # Example
///
/// ```rust,no_run
/// use kuva::plot::{PiePlot, PieLabelPosition};
/// use kuva::backend::svg::SvgBackend;
/// use kuva::render::render::render_pie;
/// use kuva::render::layout::Layout;
/// use kuva::render::plots::Plot;
///
/// let pie = PiePlot::new()
/// .with_slice("Rust", 40.0, "steelblue")
/// .with_slice("Python", 30.0, "tomato")
/// .with_slice("R", 20.0, "seagreen")
/// .with_slice("Other", 10.0, "gold")
/// .with_percent();
///
/// let plots = vec![Plot::Pie(pie.clone())];
/// let layout = Layout::auto_from_plots(&plots).with_title("Language usage");
///
/// let svg = SvgBackend.render_scene(&render_pie(&pie, &layout));
/// std::fs::write("pie.svg", svg).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct PiePlot {
pub slices: Vec<PieSlice>,
/// Inner radius in pixels. `0.0` renders a full pie; any positive value
/// cuts a hole in the centre to produce a donut chart.
pub inner_radius: f64,
pub legend_label: Option<String>,
/// Label placement strategy. Defaults to [`PieLabelPosition::Auto`].
pub label_position: PieLabelPosition,
/// When `true`, each label is suffixed with the slice's percentage of the total.
pub show_percent: bool,
/// Slices whose fraction of the total is below this threshold receive no
/// label (default `0.05`, i.e. 5 %).
pub min_label_fraction: f64,
pub show_tooltips: bool,
pub tooltip_labels: Option<Vec<String>>,
}
/// A single slice of a pie chart.
#[derive(Debug, Clone)]
pub struct PieSlice {
pub label: String,
pub value: f64,
/// Fill color as a CSS color string (e.g. `"steelblue"`, `"#4682b4"`).
pub color: String,
}
impl Default for PiePlot {
fn default() -> Self {
Self::new()
}
}
impl PiePlot {
/// Create a pie chart with default settings.
///
/// Defaults: full pie (`inner_radius = 0.0`), Auto label positioning,
/// no percentages, no legend, `min_label_fraction = 0.05`.
pub fn new() -> Self {
Self {
slices: vec![],
inner_radius: 0.0,
legend_label: None,
label_position: PieLabelPosition::Auto,
show_percent: false,
min_label_fraction: 0.05,
show_tooltips: false,
tooltip_labels: None,
}
}
/// Add a slice with a label, value, and fill color.
///
/// Slices are drawn clockwise in the order they are added, starting from
/// the top (12 o'clock). The value is proportional — only the ratio between
/// values matters, not their absolute magnitude.
///
/// ```rust,no_run
/// # use kuva::plot::PiePlot;
/// let pie = PiePlot::new()
/// .with_slice("A", 60.0, "steelblue")
/// .with_slice("B", 40.0, "tomato");
/// ```
pub fn with_slice<L, V, C>(mut self, label: L, value: V, color: C) -> Self
where
L: Into<String>,
V: Into<f64>,
C: Into<String>,
{
self.slices.push(PieSlice {
label: label.into(),
value: value.into(),
color: color.into(),
});
self
}
/// Set the inner radius in pixels to create a donut chart.
///
/// A value of `0.0` (the default) renders a solid pie. Any positive value
/// cuts a hollow centre. Typical values are in the range `40.0`–`80.0`
/// depending on the canvas size.
///
/// ```rust,no_run
/// # use kuva::plot::PiePlot;
/// let donut = PiePlot::new()
/// .with_slice("A", 60.0, "steelblue")
/// .with_slice("B", 40.0, "tomato")
/// .with_inner_radius(60.0);
/// ```
pub fn with_inner_radius(mut self, r: f64) -> Self {
self.inner_radius = r;
self
}
/// Attach a legend to the pie chart.
///
/// When a legend label is set, [`render_multiple`](crate::render::render::render_multiple)
/// adds a per-slice legend entry (colored square + slice label) in the
/// right margin. Combine with
/// [`with_label_position(PieLabelPosition::None)`](Self::with_label_position)
/// to use the legend as the sole means of identification.
///
/// ```rust,no_run
/// # use kuva::plot::{PiePlot, PieLabelPosition};
/// # use kuva::render::plots::Plot;
/// let pie = PiePlot::new()
/// .with_slice("A", 60.0, "steelblue")
/// .with_slice("B", 40.0, "tomato")
/// .with_legend("Category")
/// .with_label_position(PieLabelPosition::None);
/// ```
pub fn with_legend<S: Into<String>>(mut self, label: S) -> Self {
self.legend_label = Some(label.into());
self
}
/// Set the label placement strategy.
///
/// See [`PieLabelPosition`] for the available options. The default is
/// [`Auto`](PieLabelPosition::Auto).
pub fn with_label_position(mut self, pos: PieLabelPosition) -> Self {
self.label_position = pos;
self
}
/// Append the percentage of the total to each slice label.
///
/// The percentage is computed from the slice values and formatted to one
/// decimal place (e.g. `"Rust 40.0%"`).
pub fn with_percent(mut self) -> Self {
self.show_percent = true;
self
}
/// Set the minimum slice fraction below which no label is drawn.
///
/// Slices whose value is less than `fraction` of the total are silently
/// skipped. The default is `0.05` (5 %). Set to `0.0` to label every
/// slice regardless of size.
///
/// ```rust,no_run
/// # use kuva::plot::PiePlot;
/// // Label all slices, even tiny ones
/// let pie = PiePlot::new()
/// .with_slice("Big", 90.0, "steelblue")
/// .with_slice("Tiny", 1.0, "tomato")
/// .with_min_label_fraction(0.0);
/// ```
pub fn with_min_label_fraction(mut self, fraction: f64) -> Self {
self.min_label_fraction = fraction;
self
}
pub fn with_tooltips(mut self) -> Self {
self.show_tooltips = true;
self
}
pub fn with_tooltip_labels(
mut self,
labels: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.tooltip_labels = Some(labels.into_iter().map(|s| s.into()).collect());
self
}
}