Skip to content

Layout grids #2399

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 12 commits into from
Feb 26, 2018
63 changes: 55 additions & 8 deletions src/lib/coerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,19 +257,56 @@ exports.valObjectMeta = {
'An {array} of plot information.'
].join(' '),
requiredOpts: ['items'],
otherOpts: ['dflt', 'freeLength'],
// set dimensions=2 for a 2D array
// `items` may be a single object instead of an array, in which case
// `freeLength` must be true.
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 needed 1D and 2D arbitrary-length info_array for specifying axes and/or subplots for the grid. I didn't actually end up using coerce with these, since there's validation involved where allowed values depend on both the available axes/subplots and on earlier elements in the same array; but it's still important for documentation, and for Plotly.react it's important that these are marked as info_array and not data_array.

Anyway, they work, in case we find a need in the future... tested below.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good stuff!

Would you mind updating the updatemenus and sliders attributes

image

so that they use items: {} instead of the not-as-general items: [{}, {}, {}].

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These ones are for function arguments, and we know that the functions being called have a maximum of 3 arguments. The [{}, {}, {}] form ensures we never go past three args. I suppose it probably wouldn't hurt to change it, but I didn't feel like taking that risk as I'm not sure how well tested the edge cases are for these components.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point here about args being at-most a three-item array. Let's keep that as it 👌

But this has me thinking: should we change attributes like ticktext and tickvals to free-length info_array instead of their current data_array val type? That way, we could get rid of this ugly hack.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

should we change attributes like ticktext and tickvals to free-length info_array instead of their current data_array val type?

Hmm, possibly... would certainly be nice to keep data_array out of layout, but those are potentially long enough (and likely enough to be connected to a data source) to still warrant being called data_array so I'm a bit ambivalent about it. We could discuss this in a separate issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could discuss this in a separate issue.

Sounds good 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's continue that discussion in -> #1894

