Skip to content

Dropdown legend items #207

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

Closed
2 tasks
mdtusz opened this issue Jan 22, 2016 · 8 comments · Fixed by #373
Closed
2 tasks

Dropdown legend items #207

mdtusz opened this issue Jan 22, 2016 · 8 comments · Fixed by #373
Assignees
Labels
feature something new

Comments

@mdtusz
Copy link
Contributor

mdtusz commented Jan 22, 2016

There has been some demand for dropdown selection of legend items and custom filtering, useful in cases where there are lots of traces and data to be toggled.

This issue will come in two parts:

  • Add ability to specify legend.type with either vertical or dropdown (later on, horizontal)
  • Add custom legend-item-click-handlers i.e. the ability to add a legend entry, with a specified event onclick
@mdtusz mdtusz added the feature something new label Jan 22, 2016
@mdtusz mdtusz self-assigned this Jan 22, 2016
@mdtusz
Copy link
Contributor Author

mdtusz commented Feb 8, 2016

After some more time working on this, there's some new challenges/discoveries.

  1. We can't have mixed elements in SVG, otherwise exporting to png will be broken because of a tainted canvas.
  2. To keep plotly.js friendly to python and other languages, we need to implement any filters/ranges/actions declaratively.
  3. How do we want to handle styling? How in depth should styling options should be (if any)?
  4. What sort of behaviour should the "dropdown" section have? A traditional dropdown is just a single-select and doesn't necessarily translate 1:1 to our use case.

So far for the scrolling, I've done it all in SVG to get around the first issue of a tainted canvas, but dropdowns will be a bit substantially more complex to create in SVG.

Etienne and I had discussed how best to implement "filtering" as actions that can be in the legend box - because we want to keep python support strong, we'll need to do it declaratively, which is obviously not quite as nice as if we could just pass lambdas to a layout property.

And so it goes.

@chriddyp what are the requirements on your end for features/appearance/interface in the workspace? Maybe a mockup for the style/interaction of it will be helpful too if @delekru can whip something up.

@arikfr
Copy link

arikfr commented Feb 8, 2016

I'm not familiar enough with the limitation SVG imposes, but maybe it will be easier to implement paginated legend rather than drop down?

@chriddyp
Copy link
Member

@chriddyp what are the requirements on your end for features/appearance/interface in the workspace?

Filtering in the workspace has reference to a greater data model than what is just present inside the plotly chart. Folks can filter on columns that they aren't actually visualizing.

We've kept the UI for this entirely outside of the plot with the chart creation and exploration controls. I don't have a strong vision for what this might look like inside the plot and how we'll unify the two interfaces.

My first impression is that the filtering that we provide inside the workspace is "exportable" to the chart. These controls would be in the form of dropdown selects, search boxes, and numerical input boxes. The JSON might look something like:

data: [{
    x: [...], y: [...], filterGroup: 'f1'
}],
layout: {
   filters: {
       'f1': {
          data: ['NYC', 'Boston', 'Montreal'],
          operation: 'contains',
          x: 0, xref: 'page', y: 1, yref: 'page', label: 'filter cities'
       }
   }
}

@etpinard
Copy link
Contributor

Thanks @chriddyp.

I'm thinking, to start, that we should allow only one filter per plot, similar to legends.

Moreover, should we support trace filters? Or should the filter steps loop over all traces?

And most importantly, we'll need to find a way make the coupling between operation and data more robust.

Here's my (a little-more plotly.js-eque) version of the specs:

