From 001bd9bb883be95065c6f5c8fc33d264e8324a04 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 23 Feb 2018 11:08:26 -0500 Subject: [PATCH 01/12] rm ignored attr in grouped_bar mock --- test/image/mocks/grouped_bar.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/image/mocks/grouped_bar.json b/test/image/mocks/grouped_bar.json index 7b513a7d3f3..04a3e9b8513 100644 --- a/test/image/mocks/grouped_bar.json +++ b/test/image/mocks/grouped_bar.json @@ -33,11 +33,6 @@ "xaxis": { "type": "category" }, - "barmode": "group", - "categories": [ - "giraffes", - "orangutans", - "monkeys" - ] + "barmode": "group" } } From 606a6016e5d3670737f553c152b7682906292405 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 23 Feb 2018 11:09:25 -0500 Subject: [PATCH 02/12] get tests running again on AJ's machine --- test/jasmine/tests/annotations_test.js | 5 ++++- test/jasmine/tests/gl3d_plot_interact_test.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index 9f7c323dab4..906e354654b 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -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); diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js index 20837c9876c..d5f46bb8cbd 100644 --- a/test/jasmine/tests/gl3d_plot_interact_test.js +++ b/test/jasmine/tests/gl3d_plot_interact_test.js @@ -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)(); } it('should move with camera', function(done) { From f01db07bbd4c8a95d2e44370b5a2b8be97c73b73 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 23 Feb 2018 11:11:08 -0500 Subject: [PATCH 03/12] generalize info_array to 2D and arb. length --- src/lib/coerce.js | 63 ++++++++++++++++++++++++++---- src/plot_api/plot_schema.js | 22 +++++++++-- test/jasmine/tests/lib_test.js | 71 ++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 11 deletions(-) diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 26f15e57932..e6eba28261a 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -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); @@ -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; diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index e3915e0297d..ef8261834c9 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -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; @@ -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; + } } } diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 7ade3bc8ffe..29ce0b65301 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -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() { + 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() { From 82fbae247ae38a087a012673face02970f76603c Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 23 Feb 2018 11:27:32 -0500 Subject: [PATCH 04/12] layout.grid --- src/lib/regex.js | 19 +- src/plots/cartesian/constants.js | 2 +- src/plots/cartesian/layout_defaults.js | 3 +- src/plots/cartesian/position_defaults.js | 37 +- src/plots/{domain_attributes.js => domain.js} | 62 ++- src/plots/geo/layout/layout_attributes.js | 2 +- src/plots/gl3d/layout/layout_attributes.js | 2 +- src/plots/grid.js | 409 ++++++++++++++++++ src/plots/layout_attributes.js | 4 +- src/plots/mapbox/layout_attributes.js | 2 +- src/plots/plots.js | 4 + src/plots/polar/layout_attributes.js | 2 +- src/plots/subplot_defaults.js | 6 +- src/plots/ternary/layout/layout_attributes.js | 2 +- src/traces/parcoords/attributes.js | 2 +- src/traces/parcoords/defaults.js | 4 +- src/traces/pie/attributes.js | 2 +- src/traces/pie/defaults.js | 4 +- src/traces/sankey/attributes.js | 2 +- src/traces/sankey/defaults.js | 5 +- src/traces/table/attributes.js | 2 +- src/traces/table/defaults.js | 4 +- test/image/baselines/grid_subplot_types.png | Bin 0 -> 73356 bytes test/image/mocks/grid_subplot_types.json | 32 ++ test/jasmine/tests/plots_test.js | 234 ++++++++++ 25 files changed, 805 insertions(+), 42 deletions(-) rename src/plots/{domain_attributes.js => domain.js} (52%) create mode 100644 src/plots/grid.js create mode 100644 test/image/baselines/grid_subplot_types.png create mode 100644 test/image/mocks/grid_subplot_types.json diff --git a/src/lib/regex.js b/src/lib/regex.js index 7ebeb0d971a..acfaff9d41b 100644 --- a/src/lib/regex.js +++ b/src/lib/regex.js @@ -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); }; diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 818071ca864..a134bd71cf5 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -7,7 +7,7 @@ */ 'use strict'; -var counterRegex = require('../../lib').counterRegex; +var counterRegex = require('../../lib/regex').counter; module.exports = { diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 149bb203d1e..54cedf70d16 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -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); diff --git a/src/plots/cartesian/position_defaults.js b/src/plots/cartesian/position_defaults.js index ebbe138e368..c6feadc14df 100644 --- a/src/plots/cartesian/position_defaults.js +++ b/src/plots/cartesian/position_defaults.js @@ -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'); @@ -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'); diff --git a/src/plots/domain_attributes.js b/src/plots/domain.js similarity index 52% rename from src/plots/domain_attributes.js rename to src/plots/domain.js index d929be4f5c7..afd410c0657 100644 --- a/src/plots/domain_attributes.js +++ b/src/plots/domain.js @@ -20,6 +20,8 @@ var extendFlat = require('../lib/extend').extendFlat; * opts.trace: set to true for trace containers * @param {string} * opts.editType: editType for all pieces + * @param {boolean} + * opts.noGridCell: set to true to omit `row` and `column` * * @param {object} extra * @param {string} @@ -29,7 +31,7 @@ var extendFlat = require('../lib/extend').extendFlat; * * @return {object} attributes object containing {x,y} as specified */ -module.exports = function(opts, extra) { +exports.attributes = function(opts, extra) { opts = opts || {}; extra = extra || {}; @@ -48,7 +50,7 @@ module.exports = function(opts, extra) { var contPart = opts.trace ? 'trace ' : 'subplot '; var descPart = extra.description ? ' ' + extra.description : ''; - return { + var out = { x: extendFlat({}, base, { description: [ 'Sets the horizontal domain of this ', @@ -69,4 +71,60 @@ module.exports = function(opts, extra) { }), editType: opts.editType }; + + if(!opts.noGridCell) { + out.row = { + valType: 'integer', + min: 0, + role: 'info', + editType: opts.editType, + description: [ + 'If there is a layout grid, use the domain ', + 'for this row in the grid for this ', + namePart, + contPart, + '.', + descPart + ].join('') + }; + out.column = { + valType: 'integer', + min: 0, + role: 'info', + editType: opts.editType, + description: [ + 'If there is a layout grid, use the domain ', + 'for this column in the grid for this ', + namePart, + contPart, + '.', + descPart + ].join('') + }; + } + + return out; +}; + +exports.defaults = function(containerOut, layout, coerce, dfltDomains) { + var dfltX = (dfltDomains && dfltDomains.x) || [0, 1]; + var dfltY = (dfltDomains && dfltDomains.y) || [0, 1]; + + var grid = layout.grid; + if(grid) { + var column = coerce('domain.column'); + if(column !== undefined) { + if(column < grid.columns) dfltX = grid._domains.x[column]; + else delete containerOut.domain.column; + } + + var row = coerce('domain.row'); + if(row !== undefined) { + if(row < grid.rows) dfltY = grid._domains.y[row]; + else delete containerOut.domain.row; + } + } + + coerce('domain.x', dfltX); + coerce('domain.y', dfltY); }; diff --git a/src/plots/geo/layout/layout_attributes.js b/src/plots/geo/layout/layout_attributes.js index 4e15c3e680b..f326c816550 100644 --- a/src/plots/geo/layout/layout_attributes.js +++ b/src/plots/geo/layout/layout_attributes.js @@ -9,7 +9,7 @@ 'use strict'; var colorAttrs = require('../../../components/color/attributes'); -var domainAttrs = require('../../domain_attributes'); +var domainAttrs = require('../../domain').attributes; var constants = require('../constants'); var overrideAll = require('../../../plot_api/edit_types').overrideAll; diff --git a/src/plots/gl3d/layout/layout_attributes.js b/src/plots/gl3d/layout/layout_attributes.js index ec32f254c30..870eca0151c 100644 --- a/src/plots/gl3d/layout/layout_attributes.js +++ b/src/plots/gl3d/layout/layout_attributes.js @@ -10,7 +10,7 @@ 'use strict'; var gl3dAxisAttrs = require('./axis_attributes'); -var domainAttrs = require('../../domain_attributes'); +var domainAttrs = require('../../domain').attributes; var extendFlat = require('../../../lib/extend').extendFlat; var counterRegex = require('../../../lib').counterRegex; diff --git a/src/plots/grid.js b/src/plots/grid.js new file mode 100644 index 00000000000..0943f0a6dbd --- /dev/null +++ b/src/plots/grid.js @@ -0,0 +1,409 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Lib = require('../lib'); +var domainAttrs = require('./domain').attributes; +var counterRegex = require('../lib/regex').counter; +var cartesianIdRegex = require('./cartesian/constants').idRegex; + + +var gridAttrs = exports.attributes = { + rows: { + valType: 'integer', + min: 1, + role: 'info', + editType: 'calc', + description: [ + 'The number of rows in the grid. If you provide a 2D `subplots`', + 'array or a `yaxes` array, its length is used as the default.', + 'But it\'s also possible to have a different length, if you', + 'want to leave a row at the end for non-cartesian subplots.' + ].join(' ') + }, + roworder: { + valType: 'enumerated', + values: ['top to bottom', 'bottom to top'], + dflt: 'top to bottom', + role: 'info', + editType: 'calc', + description: [ + 'Is the first row the top or the bottom? Note that columns', + 'are always enumerated from left to right.' + ].join(' ') + }, + columns: { + valType: 'integer', + min: 1, + role: 'info', + editType: 'calc', + description: [ + 'The number of columns in the grid. If you provide a 2D `subplots`', + 'array, the length of its longest row is used as the default.', + 'If you give an `xaxes` array, its length is used as the default.', + 'But it\'s also possible to have a different length, if you', + 'want to leave a row at the end for non-cartesian subplots.' + ].join(' ') + }, + subplots: { + valType: 'info_array', + freeLength: true, + dimensions: 2, + items: {valType: 'enumerated', values: [counterRegex('xy').toString(), '']}, + role: 'info', + editType: 'calc', + description: [ + 'Used for freeform grids, where some axes may be shared across subplots', + 'but others are not. Each entry should be a cartesian subplot id, like', + '*xy* or *x3y2*, or ** to leave that cell empty. You may reuse x axes', + 'within the same column, and y axes within the same row.', + 'Non-cartesian subplots and traces that support `domain` can place themselves', + 'in this grid separately using the `gridcell` attribute.' + ].join(' ') + }, + xaxes: { + valType: 'info_array', + freeLength: true, + items: {valType: 'enumerated', values: [cartesianIdRegex.x.toString(), '']}, + role: 'info', + editType: 'calc', + description: [ + 'Used with `yaxes` when the x and y axes are shared across columns and rows.', + 'Each entry should be an x axis id like *x*, *x2*, etc., or ** to', + 'not put an x axis in that column. Entries other than ** must be unique.', + 'Ignored if `subplots` is present. If missing but `yaxes` is present,', + 'will generate consecutive IDs.' + ].join(' ') + }, + yaxes: { + valType: 'info_array', + freeLength: true, + items: {valType: 'enumerated', values: [cartesianIdRegex.y.toString(), '']}, + role: 'info', + editType: 'calc', + description: [ + 'Used with `yaxes` when the x and y axes are shared across columns and rows.', + 'Each entry should be an y axis id like *y*, *y2*, etc., or ** to', + 'not put a y axis in that row. Entries other than ** must be unique.', + 'Ignored if `subplots` is present. If missing but `xaxes` is present,', + 'will generate consecutive IDs.' + ].join(' ') + }, + pattern: { + valType: 'enumerated', + values: ['independent', 'coupled'], + dflt: 'coupled', + role: 'info', + editType: 'calc', + description: [ + 'If no `subplots`, `xaxes`, or `yaxes` are given but we do have `rows` and `columns`,', + 'we can generate defaults using consecutive axis IDs, in two ways:', + '*coupled* gives one x axis per column and one y axis per row.', + '*independent* uses a new xy pair for each cell, left-to-right across each row', + 'then iterating rows according to `roworder`.' + ].join(' ') + }, + gap: { + valType: 'number', + min: 0, + max: 1, + dflt: 0.1, + role: 'info', + editType: 'calc', + description: [ + 'Space between grid cells, expressed as a fraction of the total', + 'width or height available to one cell. You can also use `xgap`', + 'and `ygap` to space x and y differently. Defaults to 0.1 for', + 'coupled-axes grids and 0.25 for independent grids.' + ].join(' ') + }, + xgap: { + valType: 'number', + min: 0, + max: 1, + role: 'info', + editType: 'calc', + description: [ + 'Horizontal space between grid cells, expressed as a fraction', + 'of the total width available to one cell.' + ].join(' ') + }, + ygap: { + valType: 'number', + min: 0, + max: 1, + role: 'info', + editType: 'calc', + description: [ + 'Vertical space between grid cells, expressed as a fraction', + 'of the total height available to one cell.' + ].join(' ') + }, + domain: domainAttrs({name: 'grid', editType: 'calc', noGridCell: true}, { + description: [ + 'The first and last cells end exactly at the domain', + 'edges, with no grout around the edges.' + ].join(' ') + }), + xside: { + valType: 'enumerated', + values: ['bottom', 'bottom plot', 'top plot', 'top'], + dflt: 'bottom plot', + role: 'info', + editType: 'ticks', + description: [ + 'Sets where the x axis labels and titles go. *bottom* means', + 'the very bottom of the grid. *bottom plot* is the lowest plot', + 'that each x axis is used in. *top* and *top plot* are similar.' + ].join(' ') + }, + yside: { + valType: 'enumerated', + values: ['left', 'left plot', 'right plot', 'right'], + dflt: 'left plot', + role: 'info', + editType: 'ticks', + description: [ + 'Sets where the y axis labels and titles go. *left* means', + 'the very left edge of the grid. *left plot* is the leftmost plot', + 'that each y axis is used in. *right* and *right plot* are similar.' + ].join(' ') + }, + editType: 'calc' +}; + +// the shape of the grid - this needs to be done BEFORE supplyDataDefaults +// so that non-subplot traces can place themselves in the grid +exports.sizeDefaults = function(layoutIn, layoutOut) { + var gridIn = layoutIn.grid; + if(!gridIn) return; + + var hasSubplotGrid = Array.isArray(gridIn.subplots) && Array.isArray(gridIn.subplots[0]); + var hasXaxes = Array.isArray(gridIn.xaxes); + var hasYaxes = Array.isArray(gridIn.yaxes); + + var dfltRows, dfltColumns; + + if(hasSubplotGrid) { + dfltRows = gridIn.subplots.length; + dfltColumns = gridIn.subplots[0].length; + } + else { + if(hasYaxes) dfltRows = gridIn.yaxes.length; + if(hasXaxes) dfltColumns = gridIn.xaxes.length; + } + + var gridOut = layoutOut.grid = {}; + + function coerce(attr, dflt) { + return Lib.coerce(gridIn, gridOut, gridAttrs, attr, dflt); + } + + var rows = coerce('rows', dfltRows); + var columns = coerce('columns', dfltColumns); + + if(!(rows * columns > 1)) return; + + if(!hasSubplotGrid && !hasXaxes && !hasYaxes) { + var useDefaultSubplots = coerce('pattern') === 'independent'; + if(useDefaultSubplots) hasSubplotGrid = true; + } + gridOut._hasSubplotGrid = hasSubplotGrid; + + var rowOrder = coerce('roworder'); + var reversed = rowOrder === 'top to bottom'; + var gap = coerce('gap', hasSubplotGrid ? 0.25 : 0.1); + + gridOut._domains = { + x: fillGridPositions('x', coerce, gap, columns), + y: fillGridPositions('y', coerce, gap, rows, reversed) + }; +}; + +// coerce x or y sizing attributes and return an array of domains for this direction +function fillGridPositions(axLetter, coerce, gap, len, reversed) { + var dirGap = coerce(axLetter + 'gap', gap); + var domain = coerce('domain.' + axLetter); + coerce(axLetter + 'side'); + + var out = new Array(len); + var start = domain[0]; + var step = (domain[1] - start) / (len - dirGap); + var cellDomain = step * (1 - dirGap); + for(var i = 0; i < len; i++) { + var cellStart = start + step * i; + out[reversed ? (len - 1 - i) : i] = [cellStart, cellStart + cellDomain]; + } + return out; +} + +// the (cartesian) contents of the grid - this needs to happen AFTER supplyDataDefaults +// so that we know what cartesian subplots are available +exports.contentDefaults = function(layoutIn, layoutOut) { + var gridOut = layoutOut.grid; + // make sure we got to the end of handleGridSizing + if(!gridOut || !gridOut._domains) return; + + var gridIn = layoutIn.grid; + var subplots = layoutOut._subplots; + var hasSubplotGrid = gridOut._hasSubplotGrid; + var rows = gridOut.rows; + var columns = gridOut.columns; + var useDefaultSubplots = gridOut.pattern === 'independent'; + + var i, j, xId, yId, subplotId, subplotsOut, yPos; + + var axisMap = gridOut._axisMap = {}; + + if(hasSubplotGrid) { + var subplotsIn = gridIn.subplots || []; + subplotsOut = gridOut.subplots = new Array(rows); + var index = 1; + + for(i = 0; i < rows; i++) { + var rowOut = subplotsOut[i] = new Array(columns); + var rowIn = subplotsIn[i] || []; + for(j = 0; j < columns; j++) { + if(useDefaultSubplots) { + subplotId = (index === 1) ? 'xy' : ('x' + index + 'y' + index); + index++; + } + else subplotId = rowIn[j]; + + if(subplots.cartesian.indexOf(subplotId) !== -1) { + yPos = subplotId.indexOf('y'); + xId = subplotId.slice(0, yPos); + yId = subplotId.slice(yPos); + if((axisMap[xId] !== undefined && axisMap[xId] !== j) || + (axisMap[yId] !== undefined && axisMap[yId] !== i) + ) { + continue; + } + + rowOut[j] = subplotId; + axisMap[xId] = j; + axisMap[yId] = i; + } + } + } + } + else { + gridOut.xaxes = fillGridAxes(gridIn.xaxes, subplots.xaxis, columns, axisMap, 'x'); + gridOut.yaxes = fillGridAxes(gridIn.yaxes, subplots.yaxis, rows, axisMap, 'y'); + } + + var anchors = gridOut._anchors = {}; + var reversed = gridOut.roworder === 'top to bottom'; + + for(var axisId in axisMap) { + var axLetter = axisId.charAt(0); + var side = gridOut[axLetter + 'side']; + + var i0, inc, iFinal; + + if(side.length < 8) { + // grid edge - ie not "* plot" - make these as free axes + // since we're not guaranteed to have a subplot there at all + anchors[axisId] = 'free'; + } + else if(axLetter === 'x') { + if((side.charAt(0) === 't') === reversed) { + i0 = 0; + inc = 1; + iFinal = rows; + } + else { + i0 = rows - 1; + inc = -1; + iFinal = -1; + } + if(hasSubplotGrid) { + var column = axisMap[axisId]; + for(i = i0; i !== iFinal; i += inc) { + subplotId = subplotsOut[i][column]; + if(!subplotId) continue; + yPos = subplotId.indexOf('y'); + if(subplotId.slice(0, yPos) === axisId) { + anchors[axisId] = subplotId.slice(yPos); + break; + } + } + } + else { + for(i = i0; i !== iFinal; i += inc) { + yId = gridOut.yaxes[i]; + if(subplots.cartesian.indexOf(axisId + yId) !== -1) { + anchors[axisId] = yId; + break; + } + } + } + } + else { + if((side.charAt(0) === 'l')) { + i0 = 0; + inc = 1; + iFinal = columns; + } + else { + i0 = columns - 1; + inc = -1; + iFinal = -1; + } + if(hasSubplotGrid) { + var row = axisMap[axisId]; + for(i = i0; i !== iFinal; i += inc) { + subplotId = subplotsOut[row][i]; + if(!subplotId) continue; + yPos = subplotId.indexOf('y'); + if(subplotId.slice(yPos) === axisId) { + anchors[axisId] = subplotId.slice(0, yPos); + break; + } + } + } + else { + for(i = i0; i !== iFinal; i += inc) { + xId = gridOut.xaxes[i]; + if(subplots.cartesian.indexOf(xId + axisId) !== -1) { + anchors[axisId] = xId; + break; + } + } + } + } + } +}; + +function fillGridAxes(axesIn, axesAllowed, len, axisMap, axLetter) { + var out = new Array(len); + var i; + + function fillOneAxis(i, axisId) { + if(axesAllowed.indexOf(axisId) !== -1 && axisMap[axisId] === undefined) { + out[i] = axisId; + axisMap[axisId] = i; + } + } + + if(Array.isArray(axesIn)) { + for(i = 0; i < len; i++) { + fillOneAxis(i, axesIn[i]); + } + } + else { + // default axis list is the first `len` axis ids + fillOneAxis(0, axLetter); + for(i = 1; i < len; i++) { + fillOneAxis(i, axLetter + (i + 1)); + } + } + + return out; +} diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 1dfee32e8f7..1e7e8e20bea 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -10,6 +10,7 @@ var fontAttrs = require('./font_attributes'); var colorAttrs = require('../components/color/attributes'); +var gridAttrs = require('./grid').attributes; var globalFont = fontAttrs({ editType: 'calc', @@ -195,5 +196,6 @@ module.exports = { 'being treated as immutable, thus any data array with a', 'different identity from its predecessor contains new data.' ].join(' ') - } + }, + grid: gridAttrs }; diff --git a/src/plots/mapbox/layout_attributes.js b/src/plots/mapbox/layout_attributes.js index b373105c29f..2306c8b4bcf 100644 --- a/src/plots/mapbox/layout_attributes.js +++ b/src/plots/mapbox/layout_attributes.js @@ -11,7 +11,7 @@ var Lib = require('../../lib'); var defaultLine = require('../../components/color').defaultLine; -var domainAttrs = require('../domain_attributes'); +var domainAttrs = require('../domain').attributes; var fontAttrs = require('../font_attributes'); var textposition = require('../../traces/scatter/attributes').textposition; var overrideAll = require('../../plot_api/edit_types').overrideAll; diff --git a/src/plots/plots.js b/src/plots/plots.js index ae049d78d5c..db80e30dfed 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -20,6 +20,7 @@ var Lib = require('../lib'); var _ = Lib._; var Color = require('../components/color'); var BADNUM = require('../constants/numerical').BADNUM; +var Grid = require('./grid'); var plots = module.exports = {}; @@ -1250,6 +1251,8 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) { if(layoutIn.width && layoutIn.height) plots.sanitizeMargins(layoutOut); + Grid.sizeDefaults(layoutIn, layoutOut); + coerce('paper_bgcolor'); coerce('separators', formatObj.decimal + formatObj.thousands); @@ -1387,6 +1390,7 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans // ensure all cartesian axes have at least one subplot if(layoutOut._has('cartesian')) { + Grid.contentDefaults(layoutIn, layoutOut); Cartesian.finalizeSubplots(layoutIn, layoutOut); } diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js index ff663ed518a..ba4d8fe25a6 100644 --- a/src/plots/polar/layout_attributes.js +++ b/src/plots/polar/layout_attributes.js @@ -10,7 +10,7 @@ var colorAttrs = require('../../components/color/attributes'); var axesAttrs = require('../cartesian/layout_attributes'); -var domainAttrs = require('../domain_attributes'); +var domainAttrs = require('../domain').attributes; var extendFlat = require('../../lib').extendFlat; var overrideAll = require('../../plot_api/edit_types').overrideAll; diff --git a/src/plots/subplot_defaults.js b/src/plots/subplot_defaults.js index 589017a1a9a..b8c229cc8a1 100644 --- a/src/plots/subplot_defaults.js +++ b/src/plots/subplot_defaults.js @@ -10,6 +10,7 @@ 'use strict'; var Lib = require('../lib'); +var handleDomainDefaults = require('./domain').defaults; /** @@ -63,8 +64,9 @@ module.exports = function handleSubplotDefaults(layoutIn, layoutOut, fullData, o layoutOut[id] = subplotLayoutOut = {}; - coerce('domain.' + partition, [i / idsLength, (i + 1) / idsLength]); - coerce('domain.' + {x: 'y', y: 'x'}[partition]); + var dfltDomains = {}; + dfltDomains[partition] = [i / idsLength, (i + 1) / idsLength]; + handleDomainDefaults(subplotLayoutOut, layoutOut, coerce, dfltDomains); opts.id = id; handleDefaults(subplotLayoutIn, subplotLayoutOut, coerce, opts); diff --git a/src/plots/ternary/layout/layout_attributes.js b/src/plots/ternary/layout/layout_attributes.js index 10f59f289f8..77c06326974 100644 --- a/src/plots/ternary/layout/layout_attributes.js +++ b/src/plots/ternary/layout/layout_attributes.js @@ -9,7 +9,7 @@ 'use strict'; var colorAttrs = require('../../../components/color/attributes'); -var domainAttrs = require('../../domain_attributes'); +var domainAttrs = require('../../domain').attributes; var ternaryAxesAttrs = require('./axis_attributes'); var overrideAll = require('../../../plot_api/edit_types').overrideAll; diff --git a/src/traces/parcoords/attributes.js b/src/traces/parcoords/attributes.js index 1713b722d86..02f9a72ac86 100644 --- a/src/traces/parcoords/attributes.js +++ b/src/traces/parcoords/attributes.js @@ -13,7 +13,7 @@ var colorbarAttrs = require('../../components/colorbar/attributes'); var colorscales = require('../../components/colorscale/scales'); var axesAttrs = require('../../plots/cartesian/layout_attributes'); var fontAttrs = require('../../plots/font_attributes'); -var domainAttrs = require('../../plots/domain_attributes'); +var domainAttrs = require('../../plots/domain').attributes; var extend = require('../../lib/extend'); var extendDeepAll = extend.extendDeepAll; diff --git a/src/traces/parcoords/defaults.js b/src/traces/parcoords/defaults.js index 13c6e8789c1..c9f9cf93474 100644 --- a/src/traces/parcoords/defaults.js +++ b/src/traces/parcoords/defaults.js @@ -13,6 +13,7 @@ var attributes = require('./attributes'); var hasColorscale = require('../../components/colorscale/has_colorscale'); var colorscaleDefaults = require('../../components/colorscale/defaults'); var maxDimensionCount = require('./constants').maxDimensionCount; +var handleDomainDefaults = require('../../plots/domain').defaults; function handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce) { @@ -90,8 +91,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); - coerce('domain.x'); - coerce('domain.y'); + handleDomainDefaults(traceOut, layout, coerce); if(!Array.isArray(dimensions) || !dimensions.length) { traceOut.visible = false; diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js index cf6ae715d4f..9a901c61735 100644 --- a/src/traces/pie/attributes.js +++ b/src/traces/pie/attributes.js @@ -11,7 +11,7 @@ var colorAttrs = require('../../components/color/attributes'); var fontAttrs = require('../../plots/font_attributes'); var plotAttrs = require('../../plots/attributes'); -var domainAttrs = require('../../plots/domain_attributes'); +var domainAttrs = require('../../plots/domain').attributes; var extendFlat = require('../../lib/extend').extendFlat; diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index a13e89497cc..9a1aa32551c 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -10,6 +10,7 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); +var handleDomainDefaults = require('../../plots/domain').defaults; module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { @@ -56,8 +57,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } } - coerce('domain.x'); - coerce('domain.y'); + handleDomainDefaults(traceOut, layout, coerce); coerce('hole'); diff --git a/src/traces/sankey/attributes.js b/src/traces/sankey/attributes.js index 438f7912b63..4ca7d632c25 100644 --- a/src/traces/sankey/attributes.js +++ b/src/traces/sankey/attributes.js @@ -12,7 +12,7 @@ var fontAttrs = require('../../plots/font_attributes'); var plotAttrs = require('../../plots/attributes'); var colorAttrs = require('../../components/color/attributes'); var fxAttrs = require('../../components/fx/attributes'); -var domainAttrs = require('../../plots/domain_attributes'); +var domainAttrs = require('../../plots/domain').attributes; var extendFlat = require('../../lib/extend').extendFlat; var overrideAll = require('../../plot_api/edit_types').overrideAll; diff --git a/src/traces/sankey/defaults.js b/src/traces/sankey/defaults.js index 5e33b9322a4..173c88dd100 100644 --- a/src/traces/sankey/defaults.js +++ b/src/traces/sankey/defaults.js @@ -12,6 +12,7 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); var Color = require('../../components/color'); var tinycolor = require('tinycolor2'); +var handleDomainDefaults = require('../../plots/domain').defaults; module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { @@ -45,8 +46,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout 'rgba(0, 0, 0, 0.2)'; })); - coerce('domain.x'); - coerce('domain.y'); + handleDomainDefaults(traceOut, layout, coerce); + coerce('orientation'); coerce('valueformat'); coerce('valuesuffix'); diff --git a/src/traces/table/attributes.js b/src/traces/table/attributes.js index 7dbfededbe0..feccc72501b 100644 --- a/src/traces/table/attributes.js +++ b/src/traces/table/attributes.js @@ -12,7 +12,7 @@ var annAttrs = require('../../components/annotations/attributes'); var extendFlat = require('../../lib/extend').extendFlat; var overrideAll = require('../../plot_api/edit_types').overrideAll; var fontAttrs = require('../../plots/font_attributes'); -var domainAttrs = require('../../plots/domain_attributes'); +var domainAttrs = require('../../plots/domain').attributes; module.exports = overrideAll({ domain: domainAttrs({name: 'table', trace: true}), diff --git a/src/traces/table/defaults.js b/src/traces/table/defaults.js index 58504670af8..989b435766d 100644 --- a/src/traces/table/defaults.js +++ b/src/traces/table/defaults.js @@ -10,6 +10,7 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); +var handleDomainDefaults = require('../../plots/domain').defaults; function defaultColumnOrder(traceOut, coerce) { var specifiedColumnOrder = traceOut.columnorder || []; @@ -28,8 +29,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - coerce('domain.x'); - coerce('domain.y'); + handleDomainDefaults(traceOut, layout, coerce); coerce('columnwidth'); diff --git a/test/image/baselines/grid_subplot_types.png b/test/image/baselines/grid_subplot_types.png new file mode 100644 index 0000000000000000000000000000000000000000..970b25cca387005d219ce0815137a3e93532d72c GIT binary patch literal 73356 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 literal 0 HcmV?d00001 diff --git a/test/image/mocks/grid_subplot_types.json b/test/image/mocks/grid_subplot_types.json new file mode 100644 index 00000000000..cf03a874a85 --- /dev/null +++ b/test/image/mocks/grid_subplot_types.json @@ -0,0 +1,32 @@ +{ + "data": [ + {"y": [1, 2, 3]}, + {"y": [4, 6, 5], "xaxis": "x2"}, + {"x": [1, 2, 3], "y": [8, 9, 7], "yaxis": "y2"}, + {"type": "surface", "z": [[1, 2], [4, 1]], "showscale": false}, + {"type": "pie", "labels": ["a", "b"], "values": [1, 6], "domain": {"row": 0, "column": 2}}, + {"type": "scatterternary", "a": [1, 1, 3], "b": [1, 3, 1], "c": [3, 1, 1], "mode": "markers"}, + { + "type": "table", + "header": {"values": ["a", "b"]}, + "cells": {"values": [["c", "d"], ["e", "f"]]}, + "domain": {"row": 2, "column": 0} + }, + {"type": "scattergeo", "lon": [0, -75], "lat": [0, 45], "marker": {"size": [20, 30]}}, + {"type": "heatmap", "z": [[1, 2], [4, 1]], "showscale": false, "xaxis": "x3", "yaxis": "y3"} + ], + "layout": { + "xaxis": {"title": "x"}, + "xaxis2": {"title": "x2"}, + "yaxis": {"title": "y"}, + "yaxis2": {"title": "y2"}, + "xaxis3": {"title": "x3"}, + "yaxis3": {"title": "y3"}, + "grid": {"rows": 3, "columns": 3, "gap": 0.3}, + "scene": {"domain": {"row": 1, "column": 1}}, + "ternary": {"domain": {"row": 1, "column": 2}}, + "geo": {"domain": {"row": 2, "column": 1}}, + "width": 700, + "height": 700 + } +} diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index af025d61ede..c0dee094eb2 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -852,3 +852,237 @@ describe('Test Plots', function() { }); }); }); + +describe('grids', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function makeData(subplots) { + var data = []; + for(var i = 0; i < subplots.length; i++) { + var subplot = subplots[i]; + var yPos = subplot.indexOf('y'); + data.push({ + y: [1, 2], + xaxis: subplot.slice(0, yPos), + yaxis: subplot.slice(yPos) + }); + } + return data; + } + + function _assertDomains(domains) { + for(var axName in domains) { + expect(gd._fullLayout[axName].domain) + .toBeCloseToArray(domains[axName], 3, axName); + } + } + + function _assertMissing(axList) { + axList.forEach(function(axName) { + expect(gd._fullLayout[axName]).toBeUndefined(axName); + }); + } + + it('defaults to a coupled layout', function(done) { + Plotly.newPlot(gd, + // leave some empty rows/columns + makeData(['x2y2', 'x3y3']), + {grid: {rows: 4, columns: 4}} + ) + .then(function() { + _assertDomains({ + xaxis2: [1 / 3.9, 1.9 / 3.9], + yaxis2: [2 / 3.9, 2.9 / 3.9], + xaxis3: [2 / 3.9, 2.9 / 3.9], + yaxis3: [1 / 3.9, 1.9 / 3.9] + }); + _assertMissing(['xaxis', 'yaxis', 'xaxis4', 'yaxis4']); + + return Plotly.relayout(gd, { + 'grid.xaxes': ['x2', 'x', '', 'x3'], + 'grid.yaxes': ['y3', '', 'y', 'y2'] + }); + }) + .then(function() { + _assertDomains({ + xaxis2: [0, 0.9 / 3.9], + yaxis2: [0, 0.9 / 3.9], + xaxis3: [3 / 3.9, 1], + yaxis3: [3 / 3.9, 1] + }); + _assertMissing(['xaxis', 'yaxis', 'xaxis4', 'yaxis4']); + + return Plotly.relayout(gd, {'grid.roworder': 'bottom to top'}); + }) + .then(function() { + _assertDomains({ + xaxis2: [0, 0.9 / 3.9], + yaxis2: [3 / 3.9, 1], + xaxis3: [3 / 3.9, 1], + yaxis3: [0, 0.9 / 3.9] + }); + _assertMissing(['xaxis', 'yaxis', 'xaxis4', 'yaxis4']); + }) + .catch(failTest) + .then(done); + }); + + it('has a bigger default gap with independent layout', function(done) { + Plotly.newPlot(gd, + makeData(['x2y2', 'x3y3', 'x4y4']), + {grid: {rows: 3, columns: 3, pattern: 'independent'}} + ) + .then(function() { + _assertDomains({ + xaxis2: [1 / 2.75, 1.75 / 2.75], + yaxis2: [2 / 2.75, 1], + xaxis3: [2 / 2.75, 1], + yaxis3: [2 / 2.75, 1], + xaxis4: [0, 0.75 / 2.75], + yaxis4: [1 / 2.75, 1.75 / 2.75] + }); + _assertMissing(['xaxis', 'yaxis']); + + return Plotly.relayout(gd, { + 'grid.subplots': [['x4y4', '', 'x3y3'], [], ['', 'x2y2']] + }); + }) + .then(function() { + _assertDomains({ + xaxis2: [1 / 2.75, 1.75 / 2.75], + yaxis2: [0, 0.75 / 2.75], + xaxis3: [2 / 2.75, 1], + yaxis3: [2 / 2.75, 1], + xaxis4: [0, 0.75 / 2.75], + yaxis4: [2 / 2.75, 1] + }); + _assertMissing(['xaxis', 'yaxis']); + + return Plotly.relayout(gd, {'grid.roworder': 'bottom to top'}); + }) + .then(function() { + _assertDomains({ + xaxis2: [1 / 2.75, 1.75 / 2.75], + yaxis2: [2 / 2.75, 1], + xaxis3: [2 / 2.75, 1], + yaxis3: [0, 0.75 / 2.75], + xaxis4: [0, 0.75 / 2.75], + yaxis4: [0, 0.75 / 2.75] + }); + _assertMissing(['xaxis', 'yaxis']); + }) + .catch(failTest) + .then(done); + }); + + it('can set x and y gaps together or separately and change domain', function(done) { + Plotly.newPlot(gd, + // leave some empty rows/columns + makeData(['xy', 'x2y2']), + {grid: {rows: 2, columns: 2}} + ) + .then(function() { + _assertDomains({ + xaxis: [0, 0.9 / 1.9], + yaxis: [1 / 1.9, 1], + xaxis2: [1 / 1.9, 1], + yaxis2: [0, 0.9 / 1.9] + }); + + return Plotly.relayout(gd, {'grid.gap': 0.4}); + }) + .then(function() { + _assertDomains({ + xaxis: [0, 0.6 / 1.6], + yaxis: [1 / 1.6, 1], + xaxis2: [1 / 1.6, 1], + yaxis2: [0, 0.6 / 1.6] + }); + + return Plotly.relayout(gd, {'grid.xgap': 0.2}); + }) + .then(function() { + _assertDomains({ + xaxis: [0, 0.8 / 1.8], + yaxis: [1 / 1.6, 1], + xaxis2: [1 / 1.8, 1], + yaxis2: [0, 0.6 / 1.6] + }); + + return Plotly.relayout(gd, {'grid.ygap': 0.3}); + }) + .then(function() { + _assertDomains({ + xaxis: [0, 0.8 / 1.8], + yaxis: [1 / 1.7, 1], + xaxis2: [1 / 1.8, 1], + yaxis2: [0, 0.7 / 1.7] + }); + + return Plotly.relayout(gd, {'grid.domain': {x: [0.2, 0.7], y: [0, 0.5]}}); + }) + .then(function() { + _assertDomains({ + xaxis: [0.2, 0.2 + 0.4 / 1.8], + yaxis: [0.5 / 1.7, 0.5], + xaxis2: [0.2 + 0.5 / 1.8, 0.7], + yaxis2: [0, 0.35 / 1.7] + }); + }) + .catch(failTest) + .then(done); + }); + + it('responds to xside and yside', function(done) { + function checkAxis(axName, anchor, side, position) { + var ax = gd._fullLayout[axName]; + expect(ax.anchor).toBe(anchor, axName); + expect(ax.side).toBe(side, axName); + expect(ax.position).toBe(position, axName); + } + + Plotly.newPlot(gd, + // leave some empty rows/columns + makeData(['xy', 'x2y2']), + {grid: {rows: 2, columns: 2}} + ) + .then(function() { + checkAxis('xaxis', 'y', 'bottom'); + checkAxis('yaxis', 'x', 'left'); + checkAxis('xaxis2', 'y2', 'bottom'); + checkAxis('yaxis2', 'x2', 'left'); + + return Plotly.relayout(gd, {'grid.xside': 'top plot', 'grid.yside': 'right plot'}); + }) + .then(function() { + checkAxis('xaxis', 'y', 'top'); + checkAxis('yaxis', 'x', 'right'); + checkAxis('xaxis2', 'y2', 'top'); + checkAxis('yaxis2', 'x2', 'right'); + + return Plotly.relayout(gd, {'grid.xside': 'top', 'grid.yside': 'right'}); + }) + .then(function() { + checkAxis('xaxis', 'free', 'top', 1); + checkAxis('yaxis', 'free', 'right', 1); + checkAxis('xaxis2', 'free', 'top', 1); + checkAxis('yaxis2', 'free', 'right', 1); + + return Plotly.relayout(gd, {'grid.xside': 'bottom', 'grid.yside': 'left'}); + }) + .then(function() { + checkAxis('xaxis', 'free', 'bottom', 0); + checkAxis('yaxis', 'free', 'left', 0); + checkAxis('xaxis2', 'free', 'bottom', 0); + checkAxis('yaxis2', 'free', 'left', 0); + }) + .catch(failTest) + .then(done); + }); +}); From cd6e8232121b819b1618da9de9975faf9f0b3e86 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 23 Feb 2018 13:38:07 -0500 Subject: [PATCH 05/12] catch promise errors in parcoords_test --- test/jasmine/tests/parcoords_test.js | 165 +++++++++++++++------------ 1 file changed, 90 insertions(+), 75 deletions(-) diff --git a/test/jasmine/tests/parcoords_test.js b/test/jasmine/tests/parcoords_test.js index a1dc36f1130..93f4c5d31a0 100644 --- a/test/jasmine/tests/parcoords_test.js +++ b/test/jasmine/tests/parcoords_test.js @@ -272,9 +272,9 @@ describe('@gl parcoords', function() { expect(gd.data[0].dimensions[1].range).toBeDefined(); expect(gd.data[0].dimensions[1].range).toEqual([0, 700000]); expect(gd.data[0].dimensions[1].constraintrange).not.toBeDefined(); - - done(); - }); + }) + .catch(fail) + .then(done); }); it('Do something sensible if there is no panel i.e. dimension count is less than 2', function(done) { @@ -291,9 +291,9 @@ describe('@gl parcoords', function() { expect(gd.data[0].dimensions[0].range).not.toBeDefined(); expect(gd.data[0].dimensions[0].constraintrange).toBeDefined(); expect(gd.data[0].dimensions[0].constraintrange).toEqual([200, 700]); - - done(); - }); + }) + .catch(fail) + .then(done); }); it('Does not error with zero dimensions', function(done) { @@ -302,11 +302,13 @@ describe('@gl parcoords', function() { var gd = createGraphDiv(); Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + expect(gd.data.length).toEqual(1); expect(gd.data[0].dimensions.length).toEqual(0); expect(document.querySelectorAll('.axis').length).toEqual(0); - done(); - }); + }) + .catch(fail) + .then(done); }); it('Works with duplicate dimension labels', function(done) { @@ -322,8 +324,9 @@ describe('@gl parcoords', function() { expect(gd.data.length).toEqual(1); expect(gd.data[0].dimensions.length).toEqual(2); expect(document.querySelectorAll('.axis').length).toEqual(2); - done(); - }); + }) + .catch(fail) + .then(done); }); it('Works with a single line; also, use a longer color array than the number of lines', function(done) { @@ -349,8 +352,9 @@ describe('@gl parcoords', function() { expect(gd.data[0].dimensions.length).toEqual(2); expect(document.querySelectorAll('.axis').length).toEqual(2); expect(gd.data[0].dimensions[0].values.length).toEqual(1); - done(); - }); + }) + .catch(fail) + .then(done); }); it('Does not raise an error with zero lines and no specified range', function(done) { @@ -373,8 +377,9 @@ describe('@gl parcoords', function() { expect(gd.data[0].dimensions.length).toEqual(2); expect(document.querySelectorAll('.axis').length).toEqual(0); expect(gd.data[0].dimensions[0].values.length).toEqual(0); - done(); - }); + }) + .catch(fail) + .then(done); }); it('Works with non-finite `values` elements', function(done) { @@ -401,8 +406,9 @@ describe('@gl parcoords', function() { expect(gd.data[0].dimensions.length).toEqual(2); expect(document.querySelectorAll('.axis').length).toEqual(2); expect(gd.data[0].dimensions[0].values.length).toEqual(values[0].length); - done(); - }); + }) + .catch(fail) + .then(done); }); it('@noCI Works with 60 dimensions', function(done) { @@ -431,8 +437,9 @@ describe('@gl parcoords', function() { expect(gd.data.length).toEqual(1); expect(gd.data[0].dimensions.length).toEqual(60); expect(document.querySelectorAll('.axis').length).toEqual(60); - done(); - }); + }) + .catch(fail) + .then(done); }); it('@noCI Truncates 60+ dimensions to 60', function(done) { @@ -459,8 +466,9 @@ describe('@gl parcoords', function() { expect(gd.data.length).toEqual(1); expect(gd.data[0].dimensions.length).toEqual(60); expect(document.querySelectorAll('.axis').length).toEqual(60); - done(); - }); + }) + .catch(fail) + .then(done); }); it('@noCI Truncates dimension values to the shortest array, retaining only 3 lines', function(done) { @@ -488,8 +496,9 @@ describe('@gl parcoords', function() { expect(gd.data.length).toEqual(1); expect(gd.data[0].dimensions.length).toEqual(60); expect(document.querySelectorAll('.axis').length).toEqual(60); - done(); - }); + }) + .catch(fail) + .then(done); }); it('Skip dimensions which are not plain objects or whose `values` is not an array', function(done) { @@ -521,8 +530,9 @@ describe('@gl parcoords', function() { expect(gd.data.length).toEqual(1); expect(gd.data[0].dimensions.length).toEqual(5); // it's still five, but ... expect(document.querySelectorAll('.axis').length).toEqual(3); // only 3 axes shown - done(); - }); + }) + .catch(fail) + .then(done); }); @@ -539,7 +549,9 @@ describe('@gl parcoords', function() { y: [0.05, 0.85] }; gd = createGraphDiv(); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .catch(fail) + .then(done); }); it('`Plotly.plot` should have proper fields on `gd.data` on initial rendering', function() { @@ -584,9 +596,9 @@ describe('@gl parcoords', function() { expect(gd.data[1].dimensions[1].constraintrange).not.toBeDefined(); expect(document.querySelectorAll('.axis').length).toEqual(20); // one dimension is `visible: false` - - done(); - }); + }) + .catch(fail) + .then(done); }); @@ -604,9 +616,9 @@ describe('@gl parcoords', function() { expect(gd.data[0].dimensions[0].constraintrange).toBeDefined(); expect(gd.data[0].dimensions[0].constraintrange).toEqual([100000, 150000]); expect(gd.data[0].dimensions[1].constraintrange).not.toBeDefined(); - - done(); - }); + }) + .catch(fail) + .then(done); }); @@ -632,6 +644,7 @@ describe('@gl parcoords', function() { .then(restyleDimension('constraintrange', [[0, 1]])) .then(restyleDimension('values', [[0, 0.1, 0.4, 1, 2, 0, 0.1, 0.4, 1, 2]])) .then(restyleDimension('visible', false)) + .catch(fail) .then(done); }); @@ -652,11 +665,9 @@ describe('@gl parcoords', function() { expect(gd.data[0].dimensions[0].constraintrange).toBeDefined(); expect(gd.data[0].dimensions[0].constraintrange).toEqual([100000, 150000]); expect(gd.data[0].dimensions[1].constraintrange).not.toBeDefined(); - - done(); - }); - - + }) + .catch(fail) + .then(done); }); it('Should emit a \'plotly_restyle\' event', function(done) { @@ -677,10 +688,11 @@ describe('@gl parcoords', function() { expect(tester.get()).toBe(false); Plotly.restyle(gd, 'line.colorscale', 'Viridis') - .then(window.setTimeout(function() { - expect(tester.get()).toBe(true); - done(); - }, 0)); + .then(function() { + expect(tester.get()).toBe(true); + }) + .catch(fail) + .then(done); }); @@ -715,20 +727,24 @@ describe('@gl parcoords', function() { mouseEvent('mousemove', 315, 218); mouseEvent('mouseover', 315, 218); - window.setTimeout(function() { + new Promise(function(resolve) { + window.setTimeout(function() { - expect(hoverTester.get()).toBe(true); + expect(hoverTester.get()).toBe(true); - mouseEvent('mousemove', 329, 153); - mouseEvent('mouseover', 329, 153); + mouseEvent('mousemove', 329, 153); + mouseEvent('mouseover', 329, 153); - window.setTimeout(function() { + window.setTimeout(function() { - expect(unhoverTester.get()).toBe(true); - done(); - }, 20); + expect(unhoverTester.get()).toBe(true); + resolve(); + }, 20); - }, 20); + }, 20); + }) + .catch(fail) + .then(done); }); @@ -747,10 +763,9 @@ describe('@gl parcoords', function() { expect(gd.data[0].dimensions[0].constraintrange).toBeDefined(); expect(gd.data[0].dimensions[0].constraintrange).toEqual([100000, 150000]); expect(gd.data[0].dimensions[1].constraintrange).not.toBeDefined(); - - done(); - }); - + }) + .catch(fail) + .then(done); }); it('Calling `Plotly.relayout`with object should amend the preexisting parcoords', function(done) { @@ -768,10 +783,9 @@ describe('@gl parcoords', function() { expect(gd.data[0].dimensions[0].constraintrange).toBeDefined(); expect(gd.data[0].dimensions[0].constraintrange).toEqual([100000, 150000]); expect(gd.data[0].dimensions[1].constraintrange).not.toBeDefined(); - - done(); - }); - + }) + .catch(fail) + .then(done); }); it('@flaky Calling `Plotly.animate` with patches targeting `dimensions` attributes should do the right thing', function(done) { @@ -827,12 +841,13 @@ describe('@gl parcoords', function() { expect(gd.data.length).toEqual(1); - Plotly.deleteTraces(gd, 0).then(function() { + return Plotly.deleteTraces(gd, 0).then(function() { expect(d3.selectAll('.gl-canvas').node(0)).toEqual(null); expect(gd.data.length).toEqual(0); - done(); }); - }); + }) + .catch(fail) + .then(done); }); it('Plotly.deleteTraces with two traces removes the deleted plot', function(done) { @@ -864,8 +879,9 @@ describe('@gl parcoords', function() { expect(document.querySelectorAll('.gl-canvas').length).toEqual(0); expect(document.querySelectorAll('.y-axis').length).toEqual(0); expect(gd.data.length).toEqual(0); - done(); - }); + }) + .catch(fail) + .then(done); }); it('Calling `Plotly.restyle` with zero panels left should erase lines', function(done) { @@ -894,8 +910,9 @@ describe('@gl parcoords', function() { } while(!foundPixel && i < imageArray.length); expect(foundPixel).toEqual(false); }); - done(); - }); + }) + .catch(fail) + .then(done); }); describe('Having two datasets', function() { @@ -925,9 +942,9 @@ describe('@gl parcoords', function() { expect(1).toEqual(1); expect(document.querySelectorAll('.gl-container').length).toEqual(1); expect(gd.data.length).toEqual(2); - - done(); - }); + }) + .catch(fail) + .then(done); }); it('Plotly.addTraces should add a new parcoords row', function(done) { @@ -952,10 +969,9 @@ describe('@gl parcoords', function() { .then(function() { expect(document.querySelectorAll('.gl-container').length).toEqual(1); expect(gd.data.length).toEqual(2); - - done(); - }); - + }) + .catch(fail) + .then(done); }); it('Plotly.restyle should update the existing parcoords row', function(done) { @@ -1001,10 +1017,9 @@ describe('@gl parcoords', function() { expect(document.querySelectorAll('.gl-container').length).toEqual(1); expect(gd.data.length).toEqual(1); - - done(); - }); - + }) + .catch(fail) + .then(done); }); }); }); From 1aec7f7aee678161b214d095db2eb857fbcdb2a4 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 23 Feb 2018 13:50:19 -0500 Subject: [PATCH 06/12] clean up parcoords handling of uneven and missing arrays Apparently I missed parcoords in the Plotly.react cleanup --- src/lib/stats.js | 2 +- src/traces/parcoords/attributes.js | 1 - src/traces/parcoords/calc.js | 12 +++++-- src/traces/parcoords/defaults.js | 49 ++++++++++++++++------------ src/traces/parcoords/parcoords.js | 20 +++++++++--- test/jasmine/tests/parcoords_test.js | 17 ++++------ 6 files changed, 61 insertions(+), 40 deletions(-) diff --git a/src/lib/stats.js b/src/lib/stats.js index 134c17559c2..9e48bc081b4 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -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); diff --git a/src/traces/parcoords/attributes.js b/src/traces/parcoords/attributes.js index 02f9a72ac86..177a872b918 100644 --- a/src/traces/parcoords/attributes.js +++ b/src/traces/parcoords/attributes.js @@ -92,7 +92,6 @@ module.exports = { values: { valType: 'data_array', role: 'info', - dflt: [], editType: 'calc', description: [ 'Dimension values. `values[n]` represents the value of the `n`th point in the dataset,', diff --git a/src/traces/parcoords/calc.js b/src/traces/parcoords/calc.js index c9f54b985cd..702862132c7 100644 --- a/src/traces/parcoords/calc.js +++ b/src/traces/parcoords/calc.js @@ -15,11 +15,11 @@ var wrap = require('../../lib/gup').wrap; module.exports = function calc(gd, trace) { var cs = !!trace.line.colorscale && Lib.isArray(trace.line.color); - var color = cs ? trace.line.color : Array.apply(0, Array(trace.dimensions.reduce(function(p, n) {return Math.max(p, n.values.length);}, 0))).map(function() {return 0.5;}); + var color = cs ? trace.line.color : constHalf(trace._commonLength); var cscale = cs ? trace.line.colorscale : [[0, trace.line.color], [1, trace.line.color]]; if(hasColorscale(trace, 'line')) { - calcColorscale(trace, trace.line.color, 'line', 'c'); + calcColorscale(trace, color, 'line', 'c'); } return wrap({ @@ -27,3 +27,11 @@ module.exports = function calc(gd, trace) { cscale: cscale }); }; + +function constHalf(len) { + var out = new Array(len); + for(var i = 0; i < len; i++) { + out[i] = 0.5; + } + return out; +} diff --git a/src/traces/parcoords/defaults.js b/src/traces/parcoords/defaults.js index c9f9cf93474..bbcdd63b475 100644 --- a/src/traces/parcoords/defaults.js +++ b/src/traces/parcoords/defaults.js @@ -17,14 +17,20 @@ var handleDomainDefaults = require('../../plots/domain').defaults; function handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce) { - coerce('line.color', defaultColor); - - if(hasColorscale(traceIn, 'line') && Lib.isArray(traceIn.line.color)) { - coerce('line.colorscale'); - colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'line.', cLetter: 'c'}); - } - else { - coerce('line.color', defaultColor); + var lineColor = coerce('line.color', defaultColor); + + if(hasColorscale(traceIn, 'line') && Lib.isArray(lineColor)) { + if(lineColor.length) { + coerce('line.colorscale'); + colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'line.', cLetter: 'c'}); + // TODO: I think it would be better to keep showing lines beyond the last line color + // but I'm not sure what color to give these lines - probably black or white + // depending on the background color? + traceOut._commonLength = Math.min(traceOut._commonLength, lineColor.length); + } + else { + traceOut.line.color = defaultColor; + } } } @@ -53,7 +59,10 @@ function dimensionsDefaults(traceIn, traceOut) { } var values = coerce('values'); - var visible = coerce('visible', values.length > 0); + var visible = coerce('visible'); + if(!(values && values.length)) { + visible = dimensionOut.visible = false; + } if(visible) { coerce('label'); @@ -63,21 +72,14 @@ function dimensionsDefaults(traceIn, traceOut) { coerce('range'); coerce('constraintrange'); - commonLength = Math.min(commonLength, dimensionOut.values.length); + commonLength = Math.min(commonLength, values.length); } dimensionOut._index = i; dimensionsOut.push(dimensionOut); } - if(isFinite(commonLength)) { - for(i = 0; i < dimensionsOut.length; i++) { - dimensionOut = dimensionsOut[i]; - if(dimensionOut.visible && dimensionOut.values.length > commonLength) { - dimensionOut.values = dimensionOut.values.slice(0, commonLength); - } - } - } + traceOut._commonLength = commonLength; return dimensionsOut; } @@ -97,11 +99,18 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout traceOut.visible = false; } - // make default font size 10px, + // since we're not slicing uneven arrays anymore, stash the length in each dimension + // but we can't do this in dimensionsDefaults (yet?) because line.color can also + // truncate + for(var i = 0; i < dimensions.length; i++) { + if(dimensions[i].visible) dimensions[i]._length = traceOut._commonLength; + } + + // make default font size 10px (default is 12), // scale linearly with global font size var fontDflt = { family: layout.font.family, - size: Math.round(layout.font.size * (10 / 12)), + size: Math.round(layout.font.size / 1.2), color: layout.font.color }; diff --git a/src/traces/parcoords/parcoords.js b/src/traces/parcoords/parcoords.js index 74f1a31f111..c622b2ae9af 100644 --- a/src/traces/parcoords/parcoords.js +++ b/src/traces/parcoords/parcoords.js @@ -21,8 +21,8 @@ function visible(dimension) {return !('visible' in dimension) || dimension.visib function dimensionExtent(dimension) { - var lo = dimension.range ? dimension.range[0] : d3.min(dimension.values); - var hi = dimension.range ? dimension.range[1] : d3.max(dimension.values); + var lo = dimension.range ? dimension.range[0] : Lib.aggNums(Math.min, null, dimension.values, dimension._length); + var hi = dimension.range ? dimension.range[1] : Lib.aggNums(Math.max, null, dimension.values, dimension._length); if(isNaN(lo) || !isFinite(lo)) { lo = 0; @@ -139,7 +139,11 @@ function model(layout, d, i) { rangeFont = trace.rangefont; var lines = Lib.extendDeep({}, line, { - color: lineColor.map(domainToUnitScale({values: lineColor, range: [line.cmin, line.cmax]})), + color: lineColor.map(domainToUnitScale({ + values: lineColor, + range: [line.cmin, line.cmax], + _length: trace._commonLength + })), blockLineCount: c.blockLineCount, canvasOverdrag: c.overdrag * c.canvasPixelRatio }); @@ -201,6 +205,12 @@ function viewModel(model) { var foundKey = uniqueKeys[dimension.label]; uniqueKeys[dimension.label] = (foundKey || 0) + 1; var key = dimension.label + (foundKey ? '__' + foundKey : ''); + + var truncatedValues = dimension.values; + if(truncatedValues.length > dimension._length) { + truncatedValues = truncatedValues.slice(0, dimension._length); + } + return { key: key, label: dimension.label, @@ -213,8 +223,8 @@ function viewModel(model) { crossfilterDimensionIndex: i, visibleIndex: dimension._index, height: height, - values: dimension.values, - paddedUnitValues: dimension.values.map(domainToUnit).map(paddedUnitScale), + values: truncatedValues, + paddedUnitValues: truncatedValues.map(domainToUnit).map(paddedUnitScale), xScale: xScale, x: xScale(i), canvasX: xScale(i) * canvasPixelRatio, diff --git a/test/jasmine/tests/parcoords_test.js b/test/jasmine/tests/parcoords_test.js index 93f4c5d31a0..bdb36c91958 100644 --- a/test/jasmine/tests/parcoords_test.js +++ b/test/jasmine/tests/parcoords_test.js @@ -118,7 +118,7 @@ describe('parcoords initialization tests', function() { alienProperty: 'Alpha Centauri' }] }); - expect(fullTrace.dimensions).toEqual([{values: [1], visible: true, tickformat: '3s', _index: 0}]); + expect(fullTrace.dimensions).toEqual([{values: [1], visible: true, tickformat: '3s', _index: 0, _length: 1}]); }); it('\'dimension.visible\' should be set to false, and other props just passed through if \'values\' is not provided', function() { @@ -127,7 +127,7 @@ describe('parcoords initialization tests', function() { alienProperty: 'Alpha Centauri' }] }); - expect(fullTrace.dimensions).toEqual([{visible: false, values: [], _index: 0}]); + expect(fullTrace.dimensions).toEqual([{visible: false, _index: 0}]); }); it('\'dimension.visible\' should be set to false, and other props just passed through if \'values\' is an empty array', function() { @@ -147,22 +147,17 @@ describe('parcoords initialization tests', function() { alienProperty: 'Alpha Centauri' }] }); - expect(fullTrace.dimensions).toEqual([{visible: false, values: [], _index: 0}]); + expect(fullTrace.dimensions).toEqual([{visible: false, _index: 0}]); }); - it('\'dimension.values\' should get truncated to a common shortest length', function() { + it('\'dimension.values\' should get truncated to a common shortest *nonzero* length', function() { var fullTrace = _supply({dimensions: [ {values: [321, 534, 542, 674]}, {values: [562, 124, 942]}, {values: [], visible: true}, - {values: [1, 2], visible: false} // shouldn't be truncated to as false + {values: [1, 2], visible: false} // shouldn't be truncated to as visible: false ]}); - expect(fullTrace.dimensions).toEqual([ - {values: [], visible: true, tickformat: '3s', _index: 0}, - {values: [], visible: true, tickformat: '3s', _index: 1}, - {values: [], visible: true, tickformat: '3s', _index: 2}, - {values: [1, 2], visible: false, _index: 3} - ]); + expect(fullTrace._commonLength).toBe(3); }); }); From 446f9f95d7539c5dc603b362e900688634eb8b0a Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 23 Feb 2018 17:57:52 -0500 Subject: [PATCH 07/12] try marking the test that's failing on CI only as flaky still don't know why it's failing... but it seems to work when run in a smaller batch --- test/jasmine/tests/parcoords_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/parcoords_test.js b/test/jasmine/tests/parcoords_test.js index bdb36c91958..0adb306491b 100644 --- a/test/jasmine/tests/parcoords_test.js +++ b/test/jasmine/tests/parcoords_test.js @@ -496,7 +496,7 @@ describe('@gl parcoords', function() { .then(done); }); - it('Skip dimensions which are not plain objects or whose `values` is not an array', function(done) { + it('@flaky Skip dimensions which are not plain objects or whose `values` is not an array', function(done) { var mockCopy = Lib.extendDeep({}, mock1); var newDimension, i, j; From 40dd784546bbc41f8a1e874bd737135ff235cdb9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 24 Feb 2018 21:16:49 -0500 Subject: [PATCH 08/12] validate info_array, make grid defaults work better with validate --- src/plot_api/validate.js | 56 ++++++++++++++++++++++--- src/plots/grid.js | 3 ++ test/jasmine/tests/validate_test.js | 65 +++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 5 deletions(-) diff --git a/src/plot_api/validate.js b/src/plot_api/validate.js index 8f163fb92c9..0b77146a4e1 100644 --- a/src/plot_api/validate.js +++ b/src/plot_api/validate.js @@ -164,9 +164,10 @@ 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)); @@ -174,9 +175,54 @@ function crawl(objIn, objOut, schema, list, base, path) { 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; diff --git a/src/plots/grid.js b/src/plots/grid.js index 0943f0a6dbd..030022e196c 100644 --- a/src/plots/grid.js +++ b/src/plots/grid.js @@ -276,6 +276,8 @@ exports.contentDefaults = function(layoutIn, layoutOut) { } else subplotId = rowIn[j]; + rowOut[j] = ''; + if(subplots.cartesian.indexOf(subplotId) !== -1) { yPos = subplotId.indexOf('y'); xId = subplotId.slice(0, yPos); @@ -390,6 +392,7 @@ function fillGridAxes(axesIn, axesAllowed, len, axisMap, axLetter) { out[i] = axisId; axisMap[axisId] = i; } + else out[i] = ''; } if(Array.isArray(axesIn)) { diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index 3014e3540a3..ad12aeefd50 100644 --- a/test/jasmine/tests/validate_test.js +++ b/test/jasmine/tests/validate_test.js @@ -440,4 +440,69 @@ describe('Plotly.validate', function() { 'In layout, key yaxis2.overlaying (set to \'x\') got reset to \'false\' during defaults.' ); }); + + it('catches bad axes in grid definitions', function() { + var out = Plotly.validate([ + {y: [1, 2]}, + {y: [1, 2], xaxis: 'x2', yaxis: 'y2'} + ], { + grid: {xaxes: ['x3', '', 'x2', 4], yaxes: ['y', 3, '', 'y4']}, + // while we're at it check on another info_array + xaxis: {range: [5, 'lots']} + }); + + expect(out.length).toBe(5); + assertErrorContent( + out[0], 'dynamic', 'layout', null, + ['grid', 'xaxes', 0], 'grid.xaxes[0]', + 'In layout, key grid.xaxes[0] (set to \'x3\') got reset to \'\' during defaults.' + ); + assertErrorContent( + out[1], 'value', 'layout', null, + ['grid', 'xaxes', 3], 'grid.xaxes[3]', + 'In layout, key grid.xaxes[3] is set to an invalid value (4)' + ); + assertErrorContent( + out[2], 'value', 'layout', null, + ['grid', 'yaxes', 1], 'grid.yaxes[1]', + 'In layout, key grid.yaxes[1] is set to an invalid value (3)' + ); + assertErrorContent( + out[3], 'dynamic', 'layout', null, + ['grid', 'yaxes', 3], 'grid.yaxes[3]', + 'In layout, key grid.yaxes[3] (set to \'y4\') got reset to \'\' during defaults.' + ); + assertErrorContent( + out[4], 'dynamic', 'layout', null, + ['xaxis', 'range', 1], 'xaxis.range[1]', + 'In layout, key xaxis.range[1] (set to \'lots\') got reset to \'50\' during defaults.' + ); + }); + + it('catches bad subplots in grid definitions', function() { + var out = Plotly.validate([ + {y: [1, 2]}, + {y: [1, 2], xaxis: 'x2', yaxis: 'y2'}, + {y: [1, 2], xaxis: 'x2'} + ], { + grid: {subplots: [['xy', 'x2y3'], ['x2y', 'x2y2'], [5, '']]}, + }); + + expect(out.length).toBe(3); + assertErrorContent( + out[0], 'dynamic', 'layout', null, + ['grid', 'subplots', 0, 1], 'grid.subplots[0][1]', + 'In layout, key grid.subplots[0][1] (set to \'x2y3\') got reset to \'\' during defaults.' + ); + assertErrorContent( + out[1], 'dynamic', 'layout', null, + ['grid', 'subplots', 1, 0], 'grid.subplots[1][0]', + 'In layout, key grid.subplots[1][0] (set to \'x2y\') got reset to \'\' during defaults.' + ); + assertErrorContent( + out[2], 'value', 'layout', null, + ['grid', 'subplots', 2, 0], 'grid.subplots[2][0]', + 'In layout, key grid.subplots[2][0] is set to an invalid value (5)' + ); + }); }); From 670bdd59bb696fb5ef6bf0d785a183632451ba8c Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 24 Feb 2018 21:33:01 -0500 Subject: [PATCH 09/12] :hocho: grid.gap --- src/plots/grid.js | 29 ++++--------- test/image/mocks/grid_subplot_types.json | 2 +- test/jasmine/tests/plots_test.js | 52 ++++++++++-------------- 3 files changed, 30 insertions(+), 53 deletions(-) diff --git a/src/plots/grid.js b/src/plots/grid.js index 030022e196c..60313ec22e5 100644 --- a/src/plots/grid.js +++ b/src/plots/grid.js @@ -109,20 +109,6 @@ var gridAttrs = exports.attributes = { 'then iterating rows according to `roworder`.' ].join(' ') }, - gap: { - valType: 'number', - min: 0, - max: 1, - dflt: 0.1, - role: 'info', - editType: 'calc', - description: [ - 'Space between grid cells, expressed as a fraction of the total', - 'width or height available to one cell. You can also use `xgap`', - 'and `ygap` to space x and y differently. Defaults to 0.1 for', - 'coupled-axes grids and 0.25 for independent grids.' - ].join(' ') - }, xgap: { valType: 'number', min: 0, @@ -131,7 +117,8 @@ var gridAttrs = exports.attributes = { editType: 'calc', description: [ 'Horizontal space between grid cells, expressed as a fraction', - 'of the total width available to one cell.' + 'of the total width available to one cell. Defaults to 0.1', + 'for coupled-axes grids and 0.2 for independent grids.' ].join(' ') }, ygap: { @@ -142,7 +129,8 @@ var gridAttrs = exports.attributes = { editType: 'calc', description: [ 'Vertical space between grid cells, expressed as a fraction', - 'of the total height available to one cell.' + 'of the total height available to one cell. Defaults to 0.1', + 'for coupled-axes grids and 0.3 for independent grids.' ].join(' ') }, domain: domainAttrs({name: 'grid', editType: 'calc', noGridCell: true}, { @@ -218,17 +206,16 @@ exports.sizeDefaults = function(layoutIn, layoutOut) { var rowOrder = coerce('roworder'); var reversed = rowOrder === 'top to bottom'; - var gap = coerce('gap', hasSubplotGrid ? 0.25 : 0.1); gridOut._domains = { - x: fillGridPositions('x', coerce, gap, columns), - y: fillGridPositions('y', coerce, gap, rows, reversed) + x: fillGridPositions('x', coerce, hasSubplotGrid ? 0.2 : 0.1, columns), + y: fillGridPositions('y', coerce, hasSubplotGrid ? 0.3 : 0.1, rows, reversed) }; }; // coerce x or y sizing attributes and return an array of domains for this direction -function fillGridPositions(axLetter, coerce, gap, len, reversed) { - var dirGap = coerce(axLetter + 'gap', gap); +function fillGridPositions(axLetter, coerce, dfltGap, len, reversed) { + var dirGap = coerce(axLetter + 'gap', dfltGap); var domain = coerce('domain.' + axLetter); coerce(axLetter + 'side'); diff --git a/test/image/mocks/grid_subplot_types.json b/test/image/mocks/grid_subplot_types.json index cf03a874a85..fbdf2fd7454 100644 --- a/test/image/mocks/grid_subplot_types.json +++ b/test/image/mocks/grid_subplot_types.json @@ -22,7 +22,7 @@ "yaxis2": {"title": "y2"}, "xaxis3": {"title": "x3"}, "yaxis3": {"title": "y3"}, - "grid": {"rows": 3, "columns": 3, "gap": 0.3}, + "grid": {"rows": 3, "columns": 3, "xgap": 0.3, "ygap": 0.3}, "scene": {"domain": {"row": 1, "column": 1}}, "ternary": {"domain": {"row": 1, "column": 2}}, "geo": {"domain": {"row": 2, "column": 1}}, diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index c0dee094eb2..53742785274 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -940,12 +940,12 @@ describe('grids', function() { ) .then(function() { _assertDomains({ - xaxis2: [1 / 2.75, 1.75 / 2.75], - yaxis2: [2 / 2.75, 1], - xaxis3: [2 / 2.75, 1], - yaxis3: [2 / 2.75, 1], - xaxis4: [0, 0.75 / 2.75], - yaxis4: [1 / 2.75, 1.75 / 2.75] + xaxis2: [1 / 2.8, 1.8 / 2.8], + yaxis2: [2 / 2.7, 1], + xaxis3: [2 / 2.8, 1], + yaxis3: [2 / 2.7, 1], + xaxis4: [0, 0.8 / 2.8], + yaxis4: [1 / 2.7, 1.7 / 2.7] }); _assertMissing(['xaxis', 'yaxis']); @@ -955,12 +955,12 @@ describe('grids', function() { }) .then(function() { _assertDomains({ - xaxis2: [1 / 2.75, 1.75 / 2.75], - yaxis2: [0, 0.75 / 2.75], - xaxis3: [2 / 2.75, 1], - yaxis3: [2 / 2.75, 1], - xaxis4: [0, 0.75 / 2.75], - yaxis4: [2 / 2.75, 1] + xaxis2: [1 / 2.8, 1.8 / 2.8], + yaxis2: [0, 0.7 / 2.7], + xaxis3: [2 / 2.8, 1], + yaxis3: [2 / 2.7, 1], + xaxis4: [0, 0.8 / 2.8], + yaxis4: [2 / 2.7, 1] }); _assertMissing(['xaxis', 'yaxis']); @@ -968,12 +968,12 @@ describe('grids', function() { }) .then(function() { _assertDomains({ - xaxis2: [1 / 2.75, 1.75 / 2.75], - yaxis2: [2 / 2.75, 1], - xaxis3: [2 / 2.75, 1], - yaxis3: [0, 0.75 / 2.75], - xaxis4: [0, 0.75 / 2.75], - yaxis4: [0, 0.75 / 2.75] + xaxis2: [1 / 2.8, 1.8 / 2.8], + yaxis2: [2 / 2.7, 1], + xaxis3: [2 / 2.8, 1], + yaxis3: [0, 0.7 / 2.7], + xaxis4: [0, 0.8 / 2.8], + yaxis4: [0, 0.7 / 2.7] }); _assertMissing(['xaxis', 'yaxis']); }) @@ -981,7 +981,7 @@ describe('grids', function() { .then(done); }); - it('can set x and y gaps together or separately and change domain', function(done) { + it('can set x and y gaps and change domain', function(done) { Plotly.newPlot(gd, // leave some empty rows/columns makeData(['xy', 'x2y2']), @@ -995,24 +995,14 @@ describe('grids', function() { yaxis2: [0, 0.9 / 1.9] }); - return Plotly.relayout(gd, {'grid.gap': 0.4}); - }) - .then(function() { - _assertDomains({ - xaxis: [0, 0.6 / 1.6], - yaxis: [1 / 1.6, 1], - xaxis2: [1 / 1.6, 1], - yaxis2: [0, 0.6 / 1.6] - }); - return Plotly.relayout(gd, {'grid.xgap': 0.2}); }) .then(function() { _assertDomains({ xaxis: [0, 0.8 / 1.8], - yaxis: [1 / 1.6, 1], + yaxis: [1 / 1.9, 1], xaxis2: [1 / 1.8, 1], - yaxis2: [0, 0.6 / 1.6] + yaxis2: [0, 0.9 / 1.9] }); return Plotly.relayout(gd, {'grid.ygap': 0.3}); From 6b8d461f05eb5daee02caf9f13d2a7619303b6f5 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 24 Feb 2018 22:41:32 -0500 Subject: [PATCH 10/12] drop domain and grid editTypes from calc to plot --- src/plots/cartesian/layout_attributes.js | 6 +++--- src/plots/grid.js | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 225c409e249..64783e015ea 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -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(' ') diff --git a/src/plots/grid.js b/src/plots/grid.js index 60313ec22e5..7e1f75722a6 100644 --- a/src/plots/grid.js +++ b/src/plots/grid.js @@ -19,7 +19,7 @@ var gridAttrs = exports.attributes = { valType: 'integer', min: 1, role: 'info', - editType: 'calc', + editType: 'plot', description: [ 'The number of rows in the grid. If you provide a 2D `subplots`', 'array or a `yaxes` array, its length is used as the default.', @@ -32,7 +32,7 @@ var gridAttrs = exports.attributes = { values: ['top to bottom', 'bottom to top'], dflt: 'top to bottom', role: 'info', - editType: 'calc', + editType: 'plot', description: [ 'Is the first row the top or the bottom? Note that columns', 'are always enumerated from left to right.' @@ -42,7 +42,7 @@ var gridAttrs = exports.attributes = { valType: 'integer', min: 1, role: 'info', - editType: 'calc', + editType: 'plot', description: [ 'The number of columns in the grid. If you provide a 2D `subplots`', 'array, the length of its longest row is used as the default.', @@ -57,7 +57,7 @@ var gridAttrs = exports.attributes = { dimensions: 2, items: {valType: 'enumerated', values: [counterRegex('xy').toString(), '']}, role: 'info', - editType: 'calc', + editType: 'plot', description: [ 'Used for freeform grids, where some axes may be shared across subplots', 'but others are not. Each entry should be a cartesian subplot id, like', @@ -72,7 +72,7 @@ var gridAttrs = exports.attributes = { freeLength: true, items: {valType: 'enumerated', values: [cartesianIdRegex.x.toString(), '']}, role: 'info', - editType: 'calc', + editType: 'plot', description: [ 'Used with `yaxes` when the x and y axes are shared across columns and rows.', 'Each entry should be an x axis id like *x*, *x2*, etc., or ** to', @@ -86,7 +86,7 @@ var gridAttrs = exports.attributes = { freeLength: true, items: {valType: 'enumerated', values: [cartesianIdRegex.y.toString(), '']}, role: 'info', - editType: 'calc', + editType: 'plot', description: [ 'Used with `yaxes` when the x and y axes are shared across columns and rows.', 'Each entry should be an y axis id like *y*, *y2*, etc., or ** to', @@ -100,7 +100,7 @@ var gridAttrs = exports.attributes = { values: ['independent', 'coupled'], dflt: 'coupled', role: 'info', - editType: 'calc', + editType: 'plot', description: [ 'If no `subplots`, `xaxes`, or `yaxes` are given but we do have `rows` and `columns`,', 'we can generate defaults using consecutive axis IDs, in two ways:', @@ -114,7 +114,7 @@ var gridAttrs = exports.attributes = { min: 0, max: 1, role: 'info', - editType: 'calc', + editType: 'plot', description: [ 'Horizontal space between grid cells, expressed as a fraction', 'of the total width available to one cell. Defaults to 0.1', @@ -126,14 +126,14 @@ var gridAttrs = exports.attributes = { min: 0, max: 1, role: 'info', - editType: 'calc', + editType: 'plot', description: [ 'Vertical space between grid cells, expressed as a fraction', 'of the total height available to one cell. Defaults to 0.1', 'for coupled-axes grids and 0.3 for independent grids.' ].join(' ') }, - domain: domainAttrs({name: 'grid', editType: 'calc', noGridCell: true}, { + domain: domainAttrs({name: 'grid', editType: 'plot', noGridCell: true}, { description: [ 'The first and last cells end exactly at the domain', 'edges, with no grout around the edges.' @@ -163,7 +163,7 @@ var gridAttrs = exports.attributes = { 'that each y axis is used in. *right* and *right plot* are similar.' ].join(' ') }, - editType: 'calc' + editType: 'plot' }; // the shape of the grid - this needs to be done BEFORE supplyDataDefaults From 4b43e35587ea990f24f4598113189069878476a1 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 24 Feb 2018 22:52:15 -0500 Subject: [PATCH 11/12] row and column default to 0 in domain attrs --- src/plots/domain.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plots/domain.js b/src/plots/domain.js index afd410c0657..3c6dc828780 100644 --- a/src/plots/domain.js +++ b/src/plots/domain.js @@ -76,6 +76,7 @@ exports.attributes = function(opts, extra) { out.row = { valType: 'integer', min: 0, + dflt: 0, role: 'info', editType: opts.editType, description: [ @@ -90,6 +91,7 @@ exports.attributes = function(opts, extra) { out.column = { valType: 'integer', min: 0, + dflt: 0, role: 'info', editType: opts.editType, description: [ From 794669bf39accfc5bf3faa394c6c1c0d5d9a6e2e Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 26 Feb 2018 14:48:36 -0500 Subject: [PATCH 12/12] default subplot row/column test --- test/jasmine/tests/plots_test.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 53742785274..753a9b95523 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -1075,4 +1075,36 @@ describe('grids', function() { .catch(failTest) .then(done); }); + + it('places other subplots in the grid by default', function(done) { + function checkDomain(container, column, row, x, y) { + var domain = container.domain; + expect(domain.row).toBe(row); + expect(domain.column).toBe(column); + expect(domain.x).toBeCloseToArray(x, 3); + expect(domain.y).toBeCloseToArray(y, 3); + } + Plotly.newPlot(gd, [{ + type: 'pie', labels: ['a', 'b'], values: [1, 2] + }, { + type: 'scattergeo', lon: [10, 20], lat: [20, 10] + }], { + grid: {rows: 2, columns: 2, xgap: 1 / 3, ygap: 1 / 3} + }) + .then(function() { + // defaults to cell (0, 0) + // we're not smart enough to keep them from overlapping each other... should we try? + checkDomain(gd._fullData[0], 0, 0, [0, 0.4], [0.6, 1]); + checkDomain(gd._fullLayout.geo, 0, 0, [0, 0.4], [0.6, 1]); + + return Plotly.update(gd, {'domain.column': 1}, {'geo.domain.row': 1}, [0]); + }) + .then(function() { + // change row OR column, the other keeps its previous default + checkDomain(gd._fullData[0], 1, 0, [0.6, 1], [0.6, 1]); + checkDomain(gd._fullLayout.geo, 0, 1, [0, 0.4], [0, 0.4]); + }) + .catch(failTest) + .then(done); + }); });