diff --git a/.circleci/config.yml b/.circleci/config.yml index 5afd8fee571..9dc14f4d5f2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -97,6 +97,25 @@ jobs: - store_artifacts: path: build + test-image2: + docker: + - image: plotly/testbed:latest + working_directory: /var/www/streambed/image_server/plotly.js/ + steps: + - checkout + - attach_workspace: + at: /var/www/streambed/image_server/plotly.js/ + - run: + name: Run and setup container + command: | + supervisord & + npm run docker -- setup + - run: + name: Run image tests + command: ./.circleci/test.sh image2 + - store_artifacts: + path: build + test-syntax: docker: - image: circleci/node:8.9.4 @@ -123,6 +142,9 @@ workflows: - test-image: requires: - build + - test-image2: + requires: + - build - test-syntax: requires: - build diff --git a/.circleci/test.sh b/.circleci/test.sh index c2e82b26442..0a660879059 100755 --- a/.circleci/test.sh +++ b/.circleci/test.sh @@ -41,6 +41,10 @@ case $1 in image) npm run test-image || EXIT_STATE=$? + exit $EXIT_STATE + ;; + + image2) npm run test-export || EXIT_STATE=$? npm run test-image-gl2d || EXIT_STATE=$? exit $EXIT_STATE diff --git a/lib/index-gl2d.js b/lib/index-gl2d.js index bf378548a82..0b2d8358f21 100644 --- a/lib/index-gl2d.js +++ b/lib/index-gl2d.js @@ -12,6 +12,7 @@ var Plotly = require('./core'); Plotly.register([ require('./scattergl'), + require('./splom'), require('./pointcloud'), require('./heatmapgl'), require('./contourgl'), diff --git a/lib/index.js b/lib/index.js index 57e33be6c60..a2b7c9fdc61 100644 --- a/lib/index.js +++ b/lib/index.js @@ -31,6 +31,8 @@ Plotly.register([ require('./choropleth'), require('./scattergl'), + require('./splom'), + require('./pointcloud'), require('./heatmapgl'), require('./parcoords'), diff --git a/lib/splom.js b/lib/splom.js new file mode 100644 index 00000000000..04328ef414f --- /dev/null +++ b/lib/splom.js @@ -0,0 +1,11 @@ +/** +* 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'; + +module.exports = require('../src/traces/splom'); diff --git a/package-lock.json b/package-lock.json index 98883ad63ac..5a9f24d5d3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1714,6 +1714,14 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "color-alpha": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/color-alpha/-/color-alpha-1.0.2.tgz", + "integrity": "sha1-tDMtJAvCrOd72y0J9jlNkcupHjA=", + "requires": { + "color-parse": "1.3.5" + } + }, "color-convert": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", @@ -6959,8 +6967,7 @@ "left-pad": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.2.0.tgz", - "integrity": "sha1-0wpzxrggHY99jnlWupYWCHpo4O4=", - "dev": true + "integrity": "sha1-0wpzxrggHY99jnlWupYWCHpo4O4=" }, "lerp": { "version": "1.0.3", @@ -9161,8 +9168,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "permutation-parity": { "version": "1.0.0", @@ -9545,6 +9551,14 @@ } } }, + "raf": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", + "requires": { + "performance-now": "2.1.0" + } + }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", @@ -10055,9 +10069,9 @@ } }, "regl-line2d": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.0.1.tgz", - "integrity": "sha512-6hhuHQBMhWFde28/jKDTCSkc0ZdTpsJpKflGCFmUpBHo2DAOYQkNfC1ARe+eOO70FqoahkdIrB9OJKB8J8Nupg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.0.2.tgz", + "integrity": "sha512-lIFjleuKg/tqHUVuWnd4fe1bn3RulaapHDThEaqPYM5XgKTWenzVrLvl7x0SIVkO5loc/n89/RYifBQQqzDuLg==", "requires": { "array-bounds": "1.0.1", "array-normalize": "1.1.3", @@ -10074,9 +10088,9 @@ } }, "regl-scatter2d": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/regl-scatter2d/-/regl-scatter2d-3.0.0.tgz", - "integrity": "sha512-ylsZ/TZ4XB/qFncCTLLAFhXt503VE4m4/u699OwLMgncePenBl2K/9SvJ5GuNjuaJ4CY3Zj9q+zPGYChdDsDUw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/regl-scatter2d/-/regl-scatter2d-3.0.1.tgz", + "integrity": "sha512-7tqdia7+o8pILPwu9BpnzxM6m11qgTzcoS0n3Hs+cFXLl3vXFmbM3y69LTJzDsASzSyPSb8RO60/dVJ9oAuuwQ==", "requires": { "array-range": "1.0.1", "array-rearrange": "2.2.2", @@ -10095,6 +10109,42 @@ "update-diff": "1.1.0" } }, + "regl-splom": { + "version": "github:dy/regl-scattermatrix#c6d064ebc546ed71c48d8c5b7e66cfe0980ef77a", + "requires": { + "array-bounds": "1.0.1", + "array-range": "1.0.1", + "bubleify": "1.1.0", + "color-alpha": "1.0.2", + "defined": "1.0.0", + "flatten-vertex-data": "1.0.0", + "left-pad": "1.2.0", + "parse-rect": "1.2.0", + "pick-by-alias": "1.2.0", + "point-cluster": "1.0.2", + "raf": "3.4.0", + "regl-scatter2d": "3.0.1" + }, + "dependencies": { + "binary-search-bounds": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.4.tgz", + "integrity": "sha512-2hg5kgdKql5ClF2ErBcSx0U5bnl5hgS4v7wMnLFodyR47yMtj2w+UAZB+0CiqyHct2q543i7Bi4/aMIegorCCg==" + }, + "point-cluster": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/point-cluster/-/point-cluster-1.0.2.tgz", + "integrity": "sha512-pau5Py38SKgEJZ8pvD/bfXrz2TmQy6BEtMFZZSpjsQ2EmAe4CRO+HLhHw1gmgHVFaY/9KqhrfSeUPIsBOw8tDA==", + "requires": { + "array-bounds": "1.0.1", + "array-normalize": "1.1.3", + "binary-search-bounds": "2.0.4", + "clamp": "1.0.1", + "parse-rect": "1.2.0" + } + } + } + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", diff --git a/package.json b/package.json index 8afa622f458..dd932ca9847 100644 --- a/package.json +++ b/package.json @@ -99,8 +99,9 @@ "polybooljs": "^1.2.0", "regl": "^1.3.1", "regl-error2d": "^2.0.3", - "regl-line2d": "^3.0.1", - "regl-scatter2d": "^3.0.0", + "regl-line2d": "^3.0.2", + "regl-scatter2d": "^3.0.1", + "regl-splom": "^1.0.0", "right-now": "^1.0.0", "robust-orientation": "^1.1.3", "sane-topojson": "^2.0.0", diff --git a/src/components/fx/helpers.js b/src/components/fx/helpers.js index d966a6d4a57..2698a7e1761 100644 --- a/src/components/fx/helpers.js +++ b/src/components/fx/helpers.js @@ -11,10 +11,30 @@ var Lib = require('../../lib'); // look for either subplot or xaxis and yaxis attributes +// does not handle splom case exports.getSubplot = function getSubplot(trace) { return trace.subplot || (trace.xaxis + trace.yaxis) || trace.geo; }; +// is trace in given list of subplots? +// does handle splom case +exports.isTraceInSubplots = function isTraceInSubplot(trace, subplots) { + if(trace.type === 'splom') { + var xaxes = trace.xaxes || []; + var yaxes = trace.yaxes || []; + for(var i = 0; i < xaxes.length; i++) { + for(var j = 0; j < yaxes.length; j++) { + if(subplots.indexOf(xaxes[i] + yaxes[j]) !== -1) { + return true; + } + } + } + return false; + } + + return subplots.indexOf(exports.getSubplot(trace)) !== -1; +}; + // convenience functions for mapping all relevant axes exports.flat = function flat(subplots, v) { var out = new Array(subplots.length); diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index c7db8a13d7f..4215a388ac8 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -215,7 +215,7 @@ function _hover(gd, evt, subplot, noHoverEvent) { var hoverdistance = fullLayout.hoverdistance === -1 ? Infinity : fullLayout.hoverdistance; var spikedistance = fullLayout.spikedistance === -1 ? Infinity : fullLayout.spikedistance; - // hoverData: the set of candidate points we've found to highlight + // hoverData: the set of candidate points we've found to highlight var hoverData = [], // searchData: the data to search in. Mostly this is just a copy of @@ -265,7 +265,7 @@ function _hover(gd, evt, subplot, noHoverEvent) { for(curvenum = 0; curvenum < gd.calcdata.length; curvenum++) { cd = gd.calcdata[curvenum]; trace = cd[0].trace; - if(trace.hoverinfo !== 'skip' && subplots.indexOf(helpers.getSubplot(trace)) !== -1) { + if(trace.hoverinfo !== 'skip' && helpers.isTraceInSubplots(trace, subplots)) { searchData.push(cd); } } @@ -338,8 +338,15 @@ function _hover(gd, evt, subplot, noHoverEvent) { // the rest of this function from running and failing if(['carpet', 'contourcarpet'].indexOf(trace._module.name) !== -1) continue; - subplotId = helpers.getSubplot(trace); - subploti = subplots.indexOf(subplotId); + if(trace.type === 'splom') { + // splom traces do not generate overlay subplots, + // it is safe to assume here splom traces correspond to the 0th subplot + subploti = 0; + subplotId = subplots[subploti]; + } else { + subplotId = helpers.getSubplot(trace); + subploti = subplots.indexOf(subplotId); + } // within one trace mode can sometimes be overridden mode = hovermode; diff --git a/src/components/grid/index.js b/src/components/grid/index.js index 5def46b284e..f03653522a2 100644 --- a/src/components/grid/index.js +++ b/src/components/grid/index.js @@ -143,7 +143,7 @@ var gridAttrs = { values: ['bottom', 'bottom plot', 'top plot', 'top'], dflt: 'bottom plot', role: 'info', - editType: 'ticks', + editType: 'plot', description: [ 'Sets where the x axis labels and titles go. *bottom* means', 'the very bottom of the grid. *bottom plot* is the lowest plot', @@ -155,7 +155,7 @@ var gridAttrs = { values: ['left', 'left plot', 'right plot', 'right'], dflt: 'left plot', role: 'info', - editType: 'ticks', + editType: 'plot', 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', @@ -165,15 +165,30 @@ var gridAttrs = { editType: 'plot' }; +function getAxes(layout, grid, axLetter) { + var gridVal = grid[axLetter + 'axes']; + var splomVal = Object.keys((layout._splomAxes || {})[axLetter] || {}); + + if(Array.isArray(gridVal)) return gridVal; + if(splomVal.length) return splomVal; +} + // the shape of the grid - this needs to be done BEFORE supplyDataDefaults // so that non-subplot traces can place themselves in the grid function sizeDefaults(layoutIn, layoutOut) { - var gridIn = layoutIn.grid; - if(!gridIn) return; + var gridIn = layoutIn.grid || {}; + var xAxes = getAxes(layoutOut, gridIn, 'x'); + var yAxes = getAxes(layoutOut, gridIn, 'y'); + + if(!layoutIn.grid && !xAxes && !yAxes) 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 hasXaxes = Array.isArray(xAxes); + var hasYaxes = Array.isArray(yAxes); + var isSplomGenerated = ( + hasXaxes && xAxes !== gridIn.xaxes && + hasYaxes && yAxes !== gridIn.yaxes + ); var dfltRows, dfltColumns; @@ -182,8 +197,8 @@ function sizeDefaults(layoutIn, layoutOut) { dfltColumns = gridIn.subplots[0].length; } else { - if(hasYaxes) dfltRows = gridIn.yaxes.length; - if(hasXaxes) dfltColumns = gridIn.xaxes.length; + if(hasYaxes) dfltRows = yAxes.length; + if(hasXaxes) dfltColumns = xAxes.length; } var gridOut = layoutOut.grid = {}; @@ -206,17 +221,26 @@ function sizeDefaults(layoutIn, layoutOut) { var rowOrder = coerce('roworder'); var reversed = rowOrder === 'top to bottom'; + var dfltGapX = hasSubplotGrid ? 0.2 : 0.1; + var dfltGapY = hasSubplotGrid ? 0.3 : 0.1; + + var dfltSideX, dfltSideY; + if(isSplomGenerated) { + dfltSideX = 'bottom'; + dfltSideY = 'left'; + } + gridOut._domains = { - x: fillGridPositions('x', coerce, hasSubplotGrid ? 0.2 : 0.1, columns), - y: fillGridPositions('y', coerce, hasSubplotGrid ? 0.3 : 0.1, rows, reversed) + x: fillGridPositions('x', coerce, dfltGapX, dfltSideX, columns), + y: fillGridPositions('y', coerce, dfltGapY, dfltSideY, rows, reversed) }; } // coerce x or y sizing attributes and return an array of domains for this direction -function fillGridPositions(axLetter, coerce, dfltGap, len, reversed) { +function fillGridPositions(axLetter, coerce, dfltGap, dfltSide, len, reversed) { var dirGap = coerce(axLetter + 'gap', dfltGap); var domain = coerce('domain.' + axLetter); - coerce(axLetter + 'side'); + coerce(axLetter + 'side', dfltSide); var out = new Array(len); var start = domain[0]; @@ -236,7 +260,7 @@ function contentDefaults(layoutIn, layoutOut) { // make sure we got to the end of handleGridSizing if(!gridOut || !gridOut._domains) return; - var gridIn = layoutIn.grid; + var gridIn = layoutIn.grid || {}; var subplots = layoutOut._subplots; var hasSubplotGrid = gridOut._hasSubplotGrid; var rows = gridOut.rows; @@ -282,8 +306,10 @@ function contentDefaults(layoutIn, layoutOut) { } } else { - gridOut.xaxes = fillGridAxes(gridIn.xaxes, subplots.xaxis, columns, axisMap, 'x'); - gridOut.yaxes = fillGridAxes(gridIn.yaxes, subplots.yaxis, rows, axisMap, 'y'); + var xAxes = getAxes(layoutOut, gridIn, 'x'); + var yAxes = getAxes(layoutOut, gridIn, 'y'); + gridOut.xaxes = fillGridAxes(xAxes, subplots.xaxis, columns, axisMap, 'x'); + gridOut.yaxes = fillGridAxes(yAxes, subplots.yaxis, rows, axisMap, 'y'); } var anchors = gridOut._anchors = {}; diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 53673e7c173..cd7bdf4acef 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -42,7 +42,11 @@ function draw(gd) { // Remove previous shapes before drawing new in shapes in fullLayout.shapes fullLayout._shapeUpperLayer.selectAll('path').remove(); fullLayout._shapeLowerLayer.selectAll('path').remove(); - fullLayout._shapeSubplotLayers.selectAll('path').remove(); + + for(var k in fullLayout._plots) { + var shapelayer = fullLayout._plots[k].shapelayer; + if(shapelayer) shapelayer.selectAll('path').remove(); + } for(var i = 0; i < fullLayout.shapes.length; i++) { if(fullLayout.shapes[i].visible) { diff --git a/src/lib/clear_gl_canvases.js b/src/lib/clear_gl_canvases.js new file mode 100644 index 00000000000..8545339ab4c --- /dev/null +++ b/src/lib/clear_gl_canvases.js @@ -0,0 +1,26 @@ +/** +* 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'; + +/** + * Clear gl frame (if any). This is a common pattern as + * we usually set `preserveDrawingBuffer: true` during + * gl context creation (e.g. via `reglUtils.prepare`). + * + * @param {DOM node or object} gd : graph div object + */ +module.exports = function clearGlCanvases(gd) { + var fullLayout = gd._fullLayout; + + if(fullLayout._glcanvas && fullLayout._glcanvas.size()) { + fullLayout._glcanvas.each(function(d) { + if(d.regl) d.regl.clear({color: true, depth: true}); + }); + } +}; diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 4b9411d7b25..bb025e3f2c5 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -196,9 +196,10 @@ exports.valObjectMeta = { '\'geo\', \'geo2\', \'geo3\', ...' ].join(' '), requiredOpts: ['dflt'], - otherOpts: [], - coerceFunction: function(v, propOut, dflt) { - if(typeof v === 'string' && counterRegex(dflt).test(v)) { + otherOpts: ['regex'], + coerceFunction: function(v, propOut, dflt, opts) { + var regex = opts.regex || counterRegex(dflt); + if(typeof v === 'string' && regex.test(v)) { propOut.set(v); return; } diff --git a/src/lib/prepare_regl.js b/src/lib/prepare_regl.js new file mode 100644 index 00000000000..3c6ca297083 --- /dev/null +++ b/src/lib/prepare_regl.js @@ -0,0 +1,39 @@ +/** +* 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'; + +// Note that this module should be ONLY required into +// files corresponding to regl trace modules +// so that bundles with non-regl only don't include +// regl and all its bytes. +var createRegl = require('regl'); + +/** + * Idempotent version of createRegl. Create regl instances + * in the correct canvases with the correct attributes and + * options + * + * @param {DOM node or object} gd : graph div object + * @param {array} extensions : list of extension to pass to createRegl + */ +module.exports = function prepareRegl(gd, extensions) { + gd._fullLayout._glcanvas.each(function(d) { + if(d.regl) return; + + d.regl = createRegl({ + canvas: this, + attributes: { + antialias: !d.pick, + preserveDrawingBuffer: true + }, + pixelRatio: gd._context.plotGlPixelRatio || global.devicePixelRatio, + extensions: extensions || [] + }); + }); +}; diff --git a/src/lib/typed_array_truncate.js b/src/lib/typed_array_truncate.js deleted file mode 100644 index c43c17166d0..00000000000 --- a/src/lib/typed_array_truncate.js +++ /dev/null @@ -1,32 +0,0 @@ -/** -* 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'; - -function truncateFloat32(arrayIn, len) { - var arrayOut = new Float32Array(len); - for(var i = 0; i < len; i++) arrayOut[i] = arrayIn[i]; - return arrayOut; -} - -function truncateFloat64(arrayIn, len) { - var arrayOut = new Float64Array(len); - for(var i = 0; i < len; i++) arrayOut[i] = arrayIn[i]; - return arrayOut; -} - -/** - * Truncate a typed array to some length. - * For some reason, ES2015 Float32Array.prototype.slice takes - * 2x as long, therefore we aren't checking for its existence - */ -module.exports = function truncate(arrayIn, len) { - if(arrayIn instanceof Float32Array) return truncateFloat32(arrayIn, len); - if(arrayIn instanceof Float64Array) return truncateFloat64(arrayIn, len); - throw new Error('This array type is not yet supported by `truncate`.'); -}; diff --git a/src/plot_api/edit_types.js b/src/plot_api/edit_types.js index 24701a9a443..ea6defdc2c5 100644 --- a/src/plot_api/edit_types.js +++ b/src/plot_api/edit_types.js @@ -35,7 +35,7 @@ var layoutOpts = { valType: 'flaglist', extras: ['none'], flags: [ - 'calc', 'calcIfAutorange', 'plot', 'legend', 'ticks', 'margins', + 'calc', 'calcIfAutorange', 'plot', 'legend', 'ticks', 'axrange', 'margins', 'layoutstyle', 'modebar', 'camera', 'arraydraw' ], description: [ @@ -48,6 +48,7 @@ var layoutOpts = { '*legend* only redraws the legend.', '*ticks* only redraws axis ticks, labels, and gridlines.', '*margins* recomputes ticklabel automargins.', + '*axrange* minimal sequence when updating axis ranges.', '*layoutstyle* reapplies global and SVG cartesian axis styles.', '*modebar* just updates the modebar.', '*camera* just updates the camera settings for gl3d scenes.', diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index b6aed6ba7d0..9ea68b9f5e4 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -22,11 +22,11 @@ var Registry = require('../registry'); var PlotSchema = require('./plot_schema'); var Plots = require('../plots/plots'); var Polar = require('../plots/polar/legacy'); -var initInteractions = require('../plots/cartesian/graph_interact'); var Axes = require('../plots/cartesian/axes'); var Drawing = require('../components/drawing'); var Color = require('../components/color'); +var initInteractions = require('../plots/cartesian/graph_interact').initInteractions; var xmlnsNamespaces = require('../constants/xmlns_namespaces'); var svgTextUtils = require('../lib/svg_text_utils'); @@ -36,11 +36,7 @@ var helpers = require('./helpers'); var subroutines = require('./subroutines'); var editTypes = require('./edit_types'); -var cartesianConstants = require('../plots/cartesian/constants'); -var axisConstraints = require('../plots/cartesian/constraints'); -var enforceAxisConstraints = axisConstraints.enforce; -var cleanAxisConstraints = axisConstraints.clean; -var doAutoRange = require('../plots/cartesian/autorange').doAutoRange; +var AX_NAME_PATTERN = require('../plots/cartesian/constants').AX_NAME_PATTERN; var numericNameWarningCount = 0; var numericNameWarningCountLimit = 5; @@ -330,15 +326,7 @@ exports.plot = function(gd, data, layout, config) { function doAutoRangeAndConstraints() { if(gd._transitioning) return; - var axList = Axes.list(gd, '', true); - for(var i = 0; i < axList.length; i++) { - var ax = axList[i]; - cleanAxisConstraints(gd, ax); - - doAutoRange(ax); - } - - enforceAxisConstraints(gd); + subroutines.doAutoRangeAndConstraints(gd); // store initial ranges *after* enforcing constraints, otherwise // we will never look like we're at the initial ranges @@ -350,80 +338,6 @@ exports.plot = function(gd, data, layout, config) { return Axes.doTicks(gd, graphWasEmpty ? '' : 'redraw'); } - // Now plot the data - function drawData() { - var calcdata = gd.calcdata, - i, - rangesliderContainers = fullLayout._infolayer.selectAll('g.rangeslider-container'); - - // in case of traces that were heatmaps or contour maps - // previously, remove them and their colorbars explicitly - for(i = 0; i < calcdata.length; i++) { - var trace = calcdata[i][0].trace, - isVisible = (trace.visible === true), - uid = trace.uid; - - if(!isVisible || !Registry.traceIs(trace, '2dMap')) { - var query = ( - '.hm' + uid + - ',.contour' + uid + - ',#clip' + uid - ); - - fullLayout._paper - .selectAll(query) - .remove(); - - rangesliderContainers - .selectAll(query) - .remove(); - } - - if(!isVisible || !trace._module.colorbar) { - fullLayout._infolayer.selectAll('.cb' + uid).remove(); - } - } - - // loop over the base plot modules present on graph - var basePlotModules = fullLayout._basePlotModules; - for(i = 0; i < basePlotModules.length; i++) { - basePlotModules[i].plot(gd); - } - - // keep reference to shape layers in subplots - var layerSubplot = fullLayout._paper.selectAll('.layer-subplot'); - fullLayout._shapeSubplotLayers = layerSubplot.selectAll('.shapelayer'); - - // styling separate from drawing - Plots.style(gd); - - // show annotations and shapes - Registry.getComponentMethod('shapes', 'draw')(gd); - Registry.getComponentMethod('annotations', 'draw')(gd); - - // source links - Plots.addLinks(gd); - - // Mark the first render as complete - fullLayout._replotting = false; - - return Plots.previousPromises(gd); - } - - // An initial paint must be completed before these components can be - // correctly sized and the whole plot re-margined. fullLayout._replotting must - // be set to false before these will work properly. - function finalDraw() { - Registry.getComponentMethod('shapes', 'draw')(gd); - Registry.getComponentMethod('images', 'draw')(gd); - Registry.getComponentMethod('annotations', 'draw')(gd); - Registry.getComponentMethod('legend', 'draw')(gd); - Registry.getComponentMethod('rangeslider', 'draw')(gd); - Registry.getComponentMethod('rangeselector', 'draw')(gd); - Registry.getComponentMethod('sliders', 'draw')(gd); - Registry.getComponentMethod('updatemenus', 'draw')(gd); - } - var seq = [ Plots.previousPromises, addFrames, @@ -435,9 +349,10 @@ exports.plot = function(gd, data, layout, config) { seq.push(subroutines.layoutStyles); if(hasCartesian) seq.push(drawAxes); seq.push( - drawData, - finalDraw, + subroutines.drawData, + subroutines.finalDraw, initInteractions, + Plots.addLinks, Plots.rehover, Plots.previousPromises ); @@ -666,7 +581,7 @@ exports.newPlot = function(gd, data, layout, config) { gd = Lib.getGraphDiv(gd); // remove gl contexts - Plots.cleanPlot([], {}, gd._fullData || {}, gd._fullLayout || {}); + Plots.cleanPlot([], {}, gd._fullData || [], gd._fullLayout || {}, gd.calcdata || []); Plots.purge(gd); return exports.plot(gd, data, layout, config); @@ -1381,8 +1296,8 @@ exports.restyle = function restyle(gd, astr, val, _traces) { var traces = helpers.coerceTraceIndices(gd, _traces); - var specs = _restyle(gd, aobj, traces), - flags = specs.flags; + var specs = _restyle(gd, aobj, traces); + var flags = specs.flags; // clear calcdata and/or axis types if required so they get regenerated if(flags.clearCalc) gd.calcdata = undefined; @@ -1746,8 +1661,8 @@ exports.relayout = function relayout(gd, astr, val) { if(Object.keys(aobj).length) gd.changed = true; - var specs = _relayout(gd, aobj), - flags = specs.flags; + var specs = _relayout(gd, aobj); + var flags = specs.flags; // clear calcdata if required if(flags.calc) gd.calcdata = undefined; @@ -1768,6 +1683,30 @@ exports.relayout = function relayout(gd, astr, val) { if(flags.legend) seq.push(subroutines.doLegend); if(flags.layoutstyle) seq.push(subroutines.layoutStyles); + + if(flags.axrange) { + // N.B. leave as sequence of subroutines (for now) instead of + // subroutine of its own so that finalDraw always gets + // executed after drawData + seq.push( + // TODO + // no test fail when commenting out doAutoRangeAndConstraints, + // but I think we do need this (maybe just the enforce part?) + // Am I right? + // More info in: + // https://github.com/plotly/plotly.js/issues/2540 + subroutines.doAutoRangeAndConstraints, + // TODO + // can target specific axes, + // do not have to redraw all axes here + // See: + // https://github.com/plotly/plotly.js/issues/2547 + subroutines.doTicksRelayout, + subroutines.drawData, + subroutines.finalDraw + ); + } + if(flags.ticks) seq.push(subroutines.doTicksRelayout); if(flags.modebar) seq.push(subroutines.doModeBar); if(flags.camera) seq.push(subroutines.doCamera); @@ -1988,7 +1927,7 @@ function _relayout(gd, aobj) { } Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null); } - else if(pleaf.match(cartesianConstants.AX_NAME_PATTERN)) { + else if(pleaf.match(AX_NAME_PATTERN)) { var fullProp = Lib.nestedProperty(fullLayout, ai).get(), newType = (vi || {}).type; @@ -2041,8 +1980,9 @@ function _relayout(gd, aobj) { if(checkForAutorange && (refAutorange(gd, objToAutorange, 'x') || refAutorange(gd, objToAutorange, 'y'))) { flags.calc = true; } - else editTypes.update(flags, updateValObject); - + else { + editTypes.update(flags, updateValObject); + } // prepare the edits object we'll send to applyContainerArrayChanges if(!arrayEdits[arrayStr]) arrayEdits[arrayStr] = {}; @@ -2193,11 +2133,11 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { var traces = helpers.coerceTraceIndices(gd, _traces); - var restyleSpecs = _restyle(gd, Lib.extendFlat({}, traceUpdate), traces), - restyleFlags = restyleSpecs.flags; + var restyleSpecs = _restyle(gd, Lib.extendFlat({}, traceUpdate), traces); + var restyleFlags = restyleSpecs.flags; - var relayoutSpecs = _relayout(gd, Lib.extendFlat({}, layoutUpdate)), - relayoutFlags = relayoutSpecs.flags; + var relayoutSpecs = _relayout(gd, Lib.extendFlat({}, layoutUpdate)); + var relayoutFlags = relayoutSpecs.flags; // clear calcdata and/or axis types if required if(restyleFlags.clearCalc || relayoutFlags.calc) gd.calcdata = undefined; @@ -2232,6 +2172,14 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { if(restyleFlags.colorbars) seq.push(subroutines.doColorBars); if(relayoutFlags.legend) seq.push(subroutines.doLegend); if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); + if(relayoutFlags.axrange) { + seq.push( + subroutines.doAutoRangeAndConstraints, + subroutines.doTicksRelayout, + subroutines.drawData, + subroutines.finalDraw + ); + } if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); if(relayoutFlags.camera) seq.push(subroutines.doCamera); @@ -2384,6 +2332,14 @@ exports.react = function(gd, data, layout, config) { if(restyleFlags.colorbars) seq.push(subroutines.doColorBars); if(relayoutFlags.legend) seq.push(subroutines.doLegend); if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); + if(relayoutFlags.axrange) { + seq.push( + subroutines.doAutoRangeAndConstraints, + subroutines.doTicksRelayout, + subroutines.drawData, + subroutines.finalDraw + ); + } if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); if(relayoutFlags.camera) seq.push(subroutines.doCamera); @@ -3240,11 +3196,12 @@ exports.deleteFrames = function(gd, frameList) { exports.purge = function purge(gd) { gd = Lib.getGraphDiv(gd); - var fullLayout = gd._fullLayout || {}, - fullData = gd._fullData || []; + var fullLayout = gd._fullLayout || {}; + var fullData = gd._fullData || []; + var calcdata = gd.calcdata || []; // remove gl contexts - Plots.cleanPlot([], {}, fullData, fullLayout); + Plots.cleanPlot([], {}, fullData, fullLayout, calcdata); // purge properties Plots.purge(gd); diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 54994abf457..94638f8ff50 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -25,6 +25,7 @@ var editTypes = require('./edit_types'); var extendFlat = Lib.extendFlat; var extendDeepAll = Lib.extendDeepAll; +var isPlainObject = Lib.isPlainObject; var IS_SUBPLOT_OBJ = '_isSubplotObj'; var IS_LINKED_TO_ARRAY = '_isLinkedToArray'; @@ -140,7 +141,7 @@ exports.crawl = function(attrs, callback, specifiedLevel, attrString) { if(exports.isValObject(attr)) return; - if(Lib.isPlainObject(attr) && attrName !== 'impliedEdits') { + if(isPlainObject(attr) && attrName !== 'impliedEdits') { exports.crawl(attr, callback, level + 1, fullAttrString); } }); @@ -387,7 +388,7 @@ function recurseIntoValObject(valObject, parts, i) { // the innermost schema item we find. for(; i < parts.length; i++) { var newValObject = valObject[parts[i]]; - if(Lib.isPlainObject(newValObject)) valObject = newValObject; + if(isPlainObject(newValObject)) valObject = newValObject; else break; if(i === parts.length - 1) break; @@ -486,13 +487,12 @@ function getLayoutAttributes() { if(!_module.layoutAttributes) continue; - if(_module.name === 'cartesian') { - handleBasePlotModule(layoutAttributes, _module, 'xaxis'); - handleBasePlotModule(layoutAttributes, _module, 'yaxis'); - } - else { + if(Array.isArray(_module.attr)) { + for(var i = 0; i < _module.attr.length; i++) { + handleBasePlotModule(layoutAttributes, _module, _module.attr[i]); + } + } else { var astr = _module.attr === 'subplot' ? _module.name : _module.attr; - handleBasePlotModule(layoutAttributes, _module, astr); } } @@ -565,6 +565,7 @@ function getFramesAttributes() { function formatAttributes(attrs) { mergeValTypeAndRole(attrs); formatArrayContainers(attrs); + stringify(attrs); return attrs; } @@ -596,7 +597,7 @@ function mergeValTypeAndRole(attrs) { attrs[attrName + 'src'] = makeSrcAttr(attrName); } } - else if(Lib.isPlainObject(attr)) { + else if(isPlainObject(attr)) { // all attrs container objects get role 'object' attr.role = 'object'; } @@ -624,6 +625,29 @@ function formatArrayContainers(attrs) { exports.crawl(attrs, callback); } +// this can take around 10ms and should only be run from PlotSchema.get(), +// to ensure JSON.stringify(PlotSchema.get()) gives the intended result. +function stringify(attrs) { + function walk(attr) { + for(var k in attr) { + if(isPlainObject(attr[k])) { + walk(attr[k]); + } else if(Array.isArray(attr[k])) { + for(var i = 0; i < attr[k].length; i++) { + walk(attr[k][i]); + } + } else { + // as JSON.stringify(/test/) // => {} + if(attr[k] instanceof RegExp) { + attr[k] = attr[k].toString(); + } + } + } + } + + walk(attrs); +} + function assignPolarLayoutAttrs(layoutAttributes) { extendFlat(layoutAttributes, { radialaxis: polarAxisAttrs.radialaxis, diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index fa74da9e967..dfd76a1021f 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -11,16 +11,22 @@ var d3 = require('d3'); var Registry = require('../registry'); var Plots = require('../plots/plots'); + var Lib = require('../lib'); +var clearGlCanvases = require('../lib/clear_gl_canvases'); var Color = require('../components/color'); var Drawing = require('../components/drawing'); var Titles = require('../components/titles'); var ModeBar = require('../components/modebar'); + var Axes = require('../plots/cartesian/axes'); -var initInteractions = require('../plots/cartesian/graph_interact'); var cartesianConstants = require('../plots/cartesian/constants'); var alignmentConstants = require('../constants/alignment'); +var axisConstraints = require('../plots/cartesian/constraints'); +var enforceAxisConstraints = axisConstraints.enforce; +var cleanAxisConstraints = axisConstraints.clean; +var doAutoRange = require('../plots/cartesian/autorange').doAutoRange; exports.layoutStyles = function(gd) { return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd); @@ -173,7 +179,7 @@ exports.lsInner = function(gd) { .append('rect'); }); - plotClip.select('rect').attr({ + plotinfo.clipRect = plotClip.select('rect').attr({ width: xa._length, height: ya._length }); @@ -451,6 +457,12 @@ exports.doLegend = function(gd) { exports.doTicksRelayout = function(gd) { Axes.doTicks(gd, 'redraw'); + + if(gd._fullLayout._hasOnlyLargeSploms) { + clearGlCanvases(gd); + Registry.subplotsRegistry.splom.plot(gd); + } + exports.drawMainTitle(gd); return Plots.previousPromises(gd); }; @@ -459,7 +471,6 @@ exports.doModeBar = function(gd) { var fullLayout = gd._fullLayout; ModeBar.manage(gd); - initInteractions(gd); for(var i = 0; i < fullLayout._basePlotModules.length; i++) { var updateFx = fullLayout._basePlotModules[i].updateFx; @@ -480,3 +491,84 @@ exports.doCamera = function(gd) { scene.setCamera(sceneLayout.camera); } }; + +exports.drawData = function(gd) { + var fullLayout = gd._fullLayout; + var calcdata = gd.calcdata; + var rangesliderContainers = fullLayout._infolayer.selectAll('g.rangeslider-container'); + var i; + + // in case of traces that were heatmaps or contour maps + // previously, remove them and their colorbars explicitly + for(i = 0; i < calcdata.length; i++) { + var trace = calcdata[i][0].trace; + var isVisible = (trace.visible === true); + var uid = trace.uid; + + if(!isVisible || !Registry.traceIs(trace, '2dMap')) { + var query = ( + '.hm' + uid + + ',.contour' + uid + + ',#clip' + uid + ); + + fullLayout._paper + .selectAll(query) + .remove(); + + rangesliderContainers + .selectAll(query) + .remove(); + } + + if(!isVisible || !trace._module.colorbar) { + fullLayout._infolayer.selectAll('.cb' + uid).remove(); + } + } + + clearGlCanvases(gd); + + // loop over the base plot modules present on graph + var basePlotModules = fullLayout._basePlotModules; + for(i = 0; i < basePlotModules.length; i++) { + basePlotModules[i].plot(gd); + } + + // styling separate from drawing + Plots.style(gd); + + // show annotations and shapes + Registry.getComponentMethod('shapes', 'draw')(gd); + Registry.getComponentMethod('annotations', 'draw')(gd); + + // Mark the first render as complete + fullLayout._replotting = false; + + return Plots.previousPromises(gd); +}; + +exports.doAutoRangeAndConstraints = function(gd) { + var axList = Axes.list(gd, '', true); + + for(var i = 0; i < axList.length; i++) { + var ax = axList[i]; + cleanAxisConstraints(gd, ax); + doAutoRange(ax); + } + + enforceAxisConstraints(gd); +}; + +// An initial paint must be completed before these components can be +// correctly sized and the whole plot re-margined. fullLayout._replotting must +// be set to false before these will work properly. +exports.finalDraw = function(gd) { + Registry.getComponentMethod('shapes', 'draw')(gd); + Registry.getComponentMethod('images', 'draw')(gd); + Registry.getComponentMethod('annotations', 'draw')(gd); + Registry.getComponentMethod('legend', 'draw')(gd); + Registry.getComponentMethod('rangeslider', 'draw')(gd); + Registry.getComponentMethod('rangeselector', 'draw')(gd); + Registry.getComponentMethod('sliders', 'draw')(gd); + Registry.getComponentMethod('updatemenus', 'draw')(gd); +}; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index c44db0c9856..c44dd3db8f9 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1502,9 +1502,9 @@ axes.makeClipPaths = function(gd) { // ax._rl (stored linearized range for use by zoom/pan) // or can pass in an axis object directly axes.doTicks = function(gd, axid, skipTitle) { - var fullLayout = gd._fullLayout, - ax, - independent = false; + var fullLayout = gd._fullLayout; + var ax; + var independent = false; // allow passing an independent axis object instead of id if(typeof axid === 'object') { @@ -1517,18 +1517,14 @@ axes.doTicks = function(gd, axid, skipTitle) { if(axid === 'redraw') { fullLayout._paper.selectAll('g.subplot').each(function(subplot) { - var plotinfo = fullLayout._plots[subplot], - xa = plotinfo.xaxis, - ya = plotinfo.yaxis; - - plotinfo.xaxislayer - .selectAll('.' + xa._id + 'tick').remove(); - plotinfo.yaxislayer - .selectAll('.' + ya._id + 'tick').remove(); - plotinfo.gridlayer - .selectAll('path').remove(); - plotinfo.zerolinelayer - .selectAll('path').remove(); + var plotinfo = fullLayout._plots[subplot]; + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; + + plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick').remove(); + plotinfo.yaxislayer.selectAll('.' + ya._id + 'tick').remove(); + if(plotinfo.gridlayer) plotinfo.gridlayer.selectAll('path').remove(); + if(plotinfo.zerolinelayer) plotinfo.zerolinelayer.selectAll('path').remove(); fullLayout._infolayer.select('.g-' + xa._id + 'title').remove(); fullLayout._infolayer.select('.g-' + ya._id + 'title').remove(); }); @@ -1552,7 +1548,7 @@ axes.doTicks = function(gd, axid, skipTitle) { var axLetter = axid.charAt(0); var counterLetter = axes.counterLetter(axid); - var vals = axes.calcTicks(ax); + var vals = ax._vals = axes.calcTicks(ax); var datafn = function(d) { return [d.text, d.x, ax.mirror, d.font, d.fontSize, d.fontColor].join('_'); }; var tcls = axid + 'tick'; var gcls = axid + 'grid'; @@ -2092,6 +2088,8 @@ axes.doTicks = function(gd, axid, skipTitle) { } function drawGrid(plotinfo, counteraxis, subplot) { + if(fullLayout._hasOnlyLargeSploms) return; + var gridcontainer = plotinfo.gridlayer.selectAll('.' + axid); var zlcontainer = plotinfo.zerolinelayer; var gridvals = plotinfo['hidegrid' + axLetter] ? [] : valsClipped; @@ -2190,7 +2188,7 @@ axes.doTicks = function(gd, axid, skipTitle) { } drawTicks(mainPlotinfo[axLetter + 'axislayer'], tickpath); - tickSubplots = Object.keys(ax._linepositions); + tickSubplots = Object.keys(ax._linepositions || {}); } tickSubplots.map(function(subplot) { diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index cf34c9d4b21..220e5eca798 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -33,6 +33,7 @@ var setConvert = require('./set_convert'); */ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, options, layoutOut) { var letter = options.letter; + var id = containerOut._id; var font = options.font || {}; var visible = coerce('visible', !options.cheateronly); @@ -69,8 +70,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, // if axis.color was provided, use it for fonts too; otherwise, // inherit from global font color in case that was provided. var dfltFontColor = (dfltColor === containerIn.color) ? dfltColor : font.color; + // try to get default title from splom trace, fallback to graph-wide value + var dfltTitle = ((layoutOut._splomAxes || {})[letter] || {})[id] || layoutOut._dfltTitle[letter]; - coerce('title', layoutOut._dfltTitle[letter]); + coerce('title', dfltTitle); Lib.coerceFont(coerce, 'titlefont', { family: font.family, size: Math.round(font.size * 1.2), diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 2153c8de1df..010c35fd57a 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -16,6 +16,7 @@ var supportsPassive = require('has-passive-events'); var Registry = require('../../registry'); var Lib = require('../../lib'); var svgTextUtils = require('../../lib/svg_text_utils'); +var clearGlCanvases = require('../../lib/clear_gl_canvases'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var Fx = require('../../components/fx'); @@ -27,7 +28,8 @@ var Plots = require('../plots'); var doTicks = require('./axes').doTicks; var getFromId = require('./axis_ids').getFromId; -var prepSelect = require('./select'); +var prepSelect = require('./select').prepSelect; +var clearSelect = require('./select').clearSelect; var scaleZoom = require('./scale_zoom'); var constants = require('./constants'); @@ -54,66 +56,74 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // within DBLCLICKDELAY so we can check for click or doubleclick events // dragged stores whether a drag has occurred, so we don't have to // redraw unnecessarily, ie if no move bigger than MINDRAG or MINZOOM px - var fullLayout = gd._fullLayout; var zoomlayer = gd._fullLayout._zoomlayer; var isMainDrag = (ns + ew === 'nsew'); var singleEnd = (ns + ew).length === 1; - var subplots, xa, ya, xs, ys, pw, ph, xActive, yActive, cursor, - isSubplotConstrained, xaLinked, yaLinked; + // main subplot x and y (i.e. found in plotinfo - the main ones) + var xa0, ya0; + // {ax._id: ax} hash objects + var xaHash, yaHash; + // xaHash/yaHash values (arrays) + var xaxes, yaxes; + // main axis offsets + var xs, ys; + // main axis lengths + var pw, ph; + // contains keys 'xaHash', 'yaHash', 'xaxes', and 'yaxes' + // which are the x/y {ax._id: ax} hash objects and their values + // for linked axis relative to this subplot + var links; + // set to ew/ns val when active, set to '' when inactive + var xActive, yActive; + // are all axes in this subplot are fixed? + var allFixedRanges; + // is subplot constrained? + var isSubplotConstrained; + // do we need to edit x/y ranges? + var editX, editY; function recomputeAxisLists() { - xa = [plotinfo.xaxis]; - ya = [plotinfo.yaxis]; - var xa0 = xa[0]; - var ya0 = ya[0]; + xa0 = plotinfo.xaxis; + ya0 = plotinfo.yaxis; pw = xa0._length; ph = ya0._length; + xs = xa0._offset; + ys = ya0._offset; - var constraintGroups = fullLayout._axisConstraintGroups; - var xIDs = [xa0._id]; - var yIDs = [ya0._id]; + xaHash = {}; + xaHash[xa0._id] = xa0; + yaHash = {}; + yaHash[ya0._id] = ya0; // if we're dragging two axes at once, also drag overlays - subplots = [plotinfo].concat((ns && ew) ? plotinfo.overlays : []); - - for(var i = 1; i < subplots.length; i++) { - var subplotXa = subplots[i].xaxis, - subplotYa = subplots[i].yaxis; - - if(xa.indexOf(subplotXa) === -1) { - xa.push(subplotXa); - xIDs.push(subplotXa._id); - } - - if(ya.indexOf(subplotYa) === -1) { - ya.push(subplotYa); - yIDs.push(subplotYa._id); + if(ns && ew) { + var overlays = plotinfo.overlays; + for(var i = 0; i < overlays.length; i++) { + var xa = overlays[i].xaxis; + xaHash[xa._id] = xa; + var ya = overlays[i].yaxis; + yaHash[ya._id] = ya; } } - xActive = isDirectionActive(xa, ew); - yActive = isDirectionActive(ya, ns); - cursor = getDragCursor(yActive + xActive, fullLayout.dragmode); - xs = xa0._offset; - ys = ya0._offset; - - var links = calcLinks(constraintGroups, xIDs, yIDs); - isSubplotConstrained = links.xy; + xaxes = hashValues(xaHash); + yaxes = hashValues(yaHash); + xActive = isDirectionActive(xaxes, ew); + yActive = isDirectionActive(yaxes, ns); + allFixedRanges = !yActive && !xActive; - // finally make the list of axis objects to link - xaLinked = []; - for(var xLinkID in links.x) { xaLinked.push(getFromId(gd, xLinkID)); } - yaLinked = []; - for(var yLinkID in links.y) { yaLinked.push(getFromId(gd, yLinkID)); } + links = calcLinks(gd, xaHash, yaHash); + isSubplotConstrained = links.isSubplotConstrained; + editX = ew || isSubplotConstrained; + editY = ns || isSubplotConstrained; } recomputeAxisLists(); + var cursor = getDragCursor(yActive + xActive, gd._fullLayout.dragmode, isMainDrag); var dragger = makeRectDragger(plotinfo, ns + ew + 'drag', cursor, x, y, w, h); - var allFixedRanges = !yActive && !xActive; - // still need to make the element if the axes are disabled // but nuke its events (except for maindrag which needs them for hover) // and stop there @@ -130,6 +140,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { prepFn: function(e, startX, startY) { var dragModeNow = gd._fullLayout.dragmode; + recomputeAxisLists(); + if(!allFixedRanges) { if(isMainDrag) { // main dragger handles all drag modes, and changes @@ -150,8 +162,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { else dragOptions.minDrag = undefined; if(isSelectOrLasso(dragModeNow)) { - dragOptions.xaxes = xa; - dragOptions.yaxes = ya; + dragOptions.xaxes = xaxes; + dragOptions.yaxes = yaxes; prepSelect(e, startX, startY, dragOptions, dragModeNow); } else if(allFixedRanges) { @@ -183,7 +195,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { Fx.click(gd, evt, plotinfo.id); } else if(numClicks === 1 && singleEnd) { - var ax = ns ? ya[0] : xa[0], + var ax = ns ? ya0 : xa0, end = (ns === 's' || ew === 'w') ? 0 : 1, attrStr = ax._name + '.range[' + end + ']', initialText = getEndText(ax, end), @@ -203,7 +215,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { .call(svgTextUtils.makeEditable, { gd: gd, immediate: true, - background: fullLayout.paper_bgcolor, + background: gd._fullLayout.paper_bgcolor, text: String(initialText), fill: ax.tickfont ? ax.tickfont.color : '#444', horizontalAlign: hAlign, @@ -333,8 +345,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } // TODO: edit linked axes in zoomAxRanges and in dragTail - if(zoomMode === 'xy' || zoomMode === 'x') zoomAxRanges(xa, box.l / pw, box.r / pw, updates, xaLinked); - if(zoomMode === 'xy' || zoomMode === 'y') zoomAxRanges(ya, (ph - box.b) / ph, (ph - box.t) / ph, updates, yaLinked); + if(zoomMode === 'xy' || zoomMode === 'x') { + zoomAxRanges(xaxes, box.l / pw, box.r / pw, updates, links.xaxes); + } + if(zoomMode === 'xy' || zoomMode === 'y') { + zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes); + } removeZoombox(gd); dragTail(); @@ -346,14 +362,13 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // wait a little after scrolling before redrawing var redrawTimer = null; var REDRAWDELAY = constants.REDRAWDELAY; - var mainplot = plotinfo.mainplot ? - fullLayout._plots[plotinfo.mainplot] : plotinfo; + var mainplot = plotinfo.mainplot ? gd._fullLayout._plots[plotinfo.mainplot] : plotinfo; function zoomWheel(e) { // deactivate mousewheel scrolling on embedded graphs // devs can override this with layout._enablescrollzoom, // but _ ensures this setting won't leave their page - if(!gd._context.scrollZoom && !fullLayout._enablescrollzoom) { + if(!gd._context.scrollZoom && !gd._fullLayout._enablescrollzoom) { return; } @@ -404,20 +419,24 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { ax.range = axRange.map(doZoom); } - if(ew || isSubplotConstrained) { + if(editX) { // if we're only zooming this axis because of constraints, // zoom it about the center if(!ew) xfrac = 0.5; - for(i = 0; i < xa.length; i++) zoomWheelOneAxis(xa[i], xfrac, zoom); + for(i = 0; i < xaxes.length; i++) { + zoomWheelOneAxis(xaxes[i], xfrac, zoom); + } scrollViewBox[2] *= zoom; scrollViewBox[0] += scrollViewBox[2] * xfrac * (1 / zoom - 1); } - if(ns || isSubplotConstrained) { + if(editY) { if(!ns) yfrac = 0.5; - for(i = 0; i < ya.length; i++) zoomWheelOneAxis(ya[i], yfrac, zoom); + for(i = 0; i < yaxes.length; i++) { + zoomWheelOneAxis(yaxes[i], yfrac, zoom); + } scrollViewBox[3] *= zoom; scrollViewBox[1] += scrollViewBox[3] * (1 - yfrac) * (1 / zoom - 1); @@ -455,11 +474,9 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { return; } - recomputeAxisLists(); - if(xActive === 'ew' || yActive === 'ns') { - if(xActive) dragAxList(xa, dx); - if(yActive) dragAxList(ya, dy); + if(xActive) dragAxList(xaxes, dx); + if(yActive) dragAxList(yaxes, dy); updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]); ticksAndAnnotations(yActive, xActive); return; @@ -499,12 +516,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { dy = dxySign * dxyFraction * ph; } - if(xActive === 'w') dx = dz(xa, 0, dx); - else if(xActive === 'e') dx = dz(xa, 1, -dx); + if(xActive === 'w') dx = dz(xaxes, 0, dx); + else if(xActive === 'e') dx = dz(xaxes, 1, -dx); else if(!xActive) dx = 0; - if(yActive === 'n') dy = dz(ya, 1, dy); - else if(yActive === 's') dy = dz(ya, 0, -dy); + if(yActive === 'n') dy = dz(yaxes, 1, dy); + else if(yActive === 's') dy = dz(yaxes, 0, -dy); else if(!yActive) dy = 0; var x0 = (xActive === 'w') ? dx : 0; @@ -515,17 +532,17 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(!xActive && yActive.length === 1) { // dragging one end of the y axis of a constrained subplot // scale the other axis the same about its middle - for(i = 0; i < xa.length; i++) { - xa[i].range = xa[i]._r.slice(); - scaleZoom(xa[i], 1 - dy / ph); + for(i = 0; i < xaxes.length; i++) { + xaxes[i].range = xaxes[i]._r.slice(); + scaleZoom(xaxes[i], 1 - dy / ph); } dx = dy * pw / ph; x0 = dx / 2; } if(!yActive && xActive.length === 1) { - for(i = 0; i < ya.length; i++) { - ya[i].range = ya[i]._r.slice(); - scaleZoom(ya[i], 1 - dx / pw); + for(i = 0; i < yaxes.length; i++) { + yaxes[i].range = yaxes[i]._r.slice(); + scaleZoom(yaxes[i], 1 - dx / pw); } dy = dx * ph / pw; y0 = dy / 2; @@ -533,7 +550,6 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } updateSubplots([x0, y0, pw - dx, ph - dy]); - ticksAndAnnotations(yActive, xActive); } @@ -549,13 +565,13 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } } - if(ew || isSubplotConstrained) { - pushActiveAxIds(xa); - pushActiveAxIds(xaLinked); + if(editX) { + pushActiveAxIds(xaxes); + pushActiveAxIds(links.xaxes); } - if(ns || isSubplotConstrained) { - pushActiveAxIds(ya); - pushActiveAxIds(yaLinked); + if(editY) { + pushActiveAxIds(yaxes); + pushActiveAxIds(links.yaxes); } updates = {}; @@ -583,16 +599,16 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // annotations and shapes 'draw' method is slow, // use the finer-grained 'drawOne' method instead - redrawObjs(fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne')); - redrawObjs(fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne')); - redrawObjs(fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true); + redrawObjs(gd._fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne')); + redrawObjs(gd._fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne')); + redrawObjs(gd._fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true); } function doubleClick() { if(gd._transitioningWithDuration) return; var doubleClickConfig = gd._context.doubleClick, - axList = (xActive ? xa : []).concat(yActive ? ya : []), + axList = (xActive ? xaxes : []).concat(yActive ? yaxes : []), attrs = {}; var ax, i, rangeInitial; @@ -631,12 +647,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { else if(doubleClickConfig === 'reset') { // when we're resetting, reset all linked axes too, so we get back // to the fully-auto-with-constraints situation - if(xActive || isSubplotConstrained) axList = axList.concat(xaLinked); - if(yActive && !isSubplotConstrained) axList = axList.concat(yaLinked); + if(xActive || isSubplotConstrained) axList = axList.concat(links.xaxes); + if(yActive && !isSubplotConstrained) axList = axList.concat(links.yaxes); if(isSubplotConstrained) { - if(!xActive) axList = axList.concat(xa); - else if(!yActive) axList = axList.concat(ya); + if(!xActive) axList = axList.concat(xaxes); + else if(!yActive) axList = axList.concat(yaxes); } for(i = 0; i < axList.length; i++) { @@ -674,128 +690,154 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // updateSubplots - find all plot viewboxes that should be // affected by this drag, and update them. look for all plots - // sharing an affected axis (including the one being dragged) + // sharing an affected axis (including the one being dragged), + // includes also scattergl and splom logic. function updateSubplots(viewBox) { + var fullLayout = gd._fullLayout; var plotinfos = fullLayout._plots; - var subplots = Object.keys(plotinfos); - var xScaleFactor = viewBox[2] / xa[0]._length; - var yScaleFactor = viewBox[3] / ya[0]._length; - var editX = ew || isSubplotConstrained; - var editY = ns || isSubplotConstrained; - - var i, xScaleFactor2, yScaleFactor2, clipDx, clipDy; - - // Find the appropriate scaling for this axis, if it's linked to the - // dragged axes by constraints. 0 is special, it means this axis shouldn't - // ever be scaled (will be converted to 1 if the other axis is scaled) - function getLinkedScaleFactor(ax) { - if(ax.fixedrange) return 0; - - if(editX && xaLinked.indexOf(ax) !== -1) { - return xScaleFactor; - } - if(editY && (isSubplotConstrained ? xaLinked : yaLinked).indexOf(ax) !== -1) { - return yScaleFactor; - } - return 0; - } + var subplots = fullLayout._subplots.cartesian; - function scaleAndGetShift(ax, scaleFactor) { - if(scaleFactor) { - ax.range = ax._r.slice(); - scaleZoom(ax, scaleFactor); - return getShift(ax, scaleFactor); - } - return 0; + // TODO can we move these to outer scope? + var hasScatterGl = fullLayout._has('scattergl'); + var hasOnlyLargeSploms = fullLayout._hasOnlyLargeSploms; + var hasSplom = hasOnlyLargeSploms || fullLayout._has('splom'); + var hasSVG = fullLayout._has('svg'); + var hasDraggedPts = fullLayout._has('draggedPts'); + + var i, sp, xa, ya; + + if(hasSplom || hasScatterGl) { + clearGlCanvases(gd); } - function getShift(ax, scaleFactor) { - return ax._length * (1 - scaleFactor) * FROM_TL[ax.constraintoward || 'middle']; + if(hasSplom) { + Registry.subplotsRegistry.splom.drag(gd); + if(hasOnlyLargeSploms) return; } - // clear gl frame, if any, since we preserve drawing buffer - // FIXME: code duplication with cartesian.plot - if(fullLayout._glcanvas && fullLayout._glcanvas.size()) { - fullLayout._glcanvas.each(function(d) { - if(d.regl) { - d.regl.clear({ - color: true - }); + if(hasScatterGl) { + // loop over all subplots (w/o exceptions) here, + // as we cleared the gl canvases above + for(i = 0; i < subplots.length; i++) { + sp = plotinfos[subplots[i]]; + xa = sp.xaxis; + ya = sp.yaxis; + + var scene = sp._scene; + if(scene) { + // FIXME: possibly we could update axis internal _r and _rl here + var xrng = Lib.simpleMap(xa.range, xa.r2l); + var yrng = Lib.simpleMap(ya.range, ya.r2l); + scene.update({range: [xrng[0], yrng[0], xrng[1], yrng[1]]}); } - }); + } } - for(i = 0; i < subplots.length; i++) { - var subplot = plotinfos[subplots[i]], - xa2 = subplot.xaxis, - ya2 = subplot.yaxis, - editX2 = editX && !xa2.fixedrange && (xa.indexOf(xa2) !== -1), - editY2 = editY && !ya2.fixedrange && (ya.indexOf(ya2) !== -1); - - // scattergl translate - if(subplot._scene && subplot._scene.update) { - // FIXME: possibly we could update axis internal _r and _rl here - var xaRange = Lib.simpleMap(xa2.range, xa2.r2l), - yaRange = Lib.simpleMap(ya2.range, ya2.r2l); - subplot._scene.update( - {range: [xaRange[0], yaRange[0], xaRange[1], yaRange[1]]} - ); - } + if(hasSVG) { + var xScaleFactor = viewBox[2] / xa0._length; + var yScaleFactor = viewBox[3] / ya0._length; - if(editX2) { - xScaleFactor2 = xScaleFactor; - clipDx = ew ? viewBox[0] : getShift(xa2, xScaleFactor2); - } - else { - xScaleFactor2 = getLinkedScaleFactor(xa2); - clipDx = scaleAndGetShift(xa2, xScaleFactor2); - } + for(i = 0; i < subplots.length; i++) { + sp = plotinfos[subplots[i]]; + xa = sp.xaxis; + ya = sp.yaxis; - if(editY2) { - yScaleFactor2 = yScaleFactor; - clipDy = ns ? viewBox[1] : getShift(ya2, yScaleFactor2); - } - else { - yScaleFactor2 = getLinkedScaleFactor(ya2); - clipDy = scaleAndGetShift(ya2, yScaleFactor2); - } + var editX2 = editX && !xa.fixedrange && xaHash[xa._id]; + var editY2 = editY && !ya.fixedrange && yaHash[ya._id]; - // don't scale at all if neither axis is scalable here - if(!xScaleFactor2 && !yScaleFactor2) { - continue; - } + var xScaleFactor2, yScaleFactor2; + var clipDx, clipDy; - // but if only one is, reset the other axis scaling - if(!xScaleFactor2) xScaleFactor2 = 1; - if(!yScaleFactor2) yScaleFactor2 = 1; + if(editX2) { + xScaleFactor2 = xScaleFactor; + clipDx = ew ? viewBox[0] : getShift(xa, xScaleFactor2); + } else { + xScaleFactor2 = getLinkedScaleFactor(xa, xScaleFactor, yScaleFactor); + clipDx = scaleAndGetShift(xa, xScaleFactor2); + } - var plotDx = xa2._offset - clipDx / xScaleFactor2, - plotDy = ya2._offset - clipDy / yScaleFactor2; + if(editY2) { + yScaleFactor2 = yScaleFactor; + clipDy = ns ? viewBox[1] : getShift(ya, yScaleFactor2); + } else { + yScaleFactor2 = getLinkedScaleFactor(ya, xScaleFactor, yScaleFactor); + clipDy = scaleAndGetShift(ya, yScaleFactor2); + } - fullLayout._defs.select('#' + subplot.clipId + '> rect') - .call(Drawing.setTranslate, clipDx, clipDy) - .call(Drawing.setScale, xScaleFactor2, yScaleFactor2); + // don't scale at all if neither axis is scalable here + if(!xScaleFactor2 && !yScaleFactor2) { + continue; + } - var traceGroups = subplot.plot - .selectAll('.scatterlayer .trace, .boxlayer .trace, .violinlayer .trace'); + // but if only one is, reset the other axis scaling + if(!xScaleFactor2) xScaleFactor2 = 1; + if(!yScaleFactor2) yScaleFactor2 = 1; + + var plotDx = xa._offset - clipDx / xScaleFactor2; + var plotDy = ya._offset - clipDy / yScaleFactor2; + + // TODO could be more efficient here: + // setTranslate and setScale do a lot of extra work + // when working independently, should perhaps combine + // them into a single routine. + sp.clipRect + .call(Drawing.setTranslate, clipDx, clipDy) + .call(Drawing.setScale, xScaleFactor2, yScaleFactor2); + + sp.plot + .call(Drawing.setTranslate, plotDx, plotDy) + .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2); + + // TODO move these selectAll calls out of here + // and stash them somewhere nice, see: + // https://github.com/plotly/plotly.js/issues/2548 + if(hasDraggedPts) { + var traceGroups = sp.plot + .selectAll('.scatterlayer .trace, .boxlayer .trace, .violinlayer .trace'); + + // This is specifically directed at marker points in scatter, box and violin traces, + // applying an inverse scale to individual points to counteract + // the scale of the trace as a whole: + traceGroups.selectAll('.point') + .call(Drawing.setPointGroupScale, xScaleFactor2, yScaleFactor2); + traceGroups.selectAll('.textpoint') + .call(Drawing.setTextPointsScale, xScaleFactor2, yScaleFactor2); + traceGroups + .call(Drawing.hideOutsideRangePoints, sp); + + sp.plot.selectAll('.barlayer .trace') + .call(Drawing.hideOutsideRangePoints, sp, '.bartext'); + } + } + } + } - subplot.plot - .call(Drawing.setTranslate, plotDx, plotDy) - .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2); + // Find the appropriate scaling for this axis, if it's linked to the + // dragged axes by constraints. 0 is special, it means this axis shouldn't + // ever be scaled (will be converted to 1 if the other axis is scaled) + function getLinkedScaleFactor(ax, xScaleFactor, yScaleFactor) { + if(ax.fixedrange) return 0; - // This is specifically directed at marker points in scatter, box and violin traces, - // applying an inverse scale to individual points to counteract - // the scale of the trace as a whole: - traceGroups.selectAll('.point') - .call(Drawing.setPointGroupScale, xScaleFactor2, yScaleFactor2); - traceGroups.selectAll('.textpoint') - .call(Drawing.setTextPointsScale, xScaleFactor2, yScaleFactor2); - traceGroups - .call(Drawing.hideOutsideRangePoints, subplot); + if(editX && links.xaHash[ax._id]) { + return xScaleFactor; + } + if(editY && (isSubplotConstrained ? links.xaHash : links.yaHash)[ax._id]) { + return yScaleFactor; + } + return 0; + } - subplot.plot.selectAll('.barlayer .trace') - .call(Drawing.hideOutsideRangePoints, subplot, '.bartext'); + function scaleAndGetShift(ax, scaleFactor) { + if(scaleFactor) { + ax.range = ax._r.slice(); + scaleZoom(ax, scaleFactor); + return getShift(ax, scaleFactor); } + return 0; + } + + function getShift(ax, scaleFactor) { + return ax._length * (1 - scaleFactor) * FROM_TL[ax.constraintoward || 'middle']; } return dragger; @@ -897,9 +939,12 @@ function dZoom(d) { 1 / (1 / Math.max(d, -0.3) + 3.222)); } -function getDragCursor(nsew, dragmode) { +function getDragCursor(nsew, dragmode, isMainDrag) { if(!nsew) return 'pointer'; if(nsew === 'nsew') { + // in this case here, clear cursor and + // use the cursor style set on + if(isMainDrag) return ''; if(dragmode === 'pan') return 'move'; return 'crosshair'; } @@ -930,13 +975,6 @@ function makeCorners(zoomlayer, xs, ys) { .attr('d', 'M0,0Z'); } -function clearSelect(zoomlayer) { - // until we get around to persistent selections, remove the outline - // here. The selection itself will be removed when the plot redraws - // at the end. - zoomlayer.selectAll('.select-outline').remove(); -} - function updateZoombox(zb, corners, box, path0, dimmed, lum) { zb.attr('d', path0 + 'M' + (box.l) + ',' + (box.t) + 'v' + (box.h) + @@ -1002,40 +1040,40 @@ function xyCorners(box) { 'h' + clen + 'v3h-' + (clen + 3) + 'Z'; } -function calcLinks(constraintGroups, xIDs, yIDs) { +function calcLinks(gd, xaHash, yaHash) { + var constraintGroups = gd._fullLayout._axisConstraintGroups; var isSubplotConstrained = false; var xLinks = {}; var yLinks = {}; - var i, j, k; + var xID, yID, xLinkID, yLinkID; - var group, xLinkID, yLinkID; - for(i = 0; i < constraintGroups.length; i++) { - group = constraintGroups[i]; + for(var i = 0; i < constraintGroups.length; i++) { + var group = constraintGroups[i]; // check if any of the x axes we're dragging is in this constraint group - for(j = 0; j < xIDs.length; j++) { - if(group[xIDs[j]]) { + for(xID in xaHash) { + if(group[xID]) { // put the rest of these axes into xLinks, if we're not already // dragging them, so we know to scale these axes automatically too // to match the changes in the dragged x axes for(xLinkID in group) { - if((xLinkID.charAt(0) === 'x' ? xIDs : yIDs).indexOf(xLinkID) === -1) { + if(!(xLinkID.charAt(0) === 'x' ? xaHash : yaHash)[xLinkID]) { xLinks[xLinkID] = 1; } } // check if the x and y axes of THIS drag are linked - for(k = 0; k < yIDs.length; k++) { - if(group[yIDs[k]]) isSubplotConstrained = true; + for(yID in yaHash) { + if(group[yID]) isSubplotConstrained = true; } } } // now check if any of the y axes we're dragging is in this constraint group // only look for outside links, as we've already checked for links within the dragger - for(j = 0; j < yIDs.length; j++) { - if(group[yIDs[j]]) { + for(yID in yaHash) { + if(group[yID]) { for(yLinkID in group) { - if((yLinkID.charAt(0) === 'x' ? xIDs : yIDs).indexOf(yLinkID) === -1) { + if(!(yLinkID.charAt(0) === 'x' ? xaHash : yaHash)[yLinkID]) { yLinks[yLinkID] = 1; } } @@ -1050,10 +1088,29 @@ function calcLinks(constraintGroups, xIDs, yIDs) { Lib.extendFlat(xLinks, yLinks); yLinks = {}; } + + var xaHashLinked = {}; + var xaxesLinked = []; + for(xLinkID in xLinks) { + var xa = getFromId(gd, xLinkID); + xaxesLinked.push(xa); + xaHashLinked[xa._id] = xa; + } + + var yaHashLinked = {}; + var yaxesLinked = []; + for(yLinkID in yLinks) { + var ya = getFromId(gd, yLinkID); + yaxesLinked.push(ya); + yaHashLinked[ya._id] = ya; + } + return { - x: xLinks, - y: yLinks, - xy: isSubplotConstrained + xaHash: xaHashLinked, + yaHash: yaHashLinked, + xaxes: xaxesLinked, + yaxes: yaxesLinked, + isSubplotConstrained: isSubplotConstrained }; } @@ -1075,6 +1132,12 @@ function attachWheelEventHandler(element, handler) { } } +function hashValues(hash) { + var out = []; + for(var k in hash) out.push(hash[k]); + return out; +} + module.exports = { makeDragBox: makeDragBox, @@ -1087,7 +1150,6 @@ module.exports = { xyCorners: xyCorners, transitionZoombox: transitionZoombox, removeZoombox: removeZoombox, - clearSelect: clearSelect, showDoubleClickNotifier: showDoubleClickNotifier, attachWheelEventHandler: attachWheelEventHandler diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index a56d7befae1..f872ddef16e 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -13,11 +13,12 @@ var d3 = require('d3'); var Fx = require('../../components/fx'); var dragElement = require('../../components/dragelement'); +var setCursor = require('../../lib/setcursor'); -var constants = require('./constants'); var makeDragBox = require('./dragbox').makeDragBox; +var DRAGGERSIZE = require('./constants').DRAGGERSIZE; -module.exports = function initInteractions(gd) { +exports.initInteractions = function initInteractions(gd) { var fullLayout = gd._fullLayout; if(gd._context.staticPlot) { @@ -26,7 +27,7 @@ module.exports = function initInteractions(gd) { return; } - if(!fullLayout._has('cartesian') && !fullLayout._has('gl2d')) return; + if(!fullLayout._has('cartesian') && !fullLayout._has('gl2d') && !fullLayout._has('splom')) return; var subplots = Object.keys(fullLayout._plots || {}).sort(function(a, b) { // sort overlays last, then by x axis number, then y axis number @@ -43,12 +44,9 @@ module.exports = function initInteractions(gd) { subplots.forEach(function(subplot) { var plotinfo = fullLayout._plots[subplot]; - var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; - var DRAGGERSIZE = constants.DRAGGERSIZE; - // main and corner draggers need not be repeated for // overlaid subplots - these draggers drag them all if(!plotinfo.mainplot) { @@ -139,17 +137,29 @@ module.exports = function initInteractions(gd) { var hoverLayer = fullLayout._hoverlayer.node(); hoverLayer.onmousemove = function(evt) { - evt.target = fullLayout._lasthover; + evt.target = gd._fullLayout._lasthover; Fx.hover(gd, evt, fullLayout._hoversubplot); }; hoverLayer.onclick = function(evt) { - evt.target = fullLayout._lasthover; + evt.target = gd._fullLayout._lasthover; Fx.click(gd, evt); }; // also delegate mousedowns... TODO: does this actually work? hoverLayer.onmousedown = function(evt) { - fullLayout._lasthover.onmousedown(evt); + gd._fullLayout._lasthover.onmousedown(evt); }; + + exports.updateFx(fullLayout); +}; + +// Minimal set of update needed on 'modebar' edits. +// We only need to update the cursor style. +// +// Note that changing the axis configuration and/or the fixedrange attribute +// should trigger a full initInteractions. +exports.updateFx = function(fullLayout) { + var cursor = fullLayout.dragmode === 'pan' ? 'move' : 'crosshair'; + setCursor(fullLayout._draggers, cursor); }; diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 2d454e4a68f..3304737bfb6 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -135,17 +135,6 @@ exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) { } } - // clear gl frame, if any, since we preserve drawing buffer - if(fullLayout._glcanvas && fullLayout._glcanvas.size()) { - fullLayout._glcanvas.each(function(d) { - if(d.regl) { - d.regl.clear({ - color: true - }); - } - }); - } - for(i = 0; i < subplots.length; i++) { var subplot = subplots[i], subplotInfo = fullLayout._plots[subplot]; @@ -221,11 +210,23 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback } exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldModules = oldFullLayout._modules || [], - newModules = newFullLayout._modules || []; - - var hadScatter, hasScatter, hadGl, hasGl, i, oldPlots, ids, subplotInfo, moduleName; - + var oldModules = oldFullLayout._modules || []; + var newModules = newFullLayout._modules || []; + var oldPlots = oldFullLayout._plots || {}; + + var hadScatter, hasScatter; + var hadGl, hasGl; + var i, k, subplotInfo, moduleName; + + // when going from a large splom graph to something else, + // we need to clear so that the new cartesian subplot + // can have the correct layer ordering + if(oldFullLayout._hasOnlyLargeSploms && !newFullLayout._hasOnlyLargeSploms) { + for(k in oldPlots) { + subplotInfo = oldPlots[k]; + if(subplotInfo.plotgroup) subplotInfo.plotgroup.remove(); + } + } for(i = 0; i < oldModules.length; i++) { moduleName = oldModules[i].name; @@ -240,12 +241,8 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) } if(hadScatter && !hasScatter) { - oldPlots = oldFullLayout._plots; - ids = Object.keys(oldPlots || {}); - - for(i = 0; i < ids.length; i++) { - subplotInfo = oldPlots[ids[i]]; - + for(k in oldPlots) { + subplotInfo = oldPlots[k]; if(subplotInfo.plot) { subplotInfo.plot.select('g.scatterlayer') .selectAll('g.trace') @@ -260,11 +257,8 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) } if(hadGl && !hasGl) { - oldPlots = oldFullLayout._plots; - ids = Object.keys(oldPlots || {}); - - for(i = 0; i < ids.length; i++) { - subplotInfo = oldPlots[ids[i]]; + for(k in oldPlots) { + subplotInfo = oldPlots[k]; if(subplotInfo._scene) { subplotInfo._scene.destroy(); @@ -334,7 +328,7 @@ exports.drawFramework = function(gd) { // initialize list of overlay subplots plotinfo.overlays = []; - makeSubplotLayer(plotinfo); + makeSubplotLayer(gd, plotinfo); // fill in list of overlay subplots if(plotinfo.mainplot) { @@ -350,7 +344,7 @@ exports.drawFramework = function(gd) { }; exports.rangePlot = function(gd, plotinfo, cdSubplot) { - makeSubplotLayer(plotinfo); + makeSubplotLayer(gd, plotinfo); plotOne(gd, plotinfo, cdSubplot); Plots.style(gd); }; @@ -382,45 +376,59 @@ function makeSubplotData(gd) { return subplotData; } -function makeSubplotLayer(plotinfo) { +function makeSubplotLayer(gd, plotinfo) { var plotgroup = plotinfo.plotgroup; var id = plotinfo.id; var xLayer = constants.layerValue2layerClass[plotinfo.xaxis.layer]; var yLayer = constants.layerValue2layerClass[plotinfo.yaxis.layer]; + var hasOnlyLargeSploms = gd._fullLayout._hasOnlyLargeSploms; if(!plotinfo.mainplot) { - var backLayer = ensureSingle(plotgroup, 'g', 'layer-subplot'); - plotinfo.shapelayer = ensureSingle(backLayer, 'g', 'shapelayer'); - plotinfo.imagelayer = ensureSingle(backLayer, 'g', 'imagelayer'); - - plotinfo.gridlayer = ensureSingle(plotgroup, 'g', 'gridlayer'); - - plotinfo.zerolinelayer = ensureSingle(plotgroup, 'g', 'zerolinelayer'); - - ensureSingle(plotgroup, 'path', 'xlines-below'); - ensureSingle(plotgroup, 'path', 'ylines-below'); - plotinfo.overlinesBelow = ensureSingle(plotgroup, 'g', 'overlines-below'); - - ensureSingle(plotgroup, 'g', 'xaxislayer-below'); - ensureSingle(plotgroup, 'g', 'yaxislayer-below'); - plotinfo.overaxesBelow = ensureSingle(plotgroup, 'g', 'overaxes-below'); - - plotinfo.plot = ensureSingle(plotgroup, 'g', 'plot'); - plotinfo.overplot = ensureSingle(plotgroup, 'g', 'overplot'); - - ensureSingle(plotgroup, 'path', 'xlines-above'); - ensureSingle(plotgroup, 'path', 'ylines-above'); - plotinfo.overlinesAbove = ensureSingle(plotgroup, 'g', 'overlines-above'); - - ensureSingle(plotgroup, 'g', 'xaxislayer-above'); - ensureSingle(plotgroup, 'g', 'yaxislayer-above'); - plotinfo.overaxesAbove = ensureSingle(plotgroup, 'g', 'overaxes-above'); - - // set refs to correct layers as determined by 'axis.layer' - plotinfo.xlines = plotgroup.select('.xlines-' + xLayer); - plotinfo.ylines = plotgroup.select('.ylines-' + yLayer); - plotinfo.xaxislayer = plotgroup.select('.xaxislayer-' + xLayer); - plotinfo.yaxislayer = plotgroup.select('.yaxislayer-' + yLayer); + if(hasOnlyLargeSploms) { + // TODO could do even better + // - we don't need plot (but we would have to mock it in lsInner + // and other places + // - we don't (x|y)lines and (x|y)axislayer for most subplots + // usually just the bottom x and left y axes. + plotinfo.plot = ensureSingle(plotgroup, 'g', 'plot'); + plotinfo.xlines = ensureSingle(plotgroup, 'path', 'xlines-above'); + plotinfo.ylines = ensureSingle(plotgroup, 'path', 'ylines-above'); + plotinfo.xaxislayer = ensureSingle(plotgroup, 'g', 'xaxislayer-above'); + plotinfo.yaxislayer = ensureSingle(plotgroup, 'g', 'yaxislayer-above'); + } + else { + var backLayer = ensureSingle(plotgroup, 'g', 'layer-subplot'); + plotinfo.shapelayer = ensureSingle(backLayer, 'g', 'shapelayer'); + plotinfo.imagelayer = ensureSingle(backLayer, 'g', 'imagelayer'); + + plotinfo.gridlayer = ensureSingle(plotgroup, 'g', 'gridlayer'); + plotinfo.zerolinelayer = ensureSingle(plotgroup, 'g', 'zerolinelayer'); + + ensureSingle(plotgroup, 'path', 'xlines-below'); + ensureSingle(plotgroup, 'path', 'ylines-below'); + plotinfo.overlinesBelow = ensureSingle(plotgroup, 'g', 'overlines-below'); + + ensureSingle(plotgroup, 'g', 'xaxislayer-below'); + ensureSingle(plotgroup, 'g', 'yaxislayer-below'); + plotinfo.overaxesBelow = ensureSingle(plotgroup, 'g', 'overaxes-below'); + + plotinfo.plot = ensureSingle(plotgroup, 'g', 'plot'); + plotinfo.overplot = ensureSingle(plotgroup, 'g', 'overplot'); + + plotinfo.xlines = ensureSingle(plotgroup, 'path', 'xlines-above'); + plotinfo.ylines = ensureSingle(plotgroup, 'path', 'ylines-above'); + plotinfo.overlinesAbove = ensureSingle(plotgroup, 'g', 'overlines-above'); + + ensureSingle(plotgroup, 'g', 'xaxislayer-above'); + ensureSingle(plotgroup, 'g', 'yaxislayer-above'); + plotinfo.overaxesAbove = ensureSingle(plotgroup, 'g', 'overaxes-above'); + + // set refs to correct layers as determined by 'axis.layer' + plotinfo.xlines = plotgroup.select('.xlines-' + xLayer); + plotinfo.ylines = plotgroup.select('.ylines-' + yLayer); + plotinfo.xaxislayer = plotgroup.select('.xaxislayer-' + xLayer); + plotinfo.yaxislayer = plotgroup.select('.yaxislayer-' + yLayer); + } } else { var mainplotinfo = plotinfo.mainplotinfo; @@ -455,14 +463,16 @@ function makeSubplotLayer(plotinfo) { plotinfo.yaxislayer = mainplotgroup.select('.overaxes-' + yLayer).select('.' + yId); } - ensureSingleAndAddDatum(plotinfo.gridlayer, 'g', plotinfo.xaxis._id); - ensureSingleAndAddDatum(plotinfo.gridlayer, 'g', plotinfo.yaxis._id); - plotinfo.gridlayer.selectAll('g').sort(axisIds.idSort); - // common attributes for all subplots, overlays or not - for(var i = 0; i < constants.traceLayerClasses.length; i++) { - ensureSingle(plotinfo.plot, 'g', constants.traceLayerClasses[i]); + if(!hasOnlyLargeSploms) { + ensureSingleAndAddDatum(plotinfo.gridlayer, 'g', plotinfo.xaxis._id); + ensureSingleAndAddDatum(plotinfo.gridlayer, 'g', plotinfo.yaxis._id); + plotinfo.gridlayer.selectAll('g').sort(axisIds.idSort); + + for(var i = 0; i < constants.traceLayerClasses.length; i++) { + ensureSingle(plotinfo.plot, 'g', constants.traceLayerClasses[i]); + } } plotinfo.xlines @@ -539,3 +549,5 @@ exports.toSVG = function(gd) { canvases.each(canvasToImage); }; + +exports.updateFx = require('./graph_interact').updateFx; diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 8334ab5d0ec..25bfda44393 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -100,10 +100,10 @@ module.exports = { valType: 'info_array', role: 'info', items: [ - {valType: 'any', editType: 'plot+margins', impliedEdits: {'^autorange': false}}, - {valType: 'any', editType: 'plot+margins', impliedEdits: {'^autorange': false}} + {valType: 'any', editType: 'axrange+margins', impliedEdits: {'^autorange': false}}, + {valType: 'any', editType: 'axrange+margins', impliedEdits: {'^autorange': false}} ], - editType: 'plot+margins', + editType: 'axrange+margins', impliedEdits: {'autorange': false}, description: [ 'Sets the range of this axis.', diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 92837250622..4eda20dec49 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -10,59 +10,83 @@ 'use strict'; var polybool = require('polybooljs'); + +var Registry = require('../../registry'); +var Color = require('../../components/color'); +var Fx = require('../../components/fx'); + var polygon = require('../../lib/polygon'); var throttle = require('../../lib/throttle'); -var color = require('../../components/color'); var makeEventData = require('../../components/fx/helpers').makeEventData; -var Fx = require('../../components/fx'); +var getFromId = require('./axis_ids').getFromId; +var sortModules = require('../sort_modules').sortModules; -var axes = require('./axes'); var constants = require('./constants'); +var MINSELECT = constants.MINSELECT; var filteredPolygon = polygon.filter; var polygonTester = polygon.tester; var multipolygonTester = polygon.multitester; -var MINSELECT = constants.MINSELECT; function getAxId(ax) { return ax._id; } -module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { - var gd = dragOptions.gd, - fullLayout = gd._fullLayout, - zoomLayer = fullLayout._zoomlayer, - dragBBox = dragOptions.element.getBoundingClientRect(), - plotinfo = dragOptions.plotinfo, - xs = plotinfo.xaxis._offset, - ys = plotinfo.yaxis._offset, - x0 = startX - dragBBox.left, - y0 = startY - dragBBox.top, - x1 = x0, - y1 = y0, - path0 = 'M' + x0 + ',' + y0, - pw = dragOptions.xaxes[0]._length, - ph = dragOptions.yaxes[0]._length, - xAxisIds = dragOptions.xaxes.map(getAxId), - yAxisIds = dragOptions.yaxes.map(getAxId), - allAxes = dragOptions.xaxes.concat(dragOptions.yaxes), - filterPoly, testPoly, mergedPolygons, currentPolygon, - subtract = e.altKey; - - - // take over selection polygons from prev mode, if any - if((e.shiftKey || e.altKey) && (plotinfo.selection && plotinfo.selection.polygons) && !dragOptions.polygons) { +function prepSelect(e, startX, startY, dragOptions, mode) { + var gd = dragOptions.gd; + var fullLayout = gd._fullLayout; + var zoomLayer = fullLayout._zoomlayer; + var dragBBox = dragOptions.element.getBoundingClientRect(); + var plotinfo = dragOptions.plotinfo; + var xs = plotinfo.xaxis._offset; + var ys = plotinfo.yaxis._offset; + var x0 = startX - dragBBox.left; + var y0 = startY - dragBBox.top; + var x1 = x0; + var y1 = y0; + var path0 = 'M' + x0 + ',' + y0; + var pw = dragOptions.xaxes[0]._length; + var ph = dragOptions.yaxes[0]._length; + var xAxisIds = dragOptions.xaxes.map(getAxId); + var yAxisIds = dragOptions.yaxes.map(getAxId); + var allAxes = dragOptions.xaxes.concat(dragOptions.yaxes); + var subtract = e.altKey; + + var filterPoly, testPoly, mergedPolygons, currentPolygon; + var i, cd, trace, searchInfo, eventData; + + var selectingOnSameSubplot = ( + fullLayout._lastSelectedSubplot && + fullLayout._lastSelectedSubplot === plotinfo.id + ); + + if( + selectingOnSameSubplot && + (e.shiftKey || e.altKey) && + (plotinfo.selection && plotinfo.selection.polygons) && + !dragOptions.polygons + ) { + // take over selection polygons from prev mode, if any dragOptions.polygons = plotinfo.selection.polygons; dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons; - } - // create new polygons, if shift mode - else if((!e.shiftKey && !e.altKey) || ((e.shiftKey || e.altKey) && !plotinfo.selection)) { + } else if( + (!e.shiftKey && !e.altKey) || + ((e.shiftKey || e.altKey) && !plotinfo.selection) + ) { + // create new polygons, if shift mode or selecting across different subplots plotinfo.selection = {}; plotinfo.selection.polygons = dragOptions.polygons = []; plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = []; } + // clear selection outline when selecting a different subplot + if(!selectingOnSameSubplot) { + clearSelect(zoomLayer); + fullLayout._lastSelectedSubplot = plotinfo.id; + } + if(mode === 'lasso') { filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX); } + var outlines = zoomLayer.selectAll('path.select-outline-' + plotinfo.id).data([1, 2]); outlines.enter() @@ -74,8 +98,8 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { var corners = zoomLayer.append('path') .attr('class', 'zoombox-corners') .style({ - fill: color.background, - stroke: color.defaultLine, + fill: Color.background, + stroke: Color.defaultLine, 'stroke-width': 1 }) .attr('transform', 'translate(' + xs + ', ' + ys + ')') @@ -86,11 +110,11 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { var searchTraces = []; var throttleID = fullLayout._uid + constants.SELECTID; var selection = []; - var i, cd, trace, searchInfo, eventData; for(i = 0; i < gd.calcdata.length; i++) { cd = gd.calcdata[i]; trace = cd[0].trace; + if(trace.visible !== true || !trace._module || !trace._module.selectPoints) continue; if(dragOptions.subplot) { @@ -99,23 +123,32 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { trace.geo === dragOptions.subplot ) { searchTraces.push({ - selectPoints: trace._module.selectPoints, - style: trace._module.style, + _module: trace._module, cd: cd, xaxis: dragOptions.xaxes[0], yaxis: dragOptions.yaxes[0] }); } + } else if( + trace.type === 'splom' && + // FIXME: make sure we don't have more than single axis for splom + trace._xaxes[xAxisIds[0]] && trace._yaxes[yAxisIds[0]] + ) { + searchTraces.push({ + _module: trace._module, + cd: cd, + xaxis: dragOptions.xaxes[0], + yaxis: dragOptions.yaxes[0] + }); } else { if(xAxisIds.indexOf(trace.xaxis) === -1) continue; if(yAxisIds.indexOf(trace.yaxis) === -1) continue; searchTraces.push({ - selectPoints: trace._module.selectPoints, - style: trace._module.style, + _module: trace._module, cd: cd, - xaxis: axes.getFromId(gd, trace.xaxis), - yaxis: axes.getFromId(gd, trace.yaxis) + xaxis: getFromId(gd, trace.xaxis), + yaxis: getFromId(gd, trace.yaxis) }); } } @@ -238,7 +271,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { for(i = 0; i < searchTraces.length; i++) { searchInfo = searchTraces[i]; - traceSelection = searchInfo.selectPoints(searchInfo, testPoly); + traceSelection = searchInfo._module.selectPoints(searchInfo, testPoly); traceSelections.push(traceSelection); thisSelection = fillSelectionItem(traceSelection, searchInfo); @@ -269,7 +302,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { outlines.remove(); for(i = 0; i < searchTraces.length; i++) { searchInfo = searchTraces[i]; - searchInfo.selectPoints(searchInfo, false); + searchInfo._module.selectPoints(searchInfo, false); } updateSelectedState(gd, searchTraces); @@ -303,10 +336,10 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { } }); }; -}; +} function updateSelectedState(gd, searchTraces, eventData) { - var i, searchInfo, trace; + var i, j, searchInfo, trace; if(eventData) { var pts = eventData.points || []; @@ -336,17 +369,44 @@ function updateSelectedState(gd, searchTraces, eventData) { trace = searchTraces[i].cd[0].trace; delete trace.selectedpoints; delete trace._input.selectedpoints; - - // delete scattergl selection - if(searchTraces[i].cd[0].t && searchTraces[i].cd[0].t.scene) { - searchTraces[i].cd[0].t.scene.clearSelect(); - } } } + // group searchInfo traces by trace modules + var lookup = {}; + for(i = 0; i < searchTraces.length; i++) { searchInfo = searchTraces[i]; - if(searchInfo.style) searchInfo.style(gd, searchInfo.cd); + + var name = searchInfo._module.name; + if(lookup[name]) { + lookup[name].push(searchInfo); + } else { + lookup[name] = [searchInfo]; + } + } + + var keys = Object.keys(lookup).sort(sortModules); + + for(i = 0; i < keys.length; i++) { + var items = lookup[keys[i]]; + var len = items.length; + var item0 = items[0]; + var trace0 = item0.cd[0].trace; + + if(Registry.traceIs(trace0, 'regl')) { + // plot regl traces per module + var cds = new Array(len); + for(j = 0; j < len; j++) { + cds[j] = items[j].cd; + } + item0._module.style(gd, cds); + } else { + // plot svg trace per trace + for(j = 0; j < len; j++) { + item0._module.style(gd, items[j].cd); + } + } } } @@ -388,3 +448,15 @@ function fillSelectionItem(selection, searchInfo) { return selection; } + +function clearSelect(zoomlayer) { + // until we get around to persistent selections, remove the outline + // here. The selection itself will be removed when the plot redraws + // at the end. + zoomlayer.selectAll('.select-outline').remove(); +} + +module.exports = { + prepSelect: prepSelect, + clearSelect: clearSelect +}; diff --git a/src/plots/cartesian/transition_axes.js b/src/plots/cartesian/transition_axes.js index 281b9964b9b..c8d306d5e0e 100644 --- a/src/plots/cartesian/transition_axes.js +++ b/src/plots/cartesian/transition_axes.js @@ -233,7 +233,7 @@ module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCo var plotDx = xa2._offset - fracDx, plotDy = ya2._offset - fracDy; - fullLayout._defs.select('#' + subplot.clipId + '> rect') + subplot.clipRect .call(Drawing.setTranslate, clipDx, clipDy) .call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor); diff --git a/src/plots/cartesian/type_defaults.js b/src/plots/cartesian/type_defaults.js index eba7d535c3b..3d4f7c4bf35 100644 --- a/src/plots/cartesian/type_defaults.js +++ b/src/plots/cartesian/type_defaults.js @@ -46,8 +46,8 @@ function setAutoType(ax, data) { // only autotype if type is '-' if(ax.type !== '-') return; - var id = ax._id, - axLetter = id.charAt(0); + var id = ax._id; + var axLetter = id.charAt(0); // support 3d if(id.indexOf('scene') !== -1) id = axLetter; @@ -63,18 +63,18 @@ function setAutoType(ax, data) { return; } - var calAttr = axLetter + 'calendar', - calendar = d0[calAttr]; + var calAttr = axLetter + 'calendar'; + var calendar = d0[calAttr]; + var i; // check all boxes on this x axis to see // if they're dates, numbers, or categories if(isBoxWithoutPositionCoords(d0, axLetter)) { - var posLetter = getBoxPosLetter(d0), - boxPositions = [], - trace; + var posLetter = getBoxPosLetter(d0); + var boxPositions = []; - for(var i = 0; i < data.length; i++) { - trace = data[i]; + for(i = 0; i < data.length; i++) { + var trace = data[i]; if(!Registry.traceIs(trace, 'box-violin') || (trace[axLetter + 'axis'] || axLetter) !== id) continue; @@ -87,6 +87,16 @@ function setAutoType(ax, data) { ax.type = autoType(boxPositions, calendar); } + else if(d0.type === 'splom') { + var dimensions = d0.dimensions; + for(i = 0; i < dimensions.length; i++) { + var dim = dimensions[i]; + if(dim.visible) { + ax.type = autoType(dim.values, calendar); + break; + } + } + } else { ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']], calendar); } @@ -96,6 +106,13 @@ function getFirstNonEmptyTrace(data, id, axLetter) { for(var i = 0; i < data.length; i++) { var trace = data[i]; + if(trace.type === 'splom' && + trace._commonLength > 0 && + trace['_' + axLetter + 'axes'][id] + ) { + return trace; + } + if((trace[axLetter + 'axis'] || axLetter) === id) { if(isBoxWithoutPositionCoords(trace, axLetter)) { return trace; diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index f5ca806f1bb..e6d419503b5 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -20,7 +20,7 @@ var Fx = require('../../components/fx'); var Plots = require('../plots'); var Axes = require('../cartesian/axes'); var dragElement = require('../../components/dragelement'); -var prepSelect = require('../cartesian/select'); +var prepSelect = require('../cartesian/select').prepSelect; var createGeoZoom = require('./zoom'); var constants = require('./constants'); diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 4df6dfd4555..18db3935a4d 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -14,7 +14,7 @@ var mapboxgl = require('mapbox-gl'); var Fx = require('../../components/fx'); var Lib = require('../../lib'); var dragElement = require('../../components/dragelement'); -var prepSelect = require('../cartesian/select'); +var prepSelect = require('../cartesian/select').prepSelect; var constants = require('./constants'); var layoutAttributes = require('./layout_attributes'); var createMapboxLayer = require('./layers'); diff --git a/src/plots/plots.js b/src/plots/plots.js index 56c45fcdb17..80af2b79bc5 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -14,18 +14,20 @@ var isNumeric = require('fast-isnumeric'); var Registry = require('../registry'); var PlotSchema = require('../plot_api/plot_schema'); -var axisIDs = require('../plots/cartesian/axis_ids'); var Lib = require('../lib'); -var _ = Lib._; var Color = require('../components/color'); var BADNUM = require('../constants/numerical').BADNUM; -var plots = module.exports = {}; +var axisIDs = require('../plots/cartesian/axis_ids'); +var sortBasePlotModules = require('./sort_modules').sortBasePlotModules; var animationAttrs = require('./animation_attributes'); var frameAttrs = require('./frame_attributes'); var relinkPrivateKeys = Lib.relinkPrivateKeys; +var _ = Lib._; + +var plots = module.exports = {}; // Expose registry methods on Plots for backward-compatibility Lib.extendFlat(plots, Registry); @@ -287,6 +289,8 @@ plots.supplyDefaults = function(gd) { var newFullData = gd._fullData = []; var newData = gd.data || []; + var oldCalcdata = gd.calcdata || []; + var context = gd._context || {}; var i; @@ -321,7 +325,6 @@ plots.supplyDefaults = function(gd) { // first fill in what we can of layout without looking at data // because fullData needs a few things from layout - if(oldFullLayout._initialAutoSizeIsDone) { // coerce the updated layout while preserving width and height @@ -364,12 +367,34 @@ plots.supplyDefaults = function(gd) { // clear the lists of trace and baseplot modules, and subplots newFullLayout._modules = []; newFullLayout._basePlotModules = []; - newFullLayout._subplots = emptySubplotLists(); + var subplots = newFullLayout._subplots = emptySubplotLists(); + + // initialize axis and subplot hash objects for splom-generated grids + var splomAxes = newFullLayout._splomAxes = {x: {}, y: {}}; + var splomSubplots = newFullLayout._splomSubplots = {}; // then do the data newFullLayout._globalTransforms = (gd._context || {}).globalTransforms; plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout); + // redo grid size defaults with info about splom x/y axes, + // and fill in generated cartesian axes and subplots + var splomXa = Object.keys(splomAxes.x); + var splomYa = Object.keys(splomAxes.y); + if(splomXa.length > 1 && splomYa.length > 1) { + Registry.getComponentMethod('grid', 'sizeDefaults')(newLayout, newFullLayout); + + for(i = 0; i < splomXa.length; i++) { + Lib.pushUnique(subplots.xaxis, splomXa[i]); + } + for(i = 0; i < splomYa.length; i++) { + Lib.pushUnique(subplots.yaxis, splomYa[i]); + } + for(var k in splomSubplots) { + Lib.pushUnique(subplots.cartesian, k); + } + } + // attach helper method to check whether a plot type is present on graph newFullLayout._has = plots._hasPlotType.bind(newFullLayout); @@ -389,6 +414,17 @@ plots.supplyDefaults = function(gd) { // finally, fill in the pieces of layout that may need to look at data plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData, gd._transitionData); + // turn on flag to optimize large splom-only graphs + // mostly by omitting SVG layers during Cartesian.drawFramework + newFullLayout._hasOnlyLargeSploms = ( + newFullLayout._basePlotModules.length === 1 && + newFullLayout._basePlotModules[0].name === 'splom' && + splomXa.length > 15 && + splomYa.length > 15 && + newFullLayout.shapes.length === 0 && + newFullLayout.images.length === 0 + ); + // TODO remove in v2.0.0 // add has-plot-type refs to fullLayout for backward compatibility newFullLayout._hasCartesian = newFullLayout._has('cartesian'); @@ -399,7 +435,7 @@ plots.supplyDefaults = function(gd) { newFullLayout._hasPie = newFullLayout._has('pie'); // clean subplots and other artifacts from previous plot calls - plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); + plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata); // relink / initialize subplot axis objects plots.linkSubplots(newFullData, newFullLayout, oldFullData, oldFullLayout); @@ -418,10 +454,10 @@ plots.supplyDefaults = function(gd) { } // update object references in calcdata - if((gd.calcdata || []).length === newFullData.length) { + if(oldCalcdata.length === newFullData.length) { for(i = 0; i < newFullData.length; i++) { var newTrace = newFullData[i]; - var cd0 = gd.calcdata[i][0]; + var cd0 = oldCalcdata[i][0]; if(cd0 && cd0.trace) { if(cd0.trace._hasCalcTransform) { remapTransformedArrays(cd0, newTrace); @@ -431,6 +467,9 @@ plots.supplyDefaults = function(gd) { } } } + + // sort base plot modules for consistent ordering + newFullLayout._basePlotModules.sort(sortBasePlotModules); }; /** @@ -596,30 +635,28 @@ plots.createTransitionData = function(gd) { // whether a certain plot type is present on plot // or trace has a category plots._hasPlotType = function(category) { - // check plot - var basePlotModules = this._basePlotModules || []; var i; + // check base plot modules + var basePlotModules = this._basePlotModules || []; for(i = 0; i < basePlotModules.length; i++) { - var _module = basePlotModules[i]; - - if(_module.name === category) return true; + if(basePlotModules[i].name === category) return true; } - // check trace + // check trace modules var modules = this._modules || []; - for(i = 0; i < modules.length; i++) { - var modulei = modules[i]; - if(modulei.categories && modulei.categories.indexOf(category) >= 0) { - return true; - } + var name = modules[i].name; + if(name === category) return true; + // N.B. this is modules[i] along with 'categories' as a hash object + var _module = Registry.modules[name]; + if(_module && _module.categories[category]) return true; } return false; }; -plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { +plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata) { var i, j; var basePlotModules = oldFullLayout._basePlotModules || []; @@ -627,7 +664,7 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou var _module = basePlotModules[i]; if(_module.clean) { - _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout); + _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata); } } @@ -1064,9 +1101,9 @@ plots.supplyTraceDefaults = function(traceIn, colorIndex, layout, traceInIndex) if(_module) { var basePlotModule = _module.basePlotModule; var subplotAttr = basePlotModule.attr; - if(subplotAttr) { + var subplotAttrs = basePlotModule.attributes; + if(subplotAttr && subplotAttrs) { var subplots = layout._subplots; - var subplotAttrs = basePlotModule.attributes; var subplotId = ''; // TODO - currently if we draw an empty gl2d subplot, it draws diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index 23a1faebca6..555a16252b7 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -22,7 +22,8 @@ var dragElement = require('../../components/dragelement'); var dragBox = require('../cartesian/dragbox'); var Fx = require('../../components/fx'); var Titles = require('../../components/titles'); -var prepSelect = require('../cartesian/select'); +var prepSelect = require('../cartesian/select').prepSelect; +var clearSelect = require('../cartesian/select').clearSelect; var setCursor = require('../../lib/setcursor'); var MID_SHIFT = require('../../constants/alignment').MID_SHIFT; @@ -634,7 +635,7 @@ proto.updateMainDrag = function(fullLayout, polarLayout) { zb = dragBox.makeZoombox(zoomlayer, lum, cx, cy, path0); zb.attr('fill-rule', 'evenodd'); corners = dragBox.makeCorners(zoomlayer, cx, cy); - dragBox.clearSelect(zoomlayer); + clearSelect(zoomlayer); } function zoomMove(dx, dy) { @@ -868,7 +869,7 @@ proto.updateRadialDrag = function(fullLayout, polarLayout) { dragOpts.moveFn = moveFn; dragOpts.doneFn = doneFn; - dragBox.clearSelect(fullLayout._zoomlayer); + clearSelect(fullLayout._zoomlayer); }; dragOpts.clampFn = function(dx, dy) { @@ -1000,7 +1001,7 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) { dragOpts.moveFn = moveFn; dragOpts.doneFn = doneFn; - dragBox.clearSelect(fullLayout._zoomlayer); + clearSelect(fullLayout._zoomlayer); }; dragElement.init(dragOpts); diff --git a/src/plots/sort_modules.js b/src/plots/sort_modules.js new file mode 100644 index 00000000000..38090667d40 --- /dev/null +++ b/src/plots/sort_modules.js @@ -0,0 +1,25 @@ +/** +* 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'; + +// always plot splom before cartesian (i.e. scattergl traces) +function sortModules(a, b) { + if(a === 'splom') return -1; + if(b === 'splom') return 1; + return 0; +} + +function sortBasePlotModules(a, b) { + return sortModules(a.name, b.name); +} + +module.exports = { + sortBasePlotModules: sortBasePlotModules, + sortModules: sortModules +}; diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index a20e706db8d..122430b660e 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -24,7 +24,8 @@ var Axes = require('../cartesian/axes'); var dragElement = require('../../components/dragelement'); var Fx = require('../../components/fx'); var Titles = require('../../components/titles'); -var prepSelect = require('../cartesian/select'); +var prepSelect = require('../cartesian/select').prepSelect; +var clearSelect = require('../cartesian/select').clearSelect; var constants = require('../cartesian/constants'); function Ternary(options, fullLayout) { @@ -478,7 +479,7 @@ proto.initInteractions = function() { dragOptions.moveFn = plotDrag; dragOptions.doneFn = dragDone; panPrep(); - clearSelect(); + clearSelect(zoomContainer); } else if(dragModeNow === 'select' || dragModeNow === 'lasso') { prepSelect(e, startX, startY, dragOptions, dragModeNow); @@ -536,7 +537,7 @@ proto.initInteractions = function() { }) .attr('d', 'M0,0Z'); - clearSelect(); + clearSelect(zoomContainer); } function getAFrac(x, y) { return 1 - (y / _this.h); } @@ -680,13 +681,6 @@ proto.initInteractions = function() { Registry.call('relayout', gd, attrs); } - function clearSelect() { - // until we get around to persistent selections, remove the outline - // here. The selection itself will be removed when the plot redraws - // at the end. - zoomContainer.selectAll('.select-outline').remove(); - } - // finally, set up hover and click // these event handlers must already be set before dragElement.init // so it can stash them and override them. diff --git a/src/traces/bar/index.js b/src/traces/bar/index.js index 6ff430354b8..1798b2a7c88 100644 --- a/src/traces/bar/index.js +++ b/src/traces/bar/index.js @@ -27,7 +27,7 @@ Bar.selectPoints = require('./select'); Bar.moduleType = 'trace'; Bar.name = 'bar'; Bar.basePlotModule = require('../../plots/cartesian'); -Bar.categories = ['cartesian', 'bar', 'oriented', 'markerColorscale', 'errorBarsOK', 'showLegend']; +Bar.categories = ['cartesian', 'svg', 'bar', 'oriented', 'markerColorscale', 'errorBarsOK', 'showLegend', 'draggedPts']; Bar.meta = { description: [ 'The data visualized by the span of the bars is set in `y`', diff --git a/src/traces/box/index.js b/src/traces/box/index.js index 5395dd0af66..ad32d7000aa 100644 --- a/src/traces/box/index.js +++ b/src/traces/box/index.js @@ -24,7 +24,7 @@ Box.selectPoints = require('./select'); Box.moduleType = 'trace'; Box.name = 'box'; Box.basePlotModule = require('../../plots/cartesian'); -Box.categories = ['cartesian', 'symbols', 'oriented', 'box-violin', 'showLegend']; +Box.categories = ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'draggedPts']; Box.meta = { description: [ 'In vertical (horizontal) box plots,', diff --git a/src/traces/candlestick/index.js b/src/traces/candlestick/index.js index dff5c003935..ef94d47bc17 100644 --- a/src/traces/candlestick/index.js +++ b/src/traces/candlestick/index.js @@ -14,7 +14,7 @@ module.exports = { moduleType: 'trace', name: 'candlestick', basePlotModule: require('../../plots/cartesian'), - categories: ['cartesian', 'showLegend', 'candlestick'], + categories: ['cartesian', 'svg', 'showLegend', 'candlestick'], meta: { description: [ 'The candlestick is a style of financial chart describing', diff --git a/src/traces/carpet/index.js b/src/traces/carpet/index.js index 213d473b8f2..2ebbb14a766 100644 --- a/src/traces/carpet/index.js +++ b/src/traces/carpet/index.js @@ -21,7 +21,7 @@ Carpet.isContainer = true; // so carpet traces get `calc` before other traces Carpet.moduleType = 'trace'; Carpet.name = 'carpet'; Carpet.basePlotModule = require('../../plots/cartesian'); -Carpet.categories = ['cartesian', 'carpet', 'carpetAxis', 'notLegendIsolatable']; +Carpet.categories = ['cartesian', 'svg', 'carpet', 'carpetAxis', 'notLegendIsolatable']; Carpet.meta = { description: [ 'The data describing carpet axis layout is set in `y` and (optionally)', diff --git a/src/traces/contour/index.js b/src/traces/contour/index.js index f56f61cd7ec..f498cf78d98 100644 --- a/src/traces/contour/index.js +++ b/src/traces/contour/index.js @@ -22,7 +22,7 @@ Contour.hoverPoints = require('./hover'); Contour.moduleType = 'trace'; Contour.name = 'contour'; Contour.basePlotModule = require('../../plots/cartesian'); -Contour.categories = ['cartesian', '2dMap', 'contour', 'showLegend']; +Contour.categories = ['cartesian', 'svg', '2dMap', 'contour', 'showLegend']; Contour.meta = { description: [ 'The data from which contour lines are computed is set in `z`.', diff --git a/src/traces/contourcarpet/index.js b/src/traces/contourcarpet/index.js index 7853dae6fc1..1529594f1bc 100644 --- a/src/traces/contourcarpet/index.js +++ b/src/traces/contourcarpet/index.js @@ -20,7 +20,7 @@ ContourCarpet.style = require('../contour/style'); ContourCarpet.moduleType = 'trace'; ContourCarpet.name = 'contourcarpet'; ContourCarpet.basePlotModule = require('../../plots/cartesian'); -ContourCarpet.categories = ['cartesian', 'carpet', 'contour', 'symbols', 'showLegend', 'hasLines', 'carpetDependent']; +ContourCarpet.categories = ['cartesian', 'svg', 'carpet', 'contour', 'symbols', 'showLegend', 'hasLines', 'carpetDependent']; ContourCarpet.meta = { hrName: 'contour_carpet', description: [ diff --git a/src/traces/heatmap/index.js b/src/traces/heatmap/index.js index d50b941e377..12ccc878755 100644 --- a/src/traces/heatmap/index.js +++ b/src/traces/heatmap/index.js @@ -22,7 +22,7 @@ Heatmap.hoverPoints = require('./hover'); Heatmap.moduleType = 'trace'; Heatmap.name = 'heatmap'; Heatmap.basePlotModule = require('../../plots/cartesian'); -Heatmap.categories = ['cartesian', '2dMap']; +Heatmap.categories = ['cartesian', 'svg', '2dMap']; Heatmap.meta = { description: [ 'The data that describes the heatmap value-to-color mapping', diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index f1c107e7555..c0949a06447 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -41,7 +41,7 @@ Histogram.eventData = require('./event_data'); Histogram.moduleType = 'trace'; Histogram.name = 'histogram'; Histogram.basePlotModule = require('../../plots/cartesian'); -Histogram.categories = ['cartesian', 'bar', 'histogram', 'oriented', 'errorBarsOK', 'showLegend']; +Histogram.categories = ['cartesian', 'svg', 'bar', 'histogram', 'oriented', 'errorBarsOK', 'showLegend']; Histogram.meta = { description: [ 'The sample data from which statistics are computed is set in `x`', diff --git a/src/traces/histogram2d/index.js b/src/traces/histogram2d/index.js index 324a952598e..c8e9695b8dc 100644 --- a/src/traces/histogram2d/index.js +++ b/src/traces/histogram2d/index.js @@ -23,7 +23,7 @@ Histogram2D.eventData = require('../histogram/event_data'); Histogram2D.moduleType = 'trace'; Histogram2D.name = 'histogram2d'; Histogram2D.basePlotModule = require('../../plots/cartesian'); -Histogram2D.categories = ['cartesian', '2dMap', 'histogram']; +Histogram2D.categories = ['cartesian', 'svg', '2dMap', 'histogram']; Histogram2D.meta = { hrName: 'histogram_2d', description: [ diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js index 9c3d5f2e2da..7953ff48354 100644 --- a/src/traces/histogram2dcontour/index.js +++ b/src/traces/histogram2dcontour/index.js @@ -22,7 +22,7 @@ Histogram2dContour.hoverPoints = require('../contour/hover'); Histogram2dContour.moduleType = 'trace'; Histogram2dContour.name = 'histogram2dcontour'; Histogram2dContour.basePlotModule = require('../../plots/cartesian'); -Histogram2dContour.categories = ['cartesian', '2dMap', 'contour', 'histogram']; +Histogram2dContour.categories = ['cartesian', 'svg', '2dMap', 'contour', 'histogram']; Histogram2dContour.meta = { hrName: 'histogram_2d_contour', description: [ diff --git a/src/traces/ohlc/index.js b/src/traces/ohlc/index.js index d8d2f12f650..cabac0d8568 100644 --- a/src/traces/ohlc/index.js +++ b/src/traces/ohlc/index.js @@ -14,7 +14,7 @@ module.exports = { moduleType: 'trace', name: 'ohlc', basePlotModule: require('../../plots/cartesian'), - categories: ['cartesian', 'showLegend'], + categories: ['cartesian', 'svg', 'showLegend'], meta: { description: [ 'The ohlc (short for Open-High-Low-Close) is a style of financial chart describing', diff --git a/src/traces/parcoords/plot.js b/src/traces/parcoords/plot.js index de08fc5e180..f8a0a485376 100644 --- a/src/traces/parcoords/plot.js +++ b/src/traces/parcoords/plot.js @@ -9,7 +9,7 @@ 'use strict'; var parcoords = require('./parcoords'); -var createRegl = require('regl'); +var prepareRegl = require('../../lib/prepare_regl'); module.exports = function plot(gd, cdparcoords) { var fullLayout = gd._fullLayout; @@ -17,18 +17,7 @@ module.exports = function plot(gd, cdparcoords) { var root = fullLayout._paperdiv; var container = fullLayout._glcontainer; - // make sure proper regl instances are created - fullLayout._glcanvas.each(function(d) { - if(d.regl) return; - d.regl = createRegl({ - canvas: this, - attributes: { - antialias: !d.pick, - preserveDrawingBuffer: true - }, - pixelRatio: gd._context.plotGlPixelRatio || global.devicePixelRatio - }); - }); + prepareRegl(gd); var gdDimensions = {}; var gdDimensionsOriginalOrder = {}; diff --git a/src/traces/pie/base_plot.js b/src/traces/pie/base_plot.js index 081b63616a8..b6dc4549031 100644 --- a/src/traces/pie/base_plot.js +++ b/src/traces/pie/base_plot.js @@ -9,13 +9,13 @@ 'use strict'; var Registry = require('../../registry'); - +var getModuleCalcData = require('../../plots/get_data').getModuleCalcData; exports.name = 'pie'; exports.plot = function(gd) { var Pie = Registry.getModule('pie'); - var cdPie = getCdModule(gd.calcdata, Pie); + var cdPie = getModuleCalcData(gd.calcdata, Pie); if(cdPie.length) Pie.plot(gd, cdPie); }; @@ -28,18 +28,3 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) oldFullLayout._pielayer.selectAll('g.trace').remove(); } }; - -function getCdModule(calcdata, _module) { - var cdModule = []; - - for(var i = 0; i < calcdata.length; i++) { - var cd = calcdata[i]; - var trace = cd[0].trace; - - if((trace._module === _module) && (trace.visible === true)) { - cdModule.push(cd); - } - } - - return cdModule; -} diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js index f551f3f0249..9362e27efd6 100644 --- a/src/traces/scatter/calc.js +++ b/src/traces/scatter/calc.js @@ -75,7 +75,7 @@ function calcAxisExpansion(gd, trace, xa, ya, x, y, ppad) { } // if no error bars, markers or text, or fill to y=0 remove x padding - else if(!trace.error_y.visible && ( + else if(!(trace.error_y || {}).visible && ( ['tonexty', 'tozeroy'].indexOf(trace.fill) !== -1 || (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace)) )) { diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js index 8bca686de6a..3808dcf7628 100644 --- a/src/traces/scatter/index.js +++ b/src/traces/scatter/index.js @@ -34,7 +34,7 @@ Scatter.animatable = true; Scatter.moduleType = 'trace'; Scatter.name = 'scatter'; Scatter.basePlotModule = require('../../plots/cartesian'); -Scatter.categories = ['cartesian', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like']; +Scatter.categories = ['cartesian', 'svg', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like', 'draggedPts']; Scatter.meta = { description: [ 'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.', diff --git a/src/traces/scatter/subtypes.js b/src/traces/scatter/subtypes.js index ddd32ad8547..23bb539b98e 100644 --- a/src/traces/scatter/subtypes.js +++ b/src/traces/scatter/subtypes.js @@ -18,8 +18,11 @@ module.exports = { }, hasMarkers: function(trace) { - return trace.visible && trace.mode && - trace.mode.indexOf('markers') !== -1; + return trace.visible && ( + (trace.mode && trace.mode.indexOf('markers') !== -1) || + // until splom implements 'mode' + trace.type === 'splom' + ); }, hasText: function(trace) { diff --git a/src/traces/scattercarpet/index.js b/src/traces/scattercarpet/index.js index 689f4cedf5f..c8864a6c45d 100644 --- a/src/traces/scattercarpet/index.js +++ b/src/traces/scattercarpet/index.js @@ -23,7 +23,7 @@ ScatterCarpet.eventData = require('./event_data'); ScatterCarpet.moduleType = 'trace'; ScatterCarpet.name = 'scattercarpet'; ScatterCarpet.basePlotModule = require('../../plots/cartesian'); -ScatterCarpet.categories = ['carpet', 'symbols', 'markerColorscale', 'showLegend', 'carpetDependent']; +ScatterCarpet.categories = ['svg', 'carpet', 'symbols', 'markerColorscale', 'showLegend', 'carpetDependent', 'draggedPts']; ScatterCarpet.meta = { hrName: 'scatter_carpet', description: [ diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index dac9fb5b750..8c4d4199438 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -87,7 +87,7 @@ function convertStyle(gd, trace) { } function convertMarkerStyle(trace) { - var count = trace._length || (trace.dimensions || [])._length; + var count = trace._length || trace._commonLength; var optsIn = trace.marker; var optsOut = {}; var i; @@ -401,6 +401,8 @@ function convertErrorBarPositions(gd, trace, positions) { module.exports = { convertStyle: convertStyle, + convertMarkerStyle: convertMarkerStyle, + convertMarkerSelection: convertMarkerSelection, convertLinePositions: convertLinePositions, convertErrorBarPositions: convertErrorBarPositions }; diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index ddebaf3df3b..f8ca803edd5 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -8,7 +8,6 @@ 'use strict'; -var createRegl = require('regl'); var createScatter = require('regl-scatter2d'); var createLine = require('regl-line2d'); var createError = require('regl-error2d'); @@ -17,6 +16,7 @@ var arrayRange = require('array-range'); var Registry = require('../../registry'); var Lib = require('../../lib'); +var prepareRegl = require('../../lib/prepare_regl'); var AxisIDs = require('../../plots/cartesian/axis_ids'); var subTypes = require('../scatter/subtypes'); @@ -280,16 +280,6 @@ function sceneUpdate(gd, subplot) { } }; - // remove selection - scene.clearSelect = function clearSelect() { - if(!scene.selectBatch) return; - scene.selectBatch = null; - scene.unselectBatch = null; - scene.scatter2d.update(scene.markerOptions); - scene.clear(); - scene.draw(); - }; - // remove scene resources scene.destroy = function destroy() { if(scene.fill2d) scene.fill2d.destroy(); @@ -336,20 +326,7 @@ function plot(gd, subplot, cdata) { var width = fullLayout.width; var height = fullLayout.height; - // make sure proper regl instances are created - fullLayout._glcanvas.each(function(d) { - if(d.regl || d.pick) return; - d.regl = createRegl({ - canvas: this, - attributes: { - antialias: !d.pick, - preserveDrawingBuffer: true - }, - extensions: ['ANGLE_instanced_arrays', 'OES_element_index_uint'], - pixelRatio: gd._context.plotGlPixelRatio || global.devicePixelRatio - }); - }); - + prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']); var regl = fullLayout._glcanvas.data()[0].regl; // that is needed for fills @@ -631,9 +608,9 @@ function hoverPoints(pointData, xval, yval, hovermode) { // pick the id closest to the point // note that point possibly may not be found - var minDist = maxDistance; var id, ptx, pty, i, dx, dy, dist, dxy; + var minDist = maxDistance; if(hovermode === 'x') { for(i = 0; i < ids.length; i++) { ptx = x[ids[i]]; @@ -662,9 +639,24 @@ function hoverPoints(pointData, xval, yval, hovermode) { } pointData.index = id; + pointData.distance = minDist; + pointData.dxy = dxy; if(id === undefined) return [pointData]; + calcHover(pointData, x, y, trace); + + return [pointData]; +} + + +function calcHover(pointData, x, y, trace) { + var xa = pointData.xa; + var ya = pointData.ya; + var minDist = pointData.distance; + var dxy = pointData.dxy; + var id = pointData.index; + // the closest data point var di = { pointNumber: id, @@ -750,9 +742,10 @@ function hoverPoints(pointData, xval, yval, hovermode) { fillHoverText(di, trace, pointData); Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, pointData); - return [pointData]; + return pointData; } + function selectPoints(searchInfo, polygon) { var cd = searchInfo.cd; var selection = []; @@ -813,13 +806,19 @@ function selectPoints(searchInfo, polygon) { return selection; } -function style(gd, cd) { - if(cd) { - var stash = cd[0].t; - var scene = stash._scene; +function style(gd, cds) { + if(!cds) return; + + var stash = cds[0][0].t; + var scene = stash._scene; + + // don't clear the subplot if there are splom traces + // on the graph + if(!gd._fullLayout._has('splom')) { scene.clear(); - scene.draw(); } + + scene.draw(); } module.exports = { @@ -840,6 +839,7 @@ module.exports = { sceneOptions: sceneOptions, sceneUpdate: sceneUpdate, + calcHover: calcHover, meta: { hrName: 'scatter_gl', diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js new file mode 100644 index 00000000000..aa73330548d --- /dev/null +++ b/src/traces/splom/attributes.js @@ -0,0 +1,128 @@ +/** +* 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 scatterGlAttrs = require('../scattergl/attributes'); +var cartesianIdRegex = require('../../plots/cartesian/constants').idRegex; + +function makeAxesValObject(axLetter) { + return { + valType: 'info_array', + freeLength: true, + role: 'info', + editType: 'calc', + items: { + valType: 'subplotid', + regex: cartesianIdRegex[axLetter], + editType: 'plot' + }, + description: [ + 'Sets the list of ' + axLetter + ' axes', + 'corresponding to this splom trace.', + 'By default, a splom will match the first N ' + axLetter + 'axes', + 'where N is the number of input dimensions.' + ].join(' ') + }; +} + +module.exports = { + dimensions: { + _isLinkedToArray: 'dimension', + + visible: { + valType: 'boolean', + role: 'info', + dflt: true, + editType: 'calc', + description: [ + 'Determines whether or not this dimension is shown on the graph.', + 'Note that even visible false dimension contribute to the', + 'default grid generate by this splom trace.' + ].join(' ') + }, + label: { + valType: 'string', + role: 'info', + editType: 'calc', + description: 'Sets the label corresponding to this splom dimension.' + }, + values: { + valType: 'data_array', + role: 'info', + editType: 'calc+clearAxisTypes', + description: 'Sets the dimension values to be plotted.' + }, + + // TODO should add an attribute to pin down x only vars and y only vars + // like https://seaborn.pydata.org/generated/seaborn.pairplot.html + // x_vars and y_vars + + // maybe more axis defaulting option e.g. `showgrid: false` + + editType: 'calc+clearAxisTypes' + }, + + // mode: {}, (only 'markers' for now) + + text: scatterGlAttrs.text, + marker: scatterGlAttrs.marker, + + xaxes: makeAxesValObject('x'), + yaxes: makeAxesValObject('y'), + + diagonal: { + visible: { + valType: 'boolean', + role: 'info', + dflt: true, + editType: 'calc', + description: [ + 'Determines whether or not subplots on the diagonal are displayed.' + ].join(' ') + }, + + // type: 'scattergl' | 'histogram' | 'box' | 'violin' + // ... + // more options + + editType: 'calc' + }, + + showupperhalf: { + valType: 'boolean', + role: 'info', + dflt: true, + editType: 'calc', + description: [ + 'Determines whether or not subplots on the upper half', + 'from the diagonal are displayed.' + ].join(' ') + }, + showlowerhalf: { + valType: 'boolean', + role: 'info', + dflt: true, + editType: 'calc', + description: [ + 'Determines whether or not subplots on the lower half', + 'from the diagonal are displayed.' + ].join(' ') + }, + + selected: { + marker: scatterGlAttrs.selected.marker, + editType: 'calc' + }, + unselected: { + marker: scatterGlAttrs.unselected.marker, + editType: 'calc' + }, + + opacity: scatterGlAttrs.opacity +}; diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js new file mode 100644 index 00000000000..73fbc59d11f --- /dev/null +++ b/src/traces/splom/base_plot.js @@ -0,0 +1,240 @@ +/** +* 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 createLine = require('regl-line2d'); + +var Registry = require('../../registry'); +var Lib = require('../../lib'); +var prepareRegl = require('../../lib/prepare_regl'); +var getModuleCalcData = require('../../plots/get_data').getModuleCalcData; +var Cartesian = require('../../plots/cartesian'); +var AxisIDs = require('../../plots/cartesian/axis_ids'); + +var SPLOM = 'splom'; + +function plot(gd) { + var fullLayout = gd._fullLayout; + var _module = Registry.getModule(SPLOM); + var splomCalcData = getModuleCalcData(gd.calcdata, _module); + + prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']); + + if(fullLayout._hasOnlyLargeSploms) { + drawGrid(gd); + } + + _module.plot(gd, {}, splomCalcData); +} + +function drag(gd) { + var cd = gd.calcdata; + var fullLayout = gd._fullLayout; + + if(fullLayout._hasOnlyLargeSploms) { + drawGrid(gd); + } + + for(var i = 0; i < cd.length; i++) { + var cd0 = cd[i][0]; + var trace = cd0.trace; + var scene = cd0.t._scene; + + if(trace.type === 'splom' && scene && scene.matrix) { + dragOne(gd, trace, scene); + } + } +} + +function dragOne(gd, trace, scene) { + var dimensions = trace.dimensions; + var visibleLength = scene.matrixOptions.data.length; + var ranges = new Array(visibleLength); + + for(var i = 0, k = 0; i < dimensions.length; i++) { + if(dimensions[i].visible) { + var rng = ranges[k] = new Array(4); + + var xa = AxisIDs.getFromId(gd, trace._diag[i][0]); + if(xa) { + rng[0] = xa.r2l(xa.range[0]); + rng[2] = xa.r2l(xa.range[1]); + } + + var ya = AxisIDs.getFromId(gd, trace._diag[i][1]); + if(ya) { + rng[1] = ya.r2l(ya.range[0]); + rng[3] = ya.r2l(ya.range[1]); + } + + k++; + } + } + + if(scene.selectBatch) { + scene.matrix.update({ranges: ranges}, {ranges: ranges}); + scene.matrix.draw(scene.unselectBatch, scene.selectBatch); + } else { + scene.matrix.update({ranges: ranges}); + scene.matrix.draw(); + } +} + +function drawGrid(gd) { + var fullLayout = gd._fullLayout; + var regl = fullLayout._glcanvas.data()[0].regl; + var splomGrid = fullLayout._splomGrid; + + if(!splomGrid) { + splomGrid = fullLayout._splomGrid = createLine(regl); + } + + splomGrid.update(makeGridData(gd)); + splomGrid.draw(); +} + +function makeGridData(gd) { + var fullLayout = gd._fullLayout; + var gs = fullLayout._size; + var fullView = [0, 0, fullLayout.width, fullLayout.height]; + var lookup = {}; + var k; + + function push(prefix, ax, x0, x1, y0, y1) { + var lcolor = ax[prefix + 'color']; + var lwidth = ax[prefix + 'width']; + var key = String(lcolor + lwidth); + + if(key in lookup) { + lookup[key].data.push(NaN, NaN, x0, x1, y0, y1); + } else { + lookup[key] = { + data: [x0, x1, y0, y1], + join: 'rect', + thickness: lwidth, + color: lcolor, + viewport: fullView, + range: fullView, + overlay: false + }; + } + } + + for(k in fullLayout._splomSubplots) { + var sp = fullLayout._plots[k]; + var xa = sp.xaxis; + var ya = sp.yaxis; + var xVals = xa._vals; + var yVals = ya._vals; + // ya.l2p assumes top-to-bottom coordinate system (a la SVG), + // we need to compute bottom-to-top offsets and slopes: + var yOffset = gs.b + ya.domain[0] * gs.h; + var ym = -ya._m; + var yb = -ym * ya.r2l(ya.range[0], ya.calendar); + var x, y; + + if(xa.showgrid) { + for(k = 0; k < xVals.length; k++) { + x = xa._offset + xa.l2p(xVals[k].x); + push('grid', xa, x, yOffset, x, yOffset + ya._length); + } + } + if(showZeroLine(xa)) { + x = xa._offset + xa.l2p(0); + push('zeroline', xa, x, yOffset, x, yOffset + ya._length); + } + if(ya.showgrid) { + for(k = 0; k < yVals.length; k++) { + y = yOffset + yb + ym * yVals[k].x; + push('grid', ya, xa._offset, y, xa._offset + xa._length, y); + } + } + if(showZeroLine(ya)) { + y = yOffset + yb + 0; + push('zeroline', ya, xa._offset, y, xa._offset + xa._length, y); + } + } + + var gridBatches = []; + for(k in lookup) { + gridBatches.push(lookup[k]); + } + + return gridBatches; +} + +// just like in Axes.doTicks but without the loop over traces +function showZeroLine(ax) { + var rng = Lib.simpleMap(ax.range, ax.r2l); + var p0 = ax.l2p(0); + + return ( + ax.zeroline && + ax._vals && ax._vals.length && + (rng[0] * rng[1] <= 0) && + (ax.type === 'linear' || ax.type === '-') && + ((p0 > 1 && p0 < ax._length - 1) || !ax.showline) + ); +} + +function clean(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata) { + var oldModules = oldFullLayout._modules || []; + var newModules = newFullLayout._modules || []; + + var hadSplom, hasSplom; + var i; + + for(i = 0; i < oldModules.length; i++) { + if(oldModules[i].name === 'splom') { + hadSplom = true; + break; + } + } + for(i = 0; i < newModules.length; i++) { + if(newModules[i].name === 'splom') { + hasSplom = true; + break; + } + } + + if(hadSplom && !hasSplom) { + for(i = 0; i < oldCalcdata.length; i++) { + var cd0 = oldCalcdata[i][0]; + var trace = cd0.trace; + var scene = cd0.t._scene; + + if(trace.type === 'splom' && scene && scene.matrix) { + scene.matrix.destroy(); + cd0.t._scene = null; + } + } + } + + if(oldFullLayout._splomGrid && + (!newFullLayout._hasOnlyLargeSploms && oldFullLayout._hasOnlyLargeSploms)) { + oldFullLayout._splomGrid.destroy(); + oldFullLayout._splomGrid = null; + } + + Cartesian.clean(newFullData, newFullLayout, oldFullData, oldFullLayout); +} + +module.exports = { + name: SPLOM, + attr: Cartesian.attr, + attrRegex: Cartesian.attrRegex, + layoutAttributes: Cartesian.layoutAttributes, + supplyLayoutDefaults: Cartesian.supplyLayoutDefaults, + drawFramework: Cartesian.drawFramework, + plot: plot, + drag: drag, + clean: clean, + updateFx: Cartesian.updateFx, + toSVG: Cartesian.toSVG +}; diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js new file mode 100644 index 00000000000..aceab9290dd --- /dev/null +++ b/src/traces/splom/defaults.js @@ -0,0 +1,182 @@ +/** +* 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 attributes = require('./attributes'); +var subTypes = require('../scatter/subtypes'); +var handleMarkerDefaults = require('../scatter/marker_defaults'); +var OPEN_RE = /-open/; + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var dimLength = handleDimensionsDefaults(traceIn, traceOut); + + var showDiag = coerce('diagonal.visible'); + var showUpper = coerce('showupperhalf'); + var showLower = coerce('showlowerhalf'); + + if(!dimLength || (!showDiag && !showUpper && !showLower)) { + traceOut.visible = false; + return; + } + + coerce('text'); + + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + + var isOpen = OPEN_RE.test(traceOut.marker.symbol); + var isBubble = subTypes.isBubble(traceOut); + coerce('marker.line.width', isOpen || isBubble ? 1 : 0); + + handleAxisDefaults(traceIn, traceOut, layout, coerce); + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); +}; + +function handleDimensionsDefaults(traceIn, traceOut) { + var dimensionsIn = traceIn.dimensions; + if(!Array.isArray(dimensionsIn)) return 0; + + var dimLength = dimensionsIn.length; + var commonLength = 0; + var dimensionsOut = traceOut.dimensions = new Array(dimLength); + var dimIn; + var dimOut; + var i; + + function coerce(attr, dflt) { + return Lib.coerce(dimIn, dimOut, attributes.dimensions, attr, dflt); + } + + for(i = 0; i < dimLength; i++) { + dimIn = dimensionsIn[i]; + dimOut = dimensionsOut[i] = {}; + + // coerce label even if dimensions may be `visible: false`, + // to fill in axis title defaults + coerce('label'); + + // wait until plot step to filter out visible false dimensions + var visible = coerce('visible'); + if(!visible) continue; + + var values = coerce('values'); + if(!values || !values.length) { + dimOut.visible = false; + continue; + } + + commonLength = Math.max(commonLength, values.length); + dimOut._index = i; + } + + for(i = 0; i < dimLength; i++) { + dimOut = dimensionsOut[i]; + if(dimOut.visible) dimOut._length = commonLength; + } + + traceOut._commonLength = commonLength; + + return dimensionsOut.length; +} + +function handleAxisDefaults(traceIn, traceOut, layout, coerce) { + var dimensions = traceOut.dimensions; + var dimLength = dimensions.length; + var showUpper = traceOut.showupperhalf; + var showLower = traceOut.showlowerhalf; + var showDiag = traceOut.diagonal.visible; + var i, j; + + // N.B. one less x axis AND one less y axis when hiding one half and the diagonal + var axDfltLength = !showDiag && (!showUpper || !showLower) ? dimLength - 1 : dimLength; + + var xaxes = coerce('xaxes', fillAxisIdArray('x', axDfltLength)); + var yaxes = coerce('yaxes', fillAxisIdArray('y', axDfltLength)); + + // to avoid costly indexOf + traceOut._xaxes = arrayToHashObject(xaxes); + traceOut._yaxes = arrayToHashObject(yaxes); + + // allow users to under-specify number of axes + var axLength = Math.min(axDfltLength, xaxes.length, yaxes.length); + + // fill in splom subplot keys + for(i = 0; i < axLength; i++) { + for(j = 0; j < axLength; j++) { + var id = [xaxes[i] + yaxes[j]]; + + if(i > j && showUpper) { + layout._splomSubplots[id] = 1; + } else if(i < j && showLower) { + layout._splomSubplots[id] = 1; + } else if(i === j && (showDiag || !showLower || !showUpper)) { + // need to include diagonal subplots when + // hiding one half and the diagonal + layout._splomSubplots[id] = 1; + } + } + } + + // build list of [x,y] axis corresponding to each dimensions[i], + // very useful for passing options to regl-splom + var diag = traceOut._diag = new Array(dimLength); + + // cases where showDiag and showLower or showUpper are false + // no special treatment as the xaxes and yaxes items no longer match + // the dimensions items 1-to-1 + var xShift = !showDiag && !showLower ? -1 : 0; + var yShift = !showDiag && !showUpper ? -1 : 0; + + for(i = 0; i < dimLength; i++) { + var dim = dimensions[i]; + var xa = xaxes[i + xShift]; + var ya = yaxes[i + yShift]; + + fillAxisStash(layout, xa, dim); + fillAxisStash(layout, ya, dim); + + // note that some the entries here may be undefined + diag[i] = [xa, ya]; + } +} + +function fillAxisIdArray(axLetter, len) { + var out = new Array(len); + + for(var i = 0; i < len; i++) { + out[i] = axLetter + (i ? i + 1 : ''); + } + + return out; +} + +function fillAxisStash(layout, axId, dim) { + if(!axId) return; + + var axLetter = axId.charAt(0); + var stash = layout._splomAxes[axLetter]; + + if(!(axId in stash)) { + stash[axId] = (dim || {}).label || ''; + } +} + +function arrayToHashObject(arr) { + var obj = {}; + for(var i = 0; i < arr.length; i++) { + obj[arr[i]] = 1; + } + return obj; +} diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js new file mode 100644 index 00000000000..3d9feff2f4b --- /dev/null +++ b/src/traces/splom/index.js @@ -0,0 +1,488 @@ +/** +* 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 createMatrix = require('regl-splom'); +var arrayRange = require('array-range'); + +var Registry = require('../../registry'); +var Grid = require('../../components/grid'); +var Lib = require('../../lib'); +var AxisIDs = require('../../plots/cartesian/axis_ids'); + +var subTypes = require('../scatter/subtypes'); +var calcMarkerSize = require('../scatter/calc').calcMarkerSize; +var calcAxisExpansion = require('../scatter/calc').calcAxisExpansion; +var calcColorscales = require('../scatter/colorscale_calc'); +var convertMarkerSelection = require('../scattergl/convert').convertMarkerSelection; +var convertMarkerStyle = require('../scattergl/convert').convertMarkerStyle; +var calcHover = require('../scattergl').calcHover; + +var BADNUM = require('../../constants/numerical').BADNUM; +var TOO_MANY_POINTS = require('../scattergl/constants').TOO_MANY_POINTS; + +function calc(gd, trace) { + var dimensions = trace.dimensions; + var commonLength = trace._commonLength; + var stash = {}; + var opts = {}; + // 'c' for calculated, 'l' for linear, + // only differ here for log axes, pass ldata to createMatrix as 'data' + var cdata = opts.cdata = []; + var ldata = opts.data = []; + var i, k, dim; + + for(i = 0; i < dimensions.length; i++) { + dim = dimensions[i]; + + if(dim.visible) { + var axId = trace._diag[i][0] || trace._diag[i][1]; + var ax = AxisIDs.getFromId(gd, axId); + if(ax) { + var ccol = makeCalcdata(ax, trace, dim); + var lcol = ax.type === 'log' ? Lib.simpleMap(ccol, ax.c2l) : ccol; + cdata.push(ccol); + ldata.push(lcol); + } + } + } + + calcColorscales(trace); + Lib.extendFlat(opts, convertMarkerStyle(trace)); + + var visibleLength = cdata.length; + var hasTooManyPoints = (visibleLength * commonLength) > TOO_MANY_POINTS; + + for(i = 0, k = 0; i < dimensions.length; i++) { + dim = dimensions[i]; + + if(dim.visible) { + var xa = AxisIDs.getFromId(gd, trace._diag[i][0]) || {}; + var ya = AxisIDs.getFromId(gd, trace._diag[i][1]) || {}; + + // Re-use SVG scatter axis expansion routine except + // for graph with very large number of points where it + // performs poorly. + // In big data case, fake Axes.expand outputs with data bounds, + // and an average size for array marker.size inputs. + var ppad; + if(hasTooManyPoints) { + ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3)); + } else { + ppad = calcMarkerSize(trace, commonLength); + } + + calcAxisExpansion(gd, trace, xa, ya, cdata[k], cdata[k], ppad); + k++; + } + } + + var scene = stash._scene = sceneUpdate(gd, stash); + if(!scene.matrix) scene.matrix = true; + scene.matrixOptions = opts; + + scene.selectedOptions = convertMarkerSelection(trace, trace.selected); + scene.unselectedOptions = convertMarkerSelection(trace, trace.unselected); + + return [{x: false, y: false, t: stash, trace: trace}]; +} + +function makeCalcdata(ax, trace, dim) { + // call makeCalcdata with fake input + var ccol = ax.makeCalcdata({ + v: dim.values, + vcalendar: trace.calendar + }, 'v'); + + for(var i = 0; i < ccol.length; i++) { + ccol[i] = ccol[i] === BADNUM ? NaN : ccol[i]; + } + + return ccol; +} + +function sceneUpdate(gd, stash) { + var scene = stash._scene; + + var reset = { + dirty: true + }; + + var first = { + selectBatch: null, + unselectBatch: null, + matrix: false, + select: null + }; + + if(!scene) { + scene = stash._scene = Lib.extendFlat({}, reset, first); + + scene.draw = function draw() { + // draw traces in selection mode + if(scene.matrix && scene.selectBatch) { + scene.matrix.draw(scene.unselectBatch, scene.selectBatch); + } + + else if(scene.matrix) { + scene.matrix.draw(); + } + + scene.dirty = false; + }; + + // remove scene resources + scene.destroy = function destroy() { + if(scene.matrix) scene.matrix.destroy(); + + scene.matrixOptions = null; + scene.selectBatch = null; + scene.unselectBatch = null; + + stash._scene = null; + }; + } + + // In case if we have scene from the last calc - reset data + if(!scene.dirty) { + Lib.extendFlat(scene, reset); + } + + return scene; +} + +function plot(gd, _, splomCalcData) { + if(!splomCalcData.length) return; + + for(var i = 0; i < splomCalcData.length; i++) { + plotOne(gd, splomCalcData[i][0]); + } +} + +function plotOne(gd, cd0) { + var fullLayout = gd._fullLayout; + var gs = fullLayout._size; + var trace = cd0.trace; + var stash = cd0.t; + var scene = stash._scene; + var matrixOpts = scene.matrixOptions; + var cdata = matrixOpts.cdata; + var regl = fullLayout._glcanvas.data()[0].regl; + var dragmode = fullLayout.dragmode; + var xa, ya; + var i, j, k; + + if(cdata.length === 0) return; + + // augment options with proper upper/lower halves + // regl-splom's default grid starts from bottom-left + matrixOpts.lower = trace.showupperhalf; + matrixOpts.upper = trace.showlowerhalf; + matrixOpts.diagonal = trace.diagonal.visible; + + var dimensions = trace.dimensions; + var visibleLength = cdata.length; + var viewOpts = {}; + viewOpts.ranges = new Array(visibleLength); + viewOpts.domains = new Array(visibleLength); + + for(i = 0, k = 0; i < dimensions.length; i++) { + if(trace.dimensions[i].visible) { + var rng = viewOpts.ranges[k] = new Array(4); + var dmn = viewOpts.domains[k] = new Array(4); + + xa = AxisIDs.getFromId(gd, trace._diag[i][0]); + if(xa) { + rng[0] = xa._rl[0]; + rng[2] = xa._rl[1]; + dmn[0] = xa.domain[0]; + dmn[2] = xa.domain[1]; + } + + ya = AxisIDs.getFromId(gd, trace._diag[i][1]); + if(ya) { + rng[1] = ya._rl[0]; + rng[3] = ya._rl[1]; + dmn[1] = ya.domain[0]; + dmn[3] = ya.domain[1]; + } + + k++; + } + } + + viewOpts.viewport = [gs.l, gs.b, gs.w + gs.l, gs.h + gs.b]; + + if(scene.matrix === true) { + scene.matrix = createMatrix(regl); + } + + var selectMode = dragmode === 'lasso' || dragmode === 'select' || !!trace.selectedpoints; + scene.selectBatch = null; + scene.unselectBatch = null; + + if(selectMode) { + var commonLength = trace._commonLength; + + if(!scene.selectBatch) { + scene.selectBatch = []; + scene.unselectBatch = []; + } + + // regenerate scene batch, if traces number changed during selection + if(trace.selectedpoints) { + scene.selectBatch = trace.selectedpoints; + + var selPts = trace.selectedpoints; + var selDict = {}; + for(i = 0; i < selPts.length; i++) { + selDict[selPts[i]] = true; + } + var unselPts = []; + for(i = 0; i < commonLength; i++) { + if(!selDict[i]) unselPts.push(i); + } + scene.unselectBatch = unselPts; + } + + // precalculate px coords since we are not going to pan during select + var xpx = stash.xpx = new Array(visibleLength); + var ypx = stash.ypx = new Array(visibleLength); + + for(i = 0, k = 0; i < dimensions.length; i++) { + if(trace.dimensions[i].visible) { + xa = AxisIDs.getFromId(gd, trace._diag[i][0]); + if(xa) { + xpx[k] = new Array(commonLength); + for(j = 0; j < commonLength; j++) { + xpx[k][j] = xa.c2p(cdata[k][j]); + } + } + + ya = AxisIDs.getFromId(gd, trace._diag[i][1]); + if(ya) { + ypx[k] = new Array(commonLength); + for(j = 0; j < commonLength; j++) { + ypx[k][j] = ya.c2p(cdata[k][j]); + } + } + + k++; + } + } + + if(scene.selectBatch) { + scene.matrix.update(matrixOpts, matrixOpts); + scene.matrix.update(scene.unselectedOptions, scene.selectedOptions); + scene.matrix.update(viewOpts, viewOpts); + } + else { + // delete selection pass + scene.matrix.update(viewOpts, null); + } + } + else { + scene.matrix.update(matrixOpts); + scene.matrix.update(viewOpts); + stash.xpx = stash.ypx = null; + } + + scene.draw(); +} + +function hoverPoints(pointData, xval, yval) { + var cd = pointData.cd; + var trace = cd[0].trace; + var stash = cd[0].t; + var scene = stash._scene; + var cdata = scene.matrixOptions.cdata; + var xa = pointData.xa; + var ya = pointData.ya; + var xpx = xa.c2p(xval); + var ypx = ya.c2p(yval); + var maxDistance = pointData.distance; + + var xi = getDimIndex(trace, xa); + var yi = getDimIndex(trace, ya); + if(xi === false || yi === false) return [pointData]; + + var x = cdata[xi]; + var y = cdata[yi]; + + var id, dxy; + var minDist = maxDistance; + + for(var i = 0; i < x.length; i++) { + var ptx = x[i]; + var pty = y[i]; + var dx = xa.c2p(ptx) - xpx; + var dy = ya.c2p(pty) - ypx; + var dist = Math.sqrt(dx * dx + dy * dy); + + if(dist < minDist) { + minDist = dxy = dist; + id = i; + } + } + + pointData.index = id; + pointData.distance = minDist; + pointData.dxy = dxy; + + if(id === undefined) return [pointData]; + + calcHover(pointData, x, y, trace); + + return [pointData]; +} + +function selectPoints(searchInfo, polygon) { + var cd = searchInfo.cd; + var trace = cd[0].trace; + var stash = cd[0].t; + var scene = stash._scene; + var cdata = scene.matrixOptions.cdata; + var xa = searchInfo.xaxis; + var ya = searchInfo.yaxis; + var selection = []; + var i; + + if(!scene) return selection; + + var hasOnlyLines = (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace)); + if(trace.visible !== true || hasOnlyLines) return selection; + + var xi = getDimIndex(trace, xa); + var yi = getDimIndex(trace, ya); + if(xi === false || yi === false) return selection; + + var xpx = stash.xpx[xi]; + var ypx = stash.ypx[yi]; + var x = cdata[xi]; + var y = cdata[yi]; + + // degenerate polygon does not enable selection + // filter out points by visible scatter ones + var els = null; + var unels = null; + if(polygon !== false && !polygon.degenerate) { + els = [], unels = []; + for(i = 0; i < x.length; i++) { + if(polygon.contains([xpx[i], ypx[i]])) { + els.push(i); + selection.push({ + pointNumber: i, + x: x[i], + y: y[i] + }); + } + else { + unels.push(i); + } + } + } else { + unels = arrayRange(stash.count); + } + + // make sure selectBatch is created + if(!scene.selectBatch) { + scene.selectBatch = []; + scene.unselectBatch = []; + } + + if(!scene.selectBatch) { + // enter every trace select mode + for(i = 0; i < scene.count; i++) { + scene.selectBatch = []; + scene.unselectBatch = []; + } + // we should turn scatter2d into unselected once we have any points selected + scene.matrix.update(scene.unselectedOptions, scene.selectedOptions); + } + + scene.selectBatch = els; + scene.unselectBatch = unels; + + + return selection; +} + +function style(gd, cds) { + if(!cds) return; + + var fullLayout = gd._fullLayout; + var cd0 = cds[0]; + var scene0 = cd0[0].t._scene; + scene0.matrix.regl.clear({color: true, depth: true}); + + if(fullLayout._splomGrid) { + fullLayout._splomGrid.draw(); + } + + for(var i = 0; i < cds.length; i++) { + var scene = cds[i][0].t._scene; + scene.draw(); + } + + // redraw all subplot with scattergl traces, + // as we cleared the whole canvas above + if(fullLayout._has('cartesian')) { + for(var k in fullLayout._plots) { + var sp = fullLayout._plots[k]; + if(sp._scene) sp._scene.draw(); + } + } +} + +function getDimIndex(trace, ax) { + var axId = ax._id; + var axLetter = axId.charAt(0); + var ind = {x: 0, y: 1}[axLetter]; + var dimensions = trace.dimensions; + + for(var i = 0, k = 0; i < dimensions.length; i++) { + if(dimensions[i].visible) { + if(trace._diag[i][ind] === axId) return k; + k++; + } + } + return false; +} + +module.exports = { + moduleType: 'trace', + name: 'splom', + + basePlotModule: require('./base_plot'), + categories: ['gl', 'regl', 'cartesian', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'], + + attributes: require('./attributes'), + supplyDefaults: require('./defaults'), + + calc: calc, + plot: plot, + hoverPoints: hoverPoints, + selectPoints: selectPoints, + style: style, + + meta: { + description: [ + 'Splom traces generate scatter plot matrix visualizations.', + 'Each splom `dimensions` items correspond to a generated axis.', + 'Values for each of those dimensions are set in `dimensions[i].values`.', + 'Splom traces support all `scattergl` marker style attributes.', + 'Specify `layout.grid` attributes and/or layout x-axis and y-axis attributes', + 'for more control over the axis positioning and style. ' + ].join(' ') + } +}; + +// splom traces use the 'grid' component to generate their axes, +// register it here +Registry.register(Grid); diff --git a/src/traces/violin/index.js b/src/traces/violin/index.js index c97355078d2..26e39f644f6 100644 --- a/src/traces/violin/index.js +++ b/src/traces/violin/index.js @@ -23,7 +23,7 @@ module.exports = { moduleType: 'trace', name: 'violin', basePlotModule: require('../../plots/cartesian'), - categories: ['cartesian', 'symbols', 'oriented', 'box-violin', 'showLegend'], + categories: ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'draggedPts'], meta: { description: [ 'In vertical (horizontal) violin plots,', diff --git a/tasks/baseline.js b/tasks/baseline.js index 2e6f666a883..707ce8a74be 100644 --- a/tasks/baseline.js +++ b/tasks/baseline.js @@ -14,4 +14,6 @@ var cmd = containerCommands.getRunCmd( ); console.log(msg); -common.execCmd(cmd); +common.execCmd(containerCommands.ping, function() { + common.execCmd(cmd); +}); diff --git a/test/image/baselines/splom_0.png b/test/image/baselines/splom_0.png new file mode 100644 index 00000000000..e4729fe2691 Binary files /dev/null and b/test/image/baselines/splom_0.png differ diff --git a/test/image/baselines/splom_array-styles.png b/test/image/baselines/splom_array-styles.png new file mode 100644 index 00000000000..d608b2f9d40 Binary files /dev/null and b/test/image/baselines/splom_array-styles.png differ diff --git a/test/image/baselines/splom_dates.png b/test/image/baselines/splom_dates.png new file mode 100644 index 00000000000..fa6cfc8aa57 Binary files /dev/null and b/test/image/baselines/splom_dates.png differ diff --git a/test/image/baselines/splom_iris.png b/test/image/baselines/splom_iris.png new file mode 100644 index 00000000000..a97f4c5b85a Binary files /dev/null and b/test/image/baselines/splom_iris.png differ diff --git a/test/image/baselines/splom_large.png b/test/image/baselines/splom_large.png new file mode 100644 index 00000000000..1fcf729c532 Binary files /dev/null and b/test/image/baselines/splom_large.png differ diff --git a/test/image/baselines/splom_log.png b/test/image/baselines/splom_log.png new file mode 100644 index 00000000000..e32063f9f3e Binary files /dev/null and b/test/image/baselines/splom_log.png differ diff --git a/test/image/baselines/splom_lower-nodiag.png b/test/image/baselines/splom_lower-nodiag.png new file mode 100644 index 00000000000..1a4f8f587ac Binary files /dev/null and b/test/image/baselines/splom_lower-nodiag.png differ diff --git a/test/image/baselines/splom_lower.png b/test/image/baselines/splom_lower.png new file mode 100644 index 00000000000..d3a4518a7bf Binary files /dev/null and b/test/image/baselines/splom_lower.png differ diff --git a/test/image/baselines/splom_ragged-via-axes.png b/test/image/baselines/splom_ragged-via-axes.png new file mode 100644 index 00000000000..b3a54479fa2 Binary files /dev/null and b/test/image/baselines/splom_ragged-via-axes.png differ diff --git a/test/image/baselines/splom_ragged-via-visible-false.png b/test/image/baselines/splom_ragged-via-visible-false.png new file mode 100644 index 00000000000..8b95fff9ffb Binary files /dev/null and b/test/image/baselines/splom_ragged-via-visible-false.png differ diff --git a/test/image/baselines/splom_upper-nodiag.png b/test/image/baselines/splom_upper-nodiag.png new file mode 100644 index 00000000000..9398120da17 Binary files /dev/null and b/test/image/baselines/splom_upper-nodiag.png differ diff --git a/test/image/baselines/splom_upper.png b/test/image/baselines/splom_upper.png new file mode 100644 index 00000000000..2d955d8643d Binary files /dev/null and b/test/image/baselines/splom_upper.png differ diff --git a/test/image/baselines/splom_with-cartesian.png b/test/image/baselines/splom_with-cartesian.png new file mode 100644 index 00000000000..db20b630af7 Binary files /dev/null and b/test/image/baselines/splom_with-cartesian.png differ diff --git a/test/image/mocks/splom_0.json b/test/image/mocks/splom_0.json new file mode 100644 index 00000000000..4bd69a7d75e --- /dev/null +++ b/test/image/mocks/splom_0.json @@ -0,0 +1,12 @@ +{ + "data": [{ + "type": "splom", + "dimensions": [{ + "values": [1, 2, 3], + "label": "A" + }, { + "values": [2, 5, 6], + "label": "B" + }] + }] +} diff --git a/test/image/mocks/splom_array-styles.json b/test/image/mocks/splom_array-styles.json new file mode 100644 index 00000000000..1c59d79a657 --- /dev/null +++ b/test/image/mocks/splom_array-styles.json @@ -0,0 +1,22 @@ +{ + "data": [{ + "type": "splom", + "dimensions": [{ + "values": [1, 2, 3], + "label": "A" + }, { + "values": [2, 5, 6], + "label": "B" + }], + "marker": { + "symbol": ["diamond", "cross", "square"], + "color": ["green", "blue", "red"], + "size": [20, 40, 10], + "line": { + "width": [2, 0, 3], + "color": ["red", "", "blue"] + }, + "opacity": [1, 0.8, 0.6] + } + }] +} diff --git a/test/image/mocks/splom_dates.json b/test/image/mocks/splom_dates.json new file mode 100644 index 00000000000..33be6f2bd8f --- /dev/null +++ b/test/image/mocks/splom_dates.json @@ -0,0 +1,22 @@ +{ + "data": [{ + "type": "splom", + "dimensions": [{ + "values": ["2000-01-01", "2001-02-01", "2010-10-03"], + "label": "A" + }, { + "values": ["2003-04-21", "2012-02-01", "2005-10-03"], + "label": "B" + }], + "marker": { + "symbol": "cross" + }, + "selected": { + "marker": {"color": "green"} + }, + "unselected": { + "marker": {"color": "red"} + }, + "selectedpoints": [0, 2] + }] +} diff --git a/test/image/mocks/splom_iris.json b/test/image/mocks/splom_iris.json new file mode 100644 index 00000000000..49d25615c23 --- /dev/null +++ b/test/image/mocks/splom_iris.json @@ -0,0 +1,696 @@ +{ + "data": [ + { + "type": "splom", + "name": "Setosa", + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "5.1", + "4.9", + "4.7", + "4.6", + "5.0", + "5.4", + "4.6", + "5.0", + "4.4", + "4.9", + "5.4", + "4.8", + "4.8", + "4.3", + "5.8", + "5.7", + "5.4", + "5.1", + "5.7", + "5.1", + "5.4", + "5.1", + "4.6", + "5.1", + "4.8", + "5.0", + "5.0", + "5.2", + "5.2", + "4.7", + "4.8", + "5.4", + "5.2", + "5.5", + "4.9", + "5.0", + "5.5", + "4.9", + "4.4", + "5.1", + "5.0", + "4.5", + "4.4", + "5.0", + "5.1", + "4.8", + "5.1", + "4.6", + "5.3", + "5.0" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.5", + "3.0", + "3.2", + "3.1", + "3.6", + "3.9", + "3.4", + "3.4", + "2.9", + "3.1", + "3.7", + "3.4", + "3.0", + "3.0", + "4.0", + "4.4", + "3.9", + "3.5", + "3.8", + "3.8", + "3.4", + "3.7", + "3.6", + "3.3", + "3.4", + "3.0", + "3.4", + "3.5", + "3.4", + "3.2", + "3.1", + "3.4", + "4.1", + "4.2", + "3.1", + "3.2", + "3.5", + "3.1", + "3.0", + "3.4", + "3.5", + "2.3", + "3.2", + "3.5", + "3.8", + "3.0", + "3.8", + "3.2", + "3.7", + "3.3" + ] + }, + { + "label": "PetalLength", + "values": [ + "1.4", + "1.4", + "1.3", + "1.5", + "1.4", + "1.7", + "1.4", + "1.5", + "1.4", + "1.5", + "1.5", + "1.6", + "1.4", + "1.1", + "1.2", + "1.5", + "1.3", + "1.4", + "1.7", + "1.5", + "1.7", + "1.5", + "1.0", + "1.7", + "1.9", + "1.6", + "1.6", + "1.5", + "1.4", + "1.6", + "1.6", + "1.5", + "1.5", + "1.4", + "1.5", + "1.2", + "1.3", + "1.5", + "1.3", + "1.5", + "1.3", + "1.3", + "1.3", + "1.6", + "1.9", + "1.4", + "1.6", + "1.4", + "1.5", + "1.4" + ] + }, + { + "label": "PetalWidth", + "values": [ + "0.2", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.3", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.1", + "0.2", + "0.4", + "0.4", + "0.3", + "0.3", + "0.3", + "0.2", + "0.4", + "0.2", + "0.5", + "0.2", + "0.2", + "0.4", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.1", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.3", + "0.3", + "0.2", + "0.6", + "0.4", + "0.3", + "0.2", + "0.2", + "0.2", + "0.2" + ] + } + ], + "marker": { + "color": "red" + } + }, + { + "type": "splom", + "name": "Versicolor", + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "7.0", + "6.4", + "6.9", + "5.5", + "6.5", + "5.7", + "6.3", + "4.9", + "6.6", + "5.2", + "5.0", + "5.9", + "6.0", + "6.1", + "5.6", + "6.7", + "5.6", + "5.8", + "6.2", + "5.6", + "5.9", + "6.1", + "6.3", + "6.1", + "6.4", + "6.6", + "6.8", + "6.7", + "6.0", + "5.7", + "5.5", + "5.5", + "5.8", + "6.0", + "5.4", + "6.0", + "6.7", + "6.3", + "5.6", + "5.5", + "5.5", + "6.1", + "5.8", + "5.0", + "5.6", + "5.7", + "5.7", + "6.2", + "5.1", + "5.7" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.2", + "3.2", + "3.1", + "2.3", + "2.8", + "2.8", + "3.3", + "2.4", + "2.9", + "2.7", + "2.0", + "3.0", + "2.2", + "2.9", + "2.9", + "3.1", + "3.0", + "2.7", + "2.2", + "2.5", + "3.2", + "2.8", + "2.5", + "2.8", + "2.9", + "3.0", + "2.8", + "3.0", + "2.9", + "2.6", + "2.4", + "2.4", + "2.7", + "2.7", + "3.0", + "3.4", + "3.1", + "2.3", + "3.0", + "2.5", + "2.6", + "3.0", + "2.6", + "2.3", + "2.7", + "3.0", + "2.9", + "2.9", + "2.5", + "2.8" + ] + }, + { + "label": "PetalLength", + "values": [ + "4.7", + "4.5", + "4.9", + "4.0", + "4.6", + "4.5", + "4.7", + "3.3", + "4.6", + "3.9", + "3.5", + "4.2", + "4.0", + "4.7", + "3.6", + "4.4", + "4.5", + "4.1", + "4.5", + "3.9", + "4.8", + "4.0", + "4.9", + "4.7", + "4.3", + "4.4", + "4.8", + "5.0", + "4.5", + "3.5", + "3.8", + "3.7", + "3.9", + "5.1", + "4.5", + "4.5", + "4.7", + "4.4", + "4.1", + "4.0", + "4.4", + "4.6", + "4.0", + "3.3", + "4.2", + "4.2", + "4.2", + "4.3", + "3.0", + "4.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "1.4", + "1.5", + "1.5", + "1.3", + "1.5", + "1.3", + "1.6", + "1.0", + "1.3", + "1.4", + "1.0", + "1.5", + "1.0", + "1.4", + "1.3", + "1.4", + "1.5", + "1.0", + "1.5", + "1.1", + "1.8", + "1.3", + "1.5", + "1.2", + "1.3", + "1.4", + "1.4", + "1.7", + "1.5", + "1.0", + "1.1", + "1.0", + "1.2", + "1.6", + "1.5", + "1.6", + "1.5", + "1.3", + "1.3", + "1.3", + "1.2", + "1.4", + "1.2", + "1.0", + "1.3", + "1.2", + "1.3", + "1.3", + "1.1", + "1.3" + ] + } + ], + "marker": { + "color": "green" + } + }, + { + "type": "splom", + "name": "Virginica", + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "6.3", + "5.8", + "7.1", + "6.3", + "6.5", + "7.6", + "4.9", + "7.3", + "6.7", + "7.2", + "6.5", + "6.4", + "6.8", + "5.7", + "5.8", + "6.4", + "6.5", + "7.7", + "7.7", + "6.0", + "6.9", + "5.6", + "7.7", + "6.3", + "6.7", + "7.2", + "6.2", + "6.1", + "6.4", + "7.2", + "7.4", + "7.9", + "6.4", + "6.3", + "6.1", + "7.7", + "6.3", + "6.4", + "6.0", + "6.9", + "6.7", + "6.9", + "5.8", + "6.8", + "6.7", + "6.7", + "6.3", + "6.5", + "6.2", + "5.9" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.3", + "2.7", + "3.0", + "2.9", + "3.0", + "3.0", + "2.5", + "2.9", + "2.5", + "3.6", + "3.2", + "2.7", + "3.0", + "2.5", + "2.8", + "3.2", + "3.0", + "3.8", + "2.6", + "2.2", + "3.2", + "2.8", + "2.8", + "2.7", + "3.3", + "3.2", + "2.8", + "3.0", + "2.8", + "3.0", + "2.8", + "3.8", + "2.8", + "2.8", + "2.6", + "3.0", + "3.4", + "3.1", + "3.0", + "3.1", + "3.1", + "3.1", + "2.7", + "3.2", + "3.3", + "3.0", + "2.5", + "3.0", + "3.4", + "3.0" + ] + }, + { + "label": "PetalLength", + "values": [ + "6.0", + "5.1", + "5.9", + "5.6", + "5.8", + "6.6", + "4.5", + "6.3", + "5.8", + "6.1", + "5.1", + "5.3", + "5.5", + "5.0", + "5.1", + "5.3", + "5.5", + "6.7", + "6.9", + "5.0", + "5.7", + "4.9", + "6.7", + "4.9", + "5.7", + "6.0", + "4.8", + "4.9", + "5.6", + "5.8", + "6.1", + "6.4", + "5.6", + "5.1", + "5.6", + "6.1", + "5.6", + "5.5", + "4.8", + "5.4", + "5.6", + "5.1", + "5.1", + "5.9", + "5.7", + "5.2", + "5.0", + "5.2", + "5.4", + "5.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "2.5", + "1.9", + "2.1", + "1.8", + "2.2", + "2.1", + "1.7", + "1.8", + "1.8", + "2.5", + "2.0", + "1.9", + "2.1", + "2.0", + "2.4", + "2.3", + "1.8", + "2.2", + "2.3", + "1.5", + "2.3", + "2.0", + "2.0", + "1.8", + "2.1", + "1.8", + "1.8", + "1.8", + "2.1", + "1.6", + "1.9", + "2.0", + "2.2", + "1.5", + "1.4", + "2.3", + "2.4", + "1.8", + "1.8", + "2.1", + "2.4", + "2.3", + "1.9", + "2.3", + "2.5", + "2.3", + "1.9", + "2.0", + "2.3", + "1.8" + ] + } + ], + "marker": { + "color": "blue" + } + } + ], + "layout": { + "title": "Iris dataset splom", + "width": 600, + "height": 500 + } +} diff --git a/test/image/mocks/splom_large.json b/test/image/mocks/splom_large.json new file mode 100644 index 00000000000..1c8ec021e7a --- /dev/null +++ b/test/image/mocks/splom_large.json @@ -0,0 +1,302 @@ +{ + "data": [ + { + "type": "splom", + "dimensions": [ + { + "values": [ + 0.08580232218589545, + 0.6767470576947283, + 0.8399763393645798, + 0.6233479334978567, + 0.08481140931214393, + 0.27902188027625985, + 0.025236798479649547, + 0.34783438974705616, + 0.6285357690300772, + 0.13128833631128067 + ] + }, + { + "values": [ + 0.05246062923863537, + 0.46333116868608726, + 0.16715434507619897, + 0.05018811233733689, + 0.2853876011714973, + 0.7507188960271824, + 0.7414407081329897, + 0.4931548658431939, + 0.008003443608825211, + 0.8401897754160457 + ] + }, + { + "values": [ + 0.06520810239108199, + 0.8495982108318272, + 0.9046045659524642, + 0.5725252028953924, + 0.8553173173083635, + 0.29449164621853474, + 0.7745611908355845, + 0.4757865646903625, + 0.14563435980102013, + 0.897525101750952 + ] + }, + { + "values": [ + 0.435165783447075, + 0.8082148816441925, + 0.31066944105379823, + 0.8166938174345009, + 0.6703382645297264, + 0.18962249374111084, + 0.860080342517918, + 0.5591463874851217, + 0.4826869165499734, + 0.7729727246187266 + ] + }, + { + "values": [ + 0.630098234895871, + 0.10669275996298366, + 0.05951757006824776, + 0.5588158941493411, + 0.1765069271002584, + 0.015520260287421817, + 0.9541158554813636, + 0.66976671303951, + 0.12611169188802474, + 0.32517056116627363 + ] + }, + { + "values": [ + 0.5571953297919265, + 0.08237697621302642, + 0.46356179788018537, + 0.2283577655950968, + 0.671248603613811, + 0.45165109690210636, + 0.8550100301829344, + 0.43670534843721587, + 0.18312610870964297, + 0.17910843367122276 + ] + }, + { + "values": [ + 0.20510914327614138, + 0.6009777248652213, + 0.25303102693529844, + 0.43614098207137664, + 0.2593560264066288, + 0.9335830621077947, + 0.05298398343530342, + 0.6010736830375696, + 0.7180627032220392, + 0.7916282763768157 + ] + }, + { + "values": [ + 0.4701029548510385, + 0.1615863398834989, + 0.27793363854136377, + 0.5006702407502888, + 0.3769713203055185, + 0.3698032504585034, + 0.7368873675721836, + 0.8255155665205895, + 0.5837545175648222, + 0.7362198040889636 + ] + }, + { + "values": [ + 0.3247821731546314, + 0.9700757962151294, + 0.8779166627266124, + 0.9903510723754136, + 0.7783682733815458, + 0.5488486035822286, + 0.4114313577586548, + 0.030434854263728717, + 0.7456544486246504, + 0.8103302611231877 + ] + }, + { + "values": [ + 0.4269295190776543, + 0.1609650068167563, + 0.92842164174826, + 0.6442505274731554, + 0.7334215664663768, + 0.9218766505786056, + 0.44096562168773, + 0.8741879191864952, + 0.5950608359707568, + 0.4529164095766698 + ] + }, + { + "values": [ + 0.6754203197568098, + 0.6808047852168977, + 0.4353488050618257, + 0.35516657099681637, + 0.15142747472405116, + 0.05994585078335124, + 0.727449276326178, + 0.5834772499810623, + 0.141839296932462, + 0.5338457923596154 + ] + }, + { + "values": [ + 0.47302662880016744, + 0.8157932884895502, + 0.4544100891785916, + 0.9704393989173103, + 0.5641930022582622, + 0.24022301089480513, + 0.8516804346875824, + 0.2424559182552377, + 0.9810817730745405, + 0.5722415623556636 + ] + }, + { + "values": [ + 0.9835886244745469, + 0.005823780299727188, + 0.5840877831857845, + 0.40081036928303715, + 0.47573859777702054, + 0.9485985220903039, + 0.7887429836279782, + 0.6610711008500716, + 0.6008170543130837, + 0.2935777438750209 + ] + }, + { + "values": [ + 0.9879737148378489, + 0.9425983110033131, + 0.7892004437897853, + 0.957875505374933, + 0.31571744199972906, + 0.034133515508368184, + 0.7089369307443012, + 0.9566945192342795, + 0.1469133087864285, + 0.38255414983814706 + ] + }, + { + "values": [ + 0.7258788568242582, + 0.15803434660836024, + 0.48035145055604733, + 0.1754872010631272, + 0.0943373904795064, + 0.5284316947074943, + 0.3449912254162446, + 0.4743126200307999, + 0.6169826341413327, + 0.6237454994146361 + ] + }, + { + "values": [ + 0.7014025222418512, + 0.48264213331162287, + 0.5321815314013991, + 0.5095588522475991, + 0.09731577257522939, + 0.3803088428846304, + 0.43210179892978906, + 0.28328046635762205, + 0.5255134053702564, + 0.05773536414283709 + ] + }, + { + "values": [ + 0.06202609532657033, + 0.8665158901026537, + 0.3218485709140262, + 0.49053646244974103, + 0.19532382906181467, + 0.2675695479993139, + 0.5871232161585691, + 0.6760894505442174, + 0.896410114646955, + 0.04000902917396676 + ] + }, + { + "values": [ + 0.6871231252786911, + 0.034405780777586825, + 0.5302260880695147, + 0.37267746064321083, + 0.21779005210840952, + 0.8520888106169335, + 0.3820957228405504, + 0.29543100636475117, + 0.3324100288461711, + 0.6317047124071087 + ] + }, + { + "values": [ + 0.8670401189375461, + 0.7983338320962376, + 0.8367762774071041, + 0.5174816210864217, + 0.007785283032919477, + 0.059056926891100536, + 0.8979079754970136, + 0.14826669761408717, + 0.5356319781873422, + 0.7527907633534512 + ] + }, + { + "values": [ + 0.38348125720617166, + 0.4723378247456569, + 0.9472589658711628, + 0.3536431949921497, + 0.5983873814500404, + 0.49254652667510146, + 0.015100469074888823, + 0.11575656088204078, + 0.21089120172691755, + 0.3102753400426521 + ] + } + ] + } + ], + "layout": { + "width": 1500, + "height": 1500, + "xaxis": { + "dtick": 0.1, + "gridcolor": "cyan", + "gridwidth": 2 + }, + "yaxis": { + "zerolinecolor": "red", + "zerolinewidth": 2 + } + } +} diff --git a/test/image/mocks/splom_log.json b/test/image/mocks/splom_log.json new file mode 100644 index 00000000000..a7adf6f99a0 --- /dev/null +++ b/test/image/mocks/splom_log.json @@ -0,0 +1,24 @@ +{ + "data": [{ + "type": "splom", + "dimensions": [{ + "values": [1e1, 1e2, 1e3], + "label": "A" + }, { + "values": [1e2, 1e5, 1e6], + "label": "B" + }], + "marker": { + "size": 20, + "line": {"width": 2, "color": "#444"}, + "color": [10, 40, 100], + "colorscale": "Reds" + } + }], + "layout": { + "xaxis": {"type": "log"}, + "yaxis": {"type": "log"}, + "xaxis2": {"type": "log"}, + "yaxis2": {"type": "log"} + } +} diff --git a/test/image/mocks/splom_lower-nodiag.json b/test/image/mocks/splom_lower-nodiag.json new file mode 100644 index 00000000000..4ade038cdc1 --- /dev/null +++ b/test/image/mocks/splom_lower-nodiag.json @@ -0,0 +1,706 @@ +{ + "data": [ + { + "type": "splom", + "name": "Setosa", + "showupperhalf": false, + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "5.1", + "4.9", + "4.7", + "4.6", + "5.0", + "5.4", + "4.6", + "5.0", + "4.4", + "4.9", + "5.4", + "4.8", + "4.8", + "4.3", + "5.8", + "5.7", + "5.4", + "5.1", + "5.7", + "5.1", + "5.4", + "5.1", + "4.6", + "5.1", + "4.8", + "5.0", + "5.0", + "5.2", + "5.2", + "4.7", + "4.8", + "5.4", + "5.2", + "5.5", + "4.9", + "5.0", + "5.5", + "4.9", + "4.4", + "5.1", + "5.0", + "4.5", + "4.4", + "5.0", + "5.1", + "4.8", + "5.1", + "4.6", + "5.3", + "5.0" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.5", + "3.0", + "3.2", + "3.1", + "3.6", + "3.9", + "3.4", + "3.4", + "2.9", + "3.1", + "3.7", + "3.4", + "3.0", + "3.0", + "4.0", + "4.4", + "3.9", + "3.5", + "3.8", + "3.8", + "3.4", + "3.7", + "3.6", + "3.3", + "3.4", + "3.0", + "3.4", + "3.5", + "3.4", + "3.2", + "3.1", + "3.4", + "4.1", + "4.2", + "3.1", + "3.2", + "3.5", + "3.1", + "3.0", + "3.4", + "3.5", + "2.3", + "3.2", + "3.5", + "3.8", + "3.0", + "3.8", + "3.2", + "3.7", + "3.3" + ] + }, + { + "label": "PetalLength", + "values": [ + "1.4", + "1.4", + "1.3", + "1.5", + "1.4", + "1.7", + "1.4", + "1.5", + "1.4", + "1.5", + "1.5", + "1.6", + "1.4", + "1.1", + "1.2", + "1.5", + "1.3", + "1.4", + "1.7", + "1.5", + "1.7", + "1.5", + "1.0", + "1.7", + "1.9", + "1.6", + "1.6", + "1.5", + "1.4", + "1.6", + "1.6", + "1.5", + "1.5", + "1.4", + "1.5", + "1.2", + "1.3", + "1.5", + "1.3", + "1.5", + "1.3", + "1.3", + "1.3", + "1.6", + "1.9", + "1.4", + "1.6", + "1.4", + "1.5", + "1.4" + ] + }, + { + "label": "PetalWidth", + "values": [ + "0.2", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.3", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.1", + "0.2", + "0.4", + "0.4", + "0.3", + "0.3", + "0.3", + "0.2", + "0.4", + "0.2", + "0.5", + "0.2", + "0.2", + "0.4", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.1", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.3", + "0.3", + "0.2", + "0.6", + "0.4", + "0.3", + "0.2", + "0.2", + "0.2", + "0.2" + ] + } + ], + "marker": { + "color": "red" + } + }, + { + "type": "splom", + "name": "Versicolor", + "showupperhalf": false, + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "7.0", + "6.4", + "6.9", + "5.5", + "6.5", + "5.7", + "6.3", + "4.9", + "6.6", + "5.2", + "5.0", + "5.9", + "6.0", + "6.1", + "5.6", + "6.7", + "5.6", + "5.8", + "6.2", + "5.6", + "5.9", + "6.1", + "6.3", + "6.1", + "6.4", + "6.6", + "6.8", + "6.7", + "6.0", + "5.7", + "5.5", + "5.5", + "5.8", + "6.0", + "5.4", + "6.0", + "6.7", + "6.3", + "5.6", + "5.5", + "5.5", + "6.1", + "5.8", + "5.0", + "5.6", + "5.7", + "5.7", + "6.2", + "5.1", + "5.7" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.2", + "3.2", + "3.1", + "2.3", + "2.8", + "2.8", + "3.3", + "2.4", + "2.9", + "2.7", + "2.0", + "3.0", + "2.2", + "2.9", + "2.9", + "3.1", + "3.0", + "2.7", + "2.2", + "2.5", + "3.2", + "2.8", + "2.5", + "2.8", + "2.9", + "3.0", + "2.8", + "3.0", + "2.9", + "2.6", + "2.4", + "2.4", + "2.7", + "2.7", + "3.0", + "3.4", + "3.1", + "2.3", + "3.0", + "2.5", + "2.6", + "3.0", + "2.6", + "2.3", + "2.7", + "3.0", + "2.9", + "2.9", + "2.5", + "2.8" + ] + }, + { + "label": "PetalLength", + "values": [ + "4.7", + "4.5", + "4.9", + "4.0", + "4.6", + "4.5", + "4.7", + "3.3", + "4.6", + "3.9", + "3.5", + "4.2", + "4.0", + "4.7", + "3.6", + "4.4", + "4.5", + "4.1", + "4.5", + "3.9", + "4.8", + "4.0", + "4.9", + "4.7", + "4.3", + "4.4", + "4.8", + "5.0", + "4.5", + "3.5", + "3.8", + "3.7", + "3.9", + "5.1", + "4.5", + "4.5", + "4.7", + "4.4", + "4.1", + "4.0", + "4.4", + "4.6", + "4.0", + "3.3", + "4.2", + "4.2", + "4.2", + "4.3", + "3.0", + "4.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "1.4", + "1.5", + "1.5", + "1.3", + "1.5", + "1.3", + "1.6", + "1.0", + "1.3", + "1.4", + "1.0", + "1.5", + "1.0", + "1.4", + "1.3", + "1.4", + "1.5", + "1.0", + "1.5", + "1.1", + "1.8", + "1.3", + "1.5", + "1.2", + "1.3", + "1.4", + "1.4", + "1.7", + "1.5", + "1.0", + "1.1", + "1.0", + "1.2", + "1.6", + "1.5", + "1.6", + "1.5", + "1.3", + "1.3", + "1.3", + "1.2", + "1.4", + "1.2", + "1.0", + "1.3", + "1.2", + "1.3", + "1.3", + "1.1", + "1.3" + ] + } + ], + "marker": { + "color": "green" + } + }, + { + "type": "splom", + "name": "Virginica", + "showupperhalf": false, + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "6.3", + "5.8", + "7.1", + "6.3", + "6.5", + "7.6", + "4.9", + "7.3", + "6.7", + "7.2", + "6.5", + "6.4", + "6.8", + "5.7", + "5.8", + "6.4", + "6.5", + "7.7", + "7.7", + "6.0", + "6.9", + "5.6", + "7.7", + "6.3", + "6.7", + "7.2", + "6.2", + "6.1", + "6.4", + "7.2", + "7.4", + "7.9", + "6.4", + "6.3", + "6.1", + "7.7", + "6.3", + "6.4", + "6.0", + "6.9", + "6.7", + "6.9", + "5.8", + "6.8", + "6.7", + "6.7", + "6.3", + "6.5", + "6.2", + "5.9" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.3", + "2.7", + "3.0", + "2.9", + "3.0", + "3.0", + "2.5", + "2.9", + "2.5", + "3.6", + "3.2", + "2.7", + "3.0", + "2.5", + "2.8", + "3.2", + "3.0", + "3.8", + "2.6", + "2.2", + "3.2", + "2.8", + "2.8", + "2.7", + "3.3", + "3.2", + "2.8", + "3.0", + "2.8", + "3.0", + "2.8", + "3.8", + "2.8", + "2.8", + "2.6", + "3.0", + "3.4", + "3.1", + "3.0", + "3.1", + "3.1", + "3.1", + "2.7", + "3.2", + "3.3", + "3.0", + "2.5", + "3.0", + "3.4", + "3.0" + ] + }, + { + "label": "PetalLength", + "values": [ + "6.0", + "5.1", + "5.9", + "5.6", + "5.8", + "6.6", + "4.5", + "6.3", + "5.8", + "6.1", + "5.1", + "5.3", + "5.5", + "5.0", + "5.1", + "5.3", + "5.5", + "6.7", + "6.9", + "5.0", + "5.7", + "4.9", + "6.7", + "4.9", + "5.7", + "6.0", + "4.8", + "4.9", + "5.6", + "5.8", + "6.1", + "6.4", + "5.6", + "5.1", + "5.6", + "6.1", + "5.6", + "5.5", + "4.8", + "5.4", + "5.6", + "5.1", + "5.1", + "5.9", + "5.7", + "5.2", + "5.0", + "5.2", + "5.4", + "5.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "2.5", + "1.9", + "2.1", + "1.8", + "2.2", + "2.1", + "1.7", + "1.8", + "1.8", + "2.5", + "2.0", + "1.9", + "2.1", + "2.0", + "2.4", + "2.3", + "1.8", + "2.2", + "2.3", + "1.5", + "2.3", + "2.0", + "2.0", + "1.8", + "2.1", + "1.8", + "1.8", + "1.8", + "2.1", + "1.6", + "1.9", + "2.0", + "2.2", + "1.5", + "1.4", + "2.3", + "2.4", + "1.8", + "1.8", + "2.1", + "2.4", + "2.3", + "1.9", + "2.3", + "2.5", + "2.3", + "1.9", + "2.0", + "2.3", + "1.8" + ] + } + ], + "marker": { + "color": "blue" + } + } + ], + "layout": { + "title": "Iris dataset splom", + "width": 600, + "height": 500, + "legend": { + "x": 1, + "xanchor": "right" + } + } +} diff --git a/test/image/mocks/splom_lower.json b/test/image/mocks/splom_lower.json new file mode 100644 index 00000000000..192f1c61c85 --- /dev/null +++ b/test/image/mocks/splom_lower.json @@ -0,0 +1,699 @@ +{ + "data": [ + { + "type": "splom", + "name": "Setosa", + "showupperhalf": false, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "5.1", + "4.9", + "4.7", + "4.6", + "5.0", + "5.4", + "4.6", + "5.0", + "4.4", + "4.9", + "5.4", + "4.8", + "4.8", + "4.3", + "5.8", + "5.7", + "5.4", + "5.1", + "5.7", + "5.1", + "5.4", + "5.1", + "4.6", + "5.1", + "4.8", + "5.0", + "5.0", + "5.2", + "5.2", + "4.7", + "4.8", + "5.4", + "5.2", + "5.5", + "4.9", + "5.0", + "5.5", + "4.9", + "4.4", + "5.1", + "5.0", + "4.5", + "4.4", + "5.0", + "5.1", + "4.8", + "5.1", + "4.6", + "5.3", + "5.0" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.5", + "3.0", + "3.2", + "3.1", + "3.6", + "3.9", + "3.4", + "3.4", + "2.9", + "3.1", + "3.7", + "3.4", + "3.0", + "3.0", + "4.0", + "4.4", + "3.9", + "3.5", + "3.8", + "3.8", + "3.4", + "3.7", + "3.6", + "3.3", + "3.4", + "3.0", + "3.4", + "3.5", + "3.4", + "3.2", + "3.1", + "3.4", + "4.1", + "4.2", + "3.1", + "3.2", + "3.5", + "3.1", + "3.0", + "3.4", + "3.5", + "2.3", + "3.2", + "3.5", + "3.8", + "3.0", + "3.8", + "3.2", + "3.7", + "3.3" + ] + }, + { + "label": "PetalLength", + "values": [ + "1.4", + "1.4", + "1.3", + "1.5", + "1.4", + "1.7", + "1.4", + "1.5", + "1.4", + "1.5", + "1.5", + "1.6", + "1.4", + "1.1", + "1.2", + "1.5", + "1.3", + "1.4", + "1.7", + "1.5", + "1.7", + "1.5", + "1.0", + "1.7", + "1.9", + "1.6", + "1.6", + "1.5", + "1.4", + "1.6", + "1.6", + "1.5", + "1.5", + "1.4", + "1.5", + "1.2", + "1.3", + "1.5", + "1.3", + "1.5", + "1.3", + "1.3", + "1.3", + "1.6", + "1.9", + "1.4", + "1.6", + "1.4", + "1.5", + "1.4" + ] + }, + { + "label": "PetalWidth", + "values": [ + "0.2", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.3", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.1", + "0.2", + "0.4", + "0.4", + "0.3", + "0.3", + "0.3", + "0.2", + "0.4", + "0.2", + "0.5", + "0.2", + "0.2", + "0.4", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.1", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.3", + "0.3", + "0.2", + "0.6", + "0.4", + "0.3", + "0.2", + "0.2", + "0.2", + "0.2" + ] + } + ], + "marker": { + "color": "red" + } + }, + { + "type": "splom", + "name": "Versicolor", + "showupperhalf": false, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "7.0", + "6.4", + "6.9", + "5.5", + "6.5", + "5.7", + "6.3", + "4.9", + "6.6", + "5.2", + "5.0", + "5.9", + "6.0", + "6.1", + "5.6", + "6.7", + "5.6", + "5.8", + "6.2", + "5.6", + "5.9", + "6.1", + "6.3", + "6.1", + "6.4", + "6.6", + "6.8", + "6.7", + "6.0", + "5.7", + "5.5", + "5.5", + "5.8", + "6.0", + "5.4", + "6.0", + "6.7", + "6.3", + "5.6", + "5.5", + "5.5", + "6.1", + "5.8", + "5.0", + "5.6", + "5.7", + "5.7", + "6.2", + "5.1", + "5.7" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.2", + "3.2", + "3.1", + "2.3", + "2.8", + "2.8", + "3.3", + "2.4", + "2.9", + "2.7", + "2.0", + "3.0", + "2.2", + "2.9", + "2.9", + "3.1", + "3.0", + "2.7", + "2.2", + "2.5", + "3.2", + "2.8", + "2.5", + "2.8", + "2.9", + "3.0", + "2.8", + "3.0", + "2.9", + "2.6", + "2.4", + "2.4", + "2.7", + "2.7", + "3.0", + "3.4", + "3.1", + "2.3", + "3.0", + "2.5", + "2.6", + "3.0", + "2.6", + "2.3", + "2.7", + "3.0", + "2.9", + "2.9", + "2.5", + "2.8" + ] + }, + { + "label": "PetalLength", + "values": [ + "4.7", + "4.5", + "4.9", + "4.0", + "4.6", + "4.5", + "4.7", + "3.3", + "4.6", + "3.9", + "3.5", + "4.2", + "4.0", + "4.7", + "3.6", + "4.4", + "4.5", + "4.1", + "4.5", + "3.9", + "4.8", + "4.0", + "4.9", + "4.7", + "4.3", + "4.4", + "4.8", + "5.0", + "4.5", + "3.5", + "3.8", + "3.7", + "3.9", + "5.1", + "4.5", + "4.5", + "4.7", + "4.4", + "4.1", + "4.0", + "4.4", + "4.6", + "4.0", + "3.3", + "4.2", + "4.2", + "4.2", + "4.3", + "3.0", + "4.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "1.4", + "1.5", + "1.5", + "1.3", + "1.5", + "1.3", + "1.6", + "1.0", + "1.3", + "1.4", + "1.0", + "1.5", + "1.0", + "1.4", + "1.3", + "1.4", + "1.5", + "1.0", + "1.5", + "1.1", + "1.8", + "1.3", + "1.5", + "1.2", + "1.3", + "1.4", + "1.4", + "1.7", + "1.5", + "1.0", + "1.1", + "1.0", + "1.2", + "1.6", + "1.5", + "1.6", + "1.5", + "1.3", + "1.3", + "1.3", + "1.2", + "1.4", + "1.2", + "1.0", + "1.3", + "1.2", + "1.3", + "1.3", + "1.1", + "1.3" + ] + } + ], + "marker": { + "color": "green" + } + }, + { + "type": "splom", + "name": "Virginica", + "showupperhalf": false, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "6.3", + "5.8", + "7.1", + "6.3", + "6.5", + "7.6", + "4.9", + "7.3", + "6.7", + "7.2", + "6.5", + "6.4", + "6.8", + "5.7", + "5.8", + "6.4", + "6.5", + "7.7", + "7.7", + "6.0", + "6.9", + "5.6", + "7.7", + "6.3", + "6.7", + "7.2", + "6.2", + "6.1", + "6.4", + "7.2", + "7.4", + "7.9", + "6.4", + "6.3", + "6.1", + "7.7", + "6.3", + "6.4", + "6.0", + "6.9", + "6.7", + "6.9", + "5.8", + "6.8", + "6.7", + "6.7", + "6.3", + "6.5", + "6.2", + "5.9" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.3", + "2.7", + "3.0", + "2.9", + "3.0", + "3.0", + "2.5", + "2.9", + "2.5", + "3.6", + "3.2", + "2.7", + "3.0", + "2.5", + "2.8", + "3.2", + "3.0", + "3.8", + "2.6", + "2.2", + "3.2", + "2.8", + "2.8", + "2.7", + "3.3", + "3.2", + "2.8", + "3.0", + "2.8", + "3.0", + "2.8", + "3.8", + "2.8", + "2.8", + "2.6", + "3.0", + "3.4", + "3.1", + "3.0", + "3.1", + "3.1", + "3.1", + "2.7", + "3.2", + "3.3", + "3.0", + "2.5", + "3.0", + "3.4", + "3.0" + ] + }, + { + "label": "PetalLength", + "values": [ + "6.0", + "5.1", + "5.9", + "5.6", + "5.8", + "6.6", + "4.5", + "6.3", + "5.8", + "6.1", + "5.1", + "5.3", + "5.5", + "5.0", + "5.1", + "5.3", + "5.5", + "6.7", + "6.9", + "5.0", + "5.7", + "4.9", + "6.7", + "4.9", + "5.7", + "6.0", + "4.8", + "4.9", + "5.6", + "5.8", + "6.1", + "6.4", + "5.6", + "5.1", + "5.6", + "6.1", + "5.6", + "5.5", + "4.8", + "5.4", + "5.6", + "5.1", + "5.1", + "5.9", + "5.7", + "5.2", + "5.0", + "5.2", + "5.4", + "5.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "2.5", + "1.9", + "2.1", + "1.8", + "2.2", + "2.1", + "1.7", + "1.8", + "1.8", + "2.5", + "2.0", + "1.9", + "2.1", + "2.0", + "2.4", + "2.3", + "1.8", + "2.2", + "2.3", + "1.5", + "2.3", + "2.0", + "2.0", + "1.8", + "2.1", + "1.8", + "1.8", + "1.8", + "2.1", + "1.6", + "1.9", + "2.0", + "2.2", + "1.5", + "1.4", + "2.3", + "2.4", + "1.8", + "1.8", + "2.1", + "2.4", + "2.3", + "1.9", + "2.3", + "2.5", + "2.3", + "1.9", + "2.0", + "2.3", + "1.8" + ] + } + ], + "marker": { + "color": "blue" + } + } + ], + "layout": { + "title": "Iris dataset splom", + "width": 600, + "height": 500 + } +} diff --git a/test/image/mocks/splom_ragged-via-axes.json b/test/image/mocks/splom_ragged-via-axes.json new file mode 100644 index 00000000000..d305a6a801c --- /dev/null +++ b/test/image/mocks/splom_ragged-via-axes.json @@ -0,0 +1,58 @@ +{ + "data": [ + { + "type": "splom", + "dimensions": [ + { + "values": [ + 1, + 2, + 3 + ], + "label": "A" + }, + { + "values": [ + 4, + 5, + 6 + ], + "label": "B" + } + ] + }, + { + "type": "splom", + "dimensions": [ + { + "values": [ + 1, + 2, + 3 + ], + "label": "C" + }, + { + "values": [ + 4, + 5, + 6 + ], + "label": "D" + } + ], + "xaxes": [ + "x3", + "x4" + ], + "yaxes": [ + "y3", + "y4" + ] + } + ], + "layout": { + "title": "ragged sploms using trace xaxes yaxes", + "showlegend": false + } +} diff --git a/test/image/mocks/splom_ragged-via-visible-false.json b/test/image/mocks/splom_ragged-via-visible-false.json new file mode 100644 index 00000000000..29c62e9a64c --- /dev/null +++ b/test/image/mocks/splom_ragged-via-visible-false.json @@ -0,0 +1,56 @@ +{ + "data": [ + { + "type": "splom", + "dimensions": [ + { + "values": [ + 1, + 2, + 3 + ], + "label": "A" + }, + { + "values": [ + 4, + 5, + 6 + ], + "label": "B" + } + ] + }, + { + "type": "splom", + "dimensions": [ + { + "visible": false + }, + { + "visible": false + }, + { + "values": [ + 1, + 2, + 3 + ], + "label": "C" + }, + { + "values": [ + 4, + 5, + 6 + ], + "label": "D" + } + ] + } + ], + "layout": { + "showlegend": false, + "title": "ragged sploms using visible false dimensions" + } +} diff --git a/test/image/mocks/splom_upper-nodiag.json b/test/image/mocks/splom_upper-nodiag.json new file mode 100644 index 00000000000..4e04bea432e --- /dev/null +++ b/test/image/mocks/splom_upper-nodiag.json @@ -0,0 +1,708 @@ +{ + "data": [ + { + "type": "splom", + "name": "Setosa", + "showlowerhalf": false, + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "5.1", + "4.9", + "4.7", + "4.6", + "5.0", + "5.4", + "4.6", + "5.0", + "4.4", + "4.9", + "5.4", + "4.8", + "4.8", + "4.3", + "5.8", + "5.7", + "5.4", + "5.1", + "5.7", + "5.1", + "5.4", + "5.1", + "4.6", + "5.1", + "4.8", + "5.0", + "5.0", + "5.2", + "5.2", + "4.7", + "4.8", + "5.4", + "5.2", + "5.5", + "4.9", + "5.0", + "5.5", + "4.9", + "4.4", + "5.1", + "5.0", + "4.5", + "4.4", + "5.0", + "5.1", + "4.8", + "5.1", + "4.6", + "5.3", + "5.0" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.5", + "3.0", + "3.2", + "3.1", + "3.6", + "3.9", + "3.4", + "3.4", + "2.9", + "3.1", + "3.7", + "3.4", + "3.0", + "3.0", + "4.0", + "4.4", + "3.9", + "3.5", + "3.8", + "3.8", + "3.4", + "3.7", + "3.6", + "3.3", + "3.4", + "3.0", + "3.4", + "3.5", + "3.4", + "3.2", + "3.1", + "3.4", + "4.1", + "4.2", + "3.1", + "3.2", + "3.5", + "3.1", + "3.0", + "3.4", + "3.5", + "2.3", + "3.2", + "3.5", + "3.8", + "3.0", + "3.8", + "3.2", + "3.7", + "3.3" + ] + }, + { + "label": "PetalLength", + "values": [ + "1.4", + "1.4", + "1.3", + "1.5", + "1.4", + "1.7", + "1.4", + "1.5", + "1.4", + "1.5", + "1.5", + "1.6", + "1.4", + "1.1", + "1.2", + "1.5", + "1.3", + "1.4", + "1.7", + "1.5", + "1.7", + "1.5", + "1.0", + "1.7", + "1.9", + "1.6", + "1.6", + "1.5", + "1.4", + "1.6", + "1.6", + "1.5", + "1.5", + "1.4", + "1.5", + "1.2", + "1.3", + "1.5", + "1.3", + "1.5", + "1.3", + "1.3", + "1.3", + "1.6", + "1.9", + "1.4", + "1.6", + "1.4", + "1.5", + "1.4" + ] + }, + { + "label": "PetalWidth", + "values": [ + "0.2", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.3", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.1", + "0.2", + "0.4", + "0.4", + "0.3", + "0.3", + "0.3", + "0.2", + "0.4", + "0.2", + "0.5", + "0.2", + "0.2", + "0.4", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.1", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.3", + "0.3", + "0.2", + "0.6", + "0.4", + "0.3", + "0.2", + "0.2", + "0.2", + "0.2" + ] + } + ], + "marker": { + "color": "red" + } + }, + { + "type": "splom", + "name": "Versicolor", + "showlowerhalf": false, + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "7.0", + "6.4", + "6.9", + "5.5", + "6.5", + "5.7", + "6.3", + "4.9", + "6.6", + "5.2", + "5.0", + "5.9", + "6.0", + "6.1", + "5.6", + "6.7", + "5.6", + "5.8", + "6.2", + "5.6", + "5.9", + "6.1", + "6.3", + "6.1", + "6.4", + "6.6", + "6.8", + "6.7", + "6.0", + "5.7", + "5.5", + "5.5", + "5.8", + "6.0", + "5.4", + "6.0", + "6.7", + "6.3", + "5.6", + "5.5", + "5.5", + "6.1", + "5.8", + "5.0", + "5.6", + "5.7", + "5.7", + "6.2", + "5.1", + "5.7" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.2", + "3.2", + "3.1", + "2.3", + "2.8", + "2.8", + "3.3", + "2.4", + "2.9", + "2.7", + "2.0", + "3.0", + "2.2", + "2.9", + "2.9", + "3.1", + "3.0", + "2.7", + "2.2", + "2.5", + "3.2", + "2.8", + "2.5", + "2.8", + "2.9", + "3.0", + "2.8", + "3.0", + "2.9", + "2.6", + "2.4", + "2.4", + "2.7", + "2.7", + "3.0", + "3.4", + "3.1", + "2.3", + "3.0", + "2.5", + "2.6", + "3.0", + "2.6", + "2.3", + "2.7", + "3.0", + "2.9", + "2.9", + "2.5", + "2.8" + ] + }, + { + "label": "PetalLength", + "values": [ + "4.7", + "4.5", + "4.9", + "4.0", + "4.6", + "4.5", + "4.7", + "3.3", + "4.6", + "3.9", + "3.5", + "4.2", + "4.0", + "4.7", + "3.6", + "4.4", + "4.5", + "4.1", + "4.5", + "3.9", + "4.8", + "4.0", + "4.9", + "4.7", + "4.3", + "4.4", + "4.8", + "5.0", + "4.5", + "3.5", + "3.8", + "3.7", + "3.9", + "5.1", + "4.5", + "4.5", + "4.7", + "4.4", + "4.1", + "4.0", + "4.4", + "4.6", + "4.0", + "3.3", + "4.2", + "4.2", + "4.2", + "4.3", + "3.0", + "4.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "1.4", + "1.5", + "1.5", + "1.3", + "1.5", + "1.3", + "1.6", + "1.0", + "1.3", + "1.4", + "1.0", + "1.5", + "1.0", + "1.4", + "1.3", + "1.4", + "1.5", + "1.0", + "1.5", + "1.1", + "1.8", + "1.3", + "1.5", + "1.2", + "1.3", + "1.4", + "1.4", + "1.7", + "1.5", + "1.0", + "1.1", + "1.0", + "1.2", + "1.6", + "1.5", + "1.6", + "1.5", + "1.3", + "1.3", + "1.3", + "1.2", + "1.4", + "1.2", + "1.0", + "1.3", + "1.2", + "1.3", + "1.3", + "1.1", + "1.3" + ] + } + ], + "marker": { + "color": "green" + } + }, + { + "type": "splom", + "name": "Virginica", + "showlowerhalf": false, + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "6.3", + "5.8", + "7.1", + "6.3", + "6.5", + "7.6", + "4.9", + "7.3", + "6.7", + "7.2", + "6.5", + "6.4", + "6.8", + "5.7", + "5.8", + "6.4", + "6.5", + "7.7", + "7.7", + "6.0", + "6.9", + "5.6", + "7.7", + "6.3", + "6.7", + "7.2", + "6.2", + "6.1", + "6.4", + "7.2", + "7.4", + "7.9", + "6.4", + "6.3", + "6.1", + "7.7", + "6.3", + "6.4", + "6.0", + "6.9", + "6.7", + "6.9", + "5.8", + "6.8", + "6.7", + "6.7", + "6.3", + "6.5", + "6.2", + "5.9" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.3", + "2.7", + "3.0", + "2.9", + "3.0", + "3.0", + "2.5", + "2.9", + "2.5", + "3.6", + "3.2", + "2.7", + "3.0", + "2.5", + "2.8", + "3.2", + "3.0", + "3.8", + "2.6", + "2.2", + "3.2", + "2.8", + "2.8", + "2.7", + "3.3", + "3.2", + "2.8", + "3.0", + "2.8", + "3.0", + "2.8", + "3.8", + "2.8", + "2.8", + "2.6", + "3.0", + "3.4", + "3.1", + "3.0", + "3.1", + "3.1", + "3.1", + "2.7", + "3.2", + "3.3", + "3.0", + "2.5", + "3.0", + "3.4", + "3.0" + ] + }, + { + "label": "PetalLength", + "values": [ + "6.0", + "5.1", + "5.9", + "5.6", + "5.8", + "6.6", + "4.5", + "6.3", + "5.8", + "6.1", + "5.1", + "5.3", + "5.5", + "5.0", + "5.1", + "5.3", + "5.5", + "6.7", + "6.9", + "5.0", + "5.7", + "4.9", + "6.7", + "4.9", + "5.7", + "6.0", + "4.8", + "4.9", + "5.6", + "5.8", + "6.1", + "6.4", + "5.6", + "5.1", + "5.6", + "6.1", + "5.6", + "5.5", + "4.8", + "5.4", + "5.6", + "5.1", + "5.1", + "5.9", + "5.7", + "5.2", + "5.0", + "5.2", + "5.4", + "5.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "2.5", + "1.9", + "2.1", + "1.8", + "2.2", + "2.1", + "1.7", + "1.8", + "1.8", + "2.5", + "2.0", + "1.9", + "2.1", + "2.0", + "2.4", + "2.3", + "1.8", + "2.2", + "2.3", + "1.5", + "2.3", + "2.0", + "2.0", + "1.8", + "2.1", + "1.8", + "1.8", + "1.8", + "2.1", + "1.6", + "1.9", + "2.0", + "2.2", + "1.5", + "1.4", + "2.3", + "2.4", + "1.8", + "1.8", + "2.1", + "2.4", + "2.3", + "1.9", + "2.3", + "2.5", + "2.3", + "1.9", + "2.0", + "2.3", + "1.8" + ] + } + ], + "marker": { + "color": "blue" + } + } + ], + "layout": { + "title": "Iris dataset splom", + "width": 600, + "height": 500, + "legend": { + "x": 0, + "xanchor": "left", + "y": 0, + "yanchor": "bottom" + } + } +} diff --git a/test/image/mocks/splom_upper.json b/test/image/mocks/splom_upper.json new file mode 100644 index 00000000000..ba30aa70a03 --- /dev/null +++ b/test/image/mocks/splom_upper.json @@ -0,0 +1,699 @@ +{ + "data": [ + { + "type": "splom", + "name": "Setosa", + "showlowerhalf": false, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "5.1", + "4.9", + "4.7", + "4.6", + "5.0", + "5.4", + "4.6", + "5.0", + "4.4", + "4.9", + "5.4", + "4.8", + "4.8", + "4.3", + "5.8", + "5.7", + "5.4", + "5.1", + "5.7", + "5.1", + "5.4", + "5.1", + "4.6", + "5.1", + "4.8", + "5.0", + "5.0", + "5.2", + "5.2", + "4.7", + "4.8", + "5.4", + "5.2", + "5.5", + "4.9", + "5.0", + "5.5", + "4.9", + "4.4", + "5.1", + "5.0", + "4.5", + "4.4", + "5.0", + "5.1", + "4.8", + "5.1", + "4.6", + "5.3", + "5.0" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.5", + "3.0", + "3.2", + "3.1", + "3.6", + "3.9", + "3.4", + "3.4", + "2.9", + "3.1", + "3.7", + "3.4", + "3.0", + "3.0", + "4.0", + "4.4", + "3.9", + "3.5", + "3.8", + "3.8", + "3.4", + "3.7", + "3.6", + "3.3", + "3.4", + "3.0", + "3.4", + "3.5", + "3.4", + "3.2", + "3.1", + "3.4", + "4.1", + "4.2", + "3.1", + "3.2", + "3.5", + "3.1", + "3.0", + "3.4", + "3.5", + "2.3", + "3.2", + "3.5", + "3.8", + "3.0", + "3.8", + "3.2", + "3.7", + "3.3" + ] + }, + { + "label": "PetalLength", + "values": [ + "1.4", + "1.4", + "1.3", + "1.5", + "1.4", + "1.7", + "1.4", + "1.5", + "1.4", + "1.5", + "1.5", + "1.6", + "1.4", + "1.1", + "1.2", + "1.5", + "1.3", + "1.4", + "1.7", + "1.5", + "1.7", + "1.5", + "1.0", + "1.7", + "1.9", + "1.6", + "1.6", + "1.5", + "1.4", + "1.6", + "1.6", + "1.5", + "1.5", + "1.4", + "1.5", + "1.2", + "1.3", + "1.5", + "1.3", + "1.5", + "1.3", + "1.3", + "1.3", + "1.6", + "1.9", + "1.4", + "1.6", + "1.4", + "1.5", + "1.4" + ] + }, + { + "label": "PetalWidth", + "values": [ + "0.2", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.3", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.1", + "0.2", + "0.4", + "0.4", + "0.3", + "0.3", + "0.3", + "0.2", + "0.4", + "0.2", + "0.5", + "0.2", + "0.2", + "0.4", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.1", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.3", + "0.3", + "0.2", + "0.6", + "0.4", + "0.3", + "0.2", + "0.2", + "0.2", + "0.2" + ] + } + ], + "marker": { + "color": "red" + } + }, + { + "type": "splom", + "name": "Versicolor", + "showlowerhalf": false, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "7.0", + "6.4", + "6.9", + "5.5", + "6.5", + "5.7", + "6.3", + "4.9", + "6.6", + "5.2", + "5.0", + "5.9", + "6.0", + "6.1", + "5.6", + "6.7", + "5.6", + "5.8", + "6.2", + "5.6", + "5.9", + "6.1", + "6.3", + "6.1", + "6.4", + "6.6", + "6.8", + "6.7", + "6.0", + "5.7", + "5.5", + "5.5", + "5.8", + "6.0", + "5.4", + "6.0", + "6.7", + "6.3", + "5.6", + "5.5", + "5.5", + "6.1", + "5.8", + "5.0", + "5.6", + "5.7", + "5.7", + "6.2", + "5.1", + "5.7" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.2", + "3.2", + "3.1", + "2.3", + "2.8", + "2.8", + "3.3", + "2.4", + "2.9", + "2.7", + "2.0", + "3.0", + "2.2", + "2.9", + "2.9", + "3.1", + "3.0", + "2.7", + "2.2", + "2.5", + "3.2", + "2.8", + "2.5", + "2.8", + "2.9", + "3.0", + "2.8", + "3.0", + "2.9", + "2.6", + "2.4", + "2.4", + "2.7", + "2.7", + "3.0", + "3.4", + "3.1", + "2.3", + "3.0", + "2.5", + "2.6", + "3.0", + "2.6", + "2.3", + "2.7", + "3.0", + "2.9", + "2.9", + "2.5", + "2.8" + ] + }, + { + "label": "PetalLength", + "values": [ + "4.7", + "4.5", + "4.9", + "4.0", + "4.6", + "4.5", + "4.7", + "3.3", + "4.6", + "3.9", + "3.5", + "4.2", + "4.0", + "4.7", + "3.6", + "4.4", + "4.5", + "4.1", + "4.5", + "3.9", + "4.8", + "4.0", + "4.9", + "4.7", + "4.3", + "4.4", + "4.8", + "5.0", + "4.5", + "3.5", + "3.8", + "3.7", + "3.9", + "5.1", + "4.5", + "4.5", + "4.7", + "4.4", + "4.1", + "4.0", + "4.4", + "4.6", + "4.0", + "3.3", + "4.2", + "4.2", + "4.2", + "4.3", + "3.0", + "4.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "1.4", + "1.5", + "1.5", + "1.3", + "1.5", + "1.3", + "1.6", + "1.0", + "1.3", + "1.4", + "1.0", + "1.5", + "1.0", + "1.4", + "1.3", + "1.4", + "1.5", + "1.0", + "1.5", + "1.1", + "1.8", + "1.3", + "1.5", + "1.2", + "1.3", + "1.4", + "1.4", + "1.7", + "1.5", + "1.0", + "1.1", + "1.0", + "1.2", + "1.6", + "1.5", + "1.6", + "1.5", + "1.3", + "1.3", + "1.3", + "1.2", + "1.4", + "1.2", + "1.0", + "1.3", + "1.2", + "1.3", + "1.3", + "1.1", + "1.3" + ] + } + ], + "marker": { + "color": "green" + } + }, + { + "type": "splom", + "name": "Virginica", + "showlowerhalf": false, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "6.3", + "5.8", + "7.1", + "6.3", + "6.5", + "7.6", + "4.9", + "7.3", + "6.7", + "7.2", + "6.5", + "6.4", + "6.8", + "5.7", + "5.8", + "6.4", + "6.5", + "7.7", + "7.7", + "6.0", + "6.9", + "5.6", + "7.7", + "6.3", + "6.7", + "7.2", + "6.2", + "6.1", + "6.4", + "7.2", + "7.4", + "7.9", + "6.4", + "6.3", + "6.1", + "7.7", + "6.3", + "6.4", + "6.0", + "6.9", + "6.7", + "6.9", + "5.8", + "6.8", + "6.7", + "6.7", + "6.3", + "6.5", + "6.2", + "5.9" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.3", + "2.7", + "3.0", + "2.9", + "3.0", + "3.0", + "2.5", + "2.9", + "2.5", + "3.6", + "3.2", + "2.7", + "3.0", + "2.5", + "2.8", + "3.2", + "3.0", + "3.8", + "2.6", + "2.2", + "3.2", + "2.8", + "2.8", + "2.7", + "3.3", + "3.2", + "2.8", + "3.0", + "2.8", + "3.0", + "2.8", + "3.8", + "2.8", + "2.8", + "2.6", + "3.0", + "3.4", + "3.1", + "3.0", + "3.1", + "3.1", + "3.1", + "2.7", + "3.2", + "3.3", + "3.0", + "2.5", + "3.0", + "3.4", + "3.0" + ] + }, + { + "label": "PetalLength", + "values": [ + "6.0", + "5.1", + "5.9", + "5.6", + "5.8", + "6.6", + "4.5", + "6.3", + "5.8", + "6.1", + "5.1", + "5.3", + "5.5", + "5.0", + "5.1", + "5.3", + "5.5", + "6.7", + "6.9", + "5.0", + "5.7", + "4.9", + "6.7", + "4.9", + "5.7", + "6.0", + "4.8", + "4.9", + "5.6", + "5.8", + "6.1", + "6.4", + "5.6", + "5.1", + "5.6", + "6.1", + "5.6", + "5.5", + "4.8", + "5.4", + "5.6", + "5.1", + "5.1", + "5.9", + "5.7", + "5.2", + "5.0", + "5.2", + "5.4", + "5.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "2.5", + "1.9", + "2.1", + "1.8", + "2.2", + "2.1", + "1.7", + "1.8", + "1.8", + "2.5", + "2.0", + "1.9", + "2.1", + "2.0", + "2.4", + "2.3", + "1.8", + "2.2", + "2.3", + "1.5", + "2.3", + "2.0", + "2.0", + "1.8", + "2.1", + "1.8", + "1.8", + "1.8", + "2.1", + "1.6", + "1.9", + "2.0", + "2.2", + "1.5", + "1.4", + "2.3", + "2.4", + "1.8", + "1.8", + "2.1", + "2.4", + "2.3", + "1.9", + "2.3", + "2.5", + "2.3", + "1.9", + "2.0", + "2.3", + "1.8" + ] + } + ], + "marker": { + "color": "blue" + } + } + ], + "layout": { + "title": "Iris dataset splom", + "width": 600, + "height": 500 + } +} diff --git a/test/image/mocks/splom_with-cartesian.json b/test/image/mocks/splom_with-cartesian.json new file mode 100644 index 00000000000..4640328febe --- /dev/null +++ b/test/image/mocks/splom_with-cartesian.json @@ -0,0 +1,45 @@ +{ + "data": [{ + "type": "scattergl", + "name": "scattergl", + "mode": "markers", + "text": "should be above splom", + "x": [2], + "y": [2] + }, { + "type": "splom", + "name": "splom", + "dimensions": [{ + "values": [1, 2, 3], + "label": "A" + }, { + "values": [2, 5, 6], + "label": "B" + }] + }, { + "name": "scatter", + "y": [1, 2, 1] + }, { + "type": "box", + "name": "box", + "x0": 0, + "y": [1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 6], + "yaxis": "y2" + }, { + "type": "scattergl", + "name": "scattergl", + "y": [1, 2, 1], + "xaxis": "x2" + }], + "layout": { + "grid": { + "roworder": "bottom to top" + }, + "legend": { + "x": 0, + "y": 1.1, + "yanchor": "bottom", + "orientation": "h" + } + } +} diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index bb9d6c3073b..d105bb5b4a3 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -530,6 +530,30 @@ describe('axis zoom/pan and main plot zoom', function() { .catch(failTest) .then(done); }); + + it('updates linked axes when there are constraints (axes_scaleanchor mock)', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/axes_scaleanchor.json')); + + function _assert(y3rng, y4rng) { + expect(gd._fullLayout.yaxis3.range).toBeCloseToArray(y3rng, 2, 'y3 rng'); + expect(gd._fullLayout.yaxis4.range).toBeCloseToArray(y4rng, 2, 'y3 rng'); + } + + Plotly.plot(gd, fig) + .then(function() { + _assert([-0.36, 4.36], [-0.36, 4.36]); + }) + .then(doDrag('x2y3', 'nsew', 0, 100)) + .then(function() { + _assert([-0.36, 2], [0.82, 3.18]); + }) + .then(doDrag('x2y4', 'nsew', 0, 50)) + .then(function() { + _assert([0.41, 1.23], [1.18, 2]); + }) + .catch(failTest) + .then(done); + }); }); describe('Event data:', function() { diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js index 293d482e63d..50b2172c9e9 100644 --- a/test/jasmine/tests/fx_test.js +++ b/test/jasmine/tests/fx_test.js @@ -201,13 +201,12 @@ describe('relayout', function() { afterEach(destroyGraphDiv); it('should update main drag with correct', function(done) { - function assertMainDrag(cursor, isActive) { expect(d3.selectAll('rect.nsewdrag').size()).toEqual(1, 'number of nodes'); - var mainDrag = d3.select('rect.nsewdrag'), - node = mainDrag.node(); + var mainDrag = d3.select('rect.nsewdrag'); + var node = mainDrag.node(); - expect(mainDrag.classed('cursor-' + cursor)).toBe(true, 'cursor ' + cursor); + expect(window.getComputedStyle(node).cursor).toBe(cursor, 'cursor ' + cursor); expect(node.style.pointerEvents).toEqual('all', 'pointer event'); expect(!!node.onmousedown).toBe(isActive, 'mousedown handler'); } diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 96319a39fb2..c5177d04480 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -516,7 +516,10 @@ describe('Test plot api', function() { 'layoutStyles', 'doTicksRelayout', 'doModeBar', - 'doCamera' + 'doCamera', + 'doAutoRangeAndConstraints', + 'drawData', + 'finalDraw' ]; var gd; @@ -625,6 +628,46 @@ describe('Test plot api', function() { expectReplot(attr); } }); + + it('should trigger minimal sequence for cartesian axis range updates', function() { + var seq = ['doAutoRangeAndConstraints', 'doTicksRelayout', 'drawData', 'finalDraw']; + + function _assert(msg) { + expect(gd.calcdata).toBeDefined(); + mockedMethods.forEach(function(m) { + expect(subroutines[m].calls.count()).toBe( + seq.indexOf(m) === -1 ? 0 : 1, + '# of ' + m + ' calls - ' + msg + ); + }); + } + + var trace = {y: [1, 2, 1]}; + + var specs = [ + ['relayout', ['xaxis.range[0]', 0]], + ['relayout', ['xaxis.range[1]', 3]], + ['relayout', ['xaxis.range', [-1, 5]]], + ['update', [{}, {'xaxis.range': [-1, 10]}]], + ['react', [[trace], {xaxis: {range: [0, 1]}}]] + ]; + + specs.forEach(function(s) { + // create 'real' div for Plotly.react to work + gd = createGraphDiv(); + Plotly.plot(gd, [trace], {xaxis: {range: [1, 2]}}); + mock(gd); + + Plotly[s[0]](gd, s[1][0], s[1][1]); + + _assert([ + 'Plotly.', s[0], + '(gd, ', JSON.stringify(s[1][0]), ', ', JSON.stringify(s[1][1]), ')' + ].join('')); + + destroyGraphDiv(); + }); + }); }); describe('Plotly.restyle subroutines switchboard', function() { @@ -2838,6 +2881,7 @@ describe('Test plot api', function() { ['range_selector_style', require('@mocks/range_selector_style.json')], ['range_slider_multiple', require('@mocks/range_slider_multiple.json')], ['sankey_energy', require('@mocks/sankey_energy.json')], + ['splom_iris', require('@mocks/splom_iris.json')], ['table_wrapped_birds', require('@mocks/table_wrapped_birds.json')], ['ternary_fill', require('@mocks/ternary_fill.json')], ['text_chart_arrays', require('@mocks/text_chart_arrays.json')], diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 753a9b95523..61b3ae29497 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -13,7 +13,6 @@ describe('Test Plots', function() { 'use strict'; describe('Plots.supplyDefaults', function() { - it('should not throw an error when gd is a plain object', function() { var height = 100, gd = { @@ -154,6 +153,26 @@ describe('Test Plots', function() { testSanitizeMarginsHasBeenCalledOnlyOnce(gd); }); + + it('should sort base plot modules on fullLayout object', function() { + var gd = Lib.extendDeep({}, require('@mocks/plot_types.json')); + gd.data.unshift({type: 'scattergl'}); + gd.data.push({type: 'splom'}); + + supplyAllDefaults(gd); + var names = gd._fullLayout._basePlotModules.map(function(m) { + return m.name; + }); + + expect(names).toEqual([ + 'splom', + 'cartesian', + 'gl3d', + 'geo', + 'pie', + 'ternary' + ]); + }); }); describe('Plots.supplyLayoutGlobalDefaults should', function() { diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index c071673f909..452ede89a77 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -349,6 +349,15 @@ describe('plot schema', function() { expect(scatterglSchema.error_y.copy_ystyle).toBeUndefined(); expect(scatterglSchema.error_y.copy_zstyle).toBeUndefined(); }); + + it('should convert regex valObject fields to strings', function() { + var splomAttrs = plotSchema.traces.splom.attributes; + + expect(typeof splomAttrs.xaxes.items.regex).toBe('string'); + expect(splomAttrs.xaxes.items.regex).toBe('/^x([2-9]|[1-9][0-9]+)?$/'); + expect(typeof splomAttrs.yaxes.items.regex).toBe('string'); + expect(splomAttrs.yaxes.items.regex).toBe('/^y([2-9]|[1-9][0-9]+)?$/'); + }); }); describe('getTraceValObject', function() { diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js new file mode 100644 index 00000000000..357f5b943e2 --- /dev/null +++ b/test/jasmine/tests/splom_test.js @@ -0,0 +1,914 @@ +var Plotly = require('@lib'); +var Lib = require('@src/lib'); +var Plots = require('@src/plots/plots'); +var SUBPLOT_PATTERN = require('@src/plots/cartesian/constants').SUBPLOT_PATTERN; + +var d3 = require('d3'); +var supplyAllDefaults = require('../assets/supply_defaults'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); +var mouseEvent = require('../assets/mouse_event'); +var drag = require('../assets/drag'); + +var customAssertions = require('../assets/custom_assertions'); +var assertHoverLabelContent = customAssertions.assertHoverLabelContent; + +describe('Test splom trace defaults:', function() { + var gd; + + function _supply(opts, layout) { + gd = {}; + opts = Array.isArray(opts) ? opts : [opts]; + + gd.data = opts.map(function(o) { + return Lib.extendFlat({type: 'splom'}, o || {}); + }); + gd.layout = layout || {}; + + supplyAllDefaults(gd); + } + + it('should set `visible: false` dimensions-less traces', function() { + _supply([{}, {dimensions: []}]); + + expect(gd._fullData[0].visible).toBe(false); + expect(gd._fullData[1].visible).toBe(false); + }); + + it('should set `visible: false` to traces with showupperhalf, showlowerhalf, and diagonal.visible false', function() { + _supply({ + dimensions: [{ + values: [1, 2, 3] + }], + showupperhalf: false, + showlowerhalf: false, + diagonal: {visible: false} + }); + + expect(gd._fullData[0].visible).toBe(false); + }); + + it('should set `visible: false` to values-less dimensions', function() { + _supply({ + dimensions: [ + 'not-an-object', + {other: 'stuff'} + ] + }); + + expect(gd._fullData[0].dimensions[0].visible).toBe(false); + expect(gd._fullData[0].dimensions[1].visible).toBe(false); + }); + + it('should work with only one dimensions', function() { + _supply({ + dimensions: [ + {values: [2, 1, 2]} + ] + }); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.domain).toBeCloseToArray([0, 1]); + expect(fullLayout.yaxis.domain).toBeCloseToArray([0, 1]); + }); + + it('should set `grid.xaxes` and `grid.yaxes` default using the new of dimensions', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]} + ] + }); + + var fullTrace = gd._fullData[0]; + expect(fullTrace._commonLength).toBe(3, 'common length'); + expect(fullTrace.dimensions[0]._length).toBe(3, 'dim 0 length'); + expect(fullTrace.dimensions[1]._length).toBe(3, 'dim 1 length'); + expect(fullTrace.xaxes).toEqual(['x', 'x2']); + expect(fullTrace.yaxes).toEqual(['y', 'y2']); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.domain).toBeCloseToArray([0, 0.47]); + expect(fullLayout.yaxis.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.xaxis2.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.yaxis2.domain).toBeCloseToArray([0, 0.47]); + + var subplots = fullLayout._subplots; + expect(subplots.xaxis).toEqual(['x', 'x2']); + expect(subplots.yaxis).toEqual(['y', 'y2']); + expect(subplots.cartesian).toEqual(['xy', 'xy2', 'x2y', 'x2y2']); + }); + + it('should use special `grid.xside` and `grid.yside` defaults on splom generated grids', function() { + var gridOut; + + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]} + ] + }); + + gridOut = gd._fullLayout.grid; + expect(gridOut.xside).toBe('bottom'); + expect(gridOut.yside).toBe('left'); + + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]} + ] + }, { + grid: { + xaxes: ['x', 'x2'], + yaxes: ['y', 'y2'] + } + }); + + gridOut = gd._fullLayout.grid; + expect(gridOut.xside).toBe('bottom plot'); + expect(gridOut.yside).toBe('left plot'); + }); + + it('should honor `grid.xaxes` and `grid.yaxes` settings', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]} + ] + }, { + grid: {domain: {x: [0, 0.5], y: [0, 0.5]}} + }); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.domain).toBeCloseToArray([0, 0.24]); + expect(fullLayout.yaxis.domain).toBeCloseToArray([0.26, 0.5]); + expect(fullLayout.xaxis2.domain).toBeCloseToArray([0.26, 0.5]); + expect(fullLayout.yaxis2.domain).toBeCloseToArray([0, 0.24]); + }); + + it('should honor xaxis and yaxis settings', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]} + ] + }, { + xaxis: {domain: [0, 0.4]}, + yaxis2: {domain: [0, 0.3]} + }); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.domain).toBeCloseToArray([0, 0.4]); + expect(fullLayout.yaxis.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.xaxis2.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.yaxis2.domain).toBeCloseToArray([0, 0.3]); + }); + + it('should set axis title default using dimensions *label*', function() { + _supply({ + dimensions: [{ + label: 'A', + values: [2, 3, 1] + }, { + label: 'B', + values: [1, 2, 1] + }] + }); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.title).toBe('A'); + expect(fullLayout.yaxis.title).toBe('A'); + expect(fullLayout.xaxis2.title).toBe('B'); + expect(fullLayout.yaxis2.title).toBe('B'); + }); + + it('should set axis title default using dimensions *label* (even visible false dimensions)', function() { + _supply({ + dimensions: [{ + label: 'A', + values: [2, 3, 1] + }, { + label: 'B', + visible: false + }, { + label: 'C', + values: [1, 2, 1] + }] + }); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.title).toBe('A'); + expect(fullLayout.yaxis.title).toBe('A'); + expect(fullLayout.xaxis2.title).toBe('B'); + expect(fullLayout.yaxis2.title).toBe('B'); + expect(fullLayout.xaxis3.title).toBe('C'); + expect(fullLayout.yaxis3.title).toBe('C'); + }); + + it('should ignore (x|y)axes values beyond dimensions length', function() { + _supply({ + dimensions: [{ + label: 'A', + values: [2, 3, 1] + }, { + label: 'B', + values: [0, 1, 0.5] + }, { + label: 'C', + values: [1, 2, 1] + }], + xaxes: ['x', 'x2', 'x3', 'x4'], + yaxes: ['y', 'y2', 'y3', 'y4'] + }); + + var fullTrace = gd._fullData[0]; + // keeps 1-to-1 relationship with input data + expect(fullTrace.xaxes).toEqual(['x', 'x2', 'x3', 'x4']); + expect(fullTrace.yaxes).toEqual(['y', 'y2', 'y3', 'y4']); + + var fullLayout = gd._fullLayout; + // this here does the 'ignoring' part + expect(Object.keys(fullLayout._splomSubplots)).toEqual([ + 'xy', 'xy2', 'xy3', + 'x2y', 'x2y2', 'x2y3', + 'x3y', 'x3y2', 'x3y3' + ]); + expect(fullLayout.xaxis.title).toBe('A'); + expect(fullLayout.yaxis.title).toBe('A'); + expect(fullLayout.xaxis2.title).toBe('B'); + expect(fullLayout.yaxis2.title).toBe('B'); + expect(fullLayout.xaxis3.title).toBe('C'); + expect(fullLayout.yaxis3.title).toBe('C'); + expect(fullLayout.xaxis4).toBe(undefined); + expect(fullLayout.yaxis4).toBe(undefined); + }); + + it('should ignore (x|y)axes values beyond dimensions length (case 2)', function() { + _supply({ + dimensions: [{ + label: 'A', + values: [2, 3, 1] + }, { + label: 'B', + values: [0, 1, 0.5] + }, { + label: 'C', + values: [1, 2, 1] + }], + xaxes: ['x2', 'x3', 'x4', 'x5'], + yaxes: ['y2', 'y3', 'y4', 'y5'] + }); + + var fullTrace = gd._fullData[0]; + // keeps 1-to-1 relationship with input data + expect(fullTrace.xaxes).toEqual(['x2', 'x3', 'x4', 'x5']); + expect(fullTrace.yaxes).toEqual(['y2', 'y3', 'y4', 'y5']); + + var fullLayout = gd._fullLayout; + // this here does the 'ignoring' part + expect(Object.keys(fullLayout._splomSubplots)).toEqual([ + 'x2y2', 'x2y3', 'x2y4', + 'x3y2', 'x3y3', 'x3y4', + 'x4y2', 'x4y3', 'x4y4' + ]); + expect(fullLayout.xaxis).toBe(undefined); + expect(fullLayout.yaxis).toBe(undefined); + expect(fullLayout.xaxis2.title).toBe('A'); + expect(fullLayout.yaxis2.title).toBe('A'); + expect(fullLayout.xaxis3.title).toBe('B'); + expect(fullLayout.yaxis3.title).toBe('B'); + expect(fullLayout.xaxis4.title).toBe('C'); + expect(fullLayout.yaxis4.title).toBe('C'); + expect(fullLayout.xaxis5).toBe(undefined); + expect(fullLayout.yaxis5).toBe(undefined); + }); + + it('should ignore dimensions beyond (x|y)axes length', function() { + _supply({ + dimensions: [{ + label: 'A', + values: [2, 3, 1] + }, { + label: 'B', + values: [0, 1, 0.5] + }, { + label: 'C', + values: [1, 2, 1] + }], + xaxes: ['x2', 'x3'], + yaxes: ['y2', 'y3'] + }); + + var fullTrace = gd._fullData[0]; + expect(fullTrace.xaxes).toEqual(['x2', 'x3']); + expect(fullTrace.yaxes).toEqual(['y2', 'y3']); + // keep 1-to-1 relationship with input data + expect(fullTrace.dimensions.length).toBe(3); + + var fullLayout = gd._fullLayout; + // this here does the 'ignoring' part + expect(Object.keys(fullLayout._splomSubplots)).toEqual([ + 'x2y2', 'x2y3', + 'x3y2', 'x3y3' + ]); + }); + + it('should lead to correct axis auto type value', function() { + _supply({ + dimensions: [ + {values: ['a', 'b', 'c']}, + {values: ['A', 't', 'd']} + ] + }); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.type).toBe('category'); + expect(fullLayout.yaxis.type).toBe('category'); + }); + + it('should lead to correct axis auto type value (case 2)', function() { + _supply({ + dimensions: [ + {visible: false, values: ['2018-01-01', '2018-02-01', '2018-03-03']}, + {values: ['2018-01-01', '2018-02-01', '2018-03-03']} + ] + }); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.type).toBe('date'); + expect(fullLayout.yaxis.type).toBe('date'); + }); +}); + +describe('@gl Test splom interactions:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it('should destroy gl objects on Plots.cleanPlot', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/splom_large.json')); + + Plotly.plot(gd, fig).then(function() { + expect(gd._fullLayout._splomGrid).toBeDefined(); + expect(gd.calcdata[0][0].t._scene).toBeDefined(); + + return Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout, gd.calcdata); + }) + .then(function() { + expect(gd._fullLayout._splomGrid).toBe(null); + expect(gd.calcdata[0][0].t._scene).toBe(null); + }) + .catch(failTest) + .then(done); + }); + + it('when hasOnlyLargeSploms, should create correct regl-line2d data for grid', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/splom_large.json')); + var cnt = 1; + + function _assert(dims) { + var gridData = gd._fullLayout._splomGrid.passes; + var gridLengths = gridData.map(function(d) { return d.count * 2; }); + var msg = ' - call #' + cnt; + + expect(Object.keys(gridData).length) + .toBe(dims.length, '# of batches' + msg); + gridLengths.forEach(function(l, i) { + expect(l).toBe(dims[i], '# of coords in batch ' + i + msg); + }); + cnt++; + } + + Plotly.plot(gd, fig).then(function() { + _assert([1198, 3478, 16318, 118]); + return Plotly.restyle(gd, 'showupperhalf', false); + }) + .then(function() { + _assert([1198, 1882, 8452, 4]); + return Plotly.restyle(gd, 'diagonal.visible', false); + }) + .then(function() { + _assert([1138, 1702, 7636, 4]); + return Plotly.restyle(gd, { + showupperhalf: true, + showlowerhalf: false + }); + }) + .then(function() { + _assert([64, 1594, 7852, 112]); + return Plotly.restyle(gd, 'diagonal.visible', true); + }) + .then(function() { + _assert([58, 1768, 8680, 118]); + return Plotly.relayout(gd, { + 'xaxis.gridcolor': null, + 'xaxis.gridwidth': null, + 'yaxis.zerolinecolor': null, + 'yaxis.zerolinewidth': null + }); + }) + .then(function() { + // one batch for all 'grid' lines + // and another for all 'zeroline' lines + _assert([8740, 1888]); + }) + .catch(failTest) + .then(done); + }); + + it('should update properly in-and-out of hasOnlyLargeSploms regime', function(done) { + var figLarge = Lib.extendDeep({}, require('@mocks/splom_large.json')); + var dimsLarge = figLarge.data[0].dimensions; + var dimsSmall = dimsLarge.slice(0, 5); + var cnt = 1; + + function _assert(exp) { + var msg = ' - call #' + cnt; + var subplots = d3.selectAll('g.cartesianlayer > g.subplot'); + + expect(subplots.size()) + .toBe(exp.subplotCnt, '# of ' + msg); + + var failedSubplots = []; + subplots.each(function(d, i) { + var actual = this.children.length; + var expected = typeof exp.innerSubplotNodeCnt === 'function' ? + exp.innerSubplotNodeCnt(d, i) : + exp.innerSubplotNodeCnt; + if(actual !== expected) { + failedSubplots.push([d, actual, 'vs', expected].join(' ')); + } + }); + expect(failedSubplots) + .toEqual([], '# of nodes inside ' + msg); + + expect(!!gd._fullLayout._splomGrid) + .toBe(exp.hasSplomGrid, 'has regl-line2d splom grid' + msg); + + cnt++; + } + + Plotly.plot(gd, figLarge).then(function() { + _assert({ + subplotCnt: 400, + innerSubplotNodeCnt: 5, + hasSplomGrid: true + }); + return Plotly.restyle(gd, 'dimensions', [dimsSmall]); + }) + .then(function() { + _assert({ + subplotCnt: 25, + innerSubplotNodeCnt: 17, + hasSplomGrid: false + }); + + // make sure 'new' subplot layers are in order + var gridIndex = -1; + var xaxisIndex = -1; + var subplot0 = d3.select('g.cartesianlayer > g.subplot').node(); + for(var i in subplot0.children) { + var cl = subplot0.children[i].classList; + if(cl) { + if(cl.contains('gridlayer')) gridIndex = +i; + else if(cl.contains('xaxislayer-above')) xaxisIndex = +i; + } + } + // from large -> small splom: + // grid layer would be above xaxis layer, + // if we didn't clear subplot children. + expect(gridIndex).toBe(1, ' index'); + expect(xaxisIndex).toBe(14, ' index'); + + return Plotly.restyle(gd, 'dimensions', [dimsLarge]); + }) + .then(function() { + _assert({ + subplotCnt: 400, + // from small -> large splom: + // no need to clear subplots children in existing subplots, + // new subplots though have reduced number of children. + innerSubplotNodeCnt: function(d) { + var p = d.match(SUBPLOT_PATTERN); + return (p[1] > 5 || p[2] > 5) ? 5 : 17; + }, + hasSplomGrid: true + }); + }) + .catch(failTest) + .then(done); + }); + + it('should correctly move axis layers when relayouting *grid.(x|y)side*', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/splom_upper-nodiag.json')); + + function _assert(exp) { + var g = d3.select(gd).select('g.cartesianlayer'); + for(var k in exp) { + // all ticks are set to same position, + // only check first one + var tick0 = g.select('g.' + k + 'tick > text'); + var pos = {x: 'y', y: 'x'}[k.charAt(0)]; + expect(+tick0.attr(pos)) + .toBeWithin(exp[k], 1, pos + ' position for axis ' + k); + } + } + + Plotly.plot(gd, fig).then(function() { + expect(gd._fullLayout.grid.xside).toBe('bottom', 'sanity check dflt grid.xside'); + expect(gd._fullLayout.grid.yside).toBe('left', 'sanity check dflt grid.yside'); + + _assert({ + x: 433, x2: 433, x3: 433, + y: 80, y2: 80, y3: 80 + }); + return Plotly.relayout(gd, 'grid.yside', 'left plot'); + }) + .then(function() { + _assert({ + x: 433, x2: 433, x3: 433, + y: 79, y2: 230, y3: 382 + }); + return Plotly.relayout(gd, 'grid.xside', 'bottom plot'); + }) + .then(function() { + _assert({ + x: 212, x2: 323, x3: 433, + y: 79, y2: 230, y3: 382 + }); + }) + .catch(failTest) + .then(done); + }); +}); + +describe('@gl Test splom hover:', function() { + var gd; + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + function run(s, done) { + gd = createGraphDiv(); + + var fig = Lib.extendDeep({}, + s.mock || require('@mocks/splom_iris.json') + ); + + if(s.patch) { + fig = s.patch(fig); + } + + var pos = s.pos || [200, 100]; + + return Plotly.plot(gd, fig).then(function() { + var to = setTimeout(function() { + failTest('no event data received'); + done(); + }, 100); + + gd.on('plotly_hover', function(d) { + clearTimeout(to); + assertHoverLabelContent(s); + + var msg = ' - event data ' + s.desc; + var actual = d.points || []; + var exp = s.evtPts; + expect(actual.length).toBe(exp.length, 'pt length' + msg); + for(var i = 0; i < exp.length; i++) { + for(var k in exp[i]) { + var m = 'key ' + k + ' in pt ' + i + msg; + expect(actual[i][k]).toBe(exp[i][k], m); + } + } + + // w/o this purge gets called before + // hover throttle is complete + setTimeout(done, 0); + }); + + mouseEvent('mousemove', pos[0], pos[1]); + }) + .catch(failTest); + } + + var specs = [{ + desc: 'basic', + nums: '7.7', + name: 'Virginica', + axis: '2.6', + evtPts: [{x: 2.6, y: 7.7, pointNumber: 18, curveNumber: 2}] + }, { + desc: 'hovermode closest', + patch: function(fig) { + fig.layout.hovermode = 'closest'; + return fig; + }, + nums: '(2.6, 7.7)', + name: 'Virginica', + evtPts: [{x: 2.6, y: 7.7, pointNumber: 18, curveNumber: 2}] + }, { + desc: 'skipping over visible false dims', + patch: function(fig) { + fig.data[0].dimensions[0].visible = false; + return fig; + }, + nums: '7.7', + name: 'Virginica', + axis: '2.6', + evtPts: [{x: 2.6, y: 7.7, pointNumber: 18, curveNumber: 2}] + }, { + desc: 'on log axes', + mock: require('@mocks/splom_log.json'), + patch: function(fig) { + fig.layout.margin = {t: 0, l: 0, b: 0, r: 0}; + fig.layout.width = 400; + fig.layout.height = 400; + return fig; + }, + pos: [20, 380], + nums: '100', + axis: '10', + evtPts: [{x: 10, y: 100, pointNumber: 0}] + }, { + desc: 'on date axes', + mock: require('@mocks/splom_dates.json'), + patch: function(fig) { + fig.layout = { + margin: {t: 0, l: 0, b: 0, r: 0}, + width: 400, + height: 400 + }; + return fig; + }, + pos: [20, 380], + nums: 'Apr 2003', + axis: 'Jan 2000', + evtPts: [{x: '2000-01-01', y: '2003-04-21', pointNumber: 0}] + }]; + + specs.forEach(function(s) { + it('should generate correct hover labels ' + s.desc, function(done) { + run(s, done); + }); + }); +}); + +describe('@gl Test splom drag:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + function _drag(p0, p1) { + var node = d3.select('.nsewdrag[data-subplot="xy"]').node(); + var dx = p1[0] - p0[0]; + var dy = p1[1] - p0[1]; + return drag(node, dx, dy, null, p0[0], p0[1]); + } + + it('should update scattermatrix ranges on pan', function(done) { + var fig = require('@mocks/splom_iris.json'); + fig.layout.dragmode = 'pan'; + + var xaxes = ['xaxis', 'xaxis2', 'xaxis3']; + var yaxes = ['yaxis', 'yaxis2', 'yaxis3']; + + function _assertRanges(msg, xRanges, yRanges) { + xaxes.forEach(function(n, i) { + expect(gd._fullLayout[n].range) + .toBeCloseToArray(xRanges[i], 1, n + ' range - ' + msg); + }); + yaxes.forEach(function(n, i) { + expect(gd._fullLayout[n].range) + .toBeCloseToArray(yRanges[i], 1, n + ' range - ' + msg); + }); + } + + Plotly.plot(gd, fig) + .then(function() { + var scene = gd.calcdata[0][0].t._scene; + spyOn(scene.matrix, 'update'); + spyOn(scene.matrix, 'draw'); + + _assertRanges('before drag', [ + [3.9, 8.3], + [1.7, 4.7], + [0.3, 7.6] + ], [ + [3.8, 8.4], + [1.7, 4.7], + [0.3, 7.6] + ]); + }) + .then(function() { return _drag([130, 130], [150, 150]); }) + .then(function() { + var scene = gd.calcdata[0][0].t._scene; + // N.B. _drag triggers two updateSubplots call + // - 1 update and 1 draw call per updateSubplot + // - 2 update calls (1 for data, 1 for view opts) + // during splom plot on mouseup + // - 1 draw call during splom plot on mouseup + expect(scene.matrix.update).toHaveBeenCalledTimes(4); + expect(scene.matrix.draw).toHaveBeenCalledTimes(3); + + _assertRanges('after drag', [ + [2.9, 7.3], + [1.7, 4.7], + [0.3, 7.6] + ], [ + [5.1, 9.6], + [1.7, 4.7], + [0.3, 7.6] + ]); + }) + .catch(failTest) + .then(done); + }); +}); + +describe('@gl Test splom select:', function() { + var gd; + var ptData; + var subplot; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + function _select(path, opts) { + return new Promise(function(resolve, reject) { + opts = opts || {}; + ptData = null; + subplot = null; + + var to = setTimeout(function() { + reject('fail: plotly_selected not emitter'); + }, 200); + + gd.once('plotly_selected', function(d) { + clearTimeout(to); + ptData = (d || {}).points; + subplot = Object.keys(d.range || {}).join(''); + resolve(); + }); + + Lib.clearThrottle(); + mouseEvent('mousemove', path[0][0], path[0][1], opts); + mouseEvent('mousedown', path[0][0], path[0][1], opts); + + var len = path.length; + path.slice(1, len).forEach(function(pt) { + Lib.clearThrottle(); + mouseEvent('mousemove', pt[0], pt[1], opts); + }); + + mouseEvent('mouseup', path[len - 1][0], path[len - 1][1], opts); + }); + } + + it('should emit correct event data and draw selection outlines', function(done) { + var fig = require('@mocks/splom_0.json'); + fig.layout = { + dragmode: 'select', + width: 400, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + grid: {xgap: 0, ygap: 0} + }; + + function _assert(_msg, ptExp, otherExp) { + var msg = ' - ' + _msg; + + expect(ptData.length).toBe(ptExp.length, 'pt length' + msg); + for(var i = 0; i < ptExp.length; i++) { + for(var k in ptExp[i]) { + var m = 'key ' + k + ' in pt ' + i + msg; + expect(ptData[i][k]).toBe(ptExp[i][k], m); + } + } + + expect(subplot).toBe(otherExp.subplot, 'subplot of selection' + msg); + + expect(d3.selectAll('.zoomlayer > .select-outline').size()) + .toBe(otherExp.selectionOutlineCnt, 'selection outline cnt' + msg); + } + + Plotly.newPlot(gd, fig) + .then(function() { return _select([[5, 5], [195, 195]]); }) + .then(function() { + _assert('first', [ + {pointNumber: 0, x: 1, y: 1}, + {pointNumber: 1, x: 2, y: 2}, + {pointNumber: 2, x: 3, y: 3} + ], { + subplot: 'xy', + selectionOutlineCnt: 2 + }); + }) + .then(function() { return _select([[50, 50], [100, 100]]); }) + .then(function() { + _assert('second', [ + {pointNumber: 1, x: 2, y: 2} + ], { + subplot: 'xy', + selectionOutlineCnt: 2 + }); + }) + .then(function() { return _select([[5, 195], [100, 100]], {shiftKey: true}); }) + .then(function() { + _assert('multi-select', [ + {pointNumber: 0, x: 1, y: 1}, + {pointNumber: 1, x: 2, y: 2} + ], { + subplot: 'xy', + // still '2' as the selection get merged + selectionOutlineCnt: 2 + }); + }) + .then(function() { return _select([[205, 205], [395, 395]]); }) + .then(function() { + _assert('across other subplot', [ + {pointNumber: 0, x: 2, y: 2}, + {pointNumber: 1, x: 5, y: 5}, + {pointNumber: 2, x: 6, y: 6} + ], { + subplot: 'x2y2', + // outlines from previous subplot are cleared! + selectionOutlineCnt: 2 + }); + }) + .then(function() { return _select([[50, 50], [100, 100]]); }) + .then(function() { + _assert('multi-select across other subplot (prohibited for now)', [ + {pointNumber: 1, x: 2, y: 2} + ], { + subplot: 'xy', + // outlines from previous subplot are cleared! + selectionOutlineCnt: 2 + }); + }) + .catch(failTest) + .then(done); + }); + + it('should redraw splom traces before scattergl trace (if any)', function(done) { + var fig = require('@mocks/splom_with-cartesian.json'); + fig.layout.dragmode = 'select'; + fig.layout.width = 400; + fig.layout.height = 400; + fig.layout.margin = {l: 0, t: 0, r: 0, b: 0}; + fig.layout.grid.xgap = 0; + fig.layout.grid.ygap = 0; + + var cnt = 0; + var scatterGlCnt = 0; + var splomCnt = 0; + + Plotly.newPlot(gd, fig).then(function() { + // 'scattergl' trace module + spyOn(gd._fullLayout._modules[0], 'style').and.callFake(function() { + cnt++; + scatterGlCnt = cnt; + }); + // 'splom' trace module + spyOn(gd._fullLayout._modules[1], 'style').and.callFake(function() { + cnt++; + splomCnt = cnt; + }); + }) + .then(function() { return _select([[20, 395], [195, 205]]); }) + .then(function() { + expect(gd._fullLayout._modules[0].style).toHaveBeenCalledTimes(1); + expect(gd._fullLayout._modules[1].style).toHaveBeenCalledTimes(1); + + expect(cnt).toBe(2); + expect(splomCnt).toBe(1, 'splom redraw before scattergl'); + expect(scatterGlCnt).toBe(2, 'scattergl redraw after splom'); + }) + .catch(failTest) + .then(done); + }); +});