layout: {
  filter: {
    type: 'buttons'  // or 'dropdown'
    orientation: 'h',  // or 'v' or merge into type e.g 'horiz buttons' / 'vert butons'

    items: [{
      visible: true,
      mode: 'eq', // 'lt', 'gt', 'contains', 'range'
      value: 20 // numeric for 'eq', 'lt and 'gt' ,
                // 2-item array for 'range' ,
                // array or string for 'contains',
      label: 'my filter' // string to be display on button
    }, {
       /* ... */
    }],

    // some positioning options
    x: 1,  // always on a 'paper'
    y: 0,
    xanchor: 'left',
    yanchor: 'bottom',

    // some style options
    len: 0.4  // length in normalized coordinates 
              // the button width would be len / 0-1 to px / items.length 

    borderwidth: 1,
    bordercolor: '#fff'
  }
}

cc @mdtusz @alexcjohnson @jackparmer

@jackparmer
Copy link
Contributor

The dropdown filtering use-case that @cldougl and I hear most from dashboard users and Plotly customers is simply showing one trace while hiding all the other traces. Here's a demo using the postMessage API:

thing

(From: http://help.plot.ly/documentation/dashboards/sales/)

Just want to vote for this basic use case to get covered!

@etpinard
Copy link
Contributor

etpinard commented Mar 4, 2016

Some observations

Most filtering operations will come in two forms:

  • trace filters: toggle trace visibility based on a filtergroup attribute specified in each of the traces.
  • range filters: perform a slicing operation on a data arrays linked to a particular attribute on all traces present.

Trace filters

For example, the dashboard in @jackparmer ⏫ comment would be described as:

layout.tracefilter = {
  mode: 'dropdown',  // or 'buttons'

  labels: [],  // list of button labels to optionally override the filter group values
  includeall: false,  // or true - include button that shows all filter groups
  includereset: false, // or true - include button that reset to first view
  multiplegroups: false  // or true - can multiple group be selected at the same time?

  // ++ common filter style and positioning attributes
};

where there would as many buttons as distinct filtergroup values found in the data.

Range filters

For example, the range selectors as in the top-left corner of:

image

would be described as:

layout.rangefilter = {
  mode: 'buttons',  // or 'dropdown'
  orientation: 'h'     // or 'v'

  updatemode: 'base', // or 'current' - i.e. is slicing performed on the base state or current one?
  arrayattribute: 'x'  // about which array attribute is the slicing performed? could be 'marker.color' 

  items: [{
    label: 'all',
    range: [null, null]  // null in range[0] means -Infinity , in range[1] means Infinity
  }, {
    label: 'last month',
    range: [ (new Date(2016, 1).getTime(), null ]
  }]

  // ++ common filter style and positioning attributes  
};

Custom filters

Down the road, we could easily add a fully-custom filter type where sequences of operations could be declared. For example:

{
    mode: 'buttons',

    items: [{
        label: 'x > 30 and y < 50',
        updatemode: 'base',
        operations: [{
            type: 'range',
            arrayattribute: 'x',
            range: [30, null]
        }, {
            type: 'range',
            arrayattribute: 'y',
            range: [null, 50]
        }]
    }, {
       label: 'group1',
       updatetype: 'current',
       operations: [{
            type: 'filtergroup',
            group: 'group1'
        }]
    }, {
        label: '10 < x < 30 and group1',
        updatetype: 'base',
        operations: [{
            type: 'range',
            arrayattribute: 'x',
            range: [10, 30]
        }, {
            type: 'filtergroup',
            group: 'group1'
        }]
    }];
}

@mdtusz
Copy link
Contributor Author

mdtusz commented Mar 4, 2016

This looks like a good interface, but we still won't be able to do any conditional range/filtering - i.e. the last month example wouldn't work using new Date(2016, 1).getTime().

One way we could get around this could be to use Infinity and -Infinity for our infinity values, and null for, well, nulls. This would mean we could have an interface where

range: [ -30, null ]

could indicate using the last 30 datapoints on the axis, and

range: [-30, Infinity]

would indicate using from -30 to infinity.

Perhaps that's a bit more convoluted than necessary, but it would allow for proper "moving" ranges depending on the data provided.

@etpinard
Copy link
Contributor

Some initial work on branch range-selector

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants