plotly/layout/
update_menu.rs

1//! Buttons and Dropdowns.
2
3use plotly_derive::FieldSetter;
4use serde::Serialize;
5use serde_json::{Map, Value};
6
7use crate::{
8    color::Color,
9    common::{Anchor, Font, Pad},
10    layout::{Animation, ControlBuilderError},
11    Relayout, Restyle,
12};
13
14/// Sets the Plotly method to be called on click. If the `skip` method is used,
15/// the API updatemenu will function as normal but will perform no API calls and
16/// will not bind automatically to state updates. This may be used to create a
17/// component interface and attach to updatemenu events manually via JavaScript.
18#[derive(Serialize, Debug, Copy, Clone)]
19#[serde(rename_all = "snake_case")]
20pub enum ButtonMethod {
21    /// The restyle method should be used when modifying the data and data
22    /// attributes of the graph
23    Restyle,
24    /// The relayout method should be used when modifying the layout attributes
25    /// of the graph.
26    Relayout,
27    /// The update method should be used when modifying the data and layout
28    /// sections of the graph.
29    Update,
30    /// Animates a sequence of frames
31    Animate,
32    /// With `skip` method, the API updatemenu will function as normal but will
33    /// perform no API calls and will not bind automatically to state updates.
34    /// This may be used to create a component interface and attach to
35    /// updatemenu events manually via JavaScript.
36    Skip,
37}
38
39#[serde_with::skip_serializing_none]
40#[derive(Serialize, Clone, Debug, FieldSetter)]
41pub struct Button {
42    /// Sets the arguments values to be passed to the Plotly method set in
43    /// `method` on click.
44    args: Option<Value>,
45    /// Sets a 2nd set of `args`, these arguments values are passed to the
46    /// Plotly method set in `method` when clicking this button while in the
47    /// active state. Use this to create toggle buttons.
48    args2: Option<Value>,
49    /// When true, the API method is executed. When false, all other behaviors
50    /// are the same and command execution is skipped. This may be useful
51    /// when hooking into, for example, the `plotly_buttonclicked` method
52    /// and executing the API command manually without losing the benefit of
53    /// the updatemenu automatically binding to the state of the plot through
54    /// the specification of `method` and `args`.
55    ///
56    /// Default: true
57    execute: Option<bool>,
58    /// Sets the text label to appear on the button.
59    label: Option<String>,
60    /// Sets the Plotly method to be called on click. If the `skip` method is
61    /// used, the API updatemenu will function as normal but will perform no
62    /// API calls and will not bind automatically to state updates. This may
63    /// be used to create a component interface and attach to updatemenu
64    /// events manually via JavaScript.
65    method: Option<ButtonMethod>,
66    /// When used in a template, named items are created in the output figure in
67    /// addition to any items the figure already has in this array. You can
68    /// modify these items in the output figure by making your own item with
69    /// `templateitemname` matching this `name` alongside your modifications
70    /// (including `visible: false` or `enabled: false` to hide it). Has no
71    /// effect outside of a template.
72    name: Option<String>,
73    /// Used to refer to a named item in this array in the template. Named items
74    /// from the template will be created even without a matching item in
75    /// the input figure, but you can modify one by making an item with
76    /// `templateitemname` matching its `name`, alongside your modifications
77    /// (including `visible: false` or `enabled: false` to hide it). If there is
78    /// no template or no matching item, this item will be hidden unless you
79    /// explicitly show it with `visible: true`
80    #[serde(rename = "templateitemname")]
81    template_item_name: Option<String>,
82    /// Determines whether or not this button is visible.
83    visible: Option<bool>,
84}
85
86impl Button {
87    pub fn new() -> Self {
88        Default::default()
89    }
90}
91
92/// Builder struct to create buttons which can do restyles and/or relayouts
93#[derive(FieldSetter)]
94pub struct ButtonBuilder {
95    label: Option<String>,
96    name: Option<String>,
97    template_item_name: Option<String>,
98    visible: Option<bool>,
99    #[field_setter(default = "Map::new()")]
100    restyles: Map<String, Value>,
101    #[field_setter(default = "Map::new()")]
102    relayouts: Map<String, Value>,
103    #[field_setter(skip)]
104    error: Option<ControlBuilderError>,
105    // Animation configuration
106    #[field_setter(skip)]
107    animation: Option<Animation>,
108}
109
110impl ButtonBuilder {
111    pub fn new() -> Self {
112        Default::default()
113    }
114
115    pub fn push_restyle(mut self, restyle: impl Restyle) -> Self {
116        if self.error.is_none() {
117            if let Err(e) = self.try_push_restyle(restyle) {
118                self.error = Some(e);
119            }
120        }
121        self
122    }
123
124    fn try_push_restyle(&mut self, restyle: impl Restyle) -> Result<(), ControlBuilderError> {
125        let restyle_value = serde_json::to_value(&restyle)
126            .map_err(|e| ControlBuilderError::RestyleSerializationError(e.to_string()))?;
127        let restyle_obj = restyle_value
128            .as_object()
129            .ok_or_else(|| ControlBuilderError::InvalidRestyleObject(restyle_value.to_string()))?;
130        for (k, v) in restyle_obj {
131            self.restyles.insert(k.clone(), v.clone());
132        }
133        Ok(())
134    }
135
136    pub fn push_relayout(mut self, relayout: impl Relayout + Serialize) -> Self {
137        if self.error.is_none() {
138            if let Err(e) = self.try_push_relayout(relayout) {
139                self.error = Some(e);
140            }
141        }
142        self
143    }
144
145    fn try_push_relayout(
146        &mut self,
147        relayout: impl Relayout + Serialize,
148    ) -> Result<(), ControlBuilderError> {
149        let relayout_value = serde_json::to_value(&relayout)
150            .map_err(|e| ControlBuilderError::RelayoutSerializationError(e.to_string()))?;
151        let relayout_obj = relayout_value.as_object().ok_or_else(|| {
152            ControlBuilderError::InvalidRelayoutObject(relayout_value.to_string())
153        })?;
154        for (k, v) in relayout_obj {
155            self.relayouts.insert(k.clone(), v.clone());
156        }
157        Ok(())
158    }
159
160    /// Sets the animation configuration for the button
161    pub fn animation(mut self, animation: Animation) -> Self {
162        self.animation = Some(animation);
163        self
164    }
165
166    pub fn build(self) -> Result<Button, ControlBuilderError> {
167        if let Some(error) = self.error {
168            return Err(error);
169        }
170
171        let (method, args) = match (
172            self.animation,
173            self.restyles.is_empty(),
174            self.relayouts.is_empty(),
175        ) {
176            // Animation takes precedence
177            (Some(animation), _, _) => {
178                let animation_args = serde_json::to_value(animation)
179                    .map_err(|e| ControlBuilderError::AnimationSerializationError(e.to_string()))?;
180                (ButtonMethod::Animate, animation_args)
181            }
182            // Regular restyle/relayout combinations
183            (None, true, true) => (ButtonMethod::Skip, Value::Null),
184            (None, false, true) => (ButtonMethod::Restyle, vec![self.restyles].into()),
185            (None, true, false) => (ButtonMethod::Relayout, vec![self.relayouts].into()),
186            (None, false, false) => (
187                ButtonMethod::Update,
188                vec![self.restyles, self.relayouts].into(),
189            ),
190        };
191
192        Ok(Button {
193            label: self.label,
194            args: Some(args),
195            method: Some(method),
196            name: self.name,
197            template_item_name: self.template_item_name,
198            visible: self.visible,
199            ..Default::default()
200        })
201    }
202}
203
204/// Determines whether the buttons are accessible via a dropdown menu or whether
205/// the buttons are stacked horizontally or vertically
206///
207/// Default: "dropdown"
208#[derive(Serialize, Debug, Clone)]
209#[serde(rename_all = "snake_case")]
210pub enum UpdateMenuType {
211    Dropdown,
212    Buttons,
213}
214
215/// Determines the direction in which the buttons are laid out, whether in a
216/// dropdown menu or a row/column of buttons. For `left` and `up`, the buttons
217/// will still appear in left-to-right or top-to-bottom order respectively.
218///
219/// Default: "down"
220#[derive(Serialize, Debug, Clone)]
221#[serde(rename_all = "snake_case")]
222pub enum UpdateMenuDirection {
223    Left,
224    Right,
225    Up,
226    Down,
227}
228
229#[serde_with::skip_serializing_none]
230#[derive(Serialize, Debug, FieldSetter, Clone)]
231pub struct UpdateMenu {
232    /// Determines which button (by index starting from 0) is considered active.
233    active: Option<i32>,
234    /// Sets the background color of the update menu buttons.
235    #[serde(rename = "bgcolor")]
236    background_color: Option<Box<dyn Color>>,
237    /// Sets the color of the border enclosing the update menu.
238    #[serde(rename = "bordercolor")]
239    border_color: Option<Box<dyn Color>>,
240    /// Sets the width (in px) of the border enclosing the update menu.
241    #[serde(rename = "borderwidth")]
242    border_width: Option<usize>,
243    buttons: Option<Vec<Button>>,
244    /// Determines the direction in which the buttons are laid out, whether in
245    /// a dropdown menu or a row/column of buttons. For `left` and `up`,
246    /// the buttons will still appear in left-to-right or top-to-bottom order
247    /// respectively.
248    direction: Option<UpdateMenuDirection>,
249    /// Sets the font of the update menu button text.
250    font: Option<Font>,
251    /// When used in a template, named items are created in the output figure in
252    /// addition to any items the figure already has in this array. You can
253    /// modify these items in the output figure by making your own item with
254    /// `templateitemname` matching this `name` alongside your modifications
255    /// (including `visible: false` or `enabled: false` to hide it). Has no
256    /// effect outside of a template.
257    name: Option<String>,
258    /// Sets the padding around the buttons or dropdown menu.
259    pad: Option<Pad>,
260    /// Highlights active dropdown item or active button if true.
261    #[serde(rename = "showactive")]
262    show_active: Option<bool>,
263    /// Used to refer to a named item in this array in the template. Named items
264    /// from the template will be created even without a matching item in
265    /// the input figure, but you can modify one by making an item with
266    /// `templateitemname` matching its `name`, alongside your modifications
267    /// (including `visible: false` or `enabled: false` to hide it). If there is
268    /// no template or no matching item, this item will be hidden unless you
269    /// explicitly show it with `visible: true`.
270    template_item_name: Option<String>,
271    /// Determines whether the buttons are accessible via a dropdown menu or
272    /// whether the buttons are stacked horizontally or vertically
273    #[serde(rename = "type")]
274    ty: Option<UpdateMenuType>,
275    /// Determines whether or not the update menu is visible.
276    visible: Option<bool>,
277    /// Type: number between or equal to -2 and 3
278    /// Default: -0.05
279    /// Sets the x position (in normalized coordinates) of the update menu.
280    x: Option<f64>,
281    /// Sets the update menu's horizontal position anchor. This anchor binds the
282    /// `x` position to the "left", "center" or "right" of the range
283    /// selector. Default: "right"
284    #[serde(rename = "xanchor")]
285    x_anchor: Option<Anchor>,
286    /// Type: number between or equal to -2 and 3
287    /// Default: 1
288    /// Sets the y position (in normalized coordinates) of the update menu.
289    y: Option<f64>,
290    /// Sets the update menu's vertical position anchor This anchor binds the
291    /// `y` position to the "top", "middle" or "bottom" of the range
292    /// selector. Default: "top"
293    #[serde(rename = "yanchor")]
294    y_anchor: Option<Anchor>,
295}
296
297impl UpdateMenu {
298    pub fn new() -> Self {
299        Default::default()
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use serde_json::{json, to_value};
306
307    use super::*;
308    use crate::{common::Visible, Layout};
309
310    #[test]
311    fn serialize_button_method() {
312        assert_eq!(to_value(ButtonMethod::Restyle).unwrap(), json!("restyle"));
313        assert_eq!(to_value(ButtonMethod::Relayout).unwrap(), json!("relayout"));
314        assert_eq!(to_value(ButtonMethod::Animate).unwrap(), json!("animate"));
315        assert_eq!(to_value(ButtonMethod::Update).unwrap(), json!("update"));
316        assert_eq!(to_value(ButtonMethod::Skip).unwrap(), json!("skip"));
317    }
318
319    #[test]
320    fn serialize_button() {
321        let button = Button::new()
322            .args(json!([
323                { "visible": [true, false] },
324                { "width": 20},
325            ]))
326            .args2(json!([]))
327            .execute(true)
328            .label("Label")
329            .method(ButtonMethod::Update)
330            .name("Name")
331            .template_item_name("Template")
332            .visible(true);
333
334        let expected = json!({
335            "args": [
336                { "visible": [true, false] },
337                { "width": 20},
338            ],
339            "args2": [],
340            "execute": true,
341            "label": "Label",
342            "method": "update",
343            "name": "Name",
344            "templateitemname": "Template",
345            "visible": true,
346        });
347
348        assert_eq!(to_value(button).unwrap(), expected);
349    }
350
351    #[test]
352    fn serialize_button_builder() {
353        let expected = json!({
354            "args": [
355                { "visible": [true, false] },
356                { "title": {"text": "Hello"}, "width": 20},
357            ],
358            "label": "Label",
359            "method": "update",
360            "name": "Name",
361            "templateitemname": "Template",
362            "visible": true,
363        });
364
365        let button = ButtonBuilder::new()
366            .label("Label")
367            .name("Name")
368            .template_item_name("Template")
369            .visible(true)
370            .push_restyle(crate::Bar::<i32, i32>::modify_visible(vec![
371                Visible::True,
372                Visible::False,
373            ]))
374            .push_relayout(Layout::modify_title("Hello"))
375            .push_relayout(Layout::modify_width(20))
376            .build()
377            .unwrap();
378
379        assert_eq!(to_value(button).unwrap(), expected);
380    }
381
382    #[test]
383    fn test_button_builder_push_restyle_valid() {
384        let button = ButtonBuilder::new()
385            .label("Test Button")
386            .push_restyle(crate::Bar::<i32, i32>::modify_visible(vec![
387                Visible::True,
388                Visible::False,
389            ]))
390            .build()
391            .unwrap();
392
393        let expected = json!({
394            "args": [{ "visible": [true, false] }],
395            "label": "Test Button",
396            "method": "restyle",
397        });
398
399        assert_eq!(to_value(button).unwrap(), expected);
400    }
401
402    #[test]
403    fn test_button_builder_push_relayout_valid() {
404        let button = ButtonBuilder::new()
405            .label("Test Button")
406            .push_relayout(Layout::modify_title("Test Title"))
407            .build()
408            .unwrap();
409
410        let expected = json!({
411            "args": [{ "title": {"text": "Test Title"} }],
412            "label": "Test Button",
413            "method": "relayout",
414        });
415
416        assert_eq!(to_value(button).unwrap(), expected);
417    }
418
419    #[test]
420    fn test_button_builder_push_restyle_invalid() {
421        // Create a dummy struct that implements Restyle but serializes to null
422        #[derive(Serialize)]
423        struct InvalidRestyle;
424        impl Restyle for InvalidRestyle {}
425
426        let result = ButtonBuilder::new()
427            .label("Test Button")
428            .push_restyle(InvalidRestyle)
429            .build();
430
431        assert!(result.is_err());
432        match result.unwrap_err() {
433            ControlBuilderError::InvalidRestyleObject(_) => {}
434            _ => panic!("Expected InvalidRestyleObject error"),
435        }
436    }
437
438    #[test]
439    fn test_button_builder_push_relayout_invalid() {
440        // Create a dummy struct that implements Relayout but serializes to null
441        #[derive(Serialize)]
442        struct InvalidRelayout;
443        impl Relayout for InvalidRelayout {}
444
445        let result = ButtonBuilder::new()
446            .label("Test Button")
447            .push_relayout(InvalidRelayout)
448            .build();
449
450        assert!(result.is_err());
451        match result.unwrap_err() {
452            ControlBuilderError::InvalidRelayoutObject(_) => {}
453            _ => panic!("Expected InvalidRelayoutObject error"),
454        }
455    }
456
457    #[test]
458    fn serialize_animation_button_args() {
459        let animation = Animation::all_frames();
460
461        let button = ButtonBuilder::new()
462            .label("Animate")
463            .animation(animation)
464            .build()
465            .unwrap();
466
467        let args = button.args.unwrap();
468        assert!(args.is_array(), "Animation button args must be an array");
469
470        // Verify the structure: [frameArg, options]
471        assert_eq!(args[0], json!(null)); // Should be null for all_frames
472        assert!(
473            args[1].is_object(),
474            "Second arg should be animation options object"
475        );
476        assert_eq!(to_value(button.method.unwrap()).unwrap(), json!("animate"));
477    }
478}