Skip to content

Introducing transform plugins #499

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 27 commits into from
Jul 22, 2016
Merged

Introducing transform plugins #499

merged 27 commits into from
Jul 22, 2016

Conversation

etpinard
Copy link
Contributor

@etpinard etpinard commented May 3, 2016

@mdtusz @chriddyp @bpostlethwaite @alexcjohnson @jackparmer

This PR adds support for custom data transforms to be plugged into plotly.js via Plotly.register.

In brief

  • Plotly.register can now register transform modules 9ac6d0f
  • The supply-defaults step can now handle data transforms. IMPORTANT User data and gd._fullData no longer have to be of the same length. 06f2e6d
  • Plotly.restyle can now handle transformed data 8ab9d0c

Examples

A filter and group-by were added as examples in 2697975

Does it work?

See 56665f2

TODOs

  • Agree on how to handle legend items in group-by transformed data.
  • Agree on the API for transform modules' transform method.
  • Agree on where to place and how to named full transformed data and other intermediate objects that may be useful for folks building toolpanels. (done, see 9a341c1)
  • Integrate transforms attributes into plot schema. (done, see b5ec01e)

etpinard added 10 commits May 3, 2016 12:24
- split per-trace defaults step from plots.supplyDataDefaults
- call transform module supplyDefaults at end of trace-defaults
  routine, so that transform default logic can rely on trace defaults
- add an applyTransforms step which generates 'expanded' data
  which themselves go through a trace-default step.
- transform functions are expected to supply an array of traces
- the 'traces' array of trace index is now based off gd.data
- make all transforms restyle calls go through doCalcdata
  which is the worse case scenario (i.e. we take no chance!)
@etpinard
Copy link
Contributor Author

etpinard commented May 3, 2016

EDIT

This problem below is solved via 9a341c1 where we made the expanded full traces carry the index of their parent gd.data trace in the index field (as for regular / non-expanded full traces).


More on the first TODO item ⏫ :

First, see example of group-by transform module here.

In this module, transformed trace are expanded into as many traces as there are groups items. At the moment, these expanded traces are all displayed in the legend. BUT, these cannot be as they do not match gd.data one-to-one and I made Plotly.restyle to be based off gd.data in 8ab9d0c.

How to resolve this problem? Here are a few possibilities:

  1. Force expanded traces from the same user data trace to have to be toggled at once. Essentially, like they had the same legendgroup value.
  2. Write the group-by transform plugins such that as groups and the number of traces sent to Plotly.plot match one-to-one. It would be easy to have the transform look up the same data arrays for the transform step. This is the most robust solution from plotly.js's perspective.
  3. Make Plotly.restyle handle transformed indices too. Maybe with an option flag? But this sounds useful only for the visibility toggling.
  4. Add an attribute to the group-by transform module to would toggle the visibility of a particular group and somehow let the legend code know about it. For example,
transforms: [{
  type: 'groupby',
  groups: ['a', 'b', 'a', 'b'],
  groupsVisible: ['a']
}]

