Skip to content

Commit 77c218a

Browse files
authored
Merge pull request #2399 from plotly/layout-grid
Layout grids
2 parents 1256c4f + 794669b commit 77c218a

38 files changed

+1241
-185
lines changed

src/lib/coerce.js

+55-8
Original file line numberDiff line numberDiff line change
@@ -257,19 +257,56 @@ exports.valObjectMeta = {
257257
'An {array} of plot information.'
258258
].join(' '),
259259
requiredOpts: ['items'],
260-
otherOpts: ['dflt', 'freeLength'],
260+
// set dimensions=2 for a 2D array
261+
// `items` may be a single object instead of an array, in which case
262+
// `freeLength` must be true.
263+
otherOpts: ['dflt', 'freeLength', 'dimensions'],
261264
coerceFunction: function(v, propOut, dflt, opts) {
265+
266+
// simplified coerce function just for array items
267+
function coercePart(v, opts, dflt) {
268+
var out;
269+
var propPart = {set: function(v) { out = v; }};
270+
271+
if(dflt === undefined) dflt = opts.dflt;
272+
273+
exports.valObjectMeta[opts.valType].coerceFunction(v, propPart, dflt, opts);
274+
275+
return out;
276+
}
277+
278+
var twoD = opts.dimensions === 2;
279+
262280
if(!Array.isArray(v)) {
263281
propOut.set(dflt);
264282
return;
265283
}
266284

267-
var items = opts.items,
268-
vOut = [];
285+
var items = opts.items;
286+
var vOut = [];
287+
var arrayItems = Array.isArray(items);
288+
var len = arrayItems ? items.length : v.length;
289+
290+
var i, j, len2, vNew;
291+
269292
dflt = Array.isArray(dflt) ? dflt : [];
270293

271-
for(var i = 0; i < items.length; i++) {
272-
exports.coerce(v, vOut, items, '[' + i + ']', dflt[i]);
294+
if(twoD) {
295+
for(i = 0; i < len; i++) {
296+
vOut[i] = [];
297+
var row = Array.isArray(v[i]) ? v[i] : [];
298+
len2 = arrayItems ? items[i].length : row.length;
299+
for(j = 0; j < len2; j++) {
300+
vNew = coercePart(row[j], arrayItems ? items[i][j] : items, (dflt[i] || [])[j]);
301+
if(vNew !== undefined) vOut[i][j] = vNew;
302+
}
303+
}
304+
}
305+
else {
306+
for(i = 0; i < len; i++) {
307+
vNew = coercePart(v[i], arrayItems ? items[i] : items, dflt[i]);
308+
if(vNew !== undefined) vOut[i] = vNew;
309+
}
273310
}
274311

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

280317
var items = opts.items;
318+
var arrayItems = Array.isArray(items);
319+
var twoD = opts.dimensions === 2;
281320

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

285324
// valid when all input items are valid
286325
for(var i = 0; i < v.length; i++) {
287-
var isItemValid = exports.validate(v[i], opts.items[i]);
288-
289-
if(!isItemValid) return false;
326+
if(twoD) {
327+
if(!Array.isArray(v[i]) || (!opts.freeLength && v[i].length !== items[i].length)) {
328+
return false;
329+
}
330+
for(var j = 0; j < v[i].length; j++) {
331+
if(!exports.validate(v[i][j], arrayItems ? items[i][j] : items)) {
332+
return false;
333+
}
334+
}
335+
}
336+
else if(!exports.validate(v[i], arrayItems ? items[i] : items)) return false;
290337
}
291338

292339
return true;

src/lib/regex.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@
88

99
'use strict';
1010

11-
// Simple helper functions
12-
// none of these need any external deps
13-
1411
/*
1512
* make a regex for matching counter ids/names ie xaxis, xaxis2, xaxis10...
16-
* eg: regexCounter('x')
17-
* tail is an optional piece after the id
18-
* eg regexCounter('scene', '.annotations') for scene2.annotations etc.
13+
*
14+
* @param {string} head: the head of the pattern, eg 'x' matches 'x', 'x2', 'x10' etc.
15+
* 'xy' is a special case for cartesian subplots: it matches 'x2y3' etc
16+
* @param {Optional(string)} tail: a fixed piece after the id
17+
* eg counterRegex('scene', '.annotations') for scene2.annotations etc.
18+
* @param {boolean} openEnded: if true, the string may continue past the match.
1919
*/
2020
exports.counter = function(head, tail, openEnded) {
21-
return new RegExp('^' + head + '([2-9]|[1-9][0-9]+)?' +
22-
(tail || '') + (openEnded ? '' : '$'));
21+
var fullTail = (tail || '') + (openEnded ? '' : '$');
22+
if(head === 'xy') {
23+
return new RegExp('^x([2-9]|[1-9][0-9]+)?y([2-9]|[1-9][0-9]+)?' + fullTail);
24+
}
25+
return new RegExp('^' + head + '([2-9]|[1-9][0-9]+)?' + fullTail);
2326
};

src/lib/stats.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ var isNumeric = require('fast-isnumeric');
2828
exports.aggNums = function(f, v, a, len) {
2929
var i,
3030
b;
31-
if(!len) len = a.length;
31+
if(!len || len > a.length) len = a.length;
3232
if(!isNumeric(v)) v = false;
3333
if(Array.isArray(a[0])) {
3434
b = new Array(len);

src/plot_api/plot_schema.js

+19-3
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,8 @@ function recurseIntoValObject(valObject, parts, i) {
383383
}
384384

385385
// now recurse as far as we can. Occasionally we have an attribute
386-
// setting an internal part below what's
386+
// setting an internal part below what's in the schema; just return
387+
// the innermost schema item we find.
387388
for(; i < parts.length; i++) {
388389
var newValObject = valObject[parts[i]];
389390
if(Lib.isPlainObject(newValObject)) valObject = newValObject;
@@ -398,8 +399,23 @@ function recurseIntoValObject(valObject, parts, i) {
398399
else if(valObject.valType === 'info_array') {
399400
i++;
400401
var index = parts[i];
401-
if(!isIndex(index) || index >= valObject.items.length) return false;
402-
valObject = valObject.items[index];
402+
if(!isIndex(index)) return false;
403+
404+
var items = valObject.items;
405+
if(Array.isArray(items)) {
406+
if(index >= items.length) return false;
407+
if(valObject.dimensions === 2) {
408+
i++;
409+
if(parts.length === i) return valObject;
410+
var index2 = parts[i];
411+
if(!isIndex(index2)) return false;
412+
valObject = items[index][index2];
413+
}
414+
else valObject = items[index];
415+
}
416+
else {
417+
valObject = items;
418+
}
403419
}
404420
}
405421

src/plot_api/validate.js

+51-5
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,65 @@ function crawl(objIn, objOut, schema, list, base, path) {
164164
var valIn = objIn[k],
165165
valOut = objOut[k];
166166

167-
var nestedSchema = getNestedSchema(schema, k),
168-
isInfoArray = (nestedSchema || {}).valType === 'info_array',
169-
isColorscale = (nestedSchema || {}).valType === 'colorscale';
167+
var nestedSchema = getNestedSchema(schema, k);
168+
var isInfoArray = (nestedSchema || {}).valType === 'info_array';
169+
var isColorscale = (nestedSchema || {}).valType === 'colorscale';
170+
var items = (nestedSchema || {}).items;
170171

171172
if(!isInSchema(schema, k)) {
172173
list.push(format('schema', base, p));
173174
}
174175
else if(isPlainObject(valIn) && isPlainObject(valOut)) {
175176
crawl(valIn, valOut, nestedSchema, list, base, p);
176177
}
178+
else if(isInfoArray && isArray(valIn)) {
179+
if(valIn.length > valOut.length) {
180+
list.push(format('unused', base, p.concat(valOut.length)));
181+
}
182+
var len = valOut.length;
183+
var arrayItems = Array.isArray(items);
184+
if(arrayItems) len = Math.min(len, items.length);
185+
var m, n, item, valInPart, valOutPart;
186+
if(nestedSchema.dimensions === 2) {
187+
for(n = 0; n < len; n++) {
188+
if(isArray(valIn[n])) {
189+
if(valIn[n].length > valOut[n].length) {
190+
list.push(format('unused', base, p.concat(n, valOut[n].length)));
191+
}
192+
var len2 = valOut[n].length;
193+
for(m = 0; m < (arrayItems ? Math.min(len2, items[n].length) : len2); m++) {
194+
item = arrayItems ? items[n][m] : items;
195+
valInPart = valIn[n][m];
196+
valOutPart = valOut[n][m];
197+
if(!Lib.validate(valInPart, item)) {
198+
list.push(format('value', base, p.concat(n, m), valInPart));
199+
}
200+
else if(valOutPart !== valInPart && valOutPart !== +valInPart) {
201+
list.push(format('dynamic', base, p.concat(n, m), valInPart, valOutPart));
202+
}
203+
}
204+
}
205+
else {
206+
list.push(format('array', base, p.concat(n), valIn[n]));
207+
}
208+
}
209+
}
210+
else {
211+
for(n = 0; n < len; n++) {
212+
item = arrayItems ? items[n] : items;
213+
valInPart = valIn[n];
214+
valOutPart = valOut[n];
215+
if(!Lib.validate(valInPart, item)) {
216+
list.push(format('value', base, p.concat(n), valInPart));
217+
}
218+
else if(valOutPart !== valInPart && valOutPart !== +valInPart) {
219+
list.push(format('dynamic', base, p.concat(n), valInPart, valOutPart));
220+
}
221+
}
222+
}
223+
}
177224
else if(nestedSchema.items && !isInfoArray && isArray(valIn)) {
178-
var items = nestedSchema.items,
179-
_nestedSchema = items[Object.keys(items)[0]],
225+
var _nestedSchema = items[Object.keys(items)[0]],
180226
indexList = [];
181227

182228
var j, _p;

src/plots/cartesian/constants.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
'use strict';
10-
var counterRegex = require('../../lib').counterRegex;
10+
var counterRegex = require('../../lib/regex').counter;
1111

1212

1313
module.exports = {

src/plots/cartesian/layout_attributes.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -685,11 +685,11 @@ module.exports = {
685685
valType: 'info_array',
686686
role: 'info',
687687
items: [
688-
{valType: 'number', min: 0, max: 1, editType: 'calc'},
689-
{valType: 'number', min: 0, max: 1, editType: 'calc'}
688+
{valType: 'number', min: 0, max: 1, editType: 'plot'},
689+
{valType: 'number', min: 0, max: 1, editType: 'plot'}
690690
],
691691
dflt: [0, 1],
692-
editType: 'calc',
692+
editType: 'plot',
693693
description: [
694694
'Sets the domain of this axis (in plot fraction).'
695695
].join(' ')

src/plots/cartesian/layout_defaults.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
161161
var positioningOptions = {
162162
letter: axLetter,
163163
counterAxes: counterAxes[axLetter],
164-
overlayableAxes: overlayableAxes
164+
overlayableAxes: overlayableAxes,
165+
grid: layoutOut.grid
165166
};
166167

167168
handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, positioningOptions);

src/plots/cartesian/position_defaults.js

+27-10
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,43 @@ var Lib = require('../../lib');
1515

1616

1717
module.exports = function handlePositionDefaults(containerIn, containerOut, coerce, options) {
18-
var counterAxes = options.counterAxes || [],
19-
overlayableAxes = options.overlayableAxes || [],
20-
letter = options.letter;
18+
var counterAxes = options.counterAxes || [];
19+
var overlayableAxes = options.overlayableAxes || [];
20+
var letter = options.letter;
21+
var grid = options.grid;
22+
23+
var dfltAnchor, dfltDomain, dfltSide, dfltPosition;
24+
25+
if(grid) {
26+
dfltDomain = grid._domains[letter][grid._axisMap[containerOut._id]];
27+
dfltAnchor = grid._anchors[containerOut._id];
28+
if(dfltDomain) {
29+
dfltSide = grid[letter + 'side'].split(' ')[0];
30+
dfltPosition = grid.domain[letter][dfltSide === 'right' || dfltSide === 'top' ? 1 : 0];
31+
}
32+
}
33+
34+
// Even if there's a grid, this axis may not be in it - fall back on non-grid defaults
35+
dfltDomain = dfltDomain || [0, 1];
36+
dfltAnchor = dfltAnchor || (isNumeric(containerIn.position) ? 'free' : (counterAxes[0] || 'free'));
37+
dfltSide = dfltSide || (letter === 'x' ? 'bottom' : 'left');
38+
dfltPosition = dfltPosition || 0;
2139

2240
var anchor = Lib.coerce(containerIn, containerOut, {
2341
anchor: {
2442
valType: 'enumerated',
2543
values: ['free'].concat(counterAxes),
26-
dflt: isNumeric(containerIn.position) ? 'free' :
27-
(counterAxes[0] || 'free')
44+
dflt: dfltAnchor
2845
}
2946
}, 'anchor');
3047

31-
if(anchor === 'free') coerce('position');
48+
if(anchor === 'free') coerce('position', dfltPosition);
3249

3350
Lib.coerce(containerIn, containerOut, {
3451
side: {
3552
valType: 'enumerated',
3653
values: letter === 'x' ? ['bottom', 'top'] : ['left', 'right'],
37-
dflt: letter === 'x' ? 'bottom' : 'left'
54+
dflt: dfltSide
3855
}
3956
}, 'side');
4057

@@ -54,9 +71,9 @@ module.exports = function handlePositionDefaults(containerIn, containerOut, coer
5471
// in ax.setscale()... but this means we still need (imperfect) logic
5572
// in the axes popover to hide domain for the overlaying axis.
5673
// perhaps I should make a private version _domain that all axes get???
57-
var domain = coerce('domain');
58-
if(domain[0] > domain[1] - 0.01) containerOut.domain = [0, 1];
59-
Lib.noneOrAll(containerIn.domain, containerOut.domain, [0, 1]);
74+
var domain = coerce('domain', dfltDomain);
75+
if(domain[0] > domain[1] - 0.01) containerOut.domain = dfltDomain;
76+
Lib.noneOrAll(containerIn.domain, containerOut.domain, dfltDomain);
6077
}
6178

6279
coerce('layer');

0 commit comments

Comments
 (0)