otherOpts: ['dflt', 'freeLength', 'dimensions'],
coerceFunction: function(v, propOut, dflt, opts) {

// simplified coerce function just for array items
function coercePart(v, opts, dflt) {
var out;
var propPart = {set: function(v) { out = v; }};

if(dflt === undefined) dflt = opts.dflt;

exports.valObjectMeta[opts.valType].coerceFunction(v, propPart, dflt, opts);

return out;
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 had change this section to support non-array items - but also for existing info_array attributes this should be substantially faster than using regular coerce for each of the elements, since we're not constructing fake attribute strings and using them in the multiple nestedProperty constructs that coerce does.

Copy link
Contributor

Choose a reason for hiding this comment

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

Amazing thanks!

}

var twoD = opts.dimensions === 2;

if(!Array.isArray(v)) {
propOut.set(dflt);
return;
}

var items = opts.items,
vOut = [];
var items = opts.items;
var vOut = [];
var arrayItems = Array.isArray(items);
var len = arrayItems ? items.length : v.length;

var i, j, len2, vNew;

dflt = Array.isArray(dflt) ? dflt : [];

for(var i = 0; i < items.length; i++) {
exports.coerce(v, vOut, items, '[' + i + ']', dflt[i]);
if(twoD) {
for(i = 0; i < len; i++) {
vOut[i] = [];
var row = Array.isArray(v[i]) ? v[i] : [];
len2 = arrayItems ? items[i].length : row.length;
for(j = 0; j < len2; j++) {
vNew = coercePart(row[j], arrayItems ? items[i][j] : items, (dflt[i] || [])[j]);
if(vNew !== undefined) vOut[i][j] = vNew;
}
}
}
else {
for(i = 0; i < len; i++) {
vNew = coercePart(v[i], arrayItems ? items[i] : items, dflt[i]);
if(vNew !== undefined) vOut[i] = vNew;
}
}

propOut.set(vOut);
Expand All @@ -278,15 +315,25 @@ exports.valObjectMeta = {
if(!Array.isArray(v)) return false;

var items = opts.items;
var arrayItems = Array.isArray(items);
var twoD = opts.dimensions === 2;

// when free length is off, input and declared lengths must match
if(!opts.freeLength && v.length !== items.length) return false;

// valid when all input items are valid
for(var i = 0; i < v.length; i++) {
var isItemValid = exports.validate(v[i], opts.items[i]);

if(!isItemValid) return false;
if(twoD) {
if(!Array.isArray(v[i]) || (!opts.freeLength && v[i].length !== items[i].length)) {
return false;
}
for(var j = 0; j < v[i].length; j++) {
if(!exports.validate(v[i][j], arrayItems ? items[i][j] : items)) {
return false;
}
}
}
else if(!exports.validate(v[i], arrayItems ? items[i] : items)) return false;
}

return true;
Expand Down
22 changes: 19 additions & 3 deletions src/plot_api/plot_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,8 @@ function recurseIntoValObject(valObject, parts, i) {
}

// now recurse as far as we can. Occasionally we have an attribute
// setting an internal part below what's
// setting an internal part below what's in the schema; just return
// the innermost schema item we find.
for(; i < parts.length; i++) {
var newValObject = valObject[parts[i]];
if(Lib.isPlainObject(newValObject)) valObject = newValObject;
Expand All @@ -398,8 +399,23 @@ function recurseIntoValObject(valObject, parts, i) {
else if(valObject.valType === 'info_array') {
i++;
var index = parts[i];
if(!isIndex(index) || index >= valObject.items.length) return false;
valObject = valObject.items[index];
if(!isIndex(index)) return false;

var items = valObject.items;
if(Array.isArray(items)) {
if(index >= items.length) return false;
if(valObject.dimensions === 2) {
i++;
if(parts.length === i) return valObject;
var index2 = parts[i];
if(!isIndex(index2)) return false;
valObject = items[index][index2];
}
else valObject = items[index];
}
else {
valObject = items;
}
}
}

Expand Down
7 changes: 1 addition & 6 deletions test/image/mocks/grouped_bar.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@
"xaxis": {
"type": "category"
},
"barmode": "group",
"categories": [
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 this was supposed to be xaxis.categories? Anyway it was ignored so 🔪

Copy link
Contributor

Choose a reason for hiding this comment

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

This mock is ancient. Way older than the axis.categories attribute. This must be a typo. 👍

"giraffes",
"orangutans",
"monkeys"
]
"barmode": "group"
}
}
5 changes: 4 additions & 1 deletion test/jasmine/tests/annotations_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,10 @@ describe('annotation effects', function() {
})
.then(function() {
expect(gd._fullLayout.annotations[0].x).toBe('2018-01-29 13:29:41.4857');
expect(gd._fullLayout.annotations[0].y).toBe('2017-02-02 06:36:46.8112');
// AJ loosened this test - expected '2017-02-02 06:36:46.8112'
// but when I run it I get '2017-02-02 06:28:39.9586'.
// must be different fonts altering autoranging
expect(gd._fullLayout.annotations[0].y.substr(0, 10)).toBe('2017-02-02');
})
.catch(failTest)
.then(done);
Expand Down
4 changes: 2 additions & 2 deletions test/jasmine/tests/gl3d_plot_interact_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1020,8 +1020,8 @@ describe('@gl Test gl3d annotations', function() {
camera.eye = {x: x, y: y, z: z};
scene.setCamera(camera);
// need a fairly long delay to let the camera update here
// 200 was not robust for me (AJ), 300 seems to be.
return delay(300)();
// 300 was not robust for me (AJ), 500 seems to be.
return delay(500)();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should I be worried that I've had to increase this delay twice now? I just ignored this test for a while, but it had been failing for me locally for at least the last 6 months...

}

it('should move with camera', function(done) {
Expand Down
71 changes: 71 additions & 0 deletions test/jasmine/tests/lib_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,77 @@ describe('Test lib.js:', function() {
expect(coerce({domain: [0, 0.5, 1]}, {}, infoArrayAttrs, 'domain'))
.toEqual([0, 0.5]);
});

it('supports bounded freeLength attributes', function() {
var attrs = {
x: {
valType: 'info_array',
freeLength: true,
items: [
{valType: 'integer', min: 0},
{valType: 'integer', max: -1}
],
dflt: [1, -2]
},
};
expect(coerce({}, {}, attrs, 'x')).toEqual([1, -2]);
expect(coerce({x: []}, {}, attrs, 'x')).toEqual([1, -2]);
expect(coerce({x: [5]}, {}, attrs, 'x')).toEqual([5, -2]);
expect(coerce({x: [-5]}, {}, attrs, 'x')).toEqual([1, -2]);
expect(coerce({x: [5, -5]}, {}, attrs, 'x')).toEqual([5, -5]);
expect(coerce({x: [3, -3, 3]}, {}, attrs, 'x')).toEqual([3, -3]);
});

it('supports unbounded freeLength attributes', function() {
var attrs = {
x: {
valType: 'info_array',
freeLength: true,
items: {valType: 'integer', min: 0, dflt: 1}
}
};
expect(coerce({}, {}, attrs, 'x')).toBeUndefined();
expect(coerce({x: []}, {}, attrs, 'x')).toEqual([]);
expect(coerce({x: [3]}, {}, attrs, 'x')).toEqual([3]);
expect(coerce({x: [-3]}, {}, attrs, 'x')).toEqual([1]);
expect(coerce({x: [-1, 4, 'hi', 5]}, {}, attrs, 'x'))
.toEqual([1, 4, 1, 5]);
});

it('supports 2D fixed-size arrays', function() {
var attrs = {
x: {
valType: 'info_array',
dimensions: 2,
items: [
[{valType: 'integer', min: 0, max: 2}, {valType: 'integer', min: 3, max: 5}],
[{valType: 'integer', min: 6, max: 8}, {valType: 'integer', min: 9, max: 11}]
],
dflt: [[1, 4], [7, 10]]
}
};
expect(coerce({}, {}, attrs, 'x')).toEqual([[1, 4], [7, 10]]);
expect(coerce({x: []}, {}, attrs, 'x')).toEqual([[1, 4], [7, 10]]);
expect(coerce({x: [[0, 3], [8, 11]]}, {}, attrs, 'x'))
.toEqual([[0, 3], [8, 11]]);
expect(coerce({x: [[10, 5, 10], [6], [1, 2, 3]]}, {}, attrs, 'x'))
.toEqual([[1, 5], [6, 10]]);
});

it('supports unbounded 2D freeLength arrays', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for adding Lib.validate tests!

Would it too much to ask to add a Plotly.validate test for the new grid attribute?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call, turns out Plotly.validate didn't dig into info_array - was there any reason for that? Anyway, now it does -> 40dd784
I had to tweak the behavior there, '' vs undefined - seems reasonable, shouldn't affect behavior but cleans up validate.

var attrs = {
x: {
valType: 'info_array',
freeLength: true,
dimensions: 2,
items: {valType: 'integer', min: 0, dflt: 1}
}
};
expect(coerce({}, {}, attrs, 'x')).toBeUndefined();
expect(coerce({x: []}, {}, attrs, 'x')).toEqual([]);
expect(coerce({x: [[], [0], [-1, 2], [5, 'a', 4, 6.6]]}, {}, attrs, 'x'))
.toEqual([[], [0], [1, 2], [5, 1, 4, 1]]);
});
});

describe('subplotid valtype', function() {
Expand Down