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}