Skip to content

Button support and FieldSetter derive macro #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 6, 2022

Conversation

sreenathkrishnan
Copy link
Contributor

@sreenathkrishnan sreenathkrishnan commented Sep 1, 2022

Thank you for this wonderful crate. I have personally found it much easier to make a plotly chart using this crate aided by the Rust type system as opposed to other options. Excited to contribute to this repository.

This PR introduces two changes:

Change 1: FieldSetter derive marco

There are a number of structs defined in this crate that have private fields and public functions to set the value for each of these fields. The FieldSetter procedural marco, when applied on a struct, auto-derives the public setter functions for each of the fields in the struct. The setter functions have a small number of distinct signatures based on the type of the field. Types such as Dim, String, Color, NumOrString get a special syntax. The procedural macro is defined in the plotly_derive crate. I have updated the traces and layout modules to use the procedural macro (resulting in a lot of code deletions). The macro also proved two attributes

  • #[field_setter(box_self)] - The setter functions return Box<Self> instead of Self if applied to the struct
  • #[field_setter(skip)] - To skip generating setter function for a specific field

The procedural macro is not fully general to cover type aliasing etc, but works in practice for this crate.

Pros

  • Consistent setter API across all structs
  • Minimal code required to define new structs

Cons

  • Additional dependencies
  • Slightly slower compilation due to proc macro expansion

Change 2: Support for Buttons (update menu)

I have defined the structs required to support buttons in a plotly chart (https://plotly.com/javascript/reference/layout/updatemenus/) in a new module layout/update_menu.rs.

Using these structs we can add buttons with this crate. However the button API (https://plotly.com/python/dropdowns/) is not strongly-typed and needs lots of "stringy" arguments. Instead, I have added a new approach to help create buttons. There are three button methods that I have focused on:

  • restyle: modify traces
  • relayout: modify layout
  • update modify traces and layout

Restyles

First I have defined a Restyle marker trait. The FieldSetter macro defines a new enum for each struct if it is marked as #[field_setter(kind = "trace")]. As an example, say we are deriving FieldSetter on the (simplified) SimpleBar struct

#[serde_with::skip_serializing_none]
#[derive(Serialize, Debug, Clone, FieldSetter)]
#[field_setter(box_self, kind = "trace")]
pub struct SimpleBar
{
    name: Option<String>,
    visible: Option<Visible>,
}

The derived enum would look like

pub enum RestyleSimpleBar {
  ModifyName { name: Option<Dim<String>> },
  ModifyVisible { name: Option<Dim<Visible>> },
}

i.e it contains one enum variant for modifying each of the struct fields. The reason each field is wrapped with a Dim is that plotly provide option to modify all the traces in the plot using a scalar argument or control what to modify in each trace using a list argument. This enum implements the Restyle trait.

Additionally we also define helper functions for SimpleBar

impl Bar {
  pub fn modify_all_name(value: impl AsRef<str>) -> RestlyeSimpleBar {
    RestlyeSimpleBar::ModifyName { name: Some(Dim::Scalar(value.as_ref().into())) }
  }
  pub fn modify_name(values: vec![impl AsRef<str>]) -> RestlyeSimpleBar {
    RestlyeSimpleBar::ModifyName { name: ... }
  }
  // .... similarly for visible
}

Coupled with this we have a ButtonBuilder struct that has a push_restyle() function which accepts any struct that implements Restyle trait. On invoking the ButtonBuilder::build() function, the builder sets up the Button correctly with the arguments in the right form.

So for example, if we have two bar plots and we want a button to alternate between them, we can create the button using

let buttons = vec![
      ButtonBuilder::new()
          .label("Chart1")
          .push_restyle(SimpleBar::modify_visible(vec![Visible::True, Visible::False]))
          .build(),
      ButtonBuilder::new()
          .label("Chart2")
          .push_restyle(SimpleBar::modify_visible(vec![Visible::False, Visible::True]))
          .build(),
  ]

Relayout

Similar to Restyle we have a Relayout trait. We derive a new enum which implements Relayout if the struct is annotated with #[field_setter(kind = "layout")]. Since you only have a single layout per plot, we don't do the Dim wrapping here. The ButtonBuilder also has a push_relayout function.

Examples

cargo r --example=buttons

Three plots that demonstrate the use of UpdateMenu to create buttons:

  • Dropdown to alternate between two bar plots (restyle)
    Screen-Recording-2022-09-01-at-3 45 50-PM

  • Heatmap with buttons to choose colorscale (restyle)
    Screen-Recording-2022-09-01-at-3 49 53-PM

  • Button to change barmode (relayout)
    Screen-Recording-2022-09-01-at-3 50 43-PM

@sreenathkrishnan sreenathkrishnan marked this pull request as ready for review September 1, 2022 22:56
@mfreeborn
Copy link
Contributor

Thank you for this effort! It was something that I was thinking about implementing, but I have no experience with writing macros.

I'll review these changes fully as soon as I can, which will be early next week at the latest.

@mfreeborn mfreeborn merged commit 6053699 into plotly:dev Sep 6, 2022
@sreenathkrishnan sreenathkrishnan deleted the updatemenu branch September 6, 2022 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants