diff --git a/examples/basic_charts/src/main.rs b/examples/basic_charts/src/main.rs index 89ba5c36..d410665b 100644 --- a/examples/basic_charts/src/main.rs +++ b/examples/basic_charts/src/main.rs @@ -8,7 +8,8 @@ use plotly::{ Marker, Mode, Orientation, Pattern, PatternShape, }, layout::{ - Annotation, Axis, AxisRange, BarMode, CategoryOrder, Layout, LayoutGrid, Legend, + AngularAxis, Annotation, Axis, AxisRange, BarMode, CategoryOrder, Layout, LayoutGrid, + LayoutPolar, Legend, PolarAxisAttributes, PolarAxisTicks, PolarDirection, RadialAxis, TicksDirection, TraceOrder, }, sankey::{Line as SankeyLine, Link, Node}, @@ -119,6 +120,29 @@ fn polar_scatter_plot(show: bool, file_name: &str) { let mut plot = Plot::new(); plot.add_trace(trace); + let ticks = PolarAxisTicks::new().tick_color("#222222"); + + let axis_attributes = PolarAxisAttributes::new() + .grid_color("#888888") + .ticks(ticks); + + let radial_axis = RadialAxis::new() + .title("My Title") + .axis_attributes(axis_attributes.clone()); + + let angular_axis = AngularAxis::new() + .direction(PolarDirection::Clockwise) + .rotation(45.0) + .axis_attributes(axis_attributes); + + let layout_polar = LayoutPolar::new() + .bg_color("#eeeeee") + .radial_axis(radial_axis) + .angular_axis(angular_axis); + + let layout = Layout::new().polar(layout_polar); + plot.set_layout(layout); + let path = write_example_to_html(&plot, file_name); if show { plot.show_html(path); diff --git a/plotly/src/layout/mod.rs b/plotly/src/layout/mod.rs index 0424b593..69116156 100644 --- a/plotly/src/layout/mod.rs +++ b/plotly/src/layout/mod.rs @@ -19,6 +19,7 @@ mod grid; mod legend; mod mapbox; mod modes; +mod polar; mod rangebreaks; mod scene; mod shape; @@ -42,6 +43,11 @@ pub use self::mapbox::{Center, Mapbox, MapboxStyle}; pub use self::modes::{ AspectMode, BarMode, BarNorm, BoxMode, ClickMode, UniformTextMode, ViolinMode, WaterfallMode, }; +pub use self::polar::{ + AngularAxis, AngularAxisType, AutoRange, AutoRangeOptions, AutoTypeNumbers, AxisLayer, + GridShape, Hole, LayoutPolar, MinorLogLabels, PolarAxisAttributes, PolarAxisTicks, + PolarDirection, PolarTickMode, RadialAxis, RadialAxisType, ThetaUnit, +}; pub use self::rangebreaks::RangeBreak; pub use self::scene::{ AspectRatio, Camera, CameraCenter, DragMode, DragMode3D, Eye, HoverMode, LayoutScene, @@ -330,7 +336,7 @@ pub struct LayoutFields { // ternary: Option, scene: Option, geo: Option, - // polar: Option, + polar: Option, annotations: Option>, shapes: Option>, #[serde(rename = "newshape")] diff --git a/plotly/src/layout/polar.rs b/plotly/src/layout/polar.rs new file mode 100644 index 00000000..64ac0dfd --- /dev/null +++ b/plotly/src/layout/polar.rs @@ -0,0 +1,628 @@ +use std::{fmt::Display, num::NonZeroU8}; + +use plotly_derive::FieldSetter; +use serde::Serialize; + +use crate::{ + color::Color, + common::{DashType, ExponentFormat, Font, TickFormatStop, Ticks, Title}, + layout::{ArrayShow, CategoryOrder, RangeMode}, + private::NumOrString, +}; + +/// The layout for a polar (circular) plot, consisting of a radial axis R +/// (distance from the center) and an angular axis Theta (distance around the +/// circumference). See [`ScatterPolar`](crate::traces::ScatterPolar) for +/// details on traces. +#[derive(Clone, Debug, FieldSetter, Serialize)] +pub struct LayoutPolar { + /// Sets the angular span of the polar subplot using two angles (in + /// degrees). Sectors are assumed to be spanned in the counterclockwise + /// direction, with `0` corresponding to the rightmost limit of the polar + /// subplot. + sector: Option<[f64; 2]>, + /// Sets the fraction of the radius to remove from the center of the polar + /// subplot. The value wrapped by the [`Hole`] must be between 0.0 and 1.0. + hole: Option, + /// Sets the background color of the polar subplot. + #[serde(rename = "bgcolor")] + bg_color: Option>, + /// The attributes describing the radial axis of the plot. + #[serde(rename = "radialaxis")] + radial_axis: Option, + /// The attributes describing the angular axis of the plot. + #[serde(rename = "angularaxis")] + angular_axis: Option, + /// When the axis type is set to [`RadialAxisType::Category`] or + /// [`AngularAxisType::Category`] the [`GridShape`] determines whether the + /// radial axis grid lines and angular axis line are drawn as circular + /// sectors or as linear (polygon) sectors. + #[serde(rename = "gridshape")] + grid_shape: Option, + /// Controls the persistence of user-driven changes in the axis `range`, + /// `autorange`, `angle`, and `title` when in the `editable: true` + /// configuration. + #[serde(rename = "uirevision")] + ui_revision: Option, +} + +impl LayoutPolar { + /// Create a new layout with default settings. + pub fn new() -> Self { + Default::default() + } +} + +/// Describes the radial axis of the plot, extending from the center to the +/// periphery. +#[derive(Clone, Debug, FieldSetter, Serialize)] +pub struct RadialAxis { + visible: Option, + /// Explicitly set the [`RadialAxisType`]. By default, Plotly attempts to + /// infer the axis type from the data in the plot's traces. + #[serde(rename = "type")] + axis_type: Option, + /// Setting [`AutoTypeNumbers::Strict`](AutoTypeNumbers) prevents Plotly + /// from coercing numeric strings in trace data into numbers. This may + /// affect the inferred [`axis_type`](RadialAxis::axis_type). + #[serde(rename = "autotypenumbers")] + auto_type_numbers: Option, + /// Sets the autorange options. See [`AutoRangeOptions`]. + #[serde(rename = "autorangeoptions")] + auto_range_options: Option, + #[serde(rename = "autorange")] + auto_range: Option, + /// Set the [`RangeMode`]: + /// + /// - [`RangeMode::Normal`]: The range is computed relative to the extremes + /// of the input data. + /// - [`RangeMode::ToZero`]: The range extends to 0, regardless of the input + /// data. + /// - [`RangeMode::NonNegative`]: The range is non-negative, regardless of + /// the input data. + #[serde(rename = "rangemode")] + range_mode: Option, + /// Determines the minimum range of this axis. + #[serde(rename = "minallowed")] + min_allowed: Option, + /// Determines the maximum range of this axis. + #[serde(rename = "maxallowed")] + max_allowed: Option, + /// Sets the range of this axis by supplying minimum and maximum values. If + /// the [`axis_type`](RadialAxis::axis_type) is + /// [`Log`](RadialAxisType::Log), then input the log of your desired + /// range. For example, to set the range from 1 to 100, set the log + /// range from 0 to 2. If the [`axis_type`](RadialAxis::axis_type) is + /// [`Date`](RadialAxisType::Date), then set the range with date + /// strings. If the [`axis_type`](RadialAxis::axis_type) + /// is [`Category`](RadialAxisType::Category), then supply integers, which + /// are applied serially to each category. + range: Option<[numorstring; 2]>, + #[serde(rename = "categoryorder")] + category_order: Option, + /// Supply categories for this axis. The order is determined by + /// [`category_order`](RadialAxis::category_order). + #[serde(rename = "categoryarray")] + category_array: Option>, + /// Sets the angle (in degrees) from which the radial axis is drawn. By + /// default, the radial axis line is drawn on the theta=0 line, pointing to + /// the right. If you set a [`sector`](LayoutPolar::sector), the line will + /// be drawn on the first angle of the sector. + angle: Option, + /// When [`tick_angle`](PolarAxisTicks::tick_angle) is unspecified, it will + /// automatically be set to the first angle in this [`Vec`] that is large + /// enough to prevent label overlap. + #[serde(rename = "autotickangles")] + auto_tick_angles: Option>, + /// Determines on which side of the radial axis line the ticks and tick + /// labels appear. + #[serde(rename = "side")] + tick_side: Option, + /// Set the axis [`Title`]. + title: Option, + #[serde(rename = "hoverformat")] + hover_format: Option<string>, + /// Controls the persistence of user-driven changes in radial axis `range`, + /// `autorange`, `angle`, and `title` when in the `editable: true` + /// configuration. Defaults to [`ui_revision`](LayoutPolar::ui_revision). + #[serde(rename = "uirevision")] + ui_revision: Option<numorstring>, + /// Sets the axis attributes. See [`PolarAxisAttributes`]. + #[serde(flatten)] + axis_attributes: Option<polaraxisattributes>, +} + +impl RadialAxis { + /// Create a new radial axis with default settings. + pub fn new() -> Self { + Default::default() + } +} + +/// Describes the angular (circular) axis of the plot. +#[derive(Clone, Debug, FieldSetter, Serialize)] +pub struct AngularAxis { + visible: Option<bool>, + /// Explicitly set the [`AngularAxisType`]. By default, Plotly attempts to + /// infer the axis type from the data in the plot's traces. + #[serde(rename = "type")] + axis_type: Option<angularaxistype>, + /// Setting [`AutoTypeNumbers::Strict`] prevents Plotly from coercing + /// numeric strings in trace data into numbers. This may affect the inferred + /// [`axis_type`](AngularAxis::axis_type). + #[serde(rename = "autotypenumbers")] + auto_type_numbers: Option<autotypenumbers>, + #[serde(rename = "categoryorder")] + category_order: Option<categoryorder>, + /// Supply categories for this axis. The order is determined by + /// [`category_order`](AngularAxis::category_order). + #[serde(rename = "categoryarray")] + category_array: Option<vec<numorstring>>, + /// Set the units for the angular axis. See [`ThetaUnit`]. + #[serde(rename = "thetaunit")] + theta_unit: Option<thetaunit>, + /// If the [`axis_type`](AngularAxis::axis_type) is + /// [`AngularAxisType::Category`], this value will be used for the angular + /// period. + period: Option<usize>, + /// Sets the direction corresponding to positive angles. See + /// [`PolarDirection`]. + direction: Option<polardirection>, + /// Sets the start position (in degrees) of the angular axis. By default, + /// polar subplots with [`direction`](AngularAxis::direction) set to + /// [`PolarDirection::Counterclockwise`] will get a `rotation` of `0`, which + /// corresponds to due East (like what mathematicians prefer). Polar + /// subplots with [`direction`](AngularAxis::direction) set to + /// [`PolarDirection::Clockwise`] will get a `rotation` of `90`, which + /// corresponds to due North (like on a compass). + rotation: Option<f64>, + #[serde(rename = "hoverformat")] + hover_format: Option<string>, + /// Controls the persistence of user-driven changes in angular axis + /// `rotation` when in the `editable: true` configuration. Defaults to + /// [`ui_revision`](LayoutPolar::ui_revision). + #[serde(rename = "uirevision")] + ui_revision: Option<numorstring>, + /// Sets the axis attributes. See [`PolarAxisAttributes`]. + #[serde(flatten)] + axis_attributes: Option<polaraxisattributes>, +} + +impl AngularAxis { + /// Create a new angular axis with default settings. + pub fn new() -> Self { + Default::default() + } +} + +/// Provides styles for an axis in [`LayoutPolar`]. May be applied to +/// [`RadialAxis`] or [`AngularAxis`]. +#[derive(Clone, Debug, FieldSetter, Serialize)] +pub struct PolarAxisAttributes { + color: Option<box<dyn Color>>, + #[serde(rename = "showline")] + show_line: Option<bool>, + #[serde(rename = "linecolor")] + line_color: Option<box<dyn Color>>, + /// Set the width of the axis line in px. + #[serde(rename = "linewidth")] + line_width: Option<usize>, + #[serde(rename = "showgrid")] + show_grid: Option<bool>, + #[serde(rename = "gridcolor")] + grid_color: Option<box<dyn Color>>, + /// Set the width of the grid lines in px. + #[serde(rename = "gridwidth")] + grid_width: Option<usize>, + #[serde(rename = "griddash")] + grid_dash: Option<dashtype>, + #[serde(flatten)] + ticks: Option<polaraxisticks>, +} + +impl PolarAxisAttributes { + /// Create a new set of axis attributes with default settings. + pub fn new() -> Self { + Default::default() + } +} + +/// Provides styles for the axis ticks in [`LayoutPolar`]. May be applied to +/// [`RadialAxis`] or [`AngularAxis`]. +#[derive(Clone, Debug, FieldSetter, Serialize)] +pub struct PolarAxisTicks { + #[serde(flatten)] + tick_mode: Option<polartickmode>, + ticks: Option<ticks>, + /// Set the length of the ticks in px. + #[serde(rename = "ticklen")] + tick_length: Option<usize>, + /// Set the width of the ticks in px. + #[serde(rename = "tickwidth")] + tick_width: Option<usize>, + #[serde(rename = "tickcolor")] + tick_color: Option<box<dyn Color>>, + /// A label will be applied to every `n` ticks, where `n` is the value of + /// [`tick_label_step`](PolarAxisTicks::tick_label_step). This setting will + /// be overridden by any text explicitly supplied in + /// [`PolarTickMode::Array`]. + #[serde(rename = "ticklabelstep")] + tick_label_step: Option<nonzerou8>, + #[serde(rename = "showticklabels")] + show_tick_labels: Option<bool>, + /// Applies a label alias to tick or hover labels matching specific + /// patterns. For example using { US: \'USA\', CA: \'Canada\' } changes US + /// to USA, and CA to Canada. The labels we would have shown must match the + /// keys exactly, after adding any tick_prefix or tick_suffix. `label_alias` + /// can be used with any axis type, and both keys and values can include + /// html-like tags or MathJax. + #[serde(rename = "labelalias")] + label_alias: Option<string>, + #[serde(rename = "minorloglabels")] + minor_log_labels: Option<minorloglabels>, + #[serde(rename = "showtickprefix")] + show_tick_prefix: Option<arrayshow>, + #[serde(rename = "tickprefix")] + tick_prefix: Option<string>, + #[serde(rename = "showticksuffix")] + show_tick_suffix: Option<arrayshow>, + #[serde(rename = "ticksuffix")] + tick_suffix: Option<string>, + #[serde(rename = "showexponent")] + show_exponent: Option<arrayshow>, + #[serde(rename = "exponentformat")] + exponent_format: Option<exponentformat>, + /// For an [`exponent_format`](PolarAxisTicks::exponent_format) of + /// [`SI`](ExponentFormat::SI), hide the SI prefix if the exponent is below + /// this number. + #[serde(rename = "minexponent")] + min_exponent: Option<u8>, + #[serde(rename = "separatethousands")] + separate_thousands: Option<bool>, + #[serde(rename = "tickfont")] + tick_font: Option, + #[serde(rename = "tickangle")] + tick_angle: Option<f64>, + #[serde(rename = "tickformat")] + tick_format: Option<string>, + #[serde(rename = "tickformatstops")] + tick_format_stops: Option<tickformatstop>, + /// Sets the layer on which the axis is displayed relative to the traces. + /// When combined with a [`clip_on_axis`](crate::ScatterPolar::clip_on_axis) + /// value of `false`, markers and text nodes can be set to appear above the + /// axis. + layer: Option<axislayer>, +} + +impl PolarAxisTicks { + /// Create a new tick layout with default settings. + pub fn new() -> Self { + Default::default() + } +} + +/// The type of the angular (circular) axis of a polar plot. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum AngularAxisType { + /// Infer the axis type from the data in its traces. + #[serde(rename = "-")] + Default, + Linear, + Category, +} + +/// Determines whether or not the range of this axis is computed in relation to +/// the input data. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum AutoRange { + /// Use autorange only to set the maximum value. + Max, + /// Use autorange only to set the maximum value on a reversed axis. + #[serde(rename = "max reversed")] + MaxReversed, + /// Use autorange only to set the minimum value. + Min, + /// Use autorange only to set the minimum value on a reversed axis. + #[serde(rename = "min reversed")] + MinReversed, + /// Reverse the axis, and use autorange to set both minimum and maximum + /// values. + Reversed, + /// If true, use autorange to set both minimum and maximum values. If false, + /// do not use autorange to set either value. + #[serde(untagged)] + Bool(bool), +} + +/// Controls how the maximum and minimum values are calculated by autorange. +#[derive(Clone, Debug, FieldSetter, Serialize)] +pub struct AutoRangeOptions { + /// Use this value as the autorange minimum. + #[serde(rename = "minallowed")] + min_allowed: Option<numorstring>, + /// Use this value as the autorange maximum. + #[serde(rename = "maxallowed")] + max_allowed: Option<numorstring>, + /// Clip the autorange minimum if it goes beyond this value. If + /// [`min_allowed`](AutoRangeOptions::min_allowed) is also specified, it + /// will take precedence. + #[serde(rename = "clipmin")] + clip_min: Option<numorstring>, + /// Clip the autorange maximum if it goes beyond this value. If + /// [`max_allowed`](AutoRangeOptions::max_allowed) is also specified, it + /// will take precedence. + #[serde(rename = "clipmax")] + clip_max: Option<numorstring>, + include: Option<vec<numorstring>>, +} + +impl AutoRangeOptions { + /// Create a new set of autorange options with default settings. + pub fn new() -> Self { + Default::default() + } +} + +/// Setting [`AutoTypeNumbers::Strict`] prevents Plotly from coercing numeric +/// strings in trace data into numbers. This may affect the inferred +/// [`axis_type`](RadialAxis::axis_type). Coercing/converting is the default +/// behavior. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum AutoTypeNumbers { + #[serde(rename = "convert types")] + Convert, + Strict, +} + +/// Whether to layer the axis above or below its traces. +#[derive(Clone, Debug, Serialize)] +pub enum AxisLayer { + #[serde(rename = "above traces")] + Above, + #[serde(rename = "below traces")] + Below, +} + +/// When the axis type is set to [`RadialAxisType::Category`] or +/// [`AngularAxisType::Category`], the [`GridShape`] determines if the radial +/// axis grid lines and angular axis line are drawn as circular sectors or as +/// linear (polygon) sectors. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum GridShape { + Circular, + Linear, +} + +/// Removes material from the center of a polar plot, by supplying a percentage +/// of the radial axis to eliminate. The supplied value must be between `0.0` +/// and `1.0`. +#[derive(Clone, Debug, Serialize)] +pub struct Hole(f64); + +impl Hole { + /// Create a new hole from the given value. + pub fn new(value: f64) -> Result<self, Box<dyn std::error::Error>> { + if (0.0..=1.0).contains(&value) { + Ok(Self(value)) + } else { + Err(format!("The value for a LayoutPolar angular axis Hole must be between 0.0 and 1.0. Given value: {value}").into()) + } + } + + /// Return the inner value of the hole. + pub fn inner(&self) -> f64 { + self.0 + } +} + +impl AsRef<f64> for Hole { + fn as_ref(&self) -> &f64 { + &self.0 + } +} + +impl Display for Hole { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Specify how minor log labels are displayed. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum MinorLogLabels { + #[serde(rename = "small digits")] + SmallDigits, + Complete, + None, +} + +/// A direction around the angular axis of a polar plot. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum PolarDirection { + Clockwise, + Counterclockwise, +} + +/// Sets the tick mode for this axis. +/// +/// - [`Auto`](PolarTickMode::Auto): the number of ticks is set via +/// [`n_ticks`](PolarTickMode::Auto::n_ticks). +/// - [`Linear`](PolarTickMode::Linear): the placement of the ticks is +/// determined by a starting position +/// ([`tick_0`](PolarTickMode::Linear::tick_0)), and a tick step +/// ([`d_tick`](PolarTickMode::Linear::d_tick)). +/// - [`Array`](PolarTickMode::Array): the placement of the ticks is set via +/// [`tick_values`](PolarTickMode::Array::tick_values), and the tick text is +/// set via [`tick_text`](PolarTickMode::Array::tick_text). +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +#[serde(tag = "tickmode")] +pub enum PolarTickMode { + Auto { + /// Specifies the maximum number of ticks for this axis. The actual + /// number of ticks will be chosen automatically to be less than or + /// equal to [`n_ticks`](PolarTickMode::Auto::n_ticks). + #[serde(rename = "nticks")] + n_ticks: Option<usize>, + }, + + Linear { + /// Sets the placement of the first tick on this axis. If the + /// [`axis_type`](RadialAxis::axis_type) is [`RadialAxisType::Log`] then + /// you must take the log of your starting tick. For example, to set the + /// starting tick to 100, set [`tick_0`](PolarTickMode::Linear::tick_0) + /// to 2 (except when [`d_tick`](PolarTickMode::Linear::d_tick) = *L<f>* + /// (see [`d_tick`](PolarTickMode::Linear::d_tick) for more + /// information). If the [`axis_type`](RadialAxis::axis_type) is + /// [`RadialAxisType::Date`], it should be a date string. If the + /// [`axis_type`](RadialAxis::axis_type) is [`RadialAxisType::Category`] + /// or [`AngularAxisType::Category`] then supply an integer, which will + /// be incremented with each category. + #[serde(rename = "tick0")] + tick_0: Option<numorstring>, + /// Sets the step size between ticks on this axis. Must be a positive + /// number or a string suitable for *log* or *date* axes. If the axis + /// type is set to [`RadialAxisType::Log`], ticks will be set every + /// 10^(n*d_tick) where n is the tick number. For example, to set a tick + /// mark at 1, 10, 100, 1000, ... set d_tick to 1. To set tick marks at + /// 1, 100, 10000, ... set dtick to 2. To set tick marks at 1, 5, 25, + /// 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433. + /// + /// [`RadialAxisType::Log`] has several special values: + /// + /// - *L<f>*, where `f` is a positive number, gives ticks linearly + /// spaced in value (but not position). For example, + /// [`tick_0`](PolarTickMode::Linear::tick_0) = 0.1, + /// [`d_tick`](PolarTickMode::Linear::d_tick) = *L0.5* will put ticks + /// at 0.1, 0.6, 1.1, 1.6, ... + /// - To show powers of 10 plus small digits between, use *D1* (all + /// digits) or *D2* (only 2 and 5). + /// [`tick_0`](PolarTickMode::Linear::tick_0) is ignored for *D1* and + /// *D2*. If the axis `type` is [`RadialAxisType::Date`], then you + /// must convert the time to milliseconds. For example, to set the + /// interval between ticks to one day, set + /// [`d_tick`](PolarTickMode::Linear::d_tick) to 86400000.0. + /// + /// [`RadialAxisType::Date`] also has special values: + /// + /// - *M<n>* gives ticks spaced by a number of months, where `n` is a + /// positive integer. To set ticks on the 15th of every third month, + /// set [`tick_0`](PolarTickMode::Linear::tick_0) to *2000年01月15日* and + /// [`d_tick`](PolarTickMode::Linear::d_tick) to *M3*. To set ticks + /// every 4 years, set [`d_tick`](PolarTickMode::Linear::d_tick) to + /// *M48* + #[serde(rename = "dtick")] + d_tick: Option<numorstring>, + }, + + Array { + /// Sets the values at which ticks on this axis appear. + #[serde(rename = "tickvals")] + tick_values: Option<vec<f64>>, + /// Sets the text associated with each value in + /// [`tick_values`](PolarTickMode::Array::tick_values). + #[serde(rename = "ticktext")] + tick_text: Option<vec<string>>, + }, +} + +/// The type of the radial (center outward) axis of a polar plot. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum RadialAxisType { + #[serde(rename = "-")] + Default, + Linear, + Log, + Date, + Category, +} + +/// Specify the units for the angular axis of a polar plot. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ThetaUnit { + Degrees, + Radians, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{common::Mode, Layout, Plot, ScatterPolar}; + + // The focus of the test is serialization, so we test all options for + // [`LayoutPolar`], even though some of those options would normally be mutually + // exclusive. + #[test] + fn serialize_layout_polar() { + let ticks = PolarAxisTicks::new() + .tick_mode(PolarTickMode::Auto { n_ticks: None }) + .tick_color("#dddddd") + .tick_font(Font::new().color("#eeeeee")); + + let axis_attributes = PolarAxisAttributes::new() + .color("#111111") + .show_line(true) + .line_color("#ffff00") + .line_width(5) + .show_grid(true) + .grid_color("#444444") + .grid_width(2) + .grid_dash(DashType::Solid) + .ticks(ticks); + + let radial_axis = RadialAxis::new() + .visible(true) + .axis_type(RadialAxisType::Linear) + .auto_type_numbers(AutoTypeNumbers::Strict) + .auto_range_options(AutoRangeOptions::new().min_allowed(1)) + .auto_range(AutoRange::Bool(true)) + .range_mode(RangeMode::Normal) + .min_allowed(0_u32) + .max_allowed(105_u32) + .range([5.into(), 100.into()]) + .category_order(CategoryOrder::Trace) + .category_array(vec!["category 1".into(), "category 2".into()]) + .angle(0.0) + .auto_tick_angles(vec![0.0, 12.2, 30.85]) + .tick_side(PolarDirection::Counterclockwise) + .title("My Title") + .hover_format("%{label}: <br>Popularity: %{percent} </br> %{text}") + .axis_attributes(axis_attributes.clone()); + + let angular_axis = AngularAxis::new() + .visible(true) + .axis_type(AngularAxisType::Default) + .auto_type_numbers(AutoTypeNumbers::Convert) + .category_order(CategoryOrder::CategoryAscending) + .category_array(vec!["category 3".into(), "category 4".into()]) + .theta_unit(ThetaUnit::Radians) + .period(10) + .direction(PolarDirection::Counterclockwise) + .rotation(5.0) + .hover_format("GDP: %{x} <br>Life Expectancy: %{y}") + .axis_attributes(axis_attributes); + + let layout_polar = LayoutPolar::new() + .sector([0.0, 270.0]) + .hole(Hole::new(0.2).unwrap()) + .bg_color("#dddddd") + .radial_axis(radial_axis) + .angular_axis(angular_axis) + .grid_shape(GridShape::Circular); + + let layout = Layout::new().polar(layout_polar); + let json = serde_json::to_string(&layout).unwrap(); + + let expected = r##"{"polar":{"sector":[0.0,270.0],"hole":0.2,"bgcolor":"#dddddd","radialaxis":{"visible":true,"type":"linear","autotypenumbers":"strict","autorangeoptions":{"minallowed":1,"maxallowed":null,"clipmin":null,"clipmax":null,"include":null},"autorange":true,"rangemode":"normal","minallowed":0,"maxallowed":105,"range":[5,100],"categoryorder":"trace","categoryarray":["category 1","category 2"],"angle":0.0,"autotickangles":[0.0,12.2,30.85],"side":"counterclockwise","title":{"text":"My Title"},"hoverformat":"%{label}: <br>Popularity: %{percent} </br> %{text}","uirevision":null,"color":"#111111","showline":true,"linecolor":"#ffff00","linewidth":5,"showgrid":true,"gridcolor":"#444444","gridwidth":2,"griddash":"solid","tickmode":"auto","nticks":null,"ticks":null,"ticklen":null,"tickwidth":null,"tickcolor":"#dddddd","ticklabelstep":null,"showticklabels":null,"labelalias":null,"minorloglabels":null,"showtickprefix":null,"tickprefix":null,"showticksuffix":null,"ticksuffix":null,"showexponent":null,"exponentformat":null,"minexponent":null,"separatethousands":null,"tickfont":{"color":"#eeeeee"},"tickangle":null,"tickformat":null,"tickformatstops":null,"layer":null},"angularaxis":{"visible":true,"type":"-","autotypenumbers":"convert types","categoryorder":"category ascending","categoryarray":["category 3","category 4"],"thetaunit":"radians","period":10,"direction":"counterclockwise","rotation":5.0,"hoverformat":"GDP: %{x} <br>Life Expectancy: %{y}","uirevision":null,"color":"#111111","showline":true,"linecolor":"#ffff00","linewidth":5,"showgrid":true,"gridcolor":"#444444","gridwidth":2,"griddash":"solid","tickmode":"auto","nticks":null,"ticks":null,"ticklen":null,"tickwidth":null,"tickcolor":"#dddddd","ticklabelstep":null,"showticklabels":null,"labelalias":null,"minorloglabels":null,"showtickprefix":null,"tickprefix":null,"showticksuffix":null,"ticksuffix":null,"showexponent":null,"exponentformat":null,"minexponent":null,"separatethousands":null,"tickfont":{"color":"#eeeeee"},"tickangle":null,"tickformat":null,"tickformatstops":null,"layer":null},"gridshape":"circular","uirevision":null}}"##; + + assert_eq!(json, expected); + } +} </div><div class="naked_ctrl"> <form action="/index.cgi/contrast" method="get" name="gate"> <p><a href="http://altstyle.alfasado.net">AltStyle</a> によって変換されたページ <a href="https://patch-diff.githubusercontent.com/raw/plotly/plotly.rs/pull/355.diff">(->オリジナル)</a> / <label>アドレス: <input type="text" name="naked_post_url" value="https://patch-diff.githubusercontent.com/raw/plotly/plotly.rs/pull/355.diff" size="22" /></label> <label>モード: <select name="naked_post_mode"> <option value="default">デフォルト</option> <option value="speech">音声ブラウザ</option> <option value="ruby">ルビ付き</option> <option value="contrast" selected="selected">配色反転</option> <option value="larger-text">文字拡大</option> <option value="mobile">モバイル</option> </select> <input type="submit" value="表示" /> </p> </form> </div>