|
1 | 1 | //! Indicator trace |
2 | 2 |
|
| 3 | +use std::collections::HashMap; |
| 4 | + |
3 | 5 | use plotly_derive::FieldSetter; |
4 | 6 | use serde::Serialize; |
5 | 7 |
|
6 | 8 | use crate::color::Color; |
| 9 | +use crate::layout::{ArrayShow, TicksDirection}; |
7 | 10 | use crate::private::{NumOrString, NumOrStringCollection}; |
8 | 11 | use crate::{ |
9 | | - common::{Domain, Font, Line, PlotType}, |
| 12 | + common::{Domain, ExponentFormat, Font, Line, PlotType, TickFormatStop, TickMode}, |
10 | 13 | Trace, |
11 | 14 | }; |
12 | 15 |
|
@@ -151,34 +154,108 @@ impl Delta { |
151 | 154 |
|
152 | 155 | /// Configures the axis of a gauge (its range and ticks). |
153 | 156 | /// |
154 | | -/// This is a minimal subset of a full Cartesian axis, exposing only the |
155 | | -/// attributes relevant to gauges. |
| 157 | +/// This mirrors the subset of a Cartesian axis that Plotly.js exposes on |
| 158 | +/// `indicator.gauge.axis`: a numeric `range` plus the full tick-styling |
| 159 | +/// machinery. It is intentionally distinct from [`crate::layout::Axis`], which |
| 160 | +/// carries many attributes (grid lines, spikes, range sliders, subplot |
| 161 | +/// anchoring, …) that a gauge axis does not support. |
156 | 162 | #[serde_with::skip_serializing_none] |
157 | 163 | #[derive(Serialize, Clone, Debug, FieldSetter)] |
158 | 164 | pub struct GaugeAxis { |
159 | 165 | /// Sets the range of the gauge axis. |
160 | 166 | range: Option<[f64; 2]>, |
161 | 167 | /// Determines whether or not the gauge axis is visible. |
162 | 168 | visible: Option<bool>, |
163 | | - /// Sets the tick mode of the gauge axis (e.g. `"auto"`, `"linear"`, |
164 | | - /// `"array"`). |
| 169 | + /// Sets the tick mode of the gauge axis. |
165 | 170 | #[serde(rename = "tickmode")] |
166 | | - tick_mode: Option<String>, |
167 | | - /// Sets the number of ticks. |
| 171 | + tick_mode: Option<TickMode>, |
| 172 | + /// Sets the number of ticks (with `tick_mode` [`TickMode::Auto`]). |
168 | 173 | #[serde(rename = "nticks")] |
169 | 174 | n_ticks: Option<usize>, |
170 | | - /// Sets the values at which ticks are drawn (with `tick_mode` `"array"`). |
| 175 | + /// Sets the placement of the first tick (with `tick_mode` |
| 176 | + /// [`TickMode::Linear`]). |
| 177 | + tick0: Option<f64>, |
| 178 | + /// Sets the step between ticks (with `tick_mode` [`TickMode::Linear`]). |
| 179 | + dtick: Option<f64>, |
| 180 | + /// Sets the values at which ticks are drawn (with `tick_mode` |
| 181 | + /// [`TickMode::Array`]). |
171 | 182 | #[serde(rename = "tickvals")] |
172 | 183 | tick_vals: Option<Vec<f64>>, |
173 | | - /// Sets the text displayed at the tick values (with `tick_mode` `"array"`). |
| 184 | + /// Sets the text displayed at the tick values (with `tick_mode` |
| 185 | + /// [`TickMode::Array`]). |
174 | 186 | #[serde(rename = "ticktext")] |
175 | 187 | tick_text: Option<Vec<String>>, |
| 188 | + /// Determines whether ticks are drawn inside or outside the axis line. |
| 189 | + ticks: Option<TicksDirection>, |
| 190 | + /// Sets the tick length (in px). |
| 191 | + #[serde(rename = "ticklen")] |
| 192 | + tick_length: Option<usize>, |
| 193 | + /// Sets the tick width (in px). |
| 194 | + #[serde(rename = "tickwidth")] |
| 195 | + tick_width: Option<usize>, |
| 196 | + /// Sets the tick color. |
| 197 | + #[serde(rename = "tickcolor")] |
| 198 | + #[field_setter(skip)] |
| 199 | + tick_color: Option<Box<dyn Color>>, |
| 200 | + /// Determines whether or not the tick labels are drawn. |
| 201 | + #[serde(rename = "showticklabels")] |
| 202 | + show_tick_labels: Option<bool>, |
| 203 | + /// Sets the font used for the tick labels. |
| 204 | + #[serde(rename = "tickfont")] |
| 205 | + tick_font: Option<Font>, |
| 206 | + /// Sets the angle (in degrees) of the tick labels. |
| 207 | + #[serde(rename = "tickangle")] |
| 208 | + tick_angle: Option<f64>, |
| 209 | + /// Sets a prefix appearing before each tick label. |
| 210 | + #[serde(rename = "tickprefix")] |
| 211 | + tick_prefix: Option<String>, |
| 212 | + /// Determines on which ticks the prefix is shown. |
| 213 | + #[serde(rename = "showtickprefix")] |
| 214 | + show_tick_prefix: Option<ArrayShow>, |
| 215 | + /// Sets a suffix appearing after each tick label. |
| 216 | + #[serde(rename = "ticksuffix")] |
| 217 | + tick_suffix: Option<String>, |
| 218 | + /// Determines on which ticks the suffix is shown. |
| 219 | + #[serde(rename = "showticksuffix")] |
| 220 | + show_tick_suffix: Option<ArrayShow>, |
| 221 | + /// Determines on which ticks the exponent is shown. |
| 222 | + #[serde(rename = "showexponent")] |
| 223 | + show_exponent: Option<ArrayShow>, |
| 224 | + /// Determines the formatting of exponents. |
| 225 | + #[serde(rename = "exponentformat")] |
| 226 | + exponent_format: Option<ExponentFormat>, |
| 227 | + /// Sets the smallest exponent that gets a suffix/prefix (with |
| 228 | + /// `exponent_format` [`ExponentFormat::SI`]/[`ExponentFormat::B`]). |
| 229 | + #[serde(rename = "minexponent")] |
| 230 | + min_exponent: Option<usize>, |
| 231 | + /// Determines whether thousands are separated (e.g. `1,000,000`). |
| 232 | + #[serde(rename = "separatethousands")] |
| 233 | + separate_thousands: Option<bool>, |
| 234 | + /// Sets the tick label formatting rule using d3 formatting |
| 235 | + /// mini-languages. |
| 236 | + #[serde(rename = "tickformat")] |
| 237 | + tick_format: Option<String>, |
| 238 | + /// Sets conditional tick-label formatting rules by range. |
| 239 | + #[serde(rename = "tickformatstops")] |
| 240 | + tick_format_stops: Option<Vec<TickFormatStop>>, |
| 241 | + /// Sets the spacing between tick labels: every n-th label is shown. |
| 242 | + #[serde(rename = "ticklabelstep")] |
| 243 | + tick_label_step: Option<usize>, |
| 244 | + /// Replaces specific tick labels with the provided aliases. |
| 245 | + #[serde(rename = "labelalias")] |
| 246 | + label_alias: Option<HashMap<String, String>>, |
176 | 247 | } |
177 | 248 |
|
178 | 249 | impl GaugeAxis { |
179 | 250 | pub fn new() -> Self { |
180 | 251 | Default::default() |
181 | 252 | } |
| 253 | + |
| 254 | + /// Sets the tick color. |
| 255 | + pub fn tick_color<C: Color>(mut self, color: C) -> Self { |
| 256 | + self.tick_color = Some(Box::new(color)); |
| 257 | + self |
| 258 | + } |
182 | 259 | } |
183 | 260 |
|
184 | 261 | /// Configures the value bar drawn inside a gauge. |
@@ -495,6 +572,85 @@ mod tests { |
495 | 572 | assert_eq!(to_value(trace).unwrap(), expected); |
496 | 573 | } |
497 | 574 |
|
| 575 | + #[test] |
| 576 | + fn serialize_gauge_axis() { |
| 577 | + let axis = GaugeAxis::new() |
| 578 | + .range([0.0, 100.0]) |
| 579 | + .visible(true) |
| 580 | + .tick_mode(TickMode::Array) |
| 581 | + .n_ticks(5) |
| 582 | + .tick0(0.0) |
| 583 | + .dtick(10.0) |
| 584 | + .tick_vals(vec![0.0, 50.0, 100.0]) |
| 585 | + .tick_text(vec![ |
| 586 | + "low".to_string(), |
| 587 | + "mid".to_string(), |
| 588 | + "high".to_string(), |
| 589 | + ]) |
| 590 | + .ticks(TicksDirection::Outside) |
| 591 | + .tick_length(8) |
| 592 | + .tick_width(2) |
| 593 | + .tick_color("gray") |
| 594 | + .show_tick_labels(true) |
| 595 | + .tick_font(Font::new()) |
| 596 | + .tick_angle(45.0) |
| 597 | + .tick_prefix("$") |
| 598 | + .show_tick_prefix(ArrayShow::All) |
| 599 | + .tick_suffix("k") |
| 600 | + .show_tick_suffix(ArrayShow::Last) |
| 601 | + .show_exponent(ArrayShow::First) |
| 602 | + .exponent_format(ExponentFormat::SI) |
| 603 | + .min_exponent(3) |
| 604 | + .separate_thousands(true) |
| 605 | + .tick_format(".2f") |
| 606 | + .tick_label_step(2); |
| 607 | + |
| 608 | + let expected = json!({ |
| 609 | + "range": [0.0, 100.0], |
| 610 | + "visible": true, |
| 611 | + "tickmode": "array", |
| 612 | + "nticks": 5, |
| 613 | + "tick0": 0.0, |
| 614 | + "dtick": 10.0, |
| 615 | + "tickvals": [0.0, 50.0, 100.0], |
| 616 | + "ticktext": ["low", "mid", "high"], |
| 617 | + "ticks": "outside", |
| 618 | + "ticklen": 8, |
| 619 | + "tickwidth": 2, |
| 620 | + "tickcolor": "gray", |
| 621 | + "showticklabels": true, |
| 622 | + "tickfont": {}, |
| 623 | + "tickangle": 45.0, |
| 624 | + "tickprefix": "$", |
| 625 | + "showtickprefix": "all", |
| 626 | + "ticksuffix": "k", |
| 627 | + "showticksuffix": "last", |
| 628 | + "showexponent": "first", |
| 629 | + "exponentformat": "SI", |
| 630 | + "minexponent": 3, |
| 631 | + "separatethousands": true, |
| 632 | + "tickformat": ".2f", |
| 633 | + "ticklabelstep": 2, |
| 634 | + }); |
| 635 | + |
| 636 | + assert_eq!(to_value(axis).unwrap(), expected); |
| 637 | + } |
| 638 | + |
| 639 | + #[test] |
| 640 | + fn serialize_gauge_axis_label_alias_and_format_stops() { |
| 641 | + use std::collections::HashMap; |
| 642 | + |
| 643 | + let mut alias = HashMap::new(); |
| 644 | + alias.insert("0".to_string(), "min".to_string()); |
| 645 | + let axis = GaugeAxis::new() |
| 646 | + .label_alias(alias) |
| 647 | + .tick_format_stops(vec![TickFormatStop::new()]); |
| 648 | + |
| 649 | + let v = to_value(axis).unwrap(); |
| 650 | + assert_eq!(v["labelalias"], json!({"0": "min"})); |
| 651 | + assert!(v["tickformatstops"].is_array()); |
| 652 | + } |
| 653 | + |
498 | 654 | #[test] |
499 | 655 | fn serialize_indicator_legend_and_uirevision() { |
500 | 656 | let trace = Indicator::new(1.0) |
|
0 commit comments