Skip to content
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

Cartesian subplot updates using data joins #946

Merged
merged 17 commits into from
Oct 6, 2016
Merged

Conversation

etpinard
Copy link
Contributor

resolves #634

In trying to make the cartesian plot routine a little more reusable for range slider 2.0, I ran into these

// in plot_api.js
var subplots = Plotly.Axes.getSubplots(gd).join(''),
      oldSubplots = Object.keys(gd._fullLayout._plots || {}).join(''),
      hasSameSubplots = (oldSubplots === subplots);

if(!hasSameSubplots) makePlotFramework(gd);

which basically destroys and redraw all axes every time the subplot layout changes. We could do better.

In this PR, I attempt to move the subplot creation / updates out of the makePlotFramework and into the Cartesian module using a d3 data join. In doing so, fullLayout._plots logic is cleaned up considerably (resolving issue #634).

cc @rreusser @alexcjohnson

- which handles fullLayout._plots creation and reference updates
- rm block in Plots.transition that used to handle that
- we must now call drawAxes AFTER drawData (i.e. after updateSubplots)
  as drawAxes looks for some <g> that updateSubplots creates
- move layoutStyles / Fx.init out of makePlotFramework
- make makePlotFramework return 'FRAMEWORK' so that
  restyle polar <-> cartesian still works
@etpinard
Copy link
Contributor Author

This PR is currently in progress. Some image test mocks and a few relayout tests are failing.

Most of these issues are - I believe - caused by Axes.doTicks which computes and mutates a few axis properties as well as drawing ticks - which turned out to be order dependent.

To note also, subroutine layoutStyles also mutates an axis property (this time _linepositions).

- so that these ref on fullLayout exist even for
  non-cartesian trace types.
- after several failed attempts at getting cartesian
  subplot creation/update/removal into the Cartesian.plot
  (like the other base plot module), I decide to go with a less
  ambitious refactoring where a drawFramework update step
  is added to the main Plotly.plot code path
- Main reason: several layout components require the cartesian
  framework to be present in order to work properly.
- this suite used to rely on className 'bg' to grab
  the legend background element, but now
  className 'bg' is also used in <g subplot>.
- doCalcdata is now called before drawFramwework,
  so the ordering for which _ keys are added to the gd
  differs slightly.
subplotLayers.enter().append('g')
.classed('subplot', true);

subplotLayers.order();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

See http://codepen.io/etpinard/pen/mAEBVz for example of this pattern

@etpinard
Copy link
Contributor Author

In commit 937c54b I settled for a less ambitious refactoring attempt in this PR.

It turns out that (parts of) the cartesian framework are required in order to make (some) layout components draw properly. Meaning that we must draw the cartesian framework before getting to the main basePlotModules loop in the drawData sub-task - unlike other subplot types.

So, I decided to a drawFramework method on the Cartesian module object.

In a future PR(s), we should spent some time refactoring axes.js where several drawing methods mutate the axis objects, making it really hard to know what step should come first. In other words, we should try to refactor axes.js into calc steps and draw steps.

@@ -54,7 +54,7 @@ describe('Test Plots', function() {
expect(gd._fullData[1].z).toBe(newData[1].z);
expect(gd._fullData[1]._empties).toBe(oldFullData[1]._empties);
expect(gd._fullLayout.scene._scene).toBe(oldFullLayout.scene._scene);
expect(gd._fullLayout._plots).toBe(oldFullLayout._plots);
expect(gd._fullLayout._plots.plot).toBe(oldFullLayout._plots.plot);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

_plots is no longer relinked in Plots.supplyDefaults as

newFullLayout._plots = oldFullLayout._plots

but as

    var oldSubplots = oldFullLayout._plots || {},
        newSubplots = newFullLayout._plots = {};

    var ids = Plotly.Axes.getSubplots(mockGd);

    for(var i = 0; i < ids.length; i++) {
        var id = ids[i],
            oldSubplot = oldSubplots[id],
            plotinfo;

        if(oldSubplot) {
            plotinfo = newSubplots[id] = oldSubplot;
        }
        else {
            plotinfo = newSubplots[id] = {};
            plotinfo.id = id;
        }

        plotinfo.x = getAxisFunc(id, 'x');
        plotinfo.y = getAxisFunc(id, 'y');
        plotinfo.xaxis = plotinfo.x();
        plotinfo.yaxis = plotinfo.y();
    }

cc @rreusser

Copy link
Contributor

Choose a reason for hiding this comment

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

So it looks like it reconstructs a new _plots object with the same contents vs. just transferring it? Is that what makes the join work correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it looks like it reconstructs a new _plots object with the same contents vs. just transferring it?

Correct. Thanks for the headsup. Maybe we could do better.

Is that what makes the join work correctly?

I don't think reconstructing _plots is necessary to make the joins work correctly.


Plotly.addTraces(gd, { x: [1, 2, 3], y: [4, 3, 2], name: 'Test2' }).then(function() {
legend = document.getElementsByClassName('legend')[0];
bg = document.getElementsByClassName('bg')[0];
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 subplot bg layer now also has class bg, which made this query grab the wrong node.

// the new layout gets ignored (as it should)
// but if there's no data there yet, it's just a placeholder...
// then it should destroy and remake the plot
if(hasData) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alexcjohnson can you review these few lines?

Can you think of any other (edge) cases that I could have missed?

[] :
[marginPushers, subroutines.layoutStyles];

return Lib.syncOrAsync(seq, gd);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

no need to call Fx.init here too as it is called in drawFramework on every Plotly.plot.

Previously, we need a change of subplot in order to trigger Fx.init.

}


function joinLayer(parent, nodeType, className) {
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 enough for Lib function ???

Copy link
Contributor

@rreusser rreusser Oct 5, 2016

Choose a reason for hiding this comment

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

Yes, def. We use this pattern all over the place, and it's invalid in d3 v4 without some minor modifications, so would be good to extract.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll defer that to a future PR.

@@ -559,6 +562,41 @@ function relinkPrivateKeys(toContainer, fromContainer) {
}
}

plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This routine here should make fullLayout._plots[' '].x() and fullLayout._plots[' '].y() method obsolete.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 2b05978

}

plotinfo.x = getAxisFunc(id, 'x');
plotinfo.y = getAxisFunc(id, 'y');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here they are for backward-compatibility.

It might be worth the time to remove all x() and y() and replace them with _plots[''].xaxis and _plots[''].yaxis respectively.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I meant here. done in 2b05978

@etpinard etpinard added this to the v1.18.0 milestone Oct 5, 2016
Copy link
Contributor

@rreusser rreusser left a comment

Choose a reason for hiding this comment

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

I don't see anything blocking here. I don't understand every bit of it since I haven't gotten too deep into the plot framework, but I like the change, so if it passes the tests, then I say 💃🏻

var xa = plotinfo.x(),
ya = plotinfo.y();

var xa = plotinfo.xaxis,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is plotinfo.x() removed here or in general? I had some subtle problems with one or the other being up to date as axis references change.

}
else if(!hasSameSubplots) {
// polar need a different framework
if(gd.framework !== makePlotFramework) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The naming is a bit confusing to me since framework = noun while makePlotFramework = verb, but this is probably nothing new and not a big deal.

Copy link
Contributor Author

@etpinard etpinard Oct 6, 2016

Choose a reason for hiding this comment

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

I agree this is very strange. But, like pointed out by the comment above, the only reason this line is here is because of polar.

As polar is so broken at the moment, I prefer not changing things too much until we fully refactor it.

.style('fill', 'none')
.classed('crisp', true);
});
return 'FRAMEWORK';
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this return code do? It seems 10% strange, so maybe a comment would be good if it's actually needed/expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh good catch.

There's no need for this line anymore.

@@ -1369,8 +1373,8 @@ axes.doTicks = function(gd, axid, skipTitle) {
if(axid === 'redraw') {
fullLayout._paper.selectAll('g.subplot').each(function(subplot) {
var plotinfo = fullLayout._plots[subplot],
xa = plotinfo.x(),
ya = plotinfo.y();
xa = plotinfo.xaxis,
Copy link
Contributor

Choose a reason for hiding this comment

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

Big fan of this change! 🍻

}


function joinLayer(parent, nodeType, className) {
Copy link
Contributor

@rreusser rreusser Oct 5, 2016

Choose a reason for hiding this comment

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

Yes, def. We use this pattern all over the place, and it's invalid in d3 v4 without some minor modifications, so would be good to extract.

@@ -54,7 +54,7 @@ describe('Test Plots', function() {
expect(gd._fullData[1].z).toBe(newData[1].z);
expect(gd._fullData[1]._empties).toBe(oldFullData[1]._empties);
expect(gd._fullLayout.scene._scene).toBe(oldFullLayout.scene._scene);
expect(gd._fullLayout._plots).toBe(oldFullLayout._plots);
expect(gd._fullLayout._plots.plot).toBe(oldFullLayout._plots.plot);
Copy link
Contributor

Choose a reason for hiding this comment

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

So it looks like it reconstructs a new _plots object with the same contents vs. just transferring it? Is that what makes the join work correctly?

- to allow calling outside the drawFramework step
@rreusser
Copy link
Contributor

rreusser commented Oct 6, 2016

None of the changes or comments were blocking, so gets the 💃🏻 from me (once you're happy)!

return subplotData;
}

function makeSubplotLayer(plotinfo) {
Copy link
Contributor Author

@etpinard etpinard Oct 6, 2016

Choose a reason for hiding this comment

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

Important the subplot layer join step is free of references to gd._fullLayout.

This means that other components (e.g. the range slider 😏 ) can call it (with sufficiently mocked plotinfo object with xaxis, yaxis, _id and plotgroup ) and get the subplot layers appended to plotinfo.plotgroup in return !

Copy link
Contributor

Choose a reason for hiding this comment

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

Hooray for clever code reuse! As long as no too clever. 🎉

@etpinard etpinard mentioned this pull request Oct 6, 2016
@etpinard etpinard merged commit 5a08fbc into master Oct 6, 2016
@etpinard etpinard deleted the cartesian-per-subplot branch October 6, 2016 02:50
@rreusser
Copy link
Contributor

rreusser commented Oct 6, 2016

Merge looks great, but I did have a question about this:

how does it affect non-cartesian plots? Or does it affect non-cartesian plots? Specifically, does this add or modify the considerations involved in adding object constancy to pie or bar charts? (though bar is cartesian, right? I haven't worked with them much beyond making sure I don't break them)

@etpinard
Copy link
Contributor Author

etpinard commented Oct 6, 2016

how does it affect non-cartesian plots?

It should not affect non-cartesian whatsoever. After this PR, all cartesian subplots are inserted in <g cartesianlayer>. You can that pies are inserted here and similarly for other plot types.

All per-subplot routines are now called inside basePlotModules loop e.g. here and here, so I'm pretty confident this PR has cross-subplot type side-effects.

This was referenced Oct 17, 2016
etpinard added a commit that referenced this pull request Jan 10, 2017
etpinard added a commit that referenced this pull request Jul 6, 2017
- bug noticed when both overlaying x and y axes have
  `showline: true`.
- ... but `Plotly.restyle(gd, 'visible', true, [1])` does
      not work for the added mock
- both bugs have been broken since PR
    #946
  released in 1.18.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Make sure that fullLayout._plots is updated on relayout
2 participants