would hide the 'b' group. Note that some people have asked for custom legend interactions (see #65). Maybe this isn't such a bad idea.

else {
dataOut.push(fullTrace);
fill(modules, fullTrace._module);
detect(trace);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detect(fullTrace); ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in fact, this untransformed case needs i linked in as well. Might as well make eg:

function addFinalTrace(finalTrace) {
    finalTrace._input = trace;
    finalTrace._inputIndex = i;
    finalTrace._expandedIndex = cnt;
    dataOut.push(finalTrace);
    fill(modules, finalTrace);
    detect(finalTrace);
    cnt++;
}

since all of that needs to happen identically whichever way you added a trace to _fullData.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch ⚾ . Thanks!

@chriddyp
Copy link
Member

chriddyp commented May 3, 2016

this looks awesome! with this architecture, my "visual" transforms will also need to have something like a "transform inverse".

data

[{
    x: [1, 2, 1, 2],
    y: [3, 1, 5, 4],
    transforms: [{type: 'groupby', groups: ['a', 'a', 'b', 'b']}]
}]

_fullData

[
   {
      x: [1, 2], y: [3, 1], 
      // + all of the defaults
      marker: {color: 'blue', size: 6, symbol: 'circle', line: {}}
   },
   {
      x: [1, 2], y: [5, 4], 
      // + all of the defaults
      marker: {color: 'red', size: 6, symbol: 'circle', line: {}}
   }
]

transformInverse(data, _fullData, transformIndices)

[{
    x: [1, 2, 1, 2],
    y: [3, 1, 5, 4],
    transforms: [{
        type: 'groupby',
        groups: ['a', 'a', 'b', 'b'],

        // + all of the default properties mapped *back* to the original trace
        // the transform author writes this `transformInverse` function
        marker: {
           color: {
                  a: 'blue', b: 'red'
            },
            size: 6,
            symbol: 'circle',
            line: {}
        }
   }]
}]

my style panels will be built off of transformInverse(_fullData).

similar example with candlestick charts:

data

{
    x: [1, 2, 3],
    transform: [{
       type: 'candlestick',
       open: [6, 2, 3], high: [4, 1, 3], low: [3, 4, 1], close: [5, 8, 3]
    }]
}

_fullData

[
   // "high" trace 
   {
        x: [....], y: [....],
        type: 'box',
        // + all the default box properties
        orientation: 'v',
        jitter: 0,
       marker: {
           color: 'blue', 
           // ... and more
       },
       // .. etc
   // "low" trace 
   {
        x: [....], y: [....],
        type: 'box',
        // + all the default box properties
        orientation: 'v',
        jitter: 0,
       marker: {
           color: 'red', 
           // ... and more
       },
    }
]

transformInverse(data, _fullData, transformIndices)

{
    x: [1, 2, 3],
    transform: [{
       type: 'candlestick',
       open: [6, 2, 3], high: [4, 1, 3], low: [3, 4, 1], close: [5, 8, 3],

        // then, all the default properties mapped back
        orientation: 'v',
        jitter: 0,
        marker: {
            color: {high: 'blue', low: 'red'},
         }
    }]
}


if(k.charAt(0) === '_') return;

if(k === 'transforms') {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think k === 'colorscales' might need a special case too. I'm not using this plugin but adapted this approach for my own purposes, and I found that when coloring by a continuous variable you end up with errors unless colorscales are left alone. (And just by inspecting the docs, perhaps ticktext and tickvals might be special as well.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ideal solution would use our Plotly.PlotSchema.crawl utility to check for data_array attributes in the incoming traces. Here, only data_array attributes would have to be truncated.

}

newTrace.name = groupName;
newTrace.marker.color = opts.groupColors[groupName];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably set the color for each relevant trace mode, i.e.

for (var i = 0; i < modes.length; i++) {
  newTrace[modes[i]].color = opts.groupColors[groupName];
}

@timelyportfolio
Copy link
Contributor

timelyportfolio commented Jun 29, 2016

looks like this pull is still alive and kicking. In terms of transforms, I think the vega vocabulary is fairly complete.

image

vega transform code

I assume though that the supporting datalib is probably beyond the scope of plotly.js.

I wonder how we might combine group_by with filter, such that the groups are true and false based on the evaluation of the filter. I think this will be a common use case in R as we try to achieve inter-widget communication.

Also, one other thought is we might want to look at how dc.js handles its filters. As a good test, I would like to try to splice a dc.js chart with plotly.js.

@mcin-armintaheri-archive

I gotta say, this transform stuff is working quite well!

Though there may be a bug, I'm still trying to figure it out:

I'm not 100% sure why this happens, but for any transform you make, if you add a new trace during the transform, you can't hide the trace by clicking it in the legend :s

I've only tried this by adding a new scatter trace with mode lines.

@etpinard
Copy link
Contributor Author

etpinard commented Jul 8, 2016

@armintaheri

I'm not 100% sure why this happens, but for any transform you make, if you add a new trace during the transform, you can't hide the trace by clicking it in the legend

Yep, that's a known bug. See #499 (comment)

If you have any insight on what the desired behaviour should be, let me know.

@timelyportfolio
Copy link
Contributor

@armintaheri, the legend seems to get fixed when I merged in @rreusser's animate-api-take-3.

@etpinard
Copy link
Contributor Author

etpinard commented Jul 8, 2016

@timelyportfolio

the legend seems to get fixed when I merged in @rreusser's animate-api-take-3.

I doubt it. That would be some crazy magic fix from 🙏 if that's the case.

@etpinard
Copy link
Contributor Author

Yeah @timelyportfolio the transforms included in this PR are meant for testing purposes only. Looking forward to seeing what you come up with 🍻

@etpinard
Copy link
Contributor Author

etpinard commented Jul 19, 2016

@bpostlethwaite please review commits (off a different branch):

etpinard added 10 commits July 19, 2016 15:47
- remove error when array container are not of the same length
- this is a temporary fix until
  #749
  is addressed.
- move traceOut.index and traceOut._input assignments
  out of supplyTraceDefaults for clarity
- 'index' ref -> index in user data (as before)
- '_input' ref -> trace in user data (as before)
- '_expandedIndex' ref -> index in fullData
- '_expandedInput' ref -> full trace after transform,
  before 2nd trace dflts
- so that the 'same' expanded trace keep the same uid
  on update.
- module.transform must be a function
- module.attributes and module.supplyDefaults are
  recommended but optional
- log warning if transform module associated with transform
  type isn't found
- extend traceIn if no supplyDefaults method is found.
@timelyportfolio
Copy link
Contributor

@etpinard, should I be using transform-plugins or transform-plugins-2?

@etpinard
Copy link
Contributor Author

@bpostlethwaite @chriddyp @timelyportfolio @rreusser @armintaheri this PR is now ready for review.

etpinard added 2 commits July 20, 2016 14:49
 assets/ is ambiguous as now test/images/assets/ exists
@bpostlethwaite
Copy link
Member

nice work! New age of hackerdackery for Plotly.js 💃 !

@etpinard etpinard merged commit e020bb1 into master Jul 22, 2016
@etpinard etpinard deleted the transform-plugins branch July 22, 2016 13:33
@etpinard
Copy link
Contributor Author

etpinard commented Aug 4, 2016

@timelyportfolio have you made any progress on your transform modules?

@timelyportfolio
Copy link
Contributor

@etpinard, yes, in multiple branches. Yesterday, I started new branch off master and added the filter with additional in \ notin and within \ notwithin (name suggestions welcome) and your groupby. @rreusser and I have been testing, playing, experimenting as we move along with his animate pull.

I moved transforms to src/transforms as you suggested, registered filter by default, and built to insure that all will work as we add other transforms.

On groupby I would like to work with you with the uuid to properly identify the sub-traces for things like the legend.

In terms of a fuller discussion in case we want to really push it, I just set up this Wiki https://github.com/timelyportfolio/plotly.js/wiki/Discussion---Transforms.

@etpinard
Copy link
Contributor Author

etpinard commented Aug 4, 2016

@timelyportfolio thanks for the info!

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 this pull request may close these issues.

9 participants