plotly/layout/
shape.rs

1use plotly_derive::FieldSetter;
2use serde::Serialize;
3
4use crate::color::Color;
5use crate::common::DashType;
6use crate::private::NumOrString;
7
8#[derive(Serialize, Debug, Clone)]
9#[serde(rename_all = "lowercase")]
10pub enum ShapeType {
11    Circle,
12    Rect,
13    Path,
14    Line,
15}
16
17#[derive(Serialize, Debug, Clone)]
18#[serde(rename_all = "lowercase")]
19pub enum ShapeLayer {
20    Below,
21    Above,
22}
23
24#[derive(Serialize, Debug, Clone)]
25#[serde(rename_all = "lowercase")]
26pub enum ShapeSizeMode {
27    Scaled,
28    Pixel,
29}
30
31#[serde_with::skip_serializing_none]
32#[derive(Serialize, Debug, Clone, FieldSetter)]
33pub struct ShapeLine {
34    /// Sets the line color.
35    color: Option<Box<dyn Color>>,
36    /// Sets the line width (in px).
37    width: Option<f64>,
38    /// Sets the dash style of lines. Set to a dash type string ("solid", "dot",
39    /// "dash", "longdash", "dashdot", or "longdashdot") or a dash length
40    /// list in px (eg "5px,10px,2px,2px").
41    dash: Option<DashType>,
42}
43
44impl ShapeLine {
45    pub fn new() -> Self {
46        Default::default()
47    }
48}
49
50#[derive(Serialize, Debug, Clone)]
51#[serde(rename_all = "lowercase")]
52pub enum FillRule {
53    EvenOdd,
54    NonZero,
55}
56
57#[serde_with::skip_serializing_none]
58#[derive(Serialize, Debug, Clone, FieldSetter)]
59pub struct Shape {
60    /// Determines whether or not this shape is visible.
61    visible: Option<bool>,
62    #[field_setter(skip)]
63    r#type: Option<ShapeType>,
64    /// Specifies whether shapes are drawn below or above traces.
65    layer: Option<ShapeLayer>,
66    /// Sets the shape's x coordinate axis. If set to an x axis id (e.g. "x" or
67    /// "x2"), the `x` position refers to an x coordinate. If set to
68    /// "paper", the `x` position refers to the distance from the left side
69    /// of the plotting area in normalized coordinates where "0" ("1")
70    /// corresponds to the left (right) side. If the axis `type` is "log", then
71    /// you must take the log of your desired range. If the axis `type` is
72    /// "date", then you must convert the date to unix time in milliseconds.
73    #[serde(rename = "xref")]
74    x_ref: Option<String>,
75    /// Sets the shapes's sizing mode along the x axis. If set to "scaled",
76    /// `x0`, `x1` and x coordinates within `path` refer to data values on
77    /// the x axis or a fraction of the plot area's width (`xref` set to
78    /// "paper"). If set to "pixel", `xanchor` specifies the x position
79    /// in terms of data or plot fraction but `x0`, `x1` and x coordinates
80    /// within `path` are pixels relative to `xanchor`. This way, the shape
81    /// can have a fixed width while maintaining a position relative to data
82    /// or plot fraction.
83    #[serde(rename = "xsizemode")]
84    x_size_mode: Option<ShapeSizeMode>,
85    /// Only relevant in conjunction with `xsizemode` set to "pixel". Specifies
86    /// the anchor point on the x axis to which `x0`, `x1` and x coordinates
87    /// within `path` are relative to. E.g. useful to attach a pixel sized
88    /// shape to a certain data value. No effect when `xsizemode` not set to
89    /// "pixel".
90    #[serde(rename = "xanchor")]
91    x_anchor: Option<NumOrString>,
92    /// Sets the shape's starting x position. See `type` and `xsizemode` for
93    /// more info.
94    x0: Option<NumOrString>,
95    /// Sets the shape's end x position. See `type` and `xsizemode` for more
96    /// info.
97    x1: Option<NumOrString>,
98    /// Sets the annotation's y coordinate axis. If set to an y axis id (e.g.
99    /// "y" or "y2"), the `y` position refers to an y coordinate If set to
100    /// "paper", the `y` position refers to the distance from the bottom of
101    /// the plotting area in normalized coordinates where "0" ("1")
102    /// corresponds to the bottom (top).
103    #[serde(rename = "yref")]
104    y_ref: Option<String>,
105    /// Sets the shapes's sizing mode along the y axis. If set to "scaled",
106    /// `y0`, `y1` and y coordinates within `path` refer to data values on
107    /// the y axis or a fraction of the plot area's height (`yref` set to
108    /// "paper"). If set to "pixel", `yanchor` specifies the y position
109    /// in terms of data or plot fraction but `y0`, `y1` and y coordinates
110    /// within `path` are pixels relative to `yanchor`. This way, the shape
111    /// can have a fixed height while maintaining a position relative to
112    /// data or plot fraction.
113    #[serde(rename = "ysizemode")]
114    y_size_mode: Option<ShapeSizeMode>,
115    /// Only relevant in conjunction with `ysizemode` set to "pixel". Specifies
116    /// the anchor point on the y axis to which `y0`, `y1` and y coordinates
117    /// within `path` are relative to. E.g. useful to attach a pixel sized
118    /// shape to a certain data value. No effect when `ysizemode` not set to
119    /// "pixel".
120    #[serde(rename = "yanchor")]
121    y_anchor: Option<NumOrString>,
122    /// Sets the shape's starting y position. See `type` and `ysizemode` for
123    /// more info.
124    y0: Option<NumOrString>,
125    /// Sets the shape's end y position. See `type` and `ysizemode` for more
126    /// info.
127    y1: Option<NumOrString>,
128    /// For `type` "path" - a valid SVG path with the pixel values replaced by
129    /// data values in `xsizemode`/`ysizemode` being "scaled" and taken
130    /// unmodified as pixels relative to `xanchor` and `yanchor` in case of
131    /// "pixel" size mode. There are a few restrictions / quirks
132    /// only absolute instructions, not relative. So the allowed segments
133    /// are: M, L, H, V, Q, C, T, S, and Z arcs (A) are not allowed because
134    /// radius rx and ry are relative. In the future we could consider
135    /// supporting relative commands, but we would have to decide on how to
136    /// handle date and log axes. Note that even as is, Q and C Bezier paths
137    /// that are smooth on linear axes may not be smooth on log, and vice versa.
138    /// no chained "polybezier" commands - specify the segment type for each
139    /// one. On category axes, values are numbers scaled to the serial
140    /// numbers of categories because using the categories themselves
141    /// there would be no way to describe fractional positions On data axes:
142    /// because space and T are both normal components of path strings, we
143    /// can't use either to separate date from time parts. Therefore we'll
144    /// use underscore for this purpose: 2015-02-21_13:45:56.789
145    path: Option<String>,
146    /// Sets the opacity of the shape. Number between or equal to 0 and 1.
147    opacity: Option<f64>,
148    /// Sets the shape line properties (`color`, `width`, `dash`).
149    line: Option<ShapeLine>,
150    /// Sets the color filling the shape's interior. Only applies to closed
151    /// shapes.
152    #[serde(rename = "fillcolor")]
153    fill_color: Option<Box<dyn Color>>,
154    /// Determines which regions of complex paths constitute the interior. For
155    /// more info please visit <https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule>
156    #[serde(rename = "fillrule")]
157    fill_rule: Option<FillRule>,
158    /// Determines whether the shape could be activated for edit or not. Has no
159    /// effect when the older editable shapes mode is enabled via
160    /// `config.editable` or `config.edits.shapePosition`.
161    editable: Option<bool>,
162    /// When used in a template, named items are created in the output figure in
163    /// addition to any items the figure already has in this array. You can
164    /// modify these items in the output figure by making your own item with
165    /// `templateitemname` matching this `name` alongside your modifications
166    /// (including `visible: false` or `enabled: false` to hide it). Has no
167    /// effect outside of a template.
168    name: Option<String>,
169    /// Used to refer to a named item in this array in the template. Named items
170    /// from the template will be created even without a matching item in
171    /// the input figure, but you can modify one by making an item with
172    /// `templateitemname` matching its `name`, alongside your modifications
173    /// (including `visible: false` or `enabled: false` to hide it). If there is
174    /// no template or no matching item, this item will be hidden unless you
175    /// explicitly show it with `visible: true`.
176    #[serde(rename = "templateitemname")]
177    template_item_name: Option<String>,
178}
179
180impl Shape {
181    pub fn new() -> Self {
182        Default::default()
183    }
184
185    /// Specifies the shape type to be drawn. If "line", a line is drawn from
186    /// (`x0`,`y0`) to (`x1`,`y1`) with respect to the axes' sizing mode. If
187    /// "circle", a circle is drawn from ((`x0`+`x1`)/2, (`y0`+`y1`)/2))
188    /// with radius (|(`x0`+`x1`)/2 - `x0`|, |(`y0`+`y1`)/2 -`y0`)|)
189    /// with respect to the axes' sizing mode. If "rect", a rectangle is drawn
190    /// linking (`x0`,`y0`), (`x1`,`y0`), (`x1`,`y1`), (`x0`,`y1`),
191    /// (`x0`,`y0`) with respect to the axes' sizing mode. If "path", draw a
192    /// custom SVG path using `path`. with respect to the axes' sizing mode.
193    pub fn shape_type(mut self, shape_type: ShapeType) -> Self {
194        self.r#type = Some(shape_type);
195        self
196    }
197}
198
199#[derive(Serialize, Debug, Clone)]
200#[serde(rename_all = "lowercase")]
201pub enum DrawDirection {
202    Ortho,
203    Horizontal,
204    Vertical,
205    Diagonal,
206}
207
208#[serde_with::skip_serializing_none]
209#[derive(Serialize, Debug, Clone, FieldSetter)]
210pub struct NewShape {
211    /// Sets the shape line properties (`color`, `width`, `dash`).
212    line: Option<ShapeLine>,
213    /// Sets the color filling new shapes' interior. Please note that if using a
214    /// fillcolor with alpha greater than half, drag inside the active shape
215    /// starts moving the shape underneath, otherwise a new shape could be
216    /// started over.
217    #[serde(rename = "fillcolor")]
218    fill_color: Option<Box<dyn Color>>,
219    /// Determines the path's interior. For more info please
220    /// visit <https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule>
221    #[serde(rename = "fillrule")]
222    fill_rule: Option<FillRule>,
223    /// Sets the opacity of new shapes. Number between or equal to 0 and 1.
224    opacity: Option<f64>,
225    /// Specifies whether new shapes are drawn below or above traces.
226    layer: Option<ShapeLayer>,
227    /// When `dragmode` is set to "drawrect", "drawline" or "drawcircle" this
228    /// limits the drag to be horizontal, vertical or diagonal. Using
229    /// "diagonal" there is no limit e.g. in drawing lines in any direction.
230    /// "ortho" limits the draw to be either horizontal or vertical.
231    /// "horizontal" allows horizontal extend. "vertical" allows vertical
232    /// extend.
233    #[serde(rename = "drawdirection")]
234    draw_direction: Option<DrawDirection>,
235}
236
237impl NewShape {
238    pub fn new() -> Self {
239        Default::default()
240    }
241}
242
243#[serde_with::skip_serializing_none]
244#[derive(Serialize, Debug, Clone, FieldSetter)]
245pub struct ActiveShape {
246    /// Sets the color filling the active shape' interior.
247    #[serde(rename = "fillcolor")]
248    fill_color: Option<Box<dyn Color>>,
249    /// Sets the opacity of the active shape. Number between or equal to 0 and
250    /// 1.
251    opacity: Option<f64>,
252}
253
254impl ActiveShape {
255    pub fn new() -> Self {
256        Default::default()
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use serde_json::{json, to_value};
263
264    use super::*;
265
266    #[test]
267    fn serialize_shape_type() {
268        assert_eq!(to_value(ShapeType::Circle).unwrap(), json!("circle"));
269        assert_eq!(to_value(ShapeType::Rect).unwrap(), json!("rect"));
270        assert_eq!(to_value(ShapeType::Path).unwrap(), json!("path"));
271        assert_eq!(to_value(ShapeType::Line).unwrap(), json!("line"));
272    }
273
274    #[test]
275    fn serialize_shape_layer() {
276        assert_eq!(to_value(ShapeLayer::Below).unwrap(), json!("below"));
277        assert_eq!(to_value(ShapeLayer::Above).unwrap(), json!("above"));
278    }
279
280    #[test]
281    fn serialize_shape_size_mode() {
282        assert_eq!(to_value(ShapeSizeMode::Scaled).unwrap(), json!("scaled"));
283        assert_eq!(to_value(ShapeSizeMode::Pixel).unwrap(), json!("pixel"));
284    }
285
286    #[test]
287    fn serialize_fill_rule() {
288        assert_eq!(to_value(FillRule::EvenOdd).unwrap(), json!("evenodd"));
289        assert_eq!(to_value(FillRule::NonZero).unwrap(), json!("nonzero"));
290    }
291
292    #[test]
293    fn serialize_shape_line() {
294        let shape_line = ShapeLine::new()
295            .color("#000FFF")
296            .width(100.)
297            .dash(DashType::LongDashDot);
298        let expected = json!({
299            "color": "#000FFF",
300            "width": 100.0,
301            "dash": "longdashdot",
302        });
303
304        assert_eq!(to_value(shape_line).unwrap(), expected);
305    }
306
307    #[test]
308    fn serialize_shape() {
309        let shape = Shape::new()
310            .visible(false)
311            .shape_type(ShapeType::Circle)
312            .layer(ShapeLayer::Above)
313            .x_ref("xref")
314            .x_size_mode(ShapeSizeMode::Pixel)
315            .x_anchor(5)
316            .x0(7)
317            .x1(8)
318            .y_ref("paper")
319            .y0(1)
320            .y1(2)
321            .y_anchor("yanchor")
322            .y_size_mode(ShapeSizeMode::Scaled)
323            .path("path")
324            .opacity(0.2)
325            .line(ShapeLine::new())
326            .fill_color("#FEFEFE")
327            .fill_rule(FillRule::NonZero)
328            .editable(true)
329            .name("name")
330            .template_item_name("templateitemname");
331
332        let expected = json!({
333            "visible": false,
334            "type": "circle",
335            "layer": "above",
336            "xref": "xref",
337            "xsizemode": "pixel",
338            "xanchor": 5,
339            "x0": 7,
340            "x1": 8,
341            "yref": "paper",
342            "y0": 1,
343            "y1": 2,
344            "yanchor": "yanchor",
345            "ysizemode": "scaled",
346            "path": "path",
347            "opacity": 0.2,
348            "line": {},
349            "fillcolor": "#FEFEFE",
350            "fillrule": "nonzero",
351            "editable": true,
352            "name": "name",
353            "templateitemname": "templateitemname"
354        });
355
356        assert_eq!(to_value(shape).unwrap(), expected)
357    }
358
359    #[test]
360    #[rustfmt::skip]
361    fn serialize_draw_direction() {
362        assert_eq!(to_value(DrawDirection::Ortho).unwrap(), json!("ortho"));
363        assert_eq!(to_value(DrawDirection::Horizontal).unwrap(), json!("horizontal"));
364        assert_eq!(to_value(DrawDirection::Vertical).unwrap(), json!("vertical"));
365        assert_eq!(to_value(DrawDirection::Diagonal).unwrap(), json!("diagonal"));
366    }
367
368    #[test]
369    fn serialize_new_shape() {
370        let new_shape = NewShape::new()
371            .line(ShapeLine::new())
372            .fill_color("#123ABC")
373            .fill_rule(FillRule::EvenOdd)
374            .opacity(0.02)
375            .layer(ShapeLayer::Below)
376            .draw_direction(DrawDirection::Ortho);
377
378        let expected = json!({
379            "line": {},
380            "fillcolor": "#123ABC",
381            "fillrule": "evenodd",
382            "opacity": 0.02,
383            "layer": "below",
384            "drawdirection": "ortho",
385        });
386
387        assert_eq!(to_value(new_shape).unwrap(), expected)
388    }
389
390    #[test]
391    fn serialize_active_shape() {
392        let active_shape = ActiveShape::new().fill_color("#123ABC").opacity(0.02);
393
394        let expected = json!({
395            "fillcolor": "#123ABC",
396            "opacity": 0.02,
397        });
398
399        assert_eq!(to_value(active_shape).unwrap(), expected);
400    }
401}