Skip to content

Commit

Permalink
Merge pull request #2399 from plotly/layout-grid
Browse files Browse the repository at this point in the history
Layout grids
  • Loading branch information
alexcjohnson authored Feb 26, 2018
2 parents 1256c4f + 794669b commit 77c218a
Show file tree
Hide file tree
Showing 38 changed files with 1,241 additions and 185 deletions.
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.
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;
}

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
19 changes: 11 additions & 8 deletions src/lib/regex.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@

'use strict';

// Simple helper functions
// none of these need any external deps

/*
* make a regex for matching counter ids/names ie xaxis, xaxis2, xaxis10...
* eg: regexCounter('x')
* tail is an optional piece after the id
* eg regexCounter('scene', '.annotations') for scene2.annotations etc.
*
* @param {string} head: the head of the pattern, eg 'x' matches 'x', 'x2', 'x10' etc.
* 'xy' is a special case for cartesian subplots: it matches 'x2y3' etc
* @param {Optional(string)} tail: a fixed piece after the id
* eg counterRegex('scene', '.annotations') for scene2.annotations etc.
* @param {boolean} openEnded: if true, the string may continue past the match.
*/
exports.counter = function(head, tail, openEnded) {
return new RegExp('^' + head + '([2-9]|[1-9][0-9]+)?' +
(tail || '') + (openEnded ? '' : '$'));
var fullTail = (tail || '') + (openEnded ? '' : '$');
if(head === 'xy') {
return new RegExp('^x([2-9]|[1-9][0-9]+)?y([2-9]|[1-9][0-9]+)?' + fullTail);
}
return new RegExp('^' + head + '([2-9]|[1-9][0-9]+)?' + fullTail);
};
2 changes: 1 addition & 1 deletion src/lib/stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var isNumeric = require('fast-isnumeric');
exports.aggNums = function(f, v, a, len) {
var i,
b;
if(!len) len = a.length;
if(!len || len > a.length) len = a.length;
if(!isNumeric(v)) v = false;
if(Array.isArray(a[0])) {
b = new Array(len);
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
56 changes: 51 additions & 5 deletions src/plot_api/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,19 +164,65 @@ function crawl(objIn, objOut, schema, list, base, path) {
var valIn = objIn[k],
valOut = objOut[k];

var nestedSchema = getNestedSchema(schema, k),
isInfoArray = (nestedSchema || {}).valType === 'info_array',
isColorscale = (nestedSchema || {}).valType === 'colorscale';
var nestedSchema = getNestedSchema(schema, k);
var isInfoArray = (nestedSchema || {}).valType === 'info_array';
var isColorscale = (nestedSchema || {}).valType === 'colorscale';
var items = (nestedSchema || {}).items;

if(!isInSchema(schema, k)) {
list.push(format('schema', base, p));
}
else if(isPlainObject(valIn) && isPlainObject(valOut)) {
crawl(valIn, valOut, nestedSchema, list, base, p);
}
else if(isInfoArray && isArray(valIn)) {
if(valIn.length > valOut.length) {
list.push(format('unused', base, p.concat(valOut.length)));
}
var len = valOut.length;
var arrayItems = Array.isArray(items);
if(arrayItems) len = Math.min(len, items.length);
var m, n, item, valInPart, valOutPart;
if(nestedSchema.dimensions === 2) {
for(n = 0; n < len; n++) {
if(isArray(valIn[n])) {
if(valIn[n].length > valOut[n].length) {
list.push(format('unused', base, p.concat(n, valOut[n].length)));
}
var len2 = valOut[n].length;
for(m = 0; m < (arrayItems ? Math.min(len2, items[n].length) : len2); m++) {
item = arrayItems ? items[n][m] : items;
valInPart = valIn[n][m];
valOutPart = valOut[n][m];
if(!Lib.validate(valInPart, item)) {
list.push(format('value', base, p.concat(n, m), valInPart));
}
else if(valOutPart !== valInPart && valOutPart !== +valInPart) {
list.push(format('dynamic', base, p.concat(n, m), valInPart, valOutPart));
}
}
}
else {
list.push(format('array', base, p.concat(n), valIn[n]));
}
}
}
else {
for(n = 0; n < len; n++) {
item = arrayItems ? items[n] : items;
valInPart = valIn[n];
valOutPart = valOut[n];
if(!Lib.validate(valInPart, item)) {
list.push(format('value', base, p.concat(n), valInPart));
}
else if(valOutPart !== valInPart && valOutPart !== +valInPart) {
list.push(format('dynamic', base, p.concat(n), valInPart, valOutPart));
}
}
}
}
else if(nestedSchema.items && !isInfoArray && isArray(valIn)) {
var items = nestedSchema.items,
_nestedSchema = items[Object.keys(items)[0]],
var _nestedSchema = items[Object.keys(items)[0]],
indexList = [];

var j, _p;
Expand Down
2 changes: 1 addition & 1 deletion src/plots/cartesian/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

'use strict';
var counterRegex = require('../../lib').counterRegex;
var counterRegex = require('../../lib/regex').counter;


module.exports = {
Expand Down
6 changes: 3 additions & 3 deletions src/plots/cartesian/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -685,11 +685,11 @@ module.exports = {
valType: 'info_array',
role: 'info',
items: [
{valType: 'number', min: 0, max: 1, editType: 'calc'},
{valType: 'number', min: 0, max: 1, editType: 'calc'}
{valType: 'number', min: 0, max: 1, editType: 'plot'},
{valType: 'number', min: 0, max: 1, editType: 'plot'}
],
dflt: [0, 1],
editType: 'calc',
editType: 'plot',
description: [
'Sets the domain of this axis (in plot fraction).'
].join(' ')
Expand Down
3 changes: 2 additions & 1 deletion src/plots/cartesian/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
var positioningOptions = {
letter: axLetter,
counterAxes: counterAxes[axLetter],
overlayableAxes: overlayableAxes
overlayableAxes: overlayableAxes,
grid: layoutOut.grid
};

handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, positioningOptions);
Expand Down
37 changes: 27 additions & 10 deletions src/plots/cartesian/position_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,43 @@ var Lib = require('../../lib');


module.exports = function handlePositionDefaults(containerIn, containerOut, coerce, options) {
var counterAxes = options.counterAxes || [],
overlayableAxes = options.overlayableAxes || [],
letter = options.letter;
var counterAxes = options.counterAxes || [];
var overlayableAxes = options.overlayableAxes || [];
var letter = options.letter;
var grid = options.grid;

var dfltAnchor, dfltDomain, dfltSide, dfltPosition;

if(grid) {
dfltDomain = grid._domains[letter][grid._axisMap[containerOut._id]];
dfltAnchor = grid._anchors[containerOut._id];
if(dfltDomain) {
dfltSide = grid[letter + 'side'].split(' ')[0];
dfltPosition = grid.domain[letter][dfltSide === 'right' || dfltSide === 'top' ? 1 : 0];
}
}

// Even if there's a grid, this axis may not be in it - fall back on non-grid defaults
dfltDomain = dfltDomain || [0, 1];
dfltAnchor = dfltAnchor || (isNumeric(containerIn.position) ? 'free' : (counterAxes[0] || 'free'));
dfltSide = dfltSide || (letter === 'x' ? 'bottom' : 'left');
dfltPosition = dfltPosition || 0;

var anchor = Lib.coerce(containerIn, containerOut, {
anchor: {
valType: 'enumerated',
values: ['free'].concat(counterAxes),
dflt: isNumeric(containerIn.position) ? 'free' :
(counterAxes[0] || 'free')
dflt: dfltAnchor
}
}, 'anchor');

if(anchor === 'free') coerce('position');
if(anchor === 'free') coerce('position', dfltPosition);

Lib.coerce(containerIn, containerOut, {
side: {
valType: 'enumerated',
values: letter === 'x' ? ['bottom', 'top'] : ['left', 'right'],
dflt: letter === 'x' ? 'bottom' : 'left'
dflt: dfltSide
}
}, 'side');

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

coerce('layer');
Expand Down
Loading

0 comments on commit 77c218a

Please sign in to comment.