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

restyle/relayout refactor #1999

Merged
merged 43 commits into from
Sep 20, 2017
Merged

restyle/relayout refactor #1999

merged 43 commits into from
Sep 20, 2017

Conversation

alexcjohnson
Copy link
Collaborator

Something we've wanted to do for a looong time... get (nearly all) the attribute names and regex tests out of _restyle and _relayout, and use the schema instead:

  • We already had editType as an optional field in the attribute schema, this makes it mandatory and uses it as the key way to determine what flavor of redraw to invoke.
  • Adds a new optional field impliedEdits for correlated changes - for example range has impliedEdits: {autorange: false} because setting an explicit range is expected to disable the associated autorange. Note that the converse autorange.impliedEdits: {range: undefined} is used as it was before just to record the previous values for use in undo/redo, which we've discussed removing in v2.

Also includes a fix for #1325 as that came along essentially for free with generalizing our attribute string regexes.

I want to do a bit more testing of specific restyle/relayout calls to make sure we're doing the right redraws in all cases - especially of base/module interactions like hoverlabel, and module/module interactions like heatmapgl/colorbar. But I'd appreciate a high-level review ASAP @etpinard @rreusser

@@ -21,6 +20,7 @@ module.exports = {
valType: 'boolean',
role: 'info',
dflt: true,
editType: 'docalcAutorange',
Copy link
Contributor

Choose a reason for hiding this comment

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

Would docalc+autorange be a sensible name for this? It sounds like an enum camelcased and concatenated, but I could be mistaken.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see. Unless I'm mistaken, this adds no new labels. That's just plugging into what already exists. Still seems like maybe an enum in disguise, but nothing to change here then.

Copy link
Contributor

Choose a reason for hiding this comment

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

One additional note while digging through here is that the do- prefix seems to be carried forward from what was originally just internal variable names. Would editType: 'calcAutorange' or editType: 'calc+autorange' make sense? It seems a bit friendlier if you're trying to make sense of the schema as a standalone concept.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

docalcAutorange is a single thing, means "do a recalc only if there's an autoranged axis related to this attribute" - Actually, ideally we'd specify what to do if there isn't an autoranged axis... so like docalcAutorange+dostyle or something.

Would editType: 'calcAutorange' make sense?

Yeah totally, good idea! And to help us out (to figure out what editType a new attribute should get) I should probably describe somewhere what these flags actually mean in terms of the code paths they lead to.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe calcIfAutorange?

to figure out what editType a new attribute should get

FWIW, I have a history of making bad choices here for lack of really understanding the implications of some schema concepts. If you want I can try making docs/schema.md that explains, at the very least, what some of the schema items link up with. Some of the concepts like _isLinkedToArray get a little subtle. Even for the fifth time interacting with it, I think a brief description with links to a couple relevant spots in the code would help.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe calcIfAutorange?

🏆

If you want I can try making docs/schema.md

Definitely time for something like that! @etpinard thoughts about how/where to do this? Should it be a standalone thing, or somehow included in Plotly.PlotSchema.get() or...?

Copy link
Contributor

Choose a reason for hiding this comment

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

My vote would be for not including in the code so that it's as accessible and readable as possible without having to realize it's an API function and instantiate anything in order to access.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd vote for adding a attribute-like edit type declaration object in edit_types.js with a description field. For example,

var traceEditTypes: {
  docalc: {
    description: [
       // ....
    ].join(' ')
  },
  // ....
}

That description field would get stripped off by the compress_attributes transform. Plotly.PlotSchema.get().defs.editTypes would then return the list of edit types with their description.

I see your point @rreusser about relying an API function to show docs being less-than-ideal. But since we already have a hard time keeping https://github.com/plotly/documentation/ up to speed, adding another layer of static (e.g. a github wiki or a docs/ directory) documentation sounds dangerous. No docs is better than bad docs, right? That said, it wouldn't hurt adding a few lines in our CONTRIBUTING.md over the edit types.

font: extendFlat({}, fontAttrs, {
font: fontAttrs({
editType: 'docalcAutorange',
colorEditType: 'doarraydraw',
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you point me toward what doarraydraw does? It's not entirely clear from the name, and if it appears somewhere else in the source, it's not jumping out at me.

Copy link
Contributor

Choose a reason for hiding this comment

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

Never mind. Found it in the suppressed diff.

Copy link
Contributor

@etpinard etpinard Sep 13, 2017

Choose a reason for hiding this comment

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

Wait, where is doarraydraw being used? I can't find it (even in the suppressed diffs 🙃 )

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think at one point I had none instead of doarraydraw but that looked weird. Then at another point I had a block just for debugging that checked that everything labeled doarraydraw was captured by an arrayContainers block and threw an error otherwise, but I removed that once I was satisfied that it was never getting hit... happy to change it if you think it would make more sense another way.

Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand correctly, doarraydraw is just a placeholder for edits in array containers that are handled by applyContainerArrayChanges in manage_array.js.

In this case, I prefer setting editType: 'doarraydraw' over editType: 'none'. Just make sure that this behavior is documented in the edit type description field (cc #1999 (comment)).

@@ -13,6 +13,8 @@ module.exports = {
valType: 'boolean',
role: 'info',
dflt: true,
editType: 'docalc',
impliedEdits: {zmin: undefined, zmax: undefined},
Copy link
Contributor

Choose a reason for hiding this comment

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

Same with impliedEdits. I assume these are attributes that are automatically applied within the same attr path, but I'm unable to find where it's implemented and it's not immediately clear what the ^ prefix means.

Copy link
Contributor

Choose a reason for hiding this comment

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

Never mind. Github was suppressing the plot_api diff. Found it.

}),
color: extendFlat({}, fontAttrs.color)
},
font: fontAttrs,
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

* an attribute string, such as 'annotations[3].x'. The "current location"
* is the attribute string minus the last component ('annotations[3]')
* @param {string} relativeAttr:
* a route to the desired attribute string, using '^' to ascend
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I think I understand. So relative to a.b.c, ^foo would be a.foo. I don't know a solution, but one challenge with the schema is that it implements a lot of very precise semantics that can only be understood by locating the part of the code that handles them and working backwards (which isn't always possible since you might not even know that python is doing something with role, for example). I'm tempted to take a stab at docs/schema.md.

'use strict';


var ASCEND = /^(.*)(\.[^\.\[\]]+|\[\d\])$/;
Copy link
Contributor

@rreusser rreusser Sep 13, 2017

Choose a reason for hiding this comment

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

Could you add a very short comment about what this matches? In particular figuring out whether ^ is negation or an actual caret takes a good amount of staring and recalling js-specific regex escaping rules since this particular use could very reasonably involve either one.

Copy link
Contributor

@rreusser rreusser Sep 13, 2017

Choose a reason for hiding this comment

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

Re-reading, matches foo.string or foo[5] or foo.bar[5], I think, while capturing everything before the last chunk (as well as the last chunk but only so it can be thrown away). It's not so bad, I guess. Oh, so ASCEND refers to the way it chops off the last attr chunk. I see.


if(valObject && valObject.impliedEdits && newVal !== null) {
for(var impliedKey in valObject.impliedEdits) {
doextra(Lib.relativeAttr(ai, impliedKey), valObject.impliedEdits[impliedKey], i);
Copy link
Contributor

Choose a reason for hiding this comment

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

One question about implied edits is whether it moves away from declarative plots. That is, if you set foo (which, e.g., implicitly also sets bar: false), is it valid to then set bar: true? Is there pathway that might accidentally circumvent the implied edits and lead to a bit of inconsistency? Would it be better/worse to think of implied edits as a concern of the plotly API consumer rather than the plotly API itself? Like the workspace would recognize the attribute you're modifying has an implied edit and send both of those to the API? It seems nice from an end-user perspective; my only concern is that it might be surprising/confusing to modify an attribute and find that additional changes have been made to your plot state.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This PR by itself isn't moving toward or away from anything as far as users are concerned... all of these impliedEdits were previously handled by dedicated blocks in _restyle/_relayout. Without them, a call like relayout(gd, 'xaxis.range', [5, 10]) wouldn't do anything if xaxis.autorange was true.

Looking ahead though to Plotly.react, I'm hoping that by having impliedEdits in the schema we can help people make these implied changes in a transparent way - so if you have a range control that modifies the state and passes it back to the plot in Plotly.react, it would be able to look up the implied changes we think it should make, but the control would have final say over whether to apply those changes or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it. Thanks for the clarification.

if(valObject) {
/*
* In occasional cases we can't the innermost valObject
Copy link
Contributor

Choose a reason for hiding this comment

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

"can't ____ the innermost…"

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh never mind. This is in the removed section. 👍

@@ -208,6 +216,182 @@ exports.findArrayAttributes = function(trace) {
return arrayAttributes;
};

/*
* Find the valObject for one attribute in an existing trace
Copy link
Contributor

@rreusser rreusser Sep 13, 2017

Choose a reason for hiding this comment

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

Is a valObject just a value? Or is it a nested property object? Or the attribute definition?
Or something else?

Copy link
Contributor

Choose a reason for hiding this comment

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

Less vaguely, valObjects are defined here, but I'm trying to process how the return value relates to those definitions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

hah yeah, coerce.valObjects describes valObjects but it isn't itself at set of valObjects. What we mean by it in this context is a schema entry describing one attribute. I'll see what I can do in terms of documenting this better...

Copy link
Contributor

Choose a reason for hiding this comment

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

Not a big deal. Just another part of the code I haven't gone too deep in. To clear up a common misconception though, naming in CS is a very easy problem (randomly generated strings will do). Naming for maximum interpretability is the hard part. 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like coerce.valObjects should be named coerce.valObjectMeta instead. To me and to PlotSchema.isValObject a val object is any object with a valType key - which is essentially what getTraceValObject and getLayoutValObject return.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

valObjectMeta - and descriptions for editType and impliedEdits that make their way into the schema - in 96cc57f

var typeAttr = axAttr + '.type';

if(layoutUpdate[axAttr] === undefined && layoutUpdate[typeAttr] === undefined) {
Lib.nestedProperty(gd.layout, typeAttr).set(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

Much better than delete /* */ 👌

package.json Outdated
@@ -119,6 +119,7 @@
"gzip-size": "^3.0.0",
"image-size": "^0.5.1",
"jasmine-core": "^2.4.1",
"jsdom": "^9.5.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not use the latest version jsdom@11?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Because 9.5.0 is what we have in streambed, and it looked like it was going to take me more than 5 minutes to figure out the new API 🙈

But you're right, not exactly a good reason - I'll try to upgrade it.

// jsdom by itself doesn't support getContext, and adding the npm canvas
// package is annoying and platform-dependent.
// see https://github.com/tmpvar/jsdom/issues/1782
w.HTMLCanvasElement.prototype.getContext = function() { return null; };
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

dunno why it didn't happen with the old version, but with the new jsdom without this patch you get an ugly but nonblocking error Error: Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, no need to add a dependency for this 👌

@@ -2152,10 +2154,6 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) {
// so we auto-set them again
var axLetters = ['x', 'y', 'z'];
function clearAxisTypes(gd, traces, layoutUpdate) {
if(typeof traces === 'number') traces = [traces];
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks!


var d3 = require('d3');

module.exports = function checkTicks(axLetter, vals, msg) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I've been trying to centralized common custom assertions like this one in:

https://github.com/plotly/plotly.js/blob/master/test/jasmine/assets/custom_assertions.js

I'll let decide whether separate files or a common module is better for stuff like this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh that's nice - yeah, I'll put checkTicks in there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

* if(xIsSomethingElse) expect(x).not.toBe(0);
* else expect(x).toBe(0);
*/
module.exports = function negateIf(expectation, negate) {
Copy link
Contributor

@etpinard etpinard Sep 20, 2017

Choose a reason for hiding this comment

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

Non-blocking but I wonder if there's a way to rewrite this as a custom jasmine matcher. So that we have something like:

expect(x).negateIf(negate).toBe(0)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

expect(x).negateIf(negate).toBe(0) would definitely be more readable - I'll give it a shot.

@@ -463,6 +463,10 @@ describe('Test histogram', function() {
describe('plot / restyle', function() {
var gd;

beforeAll(function() {
jasmine.addMatchers(customMatchers);
Copy link
Contributor

Choose a reason for hiding this comment

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

(note-to-self comment) I'm getting tired of these jasmine.addMatchers. I'll try to find a way to add our custom matchers globally once and for all.

Copy link
Contributor

Choose a reason for hiding this comment

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

❤️ 🎉 🥇 👏 💯

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

global addMatchers, and turning negateIf into a method (which I could only see how to do globally) ->0729921 hopefully it works right on circle 🙏

@etpinard
Copy link
Contributor

💃 💃 💃

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

Successfully merging this pull request may close these issues.

4 participants