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
/// One row in a forest plot: a study with an effect estimate and confidence interval.
pub struct ForestRow {
pub label: String,
pub estimate: f64,
pub ci_lower: f64,
pub ci_upper: f64,
/// Optional study weight — used to scale the marker radius.
pub weight: Option<f64>,
/// Optional per-row color override (CSS color string).
pub color: Option<String>,
}
impl ForestRow {
/// Create a row with the required fields; weight and color default to `None`.
pub fn new<S: Into<String>>(label: S, estimate: f64, ci_lower: f64, ci_upper: f64) -> Self {
Self {
label: label.into(),
estimate,
ci_lower,
ci_upper,
weight: None,
color: None,
}
}
}
/// Builder for a forest plot (meta-analysis).
///
/// Each row represents a study: a point estimate with a confidence interval
/// on a numeric X-axis, and a label on a categorical Y-axis. A vertical
/// reference line (null effect, typically at x = 0) can be shown.
///
/// # Example
///
/// ```rust,no_run
/// use kuva::plot::ForestPlot;
/// use kuva::backend::svg::SvgBackend;
/// use kuva::render::render::render_multiple;
/// use kuva::render::layout::Layout;
/// use kuva::render::plots::Plot;
///
/// let forest = ForestPlot::new()
/// .with_row("Study A", 0.50, 0.10, 0.90)
/// .with_row("Study B", -0.30, -0.80, 0.20)
/// .with_row("Study C", 0.20, -0.10, 0.50)
/// .with_null_value(0.0);
///
/// let plots = vec![Plot::Forest(forest)];
/// let layout = Layout::auto_from_plots(&plots)
/// .with_title("Meta-Analysis")
/// .with_x_label("Effect Size");
///
/// let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
/// std::fs::write("forest.svg", svg).unwrap();
/// ```
pub struct ForestPlot {
pub rows: Vec<ForestRow>,
/// Point/whisker color (CSS color string). Default `"steelblue"`.
pub color: String,
/// Base marker half-width in pixels. Default `6.0`.
pub marker_size: f64,
/// Whisker stroke width in pixels. Default `1.5`.
pub whisker_width: f64,
/// Null-effect reference value (vertical dashed line). Default `Some(0.0)`.
pub null_value: Option<f64>,
/// Whether to draw the null reference line. Default `true`.
pub show_null_line: bool,
/// Cap half-height in pixels for whisker end caps. Default `0.0` (no caps).
pub cap_size: f64,
pub legend_label: Option<String>,
}
impl Default for ForestPlot {
fn default() -> Self { Self::new() }
}
impl ForestPlot {
/// Create a forest plot with default settings.
///
/// Defaults: color `"steelblue"`, marker size `6.0`, whisker width `1.5`,
/// null value `0.0`, null line shown.
pub fn new() -> Self {
Self {
rows: vec![],
color: "steelblue".into(),
marker_size: 6.0,
whisker_width: 1.5,
null_value: Some(0.0),
show_null_line: true,
cap_size: 0.0,
legend_label: None,
}
}
/// Add a row (study) with a label, point estimate, and confidence interval bounds.
///
/// Rows are rendered top-to-bottom in the order they are added.
///
/// ```rust,no_run
/// # use kuva::plot::ForestPlot;
/// let forest = ForestPlot::new()
/// .with_row("Study A", 0.50, 0.10, 0.90)
/// .with_row("Study B", -0.30, -0.80, 0.20);
/// ```
pub fn with_row<S: Into<String>>(mut self, label: S, estimate: f64, ci_lower: f64, ci_upper: f64) -> Self {
self.rows.push(ForestRow::new(label, estimate, ci_lower, ci_upper));
self
}
/// Add a weighted row. The marker size scales with `sqrt(weight / max_weight)`.
///
/// ```rust,no_run
/// # use kuva::plot::ForestPlot;
/// let forest = ForestPlot::new()
/// .with_weighted_row("Study A", 0.50, 0.10, 0.90, 5.2)
/// .with_weighted_row("Study B", -0.30, -0.80, 0.20, 3.8);
/// ```
pub fn with_weighted_row<S: Into<String>>(mut self, label: S, estimate: f64, ci_lower: f64, ci_upper: f64, weight: f64) -> Self {
let mut row = ForestRow::new(label, estimate, ci_lower, ci_upper);
row.weight = Some(weight);
self.rows.push(row);
self
}
/// Add a row with a per-row color override.
///
/// ```rust,no_run
/// # use kuva::plot::ForestPlot;
/// let forest = ForestPlot::new()
/// .with_colored_row("Favours treatment", 0.50, 0.10, 0.90, "seagreen")
/// .with_colored_row("Favours control", -0.30, -0.80, 0.20, "tomato");
/// ```
pub fn with_colored_row<S: Into<String>, C: Into<String>>(mut self, label: S, estimate: f64, ci_lower: f64, ci_upper: f64, color: C) -> Self {
let mut row = ForestRow::new(label, estimate, ci_lower, ci_upper);
row.color = Some(color.into());
self.rows.push(row);
self
}
/// Add a row with both a weight and a per-row color override.
pub fn with_weighted_colored_row<S: Into<String>, C: Into<String>>(
mut self, label: S, estimate: f64, ci_lower: f64, ci_upper: f64, weight: f64, color: C,
) -> Self {
let mut row = ForestRow::new(label, estimate, ci_lower, ci_upper);
row.weight = Some(weight);
row.color = Some(color.into());
self.rows.push(row);
self
}
/// Set the point fill and whisker color (CSS color string, default `"steelblue"`).
pub fn with_color<S: Into<String>>(mut self, color: S) -> Self {
self.color = color.into();
self
}
/// Set the base marker half-width in pixels (default `6.0`).
///
/// When weights are present, the actual size is scaled by
/// `sqrt(weight / max_weight)`.
pub fn with_marker_size(mut self, size: f64) -> Self {
self.marker_size = size;
self
}
/// Set the whisker (CI line) stroke width in pixels (default `1.5`).
pub fn with_whisker_width(mut self, width: f64) -> Self {
self.whisker_width = width;
self
}
/// Set the null-effect reference value (default `0.0`).
///
/// A vertical dashed line is drawn at this value when
/// [`show_null_line`](Self::with_show_null_line) is `true`.
pub fn with_null_value(mut self, value: f64) -> Self {
self.null_value = Some(value);
self
}
/// Toggle the null-effect reference line (default `true`).
pub fn with_show_null_line(mut self, show: bool) -> Self {
self.show_null_line = show;
self
}
/// Set the whisker end-cap half-height in pixels (default `0.0`, no caps).
///
/// Set to e.g. `4.0` to add visible serifs at each end of the CI whisker.
pub fn with_cap_size(mut self, size: f64) -> Self {
self.cap_size = size;
self
}
/// Attach a legend label to this forest plot.
pub fn with_legend<S: Into<String>>(mut self, label: S) -> Self {
self.legend_label = Some(label.into());
self
}
}