diff --git a/lib/bar.js b/lib/bar.js index 64283813cb1..37a62cfa50e 100644 --- a/lib/bar.js +++ b/lib/bar.js @@ -5,7 +5,5 @@ * 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/bar'); +"use strict"; +module.exports = require("../src/traces/bar"); diff --git a/lib/box.js b/lib/box.js index f3fde994b63..6ef50340d61 100644 --- a/lib/box.js +++ b/lib/box.js @@ -5,7 +5,5 @@ * 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/box'); +"use strict"; +module.exports = require("../src/traces/box"); diff --git a/lib/calendars.js b/lib/calendars.js index 8ad06f1d6da..86eb1b1fb0d 100644 --- a/lib/calendars.js +++ b/lib/calendars.js @@ -5,7 +5,5 @@ * 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/components/calendars'); +"use strict"; +module.exports = require("../src/components/calendars"); diff --git a/lib/candlestick.js b/lib/candlestick.js index c608d5b6755..a4c16de27f6 100644 --- a/lib/candlestick.js +++ b/lib/candlestick.js @@ -5,7 +5,5 @@ * 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/candlestick'); +"use strict"; +module.exports = require("../src/traces/candlestick"); diff --git a/lib/choropleth.js b/lib/choropleth.js index 4e1f61186c5..37045b2e42c 100644 --- a/lib/choropleth.js +++ b/lib/choropleth.js @@ -5,7 +5,5 @@ * 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/choropleth'); +"use strict"; +module.exports = require("../src/traces/choropleth"); diff --git a/lib/contour.js b/lib/contour.js index 703eb8d311b..25cd26592d2 100644 --- a/lib/contour.js +++ b/lib/contour.js @@ -5,7 +5,5 @@ * 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/contour'); +"use strict"; +module.exports = require("../src/traces/contour"); diff --git a/lib/contourgl.js b/lib/contourgl.js index 74901df14b4..47f7f4d8b32 100644 --- a/lib/contourgl.js +++ b/lib/contourgl.js @@ -5,7 +5,5 @@ * 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/contourgl'); +"use strict"; +module.exports = require("../src/traces/contourgl"); diff --git a/lib/core.js b/lib/core.js index afec96d280d..46b80c73a1f 100644 --- a/lib/core.js +++ b/lib/core.js @@ -5,7 +5,5 @@ * 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/core'); +"use strict"; +module.exports = require("../src/core"); diff --git a/lib/filter.js b/lib/filter.js index 14ffd87f7e1..4112f572a32 100644 --- a/lib/filter.js +++ b/lib/filter.js @@ -5,7 +5,5 @@ * 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/transforms/filter'); +"use strict"; +module.exports = require("../src/transforms/filter"); diff --git a/lib/groupby.js b/lib/groupby.js index 8ef1dd1f6c2..f0cc35899f4 100644 --- a/lib/groupby.js +++ b/lib/groupby.js @@ -5,7 +5,5 @@ * 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/transforms/groupby'); +"use strict"; +module.exports = require("../src/transforms/groupby"); diff --git a/lib/heatmap.js b/lib/heatmap.js index 0560a47d95a..9aded18216d 100644 --- a/lib/heatmap.js +++ b/lib/heatmap.js @@ -5,7 +5,5 @@ * 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/heatmap'); +"use strict"; +module.exports = require("../src/traces/heatmap"); diff --git a/lib/heatmapgl.js b/lib/heatmapgl.js index 26352a58216..d36c77033c3 100644 --- a/lib/heatmapgl.js +++ b/lib/heatmapgl.js @@ -5,7 +5,5 @@ * 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/heatmapgl'); +"use strict"; +module.exports = require("../src/traces/heatmapgl"); diff --git a/lib/histogram.js b/lib/histogram.js index f704db7fa70..dcb27108677 100644 --- a/lib/histogram.js +++ b/lib/histogram.js @@ -5,7 +5,5 @@ * 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/histogram'); +"use strict"; +module.exports = require("../src/traces/histogram"); diff --git a/lib/histogram2d.js b/lib/histogram2d.js index 69d2f691c03..872ab1eb287 100644 --- a/lib/histogram2d.js +++ b/lib/histogram2d.js @@ -5,7 +5,5 @@ * 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/histogram2d'); +"use strict"; +module.exports = require("../src/traces/histogram2d"); diff --git a/lib/histogram2dcontour.js b/lib/histogram2dcontour.js index 133617a9961..f3094de2f0c 100644 --- a/lib/histogram2dcontour.js +++ b/lib/histogram2dcontour.js @@ -5,7 +5,5 @@ * 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/histogram2dcontour'); +"use strict"; +module.exports = require("../src/traces/histogram2dcontour"); diff --git a/lib/index-basic.js b/lib/index-basic.js index 4c837e413ad..80f5838f296 100644 --- a/lib/index-basic.js +++ b/lib/index-basic.js @@ -5,14 +5,9 @@ * 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 Plotly = require("./core"); -'use strict'; - -var Plotly = require('./core'); - -Plotly.register([ - require('./bar'), - require('./pie') -]); +Plotly.register([require("./bar"), require("./pie")]); module.exports = Plotly; diff --git a/lib/index-cartesian.js b/lib/index-cartesian.js index 4d07f5f5f09..c0243480edd 100644 --- a/lib/index-cartesian.js +++ b/lib/index-cartesian.js @@ -5,21 +5,19 @@ * 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 Plotly = require('./core'); +"use strict"; +var Plotly = require("./core"); Plotly.register([ - require('./bar'), - require('./box'), - require('./heatmap'), - require('./histogram'), - require('./histogram2d'), - require('./histogram2dcontour'), - require('./pie'), - require('./contour'), - require('./scatterternary') + require("./bar"), + require("./box"), + require("./heatmap"), + require("./histogram"), + require("./histogram2d"), + require("./histogram2dcontour"), + require("./pie"), + require("./contour"), + require("./scatterternary") ]); module.exports = Plotly; diff --git a/lib/index-finance.js b/lib/index-finance.js index 4759344b760..486e3ec999e 100644 --- a/lib/index-finance.js +++ b/lib/index-finance.js @@ -5,17 +5,15 @@ * 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 Plotly = require('./core'); +"use strict"; +var Plotly = require("./core"); Plotly.register([ - require('./bar'), - require('./histogram'), - require('./pie'), - require('./ohlc'), - require('./candlestick') + require("./bar"), + require("./histogram"), + require("./pie"), + require("./ohlc"), + require("./candlestick") ]); module.exports = Plotly; diff --git a/lib/index-geo.js b/lib/index-geo.js index 2283f1f0489..aa7aea199f9 100644 --- a/lib/index-geo.js +++ b/lib/index-geo.js @@ -5,14 +5,9 @@ * 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 Plotly = require("./core"); -'use strict'; - -var Plotly = require('./core'); - -Plotly.register([ - require('./scattergeo'), - require('./choropleth') -]); +Plotly.register([require("./scattergeo"), require("./choropleth")]); module.exports = Plotly; diff --git a/lib/index-gl2d.js b/lib/index-gl2d.js index a7738a91744..cfceda4cbf5 100644 --- a/lib/index-gl2d.js +++ b/lib/index-gl2d.js @@ -5,16 +5,14 @@ * 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 Plotly = require('./core'); +"use strict"; +var Plotly = require("./core"); Plotly.register([ - require('./scattergl'), - require('./pointcloud'), - require('./heatmapgl'), - require('./contourgl') + require("./scattergl"), + require("./pointcloud"), + require("./heatmapgl"), + require("./contourgl") ]); module.exports = Plotly; diff --git a/lib/index-gl3d.js b/lib/index-gl3d.js index 7134846cb7d..69464017f61 100644 --- a/lib/index-gl3d.js +++ b/lib/index-gl3d.js @@ -5,15 +5,13 @@ * 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 Plotly = require('./core'); +"use strict"; +var Plotly = require("./core"); Plotly.register([ - require('./scatter3d'), - require('./surface'), - require('./mesh3d') + require("./scatter3d"), + require("./surface"), + require("./mesh3d") ]); module.exports = Plotly; diff --git a/lib/index-mapbox.js b/lib/index-mapbox.js index 17b075b5f49..234182976ad 100644 --- a/lib/index-mapbox.js +++ b/lib/index-mapbox.js @@ -5,13 +5,9 @@ * 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 Plotly = require("./core"); -'use strict'; - -var Plotly = require('./core'); - -Plotly.register([ - require('./scattermapbox') -]); +Plotly.register([require("./scattermapbox")]); module.exports = Plotly; diff --git a/lib/index.js b/lib/index.js index 1cbbb8bba08..79f107f9b22 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,38 +5,31 @@ * 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 Plotly = require('./core'); +"use strict"; +var Plotly = require("./core"); // traces Plotly.register([ - require('./bar'), - require('./box'), - require('./heatmap'), - require('./histogram'), - require('./histogram2d'), - require('./histogram2dcontour'), - require('./pie'), - require('./contour'), - require('./scatterternary'), - - require('./scatter3d'), - require('./surface'), - require('./mesh3d'), - - require('./scattergeo'), - require('./choropleth'), - - require('./scattergl'), - require('./pointcloud'), - require('./heatmapgl'), - - require('./scattermapbox'), - - require('./ohlc'), - require('./candlestick') + require("./bar"), + require("./box"), + require("./heatmap"), + require("./histogram"), + require("./histogram2d"), + require("./histogram2dcontour"), + require("./pie"), + require("./contour"), + require("./scatterternary"), + require("./scatter3d"), + require("./surface"), + require("./mesh3d"), + require("./scattergeo"), + require("./choropleth"), + require("./scattergl"), + require("./pointcloud"), + require("./heatmapgl"), + require("./scattermapbox"), + require("./ohlc"), + require("./candlestick") ]); // transforms @@ -49,14 +42,9 @@ Plotly.register([ // For more info, see: // https://github.com/plotly/plotly.js/pull/978#pullrequestreview-2403353 // -Plotly.register([ - require('./filter'), - require('./groupby') -]); +Plotly.register([require("./filter"), require("./groupby")]); // components -Plotly.register([ - require('./calendars') -]); +Plotly.register([require("./calendars")]); module.exports = Plotly; diff --git a/lib/mesh3d.js b/lib/mesh3d.js index 13453614a45..e6c99f2f39d 100644 --- a/lib/mesh3d.js +++ b/lib/mesh3d.js @@ -5,7 +5,5 @@ * 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/mesh3d'); +"use strict"; +module.exports = require("../src/traces/mesh3d"); diff --git a/lib/ohlc.js b/lib/ohlc.js index 40537cfd011..086c85e64de 100644 --- a/lib/ohlc.js +++ b/lib/ohlc.js @@ -5,7 +5,5 @@ * 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/ohlc'); +"use strict"; +module.exports = require("../src/traces/ohlc"); diff --git a/lib/pie.js b/lib/pie.js index 08f1d09ab76..099234fc945 100644 --- a/lib/pie.js +++ b/lib/pie.js @@ -5,7 +5,5 @@ * 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/pie'); +"use strict"; +module.exports = require("../src/traces/pie"); diff --git a/lib/pointcloud.js b/lib/pointcloud.js index 4d6c7d286e0..edbaa5cebb6 100644 --- a/lib/pointcloud.js +++ b/lib/pointcloud.js @@ -5,7 +5,5 @@ * 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/pointcloud'); +"use strict"; +module.exports = require("../src/traces/pointcloud"); diff --git a/lib/scatter.js b/lib/scatter.js index 589c7bd8b0c..876e9d0045e 100644 --- a/lib/scatter.js +++ b/lib/scatter.js @@ -5,7 +5,5 @@ * 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/scatter'); +"use strict"; +module.exports = require("../src/traces/scatter"); diff --git a/lib/scatter3d.js b/lib/scatter3d.js index c4ee1a63465..cbb8d1ca233 100644 --- a/lib/scatter3d.js +++ b/lib/scatter3d.js @@ -5,7 +5,5 @@ * 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/scatter3d'); +"use strict"; +module.exports = require("../src/traces/scatter3d"); diff --git a/lib/scattergeo.js b/lib/scattergeo.js index 90f892115bb..02421046954 100644 --- a/lib/scattergeo.js +++ b/lib/scattergeo.js @@ -5,7 +5,5 @@ * 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/scattergeo'); +"use strict"; +module.exports = require("../src/traces/scattergeo"); diff --git a/lib/scattergl.js b/lib/scattergl.js index 7c5b6f2ba79..72006b1cc34 100644 --- a/lib/scattergl.js +++ b/lib/scattergl.js @@ -5,7 +5,5 @@ * 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/scattergl'); +"use strict"; +module.exports = require("../src/traces/scattergl"); diff --git a/lib/scattermapbox.js b/lib/scattermapbox.js index 5391bc32630..11933be948c 100644 --- a/lib/scattermapbox.js +++ b/lib/scattermapbox.js @@ -5,7 +5,5 @@ * 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/scattermapbox'); +"use strict"; +module.exports = require("../src/traces/scattermapbox"); diff --git a/lib/scatterternary.js b/lib/scatterternary.js index 2196d3c41ae..0fd0aee69bc 100644 --- a/lib/scatterternary.js +++ b/lib/scatterternary.js @@ -5,7 +5,5 @@ * 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/scatterternary'); +"use strict"; +module.exports = require("../src/traces/scatterternary"); diff --git a/lib/surface.js b/lib/surface.js index d4a3197a2e8..2fc1f1c0bac 100644 --- a/lib/surface.js +++ b/lib/surface.js @@ -5,7 +5,5 @@ * 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/surface'); +"use strict"; +module.exports = require("../src/traces/surface"); diff --git a/package.json b/package.json index f4fbc318a68..e587ce21ec3 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "build": "npm run preprocess && npm run bundle && npm run header && npm run stats", "cibuild": "npm run preprocess && node tasks/cibundle.js", "watch": "node tasks/watch.js", - "lint": "eslint --version && eslint . || true", - "lint-fix": "eslint . --fix", + "lint": "prettier --write \"src/**/*.js\" \"test/**/*.js\" \"lib/**/*.js\"", + "lint-fix": "prettier --write \"src/**/*.js\" \"test/**/*.js\" \"lib/**/*.js\"", "docker": "node tasks/docker.js", "pretest": "node tasks/pretest.js", "test-jasmine": "karma start test/jasmine/karma.conf.js", @@ -117,6 +117,7 @@ "npm-link-check": "^1.2.0", "open": "0.0.5", "prepend-file": "^1.3.1", + "prettier": "^0.16.0", "prettysize": "0.0.3", "requirejs": "^2.3.1", "through2": "^2.0.3", diff --git a/src/assets/geo_assets.js b/src/assets/geo_assets.js index 5222e524f44..8b137891791 100644 --- a/src/assets/geo_assets.js +++ b/src/assets/geo_assets.js @@ -1,17 +1 @@ -/** -* Copyright 2012-2017, 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 saneTopojson = require('sane-topojson'); - - -// package version injected by `npm run preprocess` -exports.version = '1.23.1'; - -exports.topojson = saneTopojson; diff --git a/src/components/annotations/annotation_defaults.js b/src/components/annotations/annotation_defaults.js index 7df91978c59..8b137891791 100644 --- a/src/components/annotations/annotation_defaults.js +++ b/src/components/annotations/annotation_defaults.js @@ -1,107 +1 @@ -/** -* Copyright 2012-2017, 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 Color = require('../color'); -var Axes = require('../../plots/cartesian/axes'); - -var attributes = require('./attributes'); - - -module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, opts, itemOpts) { - opts = opts || {}; - itemOpts = itemOpts || {}; - - function coerce(attr, dflt) { - return Lib.coerce(annIn, annOut, attributes, attr, dflt); - } - - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); - var clickToShow = coerce('clicktoshow'); - - if(!(visible || clickToShow)) return annOut; - - coerce('opacity'); - coerce('align'); - coerce('bgcolor'); - - var borderColor = coerce('bordercolor'), - borderOpacity = Color.opacity(borderColor); - - coerce('borderpad'); - - var borderWidth = coerce('borderwidth'); - var showArrow = coerce('showarrow'); - - coerce('text', showArrow ? ' ' : 'new text'); - coerce('textangle'); - Lib.coerceFont(coerce, 'font', fullLayout.font); - - // positioning - var axLetters = ['x', 'y'], - arrowPosDflt = [-10, -30], - gdMock = {_fullLayout: fullLayout}; - for(var i = 0; i < 2; i++) { - var axLetter = axLetters[i]; - - // xref, yref - var axRef = Axes.coerceRef(annIn, annOut, gdMock, axLetter, '', 'paper'); - - // x, y - Axes.coercePosition(annOut, gdMock, coerce, axRef, axLetter, 0.5); - - if(showArrow) { - var arrowPosAttr = 'a' + axLetter, - // axref, ayref - aaxRef = Axes.coerceRef(annIn, annOut, gdMock, arrowPosAttr, 'pixel'); - - // for now the arrow can only be on the same axis or specified as pixels - // TODO: sometime it might be interesting to allow it to be on *any* axis - // but that would require updates to drawing & autorange code and maybe more - if(aaxRef !== 'pixel' && aaxRef !== axRef) { - aaxRef = annOut[arrowPosAttr] = 'pixel'; - } - - // ax, ay - var aDflt = (aaxRef === 'pixel') ? arrowPosDflt[i] : 0.4; - Axes.coercePosition(annOut, gdMock, coerce, aaxRef, arrowPosAttr, aDflt); - } - - // xanchor, yanchor - coerce(axLetter + 'anchor'); - } - - // if you have one coordinate you should have both - Lib.noneOrAll(annIn, annOut, ['x', 'y']); - - if(showArrow) { - coerce('arrowcolor', borderOpacity ? annOut.bordercolor : Color.defaultLine); - coerce('arrowhead'); - coerce('arrowsize'); - coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2); - coerce('standoff'); - - // if you have one part of arrow length you should have both - Lib.noneOrAll(annIn, annOut, ['ax', 'ay']); - } - - if(clickToShow) { - var xClick = coerce('xclick'); - var yClick = coerce('yclick'); - - // put the actual click data to bind to into private attributes - // so we don't have to do this little bit of logic on every hover event - annOut._xclick = (xClick === undefined) ? annOut.x : xClick; - annOut._yclick = (yClick === undefined) ? annOut.y : yClick; - } - - return annOut; -}; diff --git a/src/components/annotations/arrow_paths.js b/src/components/annotations/arrow_paths.js index 3f27bbaf83a..8b137891791 100644 --- a/src/components/annotations/arrow_paths.js +++ b/src/components/annotations/arrow_paths.js @@ -1,63 +1 @@ -/** -* Copyright 2012-2017, 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'; - -/** - * centerx is a center of scaling tuned for maximum scalability of - * the arrowhead ie throughout mag=0.3..3 the head is joined smoothly - * to the line, but the endpoint moves. - * backoff is the distance to move the arrowhead, and the end of the - * line, in order to end at the right place - * - * TODO: option to have the pointed-to point a little in front of the - * end of the line, as people tend to want a bit of a gap there... - */ - -module.exports = [ - // no arrow - { - path: '', - backoff: 0 - }, - // wide with flat back - { - path: 'M-2.4,-3V3L0.6,0Z', - backoff: 0.6 - }, - // narrower with flat back - { - path: 'M-3.7,-2.5V2.5L1.3,0Z', - backoff: 1.3 - }, - // barbed - { - path: 'M-4.45,-3L-1.65,-0.2V0.2L-4.45,3L1.55,0Z', - backoff: 1.55 - }, - // wide line-drawn - { - path: 'M-2.2,-2.2L-0.2,-0.2V0.2L-2.2,2.2L-1.4,3L1.6,0L-1.4,-3Z', - backoff: 1.6 - }, - // narrower line-drawn - { - path: 'M-4.4,-2.1L-0.6,-0.2V0.2L-4.4,2.1L-4,3L2,0L-4,-3Z', - backoff: 2 - }, - // circle - { - path: 'M2,0A2,2 0 1,1 0,-2A2,2 0 0,1 2,0Z', - backoff: 0 - }, - // square - { - path: 'M2,2V-2H-2V2Z', - backoff: 0 - } -]; diff --git a/src/components/annotations/attributes.js b/src/components/annotations/attributes.js index cdaccd8bc73..8b137891791 100644 --- a/src/components/annotations/attributes.js +++ b/src/components/annotations/attributes.js @@ -1,359 +1 @@ -/** -* Copyright 2012-2017, 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 ARROWPATHS = require('./arrow_paths'); -var fontAttrs = require('../../plots/font_attributes'); -var cartesianConstants = require('../../plots/cartesian/constants'); -var extendFlat = require('../../lib/extend').extendFlat; - - -module.exports = { - _isLinkedToArray: 'annotation', - - visible: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines whether or not this annotation is visible.' - ].join(' ') - }, - - text: { - valType: 'string', - role: 'info', - description: [ - 'Sets the text associated with this annotation.', - 'Plotly uses a subset of HTML tags to do things like', - 'newline (
), bold (), italics (),', - 'hyperlinks (). Tags , , ', - ' are also supported.' - ].join(' ') - }, - textangle: { - valType: 'angle', - dflt: 0, - role: 'style', - description: [ - 'Sets the angle at which the `text` is drawn', - 'with respect to the horizontal.' - ].join(' ') - }, - font: extendFlat({}, fontAttrs, { - description: 'Sets the annotation text font.' - }), - opacity: { - valType: 'number', - min: 0, - max: 1, - dflt: 1, - role: 'style', - description: 'Sets the opacity of the annotation (text + arrow).' - }, - align: { - valType: 'enumerated', - values: ['left', 'center', 'right'], - dflt: 'center', - role: 'style', - description: [ - 'Sets the vertical alignment of the `text` with', - 'respect to the set `x` and `y` position.', - 'Has only an effect if `text` spans more two or more lines', - '(i.e. `text` contains one or more
HTML tags).' - ].join(' ') - }, - bgcolor: { - valType: 'color', - dflt: 'rgba(0,0,0,0)', - role: 'style', - description: 'Sets the background color of the annotation.' - }, - bordercolor: { - valType: 'color', - dflt: 'rgba(0,0,0,0)', - role: 'style', - description: [ - 'Sets the color of the border enclosing the annotation `text`.' - ].join(' ') - }, - borderpad: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: [ - 'Sets the padding (in px) between the `text`', - 'and the enclosing border.' - ].join(' ') - }, - borderwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: [ - 'Sets the width (in px) of the border enclosing', - 'the annotation `text`.' - ].join(' ') - }, - // arrow - showarrow: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Determines whether or not the annotation is drawn with an arrow.', - 'If *true*, `text` is placed near the arrow\'s tail.', - 'If *false*, `text` lines up with the `x` and `y` provided.' - ].join(' ') - }, - arrowcolor: { - valType: 'color', - role: 'style', - description: 'Sets the color of the annotation arrow.' - }, - arrowhead: { - valType: 'integer', - min: 0, - max: ARROWPATHS.length, - dflt: 1, - role: 'style', - description: 'Sets the annotation arrow head style.' - }, - arrowsize: { - valType: 'number', - min: 0.3, - dflt: 1, - role: 'style', - description: 'Sets the size (in px) of annotation arrow head.' - }, - arrowwidth: { - valType: 'number', - min: 0.1, - role: 'style', - description: 'Sets the width (in px) of annotation arrow.' - }, - standoff: { - valType: 'number', - min: 0, - dflt: 0, - role: 'style', - description: [ - 'Sets a distance, in pixels, to move the arrowhead away from the', - 'position it is pointing at, for example to point at the edge of', - 'a marker independent of zoom.' - ].join(' ') - }, - ax: { - valType: 'any', - role: 'info', - description: [ - 'Sets the x component of the arrow tail about the arrow head.', - 'If `axref` is `pixel`, a positive (negative) ', - 'component corresponds to an arrow pointing', - 'from right to left (left to right).', - 'If `axref` is an axis, this is an absolute value on that axis,', - 'like `x`, NOT a relative value.' - ].join(' ') - }, - ay: { - valType: 'any', - role: 'info', - description: [ - 'Sets the y component of the arrow tail about the arrow head.', - 'If `ayref` is `pixel`, a positive (negative) ', - 'component corresponds to an arrow pointing', - 'from bottom to top (top to bottom).', - 'If `ayref` is an axis, this is an absolute value on that axis,', - 'like `y`, NOT a relative value.' - ].join(' ') - }, - axref: { - valType: 'enumerated', - dflt: 'pixel', - values: [ - 'pixel', - cartesianConstants.idRegex.x.toString() - ], - role: 'info', - description: [ - 'Indicates in what terms the tail of the annotation (ax,ay) ', - 'is specified. If `pixel`, `ax` is a relative offset in pixels ', - 'from `x`. If set to an x axis id (e.g. *x* or *x2*), `ax` is ', - 'specified in the same terms as that axis. This is useful ', - 'for trendline annotations which should continue to indicate ', - 'the correct trend when zoomed.' - ].join(' ') - }, - ayref: { - valType: 'enumerated', - dflt: 'pixel', - values: [ - 'pixel', - cartesianConstants.idRegex.y.toString() - ], - role: 'info', - description: [ - 'Indicates in what terms the tail of the annotation (ax,ay) ', - 'is specified. If `pixel`, `ay` is a relative offset in pixels ', - 'from `y`. If set to a y axis id (e.g. *y* or *y2*), `ay` is ', - 'specified in the same terms as that axis. This is useful ', - 'for trendline annotations which should continue to indicate ', - 'the correct trend when zoomed.' - ].join(' ') - }, - // positioning - xref: { - valType: 'enumerated', - values: [ - 'paper', - cartesianConstants.idRegex.x.toString() - ], - role: 'info', - description: [ - 'Sets the annotation\'s x coordinate axis.', - 'If set to an x axis id (e.g. *x* or *x2*), the `x` position', - 'refers to an x coordinate', - 'If set to *paper*, the `x` position refers to the distance from', - 'the left side of the plotting area in normalized coordinates', - 'where 0 (1) corresponds to the left (right) side.' - ].join(' ') - }, - x: { - valType: 'any', - role: 'info', - description: [ - 'Sets the annotation\'s x position.', - 'If the axis `type` is *log*, then you must take the', - 'log of your desired range.', - 'If the axis `type` is *date*, it should be date strings,', - 'like date data, though Date objects and unix milliseconds', - 'will be accepted and converted to strings.', - 'If the axis `type` is *category*, it should be numbers,', - 'using the scale where each category is assigned a serial', - 'number from zero in the order it appears.' - ].join(' ') - }, - xanchor: { - valType: 'enumerated', - values: ['auto', 'left', 'center', 'right'], - dflt: 'auto', - role: 'info', - description: [ - 'Sets the text box\'s horizontal position anchor', - 'This anchor binds the `x` position to the *left*, *center*', - 'or *right* of the annotation.', - 'For example, if `x` is set to 1, `xref` to *paper* and', - '`xanchor` to *right* then the right-most portion of the', - 'annotation lines up with the right-most edge of the', - 'plotting area.', - 'If *auto*, the anchor is equivalent to *center* for', - 'data-referenced annotations or if there is an arrow,', - 'whereas for paper-referenced with no arrow, the anchor picked', - 'corresponds to the closest side.' - ].join(' ') - }, - yref: { - valType: 'enumerated', - values: [ - 'paper', - cartesianConstants.idRegex.y.toString() - ], - role: 'info', - description: [ - 'Sets the annotation\'s y coordinate axis.', - 'If set to an y axis id (e.g. *y* or *y2*), the `y` position', - 'refers to an y coordinate', - 'If set to *paper*, the `y` position refers to the distance from', - 'the bottom of the plotting area in normalized coordinates', - 'where 0 (1) corresponds to the bottom (top).' - ].join(' ') - }, - y: { - valType: 'any', - role: 'info', - description: [ - 'Sets the annotation\'s y position.', - 'If the axis `type` is *log*, then you must take the', - 'log of your desired range.', - 'If the axis `type` is *date*, it should be date strings,', - 'like date data, though Date objects and unix milliseconds', - 'will be accepted and converted to strings.', - 'If the axis `type` is *category*, it should be numbers,', - 'using the scale where each category is assigned a serial', - 'number from zero in the order it appears.' - ].join(' ') - }, - yanchor: { - valType: 'enumerated', - values: ['auto', 'top', 'middle', 'bottom'], - dflt: 'auto', - role: 'info', - description: [ - 'Sets the text box\'s vertical position anchor', - 'This anchor binds the `y` position to the *top*, *middle*', - 'or *bottom* of the annotation.', - 'For example, if `y` is set to 1, `yref` to *paper* and', - '`yanchor` to *top* then the top-most portion of the', - 'annotation lines up with the top-most edge of the', - 'plotting area.', - 'If *auto*, the anchor is equivalent to *middle* for', - 'data-referenced annotations or if there is an arrow,', - 'whereas for paper-referenced with no arrow, the anchor picked', - 'corresponds to the closest side.' - ].join(' ') - }, - clicktoshow: { - valType: 'enumerated', - values: [false, 'onoff', 'onout'], - dflt: false, - role: 'style', - description: [ - 'Makes this annotation respond to clicks on the plot.', - 'If you click a data point that exactly matches the `x` and `y`', - 'values of this annotation, and it is hidden (visible: false),', - 'it will appear. In *onoff* mode, you must click the same point', - 'again to make it disappear, so if you click multiple points,', - 'you can show multiple annotations. In *onout* mode, a click', - 'anywhere else in the plot (on another data point or not) will', - 'hide this annotation.', - 'If you need to show/hide this annotation in response to different', - '`x` or `y` values, you can set `xclick` and/or `yclick`. This is', - 'useful for example to label the side of a bar. To label markers', - 'though, `standoff` is preferred over `xclick` and `yclick`.' - ].join(' ') - }, - xclick: { - valType: 'any', - role: 'info', - description: [ - 'Toggle this annotation when clicking a data point whose `x` value', - 'is `xclick` rather than the annotation\'s `x` value.' - ].join(' ') - }, - yclick: { - valType: 'any', - role: 'info', - description: [ - 'Toggle this annotation when clicking a data point whose `y` value', - 'is `yclick` rather than the annotation\'s `y` value.' - ].join(' ') - }, - - _deprecated: { - ref: { - valType: 'string', - role: 'info', - description: [ - 'Obsolete. Set `xref` and `yref` separately instead.' - ].join(' ') - } - } -}; diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js index f68ea537c63..8b137891791 100644 --- a/src/components/annotations/calc_autorange.js +++ b/src/components/annotations/calc_autorange.js @@ -1,93 +1 @@ -/** -* Copyright 2012-2017, 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 Axes = require('../../plots/cartesian/axes'); - -var draw = require('./draw').draw; - - -module.exports = function calcAutorange(gd) { - var fullLayout = gd._fullLayout, - annotationList = Lib.filterVisible(fullLayout.annotations); - - if(!annotationList.length || !gd._fullData.length) return; - - var annotationAxes = {}; - annotationList.forEach(function(ann) { - annotationAxes[ann.xref] = true; - annotationAxes[ann.yref] = true; - }); - - var autorangedAnnos = Axes.list(gd).filter(function(ax) { - return ax.autorange && annotationAxes[ax._id]; - }); - if(!autorangedAnnos.length) return; - - return Lib.syncOrAsync([ - draw, - annAutorange - ], gd); -}; - -function annAutorange(gd) { - var fullLayout = gd._fullLayout; - - // find the bounding boxes for each of these annotations' - // relative to their anchor points - // use the arrow and the text bg rectangle, - // as the whole anno may include hidden text in its bbox - Lib.filterVisible(fullLayout.annotations).forEach(function(ann) { - var xa = Axes.getFromId(gd, ann.xref), - ya = Axes.getFromId(gd, ann.yref), - headSize = 3 * ann.arrowsize * ann.arrowwidth || 0; - - if(xa && xa.autorange) { - if(ann.axref === ann.xref) { - // expand for the arrowhead (padded by arrowhead) - Axes.expand(xa, [xa.r2c(ann.x)], { - ppadplus: headSize, - ppadminus: headSize - }); - // again for the textbox (padded by textbox) - Axes.expand(xa, [xa.r2c(ann.ax)], { - ppadplus: ann._xpadplus, - ppadminus: ann._xpadminus - }); - } - else { - Axes.expand(xa, [xa.r2c(ann.x)], { - ppadplus: Math.max(ann._xpadplus, headSize), - ppadminus: Math.max(ann._xpadminus, headSize) - }); - } - } - - if(ya && ya.autorange) { - if(ann.ayref === ann.yref) { - Axes.expand(ya, [ya.r2c(ann.y)], { - ppadplus: headSize, - ppadminus: headSize - }); - Axes.expand(ya, [ya.r2c(ann.ay)], { - ppadplus: ann._ypadplus, - ppadminus: ann._ypadminus - }); - } - else { - Axes.expand(ya, [ya.r2c(ann.y)], { - ppadplus: Math.max(ann._ypadplus, headSize), - ppadminus: Math.max(ann._ypadminus, headSize) - }); - } - } - }); -} diff --git a/src/components/annotations/click.js b/src/components/annotations/click.js index 8fe77ce8286..8b137891791 100644 --- a/src/components/annotations/click.js +++ b/src/components/annotations/click.js @@ -1,121 +1 @@ -/** -* Copyright 2012-2017, 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 Plotly = require('../../plotly'); - - -module.exports = { - hasClickToShow: hasClickToShow, - onClick: onClick -}; - -/* - * hasClickToShow: does the given hoverData have ANY annotations which will - * turn ON if we click here? (used by hover events to set cursor) - * - * gd: graphDiv - * hoverData: a hoverData array, as included with the *plotly_hover* or - * *plotly_click* events in the `points` attribute - * - * returns: boolean - */ -function hasClickToShow(gd, hoverData) { - var sets = getToggleSets(gd, hoverData); - return sets.on.length > 0 || sets.explicitOff.length > 0; -} - -/* - * onClick: perform the toggling (via Plotly.update) implied by clicking - * at this hoverData - * - * gd: graphDiv - * hoverData: a hoverData array, as included with the *plotly_hover* or - * *plotly_click* events in the `points` attribute - * - * returns: Promise that the update is complete - */ -function onClick(gd, hoverData) { - var toggleSets = getToggleSets(gd, hoverData), - onSet = toggleSets.on, - offSet = toggleSets.off.concat(toggleSets.explicitOff), - update = {}, - i; - - if(!(onSet.length || offSet.length)) return; - - for(i = 0; i < onSet.length; i++) { - update['annotations[' + onSet[i] + '].visible'] = true; - } - - for(i = 0; i < offSet.length; i++) { - update['annotations[' + offSet[i] + '].visible'] = false; - } - - return Plotly.update(gd, {}, update); -} - -/* - * getToggleSets: find the annotations which will turn on or off at this - * hoverData - * - * gd: graphDiv - * hoverData: a hoverData array, as included with the *plotly_hover* or - * *plotly_click* events in the `points` attribute - * - * returns: { - * on: Array (indices of annotations to turn on), - * off: Array (indices to turn off because you're not hovering on them), - * explicitOff: Array (indices to turn off because you *are* hovering on them) - * } - */ -function getToggleSets(gd, hoverData) { - var annotations = gd._fullLayout.annotations, - onSet = [], - offSet = [], - explicitOffSet = [], - hoverLen = (hoverData || []).length; - - var i, j, anni, showMode, pointj, toggleType; - - for(i = 0; i < annotations.length; i++) { - anni = annotations[i]; - showMode = anni.clicktoshow; - if(showMode) { - for(j = 0; j < hoverLen; j++) { - pointj = hoverData[j]; - if(pointj.x === anni._xclick && pointj.y === anni._yclick && - pointj.xaxis._id === anni.xref && - pointj.yaxis._id === anni.yref) { - // match! toggle this annotation - // regardless of its clicktoshow mode - // but if it's onout mode, off is implicit - if(anni.visible) { - if(showMode === 'onout') toggleType = offSet; - else toggleType = explicitOffSet; - } - else { - toggleType = onSet; - } - toggleType.push(i); - break; - } - } - - if(j === hoverLen) { - // no match - only turn this annotation OFF, and only if - // showmode is 'onout' - if(anni.visible && showMode === 'onout') offSet.push(i); - } - } - } - - return {on: onSet, off: offSet, explicitOff: explicitOffSet}; -} diff --git a/src/components/annotations/defaults.js b/src/components/annotations/defaults.js index a4e9b9b45df..8b137891791 100644 --- a/src/components/annotations/defaults.js +++ b/src/components/annotations/defaults.js @@ -1,23 +1 @@ -/** -* Copyright 2012-2017, 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 handleArrayContainerDefaults = require('../../plots/array_container_defaults'); -var handleAnnotationDefaults = require('./annotation_defaults'); - - -module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { - var opts = { - name: 'annotations', - handleItemDefaults: handleAnnotationDefaults - }; - - handleArrayContainerDefaults(layoutIn, layoutOut, opts); -}; diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index f6cb37b8515..8b137891791 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -1,775 +1 @@ -/** -* Copyright 2012-2017, 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 d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var Plotly = require('../../plotly'); -var Plots = require('../../plots/plots'); -var Lib = require('../../lib'); -var Axes = require('../../plots/cartesian/axes'); -var Color = require('../color'); -var Drawing = require('../drawing'); -var svgTextUtils = require('../../lib/svg_text_utils'); -var setCursor = require('../../lib/setcursor'); -var dragElement = require('../dragelement'); - -var handleAnnotationDefaults = require('./annotation_defaults'); -var supplyLayoutDefaults = require('./defaults'); -var drawArrowHead = require('./draw_arrow_head'); - - -// Annotations are stored in gd.layout.annotations, an array of objects -// index can point to one item in this array, -// or non-numeric to simply add a new one -// or -1 to modify all existing -// opt can be the full options object, or one key (to be set to value) -// or undefined to simply redraw -// if opt is blank, val can be 'add' or a full options object to add a new -// annotation at that point in the array, or 'remove' to delete this one - -module.exports = { - draw: draw, - drawOne: drawOne -}; - -function draw(gd) { - var fullLayout = gd._fullLayout; - - fullLayout._infolayer.selectAll('.annotation').remove(); - - for(var i = 0; i < fullLayout.annotations.length; i++) { - if(fullLayout.annotations[i].visible) { - drawOne(gd, i); - } - } - - return Plots.previousPromises(gd); -} - -function drawOne(gd, index, opt, value) { - var layout = gd.layout, - fullLayout = gd._fullLayout, - i; - - if(!isNumeric(index) || index === -1) { - - // no index provided - we're operating on ALL annotations - if(!index && Array.isArray(value)) { - // a whole annotation array is passed in - // (as in, redo of delete all) - layout.annotations = value; - supplyLayoutDefaults(layout, fullLayout); - draw(gd); - return; - } - else if(value === 'remove') { - // delete all - delete layout.annotations; - fullLayout.annotations = []; - draw(gd); - return; - } - else if(opt && value !== 'add') { - // make the same change to all annotations - for(i = 0; i < fullLayout.annotations.length; i++) { - drawOne(gd, i, opt, value); - } - return; - } - else { - // add a new empty annotation - index = fullLayout.annotations.length; - fullLayout.annotations.push({}); - } - } - - if(!opt && value) { - if(value === 'remove') { - fullLayout._infolayer.selectAll('.annotation[data-index="' + index + '"]') - .remove(); - fullLayout.annotations.splice(index, 1); - layout.annotations.splice(index, 1); - for(i = index; i < fullLayout.annotations.length; i++) { - fullLayout._infolayer - .selectAll('.annotation[data-index="' + (i + 1) + '"]') - .attr('data-index', String(i)); - - // redraw all annotations past the removed one, - // so they bind to the right events - drawOne(gd, i); - } - return; - } - else if(value === 'add' || Lib.isPlainObject(value)) { - fullLayout.annotations.splice(index, 0, {}); - - var rule = Lib.isPlainObject(value) ? - Lib.extendFlat({}, value) : - {text: 'New text'}; - - if(layout.annotations) { - layout.annotations.splice(index, 0, rule); - } else { - layout.annotations = [rule]; - } - - for(i = fullLayout.annotations.length - 1; i > index; i--) { - fullLayout._infolayer - .selectAll('.annotation[data-index="' + (i - 1) + '"]') - .attr('data-index', String(i)); - drawOne(gd, i); - } - } - } - - // remove the existing annotation if there is one - fullLayout._infolayer.selectAll('.annotation[data-index="' + index + '"]').remove(); - - // remember a few things about what was already there, - var optionsIn = layout.annotations[index], - oldPrivate = fullLayout.annotations[index]; - - // not sure how we're getting here... but C12 is seeing a bug - // where we fail here when they add/remove annotations - if(!optionsIn) return; - - // alter the input annotation as requested - var optionsEdit = {}; - if(typeof opt === 'string' && opt) optionsEdit[opt] = value; - else if(Lib.isPlainObject(opt)) optionsEdit = opt; - - var optionKeys = Object.keys(optionsEdit); - for(i = 0; i < optionKeys.length; i++) { - var k = optionKeys[i]; - Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]); - } - - // return early in visible: false updates - if(optionsIn.visible === false) return; - - var gs = fullLayout._size; - var oldRef = {xref: optionsIn.xref, yref: optionsIn.yref}; - - var axLetters = ['x', 'y']; - for(i = 0; i < 2; i++) { - var axLetter = axLetters[i]; - // if we don't have an explicit position already, - // don't set one just because we're changing references - // or axis type. - // the defaults will be consistent most of the time anyway, - // except in log/linear changes - if(optionsEdit[axLetter] !== undefined || - optionsIn[axLetter] === undefined) { - continue; - } - - var axOld = Axes.getFromId(gd, Axes.coerceRef(oldRef, {}, gd, axLetter, '', 'paper')), - axNew = Axes.getFromId(gd, Axes.coerceRef(optionsIn, {}, gd, axLetter, '', 'paper')), - position = optionsIn[axLetter], - axTypeOld = oldPrivate['_' + axLetter + 'type']; - - if(optionsEdit[axLetter + 'ref'] !== undefined) { - - // TODO: include ax / ay / axref / ayref here if not 'pixel' - // or even better, move all of this machinery out of here and into - // streambed as extra attributes to a regular relayout call - // we should do this after v2.0 when it can work equivalently for - // annotations, shapes, and images. - - var autoAnchor = optionsIn[axLetter + 'anchor'] === 'auto', - plotSize = (axLetter === 'x' ? gs.w : gs.h), - halfSizeFrac = (oldPrivate['_' + axLetter + 'size'] || 0) / - (2 * plotSize); - if(axOld && axNew) { // data -> different data - // go to the same fraction of the axis length - // whether or not these axes share a domain - - position = axNew.fraction2r(axOld.r2fraction(position)); - } - else if(axOld) { // data -> paper - // first convert to fraction of the axis - position = axOld.r2fraction(position); - - // next scale the axis to the whole plot - position = axOld.domain[0] + - position * (axOld.domain[1] - axOld.domain[0]); - - // finally see if we need to adjust auto alignment - // because auto always means middle / center alignment for data, - // but it changes for page alignment based on the closest side - if(autoAnchor) { - var posPlus = position + halfSizeFrac, - posMinus = position - halfSizeFrac; - if(position + posMinus < 2 / 3) position = posMinus; - else if(position + posPlus > 4 / 3) position = posPlus; - } - } - else if(axNew) { // paper -> data - // first see if we need to adjust auto alignment - if(autoAnchor) { - if(position < 1 / 3) position += halfSizeFrac; - else if(position > 2 / 3) position -= halfSizeFrac; - } - - // next convert to fraction of the axis - position = (position - axNew.domain[0]) / - (axNew.domain[1] - axNew.domain[0]); - - // finally convert to data coordinates - position = axNew.fraction2r(position); - } - } - - if(axNew && axNew === axOld && axTypeOld) { - if(axTypeOld === 'log' && axNew.type !== 'log') { - position = Math.pow(10, position); - } - else if(axTypeOld !== 'log' && axNew.type === 'log') { - position = (position > 0) ? - Math.log(position) / Math.LN10 : undefined; - } - } - - optionsIn[axLetter] = position; - } - - var options = {}; - handleAnnotationDefaults(optionsIn, options, fullLayout); - fullLayout.annotations[index] = options; - - var xa = Axes.getFromId(gd, options.xref), - ya = Axes.getFromId(gd, options.yref), - - // calculated pixel positions - // x & y each will get text, head, and tail as appropriate - annPosPx = {x: {}, y: {}}, - textangle = +options.textangle || 0; - - // create the components - // made a single group to contain all, so opacity can work right - // with border/arrow together this could handle a whole bunch of - // cleanup at this point, but works for now - var annGroup = fullLayout._infolayer.append('g') - .classed('annotation', true) - .attr('data-index', String(index)) - .style('opacity', options.opacity) - .on('click', function() { - gd._dragging = false; - gd.emit('plotly_clickannotation', { - index: index, - annotation: optionsIn, - fullAnnotation: options - }); - }); - - // another group for text+background so that they can rotate together - var annTextGroup = annGroup.append('g') - .classed('annotation-text-g', true) - .attr('data-index', String(index)); - - var annTextGroupInner = annTextGroup.append('g'); - - var borderwidth = options.borderwidth, - borderpad = options.borderpad, - borderfull = borderwidth + borderpad; - - var annTextBG = annTextGroupInner.append('rect') - .attr('class', 'bg') - .style('stroke-width', borderwidth + 'px') - .call(Color.stroke, options.bordercolor) - .call(Color.fill, options.bgcolor); - - var font = options.font; - - var annText = annTextGroupInner.append('text') - .classed('annotation', true) - .attr('data-unformatted', options.text) - .text(options.text); - - function textLayout(s) { - s.call(Drawing.font, font) - .attr({ - 'text-anchor': { - left: 'start', - right: 'end' - }[options.align] || 'middle' - }); - - svgTextUtils.convertToTspans(s, drawGraphicalElements); - return s; - } - - function drawGraphicalElements() { - - // make sure lines are aligned the way they will be - // at the end, even if their position changes - annText.selectAll('tspan.line').attr({y: 0, x: 0}); - - var mathjaxGroup = annTextGroupInner.select('.annotation-math-group'), - hasMathjax = !mathjaxGroup.empty(), - anntextBB = Drawing.bBox( - (hasMathjax ? mathjaxGroup : annText).node()), - annwidth = anntextBB.width, - annheight = anntextBB.height, - outerwidth = Math.round(annwidth + 2 * borderfull), - outerheight = Math.round(annheight + 2 * borderfull); - - - // save size in the annotation object for use by autoscale - options._w = annwidth; - options._h = annheight; - - function shiftFraction(v, anchor) { - if(anchor === 'auto') { - if(v < 1 / 3) anchor = 'left'; - else if(v > 2 / 3) anchor = 'right'; - else anchor = 'center'; - } - return { - center: 0, - middle: 0, - left: 0.5, - bottom: -0.5, - right: -0.5, - top: 0.5 - }[anchor]; - } - - var annotationIsOffscreen = false; - ['x', 'y'].forEach(function(axLetter) { - var axRef = options[axLetter + 'ref'] || axLetter, - tailRef = options['a' + axLetter + 'ref'], - ax = Axes.getFromId(gd, axRef), - dimAngle = (textangle + (axLetter === 'x' ? 0 : -90)) * Math.PI / 180, - // note that these two can be either positive or negative - annSizeFromWidth = outerwidth * Math.cos(dimAngle), - annSizeFromHeight = outerheight * Math.sin(dimAngle), - // but this one is the positive total size - annSize = Math.abs(annSizeFromWidth) + Math.abs(annSizeFromHeight), - anchor = options[axLetter + 'anchor'], - posPx = annPosPx[axLetter], - basePx, - textPadShift, - alignPosition, - autoAlignFraction, - textShift; - - /* - * calculate the *primary* pixel position - * which is the arrowhead if there is one, - * otherwise the text anchor point - */ - if(ax) { - /* - * hide the annotation if it's pointing outside the visible plot - * as long as the axis isn't autoranged - then we need to draw it - * anyway to get its bounding box. When we're dragging, an axis can - * still look autoranged even though it won't be when the drag finishes. - */ - var posFraction = ax.r2fraction(options[axLetter]); - if((gd._dragging || !ax.autorange) && (posFraction < 0 || posFraction > 1)) { - if(tailRef === axRef) { - posFraction = ax.r2fraction(options['a' + axLetter]); - if(posFraction < 0 || posFraction > 1) { - annotationIsOffscreen = true; - } - } - else { - annotationIsOffscreen = true; - } - - if(annotationIsOffscreen) return; - } - basePx = ax._offset + ax.r2p(options[axLetter]); - autoAlignFraction = 0.5; - } - else { - if(axLetter === 'x') { - alignPosition = options[axLetter]; - basePx = gs.l + gs.w * alignPosition; - } - else { - alignPosition = 1 - options[axLetter]; - basePx = gs.t + gs.h * alignPosition; - } - autoAlignFraction = options.showarrow ? 0.5 : alignPosition; - } - - // now translate this into pixel positions of head, tail, and text - // as well as paddings for autorange - if(options.showarrow) { - posPx.head = basePx; - - var arrowLength = options['a' + axLetter]; - - // with an arrow, the text rotates around the anchor point - textShift = annSizeFromWidth * shiftFraction(0.5, options.xanchor) - - annSizeFromHeight * shiftFraction(0.5, options.yanchor); - - if(tailRef === axRef) { - posPx.tail = ax._offset + ax.r2p(arrowLength); - // tail is data-referenced: autorange pads the text in px from the tail - textPadShift = textShift; - } - else { - posPx.tail = basePx + arrowLength; - // tail is specified in px from head, so autorange also pads vs head - textPadShift = textShift + arrowLength; - } - - posPx.text = posPx.tail + textShift; - - // constrain pixel/paper referenced so the draggers are at least - // partially visible - var maxPx = fullLayout[(axLetter === 'x') ? 'width' : 'height']; - if(axRef === 'paper') { - posPx.head = Lib.constrain(posPx.head, 1, maxPx - 1); - } - if(tailRef === 'pixel') { - var shiftPlus = -Math.max(posPx.tail - 3, posPx.text), - shiftMinus = Math.min(posPx.tail + 3, posPx.text) - maxPx; - if(shiftPlus > 0) { - posPx.tail += shiftPlus; - posPx.text += shiftPlus; - } - else if(shiftMinus > 0) { - posPx.tail -= shiftMinus; - posPx.text -= shiftMinus; - } - } - } - else { - // with no arrow, the text rotates and *then* we put the anchor - // relative to the new bounding box - textShift = annSize * shiftFraction(autoAlignFraction, anchor); - textPadShift = textShift; - posPx.text = basePx + textShift; - } - - options['_' + axLetter + 'padplus'] = (annSize / 2) + textPadShift; - options['_' + axLetter + 'padminus'] = (annSize / 2) - textPadShift; - - // save the current axis type for later log/linear changes - options['_' + axLetter + 'type'] = ax && ax.type; - }); - - if(annotationIsOffscreen) { - annTextGroupInner.remove(); - return; - } - - if(hasMathjax) { - mathjaxGroup.select('svg').attr({x: borderfull - 1, y: borderfull}); - } - else { - var texty = borderfull - anntextBB.top, - textx = borderfull - anntextBB.left; - annText.attr({x: textx, y: texty}); - annText.selectAll('tspan.line').attr({y: texty, x: textx}); - } - - annTextBG.call(Drawing.setRect, borderwidth / 2, borderwidth / 2, - outerwidth - borderwidth, outerheight - borderwidth); - - annTextGroupInner.call(Drawing.setTranslate, - Math.round(annPosPx.x.text - outerwidth / 2), - Math.round(annPosPx.y.text - outerheight / 2)); - - /* - * rotate text and background - * we already calculated the text center position *as rotated* - * because we needed that for autoranging anyway, so now whether - * we have an arrow or not, we rotate about the text center. - */ - annTextGroup.attr({transform: 'rotate(' + textangle + ',' + - annPosPx.x.text + ',' + annPosPx.y.text + ')'}); - - var annbase = 'annotations[' + index + ']'; - - /* - * add the arrow - * uses options[arrowwidth,arrowcolor,arrowhead] for styling - * dx and dy are normally zero, but when you are dragging the textbox - * while the head stays put, dx and dy are the pixel offsets - */ - var drawArrow = function(dx, dy) { - d3.select(gd) - .selectAll('.annotation-arrow-g[data-index="' + index + '"]') - .remove(); - - var headX = annPosPx.x.head, - headY = annPosPx.y.head, - tailX = annPosPx.x.tail + dx, - tailY = annPosPx.y.tail + dy, - textX = annPosPx.x.text + dx, - textY = annPosPx.y.text + dy, - - // find the edge of the text box, where we'll start the arrow: - // create transform matrix to rotate the text box corners - transform = Lib.rotationXYMatrix(textangle, textX, textY), - applyTransform = Lib.apply2DTransform(transform), - applyTransform2 = Lib.apply2DTransform2(transform), - - // calculate and transform bounding box - width = +annTextBG.attr('width'), - height = +annTextBG.attr('height'), - xLeft = textX - 0.5 * width, - xRight = xLeft + width, - yTop = textY - 0.5 * height, - yBottom = yTop + height, - edges = [ - [xLeft, yTop, xLeft, yBottom], - [xLeft, yBottom, xRight, yBottom], - [xRight, yBottom, xRight, yTop], - [xRight, yTop, xLeft, yTop] - ].map(applyTransform2); - - // Remove the line if it ends inside the box. Use ray - // casting for rotated boxes: see which edges intersect a - // line from the arrowhead to far away and reduce with xor - // to get the parity of the number of intersections. - if(edges.reduce(function(a, x) { - return a ^ - !!lineIntersect(headX, headY, headX + 1e6, headY + 1e6, - x[0], x[1], x[2], x[3]); - }, false)) { - // no line or arrow - so quit drawArrow now - return; - } - - edges.forEach(function(x) { - var p = lineIntersect(tailX, tailY, headX, headY, - x[0], x[1], x[2], x[3]); - if(p) { - tailX = p.x; - tailY = p.y; - } - }); - - var strokewidth = options.arrowwidth, - arrowColor = options.arrowcolor; - - var arrowGroup = annGroup.append('g') - .style({opacity: Color.opacity(arrowColor)}) - .classed('annotation-arrow-g', true) - .attr('data-index', String(index)); - - var arrow = arrowGroup.append('path') - .attr('d', 'M' + tailX + ',' + tailY + 'L' + headX + ',' + headY) - .style('stroke-width', strokewidth + 'px') - .call(Color.stroke, Color.rgb(arrowColor)); - - drawArrowHead(arrow, options.arrowhead, 'end', options.arrowsize, options.standoff); - - // the arrow dragger is a small square right at the head, then a line to the tail, - // all expanded by a stroke width of 6px plus the arrow line width - if(gd._context.editable && arrow.node().parentNode) { - var arrowDragHeadX = headX; - var arrowDragHeadY = headY; - if(options.standoff) { - var arrowLength = Math.sqrt(Math.pow(headX - tailX, 2) + Math.pow(headY - tailY, 2)); - arrowDragHeadX += options.standoff * (tailX - headX) / arrowLength; - arrowDragHeadY += options.standoff * (tailY - headY) / arrowLength; - } - var arrowDrag = arrowGroup.append('path') - .classed('annotation', true) - .classed('anndrag', true) - .attr({ - 'data-index': String(index), - d: 'M3,3H-3V-3H3ZM0,0L' + (tailX - arrowDragHeadX) + ',' + (tailY - arrowDragHeadY), - transform: 'translate(' + arrowDragHeadX + ',' + arrowDragHeadY + ')' - }) - .style('stroke-width', (strokewidth + 6) + 'px') - .call(Color.stroke, 'rgba(0,0,0,0)') - .call(Color.fill, 'rgba(0,0,0,0)'); - - var update, - annx0, - anny0; - - // dragger for the arrow & head: translates the whole thing - // (head/tail/text) all together - dragElement.init({ - element: arrowDrag.node(), - prepFn: function() { - var pos = Drawing.getTranslate(annTextGroupInner); - - annx0 = pos.x; - anny0 = pos.y; - update = {}; - if(xa && xa.autorange) { - update[xa._name + '.autorange'] = true; - } - if(ya && ya.autorange) { - update[ya._name + '.autorange'] = true; - } - }, - moveFn: function(dx, dy) { - var annxy0 = applyTransform(annx0, anny0), - xcenter = annxy0[0] + dx, - ycenter = annxy0[1] + dy; - annTextGroupInner.call(Drawing.setTranslate, xcenter, ycenter); - - update[annbase + '.x'] = xa ? - xa.p2r(xa.r2p(options.x) + dx) : - ((headX + dx - gs.l) / gs.w); - update[annbase + '.y'] = ya ? - ya.p2r(ya.r2p(options.y) + dy) : - (1 - ((headY + dy - gs.t) / gs.h)); - - if(options.axref === options.xref) { - update[annbase + '.ax'] = xa ? - xa.p2r(xa.r2p(options.ax) + dx) : - ((headX + dx - gs.l) / gs.w); - } - - if(options.ayref === options.yref) { - update[annbase + '.ay'] = ya ? - ya.p2r(ya.r2p(options.ay) + dy) : - (1 - ((headY + dy - gs.t) / gs.h)); - } - - arrowGroup.attr('transform', 'translate(' + dx + ',' + dy + ')'); - annTextGroup.attr({ - transform: 'rotate(' + textangle + ',' + - xcenter + ',' + ycenter + ')' - }); - }, - doneFn: function(dragged) { - if(dragged) { - Plotly.relayout(gd, update); - var notesBox = document.querySelector('.js-notes-box-panel'); - if(notesBox) notesBox.redraw(notesBox.selectedObj); - } - } - }); - } - }; - - if(options.showarrow) drawArrow(0, 0); - - // user dragging the annotation (text, not arrow) - if(gd._context.editable) { - var update, - baseTextTransform; - - // dragger for the textbox: if there's an arrow, just drag the - // textbox and tail, leave the head untouched - dragElement.init({ - element: annTextGroupInner.node(), - prepFn: function() { - baseTextTransform = annTextGroup.attr('transform'); - update = {}; - }, - moveFn: function(dx, dy) { - var csr = 'pointer'; - if(options.showarrow) { - if(options.axref === options.xref) { - update[annbase + '.ax'] = xa.p2r(xa.r2p(options.ax) + dx); - } else { - update[annbase + '.ax'] = options.ax + dx; - } - - if(options.ayref === options.yref) { - update[annbase + '.ay'] = ya.p2r(ya.r2p(options.ay) + dy); - } else { - update[annbase + '.ay'] = options.ay + dy; - } - - drawArrow(dx, dy); - } - else { - if(xa) update[annbase + '.x'] = options.x + dx / xa._m; - else { - var widthFraction = options._xsize / gs.w, - xLeft = options.x + options._xshift / gs.w - widthFraction / 2; - - update[annbase + '.x'] = dragElement.align(xLeft + dx / gs.w, - widthFraction, 0, 1, options.xanchor); - } - - if(ya) update[annbase + '.y'] = options.y + dy / ya._m; - else { - var heightFraction = options._ysize / gs.h, - yBottom = options.y - options._yshift / gs.h - heightFraction / 2; - - update[annbase + '.y'] = dragElement.align(yBottom - dy / gs.h, - heightFraction, 0, 1, options.yanchor); - } - if(!xa || !ya) { - csr = dragElement.getCursor( - xa ? 0.5 : update[annbase + '.x'], - ya ? 0.5 : update[annbase + '.y'], - options.xanchor, options.yanchor - ); - } - } - - annTextGroup.attr({ - transform: 'translate(' + dx + ',' + dy + ')' + baseTextTransform - }); - - setCursor(annTextGroupInner, csr); - }, - doneFn: function(dragged) { - setCursor(annTextGroupInner); - if(dragged) { - Plotly.relayout(gd, update); - var notesBox = document.querySelector('.js-notes-box-panel'); - if(notesBox) notesBox.redraw(notesBox.selectedObj); - } - } - }); - } - } - - if(gd._context.editable) { - annText.call(svgTextUtils.makeEditable, annTextGroupInner) - .call(textLayout) - .on('edit', function(_text) { - options.text = _text; - this.attr({'data-unformatted': options.text}); - this.call(textLayout); - var update = {}; - update['annotations[' + index + '].text'] = options.text; - if(xa && xa.autorange) { - update[xa._name + '.autorange'] = true; - } - if(ya && ya.autorange) { - update[ya._name + '.autorange'] = true; - } - Plotly.relayout(gd, update); - }); - } - else annText.call(textLayout); -} - -// look for intersection of two line segments -// (1->2 and 3->4) - returns array [x,y] if they do, null if not -function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) { - var a = x2 - x1, - b = x3 - x1, - c = x4 - x3, - d = y2 - y1, - e = y3 - y1, - f = y4 - y3, - det = a * f - c * d; - // parallel lines? intersection is undefined - // ignore the case where they are colinear - if(det === 0) return null; - var t = (b * f - c * e) / det, - u = (b * d - a * e) / det; - // segments do not intersect? - if(u < 0 || u > 1 || t < 0 || t > 1) return null; - - return {x: x1 + a * t, y: y1 + d * t}; -} diff --git a/src/components/annotations/draw_arrow_head.js b/src/components/annotations/draw_arrow_head.js index 69e5181914c..8b137891791 100644 --- a/src/components/annotations/draw_arrow_head.js +++ b/src/components/annotations/draw_arrow_head.js @@ -1,132 +1 @@ -/** -* Copyright 2012-2017, 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 d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var Color = require('../color'); -var Drawing = require('../drawing'); - -var ARROWPATHS = require('./arrow_paths'); - -// add arrowhead(s) to a path or line d3 element el3 -// style: 1-6, first 5 are pointers, 6 is circle, 7 is square, 8 is none -// ends is 'start', 'end' (default), 'start+end' -// mag is magnification vs. default (default 1) - -module.exports = function drawArrowHead(el3, style, ends, mag, standoff) { - if(!isNumeric(mag)) mag = 1; - var el = el3.node(), - headStyle = ARROWPATHS[style||0]; - - if(typeof ends !== 'string' || !ends) ends = 'end'; - - var scale = (Drawing.getPx(el3, 'stroke-width') || 1) * mag, - stroke = el3.style('stroke') || Color.defaultLine, - opacity = el3.style('stroke-opacity') || 1, - doStart = ends.indexOf('start') >= 0, - doEnd = ends.indexOf('end') >= 0, - backOff = headStyle.backoff * scale + standoff, - start, - end, - startRot, - endRot; - - if(el.nodeName === 'line') { - start = {x: +el3.attr('x1'), y: +el3.attr('y1')}; - end = {x: +el3.attr('x2'), y: +el3.attr('y2')}; - - var dx = start.x - end.x, - dy = start.y - end.y; - - startRot = Math.atan2(dy, dx); - endRot = startRot + Math.PI; - if(backOff) { - if(backOff * backOff > dx * dx + dy * dy) { - hideLine(); - return; - } - var backOffX = backOff * Math.cos(startRot), - backOffY = backOff * Math.sin(startRot); - - if(doStart) { - start.x -= backOffX; - start.y -= backOffY; - el3.attr({x1: start.x, y1: start.y}); - } - if(doEnd) { - end.x += backOffX; - end.y += backOffY; - el3.attr({x2: end.x, y2: end.y}); - } - } - } - else if(el.nodeName === 'path') { - var pathlen = el.getTotalLength(), - // using dash to hide the backOff region of the path. - // if we ever allow dash for the arrow we'll have to - // do better than this hack... maybe just manually - // combine the two - dashArray = ''; - - if(pathlen < backOff) { - hideLine(); - return; - } - - if(doStart) { - var start0 = el.getPointAtLength(0), - dstart = el.getPointAtLength(0.1); - startRot = Math.atan2(start0.y - dstart.y, start0.x - dstart.x); - start = el.getPointAtLength(Math.min(backOff, pathlen)); - if(backOff) dashArray = '0px,' + backOff + 'px,'; - } - - if(doEnd) { - var end0 = el.getPointAtLength(pathlen), - dend = el.getPointAtLength(pathlen - 0.1); - endRot = Math.atan2(end0.y - dend.y, end0.x - dend.x); - end = el.getPointAtLength(Math.max(0, pathlen - backOff)); - - if(backOff) { - var shortening = dashArray ? 2 * backOff : backOff; - dashArray += (pathlen - shortening) + 'px,' + pathlen + 'px'; - } - } - else if(dashArray) dashArray += pathlen + 'px'; - - if(dashArray) el3.style('stroke-dasharray', dashArray); - } - - function hideLine() { el3.style('stroke-dasharray', '0px,100px'); } - - function drawhead(p, rot) { - if(!headStyle.path) return; - if(style > 5) rot = 0; // don't rotate square or circle - d3.select(el.parentElement).append('path') - .attr({ - 'class': el3.attr('class'), - d: headStyle.path, - transform: - 'translate(' + p.x + ',' + p.y + ')' + - 'rotate(' + (rot * 180 / Math.PI) + ')' + - 'scale(' + scale + ')' - }) - .style({ - fill: stroke, - opacity: opacity, - 'stroke-width': 0 - }); - } - - if(doStart) drawhead(start, startRot); - if(doEnd) drawhead(end, endRot); -}; diff --git a/src/components/annotations/index.js b/src/components/annotations/index.js index bb32b6b69df..8b137891791 100644 --- a/src/components/annotations/index.js +++ b/src/components/annotations/index.js @@ -1,28 +1 @@ -/** -* Copyright 2012-2017, 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 drawModule = require('./draw'); -var clickModule = require('./click'); - -module.exports = { - moduleType: 'component', - name: 'annotations', - - layoutAttributes: require('./attributes'), - supplyLayoutDefaults: require('./defaults'), - - calcAutorange: require('./calc_autorange'), - draw: drawModule.draw, - drawOne: drawModule.drawOne, - - hasClickToShow: clickModule.hasClickToShow, - onClick: clickModule.onClick -}; diff --git a/src/components/calendars/calendars.js b/src/components/calendars/calendars.js index 2f2a1dec9d5..8b137891791 100644 --- a/src/components/calendars/calendars.js +++ b/src/components/calendars/calendars.js @@ -1,31 +1 @@ -/** -* Copyright 2012-2017, 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'; - -// a trimmed down version of: -// https://github.com/alexcjohnson/world-calendars/blob/master/dist/index.js - -module.exports = require('world-calendars/dist/main'); - -require('world-calendars/dist/plus'); - -require('world-calendars/dist/calendars/chinese'); -require('world-calendars/dist/calendars/coptic'); -require('world-calendars/dist/calendars/discworld'); -require('world-calendars/dist/calendars/ethiopian'); -require('world-calendars/dist/calendars/hebrew'); -require('world-calendars/dist/calendars/islamic'); -require('world-calendars/dist/calendars/julian'); -require('world-calendars/dist/calendars/mayan'); -require('world-calendars/dist/calendars/nanakshahi'); -require('world-calendars/dist/calendars/nepali'); -require('world-calendars/dist/calendars/persian'); -require('world-calendars/dist/calendars/taiwan'); -require('world-calendars/dist/calendars/thai'); -require('world-calendars/dist/calendars/ummalqura'); diff --git a/src/components/calendars/index.js b/src/components/calendars/index.js index eca51c1ac8a..8b137891791 100644 --- a/src/components/calendars/index.js +++ b/src/components/calendars/index.js @@ -1,257 +1 @@ -/** -* Copyright 2012-2017, 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 calendars = require('./calendars'); - -var Lib = require('../../lib'); -var constants = require('../../constants/numerical'); - -var EPOCHJD = constants.EPOCHJD; -var ONEDAY = constants.ONEDAY; - -var attributes = { - valType: 'enumerated', - values: Object.keys(calendars.calendars), - role: 'info', - dflt: 'gregorian' -}; - -var handleDefaults = function(contIn, contOut, attr, dflt) { - var attrs = {}; - attrs[attr] = attributes; - - return Lib.coerce(contIn, contOut, attrs, attr, dflt); -}; - -var handleTraceDefaults = function(traceIn, traceOut, coords, layout) { - for(var i = 0; i < coords.length; i++) { - handleDefaults(traceIn, traceOut, coords[i] + 'calendar', layout.calendar); - } -}; - -// each calendar needs its own default canonical tick. I would love to use -// 2000-01-01 (or even 0000-01-01) for them all but they don't necessarily -// all support either of those dates. Instead I'll use the most significant -// number they *do* support, biased toward the present day. -var CANONICAL_TICK = { - chinese: '2000-01-01', - coptic: '2000-01-01', - discworld: '2000-01-01', - ethiopian: '2000-01-01', - hebrew: '5000-01-01', - islamic: '1000-01-01', - julian: '2000-01-01', - mayan: '5000-01-01', - nanakshahi: '1000-01-01', - nepali: '2000-01-01', - persian: '1000-01-01', - jalali: '1000-01-01', - taiwan: '1000-01-01', - thai: '2000-01-01', - ummalqura: '1400-01-01' -}; - -// Start on a Sunday - for week ticks -// Discworld and Mayan calendars don't have 7-day weeks but we're going to give them -// 7-day week ticks so start on our Sundays. -// If anyone really cares we can customize the auto tick spacings for these calendars. -var CANONICAL_SUNDAY = { - chinese: '2000-01-02', - coptic: '2000-01-03', - discworld: '2000-01-03', - ethiopian: '2000-01-05', - hebrew: '5000-01-01', - islamic: '1000-01-02', - julian: '2000-01-03', - mayan: '5000-01-01', - nanakshahi: '1000-01-05', - nepali: '2000-01-05', - persian: '1000-01-01', - jalali: '1000-01-01', - taiwan: '1000-01-04', - thai: '2000-01-04', - ummalqura: '1400-01-06' -}; - -var DFLTRANGE = { - chinese: ['2000-01-01', '2001-01-01'], - coptic: ['1700-01-01', '1701-01-01'], - discworld: ['1800-01-01', '1801-01-01'], - ethiopian: ['2000-01-01', '2001-01-01'], - hebrew: ['5700-01-01', '5701-01-01'], - islamic: ['1400-01-01', '1401-01-01'], - julian: ['2000-01-01', '2001-01-01'], - mayan: ['5200-01-01', '5201-01-01'], - nanakshahi: ['0500-01-01', '0501-01-01'], - nepali: ['2000-01-01', '2001-01-01'], - persian: ['1400-01-01', '1401-01-01'], - jalali: ['1400-01-01', '1401-01-01'], - taiwan: ['0100-01-01', '0101-01-01'], - thai: ['2500-01-01', '2501-01-01'], - ummalqura: ['1400-01-01', '1401-01-01'] -}; - -/* - * convert d3 templates to world-calendars templates, so our users only need - * to know d3's specifiers. Map space padding to no padding, and unknown fields - * to an ugly placeholder - */ -var UNKNOWN = '##'; -var d3ToWorldCalendars = { - 'd': {'0': 'dd', '-': 'd'}, // 2-digit or unpadded day of month - 'e': {'0': 'd', '-': 'd'}, // alternate, always unpadded day of month - 'a': {'0': 'D', '-': 'D'}, // short weekday name - 'A': {'0': 'DD', '-': 'DD'}, // full weekday name - 'j': {'0': 'oo', '-': 'o'}, // 3-digit or unpadded day of the year - 'W': {'0': 'ww', '-': 'w'}, // 2-digit or unpadded week of the year (Monday first) - 'm': {'0': 'mm', '-': 'm'}, // 2-digit or unpadded month number - 'b': {'0': 'M', '-': 'M'}, // short month name - 'B': {'0': 'MM', '-': 'MM'}, // full month name - 'y': {'0': 'yy', '-': 'yy'}, // 2-digit year (map unpadded to zero-padded) - 'Y': {'0': 'yyyy', '-': 'yyyy'}, // 4-digit year (map unpadded to zero-padded) - 'U': UNKNOWN, // Sunday-first week of the year - 'w': UNKNOWN, // day of the week [0(sunday),6] - // combined format, we replace the date part with the world-calendar version - // and the %X stays there for d3 to handle with time parts - 'c': {'0': 'D M d %X yyyy', '-': 'D M d %X yyyy'}, - 'x': {'0': 'mm/dd/yyyy', '-': 'mm/dd/yyyy'} -}; - -function worldCalFmt(fmt, x, calendar) { - var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD, - cDate = getCal(calendar).fromJD(dateJD), - i = 0, - modifier, directive, directiveLen, directiveObj, replacementPart; - while((i = fmt.indexOf('%', i)) !== -1) { - modifier = fmt.charAt(i + 1); - if(modifier === '0' || modifier === '-' || modifier === '_') { - directiveLen = 3; - directive = fmt.charAt(i + 2); - if(modifier === '_') modifier = '-'; - } - else { - directive = modifier; - modifier = '0'; - directiveLen = 2; - } - directiveObj = d3ToWorldCalendars[directive]; - if(!directiveObj) { - i += directiveLen; - } - else { - // code is recognized as a date part but world-calendars doesn't support it - if(directiveObj === UNKNOWN) replacementPart = UNKNOWN; - - // format the cDate according to the translated directive - else replacementPart = cDate.formatDate(directiveObj[modifier]); - - fmt = fmt.substr(0, i) + replacementPart + fmt.substr(i + directiveLen); - i += replacementPart.length; - } - } - return fmt; -} - -// cache world calendars, so we don't have to reinstantiate -// during each date-time conversion -var allCals = {}; -function getCal(calendar) { - var calendarObj = allCals[calendar]; - if(calendarObj) return calendarObj; - - calendarObj = allCals[calendar] = calendars.instance(calendar); - return calendarObj; -} - -function makeAttrs(description) { - return Lib.extendFlat({}, attributes, { description: description }); -} - -function makeTraceAttrsDescription(coord) { - return 'Sets the calendar system to use with `' + coord + '` date data.'; -} - -var xAttrs = { - xcalendar: makeAttrs(makeTraceAttrsDescription('x')) -}; - -var xyAttrs = Lib.extendFlat({}, xAttrs, { - ycalendar: makeAttrs(makeTraceAttrsDescription('y')) -}); - -var xyzAttrs = Lib.extendFlat({}, xyAttrs, { - zcalendar: makeAttrs(makeTraceAttrsDescription('z')) -}); - -var axisAttrs = makeAttrs([ - 'Sets the calendar system to use for `range` and `tick0`', - 'if this is a date axis. This does not set the calendar for', - 'interpreting data on this axis, that\'s specified in the trace', - 'or via the global `layout.calendar`' -].join(' ')); - -module.exports = { - moduleType: 'component', - name: 'calendars', - - schema: { - traces: { - scatter: xyAttrs, - bar: xyAttrs, - heatmap: xyAttrs, - contour: xyAttrs, - histogram: xyAttrs, - histogram2d: xyAttrs, - histogram2dcontour: xyAttrs, - scatter3d: xyzAttrs, - surface: xyzAttrs, - mesh3d: xyzAttrs, - scattergl: xyAttrs, - ohlc: xAttrs, - candlestick: xAttrs - }, - layout: { - calendar: makeAttrs([ - 'Sets the default calendar system to use for interpreting and', - 'displaying dates throughout the plot.' - ].join(' ')), - 'xaxis.calendar': axisAttrs, - 'yaxis.calendar': axisAttrs, - 'scene.xaxis.calendar': axisAttrs, - 'scene.yaxis.calendar': axisAttrs, - 'scene.zaxis.calendar': axisAttrs - }, - transforms: { - filter: { - valuecalendar: makeAttrs([ - 'Sets the calendar system to use for `value`, if it is a date.' - ].join(' ')), - targetcalendar: makeAttrs([ - 'Sets the calendar system to use for `target`, if it is an', - 'array of dates. If `target` is a string (eg *x*) we use the', - 'corresponding trace attribute (eg `xcalendar`) if it exists,', - 'even if `targetcalendar` is provided.' - ].join(' ')) - } - } - }, - - layoutAttributes: attributes, - - handleDefaults: handleDefaults, - handleTraceDefaults: handleTraceDefaults, - - CANONICAL_SUNDAY: CANONICAL_SUNDAY, - CANONICAL_TICK: CANONICAL_TICK, - DFLTRANGE: DFLTRANGE, - - getCal: getCal, - worldCalFmt: worldCalFmt -}; diff --git a/src/components/color/attributes.js b/src/components/color/attributes.js index e4a5c6d2c35..8b137891791 100644 --- a/src/components/color/attributes.js +++ b/src/components/color/attributes.js @@ -1,38 +1 @@ -/** -* Copyright 2012-2017, 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'; - - -// IMPORTANT - default colors should be in hex for compatibility -exports.defaults = [ - '#1f77b4', // muted blue - '#ff7f0e', // safety orange - '#2ca02c', // cooked asparagus green - '#d62728', // brick red - '#9467bd', // muted purple - '#8c564b', // chestnut brown - '#e377c2', // raspberry yogurt pink - '#7f7f7f', // middle gray - '#bcbd22', // curry yellow-green - '#17becf' // blue-teal -]; - -exports.defaultLine = '#444'; - -exports.lightLine = '#eee'; - -exports.background = '#fff'; - -exports.borderLine = '#BEC8D9'; - -// with axis.color and Color.interp we aren't using lightLine -// itself anymore, instead interpolating between axis.color -// and the background color using tinycolor.mix. lightFraction -// gives back exactly lightLine if the other colors are defaults. -exports.lightFraction = 100 * (0xe - 0x4) / (0xf - 0x4); diff --git a/src/components/color/index.js b/src/components/color/index.js index 714d5a05cad..8b137891791 100644 --- a/src/components/color/index.js +++ b/src/components/color/index.js @@ -1,155 +1 @@ -/** -* Copyright 2012-2017, 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 tinycolor = require('tinycolor2'); -var isNumeric = require('fast-isnumeric'); - -var color = module.exports = {}; - -var colorAttrs = require('./attributes'); -color.defaults = colorAttrs.defaults; -color.defaultLine = colorAttrs.defaultLine; -color.lightLine = colorAttrs.lightLine; -color.background = colorAttrs.background; - -color.tinyRGB = function(tc) { - var c = tc.toRgb(); - return 'rgb(' + Math.round(c.r) + ', ' + - Math.round(c.g) + ', ' + Math.round(c.b) + ')'; -}; - -color.rgb = function(cstr) { return color.tinyRGB(tinycolor(cstr)); }; - -color.opacity = function(cstr) { return cstr ? tinycolor(cstr).getAlpha() : 0; }; - -color.addOpacity = function(cstr, op) { - var c = tinycolor(cstr).toRgb(); - return 'rgba(' + Math.round(c.r) + ', ' + - Math.round(c.g) + ', ' + Math.round(c.b) + ', ' + op + ')'; -}; - -// combine two colors into one apparent color -// if back has transparency or is missing, -// color.background is assumed behind it -color.combine = function(front, back) { - var fc = tinycolor(front).toRgb(); - if(fc.a === 1) return tinycolor(front).toRgbString(); - - var bc = tinycolor(back || color.background).toRgb(), - bcflat = bc.a === 1 ? bc : { - r: 255 * (1 - bc.a) + bc.r * bc.a, - g: 255 * (1 - bc.a) + bc.g * bc.a, - b: 255 * (1 - bc.a) + bc.b * bc.a - }, - fcflat = { - r: bcflat.r * (1 - fc.a) + fc.r * fc.a, - g: bcflat.g * (1 - fc.a) + fc.g * fc.a, - b: bcflat.b * (1 - fc.a) + fc.b * fc.a - }; - return tinycolor(fcflat).toRgbString(); -}; - -color.contrast = function(cstr, lightAmount, darkAmount) { - var tc = tinycolor(cstr); - - var newColor = tc.isLight() ? - tc.darken(darkAmount) : - tc.lighten(lightAmount); - - return newColor.toString(); -}; - -color.stroke = function(s, c) { - var tc = tinycolor(c); - s.style({'stroke': color.tinyRGB(tc), 'stroke-opacity': tc.getAlpha()}); -}; - -color.fill = function(s, c) { - var tc = tinycolor(c); - s.style({ - 'fill': color.tinyRGB(tc), - 'fill-opacity': tc.getAlpha() - }); -}; - -// search container for colors with the deprecated rgb(fractions) format -// and convert them to rgb(0-255 values) -color.clean = function(container) { - if(!container || typeof container !== 'object') return; - - var keys = Object.keys(container), - i, - j, - key, - val; - - for(i = 0; i < keys.length; i++) { - key = keys[i]; - val = container[key]; - - // only sanitize keys that end in "color" or "colorscale" - if(key.substr(key.length - 5) === 'color') { - if(Array.isArray(val)) { - for(j = 0; j < val.length; j++) val[j] = cleanOne(val[j]); - } - else container[key] = cleanOne(val); - } - else if(key.substr(key.length - 10) === 'colorscale' && Array.isArray(val)) { - // colorscales have the format [[0, color1], [frac, color2], ... [1, colorN]] - for(j = 0; j < val.length; j++) { - if(Array.isArray(val[j])) val[j][1] = cleanOne(val[j][1]); - } - } - // recurse into arrays of objects, and plain objects - else if(Array.isArray(val)) { - var el0 = val[0]; - if(!Array.isArray(el0) && el0 && typeof el0 === 'object') { - for(j = 0; j < val.length; j++) color.clean(val[j]); - } - } - else if(val && typeof val === 'object') color.clean(val); - } -}; - -function cleanOne(val) { - if(isNumeric(val) || typeof val !== 'string') return val; - - var valTrim = val.trim(); - if(valTrim.substr(0, 3) !== 'rgb') return val; - - var match = valTrim.match(/^rgba?\s*\(([^()]*)\)$/); - if(!match) return val; - - var parts = match[1].trim().split(/\s*[\s,]\s*/), - rgba = valTrim.charAt(3) === 'a' && parts.length === 4; - if(!rgba && parts.length !== 3) return val; - - for(var i = 0; i < parts.length; i++) { - if(!parts[i].length) return val; - parts[i] = Number(parts[i]); - - // all parts must be non-negative numbers - if(!(parts[i] >= 0)) return val; - // alpha>1 gets clipped to 1 - if(i === 3) { - if(parts[i] > 1) parts[i] = 1; - } - // r, g, b must be < 1 (ie 1 itself is not allowed) - else if(parts[i] >= 1) return val; - } - - var rgbStr = Math.round(parts[0] * 255) + ', ' + - Math.round(parts[1] * 255) + ', ' + - Math.round(parts[2] * 255); - - if(rgba) return 'rgba(' + rgbStr + ', ' + parts[3] + ')'; - return 'rgb(' + rgbStr + ')'; -} diff --git a/src/components/colorbar/attributes.js b/src/components/colorbar/attributes.js index 62f1e031ff8..8b137891791 100644 --- a/src/components/colorbar/attributes.js +++ b/src/components/colorbar/attributes.js @@ -1,194 +1 @@ -/** -* Copyright 2012-2017, 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 axesAttrs = require('../../plots/cartesian/layout_attributes'); -var fontAttrs = require('../../plots/font_attributes'); -var extendFlat = require('../../lib/extend').extendFlat; - - -module.exports = { -// TODO: only right is supported currently -// orient: { -// valType: 'enumerated', -// role: 'info', -// values: ['left', 'right', 'top', 'bottom'], -// dflt: 'right', -// description: [ -// 'Determines which side are the labels on', -// '(so left and right make vertical bars, etc.)' -// ].join(' ') -// }, - thicknessmode: { - valType: 'enumerated', - values: ['fraction', 'pixels'], - role: 'style', - dflt: 'pixels', - description: [ - 'Determines whether this color bar\'s thickness', - '(i.e. the measure in the constant color direction)', - 'is set in units of plot *fraction* or in *pixels*.', - 'Use `thickness` to set the value.' - ].join(' ') - }, - thickness: { - valType: 'number', - role: 'style', - min: 0, - dflt: 30, - description: [ - 'Sets the thickness of the color bar', - 'This measure excludes the size of the padding, ticks and labels.' - ].join(' ') - }, - lenmode: { - valType: 'enumerated', - values: ['fraction', 'pixels'], - role: 'info', - dflt: 'fraction', - description: [ - 'Determines whether this color bar\'s length', - '(i.e. the measure in the color variation direction)', - 'is set in units of plot *fraction* or in *pixels.', - 'Use `len` to set the value.' - ].join(' ') - }, - len: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: [ - 'Sets the length of the color bar', - 'This measure excludes the padding of both ends.', - 'That is, the color bar length is this length minus the', - 'padding on both ends.' - ].join(' ') - }, - x: { - valType: 'number', - dflt: 1.02, - min: -2, - max: 3, - role: 'style', - description: [ - 'Sets the x position of the color bar (in plot fraction).' - ].join(' ') - }, - xanchor: { - valType: 'enumerated', - values: ['left', 'center', 'right'], - dflt: 'left', - role: 'style', - description: [ - 'Sets this color bar\'s horizontal position anchor.', - 'This anchor binds the `x` position to the *left*, *center*', - 'or *right* of the color bar.' - ].join(' ') - }, - xpad: { - valType: 'number', - role: 'style', - min: 0, - dflt: 10, - description: 'Sets the amount of padding (in px) along the x direction.' - }, - y: { - valType: 'number', - role: 'style', - dflt: 0.5, - min: -2, - max: 3, - description: [ - 'Sets the y position of the color bar (in plot fraction).' - ].join(' ') - }, - yanchor: { - valType: 'enumerated', - values: ['top', 'middle', 'bottom'], - role: 'style', - dflt: 'middle', - description: [ - 'Sets this color bar\'s vertical position anchor', - 'This anchor binds the `y` position to the *top*, *middle*', - 'or *bottom* of the color bar.' - ].join(' ') - }, - ypad: { - valType: 'number', - role: 'style', - min: 0, - dflt: 10, - description: 'Sets the amount of padding (in px) along the y direction.' - }, - // a possible line around the bar itself - outlinecolor: axesAttrs.linecolor, - outlinewidth: axesAttrs.linewidth, - // Should outlinewidth have {dflt: 0} ? - // another possible line outside the padding and tick labels - bordercolor: axesAttrs.linecolor, - borderwidth: { - valType: 'number', - role: 'style', - min: 0, - dflt: 0, - description: [ - 'Sets the width (in px) or the border enclosing this color bar.' - ].join(' ') - }, - bgcolor: { - valType: 'color', - role: 'style', - dflt: 'rgba(0,0,0,0)', - description: 'Sets the color of padded area.' - }, - // tick and title properties named and function exactly as in axes - tickmode: axesAttrs.tickmode, - nticks: axesAttrs.nticks, - tick0: axesAttrs.tick0, - dtick: axesAttrs.dtick, - tickvals: axesAttrs.tickvals, - ticktext: axesAttrs.ticktext, - ticks: extendFlat({}, axesAttrs.ticks, {dflt: ''}), - ticklen: axesAttrs.ticklen, - tickwidth: axesAttrs.tickwidth, - tickcolor: axesAttrs.tickcolor, - showticklabels: axesAttrs.showticklabels, - tickfont: axesAttrs.tickfont, - tickangle: axesAttrs.tickangle, - tickformat: axesAttrs.tickformat, - tickprefix: axesAttrs.tickprefix, - showtickprefix: axesAttrs.showtickprefix, - ticksuffix: axesAttrs.ticksuffix, - showticksuffix: axesAttrs.showticksuffix, - separatethousands: axesAttrs.separatethousands, - exponentformat: axesAttrs.exponentformat, - showexponent: axesAttrs.showexponent, - title: { - valType: 'string', - role: 'info', - dflt: 'Click to enter colorscale title', - description: 'Sets the title of the color bar.' - }, - titlefont: extendFlat({}, fontAttrs, { - description: [ - 'Sets this color bar\'s title font.' - ].join(' ') - }), - titleside: { - valType: 'enumerated', - values: ['right', 'top', 'bottom'], - role: 'style', - dflt: 'top', - description: [ - 'Determines the location of the colorbar title', - 'with respect to the color bar.' - ].join(' ') - } -}; diff --git a/src/components/colorbar/defaults.js b/src/components/colorbar/defaults.js index be767c67524..8b137891791 100644 --- a/src/components/colorbar/defaults.js +++ b/src/components/colorbar/defaults.js @@ -1,65 +1 @@ -/** -* Copyright 2012-2017, 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 handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults'); -var handleTickMarkDefaults = require('../../plots/cartesian/tick_mark_defaults'); -var handleTickLabelDefaults = require('../../plots/cartesian/tick_label_defaults'); - -var attributes = require('./attributes'); - - -module.exports = function colorbarDefaults(containerIn, containerOut, layout) { - var colorbarOut = containerOut.colorbar = {}, - colorbarIn = containerIn.colorbar || {}; - - function coerce(attr, dflt) { - return Lib.coerce(colorbarIn, colorbarOut, attributes, attr, dflt); - } - - var thicknessmode = coerce('thicknessmode'); - coerce('thickness', (thicknessmode === 'fraction') ? - 30 / (layout.width - layout.margin.l - layout.margin.r) : - 30 - ); - - var lenmode = coerce('lenmode'); - coerce('len', (lenmode === 'fraction') ? - 1 : - layout.height - layout.margin.t - layout.margin.b - ); - - coerce('x'); - coerce('xanchor'); - coerce('xpad'); - coerce('y'); - coerce('yanchor'); - coerce('ypad'); - Lib.noneOrAll(colorbarIn, colorbarOut, ['x', 'y']); - - coerce('outlinecolor'); - coerce('outlinewidth'); - coerce('bordercolor'); - coerce('borderwidth'); - coerce('bgcolor'); - - handleTickValueDefaults(colorbarIn, colorbarOut, coerce, 'linear'); - - handleTickLabelDefaults(colorbarIn, colorbarOut, coerce, 'linear', - {outerTicks: false, font: layout.font, noHover: true}); - - handleTickMarkDefaults(colorbarIn, colorbarOut, coerce, 'linear', - {outerTicks: false, font: layout.font, noHover: true}); - - coerce('title'); - Lib.coerceFont(coerce, 'titlefont', layout.font); - coerce('titleside'); -}; diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index b7eef65140c..8b137891791 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -1,631 +1 @@ -/** -* Copyright 2012-2017, 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 d3 = require('d3'); -var tinycolor = require('tinycolor2'); - -var Plotly = require('../../plotly'); -var Plots = require('../../plots/plots'); -var Registry = require('../../registry'); -var Axes = require('../../plots/cartesian/axes'); -var dragElement = require('../dragelement'); -var Lib = require('../../lib'); -var extendFlat = require('../../lib/extend').extendFlat; -var setCursor = require('../../lib/setcursor'); -var Drawing = require('../drawing'); -var Color = require('../color'); -var Titles = require('../titles'); - -var handleAxisDefaults = require('../../plots/cartesian/axis_defaults'); -var handleAxisPositionDefaults = require('../../plots/cartesian/position_defaults'); -var axisLayoutAttrs = require('../../plots/cartesian/layout_attributes'); - -var attributes = require('./attributes'); - - -module.exports = function draw(gd, id) { - // opts: options object, containing everything from attributes - // plus a few others that are the equivalent of the colorbar "data" - var opts = {}; - Object.keys(attributes).forEach(function(k) { - opts[k] = null; - }); - // fillcolor can be a d3 scale, domain is z values, range is colors - // or leave it out for no fill, - // or set to a string constant for single-color fill - opts.fillcolor = null; - // line.color has the same options as fillcolor - opts.line = {color: null, width: null, dash: null}; - // levels of lines to draw. - // note that this DOES NOT determine the extent of the bar - // that's given by the domain of fillcolor - // (or line.color if no fillcolor domain) - opts.levels = {start: null, end: null, size: null}; - // separate fill levels (for example, heatmap coloring of a - // contour map) if this is omitted, fillcolors will be - // evaluated halfway between levels - opts.filllevels = null; - - function component() { - var fullLayout = gd._fullLayout, - gs = fullLayout._size; - if((typeof opts.fillcolor !== 'function') && - (typeof opts.line.color !== 'function')) { - fullLayout._infolayer.selectAll('g.' + id).remove(); - return; - } - var zrange = d3.extent(((typeof opts.fillcolor === 'function') ? - opts.fillcolor : opts.line.color).domain()), - linelevels = [], - filllevels = [], - l, - linecolormap = typeof opts.line.color === 'function' ? - opts.line.color : function() { return opts.line.color; }, - fillcolormap = typeof opts.fillcolor === 'function' ? - opts.fillcolor : function() { return opts.fillcolor; }; - - var l0 = opts.levels.end + opts.levels.size / 100, - ls = opts.levels.size, - zr0 = (1.001 * zrange[0] - 0.001 * zrange[1]), - zr1 = (1.001 * zrange[1] - 0.001 * zrange[0]); - for(l = opts.levels.start; (l - l0) * ls < 0; l += ls) { - if(l > zr0 && l < zr1) linelevels.push(l); - } - - if(typeof opts.fillcolor === 'function') { - if(opts.filllevels) { - l0 = opts.filllevels.end + opts.filllevels.size / 100; - ls = opts.filllevels.size; - for(l = opts.filllevels.start; (l - l0) * ls < 0; l += ls) { - if(l > zrange[0] && l < zrange[1]) filllevels.push(l); - } - } - else { - filllevels = linelevels.map(function(v) { - return v - opts.levels.size / 2; - }); - filllevels.push(filllevels[filllevels.length - 1] + - opts.levels.size); - } - } - else if(opts.fillcolor && typeof opts.fillcolor === 'string') { - // doesn't matter what this value is, with a single value - // we'll make a single fill rect covering the whole bar - filllevels = [0]; - } - - if(opts.levels.size < 0) { - linelevels.reverse(); - filllevels.reverse(); - } - - // now make a Plotly Axes object to scale with and draw ticks - // TODO: does not support orientation other than right - - // we calculate pixel sizes based on the specified graph size, - // not the actual (in case something pushed the margins around) - // which is a little odd but avoids an odd iterative effect - // when the colorbar itself is pushing the margins. - // but then the fractional size is calculated based on the - // actual graph size, so that the axes will size correctly. - var originalPlotHeight = fullLayout.height - fullLayout.margin.t - fullLayout.margin.b, - originalPlotWidth = fullLayout.width - fullLayout.margin.l - fullLayout.margin.r, - thickPx = Math.round(opts.thickness * - (opts.thicknessmode === 'fraction' ? originalPlotWidth : 1)), - thickFrac = thickPx / gs.w, - lenPx = Math.round(opts.len * - (opts.lenmode === 'fraction' ? originalPlotHeight : 1)), - lenFrac = lenPx / gs.h, - xpadFrac = opts.xpad / gs.w, - yExtraPx = (opts.borderwidth + opts.outlinewidth) / 2, - ypadFrac = opts.ypad / gs.h, - - // x positioning: do it initially just for left anchor, - // then fix at the end (since we don't know the width yet) - xLeft = Math.round(opts.x * gs.w + opts.xpad), - // for dragging... this is getting a little muddled... - xLeftFrac = opts.x - thickFrac * - ({middle: 0.5, right: 1}[opts.xanchor]||0), - - // y positioning we can do correctly from the start - yBottomFrac = opts.y + lenFrac * - (({top: -0.5, bottom: 0.5}[opts.yanchor] || 0) - 0.5), - yBottomPx = Math.round(gs.h * (1 - yBottomFrac)), - yTopPx = yBottomPx - lenPx, - titleEl, - cbAxisIn = { - type: 'linear', - range: zrange, - tickmode: opts.tickmode, - nticks: opts.nticks, - tick0: opts.tick0, - dtick: opts.dtick, - tickvals: opts.tickvals, - ticktext: opts.ticktext, - ticks: opts.ticks, - ticklen: opts.ticklen, - tickwidth: opts.tickwidth, - tickcolor: opts.tickcolor, - showticklabels: opts.showticklabels, - tickfont: opts.tickfont, - tickangle: opts.tickangle, - tickformat: opts.tickformat, - exponentformat: opts.exponentformat, - separatethousands: opts.separatethousands, - showexponent: opts.showexponent, - showtickprefix: opts.showtickprefix, - tickprefix: opts.tickprefix, - showticksuffix: opts.showticksuffix, - ticksuffix: opts.ticksuffix, - title: opts.title, - titlefont: opts.titlefont, - anchor: 'free', - position: 1 - }, - cbAxisOut = {}, - axisOptions = { - letter: 'y', - font: fullLayout.font, - noHover: true, - calendar: fullLayout.calendar // not really necessary (yet?) - }; - - // Coerce w.r.t. Axes layoutAttributes: - // re-use axes.js logic without updating _fullData - function coerce(attr, dflt) { - return Lib.coerce(cbAxisIn, cbAxisOut, axisLayoutAttrs, attr, dflt); - } - - // Prepare the Plotly axis object - handleAxisDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions); - handleAxisPositionDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions); - - cbAxisOut._id = 'y' + id; - cbAxisOut._gd = gd; - - // position can't go in through supplyDefaults - // because that restricts it to [0,1] - cbAxisOut.position = opts.x + xpadFrac + thickFrac; - - // save for other callers to access this axis - component.axis = cbAxisOut; - - if(['top', 'bottom'].indexOf(opts.titleside) !== -1) { - cbAxisOut.titleside = opts.titleside; - cbAxisOut.titlex = opts.x + xpadFrac; - cbAxisOut.titley = yBottomFrac + - (opts.titleside === 'top' ? lenFrac - ypadFrac : ypadFrac); - } - - if(opts.line.color && opts.tickmode === 'auto') { - cbAxisOut.tickmode = 'linear'; - cbAxisOut.tick0 = opts.levels.start; - var dtick = opts.levels.size; - // expand if too many contours, so we don't get too many ticks - var autoNtick = Lib.constrain( - (yBottomPx - yTopPx) / 50, 4, 15) + 1, - dtFactor = (zrange[1] - zrange[0]) / - ((opts.nticks || autoNtick) * dtick); - if(dtFactor > 1) { - var dtexp = Math.pow(10, Math.floor( - Math.log(dtFactor) / Math.LN10)); - dtick *= dtexp * Lib.roundUp(dtFactor / dtexp, [2, 5, 10]); - // if the contours are at round multiples, reset tick0 - // so they're still at round multiples. Otherwise, - // keep the first label on the first contour level - if((Math.abs(opts.levels.start) / - opts.levels.size + 1e-6) % 1 < 2e-6) { - cbAxisOut.tick0 = 0; - } - } - cbAxisOut.dtick = dtick; - } - - // set domain after init, because we may want to - // allow it outside [0,1] - cbAxisOut.domain = [ - yBottomFrac + ypadFrac, - yBottomFrac + lenFrac - ypadFrac - ]; - cbAxisOut.setScale(); - - // now draw the elements - var container = fullLayout._infolayer.selectAll('g.' + id).data([0]); - container.enter().append('g').classed(id, true) - .each(function() { - var s = d3.select(this); - s.append('rect').classed('cbbg', true); - s.append('g').classed('cbfills', true); - s.append('g').classed('cblines', true); - s.append('g').classed('cbaxis', true).classed('crisp', true); - s.append('g').classed('cbtitleunshift', true) - .append('g').classed('cbtitle', true); - s.append('rect').classed('cboutline', true); - s.select('.cbtitle').datum(0); - }); - container.attr('transform', 'translate(' + Math.round(gs.l) + - ',' + Math.round(gs.t) + ')'); - // TODO: this opposite transform is a hack until we make it - // more rational which items get this offset - var titleCont = container.select('.cbtitleunshift') - .attr('transform', 'translate(-' + - Math.round(gs.l) + ',-' + - Math.round(gs.t) + ')'); - - cbAxisOut._axislayer = container.select('.cbaxis'); - var titleHeight = 0; - if(['top', 'bottom'].indexOf(opts.titleside) !== -1) { - // draw the title so we know how much room it needs - // when we squish the axis. This one only applies to - // top or bottom titles, not right side. - var x = gs.l + (opts.x + xpadFrac) * gs.w, - fontSize = cbAxisOut.titlefont.size, - y; - - if(opts.titleside === 'top') { - y = (1 - (yBottomFrac + lenFrac - ypadFrac)) * gs.h + - gs.t + 3 + fontSize * 0.75; - } - else { - y = (1 - (yBottomFrac + ypadFrac)) * gs.h + - gs.t - 3 - fontSize * 0.25; - } - drawTitle(cbAxisOut._id + 'title', { - attributes: {x: x, y: y, 'text-anchor': 'start'} - }); - } - - function drawAxis() { - if(['top', 'bottom'].indexOf(opts.titleside) !== -1) { - // squish the axis top to make room for the title - var titleGroup = container.select('.cbtitle'), - titleText = titleGroup.select('text'), - titleTrans = - [-opts.outlinewidth / 2, opts.outlinewidth / 2], - mathJaxNode = titleGroup - .select('.h' + cbAxisOut._id + 'title-math-group') - .node(), - lineSize = 15.6; - if(titleText.node()) { - lineSize = - parseInt(titleText.style('font-size'), 10) * 1.3; - } - if(mathJaxNode) { - titleHeight = Drawing.bBox(mathJaxNode).height; - if(titleHeight > lineSize) { - // not entirely sure how mathjax is doing - // vertical alignment, but this seems to work. - titleTrans[1] -= (titleHeight - lineSize) / 2; - } - } - else if(titleText.node() && - !titleText.classed('js-placeholder')) { - titleHeight = Drawing.bBox( - titleGroup.node()).height; - } - if(titleHeight) { - // buffer btwn colorbar and title - // TODO: configurable - titleHeight += 5; - - if(opts.titleside === 'top') { - cbAxisOut.domain[1] -= titleHeight / gs.h; - titleTrans[1] *= -1; - } - else { - cbAxisOut.domain[0] += titleHeight / gs.h; - var nlines = Math.max(1, - titleText.selectAll('tspan.line').size()); - titleTrans[1] += (1 - nlines) * lineSize; - } - - titleGroup.attr('transform', - 'translate(' + titleTrans + ')'); - - cbAxisOut.setScale(); - } - } - - container.selectAll('.cbfills,.cblines,.cbaxis') - .attr('transform', 'translate(0,' + - Math.round(gs.h * (1 - cbAxisOut.domain[1])) + ')'); - - var fills = container.select('.cbfills') - .selectAll('rect.cbfill') - .data(filllevels); - fills.enter().append('rect') - .classed('cbfill', true) - .style('stroke', 'none'); - fills.exit().remove(); - fills.each(function(d, i) { - var z = [ - (i === 0) ? zrange[0] : - (filllevels[i] + filllevels[i - 1]) / 2, - (i === filllevels.length - 1) ? zrange[1] : - (filllevels[i] + filllevels[i + 1]) / 2 - ] - .map(cbAxisOut.c2p) - .map(Math.round); - - // offset the side adjoining the next rectangle so they - // overlap, to prevent antialiasing gaps - if(i !== filllevels.length - 1) { - z[1] += (z[1] > z[0]) ? 1 : -1; - } - - - // Tinycolor can't handle exponents and - // at this scale, removing it makes no difference. - var colorString = fillcolormap(d).replace('e-', ''), - opaqueColor = tinycolor(colorString).toHexString(); - - // Colorbar cannot currently support opacities so we - // use an opaque fill even when alpha channels present - d3.select(this).attr({ - x: xLeft, - width: Math.max(thickPx, 2), - y: d3.min(z), - height: Math.max(d3.max(z) - d3.min(z), 2), - fill: opaqueColor - }); - }); - - var lines = container.select('.cblines') - .selectAll('path.cbline') - .data(opts.line.color && opts.line.width ? - linelevels : []); - lines.enter().append('path') - .classed('cbline', true); - lines.exit().remove(); - lines.each(function(d) { - d3.select(this) - .attr('d', 'M' + xLeft + ',' + - (Math.round(cbAxisOut.c2p(d)) + (opts.line.width / 2) % 1) + - 'h' + thickPx) - .call(Drawing.lineGroupStyle, - opts.line.width, linecolormap(d), opts.line.dash); - }); - - // force full redraw of labels and ticks - cbAxisOut._axislayer.selectAll('g.' + cbAxisOut._id + 'tick,path') - .remove(); - - cbAxisOut._pos = xLeft + thickPx + - (opts.outlinewidth||0) / 2 - (opts.ticks === 'outside' ? 1 : 0); - cbAxisOut.side = 'right'; - - // separate out axis and title drawing, - // so we don't need such complicated logic in Titles.draw - // if title is on the top or bottom, we've already drawn it - // this title call only handles side=right - return Lib.syncOrAsync([ - function() { - return Axes.doTicks(gd, cbAxisOut, true); - }, - function() { - if(['top', 'bottom'].indexOf(opts.titleside) === -1) { - var fontSize = cbAxisOut.titlefont.size, - y = cbAxisOut._offset + cbAxisOut._length / 2, - x = gs.l + (cbAxisOut.position || 0) * gs.w + ((cbAxisOut.side === 'right') ? - 10 + fontSize * ((cbAxisOut.showticklabels ? 1 : 0.5)) : - -10 - fontSize * ((cbAxisOut.showticklabels ? 0.5 : 0))); - - // the 'h' + is a hack to get around the fact that - // convertToTspans rotates any 'y...' class by 90 degrees. - // TODO: find a better way to control this. - drawTitle('h' + cbAxisOut._id + 'title', { - avoid: { - selection: d3.select(gd).selectAll('g.' + cbAxisOut._id + 'tick'), - side: opts.titleside, - offsetLeft: gs.l, - offsetTop: gs.t, - maxShift: fullLayout.width - }, - attributes: {x: x, y: y, 'text-anchor': 'middle'}, - transform: {rotate: '-90', offset: 0} - }); - } - }]); - } - - function drawTitle(titleClass, titleOpts) { - var trace = getTrace(), - propName; - if(Registry.traceIs(trace, 'markerColorscale')) { - propName = 'marker.colorbar.title'; - } - else propName = 'colorbar.title'; - - var dfltTitleOpts = { - propContainer: cbAxisOut, - propName: propName, - traceIndex: trace.index, - dfltName: 'colorscale', - containerGroup: container.select('.cbtitle') - }; - - // this class-to-rotate thing with convertToTspans is - // getting hackier and hackier... delete groups with the - // wrong class (in case earlier the colorbar was drawn on - // a different side, I think?) - var otherClass = titleClass.charAt(0) === 'h' ? - titleClass.substr(1) : ('h' + titleClass); - container.selectAll('.' + otherClass + ',.' + otherClass + '-math-group') - .remove(); - - Titles.draw(gd, titleClass, - extendFlat(dfltTitleOpts, titleOpts || {})); - } - - function positionCB() { - // wait for the axis & title to finish rendering before - // continuing positioning - // TODO: why are we redrawing multiple times now with this? - // I guess autoMargin doesn't like being post-promise? - var innerWidth = thickPx + opts.outlinewidth / 2 + - Drawing.bBox(cbAxisOut._axislayer.node()).width; - titleEl = titleCont.select('text'); - if(titleEl.node() && !titleEl.classed('js-placeholder')) { - var mathJaxNode = titleCont - .select('.h' + cbAxisOut._id + 'title-math-group') - .node(), - titleWidth; - if(mathJaxNode && - ['top', 'bottom'].indexOf(opts.titleside) !== -1) { - titleWidth = Drawing.bBox(mathJaxNode).width; - } - else { - // note: the formula below works for all titlesides, - // (except for top/bottom mathjax, above) - // but the weird gs.l is because the titleunshift - // transform gets removed by Drawing.bBox - titleWidth = - Drawing.bBox(titleCont.node()).right - - xLeft - gs.l; - } - innerWidth = Math.max(innerWidth, titleWidth); - } - - var outerwidth = 2 * opts.xpad + innerWidth + - opts.borderwidth + opts.outlinewidth / 2, - outerheight = yBottomPx - yTopPx; - - container.select('.cbbg').attr({ - x: xLeft - opts.xpad - - (opts.borderwidth + opts.outlinewidth) / 2, - y: yTopPx - yExtraPx, - width: Math.max(outerwidth, 2), - height: Math.max(outerheight + 2 * yExtraPx, 2) - }) - .call(Color.fill, opts.bgcolor) - .call(Color.stroke, opts.bordercolor) - .style({'stroke-width': opts.borderwidth}); - - container.selectAll('.cboutline').attr({ - x: xLeft, - y: yTopPx + opts.ypad + - (opts.titleside === 'top' ? titleHeight : 0), - width: Math.max(thickPx, 2), - height: Math.max(outerheight - 2 * opts.ypad - titleHeight, 2) - }) - .call(Color.stroke, opts.outlinecolor) - .style({ - fill: 'None', - 'stroke-width': opts.outlinewidth - }); - - // fix positioning for xanchor!='left' - var xoffset = ({center: 0.5, right: 1}[opts.xanchor] || 0) * - outerwidth; - container.attr('transform', - 'translate(' + (gs.l - xoffset) + ',' + gs.t + ')'); - - // auto margin adjustment - Plots.autoMargin(gd, id, { - x: opts.x, - y: opts.y, - l: outerwidth * ({right: 1, center: 0.5}[opts.xanchor] || 0), - r: outerwidth * ({left: 1, center: 0.5}[opts.xanchor] || 0), - t: outerheight * ({bottom: 1, middle: 0.5}[opts.yanchor] || 0), - b: outerheight * ({top: 1, middle: 0.5}[opts.yanchor] || 0) - }); - } - - var cbDone = Lib.syncOrAsync([ - Plots.previousPromises, - drawAxis, - Plots.previousPromises, - positionCB - ], gd); - - if(cbDone && cbDone.then) (gd._promises || []).push(cbDone); - - // dragging... - if(gd._context.editable) { - var t0, - xf, - yf; - - dragElement.init({ - element: container.node(), - prepFn: function() { - t0 = container.attr('transform'); - setCursor(container); - }, - moveFn: function(dx, dy) { - container.attr('transform', - t0 + ' ' + 'translate(' + dx + ',' + dy + ')'); - - xf = dragElement.align(xLeftFrac + (dx / gs.w), thickFrac, - 0, 1, opts.xanchor); - yf = dragElement.align(yBottomFrac - (dy / gs.h), lenFrac, - 0, 1, opts.yanchor); - - var csr = dragElement.getCursor(xf, yf, - opts.xanchor, opts.yanchor); - setCursor(container, csr); - }, - doneFn: function(dragged) { - setCursor(container); - - if(dragged && xf !== undefined && yf !== undefined) { - Plotly.restyle(gd, - {'colorbar.x': xf, 'colorbar.y': yf}, - getTrace().index); - } - } - }); - } - return cbDone; - } - - function getTrace() { - var idNum = id.substr(2), - i, - trace; - for(i = 0; i < gd._fullData.length; i++) { - trace = gd._fullData[i]; - if(trace.uid === idNum) return trace; - } - } - - // setter/getters for every item defined in opts - Object.keys(opts).forEach(function(name) { - component[name] = function(v) { - // getter - if(!arguments.length) return opts[name]; - - // setter - for multi-part properties, - // set only the parts that are provided - opts[name] = Lib.isPlainObject(opts[name]) ? - Lib.extendFlat(opts[name], v) : - v; - - return component; - }; - }); - - // or use .options to set multiple options at once via a dictionary - component.options = function(o) { - Object.keys(o).forEach(function(name) { - // in case something random comes through - // that's not an option, ignore it - if(typeof component[name] === 'function') { - component[name](o[name]); - } - }); - return component; - }; - - component._opts = opts; - - return component; -}; diff --git a/src/components/colorbar/has_colorbar.js b/src/components/colorbar/has_colorbar.js index fb32bc8b6cc..8b137891791 100644 --- a/src/components/colorbar/has_colorbar.js +++ b/src/components/colorbar/has_colorbar.js @@ -1,17 +1 @@ -/** -* Copyright 2012-2017, 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'); - - -module.exports = function hasColorbar(container) { - return Lib.isPlainObject(container.colorbar); -}; diff --git a/src/components/colorbar/index.js b/src/components/colorbar/index.js index c0960b78f7c..8b137891791 100644 --- a/src/components/colorbar/index.js +++ b/src/components/colorbar/index.js @@ -1,19 +1 @@ -/** -* Copyright 2012-2017, 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'; - - -exports.attributes = require('./attributes'); - -exports.supplyDefaults = require('./defaults'); - -exports.draw = require('./draw'); - -exports.hasColorbar = require('./has_colorbar'); diff --git a/src/components/colorscale/attributes.js b/src/components/colorscale/attributes.js index bbbfc60e9ff..8b137891791 100644 --- a/src/components/colorscale/attributes.js +++ b/src/components/colorscale/attributes.js @@ -1,71 +1 @@ -/** -* Copyright 2012-2017, 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 = { - zauto: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines the whether or not the color domain is computed', - 'with respect to the input data.' - ].join(' ') - }, - zmin: { - valType: 'number', - role: 'info', - dflt: null, - description: 'Sets the lower bound of color domain.' - }, - zmax: { - valType: 'number', - role: 'info', - dflt: null, - description: 'Sets the upper bound of color domain.' - }, - colorscale: { - valType: 'colorscale', - role: 'style', - description: [ - 'Sets the colorscale.', - 'The colorscale must be an array containing', - 'arrays mapping a normalized value to an', - 'rgb, rgba, hex, hsl, hsv, or named color string.', - 'At minimum, a mapping for the lowest (0) and highest (1)', - 'values are required. For example,', - '`[[0, \'rgb(0,0,255)\', [1, \'rgb(255,0,0)\']]`.', - 'To control the bounds of the colorscale in z space,', - 'use zmin and zmax' - ].join(' ') - }, - autocolorscale: { - valType: 'boolean', - role: 'style', - dflt: true, // gets overrode in 'heatmap' & 'surface' for backwards comp. - description: [ - 'Determines whether or not the colorscale is picked using the sign of', - 'the input z values.' - ].join(' ') - }, - reversescale: { - valType: 'boolean', - role: 'style', - dflt: false, - description: 'Reverses the colorscale.' - }, - showscale: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines whether or not a colorbar is displayed for this trace.' - ].join(' ') - } -}; diff --git a/src/components/colorscale/calc.js b/src/components/colorscale/calc.js index 8d095e8642d..8b137891791 100644 --- a/src/components/colorscale/calc.js +++ b/src/components/colorscale/calc.js @@ -1,77 +1 @@ -/** -* Copyright 2012-2017, 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 scales = require('./scales'); -var flipScale = require('./flip_scale'); - - -module.exports = function calc(trace, vals, containerStr, cLetter) { - var container, inputContainer; - - if(containerStr) { - container = Lib.nestedProperty(trace, containerStr).get(); - inputContainer = Lib.nestedProperty(trace._input, containerStr).get(); - } - else { - container = trace; - inputContainer = trace._input; - } - - var autoAttr = cLetter + 'auto', - minAttr = cLetter + 'min', - maxAttr = cLetter + 'max', - auto = container[autoAttr], - min = container[minAttr], - max = container[maxAttr], - scl = container.colorscale; - - if(auto !== false || min === undefined) { - min = Lib.aggNums(Math.min, null, vals); - } - - if(auto !== false || max === undefined) { - max = Lib.aggNums(Math.max, null, vals); - } - - if(min === max) { - min -= 0.5; - max += 0.5; - } - - container[minAttr] = min; - container[maxAttr] = max; - - inputContainer[minAttr] = min; - inputContainer[maxAttr] = max; - - /* - * If auto was explicitly false but min or max was missing, - * we filled in the missing piece here but later the trace does - * not look auto. - * Otherwise make sure the trace still looks auto as far as later - * changes are concerned. - */ - inputContainer[autoAttr] = (auto !== false || - (min === undefined && max === undefined)); - - if(container.autocolorscale) { - if(min * max < 0) scl = scales.RdBu; - else if(min >= 0) scl = scales.Reds; - else scl = scales.Blues; - - // reversescale is handled at the containerOut level - inputContainer.colorscale = scl; - if(container.reversescale) scl = flipScale(scl); - container.colorscale = scl; - } -}; diff --git a/src/components/colorscale/color_attributes.js b/src/components/colorscale/color_attributes.js index 9c8f6cdb065..8b137891791 100644 --- a/src/components/colorscale/color_attributes.js +++ b/src/components/colorscale/color_attributes.js @@ -1,88 +1 @@ -/** -* Copyright 2012-2017, 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 colorScaleAttributes = require('./attributes'); -var extendDeep = require('../../lib/extend').extendDeep; -var palettes = require('./scales.js'); - -module.exports = function makeColorScaleAttributes(context) { - return { - color: { - valType: 'color', - arrayOk: true, - role: 'style', - description: [ - 'Sets the ', context, ' color. It accepts either a specific color', - ' or an array of numbers that are mapped to the colorscale', - ' relative to the max and min values of the array or relative to', - ' `cmin` and `cmax` if set.' - ].join('') - }, - colorscale: extendDeep({}, colorScaleAttributes.colorscale, { - description: [ - 'Sets the colorscale and only has an effect', - ' if `', context, '.color` is set to a numerical array.', - ' The colorscale must be an array containing', - ' arrays mapping a normalized value to an', - ' rgb, rgba, hex, hsl, hsv, or named color string.', - ' At minimum, a mapping for the lowest (0) and highest (1)', - ' values are required. For example,', - ' `[[0, \'rgb(0,0,255)\', [1, \'rgb(255,0,0)\']]`.', - ' To control the bounds of the colorscale in color space,', - ' use `', context, '.cmin` and `', context, '.cmax`.', - ' Alternatively, `colorscale` may be a palette name string', - ' of the following list: ' - ].join('').concat(Object.keys(palettes).join(', ')) - }), - cauto: extendDeep({}, colorScaleAttributes.zauto, { - description: [ - 'Has an effect only if `', context, '.color` is set to a numerical array', - ' and `cmin`, `cmax` are set by the user. In this case,', - ' it controls whether the range of colors in `colorscale` is mapped to', - ' the range of values in the `color` array (`cauto: true`), or the `cmin`/`cmax`', - ' values (`cauto: false`).', - ' Defaults to `false` when `cmin`, `cmax` are set by the user.' - ].join('') - }), - cmax: extendDeep({}, colorScaleAttributes.zmax, { - description: [ - 'Has an effect only if `', context, '.color` is set to a numerical array.', - ' Sets the upper bound of the color domain.', - ' Value should be associated to the `', context, '.color` array index,', - ' and if set, `', context, '.cmin` must be set as well.' - ].join('') - }), - cmin: extendDeep({}, colorScaleAttributes.zmin, { - description: [ - 'Has an effect only if `', context, '.color` is set to a numerical array.', - ' Sets the lower bound of the color domain.', - ' Value should be associated to the `', context, '.color` array index,', - ' and if set, `', context, '.cmax` must be set as well.' - ].join('') - }), - autocolorscale: extendDeep({}, colorScaleAttributes.autocolorscale, { - description: [ - 'Has an effect only if `', context, '.color` is set to a numerical array.', - ' Determines whether the colorscale is a default palette (`autocolorscale: true`)', - ' or the palette determined by `', context, '.colorscale`.', - ' In case `colorscale` is unspecified or `autocolorscale` is true, the default ', - ' palette will be chosen according to whether numbers in the `color` array are', - ' all positive, all negative or mixed.' - ].join('') - }), - reversescale: extendDeep({}, colorScaleAttributes.reversescale, { - description: [ - 'Has an effect only if `', context, '.color` is set to a numerical array.', - ' Reverses the color mapping if true (`cmin` will correspond to the last color', - ' in the array and `cmax` will correspond to the first color).' - ].join('') - }) - }; -}; diff --git a/src/components/colorscale/default_scale.js b/src/components/colorscale/default_scale.js index 286663dac37..8b137891791 100644 --- a/src/components/colorscale/default_scale.js +++ b/src/components/colorscale/default_scale.js @@ -1,14 +1 @@ -/** -* Copyright 2012-2017, 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 scales = require('./scales'); - - -module.exports = scales.RdBu; diff --git a/src/components/colorscale/defaults.js b/src/components/colorscale/defaults.js index 55444bb4094..8b137891791 100644 --- a/src/components/colorscale/defaults.js +++ b/src/components/colorscale/defaults.js @@ -1,62 +1 @@ -/** -* Copyright 2012-2017, 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 isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); - -var hasColorbar = require('../colorbar/has_colorbar'); -var colorbarDefaults = require('../colorbar/defaults'); -var isValidScale = require('./is_valid_scale'); -var flipScale = require('./flip_scale'); - - -module.exports = function colorScaleDefaults(traceIn, traceOut, layout, coerce, opts) { - var prefix = opts.prefix, - cLetter = opts.cLetter, - containerStr = prefix.slice(0, prefix.length - 1), - containerIn = prefix ? - Lib.nestedProperty(traceIn, containerStr).get() || {} : - traceIn, - containerOut = prefix ? - Lib.nestedProperty(traceOut, containerStr).get() || {} : - traceOut, - minIn = containerIn[cLetter + 'min'], - maxIn = containerIn[cLetter + 'max'], - sclIn = containerIn.colorscale; - - var validMinMax = isNumeric(minIn) && isNumeric(maxIn) && (minIn < maxIn); - coerce(prefix + cLetter + 'auto', !validMinMax); - coerce(prefix + cLetter + 'min'); - coerce(prefix + cLetter + 'max'); - - // handles both the trace case (autocolorscale is false by default) and - // the marker and marker.line case (autocolorscale is true by default) - var autoColorscaleDftl; - if(sclIn !== undefined) autoColorscaleDftl = !isValidScale(sclIn); - coerce(prefix + 'autocolorscale', autoColorscaleDftl); - var sclOut = coerce(prefix + 'colorscale'); - - // reversescale is handled at the containerOut level - var reverseScale = coerce(prefix + 'reversescale'); - if(reverseScale) containerOut.colorscale = flipScale(sclOut); - - // ... until Scatter.colorbar can handle marker line colorbars - if(prefix === 'marker.line.') return; - - // handle both the trace case where the dflt is listed in attributes and - // the marker case where the dflt is determined by hasColorbar - var showScaleDftl; - if(prefix) showScaleDftl = hasColorbar(containerIn); - var showScale = coerce(prefix + 'showscale', showScaleDftl); - - if(showScale) colorbarDefaults(containerIn, containerOut, layout); -}; diff --git a/src/components/colorscale/extract_scale.js b/src/components/colorscale/extract_scale.js index d1e3c83d4ff..8b137891791 100644 --- a/src/components/colorscale/extract_scale.js +++ b/src/components/colorscale/extract_scale.js @@ -1,35 +1 @@ -/** -* Copyright 2012-2017, 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'; - -/** - * Extract colorscale into numeric domain and color range. - * - * @param {array} scl colorscale array of arrays - * @param {number} cmin minimum color value (used to clamp scale) - * @param {number} cmax maximum color value (used to clamp scale) - */ -module.exports = function extractScale(scl, cmin, cmax) { - var N = scl.length, - domain = new Array(N), - range = new Array(N); - - for(var i = 0; i < N; i++) { - var si = scl[i]; - - domain[i] = cmin + si[0] * (cmax - cmin); - range[i] = si[1]; - } - - return { - domain: domain, - range: range - }; -}; diff --git a/src/components/colorscale/flip_scale.js b/src/components/colorscale/flip_scale.js index 5e974846ea6..8b137891791 100644 --- a/src/components/colorscale/flip_scale.js +++ b/src/components/colorscale/flip_scale.js @@ -1,23 +1 @@ -/** -* Copyright 2012-2017, 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 = function flipScale(scl) { - var N = scl.length, - sclNew = new Array(N), - si; - - for(var i = N - 1, j = 0; i >= 0; i--, j++) { - si = scl[i]; - sclNew[j] = [1 - si[0], si[1]]; - } - - return sclNew; -}; diff --git a/src/components/colorscale/get_scale.js b/src/components/colorscale/get_scale.js index 1f88c328a42..8b137891791 100644 --- a/src/components/colorscale/get_scale.js +++ b/src/components/colorscale/get_scale.js @@ -1,38 +1 @@ -/** -* Copyright 2012-2017, 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 scales = require('./scales'); -var defaultScale = require('./default_scale'); -var isValidScaleArray = require('./is_valid_scale_array'); - - -module.exports = function getScale(scl, dflt) { - if(!dflt) dflt = defaultScale; - if(!scl) return dflt; - - function parseScale() { - try { - scl = scales[scl] || JSON.parse(scl); - } - catch(e) { - scl = dflt; - } - } - - if(typeof scl === 'string') { - parseScale(); - // occasionally scl is double-JSON encoded... - if(typeof scl === 'string') parseScale(); - } - - if(!isValidScaleArray(scl)) return dflt; - return scl; -}; diff --git a/src/components/colorscale/has_colorscale.js b/src/components/colorscale/has_colorscale.js index 2744e956442..8b137891791 100644 --- a/src/components/colorscale/has_colorscale.js +++ b/src/components/colorscale/has_colorscale.js @@ -1,44 +1 @@ -/** -* Copyright 2012-2017, 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 isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); - -var isValidScale = require('./is_valid_scale'); - - -module.exports = function hasColorscale(trace, containerStr) { - var container = containerStr ? - Lib.nestedProperty(trace, containerStr).get() || {} : - trace, - color = container.color, - isArrayWithOneNumber = false; - - if(Array.isArray(color)) { - for(var i = 0; i < color.length; i++) { - if(isNumeric(color[i])) { - isArrayWithOneNumber = true; - break; - } - } - } - - return ( - Lib.isPlainObject(container) && ( - isArrayWithOneNumber || - container.showscale === true || - (isNumeric(container.cmin) && isNumeric(container.cmax)) || - isValidScale(container.colorscale) || - Lib.isPlainObject(container.colorbar) - ) - ); -}; diff --git a/src/components/colorscale/index.js b/src/components/colorscale/index.js index 0e07e23c32c..8b137891791 100644 --- a/src/components/colorscale/index.js +++ b/src/components/colorscale/index.js @@ -1,32 +1 @@ -/** -* Copyright 2012-2017, 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'; - -exports.scales = require('./scales'); - -exports.defaultScale = require('./default_scale'); - -exports.attributes = require('./attributes'); - -exports.handleDefaults = require('./defaults'); - -exports.calc = require('./calc'); - -exports.hasColorscale = require('./has_colorscale'); - -exports.isValidScale = require('./is_valid_scale'); - -exports.getScale = require('./get_scale'); - -exports.flipScale = require('./flip_scale'); - -exports.extractScale = require('./extract_scale'); - -exports.makeColorScaleFunc = require('./make_color_scale_func'); diff --git a/src/components/colorscale/is_valid_scale.js b/src/components/colorscale/is_valid_scale.js index f3137486694..8b137891791 100644 --- a/src/components/colorscale/is_valid_scale.js +++ b/src/components/colorscale/is_valid_scale.js @@ -1,19 +1 @@ -/** -* Copyright 2012-2017, 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 scales = require('./scales'); -var isValidScaleArray = require('./is_valid_scale_array'); - - -module.exports = function isValidScale(scl) { - if(scales[scl] !== undefined) return true; - else return isValidScaleArray(scl); -}; diff --git a/src/components/colorscale/is_valid_scale_array.js b/src/components/colorscale/is_valid_scale_array.js index 324b576b50f..8b137891791 100644 --- a/src/components/colorscale/is_valid_scale_array.js +++ b/src/components/colorscale/is_valid_scale_array.js @@ -1,35 +1 @@ -/** -* Copyright 2012-2017, 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 tinycolor = require('tinycolor2'); - - -module.exports = function isValidScaleArray(scl) { - var highestVal = 0; - - if(!Array.isArray(scl) || scl.length < 2) return false; - - if(!scl[0] || !scl[scl.length - 1]) return false; - - if(+scl[0][0] !== 0 || +scl[scl.length - 1][0] !== 1) return false; - - for(var i = 0; i < scl.length; i++) { - var si = scl[i]; - - if(si.length !== 2 || +si[0] < highestVal || !tinycolor(si[1]).isValid()) { - return false; - } - - highestVal = +si[0]; - } - - return true; -}; diff --git a/src/components/colorscale/make_color_scale_func.js b/src/components/colorscale/make_color_scale_func.js index 562e104b00a..8b137891791 100644 --- a/src/components/colorscale/make_color_scale_func.js +++ b/src/components/colorscale/make_color_scale_func.js @@ -1,94 +1 @@ -/** -* Copyright 2012-2017, 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 d3 = require('d3'); -var tinycolor = require('tinycolor2'); -var isNumeric = require('fast-isnumeric'); - -var Color = require('../color'); - -/** - * General colorscale function generator. - * - * @param {object} specs output of Colorscale.extractScale or precomputed domain, range. - * - domain {array} - * - range {array} - * - * @param {object} opts - * - noNumericCheck {boolean} if true, scale func bypasses numeric checks - * - returnArray {boolean} if true, scale func return 4-item array instead of color strings - * - * @return {function} - */ -module.exports = function makeColorScaleFunc(specs, opts) { - opts = opts || {}; - - var domain = specs.domain, - range = specs.range, - N = range.length, - _range = new Array(N); - - for(var i = 0; i < N; i++) { - var rgba = tinycolor(range[i]).toRgb(); - _range[i] = [rgba.r, rgba.g, rgba.b, rgba.a]; - } - - var _sclFunc = d3.scale.linear() - .domain(domain) - .range(_range) - .clamp(true); - - var noNumericCheck = opts.noNumericCheck, - returnArray = opts.returnArray, - sclFunc; - - if(noNumericCheck && returnArray) { - sclFunc = _sclFunc; - } - else if(noNumericCheck) { - sclFunc = function(v) { - return colorArray2rbga(_sclFunc(v)); - }; - } - else if(returnArray) { - sclFunc = function(v) { - if(isNumeric(v)) return _sclFunc(v); - else if(tinycolor(v).isValid()) return v; - else return Color.defaultLine; - }; - } - else { - sclFunc = function(v) { - if(isNumeric(v)) return colorArray2rbga(_sclFunc(v)); - else if(tinycolor(v).isValid()) return v; - else return Color.defaultLine; - }; - } - - // colorbar draw looks into the d3 scale closure for domain and range - - sclFunc.domain = _sclFunc.domain; - - sclFunc.range = function() { return range; }; - - return sclFunc; -}; - -function colorArray2rbga(colorArray) { - var colorObj = { - r: colorArray[0], - g: colorArray[1], - b: colorArray[2], - a: colorArray[3] - }; - - return tinycolor(colorObj).toRgbString(); -} diff --git a/src/components/colorscale/scales.js b/src/components/colorscale/scales.js index 1993b937264..8b137891791 100644 --- a/src/components/colorscale/scales.js +++ b/src/components/colorscale/scales.js @@ -1,129 +1 @@ -/** -* Copyright 2012-2017, 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 = { - 'Greys': [ - [0, 'rgb(0,0,0)'], [1, 'rgb(255,255,255)'] - ], - - 'YlGnBu': [ - [0, 'rgb(8,29,88)'], [0.125, 'rgb(37,52,148)'], - [0.25, 'rgb(34,94,168)'], [0.375, 'rgb(29,145,192)'], - [0.5, 'rgb(65,182,196)'], [0.625, 'rgb(127,205,187)'], - [0.75, 'rgb(199,233,180)'], [0.875, 'rgb(237,248,217)'], - [1, 'rgb(255,255,217)'] - ], - - 'Greens': [ - [0, 'rgb(0,68,27)'], [0.125, 'rgb(0,109,44)'], - [0.25, 'rgb(35,139,69)'], [0.375, 'rgb(65,171,93)'], - [0.5, 'rgb(116,196,118)'], [0.625, 'rgb(161,217,155)'], - [0.75, 'rgb(199,233,192)'], [0.875, 'rgb(229,245,224)'], - [1, 'rgb(247,252,245)'] - ], - - 'YlOrRd': [ - [0, 'rgb(128,0,38)'], [0.125, 'rgb(189,0,38)'], - [0.25, 'rgb(227,26,28)'], [0.375, 'rgb(252,78,42)'], - [0.5, 'rgb(253,141,60)'], [0.625, 'rgb(254,178,76)'], - [0.75, 'rgb(254,217,118)'], [0.875, 'rgb(255,237,160)'], - [1, 'rgb(255,255,204)'] - ], - - 'Bluered': [ - [0, 'rgb(0,0,255)'], [1, 'rgb(255,0,0)'] - ], - - // modified RdBu based on - // www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf - 'RdBu': [ - [0, 'rgb(5,10,172)'], [0.35, 'rgb(106,137,247)'], - [0.5, 'rgb(190,190,190)'], [0.6, 'rgb(220,170,132)'], - [0.7, 'rgb(230,145,90)'], [1, 'rgb(178,10,28)'] - ], - - // Scale for non-negative numeric values - 'Reds': [ - [0, 'rgb(220,220,220)'], [0.2, 'rgb(245,195,157)'], - [0.4, 'rgb(245,160,105)'], [1, 'rgb(178,10,28)'] - ], - - // Scale for non-positive numeric values - 'Blues': [ - [0, 'rgb(5,10,172)'], [0.35, 'rgb(40,60,190)'], - [0.5, 'rgb(70,100,245)'], [0.6, 'rgb(90,120,245)'], - [0.7, 'rgb(106,137,247)'], [1, 'rgb(220,220,220)'] - ], - - 'Picnic': [ - [0, 'rgb(0,0,255)'], [0.1, 'rgb(51,153,255)'], - [0.2, 'rgb(102,204,255)'], [0.3, 'rgb(153,204,255)'], - [0.4, 'rgb(204,204,255)'], [0.5, 'rgb(255,255,255)'], - [0.6, 'rgb(255,204,255)'], [0.7, 'rgb(255,153,255)'], - [0.8, 'rgb(255,102,204)'], [0.9, 'rgb(255,102,102)'], - [1, 'rgb(255,0,0)'] - ], - - 'Rainbow': [ - [0, 'rgb(150,0,90)'], [0.125, 'rgb(0,0,200)'], - [0.25, 'rgb(0,25,255)'], [0.375, 'rgb(0,152,255)'], - [0.5, 'rgb(44,255,150)'], [0.625, 'rgb(151,255,0)'], - [0.75, 'rgb(255,234,0)'], [0.875, 'rgb(255,111,0)'], - [1, 'rgb(255,0,0)'] - ], - - 'Portland': [ - [0, 'rgb(12,51,131)'], [0.25, 'rgb(10,136,186)'], - [0.5, 'rgb(242,211,56)'], [0.75, 'rgb(242,143,56)'], - [1, 'rgb(217,30,30)'] - ], - - 'Jet': [ - [0, 'rgb(0,0,131)'], [0.125, 'rgb(0,60,170)'], - [0.375, 'rgb(5,255,255)'], [0.625, 'rgb(255,255,0)'], - [0.875, 'rgb(250,0,0)'], [1, 'rgb(128,0,0)'] - ], - - 'Hot': [ - [0, 'rgb(0,0,0)'], [0.3, 'rgb(230,0,0)'], - [0.6, 'rgb(255,210,0)'], [1, 'rgb(255,255,255)'] - ], - - 'Blackbody': [ - [0, 'rgb(0,0,0)'], [0.2, 'rgb(230,0,0)'], - [0.4, 'rgb(230,210,0)'], [0.7, 'rgb(255,255,255)'], - [1, 'rgb(160,200,255)'] - ], - - 'Earth': [ - [0, 'rgb(0,0,130)'], [0.1, 'rgb(0,180,180)'], - [0.2, 'rgb(40,210,40)'], [0.4, 'rgb(230,230,50)'], - [0.6, 'rgb(120,70,20)'], [1, 'rgb(255,255,255)'] - ], - - 'Electric': [ - [0, 'rgb(0,0,0)'], [0.15, 'rgb(30,0,100)'], - [0.4, 'rgb(120,0,100)'], [0.6, 'rgb(160,90,0)'], - [0.8, 'rgb(230,200,0)'], [1, 'rgb(255,250,220)'] - ], - - 'Viridis': [ - [0, '#440154'], [0.06274509803921569, '#48186a'], - [0.12549019607843137, '#472d7b'], [0.18823529411764706, '#424086'], - [0.25098039215686274, '#3b528b'], [0.3137254901960784, '#33638d'], - [0.3764705882352941, '#2c728e'], [0.4392156862745098, '#26828e'], - [0.5019607843137255, '#21918c'], [0.5647058823529412, '#1fa088'], - [0.6274509803921569, '#28ae80'], [0.6901960784313725, '#3fbc73'], - [0.7529411764705882, '#5ec962'], [0.8156862745098039, '#84d44b'], - [0.8784313725490196, '#addc30'], [0.9411764705882353, '#d8e219'], - [1, '#fde725'] - ] -}; diff --git a/src/components/dragelement/align.js b/src/components/dragelement/align.js index 9503473ed6c..8b137891791 100644 --- a/src/components/dragelement/align.js +++ b/src/components/dragelement/align.js @@ -1,31 +1 @@ -/** -* Copyright 2012-2017, 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'; - - -// for automatic alignment on dragging, <1/3 means left align, -// >2/3 means right, and between is center. Pick the right fraction -// based on where you are, and return the fraction corresponding to -// that position on the object -module.exports = function align(v, dv, v0, v1, anchor) { - var vmin = (v - v0) / (v1 - v0), - vmax = vmin + dv / (v1 - v0), - vc = (vmin + vmax) / 2; - - // explicitly specified anchor - if(anchor === 'left' || anchor === 'bottom') return vmin; - if(anchor === 'center' || anchor === 'middle') return vc; - if(anchor === 'right' || anchor === 'top') return vmax; - - // automatic based on position - if(vmin < (2 / 3) - vc) return vmin; - if(vmax > (4 / 3) - vc) return vmax; - return vc; -}; diff --git a/src/components/dragelement/cursor.js b/src/components/dragelement/cursor.js index 5601c8aaa30..8b137891791 100644 --- a/src/components/dragelement/cursor.js +++ b/src/components/dragelement/cursor.js @@ -1,36 +1 @@ -/** -* Copyright 2012-2017, 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'); - - -// set cursors pointing toward the closest corner/side, -// to indicate alignment -// x and y are 0-1, fractions of the plot area -var cursorset = [ - ['sw-resize', 's-resize', 'se-resize'], - ['w-resize', 'move', 'e-resize'], - ['nw-resize', 'n-resize', 'ne-resize'] -]; - -module.exports = function getCursor(x, y, xanchor, yanchor) { - if(xanchor === 'left') x = 0; - else if(xanchor === 'center') x = 1; - else if(xanchor === 'right') x = 2; - else x = Lib.constrain(Math.floor(x * 3), 0, 2); - - if(yanchor === 'bottom') y = 0; - else if(yanchor === 'middle') y = 1; - else if(yanchor === 'top') y = 2; - else y = Lib.constrain(Math.floor(y * 3), 0, 2); - - return cursorset[y][x]; -}; diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index 8b531a1b5a8..8b137891791 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -1,185 +1 @@ -/** -* Copyright 2012-2017, 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 Plotly = require('../../plotly'); -var Lib = require('../../lib'); - -var constants = require('../../plots/cartesian/constants'); - - -var dragElement = module.exports = {}; - -dragElement.align = require('./align'); -dragElement.getCursor = require('./cursor'); - -var unhover = require('./unhover'); -dragElement.unhover = unhover.wrapped; -dragElement.unhoverRaw = unhover.raw; - -/** - * Abstracts click & drag interactions - * @param {object} options with keys: - * element (required) the DOM element to drag - * prepFn (optional) function(event, startX, startY) - * executed on mousedown - * startX and startY are the clientX and clientY pixel position - * of the mousedown event - * moveFn (optional) function(dx, dy, dragged) - * executed on move - * dx and dy are the net pixel offset of the drag, - * dragged is true/false, has the mouse moved enough to - * constitute a drag - * doneFn (optional) function(dragged, numClicks) - * executed on mouseup, or mouseout of window since - * we don't get events after that - * dragged is as in moveFn - * numClicks is how many clicks we've registered within - * a doubleclick time - * setCursor (optional) function(event) - * executed on mousemove before mousedown - * the purpose of this callback is to update the mouse cursor before - * the click & drag interaction has been initiated - */ -dragElement.init = function init(options) { - var gd = Lib.getPlotDiv(options.element) || {}, - numClicks = 1, - DBLCLICKDELAY = constants.DBLCLICKDELAY, - startX, - startY, - newMouseDownTime, - dragCover, - initialTarget, - initialOnMouseMove; - - if(!gd._mouseDownTime) gd._mouseDownTime = 0; - - function onStart(e) { - // disable call to options.setCursor(evt) - options.element.onmousemove = initialOnMouseMove; - - // make dragging and dragged into properties of gd - // so that others can look at and modify them - gd._dragged = false; - gd._dragging = true; - startX = e.clientX; - startY = e.clientY; - initialTarget = e.target; - - newMouseDownTime = (new Date()).getTime(); - if(newMouseDownTime - gd._mouseDownTime < DBLCLICKDELAY) { - // in a click train - numClicks += 1; - } - else { - // new click train - numClicks = 1; - gd._mouseDownTime = newMouseDownTime; - } - - if(options.prepFn) options.prepFn(e, startX, startY); - - dragCover = coverSlip(); - - dragCover.onmousemove = onMove; - dragCover.onmouseup = onDone; - dragCover.onmouseout = onDone; - - dragCover.style.cursor = window.getComputedStyle(options.element).cursor; - - return Lib.pauseEvent(e); - } - - function onMove(e) { - var dx = e.clientX - startX, - dy = e.clientY - startY, - minDrag = options.minDrag || constants.MINDRAG; - - if(Math.abs(dx) < minDrag) dx = 0; - if(Math.abs(dy) < minDrag) dy = 0; - if(dx || dy) { - gd._dragged = true; - dragElement.unhover(gd); - } - - if(options.moveFn) options.moveFn(dx, dy, gd._dragged); - - return Lib.pauseEvent(e); - } - - function onDone(e) { - // re-enable call to options.setCursor(evt) - initialOnMouseMove = options.element.onmousemove; - if(options.setCursor) options.element.onmousemove = options.setCursor; - - dragCover.onmousemove = null; - dragCover.onmouseup = null; - dragCover.onmouseout = null; - Lib.removeElement(dragCover); - - if(!gd._dragging) { - gd._dragged = false; - return; - } - gd._dragging = false; - - // don't count as a dblClick unless the mouseUp is also within - // the dblclick delay - if((new Date()).getTime() - gd._mouseDownTime > DBLCLICKDELAY) { - numClicks = Math.max(numClicks - 1, 1); - } - - if(options.doneFn) options.doneFn(gd._dragged, numClicks); - - if(!gd._dragged) { - var e2 = document.createEvent('MouseEvents'); - e2.initEvent('click', true, true); - initialTarget.dispatchEvent(e2); - } - - finishDrag(gd); - - gd._dragged = false; - - return Lib.pauseEvent(e); - } - - // enable call to options.setCursor(evt) - initialOnMouseMove = options.element.onmousemove; - if(options.setCursor) options.element.onmousemove = options.setCursor; - - options.element.onmousedown = onStart; - options.element.style.pointerEvents = 'all'; -}; - -function coverSlip() { - var cover = document.createElement('div'); - - cover.className = 'dragcover'; - var cStyle = cover.style; - cStyle.position = 'fixed'; - cStyle.left = 0; - cStyle.right = 0; - cStyle.top = 0; - cStyle.bottom = 0; - cStyle.zIndex = 999999999; - cStyle.background = 'none'; - - document.body.appendChild(cover); - - return cover; -} - -dragElement.coverSlip = coverSlip; - -function finishDrag(gd) { - gd._dragging = false; - if(gd._replotPending) Plotly.plot(gd); -} diff --git a/src/components/dragelement/unhover.js b/src/components/dragelement/unhover.js index 61b602ac6e6..8b137891791 100644 --- a/src/components/dragelement/unhover.js +++ b/src/components/dragelement/unhover.js @@ -1,49 +1 @@ -/** -* Copyright 2012-2017, 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 Events = require('../../lib/events'); - - -var unhover = module.exports = {}; - - -unhover.wrapped = function(gd, evt, subplot) { - if(typeof gd === 'string') gd = document.getElementById(gd); - - // Important, clear any queued hovers - if(gd._hoverTimer) { - clearTimeout(gd._hoverTimer); - gd._hoverTimer = undefined; - } - - unhover.raw(gd, evt, subplot); -}; - - -// remove hover effects on mouse out, and emit unhover event -unhover.raw = function unhoverRaw(gd, evt) { - var fullLayout = gd._fullLayout; - - if(!evt) evt = {}; - if(evt.target && - Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) { - return; - } - - fullLayout._hoverlayer.selectAll('g').remove(); - - if(evt.target && gd._hoverdata) { - gd.emit('plotly_unhover', {points: gd._hoverdata}); - } - - gd._hoverdata = undefined; -}; diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index fa995b0641e..8b137891791 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -1,680 +1 @@ -/** -* Copyright 2012-2017, 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 d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var Registry = require('../../registry'); -var Color = require('../color'); -var Colorscale = require('../colorscale'); -var Lib = require('../../lib'); -var svgTextUtils = require('../../lib/svg_text_utils'); - -var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); -var subTypes = require('../../traces/scatter/subtypes'); -var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func'); - -var drawing = module.exports = {}; - -// ----------------------------------------------------- -// styling functions for plot elements -// ----------------------------------------------------- - -drawing.font = function(s, family, size, color) { - // also allow the form font(s, {family, size, color}) - if(family && family.family) { - color = family.color; - size = family.size; - family = family.family; - } - if(family) s.style('font-family', family); - if(size + 1) s.style('font-size', size + 'px'); - if(color) s.call(Color.fill, color); -}; - -drawing.setPosition = function(s, x, y) { s.attr('x', x).attr('y', y); }; -drawing.setSize = function(s, w, h) { s.attr('width', w).attr('height', h); }; -drawing.setRect = function(s, x, y, w, h) { - s.call(drawing.setPosition, x, y).call(drawing.setSize, w, h); -}; - -drawing.translatePoint = function(d, sel, xa, ya) { - // put xp and yp into d if pixel scaling is already done - var x = d.xp || xa.c2p(d.x), - y = d.yp || ya.c2p(d.y); - - if(isNumeric(x) && isNumeric(y) && sel.node()) { - // for multiline text this works better - if(sel.node().nodeName === 'text') { - sel.attr('x', x).attr('y', y); - } else { - sel.attr('transform', 'translate(' + x + ',' + y + ')'); - } - } - else sel.remove(); -}; - -drawing.translatePoints = function(s, xa, ya, trace) { - s.each(function(d) { - var sel = d3.select(this); - drawing.translatePoint(d, sel, xa, ya, trace); - }); -}; - -drawing.getPx = function(s, styleAttr) { - // helper to pull out a px value from a style that may contain px units - // s is a d3 selection (will pull from the first one) - return Number(s.style(styleAttr).replace(/px$/, '')); -}; - -drawing.crispRound = function(gd, lineWidth, dflt) { - // for lines that disable antialiasing we want to - // make sure the width is an integer, and at least 1 if it's nonzero - - if(!lineWidth || !isNumeric(lineWidth)) return dflt || 0; - - // but not for static plots - these don't get antialiased anyway. - if(gd._context.staticPlot) return lineWidth; - - if(lineWidth < 1) return 1; - return Math.round(lineWidth); -}; - -drawing.singleLineStyle = function(d, s, lw, lc, ld) { - s.style('fill', 'none'); - var line = (((d || [])[0] || {}).trace || {}).line || {}, - lw1 = lw || line.width||0, - dash = ld || line.dash || ''; - - Color.stroke(s, lc || line.color); - drawing.dashLine(s, dash, lw1); -}; - -drawing.lineGroupStyle = function(s, lw, lc, ld) { - s.style('fill', 'none') - .each(function(d) { - var line = (((d || [])[0] || {}).trace || {}).line || {}, - lw1 = lw || line.width||0, - dash = ld || line.dash || ''; - - d3.select(this) - .call(Color.stroke, lc || line.color) - .call(drawing.dashLine, dash, lw1); - }); -}; - -drawing.dashLine = function(s, dash, lineWidth) { - lineWidth = +lineWidth || 0; - var dlw = Math.max(lineWidth, 3); - - if(dash === 'solid') dash = ''; - else if(dash === 'dot') dash = dlw + 'px,' + dlw + 'px'; - else if(dash === 'dash') dash = (3 * dlw) + 'px,' + (3 * dlw) + 'px'; - else if(dash === 'longdash') dash = (5 * dlw) + 'px,' + (5 * dlw) + 'px'; - else if(dash === 'dashdot') { - dash = (3 * dlw) + 'px,' + dlw + 'px,' + dlw + 'px,' + dlw + 'px'; - } - else if(dash === 'longdashdot') { - dash = (5 * dlw) + 'px,' + (2 * dlw) + 'px,' + dlw + 'px,' + (2 * dlw) + 'px'; - } - // otherwise user wrote the dasharray themselves - leave it be - - s.style({ - 'stroke-dasharray': dash, - 'stroke-width': lineWidth + 'px' - }); -}; - -drawing.fillGroupStyle = function(s) { - s.style('stroke-width', 0) - .each(function(d) { - var shape = d3.select(this); - try { - shape.call(Color.fill, d[0].trace.fillcolor); - } - catch(e) { - Lib.error(e, s); - shape.remove(); - } - }); -}; - -var SYMBOLDEFS = require('./symbol_defs'); - -drawing.symbolNames = []; -drawing.symbolFuncs = []; -drawing.symbolNeedLines = {}; -drawing.symbolNoDot = {}; -drawing.symbolList = []; - -Object.keys(SYMBOLDEFS).forEach(function(k) { - var symDef = SYMBOLDEFS[k]; - drawing.symbolList = drawing.symbolList.concat( - [symDef.n, k, symDef.n + 100, k + '-open']); - drawing.symbolNames[symDef.n] = k; - drawing.symbolFuncs[symDef.n] = symDef.f; - if(symDef.needLine) { - drawing.symbolNeedLines[symDef.n] = true; - } - if(symDef.noDot) { - drawing.symbolNoDot[symDef.n] = true; - } - else { - drawing.symbolList = drawing.symbolList.concat( - [symDef.n + 200, k + '-dot', symDef.n + 300, k + '-open-dot']); - } -}); -var MAXSYMBOL = drawing.symbolNames.length, - // add a dot in the middle of the symbol - DOTPATH = 'M0,0.5L0.5,0L0,-0.5L-0.5,0Z'; - -drawing.symbolNumber = function(v) { - if(typeof v === 'string') { - var vbase = 0; - if(v.indexOf('-open') > 0) { - vbase = 100; - v = v.replace('-open', ''); - } - if(v.indexOf('-dot') > 0) { - vbase += 200; - v = v.replace('-dot', ''); - } - v = drawing.symbolNames.indexOf(v); - if(v >= 0) { v += vbase; } - } - if((v % 100 >= MAXSYMBOL) || v >= 400) { return 0; } - return Math.floor(Math.max(v, 0)); -}; - -function singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerLine) { - // only scatter & box plots get marker path and opacity - // bars, histograms don't - if(Registry.traceIs(trace, 'symbols')) { - var sizeFn = makeBubbleSizeFn(trace); - - sel.attr('d', function(d) { - var r; - - // handle multi-trace graph edit case - if(d.ms === 'various' || marker.size === 'various') r = 3; - else { - r = subTypes.isBubble(trace) ? - sizeFn(d.ms) : (marker.size || 6) / 2; - } - - // store the calculated size so hover can use it - d.mrc = r; - - // turn the symbol into a sanitized number - var x = drawing.symbolNumber(d.mx || marker.symbol) || 0, - xBase = x % 100; - - // save if this marker is open - // because that impacts how to handle colors - d.om = x % 200 >= 100; - - return drawing.symbolFuncs[xBase](r) + - (x >= 200 ? DOTPATH : ''); - }) - .style('opacity', function(d) { - return (d.mo + 1 || marker.opacity + 1) - 1; - }); - } - - // 'so' is suspected outliers, for box plots - var fillColor, - lineColor, - lineWidth; - if(d.so) { - lineWidth = markerLine.outlierwidth; - lineColor = markerLine.outliercolor; - fillColor = marker.outliercolor; - } - else { - lineWidth = (d.mlw + 1 || markerLine.width + 1 || - // TODO: we need the latter for legends... can we get rid of it? - (d.trace ? d.trace.marker.line.width : 0) + 1) - 1; - - if('mlc' in d) lineColor = d.mlcc = lineScale(d.mlc); - // weird case: array wasn't long enough to apply to every point - else if(Array.isArray(markerLine.color)) lineColor = Color.defaultLine; - else lineColor = markerLine.color; - - if('mc' in d) fillColor = d.mcc = markerScale(d.mc); - else if(Array.isArray(marker.color)) fillColor = Color.defaultLine; - else fillColor = marker.color || 'rgba(0,0,0,0)'; - } - - if(d.om) { - // open markers can't have zero linewidth, default to 1px, - // and use fill color as stroke color - sel.call(Color.stroke, fillColor) - .style({ - 'stroke-width': (lineWidth || 1) + 'px', - fill: 'none' - }); - } - else { - sel.style('stroke-width', lineWidth + 'px') - .call(Color.fill, fillColor); - if(lineWidth) { - sel.call(Color.stroke, lineColor); - } - } -} - -drawing.singlePointStyle = function(d, sel, trace) { - var marker = trace.marker, - markerLine = marker.line; - - // allow array marker and marker line colors to be - // scaled by given max and min to colorscales - var markerScale = drawing.tryColorscale(marker, ''), - lineScale = drawing.tryColorscale(marker, 'line'); - - singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerLine); - -}; - -drawing.pointStyle = function(s, trace) { - if(!s.size()) return; - - // allow array marker and marker line colors to be - // scaled by given max and min to colorscales - var marker = trace.marker; - var markerScale = drawing.tryColorscale(marker, ''), - lineScale = drawing.tryColorscale(marker, 'line'); - - s.each(function(d) { - drawing.singlePointStyle(d, d3.select(this), trace, markerScale, lineScale); - }); -}; - -drawing.tryColorscale = function(marker, prefix) { - var cont = prefix ? Lib.nestedProperty(marker, prefix).get() : marker, - scl = cont.colorscale, - colorArray = cont.color; - - if(scl && Array.isArray(colorArray)) { - return Colorscale.makeColorScaleFunc( - Colorscale.extractScale(scl, cont.cmin, cont.cmax) - ); - } - else return Lib.identity; -}; - -// draw text at points -var TEXTOFFSETSIGN = {start: 1, end: -1, middle: 0, bottom: 1, top: -1}, - LINEEXPAND = 1.3; -drawing.textPointStyle = function(s, trace) { - s.each(function(d) { - var p = d3.select(this), - text = d.tx || trace.text; - - if(!text || Array.isArray(text)) { - // isArray test handles the case of (intentionally) missing - // or empty text within a text array - p.remove(); - return; - } - - var pos = d.tp || trace.textposition, - v = pos.indexOf('top') !== -1 ? 'top' : - pos.indexOf('bottom') !== -1 ? 'bottom' : 'middle', - h = pos.indexOf('left') !== -1 ? 'end' : - pos.indexOf('right') !== -1 ? 'start' : 'middle', - fontSize = d.ts || trace.textfont.size, - // if markers are shown, offset a little more than - // the nominal marker size - // ie 2/1.6 * nominal, bcs some markers are a bit bigger - r = d.mrc ? (d.mrc / 0.8 + 1) : 0; - - fontSize = (isNumeric(fontSize) && fontSize > 0) ? fontSize : 0; - - p.call(drawing.font, - d.tf || trace.textfont.family, - fontSize, - d.tc || trace.textfont.color) - .attr('text-anchor', h) - .text(text) - .call(svgTextUtils.convertToTspans); - var pgroup = d3.select(this.parentNode), - tspans = p.selectAll('tspan.line'), - numLines = ((tspans[0].length || 1) - 1) * LINEEXPAND + 1, - dx = TEXTOFFSETSIGN[h] * r, - dy = fontSize * 0.75 + TEXTOFFSETSIGN[v] * r + - (TEXTOFFSETSIGN[v] - 1) * numLines * fontSize / 2; - - // fix the overall text group position - pgroup.attr('transform', 'translate(' + dx + ',' + dy + ')'); - - // then fix multiline text - if(numLines > 1) { - tspans.attr({ x: p.attr('x'), y: p.attr('y') }); - } - }); -}; - -// generalized Catmull-Rom splines, per -// http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf -var CatmullRomExp = 0.5; -drawing.smoothopen = function(pts, smoothness) { - if(pts.length < 3) { return 'M' + pts.join('L');} - var path = 'M' + pts[0], - tangents = [], i; - for(i = 1; i < pts.length - 1; i++) { - tangents.push(makeTangent(pts[i - 1], pts[i], pts[i + 1], smoothness)); - } - path += 'Q' + tangents[0][0] + ' ' + pts[1]; - for(i = 2; i < pts.length - 1; i++) { - path += 'C' + tangents[i - 2][1] + ' ' + tangents[i - 1][0] + ' ' + pts[i]; - } - path += 'Q' + tangents[pts.length - 3][1] + ' ' + pts[pts.length - 1]; - return path; -}; - -drawing.smoothclosed = function(pts, smoothness) { - if(pts.length < 3) { return 'M' + pts.join('L') + 'Z'; } - var path = 'M' + pts[0], - pLast = pts.length - 1, - tangents = [makeTangent(pts[pLast], - pts[0], pts[1], smoothness)], - i; - for(i = 1; i < pLast; i++) { - tangents.push(makeTangent(pts[i - 1], pts[i], pts[i + 1], smoothness)); - } - tangents.push( - makeTangent(pts[pLast - 1], pts[pLast], pts[0], smoothness) - ); - - for(i = 1; i <= pLast; i++) { - path += 'C' + tangents[i - 1][1] + ' ' + tangents[i][0] + ' ' + pts[i]; - } - path += 'C' + tangents[pLast][1] + ' ' + tangents[0][0] + ' ' + pts[0] + 'Z'; - return path; -}; - -function makeTangent(prevpt, thispt, nextpt, smoothness) { - var d1x = prevpt[0] - thispt[0], - d1y = prevpt[1] - thispt[1], - d2x = nextpt[0] - thispt[0], - d2y = nextpt[1] - thispt[1], - d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2), - d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2), - numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness, - numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness, - denom1 = 3 * d2a * (d1a + d2a), - denom2 = 3 * d1a * (d1a + d2a); - return [ - [ - d3.round(thispt[0] + (denom1 && numx / denom1), 2), - d3.round(thispt[1] + (denom1 && numy / denom1), 2) - ], [ - d3.round(thispt[0] - (denom2 && numx / denom2), 2), - d3.round(thispt[1] - (denom2 && numy / denom2), 2) - ] - ]; -} - -// step paths - returns a generator function for paths -// with the given step shape -var STEPPATH = { - hv: function(p0, p1) { - return 'H' + d3.round(p1[0], 2) + 'V' + d3.round(p1[1], 2); - }, - vh: function(p0, p1) { - return 'V' + d3.round(p1[1], 2) + 'H' + d3.round(p1[0], 2); - }, - hvh: function(p0, p1) { - return 'H' + d3.round((p0[0] + p1[0]) / 2, 2) + 'V' + - d3.round(p1[1], 2) + 'H' + d3.round(p1[0], 2); - }, - vhv: function(p0, p1) { - return 'V' + d3.round((p0[1] + p1[1]) / 2, 2) + 'H' + - d3.round(p1[0], 2) + 'V' + d3.round(p1[1], 2); - } -}; -var STEPLINEAR = function(p0, p1) { - return 'L' + d3.round(p1[0], 2) + ',' + d3.round(p1[1], 2); -}; -drawing.steps = function(shape) { - var onestep = STEPPATH[shape] || STEPLINEAR; - return function(pts) { - var path = 'M' + d3.round(pts[0][0], 2) + ',' + d3.round(pts[0][1], 2); - for(var i = 1; i < pts.length; i++) { - path += onestep(pts[i - 1], pts[i]); - } - return path; - }; -}; - -// off-screen svg render testing element, shared by the whole page -// uses the id 'js-plotly-tester' and stores it in gd._tester -// makes a hash of cached text items in tester.node()._cache -// so we can add references to rendered text (including all info -// needed to fully determine its bounding rect) -drawing.makeTester = function(gd) { - var tester = d3.select('body') - .selectAll('#js-plotly-tester') - .data([0]); - - tester.enter().append('svg') - .attr('id', 'js-plotly-tester') - .attr(xmlnsNamespaces.svgAttrs) - .style({ - position: 'absolute', - left: '-10000px', - top: '-10000px', - width: '9000px', - height: '9000px', - 'z-index': '1' - }); - - // browsers differ on how they describe the bounding rect of - // the svg if its contents spill over... so make a 1x1px - // reference point we can measure off of. - var testref = tester.selectAll('.js-reference-point').data([0]); - testref.enter().append('path') - .classed('js-reference-point', true) - .attr('d', 'M0,0H1V1H0Z') - .style({ - 'stroke-width': 0, - fill: 'black' - }); - - if(!tester.node()._cache) { - tester.node()._cache = {}; - } - - gd._tester = tester; - gd._testref = testref; -}; - -// use our offscreen tester to get a clientRect for an element, -// in a reference frame where it isn't translated and its anchor -// point is at (0,0) -// always returns a copy of the bbox, so the caller can modify it safely -var savedBBoxes = [], - maxSavedBBoxes = 10000; -drawing.bBox = function(node) { - // cache elements we've already measured so we don't have to - // remeasure the same thing many times - var saveNum = node.attributes['data-bb']; - if(saveNum && saveNum.value) { - return Lib.extendFlat({}, savedBBoxes[saveNum.value]); - } - - var test3 = d3.select('#js-plotly-tester'), - tester = test3.node(); - - // copy the node to test into the tester - var testNode = node.cloneNode(true); - tester.appendChild(testNode); - // standardize its position... do we really want to do this? - d3.select(testNode).attr({ - x: 0, - y: 0, - transform: '' - }); - - var testRect = testNode.getBoundingClientRect(), - refRect = test3.select('.js-reference-point') - .node().getBoundingClientRect(); - - tester.removeChild(testNode); - - var bb = { - height: testRect.height, - width: testRect.width, - left: testRect.left - refRect.left, - top: testRect.top - refRect.top, - right: testRect.right - refRect.left, - bottom: testRect.bottom - refRect.top - }; - - // make sure we don't have too many saved boxes, - // or a long session could overload on memory - // by saving boxes for long-gone elements - if(savedBBoxes.length >= maxSavedBBoxes) { - d3.selectAll('[data-bb]').attr('data-bb', null); - savedBBoxes = []; - } - - // cache this bbox - node.setAttribute('data-bb', savedBBoxes.length); - savedBBoxes.push(bb); - - return Lib.extendFlat({}, bb); -}; - -/* - * make a robust clipPath url from a local id - * note! We'd better not be exporting from a page - * with a or the svg will not be portable! - */ -drawing.setClipUrl = function(s, localId) { - if(!localId) { - s.attr('clip-path', null); - return; - } - - var url = '#' + localId, - base = d3.select('base'); - - // add id to location href w/o hashes if any) - if(base.size() && base.attr('href')) { - url = window.location.href.split('#')[0] + url; - } - - s.attr('clip-path', 'url(' + url + ')'); -}; - -drawing.getTranslate = function(element) { - // Note the separator [^\d] between x and y in this regex - // We generally use ',' but IE will convert it to ' ' - var re = /.*\btranslate\((-?\d*\.?\d*)[^-\d]*(-?\d*\.?\d*)[^\d].*/, - getter = element.attr ? 'attr' : 'getAttribute', - transform = element[getter]('transform') || ''; - - var translate = transform.replace(re, function(match, p1, p2) { - return [p1, p2].join(' '); - }) - .split(' '); - - return { - x: +translate[0] || 0, - y: +translate[1] || 0 - }; -}; - -drawing.setTranslate = function(element, x, y) { - - var re = /(\btranslate\(.*?\);?)/, - getter = element.attr ? 'attr' : 'getAttribute', - setter = element.attr ? 'attr' : 'setAttribute', - transform = element[getter]('transform') || ''; - - x = x || 0; - y = y || 0; - - transform = transform.replace(re, '').trim(); - transform += ' translate(' + x + ', ' + y + ')'; - transform = transform.trim(); - - element[setter]('transform', transform); - - return transform; -}; - -drawing.getScale = function(element) { - - var re = /.*\bscale\((\d*\.?\d*)[^\d]*(\d*\.?\d*)[^\d].*/, - getter = element.attr ? 'attr' : 'getAttribute', - transform = element[getter]('transform') || ''; - - var translate = transform.replace(re, function(match, p1, p2) { - return [p1, p2].join(' '); - }) - .split(' '); - - return { - x: +translate[0] || 1, - y: +translate[1] || 1 - }; -}; - -drawing.setScale = function(element, x, y) { - - var re = /(\bscale\(.*?\);?)/, - getter = element.attr ? 'attr' : 'getAttribute', - setter = element.attr ? 'attr' : 'setAttribute', - transform = element[getter]('transform') || ''; - - x = x || 1; - y = y || 1; - - transform = transform.replace(re, '').trim(); - transform += ' scale(' + x + ', ' + y + ')'; - transform = transform.trim(); - - element[setter]('transform', transform); - - return transform; -}; - -drawing.setPointGroupScale = function(selection, x, y) { - var t, scale, re; - - x = x || 1; - y = y || 1; - - if(x === 1 && y === 1) { - scale = ''; - } else { - // The same scale transform for every point: - scale = ' scale(' + x + ',' + y + ')'; - } - - // A regex to strip any existing scale: - re = /\s*sc.*/; - - selection.each(function() { - // Get the transform: - t = (this.getAttribute('transform') || '').replace(re, ''); - t += scale; - t = t.trim(); - - // Append the scale transform - this.setAttribute('transform', t); - }); - - return scale; -}; diff --git a/src/components/drawing/symbol_defs.js b/src/components/drawing/symbol_defs.js index 548a8c5c307..46e44801d72 100644 --- a/src/components/drawing/symbol_defs.js +++ b/src/components/drawing/symbol_defs.js @@ -5,11 +5,8 @@ * 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 d3 = require('d3'); +"use strict"; +var d3 = require("d3"); /** Marker symbol definitions * users can specify markers either by number or name @@ -18,457 +15,931 @@ var d3 = require('d3'); * add 200 (or '-dot') and you get a dot in the middle * add both and you get both */ - module.exports = { - circle: { - n: 0, - f: function(r) { - var rs = d3.round(r, 2); - return 'M' + rs + ',0A' + rs + ',' + rs + ' 0 1,1 0,-' + rs + - 'A' + rs + ',' + rs + ' 0 0,1 ' + rs + ',0Z'; - } - }, - square: { - n: 1, - f: function(r) { - var rs = d3.round(r, 2); - return 'M' + rs + ',' + rs + 'H-' + rs + 'V-' + rs + 'H' + rs + 'Z'; - } - }, - diamond: { - n: 2, - f: function(r) { - var rd = d3.round(r * 1.3, 2); - return 'M' + rd + ',0L0,' + rd + 'L-' + rd + ',0L0,-' + rd + 'Z'; - } - }, - cross: { - n: 3, - f: function(r) { - var rc = d3.round(r * 0.4, 2), - rc2 = d3.round(r * 1.2, 2); - return 'M' + rc2 + ',' + rc + 'H' + rc + 'V' + rc2 + 'H-' + rc + - 'V' + rc + 'H-' + rc2 + 'V-' + rc + 'H-' + rc + 'V-' + rc2 + - 'H' + rc + 'V-' + rc + 'H' + rc2 + 'Z'; - } - }, - x: { - n: 4, - f: function(r) { - var rx = d3.round(r * 0.8 / Math.sqrt(2), 2), - ne = 'l' + rx + ',' + rx, - se = 'l' + rx + ',-' + rx, - sw = 'l-' + rx + ',-' + rx, - nw = 'l-' + rx + ',' + rx; - return 'M0,' + rx + ne + se + sw + se + sw + nw + sw + nw + ne + nw + ne + 'Z'; - } - }, - 'triangle-up': { - n: 5, - f: function(r) { - var rt = d3.round(r * 2 / Math.sqrt(3), 2), - r2 = d3.round(r / 2, 2), - rs = d3.round(r, 2); - return 'M-' + rt + ',' + r2 + 'H' + rt + 'L0,-' + rs + 'Z'; - } - }, - 'triangle-down': { - n: 6, - f: function(r) { - var rt = d3.round(r * 2 / Math.sqrt(3), 2), - r2 = d3.round(r / 2, 2), - rs = d3.round(r, 2); - return 'M-' + rt + ',-' + r2 + 'H' + rt + 'L0,' + rs + 'Z'; - } - }, - 'triangle-left': { - n: 7, - f: function(r) { - var rt = d3.round(r * 2 / Math.sqrt(3), 2), - r2 = d3.round(r / 2, 2), - rs = d3.round(r, 2); - return 'M' + r2 + ',-' + rt + 'V' + rt + 'L-' + rs + ',0Z'; - } - }, - 'triangle-right': { - n: 8, - f: function(r) { - var rt = d3.round(r * 2 / Math.sqrt(3), 2), - r2 = d3.round(r / 2, 2), - rs = d3.round(r, 2); - return 'M-' + r2 + ',-' + rt + 'V' + rt + 'L' + rs + ',0Z'; - } - }, - 'triangle-ne': { - n: 9, - f: function(r) { - var r1 = d3.round(r * 0.6, 2), - r2 = d3.round(r * 1.2, 2); - return 'M-' + r2 + ',-' + r1 + 'H' + r1 + 'V' + r2 + 'Z'; - } - }, - 'triangle-se': { - n: 10, - f: function(r) { - var r1 = d3.round(r * 0.6, 2), - r2 = d3.round(r * 1.2, 2); - return 'M' + r1 + ',-' + r2 + 'V' + r1 + 'H-' + r2 + 'Z'; - } - }, - 'triangle-sw': { - n: 11, - f: function(r) { - var r1 = d3.round(r * 0.6, 2), - r2 = d3.round(r * 1.2, 2); - return 'M' + r2 + ',' + r1 + 'H-' + r1 + 'V-' + r2 + 'Z'; - } - }, - 'triangle-nw': { - n: 12, - f: function(r) { - var r1 = d3.round(r * 0.6, 2), - r2 = d3.round(r * 1.2, 2); - return 'M-' + r1 + ',' + r2 + 'V-' + r1 + 'H' + r2 + 'Z'; - } - }, - pentagon: { - n: 13, - f: function(r) { - var x1 = d3.round(r * 0.951, 2), - x2 = d3.round(r * 0.588, 2), - y0 = d3.round(-r, 2), - y1 = d3.round(r * -0.309, 2), - y2 = d3.round(r * 0.809, 2); - return 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2 + 'H-' + x2 + - 'L-' + x1 + ',' + y1 + 'L0,' + y0 + 'Z'; - } - }, - hexagon: { - n: 14, - f: function(r) { - var y0 = d3.round(r, 2), - y1 = d3.round(r / 2, 2), - x = d3.round(r * Math.sqrt(3) / 2, 2); - return 'M' + x + ',-' + y1 + 'V' + y1 + 'L0,' + y0 + - 'L-' + x + ',' + y1 + 'V-' + y1 + 'L0,-' + y0 + 'Z'; - } - }, - hexagon2: { - n: 15, - f: function(r) { - var x0 = d3.round(r, 2), - x1 = d3.round(r / 2, 2), - y = d3.round(r * Math.sqrt(3) / 2, 2); - return 'M-' + x1 + ',' + y + 'H' + x1 + 'L' + x0 + - ',0L' + x1 + ',-' + y + 'H-' + x1 + 'L-' + x0 + ',0Z'; - } - }, - octagon: { - n: 16, - f: function(r) { - var a = d3.round(r * 0.924, 2), - b = d3.round(r * 0.383, 2); - return 'M-' + b + ',-' + a + 'H' + b + 'L' + a + ',-' + b + 'V' + b + - 'L' + b + ',' + a + 'H-' + b + 'L-' + a + ',' + b + 'V-' + b + 'Z'; - } - }, - star: { - n: 17, - f: function(r) { - var rs = r * 1.4, - x1 = d3.round(rs * 0.225, 2), - x2 = d3.round(rs * 0.951, 2), - x3 = d3.round(rs * 0.363, 2), - x4 = d3.round(rs * 0.588, 2), - y0 = d3.round(-rs, 2), - y1 = d3.round(rs * -0.309, 2), - y3 = d3.round(rs * 0.118, 2), - y4 = d3.round(rs * 0.809, 2), - y5 = d3.round(rs * 0.382, 2); - return 'M' + x1 + ',' + y1 + 'H' + x2 + 'L' + x3 + ',' + y3 + - 'L' + x4 + ',' + y4 + 'L0,' + y5 + 'L-' + x4 + ',' + y4 + - 'L-' + x3 + ',' + y3 + 'L-' + x2 + ',' + y1 + 'H-' + x1 + - 'L0,' + y0 + 'Z'; - } - }, - hexagram: { - n: 18, - f: function(r) { - var y = d3.round(r * 0.66, 2), - x1 = d3.round(r * 0.38, 2), - x2 = d3.round(r * 0.76, 2); - return 'M-' + x2 + ',0l-' + x1 + ',-' + y + 'h' + x2 + - 'l' + x1 + ',-' + y + 'l' + x1 + ',' + y + 'h' + x2 + - 'l-' + x1 + ',' + y + 'l' + x1 + ',' + y + 'h-' + x2 + - 'l-' + x1 + ',' + y + 'l-' + x1 + ',-' + y + 'h-' + x2 + 'Z'; - } - }, - 'star-triangle-up': { - n: 19, - f: function(r) { - var x = d3.round(r * Math.sqrt(3) * 0.8, 2), - y1 = d3.round(r * 0.8, 2), - y2 = d3.round(r * 1.6, 2), - rc = d3.round(r * 4, 2), - aPart = 'A ' + rc + ',' + rc + ' 0 0 1 '; - return 'M-' + x + ',' + y1 + aPart + x + ',' + y1 + - aPart + '0,-' + y2 + aPart + '-' + x + ',' + y1 + 'Z'; - } - }, - 'star-triangle-down': { - n: 20, - f: function(r) { - var x = d3.round(r * Math.sqrt(3) * 0.8, 2), - y1 = d3.round(r * 0.8, 2), - y2 = d3.round(r * 1.6, 2), - rc = d3.round(r * 4, 2), - aPart = 'A ' + rc + ',' + rc + ' 0 0 1 '; - return 'M' + x + ',-' + y1 + aPart + '-' + x + ',-' + y1 + - aPart + '0,' + y2 + aPart + x + ',-' + y1 + 'Z'; - } - }, - 'star-square': { - n: 21, - f: function(r) { - var rp = d3.round(r * 1.1, 2), - rc = d3.round(r * 2, 2), - aPart = 'A ' + rc + ',' + rc + ' 0 0 1 '; - return 'M-' + rp + ',-' + rp + aPart + '-' + rp + ',' + rp + - aPart + rp + ',' + rp + aPart + rp + ',-' + rp + - aPart + '-' + rp + ',-' + rp + 'Z'; - } - }, - 'star-diamond': { - n: 22, - f: function(r) { - var rp = d3.round(r * 1.4, 2), - rc = d3.round(r * 1.9, 2), - aPart = 'A ' + rc + ',' + rc + ' 0 0 1 '; - return 'M-' + rp + ',0' + aPart + '0,' + rp + - aPart + rp + ',0' + aPart + '0,-' + rp + - aPart + '-' + rp + ',0' + 'Z'; - } - }, - 'diamond-tall': { - n: 23, - f: function(r) { - var x = d3.round(r * 0.7, 2), - y = d3.round(r * 1.4, 2); - return 'M0,' + y + 'L' + x + ',0L0,-' + y + 'L-' + x + ',0Z'; - } - }, - 'diamond-wide': { - n: 24, - f: function(r) { - var x = d3.round(r * 1.4, 2), - y = d3.round(r * 0.7, 2); - return 'M0,' + y + 'L' + x + ',0L0,-' + y + 'L-' + x + ',0Z'; - } - }, - hourglass: { - n: 25, - f: function(r) { - var rs = d3.round(r, 2); - return 'M' + rs + ',' + rs + 'H-' + rs + 'L' + rs + ',-' + rs + 'H-' + rs + 'Z'; - }, - noDot: true - }, - bowtie: { - n: 26, - f: function(r) { - var rs = d3.round(r, 2); - return 'M' + rs + ',' + rs + 'V-' + rs + 'L-' + rs + ',' + rs + 'V-' + rs + 'Z'; - }, - noDot: true - }, - 'circle-cross': { - n: 27, - f: function(r) { - var rs = d3.round(r, 2); - return 'M0,' + rs + 'V-' + rs + 'M' + rs + ',0H-' + rs + - 'M' + rs + ',0A' + rs + ',' + rs + ' 0 1,1 0,-' + rs + - 'A' + rs + ',' + rs + ' 0 0,1 ' + rs + ',0Z'; - }, - needLine: true, - noDot: true - }, - 'circle-x': { - n: 28, - f: function(r) { - var rs = d3.round(r, 2), - rc = d3.round(r / Math.sqrt(2), 2); - return 'M' + rc + ',' + rc + 'L-' + rc + ',-' + rc + - 'M' + rc + ',-' + rc + 'L-' + rc + ',' + rc + - 'M' + rs + ',0A' + rs + ',' + rs + ' 0 1,1 0,-' + rs + - 'A' + rs + ',' + rs + ' 0 0,1 ' + rs + ',0Z'; - }, - needLine: true, - noDot: true - }, - 'square-cross': { - n: 29, - f: function(r) { - var rs = d3.round(r, 2); - return 'M0,' + rs + 'V-' + rs + 'M' + rs + ',0H-' + rs + - 'M' + rs + ',' + rs + 'H-' + rs + 'V-' + rs + 'H' + rs + 'Z'; - }, - needLine: true, - noDot: true - }, - 'square-x': { - n: 30, - f: function(r) { - var rs = d3.round(r, 2); - return 'M' + rs + ',' + rs + 'L-' + rs + ',-' + rs + - 'M' + rs + ',-' + rs + 'L-' + rs + ',' + rs + - 'M' + rs + ',' + rs + 'H-' + rs + 'V-' + rs + 'H' + rs + 'Z'; - }, - needLine: true, - noDot: true - }, - 'diamond-cross': { - n: 31, - f: function(r) { - var rd = d3.round(r * 1.3, 2); - return 'M' + rd + ',0L0,' + rd + 'L-' + rd + ',0L0,-' + rd + 'Z' + - 'M0,-' + rd + 'V' + rd + 'M-' + rd + ',0H' + rd; - }, - needLine: true, - noDot: true - }, - 'diamond-x': { - n: 32, - f: function(r) { - var rd = d3.round(r * 1.3, 2), - r2 = d3.round(r * 0.65, 2); - return 'M' + rd + ',0L0,' + rd + 'L-' + rd + ',0L0,-' + rd + 'Z' + - 'M-' + r2 + ',-' + r2 + 'L' + r2 + ',' + r2 + - 'M-' + r2 + ',' + r2 + 'L' + r2 + ',-' + r2; - }, - needLine: true, - noDot: true - }, - 'cross-thin': { - n: 33, - f: function(r) { - var rc = d3.round(r * 1.4, 2); - return 'M0,' + rc + 'V-' + rc + 'M' + rc + ',0H-' + rc; - }, - needLine: true, - noDot: true - }, - 'x-thin': { - n: 34, - f: function(r) { - var rx = d3.round(r, 2); - return 'M' + rx + ',' + rx + 'L-' + rx + ',-' + rx + - 'M' + rx + ',-' + rx + 'L-' + rx + ',' + rx; - }, - needLine: true, - noDot: true - }, - asterisk: { - n: 35, - f: function(r) { - var rc = d3.round(r * 1.2, 2); - var rs = d3.round(r * 0.85, 2); - return 'M0,' + rc + 'V-' + rc + 'M' + rc + ',0H-' + rc + - 'M' + rs + ',' + rs + 'L-' + rs + ',-' + rs + - 'M' + rs + ',-' + rs + 'L-' + rs + ',' + rs; - }, - needLine: true, - noDot: true - }, - hash: { - n: 36, - f: function(r) { - var r1 = d3.round(r / 2, 2), - r2 = d3.round(r, 2); - return 'M' + r1 + ',' + r2 + 'V-' + r2 + - 'm-' + r2 + ',0V' + r2 + - 'M' + r2 + ',' + r1 + 'H-' + r2 + - 'm0,-' + r2 + 'H' + r2; - }, - needLine: true - }, - 'y-up': { - n: 37, - f: function(r) { - var x = d3.round(r * 1.2, 2), - y0 = d3.round(r * 1.6, 2), - y1 = d3.round(r * 0.8, 2); - return 'M-' + x + ',' + y1 + 'L0,0M' + x + ',' + y1 + 'L0,0M0,-' + y0 + 'L0,0'; - }, - needLine: true, - noDot: true - }, - 'y-down': { - n: 38, - f: function(r) { - var x = d3.round(r * 1.2, 2), - y0 = d3.round(r * 1.6, 2), - y1 = d3.round(r * 0.8, 2); - return 'M-' + x + ',-' + y1 + 'L0,0M' + x + ',-' + y1 + 'L0,0M0,' + y0 + 'L0,0'; - }, - needLine: true, - noDot: true - }, - 'y-left': { - n: 39, - f: function(r) { - var y = d3.round(r * 1.2, 2), - x0 = d3.round(r * 1.6, 2), - x1 = d3.round(r * 0.8, 2); - return 'M' + x1 + ',' + y + 'L0,0M' + x1 + ',-' + y + 'L0,0M-' + x0 + ',0L0,0'; - }, - needLine: true, - noDot: true - }, - 'y-right': { - n: 40, - f: function(r) { - var y = d3.round(r * 1.2, 2), - x0 = d3.round(r * 1.6, 2), - x1 = d3.round(r * 0.8, 2); - return 'M-' + x1 + ',' + y + 'L0,0M-' + x1 + ',-' + y + 'L0,0M' + x0 + ',0L0,0'; - }, - needLine: true, - noDot: true - }, - 'line-ew': { - n: 41, - f: function(r) { - var rc = d3.round(r * 1.4, 2); - return 'M' + rc + ',0H-' + rc; - }, - needLine: true, - noDot: true - }, - 'line-ns': { - n: 42, - f: function(r) { - var rc = d3.round(r * 1.4, 2); - return 'M0,' + rc + 'V-' + rc; - }, - needLine: true, - noDot: true - }, - 'line-ne': { - n: 43, - f: function(r) { - var rx = d3.round(r, 2); - return 'M' + rx + ',-' + rx + 'L-' + rx + ',' + rx; - }, - needLine: true, - noDot: true - }, - 'line-nw': { - n: 44, - f: function(r) { - var rx = d3.round(r, 2); - return 'M' + rx + ',' + rx + 'L-' + rx + ',-' + rx; - }, - needLine: true, - noDot: true + circle: { + n: 0, + f: function(r) { + var rs = d3.round(r, 2); + return "M" + + rs + + ",0A" + + rs + + "," + + rs + + " 0 1,1 0,-" + + rs + + "A" + + rs + + "," + + rs + + " 0 0,1 " + + rs + + ",0Z"; + } + }, + square: { + n: 1, + f: function(r) { + var rs = d3.round(r, 2); + return "M" + rs + "," + rs + "H-" + rs + "V-" + rs + "H" + rs + "Z"; + } + }, + diamond: { + n: 2, + f: function(r) { + var rd = d3.round(r * 1.3, 2); + return "M" + rd + ",0L0," + rd + "L-" + rd + ",0L0,-" + rd + "Z"; + } + }, + cross: { + n: 3, + f: function(r) { + var rc = d3.round(r * 0.4, 2), rc2 = d3.round(r * 1.2, 2); + return "M" + + rc2 + + "," + + rc + + "H" + + rc + + "V" + + rc2 + + "H-" + + rc + + "V" + + rc + + "H-" + + rc2 + + "V-" + + rc + + "H-" + + rc + + "V-" + + rc2 + + "H" + + rc + + "V-" + + rc + + "H" + + rc2 + + "Z"; + } + }, + x: { + n: 4, + f: function(r) { + var rx = d3.round(r * 0.8 / Math.sqrt(2), 2), + ne = "l" + rx + "," + rx, + se = "l" + rx + ",-" + rx, + sw = "l-" + rx + ",-" + rx, + nw = "l-" + rx + "," + rx; + return "M0," + + rx + + ne + + se + + sw + + se + + sw + + nw + + sw + + nw + + ne + + nw + + ne + + "Z"; + } + }, + "triangle-up": { + n: 5, + f: function(r) { + var rt = d3.round(r * 2 / Math.sqrt(3), 2), + r2 = d3.round(r / 2, 2), + rs = d3.round(r, 2); + return "M-" + rt + "," + r2 + "H" + rt + "L0,-" + rs + "Z"; + } + }, + "triangle-down": { + n: 6, + f: function(r) { + var rt = d3.round(r * 2 / Math.sqrt(3), 2), + r2 = d3.round(r / 2, 2), + rs = d3.round(r, 2); + return "M-" + rt + ",-" + r2 + "H" + rt + "L0," + rs + "Z"; + } + }, + "triangle-left": { + n: 7, + f: function(r) { + var rt = d3.round(r * 2 / Math.sqrt(3), 2), + r2 = d3.round(r / 2, 2), + rs = d3.round(r, 2); + return "M" + r2 + ",-" + rt + "V" + rt + "L-" + rs + ",0Z"; + } + }, + "triangle-right": { + n: 8, + f: function(r) { + var rt = d3.round(r * 2 / Math.sqrt(3), 2), + r2 = d3.round(r / 2, 2), + rs = d3.round(r, 2); + return "M-" + r2 + ",-" + rt + "V" + rt + "L" + rs + ",0Z"; + } + }, + "triangle-ne": { + n: 9, + f: function(r) { + var r1 = d3.round(r * 0.6, 2), r2 = d3.round(r * 1.2, 2); + return "M-" + r2 + ",-" + r1 + "H" + r1 + "V" + r2 + "Z"; + } + }, + "triangle-se": { + n: 10, + f: function(r) { + var r1 = d3.round(r * 0.6, 2), r2 = d3.round(r * 1.2, 2); + return "M" + r1 + ",-" + r2 + "V" + r1 + "H-" + r2 + "Z"; + } + }, + "triangle-sw": { + n: 11, + f: function(r) { + var r1 = d3.round(r * 0.6, 2), r2 = d3.round(r * 1.2, 2); + return "M" + r2 + "," + r1 + "H-" + r1 + "V-" + r2 + "Z"; + } + }, + "triangle-nw": { + n: 12, + f: function(r) { + var r1 = d3.round(r * 0.6, 2), r2 = d3.round(r * 1.2, 2); + return "M-" + r1 + "," + r2 + "V-" + r1 + "H" + r2 + "Z"; + } + }, + pentagon: { + n: 13, + f: function(r) { + var x1 = d3.round(r * 0.951, 2), + x2 = d3.round(r * 0.588, 2), + y0 = d3.round(-r, 2), + y1 = d3.round(r * (-0.309), 2), + y2 = d3.round(r * 0.809, 2); + return "M" + + x1 + + "," + + y1 + + "L" + + x2 + + "," + + y2 + + "H-" + + x2 + + "L-" + + x1 + + "," + + y1 + + "L0," + + y0 + + "Z"; + } + }, + hexagon: { + n: 14, + f: function(r) { + var y0 = d3.round(r, 2), + y1 = d3.round(r / 2, 2), + x = d3.round(r * Math.sqrt(3) / 2, 2); + return "M" + + x + + ",-" + + y1 + + "V" + + y1 + + "L0," + + y0 + + "L-" + + x + + "," + + y1 + + "V-" + + y1 + + "L0,-" + + y0 + + "Z"; + } + }, + hexagon2: { + n: 15, + f: function(r) { + var x0 = d3.round(r, 2), + x1 = d3.round(r / 2, 2), + y = d3.round(r * Math.sqrt(3) / 2, 2); + return "M-" + + x1 + + "," + + y + + "H" + + x1 + + "L" + + x0 + + ",0L" + + x1 + + ",-" + + y + + "H-" + + x1 + + "L-" + + x0 + + ",0Z"; + } + }, + octagon: { + n: 16, + f: function(r) { + var a = d3.round(r * 0.924, 2), b = d3.round(r * 0.383, 2); + return "M-" + + b + + ",-" + + a + + "H" + + b + + "L" + + a + + ",-" + + b + + "V" + + b + + "L" + + b + + "," + + a + + "H-" + + b + + "L-" + + a + + "," + + b + + "V-" + + b + + "Z"; + } + }, + star: { + n: 17, + f: function(r) { + var rs = r * 1.4, + x1 = d3.round(rs * 0.225, 2), + x2 = d3.round(rs * 0.951, 2), + x3 = d3.round(rs * 0.363, 2), + x4 = d3.round(rs * 0.588, 2), + y0 = d3.round(-rs, 2), + y1 = d3.round(rs * (-0.309), 2), + y3 = d3.round(rs * 0.118, 2), + y4 = d3.round(rs * 0.809, 2), + y5 = d3.round(rs * 0.382, 2); + return "M" + + x1 + + "," + + y1 + + "H" + + x2 + + "L" + + x3 + + "," + + y3 + + "L" + + x4 + + "," + + y4 + + "L0," + + y5 + + "L-" + + x4 + + "," + + y4 + + "L-" + + x3 + + "," + + y3 + + "L-" + + x2 + + "," + + y1 + + "H-" + + x1 + + "L0," + + y0 + + "Z"; + } + }, + hexagram: { + n: 18, + f: function(r) { + var y = d3.round(r * 0.66, 2), + x1 = d3.round(r * 0.38, 2), + x2 = d3.round(r * 0.76, 2); + return "M-" + + x2 + + ",0l-" + + x1 + + ",-" + + y + + "h" + + x2 + + "l" + + x1 + + ",-" + + y + + "l" + + x1 + + "," + + y + + "h" + + x2 + + "l-" + + x1 + + "," + + y + + "l" + + x1 + + "," + + y + + "h-" + + x2 + + "l-" + + x1 + + "," + + y + + "l-" + + x1 + + ",-" + + y + + "h-" + + x2 + + "Z"; + } + }, + "star-triangle-up": { + n: 19, + f: function(r) { + var x = d3.round(r * Math.sqrt(3) * 0.8, 2), + y1 = d3.round(r * 0.8, 2), + y2 = d3.round(r * 1.6, 2), + rc = d3.round(r * 4, 2), + aPart = "A " + rc + "," + rc + " 0 0 1 "; + return "M-" + + x + + "," + + y1 + + aPart + + x + + "," + + y1 + + aPart + + "0,-" + + y2 + + aPart + + "-" + + x + + "," + + y1 + + "Z"; + } + }, + "star-triangle-down": { + n: 20, + f: function(r) { + var x = d3.round(r * Math.sqrt(3) * 0.8, 2), + y1 = d3.round(r * 0.8, 2), + y2 = d3.round(r * 1.6, 2), + rc = d3.round(r * 4, 2), + aPart = "A " + rc + "," + rc + " 0 0 1 "; + return "M" + + x + + ",-" + + y1 + + aPart + + "-" + + x + + ",-" + + y1 + + aPart + + "0," + + y2 + + aPart + + x + + ",-" + + y1 + + "Z"; + } + }, + "star-square": { + n: 21, + f: function(r) { + var rp = d3.round(r * 1.1, 2), + rc = d3.round(r * 2, 2), + aPart = "A " + rc + "," + rc + " 0 0 1 "; + return "M-" + + rp + + ",-" + + rp + + aPart + + "-" + + rp + + "," + + rp + + aPart + + rp + + "," + + rp + + aPart + + rp + + ",-" + + rp + + aPart + + "-" + + rp + + ",-" + + rp + + "Z"; + } + }, + "star-diamond": { + n: 22, + f: function(r) { + var rp = d3.round(r * 1.4, 2), + rc = d3.round(r * 1.9, 2), + aPart = "A " + rc + "," + rc + " 0 0 1 "; + return "M-" + + rp + + ",0" + + aPart + + "0," + + rp + + aPart + + rp + + ",0" + + aPart + + "0,-" + + rp + + aPart + + "-" + + rp + + ",0" + + "Z"; + } + }, + "diamond-tall": { + n: 23, + f: function(r) { + var x = d3.round(r * 0.7, 2), y = d3.round(r * 1.4, 2); + return "M0," + y + "L" + x + ",0L0,-" + y + "L-" + x + ",0Z"; + } + }, + "diamond-wide": { + n: 24, + f: function(r) { + var x = d3.round(r * 1.4, 2), y = d3.round(r * 0.7, 2); + return "M0," + y + "L" + x + ",0L0,-" + y + "L-" + x + ",0Z"; } + }, + hourglass: { + n: 25, + f: function(r) { + var rs = d3.round(r, 2); + return "M" + + rs + + "," + + rs + + "H-" + + rs + + "L" + + rs + + ",-" + + rs + + "H-" + + rs + + "Z"; + }, + noDot: true + }, + bowtie: { + n: 26, + f: function(r) { + var rs = d3.round(r, 2); + return "M" + + rs + + "," + + rs + + "V-" + + rs + + "L-" + + rs + + "," + + rs + + "V-" + + rs + + "Z"; + }, + noDot: true + }, + "circle-cross": { + n: 27, + f: function(r) { + var rs = d3.round(r, 2); + return "M0," + + rs + + "V-" + + rs + + "M" + + rs + + ",0H-" + + rs + + "M" + + rs + + ",0A" + + rs + + "," + + rs + + " 0 1,1 0,-" + + rs + + "A" + + rs + + "," + + rs + + " 0 0,1 " + + rs + + ",0Z"; + }, + needLine: true, + noDot: true + }, + "circle-x": { + n: 28, + f: function(r) { + var rs = d3.round(r, 2), rc = d3.round(r / Math.sqrt(2), 2); + return "M" + + rc + + "," + + rc + + "L-" + + rc + + ",-" + + rc + + "M" + + rc + + ",-" + + rc + + "L-" + + rc + + "," + + rc + + "M" + + rs + + ",0A" + + rs + + "," + + rs + + " 0 1,1 0,-" + + rs + + "A" + + rs + + "," + + rs + + " 0 0,1 " + + rs + + ",0Z"; + }, + needLine: true, + noDot: true + }, + "square-cross": { + n: 29, + f: function(r) { + var rs = d3.round(r, 2); + return "M0," + + rs + + "V-" + + rs + + "M" + + rs + + ",0H-" + + rs + + "M" + + rs + + "," + + rs + + "H-" + + rs + + "V-" + + rs + + "H" + + rs + + "Z"; + }, + needLine: true, + noDot: true + }, + "square-x": { + n: 30, + f: function(r) { + var rs = d3.round(r, 2); + return "M" + + rs + + "," + + rs + + "L-" + + rs + + ",-" + + rs + + "M" + + rs + + ",-" + + rs + + "L-" + + rs + + "," + + rs + + "M" + + rs + + "," + + rs + + "H-" + + rs + + "V-" + + rs + + "H" + + rs + + "Z"; + }, + needLine: true, + noDot: true + }, + "diamond-cross": { + n: 31, + f: function(r) { + var rd = d3.round(r * 1.3, 2); + return "M" + + rd + + ",0L0," + + rd + + "L-" + + rd + + ",0L0,-" + + rd + + "Z" + + "M0,-" + + rd + + "V" + + rd + + "M-" + + rd + + ",0H" + + rd; + }, + needLine: true, + noDot: true + }, + "diamond-x": { + n: 32, + f: function(r) { + var rd = d3.round(r * 1.3, 2), r2 = d3.round(r * 0.65, 2); + return "M" + + rd + + ",0L0," + + rd + + "L-" + + rd + + ",0L0,-" + + rd + + "Z" + + "M-" + + r2 + + ",-" + + r2 + + "L" + + r2 + + "," + + r2 + + "M-" + + r2 + + "," + + r2 + + "L" + + r2 + + ",-" + + r2; + }, + needLine: true, + noDot: true + }, + "cross-thin": { + n: 33, + f: function(r) { + var rc = d3.round(r * 1.4, 2); + return "M0," + rc + "V-" + rc + "M" + rc + ",0H-" + rc; + }, + needLine: true, + noDot: true + }, + "x-thin": { + n: 34, + f: function(r) { + var rx = d3.round(r, 2); + return "M" + + rx + + "," + + rx + + "L-" + + rx + + ",-" + + rx + + "M" + + rx + + ",-" + + rx + + "L-" + + rx + + "," + + rx; + }, + needLine: true, + noDot: true + }, + asterisk: { + n: 35, + f: function(r) { + var rc = d3.round(r * 1.2, 2); + var rs = d3.round(r * 0.85, 2); + return "M0," + + rc + + "V-" + + rc + + "M" + + rc + + ",0H-" + + rc + + "M" + + rs + + "," + + rs + + "L-" + + rs + + ",-" + + rs + + "M" + + rs + + ",-" + + rs + + "L-" + + rs + + "," + + rs; + }, + needLine: true, + noDot: true + }, + hash: { + n: 36, + f: function(r) { + var r1 = d3.round(r / 2, 2), r2 = d3.round(r, 2); + return "M" + + r1 + + "," + + r2 + + "V-" + + r2 + + "m-" + + r2 + + ",0V" + + r2 + + "M" + + r2 + + "," + + r1 + + "H-" + + r2 + + "m0,-" + + r2 + + "H" + + r2; + }, + needLine: true + }, + "y-up": { + n: 37, + f: function(r) { + var x = d3.round(r * 1.2, 2), + y0 = d3.round(r * 1.6, 2), + y1 = d3.round(r * 0.8, 2); + return "M-" + + x + + "," + + y1 + + "L0,0M" + + x + + "," + + y1 + + "L0,0M0,-" + + y0 + + "L0,0"; + }, + needLine: true, + noDot: true + }, + "y-down": { + n: 38, + f: function(r) { + var x = d3.round(r * 1.2, 2), + y0 = d3.round(r * 1.6, 2), + y1 = d3.round(r * 0.8, 2); + return "M-" + + x + + ",-" + + y1 + + "L0,0M" + + x + + ",-" + + y1 + + "L0,0M0," + + y0 + + "L0,0"; + }, + needLine: true, + noDot: true + }, + "y-left": { + n: 39, + f: function(r) { + var y = d3.round(r * 1.2, 2), + x0 = d3.round(r * 1.6, 2), + x1 = d3.round(r * 0.8, 2); + return "M" + + x1 + + "," + + y + + "L0,0M" + + x1 + + ",-" + + y + + "L0,0M-" + + x0 + + ",0L0,0"; + }, + needLine: true, + noDot: true + }, + "y-right": { + n: 40, + f: function(r) { + var y = d3.round(r * 1.2, 2), + x0 = d3.round(r * 1.6, 2), + x1 = d3.round(r * 0.8, 2); + return "M-" + + x1 + + "," + + y + + "L0,0M-" + + x1 + + ",-" + + y + + "L0,0M" + + x0 + + ",0L0,0"; + }, + needLine: true, + noDot: true + }, + "line-ew": { + n: 41, + f: function(r) { + var rc = d3.round(r * 1.4, 2); + return "M" + rc + ",0H-" + rc; + }, + needLine: true, + noDot: true + }, + "line-ns": { + n: 42, + f: function(r) { + var rc = d3.round(r * 1.4, 2); + return "M0," + rc + "V-" + rc; + }, + needLine: true, + noDot: true + }, + "line-ne": { + n: 43, + f: function(r) { + var rx = d3.round(r, 2); + return "M" + rx + ",-" + rx + "L-" + rx + "," + rx; + }, + needLine: true, + noDot: true + }, + "line-nw": { + n: 44, + f: function(r) { + var rx = d3.round(r, 2); + return "M" + rx + "," + rx + "L-" + rx + ",-" + rx; + }, + needLine: true, + noDot: true + } }; diff --git a/src/components/errorbars/attributes.js b/src/components/errorbars/attributes.js index be441d7b364..58a5e6896d8 100644 --- a/src/components/errorbars/attributes.js +++ b/src/components/errorbars/attributes.js @@ -5,136 +5,112 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - visible: { - valType: 'boolean', - role: 'info', - description: [ - 'Determines whether or not this set of error bars is visible.' - ].join(' ') - }, - type: { - valType: 'enumerated', - values: ['percent', 'constant', 'sqrt', 'data'], - role: 'info', - description: [ - 'Determines the rule used to generate the error bars.', - - 'If *constant`, the bar lengths are of a constant value.', - 'Set this constant in `value`.', - - 'If *percent*, the bar lengths correspond to a percentage of', - 'underlying data. Set this percentage in `value`.', - - 'If *sqrt*, the bar lengths correspond to the sqaure of the', - 'underlying data.', - - 'If *array*, the bar lengths are set with data set `array`.' - ].join(' ') - }, - symmetric: { - valType: 'boolean', - role: 'info', - description: [ - 'Determines whether or not the error bars have the same length', - 'in both direction', - '(top/bottom for vertical bars, left/right for horizontal bars.' - ].join(' ') - }, - array: { - valType: 'data_array', - description: [ - 'Sets the data corresponding the length of each error bar.', - 'Values are plotted relative to the underlying data.' - ].join(' ') - }, - arrayminus: { - valType: 'data_array', - description: [ - 'Sets the data corresponding the length of each error bar in the', - 'bottom (left) direction for vertical (horizontal) bars', - 'Values are plotted relative to the underlying data.' - ].join(' ') - }, - value: { - valType: 'number', - min: 0, - dflt: 10, - role: 'info', - description: [ - 'Sets the value of either the percentage', - '(if `type` is set to *percent*) or the constant', - '(if `type` is set to *constant*) corresponding to the lengths of', - 'the error bars.' - ].join(' ') - }, - valueminus: { - valType: 'number', - min: 0, - dflt: 10, - role: 'info', - description: [ - 'Sets the value of either the percentage', - '(if `type` is set to *percent*) or the constant', - '(if `type` is set to *constant*) corresponding to the lengths of', - 'the error bars in the', - 'bottom (left) direction for vertical (horizontal) bars' - ].join(' ') - }, - traceref: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'info' - }, - tracerefminus: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'info' - }, - copy_ystyle: { - valType: 'boolean', - role: 'style' - }, - copy_zstyle: { - valType: 'boolean', - role: 'style' - }, - color: { - valType: 'color', - role: 'style', - description: 'Sets the stoke color of the error bars.' - }, - thickness: { - valType: 'number', - min: 0, - dflt: 2, - role: 'style', - description: 'Sets the thickness (in px) of the error bars.' - }, - width: { - valType: 'number', - min: 0, - role: 'style', - description: [ - 'Sets the width (in px) of the cross-bar at both ends', - 'of the error bars.' - ].join(' ') - }, - - _deprecated: { - opacity: { - valType: 'number', - role: 'style', - description: [ - 'Obsolete.', - 'Use the alpha channel in error bar `color` to set the opacity.' - ].join(' ') - } + visible: { + valType: "boolean", + role: "info", + description: [ + "Determines whether or not this set of error bars is visible." + ].join(" ") + }, + type: { + valType: "enumerated", + values: ["percent", "constant", "sqrt", "data"], + role: "info", + description: [ + "Determines the rule used to generate the error bars.", + "If *constant`, the bar lengths are of a constant value.", + "Set this constant in `value`.", + "If *percent*, the bar lengths correspond to a percentage of", + "underlying data. Set this percentage in `value`.", + "If *sqrt*, the bar lengths correspond to the sqaure of the", + "underlying data.", + "If *array*, the bar lengths are set with data set `array`." + ].join(" ") + }, + symmetric: { + valType: "boolean", + role: "info", + description: [ + "Determines whether or not the error bars have the same length", + "in both direction", + "(top/bottom for vertical bars, left/right for horizontal bars." + ].join(" ") + }, + array: { + valType: "data_array", + description: [ + "Sets the data corresponding the length of each error bar.", + "Values are plotted relative to the underlying data." + ].join(" ") + }, + arrayminus: { + valType: "data_array", + description: [ + "Sets the data corresponding the length of each error bar in the", + "bottom (left) direction for vertical (horizontal) bars", + "Values are plotted relative to the underlying data." + ].join(" ") + }, + value: { + valType: "number", + min: 0, + dflt: 10, + role: "info", + description: [ + "Sets the value of either the percentage", + "(if `type` is set to *percent*) or the constant", + "(if `type` is set to *constant*) corresponding to the lengths of", + "the error bars." + ].join(" ") + }, + valueminus: { + valType: "number", + min: 0, + dflt: 10, + role: "info", + description: [ + "Sets the value of either the percentage", + "(if `type` is set to *percent*) or the constant", + "(if `type` is set to *constant*) corresponding to the lengths of", + "the error bars in the", + "bottom (left) direction for vertical (horizontal) bars" + ].join(" ") + }, + traceref: { valType: "integer", min: 0, dflt: 0, role: "info" }, + tracerefminus: { valType: "integer", min: 0, dflt: 0, role: "info" }, + copy_ystyle: { valType: "boolean", role: "style" }, + copy_zstyle: { valType: "boolean", role: "style" }, + color: { + valType: "color", + role: "style", + description: "Sets the stoke color of the error bars." + }, + thickness: { + valType: "number", + min: 0, + dflt: 2, + role: "style", + description: "Sets the thickness (in px) of the error bars." + }, + width: { + valType: "number", + min: 0, + role: "style", + description: [ + "Sets the width (in px) of the cross-bar at both ends", + "of the error bars." + ].join(" ") + }, + _deprecated: { + opacity: { + valType: "number", + role: "style", + description: [ + "Obsolete.", + "Use the alpha channel in error bar `color` to set the opacity." + ].join(" ") } + } }; diff --git a/src/components/errorbars/calc.js b/src/components/errorbars/calc.js index d631758fcb6..cb1295aeea7 100644 --- a/src/components/errorbars/calc.js +++ b/src/components/errorbars/calc.js @@ -5,57 +5,51 @@ * 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 isNumeric = require("fast-isnumeric"); +var Registry = require("../../registry"); +var Axes = require("../../plots/cartesian/axes"); -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Registry = require('../../registry'); -var Axes = require('../../plots/cartesian/axes'); - -var makeComputeError = require('./compute_error'); - +var makeComputeError = require("./compute_error"); module.exports = function calc(gd) { - var calcdata = gd.calcdata; + var calcdata = gd.calcdata; - for(var i = 0; i < calcdata.length; i++) { - var calcTrace = calcdata[i], - trace = calcTrace[0].trace; + for (var i = 0; i < calcdata.length; i++) { + var calcTrace = calcdata[i], trace = calcTrace[0].trace; - if(!Registry.traceIs(trace, 'errorBarsOK')) continue; + if (!Registry.traceIs(trace, "errorBarsOK")) continue; - var xa = Axes.getFromId(gd, trace.xaxis), - ya = Axes.getFromId(gd, trace.yaxis); + var xa = Axes.getFromId(gd, trace.xaxis), + ya = Axes.getFromId(gd, trace.yaxis); - calcOneAxis(calcTrace, trace, xa, 'x'); - calcOneAxis(calcTrace, trace, ya, 'y'); - } + calcOneAxis(calcTrace, trace, xa, "x"); + calcOneAxis(calcTrace, trace, ya, "y"); + } }; function calcOneAxis(calcTrace, trace, axis, coord) { - var opts = trace['error_' + coord] || {}, - isVisible = (opts.visible && ['linear', 'log'].indexOf(axis.type) !== -1), - vals = []; + var opts = trace["error_" + coord] || {}, + isVisible = opts.visible && ["linear", "log"].indexOf(axis.type) !== -1, + vals = []; - if(!isVisible) return; + if (!isVisible) return; - var computeError = makeComputeError(opts); + var computeError = makeComputeError(opts); - for(var i = 0; i < calcTrace.length; i++) { - var calcPt = calcTrace[i], - calcCoord = calcPt[coord]; + for (var i = 0; i < calcTrace.length; i++) { + var calcPt = calcTrace[i], calcCoord = calcPt[coord]; - if(!isNumeric(axis.c2l(calcCoord))) continue; + if (!isNumeric(axis.c2l(calcCoord))) continue; - var errors = computeError(calcCoord, i); - if(isNumeric(errors[0]) && isNumeric(errors[1])) { - var shoe = calcPt[coord + 's'] = calcCoord - errors[0], - hat = calcPt[coord + 'h'] = calcCoord + errors[1]; - vals.push(shoe, hat); - } + var errors = computeError(calcCoord, i); + if (isNumeric(errors[0]) && isNumeric(errors[1])) { + var shoe = calcPt[coord + "s"] = calcCoord - errors[0], + hat = calcPt[coord + "h"] = calcCoord + errors[1]; + vals.push(shoe, hat); } + } - Axes.expand(axis, vals, {padded: true}); + Axes.expand(axis, vals, { padded: true }); } diff --git a/src/components/errorbars/compute_error.js b/src/components/errorbars/compute_error.js index dd0b189662d..74cd965e790 100644 --- a/src/components/errorbars/compute_error.js +++ b/src/components/errorbars/compute_error.js @@ -5,11 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; /** * Error bar computing function generator * @@ -26,44 +22,36 @@ * - error[1] : " " " " positive " */ module.exports = function makeComputeError(opts) { - var type = opts.type, - symmetric = opts.symmetric; + var type = opts.type, symmetric = opts.symmetric; - if(type === 'data') { - var array = opts.array, - arrayminus = opts.arrayminus; + if (type === "data") { + var array = opts.array, arrayminus = opts.arrayminus; - if(symmetric || arrayminus === undefined) { - return function computeError(dataPt, index) { - var val = +(array[index]); - return [val, val]; - }; - } - else { - return function computeError(dataPt, index) { - return [+arrayminus[index], +array[index]]; - }; - } + if (symmetric || arrayminus === undefined) { + return function computeError(dataPt, index) { + var val = +array[index]; + return [val, val]; + }; + } else { + return function computeError(dataPt, index) { + return [+arrayminus[index], +array[index]]; + }; } - else { - var computeErrorValue = makeComputeErrorValue(type, opts.value), - computeErrorValueMinus = makeComputeErrorValue(type, opts.valueminus); + } else { + var computeErrorValue = makeComputeErrorValue(type, opts.value), + computeErrorValueMinus = makeComputeErrorValue(type, opts.valueminus); - if(symmetric || opts.valueminus === undefined) { - return function computeError(dataPt) { - var val = computeErrorValue(dataPt); - return [val, val]; - }; - } - else { - return function computeError(dataPt) { - return [ - computeErrorValueMinus(dataPt), - computeErrorValue(dataPt) - ]; - }; - } + if (symmetric || opts.valueminus === undefined) { + return function computeError(dataPt) { + var val = computeErrorValue(dataPt); + return [val, val]; + }; + } else { + return function computeError(dataPt) { + return [computeErrorValueMinus(dataPt), computeErrorValue(dataPt)]; + }; } + } }; /** @@ -76,19 +64,19 @@ module.exports = function makeComputeError(opts) { * @param {numeric} dataPt */ function makeComputeErrorValue(type, value) { - if(type === 'percent') { - return function(dataPt) { - return Math.abs(dataPt * value / 100); - }; - } - if(type === 'constant') { - return function() { - return Math.abs(value); - }; - } - if(type === 'sqrt') { - return function(dataPt) { - return Math.sqrt(Math.abs(dataPt)); - }; - } + if (type === "percent") { + return function(dataPt) { + return Math.abs(dataPt * value / 100); + }; + } + if (type === "constant") { + return function() { + return Math.abs(value); + }; + } + if (type === "sqrt") { + return function(dataPt) { + return Math.sqrt(Math.abs(dataPt)); + }; + } } diff --git a/src/components/errorbars/defaults.js b/src/components/errorbars/defaults.js index 433f585352b..4957cf43278 100644 --- a/src/components/errorbars/defaults.js +++ b/src/components/errorbars/defaults.js @@ -5,71 +5,70 @@ * 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 isNumeric = require("fast-isnumeric"); -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Registry = require('../../registry'); -var Lib = require('../../lib'); - -var attributes = require('./attributes'); +var Registry = require("../../registry"); +var Lib = require("../../lib"); +var attributes = require("./attributes"); module.exports = function(traceIn, traceOut, defaultColor, opts) { - var objName = 'error_' + opts.axis, - containerOut = traceOut[objName] = {}, - containerIn = traceIn[objName] || {}; + var objName = "error_" + opts.axis, + containerOut = traceOut[objName] = {}, + containerIn = traceIn[objName] || {}; - function coerce(attr, dflt) { - return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); - } + function coerce(attr, dflt) { + return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); + } - var hasErrorBars = ( - containerIn.array !== undefined || - containerIn.value !== undefined || - containerIn.type === 'sqrt' - ); + var hasErrorBars = containerIn.array !== undefined || + containerIn.value !== undefined || + containerIn.type === "sqrt"; - var visible = coerce('visible', hasErrorBars); + var visible = coerce("visible", hasErrorBars); - if(visible === false) return; + if (visible === false) return; - var type = coerce('type', 'array' in containerIn ? 'data' : 'percent'), - symmetric = true; + var type = coerce("type", "array" in containerIn ? "data" : "percent"), + symmetric = true; - if(type !== 'sqrt') { - symmetric = coerce('symmetric', - !((type === 'data' ? 'arrayminus' : 'valueminus') in containerIn)); - } + if (type !== "sqrt") { + symmetric = coerce( + "symmetric", + !((type === "data" ? "arrayminus" : "valueminus") in containerIn) + ); + } - if(type === 'data') { - var array = coerce('array'); - if(!array) containerOut.array = []; - coerce('traceref'); - if(!symmetric) { - var arrayminus = coerce('arrayminus'); - if(!arrayminus) containerOut.arrayminus = []; - coerce('tracerefminus'); - } - } - else if(type === 'percent' || type === 'constant') { - coerce('value'); - if(!symmetric) coerce('valueminus'); + if (type === "data") { + var array = coerce("array"); + if (!array) containerOut.array = []; + coerce("traceref"); + if (!symmetric) { + var arrayminus = coerce("arrayminus"); + if (!arrayminus) containerOut.arrayminus = []; + coerce("tracerefminus"); } + } else if (type === "percent" || type === "constant") { + coerce("value"); + if (!symmetric) coerce("valueminus"); + } - var copyAttr = 'copy_' + opts.inherit + 'style'; - if(opts.inherit) { - var inheritObj = traceOut['error_' + opts.inherit]; - if((inheritObj || {}).visible) { - coerce(copyAttr, !(containerIn.color || - isNumeric(containerIn.thickness) || - isNumeric(containerIn.width))); - } - } - if(!opts.inherit || !containerOut[copyAttr]) { - coerce('color', defaultColor); - coerce('thickness'); - coerce('width', Registry.traceIs(traceOut, 'gl3d') ? 0 : 4); + var copyAttr = "copy_" + opts.inherit + "style"; + if (opts.inherit) { + var inheritObj = traceOut["error_" + opts.inherit]; + if ((inheritObj || {}).visible) { + coerce( + copyAttr, + !(containerIn.color || + isNumeric(containerIn.thickness) || + isNumeric(containerIn.width)) + ); } + } + if (!opts.inherit || !containerOut[copyAttr]) { + coerce("color", defaultColor); + coerce("thickness"); + coerce("width", Registry.traceIs(traceOut, "gl3d") ? 0 : 4); + } }; diff --git a/src/components/errorbars/index.js b/src/components/errorbars/index.js index a27378ad6e7..4ab3e45d7d6 100644 --- a/src/components/errorbars/index.js +++ b/src/components/errorbars/index.js @@ -5,53 +5,46 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var errorBars = module.exports = {}; -errorBars.attributes = require('./attributes'); +errorBars.attributes = require("./attributes"); -errorBars.supplyDefaults = require('./defaults'); +errorBars.supplyDefaults = require("./defaults"); -errorBars.calc = require('./calc'); +errorBars.calc = require("./calc"); errorBars.calcFromTrace = function(trace, layout) { - var x = trace.x || [], - y = trace.y, - len = x.length || y.length; + var x = trace.x || [], y = trace.y, len = x.length || y.length; - var calcdataMock = new Array(len); + var calcdataMock = new Array(len); - for(var i = 0; i < len; i++) { - calcdataMock[i] = { - x: x[i], - y: y[i] - }; - } + for (var i = 0; i < len; i++) { + calcdataMock[i] = { x: x[i], y: y[i] }; + } - calcdataMock[0].trace = trace; + calcdataMock[0].trace = trace; - errorBars.calc({ - calcdata: [calcdataMock], - _fullLayout: layout - }); + errorBars.calc({ calcdata: [calcdataMock], _fullLayout: layout }); - return calcdataMock; + return calcdataMock; }; -errorBars.plot = require('./plot'); +errorBars.plot = require("./plot"); -errorBars.style = require('./style'); +errorBars.style = require("./style"); errorBars.hoverInfo = function(calcPoint, trace, hoverPoint) { - if((trace.error_y || {}).visible) { - hoverPoint.yerr = calcPoint.yh - calcPoint.y; - if(!trace.error_y.symmetric) hoverPoint.yerrneg = calcPoint.y - calcPoint.ys; + if ((trace.error_y || {}).visible) { + hoverPoint.yerr = calcPoint.yh - calcPoint.y; + if (!trace.error_y.symmetric) { + hoverPoint.yerrneg = calcPoint.y - calcPoint.ys; } - if((trace.error_x || {}).visible) { - hoverPoint.xerr = calcPoint.xh - calcPoint.x; - if(!trace.error_x.symmetric) hoverPoint.xerrneg = calcPoint.x - calcPoint.xs; + } + if ((trace.error_x || {}).visible) { + hoverPoint.xerr = calcPoint.xh - calcPoint.x; + if (!trace.error_x.symmetric) { + hoverPoint.xerrneg = calcPoint.x - calcPoint.xs; } + } }; diff --git a/src/components/errorbars/plot.js b/src/components/errorbars/plot.js index 84bc05504bf..57e8fb7309e 100644 --- a/src/components/errorbars/plot.js +++ b/src/components/errorbars/plot.js @@ -5,158 +5,171 @@ * 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 d3 = require("d3"); +var isNumeric = require("fast-isnumeric"); - -'use strict'; - -var d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var subTypes = require('../../traces/scatter/subtypes'); +var subTypes = require("../../traces/scatter/subtypes"); module.exports = function plot(traces, plotinfo, transitionOpts) { - var isNew; - - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis; - - var hasAnimation = transitionOpts && transitionOpts.duration > 0; - - traces.each(function(d) { - var trace = d[0].trace, - // || {} is in case the trace (specifically scatterternary) - // doesn't support error bars at all, but does go through - // the scatter.plot mechanics, which calls ErrorBars.plot - // internally - xObj = trace.error_x || {}, - yObj = trace.error_y || {}; - - var keyFunc; - - if(trace.ids) { - keyFunc = function(d) {return d.id;}; - } - - var sparse = ( - subTypes.hasMarkers(trace) && - trace.marker.maxdisplayed > 0 - ); - - if(!yObj.visible && !xObj.visible) return; - - var errorbars = d3.select(this).selectAll('g.errorbar') - .data(d, keyFunc); - - errorbars.exit().remove(); + var isNew; - errorbars.style('opacity', 1); + var xa = plotinfo.xaxis, ya = plotinfo.yaxis; - var enter = errorbars.enter().append('g') - .classed('errorbar', true); - - if(hasAnimation) { - enter.style('opacity', 0).transition() - .duration(transitionOpts.duration) - .style('opacity', 1); - } - - errorbars.each(function(d) { - var errorbar = d3.select(this); - var coords = errorCoords(d, xa, ya); - - if(sparse && !d.vis) return; - - var path; - - if(yObj.visible && isNumeric(coords.x) && - isNumeric(coords.yh) && - isNumeric(coords.ys)) { - var yw = yObj.width; - - path = 'M' + (coords.x - yw) + ',' + - coords.yh + 'h' + (2 * yw) + // hat - 'm-' + yw + ',0V' + coords.ys; // bar + var hasAnimation = transitionOpts && transitionOpts.duration > 0; + traces.each(function(d) { + var trace = d[0].trace, + // || {} is in case the trace (specifically scatterternary) + // doesn't support error bars at all, but does go through + // the scatter.plot mechanics, which calls ErrorBars.plot + // internally + xObj = trace.error_x || {}, + yObj = trace.error_y || {}; - if(!coords.noYS) path += 'm-' + yw + ',0h' + (2 * yw); // shoe + var keyFunc; - var yerror = errorbar.select('path.yerror'); + if (trace.ids) { + keyFunc = function(d) { + return d.id; + }; + } - isNew = !yerror.size(); + var sparse = subTypes.hasMarkers(trace) && trace.marker.maxdisplayed > 0; - if(isNew) { - yerror = errorbar.append('path') - .classed('yerror', true); - } else if(hasAnimation) { - yerror = yerror - .transition() - .duration(transitionOpts.duration) - .ease(transitionOpts.easing); - } + if (!yObj.visible && !xObj.visible) return; - yerror.attr('d', path); - } + var errorbars = d3.select(this).selectAll("g.errorbar").data(d, keyFunc); - if(xObj.visible && isNumeric(coords.y) && - isNumeric(coords.xh) && - isNumeric(coords.xs)) { - var xw = (xObj.copy_ystyle ? yObj : xObj).width; + errorbars.exit().remove(); - path = 'M' + coords.xh + ',' + - (coords.y - xw) + 'v' + (2 * xw) + // hat - 'm0,-' + xw + 'H' + coords.xs; // bar + errorbars.style("opacity", 1); - if(!coords.noXS) path += 'm0,-' + xw + 'v' + (2 * xw); // shoe + var enter = errorbars.enter().append("g").classed("errorbar", true); - var xerror = errorbar.select('path.xerror'); + if (hasAnimation) { + enter + .style("opacity", 0) + .transition() + .duration(transitionOpts.duration) + .style("opacity", 1); + } - isNew = !xerror.size(); + errorbars.each(function(d) { + var errorbar = d3.select(this); + var coords = errorCoords(d, xa, ya); + + if (sparse && !d.vis) return; + + var path; + + if ( + yObj.visible && + isNumeric(coords.x) && + isNumeric(coords.yh) && + isNumeric(coords.ys) + ) { + var yw = yObj.width; + + path = "M" + + (coords.x - yw) + + "," + + coords.yh + + "h" + + 2 * yw + // hat + "m-" + + yw + + ",0V" + + coords.ys; + + // bar + if (!coords.noYS) path += "m-" + yw + ",0h" + 2 * yw; + + // shoe + var yerror = errorbar.select("path.yerror"); + + isNew = !yerror.size(); + + if (isNew) { + yerror = errorbar.append("path").classed("yerror", true); + } else if (hasAnimation) { + yerror = yerror + .transition() + .duration(transitionOpts.duration) + .ease(transitionOpts.easing); + } - if(isNew) { - xerror = errorbar.append('path') - .classed('xerror', true); - } else if(hasAnimation) { - xerror = xerror - .transition() - .duration(transitionOpts.duration) - .ease(transitionOpts.easing); - } + yerror.attr("d", path); + } + + if ( + xObj.visible && + isNumeric(coords.y) && + isNumeric(coords.xh) && + isNumeric(coords.xs) + ) { + var xw = (xObj.copy_ystyle ? yObj : xObj).width; + + path = "M" + + coords.xh + + "," + + (coords.y - xw) + + "v" + + 2 * xw + // hat + "m0,-" + + xw + + "H" + + coords.xs; + + // bar + if (!coords.noXS) path += "m0,-" + xw + "v" + 2 * xw; + + // shoe + var xerror = errorbar.select("path.xerror"); + + isNew = !xerror.size(); + + if (isNew) { + xerror = errorbar.append("path").classed("xerror", true); + } else if (hasAnimation) { + xerror = xerror + .transition() + .duration(transitionOpts.duration) + .ease(transitionOpts.easing); + } - xerror.attr('d', path); - } - }); + xerror.attr("d", path); + } }); + }); }; // compute the coordinates of the error-bar objects function errorCoords(d, xa, ya) { - var out = { - x: xa.c2p(d.x), - y: ya.c2p(d.y) - }; - - // calculate the error bar size and hat and shoe locations - if(d.yh !== undefined) { - out.yh = ya.c2p(d.yh); - out.ys = ya.c2p(d.ys); - - // if the shoes go off-scale (ie log scale, error bars past zero) - // clip the bar and hide the shoes - if(!isNumeric(out.ys)) { - out.noYS = true; - out.ys = ya.c2p(d.ys, true); - } + var out = { x: xa.c2p(d.x), y: ya.c2p(d.y) }; + + // calculate the error bar size and hat and shoe locations + if (d.yh !== undefined) { + out.yh = ya.c2p(d.yh); + out.ys = ya.c2p(d.ys); + + // if the shoes go off-scale (ie log scale, error bars past zero) + // clip the bar and hide the shoes + if (!isNumeric(out.ys)) { + out.noYS = true; + out.ys = ya.c2p(d.ys, true); } + } - if(d.xh !== undefined) { - out.xh = xa.c2p(d.xh); - out.xs = xa.c2p(d.xs); + if (d.xh !== undefined) { + out.xh = xa.c2p(d.xh); + out.xs = xa.c2p(d.xs); - if(!isNumeric(out.xs)) { - out.noXS = true; - out.xs = xa.c2p(d.xs, true); - } + if (!isNumeric(out.xs)) { + out.noXS = true; + out.xs = xa.c2p(d.xs, true); } + } - return out; + return out; } diff --git a/src/components/errorbars/style.js b/src/components/errorbars/style.js index b6c81feb662..fa6b3fe0239 100644 --- a/src/components/errorbars/style.js +++ b/src/components/errorbars/style.js @@ -5,31 +5,29 @@ * 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 d3 = require("d3"); - -'use strict'; - -var d3 = require('d3'); - -var Color = require('../color'); - +var Color = require("../color"); module.exports = function style(traces) { - traces.each(function(d) { - var trace = d[0].trace, - yObj = trace.error_y || {}, - xObj = trace.error_x || {}; + traces.each(function(d) { + var trace = d[0].trace, + yObj = trace.error_y || {}, + xObj = trace.error_x || {}; - var s = d3.select(this); + var s = d3.select(this); - s.selectAll('path.yerror') - .style('stroke-width', yObj.thickness + 'px') - .call(Color.stroke, yObj.color); + s + .selectAll("path.yerror") + .style("stroke-width", yObj.thickness + "px") + .call(Color.stroke, yObj.color); - if(xObj.copy_ystyle) xObj = yObj; + if (xObj.copy_ystyle) xObj = yObj; - s.selectAll('path.xerror') - .style('stroke-width', xObj.thickness + 'px') - .call(Color.stroke, xObj.color); - }); + s + .selectAll("path.xerror") + .style("stroke-width", xObj.thickness + "px") + .call(Color.stroke, xObj.color); + }); }; diff --git a/src/components/images/attributes.js b/src/components/images/attributes.js index 2f15c72f908..4eaa3795d93 100644 --- a/src/components/images/attributes.js +++ b/src/components/images/attributes.js @@ -5,163 +5,138 @@ * 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 cartesianConstants = require('../../plots/cartesian/constants'); - +"use strict"; +var cartesianConstants = require("../../plots/cartesian/constants"); module.exports = { - _isLinkedToArray: 'image', - - visible: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines whether or not this image is visible.' - ].join(' ') - }, - - source: { - valType: 'string', - role: 'info', - description: [ - 'Specifies the URL of the image to be used.', - 'The URL must be accessible from the domain where the', - 'plot code is run, and can be either relative or absolute.' - - ].join(' ') - }, - - layer: { - valType: 'enumerated', - values: ['below', 'above'], - dflt: 'above', - role: 'info', - description: [ - 'Specifies whether images are drawn below or above traces.', - 'When `xref` and `yref` are both set to `paper`,', - 'image is drawn below the entire plot area.' - ].join(' ') - }, - - sizex: { - valType: 'number', - role: 'info', - dflt: 0, - description: [ - 'Sets the image container size horizontally.', - 'The image will be sized based on the `position` value.', - 'When `xref` is set to `paper`, units are sized relative', - 'to the plot width.' - ].join(' ') - }, - - sizey: { - valType: 'number', - role: 'info', - dflt: 0, - description: [ - 'Sets the image container size vertically.', - 'The image will be sized based on the `position` value.', - 'When `yref` is set to `paper`, units are sized relative', - 'to the plot height.' - ].join(' ') - }, - - sizing: { - valType: 'enumerated', - values: ['fill', 'contain', 'stretch'], - dflt: 'contain', - role: 'info', - description: [ - 'Specifies which dimension of the image to constrain.' - ].join(' ') - }, - - opacity: { - valType: 'number', - role: 'info', - min: 0, - max: 1, - dflt: 1, - description: 'Sets the opacity of the image.' - }, - - x: { - valType: 'any', - role: 'info', - dflt: 0, - description: [ - 'Sets the image\'s x position.', - 'When `xref` is set to `paper`, units are sized relative', - 'to the plot height.', - 'See `xref` for more info' - ].join(' ') - }, - - y: { - valType: 'any', - role: 'info', - dflt: 0, - description: [ - 'Sets the image\'s y position.', - 'When `yref` is set to `paper`, units are sized relative', - 'to the plot height.', - 'See `yref` for more info' - ].join(' ') - }, - - xanchor: { - valType: 'enumerated', - values: ['left', 'center', 'right'], - dflt: 'left', - role: 'info', - description: 'Sets the anchor for the x position' - }, - - yanchor: { - valType: 'enumerated', - values: ['top', 'middle', 'bottom'], - dflt: 'top', - role: 'info', - description: 'Sets the anchor for the y position.' - }, - - xref: { - valType: 'enumerated', - values: [ - 'paper', - cartesianConstants.idRegex.x.toString() - ], - dflt: 'paper', - role: 'info', - description: [ - 'Sets the images\'s x coordinate axis.', - 'If set to a x axis id (e.g. *x* or *x2*), the `x` position', - 'refers to an x data coordinate', - 'If set to *paper*, the `x` position refers to the distance from', - 'the left of plot in normalized coordinates', - 'where *0* (*1*) corresponds to the left (right).' - ].join(' ') - }, - - yref: { - valType: 'enumerated', - values: [ - 'paper', - cartesianConstants.idRegex.y.toString() - ], - dflt: 'paper', - role: 'info', - description: [ - 'Sets the images\'s y coordinate axis.', - 'If set to a y axis id (e.g. *y* or *y2*), the `y` position', - 'refers to a y data coordinate.', - 'If set to *paper*, the `y` position refers to the distance from', - 'the bottom of the plot in normalized coordinates', - 'where *0* (*1*) corresponds to the bottom (top).' - ].join(' ') - } + _isLinkedToArray: "image", + visible: { + valType: "boolean", + role: "info", + dflt: true, + description: ["Determines whether or not this image is visible."].join(" ") + }, + source: { + valType: "string", + role: "info", + description: [ + "Specifies the URL of the image to be used.", + "The URL must be accessible from the domain where the", + "plot code is run, and can be either relative or absolute." + ].join(" ") + }, + layer: { + valType: "enumerated", + values: ["below", "above"], + dflt: "above", + role: "info", + description: [ + "Specifies whether images are drawn below or above traces.", + "When `xref` and `yref` are both set to `paper`,", + "image is drawn below the entire plot area." + ].join(" ") + }, + sizex: { + valType: "number", + role: "info", + dflt: 0, + description: [ + "Sets the image container size horizontally.", + "The image will be sized based on the `position` value.", + "When `xref` is set to `paper`, units are sized relative", + "to the plot width." + ].join(" ") + }, + sizey: { + valType: "number", + role: "info", + dflt: 0, + description: [ + "Sets the image container size vertically.", + "The image will be sized based on the `position` value.", + "When `yref` is set to `paper`, units are sized relative", + "to the plot height." + ].join(" ") + }, + sizing: { + valType: "enumerated", + values: ["fill", "contain", "stretch"], + dflt: "contain", + role: "info", + description: ["Specifies which dimension of the image to constrain."].join( + " " + ) + }, + opacity: { + valType: "number", + role: "info", + min: 0, + max: 1, + dflt: 1, + description: "Sets the opacity of the image." + }, + x: { + valType: "any", + role: "info", + dflt: 0, + description: [ + "Sets the image's x position.", + "When `xref` is set to `paper`, units are sized relative", + "to the plot height.", + "See `xref` for more info" + ].join(" ") + }, + y: { + valType: "any", + role: "info", + dflt: 0, + description: [ + "Sets the image's y position.", + "When `yref` is set to `paper`, units are sized relative", + "to the plot height.", + "See `yref` for more info" + ].join(" ") + }, + xanchor: { + valType: "enumerated", + values: ["left", "center", "right"], + dflt: "left", + role: "info", + description: "Sets the anchor for the x position" + }, + yanchor: { + valType: "enumerated", + values: ["top", "middle", "bottom"], + dflt: "top", + role: "info", + description: "Sets the anchor for the y position." + }, + xref: { + valType: "enumerated", + values: ["paper", cartesianConstants.idRegex.x.toString()], + dflt: "paper", + role: "info", + description: [ + "Sets the images's x coordinate axis.", + "If set to a x axis id (e.g. *x* or *x2*), the `x` position", + "refers to an x data coordinate", + "If set to *paper*, the `x` position refers to the distance from", + "the left of plot in normalized coordinates", + "where *0* (*1*) corresponds to the left (right)." + ].join(" ") + }, + yref: { + valType: "enumerated", + values: ["paper", cartesianConstants.idRegex.y.toString()], + dflt: "paper", + role: "info", + description: [ + "Sets the images's y coordinate axis.", + "If set to a y axis id (e.g. *y* or *y2*), the `y` position", + "refers to a y data coordinate.", + "If set to *paper*, the `y` position refers to the distance from", + "the bottom of the plot in normalized coordinates", + "where *0* (*1*) corresponds to the bottom (top)." + ].join(" ") + } }; diff --git a/src/components/images/defaults.js b/src/components/images/defaults.js index 0c6c5b32c93..7705519adde 100644 --- a/src/components/images/defaults.js +++ b/src/components/images/defaults.js @@ -5,54 +5,48 @@ * 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 Axes = require("../../plots/cartesian/axes"); +var handleArrayContainerDefaults = require( + "../../plots/array_container_defaults" +); -'use strict'; - -var Lib = require('../../lib'); -var Axes = require('../../plots/cartesian/axes'); -var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); - -var attributes = require('./attributes'); -var name = 'images'; +var attributes = require("./attributes"); +var name = "images"; module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { - var opts = { - name: name, - handleItemDefaults: imageDefaults - }; + var opts = { name: name, handleItemDefaults: imageDefaults }; - handleArrayContainerDefaults(layoutIn, layoutOut, opts); + handleArrayContainerDefaults(layoutIn, layoutOut, opts); }; - function imageDefaults(imageIn, imageOut, fullLayout) { + function coerce(attr, dflt) { + return Lib.coerce(imageIn, imageOut, attributes, attr, dflt); + } - function coerce(attr, dflt) { - return Lib.coerce(imageIn, imageOut, attributes, attr, dflt); - } - - var source = coerce('source'); - var visible = coerce('visible', !!source); + var source = coerce("source"); + var visible = coerce("visible", !!source); - if(!visible) return imageOut; + if (!visible) return imageOut; - coerce('layer'); - coerce('x'); - coerce('y'); - coerce('xanchor'); - coerce('yanchor'); - coerce('sizex'); - coerce('sizey'); - coerce('sizing'); - coerce('opacity'); + coerce("layer"); + coerce("x"); + coerce("y"); + coerce("xanchor"); + coerce("yanchor"); + coerce("sizex"); + coerce("sizey"); + coerce("sizing"); + coerce("opacity"); - var gdMock = { _fullLayout: fullLayout }, - axLetters = ['x', 'y']; + var gdMock = { _fullLayout: fullLayout }, axLetters = ["x", "y"]; - for(var i = 0; i < 2; i++) { - // 'paper' is the fallback axref - Axes.coerceRef(imageIn, imageOut, gdMock, axLetters[i], 'paper'); - } + for (var i = 0; i < 2; i++) { + // 'paper' is the fallback axref + Axes.coerceRef(imageIn, imageOut, gdMock, axLetters[i], "paper"); + } - return imageOut; + return imageOut; } diff --git a/src/components/images/draw.js b/src/components/images/draw.js index 228916d3de4..7680e3b5836 100644 --- a/src/components/images/draw.js +++ b/src/components/images/draw.js @@ -5,174 +5,172 @@ * 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 d3 = require('d3'); -var Drawing = require('../drawing'); -var Axes = require('../../plots/cartesian/axes'); -var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); +"use strict"; +var d3 = require("d3"); +var Drawing = require("../drawing"); +var Axes = require("../../plots/cartesian/axes"); +var xmlnsNamespaces = require("../../constants/xmlns_namespaces"); module.exports = function draw(gd) { - var fullLayout = gd._fullLayout, - imageDataAbove = [], - imageDataSubplot = [], - imageDataBelow = []; - - // Sort into top, subplot, and bottom layers - for(var i = 0; i < fullLayout.images.length; i++) { - var img = fullLayout.images[i]; - - if(img.visible) { - if(img.layer === 'below' && img.xref !== 'paper' && img.yref !== 'paper') { - imageDataSubplot.push(img); - } else if(img.layer === 'above') { - imageDataAbove.push(img); - } else { - imageDataBelow.push(img); - } - } + var fullLayout = gd._fullLayout, + imageDataAbove = [], + imageDataSubplot = [], + imageDataBelow = []; + + // Sort into top, subplot, and bottom layers + for (var i = 0; i < fullLayout.images.length; i++) { + var img = fullLayout.images[i]; + + if (img.visible) { + if ( + img.layer === "below" && img.xref !== "paper" && img.yref !== "paper" + ) { + imageDataSubplot.push(img); + } else if (img.layer === "above") { + imageDataAbove.push(img); + } else { + imageDataBelow.push(img); + } } + } + + var anchors = { + x: { + left: { sizing: "xMin", offset: 0 }, + center: { sizing: "xMid", offset: (-1) / 2 }, + right: { sizing: "xMax", offset: -1 } + }, + y: { + top: { sizing: "YMin", offset: 0 }, + middle: { sizing: "YMid", offset: (-1) / 2 }, + bottom: { sizing: "YMax", offset: -1 } + } + }; + // Images must be converted to dataURL's for exporting. + function setImage(d) { + var thisImage = d3.select(this); - var anchors = { - x: { - left: { sizing: 'xMin', offset: 0 }, - center: { sizing: 'xMid', offset: -1 / 2 }, - right: { sizing: 'xMax', offset: -1 } - }, - y: { - top: { sizing: 'YMin', offset: 0 }, - middle: { sizing: 'YMid', offset: -1 / 2 }, - bottom: { sizing: 'YMax', offset: -1 } - } - }; - - - // Images must be converted to dataURL's for exporting. - function setImage(d) { - var thisImage = d3.select(this); - - if(this.img && this.img.src === d.source) { - return; - } - - thisImage.attr('xmlns', xmlnsNamespaces.svg); - - var imagePromise = new Promise(function(resolve) { - - var img = new Image(); - this.img = img; - - // If not set, a `tainted canvas` error is thrown - img.setAttribute('crossOrigin', 'anonymous'); - img.onerror = errorHandler; - img.onload = function() { - var canvas = document.createElement('canvas'); - canvas.width = this.width; - canvas.height = this.height; - - var ctx = canvas.getContext('2d'); - ctx.drawImage(this, 0, 0); - - var dataURL = canvas.toDataURL('image/png'); - - thisImage.attr('xlink:href', dataURL); - }; + if (this.img && this.img.src === d.source) { + return; + } + thisImage.attr("xmlns", xmlnsNamespaces.svg); - thisImage.on('error', errorHandler); - thisImage.on('load', resolve); + var imagePromise = new Promise( + (function(resolve) { + var img = new Image(); + this.img = img; - img.src = d.source; + // If not set, a `tainted canvas` error is thrown + img.setAttribute("crossOrigin", "anonymous"); + img.onerror = errorHandler; + img.onload = function() { + var canvas = document.createElement("canvas"); + canvas.width = this.width; + canvas.height = this.height; - function errorHandler() { - thisImage.remove(); - resolve(); - } - }.bind(this)); + var ctx = canvas.getContext("2d"); + ctx.drawImage(this, 0, 0); - gd._promises.push(imagePromise); - } + var dataURL = canvas.toDataURL("image/png"); - function applyAttributes(d) { - var thisImage = d3.select(this); + thisImage.attr("xlink:href", dataURL); + }; - // Axes if specified - var xa = Axes.getFromId(gd, d.xref), - ya = Axes.getFromId(gd, d.yref); + thisImage.on("error", errorHandler); + thisImage.on("load", resolve); - var size = fullLayout._size, - width = xa ? Math.abs(xa.l2p(d.sizex) - xa.l2p(0)) : d.sizex * size.w, - height = ya ? Math.abs(ya.l2p(d.sizey) - ya.l2p(0)) : d.sizey * size.h; + img.src = d.source; - // Offsets for anchor positioning - var xOffset = width * anchors.x[d.xanchor].offset, - yOffset = height * anchors.y[d.yanchor].offset; + function errorHandler() { + thisImage.remove(); + resolve(); + } + }).bind(this) + ); - var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing; + gd._promises.push(imagePromise); + } - // Final positions - var xPos = (xa ? xa.r2p(d.x) + xa._offset : d.x * size.w + size.l) + xOffset, - yPos = (ya ? ya.r2p(d.y) + ya._offset : size.h - d.y * size.h + size.t) + yOffset; + function applyAttributes(d) { + var thisImage = d3.select(this); + // Axes if specified + var xa = Axes.getFromId(gd, d.xref), ya = Axes.getFromId(gd, d.yref); - // Construct the proper aspectRatio attribute - switch(d.sizing) { - case 'fill': - sizing += ' slice'; - break; + var size = fullLayout._size, + width = xa ? Math.abs(xa.l2p(d.sizex) - xa.l2p(0)) : d.sizex * size.w, + height = ya ? Math.abs(ya.l2p(d.sizey) - ya.l2p(0)) : d.sizey * size.h; - case 'stretch': - sizing = 'none'; - break; - } + // Offsets for anchor positioning + var xOffset = width * anchors.x[d.xanchor].offset, + yOffset = height * anchors.y[d.yanchor].offset; - thisImage.attr({ - x: xPos, - y: yPos, - width: width, - height: height, - preserveAspectRatio: sizing, - opacity: d.opacity - }); + var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing; + // Final positions + var xPos = (xa ? xa.r2p(d.x) + xa._offset : d.x * size.w + size.l) + + xOffset, + yPos = (ya ? ya.r2p(d.y) + ya._offset : size.h - d.y * size.h + size.t) + + yOffset; - // Set proper clipping on images - var xId = xa ? xa._id : '', - yId = ya ? ya._id : '', - clipAxes = xId + yId; + // Construct the proper aspectRatio attribute + switch (d.sizing) { + case "fill": + sizing += " slice"; + break; - if(clipAxes) { - thisImage.call(Drawing.setClipUrl, 'clip' + fullLayout._uid + clipAxes); - } + case "stretch": + sizing = "none"; + break; } - var imagesBelow = fullLayout._imageLowerLayer.selectAll('image') - .data(imageDataBelow), - imagesSubplot = fullLayout._imageSubplotLayer.selectAll('image') - .data(imageDataSubplot), - imagesAbove = fullLayout._imageUpperLayer.selectAll('image') - .data(imageDataAbove); - - imagesBelow.enter().append('image'); - imagesSubplot.enter().append('image'); - imagesAbove.enter().append('image'); + thisImage.attr({ + x: xPos, + y: yPos, + width: width, + height: height, + preserveAspectRatio: sizing, + opacity: d.opacity + }); - imagesBelow.exit().remove(); - imagesSubplot.exit().remove(); - imagesAbove.exit().remove(); + // Set proper clipping on images + var xId = xa ? xa._id : "", yId = ya ? ya._id : "", clipAxes = xId + yId; - imagesBelow.each(function(d) { - setImage.bind(this)(d); - applyAttributes.bind(this)(d); - }); - imagesSubplot.each(function(d) { - setImage.bind(this)(d); - applyAttributes.bind(this)(d); - }); - imagesAbove.each(function(d) { - setImage.bind(this)(d); - applyAttributes.bind(this)(d); - }); + if (clipAxes) { + thisImage.call(Drawing.setClipUrl, "clip" + fullLayout._uid + clipAxes); + } + } + + var imagesBelow = fullLayout._imageLowerLayer + .selectAll("image") + .data(imageDataBelow), + imagesSubplot = fullLayout._imageSubplotLayer + .selectAll("image") + .data(imageDataSubplot), + imagesAbove = fullLayout._imageUpperLayer + .selectAll("image") + .data(imageDataAbove); + + imagesBelow.enter().append("image"); + imagesSubplot.enter().append("image"); + imagesAbove.enter().append("image"); + + imagesBelow.exit().remove(); + imagesSubplot.exit().remove(); + imagesAbove.exit().remove(); + + imagesBelow.each(function(d) { + setImage.bind(this)(d); + applyAttributes.bind(this)(d); + }); + imagesSubplot.each(function(d) { + setImage.bind(this)(d); + applyAttributes.bind(this)(d); + }); + imagesAbove.each(function(d) { + setImage.bind(this)(d); + applyAttributes.bind(this)(d); + }); }; diff --git a/src/components/images/index.js b/src/components/images/index.js index d7ce308ae28..61363f699a7 100644 --- a/src/components/images/index.js +++ b/src/components/images/index.js @@ -5,15 +5,11 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = { - moduleType: 'component', - name: 'images', - - layoutAttributes: require('./attributes'), - supplyLayoutDefaults: require('./defaults'), - - draw: require('./draw') + moduleType: "component", + name: "images", + layoutAttributes: require("./attributes"), + supplyLayoutDefaults: require("./defaults"), + draw: require("./draw") }; diff --git a/src/components/legend/anchor_utils.js b/src/components/legend/anchor_utils.js index 2dcc0161538..5975e6d623b 100644 --- a/src/components/legend/anchor_utils.js +++ b/src/components/legend/anchor_utils.js @@ -5,11 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; /** * Determine the position anchor property of x/y xanchor/yanchor components. * @@ -19,29 +15,20 @@ */ exports.isRightAnchor = function isRightAnchor(opts) { - return ( - opts.xanchor === 'right' || - (opts.xanchor === 'auto' && opts.x >= 2 / 3) - ); + return opts.xanchor === "right" || opts.xanchor === "auto" && opts.x >= 2 / 3; }; exports.isCenterAnchor = function isCenterAnchor(opts) { - return ( - opts.xanchor === 'center' || - (opts.xanchor === 'auto' && opts.x > 1 / 3 && opts.x < 2 / 3) - ); + return opts.xanchor === "center" || + opts.xanchor === "auto" && opts.x > 1 / 3 && opts.x < 2 / 3; }; exports.isBottomAnchor = function isBottomAnchor(opts) { - return ( - opts.yanchor === 'bottom' || - (opts.yanchor === 'auto' && opts.y <= 1 / 3) - ); + return opts.yanchor === "bottom" || + opts.yanchor === "auto" && opts.y <= 1 / 3; }; exports.isMiddleAnchor = function isMiddleAnchor(opts) { - return ( - opts.yanchor === 'middle' || - (opts.yanchor === 'auto' && opts.y > 1 / 3 && opts.y < 2 / 3) - ); + return opts.yanchor === "middle" || + opts.yanchor === "auto" && opts.y > 1 / 3 && opts.y < 2 / 3; }; diff --git a/src/components/legend/attributes.js b/src/components/legend/attributes.js index 8ae61ac29be..77a1825dbb1 100644 --- a/src/components/legend/attributes.js +++ b/src/components/legend/attributes.js @@ -5,109 +5,102 @@ * 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 fontAttrs = require('../../plots/font_attributes'); -var colorAttrs = require('../color/attributes'); -var extendFlat = require('../../lib/extend').extendFlat; - +"use strict"; +var fontAttrs = require("../../plots/font_attributes"); +var colorAttrs = require("../color/attributes"); +var extendFlat = require("../../lib/extend").extendFlat; module.exports = { - bgcolor: { - valType: 'color', - role: 'style', - description: 'Sets the legend background color.' - }, - bordercolor: { - valType: 'color', - dflt: colorAttrs.defaultLine, - role: 'style', - description: 'Sets the color of the border enclosing the legend.' - }, - borderwidth: { - valType: 'number', - min: 0, - dflt: 0, - role: 'style', - description: 'Sets the width (in px) of the border enclosing the legend.' - }, - font: extendFlat({}, fontAttrs, { - description: 'Sets the font used to text the legend items.' - }), - orientation: { - valType: 'enumerated', - values: ['v', 'h'], - dflt: 'v', - role: 'info', - description: 'Sets the orientation of the legend.' - }, - traceorder: { - valType: 'flaglist', - flags: ['reversed', 'grouped'], - extras: ['normal'], - role: 'style', - description: [ - 'Determines the order at which the legend items are displayed.', - - 'If *normal*, the items are displayed top-to-bottom in the same', - 'order as the input data.', - - 'If *reversed*, the items are displayed in the opposite order', - 'as *normal*.', - - 'If *grouped*, the items are displayed in groups', - '(when a trace `legendgroup` is provided).', - - 'if *grouped+reversed*, the items are displayed in the opposite order', - 'as *grouped*.' - ].join(' ') - }, - tracegroupgap: { - valType: 'number', - min: 0, - dflt: 10, - role: 'style', - description: [ - 'Sets the amount of vertical space (in px) between legend groups.' - ].join(' ') - }, - x: { - valType: 'number', - min: -2, - max: 3, - dflt: 1.02, - role: 'style', - description: 'Sets the x position (in normalized coordinates) of the legend.' - }, - xanchor: { - valType: 'enumerated', - values: ['auto', 'left', 'center', 'right'], - dflt: 'left', - role: 'info', - description: [ - 'Sets the legend\'s horizontal position anchor.', - 'This anchor binds the `x` position to the *left*, *center*', - 'or *right* of the legend.' - ].join(' ') - }, - y: { - valType: 'number', - min: -2, - max: 3, - dflt: 1, - role: 'style', - description: 'Sets the y position (in normalized coordinates) of the legend.' - }, - yanchor: { - valType: 'enumerated', - values: ['auto', 'top', 'middle', 'bottom'], - dflt: 'auto', - role: 'info', - description: [ - 'Sets the legend\'s vertical position anchor', - 'This anchor binds the `y` position to the *top*, *middle*', - 'or *bottom* of the legend.' - ].join(' ') - } + bgcolor: { + valType: "color", + role: "style", + description: "Sets the legend background color." + }, + bordercolor: { + valType: "color", + dflt: colorAttrs.defaultLine, + role: "style", + description: "Sets the color of the border enclosing the legend." + }, + borderwidth: { + valType: "number", + min: 0, + dflt: 0, + role: "style", + description: "Sets the width (in px) of the border enclosing the legend." + }, + font: extendFlat({}, fontAttrs, { + description: "Sets the font used to text the legend items." + }), + orientation: { + valType: "enumerated", + values: ["v", "h"], + dflt: "v", + role: "info", + description: "Sets the orientation of the legend." + }, + traceorder: { + valType: "flaglist", + flags: ["reversed", "grouped"], + extras: ["normal"], + role: "style", + description: [ + "Determines the order at which the legend items are displayed.", + "If *normal*, the items are displayed top-to-bottom in the same", + "order as the input data.", + "If *reversed*, the items are displayed in the opposite order", + "as *normal*.", + "If *grouped*, the items are displayed in groups", + "(when a trace `legendgroup` is provided).", + "if *grouped+reversed*, the items are displayed in the opposite order", + "as *grouped*." + ].join(" ") + }, + tracegroupgap: { + valType: "number", + min: 0, + dflt: 10, + role: "style", + description: [ + "Sets the amount of vertical space (in px) between legend groups." + ].join(" ") + }, + x: { + valType: "number", + min: -2, + max: 3, + dflt: 1.02, + role: "style", + description: "Sets the x position (in normalized coordinates) of the legend." + }, + xanchor: { + valType: "enumerated", + values: ["auto", "left", "center", "right"], + dflt: "left", + role: "info", + description: [ + "Sets the legend's horizontal position anchor.", + "This anchor binds the `x` position to the *left*, *center*", + "or *right* of the legend." + ].join(" ") + }, + y: { + valType: "number", + min: -2, + max: 3, + dflt: 1, + role: "style", + description: "Sets the y position (in normalized coordinates) of the legend." + }, + yanchor: { + valType: "enumerated", + values: ["auto", "top", "middle", "bottom"], + dflt: "auto", + role: "info", + description: [ + "Sets the legend's vertical position anchor", + "This anchor binds the `y` position to the *top*, *middle*", + "or *bottom* of the legend." + ].join(" ") + } }; diff --git a/src/components/legend/constants.js b/src/components/legend/constants.js index 527fa7ba190..344c828f6bf 100644 --- a/src/components/legend/constants.js +++ b/src/components/legend/constants.js @@ -5,12 +5,10 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = { - scrollBarWidth: 4, - scrollBarHeight: 20, - scrollBarColor: '#808BA4', - scrollBarMargin: 4 + scrollBarWidth: 4, + scrollBarHeight: 20, + scrollBarColor: "#808BA4", + scrollBarMargin: 4 }; diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index a094d5799ba..b430f38e6ea 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -5,87 +5,90 @@ * 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 Registry = require("../../registry"); +var Lib = require("../../lib"); - -'use strict'; - -var Registry = require('../../registry'); -var Lib = require('../../lib'); - -var attributes = require('./attributes'); -var basePlotLayoutAttributes = require('../../plots/layout_attributes'); -var helpers = require('./helpers'); - +var attributes = require("./attributes"); +var basePlotLayoutAttributes = require("../../plots/layout_attributes"); +var helpers = require("./helpers"); module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { - var containerIn = layoutIn.legend || {}, - containerOut = layoutOut.legend = {}; - - var visibleTraces = 0, - defaultOrder = 'normal', - defaultX, - defaultY, - defaultXAnchor, - defaultYAnchor; - - for(var i = 0; i < fullData.length; i++) { - var trace = fullData[i]; - - if(helpers.legendGetsTrace(trace)) { - visibleTraces++; - // always show the legend by default if there's a pie - if(Registry.traceIs(trace, 'pie')) visibleTraces++; - } - - if((Registry.traceIs(trace, 'bar') && layoutOut.barmode === 'stack') || - ['tonextx', 'tonexty'].indexOf(trace.fill) !== -1) { - defaultOrder = helpers.isGrouped({traceorder: defaultOrder}) ? - 'grouped+reversed' : 'reversed'; - } - - if(trace.legendgroup !== undefined && trace.legendgroup !== '') { - defaultOrder = helpers.isReversed({traceorder: defaultOrder}) ? - 'reversed+grouped' : 'grouped'; - } + var containerIn = layoutIn.legend || {}, containerOut = layoutOut.legend = {}; + + var visibleTraces = 0, + defaultOrder = "normal", + defaultX, + defaultY, + defaultXAnchor, + defaultYAnchor; + + for (var i = 0; i < fullData.length; i++) { + var trace = fullData[i]; + + if (helpers.legendGetsTrace(trace)) { + visibleTraces++; + // always show the legend by default if there's a pie + if (Registry.traceIs(trace, "pie")) visibleTraces++; } - function coerce(attr, dflt) { - return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); + if ( + Registry.traceIs(trace, "bar") && layoutOut.barmode === "stack" || + ["tonextx", "tonexty"].indexOf(trace.fill) !== -1 + ) { + defaultOrder = helpers.isGrouped({ traceorder: defaultOrder }) + ? "grouped+reversed" + : "reversed"; } - var showLegend = Lib.coerce(layoutIn, layoutOut, - basePlotLayoutAttributes, 'showlegend', visibleTraces > 1); - - if(showLegend === false) return; - - coerce('bgcolor', layoutOut.paper_bgcolor); - coerce('bordercolor'); - coerce('borderwidth'); - Lib.coerceFont(coerce, 'font', layoutOut.font); - - coerce('orientation'); - if(containerOut.orientation === 'h') { - var xaxis = layoutIn.xaxis; - if(xaxis && xaxis.rangeslider && xaxis.rangeslider.visible) { - defaultX = 0; - defaultXAnchor = 'left'; - defaultY = 1.1; - defaultYAnchor = 'bottom'; - } - else { - defaultX = 0; - defaultXAnchor = 'left'; - defaultY = -0.1; - defaultYAnchor = 'top'; - } + if (trace.legendgroup !== undefined && trace.legendgroup !== "") { + defaultOrder = helpers.isReversed({ traceorder: defaultOrder }) + ? "reversed+grouped" + : "grouped"; + } + } + + function coerce(attr, dflt) { + return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); + } + + var showLegend = Lib.coerce( + layoutIn, + layoutOut, + basePlotLayoutAttributes, + "showlegend", + visibleTraces > 1 + ); + + if (showLegend === false) return; + + coerce("bgcolor", layoutOut.paper_bgcolor); + coerce("bordercolor"); + coerce("borderwidth"); + Lib.coerceFont(coerce, "font", layoutOut.font); + + coerce("orientation"); + if (containerOut.orientation === "h") { + var xaxis = layoutIn.xaxis; + if (xaxis && xaxis.rangeslider && xaxis.rangeslider.visible) { + defaultX = 0; + defaultXAnchor = "left"; + defaultY = 1.1; + defaultYAnchor = "bottom"; + } else { + defaultX = 0; + defaultXAnchor = "left"; + defaultY = -0.1; + defaultYAnchor = "top"; } + } - coerce('traceorder', defaultOrder); - if(helpers.isGrouped(layoutOut.legend)) coerce('tracegroupgap'); + coerce("traceorder", defaultOrder); + if (helpers.isGrouped(layoutOut.legend)) coerce("tracegroupgap"); - coerce('x', defaultX); - coerce('xanchor', defaultXAnchor); - coerce('y', defaultY); - coerce('yanchor', defaultYAnchor); - Lib.noneOrAll(containerIn, containerOut, ['x', 'y']); + coerce("x", defaultX); + coerce("xanchor", defaultXAnchor); + coerce("y", defaultY); + coerce("yanchor", defaultYAnchor); + Lib.noneOrAll(containerIn, containerOut, ["x", "y"]); }; diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index d6c836c14e1..d6cf15ea24a 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -5,710 +5,677 @@ * 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 d3 = require("d3"); + +var Plotly = require("../../plotly"); +var Lib = require("../../lib"); +var Plots = require("../../plots/plots"); +var Registry = require("../../registry"); +var dragElement = require("../dragelement"); +var Drawing = require("../drawing"); +var Color = require("../color"); +var svgTextUtils = require("../../lib/svg_text_utils"); + +var constants = require("./constants"); +var getLegendData = require("./get_legend_data"); +var style = require("./style"); +var helpers = require("./helpers"); +var anchorUtils = require("./anchor_utils"); +module.exports = function draw(gd) { + var fullLayout = gd._fullLayout; + var clipId = "legend" + fullLayout._uid; -'use strict'; - -var d3 = require('d3'); - -var Plotly = require('../../plotly'); -var Lib = require('../../lib'); -var Plots = require('../../plots/plots'); -var Registry = require('../../registry'); -var dragElement = require('../dragelement'); -var Drawing = require('../drawing'); -var Color = require('../color'); -var svgTextUtils = require('../../lib/svg_text_utils'); + if (!fullLayout._infolayer || !gd.calcdata) return; -var constants = require('./constants'); -var getLegendData = require('./get_legend_data'); -var style = require('./style'); -var helpers = require('./helpers'); -var anchorUtils = require('./anchor_utils'); + var opts = fullLayout.legend, + legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts), + hiddenSlices = fullLayout.hiddenlabels || []; + if (!fullLayout.showlegend || !legendData.length) { + fullLayout._infolayer.selectAll(".legend").remove(); + fullLayout._topdefs.select("#" + clipId).remove(); -module.exports = function draw(gd) { - var fullLayout = gd._fullLayout; - var clipId = 'legend' + fullLayout._uid; + Plots.autoMargin(gd, "legend"); + return; + } - if(!fullLayout._infolayer || !gd.calcdata) return; + var legend = fullLayout._infolayer.selectAll("g.legend").data([0]); - var opts = fullLayout.legend, - legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts), - hiddenSlices = fullLayout.hiddenlabels || []; + legend.enter().append("g").attr({ class: "legend", "pointer-events": "all" }); - if(!fullLayout.showlegend || !legendData.length) { - fullLayout._infolayer.selectAll('.legend').remove(); - fullLayout._topdefs.select('#' + clipId).remove(); + var clipPath = fullLayout._topdefs.selectAll("#" + clipId).data([0]); - Plots.autoMargin(gd, 'legend'); - return; - } + clipPath.enter().append("clipPath").attr("id", clipId).append("rect"); - var legend = fullLayout._infolayer.selectAll('g.legend') - .data([0]); + var bg = legend.selectAll("rect.bg").data([0]); - legend.enter().append('g') - .attr({ - 'class': 'legend', - 'pointer-events': 'all' - }); + bg + .enter() + .append("rect") + .attr({ class: "bg", "shape-rendering": "crispEdges" }); - var clipPath = fullLayout._topdefs.selectAll('#' + clipId) - .data([0]); + bg.call(Color.stroke, opts.bordercolor); + bg.call(Color.fill, opts.bgcolor); + bg.style("stroke-width", opts.borderwidth + "px"); - clipPath.enter().append('clipPath') - .attr('id', clipId) - .append('rect'); + var scrollBox = legend.selectAll("g.scrollbox").data([0]); - var bg = legend.selectAll('rect.bg') - .data([0]); + scrollBox.enter().append("g").attr("class", "scrollbox"); - bg.enter().append('rect').attr({ - 'class': 'bg', - 'shape-rendering': 'crispEdges' - }); + var scrollBar = legend.selectAll("rect.scrollbar").data([0]); - bg.call(Color.stroke, opts.bordercolor); - bg.call(Color.fill, opts.bgcolor); - bg.style('stroke-width', opts.borderwidth + 'px'); - - var scrollBox = legend.selectAll('g.scrollbox') - .data([0]); - - scrollBox.enter().append('g') - .attr('class', 'scrollbox'); - - var scrollBar = legend.selectAll('rect.scrollbar') - .data([0]); - - scrollBar.enter().append('rect') - .attr({ - 'class': 'scrollbar', - 'rx': 20, - 'ry': 2, - 'width': 0, - 'height': 0 - }) - .call(Color.fill, '#808BA4'); - - var groups = scrollBox.selectAll('g.groups') - .data(legendData); - - groups.enter().append('g') - .attr('class', 'groups'); - - groups.exit().remove(); - - var traces = groups.selectAll('g.traces') - .data(Lib.identity); - - traces.enter().append('g').attr('class', 'traces'); - traces.exit().remove(); - - traces.call(style) - .style('opacity', function(d) { - var trace = d[0].trace; - if(Registry.traceIs(trace, 'pie')) { - return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1; - } else { - return trace.visible === 'legendonly' ? 0.5 : 1; - } - }) - .each(function() { - d3.select(this) - .call(drawTexts, gd) - .call(setupTraceToggle, gd); - }); - - var firstRender = legend.enter().size() !== 0; - if(firstRender) { - computeLegendDimensions(gd, groups, traces); - expandMargin(gd); - } + scrollBar + .enter() + .append("rect") + .attr({ class: "scrollbar", rx: 20, ry: 2, width: 0, height: 0 }) + .call(Color.fill, "#808BA4"); - // Position and size the legend - var lxMin = 0, - lxMax = fullLayout.width, - lyMin = 0, - lyMax = fullLayout.height; + var groups = scrollBox.selectAll("g.groups").data(legendData); - computeLegendDimensions(gd, groups, traces); + groups.enter().append("g").attr("class", "groups"); - if(opts.height > lyMax) { - // If the legend doesn't fit in the plot area, - // do not expand the vertical margins. - expandHorizontalMargin(gd); - } else { - expandMargin(gd); - } + groups.exit().remove(); - // Scroll section must be executed after repositionLegend. - // It requires the legend width, height, x and y to position the scrollbox - // and these values are mutated in repositionLegend. - var gs = fullLayout._size, - lx = gs.l + gs.w * opts.x, - ly = gs.t + gs.h * (1 - opts.y); + var traces = groups.selectAll("g.traces").data(Lib.identity); - if(anchorUtils.isRightAnchor(opts)) { - lx -= opts.width; - } - else if(anchorUtils.isCenterAnchor(opts)) { - lx -= opts.width / 2; - } + traces.enter().append("g").attr("class", "traces"); + traces.exit().remove(); - if(anchorUtils.isBottomAnchor(opts)) { - ly -= opts.height; - } - else if(anchorUtils.isMiddleAnchor(opts)) { - ly -= opts.height / 2; - } - - // Make sure the legend left and right sides are visible - var legendWidth = opts.width, - legendWidthMax = gs.w; + traces + .call(style) + .style("opacity", function(d) { + var trace = d[0].trace; + if (Registry.traceIs(trace, "pie")) { + return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1; + } else { + return trace.visible === "legendonly" ? 0.5 : 1; + } + }) + .each(function() { + d3.select(this).call(drawTexts, gd).call(setupTraceToggle, gd); + }); - if(legendWidth > legendWidthMax) { - lx = gs.l; - legendWidth = legendWidthMax; - } - else { - if(lx + legendWidth > lxMax) lx = lxMax - legendWidth; - if(lx < lxMin) lx = lxMin; - legendWidth = Math.min(lxMax - lx, opts.width); - } + var firstRender = legend.enter().size() !== 0; + if (firstRender) { + computeLegendDimensions(gd, groups, traces); + expandMargin(gd); + } + + // Position and size the legend + var lxMin = 0, lxMax = fullLayout.width, lyMin = 0, lyMax = fullLayout.height; + + computeLegendDimensions(gd, groups, traces); + + if (opts.height > lyMax) { + // If the legend doesn't fit in the plot area, + // do not expand the vertical margins. + expandHorizontalMargin(gd); + } else { + expandMargin(gd); + } + + // Scroll section must be executed after repositionLegend. + // It requires the legend width, height, x and y to position the scrollbox + // and these values are mutated in repositionLegend. + var gs = fullLayout._size, + lx = gs.l + gs.w * opts.x, + ly = gs.t + gs.h * (1 - opts.y); + + if (anchorUtils.isRightAnchor(opts)) { + lx -= opts.width; + } else if (anchorUtils.isCenterAnchor(opts)) { + lx -= opts.width / 2; + } + + if (anchorUtils.isBottomAnchor(opts)) { + ly -= opts.height; + } else if (anchorUtils.isMiddleAnchor(opts)) { + ly -= opts.height / 2; + } + + // Make sure the legend left and right sides are visible + var legendWidth = opts.width, legendWidthMax = gs.w; + + if (legendWidth > legendWidthMax) { + lx = gs.l; + legendWidth = legendWidthMax; + } else { + if (lx + legendWidth > lxMax) lx = lxMax - legendWidth; + if (lx < lxMin) lx = lxMin; + legendWidth = Math.min(lxMax - lx, opts.width); + } + + // Make sure the legend top and bottom are visible + // (legends with a scroll bar are not allowed to stretch beyond the extended + // margins) + var legendHeight = opts.height, legendHeightMax = gs.h; + + if (legendHeight > legendHeightMax) { + ly = gs.t; + legendHeight = legendHeightMax; + } else { + if (ly + legendHeight > lyMax) ly = lyMax - legendHeight; + if (ly < lyMin) ly = lyMin; + legendHeight = Math.min(lyMax - ly, opts.height); + } + + // Set size and position of all the elements that make up a legend: + // legend, background and border, scroll box and scroll bar + Drawing.setTranslate(legend, lx, ly); + + var scrollBarYMax = legendHeight - + constants.scrollBarHeight - + 2 * constants.scrollBarMargin, + scrollBoxYMax = opts.height - legendHeight, + scrollBarY, + scrollBoxY; + + if (opts.height <= legendHeight || gd._context.staticPlot) { + // if scrollbar should not be shown. + bg.attr({ + width: legendWidth - opts.borderwidth, + height: legendHeight - opts.borderwidth, + x: opts.borderwidth / 2, + y: opts.borderwidth / 2 + }); - // Make sure the legend top and bottom are visible - // (legends with a scroll bar are not allowed to stretch beyond the extended - // margins) - var legendHeight = opts.height, - legendHeightMax = gs.h; + Drawing.setTranslate(scrollBox, 0, 0); - if(legendHeight > legendHeightMax) { - ly = gs.t; - legendHeight = legendHeightMax; - } - else { - if(ly + legendHeight > lyMax) ly = lyMax - legendHeight; - if(ly < lyMin) ly = lyMin; - legendHeight = Math.min(lyMax - ly, opts.height); - } + clipPath.select("rect").attr({ + width: legendWidth - 2 * opts.borderwidth, + height: legendHeight - 2 * opts.borderwidth, + x: opts.borderwidth, + y: opts.borderwidth + }); - // Set size and position of all the elements that make up a legend: - // legend, background and border, scroll box and scroll bar - Drawing.setTranslate(legend, lx, ly); - - var scrollBarYMax = legendHeight - - constants.scrollBarHeight - - 2 * constants.scrollBarMargin, - scrollBoxYMax = opts.height - legendHeight, - scrollBarY, - scrollBoxY; - - if(opts.height <= legendHeight || gd._context.staticPlot) { - // if scrollbar should not be shown. - bg.attr({ - width: legendWidth - opts.borderwidth, - height: legendHeight - opts.borderwidth, - x: opts.borderwidth / 2, - y: opts.borderwidth / 2 - }); - - Drawing.setTranslate(scrollBox, 0, 0); - - clipPath.select('rect').attr({ - width: legendWidth - 2 * opts.borderwidth, - height: legendHeight - 2 * opts.borderwidth, - x: opts.borderwidth, - y: opts.borderwidth - }); - - scrollBox.call(Drawing.setClipUrl, clipId); - } - else { - scrollBarY = constants.scrollBarMargin, - scrollBoxY = scrollBox.attr('data-scroll') || 0; - - // increase the background and clip-path width - // by the scrollbar width and margin - bg.attr({ - width: legendWidth - - 2 * opts.borderwidth + - constants.scrollBarWidth + - constants.scrollBarMargin, - height: legendHeight - opts.borderwidth, - x: opts.borderwidth / 2, - y: opts.borderwidth / 2 - }); - - clipPath.select('rect').attr({ - width: legendWidth - - 2 * opts.borderwidth + - constants.scrollBarWidth + - constants.scrollBarMargin, - height: legendHeight - 2 * opts.borderwidth, - x: opts.borderwidth, - y: opts.borderwidth - scrollBoxY - }); - - scrollBox.call(Drawing.setClipUrl, clipId); - - if(firstRender) scrollHandler(scrollBarY, scrollBoxY); - - legend.on('wheel', null); // to be safe, remove previous listeners - legend.on('wheel', function() { - scrollBoxY = Lib.constrain( - scrollBox.attr('data-scroll') - - d3.event.deltaY / scrollBarYMax * scrollBoxYMax, - -scrollBoxYMax, 0); - scrollBarY = constants.scrollBarMargin - - scrollBoxY / scrollBoxYMax * scrollBarYMax; - scrollHandler(scrollBarY, scrollBoxY); - d3.event.preventDefault(); - }); - - // to be safe, remove previous listeners - scrollBar.on('.drag', null); - scrollBox.on('.drag', null); - - var drag = d3.behavior.drag().on('drag', function() { - scrollBarY = Lib.constrain( - d3.event.y - constants.scrollBarHeight / 2, - constants.scrollBarMargin, - constants.scrollBarMargin + scrollBarYMax); - scrollBoxY = - (scrollBarY - constants.scrollBarMargin) / - scrollBarYMax * scrollBoxYMax; - scrollHandler(scrollBarY, scrollBoxY); - }); - - scrollBar.call(drag); - scrollBox.call(drag); - } + scrollBox.call(Drawing.setClipUrl, clipId); + } else { + scrollBarY = constants.scrollBarMargin, scrollBoxY = scrollBox.attr( + "data-scroll" + ) || + 0; + + // increase the background and clip-path width + // by the scrollbar width and margin + bg.attr({ + width: ( + legendWidth - + 2 * opts.borderwidth + + constants.scrollBarWidth + + constants.scrollBarMargin + ), + height: legendHeight - opts.borderwidth, + x: opts.borderwidth / 2, + y: opts.borderwidth / 2 + }); + clipPath.select("rect").attr({ + width: ( + legendWidth - + 2 * opts.borderwidth + + constants.scrollBarWidth + + constants.scrollBarMargin + ), + height: legendHeight - 2 * opts.borderwidth, + x: opts.borderwidth, + y: opts.borderwidth - scrollBoxY + }); - function scrollHandler(scrollBarY, scrollBoxY) { - scrollBox - .attr('data-scroll', scrollBoxY) - .call(Drawing.setTranslate, 0, scrollBoxY); + scrollBox.call(Drawing.setClipUrl, clipId); + + if (firstRender) scrollHandler(scrollBarY, scrollBoxY); + + legend.on("wheel", null); + // to be safe, remove previous listeners + legend.on("wheel", function() { + scrollBoxY = Lib.constrain( + scrollBox.attr("data-scroll") - + d3.event.deltaY / scrollBarYMax * scrollBoxYMax, + -scrollBoxYMax, + 0 + ); + scrollBarY = constants.scrollBarMargin - + scrollBoxY / scrollBoxYMax * scrollBarYMax; + scrollHandler(scrollBarY, scrollBoxY); + d3.event.preventDefault(); + }); - scrollBar.call( - Drawing.setRect, - legendWidth, - scrollBarY, - constants.scrollBarWidth, - constants.scrollBarHeight - ); - clipPath.select('rect').attr({ - y: opts.borderwidth - scrollBoxY - }); - } + // to be safe, remove previous listeners + scrollBar.on(".drag", null); + scrollBox.on(".drag", null); + + var drag = d3.behavior.drag().on("drag", function() { + scrollBarY = Lib.constrain( + d3.event.y - constants.scrollBarHeight / 2, + constants.scrollBarMargin, + constants.scrollBarMargin + scrollBarYMax + ); + scrollBoxY = (-(scrollBarY - constants.scrollBarMargin)) / + scrollBarYMax * + scrollBoxYMax; + scrollHandler(scrollBarY, scrollBoxY); + }); - if(gd._context.editable) { - var xf, yf, x0, y0; - - legend.classed('cursor-move', true); - - dragElement.init({ - element: legend.node(), - prepFn: function() { - var transform = Drawing.getTranslate(legend); - - x0 = transform.x; - y0 = transform.y; - }, - moveFn: function(dx, dy) { - var newX = x0 + dx, - newY = y0 + dy; - - Drawing.setTranslate(legend, newX, newY); - - xf = dragElement.align(newX, 0, gs.l, gs.l + gs.w, opts.xanchor); - yf = dragElement.align(newY, 0, gs.t + gs.h, gs.t, opts.yanchor); - }, - doneFn: function(dragged) { - if(dragged && xf !== undefined && yf !== undefined) { - Plotly.relayout(gd, {'legend.x': xf, 'legend.y': yf}); - } - } - }); - } + scrollBar.call(drag); + scrollBox.call(drag); + } + + function scrollHandler(scrollBarY, scrollBoxY) { + scrollBox + .attr("data-scroll", scrollBoxY) + .call(Drawing.setTranslate, 0, scrollBoxY); + + scrollBar.call( + Drawing.setRect, + legendWidth, + scrollBarY, + constants.scrollBarWidth, + constants.scrollBarHeight + ); + clipPath.select("rect").attr({ y: opts.borderwidth - scrollBoxY }); + } + + if (gd._context.editable) { + var xf, yf, x0, y0; + + legend.classed("cursor-move", true); + + dragElement.init({ + element: legend.node(), + prepFn: function() { + var transform = Drawing.getTranslate(legend); + + x0 = transform.x; + y0 = transform.y; + }, + moveFn: function(dx, dy) { + var newX = x0 + dx, newY = y0 + dy; + + Drawing.setTranslate(legend, newX, newY); + + xf = dragElement.align(newX, 0, gs.l, gs.l + gs.w, opts.xanchor); + yf = dragElement.align(newY, 0, gs.t + gs.h, gs.t, opts.yanchor); + }, + doneFn: function(dragged) { + if (dragged && xf !== undefined && yf !== undefined) { + Plotly.relayout(gd, { "legend.x": xf, "legend.y": yf }); + } + } + }); + } }; function drawTexts(g, gd) { - var legendItem = g.data()[0][0], - fullLayout = gd._fullLayout, - trace = legendItem.trace, - isPie = Registry.traceIs(trace, 'pie'), - traceIndex = trace.index, - name = isPie ? legendItem.label : trace.name; - - var text = g.selectAll('text.legendtext') - .data([0]); - text.enter().append('text').classed('legendtext', true); - text.attr({ - x: 40, - y: 0, - 'data-unformatted': name - }) - .style('text-anchor', 'start') - .classed('user-select-none', true) + var legendItem = g.data()[0][0], + fullLayout = gd._fullLayout, + trace = legendItem.trace, + isPie = Registry.traceIs(trace, "pie"), + traceIndex = trace.index, + name = isPie ? legendItem.label : trace.name; + + var text = g.selectAll("text.legendtext").data([0]); + text.enter().append("text").classed("legendtext", true); + text + .attr({ x: 40, y: 0, "data-unformatted": name }) + .style("text-anchor", "start") + .classed("user-select-none", true) .call(Drawing.font, fullLayout.legend.font) .text(name); - function textLayout(s) { - svgTextUtils.convertToTspans(s, function() { - s.selectAll('tspan.line').attr({x: s.attr('x')}); - g.call(computeTextDimensions, gd); - }); - } + function textLayout(s) { + svgTextUtils.convertToTspans(s, function() { + s.selectAll("tspan.line").attr({ x: s.attr("x") }); + g.call(computeTextDimensions, gd); + }); + } - if(gd._context.editable && !isPie) { - text.call(svgTextUtils.makeEditable) - .call(textLayout) - .on('edit', function(text) { - this.attr({'data-unformatted': text}); + if (gd._context.editable && !isPie) { + text + .call(svgTextUtils.makeEditable) + .call(textLayout) + .on("edit", function(text) { + this.attr({ "data-unformatted": text }); - this.text(text) - .call(textLayout); + this.text(text).call(textLayout); - if(!this.text()) text = ' \u0020\u0020 '; + if (!this.text()) text = " "; - var fullInput = legendItem.trace._fullInput || {}, - astr; + var fullInput = legendItem.trace._fullInput || {}, astr; - // N.B. this block isn't super clean, - // is unfortunately untested at the moment, - // and only works for for 'ohlc' and 'candlestick', - // but should be generalized for other one-to-many transforms - if(['ohlc', 'candlestick'].indexOf(fullInput.type) !== -1) { - var transforms = legendItem.trace.transforms, - direction = transforms[transforms.length - 1].direction; + // N.B. this block isn't super clean, + // is unfortunately untested at the moment, + // and only works for for 'ohlc' and 'candlestick', + // but should be generalized for other one-to-many transforms + if (["ohlc", "candlestick"].indexOf(fullInput.type) !== -1) { + var transforms = legendItem.trace.transforms, + direction = transforms[transforms.length - 1].direction; - astr = direction + '.name'; - } - else astr = 'name'; + astr = direction + ".name"; + } else { + astr = "name"; + } - Plotly.restyle(gd, astr, text, traceIndex); - }); - } - else text.call(textLayout); + Plotly.restyle(gd, astr, text, traceIndex); + }); + } else { + text.call(textLayout); + } } function setupTraceToggle(g, gd) { - var hiddenSlices = gd._fullLayout.hiddenlabels ? - gd._fullLayout.hiddenlabels.slice() : - []; - - var traceToggle = g.selectAll('rect') - .data([0]); - - traceToggle.enter().append('rect') - .classed('legendtoggle', true) - .style('cursor', 'pointer') - .attr('pointer-events', 'all') - .call(Color.fill, 'rgba(0,0,0,0)'); - - traceToggle.on('click', function() { - if(gd._dragged) return; - - var legendItem = g.data()[0][0], - fullData = gd._fullData, - trace = legendItem.trace, - legendgroup = trace.legendgroup, - traceIndicesInGroup = [], - tracei, - newVisible; - - if(Registry.traceIs(trace, 'pie')) { - var thisLabel = legendItem.label, - thisLabelIndex = hiddenSlices.indexOf(thisLabel); - - if(thisLabelIndex === -1) hiddenSlices.push(thisLabel); - else hiddenSlices.splice(thisLabelIndex, 1); - - Plotly.relayout(gd, 'hiddenlabels', hiddenSlices); - } else { - if(legendgroup === '') { - traceIndicesInGroup = [trace.index]; - } else { - for(var i = 0; i < fullData.length; i++) { - tracei = fullData[i]; - if(tracei.legendgroup === legendgroup) { - traceIndicesInGroup.push(tracei.index); - } - } - } - - newVisible = trace.visible === true ? 'legendonly' : true; - Plotly.restyle(gd, 'visible', newVisible, traceIndicesInGroup); - } - }); -} + var hiddenSlices = gd._fullLayout.hiddenlabels + ? gd._fullLayout.hiddenlabels.slice() + : []; + + var traceToggle = g.selectAll("rect").data([0]); + + traceToggle + .enter() + .append("rect") + .classed("legendtoggle", true) + .style("cursor", "pointer") + .attr("pointer-events", "all") + .call(Color.fill, "rgba(0,0,0,0)"); + + traceToggle.on("click", function() { + if (gd._dragged) return; -function computeTextDimensions(g, gd) { var legendItem = g.data()[0][0], - mathjaxGroup = g.select('g[class*=math-group]'), - opts = gd._fullLayout.legend, - lineHeight = opts.font.size * 1.3, - height, - width; - - if(!legendItem.trace.showlegend) { - g.remove(); - return; - } + fullData = gd._fullData, + trace = legendItem.trace, + legendgroup = trace.legendgroup, + traceIndicesInGroup = [], + tracei, + newVisible; - if(mathjaxGroup.node()) { - var mathjaxBB = Drawing.bBox(mathjaxGroup.node()); + if (Registry.traceIs(trace, "pie")) { + var thisLabel = legendItem.label, + thisLabelIndex = hiddenSlices.indexOf(thisLabel); - height = mathjaxBB.height; - width = mathjaxBB.width; + if (thisLabelIndex === -1) hiddenSlices.push(thisLabel); + else hiddenSlices.splice(thisLabelIndex, 1); - Drawing.setTranslate(mathjaxGroup, 0, (height / 4)); - } - else { - var text = g.selectAll('.legendtext'), - textSpans = g.selectAll('.legendtext>tspan'), - textLines = textSpans[0].length || 1; - - height = lineHeight * textLines; - width = text.node() && Drawing.bBox(text.node()).width; - - // approximation to height offset to center the font - // to avoid getBoundingClientRect - var textY = lineHeight * (0.3 + (1 - textLines) / 2); - text.attr('y', textY); - textSpans.attr('y', textY); - } + Plotly.relayout(gd, "hiddenlabels", hiddenSlices); + } else { + if (legendgroup === "") { + traceIndicesInGroup = [trace.index]; + } else { + for (var i = 0; i < fullData.length; i++) { + tracei = fullData[i]; + if (tracei.legendgroup === legendgroup) { + traceIndicesInGroup.push(tracei.index); + } + } + } - height = Math.max(height, 16) + 3; + newVisible = trace.visible === true ? "legendonly" : true; + Plotly.restyle(gd, "visible", newVisible, traceIndicesInGroup); + } + }); +} - legendItem.height = height; - legendItem.width = width; +function computeTextDimensions(g, gd) { + var legendItem = g.data()[0][0], + mathjaxGroup = g.select("g[class*=math-group]"), + opts = gd._fullLayout.legend, + lineHeight = opts.font.size * 1.3, + height, + width; + + if (!legendItem.trace.showlegend) { + g.remove(); + return; + } + + if (mathjaxGroup.node()) { + var mathjaxBB = Drawing.bBox(mathjaxGroup.node()); + + height = mathjaxBB.height; + width = mathjaxBB.width; + + Drawing.setTranslate(mathjaxGroup, 0, height / 4); + } else { + var text = g.selectAll(".legendtext"), + textSpans = g.selectAll(".legendtext>tspan"), + textLines = textSpans[0].length || 1; + + height = lineHeight * textLines; + width = text.node() && Drawing.bBox(text.node()).width; + + // approximation to height offset to center the font + // to avoid getBoundingClientRect + var textY = lineHeight * (0.3 + (1 - textLines) / 2); + text.attr("y", textY); + textSpans.attr("y", textY); + } + + height = Math.max(height, 16) + 3; + + legendItem.height = height; + legendItem.width = width; } function computeLegendDimensions(gd, groups, traces) { - var fullLayout = gd._fullLayout, - opts = fullLayout.legend, - borderwidth = opts.borderwidth, - isGrouped = helpers.isGrouped(opts); - - if(helpers.isVertical(opts)) { - if(isGrouped) { - groups.each(function(d, i) { - Drawing.setTranslate(this, 0, i * opts.tracegroupgap); - }); - } - - opts.width = 0; - opts.height = 0; + var fullLayout = gd._fullLayout, + opts = fullLayout.legend, + borderwidth = opts.borderwidth, + isGrouped = helpers.isGrouped(opts); + + if (helpers.isVertical(opts)) { + if (isGrouped) { + groups.each(function(d, i) { + Drawing.setTranslate(this, 0, i * opts.tracegroupgap); + }); + } - traces.each(function(d) { - var legendItem = d[0], - textHeight = legendItem.height, - textWidth = legendItem.width; + opts.width = 0; + opts.height = 0; - Drawing.setTranslate(this, - borderwidth, - (5 + borderwidth + opts.height + textHeight / 2)); + traces.each(function(d) { + var legendItem = d[0], + textHeight = legendItem.height, + textWidth = legendItem.width; - opts.height += textHeight; - opts.width = Math.max(opts.width, textWidth); - }); + Drawing.setTranslate( + this, + borderwidth, + 5 + borderwidth + opts.height + textHeight / 2 + ); - opts.width += 45 + borderwidth * 2; - opts.height += 10 + borderwidth * 2; + opts.height += textHeight; + opts.width = Math.max(opts.width, textWidth); + }); - if(isGrouped) { - opts.height += (opts._lgroupsLength - 1) * opts.tracegroupgap; - } + opts.width += 45 + borderwidth * 2; + opts.height += 10 + borderwidth * 2; - // make sure we're only getting full pixels - opts.width = Math.ceil(opts.width); - opts.height = Math.ceil(opts.height); - - traces.each(function(d) { - var legendItem = d[0], - bg = d3.select(this).select('.legendtoggle'); - - bg.call(Drawing.setRect, - 0, - -legendItem.height / 2, - (gd._context.editable ? 0 : opts.width) + 40, - legendItem.height - ); - }); + if (isGrouped) { + opts.height += (opts._lgroupsLength - 1) * opts.tracegroupgap; } - else if(isGrouped) { - opts.width = 0; - opts.height = 0; - var groupXOffsets = [opts.width], - groupData = groups.data(); + // make sure we're only getting full pixels + opts.width = Math.ceil(opts.width); + opts.height = Math.ceil(opts.height); - for(var i = 0, n = groupData.length; i < n; i++) { - var textWidths = groupData[i].map(function(legendItemArray) { - return legendItemArray[0].width; - }); + traces.each(function(d) { + var legendItem = d[0], bg = d3.select(this).select(".legendtoggle"); - var groupWidth = 40 + Math.max.apply(null, textWidths); + bg.call( + Drawing.setRect, + 0, + (-legendItem.height) / 2, + (gd._context.editable ? 0 : opts.width) + 40, + legendItem.height + ); + }); + } else if (isGrouped) { + opts.width = 0; + opts.height = 0; - opts.width += opts.tracegroupgap + groupWidth; + var groupXOffsets = [opts.width], groupData = groups.data(); - groupXOffsets.push(opts.width); - } + for (var i = 0, n = groupData.length; i < n; i++) { + var textWidths = groupData[i].map(function(legendItemArray) { + return legendItemArray[0].width; + }); - groups.each(function(d, i) { - Drawing.setTranslate(this, groupXOffsets[i], 0); - }); + var groupWidth = 40 + Math.max.apply(null, textWidths); - groups.each(function() { - var group = d3.select(this), - groupTraces = group.selectAll('g.traces'), - groupHeight = 0; + opts.width += opts.tracegroupgap + groupWidth; - groupTraces.each(function(d) { - var legendItem = d[0], - textHeight = legendItem.height; + groupXOffsets.push(opts.width); + } - Drawing.setTranslate(this, - 0, - (5 + borderwidth + groupHeight + textHeight / 2)); + groups.each(function(d, i) { + Drawing.setTranslate(this, groupXOffsets[i], 0); + }); - groupHeight += textHeight; - }); + groups.each(function() { + var group = d3.select(this), + groupTraces = group.selectAll("g.traces"), + groupHeight = 0; - opts.height = Math.max(opts.height, groupHeight); - }); + groupTraces.each(function(d) { + var legendItem = d[0], textHeight = legendItem.height; - opts.height += 10 + borderwidth * 2; - opts.width += borderwidth * 2; + Drawing.setTranslate( + this, + 0, + 5 + borderwidth + groupHeight + textHeight / 2 + ); - // make sure we're only getting full pixels - opts.width = Math.ceil(opts.width); - opts.height = Math.ceil(opts.height); + groupHeight += textHeight; + }); - traces.each(function(d) { - var legendItem = d[0], - bg = d3.select(this).select('.legendtoggle'); + opts.height = Math.max(opts.height, groupHeight); + }); - bg.call(Drawing.setRect, - 0, - -legendItem.height / 2, - (gd._context.editable ? 0 : opts.width), - legendItem.height - ); - }); - } - else { - opts.width = 0; - opts.height = 0; - var rowHeight = 0, - maxTraceHeight = 0, - maxTraceWidth = 0, - offsetX = 0; - - // calculate largest width for traces and use for width of all legend items - traces.each(function(d) { - maxTraceWidth = Math.max(40 + d[0].width, maxTraceWidth); - }); - - traces.each(function(d) { - var legendItem = d[0], - traceWidth = maxTraceWidth, - traceGap = opts.tracegroupgap || 5; - - if((borderwidth + offsetX + traceGap + traceWidth) > (fullLayout.width - (fullLayout.margin.r + fullLayout.margin.l))) { - offsetX = 0; - rowHeight = rowHeight + maxTraceHeight; - opts.height = opts.height + maxTraceHeight; - // reset for next row - maxTraceHeight = 0; - } - - Drawing.setTranslate(this, - (borderwidth + offsetX), - (5 + borderwidth + legendItem.height / 2) + rowHeight); - - opts.width += traceGap + traceWidth; - opts.height = Math.max(opts.height, legendItem.height); - - // keep track of tallest trace in group - offsetX += traceGap + traceWidth; - maxTraceHeight = Math.max(legendItem.height, maxTraceHeight); - }); - - opts.width += borderwidth * 2; - opts.height += 10 + borderwidth * 2; - - // make sure we're only getting full pixels - opts.width = Math.ceil(opts.width); - opts.height = Math.ceil(opts.height); - - traces.each(function(d) { - var legendItem = d[0], - bg = d3.select(this).select('.legendtoggle'); - - bg.call(Drawing.setRect, - 0, - -legendItem.height / 2, - (gd._context.editable ? 0 : opts.width), - legendItem.height - ); - }); - } -} + opts.height += 10 + borderwidth * 2; + opts.width += borderwidth * 2; -function expandMargin(gd) { - var fullLayout = gd._fullLayout, - opts = fullLayout.legend; + // make sure we're only getting full pixels + opts.width = Math.ceil(opts.width); + opts.height = Math.ceil(opts.height); - var xanchor = 'left'; - if(anchorUtils.isRightAnchor(opts)) { - xanchor = 'right'; - } - else if(anchorUtils.isCenterAnchor(opts)) { - xanchor = 'center'; - } + traces.each(function(d) { + var legendItem = d[0], bg = d3.select(this).select(".legendtoggle"); - var yanchor = 'top'; - if(anchorUtils.isBottomAnchor(opts)) { - yanchor = 'bottom'; - } - else if(anchorUtils.isMiddleAnchor(opts)) { - yanchor = 'middle'; - } + bg.call( + Drawing.setRect, + 0, + (-legendItem.height) / 2, + gd._context.editable ? 0 : opts.width, + legendItem.height + ); + }); + } else { + opts.width = 0; + opts.height = 0; + var rowHeight = 0, maxTraceHeight = 0, maxTraceWidth = 0, offsetX = 0; + + // calculate largest width for traces and use for width of all legend items + traces.each(function(d) { + maxTraceWidth = Math.max(40 + d[0].width, maxTraceWidth); + }); - // lastly check if the margin auto-expand has changed - Plots.autoMargin(gd, 'legend', { - x: opts.x, - y: opts.y, - l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0), - r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0), - b: opts.height * ({top: 1, middle: 0.5}[yanchor] || 0), - t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) + traces.each(function(d) { + var legendItem = d[0], + traceWidth = maxTraceWidth, + traceGap = opts.tracegroupgap || 5; + + if ( + borderwidth + offsetX + traceGap + traceWidth > + fullLayout.width - (fullLayout.margin.r + fullLayout.margin.l) + ) { + offsetX = 0; + rowHeight = rowHeight + maxTraceHeight; + opts.height = opts.height + maxTraceHeight; + // reset for next row + maxTraceHeight = 0; + } + + Drawing.setTranslate( + this, + borderwidth + offsetX, + 5 + borderwidth + legendItem.height / 2 + rowHeight + ); + + opts.width += traceGap + traceWidth; + opts.height = Math.max(opts.height, legendItem.height); + + // keep track of tallest trace in group + offsetX += traceGap + traceWidth; + maxTraceHeight = Math.max(legendItem.height, maxTraceHeight); }); -} -function expandHorizontalMargin(gd) { - var fullLayout = gd._fullLayout, - opts = fullLayout.legend; + opts.width += borderwidth * 2; + opts.height += 10 + borderwidth * 2; - var xanchor = 'left'; - if(anchorUtils.isRightAnchor(opts)) { - xanchor = 'right'; - } - else if(anchorUtils.isCenterAnchor(opts)) { - xanchor = 'center'; - } + // make sure we're only getting full pixels + opts.width = Math.ceil(opts.width); + opts.height = Math.ceil(opts.height); + + traces.each(function(d) { + var legendItem = d[0], bg = d3.select(this).select(".legendtoggle"); - // lastly check if the margin auto-expand has changed - Plots.autoMargin(gd, 'legend', { - x: opts.x, - y: 0.5, - l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0), - r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0), - b: 0, - t: 0 + bg.call( + Drawing.setRect, + 0, + (-legendItem.height) / 2, + gd._context.editable ? 0 : opts.width, + legendItem.height + ); }); + } +} + +function expandMargin(gd) { + var fullLayout = gd._fullLayout, opts = fullLayout.legend; + + var xanchor = "left"; + if (anchorUtils.isRightAnchor(opts)) { + xanchor = "right"; + } else if (anchorUtils.isCenterAnchor(opts)) { + xanchor = "center"; + } + + var yanchor = "top"; + if (anchorUtils.isBottomAnchor(opts)) { + yanchor = "bottom"; + } else if (anchorUtils.isMiddleAnchor(opts)) { + yanchor = "middle"; + } + + // lastly check if the margin auto-expand has changed + Plots.autoMargin(gd, "legend", { + x: opts.x, + y: opts.y, + l: opts.width * (({ right: 1, center: 0.5 })[xanchor] || 0), + r: opts.width * (({ left: 1, center: 0.5 })[xanchor] || 0), + b: opts.height * (({ top: 1, middle: 0.5 })[yanchor] || 0), + t: opts.height * (({ bottom: 1, middle: 0.5 })[yanchor] || 0) + }); +} + +function expandHorizontalMargin(gd) { + var fullLayout = gd._fullLayout, opts = fullLayout.legend; + + var xanchor = "left"; + if (anchorUtils.isRightAnchor(opts)) { + xanchor = "right"; + } else if (anchorUtils.isCenterAnchor(opts)) { + xanchor = "center"; + } + + // lastly check if the margin auto-expand has changed + Plots.autoMargin(gd, "legend", { + x: opts.x, + y: 0.5, + l: opts.width * (({ right: 1, center: 0.5 })[xanchor] || 0), + r: opts.width * (({ left: 1, center: 0.5 })[xanchor] || 0), + b: 0, + t: 0 + }); } diff --git a/src/components/legend/get_legend_data.js b/src/components/legend/get_legend_data.js index 4ad4636d442..2522570ee03 100644 --- a/src/components/legend/get_legend_data.js +++ b/src/components/legend/get_legend_data.js @@ -5,99 +5,95 @@ * 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 Registry = require('../../registry'); -var helpers = require('./helpers'); - +"use strict"; +var Registry = require("../../registry"); +var helpers = require("./helpers"); module.exports = function getLegendData(calcdata, opts) { - var lgroupToTraces = {}, - lgroups = [], - hasOneNonBlankGroup = false, - slicesShown = {}, - lgroupi = 0; - - var i, j; - - function addOneItem(legendGroup, legendItem) { - // each '' legend group is treated as a separate group - if(legendGroup === '' || !helpers.isGrouped(opts)) { - var uniqueGroup = '~~i' + lgroupi; // TODO: check this against fullData legendgroups? - - lgroups.push(uniqueGroup); - lgroupToTraces[uniqueGroup] = [[legendItem]]; - lgroupi++; - } - else if(lgroups.indexOf(legendGroup) === -1) { - lgroups.push(legendGroup); - hasOneNonBlankGroup = true; - lgroupToTraces[legendGroup] = [[legendItem]]; - } - else lgroupToTraces[legendGroup].push([legendItem]); + var lgroupToTraces = {}, + lgroups = [], + hasOneNonBlankGroup = false, + slicesShown = {}, + lgroupi = 0; + + var i, j; + + function addOneItem(legendGroup, legendItem) { + // each '' legend group is treated as a separate group + if (legendGroup === "" || !helpers.isGrouped(opts)) { + var uniqueGroup = "~~i" + lgroupi; + + // TODO: check this against fullData legendgroups? + lgroups.push(uniqueGroup); + lgroupToTraces[uniqueGroup] = [[legendItem]]; + lgroupi++; + } else if (lgroups.indexOf(legendGroup) === -1) { + lgroups.push(legendGroup); + hasOneNonBlankGroup = true; + lgroupToTraces[legendGroup] = [[legendItem]]; + } else { + lgroupToTraces[legendGroup].push([legendItem]); } + } - // build an { legendgroup: [cd0, cd0], ... } object - for(i = 0; i < calcdata.length; i++) { - var cd = calcdata[i], - cd0 = cd[0], - trace = cd0.trace, - lgroup = trace.legendgroup; + // build an { legendgroup: [cd0, cd0], ... } object + for (i = 0; i < calcdata.length; i++) { + var cd = calcdata[i], + cd0 = cd[0], + trace = cd0.trace, + lgroup = trace.legendgroup; - if(!helpers.legendGetsTrace(trace) || !trace.showlegend) continue; + if (!helpers.legendGetsTrace(trace) || !trace.showlegend) continue; - if(Registry.traceIs(trace, 'pie')) { - if(!slicesShown[lgroup]) slicesShown[lgroup] = {}; + if (Registry.traceIs(trace, "pie")) { + if (!slicesShown[lgroup]) slicesShown[lgroup] = {}; - for(j = 0; j < cd.length; j++) { - var labelj = cd[j].label; + for (j = 0; j < cd.length; j++) { + var labelj = cd[j].label; - if(!slicesShown[lgroup][labelj]) { - addOneItem(lgroup, { - label: labelj, - color: cd[j].color, - i: cd[j].i, - trace: trace - }); + if (!slicesShown[lgroup][labelj]) { + addOneItem(lgroup, { + label: labelj, + color: cd[j].color, + i: cd[j].i, + trace: trace + }); - slicesShown[lgroup][labelj] = true; - } - } + slicesShown[lgroup][labelj] = true; } - - else addOneItem(lgroup, cd0); + } + } else { + addOneItem(lgroup, cd0); } + } - // won't draw a legend in this case - if(!lgroups.length) return []; + // won't draw a legend in this case + if (!lgroups.length) return []; - // rearrange lgroupToTraces into a d3-friendly array of arrays - var lgroupsLength = lgroups.length, - ltraces, - legendData; + // rearrange lgroupToTraces into a d3-friendly array of arrays + var lgroupsLength = lgroups.length, ltraces, legendData; - if(hasOneNonBlankGroup && helpers.isGrouped(opts)) { - legendData = new Array(lgroupsLength); + if (hasOneNonBlankGroup && helpers.isGrouped(opts)) { + legendData = new Array(lgroupsLength); - for(i = 0; i < lgroupsLength; i++) { - ltraces = lgroupToTraces[lgroups[i]]; - legendData[i] = helpers.isReversed(opts) ? ltraces.reverse() : ltraces; - } + for (i = 0; i < lgroupsLength; i++) { + ltraces = lgroupToTraces[lgroups[i]]; + legendData[i] = helpers.isReversed(opts) ? ltraces.reverse() : ltraces; } - else { - // collapse all groups into one if all groups are blank - legendData = [new Array(lgroupsLength)]; - - for(i = 0; i < lgroupsLength; i++) { - ltraces = lgroupToTraces[lgroups[i]][0]; - legendData[0][helpers.isReversed(opts) ? lgroupsLength - i - 1 : i] = ltraces; - } - lgroupsLength = 1; + } else { + // collapse all groups into one if all groups are blank + legendData = [new Array(lgroupsLength)]; + + for (i = 0; i < lgroupsLength; i++) { + ltraces = lgroupToTraces[lgroups[i]][0]; + legendData[0][ + helpers.isReversed(opts) ? lgroupsLength - i - 1 : i + ] = ltraces; } + lgroupsLength = 1; + } - // needed in repositionLegend - opts._lgroupsLength = lgroupsLength; - return legendData; + // needed in repositionLegend + opts._lgroupsLength = lgroupsLength; + return legendData; }; diff --git a/src/components/legend/helpers.js b/src/components/legend/helpers.js index 140fa6d7f5f..4117b548124 100644 --- a/src/components/legend/helpers.js +++ b/src/components/legend/helpers.js @@ -5,25 +5,21 @@ * 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 Registry = require('../../registry'); - +"use strict"; +var Registry = require("../../registry"); exports.legendGetsTrace = function legendGetsTrace(trace) { - return trace.visible && Registry.traceIs(trace, 'showLegend'); + return trace.visible && Registry.traceIs(trace, "showLegend"); }; exports.isGrouped = function isGrouped(legendLayout) { - return (legendLayout.traceorder || '').indexOf('grouped') !== -1; + return (legendLayout.traceorder || "").indexOf("grouped") !== -1; }; exports.isVertical = function isVertical(legendLayout) { - return legendLayout.orientation !== 'h'; + return legendLayout.orientation !== "h"; }; exports.isReversed = function isReversed(legendLayout) { - return (legendLayout.traceorder || '').indexOf('reversed') !== -1; + return (legendLayout.traceorder || "").indexOf("reversed") !== -1; }; diff --git a/src/components/legend/index.js b/src/components/legend/index.js index 71e45a0b723..600db0132b0 100644 --- a/src/components/legend/index.js +++ b/src/components/legend/index.js @@ -5,18 +5,12 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; module.exports = { - moduleType: 'component', - name: 'legend', - - layoutAttributes: require('./attributes'), - supplyLayoutDefaults: require('./defaults'), - - draw: require('./draw'), - style: require('./style') + moduleType: "component", + name: "legend", + layoutAttributes: require("./attributes"), + supplyLayoutDefaults: require("./defaults"), + draw: require("./draw"), + style: require("./style") }; diff --git a/src/components/legend/style.js b/src/components/legend/style.js index c3c2101e62e..1ebc5b9e271 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -5,53 +5,41 @@ * 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 d3 = require("d3"); +var Registry = require("../../registry"); +var Lib = require("../../lib"); +var Drawing = require("../drawing"); +var Color = require("../color"); -'use strict'; +var subTypes = require("../../traces/scatter/subtypes"); +var stylePie = require("../../traces/pie/style_one"); -var d3 = require('d3'); +module.exports = function style(s) { + s + .each(function(d) { + var traceGroup = d3.select(this); -var Registry = require('../../registry'); -var Lib = require('../../lib'); -var Drawing = require('../drawing'); -var Color = require('../color'); + var layers = traceGroup.selectAll("g.layers").data([0]); + layers.enter().append("g").classed("layers", true); + layers.style("opacity", d[0].trace.opacity); -var subTypes = require('../../traces/scatter/subtypes'); -var stylePie = require('../../traces/pie/style_one'); + var fill = layers.selectAll("g.legendfill").data([d]); + fill.enter().append("g").classed("legendfill", true); + var line = layers.selectAll("g.legendlines").data([d]); + line.enter().append("g").classed("legendlines", true); -module.exports = function style(s) { - s.each(function(d) { - var traceGroup = d3.select(this); - - var layers = traceGroup.selectAll('g.layers') - .data([0]); - layers.enter().append('g') - .classed('layers', true); - layers.style('opacity', d[0].trace.opacity); - - var fill = layers - .selectAll('g.legendfill') - .data([d]); - fill.enter().append('g') - .classed('legendfill', true); - - var line = layers - .selectAll('g.legendlines') - .data([d]); - line.enter().append('g') - .classed('legendlines', true); - - var symbol = layers - .selectAll('g.legendsymbols') - .data([d]); - symbol.enter().append('g') - .classed('legendsymbols', true); - - symbol.selectAll('g.legendpoints') - .data([d]) - .enter().append('g') - .classed('legendpoints', true); + var symbol = layers.selectAll("g.legendsymbols").data([d]); + symbol.enter().append("g").classed("legendsymbols", true); + + symbol + .selectAll("g.legendpoints") + .data([d]) + .enter() + .append("g") + .classed("legendpoints", true); }) .each(styleBars) .each(styleBoxes) @@ -61,166 +49,180 @@ module.exports = function style(s) { }; function styleLines(d) { - var trace = d[0].trace, - showFill = trace.visible && trace.fill && trace.fill !== 'none', - showLine = subTypes.hasLines(trace); - - var fill = d3.select(this).select('.legendfill').selectAll('path') - .data(showFill ? [d] : []); - fill.enter().append('path').classed('js-fill', true); - fill.exit().remove(); - fill.attr('d', 'M5,0h30v6h-30z') - .call(Drawing.fillGroupStyle); - - var line = d3.select(this).select('.legendlines').selectAll('path') - .data(showLine ? [d] : []); - line.enter().append('path').classed('js-line', true) - .attr('d', 'M5,0h30'); - line.exit().remove(); - line.call(Drawing.lineGroupStyle); + var trace = d[0].trace, + showFill = trace.visible && trace.fill && trace.fill !== "none", + showLine = subTypes.hasLines(trace); + + var fill = d3 + .select(this) + .select(".legendfill") + .selectAll("path") + .data(showFill ? [d] : []); + fill.enter().append("path").classed("js-fill", true); + fill.exit().remove(); + fill.attr("d", "M5,0h30v6h-30z").call(Drawing.fillGroupStyle); + + var line = d3 + .select(this) + .select(".legendlines") + .selectAll("path") + .data(showLine ? [d] : []); + line.enter().append("path").classed("js-line", true).attr("d", "M5,0h30"); + line.exit().remove(); + line.call(Drawing.lineGroupStyle); } function stylePoints(d) { - var d0 = d[0], - trace = d0.trace, - showMarkers = subTypes.hasMarkers(trace), - showText = subTypes.hasText(trace), - showLines = subTypes.hasLines(trace); - - var dMod, tMod; - - // 'scatter3d' and 'scattergeo' don't use gd.calcdata yet; - // use d0.trace to infer arrayOk attributes - - function boundVal(attrIn, arrayToValFn, bounds) { - var valIn = Lib.nestedProperty(trace, attrIn).get(), - valToBound = (Array.isArray(valIn) && arrayToValFn) ? - arrayToValFn(valIn) : valIn; - - if(bounds) { - if(valToBound < bounds[0]) return bounds[0]; - else if(valToBound > bounds[1]) return bounds[1]; - } - return valToBound; + var d0 = d[0], + trace = d0.trace, + showMarkers = subTypes.hasMarkers(trace), + showText = subTypes.hasText(trace), + showLines = subTypes.hasLines(trace); + + var dMod, tMod; + + // 'scatter3d' and 'scattergeo' don't use gd.calcdata yet; + // use d0.trace to infer arrayOk attributes + function boundVal(attrIn, arrayToValFn, bounds) { + var valIn = Lib.nestedProperty(trace, attrIn).get(), + valToBound = Array.isArray(valIn) && arrayToValFn + ? arrayToValFn(valIn) + : valIn; + + if (bounds) { + if (valToBound < bounds[0]) return bounds[0]; + else if (valToBound > bounds[1]) return bounds[1]; + } + return valToBound; + } + + function pickFirst(array) { + return array[0]; + } + + // constrain text, markers, etc so they'll fit on the legend + if (showMarkers || showText || showLines) { + var dEdit = {}, tEdit = {}; + + if (showMarkers) { + dEdit.mc = boundVal("marker.color", pickFirst); + dEdit.mo = boundVal("marker.opacity", Lib.mean, [0.2, 1]); + dEdit.ms = boundVal("marker.size", Lib.mean, [2, 16]); + dEdit.mlc = boundVal("marker.line.color", pickFirst); + dEdit.mlw = boundVal("marker.line.width", Lib.mean, [0, 5]); + tEdit.marker = { sizeref: 1, sizemin: 1, sizemode: "diameter" }; } - function pickFirst(array) { return array[0]; } - - // constrain text, markers, etc so they'll fit on the legend - if(showMarkers || showText || showLines) { - var dEdit = {}, - tEdit = {}; - - if(showMarkers) { - dEdit.mc = boundVal('marker.color', pickFirst); - dEdit.mo = boundVal('marker.opacity', Lib.mean, [0.2, 1]); - dEdit.ms = boundVal('marker.size', Lib.mean, [2, 16]); - dEdit.mlc = boundVal('marker.line.color', pickFirst); - dEdit.mlw = boundVal('marker.line.width', Lib.mean, [0, 5]); - tEdit.marker = { - sizeref: 1, - sizemin: 1, - sizemode: 'diameter' - }; - } - - if(showLines) { - tEdit.line = { - width: boundVal('line.width', pickFirst, [0, 10]) - }; - } - - if(showText) { - dEdit.tx = 'Aa'; - dEdit.tp = boundVal('textposition', pickFirst); - dEdit.ts = 10; - dEdit.tc = boundVal('textfont.color', pickFirst); - dEdit.tf = boundVal('textfont.family', pickFirst); - } - - dMod = [Lib.minExtend(d0, dEdit)]; - tMod = Lib.minExtend(trace, tEdit); + if (showLines) { + tEdit.line = { width: boundVal("line.width", pickFirst, [0, 10]) }; } - var ptgroup = d3.select(this).select('g.legendpoints'); - - var pts = ptgroup.selectAll('path.scatterpts') - .data(showMarkers ? dMod : []); - pts.enter().append('path').classed('scatterpts', true) - .attr('transform', 'translate(20,0)'); - pts.exit().remove(); - pts.call(Drawing.pointStyle, tMod); - - // 'mrc' is set in pointStyle and used in textPointStyle: - // constrain it here - if(showMarkers) dMod[0].mrc = 3; - - var txt = ptgroup.selectAll('g.pointtext') - .data(showText ? dMod : []); - txt.enter() - .append('g').classed('pointtext', true) - .append('text').attr('transform', 'translate(20,0)'); - txt.exit().remove(); - txt.selectAll('text').call(Drawing.textPointStyle, tMod); + if (showText) { + dEdit.tx = "Aa"; + dEdit.tp = boundVal("textposition", pickFirst); + dEdit.ts = 10; + dEdit.tc = boundVal("textfont.color", pickFirst); + dEdit.tf = boundVal("textfont.family", pickFirst); + } + + dMod = [Lib.minExtend(d0, dEdit)]; + tMod = Lib.minExtend(trace, tEdit); + } + + var ptgroup = d3.select(this).select("g.legendpoints"); + + var pts = ptgroup.selectAll("path.scatterpts").data(showMarkers ? dMod : []); + pts + .enter() + .append("path") + .classed("scatterpts", true) + .attr("transform", "translate(20,0)"); + pts.exit().remove(); + pts.call(Drawing.pointStyle, tMod); + + // 'mrc' is set in pointStyle and used in textPointStyle: + // constrain it here + if (showMarkers) dMod[0].mrc = 3; + + var txt = ptgroup.selectAll("g.pointtext").data(showText ? dMod : []); + txt + .enter() + .append("g") + .classed("pointtext", true) + .append("text") + .attr("transform", "translate(20,0)"); + txt.exit().remove(); + txt.selectAll("text").call(Drawing.textPointStyle, tMod); } function styleBars(d) { - var trace = d[0].trace, - marker = trace.marker || {}, - markerLine = marker.line || {}, - barpath = d3.select(this).select('g.legendpoints') - .selectAll('path.legendbar') - .data(Registry.traceIs(trace, 'bar') ? [d] : []); - barpath.enter().append('path').classed('legendbar', true) - .attr('d', 'M6,6H-6V-6H6Z') - .attr('transform', 'translate(20,0)'); - barpath.exit().remove(); - barpath.each(function(d) { - var p = d3.select(this), - d0 = d[0], - w = (d0.mlw + 1 || markerLine.width + 1) - 1; - - p.style('stroke-width', w + 'px') - .call(Color.fill, d0.mc || marker.color); - - if(w) { - p.call(Color.stroke, d0.mlc || markerLine.color); - } - }); + var trace = d[0].trace, + marker = trace.marker || {}, + markerLine = marker.line || {}, + barpath = d3 + .select(this) + .select("g.legendpoints") + .selectAll("path.legendbar") + .data(Registry.traceIs(trace, "bar") ? [d] : []); + barpath + .enter() + .append("path") + .classed("legendbar", true) + .attr("d", "M6,6H-6V-6H6Z") + .attr("transform", "translate(20,0)"); + barpath.exit().remove(); + barpath.each(function(d) { + var p = d3.select(this), + d0 = d[0], + w = (d0.mlw + 1 || markerLine.width + 1) - 1; + + p.style("stroke-width", w + "px").call(Color.fill, d0.mc || marker.color); + + if (w) { + p.call(Color.stroke, d0.mlc || markerLine.color); + } + }); } function styleBoxes(d) { - var trace = d[0].trace, - pts = d3.select(this).select('g.legendpoints') - .selectAll('path.legendbox') - .data(Registry.traceIs(trace, 'box') && trace.visible ? [d] : []); - pts.enter().append('path').classed('legendbox', true) - // if we want the median bar, prepend M6,0H-6 - .attr('d', 'M6,6H-6V-6H6Z') - .attr('transform', 'translate(20,0)'); - pts.exit().remove(); - pts.each(function() { - var w = trace.line.width, - p = d3.select(this); - - p.style('stroke-width', w + 'px') - .call(Color.fill, trace.fillcolor); - - if(w) { - p.call(Color.stroke, trace.line.color); - } - }); + var trace = d[0].trace, + pts = d3 + .select(this) + .select("g.legendpoints") + .selectAll("path.legendbox") + .data(Registry.traceIs(trace, "box") && trace.visible ? [d] : []); + pts + .enter() + .append("path") + .classed("legendbox", true) + .attr("d", "M6,6H-6V-6H6Z") + .attr("transform", "translate(20,0)"); + pts.exit().remove(); + pts.each(function() { + var w = trace.line.width, p = d3.select(this); + + p.style("stroke-width", w + "px").call(Color.fill, trace.fillcolor); + + if (w) { + p.call(Color.stroke, trace.line.color); + } + }); } function stylePies(d) { - var trace = d[0].trace, - pts = d3.select(this).select('g.legendpoints') - .selectAll('path.legendpie') - .data(Registry.traceIs(trace, 'pie') && trace.visible ? [d] : []); - pts.enter().append('path').classed('legendpie', true) - .attr('d', 'M6,6H-6V-6H6Z') - .attr('transform', 'translate(20,0)'); - pts.exit().remove(); - - if(pts.size()) pts.call(stylePie, d[0], trace); + var trace = d[0].trace, + pts = d3 + .select(this) + .select("g.legendpoints") + .selectAll("path.legendpie") + .data(Registry.traceIs(trace, "pie") && trace.visible ? [d] : []); + pts + .enter() + .append("path") + .classed("legendpie", true) + .attr("d", "M6,6H-6V-6H6Z") + .attr("transform", "translate(20,0)"); + pts.exit().remove(); + + if (pts.size()) pts.call(stylePie, d[0], trace); } diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index aae0503e368..694e4b98b56 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -5,17 +5,13 @@ * 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 Plotly = require('../../plotly'); -var Plots = require('../../plots/plots'); -var Axes = require('../../plots/cartesian/axes'); -var Lib = require('../../lib'); -var downloadImage = require('../../snapshot/download'); -var Icons = require('../../../build/ploticon'); - +"use strict"; +var Plotly = require("../../plotly"); +var Plots = require("../../plots/plots"); +var Axes = require("../../plots/cartesian/axes"); +var Lib = require("../../lib"); +var downloadImage = require("../../snapshot/download"); +var Icons = require("../../../build/ploticon"); var modeBarButtons = module.exports = {}; @@ -44,477 +40,466 @@ var modeBarButtons = module.exports = {}; * @param {boolean} [toggle] * is the button a toggle button? */ - modeBarButtons.toImage = { - name: 'toImage', - title: 'Download plot as a png', - icon: Icons.camera, - click: function(gd) { - var format = 'png'; - - Lib.notifier('Taking snapshot - this may take a few seconds', 'long'); + name: "toImage", + title: "Download plot as a png", + icon: Icons.camera, + click: function(gd) { + var format = "png"; - if(Lib.isIE()) { - Lib.notifier('IE only supports svg. Changing format to svg.', 'long'); - format = 'svg'; - } + Lib.notifier("Taking snapshot - this may take a few seconds", "long"); - downloadImage(gd, {'format': format}) - .then(function(filename) { - Lib.notifier('Snapshot succeeded - ' + filename, 'long'); - }) - .catch(function() { - Lib.notifier('Sorry there was a problem downloading your snapshot!', 'long'); - }); + if (Lib.isIE()) { + Lib.notifier("IE only supports svg. Changing format to svg.", "long"); + format = "svg"; } + + downloadImage(gd, { format: format }) + .then(function(filename) { + Lib.notifier("Snapshot succeeded - " + filename, "long"); + }) + .catch(function() { + Lib.notifier( + "Sorry there was a problem downloading your snapshot!", + "long" + ); + }); + } }; modeBarButtons.sendDataToCloud = { - name: 'sendDataToCloud', - title: 'Save and edit plot in cloud', - icon: Icons.disk, - click: function(gd) { - Plots.sendDataToCloud(gd); - } + name: "sendDataToCloud", + title: "Save and edit plot in cloud", + icon: Icons.disk, + click: function(gd) { + Plots.sendDataToCloud(gd); + } }; modeBarButtons.zoom2d = { - name: 'zoom2d', - title: 'Zoom', - attr: 'dragmode', - val: 'zoom', - icon: Icons.zoombox, - click: handleCartesian + name: "zoom2d", + title: "Zoom", + attr: "dragmode", + val: "zoom", + icon: Icons.zoombox, + click: handleCartesian }; modeBarButtons.pan2d = { - name: 'pan2d', - title: 'Pan', - attr: 'dragmode', - val: 'pan', - icon: Icons.pan, - click: handleCartesian + name: "pan2d", + title: "Pan", + attr: "dragmode", + val: "pan", + icon: Icons.pan, + click: handleCartesian }; modeBarButtons.select2d = { - name: 'select2d', - title: 'Box Select', - attr: 'dragmode', - val: 'select', - icon: Icons.selectbox, - click: handleCartesian + name: "select2d", + title: "Box Select", + attr: "dragmode", + val: "select", + icon: Icons.selectbox, + click: handleCartesian }; modeBarButtons.lasso2d = { - name: 'lasso2d', - title: 'Lasso Select', - attr: 'dragmode', - val: 'lasso', - icon: Icons.lasso, - click: handleCartesian + name: "lasso2d", + title: "Lasso Select", + attr: "dragmode", + val: "lasso", + icon: Icons.lasso, + click: handleCartesian }; modeBarButtons.zoomIn2d = { - name: 'zoomIn2d', - title: 'Zoom in', - attr: 'zoom', - val: 'in', - icon: Icons.zoom_plus, - click: handleCartesian + name: "zoomIn2d", + title: "Zoom in", + attr: "zoom", + val: "in", + icon: Icons.zoom_plus, + click: handleCartesian }; modeBarButtons.zoomOut2d = { - name: 'zoomOut2d', - title: 'Zoom out', - attr: 'zoom', - val: 'out', - icon: Icons.zoom_minus, - click: handleCartesian + name: "zoomOut2d", + title: "Zoom out", + attr: "zoom", + val: "out", + icon: Icons.zoom_minus, + click: handleCartesian }; modeBarButtons.autoScale2d = { - name: 'autoScale2d', - title: 'Autoscale', - attr: 'zoom', - val: 'auto', - icon: Icons.autoscale, - click: handleCartesian + name: "autoScale2d", + title: "Autoscale", + attr: "zoom", + val: "auto", + icon: Icons.autoscale, + click: handleCartesian }; modeBarButtons.resetScale2d = { - name: 'resetScale2d', - title: 'Reset axes', - attr: 'zoom', - val: 'reset', - icon: Icons.home, - click: handleCartesian + name: "resetScale2d", + title: "Reset axes", + attr: "zoom", + val: "reset", + icon: Icons.home, + click: handleCartesian }; modeBarButtons.hoverClosestCartesian = { - name: 'hoverClosestCartesian', - title: 'Show closest data on hover', - attr: 'hovermode', - val: 'closest', - icon: Icons.tooltip_basic, - gravity: 'ne', - click: handleCartesian + name: "hoverClosestCartesian", + title: "Show closest data on hover", + attr: "hovermode", + val: "closest", + icon: Icons.tooltip_basic, + gravity: "ne", + click: handleCartesian }; modeBarButtons.hoverCompareCartesian = { - name: 'hoverCompareCartesian', - title: 'Compare data on hover', - attr: 'hovermode', - val: function(gd) { - return gd._fullLayout._isHoriz ? 'y' : 'x'; - }, - icon: Icons.tooltip_compare, - gravity: 'ne', - click: handleCartesian + name: "hoverCompareCartesian", + title: "Compare data on hover", + attr: "hovermode", + val: function(gd) { + return gd._fullLayout._isHoriz ? "y" : "x"; + }, + icon: Icons.tooltip_compare, + gravity: "ne", + click: handleCartesian }; function handleCartesian(gd, ev) { - var button = ev.currentTarget, - astr = button.getAttribute('data-attr'), - val = button.getAttribute('data-val') || true, - fullLayout = gd._fullLayout, - aobj = {}; - - if(astr === 'zoom') { - var mag = (val === 'in') ? 0.5 : 2, - r0 = (1 + mag) / 2, - r1 = (1 - mag) / 2, - axList = Axes.list(gd, null, true); - - var ax, axName; - - for(var i = 0; i < axList.length; i++) { - ax = axList[i]; - - if(!ax.fixedrange) { - axName = ax._name; - if(val === 'auto') aobj[axName + '.autorange'] = true; - else if(val === 'reset') { - if(ax._rangeInitial === undefined) { - aobj[axName + '.autorange'] = true; - } - else { - var rangeInitial = ax._rangeInitial.slice(); - aobj[axName + '.range[0]'] = rangeInitial[0]; - aobj[axName + '.range[1]'] = rangeInitial[1]; - } - } - else { - var rangeNow = [ - ax.r2l(ax.range[0]), - ax.r2l(ax.range[1]), - ]; - - var rangeNew = [ - r0 * rangeNow[0] + r1 * rangeNow[1], - r0 * rangeNow[1] + r1 * rangeNow[0] - ]; - - aobj[axName + '.range[0]'] = ax.l2r(rangeNew[0]); - aobj[axName + '.range[1]'] = ax.l2r(rangeNew[1]); - } - } + var button = ev.currentTarget, + astr = button.getAttribute("data-attr"), + val = button.getAttribute("data-val") || true, + fullLayout = gd._fullLayout, + aobj = {}; + + if (astr === "zoom") { + var mag = val === "in" ? 0.5 : 2, + r0 = (1 + mag) / 2, + r1 = (1 - mag) / 2, + axList = Axes.list(gd, null, true); + + var ax, axName; + + for (var i = 0; i < axList.length; i++) { + ax = axList[i]; + + if (!ax.fixedrange) { + axName = ax._name; + if (val === "auto") { + aobj[axName + ".autorange"] = true; + } else if (val === "reset") { + if (ax._rangeInitial === undefined) { + aobj[axName + ".autorange"] = true; + } else { + var rangeInitial = ax._rangeInitial.slice(); + aobj[axName + ".range[0]"] = rangeInitial[0]; + aobj[axName + ".range[1]"] = rangeInitial[1]; + } + } else { + var rangeNow = [ax.r2l(ax.range[0]), ax.r2l(ax.range[1])]; + + var rangeNew = [ + r0 * rangeNow[0] + r1 * rangeNow[1], + r0 * rangeNow[1] + r1 * rangeNow[0] + ]; + + aobj[axName + ".range[0]"] = ax.l2r(rangeNew[0]); + aobj[axName + ".range[1]"] = ax.l2r(rangeNew[1]); } + } } - else { - // if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y' - if(astr === 'hovermode' && (val === 'x' || val === 'y')) { - val = fullLayout._isHoriz ? 'y' : 'x'; - button.setAttribute('data-val', val); - } - - aobj[astr] = val; + } else { + // if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y' + if (astr === "hovermode" && (val === "x" || val === "y")) { + val = fullLayout._isHoriz ? "y" : "x"; + button.setAttribute("data-val", val); } - Plotly.relayout(gd, aobj); + aobj[astr] = val; + } + + Plotly.relayout(gd, aobj); } modeBarButtons.zoom3d = { - name: 'zoom3d', - title: 'Zoom', - attr: 'scene.dragmode', - val: 'zoom', - icon: Icons.zoombox, - click: handleDrag3d + name: "zoom3d", + title: "Zoom", + attr: "scene.dragmode", + val: "zoom", + icon: Icons.zoombox, + click: handleDrag3d }; modeBarButtons.pan3d = { - name: 'pan3d', - title: 'Pan', - attr: 'scene.dragmode', - val: 'pan', - icon: Icons.pan, - click: handleDrag3d + name: "pan3d", + title: "Pan", + attr: "scene.dragmode", + val: "pan", + icon: Icons.pan, + click: handleDrag3d }; modeBarButtons.orbitRotation = { - name: 'orbitRotation', - title: 'orbital rotation', - attr: 'scene.dragmode', - val: 'orbit', - icon: Icons['3d_rotate'], - click: handleDrag3d + name: "orbitRotation", + title: "orbital rotation", + attr: "scene.dragmode", + val: "orbit", + icon: Icons["3d_rotate"], + click: handleDrag3d }; modeBarButtons.tableRotation = { - name: 'tableRotation', - title: 'turntable rotation', - attr: 'scene.dragmode', - val: 'turntable', - icon: Icons['z-axis'], - click: handleDrag3d + name: "tableRotation", + title: "turntable rotation", + attr: "scene.dragmode", + val: "turntable", + icon: Icons["z-axis"], + click: handleDrag3d }; function handleDrag3d(gd, ev) { - var button = ev.currentTarget, - attr = button.getAttribute('data-attr'), - val = button.getAttribute('data-val') || true, - fullLayout = gd._fullLayout, - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'), - layoutUpdate = {}; + var button = ev.currentTarget, + attr = button.getAttribute("data-attr"), + val = button.getAttribute("data-val") || true, + fullLayout = gd._fullLayout, + sceneIds = Plots.getSubplotIds(fullLayout, "gl3d"), + layoutUpdate = {}; - var parts = attr.split('.'); + var parts = attr.split("."); - for(var i = 0; i < sceneIds.length; i++) { - layoutUpdate[sceneIds[i] + '.' + parts[1]] = val; - } + for (var i = 0; i < sceneIds.length; i++) { + layoutUpdate[sceneIds[i] + "." + parts[1]] = val; + } - Plotly.relayout(gd, layoutUpdate); + Plotly.relayout(gd, layoutUpdate); } modeBarButtons.resetCameraDefault3d = { - name: 'resetCameraDefault3d', - title: 'Reset camera to default', - attr: 'resetDefault', - icon: Icons.home, - click: handleCamera3d + name: "resetCameraDefault3d", + title: "Reset camera to default", + attr: "resetDefault", + icon: Icons.home, + click: handleCamera3d }; modeBarButtons.resetCameraLastSave3d = { - name: 'resetCameraLastSave3d', - title: 'Reset camera to last save', - attr: 'resetLastSave', - icon: Icons.movie, - click: handleCamera3d + name: "resetCameraLastSave3d", + title: "Reset camera to last save", + attr: "resetLastSave", + icon: Icons.movie, + click: handleCamera3d }; function handleCamera3d(gd, ev) { - var button = ev.currentTarget, - attr = button.getAttribute('data-attr'), - fullLayout = gd._fullLayout, - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'), - aobj = {}; - - for(var i = 0; i < sceneIds.length; i++) { - var sceneId = sceneIds[i], - key = sceneId + '.camera', - scene = fullLayout[sceneId]._scene; - - if(attr === 'resetDefault') { - aobj[key] = null; - } - else if(attr === 'resetLastSave') { - aobj[key] = Lib.extendDeep({}, scene.cameraInitial); - } + var button = ev.currentTarget, + attr = button.getAttribute("data-attr"), + fullLayout = gd._fullLayout, + sceneIds = Plots.getSubplotIds(fullLayout, "gl3d"), + aobj = {}; + + for (var i = 0; i < sceneIds.length; i++) { + var sceneId = sceneIds[i], + key = sceneId + ".camera", + scene = fullLayout[sceneId]._scene; + + if (attr === "resetDefault") { + aobj[key] = null; + } else if (attr === "resetLastSave") { + aobj[key] = Lib.extendDeep({}, scene.cameraInitial); } + } - Plotly.relayout(gd, aobj); + Plotly.relayout(gd, aobj); } modeBarButtons.hoverClosest3d = { - name: 'hoverClosest3d', - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: null, - toggle: true, - icon: Icons.tooltip_basic, - gravity: 'ne', - click: handleHover3d + name: "hoverClosest3d", + title: "Toggle show closest data on hover", + attr: "hovermode", + val: null, + toggle: true, + icon: Icons.tooltip_basic, + gravity: "ne", + click: handleHover3d }; function handleHover3d(gd, ev) { - var button = ev.currentTarget, - val = button._previousVal || false, - layout = gd.layout, - fullLayout = gd._fullLayout, - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'); - - var axes = ['xaxis', 'yaxis', 'zaxis'], - spikeAttrs = ['showspikes', 'spikesides', 'spikethickness', 'spikecolor']; - - // initialize 'current spike' object to be stored in the DOM - var currentSpikes = {}, - axisSpikes = {}, - layoutUpdate = {}; - - if(val) { - layoutUpdate = Lib.extendDeep(layout, val); - button._previousVal = null; - } - else { - layoutUpdate = { - 'allaxes.showspikes': false - }; - - for(var i = 0; i < sceneIds.length; i++) { - var sceneId = sceneIds[i], - sceneLayout = fullLayout[sceneId], - sceneSpikes = currentSpikes[sceneId] = {}; - - sceneSpikes.hovermode = sceneLayout.hovermode; - layoutUpdate[sceneId + '.hovermode'] = false; - - // copy all the current spike attrs - for(var j = 0; j < 3; j++) { - var axis = axes[j]; - axisSpikes = sceneSpikes[axis] = {}; - - for(var k = 0; k < spikeAttrs.length; k++) { - var spikeAttr = spikeAttrs[k]; - axisSpikes[spikeAttr] = sceneLayout[axis][spikeAttr]; - } - } + var button = ev.currentTarget, + val = button._previousVal || false, + layout = gd.layout, + fullLayout = gd._fullLayout, + sceneIds = Plots.getSubplotIds(fullLayout, "gl3d"); + + var axes = ["xaxis", "yaxis", "zaxis"], + spikeAttrs = ["showspikes", "spikesides", "spikethickness", "spikecolor"]; + + // initialize 'current spike' object to be stored in the DOM + var currentSpikes = {}, axisSpikes = {}, layoutUpdate = {}; + + if (val) { + layoutUpdate = Lib.extendDeep(layout, val); + button._previousVal = null; + } else { + layoutUpdate = { "allaxes.showspikes": false }; + + for (var i = 0; i < sceneIds.length; i++) { + var sceneId = sceneIds[i], + sceneLayout = fullLayout[sceneId], + sceneSpikes = currentSpikes[sceneId] = {}; + + sceneSpikes.hovermode = sceneLayout.hovermode; + layoutUpdate[sceneId + ".hovermode"] = false; + + // copy all the current spike attrs + for (var j = 0; j < 3; j++) { + var axis = axes[j]; + axisSpikes = sceneSpikes[axis] = {}; + + for (var k = 0; k < spikeAttrs.length; k++) { + var spikeAttr = spikeAttrs[k]; + axisSpikes[spikeAttr] = sceneLayout[axis][spikeAttr]; } - - button._previousVal = Lib.extendDeep({}, currentSpikes); + } } - Plotly.relayout(gd, layoutUpdate); + button._previousVal = Lib.extendDeep({}, currentSpikes); + } + + Plotly.relayout(gd, layoutUpdate); } modeBarButtons.zoomInGeo = { - name: 'zoomInGeo', - title: 'Zoom in', - attr: 'zoom', - val: 'in', - icon: Icons.zoom_plus, - click: handleGeo + name: "zoomInGeo", + title: "Zoom in", + attr: "zoom", + val: "in", + icon: Icons.zoom_plus, + click: handleGeo }; modeBarButtons.zoomOutGeo = { - name: 'zoomOutGeo', - title: 'Zoom out', - attr: 'zoom', - val: 'out', - icon: Icons.zoom_minus, - click: handleGeo + name: "zoomOutGeo", + title: "Zoom out", + attr: "zoom", + val: "out", + icon: Icons.zoom_minus, + click: handleGeo }; modeBarButtons.resetGeo = { - name: 'resetGeo', - title: 'Reset', - attr: 'reset', - val: null, - icon: Icons.autoscale, - click: handleGeo + name: "resetGeo", + title: "Reset", + attr: "reset", + val: null, + icon: Icons.autoscale, + click: handleGeo }; modeBarButtons.hoverClosestGeo = { - name: 'hoverClosestGeo', - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: null, - toggle: true, - icon: Icons.tooltip_basic, - gravity: 'ne', - click: toggleHover + name: "hoverClosestGeo", + title: "Toggle show closest data on hover", + attr: "hovermode", + val: null, + toggle: true, + icon: Icons.tooltip_basic, + gravity: "ne", + click: toggleHover }; function handleGeo(gd, ev) { - var button = ev.currentTarget, - attr = button.getAttribute('data-attr'), - val = button.getAttribute('data-val') || true, - fullLayout = gd._fullLayout, - geoIds = Plots.getSubplotIds(fullLayout, 'geo'); - - for(var i = 0; i < geoIds.length; i++) { - var geo = fullLayout[geoIds[i]]._subplot; - - if(attr === 'zoom') { - var scale = geo.projection.scale(); - var newScale = (val === 'in') ? 2 * scale : 0.5 * scale; - geo.projection.scale(newScale); - geo.zoom.scale(newScale); - geo.render(); - } - else if(attr === 'reset') geo.zoomReset(); - } + var button = ev.currentTarget, + attr = button.getAttribute("data-attr"), + val = button.getAttribute("data-val") || true, + fullLayout = gd._fullLayout, + geoIds = Plots.getSubplotIds(fullLayout, "geo"); + + for (var i = 0; i < geoIds.length; i++) { + var geo = fullLayout[geoIds[i]]._subplot; + + if (attr === "zoom") { + var scale = geo.projection.scale(); + var newScale = val === "in" ? 2 * scale : 0.5 * scale; + geo.projection.scale(newScale); + geo.zoom.scale(newScale); + geo.render(); + } else if (attr === "reset") geo.zoomReset(); + } } modeBarButtons.hoverClosestGl2d = { - name: 'hoverClosestGl2d', - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: null, - toggle: true, - icon: Icons.tooltip_basic, - gravity: 'ne', - click: toggleHover + name: "hoverClosestGl2d", + title: "Toggle show closest data on hover", + attr: "hovermode", + val: null, + toggle: true, + icon: Icons.tooltip_basic, + gravity: "ne", + click: toggleHover }; modeBarButtons.hoverClosestPie = { - name: 'hoverClosestPie', - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: 'closest', - icon: Icons.tooltip_basic, - gravity: 'ne', - click: toggleHover + name: "hoverClosestPie", + title: "Toggle show closest data on hover", + attr: "hovermode", + val: "closest", + icon: Icons.tooltip_basic, + gravity: "ne", + click: toggleHover }; function toggleHover(gd) { - var fullLayout = gd._fullLayout; + var fullLayout = gd._fullLayout; - var onHoverVal; - if(fullLayout._has('cartesian')) { - onHoverVal = fullLayout._isHoriz ? 'y' : 'x'; - } - else onHoverVal = 'closest'; + var onHoverVal; + if (fullLayout._has("cartesian")) { + onHoverVal = fullLayout._isHoriz ? "y" : "x"; + } else { + onHoverVal = "closest"; + } - var newHover = gd._fullLayout.hovermode ? false : onHoverVal; + var newHover = gd._fullLayout.hovermode ? false : onHoverVal; - Plotly.relayout(gd, 'hovermode', newHover); + Plotly.relayout(gd, "hovermode", newHover); } // buttons when more then one plot types are present - modeBarButtons.toggleHover = { - name: 'toggleHover', - title: 'Toggle show closest data on hover', - attr: 'hovermode', - val: null, - toggle: true, - icon: Icons.tooltip_basic, - gravity: 'ne', - click: function(gd, ev) { - toggleHover(gd); - - // the 3d hovermode update must come - // last so that layout.hovermode update does not - // override scene?.hovermode?.layout. - handleHover3d(gd, ev); - } + name: "toggleHover", + title: "Toggle show closest data on hover", + attr: "hovermode", + val: null, + toggle: true, + icon: Icons.tooltip_basic, + gravity: "ne", + click: function(gd, ev) { + toggleHover(gd); + + // the 3d hovermode update must come + // last so that layout.hovermode update does not + // override scene?.hovermode?.layout. + handleHover3d(gd, ev); + } }; modeBarButtons.resetViews = { - name: 'resetViews', - title: 'Reset views', - icon: Icons.home, - click: function(gd, ev) { - var button = ev.currentTarget; - - button.setAttribute('data-attr', 'zoom'); - button.setAttribute('data-val', 'reset'); - handleCartesian(gd, ev); - - button.setAttribute('data-attr', 'resetLastSave'); - handleCamera3d(gd, ev); - - // N.B handleCamera3d also triggers a replot for - // geo subplots. - } + name: "resetViews", + title: "Reset views", + icon: Icons.home, + click: function(gd, ev) { + var button = ev.currentTarget; + + button.setAttribute("data-attr", "zoom"); + button.setAttribute("data-val", "reset"); + handleCartesian(gd, ev); + + button.setAttribute("data-attr", "resetLastSave"); + handleCamera3d(gd, ev); + // N.B handleCamera3d also triggers a replot for + // geo subplots. + } }; diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 787fa706d4b..bf310fa1aeb 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -5,8 +5,5 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - -exports.manage = require('./manage'); +"use strict"; +exports.manage = require("./manage"); diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 57e5e2d17b3..e1d7473c8dc 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -5,15 +5,12 @@ * 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 Axes = require("../../plots/cartesian/axes"); +var scatterSubTypes = require("../../traces/scatter/subtypes"); - -'use strict'; - -var Axes = require('../../plots/cartesian/axes'); -var scatterSubTypes = require('../../traces/scatter/subtypes'); - -var createModeBar = require('./modebar'); -var modeBarButtons = require('./buttons'); +var createModeBar = require("./modebar"); +var modeBarButtons = require("./buttons"); /** * ModeBar wrapper around 'create' and 'update', @@ -24,203 +21,205 @@ var modeBarButtons = require('./buttons'); * */ module.exports = function manageModeBar(gd) { - var fullLayout = gd._fullLayout, - context = gd._context, - modeBar = fullLayout._modeBar; - - if(!context.displayModeBar) { - if(modeBar) { - modeBar.destroy(); - delete fullLayout._modeBar; - } - return; - } - - if(!Array.isArray(context.modeBarButtonsToRemove)) { - throw new Error([ - '*modeBarButtonsToRemove* configuration options', - 'must be an array.' - ].join(' ')); - } - - if(!Array.isArray(context.modeBarButtonsToAdd)) { - throw new Error([ - '*modeBarButtonsToAdd* configuration options', - 'must be an array.' - ].join(' ')); - } - - var customButtons = context.modeBarButtons; - var buttonGroups; - - if(Array.isArray(customButtons) && customButtons.length) { - buttonGroups = fillCustomButton(customButtons); - } - else { - buttonGroups = getButtonGroups( - gd, - context.modeBarButtonsToRemove, - context.modeBarButtonsToAdd - ); - } - - if(modeBar) modeBar.update(gd, buttonGroups); - else fullLayout._modeBar = createModeBar(gd, buttonGroups); + var fullLayout = gd._fullLayout, + context = gd._context, + modeBar = fullLayout._modeBar; + + if (!context.displayModeBar) { + if (modeBar) { + modeBar.destroy(); + delete fullLayout._modeBar; + } + return; + } + + if (!Array.isArray(context.modeBarButtonsToRemove)) { + throw new Error( + [ + "*modeBarButtonsToRemove* configuration options", + "must be an array." + ].join(" ") + ); + } + + if (!Array.isArray(context.modeBarButtonsToAdd)) { + throw new Error( + ["*modeBarButtonsToAdd* configuration options", "must be an array."].join( + " " + ) + ); + } + + var customButtons = context.modeBarButtons; + var buttonGroups; + + if (Array.isArray(customButtons) && customButtons.length) { + buttonGroups = fillCustomButton(customButtons); + } else { + buttonGroups = getButtonGroups( + gd, + context.modeBarButtonsToRemove, + context.modeBarButtonsToAdd + ); + } + + if (modeBar) modeBar.update(gd, buttonGroups); + else fullLayout._modeBar = createModeBar(gd, buttonGroups); }; // logic behind which buttons are displayed by default function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { - var fullLayout = gd._fullLayout, - fullData = gd._fullData; + var fullLayout = gd._fullLayout, fullData = gd._fullData; - var hasCartesian = fullLayout._has('cartesian'), - hasGL3D = fullLayout._has('gl3d'), - hasGeo = fullLayout._has('geo'), - hasPie = fullLayout._has('pie'), - hasGL2D = fullLayout._has('gl2d'), - hasTernary = fullLayout._has('ternary'); + var hasCartesian = fullLayout._has("cartesian"), + hasGL3D = fullLayout._has("gl3d"), + hasGeo = fullLayout._has("geo"), + hasPie = fullLayout._has("pie"), + hasGL2D = fullLayout._has("gl2d"), + hasTernary = fullLayout._has("ternary"); - var groups = []; + var groups = []; - function addGroup(newGroup) { - var out = []; + function addGroup(newGroup) { + var out = []; - for(var i = 0; i < newGroup.length; i++) { - var button = newGroup[i]; - if(buttonsToRemove.indexOf(button) !== -1) continue; - out.push(modeBarButtons[button]); - } - - groups.push(out); + for (var i = 0; i < newGroup.length; i++) { + var button = newGroup[i]; + if (buttonsToRemove.indexOf(button) !== -1) continue; + out.push(modeBarButtons[button]); } - // buttons common to all plot types - addGroup(['toImage', 'sendDataToCloud']); + groups.push(out); + } - // graphs with more than one plot types get 'union buttons' - // which reset the view or toggle hover labels across all subplots. - if((hasCartesian || hasGL2D || hasPie || hasTernary) + hasGeo + hasGL3D > 1) { - addGroup(['resetViews', 'toggleHover']); - return appendButtonsToGroups(groups, buttonsToAdd); - } - - if(hasGL3D) { - addGroup(['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']); - addGroup(['resetCameraDefault3d', 'resetCameraLastSave3d']); - addGroup(['hoverClosest3d']); - } - - if(hasGeo) { - addGroup(['zoomInGeo', 'zoomOutGeo', 'resetGeo']); - addGroup(['hoverClosestGeo']); - } - - var allAxesFixed = areAllAxesFixed(fullLayout), - dragModeGroup = []; - - if(((hasCartesian || hasGL2D) && !allAxesFixed) || hasTernary) { - dragModeGroup = ['zoom2d', 'pan2d']; - } - if((hasCartesian || hasTernary) && isSelectable(fullData)) { - dragModeGroup.push('select2d'); - dragModeGroup.push('lasso2d'); - } - if(dragModeGroup.length) addGroup(dragModeGroup); - - if((hasCartesian || hasGL2D) && !allAxesFixed && !hasTernary) { - addGroup(['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']); - } - - if(hasCartesian && hasPie) { - addGroup(['toggleHover']); - } - else if(hasGL2D) { - addGroup(['hoverClosestGl2d']); - } - else if(hasCartesian) { - addGroup(['hoverClosestCartesian', 'hoverCompareCartesian']); - } - else if(hasPie) { - addGroup(['hoverClosestPie']); - } + // buttons common to all plot types + addGroup(["toImage", "sendDataToCloud"]); + // graphs with more than one plot types get 'union buttons' + // which reset the view or toggle hover labels across all subplots. + if ( + (hasCartesian || hasGL2D || hasPie || hasTernary) + hasGeo + hasGL3D > 1 + ) { + addGroup(["resetViews", "toggleHover"]); return appendButtonsToGroups(groups, buttonsToAdd); + } + + if (hasGL3D) { + addGroup(["zoom3d", "pan3d", "orbitRotation", "tableRotation"]); + addGroup(["resetCameraDefault3d", "resetCameraLastSave3d"]); + addGroup(["hoverClosest3d"]); + } + + if (hasGeo) { + addGroup(["zoomInGeo", "zoomOutGeo", "resetGeo"]); + addGroup(["hoverClosestGeo"]); + } + + var allAxesFixed = areAllAxesFixed(fullLayout), dragModeGroup = []; + + if ((hasCartesian || hasGL2D) && !allAxesFixed || hasTernary) { + dragModeGroup = ["zoom2d", "pan2d"]; + } + if ((hasCartesian || hasTernary) && isSelectable(fullData)) { + dragModeGroup.push("select2d"); + dragModeGroup.push("lasso2d"); + } + if (dragModeGroup.length) addGroup(dragModeGroup); + + if ((hasCartesian || hasGL2D) && !allAxesFixed && !hasTernary) { + addGroup(["zoomIn2d", "zoomOut2d", "autoScale2d", "resetScale2d"]); + } + + if (hasCartesian && hasPie) { + addGroup(["toggleHover"]); + } else if (hasGL2D) { + addGroup(["hoverClosestGl2d"]); + } else if (hasCartesian) { + addGroup(["hoverClosestCartesian", "hoverCompareCartesian"]); + } else if (hasPie) { + addGroup(["hoverClosestPie"]); + } + + return appendButtonsToGroups(groups, buttonsToAdd); } function areAllAxesFixed(fullLayout) { - var axList = Axes.list({_fullLayout: fullLayout}, null, true); - var allFixed = true; + var axList = Axes.list({ _fullLayout: fullLayout }, null, true); + var allFixed = true; - for(var i = 0; i < axList.length; i++) { - if(!axList[i].fixedrange) { - allFixed = false; - break; - } + for (var i = 0; i < axList.length; i++) { + if (!axList[i].fixedrange) { + allFixed = false; + break; } + } - return allFixed; + return allFixed; } // look for traces that support selection // to be updated as we add more selectPoints handlers function isSelectable(fullData) { - var selectable = false; + var selectable = false; - for(var i = 0; i < fullData.length; i++) { - if(selectable) break; + for (var i = 0; i < fullData.length; i++) { + if (selectable) break; - var trace = fullData[i]; + var trace = fullData[i]; - if(!trace._module || !trace._module.selectPoints) continue; + if (!trace._module || !trace._module.selectPoints) continue; - if(trace.type === 'scatter' || trace.type === 'scatterternary') { - if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) { - selectable = true; - } - } - // assume that in general if the trace module has selectPoints, - // then it's selectable. Scatter is an exception to this because it must - // have markers or text, not just be a scatter type. - else selectable = true; + if (trace.type === "scatter" || trace.type === "scatterternary") { + if (scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) { + selectable = true; + } + } else { + // assume that in general if the trace module has selectPoints, + // then it's selectable. Scatter is an exception to this because it must + // have markers or text, not just be a scatter type. + selectable = true; } + } - return selectable; + return selectable; } function appendButtonsToGroups(groups, buttons) { - if(buttons.length) { - if(Array.isArray(buttons[0])) { - for(var i = 0; i < buttons.length; i++) { - groups.push(buttons[i]); - } - } - else groups.push(buttons); + if (buttons.length) { + if (Array.isArray(buttons[0])) { + for (var i = 0; i < buttons.length; i++) { + groups.push(buttons[i]); + } + } else { + groups.push(buttons); } + } - return groups; + return groups; } // fill in custom buttons referring to default mode bar buttons function fillCustomButton(customButtons) { - for(var i = 0; i < customButtons.length; i++) { - var buttonGroup = customButtons[i]; - - for(var j = 0; j < buttonGroup.length; j++) { - var button = buttonGroup[j]; - - if(typeof button === 'string') { - if(modeBarButtons[button] !== undefined) { - customButtons[i][j] = modeBarButtons[button]; - } - else { - throw new Error([ - '*modeBarButtons* configuration options', - 'invalid button name' - ].join(' ')); - } - } + for (var i = 0; i < customButtons.length; i++) { + var buttonGroup = customButtons[i]; + + for (var j = 0; j < buttonGroup.length; j++) { + var button = buttonGroup[j]; + + if (typeof button === "string") { + if (modeBarButtons[button] !== undefined) { + customButtons[i][j] = modeBarButtons[button]; + } else { + throw new Error( + [ + "*modeBarButtons* configuration options", + "invalid button name" + ].join(" ") + ); } + } } + } - return customButtons; + return customButtons; } diff --git a/src/components/modebar/modebar.js b/src/components/modebar/modebar.js index 054a8a91928..25dfcf8448c 100644 --- a/src/components/modebar/modebar.js +++ b/src/components/modebar/modebar.js @@ -5,15 +5,11 @@ * 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 d3 = require("d3"); - -'use strict'; - -var d3 = require('d3'); - -var Lib = require('../../lib'); -var Icons = require('../../../build/ploticon'); - +var Lib = require("../../lib"); +var Icons = require("../../../build/ploticon"); /** * UI controller for interactive plots @@ -24,12 +20,12 @@ var Icons = require('../../../build/ploticon'); * @Param {object} opts.graphInfo primary plot object containing data and layout */ function ModeBar(opts) { - this.container = opts.container; - this.element = document.createElement('div'); + this.container = opts.container; + this.element = document.createElement("div"); - this.update(opts.graphInfo, opts.buttons); + this.update(opts.graphInfo, opts.buttons); - this.container.appendChild(this.element); + this.container.appendChild(this.element); } var proto = ModeBar.prototype; @@ -42,60 +38,61 @@ var proto = ModeBar.prototype; * */ proto.update = function(graphInfo, buttons) { - this.graphInfo = graphInfo; + this.graphInfo = graphInfo; - var context = this.graphInfo._context; + var context = this.graphInfo._context; - if(context.displayModeBar === 'hover') { - this.element.className = 'modebar modebar--hover'; - } - else this.element.className = 'modebar'; + if (context.displayModeBar === "hover") { + this.element.className = "modebar modebar--hover"; + } else { + this.element.className = "modebar"; + } - // if buttons or logo have changed, redraw modebar interior - var needsNewButtons = !this.hasButtons(buttons), - needsNewLogo = (this.hasLogo !== context.displaylogo); + // if buttons or logo have changed, redraw modebar interior + var needsNewButtons = !this.hasButtons(buttons), + needsNewLogo = this.hasLogo !== context.displaylogo; - if(needsNewButtons || needsNewLogo) { - this.removeAllButtons(); + if (needsNewButtons || needsNewLogo) { + this.removeAllButtons(); - this.updateButtons(buttons); + this.updateButtons(buttons); - if(context.displaylogo) { - this.element.appendChild(this.getLogo()); - this.hasLogo = true; - } + if (context.displaylogo) { + this.element.appendChild(this.getLogo()); + this.hasLogo = true; } + } - this.updateActiveButton(); + this.updateActiveButton(); }; proto.updateButtons = function(buttons) { - var _this = this; - - this.buttons = buttons; - this.buttonElements = []; - this.buttonsNames = []; - - this.buttons.forEach(function(buttonGroup) { - var group = _this.createGroup(); - - buttonGroup.forEach(function(buttonConfig) { - var buttonName = buttonConfig.name; - if(!buttonName) { - throw new Error('must provide button \'name\' in button config'); - } - if(_this.buttonsNames.indexOf(buttonName) !== -1) { - throw new Error('button name \'' + buttonName + '\' is taken'); - } - _this.buttonsNames.push(buttonName); - - var button = _this.createButton(buttonConfig); - _this.buttonElements.push(button); - group.appendChild(button); - }); - - _this.element.appendChild(group); + var _this = this; + + this.buttons = buttons; + this.buttonElements = []; + this.buttonsNames = []; + + this.buttons.forEach(function(buttonGroup) { + var group = _this.createGroup(); + + buttonGroup.forEach(function(buttonConfig) { + var buttonName = buttonConfig.name; + if (!buttonName) { + throw new Error("must provide button 'name' in button config"); + } + if (_this.buttonsNames.indexOf(buttonName) !== -1) { + throw new Error("button name '" + buttonName + "' is taken"); + } + _this.buttonsNames.push(buttonName); + + var button = _this.createButton(buttonConfig); + _this.buttonElements.push(button); + group.appendChild(button); }); + + _this.element.appendChild(group); + }); }; /** @@ -103,10 +100,10 @@ proto.updateButtons = function(buttons) { * @Return {HTMLelement} */ proto.createGroup = function() { - var group = document.createElement('div'); - group.className = 'modebar-group'; + var group = document.createElement("div"); + group.className = "modebar-group"; - return group; + return group; }; /** @@ -115,44 +112,42 @@ proto.createGroup = function() { * @Return {HTMLelement} */ proto.createButton = function(config) { - var _this = this, - button = document.createElement('a'); + var _this = this, button = document.createElement("a"); - button.setAttribute('rel', 'tooltip'); - button.className = 'modebar-btn'; + button.setAttribute("rel", "tooltip"); + button.className = "modebar-btn"; - var title = config.title; - if(title === undefined) title = config.name; - if(title || title === 0) button.setAttribute('data-title', title); + var title = config.title; + if (title === undefined) title = config.name; + if (title || title === 0) button.setAttribute("data-title", title); - if(config.attr !== undefined) button.setAttribute('data-attr', config.attr); + if (config.attr !== undefined) button.setAttribute("data-attr", config.attr); - var val = config.val; - if(val !== undefined) { - if(typeof val === 'function') val = val(this.graphInfo); - button.setAttribute('data-val', val); - } + var val = config.val; + if (val !== undefined) { + if (typeof val === "function") val = val(this.graphInfo); + button.setAttribute("data-val", val); + } - var click = config.click; - if(typeof click !== 'function') { - throw new Error('must provide button \'click\' function in button config'); - } - else { - button.addEventListener('click', function(ev) { - config.click(_this.graphInfo, ev); + var click = config.click; + if (typeof click !== "function") { + throw new Error("must provide button 'click' function in button config"); + } else { + button.addEventListener("click", function(ev) { + config.click(_this.graphInfo, ev); - // only needed for 'hoverClosestGeo' which does not call relayout - _this.updateActiveButton(ev.currentTarget); - }); - } + // only needed for 'hoverClosestGeo' which does not call relayout + _this.updateActiveButton(ev.currentTarget); + }); + } - button.setAttribute('data-toggle', config.toggle || false); - if(config.toggle) d3.select(button).classed('active', true); + button.setAttribute("data-toggle", config.toggle || false); + if (config.toggle) d3.select(button).classed("active", true); - button.appendChild(this.createIcon(config.icon || Icons.question)); - button.setAttribute('data-gravity', config.gravity || 'n'); + button.appendChild(this.createIcon(config.icon || Icons.question)); + button.setAttribute("data-gravity", config.gravity || "n"); - return button; + return button; }; /** @@ -163,20 +158,20 @@ proto.createButton = function(config) { * @Return {HTMLelement} */ proto.createIcon = function(thisIcon) { - var iconHeight = thisIcon.ascent - thisIcon.descent, - svgNS = 'http://www.w3.org/2000/svg', - icon = document.createElementNS(svgNS, 'svg'), - path = document.createElementNS(svgNS, 'path'); + var iconHeight = thisIcon.ascent - thisIcon.descent, + svgNS = "http://www.w3.org/2000/svg", + icon = document.createElementNS(svgNS, "svg"), + path = document.createElementNS(svgNS, "path"); - icon.setAttribute('height', '1em'); - icon.setAttribute('width', (thisIcon.width / iconHeight) + 'em'); - icon.setAttribute('viewBox', [0, 0, thisIcon.width, iconHeight].join(' ')); + icon.setAttribute("height", "1em"); + icon.setAttribute("width", thisIcon.width / iconHeight + "em"); + icon.setAttribute("viewBox", [0, 0, thisIcon.width, iconHeight].join(" ")); - path.setAttribute('d', thisIcon.path); - path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')'); - icon.appendChild(path); + path.setAttribute("d", thisIcon.path); + path.setAttribute("transform", "matrix(1 0 0 -1 0 " + thisIcon.ascent + ")"); + icon.appendChild(path); - return icon; + return icon; }; /** @@ -185,33 +180,31 @@ proto.createIcon = function(thisIcon) { * @Return {HTMLelement} */ proto.updateActiveButton = function(buttonClicked) { - var fullLayout = this.graphInfo._fullLayout, - dataAttrClicked = (buttonClicked !== undefined) ? - buttonClicked.getAttribute('data-attr') : - null; - - this.buttonElements.forEach(function(button) { - var thisval = button.getAttribute('data-val') || true, - dataAttr = button.getAttribute('data-attr'), - isToggleButton = (button.getAttribute('data-toggle') === 'true'), - button3 = d3.select(button); - - // Use 'data-toggle' and 'buttonClicked' to toggle buttons - // that have no one-to-one equivalent in fullLayout - if(isToggleButton) { - if(dataAttr === dataAttrClicked) { - button3.classed('active', !button3.classed('active')); - } - } - else { - var val = (dataAttr === null) ? - dataAttr : - Lib.nestedProperty(fullLayout, dataAttr).get(); - - button3.classed('active', val === thisval); - } - - }); + var fullLayout = this.graphInfo._fullLayout, + dataAttrClicked = buttonClicked !== undefined + ? buttonClicked.getAttribute("data-attr") + : null; + + this.buttonElements.forEach(function(button) { + var thisval = button.getAttribute("data-val") || true, + dataAttr = button.getAttribute("data-attr"), + isToggleButton = button.getAttribute("data-toggle") === "true", + button3 = d3.select(button); + + // Use 'data-toggle' and 'buttonClicked' to toggle buttons + // that have no one-to-one equivalent in fullLayout + if (isToggleButton) { + if (dataAttr === dataAttrClicked) { + button3.classed("active", !button3.classed("active")); + } + } else { + var val = dataAttr === null + ? dataAttr + : Lib.nestedProperty(fullLayout, dataAttr).get(); + + button3.classed("active", val === thisval); + } + }); }; /** @@ -221,68 +214,69 @@ proto.updateActiveButton = function(buttonClicked) { * @Return {boolean} */ proto.hasButtons = function(buttons) { - var currentButtons = this.buttons; + var currentButtons = this.buttons; - if(!currentButtons) return false; + if (!currentButtons) return false; - if(buttons.length !== currentButtons.length) return false; + if (buttons.length !== currentButtons.length) return false; - for(var i = 0; i < buttons.length; ++i) { - if(buttons[i].length !== currentButtons[i].length) return false; - for(var j = 0; j < buttons[i].length; j++) { - if(buttons[i][j].name !== currentButtons[i][j].name) return false; - } + for (var i = 0; i < buttons.length; ++i) { + if (buttons[i].length !== currentButtons[i].length) return false; + for (var j = 0; j < buttons[i].length; j++) { + if (buttons[i][j].name !== currentButtons[i][j].name) return false; } + } - return true; + return true; }; /** * @return {HTMLDivElement} The logo image wrapped in a group */ proto.getLogo = function() { - var group = this.createGroup(), - a = document.createElement('a'); + var group = this.createGroup(), a = document.createElement("a"); - a.href = 'https://plot.ly/'; - a.target = '_blank'; - a.setAttribute('data-title', 'Produced with Plotly'); - a.className = 'modebar-btn plotlyjsicon modebar-btn--logo'; + a.href = "https://plot.ly/"; + a.target = "_blank"; + a.setAttribute("data-title", "Produced with Plotly"); + a.className = "modebar-btn plotlyjsicon modebar-btn--logo"; - a.appendChild(this.createIcon(Icons.plotlylogo)); + a.appendChild(this.createIcon(Icons.plotlylogo)); - group.appendChild(a); - return group; + group.appendChild(a); + return group; }; proto.removeAllButtons = function() { - while(this.element.firstChild) { - this.element.removeChild(this.element.firstChild); - } + while (this.element.firstChild) { + this.element.removeChild(this.element.firstChild); + } - this.hasLogo = false; + this.hasLogo = false; }; proto.destroy = function() { - Lib.removeElement(this.container.querySelector('.modebar')); + Lib.removeElement(this.container.querySelector(".modebar")); }; function createModeBar(gd, buttons) { - var fullLayout = gd._fullLayout; - - var modeBar = new ModeBar({ - graphInfo: gd, - container: fullLayout._paperdiv.node(), - buttons: buttons - }); - - if(fullLayout._privateplot) { - d3.select(modeBar.element).append('span') - .classed('badge-private float--left', true) - .text('PRIVATE'); - } - - return modeBar; + var fullLayout = gd._fullLayout; + + var modeBar = new ModeBar({ + graphInfo: gd, + container: fullLayout._paperdiv.node(), + buttons: buttons + }); + + if (fullLayout._privateplot) { + d3 + .select(modeBar.element) + .append("span") + .classed("badge-private float--left", true) + .text("PRIVATE"); + } + + return modeBar; } module.exports = createModeBar; diff --git a/src/components/rangeselector/attributes.js b/src/components/rangeselector/attributes.js index 390afe3e200..449c6b3cec3 100644 --- a/src/components/rangeselector/attributes.js +++ b/src/components/rangeselector/attributes.js @@ -5,99 +5,92 @@ * 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 fontAttrs = require('../../plots/font_attributes'); -var colorAttrs = require('../color/attributes'); -var extendFlat = require('../../lib/extend').extendFlat; -var buttonAttrs = require('./button_attributes'); +"use strict"; +var fontAttrs = require("../../plots/font_attributes"); +var colorAttrs = require("../color/attributes"); +var extendFlat = require("../../lib/extend").extendFlat; +var buttonAttrs = require("./button_attributes"); buttonAttrs = extendFlat(buttonAttrs, { - _isLinkedToArray: 'button', - - description: [ - 'Sets the specifications for each buttons.', - 'By default, a range selector comes with no buttons.' - ].join(' ') + _isLinkedToArray: "button", + description: [ + "Sets the specifications for each buttons.", + "By default, a range selector comes with no buttons." + ].join(" ") }); module.exports = { - visible: { - valType: 'boolean', - role: 'info', - description: [ - 'Determines whether or not this range selector is visible.', - 'Note that range selectors are only available for x axes of', - '`type` set to or auto-typed to *date*.' - ].join(' ') - }, - - buttons: buttonAttrs, - - x: { - valType: 'number', - min: -2, - max: 3, - role: 'style', - description: 'Sets the x position (in normalized coordinates) of the range selector.' - }, - xanchor: { - valType: 'enumerated', - values: ['auto', 'left', 'center', 'right'], - dflt: 'left', - role: 'info', - description: [ - 'Sets the range selector\'s horizontal position anchor.', - 'This anchor binds the `x` position to the *left*, *center*', - 'or *right* of the range selector.' - ].join(' ') - }, - y: { - valType: 'number', - min: -2, - max: 3, - role: 'style', - description: 'Sets the y position (in normalized coordinates) of the range selector.' - }, - yanchor: { - valType: 'enumerated', - values: ['auto', 'top', 'middle', 'bottom'], - dflt: 'bottom', - role: 'info', - description: [ - 'Sets the range selector\'s vertical position anchor', - 'This anchor binds the `y` position to the *top*, *middle*', - 'or *bottom* of the range selector.' - ].join(' ') - }, - - font: extendFlat({}, fontAttrs, { - description: 'Sets the font of the range selector button text.' - }), - - bgcolor: { - valType: 'color', - dflt: colorAttrs.lightLine, - role: 'style', - description: 'Sets the background color of the range selector buttons.' - }, - activecolor: { - valType: 'color', - role: 'style', - description: 'Sets the background color of the active range selector button.' - }, - bordercolor: { - valType: 'color', - dflt: colorAttrs.defaultLine, - role: 'style', - description: 'Sets the color of the border enclosing the range selector.' - }, - borderwidth: { - valType: 'number', - min: 0, - dflt: 0, - role: 'style', - description: 'Sets the width (in px) of the border enclosing the range selector.' - } + visible: { + valType: "boolean", + role: "info", + description: [ + "Determines whether or not this range selector is visible.", + "Note that range selectors are only available for x axes of", + "`type` set to or auto-typed to *date*." + ].join(" ") + }, + buttons: buttonAttrs, + x: { + valType: "number", + min: -2, + max: 3, + role: "style", + description: "Sets the x position (in normalized coordinates) of the range selector." + }, + xanchor: { + valType: "enumerated", + values: ["auto", "left", "center", "right"], + dflt: "left", + role: "info", + description: [ + "Sets the range selector's horizontal position anchor.", + "This anchor binds the `x` position to the *left*, *center*", + "or *right* of the range selector." + ].join(" ") + }, + y: { + valType: "number", + min: -2, + max: 3, + role: "style", + description: "Sets the y position (in normalized coordinates) of the range selector." + }, + yanchor: { + valType: "enumerated", + values: ["auto", "top", "middle", "bottom"], + dflt: "bottom", + role: "info", + description: [ + "Sets the range selector's vertical position anchor", + "This anchor binds the `y` position to the *top*, *middle*", + "or *bottom* of the range selector." + ].join(" ") + }, + font: extendFlat({}, fontAttrs, { + description: "Sets the font of the range selector button text." + }), + bgcolor: { + valType: "color", + dflt: colorAttrs.lightLine, + role: "style", + description: "Sets the background color of the range selector buttons." + }, + activecolor: { + valType: "color", + role: "style", + description: "Sets the background color of the active range selector button." + }, + bordercolor: { + valType: "color", + dflt: colorAttrs.defaultLine, + role: "style", + description: "Sets the color of the border enclosing the range selector." + }, + borderwidth: { + valType: "number", + min: 0, + dflt: 0, + role: "style", + description: "Sets the width (in px) of the border enclosing the range selector." + } }; diff --git a/src/components/rangeselector/button_attributes.js b/src/components/rangeselector/button_attributes.js index 14fd193a0d4..7dc0e5b538a 100644 --- a/src/components/rangeselector/button_attributes.js +++ b/src/components/rangeselector/button_attributes.js @@ -5,52 +5,49 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - step: { - valType: 'enumerated', - role: 'info', - values: ['month', 'year', 'day', 'hour', 'minute', 'second', 'all'], - dflt: 'month', - description: [ - 'The unit of measurement that the `count` value will set the range by.' - ].join(' ') - }, - stepmode: { - valType: 'enumerated', - role: 'info', - values: ['backward', 'todate'], - dflt: 'backward', - description: [ - 'Sets the range update mode.', - 'If *backward*, the range update shifts the start of range', - 'back *count* times *step* milliseconds.', - 'If *todate*, the range update shifts the start of range', - 'back to the first timestamp from *count* times', - '*step* milliseconds back.', - 'For example, with `step` set to *year* and `count` set to *1*', - 'the range update shifts the start of the range back to', - 'January 01 of the current year.', - 'Month and year *todate* are currently available only', - 'for the built-in (Gregorian) calendar.' - ].join(' ') - }, - count: { - valType: 'number', - role: 'info', - min: 0, - dflt: 1, - description: [ - 'Sets the number of steps to take to update the range.', - 'Use with `step` to specify the update interval.' - ].join(' ') - }, - label: { - valType: 'string', - role: 'info', - description: 'Sets the text label to appear on the button.' - } + step: { + valType: "enumerated", + role: "info", + values: ["month", "year", "day", "hour", "minute", "second", "all"], + dflt: "month", + description: [ + "The unit of measurement that the `count` value will set the range by." + ].join(" ") + }, + stepmode: { + valType: "enumerated", + role: "info", + values: ["backward", "todate"], + dflt: "backward", + description: [ + "Sets the range update mode.", + "If *backward*, the range update shifts the start of range", + "back *count* times *step* milliseconds.", + "If *todate*, the range update shifts the start of range", + "back to the first timestamp from *count* times", + "*step* milliseconds back.", + "For example, with `step` set to *year* and `count` set to *1*", + "the range update shifts the start of the range back to", + "January 01 of the current year.", + "Month and year *todate* are currently available only", + "for the built-in (Gregorian) calendar." + ].join(" ") + }, + count: { + valType: "number", + role: "info", + min: 0, + dflt: 1, + description: [ + "Sets the number of steps to take to update the range.", + "Use with `step` to specify the update interval." + ].join(" ") + }, + label: { + valType: "string", + role: "info", + description: "Sets the text label to appear on the button." + } }; diff --git a/src/components/rangeselector/constants.js b/src/components/rangeselector/constants.js index 202e73a1cc9..973f61c41db 100644 --- a/src/components/rangeselector/constants.js +++ b/src/components/rangeselector/constants.js @@ -5,23 +5,16 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - - // 'y' position pad above counter axis domain - yPad: 0.02, - - // minimum button width (regardless of text size) - minButtonWidth: 30, - - // buttons rect radii - rx: 3, - ry: 3, - - // light fraction used to compute the 'activecolor' default - lightAmount: 25, - darkAmount: 10 + // 'y' position pad above counter axis domain + yPad: 0.02, + // minimum button width (regardless of text size) + minButtonWidth: 30, + // buttons rect radii + rx: 3, + ry: 3, + // light fraction used to compute the 'activecolor' default + lightAmount: 25, + darkAmount: 10 }; diff --git a/src/components/rangeselector/defaults.js b/src/components/rangeselector/defaults.js index a2523d69621..0a350dedd88 100644 --- a/src/components/rangeselector/defaults.js +++ b/src/components/rangeselector/defaults.js @@ -5,93 +5,102 @@ * 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 Color = require('../color'); - -var attributes = require('./attributes'); -var buttonAttrs = require('./button_attributes'); -var constants = require('./constants'); - - -module.exports = function handleDefaults(containerIn, containerOut, layout, counterAxes, calendar) { - var selectorIn = containerIn.rangeselector || {}, - selectorOut = containerOut.rangeselector = {}; - - function coerce(attr, dflt) { - return Lib.coerce(selectorIn, selectorOut, attributes, attr, dflt); - } - - var buttons = buttonsDefaults(selectorIn, selectorOut, calendar); - - var visible = coerce('visible', buttons.length > 0); - if(!visible) return; - - var posDflt = getPosDflt(containerOut, layout, counterAxes); - coerce('x', posDflt[0]); - coerce('y', posDflt[1]); - Lib.noneOrAll(containerIn, containerOut, ['x', 'y']); - - coerce('xanchor'); - coerce('yanchor'); - - Lib.coerceFont(coerce, 'font', layout.font); - - var bgColor = coerce('bgcolor'); - coerce('activecolor', Color.contrast(bgColor, constants.lightAmount, constants.darkAmount)); - coerce('bordercolor'); - coerce('borderwidth'); +"use strict"; +var Lib = require("../../lib"); +var Color = require("../color"); + +var attributes = require("./attributes"); +var buttonAttrs = require("./button_attributes"); +var constants = require("./constants"); + +module.exports = function handleDefaults( + containerIn, + containerOut, + layout, + counterAxes, + calendar +) { + var selectorIn = containerIn.rangeselector || {}, + selectorOut = containerOut.rangeselector = {}; + + function coerce(attr, dflt) { + return Lib.coerce(selectorIn, selectorOut, attributes, attr, dflt); + } + + var buttons = buttonsDefaults(selectorIn, selectorOut, calendar); + + var visible = coerce("visible", buttons.length > 0); + if (!visible) return; + + var posDflt = getPosDflt(containerOut, layout, counterAxes); + coerce("x", posDflt[0]); + coerce("y", posDflt[1]); + Lib.noneOrAll(containerIn, containerOut, ["x", "y"]); + + coerce("xanchor"); + coerce("yanchor"); + + Lib.coerceFont(coerce, "font", layout.font); + + var bgColor = coerce("bgcolor"); + coerce( + "activecolor", + Color.contrast(bgColor, constants.lightAmount, constants.darkAmount) + ); + coerce("bordercolor"); + coerce("borderwidth"); }; function buttonsDefaults(containerIn, containerOut, calendar) { - var buttonsIn = containerIn.buttons || [], - buttonsOut = containerOut.buttons = []; - - var buttonIn, buttonOut; - - function coerce(attr, dflt) { - return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt); + var buttonsIn = containerIn.buttons || [], + buttonsOut = containerOut.buttons = []; + + var buttonIn, buttonOut; + + function coerce(attr, dflt) { + return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt); + } + + for (var i = 0; i < buttonsIn.length; i++) { + buttonIn = buttonsIn[i]; + buttonOut = {}; + + if (!Lib.isPlainObject(buttonIn)) continue; + + var step = coerce("step"); + if (step !== "all") { + if ( + calendar && + calendar !== "gregorian" && + (step === "month" || step === "year") + ) { + buttonOut.stepmode = "backward"; + } else { + coerce("stepmode"); + } + + coerce("count"); } - for(var i = 0; i < buttonsIn.length; i++) { - buttonIn = buttonsIn[i]; - buttonOut = {}; - - if(!Lib.isPlainObject(buttonIn)) continue; - - var step = coerce('step'); - if(step !== 'all') { - if(calendar && calendar !== 'gregorian' && (step === 'month' || step === 'year')) { - buttonOut.stepmode = 'backward'; - } - else { - coerce('stepmode'); - } + coerce("label"); - coerce('count'); - } + buttonOut._index = i; + buttonsOut.push(buttonOut); + } - coerce('label'); - - buttonOut._index = i; - buttonsOut.push(buttonOut); - } - - return buttonsOut; + return buttonsOut; } function getPosDflt(containerOut, layout, counterAxes) { - var anchoredList = counterAxes.filter(function(ax) { - return layout[ax].anchor === containerOut._id; - }); - - var posY = 0; - for(var i = 0; i < anchoredList.length; i++) { - var domain = layout[anchoredList[i]].domain; - if(domain) posY = Math.max(domain[1], posY); - } + var anchoredList = counterAxes.filter(function(ax) { + return layout[ax].anchor === containerOut._id; + }); + + var posY = 0; + for (var i = 0; i < anchoredList.length; i++) { + var domain = layout[anchoredList[i]].domain; + if (domain) posY = Math.max(domain[1], posY); + } - return [containerOut.domain[0], posY + constants.yPad]; + return [containerOut.domain[0], posY + constants.yPad]; } diff --git a/src/components/rangeselector/draw.js b/src/components/rangeselector/draw.js index 8dbc8ff4774..1879420a5bc 100644 --- a/src/components/rangeselector/draw.js +++ b/src/components/rangeselector/draw.js @@ -5,269 +5,248 @@ * 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 d3 = require("d3"); +var Plotly = require("../../plotly"); +var Plots = require("../../plots/plots"); +var Color = require("../color"); +var Drawing = require("../drawing"); +var svgTextUtils = require("../../lib/svg_text_utils"); +var axisIds = require("../../plots/cartesian/axis_ids"); +var anchorUtils = require("../legend/anchor_utils"); -'use strict'; - -var d3 = require('d3'); - -var Plotly = require('../../plotly'); -var Plots = require('../../plots/plots'); -var Color = require('../color'); -var Drawing = require('../drawing'); -var svgTextUtils = require('../../lib/svg_text_utils'); -var axisIds = require('../../plots/cartesian/axis_ids'); -var anchorUtils = require('../legend/anchor_utils'); - -var constants = require('./constants'); -var getUpdateObject = require('./get_update_object'); - +var constants = require("./constants"); +var getUpdateObject = require("./get_update_object"); module.exports = function draw(gd) { - var fullLayout = gd._fullLayout; - - var selectors = fullLayout._infolayer.selectAll('.rangeselector') - .data(makeSelectorData(gd), selectorKeyFunc); + var fullLayout = gd._fullLayout; - selectors.enter().append('g') - .classed('rangeselector', true); + var selectors = fullLayout._infolayer + .selectAll(".rangeselector") + .data(makeSelectorData(gd), selectorKeyFunc); - selectors.exit().remove(); + selectors.enter().append("g").classed("rangeselector", true); - selectors.style({ - cursor: 'pointer', - 'pointer-events': 'all' - }); - - selectors.each(function(d) { - var selector = d3.select(this), - axisLayout = d, - selectorLayout = axisLayout.rangeselector; + selectors.exit().remove(); - var buttons = selector.selectAll('g.button') - .data(selectorLayout.buttons); + selectors.style({ cursor: "pointer", "pointer-events": "all" }); - buttons.enter().append('g') - .classed('button', true); + selectors.each(function(d) { + var selector = d3.select(this), + axisLayout = d, + selectorLayout = axisLayout.rangeselector; - buttons.exit().remove(); + var buttons = selector.selectAll("g.button").data(selectorLayout.buttons); - buttons.each(function(d) { - var button = d3.select(this); - var update = getUpdateObject(axisLayout, d); + buttons.enter().append("g").classed("button", true); - d.isActive = isActive(axisLayout, d, update); + buttons.exit().remove(); - button.call(drawButtonRect, selectorLayout, d); - button.call(drawButtonText, selectorLayout, d); + buttons.each(function(d) { + var button = d3.select(this); + var update = getUpdateObject(axisLayout, d); - button.on('click', function() { - if(gd._dragged) return; + d.isActive = isActive(axisLayout, d, update); - Plotly.relayout(gd, update); - }); + button.call(drawButtonRect, selectorLayout, d); + button.call(drawButtonText, selectorLayout, d); - button.on('mouseover', function() { - d.isHovered = true; - button.call(drawButtonRect, selectorLayout, d); - }); + button.on("click", function() { + if (gd._dragged) return; - button.on('mouseout', function() { - d.isHovered = false; - button.call(drawButtonRect, selectorLayout, d); - }); - }); + Plotly.relayout(gd, update); + }); - // N.B. this mutates selectorLayout - reposition(gd, buttons, selectorLayout, axisLayout._name); + button.on("mouseover", function() { + d.isHovered = true; + button.call(drawButtonRect, selectorLayout, d); + }); - selector.attr('transform', 'translate(' + - selectorLayout.lx + ',' + selectorLayout.ly + - ')'); + button.on("mouseout", function() { + d.isHovered = false; + button.call(drawButtonRect, selectorLayout, d); + }); }); + // N.B. this mutates selectorLayout + reposition(gd, buttons, selectorLayout, axisLayout._name); + + selector.attr( + "transform", + "translate(" + selectorLayout.lx + "," + selectorLayout.ly + ")" + ); + }); }; function makeSelectorData(gd) { - var axes = axisIds.list(gd, 'x', true); - var data = []; + var axes = axisIds.list(gd, "x", true); + var data = []; - for(var i = 0; i < axes.length; i++) { - var axis = axes[i]; + for (var i = 0; i < axes.length; i++) { + var axis = axes[i]; - if(axis.rangeselector && axis.rangeselector.visible) { - data.push(axis); - } + if (axis.rangeselector && axis.rangeselector.visible) { + data.push(axis); } + } - return data; + return data; } function selectorKeyFunc(d) { - return d._id; + return d._id; } function isActive(axisLayout, opts, update) { - if(opts.step === 'all') { - return axisLayout.autorange === true; - } - else { - var keys = Object.keys(update); - - return ( - axisLayout.range[0] === update[keys[0]] && - axisLayout.range[1] === update[keys[1]] - ); - } + if (opts.step === "all") { + return axisLayout.autorange === true; + } else { + var keys = Object.keys(update); + + return axisLayout.range[0] === update[keys[0]] && + axisLayout.range[1] === update[keys[1]]; + } } function drawButtonRect(button, selectorLayout, d) { - var rect = button.selectAll('rect') - .data([0]); + var rect = button.selectAll("rect").data([0]); - rect.enter().append('rect') - .classed('selector-rect', true); + rect.enter().append("rect").classed("selector-rect", true); - rect.attr('shape-rendering', 'crispEdges'); + rect.attr("shape-rendering", "crispEdges"); - rect.attr({ - 'rx': constants.rx, - 'ry': constants.ry - }); + rect.attr({ rx: constants.rx, ry: constants.ry }); - rect.call(Color.stroke, selectorLayout.bordercolor) - .call(Color.fill, getFillColor(selectorLayout, d)) - .style('stroke-width', selectorLayout.borderwidth + 'px'); + rect + .call(Color.stroke, selectorLayout.bordercolor) + .call(Color.fill, getFillColor(selectorLayout, d)) + .style("stroke-width", selectorLayout.borderwidth + "px"); } function getFillColor(selectorLayout, d) { - return (d.isActive || d.isHovered) ? - selectorLayout.activecolor : - selectorLayout.bgcolor; + return d.isActive || d.isHovered + ? selectorLayout.activecolor + : selectorLayout.bgcolor; } function drawButtonText(button, selectorLayout, d) { - function textLayout(s) { - svgTextUtils.convertToTspans(s); + function textLayout(s) { + svgTextUtils.convertToTspans(s); + // TODO do we need anything else here? + } - // TODO do we need anything else here? - } + var text = button.selectAll("text").data([0]); - var text = button.selectAll('text') - .data([0]); + text + .enter() + .append("text") + .classed("selector-text", true) + .classed("user-select-none", true); - text.enter().append('text') - .classed('selector-text', true) - .classed('user-select-none', true); + text.attr("text-anchor", "middle"); - text.attr('text-anchor', 'middle'); - - text.call(Drawing.font, selectorLayout.font) - .text(getLabel(d)) - .call(textLayout); + text + .call(Drawing.font, selectorLayout.font) + .text(getLabel(d)) + .call(textLayout); } function getLabel(opts) { - if(opts.label) return opts.label; + if (opts.label) return opts.label; - if(opts.step === 'all') return 'all'; + if (opts.step === "all") return "all"; - return opts.count + opts.step.charAt(0); + return opts.count + opts.step.charAt(0); } function reposition(gd, buttons, opts, axName) { - opts.width = 0; - opts.height = 0; - - var borderWidth = opts.borderwidth; - - buttons.each(function() { - var button = d3.select(this), - text = button.select('.selector-text'), - tspans = text.selectAll('tspan'); - - var tHeight = opts.font.size * 1.3, - tLines = tspans[0].length || 1, - hEff = Math.max(tHeight * tLines, 16) + 3; - - opts.height = Math.max(opts.height, hEff); - }); - - buttons.each(function() { - var button = d3.select(this), - rect = button.select('.selector-rect'), - text = button.select('.selector-text'), - tspans = text.selectAll('tspan'); - - var tWidth = text.node() && Drawing.bBox(text.node()).width, - tHeight = opts.font.size * 1.3, - tLines = tspans[0].length || 1; - - var wEff = Math.max(tWidth + 10, constants.minButtonWidth); - - // TODO add MathJax support - - // TODO add buttongap attribute - - button.attr('transform', 'translate(' + - (borderWidth + opts.width) + ',' + borderWidth + - ')'); - - rect.attr({ - x: 0, - y: 0, - width: wEff, - height: opts.height - }); - - var textAttrs = { - x: wEff / 2, - y: opts.height / 2 - ((tLines - 1) * tHeight / 2) + 3 - }; - - text.attr(textAttrs); - tspans.attr(textAttrs); - - opts.width += wEff + 5; - }); - - buttons.selectAll('rect').attr('height', opts.height); - - var graphSize = gd._fullLayout._size; - opts.lx = graphSize.l + graphSize.w * opts.x; - opts.ly = graphSize.t + graphSize.h * (1 - opts.y); - - var xanchor = 'left'; - if(anchorUtils.isRightAnchor(opts)) { - opts.lx -= opts.width; - xanchor = 'right'; - } - if(anchorUtils.isCenterAnchor(opts)) { - opts.lx -= opts.width / 2; - xanchor = 'center'; - } - - var yanchor = 'top'; - if(anchorUtils.isBottomAnchor(opts)) { - opts.ly -= opts.height; - yanchor = 'bottom'; - } - if(anchorUtils.isMiddleAnchor(opts)) { - opts.ly -= opts.height / 2; - yanchor = 'middle'; - } - - opts.width = Math.ceil(opts.width); - opts.height = Math.ceil(opts.height); - opts.lx = Math.round(opts.lx); - opts.ly = Math.round(opts.ly); - - Plots.autoMargin(gd, axName + '-range-selector', { - x: opts.x, - y: opts.y, - l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0), - r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0), - b: opts.height * ({top: 1, middle: 0.5}[yanchor] || 0), - t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) - }); + opts.width = 0; + opts.height = 0; + + var borderWidth = opts.borderwidth; + + buttons.each(function() { + var button = d3.select(this), + text = button.select(".selector-text"), + tspans = text.selectAll("tspan"); + + var tHeight = opts.font.size * 1.3, + tLines = tspans[0].length || 1, + hEff = Math.max(tHeight * tLines, 16) + 3; + + opts.height = Math.max(opts.height, hEff); + }); + + buttons.each(function() { + var button = d3.select(this), + rect = button.select(".selector-rect"), + text = button.select(".selector-text"), + tspans = text.selectAll("tspan"); + + var tWidth = text.node() && Drawing.bBox(text.node()).width, + tHeight = opts.font.size * 1.3, + tLines = tspans[0].length || 1; + + var wEff = Math.max(tWidth + 10, constants.minButtonWidth); + + // TODO add MathJax support + // TODO add buttongap attribute + button.attr( + "transform", + "translate(" + (borderWidth + opts.width) + "," + borderWidth + ")" + ); + + rect.attr({ x: 0, y: 0, width: wEff, height: opts.height }); + + var textAttrs = { + x: wEff / 2, + y: opts.height / 2 - (tLines - 1) * tHeight / 2 + 3 + }; + + text.attr(textAttrs); + tspans.attr(textAttrs); + + opts.width += wEff + 5; + }); + + buttons.selectAll("rect").attr("height", opts.height); + + var graphSize = gd._fullLayout._size; + opts.lx = graphSize.l + graphSize.w * opts.x; + opts.ly = graphSize.t + graphSize.h * (1 - opts.y); + + var xanchor = "left"; + if (anchorUtils.isRightAnchor(opts)) { + opts.lx -= opts.width; + xanchor = "right"; + } + if (anchorUtils.isCenterAnchor(opts)) { + opts.lx -= opts.width / 2; + xanchor = "center"; + } + + var yanchor = "top"; + if (anchorUtils.isBottomAnchor(opts)) { + opts.ly -= opts.height; + yanchor = "bottom"; + } + if (anchorUtils.isMiddleAnchor(opts)) { + opts.ly -= opts.height / 2; + yanchor = "middle"; + } + + opts.width = Math.ceil(opts.width); + opts.height = Math.ceil(opts.height); + opts.lx = Math.round(opts.lx); + opts.ly = Math.round(opts.ly); + + Plots.autoMargin(gd, axName + "-range-selector", { + x: opts.x, + y: opts.y, + l: opts.width * (({ right: 1, center: 0.5 })[xanchor] || 0), + r: opts.width * (({ left: 1, center: 0.5 })[xanchor] || 0), + b: opts.height * (({ top: 1, middle: 0.5 })[yanchor] || 0), + t: opts.height * (({ bottom: 1, middle: 0.5 })[yanchor] || 0) + }); } diff --git a/src/components/rangeselector/get_update_object.js b/src/components/rangeselector/get_update_object.js index e8fa28971c7..eafc64b456c 100644 --- a/src/components/rangeselector/get_update_object.js +++ b/src/components/rangeselector/get_update_object.js @@ -5,51 +5,46 @@ * 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 d3 = require('d3'); +"use strict"; +var d3 = require("d3"); module.exports = function getUpdateObject(axisLayout, buttonLayout) { - var axName = axisLayout._name; - var update = {}; + var axName = axisLayout._name; + var update = {}; - if(buttonLayout.step === 'all') { - update[axName + '.autorange'] = true; - } - else { - var xrange = getXRange(axisLayout, buttonLayout); + if (buttonLayout.step === "all") { + update[axName + ".autorange"] = true; + } else { + var xrange = getXRange(axisLayout, buttonLayout); - update[axName + '.range[0]'] = xrange[0]; - update[axName + '.range[1]'] = xrange[1]; - } + update[axName + ".range[0]"] = xrange[0]; + update[axName + ".range[1]"] = xrange[1]; + } - return update; + return update; }; function getXRange(axisLayout, buttonLayout) { - var currentRange = axisLayout.range; - var base = new Date(axisLayout.r2l(currentRange[1])); + var currentRange = axisLayout.range; + var base = new Date(axisLayout.r2l(currentRange[1])); - var step = buttonLayout.step, - count = buttonLayout.count; + var step = buttonLayout.step, count = buttonLayout.count; - var range0; + var range0; - switch(buttonLayout.stepmode) { - case 'backward': - range0 = axisLayout.l2r(+d3.time[step].utc.offset(base, -count)); - break; + switch (buttonLayout.stepmode) { + case "backward": + range0 = axisLayout.l2r(+d3.time[step].utc.offset(base, -count)); + break; - case 'todate': - var base2 = d3.time[step].utc.offset(base, -count); + case "todate": + var base2 = d3.time[step].utc.offset(base, -count); - range0 = axisLayout.l2r(+d3.time[step].utc.ceil(base2)); - break; - } + range0 = axisLayout.l2r(+d3.time[step].utc.ceil(base2)); + break; + } - var range1 = currentRange[1]; + var range1 = currentRange[1]; - return [range0, range1]; + return [range0, range1]; } diff --git a/src/components/rangeselector/index.js b/src/components/rangeselector/index.js index a4e3e7cfd58..945b46bad07 100644 --- a/src/components/rangeselector/index.js +++ b/src/components/rangeselector/index.js @@ -5,21 +5,12 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = { - moduleType: 'component', - name: 'rangeselector', - - schema: { - layout: { - 'xaxis.rangeselector': require('./attributes') - } - }, - - layoutAttributes: require('./attributes'), - handleDefaults: require('./defaults'), - - draw: require('./draw') + moduleType: "component", + name: "rangeselector", + schema: { layout: { "xaxis.rangeselector": require("./attributes") } }, + layoutAttributes: require("./attributes"), + handleDefaults: require("./defaults"), + draw: require("./draw") }; diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js index d81dfbfaecf..f02f1bcb745 100644 --- a/src/components/rangeslider/attributes.js +++ b/src/components/rangeslider/attributes.js @@ -5,69 +5,64 @@ * 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 colorAttributes = require('../color/attributes'); +"use strict"; +var colorAttributes = require("../color/attributes"); module.exports = { - bgcolor: { - valType: 'color', - dflt: colorAttributes.background, - role: 'style', - description: 'Sets the background color of the range slider.' - }, - bordercolor: { - valType: 'color', - dflt: colorAttributes.defaultLine, - role: 'style', - description: 'Sets the border color of the range slider.' - }, - borderwidth: { - valType: 'integer', - dflt: 0, - min: 0, - role: 'style', - description: 'Sets the border color of the range slider.' - }, - range: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'any'}, - {valType: 'any'} - ], - description: [ - 'Sets the range of the range slider.', - 'If not set, defaults to the full xaxis range.', - 'If the axis `type` is *log*, then you must take the', - 'log of your desired range.', - 'If the axis `type` is *date*, it should be date strings,', - 'like date data, though Date objects and unix milliseconds', - 'will be accepted and converted to strings.', - 'If the axis `type` is *category*, it should be numbers,', - 'using the scale where each category is assigned a serial', - 'number from zero in the order it appears.' - ].join(' ') - }, - thickness: { - valType: 'number', - dflt: 0.15, - min: 0, - max: 1, - role: 'style', - description: [ - 'The height of the range slider as a fraction of the', - 'total plot area height.' - ].join(' ') - }, - visible: { - valType: 'boolean', - dflt: true, - role: 'info', - description: [ - 'Determines whether or not the range slider will be visible.', - 'If visible, perpendicular axes will be set to `fixedrange`' - ].join(' ') - } + bgcolor: { + valType: "color", + dflt: colorAttributes.background, + role: "style", + description: "Sets the background color of the range slider." + }, + bordercolor: { + valType: "color", + dflt: colorAttributes.defaultLine, + role: "style", + description: "Sets the border color of the range slider." + }, + borderwidth: { + valType: "integer", + dflt: 0, + min: 0, + role: "style", + description: "Sets the border color of the range slider." + }, + range: { + valType: "info_array", + role: "info", + items: [{ valType: "any" }, { valType: "any" }], + description: [ + "Sets the range of the range slider.", + "If not set, defaults to the full xaxis range.", + "If the axis `type` is *log*, then you must take the", + "log of your desired range.", + "If the axis `type` is *date*, it should be date strings,", + "like date data, though Date objects and unix milliseconds", + "will be accepted and converted to strings.", + "If the axis `type` is *category*, it should be numbers,", + "using the scale where each category is assigned a serial", + "number from zero in the order it appears." + ].join(" ") + }, + thickness: { + valType: "number", + dflt: 0.15, + min: 0, + max: 1, + role: "style", + description: [ + "The height of the range slider as a fraction of the", + "total plot area height." + ].join(" ") + }, + visible: { + valType: "boolean", + dflt: true, + role: "info", + description: [ + "Determines whether or not the range slider will be visible.", + "If visible, perpendicular axes will be set to `fixedrange`" + ].join(" ") + } }; diff --git a/src/components/rangeslider/constants.js b/src/components/rangeslider/constants.js index 9adc42e6407..ce761a356fa 100644 --- a/src/components/rangeslider/constants.js +++ b/src/components/rangeslider/constants.js @@ -5,47 +5,34 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = { - - // attribute container name - name: 'rangeslider', - - // class names - - containerClassName: 'rangeslider-container', - bgClassName: 'rangeslider-bg', - rangePlotClassName: 'rangeslider-rangeplot', - - maskMinClassName: 'rangeslider-mask-min', - maskMaxClassName: 'rangeslider-mask-max', - slideBoxClassName: 'rangeslider-slidebox', - - grabberMinClassName: 'rangeslider-grabber-min', - grabAreaMinClassName: 'rangeslider-grabarea-min', - handleMinClassName: 'rangeslider-handle-min', - - grabberMaxClassName: 'rangeslider-grabber-max', - grabAreaMaxClassName: 'rangeslider-grabarea-max', - handleMaxClassName: 'rangeslider-handle-max', - - // style constants - - maskColor: 'rgba(0,0,0,0.4)', - - slideBoxFill: 'transparent', - slideBoxCursor: 'ew-resize', - - grabAreaFill: 'transparent', - grabAreaCursor: 'col-resize', - grabAreaWidth: 10, - grabAreaMinOffset: -6, - grabAreaMaxOffset: -2, - - handleWidth: 2, - handleRadius: 1, - handleFill: '#fff', - handleStroke: '#666', + // attribute container name + name: "rangeslider", + // class names + containerClassName: "rangeslider-container", + bgClassName: "rangeslider-bg", + rangePlotClassName: "rangeslider-rangeplot", + maskMinClassName: "rangeslider-mask-min", + maskMaxClassName: "rangeslider-mask-max", + slideBoxClassName: "rangeslider-slidebox", + grabberMinClassName: "rangeslider-grabber-min", + grabAreaMinClassName: "rangeslider-grabarea-min", + handleMinClassName: "rangeslider-handle-min", + grabberMaxClassName: "rangeslider-grabber-max", + grabAreaMaxClassName: "rangeslider-grabarea-max", + handleMaxClassName: "rangeslider-handle-max", + // style constants + maskColor: "rgba(0,0,0,0.4)", + slideBoxFill: "transparent", + slideBoxCursor: "ew-resize", + grabAreaFill: "transparent", + grabAreaCursor: "col-resize", + grabAreaWidth: 10, + grabAreaMinOffset: -6, + grabAreaMaxOffset: -2, + handleWidth: 2, + handleRadius: 1, + handleFill: "#fff", + handleStroke: "#666" }; diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 379a4596b84..21fec917766 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -5,48 +5,48 @@ * 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'); - +"use strict"; +var Lib = require("../../lib"); +var attributes = require("./attributes"); module.exports = function handleDefaults(layoutIn, layoutOut, axName) { - if(!layoutIn[axName].rangeslider) return; - - // not super proud of this (maybe store _ in axis object instead - if(!Lib.isPlainObject(layoutIn[axName].rangeslider)) { - layoutIn[axName].rangeslider = {}; - } - - var containerIn = layoutIn[axName].rangeslider, - axOut = layoutOut[axName], - containerOut = axOut.rangeslider = {}; - - function coerce(attr, dflt) { - return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); - } - - coerce('bgcolor', layoutOut.plot_bgcolor); - coerce('bordercolor'); - coerce('borderwidth'); - coerce('thickness'); - coerce('visible'); - coerce('range'); - - // Expand slider range to the axis range - if(containerOut.range && !axOut.autorange) { - // TODO: what if the ranges are reversed? - var outRange = containerOut.range, - axRange = axOut.range; - - outRange[0] = axOut.l2r(Math.min(axOut.r2l(outRange[0]), axOut.r2l(axRange[0]))); - outRange[1] = axOut.l2r(Math.max(axOut.r2l(outRange[1]), axOut.r2l(axRange[1]))); - } else { - axOut._needsExpand = true; - } - - // to map back range slider (auto) range - containerOut._input = containerIn; + if (!layoutIn[axName].rangeslider) return; + + // not super proud of this (maybe store _ in axis object instead + if (!Lib.isPlainObject(layoutIn[axName].rangeslider)) { + layoutIn[axName].rangeslider = {}; + } + + var containerIn = layoutIn[axName].rangeslider, + axOut = layoutOut[axName], + containerOut = axOut.rangeslider = {}; + + function coerce(attr, dflt) { + return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); + } + + coerce("bgcolor", layoutOut.plot_bgcolor); + coerce("bordercolor"); + coerce("borderwidth"); + coerce("thickness"); + coerce("visible"); + coerce("range"); + + // Expand slider range to the axis range + if (containerOut.range && !axOut.autorange) { + // TODO: what if the ranges are reversed? + var outRange = containerOut.range, axRange = axOut.range; + + outRange[0] = axOut.l2r( + Math.min(axOut.r2l(outRange[0]), axOut.r2l(axRange[0])) + ); + outRange[1] = axOut.l2r( + Math.max(axOut.r2l(outRange[1]), axOut.r2l(axRange[1])) + ); + } else { + axOut._needsExpand = true; + } + + // to map back range slider (auto) range + containerOut._input = containerIn; }; diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index ded3198d316..5c7acf9b850 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -5,32 +5,29 @@ * 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 d3 = require("d3"); -'use strict'; +var Plotly = require("../../plotly"); +var Plots = require("../../plots/plots"); -var d3 = require('d3'); +var Lib = require("../../lib"); +var Drawing = require("../drawing"); +var Color = require("../color"); -var Plotly = require('../../plotly'); -var Plots = require('../../plots/plots'); +var Cartesian = require("../../plots/cartesian"); +var Axes = require("../../plots/cartesian/axes"); -var Lib = require('../../lib'); -var Drawing = require('../drawing'); -var Color = require('../color'); - -var Cartesian = require('../../plots/cartesian'); -var Axes = require('../../plots/cartesian/axes'); - -var dragElement = require('../dragelement'); -var setCursor = require('../../lib/setcursor'); - -var constants = require('./constants'); +var dragElement = require("../dragelement"); +var setCursor = require("../../lib/setcursor"); +var constants = require("./constants"); module.exports = function(gd) { - var fullLayout = gd._fullLayout, - rangeSliderData = makeRangeSliderData(fullLayout); + var fullLayout = gd._fullLayout, + rangeSliderData = makeRangeSliderData(fullLayout); - /* + /* * * * < .... range plot /> @@ -46,484 +43,489 @@ module.exports = function(gd) { * * ... */ - - function keyFunction(axisOpts) { - return axisOpts._name; + function keyFunction(axisOpts) { + return axisOpts._name; + } + + var rangeSliders = fullLayout._infolayer + .selectAll("g." + constants.containerClassName) + .data(rangeSliderData, keyFunction); + + rangeSliders + .enter() + .append("g") + .classed(constants.containerClassName, true) + .attr("pointer-events", "all"); + + // remove exiting sliders and their corresponding clip paths + rangeSliders.exit().each(function(axisOpts) { + var rangeSlider = d3.select(this), opts = axisOpts[constants.name]; + + rangeSlider.remove(); + fullLayout._topdefs.select("#" + opts._clipId).remove(); + }); + + // remove push margin object(s) + if (rangeSliders.exit().size()) clearPushMargins(gd); + + // return early if no range slider is visible + if (rangeSliderData.length === 0) return; + + // for all present range sliders + rangeSliders.each(function(axisOpts) { + var rangeSlider = d3.select(this), opts = axisOpts[constants.name]; + + // compute new slider range using axis autorange if necessary + // copy back range to input range slider container to skip + // this step in subsequent draw calls + if (!opts.range) { + opts._input.range = opts.range = Axes.getAutoRange(axisOpts); } - var rangeSliders = fullLayout._infolayer - .selectAll('g.' + constants.containerClassName) - .data(rangeSliderData, keyFunction); - - rangeSliders.enter().append('g') - .classed(constants.containerClassName, true) - .attr('pointer-events', 'all'); - - // remove exiting sliders and their corresponding clip paths - rangeSliders.exit().each(function(axisOpts) { - var rangeSlider = d3.select(this), - opts = axisOpts[constants.name]; - - rangeSlider.remove(); - fullLayout._topdefs.select('#' + opts._clipId).remove(); - }); - - // remove push margin object(s) - if(rangeSliders.exit().size()) clearPushMargins(gd); - - // return early if no range slider is visible - if(rangeSliderData.length === 0) return; - - // for all present range sliders - rangeSliders.each(function(axisOpts) { - var rangeSlider = d3.select(this), - opts = axisOpts[constants.name]; - - // compute new slider range using axis autorange if necessary - // copy back range to input range slider container to skip - // this step in subsequent draw calls - if(!opts.range) { - opts._input.range = opts.range = Axes.getAutoRange(axisOpts); - } - - // update range slider dimensions - - var margin = fullLayout.margin, - graphSize = fullLayout._size, - domain = axisOpts.domain; - - opts._id = constants.name + axisOpts._id; - opts._clipId = opts._id + '-' + fullLayout._uid; - - opts._width = graphSize.w * (domain[1] - domain[0]); - opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness; - opts._offsetShift = Math.floor(opts.borderwidth / 2); - - var x = margin.l + (graphSize.w * domain[0]), - y = fullLayout.height - opts._height - margin.b; - - rangeSlider.attr('transform', 'translate(' + x + ',' + y + ')'); - - // update data <--> pixel coordinate conversion methods + // update range slider dimensions + var margin = fullLayout.margin, + graphSize = fullLayout._size, + domain = axisOpts.domain; - var range0 = axisOpts.r2l(opts.range[0]), - range1 = axisOpts.r2l(opts.range[1]), - dist = range1 - range0; + opts._id = constants.name + axisOpts._id; + opts._clipId = opts._id + "-" + fullLayout._uid; - opts.p2d = function(v) { - return (v / opts._width) * dist + range0; - }; + opts._width = graphSize.w * (domain[1] - domain[0]); + opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness; + opts._offsetShift = Math.floor(opts.borderwidth / 2); - opts.d2p = function(v) { - return (v - range0) / dist * opts._width; - }; + var x = margin.l + graphSize.w * domain[0], + y = fullLayout.height - opts._height - margin.b; - opts._rl = [range0, range1]; + rangeSlider.attr("transform", "translate(" + x + "," + y + ")"); - // update inner nodes + // update data <--> pixel coordinate conversion methods + var range0 = axisOpts.r2l(opts.range[0]), + range1 = axisOpts.r2l(opts.range[1]), + dist = range1 - range0; - rangeSlider - .call(drawBg, gd, axisOpts, opts) - .call(addClipPath, gd, axisOpts, opts) - .call(drawRangePlot, gd, axisOpts, opts) - .call(drawMasks, gd, axisOpts, opts) - .call(drawSlideBox, gd, axisOpts, opts) - .call(drawGrabbers, gd, axisOpts, opts); - - // setup drag element - setupDragElement(rangeSlider, gd, axisOpts, opts); - - // update current range - setPixelRange(rangeSlider, gd, axisOpts, opts); - - // update margins + opts.p2d = function(v) { + return v / opts._width * dist + range0; + }; - var bb = axisOpts._boundingBox ? axisOpts._boundingBox.height : 0; + opts.d2p = function(v) { + return (v - range0) / dist * opts._width; + }; - Plots.autoMargin(gd, opts._id, { - x: 0, y: 0, l: 0, r: 0, t: 0, - b: opts._height + fullLayout.margin.b + bb, - pad: 15 + opts._offsetShift * 2 - }); + opts._rl = [range0, range1]; + + // update inner nodes + rangeSlider + .call(drawBg, gd, axisOpts, opts) + .call(addClipPath, gd, axisOpts, opts) + .call(drawRangePlot, gd, axisOpts, opts) + .call(drawMasks, gd, axisOpts, opts) + .call(drawSlideBox, gd, axisOpts, opts) + .call(drawGrabbers, gd, axisOpts, opts); + + // setup drag element + setupDragElement(rangeSlider, gd, axisOpts, opts); + + // update current range + setPixelRange(rangeSlider, gd, axisOpts, opts); + + // update margins + var bb = axisOpts._boundingBox ? axisOpts._boundingBox.height : 0; + + Plots.autoMargin(gd, opts._id, { + x: 0, + y: 0, + l: 0, + r: 0, + t: 0, + b: opts._height + fullLayout.margin.b + bb, + pad: 15 + opts._offsetShift * 2 }); + }); }; function makeRangeSliderData(fullLayout) { - if(!fullLayout.xaxis) return []; - if(!fullLayout.xaxis[constants.name]) return []; - if(!fullLayout.xaxis[constants.name].visible) return []; - if(fullLayout._has('gl2d')) return []; + if (!fullLayout.xaxis) return []; + if (!fullLayout.xaxis[constants.name]) return []; + if (!fullLayout.xaxis[constants.name].visible) return []; + if (fullLayout._has("gl2d")) return []; - return [fullLayout.xaxis]; + return [fullLayout.xaxis]; } function setupDragElement(rangeSlider, gd, axisOpts, opts) { - var slideBox = rangeSlider.select('rect.' + constants.slideBoxClassName).node(), - grabAreaMin = rangeSlider.select('rect.' + constants.grabAreaMinClassName).node(), - grabAreaMax = rangeSlider.select('rect.' + constants.grabAreaMaxClassName).node(); - - rangeSlider.on('mousedown', function() { - var event = d3.event, - target = event.target, - startX = event.clientX, - offsetX = startX - rangeSlider.node().getBoundingClientRect().left, - minVal = opts.d2p(axisOpts._rl[0]), - maxVal = opts.d2p(axisOpts._rl[1]); - - var dragCover = dragElement.coverSlip(); - - dragCover.addEventListener('mousemove', mouseMove); - dragCover.addEventListener('mouseup', mouseUp); - - function mouseMove(e) { - var delta = +e.clientX - startX; - var pixelMin, pixelMax, cursor; - - switch(target) { - case slideBox: - cursor = 'ew-resize'; - pixelMin = minVal + delta; - pixelMax = maxVal + delta; - break; - - case grabAreaMin: - cursor = 'col-resize'; - pixelMin = minVal + delta; - pixelMax = maxVal; - break; - - case grabAreaMax: - cursor = 'col-resize'; - pixelMin = minVal; - pixelMax = maxVal + delta; - break; - - default: - cursor = 'ew-resize'; - pixelMin = offsetX; - pixelMax = offsetX + delta; - break; - } - - if(pixelMax < pixelMin) { - var tmp = pixelMax; - pixelMax = pixelMin; - pixelMin = tmp; - } - - opts._pixelMin = pixelMin; - opts._pixelMax = pixelMax; - - setCursor(d3.select(dragCover), cursor); - setDataRange(rangeSlider, gd, axisOpts, opts); - } - - function mouseUp() { - dragCover.removeEventListener('mousemove', mouseMove); - dragCover.removeEventListener('mouseup', mouseUp); - Lib.removeElement(dragCover); - } - }); + var slideBox = rangeSlider + .select("rect." + constants.slideBoxClassName) + .node(), + grabAreaMin = rangeSlider + .select("rect." + constants.grabAreaMinClassName) + .node(), + grabAreaMax = rangeSlider + .select("rect." + constants.grabAreaMaxClassName) + .node(); + + rangeSlider.on("mousedown", function() { + var event = d3.event, + target = event.target, + startX = event.clientX, + offsetX = startX - rangeSlider.node().getBoundingClientRect().left, + minVal = opts.d2p(axisOpts._rl[0]), + maxVal = opts.d2p(axisOpts._rl[1]); + + var dragCover = dragElement.coverSlip(); + + dragCover.addEventListener("mousemove", mouseMove); + dragCover.addEventListener("mouseup", mouseUp); + + function mouseMove(e) { + var delta = +e.clientX - startX; + var pixelMin, pixelMax, cursor; + + switch (target) { + case slideBox: + cursor = "ew-resize"; + pixelMin = minVal + delta; + pixelMax = maxVal + delta; + break; + + case grabAreaMin: + cursor = "col-resize"; + pixelMin = minVal + delta; + pixelMax = maxVal; + break; + + case grabAreaMax: + cursor = "col-resize"; + pixelMin = minVal; + pixelMax = maxVal + delta; + break; + + default: + cursor = "ew-resize"; + pixelMin = offsetX; + pixelMax = offsetX + delta; + break; + } + + if (pixelMax < pixelMin) { + var tmp = pixelMax; + pixelMax = pixelMin; + pixelMin = tmp; + } + + opts._pixelMin = pixelMin; + opts._pixelMax = pixelMax; + + setCursor(d3.select(dragCover), cursor); + setDataRange(rangeSlider, gd, axisOpts, opts); + } + + function mouseUp() { + dragCover.removeEventListener("mousemove", mouseMove); + dragCover.removeEventListener("mouseup", mouseUp); + Lib.removeElement(dragCover); + } + }); } function setDataRange(rangeSlider, gd, axisOpts, opts) { + function clamp(v) { + return axisOpts.l2r(Lib.constrain(v, opts._rl[0], opts._rl[1])); + } - function clamp(v) { - return axisOpts.l2r(Lib.constrain(v, opts._rl[0], opts._rl[1])); - } + var dataMin = clamp(opts.p2d(opts._pixelMin)), + dataMax = clamp(opts.p2d(opts._pixelMax)); - var dataMin = clamp(opts.p2d(opts._pixelMin)), - dataMax = clamp(opts.p2d(opts._pixelMax)); - - window.requestAnimationFrame(function() { - Plotly.relayout(gd, 'xaxis.range', [dataMin, dataMax]); - }); + window.requestAnimationFrame(function() { + Plotly.relayout(gd, "xaxis.range", [dataMin, dataMax]); + }); } function setPixelRange(rangeSlider, gd, axisOpts, opts) { - - function clamp(v) { - return Lib.constrain(v, 0, opts._width); - } - - var pixelMin = clamp(opts.d2p(axisOpts._rl[0])), - pixelMax = clamp(opts.d2p(axisOpts._rl[1])); - - rangeSlider.select('rect.' + constants.slideBoxClassName) - .attr('x', pixelMin) - .attr('width', pixelMax - pixelMin); - - rangeSlider.select('rect.' + constants.maskMinClassName) - .attr('width', pixelMin); - - rangeSlider.select('rect.' + constants.maskMaxClassName) - .attr('x', pixelMax) - .attr('width', opts._width - pixelMax); - - rangeSlider.select('g.' + constants.grabberMinClassName) - .attr('transform', 'translate(' + (pixelMin - constants.handleWidth - 1) + ',0)'); - - rangeSlider.select('g.' + constants.grabberMaxClassName) - .attr('transform', 'translate(' + pixelMax + ',0)'); + function clamp(v) { + return Lib.constrain(v, 0, opts._width); + } + + var pixelMin = clamp(opts.d2p(axisOpts._rl[0])), + pixelMax = clamp(opts.d2p(axisOpts._rl[1])); + + rangeSlider + .select("rect." + constants.slideBoxClassName) + .attr("x", pixelMin) + .attr("width", pixelMax - pixelMin); + + rangeSlider + .select("rect." + constants.maskMinClassName) + .attr("width", pixelMin); + + rangeSlider + .select("rect." + constants.maskMaxClassName) + .attr("x", pixelMax) + .attr("width", opts._width - pixelMax); + + rangeSlider + .select("g." + constants.grabberMinClassName) + .attr( + "transform", + "translate(" + (pixelMin - constants.handleWidth - 1) + ",0)" + ); + + rangeSlider + .select("g." + constants.grabberMaxClassName) + .attr("transform", "translate(" + pixelMax + ",0)"); } function drawBg(rangeSlider, gd, axisOpts, opts) { - var bg = rangeSlider.selectAll('rect.' + constants.bgClassName) - .data([0]); - - bg.enter().append('rect') - .classed(constants.bgClassName, true) - .attr({ - x: 0, - y: 0, - 'shape-rendering': 'crispEdges' - }); - - var borderCorrect = (opts.borderwidth % 2) === 0 ? - opts.borderwidth : - opts.borderwidth - 1; - - var offsetShift = -opts._offsetShift; - - bg.attr({ - width: opts._width + borderCorrect, - height: opts._height + borderCorrect, - transform: 'translate(' + offsetShift + ',' + offsetShift + ')', - fill: opts.bgcolor, - stroke: opts.bordercolor, - 'stroke-width': opts.borderwidth, - }); + var bg = rangeSlider.selectAll("rect." + constants.bgClassName).data([0]); + + bg + .enter() + .append("rect") + .classed(constants.bgClassName, true) + .attr({ x: 0, y: 0, "shape-rendering": "crispEdges" }); + + var borderCorrect = opts.borderwidth % 2 === 0 + ? opts.borderwidth + : opts.borderwidth - 1; + + var offsetShift = -opts._offsetShift; + + bg.attr({ + width: opts._width + borderCorrect, + height: opts._height + borderCorrect, + transform: "translate(" + offsetShift + "," + offsetShift + ")", + fill: opts.bgcolor, + stroke: opts.bordercolor, + "stroke-width": opts.borderwidth + }); } function addClipPath(rangeSlider, gd, axisOpts, opts) { - var fullLayout = gd._fullLayout; + var fullLayout = gd._fullLayout; - var clipPath = fullLayout._topdefs.selectAll('#' + opts._clipId) - .data([0]); + var clipPath = fullLayout._topdefs.selectAll("#" + opts._clipId).data([0]); - clipPath.enter().append('clipPath') - .attr('id', opts._clipId) - .append('rect') - .attr({ x: 0, y: 0 }); + clipPath + .enter() + .append("clipPath") + .attr("id", opts._clipId) + .append("rect") + .attr({ x: 0, y: 0 }); - clipPath.select('rect').attr({ - width: opts._width, - height: opts._height - }); + clipPath.select("rect").attr({ width: opts._width, height: opts._height }); } function drawRangePlot(rangeSlider, gd, axisOpts, opts) { - var subplotData = Axes.getSubplots(gd, axisOpts), - calcData = gd.calcdata; - - var rangePlots = rangeSlider.selectAll('g.' + constants.rangePlotClassName) - .data(subplotData, Lib.identity); - - rangePlots.enter().append('g') - .attr('class', function(id) { return constants.rangePlotClassName + ' ' + id; }) - .call(Drawing.setClipUrl, opts._clipId); - - rangePlots.order(); - - rangePlots.exit().remove(); - - var mainplotinfo; - - rangePlots.each(function(id, i) { - var plotgroup = d3.select(this), - isMainPlot = (i === 0); - - var oppAxisOpts = Axes.getFromId(gd, id, 'y'), - oppAxisName = oppAxisOpts._name; - - var mockFigure = { - data: [], - layout: { - xaxis: { - type: axisOpts.type, - domain: [0, 1], - range: opts.range.slice(), - calendar: axisOpts.calendar - }, - width: opts._width, - height: opts._height, - margin: { t: 0, b: 0, l: 0, r: 0 } - } - }; - - mockFigure.layout[oppAxisName] = { - domain: [0, 1], - range: oppAxisOpts.range.slice(), - calendar: oppAxisOpts.calendar - }; - - Plots.supplyDefaults(mockFigure); - - var xa = mockFigure._fullLayout.xaxis, - ya = mockFigure._fullLayout[oppAxisName]; - - var plotinfo = { - id: id, - plotgroup: plotgroup, - xaxis: xa, - yaxis: ya - }; - - if(isMainPlot) mainplotinfo = plotinfo; - else { - plotinfo.mainplot = 'xy'; - plotinfo.mainplotinfo = mainplotinfo; - } - - Cartesian.rangePlot(gd, plotinfo, filterRangePlotCalcData(calcData, id)); - - // no need for the bg layer, - // drawBg handles coloring the background - if(isMainPlot) plotinfo.bg.remove(); - }); -} + var subplotData = Axes.getSubplots(gd, axisOpts), calcData = gd.calcdata; -function filterRangePlotCalcData(calcData, subplotId) { - var out = []; + var rangePlots = rangeSlider + .selectAll("g." + constants.rangePlotClassName) + .data(subplotData, Lib.identity); - for(var i = 0; i < calcData.length; i++) { - var calcTrace = calcData[i], - trace = calcTrace[0].trace; + rangePlots + .enter() + .append("g") + .attr("class", function(id) { + return constants.rangePlotClassName + " " + id; + }) + .call(Drawing.setClipUrl, opts._clipId); - if(trace.xaxis + trace.yaxis === subplotId) { - out.push(calcTrace); - } - } + rangePlots.order(); - return out; -} + rangePlots.exit().remove(); -function drawMasks(rangeSlider, gd, axisOpts, opts) { - var maskMin = rangeSlider.selectAll('rect.' + constants.maskMinClassName) - .data([0]); + var mainplotinfo; - maskMin.enter().append('rect') - .classed(constants.maskMinClassName, true) - .attr({ x: 0, y: 0 }); + rangePlots.each(function(id, i) { + var plotgroup = d3.select(this), isMainPlot = i === 0; - maskMin - .attr('height', opts._height) - .call(Color.fill, constants.maskColor); + var oppAxisOpts = Axes.getFromId(gd, id, "y"), + oppAxisName = oppAxisOpts._name; - var maskMax = rangeSlider.selectAll('rect.' + constants.maskMaxClassName) - .data([0]); + var mockFigure = { + data: [], + layout: { + xaxis: { + type: axisOpts.type, + domain: [0, 1], + range: opts.range.slice(), + calendar: axisOpts.calendar + }, + width: opts._width, + height: opts._height, + margin: { t: 0, b: 0, l: 0, r: 0 } + } + }; + + mockFigure.layout[oppAxisName] = { + domain: [0, 1], + range: oppAxisOpts.range.slice(), + calendar: oppAxisOpts.calendar + }; - maskMax.enter().append('rect') - .classed(constants.maskMaxClassName, true) - .attr('y', 0); + Plots.supplyDefaults(mockFigure); + + var xa = mockFigure._fullLayout.xaxis, + ya = mockFigure._fullLayout[oppAxisName]; + + var plotinfo = { id: id, plotgroup: plotgroup, xaxis: xa, yaxis: ya }; + + if (isMainPlot) { + mainplotinfo = plotinfo; + } else { + plotinfo.mainplot = "xy"; + plotinfo.mainplotinfo = mainplotinfo; + } - maskMax - .attr('height', opts._height) - .call(Color.fill, constants.maskColor); + Cartesian.rangePlot(gd, plotinfo, filterRangePlotCalcData(calcData, id)); + + // no need for the bg layer, + // drawBg handles coloring the background + if (isMainPlot) plotinfo.bg.remove(); + }); } -function drawSlideBox(rangeSlider, gd, axisOpts, opts) { - if(gd._context.staticPlot) return; +function filterRangePlotCalcData(calcData, subplotId) { + var out = []; - var slideBox = rangeSlider.selectAll('rect.' + constants.slideBoxClassName) - .data([0]); + for (var i = 0; i < calcData.length; i++) { + var calcTrace = calcData[i], trace = calcTrace[0].trace; - slideBox.enter().append('rect') - .classed(constants.slideBoxClassName, true) - .attr('y', 0) - .attr('cursor', constants.slideBoxCursor); + if (trace.xaxis + trace.yaxis === subplotId) { + out.push(calcTrace); + } + } - slideBox.attr({ - height: opts._height, - fill: constants.slideBoxFill - }); + return out; } -function drawGrabbers(rangeSlider, gd, axisOpts, opts) { +function drawMasks(rangeSlider, gd, axisOpts, opts) { + var maskMin = rangeSlider + .selectAll("rect." + constants.maskMinClassName) + .data([0]); - // + maskMin + .enter() + .append("rect") + .classed(constants.maskMinClassName, true) + .attr({ x: 0, y: 0 }); - var grabberMin = rangeSlider.selectAll('g.' + constants.grabberMinClassName) - .data([0]); - grabberMin.enter().append('g') - .classed(constants.grabberMinClassName, true); + maskMin.attr("height", opts._height).call(Color.fill, constants.maskColor); - var grabberMax = rangeSlider.selectAll('g.' + constants.grabberMaxClassName) - .data([0]); - grabberMax.enter().append('g') - .classed(constants.grabberMaxClassName, true); + var maskMax = rangeSlider + .selectAll("rect." + constants.maskMaxClassName) + .data([0]); - // + maskMax + .enter() + .append("rect") + .classed(constants.maskMaxClassName, true) + .attr("y", 0); - var handleFixAttrs = { - x: 0, - width: constants.handleWidth, - rx: constants.handleRadius, - fill: constants.handleFill, - stroke: constants.handleStroke, - 'shape-rendering': 'crispEdges' - }; + maskMax.attr("height", opts._height).call(Color.fill, constants.maskColor); +} - var handleDynamicAttrs = { - y: opts._height / 4, - height: opts._height / 2, - }; +function drawSlideBox(rangeSlider, gd, axisOpts, opts) { + if (gd._context.staticPlot) return; - var handleMin = grabberMin.selectAll('rect.' + constants.handleMinClassName) - .data([0]); - handleMin.enter().append('rect') - .classed(constants.handleMinClassName, true) - .attr(handleFixAttrs); - handleMin.attr(handleDynamicAttrs); - - var handleMax = grabberMax.selectAll('rect.' + constants.handleMaxClassName) - .data([0]); - handleMax.enter().append('rect') - .classed(constants.handleMaxClassName, true) - .attr(handleFixAttrs); - handleMax.attr(handleDynamicAttrs); - - // - - if(gd._context.staticPlot) return; - - var grabAreaFixAttrs = { - width: constants.grabAreaWidth, - y: 0, - fill: constants.grabAreaFill, - cursor: constants.grabAreaCursor - }; + var slideBox = rangeSlider + .selectAll("rect." + constants.slideBoxClassName) + .data([0]); - var grabAreaMin = grabberMin.selectAll('rect.' + constants.grabAreaMinClassName) - .data([0]); - grabAreaMin.enter().append('rect') - .classed(constants.grabAreaMinClassName, true) - .attr(grabAreaFixAttrs); - grabAreaMin.attr({ - x: constants.grabAreaMinOffset, - height: opts._height - }); + slideBox + .enter() + .append("rect") + .classed(constants.slideBoxClassName, true) + .attr("y", 0) + .attr("cursor", constants.slideBoxCursor); - var grabAreaMax = grabberMax.selectAll('rect.' + constants.grabAreaMaxClassName) - .data([0]); - grabAreaMax.enter().append('rect') - .classed(constants.grabAreaMaxClassName, true) - .attr(grabAreaFixAttrs); - grabAreaMax.attr({ - x: constants.grabAreaMaxOffset, - height: opts._height - }); + slideBox.attr({ height: opts._height, fill: constants.slideBoxFill }); +} + +function drawGrabbers(rangeSlider, gd, axisOpts, opts) { + // + var grabberMin = rangeSlider + .selectAll("g." + constants.grabberMinClassName) + .data([0]); + grabberMin.enter().append("g").classed(constants.grabberMinClassName, true); + + var grabberMax = rangeSlider + .selectAll("g." + constants.grabberMaxClassName) + .data([0]); + grabberMax.enter().append("g").classed(constants.grabberMaxClassName, true); + + // + var handleFixAttrs = { + x: 0, + width: constants.handleWidth, + rx: constants.handleRadius, + fill: constants.handleFill, + stroke: constants.handleStroke, + "shape-rendering": "crispEdges" + }; + + var handleDynamicAttrs = { y: opts._height / 4, height: opts._height / 2 }; + + var handleMin = grabberMin + .selectAll("rect." + constants.handleMinClassName) + .data([0]); + handleMin + .enter() + .append("rect") + .classed(constants.handleMinClassName, true) + .attr(handleFixAttrs); + handleMin.attr(handleDynamicAttrs); + + var handleMax = grabberMax + .selectAll("rect." + constants.handleMaxClassName) + .data([0]); + handleMax + .enter() + .append("rect") + .classed(constants.handleMaxClassName, true) + .attr(handleFixAttrs); + handleMax.attr(handleDynamicAttrs); + + // + if (gd._context.staticPlot) return; + + var grabAreaFixAttrs = { + width: constants.grabAreaWidth, + y: 0, + fill: constants.grabAreaFill, + cursor: constants.grabAreaCursor + }; + + var grabAreaMin = grabberMin + .selectAll("rect." + constants.grabAreaMinClassName) + .data([0]); + grabAreaMin + .enter() + .append("rect") + .classed(constants.grabAreaMinClassName, true) + .attr(grabAreaFixAttrs); + grabAreaMin.attr({ x: constants.grabAreaMinOffset, height: opts._height }); + + var grabAreaMax = grabberMax + .selectAll("rect." + constants.grabAreaMaxClassName) + .data([0]); + grabAreaMax + .enter() + .append("rect") + .classed(constants.grabAreaMaxClassName, true) + .attr(grabAreaFixAttrs); + grabAreaMax.attr({ x: constants.grabAreaMaxOffset, height: opts._height }); } function clearPushMargins(gd) { - var pushMargins = gd._fullLayout._pushmargin || {}, - keys = Object.keys(pushMargins); + var pushMargins = gd._fullLayout._pushmargin || {}, + keys = Object.keys(pushMargins); - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; - if(k.indexOf(constants.name) !== -1) { - Plots.autoMargin(gd, k); - } + if (k.indexOf(constants.name) !== -1) { + Plots.autoMargin(gd, k); } + } } diff --git a/src/components/rangeslider/index.js b/src/components/rangeslider/index.js index 2d29e3b16fd..2181ff19749 100644 --- a/src/components/rangeslider/index.js +++ b/src/components/rangeslider/index.js @@ -5,21 +5,12 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = { - moduleType: 'component', - name: 'rangeslider', - - schema: { - layout: { - 'xaxis.rangeslider': require('./attributes') - } - }, - - layoutAttributes: require('./attributes'), - handleDefaults: require('./defaults'), - - draw: require('./draw') + moduleType: "component", + name: "rangeslider", + schema: { layout: { "xaxis.rangeslider": require("./attributes") } }, + layoutAttributes: require("./attributes"), + handleDefaults: require("./defaults"), + draw: require("./draw") }; diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js index a8974f2825e..e2a2dcccaca 100644 --- a/src/components/shapes/attributes.js +++ b/src/components/shapes/attributes.js @@ -5,161 +5,142 @@ * 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 annAttrs = require('../annotations/attributes'); -var scatterAttrs = require('../../traces/scatter/attributes'); -var extendFlat = require('../../lib/extend').extendFlat; +"use strict"; +var annAttrs = require("../annotations/attributes"); +var scatterAttrs = require("../../traces/scatter/attributes"); +var extendFlat = require("../../lib/extend").extendFlat; var scatterLineAttrs = scatterAttrs.line; module.exports = { - _isLinkedToArray: 'shape', - - visible: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines whether or not this shape is visible.' - ].join(' ') - }, - - type: { - valType: 'enumerated', - values: ['circle', 'rect', 'path', 'line'], - role: 'info', - description: [ - 'Specifies the shape type to be drawn.', - - 'If *line*, a line is drawn from (`x0`,`y0`) to (`x1`,`y1`)', - - 'If *circle*, a circle is drawn from', - '((`x0`+`x1`)/2, (`y0`+`y1`)/2))', - 'with radius', - '(|(`x0`+`x1`)/2 - `x0`|, |(`y0`+`y1`)/2 -`y0`)|)', - - 'If *rect*, a rectangle is drawn linking', - '(`x0`,`y0`), (`x1`,`y0`), (`x1`,`y1`), (`x0`,`y1`), (`x0`,`y0`)', - - 'If *path*, draw a custom SVG path using `path`.' - ].join(' ') - }, - - layer: { - valType: 'enumerated', - values: ['below', 'above'], - dflt: 'above', - role: 'info', - description: 'Specifies whether shapes are drawn below or above traces.' - }, - - xref: extendFlat({}, annAttrs.xref, { - description: [ - 'Sets the shape\'s x coordinate axis.', - 'If set to an x axis id (e.g. *x* or *x2*), the `x` position', - 'refers to an x coordinate', - 'If set to *paper*, the `x` position refers to the distance from', - 'the left side of the plotting area in normalized coordinates', - 'where *0* (*1*) corresponds to the left (right) side.', - 'If the axis `type` is *log*, then you must take the', - 'log of your desired range.', - 'If the axis `type` is *date*, then you must convert', - 'the date to unix time in milliseconds.' - ].join(' ') - }), - x0: { - valType: 'any', - role: 'info', - description: [ - 'Sets the shape\'s starting x position.', - 'See `type` for more info.' - ].join(' ') - }, - x1: { - valType: 'any', - role: 'info', - description: [ - 'Sets the shape\'s end x position.', - 'See `type` for more info.' - ].join(' ') - }, - - yref: extendFlat({}, annAttrs.yref, { - description: [ - 'Sets the annotation\'s y coordinate axis.', - 'If set to an y axis id (e.g. *y* or *y2*), the `y` position', - 'refers to an y coordinate', - 'If set to *paper*, the `y` position refers to the distance from', - 'the bottom of the plotting area in normalized coordinates', - 'where *0* (*1*) corresponds to the bottom (top).' - ].join(' ') - }), - y0: { - valType: 'any', - role: 'info', - description: [ - 'Sets the shape\'s starting y position.', - 'See `type` for more info.' - ].join(' ') - }, - y1: { - valType: 'any', - role: 'info', - description: [ - 'Sets the shape\'s end y position.', - 'See `type` for more info.' - ].join(' ') - }, - - path: { - valType: 'string', - role: 'info', - description: [ - 'For `type` *path* - a valid SVG path but with the pixel values', - 'replaced by data values. There are a few restrictions / quirks', - 'only absolute instructions, not relative. So the allowed segments', - 'are: M, L, H, V, Q, C, T, S, and Z', - 'arcs (A) are not allowed because radius rx and ry are relative.', - - 'In the future we could consider supporting relative commands,', - 'but we would have to decide on how to handle date and log axes.', - 'Note that even as is, Q and C Bezier paths that are smooth on', - 'linear axes may not be smooth on log, and vice versa.', - 'no chained "polybezier" commands - specify the segment type for', - 'each one.', - - 'On category axes, values are numbers scaled to the serial numbers', - 'of categories because using the categories themselves there would', - 'be no way to describe fractional positions', - 'On data axes: because space and T are both normal components of path', - 'strings, we can\'t use either to separate date from time parts.', - 'Therefore we\'ll use underscore for this purpose:', - '2015-02-21_13:45:56.789' - ].join(' ') - }, - - opacity: { - valType: 'number', - min: 0, - max: 1, - dflt: 1, - role: 'info', - description: 'Sets the opacity of the shape.' - }, - line: { - color: scatterLineAttrs.color, - width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash, - role: 'info' - }, - fillcolor: { - valType: 'color', - dflt: 'rgba(0,0,0,0)', - role: 'info', - description: [ - 'Sets the color filling the shape\'s interior.' - ].join(' ') - } + _isLinkedToArray: "shape", + visible: { + valType: "boolean", + role: "info", + dflt: true, + description: ["Determines whether or not this shape is visible."].join(" ") + }, + type: { + valType: "enumerated", + values: ["circle", "rect", "path", "line"], + role: "info", + description: [ + "Specifies the shape type to be drawn.", + "If *line*, a line is drawn from (`x0`,`y0`) to (`x1`,`y1`)", + "If *circle*, a circle is drawn from", + "((`x0`+`x1`)/2, (`y0`+`y1`)/2))", + "with radius", + "(|(`x0`+`x1`)/2 - `x0`|, |(`y0`+`y1`)/2 -`y0`)|)", + "If *rect*, a rectangle is drawn linking", + "(`x0`,`y0`), (`x1`,`y0`), (`x1`,`y1`), (`x0`,`y1`), (`x0`,`y0`)", + "If *path*, draw a custom SVG path using `path`." + ].join(" ") + }, + layer: { + valType: "enumerated", + values: ["below", "above"], + dflt: "above", + role: "info", + description: "Specifies whether shapes are drawn below or above traces." + }, + xref: extendFlat({}, annAttrs.xref, { + description: [ + "Sets the shape's x coordinate axis.", + "If set to an x axis id (e.g. *x* or *x2*), the `x` position", + "refers to an x coordinate", + "If set to *paper*, the `x` position refers to the distance from", + "the left side of the plotting area in normalized coordinates", + "where *0* (*1*) corresponds to the left (right) side.", + "If the axis `type` is *log*, then you must take the", + "log of your desired range.", + "If the axis `type` is *date*, then you must convert", + "the date to unix time in milliseconds." + ].join(" ") + }), + x0: { + valType: "any", + role: "info", + description: [ + "Sets the shape's starting x position.", + "See `type` for more info." + ].join(" ") + }, + x1: { + valType: "any", + role: "info", + description: [ + "Sets the shape's end x position.", + "See `type` for more info." + ].join(" ") + }, + yref: extendFlat({}, annAttrs.yref, { + description: [ + "Sets the annotation's y coordinate axis.", + "If set to an y axis id (e.g. *y* or *y2*), the `y` position", + "refers to an y coordinate", + "If set to *paper*, the `y` position refers to the distance from", + "the bottom of the plotting area in normalized coordinates", + "where *0* (*1*) corresponds to the bottom (top)." + ].join(" ") + }), + y0: { + valType: "any", + role: "info", + description: [ + "Sets the shape's starting y position.", + "See `type` for more info." + ].join(" ") + }, + y1: { + valType: "any", + role: "info", + description: [ + "Sets the shape's end y position.", + "See `type` for more info." + ].join(" ") + }, + path: { + valType: "string", + role: "info", + description: [ + "For `type` *path* - a valid SVG path but with the pixel values", + "replaced by data values. There are a few restrictions / quirks", + "only absolute instructions, not relative. So the allowed segments", + "are: M, L, H, V, Q, C, T, S, and Z", + "arcs (A) are not allowed because radius rx and ry are relative.", + "In the future we could consider supporting relative commands,", + "but we would have to decide on how to handle date and log axes.", + "Note that even as is, Q and C Bezier paths that are smooth on", + "linear axes may not be smooth on log, and vice versa.", + 'no chained "polybezier" commands - specify the segment type for', + "each one.", + "On category axes, values are numbers scaled to the serial numbers", + "of categories because using the categories themselves there would", + "be no way to describe fractional positions", + "On data axes: because space and T are both normal components of path", + "strings, we can't use either to separate date from time parts.", + "Therefore we'll use underscore for this purpose:", + "2015-02-21_13:45:56.789" + ].join(" ") + }, + opacity: { + valType: "number", + min: 0, + max: 1, + dflt: 1, + role: "info", + description: "Sets the opacity of the shape." + }, + line: { + color: scatterLineAttrs.color, + width: scatterLineAttrs.width, + dash: scatterLineAttrs.dash, + role: "info" + }, + fillcolor: { + valType: "color", + dflt: "rgba(0,0,0,0)", + role: "info", + description: ["Sets the color filling the shape's interior."].join(" ") + } }; diff --git a/src/components/shapes/calc_autorange.js b/src/components/shapes/calc_autorange.js index 6f88b4aad96..a115725b2cb 100644 --- a/src/components/shapes/calc_autorange.js +++ b/src/components/shapes/calc_autorange.js @@ -5,71 +5,78 @@ * 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 Axes = require("../../plots/cartesian/axes"); - -'use strict'; - -var Lib = require('../../lib'); -var Axes = require('../../plots/cartesian/axes'); - -var constants = require('./constants'); -var helpers = require('./helpers'); - +var constants = require("./constants"); +var helpers = require("./helpers"); module.exports = function calcAutorange(gd) { - var fullLayout = gd._fullLayout, - shapeList = Lib.filterVisible(fullLayout.shapes); - - if(!shapeList.length || !gd._fullData.length) return; - - for(var i = 0; i < shapeList.length; i++) { - var shape = shapeList[i], - ppad = shape.line.width / 2; - - var ax, bounds; - - if(shape.xref !== 'paper') { - ax = Axes.getFromId(gd, shape.xref); - bounds = shapeBounds(ax, shape.x0, shape.x1, shape.path, constants.paramIsX); - if(bounds) Axes.expand(ax, bounds, {ppad: ppad}); - } + var fullLayout = gd._fullLayout, + shapeList = Lib.filterVisible(fullLayout.shapes); + + if (!shapeList.length || !gd._fullData.length) return; + + for (var i = 0; i < shapeList.length; i++) { + var shape = shapeList[i], ppad = shape.line.width / 2; + + var ax, bounds; + + if (shape.xref !== "paper") { + ax = Axes.getFromId(gd, shape.xref); + bounds = shapeBounds( + ax, + shape.x0, + shape.x1, + shape.path, + constants.paramIsX + ); + if (bounds) Axes.expand(ax, bounds, { ppad: ppad }); + } - if(shape.yref !== 'paper') { - ax = Axes.getFromId(gd, shape.yref); - bounds = shapeBounds(ax, shape.y0, shape.y1, shape.path, constants.paramIsY); - if(bounds) Axes.expand(ax, bounds, {ppad: ppad}); - } + if (shape.yref !== "paper") { + ax = Axes.getFromId(gd, shape.yref); + bounds = shapeBounds( + ax, + shape.y0, + shape.y1, + shape.path, + constants.paramIsY + ); + if (bounds) Axes.expand(ax, bounds, { ppad: ppad }); } + } }; function shapeBounds(ax, v0, v1, path, paramsToUse) { - var convertVal = (ax.type === 'category') ? Number : ax.d2c; - - if(v0 !== undefined) return [convertVal(v0), convertVal(v1)]; - if(!path) return; - - var min = Infinity, - max = -Infinity, - segments = path.match(constants.segmentRE), - i, - segment, - drawnParam, - params, - val; - - if(ax.type === 'date') convertVal = helpers.decodeDate(convertVal); - - for(i = 0; i < segments.length; i++) { - segment = segments[i]; - drawnParam = paramsToUse[segment.charAt(0)].drawn; - if(drawnParam === undefined) continue; - - params = segments[i].substr(1).match(constants.paramRE); - if(!params || params.length < drawnParam) continue; - - val = convertVal(params[drawnParam]); - if(val < min) min = val; - if(val > max) max = val; - } - if(max >= min) return [min, max]; + var convertVal = ax.type === "category" ? Number : ax.d2c; + + if (v0 !== undefined) return [convertVal(v0), convertVal(v1)]; + if (!path) return; + + var min = Infinity, + max = -Infinity, + segments = path.match(constants.segmentRE), + i, + segment, + drawnParam, + params, + val; + + if (ax.type === "date") convertVal = helpers.decodeDate(convertVal); + + for (i = 0; i < segments.length; i++) { + segment = segments[i]; + drawnParam = paramsToUse[segment.charAt(0)].drawn; + if (drawnParam === undefined) continue; + + params = segments[i].substr(1).match(constants.paramRE); + if (!params || params.length < drawnParam) continue; + + val = convertVal(params[drawnParam]); + if (val < min) min = val; + if (val > max) max = val; + } + if (max >= min) return [min, max]; } diff --git a/src/components/shapes/constants.js b/src/components/shapes/constants.js index e0c009ca84e..c83fe912421 100644 --- a/src/components/shapes/constants.js +++ b/src/components/shapes/constants.js @@ -5,58 +5,51 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; module.exports = { - segmentRE: /[MLHVQCTSZ][^MLHVQCTSZ]*/g, - paramRE: /[^\s,]+/g, - - // which numbers in each path segment are x (or y) values - // drawn is which param is a drawn point, as opposed to a - // control point (which doesn't count toward autorange. - // TODO: this means curved paths could extend beyond the - // autorange bounds. This is a bit tricky to get right - // unless we revert to bounding boxes, but perhaps there's - // a calculation we could do...) - paramIsX: { - M: {0: true, drawn: 0}, - L: {0: true, drawn: 0}, - H: {0: true, drawn: 0}, - V: {}, - Q: {0: true, 2: true, drawn: 2}, - C: {0: true, 2: true, 4: true, drawn: 4}, - T: {0: true, drawn: 0}, - S: {0: true, 2: true, drawn: 2}, - // A: {0: true, 5: true}, - Z: {} - }, - - paramIsY: { - M: {1: true, drawn: 1}, - L: {1: true, drawn: 1}, - H: {}, - V: {0: true, drawn: 0}, - Q: {1: true, 3: true, drawn: 3}, - C: {1: true, 3: true, 5: true, drawn: 5}, - T: {1: true, drawn: 1}, - S: {1: true, 3: true, drawn: 5}, - // A: {1: true, 6: true}, - Z: {} - }, - - numParams: { - M: 2, - L: 2, - H: 1, - V: 1, - Q: 4, - C: 6, - T: 2, - S: 4, - // A: 7, - Z: 0 - } + segmentRE: /[MLHVQCTSZ][^MLHVQCTSZ]*/g, + paramRE: /[^\s,]+/g, + // which numbers in each path segment are x (or y) values + // drawn is which param is a drawn point, as opposed to a + // control point (which doesn't count toward autorange. + // TODO: this means curved paths could extend beyond the + // autorange bounds. This is a bit tricky to get right + // unless we revert to bounding boxes, but perhaps there's + // a calculation we could do...) + paramIsX: { + M: { 0: true, drawn: 0 }, + L: { 0: true, drawn: 0 }, + H: { 0: true, drawn: 0 }, + V: {}, + Q: { 0: true, 2: true, drawn: 2 }, + C: { 0: true, 2: true, 4: true, drawn: 4 }, + T: { 0: true, drawn: 0 }, + S: { 0: true, 2: true, drawn: 2 }, + // A: {0: true, 5: true}, + Z: {} + }, + paramIsY: { + M: { 1: true, drawn: 1 }, + L: { 1: true, drawn: 1 }, + H: {}, + V: { 0: true, drawn: 0 }, + Q: { 1: true, 3: true, drawn: 3 }, + C: { 1: true, 3: true, 5: true, drawn: 5 }, + T: { 1: true, drawn: 1 }, + S: { 1: true, 3: true, drawn: 5 }, + // A: {1: true, 6: true}, + Z: {} + }, + numParams: { + M: 2, + L: 2, + H: 1, + V: 1, + Q: 4, + C: 6, + T: 2, + S: 4, + // A: 7, + Z: 0 + } }; diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index ceb3b4c0a1a..dc0848dfac2 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -5,19 +5,14 @@ * 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 handleArrayContainerDefaults = require('../../plots/array_container_defaults'); -var handleShapeDefaults = require('./shape_defaults'); - +"use strict"; +var handleArrayContainerDefaults = require( + "../../plots/array_container_defaults" +); +var handleShapeDefaults = require("./shape_defaults"); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { - var opts = { - name: 'shapes', - handleItemDefaults: handleShapeDefaults - }; + var opts = { name: "shapes", handleItemDefaults: handleShapeDefaults }; - handleArrayContainerDefaults(layoutIn, layoutOut, opts); + handleArrayContainerDefaults(layoutIn, layoutOut, opts); }; diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 314cd339f76..4437642731c 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -5,26 +5,22 @@ * 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 isNumeric = require("fast-isnumeric"); +var Plotly = require("../../plotly"); +var Lib = require("../../lib"); +var Axes = require("../../plots/cartesian/axes"); +var Color = require("../color"); +var Drawing = require("../drawing"); -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Plotly = require('../../plotly'); -var Lib = require('../../lib'); -var Axes = require('../../plots/cartesian/axes'); -var Color = require('../color'); -var Drawing = require('../drawing'); - -var dragElement = require('../dragelement'); -var setCursor = require('../../lib/setcursor'); - -var constants = require('./constants'); -var helpers = require('./helpers'); -var handleShapeDefaults = require('./shape_defaults'); -var supplyLayoutDefaults = require('./defaults'); +var dragElement = require("../dragelement"); +var setCursor = require("../../lib/setcursor"); +var constants = require("./constants"); +var helpers = require("./helpers"); +var handleShapeDefaults = require("./shape_defaults"); +var supplyLayoutDefaults = require("./defaults"); // Shapes are stored in gd.layout.shapes, an array of objects // index can point to one item in this array, @@ -34,532 +30,554 @@ var supplyLayoutDefaults = require('./defaults'); // or undefined to simply redraw // if opt is blank, val can be 'add' or a full options object to add a new // annotation at that point in the array, or 'remove' to delete this one - -module.exports = { - draw: draw, - drawOne: drawOne -}; +module.exports = { draw: draw, drawOne: drawOne }; function draw(gd) { - var fullLayout = gd._fullLayout; + var fullLayout = gd._fullLayout; - // Remove previous shapes before drawing new in shapes in fullLayout.shapes - fullLayout._shapeUpperLayer.selectAll('path').remove(); - fullLayout._shapeLowerLayer.selectAll('path').remove(); - fullLayout._shapeSubplotLayer.selectAll('path').remove(); + // Remove previous shapes before drawing new in shapes in fullLayout.shapes + fullLayout._shapeUpperLayer.selectAll("path").remove(); + fullLayout._shapeLowerLayer.selectAll("path").remove(); + fullLayout._shapeSubplotLayer.selectAll("path").remove(); - for(var i = 0; i < fullLayout.shapes.length; i++) { - if(fullLayout.shapes[i].visible) { - drawOne(gd, i); - } + for (var i = 0; i < fullLayout.shapes.length; i++) { + if (fullLayout.shapes[i].visible) { + drawOne(gd, i); } - - // may need to resurrect this if we put text (LaTeX) in shapes - // return Plots.previousPromises(gd); + } + // may need to resurrect this if we put text (LaTeX) in shapes + // return Plots.previousPromises(gd); } function drawOne(gd, index, opt, value) { - if(!isNumeric(index) || index === -1) { - - // no index provided - we're operating on ALL shapes - if(!index && Array.isArray(value)) { - replaceAllShapes(gd, value); - return; - } - else if(value === 'remove') { - deleteAllShapes(gd); - return; - } - else if(opt && value !== 'add') { - updateAllShapes(gd, opt, value); - return; - } - else { - // add a new empty annotation - index = gd._fullLayout.shapes.length; - gd._fullLayout.shapes.push({}); - } + if (!isNumeric(index) || index === -1) { + // no index provided - we're operating on ALL shapes + if (!index && Array.isArray(value)) { + replaceAllShapes(gd, value); + return; + } else if (value === "remove") { + deleteAllShapes(gd); + return; + } else if (opt && value !== "add") { + updateAllShapes(gd, opt, value); + return; + } else { + // add a new empty annotation + index = gd._fullLayout.shapes.length; + gd._fullLayout.shapes.push({}); } - - if(!opt && value) { - if(value === 'remove') { - deleteShape(gd, index); - return; - } - else if(value === 'add' || Lib.isPlainObject(value)) { - insertShape(gd, index, value); - } + } + + if (!opt && value) { + if (value === "remove") { + deleteShape(gd, index); + return; + } else if (value === "add" || Lib.isPlainObject(value)) { + insertShape(gd, index, value); } + } - updateShape(gd, index, opt, value); + updateShape(gd, index, opt, value); } function replaceAllShapes(gd, newShapes) { - gd.layout.shapes = newShapes; - supplyLayoutDefaults(gd.layout, gd._fullLayout); - draw(gd); + gd.layout.shapes = newShapes; + supplyLayoutDefaults(gd.layout, gd._fullLayout); + draw(gd); } function deleteAllShapes(gd) { - delete gd.layout.shapes; - gd._fullLayout.shapes = []; - draw(gd); + delete gd.layout.shapes; + gd._fullLayout.shapes = []; + draw(gd); } function updateAllShapes(gd, opt, value) { - for(var i = 0; i < gd._fullLayout.shapes.length; i++) { - drawOne(gd, i, opt, value); - } + for (var i = 0; i < gd._fullLayout.shapes.length; i++) { + drawOne(gd, i, opt, value); + } } function deleteShape(gd, index) { - getShapeLayer(gd, index) - .selectAll('[data-index="' + index + '"]') - .remove(); + getShapeLayer(gd, index).selectAll('[data-index="' + index + '"]').remove(); - gd._fullLayout.shapes.splice(index, 1); + gd._fullLayout.shapes.splice(index, 1); - gd.layout.shapes.splice(index, 1); + gd.layout.shapes.splice(index, 1); - for(var i = index; i < gd._fullLayout.shapes.length; i++) { - // redraw all shapes past the removed one, - // so they bind to the right events - getShapeLayer(gd, i) - .selectAll('[data-index="' + (i + 1) + '"]') - .attr('data-index', i); - drawOne(gd, i); - } + for (var i = index; i < gd._fullLayout.shapes.length; i++) { + // redraw all shapes past the removed one, + // so they bind to the right events + getShapeLayer(gd, i) + .selectAll('[data-index="' + (i + 1) + '"]') + .attr("data-index", i); + drawOne(gd, i); + } } function insertShape(gd, index, newShape) { - gd._fullLayout.shapes.splice(index, 0, {}); - - var rule = Lib.isPlainObject(newShape) ? - Lib.extendFlat({}, newShape) : - {text: 'New text'}; - - if(gd.layout.shapes) { - gd.layout.shapes.splice(index, 0, rule); - } else { - gd.layout.shapes = [rule]; - } - - // there is no need to call shapes.draw(gd, index), - // because updateShape() is called from within shapes.draw() - - for(var i = gd._fullLayout.shapes.length - 1; i > index; i--) { - getShapeLayer(gd, i) - .selectAll('[data-index="' + (i - 1) + '"]') - .attr('data-index', i); - drawOne(gd, i); - } + gd._fullLayout.shapes.splice(index, 0, {}); + + var rule = Lib.isPlainObject(newShape) + ? Lib.extendFlat({}, newShape) + : { text: "New text" }; + + if (gd.layout.shapes) { + gd.layout.shapes.splice(index, 0, rule); + } else { + gd.layout.shapes = [rule]; + } + + // there is no need to call shapes.draw(gd, index), + // because updateShape() is called from within shapes.draw() + for (var i = gd._fullLayout.shapes.length - 1; i > index; i--) { + getShapeLayer(gd, i) + .selectAll('[data-index="' + (i - 1) + '"]') + .attr("data-index", i); + drawOne(gd, i); + } } function updateShape(gd, index, opt, value) { - var i, n; - - // remove the existing shape if there is one - getShapeLayer(gd, index) - .selectAll('[data-index="' + index + '"]') - .remove(); - - // remember a few things about what was already there, - var optionsIn = gd.layout.shapes[index]; - - // (from annos...) not sure how we're getting here... but C12 is seeing a bug - // where we fail here when they add/remove annotations - // TODO: clean this up and remove it. - if(!optionsIn) return; - - // alter the input shape as requested - var optionsEdit = {}; - if(typeof opt === 'string' && opt) optionsEdit[opt] = value; - else if(Lib.isPlainObject(opt)) optionsEdit = opt; - - var optionKeys = Object.keys(optionsEdit); - for(i = 0; i < optionKeys.length; i++) { - var k = optionKeys[i]; - Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]); + var i, n; + + // remove the existing shape if there is one + getShapeLayer(gd, index).selectAll('[data-index="' + index + '"]').remove(); + + // remember a few things about what was already there, + var optionsIn = gd.layout.shapes[index]; + + // (from annos...) not sure how we're getting here... but C12 is seeing a bug + // where we fail here when they add/remove annotations + // TODO: clean this up and remove it. + if (!optionsIn) return; + + // alter the input shape as requested + var optionsEdit = {}; + if (typeof opt === "string" && opt) optionsEdit[opt] = value; + else if (Lib.isPlainObject(opt)) optionsEdit = opt; + + var optionKeys = Object.keys(optionsEdit); + for (i = 0; i < optionKeys.length; i++) { + var k = optionKeys[i]; + Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]); + } + + // return early in visible: false updates + if (optionsIn.visible === false) return; + + var oldRef = { xref: optionsIn.xref, yref: optionsIn.yref }, + posAttrs = ["x0", "x1", "y0", "y1"]; + + for (i = 0; i < 4; i++) { + var posAttr = posAttrs[i]; + // if we don't have an explicit position already, + // don't set one just because we're changing references + // or axis type. + // the defaults will be consistent most of the time anyway, + // except in log/linear changes + if ( + optionsEdit[posAttr] !== undefined || optionsIn[posAttr] === undefined + ) { + continue; } - // return early in visible: false updates - if(optionsIn.visible === false) return; - - var oldRef = {xref: optionsIn.xref, yref: optionsIn.yref}, - posAttrs = ['x0', 'x1', 'y0', 'y1']; - - for(i = 0; i < 4; i++) { - var posAttr = posAttrs[i]; - // if we don't have an explicit position already, - // don't set one just because we're changing references - // or axis type. - // the defaults will be consistent most of the time anyway, - // except in log/linear changes - if(optionsEdit[posAttr] !== undefined || - optionsIn[posAttr] === undefined) { - continue; - } - - var axLetter = posAttr.charAt(0), - axOld = Axes.getFromId(gd, - Axes.coerceRef(oldRef, {}, gd, axLetter, '', 'paper')), - axNew = Axes.getFromId(gd, - Axes.coerceRef(optionsIn, {}, gd, axLetter, '', 'paper')), - position = optionsIn[posAttr], - rangePosition; - - if(optionsEdit[axLetter + 'ref'] !== undefined) { - // first convert to fraction of the axis - if(axOld) { - rangePosition = helpers.shapePositionToRange(axOld)(position); - position = axOld.r2fraction(rangePosition); - } else { - position = (position - axNew.domain[0]) / - (axNew.domain[1] - axNew.domain[0]); - } - - if(axNew) { - // then convert to new data coordinates at the same fraction - rangePosition = axNew.fraction2r(position); - position = helpers.rangeToShapePosition(axNew)(rangePosition); - } else { - // or scale to the whole plot - position = axOld.domain[0] + - position * (axOld.domain[1] - axOld.domain[0]); - } - } - - optionsIn[posAttr] = position; + var axLetter = posAttr.charAt(0), + axOld = Axes.getFromId( + gd, + Axes.coerceRef(oldRef, {}, gd, axLetter, "", "paper") + ), + axNew = Axes.getFromId( + gd, + Axes.coerceRef(optionsIn, {}, gd, axLetter, "", "paper") + ), + position = optionsIn[posAttr], + rangePosition; + + if (optionsEdit[axLetter + "ref"] !== undefined) { + // first convert to fraction of the axis + if (axOld) { + rangePosition = helpers.shapePositionToRange(axOld)(position); + position = axOld.r2fraction(rangePosition); + } else { + position = (position - axNew.domain[0]) / + (axNew.domain[1] - axNew.domain[0]); + } + + if (axNew) { + // then convert to new data coordinates at the same fraction + rangePosition = axNew.fraction2r(position); + position = helpers.rangeToShapePosition(axNew)(rangePosition); + } else { + // or scale to the whole plot + position = axOld.domain[0] + + position * (axOld.domain[1] - axOld.domain[0]); + } } - var options = {}; - handleShapeDefaults(optionsIn, options, gd._fullLayout); - gd._fullLayout.shapes[index] = options; - - var clipAxes; - if(options.layer !== 'below') { - clipAxes = (options.xref + options.yref).replace(/paper/g, ''); - drawShape(gd._fullLayout._shapeUpperLayer); + optionsIn[posAttr] = position; + } + + var options = {}; + handleShapeDefaults(optionsIn, options, gd._fullLayout); + gd._fullLayout.shapes[index] = options; + + var clipAxes; + if (options.layer !== "below") { + clipAxes = (options.xref + options.yref).replace(/paper/g, ""); + drawShape(gd._fullLayout._shapeUpperLayer); + } else if (options.xref === "paper" && options.yref === "paper") { + clipAxes = ""; + drawShape(gd._fullLayout._shapeLowerLayer); + } else { + var plots = gd._fullLayout._plots || {}, + subplots = Object.keys(plots), + plotinfo; + + for (i = 0, n = subplots.length; i < n; i++) { + plotinfo = plots[subplots[i]]; + clipAxes = subplots[i]; + + if (isShapeInSubplot(gd, options, plotinfo)) { + drawShape(plotinfo.shapelayer); + } } - else if(options.xref === 'paper' && options.yref === 'paper') { - clipAxes = ''; - drawShape(gd._fullLayout._shapeLowerLayer); - } - else { - var plots = gd._fullLayout._plots || {}, - subplots = Object.keys(plots), - plotinfo; - - for(i = 0, n = subplots.length; i < n; i++) { - plotinfo = plots[subplots[i]]; - clipAxes = subplots[i]; - - if(isShapeInSubplot(gd, options, plotinfo)) { - drawShape(plotinfo.shapelayer); - } - } + } + + function drawShape(shapeLayer) { + var attrs = { + "data-index": index, + "fill-rule": "evenodd", + d: getPathString(gd, options) + }, + lineColor = options.line.width ? options.line.color : "rgba(0,0,0,0)"; + + var path = shapeLayer + .append("path") + .attr(attrs) + .style("opacity", options.opacity) + .call(Color.stroke, lineColor) + .call(Color.fill, options.fillcolor) + .call(Drawing.dashLine, options.line.dash, options.line.width); + + if (clipAxes) { + path.call(Drawing.setClipUrl, "clip" + gd._fullLayout._uid + clipAxes); } - function drawShape(shapeLayer) { - var attrs = { - 'data-index': index, - 'fill-rule': 'evenodd', - d: getPathString(gd, options) - }, - lineColor = options.line.width ? - options.line.color : 'rgba(0,0,0,0)'; - - var path = shapeLayer.append('path') - .attr(attrs) - .style('opacity', options.opacity) - .call(Color.stroke, lineColor) - .call(Color.fill, options.fillcolor) - .call(Drawing.dashLine, options.line.dash, options.line.width); - - if(clipAxes) { - path.call(Drawing.setClipUrl, - 'clip' + gd._fullLayout._uid + clipAxes); - } - - if(gd._context.editable) setupDragElement(gd, path, options, index); - } + if (gd._context.editable) setupDragElement(gd, path, options, index); + } } function setupDragElement(gd, shapePath, shapeOptions, index) { - var MINWIDTH = 10, - MINHEIGHT = 10; - - var update; - var x0, y0, x1, y1, astrX0, astrY0, astrX1, astrY1; - var n0, s0, w0, e0, astrN, astrS, astrW, astrE, optN, optS, optW, optE; - var pathIn, astrPath; - - var xa, ya, x2p, y2p, p2x, p2y; - - var dragOptions = { - setCursor: updateDragMode, - element: shapePath.node(), - prepFn: startDrag, - doneFn: endDrag - }, - dragBBox = dragOptions.element.getBoundingClientRect(), - dragMode; - - dragElement.init(dragOptions); - - function updateDragMode(evt) { - // choose 'move' or 'resize' - // based on initial position of cursor within the drag element - var w = dragBBox.right - dragBBox.left, - h = dragBBox.bottom - dragBBox.top, - x = evt.clientX - dragBBox.left, - y = evt.clientY - dragBBox.top, - cursor = (w > MINWIDTH && h > MINHEIGHT && !evt.shiftKey) ? - dragElement.getCursor(x / w, 1 - y / h) : - 'move'; - - setCursor(shapePath, cursor); - - // possible values 'move', 'sw', 'w', 'se', 'e', 'ne', 'n', 'nw' and 'w' - dragMode = cursor.split('-')[0]; - } - - function startDrag(evt) { - // setup conversion functions - xa = Axes.getFromId(gd, shapeOptions.xref); - ya = Axes.getFromId(gd, shapeOptions.yref); - - x2p = helpers.getDataToPixel(gd, xa); - y2p = helpers.getDataToPixel(gd, ya, true); - p2x = helpers.getPixelToData(gd, xa); - p2y = helpers.getPixelToData(gd, ya, true); - - // setup update strings and initial values - var astr = 'shapes[' + index + ']'; - if(shapeOptions.type === 'path') { - pathIn = shapeOptions.path; - astrPath = astr + '.path'; - } - else { - x0 = x2p(shapeOptions.x0); - y0 = y2p(shapeOptions.y0); - x1 = x2p(shapeOptions.x1); - y1 = y2p(shapeOptions.y1); - - astrX0 = astr + '.x0'; - astrY0 = astr + '.y0'; - astrX1 = astr + '.x1'; - astrY1 = astr + '.y1'; - } - - if(x0 < x1) { - w0 = x0; astrW = astr + '.x0'; optW = 'x0'; - e0 = x1; astrE = astr + '.x1'; optE = 'x1'; - } - else { - w0 = x1; astrW = astr + '.x1'; optW = 'x1'; - e0 = x0; astrE = astr + '.x0'; optE = 'x0'; - } - if(y0 < y1) { - n0 = y0; astrN = astr + '.y0'; optN = 'y0'; - s0 = y1; astrS = astr + '.y1'; optS = 'y1'; - } - else { - n0 = y1; astrN = astr + '.y1'; optN = 'y1'; - s0 = y0; astrS = astr + '.y0'; optS = 'y0'; - } - - update = {}; - - // setup dragMode and the corresponding handler - updateDragMode(evt); - dragOptions.moveFn = (dragMode === 'move') ? moveShape : resizeShape; + var MINWIDTH = 10, MINHEIGHT = 10; + + var update; + var x0, y0, x1, y1, astrX0, astrY0, astrX1, astrY1; + var n0, s0, w0, e0, astrN, astrS, astrW, astrE, optN, optS, optW, optE; + var pathIn, astrPath; + + var xa, ya, x2p, y2p, p2x, p2y; + + var dragOptions = { + setCursor: updateDragMode, + element: shapePath.node(), + prepFn: startDrag, + doneFn: endDrag + }, + dragBBox = dragOptions.element.getBoundingClientRect(), + dragMode; + + dragElement.init(dragOptions); + + function updateDragMode(evt) { + // choose 'move' or 'resize' + // based on initial position of cursor within the drag element + var w = dragBBox.right - dragBBox.left, + h = dragBBox.bottom - dragBBox.top, + x = evt.clientX - dragBBox.left, + y = evt.clientY - dragBBox.top, + cursor = w > MINWIDTH && h > MINHEIGHT && !evt.shiftKey + ? dragElement.getCursor(x / w, 1 - y / h) + : "move"; + + setCursor(shapePath, cursor); + + // possible values 'move', 'sw', 'w', 'se', 'e', 'ne', 'n', 'nw' and 'w' + dragMode = cursor.split("-")[0]; + } + + function startDrag(evt) { + // setup conversion functions + xa = Axes.getFromId(gd, shapeOptions.xref); + ya = Axes.getFromId(gd, shapeOptions.yref); + + x2p = helpers.getDataToPixel(gd, xa); + y2p = helpers.getDataToPixel(gd, ya, true); + p2x = helpers.getPixelToData(gd, xa); + p2y = helpers.getPixelToData(gd, ya, true); + + // setup update strings and initial values + var astr = "shapes[" + index + "]"; + if (shapeOptions.type === "path") { + pathIn = shapeOptions.path; + astrPath = astr + ".path"; + } else { + x0 = x2p(shapeOptions.x0); + y0 = y2p(shapeOptions.y0); + x1 = x2p(shapeOptions.x1); + y1 = y2p(shapeOptions.y1); + + astrX0 = astr + ".x0"; + astrY0 = astr + ".y0"; + astrX1 = astr + ".x1"; + astrY1 = astr + ".y1"; } - function endDrag(dragged) { - setCursor(shapePath); - if(dragged) { - Plotly.relayout(gd, update); - } + if (x0 < x1) { + w0 = x0; + astrW = astr + ".x0"; + optW = "x0"; + e0 = x1; + astrE = astr + ".x1"; + optE = "x1"; + } else { + w0 = x1; + astrW = astr + ".x1"; + optW = "x1"; + e0 = x0; + astrE = astr + ".x0"; + optE = "x0"; } - - function moveShape(dx, dy) { - if(shapeOptions.type === 'path') { - var moveX = function moveX(x) { return p2x(x2p(x) + dx); }; - if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX); - - var moveY = function moveY(y) { return p2y(y2p(y) + dy); }; - if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY); - - shapeOptions.path = movePath(pathIn, moveX, moveY); - update[astrPath] = shapeOptions.path; - } - else { - update[astrX0] = shapeOptions.x0 = p2x(x0 + dx); - update[astrY0] = shapeOptions.y0 = p2y(y0 + dy); - update[astrX1] = shapeOptions.x1 = p2x(x1 + dx); - update[astrY1] = shapeOptions.y1 = p2y(y1 + dy); - } - - shapePath.attr('d', getPathString(gd, shapeOptions)); + if (y0 < y1) { + n0 = y0; + astrN = astr + ".y0"; + optN = "y0"; + s0 = y1; + astrS = astr + ".y1"; + optS = "y1"; + } else { + n0 = y1; + astrN = astr + ".y1"; + optN = "y1"; + s0 = y0; + astrS = astr + ".y0"; + optS = "y0"; } - function resizeShape(dx, dy) { - if(shapeOptions.type === 'path') { - // TODO: implement path resize - var moveX = function moveX(x) { return p2x(x2p(x) + dx); }; - if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX); - - var moveY = function moveY(y) { return p2y(y2p(y) + dy); }; - if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY); - - shapeOptions.path = movePath(pathIn, moveX, moveY); - update[astrPath] = shapeOptions.path; - } - else { - var newN = (~dragMode.indexOf('n')) ? n0 + dy : n0, - newS = (~dragMode.indexOf('s')) ? s0 + dy : s0, - newW = (~dragMode.indexOf('w')) ? w0 + dx : w0, - newE = (~dragMode.indexOf('e')) ? e0 + dx : e0; - - if(newS - newN > MINHEIGHT) { - update[astrN] = shapeOptions[optN] = p2y(newN); - update[astrS] = shapeOptions[optS] = p2y(newS); - } - - if(newE - newW > MINWIDTH) { - update[astrW] = shapeOptions[optW] = p2x(newW); - update[astrE] = shapeOptions[optE] = p2x(newE); - } - } - - shapePath.attr('d', getPathString(gd, shapeOptions)); - } -} + update = {}; -function getShapeLayer(gd, index) { - var shape = gd._fullLayout.shapes[index], - shapeLayer = gd._fullLayout._shapeUpperLayer; + // setup dragMode and the corresponding handler + updateDragMode(evt); + dragOptions.moveFn = dragMode === "move" ? moveShape : resizeShape; + } - if(!shape) { - Lib.log('getShapeLayer: undefined shape: index', index); + function endDrag(dragged) { + setCursor(shapePath); + if (dragged) { + Plotly.relayout(gd, update); } - else if(shape.layer === 'below') { - shapeLayer = (shape.xref === 'paper' && shape.yref === 'paper') ? - gd._fullLayout._shapeLowerLayer : - gd._fullLayout._shapeSubplotLayer; + } + + function moveShape(dx, dy) { + if (shapeOptions.type === "path") { + var moveX = function moveX(x) { + return p2x(x2p(x) + dx); + }; + if (xa && xa.type === "date") moveX = helpers.encodeDate(moveX); + + var moveY = function moveY(y) { + return p2y(y2p(y) + dy); + }; + if (ya && ya.type === "date") moveY = helpers.encodeDate(moveY); + + shapeOptions.path = movePath(pathIn, moveX, moveY); + update[astrPath] = shapeOptions.path; + } else { + update[astrX0] = shapeOptions.x0 = p2x(x0 + dx); + update[astrY0] = shapeOptions.y0 = p2y(y0 + dy); + update[astrX1] = shapeOptions.x1 = p2x(x1 + dx); + update[astrY1] = shapeOptions.y1 = p2y(y1 + dy); } - return shapeLayer; -} + shapePath.attr("d", getPathString(gd, shapeOptions)); + } -function isShapeInSubplot(gd, shape, plotinfo) { - var xa = Axes.getFromId(gd, plotinfo.id, 'x')._id, - ya = Axes.getFromId(gd, plotinfo.id, 'y')._id, - isBelow = shape.layer === 'below', - inSuplotAxis = (xa === shape.xref || ya === shape.yref), - isNotAnOverlaidSubplot = !!plotinfo.shapelayer; - return isBelow && inSuplotAxis && isNotAnOverlaidSubplot; -} + function resizeShape(dx, dy) { + if (shapeOptions.type === "path") { + // TODO: implement path resize + var moveX = function moveX(x) { + return p2x(x2p(x) + dx); + }; + if (xa && xa.type === "date") moveX = helpers.encodeDate(moveX); -function getPathString(gd, options) { - var type = options.type, - xa = Axes.getFromId(gd, options.xref), - ya = Axes.getFromId(gd, options.yref), - gs = gd._fullLayout._size, - x2r, - x2p, - y2r, - y2p; - - if(xa) { - x2r = helpers.shapePositionToRange(xa); - x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); }; - } - else { - x2p = function(v) { return gs.l + gs.w * v; }; - } + var moveY = function moveY(y) { + return p2y(y2p(y) + dy); + }; + if (ya && ya.type === "date") moveY = helpers.encodeDate(moveY); - if(ya) { - y2r = helpers.shapePositionToRange(ya); - y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); }; - } - else { - y2p = function(v) { return gs.t + gs.h * (1 - v); }; + shapeOptions.path = movePath(pathIn, moveX, moveY); + update[astrPath] = shapeOptions.path; + } else { + var newN = ~dragMode.indexOf("n") ? n0 + dy : n0, + newS = ~dragMode.indexOf("s") ? s0 + dy : s0, + newW = ~dragMode.indexOf("w") ? w0 + dx : w0, + newE = ~dragMode.indexOf("e") ? e0 + dx : e0; + + if (newS - newN > MINHEIGHT) { + update[astrN] = shapeOptions[optN] = p2y(newN); + update[astrS] = shapeOptions[optS] = p2y(newS); + } + + if (newE - newW > MINWIDTH) { + update[astrW] = shapeOptions[optW] = p2x(newW); + update[astrE] = shapeOptions[optE] = p2x(newE); + } } - if(type === 'path') { - if(xa && xa.type === 'date') x2p = helpers.decodeDate(x2p); - if(ya && ya.type === 'date') y2p = helpers.decodeDate(y2p); - return convertPath(options.path, x2p, y2p); - } + shapePath.attr("d", getPathString(gd, shapeOptions)); + } +} + +function getShapeLayer(gd, index) { + var shape = gd._fullLayout.shapes[index], + shapeLayer = gd._fullLayout._shapeUpperLayer; + + if (!shape) { + Lib.log("getShapeLayer: undefined shape: index", index); + } else if (shape.layer === "below") { + shapeLayer = shape.xref === "paper" && shape.yref === "paper" + ? gd._fullLayout._shapeLowerLayer + : gd._fullLayout._shapeSubplotLayer; + } + + return shapeLayer; +} - var x0 = x2p(options.x0), - x1 = x2p(options.x1), - y0 = y2p(options.y0), - y1 = y2p(options.y1); - - if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1; - if(type === 'rect') return 'M' + x0 + ',' + y0 + 'H' + x1 + 'V' + y1 + 'H' + x0 + 'Z'; - // circle - var cx = (x0 + x1) / 2, - cy = (y0 + y1) / 2, - rx = Math.abs(cx - x0), - ry = Math.abs(cy - y0), - rArc = 'A' + rx + ',' + ry, - rightPt = (cx + rx) + ',' + cy, - topPt = cx + ',' + (cy - ry); - return 'M' + rightPt + rArc + ' 0 1,1 ' + topPt + - rArc + ' 0 0,1 ' + rightPt + 'Z'; +function isShapeInSubplot(gd, shape, plotinfo) { + var xa = Axes.getFromId(gd, plotinfo.id, "x")._id, + ya = Axes.getFromId(gd, plotinfo.id, "y")._id, + isBelow = shape.layer === "below", + inSuplotAxis = xa === shape.xref || ya === shape.yref, + isNotAnOverlaidSubplot = !!plotinfo.shapelayer; + return isBelow && inSuplotAxis && isNotAnOverlaidSubplot; } +function getPathString(gd, options) { + var type = options.type, + xa = Axes.getFromId(gd, options.xref), + ya = Axes.getFromId(gd, options.yref), + gs = gd._fullLayout._size, + x2r, + x2p, + y2r, + y2p; + + if (xa) { + x2r = helpers.shapePositionToRange(xa); + x2p = function(v) { + return xa._offset + xa.r2p(x2r(v, true)); + }; + } else { + x2p = function(v) { + return gs.l + gs.w * v; + }; + } + + if (ya) { + y2r = helpers.shapePositionToRange(ya); + y2p = function(v) { + return ya._offset + ya.r2p(y2r(v, true)); + }; + } else { + y2p = function(v) { + return gs.t + gs.h * (1 - v); + }; + } + + if (type === "path") { + if (xa && xa.type === "date") x2p = helpers.decodeDate(x2p); + if (ya && ya.type === "date") y2p = helpers.decodeDate(y2p); + return convertPath(options.path, x2p, y2p); + } + + var x0 = x2p(options.x0), + x1 = x2p(options.x1), + y0 = y2p(options.y0), + y1 = y2p(options.y1); + + if (type === "line") return "M" + x0 + "," + y0 + "L" + x1 + "," + y1; + if (type === "rect") { + return "M" + x0 + "," + y0 + "H" + x1 + "V" + y1 + "H" + x0 + "Z"; + } + // circle + var cx = (x0 + x1) / 2, + cy = (y0 + y1) / 2, + rx = Math.abs(cx - x0), + ry = Math.abs(cy - y0), + rArc = "A" + rx + "," + ry, + rightPt = cx + rx + "," + cy, + topPt = cx + "," + (cy - ry); + return "M" + + rightPt + + rArc + + " 0 1,1 " + + topPt + + rArc + + " 0 0,1 " + + rightPt + + "Z"; +} function convertPath(pathIn, x2p, y2p) { - // convert an SVG path string from data units to pixels - return pathIn.replace(constants.segmentRE, function(segment) { - var paramNumber = 0, - segmentType = segment.charAt(0), - xParams = constants.paramIsX[segmentType], - yParams = constants.paramIsY[segmentType], - nParams = constants.numParams[segmentType]; - - var paramString = segment.substr(1).replace(constants.paramRE, function(param) { - if(xParams[paramNumber]) param = x2p(param); - else if(yParams[paramNumber]) param = y2p(param); - paramNumber++; - - if(paramNumber > nParams) param = 'X'; - return param; - }); - - if(paramNumber > nParams) { - paramString = paramString.replace(/[\s,]*X.*/, ''); - Lib.log('Ignoring extra params in segment ' + segment); - } - - return segmentType + paramString; - }); + // convert an SVG path string from data units to pixels + return pathIn.replace(constants.segmentRE, function(segment) { + var paramNumber = 0, + segmentType = segment.charAt(0), + xParams = constants.paramIsX[segmentType], + yParams = constants.paramIsY[segmentType], + nParams = constants.numParams[segmentType]; + + var paramString = segment + .substr(1) + .replace(constants.paramRE, function(param) { + if (xParams[paramNumber]) param = x2p(param); + else if (yParams[paramNumber]) param = y2p(param); + paramNumber++; + + if (paramNumber > nParams) param = "X"; + return param; + }); + + if (paramNumber > nParams) { + paramString = paramString.replace(/[\s,]*X.*/, ""); + Lib.log("Ignoring extra params in segment " + segment); + } + + return segmentType + paramString; + }); } function movePath(pathIn, moveX, moveY) { - return pathIn.replace(constants.segmentRE, function(segment) { - var paramNumber = 0, - segmentType = segment.charAt(0), - xParams = constants.paramIsX[segmentType], - yParams = constants.paramIsY[segmentType], - nParams = constants.numParams[segmentType]; + return pathIn.replace(constants.segmentRE, function(segment) { + var paramNumber = 0, + segmentType = segment.charAt(0), + xParams = constants.paramIsX[segmentType], + yParams = constants.paramIsY[segmentType], + nParams = constants.numParams[segmentType]; - var paramString = segment.substr(1).replace(constants.paramRE, function(param) { - if(paramNumber >= nParams) return param; + var paramString = segment + .substr(1) + .replace(constants.paramRE, function(param) { + if (paramNumber >= nParams) return param; - if(xParams[paramNumber]) param = moveX(param); - else if(yParams[paramNumber]) param = moveY(param); + if (xParams[paramNumber]) param = moveX(param); + else if (yParams[paramNumber]) param = moveY(param); - paramNumber++; + paramNumber++; - return param; - }); + return param; + }); - return segmentType + paramString; - }); + return segmentType + paramString; + }); } diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index 15c5337c52a..0d9ab343d71 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -5,10 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; // special position conversion functions... category axis positions can't be // specified by their data values, because they don't make a continuous mapping. // so these have to be specified in terms of the category serial numbers, @@ -19,61 +16,75 @@ // removed entirely. exports.rangeToShapePosition = function(ax) { - return (ax.type === 'log') ? ax.r2d : function(v) { return v; }; + return ax.type === "log" + ? ax.r2d + : (function(v) { + return v; + }); }; exports.shapePositionToRange = function(ax) { - return (ax.type === 'log') ? ax.d2r : function(v) { return v; }; + return ax.type === "log" + ? ax.d2r + : (function(v) { + return v; + }); }; exports.decodeDate = function(convertToPx) { - return function(v) { - if(v.replace) v = v.replace('_', ' '); - return convertToPx(v); - }; + return function(v) { + if (v.replace) v = v.replace("_", " "); + return convertToPx(v); + }; }; exports.encodeDate = function(convertToDate) { - return function(v) { return convertToDate(v).replace(' ', '_'); }; + return function(v) { + return convertToDate(v).replace(" ", "_"); + }; }; exports.getDataToPixel = function(gd, axis, isVertical) { - var gs = gd._fullLayout._size, - dataToPixel; + var gs = gd._fullLayout._size, dataToPixel; - if(axis) { - var d2r = exports.shapePositionToRange(axis); + if (axis) { + var d2r = exports.shapePositionToRange(axis); - dataToPixel = function(v) { - return axis._offset + axis.r2p(d2r(v, true)); - }; + dataToPixel = function(v) { + return axis._offset + axis.r2p(d2r(v, true)); + }; - if(axis.type === 'date') dataToPixel = exports.decodeDate(dataToPixel); - } - else if(isVertical) { - dataToPixel = function(v) { return gs.t + gs.h * (1 - v); }; - } - else { - dataToPixel = function(v) { return gs.l + gs.w * v; }; - } + if (axis.type === "date") dataToPixel = exports.decodeDate(dataToPixel); + } else if (isVertical) { + dataToPixel = function(v) { + return gs.t + gs.h * (1 - v); + }; + } else { + dataToPixel = function(v) { + return gs.l + gs.w * v; + }; + } - return dataToPixel; + return dataToPixel; }; exports.getPixelToData = function(gd, axis, isVertical) { - var gs = gd._fullLayout._size, - pixelToData; + var gs = gd._fullLayout._size, pixelToData; - if(axis) { - var r2d = exports.rangeToShapePosition(axis); - pixelToData = function(p) { return r2d(axis.p2r(p - axis._offset)); }; - } - else if(isVertical) { - pixelToData = function(p) { return 1 - (p - gs.t) / gs.h; }; - } - else { - pixelToData = function(p) { return (p - gs.l) / gs.w; }; - } + if (axis) { + var r2d = exports.rangeToShapePosition(axis); + pixelToData = function(p) { + return r2d(axis.p2r(p - axis._offset)); + }; + } else if (isVertical) { + pixelToData = function(p) { + return 1 - (p - gs.t) / gs.h; + }; + } else { + pixelToData = function(p) { + return (p - gs.l) / gs.w; + }; + } - return pixelToData; + return pixelToData; }; diff --git a/src/components/shapes/index.js b/src/components/shapes/index.js index 444ede3cd59..0711a85a820 100644 --- a/src/components/shapes/index.js +++ b/src/components/shapes/index.js @@ -5,20 +5,15 @@ * 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 drawModule = require('./draw'); +"use strict"; +var drawModule = require("./draw"); module.exports = { - moduleType: 'component', - name: 'shapes', - - layoutAttributes: require('./attributes'), - supplyLayoutDefaults: require('./defaults'), - - calcAutorange: require('./calc_autorange'), - draw: drawModule.draw, - drawOne: drawModule.drawOne + moduleType: "component", + name: "shapes", + layoutAttributes: require("./attributes"), + supplyLayoutDefaults: require("./defaults"), + calcAutorange: require("./calc_autorange"), + draw: drawModule.draw, + drawOne: drawModule.drawOne }; diff --git a/src/components/shapes/shape_defaults.js b/src/components/shapes/shape_defaults.js index 44d983fc793..091a1e950cf 100644 --- a/src/components/shapes/shape_defaults.js +++ b/src/components/shapes/shape_defaults.js @@ -5,93 +5,95 @@ * 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 Axes = require('../../plots/cartesian/axes'); - -var attributes = require('./attributes'); -var helpers = require('./helpers'); - - -module.exports = function handleShapeDefaults(shapeIn, shapeOut, fullLayout, opts, itemOpts) { - opts = opts || {}; - itemOpts = itemOpts || {}; - - function coerce(attr, dflt) { - return Lib.coerce(shapeIn, shapeOut, attributes, attr, dflt); - } - - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); - - if(!visible) return shapeOut; - - coerce('layer'); - coerce('opacity'); - coerce('fillcolor'); - coerce('line.color'); - coerce('line.width'); - coerce('line.dash'); - - var dfltType = shapeIn.path ? 'path' : 'rect', - shapeType = coerce('type', dfltType); - - // positioning - var axLetters = ['x', 'y']; - for(var i = 0; i < 2; i++) { - var axLetter = axLetters[i], - gdMock = {_fullLayout: fullLayout}; - - // xref, yref - var axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, '', 'paper'); - - if(shapeType !== 'path') { - var dflt0 = 0.25, - dflt1 = 0.75, - ax, - pos2r, - r2pos; - - if(axRef !== 'paper') { - ax = Axes.getFromId(gdMock, axRef); - r2pos = helpers.rangeToShapePosition(ax); - pos2r = helpers.shapePositionToRange(ax); - } - else { - pos2r = r2pos = Lib.identity; - } - - // hack until V2.0 when log has regular range behavior - make it look like other - // ranges to send to coerce, then put it back after - // this is all to give reasonable default position behavior on log axes, which is - // a pretty unimportant edge case so we could just ignore this. - var attr0 = axLetter + '0', - attr1 = axLetter + '1', - in0 = shapeIn[attr0], - in1 = shapeIn[attr1]; - shapeIn[attr0] = pos2r(shapeIn[attr0], true); - shapeIn[attr1] = pos2r(shapeIn[attr1], true); - - // x0, x1 (and y0, y1) - Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0); - Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1); - - // hack part 2 - shapeOut[attr0] = r2pos(shapeOut[attr0]); - shapeOut[attr1] = r2pos(shapeOut[attr1]); - shapeIn[attr0] = in0; - shapeIn[attr1] = in1; - } +"use strict"; +var Lib = require("../../lib"); +var Axes = require("../../plots/cartesian/axes"); + +var attributes = require("./attributes"); +var helpers = require("./helpers"); + +module.exports = function handleShapeDefaults( + shapeIn, + shapeOut, + fullLayout, + opts, + itemOpts +) { + opts = opts || {}; + itemOpts = itemOpts || {}; + + function coerce(attr, dflt) { + return Lib.coerce(shapeIn, shapeOut, attributes, attr, dflt); + } + + var visible = coerce("visible", !itemOpts.itemIsNotPlainObject); + + if (!visible) return shapeOut; + + coerce("layer"); + coerce("opacity"); + coerce("fillcolor"); + coerce("line.color"); + coerce("line.width"); + coerce("line.dash"); + + var dfltType = shapeIn.path ? "path" : "rect", + shapeType = coerce("type", dfltType); + + // positioning + var axLetters = ["x", "y"]; + for (var i = 0; i < 2; i++) { + var axLetter = axLetters[i], gdMock = { _fullLayout: fullLayout }; + + // xref, yref + var axRef = Axes.coerceRef( + shapeIn, + shapeOut, + gdMock, + axLetter, + "", + "paper" + ); + + if (shapeType !== "path") { + var dflt0 = 0.25, dflt1 = 0.75, ax, pos2r, r2pos; + + if (axRef !== "paper") { + ax = Axes.getFromId(gdMock, axRef); + r2pos = helpers.rangeToShapePosition(ax); + pos2r = helpers.shapePositionToRange(ax); + } else { + pos2r = r2pos = Lib.identity; + } + + // hack until V2.0 when log has regular range behavior - make it look like other + // ranges to send to coerce, then put it back after + // this is all to give reasonable default position behavior on log axes, which is + // a pretty unimportant edge case so we could just ignore this. + var attr0 = axLetter + "0", + attr1 = axLetter + "1", + in0 = shapeIn[attr0], + in1 = shapeIn[attr1]; + shapeIn[attr0] = pos2r(shapeIn[attr0], true); + shapeIn[attr1] = pos2r(shapeIn[attr1], true); + + // x0, x1 (and y0, y1) + Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0); + Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1); + + // hack part 2 + shapeOut[attr0] = r2pos(shapeOut[attr0]); + shapeOut[attr1] = r2pos(shapeOut[attr1]); + shapeIn[attr0] = in0; + shapeIn[attr1] = in1; } + } - if(shapeType === 'path') { - coerce('path'); - } - else { - Lib.noneOrAll(shapeIn, shapeOut, ['x0', 'x1', 'y0', 'y1']); - } + if (shapeType === "path") { + coerce("path"); + } else { + Lib.noneOrAll(shapeIn, shapeOut, ["x0", "x1", "y0", "y1"]); + } - return shapeOut; + return shapeOut; }; diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 8e584a2455f..4fb808df73e 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -5,268 +5,249 @@ * 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 fontAttrs = require('../../plots/font_attributes'); -var padAttrs = require('../../plots/pad_attributes'); -var extendFlat = require('../../lib/extend').extendFlat; -var extendDeep = require('../../lib/extend').extendDeep; -var animationAttrs = require('../../plots/animation_attributes'); -var constants = require('./constants'); +"use strict"; +var fontAttrs = require("../../plots/font_attributes"); +var padAttrs = require("../../plots/pad_attributes"); +var extendFlat = require("../../lib/extend").extendFlat; +var extendDeep = require("../../lib/extend").extendDeep; +var animationAttrs = require("../../plots/animation_attributes"); +var constants = require("./constants"); var stepsAttrs = { - _isLinkedToArray: 'step', - - method: { - valType: 'enumerated', - values: ['restyle', 'relayout', 'animate', 'update'], - dflt: 'restyle', - role: 'info', - description: [ - 'Sets the Plotly method to be called when the slider value is changed.' - ].join(' ') - }, - args: { - valType: 'info_array', - role: 'info', - freeLength: true, - items: [ - { valType: 'any' }, - { valType: 'any' }, - { valType: 'any' } - ], - description: [ - 'Sets the arguments values to be passed to the Plotly', - 'method set in `method` on slide.' - ].join(' ') - }, - label: { - valType: 'string', - role: 'info', - description: 'Sets the text label to appear on the slider' - }, - value: { - valType: 'string', - role: 'info', - description: [ - 'Sets the value of the slider step, used to refer to the step programatically.', - 'Defaults to the slider label if not provided.' - ].join(' ') - } + _isLinkedToArray: "step", + method: { + valType: "enumerated", + values: ["restyle", "relayout", "animate", "update"], + dflt: "restyle", + role: "info", + description: [ + "Sets the Plotly method to be called when the slider value is changed." + ].join(" ") + }, + args: { + valType: "info_array", + role: "info", + freeLength: true, + items: [{ valType: "any" }, { valType: "any" }, { valType: "any" }], + description: [ + "Sets the arguments values to be passed to the Plotly", + "method set in `method` on slide." + ].join(" ") + }, + label: { + valType: "string", + role: "info", + description: "Sets the text label to appear on the slider" + }, + value: { + valType: "string", + role: "info", + description: [ + "Sets the value of the slider step, used to refer to the step programatically.", + "Defaults to the slider label if not provided." + ].join(" ") + } }; module.exports = { - _isLinkedToArray: 'slider', - + _isLinkedToArray: "slider", + visible: { + valType: "boolean", + role: "info", + dflt: true, + description: ["Determines whether or not the slider is visible."].join(" ") + }, + active: { + valType: "number", + role: "info", + min: 0, + dflt: 0, + description: [ + "Determines which button (by index starting from 0) is", + "considered active." + ].join(" ") + }, + steps: stepsAttrs, + lenmode: { + valType: "enumerated", + values: ["fraction", "pixels"], + role: "info", + dflt: "fraction", + description: [ + "Determines whether this slider length", + "is set in units of plot *fraction* or in *pixels.", + "Use `len` to set the value." + ].join(" ") + }, + len: { + valType: "number", + min: 0, + dflt: 1, + role: "style", + description: [ + "Sets the length of the slider", + "This measure excludes the padding of both ends.", + "That is, the slider's length is this length minus the", + "padding on both ends." + ].join(" ") + }, + x: { + valType: "number", + min: -2, + max: 3, + dflt: 0, + role: "style", + description: "Sets the x position (in normalized coordinates) of the slider." + }, + pad: extendDeep( + {}, + padAttrs, + { description: "Set the padding of the slider component along each side." }, + { t: { dflt: 20 } } + ), + xanchor: { + valType: "enumerated", + values: ["auto", "left", "center", "right"], + dflt: "left", + role: "info", + description: [ + "Sets the slider's horizontal position anchor.", + "This anchor binds the `x` position to the *left*, *center*", + "or *right* of the range selector." + ].join(" ") + }, + y: { + valType: "number", + min: -2, + max: 3, + dflt: 0, + role: "style", + description: "Sets the y position (in normalized coordinates) of the slider." + }, + yanchor: { + valType: "enumerated", + values: ["auto", "top", "middle", "bottom"], + dflt: "top", + role: "info", + description: [ + "Sets the slider's vertical position anchor", + "This anchor binds the `y` position to the *top*, *middle*", + "or *bottom* of the range selector." + ].join(" ") + }, + transition: { + duration: { + valType: "number", + role: "info", + min: 0, + dflt: 150, + description: "Sets the duration of the slider transition" + }, + easing: { + valType: "enumerated", + values: animationAttrs.transition.easing.values, + role: "info", + dflt: "cubic-in-out", + description: "Sets the easing function of the slider transition" + } + }, + currentvalue: { visible: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines whether or not the slider is visible.' - ].join(' ') + valType: "boolean", + role: "info", + dflt: true, + description: [ + "Shows the currently-selected value above the slider." + ].join(" ") }, - - active: { - valType: 'number', - role: 'info', - min: 0, - dflt: 0, - description: [ - 'Determines which button (by index starting from 0) is', - 'considered active.' - ].join(' ') - }, - - steps: stepsAttrs, - - lenmode: { - valType: 'enumerated', - values: ['fraction', 'pixels'], - role: 'info', - dflt: 'fraction', - description: [ - 'Determines whether this slider length', - 'is set in units of plot *fraction* or in *pixels.', - 'Use `len` to set the value.' - ].join(' ') - }, - len: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: [ - 'Sets the length of the slider', - 'This measure excludes the padding of both ends.', - 'That is, the slider\'s length is this length minus the', - 'padding on both ends.' - ].join(' ') - }, - x: { - valType: 'number', - min: -2, - max: 3, - dflt: 0, - role: 'style', - description: 'Sets the x position (in normalized coordinates) of the slider.' - }, - pad: extendDeep({}, padAttrs, { - description: 'Set the padding of the slider component along each side.' - }, {t: {dflt: 20}}), xanchor: { - valType: 'enumerated', - values: ['auto', 'left', 'center', 'right'], - dflt: 'left', - role: 'info', - description: [ - 'Sets the slider\'s horizontal position anchor.', - 'This anchor binds the `x` position to the *left*, *center*', - 'or *right* of the range selector.' - ].join(' ') - }, - y: { - valType: 'number', - min: -2, - max: 3, - dflt: 0, - role: 'style', - description: 'Sets the y position (in normalized coordinates) of the slider.' + valType: "enumerated", + values: ["left", "center", "right"], + dflt: "left", + role: "info", + description: [ + "The alignment of the value readout relative to the length of the slider." + ].join(" ") + }, + offset: { + valType: "number", + dflt: 10, + role: "info", + description: [ + "The amount of space, in pixels, between the current value label", + "and the slider." + ].join(" ") + }, + prefix: { + valType: "string", + role: "info", + description: "When currentvalue.visible is true, this sets the prefix of the label." + }, + suffix: { + valType: "string", + role: "info", + description: "When currentvalue.visible is true, this sets the suffix of the label." }, - yanchor: { - valType: 'enumerated', - values: ['auto', 'top', 'middle', 'bottom'], - dflt: 'top', - role: 'info', - description: [ - 'Sets the slider\'s vertical position anchor', - 'This anchor binds the `y` position to the *top*, *middle*', - 'or *bottom* of the range selector.' - ].join(' ') - }, - - transition: { - duration: { - valType: 'number', - role: 'info', - min: 0, - dflt: 150, - description: 'Sets the duration of the slider transition' - }, - easing: { - valType: 'enumerated', - values: animationAttrs.transition.easing.values, - role: 'info', - dflt: 'cubic-in-out', - description: 'Sets the easing function of the slider transition' - }, - }, - - currentvalue: { - visible: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Shows the currently-selected value above the slider.' - ].join(' ') - }, - - xanchor: { - valType: 'enumerated', - values: ['left', 'center', 'right'], - dflt: 'left', - role: 'info', - description: [ - 'The alignment of the value readout relative to the length of the slider.' - ].join(' ') - }, - - offset: { - valType: 'number', - dflt: 10, - role: 'info', - description: [ - 'The amount of space, in pixels, between the current value label', - 'and the slider.' - ].join(' ') - }, - - prefix: { - valType: 'string', - role: 'info', - description: 'When currentvalue.visible is true, this sets the prefix of the label.' - }, - - suffix: { - valType: 'string', - role: 'info', - description: 'When currentvalue.visible is true, this sets the suffix of the label.' - }, - - font: extendFlat({}, fontAttrs, { - description: 'Sets the font of the current value label text.' - }), - }, - font: extendFlat({}, fontAttrs, { - description: 'Sets the font of the slider step labels.' - }), - - activebgcolor: { - valType: 'color', - role: 'style', - dflt: constants.gripBgActiveColor, - description: [ - 'Sets the background color of the slider grip', - 'while dragging.' - ].join(' ') - }, - bgcolor: { - valType: 'color', - role: 'style', - dflt: constants.railBgColor, - description: 'Sets the background color of the slider.' - }, - bordercolor: { - valType: 'color', - dflt: constants.railBorderColor, - role: 'style', - description: 'Sets the color of the border enclosing the slider.' - }, - borderwidth: { - valType: 'number', - min: 0, - dflt: constants.railBorderWidth, - role: 'style', - description: 'Sets the width (in px) of the border enclosing the slider.' - }, - ticklen: { - valType: 'number', - min: 0, - dflt: constants.tickLength, - role: 'style', - description: 'Sets the length in pixels of step tick marks' - }, - tickcolor: { - valType: 'color', - dflt: constants.tickColor, - role: 'style', - description: 'Sets the color of the border enclosing the slider.' - }, - tickwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the tick width (in px).' - }, - minorticklen: { - valType: 'number', - min: 0, - dflt: constants.minorTickLength, - role: 'style', - description: 'Sets the length in pixels of minor step tick marks' - }, + description: "Sets the font of the current value label text." + }) + }, + font: extendFlat({}, fontAttrs, { + description: "Sets the font of the slider step labels." + }), + activebgcolor: { + valType: "color", + role: "style", + dflt: constants.gripBgActiveColor, + description: [ + "Sets the background color of the slider grip", + "while dragging." + ].join(" ") + }, + bgcolor: { + valType: "color", + role: "style", + dflt: constants.railBgColor, + description: "Sets the background color of the slider." + }, + bordercolor: { + valType: "color", + dflt: constants.railBorderColor, + role: "style", + description: "Sets the color of the border enclosing the slider." + }, + borderwidth: { + valType: "number", + min: 0, + dflt: constants.railBorderWidth, + role: "style", + description: "Sets the width (in px) of the border enclosing the slider." + }, + ticklen: { + valType: "number", + min: 0, + dflt: constants.tickLength, + role: "style", + description: "Sets the length in pixels of step tick marks" + }, + tickcolor: { + valType: "color", + dflt: constants.tickColor, + role: "style", + description: "Sets the color of the border enclosing the slider." + }, + tickwidth: { + valType: "number", + min: 0, + dflt: 1, + role: "style", + description: "Sets the tick width (in px)." + }, + minorticklen: { + valType: "number", + min: 0, + dflt: constants.minorTickLength, + role: "style", + description: "Sets the length in pixels of minor step tick marks" + } }; diff --git a/src/components/sliders/constants.js b/src/components/sliders/constants.js index fedd7c088b5..6004822972e 100644 --- a/src/components/sliders/constants.js +++ b/src/components/sliders/constants.js @@ -5,91 +5,70 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; module.exports = { - - // layout attribute name - name: 'sliders', - - // class names - containerClassName: 'slider-container', - groupClassName: 'slider-group', - inputAreaClass: 'slider-input-area', - railRectClass: 'slider-rail-rect', - railTouchRectClass: 'slider-rail-touch-rect', - gripRectClass: 'slider-grip-rect', - tickRectClass: 'slider-tick-rect', - inputProxyClass: 'slider-input-proxy', - labelsClass: 'slider-labels', - labelGroupClass: 'slider-label-group', - labelClass: 'slider-label', - currentValueClass: 'slider-current-value', - - railHeight: 5, - - // DOM attribute name in button group keeping track - // of active update menu - menuIndexAttrName: 'slider-active-index', - - // id root pass to Plots.autoMargin - autoMarginIdRoot: 'slider-', - - // min item width / height - minWidth: 30, - minHeight: 30, - - // padding around item text - textPadX: 40, - - // font size to height scale - fontSizeToHeight: 1.3, - - // arrow offset off right edge - arrowOffsetX: 4, - - railRadius: 2, - railWidth: 5, - railBorder: 4, - railBorderWidth: 1, - railBorderColor: '#bec8d9', - railBgColor: '#f8fafc', - - // The distance of the rail from the edge of the touchable area - // Slightly less than the step inset because of the curved edges - // of the rail - railInset: 8, - - // The distance from the extremal tick marks to the edge of the - // touchable area. This is basically the same as the grip radius, - // but for other styles it wouldn't really need to be. - stepInset: 10, - - gripRadius: 10, - gripWidth: 20, - gripHeight: 20, - gripBorder: 20, - gripBorderWidth: 1, - gripBorderColor: '#bec8d9', - gripBgColor: '#f6f8fa', - gripBgActiveColor: '#dbdde0', - - labelPadding: 8, - labelOffset: 0, - - tickWidth: 1, - tickColor: '#333', - tickOffset: 25, - tickLength: 7, - - minorTickOffset: 25, - minorTickColor: '#333', - minorTickLength: 4, - - // Extra space below the current value label: - currentValuePadding: 8, - currentValueInset: 0, + // layout attribute name + name: "sliders", + // class names + containerClassName: "slider-container", + groupClassName: "slider-group", + inputAreaClass: "slider-input-area", + railRectClass: "slider-rail-rect", + railTouchRectClass: "slider-rail-touch-rect", + gripRectClass: "slider-grip-rect", + tickRectClass: "slider-tick-rect", + inputProxyClass: "slider-input-proxy", + labelsClass: "slider-labels", + labelGroupClass: "slider-label-group", + labelClass: "slider-label", + currentValueClass: "slider-current-value", + railHeight: 5, + // DOM attribute name in button group keeping track + // of active update menu + menuIndexAttrName: "slider-active-index", + // id root pass to Plots.autoMargin + autoMarginIdRoot: "slider-", + // min item width / height + minWidth: 30, + minHeight: 30, + // padding around item text + textPadX: 40, + // font size to height scale + fontSizeToHeight: 1.3, + // arrow offset off right edge + arrowOffsetX: 4, + railRadius: 2, + railWidth: 5, + railBorder: 4, + railBorderWidth: 1, + railBorderColor: "#bec8d9", + railBgColor: "#f8fafc", + // The distance of the rail from the edge of the touchable area + // Slightly less than the step inset because of the curved edges + // of the rail + railInset: 8, + // The distance from the extremal tick marks to the edge of the + // touchable area. This is basically the same as the grip radius, + // but for other styles it wouldn't really need to be. + stepInset: 10, + gripRadius: 10, + gripWidth: 20, + gripHeight: 20, + gripBorder: 20, + gripBorderWidth: 1, + gripBorderColor: "#bec8d9", + gripBgColor: "#f6f8fa", + gripBgActiveColor: "#dbdde0", + labelPadding: 8, + labelOffset: 0, + tickWidth: 1, + tickColor: "#333", + tickOffset: 25, + tickLength: 7, + minorTickOffset: 25, + minorTickColor: "#333", + minorTickLength: 4, + // Extra space below the current value label: + currentValuePadding: 8, + currentValueInset: 0 }; diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index b2fed316c7b..6d1057a1f18 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -5,107 +5,101 @@ * 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 handleArrayContainerDefaults = require( + "../../plots/array_container_defaults" +); -'use strict'; - -var Lib = require('../../lib'); -var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); - -var attributes = require('./attributes'); -var constants = require('./constants'); +var attributes = require("./attributes"); +var constants = require("./constants"); var name = constants.name; var stepAttrs = attributes.steps; - module.exports = function slidersDefaults(layoutIn, layoutOut) { - var opts = { - name: name, - handleItemDefaults: sliderDefaults - }; + var opts = { name: name, handleItemDefaults: sliderDefaults }; - handleArrayContainerDefaults(layoutIn, layoutOut, opts); + handleArrayContainerDefaults(layoutIn, layoutOut, opts); }; function sliderDefaults(sliderIn, sliderOut, layoutOut) { + function coerce(attr, dflt) { + return Lib.coerce(sliderIn, sliderOut, attributes, attr, dflt); + } - function coerce(attr, dflt) { - return Lib.coerce(sliderIn, sliderOut, attributes, attr, dflt); - } + var steps = stepsDefaults(sliderIn, sliderOut); - var steps = stepsDefaults(sliderIn, sliderOut); + var visible = coerce("visible", steps.length > 0); + if (!visible) return; - var visible = coerce('visible', steps.length > 0); - if(!visible) return; + coerce("active"); - coerce('active'); + coerce("x"); + coerce("y"); + Lib.noneOrAll(sliderIn, sliderOut, ["x", "y"]); - coerce('x'); - coerce('y'); - Lib.noneOrAll(sliderIn, sliderOut, ['x', 'y']); + coerce("xanchor"); + coerce("yanchor"); - coerce('xanchor'); - coerce('yanchor'); + coerce("len"); + coerce("lenmode"); - coerce('len'); - coerce('lenmode'); + coerce("pad.t"); + coerce("pad.r"); + coerce("pad.b"); + coerce("pad.l"); - coerce('pad.t'); - coerce('pad.r'); - coerce('pad.b'); - coerce('pad.l'); + Lib.coerceFont(coerce, "font", layoutOut.font); - Lib.coerceFont(coerce, 'font', layoutOut.font); + var currentValueIsVisible = coerce("currentvalue.visible"); - var currentValueIsVisible = coerce('currentvalue.visible'); + if (currentValueIsVisible) { + coerce("currentvalue.xanchor"); + coerce("currentvalue.prefix"); + coerce("currentvalue.suffix"); + coerce("currentvalue.offset"); - if(currentValueIsVisible) { - coerce('currentvalue.xanchor'); - coerce('currentvalue.prefix'); - coerce('currentvalue.suffix'); - coerce('currentvalue.offset'); + Lib.coerceFont(coerce, "currentvalue.font", sliderOut.font); + } - Lib.coerceFont(coerce, 'currentvalue.font', sliderOut.font); - } + coerce("transition.duration"); + coerce("transition.easing"); - coerce('transition.duration'); - coerce('transition.easing'); - - coerce('bgcolor'); - coerce('activebgcolor'); - coerce('bordercolor'); - coerce('borderwidth'); - coerce('ticklen'); - coerce('tickwidth'); - coerce('tickcolor'); - coerce('minorticklen'); + coerce("bgcolor"); + coerce("activebgcolor"); + coerce("bordercolor"); + coerce("borderwidth"); + coerce("ticklen"); + coerce("tickwidth"); + coerce("tickcolor"); + coerce("minorticklen"); } function stepsDefaults(sliderIn, sliderOut) { - var valuesIn = sliderIn.steps || [], - valuesOut = sliderOut.steps = []; + var valuesIn = sliderIn.steps || [], valuesOut = sliderOut.steps = []; - var valueIn, valueOut; + var valueIn, valueOut; - function coerce(attr, dflt) { - return Lib.coerce(valueIn, valueOut, stepAttrs, attr, dflt); - } + function coerce(attr, dflt) { + return Lib.coerce(valueIn, valueOut, stepAttrs, attr, dflt); + } - for(var i = 0; i < valuesIn.length; i++) { - valueIn = valuesIn[i]; - valueOut = {}; + for (var i = 0; i < valuesIn.length; i++) { + valueIn = valuesIn[i]; + valueOut = {}; - if(!Lib.isPlainObject(valueIn) || !Array.isArray(valueIn.args)) { - continue; - } + if (!Lib.isPlainObject(valueIn) || !Array.isArray(valueIn.args)) { + continue; + } - coerce('method'); - coerce('args'); - coerce('label', 'step-' + i); - coerce('value', valueOut.label); + coerce("method"); + coerce("args"); + coerce("label", "step-" + i); + coerce("value", valueOut.label); - valuesOut.push(valueOut); - } + valuesOut.push(valueOut); + } - return valuesOut; + return valuesOut; } diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 50c2ef0f35e..e5ce4991ec4 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -5,596 +5,704 @@ * 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 d3 = require("d3"); +var Plots = require("../../plots/plots"); +var Color = require("../color"); +var Drawing = require("../drawing"); +var svgTextUtils = require("../../lib/svg_text_utils"); +var anchorUtils = require("../legend/anchor_utils"); -'use strict'; - -var d3 = require('d3'); - -var Plots = require('../../plots/plots'); -var Color = require('../color'); -var Drawing = require('../drawing'); -var svgTextUtils = require('../../lib/svg_text_utils'); -var anchorUtils = require('../legend/anchor_utils'); - -var constants = require('./constants'); - +var constants = require("./constants"); module.exports = function draw(gd) { - var fullLayout = gd._fullLayout, - sliderData = makeSliderData(fullLayout); - - // draw a container for *all* sliders: - var sliders = fullLayout._infolayer - .selectAll('g.' + constants.containerClassName) - .data(sliderData.length > 0 ? [0] : []); - - sliders.enter().append('g') - .classed(constants.containerClassName, true) - .style('cursor', 'ew-resize'); + var fullLayout = gd._fullLayout, sliderData = makeSliderData(fullLayout); - sliders.exit().remove(); + // draw a container for *all* sliders: + var sliders = fullLayout._infolayer + .selectAll("g." + constants.containerClassName) + .data(sliderData.length > 0 ? [0] : []); - // If no more sliders, clear the margisn: - if(sliders.exit().size()) clearPushMargins(gd); + sliders + .enter() + .append("g") + .classed(constants.containerClassName, true) + .style("cursor", "ew-resize"); - // Return early if no menus visible: - if(sliderData.length === 0) return; + sliders.exit().remove(); - var sliderGroups = sliders.selectAll('g.' + constants.groupClassName) - .data(sliderData, keyFunction); + // If no more sliders, clear the margisn: + if (sliders.exit().size()) clearPushMargins(gd); - sliderGroups.enter().append('g') - .classed(constants.groupClassName, true); + // Return early if no menus visible: + if (sliderData.length === 0) return; - sliderGroups.exit().each(function(sliderOpts) { - d3.select(this).remove(); + var sliderGroups = sliders + .selectAll("g." + constants.groupClassName) + .data(sliderData, keyFunction); - sliderOpts._commandObserver.remove(); - delete sliderOpts._commandObserver; + sliderGroups.enter().append("g").classed(constants.groupClassName, true); - Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index); - }); + sliderGroups.exit().each(function(sliderOpts) { + d3.select(this).remove(); - // Find the dimensions of the sliders: - for(var i = 0; i < sliderData.length; i++) { - var sliderOpts = sliderData[i]; - findDimensions(gd, sliderOpts); - } + sliderOpts._commandObserver.remove(); + delete sliderOpts._commandObserver; - sliderGroups.each(function(sliderOpts) { - // If it has fewer than two options, it's not really a slider: - if(sliderOpts.steps.length < 2) return; + Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index); + }); - var gSlider = d3.select(this); + // Find the dimensions of the sliders: + for (var i = 0; i < sliderData.length; i++) { + var sliderOpts = sliderData[i]; + findDimensions(gd, sliderOpts); + } - computeLabelSteps(sliderOpts); + sliderGroups.each(function(sliderOpts) { + // If it has fewer than two options, it's not really a slider: + if (sliderOpts.steps.length < 2) return; - Plots.manageCommandObserver(gd, sliderOpts, sliderOpts.steps, function(data) { - // NB: Same as below. This is *not* always the same as sliderOpts since - // if a new set of steps comes in, the reference in this callback would - // be invalid. We need to refetch it from the slider group, which is - // the join data that creates this slider. So if this slider still exists, - // the group should be valid, *to the best of my knowledge.* If not, - // we'd have to look it up by d3 data join index/key. - var opts = gSlider.data()[0]; + var gSlider = d3.select(this); - if(opts.active === data.index) return; - if(opts._dragging) return; + computeLabelSteps(sliderOpts); - setActive(gd, gSlider, opts, data.index, false, true); - }); + Plots.manageCommandObserver(gd, sliderOpts, sliderOpts.steps, function( + data + ) { + // NB: Same as below. This is *not* always the same as sliderOpts since + // if a new set of steps comes in, the reference in this callback would + // be invalid. We need to refetch it from the slider group, which is + // the join data that creates this slider. So if this slider still exists, + // the group should be valid, *to the best of my knowledge.* If not, + // we'd have to look it up by d3 data join index/key. + var opts = gSlider.data()[0]; - drawSlider(gd, d3.select(this), sliderOpts); + if (opts.active === data.index) return; + if (opts._dragging) return; - // makeInputProxy(gd, d3.select(this), sliderOpts); + setActive(gd, gSlider, opts, data.index, false, true); }); + + drawSlider(gd, d3.select(this), sliderOpts); + // makeInputProxy(gd, d3.select(this), sliderOpts); + }); }; /* function makeInputProxy(gd, sliderGroup, sliderOpts) { sliderOpts.inputProxy = gd._fullLayout._paperdiv.selectAll('input.' + constants.inputProxyClass) .data([0]); -}*/ - +} */ // This really only just filters by visibility: function makeSliderData(fullLayout) { - var contOpts = fullLayout[constants.name], - sliderData = []; + var contOpts = fullLayout[constants.name], sliderData = []; - for(var i = 0; i < contOpts.length; i++) { - var item = contOpts[i]; - if(!item.visible || !item.steps.length) continue; - sliderData.push(item); - } + for (var i = 0; i < contOpts.length; i++) { + var item = contOpts[i]; + if (!item.visible || !item.steps.length) continue; + sliderData.push(item); + } - return sliderData; + return sliderData; } // This is set in the defaults step: function keyFunction(opts) { - return opts._index; + return opts._index; } // Compute the dimensions (mutates sliderOpts): function findDimensions(gd, sliderOpts) { - var sliderLabels = gd._tester.selectAll('g.' + constants.labelGroupClass) - .data(sliderOpts.steps); - - sliderLabels.enter().append('g') - .classed(constants.labelGroupClass, true); + var sliderLabels = gd._tester + .selectAll("g." + constants.labelGroupClass) + .data(sliderOpts.steps); - // loop over fake buttons to find width / height - var maxLabelWidth = 0; - var labelHeight = 0; - sliderLabels.each(function(stepOpts) { - var labelGroup = d3.select(this); - - var text = drawLabel(labelGroup, {step: stepOpts}, sliderOpts); - - var tWidth = (text.node() && Drawing.bBox(text.node()).width) || 0; - - // This just overwrites with the last. Which is fine as long as - // the bounding box (probably incorrectly) measures the text *on - // a single line*: - labelHeight = (text.node() && Drawing.bBox(text.node()).height) || 0; - - maxLabelWidth = Math.max(maxLabelWidth, tWidth); - }); + sliderLabels.enter().append("g").classed(constants.labelGroupClass, true); - sliderLabels.remove(); + // loop over fake buttons to find width / height + var maxLabelWidth = 0; + var labelHeight = 0; + sliderLabels.each(function(stepOpts) { + var labelGroup = d3.select(this); - sliderOpts.inputAreaWidth = Math.max( - constants.railWidth, - constants.gripHeight - ); - - sliderOpts.currentValueMaxWidth = 0; - sliderOpts.currentValueHeight = 0; - sliderOpts.currentValueTotalHeight = 0; - - if(sliderOpts.currentvalue.visible) { - // Get the dimensions of the current value label: - var dummyGroup = gd._tester.append('g'); + var text = drawLabel(labelGroup, { step: stepOpts }, sliderOpts); - sliderLabels.each(function(stepOpts) { - var curValPrefix = drawCurrentValue(dummyGroup, sliderOpts, stepOpts.label); - var curValSize = (curValPrefix.node() && Drawing.bBox(curValPrefix.node())) || {width: 0, height: 0}; - sliderOpts.currentValueMaxWidth = Math.max(sliderOpts.currentValueMaxWidth, Math.ceil(curValSize.width)); - sliderOpts.currentValueHeight = Math.max(sliderOpts.currentValueHeight, Math.ceil(curValSize.height)); - }); + var tWidth = text.node() && Drawing.bBox(text.node()).width || 0; - sliderOpts.currentValueTotalHeight = sliderOpts.currentValueHeight + sliderOpts.currentvalue.offset; + // This just overwrites with the last. Which is fine as long as + // the bounding box (probably incorrectly) measures the text *on + // a single line*: + labelHeight = text.node() && Drawing.bBox(text.node()).height || 0; - dummyGroup.remove(); - } + maxLabelWidth = Math.max(maxLabelWidth, tWidth); + }); - var graphSize = gd._fullLayout._size; - sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x; - sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); + sliderLabels.remove(); - if(sliderOpts.lenmode === 'fraction') { - // fraction: - sliderOpts.outerLength = Math.round(graphSize.w * sliderOpts.len); - } else { - // pixels: - sliderOpts.outerLength = sliderOpts.len; - } + sliderOpts.inputAreaWidth = Math.max( + constants.railWidth, + constants.gripHeight + ); - // Set the length-wise padding so that the grip ends up *on* the end of - // the bar when at either extreme - sliderOpts.lenPad = Math.round(constants.gripWidth * 0.5); + sliderOpts.currentValueMaxWidth = 0; + sliderOpts.currentValueHeight = 0; + sliderOpts.currentValueTotalHeight = 0; - // The length of the rail, *excluding* padding on either end: - sliderOpts.inputAreaStart = 0; - sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.pad.l - sliderOpts.pad.r); + if (sliderOpts.currentvalue.visible) { + // Get the dimensions of the current value label: + var dummyGroup = gd._tester.append("g"); - var textableInputLength = sliderOpts.inputAreaLength - 2 * constants.stepInset; - var availableSpacePerLabel = textableInputLength / (sliderOpts.steps.length - 1); - var computedSpacePerLabel = maxLabelWidth + constants.labelPadding; - sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); - sliderOpts.labelHeight = labelHeight; - - sliderOpts.height = sliderOpts.currentValueTotalHeight + constants.tickOffset + sliderOpts.ticklen + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; - - var xanchor = 'left'; - if(anchorUtils.isRightAnchor(sliderOpts)) { - sliderOpts.lx -= sliderOpts.outerLength; - xanchor = 'right'; - } - if(anchorUtils.isCenterAnchor(sliderOpts)) { - sliderOpts.lx -= sliderOpts.outerLength / 2; - xanchor = 'center'; - } - - var yanchor = 'top'; - if(anchorUtils.isBottomAnchor(sliderOpts)) { - sliderOpts.ly -= sliderOpts.height; - yanchor = 'bottom'; - } - if(anchorUtils.isMiddleAnchor(sliderOpts)) { - sliderOpts.ly -= sliderOpts.height / 2; - yanchor = 'middle'; - } - - sliderOpts.outerLength = Math.ceil(sliderOpts.outerLength); - sliderOpts.height = Math.ceil(sliderOpts.height); - sliderOpts.lx = Math.round(sliderOpts.lx); - sliderOpts.ly = Math.round(sliderOpts.ly); - - Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index, { - x: sliderOpts.x, - y: sliderOpts.y, - l: sliderOpts.outerLength * ({right: 1, center: 0.5}[xanchor] || 0), - r: sliderOpts.outerLength * ({left: 1, center: 0.5}[xanchor] || 0), - b: sliderOpts.height * ({top: 1, middle: 0.5}[yanchor] || 0), - t: sliderOpts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) + sliderLabels.each(function(stepOpts) { + var curValPrefix = drawCurrentValue( + dummyGroup, + sliderOpts, + stepOpts.label + ); + var curValSize = curValPrefix.node() && + Drawing.bBox(curValPrefix.node()) || + { width: 0, height: 0 }; + sliderOpts.currentValueMaxWidth = Math.max( + sliderOpts.currentValueMaxWidth, + Math.ceil(curValSize.width) + ); + sliderOpts.currentValueHeight = Math.max( + sliderOpts.currentValueHeight, + Math.ceil(curValSize.height) + ); }); + + sliderOpts.currentValueTotalHeight = sliderOpts.currentValueHeight + + sliderOpts.currentvalue.offset; + + dummyGroup.remove(); + } + + var graphSize = gd._fullLayout._size; + sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x; + sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); + + if (sliderOpts.lenmode === "fraction") { + // fraction: + sliderOpts.outerLength = Math.round(graphSize.w * sliderOpts.len); + } else { + // pixels: + sliderOpts.outerLength = sliderOpts.len; + } + + // Set the length-wise padding so that the grip ends up *on* the end of + // the bar when at either extreme + sliderOpts.lenPad = Math.round(constants.gripWidth * 0.5); + + // The length of the rail, *excluding* padding on either end: + sliderOpts.inputAreaStart = 0; + sliderOpts.inputAreaLength = Math.round( + sliderOpts.outerLength - sliderOpts.pad.l - sliderOpts.pad.r + ); + + var textableInputLength = sliderOpts.inputAreaLength - + 2 * constants.stepInset; + var availableSpacePerLabel = textableInputLength / + (sliderOpts.steps.length - 1); + var computedSpacePerLabel = maxLabelWidth + constants.labelPadding; + sliderOpts.labelStride = Math.max( + 1, + Math.ceil(computedSpacePerLabel / availableSpacePerLabel) + ); + sliderOpts.labelHeight = labelHeight; + + sliderOpts.height = sliderOpts.currentValueTotalHeight + + constants.tickOffset + + sliderOpts.ticklen + + constants.labelOffset + + sliderOpts.labelHeight + + sliderOpts.pad.t + + sliderOpts.pad.b; + + var xanchor = "left"; + if (anchorUtils.isRightAnchor(sliderOpts)) { + sliderOpts.lx -= sliderOpts.outerLength; + xanchor = "right"; + } + if (anchorUtils.isCenterAnchor(sliderOpts)) { + sliderOpts.lx -= sliderOpts.outerLength / 2; + xanchor = "center"; + } + + var yanchor = "top"; + if (anchorUtils.isBottomAnchor(sliderOpts)) { + sliderOpts.ly -= sliderOpts.height; + yanchor = "bottom"; + } + if (anchorUtils.isMiddleAnchor(sliderOpts)) { + sliderOpts.ly -= sliderOpts.height / 2; + yanchor = "middle"; + } + + sliderOpts.outerLength = Math.ceil(sliderOpts.outerLength); + sliderOpts.height = Math.ceil(sliderOpts.height); + sliderOpts.lx = Math.round(sliderOpts.lx); + sliderOpts.ly = Math.round(sliderOpts.ly); + + Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index, { + x: sliderOpts.x, + y: sliderOpts.y, + l: sliderOpts.outerLength * (({ right: 1, center: 0.5 })[xanchor] || 0), + r: sliderOpts.outerLength * (({ left: 1, center: 0.5 })[xanchor] || 0), + b: sliderOpts.height * (({ top: 1, middle: 0.5 })[yanchor] || 0), + t: sliderOpts.height * (({ bottom: 1, middle: 0.5 })[yanchor] || 0) + }); } function drawSlider(gd, sliderGroup, sliderOpts) { - // This is related to the other long notes in this file regarding what happens - // when slider steps disappear. This particular fix handles what happens when - // the *current* slider step is removed. The drawing functions will error out - // when they fail to find it, so the fix for now is that it will just draw the - // slider in the first position but will not execute the command. - if(sliderOpts.active >= sliderOpts.steps.length) { - sliderOpts.active = 0; - } - - // These are carefully ordered for proper z-ordering: - sliderGroup - .call(drawCurrentValue, sliderOpts) - .call(drawRail, sliderOpts) - .call(drawLabelGroup, sliderOpts) - .call(drawTicks, sliderOpts) - .call(drawTouchRect, gd, sliderOpts) - .call(drawGrip, gd, sliderOpts); - - // Position the rectangle: - Drawing.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.pad.l, sliderOpts.ly + sliderOpts.pad.t); - - sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), false); - sliderGroup.call(drawCurrentValue, sliderOpts); - + // This is related to the other long notes in this file regarding what happens + // when slider steps disappear. This particular fix handles what happens when + // the *current* slider step is removed. The drawing functions will error out + // when they fail to find it, so the fix for now is that it will just draw the + // slider in the first position but will not execute the command. + if (sliderOpts.active >= sliderOpts.steps.length) { + sliderOpts.active = 0; + } + + // These are carefully ordered for proper z-ordering: + sliderGroup + .call(drawCurrentValue, sliderOpts) + .call(drawRail, sliderOpts) + .call(drawLabelGroup, sliderOpts) + .call(drawTicks, sliderOpts) + .call(drawTouchRect, gd, sliderOpts) + .call(drawGrip, gd, sliderOpts); + + // Position the rectangle: + Drawing.setTranslate( + sliderGroup, + sliderOpts.lx + sliderOpts.pad.l, + sliderOpts.ly + sliderOpts.pad.t + ); + + sliderGroup.call( + setGripPosition, + sliderOpts, + sliderOpts.active / (sliderOpts.steps.length - 1), + false + ); + sliderGroup.call(drawCurrentValue, sliderOpts); } function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { - if(!sliderOpts.currentvalue.visible) return; - - var x0, textAnchor; - var text = sliderGroup.selectAll('text') - .data([0]); - - switch(sliderOpts.currentvalue.xanchor) { - case 'right': - // This is anchored left and adjusted by the width of the longest label - // so that the prefix doesn't move. The goal of this is to emphasize - // what's actually changing and make the update less distracting. - x0 = sliderOpts.inputAreaLength - constants.currentValueInset - sliderOpts.currentValueMaxWidth; - textAnchor = 'left'; - break; - case 'center': - x0 = sliderOpts.inputAreaLength * 0.5; - textAnchor = 'middle'; - break; - default: - x0 = constants.currentValueInset; - textAnchor = 'left'; - } - - text.enter().append('text') - .classed(constants.labelClass, true) - .classed('user-select-none', true) - .attr('text-anchor', textAnchor); - - var str = sliderOpts.currentvalue.prefix ? sliderOpts.currentvalue.prefix : ''; - - if(typeof valueOverride === 'string') { - str += valueOverride; - } else { - var curVal = sliderOpts.steps[sliderOpts.active].label; - str += curVal; - } - - if(sliderOpts.currentvalue.suffix) { - str += sliderOpts.currentvalue.suffix; - } - - text.call(Drawing.font, sliderOpts.currentvalue.font) - .text(str) - .call(svgTextUtils.convertToTspans); - - Drawing.setTranslate(text, x0, sliderOpts.currentValueHeight); - - return text; + if (!sliderOpts.currentvalue.visible) return; + + var x0, textAnchor; + var text = sliderGroup.selectAll("text").data([0]); + + switch (sliderOpts.currentvalue.xanchor) { + case "right": + // This is anchored left and adjusted by the width of the longest label + // so that the prefix doesn't move. The goal of this is to emphasize + // what's actually changing and make the update less distracting. + x0 = sliderOpts.inputAreaLength - + constants.currentValueInset - + sliderOpts.currentValueMaxWidth; + textAnchor = "left"; + break; + case "center": + x0 = sliderOpts.inputAreaLength * 0.5; + textAnchor = "middle"; + break; + default: + x0 = constants.currentValueInset; + textAnchor = "left"; + } + + text + .enter() + .append("text") + .classed(constants.labelClass, true) + .classed("user-select-none", true) + .attr("text-anchor", textAnchor); + + var str = sliderOpts.currentvalue.prefix + ? sliderOpts.currentvalue.prefix + : ""; + + if (typeof valueOverride === "string") { + str += valueOverride; + } else { + var curVal = sliderOpts.steps[sliderOpts.active].label; + str += curVal; + } + + if (sliderOpts.currentvalue.suffix) { + str += sliderOpts.currentvalue.suffix; + } + + text + .call(Drawing.font, sliderOpts.currentvalue.font) + .text(str) + .call(svgTextUtils.convertToTspans); + + Drawing.setTranslate(text, x0, sliderOpts.currentValueHeight); + + return text; } function drawGrip(sliderGroup, gd, sliderOpts) { - var grip = sliderGroup.selectAll('rect.' + constants.gripRectClass) - .data([0]); - - grip.enter().append('rect') - .classed(constants.gripRectClass, true) - .call(attachGripEvents, gd, sliderGroup, sliderOpts) - .style('pointer-events', 'all'); - - grip.attr({ - width: constants.gripWidth, - height: constants.gripHeight, - rx: constants.gripRadius, - ry: constants.gripRadius, + var grip = sliderGroup.selectAll("rect." + constants.gripRectClass).data([0]); + + grip + .enter() + .append("rect") + .classed(constants.gripRectClass, true) + .call(attachGripEvents, gd, sliderGroup, sliderOpts) + .style("pointer-events", "all"); + + grip + .attr({ + width: constants.gripWidth, + height: constants.gripHeight, + rx: constants.gripRadius, + ry: constants.gripRadius }) - .call(Color.stroke, sliderOpts.bordercolor) - .call(Color.fill, sliderOpts.bgcolor) - .style('stroke-width', sliderOpts.borderwidth + 'px'); + .call(Color.stroke, sliderOpts.bordercolor) + .call(Color.fill, sliderOpts.bgcolor) + .style("stroke-width", sliderOpts.borderwidth + "px"); } function drawLabel(item, data, sliderOpts) { - var text = item.selectAll('text') - .data([0]); + var text = item.selectAll("text").data([0]); - text.enter().append('text') - .classed(constants.labelClass, true) - .classed('user-select-none', true) - .attr('text-anchor', 'middle'); + text + .enter() + .append("text") + .classed(constants.labelClass, true) + .classed("user-select-none", true) + .attr("text-anchor", "middle"); - text.call(Drawing.font, sliderOpts.font) - .text(data.step.label) - .call(svgTextUtils.convertToTspans); + text + .call(Drawing.font, sliderOpts.font) + .text(data.step.label) + .call(svgTextUtils.convertToTspans); - return text; + return text; } function drawLabelGroup(sliderGroup, sliderOpts) { - var labels = sliderGroup.selectAll('g.' + constants.labelsClass) - .data([0]); + var labels = sliderGroup.selectAll("g." + constants.labelsClass).data([0]); - labels.enter().append('g') - .classed(constants.labelsClass, true); + labels.enter().append("g").classed(constants.labelsClass, true); - var labelItems = labels.selectAll('g.' + constants.labelGroupClass) - .data(sliderOpts.labelSteps); + var labelItems = labels + .selectAll("g." + constants.labelGroupClass) + .data(sliderOpts.labelSteps); - labelItems.enter().append('g') - .classed(constants.labelGroupClass, true); + labelItems.enter().append("g").classed(constants.labelGroupClass, true); - labelItems.exit().remove(); + labelItems.exit().remove(); - labelItems.each(function(d) { - var item = d3.select(this); + labelItems.each(function(d) { + var item = d3.select(this); - item.call(drawLabel, d, sliderOpts); - - Drawing.setTranslate(item, - normalizedValueToPosition(sliderOpts, d.fraction), - constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight + constants.labelOffset + sliderOpts.currentValueTotalHeight - ); - }); + item.call(drawLabel, d, sliderOpts); + Drawing.setTranslate( + item, + normalizedValueToPosition(sliderOpts, d.fraction), + constants.tickOffset + + sliderOpts.ticklen + + sliderOpts.labelHeight + + constants.labelOffset + + sliderOpts.currentValueTotalHeight + ); + }); } -function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, doTransition) { - var quantizedPosition = Math.round(normalizedPosition * (sliderOpts.steps.length - 1)); - - if(quantizedPosition !== sliderOpts.active) { - setActive(gd, sliderGroup, sliderOpts, quantizedPosition, true, doTransition); - } +function handleInput( + gd, + sliderGroup, + sliderOpts, + normalizedPosition, + doTransition +) { + var quantizedPosition = Math.round( + normalizedPosition * (sliderOpts.steps.length - 1) + ); + + if (quantizedPosition !== sliderOpts.active) { + setActive( + gd, + sliderGroup, + sliderOpts, + quantizedPosition, + true, + doTransition + ); + } } -function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) { - var previousActive = sliderOpts.active; - sliderOpts._input.active = sliderOpts.active = index; - - var step = sliderOpts.steps[sliderOpts.active]; - - sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition); - sliderGroup.call(drawCurrentValue, sliderOpts); - - gd.emit('plotly_sliderchange', { - slider: sliderOpts, - step: sliderOpts.steps[sliderOpts.active], - interaction: doCallback, - previousActive: previousActive - }); - - if(step && step.method && doCallback) { - if(sliderGroup._nextMethod) { - // If we've already queued up an update, just overwrite it with the most recent: - sliderGroup._nextMethod.step = step; - sliderGroup._nextMethod.doCallback = doCallback; - sliderGroup._nextMethod.doTransition = doTransition; - } else { - sliderGroup._nextMethod = {step: step, doCallback: doCallback, doTransition: doTransition}; - sliderGroup._nextMethodRaf = window.requestAnimationFrame(function() { - var _step = sliderGroup._nextMethod.step; - if(!_step.method) return; - - Plots.executeAPICommand(gd, _step.method, _step.args); - - sliderGroup._nextMethod = null; - sliderGroup._nextMethodRaf = null; - }); - } +function setActive( + gd, + sliderGroup, + sliderOpts, + index, + doCallback, + doTransition +) { + var previousActive = sliderOpts.active; + sliderOpts._input.active = sliderOpts.active = index; + + var step = sliderOpts.steps[sliderOpts.active]; + + sliderGroup.call( + setGripPosition, + sliderOpts, + sliderOpts.active / (sliderOpts.steps.length - 1), + doTransition + ); + sliderGroup.call(drawCurrentValue, sliderOpts); + + gd.emit("plotly_sliderchange", { + slider: sliderOpts, + step: sliderOpts.steps[sliderOpts.active], + interaction: doCallback, + previousActive: previousActive + }); + + if (step && step.method && doCallback) { + if (sliderGroup._nextMethod) { + // If we've already queued up an update, just overwrite it with the most recent: + sliderGroup._nextMethod.step = step; + sliderGroup._nextMethod.doCallback = doCallback; + sliderGroup._nextMethod.doTransition = doTransition; + } else { + sliderGroup._nextMethod = { + step: step, + doCallback: doCallback, + doTransition: doTransition + }; + sliderGroup._nextMethodRaf = window.requestAnimationFrame(function() { + var _step = sliderGroup._nextMethod.step; + if (!_step.method) return; + + Plots.executeAPICommand(gd, _step.method, _step.args); + + sliderGroup._nextMethod = null; + sliderGroup._nextMethodRaf = null; + }); } + } } function attachGripEvents(item, gd, sliderGroup) { - var node = sliderGroup.node(); - var $gd = d3.select(gd); - - // NB: This is *not* the same as sliderOpts itself! These callbacks - // are in a closure so this array won't actually be correct if the - // steps have changed since this was initialized. The sliderGroup, - // however, has not changed since that *is* the slider, so it must - // be present to receive mouse events. - function getSliderOpts() { - return sliderGroup.data()[0]; - } + var node = sliderGroup.node(); + var $gd = d3.select(gd); + + // NB: This is *not* the same as sliderOpts itself! These callbacks + // are in a closure so this array won't actually be correct if the + // steps have changed since this was initialized. The sliderGroup, + // however, has not changed since that *is* the slider, so it must + // be present to receive mouse events. + function getSliderOpts() { + return sliderGroup.data()[0]; + } + + item.on("mousedown", function() { + var sliderOpts = getSliderOpts(); + gd.emit("plotly_sliderstart", { slider: sliderOpts }); + + var grip = sliderGroup.select("." + constants.gripRectClass); + + d3.event.stopPropagation(); + d3.event.preventDefault(); + grip.call(Color.fill, sliderOpts.activebgcolor); + + var normalizedPosition = positionToNormalizedValue( + sliderOpts, + d3.mouse(node)[0] + ); + handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, true); + sliderOpts._dragging = true; + + $gd.on("mousemove", function() { + var sliderOpts = getSliderOpts(); + var normalizedPosition = positionToNormalizedValue( + sliderOpts, + d3.mouse(node)[0] + ); + handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, false); + }); + + $gd.on("mouseup", function() { + var sliderOpts = getSliderOpts(); + sliderOpts._dragging = false; + grip.call(Color.fill, sliderOpts.bgcolor); + $gd.on("mouseup", null); + $gd.on("mousemove", null); - item.on('mousedown', function() { - var sliderOpts = getSliderOpts(); - gd.emit('plotly_sliderstart', {slider: sliderOpts}); - - var grip = sliderGroup.select('.' + constants.gripRectClass); - - d3.event.stopPropagation(); - d3.event.preventDefault(); - grip.call(Color.fill, sliderOpts.activebgcolor); - - var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); - handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, true); - sliderOpts._dragging = true; - - $gd.on('mousemove', function() { - var sliderOpts = getSliderOpts(); - var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); - handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, false); - }); - - $gd.on('mouseup', function() { - var sliderOpts = getSliderOpts(); - sliderOpts._dragging = false; - grip.call(Color.fill, sliderOpts.bgcolor); - $gd.on('mouseup', null); - $gd.on('mousemove', null); - - gd.emit('plotly_sliderend', { - slider: sliderOpts, - step: sliderOpts.steps[sliderOpts.active] - }); - }); + gd.emit("plotly_sliderend", { + slider: sliderOpts, + step: sliderOpts.steps[sliderOpts.active] + }); }); + }); } function drawTicks(sliderGroup, sliderOpts) { - var tick = sliderGroup.selectAll('rect.' + constants.tickRectClass) - .data(sliderOpts.steps); + var tick = sliderGroup + .selectAll("rect." + constants.tickRectClass) + .data(sliderOpts.steps); - tick.enter().append('rect') - .classed(constants.tickRectClass, true); + tick.enter().append("rect").classed(constants.tickRectClass, true); - tick.exit().remove(); + tick.exit().remove(); - tick.attr({ - width: sliderOpts.tickwidth + 'px', - 'shape-rendering': 'crispEdges' - }); - - tick.each(function(d, i) { - var isMajor = i % sliderOpts.labelStride === 0; - var item = d3.select(this); + tick.attr({ + width: sliderOpts.tickwidth + "px", + "shape-rendering": "crispEdges" + }); - item - .attr({height: isMajor ? sliderOpts.ticklen : sliderOpts.minorticklen}) - .call(Color.fill, isMajor ? sliderOpts.tickcolor : sliderOpts.tickcolor); + tick.each(function(d, i) { + var isMajor = i % sliderOpts.labelStride === 0; + var item = d3.select(this); - Drawing.setTranslate(item, - normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * sliderOpts.tickwidth, - (isMajor ? constants.tickOffset : constants.minorTickOffset) + sliderOpts.currentValueTotalHeight - ); - }); + item + .attr({ height: isMajor ? sliderOpts.ticklen : sliderOpts.minorticklen }) + .call(Color.fill, isMajor ? sliderOpts.tickcolor : sliderOpts.tickcolor); + Drawing.setTranslate( + item, + normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - + 0.5 * sliderOpts.tickwidth, + (isMajor ? constants.tickOffset : constants.minorTickOffset) + + sliderOpts.currentValueTotalHeight + ); + }); } function computeLabelSteps(sliderOpts) { - sliderOpts.labelSteps = []; - var i0 = 0; - var nsteps = sliderOpts.steps.length; - - for(var i = i0; i < nsteps; i += sliderOpts.labelStride) { - sliderOpts.labelSteps.push({ - fraction: i / (nsteps - 1), - step: sliderOpts.steps[i] - }); - } + sliderOpts.labelSteps = []; + var i0 = 0; + var nsteps = sliderOpts.steps.length; + + for (var i = i0; i < nsteps; i += sliderOpts.labelStride) { + sliderOpts.labelSteps.push({ + fraction: i / (nsteps - 1), + step: sliderOpts.steps[i] + }); + } } function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { - var grip = sliderGroup.select('rect.' + constants.gripRectClass); - - var x = normalizedValueToPosition(sliderOpts, position); - - // If this is true, then *this component* is already invoking its own command - // and has triggered its own animation. - if(sliderOpts._invokingCommand) return; - - var el = grip; - if(doTransition && sliderOpts.transition.duration > 0) { - el = el.transition() - .duration(sliderOpts.transition.duration) - .ease(sliderOpts.transition.easing); - } - - // Drawing.setTranslate doesn't work here becasue of the transition duck-typing. - // It's also not necessary because there are no other transitions to preserve. - el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + (sliderOpts.currentValueTotalHeight) + ')'); + var grip = sliderGroup.select("rect." + constants.gripRectClass); + + var x = normalizedValueToPosition(sliderOpts, position); + + // If this is true, then *this component* is already invoking its own command + // and has triggered its own animation. + if (sliderOpts._invokingCommand) return; + + var el = grip; + if (doTransition && sliderOpts.transition.duration > 0) { + el = el + .transition() + .duration(sliderOpts.transition.duration) + .ease(sliderOpts.transition.easing); + } + + // Drawing.setTranslate doesn't work here becasue of the transition duck-typing. + // It's also not necessary because there are no other transitions to preserve. + el.attr( + "transform", + "translate(" + + (x - constants.gripWidth * 0.5) + + "," + + sliderOpts.currentValueTotalHeight + + ")" + ); } // Convert a number from [0-1] to a pixel position relative to the slider group container: function normalizedValueToPosition(sliderOpts, normalizedPosition) { - return sliderOpts.inputAreaStart + constants.stepInset + - (sliderOpts.inputAreaLength - 2 * constants.stepInset) * Math.min(1, Math.max(0, normalizedPosition)); + return sliderOpts.inputAreaStart + + constants.stepInset + + (sliderOpts.inputAreaLength - 2 * constants.stepInset) * + Math.min(1, Math.max(0, normalizedPosition)); } // Convert a position relative to the slider group to a nubmer in [0, 1] function positionToNormalizedValue(sliderOpts, position) { - return Math.min(1, Math.max(0, (position - constants.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * constants.stepInset - 2 * sliderOpts.inputAreaStart))); + return Math.min( + 1, + Math.max( + 0, + (position - constants.stepInset - sliderOpts.inputAreaStart) / + (sliderOpts.inputAreaLength - + 2 * constants.stepInset - + 2 * sliderOpts.inputAreaStart) + ) + ); } function drawTouchRect(sliderGroup, gd, sliderOpts) { - var rect = sliderGroup.selectAll('rect.' + constants.railTouchRectClass) - .data([0]); - - rect.enter().append('rect') - .classed(constants.railTouchRectClass, true) - .call(attachGripEvents, gd, sliderGroup, sliderOpts) - .style('pointer-events', 'all'); - - rect.attr({ - width: sliderOpts.inputAreaLength, - height: Math.max(sliderOpts.inputAreaWidth, constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight) + var rect = sliderGroup + .selectAll("rect." + constants.railTouchRectClass) + .data([0]); + + rect + .enter() + .append("rect") + .classed(constants.railTouchRectClass, true) + .call(attachGripEvents, gd, sliderGroup, sliderOpts) + .style("pointer-events", "all"); + + rect + .attr({ + width: sliderOpts.inputAreaLength, + height: Math.max( + sliderOpts.inputAreaWidth, + constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight + ) }) - .call(Color.fill, sliderOpts.bgcolor) - .attr('opacity', 0); + .call(Color.fill, sliderOpts.bgcolor) + .attr("opacity", 0); - Drawing.setTranslate(rect, 0, sliderOpts.currentValueTotalHeight); + Drawing.setTranslate(rect, 0, sliderOpts.currentValueTotalHeight); } function drawRail(sliderGroup, sliderOpts) { - var rect = sliderGroup.selectAll('rect.' + constants.railRectClass) - .data([0]); + var rect = sliderGroup.selectAll("rect." + constants.railRectClass).data([0]); - rect.enter().append('rect') - .classed(constants.railRectClass, true); + rect.enter().append("rect").classed(constants.railRectClass, true); - var computedLength = sliderOpts.inputAreaLength - constants.railInset * 2; + var computedLength = sliderOpts.inputAreaLength - constants.railInset * 2; - rect.attr({ - width: computedLength, - height: constants.railWidth, - rx: constants.railRadius, - ry: constants.railRadius, - 'shape-rendering': 'crispEdges' + rect + .attr({ + width: computedLength, + height: constants.railWidth, + rx: constants.railRadius, + ry: constants.railRadius, + "shape-rendering": "crispEdges" }) - .call(Color.stroke, sliderOpts.bordercolor) - .call(Color.fill, sliderOpts.bgcolor) - .style('stroke-width', sliderOpts.borderwidth + 'px'); - - Drawing.setTranslate(rect, - constants.railInset, - (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5 + sliderOpts.currentValueTotalHeight - ); + .call(Color.stroke, sliderOpts.bordercolor) + .call(Color.fill, sliderOpts.bgcolor) + .style("stroke-width", sliderOpts.borderwidth + "px"); + + Drawing.setTranslate( + rect, + constants.railInset, + (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5 + + sliderOpts.currentValueTotalHeight + ); } function clearPushMargins(gd) { - var pushMargins = gd._fullLayout._pushmargin || {}, - keys = Object.keys(pushMargins); + var pushMargins = gd._fullLayout._pushmargin || {}, + keys = Object.keys(pushMargins); - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; - if(k.indexOf(constants.autoMarginIdRoot) !== -1) { - Plots.autoMargin(gd, k); - } + if (k.indexOf(constants.autoMarginIdRoot) !== -1) { + Plots.autoMargin(gd, k); } + } } diff --git a/src/components/sliders/index.js b/src/components/sliders/index.js index 366b9aaa1ad..b44b6e9dab9 100644 --- a/src/components/sliders/index.js +++ b/src/components/sliders/index.js @@ -5,17 +5,13 @@ * 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 constants = require('./constants'); +"use strict"; +var constants = require("./constants"); module.exports = { - moduleType: 'component', - name: constants.name, - - layoutAttributes: require('./attributes'), - supplyLayoutDefaults: require('./defaults'), - - draw: require('./draw') + moduleType: "component", + name: constants.name, + layoutAttributes: require("./attributes"), + supplyLayoutDefaults: require("./defaults"), + draw: require("./draw") }; diff --git a/src/components/titles/index.js b/src/components/titles/index.js index 6fa0e64c6af..33171b7f68b 100644 --- a/src/components/titles/index.js +++ b/src/components/titles/index.js @@ -5,21 +5,17 @@ * 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 d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var Plotly = require('../../plotly'); -var Plots = require('../../plots/plots'); -var Lib = require('../../lib'); -var Drawing = require('../drawing'); -var Color = require('../color'); -var svgTextUtils = require('../../lib/svg_text_utils'); -var interactConstants = require('../../constants/interactions'); - +"use strict"; +var d3 = require("d3"); +var isNumeric = require("fast-isnumeric"); + +var Plotly = require("../../plotly"); +var Plots = require("../../plots/plots"); +var Lib = require("../../lib"); +var Drawing = require("../drawing"); +var Color = require("../color"); +var svgTextUtils = require("../../lib/svg_text_utils"); +var interactConstants = require("../../constants/interactions"); var Titles = module.exports = {}; @@ -52,180 +48,186 @@ var Titles = module.exports = {}; * title, include here. Otherwise it will go in fullLayout._infolayer */ Titles.draw = function(gd, titleClass, options) { - var cont = options.propContainer, - prop = options.propName, - traceIndex = options.traceIndex, - name = options.dfltName, - avoid = options.avoid || {}, - attributes = options.attributes, - transform = options.transform, - group = options.containerGroup, - - fullLayout = gd._fullLayout, - font = cont.titlefont.family, - fontSize = cont.titlefont.size, - fontColor = cont.titlefont.color, - - opacity = 1, - isplaceholder = false, - txt = cont.title.trim(); - if(txt === '') opacity = 0; - if(txt.match(/Click to enter .+ title/)) { - opacity = 0.2; - isplaceholder = true; - } - - if(!group) { - group = fullLayout._infolayer.selectAll('.g-' + titleClass) - .data([0]); - group.enter().append('g') - .classed('g-' + titleClass, true); - } - - var el = group.selectAll('text') - .data([0]); - el.enter().append('text'); - el.text(txt) - // this is hacky, but convertToTspans uses the class - // to determine whether to rotate mathJax... - // so we need to clear out any old class and put the - // correct one (only relevant for colorbars, at least - // for now) - ie don't use .classed - .attr('class', titleClass); - - function titleLayout(titleEl) { - Lib.syncOrAsync([drawTitle, scootTitle], titleEl); + var cont = options.propContainer, + prop = options.propName, + traceIndex = options.traceIndex, + name = options.dfltName, + avoid = options.avoid || {}, + attributes = options.attributes, + transform = options.transform, + group = options.containerGroup, + fullLayout = gd._fullLayout, + font = cont.titlefont.family, + fontSize = cont.titlefont.size, + fontColor = cont.titlefont.color, + opacity = 1, + isplaceholder = false, + txt = cont.title.trim(); + if (txt === "") opacity = 0; + if (txt.match(/Click to enter .+ title/)) { + opacity = 0.2; + isplaceholder = true; + } + + if (!group) { + group = fullLayout._infolayer.selectAll(".g-" + titleClass).data([0]); + group.enter().append("g").classed("g-" + titleClass, true); + } + + var el = group.selectAll("text").data([0]); + el.enter().append("text"); + el.text(txt).attr("class", titleClass); + + function titleLayout(titleEl) { + Lib.syncOrAsync([drawTitle, scootTitle], titleEl); + } + + function drawTitle(titleEl) { + titleEl.attr( + "transform", + transform + ? "rotate(" + + [transform.rotate, attributes.x, attributes.y] + + ") translate(0, " + + transform.offset + + ")" + : null + ); + + titleEl + .style({ + "font-family": font, + "font-size": d3.round(fontSize, 2) + "px", + fill: Color.rgb(fontColor), + opacity: opacity * Color.opacity(fontColor), + "font-weight": Plots.fontWeight + }) + .attr(attributes) + .call(svgTextUtils.convertToTspans) + .attr(attributes); + + titleEl.selectAll("tspan.line").attr(attributes); + return Plots.previousPromises(gd); + } + + function scootTitle(titleElIn) { + var titleGroup = d3.select(titleElIn.node().parentNode); + + if (avoid && avoid.selection && avoid.side && txt) { + titleGroup.attr("transform", null); + + // move toward avoid.side (= left, right, top, bottom) if needed + // can include pad (pixels, default 2) + var shift = 0, + backside = ({ + left: "right", + right: "left", + top: "bottom", + bottom: "top" + })[avoid.side], + shiftSign = ["left", "top"].indexOf(avoid.side) !== -1 ? -1 : 1, + pad = isNumeric(avoid.pad) ? avoid.pad : 2, + titlebb = Drawing.bBox(titleGroup.node()), + paperbb = { + left: 0, + top: 0, + right: fullLayout.width, + bottom: fullLayout.height + }, + maxshift = avoid.maxShift || + (paperbb[avoid.side] - titlebb[avoid.side]) * + (avoid.side === "left" || avoid.side === "top" ? -1 : 1); + // Prevent the title going off the paper + if (maxshift < 0) { + shift = maxshift; + } else { + // so we don't have to offset each avoided element, + // give the title the opposite offset + var offsetLeft = avoid.offsetLeft || 0, + offsetTop = avoid.offsetTop || 0; + titlebb.left -= offsetLeft; + titlebb.right -= offsetLeft; + titlebb.top -= offsetTop; + titlebb.bottom -= offsetTop; + + // iterate over a set of elements (avoid.selection) + // to avoid collisions with + avoid.selection.each(function() { + var avoidbb = Drawing.bBox(this); + + if (Lib.bBoxIntersect(titlebb, avoidbb, pad)) { + shift = Math.max( + shift, + shiftSign * (avoidbb[avoid.side] - titlebb[backside]) + pad + ); + } + }); + shift = Math.min(maxshift, shift); + } + if (shift > 0 || maxshift < 0) { + var shiftTemplate = ({ + left: [-shift, 0], + right: [shift, 0], + top: [0, -shift], + bottom: [0, shift] + })[avoid.side]; + titleGroup.attr("transform", "translate(" + shiftTemplate + ")"); + } } - - function drawTitle(titleEl) { - titleEl.attr('transform', transform ? - 'rotate(' + [transform.rotate, attributes.x, attributes.y] + - ') translate(0, ' + transform.offset + ')' : - null); - - titleEl.style({ - 'font-family': font, - 'font-size': d3.round(fontSize, 2) + 'px', - fill: Color.rgb(fontColor), - opacity: opacity * Color.opacity(fontColor), - 'font-weight': Plots.fontWeight - }) - .attr(attributes) - .call(svgTextUtils.convertToTspans) - .attr(attributes); - - titleEl.selectAll('tspan.line') - .attr(attributes); - return Plots.previousPromises(gd); - } - - function scootTitle(titleElIn) { - var titleGroup = d3.select(titleElIn.node().parentNode); - - if(avoid && avoid.selection && avoid.side && txt) { - titleGroup.attr('transform', null); - - // move toward avoid.side (= left, right, top, bottom) if needed - // can include pad (pixels, default 2) - var shift = 0, - backside = { - left: 'right', - right: 'left', - top: 'bottom', - bottom: 'top' - }[avoid.side], - shiftSign = (['left', 'top'].indexOf(avoid.side) !== -1) ? - -1 : 1, - pad = isNumeric(avoid.pad) ? avoid.pad : 2, - titlebb = Drawing.bBox(titleGroup.node()), - paperbb = { - left: 0, - top: 0, - right: fullLayout.width, - bottom: fullLayout.height - }, - maxshift = avoid.maxShift || ( - (paperbb[avoid.side] - titlebb[avoid.side]) * - ((avoid.side === 'left' || avoid.side === 'top') ? -1 : 1)); - // Prevent the title going off the paper - if(maxshift < 0) shift = maxshift; - else { - // so we don't have to offset each avoided element, - // give the title the opposite offset - var offsetLeft = avoid.offsetLeft || 0, - offsetTop = avoid.offsetTop || 0; - titlebb.left -= offsetLeft; - titlebb.right -= offsetLeft; - titlebb.top -= offsetTop; - titlebb.bottom -= offsetTop; - - // iterate over a set of elements (avoid.selection) - // to avoid collisions with - avoid.selection.each(function() { - var avoidbb = Drawing.bBox(this); - - if(Lib.bBoxIntersect(titlebb, avoidbb, pad)) { - shift = Math.max(shift, shiftSign * ( - avoidbb[avoid.side] - titlebb[backside]) + pad); - } - }); - shift = Math.min(maxshift, shift); - } - if(shift > 0 || maxshift < 0) { - var shiftTemplate = { - left: [-shift, 0], - right: [shift, 0], - top: [0, -shift], - bottom: [0, shift] - }[avoid.side]; - titleGroup.attr('transform', - 'translate(' + shiftTemplate + ')'); - } + } + + el.attr({ "data-unformatted": txt }).call(titleLayout); + + var placeholderText = "Click to enter " + name + " title"; + + function setPlaceholder() { + opacity = 0; + isplaceholder = true; + txt = placeholderText; + el + .attr({ "data-unformatted": txt }) + .text(txt) + .on("mouseover.opacity", function() { + d3 + .select(this) + .transition() + .duration(interactConstants.SHOW_PLACEHOLDER) + .style("opacity", 1); + }) + .on("mouseout.opacity", function() { + d3 + .select(this) + .transition() + .duration(interactConstants.HIDE_PLACEHOLDER) + .style("opacity", 0); + }); + } + + if (gd._context.editable) { + if (!txt) setPlaceholder(); + else el.on(".opacity", null); + + el + .call(svgTextUtils.makeEditable) + .on("edit", function(text) { + if (traceIndex !== undefined) { + Plotly.restyle(gd, prop, text, traceIndex); + } else { + Plotly.relayout(gd, prop, text); } - } - - el.attr({'data-unformatted': txt}) - .call(titleLayout); - - var placeholderText = 'Click to enter ' + name + ' title'; - - function setPlaceholder() { - opacity = 0; - isplaceholder = true; - txt = placeholderText; - el.attr({'data-unformatted': txt}) - .text(txt) - .on('mouseover.opacity', function() { - d3.select(this).transition() - .duration(interactConstants.SHOW_PLACEHOLDER).style('opacity', 1); - }) - .on('mouseout.opacity', function() { - d3.select(this).transition() - .duration(interactConstants.HIDE_PLACEHOLDER).style('opacity', 0); - }); - } - - if(gd._context.editable) { - if(!txt) setPlaceholder(); - else el.on('.opacity', null); - - el.call(svgTextUtils.makeEditable) - .on('edit', function(text) { - if(traceIndex !== undefined) Plotly.restyle(gd, prop, text, traceIndex); - else Plotly.relayout(gd, prop, text); - }) - .on('cancel', function() { - this.text(this.attr('data-unformatted')) - .call(titleLayout); - }) - .on('input', function(d) { - this.text(d || ' ').attr(attributes) - .selectAll('tspan.line') - .attr(attributes); - }); - } - else if(!txt || txt.match(/Click to enter .+ title/)) { - el.remove(); - } - el.classed('js-placeholder', isplaceholder); + }) + .on("cancel", function() { + this.text(this.attr("data-unformatted")).call(titleLayout); + }) + .on("input", function(d) { + this + .text(d || " ") + .attr(attributes) + .selectAll("tspan.line") + .attr(attributes); + }); + } else if (!txt || txt.match(/Click to enter .+ title/)) { + el.remove(); + } + el.classed("js-placeholder", isplaceholder); }; diff --git a/src/components/updatemenus/attributes.js b/src/components/updatemenus/attributes.js index f04465f9ec3..14b36849a96 100644 --- a/src/components/updatemenus/attributes.js +++ b/src/components/updatemenus/attributes.js @@ -5,166 +5,147 @@ * 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 fontAttrs = require('../../plots/font_attributes'); -var colorAttrs = require('../color/attributes'); -var extendFlat = require('../../lib/extend').extendFlat; -var padAttrs = require('../../plots/pad_attributes'); +"use strict"; +var fontAttrs = require("../../plots/font_attributes"); +var colorAttrs = require("../color/attributes"); +var extendFlat = require("../../lib/extend").extendFlat; +var padAttrs = require("../../plots/pad_attributes"); var buttonsAttrs = { - _isLinkedToArray: 'button', - - method: { - valType: 'enumerated', - values: ['restyle', 'relayout', 'animate', 'update'], - dflt: 'restyle', - role: 'info', - description: [ - 'Sets the Plotly method to be called on click.' - ].join(' ') - }, - args: { - valType: 'info_array', - role: 'info', - freeLength: true, - items: [ - { valType: 'any' }, - { valType: 'any' }, - { valType: 'any' } - ], - description: [ - 'Sets the arguments values to be passed to the Plotly', - 'method set in `method` on click.' - ].join(' ') - }, - label: { - valType: 'string', - role: 'info', - dflt: '', - description: 'Sets the text label to appear on the button.' - } + _isLinkedToArray: "button", + method: { + valType: "enumerated", + values: ["restyle", "relayout", "animate", "update"], + dflt: "restyle", + role: "info", + description: ["Sets the Plotly method to be called on click."].join(" ") + }, + args: { + valType: "info_array", + role: "info", + freeLength: true, + items: [{ valType: "any" }, { valType: "any" }, { valType: "any" }], + description: [ + "Sets the arguments values to be passed to the Plotly", + "method set in `method` on click." + ].join(" ") + }, + label: { + valType: "string", + role: "info", + dflt: "", + description: "Sets the text label to appear on the button." + } }; module.exports = { - _isLinkedToArray: 'updatemenu', - - visible: { - valType: 'boolean', - role: 'info', - description: [ - 'Determines whether or not the update menu is visible.' - ].join(' ') - }, - - type: { - valType: 'enumerated', - values: ['dropdown', 'buttons'], - dflt: 'dropdown', - role: 'info', - description: [ - 'Determines whether the buttons are accessible via a dropdown menu', - 'or whether the buttons are stacked horizontally or vertically' - ].join(' ') - }, - - direction: { - valType: 'enumerated', - values: ['left', 'right', 'up', 'down'], - dflt: 'down', - role: 'info', - description: [ - 'Determines the direction in which the buttons are laid out, whether', - 'in a dropdown menu or a row/column of buttons. For `left` and `up`,', - 'the buttons will still appear in left-to-right or top-to-bottom order', - 'respectively.' - ].join(' ') - }, - - active: { - valType: 'integer', - role: 'info', - min: -1, - dflt: 0, - description: [ - 'Determines which button (by index starting from 0) is', - 'considered active.' - ].join(' ') - }, - - showactive: { - valType: 'boolean', - role: 'info', - dflt: true, - description: 'Highlights active dropdown item or active button if true.' - }, - - buttons: buttonsAttrs, - - x: { - valType: 'number', - min: -2, - max: 3, - dflt: -0.05, - role: 'style', - description: 'Sets the x position (in normalized coordinates) of the update menu.' - }, - xanchor: { - valType: 'enumerated', - values: ['auto', 'left', 'center', 'right'], - dflt: 'right', - role: 'info', - description: [ - 'Sets the update menu\'s horizontal position anchor.', - 'This anchor binds the `x` position to the *left*, *center*', - 'or *right* of the range selector.' - ].join(' ') - }, - y: { - valType: 'number', - min: -2, - max: 3, - dflt: 1, - role: 'style', - description: 'Sets the y position (in normalized coordinates) of the update menu.' - }, - yanchor: { - valType: 'enumerated', - values: ['auto', 'top', 'middle', 'bottom'], - dflt: 'top', - role: 'info', - description: [ - 'Sets the update menu\'s vertical position anchor', - 'This anchor binds the `y` position to the *top*, *middle*', - 'or *bottom* of the range selector.' - ].join(' ') - }, - - pad: extendFlat({}, padAttrs, { - description: 'Sets the padding around the buttons or dropdown menu.' - }), - - font: extendFlat({}, fontAttrs, { - description: 'Sets the font of the update menu button text.' - }), - - bgcolor: { - valType: 'color', - role: 'style', - description: 'Sets the background color of the update menu buttons.' - }, - bordercolor: { - valType: 'color', - dflt: colorAttrs.borderLine, - role: 'style', - description: 'Sets the color of the border enclosing the update menu.' - }, - borderwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the border enclosing the update menu.' - } + _isLinkedToArray: "updatemenu", + visible: { + valType: "boolean", + role: "info", + description: ["Determines whether or not the update menu is visible."].join( + " " + ) + }, + type: { + valType: "enumerated", + values: ["dropdown", "buttons"], + dflt: "dropdown", + role: "info", + description: [ + "Determines whether the buttons are accessible via a dropdown menu", + "or whether the buttons are stacked horizontally or vertically" + ].join(" ") + }, + direction: { + valType: "enumerated", + values: ["left", "right", "up", "down"], + dflt: "down", + role: "info", + description: [ + "Determines the direction in which the buttons are laid out, whether", + "in a dropdown menu or a row/column of buttons. For `left` and `up`,", + "the buttons will still appear in left-to-right or top-to-bottom order", + "respectively." + ].join(" ") + }, + active: { + valType: "integer", + role: "info", + min: -1, + dflt: 0, + description: [ + "Determines which button (by index starting from 0) is", + "considered active." + ].join(" ") + }, + showactive: { + valType: "boolean", + role: "info", + dflt: true, + description: "Highlights active dropdown item or active button if true." + }, + buttons: buttonsAttrs, + x: { + valType: "number", + min: -2, + max: 3, + dflt: -0.05, + role: "style", + description: "Sets the x position (in normalized coordinates) of the update menu." + }, + xanchor: { + valType: "enumerated", + values: ["auto", "left", "center", "right"], + dflt: "right", + role: "info", + description: [ + "Sets the update menu's horizontal position anchor.", + "This anchor binds the `x` position to the *left*, *center*", + "or *right* of the range selector." + ].join(" ") + }, + y: { + valType: "number", + min: -2, + max: 3, + dflt: 1, + role: "style", + description: "Sets the y position (in normalized coordinates) of the update menu." + }, + yanchor: { + valType: "enumerated", + values: ["auto", "top", "middle", "bottom"], + dflt: "top", + role: "info", + description: [ + "Sets the update menu's vertical position anchor", + "This anchor binds the `y` position to the *top*, *middle*", + "or *bottom* of the range selector." + ].join(" ") + }, + pad: extendFlat({}, padAttrs, { + description: "Sets the padding around the buttons or dropdown menu." + }), + font: extendFlat({}, fontAttrs, { + description: "Sets the font of the update menu button text." + }), + bgcolor: { + valType: "color", + role: "style", + description: "Sets the background color of the update menu buttons." + }, + bordercolor: { + valType: "color", + dflt: colorAttrs.borderLine, + role: "style", + description: "Sets the color of the border enclosing the update menu." + }, + borderwidth: { + valType: "number", + min: 0, + dflt: 1, + role: "style", + description: "Sets the width (in px) of the border enclosing the update menu." + } }; diff --git a/src/components/updatemenus/constants.js b/src/components/updatemenus/constants.js index b1c7a2e3ef0..5f0f0f70c74 100644 --- a/src/components/updatemenus/constants.js +++ b/src/components/updatemenus/constants.js @@ -5,70 +5,50 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; module.exports = { - - // layout attribute name - name: 'updatemenus', - - // class names - containerClassName: 'updatemenu-container', - headerGroupClassName: 'updatemenu-header-group', - headerClassName: 'updatemenu-header', - headerArrowClassName: 'updatemenu-header-arrow', - dropdownButtonGroupClassName: 'updatemenu-dropdown-button-group', - dropdownButtonClassName: 'updatemenu-dropdown-button', - buttonClassName: 'updatemenu-button', - itemRectClassName: 'updatemenu-item-rect', - itemTextClassName: 'updatemenu-item-text', - - // DOM attribute name in button group keeping track - // of active update menu - menuIndexAttrName: 'updatemenu-active-index', - - // id root pass to Plots.autoMargin - autoMarginIdRoot: 'updatemenu-', - - // options when 'active: -1' - blankHeaderOpts: { label: ' ' }, - - // min item width / height - minWidth: 30, - minHeight: 30, - - // padding around item text - textPadX: 24, - arrowPadX: 16, - - // font size to height scale - fontSizeToHeight: 1.3, - - // item rect radii - rx: 2, - ry: 2, - - // item text x offset off left edge - textOffsetX: 12, - - // item text y offset (w.r.t. middle) - textOffsetY: 3, - - // arrow offset off right edge - arrowOffsetX: 4, - - // gap between header and buttons - gapButtonHeader: 5, - - // gap between between buttons - gapButton: 2, - - // color given to active buttons - activeColor: '#F4FAFF', - - // color given to hovered buttons - hoverColor: '#F4FAFF' + // layout attribute name + name: "updatemenus", + // class names + containerClassName: "updatemenu-container", + headerGroupClassName: "updatemenu-header-group", + headerClassName: "updatemenu-header", + headerArrowClassName: "updatemenu-header-arrow", + dropdownButtonGroupClassName: "updatemenu-dropdown-button-group", + dropdownButtonClassName: "updatemenu-dropdown-button", + buttonClassName: "updatemenu-button", + itemRectClassName: "updatemenu-item-rect", + itemTextClassName: "updatemenu-item-text", + // DOM attribute name in button group keeping track + // of active update menu + menuIndexAttrName: "updatemenu-active-index", + // id root pass to Plots.autoMargin + autoMarginIdRoot: "updatemenu-", + // options when 'active: -1' + blankHeaderOpts: { label: " " }, + // min item width / height + minWidth: 30, + minHeight: 30, + // padding around item text + textPadX: 24, + arrowPadX: 16, + // font size to height scale + fontSizeToHeight: 1.3, + // item rect radii + rx: 2, + ry: 2, + // item text x offset off left edge + textOffsetX: 12, + // item text y offset (w.r.t. middle) + textOffsetY: 3, + // arrow offset off right edge + arrowOffsetX: 4, + // gap between header and buttons + gapButtonHeader: 5, + // gap between between buttons + gapButton: 2, + // color given to active buttons + activeColor: "#F4FAFF", + // color given to hovered buttons + hoverColor: "#F4FAFF" }; diff --git a/src/components/updatemenus/defaults.js b/src/components/updatemenus/defaults.js index 2d4eeae7faa..f98b8e0b50f 100644 --- a/src/components/updatemenus/defaults.js +++ b/src/components/updatemenus/defaults.js @@ -5,88 +5,82 @@ * 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 handleArrayContainerDefaults = require( + "../../plots/array_container_defaults" +); -'use strict'; - -var Lib = require('../../lib'); -var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); - -var attributes = require('./attributes'); -var constants = require('./constants'); +var attributes = require("./attributes"); +var constants = require("./constants"); var name = constants.name; var buttonAttrs = attributes.buttons; - module.exports = function updateMenusDefaults(layoutIn, layoutOut) { - var opts = { - name: name, - handleItemDefaults: menuDefaults - }; + var opts = { name: name, handleItemDefaults: menuDefaults }; - handleArrayContainerDefaults(layoutIn, layoutOut, opts); + handleArrayContainerDefaults(layoutIn, layoutOut, opts); }; function menuDefaults(menuIn, menuOut, layoutOut) { + function coerce(attr, dflt) { + return Lib.coerce(menuIn, menuOut, attributes, attr, dflt); + } - function coerce(attr, dflt) { - return Lib.coerce(menuIn, menuOut, attributes, attr, dflt); - } - - var buttons = buttonsDefaults(menuIn, menuOut); + var buttons = buttonsDefaults(menuIn, menuOut); - var visible = coerce('visible', buttons.length > 0); - if(!visible) return; + var visible = coerce("visible", buttons.length > 0); + if (!visible) return; - coerce('active'); - coerce('direction'); - coerce('type'); - coerce('showactive'); + coerce("active"); + coerce("direction"); + coerce("type"); + coerce("showactive"); - coerce('x'); - coerce('y'); - Lib.noneOrAll(menuIn, menuOut, ['x', 'y']); + coerce("x"); + coerce("y"); + Lib.noneOrAll(menuIn, menuOut, ["x", "y"]); - coerce('xanchor'); - coerce('yanchor'); + coerce("xanchor"); + coerce("yanchor"); - coerce('pad.t'); - coerce('pad.r'); - coerce('pad.b'); - coerce('pad.l'); + coerce("pad.t"); + coerce("pad.r"); + coerce("pad.b"); + coerce("pad.l"); - Lib.coerceFont(coerce, 'font', layoutOut.font); + Lib.coerceFont(coerce, "font", layoutOut.font); - coerce('bgcolor', layoutOut.paper_bgcolor); - coerce('bordercolor'); - coerce('borderwidth'); + coerce("bgcolor", layoutOut.paper_bgcolor); + coerce("bordercolor"); + coerce("borderwidth"); } function buttonsDefaults(menuIn, menuOut) { - var buttonsIn = menuIn.buttons || [], - buttonsOut = menuOut.buttons = []; + var buttonsIn = menuIn.buttons || [], buttonsOut = menuOut.buttons = []; - var buttonIn, buttonOut; + var buttonIn, buttonOut; - function coerce(attr, dflt) { - return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt); - } + function coerce(attr, dflt) { + return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt); + } - for(var i = 0; i < buttonsIn.length; i++) { - buttonIn = buttonsIn[i]; - buttonOut = {}; + for (var i = 0; i < buttonsIn.length; i++) { + buttonIn = buttonsIn[i]; + buttonOut = {}; - if(!Lib.isPlainObject(buttonIn) || !Array.isArray(buttonIn.args)) { - continue; - } + if (!Lib.isPlainObject(buttonIn) || !Array.isArray(buttonIn.args)) { + continue; + } - coerce('method'); - coerce('args'); - coerce('label'); + coerce("method"); + coerce("args"); + coerce("label"); - buttonOut._index = i; - buttonsOut.push(buttonOut); - } + buttonOut._index = i; + buttonsOut.push(buttonOut); + } - return buttonsOut; + return buttonsOut; } diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 3c9c30968b2..ddd70dcf395 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -5,26 +5,22 @@ * 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 d3 = require("d3"); +var Plots = require("../../plots/plots"); +var Color = require("../color"); +var Drawing = require("../drawing"); +var svgTextUtils = require("../../lib/svg_text_utils"); +var anchorUtils = require("../legend/anchor_utils"); -'use strict'; - -var d3 = require('d3'); - -var Plots = require('../../plots/plots'); -var Color = require('../color'); -var Drawing = require('../drawing'); -var svgTextUtils = require('../../lib/svg_text_utils'); -var anchorUtils = require('../legend/anchor_utils'); - -var constants = require('./constants'); -var ScrollBox = require('./scrollbox'); +var constants = require("./constants"); +var ScrollBox = require("./scrollbox"); module.exports = function draw(gd) { - var fullLayout = gd._fullLayout, - menuData = makeMenuData(fullLayout); + var fullLayout = gd._fullLayout, menuData = makeMenuData(fullLayout); - /* Update menu data is bound to the header-group. + /* Update menu data is bound to the header-group. * The items in the header group are always present. * * Upon clicking on a header its corresponding button @@ -50,104 +46,113 @@ module.exports = function draw(gd) { * * ... */ - - // draw update menu container - var menus = fullLayout._infolayer - .selectAll('g.' + constants.containerClassName) - .data(menuData.length > 0 ? [0] : []); - - menus.enter().append('g') - .classed(constants.containerClassName, true) - .style('cursor', 'pointer'); - - menus.exit().remove(); - - // remove push margin object(s) - if(menus.exit().size()) clearPushMargins(gd); - - // return early if no update menus are visible - if(menuData.length === 0) return; - - // join header group - var headerGroups = menus.selectAll('g.' + constants.headerGroupClassName) - .data(menuData, keyFunction); - - headerGroups.enter().append('g') - .classed(constants.headerGroupClassName, true); - - // draw dropdown button container - var gButton = menus.selectAll('g.' + constants.dropdownButtonGroupClassName) - .data([0]); - - gButton.enter().append('g') - .classed(constants.dropdownButtonGroupClassName, true) - .style('pointer-events', 'all'); - - // find dimensions before plotting anything (this mutates menuOpts) - for(var i = 0; i < menuData.length; i++) { - var menuOpts = menuData[i]; - findDimensions(gd, menuOpts); - } - - // setup scrollbox - var scrollBoxId = 'updatemenus' + fullLayout._uid, - scrollBox = new ScrollBox(gd, gButton, scrollBoxId); - - // remove exiting header, remove dropped buttons and reset margins - if(headerGroups.enter().size()) { - gButton - .call(removeAllButtons) - .attr(constants.menuIndexAttrName, '-1'); - } - - headerGroups.exit().each(function(menuOpts) { - d3.select(this).remove(); - - gButton - .call(removeAllButtons) - .attr(constants.menuIndexAttrName, '-1'); - - Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index); + // draw update menu container + var menus = fullLayout._infolayer + .selectAll("g." + constants.containerClassName) + .data(menuData.length > 0 ? [0] : []); + + menus + .enter() + .append("g") + .classed(constants.containerClassName, true) + .style("cursor", "pointer"); + + menus.exit().remove(); + + // remove push margin object(s) + if (menus.exit().size()) clearPushMargins(gd); + + // return early if no update menus are visible + if (menuData.length === 0) return; + + // join header group + var headerGroups = menus + .selectAll("g." + constants.headerGroupClassName) + .data(menuData, keyFunction); + + headerGroups + .enter() + .append("g") + .classed(constants.headerGroupClassName, true); + + // draw dropdown button container + var gButton = menus + .selectAll("g." + constants.dropdownButtonGroupClassName) + .data([0]); + + gButton + .enter() + .append("g") + .classed(constants.dropdownButtonGroupClassName, true) + .style("pointer-events", "all"); + + // find dimensions before plotting anything (this mutates menuOpts) + for (var i = 0; i < menuData.length; i++) { + var menuOpts = menuData[i]; + findDimensions(gd, menuOpts); + } + + // setup scrollbox + var scrollBoxId = "updatemenus" + fullLayout._uid, + scrollBox = new ScrollBox(gd, gButton, scrollBoxId); + + // remove exiting header, remove dropped buttons and reset margins + if (headerGroups.enter().size()) { + gButton.call(removeAllButtons).attr(constants.menuIndexAttrName, "-1"); + } + + headerGroups.exit().each(function(menuOpts) { + d3.select(this).remove(); + + gButton.call(removeAllButtons).attr(constants.menuIndexAttrName, "-1"); + + Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index); + }); + + // draw headers! + headerGroups.each(function(menuOpts) { + var gHeader = d3.select(this); + + var _gButton = menuOpts.type === "dropdown" ? gButton : null; + Plots.manageCommandObserver(gd, menuOpts, menuOpts.buttons, function(data) { + setActive( + gd, + menuOpts, + menuOpts.buttons[data.index], + gHeader, + _gButton, + scrollBox, + data.index, + true + ); }); - // draw headers! - headerGroups.each(function(menuOpts) { - var gHeader = d3.select(this); - - var _gButton = menuOpts.type === 'dropdown' ? gButton : null; - Plots.manageCommandObserver(gd, menuOpts, menuOpts.buttons, function(data) { - setActive(gd, menuOpts, menuOpts.buttons[data.index], gHeader, _gButton, scrollBox, data.index, true); - }); + if (menuOpts.type === "dropdown") { + drawHeader(gd, gHeader, gButton, scrollBox, menuOpts); - if(menuOpts.type === 'dropdown') { - drawHeader(gd, gHeader, gButton, scrollBox, menuOpts); - - // if this menu is active, update the dropdown container - if(isActive(gButton, menuOpts)) { - drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); - } - } else { - drawButtons(gd, gHeader, null, null, menuOpts); - } - - }); + // if this menu is active, update the dropdown container + if (isActive(gButton, menuOpts)) { + drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); + } + } else { + drawButtons(gd, gHeader, null, null, menuOpts); + } + }); }; function makeMenuData(fullLayout) { - var contOpts = fullLayout[constants.name], - menuData = []; + var contOpts = fullLayout[constants.name], menuData = []; - // Filter visible dropdowns and attach '_index' to each - // fullLayout options object to be used for 'object constancy' - // in the data join key function. + // Filter visible dropdowns and attach '_index' to each + // fullLayout options object to be used for 'object constancy' + // in the data join key function. + for (var i = 0; i < contOpts.length; i++) { + var item = contOpts[i]; - for(var i = 0; i < contOpts.length; i++) { - var item = contOpts[i]; + if (item.visible) menuData.push(item); + } - if(item.visible) menuData.push(item); - } - - return menuData; + return menuData; } // Note that '_index' is set at the default step, @@ -155,524 +160,547 @@ function makeMenuData(fullLayout) { // Because a menu can b set invisible, // this is a more 'consistent' field than the index in the menuData. function keyFunction(menuOpts) { - return menuOpts._index; + return menuOpts._index; } function isFolded(gButton) { - return +gButton.attr(constants.menuIndexAttrName) === -1; + return +gButton.attr(constants.menuIndexAttrName) === -1; } function isActive(gButton, menuOpts) { - return +gButton.attr(constants.menuIndexAttrName) === menuOpts._index; + return +gButton.attr(constants.menuIndexAttrName) === menuOpts._index; } -function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex, isSilentUpdate) { - // update 'active' attribute in menuOpts - menuOpts._input.active = menuOpts.active = buttonIndex; - - if(menuOpts.type === 'buttons') { - drawButtons(gd, gHeader, null, null, menuOpts); - } - else if(menuOpts.type === 'dropdown') { - // fold up buttons and redraw header - gButton.attr(constants.menuIndexAttrName, '-1'); - - drawHeader(gd, gHeader, gButton, scrollBox, menuOpts); - - if(!isSilentUpdate) { - drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); - } +function setActive( + gd, + menuOpts, + buttonOpts, + gHeader, + gButton, + scrollBox, + buttonIndex, + isSilentUpdate +) { + // update 'active' attribute in menuOpts + menuOpts._input.active = menuOpts.active = buttonIndex; + + if (menuOpts.type === "buttons") { + drawButtons(gd, gHeader, null, null, menuOpts); + } else if (menuOpts.type === "dropdown") { + // fold up buttons and redraw header + gButton.attr(constants.menuIndexAttrName, "-1"); + + drawHeader(gd, gHeader, gButton, scrollBox, menuOpts); + + if (!isSilentUpdate) { + drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); } + } } function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) { - var header = gHeader.selectAll('g.' + constants.headerClassName) - .data([0]); - - header.enter().append('g') - .classed(constants.headerClassName, true) - .style('pointer-events', 'all'); - - var active = menuOpts.active, - headerOpts = menuOpts.buttons[active] || constants.blankHeaderOpts, - posOpts = { y: menuOpts.pad.t, yPad: 0, x: menuOpts.pad.l, xPad: 0, index: 0 }, - positionOverrides = { - width: menuOpts.headerWidth, - height: menuOpts.headerHeight - }; - - header - .call(drawItem, menuOpts, headerOpts) - .call(setItemPosition, menuOpts, posOpts, positionOverrides); - - // draw drop arrow at the right edge - var arrow = gHeader.selectAll('text.' + constants.headerArrowClassName) - .data([0]); - - arrow.enter().append('text') - .classed(constants.headerArrowClassName, true) - .classed('user-select-none', true) - .attr('text-anchor', 'end') - .call(Drawing.font, menuOpts.font) - .text('▼'); - - arrow.attr({ - x: menuOpts.headerWidth - constants.arrowOffsetX + menuOpts.pad.l, - y: menuOpts.headerHeight / 2 + constants.textOffsetY + menuOpts.pad.t - }); - - header.on('click', function() { - gButton.call(removeAllButtons); - - - // if this menu is active, fold the dropdown container - // otherwise, make this menu active - gButton.attr( - constants.menuIndexAttrName, - isActive(gButton, menuOpts) ? - -1 : - String(menuOpts._index) - ); - - drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); - }); - - header.on('mouseover', function() { - header.call(styleOnMouseOver); - }); - - header.on('mouseout', function() { - header.call(styleOnMouseOut, menuOpts); - }); + var header = gHeader.selectAll("g." + constants.headerClassName).data([0]); + + header + .enter() + .append("g") + .classed(constants.headerClassName, true) + .style("pointer-events", "all"); + + var active = menuOpts.active, + headerOpts = menuOpts.buttons[active] || constants.blankHeaderOpts, + posOpts = { + y: menuOpts.pad.t, + yPad: 0, + x: menuOpts.pad.l, + xPad: 0, + index: 0 + }, + positionOverrides = { + width: menuOpts.headerWidth, + height: menuOpts.headerHeight + }; - // translate header group - Drawing.setTranslate(gHeader, menuOpts.lx, menuOpts.ly); + header + .call(drawItem, menuOpts, headerOpts) + .call(setItemPosition, menuOpts, posOpts, positionOverrides); + + // draw drop arrow at the right edge + var arrow = gHeader + .selectAll("text." + constants.headerArrowClassName) + .data([0]); + + arrow + .enter() + .append("text") + .classed(constants.headerArrowClassName, true) + .classed("user-select-none", true) + .attr("text-anchor", "end") + .call(Drawing.font, menuOpts.font) + .text("\u25BC"); + + arrow.attr({ + x: menuOpts.headerWidth - constants.arrowOffsetX + menuOpts.pad.l, + y: menuOpts.headerHeight / 2 + constants.textOffsetY + menuOpts.pad.t + }); + + header.on("click", function() { + gButton.call(removeAllButtons); + + // if this menu is active, fold the dropdown container + // otherwise, make this menu active + gButton.attr( + constants.menuIndexAttrName, + isActive(gButton, menuOpts) ? -1 : String(menuOpts._index) + ); + + drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); + }); + + header.on("mouseover", function() { + header.call(styleOnMouseOver); + }); + + header.on("mouseout", function() { + header.call(styleOnMouseOut, menuOpts); + }); + + // translate header group + Drawing.setTranslate(gHeader, menuOpts.lx, menuOpts.ly); } function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { - // If this is a set of buttons, set pointer events = all since we play - // some minor games with which container is which in order to simplify - // the drawing of *either* buttons or menus - if(!gButton) { - gButton = gHeader; - gButton.attr('pointer-events', 'all'); - } + // If this is a set of buttons, set pointer events = all since we play + // some minor games with which container is which in order to simplify + // the drawing of *either* buttons or menus + if (!gButton) { + gButton = gHeader; + gButton.attr("pointer-events", "all"); + } - var buttonData = (!isFolded(gButton) || menuOpts.type === 'buttons') ? - menuOpts.buttons : - []; + var buttonData = !isFolded(gButton) || menuOpts.type === "buttons" + ? menuOpts.buttons + : []; - var klass = menuOpts.type === 'dropdown' ? constants.dropdownButtonClassName : constants.buttonClassName; + var klass = menuOpts.type === "dropdown" + ? constants.dropdownButtonClassName + : constants.buttonClassName; - var buttons = gButton.selectAll('g.' + klass) - .data(buttonData); + var buttons = gButton.selectAll("g." + klass).data(buttonData); - var enter = buttons.enter().append('g') - .classed(klass, true); + var enter = buttons.enter().append("g").classed(klass, true); - var exit = buttons.exit(); + var exit = buttons.exit(); - if(menuOpts.type === 'dropdown') { - enter.attr('opacity', '0') - .transition() - .attr('opacity', '1'); + if (menuOpts.type === "dropdown") { + enter.attr("opacity", "0").transition().attr("opacity", "1"); - exit.transition() - .attr('opacity', '0') - .remove(); - } else { - exit.remove(); - } + exit.transition().attr("opacity", "0").remove(); + } else { + exit.remove(); + } - var x0 = 0; - var y0 = 0; + var x0 = 0; + var y0 = 0; - var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1; + var isVertical = ["up", "down"].indexOf(menuOpts.direction) !== -1; - if(menuOpts.type === 'dropdown') { - if(isVertical) { - y0 = menuOpts.headerHeight + constants.gapButtonHeader; - } else { - x0 = menuOpts.headerWidth + constants.gapButtonHeader; - } - } - - if(menuOpts.type === 'dropdown' && menuOpts.direction === 'up') { - y0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openHeight; - } - - if(menuOpts.type === 'dropdown' && menuOpts.direction === 'left') { - x0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openWidth; + if (menuOpts.type === "dropdown") { + if (isVertical) { + y0 = menuOpts.headerHeight + constants.gapButtonHeader; + } else { + x0 = menuOpts.headerWidth + constants.gapButtonHeader; } - - var posOpts = { - x: menuOpts.lx + x0 + menuOpts.pad.l, - y: menuOpts.ly + y0 + menuOpts.pad.t, - yPad: constants.gapButton, - xPad: constants.gapButton, - index: 0, - }; - - var scrollBoxPosition = { - l: posOpts.x + menuOpts.borderwidth, - t: posOpts.y + menuOpts.borderwidth - }; - - buttons.each(function(buttonOpts, buttonIndex) { - var button = d3.select(this); - - button - .call(drawItem, menuOpts, buttonOpts) - .call(setItemPosition, menuOpts, posOpts); - - button.on('click', function() { - // skip `dragend` events - if(d3.event.defaultPrevented) return; - - setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex); - - Plots.executeAPICommand(gd, buttonOpts.method, buttonOpts.args); - - gd.emit('plotly_buttonclicked', {menu: menuOpts, button: buttonOpts, active: menuOpts.active}); - }); - - button.on('mouseover', function() { - button.call(styleOnMouseOver); - }); - - button.on('mouseout', function() { - button.call(styleOnMouseOut, menuOpts); - buttons.call(styleButtons, menuOpts); - }); + } + + if (menuOpts.type === "dropdown" && menuOpts.direction === "up") { + y0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openHeight; + } + + if (menuOpts.type === "dropdown" && menuOpts.direction === "left") { + x0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openWidth; + } + + var posOpts = { + x: menuOpts.lx + x0 + menuOpts.pad.l, + y: menuOpts.ly + y0 + menuOpts.pad.t, + yPad: constants.gapButton, + xPad: constants.gapButton, + index: 0 + }; + + var scrollBoxPosition = { + l: posOpts.x + menuOpts.borderwidth, + t: posOpts.y + menuOpts.borderwidth + }; + + buttons.each(function(buttonOpts, buttonIndex) { + var button = d3.select(this); + + button + .call(drawItem, menuOpts, buttonOpts) + .call(setItemPosition, menuOpts, posOpts); + + button.on("click", function() { + // skip `dragend` events + if (d3.event.defaultPrevented) return; + + setActive( + gd, + menuOpts, + buttonOpts, + gHeader, + gButton, + scrollBox, + buttonIndex + ); + + Plots.executeAPICommand(gd, buttonOpts.method, buttonOpts.args); + + gd.emit("plotly_buttonclicked", { + menu: menuOpts, + button: buttonOpts, + active: menuOpts.active + }); }); - buttons.call(styleButtons, menuOpts); - - if(isVertical) { - scrollBoxPosition.w = Math.max(menuOpts.openWidth, menuOpts.headerWidth); - scrollBoxPosition.h = posOpts.y - scrollBoxPosition.t; - } - else { - scrollBoxPosition.w = posOpts.x - scrollBoxPosition.l; - scrollBoxPosition.h = Math.max(menuOpts.openHeight, menuOpts.headerHeight); - } - - scrollBoxPosition.direction = menuOpts.direction; + button.on("mouseover", function() { + button.call(styleOnMouseOver); + }); - if(scrollBox) { - if(buttons.size()) { - drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, scrollBoxPosition); - } - else { - hideScrollBox(scrollBox); - } + button.on("mouseout", function() { + button.call(styleOnMouseOut, menuOpts); + buttons.call(styleButtons, menuOpts); + }); + }); + + buttons.call(styleButtons, menuOpts); + + if (isVertical) { + scrollBoxPosition.w = Math.max(menuOpts.openWidth, menuOpts.headerWidth); + scrollBoxPosition.h = posOpts.y - scrollBoxPosition.t; + } else { + scrollBoxPosition.w = posOpts.x - scrollBoxPosition.l; + scrollBoxPosition.h = Math.max(menuOpts.openHeight, menuOpts.headerHeight); + } + + scrollBoxPosition.direction = menuOpts.direction; + + if (scrollBox) { + if (buttons.size()) { + drawScrollBox( + gd, + gHeader, + gButton, + scrollBox, + menuOpts, + scrollBoxPosition + ); + } else { + hideScrollBox(scrollBox); } + } } function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, position) { - // enable the scrollbox - var direction = menuOpts.direction, - isVertical = (direction === 'up' || direction === 'down'); - - var active = menuOpts.active, - translateX, translateY, - i; - if(isVertical) { - translateY = 0; - for(i = 0; i < active; i++) { - translateY += menuOpts.heights[i] + constants.gapButton; - } + // enable the scrollbox + var direction = menuOpts.direction, + isVertical = direction === "up" || direction === "down"; + + var active = menuOpts.active, translateX, translateY, i; + if (isVertical) { + translateY = 0; + for (i = 0; i < active; i++) { + translateY += menuOpts.heights[i] + constants.gapButton; } - else { - translateX = 0; - for(i = 0; i < active; i++) { - translateX += menuOpts.widths[i] + constants.gapButton; - } + } else { + translateX = 0; + for (i = 0; i < active; i++) { + translateX += menuOpts.widths[i] + constants.gapButton; } + } - scrollBox.enable(position, translateX, translateY); + scrollBox.enable(position, translateX, translateY); - if(scrollBox.hbar) { - scrollBox.hbar - .attr('opacity', '0') - .transition() - .attr('opacity', '1'); - } + if (scrollBox.hbar) { + scrollBox.hbar.attr("opacity", "0").transition().attr("opacity", "1"); + } - if(scrollBox.vbar) { - scrollBox.vbar - .attr('opacity', '0') - .transition() - .attr('opacity', '1'); - } + if (scrollBox.vbar) { + scrollBox.vbar.attr("opacity", "0").transition().attr("opacity", "1"); + } } function hideScrollBox(scrollBox) { - var hasHBar = !!scrollBox.hbar, - hasVBar = !!scrollBox.vbar; - - if(hasHBar) { - scrollBox.hbar - .transition() - .attr('opacity', '0') - .each('end', function() { - hasHBar = false; - if(!hasVBar) scrollBox.disable(); - }); - } + var hasHBar = !!scrollBox.hbar, hasVBar = !!scrollBox.vbar; - if(hasVBar) { - scrollBox.vbar - .transition() - .attr('opacity', '0') - .each('end', function() { - hasVBar = false; - if(!hasHBar) scrollBox.disable(); - }); - } + if (hasHBar) { + scrollBox.hbar.transition().attr("opacity", "0").each("end", function() { + hasHBar = false; + if (!hasVBar) scrollBox.disable(); + }); + } + + if (hasVBar) { + scrollBox.vbar.transition().attr("opacity", "0").each("end", function() { + hasVBar = false; + if (!hasHBar) scrollBox.disable(); + }); + } } function drawItem(item, menuOpts, itemOpts) { - item.call(drawItemRect, menuOpts) - .call(drawItemText, menuOpts, itemOpts); + item.call(drawItemRect, menuOpts).call(drawItemText, menuOpts, itemOpts); } function drawItemRect(item, menuOpts) { - var rect = item.selectAll('rect') - .data([0]); - - rect.enter().append('rect') - .classed(constants.itemRectClassName, true) - .attr({ - rx: constants.rx, - ry: constants.ry, - 'shape-rendering': 'crispEdges' - }); - - rect.call(Color.stroke, menuOpts.bordercolor) - .call(Color.fill, menuOpts.bgcolor) - .style('stroke-width', menuOpts.borderwidth + 'px'); + var rect = item.selectAll("rect").data([0]); + + rect.enter().append("rect").classed(constants.itemRectClassName, true).attr({ + rx: constants.rx, + ry: constants.ry, + "shape-rendering": "crispEdges" + }); + + rect + .call(Color.stroke, menuOpts.bordercolor) + .call(Color.fill, menuOpts.bgcolor) + .style("stroke-width", menuOpts.borderwidth + "px"); } function drawItemText(item, menuOpts, itemOpts) { - var text = item.selectAll('text') - .data([0]); - - text.enter().append('text') - .classed(constants.itemTextClassName, true) - .classed('user-select-none', true) - .attr('text-anchor', 'start'); - - text.call(Drawing.font, menuOpts.font) - .text(itemOpts.label) - .call(svgTextUtils.convertToTspans); + var text = item.selectAll("text").data([0]); + + text + .enter() + .append("text") + .classed(constants.itemTextClassName, true) + .classed("user-select-none", true) + .attr("text-anchor", "start"); + + text + .call(Drawing.font, menuOpts.font) + .text(itemOpts.label) + .call(svgTextUtils.convertToTspans); } function styleButtons(buttons, menuOpts) { - var active = menuOpts.active; + var active = menuOpts.active; - buttons.each(function(buttonOpts, i) { - var button = d3.select(this); + buttons.each(function(buttonOpts, i) { + var button = d3.select(this); - if(i === active && menuOpts.showactive) { - button.select('rect.' + constants.itemRectClassName) - .call(Color.fill, constants.activeColor); - } - }); + if (i === active && menuOpts.showactive) { + button + .select("rect." + constants.itemRectClassName) + .call(Color.fill, constants.activeColor); + } + }); } function styleOnMouseOver(item) { - item.select('rect.' + constants.itemRectClassName) - .call(Color.fill, constants.hoverColor); + item + .select("rect." + constants.itemRectClassName) + .call(Color.fill, constants.hoverColor); } function styleOnMouseOut(item, menuOpts) { - item.select('rect.' + constants.itemRectClassName) - .call(Color.fill, menuOpts.bgcolor); + item + .select("rect." + constants.itemRectClassName) + .call(Color.fill, menuOpts.bgcolor); } // find item dimensions (this mutates menuOpts) function findDimensions(gd, menuOpts) { - menuOpts.width1 = 0; - menuOpts.height1 = 0; - menuOpts.heights = []; - menuOpts.widths = []; - menuOpts.totalWidth = 0; - menuOpts.totalHeight = 0; - menuOpts.openWidth = 0; - menuOpts.openHeight = 0; - menuOpts.lx = 0; - menuOpts.ly = 0; - - var fakeButtons = gd._tester.selectAll('g.' + constants.dropdownButtonClassName) - .data(menuOpts.buttons); - - fakeButtons.enter().append('g') - .classed(constants.dropdownButtonClassName, true); - - var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1; - - // loop over fake buttons to find width / height - fakeButtons.each(function(buttonOpts, i) { - var button = d3.select(this); - - button.call(drawItem, menuOpts, buttonOpts); - - var text = button.select('.' + constants.itemTextClassName), - tspans = text.selectAll('tspan'); - - // width is given by max width of all buttons - var tWidth = text.node() && Drawing.bBox(text.node()).width, - wEff = Math.max(tWidth + constants.textPadX, constants.minWidth); - - // height is determined by item text - var tHeight = menuOpts.font.size * constants.fontSizeToHeight, - tLines = tspans[0].length || 1, - hEff = Math.max(tHeight * tLines, constants.minHeight) + constants.textOffsetY; - - hEff = Math.ceil(hEff); - wEff = Math.ceil(wEff); - - // Store per-item sizes since a row of horizontal buttons, for example, - // don't all need to be the same width: - menuOpts.widths[i] = wEff; - menuOpts.heights[i] = hEff; - - // Height and width of individual element: - menuOpts.height1 = Math.max(menuOpts.height1, hEff); - menuOpts.width1 = Math.max(menuOpts.width1, wEff); - - if(isVertical) { - menuOpts.totalWidth = Math.max(menuOpts.totalWidth, wEff); - menuOpts.openWidth = menuOpts.totalWidth; - menuOpts.totalHeight += hEff + constants.gapButton; - menuOpts.openHeight += hEff + constants.gapButton; - } else { - menuOpts.totalWidth += wEff + constants.gapButton; - menuOpts.openWidth += wEff + constants.gapButton; - menuOpts.totalHeight = Math.max(menuOpts.totalHeight, hEff); - menuOpts.openHeight = menuOpts.totalHeight; - } - }); - - if(isVertical) { - menuOpts.totalHeight -= constants.gapButton; + menuOpts.width1 = 0; + menuOpts.height1 = 0; + menuOpts.heights = []; + menuOpts.widths = []; + menuOpts.totalWidth = 0; + menuOpts.totalHeight = 0; + menuOpts.openWidth = 0; + menuOpts.openHeight = 0; + menuOpts.lx = 0; + menuOpts.ly = 0; + + var fakeButtons = gd._tester + .selectAll("g." + constants.dropdownButtonClassName) + .data(menuOpts.buttons); + + fakeButtons + .enter() + .append("g") + .classed(constants.dropdownButtonClassName, true); + + var isVertical = ["up", "down"].indexOf(menuOpts.direction) !== -1; + + // loop over fake buttons to find width / height + fakeButtons.each(function(buttonOpts, i) { + var button = d3.select(this); + + button.call(drawItem, menuOpts, buttonOpts); + + var text = button.select("." + constants.itemTextClassName), + tspans = text.selectAll("tspan"); + + // width is given by max width of all buttons + var tWidth = text.node() && Drawing.bBox(text.node()).width, + wEff = Math.max(tWidth + constants.textPadX, constants.minWidth); + + // height is determined by item text + var tHeight = menuOpts.font.size * constants.fontSizeToHeight, + tLines = tspans[0].length || 1, + hEff = Math.max(tHeight * tLines, constants.minHeight) + + constants.textOffsetY; + + hEff = Math.ceil(hEff); + wEff = Math.ceil(wEff); + + // Store per-item sizes since a row of horizontal buttons, for example, + // don't all need to be the same width: + menuOpts.widths[i] = wEff; + menuOpts.heights[i] = hEff; + + // Height and width of individual element: + menuOpts.height1 = Math.max(menuOpts.height1, hEff); + menuOpts.width1 = Math.max(menuOpts.width1, wEff); + + if (isVertical) { + menuOpts.totalWidth = Math.max(menuOpts.totalWidth, wEff); + menuOpts.openWidth = menuOpts.totalWidth; + menuOpts.totalHeight += hEff + constants.gapButton; + menuOpts.openHeight += hEff + constants.gapButton; } else { - menuOpts.totalWidth -= constants.gapButton; - } - - - menuOpts.headerWidth = menuOpts.width1 + constants.arrowPadX; - menuOpts.headerHeight = menuOpts.height1; - - if(menuOpts.type === 'dropdown') { - if(isVertical) { - menuOpts.width1 += constants.arrowPadX; - menuOpts.totalHeight = menuOpts.height1; - } else { - menuOpts.totalWidth = menuOpts.width1; - } - menuOpts.totalWidth += constants.arrowPadX; + menuOpts.totalWidth += wEff + constants.gapButton; + menuOpts.openWidth += wEff + constants.gapButton; + menuOpts.totalHeight = Math.max(menuOpts.totalHeight, hEff); + menuOpts.openHeight = menuOpts.totalHeight; } + }); - fakeButtons.remove(); + if (isVertical) { + menuOpts.totalHeight -= constants.gapButton; + } else { + menuOpts.totalWidth -= constants.gapButton; + } - var paddedWidth = menuOpts.totalWidth + menuOpts.pad.l + menuOpts.pad.r; - var paddedHeight = menuOpts.totalHeight + menuOpts.pad.t + menuOpts.pad.b; + menuOpts.headerWidth = menuOpts.width1 + constants.arrowPadX; + menuOpts.headerHeight = menuOpts.height1; - var graphSize = gd._fullLayout._size; - menuOpts.lx = graphSize.l + graphSize.w * menuOpts.x; - menuOpts.ly = graphSize.t + graphSize.h * (1 - menuOpts.y); - - var xanchor = 'left'; - if(anchorUtils.isRightAnchor(menuOpts)) { - menuOpts.lx -= paddedWidth; - xanchor = 'right'; - } - if(anchorUtils.isCenterAnchor(menuOpts)) { - menuOpts.lx -= paddedWidth / 2; - xanchor = 'center'; - } - - var yanchor = 'top'; - if(anchorUtils.isBottomAnchor(menuOpts)) { - menuOpts.ly -= paddedHeight; - yanchor = 'bottom'; - } - if(anchorUtils.isMiddleAnchor(menuOpts)) { - menuOpts.ly -= paddedHeight / 2; - yanchor = 'middle'; + if (menuOpts.type === "dropdown") { + if (isVertical) { + menuOpts.width1 += constants.arrowPadX; + menuOpts.totalHeight = menuOpts.height1; + } else { + menuOpts.totalWidth = menuOpts.width1; } - - menuOpts.totalWidth = Math.ceil(menuOpts.totalWidth); - menuOpts.totalHeight = Math.ceil(menuOpts.totalHeight); - menuOpts.lx = Math.round(menuOpts.lx); - menuOpts.ly = Math.round(menuOpts.ly); - - Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index, { - x: menuOpts.x, - y: menuOpts.y, - l: paddedWidth * ({right: 1, center: 0.5}[xanchor] || 0), - r: paddedWidth * ({left: 1, center: 0.5}[xanchor] || 0), - b: paddedHeight * ({top: 1, middle: 0.5}[yanchor] || 0), - t: paddedHeight * ({bottom: 1, middle: 0.5}[yanchor] || 0) - }); + menuOpts.totalWidth += constants.arrowPadX; + } + + fakeButtons.remove(); + + var paddedWidth = menuOpts.totalWidth + menuOpts.pad.l + menuOpts.pad.r; + var paddedHeight = menuOpts.totalHeight + menuOpts.pad.t + menuOpts.pad.b; + + var graphSize = gd._fullLayout._size; + menuOpts.lx = graphSize.l + graphSize.w * menuOpts.x; + menuOpts.ly = graphSize.t + graphSize.h * (1 - menuOpts.y); + + var xanchor = "left"; + if (anchorUtils.isRightAnchor(menuOpts)) { + menuOpts.lx -= paddedWidth; + xanchor = "right"; + } + if (anchorUtils.isCenterAnchor(menuOpts)) { + menuOpts.lx -= paddedWidth / 2; + xanchor = "center"; + } + + var yanchor = "top"; + if (anchorUtils.isBottomAnchor(menuOpts)) { + menuOpts.ly -= paddedHeight; + yanchor = "bottom"; + } + if (anchorUtils.isMiddleAnchor(menuOpts)) { + menuOpts.ly -= paddedHeight / 2; + yanchor = "middle"; + } + + menuOpts.totalWidth = Math.ceil(menuOpts.totalWidth); + menuOpts.totalHeight = Math.ceil(menuOpts.totalHeight); + menuOpts.lx = Math.round(menuOpts.lx); + menuOpts.ly = Math.round(menuOpts.ly); + + Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index, { + x: menuOpts.x, + y: menuOpts.y, + l: paddedWidth * (({ right: 1, center: 0.5 })[xanchor] || 0), + r: paddedWidth * (({ left: 1, center: 0.5 })[xanchor] || 0), + b: paddedHeight * (({ top: 1, middle: 0.5 })[yanchor] || 0), + t: paddedHeight * (({ bottom: 1, middle: 0.5 })[yanchor] || 0) + }); } // set item positions (mutates posOpts) function setItemPosition(item, menuOpts, posOpts, overrideOpts) { - overrideOpts = overrideOpts || {}; - var rect = item.select('.' + constants.itemRectClassName), - text = item.select('.' + constants.itemTextClassName), - tspans = text.selectAll('tspan'), - borderWidth = menuOpts.borderwidth, - index = posOpts.index; - - Drawing.setTranslate(item, borderWidth + posOpts.x, borderWidth + posOpts.y); - - var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1; - - rect.attr({ - x: 0, - y: 0, - width: overrideOpts.width || (isVertical ? menuOpts.width1 : menuOpts.widths[index]), - height: overrideOpts.height || (isVertical ? menuOpts.heights[index] : menuOpts.height1) - }); - - var tHeight = menuOpts.font.size * constants.fontSizeToHeight, - tLines = tspans[0].length || 1, - spanOffset = ((tLines - 1) * tHeight / 4); - - var textAttrs = { - x: constants.textOffsetX, - y: menuOpts.heights[index] / 2 - spanOffset + constants.textOffsetY - }; - - text.attr(textAttrs); - tspans.attr(textAttrs); - - if(isVertical) { - posOpts.y += menuOpts.heights[index] + posOpts.yPad; - } else { - posOpts.x += menuOpts.widths[index] + posOpts.xPad; - } - - posOpts.index++; + overrideOpts = overrideOpts || {}; + var rect = item.select("." + constants.itemRectClassName), + text = item.select("." + constants.itemTextClassName), + tspans = text.selectAll("tspan"), + borderWidth = menuOpts.borderwidth, + index = posOpts.index; + + Drawing.setTranslate(item, borderWidth + posOpts.x, borderWidth + posOpts.y); + + var isVertical = ["up", "down"].indexOf(menuOpts.direction) !== -1; + + rect.attr({ + x: 0, + y: 0, + width: ( + overrideOpts.width || + (isVertical ? menuOpts.width1 : menuOpts.widths[index]) + ), + height: ( + overrideOpts.height || + (isVertical ? menuOpts.heights[index] : menuOpts.height1) + ) + }); + + var tHeight = menuOpts.font.size * constants.fontSizeToHeight, + tLines = tspans[0].length || 1, + spanOffset = (tLines - 1) * tHeight / 4; + + var textAttrs = { + x: constants.textOffsetX, + y: menuOpts.heights[index] / 2 - spanOffset + constants.textOffsetY + }; + + text.attr(textAttrs); + tspans.attr(textAttrs); + + if (isVertical) { + posOpts.y += menuOpts.heights[index] + posOpts.yPad; + } else { + posOpts.x += menuOpts.widths[index] + posOpts.xPad; + } + + posOpts.index++; } function removeAllButtons(gButton) { - gButton.selectAll('g.' + constants.dropdownButtonClassName).remove(); + gButton.selectAll("g." + constants.dropdownButtonClassName).remove(); } function clearPushMargins(gd) { - var pushMargins = gd._fullLayout._pushmargin || {}, - keys = Object.keys(pushMargins); + var pushMargins = gd._fullLayout._pushmargin || {}, + keys = Object.keys(pushMargins); - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; - if(k.indexOf(constants.autoMarginIdRoot) !== -1) { - Plots.autoMargin(gd, k); - } + if (k.indexOf(constants.autoMarginIdRoot) !== -1) { + Plots.autoMargin(gd, k); } + } } diff --git a/src/components/updatemenus/index.js b/src/components/updatemenus/index.js index 366b9aaa1ad..b44b6e9dab9 100644 --- a/src/components/updatemenus/index.js +++ b/src/components/updatemenus/index.js @@ -5,17 +5,13 @@ * 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 constants = require('./constants'); +"use strict"; +var constants = require("./constants"); module.exports = { - moduleType: 'component', - name: constants.name, - - layoutAttributes: require('./attributes'), - supplyLayoutDefaults: require('./defaults'), - - draw: require('./draw') + moduleType: "component", + name: constants.name, + layoutAttributes: require("./attributes"), + supplyLayoutDefaults: require("./defaults"), + draw: require("./draw") }; diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js index 6c3431536cf..a1ff5141a27 100644 --- a/src/components/updatemenus/scrollbox.js +++ b/src/components/updatemenus/scrollbox.js @@ -5,17 +5,15 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = ScrollBox; -var d3 = require('d3'); +var d3 = require("d3"); -var Color = require('../color'); -var Drawing = require('../drawing'); +var Color = require("../color"); +var Drawing = require("../drawing"); -var Lib = require('../../lib'); +var Lib = require("../../lib"); /** * Helper class to setup a scroll box @@ -26,35 +24,33 @@ var Lib = require('../../lib'); * @param {string} id Id for the clip path to implement the scroll box */ function ScrollBox(gd, container, id) { - this.gd = gd; - this.container = container; - this.id = id; - - // See ScrollBox.prototype.enable for further definition - this.position = null; // scrollbox position - this.translateX = null; // scrollbox horizontal translation - this.translateY = null; // scrollbox vertical translation - this.hbar = null; // horizontal scrollbar D3 selection - this.vbar = null; // vertical scrollbar D3 selection - - // element to capture pointer events - this.bg = this.container.selectAll('rect.scrollbox-bg').data([0]); - - this.bg.exit() - .on('.drag', null) - .on('wheel', null) - .remove(); - - this.bg.enter().append('rect') - .classed('scrollbox-bg', true) - .style('pointer-events', 'all') - .attr({ - opacity: 0, - x: 0, - y: 0, - width: 0, - height: 0 - }); + this.gd = gd; + this.container = container; + this.id = id; + + // See ScrollBox.prototype.enable for further definition + this.position = null; + // scrollbox position + this.translateX = null; + // scrollbox horizontal translation + this.translateY = null; + // scrollbox vertical translation + this.hbar = null; + // horizontal scrollbar D3 selection + this.vbar = null; + + // vertical scrollbar D3 selection + // element to capture pointer events + this.bg = this.container.selectAll("rect.scrollbox-bg").data([0]); + + this.bg.exit().on(".drag", null).on("wheel", null).remove(); + + this.bg + .enter() + .append("rect") + .classed("scrollbox-bg", true) + .style("pointer-events", "all") + .attr({ opacity: 0, x: 0, y: 0, width: 0, height: 0 }); } // scroll bar dimensions @@ -62,7 +58,7 @@ ScrollBox.barWidth = 2; ScrollBox.barLength = 20; ScrollBox.barRadius = 2; ScrollBox.barPad = 1; -ScrollBox.barColor = '#808BA4'; +ScrollBox.barColor = "#808BA4"; /** * If needed, setup a clip path and scrollbars @@ -79,238 +75,220 @@ ScrollBox.barColor = '#808BA4'; * @param {number} [translateY=0] Vertical offset (in pixels) */ ScrollBox.prototype.enable = function enable(position, translateX, translateY) { - var fullLayout = this.gd._fullLayout, - fullWidth = fullLayout.width, - fullHeight = fullLayout.height; - - // compute position of scrollbox - this.position = position; - - var l = this.position.l, - w = this.position.w, - t = this.position.t, - h = this.position.h, - direction = this.position.direction, - isDown = (direction === 'down'), - isLeft = (direction === 'left'), - isRight = (direction === 'right'), - isUp = (direction === 'up'), - boxW = w, - boxH = h, - boxL, boxR, - boxT, boxB; - - if(!isDown && !isLeft && !isRight && !isUp) { - this.position.direction = 'down'; - isDown = true; - } - - var isVertical = isDown || isUp; - if(isVertical) { - boxL = l; - boxR = boxL + boxW; - - if(isDown) { - // anchor to top side - boxT = t; - boxB = Math.min(boxT + boxH, fullHeight); - boxH = boxB - boxT; - } - else { - // anchor to bottom side - boxB = t + boxH; - boxT = Math.max(boxB - boxH, 0); - boxH = boxB - boxT; - } + var fullLayout = this.gd._fullLayout, + fullWidth = fullLayout.width, + fullHeight = fullLayout.height; + + // compute position of scrollbox + this.position = position; + + var l = this.position.l, + w = this.position.w, + t = this.position.t, + h = this.position.h, + direction = this.position.direction, + isDown = direction === "down", + isLeft = direction === "left", + isRight = direction === "right", + isUp = direction === "up", + boxW = w, + boxH = h, + boxL, + boxR, + boxT, + boxB; + + if (!isDown && !isLeft && !isRight && !isUp) { + this.position.direction = "down"; + isDown = true; + } + + var isVertical = isDown || isUp; + if (isVertical) { + boxL = l; + boxR = boxL + boxW; + + if (isDown) { + // anchor to top side + boxT = t; + boxB = Math.min(boxT + boxH, fullHeight); + boxH = boxB - boxT; + } else { + // anchor to bottom side + boxB = t + boxH; + boxT = Math.max(boxB - boxH, 0); + boxH = boxB - boxT; } - else { - boxT = t; - boxB = boxT + boxH; - - if(isLeft) { - // anchor to right side - boxR = l + boxW; - boxL = Math.max(boxR - boxW, 0); - boxW = boxR - boxL; - } - else { - // anchor to left side - boxL = l; - boxR = Math.min(boxL + boxW, fullWidth); - boxW = boxR - boxL; - } + } else { + boxT = t; + boxB = boxT + boxH; + + if (isLeft) { + // anchor to right side + boxR = l + boxW; + boxL = Math.max(boxR - boxW, 0); + boxW = boxR - boxL; + } else { + // anchor to left side + boxL = l; + boxR = Math.min(boxL + boxW, fullWidth); + boxW = boxR - boxL; } - - this._box = { - l: boxL, - t: boxT, - w: boxW, - h: boxH - }; - - // compute position of horizontal scroll bar - var needsHorizontalScrollBar = (w > boxW), - hbarW = ScrollBox.barLength + 2 * ScrollBox.barPad, - hbarH = ScrollBox.barWidth + 2 * ScrollBox.barPad, - // draw horizontal scrollbar on the bottom side - hbarL = l, - hbarT = t + h; - - if(hbarT + hbarH > fullHeight) hbarT = fullHeight - hbarH; - - var hbar = this.container.selectAll('rect.scrollbar-horizontal').data( - (needsHorizontalScrollBar) ? [0] : []); - - hbar.exit() - .on('.drag', null) - .remove(); - - hbar.enter().append('rect') - .classed('scrollbar-horizontal', true) - .call(Color.fill, ScrollBox.barColor); - - if(needsHorizontalScrollBar) { - this.hbar = hbar.attr({ - 'rx': ScrollBox.barRadius, - 'ry': ScrollBox.barRadius, - 'x': hbarL, - 'y': hbarT, - 'width': hbarW, - 'height': hbarH - }); - - // hbar center moves between hbarXMin and hbarXMin + hbarTranslateMax - this._hbarXMin = hbarL + hbarW / 2; - this._hbarTranslateMax = boxW - hbarW; - } - else { - delete this.hbar; - delete this._hbarXMin; - delete this._hbarTranslateMax; + } + + this._box = { l: boxL, t: boxT, w: boxW, h: boxH }; + + // compute position of horizontal scroll bar + var needsHorizontalScrollBar = w > boxW, + hbarW = ScrollBox.barLength + 2 * ScrollBox.barPad, + hbarH = ScrollBox.barWidth + 2 * ScrollBox.barPad, + // draw horizontal scrollbar on the bottom side + hbarL = l, + hbarT = t + h; + + if (hbarT + hbarH > fullHeight) hbarT = fullHeight - hbarH; + + var hbar = this.container + .selectAll("rect.scrollbar-horizontal") + .data(needsHorizontalScrollBar ? [0] : []); + + hbar.exit().on(".drag", null).remove(); + + hbar + .enter() + .append("rect") + .classed("scrollbar-horizontal", true) + .call(Color.fill, ScrollBox.barColor); + + if (needsHorizontalScrollBar) { + this.hbar = hbar.attr({ + rx: ScrollBox.barRadius, + ry: ScrollBox.barRadius, + x: hbarL, + y: hbarT, + width: hbarW, + height: hbarH + }); + + // hbar center moves between hbarXMin and hbarXMin + hbarTranslateMax + this._hbarXMin = hbarL + hbarW / 2; + this._hbarTranslateMax = boxW - hbarW; + } else { + delete this.hbar; + delete this._hbarXMin; + delete this._hbarTranslateMax; + } + + // compute position of vertical scroll bar + var needsVerticalScrollBar = h > boxH, + vbarW = ScrollBox.barWidth + 2 * ScrollBox.barPad, + vbarH = ScrollBox.barLength + 2 * ScrollBox.barPad, + // draw vertical scrollbar on the right side + vbarL = l + w, + vbarT = t; + + if (vbarL + vbarW > fullWidth) vbarL = fullWidth - vbarW; + + var vbar = this.container + .selectAll("rect.scrollbar-vertical") + .data(needsVerticalScrollBar ? [0] : []); + + vbar.exit().on(".drag", null).remove(); + + vbar + .enter() + .append("rect") + .classed("scrollbar-vertical", true) + .call(Color.fill, ScrollBox.barColor); + + if (needsVerticalScrollBar) { + this.vbar = vbar.attr({ + rx: ScrollBox.barRadius, + ry: ScrollBox.barRadius, + x: vbarL, + y: vbarT, + width: vbarW, + height: vbarH + }); + + // vbar center moves between vbarYMin and vbarYMin + vbarTranslateMax + this._vbarYMin = vbarT + vbarH / 2; + this._vbarTranslateMax = boxH - vbarH; + } else { + delete this.vbar; + delete this._vbarYMin; + delete this._vbarTranslateMax; + } + + // setup a clip path (if scroll bars are needed) + var clipId = this.id, + clipL = boxL - 0.5, + clipR = needsVerticalScrollBar ? boxR + vbarW + 0.5 : boxR + 0.5, + clipT = boxT - 0.5, + clipB = needsHorizontalScrollBar ? boxB + hbarH + 0.5 : boxB + 0.5; + + var clipPath = fullLayout._topdefs + .selectAll("#" + clipId) + .data(needsHorizontalScrollBar || needsVerticalScrollBar ? [0] : []); + + clipPath.exit().remove(); + + clipPath.enter().append("clipPath").attr("id", clipId).append("rect"); + + if (needsHorizontalScrollBar || needsVerticalScrollBar) { + this._clipRect = clipPath.select("rect").attr({ + x: Math.floor(clipL), + y: Math.floor(clipT), + width: Math.ceil(clipR) - Math.floor(clipL), + height: Math.ceil(clipB) - Math.floor(clipT) + }); + + this.container.call(Drawing.setClipUrl, clipId); + + this.bg.attr({ x: l, y: t, width: w, height: h }); + } else { + this.bg.attr({ width: 0, height: 0 }); + this.container + .on("wheel", null) + .on(".drag", null) + .call(Drawing.setClipUrl, null); + delete this._clipRect; + } + + // set up drag listeners (if scroll bars are needed) + if (needsHorizontalScrollBar || needsVerticalScrollBar) { + var onBoxDrag = d3.behavior + .drag() + .on("dragstart", function() { + d3.event.sourceEvent.preventDefault(); + }) + .on("drag", this._onBoxDrag.bind(this)); + + this.container + .on("wheel", null) + .on("wheel", this._onBoxWheel.bind(this)) + .on(".drag", null) + .call(onBoxDrag); + + var onBarDrag = d3.behavior + .drag() + .on("dragstart", function() { + d3.event.sourceEvent.preventDefault(); + d3.event.sourceEvent.stopPropagation(); + }) + .on("drag", this._onBarDrag.bind(this)); + + if (needsHorizontalScrollBar) { + this.hbar.on(".drag", null).call(onBarDrag); } - // compute position of vertical scroll bar - var needsVerticalScrollBar = (h > boxH), - vbarW = ScrollBox.barWidth + 2 * ScrollBox.barPad, - vbarH = ScrollBox.barLength + 2 * ScrollBox.barPad, - // draw vertical scrollbar on the right side - vbarL = l + w, - vbarT = t; - - if(vbarL + vbarW > fullWidth) vbarL = fullWidth - vbarW; - - var vbar = this.container.selectAll('rect.scrollbar-vertical').data( - (needsVerticalScrollBar) ? [0] : []); - - vbar.exit() - .on('.drag', null) - .remove(); - - vbar.enter().append('rect') - .classed('scrollbar-vertical', true) - .call(Color.fill, ScrollBox.barColor); - - if(needsVerticalScrollBar) { - this.vbar = vbar.attr({ - 'rx': ScrollBox.barRadius, - 'ry': ScrollBox.barRadius, - 'x': vbarL, - 'y': vbarT, - 'width': vbarW, - 'height': vbarH - }); - - // vbar center moves between vbarYMin and vbarYMin + vbarTranslateMax - this._vbarYMin = vbarT + vbarH / 2; - this._vbarTranslateMax = boxH - vbarH; - } - else { - delete this.vbar; - delete this._vbarYMin; - delete this._vbarTranslateMax; + if (needsVerticalScrollBar) { + this.vbar.on(".drag", null).call(onBarDrag); } + } - // setup a clip path (if scroll bars are needed) - var clipId = this.id, - clipL = boxL - 0.5, - clipR = (needsVerticalScrollBar) ? boxR + vbarW + 0.5 : boxR + 0.5, - clipT = boxT - 0.5, - clipB = (needsHorizontalScrollBar) ? boxB + hbarH + 0.5 : boxB + 0.5; - - var clipPath = fullLayout._topdefs.selectAll('#' + clipId) - .data((needsHorizontalScrollBar || needsVerticalScrollBar) ? [0] : []); - - clipPath.exit().remove(); - - clipPath.enter() - .append('clipPath').attr('id', clipId) - .append('rect'); - - if(needsHorizontalScrollBar || needsVerticalScrollBar) { - this._clipRect = clipPath.select('rect').attr({ - x: Math.floor(clipL), - y: Math.floor(clipT), - width: Math.ceil(clipR) - Math.floor(clipL), - height: Math.ceil(clipB) - Math.floor(clipT) - }); - - this.container.call(Drawing.setClipUrl, clipId); - - this.bg.attr({ - x: l, - y: t, - width: w, - height: h - }); - } - else { - this.bg.attr({ - width: 0, - height: 0 - }); - this.container - .on('wheel', null) - .on('.drag', null) - .call(Drawing.setClipUrl, null); - delete this._clipRect; - } - - // set up drag listeners (if scroll bars are needed) - if(needsHorizontalScrollBar || needsVerticalScrollBar) { - var onBoxDrag = d3.behavior.drag() - .on('dragstart', function() { - d3.event.sourceEvent.preventDefault(); - }) - .on('drag', this._onBoxDrag.bind(this)); - - this.container - .on('wheel', null) - .on('wheel', this._onBoxWheel.bind(this)) - .on('.drag', null) - .call(onBoxDrag); - - var onBarDrag = d3.behavior.drag() - .on('dragstart', function() { - d3.event.sourceEvent.preventDefault(); - d3.event.sourceEvent.stopPropagation(); - }) - .on('drag', this._onBarDrag.bind(this)); - - if(needsHorizontalScrollBar) { - this.hbar - .on('.drag', null) - .call(onBarDrag); - } - - if(needsVerticalScrollBar) { - this.vbar - .on('.drag', null) - .call(onBarDrag); - } - } - - // set scrollbox translation - this.setTranslate(translateX, translateY); + // set scrollbox translation + this.setTranslate(translateX, translateY); }; /** @@ -319,33 +297,30 @@ ScrollBox.prototype.enable = function enable(position, translateX, translateY) { * @method */ ScrollBox.prototype.disable = function disable() { - if(this.hbar || this.vbar) { - this.bg.attr({ - width: 0, - height: 0 - }); - this.container - .on('wheel', null) - .on('.drag', null) - .call(Drawing.setClipUrl, null); - delete this._clipRect; - } - - if(this.hbar) { - this.hbar.on('.drag', null); - this.hbar.remove(); - delete this.hbar; - delete this._hbarXMin; - delete this._hbarTranslateMax; - } - - if(this.vbar) { - this.vbar.on('.drag', null); - this.vbar.remove(); - delete this.vbar; - delete this._vbarYMin; - delete this._vbarTranslateMax; - } + if (this.hbar || this.vbar) { + this.bg.attr({ width: 0, height: 0 }); + this.container + .on("wheel", null) + .on(".drag", null) + .call(Drawing.setClipUrl, null); + delete this._clipRect; + } + + if (this.hbar) { + this.hbar.on(".drag", null); + this.hbar.remove(); + delete this.hbar; + delete this._hbarXMin; + delete this._hbarTranslateMax; + } + + if (this.vbar) { + this.vbar.on(".drag", null); + this.vbar.remove(); + delete this.vbar; + delete this._vbarYMin; + delete this._vbarTranslateMax; + } }; /** @@ -354,18 +329,17 @@ ScrollBox.prototype.disable = function disable() { * @method */ ScrollBox.prototype._onBoxDrag = function onBarDrag() { - var translateX = this.translateX, - translateY = this.translateY; + var translateX = this.translateX, translateY = this.translateY; - if(this.hbar) { - translateX -= d3.event.dx; - } + if (this.hbar) { + translateX -= d3.event.dx; + } - if(this.vbar) { - translateY -= d3.event.dy; - } + if (this.vbar) { + translateY -= d3.event.dy; + } - this.setTranslate(translateX, translateY); + this.setTranslate(translateX, translateY); }; /** @@ -374,18 +348,17 @@ ScrollBox.prototype._onBoxDrag = function onBarDrag() { * @method */ ScrollBox.prototype._onBoxWheel = function onBarWheel() { - var translateX = this.translateX, - translateY = this.translateY; + var translateX = this.translateX, translateY = this.translateY; - if(this.hbar) { - translateX += d3.event.deltaY; - } + if (this.hbar) { + translateX += d3.event.deltaY; + } - if(this.vbar) { - translateY += d3.event.deltaY; - } + if (this.vbar) { + translateY += d3.event.deltaY; + } - this.setTranslate(translateX, translateY); + this.setTranslate(translateX, translateY); }; /** @@ -394,32 +367,31 @@ ScrollBox.prototype._onBoxWheel = function onBarWheel() { * @method */ ScrollBox.prototype._onBarDrag = function onBarDrag() { - var translateX = this.translateX, - translateY = this.translateY; + var translateX = this.translateX, translateY = this.translateY; - if(this.hbar) { - var xMin = translateX + this._hbarXMin, - xMax = xMin + this._hbarTranslateMax, - x = Lib.constrain(d3.event.x, xMin, xMax), - xf = (x - xMin) / (xMax - xMin); + if (this.hbar) { + var xMin = translateX + this._hbarXMin, + xMax = xMin + this._hbarTranslateMax, + x = Lib.constrain(d3.event.x, xMin, xMax), + xf = (x - xMin) / (xMax - xMin); - var translateXMax = this.position.w - this._box.w; + var translateXMax = this.position.w - this._box.w; - translateX = xf * translateXMax; - } + translateX = xf * translateXMax; + } - if(this.vbar) { - var yMin = translateY + this._vbarYMin, - yMax = yMin + this._vbarTranslateMax, - y = Lib.constrain(d3.event.y, yMin, yMax), - yf = (y - yMin) / (yMax - yMin); + if (this.vbar) { + var yMin = translateY + this._vbarYMin, + yMax = yMin + this._vbarTranslateMax, + y = Lib.constrain(d3.event.y, yMin, yMax), + yf = (y - yMin) / (yMax - yMin); - var translateYMax = this.position.h - this._box.h; + var translateYMax = this.position.h - this._box.h; - translateY = yf * translateYMax; - } + translateY = yf * translateYMax; + } - this.setTranslate(translateX, translateY); + this.setTranslate(translateX, translateY); }; /** @@ -429,41 +401,50 @@ ScrollBox.prototype._onBarDrag = function onBarDrag() { * @param {number} [translateX=0] Horizontal offset (in pixels) * @param {number} [translateY=0] Vertical offset (in pixels) */ -ScrollBox.prototype.setTranslate = function setTranslate(translateX, translateY) { - // store translateX and translateY (needed by mouse event handlers) - var translateXMax = this.position.w - this._box.w, - translateYMax = this.position.h - this._box.h; - - translateX = Lib.constrain(translateX || 0, 0, translateXMax); - translateY = Lib.constrain(translateY || 0, 0, translateYMax); - - this.translateX = translateX; - this.translateY = translateY; - - this.container.call(Drawing.setTranslate, - this._box.l - this.position.l - translateX, - this._box.t - this.position.t - translateY); - - if(this._clipRect) { - this._clipRect.attr({ - x: Math.floor(this.position.l + translateX - 0.5), - y: Math.floor(this.position.t + translateY - 0.5) - }); - } - - if(this.hbar) { - var xf = translateX / translateXMax; - - this.hbar.call(Drawing.setTranslate, - translateX + xf * this._hbarTranslateMax, - translateY); - } - - if(this.vbar) { - var yf = translateY / translateYMax; - - this.vbar.call(Drawing.setTranslate, - translateX, - translateY + yf * this._vbarTranslateMax); - } +ScrollBox.prototype.setTranslate = function setTranslate( + translateX, + translateY +) { + // store translateX and translateY (needed by mouse event handlers) + var translateXMax = this.position.w - this._box.w, + translateYMax = this.position.h - this._box.h; + + translateX = Lib.constrain(translateX || 0, 0, translateXMax); + translateY = Lib.constrain(translateY || 0, 0, translateYMax); + + this.translateX = translateX; + this.translateY = translateY; + + this.container.call( + Drawing.setTranslate, + this._box.l - this.position.l - translateX, + this._box.t - this.position.t - translateY + ); + + if (this._clipRect) { + this._clipRect.attr({ + x: Math.floor(this.position.l + translateX - 0.5), + y: Math.floor(this.position.t + translateY - 0.5) + }); + } + + if (this.hbar) { + var xf = translateX / translateXMax; + + this.hbar.call( + Drawing.setTranslate, + translateX + xf * this._hbarTranslateMax, + translateY + ); + } + + if (this.vbar) { + var yf = translateY / translateYMax; + + this.vbar.call( + Drawing.setTranslate, + translateX, + translateY + yf * this._vbarTranslateMax + ); + } }; diff --git a/src/constants/gl2d_dashes.js b/src/constants/gl2d_dashes.js index 8675739aa56..67d116470c9 100644 --- a/src/constants/gl2d_dashes.js +++ b/src/constants/gl2d_dashes.js @@ -5,15 +5,12 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; module.exports = { - solid: [1], - dot: [1, 1], - dash: [4, 1], - longdash: [8, 1], - dashdot: [4, 1, 1, 1], - longdashdot: [8, 1, 1, 1] + solid: [1], + dot: [1, 1], + dash: [4, 1], + longdash: [8, 1], + dashdot: [4, 1, 1, 1], + longdashdot: [8, 1, 1, 1] }; diff --git a/src/constants/gl3d_dashes.js b/src/constants/gl3d_dashes.js index 8e4ac98164a..dee80abd774 100644 --- a/src/constants/gl3d_dashes.js +++ b/src/constants/gl3d_dashes.js @@ -5,15 +5,12 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; module.exports = { - solid: [[], 0], - dot: [[0.5, 1], 200], - dash: [[0.5, 1], 50], - longdash: [[0.5, 1], 10], - dashdot: [[0.5, 0.625, 0.875, 1], 50], - longdashdot: [[0.5, 0.7, 0.8, 1], 10] + solid: [[], 0], + dot: [[0.5, 1], 200], + dash: [[0.5, 1], 50], + longdash: [[0.5, 1], 10], + dashdot: [[0.5, 0.625, 0.875, 1], 50], + longdashdot: [[0.5, 0.7, 0.8, 1], 10] }; diff --git a/src/constants/gl_markers.js b/src/constants/gl_markers.js index e10354e84a4..59866362cc7 100644 --- a/src/constants/gl_markers.js +++ b/src/constants/gl_markers.js @@ -5,17 +5,14 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; module.exports = { - circle: '●', - 'circle-open': '○', - square: '■', - 'square-open': '□', - diamond: '◆', - 'diamond-open': '◇', - cross: '+', - x: '❌' + circle: "\u25CF", + "circle-open": "\u25CB", + square: "\u25A0", + "square-open": "\u25A1", + diamond: "\u25C6", + "diamond-open": "\u25C7", + cross: "+", + x: "\u274C" }; diff --git a/src/constants/interactions.js b/src/constants/interactions.js index 132e9ca4c37..681772f16d1 100644 --- a/src/constants/interactions.js +++ b/src/constants/interactions.js @@ -5,14 +5,11 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - /** + /** * Timing information for interactive elements */ - SHOW_PLACEHOLDER: 100, - HIDE_PLACEHOLDER: 1000 + SHOW_PLACEHOLDER: 100, + HIDE_PLACEHOLDER: 1000 }; diff --git a/src/constants/numerical.js b/src/constants/numerical.js index c881daa72c4..35da533fa1f 100644 --- a/src/constants/numerical.js +++ b/src/constants/numerical.js @@ -5,42 +5,40 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - /** + /** * Standardize all missing data in calcdata to use undefined * never null or NaN. * That way we can use !==undefined, or !== BADNUM, * to test for real data */ - BADNUM: undefined, - - /* + BADNUM: undefined, + /* * Limit certain operations to well below floating point max value * to avoid glitches: Make sure that even when you multiply it by the * number of pixels on a giant screen it still works */ - FP_SAFE: Number.MAX_VALUE / 10000, - - /* + FP_SAFE: ( + Number.MAX_VALUE / 10000 + ), + /* * conversion of date units to milliseconds * year and month constants are marked "AVG" * to remind us that not all years and months * have the same length */ - ONEAVGYEAR: 31557600000, // 365.25 days - ONEAVGMONTH: 2629800000, // 1/12 of ONEAVGYEAR - ONEDAY: 86400000, - ONEHOUR: 3600000, - ONEMIN: 60000, - ONESEC: 1000, - - /* + ONEAVGYEAR: 31557600000, + // 365.25 days + ONEAVGMONTH: 2629800000, + // 1/12 of ONEAVGYEAR + ONEDAY: 86400000, + ONEHOUR: 3600000, + ONEMIN: 60000, + ONESEC: 1000, + /* * For fast conversion btwn world calendars and epoch ms, the Julian Day Number * of the unix epoch. From calendars.instance().newDate(1970, 1, 1).toJD() */ - EPOCHJD: 2440587.5 + EPOCHJD: 2440587.5 }; diff --git a/src/constants/string_mappings.js b/src/constants/string_mappings.js index a2760f7b6c0..052735edb7d 100644 --- a/src/constants/string_mappings.js +++ b/src/constants/string_mappings.js @@ -5,32 +5,26 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; // N.B. HTML entities are listed without the leading '&' and trailing ';' module.exports = { - - entityToUnicode: { - 'mu': 'μ', - 'amp': '&', - 'lt': '<', - 'gt': '>', - 'nbsp': ' ', - 'times': '×', - 'plusmn': '±', - 'deg': '°' - }, - - unicodeToEntity: { - '&': 'amp', - '<': 'lt', - '>': 'gt', - '"': 'quot', - '\'': '#x27', - '\/': '#x2F' - } - + entityToUnicode: { + mu: "\u03BC", + amp: "&", + lt: "<", + gt: ">", + nbsp: "\xA0", + times: "\xD7", + plusmn: "\xB1", + deg: "\xB0" + }, + unicodeToEntity: { + "&": "amp", + "<": "lt", + ">": "gt", + '"': "quot", + "'": "#x27", + "/": "#x2F" + } }; diff --git a/src/constants/xmlns_namespaces.js b/src/constants/xmlns_namespaces.js index aaeaea827a3..26baebcbc80 100644 --- a/src/constants/xmlns_namespaces.js +++ b/src/constants/xmlns_namespaces.js @@ -5,18 +5,11 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - -exports.xmlns = 'http://www.w3.org/2000/xmlns/'; -exports.svg = 'http://www.w3.org/2000/svg'; -exports.xlink = 'http://www.w3.org/1999/xlink'; +"use strict"; +exports.xmlns = "http://www.w3.org/2000/xmlns/"; +exports.svg = "http://www.w3.org/2000/svg"; +exports.xlink = "http://www.w3.org/1999/xlink"; // the 'old' d3 quirk got fix in v3.5.7 // https://github.com/mbostock/d3/commit/a6f66e9dd37f764403fc7c1f26be09ab4af24fed -exports.svgAttrs = { - xmlns: exports.svg, - 'xmlns:xlink': exports.xlink -}; +exports.svgAttrs = { xmlns: exports.svg, "xmlns:xlink": exports.xlink }; diff --git a/src/core.js b/src/core.js index 23774702e83..c6420fa69ce 100644 --- a/src/core.js +++ b/src/core.js @@ -5,26 +5,24 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; /* * Export the plotly.js API methods. */ -var Plotly = require('./plotly'); +var Plotly = require("./plotly"); // package version injected by `npm run preprocess` -exports.version = '1.23.1'; +exports.version = "1.23.1"; // inject promise polyfill -require('es6-promise').polyfill(); +require("es6-promise").polyfill(); // inject plot css -require('../build/plotcss'); +require("../build/plotcss"); // inject default MathJax config -require('./fonts/mathjax_config'); +require("./fonts/mathjax_config"); // plot api exports.plot = Plotly.plot; @@ -39,39 +37,39 @@ exports.addTraces = Plotly.addTraces; exports.deleteTraces = Plotly.deleteTraces; exports.moveTraces = Plotly.moveTraces; exports.purge = Plotly.purge; -exports.setPlotConfig = require('./plot_api/set_plot_config'); -exports.register = require('./plot_api/register'); -exports.toImage = require('./plot_api/to_image'); -exports.downloadImage = require('./snapshot/download'); -exports.validate = require('./plot_api/validate'); +exports.setPlotConfig = require("./plot_api/set_plot_config"); +exports.register = require("./plot_api/register"); +exports.toImage = require("./plot_api/to_image"); +exports.downloadImage = require("./snapshot/download"); +exports.validate = require("./plot_api/validate"); exports.addFrames = Plotly.addFrames; exports.deleteFrames = Plotly.deleteFrames; exports.animate = Plotly.animate; // scatter is the only trace included by default -exports.register(require('./traces/scatter')); +exports.register(require("./traces/scatter")); // register all registrable components modules exports.register([ - require('./components/legend'), - require('./components/annotations'), - require('./components/shapes'), - require('./components/images'), - require('./components/updatemenus'), - require('./components/sliders'), - require('./components/rangeslider'), - require('./components/rangeselector') + require("./components/legend"), + require("./components/annotations"), + require("./components/shapes"), + require("./components/images"), + require("./components/updatemenus"), + require("./components/sliders"), + require("./components/rangeslider"), + require("./components/rangeselector") ]); // plot icons -exports.Icons = require('../build/ploticon'); +exports.Icons = require("../build/ploticon"); // unofficial 'beta' plot methods, use at your own risk exports.Plots = Plotly.Plots; exports.Fx = Plotly.Fx; -exports.Snapshot = require('./snapshot'); -exports.PlotSchema = require('./plot_api/plot_schema'); -exports.Queue = require('./lib/queue'); +exports.Snapshot = require("./snapshot"); +exports.PlotSchema = require("./plot_api/plot_schema"); +exports.Queue = require("./lib/queue"); // export d3 used in the bundle -exports.d3 = require('d3'); +exports.d3 = require("d3"); diff --git a/src/fonts/mathjax_config.js b/src/fonts/mathjax_config.js index 8005ad86e13..1799482a286 100644 --- a/src/fonts/mathjax_config.js +++ b/src/fonts/mathjax_config.js @@ -5,27 +5,23 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; /* global MathJax:false */ /** * Check and configure MathJax */ -if(typeof MathJax !== 'undefined') { - exports.MathJax = true; +if (typeof MathJax !== "undefined") { + exports.MathJax = true; - MathJax.Hub.Config({ - messageStyle: 'none', - skipStartupTypeset: true, - displayAlign: 'left', - tex2jax: { - inlineMath: [['$', '$'], ['\\(', '\\)']] - } - }); + MathJax.Hub.Config({ + messageStyle: "none", + skipStartupTypeset: true, + displayAlign: "left", + tex2jax: { inlineMath: [["$", "$"], ["\\(", "\\)"]] } + }); - MathJax.Hub.Configured(); + MathJax.Hub.Configured(); } else { - exports.MathJax = false; + exports.MathJax = false; } diff --git a/src/lib/array_to_calc_item.js b/src/lib/array_to_calc_item.js index 4a968234f0a..bd326d91b07 100644 --- a/src/lib/array_to_calc_item.js +++ b/src/lib/array_to_calc_item.js @@ -5,11 +5,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; // similar to Lib.mergeArray, but using inside a loop module.exports = function arrayToCalcItem(traceAttr, calcItem, calcAttr, i) { - if(Array.isArray(traceAttr)) calcItem[calcAttr] = traceAttr[i]; + if (Array.isArray(traceAttr)) calcItem[calcAttr] = traceAttr[i]; }; diff --git a/src/lib/clean_number.js b/src/lib/clean_number.js index 922c2db7e94..26d393b6499 100644 --- a/src/lib/clean_number.js +++ b/src/lib/clean_number.js @@ -5,13 +5,10 @@ * 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 isNumeric = require("fast-isnumeric"); - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var BADNUM = require('../constants/numerical').BADNUM; +var BADNUM = require("../constants/numerical").BADNUM; // precompile for speed var JUNK = /^['"%,$#\s']+|[, ]|['"%,$#\s']+$/g; @@ -21,11 +18,11 @@ var JUNK = /^['"%,$#\s']+|[, ]|['"%,$#\s']+$/g; * Always returns either a number or BADNUM. */ module.exports = function cleanNumber(v) { - if(typeof v === 'string') { - v = v.replace(JUNK, ''); - } + if (typeof v === "string") { + v = v.replace(JUNK, ""); + } - if(isNumeric(v)) return Number(v); + if (isNumeric(v)) return Number(v); - return BADNUM; + return BADNUM; }; diff --git a/src/lib/coerce.js b/src/lib/coerce.js index f3ef35d6598..346011173c2 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -5,269 +5,273 @@ * 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 isNumeric = require("fast-isnumeric"); +var tinycolor = require("tinycolor2"); - -'use strict'; - -var isNumeric = require('fast-isnumeric'); -var tinycolor = require('tinycolor2'); - -var getColorscale = require('../components/colorscale/get_scale'); -var colorscaleNames = Object.keys(require('../components/colorscale/scales')); -var nestedProperty = require('./nested_property'); +var getColorscale = require("../components/colorscale/get_scale"); +var colorscaleNames = Object.keys(require("../components/colorscale/scales")); +var nestedProperty = require("./nested_property"); var ID_REGEX = /^([2-9]|[1-9][0-9]+)$/; exports.valObjects = { - data_array: { - // You can use *dflt=[] to force said array to exist though. - description: [ - 'An {array} of data.', - 'The value MUST be an {array}, or we ignore it.' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - if(Array.isArray(v)) propOut.set(v); - else if(dflt !== undefined) propOut.set(dflt); - } - }, - enumerated: { - description: [ - 'Enumerated value type. The available values are listed', - 'in `values`.' - ].join(' '), - requiredOpts: ['values'], - otherOpts: ['dflt', 'coerceNumber', 'arrayOk'], - coerceFunction: function(v, propOut, dflt, opts) { - if(opts.coerceNumber) v = +v; - if(opts.values.indexOf(v) === -1) propOut.set(dflt); - else propOut.set(v); - } - }, - 'boolean': { - description: 'A boolean (true/false) value.', - requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - if(v === true || v === false) propOut.set(v); - else propOut.set(dflt); - } - }, - number: { - description: [ - 'A number or a numeric value', - '(e.g. a number inside a string).', - 'When applicable, values greater (less) than `max` (`min`)', - 'are coerced to the `dflt`.' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt', 'min', 'max', 'arrayOk'], - coerceFunction: function(v, propOut, dflt, opts) { - if(!isNumeric(v) || - (opts.min !== undefined && v < opts.min) || - (opts.max !== undefined && v > opts.max)) { - propOut.set(dflt); - } - else propOut.set(+v); - } - }, - integer: { - description: [ - 'An integer or an integer inside a string.', - 'When applicable, values greater (less) than `max` (`min`)', - 'are coerced to the `dflt`.' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt', 'min', 'max'], - coerceFunction: function(v, propOut, dflt, opts) { - if(v % 1 || !isNumeric(v) || - (opts.min !== undefined && v < opts.min) || - (opts.max !== undefined && v > opts.max)) { - propOut.set(dflt); - } - else propOut.set(+v); - } - }, - string: { - description: [ - 'A string value.', - 'Numbers are converted to strings except for attributes with', - '`strict` set to true.' - ].join(' '), - requiredOpts: [], - // TODO 'values shouldn't be in there (edge case: 'dash' in Scatter) - otherOpts: ['dflt', 'noBlank', 'strict', 'arrayOk', 'values'], - coerceFunction: function(v, propOut, dflt, opts) { - if(typeof v !== 'string') { - var okToCoerce = (typeof v === 'number'); - - if(opts.strict === true || !okToCoerce) propOut.set(dflt); - else propOut.set(String(v)); - } - else if(opts.noBlank && !v) propOut.set(dflt); - else propOut.set(v); - } - }, - color: { - description: [ - 'A string describing color.', - 'Supported formats:', - '- hex (e.g. \'#d3d3d3\')', - '- rgb (e.g. \'rgb(255, 0, 0)\')', - '- rgba (e.g. \'rgb(255, 0, 0, 0.5)\')', - '- hsl (e.g. \'hsl(0, 100%, 50%)\')', - '- hsv (e.g. \'hsv(0, 100%, 100%)\')', - '- named colors (full list: http://www.w3.org/TR/css3-color/#svg-color)' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt', 'arrayOk'], - coerceFunction: function(v, propOut, dflt) { - if(tinycolor(v).isValid()) propOut.set(v); - else propOut.set(dflt); - } - }, - colorscale: { - description: [ - 'A Plotly colorscale either picked by a name:', - '(any of', colorscaleNames.join(', '), ')', - 'customized as an {array} of 2-element {arrays} where', - 'the first element is the normalized color level value', - '(starting at *0* and ending at *1*),', - 'and the second item is a valid color string.' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - propOut.set(getColorscale(v, dflt)); - } - }, - angle: { - description: [ - 'A number (in degree) between -180 and 180.' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - if(v === 'auto') propOut.set('auto'); - else if(!isNumeric(v)) propOut.set(dflt); - else { - if(Math.abs(v) > 180) v -= Math.round(v / 360) * 360; - propOut.set(+v); - } - } + data_array: { + // You can use *dflt=[] to force said array to exist though. + description: [ + "An {array} of data.", + "The value MUST be an {array}, or we ignore it." + ].join(" "), + requiredOpts: [], + otherOpts: ["dflt"], + coerceFunction: function(v, propOut, dflt) { + if (Array.isArray(v)) propOut.set(v); + else if (dflt !== undefined) propOut.set(dflt); + } + }, + enumerated: { + description: [ + "Enumerated value type. The available values are listed", + "in `values`." + ].join(" "), + requiredOpts: ["values"], + otherOpts: ["dflt", "coerceNumber", "arrayOk"], + coerceFunction: function(v, propOut, dflt, opts) { + if (opts.coerceNumber) v = +v; + if (opts.values.indexOf(v) === -1) propOut.set(dflt); + else propOut.set(v); + } + }, + boolean: { + description: "A boolean (true/false) value.", + requiredOpts: [], + otherOpts: ["dflt"], + coerceFunction: function(v, propOut, dflt) { + if (v === true || v === false) propOut.set(v); + else propOut.set(dflt); + } + }, + number: { + description: [ + "A number or a numeric value", + "(e.g. a number inside a string).", + "When applicable, values greater (less) than `max` (`min`)", + "are coerced to the `dflt`." + ].join(" "), + requiredOpts: [], + otherOpts: ["dflt", "min", "max", "arrayOk"], + coerceFunction: function(v, propOut, dflt, opts) { + if ( + !isNumeric(v) || + opts.min !== undefined && v < opts.min || + opts.max !== undefined && v > opts.max + ) { + propOut.set(dflt); + } else { + propOut.set(+v); + } + } + }, + integer: { + description: [ + "An integer or an integer inside a string.", + "When applicable, values greater (less) than `max` (`min`)", + "are coerced to the `dflt`." + ].join(" "), + requiredOpts: [], + otherOpts: ["dflt", "min", "max"], + coerceFunction: function(v, propOut, dflt, opts) { + if ( + v % 1 || + !isNumeric(v) || + opts.min !== undefined && v < opts.min || + opts.max !== undefined && v > opts.max + ) { + propOut.set(dflt); + } else { + propOut.set(+v); + } + } + }, + string: { + description: [ + "A string value.", + "Numbers are converted to strings except for attributes with", + "`strict` set to true." + ].join(" "), + requiredOpts: [], + // TODO 'values shouldn't be in there (edge case: 'dash' in Scatter) + otherOpts: ["dflt", "noBlank", "strict", "arrayOk", "values"], + coerceFunction: function(v, propOut, dflt, opts) { + if (typeof v !== "string") { + var okToCoerce = typeof v === "number"; + + if (opts.strict === true || !okToCoerce) propOut.set(dflt); + else propOut.set(String(v)); + } else if (opts.noBlank && !v) propOut.set(dflt); + else propOut.set(v); + } + }, + color: { + description: [ + "A string describing color.", + "Supported formats:", + "- hex (e.g. '#d3d3d3')", + "- rgb (e.g. 'rgb(255, 0, 0)')", + "- rgba (e.g. 'rgb(255, 0, 0, 0.5)')", + "- hsl (e.g. 'hsl(0, 100%, 50%)')", + "- hsv (e.g. 'hsv(0, 100%, 100%)')", + "- named colors (full list: http://www.w3.org/TR/css3-color/#svg-color)" + ].join(" "), + requiredOpts: [], + otherOpts: ["dflt", "arrayOk"], + coerceFunction: function(v, propOut, dflt) { + if (tinycolor(v).isValid()) propOut.set(v); + else propOut.set(dflt); + } + }, + colorscale: { + description: [ + "A Plotly colorscale either picked by a name:", + "(any of", + colorscaleNames.join(", "), + ")", + "customized as an {array} of 2-element {arrays} where", + "the first element is the normalized color level value", + "(starting at *0* and ending at *1*),", + "and the second item is a valid color string." + ].join(" "), + requiredOpts: [], + otherOpts: ["dflt"], + coerceFunction: function(v, propOut, dflt) { + propOut.set(getColorscale(v, dflt)); + } + }, + angle: { + description: ["A number (in degree) between -180 and 180."].join(" "), + requiredOpts: [], + otherOpts: ["dflt"], + coerceFunction: function(v, propOut, dflt) { + if (v === "auto") { + propOut.set("auto"); + } else if (!isNumeric(v)) { + propOut.set(dflt); + } else { + if (Math.abs(v) > 180) v -= Math.round(v / 360) * 360; + propOut.set(+v); + } + } + }, + subplotid: { + description: [ + "An id string of a subplot type (given by dflt), optionally", + "followed by an integer >1. e.g. if dflt='geo', we can have", + "'geo', 'geo2', 'geo3', ..." + ].join(" "), + requiredOpts: ["dflt"], + otherOpts: [], + coerceFunction: function(v, propOut, dflt) { + var dlen = dflt.length; + if ( + typeof v === "string" && + v.substr(0, dlen) === dflt && + ID_REGEX.test(v.substr(dlen)) + ) { + propOut.set(v); + return; + } + propOut.set(dflt); }, - subplotid: { - description: [ - 'An id string of a subplot type (given by dflt), optionally', - 'followed by an integer >1. e.g. if dflt=\'geo\', we can have', - '\'geo\', \'geo2\', \'geo3\', ...' - ].join(' '), - requiredOpts: ['dflt'], - otherOpts: [], - coerceFunction: function(v, propOut, dflt) { - var dlen = dflt.length; - if(typeof v === 'string' && v.substr(0, dlen) === dflt && - ID_REGEX.test(v.substr(dlen))) { - propOut.set(v); - return; - } - propOut.set(dflt); - }, - validateFunction: function(v, opts) { - var dflt = opts.dflt, - dlen = dflt.length; + validateFunction: function(v, opts) { + var dflt = opts.dflt, dlen = dflt.length; - if(v === dflt) return true; - if(typeof v !== 'string') return false; - if(v.substr(0, dlen) === dflt && ID_REGEX.test(v.substr(dlen))) { - return true; - } + if (v === dflt) return true; + if (typeof v !== "string") return false; + if (v.substr(0, dlen) === dflt && ID_REGEX.test(v.substr(dlen))) { + return true; + } - return false; - } - }, - flaglist: { - description: [ - 'A string representing a combination of flags', - '(order does not matter here).', - 'Combine any of the available `flags` with *+*.', - '(e.g. (\'lines+markers\')).', - 'Values in `extras` cannot be combined.' - ].join(' '), - requiredOpts: ['flags'], - otherOpts: ['dflt', 'extras'], - coerceFunction: function(v, propOut, dflt, opts) { - if(typeof v !== 'string') { - propOut.set(dflt); - return; - } - if((opts.extras || []).indexOf(v) !== -1) { - propOut.set(v); - return; - } - var vParts = v.split('+'), - i = 0; - while(i < vParts.length) { - var vi = vParts[i]; - if(opts.flags.indexOf(vi) === -1 || vParts.indexOf(vi) < i) { - vParts.splice(i, 1); - } - else i++; - } - if(!vParts.length) propOut.set(dflt); - else propOut.set(vParts.join('+')); - } - }, - any: { - description: 'Any type.', - requiredOpts: [], - otherOpts: ['dflt', 'values', 'arrayOk'], - coerceFunction: function(v, propOut, dflt) { - if(v === undefined) propOut.set(dflt); - else propOut.set(v); + return false; + } + }, + flaglist: { + description: [ + "A string representing a combination of flags", + "(order does not matter here).", + "Combine any of the available `flags` with *+*.", + "(e.g. ('lines+markers')).", + "Values in `extras` cannot be combined." + ].join(" "), + requiredOpts: ["flags"], + otherOpts: ["dflt", "extras"], + coerceFunction: function(v, propOut, dflt, opts) { + if (typeof v !== "string") { + propOut.set(dflt); + return; + } + if ((opts.extras || []).indexOf(v) !== -1) { + propOut.set(v); + return; + } + var vParts = v.split("+"), i = 0; + while (i < vParts.length) { + var vi = vParts[i]; + if (opts.flags.indexOf(vi) === -1 || vParts.indexOf(vi) < i) { + vParts.splice(i, 1); + } else { + i++; } + } + if (!vParts.length) propOut.set(dflt); + else propOut.set(vParts.join("+")); + } + }, + any: { + description: "Any type.", + requiredOpts: [], + otherOpts: ["dflt", "values", "arrayOk"], + coerceFunction: function(v, propOut, dflt) { + if (v === undefined) propOut.set(dflt); + else propOut.set(v); + } + }, + info_array: { + description: ["An {array} of plot information."].join(" "), + requiredOpts: ["items"], + otherOpts: ["dflt", "freeLength"], + coerceFunction: function(v, propOut, dflt, opts) { + if (!Array.isArray(v)) { + propOut.set(dflt); + return; + } + + var items = opts.items, vOut = []; + dflt = Array.isArray(dflt) ? dflt : []; + + for (var i = 0; i < items.length; i++) { + exports.coerce(v, vOut, items, "[" + i + "]", dflt[i]); + } + + propOut.set(vOut); }, - info_array: { - description: [ - 'An {array} of plot information.' - ].join(' '), - requiredOpts: ['items'], - otherOpts: ['dflt', 'freeLength'], - coerceFunction: function(v, propOut, dflt, opts) { - if(!Array.isArray(v)) { - propOut.set(dflt); - return; - } - - var items = opts.items, - vOut = []; - dflt = Array.isArray(dflt) ? dflt : []; - - for(var i = 0; i < items.length; i++) { - exports.coerce(v, vOut, items, '[' + i + ']', dflt[i]); - } + validateFunction: function(v, opts) { + if (!Array.isArray(v)) return false; - propOut.set(vOut); - }, - validateFunction: function(v, opts) { - if(!Array.isArray(v)) return false; + var items = opts.items; - var items = opts.items; + // when free length is off, input and declared lengths must match + if (!opts.freeLength && v.length !== items.length) return false; - // when free length is off, input and declared lengths must match - if(!opts.freeLength && v.length !== items.length) return false; + // valid when all input items are valid + for (var i = 0; i < v.length; i++) { + var isItemValid = exports.validate(v[i], opts.items[i]); - // valid when all input items are valid - for(var i = 0; i < v.length; i++) { - var isItemValid = exports.validate(v[i], opts.items[i]); + if (!isItemValid) return false; + } - if(!isItemValid) return false; - } - - return true; - } + return true; } + } }; /** @@ -282,28 +286,34 @@ exports.valObjects = { * if dflt is provided as an argument to lib.coerce it takes precedence * as a convenience, returns the value it finally set */ -exports.coerce = function(containerIn, containerOut, attributes, attribute, dflt) { - var opts = nestedProperty(attributes, attribute).get(), - propIn = nestedProperty(containerIn, attribute), - propOut = nestedProperty(containerOut, attribute), - v = propIn.get(); - - if(dflt === undefined) dflt = opts.dflt; - - /** +exports.coerce = function( + containerIn, + containerOut, + attributes, + attribute, + dflt +) { + var opts = nestedProperty(attributes, attribute).get(), + propIn = nestedProperty(containerIn, attribute), + propOut = nestedProperty(containerOut, attribute), + v = propIn.get(); + + if (dflt === undefined) dflt = opts.dflt; + + /** * arrayOk: value MAY be an array, then we do no value checking * at this point, because it can be more complicated than the * individual form (eg. some array vals can be numbers, even if the * single values must be color strings) */ - if(opts.arrayOk && Array.isArray(v)) { - propOut.set(v); - return v; - } + if (opts.arrayOk && Array.isArray(v)) { + propOut.set(v); + return v; + } - exports.valObjects[opts.valType].coerceFunction(v, propOut, dflt, opts); + exports.valObjects[opts.valType].coerceFunction(v, propOut, dflt, opts); - return propOut.get(); + return propOut.get(); }; /** @@ -313,12 +323,24 @@ exports.coerce = function(containerIn, containerOut, attributes, attribute, dflt * returns attribute default if user input it not valid or * returns false if there is no user input. */ -exports.coerce2 = function(containerIn, containerOut, attributes, attribute, dflt) { - var propIn = nestedProperty(containerIn, attribute), - propOut = exports.coerce(containerIn, containerOut, attributes, attribute, dflt), - valIn = propIn.get(); - - return (valIn !== undefined && valIn !== null) ? propOut : false; +exports.coerce2 = function( + containerIn, + containerOut, + attributes, + attribute, + dflt +) { + var propIn = nestedProperty(containerIn, attribute), + propOut = exports.coerce( + containerIn, + containerOut, + attributes, + attribute, + dflt + ), + valIn = propIn.get(); + + return valIn !== undefined && valIn !== null ? propOut : false; }; /* @@ -327,32 +349,35 @@ exports.coerce2 = function(containerIn, containerOut, attributes, attribute, dfl * 'coerce' is a lib.coerce wrapper with implied first three arguments */ exports.coerceFont = function(coerce, attr, dfltObj) { - var out = {}; + var out = {}; - dfltObj = dfltObj || {}; + dfltObj = dfltObj || {}; - out.family = coerce(attr + '.family', dfltObj.family); - out.size = coerce(attr + '.size', dfltObj.size); - out.color = coerce(attr + '.color', dfltObj.color); + out.family = coerce(attr + ".family", dfltObj.family); + out.size = coerce(attr + ".size", dfltObj.size); + out.color = coerce(attr + ".color", dfltObj.color); - return out; + return out; }; exports.validate = function(value, opts) { - var valObject = exports.valObjects[opts.valType]; - - if(opts.arrayOk && Array.isArray(value)) return true; + var valObject = exports.valObjects[opts.valType]; - if(valObject.validateFunction) { - return valObject.validateFunction(value, opts); - } + if (opts.arrayOk && Array.isArray(value)) return true; - var failed = {}, - out = failed, - propMock = { set: function(v) { out = v; } }; + if (valObject.validateFunction) { + return valObject.validateFunction(value, opts); + } - // 'failed' just something mutable that won't be === anything else + var failed = {}, + out = failed, + propMock = { + set: function(v) { + out = v; + } + }; - valObject.coerceFunction(value, propMock, failed, opts); - return out !== failed; + // 'failed' just something mutable that won't be === anything else + valObject.coerceFunction(value, propMock, failed, opts); + return out !== failed; }; diff --git a/src/lib/dates.js b/src/lib/dates.js index c5c05fcfda9..dd91ba84ffe 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -5,17 +5,14 @@ * 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 d3 = require("d3"); +var isNumeric = require("fast-isnumeric"); +var logError = require("./loggers").error; +var mod = require("./mod"); -'use strict'; - -var d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var logError = require('./loggers').error; -var mod = require('./mod'); - -var constants = require('../constants/numerical'); +var constants = require("../constants/numerical"); var BADNUM = constants.BADNUM; var ONEDAY = constants.ONEDAY; var ONEHOUR = constants.ONEHOUR; @@ -23,7 +20,7 @@ var ONEMIN = constants.ONEMIN; var ONESEC = constants.ONESEC; var EPOCHJD = constants.EPOCHJD; -var Registry = require('../registry'); +var Registry = require("../registry"); var utcFormat = d3.time.format.utc; @@ -35,11 +32,10 @@ var DATETIME_REGEXP_CN = /^\s*(-?\d\d\d\d|\d\d)(-(\d?\di?)(-(\d?\d)([ Tt]([01]?\ var YFIRST = new Date().getFullYear() - 70; function isWorldCalendar(calendar) { - return ( - calendar && - Registry.componentsRegistry.calendars && - typeof calendar === 'string' && calendar !== 'gregorian' - ); + return calendar && + Registry.componentsRegistry.calendars && + typeof calendar === "string" && + calendar !== "gregorian"; } /* @@ -48,31 +44,29 @@ function isWorldCalendar(calendar) { * bool sunday is for week ticks, shift it to a Sunday. */ exports.dateTick0 = function(calendar, sunday) { - if(isWorldCalendar(calendar)) { - return sunday ? - Registry.getComponentMethod('calendars', 'CANONICAL_SUNDAY')[calendar] : - Registry.getComponentMethod('calendars', 'CANONICAL_TICK')[calendar]; - } - else { - return sunday ? '2000-01-02' : '2000-01-01'; - } + if (isWorldCalendar(calendar)) { + return sunday + ? Registry.getComponentMethod("calendars", "CANONICAL_SUNDAY")[calendar] + : Registry.getComponentMethod("calendars", "CANONICAL_TICK")[calendar]; + } else { + return sunday ? "2000-01-02" : "2000-01-01"; + } }; /* * dfltRange: for each calendar, give a valid default range */ exports.dfltRange = function(calendar) { - if(isWorldCalendar(calendar)) { - return Registry.getComponentMethod('calendars', 'DFLTRANGE')[calendar]; - } - else { - return ['2000-01-01', '2001-01-01']; - } + if (isWorldCalendar(calendar)) { + return Registry.getComponentMethod("calendars", "DFLTRANGE")[calendar]; + } else { + return ["2000-01-01", "2001-01-01"]; + } }; // is an object a javascript date? exports.isJSDate = function(v) { - return typeof v === 'object' && v !== null && typeof v.getTime === 'function'; + return typeof v === "object" && v !== null && typeof v.getTime === "function"; }; // The absolute limits of our date-time system @@ -134,97 +128,107 @@ var MIN_MS, MAX_MS; * 1946-2045 */ exports.dateTime2ms = function(s, calendar) { - // first check if s is a date object - if(exports.isJSDate(s)) { - // Convert to the UTC milliseconds that give the same - // hours as this date has in the local timezone - s = Number(s) - s.getTimezoneOffset() * ONEMIN; - if(s >= MIN_MS && s <= MAX_MS) return s; - return BADNUM; + // first check if s is a date object + if (exports.isJSDate(s)) { + // Convert to the UTC milliseconds that give the same + // hours as this date has in the local timezone + s = Number(s) - s.getTimezoneOffset() * ONEMIN; + if (s >= MIN_MS && s <= MAX_MS) return s; + return BADNUM; + } + // otherwise only accept strings and numbers + if (typeof s !== "string" && typeof s !== "number") return BADNUM; + + s = String(s); + + var isWorld = isWorldCalendar(calendar); + + // to handle out-of-range dates in international calendars, accept + // 'G' as a prefix to force the built-in gregorian calendar. + var s0 = s.charAt(0); + if (isWorld && (s0 === "G" || s0 === "g")) { + s = s.substr(1); + calendar = ""; + } + + var isChinese = isWorld && calendar.substr(0, 7) === "chinese"; + + var match = s.match(isChinese ? DATETIME_REGEXP_CN : DATETIME_REGEXP); + if (!match) return BADNUM; + var y = match[1], + m = match[3] || "1", + d = Number(match[5] || 1), + H = Number(match[7] || 0), + M = Number(match[9] || 0), + S = Number(match[11] || 0); + + if (isWorld) { + // disallow 2-digit years for world calendars + if (y.length === 2) return BADNUM; + y = Number(y); + + var cDate; + try { + var calInstance = Registry.getComponentMethod("calendars", "getCal")( + calendar + ); + if (isChinese) { + var isIntercalary = m.charAt(m.length - 1) === "i"; + m = parseInt(m, 10); + cDate = calInstance.newDate( + y, + calInstance.toMonthIndex(y, m, isIntercalary), + d + ); + } else { + cDate = calInstance.newDate(y, Number(m), d); + } + } catch (e) { + return BADNUM; } - // otherwise only accept strings and numbers - if(typeof s !== 'string' && typeof s !== 'number') return BADNUM; - s = String(s); + // Invalid ... date + if (!cDate) return BADNUM; - var isWorld = isWorldCalendar(calendar); + return (cDate.toJD() - EPOCHJD) * ONEDAY + + H * ONEHOUR + + M * ONEMIN + + S * ONESEC; + } - // to handle out-of-range dates in international calendars, accept - // 'G' as a prefix to force the built-in gregorian calendar. - var s0 = s.charAt(0); - if(isWorld && (s0 === 'G' || s0 === 'g')) { - s = s.substr(1); - calendar = ''; - } - - var isChinese = isWorld && calendar.substr(0, 7) === 'chinese'; - - var match = s.match(isChinese ? DATETIME_REGEXP_CN : DATETIME_REGEXP); - if(!match) return BADNUM; - var y = match[1], - m = match[3] || '1', - d = Number(match[5] || 1), - H = Number(match[7] || 0), - M = Number(match[9] || 0), - S = Number(match[11] || 0); - - if(isWorld) { - // disallow 2-digit years for world calendars - if(y.length === 2) return BADNUM; - y = Number(y); - - var cDate; - try { - var calInstance = Registry.getComponentMethod('calendars', 'getCal')(calendar); - if(isChinese) { - var isIntercalary = m.charAt(m.length - 1) === 'i'; - m = parseInt(m, 10); - cDate = calInstance.newDate(y, calInstance.toMonthIndex(y, m, isIntercalary), d); - } - else { - cDate = calInstance.newDate(y, Number(m), d); - } - } - catch(e) { return BADNUM; } // Invalid ... date - - if(!cDate) return BADNUM; - - return ((cDate.toJD() - EPOCHJD) * ONEDAY) + - (H * ONEHOUR) + (M * ONEMIN) + (S * ONESEC); - } - - if(y.length === 2) { - y = (Number(y) + 2000 - YFIRST) % 100 + YFIRST; - } - else y = Number(y); + if (y.length === 2) { + y = (Number(y) + 2000 - YFIRST) % 100 + YFIRST; + } else { + y = Number(y); + } - // new Date uses months from 0; subtract 1 here just so we - // don't have to do it again during the validity test below - m -= 1; + // new Date uses months from 0; subtract 1 here just so we + // don't have to do it again during the validity test below + m -= 1; - // javascript takes new Date(0..99,m,d) to mean 1900-1999, so - // to support years 0-99 we need to use setFullYear explicitly - // Note that 2000 is a leap year. - var date = new Date(Date.UTC(2000, m, d, H, M)); - date.setUTCFullYear(y); + // javascript takes new Date(0..99,m,d) to mean 1900-1999, so + // to support years 0-99 we need to use setFullYear explicitly + // Note that 2000 is a leap year. + var date = new Date(Date.UTC(2000, m, d, H, M)); + date.setUTCFullYear(y); - if(date.getUTCMonth() !== m) return BADNUM; - if(date.getUTCDate() !== d) return BADNUM; + if (date.getUTCMonth() !== m) return BADNUM; + if (date.getUTCDate() !== d) return BADNUM; - return date.getTime() + S * ONESEC; + return date.getTime() + S * ONESEC; }; -MIN_MS = exports.MIN_MS = exports.dateTime2ms('-9999'); -MAX_MS = exports.MAX_MS = exports.dateTime2ms('9999-12-31 23:59:59.9999'); +MIN_MS = exports.MIN_MS = exports.dateTime2ms("-9999"); +MAX_MS = exports.MAX_MS = exports.dateTime2ms("9999-12-31 23:59:59.9999"); // is string s a date? (see above) exports.isDateTime = function(s, calendar) { - return (exports.dateTime2ms(s, calendar) !== BADNUM); + return exports.dateTime2ms(s, calendar) !== BADNUM; }; // pad a number with zeroes, to given # of digits before the decimal point function lpad(val, digits) { - return String(val + Math.pow(10, digits)).substr(1); + return String(val + Math.pow(10, digits)).substr(1); } /** @@ -239,58 +243,65 @@ var NINETYDAYS = 90 * ONEDAY; var THREEHOURS = 3 * ONEHOUR; var FIVEMIN = 5 * ONEMIN; exports.ms2DateTime = function(ms, r, calendar) { - if(typeof ms !== 'number' || !(ms >= MIN_MS && ms <= MAX_MS)) return BADNUM; - - if(!r) r = 0; - - var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10), - msRounded = Math.round(ms - msecTenths / 10), - dateStr, h, m, s, msec10, d; - - if(isWorldCalendar(calendar)) { - var dateJD = Math.floor(msRounded / ONEDAY) + EPOCHJD, - timeMs = Math.floor(mod(ms, ONEDAY)); - try { - dateStr = Registry.getComponentMethod('calendars', 'getCal')(calendar) - .fromJD(dateJD).formatDate('yyyy-mm-dd'); - } - catch(e) { - // invalid date in this calendar - fall back to Gyyyy-mm-dd - dateStr = utcFormat('G%Y-%m-%d')(new Date(msRounded)); - } - - // yyyy does NOT guarantee 4-digit years. YYYY mostly does, but does - // other things for a few calendars, so we can't trust it. Just pad - // it manually (after the '-' if there is one) - if(dateStr.charAt(0) === '-') { - while(dateStr.length < 11) dateStr = '-0' + dateStr.substr(1); - } - else { - while(dateStr.length < 10) dateStr = '0' + dateStr; - } - - // TODO: if this is faster, we could use this block for extracting - // the time components of regular gregorian too - h = (r < NINETYDAYS) ? Math.floor(timeMs / ONEHOUR) : 0; - m = (r < NINETYDAYS) ? Math.floor((timeMs % ONEHOUR) / ONEMIN) : 0; - s = (r < THREEHOURS) ? Math.floor((timeMs % ONEMIN) / ONESEC) : 0; - msec10 = (r < FIVEMIN) ? (timeMs % ONESEC) * 10 + msecTenths : 0; + if (typeof ms !== "number" || !(ms >= MIN_MS && ms <= MAX_MS)) return BADNUM; + + if (!r) r = 0; + + var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10), + msRounded = Math.round(ms - msecTenths / 10), + dateStr, + h, + m, + s, + msec10, + d; + + if (isWorldCalendar(calendar)) { + var dateJD = Math.floor(msRounded / ONEDAY) + EPOCHJD, + timeMs = Math.floor(mod(ms, ONEDAY)); + try { + dateStr = Registry.getComponentMethod("calendars", "getCal")(calendar) + .fromJD(dateJD) + .formatDate("yyyy-mm-dd"); + } catch (e) { + // invalid date in this calendar - fall back to Gyyyy-mm-dd + dateStr = utcFormat("G%Y-%m-%d")(new Date(msRounded)); } - else { - d = new Date(msRounded); - - dateStr = utcFormat('%Y-%m-%d')(d); - - // <90 days: add hours and minutes - never *only* add hours - h = (r < NINETYDAYS) ? d.getUTCHours() : 0; - m = (r < NINETYDAYS) ? d.getUTCMinutes() : 0; - // <3 hours: add seconds - s = (r < THREEHOURS) ? d.getUTCSeconds() : 0; - // <5 minutes: add ms (plus one extra digit, this is msec*10) - msec10 = (r < FIVEMIN) ? d.getUTCMilliseconds() * 10 + msecTenths : 0; + + // yyyy does NOT guarantee 4-digit years. YYYY mostly does, but does + // other things for a few calendars, so we can't trust it. Just pad + // it manually (after the '-' if there is one) + if (dateStr.charAt(0) === "-") { + while (dateStr.length < 11) { + dateStr = "-0" + dateStr.substr(1); + } + } else { + while (dateStr.length < 10) { + dateStr = "0" + dateStr; + } } - return includeTime(dateStr, h, m, s, msec10); + // TODO: if this is faster, we could use this block for extracting + // the time components of regular gregorian too + h = r < NINETYDAYS ? Math.floor(timeMs / ONEHOUR) : 0; + m = r < NINETYDAYS ? Math.floor(timeMs % ONEHOUR / ONEMIN) : 0; + s = r < THREEHOURS ? Math.floor(timeMs % ONEMIN / ONESEC) : 0; + msec10 = r < FIVEMIN ? timeMs % ONESEC * 10 + msecTenths : 0; + } else { + d = new Date(msRounded); + + dateStr = utcFormat("%Y-%m-%d")(d); + + // <90 days: add hours and minutes - never *only* add hours + h = r < NINETYDAYS ? d.getUTCHours() : 0; + m = r < NINETYDAYS ? d.getUTCMinutes() : 0; + // <3 hours: add seconds + s = r < THREEHOURS ? d.getUTCSeconds() : 0; + // <5 minutes: add ms (plus one extra digit, this is msec*10) + msec10 = r < FIVEMIN ? d.getUTCMilliseconds() * 10 + msecTenths : 0; + } + + return includeTime(dateStr, h, m, s, msec10); }; // For converting old-style milliseconds to date strings, @@ -300,67 +311,68 @@ exports.ms2DateTime = function(ms, r, calendar) { // Clip one extra day off our date range though so we can't get // thrown beyond the range by the timezone shift. exports.ms2DateTimeLocal = function(ms) { - if(!(ms >= MIN_MS + ONEDAY && ms <= MAX_MS - ONEDAY)) return BADNUM; + if (!(ms >= MIN_MS + ONEDAY && ms <= MAX_MS - ONEDAY)) return BADNUM; - var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10), - d = new Date(Math.round(ms - msecTenths / 10)), - dateStr = d3.time.format('%Y-%m-%d')(d), - h = d.getHours(), - m = d.getMinutes(), - s = d.getSeconds(), - msec10 = d.getUTCMilliseconds() * 10 + msecTenths; + var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10), + d = new Date(Math.round(ms - msecTenths / 10)), + dateStr = d3.time.format("%Y-%m-%d")(d), + h = d.getHours(), + m = d.getMinutes(), + s = d.getSeconds(), + msec10 = d.getUTCMilliseconds() * 10 + msecTenths; - return includeTime(dateStr, h, m, s, msec10); + return includeTime(dateStr, h, m, s, msec10); }; function includeTime(dateStr, h, m, s, msec10) { - // include each part that has nonzero data in or after it - if(h || m || s || msec10) { - dateStr += ' ' + lpad(h, 2) + ':' + lpad(m, 2); - if(s || msec10) { - dateStr += ':' + lpad(s, 2); - if(msec10) { - var digits = 4; - while(msec10 % 10 === 0) { - digits -= 1; - msec10 /= 10; - } - dateStr += '.' + lpad(msec10, digits); - } + // include each part that has nonzero data in or after it + if (h || m || s || msec10) { + dateStr += " " + lpad(h, 2) + ":" + lpad(m, 2); + if (s || msec10) { + dateStr += ":" + lpad(s, 2); + if (msec10) { + var digits = 4; + while (msec10 % 10 === 0) { + digits -= 1; + msec10 /= 10; } + dateStr += "." + lpad(msec10, digits); + } } - return dateStr; + } + return dateStr; } // normalize date format to date string, in case it starts as // a Date object or milliseconds // optional dflt is the return value if cleaning fails exports.cleanDate = function(v, dflt, calendar) { - if(exports.isJSDate(v) || typeof v === 'number') { - // do not allow milliseconds (old) or jsdate objects (inherently - // described as gregorian dates) with world calendars - if(isWorldCalendar(calendar)) { - logError('JS Dates and milliseconds are incompatible with world calendars', v); - return dflt; - } - - // NOTE: if someone puts in a year as a number rather than a string, - // this will mistakenly convert it thinking it's milliseconds from 1970 - // that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds - v = exports.ms2DateTimeLocal(+v); - if(!v && dflt !== undefined) return dflt; + if (exports.isJSDate(v) || typeof v === "number") { + // do not allow milliseconds (old) or jsdate objects (inherently + // described as gregorian dates) with world calendars + if (isWorldCalendar(calendar)) { + logError( + "JS Dates and milliseconds are incompatible with world calendars", + v + ); + return dflt; } - else if(!exports.isDateTime(v, calendar)) { - logError('unrecognized date', v); - return dflt; - } - return v; + + // NOTE: if someone puts in a year as a number rather than a string, + // this will mistakenly convert it thinking it's milliseconds from 1970 + // that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds + v = exports.ms2DateTimeLocal(+v); + if (!v && dflt !== undefined) return dflt; + } else if (!exports.isDateTime(v, calendar)) { + logError("unrecognized date", v); + return dflt; + } + return v; }; /* * Date formatting for ticks and hovertext */ - /* * modDateFormat: Support world calendars, and add one item to * d3's vocabulary: @@ -368,26 +380,30 @@ exports.cleanDate = function(v, dflt, calendar) { */ var fracMatch = /%\d?f/g; function modDateFormat(fmt, x, calendar) { - - fmt = fmt.replace(fracMatch, function(match) { - var digits = Math.min(+(match.charAt(1)) || 6, 6), - fracSecs = ((x / 1000 % 1) + 2) - .toFixed(digits) - .substr(2).replace(/0+$/, '') || '0'; - return fracSecs; - }); - - var d = new Date(Math.floor(x + 0.05)); - - if(isWorldCalendar(calendar)) { - try { - fmt = Registry.getComponentMethod('calendars', 'worldCalFmt')(fmt, x, calendar); - } - catch(e) { - return 'Invalid'; - } + fmt = fmt.replace(fracMatch, function(match) { + var digits = Math.min(+match.charAt(1) || 6, 6), + fracSecs = (x / 1000 % 1 + 2) + .toFixed(digits) + .substr(2) + .replace(/0+$/, "") || + "0"; + return fracSecs; + }); + + var d = new Date(Math.floor(x + 0.05)); + + if (isWorldCalendar(calendar)) { + try { + fmt = Registry.getComponentMethod("calendars", "worldCalFmt")( + fmt, + x, + calendar + ); + } catch (e) { + return "Invalid"; } - return utcFormat(fmt)(d); + } + return utcFormat(fmt)(d); } /* @@ -398,15 +414,17 @@ function modDateFormat(fmt, x, calendar) { */ var MAXSECONDS = [59, 59.9, 59.99, 59.999, 59.9999]; function formatTime(x, tr) { - var timePart = mod(x + 0.05, ONEDAY); + var timePart = mod(x + 0.05, ONEDAY); - var timeStr = lpad(Math.floor(timePart / ONEHOUR), 2) + ':' + - lpad(mod(Math.floor(timePart / ONEMIN), 60), 2); + var timeStr = lpad(Math.floor(timePart / ONEHOUR), 2) + + ":" + + lpad(mod(Math.floor(timePart / ONEMIN), 60), 2); - if(tr !== 'M') { - if(!isNumeric(tr)) tr = 0; // should only be 'S' + if (tr !== "M") { + if (!isNumeric(tr)) tr = 0; - /* + // should only be 'S' + /* * this is a weird one - and shouldn't come up unless people * monkey with tick0 in weird ways, but we need to do something! * IN PARTICULAR we had better not display garbage (see below) @@ -421,27 +439,35 @@ function formatTime(x, tr) { * say we round seconds but floor everything else. BUT that means * we need to never round up to 60 seconds, ie 23:59:60 */ - var sec = Math.min(mod(x / ONESEC, 60), MAXSECONDS[tr]); - - var secStr = (100 + sec).toFixed(tr).substr(1); - if(tr > 0) { - secStr = secStr.replace(/0+$/, '').replace(/[\.]$/, ''); - } + var sec = Math.min(mod(x / ONESEC, 60), MAXSECONDS[tr]); - timeStr += ':' + secStr; + var secStr = (100 + sec).toFixed(tr).substr(1); + if (tr > 0) { + secStr = secStr.replace(/0+$/, "").replace(/[\.]$/, ""); } - return timeStr; + + timeStr += ":" + secStr; + } + return timeStr; } -var yearFormat = utcFormat('%Y'), - monthFormat = utcFormat('%b %Y'), - dayFormat = utcFormat('%b %-d'), - yearMonthDayFormat = utcFormat('%b %-d, %Y'); +var yearFormat = utcFormat("%Y"), + monthFormat = utcFormat("%b %Y"), + dayFormat = utcFormat("%b %-d"), + yearMonthDayFormat = utcFormat("%b %-d, %Y"); -function yearFormatWorld(cDate) { return cDate.formatDate('yyyy'); } -function monthFormatWorld(cDate) { return cDate.formatDate('M yyyy'); } -function dayFormatWorld(cDate) { return cDate.formatDate('M d'); } -function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy'); } +function yearFormatWorld(cDate) { + return cDate.formatDate("yyyy"); +} +function monthFormatWorld(cDate) { + return cDate.formatDate("M yyyy"); +} +function dayFormatWorld(cDate) { + return cDate.formatDate("M d"); +} +function yearMonthDayFormatWorld(cDate) { + return cDate.formatDate("M d, yyyy"); +} /* * formatDate: turn a date into tick or hover label text. @@ -459,48 +485,50 @@ function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy'); * one tick to the next (as it does with automatic formatting) */ exports.formatDate = function(x, fmt, tr, calendar) { - var headStr, - dateStr; - - calendar = isWorldCalendar(calendar) && calendar; - - if(fmt) return modDateFormat(fmt, x, calendar); - - if(calendar) { - try { - var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD, - cDate = Registry.getComponentMethod('calendars', 'getCal')(calendar) - .fromJD(dateJD); - - if(tr === 'y') dateStr = yearFormatWorld(cDate); - else if(tr === 'm') dateStr = monthFormatWorld(cDate); - else if(tr === 'd') { - headStr = yearFormatWorld(cDate); - dateStr = dayFormatWorld(cDate); - } - else { - headStr = yearMonthDayFormatWorld(cDate); - dateStr = formatTime(x, tr); - } - } - catch(e) { return 'Invalid'; } + var headStr, dateStr; + + calendar = isWorldCalendar(calendar) && calendar; + + if (fmt) return modDateFormat(fmt, x, calendar); + + if (calendar) { + try { + var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD, + cDate = Registry.getComponentMethod("calendars", "getCal")( + calendar + ).fromJD(dateJD); + + if (tr === "y") { + dateStr = yearFormatWorld(cDate); + } else if (tr === "m") { + dateStr = monthFormatWorld(cDate); + } else if (tr === "d") { + headStr = yearFormatWorld(cDate); + dateStr = dayFormatWorld(cDate); + } else { + headStr = yearMonthDayFormatWorld(cDate); + dateStr = formatTime(x, tr); + } + } catch (e) { + return "Invalid"; } - else { - var d = new Date(Math.floor(x + 0.05)); - - if(tr === 'y') dateStr = yearFormat(d); - else if(tr === 'm') dateStr = monthFormat(d); - else if(tr === 'd') { - headStr = yearFormat(d); - dateStr = dayFormat(d); - } - else { - headStr = yearMonthDayFormat(d); - dateStr = formatTime(x, tr); - } + } else { + var d = new Date(Math.floor(x + 0.05)); + + if (tr === "y") { + dateStr = yearFormat(d); + } else if (tr === "m") { + dateStr = monthFormat(d); + } else if (tr === "d") { + headStr = yearFormat(d); + dateStr = dayFormat(d); + } else { + headStr = yearMonthDayFormat(d); + dateStr = formatTime(x, tr); } + } - return dateStr + (headStr ? '\n' + headStr : ''); + return dateStr + (headStr ? "\n" + headStr : ""); }; /* @@ -531,33 +559,34 @@ exports.formatDate = function(x, fmt, tr, calendar) { */ var THREEDAYS = 3 * ONEDAY; exports.incrementMonth = function(ms, dMonth, calendar) { - calendar = isWorldCalendar(calendar) && calendar; - - // pull time out and operate on pure dates, then add time back at the end - // this gives maximum precision - not that we *normally* care if we're - // incrementing by month, but better to be safe! - var timeMs = mod(ms, ONEDAY); - ms = Math.round(ms - timeMs); - - if(calendar) { - try { - var dateJD = Math.round(ms / ONEDAY) + EPOCHJD, - calInstance = Registry.getComponentMethod('calendars', 'getCal')(calendar), - cDate = calInstance.fromJD(dateJD); - - if(dMonth % 12) calInstance.add(cDate, dMonth, 'm'); - else calInstance.add(cDate, dMonth / 12, 'y'); - - return (cDate.toJD() - EPOCHJD) * ONEDAY + timeMs; - } - catch(e) { - logError('invalid ms ' + ms + ' in calendar ' + calendar); - // then keep going in gregorian even though the result will be 'Invalid' - } + calendar = isWorldCalendar(calendar) && calendar; + + // pull time out and operate on pure dates, then add time back at the end + // this gives maximum precision - not that we *normally* care if we're + // incrementing by month, but better to be safe! + var timeMs = mod(ms, ONEDAY); + ms = Math.round(ms - timeMs); + + if (calendar) { + try { + var dateJD = Math.round(ms / ONEDAY) + EPOCHJD, + calInstance = Registry.getComponentMethod("calendars", "getCal")( + calendar + ), + cDate = calInstance.fromJD(dateJD); + + if (dMonth % 12) calInstance.add(cDate, dMonth, "m"); + else calInstance.add(cDate, dMonth / 12, "y"); + + return (cDate.toJD() - EPOCHJD) * ONEDAY + timeMs; + } catch (e) { + logError("invalid ms " + ms + " in calendar " + calendar); + // then keep going in gregorian even though the result will be 'Invalid' } + } - var y = new Date(ms + THREEDAYS); - return y.setUTCMonth(y.getUTCMonth() + dMonth) + timeMs - THREEDAYS; + var y = new Date(ms + THREEDAYS); + return y.setUTCMonth(y.getUTCMonth() + dMonth) + timeMs - THREEDAYS; }; /* @@ -567,60 +596,52 @@ exports.incrementMonth = function(ms, dMonth, calendar) { * calendar (string) the calendar to test against */ exports.findExactDates = function(data, calendar) { - var exactYears = 0, - exactMonths = 0, - exactDays = 0, - blankCount = 0, - d, - di; - - var calInstance = ( - isWorldCalendar(calendar) && - Registry.getComponentMethod('calendars', 'getCal')(calendar) - ); - - for(var i = 0; i < data.length; i++) { - di = data[i]; - - // not date data at all - if(!isNumeric(di)) { - blankCount ++; - continue; - } + var exactYears = 0, exactMonths = 0, exactDays = 0, blankCount = 0, d, di; - // not an exact date - if(di % ONEDAY) continue; - - if(calInstance) { - try { - d = calInstance.fromJD(di / ONEDAY + EPOCHJD); - if(d.day() === 1) { - if(d.month() === 1) exactYears++; - else exactMonths++; - } - else exactDays++; - } - catch(e) { - // invalid date in this calendar - ignore it here. - } - } - else { - d = new Date(di); - if(d.getUTCDate() === 1) { - if(d.getUTCMonth() === 0) exactYears++; - else exactMonths++; - } - else exactDays++; + var calInstance = isWorldCalendar(calendar) && + Registry.getComponentMethod("calendars", "getCal")(calendar); + + for (var i = 0; i < data.length; i++) { + di = data[i]; + + // not date data at all + if (!isNumeric(di)) { + blankCount++; + continue; + } + + // not an exact date + if (di % ONEDAY) continue; + + if (calInstance) { + try { + d = calInstance.fromJD(di / ONEDAY + EPOCHJD); + if (d.day() === 1) { + if (d.month() === 1) exactYears++; + else exactMonths++; + } else { + exactDays++; } + } catch (e) { + } + } else { + d = new Date(di); + if (d.getUTCDate() === 1) { + if (d.getUTCMonth() === 0) exactYears++; + else exactMonths++; + } else { + exactDays++; + } } - exactMonths += exactYears; - exactDays += exactMonths; + } + exactMonths += exactYears; + exactDays += exactMonths; - var dataCount = data.length - blankCount; + var dataCount = data.length - blankCount; - return { - exactYears: exactYears / dataCount, - exactMonths: exactMonths / dataCount, - exactDays: exactDays / dataCount - }; + return { + exactYears: exactYears / dataCount, + exactMonths: exactMonths / dataCount, + exactDays: exactDays / dataCount + }; }; diff --git a/src/lib/events.js b/src/lib/events.js index 8238384242a..6648ea2a76f 100644 --- a/src/lib/events.js +++ b/src/lib/events.js @@ -5,35 +5,30 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; /* global jQuery:false */ -var EventEmitter = require('events').EventEmitter; +var EventEmitter = require("events").EventEmitter; var Events = { - - init: function(plotObj) { - - /* + init: function(plotObj) { + /* * If we have already instantiated an emitter for this plot * return early. */ - if(plotObj._ev instanceof EventEmitter) return plotObj; + if (plotObj._ev instanceof EventEmitter) return plotObj; - var ev = new EventEmitter(); - var internalEv = new EventEmitter(); + var ev = new EventEmitter(); + var internalEv = new EventEmitter(); - /* + /* * Assign to plot._ev while we still live in a land * where plot is a DOM element with stuff attached to it. * In the future we can make plot the event emitter itself. */ - plotObj._ev = ev; + plotObj._ev = ev; - /* + /* * Create a second event handler that will manage events *internally*. * This allows parts of plotly to respond to thing like relayout without * having to use the user-facing event handler. They cannot peacefully @@ -41,9 +36,9 @@ var Events = { * plotObj.removeAllListeners() would detach internal events, breaking * plotly. */ - plotObj._internalEv = internalEv; + plotObj._internalEv = internalEv; - /* + /* * Assign bound methods from the ev to the plot object. These methods * will reference the 'this' of plot._ev even though they are methods * of plot. This will keep the event machinery away from the plot object @@ -52,39 +47,42 @@ var Events = { * methods have been bound to `plot` as some do not currently add value to * the Plotly event API. */ - plotObj.on = ev.on.bind(ev); - plotObj.once = ev.once.bind(ev); - plotObj.removeListener = ev.removeListener.bind(ev); - plotObj.removeAllListeners = ev.removeAllListeners.bind(ev); + plotObj.on = ev.on.bind(ev); + plotObj.once = ev.once.bind(ev); + plotObj.removeListener = ev.removeListener.bind(ev); + plotObj.removeAllListeners = ev.removeAllListeners.bind(ev); - /* + /* * Create funtions for managing internal events. These are *only* triggered * by the mirroring of external events via the emit function. */ - plotObj._internalOn = internalEv.on.bind(internalEv); - plotObj._internalOnce = internalEv.once.bind(internalEv); - plotObj._removeInternalListener = internalEv.removeListener.bind(internalEv); - plotObj._removeAllInternalListeners = internalEv.removeAllListeners.bind(internalEv); + plotObj._internalOn = internalEv.on.bind(internalEv); + plotObj._internalOnce = internalEv.once.bind(internalEv); + plotObj._removeInternalListener = internalEv.removeListener.bind( + internalEv + ); + plotObj._removeAllInternalListeners = internalEv.removeAllListeners.bind( + internalEv + ); - /* + /* * We must wrap emit to continue to support JQuery events. The idea * is to check to see if the user is using JQuery events, if they are * we emit JQuery events to trigger user handlers as well as the EventEmitter * events. */ - plotObj.emit = function(event, data) { - if(typeof jQuery !== 'undefined') { - jQuery(plotObj).trigger(event, data); - } - - ev.emit(event, data); - internalEv.emit(event, data); - }; - - return plotObj; - }, - - /* + plotObj.emit = function(event, data) { + if (typeof jQuery !== "undefined") { + jQuery(plotObj).trigger(event, data); + } + + ev.emit(event, data); + internalEv.emit(event, data); + }; + + return plotObj; + }, + /* * This function behaves like jQueries triggerHandler. It calls * all handlers for a particular event and returns the return value * of the LAST handler. This function also triggers jQuery's @@ -94,71 +92,70 @@ var Events = { * so the additional behavior of triggerHandler triggering internal events * is deliberate excluded in order to avoid reinforcing more usage. */ - triggerHandler: function(plotObj, event, data) { - var jQueryHandlerValue; - var nodeEventHandlerValue; - /* + triggerHandler: function(plotObj, event, data) { + var jQueryHandlerValue; + var nodeEventHandlerValue; + /* * If Jquery exists run all its handlers for this event and * collect the return value of the LAST handler function */ - if(typeof jQuery !== 'undefined') { - jQueryHandlerValue = jQuery(plotObj).triggerHandler(event, data); - } + if (typeof jQuery !== "undefined") { + jQueryHandlerValue = jQuery(plotObj).triggerHandler(event, data); + } - /* + /* * Now run all the node style event handlers */ - var ev = plotObj._ev; - if(!ev) return jQueryHandlerValue; + var ev = plotObj._ev; + if (!ev) return jQueryHandlerValue; - var handlers = ev._events[event]; - if(!handlers) return jQueryHandlerValue; + var handlers = ev._events[event]; + if (!handlers) return jQueryHandlerValue; - /* + /* * handlers can be function or an array of functions */ - if(typeof handlers === 'function') handlers = [handlers]; - var lastHandler = handlers.pop(); + if (typeof handlers === "function") handlers = [handlers]; + var lastHandler = handlers.pop(); - /* + /* * Call all the handlers except the last one. */ - for(var i = 0; i < handlers.length; i++) { - handlers[i](data); - } + for (var i = 0; i < handlers.length; i++) { + handlers[i](data); + } - /* + /* * Now call the final handler and collect its value */ - nodeEventHandlerValue = lastHandler(data); + nodeEventHandlerValue = lastHandler(data); - /* + /* * Return either the jquery handler value if it exists or the * nodeEventHandler value. Jquery event value superceeds nodejs * events for backwards compatability reasons. */ - return jQueryHandlerValue !== undefined ? jQueryHandlerValue : - nodeEventHandlerValue; - }, - - purge: function(plotObj) { - delete plotObj._ev; - delete plotObj.on; - delete plotObj.once; - delete plotObj.removeListener; - delete plotObj.removeAllListeners; - delete plotObj.emit; - - delete plotObj._ev; - delete plotObj._internalEv; - delete plotObj._internalOn; - delete plotObj._internalOnce; - delete plotObj._removeInternalListener; - delete plotObj._removeAllInternalListeners; - - return plotObj; - } - + return jQueryHandlerValue !== undefined + ? jQueryHandlerValue + : nodeEventHandlerValue; + }, + purge: function(plotObj) { + delete plotObj._ev; + delete plotObj.on; + delete plotObj.once; + delete plotObj.removeListener; + delete plotObj.removeAllListeners; + delete plotObj.emit; + + delete plotObj._ev; + delete plotObj._internalEv; + delete plotObj._internalOn; + delete plotObj._internalOnce; + delete plotObj._removeInternalListener; + delete plotObj._removeAllInternalListeners; + + return plotObj; + } }; module.exports = Events; diff --git a/src/lib/extend.js b/src/lib/extend.js index b0591778b64..06f1b77e2d2 100644 --- a/src/lib/extend.js +++ b/src/lib/extend.js @@ -5,41 +5,38 @@ * 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 isPlainObject = require('./is_plain_object.js'); +"use strict"; +var isPlainObject = require("./is_plain_object.js"); var isArray = Array.isArray; function primitivesLoopSplice(source, target) { - var i, value; - for(i = 0; i < source.length; i++) { - value = source[i]; - if(value !== null && typeof(value) === 'object') { - return false; - } - if(value !== void(0)) { - target[i] = value; - } + var i, value; + for (i = 0; i < source.length; i++) { + value = source[i]; + if (value !== null && typeof value === "object") { + return false; + } + if (value !== void 0) { + target[i] = value; } - return true; + } + return true; } exports.extendFlat = function() { - return _extend(arguments, false, false, false); + return _extend(arguments, false, false, false); }; exports.extendDeep = function() { - return _extend(arguments, true, false, false); + return _extend(arguments, true, false, false); }; exports.extendDeepAll = function() { - return _extend(arguments, true, true, false); + return _extend(arguments, true, true, false); }; exports.extendDeepNoArrays = function() { - return _extend(arguments, true, false, true); + return _extend(arguments, true, false, true); }; /* @@ -60,53 +57,56 @@ exports.extendDeepNoArrays = function() { * */ function _extend(inputs, isDeep, keepAllKeys, noArrayCopies) { - var target = inputs[0], - length = inputs.length; + var target = inputs[0], length = inputs.length; - var input, key, src, copy, copyIsArray, clone, allPrimitives; + var input, key, src, copy, copyIsArray, clone, allPrimitives; - if(length === 2 && isArray(target) && isArray(inputs[1]) && target.length === 0) { + if ( + length === 2 && isArray(target) && isArray(inputs[1]) && target.length === 0 + ) { + allPrimitives = primitivesLoopSplice(inputs[1], target); - allPrimitives = primitivesLoopSplice(inputs[1], target); - - if(allPrimitives) { - return target; + if (allPrimitives) { + return target; + } else { + target.splice(0, target.length); // reset target and continue to next block + } + } + + for (var i = 1; i < length; i++) { + input = inputs[i]; + + for (key in input) { + src = target[key]; + copy = input[key]; + + // Stop early and just transfer the array if array copies are disallowed: + if (noArrayCopies && isArray(copy)) { + target[key] = copy; + } else if ( + isDeep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy))) + ) { + // recurse if we're merging plain objects or arrays + if (copyIsArray) { + copyIsArray = false; + clone = src && isArray(src) ? src : []; } else { - target.splice(0, target.length); // reset target and continue to next block + clone = src && isPlainObject(src) ? src : {}; } - } - - for(var i = 1; i < length; i++) { - input = inputs[i]; - - for(key in input) { - src = target[key]; - copy = input[key]; - - // Stop early and just transfer the array if array copies are disallowed: - if(noArrayCopies && isArray(copy)) { - target[key] = copy; - } - // recurse if we're merging plain objects or arrays - else if(isDeep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { - if(copyIsArray) { - copyIsArray = false; - clone = src && isArray(src) ? src : []; - } else { - clone = src && isPlainObject(src) ? src : {}; - } - - // never move original objects, clone them - target[key] = _extend([clone, copy], isDeep, keepAllKeys, noArrayCopies); - } - - // don't bring in undefined values, except for extendDeepAll - else if(typeof copy !== 'undefined' || keepAllKeys) { - target[key] = copy; - } - } + // never move original objects, clone them + target[key] = _extend( + [clone, copy], + isDeep, + keepAllKeys, + noArrayCopies + ); + } else if (typeof copy !== "undefined" || keepAllKeys) { + // don't bring in undefined values, except for extendDeepAll + target[key] = copy; + } } + } - return target; + return target; } diff --git a/src/lib/filter_unique.js b/src/lib/filter_unique.js index 5d035707696..927fa0fb7bc 100644 --- a/src/lib/filter_unique.js +++ b/src/lib/filter_unique.js @@ -5,11 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; /** * Return news array containing only the unique items * found in input array. @@ -32,18 +28,16 @@ * @return {array} new filtered array */ module.exports = function filterUnique(array) { - var seen = {}, - out = [], - j = 0; + var seen = {}, out = [], j = 0; - for(var i = 0; i < array.length; i++) { - var item = array[i]; + for (var i = 0; i < array.length; i++) { + var item = array[i]; - if(seen[item] !== 1) { - seen[item] = 1; - out[j++] = item; - } + if (seen[item] !== 1) { + seen[item] = 1; + out[j++] = item; } + } - return out; + return out; }; diff --git a/src/lib/filter_visible.js b/src/lib/filter_visible.js index fdcf6674de3..3bde55405af 100644 --- a/src/lib/filter_visible.js +++ b/src/lib/filter_visible.js @@ -5,10 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; /** Filter out object items with visible !== true * insider array container. * @@ -17,13 +14,13 @@ * */ module.exports = function filterVisible(container) { - var out = []; + var out = []; - for(var i = 0; i < container.length; i++) { - var item = container[i]; + for (var i = 0; i < container.length; i++) { + var item = container[i]; - if(item.visible === true) out.push(item); - } + if (item.visible === true) out.push(item); + } - return out; + return out; }; diff --git a/src/lib/geo_location_utils.js b/src/lib/geo_location_utils.js index 30795820c37..c12e288f5de 100644 --- a/src/lib/geo_location_utils.js +++ b/src/lib/geo_location_utils.js @@ -5,56 +5,54 @@ * 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 countryRegex = require('country-regex'); -var Lib = require('../lib'); - +"use strict"; +var countryRegex = require("country-regex"); +var Lib = require("../lib"); // make list of all country iso3 ids from at runtime var countryIds = Object.keys(countryRegex); var locationmodeToIdFinder = { - 'ISO-3': Lib.identity, - 'USA-states': Lib.identity, - 'country names': countryNameToISO3 + "ISO-3": Lib.identity, + "USA-states": Lib.identity, + "country names": countryNameToISO3 }; exports.locationToFeature = function(locationmode, location, features) { - var locationId = getLocationId(locationmode, location); + var locationId = getLocationId(locationmode, location); - if(locationId) { - for(var i = 0; i < features.length; i++) { - var feature = features[i]; + if (locationId) { + for (var i = 0; i < features.length; i++) { + var feature = features[i]; - if(feature.id === locationId) return feature; - } - - Lib.warn([ - 'Location with id', locationId, - 'does not have a matching topojson feature at this resolution.' - ].join(' ')); + if (feature.id === locationId) return feature; } - return false; + Lib.warn( + [ + "Location with id", + locationId, + "does not have a matching topojson feature at this resolution." + ].join(" ") + ); + } + + return false; }; function getLocationId(locationmode, location) { - var idFinder = locationmodeToIdFinder[locationmode]; - return idFinder(location); + var idFinder = locationmodeToIdFinder[locationmode]; + return idFinder(location); } function countryNameToISO3(countryName) { - for(var i = 0; i < countryIds.length; i++) { - var iso3 = countryIds[i], - regex = new RegExp(countryRegex[iso3]); + for (var i = 0; i < countryIds.length; i++) { + var iso3 = countryIds[i], regex = new RegExp(countryRegex[iso3]); - if(regex.test(countryName.toLowerCase())) return iso3; - } + if (regex.test(countryName.toLowerCase())) return iso3; + } - Lib.warn('Unrecognized country name: ' + countryName + '.'); + Lib.warn("Unrecognized country name: " + countryName + "."); - return false; + return false; } diff --git a/src/lib/geojson_utils.js b/src/lib/geojson_utils.js index 919472f4b0d..b26e74acbab 100644 --- a/src/lib/geojson_utils.js +++ b/src/lib/geojson_utils.js @@ -5,10 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; /** * Convert calcTrace to GeoJSON 'MultiLineString' coordinate arrays * @@ -21,29 +18,26 @@ * */ exports.calcTraceToLineCoords = function(calcTrace) { - var trace = calcTrace[0].trace, - connectgaps = trace.connectgaps; + var trace = calcTrace[0].trace, connectgaps = trace.connectgaps; - var coords = [], - lineString = []; + var coords = [], lineString = []; - for(var i = 0; i < calcTrace.length; i++) { - var calcPt = calcTrace[i]; + for (var i = 0; i < calcTrace.length; i++) { + var calcPt = calcTrace[i]; - lineString.push(calcPt.lonlat); + lineString.push(calcPt.lonlat); - if(!connectgaps && calcPt.gapAfter && lineString.length > 0) { - coords.push(lineString); - lineString = []; - } + if (!connectgaps && calcPt.gapAfter && lineString.length > 0) { + coords.push(lineString); + lineString = []; } + } - coords.push(lineString); + coords.push(lineString); - return coords; + return coords; }; - /** * Make line ('LineString' or 'MultiLineString') GeoJSON * @@ -57,24 +51,17 @@ exports.calcTraceToLineCoords = function(calcTrace) { * */ exports.makeLine = function(coords, trace) { - var out = {}; + var out = {}; - if(coords.length === 1) { - out = { - type: 'LineString', - coordinates: coords[0] - }; - } - else { - out = { - type: 'MultiLineString', - coordinates: coords - }; - } + if (coords.length === 1) { + out = { type: "LineString", coordinates: coords[0] }; + } else { + out = { type: "MultiLineString", coordinates: coords }; + } - if(trace) out.trace = trace; + if (trace) out.trace = trace; - return out; + return out; }; /** @@ -89,30 +76,23 @@ exports.makeLine = function(coords, trace) { * GeoJSON object */ exports.makePolygon = function(coords, trace) { - var out = {}; - - if(coords.length === 1) { - out = { - type: 'Polygon', - coordinates: coords - }; - } - else { - var _coords = new Array(coords.length); + var out = {}; - for(var i = 0; i < coords.length; i++) { - _coords[i] = [coords[i]]; - } + if (coords.length === 1) { + out = { type: "Polygon", coordinates: coords }; + } else { + var _coords = new Array(coords.length); - out = { - type: 'MultiPolygon', - coordinates: _coords - }; + for (var i = 0; i < coords.length; i++) { + _coords[i] = [coords[i]]; } - if(trace) out.trace = trace; + out = { type: "MultiPolygon", coordinates: _coords }; + } + + if (trace) out.trace = trace; - return out; + return out; }; /** @@ -123,8 +103,5 @@ exports.makePolygon = function(coords, trace) { * */ exports.makeBlank = function() { - return { - type: 'Point', - coordinates: [] - }; + return { type: "Point", coordinates: [] }; }; diff --git a/src/lib/gl_format_color.js b/src/lib/gl_format_color.js index ac3f08aeb3e..0804799232d 100644 --- a/src/lib/gl_format_color.js +++ b/src/lib/gl_format_color.js @@ -5,77 +5,78 @@ * 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 tinycolor = require("tinycolor2"); +var isNumeric = require("fast-isnumeric"); +var Colorscale = require("../components/colorscale"); +var colorDflt = require("../components/color/attributes").defaultLine; -'use strict'; - -var tinycolor = require('tinycolor2'); -var isNumeric = require('fast-isnumeric'); - -var Colorscale = require('../components/colorscale'); -var colorDflt = require('../components/color/attributes').defaultLine; - -var str2RgbaArray = require('./str2rgbarray'); +var str2RgbaArray = require("./str2rgbarray"); var opacityDflt = 1; function calculateColor(colorIn, opacityIn) { - var colorOut = str2RgbaArray(colorIn); - colorOut[3] *= opacityIn; - return colorOut; + var colorOut = str2RgbaArray(colorIn); + colorOut[3] *= opacityIn; + return colorOut; } function validateColor(colorIn) { - return tinycolor(colorIn).isValid() ? colorIn : colorDflt; + return tinycolor(colorIn).isValid() ? colorIn : colorDflt; } function validateOpacity(opacityIn) { - return isNumeric(opacityIn) ? opacityIn : opacityDflt; + return isNumeric(opacityIn) ? opacityIn : opacityDflt; } function formatColor(containerIn, opacityIn, len) { - var colorIn = containerIn.color, - isArrayColorIn = Array.isArray(colorIn), - isArrayOpacityIn = Array.isArray(opacityIn), - colorOut = []; - - var sclFunc, getColor, getOpacity, colori, opacityi; - - if(containerIn.colorscale !== undefined) { - sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - containerIn.colorscale, - containerIn.cmin, - containerIn.cmax - ) - ); - } - else sclFunc = validateColor; - - if(isArrayColorIn) { - getColor = function(c, i) { - return c[i] === undefined ? colorDflt : sclFunc(c[i]); - }; - } - else getColor = validateColor; - - if(isArrayOpacityIn) { - getOpacity = function(o, i) { - return o[i] === undefined ? opacityDflt : validateOpacity(o[i]); - }; - } - else getOpacity = validateOpacity; - - if(isArrayColorIn || isArrayOpacityIn) { - for(var i = 0; i < len; i++) { - colori = getColor(colorIn, i); - opacityi = getOpacity(opacityIn, i); - colorOut[i] = calculateColor(colori, opacityi); - } + var colorIn = containerIn.color, + isArrayColorIn = Array.isArray(colorIn), + isArrayOpacityIn = Array.isArray(opacityIn), + colorOut = []; + + var sclFunc, getColor, getOpacity, colori, opacityi; + + if (containerIn.colorscale !== undefined) { + sclFunc = Colorscale.makeColorScaleFunc( + Colorscale.extractScale( + containerIn.colorscale, + containerIn.cmin, + containerIn.cmax + ) + ); + } else { + sclFunc = validateColor; + } + + if (isArrayColorIn) { + getColor = function(c, i) { + return c[i] === undefined ? colorDflt : sclFunc(c[i]); + }; + } else { + getColor = validateColor; + } + + if (isArrayOpacityIn) { + getOpacity = function(o, i) { + return o[i] === undefined ? opacityDflt : validateOpacity(o[i]); + }; + } else { + getOpacity = validateOpacity; + } + + if (isArrayColorIn || isArrayOpacityIn) { + for (var i = 0; i < len; i++) { + colori = getColor(colorIn, i); + opacityi = getOpacity(opacityIn, i); + colorOut[i] = calculateColor(colori, opacityi); } - else colorOut = calculateColor(colorIn, opacityIn); + } else { + colorOut = calculateColor(colorIn, opacityIn); + } - return colorOut; + return colorOut; } module.exports = formatColor; diff --git a/src/lib/html2unicode.js b/src/lib/html2unicode.js index 346ecaaf90f..e184cfb31c5 100644 --- a/src/lib/html2unicode.js +++ b/src/lib/html2unicode.js @@ -5,63 +5,57 @@ * 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 toSuperScript = require('superscript-text'); -var stringMappings = require('../constants/string_mappings'); +"use strict"; +var toSuperScript = require("superscript-text"); +var stringMappings = require("../constants/string_mappings"); function fixSuperScript(x) { - var idx = 0; + var idx = 0; - while((idx = x.indexOf('', idx)) >= 0) { - var nidx = x.indexOf('', idx); - if(nidx < idx) break; + while ((idx = x.indexOf("", idx)) >= 0) { + var nidx = x.indexOf("", idx); + if (nidx < idx) break; - x = x.slice(0, idx) + toSuperScript(x.slice(idx + 5, nidx)) + x.slice(nidx + 6); - } + x = x.slice(0, idx) + + toSuperScript(x.slice(idx + 5, nidx)) + + x.slice(nidx + 6); + } - return x; + return x; } function fixBR(x) { - return x.replace(/\/g, '\n'); + return x.replace(/\/g, "\n"); } function stripTags(x) { - return x.replace(/\<.*\>/g, ''); + return x.replace(/\<.*\>/g, ""); } function fixEntities(x) { - var entityToUnicode = stringMappings.entityToUnicode; - var idx = 0; - - while((idx = x.indexOf('&', idx)) >= 0) { - var nidx = x.indexOf(';', idx); - if(nidx < idx) { - idx += 1; - continue; - } + var entityToUnicode = stringMappings.entityToUnicode; + var idx = 0; + + while ((idx = x.indexOf("&", idx)) >= 0) { + var nidx = x.indexOf(";", idx); + if (nidx < idx) { + idx += 1; + continue; + } - var entity = entityToUnicode[x.slice(idx + 1, nidx)]; - if(entity) { - x = x.slice(0, idx) + entity + x.slice(nidx + 1); - } else { - x = x.slice(0, idx) + x.slice(nidx + 1); - } + var entity = entityToUnicode[x.slice(idx + 1, nidx)]; + if (entity) { + x = x.slice(0, idx) + entity + x.slice(nidx + 1); + } else { + x = x.slice(0, idx) + x.slice(nidx + 1); } + } - return x; + return x; } function convertHTMLToUnicode(html) { - return '' + - fixEntities( - stripTags( - fixSuperScript( - fixBR( - html)))); + return "" + fixEntities(stripTags(fixSuperScript(fixBR(html)))); } module.exports = convertHTMLToUnicode; diff --git a/src/lib/index.js b/src/lib/index.js index 9544f4b3794..cb7c2fe644a 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -5,27 +5,24 @@ * 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 d3 = require('d3'); +"use strict"; +var d3 = require("d3"); var lib = module.exports = {}; -lib.nestedProperty = require('./nested_property'); -lib.isPlainObject = require('./is_plain_object'); -lib.isArray = require('./is_array'); -lib.mod = require('./mod'); +lib.nestedProperty = require("./nested_property"); +lib.isPlainObject = require("./is_plain_object"); +lib.isArray = require("./is_array"); +lib.mod = require("./mod"); -var coerceModule = require('./coerce'); +var coerceModule = require("./coerce"); lib.valObjects = coerceModule.valObjects; lib.coerce = coerceModule.coerce; lib.coerce2 = coerceModule.coerce2; lib.coerceFont = coerceModule.coerceFont; lib.validate = coerceModule.validate; -var datesModule = require('./dates'); +var datesModule = require("./dates"); lib.dateTime2ms = datesModule.dateTime2ms; lib.isDateTime = datesModule.isDateTime; lib.ms2DateTime = datesModule.ms2DateTime; @@ -40,14 +37,14 @@ lib.findExactDates = datesModule.findExactDates; lib.MIN_MS = datesModule.MIN_MS; lib.MAX_MS = datesModule.MAX_MS; -var searchModule = require('./search'); +var searchModule = require("./search"); lib.findBin = searchModule.findBin; lib.sorterAsc = searchModule.sorterAsc; lib.sorterDes = searchModule.sorterDes; lib.distinctVals = searchModule.distinctVals; lib.roundUp = searchModule.roundUp; -var statsModule = require('./stats'); +var statsModule = require("./stats"); lib.aggNums = statsModule.aggNums; lib.len = statsModule.len; lib.mean = statsModule.mean; @@ -55,7 +52,7 @@ lib.variance = statsModule.variance; lib.stdev = statsModule.stdev; lib.interp = statsModule.interp; -var matrixModule = require('./matrix'); +var matrixModule = require("./matrix"); lib.init2dArray = matrixModule.init2dArray; lib.transposeRagged = matrixModule.transposeRagged; lib.dot = matrixModule.dot; @@ -65,24 +62,23 @@ lib.rotationXYMatrix = matrixModule.rotationXYMatrix; lib.apply2DTransform = matrixModule.apply2DTransform; lib.apply2DTransform2 = matrixModule.apply2DTransform2; -var extendModule = require('./extend'); +var extendModule = require("./extend"); lib.extendFlat = extendModule.extendFlat; lib.extendDeep = extendModule.extendDeep; lib.extendDeepAll = extendModule.extendDeepAll; lib.extendDeepNoArrays = extendModule.extendDeepNoArrays; -var loggersModule = require('./loggers'); +var loggersModule = require("./loggers"); lib.log = loggersModule.log; lib.warn = loggersModule.warn; lib.error = loggersModule.error; -lib.notifier = require('./notifier'); +lib.notifier = require("./notifier"); -lib.filterUnique = require('./filter_unique'); -lib.filterVisible = require('./filter_visible'); +lib.filterUnique = require("./filter_unique"); +lib.filterVisible = require("./filter_visible"); - -lib.cleanNumber = require('./clean_number'); +lib.cleanNumber = require("./clean_number"); /** * swap x and y of the same attribute in container cont @@ -90,16 +86,16 @@ lib.cleanNumber = require('./clean_number'); * you can also swap other things than x/y by providing part1 and part2 */ lib.swapAttrs = function(cont, attrList, part1, part2) { - if(!part1) part1 = 'x'; - if(!part2) part2 = 'y'; - for(var i = 0; i < attrList.length; i++) { - var attr = attrList[i], - xp = lib.nestedProperty(cont, attr.replace('?', part1)), - yp = lib.nestedProperty(cont, attr.replace('?', part2)), - temp = xp.get(); - xp.set(yp.get()); - yp.set(temp); - } + if (!part1) part1 = "x"; + if (!part2) part2 = "y"; + for (var i = 0; i < attrList.length; i++) { + var attr = attrList[i], + xp = lib.nestedProperty(cont, attr.replace("?", part1)), + yp = lib.nestedProperty(cont, attr.replace("?", part2)), + temp = xp.get(); + xp.set(yp.get()); + yp.set(temp); + } }; /** @@ -110,16 +106,16 @@ lib.swapAttrs = function(cont, attrList, part1, part2) { * return pauseEvent(e); */ lib.pauseEvent = function(e) { - if(e.stopPropagation) e.stopPropagation(); - if(e.preventDefault) e.preventDefault(); - e.cancelBubble = true; - return false; + if (e.stopPropagation) e.stopPropagation(); + if (e.preventDefault) e.preventDefault(); + e.cancelBubble = true; + return false; }; // constrain - restrict a number v to be between v0 and v1 lib.constrain = function(v, v0, v1) { - if(v0 > v1) return Math.max(v1, Math.min(v0, v)); - return Math.max(v0, Math.min(v1, v)); + if (v0 > v1) return Math.max(v1, Math.min(v0, v)); + return Math.max(v0, Math.min(v1, v)); }; /** @@ -128,15 +124,17 @@ lib.constrain = function(v, v0, v1) { * takes optional padding pixels */ lib.bBoxIntersect = function(a, b, pad) { - pad = pad || 0; - return (a.left <= b.right + pad && - b.left <= a.right + pad && - a.top <= b.bottom + pad && - b.top <= a.bottom + pad); + pad = pad || 0; + return a.left <= b.right + pad && + b.left <= a.right + pad && + a.top <= b.bottom + pad && + b.top <= a.bottom + pad; }; // minor convenience/performance booster for d3... -lib.identity = function(d) { return d; }; +lib.identity = function(d) { + return d; +}; // minor convenience helper lib.noop = function() {}; @@ -151,55 +149,55 @@ lib.noop = function() {}; * x1, x2: optional extra args */ lib.simpleMap = function(array, func, x1, x2) { - var len = array.length, - out = new Array(len); - for(var i = 0; i < len; i++) out[i] = func(array[i], x1, x2); - return out; + var len = array.length, out = new Array(len); + for (var i = 0; i < len; i++) { + out[i] = func(array[i], x1, x2); + } + return out; }; // random string generator lib.randstr = function randstr(existing, bits, base) { - /* + /* * Include number of bits, the base of the string you want * and an optional array of existing strings to avoid. */ - if(!base) base = 16; - if(bits === undefined) bits = 24; - if(bits <= 0) return '0'; - - var digits = Math.log(Math.pow(2, bits)) / Math.log(base), - res = '', - i, - b, - x; - - for(i = 2; digits === Infinity; i *= 2) { - digits = Math.log(Math.pow(2, bits / i)) / Math.log(base) * i; - } - - var rem = digits - Math.floor(digits); - - for(i = 0; i < Math.floor(digits); i++) { - x = Math.floor(Math.random() * base).toString(base); - res = x + res; - } - - if(rem) { - b = Math.pow(base, rem); - x = Math.floor(Math.random() * b).toString(base); - res = x + res; - } - - var parsed = parseInt(res, base); - if((existing && (existing.indexOf(res) > -1)) || - (parsed !== Infinity && parsed >= Math.pow(2, bits))) { - return randstr(existing, bits, base); - } - else return res; + if (!base) base = 16; + if (bits === undefined) bits = 24; + if (bits <= 0) return "0"; + + var digits = Math.log(Math.pow(2, bits)) / Math.log(base), res = "", i, b, x; + + for (i = 2; digits === Infinity; i *= 2) { + digits = Math.log(Math.pow(2, bits / i)) / Math.log(base) * i; + } + + var rem = digits - Math.floor(digits); + + for (i = 0; i < Math.floor(digits); i++) { + x = Math.floor(Math.random() * base).toString(base); + res = x + res; + } + + if (rem) { + b = Math.pow(base, rem); + x = Math.floor(Math.random() * b).toString(base); + res = x + res; + } + + var parsed = parseInt(res, base); + if ( + existing && existing.indexOf(res) > -1 || + parsed !== Infinity && parsed >= Math.pow(2, bits) + ) { + return randstr(existing, bits, base); + } else { + return res; + } }; lib.OptionControl = function(opt, optname) { - /* + /* * An environment to contain all option setters and * getters that collectively modify opts. * @@ -208,20 +206,20 @@ lib.OptionControl = function(opt, optname) { * * See FitOpts for example of usage */ - if(!opt) opt = {}; - if(!optname) optname = 'opt'; + if (!opt) opt = {}; + if (!optname) optname = "opt"; - var self = {}; - self.optionList = []; + var self = {}; + self.optionList = []; - self._newoption = function(optObj) { - optObj[optname] = opt; - self[optObj.name] = optObj; - self.optionList.push(optObj); - }; + self._newoption = function(optObj) { + optObj[optname] = opt; + self[optObj.name] = optObj; + self.optionList.push(optObj); + }; - self['_' + optname] = opt; - return self; + self["_" + optname] = opt; + return self; }; /** @@ -230,44 +228,45 @@ lib.OptionControl = function(opt, optname) { * bounce the ends in, so the output has the same length as the input */ lib.smooth = function(arrayIn, FWHM) { - FWHM = Math.round(FWHM) || 0; // only makes sense for integers - if(FWHM < 2) return arrayIn; - - var alen = arrayIn.length, - alen2 = 2 * alen, - wlen = 2 * FWHM - 1, - w = new Array(wlen), - arrayOut = new Array(alen), - i, - j, - k, - v; - - // first make the window array - for(i = 0; i < wlen; i++) { - w[i] = (1 - Math.cos(Math.PI * (i + 1) / FWHM)) / (2 * FWHM); - } - - // now do the convolution - for(i = 0; i < alen; i++) { - v = 0; - for(j = 0; j < wlen; j++) { - k = i + j + 1 - FWHM; - - // multibounce - if(k < -alen) k -= alen2 * Math.round(k / alen2); - else if(k >= alen2) k -= alen2 * Math.floor(k / alen2); - - // single bounce - if(k < 0) k = - 1 - k; - else if(k >= alen) k = alen2 - 1 - k; - - v += arrayIn[k] * w[j]; - } - arrayOut[i] = v; + FWHM = Math.round(FWHM) || 0; + // only makes sense for integers + if (FWHM < 2) return arrayIn; + + var alen = arrayIn.length, + alen2 = 2 * alen, + wlen = 2 * FWHM - 1, + w = new Array(wlen), + arrayOut = new Array(alen), + i, + j, + k, + v; + + // first make the window array + for (i = 0; i < wlen; i++) { + w[i] = (1 - Math.cos(Math.PI * (i + 1) / FWHM)) / (2 * FWHM); + } + + // now do the convolution + for (i = 0; i < alen; i++) { + v = 0; + for (j = 0; j < wlen; j++) { + k = i + j + 1 - FWHM; + + // multibounce + if (k < -alen) k -= alen2 * Math.round(k / alen2); + else if (k >= alen2) k -= alen2 * Math.floor(k / alen2); + + // single bounce + if (k < 0) k = -1 - k; + else if (k >= alen) k = alen2 - 1 - k; + + v += arrayIn[k] * w[j]; } + arrayOut[i] = v; + } - return arrayOut; + return arrayOut; }; /** @@ -282,59 +281,54 @@ lib.smooth = function(arrayIn, FWHM) { * that it gets reported */ lib.syncOrAsync = function(sequence, arg, finalStep) { - var ret, fni; + var ret, fni; - function continueAsync() { - return lib.syncOrAsync(sequence, arg, finalStep); - } + function continueAsync() { + return lib.syncOrAsync(sequence, arg, finalStep); + } - while(sequence.length) { - fni = sequence.splice(0, 1)[0]; - ret = fni(arg); + while (sequence.length) { + fni = sequence.splice(0, 1)[0]; + ret = fni(arg); - if(ret && ret.then) { - return ret.then(continueAsync) - .then(undefined, lib.promiseError); - } + if (ret && ret.then) { + return ret.then(continueAsync).then(undefined, lib.promiseError); } + } - return finalStep && finalStep(arg); + return finalStep && finalStep(arg); }; - /** * Helper to strip trailing slash, from * http://stackoverflow.com/questions/6680825/return-string-without-trailing-slash */ lib.stripTrailingSlash = function(str) { - if(str.substr(-1) === '/') return str.substr(0, str.length - 1); - return str; + if (str.substr(-1) === "/") return str.substr(0, str.length - 1); + return str; }; lib.noneOrAll = function(containerIn, containerOut, attrList) { - /** + /** * some attributes come together, so if you have one of them * in the input, you should copy the default values of the others * to the input as well. */ - if(!containerIn) return; + if (!containerIn) return; - var hasAny = false, - hasAll = true, - i, - val; + var hasAny = false, hasAll = true, i, val; - for(i = 0; i < attrList.length; i++) { - val = containerIn[attrList[i]]; - if(val !== undefined && val !== null) hasAny = true; - else hasAll = false; - } + for (i = 0; i < attrList.length; i++) { + val = containerIn[attrList[i]]; + if (val !== undefined && val !== null) hasAny = true; + else hasAll = false; + } - if(hasAny && !hasAll) { - for(i = 0; i < attrList.length; i++) { - containerIn[attrList[i]] = containerOut[attrList[i]]; - } + if (hasAny && !hasAll) { + for (i = 0; i < attrList.length; i++) { + containerIn[attrList[i]] = containerOut[attrList[i]]; } + } }; /** @@ -349,16 +343,18 @@ lib.noneOrAll = function(containerIn, containerOut, attrList) { * */ lib.pushUnique = function(array, item) { - if(item && array.indexOf(item) === -1) array.push(item); + if (item && array.indexOf(item) === -1) array.push(item); - return array; + return array; }; lib.mergeArray = function(traceAttr, cd, cdAttr) { - if(Array.isArray(traceAttr)) { - var imax = Math.min(traceAttr.length, cd.length); - for(var i = 0; i < imax; i++) cd[i][cdAttr] = traceAttr[i]; + if (Array.isArray(traceAttr)) { + var imax = Math.min(traceAttr.length, cd.length); + for (var i = 0; i < imax; i++) { + cd[i][cdAttr] = traceAttr[i]; } + } }; /** @@ -368,63 +364,67 @@ lib.mergeArray = function(traceAttr, cd, cdAttr) { * obj2 is assumed to already be clean of these things (including no arrays) */ lib.minExtend = function(obj1, obj2) { - var objOut = {}; - if(typeof obj2 !== 'object') obj2 = {}; - var arrayLen = 3, - keys = Object.keys(obj1), - i, - k, - v; - for(i = 0; i < keys.length; i++) { - k = keys[i]; - v = obj1[k]; - if(k.charAt(0) === '_' || typeof v === 'function') continue; - else if(k === 'module') objOut[k] = v; - else if(Array.isArray(v)) objOut[k] = v.slice(0, arrayLen); - else if(v && (typeof v === 'object')) objOut[k] = lib.minExtend(obj1[k], obj2[k]); - else objOut[k] = v; + var objOut = {}; + if (typeof obj2 !== "object") obj2 = {}; + var arrayLen = 3, keys = Object.keys(obj1), i, k, v; + for (i = 0; i < keys.length; i++) { + k = keys[i]; + v = obj1[k]; + if (k.charAt(0) === "_" || typeof v === "function") { + continue; + } else if (k === "module") { + objOut[k] = v; + } else if (Array.isArray(v)) { + objOut[k] = v.slice(0, arrayLen); + } else if (v && typeof v === "object") { + objOut[k] = lib.minExtend(obj1[k], obj2[k]); + } else { + objOut[k] = v; } - - keys = Object.keys(obj2); - for(i = 0; i < keys.length; i++) { - k = keys[i]; - v = obj2[k]; - if(typeof v !== 'object' || !(k in objOut) || typeof objOut[k] !== 'object') { - objOut[k] = v; - } + } + + keys = Object.keys(obj2); + for (i = 0; i < keys.length; i++) { + k = keys[i]; + v = obj2[k]; + if ( + typeof v !== "object" || !(k in objOut) || typeof objOut[k] !== "object" + ) { + objOut[k] = v; } + } - return objOut; + return objOut; }; lib.titleCase = function(s) { - return s.charAt(0).toUpperCase() + s.substr(1); + return s.charAt(0).toUpperCase() + s.substr(1); }; lib.containsAny = function(s, fragments) { - for(var i = 0; i < fragments.length; i++) { - if(s.indexOf(fragments[i]) !== -1) return true; - } - return false; + for (var i = 0; i < fragments.length; i++) { + if (s.indexOf(fragments[i]) !== -1) return true; + } + return false; }; // get the parent Plotly plot of any element. Whoo jquery-free tree climbing! lib.getPlotDiv = function(el) { - for(; el && el.removeAttribute; el = el.parentNode) { - if(lib.isPlotDiv(el)) return el; - } + for (; el && el.removeAttribute; el = el.parentNode) { + if (lib.isPlotDiv(el)) return el; + } }; lib.isPlotDiv = function(el) { - var el3 = d3.select(el); - return el3.node() instanceof HTMLElement && - el3.size() && - el3.classed('js-plotly-plot'); + var el3 = d3.select(el); + return el3.node() instanceof HTMLElement && + el3.size() && + el3.classed("js-plotly-plot"); }; lib.removeElement = function(el) { - var elParent = el && el.parentNode; - if(elParent) elParent.removeChild(el); + var elParent = el && el.parentNode; + if (elParent) elParent.removeChild(el); }; /** @@ -433,26 +433,26 @@ lib.removeElement = function(el) { * by all calls to this function */ lib.addStyleRule = function(selector, styleString) { - if(!lib.styleSheet) { - var style = document.createElement('style'); - // WebKit hack :( - style.appendChild(document.createTextNode('')); - document.head.appendChild(style); - lib.styleSheet = style.sheet; - } - var styleSheet = lib.styleSheet; - - if(styleSheet.insertRule) { - styleSheet.insertRule(selector + '{' + styleString + '}', 0); - } - else if(styleSheet.addRule) { - styleSheet.addRule(selector, styleString, 0); - } - else lib.warn('addStyleRule failed'); + if (!lib.styleSheet) { + var style = document.createElement("style"); + // WebKit hack :( + style.appendChild(document.createTextNode("")); + document.head.appendChild(style); + lib.styleSheet = style.sheet; + } + var styleSheet = lib.styleSheet; + + if (styleSheet.insertRule) { + styleSheet.insertRule(selector + "{" + styleString + "}", 0); + } else if (styleSheet.addRule) { + styleSheet.addRule(selector, styleString, 0); + } else { + lib.warn("addStyleRule failed"); + } }; lib.isIE = function() { - return typeof window.navigator.msSaveBlob !== 'undefined'; + return typeof window.navigator.msSaveBlob !== "undefined"; }; /** @@ -460,10 +460,9 @@ lib.isIE = function() { * because it doesn't handle instanceof like modern browsers */ lib.isD3Selection = function(obj) { - return obj && (typeof obj.classed === 'function'); + return obj && typeof obj.classed === "function"; }; - /** * Converts a string path to an object. * @@ -480,42 +479,39 @@ lib.isD3Selection = function(obj) { * @return {Object} the constructed object with a full nested path */ lib.objectFromPath = function(path, value) { - var keys = path.split('.'), - tmpObj, - obj = tmpObj = {}; + var keys = path.split("."), tmpObj, obj = tmpObj = {}; - for(var i = 0; i < keys.length; i++) { - var key = keys[i]; - var el = null; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var el = null; - var parts = keys[i].match(/(.*)\[([0-9]+)\]/); + var parts = keys[i].match(/(.*)\[([0-9]+)\]/); - if(parts) { - key = parts[1]; - el = parts[2]; + if (parts) { + key = parts[1]; + el = parts[2]; - tmpObj = tmpObj[key] = []; + tmpObj = tmpObj[key] = []; - if(i === keys.length - 1) { - tmpObj[el] = value; - } else { - tmpObj[el] = {}; - } + if (i === keys.length - 1) { + tmpObj[el] = value; + } else { + tmpObj[el] = {}; + } - tmpObj = tmpObj[el]; - } else { + tmpObj = tmpObj[el]; + } else { + if (i === keys.length - 1) { + tmpObj[key] = value; + } else { + tmpObj[key] = {}; + } - if(i === keys.length - 1) { - tmpObj[key] = value; - } else { - tmpObj[key] = {}; - } - - tmpObj = tmpObj[key]; - } + tmpObj = tmpObj[key]; } + } - return obj; + return obj; }; /** @@ -541,7 +537,6 @@ lib.objectFromPath = function(path, value) { * lib.expandObjectPaths({'marker[1].range[1]': 5, 'marker[1].range[0]': 4}) * => { marker: [null, {range: 4}] } */ - // Store this to avoid recompiling regex on *every* prop since this may happen many // many times for animations. Could maybe be inside the function. Not sure about // scoping vs. recompilation tradeoff, but at least it's not just inlining it into @@ -550,59 +545,65 @@ var dottedPropertyRegex = /^([^\[\.]+)\.(.+)?/; var indexedPropertyRegex = /^([^\.]+)\[([0-9]+)\](\.)?(.+)?/; lib.expandObjectPaths = function(data) { - var match, key, prop, datum, idx, dest, trailingPath; - if(typeof data === 'object' && !Array.isArray(data)) { - for(key in data) { - if(data.hasOwnProperty(key)) { - if((match = key.match(dottedPropertyRegex))) { - datum = data[key]; - prop = match[1]; - - delete data[key]; - - data[prop] = lib.extendDeepNoArrays(data[prop] || {}, lib.objectFromPath(key, lib.expandObjectPaths(datum))[prop]); - } else if((match = key.match(indexedPropertyRegex))) { - datum = data[key]; - - prop = match[1]; - idx = parseInt(match[2]); - - delete data[key]; - - data[prop] = data[prop] || []; - - if(match[3] === '.') { - // This is the case where theere are subsequent properties into which - // we must recurse, e.g. transforms[0].value - trailingPath = match[4]; - dest = data[prop][idx] = data[prop][idx] || {}; - - // NB: Extend deep no arrays prevents this from working on multiple - // nested properties in the same object, e.g. - // - // { - // foo[0].bar[1].range - // foo[0].bar[0].range - // } - // - // In this case, the extendDeepNoArrays will overwrite one array with - // the other, so that both properties *will not* be present in the - // result. Fixing this would require a more intelligent tracking - // of changes and merging than extendDeepNoArrays currently accomplishes. - lib.extendDeepNoArrays(dest, lib.objectFromPath(trailingPath, lib.expandObjectPaths(datum))); - } else { - // This is the case where this property is the end of the line, - // e.g. xaxis.range[0] - data[prop][idx] = lib.expandObjectPaths(datum); - } - } else { - data[key] = lib.expandObjectPaths(data[key]); - } - } + var match, key, prop, datum, idx, dest, trailingPath; + if (typeof data === "object" && !Array.isArray(data)) { + for (key in data) { + if (data.hasOwnProperty(key)) { + if (match = key.match(dottedPropertyRegex)) { + datum = data[key]; + prop = match[1]; + + delete data[key]; + + data[prop] = lib.extendDeepNoArrays( + data[prop] || {}, + lib.objectFromPath(key, lib.expandObjectPaths(datum))[prop] + ); + } else if (match = key.match(indexedPropertyRegex)) { + datum = data[key]; + + prop = match[1]; + idx = parseInt(match[2]); + + delete data[key]; + + data[prop] = data[prop] || []; + + if (match[3] === ".") { + // This is the case where theere are subsequent properties into which + // we must recurse, e.g. transforms[0].value + trailingPath = match[4]; + dest = data[prop][idx] = data[prop][idx] || {}; + + // NB: Extend deep no arrays prevents this from working on multiple + // nested properties in the same object, e.g. + // + // { + // foo[0].bar[1].range + // foo[0].bar[0].range + // } + // + // In this case, the extendDeepNoArrays will overwrite one array with + // the other, so that both properties *will not* be present in the + // result. Fixing this would require a more intelligent tracking + // of changes and merging than extendDeepNoArrays currently accomplishes. + lib.extendDeepNoArrays( + dest, + lib.objectFromPath(trailingPath, lib.expandObjectPaths(datum)) + ); + } else { + // This is the case where this property is the end of the line, + // e.g. xaxis.range[0] + data[prop][idx] = lib.expandObjectPaths(datum); + } + } else { + data[key] = lib.expandObjectPaths(data[key]); } + } } + } - return data; + return data; }; /** @@ -627,30 +628,30 @@ lib.expandObjectPaths = function(data) { * @return {string} the value that has been separated */ lib.numSeparate = function(value, separators, separatethousands) { - if(!separatethousands) separatethousands = false; + if (!separatethousands) separatethousands = false; - if(typeof separators !== 'string' || separators.length === 0) { - throw new Error('Separator string required for formatting!'); - } + if (typeof separators !== "string" || separators.length === 0) { + throw new Error("Separator string required for formatting!"); + } - if(typeof value === 'number') { - value = String(value); - } + if (typeof value === "number") { + value = String(value); + } - var thousandsRe = /(\d+)(\d{3})/, - decimalSep = separators.charAt(0), - thouSep = separators.charAt(1); + var thousandsRe = /(\d+)(\d{3})/, + decimalSep = separators.charAt(0), + thouSep = separators.charAt(1); - var x = value.split('.'), - x1 = x[0], - x2 = x.length > 1 ? decimalSep + x[1] : ''; + var x = value.split("."), + x1 = x[0], + x2 = x.length > 1 ? decimalSep + x[1] : ""; - // Years are ignored for thousands separators - if(thouSep && (x.length > 1 || x1.length > 4 || separatethousands)) { - while(thousandsRe.test(x1)) { - x1 = x1.replace(thousandsRe, '$1' + thouSep + '$2'); - } + // Years are ignored for thousands separators + if (thouSep && (x.length > 1 || x1.length > 4 || separatethousands)) { + while (thousandsRe.test(x1)) { + x1 = x1.replace(thousandsRe, "$1" + thouSep + "$2"); } + } - return x1 + x2; + return x1 + x2; }; diff --git a/src/lib/is_array.js b/src/lib/is_array.js index cda78eeb627..c0b7362fd6a 100644 --- a/src/lib/is_array.js +++ b/src/lib/is_array.js @@ -5,18 +5,20 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; /** * Return true for arrays, whether they're untyped or not. */ // IE9 fallback -var ab = (typeof ArrayBuffer === 'undefined' || !ArrayBuffer.isView) ? - {isView: function() { return false; }} : - ArrayBuffer; +var ab = typeof ArrayBuffer === "undefined" || !ArrayBuffer.isView + ? { + isView: function() { + return false; + } + } + : ArrayBuffer; module.exports = function isArray(a) { - return Array.isArray(a) || ab.isView(a); + return Array.isArray(a) || ab.isView(a); }; diff --git a/src/lib/is_plain_object.js b/src/lib/is_plain_object.js index d114e022d2f..b22eee1faa2 100644 --- a/src/lib/is_plain_object.js +++ b/src/lib/is_plain_object.js @@ -5,23 +5,17 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; // more info: http://stackoverflow.com/questions/18531624/isplainobject-thing module.exports = function isPlainObject(obj) { + // We need to be a little less strict in the `imagetest` container because + // of how async image requests are handled. + // + // N.B. isPlainObject(new Constructor()) will return true in `imagetest` + if (window && window.process && window.process.versions) { + return Object.prototype.toString.call(obj) === "[object Object]"; + } - // We need to be a little less strict in the `imagetest` container because - // of how async image requests are handled. - // - // N.B. isPlainObject(new Constructor()) will return true in `imagetest` - if(window && window.process && window.process.versions) { - return Object.prototype.toString.call(obj) === '[object Object]'; - } - - return ( - Object.prototype.toString.call(obj) === '[object Object]' && - Object.getPrototypeOf(obj) === Object.prototype - ); + return Object.prototype.toString.call(obj) === "[object Object]" && + Object.getPrototypeOf(obj) === Object.prototype; }; diff --git a/src/lib/loggers.js b/src/lib/loggers.js index 428f053e000..ff51d598c98 100644 --- a/src/lib/loggers.js +++ b/src/lib/loggers.js @@ -5,12 +5,10 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; /* eslint-disable no-console */ -var config = require('../plot_api/plot_config'); +var config = require("../plot_api/plot_config"); var loggers = module.exports = {}; @@ -19,41 +17,40 @@ var loggers = module.exports = {}; * debugging tools * ------------------------------------------ */ - loggers.log = function() { - if(config.logging > 1) { - var messages = ['LOG:']; + if (config.logging > 1) { + var messages = ["LOG:"]; - for(var i = 0; i < arguments.length; i++) { - messages.push(arguments[i]); - } - - apply(console.trace || console.log, messages); + for (var i = 0; i < arguments.length; i++) { + messages.push(arguments[i]); } + + apply(console.trace || console.log, messages); + } }; loggers.warn = function() { - if(config.logging > 0) { - var messages = ['WARN:']; + if (config.logging > 0) { + var messages = ["WARN:"]; - for(var i = 0; i < arguments.length; i++) { - messages.push(arguments[i]); - } - - apply(console.trace || console.log, messages); + for (var i = 0; i < arguments.length; i++) { + messages.push(arguments[i]); } + + apply(console.trace || console.log, messages); + } }; loggers.error = function() { - if(config.logging > 0) { - var messages = ['ERROR:']; - - for(var i = 0; i < arguments.length; i++) { - messages.push(arguments[i]); - } + if (config.logging > 0) { + var messages = ["ERROR:"]; - apply(console.error, messages); + for (var i = 0; i < arguments.length; i++) { + messages.push(arguments[i]); } + + apply(console.error, messages); + } }; /* @@ -61,12 +58,11 @@ loggers.error = function() { * apply like other functions do */ function apply(f, args) { - if(f.apply) { - f.apply(f, args); - } - else { - for(var i = 0; i < args.length; i++) { - f(args[i]); - } + if (f.apply) { + f.apply(f, args); + } else { + for (var i = 0; i < args.length; i++) { + f(args[i]); } + } } diff --git a/src/lib/matrix.js b/src/lib/matrix.js index 2429195de05..84968de207c 100644 --- a/src/lib/matrix.js +++ b/src/lib/matrix.js @@ -5,15 +5,13 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; exports.init2dArray = function(rowLength, colLength) { - var array = new Array(rowLength); - for(var i = 0; i < rowLength; i++) array[i] = new Array(colLength); - return array; + var array = new Array(rowLength); + for (var i = 0; i < rowLength; i++) { + array[i] = new Array(colLength); + } + return array; }; /** @@ -22,87 +20,93 @@ exports.init2dArray = function(rowLength, colLength) { * transposing-a-2d-array-in-javascript */ exports.transposeRagged = function(z) { - var maxlen = 0, - zlen = z.length, - i, - j; - // Maximum row length: - for(i = 0; i < zlen; i++) maxlen = Math.max(maxlen, z[i].length); - - var t = new Array(maxlen); - for(i = 0; i < maxlen; i++) { - t[i] = new Array(zlen); - for(j = 0; j < zlen; j++) t[i][j] = z[j][i]; + var maxlen = 0, zlen = z.length, i, j; + // Maximum row length: + for (i = 0; i < zlen; i++) { + maxlen = Math.max(maxlen, z[i].length); + } + + var t = new Array(maxlen); + for (i = 0; i < maxlen; i++) { + t[i] = new Array(zlen); + for (j = 0; j < zlen; j++) { + t[i][j] = z[j][i]; } + } - return t; + return t; }; // our own dot function so that we don't need to include numeric exports.dot = function(x, y) { - if(!(x.length && y.length) || x.length !== y.length) return null; + if (!(x.length && y.length) || x.length !== y.length) return null; - var len = x.length, - out, - i; + var len = x.length, out, i; - if(x[0].length) { - // mat-vec or mat-mat - out = new Array(len); - for(i = 0; i < len; i++) out[i] = exports.dot(x[i], y); + if (x[0].length) { + // mat-vec or mat-mat + out = new Array(len); + for (i = 0; i < len; i++) { + out[i] = exports.dot(x[i], y); } - else if(y[0].length) { - // vec-mat - var yTranspose = exports.transposeRagged(y); - out = new Array(yTranspose.length); - for(i = 0; i < yTranspose.length; i++) out[i] = exports.dot(x, yTranspose[i]); + } else if (y[0].length) { + // vec-mat + var yTranspose = exports.transposeRagged(y); + out = new Array(yTranspose.length); + for (i = 0; i < yTranspose.length; i++) { + out[i] = exports.dot(x, yTranspose[i]); } - else { - // vec-vec - out = 0; - for(i = 0; i < len; i++) out += x[i] * y[i]; + } else { + // vec-vec + out = 0; + for (i = 0; i < len; i++) { + out += x[i] * y[i]; } + } - return out; + return out; }; // translate by (x,y) exports.translationMatrix = function(x, y) { - return [[1, 0, x], [0, 1, y], [0, 0, 1]]; + return [[1, 0, x], [0, 1, y], [0, 0, 1]]; }; // rotate by alpha around (0,0) exports.rotationMatrix = function(alpha) { - var a = alpha * Math.PI / 180; - return [[Math.cos(a), -Math.sin(a), 0], - [Math.sin(a), Math.cos(a), 0], - [0, 0, 1]]; + var a = alpha * Math.PI / 180; + return [ + [Math.cos(a), -Math.sin(a), 0], + [Math.sin(a), Math.cos(a), 0], + [0, 0, 1] + ]; }; // rotate by alpha around (x,y) exports.rotationXYMatrix = function(a, x, y) { - return exports.dot( - exports.dot(exports.translationMatrix(x, y), - exports.rotationMatrix(a)), - exports.translationMatrix(-x, -y)); + return exports.dot( + exports.dot(exports.translationMatrix(x, y), exports.rotationMatrix(a)), + exports.translationMatrix(-x, -y) + ); }; // applies a 2D transformation matrix to either x and y params or an [x,y] array exports.apply2DTransform = function(transform) { - return function() { - var args = arguments; - if(args.length === 3) { - args = args[0]; - }// from map - var xy = arguments.length === 1 ? args[0] : [args[0], args[1]]; - return exports.dot(transform, [xy[0], xy[1], 1]).slice(0, 2); - }; + return function() { + var args = arguments; + if (args.length === 3) { + args = args[0]; + } + // from map + var xy = arguments.length === 1 ? args[0] : [args[0], args[1]]; + return exports.dot(transform, [xy[0], xy[1], 1]).slice(0, 2); + }; }; // applies a 2D transformation matrix to an [x1,y1,x2,y2] array (to transform a segment) exports.apply2DTransform2 = function(transform) { - var at = exports.apply2DTransform(transform); - return function(xys) { - return at(xys.slice(0, 2)).concat(at(xys.slice(2, 4))); - }; + var at = exports.apply2DTransform(transform); + return function(xys) { + return at(xys.slice(0, 2)).concat(at(xys.slice(2, 4))); + }; }; diff --git a/src/lib/mod.js b/src/lib/mod.js index 6ddf24e2563..c3951c6d6d1 100644 --- a/src/lib/mod.js +++ b/src/lib/mod.js @@ -5,14 +5,12 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; /** * sanitized modulus function that always returns in the range [0, d) * rather than (-d, 0] if v is negative */ module.exports = function mod(v, d) { - var out = v % d; - return out < 0 ? out + d : out; + var out = v % d; + return out < 0 ? out + d : out; }; diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js index a00cd17137a..b630cad00df 100644 --- a/src/lib/nested_property.js +++ b/src/lib/nested_property.js @@ -5,12 +5,9 @@ * 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 isNumeric = require('fast-isnumeric'); -var isArray = require('./is_array'); +"use strict"; +var isNumeric = require("fast-isnumeric"); +var isArray = require("./is_array"); /** * convert a string s (such as 'xaxis.range[0]') @@ -27,89 +24,85 @@ var isArray = require('./is_array'); * but you can do nestedProperty(obj, 'arr').set([5, 5, 5]) */ module.exports = function nestedProperty(container, propStr) { - if(isNumeric(propStr)) propStr = String(propStr); - else if(typeof propStr !== 'string' || - propStr.substr(propStr.length - 4) === '[-1]') { - throw 'bad property string'; - } - - var j = 0, - propParts = propStr.split('.'), - indexed, - indices, - i; - - // check for parts of the nesting hierarchy that are numbers (ie array elements) - while(j < propParts.length) { - // look for non-bracket chars, then any number of [##] blocks - indexed = String(propParts[j]).match(/^([^\[\]]*)((\[\-?[0-9]*\])+)$/); - if(indexed) { - if(indexed[1]) propParts[j] = indexed[1]; - // allow propStr to start with bracketed array indices - else if(j === 0) propParts.splice(0, 1); - else throw 'bad property string'; - - indices = indexed[2] - .substr(1, indexed[2].length - 2) - .split(']['); - - for(i = 0; i < indices.length; i++) { - j++; - propParts.splice(j, 0, Number(indices[i])); - } - } + if (isNumeric(propStr)) { + propStr = String(propStr); + } else if ( + typeof propStr !== "string" || propStr.substr(propStr.length - 4) === "[-1]" + ) { + throw "bad property string"; + } + + var j = 0, propParts = propStr.split("."), indexed, indices, i; + + // check for parts of the nesting hierarchy that are numbers (ie array elements) + while (j < propParts.length) { + // look for non-bracket chars, then any number of [##] blocks + indexed = String(propParts[j]).match(/^([^\[\]]*)((\[\-?[0-9]*\])+)$/); + if (indexed) { + if (indexed[1]) { + propParts[j] = indexed[1]; + } else if ( + j === 0 // allow propStr to start with bracketed array indices + ) { + propParts.splice(0, 1); + } else { + throw "bad property string"; + } + + indices = indexed[2].substr(1, indexed[2].length - 2).split("]["); + + for (i = 0; i < indices.length; i++) { j++; + propParts.splice(j, 0, Number(indices[i])); + } } - - if(typeof container !== 'object') { - return badContainer(container, propStr, propParts); - } - - return { - set: npSet(container, propParts), - get: npGet(container, propParts), - astr: propStr, - parts: propParts, - obj: container - }; + j++; + } + + if (typeof container !== "object") { + return badContainer(container, propStr, propParts); + } + + return { + set: npSet(container, propParts), + get: npGet(container, propParts), + astr: propStr, + parts: propParts, + obj: container + }; }; function npGet(cont, parts) { - return function() { - var curCont = cont, - curPart, - allSame, - out, - i, - j; - - for(i = 0; i < parts.length - 1; i++) { - curPart = parts[i]; - if(curPart === -1) { - allSame = true; - out = []; - for(j = 0; j < curCont.length; j++) { - out[j] = npGet(curCont[j], parts.slice(i + 1))(); - if(out[j] !== out[0]) allSame = false; - } - return allSame ? out[0] : out; - } - if(typeof curPart === 'number' && !isArray(curCont)) { - return undefined; - } - curCont = curCont[curPart]; - if(typeof curCont !== 'object' || curCont === null) { - return undefined; - } + return function() { + var curCont = cont, curPart, allSame, out, i, j; + + for (i = 0; i < parts.length - 1; i++) { + curPart = parts[i]; + if (curPart === -1) { + allSame = true; + out = []; + for (j = 0; j < curCont.length; j++) { + out[j] = npGet(curCont[j], parts.slice(i + 1))(); + if (out[j] !== out[0]) allSame = false; } + return allSame ? out[0] : out; + } + if (typeof curPart === "number" && !isArray(curCont)) { + return undefined; + } + curCont = curCont[curPart]; + if (typeof curCont !== "object" || curCont === null) { + return undefined; + } + } - // only hit this if parts.length === 1 - if(typeof curCont !== 'object' || curCont === null) return undefined; + // only hit this if parts.length === 1 + if (typeof curCont !== "object" || curCont === null) return undefined; - out = curCont[parts[i]]; - if(out === null) return undefined; - return out; - }; + out = curCont[parts[i]]; + if (out === null) return undefined; + return out; + }; } /* @@ -119,77 +112,77 @@ function npGet(cont, parts) { * AND the replacement value is an array. */ function isDataArray(val, key) { + var containers = ["annotations", "shapes", "range", "domain", "buttons"], + isNotAContainer = containers.indexOf(key) === -1; - var containers = ['annotations', 'shapes', 'range', 'domain', 'buttons'], - isNotAContainer = containers.indexOf(key) === -1; - - return isArray(val) && isNotAContainer; + return isArray(val) && isNotAContainer; } function npSet(cont, parts) { - return function(val) { - var curCont = cont, - containerLevels = [cont], - toDelete = emptyObj(val) && !isDataArray(val, parts[parts.length - 1]), - curPart, - i; - - for(i = 0; i < parts.length - 1; i++) { - curPart = parts[i]; - - if(typeof curPart === 'number' && !isArray(curCont)) { - throw 'array index but container is not an array'; - } - - // handle special -1 array index - if(curPart === -1) { - toDelete = !setArrayAll(curCont, parts.slice(i + 1), val); - if(toDelete) break; - else return; - } - - if(!checkNewContainer(curCont, curPart, parts[i + 1], toDelete)) { - break; - } - - curCont = curCont[curPart]; - - if(typeof curCont !== 'object' || curCont === null) { - throw 'container is not an object'; - } - - containerLevels.push(curCont); - } + return function(val) { + var curCont = cont, + containerLevels = [cont], + toDelete = emptyObj(val) && !isDataArray(val, parts[parts.length - 1]), + curPart, + i; + + for (i = 0; i < parts.length - 1; i++) { + curPart = parts[i]; + + if (typeof curPart === "number" && !isArray(curCont)) { + throw "array index but container is not an array"; + } + + // handle special -1 array index + if (curPart === -1) { + toDelete = !setArrayAll(curCont, parts.slice(i + 1), val); + if (toDelete) break; + else return; + } + + if (!checkNewContainer(curCont, curPart, parts[i + 1], toDelete)) { + break; + } + + curCont = curCont[curPart]; + + if (typeof curCont !== "object" || curCont === null) { + throw "container is not an object"; + } + + containerLevels.push(curCont); + } - if(toDelete) { - if(i === parts.length - 1) delete curCont[parts[i]]; - pruneContainers(containerLevels); - } - else curCont[parts[i]] = val; - }; + if (toDelete) { + if (i === parts.length - 1) delete curCont[parts[i]]; + pruneContainers(containerLevels); + } else { + curCont[parts[i]] = val; + } + }; } // handle special -1 array index function setArrayAll(containerArray, innerParts, val) { - var arrayVal = isArray(val), - allSet = true, - thisVal = val, - deleteThis = arrayVal ? false : emptyObj(val), - firstPart = innerParts[0], - i; - - for(i = 0; i < containerArray.length; i++) { - if(arrayVal) { - thisVal = val[i % val.length]; - deleteThis = emptyObj(thisVal); - } - if(deleteThis) allSet = false; - if(!checkNewContainer(containerArray, i, firstPart, deleteThis)) { - continue; - } - npSet(containerArray[i], innerParts)(thisVal); + var arrayVal = isArray(val), + allSet = true, + thisVal = val, + deleteThis = arrayVal ? false : emptyObj(val), + firstPart = innerParts[0], + i; + + for (i = 0; i < containerArray.length; i++) { + if (arrayVal) { + thisVal = val[i % val.length]; + deleteThis = emptyObj(thisVal); } - return allSet; + if (deleteThis) allSet = false; + if (!checkNewContainer(containerArray, i, firstPart, deleteThis)) { + continue; + } + npSet(containerArray[i], innerParts)(thisVal); + } + return allSet; } /** @@ -198,58 +191,63 @@ function setArrayAll(containerArray, innerParts, val) { * because we're only deleting an attribute */ function checkNewContainer(container, part, nextPart, toDelete) { - if(container[part] === undefined) { - if(toDelete) return false; + if (container[part] === undefined) { + if (toDelete) return false; - if(typeof nextPart === 'number') container[part] = []; - else container[part] = {}; - } - return true; + if (typeof nextPart === "number") container[part] = []; + else container[part] = {}; + } + return true; } function pruneContainers(containerLevels) { - var i, - j, - curCont, - keys, - remainingKeys; - for(i = containerLevels.length - 1; i >= 0; i--) { - curCont = containerLevels[i]; - remainingKeys = false; - if(isArray(curCont)) { - for(j = curCont.length - 1; j >= 0; j--) { - if(emptyObj(curCont[j])) { - if(remainingKeys) curCont[j] = undefined; - else curCont.pop(); - } - else remainingKeys = true; - } + var i, j, curCont, keys, remainingKeys; + for (i = containerLevels.length - 1; i >= 0; i--) { + curCont = containerLevels[i]; + remainingKeys = false; + if (isArray(curCont)) { + for (j = curCont.length - 1; j >= 0; j--) { + if (emptyObj(curCont[j])) { + if (remainingKeys) curCont[j] = undefined; + else curCont.pop(); + } else { + remainingKeys = true; } - else if(typeof curCont === 'object' && curCont !== null) { - keys = Object.keys(curCont); - remainingKeys = false; - for(j = keys.length - 1; j >= 0; j--) { - if(emptyObj(curCont[keys[j]]) && !isDataArray(curCont[keys[j]], keys[j])) delete curCont[keys[j]]; - else remainingKeys = true; - } + } + } else if (typeof curCont === "object" && curCont !== null) { + keys = Object.keys(curCont); + remainingKeys = false; + for (j = keys.length - 1; j >= 0; j--) { + if ( + emptyObj(curCont[keys[j]]) && !isDataArray(curCont[keys[j]], keys[j]) + ) { + delete curCont[keys[j]]; + } else { + remainingKeys = true; } - if(remainingKeys) return; + } } + if (remainingKeys) return; + } } function emptyObj(obj) { - if(obj === undefined || obj === null) return true; - if(typeof obj !== 'object') return false; // any plain value - if(isArray(obj)) return !obj.length; // [] - return !Object.keys(obj).length; // {} + if (obj === undefined || obj === null) return true; + if (typeof obj !== "object") return false; + // any plain value + if (isArray(obj)) return !obj.length; + // [] + return !Object.keys(obj).length; // {} } function badContainer(container, propStr, propParts) { - return { - set: function() { throw 'bad container'; }, - get: function() {}, - astr: propStr, - parts: propParts, - obj: container - }; + return { + set: function() { + throw "bad container"; + }, + get: function() {}, + astr: propStr, + parts: propParts, + obj: container + }; } diff --git a/src/lib/notifier.js b/src/lib/notifier.js index e7443afd7a0..25187c6ec36 100644 --- a/src/lib/notifier.js +++ b/src/lib/notifier.js @@ -5,12 +5,9 @@ * 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 d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); +"use strict"; +var d3 = require("d3"); +var isNumeric = require("fast-isnumeric"); var NOTEDATA = []; @@ -22,59 +19,62 @@ var NOTEDATA = []; * @return {undefined} this function does not return a value */ module.exports = function(text, displayLength) { - if(NOTEDATA.indexOf(text) !== -1) return; + if (NOTEDATA.indexOf(text) !== -1) return; - NOTEDATA.push(text); + NOTEDATA.push(text); - var ts = 1000; - if(isNumeric(displayLength)) ts = displayLength; - else if(displayLength === 'long') ts = 3000; + var ts = 1000; + if (isNumeric(displayLength)) ts = displayLength; + else if (displayLength === "long") ts = 3000; - var notifierContainer = d3.select('body') - .selectAll('.plotly-notifier') - .data([0]); - notifierContainer.enter() - .append('div') - .classed('plotly-notifier', true); + var notifierContainer = d3 + .select("body") + .selectAll(".plotly-notifier") + .data([0]); + notifierContainer.enter().append("div").classed("plotly-notifier", true); - var notes = notifierContainer.selectAll('.notifier-note').data(NOTEDATA); + var notes = notifierContainer.selectAll(".notifier-note").data(NOTEDATA); - function killNote(transition) { - transition - .duration(700) - .style('opacity', 0) - .each('end', function(thisText) { - var thisIndex = NOTEDATA.indexOf(thisText); - if(thisIndex !== -1) NOTEDATA.splice(thisIndex, 1); - d3.select(this).remove(); - }); - } + function killNote(transition) { + transition + .duration(700) + .style("opacity", 0) + .each("end", function(thisText) { + var thisIndex = NOTEDATA.indexOf(thisText); + if (thisIndex !== -1) NOTEDATA.splice(thisIndex, 1); + d3.select(this).remove(); + }); + } - notes.enter().append('div') - .classed('notifier-note', true) - .style('opacity', 0) - .each(function(thisText) { - var note = d3.select(this); + notes + .enter() + .append("div") + .classed("notifier-note", true) + .style("opacity", 0) + .each(function(thisText) { + var note = d3.select(this); - note.append('button') - .classed('notifier-close', true) - .html('×') - .on('click', function() { - note.transition().call(killNote); - }); + note + .append("button") + .classed("notifier-close", true) + .html("×") + .on("click", function() { + note.transition().call(killNote); + }); - var p = note.append('p'); - var lines = thisText.split(//g); - for(var i = 0; i < lines.length; i++) { - if(i) p.append('br'); - p.append('span').text(lines[i]); - } + var p = note.append("p"); + var lines = thisText.split(//g); + for (var i = 0; i < lines.length; i++) { + if (i) p.append("br"); + p.append("span").text(lines[i]); + } - note.transition() - .duration(700) - .style('opacity', 1) - .transition() - .delay(ts) - .call(killNote); - }); + note + .transition() + .duration(700) + .style("opacity", 1) + .transition() + .delay(ts) + .call(killNote); + }); }; diff --git a/src/lib/override_cursor.js b/src/lib/override_cursor.js index ebbd290951e..27d25df7ae9 100644 --- a/src/lib/override_cursor.js +++ b/src/lib/override_cursor.js @@ -5,14 +5,11 @@ * 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 setCursor = require("./setcursor"); - -'use strict'; - -var setCursor = require('./setcursor'); - -var STASHATTR = 'data-savedcursor'; -var NO_CURSOR = '!!'; +var STASHATTR = "data-savedcursor"; +var NO_CURSOR = "!!"; /* * works with our CSS cursor classes (see css/_cursor.scss) @@ -21,27 +18,25 @@ var NO_CURSOR = '!!'; * omit cursor to revert to the previously set value. */ module.exports = function overrideCursor(el3, csr) { - var savedCursor = el3.attr(STASHATTR); - if(csr) { - if(!savedCursor) { - var classes = (el3.attr('class') || '').split(' '); - for(var i = 0; i < classes.length; i++) { - var cls = classes[i]; - if(cls.indexOf('cursor-') === 0) { - el3.attr(STASHATTR, cls.substr(7)) - .classed(cls, false); - } - } - if(!el3.attr(STASHATTR)) { - el3.attr(STASHATTR, NO_CURSOR); - } + var savedCursor = el3.attr(STASHATTR); + if (csr) { + if (!savedCursor) { + var classes = (el3.attr("class") || "").split(" "); + for (var i = 0; i < classes.length; i++) { + var cls = classes[i]; + if (cls.indexOf("cursor-") === 0) { + el3.attr(STASHATTR, cls.substr(7)).classed(cls, false); } - setCursor(el3, csr); + } + if (!el3.attr(STASHATTR)) { + el3.attr(STASHATTR, NO_CURSOR); + } } - else if(savedCursor) { - el3.attr(STASHATTR, null); + setCursor(el3, csr); + } else if (savedCursor) { + el3.attr(STASHATTR, null); - if(savedCursor === NO_CURSOR) setCursor(el3); - else setCursor(el3, savedCursor); - } + if (savedCursor === NO_CURSOR) setCursor(el3); + else setCursor(el3, savedCursor); + } }; diff --git a/src/lib/polygon.js b/src/lib/polygon.js index befd593e275..59a94cd29ca 100644 --- a/src/lib/polygon.js +++ b/src/lib/polygon.js @@ -5,11 +5,8 @@ * 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 dot = require('./matrix').dot; +"use strict"; +var dot = require("./matrix").dot; var polygon = module.exports = {}; @@ -30,133 +27,139 @@ var polygon = module.exports = {}; * returns boolean: is pt inside the polygon (including on its edges) */ polygon.tester = function tester(ptsIn) { - var pts = ptsIn.slice(), - xmin = pts[0][0], - xmax = xmin, - ymin = pts[0][1], - ymax = ymin; - - pts.push(pts[0]); - for(var i = 1; i < pts.length; i++) { - xmin = Math.min(xmin, pts[i][0]); - xmax = Math.max(xmax, pts[i][0]); - ymin = Math.min(ymin, pts[i][1]); - ymax = Math.max(ymax, pts[i][1]); + var pts = ptsIn.slice(), + xmin = pts[0][0], + xmax = xmin, + ymin = pts[0][1], + ymax = ymin; + + pts.push(pts[0]); + for (var i = 1; i < pts.length; i++) { + xmin = Math.min(xmin, pts[i][0]); + xmax = Math.max(xmax, pts[i][0]); + ymin = Math.min(ymin, pts[i][1]); + ymax = Math.max(ymax, pts[i][1]); + } + + // do we have a rectangle? Handle this here, so we can use the same + // tester for the rectangular case without sacrificing speed + var isRect = false, rectFirstEdgeTest; + + if (pts.length === 5) { + if (pts[0][0] === pts[1][0]) { + // vert, horz, vert, horz + if ( + pts[2][0] === pts[3][0] && + pts[0][1] === pts[3][1] && + pts[1][1] === pts[2][1] + ) { + isRect = true; + rectFirstEdgeTest = function(pt) { + return pt[0] === pts[0][0]; + }; + } + } else if (pts[0][1] === pts[1][1]) { + // horz, vert, horz, vert + if ( + pts[2][1] === pts[3][1] && + pts[0][0] === pts[3][0] && + pts[1][0] === pts[2][0] + ) { + isRect = true; + rectFirstEdgeTest = function(pt) { + return pt[1] === pts[0][1]; + }; + } } + } - // do we have a rectangle? Handle this here, so we can use the same - // tester for the rectangular case without sacrificing speed + function rectContains(pt, omitFirstEdge) { + var x = pt[0], y = pt[1]; - var isRect = false, - rectFirstEdgeTest; - - if(pts.length === 5) { - if(pts[0][0] === pts[1][0]) { // vert, horz, vert, horz - if(pts[2][0] === pts[3][0] && - pts[0][1] === pts[3][1] && - pts[1][1] === pts[2][1]) { - isRect = true; - rectFirstEdgeTest = function(pt) { return pt[0] === pts[0][0]; }; - } - } - else if(pts[0][1] === pts[1][1]) { // horz, vert, horz, vert - if(pts[2][1] === pts[3][1] && - pts[0][0] === pts[3][0] && - pts[1][0] === pts[2][0]) { - isRect = true; - rectFirstEdgeTest = function(pt) { return pt[1] === pts[0][1]; }; - } - } + if (x < xmin || x > xmax || y < ymin || y > ymax) { + // pt is outside the bounding box of polygon + return false; } + if (omitFirstEdge && rectFirstEdgeTest(pt)) return false; - function rectContains(pt, omitFirstEdge) { - var x = pt[0], - y = pt[1]; + return true; + } - if(x < xmin || x > xmax || y < ymin || y > ymax) { - // pt is outside the bounding box of polygon - return false; - } - if(omitFirstEdge && rectFirstEdgeTest(pt)) return false; + function contains(pt, omitFirstEdge) { + var x = pt[0], y = pt[1]; - return true; + if (x < xmin || x > xmax || y < ymin || y > ymax) { + // pt is outside the bounding box of polygon + return false; } - function contains(pt, omitFirstEdge) { - var x = pt[0], - y = pt[1]; - - if(x < xmin || x > xmax || y < ymin || y > ymax) { - // pt is outside the bounding box of polygon - return false; + var imax = pts.length, + x1 = pts[0][0], + y1 = pts[0][1], + crossings = 0, + i, + x0, + y0, + xmini, + ycross; + + for (i = 1; i < imax; i++) { + // find all crossings of a vertical line upward from pt with + // polygon segments + // crossings exactly at xmax don't count, unless the point is + // exactly on the segment, then it counts as inside. + x0 = x1; + y0 = y1; + x1 = pts[i][0]; + y1 = pts[i][1]; + xmini = Math.min(x0, x1); + + // outside the bounding box of this segment, it's only a crossing + // if it's below the box. + if (x < xmini || x > Math.max(x0, x1) || y > Math.max(y0, y1)) { + continue; + } else if (y < Math.min(y0, y1)) { + // don't count the left-most point of the segment as a crossing + // because we don't want to double-count adjacent crossings + // UNLESS the polygon turns past vertical at exactly this x + // Note that this is repeated below, but we can't factor it out + // because + if (x !== xmini) crossings++; + } else { + // inside the bounding box, check the actual line intercept + // vertical segment - we know already that the point is exactly + // on the segment, so mark the crossing as exactly at the point. + if (x1 === x0) { + ycross = y; + } else { + // any other angle + ycross = y0 + (x - x0) * (y1 - y0) / (x1 - x0); } - var imax = pts.length, - x1 = pts[0][0], - y1 = pts[0][1], - crossings = 0, - i, - x0, - y0, - xmini, - ycross; - - for(i = 1; i < imax; i++) { - // find all crossings of a vertical line upward from pt with - // polygon segments - // crossings exactly at xmax don't count, unless the point is - // exactly on the segment, then it counts as inside. - x0 = x1; - y0 = y1; - x1 = pts[i][0]; - y1 = pts[i][1]; - xmini = Math.min(x0, x1); - - // outside the bounding box of this segment, it's only a crossing - // if it's below the box. - if(x < xmini || x > Math.max(x0, x1) || y > Math.max(y0, y1)) { - continue; - } - else if(y < Math.min(y0, y1)) { - // don't count the left-most point of the segment as a crossing - // because we don't want to double-count adjacent crossings - // UNLESS the polygon turns past vertical at exactly this x - // Note that this is repeated below, but we can't factor it out - // because - if(x !== xmini) crossings++; - } - // inside the bounding box, check the actual line intercept - else { - // vertical segment - we know already that the point is exactly - // on the segment, so mark the crossing as exactly at the point. - if(x1 === x0) ycross = y; - // any other angle - else ycross = y0 + (x - x0) * (y1 - y0) / (x1 - x0); - - // exactly on the edge: counts as inside the polygon, unless it's the - // first edge and we're omitting it. - if(y === ycross) { - if(i === 1 && omitFirstEdge) return false; - return true; - } - - if(y <= ycross && x !== xmini) crossings++; - } + // exactly on the edge: counts as inside the polygon, unless it's the + // first edge and we're omitting it. + if (y === ycross) { + if (i === 1 && omitFirstEdge) return false; + return true; } - // if we've gotten this far, odd crossings means inside, even is outside - return crossings % 2 === 1; + if (y <= ycross && x !== xmini) crossings++; + } } - return { - xmin: xmin, - xmax: xmax, - ymin: ymin, - ymax: ymax, - pts: pts, - contains: isRect ? rectContains : contains, - isRect: isRect - }; + // if we've gotten this far, odd crossings means inside, even is outside + return crossings % 2 === 1; + } + + return { + xmin: xmin, + xmax: xmax, + ymin: ymin, + ymax: ymax, + pts: pts, + contains: isRect ? rectContains : contains, + isRect: isRect + }; }; /** @@ -169,24 +172,34 @@ polygon.tester = function tester(ptsIn) { * before the line counts as bent * @returns boolean: true means this segment is bent, false means straight */ -var isBent = polygon.isSegmentBent = function isBent(pts, start, end, tolerance) { - var startPt = pts[start], - segment = [pts[end][0] - startPt[0], pts[end][1] - startPt[1]], - segmentSquared = dot(segment, segment), - segmentLen = Math.sqrt(segmentSquared), - unitPerp = [-segment[1] / segmentLen, segment[0] / segmentLen], - i, - part, - partParallel; - - for(i = start + 1; i < end; i++) { - part = [pts[i][0] - startPt[0], pts[i][1] - startPt[1]]; - partParallel = dot(part, segment); - - if(partParallel < 0 || partParallel > segmentSquared || - Math.abs(dot(part, unitPerp)) > tolerance) return true; +var isBent = polygon.isSegmentBent = function isBent( + pts, + start, + end, + tolerance +) { + var startPt = pts[start], + segment = [pts[end][0] - startPt[0], pts[end][1] - startPt[1]], + segmentSquared = dot(segment, segment), + segmentLen = Math.sqrt(segmentSquared), + unitPerp = [(-segment[1]) / segmentLen, segment[0] / segmentLen], + i, + part, + partParallel; + + for (i = start + 1; i < end; i++) { + part = [pts[i][0] - startPt[0], pts[i][1] - startPt[1]]; + partParallel = dot(part, segment); + + if ( + partParallel < 0 || + partParallel > segmentSquared || + Math.abs(dot(part, unitPerp)) > tolerance + ) { + return true; } - return false; + } + return false; }; /** @@ -203,36 +216,29 @@ var isBent = polygon.isSegmentBent = function isBent(pts, start, end, tolerance) * filtered is the resulting filtered Array of [x, y] pairs */ polygon.filter = function filter(pts, tolerance) { - var ptsFiltered = [pts[0]], - doneRawIndex = 0, - doneFilteredIndex = 0; - - function addPt(pt) { - pts.push(pt); - var prevFilterLen = ptsFiltered.length, - iLast = doneRawIndex; - ptsFiltered.splice(doneFilteredIndex + 1); - - for(var i = iLast + 1; i < pts.length; i++) { - if(i === pts.length - 1 || isBent(pts, iLast, i + 1, tolerance)) { - ptsFiltered.push(pts[i]); - if(ptsFiltered.length < prevFilterLen - 2) { - doneRawIndex = i; - doneFilteredIndex = ptsFiltered.length - 1; - } - iLast = i; - } + var ptsFiltered = [pts[0]], doneRawIndex = 0, doneFilteredIndex = 0; + + function addPt(pt) { + pts.push(pt); + var prevFilterLen = ptsFiltered.length, iLast = doneRawIndex; + ptsFiltered.splice(doneFilteredIndex + 1); + + for (var i = iLast + 1; i < pts.length; i++) { + if (i === pts.length - 1 || isBent(pts, iLast, i + 1, tolerance)) { + ptsFiltered.push(pts[i]); + if (ptsFiltered.length < prevFilterLen - 2) { + doneRawIndex = i; + doneFilteredIndex = ptsFiltered.length - 1; } + iLast = i; + } } + } - if(pts.length > 1) { - var lastPt = pts.pop(); - addPt(lastPt); - } + if (pts.length > 1) { + var lastPt = pts.pop(); + addPt(lastPt); + } - return { - addPt: addPt, - raw: pts, - filtered: ptsFiltered - }; + return { addPt: addPt, raw: pts, filtered: ptsFiltered }; }; diff --git a/src/lib/queue.js b/src/lib/queue.js index 815fd915723..e793887d047 100644 --- a/src/lib/queue.js +++ b/src/lib/queue.js @@ -5,13 +5,9 @@ * 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 config = require('../plot_api/plot_config'); - +"use strict"; +var Lib = require("../lib"); +var config = require("../plot_api/plot_config"); /** * Copy arg array *without* removing `undefined` values from objects. @@ -21,34 +17,32 @@ var config = require('../plot_api/plot_config'); * @returns {Array} */ function copyArgArray(gd, args) { - var copy = []; - var arg; - - for(var i = 0; i < args.length; i++) { - arg = args[i]; - - if(arg === gd) copy[i] = arg; - else if(typeof arg === 'object') { - copy[i] = Array.isArray(arg) ? - Lib.extendDeep([], arg) : - Lib.extendDeepAll({}, arg); - } - else copy[i] = arg; + var copy = []; + var arg; + + for (var i = 0; i < args.length; i++) { + arg = args[i]; + + if (arg === gd) { + copy[i] = arg; + } else if (typeof arg === "object") { + copy[i] = Array.isArray(arg) + ? Lib.extendDeep([], arg) + : Lib.extendDeepAll({}, arg); + } else { + copy[i] = arg; } + } - return copy; + return copy; } - // ----------------------------------------------------- // Undo/Redo queue for plots // ----------------------------------------------------- - - var queue = {}; // TODO: disable/enable undo and redo buttons appropriately - /** * Add an item to the undoQueue for a graphDiv * @@ -59,42 +53,45 @@ var queue = {}; * @param redoArgs Args to supply redoFunc with */ queue.add = function(gd, undoFunc, undoArgs, redoFunc, redoArgs) { - var queueObj, - queueIndex; - - // make sure we have the queue and our position in it - gd.undoQueue = gd.undoQueue || {index: 0, queue: [], sequence: false}; - queueIndex = gd.undoQueue.index; - - // if we're already playing an undo or redo, or if this is an auto operation - // (like pane resize... any others?) then we don't save this to the undo queue - if(gd.autoplay) { - if(!gd.undoQueue.inSequence) gd.autoplay = false; - return; - } - - // if we're not in a sequence or are just starting, we need a new queue item - if(!gd.undoQueue.sequence || gd.undoQueue.beginSequence) { - queueObj = {undo: {calls: [], args: []}, redo: {calls: [], args: []}}; - gd.undoQueue.queue.splice(queueIndex, gd.undoQueue.queue.length - queueIndex, queueObj); - gd.undoQueue.index += 1; - } else { - queueObj = gd.undoQueue.queue[queueIndex - 1]; - } - gd.undoQueue.beginSequence = false; - - // we unshift to handle calls for undo in a forward for loop later - if(queueObj) { - queueObj.undo.calls.unshift(undoFunc); - queueObj.undo.args.unshift(undoArgs); - queueObj.redo.calls.push(redoFunc); - queueObj.redo.args.push(redoArgs); - } - - if(gd.undoQueue.queue.length > config.queueLength) { - gd.undoQueue.queue.shift(); - gd.undoQueue.index--; - } + var queueObj, queueIndex; + + // make sure we have the queue and our position in it + gd.undoQueue = gd.undoQueue || { index: 0, queue: [], sequence: false }; + queueIndex = gd.undoQueue.index; + + // if we're already playing an undo or redo, or if this is an auto operation + // (like pane resize... any others?) then we don't save this to the undo queue + if (gd.autoplay) { + if (!gd.undoQueue.inSequence) gd.autoplay = false; + return; + } + + // if we're not in a sequence or are just starting, we need a new queue item + if (!gd.undoQueue.sequence || gd.undoQueue.beginSequence) { + queueObj = { undo: { calls: [], args: [] }, redo: { calls: [], args: [] } }; + gd.undoQueue.queue.splice( + queueIndex, + gd.undoQueue.queue.length - queueIndex, + queueObj + ); + gd.undoQueue.index += 1; + } else { + queueObj = gd.undoQueue.queue[queueIndex - 1]; + } + gd.undoQueue.beginSequence = false; + + // we unshift to handle calls for undo in a forward for loop later + if (queueObj) { + queueObj.undo.calls.unshift(undoFunc); + queueObj.undo.args.unshift(undoArgs); + queueObj.redo.calls.push(redoFunc); + queueObj.redo.args.push(redoArgs); + } + + if (gd.undoQueue.queue.length > config.queueLength) { + gd.undoQueue.queue.shift(); + gd.undoQueue.index--; + } }; /** @@ -103,9 +100,9 @@ queue.add = function(gd, undoFunc, undoArgs, redoFunc, redoArgs) { * @param gd */ queue.startSequence = function(gd) { - gd.undoQueue = gd.undoQueue || {index: 0, queue: [], sequence: false}; - gd.undoQueue.sequence = true; - gd.undoQueue.beginSequence = true; + gd.undoQueue = gd.undoQueue || { index: 0, queue: [], sequence: false }; + gd.undoQueue.sequence = true; + gd.undoQueue.beginSequence = true; }; /** @@ -116,9 +113,9 @@ queue.startSequence = function(gd) { * @param gd */ queue.stopSequence = function(gd) { - gd.undoQueue = gd.undoQueue || {index: 0, queue: [], sequence: false}; - gd.undoQueue.sequence = false; - gd.undoQueue.beginSequence = false; + gd.undoQueue = gd.undoQueue || { index: 0, queue: [], sequence: false }; + gd.undoQueue.sequence = false; + gd.undoQueue.beginSequence = false; }; /** @@ -127,31 +124,33 @@ queue.stopSequence = function(gd) { * @param gd */ queue.undo = function undo(gd) { - var queueObj, i; - - if(gd.framework && gd.framework.isPolar) { - gd.framework.undo(); - return; - } - if(gd.undoQueue === undefined || - isNaN(gd.undoQueue.index) || - gd.undoQueue.index <= 0) { - return; - } - - // index is pointing to next *forward* queueObj, point to the one we're undoing - gd.undoQueue.index--; - - // get the queueObj for instructions on how to undo - queueObj = gd.undoQueue.queue[gd.undoQueue.index]; - - // this sequence keeps things from adding to the queue during undo/redo - gd.undoQueue.inSequence = true; - for(i = 0; i < queueObj.undo.calls.length; i++) { - queue.plotDo(gd, queueObj.undo.calls[i], queueObj.undo.args[i]); - } - gd.undoQueue.inSequence = false; - gd.autoplay = false; + var queueObj, i; + + if (gd.framework && gd.framework.isPolar) { + gd.framework.undo(); + return; + } + if ( + gd.undoQueue === undefined || + isNaN(gd.undoQueue.index) || + gd.undoQueue.index <= 0 + ) { + return; + } + + // index is pointing to next *forward* queueObj, point to the one we're undoing + gd.undoQueue.index--; + + // get the queueObj for instructions on how to undo + queueObj = gd.undoQueue.queue[gd.undoQueue.index]; + + // this sequence keeps things from adding to the queue during undo/redo + gd.undoQueue.inSequence = true; + for (i = 0; i < queueObj.undo.calls.length; i++) { + queue.plotDo(gd, queueObj.undo.calls[i], queueObj.undo.args[i]); + } + gd.undoQueue.inSequence = false; + gd.autoplay = false; }; /** @@ -160,31 +159,33 @@ queue.undo = function undo(gd) { * @param gd */ queue.redo = function redo(gd) { - var queueObj, i; - - if(gd.framework && gd.framework.isPolar) { - gd.framework.redo(); - return; - } - if(gd.undoQueue === undefined || - isNaN(gd.undoQueue.index) || - gd.undoQueue.index >= gd.undoQueue.queue.length) { - return; - } - - // get the queueObj for instructions on how to undo - queueObj = gd.undoQueue.queue[gd.undoQueue.index]; - - // this sequence keeps things from adding to the queue during undo/redo - gd.undoQueue.inSequence = true; - for(i = 0; i < queueObj.redo.calls.length; i++) { - queue.plotDo(gd, queueObj.redo.calls[i], queueObj.redo.args[i]); - } - gd.undoQueue.inSequence = false; - gd.autoplay = false; - - // index is pointing to the thing we just redid, move it - gd.undoQueue.index++; + var queueObj, i; + + if (gd.framework && gd.framework.isPolar) { + gd.framework.redo(); + return; + } + if ( + gd.undoQueue === undefined || + isNaN(gd.undoQueue.index) || + gd.undoQueue.index >= gd.undoQueue.queue.length + ) { + return; + } + + // get the queueObj for instructions on how to undo + queueObj = gd.undoQueue.queue[gd.undoQueue.index]; + + // this sequence keeps things from adding to the queue during undo/redo + gd.undoQueue.inSequence = true; + for (i = 0; i < queueObj.redo.calls.length; i++) { + queue.plotDo(gd, queueObj.redo.calls[i], queueObj.redo.args[i]); + } + gd.undoQueue.inSequence = false; + gd.autoplay = false; + + // index is pointing to the thing we just redid, move it + gd.undoQueue.index++; }; /** @@ -197,13 +198,13 @@ queue.redo = function redo(gd) { * @param args */ queue.plotDo = function(gd, func, args) { - gd.autoplay = true; + gd.autoplay = true; - // this *won't* copy gd and it preserves `undefined` properties! - args = copyArgArray(gd, args); + // this *won't* copy gd and it preserves `undefined` properties! + args = copyArgArray(gd, args); - // call the supplied function - func.apply(null, args); + // call the supplied function + func.apply(null, args); }; module.exports = queue; diff --git a/src/lib/search.js b/src/lib/search.js index 8cb4275f1f3..e779a81106f 100644 --- a/src/lib/search.js +++ b/src/lib/search.js @@ -5,13 +5,9 @@ * 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 isNumeric = require('fast-isnumeric'); -var loggers = require('./loggers'); - +"use strict"; +var isNumeric = require("fast-isnumeric"); +var loggers = require("./loggers"); /** * findBin - find the bin for val - note that it can return outside the @@ -24,40 +20,47 @@ var loggers = require('./loggers'); * the lower bin rather than the default upper bin */ exports.findBin = function(val, bins, linelow) { - if(isNumeric(bins.start)) { - return linelow ? - Math.ceil((val - bins.start) / bins.size) - 1 : - Math.floor((val - bins.start) / bins.size); + if (isNumeric(bins.start)) { + return linelow + ? Math.ceil((val - bins.start) / bins.size) - 1 + : Math.floor((val - bins.start) / bins.size); + } else { + var n1 = 0, n2 = bins.length, c = 0, n, test; + if (bins[bins.length - 1] >= bins[0]) { + test = linelow ? lessThan : lessOrEqual; + } else { + test = linelow ? greaterOrEqual : greaterThan; } - else { - var n1 = 0, - n2 = bins.length, - c = 0, - n, - test; - if(bins[bins.length - 1] >= bins[0]) { - test = linelow ? lessThan : lessOrEqual; - } else { - test = linelow ? greaterOrEqual : greaterThan; - } - // c is just to avoid infinite loops if there's an error - while(n1 < n2 && c++ < 100) { - n = Math.floor((n1 + n2) / 2); - if(test(bins[n], val)) n1 = n + 1; - else n2 = n; - } - if(c > 90) loggers.log('Long binary search...'); - return n1 - 1; + // c is just to avoid infinite loops if there's an error + while (n1 < n2 && c++ < 100) { + n = Math.floor((n1 + n2) / 2); + if (test(bins[n], val)) n1 = n + 1; + else n2 = n; } + if (c > 90) loggers.log("Long binary search..."); + return n1 - 1; + } }; -function lessThan(a, b) { return a < b; } -function lessOrEqual(a, b) { return a <= b; } -function greaterThan(a, b) { return a > b; } -function greaterOrEqual(a, b) { return a >= b; } +function lessThan(a, b) { + return a < b; +} +function lessOrEqual(a, b) { + return a <= b; +} +function greaterThan(a, b) { + return a > b; +} +function greaterOrEqual(a, b) { + return a >= b; +} -exports.sorterAsc = function(a, b) { return a - b; }; -exports.sorterDes = function(a, b) { return b - a; }; +exports.sorterAsc = function(a, b) { + return a - b; +}; +exports.sorterDes = function(a, b) { + return b - a; +}; /** * find distinct values in an array, lumping together ones that appear to @@ -65,23 +68,24 @@ exports.sorterDes = function(a, b) { return b - a; }; * return the distinct values and the minimum difference between any two */ exports.distinctVals = function(valsIn) { - var vals = valsIn.slice(); // otherwise we sort the original array... - vals.sort(exports.sorterAsc); + var vals = valsIn.slice(); + // otherwise we sort the original array... + vals.sort(exports.sorterAsc); - var l = vals.length - 1, - minDiff = (vals[l] - vals[0]) || 1, - errDiff = minDiff / (l || 1) / 10000, - v2 = [vals[0]]; + var l = vals.length - 1, + minDiff = vals[l] - vals[0] || 1, + errDiff = minDiff / (l || 1) / 10000, + v2 = [vals[0]]; - for(var i = 0; i < l; i++) { - // make sure values aren't just off by a rounding error - if(vals[i + 1] > vals[i] + errDiff) { - minDiff = Math.min(minDiff, vals[i + 1] - vals[i]); - v2.push(vals[i + 1]); - } + for (var i = 0; i < l; i++) { + // make sure values aren't just off by a rounding error + if (vals[i + 1] > vals[i] + errDiff) { + minDiff = Math.min(minDiff, vals[i + 1] - vals[i]); + v2.push(vals[i + 1]); } + } - return {vals: v2, minDiff: minDiff}; + return { vals: v2, minDiff: minDiff }; }; /** @@ -92,18 +96,18 @@ exports.distinctVals = function(valsIn) { * binary search is probably overkill here... */ exports.roundUp = function(val, arrayIn, reverse) { - var low = 0, - high = arrayIn.length - 1, - mid, - c = 0, - dlow = reverse ? 0 : 1, - dhigh = reverse ? 1 : 0, - rounded = reverse ? Math.ceil : Math.floor; - // c is just to avoid infinite loops if there's an error - while(low < high && c++ < 100) { - mid = rounded((low + high) / 2); - if(arrayIn[mid] <= val) low = mid + dlow; - else high = mid - dhigh; - } - return arrayIn[low]; + var low = 0, + high = arrayIn.length - 1, + mid, + c = 0, + dlow = reverse ? 0 : 1, + dhigh = reverse ? 1 : 0, + rounded = reverse ? Math.ceil : Math.floor; + // c is just to avoid infinite loops if there's an error + while (low < high && c++ < 100) { + mid = rounded((low + high) / 2); + if (arrayIn[mid] <= val) low = mid + dlow; + else high = mid - dhigh; + } + return arrayIn[low]; }; diff --git a/src/lib/setcursor.js b/src/lib/setcursor.js index ef70880a1d5..007605d775c 100644 --- a/src/lib/setcursor.js +++ b/src/lib/setcursor.js @@ -5,17 +5,14 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; // works with our CSS cursor classes (see css/_cursor.scss) // to apply cursors to d3 single-element selections. // omit cursor to revert to the default. module.exports = function setCursor(el3, csr) { - (el3.attr('class') || '').split(' ').forEach(function(cls) { - if(cls.indexOf('cursor-') === 0) el3.classed(cls, false); - }); + (el3.attr("class") || "").split(" ").forEach(function(cls) { + if (cls.indexOf("cursor-") === 0) el3.classed(cls, false); + }); - if(csr) el3.classed('cursor-' + csr, true); + if (csr) el3.classed("cursor-" + csr, true); }; diff --git a/src/lib/show_no_webgl_msg.js b/src/lib/show_no_webgl_msg.js index 40b84c680bf..a952b1ac2e6 100644 --- a/src/lib/show_no_webgl_msg.js +++ b/src/lib/show_no_webgl_msg.js @@ -5,15 +5,11 @@ * 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 Color = require('../components/color'); +"use strict"; +var Color = require("../components/color"); var noop = function() {}; - /** * Prints a no webgl error message into the scene container * @param {scene instance} scene @@ -22,26 +18,26 @@ var noop = function() {}; * */ module.exports = function showWebGlMsg(scene) { - for(var prop in scene) { - if(typeof scene[prop] === 'function') scene[prop] = noop; - } - - scene.destroy = function() { - scene.container.parentNode.removeChild(scene.container); - }; - - var div = document.createElement('div'); - div.textContent = 'Webgl is not supported by your browser - visit http://get.webgl.org for more info'; - div.style.cursor = 'pointer'; - div.style.fontSize = '24px'; - div.style.color = Color.defaults[0]; - - scene.container.appendChild(div); - scene.container.style.background = '#FFFFFF'; - scene.container.onclick = function() { - window.open('http://get.webgl.org'); - }; - - // return before setting up camera and onrender methods - return false; + for (var prop in scene) { + if (typeof scene[prop] === "function") scene[prop] = noop; + } + + scene.destroy = function() { + scene.container.parentNode.removeChild(scene.container); + }; + + var div = document.createElement("div"); + div.textContent = "Webgl is not supported by your browser - visit http://get.webgl.org for more info"; + div.style.cursor = "pointer"; + div.style.fontSize = "24px"; + div.style.color = Color.defaults[0]; + + scene.container.appendChild(div); + scene.container.style.background = "#FFFFFF"; + scene.container.onclick = function() { + window.open("http://get.webgl.org"); + }; + + // return before setting up camera and onrender methods + return false; }; diff --git a/src/lib/stats.js b/src/lib/stats.js index a365d4ce33f..37e2718354c 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -5,12 +5,8 @@ * 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 isNumeric = require('fast-isnumeric'); - +"use strict"; +var isNumeric = require("fast-isnumeric"); /** * aggNums() returns the result of an aggregate function applied to an array of @@ -26,21 +22,22 @@ var isNumeric = require('fast-isnumeric'); * @return {Number} - result of f applied to a starting from v */ exports.aggNums = function(f, v, a, len) { - var i, - b; - if(!len) len = a.length; - if(!isNumeric(v)) v = false; - if(Array.isArray(a[0])) { - b = new Array(len); - for(i = 0; i < len; i++) b[i] = exports.aggNums(f, v, a[i]); - a = b; + var i, b; + if (!len) len = a.length; + if (!isNumeric(v)) v = false; + if (Array.isArray(a[0])) { + b = new Array(len); + for (i = 0; i < len; i++) { + b[i] = exports.aggNums(f, v, a[i]); } + a = b; + } - for(i = 0; i < len; i++) { - if(!isNumeric(v)) v = a[i]; - else if(isNumeric(a[i])) v = f(+v, +a[i]); - } - return v; + for (i = 0; i < len; i++) { + if (!isNumeric(v)) v = a[i]; + else if (isNumeric(a[i])) v = f(+v, +a[i]); + } + return v; }; /** @@ -48,25 +45,43 @@ exports.aggNums = function(f, v, a, len) { * even need to use aggNums instead of .length, to toss out non-numerics */ exports.len = function(data) { - return exports.aggNums(function(a) { return a + 1; }, 0, data); + return exports.aggNums( + function(a) { + return a + 1; + }, + 0, + data + ); }; exports.mean = function(data, len) { - if(!len) len = exports.len(data); - return exports.aggNums(function(a, b) { return a + b; }, 0, data) / len; + if (!len) len = exports.len(data); + return exports.aggNums( + function(a, b) { + return a + b; + }, + 0, + data + ) / + len; }; exports.variance = function(data, len, mean) { - if(!len) len = exports.len(data); - if(!isNumeric(mean)) mean = exports.mean(data, len); + if (!len) len = exports.len(data); + if (!isNumeric(mean)) mean = exports.mean(data, len); - return exports.aggNums(function(a, b) { - return a + Math.pow(b - mean, 2); - }, 0, data) / len; + return exports.aggNums( + function(a, b) { + return a + Math.pow(b - mean, 2); + }, + 0, + data + ) / + len; }; exports.stdev = function(data, len, mean) { - return Math.sqrt(exports.variance(data, len, mean)); + return Math.sqrt(exports.variance(data, len, mean)); }; /** @@ -85,10 +100,10 @@ exports.stdev = function(data, len, mean) { * @return {Number} - percentile */ exports.interp = function(arr, n) { - if(!isNumeric(n)) throw 'n should be a finite number'; - n = n * arr.length - 0.5; - if(n < 0) return arr[0]; - if(n > arr.length - 1) return arr[arr.length - 1]; - var frac = n % 1; - return frac * arr[Math.ceil(n)] + (1 - frac) * arr[Math.floor(n)]; + if (!isNumeric(n)) throw "n should be a finite number"; + n = n * arr.length - 0.5; + if (n < 0) return arr[0]; + if (n > arr.length - 1) return arr[arr.length - 1]; + var frac = n % 1; + return frac * arr[Math.ceil(n)] + (1 - frac) * arr[Math.floor(n)]; }; diff --git a/src/lib/str2rgbarray.js b/src/lib/str2rgbarray.js index 0dd3c027fce..f35fa8ecb80 100644 --- a/src/lib/str2rgbarray.js +++ b/src/lib/str2rgbarray.js @@ -5,16 +5,13 @@ * 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 tinycolor = require('tinycolor2'); -var arrtools = require('arraytools'); +"use strict"; +var tinycolor = require("tinycolor2"); +var arrtools = require("arraytools"); function str2RgbaArray(color) { - color = tinycolor(color); - return arrtools.str2RgbaArray(color.toRgbString()); + color = tinycolor(color); + return arrtools.str2RgbaArray(color.toRgbString()); } module.exports = str2RgbaArray; diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index a55334d5157..9aa02e19aa2 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -5,298 +5,324 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; /* global MathJax:false */ -var d3 = require('d3'); +var d3 = require("d3"); -var Lib = require('../lib'); -var xmlnsNamespaces = require('../constants/xmlns_namespaces'); -var stringMappings = require('../constants/string_mappings'); +var Lib = require("../lib"); +var xmlnsNamespaces = require("../constants/xmlns_namespaces"); +var stringMappings = require("../constants/string_mappings"); // Append SVG - d3.selection.prototype.appendSVG = function(_svgString) { - var skeleton = [ - '', - _svgString, - '' - ].join(''); - - var dom = new DOMParser().parseFromString(skeleton, 'application/xml'), - childNode = dom.documentElement.firstChild; - - while(childNode) { - this.node().appendChild(this.node().ownerDocument.importNode(childNode, true)); - childNode = childNode.nextSibling; - } - if(dom.querySelector('parsererror')) { - Lib.log(dom.querySelector('parsererror div').textContent); - return null; - } - return d3.select(this.node().lastChild); + var skeleton = [ + '', + _svgString, + "" + ].join(""); + + var dom = new DOMParser().parseFromString(skeleton, "application/xml"), + childNode = dom.documentElement.firstChild; + + while (childNode) { + this + .node() + .appendChild(this.node().ownerDocument.importNode(childNode, true)); + childNode = childNode.nextSibling; + } + if (dom.querySelector("parsererror")) { + Lib.log(dom.querySelector("parsererror div").textContent); + return null; + } + return d3.select(this.node().lastChild); }; // Text utilities - exports.html_entity_decode = function(s) { - var hiddenDiv = d3.select('body').append('div').style({display: 'none'}).html(''); - var replaced = s.replace(/(&[^;]*;)/gi, function(d) { - if(d === '<') { return '<'; } // special handling for brackets - if(d === '&rt;') { return '>'; } - if(d.indexOf('<') !== -1 || d.indexOf('>') !== -1) { return ''; } - return hiddenDiv.html(d).text(); // everything else, let the browser decode it to unicode - }); - hiddenDiv.remove(); - return replaced; + var hiddenDiv = d3 + .select("body") + .append("div") + .style({ display: "none" }) + .html(""); + var replaced = s.replace(/(&[^;]*;)/gi, function(d) { + if (d === "<") { + return "<"; + } + // special handling for brackets + if (d === "&rt;") { + return ">"; + } + if (d.indexOf("<") !== -1 || d.indexOf(">") !== -1) { + return ""; + } + return hiddenDiv.html(d).text(); // everything else, let the browser decode it to unicode + }); + hiddenDiv.remove(); + return replaced; }; exports.xml_entity_encode = function(str) { - return str.replace(/&(?!\w+;|\#[0-9]+;| \#x[0-9A-F]+;)/g, '&'); + return str.replace(/&(?!\w+;|\#[0-9]+;| \#x[0-9A-F]+;)/g, "&"); }; // text converter - function getSize(_selection, _dimension) { - return _selection.node().getBoundingClientRect()[_dimension]; + return _selection.node().getBoundingClientRect()[_dimension]; } exports.convertToTspans = function(_context, _callback) { - var str = _context.text(); - var converted = convertToSVG(str); - var that = _context; - - // Until we get tex integrated more fully (so it can be used along with non-tex) - // allow some elements to prohibit it by attaching 'data-notex' to the original - var tex = (!that.attr('data-notex')) && converted.match(/([^$]*)([$]+[^$]*[$]+)([^$]*)/); - var result = str; - var parent = d3.select(that.node().parentNode); - if(parent.empty()) return; - var svgClass = (that.attr('class')) ? that.attr('class').split(' ')[0] : 'text'; - svgClass += '-math'; - parent.selectAll('svg.' + svgClass).remove(); - parent.selectAll('g.' + svgClass + '-group').remove(); - _context.style({visibility: null}); - for(var up = _context.node(); up && up.removeAttribute; up = up.parentNode) { - up.removeAttribute('data-bb'); + var str = _context.text(); + var converted = convertToSVG(str); + var that = _context; + + // Until we get tex integrated more fully (so it can be used along with non-tex) + // allow some elements to prohibit it by attaching 'data-notex' to the original + var tex = !that.attr("data-notex") && + converted.match(/([^$]*)([$]+[^$]*[$]+)([^$]*)/); + var result = str; + var parent = d3.select(that.node().parentNode); + if (parent.empty()) return; + var svgClass = that.attr("class") ? that.attr("class").split(" ")[0] : "text"; + svgClass += "-math"; + parent.selectAll("svg." + svgClass).remove(); + parent.selectAll("g." + svgClass + "-group").remove(); + _context.style({ visibility: null }); + for (var up = _context.node(); up && up.removeAttribute; up = up.parentNode) { + up.removeAttribute("data-bb"); + } + + function showText() { + if (!parent.empty()) { + svgClass = that.attr("class") + "-math"; + parent.select("svg." + svgClass).remove(); + } + _context.text("").style({ visibility: "inherit", "white-space": "pre" }); + + result = _context.appendSVG(converted); + + if (!result) _context.text(str); + + if (_context.select("a").size()) { + // at least in Chrome, pointer-events does not seem + // to be honored in children of elements + // so if we have an anchor, we have to make the + // whole element respond + _context.style("pointer-events", "all"); } - function showText() { - if(!parent.empty()) { - svgClass = that.attr('class') + '-math'; - parent.select('svg.' + svgClass).remove(); + if (_callback) _callback.call(that); + } + + if (tex) { + var gd = Lib.getPlotDiv(that.node()); + (gd && gd._promises || []).push(new Promise(function(resolve) { + that.style({ visibility: "hidden" }); + var config = { fontSize: parseInt(that.style("font-size"), 10) }; + + texToSVG(tex[2], config, function(_svgEl, _glyphDefs, _svgBBox) { + parent.selectAll("svg." + svgClass).remove(); + parent.selectAll("g." + svgClass + "-group").remove(); + + var newSvg = _svgEl && _svgEl.select("svg"); + if (!newSvg || !newSvg.node()) { + showText(); + resolve(); + return; } - _context.text('') - .style({ - visibility: 'inherit', - 'white-space': 'pre' - }); - - result = _context.appendSVG(converted); - - if(!result) _context.text(str); - - if(_context.select('a').size()) { - // at least in Chrome, pointer-events does not seem - // to be honored in children of elements - // so if we have an anchor, we have to make the - // whole element respond - _context.style('pointer-events', 'all'); + + var mathjaxGroup = parent + .append("g") + .classed(svgClass + "-group", true) + .attr({ "pointer-events": "none" }); + + mathjaxGroup.node().appendChild(newSvg.node()); + + // stitch the glyph defs + if (_glyphDefs && _glyphDefs.node()) { + newSvg + .node() + .insertBefore( + _glyphDefs.node().cloneNode(true), + newSvg.node().firstChild + ); } - if(_callback) _callback.call(that); - } + newSvg + .attr({ + class: svgClass, + height: _svgBBox.height, + preserveAspectRatio: "xMinYMin meet" + }) + .style({ overflow: "visible", "pointer-events": "none" }); + + var fill = that.style("fill") || "black"; + newSvg.select("g").attr({ fill: fill, stroke: fill }); + + var newSvgW = getSize(newSvg, "width"), + newSvgH = getSize(newSvg, "height"), + newX = +that.attr("x") - + newSvgW * + ({ start: 0, middle: 0.5, end: 1 })[ + that.attr("text-anchor") || "start" + ], + // font baseline is about 1/4 fontSize below centerline + textHeight = parseInt(that.style("font-size"), 10) || + getSize(that, "height"), + dy = (-textHeight) / 4; + + if (svgClass[0] === "y") { + mathjaxGroup.attr({ + transform: ( + "rotate(" + + [-90, +that.attr("x"), +that.attr("y")] + + ") translate(" + + [(-newSvgW) / 2, dy - newSvgH / 2] + + ")" + ) + }); + newSvg.attr({ x: +that.attr("x"), y: +that.attr("y") }); + } else if (svgClass[0] === "l") { + newSvg.attr({ x: that.attr("x"), y: dy - newSvgH / 2 }); + } else if (svgClass[0] === "a") { + newSvg.attr({ x: 0, y: dy }); + } else { + newSvg.attr({ x: newX, y: +that.attr("y") + dy - newSvgH / 2 }); + } - if(tex) { - var gd = Lib.getPlotDiv(that.node()); - ((gd && gd._promises) || []).push(new Promise(function(resolve) { - that.style({visibility: 'hidden'}); - var config = {fontSize: parseInt(that.style('font-size'), 10)}; - - texToSVG(tex[2], config, function(_svgEl, _glyphDefs, _svgBBox) { - parent.selectAll('svg.' + svgClass).remove(); - parent.selectAll('g.' + svgClass + '-group').remove(); - - var newSvg = _svgEl && _svgEl.select('svg'); - if(!newSvg || !newSvg.node()) { - showText(); - resolve(); - return; - } - - var mathjaxGroup = parent.append('g') - .classed(svgClass + '-group', true) - .attr({'pointer-events': 'none'}); - - mathjaxGroup.node().appendChild(newSvg.node()); - - // stitch the glyph defs - if(_glyphDefs && _glyphDefs.node()) { - newSvg.node().insertBefore(_glyphDefs.node().cloneNode(true), - newSvg.node().firstChild); - } - - newSvg.attr({ - 'class': svgClass, - height: _svgBBox.height, - preserveAspectRatio: 'xMinYMin meet' - }) - .style({overflow: 'visible', 'pointer-events': 'none'}); - - var fill = that.style('fill') || 'black'; - newSvg.select('g').attr({fill: fill, stroke: fill}); - - var newSvgW = getSize(newSvg, 'width'), - newSvgH = getSize(newSvg, 'height'), - newX = +that.attr('x') - newSvgW * - {start: 0, middle: 0.5, end: 1}[that.attr('text-anchor') || 'start'], - // font baseline is about 1/4 fontSize below centerline - textHeight = parseInt(that.style('font-size'), 10) || - getSize(that, 'height'), - dy = -textHeight / 4; - - if(svgClass[0] === 'y') { - mathjaxGroup.attr({ - transform: 'rotate(' + [-90, +that.attr('x'), +that.attr('y')] + - ') translate(' + [-newSvgW / 2, dy - newSvgH / 2] + ')' - }); - newSvg.attr({x: +that.attr('x'), y: +that.attr('y')}); - } - else if(svgClass[0] === 'l') { - newSvg.attr({x: that.attr('x'), y: dy - (newSvgH / 2)}); - } - else if(svgClass[0] === 'a') { - newSvg.attr({x: 0, y: dy}); - } - else { - newSvg.attr({x: newX, y: (+that.attr('y') + dy - newSvgH / 2)}); - } - - if(_callback) _callback.call(that, mathjaxGroup); - resolve(mathjaxGroup); - }); - })); - } - else showText(); + if (_callback) _callback.call(that, mathjaxGroup); + resolve(mathjaxGroup); + }); + })); + } else { + showText(); + } - return _context; + return _context; }; - // MathJax - function cleanEscapesForTex(s) { - return s.replace(/(<|<|<)/g, '\\lt ') - .replace(/(>|>|>)/g, '\\gt '); + return s + .replace(/(<|<|<)/g, "\\lt ") + .replace(/(>|>|>)/g, "\\gt "); } function texToSVG(_texString, _config, _callback) { - var randomID = 'math-output-' + Lib.randstr([], 64); - var tmpDiv = d3.select('body').append('div') - .attr({id: randomID}) - .style({visibility: 'hidden', position: 'absolute'}) - .style({'font-size': _config.fontSize + 'px'}) - .text(cleanEscapesForTex(_texString)); - - MathJax.Hub.Queue(['Typeset', MathJax.Hub, tmpDiv.node()], function() { - var glyphDefs = d3.select('body').select('#MathJax_SVG_glyphs'); - - if(tmpDiv.select('.MathJax_SVG').empty() || !tmpDiv.select('svg').node()) { - Lib.log('There was an error in the tex syntax.', _texString); - _callback(); - } - else { - var svgBBox = tmpDiv.select('svg').node().getBoundingClientRect(); - _callback(tmpDiv.select('.MathJax_SVG'), glyphDefs, svgBBox); - } + var randomID = "math-output-" + Lib.randstr([], 64); + var tmpDiv = d3 + .select("body") + .append("div") + .attr({ id: randomID }) + .style({ visibility: "hidden", position: "absolute" }) + .style({ "font-size": _config.fontSize + "px" }) + .text(cleanEscapesForTex(_texString)); + + MathJax.Hub.Queue(["Typeset", MathJax.Hub, tmpDiv.node()], function() { + var glyphDefs = d3.select("body").select("#MathJax_SVG_glyphs"); + + if (tmpDiv.select(".MathJax_SVG").empty() || !tmpDiv.select("svg").node()) { + Lib.log("There was an error in the tex syntax.", _texString); + _callback(); + } else { + var svgBBox = tmpDiv.select("svg").node().getBoundingClientRect(); + _callback(tmpDiv.select(".MathJax_SVG"), glyphDefs, svgBBox); + } - tmpDiv.remove(); - }); + tmpDiv.remove(); + }); } var TAG_STYLES = { - // would like to use baseline-shift but FF doesn't support it yet - // so we need to use dy along with the uber hacky shift-back-to - // baseline below - sup: 'font-size:70%" dy="-0.6em', - sub: 'font-size:70%" dy="0.3em', - b: 'font-weight:bold', - i: 'font-style:italic', - a: '', - span: '', - br: '', - em: 'font-style:italic;font-weight:bold' + // would like to use baseline-shift but FF doesn't support it yet + // so we need to use dy along with the uber hacky shift-back-to + // baseline below + sup: 'font-size:70%" dy="-0.6em', + sub: 'font-size:70%" dy="0.3em', + b: "font-weight:bold", + i: "font-style:italic", + a: "", + span: "", + br: "", + em: "font-style:italic;font-weight:bold" }; -var PROTOCOLS = ['http:', 'https:', 'mailto:']; +var PROTOCOLS = ["http:", "https:", "mailto:"]; -var STRIP_TAGS = new RegExp(']*)?/?>', 'g'); +var STRIP_TAGS = new RegExp( + "]*)?/?>", + "g" +); -var ENTITY_TO_UNICODE = Object.keys(stringMappings.entityToUnicode).map(function(k) { - return { - regExp: new RegExp('&' + k + ';', 'g'), - sub: stringMappings.entityToUnicode[k] - }; +var ENTITY_TO_UNICODE = Object.keys( + stringMappings.entityToUnicode +).map(function(k) { + return { + regExp: new RegExp("&" + k + ";", "g"), + sub: stringMappings.entityToUnicode[k] + }; }); -var UNICODE_TO_ENTITY = Object.keys(stringMappings.unicodeToEntity).map(function(k) { - return { - regExp: new RegExp(k, 'g'), - sub: '&' + stringMappings.unicodeToEntity[k] + ';' - }; +var UNICODE_TO_ENTITY = Object.keys( + stringMappings.unicodeToEntity +).map(function(k) { + return { + regExp: new RegExp(k, "g"), + sub: "&" + stringMappings.unicodeToEntity[k] + ";" + }; }); var NEWLINES = /(\r\n?|\n)/g; exports.plainText = function(_str) { - // strip out our pseudo-html so we have a readable - // version to put into text fields - return (_str || '').replace(STRIP_TAGS, ' '); + // strip out our pseudo-html so we have a readable + // version to put into text fields + return (_str || "").replace(STRIP_TAGS, " "); }; function replaceFromMapObject(_str, list) { - var out = _str || ''; + var out = _str || ""; - for(var i = 0; i < list.length; i++) { - var item = list[i]; - out = out.replace(item.regExp, item.sub); - } + for (var i = 0; i < list.length; i++) { + var item = list[i]; + out = out.replace(item.regExp, item.sub); + } - return out; + return out; } function convertEntities(_str) { - return replaceFromMapObject(_str, ENTITY_TO_UNICODE); + return replaceFromMapObject(_str, ENTITY_TO_UNICODE); } function encodeForHTML(_str) { - return replaceFromMapObject(_str, UNICODE_TO_ENTITY); + return replaceFromMapObject(_str, UNICODE_TO_ENTITY); } function convertToSVG(_str) { - _str = convertEntities(_str); - - // normalize behavior between IE and others wrt newlines and whitespace:pre - // this combination makes IE barf https://github.com/plotly/plotly.js/issues/746 - // Chrome and FF display \n, \r, or \r\n as a space in this mode. - // I feel like at some point we turned these into
but currently we don't so - // I'm just going to cement what we do now in Chrome and FF - _str = _str.replace(NEWLINES, ' '); - - var result = _str - .split(/(<[^<>]*>)/).map(function(d) { - var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i), - tag = match && match[2].toLowerCase(), - style = TAG_STYLES[tag]; - - if(style !== undefined) { - var close = match[1], - extra = match[3], - /** + _str = convertEntities(_str); + + // normalize behavior between IE and others wrt newlines and whitespace:pre + // this combination makes IE barf https://github.com/plotly/plotly.js/issues/746 + // Chrome and FF display \n, \r, or \r\n as a space in this mode. + // I feel like at some point we turned these into
but currently we don't so + // I'm just going to cement what we do now in Chrome and FF + _str = _str.replace(NEWLINES, " "); + + var result = _str.split(/(<[^<>]*>)/).map(function(d) { + var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i), + tag = match && match[2].toLowerCase(), + style = TAG_STYLES[tag]; + + if (style !== undefined) { + var close = match[1], + extra = match[3], + /** * extraStyle: any random extra css (that's supported by svg) * use this like to change font in the middle * @@ -304,232 +330,265 @@ function convertToSVG(_str) { * valid HTML anymore and we dropped it accidentally for many months, we will not * resurrect it. */ - extraStyle = extra.match(/^style\s*=\s*"([^"]+)"\s*/i); - - // anchor and br are the only ones that don't turn into a tspan - if(tag === 'a') { - if(close) return ''; - else if(extra.substr(0, 4).toLowerCase() !== 'href') return ''; - else { - // remove quotes, leading '=', replace '&' with '&' - var href = extra.substr(4) - .replace(/["']/g, '') - .replace(/=/, ''); - - // check protocol - var dummyAnchor = document.createElement('a'); - dummyAnchor.href = href; - if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return ''; - - return ''; - } - } - else if(tag === 'br') return '
'; - else if(close) { - // closing tag - - // sub/sup: extra tspan with zero-width space to get back to the right baseline - if(tag === 'sup') return ''; - if(tag === 'sub') return ''; - else return ''; - } - else { - var tspanStart = ''; - } - } - else { - return exports.xml_entity_encode(d).replace(/"; + } else if (extra.substr(0, 4).toLowerCase() !== "href") { + return "
"; + } else { + // remove quotes, leading '=', replace '&' with '&' + var href = extra.substr(4).replace(/["']/g, "").replace(/=/, ""); + + // check protocol + var dummyAnchor = document.createElement("a"); + dummyAnchor.href = href; + if (PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return ""; + + return ''; + } + } else if (tag === "br") { + return "
"; + } else if (close) { + // closing tag + // sub/sup: extra tspan with zero-width space to get back to the right baseline + if (tag === "sup") return ''; + if (tag === "sub") { + return ''; + } else { + return ""; + } + } else { + var tspanStart = "'); index > 0; index = result.indexOf('
', index + 1)) { - indices.push(index); - } - var count = 0; - indices.forEach(function(d) { - var brIndex = d + count; - var search = result.slice(0, brIndex); - var previousOpenTag = ''; - for(var i2 = search.length - 1; i2 >= 0; i2--) { - var isTag = search[i2].match(/<(\/?).*>/i); - if(isTag && search[i2] !== '
') { - if(!isTag[1]) previousOpenTag = search[i2]; - break; - } + if (tag === "sup" || tag === "sub") { + // sub/sup: extra zero-width space, fixes problem if new line starts with sub/sup + tspanStart = "​" + tspanStart; } - if(previousOpenTag) { - result.splice(brIndex + 1, 0, previousOpenTag); - result.splice(brIndex, 0, ''); - count += 2; + + if (extraStyle) { + // most of the svg css users will care about is just like html, + // but font color is different. Let our users ignore this. + extraStyle = extraStyle[1].replace(/(^|;)\s*color:/, "$1 fill:"); + style = encodeForHTML(extraStyle) + (style ? ";" + style : ""); } - }); - var joined = result.join(''); - var splitted = joined.split(/
/gi); - if(splitted.length > 1) { - result = splitted.map(function(d, i) { - // TODO: figure out max font size of this line and alter dy - // this requires either: - // 1) bringing the base font size into convertToTspans, or - // 2) only allowing relative percentage font sizes. - // I think #2 is the way to go - return '' + d + ''; - }); + return tspanStart + (style ? ' style="' + style + '"' : "") + ">"; + } + } else { + return exports.xml_entity_encode(d).replace(/"); + index > 0; + index = result.indexOf("
", index + 1) + ) { + indices.push(index); + } + var count = 0; + indices.forEach(function(d) { + var brIndex = d + count; + var search = result.slice(0, brIndex); + var previousOpenTag = ""; + for (var i2 = search.length - 1; i2 >= 0; i2--) { + var isTag = search[i2].match(/<(\/?).*>/i); + if (isTag && search[i2] !== "
") { + if (!isTag[1]) previousOpenTag = search[i2]; + break; + } } + if (previousOpenTag) { + result.splice(brIndex + 1, 0, previousOpenTag); + result.splice(brIndex, 0, ""); + count += 2; + } + }); + + var joined = result.join(""); + var splitted = joined.split(/
/gi); + if (splitted.length > 1) { + result = splitted.map(function(d, i) { + // TODO: figure out max font size of this line and alter dy + // this requires either: + // 1) bringing the base font size into convertToTspans, or + // 2) only allowing relative percentage font sizes. + // I think #2 is the way to go + return '' + d + ""; + }); + } - return result.join(''); + return result.join(""); } function alignHTMLWith(_base, container, options) { - var alignH = options.horizontalAlign, - alignV = options.verticalAlign || 'top', - bRect = _base.node().getBoundingClientRect(), - cRect = container.node().getBoundingClientRect(), - thisRect, - getTop, - getLeft; - - if(alignV === 'bottom') { - getTop = function() { return bRect.bottom - thisRect.height; }; - } else if(alignV === 'middle') { - getTop = function() { return bRect.top + (bRect.height - thisRect.height) / 2; }; - } else { // default: top - getTop = function() { return bRect.top; }; - } - - if(alignH === 'right') { - getLeft = function() { return bRect.right - thisRect.width; }; - } else if(alignH === 'center') { - getLeft = function() { return bRect.left + (bRect.width - thisRect.width) / 2; }; - } else { // default: left - getLeft = function() { return bRect.left; }; - } + var alignH = options.horizontalAlign, + alignV = options.verticalAlign || "top", + bRect = _base.node().getBoundingClientRect(), + cRect = container.node().getBoundingClientRect(), + thisRect, + getTop, + getLeft; + + if (alignV === "bottom") { + getTop = function() { + return bRect.bottom - thisRect.height; + }; + } else if (alignV === "middle") { + getTop = function() { + return bRect.top + (bRect.height - thisRect.height) / 2; + }; + } else { + // default: top + getTop = function() { + return bRect.top; + }; + } - return function() { - thisRect = this.node().getBoundingClientRect(); - this.style({ - top: (getTop() - cRect.top) + 'px', - left: (getLeft() - cRect.left) + 'px', - 'z-index': 1000 - }); - return this; + if (alignH === "right") { + getLeft = function() { + return bRect.right - thisRect.width; + }; + } else if (alignH === "center") { + getLeft = function() { + return bRect.left + (bRect.width - thisRect.width) / 2; }; + } else { + // default: left + getLeft = function() { + return bRect.left; + }; + } + + return function() { + thisRect = this.node().getBoundingClientRect(); + this.style({ + top: getTop() - cRect.top + "px", + left: getLeft() - cRect.left + "px", + "z-index": 1000 + }); + return this; + }; } // Editable title - exports.makeEditable = function(context, _delegate, options) { - if(!options) options = {}; - var that = this; - var dispatch = d3.dispatch('edit', 'input', 'cancel'); - var textSelection = d3.select(this.node()) - .style({'pointer-events': 'all'}); - - var handlerElement = _delegate || textSelection; - if(_delegate) textSelection.style({'pointer-events': 'none'}); - - function handleClick() { - appendEditable(); - that.style({opacity: 0}); - // also hide any mathjax svg - var svgClass = handlerElement.attr('class'), - mathjaxClass; - if(svgClass) mathjaxClass = '.' + svgClass.split(' ')[0] + '-math-group'; - else mathjaxClass = '[class*=-math-group]'; - if(mathjaxClass) { - d3.select(that.node().parentNode).select(mathjaxClass).style({opacity: 0}); - } + if (!options) options = {}; + var that = this; + var dispatch = d3.dispatch("edit", "input", "cancel"); + var textSelection = d3.select(this.node()).style({ "pointer-events": "all" }); + + var handlerElement = _delegate || textSelection; + if (_delegate) textSelection.style({ "pointer-events": "none" }); + + function handleClick() { + appendEditable(); + that.style({ opacity: 0 }); + // also hide any mathjax svg + var svgClass = handlerElement.attr("class"), mathjaxClass; + if (svgClass) mathjaxClass = "." + svgClass.split(" ")[0] + "-math-group"; + else mathjaxClass = "[class*=-math-group]"; + if (mathjaxClass) { + d3 + .select(that.node().parentNode) + .select(mathjaxClass) + .style({ opacity: 0 }); } - - function selectElementContents(_el) { - var el = _el.node(); - var range = document.createRange(); - range.selectNodeContents(el); - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - el.focus(); - } - - function appendEditable() { - var plotDiv = d3.select(Lib.getPlotDiv(that.node())), - container = plotDiv.select('.svg-container'), - div = container.append('div'); - div.classed('plugin-editable editable', true) - .style({ - position: 'absolute', - 'font-family': that.style('font-family') || 'Arial', - 'font-size': that.style('font-size') || 12, - color: options.fill || that.style('fill') || 'black', - opacity: 1, - 'background-color': options.background || 'transparent', - outline: '#ffffff33 1px solid', - margin: [-parseFloat(that.style('font-size')) / 8 + 1, 0, 0, -1].join('px ') + 'px', - padding: '0', - 'box-sizing': 'border-box' - }) - .attr({contenteditable: true}) - .text(options.text || that.attr('data-unformatted')) - .call(alignHTMLWith(that, container, options)) - .on('blur', function() { - that.text(this.textContent) - .style({opacity: 1}); - var svgClass = d3.select(this).attr('class'), - mathjaxClass; - if(svgClass) mathjaxClass = '.' + svgClass.split(' ')[0] + '-math-group'; - else mathjaxClass = '[class*=-math-group]'; - if(mathjaxClass) { - d3.select(that.node().parentNode).select(mathjaxClass).style({opacity: 0}); - } - var text = this.textContent; - d3.select(this).transition().duration(0).remove(); - d3.select(document).on('mouseup', null); - dispatch.edit.call(that, text); - }) - .on('focus', function() { - var context = this; - d3.select(document).on('mouseup', function() { - if(d3.event.target === context) return false; - if(document.activeElement === div.node()) div.node().blur(); - }); - }) - .on('keyup', function() { - if(d3.event.which === 27) { - that.style({opacity: 1}); - d3.select(this) - .style({opacity: 0}) - .on('blur', function() { return false; }) - .transition().remove(); - dispatch.cancel.call(that, this.textContent); - } - else { - dispatch.input.call(that, this.textContent); - d3.select(this).call(alignHTMLWith(that, container, options)); - } - }) - .on('keydown', function() { - if(d3.event.which === 13) this.blur(); + } + + function selectElementContents(_el) { + var el = _el.node(); + var range = document.createRange(); + range.selectNodeContents(el); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + el.focus(); + } + + function appendEditable() { + var plotDiv = d3.select(Lib.getPlotDiv(that.node())), + container = plotDiv.select(".svg-container"), + div = container.append("div"); + div + .classed("plugin-editable editable", true) + .style({ + position: "absolute", + "font-family": that.style("font-family") || "Arial", + "font-size": that.style("font-size") || 12, + color: options.fill || that.style("fill") || "black", + opacity: 1, + "background-color": options.background || "transparent", + outline: "#ffffff33 1px solid", + margin: ( + [(-parseFloat(that.style("font-size"))) / 8 + 1, 0, 0, -1].join( + "px " + ) + + "px" + ), + padding: "0", + "box-sizing": "border-box" + }) + .attr({ contenteditable: true }) + .text(options.text || that.attr("data-unformatted")) + .call(alignHTMLWith(that, container, options)) + .on("blur", function() { + that.text(this.textContent).style({ opacity: 1 }); + var svgClass = d3.select(this).attr("class"), mathjaxClass; + if (svgClass) { + mathjaxClass = "." + svgClass.split(" ")[0] + "-math-group"; + } else { + mathjaxClass = "[class*=-math-group]"; + } + if (mathjaxClass) { + d3 + .select(that.node().parentNode) + .select(mathjaxClass) + .style({ opacity: 0 }); + } + var text = this.textContent; + d3.select(this).transition().duration(0).remove(); + d3.select(document).on("mouseup", null); + dispatch.edit.call(that, text); + }) + .on("focus", function() { + var context = this; + d3.select(document).on("mouseup", function() { + if (d3.event.target === context) return false; + if (document.activeElement === div.node()) div.node().blur(); + }); + }) + .on("keyup", function() { + if (d3.event.which === 27) { + that.style({ opacity: 1 }); + d3 + .select(this) + .style({ opacity: 0 }) + .on("blur", function() { + return false; }) - .call(selectElementContents); - } + .transition() + .remove(); + dispatch.cancel.call(that, this.textContent); + } else { + dispatch.input.call(that, this.textContent); + d3.select(this).call(alignHTMLWith(that, container, options)); + } + }) + .on("keydown", function() { + if (d3.event.which === 13) this.blur(); + }) + .call(selectElementContents); + } - if(options.immediate) handleClick(); - else handlerElement.on('click', handleClick); + if (options.immediate) handleClick(); + else handlerElement.on("click", handleClick); - return d3.rebind(this, dispatch, 'on'); + return d3.rebind(this, dispatch, "on"); }; diff --git a/src/lib/topojson_utils.js b/src/lib/topojson_utils.js index bdd5d7bf196..67574b3d1f6 100644 --- a/src/lib/topojson_utils.js +++ b/src/lib/topojson_utils.js @@ -5,30 +5,28 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var topojsonUtils = module.exports = {}; -var locationmodeToLayer = require('../plots/geo/constants').locationmodeToLayer; -var topojsonFeature = require('topojson-client').feature; - +var locationmodeToLayer = require("../plots/geo/constants").locationmodeToLayer; +var topojsonFeature = require("topojson-client").feature; topojsonUtils.getTopojsonName = function(geoLayout) { - return [ - geoLayout.scope.replace(/ /g, '-'), '_', - geoLayout.resolution.toString(), 'm' - ].join(''); + return [ + geoLayout.scope.replace(/ /g, "-"), + "_", + geoLayout.resolution.toString(), + "m" + ].join(""); }; topojsonUtils.getTopojsonPath = function(topojsonURL, topojsonName) { - return topojsonURL + topojsonName + '.json'; + return topojsonURL + topojsonName + ".json"; }; topojsonUtils.getTopojsonFeatures = function(trace, topojson) { - var layer = locationmodeToLayer[trace.locationmode], - obj = topojson.objects[layer]; + var layer = locationmodeToLayer[trace.locationmode], + obj = topojson.objects[layer]; - return topojsonFeature(topojson, obj).features; + return topojsonFeature(topojson, obj).features; }; diff --git a/src/lib/typed_array_truncate.js b/src/lib/typed_array_truncate.js index 3bacc8ebe56..718402f680b 100644 --- a/src/lib/typed_array_truncate.js +++ b/src/lib/typed_array_truncate.js @@ -5,19 +5,21 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; function truncateFloat32(arrayIn, len) { - var arrayOut = new Float32Array(len); - for(var i = 0; i < len; i++) arrayOut[i] = arrayIn[i]; - return arrayOut; + 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; + var arrayOut = new Float64Array(len); + for (var i = 0; i < len; i++) { + arrayOut[i] = arrayIn[i]; + } + return arrayOut; } /** @@ -26,7 +28,7 @@ function truncateFloat64(arrayIn, len) { * 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`.'); + 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/helpers.js b/src/plot_api/helpers.js index 62a4b7e38d5..6984f674f63 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -5,436 +5,456 @@ * 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 isNumeric = require("fast-isnumeric"); +var m4FromQuat = require("gl-mat4/fromQuat"); - -'use strict'; - -var isNumeric = require('fast-isnumeric'); -var m4FromQuat = require('gl-mat4/fromQuat'); - -var Registry = require('../registry'); -var Lib = require('../lib'); -var Plots = require('../plots/plots'); -var Axes = require('../plots/cartesian/axes'); -var Color = require('../components/color'); - +var Registry = require("../registry"); +var Lib = require("../lib"); +var Plots = require("../plots/plots"); +var Axes = require("../plots/cartesian/axes"); +var Color = require("../components/color"); // Get the container div: we store all variables for this plot as // properties of this div // some callers send this in by DOM element, others by id (string) exports.getGraphDiv = function(gd) { - var gdElement; + var gdElement; - if(typeof gd === 'string') { - gdElement = document.getElementById(gd); + if (typeof gd === "string") { + gdElement = document.getElementById(gd); - if(gdElement === null) { - throw new Error('No DOM element with id \'' + gd + '\' exists on the page.'); - } - - return gdElement; - } - else if(gd === null || gd === undefined) { - throw new Error('DOM element provided is null or undefined'); + if (gdElement === null) { + throw new Error( + "No DOM element with id '" + gd + "' exists on the page." + ); } - return gd; // otherwise assume that gd is a DOM element + return gdElement; + } else if (gd === null || gd === undefined) { + throw new Error("DOM element provided is null or undefined"); + } + + return gd; // otherwise assume that gd is a DOM element }; // clear the promise queue if one of them got rejected exports.clearPromiseQueue = function(gd) { - if(Array.isArray(gd._promises) && gd._promises.length > 0) { - Lib.log('Clearing previous rejected promises from queue.'); - } + if (Array.isArray(gd._promises) && gd._promises.length > 0) { + Lib.log("Clearing previous rejected promises from queue."); + } - gd._promises = []; + gd._promises = []; }; // make a few changes to the layout right away // before it gets used for anything // backward compatibility and cleanup of nonstandard options exports.cleanLayout = function(layout) { - var i, j; - - if(!layout) layout = {}; + var i, j; + + if (!layout) layout = {}; + + // cannot have (x|y)axis1, numbering goes axis, axis2, axis3... + if (layout.xaxis1) { + if (!layout.xaxis) layout.xaxis = layout.xaxis1; + delete layout.xaxis1; + } + if (layout.yaxis1) { + if (!layout.yaxis) layout.yaxis = layout.yaxis1; + delete layout.yaxis1; + } + + var axList = Axes.list({ _fullLayout: layout }); + for (i = 0; i < axList.length; i++) { + var ax = axList[i]; + if (ax.anchor && ax.anchor !== "free") { + ax.anchor = Axes.cleanId(ax.anchor); + } + if (ax.overlaying) ax.overlaying = Axes.cleanId(ax.overlaying); - // cannot have (x|y)axis1, numbering goes axis, axis2, axis3... - if(layout.xaxis1) { - if(!layout.xaxis) layout.xaxis = layout.xaxis1; - delete layout.xaxis1; + // old method of axis type - isdate and islog (before category existed) + if (!ax.type) { + if (ax.isdate) ax.type = "date"; + else if (ax.islog) ax.type = "log"; + else if (ax.isdate === false && ax.islog === false) ax.type = "linear"; } - if(layout.yaxis1) { - if(!layout.yaxis) layout.yaxis = layout.yaxis1; - delete layout.yaxis1; + if (ax.autorange === "withzero" || ax.autorange === "tozero") { + ax.autorange = true; + ax.rangemode = "tozero"; } - - var axList = Axes.list({_fullLayout: layout}); - for(i = 0; i < axList.length; i++) { - var ax = axList[i]; - if(ax.anchor && ax.anchor !== 'free') { - ax.anchor = Axes.cleanId(ax.anchor); - } - if(ax.overlaying) ax.overlaying = Axes.cleanId(ax.overlaying); - - // old method of axis type - isdate and islog (before category existed) - if(!ax.type) { - if(ax.isdate) ax.type = 'date'; - else if(ax.islog) ax.type = 'log'; - else if(ax.isdate === false && ax.islog === false) ax.type = 'linear'; - } - if(ax.autorange === 'withzero' || ax.autorange === 'tozero') { - ax.autorange = true; - ax.rangemode = 'tozero'; - } - delete ax.islog; - delete ax.isdate; - delete ax.categories; // replaced by _categories - - // prune empty domain arrays made before the new nestedProperty - if(emptyContainer(ax, 'domain')) delete ax.domain; - - // autotick -> tickmode - if(ax.autotick !== undefined) { - if(ax.tickmode === undefined) { - ax.tickmode = ax.autotick ? 'auto' : 'linear'; - } - delete ax.autotick; - } + delete ax.islog; + delete ax.isdate; + delete ax.categories; + + // replaced by _categories + // prune empty domain arrays made before the new nestedProperty + if (emptyContainer(ax, "domain")) delete ax.domain; + + // autotick -> tickmode + if (ax.autotick !== undefined) { + if (ax.tickmode === undefined) { + ax.tickmode = ax.autotick ? "auto" : "linear"; + } + delete ax.autotick; } - - var annotationsLen = Array.isArray(layout.annotations) ? layout.annotations.length : 0; - for(i = 0; i < annotationsLen; i++) { - var ann = layout.annotations[i]; - - if(!Lib.isPlainObject(ann)) continue; - - if(ann.ref) { - if(ann.ref === 'paper') { - ann.xref = 'paper'; - ann.yref = 'paper'; - } - else if(ann.ref === 'data') { - ann.xref = 'x'; - ann.yref = 'y'; - } - delete ann.ref; - } - - cleanAxRef(ann, 'xref'); - cleanAxRef(ann, 'yref'); + } + + var annotationsLen = Array.isArray(layout.annotations) + ? layout.annotations.length + : 0; + for (i = 0; i < annotationsLen; i++) { + var ann = layout.annotations[i]; + + if (!Lib.isPlainObject(ann)) continue; + + if (ann.ref) { + if (ann.ref === "paper") { + ann.xref = "paper"; + ann.yref = "paper"; + } else if (ann.ref === "data") { + ann.xref = "x"; + ann.yref = "y"; + } + delete ann.ref; } - var shapesLen = Array.isArray(layout.shapes) ? layout.shapes.length : 0; - for(i = 0; i < shapesLen; i++) { - var shape = layout.shapes[i]; - - if(!Lib.isPlainObject(shape)) continue; - - cleanAxRef(shape, 'xref'); - cleanAxRef(shape, 'yref'); + cleanAxRef(ann, "xref"); + cleanAxRef(ann, "yref"); + } + + var shapesLen = Array.isArray(layout.shapes) ? layout.shapes.length : 0; + for (i = 0; i < shapesLen; i++) { + var shape = layout.shapes[i]; + + if (!Lib.isPlainObject(shape)) continue; + + cleanAxRef(shape, "xref"); + cleanAxRef(shape, "yref"); + } + + var legend = layout.legend; + if (legend) { + // check for old-style legend positioning (x or y is +/- 100) + if (legend.x > 3) { + legend.x = 1.02; + legend.xanchor = "left"; + } else if (legend.x < -2) { + legend.x = -0.02; + legend.xanchor = "right"; } - var legend = layout.legend; - if(legend) { - // check for old-style legend positioning (x or y is +/- 100) - if(legend.x > 3) { - legend.x = 1.02; - legend.xanchor = 'left'; - } - else if(legend.x < -2) { - legend.x = -0.02; - legend.xanchor = 'right'; - } - - if(legend.y > 3) { - legend.y = 1.02; - legend.yanchor = 'bottom'; - } - else if(legend.y < -2) { - legend.y = -0.02; - legend.yanchor = 'top'; - } + if (legend.y > 3) { + legend.y = 1.02; + legend.yanchor = "bottom"; + } else if (legend.y < -2) { + legend.y = -0.02; + legend.yanchor = "top"; } + } - /* + /* * Moved from rotate -> orbit for dragmode */ - if(layout.dragmode === 'rotate') layout.dragmode = 'orbit'; + if (layout.dragmode === "rotate") layout.dragmode = "orbit"; - // cannot have scene1, numbering goes scene, scene2, scene3... - if(layout.scene1) { - if(!layout.scene) layout.scene = layout.scene1; - delete layout.scene1; - } + // cannot have scene1, numbering goes scene, scene2, scene3... + if (layout.scene1) { + if (!layout.scene) layout.scene = layout.scene1; + delete layout.scene1; + } - /* + /* * Clean up Scene layouts */ - var sceneIds = Plots.getSubplotIds(layout, 'gl3d'); - for(i = 0; i < sceneIds.length; i++) { - var scene = layout[sceneIds[i]]; - - // clean old Camera coords - var cameraposition = scene.cameraposition; - if(Array.isArray(cameraposition) && cameraposition[0].length === 4) { - var rotation = cameraposition[0], - center = cameraposition[1], - radius = cameraposition[2], - mat = m4FromQuat([], rotation), - eye = []; - - for(j = 0; j < 3; ++j) { - eye[j] = center[i] + radius * mat[2 + 4 * j]; - } - - scene.camera = { - eye: {x: eye[0], y: eye[1], z: eye[2]}, - center: {x: center[0], y: center[1], z: center[2]}, - up: {x: mat[1], y: mat[5], z: mat[9]} - }; - - delete scene.cameraposition; - } + var sceneIds = Plots.getSubplotIds(layout, "gl3d"); + for (i = 0; i < sceneIds.length; i++) { + var scene = layout[sceneIds[i]]; + + // clean old Camera coords + var cameraposition = scene.cameraposition; + if (Array.isArray(cameraposition) && cameraposition[0].length === 4) { + var rotation = cameraposition[0], + center = cameraposition[1], + radius = cameraposition[2], + mat = m4FromQuat([], rotation), + eye = []; + + for (j = 0; j < 3; ++j) { + eye[j] = center[i] + radius * mat[2 + 4 * j]; + } + + scene.camera = { + eye: { x: eye[0], y: eye[1], z: eye[2] }, + center: { x: center[0], y: center[1], z: center[2] }, + up: { x: mat[1], y: mat[5], z: mat[9] } + }; + + delete scene.cameraposition; } + } - // sanitize rgb(fractions) and rgba(fractions) that old tinycolor - // supported, but new tinycolor does not because they're not valid css - Color.clean(layout); + // sanitize rgb(fractions) and rgba(fractions) that old tinycolor + // supported, but new tinycolor does not because they're not valid css + Color.clean(layout); - return layout; + return layout; }; function cleanAxRef(container, attr) { - var valIn = container[attr], - axLetter = attr.charAt(0); - if(valIn && valIn !== 'paper') { - container[attr] = Axes.cleanId(valIn, axLetter); - } + var valIn = container[attr], axLetter = attr.charAt(0); + if (valIn && valIn !== "paper") { + container[attr] = Axes.cleanId(valIn, axLetter); + } } // Make a few changes to the data right away // before it gets used for anything exports.cleanData = function(data, existingData) { + // Enforce unique IDs + var suids = [], + // seen uids --- so we can weed out incoming repeats + uids = data + .concat(Array.isArray(existingData) ? existingData : []) + .filter(function(trace) { + return "uid" in trace; + }) + .map(function(trace) { + return trace.uid; + }); + + for (var tracei = 0; tracei < data.length; tracei++) { + var trace = data[tracei]; + var i; - // Enforce unique IDs - var suids = [], // seen uids --- so we can weed out incoming repeats - uids = data.concat(Array.isArray(existingData) ? existingData : []) - .filter(function(trace) { return 'uid' in trace; }) - .map(function(trace) { return trace.uid; }); + // assign uids to each trace and detect collisions. + if (!("uid" in trace) || suids.indexOf(trace.uid) !== -1) { + var newUid; - for(var tracei = 0; tracei < data.length; tracei++) { - var trace = data[tracei]; - var i; + for (i = 0; i < 100; i++) { + newUid = Lib.randstr(uids); + if (suids.indexOf(newUid) === -1) break; + } + trace.uid = Lib.randstr(uids); + uids.push(trace.uid); + } + // keep track of already seen uids, so that if there are + // doubles we force the trace with a repeat uid to + // acquire a new one + suids.push(trace.uid); + + // BACKWARD COMPATIBILITY FIXES + // use xbins to bin data in x, and ybins to bin data in y + if ( + trace.type === "histogramy" && "xbins" in trace && !("ybins" in trace) + ) { + trace.ybins = trace.xbins; + delete trace.xbins; + } - // assign uids to each trace and detect collisions. - if(!('uid' in trace) || suids.indexOf(trace.uid) !== -1) { - var newUid; + // error_y.opacity is obsolete - merge into color + if (trace.error_y && "opacity" in trace.error_y) { + var dc = Color.defaults, + yeColor = trace.error_y.color || + (Registry.traceIs(trace, "bar") + ? Color.defaultLine + : dc[tracei % dc.length]); + trace.error_y.color = Color.addOpacity( + Color.rgb(yeColor), + Color.opacity(yeColor) * trace.error_y.opacity + ); + delete trace.error_y.opacity; + } - for(i = 0; i < 100; i++) { - newUid = Lib.randstr(uids); - if(suids.indexOf(newUid) === -1) break; - } - trace.uid = Lib.randstr(uids); - uids.push(trace.uid); - } - // keep track of already seen uids, so that if there are - // doubles we force the trace with a repeat uid to - // acquire a new one - suids.push(trace.uid); + // convert bardir to orientation, and put the data into + // the axes it's eventually going to be used with + if ("bardir" in trace) { + if ( + trace.bardir === "h" && + (Registry.traceIs(trace, "bar") || + trace.type.substr(0, 9) === "histogram") + ) { + trace.orientation = "h"; + exports.swapXYData(trace); + } + delete trace.bardir; + } - // BACKWARD COMPATIBILITY FIXES + // now we have only one 1D histogram type, and whether + // it uses x or y data depends on trace.orientation + if (trace.type === "histogramy") exports.swapXYData(trace); + if (trace.type === "histogramx" || trace.type === "histogramy") { + trace.type = "histogram"; + } - // use xbins to bin data in x, and ybins to bin data in y - if(trace.type === 'histogramy' && 'xbins' in trace && !('ybins' in trace)) { - trace.ybins = trace.xbins; - delete trace.xbins; - } + // scl->scale, reversescl->reversescale + if ("scl" in trace) { + trace.colorscale = trace.scl; + delete trace.scl; + } + if ("reversescl" in trace) { + trace.reversescale = trace.reversescl; + delete trace.reversescl; + } - // error_y.opacity is obsolete - merge into color - if(trace.error_y && 'opacity' in trace.error_y) { - var dc = Color.defaults, - yeColor = trace.error_y.color || - (Registry.traceIs(trace, 'bar') ? Color.defaultLine : dc[tracei % dc.length]); - trace.error_y.color = Color.addOpacity( - Color.rgb(yeColor), - Color.opacity(yeColor) * trace.error_y.opacity); - delete trace.error_y.opacity; - } + // axis ids x1 -> x, y1-> y + if (trace.xaxis) trace.xaxis = Axes.cleanId(trace.xaxis, "x"); + if (trace.yaxis) trace.yaxis = Axes.cleanId(trace.yaxis, "y"); - // convert bardir to orientation, and put the data into - // the axes it's eventually going to be used with - if('bardir' in trace) { - if(trace.bardir === 'h' && (Registry.traceIs(trace, 'bar') || - trace.type.substr(0, 9) === 'histogram')) { - trace.orientation = 'h'; - exports.swapXYData(trace); - } - delete trace.bardir; - } + // scene ids scene1 -> scene + if (Registry.traceIs(trace, "gl3d") && trace.scene) { + trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene); + } - // now we have only one 1D histogram type, and whether - // it uses x or y data depends on trace.orientation - if(trace.type === 'histogramy') exports.swapXYData(trace); - if(trace.type === 'histogramx' || trace.type === 'histogramy') { - trace.type = 'histogram'; - } + if (!Registry.traceIs(trace, "pie") && !Registry.traceIs(trace, "bar")) { + if (Array.isArray(trace.textposition)) { + trace.textposition = trace.textposition.map(cleanTextPosition); + } else if (trace.textposition) { + trace.textposition = cleanTextPosition(trace.textposition); + } + } - // scl->scale, reversescl->reversescale - if('scl' in trace) { - trace.colorscale = trace.scl; - delete trace.scl; - } - if('reversescl' in trace) { - trace.reversescale = trace.reversescl; - delete trace.reversescl; - } + // fix typo in colorscale definition + if (Registry.traceIs(trace, "2dMap")) { + if (trace.colorscale === "YIGnBu") trace.colorscale = "YlGnBu"; + if (trace.colorscale === "YIOrRd") trace.colorscale = "YlOrRd"; + } + if (Registry.traceIs(trace, "markerColorscale") && trace.marker) { + var cont = trace.marker; + if (cont.colorscale === "YIGnBu") cont.colorscale = "YlGnBu"; + if (cont.colorscale === "YIOrRd") cont.colorscale = "YlOrRd"; + } - // axis ids x1 -> x, y1-> y - if(trace.xaxis) trace.xaxis = Axes.cleanId(trace.xaxis, 'x'); - if(trace.yaxis) trace.yaxis = Axes.cleanId(trace.yaxis, 'y'); + // fix typo in surface 'highlight*' definitions + if (trace.type === "surface" && Lib.isPlainObject(trace.contours)) { + var dims = ["x", "y", "z"]; - // scene ids scene1 -> scene - if(Registry.traceIs(trace, 'gl3d') && trace.scene) { - trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene); - } + for (i = 0; i < dims.length; i++) { + var opts = trace.contours[dims[i]]; - if(!Registry.traceIs(trace, 'pie') && !Registry.traceIs(trace, 'bar')) { - if(Array.isArray(trace.textposition)) { - trace.textposition = trace.textposition.map(cleanTextPosition); - } - else if(trace.textposition) { - trace.textposition = cleanTextPosition(trace.textposition); - } - } + if (!Lib.isPlainObject(opts)) continue; - // fix typo in colorscale definition - if(Registry.traceIs(trace, '2dMap')) { - if(trace.colorscale === 'YIGnBu') trace.colorscale = 'YlGnBu'; - if(trace.colorscale === 'YIOrRd') trace.colorscale = 'YlOrRd'; + if (opts.highlightColor) { + opts.highlightcolor = opts.highlightColor; + delete opts.highlightColor; } - if(Registry.traceIs(trace, 'markerColorscale') && trace.marker) { - var cont = trace.marker; - if(cont.colorscale === 'YIGnBu') cont.colorscale = 'YlGnBu'; - if(cont.colorscale === 'YIOrRd') cont.colorscale = 'YlOrRd'; - } - - // fix typo in surface 'highlight*' definitions - if(trace.type === 'surface' && Lib.isPlainObject(trace.contours)) { - var dims = ['x', 'y', 'z']; - - for(i = 0; i < dims.length; i++) { - var opts = trace.contours[dims[i]]; - if(!Lib.isPlainObject(opts)) continue; - - if(opts.highlightColor) { - opts.highlightcolor = opts.highlightColor; - delete opts.highlightColor; - } - - if(opts.highlightWidth) { - opts.highlightwidth = opts.highlightWidth; - delete opts.highlightWidth; - } - } + if (opts.highlightWidth) { + opts.highlightwidth = opts.highlightWidth; + delete opts.highlightWidth; } + } + } - // transforms backward compatibility fixes - if(Array.isArray(trace.transforms)) { - var transforms = trace.transforms; + // transforms backward compatibility fixes + if (Array.isArray(trace.transforms)) { + var transforms = trace.transforms; - for(i = 0; i < transforms.length; i++) { - var transform = transforms[i]; + for (i = 0; i < transforms.length; i++) { + var transform = transforms[i]; - if(!Lib.isPlainObject(transform)) continue; + if (!Lib.isPlainObject(transform)) continue; - if(transform.type === 'filter') { - if(transform.filtersrc) { - transform.target = transform.filtersrc; - delete transform.filtersrc; - } + if (transform.type === "filter") { + if (transform.filtersrc) { + transform.target = transform.filtersrc; + delete transform.filtersrc; + } - if(transform.calendar) { - if(!transform.valuecalendar) { - transform.valuecalendar = transform.calendar; - } - delete transform.calendar; - } - } + if (transform.calendar) { + if (!transform.valuecalendar) { + transform.valuecalendar = transform.calendar; } + delete transform.calendar; + } } + } + } - // prune empty containers made before the new nestedProperty - if(emptyContainer(trace, 'line')) delete trace.line; - if('marker' in trace) { - if(emptyContainer(trace.marker, 'line')) delete trace.marker.line; - if(emptyContainer(trace, 'marker')) delete trace.marker; - } - - // sanitize rgb(fractions) and rgba(fractions) that old tinycolor - // supported, but new tinycolor does not because they're not valid css - Color.clean(trace); + // prune empty containers made before the new nestedProperty + if (emptyContainer(trace, "line")) delete trace.line; + if ("marker" in trace) { + if (emptyContainer(trace.marker, "line")) delete trace.marker.line; + if (emptyContainer(trace, "marker")) delete trace.marker; } + + // sanitize rgb(fractions) and rgba(fractions) that old tinycolor + // supported, but new tinycolor does not because they're not valid css + Color.clean(trace); + } }; // textposition - support partial attributes (ie just 'top') // and incorrect use of middle / center etc. function cleanTextPosition(textposition) { - var posY = 'middle', - posX = 'center'; - if(textposition.indexOf('top') !== -1) posY = 'top'; - else if(textposition.indexOf('bottom') !== -1) posY = 'bottom'; + var posY = "middle", posX = "center"; + if (textposition.indexOf("top") !== -1) posY = "top"; + else if (textposition.indexOf("bottom") !== -1) posY = "bottom"; - if(textposition.indexOf('left') !== -1) posX = 'left'; - else if(textposition.indexOf('right') !== -1) posX = 'right'; + if (textposition.indexOf("left") !== -1) posX = "left"; + else if (textposition.indexOf("right") !== -1) posX = "right"; - return posY + ' ' + posX; + return posY + " " + posX; } function emptyContainer(outer, innerStr) { - return (innerStr in outer) && - (typeof outer[innerStr] === 'object') && - (Object.keys(outer[innerStr]).length === 0); + return innerStr in outer && + typeof outer[innerStr] === "object" && + Object.keys(outer[innerStr]).length === 0; } - // swap all the data and data attributes associated with x and y exports.swapXYData = function(trace) { - var i; - Lib.swapAttrs(trace, ['?', '?0', 'd?', '?bins', 'nbins?', 'autobin?', '?src', 'error_?']); - if(Array.isArray(trace.z) && Array.isArray(trace.z[0])) { - if(trace.transpose) delete trace.transpose; - else trace.transpose = true; + var i; + Lib.swapAttrs(trace, [ + "?", + "?0", + "d?", + "?bins", + "nbins?", + "autobin?", + "?src", + "error_?" + ]); + if (Array.isArray(trace.z) && Array.isArray(trace.z[0])) { + if (trace.transpose) delete trace.transpose; + else trace.transpose = true; + } + if (trace.error_x && trace.error_y) { + var errorY = trace.error_y, + copyYstyle = "copy_ystyle" in errorY + ? errorY.copy_ystyle + : !(errorY.color || errorY.thickness || errorY.width); + Lib.swapAttrs(trace, ["error_?.copy_ystyle"]); + if (copyYstyle) { + Lib.swapAttrs(trace, [ + "error_?.color", + "error_?.thickness", + "error_?.width" + ]); } - if(trace.error_x && trace.error_y) { - var errorY = trace.error_y, - copyYstyle = ('copy_ystyle' in errorY) ? errorY.copy_ystyle : - !(errorY.color || errorY.thickness || errorY.width); - Lib.swapAttrs(trace, ['error_?.copy_ystyle']); - if(copyYstyle) { - Lib.swapAttrs(trace, ['error_?.color', 'error_?.thickness', 'error_?.width']); - } - } - if(trace.hoverinfo) { - var hoverInfoParts = trace.hoverinfo.split('+'); - for(i = 0; i < hoverInfoParts.length; i++) { - if(hoverInfoParts[i] === 'x') hoverInfoParts[i] = 'y'; - else if(hoverInfoParts[i] === 'y') hoverInfoParts[i] = 'x'; - } - trace.hoverinfo = hoverInfoParts.join('+'); + } + if (trace.hoverinfo) { + var hoverInfoParts = trace.hoverinfo.split("+"); + for (i = 0; i < hoverInfoParts.length; i++) { + if (hoverInfoParts[i] === "x") hoverInfoParts[i] = "y"; + else if (hoverInfoParts[i] === "y") hoverInfoParts[i] = "x"; } + trace.hoverinfo = hoverInfoParts.join("+"); + } }; // coerce traceIndices input to array of trace indices exports.coerceTraceIndices = function(gd, traceIndices) { - if(isNumeric(traceIndices)) { - return [traceIndices]; - } - else if(!Array.isArray(traceIndices) || !traceIndices.length) { - return gd.data.map(function(_, i) { return i; }); - } - - return traceIndices; + if (isNumeric(traceIndices)) { + return [traceIndices]; + } else if (!Array.isArray(traceIndices) || !traceIndices.length) { + return gd.data.map(function(_, i) { + return i; + }); + } + + return traceIndices; }; /** @@ -450,37 +470,31 @@ exports.coerceTraceIndices = function(gd, traceIndices) { * */ exports.manageArrayContainers = function(np, newVal, undoit) { - var obj = np.obj, - parts = np.parts, - pLength = parts.length, - pLast = parts[pLength - 1]; - - var pLastIsNumber = isNumeric(pLast); - - // delete item - if(pLastIsNumber && newVal === null) { - - // Clear item in array container when new value is null - var contPath = parts.slice(0, pLength - 1).join('.'), - cont = Lib.nestedProperty(obj, contPath).get(); - cont.splice(pLast, 1); - - // Note that nested property clears null / undefined at end of - // array container, but not within them. - } + var obj = np.obj, + parts = np.parts, + pLength = parts.length, + pLast = parts[pLength - 1]; + + var pLastIsNumber = isNumeric(pLast); + + // delete item + if (pLastIsNumber && newVal === null) { + // Clear item in array container when new value is null + var contPath = parts.slice(0, pLength - 1).join("."), + cont = Lib.nestedProperty(obj, contPath).get(); + cont.splice(pLast, 1); + // Note that nested property clears null / undefined at end of + // array container, but not within them. + } else if (pLastIsNumber && np.get() === undefined) { // create item - else if(pLastIsNumber && np.get() === undefined) { - - // When adding a new item, make sure undo command will remove it - if(np.get() === undefined) undoit[np.astr] = null; + // When adding a new item, make sure undo command will remove it + if (np.get() === undefined) undoit[np.astr] = null; - np.set(newVal); - } + np.set(newVal); + } else { // update item - else { - - // If the last part of attribute string isn't a number, - // np.set is all we need. - np.set(newVal); - } + // If the last part of attribute string isn't a number, + // np.set is all we need. + np.set(newVal); + } }; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 3f5f80444d5..7a6d6062308 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -5,32 +5,27 @@ * 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 d3 = require("d3"); +var isNumeric = require("fast-isnumeric"); +var Plotly = require("../plotly"); +var Lib = require("../lib"); +var Events = require("../lib/events"); +var Queue = require("../lib/queue"); -'use strict'; +var Registry = require("../registry"); +var Plots = require("../plots/plots"); +var Fx = require("../plots/cartesian/graph_interact"); +var Polar = require("../plots/polar"); +var Drawing = require("../components/drawing"); +var ErrorBars = require("../components/errorbars"); +var xmlnsNamespaces = require("../constants/xmlns_namespaces"); +var svgTextUtils = require("../lib/svg_text_utils"); -var d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var Plotly = require('../plotly'); -var Lib = require('../lib'); -var Events = require('../lib/events'); -var Queue = require('../lib/queue'); - -var Registry = require('../registry'); -var Plots = require('../plots/plots'); -var Fx = require('../plots/cartesian/graph_interact'); -var Polar = require('../plots/polar'); - -var Drawing = require('../components/drawing'); -var ErrorBars = require('../components/errorbars'); -var xmlnsNamespaces = require('../constants/xmlns_namespaces'); -var svgTextUtils = require('../lib/svg_text_utils'); - -var helpers = require('./helpers'); -var subroutines = require('./subroutines'); - +var helpers = require("./helpers"); +var subroutines = require("./subroutines"); /** * Main plot-creation function @@ -47,465 +42,468 @@ var subroutines = require('./subroutines'); * */ Plotly.plot = function(gd, data, layout, config) { - var frames; - - gd = helpers.getGraphDiv(gd); - - // Events.init is idempotent and bails early if gd has already been init'd - Events.init(gd); - - if(Lib.isPlainObject(data)) { - var obj = data; - data = obj.data; - layout = obj.layout; - config = obj.config; - frames = obj.frames; - } - - var okToPlot = Events.triggerHandler(gd, 'plotly_beforeplot', [data, layout, config]); - if(okToPlot === false) return Promise.reject(); - - // if there's no data or layout, and this isn't yet a plotly plot - // container, log a warning to help plotly.js users debug - if(!data && !layout && !Lib.isPlotDiv(gd)) { - Lib.warn('Calling Plotly.plot as if redrawing ' + - 'but this container doesn\'t yet have a plot.', gd); - } + var frames; + + gd = helpers.getGraphDiv(gd); + + // Events.init is idempotent and bails early if gd has already been init'd + Events.init(gd); + + if (Lib.isPlainObject(data)) { + var obj = data; + data = obj.data; + layout = obj.layout; + config = obj.config; + frames = obj.frames; + } + + var okToPlot = Events.triggerHandler(gd, "plotly_beforeplot", [ + data, + layout, + config + ]); + if (okToPlot === false) return Promise.reject(); + + // if there's no data or layout, and this isn't yet a plotly plot + // container, log a warning to help plotly.js users debug + if (!data && !layout && !Lib.isPlotDiv(gd)) { + Lib.warn( + "Calling Plotly.plot as if redrawing " + + "but this container doesn't yet have a plot.", + gd + ); + } - function addFrames() { - if(frames) { - return Plotly.addFrames(gd, frames); - } + function addFrames() { + if (frames) { + return Plotly.addFrames(gd, frames); } + } - // transfer configuration options to gd until we move over to - // a more OO like model - setPlotContext(gd, config); + // transfer configuration options to gd until we move over to + // a more OO like model + setPlotContext(gd, config); - if(!layout) layout = {}; + if (!layout) layout = {}; - // hook class for plots main container (in case of plotly.js - // this won't be #embedded-graph or .js-tab-contents) - d3.select(gd).classed('js-plotly-plot', true); + // hook class for plots main container (in case of plotly.js + // this won't be #embedded-graph or .js-tab-contents) + d3.select(gd).classed("js-plotly-plot", true); - // off-screen getBoundingClientRect testing space, - // in #js-plotly-tester (and stored as gd._tester) - // so we can share cached text across tabs - Drawing.makeTester(gd); + // off-screen getBoundingClientRect testing space, + // in #js-plotly-tester (and stored as gd._tester) + // so we can share cached text across tabs + Drawing.makeTester(gd); - // collect promises for any async actions during plotting - // any part of the plotting code can push to gd._promises, then - // before we move to the next step, we check that they're all - // complete, and empty out the promise list again. - gd._promises = []; + // collect promises for any async actions during plotting + // any part of the plotting code can push to gd._promises, then + // before we move to the next step, we check that they're all + // complete, and empty out the promise list again. + gd._promises = []; - var graphWasEmpty = ((gd.data || []).length === 0 && Array.isArray(data)); + var graphWasEmpty = (gd.data || []).length === 0 && Array.isArray(data); - // if there is already data on the graph, append the new data - // if you only want to redraw, pass a non-array for data - if(Array.isArray(data)) { - helpers.cleanData(data, gd.data); + // if there is already data on the graph, append the new data + // if you only want to redraw, pass a non-array for data + if (Array.isArray(data)) { + helpers.cleanData(data, gd.data); - if(graphWasEmpty) gd.data = data; - else gd.data.push.apply(gd.data, data); - - // for routines outside graph_obj that want a clean tab - // (rather than appending to an existing one) gd.empty - // is used to determine whether to make a new tab - gd.empty = false; - } + if (graphWasEmpty) gd.data = data; + else gd.data.push.apply(gd.data, data); - if(!gd.layout || graphWasEmpty) gd.layout = helpers.cleanLayout(layout); + // for routines outside graph_obj that want a clean tab + // (rather than appending to an existing one) gd.empty + // is used to determine whether to make a new tab + gd.empty = false; + } - // if the user is trying to drag the axes, allow new data and layout - // to come in but don't allow a replot. - if(gd._dragging && !gd._transitioning) { - // signal to drag handler that after everything else is done - // we need to replot, because something has changed - gd._replotPending = true; - return Promise.reject(); - } else { - // we're going ahead with a replot now - gd._replotPending = false; - } + if (!gd.layout || graphWasEmpty) gd.layout = helpers.cleanLayout(layout); - Plots.supplyDefaults(gd); + // if the user is trying to drag the axes, allow new data and layout + // to come in but don't allow a replot. + if (gd._dragging && !gd._transitioning) { + // signal to drag handler that after everything else is done + // we need to replot, because something has changed + gd._replotPending = true; + return Promise.reject(); + } else { + // we're going ahead with a replot now + gd._replotPending = false; + } - // Polar plots - if(data && data[0] && data[0].r) return plotPolar(gd, data, layout); + Plots.supplyDefaults(gd); - // so we don't try to re-call Plotly.plot from inside - // legend and colorbar, if margins changed - gd._replotting = true; + // Polar plots + if (data && data[0] && data[0].r) return plotPolar(gd, data, layout); - // make or remake the framework if we need to - if(graphWasEmpty) makePlotFramework(gd); + // so we don't try to re-call Plotly.plot from inside + // legend and colorbar, if margins changed + gd._replotting = true; - // polar need a different framework - if(gd.framework !== makePlotFramework) { - gd.framework = makePlotFramework; - makePlotFramework(gd); - } + // make or remake the framework if we need to + if (graphWasEmpty) makePlotFramework(gd); - // save initial axis range once per graph - if(graphWasEmpty) Plotly.Axes.saveRangeInitial(gd); + // polar need a different framework + if (gd.framework !== makePlotFramework) { + gd.framework = makePlotFramework; + makePlotFramework(gd); + } - var fullLayout = gd._fullLayout; + // save initial axis range once per graph + if (graphWasEmpty) Plotly.Axes.saveRangeInitial(gd); - // prepare the data and find the autorange + var fullLayout = gd._fullLayout; - // generate calcdata, if we need to - // to force redoing calcdata, just delete it before calling Plotly.plot - var recalc = !gd.calcdata || gd.calcdata.length !== (gd._fullData || []).length; - if(recalc) Plots.doCalcdata(gd); + // prepare the data and find the autorange + // generate calcdata, if we need to + // to force redoing calcdata, just delete it before calling Plotly.plot + var recalc = !gd.calcdata || + gd.calcdata.length !== (gd._fullData || []).length; + if (recalc) Plots.doCalcdata(gd); - // in case it has changed, attach fullData traces to calcdata - for(var i = 0; i < gd.calcdata.length; i++) { - gd.calcdata[i][0].trace = gd._fullData[i]; - } + // in case it has changed, attach fullData traces to calcdata + for (var i = 0; i < gd.calcdata.length; i++) { + gd.calcdata[i][0].trace = gd._fullData[i]; + } - /* + /* * start async-friendly code - now we're actually drawing things */ + var oldmargins = JSON.stringify(fullLayout._size); + + // draw framework first so that margin-pushing + // components can position themselves correctly + function drawFramework() { + var basePlotModules = fullLayout._basePlotModules; + + for (var i = 0; i < basePlotModules.length; i++) { + if (basePlotModules[i].drawFramework) { + basePlotModules[i].drawFramework(gd); + } + } + + return Lib.syncOrAsync([subroutines.layoutStyles, drawAxes, Fx.init], gd); + } + + // draw anything that can affect margins. + // currently this is legend and colorbars + function marginPushers() { + var calcdata = gd.calcdata; + var i, cd, trace; + + Registry.getComponentMethod("legend", "draw")(gd); + Registry.getComponentMethod("rangeselector", "draw")(gd); + Registry.getComponentMethod("sliders", "draw")(gd); + Registry.getComponentMethod("updatemenus", "draw")(gd); + + for (i = 0; i < calcdata.length; i++) { + cd = calcdata[i]; + trace = cd[0].trace; + if (trace.visible !== true || !trace._module.colorbar) { + Plots.autoMargin(gd, "cb" + trace.uid); + } else { + trace._module.colorbar(gd, cd); + } + } + + Plots.doAutoMargin(gd); + return Plots.previousPromises(gd); + } + + // in case the margins changed, draw margin pushers again + function marginPushersAgain() { + var seq = JSON.stringify(fullLayout._size) === oldmargins + ? [] + : [marginPushers, subroutines.layoutStyles]; + + // re-initialize cartesian interaction, + // which are sometimes cleared during marginPushers + seq = seq.concat(Fx.init); + + return Lib.syncOrAsync(seq, gd); + } + + function positionAndAutorange() { + if (!recalc) return; + + var subplots = Plots.getSubplotIds(fullLayout, "cartesian"), + modules = fullLayout._modules; + + // position and range calculations for traces that + // depend on each other ie bars (stacked or grouped) + // and boxes (grouped) push each other out of the way + var subplotInfo, _module; + + for (var i = 0; i < subplots.length; i++) { + subplotInfo = fullLayout._plots[subplots[i]]; + + for (var j = 0; j < modules.length; j++) { + _module = modules[j]; + if (_module.setPositions) _module.setPositions(gd, subplotInfo); + } + } + + // calc and autorange for errorbars + ErrorBars.calc(gd); + + // TODO: autosize extra for text markers + return Lib.syncOrAsync( + [ + Registry.getComponentMethod("shapes", "calcAutorange"), + Registry.getComponentMethod("annotations", "calcAutorange"), + doAutoRange + ], + gd + ); + } - var oldmargins = JSON.stringify(fullLayout._size); - - // draw framework first so that margin-pushing - // components can position themselves correctly - function drawFramework() { - var basePlotModules = fullLayout._basePlotModules; - - for(var i = 0; i < basePlotModules.length; i++) { - if(basePlotModules[i].drawFramework) { - basePlotModules[i].drawFramework(gd); - } - } - - return Lib.syncOrAsync([ - subroutines.layoutStyles, - drawAxes, - Fx.init - ], gd); - } - - // draw anything that can affect margins. - // currently this is legend and colorbars - function marginPushers() { - var calcdata = gd.calcdata; - var i, cd, trace; - - Registry.getComponentMethod('legend', 'draw')(gd); - Registry.getComponentMethod('rangeselector', 'draw')(gd); - Registry.getComponentMethod('sliders', 'draw')(gd); - Registry.getComponentMethod('updatemenus', 'draw')(gd); - - for(i = 0; i < calcdata.length; i++) { - cd = calcdata[i]; - trace = cd[0].trace; - if(trace.visible !== true || !trace._module.colorbar) { - Plots.autoMargin(gd, 'cb' + trace.uid); - } - else trace._module.colorbar(gd, cd); - } - - Plots.doAutoMargin(gd); - return Plots.previousPromises(gd); - } - - // in case the margins changed, draw margin pushers again - function marginPushersAgain() { - var seq = JSON.stringify(fullLayout._size) === oldmargins ? - [] : - [marginPushers, subroutines.layoutStyles]; - - // re-initialize cartesian interaction, - // which are sometimes cleared during marginPushers - seq = seq.concat(Fx.init); + function doAutoRange() { + if (gd._transitioning) return; - return Lib.syncOrAsync(seq, gd); + var axList = Plotly.Axes.list(gd, "", true); + for (var i = 0; i < axList.length; i++) { + Plotly.Axes.doAutoRange(axList[i]); } + } - function positionAndAutorange() { - if(!recalc) return; + // draw ticks, titles, and calculate axis scaling (._b, ._m) + function drawAxes() { + return Plotly.Axes.doTicks(gd, "redraw"); + } - var subplots = Plots.getSubplotIds(fullLayout, 'cartesian'), - modules = fullLayout._modules; + // Now plot the data + function drawData() { + var calcdata = gd.calcdata, i; - // position and range calculations for traces that - // depend on each other ie bars (stacked or grouped) - // and boxes (grouped) push each other out of the way - - var subplotInfo, _module; - - for(var i = 0; i < subplots.length; i++) { - subplotInfo = fullLayout._plots[subplots[i]]; - - for(var j = 0; j < modules.length; j++) { - _module = modules[j]; - if(_module.setPositions) _module.setPositions(gd, subplotInfo); - } - } + // 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; - // calc and autorange for errorbars - ErrorBars.calc(gd); + if (!isVisible || !Registry.traceIs(trace, "2dMap")) { + fullLayout._paper + .selectAll(".hm" + uid + ",.contour" + uid + ",#clip" + uid) + .remove(); + } - // TODO: autosize extra for text markers - return Lib.syncOrAsync([ - Registry.getComponentMethod('shapes', 'calcAutorange'), - Registry.getComponentMethod('annotations', 'calcAutorange'), - doAutoRange - ], gd); + if (!isVisible || !trace._module.colorbar) { + fullLayout._infolayer.selectAll(".cb" + uid).remove(); + } } - function doAutoRange() { - if(gd._transitioning) return; - - var axList = Plotly.Axes.list(gd, '', true); - for(var i = 0; i < axList.length; i++) { - Plotly.Axes.doAutoRange(axList[i]); - } + // loop over the base plot modules present on graph + var basePlotModules = fullLayout._basePlotModules; + for (i = 0; i < basePlotModules.length; i++) { + basePlotModules[i].plot(gd); } - // draw ticks, titles, and calculate axis scaling (._b, ._m) - function drawAxes() { - return Plotly.Axes.doTicks(gd, 'redraw'); - } + // keep reference to shape layers in subplots + var layerSubplot = fullLayout._paper.selectAll(".layer-subplot"); + fullLayout._imageSubplotLayer = layerSubplot.selectAll(".imagelayer"); + fullLayout._shapeSubplotLayer = layerSubplot.selectAll(".shapelayer"); - // Now plot the data - function drawData() { - var calcdata = gd.calcdata, - 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, - isVisible = (trace.visible === true), - uid = trace.uid; - - if(!isVisible || !Registry.traceIs(trace, '2dMap')) { - fullLayout._paper.selectAll( - '.hm' + uid + - ',.contour' + uid + - ',#clip' + uid - ).remove(); - } + // styling separate from drawing + Plots.style(gd); - if(!isVisible || !trace._module.colorbar) { - fullLayout._infolayer.selectAll('.cb' + uid).remove(); - } - } + // show annotations and shapes + Registry.getComponentMethod("shapes", "draw")(gd); + Registry.getComponentMethod("annotations", "draw")(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); - } - - // keep reference to shape layers in subplots - var layerSubplot = fullLayout._paper.selectAll('.layer-subplot'); - fullLayout._imageSubplotLayer = layerSubplot.selectAll('.imagelayer'); - fullLayout._shapeSubplotLayer = 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 - gd._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. gd._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); - } + // source links + Plots.addLinks(gd); - Lib.syncOrAsync([ - Plots.previousPromises, - addFrames, - drawFramework, - marginPushers, - marginPushersAgain, - positionAndAutorange, - subroutines.layoutStyles, - drawAxes, - drawData, - finalDraw - ], gd); - - // even if everything we did was synchronous, return a promise - // so that the caller doesn't care which route we took - return Promise.all(gd._promises).then(function() { - gd.emit('plotly_afterplot'); - return gd; - }); + // Mark the first render as complete + gd._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. gd._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); + } + + Lib.syncOrAsync( + [ + Plots.previousPromises, + addFrames, + drawFramework, + marginPushers, + marginPushersAgain, + positionAndAutorange, + subroutines.layoutStyles, + drawAxes, + drawData, + finalDraw + ], + gd + ); + + // even if everything we did was synchronous, return a promise + // so that the caller doesn't care which route we took + return Promise.all(gd._promises).then(function() { + gd.emit("plotly_afterplot"); + return gd; + }); }; - function opaqueSetBackground(gd, bgColor) { - gd._fullLayout._paperdiv.style('background', 'white'); - Plotly.defaultConfig.setBackground(gd, bgColor); + gd._fullLayout._paperdiv.style("background", "white"); + Plotly.defaultConfig.setBackground(gd, bgColor); } function setPlotContext(gd, config) { - if(!gd._context) gd._context = Lib.extendFlat({}, Plotly.defaultConfig); - var context = gd._context; - - if(config) { - Object.keys(config).forEach(function(key) { - if(key in context) { - if(key === 'setBackground' && config[key] === 'opaque') { - context[key] = opaqueSetBackground; - } - else context[key] = config[key]; - } - }); - - // map plot3dPixelRatio to plotGlPixelRatio for backward compatibility - if(config.plot3dPixelRatio && !context.plotGlPixelRatio) { - context.plotGlPixelRatio = context.plot3dPixelRatio; + if (!gd._context) gd._context = Lib.extendFlat({}, Plotly.defaultConfig); + var context = gd._context; + + if (config) { + Object.keys(config).forEach(function(key) { + if (key in context) { + if (key === "setBackground" && config[key] === "opaque") { + context[key] = opaqueSetBackground; + } else { + context[key] = config[key]; } - } + } + }); - // staticPlot forces a bunch of others: - if(context.staticPlot) { - context.editable = false; - context.autosizable = false; - context.scrollZoom = false; - context.doubleClick = false; - context.showTips = false; - context.showLink = false; - context.displayModeBar = false; - } + // map plot3dPixelRatio to plotGlPixelRatio for backward compatibility + if (config.plot3dPixelRatio && !context.plotGlPixelRatio) { + context.plotGlPixelRatio = context.plot3dPixelRatio; + } + } + + // staticPlot forces a bunch of others: + if (context.staticPlot) { + context.editable = false; + context.autosizable = false; + context.scrollZoom = false; + context.doubleClick = false; + context.showTips = false; + context.showLink = false; + context.displayModeBar = false; + } } function plotPolar(gd, data, layout) { - // build or reuse the container skeleton - var plotContainer = d3.select(gd).selectAll('.plot-container') - .data([0]); - plotContainer.enter() - .insert('div', ':first-child') - .classed('plot-container plotly', true); - var paperDiv = plotContainer.selectAll('.svg-container') - .data([0]); - paperDiv.enter().append('div') - .classed('svg-container', true) - .style('position', 'relative'); - - // empty it everytime for now - paperDiv.html(''); - - // fulfill gd requirements - if(data) gd.data = data; - if(layout) gd.layout = layout; - Polar.manager.fillLayout(gd); - - // resize canvas - paperDiv.style({ - width: gd._fullLayout.width + 'px', - height: gd._fullLayout.height + 'px' - }); - - // instantiate framework - gd.framework = Polar.manager.framework(gd); - - // plot - gd.framework({data: gd.data, layout: gd.layout}, paperDiv.node()); - - // set undo point - gd.framework.setUndoPoint(); - - // get the resulting svg for extending it - var polarPlotSVG = gd.framework.svg(); - - // editable title - var opacity = 1; - var txt = gd._fullLayout.title; - if(txt === '' || !txt) opacity = 0; - var placeholderText = 'Click to enter title'; + // build or reuse the container skeleton + var plotContainer = d3.select(gd).selectAll(".plot-container").data([0]); + plotContainer + .enter() + .insert("div", ":first-child") + .classed("plot-container plotly", true); + var paperDiv = plotContainer.selectAll(".svg-container").data([0]); + paperDiv + .enter() + .append("div") + .classed("svg-container", true) + .style("position", "relative"); + + // empty it everytime for now + paperDiv.html(""); + + // fulfill gd requirements + if (data) gd.data = data; + if (layout) gd.layout = layout; + Polar.manager.fillLayout(gd); + + // resize canvas + paperDiv.style({ + width: gd._fullLayout.width + "px", + height: gd._fullLayout.height + "px" + }); + + // instantiate framework + gd.framework = Polar.manager.framework(gd); + + // plot + gd.framework({ data: gd.data, layout: gd.layout }, paperDiv.node()); + + // set undo point + gd.framework.setUndoPoint(); + + // get the resulting svg for extending it + var polarPlotSVG = gd.framework.svg(); + + // editable title + var opacity = 1; + var txt = gd._fullLayout.title; + if (txt === "" || !txt) opacity = 0; + var placeholderText = "Click to enter title"; + + var titleLayout = function() { + this.call(svgTextUtils.convertToTspans); + // TODO: html/mathjax + // TODO: center title + }; + + var title = polarPlotSVG.select(".title-group text").call(titleLayout); + + if (gd._context.editable) { + title.attr({ "data-unformatted": txt }); + if (!txt || txt === placeholderText) { + opacity = 0.2; + title + .attr({ "data-unformatted": placeholderText }) + .text(placeholderText) + .style({ opacity: opacity }) + .on("mouseover.opacity", function() { + d3.select(this).transition().duration(100).style("opacity", 1); + }) + .on("mouseout.opacity", function() { + d3.select(this).transition().duration(1000).style("opacity", 0); + }); + } - var titleLayout = function() { - this.call(svgTextUtils.convertToTspans); - // TODO: html/mathjax - // TODO: center title + var setContenteditable = function() { + this + .call(svgTextUtils.makeEditable) + .on("edit", function(text) { + gd.framework({ layout: { title: text } }); + this.attr({ "data-unformatted": text }).text(text).call(titleLayout); + this.call(setContenteditable); + }) + .on("cancel", function() { + var txt = this.attr("data-unformatted"); + this.text(txt).call(titleLayout); + }); }; + title.call(setContenteditable); + } - var title = polarPlotSVG.select('.title-group text') - .call(titleLayout); - - if(gd._context.editable) { - title.attr({'data-unformatted': txt}); - if(!txt || txt === placeholderText) { - opacity = 0.2; - title.attr({'data-unformatted': placeholderText}) - .text(placeholderText) - .style({opacity: opacity}) - .on('mouseover.opacity', function() { - d3.select(this).transition().duration(100) - .style('opacity', 1); - }) - .on('mouseout.opacity', function() { - d3.select(this).transition().duration(1000) - .style('opacity', 0); - }); - } - - var setContenteditable = function() { - this.call(svgTextUtils.makeEditable) - .on('edit', function(text) { - gd.framework({layout: {title: text}}); - this.attr({'data-unformatted': text}) - .text(text) - .call(titleLayout); - this.call(setContenteditable); - }) - .on('cancel', function() { - var txt = this.attr('data-unformatted'); - this.text(txt).call(titleLayout); - }); - }; - title.call(setContenteditable); - } - - gd._context.setBackground(gd, gd._fullLayout.paper_bgcolor); - Plots.addLinks(gd); + gd._context.setBackground(gd, gd._fullLayout.paper_bgcolor); + Plots.addLinks(gd); - return Promise.resolve(); + return Promise.resolve(); } // convenience function to force a full redraw, mostly for use by plotly.js Plotly.redraw = function(gd) { - gd = helpers.getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); - if(!Lib.isPlotDiv(gd)) { - throw new Error('This element is not a Plotly plot: ' + gd); - } + if (!Lib.isPlotDiv(gd)) { + throw new Error("This element is not a Plotly plot: " + gd); + } - helpers.cleanData(gd.data, gd.data); - helpers.cleanLayout(gd.layout); + helpers.cleanData(gd.data, gd.data); + helpers.cleanLayout(gd.layout); - gd.calcdata = undefined; - return Plotly.plot(gd).then(function() { - gd.emit('plotly_redraw'); - return gd; - }); + gd.calcdata = undefined; + return Plotly.plot(gd).then(function() { + gd.emit("plotly_redraw"); + return gd; + }); }; /** @@ -517,13 +515,13 @@ Plotly.redraw = function(gd) { * @param {Object} config */ Plotly.newPlot = function(gd, data, layout, config) { - gd = helpers.getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); - // remove gl contexts - Plots.cleanPlot([], {}, gd._fullData || {}, gd._fullLayout || {}); + // remove gl contexts + Plots.cleanPlot([], {}, gd._fullData || {}, gd._fullLayout || {}); - Plots.purge(gd); - return Plotly.plot(gd, data, layout, config); + Plots.purge(gd); + return Plotly.plot(gd, data, layout, config); }; /** @@ -533,20 +531,17 @@ Plotly.newPlot = function(gd, data, layout, config) { * @param {Number} maxIndex The maximum index allowable (arr.length - 1) */ function positivifyIndices(indices, maxIndex) { - var parentLength = maxIndex + 1, - positiveIndices = [], - i, - index; - - for(i = 0; i < indices.length; i++) { - index = indices[i]; - if(index < 0) { - positiveIndices.push(parentLength + index); - } else { - positiveIndices.push(index); - } + var parentLength = maxIndex + 1, positiveIndices = [], i, index; + + for (i = 0; i < indices.length; i++) { + index = indices[i]; + if (index < 0) { + positiveIndices.push(parentLength + index); + } else { + positiveIndices.push(index); } - return positiveIndices; + } + return positiveIndices; } /** @@ -559,29 +554,30 @@ function positivifyIndices(indices, maxIndex) { * @param arrayName */ function assertIndexArray(gd, indices, arrayName) { - var i, - index; + var i, index; - for(i = 0; i < indices.length; i++) { - index = indices[i]; + for (i = 0; i < indices.length; i++) { + index = indices[i]; - // validate that indices are indeed integers - if(index !== parseInt(index, 10)) { - throw new Error('all values in ' + arrayName + ' must be integers'); - } + // validate that indices are indeed integers + if (index !== parseInt(index, 10)) { + throw new Error("all values in " + arrayName + " must be integers"); + } - // check that all indices are in bounds for given gd.data array length - if(index >= gd.data.length || index < -gd.data.length) { - throw new Error(arrayName + ' must be valid indices for gd.data.'); - } + // check that all indices are in bounds for given gd.data array length + if (index >= gd.data.length || index < -gd.data.length) { + throw new Error(arrayName + " must be valid indices for gd.data."); + } - // check that indices aren't repeated - if(indices.indexOf(index, i + 1) > -1 || - index >= 0 && indices.indexOf(-gd.data.length + index) > -1 || - index < 0 && indices.indexOf(gd.data.length + index) > -1) { - throw new Error('each index in ' + arrayName + ' must be unique.'); - } + // check that indices aren't repeated + if ( + indices.indexOf(index, i + 1) > -1 || + index >= 0 && indices.indexOf(-gd.data.length + index) > -1 || + index < 0 && indices.indexOf(gd.data.length + index) > -1 + ) { + throw new Error("each index in " + arrayName + " must be unique."); } + } } /** @@ -592,33 +588,34 @@ function assertIndexArray(gd, indices, arrayName) { * @param newIndices */ function checkMoveTracesArgs(gd, currentIndices, newIndices) { - - // check that gd has attribute 'data' and 'data' is array - if(!Array.isArray(gd.data)) { - throw new Error('gd.data must be an array.'); - } - - // validate currentIndices array - if(typeof currentIndices === 'undefined') { - throw new Error('currentIndices is a required argument.'); - } else if(!Array.isArray(currentIndices)) { - currentIndices = [currentIndices]; - } - assertIndexArray(gd, currentIndices, 'currentIndices'); - - // validate newIndices array if it exists - if(typeof newIndices !== 'undefined' && !Array.isArray(newIndices)) { - newIndices = [newIndices]; - } - if(typeof newIndices !== 'undefined') { - assertIndexArray(gd, newIndices, 'newIndices'); - } - - // check currentIndices and newIndices are the same length if newIdices exists - if(typeof newIndices !== 'undefined' && currentIndices.length !== newIndices.length) { - throw new Error('current and new indices must be of equal length.'); - } - + // check that gd has attribute 'data' and 'data' is array + if (!Array.isArray(gd.data)) { + throw new Error("gd.data must be an array."); + } + + // validate currentIndices array + if (typeof currentIndices === "undefined") { + throw new Error("currentIndices is a required argument."); + } else if (!Array.isArray(currentIndices)) { + currentIndices = [currentIndices]; + } + assertIndexArray(gd, currentIndices, "currentIndices"); + + // validate newIndices array if it exists + if (typeof newIndices !== "undefined" && !Array.isArray(newIndices)) { + newIndices = [newIndices]; + } + if (typeof newIndices !== "undefined") { + assertIndexArray(gd, newIndices, "newIndices"); + } + + // check currentIndices and newIndices are the same length if newIdices exists + if ( + typeof newIndices !== "undefined" && + currentIndices.length !== newIndices.length + ) { + throw new Error("current and new indices must be of equal length."); + } } /** * A private function to reduce the type checking clutter in addTraces. @@ -628,40 +625,42 @@ function checkMoveTracesArgs(gd, currentIndices, newIndices) { * @param newIndices */ function checkAddTracesArgs(gd, traces, newIndices) { - var i, value; - - // check that gd has attribute 'data' and 'data' is array - if(!Array.isArray(gd.data)) { - throw new Error('gd.data must be an array.'); - } - - // make sure traces exists - if(typeof traces === 'undefined') { - throw new Error('traces must be defined.'); - } - - // make sure traces is an array - if(!Array.isArray(traces)) { - traces = [traces]; - } - - // make sure each value in traces is an object - for(i = 0; i < traces.length; i++) { - value = traces[i]; - if(typeof value !== 'object' || (Array.isArray(value) || value === null)) { - throw new Error('all values in traces array must be non-array objects'); - } - } - - // make sure we have an index for each trace - if(typeof newIndices !== 'undefined' && !Array.isArray(newIndices)) { - newIndices = [newIndices]; - } - if(typeof newIndices !== 'undefined' && newIndices.length !== traces.length) { - throw new Error( - 'if indices is specified, traces.length must equal indices.length' - ); - } + var i, value; + + // check that gd has attribute 'data' and 'data' is array + if (!Array.isArray(gd.data)) { + throw new Error("gd.data must be an array."); + } + + // make sure traces exists + if (typeof traces === "undefined") { + throw new Error("traces must be defined."); + } + + // make sure traces is an array + if (!Array.isArray(traces)) { + traces = [traces]; + } + + // make sure each value in traces is an object + for (i = 0; i < traces.length; i++) { + value = traces[i]; + if (typeof value !== "object" || (Array.isArray(value) || value === null)) { + throw new Error("all values in traces array must be non-array objects"); + } + } + + // make sure we have an index for each trace + if (typeof newIndices !== "undefined" && !Array.isArray(newIndices)) { + newIndices = [newIndices]; + } + if ( + typeof newIndices !== "undefined" && newIndices.length !== traces.length + ) { + throw new Error( + "if indices is specified, traces.length must equal indices.length" + ); + } } /** @@ -675,42 +674,49 @@ function checkAddTracesArgs(gd, traces, newIndices) { * @param maxPoints */ function assertExtendTracesArgs(gd, update, indices, maxPoints) { + var maxPointsIsObject = Lib.isPlainObject(maxPoints); - var maxPointsIsObject = Lib.isPlainObject(maxPoints); - - if(!Array.isArray(gd.data)) { - throw new Error('gd.data must be an array'); - } - if(!Lib.isPlainObject(update)) { - throw new Error('update must be a key:value object'); - } - - if(typeof indices === 'undefined') { - throw new Error('indices must be an integer or array of integers'); - } + if (!Array.isArray(gd.data)) { + throw new Error("gd.data must be an array"); + } + if (!Lib.isPlainObject(update)) { + throw new Error("update must be a key:value object"); + } - assertIndexArray(gd, indices, 'indices'); + if (typeof indices === "undefined") { + throw new Error("indices must be an integer or array of integers"); + } - for(var key in update) { + assertIndexArray(gd, indices, "indices"); - /* + for (var key in update) { + /* * Verify that the attribute to be updated contains as many trace updates * as indices. Failure must result in throw and no-op */ - if(!Array.isArray(update[key]) || update[key].length !== indices.length) { - throw new Error('attribute ' + key + ' must be an array of length equal to indices array length'); - } + if (!Array.isArray(update[key]) || update[key].length !== indices.length) { + throw new Error( + "attribute " + + key + + " must be an array of length equal to indices array length" + ); + } - /* + /* * if maxPoints is an object it must match keys and array lengths of 'update' 1:1 */ - if(maxPointsIsObject && - (!(key in maxPoints) || !Array.isArray(maxPoints[key]) || - maxPoints[key].length !== update[key].length)) { - throw new Error('when maxPoints is set as a key:value object it must contain a 1:1 ' + - 'corrispondence with the keys and number of traces in the update object'); - } - } + if ( + maxPointsIsObject && + (!(key in maxPoints) || + !Array.isArray(maxPoints[key]) || + maxPoints[key].length !== update[key].length) + ) { + throw new Error( + "when maxPoints is set as a key:value object it must contain a 1:1 " + + "corrispondence with the keys and number of traces in the update object" + ); + } + } } /** @@ -723,68 +729,66 @@ function assertExtendTracesArgs(gd, update, indices, maxPoints) { * @return {Object[]} */ function getExtendProperties(gd, update, indices, maxPoints) { + var maxPointsIsObject = Lib.isPlainObject(maxPoints), updateProps = []; + var trace, target, prop, insert, maxp; - var maxPointsIsObject = Lib.isPlainObject(maxPoints), - updateProps = []; - var trace, target, prop, insert, maxp; + // allow scalar index to represent a single trace position + if (!Array.isArray(indices)) indices = [indices]; - // allow scalar index to represent a single trace position - if(!Array.isArray(indices)) indices = [indices]; + // negative indices are wrapped around to their positive value. Equivalent to python indexing. + indices = positivifyIndices(indices, gd.data.length - 1); - // negative indices are wrapped around to their positive value. Equivalent to python indexing. - indices = positivifyIndices(indices, gd.data.length - 1); - - // loop through all update keys and traces and harvest validated data. - for(var key in update) { - - for(var j = 0; j < indices.length; j++) { - - /* + // loop through all update keys and traces and harvest validated data. + for (var key in update) { + for (var j = 0; j < indices.length; j++) { + /* * Choose the trace indexed by the indices map argument and get the prop setter-getter * instance that references the key and value for this particular trace. */ - trace = gd.data[indices[j]]; - prop = Lib.nestedProperty(trace, key); + trace = gd.data[indices[j]]; + prop = Lib.nestedProperty(trace, key); - /* + /* * Target is the existing gd.data.trace.dataArray value like "x" or "marker.size" * Target must exist as an Array to allow the extend operation to be performed. */ - target = prop.get(); - insert = update[key][j]; + target = prop.get(); + insert = update[key][j]; - if(!Array.isArray(insert)) { - throw new Error('attribute: ' + key + ' index: ' + j + ' must be an array'); - } - if(!Array.isArray(target)) { - throw new Error('cannot extend missing or non-array attribute: ' + key); - } + if (!Array.isArray(insert)) { + throw new Error( + "attribute: " + key + " index: " + j + " must be an array" + ); + } + if (!Array.isArray(target)) { + throw new Error("cannot extend missing or non-array attribute: " + key); + } - /* + /* * maxPoints may be an object map or a scalar. If object select the key:value, else * Use the scalar maxPoints for all key and trace combinations. */ - maxp = maxPointsIsObject ? maxPoints[key][j] : maxPoints; + maxp = maxPointsIsObject ? maxPoints[key][j] : maxPoints; - // could have chosen null here, -1 just tells us to not take a window - if(!isNumeric(maxp)) maxp = -1; + // could have chosen null here, -1 just tells us to not take a window + if (!isNumeric(maxp)) maxp = -1; - /* + /* * Wrap the nestedProperty in an object containing required data * for lengthening and windowing this particular trace - key combination. * Flooring maxp mirrors the behaviour of floats in the Array.slice JSnative function. */ - updateProps.push({ - prop: prop, - target: target, - insert: insert, - maxp: Math.floor(maxp) - }); - } + updateProps.push({ + prop: prop, + target: target, + insert: insert, + maxp: Math.floor(maxp) + }); } + } - // all target and insertion data now validated - return updateProps; + // all target and insertion data now validated + return updateProps; } /** @@ -798,58 +802,65 @@ function getExtendProperties(gd, update, indices, maxPoints) { * @param {Function} spliceArray * @return {Object} */ -function spliceTraces(gd, update, indices, maxPoints, lengthenArray, spliceArray) { - - assertExtendTracesArgs(gd, update, indices, maxPoints); - - var updateProps = getExtendProperties(gd, update, indices, maxPoints), - remainder = [], - undoUpdate = {}, - undoPoints = {}; - var target, prop, maxp; - - for(var i = 0; i < updateProps.length; i++) { - - /* +function spliceTraces( + gd, + update, + indices, + maxPoints, + lengthenArray, + spliceArray +) { + assertExtendTracesArgs(gd, update, indices, maxPoints); + + var updateProps = getExtendProperties(gd, update, indices, maxPoints), + remainder = [], + undoUpdate = {}, + undoPoints = {}; + var target, prop, maxp; + + for (var i = 0; i < updateProps.length; i++) { + /* * prop is the object returned by Lib.nestedProperties */ - prop = updateProps[i].prop; - maxp = updateProps[i].maxp; + prop = updateProps[i].prop; + maxp = updateProps[i].maxp; - target = lengthenArray(updateProps[i].target, updateProps[i].insert); + target = lengthenArray(updateProps[i].target, updateProps[i].insert); - /* + /* * If maxp is set within post-extension trace.length, splice to maxp length. * Otherwise skip function call as splice op will have no effect anyway. */ - if(maxp >= 0 && maxp < target.length) remainder = spliceArray(target, maxp); + if (maxp >= 0 && maxp < target.length) { + remainder = spliceArray(target, maxp); + } - /* + /* * to reverse this operation we need the size of the original trace as the reverse * operation will need to window out any lengthening operation performed in this pass. */ - maxp = updateProps[i].target.length; + maxp = updateProps[i].target.length; - /* + /* * Magic happens here! update gd.data.trace[key] with new array data. */ - prop.set(target); + prop.set(target); - if(!Array.isArray(undoUpdate[prop.astr])) undoUpdate[prop.astr] = []; - if(!Array.isArray(undoPoints[prop.astr])) undoPoints[prop.astr] = []; + if (!Array.isArray(undoUpdate[prop.astr])) undoUpdate[prop.astr] = []; + if (!Array.isArray(undoPoints[prop.astr])) undoPoints[prop.astr] = []; - /* + /* * build the inverse update object for the undo operation */ - undoUpdate[prop.astr].push(remainder); + undoUpdate[prop.astr].push(remainder); - /* + /* * build the matching maxPoints undo object containing original trace lengths. */ - undoPoints[prop.astr].push(maxp); - } + undoPoints[prop.astr].push(maxp); + } - return {update: undoUpdate, maxPoints: undoPoints}; + return { update: undoUpdate, maxPoints: undoPoints }; } /** @@ -870,57 +881,63 @@ function spliceTraces(gd, update, indices, maxPoints, lengthenArray, spliceArray * */ Plotly.extendTraces = function extendTraces(gd, update, indices, maxPoints) { - gd = helpers.getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); - var undo = spliceTraces(gd, update, indices, maxPoints, - - /* + var undo = spliceTraces( + gd, + update, + indices, + maxPoints, + /* * The Lengthen operation extends trace from end with insert */ - function(target, insert) { - return target.concat(insert); - }, - - /* + function(target, insert) { + return target.concat(insert); + }, + /* * Window the trace keeping maxPoints, counting back from the end */ - function(target, maxPoints) { - return target.splice(0, target.length - maxPoints); - }); + function(target, maxPoints) { + return target.splice(0, target.length - maxPoints); + } + ); - var promise = Plotly.redraw(gd); + var promise = Plotly.redraw(gd); - var undoArgs = [gd, undo.update, indices, undo.maxPoints]; - Queue.add(gd, Plotly.prependTraces, undoArgs, extendTraces, arguments); + var undoArgs = [gd, undo.update, indices, undo.maxPoints]; + Queue.add(gd, Plotly.prependTraces, undoArgs, extendTraces, arguments); - return promise; + return promise; }; Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) { - gd = helpers.getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); - var undo = spliceTraces(gd, update, indices, maxPoints, - - /* + var undo = spliceTraces( + gd, + update, + indices, + maxPoints, + /* * The Lengthen operation extends trace by appending insert to start */ - function(target, insert) { - return insert.concat(target); - }, - - /* + function(target, insert) { + return insert.concat(target); + }, + /* * Window the trace keeping maxPoints, counting forward from the start */ - function(target, maxPoints) { - return target.splice(maxPoints, target.length); - }); + function(target, maxPoints) { + return target.splice(maxPoints, target.length); + } + ); - var promise = Plotly.redraw(gd); + var promise = Plotly.redraw(gd); - var undoArgs = [gd, undo.update, indices, undo.maxPoints]; - Queue.add(gd, Plotly.extendTraces, undoArgs, prependTraces, arguments); + var undoArgs = [gd, undo.update, indices, undo.maxPoints]; + Queue.add(gd, Plotly.extendTraces, undoArgs, prependTraces, arguments); - return promise; + return promise; }; /** @@ -933,73 +950,71 @@ Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) { * */ Plotly.addTraces = function addTraces(gd, traces, newIndices) { - gd = helpers.getGraphDiv(gd); - - var currentIndices = [], - undoFunc = Plotly.deleteTraces, - redoFunc = addTraces, - undoArgs = [gd, currentIndices], - redoArgs = [gd, traces], // no newIndices here - i, - promise; - - // all validation is done elsewhere to remove clutter here - checkAddTracesArgs(gd, traces, newIndices); - - // make sure traces is an array - if(!Array.isArray(traces)) { - traces = [traces]; - } - - // make sure traces do not repeat existing ones - traces = traces.map(function(trace) { - return Lib.extendFlat({}, trace); - }); - - helpers.cleanData(traces, gd.data); - - // add the traces to gd.data (no redrawing yet!) - for(i = 0; i < traces.length; i++) { - gd.data.push(traces[i]); - } - - // to continue, we need to call moveTraces which requires currentIndices - for(i = 0; i < traces.length; i++) { - currentIndices.push(-traces.length + i); - } - - // if the user didn't define newIndices, they just want the traces appended - // i.e., we can simply redraw and be done - if(typeof newIndices === 'undefined') { - promise = Plotly.redraw(gd); - Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); - return promise; - } - - // make sure indices is property defined - if(!Array.isArray(newIndices)) { - newIndices = [newIndices]; - } - - try { - - // this is redundant, but necessary to not catch later possible errors! - checkMoveTracesArgs(gd, currentIndices, newIndices); - } - catch(error) { - - // something went wrong, reset gd to be safe and rethrow error - gd.data.splice(gd.data.length - traces.length, traces.length); - throw error; - } - - // if we're here, the user has defined specific places to place the new traces - // this requires some extra work that moveTraces will do - Queue.startSequence(gd); + gd = helpers.getGraphDiv(gd); + + var currentIndices = [], + undoFunc = Plotly.deleteTraces, + redoFunc = addTraces, + undoArgs = [gd, currentIndices], + redoArgs = [gd, traces], + // no newIndices here + i, + promise; + + // all validation is done elsewhere to remove clutter here + checkAddTracesArgs(gd, traces, newIndices); + + // make sure traces is an array + if (!Array.isArray(traces)) { + traces = [traces]; + } + + // make sure traces do not repeat existing ones + traces = traces.map(function(trace) { + return Lib.extendFlat({}, trace); + }); + + helpers.cleanData(traces, gd.data); + + // add the traces to gd.data (no redrawing yet!) + for (i = 0; i < traces.length; i++) { + gd.data.push(traces[i]); + } + + // to continue, we need to call moveTraces which requires currentIndices + for (i = 0; i < traces.length; i++) { + currentIndices.push(-traces.length + i); + } + + // if the user didn't define newIndices, they just want the traces appended + // i.e., we can simply redraw and be done + if (typeof newIndices === "undefined") { + promise = Plotly.redraw(gd); Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); - promise = Plotly.moveTraces(gd, currentIndices, newIndices); - Queue.stopSequence(gd); return promise; + } + + // make sure indices is property defined + if (!Array.isArray(newIndices)) { + newIndices = [newIndices]; + } + + try { + // this is redundant, but necessary to not catch later possible errors! + checkMoveTracesArgs(gd, currentIndices, newIndices); + } catch (error) { + // something went wrong, reset gd to be safe and rethrow error + gd.data.splice(gd.data.length - traces.length, traces.length); + throw error; + } + + // if we're here, the user has defined specific places to place the new traces + // this requires some extra work that moveTraces will do + Queue.startSequence(gd); + Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + promise = Plotly.moveTraces(gd, currentIndices, newIndices); + Queue.stopSequence(gd); + return promise; }; /** @@ -1010,38 +1025,38 @@ Plotly.addTraces = function addTraces(gd, traces, newIndices) { * @param {Number|Number[]} indices The indices */ Plotly.deleteTraces = function deleteTraces(gd, indices) { - gd = helpers.getGraphDiv(gd); - - var traces = [], - undoFunc = Plotly.addTraces, - redoFunc = deleteTraces, - undoArgs = [gd, traces, indices], - redoArgs = [gd, indices], - i, - deletedTrace; - - // make sure indices are defined - if(typeof indices === 'undefined') { - throw new Error('indices must be an integer or array of integers.'); - } else if(!Array.isArray(indices)) { - indices = [indices]; - } - assertIndexArray(gd, indices, 'indices'); - - // convert negative indices to positive indices - indices = positivifyIndices(indices, gd.data.length - 1); - - // we want descending here so that splicing later doesn't affect indexing - indices.sort(Lib.sorterDes); - for(i = 0; i < indices.length; i += 1) { - deletedTrace = gd.data.splice(indices[i], 1)[0]; - traces.push(deletedTrace); - } - - var promise = Plotly.redraw(gd); - Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); - - return promise; + gd = helpers.getGraphDiv(gd); + + var traces = [], + undoFunc = Plotly.addTraces, + redoFunc = deleteTraces, + undoArgs = [gd, traces, indices], + redoArgs = [gd, indices], + i, + deletedTrace; + + // make sure indices are defined + if (typeof indices === "undefined") { + throw new Error("indices must be an integer or array of integers."); + } else if (!Array.isArray(indices)) { + indices = [indices]; + } + assertIndexArray(gd, indices, "indices"); + + // convert negative indices to positive indices + indices = positivifyIndices(indices, gd.data.length - 1); + + // we want descending here so that splicing later doesn't affect indexing + indices.sort(Lib.sorterDes); + for (i = 0; i < indices.length; i += 1) { + deletedTrace = gd.data.splice(indices[i], 1)[0]; + traces.push(deletedTrace); + } + + var promise = Plotly.redraw(gd); + Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + + return promise; }; /** @@ -1076,70 +1091,73 @@ Plotly.deleteTraces = function deleteTraces(gd, indices) { * Plotly.moveTraces(gd, [b, d, e, a, c]) // same as 'move to end' */ Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) { - gd = helpers.getGraphDiv(gd); - - var newData = [], - movingTraceMap = [], - undoFunc = moveTraces, - redoFunc = moveTraces, - undoArgs = [gd, newIndices, currentIndices], - redoArgs = [gd, currentIndices, newIndices], - i; - - // to reduce complexity here, check args elsewhere - // this throws errors where appropriate - checkMoveTracesArgs(gd, currentIndices, newIndices); - - // make sure currentIndices is an array - currentIndices = Array.isArray(currentIndices) ? currentIndices : [currentIndices]; - - // if undefined, define newIndices to point to the end of gd.data array - if(typeof newIndices === 'undefined') { - newIndices = []; - for(i = 0; i < currentIndices.length; i++) { - newIndices.push(-currentIndices.length + i); - } - } - - // make sure newIndices is an array if it's user-defined - newIndices = Array.isArray(newIndices) ? newIndices : [newIndices]; - - // convert negative indices to positive indices (they're the same length) - currentIndices = positivifyIndices(currentIndices, gd.data.length - 1); - newIndices = positivifyIndices(newIndices, gd.data.length - 1); - - // at this point, we've coerced the index arrays into predictable forms - - // get the traces that aren't being moved around - for(i = 0; i < gd.data.length; i++) { - - // if index isn't in currentIndices, include it in ignored! - if(currentIndices.indexOf(i) === -1) { - newData.push(gd.data[i]); - } - } - - // get a mapping of indices to moving traces - for(i = 0; i < currentIndices.length; i++) { - movingTraceMap.push({newIndex: newIndices[i], trace: gd.data[currentIndices[i]]}); - } - - // reorder this mapping by newIndex, ascending - movingTraceMap.sort(function(a, b) { - return a.newIndex - b.newIndex; + gd = helpers.getGraphDiv(gd); + + var newData = [], + movingTraceMap = [], + undoFunc = moveTraces, + redoFunc = moveTraces, + undoArgs = [gd, newIndices, currentIndices], + redoArgs = [gd, currentIndices, newIndices], + i; + + // to reduce complexity here, check args elsewhere + // this throws errors where appropriate + checkMoveTracesArgs(gd, currentIndices, newIndices); + + // make sure currentIndices is an array + currentIndices = Array.isArray(currentIndices) + ? currentIndices + : [currentIndices]; + + // if undefined, define newIndices to point to the end of gd.data array + if (typeof newIndices === "undefined") { + newIndices = []; + for (i = 0; i < currentIndices.length; i++) { + newIndices.push(-currentIndices.length + i); + } + } + + // make sure newIndices is an array if it's user-defined + newIndices = Array.isArray(newIndices) ? newIndices : [newIndices]; + + // convert negative indices to positive indices (they're the same length) + currentIndices = positivifyIndices(currentIndices, gd.data.length - 1); + newIndices = positivifyIndices(newIndices, gd.data.length - 1); + + // at this point, we've coerced the index arrays into predictable forms + // get the traces that aren't being moved around + for (i = 0; i < gd.data.length; i++) { + // if index isn't in currentIndices, include it in ignored! + if (currentIndices.indexOf(i) === -1) { + newData.push(gd.data[i]); + } + } + + // get a mapping of indices to moving traces + for (i = 0; i < currentIndices.length; i++) { + movingTraceMap.push({ + newIndex: newIndices[i], + trace: gd.data[currentIndices[i]] }); + } - // now, add the moving traces back in, in order! - for(i = 0; i < movingTraceMap.length; i += 1) { - newData.splice(movingTraceMap[i].newIndex, 0, movingTraceMap[i].trace); - } + // reorder this mapping by newIndex, ascending + movingTraceMap.sort(function(a, b) { + return a.newIndex - b.newIndex; + }); - gd.data = newData; + // now, add the moving traces back in, in order! + for (i = 0; i < movingTraceMap.length; i += 1) { + newData.splice(movingTraceMap[i].newIndex, 0, movingTraceMap[i].trace); + } - var promise = Plotly.redraw(gd); - Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + gd.data = newData; - return promise; + var promise = Plotly.redraw(gd); + Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + + return promise; }; /** @@ -1173,497 +1191,625 @@ Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) { * style files that want to specify cyclical default values). */ Plotly.restyle = function restyle(gd, astr, val, traces) { - gd = helpers.getGraphDiv(gd); - helpers.clearPromiseQueue(gd); - - var aobj = {}; - if(typeof astr === 'string') aobj[astr] = val; - else if(Lib.isPlainObject(astr)) { - // the 3-arg form - aobj = astr; - if(traces === undefined) traces = val; - } - else { - Lib.warn('Restyle fail.', astr, val, traces); - return Promise.reject(); - } + gd = helpers.getGraphDiv(gd); + helpers.clearPromiseQueue(gd); - if(Object.keys(aobj).length) gd.changed = true; + var aobj = {}; + if (typeof astr === "string") { + aobj[astr] = val; + } else if (Lib.isPlainObject(astr)) { + // the 3-arg form + aobj = astr; + if (traces === undefined) traces = val; + } else { + Lib.warn("Restyle fail.", astr, val, traces); + return Promise.reject(); + } - var specs = _restyle(gd, aobj, traces), - flags = specs.flags; + if (Object.keys(aobj).length) gd.changed = true; - // clear calcdata if required - if(flags.clearCalc) gd.calcdata = undefined; + var specs = _restyle(gd, aobj, traces), flags = specs.flags; - // fill in redraw sequence - var seq = []; + // clear calcdata if required + if (flags.clearCalc) gd.calcdata = undefined; - if(flags.fullReplot) { - seq.push(Plotly.plot); - } - else { - seq.push(Plots.previousPromises); + // fill in redraw sequence + var seq = []; - Plots.supplyDefaults(gd); + if (flags.fullReplot) { + seq.push(Plotly.plot); + } else { + seq.push(Plots.previousPromises); - if(flags.dostyle) seq.push(subroutines.doTraceStyle); - if(flags.docolorbars) seq.push(subroutines.doColorBars); - } + Plots.supplyDefaults(gd); - Queue.add(gd, - restyle, [gd, specs.undoit, specs.traces], - restyle, [gd, specs.redoit, specs.traces] - ); + if (flags.dostyle) seq.push(subroutines.doTraceStyle); + if (flags.docolorbars) seq.push(subroutines.doColorBars); + } - var plotDone = Lib.syncOrAsync(seq, gd); - if(!plotDone || !plotDone.then) plotDone = Promise.resolve(); + Queue.add(gd, restyle, [gd, specs.undoit, specs.traces], restyle, [ + gd, + specs.redoit, + specs.traces + ]); - return plotDone.then(function() { - gd.emit('plotly_restyle', specs.eventData); - return gd; - }); + var plotDone = Lib.syncOrAsync(seq, gd); + if (!plotDone || !plotDone.then) plotDone = Promise.resolve(); + + return plotDone.then(function() { + gd.emit("plotly_restyle", specs.eventData); + return gd; + }); }; function _restyle(gd, aobj, _traces) { - var fullLayout = gd._fullLayout, - fullData = gd._fullData, - data = gd.data, - i; - - var traces = helpers.coerceTraceIndices(gd, _traces); - - // initialize flags - var flags = { - docalc: false, - docalcAutorange: false, - doplot: false, - dostyle: false, - docolorbars: false, - autorangeOn: false, - clearCalc: false, - fullReplot: false - }; - - // copies of the change (and previous values of anything affected) - // for the undo / redo queue - var redoit = {}, - undoit = {}, - axlist, - flagAxForDelete = {}; - - // recalcAttrs attributes need a full regeneration of calcdata - // as well as a replot, because the right objects may not exist, - // or autorange may need recalculating - // in principle we generally shouldn't need to redo ALL traces... that's - // harder though. - var recalcAttrs = [ - 'mode', 'visible', 'type', 'orientation', 'fill', - 'histfunc', 'histnorm', 'text', - 'x', 'y', 'z', - 'a', 'b', 'c', - 'open', 'high', 'low', 'close', - 'base', 'width', 'offset', - 'xtype', 'x0', 'dx', 'ytype', 'y0', 'dy', 'xaxis', 'yaxis', - 'line.width', - 'connectgaps', 'transpose', 'zsmooth', - 'showscale', 'marker.showscale', - 'zauto', 'marker.cauto', - 'autocolorscale', 'marker.autocolorscale', - 'colorscale', 'marker.colorscale', - 'reversescale', 'marker.reversescale', - 'autobinx', 'nbinsx', 'xbins', 'xbins.start', 'xbins.end', 'xbins.size', - 'autobiny', 'nbinsy', 'ybins', 'ybins.start', 'ybins.end', 'ybins.size', - 'autocontour', 'ncontours', 'contours', 'contours.coloring', - 'error_y', 'error_y.visible', 'error_y.value', 'error_y.type', - 'error_y.traceref', 'error_y.array', 'error_y.symmetric', - 'error_y.arrayminus', 'error_y.valueminus', 'error_y.tracerefminus', - 'error_x', 'error_x.visible', 'error_x.value', 'error_x.type', - 'error_x.traceref', 'error_x.array', 'error_x.symmetric', - 'error_x.arrayminus', 'error_x.valueminus', 'error_x.tracerefminus', - 'swapxy', 'swapxyaxes', 'orientationaxes', - 'marker.colors', 'values', 'labels', 'label0', 'dlabel', 'sort', - 'textinfo', 'textposition', 'textfont.size', 'textfont.family', 'textfont.color', - 'insidetextfont.size', 'insidetextfont.family', 'insidetextfont.color', - 'outsidetextfont.size', 'outsidetextfont.family', 'outsidetextfont.color', - 'hole', 'scalegroup', 'domain', 'domain.x', 'domain.y', - 'domain.x[0]', 'domain.x[1]', 'domain.y[0]', 'domain.y[1]', - 'tilt', 'tiltaxis', 'depth', 'direction', 'rotation', 'pull', - 'line.showscale', 'line.cauto', 'line.autocolorscale', 'line.reversescale', - 'marker.line.showscale', 'marker.line.cauto', 'marker.line.autocolorscale', 'marker.line.reversescale', - 'xcalendar', 'ycalendar', - 'cumulative', 'cumulative.enabled', 'cumulative.direction', 'cumulative.currentbin' - ]; - - for(i = 0; i < traces.length; i++) { - if(Registry.traceIs(fullData[traces[i]], 'box')) { - recalcAttrs.push('name'); - break; - } - } - - // autorangeAttrs attributes need a full redo of calcdata - // only if an axis is autoranged, - // because .calc() is where the autorange gets determined - // TODO: could we break this out as well? - var autorangeAttrs = [ - 'marker', 'marker.size', 'textfont', - 'boxpoints', 'jitter', 'pointpos', 'whiskerwidth', 'boxmean', - 'tickwidth' - ]; - - // replotAttrs attributes need a replot (because different - // objects need to be made) but not a recalc - var replotAttrs = [ - 'zmin', 'zmax', 'zauto', - 'xgap', 'ygap', - 'marker.cmin', 'marker.cmax', 'marker.cauto', - 'line.cmin', 'line.cmax', - 'marker.line.cmin', 'marker.line.cmax', - 'contours.start', 'contours.end', 'contours.size', - 'contours.showlines', - 'line', 'line.smoothing', 'line.shape', - 'error_y.width', 'error_x.width', 'error_x.copy_ystyle', - 'marker.maxdisplayed' - ]; - - // these ones may alter the axis type - // (at least if the first trace is involved) - var axtypeAttrs = [ - 'type', 'x', 'y', 'x0', 'y0', 'orientation', 'xaxis', 'yaxis' - ]; - - var zscl = ['zmin', 'zmax'], - xbins = ['xbins.start', 'xbins.end', 'xbins.size'], - ybins = ['ybins.start', 'ybins.end', 'ybins.size'], - contourAttrs = ['contours.start', 'contours.end', 'contours.size']; - - // At the moment, only cartesian, pie and ternary plot types can afford - // to not go through a full replot - var doPlotWhiteList = ['cartesian', 'pie', 'ternary']; - fullLayout._basePlotModules.forEach(function(_module) { - if(doPlotWhiteList.indexOf(_module.name) === -1) flags.docalc = true; - }); - - // make a new empty vals array for undoit - function a0() { return traces.map(function() { return undefined; }); } - - // for autoranging multiple axes - function addToAxlist(axid) { - var axName = Plotly.Axes.id2name(axid); - if(axlist.indexOf(axName) === -1) axlist.push(axName); - } - - function autorangeAttr(axName) { return 'LAYOUT' + axName + '.autorange'; } - - function rangeAttr(axName) { return 'LAYOUT' + axName + '.range'; } - - // for attrs that interact (like scales & autoscales), save the - // old vals before making the change - // val=undefined will not set a value, just record what the value was. - // val=null will delete the attribute - // attr can be an array to set several at once (all to the same val) - function doextra(attr, val, i) { - if(Array.isArray(attr)) { - attr.forEach(function(a) { doextra(a, val, i); }); - return; - } - // quit if explicitly setting this elsewhere - if(attr in aobj) return; - - var extraparam; - if(attr.substr(0, 6) === 'LAYOUT') { - extraparam = Lib.nestedProperty(gd.layout, attr.replace('LAYOUT', '')); - } else { - extraparam = Lib.nestedProperty(data[traces[i]], attr); - } - - if(!(attr in undoit)) { - undoit[attr] = a0(); - } - if(undoit[attr][i] === undefined) { - undoit[attr][i] = extraparam.get(); - } - if(val !== undefined) { - extraparam.set(val); - } - } - - // now make the changes to gd.data (and occasionally gd.layout) - // and figure out what kind of graphics update we need to do - for(var ai in aobj) { - var vi = aobj[ai], - cont, - contFull, - param, - oldVal, - newVal; - - redoit[ai] = vi; - - if(ai.substr(0, 6) === 'LAYOUT') { - param = Lib.nestedProperty(gd.layout, ai.replace('LAYOUT', '')); - undoit[ai] = [param.get()]; - // since we're allowing val to be an array, allow it here too, - // even though that's meaningless - param.set(Array.isArray(vi) ? vi[0] : vi); - // ironically, the layout attrs in restyle only require replot, - // not relayout - flags.docalc = true; - continue; - } - - // set attribute in gd.data - undoit[ai] = a0(); - for(i = 0; i < traces.length; i++) { - cont = data[traces[i]]; - contFull = fullData[traces[i]]; - param = Lib.nestedProperty(cont, ai); - oldVal = param.get(); - newVal = Array.isArray(vi) ? vi[i % vi.length] : vi; - - if(newVal === undefined) continue; - - // setting bin or z settings should turn off auto - // and setting auto should save bin or z settings - if(zscl.indexOf(ai) !== -1) { - doextra('zauto', false, i); - } - else if(ai === 'colorscale') { - doextra('autocolorscale', false, i); - } - else if(ai === 'autocolorscale') { - doextra('colorscale', undefined, i); - } - else if(ai === 'marker.colorscale') { - doextra('marker.autocolorscale', false, i); - } - else if(ai === 'marker.autocolorscale') { - doextra('marker.colorscale', undefined, i); - } - else if(ai === 'zauto') { - doextra(zscl, undefined, i); - } - else if(xbins.indexOf(ai) !== -1) { - doextra('autobinx', false, i); - } - else if(ai === 'autobinx') { - doextra(xbins, undefined, i); - } - else if(ybins.indexOf(ai) !== -1) { - doextra('autobiny', false, i); - } - else if(ai === 'autobiny') { - doextra(ybins, undefined, i); - } - else if(contourAttrs.indexOf(ai) !== -1) { - doextra('autocontour', false, i); - } - else if(ai === 'autocontour') { - doextra(contourAttrs, undefined, i); - } - // heatmaps: setting x0 or dx, y0 or dy, - // should turn xtype/ytype to 'scaled' if 'array' - else if(['x0', 'dx'].indexOf(ai) !== -1 && - contFull.x && contFull.xtype !== 'scaled') { - doextra('xtype', 'scaled', i); - } - else if(['y0', 'dy'].indexOf(ai) !== -1 && - contFull.y && contFull.ytype !== 'scaled') { - doextra('ytype', 'scaled', i); - } - // changing colorbar size modes, - // make the resulting size not change - // note that colorbar fractional sizing is based on the - // original plot size, before anything (like a colorbar) - // increases the margins - else if(ai === 'colorbar.thicknessmode' && param.get() !== newVal && - ['fraction', 'pixels'].indexOf(newVal) !== -1 && - contFull.colorbar) { - var thicknorm = - ['top', 'bottom'].indexOf(contFull.colorbar.orient) !== -1 ? - (fullLayout.height - fullLayout.margin.t - fullLayout.margin.b) : - (fullLayout.width - fullLayout.margin.l - fullLayout.margin.r); - doextra('colorbar.thickness', contFull.colorbar.thickness * - (newVal === 'fraction' ? 1 / thicknorm : thicknorm), i); - } - else if(ai === 'colorbar.lenmode' && param.get() !== newVal && - ['fraction', 'pixels'].indexOf(newVal) !== -1 && - contFull.colorbar) { - var lennorm = - ['top', 'bottom'].indexOf(contFull.colorbar.orient) !== -1 ? - (fullLayout.width - fullLayout.margin.l - fullLayout.margin.r) : - (fullLayout.height - fullLayout.margin.t - fullLayout.margin.b); - doextra('colorbar.len', contFull.colorbar.len * - (newVal === 'fraction' ? 1 / lennorm : lennorm), i); - } - else if(ai === 'colorbar.tick0' || ai === 'colorbar.dtick') { - doextra('colorbar.tickmode', 'linear', i); - } - else if(ai === 'colorbar.tickmode') { - doextra(['colorbar.tick0', 'colorbar.dtick'], undefined, i); - } - - - if(ai === 'type' && (newVal === 'pie') !== (oldVal === 'pie')) { - var labelsTo = 'x', - valuesTo = 'y'; - if((newVal === 'bar' || oldVal === 'bar') && cont.orientation === 'h') { - labelsTo = 'y'; - valuesTo = 'x'; - } - Lib.swapAttrs(cont, ['?', '?src'], 'labels', labelsTo); - Lib.swapAttrs(cont, ['d?', '?0'], 'label', labelsTo); - Lib.swapAttrs(cont, ['?', '?src'], 'values', valuesTo); - - if(oldVal === 'pie') { - Lib.nestedProperty(cont, 'marker.color') - .set(Lib.nestedProperty(cont, 'marker.colors').get()); - - // super kludgy - but if all pies are gone we won't remove them otherwise - fullLayout._pielayer.selectAll('g.trace').remove(); - } else if(Registry.traceIs(cont, 'cartesian')) { - Lib.nestedProperty(cont, 'marker.colors') - .set(Lib.nestedProperty(cont, 'marker.color').get()); - // look for axes that are no longer in use and delete them - flagAxForDelete[cont.xaxis || 'x'] = true; - flagAxForDelete[cont.yaxis || 'y'] = true; - } - } - - undoit[ai][i] = oldVal; - // set the new value - if val is an array, it's one el per trace - // first check for attributes that get more complex alterations - var swapAttrs = [ - 'swapxy', 'swapxyaxes', 'orientation', 'orientationaxes' - ]; - if(swapAttrs.indexOf(ai) !== -1) { - // setting an orientation: make sure it's changing - // before we swap everything else - if(ai === 'orientation') { - param.set(newVal); - if(param.get() === undoit[ai][i]) continue; - } - // orientationaxes has no value, - // it flips everything and the axes - else if(ai === 'orientationaxes') { - cont.orientation = - {v: 'h', h: 'v'}[contFull.orientation]; - } - helpers.swapXYData(cont); - } - else if(Plots.dataArrayContainers.indexOf(param.parts[0]) !== -1) { - helpers.manageArrayContainers(param, newVal, undoit); - flags.docalc = true; - } - // all the other ones, just modify that one attribute - else param.set(newVal); - - } - - // swap the data attributes of the relevant x and y axes? - if(['swapxyaxes', 'orientationaxes'].indexOf(ai) !== -1) { - Plotly.Axes.swap(gd, traces); - } - - // swap hovermode if set to "compare x/y data" - if(ai === 'orientationaxes') { - var hovermode = Lib.nestedProperty(gd.layout, 'hovermode'); - if(hovermode.get() === 'x') { - hovermode.set('y'); - } else if(hovermode.get() === 'y') { - hovermode.set('x'); - } - } - - // check if we need to call axis type - if((traces.indexOf(0) !== -1) && (axtypeAttrs.indexOf(ai) !== -1)) { - Plotly.Axes.clearTypes(gd, traces); - flags.docalc = true; - } - - // switching from auto to manual binning or z scaling doesn't - // actually do anything but change what you see in the styling - // box. everything else at least needs to apply styles - if((['autobinx', 'autobiny', 'zauto'].indexOf(ai) === -1) || - newVal !== false) { - flags.dostyle = true; - } - if(['colorbar', 'line'].indexOf(param.parts[0]) !== -1 || - param.parts[0] === 'marker' && param.parts[1] === 'colorbar') { - flags.docolorbars = true; - } - - var aiArrayStart = ai.indexOf('['), - aiAboveArray = aiArrayStart === -1 ? ai : ai.substr(0, aiArrayStart); - - if(recalcAttrs.indexOf(aiAboveArray) !== -1) { - // major enough changes deserve autoscale, autobin, and - // non-reversed axes so people don't get confused - if(['orientation', 'type'].indexOf(ai) !== -1) { - axlist = []; - for(i = 0; i < traces.length; i++) { - var trace = data[traces[i]]; - - if(Registry.traceIs(trace, 'cartesian')) { - addToAxlist(trace.xaxis || 'x'); - addToAxlist(trace.yaxis || 'y'); - - if(ai === 'type') { - doextra(['autobinx', 'autobiny'], true, i); - } - } - } - - doextra(axlist.map(autorangeAttr), true, 0); - doextra(axlist.map(rangeAttr), [0, 1], 0); - } - flags.docalc = true; - } - else if(replotAttrs.indexOf(aiAboveArray) !== -1) flags.doplot = true; - else if(autorangeAttrs.indexOf(aiAboveArray) !== -1) flags.docalcAutorange = true; - } - - // do we need to force a recalc? - Plotly.Axes.list(gd).forEach(function(ax) { - if(ax.autorange) flags.autorangeOn = true; + var fullLayout = gd._fullLayout, fullData = gd._fullData, data = gd.data, i; + + var traces = helpers.coerceTraceIndices(gd, _traces); + + // initialize flags + var flags = { + docalc: false, + docalcAutorange: false, + doplot: false, + dostyle: false, + docolorbars: false, + autorangeOn: false, + clearCalc: false, + fullReplot: false + }; + + // copies of the change (and previous values of anything affected) + // for the undo / redo queue + var redoit = {}, undoit = {}, axlist, flagAxForDelete = {}; + + // recalcAttrs attributes need a full regeneration of calcdata + // as well as a replot, because the right objects may not exist, + // or autorange may need recalculating + // in principle we generally shouldn't need to redo ALL traces... that's + // harder though. + var recalcAttrs = [ + "mode", + "visible", + "type", + "orientation", + "fill", + "histfunc", + "histnorm", + "text", + "x", + "y", + "z", + "a", + "b", + "c", + "open", + "high", + "low", + "close", + "base", + "width", + "offset", + "xtype", + "x0", + "dx", + "ytype", + "y0", + "dy", + "xaxis", + "yaxis", + "line.width", + "connectgaps", + "transpose", + "zsmooth", + "showscale", + "marker.showscale", + "zauto", + "marker.cauto", + "autocolorscale", + "marker.autocolorscale", + "colorscale", + "marker.colorscale", + "reversescale", + "marker.reversescale", + "autobinx", + "nbinsx", + "xbins", + "xbins.start", + "xbins.end", + "xbins.size", + "autobiny", + "nbinsy", + "ybins", + "ybins.start", + "ybins.end", + "ybins.size", + "autocontour", + "ncontours", + "contours", + "contours.coloring", + "error_y", + "error_y.visible", + "error_y.value", + "error_y.type", + "error_y.traceref", + "error_y.array", + "error_y.symmetric", + "error_y.arrayminus", + "error_y.valueminus", + "error_y.tracerefminus", + "error_x", + "error_x.visible", + "error_x.value", + "error_x.type", + "error_x.traceref", + "error_x.array", + "error_x.symmetric", + "error_x.arrayminus", + "error_x.valueminus", + "error_x.tracerefminus", + "swapxy", + "swapxyaxes", + "orientationaxes", + "marker.colors", + "values", + "labels", + "label0", + "dlabel", + "sort", + "textinfo", + "textposition", + "textfont.size", + "textfont.family", + "textfont.color", + "insidetextfont.size", + "insidetextfont.family", + "insidetextfont.color", + "outsidetextfont.size", + "outsidetextfont.family", + "outsidetextfont.color", + "hole", + "scalegroup", + "domain", + "domain.x", + "domain.y", + "domain.x[0]", + "domain.x[1]", + "domain.y[0]", + "domain.y[1]", + "tilt", + "tiltaxis", + "depth", + "direction", + "rotation", + "pull", + "line.showscale", + "line.cauto", + "line.autocolorscale", + "line.reversescale", + "marker.line.showscale", + "marker.line.cauto", + "marker.line.autocolorscale", + "marker.line.reversescale", + "xcalendar", + "ycalendar", + "cumulative", + "cumulative.enabled", + "cumulative.direction", + "cumulative.currentbin" + ]; + + for (i = 0; i < traces.length; i++) { + if (Registry.traceIs(fullData[traces[i]], "box")) { + recalcAttrs.push("name"); + break; + } + } + + // autorangeAttrs attributes need a full redo of calcdata + // only if an axis is autoranged, + // because .calc() is where the autorange gets determined + // TODO: could we break this out as well? + var autorangeAttrs = [ + "marker", + "marker.size", + "textfont", + "boxpoints", + "jitter", + "pointpos", + "whiskerwidth", + "boxmean", + "tickwidth" + ]; + + // replotAttrs attributes need a replot (because different + // objects need to be made) but not a recalc + var replotAttrs = [ + "zmin", + "zmax", + "zauto", + "xgap", + "ygap", + "marker.cmin", + "marker.cmax", + "marker.cauto", + "line.cmin", + "line.cmax", + "marker.line.cmin", + "marker.line.cmax", + "contours.start", + "contours.end", + "contours.size", + "contours.showlines", + "line", + "line.smoothing", + "line.shape", + "error_y.width", + "error_x.width", + "error_x.copy_ystyle", + "marker.maxdisplayed" + ]; + + // these ones may alter the axis type + // (at least if the first trace is involved) + var axtypeAttrs = [ + "type", + "x", + "y", + "x0", + "y0", + "orientation", + "xaxis", + "yaxis" + ]; + + var zscl = ["zmin", "zmax"], + xbins = ["xbins.start", "xbins.end", "xbins.size"], + ybins = ["ybins.start", "ybins.end", "ybins.size"], + contourAttrs = ["contours.start", "contours.end", "contours.size"]; + + // At the moment, only cartesian, pie and ternary plot types can afford + // to not go through a full replot + var doPlotWhiteList = ["cartesian", "pie", "ternary"]; + fullLayout._basePlotModules.forEach(function(_module) { + if (doPlotWhiteList.indexOf(_module.name) === -1) flags.docalc = true; + }); + + // make a new empty vals array for undoit + function a0() { + return traces.map(function() { + return undefined; }); - - // check axes we've flagged for possible deletion - // flagAxForDelete is a hash so we can make sure we only get each axis once - var axListForDelete = Object.keys(flagAxForDelete); - axisLoop: - for(i = 0; i < axListForDelete.length; i++) { - var axId = axListForDelete[i], - axLetter = axId.charAt(0), - axAttr = axLetter + 'axis'; - - for(var j = 0; j < data.length; j++) { - if(Registry.traceIs(data[j], 'cartesian') && - (data[j][axAttr] || axLetter) === axId) { - continue axisLoop; - } - } - - // no data on this axis - delete it. - doextra('LAYOUT' + Plotly.Axes.id2name(axId), null, 0); - } - - // combine a few flags together; - if(flags.docalc || (flags.docalcAutorange && flags.autorangeOn)) { - flags.clearCalc = true; - } - if(flags.docalc || flags.doplot || flags.docalcAutorange) { - flags.fullReplot = true; - } - - return { - flags: flags, - undoit: undoit, - redoit: redoit, - traces: traces, - eventData: Lib.extendDeepNoArrays([], [redoit, traces]) - }; + } + + // for autoranging multiple axes + function addToAxlist(axid) { + var axName = Plotly.Axes.id2name(axid); + if (axlist.indexOf(axName) === -1) axlist.push(axName); + } + + function autorangeAttr(axName) { + return "LAYOUT" + axName + ".autorange"; + } + + function rangeAttr(axName) { + return "LAYOUT" + axName + ".range"; + } + + // for attrs that interact (like scales & autoscales), save the + // old vals before making the change + // val=undefined will not set a value, just record what the value was. + // val=null will delete the attribute + // attr can be an array to set several at once (all to the same val) + function doextra(attr, val, i) { + if (Array.isArray(attr)) { + attr.forEach(function(a) { + doextra(a, val, i); + }); + return; + } + // quit if explicitly setting this elsewhere + if (attr in aobj) return; + + var extraparam; + if (attr.substr(0, 6) === "LAYOUT") { + extraparam = Lib.nestedProperty(gd.layout, attr.replace("LAYOUT", "")); + } else { + extraparam = Lib.nestedProperty(data[traces[i]], attr); + } + + if (!(attr in undoit)) { + undoit[attr] = a0(); + } + if (undoit[attr][i] === undefined) { + undoit[attr][i] = extraparam.get(); + } + if (val !== undefined) { + extraparam.set(val); + } + } + + // now make the changes to gd.data (and occasionally gd.layout) + // and figure out what kind of graphics update we need to do + for (var ai in aobj) { + var vi = aobj[ai], cont, contFull, param, oldVal, newVal; + + redoit[ai] = vi; + + if (ai.substr(0, 6) === "LAYOUT") { + param = Lib.nestedProperty(gd.layout, ai.replace("LAYOUT", "")); + undoit[ai] = [param.get()]; + // since we're allowing val to be an array, allow it here too, + // even though that's meaningless + param.set(Array.isArray(vi) ? vi[0] : vi); + // ironically, the layout attrs in restyle only require replot, + // not relayout + flags.docalc = true; + continue; + } + + // set attribute in gd.data + undoit[ai] = a0(); + for (i = 0; i < traces.length; i++) { + cont = data[traces[i]]; + contFull = fullData[traces[i]]; + param = Lib.nestedProperty(cont, ai); + oldVal = param.get(); + newVal = Array.isArray(vi) ? vi[i % vi.length] : vi; + + if (newVal === undefined) continue; + + // setting bin or z settings should turn off auto + // and setting auto should save bin or z settings + if (zscl.indexOf(ai) !== -1) { + doextra("zauto", false, i); + } else if (ai === "colorscale") { + doextra("autocolorscale", false, i); + } else if (ai === "autocolorscale") { + doextra("colorscale", undefined, i); + } else if (ai === "marker.colorscale") { + doextra("marker.autocolorscale", false, i); + } else if (ai === "marker.autocolorscale") { + doextra("marker.colorscale", undefined, i); + } else if (ai === "zauto") { + doextra(zscl, undefined, i); + } else if (xbins.indexOf(ai) !== -1) { + doextra("autobinx", false, i); + } else if (ai === "autobinx") { + doextra(xbins, undefined, i); + } else if (ybins.indexOf(ai) !== -1) { + doextra("autobiny", false, i); + } else if (ai === "autobiny") { + doextra(ybins, undefined, i); + } else if (contourAttrs.indexOf(ai) !== -1) { + doextra("autocontour", false, i); + } else if (ai === "autocontour") { + doextra(contourAttrs, undefined, i); + } else if ( + ["x0", "dx"].indexOf(ai) !== -1 && + contFull.x && + contFull.xtype !== "scaled" + ) { + // heatmaps: setting x0 or dx, y0 or dy, + // should turn xtype/ytype to 'scaled' if 'array' + doextra("xtype", "scaled", i); + } else if ( + ["y0", "dy"].indexOf(ai) !== -1 && + contFull.y && + contFull.ytype !== "scaled" + ) { + doextra("ytype", "scaled", i); + } else if ( + ai === "colorbar.thicknessmode" && + param.get() !== newVal && + ["fraction", "pixels"].indexOf(newVal) !== -1 && + contFull.colorbar + ) { + // changing colorbar size modes, + // make the resulting size not change + // note that colorbar fractional sizing is based on the + // original plot size, before anything (like a colorbar) + // increases the margins + var thicknorm = ["top", "bottom"].indexOf(contFull.colorbar.orient) !== + -1 + ? fullLayout.height - fullLayout.margin.t - fullLayout.margin.b + : fullLayout.width - fullLayout.margin.l - fullLayout.margin.r; + doextra( + "colorbar.thickness", + contFull.colorbar.thickness * + (newVal === "fraction" ? 1 / thicknorm : thicknorm), + i + ); + } else if ( + ai === "colorbar.lenmode" && + param.get() !== newVal && + ["fraction", "pixels"].indexOf(newVal) !== -1 && + contFull.colorbar + ) { + var lennorm = ["top", "bottom"].indexOf(contFull.colorbar.orient) !== -1 + ? fullLayout.width - fullLayout.margin.l - fullLayout.margin.r + : fullLayout.height - fullLayout.margin.t - fullLayout.margin.b; + doextra( + "colorbar.len", + contFull.colorbar.len * + (newVal === "fraction" ? 1 / lennorm : lennorm), + i + ); + } else if (ai === "colorbar.tick0" || ai === "colorbar.dtick") { + doextra("colorbar.tickmode", "linear", i); + } else if (ai === "colorbar.tickmode") { + doextra(["colorbar.tick0", "colorbar.dtick"], undefined, i); + } + + if (ai === "type" && newVal === "pie" !== (oldVal === "pie")) { + var labelsTo = "x", valuesTo = "y"; + if ( + (newVal === "bar" || oldVal === "bar") && cont.orientation === "h" + ) { + labelsTo = "y"; + valuesTo = "x"; + } + Lib.swapAttrs(cont, ["?", "?src"], "labels", labelsTo); + Lib.swapAttrs(cont, ["d?", "?0"], "label", labelsTo); + Lib.swapAttrs(cont, ["?", "?src"], "values", valuesTo); + + if (oldVal === "pie") { + Lib.nestedProperty(cont, "marker.color").set( + Lib.nestedProperty(cont, "marker.colors").get() + ); + + // super kludgy - but if all pies are gone we won't remove them otherwise + fullLayout._pielayer.selectAll("g.trace").remove(); + } else if (Registry.traceIs(cont, "cartesian")) { + Lib.nestedProperty(cont, "marker.colors").set( + Lib.nestedProperty(cont, "marker.color").get() + ); + // look for axes that are no longer in use and delete them + flagAxForDelete[cont.xaxis || "x"] = true; + flagAxForDelete[cont.yaxis || "y"] = true; + } + } + + undoit[ai][i] = oldVal; + // set the new value - if val is an array, it's one el per trace + // first check for attributes that get more complex alterations + var swapAttrs = [ + "swapxy", + "swapxyaxes", + "orientation", + "orientationaxes" + ]; + if (swapAttrs.indexOf(ai) !== -1) { + // setting an orientation: make sure it's changing + // before we swap everything else + if (ai === "orientation") { + param.set(newVal); + if (param.get() === undoit[ai][i]) continue; + } else if (ai === "orientationaxes") { + // orientationaxes has no value, + // it flips everything and the axes + cont.orientation = ({ v: "h", h: "v" })[contFull.orientation]; + } + helpers.swapXYData(cont); + } else if (Plots.dataArrayContainers.indexOf(param.parts[0]) !== -1) { + helpers.manageArrayContainers(param, newVal, undoit); + flags.docalc = true; + } else { + // all the other ones, just modify that one attribute + param.set(newVal); + } + } + + // swap the data attributes of the relevant x and y axes? + if (["swapxyaxes", "orientationaxes"].indexOf(ai) !== -1) { + Plotly.Axes.swap(gd, traces); + } + + // swap hovermode if set to "compare x/y data" + if (ai === "orientationaxes") { + var hovermode = Lib.nestedProperty(gd.layout, "hovermode"); + if (hovermode.get() === "x") { + hovermode.set("y"); + } else if (hovermode.get() === "y") { + hovermode.set("x"); + } + } + + // check if we need to call axis type + if (traces.indexOf(0) !== -1 && axtypeAttrs.indexOf(ai) !== -1) { + Plotly.Axes.clearTypes(gd, traces); + flags.docalc = true; + } + + // switching from auto to manual binning or z scaling doesn't + // actually do anything but change what you see in the styling + // box. everything else at least needs to apply styles + if ( + ["autobinx", "autobiny", "zauto"].indexOf(ai) === -1 || newVal !== false + ) { + flags.dostyle = true; + } + if ( + ["colorbar", "line"].indexOf(param.parts[0]) !== -1 || + param.parts[0] === "marker" && param.parts[1] === "colorbar" + ) { + flags.docolorbars = true; + } + + var aiArrayStart = ai.indexOf("["), + aiAboveArray = aiArrayStart === -1 ? ai : ai.substr(0, aiArrayStart); + + if (recalcAttrs.indexOf(aiAboveArray) !== -1) { + // major enough changes deserve autoscale, autobin, and + // non-reversed axes so people don't get confused + if (["orientation", "type"].indexOf(ai) !== -1) { + axlist = []; + for (i = 0; i < traces.length; i++) { + var trace = data[traces[i]]; + + if (Registry.traceIs(trace, "cartesian")) { + addToAxlist(trace.xaxis || "x"); + addToAxlist(trace.yaxis || "y"); + + if (ai === "type") { + doextra(["autobinx", "autobiny"], true, i); + } + } + } + + doextra(axlist.map(autorangeAttr), true, 0); + doextra(axlist.map(rangeAttr), [0, 1], 0); + } + flags.docalc = true; + } else if (replotAttrs.indexOf(aiAboveArray) !== -1) { + flags.doplot = true; + } else if (autorangeAttrs.indexOf(aiAboveArray) !== -1) { + flags.docalcAutorange = true; + } + } + + // do we need to force a recalc? + Plotly.Axes.list(gd).forEach(function(ax) { + if (ax.autorange) flags.autorangeOn = true; + }); + + // check axes we've flagged for possible deletion + // flagAxForDelete is a hash so we can make sure we only get each axis once + var axListForDelete = Object.keys(flagAxForDelete); + axisLoop: + for (i = 0; i < axListForDelete.length; i++) { + var axId = axListForDelete[i], + axLetter = axId.charAt(0), + axAttr = axLetter + "axis"; + + for (var j = 0; j < data.length; j++) { + if ( + Registry.traceIs(data[j], "cartesian") && + (data[j][axAttr] || axLetter) === axId + ) { + continue axisLoop; + } + } + + // no data on this axis - delete it. + doextra("LAYOUT" + Plotly.Axes.id2name(axId), null, 0); + } + + // combine a few flags together; + if (flags.docalc || flags.docalcAutorange && flags.autorangeOn) { + flags.clearCalc = true; + } + if (flags.docalc || flags.doplot || flags.docalcAutorange) { + flags.fullReplot = true; + } + + return { + flags: flags, + undoit: undoit, + redoit: redoit, + traces: traces, + eventData: Lib.extendDeepNoArrays([], [redoit, traces]) + }; } /** @@ -1687,373 +1833,365 @@ function _restyle(gd, aobj, _traces) { * allows setting multiple attributes simultaneously */ Plotly.relayout = function relayout(gd, astr, val) { - gd = helpers.getGraphDiv(gd); - helpers.clearPromiseQueue(gd); + gd = helpers.getGraphDiv(gd); + helpers.clearPromiseQueue(gd); - if(gd.framework && gd.framework.isPolar) { - return Promise.resolve(gd); - } + if (gd.framework && gd.framework.isPolar) { + return Promise.resolve(gd); + } - var aobj = {}; - if(typeof astr === 'string') aobj[astr] = val; - else if(Lib.isPlainObject(astr)) aobj = astr; - else { - Lib.warn('Relayout fail.', astr, val); - return Promise.reject(); - } + var aobj = {}; + if (typeof astr === "string") { + aobj[astr] = val; + } else if (Lib.isPlainObject(astr)) { + aobj = astr; + } else { + Lib.warn("Relayout fail.", astr, val); + return Promise.reject(); + } - if(Object.keys(aobj).length) gd.changed = true; + if (Object.keys(aobj).length) gd.changed = true; - var specs = _relayout(gd, aobj), - flags = specs.flags; + var specs = _relayout(gd, aobj), flags = specs.flags; - // clear calcdata if required - if(flags.docalc) gd.calcdata = undefined; + // clear calcdata if required + if (flags.docalc) gd.calcdata = undefined; - // fill in redraw sequence - var seq = []; + // fill in redraw sequence + var seq = []; - if(flags.layoutReplot) { - seq.push(subroutines.layoutReplot); - } - else if(Object.keys(aobj).length) { - seq.push(Plots.previousPromises); - Plots.supplyDefaults(gd); - - if(flags.dolegend) seq.push(subroutines.doLegend); - if(flags.dolayoutstyle) seq.push(subroutines.layoutStyles); - if(flags.doticks) seq.push(subroutines.doTicksRelayout); - if(flags.domodebar) seq.push(subroutines.doModeBar); - if(flags.docamera) seq.push(subroutines.doCamera); - } + if (flags.layoutReplot) { + seq.push(subroutines.layoutReplot); + } else if (Object.keys(aobj).length) { + seq.push(Plots.previousPromises); + Plots.supplyDefaults(gd); - Queue.add(gd, - relayout, [gd, specs.undoit], - relayout, [gd, specs.redoit] - ); + if (flags.dolegend) seq.push(subroutines.doLegend); + if (flags.dolayoutstyle) seq.push(subroutines.layoutStyles); + if (flags.doticks) seq.push(subroutines.doTicksRelayout); + if (flags.domodebar) seq.push(subroutines.doModeBar); + if (flags.docamera) seq.push(subroutines.doCamera); + } - var plotDone = Lib.syncOrAsync(seq, gd); - if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); + Queue.add(gd, relayout, [gd, specs.undoit], relayout, [gd, specs.redoit]); - return plotDone.then(function() { - gd.emit('plotly_relayout', specs.eventData); - return gd; - }); + var plotDone = Lib.syncOrAsync(seq, gd); + if (!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); + + return plotDone.then(function() { + gd.emit("plotly_relayout", specs.eventData); + return gd; + }); }; function _relayout(gd, aobj) { - var layout = gd.layout, - fullLayout = gd._fullLayout, - keys = Object.keys(aobj), - axes = Plotly.Axes.list(gd), - i; - - // look for 'allaxes', split out into all axes - // in case of 3D the axis are nested within a scene which is held in _id - for(i = 0; i < keys.length; i++) { - if(keys[i].indexOf('allaxes') === 0) { - for(var j = 0; j < axes.length; j++) { - var scene = axes[j]._id.substr(1), - axisAttr = (scene.indexOf('scene') !== -1) ? (scene + '.') : '', - newkey = keys[i].replace('allaxes', axisAttr + axes[j]._name); - - if(!aobj[newkey]) aobj[newkey] = aobj[keys[i]]; - } - - delete aobj[keys[i]]; - } - } - - // initialize flags - var flags = { - dolegend: false, - doticks: false, - dolayoutstyle: false, - doplot: false, - docalc: false, - domodebar: false, - docamera: false, - layoutReplot: false - }; - - // copies of the change (and previous values of anything affected) - // for the undo / redo queue - var redoit = {}, - undoit = {}; - - // for attrs that interact (like scales & autoscales), save the - // old vals before making the change - // val=undefined will not set a value, just record what the value was. - // attr can be an array to set several at once (all to the same val) - function doextra(attr, val) { - if(Array.isArray(attr)) { - attr.forEach(function(a) { doextra(a, val); }); - return; - } - // quit if explicitly setting this elsewhere - if(attr in aobj) return; - - var p = Lib.nestedProperty(layout, attr); - if(!(attr in undoit)) undoit[attr] = p.get(); - if(val !== undefined) p.set(val); - } - - // for editing annotations or shapes - is it on autoscaled axes? - function refAutorange(obj, axletter) { - var axName = Plotly.Axes.id2name(obj[axletter + 'ref'] || axletter); - return (fullLayout[axName] || {}).autorange; - } - - // alter gd.layout - for(var ai in aobj) { - var p = Lib.nestedProperty(layout, ai), - vi = aobj[ai], - plen = p.parts.length, - // p.parts may end with an index integer if the property is an array - pend = typeof p.parts[plen - 1] === 'string' ? (plen - 1) : (plen - 2), - // last property in chain (leaf node) - pleaf = p.parts[pend], - // leaf plus immediate parent - pleafPlus = p.parts[pend - 1] + '.' + pleaf, - // trunk nodes (everything except the leaf) - ptrunk = p.parts.slice(0, pend).join('.'), - parentIn = Lib.nestedProperty(gd.layout, ptrunk).get(), - parentFull = Lib.nestedProperty(fullLayout, ptrunk).get(); - - if(vi === undefined) continue; - - redoit[ai] = vi; - - // axis reverse is special - it is its own inverse - // op and has no flag. - undoit[ai] = (pleaf === 'reverse') ? vi : p.get(); - - // Setting width or height to null must reset the graph's width / height - // back to its initial value as computed during the first pass in Plots.plotAutoSize. - // - // To do so, we must manually set them back here using the _initialAutoSize cache. - if(['width', 'height'].indexOf(ai) !== -1 && vi === null) { - gd._fullLayout[ai] = gd._initialAutoSize[ai]; - } - // check autorange vs range - else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) { - doextra(ptrunk + '.autorange', false); - } - else if(pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) { - doextra([ptrunk + '.range[0]', ptrunk + '.range[1]'], - undefined); - } - else if(pleafPlus.match(/^aspectratio\.[xyz]$/)) { - doextra(p.parts[0] + '.aspectmode', 'manual'); - } - else if(pleafPlus.match(/^aspectmode$/)) { - doextra([ptrunk + '.x', ptrunk + '.y', ptrunk + '.z'], undefined); - } - else if(pleaf === 'tick0' || pleaf === 'dtick') { - doextra(ptrunk + '.tickmode', 'linear'); - } - else if(pleaf === 'tickmode') { - doextra([ptrunk + '.tick0', ptrunk + '.dtick'], undefined); - } - else if(/[xy]axis[0-9]*?$/.test(pleaf) && !Object.keys(vi || {}).length) { - flags.docalc = true; - } - else if(/[xy]axis[0-9]*\.categoryorder$/.test(pleafPlus)) { - flags.docalc = true; - } - else if(/[xy]axis[0-9]*\.categoryarray/.test(pleafPlus)) { - flags.docalc = true; - } - - if(pleafPlus.indexOf('rangeslider') !== -1) { - flags.docalc = true; - } - - // toggling log without autorange: need to also recalculate ranges - // logical XOR (ie are we toggling log) - if(pleaf === 'type' && ((parentFull.type === 'log') !== (vi === 'log'))) { - var ax = parentIn; - - if(!ax || !ax.range) { - doextra(ptrunk + '.autorange', true); - } - else if(!parentFull.autorange) { - var r0 = ax.range[0], - r1 = ax.range[1]; - if(vi === 'log') { - // if both limits are negative, autorange - if(r0 <= 0 && r1 <= 0) { - doextra(ptrunk + '.autorange', true); - } - // if one is negative, set it 6 orders below the other. - if(r0 <= 0) r0 = r1 / 1e6; - else if(r1 <= 0) r1 = r0 / 1e6; - // now set the range values as appropriate - doextra(ptrunk + '.range[0]', Math.log(r0) / Math.LN10); - doextra(ptrunk + '.range[1]', Math.log(r1) / Math.LN10); - } - else { - doextra(ptrunk + '.range[0]', Math.pow(10, r0)); - doextra(ptrunk + '.range[1]', Math.pow(10, r1)); - } - } - else if(vi === 'log') { - // just make sure the range is positive and in the right - // order, it'll get recalculated later - ax.range = (ax.range[1] > ax.range[0]) ? [1, 2] : [2, 1]; - } - } - - // handle axis reversal explicitly, as there's no 'reverse' flag - if(pleaf === 'reverse') { - if(parentIn.range) parentIn.range.reverse(); - else { - doextra(ptrunk + '.autorange', true); - parentIn.range = [1, 0]; - } - - if(parentFull.autorange) flags.docalc = true; - else flags.doplot = true; - } - // send annotation and shape mods one-by-one through Annotations.draw(), - // don't set via nestedProperty - // that's because add and remove are special - else if(p.parts[0] === 'annotations' || p.parts[0] === 'shapes') { - var objNum = p.parts[1], - objType = p.parts[0], - objList = layout[objType] || [], - obji = objList[objNum] || {}; - - // if p.parts is just an annotation number, and val is either - // 'add' or an entire annotation to add, the undo is 'remove' - // if val is 'remove' then undo is the whole annotation object - if(p.parts.length === 2) { - - // new API, remove annotation / shape with `null` - if(vi === null) aobj[ai] = 'remove'; - - if(aobj[ai] === 'add' || Lib.isPlainObject(aobj[ai])) { - undoit[ai] = 'remove'; - } - else if(aobj[ai] === 'remove') { - if(objNum === -1) { - undoit[objType] = objList; - delete undoit[ai]; - } - else undoit[ai] = obji; - } - else Lib.log('???', aobj); - } - - if((refAutorange(obji, 'x') || refAutorange(obji, 'y')) && - !Lib.containsAny(ai, ['color', 'opacity', 'align', 'dash'])) { - flags.docalc = true; - } - - // TODO: combine all edits to a given annotation / shape into one call - // as it is we get separate calls for x and y (or ax and ay) on move - - var drawOne = Registry.getComponentMethod(objType, 'drawOne'); - drawOne(gd, objNum, p.parts.slice(2).join('.'), aobj[ai]); - delete aobj[ai]; - } - else if( - Plots.layoutArrayContainers.indexOf(p.parts[0]) !== -1 || - (p.parts[0] === 'mapbox' && p.parts[1] === 'layers') - ) { - helpers.manageArrayContainers(p, vi, undoit); - flags.doplot = true; - } - // alter gd.layout - else { - var pp1 = String(p.parts[1] || ''); - // check whether we can short-circuit a full redraw - // 3d or geo at this point just needs to redraw. - if(p.parts[0].indexOf('scene') === 0) { - if(p.parts[1] === 'camera') flags.docamera = true; - else flags.doplot = true; - } - else if(p.parts[0].indexOf('geo') === 0) flags.doplot = true; - else if(p.parts[0].indexOf('ternary') === 0) flags.doplot = true; - else if(ai === 'paper_bgcolor') flags.doplot = true; - else if(fullLayout._has('gl2d') && - (ai.indexOf('axis') !== -1 || p.parts[0] === 'plot_bgcolor') - ) flags.doplot = true; - else if(ai === 'hiddenlabels') flags.docalc = true; - else if(p.parts[0].indexOf('legend') !== -1) flags.dolegend = true; - else if(ai.indexOf('title') !== -1) flags.doticks = true; - else if(p.parts[0].indexOf('bgcolor') !== -1) flags.dolayoutstyle = true; - else if(p.parts.length > 1 && - Lib.containsAny(pp1, ['tick', 'exponent', 'grid', 'zeroline'])) { - flags.doticks = true; - } - else if(ai.indexOf('.linewidth') !== -1 && - ai.indexOf('axis') !== -1) { - flags.doticks = flags.dolayoutstyle = true; - } - else if(p.parts.length > 1 && pp1.indexOf('line') !== -1) { - flags.dolayoutstyle = true; - } - else if(p.parts.length > 1 && pp1 === 'mirror') { - flags.doticks = flags.dolayoutstyle = true; - } - else if(ai === 'margin.pad') { - flags.doticks = flags.dolayoutstyle = true; - } - else if(p.parts[0] === 'margin' || - p.parts[1] === 'autorange' || - p.parts[1] === 'rangemode' || - p.parts[1] === 'type' || - p.parts[1] === 'domain' || - ai.indexOf('calendar') !== -1 || - ai.match(/^(bar|box|font)/)) { - flags.docalc = true; - } - /* + var layout = gd.layout, + fullLayout = gd._fullLayout, + keys = Object.keys(aobj), + axes = Plotly.Axes.list(gd), + i; + + // look for 'allaxes', split out into all axes + // in case of 3D the axis are nested within a scene which is held in _id + for (i = 0; i < keys.length; i++) { + if (keys[i].indexOf("allaxes") === 0) { + for (var j = 0; j < axes.length; j++) { + var scene = axes[j]._id.substr(1), + axisAttr = scene.indexOf("scene") !== -1 ? scene + "." : "", + newkey = keys[i].replace("allaxes", axisAttr + axes[j]._name); + + if (!aobj[newkey]) aobj[newkey] = aobj[keys[i]]; + } + + delete aobj[keys[i]]; + } + } + + // initialize flags + var flags = { + dolegend: false, + doticks: false, + dolayoutstyle: false, + doplot: false, + docalc: false, + domodebar: false, + docamera: false, + layoutReplot: false + }; + + // copies of the change (and previous values of anything affected) + // for the undo / redo queue + var redoit = {}, undoit = {}; + + // for attrs that interact (like scales & autoscales), save the + // old vals before making the change + // val=undefined will not set a value, just record what the value was. + // attr can be an array to set several at once (all to the same val) + function doextra(attr, val) { + if (Array.isArray(attr)) { + attr.forEach(function(a) { + doextra(a, val); + }); + return; + } + // quit if explicitly setting this elsewhere + if (attr in aobj) return; + + var p = Lib.nestedProperty(layout, attr); + if (!(attr in undoit)) undoit[attr] = p.get(); + if (val !== undefined) p.set(val); + } + + // for editing annotations or shapes - is it on autoscaled axes? + function refAutorange(obj, axletter) { + var axName = Plotly.Axes.id2name(obj[axletter + "ref"] || axletter); + return (fullLayout[axName] || {}).autorange; + } + + // alter gd.layout + for (var ai in aobj) { + var p = Lib.nestedProperty(layout, ai), + vi = aobj[ai], + plen = p.parts.length, + // p.parts may end with an index integer if the property is an array + pend = typeof p.parts[plen - 1] === "string" ? plen - 1 : plen - 2, + // last property in chain (leaf node) + pleaf = p.parts[pend], + // leaf plus immediate parent + pleafPlus = p.parts[pend - 1] + "." + pleaf, + // trunk nodes (everything except the leaf) + ptrunk = p.parts.slice(0, pend).join("."), + parentIn = Lib.nestedProperty(gd.layout, ptrunk).get(), + parentFull = Lib.nestedProperty(fullLayout, ptrunk).get(); + + if (vi === undefined) continue; + + redoit[ai] = vi; + + // axis reverse is special - it is its own inverse + // op and has no flag. + undoit[ai] = pleaf === "reverse" ? vi : p.get(); + + // Setting width or height to null must reset the graph's width / height + // back to its initial value as computed during the first pass in Plots.plotAutoSize. + // + // To do so, we must manually set them back here using the _initialAutoSize cache. + if (["width", "height"].indexOf(ai) !== -1 && vi === null) { + gd._fullLayout[ai] = gd._initialAutoSize[ai]; + } else if (pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) { + // check autorange vs range + doextra(ptrunk + ".autorange", false); + } else if (pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) { + doextra([ptrunk + ".range[0]", ptrunk + ".range[1]"], undefined); + } else if (pleafPlus.match(/^aspectratio\.[xyz]$/)) { + doextra(p.parts[0] + ".aspectmode", "manual"); + } else if (pleafPlus.match(/^aspectmode$/)) { + doextra([ptrunk + ".x", ptrunk + ".y", ptrunk + ".z"], undefined); + } else if (pleaf === "tick0" || pleaf === "dtick") { + doextra(ptrunk + ".tickmode", "linear"); + } else if (pleaf === "tickmode") { + doextra([ptrunk + ".tick0", ptrunk + ".dtick"], undefined); + } else if ( + /[xy]axis[0-9]*?$/.test(pleaf) && !Object.keys(vi || {}).length + ) { + flags.docalc = true; + } else if (/[xy]axis[0-9]*\.categoryorder$/.test(pleafPlus)) { + flags.docalc = true; + } else if (/[xy]axis[0-9]*\.categoryarray/.test(pleafPlus)) { + flags.docalc = true; + } + + if (pleafPlus.indexOf("rangeslider") !== -1) { + flags.docalc = true; + } + + // toggling log without autorange: need to also recalculate ranges + // logical XOR (ie are we toggling log) + if (pleaf === "type" && parentFull.type === "log" !== (vi === "log")) { + var ax = parentIn; + + if (!ax || !ax.range) { + doextra(ptrunk + ".autorange", true); + } else if (!parentFull.autorange) { + var r0 = ax.range[0], r1 = ax.range[1]; + if (vi === "log") { + // if both limits are negative, autorange + if (r0 <= 0 && r1 <= 0) { + doextra(ptrunk + ".autorange", true); + } + // if one is negative, set it 6 orders below the other. + if (r0 <= 0) r0 = r1 / 1e6; + else if (r1 <= 0) r1 = r0 / 1e6; + // now set the range values as appropriate + doextra(ptrunk + ".range[0]", Math.log(r0) / Math.LN10); + doextra(ptrunk + ".range[1]", Math.log(r1) / Math.LN10); + } else { + doextra(ptrunk + ".range[0]", Math.pow(10, r0)); + doextra(ptrunk + ".range[1]", Math.pow(10, r1)); + } + } else if (vi === "log") { + // just make sure the range is positive and in the right + // order, it'll get recalculated later + ax.range = ax.range[1] > ax.range[0] ? [1, 2] : [2, 1]; + } + } + + // handle axis reversal explicitly, as there's no 'reverse' flag + if (pleaf === "reverse") { + if (parentIn.range) { + parentIn.range.reverse(); + } else { + doextra(ptrunk + ".autorange", true); + parentIn.range = [1, 0]; + } + + if (parentFull.autorange) flags.docalc = true; + else flags.doplot = true; + } else if (p.parts[0] === "annotations" || p.parts[0] === "shapes") { + // send annotation and shape mods one-by-one through Annotations.draw(), + // don't set via nestedProperty + // that's because add and remove are special + var objNum = p.parts[1], + objType = p.parts[0], + objList = layout[objType] || [], + obji = objList[objNum] || {}; + + // if p.parts is just an annotation number, and val is either + // 'add' or an entire annotation to add, the undo is 'remove' + // if val is 'remove' then undo is the whole annotation object + if (p.parts.length === 2) { + // new API, remove annotation / shape with `null` + if (vi === null) aobj[ai] = "remove"; + + if (aobj[ai] === "add" || Lib.isPlainObject(aobj[ai])) { + undoit[ai] = "remove"; + } else if (aobj[ai] === "remove") { + if (objNum === -1) { + undoit[objType] = objList; + delete undoit[ai]; + } else { + undoit[ai] = obji; + } + } else { + Lib.log("???", aobj); + } + } + + if ( + (refAutorange(obji, "x") || refAutorange(obji, "y")) && + !Lib.containsAny(ai, ["color", "opacity", "align", "dash"]) + ) { + flags.docalc = true; + } + + // TODO: combine all edits to a given annotation / shape into one call + // as it is we get separate calls for x and y (or ax and ay) on move + var drawOne = Registry.getComponentMethod(objType, "drawOne"); + drawOne(gd, objNum, p.parts.slice(2).join("."), aobj[ai]); + delete aobj[ai]; + } else if ( + Plots.layoutArrayContainers.indexOf(p.parts[0]) !== -1 || + p.parts[0] === "mapbox" && p.parts[1] === "layers" + ) { + helpers.manageArrayContainers(p, vi, undoit); + flags.doplot = true; + } else { + // alter gd.layout + var pp1 = String(p.parts[1] || ""); + // check whether we can short-circuit a full redraw + // 3d or geo at this point just needs to redraw. + if (p.parts[0].indexOf("scene") === 0) { + if (p.parts[1] === "camera") flags.docamera = true; + else flags.doplot = true; + } else if (p.parts[0].indexOf("geo") === 0) { + flags.doplot = true; + } else if (p.parts[0].indexOf("ternary") === 0) { + flags.doplot = true; + } else if (ai === "paper_bgcolor") { + flags.doplot = true; + } else if ( + fullLayout._has("gl2d") && + (ai.indexOf("axis") !== -1 || p.parts[0] === "plot_bgcolor") + ) { + flags.doplot = true; + } else if (ai === "hiddenlabels") { + flags.docalc = true; + } else if (p.parts[0].indexOf("legend") !== -1) { + flags.dolegend = true; + } else if (ai.indexOf("title") !== -1) { + flags.doticks = true; + } else if (p.parts[0].indexOf("bgcolor") !== -1) { + flags.dolayoutstyle = true; + } else if ( + p.parts.length > 1 && + Lib.containsAny(pp1, ["tick", "exponent", "grid", "zeroline"]) + ) { + flags.doticks = true; + } else if (ai.indexOf(".linewidth") !== -1 && ai.indexOf("axis") !== -1) { + flags.doticks = flags.dolayoutstyle = true; + } else if (p.parts.length > 1 && pp1.indexOf("line") !== -1) { + flags.dolayoutstyle = true; + } else if (p.parts.length > 1 && pp1 === "mirror") { + flags.doticks = flags.dolayoutstyle = true; + } else if (ai === "margin.pad") { + flags.doticks = flags.dolayoutstyle = true; + } else if ( + p.parts[0] === "margin" || + p.parts[1] === "autorange" || + p.parts[1] === "rangemode" || + p.parts[1] === "type" || + p.parts[1] === "domain" || + ai.indexOf("calendar") !== -1 || + ai.match(/^(bar|box|font)/) + ) { + flags.docalc = true; + } else if (["hovermode", "dragmode"].indexOf(ai) !== -1) { + /* * hovermode and dragmode don't need any redrawing, since they just * affect reaction to user input, everything else, assume full replot. * height, width, autosize get dealt with below. Except for the case of * of subplots - scenes - which require scene.updateFx to be called. */ - else if(['hovermode', 'dragmode'].indexOf(ai) !== -1) flags.domodebar = true; - else if(['hovermode', 'dragmode', 'height', - 'width', 'autosize'].indexOf(ai) === -1) { - flags.doplot = true; - } - - p.set(vi); - } - } - - var oldWidth = gd._fullLayout.width, - oldHeight = gd._fullLayout.height; - - // coerce the updated layout - Plots.supplyDefaults(gd); - - // calculate autosizing - if(gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, gd._fullLayout); - - // avoid unnecessary redraws - var hasSizechanged = aobj.height || aobj.width || - (gd._fullLayout.width !== oldWidth) || - (gd._fullLayout.height !== oldHeight); - - if(hasSizechanged) flags.docalc = true; - - if(flags.doplot || flags.docalc) { - flags.layoutReplot = true; - } - - // now all attribute mods are done, as are - // redo and undo so we can save them - - return { - flags: flags, - undoit: undoit, - redoit: redoit, - eventData: Lib.extendDeep({}, redoit) - }; + flags.domodebar = true; + } else if ( + ["hovermode", "dragmode", "height", "width", "autosize"].indexOf(ai) === + -1 + ) { + flags.doplot = true; + } + + p.set(vi); + } + } + + var oldWidth = gd._fullLayout.width, oldHeight = gd._fullLayout.height; + + // coerce the updated layout + Plots.supplyDefaults(gd); + + // calculate autosizing + if (gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, gd._fullLayout); + + // avoid unnecessary redraws + var hasSizechanged = aobj.height || + aobj.width || + gd._fullLayout.width !== oldWidth || + gd._fullLayout.height !== oldHeight; + + if (hasSizechanged) flags.docalc = true; + + if (flags.doplot || flags.docalc) { + flags.layoutReplot = true; + } + + // now all attribute mods are done, as are + // redo and undo so we can save them + return { + flags: flags, + undoit: undoit, + redoit: redoit, + eventData: Lib.extendDeep({}, redoit) + }; } /** @@ -2072,77 +2210,78 @@ function _relayout(gd, aobj) { * */ Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) { - gd = helpers.getGraphDiv(gd); - helpers.clearPromiseQueue(gd); - - if(gd.framework && gd.framework.isPolar) { - return Promise.resolve(gd); - } + gd = helpers.getGraphDiv(gd); + helpers.clearPromiseQueue(gd); - if(!Lib.isPlainObject(traceUpdate)) traceUpdate = {}; - if(!Lib.isPlainObject(layoutUpdate)) layoutUpdate = {}; + if (gd.framework && gd.framework.isPolar) { + return Promise.resolve(gd); + } - if(Object.keys(traceUpdate).length) gd.changed = true; - if(Object.keys(layoutUpdate).length) gd.changed = true; + if (!Lib.isPlainObject(traceUpdate)) traceUpdate = {}; + if (!Lib.isPlainObject(layoutUpdate)) layoutUpdate = {}; - var restyleSpecs = _restyle(gd, traceUpdate, traces), - restyleFlags = restyleSpecs.flags; + if (Object.keys(traceUpdate).length) gd.changed = true; + if (Object.keys(layoutUpdate).length) gd.changed = true; - var relayoutSpecs = _relayout(gd, layoutUpdate), - relayoutFlags = relayoutSpecs.flags; + var restyleSpecs = _restyle(gd, traceUpdate, traces), + restyleFlags = restyleSpecs.flags; - // clear calcdata if required - if(restyleFlags.clearCalc || relayoutFlags.docalc) gd.calcdata = undefined; + var relayoutSpecs = _relayout(gd, layoutUpdate), + relayoutFlags = relayoutSpecs.flags; - // fill in redraw sequence - var seq = []; + // clear calcdata if required + if (restyleFlags.clearCalc || relayoutFlags.docalc) gd.calcdata = undefined; - if(restyleFlags.fullReplot && relayoutFlags.layoutReplot) { - var data = gd.data, - layout = gd.layout; - - // clear existing data/layout on gd - // so that Plotly.plot doesn't try to extend them - gd.data = undefined; - gd.layout = undefined; - - seq.push(function() { return Plotly.plot(gd, data, layout); }); - } - else if(restyleFlags.fullReplot) { - seq.push(Plotly.plot); - } - else if(relayoutFlags.layoutReplot) { - seq.push(subroutines.layoutReplot); - } - else { - seq.push(Plots.previousPromises); - Plots.supplyDefaults(gd); - - if(restyleFlags.dostyle) seq.push(subroutines.doTraceStyle); - if(restyleFlags.docolorbars) seq.push(subroutines.doColorBars); - if(relayoutFlags.dolegend) seq.push(subroutines.doLegend); - if(relayoutFlags.dolayoutstyle) seq.push(subroutines.layoutStyles); - if(relayoutFlags.doticks) seq.push(subroutines.doTicksRelayout); - if(relayoutFlags.domodebar) seq.push(subroutines.doModeBar); - if(relayoutFlags.doCamera) seq.push(subroutines.doCamera); - } + // fill in redraw sequence + var seq = []; - Queue.add(gd, - update, [gd, restyleSpecs.undoit, relayoutSpecs.undoit, restyleSpecs.traces], - update, [gd, restyleSpecs.redoit, relayoutSpecs.redoit, restyleSpecs.traces] - ); + if (restyleFlags.fullReplot && relayoutFlags.layoutReplot) { + var data = gd.data, layout = gd.layout; - var plotDone = Lib.syncOrAsync(seq, gd); - if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); + // clear existing data/layout on gd + // so that Plotly.plot doesn't try to extend them + gd.data = undefined; + gd.layout = undefined; - return plotDone.then(function() { - gd.emit('plotly_update', { - data: restyleSpecs.eventData, - layout: relayoutSpecs.eventData - }); + seq.push(function() { + return Plotly.plot(gd, data, layout); + }); + } else if (restyleFlags.fullReplot) { + seq.push(Plotly.plot); + } else if (relayoutFlags.layoutReplot) { + seq.push(subroutines.layoutReplot); + } else { + seq.push(Plots.previousPromises); + Plots.supplyDefaults(gd); - return gd; + if (restyleFlags.dostyle) seq.push(subroutines.doTraceStyle); + if (restyleFlags.docolorbars) seq.push(subroutines.doColorBars); + if (relayoutFlags.dolegend) seq.push(subroutines.doLegend); + if (relayoutFlags.dolayoutstyle) seq.push(subroutines.layoutStyles); + if (relayoutFlags.doticks) seq.push(subroutines.doTicksRelayout); + if (relayoutFlags.domodebar) seq.push(subroutines.doModeBar); + if (relayoutFlags.doCamera) seq.push(subroutines.doCamera); + } + + Queue.add( + gd, + update, + [gd, restyleSpecs.undoit, relayoutSpecs.undoit, restyleSpecs.traces], + update, + [gd, restyleSpecs.redoit, relayoutSpecs.redoit, restyleSpecs.traces] + ); + + var plotDone = Lib.syncOrAsync(seq, gd); + if (!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); + + return plotDone.then(function() { + gd.emit("plotly_update", { + data: restyleSpecs.eventData, + layout: relayoutSpecs.eventData }); + + return gd; + }); }; /** @@ -2173,348 +2312,364 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) { * configuration for the animation */ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) { - gd = helpers.getGraphDiv(gd); - - if(!Lib.isPlotDiv(gd)) { - throw new Error( - 'This element is not a Plotly plot: ' + gd + '. It\'s likely that you\'ve failed ' + - 'to create a plot before animating it. For more details, see ' + - 'https://plot.ly/javascript/animations/' - ); - } - - var trans = gd._transitionData; - - // This is the queue of frames that will be animated as soon as possible. They - // are popped immediately upon the *start* of a transition: - if(!trans._frameQueue) { - trans._frameQueue = []; + gd = helpers.getGraphDiv(gd); + + if (!Lib.isPlotDiv(gd)) { + throw new Error( + "This element is not a Plotly plot: " + + gd + + ". It's likely that you've failed " + + "to create a plot before animating it. For more details, see " + + "https://plot.ly/javascript/animations/" + ); + } + + var trans = gd._transitionData; + + // This is the queue of frames that will be animated as soon as possible. They + // are popped immediately upon the *start* of a transition: + if (!trans._frameQueue) { + trans._frameQueue = []; + } + + animationOpts = Plots.supplyAnimationDefaults(animationOpts); + var transitionOpts = animationOpts.transition; + var frameOpts = animationOpts.frame; + + // Since frames are popped immediately, an empty queue only means all frames have + // *started* to transition, not that the animation is complete. To solve that, + // track a separate counter that increments at the same time as frames are added + // to the queue, but decrements only when the transition is complete. + if (trans._frameWaitingCnt === undefined) { + trans._frameWaitingCnt = 0; + } + + function getTransitionOpts(i) { + if (Array.isArray(transitionOpts)) { + if (i >= transitionOpts.length) { + return transitionOpts[0]; + } else { + return transitionOpts[i]; + } + } else { + return transitionOpts; } + } - animationOpts = Plots.supplyAnimationDefaults(animationOpts); - var transitionOpts = animationOpts.transition; - var frameOpts = animationOpts.frame; - - // Since frames are popped immediately, an empty queue only means all frames have - // *started* to transition, not that the animation is complete. To solve that, - // track a separate counter that increments at the same time as frames are added - // to the queue, but decrements only when the transition is complete. - if(trans._frameWaitingCnt === undefined) { - trans._frameWaitingCnt = 0; - } + function getFrameOpts(i) { + if (Array.isArray(frameOpts)) { + if (i >= frameOpts.length) { + return frameOpts[0]; + } else { + return frameOpts[i]; + } + } else { + return frameOpts; + } + } + + // Execute a callback after the wrapper function has been called n times. + // This is used to defer the resolution until a transition has resovled *and* + // the frame has completed. If it's not done this way, then we get a race + // condition in which the animation might resolve before a transition is complete + // or vice versa. + function callbackOnNthTime(cb, n) { + var cnt = 0; + return function() { + if (cb && ++cnt === n) { + return cb(); + } + }; + } - function getTransitionOpts(i) { - if(Array.isArray(transitionOpts)) { - if(i >= transitionOpts.length) { - return transitionOpts[0]; - } else { - return transitionOpts[i]; - } - } else { - return transitionOpts; - } - } + return new Promise(function(resolve, reject) { + function discardExistingFrames() { + if (trans._frameQueue.length === 0) { + return; + } - function getFrameOpts(i) { - if(Array.isArray(frameOpts)) { - if(i >= frameOpts.length) { - return frameOpts[0]; - } else { - return frameOpts[i]; - } - } else { - return frameOpts; + while (trans._frameQueue.length) { + var next = trans._frameQueue.pop(); + if (next.onInterrupt) { + next.onInterrupt(); } - } + } - // Execute a callback after the wrapper function has been called n times. - // This is used to defer the resolution until a transition has resovled *and* - // the frame has completed. If it's not done this way, then we get a race - // condition in which the animation might resolve before a transition is complete - // or vice versa. - function callbackOnNthTime(cb, n) { - var cnt = 0; - return function() { - if(cb && ++cnt === n) { - return cb(); - } - }; + gd.emit("plotly_animationinterrupted", []); } - return new Promise(function(resolve, reject) { - function discardExistingFrames() { - if(trans._frameQueue.length === 0) { - return; - } + function queueFrames(frameList) { + if (frameList.length === 0) return; - while(trans._frameQueue.length) { - var next = trans._frameQueue.pop(); - if(next.onInterrupt) { - next.onInterrupt(); - } - } + for (var i = 0; i < frameList.length; i++) { + var computedFrame; - gd.emit('plotly_animationinterrupted', []); - } - - function queueFrames(frameList) { - if(frameList.length === 0) return; - - for(var i = 0; i < frameList.length; i++) { - var computedFrame; - - if(frameList[i].type === 'byname') { - // If it's a named frame, compute it: - computedFrame = Plots.computeFrame(gd, frameList[i].name); - } else { - // Otherwise we must have been given a simple object, so treat - // the input itself as the computed frame. - computedFrame = frameList[i].data; - } - - var frameOpts = getFrameOpts(i); - var transitionOpts = getTransitionOpts(i); - - // It doesn't make much sense for the transition duration to be greater than - // the frame duration, so limit it: - transitionOpts.duration = Math.min(transitionOpts.duration, frameOpts.duration); - - var nextFrame = { - frame: computedFrame, - name: frameList[i].name, - frameOpts: frameOpts, - transitionOpts: transitionOpts, - }; - if(i === frameList.length - 1) { - // The last frame in this .animate call stores the promise resolve - // and reject callbacks. This is how we ensure that the animation - // loop (which may exist as a result of a *different* .animate call) - // still resolves or rejecdts this .animate call's promise. once it's - // complete. - nextFrame.onComplete = callbackOnNthTime(resolve, 2); - nextFrame.onInterrupt = reject; - } - - trans._frameQueue.push(nextFrame); - } - - // Set it as never having transitioned to a frame. This will cause the animation - // loop to immediately transition to the next frame (which, for immediate mode, - // is the first frame in the list since all others would have been discarded - // below) - if(animationOpts.mode === 'immediate') { - trans._lastFrameAt = -Infinity; - } - - // Only it's not already running, start a RAF loop. This could be avoided in the - // case that there's only one frame, but it significantly complicated the logic - // and only sped things up by about 5% or so for a lorenz attractor simulation. - // It would be a fine thing to implement, but the benefit of that optimization - // doesn't seem worth the extra complexity. - if(!trans._animationRaf) { - beginAnimationLoop(); - } - } - - function stopAnimationLoop() { - gd.emit('plotly_animated'); - - // Be sure to unset also since it's how we know whether a loop is already running: - window.cancelAnimationFrame(trans._animationRaf); - trans._animationRaf = null; - } - - function nextFrame() { - if(trans._currentFrame && trans._currentFrame.onComplete) { - // Execute the callback and unset it to ensure it doesn't - // accidentally get called twice - trans._currentFrame.onComplete(); - } - - var newFrame = trans._currentFrame = trans._frameQueue.shift(); - - if(newFrame) { - // Since it's sometimes necessary to do deep digging into frame data, - // we'll consider it not 100% impossible for nulls or numbers to sneak through, - // so check when casting the name, just to be absolutely certain: - var stringName = newFrame.name ? newFrame.name.toString() : null; - gd._fullLayout._currentFrame = stringName; - - trans._lastFrameAt = Date.now(); - trans._timeToNext = newFrame.frameOpts.duration; - - // This is simply called and it's left to .transition to decide how to manage - // interrupting current transitions. That means we don't need to worry about - // how it resolves or what happens after this: - Plots.transition(gd, - newFrame.frame.data, - newFrame.frame.layout, - helpers.coerceTraceIndices(gd, newFrame.frame.traces), - newFrame.frameOpts, - newFrame.transitionOpts - ).then(function() { - if(newFrame.onComplete) { - newFrame.onComplete(); - } - - }); - - gd.emit('plotly_animatingframe', { - name: stringName, - frame: newFrame.frame, - animation: { - frame: newFrame.frameOpts, - transition: newFrame.transitionOpts, - } - }); - } else { - // If there are no more frames, then stop the RAF loop: - stopAnimationLoop(); - } + if (frameList[i].type === "byname") { + // If it's a named frame, compute it: + computedFrame = Plots.computeFrame(gd, frameList[i].name); + } else { + // Otherwise we must have been given a simple object, so treat + // the input itself as the computed frame. + computedFrame = frameList[i].data; } - function beginAnimationLoop() { - gd.emit('plotly_animating'); + var frameOpts = getFrameOpts(i); + var transitionOpts = getTransitionOpts(i); - // If no timer is running, then set last frame = long ago so that the next - // frame is immediately transitioned: - trans._lastFrameAt = -Infinity; - trans._timeToNext = 0; - trans._runningTransitions = 0; - trans._currentFrame = null; - - var doFrame = function() { - // This *must* be requested before nextFrame since nextFrame may decide - // to cancel it if there's nothing more to animated: - trans._animationRaf = window.requestAnimationFrame(doFrame); - - // Check if we're ready for a new frame: - if(Date.now() - trans._lastFrameAt > trans._timeToNext) { - nextFrame(); - } - }; + // It doesn't make much sense for the transition duration to be greater than + // the frame duration, so limit it: + transitionOpts.duration = Math.min( + transitionOpts.duration, + frameOpts.duration + ); - doFrame(); - } + var nextFrame = { + frame: computedFrame, + name: frameList[i].name, + frameOpts: frameOpts, + transitionOpts: transitionOpts + }; + if (i === frameList.length - 1) { + // The last frame in this .animate call stores the promise resolve + // and reject callbacks. This is how we ensure that the animation + // loop (which may exist as a result of a *different* .animate call) + // still resolves or rejecdts this .animate call's promise. once it's + // complete. + nextFrame.onComplete = callbackOnNthTime(resolve, 2); + nextFrame.onInterrupt = reject; + } + + trans._frameQueue.push(nextFrame); + } + + // Set it as never having transitioned to a frame. This will cause the animation + // loop to immediately transition to the next frame (which, for immediate mode, + // is the first frame in the list since all others would have been discarded + // below) + if (animationOpts.mode === "immediate") { + trans._lastFrameAt = -Infinity; + } + + // Only it's not already running, start a RAF loop. This could be avoided in the + // case that there's only one frame, but it significantly complicated the logic + // and only sped things up by about 5% or so for a lorenz attractor simulation. + // It would be a fine thing to implement, but the benefit of that optimization + // doesn't seem worth the extra complexity. + if (!trans._animationRaf) { + beginAnimationLoop(); + } + } + + function stopAnimationLoop() { + gd.emit("plotly_animated"); + + // Be sure to unset also since it's how we know whether a loop is already running: + window.cancelAnimationFrame(trans._animationRaf); + trans._animationRaf = null; + } + + function nextFrame() { + if (trans._currentFrame && trans._currentFrame.onComplete) { + // Execute the callback and unset it to ensure it doesn't + // accidentally get called twice + trans._currentFrame.onComplete(); + } + + var newFrame = trans._currentFrame = trans._frameQueue.shift(); + + if (newFrame) { + // Since it's sometimes necessary to do deep digging into frame data, + // we'll consider it not 100% impossible for nulls or numbers to sneak through, + // so check when casting the name, just to be absolutely certain: + var stringName = newFrame.name ? newFrame.name.toString() : null; + gd._fullLayout._currentFrame = stringName; + + trans._lastFrameAt = Date.now(); + trans._timeToNext = newFrame.frameOpts.duration; + + // This is simply called and it's left to .transition to decide how to manage + // interrupting current transitions. That means we don't need to worry about + // how it resolves or what happens after this: + Plots.transition( + gd, + newFrame.frame.data, + newFrame.frame.layout, + helpers.coerceTraceIndices(gd, newFrame.frame.traces), + newFrame.frameOpts, + newFrame.transitionOpts + ).then(function() { + if (newFrame.onComplete) { + newFrame.onComplete(); + } + }); - // This is an animate-local counter that helps match up option input list - // items with the particular frame. - var configCounter = 0; - function setTransitionConfig(frame) { - if(Array.isArray(transitionOpts)) { - if(configCounter >= transitionOpts.length) { - frame.transitionOpts = transitionOpts[configCounter]; - } else { - frame.transitionOpts = transitionOpts[0]; - } - } else { - frame.transitionOpts = transitionOpts; - } - configCounter++; - return frame; - } + gd.emit("plotly_animatingframe", { + name: stringName, + frame: newFrame.frame, + animation: { + frame: newFrame.frameOpts, + transition: newFrame.transitionOpts + } + }); + } else { + // If there are no more frames, then stop the RAF loop: + stopAnimationLoop(); + } + } - // Disambiguate what's sort of frames have been received - var i, frame; - var frameList = []; - var allFrames = frameOrGroupNameOrFrameList === undefined || frameOrGroupNameOrFrameList === null; - var isFrameArray = Array.isArray(frameOrGroupNameOrFrameList); - var isSingleFrame = !allFrames && !isFrameArray && Lib.isPlainObject(frameOrGroupNameOrFrameList); - - if(isSingleFrame) { - // In this case, a simple object has been passed to animate. - frameList.push({ - type: 'object', - data: setTransitionConfig(Lib.extendFlat({}, frameOrGroupNameOrFrameList)) - }); - } else if(allFrames || ['string', 'number'].indexOf(typeof frameOrGroupNameOrFrameList) !== -1) { - // In this case, null or undefined has been passed so that we want to - // animate *all* currently defined frames - for(i = 0; i < trans._frames.length; i++) { - frame = trans._frames[i]; - - if(!frame) continue; - - if(allFrames || String(frame.group) === String(frameOrGroupNameOrFrameList)) { - frameList.push({ - type: 'byname', - name: String(frame.name), - data: setTransitionConfig({name: frame.name}) - }); - } - } - } else if(isFrameArray) { - for(i = 0; i < frameOrGroupNameOrFrameList.length; i++) { - var frameOrName = frameOrGroupNameOrFrameList[i]; - if(['number', 'string'].indexOf(typeof frameOrName) !== -1) { - frameOrName = String(frameOrName); - // In this case, there's an array and this frame is a string name: - frameList.push({ - type: 'byname', - name: frameOrName, - data: setTransitionConfig({name: frameOrName}) - }); - } else if(Lib.isPlainObject(frameOrName)) { - frameList.push({ - type: 'object', - data: setTransitionConfig(Lib.extendFlat({}, frameOrName)) - }); - } - } - } + function beginAnimationLoop() { + gd.emit("plotly_animating"); - // Verify that all of these frames actually exist; return and reject if not: - for(i = 0; i < frameList.length; i++) { - frame = frameList[i]; - if(frame.type === 'byname' && !trans._frameHash[frame.data.name]) { - Lib.warn('animate failure: frame not found: "' + frame.data.name + '"'); - reject(); - return; - } - } + // If no timer is running, then set last frame = long ago so that the next + // frame is immediately transitioned: + trans._lastFrameAt = -Infinity; + trans._timeToNext = 0; + trans._runningTransitions = 0; + trans._currentFrame = null; - // If the mode is either next or immediate, then all currently queued frames must - // be dumped and the corresponding .animate promises rejected. - if(['next', 'immediate'].indexOf(animationOpts.mode) !== -1) { - discardExistingFrames(); - } + var doFrame = function() { + // This *must* be requested before nextFrame since nextFrame may decide + // to cancel it if there's nothing more to animated: + trans._animationRaf = window.requestAnimationFrame(doFrame); - if(animationOpts.direction === 'reverse') { - frameList.reverse(); + // Check if we're ready for a new frame: + if (Date.now() - trans._lastFrameAt > trans._timeToNext) { + nextFrame(); } + }; - var currentFrame = gd._fullLayout._currentFrame; - if(currentFrame && animationOpts.fromcurrent) { - var idx = -1; - for(i = 0; i < frameList.length; i++) { - frame = frameList[i]; - if(frame.type === 'byname' && frame.name === currentFrame) { - idx = i; - break; - } - } - - if(idx > 0 && idx < frameList.length - 1) { - var filteredFrameList = []; - for(i = 0; i < frameList.length; i++) { - frame = frameList[i]; - if(frameList[i].type !== 'byname' || i > idx) { - filteredFrameList.push(frame); - } - } - frameList = filteredFrameList; - } - } + doFrame(); + } - if(frameList.length > 0) { - queueFrames(frameList); + // This is an animate-local counter that helps match up option input list + // items with the particular frame. + var configCounter = 0; + function setTransitionConfig(frame) { + if (Array.isArray(transitionOpts)) { + if (configCounter >= transitionOpts.length) { + frame.transitionOpts = transitionOpts[configCounter]; } else { - // This is the case where there were simply no frames. It's a little strange - // since there's not much to do: - gd.emit('plotly_animated'); - resolve(); - } - }); + frame.transitionOpts = transitionOpts[0]; + } + } else { + frame.transitionOpts = transitionOpts; + } + configCounter++; + return frame; + } + + // Disambiguate what's sort of frames have been received + var i, frame; + var frameList = []; + var allFrames = frameOrGroupNameOrFrameList === undefined || + frameOrGroupNameOrFrameList === null; + var isFrameArray = Array.isArray(frameOrGroupNameOrFrameList); + var isSingleFrame = !allFrames && + !isFrameArray && + Lib.isPlainObject(frameOrGroupNameOrFrameList); + + if (isSingleFrame) { + // In this case, a simple object has been passed to animate. + frameList.push({ + type: "object", + data: setTransitionConfig( + Lib.extendFlat({}, frameOrGroupNameOrFrameList) + ) + }); + } else if ( + allFrames || + ["string", "number"].indexOf(typeof frameOrGroupNameOrFrameList) !== -1 + ) { + // In this case, null or undefined has been passed so that we want to + // animate *all* currently defined frames + for (i = 0; i < trans._frames.length; i++) { + frame = trans._frames[i]; + + if (!frame) continue; + + if ( + allFrames || + String(frame.group) === String(frameOrGroupNameOrFrameList) + ) { + frameList.push({ + type: "byname", + name: String(frame.name), + data: setTransitionConfig({ name: frame.name }) + }); + } + } + } else if (isFrameArray) { + for (i = 0; i < frameOrGroupNameOrFrameList.length; i++) { + var frameOrName = frameOrGroupNameOrFrameList[i]; + if (["number", "string"].indexOf(typeof frameOrName) !== -1) { + frameOrName = String(frameOrName); + // In this case, there's an array and this frame is a string name: + frameList.push({ + type: "byname", + name: frameOrName, + data: setTransitionConfig({ name: frameOrName }) + }); + } else if (Lib.isPlainObject(frameOrName)) { + frameList.push({ + type: "object", + data: setTransitionConfig(Lib.extendFlat({}, frameOrName)) + }); + } + } + } + + // Verify that all of these frames actually exist; return and reject if not: + for (i = 0; i < frameList.length; i++) { + frame = frameList[i]; + if (frame.type === "byname" && !trans._frameHash[frame.data.name]) { + Lib.warn('animate failure: frame not found: "' + frame.data.name + '"'); + reject(); + return; + } + } + + // If the mode is either next or immediate, then all currently queued frames must + // be dumped and the corresponding .animate promises rejected. + if (["next", "immediate"].indexOf(animationOpts.mode) !== -1) { + discardExistingFrames(); + } + + if (animationOpts.direction === "reverse") { + frameList.reverse(); + } + + var currentFrame = gd._fullLayout._currentFrame; + if (currentFrame && animationOpts.fromcurrent) { + var idx = -1; + for (i = 0; i < frameList.length; i++) { + frame = frameList[i]; + if (frame.type === "byname" && frame.name === currentFrame) { + idx = i; + break; + } + } + + if (idx > 0 && idx < frameList.length - 1) { + var filteredFrameList = []; + for (i = 0; i < frameList.length; i++) { + frame = frameList[i]; + if (frameList[i].type !== "byname" || i > idx) { + filteredFrameList.push(frame); + } + } + frameList = filteredFrameList; + } + } + + if (frameList.length > 0) { + queueFrames(frameList); + } else { + // This is the case where there were simply no frames. It's a little strange + // since there's not much to do: + gd.emit("plotly_animated"); + resolve(); + } + }); }; /** @@ -2537,118 +2692,133 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) { * will be overwritten. */ Plotly.addFrames = function(gd, frameList, indices) { - gd = helpers.getGraphDiv(gd); - - var numericNameWarningCount = 0; - - if(frameList === null || frameList === undefined) { - return Promise.resolve(); - } - - if(!Lib.isPlotDiv(gd)) { - throw new Error( - 'This element is not a Plotly plot: ' + gd + '. It\'s likely that you\'ve failed ' + - 'to create a plot before adding frames. For more details, see ' + - 'https://plot.ly/javascript/animations/' - ); - } + gd = helpers.getGraphDiv(gd); - var i, frame, j, idx; - var _frames = gd._transitionData._frames; - var _hash = gd._transitionData._frameHash; + var numericNameWarningCount = 0; + if (frameList === null || frameList === undefined) { + return Promise.resolve(); + } + + if (!Lib.isPlotDiv(gd)) { + throw new Error( + "This element is not a Plotly plot: " + + gd + + ". It's likely that you've failed " + + "to create a plot before adding frames. For more details, see " + + "https://plot.ly/javascript/animations/" + ); + } - if(!Array.isArray(frameList)) { - throw new Error('addFrames failure: frameList must be an Array of frame definitions' + frameList); - } - - // Create a sorted list of insertions since we run into lots of problems if these - // aren't in ascending order of index: - // - // Strictly for sorting. Make sure this is guaranteed to never collide with any - // already-exisisting indices: - var bigIndex = _frames.length + frameList.length * 2; - - var insertions = []; - for(i = frameList.length - 1; i >= 0; i--) { - if(!Lib.isPlainObject(frameList[i])) continue; - - var name = (_hash[frameList[i].name] || {}).name; - var newName = frameList[i].name; - - if(name && newName && typeof newName === 'number' && _hash[name]) { - numericNameWarningCount++; - - Lib.warn('addFrames: overwriting frame "' + _hash[name].name + - '" with a frame whose name of type "number" also equates to "' + - name + '". This is valid but may potentially lead to unexpected ' + - 'behavior since all plotly.js frame names are stored internally ' + - 'as strings.'); - - if(numericNameWarningCount > 5) { - Lib.warn('addFrames: This API call has yielded too many warnings. ' + - 'For the rest of this call, further warnings about numeric frame ' + - 'names will be suppressed.'); - } - } + var i, frame, j, idx; + var _frames = gd._transitionData._frames; + var _hash = gd._transitionData._frameHash; - insertions.push({ - frame: Plots.supplyFrameDefaults(frameList[i]), - index: (indices && indices[i] !== undefined && indices[i] !== null) ? indices[i] : bigIndex + i - }); + if (!Array.isArray(frameList)) { + throw new Error( + "addFrames failure: frameList must be an Array of frame definitions" + + frameList + ); + } + + // Create a sorted list of insertions since we run into lots of problems if these + // aren't in ascending order of index: + // + // Strictly for sorting. Make sure this is guaranteed to never collide with any + // already-exisisting indices: + var bigIndex = _frames.length + frameList.length * 2; + + var insertions = []; + for (i = frameList.length - 1; i >= 0; i--) { + if (!Lib.isPlainObject(frameList[i])) continue; + + var name = (_hash[frameList[i].name] || {}).name; + var newName = frameList[i].name; + + if (name && newName && typeof newName === "number" && _hash[name]) { + numericNameWarningCount++; + + Lib.warn( + 'addFrames: overwriting frame "' + + _hash[name].name + + '" with a frame whose name of type "number" also equates to "' + + name + + '". This is valid but may potentially lead to unexpected ' + + "behavior since all plotly.js frame names are stored internally " + + "as strings." + ); + + if (numericNameWarningCount > 5) { + Lib.warn( + "addFrames: This API call has yielded too many warnings. " + + "For the rest of this call, further warnings about numeric frame " + + "names will be suppressed." + ); + } } - // Sort this, taking note that undefined insertions end up at the end: - insertions.sort(function(a, b) { - if(a.index > b.index) return -1; - if(a.index < b.index) return 1; - return 0; + insertions.push({ + frame: Plots.supplyFrameDefaults(frameList[i]), + index: ( + indices && indices[i] !== undefined && indices[i] !== null + ? indices[i] + : bigIndex + i + ) }); + } + + // Sort this, taking note that undefined insertions end up at the end: + insertions.sort(function(a, b) { + if (a.index > b.index) return -1; + if (a.index < b.index) return 1; + return 0; + }); + + var ops = []; + var revops = []; + var frameCount = _frames.length; + + for (i = insertions.length - 1; i >= 0; i--) { + frame = insertions[i].frame; + + if (typeof frame.name === "number") { + Lib.warn( + "Warning: addFrames accepts frames with numeric names, but the numbers are" + + "implicitly cast to strings" + ); + } + + if (!frame.name) { + // Repeatedly assign a default name, incrementing the counter each time until + // we get a name that's not in the hashed lookup table: + while (_hash[frame.name = "frame " + gd._transitionData._counter++]); + } + + if (_hash[frame.name]) { + // If frame is present, overwrite its definition: + for (j = 0; j < _frames.length; j++) { + if ((_frames[j] || {}).name === frame.name) break; + } + ops.push({ type: "replace", index: j, value: frame }); + revops.unshift({ type: "replace", index: j, value: _frames[j] }); + } else { + // Otherwise insert it at the end of the list: + idx = Math.max(0, Math.min(insertions[i].index, frameCount)); - var ops = []; - var revops = []; - var frameCount = _frames.length; - - for(i = insertions.length - 1; i >= 0; i--) { - frame = insertions[i].frame; - - if(typeof frame.name === 'number') { - Lib.warn('Warning: addFrames accepts frames with numeric names, but the numbers are' + - 'implicitly cast to strings'); - - } - - if(!frame.name) { - // Repeatedly assign a default name, incrementing the counter each time until - // we get a name that's not in the hashed lookup table: - while(_hash[(frame.name = 'frame ' + gd._transitionData._counter++)]); - } - - if(_hash[frame.name]) { - // If frame is present, overwrite its definition: - for(j = 0; j < _frames.length; j++) { - if((_frames[j] || {}).name === frame.name) break; - } - ops.push({type: 'replace', index: j, value: frame}); - revops.unshift({type: 'replace', index: j, value: _frames[j]}); - } else { - // Otherwise insert it at the end of the list: - idx = Math.max(0, Math.min(insertions[i].index, frameCount)); - - ops.push({type: 'insert', index: idx, value: frame}); - revops.unshift({type: 'delete', index: idx}); - frameCount++; - } + ops.push({ type: "insert", index: idx, value: frame }); + revops.unshift({ type: "delete", index: idx }); + frameCount++; } + } - var undoFunc = Plots.modifyFrames, - redoFunc = Plots.modifyFrames, - undoArgs = [gd, revops], - redoArgs = [gd, ops]; + var undoFunc = Plots.modifyFrames, + redoFunc = Plots.modifyFrames, + undoArgs = [gd, revops], + redoArgs = [gd, ops]; - if(Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + if (Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); - return Plots.modifyFrames(gd, ops); + return Plots.modifyFrames(gd, ops); }; /** @@ -2661,34 +2831,34 @@ Plotly.addFrames = function(gd, frameList, indices) { * list of integer indices of frames to be deleted */ Plotly.deleteFrames = function(gd, frameList) { - gd = helpers.getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); - if(!Lib.isPlotDiv(gd)) { - throw new Error('This element is not a Plotly plot: ' + gd); - } + if (!Lib.isPlotDiv(gd)) { + throw new Error("This element is not a Plotly plot: " + gd); + } - var i, idx; - var _frames = gd._transitionData._frames; - var ops = []; - var revops = []; + var i, idx; + var _frames = gd._transitionData._frames; + var ops = []; + var revops = []; - frameList = frameList.slice(0); - frameList.sort(); + frameList = frameList.slice(0); + frameList.sort(); - for(i = frameList.length - 1; i >= 0; i--) { - idx = frameList[i]; - ops.push({type: 'delete', index: idx}); - revops.unshift({type: 'insert', index: idx, value: _frames[idx]}); - } + for (i = frameList.length - 1; i >= 0; i--) { + idx = frameList[i]; + ops.push({ type: "delete", index: idx }); + revops.unshift({ type: "insert", index: idx, value: _frames[idx] }); + } - var undoFunc = Plots.modifyFrames, - redoFunc = Plots.modifyFrames, - undoArgs = [gd, revops], - redoArgs = [gd, ops]; + var undoFunc = Plots.modifyFrames, + redoFunc = Plots.modifyFrames, + undoArgs = [gd, revops], + redoArgs = [gd, ops]; - if(Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + if (Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); - return Plots.modifyFrames(gd, ops); + return Plots.modifyFrames(gd, ops); }; /** @@ -2698,131 +2868,157 @@ Plotly.deleteFrames = function(gd, frameList) { * the id or DOM element of the graph container div */ Plotly.purge = function purge(gd) { - gd = helpers.getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); - var fullLayout = gd._fullLayout || {}, - fullData = gd._fullData || []; + var fullLayout = gd._fullLayout || {}, fullData = gd._fullData || []; - // remove gl contexts - Plots.cleanPlot([], {}, fullData, fullLayout); + // remove gl contexts + Plots.cleanPlot([], {}, fullData, fullLayout); - // purge properties - Plots.purge(gd); + // purge properties + Plots.purge(gd); - // purge event emitter methods - Events.purge(gd); + // purge event emitter methods + Events.purge(gd); - // remove plot container - if(fullLayout._container) fullLayout._container.remove(); + // remove plot container + if (fullLayout._container) fullLayout._container.remove(); - delete gd._context; - delete gd._replotPending; - delete gd._mouseDownTime; - delete gd._hmpixcount; - delete gd._hmlumcount; + delete gd._context; + delete gd._replotPending; + delete gd._mouseDownTime; + delete gd._hmpixcount; + delete gd._hmlumcount; - return gd; + return gd; }; // ------------------------------------------------------- // makePlotFramework: Create the plot container and axes // ------------------------------------------------------- function makePlotFramework(gd) { - var gd3 = d3.select(gd), - fullLayout = gd._fullLayout; - - // Plot container - fullLayout._container = gd3.selectAll('.plot-container').data([0]); - fullLayout._container.enter().insert('div', ':first-child') - .classed('plot-container', true) - .classed('plotly', true); - - // Make the svg container - fullLayout._paperdiv = fullLayout._container.selectAll('.svg-container').data([0]); - fullLayout._paperdiv.enter().append('div') - .classed('svg-container', true) - .style('position', 'relative'); - - // Make the graph containers - // start fresh each time we get here, so we know the order comes out - // right, rather than enter/exit which can muck up the order - // TODO: sort out all the ordering so we don't have to - // explicitly delete anything - fullLayout._glcontainer = fullLayout._paperdiv.selectAll('.gl-container') - .data([0]); - fullLayout._glcontainer.enter().append('div') - .classed('gl-container', true); - - fullLayout._geocontainer = fullLayout._paperdiv.selectAll('.geo-container') - .data([0]); - fullLayout._geocontainer.enter().append('div') - .classed('geo-container', true); - - fullLayout._paperdiv.selectAll('.main-svg').remove(); - - fullLayout._paper = fullLayout._paperdiv.insert('svg', ':first-child') - .classed('main-svg', true); - - fullLayout._toppaper = fullLayout._paperdiv.append('svg') - .classed('main-svg', true); - - if(!fullLayout._uid) { - var otherUids = []; - d3.selectAll('defs').each(function() { - if(this.id) otherUids.push(this.id.split('-')[1]); - }); - fullLayout._uid = Lib.randstr(otherUids); - } - - fullLayout._paperdiv.selectAll('.main-svg') - .attr(xmlnsNamespaces.svgAttrs); - - fullLayout._defs = fullLayout._paper.append('defs') - .attr('id', 'defs-' + fullLayout._uid); - - fullLayout._topdefs = fullLayout._toppaper.append('defs') - .attr('id', 'topdefs-' + fullLayout._uid); - - fullLayout._draggers = fullLayout._paper.append('g') - .classed('draglayer', true); - - // lower shape layer - // (only for shapes to be drawn below the whole plot) - var layerBelow = fullLayout._paper.append('g') - .classed('layer-below', true); - fullLayout._imageLowerLayer = layerBelow.append('g') - .classed('imagelayer', true); - fullLayout._shapeLowerLayer = layerBelow.append('g') - .classed('shapelayer', true); - - // single cartesian layer for the whole plot - fullLayout._cartesianlayer = fullLayout._paper.append('g').classed('cartesianlayer', true); - - // single ternary layer for the whole plot - fullLayout._ternarylayer = fullLayout._paper.append('g').classed('ternarylayer', true); - - // upper shape layer - // (only for shapes to be drawn above the whole plot, including subplots) - var layerAbove = fullLayout._paper.append('g') - .classed('layer-above', true); - fullLayout._imageUpperLayer = layerAbove.append('g') - .classed('imagelayer', true); - fullLayout._shapeUpperLayer = layerAbove.append('g') - .classed('shapelayer', true); - - // single pie layer for the whole plot - fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true); - - // fill in image server scrape-svg - fullLayout._glimages = fullLayout._paper.append('g').classed('glimages', true); - fullLayout._geoimages = fullLayout._paper.append('g').classed('geoimages', true); - - // lastly info (legend, annotations) and hover layers go on top - // these are in a different svg element normally, but get collapsed into a single - // svg when exporting (after inserting 3D) - fullLayout._infolayer = fullLayout._toppaper.append('g').classed('infolayer', true); - fullLayout._zoomlayer = fullLayout._toppaper.append('g').classed('zoomlayer', true); - fullLayout._hoverlayer = fullLayout._toppaper.append('g').classed('hoverlayer', true); - - gd.emit('plotly_framework'); + var gd3 = d3.select(gd), fullLayout = gd._fullLayout; + + // Plot container + fullLayout._container = gd3.selectAll(".plot-container").data([0]); + fullLayout._container + .enter() + .insert("div", ":first-child") + .classed("plot-container", true) + .classed("plotly", true); + + // Make the svg container + fullLayout._paperdiv = fullLayout._container + .selectAll(".svg-container") + .data([0]); + fullLayout._paperdiv + .enter() + .append("div") + .classed("svg-container", true) + .style("position", "relative"); + + // Make the graph containers + // start fresh each time we get here, so we know the order comes out + // right, rather than enter/exit which can muck up the order + // TODO: sort out all the ordering so we don't have to + // explicitly delete anything + fullLayout._glcontainer = fullLayout._paperdiv + .selectAll(".gl-container") + .data([0]); + fullLayout._glcontainer.enter().append("div").classed("gl-container", true); + + fullLayout._geocontainer = fullLayout._paperdiv + .selectAll(".geo-container") + .data([0]); + fullLayout._geocontainer.enter().append("div").classed("geo-container", true); + + fullLayout._paperdiv.selectAll(".main-svg").remove(); + + fullLayout._paper = fullLayout._paperdiv + .insert("svg", ":first-child") + .classed("main-svg", true); + + fullLayout._toppaper = fullLayout._paperdiv + .append("svg") + .classed("main-svg", true); + + if (!fullLayout._uid) { + var otherUids = []; + d3.selectAll("defs").each(function() { + if (this.id) otherUids.push(this.id.split("-")[1]); + }); + fullLayout._uid = Lib.randstr(otherUids); + } + + fullLayout._paperdiv.selectAll(".main-svg").attr(xmlnsNamespaces.svgAttrs); + + fullLayout._defs = fullLayout._paper + .append("defs") + .attr("id", "defs-" + fullLayout._uid); + + fullLayout._topdefs = fullLayout._toppaper + .append("defs") + .attr("id", "topdefs-" + fullLayout._uid); + + fullLayout._draggers = fullLayout._paper + .append("g") + .classed("draglayer", true); + + // lower shape layer + // (only for shapes to be drawn below the whole plot) + var layerBelow = fullLayout._paper.append("g").classed("layer-below", true); + fullLayout._imageLowerLayer = layerBelow + .append("g") + .classed("imagelayer", true); + fullLayout._shapeLowerLayer = layerBelow + .append("g") + .classed("shapelayer", true); + + // single cartesian layer for the whole plot + fullLayout._cartesianlayer = fullLayout._paper + .append("g") + .classed("cartesianlayer", true); + + // single ternary layer for the whole plot + fullLayout._ternarylayer = fullLayout._paper + .append("g") + .classed("ternarylayer", true); + + // upper shape layer + // (only for shapes to be drawn above the whole plot, including subplots) + var layerAbove = fullLayout._paper.append("g").classed("layer-above", true); + fullLayout._imageUpperLayer = layerAbove + .append("g") + .classed("imagelayer", true); + fullLayout._shapeUpperLayer = layerAbove + .append("g") + .classed("shapelayer", true); + + // single pie layer for the whole plot + fullLayout._pielayer = fullLayout._paper + .append("g") + .classed("pielayer", true); + + // fill in image server scrape-svg + fullLayout._glimages = fullLayout._paper + .append("g") + .classed("glimages", true); + fullLayout._geoimages = fullLayout._paper + .append("g") + .classed("geoimages", true); + + // lastly info (legend, annotations) and hover layers go on top + // these are in a different svg element normally, but get collapsed into a single + // svg when exporting (after inserting 3D) + fullLayout._infolayer = fullLayout._toppaper + .append("g") + .classed("infolayer", true); + fullLayout._zoomlayer = fullLayout._toppaper + .append("g") + .classed("zoomlayer", true); + fullLayout._hoverlayer = fullLayout._toppaper + .append("g") + .classed("hoverlayer", true); + + gd.emit("plotly_framework"); } diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index d54dd1a05a6..f382090aa12 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -5,9 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; /* eslint-disable no-console */ /** @@ -19,100 +17,75 @@ */ module.exports = { - - // no interactivity, for export or image generation - staticPlot: false, - - // we can edit titles, move annotations, etc - editable: false, - - // DO autosize once regardless of layout.autosize - // (use default width or height values otherwise) - autosizable: false, - - // set the length of the undo/redo queue - queueLength: 0, - - // if we DO autosize, do we fill the container or the screen? - fillFrame: false, - - // if we DO autosize, set the frame margins in percents of plot size - frameMargins: 0, - - // mousewheel or two-finger scroll zooms the plot - scrollZoom: false, - - // double click interaction (false, 'reset', 'autosize' or 'reset+autosize') - doubleClick: 'reset+autosize', - - // new users see some hints about interactivity - showTips: true, - - // link to open this plot in plotly - showLink: false, - - // if we show a link, does it contain data or just link to a plotly file? - sendData: true, - - // text appearing in the sendData link - linkText: 'Edit chart', - - // false or function adding source(s) to linkText - showSources: false, - - // display the mode bar (true, false, or 'hover') - displayModeBar: 'hover', - - // remove mode bar button by name - // (see ./components/modebar/buttons.js for the list of names) - modeBarButtonsToRemove: [], - - // add mode bar button using config objects - // (see ./components/modebar/buttons.js for list of arguments) - modeBarButtonsToAdd: [], - - // fully custom mode bar buttons as nested array, - // where the outer arrays represents button groups, and - // the inner arrays have buttons config objects or names of default buttons - // (see ./components/modebar/buttons.js for more info) - modeBarButtons: false, - - // add the plotly logo on the end of the mode bar - displaylogo: true, - - // increase the pixel ratio for Gl plot images - plotGlPixelRatio: 2, - - // function to add the background color to a different container - // or 'opaque' to ensure there's white behind it - setBackground: defaultSetBackground, - - // URL to topojson files used in geo charts - topojsonURL: 'https://cdn.plot.ly/', - - // Mapbox access token (required to plot mapbox trace types) - // If using an Mapbox Atlas server, set this option to '', - // so that plotly.js won't attempt to authenticate to the public Mapbox server. - mapboxAccessToken: null, - - // Turn all console logging on or off (errors will be thrown) - // This should ONLY be set via Plotly.setPlotConfig - logging: false, - - // Set global transform to be applied to all traces with no - // specification needed - globalTransforms: [] + // no interactivity, for export or image generation + staticPlot: false, + // we can edit titles, move annotations, etc + editable: false, + // DO autosize once regardless of layout.autosize + // (use default width or height values otherwise) + autosizable: false, + // set the length of the undo/redo queue + queueLength: 0, + // if we DO autosize, do we fill the container or the screen? + fillFrame: false, + // if we DO autosize, set the frame margins in percents of plot size + frameMargins: 0, + // mousewheel or two-finger scroll zooms the plot + scrollZoom: false, + // double click interaction (false, 'reset', 'autosize' or 'reset+autosize') + doubleClick: "reset+autosize", + // new users see some hints about interactivity + showTips: true, + // link to open this plot in plotly + showLink: false, + // if we show a link, does it contain data or just link to a plotly file? + sendData: true, + // text appearing in the sendData link + linkText: "Edit chart", + // false or function adding source(s) to linkText + showSources: false, + // display the mode bar (true, false, or 'hover') + displayModeBar: "hover", + // remove mode bar button by name + // (see ./components/modebar/buttons.js for the list of names) + modeBarButtonsToRemove: [], + // add mode bar button using config objects + // (see ./components/modebar/buttons.js for list of arguments) + modeBarButtonsToAdd: [], + // fully custom mode bar buttons as nested array, + // where the outer arrays represents button groups, and + // the inner arrays have buttons config objects or names of default buttons + // (see ./components/modebar/buttons.js for more info) + modeBarButtons: false, + // add the plotly logo on the end of the mode bar + displaylogo: true, + // increase the pixel ratio for Gl plot images + plotGlPixelRatio: 2, + // function to add the background color to a different container + // or 'opaque' to ensure there's white behind it + setBackground: defaultSetBackground, + // URL to topojson files used in geo charts + topojsonURL: "https://cdn.plot.ly/", + // Mapbox access token (required to plot mapbox trace types) + // If using an Mapbox Atlas server, set this option to '', + // so that plotly.js won't attempt to authenticate to the public Mapbox server. + mapboxAccessToken: null, + // Turn all console logging on or off (errors will be thrown) + // This should ONLY be set via Plotly.setPlotConfig + logging: false, + // Set global transform to be applied to all traces with no + // specification needed + globalTransforms: [] }; // where and how the background gets set can be overridden by context // so we define the default (plotly.js) behavior here function defaultSetBackground(gd, bgColor) { - try { - gd._fullLayout._paper.style('background', bgColor); - } - catch(e) { - if(module.exports.logging > 0) { - console.error(e); - } + try { + gd._fullLayout._paper.style("background", bgColor); + } catch (e) { + if (module.exports.logging > 0) { + console.error(e); } + } } diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 2075674108a..2cd76726ed6 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -5,28 +5,25 @@ * 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 Registry = require("../registry"); +var Lib = require("../lib"); - -'use strict'; - -var Registry = require('../registry'); -var Lib = require('../lib'); - -var baseAttributes = require('../plots/attributes'); -var baseLayoutAttributes = require('../plots/layout_attributes'); -var frameAttributes = require('../plots/frame_attributes'); -var animationAttributes = require('../plots/animation_attributes'); +var baseAttributes = require("../plots/attributes"); +var baseLayoutAttributes = require("../plots/layout_attributes"); +var frameAttributes = require("../plots/frame_attributes"); +var animationAttributes = require("../plots/animation_attributes"); // polar attributes are not part of the Registry yet -var polarAreaAttrs = require('../plots/polar/area_attributes'); -var polarAxisAttrs = require('../plots/polar/axis_attributes'); +var polarAreaAttrs = require("../plots/polar/area_attributes"); +var polarAxisAttrs = require("../plots/polar/axis_attributes"); var extendFlat = Lib.extendFlat; var extendDeep = Lib.extendDeep; -var IS_SUBPLOT_OBJ = '_isSubplotObj'; -var IS_LINKED_TO_ARRAY = '_isLinkedToArray'; -var DEPRECATED = '_deprecated'; +var IS_SUBPLOT_OBJ = "_isSubplotObj"; +var IS_LINKED_TO_ARRAY = "_isLinkedToArray"; +var DEPRECATED = "_deprecated"; var UNDERSCORE_ATTRS = [IS_SUBPLOT_OBJ, IS_LINKED_TO_ARRAY, DEPRECATED]; exports.IS_SUBPLOT_OBJ = IS_SUBPLOT_OBJ; @@ -46,32 +43,29 @@ exports.UNDERSCORE_ATTRS = UNDERSCORE_ATTRS; * - config (coming soon ...) */ exports.get = function() { - var traces = {}; - - Registry.allTypes.concat('area').forEach(function(type) { - traces[type] = getTraceAttributes(type); - }); - - var transforms = {}; - - Object.keys(Registry.transformsRegistry).forEach(function(type) { - transforms[type] = getTransformAttributes(type); - }); - - return { - defs: { - valObjects: Lib.valObjects, - metaKeys: UNDERSCORE_ATTRS.concat(['description', 'role']) - }, - - traces: traces, - layout: getLayoutAttributes(), - - transforms: transforms, - - frames: getFramesAttributes(), - animation: formatAttributes(animationAttributes) - }; + var traces = {}; + + Registry.allTypes.concat("area").forEach(function(type) { + traces[type] = getTraceAttributes(type); + }); + + var transforms = {}; + + Object.keys(Registry.transformsRegistry).forEach(function(type) { + transforms[type] = getTransformAttributes(type); + }); + + return { + defs: { + valObjects: Lib.valObjects, + metaKeys: UNDERSCORE_ATTRS.concat(["description", "role"]) + }, + traces: traces, + layout: getLayoutAttributes(), + transforms: transforms, + frames: getFramesAttributes(), + animation: formatAttributes(animationAttributes) + }; }; /** @@ -98,19 +92,19 @@ exports.get = function() { * copy of transformIn that contains attribute defaults */ exports.crawl = function(attrs, callback, specifiedLevel) { - var level = specifiedLevel || 0; + var level = specifiedLevel || 0; - Object.keys(attrs).forEach(function(attrName) { - var attr = attrs[attrName]; + Object.keys(attrs).forEach(function(attrName) { + var attr = attrs[attrName]; - if(UNDERSCORE_ATTRS.indexOf(attrName) !== -1) return; + if (UNDERSCORE_ATTRS.indexOf(attrName) !== -1) return; - callback(attr, attrName, attrs, level); + callback(attr, attrName, attrs, level); - if(exports.isValObject(attr)) return; + if (exports.isValObject(attr)) return; - if(Lib.isPlainObject(attr)) exports.crawl(attr, callback, level + 1); - }); + if (Lib.isPlainObject(attr)) exports.crawl(attr, callback, level + 1); + }); }; /** Is object a value object (or a container object)? @@ -121,7 +115,7 @@ exports.crawl = function(attrs, callback, specifiedLevel) { * false for tree nodes in the attribute hierarchy */ exports.isValObject = function(obj) { - return obj && obj.valType !== undefined; + return obj && obj.valType !== undefined; }; /** @@ -135,272 +129,267 @@ exports.isValObject = function(obj) { * list of array attributes for the given trace */ exports.findArrayAttributes = function(trace) { - var arrayAttributes = [], - stack = []; + var arrayAttributes = [], stack = []; - function callback(attr, attrName, attrs, level) { - stack = stack.slice(0, level).concat([attrName]); + function callback(attr, attrName, attrs, level) { + stack = stack.slice(0, level).concat([attrName]); - var splittableAttr = attr && (attr.valType === 'data_array' || attr.arrayOk === true); - if(!splittableAttr) return; + var splittableAttr = attr && + (attr.valType === "data_array" || attr.arrayOk === true); + if (!splittableAttr) return; - var astr = toAttrString(stack); - var val = Lib.nestedProperty(trace, astr).get(); - if(!Array.isArray(val)) return; + var astr = toAttrString(stack); + var val = Lib.nestedProperty(trace, astr).get(); + if (!Array.isArray(val)) return; - arrayAttributes.push(astr); - } + arrayAttributes.push(astr); + } - function toAttrString(stack) { - return stack.join('.'); - } + function toAttrString(stack) { + return stack.join("."); + } - exports.crawl(trace._module.attributes, callback); + exports.crawl(trace._module.attributes, callback); - if(trace.transforms) { - var transforms = trace.transforms; + if (trace.transforms) { + var transforms = trace.transforms; - for(var i = 0; i < transforms.length; i++) { - var transform = transforms[i]; + for (var i = 0; i < transforms.length; i++) { + var transform = transforms[i]; - stack = ['transforms[' + i + ']']; + stack = ["transforms[" + i + "]"]; - exports.crawl(transform._module.attributes, callback, 1); - } + exports.crawl(transform._module.attributes, callback, 1); } + } - // Look into the fullInput module attributes for array attributes - // to make sure that 'custom' array attributes are detected. - // - // At the moment, we need this block to make sure that - // ohlc and candlestick 'open', 'high', 'low', 'close' can be - // used with filter ang groupby transforms. - if(trace._fullInput) { - exports.crawl(trace._fullInput._module.attributes, callback); + // Look into the fullInput module attributes for array attributes + // to make sure that 'custom' array attributes are detected. + // + // At the moment, we need this block to make sure that + // ohlc and candlestick 'open', 'high', 'low', 'close' can be + // used with filter ang groupby transforms. + if (trace._fullInput) { + exports.crawl(trace._fullInput._module.attributes, callback); - arrayAttributes = Lib.filterUnique(arrayAttributes); - } + arrayAttributes = Lib.filterUnique(arrayAttributes); + } - return arrayAttributes; + return arrayAttributes; }; function getTraceAttributes(type) { - var _module, basePlotModule; - - if(type === 'area') { - _module = { attributes: polarAreaAttrs }; - basePlotModule = {}; - } - else { - _module = Registry.modules[type]._module, - basePlotModule = _module.basePlotModule; - } - - var attributes = {}; - - // make 'type' the first attribute in the object - attributes.type = null; - - // base attributes (same for all trace types) - extendDeep(attributes, baseAttributes); - - // module attributes - extendDeep(attributes, _module.attributes); - - // subplot attributes - if(basePlotModule.attributes) { - extendDeep(attributes, basePlotModule.attributes); + var _module, basePlotModule; + + if (type === "area") { + _module = { attributes: polarAreaAttrs }; + basePlotModule = {}; + } else { + _module = Registry.modules[ + type + ]._module, basePlotModule = _module.basePlotModule; + } + + var attributes = {}; + + // make 'type' the first attribute in the object + attributes.type = null; + + // base attributes (same for all trace types) + extendDeep(attributes, baseAttributes); + + // module attributes + extendDeep(attributes, _module.attributes); + + // subplot attributes + if (basePlotModule.attributes) { + extendDeep(attributes, basePlotModule.attributes); + } + + // add registered components trace attributes + Object.keys(Registry.componentsRegistry).forEach(function(k) { + var _module = Registry.componentsRegistry[k]; + + if ( + _module.schema && _module.schema.traces && _module.schema.traces[type] + ) { + Object.keys(_module.schema.traces[type]).forEach(function(v) { + insertAttrs(attributes, _module.schema.traces[type][v], v); + }); } + }); - // add registered components trace attributes - Object.keys(Registry.componentsRegistry).forEach(function(k) { - var _module = Registry.componentsRegistry[k]; + // 'type' gets overwritten by baseAttributes; reset it here + attributes.type = type; - if(_module.schema && _module.schema.traces && _module.schema.traces[type]) { - Object.keys(_module.schema.traces[type]).forEach(function(v) { - insertAttrs(attributes, _module.schema.traces[type][v], v); - }); - } - }); + var out = { + meta: _module.meta || {}, + attributes: formatAttributes(attributes) + }; - // 'type' gets overwritten by baseAttributes; reset it here - attributes.type = type; - - var out = { - meta: _module.meta || {}, - attributes: formatAttributes(attributes), - }; - - // trace-specific layout attributes - if(_module.layoutAttributes) { - var layoutAttributes = {}; + // trace-specific layout attributes + if (_module.layoutAttributes) { + var layoutAttributes = {}; - extendDeep(layoutAttributes, _module.layoutAttributes); - out.layoutAttributes = formatAttributes(layoutAttributes); - } + extendDeep(layoutAttributes, _module.layoutAttributes); + out.layoutAttributes = formatAttributes(layoutAttributes); + } - return out; + return out; } function getLayoutAttributes() { - var layoutAttributes = {}; + var layoutAttributes = {}; - // global layout attributes - extendDeep(layoutAttributes, baseLayoutAttributes); + // global layout attributes + extendDeep(layoutAttributes, baseLayoutAttributes); - // add base plot module layout attributes - Object.keys(Registry.subplotsRegistry).forEach(function(k) { - var _module = Registry.subplotsRegistry[k]; + // add base plot module layout attributes + Object.keys(Registry.subplotsRegistry).forEach(function(k) { + var _module = Registry.subplotsRegistry[k]; - if(!_module.layoutAttributes) return; + if (!_module.layoutAttributes) return; - if(_module.name === 'cartesian') { - handleBasePlotModule(layoutAttributes, _module, 'xaxis'); - handleBasePlotModule(layoutAttributes, _module, 'yaxis'); - } - else { - var astr = _module.attr === 'subplot' ? _module.name : _module.attr; + if (_module.name === "cartesian") { + handleBasePlotModule(layoutAttributes, _module, "xaxis"); + handleBasePlotModule(layoutAttributes, _module, "yaxis"); + } else { + var astr = _module.attr === "subplot" ? _module.name : _module.attr; - handleBasePlotModule(layoutAttributes, _module, astr); - } - }); + handleBasePlotModule(layoutAttributes, _module, astr); + } + }); - // polar layout attributes - layoutAttributes = assignPolarLayoutAttrs(layoutAttributes); + // polar layout attributes + layoutAttributes = assignPolarLayoutAttrs(layoutAttributes); - // add registered components layout attributes - Object.keys(Registry.componentsRegistry).forEach(function(k) { - var _module = Registry.componentsRegistry[k]; + // add registered components layout attributes + Object.keys(Registry.componentsRegistry).forEach(function(k) { + var _module = Registry.componentsRegistry[k]; - if(!_module.layoutAttributes) return; + if (!_module.layoutAttributes) return; - if(_module.schema && _module.schema.layout) { - Object.keys(_module.schema.layout).forEach(function(v) { - insertAttrs(layoutAttributes, _module.schema.layout[v], v); - }); - } - else { - insertAttrs(layoutAttributes, _module.layoutAttributes, _module.name); - } - }); + if (_module.schema && _module.schema.layout) { + Object.keys(_module.schema.layout).forEach(function(v) { + insertAttrs(layoutAttributes, _module.schema.layout[v], v); + }); + } else { + insertAttrs(layoutAttributes, _module.layoutAttributes, _module.name); + } + }); - return { - layoutAttributes: formatAttributes(layoutAttributes) - }; + return { layoutAttributes: formatAttributes(layoutAttributes) }; } function getTransformAttributes(type) { - var _module = Registry.transformsRegistry[type]; - var attributes = extendDeep({}, _module.attributes); - - // add registered components transform attributes - Object.keys(Registry.componentsRegistry).forEach(function(k) { - var _module = Registry.componentsRegistry[k]; - - if(_module.schema && _module.schema.transforms && _module.schema.transforms[type]) { - Object.keys(_module.schema.transforms[type]).forEach(function(v) { - insertAttrs(attributes, _module.schema.transforms[type][v], v); - }); - } - }); + var _module = Registry.transformsRegistry[type]; + var attributes = extendDeep({}, _module.attributes); + + // add registered components transform attributes + Object.keys(Registry.componentsRegistry).forEach(function(k) { + var _module = Registry.componentsRegistry[k]; + + if ( + _module.schema && + _module.schema.transforms && + _module.schema.transforms[type] + ) { + Object.keys(_module.schema.transforms[type]).forEach(function(v) { + insertAttrs(attributes, _module.schema.transforms[type][v], v); + }); + } + }); - return { - attributes: formatAttributes(attributes) - }; + return { attributes: formatAttributes(attributes) }; } function getFramesAttributes() { - var attrs = { - frames: Lib.extendDeep({}, frameAttributes) - }; + var attrs = { frames: Lib.extendDeep({}, frameAttributes) }; - formatAttributes(attrs); + formatAttributes(attrs); - return attrs.frames; + return attrs.frames; } function formatAttributes(attrs) { - mergeValTypeAndRole(attrs); - formatArrayContainers(attrs); + mergeValTypeAndRole(attrs); + formatArrayContainers(attrs); - return attrs; + return attrs; } function mergeValTypeAndRole(attrs) { - - function makeSrcAttr(attrName) { - return { - valType: 'string', - role: 'info', - description: [ - 'Sets the source reference on plot.ly for ', - attrName, '.' - ].join(' ') - }; - } - - function callback(attr, attrName, attrs) { - if(exports.isValObject(attr)) { - if(attr.valType === 'data_array') { - // all 'data_array' attrs have role 'data' - attr.role = 'data'; - // all 'data_array' attrs have a corresponding 'src' attr - attrs[attrName + 'src'] = makeSrcAttr(attrName); - } - else if(attr.arrayOk === true) { - // all 'arrayOk' attrs have a corresponding 'src' attr - attrs[attrName + 'src'] = makeSrcAttr(attrName); - } - } - else if(Lib.isPlainObject(attr)) { - // all attrs container objects get role 'object' - attr.role = 'object'; - } + function makeSrcAttr(attrName) { + return { + valType: "string", + role: "info", + description: [ + "Sets the source reference on plot.ly for ", + attrName, + "." + ].join(" ") + }; + } + + function callback(attr, attrName, attrs) { + if (exports.isValObject(attr)) { + if (attr.valType === "data_array") { + // all 'data_array' attrs have role 'data' + attr.role = "data"; + // all 'data_array' attrs have a corresponding 'src' attr + attrs[attrName + "src"] = makeSrcAttr(attrName); + } else if (attr.arrayOk === true) { + // all 'arrayOk' attrs have a corresponding 'src' attr + attrs[attrName + "src"] = makeSrcAttr(attrName); + } + } else if (Lib.isPlainObject(attr)) { + // all attrs container objects get role 'object' + attr.role = "object"; } + } - exports.crawl(attrs, callback); + exports.crawl(attrs, callback); } function formatArrayContainers(attrs) { + function callback(attr, attrName, attrs) { + if (!attr) return; - function callback(attr, attrName, attrs) { - if(!attr) return; + var itemName = attr[IS_LINKED_TO_ARRAY]; - var itemName = attr[IS_LINKED_TO_ARRAY]; + if (!itemName) return; - if(!itemName) return; + delete attr[IS_LINKED_TO_ARRAY]; - delete attr[IS_LINKED_TO_ARRAY]; - - attrs[attrName] = { items: {} }; - attrs[attrName].items[itemName] = attr; - attrs[attrName].role = 'object'; - } + attrs[attrName] = { items: {} }; + attrs[attrName].items[itemName] = attr; + attrs[attrName].role = "object"; + } - exports.crawl(attrs, callback); + exports.crawl(attrs, callback); } function assignPolarLayoutAttrs(layoutAttributes) { - extendFlat(layoutAttributes, { - radialaxis: polarAxisAttrs.radialaxis, - angularaxis: polarAxisAttrs.angularaxis - }); + extendFlat(layoutAttributes, { + radialaxis: polarAxisAttrs.radialaxis, + angularaxis: polarAxisAttrs.angularaxis + }); - extendFlat(layoutAttributes, polarAxisAttrs.layout); + extendFlat(layoutAttributes, polarAxisAttrs.layout); - return layoutAttributes; + return layoutAttributes; } function handleBasePlotModule(layoutAttributes, _module, astr) { - var np = Lib.nestedProperty(layoutAttributes, astr), - attrs = extendDeep({}, _module.layoutAttributes); + var np = Lib.nestedProperty(layoutAttributes, astr), + attrs = extendDeep({}, _module.layoutAttributes); - attrs[IS_SUBPLOT_OBJ] = true; - np.set(attrs); + attrs[IS_SUBPLOT_OBJ] = true; + np.set(attrs); } function insertAttrs(baseAttrs, newAttrs, astr) { - var np = Lib.nestedProperty(baseAttrs, astr); + var np = Lib.nestedProperty(baseAttrs, astr); - np.set(extendDeep(np.get() || {}, newAttrs)); + np.set(extendDeep(np.get() || {}, newAttrs)); } diff --git a/src/plot_api/register.js b/src/plot_api/register.js index 87dae2e49b2..7f578029b5a 100644 --- a/src/plot_api/register.js +++ b/src/plot_api/register.js @@ -5,93 +5,97 @@ * 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 Registry = require('../registry'); -var Lib = require('../lib'); - +"use strict"; +var Registry = require("../registry"); +var Lib = require("../lib"); module.exports = function register(_modules) { - if(!_modules) { - throw new Error('No argument passed to Plotly.register.'); - } - else if(_modules && !Array.isArray(_modules)) { - _modules = [_modules]; - } + if (!_modules) { + throw new Error("No argument passed to Plotly.register."); + } else if (_modules && !Array.isArray(_modules)) { + _modules = [_modules]; + } - for(var i = 0; i < _modules.length; i++) { - var newModule = _modules[i]; + for (var i = 0; i < _modules.length; i++) { + var newModule = _modules[i]; - if(!newModule) { - throw new Error('Invalid module was attempted to be registered!'); - } + if (!newModule) { + throw new Error("Invalid module was attempted to be registered!"); + } - switch(newModule.moduleType) { - case 'trace': - registerTraceModule(newModule); - break; + switch (newModule.moduleType) { + case "trace": + registerTraceModule(newModule); + break; - case 'transform': - registerTransformModule(newModule); - break; + case "transform": + registerTransformModule(newModule); + break; - case 'component': - registerComponentModule(newModule); - break; + case "component": + registerComponentModule(newModule); + break; - default: - throw new Error('Invalid module was attempted to be registered!'); - } + default: + throw new Error("Invalid module was attempted to be registered!"); } + } }; function registerTraceModule(newModule) { - Registry.register(newModule, newModule.name, newModule.categories, newModule.meta); - - if(!Registry.subplotsRegistry[newModule.basePlotModule.name]) { - Registry.registerSubplot(newModule.basePlotModule); - } + Registry.register( + newModule, + newModule.name, + newModule.categories, + newModule.meta + ); + + if (!Registry.subplotsRegistry[newModule.basePlotModule.name]) { + Registry.registerSubplot(newModule.basePlotModule); + } } function registerTransformModule(newModule) { - if(typeof newModule.name !== 'string') { - throw new Error('Transform module *name* must be a string.'); - } - - var prefix = 'Transform module ' + newModule.name; - - var hasTransform = typeof newModule.transform === 'function', - hasCalcTransform = typeof newModule.calcTransform === 'function'; - - - if(!hasTransform && !hasCalcTransform) { - throw new Error(prefix + ' is missing a *transform* or *calcTransform* method.'); - } - - if(hasTransform && hasCalcTransform) { - Lib.log([ - prefix + ' has both a *transform* and *calcTransform* methods.', - 'Please note that all *transform* methods are executed', - 'before all *calcTransform* methods.' - ].join(' ')); - } - - if(!Lib.isPlainObject(newModule.attributes)) { - Lib.log(prefix + ' registered without an *attributes* object.'); - } - - if(typeof newModule.supplyDefaults !== 'function') { - Lib.log(prefix + ' registered without a *supplyDefaults* method.'); - } - - Registry.transformsRegistry[newModule.name] = newModule; + if (typeof newModule.name !== "string") { + throw new Error("Transform module *name* must be a string."); + } + + var prefix = "Transform module " + newModule.name; + + var hasTransform = typeof newModule.transform === "function", + hasCalcTransform = typeof newModule.calcTransform === "function"; + + if (!hasTransform && !hasCalcTransform) { + throw new Error( + prefix + " is missing a *transform* or *calcTransform* method." + ); + } + + if (hasTransform && hasCalcTransform) { + Lib.log( + [ + prefix + " has both a *transform* and *calcTransform* methods.", + "Please note that all *transform* methods are executed", + "before all *calcTransform* methods." + ].join(" ") + ); + } + + if (!Lib.isPlainObject(newModule.attributes)) { + Lib.log(prefix + " registered without an *attributes* object."); + } + + if (typeof newModule.supplyDefaults !== "function") { + Lib.log(prefix + " registered without a *supplyDefaults* method."); + } + + Registry.transformsRegistry[newModule.name] = newModule; } function registerComponentModule(newModule) { - if(typeof newModule.name !== 'string') { - throw new Error('Component module *name* must be a string.'); - } + if (typeof newModule.name !== "string") { + throw new Error("Component module *name* must be a string."); + } - Registry.registerComponent(newModule); + Registry.registerComponent(newModule); } diff --git a/src/plot_api/set_plot_config.js b/src/plot_api/set_plot_config.js index e4f9e058ca4..316c79a776e 100644 --- a/src/plot_api/set_plot_config.js +++ b/src/plot_api/set_plot_config.js @@ -5,12 +5,9 @@ * 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 Plotly = require('../plotly'); -var Lib = require('../lib'); +"use strict"; +var Plotly = require("../plotly"); +var Lib = require("../lib"); /** * Extends the plot config @@ -20,5 +17,5 @@ var Lib = require('../lib'); * */ module.exports = function setPlotConfig(configObj) { - return Lib.extendFlat(Plotly.defaultConfig, configObj); + return Lib.extendFlat(Plotly.defaultConfig, configObj); }; diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index ceb2a4dbf64..0a97354fd96 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -5,225 +5,221 @@ * 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 Plotly = require("../plotly"); +var Registry = require("../registry"); +var Plots = require("../plots/plots"); +var Lib = require("../lib"); +var Color = require("../components/color"); +var Drawing = require("../components/drawing"); +var Titles = require("../components/titles"); +var ModeBar = require("../components/modebar"); -'use strict'; +exports.layoutStyles = function(gd) { + return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd); +}; -var Plotly = require('../plotly'); -var Registry = require('../registry'); -var Plots = require('../plots/plots'); -var Lib = require('../lib'); +exports.lsInner = function(gd) { + var fullLayout = gd._fullLayout, + gs = fullLayout._size, + axList = Plotly.Axes.list(gd), + i; + + // clear axis line positions, to be set in the subplot loop below + for (i = 0; i < axList.length; i++) { + axList[i]._linepositions = {}; + } + + fullLayout._paperdiv + .style({ width: fullLayout.width + "px", height: fullLayout.height + "px" }) + .selectAll(".main-svg") + .call(Drawing.setSize, fullLayout.width, fullLayout.height); + + gd._context.setBackground(gd, fullLayout.paper_bgcolor); + + var freefinished = []; + fullLayout._paper.selectAll("g.subplot").each(function(subplot) { + var plotinfo = fullLayout._plots[subplot], + xa = Plotly.Axes.getFromId(gd, subplot, "x"), + ya = Plotly.Axes.getFromId(gd, subplot, "y"); + + xa.setScale(); + // this may already be done... not sure + ya.setScale(); + + if (plotinfo.bg) { + plotinfo.bg + .call( + Drawing.setRect, + xa._offset - gs.p, + ya._offset - gs.p, + xa._length + 2 * gs.p, + ya._length + 2 * gs.p + ) + .call(Color.fill, fullLayout.plot_bgcolor); + } -var Color = require('../components/color'); -var Drawing = require('../components/drawing'); -var Titles = require('../components/titles'); -var ModeBar = require('../components/modebar'); + // Clip so that data only shows up on the plot area. + plotinfo.clipId = "clip" + fullLayout._uid + subplot + "plot"; + + var plotClip = fullLayout._defs + .selectAll("g.clips") + .selectAll("#" + plotinfo.clipId) + .data([0]); + + plotClip + .enter() + .append("clipPath") + .attr({ class: "plotclip", id: plotinfo.clipId }) + .append("rect"); + + plotClip.selectAll("rect").attr({ width: xa._length, height: ya._length }); + + plotinfo.plot.call(Drawing.setTranslate, xa._offset, ya._offset); + plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId); + + var xlw = Drawing.crispRound(gd, xa.linewidth, 1), + ylw = Drawing.crispRound(gd, ya.linewidth, 1), + xp = gs.p + ylw, + xpathPrefix = "M" + (-xp) + ",", + xpathSuffix = "h" + (xa._length + 2 * xp), + showfreex = xa.anchor === "free" && freefinished.indexOf(xa._id) === -1, + freeposx = gs.h * (1 - (xa.position || 0)) + xlw / 2 % 1, + showbottom = xa.anchor === ya._id && (xa.mirror || xa.side !== "top") || + xa.mirror === "all" || + xa.mirror === "allticks" || + xa.mirrors && xa.mirrors[ya._id + "bottom"], + bottompos = ya._length + gs.p + xlw / 2, + showtop = xa.anchor === ya._id && (xa.mirror || xa.side === "top") || + xa.mirror === "all" || + xa.mirror === "allticks" || + xa.mirrors && xa.mirrors[ya._id + "top"], + toppos = -gs.p - xlw / 2, + // shorten y axis lines so they don't overlap x axis lines + yp = gs.p, + // except where there's no x line + // TODO: this gets more complicated with multiple x and y axes + ypbottom = showbottom ? 0 : xlw, + yptop = showtop ? 0 : xlw, + ypathSuffix = "," + + (-yp - yptop) + + "v" + + (ya._length + 2 * yp + yptop + ypbottom), + showfreey = ya.anchor === "free" && freefinished.indexOf(ya._id) === -1, + freeposy = gs.w * (ya.position || 0) + ylw / 2 % 1, + showleft = ya.anchor === xa._id && (ya.mirror || ya.side !== "right") || + ya.mirror === "all" || + ya.mirror === "allticks" || + ya.mirrors && ya.mirrors[xa._id + "left"], + leftpos = -gs.p - ylw / 2, + showright = ya.anchor === xa._id && (ya.mirror || ya.side === "right") || + ya.mirror === "all" || + ya.mirror === "allticks" || + ya.mirrors && ya.mirrors[xa._id + "right"], + rightpos = xa._length + gs.p + ylw / 2; + + // save axis line positions for ticks, draggers, etc to reference + // each subplot gets an entry: + // [left or bottom, right or top, free, main] + // main is the position at which to draw labels and draggers, if any + xa._linepositions[subplot] = [ + showbottom ? bottompos : undefined, + showtop ? toppos : undefined, + showfreex ? freeposx : undefined + ]; + if (xa.anchor === ya._id) { + xa._linepositions[subplot][3] = xa.side === "top" ? toppos : bottompos; + } else if (showfreex) { + xa._linepositions[subplot][3] = freeposx; + } + ya._linepositions[subplot] = [ + showleft ? leftpos : undefined, + showright ? rightpos : undefined, + showfreey ? freeposy : undefined + ]; + if (ya.anchor === xa._id) { + ya._linepositions[subplot][3] = ya.side === "right" ? rightpos : leftpos; + } else if (showfreey) { + ya._linepositions[subplot][3] = freeposy; + } -exports.layoutStyles = function(gd) { - return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd); -}; + // translate all the extra stuff to have the + // same origin as the plot area or axes + var origin = "translate(" + xa._offset + "," + ya._offset + ")", + originx = origin, + originy = origin; + if (showfreex) { + originx = "translate(" + xa._offset + "," + gs.t + ")"; + toppos += ya._offset - gs.t; + bottompos += ya._offset - gs.t; + } + if (showfreey) { + originy = "translate(" + gs.l + "," + ya._offset + ")"; + leftpos += xa._offset - gs.l; + rightpos += xa._offset - gs.l; + } -exports.lsInner = function(gd) { - var fullLayout = gd._fullLayout, - gs = fullLayout._size, - axList = Plotly.Axes.list(gd), - i; - - // clear axis line positions, to be set in the subplot loop below - for(i = 0; i < axList.length; i++) axList[i]._linepositions = {}; - - fullLayout._paperdiv - .style({ - width: fullLayout.width + 'px', - height: fullLayout.height + 'px' - }) - .selectAll('.main-svg') - .call(Drawing.setSize, fullLayout.width, fullLayout.height); - - gd._context.setBackground(gd, fullLayout.paper_bgcolor); - - var freefinished = []; - fullLayout._paper.selectAll('g.subplot').each(function(subplot) { - var plotinfo = fullLayout._plots[subplot], - xa = Plotly.Axes.getFromId(gd, subplot, 'x'), - ya = Plotly.Axes.getFromId(gd, subplot, 'y'); - - xa.setScale(); // this may already be done... not sure - ya.setScale(); - - if(plotinfo.bg) { - plotinfo.bg - .call(Drawing.setRect, - xa._offset - gs.p, ya._offset - gs.p, - xa._length + 2 * gs.p, ya._length + 2 * gs.p) - .call(Color.fill, fullLayout.plot_bgcolor); - } - - // Clip so that data only shows up on the plot area. - plotinfo.clipId = 'clip' + fullLayout._uid + subplot + 'plot'; - - var plotClip = fullLayout._defs.selectAll('g.clips') - .selectAll('#' + plotinfo.clipId) - .data([0]); - - plotClip.enter().append('clipPath') - .attr({ - 'class': 'plotclip', - 'id': plotinfo.clipId - }) - .append('rect'); - - plotClip.selectAll('rect') - .attr({ - 'width': xa._length, - 'height': ya._length - }); - - - plotinfo.plot.call(Drawing.setTranslate, xa._offset, ya._offset); - plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId); - - var xlw = Drawing.crispRound(gd, xa.linewidth, 1), - ylw = Drawing.crispRound(gd, ya.linewidth, 1), - xp = gs.p + ylw, - xpathPrefix = 'M' + (-xp) + ',', - xpathSuffix = 'h' + (xa._length + 2 * xp), - showfreex = xa.anchor === 'free' && - freefinished.indexOf(xa._id) === -1, - freeposx = gs.h * (1 - (xa.position||0)) + ((xlw / 2) % 1), - showbottom = - (xa.anchor === ya._id && (xa.mirror || xa.side !== 'top')) || - xa.mirror === 'all' || xa.mirror === 'allticks' || - (xa.mirrors && xa.mirrors[ya._id + 'bottom']), - bottompos = ya._length + gs.p + xlw / 2, - showtop = - (xa.anchor === ya._id && (xa.mirror || xa.side === 'top')) || - xa.mirror === 'all' || xa.mirror === 'allticks' || - (xa.mirrors && xa.mirrors[ya._id + 'top']), - toppos = -gs.p - xlw / 2, - - // shorten y axis lines so they don't overlap x axis lines - yp = gs.p, - // except where there's no x line - // TODO: this gets more complicated with multiple x and y axes - ypbottom = showbottom ? 0 : xlw, - yptop = showtop ? 0 : xlw, - ypathSuffix = ',' + (-yp - yptop) + - 'v' + (ya._length + 2 * yp + yptop + ypbottom), - showfreey = ya.anchor === 'free' && - freefinished.indexOf(ya._id) === -1, - freeposy = gs.w * (ya.position||0) + ((ylw / 2) % 1), - showleft = - (ya.anchor === xa._id && (ya.mirror || ya.side !== 'right')) || - ya.mirror === 'all' || ya.mirror === 'allticks' || - (ya.mirrors && ya.mirrors[xa._id + 'left']), - leftpos = -gs.p - ylw / 2, - showright = - (ya.anchor === xa._id && (ya.mirror || ya.side === 'right')) || - ya.mirror === 'all' || ya.mirror === 'allticks' || - (ya.mirrors && ya.mirrors[xa._id + 'right']), - rightpos = xa._length + gs.p + ylw / 2; - - // save axis line positions for ticks, draggers, etc to reference - // each subplot gets an entry: - // [left or bottom, right or top, free, main] - // main is the position at which to draw labels and draggers, if any - xa._linepositions[subplot] = [ - showbottom ? bottompos : undefined, - showtop ? toppos : undefined, - showfreex ? freeposx : undefined - ]; - if(xa.anchor === ya._id) { - xa._linepositions[subplot][3] = xa.side === 'top' ? - toppos : bottompos; - } - else if(showfreex) { - xa._linepositions[subplot][3] = freeposx; - } - - ya._linepositions[subplot] = [ - showleft ? leftpos : undefined, - showright ? rightpos : undefined, - showfreey ? freeposy : undefined - ]; - if(ya.anchor === xa._id) { - ya._linepositions[subplot][3] = ya.side === 'right' ? - rightpos : leftpos; - } - else if(showfreey) { - ya._linepositions[subplot][3] = freeposy; - } - - // translate all the extra stuff to have the - // same origin as the plot area or axes - var origin = 'translate(' + xa._offset + ',' + ya._offset + ')', - originx = origin, - originy = origin; - if(showfreex) { - originx = 'translate(' + xa._offset + ',' + gs.t + ')'; - toppos += ya._offset - gs.t; - bottompos += ya._offset - gs.t; - } - if(showfreey) { - originy = 'translate(' + gs.l + ',' + ya._offset + ')'; - leftpos += xa._offset - gs.l; - rightpos += xa._offset - gs.l; - } - - plotinfo.xlines - .attr('transform', originx) - .attr('d', ( - (showbottom ? (xpathPrefix + bottompos + xpathSuffix) : '') + - (showtop ? (xpathPrefix + toppos + xpathSuffix) : '') + - (showfreex ? (xpathPrefix + freeposx + xpathSuffix) : '')) || - // so it doesn't barf with no lines shown - 'M0,0') - .style('stroke-width', xlw + 'px') - .call(Color.stroke, xa.showline ? - xa.linecolor : 'rgba(0,0,0,0)'); - plotinfo.ylines - .attr('transform', originy) - .attr('d', ( - (showleft ? ('M' + leftpos + ypathSuffix) : '') + - (showright ? ('M' + rightpos + ypathSuffix) : '') + - (showfreey ? ('M' + freeposy + ypathSuffix) : '')) || - 'M0,0') - .attr('stroke-width', ylw + 'px') - .call(Color.stroke, ya.showline ? - ya.linecolor : 'rgba(0,0,0,0)'); - - plotinfo.xaxislayer.attr('transform', originx); - plotinfo.yaxislayer.attr('transform', originy); - plotinfo.gridlayer.attr('transform', origin); - plotinfo.zerolinelayer.attr('transform', origin); - plotinfo.draglayer.attr('transform', origin); - - // mark free axes as displayed, so we don't draw them again - if(showfreex) { freefinished.push(xa._id); } - if(showfreey) { freefinished.push(ya._id); } - }); - - Plotly.Axes.makeClipPaths(gd); - exports.drawMainTitle(gd); - ModeBar.manage(gd); - - return gd._promises.length && Promise.all(gd._promises); + plotinfo.xlines + .attr("transform", originx) + .attr( + "d", + (showbottom ? xpathPrefix + bottompos + xpathSuffix : "") + + (showtop ? xpathPrefix + toppos + xpathSuffix : "") + + (showfreex ? xpathPrefix + freeposx + xpathSuffix : "") || + // so it doesn't barf with no lines shown + "M0,0" + ) + .style("stroke-width", xlw + "px") + .call(Color.stroke, xa.showline ? xa.linecolor : "rgba(0,0,0,0)"); + plotinfo.ylines + .attr("transform", originy) + .attr( + "d", + (showleft ? "M" + leftpos + ypathSuffix : "") + + (showright ? "M" + rightpos + ypathSuffix : "") + + (showfreey ? "M" + freeposy + ypathSuffix : "") || + "M0,0" + ) + .attr("stroke-width", ylw + "px") + .call(Color.stroke, ya.showline ? ya.linecolor : "rgba(0,0,0,0)"); + + plotinfo.xaxislayer.attr("transform", originx); + plotinfo.yaxislayer.attr("transform", originy); + plotinfo.gridlayer.attr("transform", origin); + plotinfo.zerolinelayer.attr("transform", origin); + plotinfo.draglayer.attr("transform", origin); + + // mark free axes as displayed, so we don't draw them again + if (showfreex) { + freefinished.push(xa._id); + } + if (showfreey) { + freefinished.push(ya._id); + } + }); + + Plotly.Axes.makeClipPaths(gd); + exports.drawMainTitle(gd); + ModeBar.manage(gd); + + return gd._promises.length && Promise.all(gd._promises); }; exports.drawMainTitle = function(gd) { - var fullLayout = gd._fullLayout; - - Titles.draw(gd, 'gtitle', { - propContainer: fullLayout, - propName: 'title', - dfltName: 'Plot', - attributes: { - x: fullLayout.width / 2, - y: fullLayout._size.t / 2, - 'text-anchor': 'middle' - } - }); + var fullLayout = gd._fullLayout; + + Titles.draw(gd, "gtitle", { + propContainer: fullLayout, + propName: "title", + dfltName: "Plot", + attributes: { + x: fullLayout.width / 2, + y: fullLayout._size.t / 2, + "text-anchor": "middle" + } + }); }; // First, see if we need to do arraysToCalcdata @@ -231,98 +227,98 @@ exports.drawMainTitle = function(gd) { // supplyDefaults brought in an array that was already // in gd.data but not in gd._fullData previously exports.doTraceStyle = function(gd) { - for(var i = 0; i < gd.calcdata.length; i++) { - var cdi = gd.calcdata[i], - _module = ((cdi[0] || {}).trace || {})._module || {}, - arraysToCalcdata = _module.arraysToCalcdata; + for (var i = 0; i < gd.calcdata.length; i++) { + var cdi = gd.calcdata[i], + _module = ((cdi[0] || {}).trace || {})._module || {}, + arraysToCalcdata = _module.arraysToCalcdata; - if(arraysToCalcdata) arraysToCalcdata(cdi, cdi[0].trace); - } + if (arraysToCalcdata) arraysToCalcdata(cdi, cdi[0].trace); + } - Plots.style(gd); - Registry.getComponentMethod('legend', 'draw')(gd); + Plots.style(gd); + Registry.getComponentMethod("legend", "draw")(gd); - return Plots.previousPromises(gd); + return Plots.previousPromises(gd); }; exports.doColorBars = function(gd) { - for(var i = 0; i < gd.calcdata.length; i++) { - var cdi0 = gd.calcdata[i][0]; - - if((cdi0.t || {}).cb) { - var trace = cdi0.trace, - cb = cdi0.t.cb; - - if(Registry.traceIs(trace, 'contour')) { - cb.line({ - width: trace.contours.showlines !== false ? - trace.line.width : 0, - dash: trace.line.dash, - color: trace.contours.coloring === 'line' ? - cb._opts.line.color : trace.line.color - }); - } - if(Registry.traceIs(trace, 'markerColorscale')) { - cb.options(trace.marker.colorbar)(); - } - else cb.options(trace.colorbar)(); - } + for (var i = 0; i < gd.calcdata.length; i++) { + var cdi0 = gd.calcdata[i][0]; + + if ((cdi0.t || {}).cb) { + var trace = cdi0.trace, cb = cdi0.t.cb; + + if (Registry.traceIs(trace, "contour")) { + cb.line({ + width: trace.contours.showlines !== false ? trace.line.width : 0, + dash: trace.line.dash, + color: ( + trace.contours.coloring === "line" + ? cb._opts.line.color + : trace.line.color + ) + }); + } + if (Registry.traceIs(trace, "markerColorscale")) { + cb.options(trace.marker.colorbar)(); + } else { + cb.options(trace.colorbar)(); + } } + } - return Plots.previousPromises(gd); + return Plots.previousPromises(gd); }; // force plot() to redo the layout and replot with the modified layout exports.layoutReplot = function(gd) { - var layout = gd.layout; - gd.layout = undefined; - return Plotly.plot(gd, '', layout); + var layout = gd.layout; + gd.layout = undefined; + return Plotly.plot(gd, "", layout); }; exports.doLegend = function(gd) { - Registry.getComponentMethod('legend', 'draw')(gd); - return Plots.previousPromises(gd); + Registry.getComponentMethod("legend", "draw")(gd); + return Plots.previousPromises(gd); }; exports.doTicksRelayout = function(gd) { - Plotly.Axes.doTicks(gd, 'redraw'); - exports.drawMainTitle(gd); - return Plots.previousPromises(gd); + Plotly.Axes.doTicks(gd, "redraw"); + exports.drawMainTitle(gd); + return Plots.previousPromises(gd); }; exports.doModeBar = function(gd) { - var fullLayout = gd._fullLayout; - var subplotIds, i; - - ModeBar.manage(gd); - Plotly.Fx.init(gd); - - subplotIds = Plots.getSubplotIds(fullLayout, 'gl3d'); - for(i = 0; i < subplotIds.length; i++) { - var scene = fullLayout[subplotIds[i]]._scene; - scene.updateFx(fullLayout.dragmode, fullLayout.hovermode); - } - - // no need to do this for gl2d subplots, - // Plots.linkSubplots takes care of it all. - - subplotIds = Plots.getSubplotIds(fullLayout, 'geo'); - for(i = 0; i < subplotIds.length; i++) { - var geo = fullLayout[subplotIds[i]]._subplot; - geo.updateFx(fullLayout.hovermode); - } - - return Plots.previousPromises(gd); + var fullLayout = gd._fullLayout; + var subplotIds, i; + + ModeBar.manage(gd); + Plotly.Fx.init(gd); + + subplotIds = Plots.getSubplotIds(fullLayout, "gl3d"); + for (i = 0; i < subplotIds.length; i++) { + var scene = fullLayout[subplotIds[i]]._scene; + scene.updateFx(fullLayout.dragmode, fullLayout.hovermode); + } + + // no need to do this for gl2d subplots, + // Plots.linkSubplots takes care of it all. + subplotIds = Plots.getSubplotIds(fullLayout, "geo"); + for (i = 0; i < subplotIds.length; i++) { + var geo = fullLayout[subplotIds[i]]._subplot; + geo.updateFx(fullLayout.hovermode); + } + + return Plots.previousPromises(gd); }; exports.doCamera = function(gd) { - var fullLayout = gd._fullLayout, - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'); + var fullLayout = gd._fullLayout, + sceneIds = Plots.getSubplotIds(fullLayout, "gl3d"); - for(var i = 0; i < sceneIds.length; i++) { - var sceneLayout = fullLayout[sceneIds[i]], - scene = sceneLayout._scene; + for (var i = 0; i < sceneIds.length; i++) { + var sceneLayout = fullLayout[sceneIds[i]], scene = sceneLayout._scene; - scene.setCamera(sceneLayout.camera); - } + scene.setCamera(sceneLayout.camera); + } }; diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index 6ebcf75b367..a9b3ce975f4 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -5,18 +5,16 @@ * 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 isNumeric = require("fast-isnumeric"); -'use strict'; +var Plotly = require("../plotly"); +var Lib = require("../lib"); -var isNumeric = require('fast-isnumeric'); - -var Plotly = require('../plotly'); -var Lib = require('../lib'); - -var helpers = require('../snapshot/helpers'); -var clonePlot = require('../snapshot/cloneplot'); -var toSVG = require('../snapshot/tosvg'); -var svgToImg = require('../snapshot/svgtoimg'); +var helpers = require("../snapshot/helpers"); +var clonePlot = require("../snapshot/cloneplot"); +var toSVG = require("../snapshot/tosvg"); +var svgToImg = require("../snapshot/svgtoimg"); /** * @param {object} gd figure Object @@ -26,83 +24,92 @@ var svgToImg = require('../snapshot/svgtoimg'); * @param opts.height height of snapshot in px */ function toImage(gd, opts) { - - var promise = new Promise(function(resolve, reject) { - // check for undefined opts - opts = opts || {}; - // default to png - opts.format = opts.format || 'png'; - - var isSizeGood = function(size) { - // undefined and null are valid options - if(size === undefined || size === null) { - return true; - } - - if(isNumeric(size) && size > 1) { - return true; - } - - return false; - }; - - if(!isSizeGood(opts.width) || !isSizeGood(opts.height)) { - reject(new Error('Height and width should be pixel values.')); - } - - // first clone the GD so we can operate in a clean environment - var clone = clonePlot(gd, {format: 'png', height: opts.height, width: opts.width}); - var clonedGd = clone.gd; - - // put the cloned div somewhere off screen before attaching to DOM - clonedGd.style.position = 'absolute'; - clonedGd.style.left = '-5000px'; - document.body.appendChild(clonedGd); - - function wait() { - var delay = helpers.getDelay(clonedGd._fullLayout); - - return new Promise(function(resolve, reject) { - setTimeout(function() { - var svg = toSVG(clonedGd); - - var canvas = document.createElement('canvas'); - canvas.id = Lib.randstr(); - - svgToImg({ - format: opts.format, - width: clonedGd._fullLayout.width, - height: clonedGd._fullLayout.height, - canvas: canvas, - svg: svg, - // ask svgToImg to return a Promise - // rather than EventEmitter - // leave EventEmitter for backward - // compatibility - promise: true - }).then(function(url) { - if(clonedGd) document.body.removeChild(clonedGd); - resolve(url); - }).catch(function(err) { - reject(err); - }); - - }, delay); - }); - } - - var redrawFunc = helpers.getRedrawFunc(clonedGd); - - Plotly.plot(clonedGd, clone.data, clone.layout, clone.config) - .then(redrawFunc) - .then(wait) - .then(function(url) { resolve(url); }) - .catch(function(err) { - reject(err); - }); + var promise = new Promise(function(resolve, reject) { + // check for undefined opts + opts = opts || {}; + // default to png + opts.format = opts.format || "png"; + + var isSizeGood = function(size) { + // undefined and null are valid options + if (size === undefined || size === null) { + return true; + } + + if (isNumeric(size) && size > 1) { + return true; + } + + return false; + }; + + if (!isSizeGood(opts.width) || !isSizeGood(opts.height)) { + reject(new Error("Height and width should be pixel values.")); + } + + // first clone the GD so we can operate in a clean environment + var clone = clonePlot(gd, { + format: "png", + height: opts.height, + width: opts.width }); - - return promise; + var clonedGd = clone.gd; + + // put the cloned div somewhere off screen before attaching to DOM + clonedGd.style.position = "absolute"; + clonedGd.style.left = "-5000px"; + document.body.appendChild(clonedGd); + + function wait() { + var delay = helpers.getDelay(clonedGd._fullLayout); + + return new Promise(function(resolve, reject) { + setTimeout( + function() { + var svg = toSVG(clonedGd); + + var canvas = document.createElement("canvas"); + canvas.id = Lib.randstr(); + + svgToImg({ + format: opts.format, + width: clonedGd._fullLayout.width, + height: clonedGd._fullLayout.height, + canvas: canvas, + svg: svg, + // ask svgToImg to return a Promise + // rather than EventEmitter + // leave EventEmitter for backward + // compatibility + promise: true + }) + .then(function(url) { + if (clonedGd) document.body.removeChild(clonedGd); + resolve(url); + }) + .catch(function(err) { + reject(err); + }); + }, + delay + ); + }); + } + + var redrawFunc = helpers.getRedrawFunc(clonedGd); + + Plotly.plot(clonedGd, clone.data, clone.layout, clone.config) + .then(redrawFunc) + .then(wait) + .then(function(url) { + resolve(url); + }) + .catch(function(err) { + reject(err); + }); + }); + + return promise; } module.exports = toImage; diff --git a/src/plot_api/validate.js b/src/plot_api/validate.js index b06e5506f4d..e4759202133 100644 --- a/src/plot_api/validate.js +++ b/src/plot_api/validate.js @@ -5,19 +5,14 @@ * 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 Plots = require('../plots/plots'); -var PlotSchema = require('./plot_schema'); +"use strict"; +var Lib = require("../lib"); +var Plots = require("../plots/plots"); +var PlotSchema = require("./plot_schema"); var isPlainObject = Lib.isPlainObject; var isArray = Array.isArray; - /** * Validate a data array and layout object. * @@ -40,329 +35,310 @@ var isArray = Array.isArray; * error message (shown in console in logger config argument is enable) */ module.exports = function valiate(data, layout) { - var schema = PlotSchema.get(), - errorList = [], - gd = {}; + var schema = PlotSchema.get(), errorList = [], gd = {}; + + var dataIn, layoutIn; + + if (isArray(data)) { + gd.data = Lib.extendDeep([], data); + dataIn = data; + } else { + gd.data = []; + dataIn = []; + errorList.push(format("array", "data")); + } + + if (isPlainObject(layout)) { + gd.layout = Lib.extendDeep({}, layout); + layoutIn = layout; + } else { + gd.layout = {}; + layoutIn = {}; + if (arguments.length > 1) { + errorList.push(format("object", "layout")); + } + } - var dataIn, layoutIn; + // N.B. dataIn and layoutIn are in general not the same as + // gd.data and gd.layout after supplyDefaults as some attributes + // in gd.data and gd.layout (still) get mutated during this step. + Plots.supplyDefaults(gd); - if(isArray(data)) { - gd.data = Lib.extendDeep([], data); - dataIn = data; - } - else { - gd.data = []; - dataIn = []; - errorList.push(format('array', 'data')); - } + var dataOut = gd._fullData, len = dataIn.length; - if(isPlainObject(layout)) { - gd.layout = Lib.extendDeep({}, layout); - layoutIn = layout; - } - else { - gd.layout = {}; - layoutIn = {}; - if(arguments.length > 1) { - errorList.push(format('object', 'layout')); - } + for (var i = 0; i < len; i++) { + var traceIn = dataIn[i], base = ["data", i]; + + if (!isPlainObject(traceIn)) { + errorList.push(format("object", base)); + continue; } - // N.B. dataIn and layoutIn are in general not the same as - // gd.data and gd.layout after supplyDefaults as some attributes - // in gd.data and gd.layout (still) get mutated during this step. + var traceOut = dataOut[i], + traceType = traceOut.type, + traceSchema = schema.traces[traceType].attributes; + + // PlotSchema does something fancy with trace 'type', reset it here + // to make the trace schema compatible with Lib.validate. + traceSchema.type = { valType: "enumerated", values: [traceType] }; - Plots.supplyDefaults(gd); + if (traceOut.visible === false && traceIn.visible !== false) { + errorList.push(format("invisible", base)); + } - var dataOut = gd._fullData, - len = dataIn.length; + crawl(traceIn, traceOut, traceSchema, errorList, base); - for(var i = 0; i < len; i++) { - var traceIn = dataIn[i], - base = ['data', i]; + var transformsIn = traceIn.transforms, transformsOut = traceOut.transforms; - if(!isPlainObject(traceIn)) { - errorList.push(format('object', base)); - continue; - } + if (transformsIn) { + if (!isArray(transformsIn)) { + errorList.push(format("array", base, ["transforms"])); + } - var traceOut = dataOut[i], - traceType = traceOut.type, - traceSchema = schema.traces[traceType].attributes; + base.push("transforms"); - // PlotSchema does something fancy with trace 'type', reset it here - // to make the trace schema compatible with Lib.validate. - traceSchema.type = { - valType: 'enumerated', - values: [traceType] - }; + for (var j = 0; j < transformsIn.length; j++) { + var path = ["transforms", j], transformType = transformsIn[j].type; - if(traceOut.visible === false && traceIn.visible !== false) { - errorList.push(format('invisible', base)); + if (!isPlainObject(transformsIn[j])) { + errorList.push(format("object", base, path)); + continue; } - crawl(traceIn, traceOut, traceSchema, errorList, base); + var transformSchema = schema.transforms[transformType] + ? schema.transforms[transformType].attributes + : {}; - var transformsIn = traceIn.transforms, - transformsOut = traceOut.transforms; + // add 'type' to transform schema to validate the transform type + transformSchema.type = { + valType: "enumerated", + values: Object.keys(schema.transforms) + }; - if(transformsIn) { - if(!isArray(transformsIn)) { - errorList.push(format('array', base, ['transforms'])); - } + crawl( + transformsIn[j], + transformsOut[j], + transformSchema, + errorList, + base, + path + ); + } + } + } - base.push('transforms'); + var layoutOut = gd._fullLayout, + layoutSchema = fillLayoutSchema(schema, dataOut); - for(var j = 0; j < transformsIn.length; j++) { - var path = ['transforms', j], - transformType = transformsIn[j].type; + crawl(layoutIn, layoutOut, layoutSchema, errorList, "layout"); - if(!isPlainObject(transformsIn[j])) { - errorList.push(format('object', base, path)); - continue; - } + // return undefined if no validation errors were found + return errorList.length === 0 ? void 0 : errorList; +}; - var transformSchema = schema.transforms[transformType] ? - schema.transforms[transformType].attributes : - {}; +function crawl(objIn, objOut, schema, list, base, path) { + path = path || []; - // add 'type' to transform schema to validate the transform type - transformSchema.type = { - valType: 'enumerated', - values: Object.keys(schema.transforms) - }; + var keys = Object.keys(objIn); - crawl(transformsIn[j], transformsOut[j], transformSchema, errorList, base, path); - } - } - } + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; - var layoutOut = gd._fullLayout, - layoutSchema = fillLayoutSchema(schema, dataOut); + // transforms are handled separately + if (k === "transforms") continue; - crawl(layoutIn, layoutOut, layoutSchema, errorList, 'layout'); + var p = path.slice(); + p.push(k); - // return undefined if no validation errors were found - return (errorList.length === 0) ? void(0) : errorList; -}; + var valIn = objIn[k], valOut = objOut[k]; -function crawl(objIn, objOut, schema, list, base, path) { - path = path || []; + var nestedSchema = getNestedSchema(schema, k), + isInfoArray = (nestedSchema || {}).valType === "info_array"; - var keys = Object.keys(objIn); + if (!isInSchema(schema, k)) { + list.push(format("schema", base, p)); + } else if (isPlainObject(valIn) && isPlainObject(valOut)) { + crawl(valIn, valOut, nestedSchema, list, base, p); + } else if (nestedSchema.items && !isInfoArray && isArray(valIn)) { + var items = nestedSchema.items, + _nestedSchema = items[Object.keys(items)[0]], + indexList = []; - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; + var j, _p; - // transforms are handled separately - if(k === 'transforms') continue; + // loop over valOut items while keeping track of their + // corresponding input container index (given by _index) + for (j = 0; j < valOut.length; j++) { + var _index = valOut[j]._index || j; - var p = path.slice(); - p.push(k); + _p = p.slice(); + _p.push(_index); - var valIn = objIn[k], - valOut = objOut[k]; + if (isPlainObject(valIn[_index]) && isPlainObject(valOut[j])) { + indexList.push(_index); + crawl(valIn[_index], valOut[j], _nestedSchema, list, base, _p); + } + } - var nestedSchema = getNestedSchema(schema, k), - isInfoArray = (nestedSchema || {}).valType === 'info_array'; + // loop over valIn to determine where it went wrong for some items + for (j = 0; j < valIn.length; j++) { + _p = p.slice(); + _p.push(j); - if(!isInSchema(schema, k)) { - list.push(format('schema', base, p)); - } - else if(isPlainObject(valIn) && isPlainObject(valOut)) { - crawl(valIn, valOut, nestedSchema, list, base, p); - } - else if(nestedSchema.items && !isInfoArray && isArray(valIn)) { - var items = nestedSchema.items, - _nestedSchema = items[Object.keys(items)[0]], - indexList = []; - - var j, _p; - - // loop over valOut items while keeping track of their - // corresponding input container index (given by _index) - for(j = 0; j < valOut.length; j++) { - var _index = valOut[j]._index || j; - - _p = p.slice(); - _p.push(_index); - - if(isPlainObject(valIn[_index]) && isPlainObject(valOut[j])) { - indexList.push(_index); - crawl(valIn[_index], valOut[j], _nestedSchema, list, base, _p); - } - } - - // loop over valIn to determine where it went wrong for some items - for(j = 0; j < valIn.length; j++) { - _p = p.slice(); - _p.push(j); - - if(!isPlainObject(valIn[j])) { - list.push(format('object', base, _p, valIn[j])); - } - else if(indexList.indexOf(j) === -1) { - list.push(format('unused', base, _p)); - } - } - } - else if(!isPlainObject(valIn) && isPlainObject(valOut)) { - list.push(format('object', base, p, valIn)); - } - else if(!isArray(valIn) && isArray(valOut) && !isInfoArray) { - list.push(format('array', base, p, valIn)); - } - else if(!(k in objOut)) { - list.push(format('unused', base, p, valIn)); - } - else if(!Lib.validate(valIn, nestedSchema)) { - list.push(format('value', base, p, valIn)); + if (!isPlainObject(valIn[j])) { + list.push(format("object", base, _p, valIn[j])); + } else if (indexList.indexOf(j) === -1) { + list.push(format("unused", base, _p)); } + } + } else if (!isPlainObject(valIn) && isPlainObject(valOut)) { + list.push(format("object", base, p, valIn)); + } else if (!isArray(valIn) && isArray(valOut) && !isInfoArray) { + list.push(format("array", base, p, valIn)); + } else if (!(k in objOut)) { + list.push(format("unused", base, p, valIn)); + } else if (!Lib.validate(valIn, nestedSchema)) { + list.push(format("value", base, p, valIn)); } + } - return list; + return list; } // the 'full' layout schema depends on the traces types presents function fillLayoutSchema(schema, dataOut) { - for(var i = 0; i < dataOut.length; i++) { - var traceType = dataOut[i].type, - traceLayoutAttr = schema.traces[traceType].layoutAttributes; + for (var i = 0; i < dataOut.length; i++) { + var traceType = dataOut[i].type, + traceLayoutAttr = schema.traces[traceType].layoutAttributes; - if(traceLayoutAttr) { - Lib.extendFlat(schema.layout.layoutAttributes, traceLayoutAttr); - } + if (traceLayoutAttr) { + Lib.extendFlat(schema.layout.layoutAttributes, traceLayoutAttr); } + } - return schema.layout.layoutAttributes; + return schema.layout.layoutAttributes; } // validation error codes var code2msgFunc = { - object: function(base, astr) { - var prefix; - - if(base === 'layout' && astr === '') prefix = 'The layout argument'; - else if(base[0] === 'data' && astr === '') { - prefix = 'Trace ' + base[1] + ' in the data argument'; - } - else prefix = inBase(base) + 'key ' + astr; - - return prefix + ' must be linked to an object container'; - }, - array: function(base, astr) { - var prefix; - - if(base === 'data') prefix = 'The data argument'; - else prefix = inBase(base) + 'key ' + astr; - - return prefix + ' must be linked to an array container'; - }, - schema: function(base, astr) { - return inBase(base) + 'key ' + astr + ' is not part of the schema'; - }, - unused: function(base, astr, valIn) { - var target = isPlainObject(valIn) ? 'container' : 'key'; - - return inBase(base) + target + ' ' + astr + ' did not get coerced'; - }, - invisible: function(base) { - return 'Trace ' + base[1] + ' got defaulted to be not visible'; - }, - value: function(base, astr, valIn) { - return [ - inBase(base) + 'key ' + astr, - 'is set to an invalid value (' + valIn + ')' - ].join(' '); + object: function(base, astr) { + var prefix; + + if (base === "layout" && astr === "") { + prefix = "The layout argument"; + } else if (base[0] === "data" && astr === "") { + prefix = "Trace " + base[1] + " in the data argument"; + } else { + prefix = inBase(base) + "key " + astr; } + + return prefix + " must be linked to an object container"; + }, + array: function(base, astr) { + var prefix; + + if (base === "data") prefix = "The data argument"; + else prefix = inBase(base) + "key " + astr; + + return prefix + " must be linked to an array container"; + }, + schema: function(base, astr) { + return inBase(base) + "key " + astr + " is not part of the schema"; + }, + unused: function(base, astr, valIn) { + var target = isPlainObject(valIn) ? "container" : "key"; + + return inBase(base) + target + " " + astr + " did not get coerced"; + }, + invisible: function(base) { + return "Trace " + base[1] + " got defaulted to be not visible"; + }, + value: function(base, astr, valIn) { + return [ + inBase(base) + "key " + astr, + "is set to an invalid value (" + valIn + ")" + ].join(" "); + } }; function inBase(base) { - if(isArray(base)) return 'In data trace ' + base[1] + ', '; + if (isArray(base)) return "In data trace " + base[1] + ", "; - return 'In ' + base + ', '; + return "In " + base + ", "; } function format(code, base, path, valIn) { - path = path || ''; - - var container, trace; - - // container is either 'data' or 'layout - // trace is the trace index if 'data', null otherwise - - if(isArray(base)) { - container = base[0]; - trace = base[1]; - } - else { - container = base; - trace = null; - } - - var astr = convertPathToAttributeString(path), - msg = code2msgFunc[code](base, astr, valIn); - - // log to console if logger config option is enabled - Lib.log(msg); - - return { - code: code, - container: container, - trace: trace, - path: path, - astr: astr, - msg: msg - }; + path = path || ""; + + var container, trace; + + // container is either 'data' or 'layout + // trace is the trace index if 'data', null otherwise + if (isArray(base)) { + container = base[0]; + trace = base[1]; + } else { + container = base; + trace = null; + } + + var astr = convertPathToAttributeString(path), + msg = code2msgFunc[code](base, astr, valIn); + + // log to console if logger config option is enabled + Lib.log(msg); + + return { + code: code, + container: container, + trace: trace, + path: path, + astr: astr, + msg: msg + }; } function isInSchema(schema, key) { - var parts = splitKey(key), - keyMinusId = parts.keyMinusId, - id = parts.id; + var parts = splitKey(key), keyMinusId = parts.keyMinusId, id = parts.id; - if((keyMinusId in schema) && schema[keyMinusId]._isSubplotObj && id) { - return true; - } + if (keyMinusId in schema && schema[keyMinusId]._isSubplotObj && id) { + return true; + } - return (key in schema); + return key in schema; } function getNestedSchema(schema, key) { - var parts = splitKey(key); + var parts = splitKey(key); - return schema[parts.keyMinusId]; + return schema[parts.keyMinusId]; } function splitKey(key) { - var idRegex = /([2-9]|[1-9][0-9]+)$/; + var idRegex = /([2-9]|[1-9][0-9]+)$/; - var keyMinusId = key.split(idRegex)[0], - id = key.substr(keyMinusId.length, key.length); + var keyMinusId = key.split(idRegex)[0], + id = key.substr(keyMinusId.length, key.length); - return { - keyMinusId: keyMinusId, - id: id - }; + return { keyMinusId: keyMinusId, id: id }; } function convertPathToAttributeString(path) { - if(!isArray(path)) return String(path); + if (!isArray(path)) return String(path); - var astr = ''; + var astr = ""; - for(var i = 0; i < path.length; i++) { - var p = path[i]; + for (var i = 0; i < path.length; i++) { + var p = path[i]; - if(typeof p === 'number') { - astr = astr.substr(0, astr.length - 1) + '[' + p + ']'; - } - else { - astr += p; - } - - if(i < path.length - 1) astr += '.'; + if (typeof p === "number") { + astr = astr.substr(0, astr.length - 1) + "[" + p + "]"; + } else { + astr += p; } - return astr; + if (i < path.length - 1) astr += "."; + } + + return astr; } diff --git a/src/plotly.js b/src/plotly.js index 042166b05bc..adeba58a286 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -5,9 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; /* * Pack internal modules unto an object. * @@ -19,13 +17,13 @@ */ // configuration -exports.defaultConfig = require('./plot_api/plot_config'); +exports.defaultConfig = require("./plot_api/plot_config"); // plots -exports.Plots = require('./plots/plots'); -exports.Axes = require('./plots/cartesian/axes'); -exports.Fx = require('./plots/cartesian/graph_interact'); -exports.ModeBar = require('./components/modebar'); +exports.Plots = require("./plots/plots"); +exports.Axes = require("./plots/cartesian/axes"); +exports.Fx = require("./plots/cartesian/graph_interact"); +exports.ModeBar = require("./components/modebar"); // plot api -require('./plot_api/plot_api'); +require("./plot_api/plot_api"); diff --git a/src/plots/animation_attributes.js b/src/plots/animation_attributes.js index 8b7316270cc..48ccd51d340 100644 --- a/src/plots/animation_attributes.js +++ b/src/plots/animation_attributes.js @@ -5,118 +5,116 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = { - mode: { - valType: 'enumerated', - dflt: 'afterall', - role: 'info', - values: ['immediate', 'next', 'afterall'], - description: [ - 'Describes how a new animate call interacts with currently-running', - 'animations. If `immediate`, current animations are interrupted and', - 'the new animation is started. If `next`, the current frame is allowed', - 'to complete, after which the new animation is started. If `afterall`', - 'all existing frames are animated to completion before the new animation', - 'is started.' - ].join(' ') + mode: { + valType: "enumerated", + dflt: "afterall", + role: "info", + values: ["immediate", "next", "afterall"], + description: [ + "Describes how a new animate call interacts with currently-running", + "animations. If `immediate`, current animations are interrupted and", + "the new animation is started. If `next`, the current frame is allowed", + "to complete, after which the new animation is started. If `afterall`", + "all existing frames are animated to completion before the new animation", + "is started." + ].join(" ") + }, + direction: { + valType: "enumerated", + role: "info", + values: ["forward", "reverse"], + dflt: "forward", + description: [ + "The direction in which to play the frames triggered by the animation call" + ].join(" ") + }, + fromcurrent: { + valType: "boolean", + dflt: false, + role: "info", + description: [ + "Play frames starting at the current frame instead of the beginning." + ].join(" ") + }, + frame: { + duration: { + valType: "number", + role: "info", + min: 0, + dflt: 500, + description: [ + "The duration in milliseconds of each frame. If greater than the frame", + "duration, it will be limited to the frame duration." + ].join(" ") }, - direction: { - valType: 'enumerated', - role: 'info', - values: ['forward', 'reverse'], - dflt: 'forward', - description: [ - 'The direction in which to play the frames triggered by the animation call' - ].join(' ') - }, - fromcurrent: { - valType: 'boolean', - dflt: false, - role: 'info', - description: [ - 'Play frames starting at the current frame instead of the beginning.' - ].join(' ') - }, - frame: { - duration: { - valType: 'number', - role: 'info', - min: 0, - dflt: 500, - description: [ - 'The duration in milliseconds of each frame. If greater than the frame', - 'duration, it will be limited to the frame duration.' - ].join(' ') - }, - redraw: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Redraw the plot at completion of the transition. This is desirable', - 'for transitions that include properties that cannot be transitioned,', - 'but may significantly slow down updates that do not require a full', - 'redraw of the plot' - ].join(' ') - }, + redraw: { + valType: "boolean", + role: "info", + dflt: true, + description: [ + "Redraw the plot at completion of the transition. This is desirable", + "for transitions that include properties that cannot be transitioned,", + "but may significantly slow down updates that do not require a full", + "redraw of the plot" + ].join(" ") + } + }, + transition: { + duration: { + valType: "number", + role: "info", + min: 0, + dflt: 500, + description: [ + "The duration of the transition, in milliseconds. If equal to zero,", + "updates are synchronous." + ].join(" ") }, - transition: { - duration: { - valType: 'number', - role: 'info', - min: 0, - dflt: 500, - description: [ - 'The duration of the transition, in milliseconds. If equal to zero,', - 'updates are synchronous.' - ].join(' ') - }, - easing: { - valType: 'enumerated', - dflt: 'cubic-in-out', - values: [ - 'linear', - 'quad', - 'cubic', - 'sin', - 'exp', - 'circle', - 'elastic', - 'back', - 'bounce', - 'linear-in', - 'quad-in', - 'cubic-in', - 'sin-in', - 'exp-in', - 'circle-in', - 'elastic-in', - 'back-in', - 'bounce-in', - 'linear-out', - 'quad-out', - 'cubic-out', - 'sin-out', - 'exp-out', - 'circle-out', - 'elastic-out', - 'back-out', - 'bounce-out', - 'linear-in-out', - 'quad-in-out', - 'cubic-in-out', - 'sin-in-out', - 'exp-in-out', - 'circle-in-out', - 'elastic-in-out', - 'back-in-out', - 'bounce-in-out' - ], - role: 'info', - description: 'The easing function used for the transition' - }, + easing: { + valType: "enumerated", + dflt: "cubic-in-out", + values: [ + "linear", + "quad", + "cubic", + "sin", + "exp", + "circle", + "elastic", + "back", + "bounce", + "linear-in", + "quad-in", + "cubic-in", + "sin-in", + "exp-in", + "circle-in", + "elastic-in", + "back-in", + "bounce-in", + "linear-out", + "quad-out", + "cubic-out", + "sin-out", + "exp-out", + "circle-out", + "elastic-out", + "back-out", + "bounce-out", + "linear-in-out", + "quad-in-out", + "cubic-in-out", + "sin-in-out", + "exp-in-out", + "circle-in-out", + "elastic-in-out", + "back-in-out", + "bounce-in-out" + ], + role: "info", + description: "The easing function used for the transition" } + } }; diff --git a/src/plots/array_container_defaults.js b/src/plots/array_container_defaults.js index 2754ed613c2..539f7391fde 100644 --- a/src/plots/array_container_defaults.js +++ b/src/plots/array_container_defaults.js @@ -5,11 +5,8 @@ * 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'); - +"use strict"; +var Lib = require("../lib"); /** Convenience wrapper for making array container logic DRY and consistent * @@ -41,27 +38,29 @@ var Lib = require('../lib'); * links to supplementary data (e.g. fullData for layout components) * */ -module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut, opts) { - var name = opts.name; +module.exports = function handleArrayContainerDefaults( + parentObjIn, + parentObjOut, + opts +) { + var name = opts.name; - var contIn = Array.isArray(parentObjIn[name]) ? parentObjIn[name] : [], - contOut = parentObjOut[name] = []; + var contIn = Array.isArray(parentObjIn[name]) ? parentObjIn[name] : [], + contOut = parentObjOut[name] = []; - for(var i = 0; i < contIn.length; i++) { - var itemIn = contIn[i], - itemOut = {}, - itemOpts = {}; + for (var i = 0; i < contIn.length; i++) { + var itemIn = contIn[i], itemOut = {}, itemOpts = {}; - if(!Lib.isPlainObject(itemIn)) { - itemOpts.itemIsNotPlainObject = true; - itemIn = {}; - } + if (!Lib.isPlainObject(itemIn)) { + itemOpts.itemIsNotPlainObject = true; + itemIn = {}; + } - opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); + opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); - itemOut._input = itemIn; - itemOut._index = i; + itemOut._input = itemIn; + itemOut._index = i; - contOut.push(itemOut); - } + contOut.push(itemOut); + } }; diff --git a/src/plots/attributes.js b/src/plots/attributes.js index 594fba13a6d..b5a1c44dd4b 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -5,104 +5,98 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - type: { - valType: 'enumerated', - role: 'info', - values: [], // listed dynamically - dflt: 'scatter' + type: { + valType: "enumerated", + role: "info", + values: [], + // listed dynamically + dflt: "scatter" + }, + visible: { + valType: "enumerated", + values: [true, false, "legendonly"], + role: "info", + dflt: true, + description: [ + "Determines whether or not this trace is visible.", + "If *legendonly*, the trace is not drawn,", + "but can appear as a legend item", + "(provided that the legend itself is visible)." + ].join(" ") + }, + showlegend: { + valType: "boolean", + role: "info", + dflt: true, + description: [ + "Determines whether or not an item corresponding to this", + "trace is shown in the legend." + ].join(" ") + }, + legendgroup: { + valType: "string", + role: "info", + dflt: "", + description: [ + "Sets the legend group for this trace.", + "Traces part of the same legend group hide/show at the same time", + "when toggling legend items." + ].join(" ") + }, + opacity: { + valType: "number", + role: "style", + min: 0, + max: 1, + dflt: 1, + description: "Sets the opacity of the trace." + }, + name: { + valType: "string", + role: "info", + description: [ + "Sets the trace name.", + "The trace name appear as the legend item and on hover." + ].join(" ") + }, + uid: { valType: "string", role: "info", dflt: "" }, + hoverinfo: { + valType: "flaglist", + role: "info", + flags: ["x", "y", "z", "text", "name"], + extras: ["all", "none", "skip"], + dflt: "all", + description: [ + "Determines which trace information appear on hover.", + "If `none` or `skip` are set, no information is displayed upon hovering.", + "But, if `none` is set, click and hover events are still fired." + ].join(" ") + }, + stream: { + token: { + valType: "string", + noBlank: true, + strict: true, + role: "info", + description: [ + "The stream id number links a data trace on a plot with a stream.", + "See https://plot.ly/settings for more details." + ].join(" ") }, - visible: { - valType: 'enumerated', - values: [true, false, 'legendonly'], - role: 'info', - dflt: true, - description: [ - 'Determines whether or not this trace is visible.', - 'If *legendonly*, the trace is not drawn,', - 'but can appear as a legend item', - '(provided that the legend itself is visible).' - ].join(' ') - }, - showlegend: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines whether or not an item corresponding to this', - 'trace is shown in the legend.' - ].join(' ') - }, - legendgroup: { - valType: 'string', - role: 'info', - dflt: '', - description: [ - 'Sets the legend group for this trace.', - 'Traces part of the same legend group hide/show at the same time', - 'when toggling legend items.' - ].join(' ') - }, - opacity: { - valType: 'number', - role: 'style', - min: 0, - max: 1, - dflt: 1, - description: 'Sets the opacity of the trace.' - }, - name: { - valType: 'string', - role: 'info', - description: [ - 'Sets the trace name.', - 'The trace name appear as the legend item and on hover.' - ].join(' ') - }, - uid: { - valType: 'string', - role: 'info', - dflt: '' - }, - hoverinfo: { - valType: 'flaglist', - role: 'info', - flags: ['x', 'y', 'z', 'text', 'name'], - extras: ['all', 'none', 'skip'], - dflt: 'all', - description: [ - 'Determines which trace information appear on hover.', - 'If `none` or `skip` are set, no information is displayed upon hovering.', - 'But, if `none` is set, click and hover events are still fired.' - ].join(' ') - }, - stream: { - token: { - valType: 'string', - noBlank: true, - strict: true, - role: 'info', - description: [ - 'The stream id number links a data trace on a plot with a stream.', - 'See https://plot.ly/settings for more details.' - ].join(' ') - }, - maxpoints: { - valType: 'number', - min: 0, - max: 10000, - dflt: 500, - role: 'info', - description: [ - 'Sets the maximum number of points to keep on the plots from an', - 'incoming stream.', - 'If `maxpoints` is set to *50*, only the newest 50 points will', - 'be displayed on the plot.' - ].join(' ') - } + maxpoints: { + valType: "number", + min: 0, + max: 10000, + dflt: 500, + role: "info", + description: [ + "Sets the maximum number of points to keep on the plots from an", + "incoming stream.", + "If `maxpoints` is set to *50*, only the newest 50 points will", + "be displayed on the plot." + ].join(" ") } + } }; diff --git a/src/plots/cartesian/attributes.js b/src/plots/cartesian/attributes.js index a472892edc6..40c1f408b2d 100644 --- a/src/plots/cartesian/attributes.js +++ b/src/plots/cartesian/attributes.js @@ -5,33 +5,30 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - xaxis: { - valType: 'subplotid', - role: 'info', - dflt: 'x', - description: [ - 'Sets a reference between this trace\'s x coordinates and', - 'a 2D cartesian x axis.', - 'If *x* (the default value), the x coordinates refer to', - '`layout.xaxis`.', - 'If *x2*, the x coordinates refer to `layout.xaxis2`, and so on.' - ].join(' ') - }, - yaxis: { - valType: 'subplotid', - role: 'info', - dflt: 'y', - description: [ - 'Sets a reference between this trace\'s y coordinates and', - 'a 2D cartesian y axis.', - 'If *y* (the default value), the y coordinates refer to', - '`layout.yaxis`.', - 'If *y2*, the y coordinates refer to `layout.xaxis2`, and so on.' - ].join(' ') - } + xaxis: { + valType: "subplotid", + role: "info", + dflt: "x", + description: [ + "Sets a reference between this trace's x coordinates and", + "a 2D cartesian x axis.", + "If *x* (the default value), the x coordinates refer to", + "`layout.xaxis`.", + "If *x2*, the x coordinates refer to `layout.xaxis2`, and so on." + ].join(" ") + }, + yaxis: { + valType: "subplotid", + role: "info", + dflt: "y", + description: [ + "Sets a reference between this trace's y coordinates and", + "a 2D cartesian y axis.", + "If *y* (the default value), the y coordinates refer to", + "`layout.yaxis`.", + "If *y2*, the y coordinates refer to `layout.xaxis2`, and so on." + ].join(" ") + } }; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 603a933d548..f3c02d80eb4 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -5,21 +5,18 @@ * 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 d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var Registry = require('../../registry'); -var Lib = require('../../lib'); -var svgTextUtils = require('../../lib/svg_text_utils'); -var Titles = require('../../components/titles'); -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); - -var constants = require('../../constants/numerical'); +"use strict"; +var d3 = require("d3"); +var isNumeric = require("fast-isnumeric"); + +var Registry = require("../../registry"); +var Lib = require("../../lib"); +var svgTextUtils = require("../../lib/svg_text_utils"); +var Titles = require("../../components/titles"); +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); + +var constants = require("../../constants/numerical"); var FP_SAFE = constants.FP_SAFE; var ONEAVGYEAR = constants.ONEAVGYEAR; var ONEAVGMONTH = constants.ONEAVGMONTH; @@ -29,15 +26,14 @@ var ONEMIN = constants.ONEMIN; var ONESEC = constants.ONESEC; var BADNUM = constants.BADNUM; - var axes = module.exports = {}; -axes.layoutAttributes = require('./layout_attributes'); -axes.supplyLayoutDefaults = require('./layout_defaults'); +axes.layoutAttributes = require("./layout_attributes"); +axes.supplyLayoutDefaults = require("./layout_defaults"); -axes.setConvert = require('./set_convert'); +axes.setConvert = require("./set_convert"); -var axisIds = require('./axis_ids'); +var axisIds = require("./axis_ids"); axes.id2name = axisIds.id2name; axes.cleanId = axisIds.cleanId; axes.list = axisIds.list; @@ -45,7 +41,6 @@ axes.listIds = axisIds.listIds; axes.getFromId = axisIds.getFromId; axes.getFromTrace = axisIds.getFromTrace; - /* * find the list of possible axes to reference with an xref or yref attribute * and coerce it to that list @@ -57,25 +52,31 @@ axes.getFromTrace = axisIds.getFromTrace; * extraOption: aside from existing axes with this letter, what non-axis value is allowed? * Only required if it's different from `dflt` */ -axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption) { - var axLetter = attr.charAt(attr.length - 1), - axlist = axes.listIds(gd, axLetter), - refAttr = attr + 'ref', - attrDef = {}; - - if(!dflt) dflt = axlist[0] || extraOption; - if(!extraOption) extraOption = dflt; - - // data-ref annotations are not supported in gl2d yet - - attrDef[refAttr] = { - valType: 'enumerated', - values: axlist.concat(extraOption ? [extraOption] : []), - dflt: dflt - }; - - // xref, yref - return Lib.coerce(containerIn, containerOut, attrDef, refAttr); +axes.coerceRef = function( + containerIn, + containerOut, + gd, + attr, + dflt, + extraOption +) { + var axLetter = attr.charAt(attr.length - 1), + axlist = axes.listIds(gd, axLetter), + refAttr = attr + "ref", + attrDef = {}; + + if (!dflt) dflt = axlist[0] || extraOption; + if (!extraOption) extraOption = dflt; + + // data-ref annotations are not supported in gl2d yet + attrDef[refAttr] = { + valType: "enumerated", + values: axlist.concat(extraOption ? [extraOption] : []), + dflt: dflt + }; + + // xref, yref + return Lib.coerce(containerIn, containerOut, attrDef, refAttr); }; /* @@ -101,54 +102,53 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption * - for other types: coerce them to numbers */ axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) { - var pos, - newPos; + var pos, newPos; - if(axRef === 'paper' || axRef === 'pixel') { - pos = coerce(attr, dflt); - } - else { - var ax = axes.getFromId(gd, axRef); + if (axRef === "paper" || axRef === "pixel") { + pos = coerce(attr, dflt); + } else { + var ax = axes.getFromId(gd, axRef); - dflt = ax.fraction2r(dflt); - pos = coerce(attr, dflt); + dflt = ax.fraction2r(dflt); + pos = coerce(attr, dflt); - if(ax.type === 'category') { - // if position is given as a category name, convert it to a number - if(typeof pos === 'string' && (ax._categories || []).length) { - newPos = ax._categories.indexOf(pos); - containerOut[attr] = (newPos === -1) ? dflt : newPos; - return; - } - } - else if(ax.type === 'date') { - containerOut[attr] = Lib.cleanDate(pos, BADNUM, ax.calendar); - return; - } - } - // finally make sure we have a number (unless date type already returned a string) - containerOut[attr] = isNumeric(pos) ? Number(pos) : dflt; + if (ax.type === "category") { + // if position is given as a category name, convert it to a number + if (typeof pos === "string" && (ax._categories || []).length) { + newPos = ax._categories.indexOf(pos); + containerOut[attr] = newPos === -1 ? dflt : newPos; + return; + } + } else if (ax.type === "date") { + containerOut[attr] = Lib.cleanDate(pos, BADNUM, ax.calendar); + return; + } + } + // finally make sure we have a number (unless date type already returned a string) + containerOut[attr] = isNumeric(pos) ? Number(pos) : dflt; }; // empty out types for all axes containing these traces // so we auto-set them again axes.clearTypes = function(gd, traces) { - if(!Array.isArray(traces) || !traces.length) { - traces = (gd._fullData).map(function(d, i) { return i; }); - } - traces.forEach(function(tracenum) { - var trace = gd.data[tracenum]; - delete (axes.getFromId(gd, trace.xaxis) || {}).type; - delete (axes.getFromId(gd, trace.yaxis) || {}).type; + if (!Array.isArray(traces) || !traces.length) { + traces = gd._fullData.map(function(d, i) { + return i; }); + } + traces.forEach(function(tracenum) { + var trace = gd.data[tracenum]; + delete (axes.getFromId(gd, trace.xaxis) || {}).type; + delete (axes.getFromId(gd, trace.yaxis) || {}).type; + }); }; // get counteraxis letter for this axis (name or id) // this can also be used as the id for default counter axis axes.counterLetter = function(id) { - var axLetter = id.charAt(0); - if(axLetter === 'x') return 'y'; - if(axLetter === 'y') return 'x'; + var axLetter = id.charAt(0); + if (axLetter === "x") return "y"; + if (axLetter === "y") return "x"; }; // incorporate a new minimum difference and first tick into @@ -156,35 +156,34 @@ axes.counterLetter = function(id) { // note that _forceTick0 is linearized, so needs to be turned into // a range value for setting tick0 axes.minDtick = function(ax, newDiff, newFirst, allow) { - // doesn't make sense to do forced min dTick on log or category axes, - // and the plot itself may decide to cancel (ie non-grouped bars) - if(['log', 'category'].indexOf(ax.type) !== -1 || !allow) { - ax._minDtick = 0; - } + // doesn't make sense to do forced min dTick on log or category axes, + // and the plot itself may decide to cancel (ie non-grouped bars) + if (["log", "category"].indexOf(ax.type) !== -1 || !allow) { + ax._minDtick = 0; + } else if (ax._minDtick === undefined) { // undefined means there's nothing there yet - else if(ax._minDtick === undefined) { - ax._minDtick = newDiff; - ax._forceTick0 = newFirst; - } - else if(ax._minDtick) { - // existing minDtick is an integer multiple of newDiff - // (within rounding err) - // and forceTick0 can be shifted to newFirst - if((ax._minDtick / newDiff + 1e-6) % 1 < 2e-6 && - (((newFirst - ax._forceTick0) / newDiff % 1) + - 1.000001) % 1 < 2e-6) { - ax._minDtick = newDiff; - ax._forceTick0 = newFirst; - } - // if the converse is true (newDiff is a multiple of minDtick and - // newFirst can be shifted to forceTick0) then do nothing - same - // forcing stands. Otherwise, cancel forced minimum - else if((newDiff / ax._minDtick + 1e-6) % 1 > 2e-6 || - (((newFirst - ax._forceTick0) / ax._minDtick % 1) + - 1.000001) % 1 > 2e-6) { - ax._minDtick = 0; - } - } + ax._minDtick = newDiff; + ax._forceTick0 = newFirst; + } else if (ax._minDtick) { + // existing minDtick is an integer multiple of newDiff + // (within rounding err) + // and forceTick0 can be shifted to newFirst + if ( + (ax._minDtick / newDiff + 1e-6) % 1 < 2e-6 && + ((newFirst - ax._forceTick0) / newDiff % 1 + 1.000001) % 1 < 2e-6 + ) { + ax._minDtick = newDiff; + ax._forceTick0 = newFirst; + } else if ( + (newDiff / ax._minDtick + 1e-6) % 1 > 2e-6 || + ((newFirst - ax._forceTick0) / ax._minDtick % 1 + 1.000001) % 1 > 2e-6 + ) { + // if the converse is true (newDiff is a multiple of minDtick and + // newFirst can be shifted to forceTick0) then do nothing - same + // forcing stands. Otherwise, cancel forced minimum + ax._minDtick = 0; + } + } }; // Find the autorange for this axis @@ -201,168 +200,152 @@ axes.minDtick = function(ax, newDiff, newFirst, allow) { // though, because otherwise values between categories (or outside all categories) // would be impossible. axes.getAutoRange = function(ax) { - var newRange = []; - - var minmin = ax._min[0].val, - maxmax = ax._max[0].val, - i; - - for(i = 1; i < ax._min.length; i++) { - if(minmin !== maxmax) break; - minmin = Math.min(minmin, ax._min[i].val); - } - for(i = 1; i < ax._max.length; i++) { - if(minmin !== maxmax) break; - maxmax = Math.max(maxmax, ax._max[i].val); - } - - var j, minpt, maxpt, minbest, maxbest, dp, dv, - mbest = 0, - axReverse = false; - - if(ax.range) { - var rng = Lib.simpleMap(ax.range, ax.r2l); - axReverse = rng[1] < rng[0]; - } - - // one-time setting to easily reverse the axis - // when plotting from code - if(ax.autorange === 'reversed') { - axReverse = true; - ax.autorange = true; - } - - for(i = 0; i < ax._min.length; i++) { - minpt = ax._min[i]; - for(j = 0; j < ax._max.length; j++) { - maxpt = ax._max[j]; - dv = maxpt.val - minpt.val; - dp = ax._length - minpt.pad - maxpt.pad; - if(dv > 0 && dp > 0 && dv / dp > mbest) { - minbest = minpt; - maxbest = maxpt; - mbest = dv / dp; - } - } - } - - if(minmin === maxmax) { - var lower = minmin - 1; - var upper = minmin + 1; - if(ax.rangemode === 'tozero') { - newRange = minmin < 0 ? [lower, 0] : [0, upper]; - } - else if(ax.rangemode === 'nonnegative') { - newRange = [Math.max(0, lower), Math.max(0, upper)]; - } - else { - newRange = [lower, upper]; - } - } - else if(mbest) { - if(ax.type === 'linear' || ax.type === '-') { - if(ax.rangemode === 'tozero') { - if(minbest.val >= 0) { - minbest = {val: 0, pad: 0}; - } - if(maxbest.val <= 0) { - maxbest = {val: 0, pad: 0}; - } - } - else if(ax.rangemode === 'nonnegative') { - if(minbest.val - mbest * minbest.pad < 0) { - minbest = {val: 0, pad: 0}; - } - if(maxbest.val < 0) { - maxbest = {val: 1, pad: 0}; - } - } - - // in case it changed again... - mbest = (maxbest.val - minbest.val) / - (ax._length - minbest.pad - maxbest.pad); + var newRange = []; - } + var minmin = ax._min[0].val, maxmax = ax._max[0].val, i; - newRange = [ - minbest.val - mbest * minbest.pad, - maxbest.val + mbest * maxbest.pad - ]; - } + for (i = 1; i < ax._min.length; i++) { + if (minmin !== maxmax) break; + minmin = Math.min(minmin, ax._min[i].val); + } + for (i = 1; i < ax._max.length; i++) { + if (minmin !== maxmax) break; + maxmax = Math.max(maxmax, ax._max[i].val); + } - // don't let axis have zero size, while still respecting tozero and nonnegative - if(newRange[0] === newRange[1]) { - if(ax.rangemode === 'tozero') { - if(newRange[0] < 0) { - newRange = [newRange[0], 0]; - } - else if(newRange[0] > 0) { - newRange = [0, newRange[0]]; - } - else { - newRange = [0, 1]; - } - } - else { - newRange = [newRange[0] - 1, newRange[0] + 1]; - if(ax.rangemode === 'nonnegative') { - newRange[0] = Math.max(0, newRange[0]); - } - } - } + var j, minpt, maxpt, minbest, maxbest, dp, dv, mbest = 0, axReverse = false; - // maintain reversal - if(axReverse) newRange.reverse(); - - return Lib.simpleMap(newRange, ax.l2r || Number); + if (ax.range) { + var rng = Lib.simpleMap(ax.range, ax.r2l); + axReverse = rng[1] < rng[0]; + } + + // one-time setting to easily reverse the axis + // when plotting from code + if (ax.autorange === "reversed") { + axReverse = true; + ax.autorange = true; + } + + for (i = 0; i < ax._min.length; i++) { + minpt = ax._min[i]; + for (j = 0; j < ax._max.length; j++) { + maxpt = ax._max[j]; + dv = maxpt.val - minpt.val; + dp = ax._length - minpt.pad - maxpt.pad; + if (dv > 0 && dp > 0 && dv / dp > mbest) { + minbest = minpt; + maxbest = maxpt; + mbest = dv / dp; + } + } + } + + if (minmin === maxmax) { + var lower = minmin - 1; + var upper = minmin + 1; + if (ax.rangemode === "tozero") { + newRange = minmin < 0 ? [lower, 0] : [0, upper]; + } else if (ax.rangemode === "nonnegative") { + newRange = [Math.max(0, lower), Math.max(0, upper)]; + } else { + newRange = [lower, upper]; + } + } else if (mbest) { + if (ax.type === "linear" || ax.type === "-") { + if (ax.rangemode === "tozero") { + if (minbest.val >= 0) { + minbest = { val: 0, pad: 0 }; + } + if (maxbest.val <= 0) { + maxbest = { val: 0, pad: 0 }; + } + } else if (ax.rangemode === "nonnegative") { + if (minbest.val - mbest * minbest.pad < 0) { + minbest = { val: 0, pad: 0 }; + } + if (maxbest.val < 0) { + maxbest = { val: 1, pad: 0 }; + } + } + + // in case it changed again... + mbest = (maxbest.val - minbest.val) / + (ax._length - minbest.pad - maxbest.pad); + } + + newRange = [ + minbest.val - mbest * minbest.pad, + maxbest.val + mbest * maxbest.pad + ]; + } + + // don't let axis have zero size, while still respecting tozero and nonnegative + if (newRange[0] === newRange[1]) { + if (ax.rangemode === "tozero") { + if (newRange[0] < 0) { + newRange = [newRange[0], 0]; + } else if (newRange[0] > 0) { + newRange = [0, newRange[0]]; + } else { + newRange = [0, 1]; + } + } else { + newRange = [newRange[0] - 1, newRange[0] + 1]; + if (ax.rangemode === "nonnegative") { + newRange[0] = Math.max(0, newRange[0]); + } + } + } + + // maintain reversal + if (axReverse) newRange.reverse(); + + return Lib.simpleMap(newRange, ax.l2r || Number); }; axes.doAutoRange = function(ax) { - if(!ax._length) ax.setScale(); + if (!ax._length) ax.setScale(); - // TODO do we really need this? - var hasDeps = (ax._min && ax._max && ax._min.length && ax._max.length); + // TODO do we really need this? + var hasDeps = ax._min && ax._max && ax._min.length && ax._max.length; - if(ax.autorange && hasDeps) { - ax.range = axes.getAutoRange(ax); + if (ax.autorange && hasDeps) { + ax.range = axes.getAutoRange(ax); - // doAutoRange will get called on fullLayout, - // but we want to report its results back to layout - var axIn = ax._gd.layout[ax._name]; + // doAutoRange will get called on fullLayout, + // but we want to report its results back to layout + var axIn = ax._gd.layout[ax._name]; - if(!axIn) ax._gd.layout[ax._name] = axIn = {}; + if (!axIn) ax._gd.layout[ax._name] = axIn = {}; - if(axIn !== ax) { - axIn.range = ax.range.slice(); - axIn.autorange = ax.autorange; - } + if (axIn !== ax) { + axIn.range = ax.range.slice(); + axIn.autorange = ax.autorange; } + } }; // save a copy of the initial axis ranges in fullLayout // use them in mode bar and dblclick events axes.saveRangeInitial = function(gd, overwrite) { - var axList = axes.list(gd, '', true), - hasOneAxisChanged = false; - - for(var i = 0; i < axList.length; i++) { - var ax = axList[i]; - - var isNew = (ax._rangeInitial === undefined); - var hasChanged = ( - isNew || !( - ax.range[0] === ax._rangeInitial[0] && - ax.range[1] === ax._rangeInitial[1] - ) - ); - - if((isNew && ax.autorange === false) || (overwrite && hasChanged)) { - ax._rangeInitial = ax.range.slice(); - hasOneAxisChanged = true; - } + var axList = axes.list(gd, "", true), hasOneAxisChanged = false; + + for (var i = 0; i < axList.length; i++) { + var ax = axList[i]; + + var isNew = ax._rangeInitial === undefined; + var hasChanged = isNew || + !(ax.range[0] === ax._rangeInitial[0] && + ax.range[1] === ax._rangeInitial[1]); + + if (isNew && ax.autorange === false || overwrite && hasChanged) { + ax._rangeInitial = ax.range.slice(); + hasOneAxisChanged = true; } + } - return hasOneAxisChanged; + return hasOneAxisChanged; }; // axes.expand: if autoranging, include new data in the outer limits @@ -378,435 +361,457 @@ axes.saveRangeInitial = function(gd, overwrite) { // tozero: (boolean) make sure to include zero if axis is linear, // and make it a tight bound if possible axes.expand = function(ax, data, options) { - if(!(ax.autorange || ax._needsExpand) || !data) return; - if(!ax._min) ax._min = []; - if(!ax._max) ax._max = []; - if(!options) options = {}; - if(!ax._m) ax.setScale(); - - var len = data.length, - extrappad = options.padded ? ax._length * 0.05 : 0, - tozero = options.tozero && (ax.type === 'linear' || ax.type === '-'), - i, j, v, di, dmin, dmax, - ppadiplus, ppadiminus, includeThis, vmin, vmax; - - function getPad(item) { - if(Array.isArray(item)) { - return function(i) { return Math.max(Number(item[i]||0), 0); }; - } - else { - var v = Math.max(Number(item||0), 0); - return function() { return v; }; - } - } - var ppadplus = getPad((ax._m > 0 ? - options.ppadplus : options.ppadminus) || options.ppad || 0), - ppadminus = getPad((ax._m > 0 ? - options.ppadminus : options.ppadplus) || options.ppad || 0), - vpadplus = getPad(options.vpadplus || options.vpad), - vpadminus = getPad(options.vpadminus || options.vpad); - - function addItem(i) { - di = data[i]; - if(!isNumeric(di)) return; - ppadiplus = ppadplus(i) + extrappad; - ppadiminus = ppadminus(i) + extrappad; - vmin = di - vpadminus(i); - vmax = di + vpadplus(i); - // special case for log axes: if vpad makes this object span - // more than an order of mag, clip it to one order. This is so - // we don't have non-positive errors or absurdly large lower - // range due to rounding errors - if(ax.type === 'log' && vmin < vmax / 10) { vmin = vmax / 10; } - - dmin = ax.c2l(vmin); - dmax = ax.c2l(vmax); - - if(tozero) { - dmin = Math.min(0, dmin); - dmax = Math.max(0, dmax); - } - - // In order to stop overflow errors, don't consider points - // too close to the limits of js floating point - function goodNumber(v) { - return isNumeric(v) && Math.abs(v) < FP_SAFE; - } - - if(goodNumber(dmin)) { - includeThis = true; - // take items v from ax._min and compare them to the - // presently active point: - // - if the item supercedes the new point, set includethis false - // - if the new pt supercedes the item, delete it from ax._min - for(j = 0; j < ax._min.length && includeThis; j++) { - v = ax._min[j]; - if(v.val <= dmin && v.pad >= ppadiminus) { - includeThis = false; - } - else if(v.val >= dmin && v.pad <= ppadiminus) { - ax._min.splice(j, 1); - j--; - } - } - if(includeThis) { - ax._min.push({ - val: dmin, - pad: (tozero && dmin === 0) ? 0 : ppadiminus - }); - } - } - - if(goodNumber(dmax)) { - includeThis = true; - for(j = 0; j < ax._max.length && includeThis; j++) { - v = ax._max[j]; - if(v.val >= dmax && v.pad >= ppadiplus) { - includeThis = false; - } - else if(v.val <= dmax && v.pad <= ppadiplus) { - ax._max.splice(j, 1); - j--; - } - } - if(includeThis) { - ax._max.push({ - val: dmax, - pad: (tozero && dmax === 0) ? 0 : ppadiplus - }); - } - } - } - - // For efficiency covering monotonic or near-monotonic data, - // check a few points at both ends first and then sweep - // through the middle - for(i = 0; i < 6; i++) addItem(i); - for(i = len - 1; i > 5; i--) addItem(i); - + if (!(ax.autorange || ax._needsExpand) || !data) return; + if (!ax._min) ax._min = []; + if (!ax._max) ax._max = []; + if (!options) options = {}; + if (!ax._m) ax.setScale(); + + var len = data.length, + extrappad = options.padded ? ax._length * 0.05 : 0, + tozero = options.tozero && (ax.type === "linear" || ax.type === "-"), + i, + j, + v, + di, + dmin, + dmax, + ppadiplus, + ppadiminus, + includeThis, + vmin, + vmax; + + function getPad(item) { + if (Array.isArray(item)) { + return function(i) { + return Math.max(Number(item[i] || 0), 0); + }; + } else { + var v = Math.max(Number(item || 0), 0); + return function() { + return v; + }; + } + } + var ppadplus = getPad( + (ax._m > 0 ? options.ppadplus : options.ppadminus) || options.ppad || 0 + ), + ppadminus = getPad( + (ax._m > 0 ? options.ppadminus : options.ppadplus) || options.ppad || 0 + ), + vpadplus = getPad(options.vpadplus || options.vpad), + vpadminus = getPad(options.vpadminus || options.vpad); + + function addItem(i) { + di = data[i]; + if (!isNumeric(di)) return; + ppadiplus = ppadplus(i) + extrappad; + ppadiminus = ppadminus(i) + extrappad; + vmin = di - vpadminus(i); + vmax = di + vpadplus(i); + // special case for log axes: if vpad makes this object span + // more than an order of mag, clip it to one order. This is so + // we don't have non-positive errors or absurdly large lower + // range due to rounding errors + if (ax.type === "log" && vmin < vmax / 10) { + vmin = vmax / 10; + } + + dmin = ax.c2l(vmin); + dmax = ax.c2l(vmax); + + if (tozero) { + dmin = Math.min(0, dmin); + dmax = Math.max(0, dmax); + } + + // In order to stop overflow errors, don't consider points + // too close to the limits of js floating point + function goodNumber(v) { + return isNumeric(v) && Math.abs(v) < FP_SAFE; + } + + if (goodNumber(dmin)) { + includeThis = true; + // take items v from ax._min and compare them to the + // presently active point: + // - if the item supercedes the new point, set includethis false + // - if the new pt supercedes the item, delete it from ax._min + for (j = 0; j < ax._min.length && includeThis; j++) { + v = ax._min[j]; + if (v.val <= dmin && v.pad >= ppadiminus) { + includeThis = false; + } else if (v.val >= dmin && v.pad <= ppadiminus) { + ax._min.splice(j, 1); + j--; + } + } + if (includeThis) { + ax._min.push({ val: dmin, pad: tozero && dmin === 0 ? 0 : ppadiminus }); + } + } + + if (goodNumber(dmax)) { + includeThis = true; + for (j = 0; j < ax._max.length && includeThis; j++) { + v = ax._max[j]; + if (v.val >= dmax && v.pad >= ppadiplus) { + includeThis = false; + } else if (v.val <= dmax && v.pad <= ppadiplus) { + ax._max.splice(j, 1); + j--; + } + } + if (includeThis) { + ax._max.push({ val: dmax, pad: tozero && dmax === 0 ? 0 : ppadiplus }); + } + } + } + + // For efficiency covering monotonic or near-monotonic data, + // check a few points at both ends first and then sweep + // through the middle + for (i = 0; i < 6; i++) { + addItem(i); + } + for (i = len - 1; i > 5; i--) { + addItem(i); + } }; axes.autoBin = function(data, ax, nbins, is2d, calendar) { - var dataMin = Lib.aggNums(Math.min, null, data), - dataMax = Lib.aggNums(Math.max, null, data); - - if(!calendar) calendar = ax.calendar; - - if(ax.type === 'category') { - return { - start: dataMin - 0.5, - end: dataMax + 0.5, - size: 1 - }; - } - - var size0; - if(nbins) size0 = ((dataMax - dataMin) / nbins); - else { - // totally auto: scale off std deviation so the highest bin is - // somewhat taller than the total number of bins, but don't let - // the size get smaller than the 'nice' rounded down minimum - // difference between values - var distinctData = Lib.distinctVals(data), - msexp = Math.pow(10, Math.floor( - Math.log(distinctData.minDiff) / Math.LN10)), - minSize = msexp * Lib.roundUp( - distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true); - size0 = Math.max(minSize, 2 * Lib.stdev(data) / - Math.pow(data.length, is2d ? 0.25 : 0.4)); - - // fallback if ax.d2c output BADNUMs - // e.g. when user try to plot categorical bins - // on a layout.xaxis.type: 'linear' - if(!isNumeric(size0)) size0 = 1; - } - - // piggyback off autotick code to make "nice" bin sizes - var dummyAx; - if(ax.type === 'log') { - dummyAx = { - type: 'linear', - range: [dataMin, dataMax] - }; - } - else { - dummyAx = { - type: ax.type, - range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar), - calendar: calendar - }; - } - axes.setConvert(dummyAx); - - axes.autoTicks(dummyAx, size0); - var binStart = axes.tickIncrement( - axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse', calendar), - binEnd; - - // check for too many data points right at the edges of bins - // (>50% within 1% of bin edges) or all data points integral - // and offset the bins accordingly - if(typeof dummyAx.dtick === 'number') { - binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax); - - var bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick); - binEnd = binStart + bincount * dummyAx.dtick; - } - else { - // month ticks - should be the only nonlinear kind we have at this point. - // dtick (as supplied by axes.autoTick) only has nonlinear values on - // date and log axes, but even if you display a histogram on a log axis - // we bin it on a linear axis (which one could argue against, but that's - // a separate issue) - if(dummyAx.dtick.charAt(0) === 'M') { - binStart = autoShiftMonthBins(binStart, data, dummyAx.dtick, dataMin, calendar); - } - - // calculate the endpoint for nonlinear ticks - you have to - // just increment until you're done - binEnd = binStart; - while(binEnd <= dataMax) { - binEnd = axes.tickIncrement(binEnd, dummyAx.dtick, false, calendar); - } - } + var dataMin = Lib.aggNums(Math.min, null, data), + dataMax = Lib.aggNums(Math.max, null, data); + + if (!calendar) calendar = ax.calendar; + + if (ax.type === "category") { + return { start: dataMin - 0.5, end: dataMax + 0.5, size: 1 }; + } + + var size0; + if (nbins) { + size0 = (dataMax - dataMin) / nbins; + } else { + // totally auto: scale off std deviation so the highest bin is + // somewhat taller than the total number of bins, but don't let + // the size get smaller than the 'nice' rounded down minimum + // difference between values + var distinctData = Lib.distinctVals(data), + msexp = Math.pow( + 10, + Math.floor(Math.log(distinctData.minDiff) / Math.LN10) + ), + minSize = msexp * + Lib.roundUp(distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true); + size0 = Math.max( + minSize, + 2 * Lib.stdev(data) / Math.pow(data.length, is2d ? 0.25 : 0.4) + ); - return { - start: ax.c2r(binStart, 0, calendar), - end: ax.c2r(binEnd, 0, calendar), - size: dummyAx.dtick + // fallback if ax.d2c output BADNUMs + // e.g. when user try to plot categorical bins + // on a layout.xaxis.type: 'linear' + if (!isNumeric(size0)) size0 = 1; + } + + // piggyback off autotick code to make "nice" bin sizes + var dummyAx; + if (ax.type === "log") { + dummyAx = { type: "linear", range: [dataMin, dataMax] }; + } else { + dummyAx = { + type: ax.type, + range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar), + calendar: calendar }; + } + axes.setConvert(dummyAx); + + axes.autoTicks(dummyAx, size0); + var binStart = axes.tickIncrement( + axes.tickFirst(dummyAx), + dummyAx.dtick, + "reverse", + calendar + ), + binEnd; + + // check for too many data points right at the edges of bins + // (>50% within 1% of bin edges) or all data points integral + // and offset the bins accordingly + if (typeof dummyAx.dtick === "number") { + binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax); + + var bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick); + binEnd = binStart + bincount * dummyAx.dtick; + } else { + // month ticks - should be the only nonlinear kind we have at this point. + // dtick (as supplied by axes.autoTick) only has nonlinear values on + // date and log axes, but even if you display a histogram on a log axis + // we bin it on a linear axis (which one could argue against, but that's + // a separate issue) + if (dummyAx.dtick.charAt(0) === "M") { + binStart = autoShiftMonthBins( + binStart, + data, + dummyAx.dtick, + dataMin, + calendar + ); + } + + // calculate the endpoint for nonlinear ticks - you have to + // just increment until you're done + binEnd = binStart; + while (binEnd <= dataMax) { + binEnd = axes.tickIncrement(binEnd, dummyAx.dtick, false, calendar); + } + } + + return { + start: ax.c2r(binStart, 0, calendar), + end: ax.c2r(binEnd, 0, calendar), + size: dummyAx.dtick + }; }; - function autoShiftNumericBins(binStart, data, ax, dataMin, dataMax) { - var edgecount = 0, - midcount = 0, - intcount = 0, - blankCount = 0; - - function nearEdge(v) { - // is a value within 1% of a bin edge? - return (1 + (v - binStart) * 100 / ax.dtick) % 100 < 2; - } - - for(var i = 0; i < data.length; i++) { - if(data[i] % 1 === 0) intcount++; - else if(!isNumeric(data[i])) blankCount++; - - if(nearEdge(data[i])) edgecount++; - if(nearEdge(data[i] + ax.dtick / 2)) midcount++; - } - var dataCount = data.length - blankCount; - - if(intcount === dataCount && ax.type !== 'date') { - // all integers: if bin size is <1, it's because - // that was specifically requested (large nbins) - // so respect that... but center the bins containing - // integers on those integers - if(ax.dtick < 1) { - binStart = dataMin - 0.5 * ax.dtick; - } - // otherwise start half an integer down regardless of - // the bin size, just enough to clear up endpoint - // ambiguity about which integers are in which bins. - else { - binStart -= 0.5; - if(binStart + ax.dtick < dataMin) binStart += ax.dtick; - } - } - else if(midcount < dataCount * 0.1) { - if(edgecount > dataCount * 0.3 || - nearEdge(dataMin) || nearEdge(dataMax)) { - // lots of points at the edge, not many in the middle - // shift half a bin - var binshift = ax.dtick / 2; - binStart += (binStart + binshift < dataMin) ? binshift : -binshift; - } - } - return binStart; + var edgecount = 0, midcount = 0, intcount = 0, blankCount = 0; + + function nearEdge(v) { + // is a value within 1% of a bin edge? + return (1 + (v - binStart) * 100 / ax.dtick) % 100 < 2; + } + + for (var i = 0; i < data.length; i++) { + if (data[i] % 1 === 0) intcount++; + else if (!isNumeric(data[i])) blankCount++; + + if (nearEdge(data[i])) edgecount++; + if (nearEdge(data[i] + ax.dtick / 2)) midcount++; + } + var dataCount = data.length - blankCount; + + if (intcount === dataCount && ax.type !== "date") { + // all integers: if bin size is <1, it's because + // that was specifically requested (large nbins) + // so respect that... but center the bins containing + // integers on those integers + if (ax.dtick < 1) { + binStart = dataMin - 0.5 * ax.dtick; + } else { + // otherwise start half an integer down regardless of + // the bin size, just enough to clear up endpoint + // ambiguity about which integers are in which bins. + binStart -= 0.5; + if (binStart + ax.dtick < dataMin) binStart += ax.dtick; + } + } else if (midcount < dataCount * 0.1) { + if (edgecount > dataCount * 0.3 || nearEdge(dataMin) || nearEdge(dataMax)) { + // lots of points at the edge, not many in the middle + // shift half a bin + var binshift = ax.dtick / 2; + binStart += binStart + binshift < dataMin ? binshift : -binshift; + } + } + return binStart; } - function autoShiftMonthBins(binStart, data, dtick, dataMin, calendar) { - var stats = Lib.findExactDates(data, calendar); - // number of data points that needs to be an exact value - // to shift that increment to (near) the bin center - var threshold = 0.8; - - if(stats.exactDays > threshold) { - var numMonths = Number(dtick.substr(1)); - - if((stats.exactYears > threshold) && (numMonths % 12 === 0)) { - // The exact middle of a non-leap-year is 1.5 days into July - // so if we start the bins here, all but leap years will - // get hover-labeled as exact years. - binStart = axes.tickIncrement(binStart, 'M6', 'reverse') + ONEDAY * 1.5; - } - else if(stats.exactMonths > threshold) { - // Months are not as clean, but if we shift half the *longest* - // month (31/2 days) then 31-day months will get labeled exactly - // and shorter months will get labeled with the correct month - // but shifted 12-36 hours into it. - binStart = axes.tickIncrement(binStart, 'M1', 'reverse') + ONEDAY * 15.5; - } - else { - // Shifting half a day is exact, but since these are month bins it - // will always give a somewhat odd-looking label, until we do something - // smarter like showing the bin boundaries (or the bounds of the actual - // data in each bin) - binStart -= ONEDAY / 2; - } - var nextBinStart = axes.tickIncrement(binStart, dtick); - - if(nextBinStart <= dataMin) return nextBinStart; - } - return binStart; + var stats = Lib.findExactDates(data, calendar); + // number of data points that needs to be an exact value + // to shift that increment to (near) the bin center + var threshold = 0.8; + + if (stats.exactDays > threshold) { + var numMonths = Number(dtick.substr(1)); + + if (stats.exactYears > threshold && numMonths % 12 === 0) { + // The exact middle of a non-leap-year is 1.5 days into July + // so if we start the bins here, all but leap years will + // get hover-labeled as exact years. + binStart = axes.tickIncrement(binStart, "M6", "reverse") + ONEDAY * 1.5; + } else if (stats.exactMonths > threshold) { + // Months are not as clean, but if we shift half the *longest* + // month (31/2 days) then 31-day months will get labeled exactly + // and shorter months will get labeled with the correct month + // but shifted 12-36 hours into it. + binStart = axes.tickIncrement(binStart, "M1", "reverse") + ONEDAY * 15.5; + } else { + // Shifting half a day is exact, but since these are month bins it + // will always give a somewhat odd-looking label, until we do something + // smarter like showing the bin boundaries (or the bounds of the actual + // data in each bin) + binStart -= ONEDAY / 2; + } + var nextBinStart = axes.tickIncrement(binStart, dtick); + + if (nextBinStart <= dataMin) return nextBinStart; + } + return binStart; } // ---------------------------------------------------- // Ticks and grids // ---------------------------------------------------- - // calculate the ticks: text, values, positioning // if ticks are set to automatic, determine the right values (tick0,dtick) // in any case, set tickround to # of digits to round tick labels to, // or codes to this effect for log and date scales axes.calcTicks = function calcTicks(ax) { - var rng = Lib.simpleMap(ax.range, ax.r2l); - - // calculate max number of (auto) ticks to display based on plot size - if(ax.tickmode === 'auto' || !ax.dtick) { - var nt = ax.nticks, - minPx; - if(!nt) { - if(ax.type === 'category') { - minPx = ax.tickfont ? (ax.tickfont.size || 12) * 1.2 : 15; - nt = ax._length / minPx; - } - else { - minPx = ax._id.charAt(0) === 'y' ? 40 : 80; - nt = Lib.constrain(ax._length / minPx, 4, 9) + 1; - } - } - - // add a couple of extra digits for filling in ticks when we - // have explicit tickvals without tick text - if(ax.tickmode === 'array') nt *= 100; - - axes.autoTicks(ax, Math.abs(rng[1] - rng[0]) / nt); - // check for a forced minimum dtick - if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) { - ax.dtick = ax._minDtick; - ax.tick0 = ax.l2r(ax._forceTick0); - } - } - - // check for missing tick0 - if(!ax.tick0) { - ax.tick0 = (ax.type === 'date') ? '2000-01-01' : 0; - } - - // now figure out rounding of tick values - autoTickRound(ax); - - // now that we've figured out the auto values for formatting - // in case we're missing some ticktext, we can break out for array ticks - if(ax.tickmode === 'array') return arrayTicks(ax); - - // find the first tick - ax._tmin = axes.tickFirst(ax); - - // check for reversed axis - var axrev = (rng[1] < rng[0]); + var rng = Lib.simpleMap(ax.range, ax.r2l); + + // calculate max number of (auto) ticks to display based on plot size + if (ax.tickmode === "auto" || !ax.dtick) { + var nt = ax.nticks, minPx; + if (!nt) { + if (ax.type === "category") { + minPx = ax.tickfont ? (ax.tickfont.size || 12) * 1.2 : 15; + nt = ax._length / minPx; + } else { + minPx = ax._id.charAt(0) === "y" ? 40 : 80; + nt = Lib.constrain(ax._length / minPx, 4, 9) + 1; + } + } + + // add a couple of extra digits for filling in ticks when we + // have explicit tickvals without tick text + if (ax.tickmode === "array") nt *= 100; + + axes.autoTicks(ax, Math.abs(rng[1] - rng[0]) / nt); + // check for a forced minimum dtick + if (ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) { + ax.dtick = ax._minDtick; + ax.tick0 = ax.l2r(ax._forceTick0); + } + } + + // check for missing tick0 + if (!ax.tick0) { + ax.tick0 = ax.type === "date" ? "2000-01-01" : 0; + } + + // now figure out rounding of tick values + autoTickRound(ax); + + // now that we've figured out the auto values for formatting + // in case we're missing some ticktext, we can break out for array ticks + if (ax.tickmode === "array") return arrayTicks(ax); + + // find the first tick + ax._tmin = axes.tickFirst(ax); + + // check for reversed axis + var axrev = rng[1] < rng[0]; + + // return the full set of tick vals + var vals = [], + // add a tiny bit so we get ticks which may have rounded out + endtick = rng[1] * 1.0001 - rng[0] * 0.0001; + if (ax.type === "category") { + endtick = axrev + ? Math.max(-0.5, endtick) + : Math.min(ax._categories.length - 0.5, endtick); + } + for ( + var x = ax._tmin; + axrev ? x >= endtick : x <= endtick; + x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar) + ) { + vals.push(x); - // return the full set of tick vals - var vals = [], - // add a tiny bit so we get ticks which may have rounded out - endtick = rng[1] * 1.0001 - rng[0] * 0.0001; - if(ax.type === 'category') { - endtick = (axrev) ? Math.max(-0.5, endtick) : - Math.min(ax._categories.length - 0.5, endtick); - } - for(var x = ax._tmin; - (axrev) ? (x >= endtick) : (x <= endtick); - x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar)) { - vals.push(x); - - // prevent infinite loops - if(vals.length > 1000) break; - } + // prevent infinite loops + if (vals.length > 1000) break; + } - // save the last tick as well as first, so we can - // show the exponent only on the last one - ax._tmax = vals[vals.length - 1]; + // save the last tick as well as first, so we can + // show the exponent only on the last one + ax._tmax = vals[vals.length - 1]; - // for showing the rest of a date when the main tick label is only the - // latter part: ax._prevDateHead holds what we showed most recently. - // Start with it cleared and mark that we're in calcTicks (ie calculating a - // whole string of these so we should care what the previous date head was!) - ax._prevDateHead = ''; - ax._inCalcTicks = true; + // for showing the rest of a date when the main tick label is only the + // latter part: ax._prevDateHead holds what we showed most recently. + // Start with it cleared and mark that we're in calcTicks (ie calculating a + // whole string of these so we should care what the previous date head was!) + ax._prevDateHead = ""; + ax._inCalcTicks = true; - var ticksOut = new Array(vals.length); - for(var i = 0; i < vals.length; i++) ticksOut[i] = axes.tickText(ax, vals[i]); + var ticksOut = new Array(vals.length); + for (var i = 0; i < vals.length; i++) { + ticksOut[i] = axes.tickText(ax, vals[i]); + } - ax._inCalcTicks = false; + ax._inCalcTicks = false; - return ticksOut; + return ticksOut; }; function arrayTicks(ax) { - var vals = ax.tickvals, - text = ax.ticktext, - ticksOut = new Array(vals.length), - rng = Lib.simpleMap(ax.range, ax.r2l), - r0expanded = rng[0] * 1.0001 - rng[1] * 0.0001, - r1expanded = rng[1] * 1.0001 - rng[0] * 0.0001, - tickMin = Math.min(r0expanded, r1expanded), - tickMax = Math.max(r0expanded, r1expanded), - vali, - i, - j = 0; - - // without a text array, just format the given values as any other ticks - // except with more precision to the numbers - if(!Array.isArray(text)) text = []; - - // make sure showing ticks doesn't accidentally add new categories - var tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l; - - // array ticks on log axes always show the full number - // (if no explicit ticktext overrides it) - if(ax.type === 'log' && String(ax.dtick).charAt(0) !== 'L') { - ax.dtick = 'L' + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1); - } - - for(i = 0; i < vals.length; i++) { - vali = tickVal2l(vals[i]); - if(vali > tickMin && vali < tickMax) { - if(text[i] === undefined) ticksOut[j] = axes.tickText(ax, vali); - else ticksOut[j] = tickTextObj(ax, vali, String(text[i])); - j++; - } - } - - if(j < vals.length) ticksOut.splice(j, vals.length - j); - - return ticksOut; + var vals = ax.tickvals, + text = ax.ticktext, + ticksOut = new Array(vals.length), + rng = Lib.simpleMap(ax.range, ax.r2l), + r0expanded = rng[0] * 1.0001 - rng[1] * 0.0001, + r1expanded = rng[1] * 1.0001 - rng[0] * 0.0001, + tickMin = Math.min(r0expanded, r1expanded), + tickMax = Math.max(r0expanded, r1expanded), + vali, + i, + j = 0; + + // without a text array, just format the given values as any other ticks + // except with more precision to the numbers + if (!Array.isArray(text)) text = []; + + // make sure showing ticks doesn't accidentally add new categories + var tickVal2l = ax.type === "category" ? ax.d2l_noadd : ax.d2l; + + // array ticks on log axes always show the full number + // (if no explicit ticktext overrides it) + if (ax.type === "log" && String(ax.dtick).charAt(0) !== "L") { + ax.dtick = "L" + + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1); + } + + for (i = 0; i < vals.length; i++) { + vali = tickVal2l(vals[i]); + if (vali > tickMin && vali < tickMax) { + if (text[i] === undefined) ticksOut[j] = axes.tickText(ax, vali); + else ticksOut[j] = tickTextObj(ax, vali, String(text[i])); + j++; + } + } + + if (j < vals.length) ticksOut.splice(j, vals.length - j); + + return ticksOut; } var roundBase10 = [2, 5, 10], - roundBase24 = [1, 2, 3, 6, 12], - roundBase60 = [1, 2, 5, 10, 15, 30], - // 2&3 day ticks are weird, but need something btwn 1&7 - roundDays = [1, 2, 3, 7, 14], - // approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2) - // these don't have to be exact, just close enough to round to the right value - roundLog1 = [-0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1], - roundLog2 = [-0.301, 0, 0.301, 0.699, 1]; + roundBase24 = [1, 2, 3, 6, 12], + roundBase60 = [1, 2, 5, 10, 15, 30], + // 2&3 day ticks are weird, but need something btwn 1&7 + roundDays = [1, 2, 3, 7, 14], + // approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2) + // these don't have to be exact, just close enough to round to the right value + roundLog1 = [ + -0.046, + 0, + 0.301, + 0.477, + 0.602, + 0.699, + 0.778, + 0.845, + 0.903, + 0.954, + 1 + ], + roundLog2 = [-0.301, 0, 0.301, 0.699, 1]; function roundDTick(roughDTick, base, roundingSet) { - return base * Lib.roundUp(roughDTick / base, roundingSet); + return base * Lib.roundUp(roughDTick / base, roundingSet); } // autoTicks: calculate best guess at pleasant ticks for this axis @@ -826,90 +831,78 @@ function roundDTick(roughDTick, base, roundingSet) { // log showing powers plus some intermediates: // D1 shows all digits, D2 shows 2 and 5 axes.autoTicks = function(ax, roughDTick) { - var base; - - if(ax.type === 'date') { - ax.tick0 = Lib.dateTick0(ax.calendar); - // the criteria below are all based on the rough spacing we calculate - // being > half of the final unit - so precalculate twice the rough val - var roughX2 = 2 * roughDTick; - - if(roughX2 > ONEAVGYEAR) { - roughDTick /= ONEAVGYEAR; - base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); - ax.dtick = 'M' + (12 * roundDTick(roughDTick, base, roundBase10)); - } - else if(roughX2 > ONEAVGMONTH) { - roughDTick /= ONEAVGMONTH; - ax.dtick = 'M' + roundDTick(roughDTick, 1, roundBase24); - } - else if(roughX2 > ONEDAY) { - ax.dtick = roundDTick(roughDTick, ONEDAY, roundDays); - // get week ticks on sunday - // this will also move the base tick off 2000-01-01 if dtick is - // 2 or 3 days... but that's a weird enough case that we'll ignore it. - ax.tick0 = Lib.dateTick0(ax.calendar, true); - } - else if(roughX2 > ONEHOUR) { - ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24); - } - else if(roughX2 > ONEMIN) { - ax.dtick = roundDTick(roughDTick, ONEMIN, roundBase60); - } - else if(roughX2 > ONESEC) { - ax.dtick = roundDTick(roughDTick, ONESEC, roundBase60); - } - else { - // milliseconds - base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); - ax.dtick = roundDTick(roughDTick, base, roundBase10); - } - } - else if(ax.type === 'log') { - ax.tick0 = 0; - var rng = Lib.simpleMap(ax.range, ax.r2l); - - if(roughDTick > 0.7) { - // only show powers of 10 - ax.dtick = Math.ceil(roughDTick); - } - else if(Math.abs(rng[1] - rng[0]) < 1) { - // span is less than one power of 10 - var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick); - - // ticks on a linear scale, labeled fully - roughDTick = Math.abs(Math.pow(10, rng[1]) - - Math.pow(10, rng[0])) / nt; - base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); - ax.dtick = 'L' + roundDTick(roughDTick, base, roundBase10); - } - else { - // include intermediates between powers of 10, - // labeled with small digits - // ax.dtick = "D2" (show 2 and 5) or "D1" (show all digits) - ax.dtick = (roughDTick > 0.3) ? 'D2' : 'D1'; - } - } - else if(ax.type === 'category') { - ax.tick0 = 0; - ax.dtick = Math.ceil(Math.max(roughDTick, 1)); - } - else { - // auto ticks always start at 0 - ax.tick0 = 0; - base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); - ax.dtick = roundDTick(roughDTick, base, roundBase10); - } - - // prevent infinite loops - if(ax.dtick === 0) ax.dtick = 1; + var base; + + if (ax.type === "date") { + ax.tick0 = Lib.dateTick0(ax.calendar); + // the criteria below are all based on the rough spacing we calculate + // being > half of the final unit - so precalculate twice the rough val + var roughX2 = 2 * roughDTick; + + if (roughX2 > ONEAVGYEAR) { + roughDTick /= ONEAVGYEAR; + base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); + ax.dtick = "M" + 12 * roundDTick(roughDTick, base, roundBase10); + } else if (roughX2 > ONEAVGMONTH) { + roughDTick /= ONEAVGMONTH; + ax.dtick = "M" + roundDTick(roughDTick, 1, roundBase24); + } else if (roughX2 > ONEDAY) { + ax.dtick = roundDTick(roughDTick, ONEDAY, roundDays); + // get week ticks on sunday + // this will also move the base tick off 2000-01-01 if dtick is + // 2 or 3 days... but that's a weird enough case that we'll ignore it. + ax.tick0 = Lib.dateTick0(ax.calendar, true); + } else if (roughX2 > ONEHOUR) { + ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24); + } else if (roughX2 > ONEMIN) { + ax.dtick = roundDTick(roughDTick, ONEMIN, roundBase60); + } else if (roughX2 > ONESEC) { + ax.dtick = roundDTick(roughDTick, ONESEC, roundBase60); + } else { + // milliseconds + base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); + ax.dtick = roundDTick(roughDTick, base, roundBase10); + } + } else if (ax.type === "log") { + ax.tick0 = 0; + var rng = Lib.simpleMap(ax.range, ax.r2l); - // TODO: this is from log axis histograms with autorange off - if(!isNumeric(ax.dtick) && typeof ax.dtick !== 'string') { - var olddtick = ax.dtick; - ax.dtick = 1; - throw 'ax.dtick error: ' + String(olddtick); - } + if (roughDTick > 0.7) { + // only show powers of 10 + ax.dtick = Math.ceil(roughDTick); + } else if (Math.abs(rng[1] - rng[0]) < 1) { + // span is less than one power of 10 + var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick); + + // ticks on a linear scale, labeled fully + roughDTick = Math.abs(Math.pow(10, rng[1]) - Math.pow(10, rng[0])) / nt; + base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); + ax.dtick = "L" + roundDTick(roughDTick, base, roundBase10); + } else { + // include intermediates between powers of 10, + // labeled with small digits + // ax.dtick = "D2" (show 2 and 5) or "D1" (show all digits) + ax.dtick = roughDTick > 0.3 ? "D2" : "D1"; + } + } else if (ax.type === "category") { + ax.tick0 = 0; + ax.dtick = Math.ceil(Math.max(roughDTick, 1)); + } else { + // auto ticks always start at 0 + ax.tick0 = 0; + base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); + ax.dtick = roundDTick(roughDTick, base, roundBase10); + } + + // prevent infinite loops + if (ax.dtick === 0) ax.dtick = 1; + + // TODO: this is from log axis histograms with autorange off + if (!isNumeric(ax.dtick) && typeof ax.dtick !== "string") { + var olddtick = ax.dtick; + ax.dtick = 1; + throw "ax.dtick error: " + String(olddtick); + } }; // after dtick is already known, find tickround = precision @@ -918,61 +911,67 @@ axes.autoTicks = function(ax, roughDTick) { // for date ticks, the last date part to show (y,m,d,H,M,S) // or an integer # digits past seconds function autoTickRound(ax) { - var dtick = ax.dtick; - - ax._tickexponent = 0; - if(!isNumeric(dtick) && typeof dtick !== 'string') { - dtick = 1; - } - - if(ax.type === 'category') { - ax._tickround = null; - } - if(ax.type === 'date') { - // If tick0 is unusual, give tickround a bit more information - // not necessarily *all* the information in tick0 though, if it's really odd - // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19 - // take off a leading minus (year < 0) and i (intercalary month) so length is consistent - var tick0ms = ax.r2l(ax.tick0), - tick0str = ax.l2r(tick0ms).replace(/(^-|i)/g, ''), - tick0len = tick0str.length; - - if(String(dtick).charAt(0) === 'M') { - // any tick0 more specific than a year: alway show the full date - if(tick0len > 10 || tick0str.substr(5) !== '01-01') ax._tickround = 'd'; - // show the month unless ticks are full multiples of a year - else ax._tickround = (+(dtick.substr(1)) % 12 === 0) ? 'y' : 'm'; - } - else if((dtick >= ONEDAY && tick0len <= 10) || (dtick >= ONEDAY * 15)) ax._tickround = 'd'; - else if((dtick >= ONEMIN && tick0len <= 16) || (dtick >= ONEHOUR)) ax._tickround = 'M'; - else if((dtick >= ONESEC && tick0len <= 19) || (dtick >= ONEMIN)) ax._tickround = 'S'; - else { - // tickround is a number of digits of fractional seconds - // of any two adjacent ticks, at least one will have the maximum fractional digits - // of all possible ticks - so take the max. length of tick0 and the next one - var tick1len = ax.l2r(tick0ms + dtick).replace(/^-/, '').length; - ax._tickround = Math.max(tick0len, tick1len) - 20; - } - } - else if(isNumeric(dtick) || dtick.charAt(0) === 'L') { - // linear or log (except D1, D2) - var rng = ax.range.map(ax.r2d || Number); - if(!isNumeric(dtick)) dtick = Number(dtick.substr(1)); - // 2 digits past largest digit of dtick - ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01); - - var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1])); - - var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01); - if(Math.abs(rangeexp) > 3) { - if(ax.exponentformat === 'SI' || ax.exponentformat === 'B') { - ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3); - } - else ax._tickexponent = rangeexp; - } - } + var dtick = ax.dtick; + + ax._tickexponent = 0; + if (!isNumeric(dtick) && typeof dtick !== "string") { + dtick = 1; + } + + if (ax.type === "category") { + ax._tickround = null; + } + if (ax.type === "date") { + // If tick0 is unusual, give tickround a bit more information + // not necessarily *all* the information in tick0 though, if it's really odd + // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19 + // take off a leading minus (year < 0) and i (intercalary month) so length is consistent + var tick0ms = ax.r2l(ax.tick0), + tick0str = ax.l2r(tick0ms).replace(/(^-|i)/g, ""), + tick0len = tick0str.length; + + if (String(dtick).charAt(0) === "M") { + // any tick0 more specific than a year: alway show the full date + if (tick0len > 10 || tick0str.substr(5) !== "01-01") { + ax._tickround = "d"; + } else { + // show the month unless ticks are full multiples of a year + ax._tickround = (+dtick.substr(1)) % 12 === 0 ? "y" : "m"; + } + } else if (dtick >= ONEDAY && tick0len <= 10 || dtick >= ONEDAY * 15) { + ax._tickround = "d"; + } else if (dtick >= ONEMIN && tick0len <= 16 || dtick >= ONEHOUR) { + ax._tickround = "M"; + } else if (dtick >= ONESEC && tick0len <= 19 || dtick >= ONEMIN) { + ax._tickround = "S"; + } else { + // tickround is a number of digits of fractional seconds + // of any two adjacent ticks, at least one will have the maximum fractional digits + // of all possible ticks - so take the max. length of tick0 and the next one + var tick1len = ax.l2r(tick0ms + dtick).replace(/^-/, "").length; + ax._tickround = Math.max(tick0len, tick1len) - 20; + } + } else if (isNumeric(dtick) || dtick.charAt(0) === "L") { + // linear or log (except D1, D2) + var rng = ax.range.map(ax.r2d || Number); + if (!isNumeric(dtick)) dtick = Number(dtick.substr(1)); + // 2 digits past largest digit of dtick + ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01); + + var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1])); + + var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01); + if (Math.abs(rangeexp) > 3) { + if (ax.exponentformat === "SI" || ax.exponentformat === "B") { + ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3); + } else { + ax._tickexponent = rangeexp; + } + } + } else { // D1 or D2 (log) - else ax._tickround = null; + ax._tickround = null; + } } // months and years don't have constant millisecond values @@ -982,98 +981,99 @@ function autoTickRound(ax) { // numeric ticks always have constant differences, other datetime ticks // can all be calculated as constant number of milliseconds axes.tickIncrement = function(x, dtick, axrev, calendar) { - var axSign = axrev ? -1 : 1; - - // includes linear, all dates smaller than month, and pure 10^n in log - if(isNumeric(dtick)) return x + axSign * dtick; - - // everything else is a string, one character plus a number - var tType = dtick.charAt(0), - dtSigned = axSign * Number(dtick.substr(1)); - - // Dates: months (or years - see Lib.incrementMonth) - if(tType === 'M') return Lib.incrementMonth(x, dtSigned, calendar); - - // Log scales: Linear, Digits - else if(tType === 'L') return Math.log(Math.pow(10, x) + dtSigned) / Math.LN10; - + var axSign = axrev ? -1 : 1; + + // includes linear, all dates smaller than month, and pure 10^n in log + if (isNumeric(dtick)) return x + axSign * dtick; + + // everything else is a string, one character plus a number + var tType = dtick.charAt(0), dtSigned = axSign * Number(dtick.substr(1)); + + // Dates: months (or years - see Lib.incrementMonth) + if (tType === "M") { + return Lib.incrementMonth(x, dtSigned, calendar); + } else if ( + tType === "L" // Log scales: Linear, Digits + ) { + return Math.log(Math.pow(10, x) + dtSigned) / Math.LN10; + } else if (tType === "D") { // log10 of 2,5,10, or all digits (logs just have to be // close enough to round) - else if(tType === 'D') { - var tickset = (dtick === 'D2') ? roundLog2 : roundLog1, - x2 = x + axSign * 0.01, - frac = Lib.roundUp(Lib.mod(x2, 1), tickset, axrev); - - return Math.floor(x2) + - Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10; - } - else throw 'unrecognized dtick ' + String(dtick); + var tickset = dtick === "D2" ? roundLog2 : roundLog1, + x2 = x + axSign * 0.01, + frac = Lib.roundUp(Lib.mod(x2, 1), tickset, axrev); + + return Math.floor(x2) + + Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10; + } else { + throw "unrecognized dtick " + String(dtick); + } }; // calculate the first tick on an axis axes.tickFirst = function(ax) { - var r2l = ax.r2l || Number, - rng = Lib.simpleMap(ax.range, r2l), - axrev = rng[1] < rng[0], - sRound = axrev ? Math.floor : Math.ceil, - // add a tiny extra bit to make sure we get ticks - // that may have been rounded out - r0 = rng[0] * 1.0001 - rng[1] * 0.0001, - dtick = ax.dtick, - tick0 = r2l(ax.tick0); - - if(isNumeric(dtick)) { - var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0; - - // make sure no ticks outside the category list - if(ax.type === 'category') { - tmin = Lib.constrain(tmin, 0, ax._categories.length - 1); - } - return tmin; - } - - var tType = dtick.charAt(0), - dtNum = Number(dtick.substr(1)); - - // Dates: months (or years) - if(tType === 'M') { - var cnt = 0, - t0 = tick0, - t1, - mult, - newDTick; - - // This algorithm should work for *any* nonlinear (but close to linear!) - // tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3. - while(cnt < 10) { - t1 = axes.tickIncrement(t0, dtick, axrev, ax.calendar); - if((t1 - r0) * (t0 - r0) <= 0) { - // t1 and t0 are on opposite sides of r0! we've succeeded! - if(axrev) return Math.min(t0, t1); - return Math.max(t0, t1); - } - mult = (r0 - ((t0 + t1) / 2)) / (t1 - t0); - newDTick = tType + ((Math.abs(Math.round(mult)) || 1) * dtNum); - t0 = axes.tickIncrement(t0, newDTick, mult < 0 ? !axrev : axrev, ax.calendar); - cnt++; - } - Lib.error('tickFirst did not converge', ax); - return t0; - } - + var r2l = ax.r2l || Number, + rng = Lib.simpleMap(ax.range, r2l), + axrev = rng[1] < rng[0], + sRound = axrev ? Math.floor : Math.ceil, + // add a tiny extra bit to make sure we get ticks + // that may have been rounded out + r0 = rng[0] * 1.0001 - rng[1] * 0.0001, + dtick = ax.dtick, + tick0 = r2l(ax.tick0); + + if (isNumeric(dtick)) { + var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0; + + // make sure no ticks outside the category list + if (ax.type === "category") { + tmin = Lib.constrain(tmin, 0, ax._categories.length - 1); + } + return tmin; + } + + var tType = dtick.charAt(0), dtNum = Number(dtick.substr(1)); + + // Dates: months (or years) + if (tType === "M") { + var cnt = 0, t0 = tick0, t1, mult, newDTick; + + // This algorithm should work for *any* nonlinear (but close to linear!) + // tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3. + while (cnt < 10) { + t1 = axes.tickIncrement(t0, dtick, axrev, ax.calendar); + if ((t1 - r0) * (t0 - r0) <= 0) { + // t1 and t0 are on opposite sides of r0! we've succeeded! + if (axrev) return Math.min(t0, t1); + return Math.max(t0, t1); + } + mult = (r0 - (t0 + t1) / 2) / (t1 - t0); + newDTick = tType + (Math.abs(Math.round(mult)) || 1) * dtNum; + t0 = axes.tickIncrement( + t0, + newDTick, + mult < 0 ? !axrev : axrev, + ax.calendar + ); + cnt++; + } + Lib.error("tickFirst did not converge", ax); + return t0; + } else if (tType === "L") { // Log scales: Linear, Digits - else if(tType === 'L') { - return Math.log(sRound( - (Math.pow(10, r0) - tick0) / dtNum) * dtNum + tick0) / Math.LN10; - } - else if(tType === 'D') { - var tickset = (dtick === 'D2') ? roundLog2 : roundLog1, - frac = Lib.roundUp(Lib.mod(r0, 1), tickset, axrev); - - return Math.floor(r0) + - Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10; - } - else throw 'unrecognized dtick ' + String(dtick); + return Math.log( + sRound((Math.pow(10, r0) - tick0) / dtNum) * dtNum + tick0 + ) / + Math.LN10; + } else if (tType === "D") { + var tickset = dtick === "D2" ? roundLog2 : roundLog1, + frac = Lib.roundUp(Lib.mod(r0, 1), tickset, axrev); + + return Math.floor(r0) + + Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10; + } else { + throw "unrecognized dtick " + String(dtick); + } }; // draw the text for one tick. @@ -1083,301 +1083,305 @@ axes.tickFirst = function(ax) { // hover is a (truthy) flag for whether to show numbers with a bit // more precision for hovertext axes.tickText = function(ax, x, hover) { - var out = tickTextObj(ax, x), - hideexp, - arrayMode = ax.tickmode === 'array', - extraPrecision = hover || arrayMode, - i, - tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l; - - if(arrayMode && Array.isArray(ax.ticktext)) { - var rng = Lib.simpleMap(ax.range, ax.r2l), - minDiff = Math.abs(rng[1] - rng[0]) / 10000; - for(i = 0; i < ax.ticktext.length; i++) { - if(Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break; - } - if(i < ax.ticktext.length) { - out.text = String(ax.ticktext[i]); - return out; - } - } - - function isHidden(showAttr) { - var first_or_last; - - if(showAttr === undefined) return true; - if(hover) return showAttr === 'none'; - - first_or_last = { - first: ax._tmin, - last: ax._tmax - }[showAttr]; - - return showAttr !== 'all' && x !== first_or_last; - } - - hideexp = ax.exponentformat !== 'none' && isHidden(ax.showexponent) ? 'hide' : ''; - - if(ax.type === 'date') formatDate(ax, out, hover, extraPrecision); - else if(ax.type === 'log') formatLog(ax, out, hover, extraPrecision, hideexp); - else if(ax.type === 'category') formatCategory(ax, out); - else formatLinear(ax, out, hover, extraPrecision, hideexp); - - // add prefix and suffix - if(ax.tickprefix && !isHidden(ax.showtickprefix)) out.text = ax.tickprefix + out.text; - if(ax.ticksuffix && !isHidden(ax.showticksuffix)) out.text += ax.ticksuffix; - - return out; + var out = tickTextObj(ax, x), + hideexp, + arrayMode = ax.tickmode === "array", + extraPrecision = hover || arrayMode, + i, + tickVal2l = ax.type === "category" ? ax.d2l_noadd : ax.d2l; + + if (arrayMode && Array.isArray(ax.ticktext)) { + var rng = Lib.simpleMap(ax.range, ax.r2l), + minDiff = Math.abs(rng[1] - rng[0]) / 10000; + for (i = 0; i < ax.ticktext.length; i++) { + if (Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break; + } + if (i < ax.ticktext.length) { + out.text = String(ax.ticktext[i]); + return out; + } + } + + function isHidden(showAttr) { + var first_or_last; + + if (showAttr === undefined) return true; + if (hover) return showAttr === "none"; + + first_or_last = ({ first: ax._tmin, last: ax._tmax })[showAttr]; + + return showAttr !== "all" && x !== first_or_last; + } + + hideexp = ax.exponentformat !== "none" && isHidden(ax.showexponent) + ? "hide" + : ""; + + if (ax.type === "date") { + formatDate(ax, out, hover, extraPrecision); + } else if (ax.type === "log") { + formatLog(ax, out, hover, extraPrecision, hideexp); + } else if (ax.type === "category") formatCategory(ax, out); + else formatLinear(ax, out, hover, extraPrecision, hideexp); + + // add prefix and suffix + if (ax.tickprefix && !isHidden(ax.showtickprefix)) { + out.text = ax.tickprefix + out.text; + } + if (ax.ticksuffix && !isHidden(ax.showticksuffix)) out.text += ax.ticksuffix; + + return out; }; function tickTextObj(ax, x, text) { - var tf = ax.tickfont || ax._gd._fullLayout.font; - - return { - x: x, - dx: 0, - dy: 0, - text: text || '', - fontSize: tf.size, - font: tf.family, - fontColor: tf.color - }; + var tf = ax.tickfont || ax._gd._fullLayout.font; + + return { + x: x, + dx: 0, + dy: 0, + text: text || "", + fontSize: tf.size, + font: tf.family, + fontColor: tf.color + }; } function formatDate(ax, out, hover, extraPrecision) { - var tr = ax._tickround, - fmt = (hover && ax.hoverformat) || ax.tickformat; - - if(extraPrecision) { - // second or sub-second precision: extra always shows max digits. - // for other fields, extra precision just adds one field. - if(isNumeric(tr)) tr = 4; - else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr]; - } - - var dateStr = Lib.formatDate(out.x, fmt, tr, ax.calendar), - headStr; - - var splitIndex = dateStr.indexOf('\n'); - if(splitIndex !== -1) { - headStr = dateStr.substr(splitIndex + 1); - dateStr = dateStr.substr(0, splitIndex); - } - - if(extraPrecision) { - // if extraPrecision led to trailing zeros, strip them off - // actually, this can lead to removing even more zeros than - // in the original rounding, but that's fine because in these - // contexts uniformity is not so important (if there's even - // anything to be uniform with!) - - // can we remove the whole time part? - if(dateStr === '00:00:00' || dateStr === '00:00') { - dateStr = headStr; - headStr = ''; - } - else if(dateStr.length === 8) { - // strip off seconds if they're zero (zero fractional seconds - // are already omitted) - // but we never remove minutes and leave just hours - dateStr = dateStr.replace(/:00$/, ''); - } - } - - if(headStr) { - if(hover) { - // hover puts it all on one line, so headPart works best up front - // except for year headPart: turn this into "Jan 1, 2000" etc. - if(tr === 'd') dateStr += ', ' + headStr; - else dateStr = headStr + (dateStr ? ', ' + dateStr : ''); - } - else if(!ax._inCalcTicks || (headStr !== ax._prevDateHead)) { - dateStr += '
' + headStr; - ax._prevDateHead = headStr; - } - } - - out.text = dateStr; + var tr = ax._tickround, fmt = hover && ax.hoverformat || ax.tickformat; + + if (extraPrecision) { + // second or sub-second precision: extra always shows max digits. + // for other fields, extra precision just adds one field. + if (isNumeric(tr)) tr = 4; + else tr = ({ y: "m", m: "d", d: "M", M: "S", S: 4 })[tr]; + } + + var dateStr = Lib.formatDate(out.x, fmt, tr, ax.calendar), headStr; + + var splitIndex = dateStr.indexOf("\n"); + if (splitIndex !== -1) { + headStr = dateStr.substr(splitIndex + 1); + dateStr = dateStr.substr(0, splitIndex); + } + + if (extraPrecision) { + // if extraPrecision led to trailing zeros, strip them off + // actually, this can lead to removing even more zeros than + // in the original rounding, but that's fine because in these + // contexts uniformity is not so important (if there's even + // anything to be uniform with!) + // can we remove the whole time part? + if (dateStr === "00:00:00" || dateStr === "00:00") { + dateStr = headStr; + headStr = ""; + } else if (dateStr.length === 8) { + // strip off seconds if they're zero (zero fractional seconds + // are already omitted) + // but we never remove minutes and leave just hours + dateStr = dateStr.replace(/:00$/, ""); + } + } + + if (headStr) { + if (hover) { + // hover puts it all on one line, so headPart works best up front + // except for year headPart: turn this into "Jan 1, 2000" etc. + if (tr === "d") dateStr += ", " + headStr; + else dateStr = headStr + (dateStr ? ", " + dateStr : ""); + } else if (!ax._inCalcTicks || headStr !== ax._prevDateHead) { + dateStr += "
" + headStr; + ax._prevDateHead = headStr; + } + } + + out.text = dateStr; } function formatLog(ax, out, hover, extraPrecision, hideexp) { - var dtick = ax.dtick, - x = out.x; - if(extraPrecision && ((typeof dtick !== 'string') || dtick.charAt(0) !== 'L')) dtick = 'L3'; - - if(ax.tickformat || (typeof dtick === 'string' && dtick.charAt(0) === 'L')) { - out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision); - } - else if(isNumeric(dtick) || ((dtick.charAt(0) === 'D') && (Lib.mod(x + 0.01, 1) < 0.1))) { - if(['e', 'E', 'power'].indexOf(ax.exponentformat) !== -1) { - var p = Math.round(x); - if(p === 0) out.text = 1; - else if(p === 1) out.text = '10'; - else if(p > 1) out.text = '10' + p + ''; - else out.text = '10\u2212' + -p + ''; - - out.fontSize *= 1.25; - } - else { - out.text = numFormat(Math.pow(10, x), ax, '', 'fakehover'); - if(dtick === 'D1' && ax._id.charAt(0) === 'y') { - out.dy -= out.fontSize / 6; - } - } - } - else if(dtick.charAt(0) === 'D') { - out.text = String(Math.round(Math.pow(10, Lib.mod(x, 1)))); - out.fontSize *= 0.75; - } - else throw 'unrecognized dtick ' + String(dtick); - - // if 9's are printed on log scale, move the 10's away a bit - if(ax.dtick === 'D1') { - var firstChar = String(out.text).charAt(0); - if(firstChar === '0' || firstChar === '1') { - if(ax._id.charAt(0) === 'y') { - out.dx -= out.fontSize / 4; - } - else { - out.dy += out.fontSize / 2; - out.dx += (ax.range[1] > ax.range[0] ? 1 : -1) * - out.fontSize * (x < 0 ? 0.5 : 0.25); - } - } - } + var dtick = ax.dtick, x = out.x; + if ( + extraPrecision && (typeof dtick !== "string" || dtick.charAt(0) !== "L") + ) { + dtick = "L3"; + } + + if (ax.tickformat || typeof dtick === "string" && dtick.charAt(0) === "L") { + out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision); + } else if ( + isNumeric(dtick) || dtick.charAt(0) === "D" && Lib.mod(x + 0.01, 1) < 0.1 + ) { + if (["e", "E", "power"].indexOf(ax.exponentformat) !== -1) { + var p = Math.round(x); + if (p === 0) out.text = 1; + else if (p === 1) out.text = "10"; + else if (p > 1) out.text = "10" + p + ""; + else out.text = "10\u2212" + (-p) + ""; + + out.fontSize *= 1.25; + } else { + out.text = numFormat(Math.pow(10, x), ax, "", "fakehover"); + if (dtick === "D1" && ax._id.charAt(0) === "y") { + out.dy -= out.fontSize / 6; + } + } + } else if (dtick.charAt(0) === "D") { + out.text = String(Math.round(Math.pow(10, Lib.mod(x, 1)))); + out.fontSize *= 0.75; + } else { + throw "unrecognized dtick " + String(dtick); + } + + // if 9's are printed on log scale, move the 10's away a bit + if (ax.dtick === "D1") { + var firstChar = String(out.text).charAt(0); + if (firstChar === "0" || firstChar === "1") { + if (ax._id.charAt(0) === "y") { + out.dx -= out.fontSize / 4; + } else { + out.dy += out.fontSize / 2; + out.dx += (ax.range[1] > ax.range[0] ? 1 : -1) * + out.fontSize * + (x < 0 ? 0.5 : 0.25); + } + } + } } function formatCategory(ax, out) { - var tt = ax._categories[Math.round(out.x)]; - if(tt === undefined) tt = ''; - out.text = String(tt); + var tt = ax._categories[Math.round(out.x)]; + if (tt === undefined) tt = ""; + out.text = String(tt); } function formatLinear(ax, out, hover, extraPrecision, hideexp) { - // don't add an exponent to zero if we're showing all exponents - // so the only reason you'd show an exponent on zero is if it's the - // ONLY tick to get an exponent (first or last) - if(ax.showexponent === 'all' && Math.abs(out.x / ax.dtick) < 1e-6) { - hideexp = 'hide'; - } - out.text = numFormat(out.x, ax, hideexp, extraPrecision); + // don't add an exponent to zero if we're showing all exponents + // so the only reason you'd show an exponent on zero is if it's the + // ONLY tick to get an exponent (first or last) + if (ax.showexponent === "all" && Math.abs(out.x / ax.dtick) < 1e-6) { + hideexp = "hide"; + } + out.text = numFormat(out.x, ax, hideexp, extraPrecision); } // format a number (tick value) according to the axis settings // new, more reliable procedure than d3.round or similar: // add half the rounding increment, then stringify and truncate // also automatically switch to sci. notation -var SIPREFIXES = ['f', 'p', 'n', 'μ', 'm', '', 'k', 'M', 'G', 'T']; +var SIPREFIXES = ["f", "p", "n", "\u03BC", "m", "", "k", "M", "G", "T"]; function numFormat(v, ax, fmtoverride, hover) { - // negative? - var isNeg = v < 0, - // max number of digits past decimal point to show - tickRound = ax._tickround, - exponentFormat = fmtoverride || ax.exponentformat || 'B', - exponent = ax._tickexponent, - tickformat = ax.tickformat, - separatethousands = ax.separatethousands; - - // special case for hover: set exponent just for this value, and - // add a couple more digits of precision over tick labels - if(hover) { - // make a dummy axis obj to get the auto rounding and exponent - var ah = { - exponentformat: ax.exponentformat, - dtick: ax.showexponent === 'none' ? ax.dtick : - (isNumeric(v) ? Math.abs(v) || 1 : 1), - // if not showing any exponents, don't change the exponent - // from what we calculate - range: ax.showexponent === 'none' ? ax.range.map(ax.r2d) : [0, v || 1] - }; - autoTickRound(ah); - tickRound = (Number(ah._tickround) || 0) + 4; - exponent = ah._tickexponent; - if(ax.hoverformat) tickformat = ax.hoverformat; - } - - if(tickformat) return d3.format(tickformat)(v).replace(/-/g, '\u2212'); - - // 'epsilon' - rounding increment - var e = Math.pow(10, -tickRound) / 2; - - // exponentFormat codes: - // 'e' (1.2e+6, default) - // 'E' (1.2E+6) - // 'SI' (1.2M) - // 'B' (same as SI except 10^9=B not G) - // 'none' (1200000) - // 'power' (1.2x10^6) - // 'hide' (1.2, use 3rd argument=='hide' to eg - // only show exponent on last tick) - if(exponentFormat === 'none') exponent = 0; - - // take the sign out, put it back manually at the end - // - makes cases easier - v = Math.abs(v); - if(v < e) { - // 0 is just 0, but may get exponent if it's the last tick - v = '0'; - isNeg = false; - } - else { - v += e; - // take out a common exponent, if any - if(exponent) { - v *= Math.pow(10, -exponent); - tickRound += exponent; - } - // round the mantissa - if(tickRound === 0) v = String(Math.floor(v)); - else if(tickRound < 0) { - v = String(Math.round(v)); - v = v.substr(0, v.length + tickRound); - for(var i = tickRound; i < 0; i++) v += '0'; - } - else { - v = String(v); - var dp = v.indexOf('.') + 1; - if(dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, ''); - } - // insert appropriate decimal point and thousands separator - v = Lib.numSeparate(v, ax._gd._fullLayout.separators, separatethousands); - } - - // add exponent - if(exponent && exponentFormat !== 'hide') { - var signedExponent; - if(exponent < 0) signedExponent = '\u2212' + -exponent; - else if(exponentFormat !== 'power') signedExponent = '+' + exponent; - else signedExponent = String(exponent); - - if(exponentFormat === 'e' || - ((exponentFormat === 'SI' || exponentFormat === 'B') && - (exponent > 12 || exponent < -15))) { - v += 'e' + signedExponent; - } - else if(exponentFormat === 'E') { - v += 'E' + signedExponent; - } - else if(exponentFormat === 'power') { - v += '×10' + signedExponent + ''; - } - else if(exponentFormat === 'B' && exponent === 9) { - v += 'B'; - } - else if(exponentFormat === 'SI' || exponentFormat === 'B') { - v += SIPREFIXES[exponent / 3 + 5]; - } - } - - // put sign back in and return - // replace standard minus character (which is technically a hyphen) - // with a true minus sign - if(isNeg) return '\u2212' + v; - return v; + // negative? + var isNeg = v < 0, + // max number of digits past decimal point to show + tickRound = ax._tickround, + exponentFormat = fmtoverride || ax.exponentformat || "B", + exponent = ax._tickexponent, + tickformat = ax.tickformat, + separatethousands = ax.separatethousands; + + // special case for hover: set exponent just for this value, and + // add a couple more digits of precision over tick labels + if (hover) { + // make a dummy axis obj to get the auto rounding and exponent + var ah = { + exponentformat: ax.exponentformat, + dtick: ( + ax.showexponent === "none" + ? ax.dtick + : isNumeric(v) ? Math.abs(v) || 1 : 1 + ), + // if not showing any exponents, don't change the exponent + // from what we calculate + range: ( + ax.showexponent === "none" ? ax.range.map(ax.r2d) : [0, v || 1] + ) + }; + autoTickRound(ah); + tickRound = (Number(ah._tickround) || 0) + 4; + exponent = ah._tickexponent; + if (ax.hoverformat) tickformat = ax.hoverformat; + } + + if (tickformat) return d3.format(tickformat)(v).replace(/-/g, "\u2212"); + + // 'epsilon' - rounding increment + var e = Math.pow(10, -tickRound) / 2; + + // exponentFormat codes: + // 'e' (1.2e+6, default) + // 'E' (1.2E+6) + // 'SI' (1.2M) + // 'B' (same as SI except 10^9=B not G) + // 'none' (1200000) + // 'power' (1.2x10^6) + // 'hide' (1.2, use 3rd argument=='hide' to eg + // only show exponent on last tick) + if (exponentFormat === "none") exponent = 0; + + // take the sign out, put it back manually at the end + // - makes cases easier + v = Math.abs(v); + if (v < e) { + // 0 is just 0, but may get exponent if it's the last tick + v = "0"; + isNeg = false; + } else { + v += e; + // take out a common exponent, if any + if (exponent) { + v *= Math.pow(10, -exponent); + tickRound += exponent; + } + // round the mantissa + if (tickRound === 0) { + v = String(Math.floor(v)); + } else if (tickRound < 0) { + v = String(Math.round(v)); + v = v.substr(0, v.length + tickRound); + for (var i = tickRound; i < 0; i++) { + v += "0"; + } + } else { + v = String(v); + var dp = v.indexOf(".") + 1; + if (dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, ""); + } + // insert appropriate decimal point and thousands separator + v = Lib.numSeparate(v, ax._gd._fullLayout.separators, separatethousands); + } + + // add exponent + if (exponent && exponentFormat !== "hide") { + var signedExponent; + if (exponent < 0) signedExponent = "\u2212" + (-exponent); + else if (exponentFormat !== "power") signedExponent = "+" + exponent; + else signedExponent = String(exponent); + + if ( + exponentFormat === "e" || + (exponentFormat === "SI" || exponentFormat === "B") && + (exponent > 12 || exponent < -15) + ) { + v += "e" + signedExponent; + } else if (exponentFormat === "E") { + v += "E" + signedExponent; + } else if (exponentFormat === "power") { + v += "\xD710" + signedExponent + ""; + } else if (exponentFormat === "B" && exponent === 9) { + v += "B"; + } else if (exponentFormat === "SI" || exponentFormat === "B") { + v += SIPREFIXES[exponent / 3 + 5]; + } + } + + // put sign back in and return + // replace standard minus character (which is technically a hyphen) + // with a true minus sign + if (isNeg) return "\u2212" + v; + return v; } - axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/; // getSubplots - extract all combinations of axes we need to make plots for @@ -1387,153 +1391,155 @@ axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/; // looks both for combinations of x and y found in the data // and at axes and their anchors axes.getSubplots = function(gd, ax) { - var subplots = []; - var i, j, sp; + var subplots = []; + var i, j, sp; - // look for subplots in the data - var data = gd._fullData || gd.data || []; + // look for subplots in the data + var data = gd._fullData || gd.data || []; - for(i = 0; i < data.length; i++) { - var trace = data[i]; + for (i = 0; i < data.length; i++) { + var trace = data[i]; - if(trace.visible === false || trace.visible === 'legendonly' || - !(Registry.traceIs(trace, 'cartesian') || Registry.traceIs(trace, 'gl2d')) - ) continue; + if ( + trace.visible === false || + trace.visible === "legendonly" || + !(Registry.traceIs(trace, "cartesian") || + Registry.traceIs(trace, "gl2d")) + ) { + continue; + } - var xId = trace.xaxis || 'x', - yId = trace.yaxis || 'y'; - sp = xId + yId; + var xId = trace.xaxis || "x", yId = trace.yaxis || "y"; + sp = xId + yId; - if(subplots.indexOf(sp) === -1) subplots.push(sp); - } + if (subplots.indexOf(sp) === -1) subplots.push(sp); + } - // look for subplots in the axes/anchors, so that we at least draw all axes - var axesList = axes.list(gd, '', true); + // look for subplots in the axes/anchors, so that we at least draw all axes + var axesList = axes.list(gd, "", true); - function hasAx2(sp, ax2) { - return sp.indexOf(ax2._id) !== -1; - } + function hasAx2(sp, ax2) { + return sp.indexOf(ax2._id) !== -1; + } - for(i = 0; i < axesList.length; i++) { - var ax2 = axesList[i], - ax2Letter = ax2._id.charAt(0), - ax3Id = (ax2.anchor === 'free') ? - ((ax2Letter === 'x') ? 'y' : 'x') : - ax2.anchor, - ax3 = axes.getFromId(gd, ax3Id); + for (i = 0; i < axesList.length; i++) { + var ax2 = axesList[i], + ax2Letter = ax2._id.charAt(0), + ax3Id = ax2.anchor === "free" + ? ax2Letter === "x" ? "y" : "x" + : ax2.anchor, + ax3 = axes.getFromId(gd, ax3Id); - // look if ax2 is already represented in the data - var foundAx2 = false; - for(j = 0; j < subplots.length; j++) { - if(hasAx2(subplots[j], ax2)) { - foundAx2 = true; - break; - } - } + // look if ax2 is already represented in the data + var foundAx2 = false; + for (j = 0; j < subplots.length; j++) { + if (hasAx2(subplots[j], ax2)) { + foundAx2 = true; + break; + } + } - // ignore free axes that already represented in the data - if(ax2.anchor === 'free' && foundAx2) continue; + // ignore free axes that already represented in the data + if (ax2.anchor === "free" && foundAx2) continue; - // ignore anchor-less axes - if(!ax3) continue; + // ignore anchor-less axes + if (!ax3) continue; - sp = (ax2Letter === 'x') ? - ax2._id + ax3._id : - ax3._id + ax2._id; + sp = ax2Letter === "x" ? ax2._id + ax3._id : ax3._id + ax2._id; - if(subplots.indexOf(sp) === -1) subplots.push(sp); - } + if (subplots.indexOf(sp) === -1) subplots.push(sp); + } - // filter invalid subplots - var spMatch = axes.subplotMatch, - allSubplots = []; + // filter invalid subplots + var spMatch = axes.subplotMatch, allSubplots = []; - for(i = 0; i < subplots.length; i++) { - sp = subplots[i]; - if(spMatch.test(sp)) allSubplots.push(sp); - } + for (i = 0; i < subplots.length; i++) { + sp = subplots[i]; + if (spMatch.test(sp)) allSubplots.push(sp); + } - // sort the subplot ids - allSubplots.sort(function(a, b) { - var aMatch = a.match(spMatch), - bMatch = b.match(spMatch); + // sort the subplot ids + allSubplots.sort(function(a, b) { + var aMatch = a.match(spMatch), bMatch = b.match(spMatch); - if(aMatch[1] === bMatch[1]) { - return +(aMatch[2] || 1) - (bMatch[2] || 1); - } + if (aMatch[1] === bMatch[1]) { + return +(aMatch[2] || 1) - (bMatch[2] || 1); + } - return +(aMatch[1]||0) - (bMatch[1]||0); - }); + return +(aMatch[1] || 0) - (bMatch[1] || 0); + }); - if(ax) return axes.findSubplotsWithAxis(allSubplots, ax); - return allSubplots; + if (ax) return axes.findSubplotsWithAxis(allSubplots, ax); + return allSubplots; }; // find all subplots with axis 'ax' axes.findSubplotsWithAxis = function(subplots, ax) { - var axMatch = new RegExp( - (ax._id.charAt(0) === 'x') ? ('^' + ax._id + 'y') : (ax._id + '$') - ); - var subplotsWithAxis = []; + var axMatch = new RegExp( + (ax._id.charAt(0) === "x" ? "^" + ax._id + "y" : ax._id + "$") + ); + var subplotsWithAxis = []; - for(var i = 0; i < subplots.length; i++) { - var sp = subplots[i]; - if(axMatch.test(sp)) subplotsWithAxis.push(sp); - } + for (var i = 0; i < subplots.length; i++) { + var sp = subplots[i]; + if (axMatch.test(sp)) subplotsWithAxis.push(sp); + } - return subplotsWithAxis; + return subplotsWithAxis; }; // makeClipPaths: prepare clipPaths for all single axes and all possible xy pairings axes.makeClipPaths = function(gd) { - var fullLayout = gd._fullLayout, - defs = fullLayout._defs, - fullWidth = {_offset: 0, _length: fullLayout.width, _id: ''}, - fullHeight = {_offset: 0, _length: fullLayout.height, _id: ''}, - xaList = axes.list(gd, 'x', true), - yaList = axes.list(gd, 'y', true), - clipList = [], - i, - j; - - for(i = 0; i < xaList.length; i++) { - clipList.push({x: xaList[i], y: fullHeight}); - for(j = 0; j < yaList.length; j++) { - if(i === 0) clipList.push({x: fullWidth, y: yaList[j]}); - clipList.push({x: xaList[i], y: yaList[j]}); - } - } - - var defGroup = defs.selectAll('g.clips') - .data([0]); - - defGroup.enter().append('g') - .classed('clips', true); - - // selectors don't work right with camelCase tags, - // have to use class instead - // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I - var axClips = defGroup.selectAll('.axesclip') - .data(clipList, function(d) { return d.x._id + d.y._id; }); - - axClips.enter().append('clipPath') - .classed('axesclip', true) - .attr('id', function(d) { return 'clip' + fullLayout._uid + d.x._id + d.y._id; }) - .append('rect'); - - axClips.exit().remove(); - - axClips.each(function(d) { - d3.select(this).select('rect').attr({ - x: d.x._offset || 0, - y: d.y._offset || 0, - width: d.x._length || 1, - height: d.y._length || 1 - }); + var fullLayout = gd._fullLayout, + defs = fullLayout._defs, + fullWidth = { _offset: 0, _length: fullLayout.width, _id: "" }, + fullHeight = { _offset: 0, _length: fullLayout.height, _id: "" }, + xaList = axes.list(gd, "x", true), + yaList = axes.list(gd, "y", true), + clipList = [], + i, + j; + + for (i = 0; i < xaList.length; i++) { + clipList.push({ x: xaList[i], y: fullHeight }); + for (j = 0; j < yaList.length; j++) { + if (i === 0) clipList.push({ x: fullWidth, y: yaList[j] }); + clipList.push({ x: xaList[i], y: yaList[j] }); + } + } + + var defGroup = defs.selectAll("g.clips").data([0]); + + defGroup.enter().append("g").classed("clips", true); + + // selectors don't work right with camelCase tags, + // have to use class instead + // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I + var axClips = defGroup.selectAll(".axesclip").data(clipList, function(d) { + return d.x._id + d.y._id; + }); + + axClips + .enter() + .append("clipPath") + .classed("axesclip", true) + .attr("id", function(d) { + return "clip" + fullLayout._uid + d.x._id + d.y._id; + }) + .append("rect"); + + axClips.exit().remove(); + + axClips.each(function(d) { + d3.select(this).select("rect").attr({ + x: d.x._offset || 0, + y: d.y._offset || 0, + width: d.x._length || 1, + height: d.y._length || 1 }); + }); }; - // doTicks: draw ticks, grids, and tick labels // axid: 'x', 'y', 'x2' etc, // blank to do all, @@ -1542,663 +1548,737 @@ 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; - - // allow passing an independent axis object instead of id - if(typeof axid === 'object') { - ax = axid; - axid = ax._id; - independent = true; - } - else { - ax = axes.getFromId(gd, axid); - - 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(); - }); - } - - if(!axid || axid === 'redraw') { - return Lib.syncOrAsync(axes.list(gd, '', true).map(function(ax) { - return function() { - if(!ax._id) return; - var axDone = axes.doTicks(gd, ax._id); - if(axid === 'redraw') { - ax._r = ax.range.slice(); - ax._rl = Lib.simpleMap(ax._r, ax.r2l); - } - return axDone; - }; - })); - } - } - - // make sure we only have allowed options for exponents - // (others can make confusing errors) - if(!ax.tickformat) { - if(['none', 'e', 'E', 'power', 'SI', 'B'].indexOf(ax.exponentformat) === -1) { - ax.exponentformat = 'e'; - } - if(['all', 'first', 'last', 'none'].indexOf(ax.showexponent) === -1) { - ax.showexponent = 'all'; - } - } - - // set scaling to pixels - ax.setScale(); - - var axletter = axid.charAt(0), - counterLetter = axes.counterLetter(axid), - vals = axes.calcTicks(ax), - datafn = function(d) { return [d.text, d.x, ax.mirror].join('_'); }, - tcls = axid + 'tick', - gcls = axid + 'grid', - zcls = axid + 'zl', - pad = (ax.linewidth || 1) / 2, - labelStandoff = - (ax.ticks === 'outside' ? ax.ticklen : 1) + (ax.linewidth || 0), - labelShift = 0, - gridWidth = Drawing.crispRound(gd, ax.gridwidth, 1), - zeroLineWidth = Drawing.crispRound(gd, ax.zerolinewidth, gridWidth), - tickWidth = Drawing.crispRound(gd, ax.tickwidth, 1), - sides, transfn, tickpathfn, - i; - - if(ax._counterangle && ax.ticks === 'outside') { - var caRad = ax._counterangle * Math.PI / 180; - labelStandoff = ax.ticklen * Math.cos(caRad) + (ax.linewidth || 0); - labelShift = ax.ticklen * Math.sin(caRad); - } - - // positioning arguments for x vs y axes - if(axletter === 'x') { - sides = ['bottom', 'top']; - transfn = function(d) { - return 'translate(' + ax.l2p(d.x) + ',0)'; - }; - tickpathfn = function(shift, len) { - if(ax._counterangle) { - var caRad = ax._counterangle * Math.PI / 180; - return 'M0,' + shift + 'l' + (Math.sin(caRad) * len) + ',' + (Math.cos(caRad) * len); - } - else return 'M0,' + shift + 'v' + len; - }; - } - else if(axletter === 'y') { - sides = ['left', 'right']; - transfn = function(d) { - return 'translate(0,' + ax.l2p(d.x) + ')'; - }; - tickpathfn = function(shift, len) { - if(ax._counterangle) { - var caRad = ax._counterangle * Math.PI / 180; - return 'M' + shift + ',0l' + (Math.cos(caRad) * len) + ',' + (-Math.sin(caRad) * len); + var fullLayout = gd._fullLayout, ax, independent = false; + + // allow passing an independent axis object instead of id + if (typeof axid === "object") { + ax = axid; + axid = ax._id; + independent = true; + } else { + ax = axes.getFromId(gd, axid); + + 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(); + }); + } + + if (!axid || axid === "redraw") { + return Lib.syncOrAsync( + axes.list(gd, "", true).map(function(ax) { + return function() { + if (!ax._id) return; + var axDone = axes.doTicks(gd, ax._id); + if (axid === "redraw") { + ax._r = ax.range.slice(); + ax._rl = Lib.simpleMap(ax._r, ax.r2l); } - else return 'M' + shift + ',0h' + len; - }; - } - else { - Lib.warn('Unrecognized doTicks axis:', axid); - return; - } - var axside = ax.side || sides[0], + return axDone; + }; + }) + ); + } + } + + // make sure we only have allowed options for exponents + // (others can make confusing errors) + if (!ax.tickformat) { + if ( + ["none", "e", "E", "power", "SI", "B"].indexOf(ax.exponentformat) === -1 + ) { + ax.exponentformat = "e"; + } + if (["all", "first", "last", "none"].indexOf(ax.showexponent) === -1) { + ax.showexponent = "all"; + } + } + + // set scaling to pixels + ax.setScale(); + + var axletter = axid.charAt(0), + counterLetter = axes.counterLetter(axid), + vals = axes.calcTicks(ax), + datafn = function(d) { + return [d.text, d.x, ax.mirror].join("_"); + }, + tcls = axid + "tick", + gcls = axid + "grid", + zcls = axid + "zl", + pad = (ax.linewidth || 1) / 2, + labelStandoff = (ax.ticks === "outside" ? ax.ticklen : 1) + + (ax.linewidth || 0), + labelShift = 0, + gridWidth = Drawing.crispRound(gd, ax.gridwidth, 1), + zeroLineWidth = Drawing.crispRound(gd, ax.zerolinewidth, gridWidth), + tickWidth = Drawing.crispRound(gd, ax.tickwidth, 1), + sides, + transfn, + tickpathfn, + i; + + if (ax._counterangle && ax.ticks === "outside") { + var caRad = ax._counterangle * Math.PI / 180; + labelStandoff = ax.ticklen * Math.cos(caRad) + (ax.linewidth || 0); + labelShift = ax.ticklen * Math.sin(caRad); + } + + // positioning arguments for x vs y axes + if (axletter === "x") { + sides = ["bottom", "top"]; + transfn = function(d) { + return "translate(" + ax.l2p(d.x) + ",0)"; + }; + tickpathfn = function(shift, len) { + if (ax._counterangle) { + var caRad = ax._counterangle * Math.PI / 180; + return "M0," + + shift + + "l" + + Math.sin(caRad) * len + + "," + + Math.cos(caRad) * len; + } else { + return "M0," + shift + "v" + len; + } + }; + } else if (axletter === "y") { + sides = ["left", "right"]; + transfn = function(d) { + return "translate(0," + ax.l2p(d.x) + ")"; + }; + tickpathfn = function(shift, len) { + if (ax._counterangle) { + var caRad = ax._counterangle * Math.PI / 180; + return "M" + + shift + + ",0l" + + Math.cos(caRad) * len + + "," + + (-Math.sin(caRad)) * len; + } else { + return "M" + shift + ",0h" + len; + } + }; + } else { + Lib.warn("Unrecognized doTicks axis:", axid); + return; + } + var axside = ax.side || sides[0], // which direction do the side[0], side[1], and free ticks go? // then we flip if outside XOR y axis - ticksign = [-1, 1, axside === sides[1] ? 1 : -1]; - if((ax.ticks !== 'inside') === (axletter === 'x')) { - ticksign = ticksign.map(function(v) { return -v; }); - } - - // remove zero lines, grid lines, and inside ticks if they're within - // 1 pixel of the end - // The key case here is removing zero lines when the axis bound is zero. - function clipEnds(d) { - var p = ax.l2p(d.x); - return (p > 1 && p < ax._length - 1); - } - var valsClipped = vals.filter(clipEnds); - - function drawTicks(container, tickpath) { - var ticks = container.selectAll('path.' + tcls) - .data(ax.ticks === 'inside' ? valsClipped : vals, datafn); - if(tickpath && ax.ticks) { - ticks.enter().append('path').classed(tcls, 1).classed('ticks', 1) - .classed('crisp', 1) - .call(Color.stroke, ax.tickcolor) - .style('stroke-width', tickWidth + 'px') - .attr('d', tickpath); - ticks.attr('transform', transfn); - ticks.exit().remove(); - } - else ticks.remove(); - } - - function drawLabels(container, position) { - // tick labels - for now just the main labels. - // TODO: mirror labels, esp for subplots - var tickLabels = container.selectAll('g.' + tcls).data(vals, datafn); - if(!ax.showticklabels || !isNumeric(position)) { - tickLabels.remove(); - drawAxTitle(axid); - return; - } - - var labelx, labely, labelanchor, labelpos0, flipit; - if(axletter === 'x') { - flipit = (axside === 'bottom') ? 1 : -1; - labelx = function(d) { return d.dx + labelShift * flipit; }; - labelpos0 = position + (labelStandoff + pad) * flipit; - labely = function(d) { - return d.dy + labelpos0 + d.fontSize * - ((axside === 'bottom') ? 1 : -0.5); - }; - labelanchor = function(angle) { - if(!isNumeric(angle) || angle === 0 || angle === 180) { - return 'middle'; - } - return (angle * flipit < 0) ? 'end' : 'start'; - }; - } - else { - flipit = (axside === 'right') ? 1 : -1; - labely = function(d) { return d.dy + d.fontSize / 2 - labelShift * flipit; }; - labelx = function(d) { - return d.dx + position + (labelStandoff + pad + - ((Math.abs(ax.tickangle) === 90) ? d.fontSize / 2 : 0)) * flipit; - }; - labelanchor = function(angle) { - if(isNumeric(angle) && Math.abs(angle) === 90) { - return 'middle'; - } - return axside === 'right' ? 'start' : 'end'; - }; - } - var maxFontSize = 0, - autoangle = 0, - labelsReady = []; - tickLabels.enter().append('g').classed(tcls, 1) - .append('text') - // only so tex has predictable alignment that we can - // alter later - .attr('text-anchor', 'middle') - .each(function(d) { - var thisLabel = d3.select(this), - newPromise = gd._promises.length; - thisLabel - .call(Drawing.setPosition, labelx(d), labely(d)) - .call(Drawing.font, d.font, d.fontSize, d.fontColor) - .text(d.text) - .call(svgTextUtils.convertToTspans); - newPromise = gd._promises[newPromise]; - if(newPromise) { - // if we have an async label, we'll deal with that - // all here so take it out of gd._promises and - // instead position the label and promise this in - // labelsReady - labelsReady.push(gd._promises.pop().then(function() { - positionLabels(thisLabel, ax.tickangle); - })); - } - else { - // sync label: just position it now. - positionLabels(thisLabel, ax.tickangle); - } - }); - tickLabels.exit().remove(); + ticksign = [-1, 1, axside === sides[1] ? 1 : -1]; + if (ax.ticks !== "inside" === (axletter === "x")) { + ticksign = ticksign.map(function(v) { + return -v; + }); + } + + // remove zero lines, grid lines, and inside ticks if they're within + // 1 pixel of the end + // The key case here is removing zero lines when the axis bound is zero. + function clipEnds(d) { + var p = ax.l2p(d.x); + return p > 1 && p < ax._length - 1; + } + var valsClipped = vals.filter(clipEnds); + + function drawTicks(container, tickpath) { + var ticks = container + .selectAll("path." + tcls) + .data(ax.ticks === "inside" ? valsClipped : vals, datafn); + if (tickpath && ax.ticks) { + ticks + .enter() + .append("path") + .classed(tcls, 1) + .classed("ticks", 1) + .classed("crisp", 1) + .call(Color.stroke, ax.tickcolor) + .style("stroke-width", tickWidth + "px") + .attr("d", tickpath); + ticks.attr("transform", transfn); + ticks.exit().remove(); + } else { + ticks.remove(); + } + } + + function drawLabels(container, position) { + // tick labels - for now just the main labels. + // TODO: mirror labels, esp for subplots + var tickLabels = container.selectAll("g." + tcls).data(vals, datafn); + if (!ax.showticklabels || !isNumeric(position)) { + tickLabels.remove(); + drawAxTitle(axid); + return; + } + + var labelx, labely, labelanchor, labelpos0, flipit; + if (axletter === "x") { + flipit = axside === "bottom" ? 1 : -1; + labelx = function(d) { + return d.dx + labelShift * flipit; + }; + labelpos0 = position + (labelStandoff + pad) * flipit; + labely = function(d) { + return d.dy + labelpos0 + d.fontSize * (axside === "bottom" ? 1 : -0.5); + }; + labelanchor = function(angle) { + if (!isNumeric(angle) || angle === 0 || angle === 180) { + return "middle"; + } + return angle * flipit < 0 ? "end" : "start"; + }; + } else { + flipit = axside === "right" ? 1 : -1; + labely = function(d) { + return d.dy + d.fontSize / 2 - labelShift * flipit; + }; + labelx = function(d) { + return d.dx + + position + + (labelStandoff + + pad + + (Math.abs(ax.tickangle) === 90 ? d.fontSize / 2 : 0)) * + flipit; + }; + labelanchor = function(angle) { + if (isNumeric(angle) && Math.abs(angle) === 90) { + return "middle"; + } + return axside === "right" ? "start" : "end"; + }; + } + var maxFontSize = 0, autoangle = 0, labelsReady = []; + tickLabels + .enter() + .append("g") + .classed(tcls, 1) + .append("text") + .attr("text-anchor", "middle") + .each(function(d) { + var thisLabel = d3.select(this), newPromise = gd._promises.length; + thisLabel + .call(Drawing.setPosition, labelx(d), labely(d)) + .call(Drawing.font, d.font, d.fontSize, d.fontColor) + .text(d.text) + .call(svgTextUtils.convertToTspans); + newPromise = gd._promises[newPromise]; + if (newPromise) { + // if we have an async label, we'll deal with that + // all here so take it out of gd._promises and + // instead position the label and promise this in + // labelsReady + labelsReady.push( + gd._promises.pop().then(function() { + positionLabels(thisLabel, ax.tickangle); + }) + ); + } else { + // sync label: just position it now. + positionLabels(thisLabel, ax.tickangle); + } + }); + tickLabels.exit().remove(); + + tickLabels.each(function(d) { + maxFontSize = Math.max(maxFontSize, d.fontSize); + }); + function positionLabels(s, angle) { + s.each(function(d) { + var anchor = labelanchor(angle); + var thisLabel = d3.select(this), + mathjaxGroup = thisLabel.select(".text-math-group"), + transform = transfn(d) + + (isNumeric(angle) && +angle !== 0 + ? " rotate(" + + angle + + "," + + labelx(d) + + "," + + (labely(d) - d.fontSize / 2) + + ")" + : ""); + if (mathjaxGroup.empty()) { + var txt = thisLabel + .select("text") + .attr({ transform: transform, "text-anchor": anchor }); + + if (!txt.empty()) { + txt + .selectAll("tspan.line") + .attr({ x: txt.attr("x"), y: txt.attr("y") }); + } + } else { + var mjShift = Drawing.bBox(mathjaxGroup.node()).width * + ({ end: -0.5, start: 0.5 })[anchor]; + mathjaxGroup.attr( + "transform", + transform + (mjShift ? "translate(" + mjShift + ",0)" : "") + ); + } + }); + } + + // make sure all labels are correctly positioned at their base angle + // the positionLabels call above is only for newly drawn labels. + // do this without waiting, using the last calculated angle to + // minimize flicker, then do it again when we know all labels are + // there, putting back the prescribed angle to check for overlaps. + positionLabels(tickLabels, ax._lastangle || ax.tickangle); + + function allLabelsReady() { + return labelsReady.length && Promise.all(labelsReady); + } + + function fixLabelOverlaps() { + positionLabels(tickLabels, ax.tickangle); + + // check for auto-angling if x labels overlap + // don't auto-angle at all for log axes with + // base and digit format + if ( + axletter === "x" && + !isNumeric(ax.tickangle) && + (ax.type !== "log" || String(ax.dtick).charAt(0) !== "D") + ) { + var lbbArray = []; tickLabels.each(function(d) { - maxFontSize = Math.max(maxFontSize, d.fontSize); + var s = d3.select(this), + thisLabel = s.select(".text-math-group"), + x = ax.l2p(d.x); + if (thisLabel.empty()) thisLabel = s.select("text"); + + var bb = Drawing.bBox(thisLabel.node()); + + lbbArray.push({ + // ignore about y, just deal with x overlaps + top: 0, + bottom: 10, + height: 10, + left: x - bb.width / 2, + // impose a 2px gap + right: ( + x + bb.width / 2 + 2 + ), + width: bb.width + 2 + }); }); - - function positionLabels(s, angle) { - s.each(function(d) { - var anchor = labelanchor(angle); - var thisLabel = d3.select(this), - mathjaxGroup = thisLabel.select('.text-math-group'), - transform = transfn(d) + - ((isNumeric(angle) && +angle !== 0) ? - (' rotate(' + angle + ',' + labelx(d) + ',' + - (labely(d) - d.fontSize / 2) + ')') : - ''); - if(mathjaxGroup.empty()) { - var txt = thisLabel.select('text').attr({ - transform: transform, - 'text-anchor': anchor - }); - - if(!txt.empty()) { - txt.selectAll('tspan.line').attr({ - x: txt.attr('x'), - y: txt.attr('y') - }); - } - } - else { - var mjShift = - Drawing.bBox(mathjaxGroup.node()).width * - {end: -0.5, start: 0.5}[anchor]; - mathjaxGroup.attr('transform', transform + - (mjShift ? 'translate(' + mjShift + ',0)' : '')); - } - }); - } - - // make sure all labels are correctly positioned at their base angle - // the positionLabels call above is only for newly drawn labels. - // do this without waiting, using the last calculated angle to - // minimize flicker, then do it again when we know all labels are - // there, putting back the prescribed angle to check for overlaps. - positionLabels(tickLabels, ax._lastangle || ax.tickangle); - - function allLabelsReady() { - return labelsReady.length && Promise.all(labelsReady); - } - - function fixLabelOverlaps() { - positionLabels(tickLabels, ax.tickangle); - - // check for auto-angling if x labels overlap - // don't auto-angle at all for log axes with - // base and digit format - if(axletter === 'x' && !isNumeric(ax.tickangle) && - (ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')) { - var lbbArray = []; - tickLabels.each(function(d) { - var s = d3.select(this), - thisLabel = s.select('.text-math-group'), - x = ax.l2p(d.x); - if(thisLabel.empty()) thisLabel = s.select('text'); - - var bb = Drawing.bBox(thisLabel.node()); - - lbbArray.push({ - // ignore about y, just deal with x overlaps - top: 0, - bottom: 10, - height: 10, - left: x - bb.width / 2, - // impose a 2px gap - right: x + bb.width / 2 + 2, - width: bb.width + 2 - }); - }); - for(i = 0; i < lbbArray.length - 1; i++) { - if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1])) { - // any overlap at all - set 30 degrees - autoangle = 30; - break; - } - } - if(autoangle) { - var tickspacing = Math.abs( - (vals[vals.length - 1].x - vals[0].x) * ax._m - ) / (vals.length - 1); - if(tickspacing < maxFontSize * 2.5) { - autoangle = 90; - } - positionLabels(tickLabels, autoangle); - } - ax._lastangle = autoangle; + for (i = 0; i < lbbArray.length - 1; i++) { + if (Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1])) { + // any overlap at all - set 30 degrees + autoangle = 30; + break; + } + } + if (autoangle) { + var tickspacing = Math.abs( + (vals[vals.length - 1].x - vals[0].x) * ax._m + ) / + (vals.length - 1); + if (tickspacing < maxFontSize * 2.5) { + autoangle = 90; + } + positionLabels(tickLabels, autoangle); + } + ax._lastangle = autoangle; + } + + // update the axis title + // (so it can move out of the way if needed) + // TODO: separate out scoot so we don't need to do + // a full redraw of the title (mostly relevant for MathJax) + drawAxTitle(axid); + return axid + " done"; + } + + function calcBoundingBox() { + ax._boundingBox = container.node().getBoundingClientRect(); + } + + var done = Lib.syncOrAsync([ + allLabelsReady, + fixLabelOverlaps, + calcBoundingBox + ]); + if (done && done.then) gd._promises.push(done); + return done; + } + + function drawAxTitle(axid) { + if (skipTitle) return; + + // now this only applies to regular cartesian axes; colorbars and + // others ALWAYS call doTicks with skipTitle=true so they can + // configure their own titles. + var ax = axisIds.getFromId(gd, axid), + avoidSelection = d3.select(gd).selectAll("g." + axid + "tick"), + avoid = { selection: avoidSelection, side: ax.side }, + axLetter = axid.charAt(0), + gs = gd._fullLayout._size, + offsetBase = 1.5, + fontSize = ax.titlefont.size, + transform, + counterAxis, + x, + y; + if (avoidSelection.size()) { + var translation = Drawing.getTranslate(avoidSelection.node().parentNode); + avoid.offsetLeft = translation.x; + avoid.offsetTop = translation.y; + } + + if (axLetter === "x") { + counterAxis = ax.anchor === "free" + ? { _offset: gs.t + (1 - (ax.position || 0)) * gs.h, _length: 0 } + : axisIds.getFromId(gd, ax.anchor); + + x = ax._offset + ax._length / 2; + y = counterAxis._offset + + (ax.side === "top" + ? -10 - fontSize * (offsetBase + (ax.showticklabels ? 1 : 0)) + : counterAxis._length + + 10 + + fontSize * (offsetBase + (ax.showticklabels ? 1.5 : 0.5))); + + if (ax.rangeslider && ax.rangeslider.visible && ax._boundingBox) { + y += (fullLayout.height - fullLayout.margin.b - fullLayout.margin.t) * + ax.rangeslider.thickness + + ax._boundingBox.height; + } + + if (!avoid.side) avoid.side = "bottom"; + } else { + counterAxis = ax.anchor === "free" + ? { _offset: gs.l + (ax.position || 0) * gs.w, _length: 0 } + : axisIds.getFromId(gd, ax.anchor); + + y = ax._offset + ax._length / 2; + x = counterAxis._offset + + (ax.side === "right" + ? counterAxis._length + + 10 + + fontSize * (offsetBase + (ax.showticklabels ? 1 : 0.5)) + : -10 - fontSize * (offsetBase + (ax.showticklabels ? 0.5 : 0))); + + transform = { rotate: "-90", offset: 0 }; + if (!avoid.side) avoid.side = "left"; + } + + Titles.draw(gd, axid + "title", { + propContainer: ax, + propName: ax._name + ".title", + dfltName: axLetter.toUpperCase() + " axis", + avoid: avoid, + transform: transform, + attributes: { x: x, y: y, "text-anchor": "middle" } + }); + } + + function traceHasBarsOrFill(trace, subplot) { + if (trace.visible !== true || trace.xaxis + trace.yaxis !== subplot) { + return false; + } + if ( + Registry.traceIs(trace, "bar") && + trace.orientation === ({ x: "h", y: "v" })[axletter] + ) { + return true; + } + return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axletter; + } + + function drawGrid(plotinfo, counteraxis, subplot) { + var gridcontainer = plotinfo.gridlayer, + zlcontainer = plotinfo.zerolinelayer, + gridvals = plotinfo["hidegrid" + axletter] ? [] : valsClipped, + gridpath = ax._gridpath || + "M0,0" + (axletter === "x" ? "v" : "h") + counteraxis._length, + grid = gridcontainer + .selectAll("path." + gcls) + .data(ax.showgrid === false ? [] : gridvals, datafn); + grid + .enter() + .append("path") + .classed(gcls, 1) + .classed("crisp", 1) + .attr("d", gridpath) + .each(function(d) { + if ( + ax.zeroline && + (ax.type === "linear" || ax.type === "-") && + Math.abs(d.x) < ax.dtick / 100 + ) { + d3.select(this).remove(); + } + }); + grid + .attr("transform", transfn) + .call(Color.stroke, ax.gridcolor || "#ddd") + .style("stroke-width", gridWidth + "px"); + grid.exit().remove(); + + // zero line + if (zlcontainer) { + var hasBarsOrFill = false; + for (var i = 0; i < gd._fullData.length; i++) { + if (traceHasBarsOrFill(gd._fullData[i], subplot)) { + hasBarsOrFill = true; + break; + } + } + var rng = Lib.simpleMap(ax.range, ax.r2l), + showZl = rng[0] * rng[1] <= 0 && + ax.zeroline && + (ax.type === "linear" || ax.type === "-") && + gridvals.length && + (hasBarsOrFill || clipEnds({ x: 0 }) || !ax.showline); + var zl = zlcontainer + .selectAll("path." + zcls) + .data(showZl ? [{ x: 0 }] : []); + zl + .enter() + .append("path") + .classed(zcls, 1) + .classed("zl", 1) + .classed("crisp", 1) + .attr("d", gridpath); + zl + .attr("transform", transfn) + .call(Color.stroke, ax.zerolinecolor || Color.defaultLine) + .style("stroke-width", zeroLineWidth + "px"); + zl.exit().remove(); + } + } + + if (independent) { + drawTicks( + ax._axislayer, + tickpathfn(ax._pos + pad * ticksign[2], ticksign[2] * ax.ticklen) + ); + if (ax._counteraxis) { + var fictionalPlotinfo = { + gridlayer: ax._gridlayer, + zerolinelayer: ax._zerolinelayer + }; + drawGrid(fictionalPlotinfo, ax._counteraxis); + } + return drawLabels(ax._axislayer, ax._pos); + } else { + var alldone = axes + .getSubplots(gd, ax) + .map(function(subplot) { + var plotinfo = fullLayout._plots[subplot]; + + if (!fullLayout._has("cartesian")) return; + + var container = plotinfo[axletter + "axislayer"], + // [bottom or left, top or right, free, main] + linepositions = ax._linepositions[subplot] || [], + counteraxis = plotinfo[counterLetter + "axis"], + mainSubplot = counteraxis._id === ax.anchor, + ticksides = [false, false, false], + tickpath = ""; + + // ticks + if (ax.mirror === "allticks") { + ticksides = [true, true, false]; + } else if (mainSubplot) { + if (ax.mirror === "ticks") ticksides = [true, true, false]; + else ticksides[sides.indexOf(axside)] = true; + } + if (ax.mirrors) { + for (i = 0; i < 2; i++) { + var thisMirror = ax.mirrors[counteraxis._id + sides[i]]; + if (thisMirror === "ticks" || thisMirror === "labels") { + ticksides[i] = true; } - - // update the axis title - // (so it can move out of the way if needed) - // TODO: separate out scoot so we don't need to do - // a full redraw of the title (mostly relevant for MathJax) - drawAxTitle(axid); - return axid + ' done'; + } } - function calcBoundingBox() { - ax._boundingBox = container.node().getBoundingClientRect(); - } + // free axis ticks + if (linepositions[2] !== undefined) ticksides[2] = true; - var done = Lib.syncOrAsync([ - allLabelsReady, - fixLabelOverlaps, - calcBoundingBox - ]); - if(done && done.then) gd._promises.push(done); - return done; - } - - function drawAxTitle(axid) { - if(skipTitle) return; - - // now this only applies to regular cartesian axes; colorbars and - // others ALWAYS call doTicks with skipTitle=true so they can - // configure their own titles. - var ax = axisIds.getFromId(gd, axid), - avoidSelection = d3.select(gd).selectAll('g.' + axid + 'tick'), - avoid = { - selection: avoidSelection, - side: ax.side - }, - axLetter = axid.charAt(0), - gs = gd._fullLayout._size, - offsetBase = 1.5, - fontSize = ax.titlefont.size, - transform, - counterAxis, - x, - y; - if(avoidSelection.size()) { - var translation = Drawing.getTranslate(avoidSelection.node().parentNode); - avoid.offsetLeft = translation.x; - avoid.offsetTop = translation.y; - } - - if(axLetter === 'x') { - counterAxis = (ax.anchor === 'free') ? - {_offset: gs.t + (1 - (ax.position || 0)) * gs.h, _length: 0} : - axisIds.getFromId(gd, ax.anchor); - - x = ax._offset + ax._length / 2; - y = counterAxis._offset + ((ax.side === 'top') ? - -10 - fontSize * (offsetBase + (ax.showticklabels ? 1 : 0)) : - counterAxis._length + 10 + - fontSize * (offsetBase + (ax.showticklabels ? 1.5 : 0.5))); - - if(ax.rangeslider && ax.rangeslider.visible && ax._boundingBox) { - y += (fullLayout.height - fullLayout.margin.b - fullLayout.margin.t) * - ax.rangeslider.thickness + ax._boundingBox.height; - } - - if(!avoid.side) avoid.side = 'bottom'; - } - else { - counterAxis = (ax.anchor === 'free') ? - {_offset: gs.l + (ax.position || 0) * gs.w, _length: 0} : - axisIds.getFromId(gd, ax.anchor); - - y = ax._offset + ax._length / 2; - x = counterAxis._offset + ((ax.side === 'right') ? - counterAxis._length + 10 + - fontSize * (offsetBase + (ax.showticklabels ? 1 : 0.5)) : - -10 - fontSize * (offsetBase + (ax.showticklabels ? 0.5 : 0))); - - transform = {rotate: '-90', offset: 0}; - if(!avoid.side) avoid.side = 'left'; - } - - Titles.draw(gd, axid + 'title', { - propContainer: ax, - propName: ax._name + '.title', - dfltName: axLetter.toUpperCase() + ' axis', - avoid: avoid, - transform: transform, - attributes: {x: x, y: y, 'text-anchor': 'middle'} + ticksides.forEach(function(showside, sidei) { + var pos = linepositions[sidei], tsign = ticksign[sidei]; + if (showside && isNumeric(pos)) { + tickpath += tickpathfn(pos + pad * tsign, tsign * ax.ticklen); + } }); - } - - function traceHasBarsOrFill(trace, subplot) { - if(trace.visible !== true || trace.xaxis + trace.yaxis !== subplot) return false; - if(Registry.traceIs(trace, 'bar') && trace.orientation === {x: 'h', y: 'v'}[axletter]) return true; - return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axletter; - } - - function drawGrid(plotinfo, counteraxis, subplot) { - var gridcontainer = plotinfo.gridlayer, - zlcontainer = plotinfo.zerolinelayer, - gridvals = plotinfo['hidegrid' + axletter] ? [] : valsClipped, - gridpath = ax._gridpath || - 'M0,0' + ((axletter === 'x') ? 'v' : 'h') + counteraxis._length, - grid = gridcontainer.selectAll('path.' + gcls) - .data((ax.showgrid === false) ? [] : gridvals, datafn); - grid.enter().append('path').classed(gcls, 1) - .classed('crisp', 1) - .attr('d', gridpath) - .each(function(d) { - if(ax.zeroline && (ax.type === 'linear' || ax.type === '-') && - Math.abs(d.x) < ax.dtick / 100) { - d3.select(this).remove(); - } - }); - grid.attr('transform', transfn) - .call(Color.stroke, ax.gridcolor || '#ddd') - .style('stroke-width', gridWidth + 'px'); - grid.exit().remove(); - - // zero line - if(zlcontainer) { - var hasBarsOrFill = false; - for(var i = 0; i < gd._fullData.length; i++) { - if(traceHasBarsOrFill(gd._fullData[i], subplot)) { - hasBarsOrFill = true; - break; - } - } - var rng = Lib.simpleMap(ax.range, ax.r2l), - showZl = (rng[0] * rng[1] <= 0) && ax.zeroline && - (ax.type === 'linear' || ax.type === '-') && gridvals.length && - (hasBarsOrFill || clipEnds({x: 0}) || !ax.showline); - var zl = zlcontainer.selectAll('path.' + zcls) - .data(showZl ? [{x: 0}] : []); - zl.enter().append('path').classed(zcls, 1).classed('zl', 1) - .classed('crisp', 1) - .attr('d', gridpath); - zl.attr('transform', transfn) - .call(Color.stroke, ax.zerolinecolor || Color.defaultLine) - .style('stroke-width', zeroLineWidth + 'px'); - zl.exit().remove(); - } - } - - if(independent) { - drawTicks(ax._axislayer, tickpathfn(ax._pos + pad * ticksign[2], ticksign[2] * ax.ticklen)); - if(ax._counteraxis) { - var fictionalPlotinfo = { - gridlayer: ax._gridlayer, - zerolinelayer: ax._zerolinelayer - }; - drawGrid(fictionalPlotinfo, ax._counteraxis); - } - return drawLabels(ax._axislayer, ax._pos); - } - else { - var alldone = axes.getSubplots(gd, ax).map(function(subplot) { - var plotinfo = fullLayout._plots[subplot]; - if(!fullLayout._has('cartesian')) return; - - var container = plotinfo[axletter + 'axislayer'], - - // [bottom or left, top or right, free, main] - linepositions = ax._linepositions[subplot] || [], - counteraxis = plotinfo[counterLetter + 'axis'], - mainSubplot = counteraxis._id === ax.anchor, - ticksides = [false, false, false], - tickpath = ''; - - // ticks - if(ax.mirror === 'allticks') ticksides = [true, true, false]; - else if(mainSubplot) { - if(ax.mirror === 'ticks') ticksides = [true, true, false]; - else ticksides[sides.indexOf(axside)] = true; - } - if(ax.mirrors) { - for(i = 0; i < 2; i++) { - var thisMirror = ax.mirrors[counteraxis._id + sides[i]]; - if(thisMirror === 'ticks' || thisMirror === 'labels') { - ticksides[i] = true; - } - } - } + drawTicks(container, tickpath); + drawGrid(plotinfo, counteraxis, subplot); + return drawLabels(container, linepositions[3]); + }) + .filter(function(onedone) { + return onedone && onedone.then; + }); - // free axis ticks - if(linepositions[2] !== undefined) ticksides[2] = true; - - ticksides.forEach(function(showside, sidei) { - var pos = linepositions[sidei], - tsign = ticksign[sidei]; - if(showside && isNumeric(pos)) { - tickpath += tickpathfn(pos + pad * tsign, tsign * ax.ticklen); - } - }); - - drawTicks(container, tickpath); - drawGrid(plotinfo, counteraxis, subplot); - return drawLabels(container, linepositions[3]); - }).filter(function(onedone) { return onedone && onedone.then; }); - - return alldone.length ? Promise.all(alldone) : 0; - } + return alldone.length ? Promise.all(alldone) : 0; + } }; // swap all the presentation attributes of the axes showing these traces axes.swap = function(gd, traces) { - var axGroups = makeAxisGroups(gd, traces); + var axGroups = makeAxisGroups(gd, traces); - for(var i = 0; i < axGroups.length; i++) { - swapAxisGroup(gd, axGroups[i].x, axGroups[i].y); - } + for (var i = 0; i < axGroups.length; i++) { + swapAxisGroup(gd, axGroups[i].x, axGroups[i].y); + } }; function makeAxisGroups(gd, traces) { - var groups = [], - i, - j; - - for(i = 0; i < traces.length; i++) { - var groupsi = [], - xi = gd._fullData[traces[i]].xaxis, - yi = gd._fullData[traces[i]].yaxis; - if(!xi || !yi) continue; // not a 2D cartesian trace? - - for(j = 0; j < groups.length; j++) { - if(groups[j].x.indexOf(xi) !== -1 || groups[j].y.indexOf(yi) !== -1) { - groupsi.push(j); - } - } + var groups = [], i, j; - if(!groupsi.length) { - groups.push({x: [xi], y: [yi]}); - continue; - } + for (i = 0; i < traces.length; i++) { + var groupsi = [], + xi = gd._fullData[traces[i]].xaxis, + yi = gd._fullData[traces[i]].yaxis; + if (!xi || !yi) continue; - var group0 = groups[groupsi[0]], - groupj; + // not a 2D cartesian trace? + for (j = 0; j < groups.length; j++) { + if (groups[j].x.indexOf(xi) !== -1 || groups[j].y.indexOf(yi) !== -1) { + groupsi.push(j); + } + } - if(groupsi.length > 1) { - for(j = 1; j < groupsi.length; j++) { - groupj = groups[groupsi[j]]; - mergeAxisGroups(group0.x, groupj.x); - mergeAxisGroups(group0.y, groupj.y); - } - } - mergeAxisGroups(group0.x, [xi]); - mergeAxisGroups(group0.y, [yi]); + if (!groupsi.length) { + groups.push({ x: [xi], y: [yi] }); + continue; + } + + var group0 = groups[groupsi[0]], groupj; + + if (groupsi.length > 1) { + for (j = 1; j < groupsi.length; j++) { + groupj = groups[groupsi[j]]; + mergeAxisGroups(group0.x, groupj.x); + mergeAxisGroups(group0.y, groupj.y); + } } + mergeAxisGroups(group0.x, [xi]); + mergeAxisGroups(group0.y, [yi]); + } - return groups; + return groups; } function mergeAxisGroups(intoSet, fromSet) { - for(var i = 0; i < fromSet.length; i++) { - if(intoSet.indexOf(fromSet[i]) === -1) intoSet.push(fromSet[i]); - } + for (var i = 0; i < fromSet.length; i++) { + if (intoSet.indexOf(fromSet[i]) === -1) intoSet.push(fromSet[i]); + } } function swapAxisGroup(gd, xIds, yIds) { - var i, - j, - xFullAxes = [], - yFullAxes = [], - layout = gd.layout; - - for(i = 0; i < xIds.length; i++) xFullAxes.push(axes.getFromId(gd, xIds[i])); - for(i = 0; i < yIds.length; i++) yFullAxes.push(axes.getFromId(gd, yIds[i])); - - var allAxKeys = Object.keys(xFullAxes[0]), - noSwapAttrs = [ - 'anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle' - ], - numericTypes = ['linear', 'log']; - - for(i = 0; i < allAxKeys.length; i++) { - var keyi = allAxKeys[i], - xVal = xFullAxes[0][keyi], - yVal = yFullAxes[0][keyi], - allEqual = true, - coerceLinearX = false, - coerceLinearY = false; - if(keyi.charAt(0) === '_' || typeof xVal === 'function' || - noSwapAttrs.indexOf(keyi) !== -1) { - continue; - } - for(j = 1; j < xFullAxes.length && allEqual; j++) { - var xVali = xFullAxes[j][keyi]; - if(keyi === 'type' && numericTypes.indexOf(xVal) !== -1 && - numericTypes.indexOf(xVali) !== -1 && xVal !== xVali) { - // type is special - if we find a mixture of linear and log, - // coerce them all to linear on flipping - coerceLinearX = true; - } - else if(xVali !== xVal) allEqual = false; - } - for(j = 1; j < yFullAxes.length && allEqual; j++) { - var yVali = yFullAxes[j][keyi]; - if(keyi === 'type' && numericTypes.indexOf(yVal) !== -1 && - numericTypes.indexOf(yVali) !== -1 && yVal !== yVali) { - // type is special - if we find a mixture of linear and log, - // coerce them all to linear on flipping - coerceLinearY = true; - } - else if(yFullAxes[j][keyi] !== yVal) allEqual = false; - } - if(allEqual) { - if(coerceLinearX) layout[xFullAxes[0]._name].type = 'linear'; - if(coerceLinearY) layout[yFullAxes[0]._name].type = 'linear'; - swapAxisAttrs(layout, keyi, xFullAxes, yFullAxes); - } - } - - // now swap x&y for any annotations anchored to these x & y - for(i = 0; i < gd._fullLayout.annotations.length; i++) { - var ann = gd._fullLayout.annotations[i]; - if(xIds.indexOf(ann.xref) !== -1 && - yIds.indexOf(ann.yref) !== -1) { - Lib.swapAttrs(layout.annotations[i], ['?']); - } - } + var i, j, xFullAxes = [], yFullAxes = [], layout = gd.layout; + + for (i = 0; i < xIds.length; i++) { + xFullAxes.push(axes.getFromId(gd, xIds[i])); + } + for (i = 0; i < yIds.length; i++) { + yFullAxes.push(axes.getFromId(gd, yIds[i])); + } + + var allAxKeys = Object.keys(xFullAxes[0]), + noSwapAttrs = [ + "anchor", + "domain", + "overlaying", + "position", + "side", + "tickangle" + ], + numericTypes = ["linear", "log"]; + + for (i = 0; i < allAxKeys.length; i++) { + var keyi = allAxKeys[i], + xVal = xFullAxes[0][keyi], + yVal = yFullAxes[0][keyi], + allEqual = true, + coerceLinearX = false, + coerceLinearY = false; + if ( + keyi.charAt(0) === "_" || + typeof xVal === "function" || + noSwapAttrs.indexOf(keyi) !== -1 + ) { + continue; + } + for (j = 1; j < xFullAxes.length && allEqual; j++) { + var xVali = xFullAxes[j][keyi]; + if ( + keyi === "type" && + numericTypes.indexOf(xVal) !== -1 && + numericTypes.indexOf(xVali) !== -1 && + xVal !== xVali + ) { + // type is special - if we find a mixture of linear and log, + // coerce them all to linear on flipping + coerceLinearX = true; + } else if (xVali !== xVal) allEqual = false; + } + for (j = 1; j < yFullAxes.length && allEqual; j++) { + var yVali = yFullAxes[j][keyi]; + if ( + keyi === "type" && + numericTypes.indexOf(yVal) !== -1 && + numericTypes.indexOf(yVali) !== -1 && + yVal !== yVali + ) { + // type is special - if we find a mixture of linear and log, + // coerce them all to linear on flipping + coerceLinearY = true; + } else if (yFullAxes[j][keyi] !== yVal) allEqual = false; + } + if (allEqual) { + if (coerceLinearX) layout[xFullAxes[0]._name].type = "linear"; + if (coerceLinearY) layout[yFullAxes[0]._name].type = "linear"; + swapAxisAttrs(layout, keyi, xFullAxes, yFullAxes); + } + } + + // now swap x&y for any annotations anchored to these x & y + for (i = 0; i < gd._fullLayout.annotations.length; i++) { + var ann = gd._fullLayout.annotations[i]; + if (xIds.indexOf(ann.xref) !== -1 && yIds.indexOf(ann.yref) !== -1) { + Lib.swapAttrs(layout.annotations[i], ["?"]); + } + } } function swapAxisAttrs(layout, key, xFullAxes, yFullAxes) { - // in case the value is the default for either axis, - // look at the first axis in each list and see if - // this key's value is undefined - var np = Lib.nestedProperty, - xVal = np(layout[xFullAxes[0]._name], key).get(), - yVal = np(layout[yFullAxes[0]._name], key).get(), - i; - if(key === 'title') { - // special handling of placeholder titles - if(xVal === 'Click to enter X axis title') { - xVal = 'Click to enter Y axis title'; - } - if(yVal === 'Click to enter Y axis title') { - yVal = 'Click to enter X axis title'; - } - } - - for(i = 0; i < xFullAxes.length; i++) { - np(layout, xFullAxes[i]._name + '.' + key).set(yVal); - } - for(i = 0; i < yFullAxes.length; i++) { - np(layout, yFullAxes[i]._name + '.' + key).set(xVal); - } + // in case the value is the default for either axis, + // look at the first axis in each list and see if + // this key's value is undefined + var np = Lib.nestedProperty, + xVal = np(layout[xFullAxes[0]._name], key).get(), + yVal = np(layout[yFullAxes[0]._name], key).get(), + i; + if (key === "title") { + // special handling of placeholder titles + if (xVal === "Click to enter X axis title") { + xVal = "Click to enter Y axis title"; + } + if (yVal === "Click to enter Y axis title") { + yVal = "Click to enter X axis title"; + } + } + + for (i = 0; i < xFullAxes.length; i++) { + np(layout, xFullAxes[i]._name + "." + key).set(yVal); + } + for (i = 0; i < yFullAxes.length; i++) { + np(layout, yFullAxes[i]._name + "." + key).set(xVal); + } } diff --git a/src/plots/cartesian/axis_autotype.js b/src/plots/cartesian/axis_autotype.js index 476c4ec4bff..dbc0714e9ca 100644 --- a/src/plots/cartesian/axis_autotype.js +++ b/src/plots/cartesian/axis_autotype.js @@ -5,32 +5,29 @@ * 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 isNumeric = require("fast-isnumeric"); - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var BADNUM = require('../../constants/numerical').BADNUM; +var Lib = require("../../lib"); +var BADNUM = require("../../constants/numerical").BADNUM; module.exports = function autoType(array, calendar) { - if(moreDates(array, calendar)) return 'date'; - if(category(array)) return 'category'; - if(linearOK(array)) return 'linear'; - else return '-'; + if (moreDates(array, calendar)) return "date"; + if (category(array)) return "category"; + if (linearOK(array)) return "linear"; + else return "-"; }; // is there at least one number in array? If not, we should leave // ax.type empty so it can be autoset later function linearOK(array) { - if(!array) return false; + if (!array) return false; - for(var i = 0; i < array.length; i++) { - if(isNumeric(array[i])) return true; - } + for (var i = 0; i < array.length; i++) { + if (isNumeric(array[i])) return true; + } - return false; + return false; } // does the array a have mostly dates rather than numbers? @@ -39,35 +36,35 @@ function linearOK(array) { // dates as non-dates, to exclude cases with mostly 2 & 4 digit // numbers and a few dates function moreDates(a, calendar) { - var dcnt = 0, - ncnt = 0, - // test at most 1000 points, evenly spaced - inc = Math.max(1, (a.length - 1) / 1000), - ai; + var dcnt = 0, + ncnt = 0, + // test at most 1000 points, evenly spaced + inc = Math.max(1, (a.length - 1) / 1000), + ai; - for(var i = 0; i < a.length; i += inc) { - ai = a[Math.round(i)]; - if(Lib.isDateTime(ai, calendar)) dcnt += 1; - if(isNumeric(ai)) ncnt += 1; - } + for (var i = 0; i < a.length; i += inc) { + ai = a[Math.round(i)]; + if (Lib.isDateTime(ai, calendar)) dcnt += 1; + if (isNumeric(ai)) ncnt += 1; + } - return (dcnt > ncnt * 2); + return dcnt > ncnt * 2; } // are the (x,y)-values in gd.data mostly text? // require twice as many categories as numbers function category(a) { - // test at most 1000 points - var inc = Math.max(1, (a.length - 1) / 1000), - curvenums = 0, - curvecats = 0, - ai; + // test at most 1000 points + var inc = Math.max(1, (a.length - 1) / 1000), + curvenums = 0, + curvecats = 0, + ai; - for(var i = 0; i < a.length; i += inc) { - ai = a[Math.round(i)]; - if(Lib.cleanNumber(ai) !== BADNUM) curvenums++; - else if(typeof ai === 'string' && ai !== '' && ai !== 'None') curvecats++; - } + for (var i = 0; i < a.length; i += inc) { + ai = a[Math.round(i)]; + if (Lib.cleanNumber(ai) !== BADNUM) curvenums++; + else if (typeof ai === "string" && ai !== "" && ai !== "None") curvecats++; + } - return curvecats > curvenums * 2; + return curvecats > curvenums * 2; } diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index e4e99bf8294..93199e01e18 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -5,27 +5,23 @@ * 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 isNumeric = require('fast-isnumeric'); -var colorMix = require('tinycolor2').mix; - -var Registry = require('../../registry'); -var Lib = require('../../lib'); -var lightFraction = require('../../components/color/attributes').lightFraction; - -var layoutAttributes = require('./layout_attributes'); -var handleTickValueDefaults = require('./tick_value_defaults'); -var handleTickMarkDefaults = require('./tick_mark_defaults'); -var handleTickLabelDefaults = require('./tick_label_defaults'); -var handleCategoryOrderDefaults = require('./category_order_defaults'); -var setConvert = require('./set_convert'); -var orderedCategories = require('./ordered_categories'); -var axisIds = require('./axis_ids'); -var autoType = require('./axis_autotype'); - +"use strict"; +var isNumeric = require("fast-isnumeric"); +var colorMix = require("tinycolor2").mix; + +var Registry = require("../../registry"); +var Lib = require("../../lib"); +var lightFraction = require("../../components/color/attributes").lightFraction; + +var layoutAttributes = require("./layout_attributes"); +var handleTickValueDefaults = require("./tick_value_defaults"); +var handleTickMarkDefaults = require("./tick_mark_defaults"); +var handleTickLabelDefaults = require("./tick_label_defaults"); +var handleCategoryOrderDefaults = require("./category_order_defaults"); +var setConvert = require("./set_convert"); +var orderedCategories = require("./ordered_categories"); +var axisIds = require("./axis_ids"); +var autoType = require("./axis_autotype"); /** * options: object containing: @@ -40,193 +36,216 @@ var autoType = require('./axis_autotype'); * data: the plot data to use in choosing auto type * bgColor: the plot background color, to calculate default gridline colors */ -module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, options) { - var letter = options.letter, - font = options.font || {}, - defaultTitle = 'Click to enter ' + - (options.title || (letter.toUpperCase() + ' axis')) + - ' title'; - - function coerce2(attr, dflt) { - return Lib.coerce2(containerIn, containerOut, layoutAttributes, attr, dflt); +module.exports = function handleAxisDefaults( + containerIn, + containerOut, + coerce, + options +) { + var letter = options.letter, + font = options.font || {}, + defaultTitle = "Click to enter " + + (options.title || letter.toUpperCase() + " axis") + + " title"; + + function coerce2(attr, dflt) { + return Lib.coerce2(containerIn, containerOut, layoutAttributes, attr, dflt); + } + + // set up some private properties + if (options.name) { + containerOut._name = options.name; + containerOut._id = axisIds.name2id(options.name); + } + + // now figure out type and do some more initialization + var axType = coerce("type"); + if (axType === "-") { + setAutoType(containerOut, options.data); + + if (containerOut.type === "-") { + containerOut.type = "linear"; + } else { + // copy autoType back to input axis + // note that if this object didn't exist + // in the input layout, we have to put it in + // this happens in the main supplyDefaults function + axType = containerIn.type = containerOut.type; } + } - // set up some private properties - if(options.name) { - containerOut._name = options.name; - containerOut._id = axisIds.name2id(options.name); - } - - // now figure out type and do some more initialization - var axType = coerce('type'); - if(axType === '-') { - setAutoType(containerOut, options.data); - - if(containerOut.type === '-') { - containerOut.type = 'linear'; - } - else { - // copy autoType back to input axis - // note that if this object didn't exist - // in the input layout, we have to put it in - // this happens in the main supplyDefaults function - axType = containerIn.type = containerOut.type; - } - } - - if(axType === 'date') { - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults'); - handleCalendarDefaults(containerIn, containerOut, 'calendar', options.calendar); - } - - setConvert(containerOut); - - var dfltColor = coerce('color'); - // 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; - - coerce('title', defaultTitle); - Lib.coerceFont(coerce, 'titlefont', { - family: font.family, - size: Math.round(font.size * 1.2), - color: dfltFontColor - }); - - var validRange = ( - (containerIn.range || []).length === 2 && - isNumeric(containerOut.r2l(containerIn.range[0])) && - isNumeric(containerOut.r2l(containerIn.range[1])) + if (axType === "date") { + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleDefaults" + ); + handleCalendarDefaults( + containerIn, + containerOut, + "calendar", + options.calendar + ); + } + + setConvert(containerOut); + + var dfltColor = coerce("color"); + // 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; + + coerce("title", defaultTitle); + Lib.coerceFont(coerce, "titlefont", { + family: font.family, + size: Math.round(font.size * 1.2), + color: dfltFontColor + }); + + var validRange = (containerIn.range || []).length === 2 && + isNumeric(containerOut.r2l(containerIn.range[0])) && + isNumeric(containerOut.r2l(containerIn.range[1])); + var autoRange = coerce("autorange", !validRange); + + if (autoRange) coerce("rangemode"); + + coerce("range"); + containerOut.cleanRange(); + + handleTickValueDefaults(containerIn, containerOut, coerce, axType); + handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); + handleTickMarkDefaults(containerIn, containerOut, coerce, options); + handleCategoryOrderDefaults(containerIn, containerOut, coerce); + + var lineColor = coerce2("linecolor", dfltColor), + lineWidth = coerce2("linewidth"), + showLine = coerce("showline", !!lineColor || !!lineWidth); + + if (!showLine) { + delete containerOut.linecolor; + delete containerOut.linewidth; + } + + if (showLine || containerOut.ticks) coerce("mirror"); + + var gridColor = coerce2( + "gridcolor", + colorMix(dfltColor, options.bgColor, lightFraction).toRgbString() + ), + gridWidth = coerce2("gridwidth"), + showGridLines = coerce( + "showgrid", + options.showGrid || !!gridColor || !!gridWidth ); - var autoRange = coerce('autorange', !validRange); - - if(autoRange) coerce('rangemode'); - - coerce('range'); - containerOut.cleanRange(); - - handleTickValueDefaults(containerIn, containerOut, coerce, axType); - handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); - handleTickMarkDefaults(containerIn, containerOut, coerce, options); - handleCategoryOrderDefaults(containerIn, containerOut, coerce); - - var lineColor = coerce2('linecolor', dfltColor), - lineWidth = coerce2('linewidth'), - showLine = coerce('showline', !!lineColor || !!lineWidth); - - if(!showLine) { - delete containerOut.linecolor; - delete containerOut.linewidth; - } - - if(showLine || containerOut.ticks) coerce('mirror'); - - var gridColor = coerce2('gridcolor', colorMix(dfltColor, options.bgColor, lightFraction).toRgbString()), - gridWidth = coerce2('gridwidth'), - showGridLines = coerce('showgrid', options.showGrid || !!gridColor || !!gridWidth); - - if(!showGridLines) { - delete containerOut.gridcolor; - delete containerOut.gridwidth; - } - - var zeroLineColor = coerce2('zerolinecolor', dfltColor), - zeroLineWidth = coerce2('zerolinewidth'), - showZeroLine = coerce('zeroline', options.showGrid || !!zeroLineColor || !!zeroLineWidth); - if(!showZeroLine) { - delete containerOut.zerolinecolor; - delete containerOut.zerolinewidth; - } + if (!showGridLines) { + delete containerOut.gridcolor; + delete containerOut.gridwidth; + } - // fill in categories - containerOut._initialCategories = axType === 'category' ? - orderedCategories(letter, containerOut.categoryorder, containerOut.categoryarray, options.data) : - []; + var zeroLineColor = coerce2("zerolinecolor", dfltColor), + zeroLineWidth = coerce2("zerolinewidth"), + showZeroLine = coerce( + "zeroline", + options.showGrid || !!zeroLineColor || !!zeroLineWidth + ); - return containerOut; + if (!showZeroLine) { + delete containerOut.zerolinecolor; + delete containerOut.zerolinewidth; + } + + // fill in categories + containerOut._initialCategories = axType === "category" + ? orderedCategories( + letter, + containerOut.categoryorder, + containerOut.categoryarray, + options.data + ) + : []; + + return containerOut; }; function setAutoType(ax, data) { - // new logic: let people specify any type they want, - // only autotype if type is '-' - if(ax.type !== '-') return; - - var id = ax._id, - axLetter = id.charAt(0); - - // support 3d - if(id.indexOf('scene') !== -1) id = axLetter; - - var d0 = getFirstNonEmptyTrace(data, id, axLetter); - if(!d0) return; - - // first check for histograms, as the count direction - // should always default to a linear axis - if(d0.type === 'histogram' && - axLetter === {v: 'y', h: 'x'}[d0.orientation || 'v']) { - ax.type = 'linear'; - return; + // new logic: let people specify any type they want, + // only autotype if type is '-' + if (ax.type !== "-") return; + + var id = ax._id, axLetter = id.charAt(0); + + // support 3d + if (id.indexOf("scene") !== -1) id = axLetter; + + var d0 = getFirstNonEmptyTrace(data, id, axLetter); + if (!d0) return; + + // first check for histograms, as the count direction + // should always default to a linear axis + if ( + d0.type === "histogram" && + axLetter === ({ v: "y", h: "x" })[d0.orientation || "v"] + ) { + ax.type = "linear"; + return; + } + + var calAttr = axLetter + "calendar", calendar = d0[calAttr]; + + // 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; + + for (var i = 0; i < data.length; i++) { + trace = data[i]; + if ( + !Registry.traceIs(trace, "box") || + (trace[axLetter + "axis"] || axLetter) !== id + ) { + continue; + } + + if (trace[posLetter] !== undefined) { + boxPositions.push(trace[posLetter][0]); + } else if (trace.name !== undefined) boxPositions.push(trace.name); + else boxPositions.push("text"); + + if (trace[calAttr] !== calendar) calendar = undefined; } - var calAttr = axLetter + 'calendar', - calendar = d0[calAttr]; - - // 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; - - for(var i = 0; i < data.length; i++) { - trace = data[i]; - if(!Registry.traceIs(trace, 'box') || - (trace[axLetter + 'axis'] || axLetter) !== id) continue; - - if(trace[posLetter] !== undefined) boxPositions.push(trace[posLetter][0]); - else if(trace.name !== undefined) boxPositions.push(trace.name); - else boxPositions.push('text'); - - if(trace[calAttr] !== calendar) calendar = undefined; - } - - ax.type = autoType(boxPositions, calendar); - } - else { - ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']], calendar); - } + ax.type = autoType(boxPositions, calendar); + } else { + ax.type = autoType(d0[axLetter] || [d0[axLetter + "0"]], calendar); + } } function getBoxPosLetter(trace) { - return {v: 'x', h: 'y'}[trace.orientation || 'v']; + return ({ v: "x", h: "y" })[trace.orientation || "v"]; } function isBoxWithoutPositionCoords(trace, axLetter) { - var posLetter = getBoxPosLetter(trace), - isBox = Registry.traceIs(trace, 'box'), - isCandlestick = Registry.traceIs(trace._fullInput || {}, 'candlestick'); - - return ( - isBox && - !isCandlestick && - axLetter === posLetter && - trace[posLetter] === undefined && - trace[posLetter + '0'] === undefined - ); + var posLetter = getBoxPosLetter(trace), + isBox = Registry.traceIs(trace, "box"), + isCandlestick = Registry.traceIs(trace._fullInput || {}, "candlestick"); + + return isBox && + !isCandlestick && + axLetter === posLetter && + trace[posLetter] === undefined && + trace[posLetter + "0"] === undefined; } function getFirstNonEmptyTrace(data, id, axLetter) { - for(var i = 0; i < data.length; i++) { - var trace = data[i]; - - if((trace[axLetter + 'axis'] || axLetter) === id) { - if(isBoxWithoutPositionCoords(trace, axLetter)) { - return trace; - } - else if((trace[axLetter] || []).length || trace[axLetter + '0']) { - return trace; - } - } + for (var i = 0; i < data.length; i++) { + var trace = data[i]; + + if ((trace[axLetter + "axis"] || axLetter) === id) { + if (isBoxWithoutPositionCoords(trace, axLetter)) { + return trace; + } else if ((trace[axLetter] || []).length || trace[axLetter + "0"]) { + return trace; + } } + } } diff --git a/src/plots/cartesian/axis_ids.js b/src/plots/cartesian/axis_ids.js index 63b9fae6e77..fe7212f6bd0 100644 --- a/src/plots/cartesian/axis_ids.js +++ b/src/plots/cartesian/axis_ids.js @@ -5,116 +5,107 @@ * 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 Registry = require("../../registry"); +var Plots = require("../plots"); +var Lib = require("../../lib"); -'use strict'; - -var Registry = require('../../registry'); -var Plots = require('../plots'); -var Lib = require('../../lib'); - -var constants = require('./constants'); - +var constants = require("./constants"); // convert between axis names (xaxis, xaxis2, etc, elements of gd.layout) // and axis id's (x, x2, etc). Would probably have ditched 'xaxis' // completely in favor of just 'x' if it weren't ingrained in the API etc. exports.id2name = function id2name(id) { - if(typeof id !== 'string' || !id.match(constants.AX_ID_PATTERN)) return; - var axNum = id.substr(1); - if(axNum === '1') axNum = ''; - return id.charAt(0) + 'axis' + axNum; + if (typeof id !== "string" || !id.match(constants.AX_ID_PATTERN)) return; + var axNum = id.substr(1); + if (axNum === "1") axNum = ""; + return id.charAt(0) + "axis" + axNum; }; exports.name2id = function name2id(name) { - if(!name.match(constants.AX_NAME_PATTERN)) return; - var axNum = name.substr(5); - if(axNum === '1') axNum = ''; - return name.charAt(0) + axNum; + if (!name.match(constants.AX_NAME_PATTERN)) return; + var axNum = name.substr(5); + if (axNum === "1") axNum = ""; + return name.charAt(0) + axNum; }; exports.cleanId = function cleanId(id, axLetter) { - if(!id.match(constants.AX_ID_PATTERN)) return; - if(axLetter && id.charAt(0) !== axLetter) return; + if (!id.match(constants.AX_ID_PATTERN)) return; + if (axLetter && id.charAt(0) !== axLetter) return; - var axNum = id.substr(1).replace(/^0+/, ''); - if(axNum === '1') axNum = ''; - return id.charAt(0) + axNum; + var axNum = id.substr(1).replace(/^0+/, ""); + if (axNum === "1") axNum = ""; + return id.charAt(0) + axNum; }; // get all axis object names // optionally restricted to only x or y or z by string axLetter // and optionally 2D axes only, not those inside 3D scenes function listNames(gd, axLetter, only2d) { - var fullLayout = gd._fullLayout; - if(!fullLayout) return []; - - function filterAxis(obj, extra) { - var keys = Object.keys(obj), - axMatch = /^[xyz]axis[0-9]*/, - out = []; + var fullLayout = gd._fullLayout; + if (!fullLayout) return []; - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; - if(axLetter && k.charAt(0) !== axLetter) continue; - if(axMatch.test(k)) out.push(extra + k); - } + function filterAxis(obj, extra) { + var keys = Object.keys(obj), axMatch = /^[xyz]axis[0-9]*/, out = []; - return out.sort(); + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + if (axLetter && k.charAt(0) !== axLetter) continue; + if (axMatch.test(k)) out.push(extra + k); } - var names = filterAxis(fullLayout, ''); - if(only2d) return names; + return out.sort(); + } - var sceneIds3D = Plots.getSubplotIds(fullLayout, 'gl3d') || []; - for(var i = 0; i < sceneIds3D.length; i++) { - var sceneId = sceneIds3D[i]; - names = names.concat( - filterAxis(fullLayout[sceneId], sceneId + '.') - ); - } + var names = filterAxis(fullLayout, ""); + if (only2d) return names; + + var sceneIds3D = Plots.getSubplotIds(fullLayout, "gl3d") || []; + for (var i = 0; i < sceneIds3D.length; i++) { + var sceneId = sceneIds3D[i]; + names = names.concat(filterAxis(fullLayout[sceneId], sceneId + ".")); + } - return names; + return names; } // get all axis objects, as restricted in listNames exports.list = function(gd, axletter, only2d) { - return listNames(gd, axletter, only2d) - .map(function(axName) { - return Lib.nestedProperty(gd._fullLayout, axName).get(); - }); + return listNames(gd, axletter, only2d).map(function(axName) { + return Lib.nestedProperty(gd._fullLayout, axName).get(); + }); }; // get all axis ids, optionally restricted by letter // this only makes sense for 2d axes exports.listIds = function(gd, axletter) { - return listNames(gd, axletter, true).map(exports.name2id); + return listNames(gd, axletter, true).map(exports.name2id); }; // get an axis object from its id 'x','x2' etc // optionally, id can be a subplot (ie 'x2y3') and type gets x or y from it exports.getFromId = function(gd, id, type) { - var fullLayout = gd._fullLayout; + var fullLayout = gd._fullLayout; - if(type === 'x') id = id.replace(/y[0-9]*/, ''); - else if(type === 'y') id = id.replace(/x[0-9]*/, ''); + if (type === "x") id = id.replace(/y[0-9]*/, ""); + else if (type === "y") id = id.replace(/x[0-9]*/, ""); - return fullLayout[exports.id2name(id)]; + return fullLayout[exports.id2name(id)]; }; // get an axis object of specified type from the containing trace exports.getFromTrace = function(gd, fullTrace, type) { - var fullLayout = gd._fullLayout; - var ax = null; - - if(Registry.traceIs(fullTrace, 'gl3d')) { - var scene = fullTrace.scene; - if(scene.substr(0, 5) === 'scene') { - ax = fullLayout[scene][type + 'axis']; - } - } - else { - ax = exports.getFromId(gd, fullTrace[type + 'axis'] || type); + var fullLayout = gd._fullLayout; + var ax = null; + + if (Registry.traceIs(fullTrace, "gl3d")) { + var scene = fullTrace.scene; + if (scene.substr(0, 5) === "scene") { + ax = fullLayout[scene][type + "axis"]; } + } else { + ax = exports.getFromId(gd, fullTrace[type + "axis"] || type); + } - return ax; + return ax; }; diff --git a/src/plots/cartesian/category_order_defaults.js b/src/plots/cartesian/category_order_defaults.js index c115dd28c23..e82611c2b9a 100644 --- a/src/plots/cartesian/category_order_defaults.js +++ b/src/plots/cartesian/category_order_defaults.js @@ -5,28 +5,28 @@ * 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 = function handleCategoryOrderDefaults( + containerIn, + containerOut, + coerce +) { + if (containerOut.type !== "category") return; -'use strict'; + var arrayIn = containerIn.categoryarray, orderDefault; + var isValidArray = Array.isArray(arrayIn) && arrayIn.length > 0; -module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, coerce) { - if(containerOut.type !== 'category') return; + // override default 'categoryorder' value when non-empty array is supplied + if (isValidArray) orderDefault = "array"; - var arrayIn = containerIn.categoryarray, - orderDefault; + var order = coerce("categoryorder", orderDefault); - var isValidArray = (Array.isArray(arrayIn) && arrayIn.length > 0); + // coerce 'categoryarray' only in array order case + if (order === "array") coerce("categoryarray"); - // override default 'categoryorder' value when non-empty array is supplied - if(isValidArray) orderDefault = 'array'; - - var order = coerce('categoryorder', orderDefault); - - // coerce 'categoryarray' only in array order case - if(order === 'array') coerce('categoryarray'); - - // cannot set 'categoryorder' to 'array' with an invalid 'categoryarray' - if(!isValidArray && order === 'array') { - containerOut.categoryorder = 'trace'; - } + // cannot set 'categoryorder' to 'array' with an invalid 'categoryarray' + if (!isValidArray && order === "array") { + containerOut.categoryorder = "trace"; + } }; diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 8d7ed20df45..c3643cab011 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -5,68 +5,48 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - - idRegex: { - x: /^x([2-9]|[1-9][0-9]+)?$/, - y: /^y([2-9]|[1-9][0-9]+)?$/ - }, - - attrRegex: { - x: /^xaxis([2-9]|[1-9][0-9]+)?$/, - y: /^yaxis([2-9]|[1-9][0-9]+)?$/ - }, - - // axis match regular expression - xAxisMatch: /^xaxis[0-9]*$/, - yAxisMatch: /^yaxis[0-9]*$/, - - // pattern matching axis ids and names - AX_ID_PATTERN: /^[xyz][0-9]*$/, - AX_NAME_PATTERN: /^[xyz]axis[0-9]*$/, - - // ms between first mousedown and 2nd mouseup to constitute dblclick... - // we don't seem to have access to the system setting - DBLCLICKDELAY: 300, - - // pixels to move mouse before you stop clamping to starting point - MINDRAG: 8, - - // smallest dimension allowed for a select box - MINSELECT: 12, - - // smallest dimension allowed for a zoombox - MINZOOM: 20, - - // width of axis drag regions - DRAGGERSIZE: 20, - - // max pixels away from mouse to allow a point to highlight - MAXDIST: 20, - - // hover labels for multiple horizontal bars get tilted by this angle - YANGLE: 60, - - // size and display constants for hover text - HOVERARROWSIZE: 6, // pixel size of hover arrows - HOVERTEXTPAD: 3, // pixels padding around text - HOVERFONTSIZE: 13, - HOVERFONT: 'Arial, sans-serif', - - // minimum time (msec) between hover calls - HOVERMINTIME: 50, - - // max pixels off straight before a lasso select line counts as bent - BENDPX: 1.5, - - // delay before a redraw (relayout) after smooth panning and zooming - REDRAWDELAY: 50, - - // last resort axis ranges for x and y axes if we have no data - DFLTRANGEX: [-1, 6], - DFLTRANGEY: [-1, 4] + idRegex: { x: /^x([2-9]|[1-9][0-9]+)?$/, y: /^y([2-9]|[1-9][0-9]+)?$/ }, + attrRegex: { + x: /^xaxis([2-9]|[1-9][0-9]+)?$/, + y: /^yaxis([2-9]|[1-9][0-9]+)?$/ + }, + // axis match regular expression + xAxisMatch: /^xaxis[0-9]*$/, + yAxisMatch: /^yaxis[0-9]*$/, + // pattern matching axis ids and names + AX_ID_PATTERN: /^[xyz][0-9]*$/, + AX_NAME_PATTERN: /^[xyz]axis[0-9]*$/, + // ms between first mousedown and 2nd mouseup to constitute dblclick... + // we don't seem to have access to the system setting + DBLCLICKDELAY: 300, + // pixels to move mouse before you stop clamping to starting point + MINDRAG: 8, + // smallest dimension allowed for a select box + MINSELECT: 12, + // smallest dimension allowed for a zoombox + MINZOOM: 20, + // width of axis drag regions + DRAGGERSIZE: 20, + // max pixels away from mouse to allow a point to highlight + MAXDIST: 20, + // hover labels for multiple horizontal bars get tilted by this angle + YANGLE: 60, + // size and display constants for hover text + HOVERARROWSIZE: 6, + // pixel size of hover arrows + HOVERTEXTPAD: 3, + // pixels padding around text + HOVERFONTSIZE: 13, + HOVERFONT: "Arial, sans-serif", + // minimum time (msec) between hover calls + HOVERMINTIME: 50, + // max pixels off straight before a lasso select line counts as bent + BENDPX: 1.5, + // delay before a redraw (relayout) after smooth panning and zooming + REDRAWDELAY: 50, + // last resort axis ranges for x and y axes if we have no data + DFLTRANGEX: [-1, 6], + DFLTRANGEY: [-1, 4] }; diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 22a253f287e..fcc8808315e 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -5,26 +5,22 @@ * 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 d3 = require('d3'); -var tinycolor = require('tinycolor2'); - -var Plotly = require('../../plotly'); -var Registry = require('../../registry'); -var Lib = require('../../lib'); -var svgTextUtils = require('../../lib/svg_text_utils'); -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); -var setCursor = require('../../lib/setcursor'); -var dragElement = require('../../components/dragelement'); - -var Axes = require('./axes'); -var prepSelect = require('./select'); -var constants = require('./constants'); - +"use strict"; +var d3 = require("d3"); +var tinycolor = require("tinycolor2"); + +var Plotly = require("../../plotly"); +var Registry = require("../../registry"); +var Lib = require("../../lib"); +var svgTextUtils = require("../../lib/svg_text_utils"); +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); +var setCursor = require("../../lib/setcursor"); +var dragElement = require("../../components/dragelement"); + +var Axes = require("./axes"); +var prepSelect = require("./select"); +var constants = require("./constants"); // flag for showing "doubleclick to zoom out" only at the beginning var SHOWZOOMOUTTIP = true; @@ -39,725 +35,817 @@ var SHOWZOOMOUTTIP = true; // 'ns' - top and bottom together, difference unchanged // ew - same for horizontal axis module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { - // mouseDown stores ms of first mousedown event in the last - // DBLCLICKDELAY ms on the drag bars - // numClicks stores how many mousedowns have been seen - // 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, - // if we're dragging two axes at once, also drag overlays - subplots = [plotinfo].concat((ns && ew) ? plotinfo.overlays : []), - xa = [plotinfo.xaxis], - ya = [plotinfo.yaxis], - pw = xa[0]._length, - ph = ya[0]._length, - MINDRAG = constants.MINDRAG, - MINZOOM = constants.MINZOOM, - isMainDrag = (ns + ew === 'nsew'); - - 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); - if(ya.indexOf(subplotYa) === -1) ya.push(subplotYa); + // mouseDown stores ms of first mousedown event in the last + // DBLCLICKDELAY ms on the drag bars + // numClicks stores how many mousedowns have been seen + // 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, + // if we're dragging two axes at once, also drag overlays + subplots = [plotinfo].concat(ns && ew ? plotinfo.overlays : []), + xa = [plotinfo.xaxis], + ya = [plotinfo.yaxis], + pw = xa[0]._length, + ph = ya[0]._length, + MINDRAG = constants.MINDRAG, + MINZOOM = constants.MINZOOM, + isMainDrag = ns + ew === "nsew"; + + 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); + if (ya.indexOf(subplotYa) === -1) ya.push(subplotYa); + } + + function isDirectionActive(axList, activeVal) { + for (var i = 0; i < axList.length; i++) { + if (!axList[i].fixedrange) return activeVal; } - - function isDirectionActive(axList, activeVal) { - for(var i = 0; i < axList.length; i++) { - if(!axList[i].fixedrange) return activeVal; - } - return ''; + return ""; + } + + var allaxes = xa.concat(ya), + xActive = isDirectionActive(xa, ew), + yActive = isDirectionActive(ya, ns), + cursor = getDragCursor(yActive + xActive, fullLayout.dragmode), + dragClass = ns + ew + "drag"; + + var dragger3 = plotinfo.draglayer.selectAll("." + dragClass).data([0]); + + dragger3 + .enter() + .append("rect") + .classed("drag", true) + .classed(dragClass, true) + .style({ fill: "transparent", "stroke-width": 0 }) + .attr("data-subplot", plotinfo.id); + + dragger3.call(Drawing.setRect, x, y, w, h).call(setCursor, cursor); + + var dragger = dragger3.node(); + + // 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 + if (!yActive && !xActive && !isSelectOrLasso(fullLayout.dragmode)) { + dragger.onmousedown = null; + dragger.style.pointerEvents = isMainDrag ? "all" : "none"; + return dragger; + } + + var dragOptions = { + element: dragger, + gd: gd, + plotinfo: plotinfo, + xaxes: xa, + yaxes: ya, + doubleclick: doubleClick, + prepFn: function(e, startX, startY) { + var dragModeNow = gd._fullLayout.dragmode; + + if (isMainDrag) { + // main dragger handles all drag modes, and changes + // to pan (or to zoom if it already is pan) on shift + if (e.shiftKey) { + if (dragModeNow === "pan") dragModeNow = "zoom"; + else dragModeNow = "pan"; + } + } else { + // all other draggers just pan + dragModeNow = "pan"; + } + + if (dragModeNow === "lasso") dragOptions.minDrag = 1; + else dragOptions.minDrag = undefined; + + if (dragModeNow === "zoom") { + dragOptions.moveFn = zoomMove; + dragOptions.doneFn = zoomDone; + zoomPrep(e, startX, startY); + } else if (dragModeNow === "pan") { + dragOptions.moveFn = plotDrag; + dragOptions.doneFn = dragDone; + clearSelect(); + } else if (isSelectOrLasso(dragModeNow)) { + prepSelect(e, startX, startY, dragOptions, dragModeNow); + } + } + }; + + dragElement.init(dragOptions); + + var zoomlayer = gd._fullLayout._zoomlayer, + xs = plotinfo.xaxis._offset, + ys = plotinfo.yaxis._offset, + x0, + y0, + box, + lum, + path0, + dimmed, + zoomMode, + zb, + corners; + + function recomputeAxisLists() { + xa = [plotinfo.xaxis]; + ya = [plotinfo.yaxis]; + pw = xa[0]._length; + ph = ya[0]._length; + + 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); + if (ya.indexOf(subplotYa) === -1) ya.push(subplotYa); + } + allaxes = xa.concat(ya); + xActive = isDirectionActive(xa, ew); + yActive = isDirectionActive(ya, ns); + cursor = getDragCursor(yActive + xActive, fullLayout.dragmode); + xs = plotinfo.xaxis._offset; + ys = plotinfo.yaxis._offset; + dragOptions.xa = xa; + dragOptions.ya = ya; + } + + function zoomPrep(e, startX, startY) { + var dragBBox = dragger.getBoundingClientRect(); + x0 = startX - dragBBox.left; + y0 = startY - dragBBox.top; + box = { l: x0, r: x0, w: 0, t: y0, b: y0, h: 0 }; + lum = gd._hmpixcount + ? gd._hmlumcount / gd._hmpixcount + : tinycolor(gd._fullLayout.plot_bgcolor).getLuminance(); + path0 = "M0,0H" + pw + "V" + ph + "H0V0"; + dimmed = false; + zoomMode = "xy"; + + zb = zoomlayer + .append("path") + .attr("class", "zoombox") + .style({ + fill: lum > 0.2 ? "rgba(0,0,0,0)" : "rgba(255,255,255,0)", + "stroke-width": 0 + }) + .attr("transform", "translate(" + xs + ", " + ys + ")") + .attr("d", path0 + "Z"); + + corners = zoomlayer + .append("path") + .attr("class", "zoombox-corners") + .style({ + fill: Color.background, + stroke: Color.defaultLine, + "stroke-width": 1, + opacity: 0 + }) + .attr("transform", "translate(" + xs + ", " + ys + ")") + .attr("d", "M0,0Z"); + + clearSelect(); + } + + 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. + zoomlayer.selectAll(".select-outline").remove(); + } + + function zoomMove(dx0, dy0) { + if (gd._transitioningWithDuration) { + return false; } - var allaxes = xa.concat(ya), - xActive = isDirectionActive(xa, ew), - yActive = isDirectionActive(ya, ns), - cursor = getDragCursor(yActive + xActive, fullLayout.dragmode), - dragClass = ns + ew + 'drag'; + var x1 = Math.max(0, Math.min(pw, dx0 + x0)), + y1 = Math.max(0, Math.min(ph, dy0 + y0)), + dx = Math.abs(x1 - x0), + dy = Math.abs(y1 - y0), + clen = Math.floor(Math.min(dy, dx, MINZOOM) / 2); + + box.l = Math.min(x0, x1); + box.r = Math.max(x0, x1); + box.t = Math.min(y0, y1); + box.b = Math.max(y0, y1); + + // look for small drags in one direction or the other, + // and only drag the other axis + if (!yActive || dy < Math.min(Math.max(dx * 0.6, MINDRAG), MINZOOM)) { + if (dx < MINDRAG) { + zoomMode = ""; + box.r = box.l; + box.t = box.b; + corners.attr("d", "M0,0Z"); + } else { + box.t = 0; + box.b = ph; + zoomMode = "x"; + corners.attr( + "d", + "M" + + (box.l - 0.5) + + "," + + (y0 - MINZOOM - 0.5) + + "h-3v" + + (2 * MINZOOM + 1) + + "h3ZM" + + (box.r + 0.5) + + "," + + (y0 - MINZOOM - 0.5) + + "h3v" + + (2 * MINZOOM + 1) + + "h-3Z" + ); + } + } else if (!xActive || dx < Math.min(dy * 0.6, MINZOOM)) { + box.l = 0; + box.r = pw; + zoomMode = "y"; + corners.attr( + "d", + "M" + + (x0 - MINZOOM - 0.5) + + "," + + (box.t - 0.5) + + "v-3h" + + (2 * MINZOOM + 1) + + "v3ZM" + + (x0 - MINZOOM - 0.5) + + "," + + (box.b + 0.5) + + "v3h" + + (2 * MINZOOM + 1) + + "v-3Z" + ); + } else { + zoomMode = "xy"; + corners.attr( + "d", + "M" + + (box.l - 3.5) + + "," + + (box.t - 0.5 + clen) + + "h3v" + + (-clen) + + "h" + + clen + + "v-3h-" + + (clen + 3) + + "ZM" + + (box.r + 3.5) + + "," + + (box.t - 0.5 + clen) + + "h-3v" + + (-clen) + + "h" + + (-clen) + + "v-3h" + + (clen + 3) + + "ZM" + + (box.r + 3.5) + + "," + + (box.b + 0.5 - clen) + + "h-3v" + + clen + + "h" + + (-clen) + + "v3h" + + (clen + 3) + + "ZM" + + (box.l - 3.5) + + "," + + (box.b + 0.5 - clen) + + "h3v" + + clen + + "h" + + clen + + "v3h-" + + (clen + 3) + + "Z" + ); + } + box.w = box.r - box.l; + box.h = box.b - box.t; + + // Not sure about the addition of window.scrollX/Y... + // seems to work but doesn't seem robust. + zb.attr( + "d", + path0 + + "M" + + box.l + + "," + + box.t + + "v" + + box.h + + "h" + + box.w + + "v-" + + box.h + + "h-" + + box.w + + "Z" + ); + if (!dimmed) { + zb + .transition() + .style("fill", lum > 0.2 ? "rgba(0,0,0,0.4)" : "rgba(255,255,255,0.3)") + .duration(200); + corners.transition().style("opacity", 1).duration(200); + dimmed = true; + } + } - var dragger3 = plotinfo.draglayer.selectAll('.' + dragClass).data([0]); + function zoomAxRanges(axList, r0Fraction, r1Fraction) { + var i, axi, axRangeLinear0, axRangeLinearSpan; - dragger3.enter().append('rect') - .classed('drag', true) - .classed(dragClass, true) - .style({fill: 'transparent', 'stroke-width': 0}) - .attr('data-subplot', plotinfo.id); + for (i = 0; i < axList.length; i++) { + axi = axList[i]; + if (axi.fixedrange) continue; - dragger3.call(Drawing.setRect, x, y, w, h) - .call(setCursor, cursor); + axRangeLinear0 = axi._rl[0]; + axRangeLinearSpan = axi._rl[1] - axRangeLinear0; + axi.range = [ + axi.l2r(axRangeLinear0 + axRangeLinearSpan * r0Fraction), + axi.l2r(axRangeLinear0 + axRangeLinearSpan * r1Fraction) + ]; + } + } - var dragger = dragger3.node(); + function zoomDone(dragged, numClicks) { + if (Math.min(box.h, box.w) < MINDRAG * 2) { + if (numClicks === 2) doubleClick(); - // 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 - if(!yActive && !xActive && !isSelectOrLasso(fullLayout.dragmode)) { - dragger.onmousedown = null; - dragger.style.pointerEvents = isMainDrag ? 'all' : 'none'; - return dragger; + return removeZoombox(gd); } - var dragOptions = { - element: dragger, - gd: gd, - plotinfo: plotinfo, - xaxes: xa, - yaxes: ya, - doubleclick: doubleClick, - prepFn: function(e, startX, startY) { - var dragModeNow = gd._fullLayout.dragmode; - - if(isMainDrag) { - // main dragger handles all drag modes, and changes - // to pan (or to zoom if it already is pan) on shift - if(e.shiftKey) { - if(dragModeNow === 'pan') dragModeNow = 'zoom'; - else dragModeNow = 'pan'; - } - } - // all other draggers just pan - else dragModeNow = 'pan'; - - if(dragModeNow === 'lasso') dragOptions.minDrag = 1; - else dragOptions.minDrag = undefined; - - if(dragModeNow === 'zoom') { - dragOptions.moveFn = zoomMove; - dragOptions.doneFn = zoomDone; - zoomPrep(e, startX, startY); - } - else if(dragModeNow === 'pan') { - dragOptions.moveFn = plotDrag; - dragOptions.doneFn = dragDone; - clearSelect(); - } - else if(isSelectOrLasso(dragModeNow)) { - prepSelect(e, startX, startY, dragOptions, dragModeNow); - } - } - }; - - dragElement.init(dragOptions); - - var zoomlayer = gd._fullLayout._zoomlayer, - xs = plotinfo.xaxis._offset, - ys = plotinfo.yaxis._offset, - x0, - y0, - box, - lum, - path0, - dimmed, - zoomMode, - zb, - corners; - - function recomputeAxisLists() { - xa = [plotinfo.xaxis]; - ya = [plotinfo.yaxis]; - pw = xa[0]._length; - ph = ya[0]._length; - - 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); - if(ya.indexOf(subplotYa) === -1) ya.push(subplotYa); - } - allaxes = xa.concat(ya); - xActive = isDirectionActive(xa, ew); - yActive = isDirectionActive(ya, ns); - cursor = getDragCursor(yActive + xActive, fullLayout.dragmode); - xs = plotinfo.xaxis._offset; - ys = plotinfo.yaxis._offset; - dragOptions.xa = xa; - dragOptions.ya = ya; + if (zoomMode === "xy" || zoomMode === "x") { + zoomAxRanges(xa, box.l / pw, box.r / pw); + } + if (zoomMode === "xy" || zoomMode === "y") { + zoomAxRanges(ya, (ph - box.b) / ph, (ph - box.t) / ph); } - function zoomPrep(e, startX, startY) { - var dragBBox = dragger.getBoundingClientRect(); - x0 = startX - dragBBox.left; - y0 = startY - dragBBox.top; - box = {l: x0, r: x0, w: 0, t: y0, b: y0, h: 0}; - lum = gd._hmpixcount ? - (gd._hmlumcount / gd._hmpixcount) : - tinycolor(gd._fullLayout.plot_bgcolor).getLuminance(); - path0 = 'M0,0H' + pw + 'V' + ph + 'H0V0'; - dimmed = false; - zoomMode = 'xy'; - - zb = zoomlayer.append('path') - .attr('class', 'zoombox') - .style({ - 'fill': lum > 0.2 ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)', - 'stroke-width': 0 - }) - .attr('transform', 'translate(' + xs + ', ' + ys + ')') - .attr('d', path0 + 'Z'); - - corners = zoomlayer.append('path') - .attr('class', 'zoombox-corners') - .style({ - fill: Color.background, - stroke: Color.defaultLine, - 'stroke-width': 1, - opacity: 0 - }) - .attr('transform', 'translate(' + xs + ', ' + ys + ')') - .attr('d', 'M0,0Z'); + removeZoombox(gd); + dragTail(zoomMode); - clearSelect(); + if (SHOWZOOMOUTTIP && gd.data && gd._context.showTips) { + Lib.notifier("Double-click to
zoom back out", "long"); + SHOWZOOMOUTTIP = false; } - - 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. - zoomlayer.selectAll('.select-outline').remove(); + } + + function dragDone(dragged, numClicks) { + var singleEnd = (ns + ew).length === 1; + if (dragged) { + dragTail(); + } else if (numClicks === 2 && !singleEnd) { + doubleClick(); + } else if (numClicks === 1 && singleEnd) { + var ax = ns ? ya[0] : xa[0], + end = ns === "s" || ew === "w" ? 0 : 1, + attrStr = ax._name + ".range[" + end + "]", + initialText = getEndText(ax, end), + hAlign = "left", + vAlign = "middle"; + + if (ax.fixedrange) return; + + if (ns) { + vAlign = ns === "n" ? "top" : "bottom"; + if (ax.side === "right") hAlign = "right"; + } else if (ew === "e") hAlign = "right"; + + dragger3 + .call(svgTextUtils.makeEditable, null, { + immediate: true, + background: fullLayout.paper_bgcolor, + text: String(initialText), + fill: ax.tickfont ? ax.tickfont.color : "#444", + horizontalAlign: hAlign, + verticalAlign: vAlign + }) + .on("edit", function(text) { + var v = ax.d2r(text); + if (v !== undefined) { + Plotly.relayout(gd, attrStr, v); + } + }); } - - function zoomMove(dx0, dy0) { - if(gd._transitioningWithDuration) { - return false; - } - - var x1 = Math.max(0, Math.min(pw, dx0 + x0)), - y1 = Math.max(0, Math.min(ph, dy0 + y0)), - dx = Math.abs(x1 - x0), - dy = Math.abs(y1 - y0), - clen = Math.floor(Math.min(dy, dx, MINZOOM) / 2); - - box.l = Math.min(x0, x1); - box.r = Math.max(x0, x1); - box.t = Math.min(y0, y1); - box.b = Math.max(y0, y1); - - // look for small drags in one direction or the other, - // and only drag the other axis - if(!yActive || dy < Math.min(Math.max(dx * 0.6, MINDRAG), MINZOOM)) { - if(dx < MINDRAG) { - zoomMode = ''; - box.r = box.l; - box.t = box.b; - corners.attr('d', 'M0,0Z'); - } - else { - box.t = 0; - box.b = ph; - zoomMode = 'x'; - corners.attr('d', - 'M' + (box.l - 0.5) + ',' + (y0 - MINZOOM - 0.5) + - 'h-3v' + (2 * MINZOOM + 1) + 'h3ZM' + - (box.r + 0.5) + ',' + (y0 - MINZOOM - 0.5) + - 'h3v' + (2 * MINZOOM + 1) + 'h-3Z'); - } - } - else if(!xActive || dx < Math.min(dy * 0.6, MINZOOM)) { - box.l = 0; - box.r = pw; - zoomMode = 'y'; - corners.attr('d', - 'M' + (x0 - MINZOOM - 0.5) + ',' + (box.t - 0.5) + - 'v-3h' + (2 * MINZOOM + 1) + 'v3ZM' + - (x0 - MINZOOM - 0.5) + ',' + (box.b + 0.5) + - 'v3h' + (2 * MINZOOM + 1) + 'v-3Z'); - } - else { - zoomMode = 'xy'; - corners.attr('d', - 'M' + (box.l - 3.5) + ',' + (box.t - 0.5 + clen) + 'h3v' + (-clen) + - 'h' + clen + 'v-3h-' + (clen + 3) + 'ZM' + - (box.r + 3.5) + ',' + (box.t - 0.5 + clen) + 'h-3v' + (-clen) + - 'h' + (-clen) + 'v-3h' + (clen + 3) + 'ZM' + - (box.r + 3.5) + ',' + (box.b + 0.5 - clen) + 'h-3v' + clen + - 'h' + (-clen) + 'v3h' + (clen + 3) + 'ZM' + - (box.l - 3.5) + ',' + (box.b + 0.5 - clen) + 'h3v' + clen + - 'h' + clen + 'v3h-' + (clen + 3) + 'Z'); - } - box.w = box.r - box.l; - box.h = box.b - box.t; - - // Not sure about the addition of window.scrollX/Y... - // seems to work but doesn't seem robust. - zb.attr('d', - path0 + 'M' + (box.l) + ',' + (box.t) + 'v' + (box.h) + - 'h' + (box.w) + 'v-' + (box.h) + 'h-' + (box.w) + 'Z'); - if(!dimmed) { - zb.transition() - .style('fill', lum > 0.2 ? 'rgba(0,0,0,0.4)' : - 'rgba(255,255,255,0.3)') - .duration(200); - corners.transition() - .style('opacity', 1) - .duration(200); - dimmed = true; - } + } + + // scroll zoom, on all draggers except corners + var scrollViewBox = [0, 0, pw, ph], + // wait a little after scrolling before redrawing + redrawTimer = null, + REDRAWDELAY = constants.REDRAWDELAY, + mainplot = plotinfo.mainplot + ? 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) { + return; } - function zoomAxRanges(axList, r0Fraction, r1Fraction) { - var i, - axi, - axRangeLinear0, - axRangeLinearSpan; - - for(i = 0; i < axList.length; i++) { - axi = axList[i]; - if(axi.fixedrange) continue; - - axRangeLinear0 = axi._rl[0]; - axRangeLinearSpan = axi._rl[1] - axRangeLinear0; - axi.range = [ - axi.l2r(axRangeLinear0 + axRangeLinearSpan * r0Fraction), - axi.l2r(axRangeLinear0 + axRangeLinearSpan * r1Fraction) - ]; - } + // If a transition is in progress, then disable any behavior: + if (gd._transitioningWithDuration) { + return Lib.pauseEvent(e); } - function zoomDone(dragged, numClicks) { - if(Math.min(box.h, box.w) < MINDRAG * 2) { - if(numClicks === 2) doubleClick(); + var pc = gd.querySelector(".plotly"); - return removeZoombox(gd); - } + recomputeAxisLists(); - if(zoomMode === 'xy' || zoomMode === 'x') zoomAxRanges(xa, box.l / pw, box.r / pw); - if(zoomMode === 'xy' || zoomMode === 'y') zoomAxRanges(ya, (ph - box.b) / ph, (ph - box.t) / ph); + // if the plot has scrollbars (more than a tiny excess) + // disable scrollzoom too. + if ( + pc.scrollHeight - pc.clientHeight > 10 || + pc.scrollWidth - pc.clientWidth > 10 + ) { + return; + } - removeZoombox(gd); - dragTail(zoomMode); + clearTimeout(redrawTimer); - if(SHOWZOOMOUTTIP && gd.data && gd._context.showTips) { - Lib.notifier('Double-click to
zoom back out', 'long'); - SHOWZOOMOUTTIP = false; - } + var wheelDelta = -e.deltaY; + if (!isFinite(wheelDelta)) wheelDelta = e.wheelDelta / 10; + if (!isFinite(wheelDelta)) { + Lib.log("Did not find wheel motion attributes: ", e); + return; } - function dragDone(dragged, numClicks) { - var singleEnd = (ns + ew).length === 1; - if(dragged) dragTail(); - else if(numClicks === 2 && !singleEnd) doubleClick(); - else if(numClicks === 1 && singleEnd) { - var ax = ns ? ya[0] : xa[0], - end = (ns === 's' || ew === 'w') ? 0 : 1, - attrStr = ax._name + '.range[' + end + ']', - initialText = getEndText(ax, end), - hAlign = 'left', - vAlign = 'middle'; - - if(ax.fixedrange) return; - - if(ns) { - vAlign = (ns === 'n') ? 'top' : 'bottom'; - if(ax.side === 'right') hAlign = 'right'; - } - else if(ew === 'e') hAlign = 'right'; - - dragger3 - .call(svgTextUtils.makeEditable, null, { - immediate: true, - background: fullLayout.paper_bgcolor, - text: String(initialText), - fill: ax.tickfont ? ax.tickfont.color : '#444', - horizontalAlign: hAlign, - verticalAlign: vAlign - }) - .on('edit', function(text) { - var v = ax.d2r(text); - if(v !== undefined) { - Plotly.relayout(gd, attrStr, v); - } - }); - } + var zoom = Math.exp((-Math.min(Math.max(wheelDelta, -20), 20)) / 100), + gbb = mainplot.draglayer + .select(".nsewdrag") + .node() + .getBoundingClientRect(), + xfrac = (e.clientX - gbb.left) / gbb.width, + vbx0 = scrollViewBox[0] + scrollViewBox[2] * xfrac, + yfrac = (gbb.bottom - e.clientY) / gbb.height, + vby0 = scrollViewBox[1] + scrollViewBox[3] * (1 - yfrac), + i; + + function zoomWheelOneAxis(ax, centerFraction, zoom) { + if (ax.fixedrange) return; + + var axRange = Lib.simpleMap(ax.range, ax.r2l), + v0 = axRange[0] + (axRange[1] - axRange[0]) * centerFraction; + function doZoom(v) { + return ax.l2r(v0 + (v - v0) * zoom); + } + ax.range = axRange.map(doZoom); } - // scroll zoom, on all draggers except corners - var scrollViewBox = [0, 0, pw, ph], - // wait a little after scrolling before redrawing - redrawTimer = null, - REDRAWDELAY = constants.REDRAWDELAY, - mainplot = plotinfo.mainplot ? - 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) { - return; - } - - // If a transition is in progress, then disable any behavior: - if(gd._transitioningWithDuration) { - return Lib.pauseEvent(e); - } - - var pc = gd.querySelector('.plotly'); - - recomputeAxisLists(); - - // if the plot has scrollbars (more than a tiny excess) - // disable scrollzoom too. - if(pc.scrollHeight - pc.clientHeight > 10 || - pc.scrollWidth - pc.clientWidth > 10) { - return; - } - - clearTimeout(redrawTimer); - - var wheelDelta = -e.deltaY; - if(!isFinite(wheelDelta)) wheelDelta = e.wheelDelta / 10; - if(!isFinite(wheelDelta)) { - Lib.log('Did not find wheel motion attributes: ', e); - return; - } - - var zoom = Math.exp(-Math.min(Math.max(wheelDelta, -20), 20) / 100), - gbb = mainplot.draglayer.select('.nsewdrag') - .node().getBoundingClientRect(), - xfrac = (e.clientX - gbb.left) / gbb.width, - vbx0 = scrollViewBox[0] + scrollViewBox[2] * xfrac, - yfrac = (gbb.bottom - e.clientY) / gbb.height, - vby0 = scrollViewBox[1] + scrollViewBox[3] * (1 - yfrac), - i; - - function zoomWheelOneAxis(ax, centerFraction, zoom) { - if(ax.fixedrange) return; - - var axRange = Lib.simpleMap(ax.range, ax.r2l), - v0 = axRange[0] + (axRange[1] - axRange[0]) * centerFraction; - function doZoom(v) { return ax.l2r(v0 + (v - v0) * zoom); } - ax.range = axRange.map(doZoom); - } - - if(ew) { - for(i = 0; i < xa.length; i++) zoomWheelOneAxis(xa[i], xfrac, zoom); - scrollViewBox[2] *= zoom; - scrollViewBox[0] = vbx0 - scrollViewBox[2] * xfrac; - } - if(ns) { - for(i = 0; i < ya.length; i++) zoomWheelOneAxis(ya[i], yfrac, zoom); - scrollViewBox[3] *= zoom; - scrollViewBox[1] = vby0 - scrollViewBox[3] * (1 - yfrac); - } - - // viewbox redraw at first - updateSubplots(scrollViewBox); - ticksAndAnnotations(ns, ew); - - // then replot after a delay to make sure - // no more scrolling is coming - redrawTimer = setTimeout(function() { - scrollViewBox = [0, 0, pw, ph]; - dragTail(); - }, REDRAWDELAY); - - return Lib.pauseEvent(e); + if (ew) { + for (i = 0; i < xa.length; i++) { + zoomWheelOneAxis(xa[i], xfrac, zoom); + } + scrollViewBox[2] *= zoom; + scrollViewBox[0] = vbx0 - scrollViewBox[2] * xfrac; } - - // everything but the corners gets wheel zoom - if(ns.length * ew.length !== 1) { - // still seems to be some confusion about onwheel vs onmousewheel... - if(dragger.onwheel !== undefined) dragger.onwheel = zoomWheel; - else if(dragger.onmousewheel !== undefined) dragger.onmousewheel = zoomWheel; + if (ns) { + for (i = 0; i < ya.length; i++) { + zoomWheelOneAxis(ya[i], yfrac, zoom); + } + scrollViewBox[3] *= zoom; + scrollViewBox[1] = vby0 - scrollViewBox[3] * (1 - yfrac); } - // plotDrag: move the plot in response to a drag - function plotDrag(dx, dy) { - // If a transition is in progress, then disable any behavior: - if(gd._transitioningWithDuration) { - return; - } - - recomputeAxisLists(); - - function dragAxList(axList, pix) { - for(var i = 0; i < axList.length; i++) { - var axi = axList[i]; - if(!axi.fixedrange) { - axi.range = [ - axi.l2r(axi._rl[0] - pix / axi._m), - axi.l2r(axi._rl[1] - pix / axi._m) - ]; - } - } - } - - if(xActive === 'ew' || yActive === 'ns') { - if(xActive) dragAxList(xa, dx); - if(yActive) dragAxList(ya, dy); - updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]); - ticksAndAnnotations(yActive, xActive); - return; - } - - // common transform for dragging one end of an axis - // d>0 is compressing scale (cursor is over the plot, - // the axis end should move with the cursor) - // d<0 is expanding (cursor is off the plot, axis end moves - // nonlinearly so you can expand far) - function dZoom(d) { - return 1 - ((d >= 0) ? Math.min(d, 0.9) : - 1 / (1 / Math.max(d, -0.3) + 3.222)); - } - - // dz: set a new value for one end (0 or 1) of an axis array axArray, - // and return a pixel shift for that end for the viewbox - // based on pixel drag distance d - // TODO: this makes (generally non-fatal) errors when you get - // near floating point limits - function dz(axArray, end, d) { - var otherEnd = 1 - end, - movedAx, - newLinearizedEnd; - for(var i = 0; i < axArray.length; i++) { - var axi = axArray[i]; - if(axi.fixedrange) continue; - movedAx = axi; - newLinearizedEnd = axi._rl[otherEnd] + - (axi._rl[end] - axi._rl[otherEnd]) / dZoom(d / axi._length); - var newEnd = axi.l2r(newLinearizedEnd); - - // if l2r comes back false or undefined, it means we've dragged off - // the end of valid ranges - so stop. - if(newEnd !== false && newEnd !== undefined) axi.range[end] = newEnd; - } - return movedAx._length * (movedAx._rl[end] - newLinearizedEnd) / - (movedAx._rl[end] - movedAx._rl[otherEnd]); - } - - if(xActive === 'w') dx = dz(xa, 0, dx); - else if(xActive === 'e') dx = dz(xa, 1, -dx); - else if(!xActive) dx = 0; - - if(yActive === 'n') dy = dz(ya, 1, dy); - else if(yActive === 's') dy = dz(ya, 0, -dy); - else if(!yActive) dy = 0; - - updateSubplots([ - (xActive === 'w') ? dx : 0, - (yActive === 'n') ? dy : 0, - pw - dx, - ph - dy - ]); - ticksAndAnnotations(yActive, xActive); + // viewbox redraw at first + updateSubplots(scrollViewBox); + ticksAndAnnotations(ns, ew); + + // then replot after a delay to make sure + // no more scrolling is coming + redrawTimer = setTimeout( + function() { + scrollViewBox = [0, 0, pw, ph]; + dragTail(); + }, + REDRAWDELAY + ); + + return Lib.pauseEvent(e); + } + + // everything but the corners gets wheel zoom + if (ns.length * ew.length !== 1) { + // still seems to be some confusion about onwheel vs onmousewheel... + if (dragger.onwheel !== undefined) { + dragger.onwheel = zoomWheel; + } else if (dragger.onmousewheel !== undefined) { + dragger.onmousewheel = zoomWheel; } + } - function ticksAndAnnotations(ns, ew) { - var activeAxIds = [], - i; - - function pushActiveAxIds(axList) { - for(i = 0; i < axList.length; i++) { - if(!axList[i].fixedrange) activeAxIds.push(axList[i]._id); - } - } + // plotDrag: move the plot in response to a drag + function plotDrag(dx, dy) { + // If a transition is in progress, then disable any behavior: + if (gd._transitioningWithDuration) { + return; + } - if(ew) pushActiveAxIds(xa); - if(ns) pushActiveAxIds(ya); + recomputeAxisLists(); - for(i = 0; i < activeAxIds.length; i++) { - Axes.doTicks(gd, activeAxIds[i], true); + function dragAxList(axList, pix) { + for (var i = 0; i < axList.length; i++) { + var axi = axList[i]; + if (!axi.fixedrange) { + axi.range = [ + axi.l2r(axi._rl[0] - pix / axi._m), + axi.l2r(axi._rl[1] - pix / axi._m) + ]; } + } + } - function redrawObjs(objArray, method) { - for(i = 0; i < objArray.length; i++) { - var obji = objArray[i]; + if (xActive === "ew" || yActive === "ns") { + if (xActive) dragAxList(xa, dx); + if (yActive) dragAxList(ya, dy); + updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]); + ticksAndAnnotations(yActive, xActive); + return; + } - if((ew && activeAxIds.indexOf(obji.xref) !== -1) || - (ns && activeAxIds.indexOf(obji.yref) !== -1)) { - method(gd, i); - } - } - } + // common transform for dragging one end of an axis + // d>0 is compressing scale (cursor is over the plot, + // the axis end should move with the cursor) + // d<0 is expanding (cursor is off the plot, axis end moves + // nonlinearly so you can expand far) + function dZoom(d) { + return 1 - + (d >= 0 ? Math.min(d, 0.9) : 1 / (1 / Math.max(d, -0.3) + 3.222)); + } - // annotations and shapes 'draw' method is slow, - // use the finer-grained 'drawOne' method instead + // dz: set a new value for one end (0 or 1) of an axis array axArray, + // and return a pixel shift for that end for the viewbox + // based on pixel drag distance d + // TODO: this makes (generally non-fatal) errors when you get + // near floating point limits + function dz(axArray, end, d) { + var otherEnd = 1 - end, movedAx, newLinearizedEnd; + for (var i = 0; i < axArray.length; i++) { + var axi = axArray[i]; + if (axi.fixedrange) continue; + movedAx = axi; + newLinearizedEnd = axi._rl[otherEnd] + + (axi._rl[end] - axi._rl[otherEnd]) / dZoom(d / axi._length); + var newEnd = axi.l2r(newLinearizedEnd); + + // if l2r comes back false or undefined, it means we've dragged off + // the end of valid ranges - so stop. + if (newEnd !== false && newEnd !== undefined) axi.range[end] = newEnd; + } + return movedAx._length * + (movedAx._rl[end] - newLinearizedEnd) / + (movedAx._rl[end] - movedAx._rl[otherEnd]); + } - redrawObjs(fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne')); - redrawObjs(fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne')); - redrawObjs(fullLayout.images || [], Registry.getComponentMethod('images', 'draw')); + if (xActive === "w") dx = dz(xa, 0, dx); + else if (xActive === "e") dx = dz(xa, 1, -dx); + else if (!xActive) dx = 0; + + if (yActive === "n") dy = dz(ya, 1, dy); + else if (yActive === "s") dy = dz(ya, 0, -dy); + else if (!yActive) dy = 0; + + updateSubplots([ + xActive === "w" ? dx : 0, + yActive === "n" ? dy : 0, + pw - dx, + ph - dy + ]); + ticksAndAnnotations(yActive, xActive); + } + + function ticksAndAnnotations(ns, ew) { + var activeAxIds = [], i; + + function pushActiveAxIds(axList) { + for (i = 0; i < axList.length; i++) { + if (!axList[i].fixedrange) activeAxIds.push(axList[i]._id); + } } - function doubleClick() { - if(gd._transitioningWithDuration) return; + if (ew) pushActiveAxIds(xa); + if (ns) pushActiveAxIds(ya); - var doubleClickConfig = gd._context.doubleClick, - axList = (xActive ? xa : []).concat(yActive ? ya : []), - attrs = {}; + for (i = 0; i < activeAxIds.length; i++) { + Axes.doTicks(gd, activeAxIds[i], true); + } - var ax, i, rangeInitial; + function redrawObjs(objArray, method) { + for (i = 0; i < objArray.length; i++) { + var obji = objArray[i]; - if(doubleClickConfig === 'autosize') { - for(i = 0; i < axList.length; i++) { - ax = axList[i]; - if(!ax.fixedrange) attrs[ax._name + '.autorange'] = true; - } - } - else if(doubleClickConfig === 'reset') { - for(i = 0; i < axList.length; i++) { - ax = axList[i]; - - if(!ax._rangeInitial) { - attrs[ax._name + '.autorange'] = true; - } - else { - rangeInitial = ax._rangeInitial.slice(); - attrs[ax._name + '.range[0]'] = rangeInitial[0]; - attrs[ax._name + '.range[1]'] = rangeInitial[1]; - } - } + if ( + ew && activeAxIds.indexOf(obji.xref) !== -1 || + ns && activeAxIds.indexOf(obji.yref) !== -1 + ) { + method(gd, i); } - else if(doubleClickConfig === 'reset+autosize') { - for(i = 0; i < axList.length; i++) { - ax = axList[i]; - - if(ax.fixedrange) continue; - if(ax._rangeInitial === undefined || - ax.range[0] === ax._rangeInitial[0] && - ax.range[1] === ax._rangeInitial[1] - ) { - attrs[ax._name + '.autorange'] = true; - } - else { - rangeInitial = ax._rangeInitial.slice(); - attrs[ax._name + '.range[0]'] = rangeInitial[0]; - attrs[ax._name + '.range[1]'] = rangeInitial[1]; - } - } - } - - gd.emit('plotly_doubleclick', null); - Plotly.relayout(gd, attrs); + } } - // dragTail - finish a drag event with a redraw - function dragTail(zoommode) { - var attrs = {}; - // revert to the previous axis settings, then apply the new ones - // through relayout - this lets relayout manage undo/redo - for(var i = 0; i < allaxes.length; i++) { - var axi = allaxes[i]; - if(zoommode && zoommode.indexOf(axi._id.charAt(0)) === -1) { - continue; - } - if(axi._r[0] !== axi.range[0]) attrs[axi._name + '.range[0]'] = axi.range[0]; - if(axi._r[1] !== axi.range[1]) attrs[axi._name + '.range[1]'] = axi.range[1]; - - axi.range = axi._r.slice(); - } + // 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") + ); + } + + function doubleClick() { + if (gd._transitioningWithDuration) return; + + var doubleClickConfig = gd._context.doubleClick, + axList = (xActive ? xa : []).concat(yActive ? ya : []), + attrs = {}; + + var ax, i, rangeInitial; + + if (doubleClickConfig === "autosize") { + for (i = 0; i < axList.length; i++) { + ax = axList[i]; + if (!ax.fixedrange) attrs[ax._name + ".autorange"] = true; + } + } else if (doubleClickConfig === "reset") { + for (i = 0; i < axList.length; i++) { + ax = axList[i]; + + if (!ax._rangeInitial) { + attrs[ax._name + ".autorange"] = true; + } else { + rangeInitial = ax._rangeInitial.slice(); + attrs[ax._name + ".range[0]"] = rangeInitial[0]; + attrs[ax._name + ".range[1]"] = rangeInitial[1]; + } + } + } else if (doubleClickConfig === "reset+autosize") { + for (i = 0; i < axList.length; i++) { + ax = axList[i]; + + if (ax.fixedrange) continue; + if ( + ax._rangeInitial === undefined || + ax.range[0] === ax._rangeInitial[0] && + ax.range[1] === ax._rangeInitial[1] + ) { + attrs[ax._name + ".autorange"] = true; + } else { + rangeInitial = ax._rangeInitial.slice(); + attrs[ax._name + ".range[0]"] = rangeInitial[0]; + attrs[ax._name + ".range[1]"] = rangeInitial[1]; + } + } + } - updateSubplots([0, 0, pw, ph]); - Plotly.relayout(gd, attrs); + gd.emit("plotly_doubleclick", null); + Plotly.relayout(gd, attrs); + } + + // dragTail - finish a drag event with a redraw + function dragTail(zoommode) { + var attrs = {}; + // revert to the previous axis settings, then apply the new ones + // through relayout - this lets relayout manage undo/redo + for (var i = 0; i < allaxes.length; i++) { + var axi = allaxes[i]; + if (zoommode && zoommode.indexOf(axi._id.charAt(0)) === -1) { + continue; + } + if (axi._r[0] !== axi.range[0]) { + attrs[axi._name + ".range[0]"] = axi.range[0]; + } + if (axi._r[1] !== axi.range[1]) { + attrs[axi._name + ".range[1]"] = axi.range[1]; + } + + axi.range = axi._r.slice(); } - // 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) - function updateSubplots(viewBox) { - var j; - var plotinfos = fullLayout._plots, - subplots = Object.keys(plotinfos); - - for(var i = 0; i < subplots.length; i++) { - - var subplot = plotinfos[subplots[i]], - xa2 = subplot.xaxis, - ya2 = subplot.yaxis, - editX = ew && !xa2.fixedrange, - editY = ns && !ya2.fixedrange; - - if(editX) { - var isInX = false; - for(j = 0; j < xa.length; j++) { - if(xa[j]._id === xa2._id) { - isInX = true; - break; - } - } - editX = editX && isInX; - } - - if(editY) { - var isInY = false; - for(j = 0; j < ya.length; j++) { - if(ya[j]._id === ya2._id) { - isInY = true; - break; - } - } - editY = editY && isInY; - } - - var xScaleFactor = editX ? xa2._length / viewBox[2] : 1, - yScaleFactor = editY ? ya2._length / viewBox[3] : 1; - - var clipDx = editX ? viewBox[0] : 0, - clipDy = editY ? viewBox[1] : 0; - - var fracDx = editX ? (viewBox[0] / viewBox[2] * xa2._length) : 0, - fracDy = editY ? (viewBox[1] / viewBox[3] * ya2._length) : 0; - - var plotDx = xa2._offset - fracDx, - plotDy = ya2._offset - fracDy; - - fullLayout._defs.selectAll('#' + subplot.clipId) - .call(Drawing.setTranslate, clipDx, clipDy) - .call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor); - - subplot.plot - .call(Drawing.setTranslate, plotDx, plotDy) - .call(Drawing.setScale, xScaleFactor, yScaleFactor) - - // This is specifically directed at scatter traces, applying an inverse - // scale to individual points to counteract the scale of the trace - // as a whole: - .select('.scatterlayer').selectAll('.points').selectAll('.point') - .call(Drawing.setPointGroupScale, 1 / xScaleFactor, 1 / yScaleFactor); - } + updateSubplots([0, 0, pw, ph]); + Plotly.relayout(gd, attrs); + } + + // 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) + function updateSubplots(viewBox) { + var j; + var plotinfos = fullLayout._plots, subplots = Object.keys(plotinfos); + + for (var i = 0; i < subplots.length; i++) { + var subplot = plotinfos[subplots[i]], + xa2 = subplot.xaxis, + ya2 = subplot.yaxis, + editX = ew && !xa2.fixedrange, + editY = ns && !ya2.fixedrange; + + if (editX) { + var isInX = false; + for (j = 0; j < xa.length; j++) { + if (xa[j]._id === xa2._id) { + isInX = true; + break; + } + } + editX = editX && isInX; + } + + if (editY) { + var isInY = false; + for (j = 0; j < ya.length; j++) { + if (ya[j]._id === ya2._id) { + isInY = true; + break; + } + } + editY = editY && isInY; + } + + var xScaleFactor = editX ? xa2._length / viewBox[2] : 1, + yScaleFactor = editY ? ya2._length / viewBox[3] : 1; + + var clipDx = editX ? viewBox[0] : 0, clipDy = editY ? viewBox[1] : 0; + + var fracDx = editX ? viewBox[0] / viewBox[2] * xa2._length : 0, + fracDy = editY ? viewBox[1] / viewBox[3] * ya2._length : 0; + + var plotDx = xa2._offset - fracDx, plotDy = ya2._offset - fracDy; + + fullLayout._defs + .selectAll("#" + subplot.clipId) + .call(Drawing.setTranslate, clipDx, clipDy) + .call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor); + + subplot.plot + .call(Drawing.setTranslate, plotDx, plotDy) + .call(Drawing.setScale, xScaleFactor, yScaleFactor) + .select(".scatterlayer") + .selectAll(".points") + .selectAll(".point") + .call(Drawing.setPointGroupScale, 1 / xScaleFactor, 1 / yScaleFactor); } + } - return dragger; + return dragger; }; function getEndText(ax, end) { - var initialVal = ax.range[end], - diff = Math.abs(initialVal - ax.range[1 - end]), - dig; - - // TODO: this should basically be ax.r2d but we're doing extra - // rounding here... can we clean up at all? - if(ax.type === 'date') { - return initialVal; - } - else if(ax.type === 'log') { - dig = Math.ceil(Math.max(0, -Math.log(diff) / Math.LN10)) + 3; - return d3.format('.' + dig + 'g')(Math.pow(10, initialVal)); - } - else { // linear numeric (or category... but just show numbers here) - dig = Math.floor(Math.log(Math.abs(initialVal)) / Math.LN10) - - Math.floor(Math.log(diff) / Math.LN10) + 4; - return d3.format('.' + String(dig) + 'g')(initialVal); - } + var initialVal = ax.range[end], + diff = Math.abs(initialVal - ax.range[1 - end]), + dig; + + // TODO: this should basically be ax.r2d but we're doing extra + // rounding here... can we clean up at all? + if (ax.type === "date") { + return initialVal; + } else if (ax.type === "log") { + dig = Math.ceil(Math.max(0, (-Math.log(diff)) / Math.LN10)) + 3; + return d3.format("." + dig + "g")(Math.pow(10, initialVal)); + } else { + // linear numeric (or category... but just show numbers here) + dig = Math.floor(Math.log(Math.abs(initialVal)) / Math.LN10) - + Math.floor(Math.log(diff) / Math.LN10) + + 4; + return d3.format("." + String(dig) + "g")(initialVal); + } } function getDragCursor(nsew, dragmode) { - if(!nsew) return 'pointer'; - if(nsew === 'nsew') { - if(dragmode === 'pan') return 'move'; - return 'crosshair'; - } - return nsew.toLowerCase() + '-resize'; + if (!nsew) return "pointer"; + if (nsew === "nsew") { + if (dragmode === "pan") return "move"; + return "crosshair"; + } + return nsew.toLowerCase() + "-resize"; } function removeZoombox(gd) { - d3.select(gd) - .selectAll('.zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners') - .remove(); + d3 + .select(gd) + .selectAll( + ".zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners" + ) + .remove(); } function isSelectOrLasso(dragmode) { - var modes = ['lasso', 'select']; + var modes = ["lasso", "select"]; - return modes.indexOf(dragmode) !== -1; + return modes.indexOf(dragmode) !== -1; } diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 7fea9cb893e..ffcd6639b15 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -5,28 +5,24 @@ * 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 d3 = require('d3'); -var tinycolor = require('tinycolor2'); -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Events = require('../../lib/events'); -var svgTextUtils = require('../../lib/svg_text_utils'); -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); -var dragElement = require('../../components/dragelement'); -var overrideCursor = require('../../lib/override_cursor'); -var Registry = require('../../registry'); - -var Axes = require('./axes'); -var constants = require('./constants'); -var dragBox = require('./dragbox'); -var layoutAttributes = require('../layout_attributes'); - +"use strict"; +var d3 = require("d3"); +var tinycolor = require("tinycolor2"); +var isNumeric = require("fast-isnumeric"); + +var Lib = require("../../lib"); +var Events = require("../../lib/events"); +var svgTextUtils = require("../../lib/svg_text_utils"); +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); +var dragElement = require("../../components/dragelement"); +var overrideCursor = require("../../lib/override_cursor"); +var Registry = require("../../registry"); + +var Axes = require("./axes"); +var constants = require("./constants"); +var dragBox = require("./dragbox"); +var layoutAttributes = require("../layout_attributes"); var fx = module.exports = {}; @@ -35,201 +31,271 @@ var fx = module.exports = {}; fx.unhover = dragElement.unhover; fx.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { - - function coerce(attr, dflt) { - return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); - } - - coerce('dragmode'); - - var hovermodeDflt; - if(layoutOut._has('cartesian')) { - // flag for 'horizontal' plots: - // determines the state of the mode bar 'compare' hovermode button - var isHoriz = layoutOut._isHoriz = fx.isHoriz(fullData); - hovermodeDflt = isHoriz ? 'y' : 'x'; - } - else hovermodeDflt = 'closest'; - - coerce('hovermode', hovermodeDflt); + function coerce(attr, dflt) { + return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); + } + + coerce("dragmode"); + + var hovermodeDflt; + if (layoutOut._has("cartesian")) { + // flag for 'horizontal' plots: + // determines the state of the mode bar 'compare' hovermode button + var isHoriz = layoutOut._isHoriz = fx.isHoriz(fullData); + hovermodeDflt = isHoriz ? "y" : "x"; + } else { + hovermodeDflt = "closest"; + } + + coerce("hovermode", hovermodeDflt); }; fx.isHoriz = function(fullData) { - var isHoriz = true; + var isHoriz = true; - for(var i = 0; i < fullData.length; i++) { - var trace = fullData[i]; + for (var i = 0; i < fullData.length; i++) { + var trace = fullData[i]; - if(trace.orientation !== 'h') { - isHoriz = false; - break; - } + if (trace.orientation !== "h") { + isHoriz = false; + break; } + } - return isHoriz; + return isHoriz; }; fx.init = function(gd) { - var fullLayout = gd._fullLayout; - - if(!fullLayout._has('cartesian') || gd._context.staticPlot) return; - - var subplots = Object.keys(fullLayout._plots || {}).sort(function(a, b) { - // sort overlays last, then by x axis number, then y axis number - if((fullLayout._plots[a].mainplot && true) === - (fullLayout._plots[b].mainplot && true)) { - var aParts = a.split('y'), - bParts = b.split('y'); - return (aParts[0] === bParts[0]) ? - (Number(aParts[1] || 1) - Number(bParts[1] || 1)) : - (Number(aParts[0] || 1) - Number(bParts[0] || 1)); - } - return fullLayout._plots[a].mainplot ? 1 : -1; - }); - - subplots.forEach(function(subplot) { - var plotinfo = fullLayout._plots[subplot]; - - if(!fullLayout._has('cartesian')) return; - - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - - // the y position of the main x axis line - y0 = (xa._linepositions[subplot] || [])[3], - - // the x position of the main y axis line - x0 = (ya._linepositions[subplot] || [])[3]; - - var DRAGGERSIZE = constants.DRAGGERSIZE; - if(isNumeric(y0) && xa.side === 'top') y0 -= DRAGGERSIZE; - if(isNumeric(x0) && ya.side !== 'right') x0 -= DRAGGERSIZE; - - // main and corner draggers need not be repeated for - // overlaid subplots - these draggers drag them all - if(!plotinfo.mainplot) { - // main dragger goes over the grids and data, so we use its - // mousemove events for all data hover effects - var maindrag = dragBox(gd, plotinfo, 0, 0, - xa._length, ya._length, 'ns', 'ew'); - - maindrag.onmousemove = function(evt) { - fx.hover(gd, evt, subplot); - fullLayout._lasthover = maindrag; - fullLayout._hoversubplot = subplot; - }; - - /* + var fullLayout = gd._fullLayout; + + if (!fullLayout._has("cartesian") || gd._context.staticPlot) return; + + var subplots = Object.keys(fullLayout._plots || {}).sort(function(a, b) { + // sort overlays last, then by x axis number, then y axis number + if ( + (fullLayout._plots[a].mainplot && true) === + (fullLayout._plots[b].mainplot && true) + ) { + var aParts = a.split("y"), bParts = b.split("y"); + return aParts[0] === bParts[0] + ? Number(aParts[1] || 1) - Number(bParts[1] || 1) + : Number(aParts[0] || 1) - Number(bParts[0] || 1); + } + return fullLayout._plots[a].mainplot ? 1 : -1; + }); + + subplots.forEach(function(subplot) { + var plotinfo = fullLayout._plots[subplot]; + + if (!fullLayout._has("cartesian")) return; + + var xa = plotinfo.xaxis, + ya = plotinfo.yaxis, + // the y position of the main x axis line + y0 = (xa._linepositions[subplot] || [])[3], + // the x position of the main y axis line + x0 = (ya._linepositions[subplot] || [])[3]; + + var DRAGGERSIZE = constants.DRAGGERSIZE; + if (isNumeric(y0) && xa.side === "top") y0 -= DRAGGERSIZE; + if (isNumeric(x0) && ya.side !== "right") x0 -= DRAGGERSIZE; + + // main and corner draggers need not be repeated for + // overlaid subplots - these draggers drag them all + if (!plotinfo.mainplot) { + // main dragger goes over the grids and data, so we use its + // mousemove events for all data hover effects + var maindrag = dragBox( + gd, + plotinfo, + 0, + 0, + xa._length, + ya._length, + "ns", + "ew" + ); + + maindrag.onmousemove = function(evt) { + fx.hover(gd, evt, subplot); + fullLayout._lasthover = maindrag; + fullLayout._hoversubplot = subplot; + }; + + /* * IMPORTANT: * We must check for the presence of the drag cover here. * If we don't, a 'mouseout' event is triggered on the * maindrag before each 'click' event, which has the effect * of clearing the hoverdata; thus, cancelling the click event. */ - maindrag.onmouseout = function(evt) { - if(gd._dragging) return; - - dragElement.unhover(gd, evt); - }; - - maindrag.onclick = function(evt) { - fx.click(gd, evt); - }; - - // corner draggers - dragBox(gd, plotinfo, -DRAGGERSIZE, -DRAGGERSIZE, - DRAGGERSIZE, DRAGGERSIZE, 'n', 'w'); - dragBox(gd, plotinfo, xa._length, -DRAGGERSIZE, - DRAGGERSIZE, DRAGGERSIZE, 'n', 'e'); - dragBox(gd, plotinfo, -DRAGGERSIZE, ya._length, - DRAGGERSIZE, DRAGGERSIZE, 's', 'w'); - dragBox(gd, plotinfo, xa._length, ya._length, - DRAGGERSIZE, DRAGGERSIZE, 's', 'e'); - } + maindrag.onmouseout = function(evt) { + if (gd._dragging) return; - // x axis draggers - if you have overlaid plots, - // these drag each axis separately - if(isNumeric(y0)) { - if(xa.anchor === 'free') y0 -= fullLayout._size.h * (1 - ya.domain[1]); - dragBox(gd, plotinfo, xa._length * 0.1, y0, - xa._length * 0.8, DRAGGERSIZE, '', 'ew'); - dragBox(gd, plotinfo, 0, y0, - xa._length * 0.1, DRAGGERSIZE, '', 'w'); - dragBox(gd, plotinfo, xa._length * 0.9, y0, - xa._length * 0.1, DRAGGERSIZE, '', 'e'); - } - // y axis draggers - if(isNumeric(x0)) { - if(ya.anchor === 'free') x0 -= fullLayout._size.w * xa.domain[0]; - dragBox(gd, plotinfo, x0, ya._length * 0.1, - DRAGGERSIZE, ya._length * 0.8, 'ns', ''); - dragBox(gd, plotinfo, x0, ya._length * 0.9, - DRAGGERSIZE, ya._length * 0.1, 's', ''); - dragBox(gd, plotinfo, x0, 0, - DRAGGERSIZE, ya._length * 0.1, 'n', ''); - } - }); - - // In case you mousemove over some hovertext, send it to fx.hover too - // we do this so that we can put the hover text in front of everything, - // but still be able to interact with everything as if it isn't there - var hoverLayer = fullLayout._hoverlayer.node(); + dragElement.unhover(gd, evt); + }; - hoverLayer.onmousemove = function(evt) { - evt.target = fullLayout._lasthover; - fx.hover(gd, evt, fullLayout._hoversubplot); - }; - - hoverLayer.onclick = function(evt) { - evt.target = fullLayout._lasthover; + maindrag.onclick = function(evt) { fx.click(gd, evt); - }; + }; + + // corner draggers + dragBox( + gd, + plotinfo, + -DRAGGERSIZE, + -DRAGGERSIZE, + DRAGGERSIZE, + DRAGGERSIZE, + "n", + "w" + ); + dragBox( + gd, + plotinfo, + xa._length, + -DRAGGERSIZE, + DRAGGERSIZE, + DRAGGERSIZE, + "n", + "e" + ); + dragBox( + gd, + plotinfo, + -DRAGGERSIZE, + ya._length, + DRAGGERSIZE, + DRAGGERSIZE, + "s", + "w" + ); + dragBox( + gd, + plotinfo, + xa._length, + ya._length, + DRAGGERSIZE, + DRAGGERSIZE, + "s", + "e" + ); + } - // also delegate mousedowns... TODO: does this actually work? - hoverLayer.onmousedown = function(evt) { - fullLayout._lasthover.onmousedown(evt); - }; + // x axis draggers - if you have overlaid plots, + // these drag each axis separately + if (isNumeric(y0)) { + if (xa.anchor === "free") y0 -= fullLayout._size.h * (1 - ya.domain[1]); + dragBox( + gd, + plotinfo, + xa._length * 0.1, + y0, + xa._length * 0.8, + DRAGGERSIZE, + "", + "ew" + ); + dragBox(gd, plotinfo, 0, y0, xa._length * 0.1, DRAGGERSIZE, "", "w"); + dragBox( + gd, + plotinfo, + xa._length * 0.9, + y0, + xa._length * 0.1, + DRAGGERSIZE, + "", + "e" + ); + } + // y axis draggers + if (isNumeric(x0)) { + if (ya.anchor === "free") x0 -= fullLayout._size.w * xa.domain[0]; + dragBox( + gd, + plotinfo, + x0, + ya._length * 0.1, + DRAGGERSIZE, + ya._length * 0.8, + "ns", + "" + ); + dragBox( + gd, + plotinfo, + x0, + ya._length * 0.9, + DRAGGERSIZE, + ya._length * 0.1, + "s", + "" + ); + dragBox(gd, plotinfo, x0, 0, DRAGGERSIZE, ya._length * 0.1, "n", ""); + } + }); + + // In case you mousemove over some hovertext, send it to fx.hover too + // we do this so that we can put the hover text in front of everything, + // but still be able to interact with everything as if it isn't there + var hoverLayer = fullLayout._hoverlayer.node(); + + hoverLayer.onmousemove = function(evt) { + evt.target = fullLayout._lasthover; + fx.hover(gd, evt, fullLayout._hoversubplot); + }; + + hoverLayer.onclick = function(evt) { + evt.target = fullLayout._lasthover; + fx.click(gd, evt); + }; + + // also delegate mousedowns... TODO: does this actually work? + hoverLayer.onmousedown = function(evt) { + fullLayout._lasthover.onmousedown(evt); + }; }; // hover labels for multiple horizontal bars get tilted by some angle, // then need to be offset differently if they overlap var YANGLE = constants.YANGLE, - YA_RADIANS = Math.PI * YANGLE / 180, - - // expansion of projected height - YFACTOR = 1 / Math.sin(YA_RADIANS), - - // to make the appropriate post-rotation x offset, - // you need both x and y offsets - YSHIFTX = Math.cos(YA_RADIANS), - YSHIFTY = Math.sin(YA_RADIANS); + YA_RADIANS = Math.PI * YANGLE / 180, + // expansion of projected height + YFACTOR = 1 / Math.sin(YA_RADIANS), + // to make the appropriate post-rotation x offset, + // you need both x and y offsets + YSHIFTX = Math.cos(YA_RADIANS), + YSHIFTY = Math.sin(YA_RADIANS); // convenience functions for mapping all relevant axes function flat(subplots, v) { - var out = []; - for(var i = subplots.length; i > 0; i--) out.push(v); - return out; + var out = []; + for (var i = subplots.length; i > 0; i--) { + out.push(v); + } + return out; } function p2c(axArray, v) { - var out = []; - for(var i = 0; i < axArray.length; i++) out.push(axArray[i].p2c(v)); - return out; + var out = []; + for (var i = 0; i < axArray.length; i++) { + out.push(axArray[i].p2c(v)); + } + return out; } function quadrature(dx, dy) { - return function(di) { - var x = dx(di), - y = dy(di); - return Math.sqrt(x * x + y * y); - }; + return function(di) { + var x = dx(di), y = dy(di); + return Math.sqrt(x * x + y * y); + }; } // size and display constants for hover text var HOVERARROWSIZE = constants.HOVERARROWSIZE, - HOVERTEXTPAD = constants.HOVERTEXTPAD, - HOVERFONTSIZE = constants.HOVERFONTSIZE, - HOVERFONT = constants.HOVERFONT; + HOVERTEXTPAD = constants.HOVERTEXTPAD, + HOVERFONTSIZE = constants.HOVERFONTSIZE, + HOVERFONT = constants.HOVERFONT; // fx.hover: highlight data on hover // evt can be a mousemove event, or an object with data about what points @@ -256,830 +322,901 @@ var HOVERARROWSIZE = constants.HOVERARROWSIZE, // We wrap the hovers in a timer, to limit their frequency. // The actual rendering is done by private functions // hover() and unhover(). - fx.hover = function(gd, evt, subplot) { - if(typeof gd === 'string') gd = document.getElementById(gd); - if(gd._lastHoverTime === undefined) gd._lastHoverTime = 0; - - // If we have an update queued, discard it now - if(gd._hoverTimer !== undefined) { - clearTimeout(gd._hoverTimer); - gd._hoverTimer = undefined; - } - // Is it more than 100ms since the last update? If so, force - // an update now (synchronously) and exit - if(Date.now() > gd._lastHoverTime + constants.HOVERMINTIME) { - hover(gd, evt, subplot); - gd._lastHoverTime = Date.now(); - return; - } - // Queue up the next hover for 100ms from now (if no further events) - gd._hoverTimer = setTimeout(function() { - hover(gd, evt, subplot); - gd._lastHoverTime = Date.now(); - gd._hoverTimer = undefined; - }, constants.HOVERMINTIME); + if (typeof gd === "string") gd = document.getElementById(gd); + if (gd._lastHoverTime === undefined) gd._lastHoverTime = 0; + + // If we have an update queued, discard it now + if (gd._hoverTimer !== undefined) { + clearTimeout(gd._hoverTimer); + gd._hoverTimer = undefined; + } + // Is it more than 100ms since the last update? If so, force + // an update now (synchronously) and exit + if (Date.now() > gd._lastHoverTime + constants.HOVERMINTIME) { + hover(gd, evt, subplot); + gd._lastHoverTime = Date.now(); + return; + } + // Queue up the next hover for 100ms from now (if no further events) + gd._hoverTimer = setTimeout( + function() { + hover(gd, evt, subplot); + gd._lastHoverTime = Date.now(); + gd._hoverTimer = undefined; + }, + constants.HOVERMINTIME + ); }; // The actual implementation is here: - function hover(gd, evt, subplot) { - if(subplot === 'pie') { - gd.emit('plotly_hover', { - points: [evt] - }); - return; - } - - if(!subplot) subplot = 'xy'; + if (subplot === "pie") { + gd.emit("plotly_hover", { points: [evt] }); + return; + } - // if the user passed in an array of subplots, - // use those instead of finding overlayed plots - var subplots = Array.isArray(subplot) ? subplot : [subplot]; + if (!subplot) subplot = "xy"; - var fullLayout = gd._fullLayout, - plots = fullLayout._plots || [], - plotinfo = plots[subplot]; + // if the user passed in an array of subplots, + // use those instead of finding overlayed plots + var subplots = Array.isArray(subplot) ? subplot : [subplot]; - // list of all overlaid subplots to look at - if(plotinfo) { - var overlayedSubplots = plotinfo.overlays.map(function(pi) { - return pi.id; - }); + var fullLayout = gd._fullLayout, + plots = fullLayout._plots || [], + plotinfo = plots[subplot]; - subplots = subplots.concat(overlayedSubplots); - } - - var len = subplots.length, - xaArray = new Array(len), - yaArray = new Array(len); - - for(var i = 0; i < len; i++) { - var spId = subplots[i]; + // list of all overlaid subplots to look at + if (plotinfo) { + var overlayedSubplots = plotinfo.overlays.map(function(pi) { + return pi.id; + }); - // 'cartesian' case - var plotObj = plots[spId]; - if(plotObj) { + subplots = subplots.concat(overlayedSubplots); + } - // TODO make sure that fullLayout_plots axis refs - // get updated properly so that we don't have - // to use Axes.getFromId in general. + var len = subplots.length, xaArray = new Array(len), yaArray = new Array(len); - xaArray[i] = Axes.getFromId(gd, plotObj.xaxis._id); - yaArray[i] = Axes.getFromId(gd, plotObj.yaxis._id); - continue; - } + for (var i = 0; i < len; i++) { + var spId = subplots[i]; - // other subplot types - var _subplot = fullLayout[spId]._subplot; - xaArray[i] = _subplot.xaxis; - yaArray[i] = _subplot.yaxis; + // 'cartesian' case + var plotObj = plots[spId]; + if (plotObj) { + // TODO make sure that fullLayout_plots axis refs + // get updated properly so that we don't have + // to use Axes.getFromId in general. + xaArray[i] = Axes.getFromId(gd, plotObj.xaxis._id); + yaArray[i] = Axes.getFromId(gd, plotObj.yaxis._id); + continue; } - var hovermode = evt.hovermode || fullLayout.hovermode; - - if(['x', 'y', 'closest'].indexOf(hovermode) === -1 || !gd.calcdata || - gd.querySelector('.zoombox') || gd._dragging) { - return dragElement.unhoverRaw(gd, evt); + // other subplot types + var _subplot = fullLayout[spId]._subplot; + xaArray[i] = _subplot.xaxis; + yaArray[i] = _subplot.yaxis; + } + + var hovermode = evt.hovermode || fullLayout.hovermode; + + if ( + ["x", "y", "closest"].indexOf(hovermode) === -1 || + !gd.calcdata || + gd.querySelector(".zoombox") || + gd._dragging + ) { + return dragElement.unhoverRaw(gd, evt); + } + + // 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 + // gd.calcdata, filtered to the subplot and overlays we're on + // but if a point array is supplied it will be a mapping + // of indicated curves + searchData = [], + // [x|y]valArray: the axis values of the hover event + // mapped onto each of the currently selected overlaid subplots + xvalArray, + yvalArray, + // used in loops + itemnum, + curvenum, + cd, + trace, + subplotId, + subploti, + mode, + xval, + yval, + pointData, + closedataPreviousLength; + + // Figure out what we're hovering on: + // mouse location or user-supplied data + if (Array.isArray(evt)) { + // user specified an array of points to highlight + hovermode = "array"; + for (itemnum = 0; itemnum < evt.length; itemnum++) { + cd = gd.calcdata[evt[itemnum].curveNumber || 0]; + if (cd[0].trace.hoverinfo !== "skip") { + searchData.push(cd); + } } - - // 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 - // gd.calcdata, filtered to the subplot and overlays we're on - // but if a point array is supplied it will be a mapping - // of indicated curves - searchData = [], - - // [x|y]valArray: the axis values of the hover event - // mapped onto each of the currently selected overlaid subplots - xvalArray, - yvalArray, - - // used in loops - itemnum, - curvenum, - cd, - trace, - subplotId, - subploti, - mode, - xval, - yval, - pointData, - closedataPreviousLength; - - // Figure out what we're hovering on: - // mouse location or user-supplied data - - if(Array.isArray(evt)) { - // user specified an array of points to highlight - hovermode = 'array'; - for(itemnum = 0; itemnum < evt.length; itemnum++) { - cd = gd.calcdata[evt[itemnum].curveNumber||0]; - if(cd[0].trace.hoverinfo !== 'skip') { - searchData.push(cd); - } - } + } else { + for (curvenum = 0; curvenum < gd.calcdata.length; curvenum++) { + cd = gd.calcdata[curvenum]; + trace = cd[0].trace; + if ( + trace.hoverinfo !== "skip" && subplots.indexOf(getSubplot(trace)) !== -1 + ) { + searchData.push(cd); + } } - else { - for(curvenum = 0; curvenum < gd.calcdata.length; curvenum++) { - cd = gd.calcdata[curvenum]; - trace = cd[0].trace; - if(trace.hoverinfo !== 'skip' && subplots.indexOf(getSubplot(trace)) !== -1) { - searchData.push(cd); - } - } - - // [x|y]px: the pixels (from top left) of the mouse location - // on the currently selected plot area - var xpx, ypx; - // mouse event? ie is there a target element with - // clientX and clientY values? - if(evt.target && ('clientX' in evt) && ('clientY' in evt)) { - - // fire the beforehover event and quit if it returns false - // note that we're only calling this on real mouse events, so - // manual calls to fx.hover will always run. - if(Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) { - return; - } + // [x|y]px: the pixels (from top left) of the mouse location + // on the currently selected plot area + var xpx, ypx; + + // mouse event? ie is there a target element with + // clientX and clientY values? + if (evt.target && "clientX" in evt && "clientY" in evt) { + // fire the beforehover event and quit if it returns false + // note that we're only calling this on real mouse events, so + // manual calls to fx.hover will always run. + if (Events.triggerHandler(gd, "plotly_beforehover", evt) === false) { + return; + } - var dbb = evt.target.getBoundingClientRect(); + var dbb = evt.target.getBoundingClientRect(); - xpx = evt.clientX - dbb.left; - ypx = evt.clientY - dbb.top; + xpx = evt.clientX - dbb.left; + ypx = evt.clientY - dbb.top; - // in case hover was called from mouseout into hovertext, - // it's possible you're not actually over the plot anymore - if(xpx < 0 || xpx > dbb.width || ypx < 0 || ypx > dbb.height) { - return dragElement.unhoverRaw(gd, evt); - } - } - else { - if('xpx' in evt) xpx = evt.xpx; - else xpx = xaArray[0]._length / 2; + // in case hover was called from mouseout into hovertext, + // it's possible you're not actually over the plot anymore + if (xpx < 0 || xpx > dbb.width || ypx < 0 || ypx > dbb.height) { + return dragElement.unhoverRaw(gd, evt); + } + } else { + if ("xpx" in evt) xpx = evt.xpx; + else xpx = xaArray[0]._length / 2; - if('ypx' in evt) ypx = evt.ypx; - else ypx = yaArray[0]._length / 2; - } + if ("ypx" in evt) ypx = evt.ypx; + else ypx = yaArray[0]._length / 2; + } - if('xval' in evt) xvalArray = flat(subplots, evt.xval); - else xvalArray = p2c(xaArray, xpx); + if ("xval" in evt) xvalArray = flat(subplots, evt.xval); + else xvalArray = p2c(xaArray, xpx); - if('yval' in evt) yvalArray = flat(subplots, evt.yval); - else yvalArray = p2c(yaArray, ypx); + if ("yval" in evt) yvalArray = flat(subplots, evt.yval); + else yvalArray = p2c(yaArray, ypx); - if(!isNumeric(xvalArray[0]) || !isNumeric(yvalArray[0])) { - Lib.warn('Fx.hover failed', evt, gd); - return dragElement.unhoverRaw(gd, evt); - } + if (!isNumeric(xvalArray[0]) || !isNumeric(yvalArray[0])) { + Lib.warn("Fx.hover failed", evt, gd); + return dragElement.unhoverRaw(gd, evt); } + } + + // the pixel distance to beat as a matching point + // in 'x' or 'y' mode this resets for each trace + var distance = Infinity; + + // find the closest point in each trace + // this is minimum dx and/or dy, depending on mode + // and the pixel position for the label (labelXpx, labelYpx) + for (curvenum = 0; curvenum < searchData.length; curvenum++) { + cd = searchData[curvenum]; + + // filter out invisible or broken data + if (!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue; + + trace = cd[0].trace; + subplotId = getSubplot(trace); + subploti = subplots.indexOf(subplotId); + + // within one trace mode can sometimes be overridden + mode = hovermode; + + // container for new point, also used to pass info into module.hoverPoints + pointData = { + // trace properties + cd: cd, + trace: trace, + xa: xaArray[subploti], + ya: yaArray[subploti], + name: ( + gd.data.length > 1 || trace.hoverinfo.indexOf("name") !== -1 + ? trace.name + : undefined + ), + // point properties - override all of these + index: false, + // point index in trace - only used by plotly.js hoverdata consumers + distance: Math.min(distance, constants.MAXDIST), + // pixel distance or pseudo-distance + color: Color.defaultLine, + // trace color + x0: undefined, + x1: undefined, + y0: undefined, + y1: undefined, + xLabelVal: undefined, + yLabelVal: undefined, + zLabelVal: undefined, + text: undefined + }; - // the pixel distance to beat as a matching point - // in 'x' or 'y' mode this resets for each trace - var distance = Infinity; - - // find the closest point in each trace - // this is minimum dx and/or dy, depending on mode - // and the pixel position for the label (labelXpx, labelYpx) - for(curvenum = 0; curvenum < searchData.length; curvenum++) { - cd = searchData[curvenum]; - - // filter out invisible or broken data - if(!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue; - - trace = cd[0].trace; - subplotId = getSubplot(trace); - subploti = subplots.indexOf(subplotId); - - // within one trace mode can sometimes be overridden - mode = hovermode; - - // container for new point, also used to pass info into module.hoverPoints - pointData = { - // trace properties - cd: cd, - trace: trace, - xa: xaArray[subploti], - ya: yaArray[subploti], - name: (gd.data.length > 1 || trace.hoverinfo.indexOf('name') !== -1) ? trace.name : undefined, - // point properties - override all of these - index: false, // point index in trace - only used by plotly.js hoverdata consumers - distance: Math.min(distance, constants.MAXDIST), // pixel distance or pseudo-distance - color: Color.defaultLine, // trace color - x0: undefined, - x1: undefined, - y0: undefined, - y1: undefined, - xLabelVal: undefined, - yLabelVal: undefined, - zLabelVal: undefined, - text: undefined - }; - - // add ref to subplot object (non-cartesian case) - if(fullLayout[subplotId]) { - pointData.subplot = fullLayout[subplotId]._subplot; - } - - closedataPreviousLength = hoverData.length; - - // for a highlighting array, figure out what - // we're searching for with this element - if(mode === 'array') { - var selection = evt[curvenum]; - if('pointNumber' in selection) { - pointData.index = selection.pointNumber; - mode = 'closest'; - } - else { - mode = ''; - if('xval' in selection) { - xval = selection.xval; - mode = 'x'; - } - if('yval' in selection) { - yval = selection.yval; - mode = mode ? 'closest' : 'y'; - } - } - } - else { - xval = xvalArray[subploti]; - yval = yvalArray[subploti]; - } + // add ref to subplot object (non-cartesian case) + if (fullLayout[subplotId]) { + pointData.subplot = fullLayout[subplotId]._subplot; + } - // Now find the points. - if(trace._module && trace._module.hoverPoints) { - var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode); - if(newPoints) { - var newPoint; - for(var newPointNum = 0; newPointNum < newPoints.length; newPointNum++) { - newPoint = newPoints[newPointNum]; - if(isNumeric(newPoint.x0) && isNumeric(newPoint.y0)) { - hoverData.push(cleanPoint(newPoint, hovermode)); - } - } - } + closedataPreviousLength = hoverData.length; + + // for a highlighting array, figure out what + // we're searching for with this element + if (mode === "array") { + var selection = evt[curvenum]; + if ("pointNumber" in selection) { + pointData.index = selection.pointNumber; + mode = "closest"; + } else { + mode = ""; + if ("xval" in selection) { + xval = selection.xval; + mode = "x"; } - else { - Lib.log('Unrecognized trace type in hover:', trace); - } - - // in closest mode, remove any existing (farther) points - // and don't look any farther than this latest point (or points, if boxes) - if(hovermode === 'closest' && hoverData.length > closedataPreviousLength) { - hoverData.splice(0, closedataPreviousLength); - distance = hoverData[0].distance; + if ("yval" in selection) { + yval = selection.yval; + mode = mode ? "closest" : "y"; } + } + } else { + xval = xvalArray[subploti]; + yval = yvalArray[subploti]; } - // nothing left: remove all labels and quit - if(hoverData.length === 0) return dragElement.unhoverRaw(gd, evt); - - // if there's more than one horz bar trace, - // rotate the labels so they don't overlap - var rotateLabels = hovermode === 'y' && searchData.length > 1; - - hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; }); - - var bgColor = Color.combine( - fullLayout.plot_bgcolor || Color.background, - fullLayout.paper_bgcolor - ); - - var labelOpts = { - hovermode: hovermode, - rotateLabels: rotateLabels, - bgColor: bgColor, - container: fullLayout._hoverlayer, - outerContainer: fullLayout._paperdiv - }; - var hoverLabels = createHoverText(hoverData, labelOpts); - - hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya'); - - alignHoverText(hoverLabels, rotateLabels); - - // lastly, emit custom hover/unhover events - var oldhoverdata = gd._hoverdata, - newhoverdata = []; - - // pull out just the data that's useful to - // other people and send it to the event - for(itemnum = 0; itemnum < hoverData.length; itemnum++) { - var pt = hoverData[itemnum]; - - var out = { - data: pt.trace._input, - fullData: pt.trace, - curveNumber: pt.trace.index, - pointNumber: pt.index - }; - - if(pt.trace._module.eventData) out = pt.trace._module.eventData(out, pt); - else { - out.x = pt.xVal; - out.y = pt.yVal; - out.xaxis = pt.xa; - out.yaxis = pt.ya; - - if(pt.zLabelVal !== undefined) out.z = pt.zLabelVal; + // Now find the points. + if (trace._module && trace._module.hoverPoints) { + var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode); + if (newPoints) { + var newPoint; + for ( + var newPointNum = 0; + newPointNum < newPoints.length; + newPointNum++ + ) { + newPoint = newPoints[newPointNum]; + if (isNumeric(newPoint.x0) && isNumeric(newPoint.y0)) { + hoverData.push(cleanPoint(newPoint, hovermode)); + } } - - newhoverdata.push(out); + } + } else { + Lib.log("Unrecognized trace type in hover:", trace); } - gd._hoverdata = newhoverdata; - - // TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true - // we should improve the "fx" API so other plots can use it without these hack. - if(evt.target && evt.target.tagName) { - var hasClickToShow = Registry.getComponentMethod('annotations', 'hasClickToShow')(gd, newhoverdata); - overrideCursor(d3.select(evt.target), hasClickToShow ? 'pointer' : ''); + // in closest mode, remove any existing (farther) points + // and don't look any farther than this latest point (or points, if boxes) + if (hovermode === "closest" && hoverData.length > closedataPreviousLength) { + hoverData.splice(0, closedataPreviousLength); + distance = hoverData[0].distance; } + } + + // nothing left: remove all labels and quit + if (hoverData.length === 0) return dragElement.unhoverRaw(gd, evt); + + // if there's more than one horz bar trace, + // rotate the labels so they don't overlap + var rotateLabels = hovermode === "y" && searchData.length > 1; + + hoverData.sort(function(d1, d2) { + return d1.distance - d2.distance; + }); + + var bgColor = Color.combine( + fullLayout.plot_bgcolor || Color.background, + fullLayout.paper_bgcolor + ); + + var labelOpts = { + hovermode: hovermode, + rotateLabels: rotateLabels, + bgColor: bgColor, + container: fullLayout._hoverlayer, + outerContainer: fullLayout._paperdiv + }; + var hoverLabels = createHoverText(hoverData, labelOpts); + + hoverAvoidOverlaps(hoverData, rotateLabels ? "xa" : "ya"); + + alignHoverText(hoverLabels, rotateLabels); + + // lastly, emit custom hover/unhover events + var oldhoverdata = gd._hoverdata, newhoverdata = []; + + // pull out just the data that's useful to + // other people and send it to the event + for (itemnum = 0; itemnum < hoverData.length; itemnum++) { + var pt = hoverData[itemnum]; + + var out = { + data: pt.trace._input, + fullData: pt.trace, + curveNumber: pt.trace.index, + pointNumber: pt.index + }; - if(!hoverChanged(gd, evt, oldhoverdata)) return; + if (pt.trace._module.eventData) { + out = pt.trace._module.eventData(out, pt); + } else { + out.x = pt.xVal; + out.y = pt.yVal; + out.xaxis = pt.xa; + out.yaxis = pt.ya; - if(oldhoverdata) { - gd.emit('plotly_unhover', { points: oldhoverdata }); + if (pt.zLabelVal !== undefined) out.z = pt.zLabelVal; } - gd.emit('plotly_hover', { - points: gd._hoverdata, - xaxes: xaArray, - yaxes: yaArray, - xvals: xvalArray, - yvals: yvalArray - }); + newhoverdata.push(out); + } + + gd._hoverdata = newhoverdata; + + // TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true + // we should improve the "fx" API so other plots can use it without these hack. + if (evt.target && evt.target.tagName) { + var hasClickToShow = Registry.getComponentMethod( + "annotations", + "hasClickToShow" + )(gd, newhoverdata); + overrideCursor(d3.select(evt.target), hasClickToShow ? "pointer" : ""); + } + + if (!hoverChanged(gd, evt, oldhoverdata)) return; + + if (oldhoverdata) { + gd.emit("plotly_unhover", { points: oldhoverdata }); + } + + gd.emit("plotly_hover", { + points: gd._hoverdata, + xaxes: xaArray, + yaxes: yaArray, + xvals: xvalArray, + yvals: yvalArray + }); } // look for either .subplot (currently just ternary) // or xaxis and yaxis attributes function getSubplot(trace) { - return trace.subplot || (trace.xaxis + trace.yaxis) || trace.geo; + return trace.subplot || trace.xaxis + trace.yaxis || trace.geo; } fx.getDistanceFunction = function(mode, dx, dy, dxy) { - if(mode === 'closest') return dxy || quadrature(dx, dy); - return mode === 'x' ? dx : dy; + if (mode === "closest") return dxy || quadrature(dx, dy); + return mode === "x" ? dx : dy; }; fx.getClosest = function(cd, distfn, pointData) { - // do we already have a point number? (array mode only) - if(pointData.index !== false) { - if(pointData.index >= 0 && pointData.index < cd.length) { - pointData.distance = 0; - } - else pointData.index = false; + // do we already have a point number? (array mode only) + if (pointData.index !== false) { + if (pointData.index >= 0 && pointData.index < cd.length) { + pointData.distance = 0; + } else { + pointData.index = false; } - else { - // apply the distance function to each data point - // this is the longest loop... if this bogs down, we may need - // to create pre-sorted data (by x or y), not sure how to - // do this for 'closest' - for(var i = 0; i < cd.length; i++) { - var newDistance = distfn(cd[i]); - if(newDistance <= pointData.distance) { - pointData.index = i; - pointData.distance = newDistance; - } - } + } else { + // apply the distance function to each data point + // this is the longest loop... if this bogs down, we may need + // to create pre-sorted data (by x or y), not sure how to + // do this for 'closest' + for (var i = 0; i < cd.length; i++) { + var newDistance = distfn(cd[i]); + if (newDistance <= pointData.distance) { + pointData.index = i; + pointData.distance = newDistance; + } } - return pointData; + } + return pointData; }; function cleanPoint(d, hovermode) { - d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2; - - // then constrain all the positions to be on the plot - d.x0 = Lib.constrain(d.x0, 0, d.xa._length); - d.x1 = Lib.constrain(d.x1, 0, d.xa._length); - d.y0 = Lib.constrain(d.y0, 0, d.ya._length); - d.y1 = Lib.constrain(d.y1, 0, d.ya._length); - - // and convert the x and y label values into objects - // formatted as text, with font info - var logOffScale; - if(d.xLabelVal !== undefined) { - logOffScale = (d.xa.type === 'log' && d.xLabelVal <= 0); - var xLabelObj = Axes.tickText(d.xa, - d.xa.c2l(logOffScale ? -d.xLabelVal : d.xLabelVal), 'hover'); - if(logOffScale) { - if(d.xLabelVal === 0) d.xLabel = '0'; - else d.xLabel = '-' + xLabelObj.text; - } - // TODO: should we do something special if the axis calendar and - // the data calendar are different? Somehow display both dates with - // their system names? Right now it will just display in the axis calendar - // but users could add the other one as text. - else d.xLabel = xLabelObj.text; - d.xVal = d.xa.c2d(d.xLabelVal); + d.posref = hovermode === "y" ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2; + + // then constrain all the positions to be on the plot + d.x0 = Lib.constrain(d.x0, 0, d.xa._length); + d.x1 = Lib.constrain(d.x1, 0, d.xa._length); + d.y0 = Lib.constrain(d.y0, 0, d.ya._length); + d.y1 = Lib.constrain(d.y1, 0, d.ya._length); + + // and convert the x and y label values into objects + // formatted as text, with font info + var logOffScale; + if (d.xLabelVal !== undefined) { + logOffScale = d.xa.type === "log" && d.xLabelVal <= 0; + var xLabelObj = Axes.tickText( + d.xa, + d.xa.c2l(logOffScale ? -d.xLabelVal : d.xLabelVal), + "hover" + ); + if (logOffScale) { + if (d.xLabelVal === 0) d.xLabel = "0"; + else d.xLabel = "-" + xLabelObj.text; + } else { + // TODO: should we do something special if the axis calendar and + // the data calendar are different? Somehow display both dates with + // their system names? Right now it will just display in the axis calendar + // but users could add the other one as text. + d.xLabel = xLabelObj.text; } - - if(d.yLabelVal !== undefined) { - logOffScale = (d.ya.type === 'log' && d.yLabelVal <= 0); - var yLabelObj = Axes.tickText(d.ya, - d.ya.c2l(logOffScale ? -d.yLabelVal : d.yLabelVal), 'hover'); - if(logOffScale) { - if(d.yLabelVal === 0) d.yLabel = '0'; - else d.yLabel = '-' + yLabelObj.text; - } - // TODO: see above TODO - else d.yLabel = yLabelObj.text; - d.yVal = d.ya.c2d(d.yLabelVal); + d.xVal = d.xa.c2d(d.xLabelVal); + } + + if (d.yLabelVal !== undefined) { + logOffScale = d.ya.type === "log" && d.yLabelVal <= 0; + var yLabelObj = Axes.tickText( + d.ya, + d.ya.c2l(logOffScale ? -d.yLabelVal : d.yLabelVal), + "hover" + ); + if (logOffScale) { + if (d.yLabelVal === 0) d.yLabel = "0"; + else d.yLabel = "-" + yLabelObj.text; + } else { + // TODO: see above TODO + d.yLabel = yLabelObj.text; } - - if(d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal); - - // for box means and error bars, add the range to the label - if(!isNaN(d.xerr) && !(d.xa.type === 'log' && d.xerr <= 0)) { - var xeText = Axes.tickText(d.xa, d.xa.c2l(d.xerr), 'hover').text; - if(d.xerrneg !== undefined) { - d.xLabel += ' +' + xeText + ' / -' + - Axes.tickText(d.xa, d.xa.c2l(d.xerrneg), 'hover').text; - } - else d.xLabel += ' ± ' + xeText; - - // small distance penalty for error bars, so that if there are - // traces with errors and some without, the error bar label will - // hoist up to the point - if(hovermode === 'x') d.distance += 1; + d.yVal = d.ya.c2d(d.yLabelVal); + } + + if (d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal); + + // for box means and error bars, add the range to the label + if (!isNaN(d.xerr) && !(d.xa.type === "log" && d.xerr <= 0)) { + var xeText = Axes.tickText(d.xa, d.xa.c2l(d.xerr), "hover").text; + if (d.xerrneg !== undefined) { + d.xLabel += " +" + + xeText + + " / -" + + Axes.tickText(d.xa, d.xa.c2l(d.xerrneg), "hover").text; + } else { + d.xLabel += " \xB1 " + xeText; } - if(!isNaN(d.yerr) && !(d.ya.type === 'log' && d.yerr <= 0)) { - var yeText = Axes.tickText(d.ya, d.ya.c2l(d.yerr), 'hover').text; - if(d.yerrneg !== undefined) { - d.yLabel += ' +' + yeText + ' / -' + - Axes.tickText(d.ya, d.ya.c2l(d.yerrneg), 'hover').text; - } - else d.yLabel += ' ± ' + yeText; - if(hovermode === 'y') d.distance += 1; + // small distance penalty for error bars, so that if there are + // traces with errors and some without, the error bar label will + // hoist up to the point + if (hovermode === "x") d.distance += 1; + } + if (!isNaN(d.yerr) && !(d.ya.type === "log" && d.yerr <= 0)) { + var yeText = Axes.tickText(d.ya, d.ya.c2l(d.yerr), "hover").text; + if (d.yerrneg !== undefined) { + d.yLabel += " +" + + yeText + + " / -" + + Axes.tickText(d.ya, d.ya.c2l(d.yerrneg), "hover").text; + } else { + d.yLabel += " \xB1 " + yeText; } - var infomode = d.trace.hoverinfo; - if(infomode !== 'all') { - infomode = infomode.split('+'); - if(infomode.indexOf('x') === -1) d.xLabel = undefined; - if(infomode.indexOf('y') === -1) d.yLabel = undefined; - if(infomode.indexOf('z') === -1) d.zLabel = undefined; - if(infomode.indexOf('text') === -1) d.text = undefined; - if(infomode.indexOf('name') === -1) d.name = undefined; - } + if (hovermode === "y") d.distance += 1; + } + + var infomode = d.trace.hoverinfo; + if (infomode !== "all") { + infomode = infomode.split("+"); + if (infomode.indexOf("x") === -1) d.xLabel = undefined; + if (infomode.indexOf("y") === -1) d.yLabel = undefined; + if (infomode.indexOf("z") === -1) d.zLabel = undefined; + if (infomode.indexOf("text") === -1) d.text = undefined; + if (infomode.indexOf("name") === -1) d.name = undefined; + } - return d; + return d; } fx.loneHover = function(hoverItem, opts) { - // draw a single hover item in a pre-existing svg container somewhere - // hoverItem should have keys: - // - x and y (or x0, x1, y0, and y1): - // the pixel position to mark, relative to opts.container - // - xLabel, yLabel, zLabel, text, and name: - // info to go in the label - // - color: - // the background color for the label. text & outline color will - // be chosen black or white to contrast with this - // opts should have keys: - // - bgColor: - // the background color this is against, used if the trace is - // non-opaque, and for the name, which goes outside the box - // - container: - // a dom element - must be big enough to contain the whole - // hover label - var pointData = { - color: hoverItem.color || Color.defaultLine, - x0: hoverItem.x0 || hoverItem.x || 0, - x1: hoverItem.x1 || hoverItem.x || 0, - y0: hoverItem.y0 || hoverItem.y || 0, - y1: hoverItem.y1 || hoverItem.y || 0, - xLabel: hoverItem.xLabel, - yLabel: hoverItem.yLabel, - zLabel: hoverItem.zLabel, - text: hoverItem.text, - name: hoverItem.name, - idealAlign: hoverItem.idealAlign, - - // filler to make createHoverText happy - trace: { - index: 0, - hoverinfo: '' - }, - xa: {_offset: 0}, - ya: {_offset: 0}, - index: 0 - }; - - var container3 = d3.select(opts.container), - outerContainer3 = opts.outerContainer ? - d3.select(opts.outerContainer) : container3; - - var fullOpts = { - hovermode: 'closest', - rotateLabels: false, - bgColor: opts.bgColor || Color.background, - container: container3, - outerContainer: outerContainer3 - }; - - var hoverLabel = createHoverText([pointData], fullOpts); - alignHoverText(hoverLabel, fullOpts.rotateLabels); - - return hoverLabel.node(); + // draw a single hover item in a pre-existing svg container somewhere + // hoverItem should have keys: + // - x and y (or x0, x1, y0, and y1): + // the pixel position to mark, relative to opts.container + // - xLabel, yLabel, zLabel, text, and name: + // info to go in the label + // - color: + // the background color for the label. text & outline color will + // be chosen black or white to contrast with this + // opts should have keys: + // - bgColor: + // the background color this is against, used if the trace is + // non-opaque, and for the name, which goes outside the box + // - container: + // a dom element - must be big enough to contain the whole + // hover label + var pointData = { + color: hoverItem.color || Color.defaultLine, + x0: hoverItem.x0 || hoverItem.x || 0, + x1: hoverItem.x1 || hoverItem.x || 0, + y0: hoverItem.y0 || hoverItem.y || 0, + y1: hoverItem.y1 || hoverItem.y || 0, + xLabel: hoverItem.xLabel, + yLabel: hoverItem.yLabel, + zLabel: hoverItem.zLabel, + text: hoverItem.text, + name: hoverItem.name, + idealAlign: hoverItem.idealAlign, + // filler to make createHoverText happy + trace: { index: 0, hoverinfo: "" }, + xa: { _offset: 0 }, + ya: { _offset: 0 }, + index: 0 + }; + + var container3 = d3.select(opts.container), + outerContainer3 = opts.outerContainer + ? d3.select(opts.outerContainer) + : container3; + + var fullOpts = { + hovermode: "closest", + rotateLabels: false, + bgColor: opts.bgColor || Color.background, + container: container3, + outerContainer: outerContainer3 + }; + + var hoverLabel = createHoverText([pointData], fullOpts); + alignHoverText(hoverLabel, fullOpts.rotateLabels); + + return hoverLabel.node(); }; fx.loneUnhover = function(containerOrSelection) { - // duck type whether the arg is a d3 selection because ie9 doesn't - // handle instanceof like modern browsers do. - var selection = Lib.isD3Selection(containerOrSelection) ? - containerOrSelection : - d3.select(containerOrSelection); + // duck type whether the arg is a d3 selection because ie9 doesn't + // handle instanceof like modern browsers do. + var selection = Lib.isD3Selection(containerOrSelection) + ? containerOrSelection + : d3.select(containerOrSelection); - selection.selectAll('g.hovertext').remove(); + selection.selectAll("g.hovertext").remove(); }; function createHoverText(hoverData, opts) { - var hovermode = opts.hovermode, - rotateLabels = opts.rotateLabels, - bgColor = opts.bgColor, - container = opts.container, - outerContainer = opts.outerContainer, - - c0 = hoverData[0], - xa = c0.xa, - ya = c0.ya, - commonAttr = hovermode === 'y' ? 'yLabel' : 'xLabel', - t0 = c0[commonAttr], - t00 = (String(t0) || '').split(' ')[0], - outerContainerBB = outerContainer.node().getBoundingClientRect(), - outerTop = outerContainerBB.top, - outerWidth = outerContainerBB.width, - outerHeight = outerContainerBB.height; - - // show the common label, if any, on the axis - // never show a common label in array mode, - // even if sometimes there could be one - var showCommonLabel = c0.distance <= constants.MAXDIST && - (hovermode === 'x' || hovermode === 'y'); - - // all hover traces hoverinfo must contain the hovermode - // to have common labels - var i, traceHoverinfo; - for(i = 0; i < hoverData.length; i++) { - traceHoverinfo = hoverData[i].trace.hoverinfo; - var parts = traceHoverinfo.split('+'); - if(parts.indexOf('all') === -1 && - parts.indexOf(hovermode) === -1) { - showCommonLabel = false; - break; - } + var hovermode = opts.hovermode, + rotateLabels = opts.rotateLabels, + bgColor = opts.bgColor, + container = opts.container, + outerContainer = opts.outerContainer, + c0 = hoverData[0], + xa = c0.xa, + ya = c0.ya, + commonAttr = hovermode === "y" ? "yLabel" : "xLabel", + t0 = c0[commonAttr], + t00 = (String(t0) || "").split(" ")[0], + outerContainerBB = outerContainer.node().getBoundingClientRect(), + outerTop = outerContainerBB.top, + outerWidth = outerContainerBB.width, + outerHeight = outerContainerBB.height; + + // show the common label, if any, on the axis + // never show a common label in array mode, + // even if sometimes there could be one + var showCommonLabel = c0.distance <= constants.MAXDIST && + (hovermode === "x" || hovermode === "y"); + + // all hover traces hoverinfo must contain the hovermode + // to have common labels + var i, traceHoverinfo; + for (i = 0; i < hoverData.length; i++) { + traceHoverinfo = hoverData[i].trace.hoverinfo; + var parts = traceHoverinfo.split("+"); + if (parts.indexOf("all") === -1 && parts.indexOf(hovermode) === -1) { + showCommonLabel = false; + break; } - - var commonLabel = container.selectAll('g.axistext') - .data(showCommonLabel ? [0] : []); - commonLabel.enter().append('g') - .classed('axistext', true); - commonLabel.exit().remove(); - - commonLabel.each(function() { - var label = d3.select(this), - lpath = label.selectAll('path').data([0]), - ltext = label.selectAll('text').data([0]); - - lpath.enter().append('path') - .style({fill: Color.defaultLine, 'stroke-width': '1px', stroke: Color.background}); - ltext.enter().append('text') - .call(Drawing.font, HOVERFONT, HOVERFONTSIZE, Color.background) - // prohibit tex interpretation until we can handle - // tex and regular text together - .attr('data-notex', 1); - - ltext.text(t0) - .call(svgTextUtils.convertToTspans) - .call(Drawing.setPosition, 0, 0) - .selectAll('tspan.line') - .call(Drawing.setPosition, 0, 0); - label.attr('transform', ''); - - var tbb = ltext.node().getBoundingClientRect(); - if(hovermode === 'x') { - ltext.attr('text-anchor', 'middle') - .call(Drawing.setPosition, 0, (xa.side === 'top' ? - (outerTop - tbb.bottom - HOVERARROWSIZE - HOVERTEXTPAD) : - (outerTop - tbb.top + HOVERARROWSIZE + HOVERTEXTPAD))) - .selectAll('tspan.line') - .attr({ - x: ltext.attr('x'), - y: ltext.attr('y') - }); - - var topsign = xa.side === 'top' ? '-' : ''; - lpath.attr('d', 'M0,0' + - 'L' + HOVERARROWSIZE + ',' + topsign + HOVERARROWSIZE + - 'H' + (HOVERTEXTPAD + tbb.width / 2) + - 'v' + topsign + (HOVERTEXTPAD * 2 + tbb.height) + - 'H-' + (HOVERTEXTPAD + tbb.width / 2) + - 'V' + topsign + HOVERARROWSIZE + 'H-' + HOVERARROWSIZE + 'Z'); - - label.attr('transform', 'translate(' + - (xa._offset + (c0.x0 + c0.x1) / 2) + ',' + - (ya._offset + (xa.side === 'top' ? 0 : ya._length)) + ')'); - } - else { - ltext.attr('text-anchor', ya.side === 'right' ? 'start' : 'end') - .call(Drawing.setPosition, - (ya.side === 'right' ? 1 : -1) * (HOVERTEXTPAD + HOVERARROWSIZE), - outerTop - tbb.top - tbb.height / 2) - .selectAll('tspan.line') - .attr({ - x: ltext.attr('x'), - y: ltext.attr('y') - }); - - var leftsign = ya.side === 'right' ? '' : '-'; - lpath.attr('d', 'M0,0' + - 'L' + leftsign + HOVERARROWSIZE + ',' + HOVERARROWSIZE + - 'V' + (HOVERTEXTPAD + tbb.height / 2) + - 'h' + leftsign + (HOVERTEXTPAD * 2 + tbb.width) + - 'V-' + (HOVERTEXTPAD + tbb.height / 2) + - 'H' + leftsign + HOVERARROWSIZE + 'V-' + HOVERARROWSIZE + 'Z'); - - label.attr('transform', 'translate(' + - (xa._offset + (ya.side === 'right' ? xa._length : 0)) + ',' + - (ya._offset + (c0.y0 + c0.y1) / 2) + ')'); - } - // remove the "close but not quite" points - // because of error bars, only take up to a space - hoverData = hoverData.filter(function(d) { - return (d.zLabelVal !== undefined) || - (d[commonAttr] || '').split(' ')[0] === t00; - }); + } + + var commonLabel = container + .selectAll("g.axistext") + .data(showCommonLabel ? [0] : []); + commonLabel.enter().append("g").classed("axistext", true); + commonLabel.exit().remove(); + + commonLabel.each(function() { + var label = d3.select(this), + lpath = label.selectAll("path").data([0]), + ltext = label.selectAll("text").data([0]); + + lpath.enter().append("path").style({ + fill: Color.defaultLine, + "stroke-width": "1px", + stroke: Color.background }); + ltext + .enter() + .append("text") + .call(Drawing.font, HOVERFONT, HOVERFONTSIZE, Color.background) + .attr("data-notex", 1); + + ltext + .text(t0) + .call(svgTextUtils.convertToTspans) + .call(Drawing.setPosition, 0, 0) + .selectAll("tspan.line") + .call(Drawing.setPosition, 0, 0); + label.attr("transform", ""); + + var tbb = ltext.node().getBoundingClientRect(); + if (hovermode === "x") { + ltext + .attr("text-anchor", "middle") + .call( + Drawing.setPosition, + 0, + xa.side === "top" + ? outerTop - tbb.bottom - HOVERARROWSIZE - HOVERTEXTPAD + : outerTop - tbb.top + HOVERARROWSIZE + HOVERTEXTPAD + ) + .selectAll("tspan.line") + .attr({ x: ltext.attr("x"), y: ltext.attr("y") }); + + var topsign = xa.side === "top" ? "-" : ""; + lpath.attr( + "d", + "M0,0" + + "L" + + HOVERARROWSIZE + + "," + + topsign + + HOVERARROWSIZE + + "H" + + (HOVERTEXTPAD + tbb.width / 2) + + "v" + + topsign + + (HOVERTEXTPAD * 2 + tbb.height) + + "H-" + + (HOVERTEXTPAD + tbb.width / 2) + + "V" + + topsign + + HOVERARROWSIZE + + "H-" + + HOVERARROWSIZE + + "Z" + ); + + label.attr( + "transform", + "translate(" + + (xa._offset + (c0.x0 + c0.x1) / 2) + + "," + + (ya._offset + (xa.side === "top" ? 0 : ya._length)) + + ")" + ); + } else { + ltext + .attr("text-anchor", ya.side === "right" ? "start" : "end") + .call( + Drawing.setPosition, + (ya.side === "right" ? 1 : -1) * (HOVERTEXTPAD + HOVERARROWSIZE), + outerTop - tbb.top - tbb.height / 2 + ) + .selectAll("tspan.line") + .attr({ x: ltext.attr("x"), y: ltext.attr("y") }); + + var leftsign = ya.side === "right" ? "" : "-"; + lpath.attr( + "d", + "M0,0" + + "L" + + leftsign + + HOVERARROWSIZE + + "," + + HOVERARROWSIZE + + "V" + + (HOVERTEXTPAD + tbb.height / 2) + + "h" + + leftsign + + (HOVERTEXTPAD * 2 + tbb.width) + + "V-" + + (HOVERTEXTPAD + tbb.height / 2) + + "H" + + leftsign + + HOVERARROWSIZE + + "V-" + + HOVERARROWSIZE + + "Z" + ); + + label.attr( + "transform", + "translate(" + + (xa._offset + (ya.side === "right" ? xa._length : 0)) + + "," + + (ya._offset + (c0.y0 + c0.y1) / 2) + + ")" + ); + } + // remove the "close but not quite" points + // because of error bars, only take up to a space + hoverData = hoverData.filter(function(d) { + return d.zLabelVal !== undefined || + (d[commonAttr] || "").split(" ")[0] === t00; + }); + }); + + // show all the individual labels + // first create the objects + var hoverLabels = container + .selectAll("g.hovertext") + .data(hoverData, function(d) { + return [ + d.trace.index, + d.index, + d.x0, + d.y0, + d.name, + d.attr, + d.xa, + d.ya || "" + ].join(","); + }); + hoverLabels.enter().append("g").classed("hovertext", true).each(function() { + var g = d3.select(this); + // trace name label (rect and text.name) + g.append("rect").call(Color.fill, Color.addOpacity(bgColor, 0.8)); + g + .append("text") + .classed("name", true) + .call(Drawing.font, HOVERFONT, HOVERFONTSIZE); + // trace data label (path and text.nums) + g.append("path").style("stroke-width", "1px"); + g + .append("text") + .classed("nums", true) + .call(Drawing.font, HOVERFONT, HOVERFONTSIZE); + }); + hoverLabels.exit().remove(); + + // then put the text in, position the pointer to the data, + // and figure out sizes + hoverLabels.each(function(d) { + var g = d3.select(this).attr("transform", ""), + name = "", + text = "", + // combine possible non-opaque trace color with bgColor + baseColor = Color.opacity(d.color) ? d.color : Color.defaultLine, + traceColor = Color.combine(baseColor, bgColor), + // find a contrasting color for border and text + contrastColor = tinycolor(traceColor).getBrightness() > 128 + ? "#000" + : Color.background; + + if (d.name && d.zLabelVal === undefined) { + // strip out our pseudo-html elements from d.name (if it exists at all) + name = svgTextUtils.plainText(d.name || ""); + + if (name.length > 15) name = name.substr(0, 12) + "..."; + } - // show all the individual labels - - // first create the objects - var hoverLabels = container.selectAll('g.hovertext') - .data(hoverData, function(d) { - return [d.trace.index, d.index, d.x0, d.y0, d.name, d.attr, d.xa, d.ya || ''].join(','); - }); - hoverLabels.enter().append('g') - .classed('hovertext', true) - .each(function() { - var g = d3.select(this); - // trace name label (rect and text.name) - g.append('rect') - .call(Color.fill, Color.addOpacity(bgColor, 0.8)); - g.append('text').classed('name', true) - .call(Drawing.font, HOVERFONT, HOVERFONTSIZE); - // trace data label (path and text.nums) - g.append('path') - .style('stroke-width', '1px'); - g.append('text').classed('nums', true) - .call(Drawing.font, HOVERFONT, HOVERFONTSIZE); - }); - hoverLabels.exit().remove(); - - // then put the text in, position the pointer to the data, - // and figure out sizes - hoverLabels.each(function(d) { - var g = d3.select(this).attr('transform', ''), - name = '', - text = '', - // combine possible non-opaque trace color with bgColor - baseColor = Color.opacity(d.color) ? - d.color : Color.defaultLine, - traceColor = Color.combine(baseColor, bgColor), - - // find a contrasting color for border and text - contrastColor = tinycolor(traceColor).getBrightness() > 128 ? - '#000' : Color.background; - - - if(d.name && d.zLabelVal === undefined) { - // strip out our pseudo-html elements from d.name (if it exists at all) - name = svgTextUtils.plainText(d.name || ''); - - if(name.length > 15) name = name.substr(0, 12) + '...'; - } - - // used by other modules (initially just ternary) that - // manage their own hoverinfo independent of cleanPoint - // the rest of this will still apply, so such modules - // can still put things in (x|y|z)Label, text, and name - // and hoverinfo will still determine their visibility - if(d.extraText !== undefined) text += d.extraText; - - if(d.zLabel !== undefined) { - if(d.xLabel !== undefined) text += 'x: ' + d.xLabel + '
'; - if(d.yLabel !== undefined) text += 'y: ' + d.yLabel + '
'; - text += (text ? 'z: ' : '') + d.zLabel; - } - else if(showCommonLabel && d[hovermode + 'Label'] === t0) { - text = d[(hovermode === 'x' ? 'y' : 'x') + 'Label'] || ''; - } - else if(d.xLabel === undefined) { - if(d.yLabel !== undefined) text = d.yLabel; - } - else if(d.yLabel === undefined) text = d.xLabel; - else text = '(' + d.xLabel + ', ' + d.yLabel + ')'; - - if(d.text && !Array.isArray(d.text)) text += (text ? '
' : '') + d.text; - - // if 'text' is empty at this point, - // put 'name' in main label and don't show secondary label - if(text === '') { - // if 'name' is also empty, remove entire label - if(name === '') g.remove(); - text = name; - } + // used by other modules (initially just ternary) that + // manage their own hoverinfo independent of cleanPoint + // the rest of this will still apply, so such modules + // can still put things in (x|y|z)Label, text, and name + // and hoverinfo will still determine their visibility + if (d.extraText !== undefined) text += d.extraText; + + if (d.zLabel !== undefined) { + if (d.xLabel !== undefined) text += "x: " + d.xLabel + "
"; + if (d.yLabel !== undefined) text += "y: " + d.yLabel + "
"; + text += (text ? "z: " : "") + d.zLabel; + } else if (showCommonLabel && d[hovermode + "Label"] === t0) { + text = d[(hovermode === "x" ? "y" : "x") + "Label"] || ""; + } else if (d.xLabel === undefined) { + if (d.yLabel !== undefined) text = d.yLabel; + } else if (d.yLabel === undefined) text = d.xLabel; + else text = "(" + d.xLabel + ", " + d.yLabel + ")"; + + if (d.text && !Array.isArray(d.text)) text += (text ? "
" : "") + d.text; + + // if 'text' is empty at this point, + // put 'name' in main label and don't show secondary label + if (text === "") { + // if 'name' is also empty, remove entire label + if (name === "") g.remove(); + text = name; + } - // main label - var tx = g.select('text.nums') - .style('fill', contrastColor) - .call(Drawing.setPosition, 0, 0) - .text(text) - .attr('data-notex', 1) - .call(svgTextUtils.convertToTspans); - tx.selectAll('tspan.line') - .call(Drawing.setPosition, 0, 0); - - var tx2 = g.select('text.name'), - tx2width = 0; - - // secondary label for non-empty 'name' - if(name && name !== text) { - tx2.style('fill', traceColor) - .text(name) - .call(Drawing.setPosition, 0, 0) - .attr('data-notex', 1) - .call(svgTextUtils.convertToTspans); - tx2.selectAll('tspan.line') - .call(Drawing.setPosition, 0, 0); - tx2width = tx2.node().getBoundingClientRect().width + 2 * HOVERTEXTPAD; - } - else { - tx2.remove(); - g.select('rect').remove(); - } + // main label + var tx = g + .select("text.nums") + .style("fill", contrastColor) + .call(Drawing.setPosition, 0, 0) + .text(text) + .attr("data-notex", 1) + .call(svgTextUtils.convertToTspans); + tx.selectAll("tspan.line").call(Drawing.setPosition, 0, 0); + + var tx2 = g.select("text.name"), tx2width = 0; + + // secondary label for non-empty 'name' + if (name && name !== text) { + tx2 + .style("fill", traceColor) + .text(name) + .call(Drawing.setPosition, 0, 0) + .attr("data-notex", 1) + .call(svgTextUtils.convertToTspans); + tx2.selectAll("tspan.line").call(Drawing.setPosition, 0, 0); + tx2width = tx2.node().getBoundingClientRect().width + 2 * HOVERTEXTPAD; + } else { + tx2.remove(); + g.select("rect").remove(); + } - g.select('path') - .style({ - fill: traceColor, - stroke: contrastColor - }); - var tbb = tx.node().getBoundingClientRect(), - htx = d.xa._offset + (d.x0 + d.x1) / 2, - hty = d.ya._offset + (d.y0 + d.y1) / 2, - dx = Math.abs(d.x1 - d.x0), - dy = Math.abs(d.y1 - d.y0), - txTotalWidth = tbb.width + HOVERARROWSIZE + HOVERTEXTPAD + tx2width, - anchorStartOK, - anchorEndOK; - - d.ty0 = outerTop - tbb.top; - d.bx = tbb.width + 2 * HOVERTEXTPAD; - d.by = tbb.height + 2 * HOVERTEXTPAD; - d.anchor = 'start'; - d.txwidth = tbb.width; - d.tx2width = tx2width; - d.offset = 0; - - if(rotateLabels) { - d.pos = htx; - anchorStartOK = hty + dy / 2 + txTotalWidth <= outerHeight; - anchorEndOK = hty - dy / 2 - txTotalWidth >= 0; - if((d.idealAlign === 'top' || !anchorStartOK) && anchorEndOK) { - hty -= dy / 2; - d.anchor = 'end'; - } else if(anchorStartOK) { - hty += dy / 2; - d.anchor = 'start'; - } else d.anchor = 'middle'; - } - else { - d.pos = hty; - anchorStartOK = htx + dx / 2 + txTotalWidth <= outerWidth; - anchorEndOK = htx - dx / 2 - txTotalWidth >= 0; - if((d.idealAlign === 'left' || !anchorStartOK) && anchorEndOK) { - htx -= dx / 2; - d.anchor = 'end'; - } else if(anchorStartOK) { - htx += dx / 2; - d.anchor = 'start'; - } else d.anchor = 'middle'; - } + g.select("path").style({ fill: traceColor, stroke: contrastColor }); + var tbb = tx.node().getBoundingClientRect(), + htx = d.xa._offset + (d.x0 + d.x1) / 2, + hty = d.ya._offset + (d.y0 + d.y1) / 2, + dx = Math.abs(d.x1 - d.x0), + dy = Math.abs(d.y1 - d.y0), + txTotalWidth = tbb.width + HOVERARROWSIZE + HOVERTEXTPAD + tx2width, + anchorStartOK, + anchorEndOK; + + d.ty0 = outerTop - tbb.top; + d.bx = tbb.width + 2 * HOVERTEXTPAD; + d.by = tbb.height + 2 * HOVERTEXTPAD; + d.anchor = "start"; + d.txwidth = tbb.width; + d.tx2width = tx2width; + d.offset = 0; + + if (rotateLabels) { + d.pos = htx; + anchorStartOK = hty + dy / 2 + txTotalWidth <= outerHeight; + anchorEndOK = hty - dy / 2 - txTotalWidth >= 0; + if ((d.idealAlign === "top" || !anchorStartOK) && anchorEndOK) { + hty -= dy / 2; + d.anchor = "end"; + } else if (anchorStartOK) { + hty += dy / 2; + d.anchor = "start"; + } else { + d.anchor = "middle"; + } + } else { + d.pos = hty; + anchorStartOK = htx + dx / 2 + txTotalWidth <= outerWidth; + anchorEndOK = htx - dx / 2 - txTotalWidth >= 0; + if ((d.idealAlign === "left" || !anchorStartOK) && anchorEndOK) { + htx -= dx / 2; + d.anchor = "end"; + } else if (anchorStartOK) { + htx += dx / 2; + d.anchor = "start"; + } else { + d.anchor = "middle"; + } + } - tx.attr('text-anchor', d.anchor); - if(tx2width) tx2.attr('text-anchor', d.anchor); - g.attr('transform', 'translate(' + htx + ',' + hty + ')' + - (rotateLabels ? 'rotate(' + YANGLE + ')' : '')); - }); + tx.attr("text-anchor", d.anchor); + if (tx2width) tx2.attr("text-anchor", d.anchor); + g.attr( + "transform", + "translate(" + + htx + + "," + + hty + + ")" + + (rotateLabels ? "rotate(" + YANGLE + ")" : "") + ); + }); - return hoverLabels; + return hoverLabels; } // Make groups of touching points, and within each group @@ -1095,265 +1232,312 @@ function createHoverText(hoverData, opts) { // the other, though it hardly matters - there's just too much // information then. function hoverAvoidOverlaps(hoverData, ax) { - var nummoves = 0, - - // make groups of touching points - pointgroups = hoverData - .map(function(d, i) { - var axis = d[ax]; - return [{ - i: i, - dp: 0, - pos: d.pos, - posref: d.posref, - size: d.by * (axis._id.charAt(0) === 'x' ? YFACTOR : 1) / 2, - pmin: axis._offset, - pmax: axis._offset + axis._length - }]; - }) - .sort(function(a, b) { return a[0].posref - b[0].posref; }), - donepositioning, - topOverlap, - bottomOverlap, - i, j, - pti, - sumdp; - - function constrainGroup(grp) { - var minPt = grp[0], - maxPt = grp[grp.length - 1]; - - // overlap with the top - positive vals are overlaps - topOverlap = minPt.pmin - minPt.pos - minPt.dp + minPt.size; - - // overlap with the bottom - positive vals are overlaps - bottomOverlap = maxPt.pos + maxPt.dp + maxPt.size - minPt.pmax; - - // check for min overlap first, so that we always - // see the largest labels - // allow for .01px overlap, so we don't get an - // infinite loop from rounding errors - if(topOverlap > 0.01) { - for(j = grp.length - 1; j >= 0; j--) grp[j].dp += topOverlap; - donepositioning = false; + var nummoves = 0, + // make groups of touching points + pointgroups = hoverData + .map(function(d, i) { + var axis = d[ax]; + return [ + { + i: i, + dp: 0, + pos: d.pos, + posref: d.posref, + size: d.by * (axis._id.charAt(0) === "x" ? YFACTOR : 1) / 2, + pmin: axis._offset, + pmax: axis._offset + axis._length + } + ]; + }) + .sort(function(a, b) { + return a[0].posref - b[0].posref; + }), + donepositioning, + topOverlap, + bottomOverlap, + i, + j, + pti, + sumdp; + + function constrainGroup(grp) { + var minPt = grp[0], maxPt = grp[grp.length - 1]; + + // overlap with the top - positive vals are overlaps + topOverlap = minPt.pmin - minPt.pos - minPt.dp + minPt.size; + + // overlap with the bottom - positive vals are overlaps + bottomOverlap = maxPt.pos + maxPt.dp + maxPt.size - minPt.pmax; + + // check for min overlap first, so that we always + // see the largest labels + // allow for .01px overlap, so we don't get an + // infinite loop from rounding errors + if (topOverlap > 0.01) { + for (j = grp.length - 1; j >= 0; j--) { + grp[j].dp += topOverlap; + } + donepositioning = false; + } + if (bottomOverlap < 0.01) return; + if (topOverlap < -0.01) { + // make sure we're not pushing back and forth + for (j = grp.length - 1; j >= 0; j--) { + grp[j].dp -= bottomOverlap; + } + donepositioning = false; + } + if (!donepositioning) return; + + // no room to fix positioning, delete off-screen points + // first see how many points we need to delete + var deleteCount = 0; + for (i = 0; i < grp.length; i++) { + pti = grp[i]; + if (pti.pos + pti.dp + pti.size > minPt.pmax) deleteCount++; + } + + // start by deleting points whose data is off screen + for (i = grp.length - 1; i >= 0; i--) { + if (deleteCount <= 0) break; + pti = grp[i]; + + // pos has already been constrained to [pmin,pmax] + // so look for points close to that to delete + if (pti.pos > minPt.pmax - 1) { + pti.del = true; + deleteCount--; + } + } + for (i = 0; i < grp.length; i++) { + if (deleteCount <= 0) break; + pti = grp[i]; + + // pos has already been constrained to [pmin,pmax] + // so look for points close to that to delete + if (pti.pos < minPt.pmin + 1) { + pti.del = true; + deleteCount--; + + // shift the whole group minus into this new space + bottomOverlap = pti.size * 2; + for (j = grp.length - 1; j >= 0; j--) { + grp[j].dp -= bottomOverlap; } - if(bottomOverlap < 0.01) return; - if(topOverlap < -0.01) { - // make sure we're not pushing back and forth - for(j = grp.length - 1; j >= 0; j--) grp[j].dp -= bottomOverlap; - donepositioning = false; + } + } + // then delete points that go off the bottom + for (i = grp.length - 1; i >= 0; i--) { + if (deleteCount <= 0) break; + pti = grp[i]; + if (pti.pos + pti.dp + pti.size > minPt.pmax) { + pti.del = true; + deleteCount--; + } + } + } + + // loop through groups, combining them if they overlap, + // until nothing moves + while (!donepositioning && nummoves <= hoverData.length) { + // to avoid infinite loops, don't move more times + // than there are traces + nummoves++; + + // assume nothing will move in this iteration, + // reverse this if it does + donepositioning = true; + i = 0; + while (i < pointgroups.length - 1) { + // the higher (g0) and lower (g1) point group + var g0 = pointgroups[i], + g1 = pointgroups[i + 1], + // the lowest point in the higher group (p0) + // the highest point in the lower group (p1) + p0 = g0[g0.length - 1], + p1 = g1[0]; + topOverlap = p0.pos + p0.dp + p0.size - p1.pos - p1.dp + p1.size; + + // Only group points that lie on the same axes + if (topOverlap > 0.01 && p0.pmin === p1.pmin && p0.pmax === p1.pmax) { + // push the new point(s) added to this group out of the way + for (j = g1.length - 1; j >= 0; j--) { + g1[j].dp += topOverlap; } - if(!donepositioning) return; - - // no room to fix positioning, delete off-screen points - // first see how many points we need to delete - var deleteCount = 0; - for(i = 0; i < grp.length; i++) { - pti = grp[i]; - if(pti.pos + pti.dp + pti.size > minPt.pmax) deleteCount++; - } + // add them to the group + g0.push.apply(g0, g1); + pointgroups.splice(i + 1, 1); - // start by deleting points whose data is off screen - for(i = grp.length - 1; i >= 0; i--) { - if(deleteCount <= 0) break; - pti = grp[i]; - - // pos has already been constrained to [pmin,pmax] - // so look for points close to that to delete - if(pti.pos > minPt.pmax - 1) { - pti.del = true; - deleteCount--; - } + // adjust for minimum average movement + sumdp = 0; + for (j = g0.length - 1; j >= 0; j--) { + sumdp += g0[j].dp; } - for(i = 0; i < grp.length; i++) { - if(deleteCount <= 0) break; - pti = grp[i]; - - // pos has already been constrained to [pmin,pmax] - // so look for points close to that to delete - if(pti.pos < minPt.pmin + 1) { - pti.del = true; - deleteCount--; - - // shift the whole group minus into this new space - bottomOverlap = pti.size * 2; - for(j = grp.length - 1; j >= 0; j--) grp[j].dp -= bottomOverlap; - } - } - // then delete points that go off the bottom - for(i = grp.length - 1; i >= 0; i--) { - if(deleteCount <= 0) break; - pti = grp[i]; - if(pti.pos + pti.dp + pti.size > minPt.pmax) { - pti.del = true; - deleteCount--; - } + bottomOverlap = sumdp / g0.length; + for (j = g0.length - 1; j >= 0; j--) { + g0[j].dp -= bottomOverlap; } + donepositioning = false; + } else { + i++; + } } - // loop through groups, combining them if they overlap, - // until nothing moves - while(!donepositioning && nummoves <= hoverData.length) { - // to avoid infinite loops, don't move more times - // than there are traces - nummoves++; - - // assume nothing will move in this iteration, - // reverse this if it does - donepositioning = true; - i = 0; - while(i < pointgroups.length - 1) { - // the higher (g0) and lower (g1) point group - var g0 = pointgroups[i], - g1 = pointgroups[i + 1], - - // the lowest point in the higher group (p0) - // the highest point in the lower group (p1) - p0 = g0[g0.length - 1], - p1 = g1[0]; - topOverlap = p0.pos + p0.dp + p0.size - p1.pos - p1.dp + p1.size; - - // Only group points that lie on the same axes - if(topOverlap > 0.01 && (p0.pmin === p1.pmin) && (p0.pmax === p1.pmax)) { - // push the new point(s) added to this group out of the way - for(j = g1.length - 1; j >= 0; j--) g1[j].dp += topOverlap; - - // add them to the group - g0.push.apply(g0, g1); - pointgroups.splice(i + 1, 1); - - // adjust for minimum average movement - sumdp = 0; - for(j = g0.length - 1; j >= 0; j--) sumdp += g0[j].dp; - bottomOverlap = sumdp / g0.length; - for(j = g0.length - 1; j >= 0; j--) g0[j].dp -= bottomOverlap; - donepositioning = false; - } - else i++; - } - - // check if we're going off the plot on either side and fix - pointgroups.forEach(constrainGroup); - } - - // now put these offsets into hoverData - for(i = pointgroups.length - 1; i >= 0; i--) { - var grp = pointgroups[i]; - for(j = grp.length - 1; j >= 0; j--) { - var pt = grp[j], - hoverPt = hoverData[pt.i]; - hoverPt.offset = pt.dp; - hoverPt.del = pt.del; - } + // check if we're going off the plot on either side and fix + pointgroups.forEach(constrainGroup); + } + + // now put these offsets into hoverData + for (i = pointgroups.length - 1; i >= 0; i--) { + var grp = pointgroups[i]; + for (j = grp.length - 1; j >= 0; j--) { + var pt = grp[j], hoverPt = hoverData[pt.i]; + hoverPt.offset = pt.dp; + hoverPt.del = pt.del; } + } } function alignHoverText(hoverLabels, rotateLabels) { - // finally set the text positioning relative to the data and draw the - // box around it - hoverLabels.each(function(d) { - var g = d3.select(this); - if(d.del) { - g.remove(); - return; - } - var horzSign = d.anchor === 'end' ? -1 : 1, - tx = g.select('text.nums'), - alignShift = {start: 1, end: -1, middle: 0}[d.anchor], - txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD), - tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD), - offsetX = 0, - offsetY = d.offset; - if(d.anchor === 'middle') { - txx -= d.tx2width / 2; - tx2x -= d.tx2width / 2; - } - if(rotateLabels) { - offsetY *= -YSHIFTY; - offsetX = d.offset * YSHIFTX; - } + // finally set the text positioning relative to the data and draw the + // box around it + hoverLabels.each(function(d) { + var g = d3.select(this); + if (d.del) { + g.remove(); + return; + } + var horzSign = d.anchor === "end" ? -1 : 1, + tx = g.select("text.nums"), + alignShift = ({ start: 1, end: -1, middle: 0 })[d.anchor], + txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD), + tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD), + offsetX = 0, + offsetY = d.offset; + if (d.anchor === "middle") { + txx -= d.tx2width / 2; + tx2x -= d.tx2width / 2; + } + if (rotateLabels) { + offsetY *= -YSHIFTY; + offsetX = d.offset * YSHIFTX; + } - g.select('path').attr('d', d.anchor === 'middle' ? - // middle aligned: rect centered on data - ('M-' + (d.bx / 2) + ',-' + (d.by / 2) + 'h' + d.bx + 'v' + d.by + 'h-' + d.bx + 'Z') : - // left or right aligned: side rect with arrow to data - ('M0,0L' + (horzSign * HOVERARROWSIZE + offsetX) + ',' + (HOVERARROWSIZE + offsetY) + - 'v' + (d.by / 2 - HOVERARROWSIZE) + - 'h' + (horzSign * d.bx) + - 'v-' + d.by + - 'H' + (horzSign * HOVERARROWSIZE + offsetX) + - 'V' + (offsetY - HOVERARROWSIZE) + - 'Z')); - - tx.call(Drawing.setPosition, - txx + offsetX, offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD) - .selectAll('tspan.line') - .attr({ - x: tx.attr('x'), - y: tx.attr('y') - }); - - if(d.tx2width) { - g.select('text.name, text.name tspan.line') - .call(Drawing.setPosition, - tx2x + alignShift * HOVERTEXTPAD + offsetX, - offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD); - g.select('rect') - .call(Drawing.setRect, - tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX, - offsetY - d.by / 2 - 1, - d.tx2width, d.by + 2); - } - }); + g.select("path").attr( + "d", + d.anchor === "middle" // middle aligned: rect centered on data + ? "M-" + + d.bx / 2 + + ",-" + + d.by / 2 + + "h" + + d.bx + + "v" + + d.by + + "h-" + + d.bx + + "Z" // left or right aligned: side rect with arrow to data + : "M0,0L" + + (horzSign * HOVERARROWSIZE + offsetX) + + "," + + (HOVERARROWSIZE + offsetY) + + "v" + + (d.by / 2 - HOVERARROWSIZE) + + "h" + + horzSign * d.bx + + "v-" + + d.by + + "H" + + (horzSign * HOVERARROWSIZE + offsetX) + + "V" + + (offsetY - HOVERARROWSIZE) + + "Z" + ); + + tx + .call( + Drawing.setPosition, + txx + offsetX, + offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD + ) + .selectAll("tspan.line") + .attr({ x: tx.attr("x"), y: tx.attr("y") }); + + if (d.tx2width) { + g + .select("text.name, text.name tspan.line") + .call( + Drawing.setPosition, + tx2x + alignShift * HOVERTEXTPAD + offsetX, + offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD + ); + g + .select("rect") + .call( + Drawing.setRect, + tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX, + offsetY - d.by / 2 - 1, + d.tx2width, + d.by + 2 + ); + } + }); } function hoverChanged(gd, evt, oldhoverdata) { - // don't emit any events if nothing changed or - // if fx.hover was called manually - if(!evt.target) return false; - if(!oldhoverdata || oldhoverdata.length !== gd._hoverdata.length) return true; - - for(var i = oldhoverdata.length - 1; i >= 0; i--) { - var oldPt = oldhoverdata[i], - newPt = gd._hoverdata[i]; - if(oldPt.curveNumber !== newPt.curveNumber || - String(oldPt.pointNumber) !== String(newPt.pointNumber)) { - return true; - } + // don't emit any events if nothing changed or + // if fx.hover was called manually + if (!evt.target) return false; + if (!oldhoverdata || oldhoverdata.length !== gd._hoverdata.length) { + return true; + } + + for (var i = oldhoverdata.length - 1; i >= 0; i--) { + var oldPt = oldhoverdata[i], newPt = gd._hoverdata[i]; + if ( + oldPt.curveNumber !== newPt.curveNumber || + String(oldPt.pointNumber) !== String(newPt.pointNumber) + ) { + return true; } - return false; + } + return false; } // on click fx.click = function(gd, evt) { - var annotationsDone = Registry.getComponentMethod('annotations', 'onClick')(gd, gd._hoverdata); - - function emitClick() { gd.emit('plotly_click', {points: gd._hoverdata}); } - - if(gd._hoverdata && evt && evt.target) { - if(annotationsDone && annotationsDone.then) { - annotationsDone.then(emitClick); - } - else emitClick(); - - // why do we get a double event without this??? - if(evt.stopImmediatePropagation) evt.stopImmediatePropagation(); + var annotationsDone = Registry.getComponentMethod("annotations", "onClick")( + gd, + gd._hoverdata + ); + + function emitClick() { + gd.emit("plotly_click", { points: gd._hoverdata }); + } + + if (gd._hoverdata && evt && evt.target) { + if (annotationsDone && annotationsDone.then) { + annotationsDone.then(emitClick); + } else { + emitClick(); } -}; + // why do we get a double event without this??? + if (evt.stopImmediatePropagation) evt.stopImmediatePropagation(); + } +}; // for bar charts and others with finite-size objects: you must be inside // it to see its hover info, so distance is infinite outside. // But make distance inside be at least 1/4 MAXDIST, and a little bigger // for bigger bars, to prioritize scatter and smaller bars over big bars - // note that for closest mode, two inbox's will get added in quadrature // args are (signed) difference from the two opposite edges // count one edge as in, so that over continuous ranges you never get a gap fx.inbox = function(v0, v1) { - if(v0 * v1 < 0 || v0 === 0) { - return constants.MAXDIST * (0.6 - 0.3 / Math.max(3, Math.abs(v0 - v1))); - } - return Infinity; + if (v0 * v1 < 0 || v0 === 0) { + return constants.MAXDIST * (0.6 - 0.3 / Math.max(3, Math.abs(v0 - v1))); + } + return Infinity; }; diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index d8a78abd677..2e53fec2917 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -5,371 +5,366 @@ * 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 d3 = require("d3"); +var Lib = require("../../lib"); +var Plots = require("../plots"); +var Axes = require("./axes"); +var constants = require("./constants"); +exports.name = "cartesian"; -'use strict'; +exports.attr = ["xaxis", "yaxis"]; -var d3 = require('d3'); -var Lib = require('../../lib'); -var Plots = require('../plots'); -var Axes = require('./axes'); -var constants = require('./constants'); - -exports.name = 'cartesian'; - -exports.attr = ['xaxis', 'yaxis']; - -exports.idRoot = ['x', 'y']; +exports.idRoot = ["x", "y"]; exports.idRegex = constants.idRegex; exports.attrRegex = constants.attrRegex; -exports.attributes = require('./attributes'); +exports.attributes = require("./attributes"); -exports.layoutAttributes = require('./layout_attributes'); +exports.layoutAttributes = require("./layout_attributes"); -exports.transitionAxes = require('./transition_axes'); +exports.transitionAxes = require("./transition_axes"); exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) { - var fullLayout = gd._fullLayout, - subplots = Plots.getSubplotIds(fullLayout, 'cartesian'), - calcdata = gd.calcdata, - i; - - // If traces is not provided, then it's a complete replot and missing - // traces are removed - if(!Array.isArray(traces)) { - traces = []; - - for(i = 0; i < calcdata.length; i++) { - traces.push(i); - } + var fullLayout = gd._fullLayout, + subplots = Plots.getSubplotIds(fullLayout, "cartesian"), + calcdata = gd.calcdata, + i; + + // If traces is not provided, then it's a complete replot and missing + // traces are removed + if (!Array.isArray(traces)) { + traces = []; + + for (i = 0; i < calcdata.length; i++) { + traces.push(i); } - - for(i = 0; i < subplots.length; i++) { - var subplot = subplots[i], - subplotInfo = fullLayout._plots[subplot]; - - // Get all calcdata for this subplot: - var cdSubplot = []; - var pcd; - - for(var j = 0; j < calcdata.length; j++) { - var cd = calcdata[j], - trace = cd[0].trace; - - // Skip trace if whitelist provided and it's not whitelisted: - // if (Array.isArray(traces) && traces.indexOf(i) === -1) continue; - if(trace.xaxis + trace.yaxis === subplot) { - // If this trace is specifically requested, add it to the list: - if(traces.indexOf(trace.index) !== -1) { - // Okay, so example: traces 0, 1, and 2 have fill = tonext. You animate - // traces 0 and 2. Trace 1 also needs to be updated, otherwise its fill - // is outdated. So this retroactively adds the previous trace if the - // traces are interdependent. - if( - pcd && - pcd[0].trace.xaxis + pcd[0].trace.yaxis === subplot && - ['tonextx', 'tonexty', 'tonext'].indexOf(trace.fill) !== -1 && - cdSubplot.indexOf(pcd) === -1 - ) { - cdSubplot.push(pcd); - } - - cdSubplot.push(cd); - } - - // Track the previous trace on this subplot for the retroactive-add step - // above: - pcd = cd; - } + } + + for (i = 0; i < subplots.length; i++) { + var subplot = subplots[i], subplotInfo = fullLayout._plots[subplot]; + + // Get all calcdata for this subplot: + var cdSubplot = []; + var pcd; + + for (var j = 0; j < calcdata.length; j++) { + var cd = calcdata[j], trace = cd[0].trace; + + // Skip trace if whitelist provided and it's not whitelisted: + // if (Array.isArray(traces) && traces.indexOf(i) === -1) continue; + if (trace.xaxis + trace.yaxis === subplot) { + // If this trace is specifically requested, add it to the list: + if (traces.indexOf(trace.index) !== -1) { + // Okay, so example: traces 0, 1, and 2 have fill = tonext. You animate + // traces 0 and 2. Trace 1 also needs to be updated, otherwise its fill + // is outdated. So this retroactively adds the previous trace if the + // traces are interdependent. + if ( + pcd && + pcd[0].trace.xaxis + pcd[0].trace.yaxis === subplot && + ["tonextx", "tonexty", "tonext"].indexOf(trace.fill) !== -1 && + cdSubplot.indexOf(pcd) === -1 + ) { + cdSubplot.push(pcd); + } + + cdSubplot.push(cd); } - plotOne(gd, subplotInfo, cdSubplot, transitionOpts, makeOnCompleteCallback); + // Track the previous trace on this subplot for the retroactive-add step + // above: + pcd = cd; + } } + + plotOne(gd, subplotInfo, cdSubplot, transitionOpts, makeOnCompleteCallback); + } }; -function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback) { - var fullLayout = gd._fullLayout, - modules = fullLayout._modules; - - // remove old traces, then redraw everything - // - // TODO: scatterlayer is manually excluded from this since it knows how - // to update instead of fully removing and redrawing every time. The - // remaining plot traces should also be able to do this. Once implemented, - // we won't need this - which should sometimes be a big speedup. - if(plotinfo.plot) { - plotinfo.plot.selectAll('g:not(.scatterlayer)').selectAll('g.trace').remove(); +function plotOne( + gd, + plotinfo, + cdSubplot, + transitionOpts, + makeOnCompleteCallback +) { + var fullLayout = gd._fullLayout, modules = fullLayout._modules; + + // remove old traces, then redraw everything + // + // TODO: scatterlayer is manually excluded from this since it knows how + // to update instead of fully removing and redrawing every time. The + // remaining plot traces should also be able to do this. Once implemented, + // we won't need this - which should sometimes be a big speedup. + if (plotinfo.plot) { + plotinfo.plot + .selectAll("g:not(.scatterlayer)") + .selectAll("g.trace") + .remove(); + } + + // plot all traces for each module at once + for (var j = 0; j < modules.length; j++) { + var _module = modules[j]; + + // skip over non-cartesian trace modules + if (_module.basePlotModule.name !== "cartesian") continue; + + // plot all traces of this type on this subplot at once + var cdModule = []; + for (var k = 0; k < cdSubplot.length; k++) { + var cd = cdSubplot[k], trace = cd[0].trace; + + if (trace._module === _module && trace.visible === true) { + cdModule.push(cd); + } } - // plot all traces for each module at once - for(var j = 0; j < modules.length; j++) { - var _module = modules[j]; - - // skip over non-cartesian trace modules - if(_module.basePlotModule.name !== 'cartesian') continue; - - // plot all traces of this type on this subplot at once - var cdModule = []; - for(var k = 0; k < cdSubplot.length; k++) { - var cd = cdSubplot[k], - trace = cd[0].trace; - - if((trace._module === _module) && (trace.visible === true)) { - cdModule.push(cd); - } - } - - _module.plot(gd, plotinfo, cdModule, transitionOpts, makeOnCompleteCallback); - } + _module.plot( + gd, + plotinfo, + cdModule, + transitionOpts, + makeOnCompleteCallback + ); + } } -exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldModules = oldFullLayout._modules || [], - newModules = newFullLayout._modules || []; - - var hadScatter, hasScatter, i; - - for(i = 0; i < oldModules.length; i++) { - if(oldModules[i].name === 'scatter') { - hadScatter = true; - break; - } +exports.clean = function( + newFullData, + newFullLayout, + oldFullData, + oldFullLayout +) { + var oldModules = oldFullLayout._modules || [], + newModules = newFullLayout._modules || []; + + var hadScatter, hasScatter, i; + + for (i = 0; i < oldModules.length; i++) { + if (oldModules[i].name === "scatter") { + hadScatter = true; + break; } + } - for(i = 0; i < newModules.length; i++) { - if(newModules[i].name === 'scatter') { - hasScatter = true; - break; - } + for (i = 0; i < newModules.length; i++) { + if (newModules[i].name === "scatter") { + hasScatter = true; + break; } + } - if(hadScatter && !hasScatter) { - var oldPlots = oldFullLayout._plots, - ids = Object.keys(oldPlots || {}); + if (hadScatter && !hasScatter) { + var oldPlots = oldFullLayout._plots, ids = Object.keys(oldPlots || {}); - for(i = 0; i < ids.length; i++) { - var subplotInfo = oldPlots[ids[i]]; + for (i = 0; i < ids.length; i++) { + var subplotInfo = oldPlots[ids[i]]; - if(subplotInfo.plot) { - subplotInfo.plot.select('g.scatterlayer') - .selectAll('g.trace') - .remove(); - } - } + if (subplotInfo.plot) { + subplotInfo.plot.select("g.scatterlayer").selectAll("g.trace").remove(); + } } + } - var hadCartesian = (oldFullLayout._has && oldFullLayout._has('cartesian')); - var hasCartesian = (newFullLayout._has && newFullLayout._has('cartesian')); + var hadCartesian = oldFullLayout._has && oldFullLayout._has("cartesian"); + var hasCartesian = newFullLayout._has && newFullLayout._has("cartesian"); - if(hadCartesian && !hasCartesian) { - var subplotLayers = oldFullLayout._cartesianlayer.selectAll('.subplot'); + if (hadCartesian && !hasCartesian) { + var subplotLayers = oldFullLayout._cartesianlayer.selectAll(".subplot"); - subplotLayers.call(purgeSubplotLayers, oldFullLayout); - oldFullLayout._defs.selectAll('.axesclip').remove(); - } + subplotLayers.call(purgeSubplotLayers, oldFullLayout); + oldFullLayout._defs.selectAll(".axesclip").remove(); + } }; exports.drawFramework = function(gd) { - var fullLayout = gd._fullLayout, - subplotData = makeSubplotData(gd); + var fullLayout = gd._fullLayout, subplotData = makeSubplotData(gd); - var subplotLayers = fullLayout._cartesianlayer.selectAll('.subplot') - .data(subplotData, Lib.identity); + var subplotLayers = fullLayout._cartesianlayer + .selectAll(".subplot") + .data(subplotData, Lib.identity); - subplotLayers.enter().append('g') - .attr('class', function(name) { return 'subplot ' + name; }); + subplotLayers.enter().append("g").attr("class", function(name) { + return "subplot " + name; + }); - subplotLayers.order(); + subplotLayers.order(); - subplotLayers.exit() - .call(purgeSubplotLayers, fullLayout); + subplotLayers.exit().call(purgeSubplotLayers, fullLayout); - subplotLayers.each(function(name) { - var plotinfo = fullLayout._plots[name]; + subplotLayers.each(function(name) { + var plotinfo = fullLayout._plots[name]; - // keep ref to plot group - plotinfo.plotgroup = d3.select(this); + // keep ref to plot group + plotinfo.plotgroup = d3.select(this); - // initialize list of overlay subplots - plotinfo.overlays = []; + // initialize list of overlay subplots + plotinfo.overlays = []; - makeSubplotLayer(plotinfo); + makeSubplotLayer(plotinfo); - // fill in list of overlay subplots - if(plotinfo.mainplot) { - var mainplot = fullLayout._plots[plotinfo.mainplot]; - mainplot.overlays.push(plotinfo); - } + // fill in list of overlay subplots + if (plotinfo.mainplot) { + var mainplot = fullLayout._plots[plotinfo.mainplot]; + mainplot.overlays.push(plotinfo); + } - // make separate drag layers for each subplot, - // but append them to paper rather than the plot groups, - // so they end up on top of the rest - plotinfo.draglayer = joinLayer(fullLayout._draggers, 'g', name); - }); + // make separate drag layers for each subplot, + // but append them to paper rather than the plot groups, + // so they end up on top of the rest + plotinfo.draglayer = joinLayer(fullLayout._draggers, "g", name); + }); }; exports.rangePlot = function(gd, plotinfo, cdSubplot) { - makeSubplotLayer(plotinfo); - plotOne(gd, plotinfo, cdSubplot); - Plots.style(gd); + makeSubplotLayer(plotinfo); + plotOne(gd, plotinfo, cdSubplot); + Plots.style(gd); }; function makeSubplotData(gd) { - var fullLayout = gd._fullLayout, - subplots = Object.keys(fullLayout._plots); - - var subplotData = [], - overlays = []; - - for(var i = 0; i < subplots.length; i++) { - var subplot = subplots[i], - plotinfo = fullLayout._plots[subplot]; - - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis; - - // is this subplot overlaid on another? - // ax.overlaying is the id of another axis of the same - // dimension that this one overlays to be an overlaid subplot, - // the main plot must exist make sure we're not trying to - // overlay on an axis that's already overlaying another - var xa2 = Axes.getFromId(gd, xa.overlaying) || xa; - if(xa2 !== xa && xa2.overlaying) { - xa2 = xa; - xa.overlaying = false; - } + var fullLayout = gd._fullLayout, subplots = Object.keys(fullLayout._plots); - var ya2 = Axes.getFromId(gd, ya.overlaying) || ya; - if(ya2 !== ya && ya2.overlaying) { - ya2 = ya; - ya.overlaying = false; - } + var subplotData = [], overlays = []; - var mainplot = xa2._id + ya2._id; - if(mainplot !== subplot && subplots.indexOf(mainplot) !== -1) { - plotinfo.mainplot = mainplot; - plotinfo.mainplotinfo = fullLayout._plots[mainplot]; - overlays.push(subplot); - - // for now force overlays to overlay completely... so they - // can drag together correctly and share backgrounds. - // Later perhaps we make separate axis domain and - // tick/line domain or something, so they can still share - // the (possibly larger) dragger and background but don't - // have to both be drawn over that whole domain - xa.domain = xa2.domain.slice(); - ya.domain = ya2.domain.slice(); - } - else { - subplotData.push(subplot); - } - } + for (var i = 0; i < subplots.length; i++) { + var subplot = subplots[i], plotinfo = fullLayout._plots[subplot]; - // main subplots before overlays - subplotData = subplotData.concat(overlays); + var xa = plotinfo.xaxis, ya = plotinfo.yaxis; - return subplotData; -} - -function makeSubplotLayer(plotinfo) { - var plotgroup = plotinfo.plotgroup, - id = plotinfo.id; - - // Layers to keep plot types in the right order. - // from back to front: - // 1. heatmaps, 2D histos and contour maps - // 2. bars / 1D histos - // 3. errorbars for bars and scatter - // 4. scatter - // 5. box plots - function joinPlotLayers(parent) { - joinLayer(parent, 'g', 'imagelayer'); - joinLayer(parent, 'g', 'maplayer'); - joinLayer(parent, 'g', 'barlayer'); - joinLayer(parent, 'g', 'boxlayer'); - joinLayer(parent, 'g', 'scatterlayer'); + // is this subplot overlaid on another? + // ax.overlaying is the id of another axis of the same + // dimension that this one overlays to be an overlaid subplot, + // the main plot must exist make sure we're not trying to + // overlay on an axis that's already overlaying another + var xa2 = Axes.getFromId(gd, xa.overlaying) || xa; + if (xa2 !== xa && xa2.overlaying) { + xa2 = xa; + xa.overlaying = false; } - if(!plotinfo.mainplot) { - plotinfo.bg = joinLayer(plotgroup, 'rect', 'bg'); - plotinfo.bg.style('stroke-width', 0); - - var backLayer = joinLayer(plotgroup, 'g', 'layer-subplot'); - plotinfo.shapelayer = joinLayer(backLayer, 'g', 'shapelayer'); - plotinfo.imagelayer = joinLayer(backLayer, 'g', 'imagelayer'); - - plotinfo.gridlayer = joinLayer(plotgroup, 'g', 'gridlayer'); - plotinfo.overgrid = joinLayer(plotgroup, 'g', 'overgrid'); - - plotinfo.zerolinelayer = joinLayer(plotgroup, 'g', 'zerolinelayer'); - plotinfo.overzero = joinLayer(plotgroup, 'g', 'overzero'); - - plotinfo.plot = joinLayer(plotgroup, 'g', 'plot'); - plotinfo.overplot = joinLayer(plotgroup, 'g', 'overplot'); - - plotinfo.xlines = joinLayer(plotgroup, 'path', 'xlines'); - plotinfo.ylines = joinLayer(plotgroup, 'path', 'ylines'); - plotinfo.overlines = joinLayer(plotgroup, 'g', 'overlines'); - - plotinfo.xaxislayer = joinLayer(plotgroup, 'g', 'xaxislayer'); - plotinfo.yaxislayer = joinLayer(plotgroup, 'g', 'yaxislayer'); - plotinfo.overaxes = joinLayer(plotgroup, 'g', 'overaxes'); + var ya2 = Axes.getFromId(gd, ya.overlaying) || ya; + if (ya2 !== ya && ya2.overlaying) { + ya2 = ya; + ya.overlaying = false; } - else { - var mainplotinfo = plotinfo.mainplotinfo; - - // now make the components of overlaid subplots - // overlays don't have backgrounds, and append all - // their other components to the corresponding - // extra groups of their main plots. - - plotinfo.gridlayer = joinLayer(mainplotinfo.overgrid, 'g', id); - plotinfo.zerolinelayer = joinLayer(mainplotinfo.overzero, 'g', id); - - plotinfo.plot = joinLayer(mainplotinfo.overplot, 'g', id); - plotinfo.xlines = joinLayer(mainplotinfo.overlines, 'path', id); - plotinfo.ylines = joinLayer(mainplotinfo.overlines, 'path', id); - plotinfo.xaxislayer = joinLayer(mainplotinfo.overaxes, 'g', id); - plotinfo.yaxislayer = joinLayer(mainplotinfo.overaxes, 'g', id); + + var mainplot = xa2._id + ya2._id; + if (mainplot !== subplot && subplots.indexOf(mainplot) !== -1) { + plotinfo.mainplot = mainplot; + plotinfo.mainplotinfo = fullLayout._plots[mainplot]; + overlays.push(subplot); + + // for now force overlays to overlay completely... so they + // can drag together correctly and share backgrounds. + // Later perhaps we make separate axis domain and + // tick/line domain or something, so they can still share + // the (possibly larger) dragger and background but don't + // have to both be drawn over that whole domain + xa.domain = xa2.domain.slice(); + ya.domain = ya2.domain.slice(); + } else { + subplotData.push(subplot); } + } - // common attributes for all subplots, overlays or not - plotinfo.plot.call(joinPlotLayers); + // main subplots before overlays + subplotData = subplotData.concat(overlays); - plotinfo.xlines - .style('fill', 'none') - .classed('crisp', true); + return subplotData; +} - plotinfo.ylines - .style('fill', 'none') - .classed('crisp', true); +function makeSubplotLayer(plotinfo) { + var plotgroup = plotinfo.plotgroup, id = plotinfo.id; + + // Layers to keep plot types in the right order. + // from back to front: + // 1. heatmaps, 2D histos and contour maps + // 2. bars / 1D histos + // 3. errorbars for bars and scatter + // 4. scatter + // 5. box plots + function joinPlotLayers(parent) { + joinLayer(parent, "g", "imagelayer"); + joinLayer(parent, "g", "maplayer"); + joinLayer(parent, "g", "barlayer"); + joinLayer(parent, "g", "boxlayer"); + joinLayer(parent, "g", "scatterlayer"); + } + + if (!plotinfo.mainplot) { + plotinfo.bg = joinLayer(plotgroup, "rect", "bg"); + plotinfo.bg.style("stroke-width", 0); + + var backLayer = joinLayer(plotgroup, "g", "layer-subplot"); + plotinfo.shapelayer = joinLayer(backLayer, "g", "shapelayer"); + plotinfo.imagelayer = joinLayer(backLayer, "g", "imagelayer"); + + plotinfo.gridlayer = joinLayer(plotgroup, "g", "gridlayer"); + plotinfo.overgrid = joinLayer(plotgroup, "g", "overgrid"); + + plotinfo.zerolinelayer = joinLayer(plotgroup, "g", "zerolinelayer"); + plotinfo.overzero = joinLayer(plotgroup, "g", "overzero"); + + plotinfo.plot = joinLayer(plotgroup, "g", "plot"); + plotinfo.overplot = joinLayer(plotgroup, "g", "overplot"); + + plotinfo.xlines = joinLayer(plotgroup, "path", "xlines"); + plotinfo.ylines = joinLayer(plotgroup, "path", "ylines"); + plotinfo.overlines = joinLayer(plotgroup, "g", "overlines"); + + plotinfo.xaxislayer = joinLayer(plotgroup, "g", "xaxislayer"); + plotinfo.yaxislayer = joinLayer(plotgroup, "g", "yaxislayer"); + plotinfo.overaxes = joinLayer(plotgroup, "g", "overaxes"); + } else { + var mainplotinfo = plotinfo.mainplotinfo; + + // now make the components of overlaid subplots + // overlays don't have backgrounds, and append all + // their other components to the corresponding + // extra groups of their main plots. + plotinfo.gridlayer = joinLayer(mainplotinfo.overgrid, "g", id); + plotinfo.zerolinelayer = joinLayer(mainplotinfo.overzero, "g", id); + + plotinfo.plot = joinLayer(mainplotinfo.overplot, "g", id); + plotinfo.xlines = joinLayer(mainplotinfo.overlines, "path", id); + plotinfo.ylines = joinLayer(mainplotinfo.overlines, "path", id); + plotinfo.xaxislayer = joinLayer(mainplotinfo.overaxes, "g", id); + plotinfo.yaxislayer = joinLayer(mainplotinfo.overaxes, "g", id); + } + + // common attributes for all subplots, overlays or not + plotinfo.plot.call(joinPlotLayers); + + plotinfo.xlines.style("fill", "none").classed("crisp", true); + + plotinfo.ylines.style("fill", "none").classed("crisp", true); } function purgeSubplotLayers(layers, fullLayout) { - if(!layers) return; - - layers.each(function(subplot) { - var plotgroup = d3.select(this), - clipId = 'clip' + fullLayout._uid + subplot + 'plot'; - - plotgroup.remove(); - fullLayout._draggers.selectAll('g.' + subplot).remove(); - fullLayout._defs.select('#' + clipId).remove(); - - // do not remove individual axis s here - // as other subplots may need them - }); + if (!layers) return; + + layers.each(function(subplot) { + var plotgroup = d3.select(this), + clipId = "clip" + fullLayout._uid + subplot + "plot"; + + plotgroup.remove(); + fullLayout._draggers.selectAll("g." + subplot).remove(); + fullLayout._defs.select("#" + clipId).remove(); + // do not remove individual axis s here + // as other subplots may need them + }); } function joinLayer(parent, nodeType, className) { - var layer = parent.selectAll('.' + className) - .data([0]); + var layer = parent.selectAll("." + className).data([0]); - layer.enter().append(nodeType) - .classed(className, true); + layer.enter().append(nodeType).classed(className, true); - return layer; + return layer; } diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index b917c4cf8e1..be5cd509671 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -5,528 +5,517 @@ * 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 fontAttrs = require("../font_attributes"); +var colorAttrs = require("../../components/color/attributes"); +var extendFlat = require("../../lib/extend").extendFlat; -'use strict'; - -var fontAttrs = require('../font_attributes'); -var colorAttrs = require('../../components/color/attributes'); -var extendFlat = require('../../lib/extend').extendFlat; - -var constants = require('./constants'); - +var constants = require("./constants"); module.exports = { - color: { - valType: 'color', - dflt: colorAttrs.defaultLine, - role: 'style', - description: [ - 'Sets default for all colors associated with this axis', - 'all at once: line, font, tick, and grid colors.', - 'Grid color is lightened by blending this with the plot background', - 'Individual pieces can override this.' - ].join(' ') - }, - title: { - valType: 'string', - role: 'info', - description: 'Sets the title of this axis.' - }, - titlefont: extendFlat({}, fontAttrs, { - description: [ - 'Sets this axis\' title font.' - ].join(' ') - }), - type: { - valType: 'enumerated', - // '-' means we haven't yet run autotype or couldn't find any data - // it gets turned into linear in gd._fullLayout but not copied back - // to gd.data like the others are. - values: ['-', 'linear', 'log', 'date', 'category'], - dflt: '-', - role: 'info', - description: [ - 'Sets the axis type.', - 'By default, plotly attempts to determined the axis type', - 'by looking into the data of the traces that referenced', - 'the axis in question.' - ].join(' ') - }, - autorange: { - valType: 'enumerated', - values: [true, false, 'reversed'], - dflt: true, - role: 'style', - description: [ - 'Determines whether or not the range of this axis is', - 'computed in relation to the input data.', - 'See `rangemode` for more info.', - 'If `range` is provided, then `autorange` is set to *false*.' - ].join(' ') - }, - rangemode: { - valType: 'enumerated', - values: ['normal', 'tozero', 'nonnegative'], - dflt: 'normal', - role: 'style', - description: [ - 'If *normal*, the range is computed in relation to the extrema', - 'of the input data.', - 'If *tozero*`, the range extends to 0,', - 'regardless of the input data', - 'If *nonnegative*, the range is non-negative,', - 'regardless of the input data.' - ].join(' ') - }, - range: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'any'}, - {valType: 'any'} - ], - description: [ - 'Sets the range of this axis.', - 'If the axis `type` is *log*, then you must take the log of your', - 'desired range (e.g. to set the range from 1 to 100,', - 'set the range from 0 to 2).', - 'If the axis `type` is *date*, it should be date strings,', - 'like date data, though Date objects and unix milliseconds', - 'will be accepted and converted to strings.', - 'If the axis `type` is *category*, it should be numbers,', - 'using the scale where each category is assigned a serial', - 'number from zero in the order it appears.' - ].join(' ') - }, - - fixedrange: { - valType: 'boolean', - dflt: false, - role: 'info', - description: [ - 'Determines whether or not this axis is zoom-able.', - 'If true, then zoom is disabled.' - ].join(' ') - }, - // ticks - tickmode: { - valType: 'enumerated', - values: ['auto', 'linear', 'array'], - role: 'info', - description: [ - 'Sets the tick mode for this axis.', - 'If *auto*, the number of ticks is set via `nticks`.', - 'If *linear*, the placement of the ticks is determined by', - 'a starting position `tick0` and a tick step `dtick`', - '(*linear* is the default value if `tick0` and `dtick` are provided).', - 'If *array*, the placement of the ticks is set via `tickvals`', - 'and the tick text is `ticktext`.', - '(*array* is the default value if `tickvals` is provided).' - ].join(' ') - }, - nticks: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'style', - description: [ - 'Specifies the maximum number of ticks for the particular axis.', - 'The actual number of ticks will be chosen automatically to be', - 'less than or equal to `nticks`.', - 'Has an effect only if `tickmode` is set to *auto*.' - ].join(' ') - }, - tick0: { - valType: 'any', - role: 'style', - description: [ - 'Sets the placement of the first tick on this axis.', - 'Use with `dtick`.', - 'If the axis `type` is *log*, then you must take the log of your starting tick', - '(e.g. to set the starting tick to 100, set the `tick0` to 2)', - 'except when `dtick`=*L* (see `dtick` for more info).', - 'If the axis `type` is *date*, it should be a date string, like date data.', - 'If the axis `type` is *category*, it should be a number, using the scale where', - 'each category is assigned a serial number from zero in the order it appears.' - ].join(' ') - }, - dtick: { - valType: 'any', - role: 'style', - description: [ - 'Sets the step in-between ticks on this axis. Use with `tick0`.', - 'Must be a positive number, or special strings available to *log* and *date* axes.', - 'If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n', - 'is the tick number. For example,', - 'to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1.', - 'To set tick marks at 1, 100, 10000, ... set dtick to 2.', - 'To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433.', - '*log* has several special values; *L*, where `f` is a positive number,', - 'gives ticks linearly spaced in value (but not position).', - 'For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc.', - 'To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5).', - '`tick0` is ignored for *D1* and *D2*.', - 'If the axis `type` is *date*, then you must convert the time to milliseconds.', - 'For example, to set the interval between ticks to one day,', - 'set `dtick` to 86400000.0.', - '*date* also has special values *M* gives ticks spaced by a number of months.', - '`n` must be a positive integer.', - 'To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*.', - 'To set ticks every 4 years, set `dtick` to *M48*' - ].join(' ') - }, - tickvals: { - valType: 'data_array', - description: [ - 'Sets the values at which ticks on this axis appear.', - 'Only has an effect if `tickmode` is set to *array*.', - 'Used with `ticktext`.' - ].join(' ') - }, - ticktext: { - valType: 'data_array', - description: [ - 'Sets the text displayed at the ticks position via `tickvals`.', - 'Only has an effect if `tickmode` is set to *array*.', - 'Used with `tickvals`.' - ].join(' ') - }, - ticks: { - valType: 'enumerated', - values: ['outside', 'inside', ''], - role: 'style', - description: [ - 'Determines whether ticks are drawn or not.', - 'If **, this axis\' ticks are not drawn.', - 'If *outside* (*inside*), this axis\' are drawn outside (inside)', - 'the axis lines.' - ].join(' ') - }, - mirror: { - valType: 'enumerated', - values: [true, 'ticks', false, 'all', 'allticks'], - dflt: false, - role: 'style', - description: [ - 'Determines if the axis lines or/and ticks are mirrored to', - 'the opposite side of the plotting area.', - 'If *true*, the axis lines are mirrored.', - 'If *ticks*, the axis lines and ticks are mirrored.', - 'If *false*, mirroring is disable.', - 'If *all*, axis lines are mirrored on all shared-axes subplots.', - 'If *allticks*, axis lines and ticks are mirrored', - 'on all shared-axes subplots.' - ].join(' ') - }, - ticklen: { - valType: 'number', - min: 0, - dflt: 5, - role: 'style', - description: 'Sets the tick length (in px).' - }, - tickwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the tick width (in px).' - }, - tickcolor: { - valType: 'color', - dflt: colorAttrs.defaultLine, - role: 'style', - description: 'Sets the tick color.' - }, - showticklabels: { - valType: 'boolean', - dflt: true, - role: 'style', - description: 'Determines whether or not the tick labels are drawn.' - }, - tickfont: extendFlat({}, fontAttrs, { - description: 'Sets the tick font.' - }), - tickangle: { - valType: 'angle', - dflt: 'auto', - role: 'style', - description: [ - 'Sets the angle of the tick labels with respect to the horizontal.', - 'For example, a `tickangle` of -90 draws the tick labels', - 'vertically.' - ].join(' ') - }, - tickprefix: { - valType: 'string', - dflt: '', - role: 'style', - description: 'Sets a tick label prefix.' - }, - showtickprefix: { - valType: 'enumerated', - values: ['all', 'first', 'last', 'none'], - dflt: 'all', - role: 'style', - description: [ - 'If *all*, all tick labels are displayed with a prefix.', - 'If *first*, only the first tick is displayed with a prefix.', - 'If *last*, only the last tick is displayed with a suffix.', - 'If *none*, tick prefixes are hidden.' - ].join(' ') - }, - ticksuffix: { - valType: 'string', - dflt: '', - role: 'style', - description: 'Sets a tick label suffix.' - }, - showticksuffix: { - valType: 'enumerated', - values: ['all', 'first', 'last', 'none'], - dflt: 'all', - role: 'style', - description: 'Same as `showtickprefix` but for tick suffixes.' - }, - showexponent: { - valType: 'enumerated', - values: ['all', 'first', 'last', 'none'], - dflt: 'all', - role: 'style', - description: [ - 'If *all*, all exponents are shown besides their significands.', - 'If *first*, only the exponent of the first tick is shown.', - 'If *last*, only the exponent of the last tick is shown.', - 'If *none*, no exponents appear.' - ].join(' ') - }, - exponentformat: { - valType: 'enumerated', - values: ['none', 'e', 'E', 'power', 'SI', 'B'], - dflt: 'B', - role: 'style', - description: [ - 'Determines a formatting rule for the tick exponents.', - 'For example, consider the number 1,000,000,000.', - 'If *none*, it appears as 1,000,000,000.', - 'If *e*, 1e+9.', - 'If *E*, 1E+9.', - 'If *power*, 1x10^9 (with 9 in a super script).', - 'If *SI*, 1G.', - 'If *B*, 1B.' - ].join(' ') - }, - separatethousands: { - valType: 'boolean', - dflt: false, - role: 'style', - description: [ - 'If "true", even 4-digit integers are separated' - ].join(' ') - }, - tickformat: { - valType: 'string', - dflt: '', - role: 'style', - description: [ - 'Sets the tick label formatting rule using d3 formatting mini-languages', - 'which are very similar to those in Python. For numbers, see:', - 'https://github.com/d3/d3-format/blob/master/README.md#locale_format', - 'And for dates see:', - 'https://github.com/d3/d3-time-format/blob/master/README.md#locale_format', - 'We add one item to d3\'s date formatter: *%{n}f* for fractional seconds', - 'with n digits. For example, *2016-10-13 09:15:23.456* with tickformat', - '*%H~%M~%S.%2f* would display *09~15~23.46*' - ].join(' ') - }, - hoverformat: { - valType: 'string', - dflt: '', - role: 'style', - description: [ - 'Sets the hover text formatting rule using d3 formatting mini-languages', - 'which are very similar to those in Python. For numbers, see:', - 'https://github.com/d3/d3-format/blob/master/README.md#locale_format', - 'And for dates see:', - 'https://github.com/d3/d3-time-format/blob/master/README.md#locale_format', - 'We add one item to d3\'s date formatter: *%{n}f* for fractional seconds', - 'with n digits. For example, *2016-10-13 09:15:23.456* with tickformat', - '*%H~%M~%S.%2f* would display *09~15~23.46*' - ].join(' ') - }, - // lines and grids - showline: { - valType: 'boolean', - dflt: false, - role: 'style', - description: [ - 'Determines whether or not a line bounding this axis is drawn.' - ].join(' ') - }, - linecolor: { - valType: 'color', - dflt: colorAttrs.defaultLine, - role: 'style', - description: 'Sets the axis line color.' - }, - linewidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the axis line.' - }, - showgrid: { - valType: 'boolean', - role: 'style', - description: [ - 'Determines whether or not grid lines are drawn.', - 'If *true*, the grid lines are drawn at every tick mark.' - ].join(' ') - }, - gridcolor: { - valType: 'color', - dflt: colorAttrs.lightLine, - role: 'style', - description: 'Sets the color of the grid lines.' - }, - gridwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the grid lines.' - }, - zeroline: { - valType: 'boolean', - role: 'style', - description: [ - 'Determines whether or not a line is drawn at along the 0 value', - 'of this axis.', - 'If *true*, the zero line is drawn on top of the grid lines.' - ].join(' ') - }, - zerolinecolor: { - valType: 'color', - dflt: colorAttrs.defaultLine, - role: 'style', - description: 'Sets the line color of the zero line.' - }, - zerolinewidth: { - valType: 'number', - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the zero line.' - }, - // positioning attributes - // anchor: not used directly, just put here for reference - // values are any opposite-letter axis id - anchor: { - valType: 'enumerated', - values: [ - 'free', - constants.idRegex.x.toString(), - constants.idRegex.y.toString() - ], - role: 'info', - description: [ - 'If set to an opposite-letter axis id (e.g. `xaxis2`, `yaxis`), this axis is bound to', - 'the corresponding opposite-letter axis.', - 'If set to *free*, this axis\' position is determined by `position`.' - ].join(' ') - }, - // side: not used directly, as values depend on direction - // values are top, bottom for x axes, and left, right for y - side: { - valType: 'enumerated', - values: ['top', 'bottom', 'left', 'right'], - role: 'info', - description: [ - 'Determines whether a x (y) axis is positioned', - 'at the *bottom* (*left*) or *top* (*right*)', - 'of the plotting area.' - ].join(' ') - }, - // overlaying: not used directly, just put here for reference - // values are false and any other same-letter axis id that's not - // itself overlaying anything - overlaying: { - valType: 'enumerated', - values: [ - 'free', - constants.idRegex.x.toString(), - constants.idRegex.y.toString() - ], - role: 'info', - description: [ - 'If set a same-letter axis id, this axis is overlaid on top of', - 'the corresponding same-letter axis.', - 'If *false*, this axis does not overlay any same-letter axes.' - ].join(' ') - }, - domain: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the domain of this axis (in plot fraction).' - ].join(' ') - }, - position: { - valType: 'number', - min: 0, - max: 1, - dflt: 0, - role: 'style', - description: [ - 'Sets the position of this axis in the plotting space', - '(in normalized coordinates).', - 'Only has an effect if `anchor` is set to *free*.' - ].join(' ') - }, - categoryorder: { - valType: 'enumerated', - values: [ - 'trace', 'category ascending', 'category descending', 'array' - /* , 'value ascending', 'value descending'*/ // value ascending / descending to be implemented later - ], - dflt: 'trace', - role: 'info', - description: [ - 'Specifies the ordering logic for the case of categorical variables.', - 'By default, plotly uses *trace*, which specifies the order that is present in the data supplied.', - 'Set `categoryorder` to *category ascending* or *category descending* if order should be determined by', - 'the alphanumerical order of the category names.', - /* 'Set `categoryorder` to *value ascending* or *value descending* if order should be determined by the', - 'numerical order of the values.',*/ // // value ascending / descending to be implemented later - 'Set `categoryorder` to *array* to derive the ordering from the attribute `categoryarray`. If a category', - 'is not found in the `categoryarray` array, the sorting behavior for that attribute will be identical to', - 'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.' - ].join(' ') - }, - categoryarray: { - valType: 'data_array', - role: 'info', - description: [ - 'Sets the order in which categories on this axis appear.', - 'Only has an effect if `categoryorder` is set to *array*.', - 'Used with `categoryorder`.' - ].join(' ') - }, - - _deprecated: { - autotick: { - valType: 'boolean', - role: 'info', - description: [ - 'Obsolete.', - 'Set `tickmode` to *auto* for old `autotick` *true* behavior.', - 'Set `tickmode` to *linear* for `autotick` *false*.' - ].join(' ') - } + color: { + valType: "color", + dflt: colorAttrs.defaultLine, + role: "style", + description: [ + "Sets default for all colors associated with this axis", + "all at once: line, font, tick, and grid colors.", + "Grid color is lightened by blending this with the plot background", + "Individual pieces can override this." + ].join(" ") + }, + title: { + valType: "string", + role: "info", + description: "Sets the title of this axis." + }, + titlefont: extendFlat({}, fontAttrs, { + description: ["Sets this axis' title font."].join(" ") + }), + type: { + valType: "enumerated", + // '-' means we haven't yet run autotype or couldn't find any data + // it gets turned into linear in gd._fullLayout but not copied back + // to gd.data like the others are. + values: ["-", "linear", "log", "date", "category"], + dflt: "-", + role: "info", + description: [ + "Sets the axis type.", + "By default, plotly attempts to determined the axis type", + "by looking into the data of the traces that referenced", + "the axis in question." + ].join(" ") + }, + autorange: { + valType: "enumerated", + values: [true, false, "reversed"], + dflt: true, + role: "style", + description: [ + "Determines whether or not the range of this axis is", + "computed in relation to the input data.", + "See `rangemode` for more info.", + "If `range` is provided, then `autorange` is set to *false*." + ].join(" ") + }, + rangemode: { + valType: "enumerated", + values: ["normal", "tozero", "nonnegative"], + dflt: "normal", + role: "style", + description: [ + "If *normal*, the range is computed in relation to the extrema", + "of the input data.", + "If *tozero*`, the range extends to 0,", + "regardless of the input data", + "If *nonnegative*, the range is non-negative,", + "regardless of the input data." + ].join(" ") + }, + range: { + valType: "info_array", + role: "info", + items: [{ valType: "any" }, { valType: "any" }], + description: [ + "Sets the range of this axis.", + "If the axis `type` is *log*, then you must take the log of your", + "desired range (e.g. to set the range from 1 to 100,", + "set the range from 0 to 2).", + "If the axis `type` is *date*, it should be date strings,", + "like date data, though Date objects and unix milliseconds", + "will be accepted and converted to strings.", + "If the axis `type` is *category*, it should be numbers,", + "using the scale where each category is assigned a serial", + "number from zero in the order it appears." + ].join(" ") + }, + fixedrange: { + valType: "boolean", + dflt: false, + role: "info", + description: [ + "Determines whether or not this axis is zoom-able.", + "If true, then zoom is disabled." + ].join(" ") + }, + // ticks + tickmode: { + valType: "enumerated", + values: ["auto", "linear", "array"], + role: "info", + description: [ + "Sets the tick mode for this axis.", + "If *auto*, the number of ticks is set via `nticks`.", + "If *linear*, the placement of the ticks is determined by", + "a starting position `tick0` and a tick step `dtick`", + "(*linear* is the default value if `tick0` and `dtick` are provided).", + "If *array*, the placement of the ticks is set via `tickvals`", + "and the tick text is `ticktext`.", + "(*array* is the default value if `tickvals` is provided)." + ].join(" ") + }, + nticks: { + valType: "integer", + min: 0, + dflt: 0, + role: "style", + description: [ + "Specifies the maximum number of ticks for the particular axis.", + "The actual number of ticks will be chosen automatically to be", + "less than or equal to `nticks`.", + "Has an effect only if `tickmode` is set to *auto*." + ].join(" ") + }, + tick0: { + valType: "any", + role: "style", + description: [ + "Sets the placement of the first tick on this axis.", + "Use with `dtick`.", + "If the axis `type` is *log*, then you must take the log of your starting tick", + "(e.g. to set the starting tick to 100, set the `tick0` to 2)", + "except when `dtick`=*L* (see `dtick` for more info).", + "If the axis `type` is *date*, it should be a date string, like date data.", + "If the axis `type` is *category*, it should be a number, using the scale where", + "each category is assigned a serial number from zero in the order it appears." + ].join(" ") + }, + dtick: { + valType: "any", + role: "style", + description: [ + "Sets the step in-between ticks on this axis. Use with `tick0`.", + "Must be a positive number, or special strings available to *log* and *date* axes.", + "If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n", + "is the tick number. For example,", + "to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1.", + "To set tick marks at 1, 100, 10000, ... set dtick to 2.", + "To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433.", + "*log* has several special values; *L*, where `f` is a positive number,", + "gives ticks linearly spaced in value (but not position).", + "For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc.", + "To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5).", + "`tick0` is ignored for *D1* and *D2*.", + "If the axis `type` is *date*, then you must convert the time to milliseconds.", + "For example, to set the interval between ticks to one day,", + "set `dtick` to 86400000.0.", + "*date* also has special values *M* gives ticks spaced by a number of months.", + "`n` must be a positive integer.", + "To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*.", + "To set ticks every 4 years, set `dtick` to *M48*" + ].join(" ") + }, + tickvals: { + valType: "data_array", + description: [ + "Sets the values at which ticks on this axis appear.", + "Only has an effect if `tickmode` is set to *array*.", + "Used with `ticktext`." + ].join(" ") + }, + ticktext: { + valType: "data_array", + description: [ + "Sets the text displayed at the ticks position via `tickvals`.", + "Only has an effect if `tickmode` is set to *array*.", + "Used with `tickvals`." + ].join(" ") + }, + ticks: { + valType: "enumerated", + values: ["outside", "inside", ""], + role: "style", + description: [ + "Determines whether ticks are drawn or not.", + "If **, this axis' ticks are not drawn.", + "If *outside* (*inside*), this axis' are drawn outside (inside)", + "the axis lines." + ].join(" ") + }, + mirror: { + valType: "enumerated", + values: [true, "ticks", false, "all", "allticks"], + dflt: false, + role: "style", + description: [ + "Determines if the axis lines or/and ticks are mirrored to", + "the opposite side of the plotting area.", + "If *true*, the axis lines are mirrored.", + "If *ticks*, the axis lines and ticks are mirrored.", + "If *false*, mirroring is disable.", + "If *all*, axis lines are mirrored on all shared-axes subplots.", + "If *allticks*, axis lines and ticks are mirrored", + "on all shared-axes subplots." + ].join(" ") + }, + ticklen: { + valType: "number", + min: 0, + dflt: 5, + role: "style", + description: "Sets the tick length (in px)." + }, + tickwidth: { + valType: "number", + min: 0, + dflt: 1, + role: "style", + description: "Sets the tick width (in px)." + }, + tickcolor: { + valType: "color", + dflt: colorAttrs.defaultLine, + role: "style", + description: "Sets the tick color." + }, + showticklabels: { + valType: "boolean", + dflt: true, + role: "style", + description: "Determines whether or not the tick labels are drawn." + }, + tickfont: extendFlat({}, fontAttrs, { description: "Sets the tick font." }), + tickangle: { + valType: "angle", + dflt: "auto", + role: "style", + description: [ + "Sets the angle of the tick labels with respect to the horizontal.", + "For example, a `tickangle` of -90 draws the tick labels", + "vertically." + ].join(" ") + }, + tickprefix: { + valType: "string", + dflt: "", + role: "style", + description: "Sets a tick label prefix." + }, + showtickprefix: { + valType: "enumerated", + values: ["all", "first", "last", "none"], + dflt: "all", + role: "style", + description: [ + "If *all*, all tick labels are displayed with a prefix.", + "If *first*, only the first tick is displayed with a prefix.", + "If *last*, only the last tick is displayed with a suffix.", + "If *none*, tick prefixes are hidden." + ].join(" ") + }, + ticksuffix: { + valType: "string", + dflt: "", + role: "style", + description: "Sets a tick label suffix." + }, + showticksuffix: { + valType: "enumerated", + values: ["all", "first", "last", "none"], + dflt: "all", + role: "style", + description: "Same as `showtickprefix` but for tick suffixes." + }, + showexponent: { + valType: "enumerated", + values: ["all", "first", "last", "none"], + dflt: "all", + role: "style", + description: [ + "If *all*, all exponents are shown besides their significands.", + "If *first*, only the exponent of the first tick is shown.", + "If *last*, only the exponent of the last tick is shown.", + "If *none*, no exponents appear." + ].join(" ") + }, + exponentformat: { + valType: "enumerated", + values: ["none", "e", "E", "power", "SI", "B"], + dflt: "B", + role: "style", + description: [ + "Determines a formatting rule for the tick exponents.", + "For example, consider the number 1,000,000,000.", + "If *none*, it appears as 1,000,000,000.", + "If *e*, 1e+9.", + "If *E*, 1E+9.", + "If *power*, 1x10^9 (with 9 in a super script).", + "If *SI*, 1G.", + "If *B*, 1B." + ].join(" ") + }, + separatethousands: { + valType: "boolean", + dflt: false, + role: "style", + description: ['If "true", even 4-digit integers are separated'].join(" ") + }, + tickformat: { + valType: "string", + dflt: "", + role: "style", + description: [ + "Sets the tick label formatting rule using d3 formatting mini-languages", + "which are very similar to those in Python. For numbers, see:", + "https://github.com/d3/d3-format/blob/master/README.md#locale_format", + "And for dates see:", + "https://github.com/d3/d3-time-format/blob/master/README.md#locale_format", + "We add one item to d3's date formatter: *%{n}f* for fractional seconds", + "with n digits. For example, *2016-10-13 09:15:23.456* with tickformat", + "*%H~%M~%S.%2f* would display *09~15~23.46*" + ].join(" ") + }, + hoverformat: { + valType: "string", + dflt: "", + role: "style", + description: [ + "Sets the hover text formatting rule using d3 formatting mini-languages", + "which are very similar to those in Python. For numbers, see:", + "https://github.com/d3/d3-format/blob/master/README.md#locale_format", + "And for dates see:", + "https://github.com/d3/d3-time-format/blob/master/README.md#locale_format", + "We add one item to d3's date formatter: *%{n}f* for fractional seconds", + "with n digits. For example, *2016-10-13 09:15:23.456* with tickformat", + "*%H~%M~%S.%2f* would display *09~15~23.46*" + ].join(" ") + }, + // lines and grids + showline: { + valType: "boolean", + dflt: false, + role: "style", + description: [ + "Determines whether or not a line bounding this axis is drawn." + ].join(" ") + }, + linecolor: { + valType: "color", + dflt: colorAttrs.defaultLine, + role: "style", + description: "Sets the axis line color." + }, + linewidth: { + valType: "number", + min: 0, + dflt: 1, + role: "style", + description: "Sets the width (in px) of the axis line." + }, + showgrid: { + valType: "boolean", + role: "style", + description: [ + "Determines whether or not grid lines are drawn.", + "If *true*, the grid lines are drawn at every tick mark." + ].join(" ") + }, + gridcolor: { + valType: "color", + dflt: colorAttrs.lightLine, + role: "style", + description: "Sets the color of the grid lines." + }, + gridwidth: { + valType: "number", + min: 0, + dflt: 1, + role: "style", + description: "Sets the width (in px) of the grid lines." + }, + zeroline: { + valType: "boolean", + role: "style", + description: [ + "Determines whether or not a line is drawn at along the 0 value", + "of this axis.", + "If *true*, the zero line is drawn on top of the grid lines." + ].join(" ") + }, + zerolinecolor: { + valType: "color", + dflt: colorAttrs.defaultLine, + role: "style", + description: "Sets the line color of the zero line." + }, + zerolinewidth: { + valType: "number", + dflt: 1, + role: "style", + description: "Sets the width (in px) of the zero line." + }, + // positioning attributes + // anchor: not used directly, just put here for reference + // values are any opposite-letter axis id + anchor: { + valType: "enumerated", + values: [ + "free", + constants.idRegex.x.toString(), + constants.idRegex.y.toString() + ], + role: "info", + description: [ + "If set to an opposite-letter axis id (e.g. `xaxis2`, `yaxis`), this axis is bound to", + "the corresponding opposite-letter axis.", + "If set to *free*, this axis' position is determined by `position`." + ].join(" ") + }, + // side: not used directly, as values depend on direction + // values are top, bottom for x axes, and left, right for y + side: { + valType: "enumerated", + values: ["top", "bottom", "left", "right"], + role: "info", + description: [ + "Determines whether a x (y) axis is positioned", + "at the *bottom* (*left*) or *top* (*right*)", + "of the plotting area." + ].join(" ") + }, + // overlaying: not used directly, just put here for reference + // values are false and any other same-letter axis id that's not + // itself overlaying anything + overlaying: { + valType: "enumerated", + values: [ + "free", + constants.idRegex.x.toString(), + constants.idRegex.y.toString() + ], + role: "info", + description: [ + "If set a same-letter axis id, this axis is overlaid on top of", + "the corresponding same-letter axis.", + "If *false*, this axis does not overlay any same-letter axes." + ].join(" ") + }, + domain: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: ["Sets the domain of this axis (in plot fraction)."].join(" ") + }, + position: { + valType: "number", + min: 0, + max: 1, + dflt: 0, + role: "style", + description: [ + "Sets the position of this axis in the plotting space", + "(in normalized coordinates).", + "Only has an effect if `anchor` is set to *free*." + ].join(" ") + }, + categoryorder: { + valType: "enumerated", + values: [ + "trace", + "category ascending", + "category descending", + // value ascending / descending to be implemented later + "array" + /* , 'value ascending', 'value descending' */ + ], + dflt: "trace", + role: "info", + description: [ + "Specifies the ordering logic for the case of categorical variables.", + "By default, plotly uses *trace*, which specifies the order that is present in the data supplied.", + "Set `categoryorder` to *category ascending* or *category descending* if order should be determined by", + "the alphanumerical order of the category names.", + // // value ascending / descending to be implemented later + /* 'Set `categoryorder` to *value ascending* or *value descending* if order should be determined by the', + 'numerical order of the values.', */ + "Set `categoryorder` to *array* to derive the ordering from the attribute `categoryarray`. If a category", + "is not found in the `categoryarray` array, the sorting behavior for that attribute will be identical to", + "the *trace* mode. The unspecified categories will follow the categories in `categoryarray`." + ].join(" ") + }, + categoryarray: { + valType: "data_array", + role: "info", + description: [ + "Sets the order in which categories on this axis appear.", + "Only has an effect if `categoryorder` is set to *array*.", + "Used with `categoryorder`." + ].join(" ") + }, + _deprecated: { + autotick: { + valType: "boolean", + role: "info", + description: [ + "Obsolete.", + "Set `tickmode` to *auto* for old `autotick` *true* behavior.", + "Set `tickmode` to *linear* for `autotick` *false*." + ].join(" ") } + } }; diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 3594499ffdc..e837dc3db94 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -5,196 +5,212 @@ * 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 Registry = require("../../registry"); +var Lib = require("../../lib"); +var Color = require("../../components/color"); +var basePlotLayoutAttributes = require("../layout_attributes"); + +var constants = require("./constants"); +var layoutAttributes = require("./layout_attributes"); +var handleAxisDefaults = require("./axis_defaults"); +var handlePositionDefaults = require("./position_defaults"); +var axisIds = require("./axis_ids"); +module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { + var layoutKeys = Object.keys(layoutIn), + xaListCartesian = [], + yaListCartesian = [], + xaListGl2d = [], + yaListGl2d = [], + outerTicks = {}, + noGrids = {}, + i; + + // look for axes in the data + for (i = 0; i < fullData.length; i++) { + var trace = fullData[i]; + var listX, listY; + + if (Registry.traceIs(trace, "cartesian")) { + listX = xaListCartesian; + listY = yaListCartesian; + } else if (Registry.traceIs(trace, "gl2d")) { + listX = xaListGl2d; + listY = yaListGl2d; + } else { + continue; + } -'use strict'; - -var Registry = require('../../registry'); -var Lib = require('../../lib'); -var Color = require('../../components/color'); -var basePlotLayoutAttributes = require('../layout_attributes'); - -var constants = require('./constants'); -var layoutAttributes = require('./layout_attributes'); -var handleAxisDefaults = require('./axis_defaults'); -var handlePositionDefaults = require('./position_defaults'); -var axisIds = require('./axis_ids'); + var xaName = axisIds.id2name(trace.xaxis), + yaName = axisIds.id2name(trace.yaxis); + // add axes implied by traces + if (xaName && listX.indexOf(xaName) === -1) listX.push(xaName); + if (yaName && listY.indexOf(yaName) === -1) listY.push(yaName); -module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - var layoutKeys = Object.keys(layoutIn), - xaListCartesian = [], - yaListCartesian = [], - xaListGl2d = [], - yaListGl2d = [], - outerTicks = {}, - noGrids = {}, - i; - - // look for axes in the data - for(i = 0; i < fullData.length; i++) { - var trace = fullData[i]; - var listX, listY; - - if(Registry.traceIs(trace, 'cartesian')) { - listX = xaListCartesian; - listY = yaListCartesian; - } - else if(Registry.traceIs(trace, 'gl2d')) { - listX = xaListGl2d; - listY = yaListGl2d; - } - else continue; - - var xaName = axisIds.id2name(trace.xaxis), - yaName = axisIds.id2name(trace.yaxis); - - // add axes implied by traces - if(xaName && listX.indexOf(xaName) === -1) listX.push(xaName); - if(yaName && listY.indexOf(yaName) === -1) listY.push(yaName); - - // check for default formatting tweaks - if(Registry.traceIs(trace, '2dMap')) { - outerTicks[xaName] = true; - outerTicks[yaName] = true; - } - - if(Registry.traceIs(trace, 'oriented')) { - var positionAxis = trace.orientation === 'h' ? yaName : xaName; - noGrids[positionAxis] = true; - } + // check for default formatting tweaks + if (Registry.traceIs(trace, "2dMap")) { + outerTicks[xaName] = true; + outerTicks[yaName] = true; } - // N.B. Ignore orphan axes (i.e. axes that have no data attached to them) - // if gl3d or geo is present on graph. This is retain backward compatible. - // - // TODO drop this in version 2.0 - var ignoreOrphan = (layoutOut._has('gl3d') || layoutOut._has('geo')); - - if(!ignoreOrphan) { - for(i = 0; i < layoutKeys.length; i++) { - var key = layoutKeys[i]; - - // orphan layout axes are considered cartesian subplots - - if(xaListGl2d.indexOf(key) === -1 && - xaListCartesian.indexOf(key) === -1 && - constants.xAxisMatch.test(key)) { - xaListCartesian.push(key); - } - else if(yaListGl2d.indexOf(key) === -1 && - yaListCartesian.indexOf(key) === -1 && - constants.yAxisMatch.test(key)) { - yaListCartesian.push(key); - } - } + if (Registry.traceIs(trace, "oriented")) { + var positionAxis = trace.orientation === "h" ? yaName : xaName; + noGrids[positionAxis] = true; } - - // make sure that plots with orphan cartesian axes - // are considered 'cartesian' - if(xaListCartesian.length && yaListCartesian.length) { - Lib.pushUnique(layoutOut._basePlotModules, Registry.subplotsRegistry.cartesian); + } + + // N.B. Ignore orphan axes (i.e. axes that have no data attached to them) + // if gl3d or geo is present on graph. This is retain backward compatible. + // + // TODO drop this in version 2.0 + var ignoreOrphan = layoutOut._has("gl3d") || layoutOut._has("geo"); + + if (!ignoreOrphan) { + for (i = 0; i < layoutKeys.length; i++) { + var key = layoutKeys[i]; + + // orphan layout axes are considered cartesian subplots + if ( + xaListGl2d.indexOf(key) === -1 && + xaListCartesian.indexOf(key) === -1 && + constants.xAxisMatch.test(key) + ) { + xaListCartesian.push(key); + } else if ( + yaListGl2d.indexOf(key) === -1 && + yaListCartesian.indexOf(key) === -1 && + constants.yAxisMatch.test(key) + ) { + yaListCartesian.push(key); + } } - - function axSort(a, b) { - var aNum = Number(a.substr(5) || 1), - bNum = Number(b.substr(5) || 1); - return aNum - bNum; + } + + // make sure that plots with orphan cartesian axes + // are considered 'cartesian' + if (xaListCartesian.length && yaListCartesian.length) { + Lib.pushUnique( + layoutOut._basePlotModules, + Registry.subplotsRegistry.cartesian + ); + } + + function axSort(a, b) { + var aNum = Number(a.substr(5) || 1), bNum = Number(b.substr(5) || 1); + return aNum - bNum; + } + + var xaList = xaListCartesian.concat(xaListGl2d).sort(axSort), + yaList = yaListCartesian.concat(yaListGl2d).sort(axSort), + axesList = xaList.concat(yaList); + + // plot_bgcolor only makes sense if there's a (2D) plot! + // TODO: bgcolor for each subplot, to inherit from the main one + var plot_bgcolor = Color.background; + if (xaList.length && yaList.length) { + plot_bgcolor = Lib.coerce( + layoutIn, + layoutOut, + basePlotLayoutAttributes, + "plot_bgcolor" + ); + } + + var bgColor = Color.combine(plot_bgcolor, layoutOut.paper_bgcolor); + + var axLayoutIn, axLayoutOut; + + function coerce(attr, dflt) { + return Lib.coerce(axLayoutIn, axLayoutOut, layoutAttributes, attr, dflt); + } + + axesList.forEach(function(axName) { + var axLetter = axName.charAt(0); + + axLayoutIn = layoutIn[axName] || {}; + axLayoutOut = {}; + + var defaultOptions = { + letter: axLetter, + font: layoutOut.font, + outerTicks: outerTicks[axName], + showGrid: !noGrids[axName], + name: axName, + data: fullData, + bgColor: bgColor, + calendar: layoutOut.calendar + }; + + handleAxisDefaults( + axLayoutIn, + axLayoutOut, + coerce, + defaultOptions, + layoutOut + ); + + var positioningOptions = { + letter: axLetter, + counterAxes: ({ x: yaList, y: xaList })[axLetter].map(axisIds.name2id), + overlayableAxes: ({ x: xaList, y: yaList })[axLetter] + .filter(function(axName2) { + return axName2 !== axName && !(layoutIn[axName2] || {}).overlaying; + }) + .map(axisIds.name2id) + }; + + handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, positioningOptions); + + layoutOut[axName] = axLayoutOut; + + // so we don't have to repeat autotype unnecessarily, + // copy an autotype back to layoutIn + if (!layoutIn[axName] && axLayoutIn.type !== "-") { + layoutIn[axName] = { type: axLayoutIn.type }; } - - var xaList = xaListCartesian.concat(xaListGl2d).sort(axSort), - yaList = yaListCartesian.concat(yaListGl2d).sort(axSort), - axesList = xaList.concat(yaList); - - // plot_bgcolor only makes sense if there's a (2D) plot! - // TODO: bgcolor for each subplot, to inherit from the main one - var plot_bgcolor = Color.background; - if(xaList.length && yaList.length) { - plot_bgcolor = Lib.coerce(layoutIn, layoutOut, basePlotLayoutAttributes, 'plot_bgcolor'); + }); + + // quick second pass for range slider and selector defaults + var rangeSliderDefaults = Registry.getComponentMethod( + "rangeslider", + "handleDefaults" + ), + rangeSelectorDefaults = Registry.getComponentMethod( + "rangeselector", + "handleDefaults" + ); + + xaList.forEach(function(axName) { + axLayoutIn = layoutIn[axName]; + axLayoutOut = layoutOut[axName]; + + rangeSliderDefaults(layoutIn, layoutOut, axName); + + if (axLayoutOut.type === "date") { + rangeSelectorDefaults( + axLayoutIn, + axLayoutOut, + layoutOut, + yaList, + axLayoutOut.calendar + ); } - var bgColor = Color.combine(plot_bgcolor, layoutOut.paper_bgcolor); + coerce("fixedrange"); + }); - var axLayoutIn, axLayoutOut; + yaList.forEach(function(axName) { + axLayoutIn = layoutIn[axName]; + axLayoutOut = layoutOut[axName]; - function coerce(attr, dflt) { - return Lib.coerce(axLayoutIn, axLayoutOut, layoutAttributes, attr, dflt); - } + var anchoredAxis = layoutOut[axisIds.id2name(axLayoutOut.anchor)]; + + var fixedRangeDflt = anchoredAxis && + anchoredAxis.rangeslider && + anchoredAxis.rangeslider.visible; - axesList.forEach(function(axName) { - var axLetter = axName.charAt(0); - - axLayoutIn = layoutIn[axName] || {}; - axLayoutOut = {}; - - var defaultOptions = { - letter: axLetter, - font: layoutOut.font, - outerTicks: outerTicks[axName], - showGrid: !noGrids[axName], - name: axName, - data: fullData, - bgColor: bgColor, - calendar: layoutOut.calendar - }; - - handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut); - - var positioningOptions = { - letter: axLetter, - counterAxes: {x: yaList, y: xaList}[axLetter].map(axisIds.name2id), - overlayableAxes: {x: xaList, y: yaList}[axLetter].filter(function(axName2) { - return axName2 !== axName && !(layoutIn[axName2] || {}).overlaying; - }).map(axisIds.name2id) - }; - - handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, positioningOptions); - - layoutOut[axName] = axLayoutOut; - - // so we don't have to repeat autotype unnecessarily, - // copy an autotype back to layoutIn - if(!layoutIn[axName] && axLayoutIn.type !== '-') { - layoutIn[axName] = {type: axLayoutIn.type}; - } - - }); - - // quick second pass for range slider and selector defaults - var rangeSliderDefaults = Registry.getComponentMethod('rangeslider', 'handleDefaults'), - rangeSelectorDefaults = Registry.getComponentMethod('rangeselector', 'handleDefaults'); - - xaList.forEach(function(axName) { - axLayoutIn = layoutIn[axName]; - axLayoutOut = layoutOut[axName]; - - rangeSliderDefaults(layoutIn, layoutOut, axName); - - if(axLayoutOut.type === 'date') { - rangeSelectorDefaults( - axLayoutIn, - axLayoutOut, - layoutOut, - yaList, - axLayoutOut.calendar - ); - } - - coerce('fixedrange'); - }); - - yaList.forEach(function(axName) { - axLayoutIn = layoutIn[axName]; - axLayoutOut = layoutOut[axName]; - - var anchoredAxis = layoutOut[axisIds.id2name(axLayoutOut.anchor)]; - - var fixedRangeDflt = ( - anchoredAxis && - anchoredAxis.rangeslider && - anchoredAxis.rangeslider.visible - ); - - coerce('fixedrange', fixedRangeDflt); - }); + coerce("fixedrange", fixedRangeDflt); + }); }; diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index 722e8570963..70c58b170fa 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -5,53 +5,52 @@ * 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 d3 = require('d3'); +"use strict"; +var d3 = require("d3"); // flattenUniqueSort :: String -> Function -> [[String]] -> [String] function flattenUniqueSort(axisLetter, sortFunction, data) { + // Bisection based insertion sort of distinct values for logarithmic time complexity. + // Can't use a hashmap, which is O(1), because ES5 maps coerce keys to strings. If it ever becomes a bottleneck, + // code can be separated: a hashmap (JS object) based version if all values encountered are strings; and + // downgrading to this O(log(n)) array on the first encounter of a non-string value. + var categoryArray = []; - // Bisection based insertion sort of distinct values for logarithmic time complexity. - // Can't use a hashmap, which is O(1), because ES5 maps coerce keys to strings. If it ever becomes a bottleneck, - // code can be separated: a hashmap (JS object) based version if all values encountered are strings; and - // downgrading to this O(log(n)) array on the first encounter of a non-string value. - - var categoryArray = []; - - var traceLines = data.map(function(d) {return d[axisLetter];}); - - var i, j, tracePoints, category, insertionIndex; + var traceLines = data.map(function(d) { + return d[axisLetter]; + }); - var bisector = d3.bisector(sortFunction).left; + var i, j, tracePoints, category, insertionIndex; - for(i = 0; i < traceLines.length; i++) { + var bisector = d3.bisector(sortFunction).left; - tracePoints = traceLines[i]; + for (i = 0; i < traceLines.length; i++) { + tracePoints = traceLines[i]; - for(j = 0; j < tracePoints.length; j++) { + for (j = 0; j < tracePoints.length; j++) { + category = tracePoints[j]; - category = tracePoints[j]; + // skip loop: ignore null and undefined categories + if (category === null || category === undefined) continue; - // skip loop: ignore null and undefined categories - if(category === null || category === undefined) continue; + insertionIndex = bisector(categoryArray, category); - insertionIndex = bisector(categoryArray, category); + // skip loop on already encountered values + if ( + insertionIndex < categoryArray.length && + categoryArray[insertionIndex] === category + ) { + continue; + } - // skip loop on already encountered values - if(insertionIndex < categoryArray.length && categoryArray[insertionIndex] === category) continue; - - // insert value - categoryArray.splice(insertionIndex, 0, category); - } + // insert value + categoryArray.splice(insertionIndex, 0, category); } + } - return categoryArray; + return categoryArray; } - /** * This pure function returns the ordered categories for specified axisLetter, categoryorder, categoryarray and data. * @@ -63,15 +62,23 @@ function flattenUniqueSort(axisLetter, sortFunction, data) { * See cartesian/layout_attributes.js for the definition of categoryorder and categoryarray * */ - // orderedCategories :: String -> String -> [String] -> [[String]] -> [String] -module.exports = function orderedCategories(axisLetter, categoryorder, categoryarray, data) { - - switch(categoryorder) { - case 'array': return Array.isArray(categoryarray) ? categoryarray.slice() : []; - case 'category ascending': return flattenUniqueSort(axisLetter, d3.ascending, data); - case 'category descending': return flattenUniqueSort(axisLetter, d3.descending, data); - case 'trace': return []; - default: return []; - } +module.exports = function orderedCategories( + axisLetter, + categoryorder, + categoryarray, + data +) { + switch (categoryorder) { + case "array": + return Array.isArray(categoryarray) ? categoryarray.slice() : []; + case "category ascending": + return flattenUniqueSort(axisLetter, d3.ascending, data); + case "category descending": + return flattenUniqueSort(axisLetter, d3.descending, data); + case "trace": + return []; + default: + return []; + } }; diff --git a/src/plots/cartesian/position_defaults.js b/src/plots/cartesian/position_defaults.js index 94ea5ed4e73..ab6d09dc8e2 100644 --- a/src/plots/cartesian/position_defaults.js +++ b/src/plots/cartesian/position_defaults.js @@ -5,59 +5,76 @@ * 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 isNumeric = require("fast-isnumeric"); +var Lib = require("../../lib"); -'use strict'; +module.exports = function handlePositionDefaults( + containerIn, + containerOut, + coerce, + options +) { + var counterAxes = options.counterAxes || [], + overlayableAxes = options.overlayableAxes || [], + letter = options.letter; -var isNumeric = require('fast-isnumeric'); + var anchor = Lib.coerce( + containerIn, + containerOut, + { + anchor: { + valType: "enumerated", + values: ["free"].concat(counterAxes), + dflt: ( + isNumeric(containerIn.position) ? "free" : counterAxes[0] || "free" + ) + } + }, + "anchor" + ); -var Lib = require('../../lib'); + if (anchor === "free") coerce("position"); + Lib.coerce( + containerIn, + containerOut, + { + side: { + valType: "enumerated", + values: letter === "x" ? ["bottom", "top"] : ["left", "right"], + dflt: letter === "x" ? "bottom" : "left" + } + }, + "side" + ); -module.exports = function handlePositionDefaults(containerIn, containerOut, coerce, options) { - var counterAxes = options.counterAxes || [], - overlayableAxes = options.overlayableAxes || [], - letter = options.letter; - - var anchor = Lib.coerce(containerIn, containerOut, { - anchor: { - valType: 'enumerated', - values: ['free'].concat(counterAxes), - dflt: isNumeric(containerIn.position) ? 'free' : - (counterAxes[0] || 'free') + var overlaying = false; + if (overlayableAxes.length) { + overlaying = Lib.coerce( + containerIn, + containerOut, + { + overlaying: { + valType: "enumerated", + values: [false].concat(overlayableAxes), + dflt: false } - }, 'anchor'); + }, + "overlaying" + ); + } - if(anchor === 'free') coerce('position'); + if (!overlaying) { + // TODO: right now I'm copying this domain over to overlaying axes + // in ax.setscale()... but this means we still need (imperfect) logic + // in the axes popover to hide domain for the overlaying axis. + // perhaps I should make a private version _domain that all axes get??? + var domain = coerce("domain"); + if (domain[0] > domain[1] - 0.01) containerOut.domain = [0, 1]; + Lib.noneOrAll(containerIn.domain, containerOut.domain, [0, 1]); + } - Lib.coerce(containerIn, containerOut, { - side: { - valType: 'enumerated', - values: letter === 'x' ? ['bottom', 'top'] : ['left', 'right'], - dflt: letter === 'x' ? 'bottom' : 'left' - } - }, 'side'); - - var overlaying = false; - if(overlayableAxes.length) { - overlaying = Lib.coerce(containerIn, containerOut, { - overlaying: { - valType: 'enumerated', - values: [false].concat(overlayableAxes), - dflt: false - } - }, 'overlaying'); - } - - if(!overlaying) { - // TODO: right now I'm copying this domain over to overlaying axes - // in ax.setscale()... but this means we still need (imperfect) logic - // in the axes popover to hide domain for the overlaying axis. - // perhaps I should make a private version _domain that all axes get??? - var domain = coerce('domain'); - if(domain[0] > domain[1] - 0.01) containerOut.domain = [0, 1]; - Lib.noneOrAll(containerIn.domain, containerOut.domain, [0, 1]); - } - - return containerOut; + return containerOut; }; diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 65835464e3d..940ea6d5b05 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -5,194 +5,228 @@ * 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 polygon = require("../../lib/polygon"); +var color = require("../../components/color"); - -'use strict'; - -var polygon = require('../../lib/polygon'); -var color = require('../../components/color'); - -var axes = require('./axes'); -var constants = require('./constants'); +var axes = require("./axes"); +var constants = require("./constants"); var filteredPolygon = polygon.filter; var polygonTester = polygon.tester; var MINSELECT = constants.MINSELECT; -function getAxId(ax) { return ax._id; } +function getAxId(ax) { + return ax._id; +} module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { - var plot = dragOptions.gd._fullLayout._zoomlayer, - dragBBox = dragOptions.element.getBoundingClientRect(), - xs = dragOptions.plotinfo.xaxis._offset, - ys = dragOptions.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), - pts; - - if(mode === 'lasso') { - pts = filteredPolygon([[x0, y0]], constants.BENDPX); + var plot = dragOptions.gd._fullLayout._zoomlayer, + dragBBox = dragOptions.element.getBoundingClientRect(), + xs = dragOptions.plotinfo.xaxis._offset, + ys = dragOptions.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), + pts; + + if (mode === "lasso") { + pts = filteredPolygon([[x0, y0]], constants.BENDPX); + } + + var outlines = plot.selectAll("path.select-outline").data([1, 2]); + + outlines + .enter() + .append("path") + .attr("class", function(d) { + return "select-outline select-outline-" + d; + }) + .attr("transform", "translate(" + xs + ", " + ys + ")") + .attr("d", path0 + "Z"); + + var corners = plot + .append("path") + .attr("class", "zoombox-corners") + .style({ + fill: color.background, + stroke: color.defaultLine, + "stroke-width": 1 + }) + .attr("transform", "translate(" + xs + ", " + ys + ")") + .attr("d", "M0,0Z"); + + // find the traces to search for selection points + var searchTraces = [], + gd = dragOptions.gd, + i, + cd, + trace, + searchInfo, + selection = [], + eventData; + for (i = 0; i < gd.calcdata.length; i++) { + cd = gd.calcdata[i]; + trace = cd[0].trace; + if (!trace._module || !trace._module.selectPoints) continue; + + if (dragOptions.subplot) { + if (trace.subplot !== dragOptions.subplot) continue; + + searchTraces.push({ + selectPoints: trace._module.selectPoints, + 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, + cd: cd, + xaxis: axes.getFromId(gd, trace.xaxis), + yaxis: axes.getFromId(gd, trace.yaxis) + }); } + } - var outlines = plot.selectAll('path.select-outline').data([1, 2]); - - outlines.enter() - .append('path') - .attr('class', function(d) { return 'select-outline select-outline-' + d; }) - .attr('transform', 'translate(' + xs + ', ' + ys + ')') - .attr('d', path0 + 'Z'); - - var corners = plot.append('path') - .attr('class', 'zoombox-corners') - .style({ - fill: color.background, - stroke: color.defaultLine, - 'stroke-width': 1 - }) - .attr('transform', 'translate(' + xs + ', ' + ys + ')') - .attr('d', 'M0,0Z'); - - - // find the traces to search for selection points - var searchTraces = [], - gd = dragOptions.gd, - i, - cd, - trace, - searchInfo, - selection = [], - eventData; - for(i = 0; i < gd.calcdata.length; i++) { - cd = gd.calcdata[i]; - trace = cd[0].trace; - if(!trace._module || !trace._module.selectPoints) continue; - - if(dragOptions.subplot) { - if(trace.subplot !== dragOptions.subplot) continue; - - searchTraces.push({ - selectPoints: trace._module.selectPoints, - 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, - cd: cd, - xaxis: axes.getFromId(gd, trace.xaxis), - yaxis: axes.getFromId(gd, trace.yaxis) - }); - } + function axValue(ax) { + var index = ax._id.charAt(0) === "y" ? 1 : 0; + return function(v) { + return ax.p2d(v[index]); + }; + } + + function ascending(a, b) { + return a - b; + } + + dragOptions.moveFn = function(dx0, dy0) { + var poly, ax; + x1 = Math.max(0, Math.min(pw, dx0 + x0)); + y1 = Math.max(0, Math.min(ph, dy0 + y0)); + + var dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0); + + if (mode === "select") { + if (dy < Math.min(dx * 0.6, MINSELECT)) { + // horizontal motion: make a vertical box + poly = polygonTester([[x0, 0], [x0, ph], [x1, ph], [x1, 0]]); + // extras to guide users in keeping a straight selection + corners.attr( + "d", + "M" + + poly.xmin + + "," + + (y0 - MINSELECT) + + "h-4v" + + 2 * MINSELECT + + "h4Z" + + "M" + + (poly.xmax - 1) + + "," + + (y0 - MINSELECT) + + "h4v" + + 2 * MINSELECT + + "h-4Z" + ); + } else if (dx < Math.min(dy * 0.6, MINSELECT)) { + // vertical motion: make a horizontal box + poly = polygonTester([[0, y0], [0, y1], [pw, y1], [pw, y0]]); + corners.attr( + "d", + "M" + + (x0 - MINSELECT) + + "," + + poly.ymin + + "v-4h" + + 2 * MINSELECT + + "v4Z" + + "M" + + (x0 - MINSELECT) + + "," + + (poly.ymax - 1) + + "v4h" + + 2 * MINSELECT + + "v-4Z" + ); + } else { + // diagonal motion + poly = polygonTester([[x0, y0], [x0, y1], [x1, y1], [x1, y0]]); + corners.attr("d", "M0,0Z"); + } + outlines.attr( + "d", + "M" + + poly.xmin + + "," + + poly.ymin + + "H" + + (poly.xmax - 1) + + "V" + + (poly.ymax - 1) + + "H" + + poly.xmin + + "Z" + ); + } else if (mode === "lasso") { + pts.addPt([x1, y1]); + poly = polygonTester(pts.filtered); + outlines.attr("d", "M" + pts.filtered.join("L") + "Z"); } - function axValue(ax) { - var index = (ax._id.charAt(0) === 'y') ? 1 : 0; - return function(v) { return ax.p2d(v[index]); }; + selection = []; + for (i = 0; i < searchTraces.length; i++) { + searchInfo = searchTraces[i]; + [].push.apply(selection, searchInfo.selectPoints(searchInfo, poly)); } - function ascending(a, b) { return a - b; } - - dragOptions.moveFn = function(dx0, dy0) { - var poly, - ax; - x1 = Math.max(0, Math.min(pw, dx0 + x0)); - y1 = Math.max(0, Math.min(ph, dy0 + y0)); - - var dx = Math.abs(x1 - x0), - dy = Math.abs(y1 - y0); - - if(mode === 'select') { - if(dy < Math.min(dx * 0.6, MINSELECT)) { - // horizontal motion: make a vertical box - poly = polygonTester([[x0, 0], [x0, ph], [x1, ph], [x1, 0]]); - // extras to guide users in keeping a straight selection - corners.attr('d', 'M' + poly.xmin + ',' + (y0 - MINSELECT) + - 'h-4v' + (2 * MINSELECT) + 'h4Z' + - 'M' + (poly.xmax - 1) + ',' + (y0 - MINSELECT) + - 'h4v' + (2 * MINSELECT) + 'h-4Z'); - - } - else if(dx < Math.min(dy * 0.6, MINSELECT)) { - // vertical motion: make a horizontal box - poly = polygonTester([[0, y0], [0, y1], [pw, y1], [pw, y0]]); - corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + poly.ymin + - 'v-4h' + (2 * MINSELECT) + 'v4Z' + - 'M' + (x0 - MINSELECT) + ',' + (poly.ymax - 1) + - 'v4h' + (2 * MINSELECT) + 'v-4Z'); - } - else { - // diagonal motion - poly = polygonTester([[x0, y0], [x0, y1], [x1, y1], [x1, y0]]); - corners.attr('d', 'M0,0Z'); - } - outlines.attr('d', 'M' + poly.xmin + ',' + poly.ymin + - 'H' + (poly.xmax - 1) + 'V' + (poly.ymax - 1) + - 'H' + poly.xmin + 'Z'); - } - else if(mode === 'lasso') { - pts.addPt([x1, y1]); - poly = polygonTester(pts.filtered); - outlines.attr('d', 'M' + pts.filtered.join('L') + 'Z'); - } - - selection = []; - for(i = 0; i < searchTraces.length; i++) { - searchInfo = searchTraces[i]; - [].push.apply(selection, searchInfo.selectPoints(searchInfo, poly)); - } - - eventData = {points: selection}; - - if(mode === 'select') { - var ranges = eventData.range = {}, - axLetter; - - for(i = 0; i < allAxes.length; i++) { - ax = allAxes[i]; - axLetter = ax._id.charAt(0); - ranges[ax._id] = [ - ax.p2d(poly[axLetter + 'min']), - ax.p2d(poly[axLetter + 'max'])].sort(ascending); - } - } - else { - var dataPts = eventData.lassoPoints = {}; - - for(i = 0; i < allAxes.length; i++) { - ax = allAxes[i]; - dataPts[ax._id] = pts.filtered.map(axValue(ax)); - } - } - dragOptions.gd.emit('plotly_selecting', eventData); - }; - - dragOptions.doneFn = function(dragged, numclicks) { - corners.remove(); - if(!dragged && numclicks === 2) { - // clear selection on doubleclick - outlines.remove(); - for(i = 0; i < searchTraces.length; i++) { - searchInfo = searchTraces[i]; - searchInfo.selectPoints(searchInfo, false); - } - - gd.emit('plotly_deselect', null); - } - else { - dragOptions.gd.emit('plotly_selected', eventData); - } - }; + eventData = { points: selection }; + + if (mode === "select") { + var ranges = eventData.range = {}, axLetter; + + for (i = 0; i < allAxes.length; i++) { + ax = allAxes[i]; + axLetter = ax._id.charAt(0); + ranges[ax._id] = [ + ax.p2d(poly[axLetter + "min"]), + ax.p2d(poly[axLetter + "max"]) + ].sort(ascending); + } + } else { + var dataPts = eventData.lassoPoints = {}; + + for (i = 0; i < allAxes.length; i++) { + ax = allAxes[i]; + dataPts[ax._id] = pts.filtered.map(axValue(ax)); + } + } + dragOptions.gd.emit("plotly_selecting", eventData); + }; + + dragOptions.doneFn = function(dragged, numclicks) { + corners.remove(); + if (!dragged && numclicks === 2) { + // clear selection on doubleclick + outlines.remove(); + for (i = 0; i < searchTraces.length; i++) { + searchInfo = searchTraces[i]; + searchInfo.selectPoints(searchInfo, false); + } + + gd.emit("plotly_deselect", null); + } else { + dragOptions.gd.emit("plotly_selected", eventData); + } + }; }; diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 4c757bff288..cff402268cc 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -5,34 +5,31 @@ * 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 d3 = require("d3"); +var isNumeric = require("fast-isnumeric"); - -'use strict'; - -var d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); +var Lib = require("../../lib"); var cleanNumber = Lib.cleanNumber; var ms2DateTime = Lib.ms2DateTime; var dateTime2ms = Lib.dateTime2ms; -var numConstants = require('../../constants/numerical'); +var numConstants = require("../../constants/numerical"); var FP_SAFE = numConstants.FP_SAFE; var BADNUM = numConstants.BADNUM; -var constants = require('./constants'); -var axisIds = require('./axis_ids'); +var constants = require("./constants"); +var axisIds = require("./axis_ids"); function fromLog(v) { - return Math.pow(10, v); + return Math.pow(10, v); } function num(v) { - if(!isNumeric(v)) return BADNUM; - v = Number(v); - if(v < -FP_SAFE || v > FP_SAFE) return BADNUM; - return isNumeric(v) ? Number(v) : BADNUM; + if (!isNumeric(v)) return BADNUM; + v = Number(v); + if (v < -FP_SAFE || v > FP_SAFE) return BADNUM; + return isNumeric(v) ? Number(v) : BADNUM; } /** @@ -62,55 +59,53 @@ function num(v) { * and the autotick constraints ._minDtick, ._forceTick0 */ module.exports = function setConvert(ax) { - - // clipMult: how many axis lengths past the edge do we render? - // for panning, 1-2 would suffice, but for zooming more is nice. - // also, clipping can affect the direction of lines off the edge... - var clipMult = 10; - - function toLog(v, clip) { - if(v > 0) return Math.log(v) / Math.LN10; - - else if(v <= 0 && clip && ax.range && ax.range.length === 2) { - // clip NaN (ie past negative infinity) to clipMult axis - // length past the negative edge - var r0 = ax.range[0], - r1 = ax.range[1]; - return 0.5 * (r0 + r1 - 3 * clipMult * Math.abs(r0 - r1)); - } - - else return BADNUM; + // clipMult: how many axis lengths past the edge do we render? + // for panning, 1-2 would suffice, but for zooming more is nice. + // also, clipping can affect the direction of lines off the edge... + var clipMult = 10; + + function toLog(v, clip) { + if (v > 0) { + return Math.log(v) / Math.LN10; + } else if (v <= 0 && clip && ax.range && ax.range.length === 2) { + // clip NaN (ie past negative infinity) to clipMult axis + // length past the negative edge + var r0 = ax.range[0], r1 = ax.range[1]; + return 0.5 * (r0 + r1 - 3 * clipMult * Math.abs(r0 - r1)); + } else { + return BADNUM; } + } - /* + /* * wrapped dateTime2ms that: * - accepts ms numbers for backward compatibility * - inserts a dummy arg so calendar is the 3rd arg (see notes below). * - defaults to ax.calendar */ - function dt2ms(v, _, calendar) { - // NOTE: Changed this behavior: previously we took any numeric value - // to be a ms, even if it was a string that could be a bare year. - // Now we convert it as a date if at all possible, and only try - // as (local) ms if that fails. - var ms = dateTime2ms(v, calendar || ax.calendar); - if(ms === BADNUM) { - if(isNumeric(v)) ms = dateTime2ms(new Date(+v)); - else return BADNUM; - } - return ms; + function dt2ms(v, _, calendar) { + // NOTE: Changed this behavior: previously we took any numeric value + // to be a ms, even if it was a string that could be a bare year. + // Now we convert it as a date if at all possible, and only try + // as (local) ms if that fails. + var ms = dateTime2ms(v, calendar || ax.calendar); + if (ms === BADNUM) { + if (isNumeric(v)) ms = dateTime2ms(new Date(+v)); + else return BADNUM; } + return ms; + } - // wrapped ms2DateTime to insert default ax.calendar - function ms2dt(v, r, calendar) { - return ms2DateTime(v, r, calendar || ax.calendar); - } + // wrapped ms2DateTime to insert default ax.calendar + function ms2dt(v, r, calendar) { + return ms2DateTime(v, r, calendar || ax.calendar); + } - function getCategoryName(v) { - return ax._categories[Math.round(v)]; - } + function getCategoryName(v) { + return ax._categories[Math.round(v)]; + } - /* + /* * setCategoryIndex: return the index of category v, * inserting it in the list if it's not already there * @@ -123,79 +118,98 @@ module.exports = function setConvert(ax) { * already sorted category order; otherwise there would be * a disconnect between the array and the index returned */ - function setCategoryIndex(v) { - if(v !== null && v !== undefined) { - var c = ax._categories.indexOf(v); - if(c === -1) { - ax._categories.push(v); - return ax._categories.length - 1; - } - return c; - } - return BADNUM; - } - - function getCategoryIndex(v) { - // d2l/d2c variant that that won't add categories but will also - // allow numbers to be mapped to the linearized axis positions - var index = ax._categories.indexOf(v); - if(index !== -1) return index; - if(typeof v === 'number') return v; + function setCategoryIndex(v) { + if (v !== null && v !== undefined) { + var c = ax._categories.indexOf(v); + if (c === -1) { + ax._categories.push(v); + return ax._categories.length - 1; + } + return c; } - - function l2p(v) { - if(!isNumeric(v)) return BADNUM; - - // include 2 fractional digits on pixel, for PDF zooming etc - return d3.round(ax._b + ax._m * v, 2); - } - - function p2l(px) { return (px - ax._b) / ax._m; } - - // conversions among c/l/p are fairly simple - do them together for all axis types - ax.c2l = (ax.type === 'log') ? toLog : num; - ax.l2c = (ax.type === 'log') ? fromLog : num; - - ax.l2p = l2p; - ax.p2l = p2l; - - ax.c2p = (ax.type === 'log') ? function(v, clip) { return l2p(toLog(v, clip)); } : l2p; - ax.p2c = (ax.type === 'log') ? function(px) { return fromLog(p2l(px)); } : p2l; - - /* + return BADNUM; + } + + function getCategoryIndex(v) { + // d2l/d2c variant that that won't add categories but will also + // allow numbers to be mapped to the linearized axis positions + var index = ax._categories.indexOf(v); + if (index !== -1) return index; + if (typeof v === "number") return v; + } + + function l2p(v) { + if (!isNumeric(v)) return BADNUM; + + // include 2 fractional digits on pixel, for PDF zooming etc + return d3.round(ax._b + ax._m * v, 2); + } + + function p2l(px) { + return (px - ax._b) / ax._m; + } + + // conversions among c/l/p are fairly simple - do them together for all axis types + ax.c2l = ax.type === "log" ? toLog : num; + ax.l2c = ax.type === "log" ? fromLog : num; + + ax.l2p = l2p; + ax.p2l = p2l; + + ax.c2p = ax.type === "log" + ? (function(v, clip) { + return l2p(toLog(v, clip)); + }) + : l2p; + ax.p2c = ax.type === "log" + ? (function(px) { + return fromLog(p2l(px)); + }) + : p2l; + + /* * now type-specific conversions for **ALL** other combinations * they're all written out, instead of being combinations of each other, for * both clarity and speed. */ - if(['linear', '-'].indexOf(ax.type) !== -1) { - // all are data vals, but d and r need cleaning - ax.d2r = ax.r2d = ax.d2c = ax.r2c = ax.d2l = ax.r2l = cleanNumber; - ax.c2d = ax.c2r = ax.l2d = ax.l2r = num; + if (["linear", "-"].indexOf(ax.type) !== -1) { + // all are data vals, but d and r need cleaning + ax.d2r = ax.r2d = ax.d2c = ax.r2c = ax.d2l = ax.r2l = cleanNumber; + ax.c2d = ax.c2r = ax.l2d = ax.l2r = num; - ax.d2p = ax.r2p = function(v) { return l2p(cleanNumber(v)); }; - ax.p2d = ax.p2r = p2l; - } - else if(ax.type === 'log') { - // d and c are data vals, r and l are logged (but d and r need cleaning) - ax.d2r = ax.d2l = function(v, clip) { return toLog(cleanNumber(v), clip); }; - ax.r2d = ax.r2c = function(v) { return fromLog(cleanNumber(v)); }; - - ax.d2c = ax.r2l = cleanNumber; - ax.c2d = ax.l2r = num; + ax.d2p = ax.r2p = function(v) { + return l2p(cleanNumber(v)); + }; + ax.p2d = ax.p2r = p2l; + } else if (ax.type === "log") { + // d and c are data vals, r and l are logged (but d and r need cleaning) + ax.d2r = ax.d2l = function(v, clip) { + return toLog(cleanNumber(v), clip); + }; + ax.r2d = ax.r2c = function(v) { + return fromLog(cleanNumber(v)); + }; - ax.c2r = toLog; - ax.l2d = fromLog; + ax.d2c = ax.r2l = cleanNumber; + ax.c2d = ax.l2r = num; - ax.d2p = function(v, clip) { return l2p(ax.d2r(v, clip)); }; - ax.p2d = function(px) { return fromLog(p2l(px)); }; + ax.c2r = toLog; + ax.l2d = fromLog; - ax.r2p = function(v) { return l2p(cleanNumber(v)); }; - ax.p2r = p2l; - } - else if(ax.type === 'date') { - // r and d are date strings, l and c are ms + ax.d2p = function(v, clip) { + return l2p(ax.d2r(v, clip)); + }; + ax.p2d = function(px) { + return fromLog(p2l(px)); + }; - /* + ax.r2p = function(v) { + return l2p(cleanNumber(v)); + }; + ax.p2r = p2l; + } else if (ax.type === "date") { + // r and d are date strings, l and c are ms + /* * Any of these functions with r and d on either side, calendar is the * **3rd** argument. log has reserved the second argument. * @@ -203,48 +217,52 @@ module.exports = function setConvert(ax) { * uses this to limit precision, toLog uses true to clip negatives * to offscreen low rather than undefined), it's safe to pass 0. */ - ax.d2r = ax.r2d = Lib.identity; - - ax.d2c = ax.r2c = ax.d2l = ax.r2l = dt2ms; - ax.c2d = ax.c2r = ax.l2d = ax.l2r = ms2dt; - - ax.d2p = ax.r2p = function(v, _, calendar) { return l2p(dt2ms(v, 0, calendar)); }; - ax.p2d = ax.p2r = function(px, r, calendar) { return ms2dt(p2l(px), r, calendar); }; - } - else if(ax.type === 'category') { - // d is categories; r, c, and l are indices - // TODO: should r accept category names too? - // ie r2c and r2l would be getCategoryIndex (and r2p would change) + ax.d2r = ax.r2d = Lib.identity; - ax.d2r = ax.d2c = ax.d2l = setCategoryIndex; - ax.r2d = ax.c2d = ax.l2d = getCategoryName; + ax.d2c = ax.r2c = ax.d2l = ax.r2l = dt2ms; + ax.c2d = ax.c2r = ax.l2d = ax.l2r = ms2dt; - // special d2l variant that won't add categories - ax.d2l_noadd = getCategoryIndex; + ax.d2p = ax.r2p = function(v, _, calendar) { + return l2p(dt2ms(v, 0, calendar)); + }; + ax.p2d = ax.p2r = function(px, r, calendar) { + return ms2dt(p2l(px), r, calendar); + }; + } else if (ax.type === "category") { + // d is categories; r, c, and l are indices + // TODO: should r accept category names too? + // ie r2c and r2l would be getCategoryIndex (and r2p would change) + ax.d2r = ax.d2c = ax.d2l = setCategoryIndex; + ax.r2d = ax.c2d = ax.l2d = getCategoryName; - ax.r2l = ax.l2r = ax.r2c = ax.c2r = num; + // special d2l variant that won't add categories + ax.d2l_noadd = getCategoryIndex; - ax.d2p = function(v) { return l2p(getCategoryIndex(v)); }; - ax.p2d = function(px) { return getCategoryName(p2l(px)); }; - ax.r2p = l2p; - ax.p2r = p2l; - } + ax.r2l = ax.l2r = ax.r2c = ax.c2r = num; - // find the range value at the specified (linear) fraction of the axis - ax.fraction2r = function(v) { - var rl0 = ax.r2l(ax.range[0]), - rl1 = ax.r2l(ax.range[1]); - return ax.l2r(rl0 + v * (rl1 - rl0)); + ax.d2p = function(v) { + return l2p(getCategoryIndex(v)); }; - - // find the fraction of the range at the specified range value - ax.r2fraction = function(v) { - var rl0 = ax.r2l(ax.range[0]), - rl1 = ax.r2l(ax.range[1]); - return (ax.r2l(v) - rl0) / (rl1 - rl0); + ax.p2d = function(px) { + return getCategoryName(p2l(px)); }; - - /* + ax.r2p = l2p; + ax.p2r = p2l; + } + + // find the range value at the specified (linear) fraction of the axis + ax.fraction2r = function(v) { + var rl0 = ax.r2l(ax.range[0]), rl1 = ax.r2l(ax.range[1]); + return ax.l2r(rl0 + v * (rl1 - rl0)); + }; + + // find the fraction of the range at the specified range value + ax.r2fraction = function(v) { + var rl0 = ax.r2l(ax.range[0]), rl1 = ax.r2l(ax.range[1]); + return (ax.r2l(v) - rl0) / (rl1 - rl0); + }; + + /* * cleanRange: make sure range is a couplet of valid & distinct values * keep numbers away from the limits of floating point numbers, * and dates away from the ends of our date system (+/- 9999 years) @@ -252,163 +270,159 @@ module.exports = function setConvert(ax) { * optional param rangeAttr: operate on a different attribute, like * ax._r, rather than ax.range */ - ax.cleanRange = function(rangeAttr) { - if(!rangeAttr) rangeAttr = 'range'; - var range = ax[rangeAttr], - axLetter = (ax._id || 'x').charAt(0), - i, dflt; - - if(ax.type === 'date') dflt = Lib.dfltRange(ax.calendar); - else if(axLetter === 'y') dflt = constants.DFLTRANGEY; - else dflt = constants.DFLTRANGEX; - - // make sure we don't later mutate the defaults - dflt = dflt.slice(); - - if(!range || range.length !== 2) { - ax[rangeAttr] = dflt; - return; - } + ax.cleanRange = function(rangeAttr) { + if (!rangeAttr) rangeAttr = "range"; + var range = ax[rangeAttr], axLetter = (ax._id || "x").charAt(0), i, dflt; - if(ax.type === 'date') { - // check if milliseconds or js date objects are provided for range - // and convert to date strings - range[0] = Lib.cleanDate(range[0], BADNUM, ax.calendar); - range[1] = Lib.cleanDate(range[1], BADNUM, ax.calendar); - } + if (ax.type === "date") dflt = Lib.dfltRange(ax.calendar); + else if (axLetter === "y") dflt = constants.DFLTRANGEY; + else dflt = constants.DFLTRANGEX; - for(i = 0; i < 2; i++) { - if(ax.type === 'date') { - if(!Lib.isDateTime(range[i], ax.calendar)) { - ax[rangeAttr] = dflt; - break; - } - - if(ax.r2l(range[0]) === ax.r2l(range[1])) { - // split by +/- 1 second - var linCenter = Lib.constrain(ax.r2l(range[0]), - Lib.MIN_MS + 1000, Lib.MAX_MS - 1000); - range[0] = ax.l2r(linCenter - 1000); - range[1] = ax.l2r(linCenter + 1000); - break; - } - } - else { - if(!isNumeric(range[i])) { - if(isNumeric(range[1 - i])) { - range[i] = range[1 - i] * (i ? 10 : 0.1); - } - else { - ax[rangeAttr] = dflt; - break; - } - } - - if(range[i] < -FP_SAFE) range[i] = -FP_SAFE; - else if(range[i] > FP_SAFE) range[i] = FP_SAFE; - - if(range[0] === range[1]) { - // somewhat arbitrary: split by 1 or 1ppm, whichever is bigger - var inc = Math.max(1, Math.abs(range[0] * 1e-6)); - range[0] -= inc; - range[1] += inc; - } - } - } - }; + // make sure we don't later mutate the defaults + dflt = dflt.slice(); - // set scaling to pixels - ax.setScale = function(usePrivateRange) { - var gs = ax._gd._fullLayout._size, - axLetter = ax._id.charAt(0); + if (!range || range.length !== 2) { + ax[rangeAttr] = dflt; + return; + } - // TODO cleaner way to handle this case - if(!ax._categories) ax._categories = []; + if (ax.type === "date") { + // check if milliseconds or js date objects are provided for range + // and convert to date strings + range[0] = Lib.cleanDate(range[0], BADNUM, ax.calendar); + range[1] = Lib.cleanDate(range[1], BADNUM, ax.calendar); + } - // make sure we have a domain (pull it in from the axis - // this one is overlaying if necessary) - if(ax.overlaying) { - var ax2 = axisIds.getFromId(ax._gd, ax.overlaying); - ax.domain = ax2.domain; + for (i = 0; i < 2; i++) { + if (ax.type === "date") { + if (!Lib.isDateTime(range[i], ax.calendar)) { + ax[rangeAttr] = dflt; + break; } - // While transitions are occuring, occurring, we get a double-transform - // issue if we transform the drawn layer *and* use the new axis range to - // draw the data. This allows us to construct setConvert using the pre- - // interaction values of the range: - var rangeAttr = (usePrivateRange && ax._r) ? '_r' : 'range', - calendar = ax.calendar; - ax.cleanRange(rangeAttr); - - var rl0 = ax.r2l(ax[rangeAttr][0], calendar), - rl1 = ax.r2l(ax[rangeAttr][1], calendar); - - if(axLetter === 'y') { - ax._offset = gs.t + (1 - ax.domain[1]) * gs.h; - ax._length = gs.h * (ax.domain[1] - ax.domain[0]); - ax._m = ax._length / (rl0 - rl1); - ax._b = -ax._m * rl1; + if (ax.r2l(range[0]) === ax.r2l(range[1])) { + // split by +/- 1 second + var linCenter = Lib.constrain( + ax.r2l(range[0]), + Lib.MIN_MS + 1000, + Lib.MAX_MS - 1000 + ); + range[0] = ax.l2r(linCenter - 1000); + range[1] = ax.l2r(linCenter + 1000); + break; } - else { - ax._offset = gs.l + ax.domain[0] * gs.w; - ax._length = gs.w * (ax.domain[1] - ax.domain[0]); - ax._m = ax._length / (rl1 - rl0); - ax._b = -ax._m * rl0; + } else { + if (!isNumeric(range[i])) { + if (isNumeric(range[1 - i])) { + range[i] = range[1 - i] * (i ? 10 : 0.1); + } else { + ax[rangeAttr] = dflt; + break; + } } - if(!isFinite(ax._m) || !isFinite(ax._b)) { - Lib.notifier( - 'Something went wrong with axis scaling', - 'long'); - ax._gd._replotting = false; - throw new Error('axis scaling'); - } - }; + if (range[i] < -FP_SAFE) range[i] = -FP_SAFE; + else if (range[i] > FP_SAFE) range[i] = FP_SAFE; - // makeCalcdata: takes an x or y array and converts it - // to a position on the axis object "ax" - // inputs: - // trace - a data object from gd.data - // axLetter - a string, either 'x' or 'y', for which item - // to convert (TODO: is this now always the same as - // the first letter of ax._id?) - // in case the expected data isn't there, make a list of - // integers based on the opposite data - ax.makeCalcdata = function(trace, axLetter) { - var arrayIn, arrayOut, i; - - var cal = ax.type === 'date' && trace[axLetter + 'calendar']; - - if(axLetter in trace) { - arrayIn = trace[axLetter]; - arrayOut = new Array(arrayIn.length); - - for(i = 0; i < arrayIn.length; i++) { - arrayOut[i] = ax.d2c(arrayIn[i], 0, cal); - } + if (range[0] === range[1]) { + // somewhat arbitrary: split by 1 or 1ppm, whichever is bigger + var inc = Math.max(1, Math.abs(range[0] * 1e-6)); + range[0] -= inc; + range[1] += inc; } - else { - var v0 = ((axLetter + '0') in trace) ? - ax.d2c(trace[axLetter + '0'], 0, cal) : 0, - dv = (trace['d' + axLetter]) ? - Number(trace['d' + axLetter]) : 1; + } + } + }; - // the opposing data, for size if we have x and dx etc - arrayIn = trace[{x: 'y', y: 'x'}[axLetter]]; - arrayOut = new Array(arrayIn.length); + // set scaling to pixels + ax.setScale = function(usePrivateRange) { + var gs = ax._gd._fullLayout._size, axLetter = ax._id.charAt(0); - for(i = 0; i < arrayIn.length; i++) arrayOut[i] = v0 + i * dv; - } - return arrayOut; - }; + // TODO cleaner way to handle this case + if (!ax._categories) ax._categories = []; + + // make sure we have a domain (pull it in from the axis + // this one is overlaying if necessary) + if (ax.overlaying) { + var ax2 = axisIds.getFromId(ax._gd, ax.overlaying); + ax.domain = ax2.domain; + } - // for autoranging: arrays of objects: - // {val: axis value, pad: pixel padding} - // on the low and high sides - ax._min = []; - ax._max = []; + // While transitions are occuring, occurring, we get a double-transform + // issue if we transform the drawn layer *and* use the new axis range to + // draw the data. This allows us to construct setConvert using the pre- + // interaction values of the range: + var rangeAttr = usePrivateRange && ax._r ? "_r" : "range", + calendar = ax.calendar; + ax.cleanRange(rangeAttr); + + var rl0 = ax.r2l(ax[rangeAttr][0], calendar), + rl1 = ax.r2l(ax[rangeAttr][1], calendar); + + if (axLetter === "y") { + ax._offset = gs.t + (1 - ax.domain[1]) * gs.h; + ax._length = gs.h * (ax.domain[1] - ax.domain[0]); + ax._m = ax._length / (rl0 - rl1); + ax._b = (-ax._m) * rl1; + } else { + ax._offset = gs.l + ax.domain[0] * gs.w; + ax._length = gs.w * (ax.domain[1] - ax.domain[0]); + ax._m = ax._length / (rl1 - rl0); + ax._b = (-ax._m) * rl0; + } - // and for bar charts and box plots: reset forced minimum tick spacing - delete ax._minDtick; - delete ax._forceTick0; + if (!isFinite(ax._m) || !isFinite(ax._b)) { + Lib.notifier("Something went wrong with axis scaling", "long"); + ax._gd._replotting = false; + throw new Error("axis scaling"); + } + }; + + // makeCalcdata: takes an x or y array and converts it + // to a position on the axis object "ax" + // inputs: + // trace - a data object from gd.data + // axLetter - a string, either 'x' or 'y', for which item + // to convert (TODO: is this now always the same as + // the first letter of ax._id?) + // in case the expected data isn't there, make a list of + // integers based on the opposite data + ax.makeCalcdata = function(trace, axLetter) { + var arrayIn, arrayOut, i; + + var cal = ax.type === "date" && trace[axLetter + "calendar"]; + + if (axLetter in trace) { + arrayIn = trace[axLetter]; + arrayOut = new Array(arrayIn.length); + + for (i = 0; i < arrayIn.length; i++) { + arrayOut[i] = ax.d2c(arrayIn[i], 0, cal); + } + } else { + var v0 = axLetter + "0" in trace + ? ax.d2c(trace[axLetter + "0"], 0, cal) + : 0, + dv = trace["d" + axLetter] ? Number(trace["d" + axLetter]) : 1; + + // the opposing data, for size if we have x and dx etc + arrayIn = trace[({ x: "y", y: "x" })[axLetter]]; + arrayOut = new Array(arrayIn.length); + + for (i = 0; i < arrayIn.length; i++) { + arrayOut[i] = v0 + i * dv; + } + } + return arrayOut; + }; + + // for autoranging: arrays of objects: + // {val: axis value, pad: pixel padding} + // on the low and high sides + ax._min = []; + ax._max = []; + + // and for bar charts and box plots: reset forced minimum tick spacing + delete ax._minDtick; + delete ax._forceTick0; }; diff --git a/src/plots/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js index 5f37680d331..98c2b89f637 100644 --- a/src/plots/cartesian/tick_label_defaults.js +++ b/src/plots/cartesian/tick_label_defaults.js @@ -5,50 +5,53 @@ * 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'); - +"use strict"; +var Lib = require("../../lib"); /** * options: inherits font, outerTicks, noHover from axes.handleAxisDefaults */ -module.exports = function handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options) { - var showAttrDflt = getShowAttrDflt(containerIn); +module.exports = function handleTickLabelDefaults( + containerIn, + containerOut, + coerce, + axType, + options +) { + var showAttrDflt = getShowAttrDflt(containerIn); - var tickPrefix = coerce('tickprefix'); - if(tickPrefix) coerce('showtickprefix', showAttrDflt); + var tickPrefix = coerce("tickprefix"); + if (tickPrefix) coerce("showtickprefix", showAttrDflt); - var tickSuffix = coerce('ticksuffix'); - if(tickSuffix) coerce('showticksuffix', showAttrDflt); + var tickSuffix = coerce("ticksuffix"); + if (tickSuffix) coerce("showticksuffix", showAttrDflt); - var showTickLabels = coerce('showticklabels'); - if(showTickLabels) { - var font = options.font || {}; - // as with titlefont.color, inherit axis.color only if one was - // explicitly provided - var dfltFontColor = (containerOut.color === containerIn.color) ? - containerOut.color : font.color; - Lib.coerceFont(coerce, 'tickfont', { - family: font.family, - size: font.size, - color: dfltFontColor - }); - coerce('tickangle'); + var showTickLabels = coerce("showticklabels"); + if (showTickLabels) { + var font = options.font || {}; + // as with titlefont.color, inherit axis.color only if one was + // explicitly provided + var dfltFontColor = containerOut.color === containerIn.color + ? containerOut.color + : font.color; + Lib.coerceFont(coerce, "tickfont", { + family: font.family, + size: font.size, + color: dfltFontColor + }); + coerce("tickangle"); - if(axType !== 'category') { - var tickFormat = coerce('tickformat'); - if(!tickFormat && axType !== 'date') { - coerce('showexponent', showAttrDflt); - coerce('exponentformat'); - coerce('separatethousands'); - } - } + if (axType !== "category") { + var tickFormat = coerce("tickformat"); + if (!tickFormat && axType !== "date") { + coerce("showexponent", showAttrDflt); + coerce("exponentformat"); + coerce("separatethousands"); + } } + } - if(axType !== 'category' && !options.noHover) coerce('hoverformat'); + if (axType !== "category" && !options.noHover) coerce("hoverformat"); }; /* @@ -66,17 +69,15 @@ module.exports = function handleTickLabelDefaults(containerIn, containerOut, coe * */ function getShowAttrDflt(containerIn) { - var showAttrsAll = ['showexponent', - 'showtickprefix', - 'showticksuffix'], - showAttrs = showAttrsAll.filter(function(a) { - return containerIn[a] !== undefined; - }), - sameVal = function(a) { - return containerIn[a] === containerIn[showAttrs[0]]; - }; + var showAttrsAll = ["showexponent", "showtickprefix", "showticksuffix"], + showAttrs = showAttrsAll.filter(function(a) { + return containerIn[a] !== undefined; + }), + sameVal = function(a) { + return containerIn[a] === containerIn[showAttrs[0]]; + }; - if(showAttrs.every(sameVal) || showAttrs.length === 1) { - return containerIn[showAttrs[0]]; - } + if (showAttrs.every(sameVal) || showAttrs.length === 1) { + return containerIn[showAttrs[0]]; + } } diff --git a/src/plots/cartesian/tick_mark_defaults.js b/src/plots/cartesian/tick_mark_defaults.js index def1ecdff4c..33c8dc82f85 100644 --- a/src/plots/cartesian/tick_mark_defaults.js +++ b/src/plots/cartesian/tick_mark_defaults.js @@ -5,27 +5,47 @@ * 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"); - -'use strict'; - -var Lib = require('../../lib'); - -var layoutAttributes = require('./layout_attributes'); - +var layoutAttributes = require("./layout_attributes"); /** * options: inherits outerTicks from axes.handleAxisDefaults */ -module.exports = function handleTickDefaults(containerIn, containerOut, coerce, options) { - var tickLen = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'ticklen'), - tickWidth = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'tickwidth'), - tickColor = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'tickcolor', containerOut.color), - showTicks = coerce('ticks', (options.outerTicks || tickLen || tickWidth || tickColor) ? 'outside' : ''); +module.exports = function handleTickDefaults( + containerIn, + containerOut, + coerce, + options +) { + var tickLen = Lib.coerce2( + containerIn, + containerOut, + layoutAttributes, + "ticklen" + ), + tickWidth = Lib.coerce2( + containerIn, + containerOut, + layoutAttributes, + "tickwidth" + ), + tickColor = Lib.coerce2( + containerIn, + containerOut, + layoutAttributes, + "tickcolor", + containerOut.color + ), + showTicks = coerce( + "ticks", + options.outerTicks || tickLen || tickWidth || tickColor ? "outside" : "" + ); - if(!showTicks) { - delete containerOut.ticklen; - delete containerOut.tickwidth; - delete containerOut.tickcolor; - } + if (!showTicks) { + delete containerOut.ticklen; + delete containerOut.tickwidth; + delete containerOut.tickcolor; + } }; diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index e0f4bffc2d4..4949dab13dc 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -5,78 +5,82 @@ * 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 isNumeric = require("fast-isnumeric"); +var Lib = require("../../lib"); +var ONEDAY = require("../../constants/numerical").ONEDAY; +module.exports = function handleTickValueDefaults( + containerIn, + containerOut, + coerce, + axType +) { + var tickmodeDefault = "auto"; -'use strict'; + if ( + containerIn.tickmode === "array" && (axType === "log" || axType === "date") + ) { + containerIn.tickmode = "auto"; + } -var isNumeric = require('fast-isnumeric'); -var Lib = require('../../lib'); -var ONEDAY = require('../../constants/numerical').ONEDAY; + if (Array.isArray(containerIn.tickvals)) { + tickmodeDefault = "array"; + } else if (containerIn.dtick) { + tickmodeDefault = "linear"; + } + var tickmode = coerce("tickmode", tickmodeDefault); + if (tickmode === "auto") { + coerce("nticks"); + } else if (tickmode === "linear") { + // dtick is usually a positive number, but there are some + // special strings available for log or date axes + // default is 1 day for dates, otherwise 1 + var dtickDflt = axType === "date" ? ONEDAY : 1; + var dtick = coerce("dtick", dtickDflt); + if (isNumeric(dtick)) { + containerOut.dtick = dtick > 0 ? Number(dtick) : dtickDflt; + } else if (typeof dtick !== "string") { + containerOut.dtick = dtickDflt; + } else { + // date and log special cases are all one character plus a number + var prefix = dtick.charAt(0), dtickNum = dtick.substr(1); -module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType) { - var tickmodeDefault = 'auto'; - - if(containerIn.tickmode === 'array' && - (axType === 'log' || axType === 'date')) { - containerIn.tickmode = 'auto'; - } - - if(Array.isArray(containerIn.tickvals)) tickmodeDefault = 'array'; - else if(containerIn.dtick) { - tickmodeDefault = 'linear'; + dtickNum = isNumeric(dtickNum) ? Number(dtickNum) : 0; + if ( + dtickNum <= 0 || + !(axType === "date" && + prefix === "M" && + dtickNum === Math.round(dtickNum) || + // "L" gives ticks linearly spaced in data (not in position) every (float) f + axType === "log" && prefix === "L" || + // "D1" gives powers of 10 with all small digits between, "D2" gives only 2 and 5 + axType === "log" && + prefix === "D" && + (dtickNum === 1 || dtickNum === 2)) + ) { + containerOut.dtick = dtickDflt; + } } - var tickmode = coerce('tickmode', tickmodeDefault); - if(tickmode === 'auto') coerce('nticks'); - else if(tickmode === 'linear') { - // dtick is usually a positive number, but there are some - // special strings available for log or date axes - // default is 1 day for dates, otherwise 1 - var dtickDflt = (axType === 'date') ? ONEDAY : 1; - var dtick = coerce('dtick', dtickDflt); - if(isNumeric(dtick)) { - containerOut.dtick = (dtick > 0) ? Number(dtick) : dtickDflt; - } - else if(typeof dtick !== 'string') { - containerOut.dtick = dtickDflt; - } - else { - // date and log special cases are all one character plus a number - var prefix = dtick.charAt(0), - dtickNum = dtick.substr(1); - - dtickNum = isNumeric(dtickNum) ? Number(dtickNum) : 0; - if((dtickNum <= 0) || !( - // "M" gives ticks every (integer) n months - (axType === 'date' && prefix === 'M' && dtickNum === Math.round(dtickNum)) || - // "L" gives ticks linearly spaced in data (not in position) every (float) f - (axType === 'log' && prefix === 'L') || - // "D1" gives powers of 10 with all small digits between, "D2" gives only 2 and 5 - (axType === 'log' && prefix === 'D' && (dtickNum === 1 || dtickNum === 2)) - )) { - containerOut.dtick = dtickDflt; - } - } - - // tick0 can have different valType for different axis types, so - // validate that now. Also for dates, change milliseconds to date strings - var tick0Dflt = (axType === 'date') ? Lib.dateTick0(containerOut.calendar) : 0; - var tick0 = coerce('tick0', tick0Dflt); - if(axType === 'date') { - containerOut.tick0 = Lib.cleanDate(tick0, tick0Dflt); - } - // Aside from date axes, dtick must be numeric; D1 and D2 modes ignore tick0 entirely - else if(isNumeric(tick0) && dtick !== 'D1' && dtick !== 'D2') { - containerOut.tick0 = Number(tick0); - } - else { - containerOut.tick0 = tick0Dflt; - } - } - else { - var tickvals = coerce('tickvals'); - if(tickvals === undefined) containerOut.tickmode = 'auto'; - else coerce('ticktext'); + // tick0 can have different valType for different axis types, so + // validate that now. Also for dates, change milliseconds to date strings + var tick0Dflt = axType === "date" + ? Lib.dateTick0(containerOut.calendar) + : 0; + var tick0 = coerce("tick0", tick0Dflt); + if (axType === "date") { + containerOut.tick0 = Lib.cleanDate(tick0, tick0Dflt); + } else if (isNumeric(tick0) && dtick !== "D1" && dtick !== "D2") { + // Aside from date axes, dtick must be numeric; D1 and D2 modes ignore tick0 entirely + containerOut.tick0 = Number(tick0); + } else { + containerOut.tick0 = tick0Dflt; } + } else { + var tickvals = coerce("tickvals"); + if (tickvals === undefined) containerOut.tickmode = "auto"; + else coerce("ticktext"); + } }; diff --git a/src/plots/cartesian/transition_axes.js b/src/plots/cartesian/transition_axes.js index b41c50b8cc3..77d6b788ccf 100644 --- a/src/plots/cartesian/transition_axes.js +++ b/src/plots/cartesian/transition_axes.js @@ -5,306 +5,327 @@ * 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 d3 = require("d3"); - -'use strict'; - -var d3 = require('d3'); - -var Plotly = require('../../plotly'); -var Registry = require('../../registry'); -var Drawing = require('../../components/drawing'); -var Axes = require('./axes'); +var Plotly = require("../../plotly"); +var Registry = require("../../registry"); +var Drawing = require("../../components/drawing"); +var Axes = require("./axes"); var axisRegex = /((x|y)([2-9]|[1-9][0-9]+)?)axis$/; -module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCompleteCallback) { - var fullLayout = gd._fullLayout; - var axes = []; - - function computeUpdates(layout) { - var ai, attrList, match, axis, update; - var updates = {}; - - for(ai in layout) { - attrList = ai.split('.'); - match = attrList[0].match(axisRegex); - if(match) { - var axisLetter = match[1]; - var axisName = axisLetter + 'axis'; - axis = fullLayout[axisName]; - update = {}; - - if(Array.isArray(layout[ai])) { - update.to = layout[ai].slice(0); - } else { - if(Array.isArray(layout[ai].range)) { - update.to = layout[ai].range.slice(0); - } - } - if(!update.to) continue; - - update.axisName = axisName; - update.length = axis._length; - - axes.push(axisLetter); - - updates[axisLetter] = update; - } +module.exports = function transitionAxes( + gd, + newLayout, + transitionOpts, + makeOnCompleteCallback +) { + var fullLayout = gd._fullLayout; + var axes = []; + + function computeUpdates(layout) { + var ai, attrList, match, axis, update; + var updates = {}; + + for (ai in layout) { + attrList = ai.split("."); + match = attrList[0].match(axisRegex); + if (match) { + var axisLetter = match[1]; + var axisName = axisLetter + "axis"; + axis = fullLayout[axisName]; + update = {}; + + if (Array.isArray(layout[ai])) { + update.to = layout[ai].slice(0); + } else { + if (Array.isArray(layout[ai].range)) { + update.to = layout[ai].range.slice(0); + } } + if (!update.to) continue; - return updates; - } + update.axisName = axisName; + update.length = axis._length; - function computeAffectedSubplots(fullLayout, updatedAxisIds, updates) { - var plotName; - var plotinfos = fullLayout._plots; - var affectedSubplots = []; - var toX, toY; - - for(plotName in plotinfos) { - var plotinfo = plotinfos[plotName]; - - if(affectedSubplots.indexOf(plotinfo) !== -1) continue; - - var x = plotinfo.xaxis._id; - var y = plotinfo.yaxis._id; - var fromX = plotinfo.xaxis.range; - var fromY = plotinfo.yaxis.range; - - // Store the initial range at the beginning of this transition: - plotinfo.xaxis._r = plotinfo.xaxis.range.slice(); - plotinfo.yaxis._r = plotinfo.yaxis.range.slice(); - - if(updates[x]) { - toX = updates[x].to; - } else { - toX = fromX; - } - if(updates[y]) { - toY = updates[y].to; - } else { - toY = fromY; - } - - if(fromX[0] === toX[0] && fromX[1] === toX[1] && fromY[0] === toY[0] && fromY[1] === toY[1]) continue; - - if(updatedAxisIds.indexOf(x) !== -1 || updatedAxisIds.indexOf(y) !== -1) { - affectedSubplots.push(plotinfo); - } - } + axes.push(axisLetter); - return affectedSubplots; + updates[axisLetter] = update; + } } - var updates = computeUpdates(newLayout); - var updatedAxisIds = Object.keys(updates); - var affectedSubplots = computeAffectedSubplots(fullLayout, updatedAxisIds, updates); - - if(!affectedSubplots.length) { - return false; + return updates; + } + + function computeAffectedSubplots(fullLayout, updatedAxisIds, updates) { + var plotName; + var plotinfos = fullLayout._plots; + var affectedSubplots = []; + var toX, toY; + + for (plotName in plotinfos) { + var plotinfo = plotinfos[plotName]; + + if (affectedSubplots.indexOf(plotinfo) !== -1) continue; + + var x = plotinfo.xaxis._id; + var y = plotinfo.yaxis._id; + var fromX = plotinfo.xaxis.range; + var fromY = plotinfo.yaxis.range; + + // Store the initial range at the beginning of this transition: + plotinfo.xaxis._r = plotinfo.xaxis.range.slice(); + plotinfo.yaxis._r = plotinfo.yaxis.range.slice(); + + if (updates[x]) { + toX = updates[x].to; + } else { + toX = fromX; + } + if (updates[y]) { + toY = updates[y].to; + } else { + toY = fromY; + } + + if ( + fromX[0] === toX[0] && + fromX[1] === toX[1] && + fromY[0] === toY[0] && + fromY[1] === toY[1] + ) { + continue; + } + + if ( + updatedAxisIds.indexOf(x) !== -1 || updatedAxisIds.indexOf(y) !== -1 + ) { + affectedSubplots.push(plotinfo); + } } - function ticksAndAnnotations(xa, ya) { - var activeAxIds = [], - i; - - activeAxIds = [xa._id, ya._id]; - - for(i = 0; i < activeAxIds.length; i++) { - Axes.doTicks(gd, activeAxIds[i], true); - } - - function redrawObjs(objArray, method) { - for(i = 0; i < objArray.length; i++) { - var obji = objArray[i]; - - if((activeAxIds.indexOf(obji.xref) !== -1) || - (activeAxIds.indexOf(obji.yref) !== -1)) { - method(gd, i); - } - } - } - - // 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')); - } + return affectedSubplots; + } - function unsetSubplotTransform(subplot) { - var xa2 = subplot.xaxis; - var ya2 = subplot.yaxis; + var updates = computeUpdates(newLayout); + var updatedAxisIds = Object.keys(updates); + var affectedSubplots = computeAffectedSubplots( + fullLayout, + updatedAxisIds, + updates + ); - fullLayout._defs.selectAll('#' + subplot.clipId) - .call(Drawing.setTranslate, 0, 0) - .call(Drawing.setScale, 1, 1); + if (!affectedSubplots.length) { + return false; + } - subplot.plot - .call(Drawing.setTranslate, xa2._offset, ya2._offset) - .call(Drawing.setScale, 1, 1) + function ticksAndAnnotations(xa, ya) { + var activeAxIds = [], i; - // This is specifically directed at scatter traces, applying an inverse - // scale to individual points to counteract the scale of the trace - // as a whole: - .selectAll('.points').selectAll('.point') - .call(Drawing.setPointGroupScale, 1, 1); + activeAxIds = [xa._id, ya._id]; + for (i = 0; i < activeAxIds.length; i++) { + Axes.doTicks(gd, activeAxIds[i], true); } - function updateSubplot(subplot, progress) { - var axis, r0, r1; - var xUpdate = updates[subplot.xaxis._id]; - var yUpdate = updates[subplot.yaxis._id]; - - var viewBox = []; - - if(xUpdate) { - axis = gd._fullLayout[xUpdate.axisName]; - r0 = axis._r; - r1 = xUpdate.to; - viewBox[0] = (r0[0] * (1 - progress) + progress * r1[0] - r0[0]) / (r0[1] - r0[0]) * subplot.xaxis._length; - var dx1 = r0[1] - r0[0]; - var dx2 = r1[1] - r1[0]; + function redrawObjs(objArray, method) { + for (i = 0; i < objArray.length; i++) { + var obji = objArray[i]; - axis.range[0] = r0[0] * (1 - progress) + progress * r1[0]; - axis.range[1] = r0[1] * (1 - progress) + progress * r1[1]; - - viewBox[2] = subplot.xaxis._length * ((1 - progress) + progress * dx2 / dx1); - } else { - viewBox[0] = 0; - viewBox[2] = subplot.xaxis._length; + if ( + activeAxIds.indexOf(obji.xref) !== -1 || + activeAxIds.indexOf(obji.yref) !== -1 + ) { + method(gd, i); } + } + } - if(yUpdate) { - axis = gd._fullLayout[yUpdate.axisName]; - r0 = axis._r; - r1 = yUpdate.to; - viewBox[1] = (r0[1] * (1 - progress) + progress * r1[1] - r0[1]) / (r0[0] - r0[1]) * subplot.yaxis._length; - var dy1 = r0[1] - r0[0]; - var dy2 = r1[1] - r1[0]; - - axis.range[0] = r0[0] * (1 - progress) + progress * r1[0]; - axis.range[1] = r0[1] * (1 - progress) + progress * r1[1]; - - viewBox[3] = subplot.yaxis._length * ((1 - progress) + progress * dy2 / dy1); - } else { - viewBox[1] = 0; - viewBox[3] = subplot.yaxis._length; - } + // 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") + ); + } + + function unsetSubplotTransform(subplot) { + var xa2 = subplot.xaxis; + var ya2 = subplot.yaxis; + + fullLayout._defs + .selectAll("#" + subplot.clipId) + .call(Drawing.setTranslate, 0, 0) + .call(Drawing.setScale, 1, 1); + + subplot.plot + .call(Drawing.setTranslate, xa2._offset, ya2._offset) + .call(Drawing.setScale, 1, 1) + .selectAll(".points") + .selectAll(".point") + .call(Drawing.setPointGroupScale, 1, 1); + } + + function updateSubplot(subplot, progress) { + var axis, r0, r1; + var xUpdate = updates[subplot.xaxis._id]; + var yUpdate = updates[subplot.yaxis._id]; + + var viewBox = []; + + if (xUpdate) { + axis = gd._fullLayout[xUpdate.axisName]; + r0 = axis._r; + r1 = xUpdate.to; + viewBox[0] = (r0[0] * (1 - progress) + progress * r1[0] - r0[0]) / + (r0[1] - r0[0]) * + subplot.xaxis._length; + var dx1 = r0[1] - r0[0]; + var dx2 = r1[1] - r1[0]; + + axis.range[0] = r0[0] * (1 - progress) + progress * r1[0]; + axis.range[1] = r0[1] * (1 - progress) + progress * r1[1]; + + viewBox[2] = subplot.xaxis._length * + (1 - progress + progress * dx2 / dx1); + } else { + viewBox[0] = 0; + viewBox[2] = subplot.xaxis._length; + } - ticksAndAnnotations(subplot.xaxis, subplot.yaxis); + if (yUpdate) { + axis = gd._fullLayout[yUpdate.axisName]; + r0 = axis._r; + r1 = yUpdate.to; + viewBox[1] = (r0[1] * (1 - progress) + progress * r1[1] - r0[1]) / + (r0[0] - r0[1]) * + subplot.yaxis._length; + var dy1 = r0[1] - r0[0]; + var dy2 = r1[1] - r1[0]; + + axis.range[0] = r0[0] * (1 - progress) + progress * r1[0]; + axis.range[1] = r0[1] * (1 - progress) + progress * r1[1]; + + viewBox[3] = subplot.yaxis._length * + (1 - progress + progress * dy2 / dy1); + } else { + viewBox[1] = 0; + viewBox[3] = subplot.yaxis._length; + } + ticksAndAnnotations(subplot.xaxis, subplot.yaxis); - var xa2 = subplot.xaxis; - var ya2 = subplot.yaxis; + var xa2 = subplot.xaxis; + var ya2 = subplot.yaxis; - var editX = !!xUpdate; - var editY = !!yUpdate; + var editX = !!xUpdate; + var editY = !!yUpdate; - var xScaleFactor = editX ? xa2._length / viewBox[2] : 1, - yScaleFactor = editY ? ya2._length / viewBox[3] : 1; + var xScaleFactor = editX ? xa2._length / viewBox[2] : 1, + yScaleFactor = editY ? ya2._length / viewBox[3] : 1; - var clipDx = editX ? viewBox[0] : 0, - clipDy = editY ? viewBox[1] : 0; + var clipDx = editX ? viewBox[0] : 0, clipDy = editY ? viewBox[1] : 0; - var fracDx = editX ? (viewBox[0] / viewBox[2] * xa2._length) : 0, - fracDy = editY ? (viewBox[1] / viewBox[3] * ya2._length) : 0; + var fracDx = editX ? viewBox[0] / viewBox[2] * xa2._length : 0, + fracDy = editY ? viewBox[1] / viewBox[3] * ya2._length : 0; - var plotDx = xa2._offset - fracDx, - plotDy = ya2._offset - fracDy; + var plotDx = xa2._offset - fracDx, plotDy = ya2._offset - fracDy; - fullLayout._defs.selectAll('#' + subplot.clipId) - .call(Drawing.setTranslate, clipDx, clipDy) - .call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor); + fullLayout._defs + .selectAll("#" + subplot.clipId) + .call(Drawing.setTranslate, clipDx, clipDy) + .call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor); - subplot.plot - .call(Drawing.setTranslate, plotDx, plotDy) - .call(Drawing.setScale, xScaleFactor, yScaleFactor) + subplot.plot + .call(Drawing.setTranslate, plotDx, plotDy) + .call(Drawing.setScale, xScaleFactor, yScaleFactor) + .selectAll(".points") + .selectAll(".point") + .call(Drawing.setPointGroupScale, 1 / xScaleFactor, 1 / yScaleFactor); + } - // This is specifically directed at scatter traces, applying an inverse - // scale to individual points to counteract the scale of the trace - // as a whole: - .selectAll('.points').selectAll('.point') - .call(Drawing.setPointGroupScale, 1 / xScaleFactor, 1 / yScaleFactor); + var onComplete; + if (makeOnCompleteCallback) { + // This module makes the choice whether or not it notifies Plotly.transition + // about completion: + onComplete = makeOnCompleteCallback(); + } - } + function transitionComplete() { + var aobj = {}; + for (var i = 0; i < updatedAxisIds.length; i++) { + var axi = gd._fullLayout[updates[updatedAxisIds[i]].axisName]; + var to = updates[updatedAxisIds[i]].to; + aobj[axi._name + ".range[0]"] = to[0]; + aobj[axi._name + ".range[1]"] = to[1]; - var onComplete; - if(makeOnCompleteCallback) { - // This module makes the choice whether or not it notifies Plotly.transition - // about completion: - onComplete = makeOnCompleteCallback(); + axi.range = to.slice(); } - function transitionComplete() { - var aobj = {}; - for(var i = 0; i < updatedAxisIds.length; i++) { - var axi = gd._fullLayout[updates[updatedAxisIds[i]].axisName]; - var to = updates[updatedAxisIds[i]].to; - aobj[axi._name + '.range[0]'] = to[0]; - aobj[axi._name + '.range[1]'] = to[1]; + // Signal that this transition has completed: + onComplete && onComplete(); - axi.range = to.slice(); - } + return Plotly.relayout(gd, aobj).then(function() { + for (var i = 0; i < affectedSubplots.length; i++) { + unsetSubplotTransform(affectedSubplots[i]); + } + }); + } - // Signal that this transition has completed: - onComplete && onComplete(); + function transitionInterrupt() { + var aobj = {}; + for (var i = 0; i < updatedAxisIds.length; i++) { + var axi = gd._fullLayout[updatedAxisIds[i] + "axis"]; + aobj[axi._name + ".range[0]"] = axi.range[0]; + aobj[axi._name + ".range[1]"] = axi.range[1]; - return Plotly.relayout(gd, aobj).then(function() { - for(var i = 0; i < affectedSubplots.length; i++) { - unsetSubplotTransform(affectedSubplots[i]); - } - }); + axi.range = axi._r.slice(); } - function transitionInterrupt() { - var aobj = {}; - for(var i = 0; i < updatedAxisIds.length; i++) { - var axi = gd._fullLayout[updatedAxisIds[i] + 'axis']; - aobj[axi._name + '.range[0]'] = axi.range[0]; - aobj[axi._name + '.range[1]'] = axi.range[1]; - - axi.range = axi._r.slice(); - } - - return Plotly.relayout(gd, aobj).then(function() { - for(var i = 0; i < affectedSubplots.length; i++) { - unsetSubplotTransform(affectedSubplots[i]); - } - }); - } + return Plotly.relayout(gd, aobj).then(function() { + for (var i = 0; i < affectedSubplots.length; i++) { + unsetSubplotTransform(affectedSubplots[i]); + } + }); + } - var t1, t2, raf; - var easeFn = d3.ease(transitionOpts.easing); + var t1, t2, raf; + var easeFn = d3.ease(transitionOpts.easing); - gd._transitionData._interruptCallbacks.push(function() { - window.cancelAnimationFrame(raf); - raf = null; - return transitionInterrupt(); - }); + gd._transitionData._interruptCallbacks.push(function() { + window.cancelAnimationFrame(raf); + raf = null; + return transitionInterrupt(); + }); - function doFrame() { - t2 = Date.now(); + function doFrame() { + t2 = Date.now(); - var tInterp = Math.min(1, (t2 - t1) / transitionOpts.duration); - var progress = easeFn(tInterp); + var tInterp = Math.min(1, (t2 - t1) / transitionOpts.duration); + var progress = easeFn(tInterp); - for(var i = 0; i < affectedSubplots.length; i++) { - updateSubplot(affectedSubplots[i], progress); - } + for (var i = 0; i < affectedSubplots.length; i++) { + updateSubplot(affectedSubplots[i], progress); + } - if(t2 - t1 > transitionOpts.duration) { - transitionComplete(); - raf = window.cancelAnimationFrame(doFrame); - } else { - raf = window.requestAnimationFrame(doFrame); - } + if (t2 - t1 > transitionOpts.duration) { + transitionComplete(); + raf = window.cancelAnimationFrame(doFrame); + } else { + raf = window.requestAnimationFrame(doFrame); } + } - t1 = Date.now(); - raf = window.requestAnimationFrame(doFrame); + t1 = Date.now(); + raf = window.requestAnimationFrame(doFrame); - return Promise.resolve(); + return Promise.resolve(); }; diff --git a/src/plots/command.js b/src/plots/command.js index 62c060edd5e..bf6639eaa30 100644 --- a/src/plots/command.js +++ b/src/plots/command.js @@ -5,12 +5,9 @@ * 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 Plotly = require('../plotly'); -var Lib = require('../lib'); +"use strict"; +var Plotly = require("../plotly"); +var Lib = require("../lib"); /* * Create or update an observer. This function is designed to be @@ -29,110 +26,113 @@ var Lib = require('../lib'); * with information about the new state. */ exports.manageCommandObserver = function(gd, container, commandList, onchange) { - var ret = {}; - var enabled = true; - - if(container && container._commandObserver) { - ret = container._commandObserver; - } - - if(!ret.cache) { - ret.cache = {}; - } - - // Either create or just recompute this: - ret.lookupTable = {}; - - var binding = exports.hasSimpleAPICommandBindings(gd, commandList, ret.lookupTable); - - if(container && container._commandObserver) { - if(!binding) { - // If container exists and there are no longer any bindings, - // remove existing: - if(container._commandObserver.remove) { - container._commandObserver.remove(); - container._commandObserver = null; - return ret; - } - } else { - // If container exists and there *are* bindings, then the lookup - // table should have been updated and check is already attached, - // so there's nothing to be done: - return ret; - - - } + var ret = {}; + var enabled = true; + + if (container && container._commandObserver) { + ret = container._commandObserver; + } + + if (!ret.cache) { + ret.cache = {}; + } + + // Either create or just recompute this: + ret.lookupTable = {}; + + var binding = exports.hasSimpleAPICommandBindings( + gd, + commandList, + ret.lookupTable + ); + + if (container && container._commandObserver) { + if (!binding) { + // If container exists and there are no longer any bindings, + // remove existing: + if (container._commandObserver.remove) { + container._commandObserver.remove(); + container._commandObserver = null; + return ret; + } + } else { + // If container exists and there *are* bindings, then the lookup + // table should have been updated and check is already attached, + // so there's nothing to be done: + return ret; } - - // Determine whether there's anything to do for this binding: - - if(binding) { - // Build the cache: - bindingValueHasChanged(gd, binding, ret.cache); - - ret.check = function check() { - if(!enabled) return; - - var update = bindingValueHasChanged(gd, binding, ret.cache); - - if(update.changed && onchange) { - // Disable checks for the duration of this command in order to avoid - // infinite loops: - if(ret.lookupTable[update.value] !== undefined) { - ret.disable(); - Promise.resolve(onchange({ - value: update.value, - type: binding.type, - prop: binding.prop, - traces: binding.traces, - index: ret.lookupTable[update.value] - })).then(ret.enable, ret.enable); - } - } - - return update.changed; - }; - - var checkEvents = [ - 'plotly_relayout', - 'plotly_redraw', - 'plotly_restyle', - 'plotly_update', - 'plotly_animatingframe', - 'plotly_afterplot' - ]; - - for(var i = 0; i < checkEvents.length; i++) { - gd._internalOn(checkEvents[i], ret.check); + } + + // Determine whether there's anything to do for this binding: + if (binding) { + // Build the cache: + bindingValueHasChanged(gd, binding, ret.cache); + + ret.check = function check() { + if (!enabled) return; + + var update = bindingValueHasChanged(gd, binding, ret.cache); + + if (update.changed && onchange) { + // Disable checks for the duration of this command in order to avoid + // infinite loops: + if (ret.lookupTable[update.value] !== undefined) { + ret.disable(); + Promise.resolve( + onchange({ + value: update.value, + type: binding.type, + prop: binding.prop, + traces: binding.traces, + index: ret.lookupTable[update.value] + }) + ).then(ret.enable, ret.enable); } + } - ret.remove = function() { - for(var i = 0; i < checkEvents.length; i++) { - gd._removeInternalListener(checkEvents[i], ret.check); - } - }; - } else { - // TODO: It'd be really neat to actually give a *reason* for this, but at least a warning - // is a start - Lib.warn('Unable to automatically bind plot updates to API command'); + return update.changed; + }; - ret.lookupTable = {}; - ret.remove = function() {}; + var checkEvents = [ + "plotly_relayout", + "plotly_redraw", + "plotly_restyle", + "plotly_update", + "plotly_animatingframe", + "plotly_afterplot" + ]; + + for (var i = 0; i < checkEvents.length; i++) { + gd._internalOn(checkEvents[i], ret.check); } - ret.disable = function disable() { - enabled = false; + ret.remove = function() { + for (var i = 0; i < checkEvents.length; i++) { + gd._removeInternalListener(checkEvents[i], ret.check); + } }; + } else { + // TODO: It'd be really neat to actually give a *reason* for this, but at least a warning + // is a start + Lib.warn("Unable to automatically bind plot updates to API command"); - ret.enable = function enable() { - enabled = true; - }; + ret.lookupTable = {}; + ret.remove = function() {}; + } - if(container) { - container._commandObserver = ret; - } + ret.disable = function disable() { + enabled = false; + }; - return ret; + ret.enable = function enable() { + enabled = true; + }; + + if (container) { + container._commandObserver = ret; + } + + return ret; }; /* @@ -144,108 +144,109 @@ exports.manageCommandObserver = function(gd, container, commandList, onchange) { * 2. only one property may be affected * 3. the same property must be affected by all commands */ -exports.hasSimpleAPICommandBindings = function(gd, commandList, bindingsByValue) { - var i; - var n = commandList.length; - - var refBinding; - - for(i = 0; i < n; i++) { - var binding; - var command = commandList[i]; - var method = command.method; - var args = command.args; - - // If any command has no method, refuse to bind: - if(!method) { - return false; - } - var bindings = exports.computeAPICommandBindings(gd, method, args); +exports.hasSimpleAPICommandBindings = function( + gd, + commandList, + bindingsByValue +) { + var i; + var n = commandList.length; + + var refBinding; + + for (i = 0; i < n; i++) { + var binding; + var command = commandList[i]; + var method = command.method; + var args = command.args; + + // If any command has no method, refuse to bind: + if (!method) { + return false; + } + var bindings = exports.computeAPICommandBindings(gd, method, args); - // Right now, handle one and *only* one property being set: - if(bindings.length !== 1) { - return false; - } + // Right now, handle one and *only* one property being set: + if (bindings.length !== 1) { + return false; + } - if(!refBinding) { - refBinding = bindings[0]; - if(Array.isArray(refBinding.traces)) { - refBinding.traces.sort(); + if (!refBinding) { + refBinding = bindings[0]; + if (Array.isArray(refBinding.traces)) { + refBinding.traces.sort(); + } + } else { + binding = bindings[0]; + if (binding.type !== refBinding.type) { + return false; + } + if (binding.prop !== refBinding.prop) { + return false; + } + if (Array.isArray(refBinding.traces)) { + if (Array.isArray(binding.traces)) { + binding.traces.sort(); + for (var j = 0; j < refBinding.traces.length; j++) { + if (refBinding.traces[j] !== binding.traces[j]) { + return false; } + } } else { - binding = bindings[0]; - if(binding.type !== refBinding.type) { - return false; - } - if(binding.prop !== refBinding.prop) { - return false; - } - if(Array.isArray(refBinding.traces)) { - if(Array.isArray(binding.traces)) { - binding.traces.sort(); - for(var j = 0; j < refBinding.traces.length; j++) { - if(refBinding.traces[j] !== binding.traces[j]) { - return false; - } - } - } else { - return false; - } - } else { - if(binding.prop !== refBinding.prop) { - return false; - } - } + return false; } - - binding = bindings[0]; - var value = binding.value; - if(Array.isArray(value)) { - if(value.length === 1) { - value = value[0]; - } else { - return false; - } - } - if(bindingsByValue) { - bindingsByValue[value] = i; + } else { + if (binding.prop !== refBinding.prop) { + return false; } + } } - return refBinding; -}; - -function bindingValueHasChanged(gd, binding, cache) { - var container, value, obj; - var changed = false; - - if(binding.type === 'data') { - // If it's data, we need to get a trace. Based on the limited scope - // of what we cover, we can just take the first trace from the list, - // or otherwise just the first trace: - container = gd._fullData[binding.traces !== null ? binding.traces[0] : 0]; - } else if(binding.type === 'layout') { - container = gd._fullLayout; - } else { + binding = bindings[0]; + var value = binding.value; + if (Array.isArray(value)) { + if (value.length === 1) { + value = value[0]; + } else { return false; + } } + if (bindingsByValue) { + bindingsByValue[value] = i; + } + } - value = Lib.nestedProperty(container, binding.prop).get(); - - obj = cache[binding.type] = cache[binding.type] || {}; + return refBinding; +}; - if(obj.hasOwnProperty(binding.prop)) { - if(obj[binding.prop] !== value) { - changed = true; - } +function bindingValueHasChanged(gd, binding, cache) { + var container, value, obj; + var changed = false; + + if (binding.type === "data") { + // If it's data, we need to get a trace. Based on the limited scope + // of what we cover, we can just take the first trace from the list, + // or otherwise just the first trace: + container = gd._fullData[binding.traces !== null ? binding.traces[0] : 0]; + } else if (binding.type === "layout") { + container = gd._fullLayout; + } else { + return false; + } + + value = Lib.nestedProperty(container, binding.prop).get(); + + obj = cache[binding.type] = cache[binding.type] || {}; + + if (obj.hasOwnProperty(binding.prop)) { + if (obj[binding.prop] !== value) { + changed = true; } + } - obj[binding.prop] = value; + obj[binding.prop] = value; - return { - changed: changed, - value: value - }; + return { changed: changed, value: value }; } /* @@ -260,156 +261,173 @@ function bindingValueHasChanged(gd, binding, cache) { * A list of arguments passed to the API command */ exports.executeAPICommand = function(gd, method, args) { - var apiMethod = Plotly[method]; + var apiMethod = Plotly[method]; - var allArgs = [gd]; - for(var i = 0; i < args.length; i++) { - allArgs.push(args[i]); - } + var allArgs = [gd]; + for (var i = 0; i < args.length; i++) { + allArgs.push(args[i]); + } - return apiMethod.apply(null, allArgs).catch(function(err) { - Lib.warn('API call to Plotly.' + method + ' rejected.', err); - return Promise.reject(err); - }); + return apiMethod.apply(null, allArgs).catch(function(err) { + Lib.warn("API call to Plotly." + method + " rejected.", err); + return Promise.reject(err); + }); }; exports.computeAPICommandBindings = function(gd, method, args) { - var bindings; - switch(method) { - case 'restyle': - bindings = computeDataBindings(gd, args); - break; - case 'relayout': - bindings = computeLayoutBindings(gd, args); - break; - case 'update': - bindings = computeDataBindings(gd, [args[0], args[2]]) - .concat(computeLayoutBindings(gd, [args[1]])); - break; - case 'animate': - bindings = computeAnimateBindings(gd, args); - break; - default: - // This is the case where intelligent logic about what affects - // this command is not implemented. It causes no ill effects. - // For example, addFrames simply won't bind to a control component. - bindings = []; - } - return bindings; + var bindings; + switch (method) { + case "restyle": + bindings = computeDataBindings(gd, args); + break; + case "relayout": + bindings = computeLayoutBindings(gd, args); + break; + case "update": + bindings = computeDataBindings(gd, [args[0], args[2]]).concat( + computeLayoutBindings(gd, [args[1]]) + ); + break; + case "animate": + bindings = computeAnimateBindings(gd, args); + break; + default: + // This is the case where intelligent logic about what affects + // this command is not implemented. It causes no ill effects. + // For example, addFrames simply won't bind to a control component. + bindings = []; + } + return bindings; }; function computeAnimateBindings(gd, args) { - // We'll assume that the only relevant modification an animation - // makes that's meaningfully tracked is the frame: - if(Array.isArray(args[0]) && args[0].length === 1 && ['string', 'number'].indexOf(typeof args[0][0]) !== -1) { - return [{type: 'layout', prop: '_currentFrame', value: args[0][0].toString()}]; - } else { - return []; - } + // We'll assume that the only relevant modification an animation + // makes that's meaningfully tracked is the frame: + if ( + Array.isArray(args[0]) && + args[0].length === 1 && + ["string", "number"].indexOf(typeof args[0][0]) !== -1 + ) { + return [ + { type: "layout", prop: "_currentFrame", value: args[0][0].toString() } + ]; + } else { + return []; + } } function computeLayoutBindings(gd, args) { - var bindings = []; - - var astr = args[0]; - var aobj = {}; - if(typeof astr === 'string') { - aobj[astr] = args[1]; - } else if(Lib.isPlainObject(astr)) { - aobj = astr; - } else { - return bindings; - } - - crawl(aobj, function(path, attrName, attr) { - bindings.push({type: 'layout', prop: path, value: attr}); - }, '', 0); - + var bindings = []; + + var astr = args[0]; + var aobj = {}; + if (typeof astr === "string") { + aobj[astr] = args[1]; + } else if (Lib.isPlainObject(astr)) { + aobj = astr; + } else { return bindings; + } + + crawl( + aobj, + function(path, attrName, attr) { + bindings.push({ type: "layout", prop: path, value: attr }); + }, + "", + 0 + ); + + return bindings; } function computeDataBindings(gd, args) { - var traces, astr, val, aobj; - var bindings = []; - - // Logic copied from Plotly.restyle: - astr = args[0]; - val = args[1]; - traces = args[2]; - aobj = {}; - if(typeof astr === 'string') { - aobj[astr] = val; - } else if(Lib.isPlainObject(astr)) { - // the 3-arg form - aobj = astr; - - if(traces === undefined) { - traces = val; - } - } else { - return bindings; - } - - if(traces === undefined) { - // Explicitly assign this to null instead of undefined: - traces = null; + var traces, astr, val, aobj; + var bindings = []; + + // Logic copied from Plotly.restyle: + astr = args[0]; + val = args[1]; + traces = args[2]; + aobj = {}; + if (typeof astr === "string") { + aobj[astr] = val; + } else if (Lib.isPlainObject(astr)) { + // the 3-arg form + aobj = astr; + + if (traces === undefined) { + traces = val; } - - crawl(aobj, function(path, attrName, attr) { - var thisTraces; - if(Array.isArray(attr)) { - var nAttr = Math.min(attr.length, gd.data.length); - if(traces) { - nAttr = Math.min(nAttr, traces.length); - } - thisTraces = []; - for(var j = 0; j < nAttr; j++) { - thisTraces[j] = traces ? traces[j] : j; - } - } else { - thisTraces = traces ? traces.slice(0) : null; + } else { + return bindings; + } + + if (traces === undefined) { + // Explicitly assign this to null instead of undefined: + traces = null; + } + + crawl( + aobj, + function(path, attrName, attr) { + var thisTraces; + if (Array.isArray(attr)) { + var nAttr = Math.min(attr.length, gd.data.length); + if (traces) { + nAttr = Math.min(nAttr, traces.length); } - - // Convert [7] to just 7 when traces is null: - if(thisTraces === null) { - if(Array.isArray(attr)) { - attr = attr[0]; - } - } else if(Array.isArray(thisTraces)) { - if(!Array.isArray(attr)) { - var tmp = attr; - attr = []; - for(var i = 0; i < thisTraces.length; i++) { - attr[i] = tmp; - } - } - attr.length = Math.min(thisTraces.length, attr.length); + thisTraces = []; + for (var j = 0; j < nAttr; j++) { + thisTraces[j] = traces ? traces[j] : j; } - - bindings.push({ - type: 'data', - prop: path, - traces: thisTraces, - value: attr - }); - }, '', 0); - - return bindings; + } else { + thisTraces = traces ? traces.slice(0) : null; + } + + // Convert [7] to just 7 when traces is null: + if (thisTraces === null) { + if (Array.isArray(attr)) { + attr = attr[0]; + } + } else if (Array.isArray(thisTraces)) { + if (!Array.isArray(attr)) { + var tmp = attr; + attr = []; + for (var i = 0; i < thisTraces.length; i++) { + attr[i] = tmp; + } + } + attr.length = Math.min(thisTraces.length, attr.length); + } + + bindings.push({ + type: "data", + prop: path, + traces: thisTraces, + value: attr + }); + }, + "", + 0 + ); + + return bindings; } function crawl(attrs, callback, path, depth) { - Object.keys(attrs).forEach(function(attrName) { - var attr = attrs[attrName]; + Object.keys(attrs).forEach(function(attrName) { + var attr = attrs[attrName]; - if(attrName[0] === '_') return; + if (attrName[0] === "_") return; - var thisPath = path + (depth > 0 ? '.' : '') + attrName; + var thisPath = path + (depth > 0 ? "." : "") + attrName; - if(Lib.isPlainObject(attr)) { - crawl(attr, callback, thisPath, depth + 1); - } else { - // Only execute the callback on leaf nodes: - callback(thisPath, attrName, attr); - } - }); + if (Lib.isPlainObject(attr)) { + crawl(attr, callback, thisPath, depth + 1); + } else { + // Only execute the callback on leaf nodes: + callback(thisPath, attrName, attr); + } + }); } diff --git a/src/plots/font_attributes.js b/src/plots/font_attributes.js index daf2490c563..ec52e1423ea 100644 --- a/src/plots/font_attributes.js +++ b/src/plots/font_attributes.js @@ -5,36 +5,26 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - family: { - valType: 'string', - role: 'style', - noBlank: true, - strict: true, - description: [ - 'HTML font family - the typeface that will be applied by the web browser.', - 'The web browser will only be able to apply a font if it is available on the system', - 'which it operates. Provide multiple font families, separated by commas, to indicate', - 'the preference in which to apply fonts if they aren\'t available on the system.', - 'The plotly service (at https://plot.ly or on-premise) generates images on a server,', - 'where only a select number of', - 'fonts are installed and supported.', - 'These include *Arial*, *Balto*, *Courier New*, *Droid Sans*,, *Droid Serif*,', - '*Droid Sans Mono*, *Gravitas One*, *Old Standard TT*, *Open Sans*, *Overpass*,', - '*PT Sans Narrow*, *Raleway*, *Times New Roman*.' - ].join(' ') - }, - size: { - valType: 'number', - role: 'style', - min: 1 - }, - color: { - valType: 'color', - role: 'style' - } + family: { + valType: "string", + role: "style", + noBlank: true, + strict: true, + description: [ + "HTML font family - the typeface that will be applied by the web browser.", + "The web browser will only be able to apply a font if it is available on the system", + "which it operates. Provide multiple font families, separated by commas, to indicate", + "the preference in which to apply fonts if they aren't available on the system.", + "The plotly service (at https://plot.ly or on-premise) generates images on a server,", + "where only a select number of", + "fonts are installed and supported.", + "These include *Arial*, *Balto*, *Courier New*, *Droid Sans*,, *Droid Serif*,", + "*Droid Sans Mono*, *Gravitas One*, *Old Standard TT*, *Open Sans*, *Overpass*,", + "*PT Sans Narrow*, *Raleway*, *Times New Roman*." + ].join(" ") + }, + size: { valType: "number", role: "style", min: 1 }, + color: { valType: "color", role: "style" } }; diff --git a/src/plots/frame_attributes.js b/src/plots/frame_attributes.js index cfb57e1b689..f38c6183417 100644 --- a/src/plots/frame_attributes.js +++ b/src/plots/frame_attributes.js @@ -5,56 +5,53 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = { - _isLinkedToArray: 'frames_entry', - - group: { - valType: 'string', - role: 'info', - description: [ - 'An identifier that specifies the group to which the frame belongs,', - 'used by animate to select a subset of frames.' - ].join(' ') - }, - name: { - valType: 'string', - role: 'info', - description: 'A label by which to identify the frame' - }, - traces: { - valType: 'any', - role: 'info', - description: [ - 'A list of trace indices that identify the respective traces in the', - 'data attribute' - ].join(' ') - }, - baseframe: { - valType: 'string', - role: 'info', - description: [ - 'The name of the frame into which this frame\'s properties are merged', - 'before applying. This is used to unify properties and avoid needing', - 'to specify the same values for the same properties in multiple frames.' - ].join(' ') - }, - data: { - valType: 'any', - role: 'object', - description: [ - 'A list of traces this frame modifies. The format is identical to the', - 'normal trace definition.' - ].join(' ') - }, - layout: { - valType: 'any', - role: 'object', - description: [ - 'Layout properties which this frame modifies. The format is identical', - 'to the normal layout definition.' - ].join(' ') - } + _isLinkedToArray: "frames_entry", + group: { + valType: "string", + role: "info", + description: [ + "An identifier that specifies the group to which the frame belongs,", + "used by animate to select a subset of frames." + ].join(" ") + }, + name: { + valType: "string", + role: "info", + description: "A label by which to identify the frame" + }, + traces: { + valType: "any", + role: "info", + description: [ + "A list of trace indices that identify the respective traces in the", + "data attribute" + ].join(" ") + }, + baseframe: { + valType: "string", + role: "info", + description: [ + "The name of the frame into which this frame's properties are merged", + "before applying. This is used to unify properties and avoid needing", + "to specify the same values for the same properties in multiple frames." + ].join(" ") + }, + data: { + valType: "any", + role: "object", + description: [ + "A list of traces this frame modifies. The format is identical to the", + "normal trace definition." + ].join(" ") + }, + layout: { + valType: "any", + role: "object", + description: [ + "Layout properties which this frame modifies. The format is identical", + "to the normal layout definition." + ].join(" ") + } }; diff --git a/src/plots/geo/constants.js b/src/plots/geo/constants.js index 92564b4e485..10410383b78 100644 --- a/src/plots/geo/constants.js +++ b/src/plots/geo/constants.js @@ -5,104 +5,97 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var params = module.exports = {}; // projection names to d3 function name params.projNames = { - // d3.geo.projection - 'equirectangular': 'equirectangular', - 'mercator': 'mercator', - 'orthographic': 'orthographic', - 'natural earth': 'naturalEarth', - 'kavrayskiy7': 'kavrayskiy7', - 'miller': 'miller', - 'robinson': 'robinson', - 'eckert4': 'eckert4', - 'azimuthal equal area': 'azimuthalEqualArea', - 'azimuthal equidistant': 'azimuthalEquidistant', - 'conic equal area': 'conicEqualArea', - 'conic conformal': 'conicConformal', - 'conic equidistant': 'conicEquidistant', - 'gnomonic': 'gnomonic', - 'stereographic': 'stereographic', - 'mollweide': 'mollweide', - 'hammer': 'hammer', - 'transverse mercator': 'transverseMercator', - 'albers usa': 'albersUsa', - 'winkel tripel': 'winkel3' + // d3.geo.projection + equirectangular: "equirectangular", + mercator: "mercator", + orthographic: "orthographic", + "natural earth": "naturalEarth", + kavrayskiy7: "kavrayskiy7", + miller: "miller", + robinson: "robinson", + eckert4: "eckert4", + "azimuthal equal area": "azimuthalEqualArea", + "azimuthal equidistant": "azimuthalEquidistant", + "conic equal area": "conicEqualArea", + "conic conformal": "conicConformal", + "conic equidistant": "conicEquidistant", + gnomonic: "gnomonic", + stereographic: "stereographic", + mollweide: "mollweide", + hammer: "hammer", + "transverse mercator": "transverseMercator", + "albers usa": "albersUsa", + "winkel tripel": "winkel3" }; // name of the axes -params.axesNames = ['lonaxis', 'lataxis']; +params.axesNames = ["lonaxis", "lataxis"]; // max longitudinal angular span (EXPERIMENTAL) params.lonaxisSpan = { - 'orthographic': 180, - 'azimuthal equal area': 360, - 'azimuthal equidistant': 360, - 'conic conformal': 180, - 'gnomonic': 160, - 'stereographic': 180, - 'transverse mercator': 180, - '*': 360 + orthographic: 180, + "azimuthal equal area": 360, + "azimuthal equidistant": 360, + "conic conformal": 180, + gnomonic: 160, + stereographic: 180, + "transverse mercator": 180, + "*": 360 }; // max latitudinal angular span (EXPERIMENTAL) -params.lataxisSpan = { - 'conic conformal': 150, - 'stereographic': 179.5, - '*': 180 -}; +params.lataxisSpan = { "conic conformal": 150, stereographic: 179.5, "*": 180 }; // defaults for each scope params.scopeDefaults = { - world: { - lonaxisRange: [-180, 180], - lataxisRange: [-90, 90], - projType: 'equirectangular', - projRotate: [0, 0, 0] - }, - usa: { - lonaxisRange: [-180, -50], - lataxisRange: [15, 80], - projType: 'albers usa' - }, - europe: { - lonaxisRange: [-30, 60], - lataxisRange: [30, 80], - projType: 'conic conformal', - projRotate: [15, 0, 0], - projParallels: [0, 60] - }, - asia: { - lonaxisRange: [22, 160], - lataxisRange: [-15, 55], - projType: 'mercator', - projRotate: [0, 0, 0] - }, - africa: { - lonaxisRange: [-30, 60], - lataxisRange: [-40, 40], - projType: 'mercator', - projRotate: [0, 0, 0] - }, - 'north america': { - lonaxisRange: [-180, -45], - lataxisRange: [5, 85], - projType: 'conic conformal', - projRotate: [-100, 0, 0], - projParallels: [29.5, 45.5] - }, - 'south america': { - lonaxisRange: [-100, -30], - lataxisRange: [-60, 15], - projType: 'mercator', - projRotate: [0, 0, 0] - } + world: { + lonaxisRange: [-180, 180], + lataxisRange: [-90, 90], + projType: "equirectangular", + projRotate: [0, 0, 0] + }, + usa: { + lonaxisRange: [-180, -50], + lataxisRange: [15, 80], + projType: "albers usa" + }, + europe: { + lonaxisRange: [-30, 60], + lataxisRange: [30, 80], + projType: "conic conformal", + projRotate: [15, 0, 0], + projParallels: [0, 60] + }, + asia: { + lonaxisRange: [22, 160], + lataxisRange: [-15, 55], + projType: "mercator", + projRotate: [0, 0, 0] + }, + africa: { + lonaxisRange: [-30, 60], + lataxisRange: [-40, 40], + projType: "mercator", + projRotate: [0, 0, 0] + }, + "north america": { + lonaxisRange: [-180, -45], + lataxisRange: [5, 85], + projType: "conic conformal", + projRotate: [-100, 0, 0], + projParallels: [29.5, 45.5] + }, + "south america": { + lonaxisRange: [-100, -30], + lataxisRange: [-60, 15], + projType: "mercator", + projRotate: [0, 0, 0] + } }; // angular pad to avoid rounding error around clip angles @@ -112,45 +105,50 @@ params.clipPad = 1e-3; params.precision = 0.1; // default land and water fill colors -params.landColor = '#F0DC82'; -params.waterColor = '#3399FF'; +params.landColor = "#F0DC82"; +params.waterColor = "#3399FF"; // locationmode to layer name params.locationmodeToLayer = { - 'ISO-3': 'countries', - 'USA-states': 'subunits', - 'country names': 'countries' + "ISO-3": "countries", + "USA-states": "subunits", + "country names": "countries" }; // SVG element for a sphere (use to frame maps) -params.sphereSVG = {type: 'Sphere'}; +params.sphereSVG = { type: "Sphere" }; // N.B. base layer names must be the same as in the topojson files - // base layer with a fill color -params.fillLayers = ['ocean', 'land', 'lakes']; +params.fillLayers = ["ocean", "land", "lakes"]; // base layer with a only a line color -params.lineLayers = ['subunits', 'countries', 'coastlines', 'rivers', 'frame']; +params.lineLayers = ["subunits", "countries", "coastlines", "rivers", "frame"]; // all base layers - in order params.baseLayers = [ - 'ocean', 'land', 'lakes', - 'subunits', 'countries', 'coastlines', 'rivers', - 'lataxis', 'lonaxis', - 'frame' + "ocean", + "land", + "lakes", + "subunits", + "countries", + "coastlines", + "rivers", + "lataxis", + "lonaxis", + "frame" ]; params.layerNameToAdjective = { - ocean: 'ocean', - land: 'land', - lakes: 'lake', - subunits: 'subunit', - countries: 'country', - coastlines: 'coastline', - rivers: 'river', - frame: 'frame' + ocean: "ocean", + land: "land", + lakes: "lake", + subunits: "subunit", + countries: "country", + coastlines: "coastline", + rivers: "river", + frame: "frame" }; // base layers drawn over choropleth -params.baseLayersOverChoropleth = ['rivers', 'lakes']; +params.baseLayersOverChoropleth = ["rivers", "lakes"]; diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 75bd8e37261..b613e3754c3 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -5,62 +5,58 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; /* global PlotlyGeoAssets:false */ -var d3 = require('d3'); +var d3 = require("d3"); -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); -var Plots = require('../plots'); -var Axes = require('../cartesian/axes'); -var Fx = require('../cartesian/graph_interact'); +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); +var Plots = require("../plots"); +var Axes = require("../cartesian/axes"); +var Fx = require("../cartesian/graph_interact"); -var addProjectionsToD3 = require('./projections'); -var createGeoScale = require('./set_scale'); -var createGeoZoom = require('./zoom'); -var createGeoZoomReset = require('./zoom_reset'); -var constants = require('./constants'); +var addProjectionsToD3 = require("./projections"); +var createGeoScale = require("./set_scale"); +var createGeoZoom = require("./zoom"); +var createGeoZoomReset = require("./zoom_reset"); +var constants = require("./constants"); -var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); -var topojsonUtils = require('../../lib/topojson_utils'); -var topojsonFeature = require('topojson-client').feature; +var xmlnsNamespaces = require("../../constants/xmlns_namespaces"); +var topojsonUtils = require("../../lib/topojson_utils"); +var topojsonFeature = require("topojson-client").feature; // add a few projection types to d3.geo addProjectionsToD3(d3); - function Geo(options, fullLayout) { - this.id = options.id; - this.graphDiv = options.graphDiv; - this.container = options.container; - this.topojsonURL = options.topojsonURL; + this.id = options.id; + this.graphDiv = options.graphDiv; + this.container = options.container; + this.topojsonURL = options.topojsonURL; - this.hoverContainer = null; + this.hoverContainer = null; - this.topojsonName = null; - this.topojson = null; + this.topojsonName = null; + this.topojson = null; - this.projectionType = null; - this.projection = null; + this.projectionType = null; + this.projection = null; - this.clipAngle = null; - this.setScale = null; - this.path = null; + this.clipAngle = null; + this.setScale = null; + this.path = null; - this.zoom = null; - this.zoomReset = null; + this.zoom = null; + this.zoomReset = null; - this.xaxis = null; - this.yaxis = null; + this.xaxis = null; + this.yaxis = null; - this.makeFramework(); - this.updateFx(fullLayout.hovermode); + this.makeFramework(); + this.updateFx(fullLayout.hovermode); - this.traceHash = {}; + this.traceHash = {}; } module.exports = Geo; @@ -68,168 +64,166 @@ module.exports = Geo; var proto = Geo.prototype; proto.plot = function(geoCalcData, fullLayout, promises) { - var _this = this, - geoLayout = fullLayout[_this.id], - graphSize = fullLayout._size; - - var topojsonNameNew, topojsonPath; - - // N.B. 'geoLayout' is unambiguous, no need for 'user' geo layout here - - // TODO don't reset projection on all graph edits - _this.projection = null; - - _this.setScale = createGeoScale(geoLayout, graphSize); - _this.makeProjection(geoLayout); - _this.makePath(); - _this.adjustLayout(geoLayout, graphSize); - - _this.zoom = createGeoZoom(_this, geoLayout); - _this.zoomReset = createGeoZoomReset(_this, geoLayout); - _this.mockAxis = createMockAxis(fullLayout); - - _this.framework - .call(_this.zoom) - .on('dblclick.zoom', _this.zoomReset); - - _this.framework.on('mousemove', function() { - var mouse = d3.mouse(this), - lonlat = _this.projection.invert(mouse); - - if(isNaN(lonlat[0]) || isNaN(lonlat[1])) return; - - var evt = { - target: true, - xpx: mouse[0], - ypx: mouse[1] - }; - - _this.xaxis.c2p = function() { return mouse[0]; }; - _this.xaxis.p2c = function() { return lonlat[0]; }; - _this.yaxis.c2p = function() { return mouse[1]; }; - _this.yaxis.p2c = function() { return lonlat[1]; }; - - Fx.hover(_this.graphDiv, evt, _this.id); - }); - - _this.framework.on('mouseout', function() { - Fx.loneUnhover(fullLayout._toppaper); - }); - - _this.framework.on('click', function() { - Fx.click(_this.graphDiv, { target: true }); - }); - - topojsonNameNew = topojsonUtils.getTopojsonName(geoLayout); - - if(_this.topojson === null || topojsonNameNew !== _this.topojsonName) { - _this.topojsonName = topojsonNameNew; - - if(PlotlyGeoAssets.topojson[_this.topojsonName] !== undefined) { - _this.topojson = PlotlyGeoAssets.topojson[_this.topojsonName]; - _this.onceTopojsonIsLoaded(geoCalcData, geoLayout); - } - else { - topojsonPath = topojsonUtils.getTopojsonPath( - _this.topojsonURL, - _this.topojsonName - ); - - promises.push(new Promise(function(resolve, reject) { - d3.json(topojsonPath, function(error, topojson) { - if(error) { - if(error.status === 404) { - reject(new Error([ - 'plotly.js could not find topojson file at', - topojsonPath, '.', - 'Make sure the *topojsonURL* plot config option', - 'is set properly.' - ].join(' '))); - } - else { - reject(new Error([ - 'unexpected error while fetching topojson file at', - topojsonPath - ].join(' '))); - } - return; - } - - _this.topojson = topojson; - PlotlyGeoAssets.topojson[_this.topojsonName] = topojson; - - _this.onceTopojsonIsLoaded(geoCalcData, geoLayout); - resolve(); - }); - })); - } - } - else _this.onceTopojsonIsLoaded(geoCalcData, geoLayout); + var _this = this, + geoLayout = fullLayout[_this.id], + graphSize = fullLayout._size; + + var topojsonNameNew, topojsonPath; + + // N.B. 'geoLayout' is unambiguous, no need for 'user' geo layout here + // TODO don't reset projection on all graph edits + _this.projection = null; + + _this.setScale = createGeoScale(geoLayout, graphSize); + _this.makeProjection(geoLayout); + _this.makePath(); + _this.adjustLayout(geoLayout, graphSize); + + _this.zoom = createGeoZoom(_this, geoLayout); + _this.zoomReset = createGeoZoomReset(_this, geoLayout); + _this.mockAxis = createMockAxis(fullLayout); + + _this.framework.call(_this.zoom).on("dblclick.zoom", _this.zoomReset); + + _this.framework.on("mousemove", function() { + var mouse = d3.mouse(this), lonlat = _this.projection.invert(mouse); + + if (isNaN(lonlat[0]) || isNaN(lonlat[1])) return; + + var evt = { target: true, xpx: mouse[0], ypx: mouse[1] }; - // TODO handle topojson-is-loading case - // to avoid making multiple request while streaming + _this.xaxis.c2p = function() { + return mouse[0]; + }; + _this.xaxis.p2c = function() { + return lonlat[0]; + }; + _this.yaxis.c2p = function() { + return mouse[1]; + }; + _this.yaxis.p2c = function() { + return lonlat[1]; + }; + + Fx.hover(_this.graphDiv, evt, _this.id); + }); + + _this.framework.on("mouseout", function() { + Fx.loneUnhover(fullLayout._toppaper); + }); + + _this.framework.on("click", function() { + Fx.click(_this.graphDiv, { target: true }); + }); + + topojsonNameNew = topojsonUtils.getTopojsonName(geoLayout); + + if (_this.topojson === null || topojsonNameNew !== _this.topojsonName) { + _this.topojsonName = topojsonNameNew; + + if (PlotlyGeoAssets.topojson[_this.topojsonName] !== undefined) { + _this.topojson = PlotlyGeoAssets.topojson[_this.topojsonName]; + _this.onceTopojsonIsLoaded(geoCalcData, geoLayout); + } else { + topojsonPath = topojsonUtils.getTopojsonPath( + _this.topojsonURL, + _this.topojsonName + ); + + promises.push(new Promise(function(resolve, reject) { + d3.json(topojsonPath, function(error, topojson) { + if (error) { + if (error.status === 404) { + reject(new Error( + [ + "plotly.js could not find topojson file at", + topojsonPath, + ".", + "Make sure the *topojsonURL* plot config option", + "is set properly." + ].join(" ") + )); + } else { + reject(new Error( + [ + "unexpected error while fetching topojson file at", + topojsonPath + ].join(" ") + )); + } + return; + } + + _this.topojson = topojson; + PlotlyGeoAssets.topojson[_this.topojsonName] = topojson; + + _this.onceTopojsonIsLoaded(geoCalcData, geoLayout); + resolve(); + }); + })); + } + } else { + _this.onceTopojsonIsLoaded(geoCalcData, geoLayout); + } + // TODO handle topojson-is-loading case + // to avoid making multiple request while streaming }; proto.onceTopojsonIsLoaded = function(geoCalcData, geoLayout) { - this.drawLayout(geoLayout); + this.drawLayout(geoLayout); - Plots.generalUpdatePerTraceModule(this, geoCalcData, geoLayout); + Plots.generalUpdatePerTraceModule(this, geoCalcData, geoLayout); - this.render(); + this.render(); }; proto.updateFx = function(hovermode) { - this.showHover = (hovermode !== false); - - // TODO should more strict, any layout.hovermode other - // then false will make all geo subplot display hover text. - // Instead each geo should have its own geo.hovermode - // to control hover visibility independently of other subplots. + this.showHover = hovermode !== false; + // TODO should more strict, any layout.hovermode other + // then false will make all geo subplot display hover text. + // Instead each geo should have its own geo.hovermode + // to control hover visibility independently of other subplots. }; proto.makeProjection = function(geoLayout) { - var projLayout = geoLayout.projection, - projType = projLayout.type, - isNew = this.projection === null || projType !== this.projectionType, - projection; - - if(isNew) { - this.projectionType = projType; - projection = this.projection = d3.geo[constants.projNames[projType]](); - } - else projection = this.projection; - - projection - .translate(projLayout._translate0) - .precision(constants.precision); - - if(!geoLayout._isAlbersUsa) { - projection - .rotate(projLayout._rotate) - .center(projLayout._center); - } - - if(geoLayout._clipAngle) { - this.clipAngle = geoLayout._clipAngle; // needed in proto.render - projection - .clipAngle(geoLayout._clipAngle - constants.clipPad); - } - else this.clipAngle = null; // for graph edits + var projLayout = geoLayout.projection, + projType = projLayout.type, + isNew = this.projection === null || projType !== this.projectionType, + projection; + + if (isNew) { + this.projectionType = projType; + projection = this.projection = d3.geo[constants.projNames[projType]](); + } else { + projection = this.projection; + } + + projection.translate(projLayout._translate0).precision(constants.precision); + + if (!geoLayout._isAlbersUsa) { + projection.rotate(projLayout._rotate).center(projLayout._center); + } + + if (geoLayout._clipAngle) { + this.clipAngle = geoLayout._clipAngle; + // needed in proto.render + projection.clipAngle(geoLayout._clipAngle - constants.clipPad); + } else { + this.clipAngle = null; + } - if(projLayout.parallels) { - projection - .parallels(projLayout.parallels); - } + // for graph edits + if (projLayout.parallels) { + projection.parallels(projLayout.parallels); + } - if(isNew) this.setScale(projection); + if (isNew) this.setScale(projection); - projection - .translate(projLayout._translate) - .scale(projLayout._scale); + projection.translate(projLayout._translate).scale(projLayout._scale); }; proto.makePath = function() { - this.path = d3.geo.path().projection(this.projection); + this.path = d3.geo.path().projection(this.projection); }; /* @@ -239,270 +233,258 @@ proto.makePath = function() { * */ proto.makeFramework = function() { - var geoDiv = this.geoDiv = d3.select(this.container).append('div'); - geoDiv - .attr('id', this.id) - .style('position', 'absolute'); - - // only choropleth traces use this, - // scattergeo traces use Fx.hover and fullLayout._hoverlayer - var hoverContainer = this.hoverContainer = geoDiv.append('svg'); - hoverContainer - .attr(xmlnsNamespaces.svgAttrs) - .style({ - 'position': 'absolute', - 'z-index': 20, - 'pointer-events': 'none' - }); + var geoDiv = this.geoDiv = d3.select(this.container).append("div"); + geoDiv.attr("id", this.id).style("position", "absolute"); + + // only choropleth traces use this, + // scattergeo traces use Fx.hover and fullLayout._hoverlayer + var hoverContainer = this.hoverContainer = geoDiv.append("svg"); + hoverContainer + .attr(xmlnsNamespaces.svgAttrs) + .style({ position: "absolute", "z-index": 20, "pointer-events": "none" }); + + var framework = this.framework = geoDiv.append("svg"); + framework + .attr(xmlnsNamespaces.svgAttrs) + .attr({ position: "absolute", preserveAspectRatio: "none" }); + + framework.append("g").attr("class", "bglayer").append("rect"); + + framework.append("g").attr("class", "baselayer"); + framework.append("g").attr("class", "choroplethlayer"); + framework.append("g").attr("class", "baselayeroverchoropleth"); + framework.append("g").attr("class", "scattergeolayer"); + + // N.B. disable dblclick zoom default + framework.on("dblclick.zoom", null); + + // TODO use clip paths instead of nested SVG + this.xaxis = { _id: "x" }; + this.yaxis = { _id: "y" }; +}; - var framework = this.framework = geoDiv.append('svg'); - framework - .attr(xmlnsNamespaces.svgAttrs) - .attr({ - 'position': 'absolute', - 'preserveAspectRatio': 'none' - }); +proto.adjustLayout = function(geoLayout, graphSize) { + var domain = geoLayout.domain; - framework.append('g').attr('class', 'bglayer') - .append('rect'); + var left = graphSize.l + graphSize.w * domain.x[0] + geoLayout._marginX, + top = graphSize.t + graphSize.h * (1 - domain.y[1]) + geoLayout._marginY; - framework.append('g').attr('class', 'baselayer'); - framework.append('g').attr('class', 'choroplethlayer'); - framework.append('g').attr('class', 'baselayeroverchoropleth'); - framework.append('g').attr('class', 'scattergeolayer'); + this.geoDiv.style({ + left: left + "px", + top: top + "px", + width: geoLayout._width + "px", + height: geoLayout._height + "px" + }); - // N.B. disable dblclick zoom default - framework.on('dblclick.zoom', null); + this.hoverContainer.attr({ + width: geoLayout._width, + height: geoLayout._height + }); - // TODO use clip paths instead of nested SVG + this.framework.attr({ width: geoLayout._width, height: geoLayout._height }); - this.xaxis = { _id: 'x' }; - this.yaxis = { _id: 'y' }; -}; + this.framework + .select(".bglayer") + .select("rect") + .attr({ width: geoLayout._width, height: geoLayout._height }) + .call(Color.fill, geoLayout.bgcolor); -proto.adjustLayout = function(geoLayout, graphSize) { - var domain = geoLayout.domain; - - var left = graphSize.l + graphSize.w * domain.x[0] + geoLayout._marginX, - top = graphSize.t + graphSize.h * (1 - domain.y[1]) + geoLayout._marginY; - - this.geoDiv.style({ - left: left + 'px', - top: top + 'px', - width: geoLayout._width + 'px', - height: geoLayout._height + 'px' - }); - - this.hoverContainer.attr({ - width: geoLayout._width, - height: geoLayout._height - }); - - this.framework.attr({ - width: geoLayout._width, - height: geoLayout._height - }); - - this.framework.select('.bglayer').select('rect') - .attr({ - width: geoLayout._width, - height: geoLayout._height - }) - .call(Color.fill, geoLayout.bgcolor); - - this.xaxis._offset = left; - this.xaxis._length = geoLayout._width; - - this.yaxis._offset = top; - this.yaxis._length = geoLayout._height; + this.xaxis._offset = left; + this.xaxis._length = geoLayout._width; + + this.yaxis._offset = top; + this.yaxis._length = geoLayout._height; }; proto.drawTopo = function(selection, layerName, geoLayout) { - if(geoLayout['show' + layerName] !== true) return; - - var topojson = this.topojson, - datum = layerName === 'frame' ? - constants.sphereSVG : - topojsonFeature(topojson, topojson.objects[layerName]); - - selection.append('g') - .datum(datum) - .attr('class', layerName) - .append('path') - .attr('class', 'basepath'); + if (geoLayout["show" + layerName] !== true) return; + + var topojson = this.topojson, + datum = layerName === "frame" + ? constants.sphereSVG + : topojsonFeature(topojson, topojson.objects[layerName]); + + selection + .append("g") + .datum(datum) + .attr("class", layerName) + .append("path") + .attr("class", "basepath"); }; function makeGraticule(lonaxisRange, lataxisRange, step) { - return d3.geo.graticule() - .extent([ - [lonaxisRange[0], lataxisRange[0]], - [lonaxisRange[1], lataxisRange[1]] - ]) - .step(step); + return d3.geo + .graticule() + .extent([ + [lonaxisRange[0], lataxisRange[0]], + [lonaxisRange[1], lataxisRange[1]] + ]) + .step(step); } proto.drawGraticule = function(selection, axisName, geoLayout) { - var axisLayout = geoLayout[axisName]; - - if(axisLayout.showgrid !== true) return; - - var scopeDefaults = constants.scopeDefaults[geoLayout.scope], - lonaxisRange = scopeDefaults.lonaxisRange, - lataxisRange = scopeDefaults.lataxisRange, - step = axisName === 'lonaxis' ? - [axisLayout.dtick] : - [0, axisLayout.dtick], - graticule = makeGraticule(lonaxisRange, lataxisRange, step); - - selection.append('g') - .datum(graticule) - .attr('class', axisName + 'graticule') - .append('path') - .attr('class', 'graticulepath'); + var axisLayout = geoLayout[axisName]; + + if (axisLayout.showgrid !== true) return; + + var scopeDefaults = constants.scopeDefaults[geoLayout.scope], + lonaxisRange = scopeDefaults.lonaxisRange, + lataxisRange = scopeDefaults.lataxisRange, + step = axisName === "lonaxis" ? [axisLayout.dtick] : [0, axisLayout.dtick], + graticule = makeGraticule(lonaxisRange, lataxisRange, step); + + selection + .append("g") + .datum(graticule) + .attr("class", axisName + "graticule") + .append("path") + .attr("class", "graticulepath"); }; proto.drawLayout = function(geoLayout) { - var gBaseLayer = this.framework.select('g.baselayer'), - baseLayers = constants.baseLayers, - axesNames = constants.axesNames, - layerName; - - // TODO move to more d3-idiomatic pattern (that's work on replot) - // N.B. html('') does not work in IE11 - gBaseLayer.selectAll('*').remove(); - - for(var i = 0; i < baseLayers.length; i++) { - layerName = baseLayers[i]; - - if(axesNames.indexOf(layerName) !== -1) { - this.drawGraticule(gBaseLayer, layerName, geoLayout); - } - else this.drawTopo(gBaseLayer, layerName, geoLayout); + var gBaseLayer = this.framework.select("g.baselayer"), + baseLayers = constants.baseLayers, + axesNames = constants.axesNames, + layerName; + + // TODO move to more d3-idiomatic pattern (that's work on replot) + // N.B. html('') does not work in IE11 + gBaseLayer.selectAll("*").remove(); + + for (var i = 0; i < baseLayers.length; i++) { + layerName = baseLayers[i]; + + if (axesNames.indexOf(layerName) !== -1) { + this.drawGraticule(gBaseLayer, layerName, geoLayout); + } else { + this.drawTopo(gBaseLayer, layerName, geoLayout); } + } - this.styleLayout(geoLayout); + this.styleLayout(geoLayout); }; function styleFillLayer(selection, layerName, geoLayout) { - var layerAdj = constants.layerNameToAdjective[layerName]; + var layerAdj = constants.layerNameToAdjective[layerName]; - selection.select('.' + layerName) - .selectAll('path') - .attr('stroke', 'none') - .call(Color.fill, geoLayout[layerAdj + 'color']); + selection + .select("." + layerName) + .selectAll("path") + .attr("stroke", "none") + .call(Color.fill, geoLayout[layerAdj + "color"]); } function styleLineLayer(selection, layerName, geoLayout) { - var layerAdj = constants.layerNameToAdjective[layerName]; - - selection.select('.' + layerName) - .selectAll('path') - .attr('fill', 'none') - .call(Color.stroke, geoLayout[layerAdj + 'color']) - .call(Drawing.dashLine, '', geoLayout[layerAdj + 'width']); + var layerAdj = constants.layerNameToAdjective[layerName]; + + selection + .select("." + layerName) + .selectAll("path") + .attr("fill", "none") + .call(Color.stroke, geoLayout[layerAdj + "color"]) + .call(Drawing.dashLine, "", geoLayout[layerAdj + "width"]); } function styleGraticule(selection, axisName, geoLayout) { - selection.select('.' + axisName + 'graticule') - .selectAll('path') - .attr('fill', 'none') - .call(Color.stroke, geoLayout[axisName].gridcolor) - .call(Drawing.dashLine, '', geoLayout[axisName].gridwidth); + selection + .select("." + axisName + "graticule") + .selectAll("path") + .attr("fill", "none") + .call(Color.stroke, geoLayout[axisName].gridcolor) + .call(Drawing.dashLine, "", geoLayout[axisName].gridwidth); } proto.styleLayer = function(selection, layerName, geoLayout) { - var fillLayers = constants.fillLayers, - lineLayers = constants.lineLayers; + var fillLayers = constants.fillLayers, lineLayers = constants.lineLayers; - if(fillLayers.indexOf(layerName) !== -1) { - styleFillLayer(selection, layerName, geoLayout); - } - else if(lineLayers.indexOf(layerName) !== -1) { - styleLineLayer(selection, layerName, geoLayout); - } + if (fillLayers.indexOf(layerName) !== -1) { + styleFillLayer(selection, layerName, geoLayout); + } else if (lineLayers.indexOf(layerName) !== -1) { + styleLineLayer(selection, layerName, geoLayout); + } }; proto.styleLayout = function(geoLayout) { - var gBaseLayer = this.framework.select('g.baselayer'), - baseLayers = constants.baseLayers, - axesNames = constants.axesNames, - layerName; - - for(var i = 0; i < baseLayers.length; i++) { - layerName = baseLayers[i]; - - if(axesNames.indexOf(layerName) !== -1) { - styleGraticule(gBaseLayer, layerName, geoLayout); - } - else this.styleLayer(gBaseLayer, layerName, geoLayout); + var gBaseLayer = this.framework.select("g.baselayer"), + baseLayers = constants.baseLayers, + axesNames = constants.axesNames, + layerName; + + for (var i = 0; i < baseLayers.length; i++) { + layerName = baseLayers[i]; + + if (axesNames.indexOf(layerName) !== -1) { + styleGraticule(gBaseLayer, layerName, geoLayout); + } else { + this.styleLayer(gBaseLayer, layerName, geoLayout); } + } }; proto.isLonLatOverEdges = function(lonlat) { - var clipAngle = this.clipAngle; + var clipAngle = this.clipAngle; - if(clipAngle === null) return false; + if (clipAngle === null) return false; - var p = this.projection.rotate(), - angle = d3.geo.distance(lonlat, [-p[0], -p[1]]), - maxAngle = clipAngle * Math.PI / 180; + var p = this.projection.rotate(), + angle = d3.geo.distance(lonlat, [-p[0], -p[1]]), + maxAngle = clipAngle * Math.PI / 180; - return angle > maxAngle; + return angle > maxAngle; }; // [hot code path] (re)draw all paths which depend on the projection proto.render = function() { - var _this = this, - framework = _this.framework, - gChoropleth = framework.select('g.choroplethlayer'), - gScatterGeo = framework.select('g.scattergeolayer'), - path = _this.path; - - function translatePoints(d) { - var lonlatPx = _this.projection(d.lonlat); - if(!lonlatPx) return null; - - return 'translate(' + lonlatPx[0] + ',' + lonlatPx[1] + ')'; - } - - // hide paths over edges of clipped projections - function hideShowPoints(d) { - return _this.isLonLatOverEdges(d.lonlat) ? '0' : '1.0'; - } - - framework.selectAll('path.basepath').attr('d', path); - framework.selectAll('path.graticulepath').attr('d', path); - - gChoropleth.selectAll('path.choroplethlocation').attr('d', path); - gChoropleth.selectAll('path.basepath').attr('d', path); - - gScatterGeo.selectAll('path.js-line').attr('d', path); - - if(_this.clipAngle !== null) { - gScatterGeo.selectAll('path.point') - .style('opacity', hideShowPoints) - .attr('transform', translatePoints); - gScatterGeo.selectAll('text') - .style('opacity', hideShowPoints) - .attr('transform', translatePoints); - } - else { - gScatterGeo.selectAll('path.point') - .attr('transform', translatePoints); - gScatterGeo.selectAll('text') - .attr('transform', translatePoints); - } + var _this = this, + framework = _this.framework, + gChoropleth = framework.select("g.choroplethlayer"), + gScatterGeo = framework.select("g.scattergeolayer"), + path = _this.path; + + function translatePoints(d) { + var lonlatPx = _this.projection(d.lonlat); + if (!lonlatPx) return null; + + return "translate(" + lonlatPx[0] + "," + lonlatPx[1] + ")"; + } + + // hide paths over edges of clipped projections + function hideShowPoints(d) { + return _this.isLonLatOverEdges(d.lonlat) ? "0" : "1.0"; + } + + framework.selectAll("path.basepath").attr("d", path); + framework.selectAll("path.graticulepath").attr("d", path); + + gChoropleth.selectAll("path.choroplethlocation").attr("d", path); + gChoropleth.selectAll("path.basepath").attr("d", path); + + gScatterGeo.selectAll("path.js-line").attr("d", path); + + if (_this.clipAngle !== null) { + gScatterGeo + .selectAll("path.point") + .style("opacity", hideShowPoints) + .attr("transform", translatePoints); + gScatterGeo + .selectAll("text") + .style("opacity", hideShowPoints) + .attr("transform", translatePoints); + } else { + gScatterGeo.selectAll("path.point").attr("transform", translatePoints); + gScatterGeo.selectAll("text").attr("transform", translatePoints); + } }; // create a mock axis used to format hover text function createMockAxis(fullLayout) { - var mockAxis = { - type: 'linear', - showexponent: 'all', - exponentformat: Axes.layoutAttributes.exponentformat.dflt, - _gd: { _fullLayout: fullLayout } - }; - - Axes.setConvert(mockAxis); - return mockAxis; + var mockAxis = { + type: "linear", + showexponent: "all", + exponentformat: Axes.layoutAttributes.exponentformat.dflt, + _gd: { _fullLayout: fullLayout } + }; + + Axes.setConvert(mockAxis); + return mockAxis; } diff --git a/src/plots/geo/index.js b/src/plots/geo/index.js index 03cfbdf3393..c3c2ad9477c 100644 --- a/src/plots/geo/index.js +++ b/src/plots/geo/index.js @@ -5,100 +5,100 @@ * 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 Geo = require("./geo"); +var Plots = require("../../plots/plots"); -'use strict'; +exports.name = "geo"; -var Geo = require('./geo'); +exports.attr = "geo"; -var Plots = require('../../plots/plots'); - - -exports.name = 'geo'; - -exports.attr = 'geo'; - -exports.idRoot = 'geo'; +exports.idRoot = "geo"; exports.idRegex = /^geo([2-9]|[1-9][0-9]+)?$/; exports.attrRegex = /^geo([2-9]|[1-9][0-9]+)?$/; -exports.attributes = require('./layout/attributes'); +exports.attributes = require("./layout/attributes"); -exports.layoutAttributes = require('./layout/layout_attributes'); +exports.layoutAttributes = require("./layout/layout_attributes"); -exports.supplyLayoutDefaults = require('./layout/defaults'); +exports.supplyLayoutDefaults = require("./layout/defaults"); exports.plot = function plotGeo(gd) { - var fullLayout = gd._fullLayout, - calcData = gd.calcdata, - geoIds = Plots.getSubplotIds(fullLayout, 'geo'); + var fullLayout = gd._fullLayout, + calcData = gd.calcdata, + geoIds = Plots.getSubplotIds(fullLayout, "geo"); - /** + /** * If 'plotly-geo-assets.js' is not included, * initialize object to keep reference to every loaded topojson */ - if(window.PlotlyGeoAssets === undefined) { - window.PlotlyGeoAssets = { topojson: {} }; + if (window.PlotlyGeoAssets === undefined) { + window.PlotlyGeoAssets = { topojson: {} }; + } + + for (var i = 0; i < geoIds.length; i++) { + var geoId = geoIds[i], + geoCalcData = Plots.getSubplotCalcData(calcData, "geo", geoId), + geo = fullLayout[geoId]._subplot; + + // If geo is not instantiated, create one! + if (!geo) { + geo = new Geo( + { + id: geoId, + graphDiv: gd, + container: fullLayout._geocontainer.node(), + topojsonURL: gd._context.topojsonURL + }, + fullLayout + ); + + fullLayout[geoId]._subplot = geo; } - for(var i = 0; i < geoIds.length; i++) { - var geoId = geoIds[i], - geoCalcData = Plots.getSubplotCalcData(calcData, 'geo', geoId), - geo = fullLayout[geoId]._subplot; - - // If geo is not instantiated, create one! - if(!geo) { - geo = new Geo({ - id: geoId, - graphDiv: gd, - container: fullLayout._geocontainer.node(), - topojsonURL: gd._context.topojsonURL - }, - fullLayout - ); - - fullLayout[geoId]._subplot = geo; - } - - geo.plot(geoCalcData, fullLayout, gd._promises); - } + geo.plot(geoCalcData, fullLayout, gd._promises); + } }; -exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldGeoKeys = Plots.getSubplotIds(oldFullLayout, 'geo'); +exports.clean = function( + newFullData, + newFullLayout, + oldFullData, + oldFullLayout +) { + var oldGeoKeys = Plots.getSubplotIds(oldFullLayout, "geo"); - for(var i = 0; i < oldGeoKeys.length; i++) { - var oldGeoKey = oldGeoKeys[i]; - var oldGeo = oldFullLayout[oldGeoKey]._subplot; + for (var i = 0; i < oldGeoKeys.length; i++) { + var oldGeoKey = oldGeoKeys[i]; + var oldGeo = oldFullLayout[oldGeoKey]._subplot; - if(!newFullLayout[oldGeoKey] && !!oldGeo) { - oldGeo.geoDiv.remove(); - } + if (!newFullLayout[oldGeoKey] && !!oldGeo) { + oldGeo.geoDiv.remove(); } + } }; exports.toSVG = function(gd) { - var fullLayout = gd._fullLayout, - geoIds = Plots.getSubplotIds(fullLayout, 'geo'), - size = fullLayout._size; - - for(var i = 0; i < geoIds.length; i++) { - var geoLayout = fullLayout[geoIds[i]], - domain = geoLayout.domain, - geoFramework = geoLayout._subplot.framework; - - geoFramework.attr('style', null); - geoFramework - .attr({ - x: size.l + size.w * domain.x[0] + geoLayout._marginX, - y: size.t + size.h * (1 - domain.y[1]) + geoLayout._marginY, - width: geoLayout._width, - height: geoLayout._height - }); - - fullLayout._geoimages.node() - .appendChild(geoFramework.node()); - } + var fullLayout = gd._fullLayout, + geoIds = Plots.getSubplotIds(fullLayout, "geo"), + size = fullLayout._size; + + for (var i = 0; i < geoIds.length; i++) { + var geoLayout = fullLayout[geoIds[i]], + domain = geoLayout.domain, + geoFramework = geoLayout._subplot.framework; + + geoFramework.attr("style", null); + geoFramework.attr({ + x: size.l + size.w * domain.x[0] + geoLayout._marginX, + y: size.t + size.h * (1 - domain.y[1]) + geoLayout._marginY, + width: geoLayout._width, + height: geoLayout._height + }); + + fullLayout._geoimages.node().appendChild(geoFramework.node()); + } }; diff --git a/src/plots/geo/layout/attributes.js b/src/plots/geo/layout/attributes.js index e721fb6b0c5..efb02d44c89 100644 --- a/src/plots/geo/layout/attributes.js +++ b/src/plots/geo/layout/attributes.js @@ -5,22 +5,19 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - geo: { - valType: 'subplotid', - role: 'info', - dflt: 'geo', - description: [ - 'Sets a reference between this trace\'s geospatial coordinates and', - 'a geographic map.', - 'If *geo* (the default value), the geospatial coordinates refer to', - '`layout.geo`.', - 'If *geo2*, the geospatial coordinates refer to `layout.geo2`,', - 'and so on.' - ].join(' ') - } + geo: { + valType: "subplotid", + role: "info", + dflt: "geo", + description: [ + "Sets a reference between this trace's geospatial coordinates and", + "a geographic map.", + "If *geo* (the default value), the geospatial coordinates refer to", + "`layout.geo`.", + "If *geo2*, the geospatial coordinates refer to `layout.geo2`,", + "and so on." + ].join(" ") + } }; diff --git a/src/plots/geo/layout/axis_attributes.js b/src/plots/geo/layout/axis_attributes.js index a03ea496f05..c9f875a57ec 100644 --- a/src/plots/geo/layout/axis_attributes.js +++ b/src/plots/geo/layout/axis_attributes.js @@ -5,57 +5,47 @@ * 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 colorAttrs = require('../../../components/color/attributes'); - +"use strict"; +var colorAttrs = require("../../../components/color/attributes"); module.exports = { - range: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number'}, - {valType: 'number'} - ], - description: 'Sets the range of this axis (in degrees).' - }, - showgrid: { - valType: 'boolean', - role: 'info', - dflt: false, - description: 'Sets whether or not graticule are shown on the map.' - }, - tick0: { - valType: 'number', - role: 'info', - description: [ - 'Sets the graticule\'s starting tick longitude/latitude.' - ].join(' ') - }, - dtick: { - valType: 'number', - role: 'info', - description: [ - 'Sets the graticule\'s longitude/latitude tick step.' - ].join(' ') - }, - gridcolor: { - valType: 'color', - role: 'style', - dflt: colorAttrs.lightLine, - description: [ - 'Sets the graticule\'s stroke color.' - ].join(' ') - }, - gridwidth: { - valType: 'number', - role: 'style', - min: 0, - dflt: 1, - description: [ - 'Sets the graticule\'s stroke width (in px).' - ].join(' ') - } + range: { + valType: "info_array", + role: "info", + items: [{ valType: "number" }, { valType: "number" }], + description: "Sets the range of this axis (in degrees)." + }, + showgrid: { + valType: "boolean", + role: "info", + dflt: false, + description: "Sets whether or not graticule are shown on the map." + }, + tick0: { + valType: "number", + role: "info", + description: [ + "Sets the graticule's starting tick longitude/latitude." + ].join(" ") + }, + dtick: { + valType: "number", + role: "info", + description: ["Sets the graticule's longitude/latitude tick step."].join( + " " + ) + }, + gridcolor: { + valType: "color", + role: "style", + dflt: colorAttrs.lightLine, + description: ["Sets the graticule's stroke color."].join(" ") + }, + gridwidth: { + valType: "number", + role: "style", + min: 0, + dflt: 1, + description: ["Sets the graticule's stroke width (in px)."].join(" ") + } }; diff --git a/src/plots/geo/layout/axis_defaults.js b/src/plots/geo/layout/axis_defaults.js index f3ccf86f885..a631dbcb416 100644 --- a/src/plots/geo/layout/axis_defaults.js +++ b/src/plots/geo/layout/axis_defaults.js @@ -5,68 +5,68 @@ * 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 constants = require('../constants'); -var axisAttributes = require('./axis_attributes'); - - -module.exports = function supplyGeoAxisLayoutDefaults(geoLayoutIn, geoLayoutOut) { - var axesNames = constants.axesNames; - - var axisIn, axisOut; - - function coerce(attr, dflt) { - return Lib.coerce(axisIn, axisOut, axisAttributes, attr, dflt); +"use strict"; +var Lib = require("../../../lib"); +var constants = require("../constants"); +var axisAttributes = require("./axis_attributes"); + +module.exports = function supplyGeoAxisLayoutDefaults( + geoLayoutIn, + geoLayoutOut +) { + var axesNames = constants.axesNames; + + var axisIn, axisOut; + + function coerce(attr, dflt) { + return Lib.coerce(axisIn, axisOut, axisAttributes, attr, dflt); + } + + function getRangeDflt(axisName) { + var scope = geoLayoutOut.scope; + + var projLayout, projType, projRotation, rotateAngle, dfltSpans, halfSpan; + + if (scope === "world") { + projLayout = geoLayoutOut.projection; + projType = projLayout.type; + projRotation = projLayout.rotation; + dfltSpans = constants[axisName + "Span"]; + + halfSpan = dfltSpans[projType] !== undefined + ? dfltSpans[projType] / 2 + : dfltSpans["*"] / 2; + rotateAngle = axisName === "lonaxis" + ? projRotation.lon + : projRotation.lat; + + return [rotateAngle - halfSpan, rotateAngle + halfSpan]; + } else { + return constants.scopeDefaults[scope][axisName + "Range"]; } + } - function getRangeDflt(axisName) { - var scope = geoLayoutOut.scope; + for (var i = 0; i < axesNames.length; i++) { + var axisName = axesNames[i]; + axisIn = geoLayoutIn[axisName] || {}; + axisOut = {}; - var projLayout, projType, projRotation, rotateAngle, dfltSpans, halfSpan; + var rangeDflt = getRangeDflt(axisName); - if(scope === 'world') { - projLayout = geoLayoutOut.projection; - projType = projLayout.type; - projRotation = projLayout.rotation; - dfltSpans = constants[axisName + 'Span']; + var range = coerce("range", rangeDflt); - halfSpan = dfltSpans[projType] !== undefined ? - dfltSpans[projType] / 2 : - dfltSpans['*'] / 2; - rotateAngle = axisName === 'lonaxis' ? - projRotation.lon : - projRotation.lat; + Lib.noneOrAll(axisIn.range, axisOut.range, [0, 1]); - return [rotateAngle - halfSpan, rotateAngle + halfSpan]; - } - else return constants.scopeDefaults[scope][axisName + 'Range']; - } + coerce("tick0", range[0]); + coerce("dtick", axisName === "lonaxis" ? 30 : 10); - for(var i = 0; i < axesNames.length; i++) { - var axisName = axesNames[i]; - axisIn = geoLayoutIn[axisName] || {}; - axisOut = {}; - - var rangeDflt = getRangeDflt(axisName); - - var range = coerce('range', rangeDflt); - - Lib.noneOrAll(axisIn.range, axisOut.range, [0, 1]); - - coerce('tick0', range[0]); - coerce('dtick', axisName === 'lonaxis' ? 30 : 10); - - var show = coerce('showgrid'); - if(show) { - coerce('gridcolor'); - coerce('gridwidth'); - } - - geoLayoutOut[axisName] = axisOut; - geoLayoutOut[axisName]._fullRange = rangeDflt; + var show = coerce("showgrid"); + if (show) { + coerce("gridcolor"); + coerce("gridwidth"); } + + geoLayoutOut[axisName] = axisOut; + geoLayoutOut[axisName]._fullRange = rangeDflt; + } }; diff --git a/src/plots/geo/layout/defaults.js b/src/plots/geo/layout/defaults.js index 8fa7af85365..21a29e6465b 100644 --- a/src/plots/geo/layout/defaults.js +++ b/src/plots/geo/layout/defaults.js @@ -5,113 +5,110 @@ * 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 handleSubplotDefaults = require('../../subplot_defaults'); -var constants = require('../constants'); -var layoutAttributes = require('./layout_attributes'); -var supplyGeoAxisLayoutDefaults = require('./axis_defaults'); - +"use strict"; +var handleSubplotDefaults = require("../../subplot_defaults"); +var constants = require("../constants"); +var layoutAttributes = require("./layout_attributes"); +var supplyGeoAxisLayoutDefaults = require("./axis_defaults"); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - handleSubplotDefaults(layoutIn, layoutOut, fullData, { - type: 'geo', - attributes: layoutAttributes, - handleDefaults: handleGeoDefaults, - partition: 'y' - }); + handleSubplotDefaults(layoutIn, layoutOut, fullData, { + type: "geo", + attributes: layoutAttributes, + handleDefaults: handleGeoDefaults, + partition: "y" + }); }; function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) { - var show; + var show; - var scope = coerce('scope'); - var isScoped = (scope !== 'world'); - var scopeParams = constants.scopeDefaults[scope]; + var scope = coerce("scope"); + var isScoped = scope !== "world"; + var scopeParams = constants.scopeDefaults[scope]; - var resolution = coerce('resolution'); + var resolution = coerce("resolution"); - var projType = coerce('projection.type', scopeParams.projType); - var isAlbersUsa = projType === 'albers usa'; - var isConic = projType.indexOf('conic') !== -1; + var projType = coerce("projection.type", scopeParams.projType); + var isAlbersUsa = projType === "albers usa"; + var isConic = projType.indexOf("conic") !== -1; - if(isConic) { - var dfltProjParallels = scopeParams.projParallels || [0, 60]; - coerce('projection.parallels', dfltProjParallels); - } - - if(!isAlbersUsa) { - var dfltProjRotate = scopeParams.projRotate || [0, 0, 0]; - coerce('projection.rotation.lon', dfltProjRotate[0]); - coerce('projection.rotation.lat', dfltProjRotate[1]); - coerce('projection.rotation.roll', dfltProjRotate[2]); + if (isConic) { + var dfltProjParallels = scopeParams.projParallels || [0, 60]; + coerce("projection.parallels", dfltProjParallels); + } - show = coerce('showcoastlines', !isScoped); - if(show) { - coerce('coastlinecolor'); - coerce('coastlinewidth'); - } - - show = coerce('showocean'); - if(show) coerce('oceancolor'); - } - else geoLayoutOut.scope = 'usa'; - - coerce('projection.scale'); - - show = coerce('showland'); - if(show) coerce('landcolor'); - - show = coerce('showlakes'); - if(show) coerce('lakecolor'); - - show = coerce('showrivers'); - if(show) { - coerce('rivercolor'); - coerce('riverwidth'); - } - - show = coerce('showcountries', isScoped && scope !== 'usa'); - if(show) { - coerce('countrycolor'); - coerce('countrywidth'); - } + if (!isAlbersUsa) { + var dfltProjRotate = scopeParams.projRotate || [0, 0, 0]; + coerce("projection.rotation.lon", dfltProjRotate[0]); + coerce("projection.rotation.lat", dfltProjRotate[1]); + coerce("projection.rotation.roll", dfltProjRotate[2]); - if(scope === 'usa' || (scope === 'north america' && resolution === 50)) { - // Only works for: - // USA states at 110m - // USA states + Canada provinces at 50m - coerce('showsubunits', true); - coerce('subunitcolor'); - coerce('subunitwidth'); + show = coerce("showcoastlines", !isScoped); + if (show) { + coerce("coastlinecolor"); + coerce("coastlinewidth"); } - if(!isScoped) { - // Does not work in non-world scopes - show = coerce('showframe', true); - if(show) { - coerce('framecolor'); - coerce('framewidth'); - } + show = coerce("showocean"); + if (show) coerce("oceancolor"); + } else { + geoLayoutOut.scope = "usa"; + } + + coerce("projection.scale"); + + show = coerce("showland"); + if (show) coerce("landcolor"); + + show = coerce("showlakes"); + if (show) coerce("lakecolor"); + + show = coerce("showrivers"); + if (show) { + coerce("rivercolor"); + coerce("riverwidth"); + } + + show = coerce("showcountries", isScoped && scope !== "usa"); + if (show) { + coerce("countrycolor"); + coerce("countrywidth"); + } + + if (scope === "usa" || scope === "north america" && resolution === 50) { + // Only works for: + // USA states at 110m + // USA states + Canada provinces at 50m + coerce("showsubunits", true); + coerce("subunitcolor"); + coerce("subunitwidth"); + } + + if (!isScoped) { + // Does not work in non-world scopes + show = coerce("showframe", true); + if (show) { + coerce("framecolor"); + coerce("framewidth"); } + } - coerce('bgcolor'); + coerce("bgcolor"); - supplyGeoAxisLayoutDefaults(geoLayoutIn, geoLayoutOut); + supplyGeoAxisLayoutDefaults(geoLayoutIn, geoLayoutOut); - // bind a few helper variables - geoLayoutOut._isHighRes = resolution === 50; - geoLayoutOut._clipAngle = constants.lonaxisSpan[projType] / 2; - geoLayoutOut._isAlbersUsa = isAlbersUsa; - geoLayoutOut._isConic = isConic; - geoLayoutOut._isScoped = isScoped; + // bind a few helper variables + geoLayoutOut._isHighRes = resolution === 50; + geoLayoutOut._clipAngle = constants.lonaxisSpan[projType] / 2; + geoLayoutOut._isAlbersUsa = isAlbersUsa; + geoLayoutOut._isConic = isConic; + geoLayoutOut._isScoped = isScoped; - var rotation = geoLayoutOut.projection.rotation || {}; - geoLayoutOut.projection._rotate = [ - -rotation.lon || 0, - -rotation.lat || 0, - rotation.roll || 0 - ]; + var rotation = geoLayoutOut.projection.rotation || {}; + geoLayoutOut.projection._rotate = [ + -rotation.lon || 0, + -rotation.lat || 0, + rotation.roll || 0 + ]; } diff --git a/src/plots/geo/layout/layout_attributes.js b/src/plots/geo/layout/layout_attributes.js index 0a6b9e26140..616634e5d8d 100644 --- a/src/plots/geo/layout/layout_attributes.js +++ b/src/plots/geo/layout/layout_attributes.js @@ -5,253 +5,247 @@ * 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 colorAttrs = require('../../../components/color/attributes'); -var constants = require('../constants'); -var geoAxesAttrs = require('./axis_attributes'); - +"use strict"; +var colorAttrs = require("../../../components/color/attributes"); +var constants = require("../constants"); +var geoAxesAttrs = require("./axis_attributes"); module.exports = { - domain: { - x: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the horizontal domain of this map', - '(in plot fraction).' - ].join(' ') - }, - y: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the vertical domain of this map', - '(in plot fraction).' - ].join(' ') - } - }, - resolution: { - valType: 'enumerated', - values: [110, 50], - role: 'info', - dflt: 110, - coerceNumber: true, + domain: { + x: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: [ + "Sets the horizontal domain of this map", + "(in plot fraction)." + ].join(" ") + }, + y: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: [ + "Sets the vertical domain of this map", + "(in plot fraction)." + ].join(" ") + } + }, + resolution: { + valType: "enumerated", + values: [110, 50], + role: "info", + dflt: 110, + coerceNumber: true, + description: [ + "Sets the resolution of the base layers.", + "The values have units of km/mm", + "e.g. 110 corresponds to a scale ratio of 1:110,000,000." + ].join(" ") + }, + scope: { + valType: "enumerated", + role: "info", + values: Object.keys(constants.scopeDefaults), + dflt: "world", + description: "Set the scope of the map." + }, + projection: { + type: { + valType: "enumerated", + role: "info", + values: Object.keys(constants.projNames), + description: "Sets the projection type." + }, + rotation: { + lon: { + valType: "number", + role: "info", description: [ - 'Sets the resolution of the base layers.', - 'The values have units of km/mm', - 'e.g. 110 corresponds to a scale ratio of 1:110,000,000.' - ].join(' ') - }, - scope: { - valType: 'enumerated', - role: 'info', - values: Object.keys(constants.scopeDefaults), - dflt: 'world', - description: 'Set the scope of the map.' - }, - projection: { - type: { - valType: 'enumerated', - role: 'info', - values: Object.keys(constants.projNames), - description: 'Sets the projection type.' - }, - rotation: { - lon: { - valType: 'number', - role: 'info', - description: [ - 'Rotates the map along parallels', - '(in degrees East).' - ].join(' ') - }, - lat: { - valType: 'number', - role: 'info', - description: [ - 'Rotates the map along meridians', - '(in degrees North).' - ].join(' ') - }, - roll: { - valType: 'number', - role: 'info', - description: [ - 'Roll the map (in degrees)', - 'For example, a roll of *180* makes the map appear upside down.' - ].join(' ') - } - }, - parallels: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number'}, - {valType: 'number'} - ], - description: [ - 'For conic projection types only.', - 'Sets the parallels (tangent, secant)', - 'where the cone intersects the sphere.' - ].join(' ') - }, - scale: { - valType: 'number', - role: 'info', - min: 0, - max: 10, - dflt: 1, - description: 'Zooms in or out on the map view.' - } - }, - showcoastlines: { - valType: 'boolean', - role: 'info', - description: 'Sets whether or not the coastlines are drawn.' - }, - coastlinecolor: { - valType: 'color', - role: 'style', - dflt: colorAttrs.defaultLine, - description: 'Sets the coastline color.' - }, - coastlinewidth: { - valType: 'number', - role: 'style', - min: 0, - dflt: 1, - description: 'Sets the coastline stroke width (in px).' - }, - showland: { - valType: 'boolean', - role: 'info', - dflt: false, - description: 'Sets whether or not land masses are filled in color.' - }, - landcolor: { - valType: 'color', - role: 'style', - dflt: constants.landColor, - description: 'Sets the land mass color.' - }, - showocean: { - valType: 'boolean', - role: 'info', - dflt: false, - description: 'Sets whether or not oceans are filled in color.' - }, - oceancolor: { - valType: 'color', - role: 'style', - dflt: constants.waterColor, - description: 'Sets the ocean color' - }, - showlakes: { - valType: 'boolean', - role: 'info', - dflt: false, - description: 'Sets whether or not lakes are drawn.' - }, - lakecolor: { - valType: 'color', - role: 'style', - dflt: constants.waterColor, - description: 'Sets the color of the lakes.' - }, - showrivers: { - valType: 'boolean', - role: 'info', - dflt: false, - description: 'Sets whether or not rivers are drawn.' - }, - rivercolor: { - valType: 'color', - role: 'style', - dflt: constants.waterColor, - description: 'Sets color of the rivers.' - }, - riverwidth: { - valType: 'number', - role: 'style', - min: 0, - dflt: 1, - description: 'Sets the stroke width (in px) of the rivers.' - }, - showcountries: { - valType: 'boolean', - role: 'info', - description: 'Sets whether or not country boundaries are drawn.' - }, - countrycolor: { - valType: 'color', - role: 'style', - dflt: colorAttrs.defaultLine, - description: 'Sets line color of the country boundaries.' - }, - countrywidth: { - valType: 'number', - role: 'style', - min: 0, - dflt: 1, - description: 'Sets line width (in px) of the country boundaries.' - }, - showsubunits: { - valType: 'boolean', - role: 'info', + "Rotates the map along parallels", + "(in degrees East)." + ].join(" ") + }, + lat: { + valType: "number", + role: "info", description: [ - 'Sets whether or not boundaries of subunits within countries', - '(e.g. states, provinces) are drawn.' - ].join(' ') - }, - subunitcolor: { - valType: 'color', - role: 'style', - dflt: colorAttrs.defaultLine, - description: 'Sets the color of the subunits boundaries.' - }, - subunitwidth: { - valType: 'number', - role: 'style', - min: 0, - dflt: 1, - description: 'Sets the stroke width (in px) of the subunits boundaries.' - }, - showframe: { - valType: 'boolean', - role: 'info', - description: 'Sets whether or not a frame is drawn around the map.' - }, - framecolor: { - valType: 'color', - role: 'style', - dflt: colorAttrs.defaultLine, - description: 'Sets the color the frame.' - }, - framewidth: { - valType: 'number', - role: 'style', - min: 0, - dflt: 1, - description: 'Sets the stroke width (in px) of the frame.' - }, - bgcolor: { - valType: 'color', - role: 'style', - dflt: colorAttrs.background, - description: 'Set the background color of the map' - }, - lonaxis: geoAxesAttrs, - lataxis: geoAxesAttrs + "Rotates the map along meridians", + "(in degrees North)." + ].join(" ") + }, + roll: { + valType: "number", + role: "info", + description: [ + "Roll the map (in degrees)", + "For example, a roll of *180* makes the map appear upside down." + ].join(" ") + } + }, + parallels: { + valType: "info_array", + role: "info", + items: [{ valType: "number" }, { valType: "number" }], + description: [ + "For conic projection types only.", + "Sets the parallels (tangent, secant)", + "where the cone intersects the sphere." + ].join(" ") + }, + scale: { + valType: "number", + role: "info", + min: 0, + max: 10, + dflt: 1, + description: "Zooms in or out on the map view." + } + }, + showcoastlines: { + valType: "boolean", + role: "info", + description: "Sets whether or not the coastlines are drawn." + }, + coastlinecolor: { + valType: "color", + role: "style", + dflt: colorAttrs.defaultLine, + description: "Sets the coastline color." + }, + coastlinewidth: { + valType: "number", + role: "style", + min: 0, + dflt: 1, + description: "Sets the coastline stroke width (in px)." + }, + showland: { + valType: "boolean", + role: "info", + dflt: false, + description: "Sets whether or not land masses are filled in color." + }, + landcolor: { + valType: "color", + role: "style", + dflt: constants.landColor, + description: "Sets the land mass color." + }, + showocean: { + valType: "boolean", + role: "info", + dflt: false, + description: "Sets whether or not oceans are filled in color." + }, + oceancolor: { + valType: "color", + role: "style", + dflt: constants.waterColor, + description: "Sets the ocean color" + }, + showlakes: { + valType: "boolean", + role: "info", + dflt: false, + description: "Sets whether or not lakes are drawn." + }, + lakecolor: { + valType: "color", + role: "style", + dflt: constants.waterColor, + description: "Sets the color of the lakes." + }, + showrivers: { + valType: "boolean", + role: "info", + dflt: false, + description: "Sets whether or not rivers are drawn." + }, + rivercolor: { + valType: "color", + role: "style", + dflt: constants.waterColor, + description: "Sets color of the rivers." + }, + riverwidth: { + valType: "number", + role: "style", + min: 0, + dflt: 1, + description: "Sets the stroke width (in px) of the rivers." + }, + showcountries: { + valType: "boolean", + role: "info", + description: "Sets whether or not country boundaries are drawn." + }, + countrycolor: { + valType: "color", + role: "style", + dflt: colorAttrs.defaultLine, + description: "Sets line color of the country boundaries." + }, + countrywidth: { + valType: "number", + role: "style", + min: 0, + dflt: 1, + description: "Sets line width (in px) of the country boundaries." + }, + showsubunits: { + valType: "boolean", + role: "info", + description: [ + "Sets whether or not boundaries of subunits within countries", + "(e.g. states, provinces) are drawn." + ].join(" ") + }, + subunitcolor: { + valType: "color", + role: "style", + dflt: colorAttrs.defaultLine, + description: "Sets the color of the subunits boundaries." + }, + subunitwidth: { + valType: "number", + role: "style", + min: 0, + dflt: 1, + description: "Sets the stroke width (in px) of the subunits boundaries." + }, + showframe: { + valType: "boolean", + role: "info", + description: "Sets whether or not a frame is drawn around the map." + }, + framecolor: { + valType: "color", + role: "style", + dflt: colorAttrs.defaultLine, + description: "Sets the color the frame." + }, + framewidth: { + valType: "number", + role: "style", + min: 0, + dflt: 1, + description: "Sets the stroke width (in px) of the frame." + }, + bgcolor: { + valType: "color", + role: "style", + dflt: colorAttrs.background, + description: "Set the background color of the map" + }, + lonaxis: geoAxesAttrs, + lataxis: geoAxesAttrs }; diff --git a/src/plots/geo/projections.js b/src/plots/geo/projections.js index e0f1efc3fc1..0b0cea6b183 100644 --- a/src/plots/geo/projections.js +++ b/src/plots/geo/projections.js @@ -5,7 +5,6 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - /* * Generated by https://github.com/etpinard/d3-geo-projection-picker * @@ -13,16 +12,16 @@ * * into a CommonJS require-able module. */ - -'use strict'; - +"use strict"; /* eslint-disable */ function addProjectionsToD3(d3) { d3.geo.project = function(object, projection) { var stream = projection.stream; if (!stream) throw new Error("not yet supported"); - return (object && d3_geo_projectObjectType.hasOwnProperty(object.type) ? d3_geo_projectObjectType[object.type] : d3_geo_projectGeometry)(object, stream); + return (object && d3_geo_projectObjectType.hasOwnProperty(object.type) + ? d3_geo_projectObjectType[object.type] + : d3_geo_projectGeometry)(object, stream); }; function d3_geo_projectFeature(object, stream) { return { @@ -34,12 +33,13 @@ function addProjectionsToD3(d3) { } function d3_geo_projectGeometry(geometry, stream) { if (!geometry) return null; - if (geometry.type === "GeometryCollection") return { - type: "GeometryCollection", - geometries: object.geometries.map(function(geometry) { - return d3_geo_projectGeometry(geometry, stream); - }) - }; + if (geometry.type === "GeometryCollection") + return { + type: "GeometryCollection", + geometries: object.geometries.map(function(geometry) { + return d3_geo_projectGeometry(geometry, stream); + }) + }; if (!d3_geo_projectGeometryType.hasOwnProperty(geometry.type)) return null; var sink = d3_geo_projectGeometryType[geometry.type]; d3.geo.stream(geometry, stream(sink)); @@ -59,16 +59,14 @@ function addProjectionsToD3(d3) { var d3_geo_projectPoints = [], d3_geo_projectLines = []; var d3_geo_projectPoint = { point: function(x, y) { - d3_geo_projectPoints.push([ x, y ]); + d3_geo_projectPoints.push([x, y]); }, result: function() { - var result = !d3_geo_projectPoints.length ? null : d3_geo_projectPoints.length < 2 ? { - type: "Point", - coordinates: d3_geo_projectPoints[0] - } : { - type: "MultiPoint", - coordinates: d3_geo_projectPoints - }; + var result = !d3_geo_projectPoints.length + ? null + : d3_geo_projectPoints.length < 2 + ? { type: "Point", coordinates: d3_geo_projectPoints[0] } + : { type: "MultiPoint", coordinates: d3_geo_projectPoints }; d3_geo_projectPoints = []; return result; } @@ -76,20 +74,20 @@ function addProjectionsToD3(d3) { var d3_geo_projectLine = { lineStart: d3_geo_projectNoop, point: function(x, y) { - d3_geo_projectPoints.push([ x, y ]); + d3_geo_projectPoints.push([x, y]); }, lineEnd: function() { - if (d3_geo_projectPoints.length) d3_geo_projectLines.push(d3_geo_projectPoints), - d3_geo_projectPoints = []; + if (d3_geo_projectPoints.length) + d3_geo_projectLines.push( + d3_geo_projectPoints + ), d3_geo_projectPoints = []; }, result: function() { - var result = !d3_geo_projectLines.length ? null : d3_geo_projectLines.length < 2 ? { - type: "LineString", - coordinates: d3_geo_projectLines[0] - } : { - type: "MultiLineString", - coordinates: d3_geo_projectLines - }; + var result = !d3_geo_projectLines.length + ? null + : d3_geo_projectLines.length < 2 + ? { type: "LineString", coordinates: d3_geo_projectLines[0] } + : { type: "MultiLineString", coordinates: d3_geo_projectLines }; d3_geo_projectLines = []; return result; } @@ -98,13 +96,17 @@ function addProjectionsToD3(d3) { polygonStart: d3_geo_projectNoop, lineStart: d3_geo_projectNoop, point: function(x, y) { - d3_geo_projectPoints.push([ x, y ]); + d3_geo_projectPoints.push([x, y]); }, lineEnd: function() { var n = d3_geo_projectPoints.length; if (n) { - do d3_geo_projectPoints.push(d3_geo_projectPoints[0].slice()); while (++n < 4); - d3_geo_projectLines.push(d3_geo_projectPoints), d3_geo_projectPoints = []; + do + d3_geo_projectPoints.push(d3_geo_projectPoints[0].slice()); + while (++n < 4); + d3_geo_projectLines.push( + d3_geo_projectPoints + ), d3_geo_projectPoints = []; } }, polygonEnd: d3_geo_projectNoop, @@ -112,7 +114,8 @@ function addProjectionsToD3(d3) { if (!d3_geo_projectLines.length) return null; var polygons = [], holes = []; d3_geo_projectLines.forEach(function(ring) { - if (d3_geo_projectClockwise(ring)) polygons.push([ ring ]); else holes.push(ring); + if (d3_geo_projectClockwise(ring)) polygons.push([ring]); + else holes.push(ring); }); holes.forEach(function(hole) { var point = hole[0]; @@ -121,16 +124,15 @@ function addProjectionsToD3(d3) { polygon.push(hole); return true; } - }) || polygons.push([ hole ]); + }) || + polygons.push([hole]); }); d3_geo_projectLines = []; - return !polygons.length ? null : polygons.length > 1 ? { - type: "MultiPolygon", - coordinates: polygons - } : { - type: "Polygon", - coordinates: polygons[0] - }; + return !polygons.length + ? null + : polygons.length > 1 + ? { type: "MultiPolygon", coordinates: polygons } + : { type: "Polygon", coordinates: polygons[0] }; } }; var d3_geo_projectGeometryType = { @@ -145,19 +147,34 @@ function addProjectionsToD3(d3) { function d3_geo_projectNoop() {} function d3_geo_projectClockwise(ring) { if ((n = ring.length) < 4) return false; - var i = 0, n, area = ring[n - 1][1] * ring[0][0] - ring[n - 1][0] * ring[0][1]; - while (++i < n) area += ring[i - 1][1] * ring[i][0] - ring[i - 1][0] * ring[i][1]; + var i = 0, + n, + area = ring[n - 1][1] * ring[0][0] - ring[n - 1][0] * ring[0][1]; + while (++i < n) + area += ring[i - 1][1] * ring[i][0] - ring[i - 1][0] * ring[i][1]; return area <= 0; } function d3_geo_projectContains(ring, point) { var x = point[0], y = point[1], contains = false; for (var i = 0, n = ring.length, j = n - 1; i < n; j = i++) { - var pi = ring[i], xi = pi[0], yi = pi[1], pj = ring[j], xj = pj[0], yj = pj[1]; - if (yi > y ^ yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) contains = !contains; + var pi = ring[i], + xi = pi[0], + yi = pi[1], + pj = ring[j], + xj = pj[0], + yj = pj[1]; + if (yi > y ^ yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) + contains = !contains; } return contains; } - var ε = 1e-6, ε2 = ε * ε, π = Math.PI, halfπ = π / 2, sqrtπ = Math.sqrt(π), radians = π / 180, degrees = 180 / π; + var ε = 1e-6, + ε2 = ε * ε, + π = Math.PI, + halfπ = π / 2, + sqrtπ = Math.sqrt(π), + radians = π / 180, + degrees = 180 / π; function sinci(x) { return x ? x / Math.sin(x) : 1; } @@ -173,41 +190,63 @@ function addProjectionsToD3(d3) { function asqrt(x) { return x > 0 ? Math.sqrt(x) : 0; } - var projection = d3.geo.projection, projectionMutator = d3.geo.projectionMutator; + var projection = d3.geo.projection, + projectionMutator = d3.geo.projectionMutator; d3.geo.interrupt = function(project) { - var lobes = [ [ [ [ -π, 0 ], [ 0, halfπ ], [ π, 0 ] ] ], [ [ [ -π, 0 ], [ 0, -halfπ ], [ π, 0 ] ] ] ]; + var lobes = [ + [[[-π, 0], [0, halfπ], [π, 0]]], + [[[-π, 0], [0, -halfπ], [π, 0]]] + ]; var bounds; function forward(λ, φ) { var sign = φ < 0 ? -1 : +1, hemilobes = lobes[+(φ < 0)]; - for (var i = 0, n = hemilobes.length - 1; i < n && λ > hemilobes[i][2][0]; ++i) ; + for ( + var i = 0, n = hemilobes.length - 1; + i < n && λ > hemilobes[i][2][0]; + ++i + ); var coordinates = project(λ - hemilobes[i][1][0], φ); - coordinates[0] += project(hemilobes[i][1][0], sign * φ > sign * hemilobes[i][0][1] ? hemilobes[i][0][1] : φ)[0]; + coordinates[0] += project( + hemilobes[i][1][0], + sign * φ > sign * hemilobes[i][0][1] ? hemilobes[i][0][1] : φ + )[0]; return coordinates; } function reset() { bounds = lobes.map(function(hemilobes) { return hemilobes.map(function(lobe) { - var x0 = project(lobe[0][0], lobe[0][1])[0], x1 = project(lobe[2][0], lobe[2][1])[0], y0 = project(lobe[1][0], lobe[0][1])[1], y1 = project(lobe[1][0], lobe[1][1])[1], t; + var x0 = project(lobe[0][0], lobe[0][1])[0], + x1 = project(lobe[2][0], lobe[2][1])[0], + y0 = project(lobe[1][0], lobe[0][1])[1], + y1 = project(lobe[1][0], lobe[1][1])[1], + t; if (y0 > y1) t = y0, y0 = y1, y1 = t; - return [ [ x0, y0 ], [ x1, y1 ] ]; + return [[x0, y0], [x1, y1]]; }); }); } - if (project.invert) forward.invert = function(x, y) { - var hemibounds = bounds[+(y < 0)], hemilobes = lobes[+(y < 0)]; - for (var i = 0, n = hemibounds.length; i < n; ++i) { - var b = hemibounds[i]; - if (b[0][0] <= x && x < b[1][0] && b[0][1] <= y && y < b[1][1]) { - var coordinates = project.invert(x - project(hemilobes[i][1][0], 0)[0], y); - coordinates[0] += hemilobes[i][1][0]; - return pointEqual(forward(coordinates[0], coordinates[1]), [ x, y ]) ? coordinates : null; + if (project.invert) + forward.invert = function(x, y) { + var hemibounds = bounds[+(y < 0)], hemilobes = lobes[+(y < 0)]; + for (var i = 0, n = hemibounds.length; i < n; ++i) { + var b = hemibounds[i]; + if (b[0][0] <= x && x < b[1][0] && b[0][1] <= y && y < b[1][1]) { + var coordinates = project.invert( + x - project(hemilobes[i][1][0], 0)[0], + y + ); + coordinates[0] += hemilobes[i][1][0]; + return pointEqual(forward(coordinates[0], coordinates[1]), [x, y]) + ? coordinates + : null; + } } - } - }; + }; var projection = d3.geo.projection(forward), stream_ = projection.stream; projection.stream = function(stream) { - var rotate = projection.rotate(), rotateStream = stream_(stream), sphereStream = (projection.rotate([ 0, 0 ]), - stream_(stream)); + var rotate = projection.rotate(), + rotateStream = stream_(stream), + sphereStream = (projection.rotate([0, 0]), stream_(stream)); projection.rotate(rotate); rotateStream.sphere = function() { d3.geo.stream(sphere(), sphereStream); @@ -215,14 +254,23 @@ function addProjectionsToD3(d3) { return rotateStream; }; projection.lobes = function(_) { - if (!arguments.length) return lobes.map(function(lobes) { - return lobes.map(function(lobe) { - return [ [ lobe[0][0] * 180 / π, lobe[0][1] * 180 / π ], [ lobe[1][0] * 180 / π, lobe[1][1] * 180 / π ], [ lobe[2][0] * 180 / π, lobe[2][1] * 180 / π ] ]; + if (!arguments.length) + return lobes.map(function(lobes) { + return lobes.map(function(lobe) { + return [ + [lobe[0][0] * 180 / π, lobe[0][1] * 180 / π], + [lobe[1][0] * 180 / π, lobe[1][1] * 180 / π], + [lobe[2][0] * 180 / π, lobe[2][1] * 180 / π] + ]; + }); }); - }); lobes = _.map(function(lobes) { return lobes.map(function(lobe) { - return [ [ lobe[0][0] * π / 180, lobe[0][1] * π / 180 ], [ lobe[1][0] * π / 180, lobe[1][1] * π / 180 ], [ lobe[2][0] * π / 180, lobe[2][1] * π / 180 ] ]; + return [ + [lobe[0][0] * π / 180, lobe[0][1] * π / 180], + [lobe[1][0] * π / 180, lobe[1][1] * π / 180], + [lobe[2][0] * π / 180, lobe[2][1] * π / 180] + ]; }); }); reset(); @@ -231,25 +279,59 @@ function addProjectionsToD3(d3) { function sphere() { var ε = 1e-6, coordinates = []; for (var i = 0, n = lobes[0].length; i < n; ++i) { - var lobe = lobes[0][i], λ0 = lobe[0][0] * 180 / π, φ0 = lobe[0][1] * 180 / π, φ1 = lobe[1][1] * 180 / π, λ2 = lobe[2][0] * 180 / π, φ2 = lobe[2][1] * 180 / π; - coordinates.push(resample([ [ λ0 + ε, φ0 + ε ], [ λ0 + ε, φ1 - ε ], [ λ2 - ε, φ1 - ε ], [ λ2 - ε, φ2 + ε ] ], 30)); + var lobe = lobes[0][i], + λ0 = lobe[0][0] * 180 / π, + φ0 = lobe[0][1] * 180 / π, + φ1 = lobe[1][1] * 180 / π, + λ2 = lobe[2][0] * 180 / π, + φ2 = lobe[2][1] * 180 / π; + coordinates.push( + resample( + [ + [λ0 + ε, φ0 + ε], + [λ0 + ε, φ1 - ε], + [λ2 - ε, φ1 - ε], + [λ2 - ε, φ2 + ε] + ], + 30 + ) + ); } for (var i = lobes[1].length - 1; i >= 0; --i) { - var lobe = lobes[1][i], λ0 = lobe[0][0] * 180 / π, φ0 = lobe[0][1] * 180 / π, φ1 = lobe[1][1] * 180 / π, λ2 = lobe[2][0] * 180 / π, φ2 = lobe[2][1] * 180 / π; - coordinates.push(resample([ [ λ2 - ε, φ2 - ε ], [ λ2 - ε, φ1 + ε ], [ λ0 + ε, φ1 + ε ], [ λ0 + ε, φ0 - ε ] ], 30)); + var lobe = lobes[1][i], + λ0 = lobe[0][0] * 180 / π, + φ0 = lobe[0][1] * 180 / π, + φ1 = lobe[1][1] * 180 / π, + λ2 = lobe[2][0] * 180 / π, + φ2 = lobe[2][1] * 180 / π; + coordinates.push( + resample( + [ + [λ2 - ε, φ2 - ε], + [λ2 - ε, φ1 + ε], + [λ0 + ε, φ1 + ε], + [λ0 + ε, φ0 - ε] + ], + 30 + ) + ); } - return { - type: "Polygon", - coordinates: [ d3.merge(coordinates) ] - }; + return { type: "Polygon", coordinates: [d3.merge(coordinates)] }; } function resample(coordinates, m) { - var i = -1, n = coordinates.length, p0 = coordinates[0], p1, dx, dy, resampled = []; + var i = -1, + n = coordinates.length, + p0 = coordinates[0], + p1, + dx, + dy, + resampled = []; while (++i < n) { p1 = coordinates[i]; dx = (p1[0] - p0[0]) / m; dy = (p1[1] - p0[1]) / m; - for (var j = 0; j < m; ++j) resampled.push([ p0[0] + j * dx, p0[1] + j * dy ]); + for (var j = 0; j < m; ++j) + resampled.push([p0[0] + j * dx, p0[1] + j * dy]); p0 = p1; } resampled.push(p1); @@ -267,11 +349,17 @@ function addProjectionsToD3(d3) { var cosφ = Math.cos(φ); φ -= δ = (φ + Math.sin(φ) * (cosφ + 2) - k) / (2 * cosφ * (1 + cosφ)); } - return [ 2 / Math.sqrt(π * (4 + π)) * λ * (1 + Math.cos(φ)), 2 * Math.sqrt(π / (4 + π)) * Math.sin(φ) ]; + return [ + 2 / Math.sqrt(π * (4 + π)) * λ * (1 + Math.cos(φ)), + 2 * Math.sqrt(π / (4 + π)) * Math.sin(φ) + ]; } eckert4.invert = function(x, y) { - var A = .5 * y * Math.sqrt((4 + π) / π), k = asin(A), c = Math.cos(k); - return [ x / (2 / Math.sqrt(π * (4 + π)) * (1 + c)), asin((k + A * (c + 2)) / (2 + halfπ)) ]; + var A = 0.5 * y * Math.sqrt((4 + π) / π), k = asin(A), c = Math.cos(k); + return [ + x / (2 / Math.sqrt(π * (4 + π)) * (1 + c)), + asin((k + A * (c + 2)) / (2 + halfπ)) + ]; }; (d3.geo.eckert4 = function() { return projection(eckert4); @@ -302,27 +390,27 @@ function addProjectionsToD3(d3) { return p; } function hammerQuarticAuthalic(λ, φ) { - return [ λ * Math.cos(φ) / Math.cos(φ /= 2), 2 * Math.sin(φ) ]; + return [λ * Math.cos(φ) / Math.cos(φ /= 2), 2 * Math.sin(φ)]; } hammerQuarticAuthalic.invert = function(x, y) { var φ = 2 * asin(y / 2); - return [ x * Math.cos(φ / 2) / Math.cos(φ), φ ]; + return [x * Math.cos(φ / 2) / Math.cos(φ), φ]; }; (d3.geo.hammer = hammerProjection).raw = hammer; function kavrayskiy7(λ, φ) { - return [ 3 * λ / (2 * π) * Math.sqrt(π * π / 3 - φ * φ), φ ]; + return [3 * λ / (2 * π) * Math.sqrt(π * π / 3 - φ * φ), φ]; } kavrayskiy7.invert = function(x, y) { - return [ 2 / 3 * π * x / Math.sqrt(π * π / 3 - y * y), y ]; + return [2 / 3 * π * x / Math.sqrt(π * π / 3 - y * y), y]; }; (d3.geo.kavrayskiy7 = function() { return projection(kavrayskiy7); }).raw = kavrayskiy7; function miller(λ, φ) { - return [ λ, 1.25 * Math.log(Math.tan(π / 4 + .4 * φ)) ]; + return [λ, 1.25 * Math.log(Math.tan(π / 4 + 0.4 * φ))]; } miller.invert = function(x, y) { - return [ x, 2.5 * Math.atan(Math.exp(.8 * y)) - .625 * π ]; + return [x, 2.5 * Math.atan(Math.exp(0.8 * y)) - 0.625 * π]; }; (d3.geo.miller = function() { return projection(miller); @@ -330,52 +418,121 @@ function addProjectionsToD3(d3) { function mollweideBromleyθ(Cp) { return function(θ) { var Cpsinθ = Cp * Math.sin(θ), i = 30, δ; - do θ -= δ = (θ + Math.sin(θ) - Cpsinθ) / (1 + Math.cos(θ)); while (Math.abs(δ) > ε && --i > 0); + do + θ -= δ = (θ + Math.sin(θ) - Cpsinθ) / (1 + Math.cos(θ)); + while (Math.abs(δ) > ε && --i > 0); return θ / 2; }; } function mollweideBromley(Cx, Cy, Cp) { var θ = mollweideBromleyθ(Cp); function forward(λ, φ) { - return [ Cx * λ * Math.cos(φ = θ(φ)), Cy * Math.sin(φ) ]; + return [Cx * λ * Math.cos(φ = θ(φ)), Cy * Math.sin(φ)]; } forward.invert = function(x, y) { var θ = asin(y / Cy); - return [ x / (Cx * Math.cos(θ)), asin((2 * θ + Math.sin(2 * θ)) / Cp) ]; + return [x / (Cx * Math.cos(θ)), asin((2 * θ + Math.sin(2 * θ)) / Cp)]; }; return forward; } - var mollweideθ = mollweideBromleyθ(π), mollweide = mollweideBromley(Math.SQRT2 / halfπ, Math.SQRT2, π); + var mollweideθ = mollweideBromleyθ(π), + mollweide = mollweideBromley(Math.SQRT2 / halfπ, Math.SQRT2, π); (d3.geo.mollweide = function() { return projection(mollweide); }).raw = mollweide; function naturalEarth(λ, φ) { var φ2 = φ * φ, φ4 = φ2 * φ2; - return [ λ * (.8707 - .131979 * φ2 + φ4 * (-.013791 + φ4 * (.003971 * φ2 - .001529 * φ4))), φ * (1.007226 + φ2 * (.015085 + φ4 * (-.044475 + .028874 * φ2 - .005916 * φ4))) ]; + return [ + λ * + (0.8707 - + 0.131979 * φ2 + + φ4 * (-0.013791 + φ4 * (0.003971 * φ2 - 0.001529 * φ4))), + φ * + (1.007226 + + φ2 * (0.015085 + φ4 * (-0.044475 + 0.028874 * φ2 - 0.005916 * φ4))) + ]; } naturalEarth.invert = function(x, y) { var φ = y, i = 25, δ; do { var φ2 = φ * φ, φ4 = φ2 * φ2; - φ -= δ = (φ * (1.007226 + φ2 * (.015085 + φ4 * (-.044475 + .028874 * φ2 - .005916 * φ4))) - y) / (1.007226 + φ2 * (.015085 * 3 + φ4 * (-.044475 * 7 + .028874 * 9 * φ2 - .005916 * 11 * φ4))); + φ -= δ = (φ * + (1.007226 + + φ2 * (0.015085 + φ4 * (-0.044475 + 0.028874 * φ2 - 0.005916 * φ4))) - + y) / + (1.007226 + + φ2 * + (0.015085 * 3 + + φ4 * ((-0.044475) * 7 + 0.028874 * 9 * φ2 - 0.005916 * 11 * φ4))); } while (Math.abs(δ) > ε && --i > 0); - return [ x / (.8707 + (φ2 = φ * φ) * (-.131979 + φ2 * (-.013791 + φ2 * φ2 * φ2 * (.003971 - .001529 * φ2)))), φ ]; + return [ + x / + (0.8707 + + (φ2 = φ * φ) * + (-0.131979 + + φ2 * (-0.013791 + φ2 * φ2 * φ2 * (0.003971 - 0.001529 * φ2)))), + φ + ]; }; (d3.geo.naturalEarth = function() { return projection(naturalEarth); }).raw = naturalEarth; - var robinsonConstants = [ [ .9986, -.062 ], [ 1, 0 ], [ .9986, .062 ], [ .9954, .124 ], [ .99, .186 ], [ .9822, .248 ], [ .973, .31 ], [ .96, .372 ], [ .9427, .434 ], [ .9216, .4958 ], [ .8962, .5571 ], [ .8679, .6176 ], [ .835, .6769 ], [ .7986, .7346 ], [ .7597, .7903 ], [ .7186, .8435 ], [ .6732, .8936 ], [ .6213, .9394 ], [ .5722, .9761 ], [ .5322, 1 ] ]; + var robinsonConstants = [ + [0.9986, -0.062], + [1, 0], + [0.9986, 0.062], + [0.9954, 0.124], + [0.99, 0.186], + [0.9822, 0.248], + [0.973, 0.31], + [0.96, 0.372], + [0.9427, 0.434], + [0.9216, 0.4958], + [0.8962, 0.5571], + [0.8679, 0.6176], + [0.835, 0.6769], + [0.7986, 0.7346], + [0.7597, 0.7903], + [0.7186, 0.8435], + [0.6732, 0.8936], + [0.6213, 0.9394], + [0.5722, 0.9761], + [0.5322, 1] + ]; robinsonConstants.forEach(function(d) { d[1] *= 1.0144; }); function robinson(λ, φ) { - var i = Math.min(18, Math.abs(φ) * 36 / π), i0 = Math.floor(i), di = i - i0, ax = (k = robinsonConstants[i0])[0], ay = k[1], bx = (k = robinsonConstants[++i0])[0], by = k[1], cx = (k = robinsonConstants[Math.min(19, ++i0)])[0], cy = k[1], k; - return [ λ * (bx + di * (cx - ax) / 2 + di * di * (cx - 2 * bx + ax) / 2), (φ > 0 ? halfπ : -halfπ) * (by + di * (cy - ay) / 2 + di * di * (cy - 2 * by + ay) / 2) ]; + var i = Math.min(18, Math.abs(φ) * 36 / π), + i0 = Math.floor(i), + di = i - i0, + ax = (k = robinsonConstants[i0])[0], + ay = k[1], + bx = (k = robinsonConstants[++i0])[0], + by = k[1], + cx = (k = robinsonConstants[Math.min(19, (++i0))])[0], + cy = k[1], + k; + return [ + λ * (bx + di * (cx - ax) / 2 + di * di * (cx - 2 * bx + ax) / 2), + (φ > 0 ? halfπ : -halfπ) * + (by + di * (cy - ay) / 2 + di * di * (cy - 2 * by + ay) / 2) + ]; } robinson.invert = function(x, y) { - var yy = y / halfπ, φ = yy * 90, i = Math.min(18, Math.abs(φ / 5)), i0 = Math.max(0, Math.floor(i)); + var yy = y / halfπ, + φ = yy * 90, + i = Math.min(18, Math.abs(φ / 5)), + i0 = Math.max(0, Math.floor(i)); do { - var ay = robinsonConstants[i0][1], by = robinsonConstants[i0 + 1][1], cy = robinsonConstants[Math.min(19, i0 + 2)][1], u = cy - ay, v = cy - 2 * by + ay, t = 2 * (Math.abs(yy) - by) / u, c = v / u, di = t * (1 - c * t * (1 - 2 * c * t)); + var ay = robinsonConstants[i0][1], + by = robinsonConstants[i0 + 1][1], + cy = robinsonConstants[Math.min(19, i0 + 2)][1], + u = cy - ay, + v = cy - 2 * by + ay, + t = 2 * (Math.abs(yy) - by) / u, + c = v / u, + di = t * (1 - c * t * (1 - 2 * c * t)); if (di >= 0 || i0 === 1) { φ = (y >= 0 ? 5 : -5) * (di + i); var j = 50, δ; @@ -386,55 +543,103 @@ function addProjectionsToD3(d3) { ay = robinsonConstants[i0][1]; by = robinsonConstants[i0 + 1][1]; cy = robinsonConstants[Math.min(19, i0 + 2)][1]; - φ -= (δ = (y >= 0 ? halfπ : -halfπ) * (by + di * (cy - ay) / 2 + di * di * (cy - 2 * by + ay) / 2) - y) * degrees; + φ -= (δ = (y >= 0 ? halfπ : -halfπ) * + (by + di * (cy - ay) / 2 + di * di * (cy - 2 * by + ay) / 2) - + y) * + degrees; } while (Math.abs(δ) > ε2 && --j > 0); break; } } while (--i0 >= 0); - var ax = robinsonConstants[i0][0], bx = robinsonConstants[i0 + 1][0], cx = robinsonConstants[Math.min(19, i0 + 2)][0]; - return [ x / (bx + di * (cx - ax) / 2 + di * di * (cx - 2 * bx + ax) / 2), φ * radians ]; + var ax = robinsonConstants[i0][0], + bx = robinsonConstants[i0 + 1][0], + cx = robinsonConstants[Math.min(19, i0 + 2)][0]; + return [ + x / (bx + di * (cx - ax) / 2 + di * di * (cx - 2 * bx + ax) / 2), + φ * radians + ]; }; (d3.geo.robinson = function() { return projection(robinson); }).raw = robinson; function sinusoidal(λ, φ) { - return [ λ * Math.cos(φ), φ ]; + return [λ * Math.cos(φ), φ]; } sinusoidal.invert = function(x, y) { - return [ x / Math.cos(y), y ]; + return [x / Math.cos(y), y]; }; (d3.geo.sinusoidal = function() { return projection(sinusoidal); }).raw = sinusoidal; function aitoff(λ, φ) { var cosφ = Math.cos(φ), sinciα = sinci(acos(cosφ * Math.cos(λ /= 2))); - return [ 2 * cosφ * Math.sin(λ) * sinciα, Math.sin(φ) * sinciα ]; + return [2 * cosφ * Math.sin(λ) * sinciα, Math.sin(φ) * sinciα]; } aitoff.invert = function(x, y) { if (x * x + 4 * y * y > π * π + ε) return; var λ = x, φ = y, i = 25; do { - var sinλ = Math.sin(λ), sinλ_2 = Math.sin(λ / 2), cosλ_2 = Math.cos(λ / 2), sinφ = Math.sin(φ), cosφ = Math.cos(φ), sin_2φ = Math.sin(2 * φ), sin2φ = sinφ * sinφ, cos2φ = cosφ * cosφ, sin2λ_2 = sinλ_2 * sinλ_2, C = 1 - cos2φ * cosλ_2 * cosλ_2, E = C ? acos(cosφ * cosλ_2) * Math.sqrt(F = 1 / C) : F = 0, F, fx = 2 * E * cosφ * sinλ_2 - x, fy = E * sinφ - y, δxδλ = F * (cos2φ * sin2λ_2 + E * cosφ * cosλ_2 * sin2φ), δxδφ = F * (.5 * sinλ * sin_2φ - E * 2 * sinφ * sinλ_2), δyδλ = F * .25 * (sin_2φ * sinλ_2 - E * sinφ * cos2φ * sinλ), δyδφ = F * (sin2φ * cosλ_2 + E * sin2λ_2 * cosφ), denominator = δxδφ * δyδλ - δyδφ * δxδλ; + var sinλ = Math.sin(λ), + sinλ_2 = Math.sin(λ / 2), + cosλ_2 = Math.cos(λ / 2), + sinφ = Math.sin(φ), + cosφ = Math.cos(φ), + sin_2φ = Math.sin(2 * φ), + sin2φ = sinφ * sinφ, + cos2φ = cosφ * cosφ, + sin2λ_2 = sinλ_2 * sinλ_2, + C = 1 - cos2φ * cosλ_2 * cosλ_2, + E = C ? acos(cosφ * cosλ_2) * Math.sqrt(F = 1 / C) : F = 0, + F, + fx = 2 * E * cosφ * sinλ_2 - x, + fy = E * sinφ - y, + δxδλ = F * (cos2φ * sin2λ_2 + E * cosφ * cosλ_2 * sin2φ), + δxδφ = F * (0.5 * sinλ * sin_2φ - E * 2 * sinφ * sinλ_2), + δyδλ = F * 0.25 * (sin_2φ * sinλ_2 - E * sinφ * cos2φ * sinλ), + δyδφ = F * (sin2φ * cosλ_2 + E * sin2λ_2 * cosφ), + denominator = δxδφ * δyδλ - δyδφ * δxδλ; if (!denominator) break; - var δλ = (fy * δxδφ - fx * δyδφ) / denominator, δφ = (fx * δyδλ - fy * δxδλ) / denominator; + var δλ = (fy * δxδφ - fx * δyδφ) / denominator, + δφ = (fx * δyδλ - fy * δxδλ) / denominator; λ -= δλ, φ -= δφ; } while ((Math.abs(δλ) > ε || Math.abs(δφ) > ε) && --i > 0); - return [ λ, φ ]; + return [λ, φ]; }; (d3.geo.aitoff = function() { return projection(aitoff); }).raw = aitoff; function winkel3(λ, φ) { var coordinates = aitoff(λ, φ); - return [ (coordinates[0] + λ / halfπ) / 2, (coordinates[1] + φ) / 2 ]; + return [(coordinates[0] + λ / halfπ) / 2, (coordinates[1] + φ) / 2]; } winkel3.invert = function(x, y) { var λ = x, φ = y, i = 25; do { - var cosφ = Math.cos(φ), sinφ = Math.sin(φ), sin_2φ = Math.sin(2 * φ), sin2φ = sinφ * sinφ, cos2φ = cosφ * cosφ, sinλ = Math.sin(λ), cosλ_2 = Math.cos(λ / 2), sinλ_2 = Math.sin(λ / 2), sin2λ_2 = sinλ_2 * sinλ_2, C = 1 - cos2φ * cosλ_2 * cosλ_2, E = C ? acos(cosφ * cosλ_2) * Math.sqrt(F = 1 / C) : F = 0, F, fx = .5 * (2 * E * cosφ * sinλ_2 + λ / halfπ) - x, fy = .5 * (E * sinφ + φ) - y, δxδλ = .5 * F * (cos2φ * sin2λ_2 + E * cosφ * cosλ_2 * sin2φ) + .5 / halfπ, δxδφ = F * (sinλ * sin_2φ / 4 - E * sinφ * sinλ_2), δyδλ = .125 * F * (sin_2φ * sinλ_2 - E * sinφ * cos2φ * sinλ), δyδφ = .5 * F * (sin2φ * cosλ_2 + E * sin2λ_2 * cosφ) + .5, denominator = δxδφ * δyδλ - δyδφ * δxδλ, δλ = (fy * δxδφ - fx * δyδφ) / denominator, δφ = (fx * δyδλ - fy * δxδλ) / denominator; + var cosφ = Math.cos(φ), + sinφ = Math.sin(φ), + sin_2φ = Math.sin(2 * φ), + sin2φ = sinφ * sinφ, + cos2φ = cosφ * cosφ, + sinλ = Math.sin(λ), + cosλ_2 = Math.cos(λ / 2), + sinλ_2 = Math.sin(λ / 2), + sin2λ_2 = sinλ_2 * sinλ_2, + C = 1 - cos2φ * cosλ_2 * cosλ_2, + E = C ? acos(cosφ * cosλ_2) * Math.sqrt(F = 1 / C) : F = 0, + F, + fx = 0.5 * (2 * E * cosφ * sinλ_2 + λ / halfπ) - x, + fy = 0.5 * (E * sinφ + φ) - y, + δxδλ = 0.5 * F * (cos2φ * sin2λ_2 + E * cosφ * cosλ_2 * sin2φ) + + 0.5 / halfπ, + δxδφ = F * (sinλ * sin_2φ / 4 - E * sinφ * sinλ_2), + δyδλ = 0.125 * F * (sin_2φ * sinλ_2 - E * sinφ * cos2φ * sinλ), + δyδφ = 0.5 * F * (sin2φ * cosλ_2 + E * sin2λ_2 * cosφ) + 0.5, + denominator = δxδφ * δyδλ - δyδφ * δxδλ, + δλ = (fy * δxδφ - fx * δyδφ) / denominator, + δφ = (fx * δyδλ - fy * δxδλ) / denominator; λ -= δλ, φ -= δφ; } while ((Math.abs(δλ) > ε || Math.abs(δφ) > ε) && --i > 0); - return [ λ, φ ]; + return [λ, φ]; }; (d3.geo.winkel3 = function() { return projection(winkel3); diff --git a/src/plots/geo/set_scale.js b/src/plots/geo/set_scale.js index 66ef39f0983..02e6d13f22e 100644 --- a/src/plots/geo/set_scale.js +++ b/src/plots/geo/set_scale.js @@ -5,112 +5,108 @@ * 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 d3 = require("d3"); +var clipPad = require("./constants").clipPad; -'use strict'; +function createGeoScale(geoLayout, graphSize) { + var projLayout = geoLayout.projection, + lonaxisLayout = geoLayout.lonaxis, + lataxisLayout = geoLayout.lataxis, + geoDomain = geoLayout.domain, + frameWidth = geoLayout.framewidth || 0; + + // width & height the geo div + var geoWidth = graphSize.w * (geoDomain.x[1] - geoDomain.x[0]), + geoHeight = graphSize.h * (geoDomain.y[1] - geoDomain.y[0]); + + // add padding around range to avoid aliasing + var lon0 = lonaxisLayout.range[0] + clipPad, + lon1 = lonaxisLayout.range[1] - clipPad, + lat0 = lataxisLayout.range[0] + clipPad, + lat1 = lataxisLayout.range[1] - clipPad, + lonfull0 = lonaxisLayout._fullRange[0] + clipPad, + lonfull1 = lonaxisLayout._fullRange[1] - clipPad, + latfull0 = lataxisLayout._fullRange[0] + clipPad, + latfull1 = lataxisLayout._fullRange[1] - clipPad; + + // initial translation (makes the math easier) + projLayout._translate0 = [ + graphSize.l + geoWidth / 2, + graphSize.t + geoHeight / 2 + ]; + + // center of the projection is given by + // the lon/lat ranges and the rotate angle + var dlon = lon1 - lon0, + dlat = lat1 - lat0, + c0 = [lon0 + dlon / 2, lat0 + dlat / 2], + r = projLayout._rotate; + + projLayout._center = [c0[0] + r[0], c0[1] + r[1]]; + + // needs a initial projection; it is called from makeProjection + var setScale = function(projection) { + var scale0 = projection.scale(), + translate0 = projLayout._translate0, + rangeBox = makeRangeBox(lon0, lat0, lon1, lat1), + fullRangeBox = makeRangeBox(lonfull0, latfull0, lonfull1, latfull1); + + var scale, translate, bounds, fullBounds; + + // Inspired by: http://stackoverflow.com/a/14654988/4068492 + // using the path determine the bounds of the current map and use + // these to determine better values for the scale and translation + function getScale(bounds) { + return Math.min( + scale0 * geoWidth / (bounds[1][0] - bounds[0][0]), + scale0 * geoHeight / (bounds[1][1] - bounds[0][1]) + ); + } + + // scale projection given how range box get deformed + // by the projection + bounds = getBounds(projection, rangeBox); + scale = getScale(bounds); + + // similarly, get scale at full range + fullBounds = getBounds(projection, fullRangeBox); + projLayout._fullScale = getScale(fullBounds); + + projection.scale(scale); + + // translate the projection so that the top-left corner + // of the range box is at the top-left corner of the viewbox + bounds = getBounds(projection, rangeBox); + translate = [ + translate0[0] - bounds[0][0] + frameWidth, + translate0[1] - bounds[0][1] + frameWidth + ]; + projLayout._translate = translate; + projection.translate(translate); -var d3 = require('d3'); + // clip regions out of the range box + // (these are clipping along horizontal/vertical lines) + bounds = getBounds(projection, rangeBox); + if (!geoLayout._isAlbersUsa) projection.clipExtent(bounds); -var clipPad = require('./constants').clipPad; + // adjust scale one more time with the 'scale' attribute + scale = projLayout.scale * scale; -function createGeoScale(geoLayout, graphSize) { - var projLayout = geoLayout.projection, - lonaxisLayout = geoLayout.lonaxis, - lataxisLayout = geoLayout.lataxis, - geoDomain = geoLayout.domain, - frameWidth = geoLayout.framewidth || 0; - - // width & height the geo div - var geoWidth = graphSize.w * (geoDomain.x[1] - geoDomain.x[0]), - geoHeight = graphSize.h * (geoDomain.y[1] - geoDomain.y[0]); - - // add padding around range to avoid aliasing - var lon0 = lonaxisLayout.range[0] + clipPad, - lon1 = lonaxisLayout.range[1] - clipPad, - lat0 = lataxisLayout.range[0] + clipPad, - lat1 = lataxisLayout.range[1] - clipPad, - lonfull0 = lonaxisLayout._fullRange[0] + clipPad, - lonfull1 = lonaxisLayout._fullRange[1] - clipPad, - latfull0 = lataxisLayout._fullRange[0] + clipPad, - latfull1 = lataxisLayout._fullRange[1] - clipPad; - - // initial translation (makes the math easier) - projLayout._translate0 = [ - graphSize.l + geoWidth / 2, graphSize.t + geoHeight / 2 - ]; + // set projection scale and save it + projLayout._scale = scale; + + // save the effective width & height of the geo framework + geoLayout._width = Math.round(bounds[1][0]) + frameWidth; + geoLayout._height = Math.round(bounds[1][1]) + frameWidth; + // save the margin length induced by the map scaling + geoLayout._marginX = (geoWidth - Math.round(bounds[1][0])) / 2; + geoLayout._marginY = (geoHeight - Math.round(bounds[1][1])) / 2; + }; - // center of the projection is given by - // the lon/lat ranges and the rotate angle - var dlon = lon1 - lon0, - dlat = lat1 - lat0, - c0 = [lon0 + dlon / 2, lat0 + dlat / 2], - r = projLayout._rotate; - - projLayout._center = [c0[0] + r[0], c0[1] + r[1]]; - - // needs a initial projection; it is called from makeProjection - var setScale = function(projection) { - var scale0 = projection.scale(), - translate0 = projLayout._translate0, - rangeBox = makeRangeBox(lon0, lat0, lon1, lat1), - fullRangeBox = makeRangeBox(lonfull0, latfull0, lonfull1, latfull1); - - var scale, translate, bounds, fullBounds; - - // Inspired by: http://stackoverflow.com/a/14654988/4068492 - // using the path determine the bounds of the current map and use - // these to determine better values for the scale and translation - - function getScale(bounds) { - return Math.min( - scale0 * geoWidth / (bounds[1][0] - bounds[0][0]), - scale0 * geoHeight / (bounds[1][1] - bounds[0][1]) - ); - } - - // scale projection given how range box get deformed - // by the projection - bounds = getBounds(projection, rangeBox); - scale = getScale(bounds); - - // similarly, get scale at full range - fullBounds = getBounds(projection, fullRangeBox); - projLayout._fullScale = getScale(fullBounds); - - projection.scale(scale); - - // translate the projection so that the top-left corner - // of the range box is at the top-left corner of the viewbox - bounds = getBounds(projection, rangeBox); - translate = [ - translate0[0] - bounds[0][0] + frameWidth, - translate0[1] - bounds[0][1] + frameWidth - ]; - projLayout._translate = translate; - projection.translate(translate); - - // clip regions out of the range box - // (these are clipping along horizontal/vertical lines) - bounds = getBounds(projection, rangeBox); - if(!geoLayout._isAlbersUsa) projection.clipExtent(bounds); - - // adjust scale one more time with the 'scale' attribute - scale = projLayout.scale * scale; - - // set projection scale and save it - projLayout._scale = scale; - - // save the effective width & height of the geo framework - geoLayout._width = Math.round(bounds[1][0]) + frameWidth; - geoLayout._height = Math.round(bounds[1][1]) + frameWidth; - - // save the margin length induced by the map scaling - geoLayout._marginX = (geoWidth - Math.round(bounds[1][0])) / 2; - geoLayout._marginY = (geoHeight - Math.round(bounds[1][1])) / 2; - }; - - return setScale; + return setScale; } module.exports = createGeoScale; @@ -118,32 +114,34 @@ module.exports = createGeoScale; // polygon GeoJSON corresponding to lon/lat range box // with well-defined direction function makeRangeBox(lon0, lat0, lon1, lat1) { - var dlon4 = (lon1 - lon0) / 4; - - // TODO is this enough to handle ALL cases? - // -- this makes scaling less precise than using d3.geo.graticule - // as great circles can overshoot the boundary - // (that's not a big deal I think) - return { - type: 'Polygon', - coordinates: [ - [ [lon0, lat0], - [lon0, lat1], - [lon0 + dlon4, lat1], - [lon0 + 2 * dlon4, lat1], - [lon0 + 3 * dlon4, lat1], - [lon1, lat1], - [lon1, lat0], - [lon1 - dlon4, lat0], - [lon1 - 2 * dlon4, lat0], - [lon1 - 3 * dlon4, lat0], - [lon0, lat0] ] - ] - }; + var dlon4 = (lon1 - lon0) / 4; + + // TODO is this enough to handle ALL cases? + // -- this makes scaling less precise than using d3.geo.graticule + // as great circles can overshoot the boundary + // (that's not a big deal I think) + return { + type: "Polygon", + coordinates: [ + [ + [lon0, lat0], + [lon0, lat1], + [lon0 + dlon4, lat1], + [lon0 + 2 * dlon4, lat1], + [lon0 + 3 * dlon4, lat1], + [lon1, lat1], + [lon1, lat0], + [lon1 - dlon4, lat0], + [lon1 - 2 * dlon4, lat0], + [lon1 - 3 * dlon4, lat0], + [lon0, lat0] + ] + ] + }; } // bounds array [[top, left], [bottom, right]] // of the lon/lat range box function getBounds(projection, rangeBox) { - return d3.geo.path().projection(projection).bounds(rangeBox); + return d3.geo.path().projection(projection).bounds(rangeBox); } diff --git a/src/plots/geo/zoom.js b/src/plots/geo/zoom.js index dd26c33bae8..ff1ec7bbece 100644 --- a/src/plots/geo/zoom.js +++ b/src/plots/geo/zoom.js @@ -5,273 +5,288 @@ * 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 d3 = require('d3'); +"use strict"; +var d3 = require("d3"); var radians = Math.PI / 180, - degrees = 180 / Math.PI, - zoomstartStyle = { cursor: 'pointer' }, - zoomendStyle = { cursor: 'auto' }; - + degrees = 180 / Math.PI, + zoomstartStyle = { cursor: "pointer" }, + zoomendStyle = { cursor: "auto" }; function createGeoZoom(geo, geoLayout) { - var zoomConstructor; - - if(geoLayout._isScoped) zoomConstructor = zoomScoped; - else if(geoLayout._clipAngle) zoomConstructor = zoomClipped; - else zoomConstructor = zoomNonClipped; + var zoomConstructor; - // TODO add a conic-specific zoom + if (geoLayout._isScoped) zoomConstructor = zoomScoped; + else if (geoLayout._clipAngle) zoomConstructor = zoomClipped; + else zoomConstructor = zoomNonClipped; - return zoomConstructor(geo, geoLayout.projection); + // TODO add a conic-specific zoom + return zoomConstructor(geo, geoLayout.projection); } module.exports = createGeoZoom; // common to all zoom types function initZoom(projection, projLayout) { - var fullScale = projLayout._fullScale; + var fullScale = projLayout._fullScale; - return d3.behavior.zoom() - .translate(projection.translate()) - .scale(projection.scale()) - .scaleExtent([0.5 * fullScale, 100 * fullScale]); + return d3.behavior + .zoom() + .translate(projection.translate()) + .scale(projection.scale()) + .scaleExtent([0.5 * fullScale, 100 * fullScale]); } // zoom for scoped projections function zoomScoped(geo, projLayout) { - var projection = geo.projection, - zoom = initZoom(projection, projLayout); + var projection = geo.projection, zoom = initZoom(projection, projLayout); - function handleZoomstart() { - d3.select(this).style(zoomstartStyle); - } + function handleZoomstart() { + d3.select(this).style(zoomstartStyle); + } - function handleZoom() { - projection - .scale(d3.event.scale) - .translate(d3.event.translate); + function handleZoom() { + projection.scale(d3.event.scale).translate(d3.event.translate); - geo.render(); - } + geo.render(); + } - function handleZoomend() { - d3.select(this).style(zoomendStyle); - } + function handleZoomend() { + d3.select(this).style(zoomendStyle); + } - zoom - .on('zoomstart', handleZoomstart) - .on('zoom', handleZoom) - .on('zoomend', handleZoomend); + zoom + .on("zoomstart", handleZoomstart) + .on("zoom", handleZoom) + .on("zoomend", handleZoomend); - return zoom; + return zoom; } // zoom for non-clipped projections function zoomNonClipped(geo, projLayout) { - var projection = geo.projection, - zoom = initZoom(projection, projLayout); - - var INSIDETOLORANCEPXS = 2; - - var mouse0, rotate0, translate0, lastRotate, zoomPoint, - mouse1, rotate1, point1; - - function position(x) { return projection.invert(x); } - - function outside(x) { - var pt = projection(position(x)); - return (Math.abs(pt[0] - x[0]) > INSIDETOLORANCEPXS || - Math.abs(pt[1] - x[1]) > INSIDETOLORANCEPXS); + var projection = geo.projection, zoom = initZoom(projection, projLayout); + + var INSIDETOLORANCEPXS = 2; + + var mouse0, + rotate0, + translate0, + lastRotate, + zoomPoint, + mouse1, + rotate1, + point1; + + function position(x) { + return projection.invert(x); + } + + function outside(x) { + var pt = projection(position(x)); + return Math.abs(pt[0] - x[0]) > INSIDETOLORANCEPXS || + Math.abs(pt[1] - x[1]) > INSIDETOLORANCEPXS; + } + + function handleZoomstart() { + d3.select(this).style(zoomstartStyle); + + mouse0 = d3.mouse(this); + rotate0 = projection.rotate(); + translate0 = projection.translate(); + lastRotate = rotate0; + zoomPoint = position(mouse0); + } + + function handleZoom() { + mouse1 = d3.mouse(this); + + if (outside(mouse0)) { + zoom.scale(projection.scale()); + zoom.translate(projection.translate()); + return; } - function handleZoomstart() { - d3.select(this).style(zoomstartStyle); - - mouse0 = d3.mouse(this); - rotate0 = projection.rotate(); - translate0 = projection.translate(); - lastRotate = rotate0; - zoomPoint = position(mouse0); + projection.scale(d3.event.scale); + + projection.translate([translate0[0], d3.event.translate[1]]); + + if (!zoomPoint) { + mouse0 = mouse1; + zoomPoint = position(mouse0); + } else if (position(mouse1)) { + point1 = position(mouse1); + rotate1 = [ + lastRotate[0] + (point1[0] - zoomPoint[0]), + rotate0[1], + rotate0[2] + ]; + projection.rotate(rotate1); + lastRotate = rotate1; } - function handleZoom() { - mouse1 = d3.mouse(this); + geo.render(); + } - if(outside(mouse0)) { - zoom.scale(projection.scale()); - zoom.translate(projection.translate()); - return; - } + function handleZoomend() { + d3.select(this).style(zoomendStyle); + // or something like + // http://www.jasondavies.com/maps/gilbert/ + // ... a little harder with multiple base layers + } - projection.scale(d3.event.scale); - - projection.translate([translate0[0], d3.event.translate[1]]); - - if(!zoomPoint) { - mouse0 = mouse1; - zoomPoint = position(mouse0); - } - else if(position(mouse1)) { - point1 = position(mouse1); - rotate1 = [lastRotate[0] + (point1[0] - zoomPoint[0]), rotate0[1], rotate0[2]]; - projection.rotate(rotate1); - lastRotate = rotate1; - } + zoom + .on("zoomstart", handleZoomstart) + .on("zoom", handleZoom) + .on("zoomend", handleZoomend); - geo.render(); - } - - function handleZoomend() { - d3.select(this).style(zoomendStyle); - - // or something like - // http://www.jasondavies.com/maps/gilbert/ - // ... a little harder with multiple base layers - } - - zoom - .on('zoomstart', handleZoomstart) - .on('zoom', handleZoom) - .on('zoomend', handleZoomend); - - return zoom; + return zoom; } // zoom for clipped projections // inspired by https://www.jasondavies.com/maps/d3.geo.zoom.js function zoomClipped(geo, projLayout) { - var projection = geo.projection, - view = {r: projection.rotate(), k: projection.scale()}, - zoom = initZoom(projection, projLayout), - event = d3_eventDispatch(zoom, 'zoomstart', 'zoom', 'zoomend'), - zooming = 0, - zoomOn = zoom.on; - - var zoomPoint; - - zoom.on('zoomstart', function() { - d3.select(this).style(zoomstartStyle); - - var mouse0 = d3.mouse(this), - rotate0 = projection.rotate(), - lastRotate = rotate0, - translate0 = projection.translate(), - q = quaternionFromEuler(rotate0); - - zoomPoint = position(projection, mouse0); - - zoomOn.call(zoom, 'zoom', function() { - var mouse1 = d3.mouse(this); - - projection.scale(view.k = d3.event.scale); - - if(!zoomPoint) { - // if no zoomPoint, the mouse wasn't over the actual geography yet - // maybe this point is the start... we'll find out next time! - mouse0 = mouse1; - zoomPoint = position(projection, mouse0); - } - // check if the point is on the map - // if not, don't do anything new but scale - // if it is, then we can assume between will exist below - // so we don't need the 'bank' function, whatever that is. - // TODO: is this right? - else if(position(projection, mouse1)) { - // go back to original projection temporarily - // except for scale... that's kind of independent? - projection - .rotate(rotate0) - .translate(translate0); - - // calculate the new params - var point1 = position(projection, mouse1), - between = rotateBetween(zoomPoint, point1), - newEuler = eulerFromQuaternion(multiply(q, between)), - rotateAngles = view.r = unRoll(newEuler, zoomPoint, lastRotate); - - if(!isFinite(rotateAngles[0]) || !isFinite(rotateAngles[1]) || - !isFinite(rotateAngles[2])) { - rotateAngles = lastRotate; - } - - // update the projection - projection.rotate(rotateAngles); - lastRotate = rotateAngles; - } - - zoomed(event.of(this, arguments)); - }); - - zoomstarted(event.of(this, arguments)); + var projection = geo.projection, + view = { r: projection.rotate(), k: projection.scale() }, + zoom = initZoom(projection, projLayout), + event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"), + zooming = 0, + zoomOn = zoom.on; + + var zoomPoint; + + zoom + .on("zoomstart", function() { + d3.select(this).style(zoomstartStyle); + + var mouse0 = d3.mouse(this), + rotate0 = projection.rotate(), + lastRotate = rotate0, + translate0 = projection.translate(), + q = quaternionFromEuler(rotate0); + + zoomPoint = position(projection, mouse0); + + zoomOn.call(zoom, "zoom", function() { + var mouse1 = d3.mouse(this); + + projection.scale(view.k = d3.event.scale); + + if (!zoomPoint) { + // if no zoomPoint, the mouse wasn't over the actual geography yet + // maybe this point is the start... we'll find out next time! + mouse0 = mouse1; + zoomPoint = position(projection, mouse0); + } else if (position(projection, mouse1)) { + // check if the point is on the map + // if not, don't do anything new but scale + // if it is, then we can assume between will exist below + // so we don't need the 'bank' function, whatever that is. + // TODO: is this right? + // go back to original projection temporarily + // except for scale... that's kind of independent? + projection.rotate(rotate0).translate(translate0); + + // calculate the new params + var point1 = position(projection, mouse1), + between = rotateBetween(zoomPoint, point1), + newEuler = eulerFromQuaternion(multiply(q, between)), + rotateAngles = view.r = unRoll(newEuler, zoomPoint, lastRotate); + + if ( + !isFinite(rotateAngles[0]) || + !isFinite(rotateAngles[1]) || + !isFinite(rotateAngles[2]) + ) { + rotateAngles = lastRotate; + } + + // update the projection + projection.rotate(rotateAngles); + lastRotate = rotateAngles; + } + + zoomed(event.of(this, arguments)); + }); + + zoomstarted(event.of(this, arguments)); }) - .on('zoomend', function() { - d3.select(this).style(zoomendStyle); - zoomOn.call(zoom, 'zoom', null); - zoomended(event.of(this, arguments)); + .on("zoomend", function() { + d3.select(this).style(zoomendStyle); + zoomOn.call(zoom, "zoom", null); + zoomended(event.of(this, arguments)); }) - .on('zoom.redraw', function() { - geo.render(); + .on("zoom.redraw", function() { + geo.render(); }); - function zoomstarted(dispatch) { - if(!zooming++) dispatch({type: 'zoomstart'}); - } + function zoomstarted(dispatch) { + if (!zooming++) dispatch({ type: "zoomstart" }); + } - function zoomed(dispatch) { - dispatch({type: 'zoom'}); - } + function zoomed(dispatch) { + dispatch({ type: "zoom" }); + } - function zoomended(dispatch) { - if(!--zooming) dispatch({type: 'zoomend'}); - } + function zoomended(dispatch) { + if (!--zooming) dispatch({ type: "zoomend" }); + } - return d3.rebind(zoom, event, 'on'); + return d3.rebind(zoom, event, "on"); } // -- helper functions for zoomClipped - function position(projection, point) { - var spherical = projection.invert(point); - return spherical && isFinite(spherical[0]) && isFinite(spherical[1]) && cartesian(spherical); + var spherical = projection.invert(point); + return spherical && + isFinite(spherical[0]) && + isFinite(spherical[1]) && + cartesian(spherical); } function quaternionFromEuler(euler) { - var lambda = 0.5 * euler[0] * radians, - phi = 0.5 * euler[1] * radians, - gamma = 0.5 * euler[2] * radians, - sinLambda = Math.sin(lambda), cosLambda = Math.cos(lambda), - sinPhi = Math.sin(phi), cosPhi = Math.cos(phi), - sinGamma = Math.sin(gamma), cosGamma = Math.cos(gamma); - return [ - cosLambda * cosPhi * cosGamma + sinLambda * sinPhi * sinGamma, - sinLambda * cosPhi * cosGamma - cosLambda * sinPhi * sinGamma, - cosLambda * sinPhi * cosGamma + sinLambda * cosPhi * sinGamma, - cosLambda * cosPhi * sinGamma - sinLambda * sinPhi * cosGamma - ]; + var lambda = 0.5 * euler[0] * radians, + phi = 0.5 * euler[1] * radians, + gamma = 0.5 * euler[2] * radians, + sinLambda = Math.sin(lambda), + cosLambda = Math.cos(lambda), + sinPhi = Math.sin(phi), + cosPhi = Math.cos(phi), + sinGamma = Math.sin(gamma), + cosGamma = Math.cos(gamma); + return [ + cosLambda * cosPhi * cosGamma + sinLambda * sinPhi * sinGamma, + sinLambda * cosPhi * cosGamma - cosLambda * sinPhi * sinGamma, + cosLambda * sinPhi * cosGamma + sinLambda * cosPhi * sinGamma, + cosLambda * cosPhi * sinGamma - sinLambda * sinPhi * cosGamma + ]; } function multiply(a, b) { - var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], - b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; - return [ - a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3, - a0 * b1 + a1 * b0 + a2 * b3 - a3 * b2, - a0 * b2 - a1 * b3 + a2 * b0 + a3 * b1, - a0 * b3 + a1 * b2 - a2 * b1 + a3 * b0 - ]; + var a0 = a[0], + a1 = a[1], + a2 = a[2], + a3 = a[3], + b0 = b[0], + b1 = b[1], + b2 = b[2], + b3 = b[3]; + return [ + a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3, + a0 * b1 + a1 * b0 + a2 * b3 - a3 * b2, + a0 * b2 - a1 * b3 + a2 * b0 + a3 * b1, + a0 * b3 + a1 * b2 - a2 * b1 + a3 * b0 + ]; } function rotateBetween(a, b) { - if(!a || !b) return; - var axis = cross(a, b), - norm = Math.sqrt(dot(axis, axis)), - halfgamma = 0.5 * Math.acos(Math.max(-1, Math.min(1, dot(a, b)))), - k = Math.sin(halfgamma) / norm; - return norm && [Math.cos(halfgamma), axis[2] * k, -axis[1] * k, axis[0] * k]; + if (!a || !b) return; + var axis = cross(a, b), + norm = Math.sqrt(dot(axis, axis)), + halfgamma = 0.5 * Math.acos(Math.max(-1, Math.min(1, dot(a, b)))), + k = Math.sin(halfgamma) / norm; + return norm && + [Math.cos(halfgamma), axis[2] * k, (-axis[1]) * k, axis[0] * k]; } // input: @@ -284,105 +299,110 @@ function rotateBetween(a, b) { // note that this doesn't depend on the particular projection, // just on the rotation angles function unRoll(rotateAngles, pt, lastRotate) { - // calculate the fixed point transformed by these Euler angles - // but with the desired roll undone - var ptRotated = rotateCartesian(pt, 2, rotateAngles[0]); - ptRotated = rotateCartesian(ptRotated, 1, rotateAngles[1]); - ptRotated = rotateCartesian(ptRotated, 0, rotateAngles[2] - lastRotate[2]); - - var x = pt[0], - y = pt[1], - z = pt[2], - f = ptRotated[0], - g = ptRotated[1], - h = ptRotated[2], - - // the following essentially solves: - // ptRotated = rotateCartesian(rotateCartesian(pt, 2, newYaw), 1, newPitch) - // for newYaw and newPitch, as best it can - theta = Math.atan2(y, x) * degrees, - a = Math.sqrt(x * x + y * y), - b, - newYaw1; - - if(Math.abs(g) > a) { - newYaw1 = (g > 0 ? 90 : -90) - theta; - b = 0; - } else { - newYaw1 = Math.asin(g / a) * degrees - theta; - b = Math.sqrt(a * a - g * g); - } - - var newYaw2 = 180 - newYaw1 - 2 * theta, - newPitch1 = (Math.atan2(h, f) - Math.atan2(z, b)) * degrees, - newPitch2 = (Math.atan2(h, f) - Math.atan2(z, -b)) * degrees; - - // which is closest to lastRotate[0,1]: newYaw/Pitch or newYaw2/Pitch2? - var dist1 = angleDistance(lastRotate[0], lastRotate[1], newYaw1, newPitch1), - dist2 = angleDistance(lastRotate[0], lastRotate[1], newYaw2, newPitch2); - - if(dist1 <= dist2) return [newYaw1, newPitch1, lastRotate[2]]; - else return [newYaw2, newPitch2, lastRotate[2]]; + // calculate the fixed point transformed by these Euler angles + // but with the desired roll undone + var ptRotated = rotateCartesian(pt, 2, rotateAngles[0]); + ptRotated = rotateCartesian(ptRotated, 1, rotateAngles[1]); + ptRotated = rotateCartesian(ptRotated, 0, rotateAngles[2] - lastRotate[2]); + + var x = pt[0], + y = pt[1], + z = pt[2], + f = ptRotated[0], + g = ptRotated[1], + h = ptRotated[2], + // the following essentially solves: + // ptRotated = rotateCartesian(rotateCartesian(pt, 2, newYaw), 1, newPitch) + // for newYaw and newPitch, as best it can + theta = Math.atan2(y, x) * degrees, + a = Math.sqrt(x * x + y * y), + b, + newYaw1; + + if (Math.abs(g) > a) { + newYaw1 = (g > 0 ? 90 : -90) - theta; + b = 0; + } else { + newYaw1 = Math.asin(g / a) * degrees - theta; + b = Math.sqrt(a * a - g * g); + } + + var newYaw2 = 180 - newYaw1 - 2 * theta, + newPitch1 = (Math.atan2(h, f) - Math.atan2(z, b)) * degrees, + newPitch2 = (Math.atan2(h, f) - Math.atan2(z, -b)) * degrees; + + // which is closest to lastRotate[0,1]: newYaw/Pitch or newYaw2/Pitch2? + var dist1 = angleDistance(lastRotate[0], lastRotate[1], newYaw1, newPitch1), + dist2 = angleDistance(lastRotate[0], lastRotate[1], newYaw2, newPitch2); + + if (dist1 <= dist2) return [newYaw1, newPitch1, lastRotate[2]]; + else return [newYaw2, newPitch2, lastRotate[2]]; } function angleDistance(yaw0, pitch0, yaw1, pitch1) { - var dYaw = angleMod(yaw1 - yaw0), - dPitch = angleMod(pitch1 - pitch0); - return Math.sqrt(dYaw * dYaw + dPitch * dPitch); + var dYaw = angleMod(yaw1 - yaw0), dPitch = angleMod(pitch1 - pitch0); + return Math.sqrt(dYaw * dYaw + dPitch * dPitch); } // reduce an angle in degrees to [-180,180] function angleMod(angle) { - return (angle % 360 + 540) % 360 - 180; + return (angle % 360 + 540) % 360 - 180; } // rotate a cartesian vector // axis is 0 (x), 1 (y), or 2 (z) // angle is in degrees function rotateCartesian(vector, axis, angle) { - var angleRads = angle * radians, - vectorOut = vector.slice(), - ax1 = (axis === 0) ? 1 : 0, - ax2 = (axis === 2) ? 1 : 2, - cosa = Math.cos(angleRads), - sina = Math.sin(angleRads); + var angleRads = angle * radians, + vectorOut = vector.slice(), + ax1 = axis === 0 ? 1 : 0, + ax2 = axis === 2 ? 1 : 2, + cosa = Math.cos(angleRads), + sina = Math.sin(angleRads); - vectorOut[ax1] = vector[ax1] * cosa - vector[ax2] * sina; - vectorOut[ax2] = vector[ax2] * cosa + vector[ax1] * sina; + vectorOut[ax1] = vector[ax1] * cosa - vector[ax2] * sina; + vectorOut[ax2] = vector[ax2] * cosa + vector[ax1] * sina; - return vectorOut; + return vectorOut; } function eulerFromQuaternion(q) { - return [ - Math.atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees, - Math.asin(Math.max(-1, Math.min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees, - Math.atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees - ]; + return [ + Math.atan2( + 2 * (q[0] * q[1] + q[2] * q[3]), + 1 - 2 * (q[1] * q[1] + q[2] * q[2]) + ) * + degrees, + Math.asin(Math.max(-1, Math.min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * + degrees, + Math.atan2( + 2 * (q[0] * q[3] + q[1] * q[2]), + 1 - 2 * (q[2] * q[2] + q[3] * q[3]) + ) * + degrees + ]; } function cartesian(spherical) { - var lambda = spherical[0] * radians, - phi = spherical[1] * radians, - cosPhi = Math.cos(phi); - return [ - cosPhi * Math.cos(lambda), - cosPhi * Math.sin(lambda), - Math.sin(phi) - ]; + var lambda = spherical[0] * radians, + phi = spherical[1] * radians, + cosPhi = Math.cos(phi); + return [cosPhi * Math.cos(lambda), cosPhi * Math.sin(lambda), Math.sin(phi)]; } function dot(a, b) { - var s = 0; - for(var i = 0, n = a.length; i < n; ++i) s += a[i] * b[i]; - return s; + var s = 0; + for (var i = 0, n = a.length; i < n; ++i) { + s += a[i] * b[i]; + } + return s; } function cross(a, b) { - return [ - a[1] * b[2] - a[2] * b[1], - a[2] * b[0] - a[0] * b[2], - a[0] * b[1] - a[1] * b[0] - ]; + return [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0] + ]; } // Like d3.dispatch, but for custom events abstracting native UI events. These @@ -390,36 +410,36 @@ function cross(a, b) { // the svg:g element containing the brush) and the standard arguments `d` (the // target element's data) and `i` (the selection index of the target element). function d3_eventDispatch(target) { - var i = 0, - n = arguments.length, - argumentz = []; - - while(++i < n) argumentz.push(arguments[i]); - - var dispatch = d3.dispatch.apply(null, argumentz); - - // Creates a dispatch context for the specified `thiz` (typically, the target - // DOM element that received the source event) and `argumentz` (typically, the - // data `d` and index `i` of the target element). The returned function can be - // used to dispatch an event to any registered listeners; the function takes a - // single argument as input, being the event to dispatch. The event must have - // a "type" attribute which corresponds to a type registered in the - // constructor. This context will automatically populate the "sourceEvent" and - // "target" attributes of the event, as well as setting the `d3.event` global - // for the duration of the notification. - dispatch.of = function(thiz, argumentz) { - return function(e1) { - var e0; - try { - e0 = e1.sourceEvent = d3.event; - e1.target = target; - d3.event = e1; - dispatch[e1.type].apply(thiz, argumentz); - } finally { - d3.event = e0; - } - }; + var i = 0, n = arguments.length, argumentz = []; + + while (++i < n) { + argumentz.push(arguments[i]); + } + + var dispatch = d3.dispatch.apply(null, argumentz); + + // Creates a dispatch context for the specified `thiz` (typically, the target + // DOM element that received the source event) and `argumentz` (typically, the + // data `d` and index `i` of the target element). The returned function can be + // used to dispatch an event to any registered listeners; the function takes a + // single argument as input, being the event to dispatch. The event must have + // a "type" attribute which corresponds to a type registered in the + // constructor. This context will automatically populate the "sourceEvent" and + // "target" attributes of the event, as well as setting the `d3.event` global + // for the duration of the notification. + dispatch.of = function(thiz, argumentz) { + return function(e1) { + var e0; + try { + e0 = e1.sourceEvent = d3.event; + e1.target = target; + d3.event = e1; + dispatch[e1.type].apply(thiz, argumentz); + } finally { + d3.event = e0; + } }; + }; - return dispatch; + return dispatch; } diff --git a/src/plots/geo/zoom_reset.js b/src/plots/geo/zoom_reset.js index f022197219e..52652b71aa7 100644 --- a/src/plots/geo/zoom_reset.js +++ b/src/plots/geo/zoom_reset.js @@ -5,29 +5,25 @@ * 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 Fx = require('../cartesian/graph_interact'); +"use strict"; +var Fx = require("../cartesian/graph_interact"); function createGeoZoomReset(geo, geoLayout) { - var projection = geo.projection, - zoom = geo.zoom; + var projection = geo.projection, zoom = geo.zoom; - var zoomReset = function() { - geo.makeProjection(geoLayout); - geo.makePath(); + var zoomReset = function() { + geo.makeProjection(geoLayout); + geo.makePath(); - zoom.scale(projection.scale()); - zoom.translate(projection.translate()); + zoom.scale(projection.scale()); + zoom.translate(projection.translate()); - Fx.loneUnhover(geo.hoverContainer); + Fx.loneUnhover(geo.hoverContainer); - geo.render(); - }; + geo.render(); + }; - return zoomReset; + return zoomReset; } module.exports = createGeoZoomReset; diff --git a/src/plots/gl2d/camera.js b/src/plots/gl2d/camera.js index 405795b6b57..0cc810521c7 100644 --- a/src/plots/gl2d/camera.js +++ b/src/plots/gl2d/camera.js @@ -5,167 +5,157 @@ * 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 mouseChange = require('mouse-change'); -var mouseWheel = require('mouse-wheel'); +"use strict"; +var mouseChange = require("mouse-change"); +var mouseWheel = require("mouse-wheel"); module.exports = createCamera; function Camera2D(element, plot) { - this.element = element; - this.plot = plot; - this.mouseListener = null; - this.wheelListener = null; - this.lastInputTime = Date.now(); - this.lastPos = [0, 0]; - this.boxEnabled = false; - this.boxStart = [0, 0]; - this.boxEnd = [0, 0]; + this.element = element; + this.plot = plot; + this.mouseListener = null; + this.wheelListener = null; + this.lastInputTime = Date.now(); + this.lastPos = [0, 0]; + this.boxEnabled = false; + this.boxStart = [0, 0]; + this.boxEnd = [0, 0]; } - function createCamera(scene) { - var element = scene.mouseContainer, - plot = scene.glplot, - result = new Camera2D(element, plot); - - function unSetAutoRange() { - scene.xaxis.autorange = false; - scene.yaxis.autorange = false; + var element = scene.mouseContainer, + plot = scene.glplot, + result = new Camera2D(element, plot); + + function unSetAutoRange() { + scene.xaxis.autorange = false; + scene.yaxis.autorange = false; + } + + result.mouseListener = mouseChange(element, function(buttons, x, y) { + var dataBox = scene.calcDataBox(), viewBox = plot.viewBox; + + var lastX = result.lastPos[0], lastY = result.lastPos[1]; + + x *= plot.pixelRatio; + y *= plot.pixelRatio; + + // mouseChange gives y about top; convert to about bottom + y = viewBox[3] - viewBox[1] - y; + + function updateRange(i0, start, end) { + var range0 = Math.min(start, end), range1 = Math.max(start, end); + + if (range0 !== range1) { + dataBox[i0] = range0; + dataBox[i0 + 2] = range1; + result.dataBox = dataBox; + scene.setRanges(dataBox); + } else { + scene.selectBox.selectBox = [0, 0, 1, 1]; + scene.glplot.setDirty(); + } } - result.mouseListener = mouseChange(element, function(buttons, x, y) { - var dataBox = scene.calcDataBox(), - viewBox = plot.viewBox; - - var lastX = result.lastPos[0], - lastY = result.lastPos[1]; - - x *= plot.pixelRatio; - y *= plot.pixelRatio; - - // mouseChange gives y about top; convert to about bottom - y = (viewBox[3] - viewBox[1]) - y; - - function updateRange(i0, start, end) { - var range0 = Math.min(start, end), - range1 = Math.max(start, end); - - if(range0 !== range1) { - dataBox[i0] = range0; - dataBox[i0 + 2] = range1; - result.dataBox = dataBox; - scene.setRanges(dataBox); - } - else { - scene.selectBox.selectBox = [0, 0, 1, 1]; - scene.glplot.setDirty(); - } + switch (scene.fullLayout.dragmode) { + case "zoom": + if (buttons) { + var dataX = x / + (viewBox[2] - viewBox[0]) * + (dataBox[2] - dataBox[0]) + + dataBox[0]; + var dataY = y / + (viewBox[3] - viewBox[1]) * + (dataBox[3] - dataBox[1]) + + dataBox[1]; + + if (!result.boxEnabled) { + result.boxStart[0] = dataX; + result.boxStart[1] = dataY; + } + + result.boxEnd[0] = dataX; + result.boxEnd[1] = dataY; + + result.boxEnabled = true; + } else if (result.boxEnabled) { + updateRange(0, result.boxStart[0], result.boxEnd[0]); + updateRange(1, result.boxStart[1], result.boxEnd[1]); + unSetAutoRange(); + result.boxEnabled = false; + scene.relayoutCallback(); } - - switch(scene.fullLayout.dragmode) { - case 'zoom': - if(buttons) { - var dataX = x / - (viewBox[2] - viewBox[0]) * (dataBox[2] - dataBox[0]) + - dataBox[0]; - var dataY = y / - (viewBox[3] - viewBox[1]) * (dataBox[3] - dataBox[1]) + - dataBox[1]; - - if(!result.boxEnabled) { - result.boxStart[0] = dataX; - result.boxStart[1] = dataY; - } - - result.boxEnd[0] = dataX; - result.boxEnd[1] = dataY; - - result.boxEnabled = true; - } - else if(result.boxEnabled) { - updateRange(0, result.boxStart[0], result.boxEnd[0]); - updateRange(1, result.boxStart[1], result.boxEnd[1]); - unSetAutoRange(); - result.boxEnabled = false; - scene.relayoutCallback(); - } - break; - - case 'pan': - result.boxEnabled = false; - - if(buttons) { - var dx = (lastX - x) * (dataBox[2] - dataBox[0]) / - (plot.viewBox[2] - plot.viewBox[0]); - var dy = (lastY - y) * (dataBox[3] - dataBox[1]) / - (plot.viewBox[3] - plot.viewBox[1]); - - dataBox[0] += dx; - dataBox[2] += dx; - dataBox[1] += dy; - dataBox[3] += dy; - - scene.setRanges(dataBox); - - result.panning = true; - result.lastInputTime = Date.now(); - unSetAutoRange(); - scene.cameraChanged(); - scene.handleAnnotations(); - } - else if(result.panning) { - result.panning = false; - scene.relayoutCallback(); - } - break; + break; + + case "pan": + result.boxEnabled = false; + + if (buttons) { + var dx = (lastX - x) * + (dataBox[2] - dataBox[0]) / + (plot.viewBox[2] - plot.viewBox[0]); + var dy = (lastY - y) * + (dataBox[3] - dataBox[1]) / + (plot.viewBox[3] - plot.viewBox[1]); + + dataBox[0] += dx; + dataBox[2] += dx; + dataBox[1] += dy; + dataBox[3] += dy; + + scene.setRanges(dataBox); + + result.panning = true; + result.lastInputTime = Date.now(); + unSetAutoRange(); + scene.cameraChanged(); + scene.handleAnnotations(); + } else if (result.panning) { + result.panning = false; + scene.relayoutCallback(); } + break; + } - result.lastPos[0] = x; - result.lastPos[1] = y; - }); + result.lastPos[0] = x; + result.lastPos[1] = y; + }); - result.wheelListener = mouseWheel(element, function(dx, dy) { - var dataBox = scene.calcDataBox(), - viewBox = plot.viewBox; + result.wheelListener = mouseWheel(element, function(dx, dy) { + var dataBox = scene.calcDataBox(), viewBox = plot.viewBox; - var lastX = result.lastPos[0], - lastY = result.lastPos[1]; + var lastX = result.lastPos[0], lastY = result.lastPos[1]; - switch(scene.fullLayout.dragmode) { - case 'zoom': - break; + switch (scene.fullLayout.dragmode) { + case "zoom": + break; - case 'pan': - var scale = Math.exp(0.1 * dy / (viewBox[3] - viewBox[1])); + case "pan": + var scale = Math.exp(0.1 * dy / (viewBox[3] - viewBox[1])); - var cx = lastX / - (viewBox[2] - viewBox[0]) * (dataBox[2] - dataBox[0]) + - dataBox[0]; - var cy = lastY / - (viewBox[3] - viewBox[1]) * (dataBox[3] - dataBox[1]) + - dataBox[1]; + var cx = lastX / (viewBox[2] - viewBox[0]) * (dataBox[2] - dataBox[0]) + + dataBox[0]; + var cy = lastY / (viewBox[3] - viewBox[1]) * (dataBox[3] - dataBox[1]) + + dataBox[1]; - dataBox[0] = (dataBox[0] - cx) * scale + cx; - dataBox[2] = (dataBox[2] - cx) * scale + cx; - dataBox[1] = (dataBox[1] - cy) * scale + cy; - dataBox[3] = (dataBox[3] - cy) * scale + cy; + dataBox[0] = (dataBox[0] - cx) * scale + cx; + dataBox[2] = (dataBox[2] - cx) * scale + cx; + dataBox[1] = (dataBox[1] - cy) * scale + cy; + dataBox[3] = (dataBox[3] - cy) * scale + cy; - scene.setRanges(dataBox); + scene.setRanges(dataBox); - result.lastInputTime = Date.now(); - unSetAutoRange(); - scene.cameraChanged(); - scene.handleAnnotations(); - scene.relayoutCallback(); - break; - } + result.lastInputTime = Date.now(); + unSetAutoRange(); + scene.cameraChanged(); + scene.handleAnnotations(); + scene.relayoutCallback(); + break; + } - return true; - }); + return true; + }); - return result; + return result; } diff --git a/src/plots/gl2d/convert.js b/src/plots/gl2d/convert.js index 78784294fe9..4991bd052e7 100644 --- a/src/plots/gl2d/convert.js +++ b/src/plots/gl2d/convert.js @@ -5,240 +5,223 @@ * 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 Plots = require("../plots"); +var Axes = require("../cartesian/axes"); - -'use strict'; - -var Plots = require('../plots'); -var Axes = require('../cartesian/axes'); - -var convertHTMLToUnicode = require('../../lib/html2unicode'); -var str2RGBArray = require('../../lib/str2rgbarray'); +var convertHTMLToUnicode = require("../../lib/html2unicode"); +var str2RGBArray = require("../../lib/str2rgbarray"); function Axes2DOptions(scene) { - this.scene = scene; - this.gl = scene.gl; - this.pixelRatio = scene.pixelRatio; - - this.screenBox = [0, 0, 1, 1]; - this.viewBox = [0, 0, 1, 1]; - this.dataBox = [-1, -1, 1, 1]; - - this.borderLineEnable = [false, false, false, false]; - this.borderLineWidth = [1, 1, 1, 1]; - this.borderLineColor = [ - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1] - ]; - - this.ticks = [[], []]; - this.tickEnable = [true, true, false, false]; - this.tickPad = [15, 15, 15, 15]; - this.tickAngle = [0, 0, 0, 0]; - this.tickColor = [ - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1] - ]; - this.tickMarkLength = [0, 0, 0, 0]; - this.tickMarkWidth = [0, 0, 0, 0]; - this.tickMarkColor = [ - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1] - ]; - - this.labels = ['x', 'y']; - this.labelEnable = [true, true, false, false]; - this.labelAngle = [0, Math.PI / 2, 0, 3.0 * Math.PI / 2]; - this.labelPad = [15, 15, 15, 15]; - this.labelSize = [12, 12]; - this.labelFont = ['sans-serif', 'sans-serif']; - this.labelColor = [ - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1] - ]; - - this.title = ''; - this.titleEnable = true; - this.titleCenter = [0, 0, 0, 0]; - this.titleAngle = 0; - this.titleColor = [0, 0, 0, 1]; - this.titleFont = 'sans-serif'; - this.titleSize = 18; - - this.gridLineEnable = [true, true]; - this.gridLineColor = [ - [0, 0, 0, 0.5], - [0, 0, 0, 0.5] - ]; - this.gridLineWidth = [1, 1]; - - this.zeroLineEnable = [true, true]; - this.zeroLineWidth = [1, 1]; - this.zeroLineColor = [ - [0, 0, 0, 1], - [0, 0, 0, 1] - ]; - - this.borderColor = [0, 0, 0, 0]; - this.backgroundColor = [0, 0, 0, 0]; - - this.static = this.scene.staticPlot; + this.scene = scene; + this.gl = scene.gl; + this.pixelRatio = scene.pixelRatio; + + this.screenBox = [0, 0, 1, 1]; + this.viewBox = [0, 0, 1, 1]; + this.dataBox = [-1, -1, 1, 1]; + + this.borderLineEnable = [false, false, false, false]; + this.borderLineWidth = [1, 1, 1, 1]; + this.borderLineColor = [ + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1] + ]; + + this.ticks = [[], []]; + this.tickEnable = [true, true, false, false]; + this.tickPad = [15, 15, 15, 15]; + this.tickAngle = [0, 0, 0, 0]; + this.tickColor = [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]]; + this.tickMarkLength = [0, 0, 0, 0]; + this.tickMarkWidth = [0, 0, 0, 0]; + this.tickMarkColor = [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]]; + + this.labels = ["x", "y"]; + this.labelEnable = [true, true, false, false]; + this.labelAngle = [0, Math.PI / 2, 0, 3.0 * Math.PI / 2]; + this.labelPad = [15, 15, 15, 15]; + this.labelSize = [12, 12]; + this.labelFont = ["sans-serif", "sans-serif"]; + this.labelColor = [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]]; + + this.title = ""; + this.titleEnable = true; + this.titleCenter = [0, 0, 0, 0]; + this.titleAngle = 0; + this.titleColor = [0, 0, 0, 1]; + this.titleFont = "sans-serif"; + this.titleSize = 18; + + this.gridLineEnable = [true, true]; + this.gridLineColor = [[0, 0, 0, 0.5], [0, 0, 0, 0.5]]; + this.gridLineWidth = [1, 1]; + + this.zeroLineEnable = [true, true]; + this.zeroLineWidth = [1, 1]; + this.zeroLineColor = [[0, 0, 0, 1], [0, 0, 0, 1]]; + + this.borderColor = [0, 0, 0, 0]; + this.backgroundColor = [0, 0, 0, 0]; + + this.static = this.scene.staticPlot; } var proto = Axes2DOptions.prototype; -var AXES = ['xaxis', 'yaxis']; +var AXES = ["xaxis", "yaxis"]; proto.merge = function(options) { + // titles are rendered in SVG + this.titleEnable = false; + this.backgroundColor = str2RGBArray(options.plot_bgcolor); + + var axisName, ax, axTitle, axMirror; + var hasAxisInDfltPos, + hasAxisInAltrPos, + hasSharedAxis, + mirrorLines, + mirrorTicks; + var i, j; + + for (i = 0; i < 2; ++i) { + axisName = AXES[i]; + + // get options relevant to this subplot, + // '_name' is e.g. xaxis, xaxis2, yaxis, yaxis4 ... + ax = options[this.scene[axisName]._name]; + + axTitle = /Click to enter .+ title/.test(ax.title) ? "" : ax.title; + + for (j = 0; j <= 2; j += 2) { + this.labelEnable[i + j] = false; + this.labels[i + j] = convertHTMLToUnicode(axTitle); + this.labelColor[i + j] = str2RGBArray(ax.titlefont.color); + this.labelFont[i + j] = ax.titlefont.family; + this.labelSize[i + j] = ax.titlefont.size; + this.labelPad[i + j] = this.getLabelPad(axisName, ax); + + this.tickEnable[i + j] = false; + this.tickColor[i + j] = str2RGBArray((ax.tickfont || {}).color); + this.tickAngle[i + j] = ax.tickangle === "auto" + ? 0 + : Math.PI * (-ax.tickangle) / 180; + this.tickPad[i + j] = this.getTickPad(ax); + + this.tickMarkLength[i + j] = 0; + this.tickMarkWidth[i + j] = ax.tickwidth || 0; + this.tickMarkColor[i + j] = str2RGBArray(ax.tickcolor); + + this.borderLineEnable[i + j] = false; + this.borderLineColor[i + j] = str2RGBArray(ax.linecolor); + this.borderLineWidth[i + j] = ax.linewidth || 0; + } - // titles are rendered in SVG - this.titleEnable = false; - this.backgroundColor = str2RGBArray(options.plot_bgcolor); - - var axisName, ax, axTitle, axMirror; - var hasAxisInDfltPos, hasAxisInAltrPos, hasSharedAxis, mirrorLines, mirrorTicks; - var i, j; - - for(i = 0; i < 2; ++i) { - axisName = AXES[i]; - - // get options relevant to this subplot, - // '_name' is e.g. xaxis, xaxis2, yaxis, yaxis4 ... - ax = options[this.scene[axisName]._name]; - - axTitle = /Click to enter .+ title/.test(ax.title) ? '' : ax.title; - - for(j = 0; j <= 2; j += 2) { - this.labelEnable[i + j] = false; - this.labels[i + j] = convertHTMLToUnicode(axTitle); - this.labelColor[i + j] = str2RGBArray(ax.titlefont.color); - this.labelFont[i + j] = ax.titlefont.family; - this.labelSize[i + j] = ax.titlefont.size; - this.labelPad[i + j] = this.getLabelPad(axisName, ax); - - this.tickEnable[i + j] = false; - this.tickColor[i + j] = str2RGBArray((ax.tickfont || {}).color); - this.tickAngle[i + j] = (ax.tickangle === 'auto') ? - 0 : - Math.PI * -ax.tickangle / 180; - this.tickPad[i + j] = this.getTickPad(ax); - - this.tickMarkLength[i + j] = 0; - this.tickMarkWidth[i + j] = ax.tickwidth || 0; - this.tickMarkColor[i + j] = str2RGBArray(ax.tickcolor); - - this.borderLineEnable[i + j] = false; - this.borderLineColor[i + j] = str2RGBArray(ax.linecolor); - this.borderLineWidth[i + j] = ax.linewidth || 0; - } - - hasSharedAxis = this.hasSharedAxis(ax); - hasAxisInDfltPos = this.hasAxisInDfltPos(axisName, ax) && !hasSharedAxis; - hasAxisInAltrPos = this.hasAxisInAltrPos(axisName, ax) && !hasSharedAxis; - - axMirror = ax.mirror || false; - mirrorLines = hasSharedAxis ? - (String(axMirror).indexOf('all') !== -1) : // 'all' or 'allticks' - !!axMirror; // all but false - mirrorTicks = hasSharedAxis ? - (axMirror === 'allticks') : - (String(axMirror).indexOf('ticks') !== -1); // 'ticks' or 'allticks' - - // Axis titles and tick labels can only appear of one side of the scene - // and are never show on subplots that share existing axes. - - if(hasAxisInDfltPos) this.labelEnable[i] = true; - else if(hasAxisInAltrPos) this.labelEnable[i + 2] = true; - - if(hasAxisInDfltPos) this.tickEnable[i] = ax.showticklabels; - else if(hasAxisInAltrPos) this.tickEnable[i + 2] = ax.showticklabels; - - // Grid lines and ticks can appear on both sides of the scene - // and can appear on subplot that share existing axes via `ax.mirror`. - - if(hasAxisInDfltPos || mirrorLines) this.borderLineEnable[i] = ax.showline; - if(hasAxisInAltrPos || mirrorLines) this.borderLineEnable[i + 2] = ax.showline; + hasSharedAxis = this.hasSharedAxis(ax); + hasAxisInDfltPos = this.hasAxisInDfltPos(axisName, ax) && !hasSharedAxis; + hasAxisInAltrPos = this.hasAxisInAltrPos(axisName, ax) && !hasSharedAxis; + + axMirror = ax.mirror || false; + mirrorLines = hasSharedAxis // 'all' or 'allticks' + ? String(axMirror).indexOf("all") !== -1 + : !!axMirror; + // all but false + mirrorTicks = hasSharedAxis + ? axMirror === "allticks" + : String(axMirror).indexOf("ticks") !== -1; + + // 'ticks' or 'allticks' + // Axis titles and tick labels can only appear of one side of the scene + // and are never show on subplots that share existing axes. + if (hasAxisInDfltPos) this.labelEnable[i] = true; + else if (hasAxisInAltrPos) this.labelEnable[i + 2] = true; + + if (hasAxisInDfltPos) this.tickEnable[i] = ax.showticklabels; + else if (hasAxisInAltrPos) this.tickEnable[i + 2] = ax.showticklabels; + + // Grid lines and ticks can appear on both sides of the scene + // and can appear on subplot that share existing axes via `ax.mirror`. + if (hasAxisInDfltPos || mirrorLines) this.borderLineEnable[i] = ax.showline; + if (hasAxisInAltrPos || mirrorLines) { + this.borderLineEnable[i + 2] = ax.showline; + } - if(hasAxisInDfltPos || mirrorTicks) this.tickMarkLength[i] = this.getTickMarkLength(ax); - if(hasAxisInAltrPos || mirrorTicks) this.tickMarkLength[i + 2] = this.getTickMarkLength(ax); + if (hasAxisInDfltPos || mirrorTicks) { + this.tickMarkLength[i] = this.getTickMarkLength(ax); + } + if (hasAxisInAltrPos || mirrorTicks) { + this.tickMarkLength[i + 2] = this.getTickMarkLength(ax); + } - this.gridLineEnable[i] = ax.showgrid; - this.gridLineColor[i] = str2RGBArray(ax.gridcolor); - this.gridLineWidth[i] = ax.gridwidth; + this.gridLineEnable[i] = ax.showgrid; + this.gridLineColor[i] = str2RGBArray(ax.gridcolor); + this.gridLineWidth[i] = ax.gridwidth; - this.zeroLineEnable[i] = ax.zeroline; - this.zeroLineColor[i] = str2RGBArray(ax.zerolinecolor); - this.zeroLineWidth[i] = ax.zerolinewidth; - } + this.zeroLineEnable[i] = ax.zeroline; + this.zeroLineColor[i] = str2RGBArray(ax.zerolinecolor); + this.zeroLineWidth[i] = ax.zerolinewidth; + } }; // is an axis shared with an already-drawn subplot ? proto.hasSharedAxis = function(ax) { - var scene = this.scene, - subplotIds = Plots.getSubplotIds(scene.fullLayout, 'gl2d'), - list = Axes.findSubplotsWithAxis(subplotIds, ax); + var scene = this.scene, + subplotIds = Plots.getSubplotIds(scene.fullLayout, "gl2d"), + list = Axes.findSubplotsWithAxis(subplotIds, ax); - // if index === 0, then the subplot is already drawn as subplots - // are drawn in order. - return (list.indexOf(scene.id) !== 0); + // if index === 0, then the subplot is already drawn as subplots + // are drawn in order. + return list.indexOf(scene.id) !== 0; }; // has an axis in default position (i.e. bottom/left) ? proto.hasAxisInDfltPos = function(axisName, ax) { - var axSide = ax.side; + var axSide = ax.side; - if(axisName === 'xaxis') return (axSide === 'bottom'); - else if(axisName === 'yaxis') return (axSide === 'left'); + if (axisName === "xaxis") return axSide === "bottom"; + else if (axisName === "yaxis") return axSide === "left"; }; // has an axis in alternate position (i.e. top/right) ? proto.hasAxisInAltrPos = function(axisName, ax) { - var axSide = ax.side; + var axSide = ax.side; - if(axisName === 'xaxis') return (axSide === 'top'); - else if(axisName === 'yaxis') return (axSide === 'right'); + if (axisName === "xaxis") return axSide === "top"; + else if (axisName === "yaxis") return axSide === "right"; }; proto.getLabelPad = function(axisName, ax) { - var offsetBase = 1.5, - fontSize = ax.titlefont.size, - showticklabels = ax.showticklabels; - - if(axisName === 'xaxis') { - return (ax.side === 'top') ? - -10 + fontSize * (offsetBase + (showticklabels ? 1 : 0)) : - -10 + fontSize * (offsetBase + (showticklabels ? 0.5 : 0)); - } - else if(axisName === 'yaxis') { - return (ax.side === 'right') ? - 10 + fontSize * (offsetBase + (showticklabels ? 1 : 0.5)) : - 10 + fontSize * (offsetBase + (showticklabels ? 0.5 : 0)); - } + var offsetBase = 1.5, + fontSize = ax.titlefont.size, + showticklabels = ax.showticklabels; + + if (axisName === "xaxis") { + return ax.side === "top" + ? -10 + fontSize * (offsetBase + (showticklabels ? 1 : 0)) + : -10 + fontSize * (offsetBase + (showticklabels ? 0.5 : 0)); + } else if (axisName === "yaxis") { + return ax.side === "right" + ? 10 + fontSize * (offsetBase + (showticklabels ? 1 : 0.5)) + : 10 + fontSize * (offsetBase + (showticklabels ? 0.5 : 0)); + } }; proto.getTickPad = function(ax) { - return (ax.ticks === 'outside') ? 10 + ax.ticklen : 15; + return ax.ticks === "outside" ? 10 + ax.ticklen : 15; }; proto.getTickMarkLength = function(ax) { - if(!ax.ticks) return 0; + if (!ax.ticks) return 0; - var ticklen = ax.ticklen; + var ticklen = ax.ticklen; - return (ax.ticks === 'inside') ? -ticklen : ticklen; + return ax.ticks === "inside" ? -ticklen : ticklen; }; - function createAxes2D(scene) { - return new Axes2DOptions(scene); + return new Axes2DOptions(scene); } module.exports = createAxes2D; diff --git a/src/plots/gl2d/index.js b/src/plots/gl2d/index.js index 2d4f2f7f099..569a4893c52 100644 --- a/src/plots/gl2d/index.js +++ b/src/plots/gl2d/index.js @@ -5,106 +5,106 @@ * 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 Scene2D = require("./scene2d"); +var Plots = require("../plots"); +var xmlnsNamespaces = require("../../constants/xmlns_namespaces"); +exports.name = "gl2d"; -'use strict'; +exports.attr = ["xaxis", "yaxis"]; -var Scene2D = require('./scene2d'); -var Plots = require('../plots'); -var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); - - -exports.name = 'gl2d'; - -exports.attr = ['xaxis', 'yaxis']; - -exports.idRoot = ['x', 'y']; +exports.idRoot = ["x", "y"]; exports.idRegex = { - x: /^x([2-9]|[1-9][0-9]+)?$/, - y: /^y([2-9]|[1-9][0-9]+)?$/ + x: /^x([2-9]|[1-9][0-9]+)?$/, + y: /^y([2-9]|[1-9][0-9]+)?$/ }; exports.attrRegex = { - x: /^xaxis([2-9]|[1-9][0-9]+)?$/, - y: /^yaxis([2-9]|[1-9][0-9]+)?$/ + x: /^xaxis([2-9]|[1-9][0-9]+)?$/, + y: /^yaxis([2-9]|[1-9][0-9]+)?$/ }; -exports.attributes = require('../cartesian/attributes'); +exports.attributes = require("../cartesian/attributes"); exports.plot = function plotGl2d(gd) { - var fullLayout = gd._fullLayout, - fullData = gd._fullData, - subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); - - for(var i = 0; i < subplotIds.length; i++) { - var subplotId = subplotIds[i], - subplotObj = fullLayout._plots[subplotId], - fullSubplotData = Plots.getSubplotData(fullData, 'gl2d', subplotId); - - // ref. to corresp. Scene instance - var scene = subplotObj._scene2d; - - // If Scene is not instantiated, create one! - if(scene === undefined) { - scene = new Scene2D({ - id: subplotId, - graphDiv: gd, - container: gd.querySelector('.gl-container'), - staticPlot: gd._context.staticPlot, - plotGlPixelRatio: gd._context.plotGlPixelRatio - }, - fullLayout - ); - - // set ref to Scene instance - subplotObj._scene2d = scene; - } - - scene.plot(fullSubplotData, gd.calcdata, fullLayout, gd.layout); + var fullLayout = gd._fullLayout, + fullData = gd._fullData, + subplotIds = Plots.getSubplotIds(fullLayout, "gl2d"); + + for (var i = 0; i < subplotIds.length; i++) { + var subplotId = subplotIds[i], + subplotObj = fullLayout._plots[subplotId], + fullSubplotData = Plots.getSubplotData(fullData, "gl2d", subplotId); + + // ref. to corresp. Scene instance + var scene = subplotObj._scene2d; + + // If Scene is not instantiated, create one! + if (scene === undefined) { + scene = new Scene2D( + { + id: subplotId, + graphDiv: gd, + container: gd.querySelector(".gl-container"), + staticPlot: gd._context.staticPlot, + plotGlPixelRatio: gd._context.plotGlPixelRatio + }, + fullLayout + ); + + // set ref to Scene instance + subplotObj._scene2d = scene; } -}; -exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, 'gl2d'); - - for(var i = 0; i < oldSceneKeys.length; i++) { - var id = oldSceneKeys[i], - oldSubplot = oldFullLayout._plots[id]; - - // old subplot wasn't gl2d; nothing to do - if(!oldSubplot._scene2d) continue; + scene.plot(fullSubplotData, gd.calcdata, fullLayout, gd.layout); + } +}; - // if no traces are present, delete gl2d subplot - var subplotData = Plots.getSubplotData(newFullData, 'gl2d', id); - if(subplotData.length === 0) { - oldSubplot._scene2d.destroy(); - delete oldFullLayout._plots[id]; - } +exports.clean = function( + newFullData, + newFullLayout, + oldFullData, + oldFullLayout +) { + var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, "gl2d"); + + for (var i = 0; i < oldSceneKeys.length; i++) { + var id = oldSceneKeys[i], oldSubplot = oldFullLayout._plots[id]; + + // old subplot wasn't gl2d; nothing to do + if (!oldSubplot._scene2d) continue; + + // if no traces are present, delete gl2d subplot + var subplotData = Plots.getSubplotData(newFullData, "gl2d", id); + if (subplotData.length === 0) { + oldSubplot._scene2d.destroy(); + delete oldFullLayout._plots[id]; } + } }; exports.toSVG = function(gd) { - var fullLayout = gd._fullLayout, - subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); - - for(var i = 0; i < subplotIds.length; i++) { - var subplot = fullLayout._plots[subplotIds[i]], - scene = subplot._scene2d; - - var imageData = scene.toImage('png'); - var image = fullLayout._glimages.append('svg:image'); - - image.attr({ - xmlns: xmlnsNamespaces.svg, - 'xlink:href': imageData, - x: 0, - y: 0, - width: '100%', - height: '100%', - preserveAspectRatio: 'none' - }); - - scene.destroy(); - } + var fullLayout = gd._fullLayout, + subplotIds = Plots.getSubplotIds(fullLayout, "gl2d"); + + for (var i = 0; i < subplotIds.length; i++) { + var subplot = fullLayout._plots[subplotIds[i]], scene = subplot._scene2d; + + var imageData = scene.toImage("png"); + var image = fullLayout._glimages.append("svg:image"); + + image.attr({ + xmlns: xmlnsNamespaces.svg, + "xlink:href": imageData, + x: 0, + y: 0, + width: "100%", + height: "100%", + preserveAspectRatio: "none" + }); + + scene.destroy(); + } }; diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index d86303c29f9..da33d821458 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -5,76 +5,72 @@ * 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 Registry = require('../../registry'); -var Axes = require('../../plots/cartesian/axes'); -var Fx = require('../../plots/cartesian/graph_interact'); - -var createPlot2D = require('gl-plot2d'); -var createSpikes = require('gl-spikes2d'); -var createSelectBox = require('gl-select-box'); -var getContext = require('webgl-context'); - -var createOptions = require('./convert'); -var createCamera = require('./camera'); -var convertHTMLToUnicode = require('../../lib/html2unicode'); -var showNoWebGlMsg = require('../../lib/show_no_webgl_msg'); - -var AXES = ['xaxis', 'yaxis']; +"use strict"; +var Registry = require("../../registry"); +var Axes = require("../../plots/cartesian/axes"); +var Fx = require("../../plots/cartesian/graph_interact"); + +var createPlot2D = require("gl-plot2d"); +var createSpikes = require("gl-spikes2d"); +var createSelectBox = require("gl-select-box"); +var getContext = require("webgl-context"); + +var createOptions = require("./convert"); +var createCamera = require("./camera"); +var convertHTMLToUnicode = require("../../lib/html2unicode"); +var showNoWebGlMsg = require("../../lib/show_no_webgl_msg"); + +var AXES = ["xaxis", "yaxis"]; var STATIC_CANVAS, STATIC_CONTEXT; - function Scene2D(options, fullLayout) { - this.container = options.container; - this.graphDiv = options.graphDiv; - this.pixelRatio = options.plotGlPixelRatio || window.devicePixelRatio; - this.id = options.id; - this.staticPlot = !!options.staticPlot; + this.container = options.container; + this.graphDiv = options.graphDiv; + this.pixelRatio = options.plotGlPixelRatio || window.devicePixelRatio; + this.id = options.id; + this.staticPlot = !!options.staticPlot; - this.fullData = null; - this.updateRefs(fullLayout); + this.fullData = null; + this.updateRefs(fullLayout); - this.makeFramework(); + this.makeFramework(); - // update options - this.glplotOptions = createOptions(this); - this.glplotOptions.merge(fullLayout); + // update options + this.glplotOptions = createOptions(this); + this.glplotOptions.merge(fullLayout); - // create the plot - this.glplot = createPlot2D(this.glplotOptions); + // create the plot + this.glplot = createPlot2D(this.glplotOptions); - // create camera - this.camera = createCamera(this); + // create camera + this.camera = createCamera(this); - // trace set - this.traces = {}; - this._inputs = {}; + // trace set + this.traces = {}; + this._inputs = {}; - // create axes spikes - this.spikes = createSpikes(this.glplot); + // create axes spikes + this.spikes = createSpikes(this.glplot); - this.selectBox = createSelectBox(this.glplot, { - innerFill: false, - outerFill: true - }); + this.selectBox = createSelectBox(this.glplot, { + innerFill: false, + outerFill: true + }); - // last button state - this.lastButtonState = 0; + // last button state + this.lastButtonState = 0; - // last pick result - this.pickResult = null; + // last pick result + this.pickResult = null; - this.bounds = [Infinity, Infinity, -Infinity, -Infinity]; + this.bounds = [Infinity, Infinity, -Infinity, -Infinity]; - // flag to stop render loop - this.stopped = false; + // flag to stop render loop + this.stopped = false; - // redraw the plot - this.redraw = this.draw.bind(this); - this.redraw(); + // redraw the plot + this.redraw = this.draw.bind(this); + this.redraw(); } module.exports = Scene2D; @@ -82,555 +78,548 @@ module.exports = Scene2D; var proto = Scene2D.prototype; proto.makeFramework = function() { - - // create canvas and gl context - if(this.staticPlot) { - if(!STATIC_CONTEXT) { - STATIC_CANVAS = document.createElement('canvas'); - - STATIC_CONTEXT = getContext({ - canvas: STATIC_CANVAS, - preserveDrawingBuffer: false, - premultipliedAlpha: true, - antialias: true - }); - - if(!STATIC_CONTEXT) { - throw new Error('Error creating static canvas/context for image server'); - } - } - - this.canvas = STATIC_CANVAS; - this.gl = STATIC_CONTEXT; - } - else { - var liveCanvas = document.createElement('canvas'); - - var gl = getContext({ - canvas: liveCanvas, - premultipliedAlpha: true - }); - - if(!gl) showNoWebGlMsg(this); - - this.canvas = liveCanvas; - this.gl = gl; + // create canvas and gl context + if (this.staticPlot) { + if (!STATIC_CONTEXT) { + STATIC_CANVAS = document.createElement("canvas"); + + STATIC_CONTEXT = getContext({ + canvas: STATIC_CANVAS, + preserveDrawingBuffer: false, + premultipliedAlpha: true, + antialias: true + }); + + if (!STATIC_CONTEXT) { + throw new Error( + "Error creating static canvas/context for image server" + ); + } } - // position the canvas - var canvas = this.canvas; - - canvas.style.width = '100%'; - canvas.style.height = '100%'; - canvas.style.position = 'absolute'; - canvas.style.top = '0px'; - canvas.style.left = '0px'; - canvas.style['pointer-events'] = 'none'; - - this.updateSize(canvas); - - // disabling user select on the canvas - // sanitizes double-clicks interactions - // ref: https://github.com/plotly/plotly.js/issues/744 - canvas.className += 'user-select-none'; - - // create SVG container for hover text - var svgContainer = this.svgContainer = document.createElementNS( - 'http://www.w3.org/2000/svg', - 'svg'); - svgContainer.style.position = 'absolute'; - svgContainer.style.top = svgContainer.style.left = '0px'; - svgContainer.style.width = svgContainer.style.height = '100%'; - svgContainer.style['z-index'] = 20; - svgContainer.style['pointer-events'] = 'none'; - - // create div to catch the mouse event - var mouseContainer = this.mouseContainer = document.createElement('div'); - mouseContainer.style.position = 'absolute'; - - // append canvas, hover svg and mouse div to container - var container = this.container; - container.appendChild(canvas); - container.appendChild(svgContainer); - container.appendChild(mouseContainer); + this.canvas = STATIC_CANVAS; + this.gl = STATIC_CONTEXT; + } else { + var liveCanvas = document.createElement("canvas"); + + var gl = getContext({ canvas: liveCanvas, premultipliedAlpha: true }); + + if (!gl) showNoWebGlMsg(this); + + this.canvas = liveCanvas; + this.gl = gl; + } + + // position the canvas + var canvas = this.canvas; + + canvas.style.width = "100%"; + canvas.style.height = "100%"; + canvas.style.position = "absolute"; + canvas.style.top = "0px"; + canvas.style.left = "0px"; + canvas.style["pointer-events"] = "none"; + + this.updateSize(canvas); + + // disabling user select on the canvas + // sanitizes double-clicks interactions + // ref: https://github.com/plotly/plotly.js/issues/744 + canvas.className += "user-select-none"; + + // create SVG container for hover text + var svgContainer = this.svgContainer = document.createElementNS( + "http://www.w3.org/2000/svg", + "svg" + ); + svgContainer.style.position = "absolute"; + svgContainer.style.top = svgContainer.style.left = "0px"; + svgContainer.style.width = svgContainer.style.height = "100%"; + svgContainer.style["z-index"] = 20; + svgContainer.style["pointer-events"] = "none"; + + // create div to catch the mouse event + var mouseContainer = this.mouseContainer = document.createElement("div"); + mouseContainer.style.position = "absolute"; + + // append canvas, hover svg and mouse div to container + var container = this.container; + container.appendChild(canvas); + container.appendChild(svgContainer); + container.appendChild(mouseContainer); }; proto.toImage = function(format) { - if(!format) format = 'png'; + if (!format) format = "png"; - this.stopped = true; - if(this.staticPlot) this.container.appendChild(STATIC_CANVAS); + this.stopped = true; + if (this.staticPlot) this.container.appendChild(STATIC_CANVAS); - // update canvas size - this.updateSize(this.canvas); + // update canvas size + this.updateSize(this.canvas); - // force redraw - this.glplot.setDirty(); - this.glplot.draw(); + // force redraw + this.glplot.setDirty(); + this.glplot.draw(); - // grab context and yank out pixels - var gl = this.glplot.gl, - w = gl.drawingBufferWidth, - h = gl.drawingBufferHeight; + // grab context and yank out pixels + var gl = this.glplot.gl, + w = gl.drawingBufferWidth, + h = gl.drawingBufferHeight; - gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); - var pixels = new Uint8Array(w * h * 4); - gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + var pixels = new Uint8Array(w * h * 4); + gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - // flip pixels - for(var j = 0, k = h - 1; j < k; ++j, --k) { - for(var i = 0; i < w; ++i) { - for(var l = 0; l < 4; ++l) { - var tmp = pixels[4 * (w * j + i) + l]; - pixels[4 * (w * j + i) + l] = pixels[4 * (w * k + i) + l]; - pixels[4 * (w * k + i) + l] = tmp; - } - } + // flip pixels + for (var j = 0, k = h - 1; j < k; ++j, --k) { + for (var i = 0; i < w; ++i) { + for (var l = 0; l < 4; ++l) { + var tmp = pixels[4 * (w * j + i) + l]; + pixels[4 * (w * j + i) + l] = pixels[4 * (w * k + i) + l]; + pixels[4 * (w * k + i) + l] = tmp; + } } + } - var canvas = document.createElement('canvas'); - canvas.width = w; - canvas.height = h; - - var context = canvas.getContext('2d'); - var imageData = context.createImageData(w, h); - imageData.data.set(pixels); - context.putImageData(imageData, 0, 0); - - var dataURL; - - switch(format) { - case 'jpeg': - dataURL = canvas.toDataURL('image/jpeg'); - break; - case 'webp': - dataURL = canvas.toDataURL('image/webp'); - break; - default: - dataURL = canvas.toDataURL('image/png'); - } + var canvas = document.createElement("canvas"); + canvas.width = w; + canvas.height = h; + + var context = canvas.getContext("2d"); + var imageData = context.createImageData(w, h); + imageData.data.set(pixels); + context.putImageData(imageData, 0, 0); + + var dataURL; + + switch (format) { + case "jpeg": + dataURL = canvas.toDataURL("image/jpeg"); + break; + case "webp": + dataURL = canvas.toDataURL("image/webp"); + break; + default: + dataURL = canvas.toDataURL("image/png"); + } - if(this.staticPlot) this.container.removeChild(STATIC_CANVAS); + if (this.staticPlot) this.container.removeChild(STATIC_CANVAS); - return dataURL; + return dataURL; }; proto.updateSize = function(canvas) { - if(!canvas) canvas = this.canvas; + if (!canvas) canvas = this.canvas; - var pixelRatio = this.pixelRatio, - fullLayout = this.fullLayout; + var pixelRatio = this.pixelRatio, fullLayout = this.fullLayout; - var width = fullLayout.width, - height = fullLayout.height, - pixelWidth = Math.ceil(pixelRatio * width) |0, - pixelHeight = Math.ceil(pixelRatio * height) |0; + var width = fullLayout.width, + height = fullLayout.height, + pixelWidth = Math.ceil(pixelRatio * width) | 0, + pixelHeight = Math.ceil(pixelRatio * height) | 0; - // check for resize - if(canvas.width !== pixelWidth || canvas.height !== pixelHeight) { - canvas.width = pixelWidth; - canvas.height = pixelHeight; - } + // check for resize + if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { + canvas.width = pixelWidth; + canvas.height = pixelHeight; + } - return canvas; + return canvas; }; proto.computeTickMarks = function() { - this.xaxis.setScale(); - this.yaxis.setScale(); - - // override _length from backward compatibility - // even though setScale 'should' give the correct result - this.xaxis._length = - this.glplot.viewBox[2] - this.glplot.viewBox[0]; - this.yaxis._length = - this.glplot.viewBox[3] - this.glplot.viewBox[1]; - - var nextTicks = [ - Axes.calcTicks(this.xaxis), - Axes.calcTicks(this.yaxis) - ]; + this.xaxis.setScale(); + this.yaxis.setScale(); - for(var j = 0; j < 2; ++j) { - for(var i = 0; i < nextTicks[j].length; ++i) { - // coercing tick value (may not be a string) to a string - nextTicks[j][i].text = convertHTMLToUnicode(nextTicks[j][i].text + ''); - } + // override _length from backward compatibility + // even though setScale 'should' give the correct result + this.xaxis._length = this.glplot.viewBox[2] - this.glplot.viewBox[0]; + this.yaxis._length = this.glplot.viewBox[3] - this.glplot.viewBox[1]; + + var nextTicks = [Axes.calcTicks(this.xaxis), Axes.calcTicks(this.yaxis)]; + + for (var j = 0; j < 2; ++j) { + for (var i = 0; i < nextTicks[j].length; ++i) { + // coercing tick value (may not be a string) to a string + nextTicks[j][i].text = convertHTMLToUnicode(nextTicks[j][i].text + ""); } + } - return nextTicks; + return nextTicks; }; function compareTicks(a, b) { - for(var i = 0; i < 2; ++i) { - var aticks = a[i], - bticks = b[i]; + for (var i = 0; i < 2; ++i) { + var aticks = a[i], bticks = b[i]; - if(aticks.length !== bticks.length) return true; + if (aticks.length !== bticks.length) return true; - for(var j = 0; j < aticks.length; ++j) { - if(aticks[j].x !== bticks[j].x) return true; - } + for (var j = 0; j < aticks.length; ++j) { + if (aticks[j].x !== bticks[j].x) return true; } + } - return false; + return false; } proto.updateRefs = function(newFullLayout) { - this.fullLayout = newFullLayout; + this.fullLayout = newFullLayout; - var spmatch = Axes.subplotMatch, - xaxisName = 'xaxis' + this.id.match(spmatch)[1], - yaxisName = 'yaxis' + this.id.match(spmatch)[2]; + var spmatch = Axes.subplotMatch, + xaxisName = "xaxis" + this.id.match(spmatch)[1], + yaxisName = "yaxis" + this.id.match(spmatch)[2]; - this.xaxis = this.fullLayout[xaxisName]; - this.yaxis = this.fullLayout[yaxisName]; + this.xaxis = this.fullLayout[xaxisName]; + this.yaxis = this.fullLayout[yaxisName]; }; proto.relayoutCallback = function() { - var graphDiv = this.graphDiv, - xaxis = this.xaxis, - yaxis = this.yaxis, - layout = graphDiv.layout; - - // update user layout - layout.xaxis.autorange = xaxis.autorange; - layout.xaxis.range = xaxis.range.slice(0); - layout.yaxis.autorange = yaxis.autorange; - layout.yaxis.range = yaxis.range.slice(0); - - // make a meaningful value to be passed on to the possible 'plotly_relayout' subscriber(s) - // scene.camera has no many useful projection or scale information - // helps determine which one is the latest input (if async) - var update = { - lastInputTime: this.camera.lastInputTime - }; - - update[xaxis._name] = xaxis.range.slice(0); - update[yaxis._name] = yaxis.range.slice(0); - - graphDiv.emit('plotly_relayout', update); + var graphDiv = this.graphDiv, + xaxis = this.xaxis, + yaxis = this.yaxis, + layout = graphDiv.layout; + + // update user layout + layout.xaxis.autorange = xaxis.autorange; + layout.xaxis.range = xaxis.range.slice(0); + layout.yaxis.autorange = yaxis.autorange; + layout.yaxis.range = yaxis.range.slice(0); + + // make a meaningful value to be passed on to the possible 'plotly_relayout' subscriber(s) + // scene.camera has no many useful projection or scale information + // helps determine which one is the latest input (if async) + var update = { lastInputTime: this.camera.lastInputTime }; + + update[xaxis._name] = xaxis.range.slice(0); + update[yaxis._name] = yaxis.range.slice(0); + + graphDiv.emit("plotly_relayout", update); }; proto.cameraChanged = function() { - var camera = this.camera; + var camera = this.camera; - this.glplot.setDataBox(this.calcDataBox()); + this.glplot.setDataBox(this.calcDataBox()); - var nextTicks = this.computeTickMarks(); - var curTicks = this.glplotOptions.ticks; + var nextTicks = this.computeTickMarks(); + var curTicks = this.glplotOptions.ticks; - if(compareTicks(nextTicks, curTicks)) { - this.glplotOptions.ticks = nextTicks; - this.glplotOptions.dataBox = camera.dataBox; - this.glplot.update(this.glplotOptions); - this.handleAnnotations(); - } + if (compareTicks(nextTicks, curTicks)) { + this.glplotOptions.ticks = nextTicks; + this.glplotOptions.dataBox = camera.dataBox; + this.glplot.update(this.glplotOptions); + this.handleAnnotations(); + } }; proto.handleAnnotations = function() { - var gd = this.graphDiv, - annotations = this.fullLayout.annotations; + var gd = this.graphDiv, annotations = this.fullLayout.annotations; - for(var i = 0; i < annotations.length; i++) { - var ann = annotations[i]; + for (var i = 0; i < annotations.length; i++) { + var ann = annotations[i]; - if(ann.xref === this.xaxis._id && ann.yref === this.yaxis._id) { - Registry.getComponentMethod('annotations', 'drawOne')(gd, i); - } + if (ann.xref === this.xaxis._id && ann.yref === this.yaxis._id) { + Registry.getComponentMethod("annotations", "drawOne")(gd, i); } + } }; proto.destroy = function() { - var traces = this.traces; + var traces = this.traces; - if(traces) { - Object.keys(traces).map(function(key) { - traces[key].dispose(); - delete traces[key]; - }); - } + if (traces) { + Object.keys(traces).map(function(key) { + traces[key].dispose(); + delete traces[key]; + }); + } - this.glplot.dispose(); + this.glplot.dispose(); - if(!this.staticPlot) this.container.removeChild(this.canvas); - this.container.removeChild(this.svgContainer); - this.container.removeChild(this.mouseContainer); + if (!this.staticPlot) this.container.removeChild(this.canvas); + this.container.removeChild(this.svgContainer); + this.container.removeChild(this.mouseContainer); - this.fullData = null; - this._inputs = null; - this.glplot = null; - this.stopped = true; + this.fullData = null; + this._inputs = null; + this.glplot = null; + this.stopped = true; }; proto.plot = function(fullData, calcData, fullLayout) { - var glplot = this.glplot; + var glplot = this.glplot; - this.updateRefs(fullLayout); - this.updateTraces(fullData, calcData); + this.updateRefs(fullLayout); + this.updateTraces(fullData, calcData); - var width = fullLayout.width, - height = fullLayout.height; + var width = fullLayout.width, height = fullLayout.height; - this.updateSize(this.canvas); + this.updateSize(this.canvas); - var options = this.glplotOptions; - options.merge(fullLayout); - options.screenBox = [0, 0, width, height]; + var options = this.glplotOptions; + options.merge(fullLayout); + options.screenBox = [0, 0, width, height]; - var size = fullLayout._size, - domainX = this.xaxis.domain, - domainY = this.yaxis.domain; - - options.viewBox = [ - size.l + domainX[0] * size.w, - size.b + domainY[0] * size.h, - (width - size.r) - (1 - domainX[1]) * size.w, - (height - size.t) - (1 - domainY[1]) * size.h - ]; + var size = fullLayout._size, + domainX = this.xaxis.domain, + domainY = this.yaxis.domain; - this.mouseContainer.style.width = size.w * (domainX[1] - domainX[0]) + 'px'; - this.mouseContainer.style.height = size.h * (domainY[1] - domainY[0]) + 'px'; - this.mouseContainer.height = size.h * (domainY[1] - domainY[0]); - this.mouseContainer.style.left = size.l + domainX[0] * size.w + 'px'; - this.mouseContainer.style.top = size.t + (1 - domainY[1]) * size.h + 'px'; + options.viewBox = [ + size.l + domainX[0] * size.w, + size.b + domainY[0] * size.h, + width - size.r - (1 - domainX[1]) * size.w, + height - size.t - (1 - domainY[1]) * size.h + ]; - var bounds = this.bounds; - bounds[0] = bounds[1] = Infinity; - bounds[2] = bounds[3] = -Infinity; + this.mouseContainer.style.width = size.w * (domainX[1] - domainX[0]) + "px"; + this.mouseContainer.style.height = size.h * (domainY[1] - domainY[0]) + "px"; + this.mouseContainer.height = size.h * (domainY[1] - domainY[0]); + this.mouseContainer.style.left = size.l + domainX[0] * size.w + "px"; + this.mouseContainer.style.top = size.t + (1 - domainY[1]) * size.h + "px"; - var traceIds = Object.keys(this.traces); - var ax, i; + var bounds = this.bounds; + bounds[0] = bounds[1] = Infinity; + bounds[2] = bounds[3] = -Infinity; - for(i = 0; i < traceIds.length; ++i) { - var traceObj = this.traces[traceIds[i]]; + var traceIds = Object.keys(this.traces); + var ax, i; - for(var k = 0; k < 2; ++k) { - bounds[k] = Math.min(bounds[k], traceObj.bounds[k]); - bounds[k + 2] = Math.max(bounds[k + 2], traceObj.bounds[k + 2]); - } + for (i = 0; i < traceIds.length; ++i) { + var traceObj = this.traces[traceIds[i]]; + + for (var k = 0; k < 2; ++k) { + bounds[k] = Math.min(bounds[k], traceObj.bounds[k]); + bounds[k + 2] = Math.max(bounds[k + 2], traceObj.bounds[k + 2]); } + } - for(i = 0; i < 2; ++i) { - if(bounds[i] > bounds[i + 2]) { - bounds[i] = -1; - bounds[i + 2] = 1; - } + for (i = 0; i < 2; ++i) { + if (bounds[i] > bounds[i + 2]) { + bounds[i] = -1; + bounds[i + 2] = 1; + } - ax = this[AXES[i]]; - ax._length = options.viewBox[i + 2] - options.viewBox[i]; + ax = this[AXES[i]]; + ax._length = options.viewBox[i + 2] - options.viewBox[i]; - Axes.doAutoRange(ax); - ax.setScale(); - } + Axes.doAutoRange(ax); + ax.setScale(); + } - options.ticks = this.computeTickMarks(); + options.ticks = this.computeTickMarks(); - options.dataBox = this.calcDataBox(); + options.dataBox = this.calcDataBox(); - options.merge(fullLayout); - glplot.update(options); + options.merge(fullLayout); + glplot.update(options); - // force redraw so that promise is returned when rendering is completed - this.glplot.draw(); + // force redraw so that promise is returned when rendering is completed + this.glplot.draw(); }; proto.calcDataBox = function() { - var xaxis = this.xaxis, - yaxis = this.yaxis, - xrange = xaxis.range, - yrange = yaxis.range, - xr2l = xaxis.r2l, - yr2l = yaxis.r2l; - - return [xr2l(xrange[0]), yr2l(yrange[0]), xr2l(xrange[1]), yr2l(yrange[1])]; + var xaxis = this.xaxis, + yaxis = this.yaxis, + xrange = xaxis.range, + yrange = yaxis.range, + xr2l = xaxis.r2l, + yr2l = yaxis.r2l; + + return [xr2l(xrange[0]), yr2l(yrange[0]), xr2l(xrange[1]), yr2l(yrange[1])]; }; proto.setRanges = function(dataBox) { - var xaxis = this.xaxis, - yaxis = this.yaxis, - xl2r = xaxis.l2r, - yl2r = yaxis.l2r; + var xaxis = this.xaxis, + yaxis = this.yaxis, + xl2r = xaxis.l2r, + yl2r = yaxis.l2r; - xaxis.range = [xl2r(dataBox[0]), xl2r(dataBox[2])]; - yaxis.range = [yl2r(dataBox[1]), yl2r(dataBox[3])]; + xaxis.range = [xl2r(dataBox[0]), xl2r(dataBox[2])]; + yaxis.range = [yl2r(dataBox[1]), yl2r(dataBox[3])]; }; proto.updateTraces = function(fullData, calcData) { - var traceIds = Object.keys(this.traces); - var i, j, fullTrace; + var traceIds = Object.keys(this.traces); + var i, j, fullTrace; - this.fullData = fullData; + this.fullData = fullData; - // remove empty traces - trace_id_loop: - for(i = 0; i < traceIds.length; i++) { - var oldUid = traceIds[i], - oldTrace = this.traces[oldUid]; + // remove empty traces + trace_id_loop: + for (i = 0; i < traceIds.length; i++) { + var oldUid = traceIds[i], oldTrace = this.traces[oldUid]; - for(j = 0; j < fullData.length; j++) { - fullTrace = fullData[j]; + for (j = 0; j < fullData.length; j++) { + fullTrace = fullData[j]; - if(fullTrace.uid === oldUid && fullTrace.type === oldTrace.type) { - continue trace_id_loop; - } - } - - oldTrace.dispose(); - delete this.traces[oldUid]; + if (fullTrace.uid === oldUid && fullTrace.type === oldTrace.type) { + continue trace_id_loop; + } } - // update / create trace objects - for(i = 0; i < fullData.length; i++) { - fullTrace = fullData[i]; - this._inputs[fullTrace.uid] = i; - var calcTrace = calcData[i], - traceObj = this.traces[fullTrace.uid]; - - if(traceObj) traceObj.update(fullTrace, calcTrace); - else { - traceObj = fullTrace._module.plot(this, fullTrace, calcTrace); - this.traces[fullTrace.uid] = traceObj; - } + oldTrace.dispose(); + delete this.traces[oldUid]; + } + + // update / create trace objects + for (i = 0; i < fullData.length; i++) { + fullTrace = fullData[i]; + this._inputs[fullTrace.uid] = i; + var calcTrace = calcData[i], traceObj = this.traces[fullTrace.uid]; + + if (traceObj) { + traceObj.update(fullTrace, calcTrace); + } else { + traceObj = fullTrace._module.plot(this, fullTrace, calcTrace); + this.traces[fullTrace.uid] = traceObj; } + } }; proto.emitPointAction = function(nextSelection, eventType) { - - var curveIndex = this._inputs[nextSelection.trace.uid]; - - this.graphDiv.emit(eventType, { - points: [{ - x: nextSelection.traceCoord[0], - y: nextSelection.traceCoord[1], - curveNumber: curveIndex, - pointNumber: nextSelection.pointIndex, - data: this.fullData[curveIndex]._input, - fullData: this.fullData, - xaxis: this.xaxis, - yaxis: this.yaxis - }] - }); + var curveIndex = this._inputs[nextSelection.trace.uid]; + + this.graphDiv.emit(eventType, { + points: [ + { + x: nextSelection.traceCoord[0], + y: nextSelection.traceCoord[1], + curveNumber: curveIndex, + pointNumber: nextSelection.pointIndex, + data: this.fullData[curveIndex]._input, + fullData: this.fullData, + xaxis: this.xaxis, + yaxis: this.yaxis + } + ] + }); }; proto.draw = function() { - if(this.stopped) return; + if (this.stopped) return; - requestAnimationFrame(this.redraw); + requestAnimationFrame(this.redraw); - var glplot = this.glplot, - camera = this.camera, - mouseListener = camera.mouseListener, - mouseUp = this.lastButtonState === 1 && mouseListener.buttons === 0, - fullLayout = this.fullLayout; + var glplot = this.glplot, + camera = this.camera, + mouseListener = camera.mouseListener, + mouseUp = this.lastButtonState === 1 && mouseListener.buttons === 0, + fullLayout = this.fullLayout; - this.lastButtonState = mouseListener.buttons; + this.lastButtonState = mouseListener.buttons; - this.cameraChanged(); + this.cameraChanged(); - var x = mouseListener.x * glplot.pixelRatio; - var y = this.canvas.height - glplot.pixelRatio * mouseListener.y; + var x = mouseListener.x * glplot.pixelRatio; + var y = this.canvas.height - glplot.pixelRatio * mouseListener.y; - if(camera.boxEnabled && fullLayout.dragmode === 'zoom') { - this.selectBox.enabled = true; + if (camera.boxEnabled && fullLayout.dragmode === "zoom") { + this.selectBox.enabled = true; - this.selectBox.selectBox = [ - Math.min(camera.boxStart[0], camera.boxEnd[0]), - Math.min(camera.boxStart[1], camera.boxEnd[1]), - Math.max(camera.boxStart[0], camera.boxEnd[0]), - Math.max(camera.boxStart[1], camera.boxEnd[1]) - ]; + this.selectBox.selectBox = [ + Math.min(camera.boxStart[0], camera.boxEnd[0]), + Math.min(camera.boxStart[1], camera.boxEnd[1]), + Math.max(camera.boxStart[0], camera.boxEnd[0]), + Math.max(camera.boxStart[1], camera.boxEnd[1]) + ]; - glplot.setDirty(); - } - else { - this.selectBox.enabled = false; + glplot.setDirty(); + } else { + this.selectBox.enabled = false; - var size = fullLayout._size, - domainX = this.xaxis.domain, - domainY = this.yaxis.domain; + var size = fullLayout._size, + domainX = this.xaxis.domain, + domainY = this.yaxis.domain; - var result = glplot.pick( - (x / glplot.pixelRatio) + size.l + domainX[0] * size.w, - (y / glplot.pixelRatio) - (size.t + (1 - domainY[1]) * size.h) - ); + var result = glplot.pick( + x / glplot.pixelRatio + size.l + domainX[0] * size.w, + y / glplot.pixelRatio - (size.t + (1 - domainY[1]) * size.h) + ); - var nextSelection = result && result.object._trace.handlePick(result); + var nextSelection = result && result.object._trace.handlePick(result); - if(nextSelection && mouseUp) { - this.emitPointAction(nextSelection, 'plotly_click'); - } + if (nextSelection && mouseUp) { + this.emitPointAction(nextSelection, "plotly_click"); + } - if(result && result.object._trace.hoverinfo !== 'skip' && fullLayout.hovermode) { - - if(nextSelection && ( - !this.lastPickResult || - this.lastPickResult.traceUid !== nextSelection.trace.uid || - this.lastPickResult.dataCoord[0] !== nextSelection.dataCoord[0] || - this.lastPickResult.dataCoord[1] !== nextSelection.dataCoord[1]) - ) { - var selection = nextSelection; - - this.lastPickResult = { - traceUid: nextSelection.trace ? nextSelection.trace.uid : null, - dataCoord: nextSelection.dataCoord.slice() - }; - this.spikes.update({ center: result.dataCoord }); - - selection.screenCoord = [ - ((glplot.viewBox[2] - glplot.viewBox[0]) * - (result.dataCoord[0] - glplot.dataBox[0]) / - (glplot.dataBox[2] - glplot.dataBox[0]) + glplot.viewBox[0]) / - glplot.pixelRatio, - (this.canvas.height - (glplot.viewBox[3] - glplot.viewBox[1]) * - (result.dataCoord[1] - glplot.dataBox[1]) / - (glplot.dataBox[3] - glplot.dataBox[1]) - glplot.viewBox[1]) / - glplot.pixelRatio - ]; - - // this needs to happen before the next block that deletes traceCoord data - // also it's important to copy, otherwise data is lost by the time event data is read - this.emitPointAction(nextSelection, 'plotly_hover'); - - var hoverinfo = selection.hoverinfo; - if(hoverinfo !== 'all') { - var parts = hoverinfo.split('+'); - if(parts.indexOf('x') === -1) selection.traceCoord[0] = undefined; - if(parts.indexOf('y') === -1) selection.traceCoord[1] = undefined; - if(parts.indexOf('z') === -1) selection.traceCoord[2] = undefined; - if(parts.indexOf('text') === -1) selection.textLabel = undefined; - if(parts.indexOf('name') === -1) selection.name = undefined; - } - - Fx.loneHover({ - x: selection.screenCoord[0], - y: selection.screenCoord[1], - xLabel: this.hoverFormatter('xaxis', selection.traceCoord[0]), - yLabel: this.hoverFormatter('yaxis', selection.traceCoord[1]), - zLabel: selection.traceCoord[2], - text: selection.textLabel, - name: selection.name, - color: selection.color - }, { - container: this.svgContainer - }); - } - } - else if(!result && this.lastPickResult) { - this.spikes.update({}); - this.lastPickResult = null; - this.graphDiv.emit('plotly_unhover'); - Fx.loneUnhover(this.svgContainer); + if ( + result && + result.object._trace.hoverinfo !== "skip" && + fullLayout.hovermode + ) { + if ( + nextSelection && + (!this.lastPickResult || + this.lastPickResult.traceUid !== nextSelection.trace.uid || + this.lastPickResult.dataCoord[0] !== nextSelection.dataCoord[0] || + this.lastPickResult.dataCoord[1] !== nextSelection.dataCoord[1]) + ) { + var selection = nextSelection; + + this.lastPickResult = { + traceUid: nextSelection.trace ? nextSelection.trace.uid : null, + dataCoord: nextSelection.dataCoord.slice() + }; + this.spikes.update({ center: result.dataCoord }); + + selection.screenCoord = [ + ((glplot.viewBox[2] - glplot.viewBox[0]) * + (result.dataCoord[0] - glplot.dataBox[0]) / + (glplot.dataBox[2] - glplot.dataBox[0]) + + glplot.viewBox[0]) / + glplot.pixelRatio, + (this.canvas.height - + (glplot.viewBox[3] - glplot.viewBox[1]) * + (result.dataCoord[1] - glplot.dataBox[1]) / + (glplot.dataBox[3] - glplot.dataBox[1]) - + glplot.viewBox[1]) / + glplot.pixelRatio + ]; + + // this needs to happen before the next block that deletes traceCoord data + // also it's important to copy, otherwise data is lost by the time event data is read + this.emitPointAction(nextSelection, "plotly_hover"); + + var hoverinfo = selection.hoverinfo; + if (hoverinfo !== "all") { + var parts = hoverinfo.split("+"); + if (parts.indexOf("x") === -1) selection.traceCoord[0] = undefined; + if (parts.indexOf("y") === -1) selection.traceCoord[1] = undefined; + if (parts.indexOf("z") === -1) selection.traceCoord[2] = undefined; + if (parts.indexOf("text") === -1) selection.textLabel = undefined; + if (parts.indexOf("name") === -1) selection.name = undefined; } + + Fx.loneHover( + { + x: selection.screenCoord[0], + y: selection.screenCoord[1], + xLabel: this.hoverFormatter("xaxis", selection.traceCoord[0]), + yLabel: this.hoverFormatter("yaxis", selection.traceCoord[1]), + zLabel: selection.traceCoord[2], + text: selection.textLabel, + name: selection.name, + color: selection.color + }, + { container: this.svgContainer } + ); + } + } else if (!result && this.lastPickResult) { + this.spikes.update({}); + this.lastPickResult = null; + this.graphDiv.emit("plotly_unhover"); + Fx.loneUnhover(this.svgContainer); } + } - glplot.draw(); + glplot.draw(); }; proto.hoverFormatter = function(axisName, val) { - if(val === undefined) return undefined; + if (val === undefined) return undefined; - var axis = this[axisName]; - return Axes.tickText(axis, axis.c2l(val), 'hover').text; + var axis = this[axisName]; + return Axes.tickText(axis, axis.c2l(val), "hover").text; }; diff --git a/src/plots/gl3d/camera.js b/src/plots/gl3d/camera.js index 70f93ecb2e5..31b20d7f680 100644 --- a/src/plots/gl3d/camera.js +++ b/src/plots/gl3d/camera.js @@ -5,237 +5,268 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = createCamera; -var now = require('right-now'); -var createView = require('3d-view'); -var mouseChange = require('mouse-change'); -var mouseWheel = require('mouse-wheel'); +var now = require("right-now"); +var createView = require("3d-view"); +var mouseChange = require("mouse-change"); +var mouseWheel = require("mouse-wheel"); function createCamera(element, options) { - element = element || document.body; - options = options || {}; + element = element || document.body; + options = options || {}; - var limits = [ 0.01, Infinity ]; - if('distanceLimits' in options) { - limits[0] = options.distanceLimits[0]; - limits[1] = options.distanceLimits[1]; - } - if('zoomMin' in options) { - limits[0] = options.zoomMin; - } - if('zoomMax' in options) { - limits[1] = options.zoomMax; + var limits = [0.01, Infinity]; + if ("distanceLimits" in options) { + limits[0] = options.distanceLimits[0]; + limits[1] = options.distanceLimits[1]; + } + if ("zoomMin" in options) { + limits[0] = options.zoomMin; + } + if ("zoomMax" in options) { + limits[1] = options.zoomMax; + } + + var view = createView({ + center: options.center || [0, 0, 0], + up: options.up || [0, 1, 0], + eye: options.eye || [0, 0, 10], + mode: options.mode || "orbit", + distanceLimits: limits + }); + + var pmatrix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + var distance = 0.0; + var width = element.clientWidth; + var height = element.clientHeight; + + var camera = { + keyBindingMode: "rotate", + view: view, + element: element, + delay: options.delay || 16, + rotateSpeed: options.rotateSpeed || 1, + zoomSpeed: options.zoomSpeed || 1, + translateSpeed: options.translateSpeed || 1, + flipX: !!options.flipX, + flipY: !!options.flipY, + modes: view.modes, + tick: function() { + var t = now(); + var delay = this.delay; + var ctime = t - 2 * delay; + view.idle(t - delay); + view.recalcMatrix(ctime); + view.flush(t - (100 + delay * 2)); + var allEqual = true; + var matrix = view.computedMatrix; + for (var i = 0; i < 16; ++i) { + allEqual = allEqual && pmatrix[i] === matrix[i]; + pmatrix[i] = matrix[i]; + } + var sizeChanged = element.clientWidth === width && + element.clientHeight === height; + width = element.clientWidth; + height = element.clientHeight; + if (allEqual) return !sizeChanged; + distance = Math.exp(view.computedRadius[0]); + return true; + }, + lookAt: function(center, eye, up) { + view.lookAt(view.lastT(), center, eye, up); + }, + rotate: function(pitch, yaw, roll) { + view.rotate(view.lastT(), pitch, yaw, roll); + }, + pan: function(dx, dy, dz) { + view.pan(view.lastT(), dx, dy, dz); + }, + translate: function(dx, dy, dz) { + view.translate(view.lastT(), dx, dy, dz); } + }; - var view = createView({ - center: options.center || [0, 0, 0], - up: options.up || [0, 1, 0], - eye: options.eye || [0, 0, 10], - mode: options.mode || 'orbit', - distanceLimits: limits - }); - - var pmatrix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - var distance = 0.0; - var width = element.clientWidth; - var height = element.clientHeight; - - var camera = { - keyBindingMode: 'rotate', - view: view, - element: element, - delay: options.delay || 16, - rotateSpeed: options.rotateSpeed || 1, - zoomSpeed: options.zoomSpeed || 1, - translateSpeed: options.translateSpeed || 1, - flipX: !!options.flipX, - flipY: !!options.flipY, - modes: view.modes, - tick: function() { - var t = now(); - var delay = this.delay; - var ctime = t - 2 * delay; - view.idle(t - delay); - view.recalcMatrix(ctime); - view.flush(t - (100 + delay * 2)); - var allEqual = true; - var matrix = view.computedMatrix; - for(var i = 0; i < 16; ++i) { - allEqual = allEqual && (pmatrix[i] === matrix[i]); - pmatrix[i] = matrix[i]; - } - var sizeChanged = - element.clientWidth === width && - element.clientHeight === height; - width = element.clientWidth; - height = element.clientHeight; - if(allEqual) return !sizeChanged; - distance = Math.exp(view.computedRadius[0]); - return true; - }, - lookAt: function(center, eye, up) { - view.lookAt(view.lastT(), center, eye, up); - }, - rotate: function(pitch, yaw, roll) { - view.rotate(view.lastT(), pitch, yaw, roll); - }, - pan: function(dx, dy, dz) { - view.pan(view.lastT(), dx, dy, dz); - }, - translate: function(dx, dy, dz) { - view.translate(view.lastT(), dx, dy, dz); + Object.defineProperties(camera, { + matrix: { + get: function() { + return view.computedMatrix; + }, + set: function(mat) { + view.setMatrix(view.lastT(), mat); + return view.computedMatrix; + }, + enumerable: true + }, + mode: { + get: function() { + return view.getMode(); + }, + set: function(mode) { + var curUp = view.computedUp.slice(); + var curEye = view.computedEye.slice(); + var curCenter = view.computedCenter.slice(); + view.setMode(mode); + if (mode === "turntable") { + // Hacky time warping stuff to generate smooth animation + var t0 = now(); + view._active.lookAt(t0, curEye, curCenter, curUp); + view._active.lookAt(t0 + 500, curEye, curCenter, [0, 0, 1]); + view._active.flush(t0); } - }; - - Object.defineProperties(camera, { - matrix: { - get: function() { - return view.computedMatrix; - }, - set: function(mat) { - view.setMatrix(view.lastT(), mat); - return view.computedMatrix; - }, - enumerable: true - }, - mode: { - get: function() { - return view.getMode(); - }, - set: function(mode) { - var curUp = view.computedUp.slice(); - var curEye = view.computedEye.slice(); - var curCenter = view.computedCenter.slice(); - view.setMode(mode); - if(mode === 'turntable') { - // Hacky time warping stuff to generate smooth animation - var t0 = now(); - view._active.lookAt(t0, curEye, curCenter, curUp); - view._active.lookAt(t0 + 500, curEye, curCenter, [0, 0, 1]); - view._active.flush(t0); - } - return view.getMode(); - }, - enumerable: true - }, - center: { - get: function() { - return view.computedCenter; - }, - set: function(ncenter) { - view.lookAt(view.lastT(), null, ncenter); - return view.computedCenter; - }, - enumerable: true - }, - eye: { - get: function() { - return view.computedEye; - }, - set: function(neye) { - view.lookAt(view.lastT(), neye); - return view.computedEye; - }, - enumerable: true - }, - up: { - get: function() { - return view.computedUp; - }, - set: function(nup) { - view.lookAt(view.lastT(), null, null, nup); - return view.computedUp; - }, - enumerable: true - }, - distance: { - get: function() { - return distance; - }, - set: function(d) { - view.setDistance(view.lastT(), d); - return d; - }, - enumerable: true - }, - distanceLimits: { - get: function() { - return view.getDistanceLimits(limits); - }, - set: function(v) { - view.setDistanceLimits(v); - return v; - }, - enumerable: true - } - }); + return view.getMode(); + }, + enumerable: true + }, + center: { + get: function() { + return view.computedCenter; + }, + set: function(ncenter) { + view.lookAt(view.lastT(), null, ncenter); + return view.computedCenter; + }, + enumerable: true + }, + eye: { + get: function() { + return view.computedEye; + }, + set: function(neye) { + view.lookAt(view.lastT(), neye); + return view.computedEye; + }, + enumerable: true + }, + up: { + get: function() { + return view.computedUp; + }, + set: function(nup) { + view.lookAt(view.lastT(), null, null, nup); + return view.computedUp; + }, + enumerable: true + }, + distance: { + get: function() { + return distance; + }, + set: function(d) { + view.setDistance(view.lastT(), d); + return d; + }, + enumerable: true + }, + distanceLimits: { + get: function() { + return view.getDistanceLimits(limits); + }, + set: function(v) { + view.setDistanceLimits(v); + return v; + }, + enumerable: true + } + }); - element.addEventListener('contextmenu', function(ev) { - ev.preventDefault(); - return false; - }); + element.addEventListener("contextmenu", function(ev) { + ev.preventDefault(); + return false; + }); - var lastX = 0, lastY = 0; - mouseChange(element, function(buttons, x, y, mods) { - var rotate = camera.keyBindingMode === 'rotate'; - var pan = camera.keyBindingMode === 'pan'; - var zoom = camera.keyBindingMode === 'zoom'; + var lastX = 0, lastY = 0; + mouseChange(element, function(buttons, x, y, mods) { + var rotate = camera.keyBindingMode === "rotate"; + var pan = camera.keyBindingMode === "pan"; + var zoom = camera.keyBindingMode === "zoom"; - var ctrl = !!mods.control; - var alt = !!mods.alt; - var shift = !!mods.shift; - var left = !!(buttons & 1); - var right = !!(buttons & 2); - var middle = !!(buttons & 4); + var ctrl = !!mods.control; + var alt = !!mods.alt; + var shift = !!mods.shift; + var left = !!(buttons & 1); + var right = !!(buttons & 2); + var middle = !!(buttons & 4); - var scale = 1.0 / element.clientHeight; - var dx = scale * (x - lastX); - var dy = scale * (y - lastY); + var scale = 1.0 / element.clientHeight; + var dx = scale * (x - lastX); + var dy = scale * (y - lastY); - var flipX = camera.flipX ? 1 : -1; - var flipY = camera.flipY ? 1 : -1; + var flipX = camera.flipX ? 1 : -1; + var flipY = camera.flipY ? 1 : -1; - var t = now(); + var t = now(); - var drot = Math.PI * camera.rotateSpeed; + var drot = Math.PI * camera.rotateSpeed; - if((rotate && left && !ctrl && !alt && !shift) || (left && !ctrl && !alt && shift)) { - // Rotate - view.rotate(t, flipX * drot * dx, -flipY * drot * dy, 0); - } + if ( + rotate && left && !ctrl && !alt && !shift || + left && !ctrl && !alt && shift + ) { + // Rotate + view.rotate(t, flipX * drot * dx, (-flipY) * drot * dy, 0); + } - if((pan && left && !ctrl && !alt && !shift) || right || (left && ctrl && !alt && !shift)) { - // Pan - view.pan(t, -camera.translateSpeed * dx * distance, camera.translateSpeed * dy * distance, 0); - } + if ( + pan && left && !ctrl && !alt && !shift || + right || + left && ctrl && !alt && !shift + ) { + // Pan + view.pan( + t, + (-camera.translateSpeed) * dx * distance, + camera.translateSpeed * dy * distance, + 0 + ); + } - if((zoom && left && !ctrl && !alt && !shift) || middle || (left && !ctrl && alt && !shift)) { - // Zoom - var kzoom = -camera.zoomSpeed * dy / window.innerHeight * (t - view.lastT()) * 100; - view.pan(t, 0, 0, distance * (Math.exp(kzoom) - 1)); - } + if ( + zoom && left && !ctrl && !alt && !shift || + middle || + left && !ctrl && alt && !shift + ) { + // Zoom + var kzoom = (-camera.zoomSpeed) * + dy / + window.innerHeight * + (t - view.lastT()) * + 100; + view.pan(t, 0, 0, distance * (Math.exp(kzoom) - 1)); + } - lastX = x; - lastY = y; - - return true; - }); - - mouseWheel(element, function(dx, dy) { - var flipX = camera.flipX ? 1 : -1; - var flipY = camera.flipY ? 1 : -1; - var t = now(); - if(Math.abs(dx) > Math.abs(dy)) { - view.rotate(t, 0, 0, -dx * flipX * Math.PI * camera.rotateSpeed / window.innerWidth); - } else { - var kzoom = -camera.zoomSpeed * flipY * dy / window.innerHeight * (t - view.lastT()) / 100.0; - view.pan(t, 0, 0, distance * (Math.exp(kzoom) - 1)); - } - }, true); + lastX = x; + lastY = y; + + return true; + }); + + mouseWheel( + element, + function(dx, dy) { + var flipX = camera.flipX ? 1 : -1; + var flipY = camera.flipY ? 1 : -1; + var t = now(); + if (Math.abs(dx) > Math.abs(dy)) { + view.rotate( + t, + 0, + 0, + (-dx) * flipX * Math.PI * camera.rotateSpeed / window.innerWidth + ); + } else { + var kzoom = (-camera.zoomSpeed) * + flipY * + dy / + window.innerHeight * + (t - view.lastT()) / + 100.0; + view.pan(t, 0, 0, distance * (Math.exp(kzoom) - 1)); + } + }, + true + ); - return camera; + return camera; } diff --git a/src/plots/gl3d/index.js b/src/plots/gl3d/index.js index 6e42343abc0..dc4d63806ac 100644 --- a/src/plots/gl3d/index.js +++ b/src/plots/gl3d/index.js @@ -5,126 +5,128 @@ * 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 Scene = require("./scene"); +var Plots = require("../plots"); +var Lib = require("../../lib"); +var xmlnsNamespaces = require("../../constants/xmlns_namespaces"); +var axesNames = ["xaxis", "yaxis", "zaxis"]; -'use strict'; +exports.name = "gl3d"; -var Scene = require('./scene'); -var Plots = require('../plots'); -var Lib = require('../../lib'); -var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); +exports.attr = "scene"; -var axesNames = ['xaxis', 'yaxis', 'zaxis']; - - -exports.name = 'gl3d'; - -exports.attr = 'scene'; - -exports.idRoot = 'scene'; +exports.idRoot = "scene"; exports.idRegex = /^scene([2-9]|[1-9][0-9]+)?$/; exports.attrRegex = /^scene([2-9]|[1-9][0-9]+)?$/; -exports.attributes = require('./layout/attributes'); +exports.attributes = require("./layout/attributes"); -exports.layoutAttributes = require('./layout/layout_attributes'); +exports.layoutAttributes = require("./layout/layout_attributes"); -exports.supplyLayoutDefaults = require('./layout/defaults'); +exports.supplyLayoutDefaults = require("./layout/defaults"); exports.plot = function plotGl3d(gd) { - var fullLayout = gd._fullLayout, - fullData = gd._fullData, - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'); - - for(var i = 0; i < sceneIds.length; i++) { - var sceneId = sceneIds[i], - fullSceneData = Plots.getSubplotData(fullData, 'gl3d', sceneId), - sceneLayout = fullLayout[sceneId], - scene = sceneLayout._scene; - - if(!scene) { - initAxes(gd, sceneLayout); - - scene = new Scene({ - id: sceneId, - graphDiv: gd, - container: gd.querySelector('.gl-container'), - staticPlot: gd._context.staticPlot, - plotGlPixelRatio: gd._context.plotGlPixelRatio - }, - fullLayout - ); - - // set ref to Scene instance - sceneLayout._scene = scene; - } - - // save 'initial' camera settings for modebar button - if(!scene.cameraInitial) { - scene.cameraInitial = Lib.extendDeep({}, sceneLayout.camera); - } - - scene.plot(fullSceneData, fullLayout, gd.layout); + var fullLayout = gd._fullLayout, + fullData = gd._fullData, + sceneIds = Plots.getSubplotIds(fullLayout, "gl3d"); + + for (var i = 0; i < sceneIds.length; i++) { + var sceneId = sceneIds[i], + fullSceneData = Plots.getSubplotData(fullData, "gl3d", sceneId), + sceneLayout = fullLayout[sceneId], + scene = sceneLayout._scene; + + if (!scene) { + initAxes(gd, sceneLayout); + + scene = new Scene( + { + id: sceneId, + graphDiv: gd, + container: gd.querySelector(".gl-container"), + staticPlot: gd._context.staticPlot, + plotGlPixelRatio: gd._context.plotGlPixelRatio + }, + fullLayout + ); + + // set ref to Scene instance + sceneLayout._scene = scene; } + + // save 'initial' camera settings for modebar button + if (!scene.cameraInitial) { + scene.cameraInitial = Lib.extendDeep({}, sceneLayout.camera); + } + + scene.plot(fullSceneData, fullLayout, gd.layout); + } }; -exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, 'gl3d'); +exports.clean = function( + newFullData, + newFullLayout, + oldFullData, + oldFullLayout +) { + var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, "gl3d"); - for(var i = 0; i < oldSceneKeys.length; i++) { - var oldSceneKey = oldSceneKeys[i]; + for (var i = 0; i < oldSceneKeys.length; i++) { + var oldSceneKey = oldSceneKeys[i]; - if(!newFullLayout[oldSceneKey] && !!oldFullLayout[oldSceneKey]._scene) { - oldFullLayout[oldSceneKey]._scene.destroy(); - } + if (!newFullLayout[oldSceneKey] && !!oldFullLayout[oldSceneKey]._scene) { + oldFullLayout[oldSceneKey]._scene.destroy(); } + } }; exports.toSVG = function(gd) { - var fullLayout = gd._fullLayout, - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'), - size = fullLayout._size; - - for(var i = 0; i < sceneIds.length; i++) { - var sceneLayout = fullLayout[sceneIds[i]], - domain = sceneLayout.domain, - scene = sceneLayout._scene; - - var imageData = scene.toImage('png'); - var image = fullLayout._glimages.append('svg:image'); - - image.attr({ - xmlns: xmlnsNamespaces.svg, - 'xlink:href': imageData, - x: size.l + size.w * domain.x[0], - y: size.t + size.h * (1 - domain.y[1]), - width: size.w * (domain.x[1] - domain.x[0]), - height: size.h * (domain.y[1] - domain.y[0]), - preserveAspectRatio: 'none' - }); - - scene.destroy(); - } + var fullLayout = gd._fullLayout, + sceneIds = Plots.getSubplotIds(fullLayout, "gl3d"), + size = fullLayout._size; + + for (var i = 0; i < sceneIds.length; i++) { + var sceneLayout = fullLayout[sceneIds[i]], + domain = sceneLayout.domain, + scene = sceneLayout._scene; + + var imageData = scene.toImage("png"); + var image = fullLayout._glimages.append("svg:image"); + + image.attr({ + xmlns: xmlnsNamespaces.svg, + "xlink:href": imageData, + x: size.l + size.w * domain.x[0], + y: size.t + size.h * (1 - domain.y[1]), + width: size.w * (domain.x[1] - domain.x[0]), + height: size.h * (domain.y[1] - domain.y[0]), + preserveAspectRatio: "none" + }); + + scene.destroy(); + } }; // clean scene ids, 'scene1' -> 'scene' exports.cleanId = function cleanId(id) { - if(!id.match(/^scene[0-9]*$/)) return; + if (!id.match(/^scene[0-9]*$/)) return; - var sceneNum = id.substr(5); - if(sceneNum === '1') sceneNum = ''; + var sceneNum = id.substr(5); + if (sceneNum === "1") sceneNum = ""; - return 'scene' + sceneNum; + return "scene" + sceneNum; }; -exports.setConvert = require('./set_convert'); +exports.setConvert = require("./set_convert"); function initAxes(gd, sceneLayout) { - for(var j = 0; j < 3; ++j) { - var axisName = axesNames[j]; + for (var j = 0; j < 3; ++j) { + var axisName = axesNames[j]; - sceneLayout[axisName]._gd = gd; - } + sceneLayout[axisName]._gd = gd; + } } diff --git a/src/plots/gl3d/layout/attributes.js b/src/plots/gl3d/layout/attributes.js index 4c8c714766a..3d3f5abcf6d 100644 --- a/src/plots/gl3d/layout/attributes.js +++ b/src/plots/gl3d/layout/attributes.js @@ -5,22 +5,19 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - scene: { - valType: 'subplotid', - role: 'info', - dflt: 'scene', - description: [ - 'Sets a reference between this trace\'s 3D coordinate system and', - 'a 3D scene.', - 'If *scene* (the default value), the (x,y,z) coordinates refer to', - '`layout.scene`.', - 'If *scene2*, the (x,y,z) coordinates refer to `layout.scene2`,', - 'and so on.' - ].join(' ') - } + scene: { + valType: "subplotid", + role: "info", + dflt: "scene", + description: [ + "Sets a reference between this trace's 3D coordinate system and", + "a 3D scene.", + "If *scene* (the default value), the (x,y,z) coordinates refer to", + "`layout.scene`.", + "If *scene2*, the (x,y,z) coordinates refer to `layout.scene2`,", + "and so on." + ].join(" ") + } }; diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index fe1f672b7ba..727cebd221d 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -5,110 +5,109 @@ * 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 Color = require('../../../components/color'); -var axesAttrs = require('../../cartesian/layout_attributes'); -var extendFlat = require('../../../lib/extend').extendFlat; - +"use strict"; +var Color = require("../../../components/color"); +var axesAttrs = require("../../cartesian/layout_attributes"); +var extendFlat = require("../../../lib/extend").extendFlat; module.exports = { - showspikes: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Sets whether or not spikes starting from', - 'data points to this axis\' wall are shown on hover.' - ].join(' ') - }, - spikesides: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Sets whether or not spikes extending from the', - 'projection data points to this axis\' wall boundaries', - 'are shown on hover.' - ].join(' ') - }, - spikethickness: { - valType: 'number', - role: 'style', - min: 0, - dflt: 2, - description: 'Sets the thickness (in px) of the spikes.' - }, - spikecolor: { - valType: 'color', - role: 'style', - dflt: Color.defaultLine, - description: 'Sets the color of the spikes.' - }, - showbackground: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Sets whether or not this axis\' wall', - 'has a background color.' - ].join(' ') - }, - backgroundcolor: { - valType: 'color', - role: 'style', - dflt: 'rgba(204, 204, 204, 0.5)', - description: 'Sets the background color of this axis\' wall.' - }, - showaxeslabels: { - valType: 'boolean', - role: 'info', - dflt: true, - description: 'Sets whether or not this axis is labeled' - }, - color: axesAttrs.color, - categoryorder: axesAttrs.categoryorder, - categoryarray: axesAttrs.categoryarray, - title: axesAttrs.title, - titlefont: axesAttrs.titlefont, - type: axesAttrs.type, - autorange: axesAttrs.autorange, - rangemode: axesAttrs.rangemode, - range: axesAttrs.range, - // ticks - tickmode: axesAttrs.tickmode, - nticks: axesAttrs.nticks, - tick0: axesAttrs.tick0, - dtick: axesAttrs.dtick, - tickvals: axesAttrs.tickvals, - ticktext: axesAttrs.ticktext, - ticks: axesAttrs.ticks, - mirror: axesAttrs.mirror, - ticklen: axesAttrs.ticklen, - tickwidth: axesAttrs.tickwidth, - tickcolor: axesAttrs.tickcolor, - showticklabels: axesAttrs.showticklabels, - tickfont: axesAttrs.tickfont, - tickangle: axesAttrs.tickangle, - tickprefix: axesAttrs.tickprefix, - showtickprefix: axesAttrs.showtickprefix, - ticksuffix: axesAttrs.ticksuffix, - showticksuffix: axesAttrs.showticksuffix, - showexponent: axesAttrs.showexponent, - exponentformat: axesAttrs.exponentformat, - separatethousands: axesAttrs.separatethousands, - tickformat: axesAttrs.tickformat, - hoverformat: axesAttrs.hoverformat, - // lines and grids - showline: axesAttrs.showline, - linecolor: axesAttrs.linecolor, - linewidth: axesAttrs.linewidth, - showgrid: axesAttrs.showgrid, - gridcolor: extendFlat({}, axesAttrs.gridcolor, // shouldn't this be on-par with 2D? - {dflt: 'rgb(204, 204, 204)'}), - gridwidth: axesAttrs.gridwidth, - zeroline: axesAttrs.zeroline, - zerolinecolor: axesAttrs.zerolinecolor, - zerolinewidth: axesAttrs.zerolinewidth + showspikes: { + valType: "boolean", + role: "info", + dflt: true, + description: [ + "Sets whether or not spikes starting from", + "data points to this axis' wall are shown on hover." + ].join(" ") + }, + spikesides: { + valType: "boolean", + role: "info", + dflt: true, + description: [ + "Sets whether or not spikes extending from the", + "projection data points to this axis' wall boundaries", + "are shown on hover." + ].join(" ") + }, + spikethickness: { + valType: "number", + role: "style", + min: 0, + dflt: 2, + description: "Sets the thickness (in px) of the spikes." + }, + spikecolor: { + valType: "color", + role: "style", + dflt: Color.defaultLine, + description: "Sets the color of the spikes." + }, + showbackground: { + valType: "boolean", + role: "info", + dflt: false, + description: [ + "Sets whether or not this axis' wall", + "has a background color." + ].join(" ") + }, + backgroundcolor: { + valType: "color", + role: "style", + dflt: "rgba(204, 204, 204, 0.5)", + description: "Sets the background color of this axis' wall." + }, + showaxeslabels: { + valType: "boolean", + role: "info", + dflt: true, + description: "Sets whether or not this axis is labeled" + }, + color: axesAttrs.color, + categoryorder: axesAttrs.categoryorder, + categoryarray: axesAttrs.categoryarray, + title: axesAttrs.title, + titlefont: axesAttrs.titlefont, + type: axesAttrs.type, + autorange: axesAttrs.autorange, + rangemode: axesAttrs.rangemode, + range: axesAttrs.range, + // ticks + tickmode: axesAttrs.tickmode, + nticks: axesAttrs.nticks, + tick0: axesAttrs.tick0, + dtick: axesAttrs.dtick, + tickvals: axesAttrs.tickvals, + ticktext: axesAttrs.ticktext, + ticks: axesAttrs.ticks, + mirror: axesAttrs.mirror, + ticklen: axesAttrs.ticklen, + tickwidth: axesAttrs.tickwidth, + tickcolor: axesAttrs.tickcolor, + showticklabels: axesAttrs.showticklabels, + tickfont: axesAttrs.tickfont, + tickangle: axesAttrs.tickangle, + tickprefix: axesAttrs.tickprefix, + showtickprefix: axesAttrs.showtickprefix, + ticksuffix: axesAttrs.ticksuffix, + showticksuffix: axesAttrs.showticksuffix, + showexponent: axesAttrs.showexponent, + exponentformat: axesAttrs.exponentformat, + separatethousands: axesAttrs.separatethousands, + tickformat: axesAttrs.tickformat, + hoverformat: axesAttrs.hoverformat, + // lines and grids + showline: axesAttrs.showline, + linecolor: axesAttrs.linecolor, + linewidth: axesAttrs.linewidth, + showgrid: axesAttrs.showgrid, + gridcolor: extendFlat({}, axesAttrs.gridcolor, { + // shouldn't this be on-par with 2D? + dflt: "rgb(204, 204, 204)" + }), + gridwidth: axesAttrs.gridwidth, + zeroline: axesAttrs.zeroline, + zerolinecolor: axesAttrs.zerolinecolor, + zerolinewidth: axesAttrs.zerolinewidth }; diff --git a/src/plots/gl3d/layout/axis_defaults.js b/src/plots/gl3d/layout/axis_defaults.js index 353d03eb379..78cf8681c5e 100644 --- a/src/plots/gl3d/layout/axis_defaults.js +++ b/src/plots/gl3d/layout/axis_defaults.js @@ -5,63 +5,60 @@ * 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 colorMix = require("tinycolor2").mix; +var Lib = require("../../../lib"); -'use strict'; +var layoutAttributes = require("./axis_attributes"); +var handleAxisDefaults = require("../../cartesian/axis_defaults"); -var colorMix = require('tinycolor2').mix; - -var Lib = require('../../../lib'); - -var layoutAttributes = require('./axis_attributes'); -var handleAxisDefaults = require('../../cartesian/axis_defaults'); - -var axesNames = ['xaxis', 'yaxis', 'zaxis']; +var axesNames = ["xaxis", "yaxis", "zaxis"]; // TODO: hard-coded lightness fraction based on gridline default colors // that differ from other subplot types. var gridLightness = 100 * (204 - 0x44) / (255 - 0x44); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) { - var containerIn, containerOut; - - function coerce(attr, dflt) { - return Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt); + var containerIn, containerOut; + + function coerce(attr, dflt) { + return Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt); + } + + for (var j = 0; j < axesNames.length; j++) { + var axName = axesNames[j]; + containerIn = layoutIn[axName] || {}; + + containerOut = { _id: axName[0] + options.scene, _name: axName }; + + layoutOut[ + axName + ] = containerOut = handleAxisDefaults(containerIn, containerOut, coerce, { + font: options.font, + letter: axName[0], + data: options.data, + showGrid: true, + bgColor: options.bgColor, + calendar: options.calendar + }); + + coerce( + "gridcolor", + colorMix(containerOut.color, options.bgColor, gridLightness).toRgbString() + ); + coerce("title", axName[0]); + + // shouldn't this be on-par with 2D? + containerOut.setScale = Lib.noop; + + if (coerce("showspikes")) { + coerce("spikesides"); + coerce("spikethickness"); + coerce("spikecolor", containerOut.color); } - for(var j = 0; j < axesNames.length; j++) { - var axName = axesNames[j]; - containerIn = layoutIn[axName] || {}; - - containerOut = { - _id: axName[0] + options.scene, - _name: axName - }; - - layoutOut[axName] = containerOut = handleAxisDefaults( - containerIn, - containerOut, - coerce, { - font: options.font, - letter: axName[0], - data: options.data, - showGrid: true, - bgColor: options.bgColor, - calendar: options.calendar - }); - - coerce('gridcolor', colorMix(containerOut.color, options.bgColor, gridLightness).toRgbString()); - coerce('title', axName[0]); // shouldn't this be on-par with 2D? - - containerOut.setScale = Lib.noop; - - if(coerce('showspikes')) { - coerce('spikesides'); - coerce('spikethickness'); - coerce('spikecolor', containerOut.color); - } - - coerce('showaxeslabels'); - if(coerce('showbackground')) coerce('backgroundcolor'); - } + coerce("showaxeslabels"); + if (coerce("showbackground")) coerce("backgroundcolor"); + } }; diff --git a/src/plots/gl3d/layout/convert.js b/src/plots/gl3d/layout/convert.js index 1bba5caebbe..51e762d2272 100644 --- a/src/plots/gl3d/layout/convert.js +++ b/src/plots/gl3d/layout/convert.js @@ -5,149 +5,156 @@ * 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 arrtools = require('arraytools'); -var convertHTMLToUnicode = require('../../../lib/html2unicode'); -var str2RgbaArray = require('../../../lib/str2rgbarray'); +"use strict"; +var arrtools = require("arraytools"); +var convertHTMLToUnicode = require("../../../lib/html2unicode"); +var str2RgbaArray = require("../../../lib/str2rgbarray"); var arrayCopy1D = arrtools.copy1D; -var AXES_NAMES = ['xaxis', 'yaxis', 'zaxis']; +var AXES_NAMES = ["xaxis", "yaxis", "zaxis"]; function AxesOptions() { - this.bounds = [ - [-10, -10, -10], - [10, 10, 10] - ]; - - this.ticks = [ [], [], [] ]; - this.tickEnable = [ true, true, true ]; - this.tickFont = [ 'sans-serif', 'sans-serif', 'sans-serif' ]; - this.tickSize = [ 12, 12, 12 ]; - this.tickAngle = [ 0, 0, 0 ]; - this.tickColor = [ [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1] ]; - this.tickPad = [ 18, 18, 18 ]; - - this.labels = [ 'x', 'y', 'z' ]; - this.labelEnable = [ true, true, true ]; - this.labelFont = ['Open Sans', 'Open Sans', 'Open Sans']; - this.labelSize = [ 20, 20, 20 ]; - this.labelAngle = [ 0, 0, 0 ]; - this.labelColor = [ [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1] ]; - this.labelPad = [ 30, 30, 30 ]; - - this.lineEnable = [ true, true, true ]; - this.lineMirror = [ false, false, false ]; - this.lineWidth = [ 1, 1, 1 ]; - this.lineColor = [ [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1] ]; - - this.lineTickEnable = [ true, true, true ]; - this.lineTickMirror = [ false, false, false ]; - this.lineTickLength = [ 10, 10, 10 ]; - this.lineTickWidth = [ 1, 1, 1 ]; - this.lineTickColor = [ [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1] ]; - - this.gridEnable = [ true, true, true ]; - this.gridWidth = [ 1, 1, 1 ]; - this.gridColor = [ [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1] ]; - - this.zeroEnable = [ true, true, true ]; - this.zeroLineColor = [ [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1] ]; - this.zeroLineWidth = [ 2, 2, 2 ]; - - this.backgroundEnable = [ true, true, true ]; - this.backgroundColor = [ [0.8, 0.8, 0.8, 0.5], - [0.8, 0.8, 0.8, 0.5], - [0.8, 0.8, 0.8, 0.5] ]; - - // some default values are stored for applying model transforms - this._defaultTickPad = arrayCopy1D(this.tickPad); - this._defaultLabelPad = arrayCopy1D(this.labelPad); - this._defaultLineTickLength = arrayCopy1D(this.lineTickLength); + this.bounds = [[-10, -10, -10], [10, 10, 10]]; + + this.ticks = [[], [], []]; + this.tickEnable = [true, true, true]; + this.tickFont = ["sans-serif", "sans-serif", "sans-serif"]; + this.tickSize = [12, 12, 12]; + this.tickAngle = [0, 0, 0]; + this.tickColor = [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]]; + this.tickPad = [18, 18, 18]; + + this.labels = ["x", "y", "z"]; + this.labelEnable = [true, true, true]; + this.labelFont = ["Open Sans", "Open Sans", "Open Sans"]; + this.labelSize = [20, 20, 20]; + this.labelAngle = [0, 0, 0]; + this.labelColor = [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]]; + this.labelPad = [30, 30, 30]; + + this.lineEnable = [true, true, true]; + this.lineMirror = [false, false, false]; + this.lineWidth = [1, 1, 1]; + this.lineColor = [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]]; + + this.lineTickEnable = [true, true, true]; + this.lineTickMirror = [false, false, false]; + this.lineTickLength = [10, 10, 10]; + this.lineTickWidth = [1, 1, 1]; + this.lineTickColor = [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]]; + + this.gridEnable = [true, true, true]; + this.gridWidth = [1, 1, 1]; + this.gridColor = [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]]; + + this.zeroEnable = [true, true, true]; + this.zeroLineColor = [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]]; + this.zeroLineWidth = [2, 2, 2]; + + this.backgroundEnable = [true, true, true]; + this.backgroundColor = [ + [0.8, 0.8, 0.8, 0.5], + [0.8, 0.8, 0.8, 0.5], + [0.8, 0.8, 0.8, 0.5] + ]; + + // some default values are stored for applying model transforms + this._defaultTickPad = arrayCopy1D(this.tickPad); + this._defaultLabelPad = arrayCopy1D(this.labelPad); + this._defaultLineTickLength = arrayCopy1D(this.lineTickLength); } var proto = AxesOptions.prototype; proto.merge = function(sceneLayout) { - var opts = this; - for(var i = 0; i < 3; ++i) { - var axes = sceneLayout[AXES_NAMES[i]]; - - // Axes labels - opts.labels[i] = convertHTMLToUnicode(axes.title); - if('titlefont' in axes) { - if(axes.titlefont.color) opts.labelColor[i] = str2RgbaArray(axes.titlefont.color); - if(axes.titlefont.family) opts.labelFont[i] = axes.titlefont.family; - if(axes.titlefont.size) opts.labelSize[i] = axes.titlefont.size; - } - - // Lines - if('showline' in axes) opts.lineEnable[i] = axes.showline; - if('linecolor' in axes) opts.lineColor[i] = str2RgbaArray(axes.linecolor); - if('linewidth' in axes) opts.lineWidth[i] = axes.linewidth; - - if('showgrid' in axes) opts.gridEnable[i] = axes.showgrid; - if('gridcolor' in axes) opts.gridColor[i] = str2RgbaArray(axes.gridcolor); - if('gridwidth' in axes) opts.gridWidth[i] = axes.gridwidth; - - // Remove zeroline if axis type is log - // otherwise the zeroline is incorrectly drawn at 1 on log axes - if(axes.type === 'log') opts.zeroEnable[i] = false; - else if('zeroline' in axes) opts.zeroEnable[i] = axes.zeroline; - if('zerolinecolor' in axes) opts.zeroLineColor[i] = str2RgbaArray(axes.zerolinecolor); - if('zerolinewidth' in axes) opts.zeroLineWidth[i] = axes.zerolinewidth; - - // tick lines - if('ticks' in axes && !!axes.ticks) opts.lineTickEnable[i] = true; - else opts.lineTickEnable[i] = false; - - if('ticklen' in axes) { - opts.lineTickLength[i] = opts._defaultLineTickLength[i] = axes.ticklen; - } - if('tickcolor' in axes) opts.lineTickColor[i] = str2RgbaArray(axes.tickcolor); - if('tickwidth' in axes) opts.lineTickWidth[i] = axes.tickwidth; - if('tickangle' in axes) { - opts.tickAngle[i] = (axes.tickangle === 'auto') ? - 0 : - Math.PI * -axes.tickangle / 180; - } - // tick labels - if('showticklabels' in axes) opts.tickEnable[i] = axes.showticklabels; - if('tickfont' in axes) { - if(axes.tickfont.color) opts.tickColor[i] = str2RgbaArray(axes.tickfont.color); - if(axes.tickfont.family) opts.tickFont[i] = axes.tickfont.family; - if(axes.tickfont.size) opts.tickSize[i] = axes.tickfont.size; - } - - if('mirror' in axes) { - if(['ticks', 'all', 'allticks'].indexOf(axes.mirror) !== -1) { - opts.lineTickMirror[i] = true; - opts.lineMirror[i] = true; - } else if(axes.mirror === true) { - opts.lineTickMirror[i] = false; - opts.lineMirror[i] = true; - } else { - opts.lineTickMirror[i] = false; - opts.lineMirror[i] = false; - } - } else opts.lineMirror[i] = false; - - // grid background - if('showbackground' in axes && axes.showbackground !== false) { - opts.backgroundEnable[i] = true; - opts.backgroundColor[i] = str2RgbaArray(axes.backgroundcolor); - } else opts.backgroundEnable[i] = false; + var opts = this; + for (var i = 0; i < 3; ++i) { + var axes = sceneLayout[AXES_NAMES[i]]; + + // Axes labels + opts.labels[i] = convertHTMLToUnicode(axes.title); + if ("titlefont" in axes) { + if (axes.titlefont.color) { + opts.labelColor[i] = str2RgbaArray(axes.titlefont.color); + } + if (axes.titlefont.family) opts.labelFont[i] = axes.titlefont.family; + if (axes.titlefont.size) opts.labelSize[i] = axes.titlefont.size; + } + + // Lines + if ("showline" in axes) opts.lineEnable[i] = axes.showline; + if ("linecolor" in axes) opts.lineColor[i] = str2RgbaArray(axes.linecolor); + if ("linewidth" in axes) opts.lineWidth[i] = axes.linewidth; + + if ("showgrid" in axes) opts.gridEnable[i] = axes.showgrid; + if ("gridcolor" in axes) opts.gridColor[i] = str2RgbaArray(axes.gridcolor); + if ("gridwidth" in axes) opts.gridWidth[i] = axes.gridwidth; + + // Remove zeroline if axis type is log + // otherwise the zeroline is incorrectly drawn at 1 on log axes + if (axes.type === "log") opts.zeroEnable[i] = false; + else if ("zeroline" in axes) opts.zeroEnable[i] = axes.zeroline; + if ("zerolinecolor" in axes) { + opts.zeroLineColor[i] = str2RgbaArray(axes.zerolinecolor); + } + if ("zerolinewidth" in axes) opts.zeroLineWidth[i] = axes.zerolinewidth; + + // tick lines + if ("ticks" in axes && !!axes.ticks) opts.lineTickEnable[i] = true; + else opts.lineTickEnable[i] = false; + + if ("ticklen" in axes) { + opts.lineTickLength[i] = opts._defaultLineTickLength[i] = axes.ticklen; + } + if ("tickcolor" in axes) { + opts.lineTickColor[i] = str2RgbaArray(axes.tickcolor); + } + if ("tickwidth" in axes) opts.lineTickWidth[i] = axes.tickwidth; + if ("tickangle" in axes) { + opts.tickAngle[i] = axes.tickangle === "auto" + ? 0 + : Math.PI * (-axes.tickangle) / 180; + } + // tick labels + if ("showticklabels" in axes) opts.tickEnable[i] = axes.showticklabels; + if ("tickfont" in axes) { + if (axes.tickfont.color) { + opts.tickColor[i] = str2RgbaArray(axes.tickfont.color); + } + if (axes.tickfont.family) opts.tickFont[i] = axes.tickfont.family; + if (axes.tickfont.size) opts.tickSize[i] = axes.tickfont.size; } -}; + if ("mirror" in axes) { + if (["ticks", "all", "allticks"].indexOf(axes.mirror) !== -1) { + opts.lineTickMirror[i] = true; + opts.lineMirror[i] = true; + } else if (axes.mirror === true) { + opts.lineTickMirror[i] = false; + opts.lineMirror[i] = true; + } else { + opts.lineTickMirror[i] = false; + opts.lineMirror[i] = false; + } + } else { + opts.lineMirror[i] = false; + } + + // grid background + if ("showbackground" in axes && axes.showbackground !== false) { + opts.backgroundEnable[i] = true; + opts.backgroundColor[i] = str2RgbaArray(axes.backgroundcolor); + } else { + opts.backgroundEnable[i] = false; + } + } +}; function createAxesOptions(plotlyOptions) { - var result = new AxesOptions(); - result.merge(plotlyOptions); - return result; + var result = new AxesOptions(); + result.merge(plotlyOptions); + return result; } module.exports = createAxesOptions; diff --git a/src/plots/gl3d/layout/defaults.js b/src/plots/gl3d/layout/defaults.js index 285da8b2920..28828c72e80 100644 --- a/src/plots/gl3d/layout/defaults.js +++ b/src/plots/gl3d/layout/defaults.js @@ -5,49 +5,43 @@ * 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 Color = require("../../../components/color"); - -'use strict'; - -var Color = require('../../../components/color'); - -var handleSubplotDefaults = require('../../subplot_defaults'); -var layoutAttributes = require('./layout_attributes'); -var supplyGl3dAxisLayoutDefaults = require('./axis_defaults'); - +var handleSubplotDefaults = require("../../subplot_defaults"); +var layoutAttributes = require("./layout_attributes"); +var supplyGl3dAxisLayoutDefaults = require("./axis_defaults"); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - var hasNon3D = ( - layoutOut._has('cartesian') || - layoutOut._has('geo') || - layoutOut._has('gl2d') || - layoutOut._has('pie') || - layoutOut._has('ternary') - ); - - // some layout-wide attribute are used in all scenes - // if 3D is the only visible plot type - function getDfltFromLayout(attr) { - if(hasNon3D) return; - - var isValid = layoutAttributes[attr].values.indexOf(layoutIn[attr]) !== -1; - if(isValid) return layoutIn[attr]; - } - - handleSubplotDefaults(layoutIn, layoutOut, fullData, { - type: 'gl3d', - attributes: layoutAttributes, - handleDefaults: handleGl3dDefaults, - font: layoutOut.font, - fullData: fullData, - getDfltFromLayout: getDfltFromLayout, - paper_bgcolor: layoutOut.paper_bgcolor, - calendar: layoutOut.calendar - }); + var hasNon3D = layoutOut._has("cartesian") || + layoutOut._has("geo") || + layoutOut._has("gl2d") || + layoutOut._has("pie") || + layoutOut._has("ternary"); + + // some layout-wide attribute are used in all scenes + // if 3D is the only visible plot type + function getDfltFromLayout(attr) { + if (hasNon3D) return; + + var isValid = layoutAttributes[attr].values.indexOf(layoutIn[attr]) !== -1; + if (isValid) return layoutIn[attr]; + } + + handleSubplotDefaults(layoutIn, layoutOut, fullData, { + type: "gl3d", + attributes: layoutAttributes, + handleDefaults: handleGl3dDefaults, + font: layoutOut.font, + fullData: fullData, + getDfltFromLayout: getDfltFromLayout, + paper_bgcolor: layoutOut.paper_bgcolor, + calendar: layoutOut.calendar + }); }; function handleGl3dDefaults(sceneLayoutIn, sceneLayoutOut, coerce, opts) { - /* + /* * Scene numbering proceeds as follows * scene * scene2 @@ -58,50 +52,53 @@ function handleGl3dDefaults(sceneLayoutIn, sceneLayoutOut, coerce, opts) { * Also write back a blank scene object to user layout so that some * attributes like aspectratio can be written back dynamically. */ + var bgcolor = coerce("bgcolor"), + bgColorCombined = Color.combine(bgcolor, opts.paper_bgcolor); - var bgcolor = coerce('bgcolor'), - bgColorCombined = Color.combine(bgcolor, opts.paper_bgcolor); - - var cameraKeys = Object.keys(layoutAttributes.camera); + var cameraKeys = Object.keys(layoutAttributes.camera); - for(var j = 0; j < cameraKeys.length; j++) { - coerce('camera.' + cameraKeys[j] + '.x'); - coerce('camera.' + cameraKeys[j] + '.y'); - coerce('camera.' + cameraKeys[j] + '.z'); - } + for (var j = 0; j < cameraKeys.length; j++) { + coerce("camera." + cameraKeys[j] + ".x"); + coerce("camera." + cameraKeys[j] + ".y"); + coerce("camera." + cameraKeys[j] + ".z"); + } - /* + /* * coerce to positive number (min 0) but also do not accept 0 (>0 not >=0) * note that 0's go false with the !! call */ - var hasAspect = !!coerce('aspectratio.x') && - !!coerce('aspectratio.y') && - !!coerce('aspectratio.z'); + var hasAspect = !!coerce("aspectratio.x") && + !!coerce("aspectratio.y") && + !!coerce("aspectratio.z"); - var defaultAspectMode = hasAspect ? 'manual' : 'auto'; - var aspectMode = coerce('aspectmode', defaultAspectMode); + var defaultAspectMode = hasAspect ? "manual" : "auto"; + var aspectMode = coerce("aspectmode", defaultAspectMode); - /* + /* * We need aspectratio object in all the Layouts as it is dynamically set * in the calculation steps, ie, we cant set the correct data now, it happens later. * We must also account for the case the user sends bad ratio data with 'manual' set * for the mode. In this case we must force change it here as the default coerce * misses it above. */ - if(!hasAspect) { - sceneLayoutIn.aspectratio = sceneLayoutOut.aspectratio = {x: 1, y: 1, z: 1}; - - if(aspectMode === 'manual') sceneLayoutOut.aspectmode = 'auto'; - } - - supplyGl3dAxisLayoutDefaults(sceneLayoutIn, sceneLayoutOut, { - font: opts.font, - scene: opts.id, - data: opts.fullData, - bgColor: bgColorCombined, - calendar: opts.calendar - }); - - coerce('dragmode', opts.getDfltFromLayout('dragmode')); - coerce('hovermode', opts.getDfltFromLayout('hovermode')); + if (!hasAspect) { + sceneLayoutIn.aspectratio = sceneLayoutOut.aspectratio = { + x: 1, + y: 1, + z: 1 + }; + + if (aspectMode === "manual") sceneLayoutOut.aspectmode = "auto"; + } + + supplyGl3dAxisLayoutDefaults(sceneLayoutIn, sceneLayoutOut, { + font: opts.font, + scene: opts.id, + data: opts.fullData, + bgColor: bgColorCombined, + calendar: opts.calendar + }); + + coerce("dragmode", opts.getDfltFromLayout("dragmode")); + coerce("hovermode", opts.getDfltFromLayout("hovermode")); } diff --git a/src/plots/gl3d/layout/layout_attributes.js b/src/plots/gl3d/layout/layout_attributes.js index a708ea7f1e7..bd1338bd665 100644 --- a/src/plots/gl3d/layout/layout_attributes.js +++ b/src/plots/gl3d/layout/layout_attributes.js @@ -5,164 +5,126 @@ * 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 gl3dAxisAttrs = require('./axis_attributes'); -var extendFlat = require('../../../lib/extend').extendFlat; +"use strict"; +var gl3dAxisAttrs = require("./axis_attributes"); +var extendFlat = require("../../../lib/extend").extendFlat; function makeVector(x, y, z) { - return { - x: { - valType: 'number', - role: 'info', - dflt: x - }, - y: { - valType: 'number', - role: 'info', - dflt: y - }, - z: { - valType: 'number', - role: 'info', - dflt: z - } - }; + return { + x: { valType: "number", role: "info", dflt: x }, + y: { valType: "number", role: "info", dflt: y }, + z: { valType: "number", role: "info", dflt: z } + }; } module.exports = { - bgcolor: { - valType: 'color', - role: 'style', - dflt: 'rgba(0,0,0,0)' - }, - camera: { - up: extendFlat(makeVector(0, 0, 1), { - description: [ - 'Sets the (x,y,z) components of the \'up\' camera vector.', - 'This vector determines the up direction of this scene', - 'with respect to the page.', - 'The default is *{x: 0, y: 0, z: 1}* which means that', - 'the z axis points up.' - ].join(' ') - }), - center: extendFlat(makeVector(0, 0, 0), { - description: [ - 'Sets the (x,y,z) components of the \'center\' camera vector', - 'This vector determines the translation (x,y,z) space', - 'about the center of this scene.', - 'By default, there is no such translation.' - ].join(' ') - }), - eye: extendFlat(makeVector(1.25, 1.25, 1.25), { - description: [ - 'Sets the (x,y,z) components of the \'eye\' camera vector.', - 'This vector determines the view point about the origin', - 'of this scene.' - ].join(' ') - }) - }, - domain: { - x: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the horizontal domain of this scene', - '(in plot fraction).' - ].join(' ') - }, - y: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the vertical domain of this scene', - '(in plot fraction).' - ].join(' ') - } - }, - aspectmode: { - valType: 'enumerated', - role: 'info', - values: ['auto', 'cube', 'data', 'manual'], - dflt: 'auto', - description: [ - 'If *cube*, this scene\'s axes are drawn as a cube,', - 'regardless of the axes\' ranges.', - - 'If *data*, this scene\'s axes are drawn', - 'in proportion with the axes\' ranges.', - - 'If *manual*, this scene\'s axes are drawn', - 'in proportion with the input of *aspectratio*', - '(the default behavior if *aspectratio* is provided).', - - 'If *auto*, this scene\'s axes are drawn', - 'using the results of *data* except when one axis', - 'is more than four times the size of the two others,', - 'where in that case the results of *cube* are used.' - ].join(' ') - }, - aspectratio: { // must be positive (0's are coerced to 1) - x: { - valType: 'number', - role: 'info', - min: 0 - }, - y: { - valType: 'number', - role: 'info', - min: 0 - }, - z: { - valType: 'number', - role: 'info', - min: 0 - }, - description: [ - 'Sets this scene\'s axis aspectratio.' - ].join(' ') - }, - - xaxis: gl3dAxisAttrs, - yaxis: gl3dAxisAttrs, - zaxis: gl3dAxisAttrs, - - dragmode: { - valType: 'enumerated', - role: 'info', - values: ['orbit', 'turntable', 'zoom', 'pan'], - dflt: 'turntable', - description: [ - 'Determines the mode of drag interactions for this scene.' - ].join(' ') - }, - hovermode: { - valType: 'enumerated', - role: 'info', - values: ['closest', false], - dflt: 'closest', - description: [ - 'Determines the mode of hover interactions for this scene.' - ].join(' ') + bgcolor: { valType: "color", role: "style", dflt: "rgba(0,0,0,0)" }, + camera: { + up: extendFlat(makeVector(0, 0, 1), { + description: [ + "Sets the (x,y,z) components of the 'up' camera vector.", + "This vector determines the up direction of this scene", + "with respect to the page.", + "The default is *{x: 0, y: 0, z: 1}* which means that", + "the z axis points up." + ].join(" ") + }), + center: extendFlat(makeVector(0, 0, 0), { + description: [ + "Sets the (x,y,z) components of the 'center' camera vector", + "This vector determines the translation (x,y,z) space", + "about the center of this scene.", + "By default, there is no such translation." + ].join(" ") + }), + eye: extendFlat(makeVector(1.25, 1.25, 1.25), { + description: [ + "Sets the (x,y,z) components of the 'eye' camera vector.", + "This vector determines the view point about the origin", + "of this scene." + ].join(" ") + }) + }, + domain: { + x: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: [ + "Sets the horizontal domain of this scene", + "(in plot fraction)." + ].join(" ") }, - - _deprecated: { - cameraposition: { - valType: 'info_array', - role: 'info', - description: 'Obsolete. Use `camera` instead.' - } + y: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: [ + "Sets the vertical domain of this scene", + "(in plot fraction)." + ].join(" ") + } + }, + aspectmode: { + valType: "enumerated", + role: "info", + values: ["auto", "cube", "data", "manual"], + dflt: "auto", + description: [ + "If *cube*, this scene's axes are drawn as a cube,", + "regardless of the axes' ranges.", + "If *data*, this scene's axes are drawn", + "in proportion with the axes' ranges.", + "If *manual*, this scene's axes are drawn", + "in proportion with the input of *aspectratio*", + "(the default behavior if *aspectratio* is provided).", + "If *auto*, this scene's axes are drawn", + "using the results of *data* except when one axis", + "is more than four times the size of the two others,", + "where in that case the results of *cube* are used." + ].join(" ") + }, + aspectratio: { + // must be positive (0's are coerced to 1) + x: { valType: "number", role: "info", min: 0 }, + y: { valType: "number", role: "info", min: 0 }, + z: { valType: "number", role: "info", min: 0 }, + description: ["Sets this scene's axis aspectratio."].join(" ") + }, + xaxis: gl3dAxisAttrs, + yaxis: gl3dAxisAttrs, + zaxis: gl3dAxisAttrs, + dragmode: { + valType: "enumerated", + role: "info", + values: ["orbit", "turntable", "zoom", "pan"], + dflt: "turntable", + description: [ + "Determines the mode of drag interactions for this scene." + ].join(" ") + }, + hovermode: { + valType: "enumerated", + role: "info", + values: ["closest", false], + dflt: "closest", + description: [ + "Determines the mode of hover interactions for this scene." + ].join(" ") + }, + _deprecated: { + cameraposition: { + valType: "info_array", + role: "info", + description: "Obsolete. Use `camera` instead." } + } }; diff --git a/src/plots/gl3d/layout/spikes.js b/src/plots/gl3d/layout/spikes.js index b835642f7cb..cd6eca4d473 100644 --- a/src/plots/gl3d/layout/spikes.js +++ b/src/plots/gl3d/layout/spikes.js @@ -5,40 +5,35 @@ * 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 str2RGBArray = require("../../../lib/str2rgbarray"); - -'use strict'; - -var str2RGBArray = require('../../../lib/str2rgbarray'); - -var AXES_NAMES = ['xaxis', 'yaxis', 'zaxis']; +var AXES_NAMES = ["xaxis", "yaxis", "zaxis"]; function SpikeOptions() { - this.enabled = [true, true, true]; - this.colors = [[0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1]]; - this.drawSides = [true, true, true]; - this.lineWidth = [1, 1, 1]; + this.enabled = [true, true, true]; + this.colors = [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]]; + this.drawSides = [true, true, true]; + this.lineWidth = [1, 1, 1]; } var proto = SpikeOptions.prototype; proto.merge = function(sceneLayout) { - for(var i = 0; i < 3; ++i) { - var axes = sceneLayout[AXES_NAMES[i]]; - - this.enabled[i] = axes.showspikes; - this.colors[i] = str2RGBArray(axes.spikecolor); - this.drawSides[i] = axes.spikesides; - this.lineWidth[i] = axes.spikethickness; - } + for (var i = 0; i < 3; ++i) { + var axes = sceneLayout[AXES_NAMES[i]]; + + this.enabled[i] = axes.showspikes; + this.colors[i] = str2RGBArray(axes.spikecolor); + this.drawSides[i] = axes.spikesides; + this.lineWidth[i] = axes.spikethickness; + } }; function createSpikeOptions(layout) { - var result = new SpikeOptions(); - result.merge(layout); - return result; + var result = new SpikeOptions(); + result.merge(layout); + return result; } module.exports = createSpikeOptions; diff --git a/src/plots/gl3d/layout/tick_marks.js b/src/plots/gl3d/layout/tick_marks.js index af98e6d4e45..67eb8f44742 100644 --- a/src/plots/gl3d/layout/tick_marks.js +++ b/src/plots/gl3d/layout/tick_marks.js @@ -5,90 +5,88 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -/* eslint block-scoped-var: 0*/ -/* eslint no-redeclare: 0*/ - -'use strict'; - +/* eslint block-scoped-var: 0 */ +/* eslint no-redeclare: 0 */ +"use strict"; module.exports = computeTickMarks; -var Axes = require('../../cartesian/axes'); -var Lib = require('../../../lib'); -var convertHTMLToUnicode = require('../../../lib/html2unicode'); +var Axes = require("../../cartesian/axes"); +var Lib = require("../../../lib"); +var convertHTMLToUnicode = require("../../../lib/html2unicode"); -var AXES_NAMES = ['xaxis', 'yaxis', 'zaxis']; +var AXES_NAMES = ["xaxis", "yaxis", "zaxis"]; var centerPoint = [0, 0, 0]; function contourLevelsFromTicks(ticks) { - var result = new Array(3); - for(var i = 0; i < 3; ++i) { - var tlevel = ticks[i]; - var clevel = new Array(tlevel.length); - for(var j = 0; j < tlevel.length; ++j) { - clevel[j] = tlevel[j].x; - } - result[i] = clevel; + var result = new Array(3); + for (var i = 0; i < 3; ++i) { + var tlevel = ticks[i]; + var clevel = new Array(tlevel.length); + for (var j = 0; j < tlevel.length; ++j) { + clevel[j] = tlevel[j].x; } - return result; + result[i] = clevel; + } + return result; } function computeTickMarks(scene) { - var axesOptions = scene.axesOptions; - var glRange = scene.glplot.axesPixels; - var sceneLayout = scene.fullSceneLayout; - - var ticks = [[], [], []]; - - for(var i = 0; i < 3; ++i) { - var axes = sceneLayout[AXES_NAMES[i]]; - - axes._length = (glRange[i].hi - glRange[i].lo) * - glRange[i].pixelsPerDataUnit / scene.dataScale[i]; - - if(Math.abs(axes._length) === Infinity) { - ticks[i] = []; - } else { - axes.range[0] = (glRange[i].lo) / scene.dataScale[i]; - axes.range[1] = (glRange[i].hi) / scene.dataScale[i]; - axes._m = 1.0 / (scene.dataScale[i] * glRange[i].pixelsPerDataUnit); - - if(axes.range[0] === axes.range[1]) { - axes.range[0] -= 1; - axes.range[1] += 1; - } - // this is necessary to short-circuit the 'y' handling - // in autotick part of calcTicks... Treating all axes as 'y' in this case - // running the autoticks here, then setting - // autoticks to false to get around the 2D handling in calcTicks. - var tickModeCached = axes.tickmode; - if(axes.tickmode === 'auto') { - axes.tickmode = 'linear'; - var nticks = axes.nticks || Lib.constrain((axes._length / 40), 4, 9); - Axes.autoTicks(axes, Math.abs(axes.range[1] - axes.range[0]) / nticks); - } - var dataTicks = Axes.calcTicks(axes); - for(var j = 0; j < dataTicks.length; ++j) { - dataTicks[j].x = dataTicks[j].x * scene.dataScale[i]; - dataTicks[j].text = convertHTMLToUnicode(dataTicks[j].text); - } - ticks[i] = dataTicks; - - - axes.tickmode = tickModeCached; - } + var axesOptions = scene.axesOptions; + var glRange = scene.glplot.axesPixels; + var sceneLayout = scene.fullSceneLayout; + + var ticks = [[], [], []]; + + for (var i = 0; i < 3; ++i) { + var axes = sceneLayout[AXES_NAMES[i]]; + + axes._length = (glRange[i].hi - glRange[i].lo) * + glRange[i].pixelsPerDataUnit / + scene.dataScale[i]; + + if (Math.abs(axes._length) === Infinity) { + ticks[i] = []; + } else { + axes.range[0] = glRange[i].lo / scene.dataScale[i]; + axes.range[1] = glRange[i].hi / scene.dataScale[i]; + axes._m = 1.0 / (scene.dataScale[i] * glRange[i].pixelsPerDataUnit); + + if (axes.range[0] === axes.range[1]) { + axes.range[0] -= 1; + axes.range[1] += 1; + } + // this is necessary to short-circuit the 'y' handling + // in autotick part of calcTicks... Treating all axes as 'y' in this case + // running the autoticks here, then setting + // autoticks to false to get around the 2D handling in calcTicks. + var tickModeCached = axes.tickmode; + if (axes.tickmode === "auto") { + axes.tickmode = "linear"; + var nticks = axes.nticks || Lib.constrain(axes._length / 40, 4, 9); + Axes.autoTicks(axes, Math.abs(axes.range[1] - axes.range[0]) / nticks); + } + var dataTicks = Axes.calcTicks(axes); + for (var j = 0; j < dataTicks.length; ++j) { + dataTicks[j].x = dataTicks[j].x * scene.dataScale[i]; + dataTicks[j].text = convertHTMLToUnicode(dataTicks[j].text); + } + ticks[i] = dataTicks; + + axes.tickmode = tickModeCached; } + } - axesOptions.ticks = ticks; + axesOptions.ticks = ticks; - // Calculate tick lengths dynamically - for(var i = 0; i < 3; ++i) { - centerPoint[i] = 0.5 * (scene.glplot.bounds[0][i] + scene.glplot.bounds[1][i]); - for(var j = 0; j < 2; ++j) { - axesOptions.bounds[j][i] = scene.glplot.bounds[j][i]; - } + // Calculate tick lengths dynamically + for (var i = 0; i < 3; ++i) { + centerPoint[i] = 0.5 * + (scene.glplot.bounds[0][i] + scene.glplot.bounds[1][i]); + for (var j = 0; j < 2; ++j) { + axesOptions.bounds[j][i] = scene.glplot.bounds[j][i]; } + } - scene.contourLevels = contourLevelsFromTicks(ticks); + scene.contourLevels = contourLevelsFromTicks(ticks); } diff --git a/src/plots/gl3d/project.js b/src/plots/gl3d/project.js index 2fe6c437e14..657a3af51e0 100644 --- a/src/plots/gl3d/project.js +++ b/src/plots/gl3d/project.js @@ -5,28 +5,26 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; function xformMatrix(m, v) { - var out = [0, 0, 0, 0]; - var i, j; + var out = [0, 0, 0, 0]; + var i, j; - for(i = 0; i < 4; ++i) { - for(j = 0; j < 4; ++j) { - out[j] += m[4 * i + j] * v[i]; - } + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + out[j] += m[4 * i + j] * v[i]; } + } - return out; + return out; } function project(camera, v) { - var p = xformMatrix(camera.projection, - xformMatrix(camera.view, - xformMatrix(camera.model, [v[0], v[1], v[2], 1]))); - return p; + var p = xformMatrix( + camera.projection, + xformMatrix(camera.view, xformMatrix(camera.model, [v[0], v[1], v[2], 1])) + ); + return p; } module.exports = project; diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index f287a5c3aa9..20806d6641c 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -5,706 +5,706 @@ * 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 createPlot = require("gl-plot3d"); +var getContext = require("webgl-context"); +var Lib = require("../../lib"); -'use strict'; +var Axes = require("../../plots/cartesian/axes"); +var Fx = require("../../plots/cartesian/graph_interact"); -var createPlot = require('gl-plot3d'); -var getContext = require('webgl-context'); +var str2RGBAarray = require("../../lib/str2rgbarray"); +var showNoWebGlMsg = require("../../lib/show_no_webgl_msg"); -var Lib = require('../../lib'); - -var Axes = require('../../plots/cartesian/axes'); -var Fx = require('../../plots/cartesian/graph_interact'); - -var str2RGBAarray = require('../../lib/str2rgbarray'); -var showNoWebGlMsg = require('../../lib/show_no_webgl_msg'); - -var createCamera = require('./camera'); -var project = require('./project'); -var setConvert = require('./set_convert'); -var createAxesOptions = require('./layout/convert'); -var createSpikeOptions = require('./layout/spikes'); -var computeTickMarks = require('./layout/tick_marks'); +var createCamera = require("./camera"); +var project = require("./project"); +var setConvert = require("./set_convert"); +var createAxesOptions = require("./layout/convert"); +var createSpikeOptions = require("./layout/spikes"); +var computeTickMarks = require("./layout/tick_marks"); var STATIC_CANVAS, STATIC_CONTEXT; function render(scene) { - - var trace; - - // update size of svg container - var svgContainer = scene.svgContainer; - var clientRect = scene.container.getBoundingClientRect(); - var width = clientRect.width, height = clientRect.height; - svgContainer.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height); - svgContainer.setAttributeNS(null, 'width', width); - svgContainer.setAttributeNS(null, 'height', height); - - computeTickMarks(scene); - scene.glplot.axes.update(scene.axesOptions); - - // check if pick has changed - var keys = Object.keys(scene.traces); - var lastPicked = null; - var selection = scene.glplot.selection; - for(var i = 0; i < keys.length; ++i) { - trace = scene.traces[keys[i]]; - if(trace.data.hoverinfo !== 'skip' && trace.handlePick(selection)) { - lastPicked = trace; - } - - if(trace.setContourLevels) trace.setContourLevels(); + var trace; + + // update size of svg container + var svgContainer = scene.svgContainer; + var clientRect = scene.container.getBoundingClientRect(); + var width = clientRect.width, height = clientRect.height; + svgContainer.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height); + svgContainer.setAttributeNS(null, "width", width); + svgContainer.setAttributeNS(null, "height", height); + + computeTickMarks(scene); + scene.glplot.axes.update(scene.axesOptions); + + // check if pick has changed + var keys = Object.keys(scene.traces); + var lastPicked = null; + var selection = scene.glplot.selection; + for (var i = 0; i < keys.length; ++i) { + trace = scene.traces[keys[i]]; + if (trace.data.hoverinfo !== "skip" && trace.handlePick(selection)) { + lastPicked = trace; } - function formatter(axisName, val) { - if(typeof val === 'string') return val; + if (trace.setContourLevels) trace.setContourLevels(); + } - var axis = scene.fullSceneLayout[axisName]; - return Axes.tickText(axis, axis.c2l(val), 'hover').text; - } + function formatter(axisName, val) { + if (typeof val === "string") return val; - var oldEventData; + var axis = scene.fullSceneLayout[axisName]; + return Axes.tickText(axis, axis.c2l(val), "hover").text; + } - if(lastPicked !== null) { - var pdata = project(scene.glplot.cameraParams, selection.dataCoordinate); - trace = lastPicked.data; - var hoverinfo = trace.hoverinfo; + var oldEventData; - var xVal = formatter('xaxis', selection.traceCoordinate[0]), - yVal = formatter('yaxis', selection.traceCoordinate[1]), - zVal = formatter('zaxis', selection.traceCoordinate[2]); + if (lastPicked !== null) { + var pdata = project(scene.glplot.cameraParams, selection.dataCoordinate); + trace = lastPicked.data; + var hoverinfo = trace.hoverinfo; - if(hoverinfo !== 'all') { - var hoverinfoParts = hoverinfo.split('+'); - if(hoverinfoParts.indexOf('x') === -1) xVal = undefined; - if(hoverinfoParts.indexOf('y') === -1) yVal = undefined; - if(hoverinfoParts.indexOf('z') === -1) zVal = undefined; - if(hoverinfoParts.indexOf('text') === -1) selection.textLabel = undefined; - if(hoverinfoParts.indexOf('name') === -1) lastPicked.name = undefined; - } + var xVal = formatter("xaxis", selection.traceCoordinate[0]), + yVal = formatter("yaxis", selection.traceCoordinate[1]), + zVal = formatter("zaxis", selection.traceCoordinate[2]); - if(scene.fullSceneLayout.hovermode) { - Fx.loneHover({ - x: (0.5 + 0.5 * pdata[0] / pdata[3]) * width, - y: (0.5 - 0.5 * pdata[1] / pdata[3]) * height, - xLabel: xVal, - yLabel: yVal, - zLabel: zVal, - text: selection.textLabel, - name: lastPicked.name, - color: lastPicked.color - }, { - container: svgContainer - }); - } - - var eventData = { - points: [{ - x: xVal, - y: yVal, - z: zVal, - data: trace._input, - fullData: trace, - curveNumber: trace.index, - pointNumber: selection.data.index - }] - }; - - if(selection.buttons && selection.distance < 5) { - scene.graphDiv.emit('plotly_click', eventData); - } - else { - scene.graphDiv.emit('plotly_hover', eventData); - } - - oldEventData = eventData; + if (hoverinfo !== "all") { + var hoverinfoParts = hoverinfo.split("+"); + if (hoverinfoParts.indexOf("x") === -1) xVal = undefined; + if (hoverinfoParts.indexOf("y") === -1) yVal = undefined; + if (hoverinfoParts.indexOf("z") === -1) zVal = undefined; + if (hoverinfoParts.indexOf("text") === -1) { + selection.textLabel = undefined; + } + if (hoverinfoParts.indexOf("name") === -1) lastPicked.name = undefined; } - else { - Fx.loneUnhover(svgContainer); - scene.graphDiv.emit('plotly_unhover', oldEventData); + + if (scene.fullSceneLayout.hovermode) { + Fx.loneHover( + { + x: (0.5 + 0.5 * pdata[0] / pdata[3]) * width, + y: (0.5 - 0.5 * pdata[1] / pdata[3]) * height, + xLabel: xVal, + yLabel: yVal, + zLabel: zVal, + text: selection.textLabel, + name: lastPicked.name, + color: lastPicked.color + }, + { container: svgContainer } + ); } -} -function initializeGLPlot(scene, fullLayout, canvas, gl) { - var glplotOptions = { - canvas: canvas, - gl: gl, - container: scene.container, - axes: scene.axesOptions, - spikes: scene.spikeOptions, - pickRadius: 10, - snapToData: true, - autoScale: true, - autoBounds: false + var eventData = { + points: [ + { + x: xVal, + y: yVal, + z: zVal, + data: trace._input, + fullData: trace, + curveNumber: trace.index, + pointNumber: selection.data.index + } + ] }; - // for static plots, we reuse the WebGL context - // as WebKit doesn't collect them reliably - if(scene.staticMode) { - if(!STATIC_CONTEXT) { - STATIC_CANVAS = document.createElement('canvas'); - STATIC_CONTEXT = getContext({ - canvas: STATIC_CANVAS, - preserveDrawingBuffer: true, - premultipliedAlpha: true, - antialias: true - }); - if(!STATIC_CONTEXT) { - throw new Error('error creating static canvas/context for image server'); - } - } - glplotOptions.pixelRatio = scene.pixelRatio; - glplotOptions.gl = STATIC_CONTEXT; - glplotOptions.canvas = STATIC_CANVAS; + if (selection.buttons && selection.distance < 5) { + scene.graphDiv.emit("plotly_click", eventData); + } else { + scene.graphDiv.emit("plotly_hover", eventData); } - try { - scene.glplot = createPlot(glplotOptions); + oldEventData = eventData; + } else { + Fx.loneUnhover(svgContainer); + scene.graphDiv.emit("plotly_unhover", oldEventData); + } +} + +function initializeGLPlot(scene, fullLayout, canvas, gl) { + var glplotOptions = { + canvas: canvas, + gl: gl, + container: scene.container, + axes: scene.axesOptions, + spikes: scene.spikeOptions, + pickRadius: 10, + snapToData: true, + autoScale: true, + autoBounds: false + }; + + // for static plots, we reuse the WebGL context + // as WebKit doesn't collect them reliably + if (scene.staticMode) { + if (!STATIC_CONTEXT) { + STATIC_CANVAS = document.createElement("canvas"); + STATIC_CONTEXT = getContext({ + canvas: STATIC_CANVAS, + preserveDrawingBuffer: true, + premultipliedAlpha: true, + antialias: true + }); + if (!STATIC_CONTEXT) { + throw new Error( + "error creating static canvas/context for image server" + ); + } } - catch(e) { - /* + glplotOptions.pixelRatio = scene.pixelRatio; + glplotOptions.gl = STATIC_CONTEXT; + glplotOptions.canvas = STATIC_CANVAS; + } + + try { + scene.glplot = createPlot(glplotOptions); + } catch (e) { + /* * createPlot will throw when webgl is not enabled in the client. * Lets return an instance of the module with all functions noop'd. * The destroy method - which will remove the container from the DOM * is overridden with a function that removes the container only. */ - showNoWebGlMsg(scene); - } - - var relayoutCallback = function(scene) { - var update = {}; - update[scene.id] = getLayoutCamera(scene.camera); - scene.saveCamera(scene.graphDiv.layout); - scene.graphDiv.emit('plotly_relayout', update); - }; - - scene.glplot.canvas.addEventListener('mouseup', relayoutCallback.bind(null, scene)); - scene.glplot.canvas.addEventListener('wheel', relayoutCallback.bind(null, scene)); - - if(!scene.staticMode) { - scene.glplot.canvas.addEventListener('webglcontextlost', function(ev) { - Lib.warn('Lost WebGL context.'); - ev.preventDefault(); - }); - } - - if(!scene.camera) { - var cameraData = scene.fullSceneLayout.camera; - scene.camera = createCamera(scene.container, { - center: [cameraData.center.x, cameraData.center.y, cameraData.center.z], - eye: [cameraData.eye.x, cameraData.eye.y, cameraData.eye.z], - up: [cameraData.up.x, cameraData.up.y, cameraData.up.z], - zoomMin: 0.1, - zoomMax: 100, - mode: 'orbit' - }); - } - - scene.glplot.camera = scene.camera; - - scene.glplot.oncontextloss = function() { - scene.recoverContext(); - }; - - scene.glplot.onrender = render.bind(null, scene); - - // List of scene objects - scene.traces = {}; - - return true; + showNoWebGlMsg(scene); + } + + var relayoutCallback = function(scene) { + var update = {}; + update[scene.id] = getLayoutCamera(scene.camera); + scene.saveCamera(scene.graphDiv.layout); + scene.graphDiv.emit("plotly_relayout", update); + }; + + scene.glplot.canvas.addEventListener( + "mouseup", + relayoutCallback.bind(null, scene) + ); + scene.glplot.canvas.addEventListener( + "wheel", + relayoutCallback.bind(null, scene) + ); + + if (!scene.staticMode) { + scene.glplot.canvas.addEventListener("webglcontextlost", function(ev) { + Lib.warn("Lost WebGL context."); + ev.preventDefault(); + }); + } + + if (!scene.camera) { + var cameraData = scene.fullSceneLayout.camera; + scene.camera = createCamera(scene.container, { + center: [cameraData.center.x, cameraData.center.y, cameraData.center.z], + eye: [cameraData.eye.x, cameraData.eye.y, cameraData.eye.z], + up: [cameraData.up.x, cameraData.up.y, cameraData.up.z], + zoomMin: 0.1, + zoomMax: 100, + mode: "orbit" + }); + } + + scene.glplot.camera = scene.camera; + + scene.glplot.oncontextloss = function() { + scene.recoverContext(); + }; + + scene.glplot.onrender = render.bind(null, scene); + + // List of scene objects + scene.traces = {}; + + return true; } function Scene(options, fullLayout) { - - // create sub container for plot - var sceneContainer = document.createElement('div'); - var plotContainer = options.container; - - // keep a ref to the graph div to fire hover+click events - this.graphDiv = options.graphDiv; - - // create SVG container for hover text - var svgContainer = document.createElementNS( - 'http://www.w3.org/2000/svg', - 'svg'); - svgContainer.style.position = 'absolute'; - svgContainer.style.top = svgContainer.style.left = '0px'; - svgContainer.style.width = svgContainer.style.height = '100%'; - svgContainer.style['z-index'] = 20; - svgContainer.style['pointer-events'] = 'none'; - sceneContainer.appendChild(svgContainer); - this.svgContainer = svgContainer; - - // Tag the container with the sceneID - sceneContainer.id = options.id; - sceneContainer.style.position = 'absolute'; - sceneContainer.style.top = sceneContainer.style.left = '0px'; - sceneContainer.style.width = sceneContainer.style.height = '100%'; - plotContainer.appendChild(sceneContainer); - - this.fullLayout = fullLayout; - this.id = options.id || 'scene'; - this.fullSceneLayout = fullLayout[this.id]; - - // Saved from last call to plot() - this.plotArgs = [ [], {}, {} ]; - - /* + // create sub container for plot + var sceneContainer = document.createElement("div"); + var plotContainer = options.container; + + // keep a ref to the graph div to fire hover+click events + this.graphDiv = options.graphDiv; + + // create SVG container for hover text + var svgContainer = document.createElementNS( + "http://www.w3.org/2000/svg", + "svg" + ); + svgContainer.style.position = "absolute"; + svgContainer.style.top = svgContainer.style.left = "0px"; + svgContainer.style.width = svgContainer.style.height = "100%"; + svgContainer.style["z-index"] = 20; + svgContainer.style["pointer-events"] = "none"; + sceneContainer.appendChild(svgContainer); + this.svgContainer = svgContainer; + + // Tag the container with the sceneID + sceneContainer.id = options.id; + sceneContainer.style.position = "absolute"; + sceneContainer.style.top = sceneContainer.style.left = "0px"; + sceneContainer.style.width = sceneContainer.style.height = "100%"; + plotContainer.appendChild(sceneContainer); + + this.fullLayout = fullLayout; + this.id = options.id || "scene"; + this.fullSceneLayout = fullLayout[this.id]; + + // Saved from last call to plot() + this.plotArgs = [[], {}, {}]; + + /* * Move this to calc step? Why does it work here? */ - this.axesOptions = createAxesOptions(fullLayout[this.id]); - this.spikeOptions = createSpikeOptions(fullLayout[this.id]); - this.container = sceneContainer; - this.staticMode = !!options.staticPlot; - this.pixelRatio = options.plotGlPixelRatio || 2; + this.axesOptions = createAxesOptions(fullLayout[this.id]); + this.spikeOptions = createSpikeOptions(fullLayout[this.id]); + this.container = sceneContainer; + this.staticMode = !!options.staticPlot; + this.pixelRatio = options.plotGlPixelRatio || 2; - // Coordinate rescaling - this.dataScale = [1, 1, 1]; + // Coordinate rescaling + this.dataScale = [1, 1, 1]; - this.contourLevels = [ [], [], [] ]; + this.contourLevels = [[], [], []]; - if(!initializeGLPlot(this, fullLayout)) return; // todo check the necessity for this line + if (!initializeGLPlot(this, fullLayout)) { + return; + } // todo check the necessity for this line } var proto = Scene.prototype; proto.recoverContext = function() { - var scene = this; - var gl = this.glplot.gl; - var canvas = this.glplot.canvas; - this.glplot.dispose(); - - function tryRecover() { - if(gl.isContextLost()) { - requestAnimationFrame(tryRecover); - return; - } - if(!initializeGLPlot(scene, scene.fullLayout, canvas, gl)) { - Lib.error('Catastrophic and unrecoverable WebGL error. Context lost.'); - return; - } - scene.plot.apply(scene, scene.plotArgs); + var scene = this; + var gl = this.glplot.gl; + var canvas = this.glplot.canvas; + this.glplot.dispose(); + + function tryRecover() { + if (gl.isContextLost()) { + requestAnimationFrame(tryRecover); + return; + } + if (!initializeGLPlot(scene, scene.fullLayout, canvas, gl)) { + Lib.error("Catastrophic and unrecoverable WebGL error. Context lost."); + return; } - requestAnimationFrame(tryRecover); + scene.plot.apply(scene, scene.plotArgs); + } + requestAnimationFrame(tryRecover); }; -var axisProperties = [ 'xaxis', 'yaxis', 'zaxis' ]; +var axisProperties = ["xaxis", "yaxis", "zaxis"]; function coordinateBound(axis, coord, d, bounds, calendar) { - var x; - for(var i = 0; i < coord.length; ++i) { - if(Array.isArray(coord[i])) { - for(var j = 0; j < coord[i].length; ++j) { - x = axis.d2l(coord[i][j], 0, calendar); - if(!isNaN(x) && isFinite(x)) { - bounds[0][d] = Math.min(bounds[0][d], x); - bounds[1][d] = Math.max(bounds[1][d], x); - } - } - } - else { - x = axis.d2l(coord[i], 0, calendar); - if(!isNaN(x) && isFinite(x)) { - bounds[0][d] = Math.min(bounds[0][d], x); - bounds[1][d] = Math.max(bounds[1][d], x); - } + var x; + for (var i = 0; i < coord.length; ++i) { + if (Array.isArray(coord[i])) { + for (var j = 0; j < coord[i].length; ++j) { + x = axis.d2l(coord[i][j], 0, calendar); + if (!isNaN(x) && isFinite(x)) { + bounds[0][d] = Math.min(bounds[0][d], x); + bounds[1][d] = Math.max(bounds[1][d], x); } + } + } else { + x = axis.d2l(coord[i], 0, calendar); + if (!isNaN(x) && isFinite(x)) { + bounds[0][d] = Math.min(bounds[0][d], x); + bounds[1][d] = Math.max(bounds[1][d], x); + } } + } } function computeTraceBounds(scene, trace, bounds) { - var sceneLayout = scene.fullSceneLayout; - coordinateBound(sceneLayout.xaxis, trace.x, 0, bounds, trace.xcalendar); - coordinateBound(sceneLayout.yaxis, trace.y, 1, bounds, trace.ycalendar); - coordinateBound(sceneLayout.zaxis, trace.z, 2, bounds, trace.zcalendar); + var sceneLayout = scene.fullSceneLayout; + coordinateBound(sceneLayout.xaxis, trace.x, 0, bounds, trace.xcalendar); + coordinateBound(sceneLayout.yaxis, trace.y, 1, bounds, trace.ycalendar); + coordinateBound(sceneLayout.zaxis, trace.z, 2, bounds, trace.zcalendar); } proto.plot = function(sceneData, fullLayout, layout) { - - // Save parameters - this.plotArgs = [sceneData, fullLayout, layout]; - - if(this.glplot.contextLost) return; - - var data, trace; - var i, j, axis, axisType; - var fullSceneLayout = fullLayout[this.id]; - var sceneLayout = layout[this.id]; - - if(fullSceneLayout.bgcolor) this.glplot.clearColor = str2RGBAarray(fullSceneLayout.bgcolor); - else this.glplot.clearColor = [0, 0, 0, 0]; - - this.glplot.snapToData = true; - - // Update layout - this.fullSceneLayout = fullSceneLayout; - - this.glplotLayout = fullSceneLayout; - this.axesOptions.merge(fullSceneLayout); - this.spikeOptions.merge(fullSceneLayout); - - // Update camera and camera mode - this.setCamera(fullSceneLayout.camera); - this.updateFx(fullSceneLayout.dragmode, fullSceneLayout.hovermode); - - // Update scene - this.glplot.update({}); - - // Update axes functions BEFORE updating traces - for(i = 0; i < 3; ++i) { - axis = fullSceneLayout[axisProperties[i]]; - setConvert(axis); + // Save parameters + this.plotArgs = [sceneData, fullLayout, layout]; + + if (this.glplot.contextLost) return; + + var data, trace; + var i, j, axis, axisType; + var fullSceneLayout = fullLayout[this.id]; + var sceneLayout = layout[this.id]; + + if (fullSceneLayout.bgcolor) { + this.glplot.clearColor = str2RGBAarray(fullSceneLayout.bgcolor); + } else { + this.glplot.clearColor = [0, 0, 0, 0]; + } + + this.glplot.snapToData = true; + + // Update layout + this.fullSceneLayout = fullSceneLayout; + + this.glplotLayout = fullSceneLayout; + this.axesOptions.merge(fullSceneLayout); + this.spikeOptions.merge(fullSceneLayout); + + // Update camera and camera mode + this.setCamera(fullSceneLayout.camera); + this.updateFx(fullSceneLayout.dragmode, fullSceneLayout.hovermode); + + // Update scene + this.glplot.update({}); + + // Update axes functions BEFORE updating traces + for (i = 0; i < 3; ++i) { + axis = fullSceneLayout[axisProperties[i]]; + setConvert(axis); + } + + // Convert scene data + if (!sceneData) sceneData = []; + else if (!Array.isArray(sceneData)) sceneData = [sceneData]; + + // Compute trace bounding box + var dataBounds = [ + [Infinity, Infinity, Infinity], + [-Infinity, -Infinity, -Infinity] + ]; + for (i = 0; i < sceneData.length; ++i) { + data = sceneData[i]; + if (data.visible !== true) continue; + + computeTraceBounds(this, data, dataBounds); + } + var dataScale = [1, 1, 1]; + for (j = 0; j < 3; ++j) { + if (dataBounds[0][j] > dataBounds[1][j]) { + dataScale[j] = 1.0; + } else { + if (dataBounds[1][j] === dataBounds[0][j]) { + dataScale[j] = 1.0; + } else { + dataScale[j] = 1.0 / (dataBounds[1][j] - dataBounds[0][j]); + } } + } - // Convert scene data - if(!sceneData) sceneData = []; - else if(!Array.isArray(sceneData)) sceneData = [sceneData]; - - // Compute trace bounding box - var dataBounds = [ - [Infinity, Infinity, Infinity], - [-Infinity, -Infinity, -Infinity] - ]; - for(i = 0; i < sceneData.length; ++i) { - data = sceneData[i]; - if(data.visible !== true) continue; + // Save scale + this.dataScale = dataScale; - computeTraceBounds(this, data, dataBounds); + // Update traces + for (i = 0; i < sceneData.length; ++i) { + data = sceneData[i]; + if (data.visible !== true) { + continue; } - var dataScale = [1, 1, 1]; - for(j = 0; j < 3; ++j) { - if(dataBounds[0][j] > dataBounds[1][j]) { - dataScale[j] = 1.0; - } - else { - if(dataBounds[1][j] === dataBounds[0][j]) { - dataScale[j] = 1.0; - } - else { - dataScale[j] = 1.0 / (dataBounds[1][j] - dataBounds[0][j]); - } - } + trace = this.traces[data.uid]; + if (trace) { + trace.update(data); + } else { + trace = data._module.plot(this, data); + this.traces[data.uid] = trace; } - - // Save scale - this.dataScale = dataScale; - - // Update traces - for(i = 0; i < sceneData.length; ++i) { - data = sceneData[i]; - if(data.visible !== true) { - continue; - } - trace = this.traces[data.uid]; - if(trace) { - trace.update(data); - } else { - trace = data._module.plot(this, data); - this.traces[data.uid] = trace; - } - trace.name = data.name; + trace.name = data.name; + } + + // Remove empty traces + var traceIds = Object.keys(this.traces); + + trace_id_loop: + for (i = 0; i < traceIds.length; ++i) { + for (j = 0; j < sceneData.length; ++j) { + if (sceneData[j].uid === traceIds[i] && sceneData[j].visible === true) { + continue trace_id_loop; + } } - - // Remove empty traces - var traceIds = Object.keys(this.traces); - - trace_id_loop: - for(i = 0; i < traceIds.length; ++i) { - for(j = 0; j < sceneData.length; ++j) { - if(sceneData[j].uid === traceIds[i] && sceneData[j].visible === true) { - continue trace_id_loop; - } - } - trace = this.traces[traceIds[i]]; - trace.dispose(); - delete this.traces[traceIds[i]]; + trace = this.traces[traceIds[i]]; + trace.dispose(); + delete this.traces[traceIds[i]]; + } + + // Update ranges (needs to be called *after* objects are added due to updates) + var sceneBounds = [[0, 0, 0], [0, 0, 0]], + axisDataRange = [], + axisTypeRatios = {}; + + for (i = 0; i < 3; ++i) { + axis = fullSceneLayout[axisProperties[i]]; + axisType = axis.type; + + if (axisType in axisTypeRatios) { + axisTypeRatios[axisType].acc *= dataScale[i]; + axisTypeRatios[axisType].count += 1; + } else { + axisTypeRatios[axisType] = { acc: dataScale[i], count: 1 }; } - // Update ranges (needs to be called *after* objects are added due to updates) - var sceneBounds = [[0, 0, 0], [0, 0, 0]], - axisDataRange = [], - axisTypeRatios = {}; - - for(i = 0; i < 3; ++i) { - axis = fullSceneLayout[axisProperties[i]]; - axisType = axis.type; - - if(axisType in axisTypeRatios) { - axisTypeRatios[axisType].acc *= dataScale[i]; - axisTypeRatios[axisType].count += 1; - } - else { - axisTypeRatios[axisType] = { - acc: dataScale[i], - count: 1 - }; - } - - if(axis.autorange) { - sceneBounds[0][i] = Infinity; - sceneBounds[1][i] = -Infinity; - for(j = 0; j < this.glplot.objects.length; ++j) { - var objBounds = this.glplot.objects[j].bounds; - sceneBounds[0][i] = Math.min(sceneBounds[0][i], - objBounds[0][i] / dataScale[i]); - sceneBounds[1][i] = Math.max(sceneBounds[1][i], - objBounds[1][i] / dataScale[i]); - } - if('rangemode' in axis && axis.rangemode === 'tozero') { - sceneBounds[0][i] = Math.min(sceneBounds[0][i], 0); - sceneBounds[1][i] = Math.max(sceneBounds[1][i], 0); - } - if(sceneBounds[0][i] > sceneBounds[1][i]) { - sceneBounds[0][i] = -1; - sceneBounds[1][i] = 1; - } else { - var d = sceneBounds[1][i] - sceneBounds[0][i]; - sceneBounds[0][i] -= d / 32.0; - sceneBounds[1][i] += d / 32.0; - } - } else { - var range = fullSceneLayout[axisProperties[i]].range; - sceneBounds[0][i] = range[0]; - sceneBounds[1][i] = range[1]; - } - if(sceneBounds[0][i] === sceneBounds[1][i]) { - sceneBounds[0][i] -= 1; - sceneBounds[1][i] += 1; - } - axisDataRange[i] = sceneBounds[1][i] - sceneBounds[0][i]; - - // Update plot bounds - this.glplot.bounds[0][i] = sceneBounds[0][i] * dataScale[i]; - this.glplot.bounds[1][i] = sceneBounds[1][i] * dataScale[i]; + if (axis.autorange) { + sceneBounds[0][i] = Infinity; + sceneBounds[1][i] = -Infinity; + for (j = 0; j < this.glplot.objects.length; ++j) { + var objBounds = this.glplot.objects[j].bounds; + sceneBounds[0][i] = Math.min( + sceneBounds[0][i], + objBounds[0][i] / dataScale[i] + ); + sceneBounds[1][i] = Math.max( + sceneBounds[1][i], + objBounds[1][i] / dataScale[i] + ); + } + if ("rangemode" in axis && axis.rangemode === "tozero") { + sceneBounds[0][i] = Math.min(sceneBounds[0][i], 0); + sceneBounds[1][i] = Math.max(sceneBounds[1][i], 0); + } + if (sceneBounds[0][i] > sceneBounds[1][i]) { + sceneBounds[0][i] = -1; + sceneBounds[1][i] = 1; + } else { + var d = sceneBounds[1][i] - sceneBounds[0][i]; + sceneBounds[0][i] -= d / 32.0; + sceneBounds[1][i] += d / 32.0; + } + } else { + var range = fullSceneLayout[axisProperties[i]].range; + sceneBounds[0][i] = range[0]; + sceneBounds[1][i] = range[1]; } - - var axesScaleRatio = [1, 1, 1]; - - // Compute axis scale per category - for(i = 0; i < 3; ++i) { - axis = fullSceneLayout[axisProperties[i]]; - axisType = axis.type; - var axisRatio = axisTypeRatios[axisType]; - axesScaleRatio[i] = Math.pow(axisRatio.acc, 1.0 / axisRatio.count) / dataScale[i]; + if (sceneBounds[0][i] === sceneBounds[1][i]) { + sceneBounds[0][i] -= 1; + sceneBounds[1][i] += 1; } + axisDataRange[i] = sceneBounds[1][i] - sceneBounds[0][i]; - /* - * Dynamically set the aspect ratio depending on the users aspect settings - */ - var axisAutoScaleFactor = 4; - var aspectRatio; + // Update plot bounds + this.glplot.bounds[0][i] = sceneBounds[0][i] * dataScale[i]; + this.glplot.bounds[1][i] = sceneBounds[1][i] * dataScale[i]; + } - if(fullSceneLayout.aspectmode === 'auto') { + var axesScaleRatio = [1, 1, 1]; - if(Math.max.apply(null, axesScaleRatio) / Math.min.apply(null, axesScaleRatio) <= axisAutoScaleFactor) { + // Compute axis scale per category + for (i = 0; i < 3; ++i) { + axis = fullSceneLayout[axisProperties[i]]; + axisType = axis.type; + var axisRatio = axisTypeRatios[axisType]; + axesScaleRatio[i] = Math.pow(axisRatio.acc, 1.0 / axisRatio.count) / + dataScale[i]; + } - /* + /* + * Dynamically set the aspect ratio depending on the users aspect settings + */ + var axisAutoScaleFactor = 4; + var aspectRatio; + + if (fullSceneLayout.aspectmode === "auto") { + if ( + Math.max.apply(null, axesScaleRatio) / + Math.min.apply(null, axesScaleRatio) <= + axisAutoScaleFactor + ) { + /* * USE DATA MODE WHEN AXIS RANGE DIMENSIONS ARE RELATIVELY EQUAL */ - - aspectRatio = axesScaleRatio; - } else { - - /* + aspectRatio = axesScaleRatio; + } else { + /* * USE EQUAL MODE WHEN AXIS RANGE DIMENSIONS ARE HIGHLY UNEQUAL */ - aspectRatio = [1, 1, 1]; - } - - } else if(fullSceneLayout.aspectmode === 'cube') { - aspectRatio = [1, 1, 1]; - - } else if(fullSceneLayout.aspectmode === 'data') { - aspectRatio = axesScaleRatio; - - } else if(fullSceneLayout.aspectmode === 'manual') { - var userRatio = fullSceneLayout.aspectratio; - aspectRatio = [userRatio.x, userRatio.y, userRatio.z]; - - } else { - throw new Error('scene.js aspectRatio was not one of the enumerated types'); + aspectRatio = [1, 1, 1]; } - - /* + } else if (fullSceneLayout.aspectmode === "cube") { + aspectRatio = [1, 1, 1]; + } else if (fullSceneLayout.aspectmode === "data") { + aspectRatio = axesScaleRatio; + } else if (fullSceneLayout.aspectmode === "manual") { + var userRatio = fullSceneLayout.aspectratio; + aspectRatio = [userRatio.x, userRatio.y, userRatio.z]; + } else { + throw new Error("scene.js aspectRatio was not one of the enumerated types"); + } + + /* * Write aspect Ratio back to user data and fullLayout so that it is modifies as user * manipulates the aspectmode settings and the fullLayout is up-to-date. */ - fullSceneLayout.aspectratio.x = sceneLayout.aspectratio.x = aspectRatio[0]; - fullSceneLayout.aspectratio.y = sceneLayout.aspectratio.y = aspectRatio[1]; - fullSceneLayout.aspectratio.z = sceneLayout.aspectratio.z = aspectRatio[2]; + fullSceneLayout.aspectratio.x = sceneLayout.aspectratio.x = aspectRatio[0]; + fullSceneLayout.aspectratio.y = sceneLayout.aspectratio.y = aspectRatio[1]; + fullSceneLayout.aspectratio.z = sceneLayout.aspectratio.z = aspectRatio[2]; - /* + /* * Finally assign the computed aspecratio to the glplot module. This will have an effect * on the next render cycle. */ - this.glplot.aspect = aspectRatio; - - - // Update frame position for multi plots - var domain = fullSceneLayout.domain || null, - size = fullLayout._size || null; - - if(domain && size) { - var containerStyle = this.container.style; - containerStyle.position = 'absolute'; - containerStyle.left = (size.l + domain.x[0] * size.w) + 'px'; - containerStyle.top = (size.t + (1 - domain.y[1]) * size.h) + 'px'; - containerStyle.width = (size.w * (domain.x[1] - domain.x[0])) + 'px'; - containerStyle.height = (size.h * (domain.y[1] - domain.y[0])) + 'px'; - } - - // force redraw so that promise is returned when rendering is completed - this.glplot.redraw(); + this.glplot.aspect = aspectRatio; + + // Update frame position for multi plots + var domain = fullSceneLayout.domain || null, size = fullLayout._size || null; + + if (domain && size) { + var containerStyle = this.container.style; + containerStyle.position = "absolute"; + containerStyle.left = size.l + domain.x[0] * size.w + "px"; + containerStyle.top = size.t + (1 - domain.y[1]) * size.h + "px"; + containerStyle.width = size.w * (domain.x[1] - domain.x[0]) + "px"; + containerStyle.height = size.h * (domain.y[1] - domain.y[0]) + "px"; + } + + // force redraw so that promise is returned when rendering is completed + this.glplot.redraw(); }; proto.destroy = function() { - this.glplot.dispose(); - this.container.parentNode.removeChild(this.container); + this.glplot.dispose(); + this.container.parentNode.removeChild(this.container); - // Remove reference to glplot - this.glplot = null; + // Remove reference to glplot + this.glplot = null; }; // getOrbitCamera :: plotly_coords -> orbit_camera_coords // inverse of getLayoutCamera function getOrbitCamera(camera) { - return [ - [camera.eye.x, camera.eye.y, camera.eye.z], - [camera.center.x, camera.center.y, camera.center.z], - [camera.up.x, camera.up.y, camera.up.z] - ]; + return [ + [camera.eye.x, camera.eye.y, camera.eye.z], + [camera.center.x, camera.center.y, camera.center.z], + [camera.up.x, camera.up.y, camera.up.z] + ]; } // getLayoutCamera :: orbit_camera_coords -> plotly_coords // inverse of getOrbitCamera function getLayoutCamera(camera) { - return { - up: {x: camera.up[0], y: camera.up[1], z: camera.up[2]}, - center: {x: camera.center[0], y: camera.center[1], z: camera.center[2]}, - eye: {x: camera.eye[0], y: camera.eye[1], z: camera.eye[2]} - }; + return { + up: { x: camera.up[0], y: camera.up[1], z: camera.up[2] }, + center: { x: camera.center[0], y: camera.center[1], z: camera.center[2] }, + eye: { x: camera.eye[0], y: camera.eye[1], z: camera.eye[2] } + }; } // get camera position in plotly coords from 'orbit-camera' coords proto.getCamera = function getCamera() { - this.glplot.camera.view.recalcMatrix(this.camera.view.lastT()); - return getLayoutCamera(this.glplot.camera); + this.glplot.camera.view.recalcMatrix(this.camera.view.lastT()); + return getLayoutCamera(this.glplot.camera); }; // set camera position with a set of plotly coords proto.setCamera = function setCamera(cameraData) { - this.glplot.camera.lookAt.apply(this, getOrbitCamera(cameraData)); + this.glplot.camera.lookAt.apply(this, getOrbitCamera(cameraData)); }; // save camera to user layout (i.e. gd.layout) proto.saveCamera = function saveCamera(layout) { - var cameraData = this.getCamera(), - cameraNestedProp = Lib.nestedProperty(layout, this.id + '.camera'), - cameraDataLastSave = cameraNestedProp.get(), - hasChanged = false; - - function same(x, y, i, j) { - var vectors = ['up', 'center', 'eye'], - components = ['x', 'y', 'z']; - return y[vectors[i]] && (x[vectors[i]][components[j]] === y[vectors[i]][components[j]]); - } - - if(cameraDataLastSave === undefined) hasChanged = true; - else { - for(var i = 0; i < 3; i++) { - for(var j = 0; j < 3; j++) { - if(!same(cameraData, cameraDataLastSave, i, j)) { - hasChanged = true; - break; - } - } + var cameraData = this.getCamera(), + cameraNestedProp = Lib.nestedProperty(layout, this.id + ".camera"), + cameraDataLastSave = cameraNestedProp.get(), + hasChanged = false; + + function same(x, y, i, j) { + var vectors = ["up", "center", "eye"], components = ["x", "y", "z"]; + return y[vectors[i]] && + x[vectors[i]][components[j]] === y[vectors[i]][components[j]]; + } + + if (cameraDataLastSave === undefined) { + hasChanged = true; + } else { + for (var i = 0; i < 3; i++) { + for (var j = 0; j < 3; j++) { + if (!same(cameraData, cameraDataLastSave, i, j)) { + hasChanged = true; + break; } + } } + } - if(hasChanged) cameraNestedProp.set(cameraData); + if (hasChanged) cameraNestedProp.set(cameraData); - return hasChanged; + return hasChanged; }; proto.updateFx = function(dragmode, hovermode) { - var camera = this.camera; - - if(camera) { - // rotate and orbital are synonymous - if(dragmode === 'orbit') { - camera.mode = 'orbit'; - camera.keyBindingMode = 'rotate'; - - } else if(dragmode === 'turntable') { - camera.up = [0, 0, 1]; - camera.mode = 'turntable'; - camera.keyBindingMode = 'rotate'; - - } else { - - // none rotation modes [pan or zoom] - camera.keyBindingMode = dragmode; - } + var camera = this.camera; + + if (camera) { + // rotate and orbital are synonymous + if (dragmode === "orbit") { + camera.mode = "orbit"; + camera.keyBindingMode = "rotate"; + } else if (dragmode === "turntable") { + camera.up = [0, 0, 1]; + camera.mode = "turntable"; + camera.keyBindingMode = "rotate"; + } else { + // none rotation modes [pan or zoom] + camera.keyBindingMode = dragmode; } + } - // to put dragmode and hovermode on the same grounds from relayout - this.fullSceneLayout.hovermode = hovermode; + // to put dragmode and hovermode on the same grounds from relayout + this.fullSceneLayout.hovermode = hovermode; }; proto.toImage = function(format) { - if(!format) format = 'png'; + if (!format) format = "png"; - if(this.staticMode) this.container.appendChild(STATIC_CANVAS); + if (this.staticMode) this.container.appendChild(STATIC_CANVAS); - // Force redraw - this.glplot.redraw(); + // Force redraw + this.glplot.redraw(); - // Grab context and yank out pixels - var gl = this.glplot.gl; - var w = gl.drawingBufferWidth; - var h = gl.drawingBufferHeight; + // Grab context and yank out pixels + var gl = this.glplot.gl; + var w = gl.drawingBufferWidth; + var h = gl.drawingBufferHeight; - gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); - var pixels = new Uint8Array(w * h * 4); - gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + var pixels = new Uint8Array(w * h * 4); + gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - // Flip pixels - for(var j = 0, k = h - 1; j < k; ++j, --k) { - for(var i = 0; i < w; ++i) { - for(var l = 0; l < 4; ++l) { - var tmp = pixels[4 * (w * j + i) + l]; - pixels[4 * (w * j + i) + l] = pixels[4 * (w * k + i) + l]; - pixels[4 * (w * k + i) + l] = tmp; - } - } + // Flip pixels + for (var j = 0, k = h - 1; j < k; ++j, --k) { + for (var i = 0; i < w; ++i) { + for (var l = 0; l < 4; ++l) { + var tmp = pixels[4 * (w * j + i) + l]; + pixels[4 * (w * j + i) + l] = pixels[4 * (w * k + i) + l]; + pixels[4 * (w * k + i) + l] = tmp; + } } - - var canvas = document.createElement('canvas'); - canvas.width = w; - canvas.height = h; - var context = canvas.getContext('2d'); - var imageData = context.createImageData(w, h); - imageData.data.set(pixels); - context.putImageData(imageData, 0, 0); - - var dataURL; - - switch(format) { - case 'jpeg': - dataURL = canvas.toDataURL('image/jpeg'); - break; - case 'webp': - dataURL = canvas.toDataURL('image/webp'); - break; - default: - dataURL = canvas.toDataURL('image/png'); - } - - if(this.staticMode) this.container.removeChild(STATIC_CANVAS); - - return dataURL; + } + + var canvas = document.createElement("canvas"); + canvas.width = w; + canvas.height = h; + var context = canvas.getContext("2d"); + var imageData = context.createImageData(w, h); + imageData.data.set(pixels); + context.putImageData(imageData, 0, 0); + + var dataURL; + + switch (format) { + case "jpeg": + dataURL = canvas.toDataURL("image/jpeg"); + break; + case "webp": + dataURL = canvas.toDataURL("image/webp"); + break; + default: + dataURL = canvas.toDataURL("image/png"); + } + + if (this.staticMode) this.container.removeChild(STATIC_CANVAS); + + return dataURL; }; module.exports = Scene; diff --git a/src/plots/gl3d/set_convert.js b/src/plots/gl3d/set_convert.js index d19caa8f713..692551350de 100644 --- a/src/plots/gl3d/set_convert.js +++ b/src/plots/gl3d/set_convert.js @@ -5,15 +5,11 @@ * 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 Axes = require('../cartesian/axes'); - +"use strict"; +var Lib = require("../../lib"); +var Axes = require("../cartesian/axes"); module.exports = function setConvert(containerOut) { - Axes.setConvert(containerOut); - containerOut.setScale = Lib.noop; + Axes.setConvert(containerOut); + containerOut.setScale = Lib.noop; }; diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index c2c033d8b0d..070dc903fcc 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -5,187 +5,168 @@ * 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'); +"use strict"; +var Lib = require("../lib"); var extendFlat = Lib.extendFlat; -var fontAttrs = require('./font_attributes'); -var colorAttrs = require('../components/color/attributes'); +var fontAttrs = require("./font_attributes"); +var colorAttrs = require("../components/color/attributes"); module.exports = { - font: { - family: extendFlat({}, fontAttrs.family, { - dflt: '"Open Sans", verdana, arial, sans-serif' - }), - size: extendFlat({}, fontAttrs.size, { - dflt: 12 - }), - color: extendFlat({}, fontAttrs.color, { - dflt: colorAttrs.defaultLine - }), - description: [ - 'Sets the global font.', - 'Note that fonts used in traces and other', - 'layout components inherit from the global font.' - ].join(' ') - }, - title: { - valType: 'string', - role: 'info', - dflt: 'Click to enter Plot title', - description: [ - 'Sets the plot\'s title.' - ].join(' ') - }, - titlefont: extendFlat({}, fontAttrs, { - description: 'Sets the title font.' + font: { + family: extendFlat({}, fontAttrs.family, { + dflt: '"Open Sans", verdana, arial, sans-serif' }), - autosize: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Determines whether or not a layout width or height', - 'that has been left undefined by the user', - 'is initialized on each relayout.', - - 'Note that, regardless of this attribute,', - 'an undefined layout width or height', - 'is always initialized on the first call to plot.' - ].join(' ') - }, - width: { - valType: 'number', - role: 'info', - min: 10, - dflt: 700, - description: [ - 'Sets the plot\'s width (in px).' - ].join(' ') - }, - height: { - valType: 'number', - role: 'info', - min: 10, - dflt: 450, - description: [ - 'Sets the plot\'s height (in px).' - ].join(' ') - }, - margin: { - l: { - valType: 'number', - role: 'info', - min: 0, - dflt: 80, - description: 'Sets the left margin (in px).' - }, - r: { - valType: 'number', - role: 'info', - min: 0, - dflt: 80, - description: 'Sets the right margin (in px).' - }, - t: { - valType: 'number', - role: 'info', - min: 0, - dflt: 100, - description: 'Sets the top margin (in px).' - }, - b: { - valType: 'number', - role: 'info', - min: 0, - dflt: 80, - description: 'Sets the bottom margin (in px).' - }, - pad: { - valType: 'number', - role: 'info', - min: 0, - dflt: 0, - description: [ - 'Sets the amount of padding (in px)', - 'between the plotting area and the axis lines' - ].join(' ') - }, - autoexpand: { - valType: 'boolean', - role: 'info', - dflt: true - } - }, - paper_bgcolor: { - valType: 'color', - role: 'style', - dflt: colorAttrs.background, - description: 'Sets the color of paper where the graph is drawn.' - }, - plot_bgcolor: { - // defined here, but set in Axes.supplyLayoutDefaults - // because it needs to know if there are (2D) axes or not - valType: 'color', - role: 'style', - dflt: colorAttrs.background, - description: [ - 'Sets the color of plotting area in-between x and y axes.' - ].join(' ') - }, - separators: { - valType: 'string', - role: 'style', - dflt: '.,', - description: [ - 'Sets the decimal and thousand separators.', - 'For example, *. * puts a \'.\' before decimals and', - 'a space between thousands.' - ].join(' ') + size: extendFlat({}, fontAttrs.size, { dflt: 12 }), + color: extendFlat({}, fontAttrs.color, { dflt: colorAttrs.defaultLine }), + description: [ + "Sets the global font.", + "Note that fonts used in traces and other", + "layout components inherit from the global font." + ].join(" ") + }, + title: { + valType: "string", + role: "info", + dflt: "Click to enter Plot title", + description: ["Sets the plot's title."].join(" ") + }, + titlefont: extendFlat({}, fontAttrs, { description: "Sets the title font." }), + autosize: { + valType: "boolean", + role: "info", + dflt: false, + description: [ + "Determines whether or not a layout width or height", + "that has been left undefined by the user", + "is initialized on each relayout.", + "Note that, regardless of this attribute,", + "an undefined layout width or height", + "is always initialized on the first call to plot." + ].join(" ") + }, + width: { + valType: "number", + role: "info", + min: 10, + dflt: 700, + description: ["Sets the plot's width (in px)."].join(" ") + }, + height: { + valType: "number", + role: "info", + min: 10, + dflt: 450, + description: ["Sets the plot's height (in px)."].join(" ") + }, + margin: { + l: { + valType: "number", + role: "info", + min: 0, + dflt: 80, + description: "Sets the left margin (in px)." }, - hidesources: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Determines whether or not a text link citing the data source is', - 'placed at the bottom-right cored of the figure.', - 'Has only an effect only on graphs that have been generated via', - 'forked graphs from the plotly service (at https://plot.ly or on-premise).' - ].join(' ') + r: { + valType: "number", + role: "info", + min: 0, + dflt: 80, + description: "Sets the right margin (in px)." }, - smith: { - // will become a boolean if/when we implement this - valType: 'enumerated', - role: 'info', - values: [false], - dflt: false + t: { + valType: "number", + role: "info", + min: 0, + dflt: 100, + description: "Sets the top margin (in px)." }, - showlegend: { - // handled in legend.supplyLayoutDefaults - // but included here because it's not in the legend object - valType: 'boolean', - role: 'info', - description: 'Determines whether or not a legend is drawn.' + b: { + valType: "number", + role: "info", + min: 0, + dflt: 80, + description: "Sets the bottom margin (in px)." }, - dragmode: { - valType: 'enumerated', - role: 'info', - values: ['zoom', 'pan', 'select', 'lasso', 'orbit', 'turntable'], - dflt: 'zoom', - description: [ - 'Determines the mode of drag interactions.', - '*select* and *lasso* apply only to scatter traces with', - 'markers or text. *orbit* and *turntable* apply only to', - '3D scenes.' - ].join(' ') + pad: { + valType: "number", + role: "info", + min: 0, + dflt: 0, + description: [ + "Sets the amount of padding (in px)", + "between the plotting area and the axis lines" + ].join(" ") }, - hovermode: { - valType: 'enumerated', - role: 'info', - values: ['x', 'y', 'closest', false], - description: 'Determines the mode of hover interactions.' - } + autoexpand: { valType: "boolean", role: "info", dflt: true } + }, + paper_bgcolor: { + valType: "color", + role: "style", + dflt: colorAttrs.background, + description: "Sets the color of paper where the graph is drawn." + }, + plot_bgcolor: { + // defined here, but set in Axes.supplyLayoutDefaults + // because it needs to know if there are (2D) axes or not + valType: "color", + role: "style", + dflt: colorAttrs.background, + description: [ + "Sets the color of plotting area in-between x and y axes." + ].join(" ") + }, + separators: { + valType: "string", + role: "style", + dflt: ".,", + description: [ + "Sets the decimal and thousand separators.", + "For example, *. * puts a '.' before decimals and", + "a space between thousands." + ].join(" ") + }, + hidesources: { + valType: "boolean", + role: "info", + dflt: false, + description: [ + "Determines whether or not a text link citing the data source is", + "placed at the bottom-right cored of the figure.", + "Has only an effect only on graphs that have been generated via", + "forked graphs from the plotly service (at https://plot.ly or on-premise)." + ].join(" ") + }, + smith: { + // will become a boolean if/when we implement this + valType: "enumerated", + role: "info", + values: [false], + dflt: false + }, + showlegend: { + // handled in legend.supplyLayoutDefaults + // but included here because it's not in the legend object + valType: "boolean", + role: "info", + description: "Determines whether or not a legend is drawn." + }, + dragmode: { + valType: "enumerated", + role: "info", + values: ["zoom", "pan", "select", "lasso", "orbit", "turntable"], + dflt: "zoom", + description: [ + "Determines the mode of drag interactions.", + "*select* and *lasso* apply only to scatter traces with", + "markers or text. *orbit* and *turntable* apply only to", + "3D scenes." + ].join(" ") + }, + hovermode: { + valType: "enumerated", + role: "info", + values: ["x", "y", "closest", false], + description: "Determines the mode of hover interactions." + } }; diff --git a/src/plots/mapbox/constants.js b/src/plots/mapbox/constants.js index f15fdffaf2c..a796d8cefc8 100644 --- a/src/plots/mapbox/constants.js +++ b/src/plots/mapbox/constants.js @@ -5,24 +5,17 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; module.exports = { - styleUrlPrefix: 'mapbox://styles/mapbox/', - styleUrlSuffix: 'v9', - - controlContainerClassName: 'mapboxgl-control-container', - - noAccessTokenErrorMsg: [ - 'Missing Mapbox access token.', - 'Mapbox trace type require a Mapbox access token to be registered.', - 'For example:', - ' Plotly.plot(gd, data, layout, { mapboxAccessToken: \'my-access-token\' });', - 'More info here: https://www.mapbox.com/help/define-access-token/' - ].join('\n'), - - mapOnErrorMsg: 'Mapbox error.' + styleUrlPrefix: "mapbox://styles/mapbox/", + styleUrlSuffix: "v9", + controlContainerClassName: "mapboxgl-control-container", + noAccessTokenErrorMsg: [ + "Missing Mapbox access token.", + "Mapbox trace type require a Mapbox access token to be registered.", + "For example:", + " Plotly.plot(gd, data, layout, { mapboxAccessToken: 'my-access-token' });", + "More info here: https://www.mapbox.com/help/define-access-token/" + ].join("\n"), + mapOnErrorMsg: "Mapbox error." }; diff --git a/src/plots/mapbox/convert_text_opts.js b/src/plots/mapbox/convert_text_opts.js index dcded05fe04..56c5190dcfb 100644 --- a/src/plots/mapbox/convert_text_opts.js +++ b/src/plots/mapbox/convert_text_opts.js @@ -5,12 +5,8 @@ * 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'); - +"use strict"; +var Lib = require("../../lib"); /** * Convert plotly.js 'textposition' to mapbox-gl 'anchor' and 'offset' @@ -24,49 +20,45 @@ var Lib = require('../../lib'); * - offset */ module.exports = function convertTextOpts(textposition, iconSize) { - var parts = textposition.split(' '), - vPos = parts[0], - hPos = parts[1]; - - // ballpack values - var factor = Array.isArray(iconSize) ? Lib.mean(iconSize) : iconSize, - xInc = 0.5 + (factor / 100), - yInc = 1.5 + (factor / 100); + var parts = textposition.split(" "), vPos = parts[0], hPos = parts[1]; - var anchorVals = ['', ''], - offset = [0, 0]; + // ballpack values + var factor = Array.isArray(iconSize) ? Lib.mean(iconSize) : iconSize, + xInc = 0.5 + factor / 100, + yInc = 1.5 + factor / 100; - switch(vPos) { - case 'top': - anchorVals[0] = 'top'; - offset[1] = -yInc; - break; - case 'bottom': - anchorVals[0] = 'bottom'; - offset[1] = yInc; - break; - } + var anchorVals = ["", ""], offset = [0, 0]; - switch(hPos) { - case 'left': - anchorVals[1] = 'right'; - offset[0] = -xInc; - break; - case 'right': - anchorVals[1] = 'left'; - offset[0] = xInc; - break; - } + switch (vPos) { + case "top": + anchorVals[0] = "top"; + offset[1] = -yInc; + break; + case "bottom": + anchorVals[0] = "bottom"; + offset[1] = yInc; + break; + } - // Mapbox text-anchor must be one of: - // center, left, right, top, bottom, - // top-left, top-right, bottom-left, bottom-right + switch (hPos) { + case "left": + anchorVals[1] = "right"; + offset[0] = -xInc; + break; + case "right": + anchorVals[1] = "left"; + offset[0] = xInc; + break; + } - var anchor; - if(anchorVals[0] && anchorVals[1]) anchor = anchorVals.join('-'); - else if(anchorVals[0]) anchor = anchorVals[0]; - else if(anchorVals[1]) anchor = anchorVals[1]; - else anchor = 'center'; + // Mapbox text-anchor must be one of: + // center, left, right, top, bottom, + // top-left, top-right, bottom-left, bottom-right + var anchor; + if (anchorVals[0] && anchorVals[1]) anchor = anchorVals.join("-"); + else if (anchorVals[0]) anchor = anchorVals[0]; + else if (anchorVals[1]) anchor = anchorVals[1]; + else anchor = "center"; - return { anchor: anchor, offset: offset }; + return { anchor: anchor, offset: offset }; }; diff --git a/src/plots/mapbox/index.js b/src/plots/mapbox/index.js index 25d5ef7dafc..68121962faa 100644 --- a/src/plots/mapbox/index.js +++ b/src/plots/mapbox/index.js @@ -5,142 +5,144 @@ * 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 mapboxgl = require("mapbox-gl"); +var Plots = require("../plots"); +var xmlnsNamespaces = require("../../constants/xmlns_namespaces"); -'use strict'; +var createMapbox = require("./mapbox"); +var constants = require("./constants"); -var mapboxgl = require('mapbox-gl'); +exports.name = "mapbox"; -var Plots = require('../plots'); -var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); +exports.attr = "subplot"; -var createMapbox = require('./mapbox'); -var constants = require('./constants'); - - -exports.name = 'mapbox'; - -exports.attr = 'subplot'; - -exports.idRoot = 'mapbox'; +exports.idRoot = "mapbox"; exports.idRegex = /^mapbox([2-9]|[1-9][0-9]+)?$/; exports.attrRegex = /^mapbox([2-9]|[1-9][0-9]+)?$/; exports.attributes = { - subplot: { - valType: 'subplotid', - role: 'info', - dflt: 'mapbox', - description: [ - 'Sets a reference between this trace\'s data coordinates and', - 'a mapbox subplot.', - 'If *mapbox* (the default value), the data refer to `layout.mapbox`.', - 'If *mapbox2*, the data refer to `layout.mapbox2`, and so on.' - ].join(' ') - } + subplot: { + valType: "subplotid", + role: "info", + dflt: "mapbox", + description: [ + "Sets a reference between this trace's data coordinates and", + "a mapbox subplot.", + "If *mapbox* (the default value), the data refer to `layout.mapbox`.", + "If *mapbox2*, the data refer to `layout.mapbox2`, and so on." + ].join(" ") + } }; -exports.layoutAttributes = require('./layout_attributes'); +exports.layoutAttributes = require("./layout_attributes"); -exports.supplyLayoutDefaults = require('./layout_defaults'); +exports.supplyLayoutDefaults = require("./layout_defaults"); exports.plot = function plotMapbox(gd) { - var fullLayout = gd._fullLayout, - calcData = gd.calcdata, - mapboxIds = Plots.getSubplotIds(fullLayout, 'mapbox'); - - var accessToken = findAccessToken(gd, mapboxIds); - mapboxgl.accessToken = accessToken; - - for(var i = 0; i < mapboxIds.length; i++) { - var id = mapboxIds[i], - subplotCalcData = Plots.getSubplotCalcData(calcData, 'mapbox', id), - opts = fullLayout[id], - mapbox = opts._subplot; - - // copy access token to fullLayout (to handle the context case) - opts.accesstoken = accessToken; - - if(!mapbox) { - mapbox = createMapbox({ - gd: gd, - container: fullLayout._glcontainer.node(), - id: id, - fullLayout: fullLayout, - staticPlot: gd._context.staticPlot - }); - - fullLayout[id]._subplot = mapbox; - } - - mapbox.plot(subplotCalcData, fullLayout, gd._promises); + var fullLayout = gd._fullLayout, + calcData = gd.calcdata, + mapboxIds = Plots.getSubplotIds(fullLayout, "mapbox"); + + var accessToken = findAccessToken(gd, mapboxIds); + mapboxgl.accessToken = accessToken; + + for (var i = 0; i < mapboxIds.length; i++) { + var id = mapboxIds[i], + subplotCalcData = Plots.getSubplotCalcData(calcData, "mapbox", id), + opts = fullLayout[id], + mapbox = opts._subplot; + + // copy access token to fullLayout (to handle the context case) + opts.accesstoken = accessToken; + + if (!mapbox) { + mapbox = createMapbox({ + gd: gd, + container: fullLayout._glcontainer.node(), + id: id, + fullLayout: fullLayout, + staticPlot: gd._context.staticPlot + }); + + fullLayout[id]._subplot = mapbox; } -}; - -exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldMapboxKeys = Plots.getSubplotIds(oldFullLayout, 'mapbox'); - for(var i = 0; i < oldMapboxKeys.length; i++) { - var oldMapboxKey = oldMapboxKeys[i]; + mapbox.plot(subplotCalcData, fullLayout, gd._promises); + } +}; - if(!newFullLayout[oldMapboxKey] && !!oldFullLayout[oldMapboxKey]._subplot) { - oldFullLayout[oldMapboxKey]._subplot.destroy(); - } +exports.clean = function( + newFullData, + newFullLayout, + oldFullData, + oldFullLayout +) { + var oldMapboxKeys = Plots.getSubplotIds(oldFullLayout, "mapbox"); + + for (var i = 0; i < oldMapboxKeys.length; i++) { + var oldMapboxKey = oldMapboxKeys[i]; + + if ( + !newFullLayout[oldMapboxKey] && !!oldFullLayout[oldMapboxKey]._subplot + ) { + oldFullLayout[oldMapboxKey]._subplot.destroy(); } + } }; exports.toSVG = function(gd) { - var fullLayout = gd._fullLayout, - subplotIds = Plots.getSubplotIds(fullLayout, 'mapbox'), - size = fullLayout._size; - - for(var i = 0; i < subplotIds.length; i++) { - var opts = fullLayout[subplotIds[i]], - domain = opts.domain, - mapbox = opts._subplot; - - var imageData = mapbox.toImage('png'); - var image = fullLayout._glimages.append('svg:image'); - - image.attr({ - xmlns: xmlnsNamespaces.svg, - 'xlink:href': imageData, - x: size.l + size.w * domain.x[0], - y: size.t + size.h * (1 - domain.y[1]), - width: size.w * (domain.x[1] - domain.x[0]), - height: size.h * (domain.y[1] - domain.y[0]), - preserveAspectRatio: 'none' - }); - - mapbox.destroy(); - } + var fullLayout = gd._fullLayout, + subplotIds = Plots.getSubplotIds(fullLayout, "mapbox"), + size = fullLayout._size; + + for (var i = 0; i < subplotIds.length; i++) { + var opts = fullLayout[subplotIds[i]], + domain = opts.domain, + mapbox = opts._subplot; + + var imageData = mapbox.toImage("png"); + var image = fullLayout._glimages.append("svg:image"); + + image.attr({ + xmlns: xmlnsNamespaces.svg, + "xlink:href": imageData, + x: size.l + size.w * domain.x[0], + y: size.t + size.h * (1 - domain.y[1]), + width: size.w * (domain.x[1] - domain.x[0]), + height: size.h * (domain.y[1] - domain.y[0]), + preserveAspectRatio: "none" + }); + + mapbox.destroy(); + } }; function findAccessToken(gd, mapboxIds) { - var fullLayout = gd._fullLayout, - context = gd._context; + var fullLayout = gd._fullLayout, context = gd._context; - // special case for Mapbox Atlas users - if(context.mapboxAccessToken === '') return ''; + // special case for Mapbox Atlas users + if (context.mapboxAccessToken === "") return ""; - // first look for access token in context - var accessToken = context.mapboxAccessToken; + // first look for access token in context + var accessToken = context.mapboxAccessToken; - // allow mapbox layout options to override it - for(var i = 0; i < mapboxIds.length; i++) { - var opts = fullLayout[mapboxIds[i]]; + // allow mapbox layout options to override it + for (var i = 0; i < mapboxIds.length; i++) { + var opts = fullLayout[mapboxIds[i]]; - if(opts.accesstoken) { - accessToken = opts.accesstoken; - break; - } + if (opts.accesstoken) { + accessToken = opts.accesstoken; + break; } + } - if(!accessToken) { - throw new Error(constants.noAccessTokenErrorMsg); - } + if (!accessToken) { + throw new Error(constants.noAccessTokenErrorMsg); + } - return accessToken; + return accessToken; } diff --git a/src/plots/mapbox/layers.js b/src/plots/mapbox/layers.js index d964ad39973..dca1f7faaee 100644 --- a/src/plots/mapbox/layers.js +++ b/src/plots/mapbox/layers.js @@ -5,219 +5,209 @@ * 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 convertTextOpts = require('./convert_text_opts'); - +"use strict"; +var Lib = require("../../lib"); +var convertTextOpts = require("./convert_text_opts"); function MapboxLayer(mapbox, index) { - this.mapbox = mapbox; - this.map = mapbox.map; + this.mapbox = mapbox; + this.map = mapbox.map; - this.uid = mapbox.uid + '-' + 'layer' + index; + this.uid = mapbox.uid + "-" + "layer" + index; - this.idSource = this.uid + '-source'; - this.idLayer = this.uid + '-layer'; + this.idSource = this.uid + "-source"; + this.idLayer = this.uid + "-layer"; - // some state variable to check if a remove/add step is needed - this.sourceType = null; - this.source = null; - this.layerType = null; - this.below = null; + // some state variable to check if a remove/add step is needed + this.sourceType = null; + this.source = null; + this.layerType = null; + this.below = null; - // is layer currently visible - this.visible = false; + // is layer currently visible + this.visible = false; } var proto = MapboxLayer.prototype; proto.update = function update(opts) { - if(!this.visible) { - - // IMPORTANT: must create source before layer to not cause errors - this.updateSource(opts); - this.updateLayer(opts); - } - else if(this.needsNewSource(opts)) { - - // IMPORTANT: must delete layer before source to not cause errors - this.updateLayer(opts); - this.updateSource(opts); - } - else if(this.needsNewLayer(opts)) { - this.updateLayer(opts); - } - - this.updateStyle(opts); - - this.visible = isVisible(opts); + if (!this.visible) { + // IMPORTANT: must create source before layer to not cause errors + this.updateSource(opts); + this.updateLayer(opts); + } else if (this.needsNewSource(opts)) { + // IMPORTANT: must delete layer before source to not cause errors + this.updateLayer(opts); + this.updateSource(opts); + } else if (this.needsNewLayer(opts)) { + this.updateLayer(opts); + } + + this.updateStyle(opts); + + this.visible = isVisible(opts); }; proto.needsNewSource = function(opts) { - - // for some reason changing layer to 'fill' or 'symbol' - // w/o changing the source throws an exception in mapbox-gl 0.18 ; - // stay safe and make new source on type changes - - return ( - this.sourceType !== opts.sourcetype || - this.source !== opts.source || - this.layerType !== opts.type - ); + // for some reason changing layer to 'fill' or 'symbol' + // w/o changing the source throws an exception in mapbox-gl 0.18 ; + // stay safe and make new source on type changes + return this.sourceType !== opts.sourcetype || + this.source !== opts.source || + this.layerType !== opts.type; }; proto.needsNewLayer = function(opts) { - return ( - this.layerType !== opts.type || - this.below !== opts.below - ); + return this.layerType !== opts.type || this.below !== opts.below; }; proto.updateSource = function(opts) { - var map = this.map; + var map = this.map; - if(map.getSource(this.idSource)) map.removeSource(this.idSource); + if (map.getSource(this.idSource)) map.removeSource(this.idSource); - this.sourceType = opts.sourcetype; - this.source = opts.source; + this.sourceType = opts.sourcetype; + this.source = opts.source; - if(!isVisible(opts)) return; + if (!isVisible(opts)) return; - var sourceOpts = convertSourceOpts(opts); + var sourceOpts = convertSourceOpts(opts); - map.addSource(this.idSource, sourceOpts); + map.addSource(this.idSource, sourceOpts); }; proto.updateLayer = function(opts) { - var map = this.map; + var map = this.map; - if(map.getLayer(this.idLayer)) map.removeLayer(this.idLayer); + if (map.getLayer(this.idLayer)) map.removeLayer(this.idLayer); - this.layerType = opts.type; + this.layerType = opts.type; - if(!isVisible(opts)) return; + if (!isVisible(opts)) return; - map.addLayer({ - id: this.idLayer, - source: this.idSource, - 'source-layer': opts.sourcelayer || '', - type: opts.type - }, opts.below); + map.addLayer( + { + id: this.idLayer, + source: this.idSource, + "source-layer": opts.sourcelayer || "", + type: opts.type + }, + opts.below + ); - // the only way to make a layer invisible is to remove it - var layoutOpts = { visibility: 'visible' }; - this.mapbox.setOptions(this.idLayer, 'setLayoutProperty', layoutOpts); + // the only way to make a layer invisible is to remove it + var layoutOpts = { visibility: "visible" }; + this.mapbox.setOptions(this.idLayer, "setLayoutProperty", layoutOpts); }; proto.updateStyle = function(opts) { - var convertedOpts = convertOpts(opts); + var convertedOpts = convertOpts(opts); - if(isVisible(opts)) { - this.mapbox.setOptions(this.idLayer, 'setLayoutProperty', convertedOpts.layout); - this.mapbox.setOptions(this.idLayer, 'setPaintProperty', convertedOpts.paint); - } + if (isVisible(opts)) { + this.mapbox.setOptions( + this.idLayer, + "setLayoutProperty", + convertedOpts.layout + ); + this.mapbox.setOptions( + this.idLayer, + "setPaintProperty", + convertedOpts.paint + ); + } }; proto.dispose = function dispose() { - var map = this.map; + var map = this.map; - map.removeLayer(this.idLayer); - map.removeSource(this.idSource); + map.removeLayer(this.idLayer); + map.removeSource(this.idSource); }; function isVisible(opts) { - var source = opts.source; + var source = opts.source; - return ( - Lib.isPlainObject(source) || - (typeof source === 'string' && source.length > 0) - ); + return Lib.isPlainObject(source) || + typeof source === "string" && source.length > 0; } function convertOpts(opts) { - var layout = {}, - paint = {}; - - switch(opts.type) { - - case 'circle': - Lib.extendFlat(paint, { - 'circle-radius': opts.circle.radius, - 'circle-color': opts.color, - 'circle-opacity': opts.opacity - }); - break; - - case 'line': - Lib.extendFlat(paint, { - 'line-width': opts.line.width, - 'line-color': opts.color, - 'line-opacity': opts.opacity - }); - break; - - case 'fill': - Lib.extendFlat(paint, { - 'fill-color': opts.color, - 'fill-outline-color': opts.fill.outlinecolor, - 'fill-opacity': opts.opacity - - // no way to pass specify outline width at the moment - }); - break; - - case 'symbol': - var symbol = opts.symbol, - textOpts = convertTextOpts(symbol.textposition, symbol.iconsize); - - Lib.extendFlat(layout, { - 'icon-image': symbol.icon + '-15', - 'icon-size': symbol.iconsize / 10, - - 'text-field': symbol.text, - 'text-size': symbol.textfont.size, - 'text-anchor': textOpts.anchor, - 'text-offset': textOpts.offset - - // TODO font family - // 'text-font': symbol.textfont.family.split(', '), - }); - - Lib.extendFlat(paint, { - 'icon-color': opts.color, - 'text-color': symbol.textfont.color, - 'text-opacity': opts.opacity - }); - break; - } - - return { layout: layout, paint: paint }; + var layout = {}, paint = {}; + + switch (opts.type) { + case "circle": + Lib.extendFlat(paint, { + "circle-radius": opts.circle.radius, + "circle-color": opts.color, + "circle-opacity": opts.opacity + }); + break; + + case "line": + Lib.extendFlat(paint, { + "line-width": opts.line.width, + "line-color": opts.color, + "line-opacity": opts.opacity + }); + break; + + case "fill": + Lib.extendFlat(paint, { + "fill-color": opts.color, + "fill-outline-color": opts.fill.outlinecolor, + // no way to pass specify outline width at the moment + "fill-opacity": opts.opacity + }); + break; + + case "symbol": + var symbol = opts.symbol, + textOpts = convertTextOpts(symbol.textposition, symbol.iconsize); + + Lib.extendFlat(layout, { + "icon-image": symbol.icon + "-15", + "icon-size": symbol.iconsize / 10, + "text-field": symbol.text, + "text-size": symbol.textfont.size, + "text-anchor": textOpts.anchor, + // TODO font family + // 'text-font': symbol.textfont.family.split(', '), + "text-offset": textOpts.offset + }); + + Lib.extendFlat(paint, { + "icon-color": opts.color, + "text-color": symbol.textfont.color, + "text-opacity": opts.opacity + }); + break; + } + + return { layout: layout, paint: paint }; } function convertSourceOpts(opts) { - var sourceType = opts.sourcetype, - source = opts.source, - sourceOpts = { type: sourceType }, - isSourceAString = (typeof source === 'string'), - field; + var sourceType = opts.sourcetype, + source = opts.source, + sourceOpts = { type: sourceType }, + isSourceAString = typeof source === "string", + field; - if(sourceType === 'geojson') field = 'data'; - else if(sourceType === 'vector') { - field = isSourceAString ? 'url' : 'tiles'; - } + if (sourceType === "geojson") { + field = "data"; + } else if (sourceType === "vector") { + field = isSourceAString ? "url" : "tiles"; + } - sourceOpts[field] = source; + sourceOpts[field] = source; - return sourceOpts; + return sourceOpts; } module.exports = function createMapboxLayer(mapbox, index, opts) { - var mapboxLayer = new MapboxLayer(mapbox, index); + var mapboxLayer = new MapboxLayer(mapbox, index); - mapboxLayer.update(opts); + mapboxLayer.update(opts); - return mapboxLayer; + return mapboxLayer; }; diff --git a/src/plots/mapbox/layout_attributes.js b/src/plots/mapbox/layout_attributes.js index 4df5146acde..ca12d3012bd 100644 --- a/src/plots/mapbox/layout_attributes.js +++ b/src/plots/mapbox/layout_attributes.js @@ -5,261 +5,248 @@ * 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 defaultLine = require('../../components/color').defaultLine; -var fontAttrs = require('../font_attributes'); -var textposition = require('../../traces/scatter/attributes').textposition; - +"use strict"; +var Lib = require("../../lib"); +var defaultLine = require("../../components/color").defaultLine; +var fontAttrs = require("../font_attributes"); +var textposition = require("../../traces/scatter/attributes").textposition; module.exports = { - domain: { - x: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the horizontal domain of this subplot', - '(in plot fraction).' - ].join(' ') - }, - y: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the vertical domain of this subplot', - '(in plot fraction).' - ].join(' ') - } + domain: { + x: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: [ + "Sets the horizontal domain of this subplot", + "(in plot fraction)." + ].join(" ") }, - - accesstoken: { - valType: 'string', - noBlank: true, - strict: true, - role: 'info', - description: [ - 'Sets the mapbox access token to be used for this mapbox map.', - 'Alternatively, the mapbox access token can be set in the', - 'configuration options under `mapboxAccessToken`.' - ].join(' ') + y: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: [ + "Sets the vertical domain of this subplot", + "(in plot fraction)." + ].join(" ") + } + }, + accesstoken: { + valType: "string", + noBlank: true, + strict: true, + role: "info", + description: [ + "Sets the mapbox access token to be used for this mapbox map.", + "Alternatively, the mapbox access token can be set in the", + "configuration options under `mapboxAccessToken`." + ].join(" ") + }, + style: { + valType: "any", + values: [ + "basic", + "streets", + "outdoors", + "light", + "dark", + "satellite", + "satellite-streets" + ], + dflt: "basic", + role: "style", + description: [ + "Sets the Mapbox map style.", + "Either input one of the default Mapbox style names or the URL to a custom style", + "or a valid Mapbox style JSON." + ].join(" ") + }, + center: { + lon: { + valType: "number", + dflt: 0, + role: "info", + description: "Sets the longitude of the center of the map (in degrees East)." }, - style: { - valType: 'any', - values: ['basic', 'streets', 'outdoors', 'light', 'dark', 'satellite', 'satellite-streets'], - dflt: 'basic', - role: 'style', - description: [ - 'Sets the Mapbox map style.', - 'Either input one of the default Mapbox style names or the URL to a custom style', - 'or a valid Mapbox style JSON.' - ].join(' ') + lat: { + valType: "number", + dflt: 0, + role: "info", + description: "Sets the latitude of the center of the map (in degrees North)." + } + }, + zoom: { + valType: "number", + dflt: 1, + role: "info", + description: "Sets the zoom level of the map." + }, + bearing: { + valType: "number", + dflt: 0, + role: "info", + description: "Sets the bearing angle of the map (in degrees counter-clockwise from North)." + }, + pitch: { + valType: "number", + dflt: 0, + role: "info", + description: [ + "Sets the pitch angle of the map", + "(in degrees, where *0* means perpendicular to the surface of the map)." + ].join(" ") + }, + layers: { + _isLinkedToArray: "layer", + sourcetype: { + valType: "enumerated", + values: ["geojson", "vector"], + dflt: "geojson", + role: "info", + description: [ + "Sets the source type for this layer.", + "Support for *raster*, *image* and *video* source types is coming soon." + ].join(" ") }, - - center: { - lon: { - valType: 'number', - dflt: 0, - role: 'info', - description: 'Sets the longitude of the center of the map (in degrees East).' - }, - lat: { - valType: 'number', - dflt: 0, - role: 'info', - description: 'Sets the latitude of the center of the map (in degrees North).' - } + source: { + valType: "any", + role: "info", + description: [ + "Sets the source data for this layer.", + "Source can be either a URL,", + "a geojson object (with `sourcetype` set to *geojson*)", + "or an array of tile URLS (with `sourcetype` set to *vector*)." + ].join(" ") + }, + sourcelayer: { + valType: "string", + dflt: "", + role: "info", + description: [ + "Specifies the layer to use from a vector tile source.", + "Required for *vector* source type that supports multiple layers." + ].join(" ") + }, + type: { + valType: "enumerated", + values: ["circle", "line", "fill", "symbol"], + dflt: "circle", + role: "info", + description: [ + "Sets the layer type.", + "Support for *raster*, *background* types is coming soon.", + "Note that *line* and *fill* are not compatible with Point", + "GeoJSON geometries." + ].join(" ") }, - zoom: { - valType: 'number', - dflt: 1, - role: 'info', - description: 'Sets the zoom level of the map.' + // attributes shared between all types + below: { + valType: "string", + dflt: "", + role: "info", + description: [ + "Determines if the layer will be inserted", + "before the layer with the specified ID.", + "If omitted or set to '',", + "the layer will be inserted above every existing layer." + ].join(" ") }, - bearing: { - valType: 'number', - dflt: 0, - role: 'info', - description: 'Sets the bearing angle of the map (in degrees counter-clockwise from North).' + color: { + valType: "color", + dflt: defaultLine, + role: "style", + description: [ + "Sets the primary layer color.", + "If `type` is *circle*, color corresponds to the circle color", + "If `type` is *line*, color corresponds to the line color", + "If `type` is *fill*, color corresponds to the fill color", + "If `type` is *symbol*, color corresponds to the icon color" + ].join(" ") }, - pitch: { - valType: 'number', - dflt: 0, - role: 'info', + opacity: { + valType: "number", + min: 0, + max: 1, + dflt: 1, + role: "info", + description: "Sets the opacity of the layer." + }, + // type-specific style attributes + circle: { + radius: { + valType: "number", + dflt: 15, + role: "style", description: [ - 'Sets the pitch angle of the map', - '(in degrees, where *0* means perpendicular to the surface of the map).' - ].join(' ') + "Sets the circle radius.", + "Has an effect only when `type` is set to *circle*." + ].join(" ") + } }, - - layers: { - _isLinkedToArray: 'layer', - - sourcetype: { - valType: 'enumerated', - values: ['geojson', 'vector'], - dflt: 'geojson', - role: 'info', - description: [ - 'Sets the source type for this layer.', - 'Support for *raster*, *image* and *video* source types is coming soon.' - ].join(' ') - }, - - source: { - valType: 'any', - role: 'info', - description: [ - 'Sets the source data for this layer.', - 'Source can be either a URL,', - 'a geojson object (with `sourcetype` set to *geojson*)', - 'or an array of tile URLS (with `sourcetype` set to *vector*).' - ].join(' ') - }, - - sourcelayer: { - valType: 'string', - dflt: '', - role: 'info', - description: [ - 'Specifies the layer to use from a vector tile source.', - 'Required for *vector* source type that supports multiple layers.' - ].join(' ') - }, - - type: { - valType: 'enumerated', - values: ['circle', 'line', 'fill', 'symbol'], - dflt: 'circle', - role: 'info', - description: [ - 'Sets the layer type.', - 'Support for *raster*, *background* types is coming soon.', - 'Note that *line* and *fill* are not compatible with Point', - 'GeoJSON geometries.' - ].join(' ') - }, - - // attributes shared between all types - below: { - valType: 'string', - dflt: '', - role: 'info', - description: [ - 'Determines if the layer will be inserted', - 'before the layer with the specified ID.', - 'If omitted or set to \'\',', - 'the layer will be inserted above every existing layer.' - ].join(' ') - }, - color: { - valType: 'color', - dflt: defaultLine, - role: 'style', - description: [ - 'Sets the primary layer color.', - 'If `type` is *circle*, color corresponds to the circle color', - 'If `type` is *line*, color corresponds to the line color', - 'If `type` is *fill*, color corresponds to the fill color', - 'If `type` is *symbol*, color corresponds to the icon color' - ].join(' ') - }, - opacity: { - valType: 'number', - min: 0, - max: 1, - dflt: 1, - role: 'info', - description: 'Sets the opacity of the layer.' - }, - - // type-specific style attributes - circle: { - radius: { - valType: 'number', - dflt: 15, - role: 'style', - description: [ - 'Sets the circle radius.', - 'Has an effect only when `type` is set to *circle*.' - ].join(' ') - } - }, - - line: { - width: { - valType: 'number', - dflt: 2, - role: 'style', - description: [ - 'Sets the line width.', - 'Has an effect only when `type` is set to *line*.' - ].join(' ') - } - }, - - fill: { - outlinecolor: { - valType: 'color', - dflt: defaultLine, - role: 'style', - description: [ - 'Sets the fill outline color.', - 'Has an effect only when `type` is set to *fill*.' - ].join(' ') - } - }, - - symbol: { - icon: { - valType: 'string', - dflt: 'marker', - role: 'style', - description: [ - 'Sets the symbol icon image.', - 'Full list: https://www.mapbox.com/maki-icons/' - ].join(' ') - }, - iconsize: { - valType: 'number', - dflt: 10, - role: 'style', - description: [ - 'Sets the symbol icon size.', - 'Has an effect only when `type` is set to *symbol*.' - ].join(' ') - }, - text: { - valType: 'string', - dflt: '', - role: 'info', - description: [ - 'Sets the symbol text.' - ].join(' ') - }, - textfont: Lib.extendDeep({}, fontAttrs, { - description: [ - 'Sets the icon text font.', - 'Has an effect only when `type` is set to *symbol*.' - ].join(' '), - family: { - dflt: 'Open Sans Regular, Arial Unicode MS Regular' - } - }), - textposition: Lib.extendFlat({}, textposition, { arrayOk: false }) - } + line: { + width: { + valType: "number", + dflt: 2, + role: "style", + description: [ + "Sets the line width.", + "Has an effect only when `type` is set to *line*." + ].join(" ") + } + }, + fill: { + outlinecolor: { + valType: "color", + dflt: defaultLine, + role: "style", + description: [ + "Sets the fill outline color.", + "Has an effect only when `type` is set to *fill*." + ].join(" ") + } + }, + symbol: { + icon: { + valType: "string", + dflt: "marker", + role: "style", + description: [ + "Sets the symbol icon image.", + "Full list: https://www.mapbox.com/maki-icons/" + ].join(" ") + }, + iconsize: { + valType: "number", + dflt: 10, + role: "style", + description: [ + "Sets the symbol icon size.", + "Has an effect only when `type` is set to *symbol*." + ].join(" ") + }, + text: { + valType: "string", + dflt: "", + role: "info", + description: ["Sets the symbol text."].join(" ") + }, + textfont: Lib.extendDeep({}, fontAttrs, { + description: [ + "Sets the icon text font.", + "Has an effect only when `type` is set to *symbol*." + ].join(" "), + family: { dflt: "Open Sans Regular, Arial Unicode MS Regular" } + }), + textposition: Lib.extendFlat({}, textposition, { arrayOk: false }) } - + } }; diff --git a/src/plots/mapbox/layout_defaults.js b/src/plots/mapbox/layout_defaults.js index 911278dec23..6ff5055331d 100644 --- a/src/plots/mapbox/layout_defaults.js +++ b/src/plots/mapbox/layout_defaults.js @@ -5,90 +5,85 @@ * 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"); - -'use strict'; - -var Lib = require('../../lib'); - -var handleSubplotDefaults = require('../subplot_defaults'); -var layoutAttributes = require('./layout_attributes'); - +var handleSubplotDefaults = require("../subplot_defaults"); +var layoutAttributes = require("./layout_attributes"); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - handleSubplotDefaults(layoutIn, layoutOut, fullData, { - type: 'mapbox', - attributes: layoutAttributes, - handleDefaults: handleDefaults, - partition: 'y' - }); + handleSubplotDefaults(layoutIn, layoutOut, fullData, { + type: "mapbox", + attributes: layoutAttributes, + handleDefaults: handleDefaults, + partition: "y" + }); }; function handleDefaults(containerIn, containerOut, coerce) { - coerce('accesstoken'); - coerce('style'); - coerce('center.lon'); - coerce('center.lat'); - coerce('zoom'); - coerce('bearing'); - coerce('pitch'); - - handleLayerDefaults(containerIn, containerOut); - - // copy ref to input container to update 'center' and 'zoom' on map move - containerOut._input = containerIn; + coerce("accesstoken"); + coerce("style"); + coerce("center.lon"); + coerce("center.lat"); + coerce("zoom"); + coerce("bearing"); + coerce("pitch"); + + handleLayerDefaults(containerIn, containerOut); + + // copy ref to input container to update 'center' and 'zoom' on map move + containerOut._input = containerIn; } function handleLayerDefaults(containerIn, containerOut) { - var layersIn = containerIn.layers || [], - layersOut = containerOut.layers = []; - - var layerIn, layerOut; + var layersIn = containerIn.layers || [], layersOut = containerOut.layers = []; - function coerce(attr, dflt) { - return Lib.coerce(layerIn, layerOut, layoutAttributes.layers, attr, dflt); - } + var layerIn, layerOut; - for(var i = 0; i < layersIn.length; i++) { - layerIn = layersIn[i]; - layerOut = {}; + function coerce(attr, dflt) { + return Lib.coerce(layerIn, layerOut, layoutAttributes.layers, attr, dflt); + } - if(!Lib.isPlainObject(layerIn)) continue; + for (var i = 0; i < layersIn.length; i++) { + layerIn = layersIn[i]; + layerOut = {}; - var sourceType = coerce('sourcetype'); - coerce('source'); + if (!Lib.isPlainObject(layerIn)) continue; - if(sourceType === 'vector') coerce('sourcelayer'); + var sourceType = coerce("sourcetype"); + coerce("source"); - // maybe add smart default based off GeoJSON geometry? - var type = coerce('type'); + if (sourceType === "vector") coerce("sourcelayer"); - coerce('below'); - coerce('color'); - coerce('opacity'); + // maybe add smart default based off GeoJSON geometry? + var type = coerce("type"); - if(type === 'circle') { - coerce('circle.radius'); - } + coerce("below"); + coerce("color"); + coerce("opacity"); - if(type === 'line') { - coerce('line.width'); - } + if (type === "circle") { + coerce("circle.radius"); + } - if(type === 'fill') { - coerce('fill.outlinecolor'); - } + if (type === "line") { + coerce("line.width"); + } - if(type === 'symbol') { - coerce('symbol.icon'); - coerce('symbol.iconsize'); + if (type === "fill") { + coerce("fill.outlinecolor"); + } - coerce('symbol.text'); - Lib.coerceFont(coerce, 'symbol.textfont'); - coerce('symbol.textposition'); - } + if (type === "symbol") { + coerce("symbol.icon"); + coerce("symbol.iconsize"); - layerOut._index = i; - layersOut.push(layerOut); + coerce("symbol.text"); + Lib.coerceFont(coerce, "symbol.textfont"); + coerce("symbol.textposition"); } + + layerOut._index = i; + layersOut.push(layerOut); + } } diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 522cf66b8a6..078d7a12960 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -5,455 +5,433 @@ * 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 mapboxgl = require("mapbox-gl"); - -'use strict'; - -var mapboxgl = require('mapbox-gl'); - -var Fx = require('../cartesian/graph_interact'); -var Lib = require('../../lib'); -var constants = require('./constants'); -var layoutAttributes = require('./layout_attributes'); -var createMapboxLayer = require('./layers'); - +var Fx = require("../cartesian/graph_interact"); +var Lib = require("../../lib"); +var constants = require("./constants"); +var layoutAttributes = require("./layout_attributes"); +var createMapboxLayer = require("./layers"); function Mapbox(opts) { - this.id = opts.id; - this.gd = opts.gd; - this.container = opts.container; - this.isStatic = opts.staticPlot; - - var fullLayout = opts.fullLayout; - - // unique id for this Mapbox instance - this.uid = fullLayout._uid + '-' + this.id; - - // full mapbox options (N.B. needs to be updated on every updates) - this.opts = fullLayout[this.id]; - - // create framework on instantiation for a smoother first plot call - this.div = null; - this.xaxis = null; - this.yaxis = null; - this.createFramework(fullLayout); - - // state variables used to infer how and what to update - this.map = null; - this.accessToken = null; - this.styleObj = null; - this.traceHash = {}; - this.layerList = []; + this.id = opts.id; + this.gd = opts.gd; + this.container = opts.container; + this.isStatic = opts.staticPlot; + + var fullLayout = opts.fullLayout; + + // unique id for this Mapbox instance + this.uid = fullLayout._uid + "-" + this.id; + + // full mapbox options (N.B. needs to be updated on every updates) + this.opts = fullLayout[this.id]; + + // create framework on instantiation for a smoother first plot call + this.div = null; + this.xaxis = null; + this.yaxis = null; + this.createFramework(fullLayout); + + // state variables used to infer how and what to update + this.map = null; + this.accessToken = null; + this.styleObj = null; + this.traceHash = {}; + this.layerList = []; } var proto = Mapbox.prototype; module.exports = function createMapbox(opts) { - var mapbox = new Mapbox(opts); + var mapbox = new Mapbox(opts); - return mapbox; + return mapbox; }; proto.plot = function(calcData, fullLayout, promises) { - var self = this; - - // feed in new mapbox options - var opts = self.opts = fullLayout[this.id]; - - // remove map and create a new map if access token has change - if(self.map && (opts.accesstoken !== self.accessToken)) { - self.map.remove(); - self.map = null; - self.styleObj = null; - self.traceHash = []; - self.layerList = {}; - } - - var promise; - - if(!self.map) { - promise = new Promise(function(resolve, reject) { - self.createMap(calcData, fullLayout, resolve, reject); - }); - } - else { - promise = new Promise(function(resolve, reject) { - self.updateMap(calcData, fullLayout, resolve, reject); - }); - } - - promises.push(promise); -}; - -proto.createMap = function(calcData, fullLayout, resolve, reject) { - var self = this, - gd = self.gd, - opts = self.opts; - - // store style id and URL or object - var styleObj = self.styleObj = getStyleObj(opts.style); - - // store access token associated with this map - self.accessToken = opts.accesstoken; - - // create the map! - var map = self.map = new mapboxgl.Map({ - container: self.div, - - style: styleObj.style, - center: convertCenter(opts.center), - zoom: opts.zoom, - bearing: opts.bearing, - pitch: opts.pitch, - - interactive: !self.isStatic, - preserveDrawingBuffer: self.isStatic - }); + var self = this; - // clear navigation container - var className = constants.controlContainerClassName, - controlContainer = self.div.getElementsByClassName(className)[0]; - self.div.removeChild(controlContainer); + // feed in new mapbox options + var opts = self.opts = fullLayout[this.id]; - self.rejectOnError(reject); + // remove map and create a new map if access token has change + if (self.map && opts.accesstoken !== self.accessToken) { + self.map.remove(); + self.map = null; + self.styleObj = null; + self.traceHash = []; + self.layerList = {}; + } - map.once('load', function() { - self.updateData(calcData); - self.updateLayout(fullLayout); + var promise; - self.resolveOnRender(resolve); + if (!self.map) { + promise = new Promise(function(resolve, reject) { + self.createMap(calcData, fullLayout, resolve, reject); }); - - // keep track of pan / zoom in user layout and emit relayout event - map.on('moveend', function(eventData) { - if(!self.map) return; - - var view = self.getView(); - - opts._input.center = opts.center = view.center; - opts._input.zoom = opts.zoom = view.zoom; - opts._input.bearing = opts.bearing = view.bearing; - opts._input.pitch = opts.pitch = view.pitch; - - // 'moveend' gets triggered by map.setCenter, map.setZoom, - // map.setBearing and map.setPitch. - // - // Here, we make sure that 'plotly_relayout' is - // triggered here only when the 'moveend' originates from a - // mouse target (filtering out API calls) to not - // duplicate 'plotly_relayout' events. - - if(eventData.originalEvent) { - var update = {}; - update[self.id] = Lib.extendFlat({}, view); - gd.emit('plotly_relayout', update); - } + } else { + promise = new Promise(function(resolve, reject) { + self.updateMap(calcData, fullLayout, resolve, reject); }); + } - map.on('mousemove', function(evt) { - var bb = self.div.getBoundingClientRect(); + promises.push(promise); +}; - // some hackery to get Fx.hover to work +proto.createMap = function(calcData, fullLayout, resolve, reject) { + var self = this, gd = self.gd, opts = self.opts; + + // store style id and URL or object + var styleObj = self.styleObj = getStyleObj(opts.style); + + // store access token associated with this map + self.accessToken = opts.accesstoken; + + // create the map! + var map = self.map = new mapboxgl.Map({ + container: self.div, + style: styleObj.style, + center: convertCenter(opts.center), + zoom: opts.zoom, + bearing: opts.bearing, + pitch: opts.pitch, + interactive: !self.isStatic, + preserveDrawingBuffer: self.isStatic + }); + + // clear navigation container + var className = constants.controlContainerClassName, + controlContainer = self.div.getElementsByClassName(className)[0]; + self.div.removeChild(controlContainer); + + self.rejectOnError(reject); + + map.once("load", function() { + self.updateData(calcData); + self.updateLayout(fullLayout); + + self.resolveOnRender(resolve); + }); + + // keep track of pan / zoom in user layout and emit relayout event + map.on("moveend", function(eventData) { + if (!self.map) return; + + var view = self.getView(); + + opts._input.center = opts.center = view.center; + opts._input.zoom = opts.zoom = view.zoom; + opts._input.bearing = opts.bearing = view.bearing; + opts._input.pitch = opts.pitch = view.pitch; + + // 'moveend' gets triggered by map.setCenter, map.setZoom, + // map.setBearing and map.setPitch. + // + // Here, we make sure that 'plotly_relayout' is + // triggered here only when the 'moveend' originates from a + // mouse target (filtering out API calls) to not + // duplicate 'plotly_relayout' events. + if (eventData.originalEvent) { + var update = {}; + update[self.id] = Lib.extendFlat({}, view); + gd.emit("plotly_relayout", update); + } + }); - evt.clientX = evt.point.x + bb.left; - evt.clientY = evt.point.y + bb.top; + map.on("mousemove", function(evt) { + var bb = self.div.getBoundingClientRect(); - evt.target.getBoundingClientRect = function() { return bb; }; + // some hackery to get Fx.hover to work + evt.clientX = evt.point.x + bb.left; + evt.clientY = evt.point.y + bb.top; - self.xaxis.p2c = function() { return evt.lngLat.lng; }; - self.yaxis.p2c = function() { return evt.lngLat.lat; }; + evt.target.getBoundingClientRect = function() { + return bb; + }; - Fx.hover(gd, evt, self.id); - }); + self.xaxis.p2c = function() { + return evt.lngLat.lng; + }; + self.yaxis.p2c = function() { + return evt.lngLat.lat; + }; - map.on('click', function() { - Fx.click(gd, { target: true }); - }); + Fx.hover(gd, evt, self.id); + }); - function unhover() { - Fx.loneUnhover(fullLayout._toppaper); - } + map.on("click", function() { + Fx.click(gd, { target: true }); + }); - map.on('dragstart', unhover); - map.on('zoomstart', unhover); + function unhover() { + Fx.loneUnhover(fullLayout._toppaper); + } + map.on("dragstart", unhover); + map.on("zoomstart", unhover); }; proto.updateMap = function(calcData, fullLayout, resolve, reject) { - var self = this, - map = self.map; - - self.rejectOnError(reject); + var self = this, map = self.map; - var styleObj = getStyleObj(self.opts.style); + self.rejectOnError(reject); - if(self.styleObj.id !== styleObj.id) { - self.styleObj = styleObj; - map.setStyle(styleObj.style); + var styleObj = getStyleObj(self.opts.style); - map.style.once('load', function() { + if (self.styleObj.id !== styleObj.id) { + self.styleObj = styleObj; + map.setStyle(styleObj.style); - // need to rebuild trace layers on reload - // to avoid 'lost event' errors - self.traceHash = {}; + map.style.once("load", function() { + // need to rebuild trace layers on reload + // to avoid 'lost event' errors + self.traceHash = {}; - self.updateData(calcData); - self.updateLayout(fullLayout); + self.updateData(calcData); + self.updateLayout(fullLayout); - self.resolveOnRender(resolve); - }); - } - else { - self.updateData(calcData); - self.updateLayout(fullLayout); + self.resolveOnRender(resolve); + }); + } else { + self.updateData(calcData); + self.updateLayout(fullLayout); - self.resolveOnRender(resolve); - } + self.resolveOnRender(resolve); + } }; proto.updateData = function(calcData) { - var traceHash = this.traceHash; + var traceHash = this.traceHash; - var traceObj, trace, i, j; + var traceObj, trace, i, j; - // update or create trace objects - for(i = 0; i < calcData.length; i++) { - var calcTrace = calcData[i]; + // update or create trace objects + for (i = 0; i < calcData.length; i++) { + var calcTrace = calcData[i]; - trace = calcTrace[0].trace; - traceObj = traceHash[trace.uid]; + trace = calcTrace[0].trace; + traceObj = traceHash[trace.uid]; - if(traceObj) traceObj.update(calcTrace); - else if(trace._module) { - traceHash[trace.uid] = trace._module.plot(this, calcTrace); - } + if (traceObj) { + traceObj.update(calcTrace); + } else if (trace._module) { + traceHash[trace.uid] = trace._module.plot(this, calcTrace); } + } - // remove empty trace objects - var ids = Object.keys(traceHash); - id_loop: - for(i = 0; i < ids.length; i++) { - var id = ids[i]; - - for(j = 0; j < calcData.length; j++) { - trace = calcData[j][0].trace; + // remove empty trace objects + var ids = Object.keys(traceHash); + id_loop: + for (i = 0; i < ids.length; i++) { + var id = ids[i]; - if(id === trace.uid) continue id_loop; - } + for (j = 0; j < calcData.length; j++) { + trace = calcData[j][0].trace; - traceObj = traceHash[id]; - traceObj.dispose(); - delete traceHash[id]; + if (id === trace.uid) continue id_loop; } + + traceObj = traceHash[id]; + traceObj.dispose(); + delete traceHash[id]; + } }; proto.updateLayout = function(fullLayout) { - var map = this.map, - opts = this.opts; + var map = this.map, opts = this.opts; - map.setCenter(convertCenter(opts.center)); - map.setZoom(opts.zoom); - map.setBearing(opts.bearing); - map.setPitch(opts.pitch); + map.setCenter(convertCenter(opts.center)); + map.setZoom(opts.zoom); + map.setBearing(opts.bearing); + map.setPitch(opts.pitch); - this.updateLayers(); - this.updateFramework(fullLayout); - this.map.resize(); + this.updateLayers(); + this.updateFramework(fullLayout); + this.map.resize(); }; proto.resolveOnRender = function(resolve) { - var map = this.map; + var map = this.map; - map.on('render', function onRender() { - if(map.loaded()) { - map.off('render', onRender); - resolve(); - } - }); + map.on("render", function onRender() { + if (map.loaded()) { + map.off("render", onRender); + resolve(); + } + }); }; proto.rejectOnError = function(reject) { - var map = this.map; + var map = this.map; - function handler() { - reject(new Error(constants.mapOnErrorMsg)); - } + function handler() { + reject(new Error(constants.mapOnErrorMsg)); + } - map.once('error', handler); - map.once('style.error', handler); - map.once('source.error', handler); - map.once('tile.error', handler); - map.once('layer.error', handler); + map.once("error", handler); + map.once("style.error", handler); + map.once("source.error", handler); + map.once("tile.error", handler); + map.once("layer.error", handler); }; proto.createFramework = function(fullLayout) { - var self = this; + var self = this; - var div = self.div = document.createElement('div'); + var div = self.div = document.createElement("div"); - div.id = self.uid; - div.style.position = 'absolute'; + div.id = self.uid; + div.style.position = "absolute"; - self.container.appendChild(div); + self.container.appendChild(div); - // create mock x/y axes for hover routine - - self.xaxis = { - _id: 'x', - c2p: function(v) { return self.project(v).x; } - }; + // create mock x/y axes for hover routine + self.xaxis = { + _id: "x", + c2p: function(v) { + return self.project(v).x; + } + }; - self.yaxis = { - _id: 'y', - c2p: function(v) { return self.project(v).y; } - }; + self.yaxis = { + _id: "y", + c2p: function(v) { + return self.project(v).y; + } + }; - self.updateFramework(fullLayout); + self.updateFramework(fullLayout); }; proto.updateFramework = function(fullLayout) { - var domain = fullLayout[this.id].domain, - size = fullLayout._size; - - var style = this.div.style; + var domain = fullLayout[this.id].domain, size = fullLayout._size; - // TODO Is this correct? It seems to get the map zoom level wrong? + var style = this.div.style; - style.width = size.w * (domain.x[1] - domain.x[0]) + 'px'; - style.height = size.h * (domain.y[1] - domain.y[0]) + 'px'; - style.left = size.l + domain.x[0] * size.w + 'px'; - style.top = size.t + (1 - domain.y[1]) * size.h + 'px'; + // TODO Is this correct? It seems to get the map zoom level wrong? + style.width = size.w * (domain.x[1] - domain.x[0]) + "px"; + style.height = size.h * (domain.y[1] - domain.y[0]) + "px"; + style.left = size.l + domain.x[0] * size.w + "px"; + style.top = size.t + (1 - domain.y[1]) * size.h + "px"; - this.xaxis._offset = size.l + domain.x[0] * size.w; - this.xaxis._length = size.w * (domain.x[1] - domain.x[0]); + this.xaxis._offset = size.l + domain.x[0] * size.w; + this.xaxis._length = size.w * (domain.x[1] - domain.x[0]); - this.yaxis._offset = size.t + (1 - domain.y[1]) * size.h; - this.yaxis._length = size.h * (domain.y[1] - domain.y[0]); + this.yaxis._offset = size.t + (1 - domain.y[1]) * size.h; + this.yaxis._length = size.h * (domain.y[1] - domain.y[0]); }; proto.updateLayers = function() { - var opts = this.opts, - layers = opts.layers, - layerList = this.layerList, - i; - - // if the layer arrays don't match, - // don't try to be smart, - // delete them all, and start all over. - - if(layers.length !== layerList.length) { - for(i = 0; i < layerList.length; i++) { - layerList[i].dispose(); - } + var opts = this.opts, layers = opts.layers, layerList = this.layerList, i; + + // if the layer arrays don't match, + // don't try to be smart, + // delete them all, and start all over. + if (layers.length !== layerList.length) { + for (i = 0; i < layerList.length; i++) { + layerList[i].dispose(); + } - layerList = this.layerList = []; + layerList = this.layerList = []; - for(i = 0; i < layers.length; i++) { - layerList.push(createMapboxLayer(this, i, layers[i])); - } + for (i = 0; i < layers.length; i++) { + layerList.push(createMapboxLayer(this, i, layers[i])); } - else { - for(i = 0; i < layers.length; i++) { - layerList[i].update(layers[i]); - } + } else { + for (i = 0; i < layers.length; i++) { + layerList[i].update(layers[i]); } + } }; proto.destroy = function() { - if(this.map) { - this.map.remove(); - this.map = null; - } - this.container.removeChild(this.div); + if (this.map) { + this.map.remove(); + this.map = null; + } + this.container.removeChild(this.div); }; proto.toImage = function() { - return this.map.getCanvas().toDataURL(); + return this.map.getCanvas().toDataURL(); }; // convenience wrapper to create blank GeoJSON sources // and avoid 'invalid GeoJSON' errors proto.initSource = function(idSource) { - var blank = { - type: 'geojson', - data: { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [] - } - } - }; + var blank = { + type: "geojson", + data: { type: "Feature", geometry: { type: "Point", coordinates: [] } } + }; - return this.map.addSource(idSource, blank); + return this.map.addSource(idSource, blank); }; // convenience wrapper to set data of GeoJSON sources proto.setSourceData = function(idSource, data) { - this.map.getSource(idSource).setData(data); + this.map.getSource(idSource).setData(data); }; // convenience wrapper to create set multiple layer // 'layout' or 'paint options at once. proto.setOptions = function(id, methodName, opts) { - var map = this.map, - keys = Object.keys(opts); + var map = this.map, keys = Object.keys(opts); - for(var i = 0; i < keys.length; i++) { - var key = keys[i]; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; - map[methodName](id, key, opts[key]); - } + map[methodName](id, key, opts[key]); + } }; // convenience method to project a [lon, lat] array to pixel coords proto.project = function(v) { - return this.map.project(new mapboxgl.LngLat(v[0], v[1])); + return this.map.project(new mapboxgl.LngLat(v[0], v[1])); }; // get map's current view values in plotly.js notation proto.getView = function() { - var map = this.map; + var map = this.map; - var mapCenter = map.getCenter(), - center = { lon: mapCenter.lng, lat: mapCenter.lat }; + var mapCenter = map.getCenter(), + center = { lon: mapCenter.lng, lat: mapCenter.lat }; - return { - center: center, - zoom: map.getZoom(), - bearing: map.getBearing(), - pitch: map.getPitch() - }; + return { + center: center, + zoom: map.getZoom(), + bearing: map.getBearing(), + pitch: map.getPitch() + }; }; function getStyleObj(val) { - var styleValues = layoutAttributes.style.values, - styleDflt = layoutAttributes.style.dflt, - styleObj = {}; - - if(Lib.isPlainObject(val)) { - styleObj.id = val.id; - styleObj.style = val; - } - else if(typeof val === 'string') { - styleObj.id = val; - styleObj.style = (styleValues.indexOf(val) !== -1) ? - convertStyleVal(val) : - val; - } - else { - styleObj.id = styleDflt; - styleObj.style = convertStyleVal(styleDflt); - } - - return styleObj; + var styleValues = layoutAttributes.style.values, + styleDflt = layoutAttributes.style.dflt, + styleObj = {}; + + if (Lib.isPlainObject(val)) { + styleObj.id = val.id; + styleObj.style = val; + } else if (typeof val === "string") { + styleObj.id = val; + styleObj.style = styleValues.indexOf(val) !== -1 + ? convertStyleVal(val) + : val; + } else { + styleObj.id = styleDflt; + styleObj.style = convertStyleVal(styleDflt); + } + + return styleObj; } // if style is part of the 'official' mapbox values, add URL prefix and suffix function convertStyleVal(val) { - return constants.styleUrlPrefix + val + '-' + constants.styleUrlSuffix; + return constants.styleUrlPrefix + val + "-" + constants.styleUrlSuffix; } function convertCenter(center) { - return [center.lon, center.lat]; + return [center.lon, center.lat]; } diff --git a/src/plots/pad_attributes.js b/src/plots/pad_attributes.js index f5c92700b5d..8f7f8ab5802 100644 --- a/src/plots/pad_attributes.js +++ b/src/plots/pad_attributes.js @@ -5,32 +5,30 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = { - t: { - valType: 'number', - dflt: 0, - role: 'style', - description: 'The amount of padding (in px) along the top of the component.' - }, - r: { - valType: 'number', - dflt: 0, - role: 'style', - description: 'The amount of padding (in px) on the right side of the component.' - }, - b: { - valType: 'number', - dflt: 0, - role: 'style', - description: 'The amount of padding (in px) along the bottom of the component.' - }, - l: { - valType: 'number', - dflt: 0, - role: 'style', - description: 'The amount of padding (in px) on the left side of the component.' - } + t: { + valType: "number", + dflt: 0, + role: "style", + description: "The amount of padding (in px) along the top of the component." + }, + r: { + valType: "number", + dflt: 0, + role: "style", + description: "The amount of padding (in px) on the right side of the component." + }, + b: { + valType: "number", + dflt: 0, + role: "style", + description: "The amount of padding (in px) along the bottom of the component." + }, + l: { + valType: "number", + dflt: 0, + role: "style", + description: "The amount of padding (in px) on the left side of the component." + } }; diff --git a/src/plots/plots.js b/src/plots/plots.js index 3253aba48f7..57edc1f79c3 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -5,40 +5,37 @@ * 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 d3 = require("d3"); +var isNumeric = require("fast-isnumeric"); - -'use strict'; - -var d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var Plotly = require('../plotly'); -var Registry = require('../registry'); -var Lib = require('../lib'); -var Color = require('../components/color'); +var Plotly = require("../plotly"); +var Registry = require("../registry"); +var Lib = require("../lib"); +var Color = require("../components/color"); var plots = module.exports = {}; -var animationAttrs = require('./animation_attributes'); -var frameAttrs = require('./frame_attributes'); +var animationAttrs = require("./animation_attributes"); +var frameAttrs = require("./frame_attributes"); // Expose registry methods on Plots for backward-compatibility Lib.extendFlat(plots, Registry); -plots.attributes = require('./attributes'); +plots.attributes = require("./attributes"); plots.attributes.type.values = plots.allTypes; -plots.fontAttrs = require('./font_attributes'); -plots.layoutAttributes = require('./layout_attributes'); +plots.fontAttrs = require("./font_attributes"); +plots.layoutAttributes = require("./layout_attributes"); // TODO make this a plot attribute? -plots.fontWeight = 'normal'; +plots.fontWeight = "normal"; var subplotsRegistry = plots.subplotsRegistry; var transformsRegistry = plots.transformsRegistry; -var ErrorBars = require('../components/errorbars'); +var ErrorBars = require("../components/errorbars"); -var commandModule = require('./command'); +var commandModule = require("./command"); plots.executeAPICommand = commandModule.executeAPICommand; plots.computeAPICommandBindings = commandModule.computeAPICommandBindings; plots.manageCommandObserver = commandModule.manageCommandObserver; @@ -61,21 +58,21 @@ plots.hasSimpleAPICommandBindings = commandModule.hasSimpleAPICommandBindings; * TODO incorporate cartesian/gl2d axis finders in this paradigm. */ plots.findSubplotIds = function findSubplotIds(data, type) { - var subplotIds = []; + var subplotIds = []; - if(!plots.subplotsRegistry[type]) return subplotIds; + if (!plots.subplotsRegistry[type]) return subplotIds; - var attr = plots.subplotsRegistry[type].attr; + var attr = plots.subplotsRegistry[type].attr; - for(var i = 0; i < data.length; i++) { - var trace = data[i]; + for (var i = 0; i < data.length; i++) { + var trace = data[i]; - if(plots.traceIs(trace, type) && subplotIds.indexOf(trace[attr]) === -1) { - subplotIds.push(trace[attr]); - } + if (plots.traceIs(trace, type) && subplotIds.indexOf(trace[attr]) === -1) { + subplotIds.push(trace[attr]); } + } - return subplotIds; + return subplotIds; }; /** @@ -88,36 +85,37 @@ plots.findSubplotIds = function findSubplotIds(data, type) { * */ plots.getSubplotIds = function getSubplotIds(layout, type) { - var _module = plots.subplotsRegistry[type]; + var _module = plots.subplotsRegistry[type]; - if(!_module) return []; + if (!_module) return []; - // layout must be 'fullLayout' here - if(type === 'cartesian' && (!layout._has || !layout._has('cartesian'))) return []; - if(type === 'gl2d' && (!layout._has || !layout._has('gl2d'))) return []; - if(type === 'cartesian' || type === 'gl2d') { - return Object.keys(layout._plots || {}); - } + // layout must be 'fullLayout' here + if (type === "cartesian" && (!layout._has || !layout._has("cartesian"))) { + return []; + } + if (type === "gl2d" && (!layout._has || !layout._has("gl2d"))) return []; + if (type === "cartesian" || type === "gl2d") { + return Object.keys(layout._plots || {}); + } - var idRegex = _module.idRegex, - layoutKeys = Object.keys(layout), - subplotIds = []; + var idRegex = _module.idRegex, + layoutKeys = Object.keys(layout), + subplotIds = []; - for(var i = 0; i < layoutKeys.length; i++) { - var layoutKey = layoutKeys[i]; + for (var i = 0; i < layoutKeys.length; i++) { + var layoutKey = layoutKeys[i]; - if(idRegex.test(layoutKey)) subplotIds.push(layoutKey); - } + if (idRegex.test(layoutKey)) subplotIds.push(layoutKey); + } - // order the ids - var idLen = _module.idRoot.length; - subplotIds.sort(function(a, b) { - var aNum = +(a.substr(idLen) || 1), - bNum = +(b.substr(idLen) || 1); - return aNum - bNum; - }); + // order the ids + var idLen = _module.idRoot.length; + subplotIds.sort(function(a, b) { + var aNum = +(a.substr(idLen) || 1), bNum = +(b.substr(idLen) || 1); + return aNum - bNum; + }); - return subplotIds; + return subplotIds; }; /** @@ -131,30 +129,27 @@ plots.getSubplotIds = function getSubplotIds(layout, type) { * */ plots.getSubplotData = function getSubplotData(data, type, subplotId) { - if(!plots.subplotsRegistry[type]) return []; + if (!plots.subplotsRegistry[type]) return []; - var attr = plots.subplotsRegistry[type].attr, - subplotData = [], - trace; + var attr = plots.subplotsRegistry[type].attr, subplotData = [], trace; - for(var i = 0; i < data.length; i++) { - trace = data[i]; + for (var i = 0; i < data.length; i++) { + trace = data[i]; - if(type === 'gl2d' && plots.traceIs(trace, 'gl2d')) { - var spmatch = Plotly.Axes.subplotMatch, - subplotX = 'x' + subplotId.match(spmatch)[1], - subplotY = 'y' + subplotId.match(spmatch)[2]; + if (type === "gl2d" && plots.traceIs(trace, "gl2d")) { + var spmatch = Plotly.Axes.subplotMatch, + subplotX = "x" + subplotId.match(spmatch)[1], + subplotY = "y" + subplotId.match(spmatch)[2]; - if(trace[attr[0]] === subplotX && trace[attr[1]] === subplotY) { - subplotData.push(trace); - } - } - else { - if(trace[attr] === subplotId) subplotData.push(trace); - } + if (trace[attr[0]] === subplotX && trace[attr[1]] === subplotY) { + subplotData.push(trace); + } + } else { + if (trace[attr] === subplotId) subplotData.push(trace); } + } - return subplotData; + return subplotData; }; /** @@ -167,85 +162,88 @@ plots.getSubplotData = function getSubplotData(data, type, subplotId) { * @return {array} array of calcdata traces */ plots.getSubplotCalcData = function(calcData, type, subplotId) { - if(!plots.subplotsRegistry[type]) return []; + if (!plots.subplotsRegistry[type]) return []; - var attr = plots.subplotsRegistry[type].attr; - var subplotCalcData = []; + var attr = plots.subplotsRegistry[type].attr; + var subplotCalcData = []; - for(var i = 0; i < calcData.length; i++) { - var calcTrace = calcData[i], - trace = calcTrace[0].trace; + for (var i = 0; i < calcData.length; i++) { + var calcTrace = calcData[i], trace = calcTrace[0].trace; - if(trace[attr] === subplotId) subplotCalcData.push(calcTrace); - } + if (trace[attr] === subplotId) subplotCalcData.push(calcTrace); + } - return subplotCalcData; + return subplotCalcData; }; // in some cases the browser doesn't seem to know how big // the text is at first, so it needs to draw it, // then wait a little, then draw it again plots.redrawText = function(gd) { + // do not work if polar is present + if (gd.data && gd.data[0] && gd.data[0].r) return; - // do not work if polar is present - if((gd.data && gd.data[0] && gd.data[0].r)) return; - - return new Promise(function(resolve) { - setTimeout(function() { - Registry.getComponentMethod('annotations', 'draw')(gd); - Registry.getComponentMethod('legend', 'draw')(gd); + return new Promise(function(resolve) { + setTimeout( + function() { + Registry.getComponentMethod("annotations", "draw")(gd); + Registry.getComponentMethod("legend", "draw")(gd); - (gd.calcdata || []).forEach(function(d) { - if(d[0] && d[0].t && d[0].t.cb) d[0].t.cb(); - }); + (gd.calcdata || []).forEach(function(d) { + if (d[0] && d[0].t && d[0].t.cb) d[0].t.cb(); + }); - resolve(plots.previousPromises(gd)); - }, 300); - }); + resolve(plots.previousPromises(gd)); + }, + 300 + ); + }); }; // resize plot about the container size plots.resize = function(gd) { - return new Promise(function(resolve, reject) { - - if(!gd || d3.select(gd).style('display') === 'none') { - reject(new Error('Resize must be passed a plot div element.')); - } + return new Promise(function(resolve, reject) { + if (!gd || d3.select(gd).style("display") === "none") { + reject(new Error("Resize must be passed a plot div element.")); + } - if(gd._redrawTimer) clearTimeout(gd._redrawTimer); + if (gd._redrawTimer) clearTimeout(gd._redrawTimer); - gd._redrawTimer = setTimeout(function() { - // return if there is nothing to resize - if(gd.layout.width && gd.layout.height) { - resolve(gd); - return; - } + gd._redrawTimer = setTimeout( + function() { + // return if there is nothing to resize + if (gd.layout.width && gd.layout.height) { + resolve(gd); + return; + } - delete gd.layout.width; - delete gd.layout.height; + delete gd.layout.width; + delete gd.layout.height; - // autosizing doesn't count as a change that needs saving - var oldchanged = gd.changed; + // autosizing doesn't count as a change that needs saving + var oldchanged = gd.changed; - // nor should it be included in the undo queue - gd.autoplay = true; + // nor should it be included in the undo queue + gd.autoplay = true; - Plotly.relayout(gd, { autosize: true }).then(function() { - gd.changed = oldchanged; - resolve(gd); - }); - }, 100); - }); + Plotly.relayout(gd, { autosize: true }).then(function() { + gd.changed = oldchanged; + resolve(gd); + }); + }, + 100 + ); + }); }; - // for use in Lib.syncOrAsync, check if there are any // pending promises in this plot and wait for them plots.previousPromises = function(gd) { - if((gd._promises || []).length) { - return Promise.all(gd._promises) - .then(function() { gd._promises = []; }); - } + if ((gd._promises || []).length) { + return Promise.all(gd._promises).then(function() { + gd._promises = []; + }); + } }; /** @@ -255,121 +253,119 @@ plots.previousPromises = function(gd) { * Add source links to your graph inside the 'showSources' config argument. */ plots.addLinks = function(gd) { - var fullLayout = gd._fullLayout; - - var linkContainer = fullLayout._paper - .selectAll('text.js-plot-link-container').data([0]); - - linkContainer.enter().append('text') - .classed('js-plot-link-container', true) - .style({ - 'font-family': '"Open Sans", Arial, sans-serif', - 'font-size': '12px', - 'fill': Color.defaultLine, - 'pointer-events': 'all' - }) - .each(function() { - var links = d3.select(this); - links.append('tspan').classed('js-link-to-tool', true); - links.append('tspan').classed('js-link-spacer', true); - links.append('tspan').classed('js-sourcelinks', true); - }); - - // The text node inside svg - var text = linkContainer.node(), - attrs = { - y: fullLayout._paper.attr('height') - 9 - }; - - // If text's width is bigger than the layout - // Check that text is a child node or document.body - // because otherwise IE/Edge might throw an exception - // when calling getComputedTextLength(). - // Apparently offsetParent is null for invisibles. - if(document.body.contains(text) && text.getComputedTextLength() >= (fullLayout.width - 20)) { - // Align the text at the left - attrs['text-anchor'] = 'start'; - attrs.x = 5; - } - else { - // Align the text at the right - attrs['text-anchor'] = 'end'; - attrs.x = fullLayout._paper.attr('width') - 7; - } - - linkContainer.attr(attrs); - - var toolspan = linkContainer.select('.js-link-to-tool'), - spacespan = linkContainer.select('.js-link-spacer'), - sourcespan = linkContainer.select('.js-sourcelinks'); - - if(gd._context.showSources) gd._context.showSources(gd); - - // 'view in plotly' link for embedded plots - if(gd._context.showLink) positionPlayWithData(gd, toolspan); + var fullLayout = gd._fullLayout; + + var linkContainer = fullLayout._paper + .selectAll("text.js-plot-link-container") + .data([0]); + + linkContainer + .enter() + .append("text") + .classed("js-plot-link-container", true) + .style({ + "font-family": '"Open Sans", Arial, sans-serif', + "font-size": "12px", + fill: Color.defaultLine, + "pointer-events": "all" + }) + .each(function() { + var links = d3.select(this); + links.append("tspan").classed("js-link-to-tool", true); + links.append("tspan").classed("js-link-spacer", true); + links.append("tspan").classed("js-sourcelinks", true); + }); - // separator if we have both sources and tool link - spacespan.text((toolspan.text() && sourcespan.text()) ? ' - ' : ''); + // The text node inside svg + var text = linkContainer.node(), + attrs = { y: fullLayout._paper.attr("height") - 9 }; + + // If text's width is bigger than the layout + // Check that text is a child node or document.body + // because otherwise IE/Edge might throw an exception + // when calling getComputedTextLength(). + // Apparently offsetParent is null for invisibles. + if ( + document.body.contains(text) && + text.getComputedTextLength() >= fullLayout.width - 20 + ) { + // Align the text at the left + attrs["text-anchor"] = "start"; + attrs.x = 5; + } else { + // Align the text at the right + attrs["text-anchor"] = "end"; + attrs.x = fullLayout._paper.attr("width") - 7; + } + + linkContainer.attr(attrs); + + var toolspan = linkContainer.select(".js-link-to-tool"), + spacespan = linkContainer.select(".js-link-spacer"), + sourcespan = linkContainer.select(".js-sourcelinks"); + + if (gd._context.showSources) gd._context.showSources(gd); + + // 'view in plotly' link for embedded plots + if (gd._context.showLink) positionPlayWithData(gd, toolspan); + + // separator if we have both sources and tool link + spacespan.text(toolspan.text() && sourcespan.text() ? " - " : ""); }; // note that now this function is only adding the brand in // iframes and 3rd-party apps function positionPlayWithData(gd, container) { - container.text(''); - var link = container.append('a') - .attr({ - 'xlink:xlink:href': '#', - 'class': 'link--impt link--embedview', - 'font-weight': 'bold' - }) - .text(gd._context.linkText + ' ' + String.fromCharCode(187)); - - if(gd._context.sendData) { - link.on('click', function() { - plots.sendDataToCloud(gd); - }); - } - else { - var path = window.location.pathname.split('/'); - var query = window.location.search; - link.attr({ - 'xlink:xlink:show': 'new', - 'xlink:xlink:href': '/' + path[2].split('.')[0] + '/' + path[1] + query - }); - } + container.text(""); + var link = container + .append("a") + .attr({ + "xlink:xlink:href": "#", + class: "link--impt link--embedview", + "font-weight": "bold" + }) + .text(gd._context.linkText + " " + String.fromCharCode(187)); + + if (gd._context.sendData) { + link.on("click", function() { + plots.sendDataToCloud(gd); + }); + } else { + var path = window.location.pathname.split("/"); + var query = window.location.search; + link.attr({ + "xlink:xlink:show": "new", + "xlink:xlink:href": "/" + path[2].split(".")[0] + "/" + path[1] + query + }); + } } plots.sendDataToCloud = function(gd) { - gd.emit('plotly_beforeexport'); + gd.emit("plotly_beforeexport"); - var baseUrl = (window.PLOTLYENV && window.PLOTLYENV.BASE_URL) || 'https://plot.ly'; + var baseUrl = window.PLOTLYENV && window.PLOTLYENV.BASE_URL || + "https://plot.ly"; - var hiddenformDiv = d3.select(gd) - .append('div') - .attr('id', 'hiddenform') - .style('display', 'none'); + var hiddenformDiv = d3 + .select(gd) + .append("div") + .attr("id", "hiddenform") + .style("display", "none"); - var hiddenform = hiddenformDiv - .append('form') - .attr({ - action: baseUrl + '/external', - method: 'post', - target: '_blank' - }); + var hiddenform = hiddenformDiv + .append("form") + .attr({ action: baseUrl + "/external", method: "post", target: "_blank" }); - var hiddenformInput = hiddenform - .append('input') - .attr({ - type: 'text', - name: 'data' - }); + var hiddenformInput = hiddenform + .append("input") + .attr({ type: "text", name: "data" }); - hiddenformInput.node().value = plots.graphJson(gd, false, 'keepdata'); - hiddenform.node().submit(); - hiddenformDiv.remove(); + hiddenformInput.node().value = plots.graphJson(gd, false, "keepdata"); + hiddenform.node().submit(); + hiddenformDiv.remove(); - gd.emit('plotly_afterexport'); - return false; + gd.emit("plotly_afterexport"); + return false; }; // Fill in default values: @@ -393,203 +389,213 @@ plots.sendDataToCloud = function(gd) { // is a list of all the transform modules invoked. // plots.supplyDefaults = function(gd) { - var oldFullLayout = gd._fullLayout || {}, - newFullLayout = gd._fullLayout = {}, - newLayout = gd.layout || {}; - - var oldFullData = gd._fullData || [], - newFullData = gd._fullData = [], - newData = gd.data || []; - - var i; - - // Create all the storage space for frames, but only if doesn't already exist - if(!gd._transitionData) plots.createTransitionData(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 - var oldWidth = oldFullLayout.width, - oldHeight = oldFullLayout.height; - - plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout); - - if(!newLayout.width) newFullLayout.width = oldWidth; - if(!newLayout.height) newFullLayout.height = oldHeight; - } - else { - - // coerce the updated layout and autosize if needed - plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout); - - var missingWidthOrHeight = (!newLayout.width || !newLayout.height), - autosize = newFullLayout.autosize, - autosizable = gd._context && gd._context.autosizable, - initialAutoSize = missingWidthOrHeight && (autosize || autosizable); - - if(initialAutoSize) plots.plotAutoSize(gd, newLayout, newFullLayout); - else if(missingWidthOrHeight) plots.sanitizeMargins(gd); - - // for backwards-compatibility with Plotly v1.x.x - if(!autosize && missingWidthOrHeight) { - newLayout.width = newFullLayout.width; - newLayout.height = newFullLayout.height; - } - } - - newFullLayout._initialAutoSizeIsDone = true; - - // keep track of how many traces are inputted - newFullLayout._dataLength = newData.length; - - // then do the data - newFullLayout._globalTransforms = (gd._context || {}).globalTransforms; - plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout); - - // attach helper method to check whether a plot type is present on graph - newFullLayout._has = plots._hasPlotType.bind(newFullLayout); - - // special cases that introduce interactions between traces - var _modules = newFullLayout._modules; - for(i = 0; i < _modules.length; i++) { - var _module = _modules[i]; - if(_module.cleanData) _module.cleanData(newFullData); - } - - if(oldFullData.length === newData.length) { - for(i = 0; i < newFullData.length; i++) { - relinkPrivateKeys(newFullData[i], oldFullData[i]); - } - } - - // finally, fill in the pieces of layout that may need to look at data - plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData, gd._transitionData); - - // TODO remove in v2.0.0 - // add has-plot-type refs to fullLayout for backward compatibility - newFullLayout._hasCartesian = newFullLayout._has('cartesian'); - newFullLayout._hasGeo = newFullLayout._has('geo'); - newFullLayout._hasGL3D = newFullLayout._has('gl3d'); - newFullLayout._hasGL2D = newFullLayout._has('gl2d'); - newFullLayout._hasTernary = newFullLayout._has('ternary'); - newFullLayout._hasPie = newFullLayout._has('pie'); - - // clean subplots and other artifacts from previous plot calls - plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); - - // relink / initialize subplot axis objects - plots.linkSubplots(newFullData, newFullLayout, oldFullData, oldFullLayout); - - // relink functions and _ attributes to promote consistency between plots - relinkPrivateKeys(newFullLayout, oldFullLayout); - - // TODO may return a promise - plots.doAutoMargin(gd); - - // can't quite figure out how to get rid of this... each axis needs - // a reference back to the DOM object for just a few purposes - var axList = Plotly.Axes.list(gd); - for(i = 0; i < axList.length; i++) { - var ax = axList[i]; - ax._gd = gd; - ax.setScale(); - } - - // update object references in calcdata - if((gd.calcdata || []).length === newFullData.length) { - for(i = 0; i < newFullData.length; i++) { - var trace = newFullData[i]; - (gd.calcdata[i][0] || {}).trace = trace; - } - } + var oldFullLayout = gd._fullLayout || {}, + newFullLayout = gd._fullLayout = {}, + newLayout = gd.layout || {}; + + var oldFullData = gd._fullData || [], + newFullData = gd._fullData = [], + newData = gd.data || []; + + var i; + + // Create all the storage space for frames, but only if doesn't already exist + if (!gd._transitionData) plots.createTransitionData(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 + var oldWidth = oldFullLayout.width, oldHeight = oldFullLayout.height; + + plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout); + + if (!newLayout.width) newFullLayout.width = oldWidth; + if (!newLayout.height) newFullLayout.height = oldHeight; + } else { + // coerce the updated layout and autosize if needed + plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout); + + var missingWidthOrHeight = !newLayout.width || !newLayout.height, + autosize = newFullLayout.autosize, + autosizable = gd._context && gd._context.autosizable, + initialAutoSize = missingWidthOrHeight && (autosize || autosizable); + + if (initialAutoSize) plots.plotAutoSize(gd, newLayout, newFullLayout); + else if (missingWidthOrHeight) plots.sanitizeMargins(gd); + + // for backwards-compatibility with Plotly v1.x.x + if (!autosize && missingWidthOrHeight) { + newLayout.width = newFullLayout.width; + newLayout.height = newFullLayout.height; + } + } + + newFullLayout._initialAutoSizeIsDone = true; + + // keep track of how many traces are inputted + newFullLayout._dataLength = newData.length; + + // then do the data + newFullLayout._globalTransforms = (gd._context || {}).globalTransforms; + plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout); + + // attach helper method to check whether a plot type is present on graph + newFullLayout._has = plots._hasPlotType.bind(newFullLayout); + + // special cases that introduce interactions between traces + var _modules = newFullLayout._modules; + for (i = 0; i < _modules.length; i++) { + var _module = _modules[i]; + if (_module.cleanData) _module.cleanData(newFullData); + } + + if (oldFullData.length === newData.length) { + for (i = 0; i < newFullData.length; i++) { + relinkPrivateKeys(newFullData[i], oldFullData[i]); + } + } + + // finally, fill in the pieces of layout that may need to look at data + plots.supplyLayoutModuleDefaults( + newLayout, + newFullLayout, + newFullData, + gd._transitionData + ); + + // TODO remove in v2.0.0 + // add has-plot-type refs to fullLayout for backward compatibility + newFullLayout._hasCartesian = newFullLayout._has("cartesian"); + newFullLayout._hasGeo = newFullLayout._has("geo"); + newFullLayout._hasGL3D = newFullLayout._has("gl3d"); + newFullLayout._hasGL2D = newFullLayout._has("gl2d"); + newFullLayout._hasTernary = newFullLayout._has("ternary"); + newFullLayout._hasPie = newFullLayout._has("pie"); + + // clean subplots and other artifacts from previous plot calls + plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); + + // relink / initialize subplot axis objects + plots.linkSubplots(newFullData, newFullLayout, oldFullData, oldFullLayout); + + // relink functions and _ attributes to promote consistency between plots + relinkPrivateKeys(newFullLayout, oldFullLayout); + + // TODO may return a promise + plots.doAutoMargin(gd); + + // can't quite figure out how to get rid of this... each axis needs + // a reference back to the DOM object for just a few purposes + var axList = Plotly.Axes.list(gd); + for (i = 0; i < axList.length; i++) { + var ax = axList[i]; + ax._gd = gd; + ax.setScale(); + } + + // update object references in calcdata + if ((gd.calcdata || []).length === newFullData.length) { + for (i = 0; i < newFullData.length; i++) { + var trace = newFullData[i]; + (gd.calcdata[i][0] || {}).trace = trace; + } + } }; // Create storage for all of the data related to frames and transitions: plots.createTransitionData = function(gd) { - // Set up the default keyframe if it doesn't exist: - if(!gd._transitionData) { - gd._transitionData = {}; - } - - if(!gd._transitionData._frames) { - gd._transitionData._frames = []; - } - - if(!gd._transitionData._frameHash) { - gd._transitionData._frameHash = {}; - } - - if(!gd._transitionData._counter) { - gd._transitionData._counter = 0; - } - - if(!gd._transitionData._interruptCallbacks) { - gd._transitionData._interruptCallbacks = []; - } + // Set up the default keyframe if it doesn't exist: + if (!gd._transitionData) { + gd._transitionData = {}; + } + + if (!gd._transitionData._frames) { + gd._transitionData._frames = []; + } + + if (!gd._transitionData._frameHash) { + gd._transitionData._frameHash = {}; + } + + if (!gd._transitionData._counter) { + gd._transitionData._counter = 0; + } + + if (!gd._transitionData._interruptCallbacks) { + gd._transitionData._interruptCallbacks = []; + } }; // helper function to be bound to fullLayout to check // whether a certain plot type is present on plot plots._hasPlotType = function(category) { - var basePlotModules = this._basePlotModules || []; + var basePlotModules = this._basePlotModules || []; - for(var i = 0; i < basePlotModules.length; i++) { - var _module = basePlotModules[i]; + for (var i = 0; i < basePlotModules.length; i++) { + var _module = basePlotModules[i]; - if(_module.name === category) return true; - } + if (_module.name === category) return true; + } - return false; + return false; }; -plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var i, j; +plots.cleanPlot = function( + newFullData, + newFullLayout, + oldFullData, + oldFullLayout +) { + var i, j; - var basePlotModules = oldFullLayout._basePlotModules || []; - for(i = 0; i < basePlotModules.length; i++) { - var _module = basePlotModules[i]; + var basePlotModules = oldFullLayout._basePlotModules || []; + for (i = 0; i < basePlotModules.length; i++) { + var _module = basePlotModules[i]; - if(_module.clean) { - _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout); - } + if (_module.clean) { + _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout); } + } - var hasPaper = !!oldFullLayout._paper; - var hasInfoLayer = !!oldFullLayout._infolayer; + var hasPaper = !!oldFullLayout._paper; + var hasInfoLayer = !!oldFullLayout._infolayer; - oldLoop: - for(i = 0; i < oldFullData.length; i++) { - var oldTrace = oldFullData[i], - oldUid = oldTrace.uid; + oldLoop: + for (i = 0; i < oldFullData.length; i++) { + var oldTrace = oldFullData[i], oldUid = oldTrace.uid; - for(j = 0; j < newFullData.length; j++) { - var newTrace = newFullData[j]; + for (j = 0; j < newFullData.length; j++) { + var newTrace = newFullData[j]; - if(oldUid === newTrace.uid) continue oldLoop; - } - - // clean old heatmap, contour, and scatter traces - // - // Note: This is also how scatter traces (cartesian and scatterternary) get - // removed since otherwise the scatter module is not called (and so the join - // doesn't register the removal) if scatter traces disappear entirely. - if(hasPaper) { - oldFullLayout._paper.selectAll( - '.hm' + oldUid + - ',.contour' + oldUid + - ',#clip' + oldUid + - ',.trace' + oldUid - ).remove(); - } - - // clean old colorbars - if(hasInfoLayer) { - oldFullLayout._infolayer.selectAll('.cb' + oldUid).remove(); - } + if (oldUid === newTrace.uid) continue oldLoop; } + + // clean old heatmap, contour, and scatter traces + // + // Note: This is also how scatter traces (cartesian and scatterternary) get + // removed since otherwise the scatter module is not called (and so the join + // doesn't register the removal) if scatter traces disappear entirely. + if (hasPaper) { + oldFullLayout._paper + .selectAll( + ".hm" + + oldUid + + ",.contour" + + oldUid + + ",#clip" + + oldUid + + ",.trace" + + oldUid + ) + .remove(); + } + + // clean old colorbars + if (hasInfoLayer) { + oldFullLayout._infolayer.selectAll(".cb" + oldUid).remove(); + } + } }; /** @@ -600,449 +606,475 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou * This prevents deepCopying massive structures like a webgl context. */ function relinkPrivateKeys(toContainer, fromContainer) { - var isPlainObject = Lib.isPlainObject, - isArray = Array.isArray; - - var keys = Object.keys(fromContainer || {}); - - for(var i = 0; i < keys.length; i++) { - var k = keys[i], - fromVal = fromContainer[k], - toVal = toContainer[k]; - - if(k.charAt(0) === '_' || typeof fromVal === 'function') { - - // if it already exists at this point, it's something - // that we recreate each time around, so ignore it - if(k in toContainer) continue; - - toContainer[k] = fromVal; - } - else if(isArray(fromVal) && isArray(toVal) && isPlainObject(fromVal[0])) { - - // recurse into arrays containers - for(var j = 0; j < fromVal.length; j++) { - if(isPlainObject(fromVal[j]) && isPlainObject(toVal[j])) { - relinkPrivateKeys(toVal[j], fromVal[j]); - } - } + var isPlainObject = Lib.isPlainObject, isArray = Array.isArray; + + var keys = Object.keys(fromContainer || {}); + + for (var i = 0; i < keys.length; i++) { + var k = keys[i], fromVal = fromContainer[k], toVal = toContainer[k]; + + if (k.charAt(0) === "_" || typeof fromVal === "function") { + // if it already exists at this point, it's something + // that we recreate each time around, so ignore it + if (k in toContainer) continue; + + toContainer[k] = fromVal; + } else if ( + isArray(fromVal) && isArray(toVal) && isPlainObject(fromVal[0]) + ) { + // recurse into arrays containers + for (var j = 0; j < fromVal.length; j++) { + if (isPlainObject(fromVal[j]) && isPlainObject(toVal[j])) { + relinkPrivateKeys(toVal[j], fromVal[j]); } - else if(isPlainObject(fromVal) && isPlainObject(toVal)) { - - // recurse into objects, but only if they still exist - relinkPrivateKeys(toVal, fromVal); + } + } else if (isPlainObject(fromVal) && isPlainObject(toVal)) { + // recurse into objects, but only if they still exist + relinkPrivateKeys(toVal, fromVal); - if(!Object.keys(toVal).length) delete toContainer[k]; - } + if (!Object.keys(toVal).length) delete toContainer[k]; } + } } -plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldSubplots = oldFullLayout._plots || {}, - newSubplots = newFullLayout._plots = {}; +plots.linkSubplots = function( + newFullData, + newFullLayout, + oldFullData, + oldFullLayout +) { + var oldSubplots = oldFullLayout._plots || {}, + newSubplots = newFullLayout._plots = {}; - var mockGd = { - _fullData: newFullData, - _fullLayout: newFullLayout - }; - - var ids = Plotly.Axes.getSubplots(mockGd); + var mockGd = { _fullData: newFullData, _fullLayout: newFullLayout }; - for(var i = 0; i < ids.length; i++) { - var id = ids[i], - oldSubplot = oldSubplots[id], - plotinfo; + var ids = Plotly.Axes.getSubplots(mockGd); - if(oldSubplot) { - plotinfo = newSubplots[id] = oldSubplot; + for (var i = 0; i < ids.length; i++) { + var id = ids[i], oldSubplot = oldSubplots[id], plotinfo; - if(plotinfo._scene2d) { - plotinfo._scene2d.updateRefs(newFullLayout); - } - } - else { - plotinfo = newSubplots[id] = {}; - plotinfo.id = id; - } + if (oldSubplot) { + plotinfo = newSubplots[id] = oldSubplot; - plotinfo.xaxis = Plotly.Axes.getFromId(mockGd, id, 'x'); - plotinfo.yaxis = Plotly.Axes.getFromId(mockGd, id, 'y'); + if (plotinfo._scene2d) { + plotinfo._scene2d.updateRefs(newFullLayout); + } + } else { + plotinfo = newSubplots[id] = {}; + plotinfo.id = id; } + + plotinfo.xaxis = Plotly.Axes.getFromId(mockGd, id, "x"); + plotinfo.yaxis = Plotly.Axes.getFromId(mockGd, id, "y"); + } }; plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { - var modules = fullLayout._modules = [], - basePlotModules = fullLayout._basePlotModules = [], - cnt = 0; - - fullLayout._transformModules = []; - - function pushModule(fullTrace) { - dataOut.push(fullTrace); - - var _module = fullTrace._module; - if(!_module) return; - - Lib.pushUnique(modules, _module); - Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); - - cnt++; - } - - for(var i = 0; i < dataIn.length; i++) { - var trace = dataIn[i], - fullTrace = plots.supplyTraceDefaults(trace, cnt, fullLayout, i); - - fullTrace.index = i; - fullTrace._input = trace; - fullTrace._expandedIndex = cnt; - - if(fullTrace.transforms && fullTrace.transforms.length) { - var expandedTraces = applyTransforms(fullTrace, dataOut, layout, fullLayout); - - for(var j = 0; j < expandedTraces.length; j++) { - var expandedTrace = expandedTraces[j], - fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i); - - // mutate uid here using parent uid and expanded index - // to promote consistency between update calls - expandedTrace.uid = fullExpandedTrace.uid = fullTrace.uid + j; - - // add info about parent data trace - fullExpandedTrace.index = i; - fullExpandedTrace._input = trace; - fullExpandedTrace._fullInput = fullTrace; - - // add info about the expanded data - fullExpandedTrace._expandedIndex = cnt; - fullExpandedTrace._expandedInput = expandedTrace; - - pushModule(fullExpandedTrace); - } - } - else { - - // add identify refs for consistency with transformed traces - fullTrace._fullInput = fullTrace; - fullTrace._expandedInput = fullTrace; + var modules = fullLayout._modules = [], + basePlotModules = fullLayout._basePlotModules = [], + cnt = 0; + + fullLayout._transformModules = []; + + function pushModule(fullTrace) { + dataOut.push(fullTrace); + + var _module = fullTrace._module; + if (!_module) return; + + Lib.pushUnique(modules, _module); + Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); + + cnt++; + } + + for (var i = 0; i < dataIn.length; i++) { + var trace = dataIn[i], + fullTrace = plots.supplyTraceDefaults(trace, cnt, fullLayout, i); + + fullTrace.index = i; + fullTrace._input = trace; + fullTrace._expandedIndex = cnt; + + if (fullTrace.transforms && fullTrace.transforms.length) { + var expandedTraces = applyTransforms( + fullTrace, + dataOut, + layout, + fullLayout + ); + + for (var j = 0; j < expandedTraces.length; j++) { + var expandedTrace = expandedTraces[j], + fullExpandedTrace = plots.supplyTraceDefaults( + expandedTrace, + cnt, + fullLayout, + i + ); + + // mutate uid here using parent uid and expanded index + // to promote consistency between update calls + expandedTrace.uid = fullExpandedTrace.uid = fullTrace.uid + j; + + // add info about parent data trace + fullExpandedTrace.index = i; + fullExpandedTrace._input = trace; + fullExpandedTrace._fullInput = fullTrace; + + // add info about the expanded data + fullExpandedTrace._expandedIndex = cnt; + fullExpandedTrace._expandedInput = expandedTrace; + + pushModule(fullExpandedTrace); + } + } else { + // add identify refs for consistency with transformed traces + fullTrace._fullInput = fullTrace; + fullTrace._expandedInput = fullTrace; - pushModule(fullTrace); - } + pushModule(fullTrace); } + } }; plots.supplyAnimationDefaults = function(opts) { - opts = opts || {}; - var i; - var optsOut = {}; - - function coerce(attr, dflt) { - return Lib.coerce(opts || {}, optsOut, animationAttrs, attr, dflt); - } - - coerce('mode'); - coerce('direction'); - coerce('fromcurrent'); - - if(Array.isArray(opts.frame)) { - optsOut.frame = []; - for(i = 0; i < opts.frame.length; i++) { - optsOut.frame[i] = plots.supplyAnimationFrameDefaults(opts.frame[i] || {}); - } - } else { - optsOut.frame = plots.supplyAnimationFrameDefaults(opts.frame || {}); - } - - if(Array.isArray(opts.transition)) { - optsOut.transition = []; - for(i = 0; i < opts.transition.length; i++) { - optsOut.transition[i] = plots.supplyAnimationTransitionDefaults(opts.transition[i] || {}); - } - } else { - optsOut.transition = plots.supplyAnimationTransitionDefaults(opts.transition || {}); - } - - return optsOut; + opts = opts || {}; + var i; + var optsOut = {}; + + function coerce(attr, dflt) { + return Lib.coerce(opts || {}, optsOut, animationAttrs, attr, dflt); + } + + coerce("mode"); + coerce("direction"); + coerce("fromcurrent"); + + if (Array.isArray(opts.frame)) { + optsOut.frame = []; + for (i = 0; i < opts.frame.length; i++) { + optsOut.frame[i] = plots.supplyAnimationFrameDefaults( + opts.frame[i] || {} + ); + } + } else { + optsOut.frame = plots.supplyAnimationFrameDefaults(opts.frame || {}); + } + + if (Array.isArray(opts.transition)) { + optsOut.transition = []; + for (i = 0; i < opts.transition.length; i++) { + optsOut.transition[i] = plots.supplyAnimationTransitionDefaults( + opts.transition[i] || {} + ); + } + } else { + optsOut.transition = plots.supplyAnimationTransitionDefaults( + opts.transition || {} + ); + } + + return optsOut; }; plots.supplyAnimationFrameDefaults = function(opts) { - var optsOut = {}; + var optsOut = {}; - function coerce(attr, dflt) { - return Lib.coerce(opts || {}, optsOut, animationAttrs.frame, attr, dflt); - } + function coerce(attr, dflt) { + return Lib.coerce(opts || {}, optsOut, animationAttrs.frame, attr, dflt); + } - coerce('duration'); - coerce('redraw'); + coerce("duration"); + coerce("redraw"); - return optsOut; + return optsOut; }; plots.supplyAnimationTransitionDefaults = function(opts) { - var optsOut = {}; - - function coerce(attr, dflt) { - return Lib.coerce(opts || {}, optsOut, animationAttrs.transition, attr, dflt); - } - - coerce('duration'); - coerce('easing'); - - return optsOut; + var optsOut = {}; + + function coerce(attr, dflt) { + return Lib.coerce( + opts || {}, + optsOut, + animationAttrs.transition, + attr, + dflt + ); + } + + coerce("duration"); + coerce("easing"); + + return optsOut; }; plots.supplyFrameDefaults = function(frameIn) { - var frameOut = {}; + var frameOut = {}; - function coerce(attr, dflt) { - return Lib.coerce(frameIn, frameOut, frameAttrs, attr, dflt); - } + function coerce(attr, dflt) { + return Lib.coerce(frameIn, frameOut, frameAttrs, attr, dflt); + } - coerce('group'); - coerce('name'); - coerce('traces'); - coerce('baseframe'); - coerce('data'); - coerce('layout'); + coerce("group"); + coerce("name"); + coerce("traces"); + coerce("baseframe"); + coerce("data"); + coerce("layout"); - return frameOut; + return frameOut; }; -plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInIndex) { - var traceOut = {}, - defaultColor = Color.defaults[traceOutIndex % Color.defaults.length]; +plots.supplyTraceDefaults = function( + traceIn, + traceOutIndex, + layout, + traceInIndex +) { + var traceOut = {}, + defaultColor = Color.defaults[traceOutIndex % Color.defaults.length]; - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, plots.attributes, attr, dflt); - } + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, plots.attributes, attr, dflt); + } - function coerceSubplotAttr(subplotType, subplotAttr) { - if(!plots.traceIs(traceOut, subplotType)) return; + function coerceSubplotAttr(subplotType, subplotAttr) { + if (!plots.traceIs(traceOut, subplotType)) return; - return Lib.coerce(traceIn, traceOut, - plots.subplotsRegistry[subplotType].attributes, subplotAttr); - } + return Lib.coerce( + traceIn, + traceOut, + plots.subplotsRegistry[subplotType].attributes, + subplotAttr + ); + } - var visible = coerce('visible'); + var visible = coerce("visible"); - coerce('type'); - coerce('uid'); - coerce('name', 'trace ' + traceInIndex); + coerce("type"); + coerce("uid"); + coerce("name", "trace " + traceInIndex); - // coerce subplot attributes of all registered subplot types - var subplotTypes = Object.keys(subplotsRegistry); - for(var i = 0; i < subplotTypes.length; i++) { - var subplotType = subplotTypes[i]; + // coerce subplot attributes of all registered subplot types + var subplotTypes = Object.keys(subplotsRegistry); + for (var i = 0; i < subplotTypes.length; i++) { + var subplotType = subplotTypes[i]; - // done below (only when visible is true) - // TODO unified this pattern - if(['cartesian', 'gl2d'].indexOf(subplotType) !== -1) continue; + // done below (only when visible is true) + // TODO unified this pattern + if (["cartesian", "gl2d"].indexOf(subplotType) !== -1) continue; - var attr = subplotsRegistry[subplotType].attr; - - if(attr) coerceSubplotAttr(subplotType, attr); - } + var attr = subplotsRegistry[subplotType].attr; - if(visible) { - var _module = plots.getModule(traceOut); - traceOut._module = _module; + if (attr) coerceSubplotAttr(subplotType, attr); + } - // gets overwritten in pie, geo and ternary modules - coerce('hoverinfo', (layout._dataLength === 1) ? 'x+y+z+text' : undefined); + if (visible) { + var _module = plots.getModule(traceOut); + traceOut._module = _module; - // TODO add per-base-plot-module trace defaults step + // gets overwritten in pie, geo and ternary modules + coerce("hoverinfo", layout._dataLength === 1 ? "x+y+z+text" : undefined); - if(_module) _module.supplyDefaults(traceIn, traceOut, defaultColor, layout); - - if(!plots.traceIs(traceOut, 'noOpacity')) coerce('opacity'); + // TODO add per-base-plot-module trace defaults step + if (_module) { + _module.supplyDefaults(traceIn, traceOut, defaultColor, layout); + } - coerceSubplotAttr('cartesian', 'xaxis'); - coerceSubplotAttr('cartesian', 'yaxis'); + if (!plots.traceIs(traceOut, "noOpacity")) coerce("opacity"); - coerceSubplotAttr('gl2d', 'xaxis'); - coerceSubplotAttr('gl2d', 'yaxis'); + coerceSubplotAttr("cartesian", "xaxis"); + coerceSubplotAttr("cartesian", "yaxis"); - if(plots.traceIs(traceOut, 'showLegend')) { - coerce('showlegend'); - coerce('legendgroup'); - } + coerceSubplotAttr("gl2d", "xaxis"); + coerceSubplotAttr("gl2d", "yaxis"); - supplyTransformDefaults(traceIn, traceOut, layout); + if (plots.traceIs(traceOut, "showLegend")) { + coerce("showlegend"); + coerce("legendgroup"); } - return traceOut; + supplyTransformDefaults(traceIn, traceOut, layout); + } + + return traceOut; }; function supplyTransformDefaults(traceIn, traceOut, layout) { - var globalTransforms = layout._globalTransforms || []; - - if(!Array.isArray(traceIn.transforms) && globalTransforms.length === 0) return; - - var containerIn = traceIn.transforms || [], - transformList = globalTransforms.concat(containerIn), - containerOut = traceOut.transforms = []; - - for(var i = 0; i < transformList.length; i++) { - var transformIn = transformList[i], - type = transformIn.type, - _module = transformsRegistry[type], - transformOut; - - if(!_module) Lib.warn('Unrecognized transform type ' + type + '.'); - - if(_module && _module.supplyDefaults) { - transformOut = _module.supplyDefaults(transformIn, traceOut, layout, traceIn); - transformOut.type = type; - transformOut._module = _module; - - Lib.pushUnique(layout._transformModules, _module); - } - else { - transformOut = Lib.extendFlat({}, transformIn); - } - - containerOut.push(transformOut); + var globalTransforms = layout._globalTransforms || []; + + if (!Array.isArray(traceIn.transforms) && globalTransforms.length === 0) { + return; + } + + var containerIn = traceIn.transforms || [], + transformList = globalTransforms.concat(containerIn), + containerOut = traceOut.transforms = []; + + for (var i = 0; i < transformList.length; i++) { + var transformIn = transformList[i], + type = transformIn.type, + _module = transformsRegistry[type], + transformOut; + + if (!_module) Lib.warn("Unrecognized transform type " + type + "."); + + if (_module && _module.supplyDefaults) { + transformOut = _module.supplyDefaults( + transformIn, + traceOut, + layout, + traceIn + ); + transformOut.type = type; + transformOut._module = _module; + + Lib.pushUnique(layout._transformModules, _module); + } else { + transformOut = Lib.extendFlat({}, transformIn); } + + containerOut.push(transformOut); + } } function applyTransforms(fullTrace, fullData, layout, fullLayout) { - var container = fullTrace.transforms, - dataOut = [fullTrace]; - - for(var i = 0; i < container.length; i++) { - var transform = container[i], - _module = transformsRegistry[transform.type]; - - if(_module && _module.transform) { - dataOut = _module.transform(dataOut, { - transform: transform, - fullTrace: fullTrace, - fullData: fullData, - layout: layout, - fullLayout: fullLayout, - transformIndex: i - }); - } - } + var container = fullTrace.transforms, dataOut = [fullTrace]; - return dataOut; -} + for (var i = 0; i < container.length; i++) { + var transform = container[i], _module = transformsRegistry[transform.type]; -plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { - function coerce(attr, dflt) { - return Lib.coerce(layoutIn, layoutOut, plots.layoutAttributes, attr, dflt); + if (_module && _module.transform) { + dataOut = _module.transform(dataOut, { + transform: transform, + fullTrace: fullTrace, + fullData: fullData, + layout: layout, + fullLayout: fullLayout, + transformIndex: i + }); } + } - var globalFont = Lib.coerceFont(coerce, 'font'); - - coerce('title'); - - Lib.coerceFont(coerce, 'titlefont', { - family: globalFont.family, - size: Math.round(globalFont.size * 1.4), - color: globalFont.color - }); - - // Make sure that autosize is defaulted to *true* - // on layouts with no set width and height for backward compatibly, - // in particular https://plot.ly/javascript/responsive-fluid-layout/ - // - // Before https://github.com/plotly/plotly.js/pull/635 , - // layouts with no set width and height were set temporary set to 'initial' - // to pass through the autosize routine - // - // This behavior is subject to change in v2. - coerce('autosize', !(layoutIn.width && layoutIn.height)); - - coerce('width'); - coerce('height'); - coerce('margin.l'); - coerce('margin.r'); - coerce('margin.t'); - coerce('margin.b'); - coerce('margin.pad'); - coerce('margin.autoexpand'); - - if(layoutIn.width && layoutIn.height) plots.sanitizeMargins(layoutOut); - - coerce('paper_bgcolor'); - - coerce('separators'); - coerce('hidesources'); - coerce('smith'); + return dataOut; +} - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults'); - handleCalendarDefaults(layoutIn, layoutOut, 'calendar'); +plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { + function coerce(attr, dflt) { + return Lib.coerce(layoutIn, layoutOut, plots.layoutAttributes, attr, dflt); + } + + var globalFont = Lib.coerceFont(coerce, "font"); + + coerce("title"); + + Lib.coerceFont(coerce, "titlefont", { + family: globalFont.family, + size: Math.round(globalFont.size * 1.4), + color: globalFont.color + }); + + // Make sure that autosize is defaulted to *true* + // on layouts with no set width and height for backward compatibly, + // in particular https://plot.ly/javascript/responsive-fluid-layout/ + // + // Before https://github.com/plotly/plotly.js/pull/635 , + // layouts with no set width and height were set temporary set to 'initial' + // to pass through the autosize routine + // + // This behavior is subject to change in v2. + coerce("autosize", !(layoutIn.width && layoutIn.height)); + + coerce("width"); + coerce("height"); + coerce("margin.l"); + coerce("margin.r"); + coerce("margin.t"); + coerce("margin.b"); + coerce("margin.pad"); + coerce("margin.autoexpand"); + + if (layoutIn.width && layoutIn.height) plots.sanitizeMargins(layoutOut); + + coerce("paper_bgcolor"); + + coerce("separators"); + coerce("hidesources"); + coerce("smith"); + + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleDefaults" + ); + handleCalendarDefaults(layoutIn, layoutOut, "calendar"); }; plots.plotAutoSize = function plotAutoSize(gd, layout, fullLayout) { - var context = gd._context || {}, - frameMargins = context.frameMargins, - newWidth, - newHeight; - - var isPlotDiv = Lib.isPlotDiv(gd); - - if(isPlotDiv) gd.emit('plotly_autosize'); - - // embedded in an iframe - just take the full iframe size - // if we get to this point, with no aspect ratio restrictions - if(context.fillFrame) { - newWidth = window.innerWidth; - newHeight = window.innerHeight; - - // somehow we get a few extra px height sometimes... - // just hide it - document.body.style.overflow = 'hidden'; - } - else if(isNumeric(frameMargins) && frameMargins > 0) { - var reservedMargins = calculateReservedMargins(gd._boundingBoxMargins), - reservedWidth = reservedMargins.left + reservedMargins.right, - reservedHeight = reservedMargins.bottom + reservedMargins.top, - factor = 1 - 2 * frameMargins; - - var gdBB = fullLayout._container && fullLayout._container.node ? - fullLayout._container.node().getBoundingClientRect() : { - width: fullLayout.width, - height: fullLayout.height - }; - - newWidth = Math.round(factor * (gdBB.width - reservedWidth)); - newHeight = Math.round(factor * (gdBB.height - reservedHeight)); - } - else { - // plotly.js - let the developers do what they want, either - // provide height and width for the container div, - // specify size in layout, or take the defaults, - // but don't enforce any ratio restrictions - var computedStyle = isPlotDiv ? window.getComputedStyle(gd) : {}; - - newWidth = parseFloat(computedStyle.width) || fullLayout.width; - newHeight = parseFloat(computedStyle.height) || fullLayout.height; - } - - var minWidth = plots.layoutAttributes.width.min, - minHeight = plots.layoutAttributes.height.min; - if(newWidth < minWidth) newWidth = minWidth; - if(newHeight < minHeight) newHeight = minHeight; - - var widthHasChanged = !layout.width && - (Math.abs(fullLayout.width - newWidth) > 1), - heightHasChanged = !layout.height && - (Math.abs(fullLayout.height - newHeight) > 1); - - if(heightHasChanged || widthHasChanged) { - if(widthHasChanged) fullLayout.width = newWidth; - if(heightHasChanged) fullLayout.height = newHeight; - } - - // cache initial autosize value, used in relayout when - // width or height values are set to null - if(!gd._initialAutoSize) { - gd._initialAutoSize = { width: newWidth, height: newHeight }; - } - - plots.sanitizeMargins(fullLayout); + var context = gd._context || {}, + frameMargins = context.frameMargins, + newWidth, + newHeight; + + var isPlotDiv = Lib.isPlotDiv(gd); + + if (isPlotDiv) gd.emit("plotly_autosize"); + + // embedded in an iframe - just take the full iframe size + // if we get to this point, with no aspect ratio restrictions + if (context.fillFrame) { + newWidth = window.innerWidth; + newHeight = window.innerHeight; + + // somehow we get a few extra px height sometimes... + // just hide it + document.body.style.overflow = "hidden"; + } else if (isNumeric(frameMargins) && frameMargins > 0) { + var reservedMargins = calculateReservedMargins(gd._boundingBoxMargins), + reservedWidth = reservedMargins.left + reservedMargins.right, + reservedHeight = reservedMargins.bottom + reservedMargins.top, + factor = 1 - 2 * frameMargins; + + var gdBB = fullLayout._container && fullLayout._container.node + ? fullLayout._container.node().getBoundingClientRect() + : { width: fullLayout.width, height: fullLayout.height }; + + newWidth = Math.round(factor * (gdBB.width - reservedWidth)); + newHeight = Math.round(factor * (gdBB.height - reservedHeight)); + } else { + // plotly.js - let the developers do what they want, either + // provide height and width for the container div, + // specify size in layout, or take the defaults, + // but don't enforce any ratio restrictions + var computedStyle = isPlotDiv ? window.getComputedStyle(gd) : {}; + + newWidth = parseFloat(computedStyle.width) || fullLayout.width; + newHeight = parseFloat(computedStyle.height) || fullLayout.height; + } + + var minWidth = plots.layoutAttributes.width.min, + minHeight = plots.layoutAttributes.height.min; + if (newWidth < minWidth) newWidth = minWidth; + if (newHeight < minHeight) newHeight = minHeight; + + var widthHasChanged = !layout.width && + Math.abs(fullLayout.width - newWidth) > 1, + heightHasChanged = !layout.height && + Math.abs(fullLayout.height - newHeight) > 1; + + if (heightHasChanged || widthHasChanged) { + if (widthHasChanged) fullLayout.width = newWidth; + if (heightHasChanged) fullLayout.height = newHeight; + } + + // cache initial autosize value, used in relayout when + // width or height values are set to null + if (!gd._initialAutoSize) { + gd._initialAutoSize = { width: newWidth, height: newHeight }; + } + + plots.sanitizeMargins(fullLayout); }; /** @@ -1052,175 +1084,183 @@ plots.plotAutoSize = function plotAutoSize(gd, layout, fullLayout) { * @returns {{left: number, right: number, bottom: number, top: number}} */ function calculateReservedMargins(margins) { - var resultingMargin = {left: 0, right: 0, bottom: 0, top: 0}, - marginName; - - if(margins) { - for(marginName in margins) { - if(margins.hasOwnProperty(marginName)) { - resultingMargin.left += margins[marginName].left || 0; - resultingMargin.right += margins[marginName].right || 0; - resultingMargin.bottom += margins[marginName].bottom || 0; - resultingMargin.top += margins[marginName].top || 0; - } - } - } - return resultingMargin; + var resultingMargin = { left: 0, right: 0, bottom: 0, top: 0 }, marginName; + + if (margins) { + for (marginName in margins) { + if (margins.hasOwnProperty(marginName)) { + resultingMargin.left += margins[marginName].left || 0; + resultingMargin.right += margins[marginName].right || 0; + resultingMargin.bottom += margins[marginName].bottom || 0; + resultingMargin.top += margins[marginName].top || 0; + } + } + } + return resultingMargin; } -plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, transitionData) { - var i, _module; +plots.supplyLayoutModuleDefaults = function( + layoutIn, + layoutOut, + fullData, + transitionData +) { + var i, _module; - // can't be be part of basePlotModules loop - // in order to handle the orphan axes case - Plotly.Axes.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + // can't be be part of basePlotModules loop + // in order to handle the orphan axes case + Plotly.Axes.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - // base plot module layout defaults - var basePlotModules = layoutOut._basePlotModules; - for(i = 0; i < basePlotModules.length; i++) { - _module = basePlotModules[i]; + // base plot module layout defaults + var basePlotModules = layoutOut._basePlotModules; + for (i = 0; i < basePlotModules.length; i++) { + _module = basePlotModules[i]; - // done above already - if(_module.name === 'cartesian') continue; + // done above already + if (_module.name === "cartesian") continue; - // e.g. gl2d does not have a layout-defaults step - if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - } + // e.g. gl2d does not have a layout-defaults step + if (_module.supplyLayoutDefaults) { + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); } + } - // trace module layout defaults - var modules = layoutOut._modules; - for(i = 0; i < modules.length; i++) { - _module = modules[i]; + // trace module layout defaults + var modules = layoutOut._modules; + for (i = 0; i < modules.length; i++) { + _module = modules[i]; - if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - } + if (_module.supplyLayoutDefaults) { + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); } + } - // transform module layout defaults - var transformModules = layoutOut._transformModules; - for(i = 0; i < transformModules.length; i++) { - _module = transformModules[i]; + // transform module layout defaults + var transformModules = layoutOut._transformModules; + for (i = 0; i < transformModules.length; i++) { + _module = transformModules[i]; - if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); - } + if (_module.supplyLayoutDefaults) { + _module.supplyLayoutDefaults( + layoutIn, + layoutOut, + fullData, + transitionData + ); } + } - // should FX be a component? - Plotly.Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + // should FX be a component? + Plotly.Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - var components = Object.keys(Registry.componentsRegistry); - for(i = 0; i < components.length; i++) { - _module = Registry.componentsRegistry[components[i]]; + var components = Object.keys(Registry.componentsRegistry); + for (i = 0; i < components.length; i++) { + _module = Registry.componentsRegistry[components[i]]; - if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - } + if (_module.supplyLayoutDefaults) { + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); } + } }; // Remove all plotly attributes from a div so it can be replotted fresh // TODO: these really need to be encapsulated into a much smaller set... plots.purge = function(gd) { - - // note: we DO NOT remove _context because it doesn't change when we insert - // a new plot, and may have been set outside of our scope. - - var fullLayout = gd._fullLayout || {}; - if(fullLayout._glcontainer !== undefined) fullLayout._glcontainer.remove(); - if(fullLayout._geocontainer !== undefined) fullLayout._geocontainer.remove(); - - // remove modebar - if(fullLayout._modeBar) fullLayout._modeBar.destroy(); - - if(gd._transitionData) { - // Ensure any dangling callbacks are simply dropped if the plot is purged. - // This is more or less only actually important for testing. - if(gd._transitionData._interruptCallbacks) { - gd._transitionData._interruptCallbacks.length = 0; - } - - if(gd._transitionData._animationRaf) { - window.cancelAnimationFrame(gd._transitionData._animationRaf); - } - } - - // data and layout - delete gd.data; - delete gd.layout; - delete gd._fullData; - delete gd._fullLayout; - delete gd.calcdata; - delete gd.framework; - delete gd.empty; - - delete gd.fid; - - delete gd.undoqueue; // action queue - delete gd.undonum; - delete gd.autoplay; // are we doing an action that doesn't go in undo queue? - delete gd.changed; - - // these get recreated on Plotly.plot anyway, but just to be safe - // (and to have a record of them...) - delete gd._tester; - delete gd._testref; - delete gd._promises; - delete gd._redrawTimer; - delete gd._replotting; - delete gd.firstscatter; - delete gd.hmlumcount; - delete gd.hmpixcount; - delete gd.numboxes; - delete gd._hoverTimer; - delete gd._lastHoverTime; - delete gd._transitionData; - delete gd._transitioning; - delete gd._initialAutoSize; - - // remove all event listeners - if(gd.removeAllListeners) gd.removeAllListeners(); + // note: we DO NOT remove _context because it doesn't change when we insert + // a new plot, and may have been set outside of our scope. + var fullLayout = gd._fullLayout || {}; + if (fullLayout._glcontainer !== undefined) fullLayout._glcontainer.remove(); + if (fullLayout._geocontainer !== undefined) fullLayout._geocontainer.remove(); + + // remove modebar + if (fullLayout._modeBar) fullLayout._modeBar.destroy(); + + if (gd._transitionData) { + // Ensure any dangling callbacks are simply dropped if the plot is purged. + // This is more or less only actually important for testing. + if (gd._transitionData._interruptCallbacks) { + gd._transitionData._interruptCallbacks.length = 0; + } + + if (gd._transitionData._animationRaf) { + window.cancelAnimationFrame(gd._transitionData._animationRaf); + } + } + + // data and layout + delete gd.data; + delete gd.layout; + delete gd._fullData; + delete gd._fullLayout; + delete gd.calcdata; + delete gd.framework; + delete gd.empty; + + delete gd.fid; + + delete gd.undoqueue; + // action queue + delete gd.undonum; + delete gd.autoplay; + // are we doing an action that doesn't go in undo queue? + delete gd.changed; + + // these get recreated on Plotly.plot anyway, but just to be safe + // (and to have a record of them...) + delete gd._tester; + delete gd._testref; + delete gd._promises; + delete gd._redrawTimer; + delete gd._replotting; + delete gd.firstscatter; + delete gd.hmlumcount; + delete gd.hmpixcount; + delete gd.numboxes; + delete gd._hoverTimer; + delete gd._lastHoverTime; + delete gd._transitionData; + delete gd._transitioning; + delete gd._initialAutoSize; + + // remove all event listeners + if (gd.removeAllListeners) gd.removeAllListeners(); }; plots.style = function(gd) { - var _modules = gd._fullLayout._modules; + var _modules = gd._fullLayout._modules; - for(var i = 0; i < _modules.length; i++) { - var _module = _modules[i]; + for (var i = 0; i < _modules.length; i++) { + var _module = _modules[i]; - if(_module.style) _module.style(gd); - } + if (_module.style) _module.style(gd); + } }; plots.sanitizeMargins = function(fullLayout) { - // polar doesn't do margins... - if(!fullLayout || !fullLayout.margin) return; - - var width = fullLayout.width, - height = fullLayout.height, - margin = fullLayout.margin, - plotWidth = width - (margin.l + margin.r), - plotHeight = height - (margin.t + margin.b), - correction; - - // if margin.l + margin.r = 0 then plotWidth > 0 - // as width >= 10 by supplyDefaults - // similarly for margin.t + margin.b - - if(plotWidth < 0) { - correction = (width - 1) / (margin.l + margin.r); - margin.l = Math.floor(correction * margin.l); - margin.r = Math.floor(correction * margin.r); - } - - if(plotHeight < 0) { - correction = (height - 1) / (margin.t + margin.b); - margin.t = Math.floor(correction * margin.t); - margin.b = Math.floor(correction * margin.b); - } + // polar doesn't do margins... + if (!fullLayout || !fullLayout.margin) return; + + var width = fullLayout.width, + height = fullLayout.height, + margin = fullLayout.margin, + plotWidth = width - (margin.l + margin.r), + plotHeight = height - (margin.t + margin.b), + correction; + + // if margin.l + margin.r = 0 then plotWidth > 0 + // as width >= 10 by supplyDefaults + // similarly for margin.t + margin.b + if (plotWidth < 0) { + correction = (width - 1) / (margin.l + margin.r); + margin.l = Math.floor(correction * margin.l); + margin.r = Math.floor(correction * margin.r); + } + + if (plotHeight < 0) { + correction = (height - 1) / (margin.t + margin.b); + margin.t = Math.floor(correction * margin.t); + margin.b = Math.floor(correction * margin.b); + } }; // called by legend and colorbar routines to see if we need to @@ -1229,112 +1269,111 @@ plots.sanitizeMargins = function(fullLayout) { // the rest are pixels in each direction // or leave o out to delete this entry (like if it's hidden) plots.autoMargin = function(gd, id, o) { - var fullLayout = gd._fullLayout; - - if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; - - if(fullLayout.margin.autoexpand !== false) { - if(!o) delete fullLayout._pushmargin[id]; - else { - var pad = o.pad === undefined ? 12 : o.pad; - - // if the item is too big, just give it enough automargin to - // make sure you can still grab it and bring it back - if(o.l + o.r > fullLayout.width * 0.5) o.l = o.r = 0; - if(o.b + o.t > fullLayout.height * 0.5) o.b = o.t = 0; - - fullLayout._pushmargin[id] = { - l: {val: o.x, size: o.l + pad}, - r: {val: o.x, size: o.r + pad}, - b: {val: o.y, size: o.b + pad}, - t: {val: o.y, size: o.t + pad} - }; - } + var fullLayout = gd._fullLayout; + + if (!fullLayout._pushmargin) fullLayout._pushmargin = {}; + + if (fullLayout.margin.autoexpand !== false) { + if (!o) { + delete fullLayout._pushmargin[id]; + } else { + var pad = o.pad === undefined ? 12 : o.pad; + + // if the item is too big, just give it enough automargin to + // make sure you can still grab it and bring it back + if (o.l + o.r > fullLayout.width * 0.5) o.l = o.r = 0; + if (o.b + o.t > fullLayout.height * 0.5) o.b = o.t = 0; - if(!gd._replotting) plots.doAutoMargin(gd); + fullLayout._pushmargin[id] = { + l: { val: o.x, size: o.l + pad }, + r: { val: o.x, size: o.r + pad }, + b: { val: o.y, size: o.b + pad }, + t: { val: o.y, size: o.t + pad } + }; } + + if (!gd._replotting) plots.doAutoMargin(gd); + } }; plots.doAutoMargin = function(gd) { - var fullLayout = gd._fullLayout; - if(!fullLayout._size) fullLayout._size = {}; - if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; - - var gs = fullLayout._size, - oldmargins = JSON.stringify(gs); - - // adjust margins for outside legends and colorbars - // fullLayout.margin is the requested margin, - // fullLayout._size has margins and plotsize after adjustment - var ml = Math.max(fullLayout.margin.l || 0, 0), - mr = Math.max(fullLayout.margin.r || 0, 0), - mt = Math.max(fullLayout.margin.t || 0, 0), - mb = Math.max(fullLayout.margin.b || 0, 0), - pm = fullLayout._pushmargin; - - if(fullLayout.margin.autoexpand !== false) { - // fill in the requested margins - pm.base = { - l: {val: 0, size: ml}, - r: {val: 1, size: mr}, - t: {val: 1, size: mt}, - b: {val: 0, size: mb} - }; - // now cycle through all the combinations of l and r - // (and t and b) to find the required margins - Object.keys(pm).forEach(function(k1) { - var pushleft = pm[k1].l || {}, - pushbottom = pm[k1].b || {}, - fl = pushleft.val, - pl = pushleft.size, - fb = pushbottom.val, - pb = pushbottom.size; - Object.keys(pm).forEach(function(k2) { - if(isNumeric(pl) && pm[k2].r) { - var fr = pm[k2].r.val, - pr = pm[k2].r.size; - if(fr > fl) { - var newl = (pl * fr + - (pr - fullLayout.width) * fl) / (fr - fl), - newr = (pr * (1 - fl) + - (pl - fullLayout.width) * (1 - fr)) / (fr - fl); - if(newl >= 0 && newr >= 0 && newl + newr > ml + mr) { - ml = newl; - mr = newr; - } - } - } - if(isNumeric(pb) && pm[k2].t) { - var ft = pm[k2].t.val, - pt = pm[k2].t.size; - if(ft > fb) { - var newb = (pb * ft + - (pt - fullLayout.height) * fb) / (ft - fb), - newt = (pt * (1 - fb) + - (pb - fullLayout.height) * (1 - ft)) / (ft - fb); - if(newb >= 0 && newt >= 0 && newb + newt > mb + mt) { - mb = newb; - mt = newt; - } - } - } - }); - }); - } - - gs.l = Math.round(ml); - gs.r = Math.round(mr); - gs.t = Math.round(mt); - gs.b = Math.round(mb); - gs.p = Math.round(fullLayout.margin.pad); - gs.w = Math.round(fullLayout.width) - gs.l - gs.r; - gs.h = Math.round(fullLayout.height) - gs.t - gs.b; - - // if things changed and we're not already redrawing, trigger a redraw - if(!gd._replotting && oldmargins !== '{}' && - oldmargins !== JSON.stringify(fullLayout._size)) { - return Plotly.plot(gd); - } + var fullLayout = gd._fullLayout; + if (!fullLayout._size) fullLayout._size = {}; + if (!fullLayout._pushmargin) fullLayout._pushmargin = {}; + + var gs = fullLayout._size, oldmargins = JSON.stringify(gs); + + // adjust margins for outside legends and colorbars + // fullLayout.margin is the requested margin, + // fullLayout._size has margins and plotsize after adjustment + var ml = Math.max(fullLayout.margin.l || 0, 0), + mr = Math.max(fullLayout.margin.r || 0, 0), + mt = Math.max(fullLayout.margin.t || 0, 0), + mb = Math.max(fullLayout.margin.b || 0, 0), + pm = fullLayout._pushmargin; + + if (fullLayout.margin.autoexpand !== false) { + // fill in the requested margins + pm.base = { + l: { val: 0, size: ml }, + r: { val: 1, size: mr }, + t: { val: 1, size: mt }, + b: { val: 0, size: mb } + }; + // now cycle through all the combinations of l and r + // (and t and b) to find the required margins + Object.keys(pm).forEach(function(k1) { + var pushleft = pm[k1].l || {}, + pushbottom = pm[k1].b || {}, + fl = pushleft.val, + pl = pushleft.size, + fb = pushbottom.val, + pb = pushbottom.size; + Object.keys(pm).forEach(function(k2) { + if (isNumeric(pl) && pm[k2].r) { + var fr = pm[k2].r.val, pr = pm[k2].r.size; + if (fr > fl) { + var newl = (pl * fr + (pr - fullLayout.width) * fl) / (fr - fl), + newr = (pr * (1 - fl) + (pl - fullLayout.width) * (1 - fr)) / + (fr - fl); + if (newl >= 0 && newr >= 0 && newl + newr > ml + mr) { + ml = newl; + mr = newr; + } + } + } + if (isNumeric(pb) && pm[k2].t) { + var ft = pm[k2].t.val, pt = pm[k2].t.size; + if (ft > fb) { + var newb = (pb * ft + (pt - fullLayout.height) * fb) / (ft - fb), + newt = (pt * (1 - fb) + (pb - fullLayout.height) * (1 - ft)) / + (ft - fb); + if (newb >= 0 && newt >= 0 && newb + newt > mb + mt) { + mb = newb; + mt = newt; + } + } + } + }); + }); + } + + gs.l = Math.round(ml); + gs.r = Math.round(mr); + gs.t = Math.round(mt); + gs.b = Math.round(mb); + gs.p = Math.round(fullLayout.margin.pad); + gs.w = Math.round(fullLayout.width) - gs.l - gs.r; + gs.h = Math.round(fullLayout.height) - gs.t - gs.b; + + // if things changed and we're not already redrawing, trigger a redraw + if ( + !gd._replotting && + oldmargins !== "{}" && + oldmargins !== JSON.stringify(fullLayout._size) + ) { + return Plotly.plot(gd); + } }; /** @@ -1360,90 +1399,95 @@ plots.doAutoMargin = function(gd) { * @returns {Object|String} */ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { - // if the defaults aren't supplied yet, we need to do that... - if((useDefaults && dataonly && !gd._fullData) || - (useDefaults && !dataonly && !gd._fullLayout)) { - plots.supplyDefaults(gd); - } - - var data = (useDefaults) ? gd._fullData : gd.data, - layout = (useDefaults) ? gd._fullLayout : gd.layout, - frames = (gd._transitionData || {})._frames; - - function stripObj(d) { - if(typeof d === 'function') { - return null; - } - if(Lib.isPlainObject(d)) { - var o = {}, v, src; - for(v in d) { - // remove private elements and functions - // _ is for private, [ is a mistake ie [object Object] - if(typeof d[v] === 'function' || - ['_', '['].indexOf(v.charAt(0)) !== -1) { - continue; - } - - // look for src/data matches and remove the appropriate one - if(mode === 'keepdata') { - // keepdata: remove all ...src tags - if(v.substr(v.length - 3) === 'src') { - continue; - } - } - else if(mode === 'keepstream') { - // keep sourced data if it's being streamed. - // similar to keepref, but if the 'stream' object exists - // in a trace, we will keep the data array. - src = d[v + 'src']; - if(typeof src === 'string' && src.indexOf(':') > 0) { - if(!Lib.isPlainObject(d.stream)) { - continue; - } - } - } - else if(mode !== 'keepall') { - // keepref: remove sourced data but only - // if the source tag is well-formed - src = d[v + 'src']; - if(typeof src === 'string' && src.indexOf(':') > 0) { - continue; - } - } - - // OK, we're including this... recurse into it - o[v] = stripObj(d[v]); - } - return o; + // if the defaults aren't supplied yet, we need to do that... + if ( + useDefaults && dataonly && !gd._fullData || + useDefaults && !dataonly && !gd._fullLayout + ) { + plots.supplyDefaults(gd); + } + + var data = useDefaults ? gd._fullData : gd.data, + layout = useDefaults ? gd._fullLayout : gd.layout, + frames = (gd._transitionData || {})._frames; + + function stripObj(d) { + if (typeof d === "function") { + return null; + } + if (Lib.isPlainObject(d)) { + var o = {}, v, src; + for (v in d) { + // remove private elements and functions + // _ is for private, [ is a mistake ie [object Object] + if ( + typeof d[v] === "function" || ["_", "["].indexOf(v.charAt(0)) !== -1 + ) { + continue; } - if(Array.isArray(d)) { - return d.map(stripObj); + // look for src/data matches and remove the appropriate one + if (mode === "keepdata") { + // keepdata: remove all ...src tags + if (v.substr(v.length - 3) === "src") { + continue; + } + } else if (mode === "keepstream") { + // keep sourced data if it's being streamed. + // similar to keepref, but if the 'stream' object exists + // in a trace, we will keep the data array. + src = d[v + "src"]; + if (typeof src === "string" && src.indexOf(":") > 0) { + if (!Lib.isPlainObject(d.stream)) { + continue; + } + } + } else if (mode !== "keepall") { + // keepref: remove sourced data but only + // if the source tag is well-formed + src = d[v + "src"]; + if (typeof src === "string" && src.indexOf(":") > 0) { + continue; + } } - // convert native dates to date strings... - // mostly for external users exporting to plotly - if(Lib.isJSDate(d)) return Lib.ms2DateTimeLocal(+d); + // OK, we're including this... recurse into it + o[v] = stripObj(d[v]); + } + return o; + } - return d; + if (Array.isArray(d)) { + return d.map(stripObj); } - var obj = { - data: (data || []).map(function(v) { - var d = stripObj(v); - // fit has some little arrays in it that don't contain data, - // just fit params and meta - if(dataonly) { delete d.fit; } - return d; - }) - }; - if(!dataonly) { obj.layout = stripObj(layout); } + // convert native dates to date strings... + // mostly for external users exporting to plotly + if (Lib.isJSDate(d)) return Lib.ms2DateTimeLocal(+d); + + return d; + } - if(gd.framework && gd.framework.isPolar) obj = gd.framework.getConfig(); + var obj = { + data: (data || []).map(function(v) { + var d = stripObj(v); + // fit has some little arrays in it that don't contain data, + // just fit params and meta + if (dataonly) { + delete d.fit; + } + return d; + }) + }; + if (!dataonly) { + obj.layout = stripObj(layout); + } - if(frames) obj.frames = stripObj(frames); + if (gd.framework && gd.framework.isPolar) obj = gd.framework.getConfig(); - return (output === 'object') ? obj : JSON.stringify(obj); + if (frames) obj.frames = stripObj(frames); + + return output === "object" ? obj : JSON.stringify(obj); }; /** @@ -1453,49 +1497,49 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { * Sequence of operations to be performed on the keyframes */ plots.modifyFrames = function(gd, operations) { - var i, op, frame; - var _frames = gd._transitionData._frames; - var _hash = gd._transitionData._frameHash; + var i, op, frame; + var _frames = gd._transitionData._frames; + var _hash = gd._transitionData._frameHash; - for(i = 0; i < operations.length; i++) { - op = operations[i]; + for (i = 0; i < operations.length; i++) { + op = operations[i]; - switch(op.type) { - // No reason this couldn't exist, but is currently unused/untested: - /* case 'rename': + switch (op.type) { + // No reason this couldn't exist, but is currently unused/untested: + /* case 'rename': frame = _frames[op.index]; delete _hash[frame.name]; _hash[op.name] = frame; frame.name = op.name; - break;*/ - case 'replace': - frame = op.value; - var oldName = (_frames[op.index] || {}).name; - var newName = frame.name; - _frames[op.index] = _hash[newName] = frame; - - if(newName !== oldName) { - // If name has changed in addition to replacement, then update - // the lookup table: - delete _hash[oldName]; - _hash[newName] = frame; - } - - break; - case 'insert': - frame = op.value; - _hash[frame.name] = frame; - _frames.splice(op.index, 0, frame); - break; - case 'delete': - frame = _frames[op.index]; - delete _hash[frame.name]; - _frames.splice(op.index, 1); - break; + break; */ + case "replace": + frame = op.value; + var oldName = (_frames[op.index] || {}).name; + var newName = frame.name; + _frames[op.index] = _hash[newName] = frame; + + if (newName !== oldName) { + // If name has changed in addition to replacement, then update + // the lookup table: + delete _hash[oldName]; + _hash[newName] = frame; } - } - return Promise.resolve(); + break; + case "insert": + frame = op.value; + _hash[frame.name] = frame; + _frames.splice(op.index, 0, frame); + break; + case "delete": + frame = _frames[op.index]; + delete _hash[frame.name]; + _frames.splice(op.index, 1); + break; + } + } + + return Promise.resolve(); }; /* @@ -1510,85 +1554,91 @@ plots.modifyFrames = function(gd, operations) { * Returns: a new object with the merged content */ plots.computeFrame = function(gd, frameName) { - var frameLookup = gd._transitionData._frameHash; - var i, traceIndices, traceIndex, destIndex; - - // Null or undefined will fail on .toString(). We'll allow numbers since we - // make it clear frames must be given string names, but we'll allow numbers - // here since they're otherwise fine for looking up frames as long as they're - // properly cast to strings. We really just want to ensure here that this - // 1) doesn't fail, and - // 2) doens't give an incorrect answer (which String(frameName) would) - if(!frameName) { - throw new Error('computeFrame must be given a string frame name'); - } - - var framePtr = frameLookup[frameName.toString()]; - - // Return false if the name is invalid: - if(!framePtr) { - return false; - } - - var frameStack = [framePtr]; - var frameNameStack = [framePtr.name]; - - // Follow frame pointers: - while(framePtr.baseframe && (framePtr = frameLookup[framePtr.baseframe.toString()])) { - // Avoid infinite loops: - if(frameNameStack.indexOf(framePtr.name) !== -1) break; - - frameStack.push(framePtr); - frameNameStack.push(framePtr.name); - } - - // A new object for the merged result: - var result = {}; - - // Merge, starting with the last and ending with the desired frame: - while((framePtr = frameStack.pop())) { - if(framePtr.layout) { - result.layout = plots.extendLayout(result.layout, framePtr.layout); + var frameLookup = gd._transitionData._frameHash; + var i, traceIndices, traceIndex, destIndex; + + // Null or undefined will fail on .toString(). We'll allow numbers since we + // make it clear frames must be given string names, but we'll allow numbers + // here since they're otherwise fine for looking up frames as long as they're + // properly cast to strings. We really just want to ensure here that this + // 1) doesn't fail, and + // 2) doens't give an incorrect answer (which String(frameName) would) + if (!frameName) { + throw new Error("computeFrame must be given a string frame name"); + } + + var framePtr = frameLookup[frameName.toString()]; + + // Return false if the name is invalid: + if (!framePtr) { + return false; + } + + var frameStack = [framePtr]; + var frameNameStack = [framePtr.name]; + + // Follow frame pointers: + while ( + framePtr.baseframe && + (framePtr = frameLookup[framePtr.baseframe.toString()]) + ) { + // Avoid infinite loops: + if (frameNameStack.indexOf(framePtr.name) !== -1) break; + + frameStack.push(framePtr); + frameNameStack.push(framePtr.name); + } + + // A new object for the merged result: + var result = {}; + + // Merge, starting with the last and ending with the desired frame: + while (framePtr = frameStack.pop()) { + if (framePtr.layout) { + result.layout = plots.extendLayout(result.layout, framePtr.layout); + } + + if (framePtr.data) { + if (!result.data) { + result.data = []; + } + traceIndices = framePtr.traces; + + if (!traceIndices) { + // If not defined, assume serial order starting at zero + traceIndices = []; + for (i = 0; i < framePtr.data.length; i++) { + traceIndices[i] = i; + } + } + + if (!result.traces) { + result.traces = []; + } + + for (i = 0; i < framePtr.data.length; i++) { + // Loop through this frames data, find out where it should go, + // and merge it! + traceIndex = traceIndices[i]; + if (traceIndex === undefined || traceIndex === null) { + continue; } - if(framePtr.data) { - if(!result.data) { - result.data = []; - } - traceIndices = framePtr.traces; - - if(!traceIndices) { - // If not defined, assume serial order starting at zero - traceIndices = []; - for(i = 0; i < framePtr.data.length; i++) { - traceIndices[i] = i; - } - } - - if(!result.traces) { - result.traces = []; - } - - for(i = 0; i < framePtr.data.length; i++) { - // Loop through this frames data, find out where it should go, - // and merge it! - traceIndex = traceIndices[i]; - if(traceIndex === undefined || traceIndex === null) { - continue; - } - - destIndex = result.traces.indexOf(traceIndex); - if(destIndex === -1) { - destIndex = result.data.length; - result.traces[destIndex] = traceIndex; - } - - result.data[destIndex] = plots.extendTrace(result.data[destIndex], framePtr.data[i]); - } + destIndex = result.traces.indexOf(traceIndex); + if (destIndex === -1) { + destIndex = result.data.length; + result.traces[destIndex] = traceIndex; } + + result.data[destIndex] = plots.extendTrace( + result.data[destIndex], + framePtr.data[i] + ); + } } + } - return result; + return result; }; /* @@ -1598,14 +1648,14 @@ plots.computeFrame = function(gd, frameName) { * and create and haven't updated the lookup table. */ plots.recomputeFrameHash = function(gd) { - var hash = gd._transitionData._frameHash = {}; - var frames = gd._transitionData._frames; - for(var i = 0; i < frames.length; i++) { - var frame = frames[i]; - if(frame && frame.name) { - hash[frame.name] = frame; - } - } + var hash = gd._transitionData._frameHash = {}; + var frames = gd._transitionData._frames; + for (var i = 0; i < frames.length; i++) { + var frame = frames[i]; + if (frame && frame.name) { + hash[frame.name] = frame; + } + } }; /** @@ -1619,63 +1669,73 @@ plots.recomputeFrameHash = function(gd) { * See extendTrace and extendLayout below for usage. */ plots.extendObjectWithContainers = function(dest, src, containerPaths) { - var containerProp, containerVal, i, j, srcProp, destProp, srcContainer, destContainer; - var copy = Lib.extendDeepNoArrays({}, src || {}); - var expandedObj = Lib.expandObjectPaths(copy); - var containerObj = {}; - - // Step through and extract any container properties. Otherwise extendDeepNoArrays - // will clobber any existing properties with an empty array and then supplyDefaults - // will reset everything to defaults. - if(containerPaths && containerPaths.length) { - for(i = 0; i < containerPaths.length; i++) { - containerProp = Lib.nestedProperty(expandedObj, containerPaths[i]); - containerVal = containerProp.get(); - - if(containerVal === undefined) { - Lib.nestedProperty(containerObj, containerPaths[i]).set(null); - } - else { - containerProp.set(null); - Lib.nestedProperty(containerObj, containerPaths[i]).set(containerVal); - } + var containerProp, + containerVal, + i, + j, + srcProp, + destProp, + srcContainer, + destContainer; + var copy = Lib.extendDeepNoArrays({}, src || {}); + var expandedObj = Lib.expandObjectPaths(copy); + var containerObj = {}; + + // Step through and extract any container properties. Otherwise extendDeepNoArrays + // will clobber any existing properties with an empty array and then supplyDefaults + // will reset everything to defaults. + if (containerPaths && containerPaths.length) { + for (i = 0; i < containerPaths.length; i++) { + containerProp = Lib.nestedProperty(expandedObj, containerPaths[i]); + containerVal = containerProp.get(); + + if (containerVal === undefined) { + Lib.nestedProperty(containerObj, containerPaths[i]).set(null); + } else { + containerProp.set(null); + Lib.nestedProperty(containerObj, containerPaths[i]).set(containerVal); + } + } + } + + dest = Lib.extendDeepNoArrays(dest || {}, expandedObj); + + if (containerPaths && containerPaths.length) { + for (i = 0; i < containerPaths.length; i++) { + srcProp = Lib.nestedProperty(containerObj, containerPaths[i]); + srcContainer = srcProp.get(); + + if (!srcContainer) continue; + + destProp = Lib.nestedProperty(dest, containerPaths[i]); + destContainer = destProp.get(); + + if (!Array.isArray(destContainer)) { + destContainer = []; + destProp.set(destContainer); + } + + for (j = 0; j < srcContainer.length; j++) { + var srcObj = srcContainer[j]; + + if (srcObj === null) { + destContainer[j] = null; + } else { + destContainer[j] = plots.extendObjectWithContainers( + destContainer[j], + srcObj + ); } - } - - dest = Lib.extendDeepNoArrays(dest || {}, expandedObj); - - if(containerPaths && containerPaths.length) { - for(i = 0; i < containerPaths.length; i++) { - srcProp = Lib.nestedProperty(containerObj, containerPaths[i]); - srcContainer = srcProp.get(); - - if(!srcContainer) continue; - - destProp = Lib.nestedProperty(dest, containerPaths[i]); - destContainer = destProp.get(); - - if(!Array.isArray(destContainer)) { - destContainer = []; - destProp.set(destContainer); - } - - for(j = 0; j < srcContainer.length; j++) { - var srcObj = srcContainer[j]; - - if(srcObj === null) destContainer[j] = null; - else { - destContainer[j] = plots.extendObjectWithContainers(destContainer[j], srcObj); - } - } + } - destProp.set(destContainer); - } + destProp.set(destContainer); } + } - return dest; + return dest; }; -plots.dataArrayContainers = ['transforms']; +plots.dataArrayContainers = ["transforms"]; plots.layoutArrayContainers = Registry.layoutArrayContainers; /* @@ -1687,7 +1747,11 @@ plots.layoutArrayContainers = Registry.layoutArrayContainers; * The result is the original object reference with the new contents merged in. */ plots.extendTrace = function(destTrace, srcTrace) { - return plots.extendObjectWithContainers(destTrace, srcTrace, plots.dataArrayContainers); + return plots.extendObjectWithContainers( + destTrace, + srcTrace, + plots.dataArrayContainers + ); }; /* @@ -1700,7 +1764,11 @@ plots.extendTrace = function(destTrace, srcTrace) { * The result is the original object reference with the new contents merged in. */ plots.extendLayout = function(destLayout, srcLayout) { - return plots.extendObjectWithContainers(destLayout, srcLayout, plots.layoutArrayContainers); + return plots.extendObjectWithContainers( + destLayout, + srcLayout, + plots.layoutArrayContainers + ); }; /** @@ -1719,404 +1787,432 @@ plots.extendLayout = function(destLayout, srcLayout) { * @param {Object} transitionOpts * options for the transition */ -plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts) { - var i, traceIdx; - - var dataLength = Array.isArray(data) ? data.length : 0; - var traceIndices = traces.slice(0, dataLength); - - var transitionedTraces = []; - - function prepareTransitions() { - var i; - - for(i = 0; i < traceIndices.length; i++) { - var traceIdx = traceIndices[i]; - var trace = gd._fullData[traceIdx]; - var module = trace._module; - - // There's nothing to do if this module is not defined: - if(!module) continue; - - // Don't register the trace as transitioned if it doens't know what to do. - // If it *is* registered, it will receive a callback that it's responsible - // for calling in order to register the transition as having completed. - if(module.animatable) { - transitionedTraces.push(traceIdx); - } - - gd.data[traceIndices[i]] = plots.extendTrace(gd.data[traceIndices[i]], data[i]); - } - - // Follow the same procedure. Clone it so we don't mangle the input, then - // expand any object paths so we can merge deep into gd.layout: - var layoutUpdate = Lib.expandObjectPaths(Lib.extendDeepNoArrays({}, layout)); - - // Before merging though, we need to modify the incoming layout. We only - // know how to *transition* layout ranges, so it's imperative that a new - // range not be sent to the layout before the transition has started. So - // we must remove the things we can transition: - var axisAttrRe = /^[xy]axis[0-9]*$/; - for(var attr in layoutUpdate) { - if(!axisAttrRe.test(attr)) continue; - delete layoutUpdate[attr].range; - } - - plots.extendLayout(gd.layout, layoutUpdate); +plots.transition = function( + gd, + data, + layout, + traces, + frameOpts, + transitionOpts +) { + var i, traceIdx; + + var dataLength = Array.isArray(data) ? data.length : 0; + var traceIndices = traces.slice(0, dataLength); + + var transitionedTraces = []; + + function prepareTransitions() { + var i; - // Supply defaults after applying the incoming properties. Note that any attempt - // to simplify this step and reduce the amount of work resulted in the reconstruction - // of essentially the whole supplyDefaults step, so that it seems sensible to just use - // supplyDefaults even though it's heavier than would otherwise be desired for - // transitions: - plots.supplyDefaults(gd); + for (i = 0; i < traceIndices.length; i++) { + var traceIdx = traceIndices[i]; + var trace = gd._fullData[traceIdx]; + var module = trace._module; - plots.doCalcdata(gd); + // There's nothing to do if this module is not defined: + if (!module) continue; - ErrorBars.calc(gd); + // Don't register the trace as transitioned if it doens't know what to do. + // If it *is* registered, it will receive a callback that it's responsible + // for calling in order to register the transition as having completed. + if (module.animatable) { + transitionedTraces.push(traceIdx); + } - return Promise.resolve(); + gd.data[traceIndices[i]] = plots.extendTrace( + gd.data[traceIndices[i]], + data[i] + ); } - function executeCallbacks(list) { - var p = Promise.resolve(); - if(!list) return p; - while(list.length) { - p = p.then((list.shift())); - } - return p; - } + // Follow the same procedure. Clone it so we don't mangle the input, then + // expand any object paths so we can merge deep into gd.layout: + var layoutUpdate = Lib.expandObjectPaths( + Lib.extendDeepNoArrays({}, layout) + ); - function flushCallbacks(list) { - if(!list) return; - while(list.length) { - list.shift(); - } + // Before merging though, we need to modify the incoming layout. We only + // know how to *transition* layout ranges, so it's imperative that a new + // range not be sent to the layout before the transition has started. So + // we must remove the things we can transition: + var axisAttrRe = /^[xy]axis[0-9]*$/; + for (var attr in layoutUpdate) { + if (!axisAttrRe.test(attr)) continue; + delete layoutUpdate[attr].range; } - var aborted = false; - - function executeTransitions() { - - gd.emit('plotly_transitioning', []); + plots.extendLayout(gd.layout, layoutUpdate); - return new Promise(function(resolve) { - // This flag is used to disabled things like autorange: - gd._transitioning = true; - - // When instantaneous updates are coming through quickly, it's too much to simply disable - // all interaction, so store this flag so we can disambiguate whether mouse interactions - // should be fully disabled or not: - if(transitionOpts.duration > 0) { - gd._transitioningWithDuration = true; - } + // Supply defaults after applying the incoming properties. Note that any attempt + // to simplify this step and reduce the amount of work resulted in the reconstruction + // of essentially the whole supplyDefaults step, so that it seems sensible to just use + // supplyDefaults even though it's heavier than would otherwise be desired for + // transitions: + plots.supplyDefaults(gd); + plots.doCalcdata(gd); - // If another transition is triggered, this callback will be executed simply because it's - // in the interruptCallbacks queue. If this transition completes, it will instead flush - // that queue and forget about this callback. - gd._transitionData._interruptCallbacks.push(function() { - aborted = true; - }); + ErrorBars.calc(gd); - if(frameOpts.redraw) { - gd._transitionData._interruptCallbacks.push(function() { - return Plotly.redraw(gd); - }); - } - - // Emit this and make sure it happens last: - gd._transitionData._interruptCallbacks.push(function() { - gd.emit('plotly_transitioninterrupted', []); - }); - - // Construct callbacks that are executed on transition end. This ensures the d3 transitions - // are *complete* before anything else is done. - var numCallbacks = 0; - var numCompleted = 0; - function makeCallback() { - numCallbacks++; - return function() { - numCompleted++; - // When all are complete, perform a redraw: - if(!aborted && numCompleted === numCallbacks) { - completeTransition(resolve); - } - }; - } + return Promise.resolve(); + } - var traceTransitionOpts; - var j; - var basePlotModules = gd._fullLayout._basePlotModules; - var hasAxisTransition = false; - - if(layout) { - for(j = 0; j < basePlotModules.length; j++) { - if(basePlotModules[j].transitionAxes) { - var newLayout = Lib.expandObjectPaths(layout); - hasAxisTransition = basePlotModules[j].transitionAxes(gd, newLayout, transitionOpts, makeCallback) || hasAxisTransition; - } - } - } + function executeCallbacks(list) { + var p = Promise.resolve(); + if (!list) return p; + while (list.length) { + p = p.then(list.shift()); + } + return p; + } - // Here handle the exception that we refuse to animate scales and axes at the same - // time. In other words, if there's an axis transition, then set the data transition - // to instantaneous. - if(hasAxisTransition) { - traceTransitionOpts = Lib.extendFlat({}, transitionOpts); - traceTransitionOpts.duration = 0; - } else { - traceTransitionOpts = transitionOpts; - } + function flushCallbacks(list) { + if (!list) return; + while (list.length) { + list.shift(); + } + } - for(j = 0; j < basePlotModules.length; j++) { - // Note that we pass a callback to *create* the callback that must be invoked on completion. - // This is since not all traces know about transitions, so it greatly simplifies matters if - // the trace is responsible for creating a callback, if needed, and then executing it when - // the time is right. - basePlotModules[j].plot(gd, transitionedTraces, traceTransitionOpts, makeCallback); - } + var aborted = false; - // If nothing else creates a callback, then this will trigger the completion in the next tick: - setTimeout(makeCallback()); + function executeTransitions() { + gd.emit("plotly_transitioning", []); + return new Promise(function(resolve) { + // This flag is used to disabled things like autorange: + gd._transitioning = true; + + // When instantaneous updates are coming through quickly, it's too much to simply disable + // all interaction, so store this flag so we can disambiguate whether mouse interactions + // should be fully disabled or not: + if (transitionOpts.duration > 0) { + gd._transitioningWithDuration = true; + } + + // If another transition is triggered, this callback will be executed simply because it's + // in the interruptCallbacks queue. If this transition completes, it will instead flush + // that queue and forget about this callback. + gd._transitionData._interruptCallbacks.push(function() { + aborted = true; + }); + + if (frameOpts.redraw) { + gd._transitionData._interruptCallbacks.push(function() { + return Plotly.redraw(gd); }); - } + } + + // Emit this and make sure it happens last: + gd._transitionData._interruptCallbacks.push(function() { + gd.emit("plotly_transitioninterrupted", []); + }); + + // Construct callbacks that are executed on transition end. This ensures the d3 transitions + // are *complete* before anything else is done. + var numCallbacks = 0; + var numCompleted = 0; + function makeCallback() { + numCallbacks++; + return function() { + numCompleted++; + // When all are complete, perform a redraw: + if (!aborted && numCompleted === numCallbacks) { + completeTransition(resolve); + } + }; + } + + var traceTransitionOpts; + var j; + var basePlotModules = gd._fullLayout._basePlotModules; + var hasAxisTransition = false; + + if (layout) { + for (j = 0; j < basePlotModules.length; j++) { + if (basePlotModules[j].transitionAxes) { + var newLayout = Lib.expandObjectPaths(layout); + hasAxisTransition = basePlotModules[j].transitionAxes( + gd, + newLayout, + transitionOpts, + makeCallback + ) || + hasAxisTransition; + } + } + } + + // Here handle the exception that we refuse to animate scales and axes at the same + // time. In other words, if there's an axis transition, then set the data transition + // to instantaneous. + if (hasAxisTransition) { + traceTransitionOpts = Lib.extendFlat({}, transitionOpts); + traceTransitionOpts.duration = 0; + } else { + traceTransitionOpts = transitionOpts; + } + + for (j = 0; j < basePlotModules.length; j++) { + // Note that we pass a callback to *create* the callback that must be invoked on completion. + // This is since not all traces know about transitions, so it greatly simplifies matters if + // the trace is responsible for creating a callback, if needed, and then executing it when + // the time is right. + basePlotModules[j].plot( + gd, + transitionedTraces, + traceTransitionOpts, + makeCallback + ); + } + + // If nothing else creates a callback, then this will trigger the completion in the next tick: + setTimeout(makeCallback()); + }); + } - function completeTransition(callback) { - // This a simple workaround for tests which purge the graph before animations - // have completed. That's not a very common case, so this is the simplest - // fix. - if(!gd._transitionData) return; + function completeTransition(callback) { + // This a simple workaround for tests which purge the graph before animations + // have completed. That's not a very common case, so this is the simplest + // fix. + if (!gd._transitionData) return; - flushCallbacks(gd._transitionData._interruptCallbacks); + flushCallbacks(gd._transitionData._interruptCallbacks); - return Promise.resolve().then(function() { - if(frameOpts.redraw) { - return Plotly.redraw(gd); - } - }).then(function() { - // Set transitioning false again once the redraw has occurred. This is used, for example, - // to prevent the trailing redraw from autoranging: - gd._transitioning = false; - gd._transitioningWithDuration = false; - - gd.emit('plotly_transitioned', []); - }).then(callback); - } + return Promise.resolve() + .then(function() { + if (frameOpts.redraw) { + return Plotly.redraw(gd); + } + }) + .then(function() { + // Set transitioning false again once the redraw has occurred. This is used, for example, + // to prevent the trailing redraw from autoranging: + gd._transitioning = false; + gd._transitioningWithDuration = false; - function interruptPreviousTransitions() { - // Fail-safe against purged plot: - if(!gd._transitionData) return; + gd.emit("plotly_transitioned", []); + }) + .then(callback); + } - // If a transition is interrupted, set this to false. At the moment, the only thing that would - // interrupt a transition is another transition, so that it will momentarily be set to true - // again, but this determines whether autorange or dragbox work, so it's for the sake of - // cleanliness: - gd._transitioning = false; + function interruptPreviousTransitions() { + // Fail-safe against purged plot: + if (!gd._transitionData) return; - return executeCallbacks(gd._transitionData._interruptCallbacks); - } + // If a transition is interrupted, set this to false. At the moment, the only thing that would + // interrupt a transition is another transition, so that it will momentarily be set to true + // again, but this determines whether autorange or dragbox work, so it's for the sake of + // cleanliness: + gd._transitioning = false; - for(i = 0; i < traceIndices.length; i++) { - traceIdx = traceIndices[i]; - var contFull = gd._fullData[traceIdx]; - var module = contFull._module; + return executeCallbacks(gd._transitionData._interruptCallbacks); + } - if(!module) continue; + for (i = 0; i < traceIndices.length; i++) { + traceIdx = traceIndices[i]; + var contFull = gd._fullData[traceIdx]; + var module = contFull._module; - if(!module.animatable) { - var thisUpdate = {}; + if (!module) continue; - for(var ai in data[i]) { - thisUpdate[ai] = [data[i][ai]]; - } - } + if (!module.animatable) { + var thisUpdate = {}; + + for (var ai in data[i]) { + thisUpdate[ai] = [data[i][ai]]; + } } + } - var seq = [plots.previousPromises, interruptPreviousTransitions, prepareTransitions, executeTransitions]; + var seq = [ + plots.previousPromises, + interruptPreviousTransitions, + prepareTransitions, + executeTransitions + ]; - var transitionStarting = Lib.syncOrAsync(seq, gd); + var transitionStarting = Lib.syncOrAsync(seq, gd); - if(!transitionStarting || !transitionStarting.then) { - transitionStarting = Promise.resolve(); - } + if (!transitionStarting || !transitionStarting.then) { + transitionStarting = Promise.resolve(); + } - return transitionStarting.then(function() { - return gd; - }); + return transitionStarting.then(function() { + return gd; + }); }; plots.doCalcdata = function(gd, traces) { - var axList = Plotly.Axes.list(gd), - fullData = gd._fullData, - fullLayout = gd._fullLayout; + var axList = Plotly.Axes.list(gd), + fullData = gd._fullData, + fullLayout = gd._fullLayout; - var trace, _module, i, j; + var trace, _module, i, j; - // XXX: Is this correct? Needs a closer look so that *some* traces can be recomputed without - // *all* needing doCalcdata: - var calcdata = new Array(fullData.length); - var oldCalcdata = (gd.calcdata || []).slice(0); - gd.calcdata = calcdata; + // XXX: Is this correct? Needs a closer look so that *some* traces can be recomputed without + // *all* needing doCalcdata: + var calcdata = new Array(fullData.length); + var oldCalcdata = (gd.calcdata || []).slice(0); + gd.calcdata = calcdata; - // extra helper variables - // firstscatter: fill-to-next on the first trace goes to zero - gd.firstscatter = true; + // extra helper variables + // firstscatter: fill-to-next on the first trace goes to zero + gd.firstscatter = true; - // how many box plots do we have (in case they're grouped) - gd.numboxes = 0; + // how many box plots do we have (in case they're grouped) + gd.numboxes = 0; - // for calculating avg luminosity of heatmaps - gd._hmpixcount = 0; - gd._hmlumcount = 0; + // for calculating avg luminosity of heatmaps + gd._hmpixcount = 0; + gd._hmlumcount = 0; - // for sharing colors across pies (and for legend) - fullLayout._piecolormap = {}; - fullLayout._piedefaultcolorcount = 0; + // for sharing colors across pies (and for legend) + fullLayout._piecolormap = {}; + fullLayout._piedefaultcolorcount = 0; - // initialize the category list, if there is one, so we start over - // to be filled in later by ax.d2c - for(i = 0; i < axList.length; i++) { - axList[i]._categories = axList[i]._initialCategories.slice(); - } + // initialize the category list, if there is one, so we start over + // to be filled in later by ax.d2c + for (i = 0; i < axList.length; i++) { + axList[i]._categories = axList[i]._initialCategories.slice(); + } - // If traces were specified and this trace was not included, - // then transfer it over from the old calcdata: - for(i = 0; i < fullData.length; i++) { - if(Array.isArray(traces) && traces.indexOf(i) === -1) { - calcdata[i] = oldCalcdata[i]; - continue; - } + // If traces were specified and this trace was not included, + // then transfer it over from the old calcdata: + for (i = 0; i < fullData.length; i++) { + if (Array.isArray(traces) && traces.indexOf(i) === -1) { + calcdata[i] = oldCalcdata[i]; + continue; } + } - var hasCalcTransform = false; + var hasCalcTransform = false; - // transform loop - for(i = 0; i < fullData.length; i++) { - trace = fullData[i]; + // transform loop + for (i = 0; i < fullData.length; i++) { + trace = fullData[i]; - if(trace.visible === true && trace.transforms) { - _module = trace._module; + if (trace.visible === true && trace.transforms) { + _module = trace._module; - // we need one round of trace module calc before - // the calc transform to 'fill in' the categories list - // used for example in the data-to-coordinate method - if(_module && _module.calc) _module.calc(gd, trace); + // we need one round of trace module calc before + // the calc transform to 'fill in' the categories list + // used for example in the data-to-coordinate method + if (_module && _module.calc) _module.calc(gd, trace); - for(j = 0; j < trace.transforms.length; j++) { - var transform = trace.transforms[j]; + for (j = 0; j < trace.transforms.length; j++) { + var transform = trace.transforms[j]; - _module = transformsRegistry[transform.type]; - if(_module && _module.calcTransform) { - hasCalcTransform = true; - _module.calcTransform(gd, trace, transform); - } - } + _module = transformsRegistry[transform.type]; + if (_module && _module.calcTransform) { + hasCalcTransform = true; + _module.calcTransform(gd, trace, transform); } + } } + } - // clear stuff that should recomputed in 'regular' loop - if(hasCalcTransform) { - for(i = 0; i < axList.length; i++) { - axList[i]._min = []; - axList[i]._max = []; - axList[i]._categories = []; - } + // clear stuff that should recomputed in 'regular' loop + if (hasCalcTransform) { + for (i = 0; i < axList.length; i++) { + axList[i]._min = []; + axList[i]._max = []; + axList[i]._categories = []; } + } - // 'regular' loop - for(i = 0; i < fullData.length; i++) { - var cd = []; + // 'regular' loop + for (i = 0; i < fullData.length; i++) { + var cd = []; - trace = fullData[i]; + trace = fullData[i]; - if(trace.visible === true) { - _module = trace._module; - if(_module && _module.calc) cd = _module.calc(gd, trace); - } + if (trace.visible === true) { + _module = trace._module; + if (_module && _module.calc) cd = _module.calc(gd, trace); + } - // Make sure there is a first point. - // - // This ensures there is a calcdata item for every trace, - // even if cartesian logic doesn't handle it (for things like legends). - // - // Tag this artificial calc point with 'placeholder: true', - // to make it easier to skip over them in during the plot and hover step. - if(!Array.isArray(cd) || !cd[0]) { - cd = [{x: false, y: false, placeholder: true}]; - } + // Make sure there is a first point. + // + // This ensures there is a calcdata item for every trace, + // even if cartesian logic doesn't handle it (for things like legends). + // + // Tag this artificial calc point with 'placeholder: true', + // to make it easier to skip over them in during the plot and hover step. + if (!Array.isArray(cd) || !cd[0]) { + cd = [{ x: false, y: false, placeholder: true }]; + } - // add the trace-wide properties to the first point, - // per point properties to every point - // t is the holder for trace-wide properties - if(!cd[0].t) cd[0].t = {}; - cd[0].trace = trace; + // add the trace-wide properties to the first point, + // per point properties to every point + // t is the holder for trace-wide properties + if (!cd[0].t) cd[0].t = {}; + cd[0].trace = trace; - calcdata[i] = cd; - } + calcdata[i] = cd; + } }; -plots.generalUpdatePerTraceModule = function(subplot, subplotCalcData, subplotLayout) { - var traceHashOld = subplot.traceHash, - traceHash = {}, - i; - - function filterVisible(calcDataIn) { - var calcDataOut = []; +plots.generalUpdatePerTraceModule = function( + subplot, + subplotCalcData, + subplotLayout +) { + var traceHashOld = subplot.traceHash, traceHash = {}, i; - for(var i = 0; i < calcDataIn.length; i++) { - var calcTrace = calcDataIn[i], - trace = calcTrace[0].trace; + function filterVisible(calcDataIn) { + var calcDataOut = []; - if(trace.visible === true) calcDataOut.push(calcTrace); - } + for (var i = 0; i < calcDataIn.length; i++) { + var calcTrace = calcDataIn[i], trace = calcTrace[0].trace; - return calcDataOut; + if (trace.visible === true) calcDataOut.push(calcTrace); } - // build up moduleName -> calcData hash - for(i = 0; i < subplotCalcData.length; i++) { - var calcTraces = subplotCalcData[i], - trace = calcTraces[0].trace; + return calcDataOut; + } - // skip over visible === false traces - // as they don't have `_module` ref - if(trace.visible) { - traceHash[trace.type] = traceHash[trace.type] || []; - traceHash[trace.type].push(calcTraces); - } + // build up moduleName -> calcData hash + for (i = 0; i < subplotCalcData.length; i++) { + var calcTraces = subplotCalcData[i], trace = calcTraces[0].trace; + + // skip over visible === false traces + // as they don't have `_module` ref + if (trace.visible) { + traceHash[trace.type] = traceHash[trace.type] || []; + traceHash[trace.type].push(calcTraces); } + } - var moduleNamesOld = Object.keys(traceHashOld); - var moduleNames = Object.keys(traceHash); + var moduleNamesOld = Object.keys(traceHashOld); + var moduleNames = Object.keys(traceHash); - // when a trace gets deleted, make sure that its module's - // plot method is called so that it is properly - // removed from the DOM. - for(i = 0; i < moduleNamesOld.length; i++) { - var moduleName = moduleNamesOld[i]; + // when a trace gets deleted, make sure that its module's + // plot method is called so that it is properly + // removed from the DOM. + for (i = 0; i < moduleNamesOld.length; i++) { + var moduleName = moduleNamesOld[i]; - if(moduleNames.indexOf(moduleName) === -1) { - var fakeCalcTrace = traceHashOld[moduleName][0], - fakeTrace = fakeCalcTrace[0].trace; + if (moduleNames.indexOf(moduleName) === -1) { + var fakeCalcTrace = traceHashOld[moduleName][0], + fakeTrace = fakeCalcTrace[0].trace; - fakeTrace.visible = false; - traceHash[moduleName] = [fakeCalcTrace]; - } + fakeTrace.visible = false; + traceHash[moduleName] = [fakeCalcTrace]; } + } - // update list of module names to include 'fake' traces added above - moduleNames = Object.keys(traceHash); + // update list of module names to include 'fake' traces added above + moduleNames = Object.keys(traceHash); - // call module plot method - for(i = 0; i < moduleNames.length; i++) { - var moduleCalcData = traceHash[moduleNames[i]], - _module = moduleCalcData[0][0].trace._module; + // call module plot method + for (i = 0; i < moduleNames.length; i++) { + var moduleCalcData = traceHash[moduleNames[i]], + _module = moduleCalcData[0][0].trace._module; - _module.plot(subplot, filterVisible(moduleCalcData), subplotLayout); - } + _module.plot(subplot, filterVisible(moduleCalcData), subplotLayout); + } - // update moduleName -> calcData hash - subplot.traceHash = traceHash; + // update moduleName -> calcData hash + subplot.traceHash = traceHash; }; diff --git a/src/plots/polar/area_attributes.js b/src/plots/polar/area_attributes.js index 4ae8b7edae9..68b832b4589 100644 --- a/src/plots/polar/area_attributes.js +++ b/src/plots/polar/area_attributes.js @@ -5,19 +5,17 @@ * 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 scatterAttrs = require('../../traces/scatter/attributes'); +"use strict"; +var scatterAttrs = require("../../traces/scatter/attributes"); var scatterMarkerAttrs = scatterAttrs.marker; module.exports = { - r: scatterAttrs.r, - t: scatterAttrs.t, - marker: { - color: scatterMarkerAttrs.color, - size: scatterMarkerAttrs.size, - symbol: scatterMarkerAttrs.symbol, - opacity: scatterMarkerAttrs.opacity - } + r: scatterAttrs.r, + t: scatterAttrs.t, + marker: { + color: scatterMarkerAttrs.color, + size: scatterMarkerAttrs.size, + symbol: scatterMarkerAttrs.symbol, + opacity: scatterMarkerAttrs.opacity + } }; diff --git a/src/plots/polar/axis_attributes.js b/src/plots/polar/axis_attributes.js index 681763d90b6..b508c07fe33 100644 --- a/src/plots/polar/axis_attributes.js +++ b/src/plots/polar/axis_attributes.js @@ -5,144 +5,140 @@ * 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 axesAttrs = require('../cartesian/layout_attributes'); -var extendFlat = require('../../lib/extend').extendFlat; +"use strict"; +var axesAttrs = require("../cartesian/layout_attributes"); +var extendFlat = require("../../lib/extend").extendFlat; var domainAttr = extendFlat({}, axesAttrs.domain, { - description: [ - 'Polar chart subplots are not supported yet.', - 'This key has currently no effect.' - ].join(' ') + description: [ + "Polar chart subplots are not supported yet.", + "This key has currently no effect." + ].join(" ") }); function mergeAttrs(axisName, nonCommonAttrs) { - var commonAttrs = { - showline: { - valType: 'boolean', - role: 'style', - description: [ - 'Determines whether or not the line bounding this', - axisName, 'axis', - 'will be shown on the figure.' - ].join(' ') - }, - showticklabels: { - valType: 'boolean', - role: 'style', - description: [ - 'Determines whether or not the', - axisName, 'axis ticks', - 'will feature tick labels.' - ].join(' ') - }, - tickorientation: { - valType: 'enumerated', - values: ['horizontal', 'vertical'], - role: 'style', - description: [ - 'Sets the orientation (from the paper perspective)', - 'of the', axisName, 'axis tick labels.' - ].join(' ') - }, - ticklen: { - valType: 'number', - min: 0, - role: 'style', - description: [ - 'Sets the length of the tick lines on this', axisName, 'axis.' - ].join(' ') - }, - tickcolor: { - valType: 'color', - role: 'style', - description: [ - 'Sets the color of the tick lines on this', axisName, 'axis.' - ].join(' ') - }, - ticksuffix: { - valType: 'string', - role: 'style', - description: [ - 'Sets the length of the tick lines on this', axisName, 'axis.' - ].join(' ') - }, - endpadding: { - valType: 'number', - role: 'style' - }, - visible: { - valType: 'boolean', - role: 'info', - description: [ - 'Determines whether or not this axis will be visible.' - ].join(' ') - } - }; + var commonAttrs = { + showline: { + valType: "boolean", + role: "style", + description: [ + "Determines whether or not the line bounding this", + axisName, + "axis", + "will be shown on the figure." + ].join(" ") + }, + showticklabels: { + valType: "boolean", + role: "style", + description: [ + "Determines whether or not the", + axisName, + "axis ticks", + "will feature tick labels." + ].join(" ") + }, + tickorientation: { + valType: "enumerated", + values: ["horizontal", "vertical"], + role: "style", + description: [ + "Sets the orientation (from the paper perspective)", + "of the", + axisName, + "axis tick labels." + ].join(" ") + }, + ticklen: { + valType: "number", + min: 0, + role: "style", + description: [ + "Sets the length of the tick lines on this", + axisName, + "axis." + ].join(" ") + }, + tickcolor: { + valType: "color", + role: "style", + description: [ + "Sets the color of the tick lines on this", + axisName, + "axis." + ].join(" ") + }, + ticksuffix: { + valType: "string", + role: "style", + description: [ + "Sets the length of the tick lines on this", + axisName, + "axis." + ].join(" ") + }, + endpadding: { valType: "number", role: "style" }, + visible: { + valType: "boolean", + role: "info", + description: [ + "Determines whether or not this axis will be visible." + ].join(" ") + } + }; - return extendFlat({}, nonCommonAttrs, commonAttrs); + return extendFlat({}, nonCommonAttrs, commonAttrs); } module.exports = { - radialaxis: mergeAttrs('radial', { - range: { - valType: 'info_array', - role: 'info', - items: [ - { valType: 'number' }, - { valType: 'number' } - ], - description: [ - 'Defines the start and end point of this radial axis.' - ].join(' ') - }, - domain: domainAttr, - orientation: { - valType: 'number', - role: 'style', - description: [ - 'Sets the orientation (an angle with respect to the origin)', - 'of the radial axis.' - ].join(' ') - } - }), - - angularaxis: mergeAttrs('angular', { - range: { - valType: 'info_array', - role: 'info', - items: [ - { valType: 'number', dflt: 0 }, - { valType: 'number', dflt: 360 } - ], - description: [ - 'Defines the start and end point of this angular axis.' - ].join(' ') - }, - domain: domainAttr - }), - - // attributes that appear at layout root - layout: { - direction: { - valType: 'enumerated', - values: ['clockwise', 'counterclockwise'], - role: 'info', - description: [ - 'For polar plots only.', - 'Sets the direction corresponding to positive angles.' - ].join(' ') - }, - orientation: { - valType: 'angle', - role: 'info', - description: [ - 'For polar plots only.', - 'Rotates the entire polar by the given angle.' - ].join(' ') - } + radialaxis: mergeAttrs("radial", { + range: { + valType: "info_array", + role: "info", + items: [{ valType: "number" }, { valType: "number" }], + description: [ + "Defines the start and end point of this radial axis." + ].join(" ") + }, + domain: domainAttr, + orientation: { + valType: "number", + role: "style", + description: [ + "Sets the orientation (an angle with respect to the origin)", + "of the radial axis." + ].join(" ") + } + }), + angularaxis: mergeAttrs("angular", { + range: { + valType: "info_array", + role: "info", + items: [{ valType: "number", dflt: 0 }, { valType: "number", dflt: 360 }], + description: [ + "Defines the start and end point of this angular axis." + ].join(" ") + }, + domain: domainAttr + }), + // attributes that appear at layout root + layout: { + direction: { + valType: "enumerated", + values: ["clockwise", "counterclockwise"], + role: "info", + description: [ + "For polar plots only.", + "Sets the direction corresponding to positive angles." + ].join(" ") + }, + orientation: { + valType: "angle", + role: "info", + description: [ + "For polar plots only.", + "Rotates the entire polar by the given angle." + ].join(" ") } + } }; diff --git a/src/plots/polar/index.js b/src/plots/polar/index.js index 75ce1c99adb..5adbb1fe3e9 100644 --- a/src/plots/polar/index.js +++ b/src/plots/polar/index.js @@ -5,9 +5,7 @@ * 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 Polar = module.exports = require("./micropolar"); -'use strict'; - -var Polar = module.exports = require('./micropolar'); - -Polar.manager = require('./micropolar_manager'); +Polar.manager = require("./micropolar_manager"); diff --git a/src/plots/polar/micropolar.js b/src/plots/polar/micropolar.js index 208da6995b5..11d929d36a9 100644 --- a/src/plots/polar/micropolar.js +++ b/src/plots/polar/micropolar.js @@ -6,1262 +6,1478 @@ * LICENSE file in the root directory of this source tree. */ -var d3 = require('d3'); -var Lib = require('../../lib'); +var d3 = require("d3"); +var Lib = require("../../lib"); var extendDeepAll = Lib.extendDeepAll; -var µ = module.exports = { version: '0.2.2' }; +var µ = module.exports = { version: "0.2.2" }; µ.Axis = function module() { - var config = { - data: [], - layout: {} - }, inputConfig = {}, liveConfig = {}; - var svg, container, dispatch = d3.dispatch('hover'), radialScale, angularScale; - var exports = {}; - function render(_container) { - container = _container || container; - var data = config.data; - var axisConfig = config.layout; - if (typeof container == 'string' || container.nodeName) container = d3.select(container); - container.datum(data).each(function(_data, _index) { - var dataOriginal = _data.slice(); - liveConfig = { - data: µ.util.cloneJson(dataOriginal), - layout: µ.util.cloneJson(axisConfig) - }; - var colorIndex = 0; - dataOriginal.forEach(function(d, i) { - if (!d.color) { - d.color = axisConfig.defaultColorRange[colorIndex]; - colorIndex = (colorIndex + 1) % axisConfig.defaultColorRange.length; - } - if (!d.strokeColor) { - d.strokeColor = d.geometry === 'LinePlot' ? d.color : d3.rgb(d.color).darker().toString(); - } - liveConfig.data[i].color = d.color; - liveConfig.data[i].strokeColor = d.strokeColor; - liveConfig.data[i].strokeDash = d.strokeDash; - liveConfig.data[i].strokeSize = d.strokeSize; - }); - var data = dataOriginal.filter(function(d, i) { - var visible = d.visible; - return typeof visible === 'undefined' || visible === true; - }); - var isStacked = false; - var dataWithGroupId = data.map(function(d, i) { - isStacked = isStacked || typeof d.groupId !== 'undefined'; - return d; - }); - if (isStacked) { - var grouped = d3.nest().key(function(d, i) { - return typeof d.groupId != 'undefined' ? d.groupId : 'unstacked'; - }).entries(dataWithGroupId); - var dataYStack = []; - var stacked = grouped.map(function(d, i) { - if (d.key === 'unstacked') return d.values; else { - var prevArray = d.values[0].r.map(function(d, i) { - return 0; - }); - d.values.forEach(function(d, i, a) { - d.yStack = [ prevArray ]; - dataYStack.push(prevArray); - prevArray = µ.util.sumArrays(d.r, prevArray); - }); - return d.values; - } - }); - data = d3.merge(stacked); - } - data.forEach(function(d, i) { - d.t = Array.isArray(d.t[0]) ? d.t : [ d.t ]; - d.r = Array.isArray(d.r[0]) ? d.r : [ d.r ]; - }); - var radius = Math.min(axisConfig.width - axisConfig.margin.left - axisConfig.margin.right, axisConfig.height - axisConfig.margin.top - axisConfig.margin.bottom) / 2; - radius = Math.max(10, radius); - var chartCenter = [ axisConfig.margin.left + radius, axisConfig.margin.top + radius ]; - var extent; - if (isStacked) { - var highestStackedValue = d3.max(µ.util.sumArrays(µ.util.arrayLast(data).r[0], µ.util.arrayLast(dataYStack))); - extent = [ 0, highestStackedValue ]; - } else extent = d3.extent(µ.util.flattenArray(data.map(function(d, i) { - return d.r; - }))); - if (axisConfig.radialAxis.domain != µ.DATAEXTENT) extent[0] = 0; - radialScale = d3.scale.linear().domain(axisConfig.radialAxis.domain != µ.DATAEXTENT && axisConfig.radialAxis.domain ? axisConfig.radialAxis.domain : extent).range([ 0, radius ]); - liveConfig.layout.radialAxis.domain = radialScale.domain(); - var angularDataMerged = µ.util.flattenArray(data.map(function(d, i) { - return d.t; - })); - var isOrdinal = typeof angularDataMerged[0] === 'string'; - var ticks; - if (isOrdinal) { - angularDataMerged = µ.util.deduplicate(angularDataMerged); - ticks = angularDataMerged.slice(); - angularDataMerged = d3.range(angularDataMerged.length); - data = data.map(function(d, i) { - var result = d; - d.t = [ angularDataMerged ]; - if (isStacked) result.yStack = d.yStack; - return result; - }); - } - var hasOnlyLineOrDotPlot = data.filter(function(d, i) { - return d.geometry === 'LinePlot' || d.geometry === 'DotPlot'; - }).length === data.length; - var needsEndSpacing = axisConfig.needsEndSpacing === null ? isOrdinal || !hasOnlyLineOrDotPlot : axisConfig.needsEndSpacing; - var useProvidedDomain = axisConfig.angularAxis.domain && axisConfig.angularAxis.domain != µ.DATAEXTENT && !isOrdinal && axisConfig.angularAxis.domain[0] >= 0; - var angularDomain = useProvidedDomain ? axisConfig.angularAxis.domain : d3.extent(angularDataMerged); - var angularDomainStep = Math.abs(angularDataMerged[1] - angularDataMerged[0]); - if (hasOnlyLineOrDotPlot && !isOrdinal) angularDomainStep = 0; - var angularDomainWithPadding = angularDomain.slice(); - if (needsEndSpacing && isOrdinal) angularDomainWithPadding[1] += angularDomainStep; - var tickCount = axisConfig.angularAxis.ticksCount || 4; - if (tickCount > 8) tickCount = tickCount / (tickCount / 8) + tickCount % 8; - if (axisConfig.angularAxis.ticksStep) { - tickCount = (angularDomainWithPadding[1] - angularDomainWithPadding[0]) / tickCount; - } - var angularTicksStep = axisConfig.angularAxis.ticksStep || (angularDomainWithPadding[1] - angularDomainWithPadding[0]) / (tickCount * (axisConfig.minorTicks + 1)); - if (ticks) angularTicksStep = Math.max(Math.round(angularTicksStep), 1); - if (!angularDomainWithPadding[2]) angularDomainWithPadding[2] = angularTicksStep; - var angularAxisRange = d3.range.apply(this, angularDomainWithPadding); - angularAxisRange = angularAxisRange.map(function(d, i) { - return parseFloat(d.toPrecision(12)); - }); - angularScale = d3.scale.linear().domain(angularDomainWithPadding.slice(0, 2)).range(axisConfig.direction === 'clockwise' ? [ 0, 360 ] : [ 360, 0 ]); - liveConfig.layout.angularAxis.domain = angularScale.domain(); - liveConfig.layout.angularAxis.endPadding = needsEndSpacing ? angularDomainStep : 0; - svg = d3.select(this).select('svg.chart-root'); - if (typeof svg === 'undefined' || svg.empty()) { - var skeleton = "' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '"; - var doc = new DOMParser().parseFromString(skeleton, 'application/xml'); - var newSvg = this.appendChild(this.ownerDocument.importNode(doc.documentElement, true)); - svg = d3.select(newSvg); - } - svg.select('.guides-group').style({ - 'pointer-events': 'none' - }); - svg.select('.angular.axis-group').style({ - 'pointer-events': 'none' + var config = { data: [], layout: {} }, inputConfig = {}, liveConfig = {}; + var svg, + container, + dispatch = d3.dispatch("hover"), + radialScale, + angularScale; + var exports = {}; + function render(_container) { + container = _container || container; + var data = config.data; + var axisConfig = config.layout; + if (typeof container === "string" || container.nodeName) { + container = d3.select(container); + } + container.datum(data).each(function(_data, _index) { + var dataOriginal = _data.slice(); + liveConfig = { + data: µ.util.cloneJson(dataOriginal), + layout: µ.util.cloneJson(axisConfig) + }; + var colorIndex = 0; + dataOriginal.forEach(function(d, i) { + if (!d.color) { + d.color = axisConfig.defaultColorRange[colorIndex]; + colorIndex = (colorIndex + 1) % axisConfig.defaultColorRange.length; + } + if (!d.strokeColor) { + d.strokeColor = d.geometry === "LinePlot" + ? d.color + : d3.rgb(d.color).darker().toString(); + } + liveConfig.data[i].color = d.color; + liveConfig.data[i].strokeColor = d.strokeColor; + liveConfig.data[i].strokeDash = d.strokeDash; + liveConfig.data[i].strokeSize = d.strokeSize; + }); + var data = dataOriginal.filter(function(d, i) { + var visible = d.visible; + return typeof visible === "undefined" || visible === true; + }); + var isStacked = false; + var dataWithGroupId = data.map(function(d, i) { + isStacked = isStacked || typeof d.groupId !== "undefined"; + return d; + }); + if (isStacked) { + var grouped = d3 + .nest() + .key(function(d, i) { + return typeof d.groupId !== "undefined" ? d.groupId : "unstacked"; + }) + .entries(dataWithGroupId); + var dataYStack = []; + var stacked = grouped.map(function(d, i) { + if (d.key === "unstacked") { + return d.values; + } else { + var prevArray = d.values[0].r.map(function(d, i) { + return 0; }); - svg.select('.radial.axis-group').style({ - 'pointer-events': 'none' + d.values.forEach(function(d, i, a) { + d.yStack = [prevArray]; + dataYStack.push(prevArray); + prevArray = µ.util.sumArrays(d.r, prevArray); }); - var chartGroup = svg.select('.chart-group'); - var lineStyle = { - fill: 'none', - stroke: axisConfig.tickColor - }; - var fontStyle = { - 'font-size': axisConfig.font.size, - 'font-family': axisConfig.font.family, - fill: axisConfig.font.color, - 'text-shadow': [ '-1px 0px', '1px -1px', '-1px 1px', '1px 1px' ].map(function(d, i) { - return ' ' + d + ' 0 ' + axisConfig.font.outlineColor; - }).join(',') - }; - var legendContainer; - if (axisConfig.showLegend) { - legendContainer = svg.select('.legend-group').attr({ - transform: 'translate(' + [ radius, axisConfig.margin.top ] + ')' - }).style({ - display: 'block' - }); - var elements = data.map(function(d, i) { - var datumClone = µ.util.cloneJson(d); - datumClone.symbol = d.geometry === 'DotPlot' ? d.dotType || 'circle' : d.geometry != 'LinePlot' ? 'square' : 'line'; - datumClone.visibleInLegend = typeof d.visibleInLegend === 'undefined' || d.visibleInLegend; - datumClone.color = d.geometry === 'LinePlot' ? d.strokeColor : d.color; - return datumClone; - }); - - µ.Legend().config({ - data: data.map(function(d, i) { - return d.name || 'Element' + i; - }), - legendConfig: extendDeepAll({}, - µ.Legend.defaultConfig().legendConfig, - { - container: legendContainer, - elements: elements, - reverseOrder: axisConfig.legend.reverseOrder - } - ) - })(); + return d.values; + } + }); + data = d3.merge(stacked); + } + data.forEach(function(d, i) { + d.t = Array.isArray(d.t[0]) ? d.t : [d.t]; + d.r = Array.isArray(d.r[0]) ? d.r : [d.r]; + }); + var radius = Math.min( + axisConfig.width - axisConfig.margin.left - axisConfig.margin.right, + axisConfig.height - axisConfig.margin.top - axisConfig.margin.bottom + ) / + 2; + radius = Math.max(10, radius); + var chartCenter = [ + axisConfig.margin.left + radius, + axisConfig.margin.top + radius + ]; + var extent; + if (isStacked) { + var highestStackedValue = d3.max( + µ.util.sumArrays( + µ.util.arrayLast(data).r[0], + µ.util.arrayLast(dataYStack) + ) + ); + extent = [0, highestStackedValue]; + } else { + extent = d3.extent( + µ.util.flattenArray( + data.map(function(d, i) { + return d.r; + }) + ) + ); + } + if (axisConfig.radialAxis.domain != µ.DATAEXTENT) extent[0] = 0; + radialScale = d3.scale + .linear() + .domain( + axisConfig.radialAxis.domain != µ.DATAEXTENT && + axisConfig.radialAxis.domain + ? axisConfig.radialAxis.domain + : extent + ) + .range([0, radius]); + liveConfig.layout.radialAxis.domain = radialScale.domain(); + var angularDataMerged = µ.util.flattenArray( + data.map(function(d, i) { + return d.t; + }) + ); + var isOrdinal = typeof angularDataMerged[0] === "string"; + var ticks; + if (isOrdinal) { + angularDataMerged = µ.util.deduplicate(angularDataMerged); + ticks = angularDataMerged.slice(); + angularDataMerged = d3.range(angularDataMerged.length); + data = data.map(function(d, i) { + var result = d; + d.t = [angularDataMerged]; + if (isStacked) result.yStack = d.yStack; + return result; + }); + } + var hasOnlyLineOrDotPlot = data.filter(function(d, i) { + return d.geometry === "LinePlot" || d.geometry === "DotPlot"; + }).length === + data.length; + var needsEndSpacing = axisConfig.needsEndSpacing === null + ? isOrdinal || !hasOnlyLineOrDotPlot + : axisConfig.needsEndSpacing; + var useProvidedDomain = axisConfig.angularAxis.domain && + axisConfig.angularAxis.domain != µ.DATAEXTENT && + !isOrdinal && + axisConfig.angularAxis.domain[0] >= 0; + var angularDomain = useProvidedDomain + ? axisConfig.angularAxis.domain + : d3.extent(angularDataMerged); + var angularDomainStep = Math.abs( + angularDataMerged[1] - angularDataMerged[0] + ); + if (hasOnlyLineOrDotPlot && !isOrdinal) angularDomainStep = 0; + var angularDomainWithPadding = angularDomain.slice(); + if (needsEndSpacing && isOrdinal) { + angularDomainWithPadding[1] += angularDomainStep; + } + var tickCount = axisConfig.angularAxis.ticksCount || 4; + if (tickCount > 8) { + tickCount = tickCount / (tickCount / 8) + tickCount % 8; + } + if (axisConfig.angularAxis.ticksStep) { + tickCount = (angularDomainWithPadding[1] - + angularDomainWithPadding[0]) / + tickCount; + } + var angularTicksStep = axisConfig.angularAxis.ticksStep || + (angularDomainWithPadding[1] - angularDomainWithPadding[0]) / + (tickCount * (axisConfig.minorTicks + 1)); + if (ticks) angularTicksStep = Math.max(Math.round(angularTicksStep), 1); + if (!angularDomainWithPadding[2]) { + angularDomainWithPadding[2] = angularTicksStep; + } + var angularAxisRange = d3.range.apply(this, angularDomainWithPadding); + angularAxisRange = angularAxisRange.map(function(d, i) { + return parseFloat(d.toPrecision(12)); + }); + angularScale = d3.scale + .linear() + .domain(angularDomainWithPadding.slice(0, 2)) + .range(axisConfig.direction === "clockwise" ? [0, 360] : [360, 0]); + liveConfig.layout.angularAxis.domain = angularScale.domain(); + liveConfig.layout.angularAxis.endPadding = needsEndSpacing + ? angularDomainStep + : 0; + svg = d3.select(this).select("svg.chart-root"); + if (typeof svg === "undefined" || svg.empty()) { + var skeleton = "' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '"; + var doc = new DOMParser().parseFromString(skeleton, "application/xml"); + var newSvg = this.appendChild( + this.ownerDocument.importNode(doc.documentElement, true) + ); + svg = d3.select(newSvg); + } + svg.select(".guides-group").style({ "pointer-events": "none" }); + svg.select(".angular.axis-group").style({ "pointer-events": "none" }); + svg.select(".radial.axis-group").style({ "pointer-events": "none" }); + var chartGroup = svg.select(".chart-group"); + var lineStyle = { fill: "none", stroke: axisConfig.tickColor }; + var fontStyle = { + "font-size": axisConfig.font.size, + "font-family": axisConfig.font.family, + fill: axisConfig.font.color, + "text-shadow": ["-1px 0px", "1px -1px", "-1px 1px", "1px 1px"] + .map(function(d, i) { + return " " + d + " 0 " + axisConfig.font.outlineColor; + }) + .join(",") + }; + var legendContainer; + if (axisConfig.showLegend) { + legendContainer = svg + .select(".legend-group") + .attr({ + transform: "translate(" + [radius, axisConfig.margin.top] + ")" + }) + .style({ display: "block" }); + var elements = data.map(function(d, i) { + var datumClone = µ.util.cloneJson(d); + datumClone.symbol = d.geometry === "DotPlot" + ? d.dotType || "circle" + : d.geometry != "LinePlot" ? "square" : "line"; + datumClone.visibleInLegend = typeof d.visibleInLegend === + "undefined" || + d.visibleInLegend; + datumClone.color = d.geometry === "LinePlot" + ? d.strokeColor + : d.color; + return datumClone; + }); - var legendBBox = legendContainer.node().getBBox(); - radius = Math.min(axisConfig.width - legendBBox.width - axisConfig.margin.left - axisConfig.margin.right, axisConfig.height - axisConfig.margin.top - axisConfig.margin.bottom) / 2; - radius = Math.max(10, radius); - chartCenter = [ axisConfig.margin.left + radius, axisConfig.margin.top + radius ]; - radialScale.range([ 0, radius ]); - liveConfig.layout.radialAxis.domain = radialScale.domain(); - legendContainer.attr('transform', 'translate(' + [ chartCenter[0] + radius, chartCenter[1] - radius ] + ')'); - } else { - legendContainer = svg.select('.legend-group').style({ - display: 'none' - }); - } - svg.attr({ - width: axisConfig.width, - height: axisConfig.height - }).style({ - opacity: axisConfig.opacity - }); - chartGroup.attr('transform', 'translate(' + chartCenter + ')').style({ - cursor: 'crosshair' - }); - var centeringOffset = [ (axisConfig.width - (axisConfig.margin.left + axisConfig.margin.right + radius * 2 + (legendBBox ? legendBBox.width : 0))) / 2, (axisConfig.height - (axisConfig.margin.top + axisConfig.margin.bottom + radius * 2)) / 2 ]; - centeringOffset[0] = Math.max(0, centeringOffset[0]); - centeringOffset[1] = Math.max(0, centeringOffset[1]); - svg.select('.outer-group').attr('transform', 'translate(' + centeringOffset + ')'); - if (axisConfig.title) { - var title = svg.select('g.title-group text').style(fontStyle).text(axisConfig.title); - var titleBBox = title.node().getBBox(); - title.attr({ - x: chartCenter[0] - titleBBox.width / 2, - y: chartCenter[1] - radius - 20 - }); - } - var radialAxis = svg.select('.radial.axis-group'); - if (axisConfig.radialAxis.gridLinesVisible) { - var gridCircles = radialAxis.selectAll('circle.grid-circle').data(radialScale.ticks(5)); - gridCircles.enter().append('circle').attr({ - 'class': 'grid-circle' - }).style(lineStyle); - gridCircles.attr('r', radialScale); - gridCircles.exit().remove(); - } - radialAxis.select('circle.outside-circle').attr({ - r: radius - }).style(lineStyle); - var backgroundCircle = svg.select('circle.background-circle').attr({ - r: radius - }).style({ - fill: axisConfig.backgroundColor, - stroke: axisConfig.stroke - }); - function currentAngle(d, i) { - return angularScale(d) % 360 + axisConfig.orientation; - } - if (axisConfig.radialAxis.visible) { - var axis = d3.svg.axis().scale(radialScale).ticks(5).tickSize(5); - radialAxis.call(axis).attr({ - transform: 'rotate(' + axisConfig.radialAxis.orientation + ')' - }); - radialAxis.selectAll('.domain').style(lineStyle); - radialAxis.selectAll('g>text').text(function(d, i) { - return this.textContent + axisConfig.radialAxis.ticksSuffix; - }).style(fontStyle).style({ - 'text-anchor': 'start' - }).attr({ - x: 0, - y: 0, - dx: 0, - dy: 0, - transform: function(d, i) { - if (axisConfig.radialAxis.tickOrientation === 'horizontal') { - return 'rotate(' + -axisConfig.radialAxis.orientation + ') translate(' + [ 0, fontStyle['font-size'] ] + ')'; - } else return 'translate(' + [ 0, fontStyle['font-size'] ] + ')'; - } - }); - radialAxis.selectAll('g>line').style({ - stroke: 'black' - }); + µ.Legend().config({ + data: data.map(function(d, i) { + return d.name || "Element" + i; + }), + legendConfig: extendDeepAll( + {}, + µ.Legend.defaultConfig().legendConfig, + { + container: legendContainer, + elements: elements, + reverseOrder: axisConfig.legend.reverseOrder } - var angularAxis = svg.select('.angular.axis-group').selectAll('g.angular-tick').data(angularAxisRange); - var angularAxisEnter = angularAxis.enter().append('g').classed('angular-tick', true); - angularAxis.attr({ - transform: function(d, i) { - return 'rotate(' + currentAngle(d, i) + ')'; - } - }).style({ - display: axisConfig.angularAxis.visible ? 'block' : 'none' - }); - angularAxis.exit().remove(); - angularAxisEnter.append('line').classed('grid-line', true).classed('major', function(d, i) { - return i % (axisConfig.minorTicks + 1) == 0; - }).classed('minor', function(d, i) { - return !(i % (axisConfig.minorTicks + 1) == 0); - }).style(lineStyle); - angularAxisEnter.selectAll('.minor').style({ - stroke: axisConfig.minorTickColor - }); - angularAxis.select('line.grid-line').attr({ - x1: axisConfig.tickLength ? radius - axisConfig.tickLength : 0, - x2: radius - }).style({ - display: axisConfig.angularAxis.gridLinesVisible ? 'block' : 'none' - }); - angularAxisEnter.append('text').classed('axis-text', true).style(fontStyle); - var ticksText = angularAxis.select('text.axis-text').attr({ - x: radius + axisConfig.labelOffset, - dy: '.35em', - transform: function(d, i) { - var angle = currentAngle(d, i); - var rad = radius + axisConfig.labelOffset; - var orient = axisConfig.angularAxis.tickOrientation; - if (orient == 'horizontal') return 'rotate(' + -angle + ' ' + rad + ' 0)'; else if (orient == 'radial') return angle < 270 && angle > 90 ? 'rotate(180 ' + rad + ' 0)' : null; else return 'rotate(' + (angle <= 180 && angle > 0 ? -90 : 90) + ' ' + rad + ' 0)'; - } - }).style({ - 'text-anchor': 'middle', - display: axisConfig.angularAxis.labelsVisible ? 'block' : 'none' - }).text(function(d, i) { - if (i % (axisConfig.minorTicks + 1) != 0) return ''; - if (ticks) { - return ticks[d] + axisConfig.angularAxis.ticksSuffix; - } else return d + axisConfig.angularAxis.ticksSuffix; - }).style(fontStyle); - if (axisConfig.angularAxis.rewriteTicks) ticksText.text(function(d, i) { - if (i % (axisConfig.minorTicks + 1) != 0) return ''; - return axisConfig.angularAxis.rewriteTicks(this.textContent, i); - }); - var rightmostTickEndX = d3.max(chartGroup.selectAll('.angular-tick text')[0].map(function(d, i) { - return d.getCTM().e + d.getBBox().width; - })); - legendContainer.attr({ - transform: 'translate(' + [ radius + rightmostTickEndX, axisConfig.margin.top ] + ')' - }); - var hasGeometry = svg.select('g.geometry-group').selectAll('g').size() > 0; - var geometryContainer = svg.select('g.geometry-group').selectAll('g.geometry').data(data); - geometryContainer.enter().append('g').attr({ - 'class': function(d, i) { - return 'geometry geometry' + i; - } - }); - geometryContainer.exit().remove(); - if (data[0] || hasGeometry) { - var geometryConfigs = []; - data.forEach(function(d, i) { - var geometryConfig = {}; - geometryConfig.radialScale = radialScale; - geometryConfig.angularScale = angularScale; - geometryConfig.container = geometryContainer.filter(function(dB, iB) { - return iB == i; - }); - geometryConfig.geometry = d.geometry; - geometryConfig.orientation = axisConfig.orientation; - geometryConfig.direction = axisConfig.direction; - geometryConfig.index = i; - geometryConfigs.push({ - data: d, - geometryConfig: geometryConfig - }); - }); - var geometryConfigsGrouped = d3.nest().key(function(d, i) { - return typeof d.data.groupId != 'undefined' || 'unstacked'; - }).entries(geometryConfigs); - var geometryConfigsGrouped2 = []; - geometryConfigsGrouped.forEach(function(d, i) { - if (d.key === 'unstacked') geometryConfigsGrouped2 = geometryConfigsGrouped2.concat(d.values.map(function(d, i) { - return [ d ]; - })); else geometryConfigsGrouped2.push(d.values); - }); - geometryConfigsGrouped2.forEach(function(d, i) { - var geometry; - if (Array.isArray(d)) geometry = d[0].geometryConfig.geometry; else geometry = d.geometryConfig.geometry; - var finalGeometryConfig = d.map(function(dB, iB) { - return extendDeepAll(µ[geometry].defaultConfig(), dB); - }); - µ[geometry]().config(finalGeometryConfig)(); - }); + ) + })(); + + var legendBBox = legendContainer.node().getBBox(); + radius = Math.min( + axisConfig.width - + legendBBox.width - + axisConfig.margin.left - + axisConfig.margin.right, + axisConfig.height - axisConfig.margin.top - axisConfig.margin.bottom + ) / + 2; + radius = Math.max(10, radius); + chartCenter = [ + axisConfig.margin.left + radius, + axisConfig.margin.top + radius + ]; + radialScale.range([0, radius]); + liveConfig.layout.radialAxis.domain = radialScale.domain(); + legendContainer.attr( + "transform", + "translate(" + + [chartCenter[0] + radius, chartCenter[1] - radius] + + ")" + ); + } else { + legendContainer = svg + .select(".legend-group") + .style({ display: "none" }); + } + svg + .attr({ width: axisConfig.width, height: axisConfig.height }) + .style({ opacity: axisConfig.opacity }); + chartGroup + .attr("transform", "translate(" + chartCenter + ")") + .style({ cursor: "crosshair" }); + var centeringOffset = [ + (axisConfig.width - + (axisConfig.margin.left + + axisConfig.margin.right + + radius * 2 + + (legendBBox ? legendBBox.width : 0))) / + 2, + (axisConfig.height - + (axisConfig.margin.top + axisConfig.margin.bottom + radius * 2)) / + 2 + ]; + centeringOffset[0] = Math.max(0, centeringOffset[0]); + centeringOffset[1] = Math.max(0, centeringOffset[1]); + svg + .select(".outer-group") + .attr("transform", "translate(" + centeringOffset + ")"); + if (axisConfig.title) { + var title = svg + .select("g.title-group text") + .style(fontStyle) + .text(axisConfig.title); + var titleBBox = title.node().getBBox(); + title.attr({ + x: chartCenter[0] - titleBBox.width / 2, + y: chartCenter[1] - radius - 20 + }); + } + var radialAxis = svg.select(".radial.axis-group"); + if (axisConfig.radialAxis.gridLinesVisible) { + var gridCircles = radialAxis + .selectAll("circle.grid-circle") + .data(radialScale.ticks(5)); + gridCircles + .enter() + .append("circle") + .attr({ class: "grid-circle" }) + .style(lineStyle); + gridCircles.attr("r", radialScale); + gridCircles.exit().remove(); + } + radialAxis + .select("circle.outside-circle") + .attr({ r: radius }) + .style(lineStyle); + var backgroundCircle = svg + .select("circle.background-circle") + .attr({ r: radius }) + .style({ fill: axisConfig.backgroundColor, stroke: axisConfig.stroke }); + function currentAngle(d, i) { + return angularScale(d) % 360 + axisConfig.orientation; + } + if (axisConfig.radialAxis.visible) { + var axis = d3.svg.axis().scale(radialScale).ticks(5).tickSize(5); + radialAxis.call(axis).attr({ + transform: "rotate(" + axisConfig.radialAxis.orientation + ")" + }); + radialAxis.selectAll(".domain").style(lineStyle); + radialAxis + .selectAll("g>text") + .text(function(d, i) { + return this.textContent + axisConfig.radialAxis.ticksSuffix; + }) + .style(fontStyle) + .style({ "text-anchor": "start" }) + .attr({ + x: 0, + y: 0, + dx: 0, + dy: 0, + transform: function(d, i) { + if (axisConfig.radialAxis.tickOrientation === "horizontal") { + return "rotate(" + + (-axisConfig.radialAxis.orientation) + + ") translate(" + + [0, fontStyle["font-size"]] + + ")"; + } else { + return "translate(" + [0, fontStyle["font-size"]] + ")"; + } } - var guides = svg.select('.guides-group'); - var tooltipContainer = svg.select('.tooltips-group'); - var angularTooltip = µ.tooltipPanel().config({ - container: tooltipContainer, - fontSize: 8 - })(); - var radialTooltip = µ.tooltipPanel().config({ - container: tooltipContainer, - fontSize: 8 - })(); - var geometryTooltip = µ.tooltipPanel().config({ - container: tooltipContainer, - hasTick: true - })(); - var angularValue, radialValue; - if (!isOrdinal) { - var angularGuideLine = guides.select('line').attr({ - x1: 0, - y1: 0, - y2: 0 - }).style({ - stroke: 'grey', - 'pointer-events': 'none' - }); - chartGroup.on('mousemove.angular-guide', function(d, i) { - var mouseAngle = µ.util.getMousePos(backgroundCircle).angle; - angularGuideLine.attr({ - x2: -radius, - transform: 'rotate(' + mouseAngle + ')' - }).style({ - opacity: .5 - }); - var angleWithOriginOffset = (mouseAngle + 180 + 360 - axisConfig.orientation) % 360; - angularValue = angularScale.invert(angleWithOriginOffset); - var pos = µ.util.convertToCartesian(radius + 12, mouseAngle + 180); - angularTooltip.text(µ.util.round(angularValue)).move([ pos[0] + chartCenter[0], pos[1] + chartCenter[1] ]); - }).on('mouseout.angular-guide', function(d, i) { - guides.select('line').style({ - opacity: 0 - }); - }); + }); + radialAxis.selectAll("g>line").style({ stroke: "black" }); + } + var angularAxis = svg + .select(".angular.axis-group") + .selectAll("g.angular-tick") + .data(angularAxisRange); + var angularAxisEnter = angularAxis + .enter() + .append("g") + .classed("angular-tick", true); + angularAxis + .attr({ + transform: function(d, i) { + return "rotate(" + currentAngle(d, i) + ")"; + } + }) + .style({ display: axisConfig.angularAxis.visible ? "block" : "none" }); + angularAxis.exit().remove(); + angularAxisEnter + .append("line") + .classed("grid-line", true) + .classed("major", function(d, i) { + return i % (axisConfig.minorTicks + 1) == 0; + }) + .classed("minor", function(d, i) { + return !(i % (axisConfig.minorTicks + 1) == 0); + }) + .style(lineStyle); + angularAxisEnter + .selectAll(".minor") + .style({ stroke: axisConfig.minorTickColor }); + angularAxis + .select("line.grid-line") + .attr({ + x1: axisConfig.tickLength ? radius - axisConfig.tickLength : 0, + x2: radius + }) + .style({ + display: axisConfig.angularAxis.gridLinesVisible ? "block" : "none" + }); + angularAxisEnter + .append("text") + .classed("axis-text", true) + .style(fontStyle); + var ticksText = angularAxis + .select("text.axis-text") + .attr({ + x: radius + axisConfig.labelOffset, + dy: ".35em", + transform: function(d, i) { + var angle = currentAngle(d, i); + var rad = radius + axisConfig.labelOffset; + var orient = axisConfig.angularAxis.tickOrientation; + if (orient == "horizontal") { + return "rotate(" + (-angle) + " " + rad + " 0)"; + } else if (orient == "radial") { + return angle < 270 && angle > 90 + ? "rotate(180 " + rad + " 0)" + : null; + } else { + return "rotate(" + + (angle <= 180 && angle > 0 ? -90 : 90) + + " " + + rad + + " 0)"; } - var angularGuideCircle = guides.select('circle').style({ - stroke: 'grey', - fill: 'none' - }); - chartGroup.on('mousemove.radial-guide', function(d, i) { - var r = µ.util.getMousePos(backgroundCircle).radius; - angularGuideCircle.attr({ - r: r - }).style({ - opacity: .5 - }); - radialValue = radialScale.invert(µ.util.getMousePos(backgroundCircle).radius); - var pos = µ.util.convertToCartesian(r, axisConfig.radialAxis.orientation); - radialTooltip.text(µ.util.round(radialValue)).move([ pos[0] + chartCenter[0], pos[1] + chartCenter[1] ]); - }).on('mouseout.radial-guide', function(d, i) { - angularGuideCircle.style({ - opacity: 0 - }); - geometryTooltip.hide(); - angularTooltip.hide(); - radialTooltip.hide(); - }); - svg.selectAll('.geometry-group .mark').on('mouseover.tooltip', function(d, i) { - var el = d3.select(this); - var color = el.style('fill'); - var newColor = 'black'; - var opacity = el.style('opacity') || 1; - el.attr({ - 'data-opacity': opacity - }); - if (color != 'none') { - el.attr({ - 'data-fill': color - }); - newColor = d3.hsl(color).darker().toString(); - el.style({ - fill: newColor, - opacity: 1 - }); - var textData = { - t: µ.util.round(d[0]), - r: µ.util.round(d[1]) - }; - if (isOrdinal) textData.t = ticks[d[0]]; - var text = 't: ' + textData.t + ', r: ' + textData.r; - var bbox = this.getBoundingClientRect(); - var svgBBox = svg.node().getBoundingClientRect(); - var pos = [ bbox.left + bbox.width / 2 - centeringOffset[0] - svgBBox.left, bbox.top + bbox.height / 2 - centeringOffset[1] - svgBBox.top ]; - geometryTooltip.config({ - color: newColor - }).text(text); - geometryTooltip.move(pos); - } else { - color = el.style('stroke'); - el.attr({ - 'data-stroke': color - }); - newColor = d3.hsl(color).darker().toString(); - el.style({ - stroke: newColor, - opacity: 1 - }); - } - }).on('mousemove.tooltip', function(d, i) { - if (d3.event.which != 0) return false; - if (d3.select(this).attr('data-fill')) geometryTooltip.show(); - }).on('mouseout.tooltip', function(d, i) { - geometryTooltip.hide(); - var el = d3.select(this); - var fillColor = el.attr('data-fill'); - if (fillColor) el.style({ - fill: fillColor, - opacity: el.attr('data-opacity') - }); else el.style({ - stroke: el.attr('data-stroke'), - opacity: el.attr('data-opacity') - }); - }); + } + }) + .style({ + "text-anchor": "middle", + display: axisConfig.angularAxis.labelsVisible ? "block" : "none" + }) + .text(function(d, i) { + if (i % (axisConfig.minorTicks + 1) != 0) return ""; + if (ticks) { + return ticks[d] + axisConfig.angularAxis.ticksSuffix; + } else { + return d + axisConfig.angularAxis.ticksSuffix; + } + }) + .style(fontStyle); + if (axisConfig.angularAxis.rewriteTicks) { + ticksText.text(function(d, i) { + if (i % (axisConfig.minorTicks + 1) != 0) return ""; + return axisConfig.angularAxis.rewriteTicks(this.textContent, i); }); - return exports; - } - exports.render = function(_container) { - render(_container); - return this; - }; - exports.config = function(_x) { - if (!arguments.length) return config; - var xClone = µ.util.cloneJson(_x); - xClone.data.forEach(function(d, i) { - if (!config.data[i]) config.data[i] = {}; - extendDeepAll(config.data[i], µ.Axis.defaultConfig().data[0]); - extendDeepAll(config.data[i], d); + } + var rightmostTickEndX = d3.max( + chartGroup.selectAll(".angular-tick text")[0].map(function(d, i) { + return d.getCTM().e + d.getBBox().width; + }) + ); + legendContainer.attr({ + transform: ( + "translate(" + + [radius + rightmostTickEndX, axisConfig.margin.top] + + ")" + ) + }); + var hasGeometry = svg.select("g.geometry-group").selectAll("g").size() > + 0; + var geometryContainer = svg + .select("g.geometry-group") + .selectAll("g.geometry") + .data(data); + geometryContainer.enter().append("g").attr({ + class: function(d, i) { + return "geometry geometry" + i; + } + }); + geometryContainer.exit().remove(); + if (data[0] || hasGeometry) { + var geometryConfigs = []; + data.forEach(function(d, i) { + var geometryConfig = {}; + geometryConfig.radialScale = radialScale; + geometryConfig.angularScale = angularScale; + geometryConfig.container = geometryContainer.filter(function(dB, iB) { + return iB == i; + }); + geometryConfig.geometry = d.geometry; + geometryConfig.orientation = axisConfig.orientation; + geometryConfig.direction = axisConfig.direction; + geometryConfig.index = i; + geometryConfigs.push({ data: d, geometryConfig: geometryConfig }); }); - extendDeepAll(config.layout, µ.Axis.defaultConfig().layout); - extendDeepAll(config.layout, xClone.layout); - return this; - }; - exports.getLiveConfig = function() { - return liveConfig; - }; - exports.getinputConfig = function() { - return inputConfig; - }; - exports.radialScale = function(_x) { - return radialScale; - }; - exports.angularScale = function(_x) { - return angularScale; - }; - exports.svg = function() { - return svg; - }; - d3.rebind(exports, dispatch, 'on'); + var geometryConfigsGrouped = d3 + .nest() + .key(function(d, i) { + return typeof d.data.groupId !== "undefined" || "unstacked"; + }) + .entries(geometryConfigs); + var geometryConfigsGrouped2 = []; + geometryConfigsGrouped.forEach(function(d, i) { + if (d.key === "unstacked") { + geometryConfigsGrouped2 = geometryConfigsGrouped2.concat( + d.values.map(function(d, i) { + return [d]; + }) + ); + } else { + geometryConfigsGrouped2.push(d.values); + } + }); + geometryConfigsGrouped2.forEach(function(d, i) { + var geometry; + if (Array.isArray(d)) geometry = d[0].geometryConfig.geometry; + else geometry = d.geometryConfig.geometry; + var finalGeometryConfig = d.map(function(dB, iB) { + return extendDeepAll(µ[geometry].defaultConfig(), dB); + }); + µ[geometry]().config(finalGeometryConfig)(); + }); + } + var guides = svg.select(".guides-group"); + var tooltipContainer = svg.select(".tooltips-group"); + var angularTooltip = µ + .tooltipPanel() + .config({ container: tooltipContainer, fontSize: 8 })(); + var radialTooltip = µ + .tooltipPanel() + .config({ container: tooltipContainer, fontSize: 8 })(); + var geometryTooltip = µ + .tooltipPanel() + .config({ container: tooltipContainer, hasTick: true })(); + var angularValue, radialValue; + if (!isOrdinal) { + var angularGuideLine = guides + .select("line") + .attr({ x1: 0, y1: 0, y2: 0 }) + .style({ stroke: "grey", "pointer-events": "none" }); + chartGroup + .on("mousemove.angular-guide", function(d, i) { + var mouseAngle = µ.util.getMousePos(backgroundCircle).angle; + angularGuideLine + .attr({ x2: -radius, transform: "rotate(" + mouseAngle + ")" }) + .style({ opacity: 0.5 }); + var angleWithOriginOffset = (mouseAngle + + 180 + + 360 - + axisConfig.orientation) % + 360; + angularValue = angularScale.invert(angleWithOriginOffset); + var pos = µ.util.convertToCartesian(radius + 12, mouseAngle + 180); + angularTooltip + .text(µ.util.round(angularValue)) + .move([pos[0] + chartCenter[0], pos[1] + chartCenter[1]]); + }) + .on("mouseout.angular-guide", function(d, i) { + guides.select("line").style({ opacity: 0 }); + }); + } + var angularGuideCircle = guides + .select("circle") + .style({ stroke: "grey", fill: "none" }); + chartGroup + .on("mousemove.radial-guide", function(d, i) { + var r = µ.util.getMousePos(backgroundCircle).radius; + angularGuideCircle.attr({ r: r }).style({ opacity: 0.5 }); + radialValue = radialScale.invert( + µ.util.getMousePos(backgroundCircle).radius + ); + var pos = µ.util.convertToCartesian( + r, + axisConfig.radialAxis.orientation + ); + radialTooltip + .text(µ.util.round(radialValue)) + .move([pos[0] + chartCenter[0], pos[1] + chartCenter[1]]); + }) + .on("mouseout.radial-guide", function(d, i) { + angularGuideCircle.style({ opacity: 0 }); + geometryTooltip.hide(); + angularTooltip.hide(); + radialTooltip.hide(); + }); + svg + .selectAll(".geometry-group .mark") + .on("mouseover.tooltip", function(d, i) { + var el = d3.select(this); + var color = el.style("fill"); + var newColor = "black"; + var opacity = el.style("opacity") || 1; + el.attr({ "data-opacity": opacity }); + if (color != "none") { + el.attr({ "data-fill": color }); + newColor = d3.hsl(color).darker().toString(); + el.style({ fill: newColor, opacity: 1 }); + var textData = { t: µ.util.round(d[0]), r: µ.util.round(d[1]) }; + if (isOrdinal) textData.t = ticks[d[0]]; + var text = "t: " + textData.t + ", r: " + textData.r; + var bbox = this.getBoundingClientRect(); + var svgBBox = svg.node().getBoundingClientRect(); + var pos = [ + bbox.left + bbox.width / 2 - centeringOffset[0] - svgBBox.left, + bbox.top + bbox.height / 2 - centeringOffset[1] - svgBBox.top + ]; + geometryTooltip.config({ color: newColor }).text(text); + geometryTooltip.move(pos); + } else { + color = el.style("stroke"); + el.attr({ "data-stroke": color }); + newColor = d3.hsl(color).darker().toString(); + el.style({ stroke: newColor, opacity: 1 }); + } + }) + .on("mousemove.tooltip", function(d, i) { + if (d3.event.which != 0) return false; + if (d3.select(this).attr("data-fill")) geometryTooltip.show(); + }) + .on("mouseout.tooltip", function(d, i) { + geometryTooltip.hide(); + var el = d3.select(this); + var fillColor = el.attr("data-fill"); + if (fillColor) { + el.style({ fill: fillColor, opacity: el.attr("data-opacity") }); + } else { + el.style({ + stroke: el.attr("data-stroke"), + opacity: el.attr("data-opacity") + }); + } + }); + }); return exports; + } + exports.render = function(_container) { + render(_container); + return this; + }; + exports.config = function(_x) { + if (!arguments.length) return config; + var xClone = µ.util.cloneJson(_x); + xClone.data.forEach(function(d, i) { + if (!config.data[i]) config.data[i] = {}; + extendDeepAll(config.data[i], µ.Axis.defaultConfig().data[0]); + extendDeepAll(config.data[i], d); + }); + extendDeepAll(config.layout, µ.Axis.defaultConfig().layout); + extendDeepAll(config.layout, xClone.layout); + return this; + }; + exports.getLiveConfig = function() { + return liveConfig; + }; + exports.getinputConfig = function() { + return inputConfig; + }; + exports.radialScale = function(_x) { + return radialScale; + }; + exports.angularScale = function(_x) { + return angularScale; + }; + exports.svg = function() { + return svg; + }; + d3.rebind(exports, dispatch, "on"); + return exports; }; µ.Axis.defaultConfig = function(d, i) { - var config = { - data: [ { - t: [ 1, 2, 3, 4 ], - r: [ 10, 11, 12, 13 ], - name: 'Line1', - geometry: 'LinePlot', - color: null, - strokeDash: 'solid', - strokeColor: null, - strokeSize: '1', - visibleInLegend: true, - opacity: 1 - } ], - layout: { - defaultColorRange: d3.scale.category10().range(), - title: null, - height: 450, - width: 500, - margin: { - top: 40, - right: 40, - bottom: 40, - left: 40 - }, - font: { - size: 12, - color: 'gray', - outlineColor: 'white', - family: 'Tahoma, sans-serif' - }, - direction: 'clockwise', - orientation: 0, - labelOffset: 10, - radialAxis: { - domain: null, - orientation: -45, - ticksSuffix: '', - visible: true, - gridLinesVisible: true, - tickOrientation: 'horizontal', - rewriteTicks: null - }, - angularAxis: { - domain: [ 0, 360 ], - ticksSuffix: '', - visible: true, - gridLinesVisible: true, - labelsVisible: true, - tickOrientation: 'horizontal', - rewriteTicks: null, - ticksCount: null, - ticksStep: null - }, - minorTicks: 0, - tickLength: null, - tickColor: 'silver', - minorTickColor: '#eee', - backgroundColor: 'none', - needsEndSpacing: null, - showLegend: true, - legend: { - reverseOrder: false - }, - opacity: 1 - } - }; - return config; + var config = { + data: [ + { + t: [1, 2, 3, 4], + r: [10, 11, 12, 13], + name: "Line1", + geometry: "LinePlot", + color: null, + strokeDash: "solid", + strokeColor: null, + strokeSize: "1", + visibleInLegend: true, + opacity: 1 + } + ], + layout: { + defaultColorRange: d3.scale.category10().range(), + title: null, + height: 450, + width: 500, + margin: { top: 40, right: 40, bottom: 40, left: 40 }, + font: { + size: 12, + color: "gray", + outlineColor: "white", + family: "Tahoma, sans-serif" + }, + direction: "clockwise", + orientation: 0, + labelOffset: 10, + radialAxis: { + domain: null, + orientation: -45, + ticksSuffix: "", + visible: true, + gridLinesVisible: true, + tickOrientation: "horizontal", + rewriteTicks: null + }, + angularAxis: { + domain: [0, 360], + ticksSuffix: "", + visible: true, + gridLinesVisible: true, + labelsVisible: true, + tickOrientation: "horizontal", + rewriteTicks: null, + ticksCount: null, + ticksStep: null + }, + minorTicks: 0, + tickLength: null, + tickColor: "silver", + minorTickColor: "#eee", + backgroundColor: "none", + needsEndSpacing: null, + showLegend: true, + legend: { reverseOrder: false }, + opacity: 1 + } + }; + return config; }; µ.util = {}; -µ.DATAEXTENT = 'dataExtent'; +µ.DATAEXTENT = "dataExtent"; -µ.AREA = 'AreaChart'; +µ.AREA = "AreaChart"; -µ.LINE = 'LinePlot'; +µ.LINE = "LinePlot"; -µ.DOT = 'DotPlot'; +µ.DOT = "DotPlot"; -µ.BAR = 'BarChart'; +µ.BAR = "BarChart"; µ.util._override = function(_objA, _objB) { - for (var x in _objA) if (x in _objB) _objB[x] = _objA[x]; + for (var x in _objA) { + if (x in _objB) _objB[x] = _objA[x]; + } }; µ.util._extend = function(_objA, _objB) { - for (var x in _objA) _objB[x] = _objA[x]; + for (var x in _objA) { + _objB[x] = _objA[x]; + } }; µ.util._rndSnd = function() { - return Math.random() * 2 - 1 + (Math.random() * 2 - 1) + (Math.random() * 2 - 1); + return Math.random() * 2 - + 1 + + (Math.random() * 2 - 1) + + (Math.random() * 2 - 1); }; µ.util.dataFromEquation2 = function(_equation, _step) { - var step = _step || 6; - var data = d3.range(0, 360 + step, step).map(function(deg, index) { - var theta = deg * Math.PI / 180; - var radius = _equation(theta); - return [ deg, radius ]; - }); - return data; + var step = _step || 6; + var data = d3.range(0, 360 + step, step).map(function(deg, index) { + var theta = deg * Math.PI / 180; + var radius = _equation(theta); + return [deg, radius]; + }); + return data; }; µ.util.dataFromEquation = function(_equation, _step, _name) { - var step = _step || 6; - var t = [], r = []; - d3.range(0, 360 + step, step).forEach(function(deg, index) { - var theta = deg * Math.PI / 180; - var radius = _equation(theta); - t.push(deg); - r.push(radius); - }); - var result = { - t: t, - r: r - }; - if (_name) result.name = _name; - return result; + var step = _step || 6; + var t = [], r = []; + d3.range(0, 360 + step, step).forEach(function(deg, index) { + var theta = deg * Math.PI / 180; + var radius = _equation(theta); + t.push(deg); + r.push(radius); + }); + var result = { t: t, r: r }; + if (_name) result.name = _name; + return result; }; µ.util.ensureArray = function(_val, _count) { - if (typeof _val === 'undefined') return null; - var arr = [].concat(_val); - return d3.range(_count).map(function(d, i) { - return arr[i] || arr[0]; - }); + if (typeof _val === "undefined") return null; + var arr = [].concat(_val); + return d3.range(_count).map(function(d, i) { + return arr[i] || arr[0]; + }); }; µ.util.fillArrays = function(_obj, _valueNames, _count) { - _valueNames.forEach(function(d, i) { - _obj[d] = µ.util.ensureArray(_obj[d], _count); - }); - return _obj; + _valueNames.forEach(function(d, i) { + _obj[d] = µ.util.ensureArray(_obj[d], _count); + }); + return _obj; }; µ.util.cloneJson = function(json) { - return JSON.parse(JSON.stringify(json)); + return JSON.parse(JSON.stringify(json)); }; µ.util.validateKeys = function(obj, keys) { - if (typeof keys === 'string') keys = keys.split('.'); - var next = keys.shift(); - return obj[next] && (!keys.length || objHasKeys(obj[next], keys)); + if (typeof keys === "string") keys = keys.split("."); + var next = keys.shift(); + return obj[next] && (!keys.length || objHasKeys(obj[next], keys)); }; µ.util.sumArrays = function(a, b) { - return d3.zip(a, b).map(function(d, i) { - return d3.sum(d); - }); + return d3.zip(a, b).map(function(d, i) { + return d3.sum(d); + }); }; µ.util.arrayLast = function(a) { - return a[a.length - 1]; + return a[a.length - 1]; }; µ.util.arrayEqual = function(a, b) { - var i = Math.max(a.length, b.length, 1); - while (i-- >= 0 && a[i] === b[i]) ; - return i === -2; + var i = Math.max(a.length, b.length, 1); + while (i-- >= 0 && a[i] === b[i]); + return i === -2; }; µ.util.flattenArray = function(arr) { - var r = []; - while (!µ.util.arrayEqual(r, arr)) { - r = arr; - arr = [].concat.apply([], arr); - } - return arr; + var r = []; + while (!µ.util.arrayEqual(r, arr)) { + r = arr; + arr = [].concat.apply([], arr); + } + return arr; }; µ.util.deduplicate = function(arr) { - return arr.filter(function(v, i, a) { - return a.indexOf(v) == i; - }); + return arr.filter(function(v, i, a) { + return a.indexOf(v) == i; + }); }; µ.util.convertToCartesian = function(radius, theta) { - var thetaRadians = theta * Math.PI / 180; - var x = radius * Math.cos(thetaRadians); - var y = radius * Math.sin(thetaRadians); - return [ x, y ]; + var thetaRadians = theta * Math.PI / 180; + var x = radius * Math.cos(thetaRadians); + var y = radius * Math.sin(thetaRadians); + return [x, y]; }; µ.util.round = function(_value, _digits) { - var digits = _digits || 2; - var mult = Math.pow(10, digits); - return Math.round(_value * mult) / mult; + var digits = _digits || 2; + var mult = Math.pow(10, digits); + return Math.round(_value * mult) / mult; }; µ.util.getMousePos = function(_referenceElement) { - var mousePos = d3.mouse(_referenceElement.node()); - var mouseX = mousePos[0]; - var mouseY = mousePos[1]; - var mouse = {}; - mouse.x = mouseX; - mouse.y = mouseY; - mouse.pos = mousePos; - mouse.angle = (Math.atan2(mouseY, mouseX) + Math.PI) * 180 / Math.PI; - mouse.radius = Math.sqrt(mouseX * mouseX + mouseY * mouseY); - return mouse; + var mousePos = d3.mouse(_referenceElement.node()); + var mouseX = mousePos[0]; + var mouseY = mousePos[1]; + var mouse = {}; + mouse.x = mouseX; + mouse.y = mouseY; + mouse.pos = mousePos; + mouse.angle = (Math.atan2(mouseY, mouseX) + Math.PI) * 180 / Math.PI; + mouse.radius = Math.sqrt(mouseX * mouseX + mouseY * mouseY); + return mouse; }; µ.util.duplicatesCount = function(arr) { - var uniques = {}, val; - var dups = {}; - for (var i = 0, len = arr.length; i < len; i++) { - val = arr[i]; - if (val in uniques) { - uniques[val]++; - dups[val] = uniques[val]; - } else { - uniques[val] = 1; - } + var uniques = {}, val; + var dups = {}; + for (var i = 0, len = arr.length; i < len; i++) { + val = arr[i]; + if (val in uniques) { + uniques[val]++; + dups[val] = uniques[val]; + } else { + uniques[val] = 1; } - return dups; + } + return dups; }; µ.util.duplicates = function(arr) { - return Object.keys(µ.util.duplicatesCount(arr)); + return Object.keys(µ.util.duplicatesCount(arr)); }; µ.util.translator = function(obj, sourceBranch, targetBranch, reverse) { - if (reverse) { - var targetBranchCopy = targetBranch.slice(); - targetBranch = sourceBranch; - sourceBranch = targetBranchCopy; - } - var value = sourceBranch.reduce(function(previousValue, currentValue) { - if (typeof previousValue != 'undefined') return previousValue[currentValue]; - }, obj); - if (typeof value === 'undefined') return; - sourceBranch.reduce(function(previousValue, currentValue, index) { - if (typeof previousValue == 'undefined') return; - if (index === sourceBranch.length - 1) delete previousValue[currentValue]; + if (reverse) { + var targetBranchCopy = targetBranch.slice(); + targetBranch = sourceBranch; + sourceBranch = targetBranchCopy; + } + var value = sourceBranch.reduce( + function(previousValue, currentValue) { + if (typeof previousValue !== "undefined") { return previousValue[currentValue]; - }, obj); - targetBranch.reduce(function(previousValue, currentValue, index) { - if (typeof previousValue[currentValue] === 'undefined') previousValue[currentValue] = {}; - if (index === targetBranch.length - 1) previousValue[currentValue] = value; - return previousValue[currentValue]; - }, obj); + } + }, + obj + ); + if (typeof value === "undefined") return; + sourceBranch.reduce( + function(previousValue, currentValue, index) { + if (typeof previousValue === "undefined") return; + if (index === sourceBranch.length - 1) delete previousValue[currentValue]; + return previousValue[currentValue]; + }, + obj + ); + targetBranch.reduce( + function(previousValue, currentValue, index) { + if (typeof previousValue[currentValue] === "undefined") { + previousValue[currentValue] = {}; + } + if (index === targetBranch.length - 1) { + previousValue[currentValue] = value; + } + return previousValue[currentValue]; + }, + obj + ); }; µ.PolyChart = function module() { - var config = [ µ.PolyChart.defaultConfig() ]; - var dispatch = d3.dispatch('hover'); - var dashArray = { - solid: 'none', - dash: [ 5, 2 ], - dot: [ 2, 5 ] - }; - var colorScale; - function exports() { - var geometryConfig = config[0].geometryConfig; - var container = geometryConfig.container; - if (typeof container == 'string') container = d3.select(container); - container.datum(config).each(function(_config, _index) { - var isStack = !!_config[0].data.yStack; - var data = _config.map(function(d, i) { - if (isStack) return d3.zip(d.data.t[0], d.data.r[0], d.data.yStack[0]); else return d3.zip(d.data.t[0], d.data.r[0]); - }); - var angularScale = geometryConfig.angularScale; - var domainMin = geometryConfig.radialScale.domain()[0]; - var generator = {}; - generator.bar = function(d, i, pI) { - var dataConfig = _config[pI].data; - var h = geometryConfig.radialScale(d[1]) - geometryConfig.radialScale(0); - var stackTop = geometryConfig.radialScale(d[2] || 0); - var w = dataConfig.barWidth; - d3.select(this).attr({ - 'class': 'mark bar', - d: 'M' + [ [ h + stackTop, -w / 2 ], [ h + stackTop, w / 2 ], [ stackTop, w / 2 ], [ stackTop, -w / 2 ] ].join('L') + 'Z', - transform: function(d, i) { - return 'rotate(' + (geometryConfig.orientation + angularScale(d[0])) + ')'; - } - }); - }; - generator.dot = function(d, i, pI) { - var stackedData = d[2] ? [ d[0], d[1] + d[2] ] : d; - var symbol = d3.svg.symbol().size(_config[pI].data.dotSize).type(_config[pI].data.dotType)(d, i); - d3.select(this).attr({ - 'class': 'mark dot', - d: symbol, - transform: function(d, i) { - var coord = convertToCartesian(getPolarCoordinates(stackedData)); - return 'translate(' + [ coord.x, coord.y ] + ')'; - } - }); - }; - var line = d3.svg.line.radial().interpolate(_config[0].data.lineInterpolation).radius(function(d) { - return geometryConfig.radialScale(d[1]); - }).angle(function(d) { - return geometryConfig.angularScale(d[0]) * Math.PI / 180; - }); - generator.line = function(d, i, pI) { - var lineData = d[2] ? data[pI].map(function(d, i) { - return [ d[0], d[1] + d[2] ]; - }) : data[pI]; - d3.select(this).each(generator['dot']).style({ - opacity: function(dB, iB) { - return +_config[pI].data.dotVisible; - }, - fill: markStyle.stroke(d, i, pI) - }).attr({ - 'class': 'mark dot' - }); - if (i > 0) return; - var lineSelection = d3.select(this.parentNode).selectAll('path.line').data([ 0 ]); - lineSelection.enter().insert('path'); - lineSelection.attr({ - 'class': 'line', - d: line(lineData), - transform: function(dB, iB) { - return 'rotate(' + (geometryConfig.orientation + 90) + ')'; - }, - 'pointer-events': 'none' - }).style({ - fill: function(dB, iB) { - return markStyle.fill(d, i, pI); - }, - 'fill-opacity': 0, - stroke: function(dB, iB) { - return markStyle.stroke(d, i, pI); - }, - 'stroke-width': function(dB, iB) { - return markStyle['stroke-width'](d, i, pI); - }, - 'stroke-dasharray': function(dB, iB) { - return markStyle['stroke-dasharray'](d, i, pI); - }, - opacity: function(dB, iB) { - return markStyle.opacity(d, i, pI); - }, - display: function(dB, iB) { - return markStyle.display(d, i, pI); - } - }); - }; - var angularRange = geometryConfig.angularScale.range(); - var triangleAngle = Math.abs(angularRange[1] - angularRange[0]) / data[0].length * Math.PI / 180; - var arc = d3.svg.arc().startAngle(function(d) { - return -triangleAngle / 2; - }).endAngle(function(d) { - return triangleAngle / 2; - }).innerRadius(function(d) { - return geometryConfig.radialScale(domainMin + (d[2] || 0)); - }).outerRadius(function(d) { - return geometryConfig.radialScale(domainMin + (d[2] || 0)) + geometryConfig.radialScale(d[1]); - }); - generator.arc = function(d, i, pI) { - d3.select(this).attr({ - 'class': 'mark arc', - d: arc, - transform: function(d, i) { - return 'rotate(' + (geometryConfig.orientation + angularScale(d[0]) + 90) + ')'; - } - }); - }; - var markStyle = { - fill: function(d, i, pI) { - return _config[pI].data.color; - }, - stroke: function(d, i, pI) { - return _config[pI].data.strokeColor; - }, - 'stroke-width': function(d, i, pI) { - return _config[pI].data.strokeSize + 'px'; - }, - 'stroke-dasharray': function(d, i, pI) { - return dashArray[_config[pI].data.strokeDash]; - }, - opacity: function(d, i, pI) { - return _config[pI].data.opacity; - }, - display: function(d, i, pI) { - return typeof _config[pI].data.visible === 'undefined' || _config[pI].data.visible ? 'block' : 'none'; - } - }; - var geometryLayer = d3.select(this).selectAll('g.layer').data(data); - geometryLayer.enter().append('g').attr({ - 'class': 'layer' - }); - var geometry = geometryLayer.selectAll('path.mark').data(function(d, i) { - return d; - }); - geometry.enter().append('path').attr({ - 'class': 'mark' - }); - geometry.style(markStyle).each(generator[geometryConfig.geometryType]); - geometry.exit().remove(); - geometryLayer.exit().remove(); - function getPolarCoordinates(d, i) { - var r = geometryConfig.radialScale(d[1]); - var t = (geometryConfig.angularScale(d[0]) + geometryConfig.orientation) * Math.PI / 180; - return { - r: r, - t: t - }; - } - function convertToCartesian(polarCoordinates) { - var x = polarCoordinates.r * Math.cos(polarCoordinates.t); - var y = polarCoordinates.r * Math.sin(polarCoordinates.t); - return { - x: x, - y: y - }; + var config = [µ.PolyChart.defaultConfig()]; + var dispatch = d3.dispatch("hover"); + var dashArray = { solid: "none", dash: [5, 2], dot: [2, 5] }; + var colorScale; + function exports() { + var geometryConfig = config[0].geometryConfig; + var container = geometryConfig.container; + if (typeof container === "string") container = d3.select(container); + container.datum(config).each(function(_config, _index) { + var isStack = !!_config[0].data.yStack; + var data = _config.map(function(d, i) { + if (isStack) return d3.zip(d.data.t[0], d.data.r[0], d.data.yStack[0]); + else return d3.zip(d.data.t[0], d.data.r[0]); + }); + var angularScale = geometryConfig.angularScale; + var domainMin = geometryConfig.radialScale.domain()[0]; + var generator = {}; + generator.bar = function(d, i, pI) { + var dataConfig = _config[pI].data; + var h = geometryConfig.radialScale(d[1]) - + geometryConfig.radialScale(0); + var stackTop = geometryConfig.radialScale(d[2] || 0); + var w = dataConfig.barWidth; + d3.select(this).attr({ + class: "mark bar", + d: ( + "M" + + [ + [h + stackTop, (-w) / 2], + [h + stackTop, w / 2], + [stackTop, w / 2], + [stackTop, (-w) / 2] + ].join("L") + + "Z" + ), + transform: function(d, i) { + return "rotate(" + + (geometryConfig.orientation + angularScale(d[0])) + + ")"; + } + }); + }; + generator.dot = function(d, i, pI) { + var stackedData = d[2] ? [d[0], d[1] + d[2]] : d; + var symbol = d3.svg + .symbol() + .size(_config[pI].data.dotSize) + .type(_config[pI].data.dotType)(d, i); + d3.select(this).attr({ + class: "mark dot", + d: symbol, + transform: function(d, i) { + var coord = convertToCartesian(getPolarCoordinates(stackedData)); + return "translate(" + [coord.x, coord.y] + ")"; + } + }); + }; + var line = d3.svg.line + .radial() + .interpolate(_config[0].data.lineInterpolation) + .radius(function(d) { + return geometryConfig.radialScale(d[1]); + }) + .angle(function(d) { + return geometryConfig.angularScale(d[0]) * Math.PI / 180; + }); + generator.line = function(d, i, pI) { + var lineData = d[2] + ? data[pI].map(function(d, i) { + return [d[0], d[1] + d[2]]; + }) + : data[pI]; + d3 + .select(this) + .each(generator["dot"]) + .style({ + opacity: function(dB, iB) { + return +_config[pI].data.dotVisible; + }, + fill: markStyle.stroke(d, i, pI) + }) + .attr({ class: "mark dot" }); + if (i > 0) return; + var lineSelection = d3 + .select(this.parentNode) + .selectAll("path.line") + .data([0]); + lineSelection.enter().insert("path"); + lineSelection + .attr({ + class: "line", + d: line(lineData), + transform: function(dB, iB) { + return "rotate(" + (geometryConfig.orientation + 90) + ")"; + }, + "pointer-events": "none" + }) + .style({ + fill: function(dB, iB) { + return markStyle.fill(d, i, pI); + }, + "fill-opacity": 0, + stroke: function(dB, iB) { + return markStyle.stroke(d, i, pI); + }, + "stroke-width": function(dB, iB) { + return markStyle["stroke-width"](d, i, pI); + }, + "stroke-dasharray": function(dB, iB) { + return markStyle["stroke-dasharray"](d, i, pI); + }, + opacity: function(dB, iB) { + return markStyle.opacity(d, i, pI); + }, + display: function(dB, iB) { + return markStyle.display(d, i, pI); } + }); + }; + var angularRange = geometryConfig.angularScale.range(); + var triangleAngle = Math.abs(angularRange[1] - angularRange[0]) / + data[0].length * + Math.PI / + 180; + var arc = d3.svg + .arc() + .startAngle(function(d) { + return (-triangleAngle) / 2; + }) + .endAngle(function(d) { + return triangleAngle / 2; + }) + .innerRadius(function(d) { + return geometryConfig.radialScale(domainMin + (d[2] || 0)); + }) + .outerRadius(function(d) { + return geometryConfig.radialScale(domainMin + (d[2] || 0)) + + geometryConfig.radialScale(d[1]); }); - } - exports.config = function(_x) { - if (!arguments.length) return config; - _x.forEach(function(d, i) { - if (!config[i]) config[i] = {}; - extendDeepAll(config[i], µ.PolyChart.defaultConfig()); - extendDeepAll(config[i], d); + generator.arc = function(d, i, pI) { + d3.select(this).attr({ + class: "mark arc", + d: arc, + transform: function(d, i) { + return "rotate(" + + (geometryConfig.orientation + angularScale(d[0]) + 90) + + ")"; + } }); - return this; - }; - exports.getColorScale = function() { - return colorScale; - }; - d3.rebind(exports, dispatch, 'on'); - return exports; + }; + var markStyle = { + fill: function(d, i, pI) { + return _config[pI].data.color; + }, + stroke: function(d, i, pI) { + return _config[pI].data.strokeColor; + }, + "stroke-width": function(d, i, pI) { + return _config[pI].data.strokeSize + "px"; + }, + "stroke-dasharray": function(d, i, pI) { + return dashArray[_config[pI].data.strokeDash]; + }, + opacity: function(d, i, pI) { + return _config[pI].data.opacity; + }, + display: function(d, i, pI) { + return typeof _config[pI].data.visible === "undefined" || + _config[pI].data.visible + ? "block" + : "none"; + } + }; + var geometryLayer = d3.select(this).selectAll("g.layer").data(data); + geometryLayer.enter().append("g").attr({ class: "layer" }); + var geometry = geometryLayer.selectAll("path.mark").data(function(d, i) { + return d; + }); + geometry.enter().append("path").attr({ class: "mark" }); + geometry.style(markStyle).each(generator[geometryConfig.geometryType]); + geometry.exit().remove(); + geometryLayer.exit().remove(); + function getPolarCoordinates(d, i) { + var r = geometryConfig.radialScale(d[1]); + var t = (geometryConfig.angularScale(d[0]) + + geometryConfig.orientation) * + Math.PI / + 180; + return { r: r, t: t }; + } + function convertToCartesian(polarCoordinates) { + var x = polarCoordinates.r * Math.cos(polarCoordinates.t); + var y = polarCoordinates.r * Math.sin(polarCoordinates.t); + return { x: x, y: y }; + } + }); + } + exports.config = function(_x) { + if (!arguments.length) return config; + _x.forEach(function(d, i) { + if (!config[i]) config[i] = {}; + extendDeepAll(config[i], µ.PolyChart.defaultConfig()); + extendDeepAll(config[i], d); + }); + return this; + }; + exports.getColorScale = function() { + return colorScale; + }; + d3.rebind(exports, dispatch, "on"); + return exports; }; µ.PolyChart.defaultConfig = function() { - var config = { - data: { - name: 'geom1', - t: [ [ 1, 2, 3, 4 ] ], - r: [ [ 1, 2, 3, 4 ] ], - dotType: 'circle', - dotSize: 64, - dotVisible: false, - barWidth: 20, - color: '#ffa500', - strokeSize: 1, - strokeColor: 'silver', - strokeDash: 'solid', - opacity: 1, - index: 0, - visible: true, - visibleInLegend: true - }, - geometryConfig: { - geometry: 'LinePlot', - geometryType: 'arc', - direction: 'clockwise', - orientation: 0, - container: 'body', - radialScale: null, - angularScale: null, - colorScale: d3.scale.category20() - } - }; - return config; + var config = { + data: { + name: "geom1", + t: [[1, 2, 3, 4]], + r: [[1, 2, 3, 4]], + dotType: "circle", + dotSize: 64, + dotVisible: false, + barWidth: 20, + color: "#ffa500", + strokeSize: 1, + strokeColor: "silver", + strokeDash: "solid", + opacity: 1, + index: 0, + visible: true, + visibleInLegend: true + }, + geometryConfig: { + geometry: "LinePlot", + geometryType: "arc", + direction: "clockwise", + orientation: 0, + container: "body", + radialScale: null, + angularScale: null, + colorScale: d3.scale.category20() + } + }; + return config; }; µ.BarChart = function module() { - return µ.PolyChart(); + return µ.PolyChart(); }; µ.BarChart.defaultConfig = function() { - var config = { - geometryConfig: { - geometryType: 'bar' - } - }; - return config; + var config = { geometryConfig: { geometryType: "bar" } }; + return config; }; µ.AreaChart = function module() { - return µ.PolyChart(); + return µ.PolyChart(); }; µ.AreaChart.defaultConfig = function() { - var config = { - geometryConfig: { - geometryType: 'arc' - } - }; - return config; + var config = { geometryConfig: { geometryType: "arc" } }; + return config; }; µ.DotPlot = function module() { - return µ.PolyChart(); + return µ.PolyChart(); }; µ.DotPlot.defaultConfig = function() { - var config = { - geometryConfig: { - geometryType: 'dot', - dotType: 'circle' - } - }; - return config; + var config = { geometryConfig: { geometryType: "dot", dotType: "circle" } }; + return config; }; µ.LinePlot = function module() { - return µ.PolyChart(); + return µ.PolyChart(); }; µ.LinePlot.defaultConfig = function() { - var config = { - geometryConfig: { - geometryType: 'line' - } - }; - return config; + var config = { geometryConfig: { geometryType: "line" } }; + return config; }; µ.Legend = function module() { - var config = µ.Legend.defaultConfig(); - var dispatch = d3.dispatch('hover'); - function exports() { - var legendConfig = config.legendConfig; - var flattenData = config.data.map(function(d, i) { - return [].concat(d).map(function(dB, iB) { - var element = extendDeepAll({}, legendConfig.elements[i]); - element.name = dB; - element.color = [].concat(legendConfig.elements[i].color)[iB]; - return element; - }); - }); - var data = d3.merge(flattenData); - data = data.filter(function(d, i) { - return legendConfig.elements[i] && (legendConfig.elements[i].visibleInLegend || typeof legendConfig.elements[i].visibleInLegend === 'undefined'); - }); - if (legendConfig.reverseOrder) data = data.reverse(); - var container = legendConfig.container; - if (typeof container == 'string' || container.nodeName) container = d3.select(container); - var colors = data.map(function(d, i) { - return d.color; - }); - var lineHeight = legendConfig.fontSize; - var isContinuous = legendConfig.isContinuous == null ? typeof data[0] === 'number' : legendConfig.isContinuous; - var height = isContinuous ? legendConfig.height : lineHeight * data.length; - var legendContainerGroup = container.classed('legend-group', true); - var svg = legendContainerGroup.selectAll('svg').data([ 0 ]); - var svgEnter = svg.enter().append('svg').attr({ - width: 300, - height: height + lineHeight, - xmlns: 'http://www.w3.org/2000/svg', - 'xmlns:xlink': 'http://www.w3.org/1999/xlink', - version: '1.1' + var config = µ.Legend.defaultConfig(); + var dispatch = d3.dispatch("hover"); + function exports() { + var legendConfig = config.legendConfig; + var flattenData = config.data.map(function(d, i) { + return [].concat(d).map(function(dB, iB) { + var element = extendDeepAll({}, legendConfig.elements[i]); + element.name = dB; + element.color = [].concat(legendConfig.elements[i].color)[iB]; + return element; + }); + }); + var data = d3.merge(flattenData); + data = data.filter(function(d, i) { + return legendConfig.elements[i] && + (legendConfig.elements[i].visibleInLegend || + typeof legendConfig.elements[i].visibleInLegend === "undefined"); + }); + if (legendConfig.reverseOrder) data = data.reverse(); + var container = legendConfig.container; + if (typeof container === "string" || container.nodeName) { + container = d3.select(container); + } + var colors = data.map(function(d, i) { + return d.color; + }); + var lineHeight = legendConfig.fontSize; + var isContinuous = legendConfig.isContinuous == null + ? typeof data[0] === "number" + : legendConfig.isContinuous; + var height = isContinuous ? legendConfig.height : lineHeight * data.length; + var legendContainerGroup = container.classed("legend-group", true); + var svg = legendContainerGroup.selectAll("svg").data([0]); + var svgEnter = svg.enter().append("svg").attr({ + width: 300, + height: height + lineHeight, + xmlns: "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + version: "1.1" + }); + svgEnter.append("g").classed("legend-axis", true); + svgEnter.append("g").classed("legend-marks", true); + var dataNumbered = d3.range(data.length); + var colorScale = d3.scale + [isContinuous ? "linear" : "ordinal"]() + .domain(dataNumbered) + .range(colors); + var dataScale = d3.scale + [isContinuous ? "linear" : "ordinal"]() + .domain(dataNumbered) + [isContinuous ? "range" : "rangePoints"]([0, height]); + var shapeGenerator = function(_type, _size) { + var squareSize = _size * 3; + if (_type === "line") { + return "M" + + [ + [(-_size) / 2, (-_size) / 12], + [_size / 2, (-_size) / 12], + [_size / 2, _size / 12], + [(-_size) / 2, _size / 12] + ] + + "Z"; + } else if (d3.svg.symbolTypes.indexOf(_type) != -1) { + return d3.svg.symbol().type(_type).size(squareSize)(); + } else { + return d3.svg.symbol().type("square").size(squareSize)(); + } + }; + if (isContinuous) { + var gradient = svg + .select(".legend-marks") + .append("defs") + .append("linearGradient") + .attr({ id: "grad1", x1: "0%", y1: "0%", x2: "0%", y2: "100%" }) + .selectAll("stop") + .data(colors); + gradient.enter().append("stop"); + gradient + .attr({ + offset: function(d, i) { + return i / (colors.length - 1) * 100 + "%"; + } + }) + .style({ + "stop-color": function(d, i) { + return d; + } }); - svgEnter.append('g').classed('legend-axis', true); - svgEnter.append('g').classed('legend-marks', true); - var dataNumbered = d3.range(data.length); - var colorScale = d3.scale[isContinuous ? 'linear' : 'ordinal']().domain(dataNumbered).range(colors); - var dataScale = d3.scale[isContinuous ? 'linear' : 'ordinal']().domain(dataNumbered)[isContinuous ? 'range' : 'rangePoints']([ 0, height ]); - var shapeGenerator = function(_type, _size) { - var squareSize = _size * 3; - if (_type === 'line') { - return 'M' + [ [ -_size / 2, -_size / 12 ], [ _size / 2, -_size / 12 ], [ _size / 2, _size / 12 ], [ -_size / 2, _size / 12 ] ] + 'Z'; - } else if (d3.svg.symbolTypes.indexOf(_type) != -1) return d3.svg.symbol().type(_type).size(squareSize)(); else return d3.svg.symbol().type('square').size(squareSize)(); - }; - if (isContinuous) { - var gradient = svg.select('.legend-marks').append('defs').append('linearGradient').attr({ - id: 'grad1', - x1: '0%', - y1: '0%', - x2: '0%', - y2: '100%' - }).selectAll('stop').data(colors); - gradient.enter().append('stop'); - gradient.attr({ - offset: function(d, i) { - return i / (colors.length - 1) * 100 + '%'; - } - }).style({ - 'stop-color': function(d, i) { - return d; - } - }); - svg.append('rect').classed('legend-mark', true).attr({ - height: legendConfig.height, - width: legendConfig.colorBandWidth, - fill: 'url(#grad1)' - }); - } else { - var legendElement = svg.select('.legend-marks').selectAll('path.legend-mark').data(data); - legendElement.enter().append('path').classed('legend-mark', true); - legendElement.attr({ - transform: function(d, i) { - return 'translate(' + [ lineHeight / 2, dataScale(i) + lineHeight / 2 ] + ')'; - }, - d: function(d, i) { - var symbolType = d.symbol; - return shapeGenerator(symbolType, lineHeight); - }, - fill: function(d, i) { - return colorScale(i); - } - }); - legendElement.exit().remove(); + svg.append("rect").classed("legend-mark", true).attr({ + height: legendConfig.height, + width: legendConfig.colorBandWidth, + fill: "url(#grad1)" + }); + } else { + var legendElement = svg + .select(".legend-marks") + .selectAll("path.legend-mark") + .data(data); + legendElement.enter().append("path").classed("legend-mark", true); + legendElement.attr({ + transform: function(d, i) { + return "translate(" + + [lineHeight / 2, dataScale(i) + lineHeight / 2] + + ")"; + }, + d: function(d, i) { + var symbolType = d.symbol; + return shapeGenerator(symbolType, lineHeight); + }, + fill: function(d, i) { + return colorScale(i); } - var legendAxis = d3.svg.axis().scale(dataScale).orient('right'); - var axis = svg.select('g.legend-axis').attr({ - transform: 'translate(' + [ isContinuous ? legendConfig.colorBandWidth : lineHeight, lineHeight / 2 ] + ')' - }).call(legendAxis); - axis.selectAll('.domain').style({ - fill: 'none', - stroke: 'none' - }); - axis.selectAll('line').style({ - fill: 'none', - stroke: isContinuous ? legendConfig.textColor : 'none' - }); - axis.selectAll('text').style({ - fill: legendConfig.textColor, - 'font-size': legendConfig.fontSize - }).text(function(d, i) { - return data[i].name; - }); - return exports; + }); + legendElement.exit().remove(); } - exports.config = function(_x) { - if (!arguments.length) return config; - extendDeepAll(config, _x); - return this; - }; - d3.rebind(exports, dispatch, 'on'); + var legendAxis = d3.svg.axis().scale(dataScale).orient("right"); + var axis = svg + .select("g.legend-axis") + .attr({ + transform: ( + "translate(" + + [ + isContinuous ? legendConfig.colorBandWidth : lineHeight, + lineHeight / 2 + ] + + ")" + ) + }) + .call(legendAxis); + axis.selectAll(".domain").style({ fill: "none", stroke: "none" }); + axis.selectAll("line").style({ + fill: "none", + stroke: isContinuous ? legendConfig.textColor : "none" + }); + axis + .selectAll("text") + .style({ + fill: legendConfig.textColor, + "font-size": legendConfig.fontSize + }) + .text(function(d, i) { + return data[i].name; + }); return exports; + } + exports.config = function(_x) { + if (!arguments.length) return config; + extendDeepAll(config, _x); + return this; + }; + d3.rebind(exports, dispatch, "on"); + return exports; }; µ.Legend.defaultConfig = function(d, i) { - var config = { - data: [ 'a', 'b', 'c' ], - legendConfig: { - elements: [ { - symbol: 'line', - color: 'red' - }, { - symbol: 'square', - color: 'yellow' - }, { - symbol: 'diamond', - color: 'limegreen' - } ], - height: 150, - colorBandWidth: 30, - fontSize: 12, - container: 'body', - isContinuous: null, - textColor: 'grey', - reverseOrder: false - } - }; - return config; + var config = { + data: ["a", "b", "c"], + legendConfig: { + elements: [ + { symbol: "line", color: "red" }, + { symbol: "square", color: "yellow" }, + { symbol: "diamond", color: "limegreen" } + ], + height: 150, + colorBandWidth: 30, + fontSize: 12, + container: "body", + isContinuous: null, + textColor: "grey", + reverseOrder: false + } + }; + return config; }; µ.tooltipPanel = function() { - var tooltipEl, tooltipTextEl, backgroundEl; - var config = { - container: null, - hasTick: false, - fontSize: 12, - color: 'white', - padding: 5 - }; - var id = 'tooltip-' + µ.tooltipPanel.uid++; - var tickSize = 10; - var exports = function() { - tooltipEl = config.container.selectAll('g.' + id).data([ 0 ]); - var tooltipEnter = tooltipEl.enter().append('g').classed(id, true).style({ - 'pointer-events': 'none', - display: 'none' - }); - backgroundEl = tooltipEnter.append('path').style({ - fill: 'white', - 'fill-opacity': .9 - }).attr({ - d: 'M0 0' - }); - tooltipTextEl = tooltipEnter.append('text').attr({ - dx: config.padding + tickSize, - dy: +config.fontSize * .3 - }); - return exports; - }; - exports.text = function(_text) { - var l = d3.hsl(config.color).l; - var strokeColor = l >= .5 ? '#aaa' : 'white'; - var fillColor = l >= .5 ? 'black' : 'white'; - var text = _text || ''; - tooltipTextEl.style({ - fill: fillColor, - 'font-size': config.fontSize + 'px' - }).text(text); - var padding = config.padding; - var bbox = tooltipTextEl.node().getBBox(); - var boxStyle = { - fill: config.color, - stroke: strokeColor, - 'stroke-width': '2px' - }; - var backGroundW = bbox.width + padding * 2 + tickSize; - var backGroundH = bbox.height + padding * 2; - backgroundEl.attr({ - d: 'M' + [ [ tickSize, -backGroundH / 2 ], [ tickSize, -backGroundH / 4 ], [ config.hasTick ? 0 : tickSize, 0 ], [ tickSize, backGroundH / 4 ], [ tickSize, backGroundH / 2 ], [ backGroundW, backGroundH / 2 ], [ backGroundW, -backGroundH / 2 ] ].join('L') + 'Z' - }).style(boxStyle); - tooltipEl.attr({ - transform: 'translate(' + [ tickSize, -backGroundH / 2 + padding * 2 ] + ')' - }); - tooltipEl.style({ - display: 'block' - }); - return exports; - }; - exports.move = function(_pos) { - if (!tooltipEl) return; - tooltipEl.attr({ - transform: 'translate(' + [ _pos[0], _pos[1] ] + ')' - }).style({ - display: 'block' - }); - return exports; - }; - exports.hide = function() { - if (!tooltipEl) return; - tooltipEl.style({ - display: 'none' - }); - return exports; - }; - exports.show = function() { - if (!tooltipEl) return; - tooltipEl.style({ - display: 'block' - }); - return exports; - }; - exports.config = function(_x) { - extendDeepAll(config, _x); - return exports; + var tooltipEl, tooltipTextEl, backgroundEl; + var config = { + container: null, + hasTick: false, + fontSize: 12, + color: "white", + padding: 5 + }; + var id = "tooltip-" + µ.tooltipPanel.uid++; + var tickSize = 10; + var exports = function() { + tooltipEl = config.container.selectAll("g." + id).data([0]); + var tooltipEnter = tooltipEl + .enter() + .append("g") + .classed(id, true) + .style({ "pointer-events": "none", display: "none" }); + backgroundEl = tooltipEnter + .append("path") + .style({ fill: "white", "fill-opacity": 0.9 }) + .attr({ d: "M0 0" }); + tooltipTextEl = tooltipEnter + .append("text") + .attr({ dx: config.padding + tickSize, dy: (+config.fontSize) * 0.3 }); + return exports; + }; + exports.text = function(_text) { + var l = d3.hsl(config.color).l; + var strokeColor = l >= 0.5 ? "#aaa" : "white"; + var fillColor = l >= 0.5 ? "black" : "white"; + var text = _text || ""; + tooltipTextEl + .style({ fill: fillColor, "font-size": config.fontSize + "px" }) + .text(text); + var padding = config.padding; + var bbox = tooltipTextEl.node().getBBox(); + var boxStyle = { + fill: config.color, + stroke: strokeColor, + "stroke-width": "2px" }; + var backGroundW = bbox.width + padding * 2 + tickSize; + var backGroundH = bbox.height + padding * 2; + backgroundEl + .attr({ + d: ( + "M" + + [ + [tickSize, (-backGroundH) / 2], + [tickSize, (-backGroundH) / 4], + [config.hasTick ? 0 : tickSize, 0], + [tickSize, backGroundH / 4], + [tickSize, backGroundH / 2], + [backGroundW, backGroundH / 2], + [backGroundW, (-backGroundH) / 2] + ].join("L") + + "Z" + ) + }) + .style(boxStyle); + tooltipEl.attr({ + transform: ( + "translate(" + [tickSize, (-backGroundH) / 2 + padding * 2] + ")" + ) + }); + tooltipEl.style({ display: "block" }); + return exports; + }; + exports.move = function(_pos) { + if (!tooltipEl) return; + tooltipEl + .attr({ transform: "translate(" + [_pos[0], _pos[1]] + ")" }) + .style({ display: "block" }); + return exports; + }; + exports.hide = function() { + if (!tooltipEl) return; + tooltipEl.style({ display: "none" }); + return exports; + }; + exports.show = function() { + if (!tooltipEl) return; + tooltipEl.style({ display: "block" }); return exports; + }; + exports.config = function(_x) { + extendDeepAll(config, _x); + return exports; + }; + return exports; }; µ.tooltipPanel.uid = 1; @@ -1269,149 +1485,167 @@ var µ = module.exports = { version: '0.2.2' }; µ.adapter = {}; µ.adapter.plotly = function module() { - var exports = {}; - exports.convert = function(_inputConfig, reverse) { - var outputConfig = {}; - if (_inputConfig.data) { - outputConfig.data = _inputConfig.data.map(function(d, i) { - var r = extendDeepAll({}, d); - var toTranslate = [ - [ r, [ 'marker', 'color' ], [ 'color' ] ], - [ r, [ 'marker', 'opacity' ], [ 'opacity' ] ], - [ r, [ 'marker', 'line', 'color' ], [ 'strokeColor' ] ], - [ r, [ 'marker', 'line', 'dash' ], [ 'strokeDash' ] ], - [ r, [ 'marker', 'line', 'width' ], [ 'strokeSize' ] ], - [ r, [ 'marker', 'symbol' ], [ 'dotType' ] ], - [ r, [ 'marker', 'size' ], [ 'dotSize' ] ], - [ r, [ 'marker', 'barWidth' ], [ 'barWidth' ] ], - [ r, [ 'line', 'interpolation' ], [ 'lineInterpolation' ] ], - [ r, [ 'showlegend' ], [ 'visibleInLegend' ] ] - ]; - toTranslate.forEach(function(d, i) { - µ.util.translator.apply(null, d.concat(reverse)); - }); + var exports = {}; + exports.convert = function(_inputConfig, reverse) { + var outputConfig = {}; + if (_inputConfig.data) { + outputConfig.data = _inputConfig.data.map(function(d, i) { + var r = extendDeepAll({}, d); + var toTranslate = [ + [r, ["marker", "color"], ["color"]], + [r, ["marker", "opacity"], ["opacity"]], + [r, ["marker", "line", "color"], ["strokeColor"]], + [r, ["marker", "line", "dash"], ["strokeDash"]], + [r, ["marker", "line", "width"], ["strokeSize"]], + [r, ["marker", "symbol"], ["dotType"]], + [r, ["marker", "size"], ["dotSize"]], + [r, ["marker", "barWidth"], ["barWidth"]], + [r, ["line", "interpolation"], ["lineInterpolation"]], + [r, ["showlegend"], ["visibleInLegend"]] + ]; + toTranslate.forEach(function(d, i) { + µ.util.translator.apply(null, d.concat(reverse)); + }); - if (!reverse) delete r.marker; - if (reverse) delete r.groupId; - if (!reverse) { - if (r.type === 'scatter') { - if (r.mode === 'lines') r.geometry = 'LinePlot'; else if (r.mode === 'markers') r.geometry = 'DotPlot'; else if (r.mode === 'lines+markers') { - r.geometry = 'LinePlot'; - r.dotVisible = true; - } - } else if (r.type === 'area') r.geometry = 'AreaChart'; else if (r.type === 'bar') r.geometry = 'BarChart'; - delete r.mode; - delete r.type; - } else { - if (r.geometry === 'LinePlot') { - r.type = 'scatter'; - if (r.dotVisible === true) { - delete r.dotVisible; - r.mode = 'lines+markers'; - } else r.mode = 'lines'; - } else if (r.geometry === 'DotPlot') { - r.type = 'scatter'; - r.mode = 'markers'; - } else if (r.geometry === 'AreaChart') r.type = 'area'; else if (r.geometry === 'BarChart') r.type = 'bar'; - delete r.geometry; - } - return r; - }); - if (!reverse && _inputConfig.layout && _inputConfig.layout.barmode === 'stack') { - var duplicates = µ.util.duplicates(outputConfig.data.map(function(d, i) { - return d.geometry; - })); - outputConfig.data.forEach(function(d, i) { - var idx = duplicates.indexOf(d.geometry); - if (idx != -1) outputConfig.data[i].groupId = idx; - }); + if (!reverse) delete r.marker; + if (reverse) delete r.groupId; + if (!reverse) { + if (r.type === "scatter") { + if (r.mode === "lines") { + r.geometry = "LinePlot"; + } else if (r.mode === "markers") { + r.geometry = "DotPlot"; + } else if (r.mode === "lines+markers") { + r.geometry = "LinePlot"; + r.dotVisible = true; } - } - if (_inputConfig.layout) { - var r = extendDeepAll({}, _inputConfig.layout); - var toTranslate = [ - [ r, [ 'plot_bgcolor' ], [ 'backgroundColor' ] ], - [ r, [ 'showlegend' ], [ 'showLegend' ] ], - [ r, [ 'radialaxis' ], [ 'radialAxis' ] ], - [ r, [ 'angularaxis' ], [ 'angularAxis' ] ], - [ r.angularaxis, [ 'showline' ], [ 'gridLinesVisible' ] ], - [ r.angularaxis, [ 'showticklabels' ], [ 'labelsVisible' ] ], - [ r.angularaxis, [ 'nticks' ], [ 'ticksCount' ] ], - [ r.angularaxis, [ 'tickorientation' ], [ 'tickOrientation' ] ], - [ r.angularaxis, [ 'ticksuffix' ], [ 'ticksSuffix' ] ], - [ r.angularaxis, [ 'range' ], [ 'domain' ] ], - [ r.angularaxis, [ 'endpadding' ], [ 'endPadding' ] ], - [ r.radialaxis, [ 'showline' ], [ 'gridLinesVisible' ] ], - [ r.radialaxis, [ 'tickorientation' ], [ 'tickOrientation' ] ], - [ r.radialaxis, [ 'ticksuffix' ], [ 'ticksSuffix' ] ], - [ r.radialaxis, [ 'range' ], [ 'domain' ] ], - [ r.angularAxis, [ 'showline' ], [ 'gridLinesVisible' ] ], - [ r.angularAxis, [ 'showticklabels' ], [ 'labelsVisible' ] ], - [ r.angularAxis, [ 'nticks' ], [ 'ticksCount' ] ], - [ r.angularAxis, [ 'tickorientation' ], [ 'tickOrientation' ] ], - [ r.angularAxis, [ 'ticksuffix' ], [ 'ticksSuffix' ] ], - [ r.angularAxis, [ 'range' ], [ 'domain' ] ], - [ r.angularAxis, [ 'endpadding' ], [ 'endPadding' ] ], - [ r.radialAxis, [ 'showline' ], [ 'gridLinesVisible' ] ], - [ r.radialAxis, [ 'tickorientation' ], [ 'tickOrientation' ] ], - [ r.radialAxis, [ 'ticksuffix' ], [ 'ticksSuffix' ] ], - [ r.radialAxis, [ 'range' ], [ 'domain' ] ], - [ r.font, [ 'outlinecolor' ], [ 'outlineColor' ] ], - [ r.legend, [ 'traceorder' ], [ 'reverseOrder' ] ], - [ r, [ 'labeloffset' ], [ 'labelOffset' ] ], - [ r, [ 'defaultcolorrange' ], [ 'defaultColorRange' ] ] - ]; - toTranslate.forEach(function(d, i) { - µ.util.translator.apply(null, d.concat(reverse)); - }); - - if (!reverse) { - if (r.angularAxis && typeof r.angularAxis.ticklen !== 'undefined') r.tickLength = r.angularAxis.ticklen; - if (r.angularAxis && typeof r.angularAxis.tickcolor !== 'undefined') r.tickColor = r.angularAxis.tickcolor; + } else if (r.type === "area") r.geometry = "AreaChart"; + else if (r.type === "bar") r.geometry = "BarChart"; + delete r.mode; + delete r.type; + } else { + if (r.geometry === "LinePlot") { + r.type = "scatter"; + if (r.dotVisible === true) { + delete r.dotVisible; + r.mode = "lines+markers"; } else { - if (typeof r.tickLength !== 'undefined') { - r.angularaxis.ticklen = r.tickLength; - delete r.tickLength; - } - if (r.tickColor) { - r.angularaxis.tickcolor = r.tickColor; - delete r.tickColor; - } - } - if (r.legend && typeof r.legend.reverseOrder != 'boolean') { - r.legend.reverseOrder = r.legend.reverseOrder != 'normal'; - } - if (r.legend && typeof r.legend.traceorder == 'boolean') { - r.legend.traceorder = r.legend.traceorder ? 'reversed' : 'normal'; - delete r.legend.reverseOrder; - } - if (r.margin && typeof r.margin.t != 'undefined') { - var source = [ 't', 'r', 'b', 'l', 'pad' ]; - var target = [ 'top', 'right', 'bottom', 'left', 'pad' ]; - var margin = {}; - d3.entries(r.margin).forEach(function(dB, iB) { - margin[target[source.indexOf(dB.key)]] = dB.value; - }); - r.margin = margin; - } - if (reverse) { - delete r.needsEndSpacing; - delete r.minorTickColor; - delete r.minorTicks; - delete r.angularaxis.ticksCount; - delete r.angularaxis.ticksCount; - delete r.angularaxis.ticksStep; - delete r.angularaxis.rewriteTicks; - delete r.angularaxis.nticks; - delete r.radialaxis.ticksCount; - delete r.radialaxis.ticksCount; - delete r.radialaxis.ticksStep; - delete r.radialaxis.rewriteTicks; - delete r.radialaxis.nticks; + r.mode = "lines"; } - outputConfig.layout = r; + } else if (r.geometry === "DotPlot") { + r.type = "scatter"; + r.mode = "markers"; + } else if (r.geometry === "AreaChart") r.type = "area"; + else if (r.geometry === "BarChart") r.type = "bar"; + delete r.geometry; } - return outputConfig; - }; - return exports; + return r; + }); + if ( + !reverse && + _inputConfig.layout && + _inputConfig.layout.barmode === "stack" + ) { + var duplicates = µ.util.duplicates( + outputConfig.data.map(function(d, i) { + return d.geometry; + }) + ); + outputConfig.data.forEach(function(d, i) { + var idx = duplicates.indexOf(d.geometry); + if (idx != -1) outputConfig.data[i].groupId = idx; + }); + } + } + if (_inputConfig.layout) { + var r = extendDeepAll({}, _inputConfig.layout); + var toTranslate = [ + [r, ["plot_bgcolor"], ["backgroundColor"]], + [r, ["showlegend"], ["showLegend"]], + [r, ["radialaxis"], ["radialAxis"]], + [r, ["angularaxis"], ["angularAxis"]], + [r.angularaxis, ["showline"], ["gridLinesVisible"]], + [r.angularaxis, ["showticklabels"], ["labelsVisible"]], + [r.angularaxis, ["nticks"], ["ticksCount"]], + [r.angularaxis, ["tickorientation"], ["tickOrientation"]], + [r.angularaxis, ["ticksuffix"], ["ticksSuffix"]], + [r.angularaxis, ["range"], ["domain"]], + [r.angularaxis, ["endpadding"], ["endPadding"]], + [r.radialaxis, ["showline"], ["gridLinesVisible"]], + [r.radialaxis, ["tickorientation"], ["tickOrientation"]], + [r.radialaxis, ["ticksuffix"], ["ticksSuffix"]], + [r.radialaxis, ["range"], ["domain"]], + [r.angularAxis, ["showline"], ["gridLinesVisible"]], + [r.angularAxis, ["showticklabels"], ["labelsVisible"]], + [r.angularAxis, ["nticks"], ["ticksCount"]], + [r.angularAxis, ["tickorientation"], ["tickOrientation"]], + [r.angularAxis, ["ticksuffix"], ["ticksSuffix"]], + [r.angularAxis, ["range"], ["domain"]], + [r.angularAxis, ["endpadding"], ["endPadding"]], + [r.radialAxis, ["showline"], ["gridLinesVisible"]], + [r.radialAxis, ["tickorientation"], ["tickOrientation"]], + [r.radialAxis, ["ticksuffix"], ["ticksSuffix"]], + [r.radialAxis, ["range"], ["domain"]], + [r.font, ["outlinecolor"], ["outlineColor"]], + [r.legend, ["traceorder"], ["reverseOrder"]], + [r, ["labeloffset"], ["labelOffset"]], + [r, ["defaultcolorrange"], ["defaultColorRange"]] + ]; + toTranslate.forEach(function(d, i) { + µ.util.translator.apply(null, d.concat(reverse)); + }); + + if (!reverse) { + if (r.angularAxis && typeof r.angularAxis.ticklen !== "undefined") { + r.tickLength = r.angularAxis.ticklen; + } + if (r.angularAxis && typeof r.angularAxis.tickcolor !== "undefined") { + r.tickColor = r.angularAxis.tickcolor; + } + } else { + if (typeof r.tickLength !== "undefined") { + r.angularaxis.ticklen = r.tickLength; + delete r.tickLength; + } + if (r.tickColor) { + r.angularaxis.tickcolor = r.tickColor; + delete r.tickColor; + } + } + if (r.legend && typeof r.legend.reverseOrder !== "boolean") { + r.legend.reverseOrder = r.legend.reverseOrder != "normal"; + } + if (r.legend && typeof r.legend.traceorder === "boolean") { + r.legend.traceorder = r.legend.traceorder ? "reversed" : "normal"; + delete r.legend.reverseOrder; + } + if (r.margin && typeof r.margin.t !== "undefined") { + var source = ["t", "r", "b", "l", "pad"]; + var target = ["top", "right", "bottom", "left", "pad"]; + var margin = {}; + d3.entries(r.margin).forEach(function(dB, iB) { + margin[target[source.indexOf(dB.key)]] = dB.value; + }); + r.margin = margin; + } + if (reverse) { + delete r.needsEndSpacing; + delete r.minorTickColor; + delete r.minorTicks; + delete r.angularaxis.ticksCount; + delete r.angularaxis.ticksCount; + delete r.angularaxis.ticksStep; + delete r.angularaxis.rewriteTicks; + delete r.angularaxis.nticks; + delete r.radialaxis.ticksCount; + delete r.radialaxis.ticksCount; + delete r.radialaxis.ticksStep; + delete r.radialaxis.rewriteTicks; + delete r.radialaxis.nticks; + } + outputConfig.layout = r; + } + return outputConfig; + }; + return exports; }; diff --git a/src/plots/polar/micropolar_manager.js b/src/plots/polar/micropolar_manager.js index b685ec5f6e4..830e55fa11a 100644 --- a/src/plots/polar/micropolar_manager.js +++ b/src/plots/polar/micropolar_manager.js @@ -5,80 +5,88 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - /* eslint-disable new-cap */ +"use strict"; +var d3 = require("d3"); +var Lib = require("../../lib"); +var Color = require("../../components/color"); -'use strict'; - -var d3 = require('d3'); -var Lib = require('../../lib'); -var Color = require('../../components/color'); - -var micropolar = require('./micropolar'); -var UndoManager = require('./undo_manager'); +var micropolar = require("./micropolar"); +var UndoManager = require("./undo_manager"); var extendDeepAll = Lib.extendDeepAll; var manager = module.exports = {}; manager.framework = function(_gd) { - var config, previousConfigClone, plot, convertedInput, container; - var undoManager = new UndoManager(); + var config, previousConfigClone, plot, convertedInput, container; + var undoManager = new UndoManager(); - function exports(_inputConfig, _container) { - if(_container) container = _container; - d3.select(d3.select(container).node().parentNode).selectAll('.svg-container>*:not(.chart-root)').remove(); + function exports(_inputConfig, _container) { + if (_container) container = _container; + d3 + .select(d3.select(container).node().parentNode) + .selectAll(".svg-container>*:not(.chart-root)") + .remove(); - config = (!config) ? - _inputConfig : - extendDeepAll(config, _inputConfig); + config = !config ? _inputConfig : extendDeepAll(config, _inputConfig); - if(!plot) plot = micropolar.Axis(); - convertedInput = micropolar.adapter.plotly().convert(config); - plot.config(convertedInput).render(container); - _gd.data = config.data; - _gd.layout = config.layout; - manager.fillLayout(_gd); - return config; - } - exports.isPolar = true; - exports.svg = function() { return plot.svg(); }; - exports.getConfig = function() { return config; }; - exports.getLiveConfig = function() { - return micropolar.adapter.plotly().convert(plot.getLiveConfig(), true); - }; - exports.getLiveScales = function() { return {t: plot.angularScale(), r: plot.radialScale()}; }; - exports.setUndoPoint = function() { - var that = this; - var configClone = micropolar.util.cloneJson(config); - (function(_configClone, _previousConfigClone) { - undoManager.add({ - undo: function() { - if(_previousConfigClone) that(_previousConfigClone); - }, - redo: function() { - that(_configClone); - } - }); - })(configClone, previousConfigClone); - previousConfigClone = micropolar.util.cloneJson(configClone); - }; - exports.undo = function() { undoManager.undo(); }; - exports.redo = function() { undoManager.redo(); }; - return exports; + if (!plot) plot = micropolar.Axis(); + convertedInput = micropolar.adapter.plotly().convert(config); + plot.config(convertedInput).render(container); + _gd.data = config.data; + _gd.layout = config.layout; + manager.fillLayout(_gd); + return config; + } + exports.isPolar = true; + exports.svg = function() { + return plot.svg(); + }; + exports.getConfig = function() { + return config; + }; + exports.getLiveConfig = function() { + return micropolar.adapter.plotly().convert(plot.getLiveConfig(), true); + }; + exports.getLiveScales = function() { + return { t: plot.angularScale(), r: plot.radialScale() }; + }; + exports.setUndoPoint = function() { + var that = this; + var configClone = micropolar.util.cloneJson(config); + (function(_configClone, _previousConfigClone) { + undoManager.add({ + undo: function() { + if (_previousConfigClone) that(_previousConfigClone); + }, + redo: function() { + that(_configClone); + } + }); + })(configClone, previousConfigClone); + previousConfigClone = micropolar.util.cloneJson(configClone); + }; + exports.undo = function() { + undoManager.undo(); + }; + exports.redo = function() { + undoManager.redo(); + }; + return exports; }; manager.fillLayout = function(_gd) { - var container = d3.select(_gd).selectAll('.plot-container'), - paperDiv = container.selectAll('.svg-container'), - paper = _gd.framework && _gd.framework.svg && _gd.framework.svg(), - dflts = { - width: 800, - height: 600, - paper_bgcolor: Color.background, - _container: container, - _paperdiv: paperDiv, - _paper: paper - }; + var container = d3.select(_gd).selectAll(".plot-container"), + paperDiv = container.selectAll(".svg-container"), + paper = _gd.framework && _gd.framework.svg && _gd.framework.svg(), + dflts = { + width: 800, + height: 600, + paper_bgcolor: Color.background, + _container: container, + _paperdiv: paperDiv, + _paper: paper + }; - _gd._fullLayout = extendDeepAll(dflts, _gd.layout); + _gd._fullLayout = extendDeepAll(dflts, _gd.layout); }; diff --git a/src/plots/polar/undo_manager.js b/src/plots/polar/undo_manager.js index fe8b58f9c27..576e896ca9b 100644 --- a/src/plots/polar/undo_manager.js +++ b/src/plots/polar/undo_manager.js @@ -5,60 +5,67 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; // Modified from https://github.com/ArthurClemens/Javascript-Undo-Manager // Copyright (c) 2010-2013 Arthur Clemens, arthur@visiblearea.com module.exports = function UndoManager() { - var undoCommands = [], - index = -1, - isExecuting = false, - callback; + var undoCommands = [], index = -1, isExecuting = false, callback; - function execute(command, action) { - if(!command) return this; + function execute(command, action) { + if (!command) return this; - isExecuting = true; - command[action](); - isExecuting = false; + isExecuting = true; + command[action](); + isExecuting = false; - return this; - } + return this; + } - return { - add: function(command) { - if(isExecuting) return this; - undoCommands.splice(index + 1, undoCommands.length - index); - undoCommands.push(command); - index = undoCommands.length - 1; - return this; - }, - setCallback: function(callbackFunc) { callback = callbackFunc; }, - undo: function() { - var command = undoCommands[index]; - if(!command) return this; - execute(command, 'undo'); - index -= 1; - if(callback) callback(command.undo); - return this; - }, - redo: function() { - var command = undoCommands[index + 1]; - if(!command) return this; - execute(command, 'redo'); - index += 1; - if(callback) callback(command.redo); - return this; - }, - clear: function() { - undoCommands = []; - index = -1; - }, - hasUndo: function() { return index !== -1; }, - hasRedo: function() { return index < (undoCommands.length - 1); }, - getCommands: function() { return undoCommands; }, - getPreviousCommand: function() { return undoCommands[index - 1]; }, - getIndex: function() { return index; } - }; + return { + add: function(command) { + if (isExecuting) return this; + undoCommands.splice(index + 1, undoCommands.length - index); + undoCommands.push(command); + index = undoCommands.length - 1; + return this; + }, + setCallback: function(callbackFunc) { + callback = callbackFunc; + }, + undo: function() { + var command = undoCommands[index]; + if (!command) return this; + execute(command, "undo"); + index -= 1; + if (callback) callback(command.undo); + return this; + }, + redo: function() { + var command = undoCommands[index + 1]; + if (!command) return this; + execute(command, "redo"); + index += 1; + if (callback) callback(command.redo); + return this; + }, + clear: function() { + undoCommands = []; + index = -1; + }, + hasUndo: function() { + return index !== -1; + }, + hasRedo: function() { + return index < undoCommands.length - 1; + }, + getCommands: function() { + return undoCommands; + }, + getPreviousCommand: function() { + return undoCommands[index - 1]; + }, + getIndex: function() { + return index; + } + }; }; diff --git a/src/plots/subplot_defaults.js b/src/plots/subplot_defaults.js index 1da3202973d..27be01300d7 100644 --- a/src/plots/subplot_defaults.js +++ b/src/plots/subplot_defaults.js @@ -5,13 +5,9 @@ * 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 Plots = require('./plots'); - +"use strict"; +var Lib = require("../lib"); +var Plots = require("./plots"); /** * Find and supply defaults to all subplots of a given type @@ -40,34 +36,44 @@ var Plots = require('./plots'); * additional items needed by this function here as well * } */ -module.exports = function handleSubplotDefaults(layoutIn, layoutOut, fullData, opts) { - var subplotType = opts.type, - subplotAttributes = opts.attributes, - handleDefaults = opts.handleDefaults, - partition = opts.partition || 'x'; +module.exports = function handleSubplotDefaults( + layoutIn, + layoutOut, + fullData, + opts +) { + var subplotType = opts.type, + subplotAttributes = opts.attributes, + handleDefaults = opts.handleDefaults, + partition = opts.partition || "x"; - var ids = Plots.findSubplotIds(fullData, subplotType), - idsLength = ids.length; + var ids = Plots.findSubplotIds(fullData, subplotType), idsLength = ids.length; - var subplotLayoutIn, subplotLayoutOut; + var subplotLayoutIn, subplotLayoutOut; - function coerce(attr, dflt) { - return Lib.coerce(subplotLayoutIn, subplotLayoutOut, subplotAttributes, attr, dflt); - } + function coerce(attr, dflt) { + return Lib.coerce( + subplotLayoutIn, + subplotLayoutOut, + subplotAttributes, + attr, + dflt + ); + } - for(var i = 0; i < idsLength; i++) { - var id = ids[i]; + for (var i = 0; i < idsLength; i++) { + var id = ids[i]; - // ternary traces get a layout ternary for free! - if(layoutIn[id]) subplotLayoutIn = layoutIn[id]; - else subplotLayoutIn = layoutIn[id] = {}; + // ternary traces get a layout ternary for free! + if (layoutIn[id]) subplotLayoutIn = layoutIn[id]; + else subplotLayoutIn = layoutIn[id] = {}; - layoutOut[id] = subplotLayoutOut = {}; + layoutOut[id] = subplotLayoutOut = {}; - coerce('domain.' + partition, [i / idsLength, (i + 1) / idsLength]); - coerce('domain.' + {x: 'y', y: 'x'}[partition]); + coerce("domain." + partition, [i / idsLength, (i + 1) / idsLength]); + coerce("domain." + ({ x: "y", y: "x" })[partition]); - opts.id = id; - handleDefaults(subplotLayoutIn, subplotLayoutOut, coerce, opts); - } + opts.id = id; + handleDefaults(subplotLayoutIn, subplotLayoutOut, coerce, opts); + } }; diff --git a/src/plots/ternary/index.js b/src/plots/ternary/index.js index e1da80af7b1..d80ee3288bf 100644 --- a/src/plots/ternary/index.js +++ b/src/plots/ternary/index.js @@ -5,68 +5,74 @@ * 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 Ternary = require("./ternary"); +var Plots = require("../../plots/plots"); -'use strict'; +exports.name = "ternary"; -var Ternary = require('./ternary'); +exports.attr = "subplot"; -var Plots = require('../../plots/plots'); - - -exports.name = 'ternary'; - -exports.attr = 'subplot'; - -exports.idRoot = 'ternary'; +exports.idRoot = "ternary"; exports.idRegex = /^ternary([2-9]|[1-9][0-9]+)?$/; exports.attrRegex = /^ternary([2-9]|[1-9][0-9]+)?$/; -exports.attributes = require('./layout/attributes'); +exports.attributes = require("./layout/attributes"); -exports.layoutAttributes = require('./layout/layout_attributes'); +exports.layoutAttributes = require("./layout/layout_attributes"); -exports.supplyLayoutDefaults = require('./layout/defaults'); +exports.supplyLayoutDefaults = require("./layout/defaults"); exports.plot = function plotTernary(gd) { - var fullLayout = gd._fullLayout, - calcData = gd.calcdata, - ternaryIds = Plots.getSubplotIds(fullLayout, 'ternary'); - - for(var i = 0; i < ternaryIds.length; i++) { - var ternaryId = ternaryIds[i], - ternaryCalcData = Plots.getSubplotCalcData(calcData, 'ternary', ternaryId), - ternary = fullLayout[ternaryId]._subplot; - - // If ternary is not instantiated, create one! - if(!ternary) { - ternary = new Ternary({ - id: ternaryId, - graphDiv: gd, - container: fullLayout._ternarylayer.node() - }, - fullLayout - ); - - fullLayout[ternaryId]._subplot = ternary; - } - - ternary.plot(ternaryCalcData, fullLayout, gd._promises); + var fullLayout = gd._fullLayout, + calcData = gd.calcdata, + ternaryIds = Plots.getSubplotIds(fullLayout, "ternary"); + + for (var i = 0; i < ternaryIds.length; i++) { + var ternaryId = ternaryIds[i], + ternaryCalcData = Plots.getSubplotCalcData( + calcData, + "ternary", + ternaryId + ), + ternary = fullLayout[ternaryId]._subplot; + + // If ternary is not instantiated, create one! + if (!ternary) { + ternary = new Ternary( + { + id: ternaryId, + graphDiv: gd, + container: fullLayout._ternarylayer.node() + }, + fullLayout + ); + + fullLayout[ternaryId]._subplot = ternary; } -}; - -exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldTernaryKeys = Plots.getSubplotIds(oldFullLayout, 'ternary'); - for(var i = 0; i < oldTernaryKeys.length; i++) { - var oldTernaryKey = oldTernaryKeys[i]; - var oldTernary = oldFullLayout[oldTernaryKey]._subplot; + ternary.plot(ternaryCalcData, fullLayout, gd._promises); + } +}; - if(!newFullLayout[oldTernaryKey] && !!oldTernary) { - oldTernary.plotContainer.remove(); - oldTernary.clipDef.remove(); - } +exports.clean = function( + newFullData, + newFullLayout, + oldFullData, + oldFullLayout +) { + var oldTernaryKeys = Plots.getSubplotIds(oldFullLayout, "ternary"); + + for (var i = 0; i < oldTernaryKeys.length; i++) { + var oldTernaryKey = oldTernaryKeys[i]; + var oldTernary = oldFullLayout[oldTernaryKey]._subplot; + + if (!newFullLayout[oldTernaryKey] && !!oldTernary) { + oldTernary.plotContainer.remove(); + oldTernary.clipDef.remove(); } + } }; diff --git a/src/plots/ternary/layout/attributes.js b/src/plots/ternary/layout/attributes.js index 0a95e1deb33..84e6ed18dde 100644 --- a/src/plots/ternary/layout/attributes.js +++ b/src/plots/ternary/layout/attributes.js @@ -5,20 +5,17 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - subplot: { - valType: 'subplotid', - role: 'info', - dflt: 'ternary', - description: [ - 'Sets a reference between this trace\'s data coordinates and', - 'a ternary subplot.', - 'If *ternary* (the default value), the data refer to `layout.ternary`.', - 'If *ternary2*, the data refer to `layout.ternary2`, and so on.' - ].join(' ') - } + subplot: { + valType: "subplotid", + role: "info", + dflt: "ternary", + description: [ + "Sets a reference between this trace's data coordinates and", + "a ternary subplot.", + "If *ternary* (the default value), the data refer to `layout.ternary`.", + "If *ternary2*, the data refer to `layout.ternary2`, and so on." + ].join(" ") + } }; diff --git a/src/plots/ternary/layout/axis_attributes.js b/src/plots/ternary/layout/axis_attributes.js index 05d34dfef19..4859a2b4411 100644 --- a/src/plots/ternary/layout/axis_attributes.js +++ b/src/plots/ternary/layout/axis_attributes.js @@ -5,59 +5,55 @@ * 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 axesAttrs = require('../../cartesian/layout_attributes'); -var extendFlat = require('../../../lib/extend').extendFlat; - +"use strict"; +var axesAttrs = require("../../cartesian/layout_attributes"); +var extendFlat = require("../../../lib/extend").extendFlat; module.exports = { - title: axesAttrs.title, - titlefont: axesAttrs.titlefont, - color: axesAttrs.color, - // ticks - tickmode: axesAttrs.tickmode, - nticks: extendFlat({}, axesAttrs.nticks, {dflt: 6, min: 1}), - tick0: axesAttrs.tick0, - dtick: axesAttrs.dtick, - tickvals: axesAttrs.tickvals, - ticktext: axesAttrs.ticktext, - ticks: axesAttrs.ticks, - ticklen: axesAttrs.ticklen, - tickwidth: axesAttrs.tickwidth, - tickcolor: axesAttrs.tickcolor, - showticklabels: axesAttrs.showticklabels, - showtickprefix: axesAttrs.showtickprefix, - tickprefix: axesAttrs.tickprefix, - showticksuffix: axesAttrs.showticksuffix, - ticksuffix: axesAttrs.ticksuffix, - showexponent: axesAttrs.showexponent, - exponentformat: axesAttrs.exponentformat, - separatethousands: axesAttrs.separatethousands, - tickfont: axesAttrs.tickfont, - tickangle: axesAttrs.tickangle, - tickformat: axesAttrs.tickformat, - hoverformat: axesAttrs.hoverformat, - // lines and grids - showline: extendFlat({}, axesAttrs.showline, {dflt: true}), - linecolor: axesAttrs.linecolor, - linewidth: axesAttrs.linewidth, - showgrid: extendFlat({}, axesAttrs.showgrid, {dflt: true}), - gridcolor: axesAttrs.gridcolor, - gridwidth: axesAttrs.gridwidth, - // range - min: { - valType: 'number', - dflt: 0, - role: 'info', - min: 0, - description: [ - 'The minimum value visible on this axis.', - 'The maximum is determined by the sum minus the minimum', - 'values of the other two axes. The full view corresponds to', - 'all the minima set to zero.' - ].join(' ') - } + title: axesAttrs.title, + titlefont: axesAttrs.titlefont, + color: axesAttrs.color, + // ticks + tickmode: axesAttrs.tickmode, + nticks: extendFlat({}, axesAttrs.nticks, { dflt: 6, min: 1 }), + tick0: axesAttrs.tick0, + dtick: axesAttrs.dtick, + tickvals: axesAttrs.tickvals, + ticktext: axesAttrs.ticktext, + ticks: axesAttrs.ticks, + ticklen: axesAttrs.ticklen, + tickwidth: axesAttrs.tickwidth, + tickcolor: axesAttrs.tickcolor, + showticklabels: axesAttrs.showticklabels, + showtickprefix: axesAttrs.showtickprefix, + tickprefix: axesAttrs.tickprefix, + showticksuffix: axesAttrs.showticksuffix, + ticksuffix: axesAttrs.ticksuffix, + showexponent: axesAttrs.showexponent, + exponentformat: axesAttrs.exponentformat, + separatethousands: axesAttrs.separatethousands, + tickfont: axesAttrs.tickfont, + tickangle: axesAttrs.tickangle, + tickformat: axesAttrs.tickformat, + hoverformat: axesAttrs.hoverformat, + // lines and grids + showline: extendFlat({}, axesAttrs.showline, { dflt: true }), + linecolor: axesAttrs.linecolor, + linewidth: axesAttrs.linewidth, + showgrid: extendFlat({}, axesAttrs.showgrid, { dflt: true }), + gridcolor: axesAttrs.gridcolor, + gridwidth: axesAttrs.gridwidth, + // range + min: { + valType: "number", + dflt: 0, + role: "info", + min: 0, + description: [ + "The minimum value visible on this axis.", + "The maximum is determined by the sum minus the minimum", + "values of the other two axes. The full view corresponds to", + "all the minima set to zero." + ].join(" ") + } }; diff --git a/src/plots/ternary/layout/axis_defaults.js b/src/plots/ternary/layout/axis_defaults.js index 0ed502a839e..e920ceddb37 100644 --- a/src/plots/ternary/layout/axis_defaults.js +++ b/src/plots/ternary/layout/axis_defaults.js @@ -5,78 +5,83 @@ * 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 colorMix = require('tinycolor2').mix; - -var Lib = require('../../../lib'); - -var layoutAttributes = require('./axis_attributes'); -var handleTickLabelDefaults = require('../../cartesian/tick_label_defaults'); -var handleTickMarkDefaults = require('../../cartesian/tick_mark_defaults'); -var handleTickValueDefaults = require('../../cartesian/tick_value_defaults'); - - -module.exports = function supplyLayoutDefaults(containerIn, containerOut, options) { - - function coerce(attr, dflt) { - return Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt); - } - - containerOut.type = 'linear'; // no other types allowed for ternary - - var dfltColor = coerce('color'); - // 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 : options.font.color; - - var axName = containerOut._name, - letterUpper = axName.charAt(0).toUpperCase(), - dfltTitle = 'Component ' + letterUpper; - - var title = coerce('title', dfltTitle); - containerOut._hovertitle = title === dfltTitle ? title : letterUpper; - - Lib.coerceFont(coerce, 'titlefont', { - family: options.font.family, - size: Math.round(options.font.size * 1.2), - color: dfltFontColor +"use strict"; +var colorMix = require("tinycolor2").mix; + +var Lib = require("../../../lib"); + +var layoutAttributes = require("./axis_attributes"); +var handleTickLabelDefaults = require("../../cartesian/tick_label_defaults"); +var handleTickMarkDefaults = require("../../cartesian/tick_mark_defaults"); +var handleTickValueDefaults = require("../../cartesian/tick_value_defaults"); + +module.exports = function supplyLayoutDefaults( + containerIn, + containerOut, + options +) { + function coerce(attr, dflt) { + return Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt); + } + + containerOut.type = "linear"; + + // no other types allowed for ternary + var dfltColor = coerce("color"); + // 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 + : options.font.color; + + var axName = containerOut._name, + letterUpper = axName.charAt(0).toUpperCase(), + dfltTitle = "Component " + letterUpper; + + var title = coerce("title", dfltTitle); + containerOut._hovertitle = title === dfltTitle ? title : letterUpper; + + Lib.coerceFont(coerce, "titlefont", { + family: options.font.family, + size: Math.round(options.font.size * 1.2), + color: dfltFontColor + }); + + // range is just set by 'min' - max is determined by the other axes mins + coerce("min"); + + handleTickValueDefaults(containerIn, containerOut, coerce, "linear"); + handleTickLabelDefaults(containerIn, containerOut, coerce, "linear", { + noHover: false + }); + handleTickMarkDefaults(containerIn, containerOut, coerce, { + outerTicks: true + }); + + var showTickLabels = coerce("showticklabels"); + if (showTickLabels) { + Lib.coerceFont(coerce, "tickfont", { + family: options.font.family, + size: options.font.size, + color: dfltFontColor }); - - // range is just set by 'min' - max is determined by the other axes mins - coerce('min'); - - handleTickValueDefaults(containerIn, containerOut, coerce, 'linear'); - handleTickLabelDefaults(containerIn, containerOut, coerce, 'linear', - { noHover: false }); - handleTickMarkDefaults(containerIn, containerOut, coerce, - { outerTicks: true }); - - var showTickLabels = coerce('showticklabels'); - if(showTickLabels) { - Lib.coerceFont(coerce, 'tickfont', { - family: options.font.family, - size: options.font.size, - color: dfltFontColor - }); - coerce('tickangle'); - coerce('tickformat'); - } - - coerce('hoverformat'); - - var showLine = coerce('showline'); - if(showLine) { - coerce('linecolor', dfltColor); - coerce('linewidth'); - } - - var showGridLines = coerce('showgrid'); - if(showGridLines) { - // default grid color is darker here (60%, vs cartesian default ~91%) - // because the grid is not square so the eye needs heavier cues to follow - coerce('gridcolor', colorMix(dfltColor, options.bgColor, 60).toRgbString()); - coerce('gridwidth'); - } + coerce("tickangle"); + coerce("tickformat"); + } + + coerce("hoverformat"); + + var showLine = coerce("showline"); + if (showLine) { + coerce("linecolor", dfltColor); + coerce("linewidth"); + } + + var showGridLines = coerce("showgrid"); + if (showGridLines) { + // default grid color is darker here (60%, vs cartesian default ~91%) + // because the grid is not square so the eye needs heavier cues to follow + coerce("gridcolor", colorMix(dfltColor, options.bgColor, 60).toRgbString()); + coerce("gridwidth"); + } }; diff --git a/src/plots/ternary/layout/defaults.js b/src/plots/ternary/layout/defaults.js index a6b3cface36..f6e8ca9b576 100644 --- a/src/plots/ternary/layout/defaults.js +++ b/src/plots/ternary/layout/defaults.js @@ -5,57 +5,58 @@ * 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 Color = require("../../../components/color"); +var handleSubplotDefaults = require("../../subplot_defaults"); +var layoutAttributes = require("./layout_attributes"); +var handleAxisDefaults = require("./axis_defaults"); -'use strict'; - -var Color = require('../../../components/color'); - -var handleSubplotDefaults = require('../../subplot_defaults'); -var layoutAttributes = require('./layout_attributes'); -var handleAxisDefaults = require('./axis_defaults'); - -var axesNames = ['aaxis', 'baxis', 'caxis']; +var axesNames = ["aaxis", "baxis", "caxis"]; module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - handleSubplotDefaults(layoutIn, layoutOut, fullData, { - type: 'ternary', - attributes: layoutAttributes, - handleDefaults: handleTernaryDefaults, - font: layoutOut.font, - paper_bgcolor: layoutOut.paper_bgcolor - }); + handleSubplotDefaults(layoutIn, layoutOut, fullData, { + type: "ternary", + attributes: layoutAttributes, + handleDefaults: handleTernaryDefaults, + font: layoutOut.font, + paper_bgcolor: layoutOut.paper_bgcolor + }); }; -function handleTernaryDefaults(ternaryLayoutIn, ternaryLayoutOut, coerce, options) { - var bgColor = coerce('bgcolor'); - var sum = coerce('sum'); - options.bgColor = Color.combine(bgColor, options.paper_bgcolor); - var axName, containerIn, containerOut; - - // TODO: allow most (if not all) axis attributes to be set - // in the outer container and used as defaults in the individual axes? - - for(var j = 0; j < axesNames.length; j++) { - axName = axesNames[j]; - containerIn = ternaryLayoutIn[axName] || {}; - containerOut = ternaryLayoutOut[axName] = {_name: axName}; - - handleAxisDefaults(containerIn, containerOut, options); - } - - // if the min values contradict each other, set them all to default (0) - // and delete *all* the inputs so the user doesn't get confused later by - // changing one and having them all change. - var aaxis = ternaryLayoutOut.aaxis, - baxis = ternaryLayoutOut.baxis, - caxis = ternaryLayoutOut.caxis; - if(aaxis.min + baxis.min + caxis.min >= sum) { - aaxis.min = 0; - baxis.min = 0; - caxis.min = 0; - if(ternaryLayoutIn.aaxis) delete ternaryLayoutIn.aaxis.min; - if(ternaryLayoutIn.baxis) delete ternaryLayoutIn.baxis.min; - if(ternaryLayoutIn.caxis) delete ternaryLayoutIn.caxis.min; - } +function handleTernaryDefaults( + ternaryLayoutIn, + ternaryLayoutOut, + coerce, + options +) { + var bgColor = coerce("bgcolor"); + var sum = coerce("sum"); + options.bgColor = Color.combine(bgColor, options.paper_bgcolor); + var axName, containerIn, containerOut; + + // TODO: allow most (if not all) axis attributes to be set + // in the outer container and used as defaults in the individual axes? + for (var j = 0; j < axesNames.length; j++) { + axName = axesNames[j]; + containerIn = ternaryLayoutIn[axName] || {}; + containerOut = ternaryLayoutOut[axName] = { _name: axName }; + + handleAxisDefaults(containerIn, containerOut, options); + } + + // if the min values contradict each other, set them all to default (0) + // and delete *all* the inputs so the user doesn't get confused later by + // changing one and having them all change. + var aaxis = ternaryLayoutOut.aaxis, + baxis = ternaryLayoutOut.baxis, + caxis = ternaryLayoutOut.caxis; + if (aaxis.min + baxis.min + caxis.min >= sum) { + aaxis.min = 0; + baxis.min = 0; + caxis.min = 0; + if (ternaryLayoutIn.aaxis) delete ternaryLayoutIn.aaxis.min; + if (ternaryLayoutIn.baxis) delete ternaryLayoutIn.baxis.min; + if (ternaryLayoutIn.caxis) delete ternaryLayoutIn.caxis.min; + } } diff --git a/src/plots/ternary/layout/layout_attributes.js b/src/plots/ternary/layout/layout_attributes.js index 4ac0400e472..1f320bc9a5f 100644 --- a/src/plots/ternary/layout/layout_attributes.js +++ b/src/plots/ternary/layout/layout_attributes.js @@ -5,59 +5,56 @@ * 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 colorAttrs = require('../../../components/color/attributes'); -var ternaryAxesAttrs = require('./axis_attributes'); - +"use strict"; +var colorAttrs = require("../../../components/color/attributes"); +var ternaryAxesAttrs = require("./axis_attributes"); module.exports = { - domain: { - x: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the horizontal domain of this subplot', - '(in plot fraction).' - ].join(' ') - }, - y: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the vertical domain of this subplot', - '(in plot fraction).' - ].join(' ') - } - }, - bgcolor: { - valType: 'color', - role: 'style', - dflt: colorAttrs.background, - description: 'Set the background color of the subplot' - }, - sum: { - valType: 'number', - role: 'info', - dflt: 1, - min: 0, - description: [ - 'The number each triplet should sum to,', - 'and the maximum range of each axis' - ].join(' ') + domain: { + x: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: [ + "Sets the horizontal domain of this subplot", + "(in plot fraction)." + ].join(" ") }, - aaxis: ternaryAxesAttrs, - baxis: ternaryAxesAttrs, - caxis: ternaryAxesAttrs + y: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: [ + "Sets the vertical domain of this subplot", + "(in plot fraction)." + ].join(" ") + } + }, + bgcolor: { + valType: "color", + role: "style", + dflt: colorAttrs.background, + description: "Set the background color of the subplot" + }, + sum: { + valType: "number", + role: "info", + dflt: 1, + min: 0, + description: [ + "The number each triplet should sum to,", + "and the maximum range of each axis" + ].join(" ") + }, + aaxis: ternaryAxesAttrs, + baxis: ternaryAxesAttrs, + caxis: ternaryAxesAttrs }; diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 547e427af99..9ef4a8c7a8f 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -5,33 +5,29 @@ * 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 d3 = require('d3'); -var tinycolor = require('tinycolor2'); - -var Plotly = require('../../plotly'); -var Lib = require('../../lib'); -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); -var setConvert = require('../cartesian/set_convert'); -var extendFlat = require('../../lib/extend').extendFlat; -var Plots = require('../plots'); -var Axes = require('../cartesian/axes'); -var dragElement = require('../../components/dragelement'); -var Titles = require('../../components/titles'); -var prepSelect = require('../cartesian/select'); -var constants = require('../cartesian/constants'); -var fx = require('../cartesian/graph_interact'); - +"use strict"; +var d3 = require("d3"); +var tinycolor = require("tinycolor2"); + +var Plotly = require("../../plotly"); +var Lib = require("../../lib"); +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); +var setConvert = require("../cartesian/set_convert"); +var extendFlat = require("../../lib/extend").extendFlat; +var Plots = require("../plots"); +var Axes = require("../cartesian/axes"); +var dragElement = require("../../components/dragelement"); +var Titles = require("../../components/titles"); +var prepSelect = require("../cartesian/select"); +var constants = require("../cartesian/constants"); +var fx = require("../cartesian/graph_interact"); function Ternary(options, fullLayout) { - this.id = options.id; - this.graphDiv = options.graphDiv; - this.init(fullLayout); - this.makeFramework(); + this.id = options.id; + this.graphDiv = options.graphDiv; + this.init(fullLayout); + this.makeFramework(); } module.exports = Ternary; @@ -39,605 +35,739 @@ module.exports = Ternary; var proto = Ternary.prototype; proto.init = function(fullLayout) { - this.container = fullLayout._ternarylayer; - this.defs = fullLayout._defs; - this.layoutId = fullLayout._uid; - this.traceHash = {}; + this.container = fullLayout._ternarylayer; + this.defs = fullLayout._defs; + this.layoutId = fullLayout._uid; + this.traceHash = {}; }; proto.plot = function(ternaryCalcData, fullLayout) { - var _this = this, - ternaryLayout = fullLayout[_this.id], - graphSize = fullLayout._size; + var _this = this, + ternaryLayout = fullLayout[_this.id], + graphSize = fullLayout._size; - _this.adjustLayout(ternaryLayout, graphSize); + _this.adjustLayout(ternaryLayout, graphSize); - Plots.generalUpdatePerTraceModule(_this, ternaryCalcData, ternaryLayout); + Plots.generalUpdatePerTraceModule(_this, ternaryCalcData, ternaryLayout); - _this.layers.plotbg.select('path').call(Color.fill, ternaryLayout.bgcolor); + _this.layers.plotbg.select("path").call(Color.fill, ternaryLayout.bgcolor); }; proto.makeFramework = function() { - var _this = this; - - var defGroup = _this.defs.selectAll('g.clips') - .data([0]); - defGroup.enter().append('g') - .classed('clips', true); - - // clippath for this ternary subplot - var clipId = 'clip' + _this.layoutId + _this.id; - _this.clipDef = defGroup.selectAll('#' + clipId) - .data([0]); - _this.clipDef.enter().append('clipPath').attr('id', clipId) - .append('path').attr('d', 'M0,0Z'); - - // container for everything in this ternary subplot - _this.plotContainer = _this.container.selectAll('g.' + _this.id) - .data([0]); - _this.plotContainer.enter().append('g') - .classed(_this.id, true); - - _this.layers = {}; - - // inside that container, we have one container for the data, and - // one each for the three axes around it. - var plotLayers = [ - 'draglayer', - 'plotbg', - 'backplot', - 'grids', - 'frontplot', - 'zoom', - 'aaxis', 'baxis', 'caxis', 'axlines' - ]; - var toplevel = _this.plotContainer.selectAll('g.toplevel') - .data(plotLayers); - toplevel.enter().append('g') - .attr('class', function(d) { return 'toplevel ' + d; }) - .each(function(d) { - var s = d3.select(this); - _this.layers[d] = s; - - // containers for different trace types. - // NOTE - this is different from cartesian, where all traces - // are in front of grids. Here I'm putting maps behind the grids - // so the grids will always be visible if they're requested. - // Perhaps we want that for cartesian too? - if(d === 'frontplot') s.append('g').classed('scatterlayer', true); - else if(d === 'backplot') s.append('g').classed('maplayer', true); - else if(d === 'plotbg') s.append('path').attr('d', 'M0,0Z'); - else if(d === 'axlines') { - s.selectAll('path').data(['aline', 'bline', 'cline']) - .enter().append('path').each(function(d) { - d3.select(this).classed(d, true); - }); - } - }); - - var grids = _this.plotContainer.select('.grids').selectAll('g.grid') - .data(['agrid', 'bgrid', 'cgrid']); - grids.enter().append('g') - .attr('class', function(d) { return 'grid ' + d; }) - .each(function(d) { _this.layers[d] = d3.select(this); }); - - _this.plotContainer.selectAll('.backplot,.frontplot,.grids') - .call(Drawing.setClipUrl, clipId); - - if(!_this.graphDiv._context.staticPlot) { - _this.initInteractions(); - } + var _this = this; + + var defGroup = _this.defs.selectAll("g.clips").data([0]); + defGroup.enter().append("g").classed("clips", true); + + // clippath for this ternary subplot + var clipId = "clip" + _this.layoutId + _this.id; + _this.clipDef = defGroup.selectAll("#" + clipId).data([0]); + _this.clipDef + .enter() + .append("clipPath") + .attr("id", clipId) + .append("path") + .attr("d", "M0,0Z"); + + // container for everything in this ternary subplot + _this.plotContainer = _this.container.selectAll("g." + _this.id).data([0]); + _this.plotContainer.enter().append("g").classed(_this.id, true); + + _this.layers = {}; + + // inside that container, we have one container for the data, and + // one each for the three axes around it. + var plotLayers = [ + "draglayer", + "plotbg", + "backplot", + "grids", + "frontplot", + "zoom", + "aaxis", + "baxis", + "caxis", + "axlines" + ]; + var toplevel = _this.plotContainer.selectAll("g.toplevel").data(plotLayers); + toplevel + .enter() + .append("g") + .attr("class", function(d) { + return "toplevel " + d; + }) + .each(function(d) { + var s = d3.select(this); + _this.layers[d] = s; + + // containers for different trace types. + // NOTE - this is different from cartesian, where all traces + // are in front of grids. Here I'm putting maps behind the grids + // so the grids will always be visible if they're requested. + // Perhaps we want that for cartesian too? + if (d === "frontplot") { + s.append("g").classed("scatterlayer", true); + } else if (d === "backplot") { + s.append("g").classed("maplayer", true); + } else if (d === "plotbg") { + s.append("path").attr("d", "M0,0Z"); + } else if (d === "axlines") { + s + .selectAll("path") + .data(["aline", "bline", "cline"]) + .enter() + .append("path") + .each(function(d) { + d3.select(this).classed(d, true); + }); + } + }); + + var grids = _this.plotContainer + .select(".grids") + .selectAll("g.grid") + .data(["agrid", "bgrid", "cgrid"]); + grids + .enter() + .append("g") + .attr("class", function(d) { + return "grid " + d; + }) + .each(function(d) { + _this.layers[d] = d3.select(this); + }); + + _this.plotContainer + .selectAll(".backplot,.frontplot,.grids") + .call(Drawing.setClipUrl, clipId); + + if (!_this.graphDiv._context.staticPlot) { + _this.initInteractions(); + } }; var w_over_h = Math.sqrt(4 / 3); proto.adjustLayout = function(ternaryLayout, graphSize) { - var _this = this, - domain = ternaryLayout.domain, - xDomainCenter = (domain.x[0] + domain.x[1]) / 2, - yDomainCenter = (domain.y[0] + domain.y[1]) / 2, - xDomain = domain.x[1] - domain.x[0], - yDomain = domain.y[1] - domain.y[0], - wmax = xDomain * graphSize.w, - hmax = yDomain * graphSize.h, - sum = ternaryLayout.sum, - amin = ternaryLayout.aaxis.min, - bmin = ternaryLayout.baxis.min, - cmin = ternaryLayout.caxis.min; - - var x0, y0, w, h, xDomainFinal, yDomainFinal; - - if(wmax > w_over_h * hmax) { - h = hmax; - w = h * w_over_h; - } - else { - w = wmax; - h = w / w_over_h; - } + var _this = this, + domain = ternaryLayout.domain, + xDomainCenter = (domain.x[0] + domain.x[1]) / 2, + yDomainCenter = (domain.y[0] + domain.y[1]) / 2, + xDomain = domain.x[1] - domain.x[0], + yDomain = domain.y[1] - domain.y[0], + wmax = xDomain * graphSize.w, + hmax = yDomain * graphSize.h, + sum = ternaryLayout.sum, + amin = ternaryLayout.aaxis.min, + bmin = ternaryLayout.baxis.min, + cmin = ternaryLayout.caxis.min; + + var x0, y0, w, h, xDomainFinal, yDomainFinal; + + if (wmax > w_over_h * hmax) { + h = hmax; + w = h * w_over_h; + } else { + w = wmax; + h = w / w_over_h; + } + + xDomainFinal = xDomain * w / wmax; + yDomainFinal = yDomain * h / hmax; + + x0 = graphSize.l + graphSize.w * xDomainCenter - w / 2; + y0 = graphSize.t + graphSize.h * (1 - yDomainCenter) - h / 2; + + _this.x0 = x0; + _this.y0 = y0; + _this.w = w; + _this.h = h; + _this.sum = sum; + + // set up the x and y axis objects we'll use to lay out the points + _this.xaxis = { + type: "linear", + range: [amin + 2 * cmin - sum, sum - amin - 2 * bmin], + domain: [ + xDomainCenter - xDomainFinal / 2, + xDomainCenter + xDomainFinal / 2 + ], + _id: "x", + _gd: _this.graphDiv + }; + setConvert(_this.xaxis); + _this.xaxis.setScale(); + + _this.yaxis = { + type: "linear", + range: [amin, sum - bmin - cmin], + domain: [ + yDomainCenter - yDomainFinal / 2, + yDomainCenter + yDomainFinal / 2 + ], + _id: "y", + _gd: _this.graphDiv + }; + setConvert(_this.yaxis); + _this.yaxis.setScale(); + + // set up the modified axes for tick drawing + var yDomain0 = _this.yaxis.domain[0]; + + // aaxis goes up the left side. Set it up as a y axis, but with + // fictitious angles and domain, but then rotate and translate + // it into place at the end + var aaxis = _this.aaxis = extendFlat({}, ternaryLayout.aaxis, { + range: [amin, sum - bmin - cmin], + side: "left", + _counterangle: 30, + // tickangle = 'auto' means 0 anyway for a y axis, need to coerce to 0 here + // so we can shift by 30. + tickangle: ( + (+ternaryLayout.aaxis.tickangle || 0) - 30 + ), + domain: [yDomain0, yDomain0 + yDomainFinal * w_over_h], + _axislayer: _this.layers.aaxis, + _gridlayer: _this.layers.agrid, + _pos: 0, + // _this.xaxis.domain[0] * graphSize.w, + _gd: _this.graphDiv, + _id: "y", + _length: w, + _gridpath: "M0,0l" + h + ",-" + w / 2 + }); + setConvert(aaxis); + + // baxis goes across the bottom (backward). We can set it up as an x axis + // without any enclosing transformation. + var baxis = _this.baxis = extendFlat({}, ternaryLayout.baxis, { + range: [sum - amin - cmin, bmin], + side: "bottom", + _counterangle: 30, + domain: _this.xaxis.domain, + _axislayer: _this.layers.baxis, + _gridlayer: _this.layers.bgrid, + _counteraxis: _this.aaxis, + _pos: 0, + // (1 - yDomain0) * graphSize.h, + _gd: _this.graphDiv, + _id: "x", + _length: w, + _gridpath: "M0,0l-" + w / 2 + ",-" + h + }); + setConvert(baxis); + aaxis._counteraxis = baxis; + + // caxis goes down the right side. Set it up as a y axis, with + // post-transformation similar to aaxis + var caxis = _this.caxis = extendFlat({}, ternaryLayout.caxis, { + range: [sum - amin - bmin, cmin], + side: "right", + _counterangle: 30, + tickangle: (+ternaryLayout.caxis.tickangle || 0) + 30, + domain: [yDomain0, yDomain0 + yDomainFinal * w_over_h], + _axislayer: _this.layers.caxis, + _gridlayer: _this.layers.cgrid, + _counteraxis: _this.baxis, + _pos: 0, + // _this.xaxis.domain[1] * graphSize.w, + _gd: _this.graphDiv, + _id: "y", + _length: w, + _gridpath: "M0,0l-" + h + "," + w / 2 + }); + setConvert(caxis); + + var triangleClip = "M" + + x0 + + "," + + (y0 + h) + + "h" + + w + + "l-" + + w / 2 + + ",-" + + h + + "Z"; + _this.clipDef.select("path").attr("d", triangleClip); + _this.layers.plotbg.select("path").attr("d", triangleClip); + + var plotTransform = "translate(" + x0 + "," + y0 + ")"; + _this.plotContainer + .selectAll(".scatterlayer,.maplayer,.zoom") + .attr("transform", plotTransform); + + // TODO: shift axes to accommodate linewidth*sin(30) tick mark angle + var bTransform = "translate(" + x0 + "," + (y0 + h) + ")"; + + _this.layers.baxis.attr("transform", bTransform); + _this.layers.bgrid.attr("transform", bTransform); + + var aTransform = "translate(" + (x0 + w / 2) + "," + y0 + ")rotate(30)"; + _this.layers.aaxis.attr("transform", aTransform); + _this.layers.agrid.attr("transform", aTransform); + + var cTransform = "translate(" + (x0 + w / 2) + "," + y0 + ")rotate(-30)"; + _this.layers.caxis.attr("transform", cTransform); + _this.layers.cgrid.attr("transform", cTransform); + + _this.drawAxes(true); + + // remove crispEdges - all the off-square angles in ternary plots + // make these counterproductive. + _this.plotContainer.selectAll(".crisp").classed("crisp", false); + + var axlines = _this.layers.axlines; + axlines + .select(".aline") + .attr( + "d", + aaxis.showline + ? "M" + x0 + "," + (y0 + h) + "l" + w / 2 + ",-" + h + : "M0,0" + ) + .call(Color.stroke, aaxis.linecolor || "#000") + .style("stroke-width", (aaxis.linewidth || 0) + "px"); + axlines + .select(".bline") + .attr("d", baxis.showline ? "M" + x0 + "," + (y0 + h) + "h" + w : "M0,0") + .call(Color.stroke, baxis.linecolor || "#000") + .style("stroke-width", (baxis.linewidth || 0) + "px"); + axlines + .select(".cline") + .attr( + "d", + caxis.showline + ? "M" + (x0 + w / 2) + "," + y0 + "l" + w / 2 + "," + h + : "M0,0" + ) + .call(Color.stroke, caxis.linecolor || "#000") + .style("stroke-width", (caxis.linewidth || 0) + "px"); +}; - xDomainFinal = xDomain * w / wmax; - yDomainFinal = yDomain * h / hmax; - - x0 = graphSize.l + graphSize.w * xDomainCenter - w / 2; - y0 = graphSize.t + graphSize.h * (1 - yDomainCenter) - h / 2; - - _this.x0 = x0; - _this.y0 = y0; - _this.w = w; - _this.h = h; - _this.sum = sum; - - // set up the x and y axis objects we'll use to lay out the points - _this.xaxis = { - type: 'linear', - range: [amin + 2 * cmin - sum, sum - amin - 2 * bmin], - domain: [ - xDomainCenter - xDomainFinal / 2, - xDomainCenter + xDomainFinal / 2 - ], - _id: 'x', - _gd: _this.graphDiv - }; - setConvert(_this.xaxis); - _this.xaxis.setScale(); - - _this.yaxis = { - type: 'linear', - range: [amin, sum - bmin - cmin], - domain: [ - yDomainCenter - yDomainFinal / 2, - yDomainCenter + yDomainFinal / 2 - ], - _id: 'y', - _gd: _this.graphDiv - }; - setConvert(_this.yaxis); - _this.yaxis.setScale(); - - // set up the modified axes for tick drawing - var yDomain0 = _this.yaxis.domain[0]; - - // aaxis goes up the left side. Set it up as a y axis, but with - // fictitious angles and domain, but then rotate and translate - // it into place at the end - var aaxis = _this.aaxis = extendFlat({}, ternaryLayout.aaxis, { - range: [amin, sum - bmin - cmin], - side: 'left', - _counterangle: 30, - // tickangle = 'auto' means 0 anyway for a y axis, need to coerce to 0 here - // so we can shift by 30. - tickangle: (+ternaryLayout.aaxis.tickangle || 0) - 30, - domain: [yDomain0, yDomain0 + yDomainFinal * w_over_h], - _axislayer: _this.layers.aaxis, - _gridlayer: _this.layers.agrid, - _pos: 0, // _this.xaxis.domain[0] * graphSize.w, - _gd: _this.graphDiv, - _id: 'y', - _length: w, - _gridpath: 'M0,0l' + h + ',-' + (w / 2) - }); - setConvert(aaxis); - - // baxis goes across the bottom (backward). We can set it up as an x axis - // without any enclosing transformation. - var baxis = _this.baxis = extendFlat({}, ternaryLayout.baxis, { - range: [sum - amin - cmin, bmin], - side: 'bottom', - _counterangle: 30, - domain: _this.xaxis.domain, - _axislayer: _this.layers.baxis, - _gridlayer: _this.layers.bgrid, - _counteraxis: _this.aaxis, - _pos: 0, // (1 - yDomain0) * graphSize.h, - _gd: _this.graphDiv, - _id: 'x', - _length: w, - _gridpath: 'M0,0l-' + (w / 2) + ',-' + h +proto.drawAxes = function(doTitles) { + var _this = this, + gd = _this.graphDiv, + titlesuffix = _this.id.substr(7) + "title", + aaxis = _this.aaxis, + baxis = _this.baxis, + caxis = _this.caxis; + // 3rd arg true below skips titles, so we can configure them + // correctly later on. + Axes.doTicks(gd, aaxis, true); + Axes.doTicks(gd, baxis, true); + Axes.doTicks(gd, caxis, true); + + if (doTitles) { + var apad = Math.max( + aaxis.showticklabels ? aaxis.tickfont.size / 2 : 0, + (caxis.showticklabels ? caxis.tickfont.size * 0.75 : 0) + + (caxis.ticks === "outside" ? caxis.ticklen * 0.87 : 0) + ); + Titles.draw(gd, "a" + titlesuffix, { + propContainer: aaxis, + propName: _this.id + ".aaxis.title", + dfltName: "Component A", + attributes: { + x: _this.x0 + _this.w / 2, + y: _this.y0 - aaxis.titlefont.size / 3 - apad, + "text-anchor": "middle" + } }); - setConvert(baxis); - aaxis._counteraxis = baxis; - - // caxis goes down the right side. Set it up as a y axis, with - // post-transformation similar to aaxis - var caxis = _this.caxis = extendFlat({}, ternaryLayout.caxis, { - range: [sum - amin - bmin, cmin], - side: 'right', - _counterangle: 30, - tickangle: (+ternaryLayout.caxis.tickangle || 0) + 30, - domain: [yDomain0, yDomain0 + yDomainFinal * w_over_h], - _axislayer: _this.layers.caxis, - _gridlayer: _this.layers.cgrid, - _counteraxis: _this.baxis, - _pos: 0, // _this.xaxis.domain[1] * graphSize.w, - _gd: _this.graphDiv, - _id: 'y', - _length: w, - _gridpath: 'M0,0l-' + h + ',' + (w / 2) + + var bpad = (baxis.showticklabels ? baxis.tickfont.size : 0) + + (baxis.ticks === "outside" ? baxis.ticklen : 0) + + 3; + + Titles.draw(gd, "b" + titlesuffix, { + propContainer: baxis, + propName: _this.id + ".baxis.title", + dfltName: "Component B", + attributes: { + x: _this.x0 - bpad, + y: _this.y0 + _this.h + baxis.titlefont.size * 0.83 + bpad, + "text-anchor": "middle" + } }); - setConvert(caxis); - - var triangleClip = 'M' + x0 + ',' + (y0 + h) + 'h' + w + 'l-' + (w / 2) + ',-' + h + 'Z'; - _this.clipDef.select('path').attr('d', triangleClip); - _this.layers.plotbg.select('path').attr('d', triangleClip); - - var plotTransform = 'translate(' + x0 + ',' + y0 + ')'; - _this.plotContainer.selectAll('.scatterlayer,.maplayer,.zoom') - .attr('transform', plotTransform); - - // TODO: shift axes to accommodate linewidth*sin(30) tick mark angle - - var bTransform = 'translate(' + x0 + ',' + (y0 + h) + ')'; - - _this.layers.baxis.attr('transform', bTransform); - _this.layers.bgrid.attr('transform', bTransform); - - var aTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + ')rotate(30)'; - _this.layers.aaxis.attr('transform', aTransform); - _this.layers.agrid.attr('transform', aTransform); - - var cTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + ')rotate(-30)'; - _this.layers.caxis.attr('transform', cTransform); - _this.layers.cgrid.attr('transform', cTransform); - - _this.drawAxes(true); - - // remove crispEdges - all the off-square angles in ternary plots - // make these counterproductive. - _this.plotContainer.selectAll('.crisp').classed('crisp', false); - - var axlines = _this.layers.axlines; - axlines.select('.aline') - .attr('d', aaxis.showline ? - 'M' + x0 + ',' + (y0 + h) + 'l' + (w / 2) + ',-' + h : 'M0,0') - .call(Color.stroke, aaxis.linecolor || '#000') - .style('stroke-width', (aaxis.linewidth || 0) + 'px'); - axlines.select('.bline') - .attr('d', baxis.showline ? - 'M' + x0 + ',' + (y0 + h) + 'h' + w : 'M0,0') - .call(Color.stroke, baxis.linecolor || '#000') - .style('stroke-width', (baxis.linewidth || 0) + 'px'); - axlines.select('.cline') - .attr('d', caxis.showline ? - 'M' + (x0 + w / 2) + ',' + y0 + 'l' + (w / 2) + ',' + h : 'M0,0') - .call(Color.stroke, caxis.linecolor || '#000') - .style('stroke-width', (caxis.linewidth || 0) + 'px'); -}; -proto.drawAxes = function(doTitles) { - var _this = this, - gd = _this.graphDiv, - titlesuffix = _this.id.substr(7) + 'title', - aaxis = _this.aaxis, - baxis = _this.baxis, - caxis = _this.caxis; - // 3rd arg true below skips titles, so we can configure them - // correctly later on. - Axes.doTicks(gd, aaxis, true); - Axes.doTicks(gd, baxis, true); - Axes.doTicks(gd, caxis, true); - - if(doTitles) { - var apad = Math.max(aaxis.showticklabels ? aaxis.tickfont.size / 2 : 0, - (caxis.showticklabels ? caxis.tickfont.size * 0.75 : 0) + - (caxis.ticks === 'outside' ? caxis.ticklen * 0.87 : 0)); - Titles.draw(gd, 'a' + titlesuffix, { - propContainer: aaxis, - propName: _this.id + '.aaxis.title', - dfltName: 'Component A', - attributes: { - x: _this.x0 + _this.w / 2, - y: _this.y0 - aaxis.titlefont.size / 3 - apad, - 'text-anchor': 'middle' - } - }); - - var bpad = (baxis.showticklabels ? baxis.tickfont.size : 0) + - (baxis.ticks === 'outside' ? baxis.ticklen : 0) + 3; - - Titles.draw(gd, 'b' + titlesuffix, { - propContainer: baxis, - propName: _this.id + '.baxis.title', - dfltName: 'Component B', - attributes: { - x: _this.x0 - bpad, - y: _this.y0 + _this.h + baxis.titlefont.size * 0.83 + bpad, - 'text-anchor': 'middle' - } - }); - - Titles.draw(gd, 'c' + titlesuffix, { - propContainer: caxis, - propName: _this.id + '.caxis.title', - dfltName: 'Component C', - attributes: { - x: _this.x0 + _this.w + bpad, - y: _this.y0 + _this.h + caxis.titlefont.size * 0.83 + bpad, - 'text-anchor': 'middle' - } - }); - } + Titles.draw(gd, "c" + titlesuffix, { + propContainer: caxis, + propName: _this.id + ".caxis.title", + dfltName: "Component C", + attributes: { + x: _this.x0 + _this.w + bpad, + y: _this.y0 + _this.h + caxis.titlefont.size * 0.83 + bpad, + "text-anchor": "middle" + } + }); + } }; // hard coded paths for zoom corners // uses the same sizing as cartesian, length is MINZOOM/2, width is 3px var CLEN = constants.MINZOOM / 2 + 0.87; -var BLPATH = 'm-0.87,.5h' + CLEN + 'v3h-' + (CLEN + 5.2) + - 'l' + (CLEN / 2 + 2.6) + ',-' + (CLEN * 0.87 + 4.5) + - 'l2.6,1.5l-' + (CLEN / 2) + ',' + (CLEN * 0.87) + 'Z'; -var BRPATH = 'm0.87,.5h-' + CLEN + 'v3h' + (CLEN + 5.2) + - 'l-' + (CLEN / 2 + 2.6) + ',-' + (CLEN * 0.87 + 4.5) + - 'l-2.6,1.5l' + (CLEN / 2) + ',' + (CLEN * 0.87) + 'Z'; -var TOPPATH = 'm0,1l' + (CLEN / 2) + ',' + (CLEN * 0.87) + - 'l2.6,-1.5l-' + (CLEN / 2 + 2.6) + ',-' + (CLEN * 0.87 + 4.5) + - 'l-' + (CLEN / 2 + 2.6) + ',' + (CLEN * 0.87 + 4.5) + - 'l2.6,1.5l' + (CLEN / 2) + ',-' + (CLEN * 0.87) + 'Z'; -var STARTMARKER = 'm0.5,0.5h5v-2h-5v-5h-2v5h-5v2h5v5h2Z'; +var BLPATH = "m-0.87,.5h" + + CLEN + + "v3h-" + + (CLEN + 5.2) + + "l" + + (CLEN / 2 + 2.6) + + ",-" + + (CLEN * 0.87 + 4.5) + + "l2.6,1.5l-" + + CLEN / 2 + + "," + + CLEN * 0.87 + + "Z"; +var BRPATH = "m0.87,.5h-" + + CLEN + + "v3h" + + (CLEN + 5.2) + + "l-" + + (CLEN / 2 + 2.6) + + ",-" + + (CLEN * 0.87 + 4.5) + + "l-2.6,1.5l" + + CLEN / 2 + + "," + + CLEN * 0.87 + + "Z"; +var TOPPATH = "m0,1l" + + CLEN / 2 + + "," + + CLEN * 0.87 + + "l2.6,-1.5l-" + + (CLEN / 2 + 2.6) + + ",-" + + (CLEN * 0.87 + 4.5) + + "l-" + + (CLEN / 2 + 2.6) + + "," + + (CLEN * 0.87 + 4.5) + + "l2.6,1.5l" + + CLEN / 2 + + ",-" + + CLEN * 0.87 + + "Z"; +var STARTMARKER = "m0.5,0.5h5v-2h-5v-5h-2v5h-5v2h5v5h2Z"; // I guess this could be shared with cartesian... but for now it's separate. var SHOWZOOMOUTTIP = true; proto.initInteractions = function() { - var _this = this, - dragger = _this.layers.plotbg.select('path').node(), - gd = _this.graphDiv, - zoomContainer = _this.layers.zoom; - - // use plotbg for the main interactions - var dragOptions = { - element: dragger, - gd: gd, - plotinfo: {plot: zoomContainer}, - doubleclick: doubleClick, - subplot: _this.id, - prepFn: function(e, startX, startY) { - // these aren't available yet when initInteractions - // is called - dragOptions.xaxes = [_this.xaxis]; - dragOptions.yaxes = [_this.yaxis]; - var dragModeNow = gd._fullLayout.dragmode; - if(e.shiftKey) { - if(dragModeNow === 'pan') dragModeNow = 'zoom'; - else dragModeNow = 'pan'; - } - - if(dragModeNow === 'lasso') dragOptions.minDrag = 1; - else dragOptions.minDrag = undefined; - - if(dragModeNow === 'zoom') { - dragOptions.moveFn = zoomMove; - dragOptions.doneFn = zoomDone; - zoomPrep(e, startX, startY); - } - else if(dragModeNow === 'pan') { - dragOptions.moveFn = plotDrag; - dragOptions.doneFn = dragDone; - panPrep(); - clearSelect(); - } - else if(dragModeNow === 'select' || dragModeNow === 'lasso') { - prepSelect(e, startX, startY, dragOptions, dragModeNow); - } - } - }; - - var x0, y0, mins0, span0, mins, lum, path0, dimmed, zb, corners; - - function zoomPrep(e, startX, startY) { - var dragBBox = dragger.getBoundingClientRect(); - x0 = startX - dragBBox.left; - y0 = startY - dragBBox.top; - mins0 = { - a: _this.aaxis.range[0], - b: _this.baxis.range[1], - c: _this.caxis.range[1] - }; - mins = mins0; - span0 = _this.aaxis.range[1] - mins0.a; - lum = tinycolor(_this.graphDiv._fullLayout[_this.id].bgcolor).getLuminance(); - path0 = 'M0,' + _this.h + 'L' + (_this.w / 2) + ', 0L' + _this.w + ',' + _this.h + 'Z'; - dimmed = false; - - zb = zoomContainer.append('path') - .attr('class', 'zoombox') - .style({ - 'fill': lum > 0.2 ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)', - 'stroke-width': 0 - }) - .attr('d', path0); - - corners = zoomContainer.append('path') - .attr('class', 'zoombox-corners') - .style({ - fill: Color.background, - stroke: Color.defaultLine, - 'stroke-width': 1, - opacity: 0 - }) - .attr('d', 'M0,0Z'); - + var _this = this, + dragger = _this.layers.plotbg.select("path").node(), + gd = _this.graphDiv, + zoomContainer = _this.layers.zoom; + + // use plotbg for the main interactions + var dragOptions = { + element: dragger, + gd: gd, + plotinfo: { plot: zoomContainer }, + doubleclick: doubleClick, + subplot: _this.id, + prepFn: function(e, startX, startY) { + // these aren't available yet when initInteractions + // is called + dragOptions.xaxes = [_this.xaxis]; + dragOptions.yaxes = [_this.yaxis]; + var dragModeNow = gd._fullLayout.dragmode; + if (e.shiftKey) { + if (dragModeNow === "pan") dragModeNow = "zoom"; + else dragModeNow = "pan"; + } + + if (dragModeNow === "lasso") dragOptions.minDrag = 1; + else dragOptions.minDrag = undefined; + + if (dragModeNow === "zoom") { + dragOptions.moveFn = zoomMove; + dragOptions.doneFn = zoomDone; + zoomPrep(e, startX, startY); + } else if (dragModeNow === "pan") { + dragOptions.moveFn = plotDrag; + dragOptions.doneFn = dragDone; + panPrep(); clearSelect(); + } else if (dragModeNow === "select" || dragModeNow === "lasso") { + prepSelect(e, startX, startY, dragOptions, dragModeNow); + } } - - function getAFrac(x, y) { return 1 - (y / _this.h); } - function getBFrac(x, y) { return 1 - ((x + (_this.h - y) / Math.sqrt(3)) / _this.w); } - function getCFrac(x, y) { return ((x - (_this.h - y) / Math.sqrt(3)) / _this.w); } - - function zoomMove(dx0, dy0) { - var x1 = x0 + dx0, - y1 = y0 + dy0, - afrac = Math.max(0, Math.min(1, getAFrac(x0, y0), getAFrac(x1, y1))), - bfrac = Math.max(0, Math.min(1, getBFrac(x0, y0), getBFrac(x1, y1))), - cfrac = Math.max(0, Math.min(1, getCFrac(x0, y0), getCFrac(x1, y1))), - xLeft = ((afrac / 2) + cfrac) * _this.w, - xRight = (1 - (afrac / 2) - bfrac) * _this.w, - xCenter = (xLeft + xRight) / 2, - xSpan = xRight - xLeft, - yBottom = (1 - afrac) * _this.h, - yTop = yBottom - xSpan / w_over_h; - - if(xSpan < constants.MINZOOM) { - mins = mins0; - zb.attr('d', path0); - corners.attr('d', 'M0,0Z'); - } - else { - mins = { - a: mins0.a + afrac * span0, - b: mins0.b + bfrac * span0, - c: mins0.c + cfrac * span0 - }; - zb.attr('d', path0 + 'M' + xLeft + ',' + yBottom + - 'H' + xRight + 'L' + xCenter + ',' + yTop + - 'L' + xLeft + ',' + yBottom + 'Z'); - corners.attr('d', 'M' + x0 + ',' + y0 + STARTMARKER + - 'M' + xLeft + ',' + yBottom + BLPATH + - 'M' + xRight + ',' + yBottom + BRPATH + - 'M' + xCenter + ',' + yTop + TOPPATH); - } - - if(!dimmed) { - zb.transition() - .style('fill', lum > 0.2 ? 'rgba(0,0,0,0.4)' : - 'rgba(255,255,255,0.3)') - .duration(200); - corners.transition() - .style('opacity', 1) - .duration(200); - dimmed = true; - } + }; + + var x0, y0, mins0, span0, mins, lum, path0, dimmed, zb, corners; + + function zoomPrep(e, startX, startY) { + var dragBBox = dragger.getBoundingClientRect(); + x0 = startX - dragBBox.left; + y0 = startY - dragBBox.top; + mins0 = { + a: _this.aaxis.range[0], + b: _this.baxis.range[1], + c: _this.caxis.range[1] + }; + mins = mins0; + span0 = _this.aaxis.range[1] - mins0.a; + lum = tinycolor( + _this.graphDiv._fullLayout[_this.id].bgcolor + ).getLuminance(); + path0 = "M0," + + _this.h + + "L" + + _this.w / 2 + + ", 0L" + + _this.w + + "," + + _this.h + + "Z"; + dimmed = false; + + zb = zoomContainer + .append("path") + .attr("class", "zoombox") + .style({ + fill: lum > 0.2 ? "rgba(0,0,0,0)" : "rgba(255,255,255,0)", + "stroke-width": 0 + }) + .attr("d", path0); + + corners = zoomContainer + .append("path") + .attr("class", "zoombox-corners") + .style({ + fill: Color.background, + stroke: Color.defaultLine, + "stroke-width": 1, + opacity: 0 + }) + .attr("d", "M0,0Z"); + + clearSelect(); + } + + function getAFrac(x, y) { + return 1 - y / _this.h; + } + function getBFrac(x, y) { + return 1 - (x + (_this.h - y) / Math.sqrt(3)) / _this.w; + } + function getCFrac(x, y) { + return (x - (_this.h - y) / Math.sqrt(3)) / _this.w; + } + + function zoomMove(dx0, dy0) { + var x1 = x0 + dx0, + y1 = y0 + dy0, + afrac = Math.max(0, Math.min(1, getAFrac(x0, y0), getAFrac(x1, y1))), + bfrac = Math.max(0, Math.min(1, getBFrac(x0, y0), getBFrac(x1, y1))), + cfrac = Math.max(0, Math.min(1, getCFrac(x0, y0), getCFrac(x1, y1))), + xLeft = (afrac / 2 + cfrac) * _this.w, + xRight = (1 - afrac / 2 - bfrac) * _this.w, + xCenter = (xLeft + xRight) / 2, + xSpan = xRight - xLeft, + yBottom = (1 - afrac) * _this.h, + yTop = yBottom - xSpan / w_over_h; + + if (xSpan < constants.MINZOOM) { + mins = mins0; + zb.attr("d", path0); + corners.attr("d", "M0,0Z"); + } else { + mins = { + a: mins0.a + afrac * span0, + b: mins0.b + bfrac * span0, + c: mins0.c + cfrac * span0 + }; + zb.attr( + "d", + path0 + + "M" + + xLeft + + "," + + yBottom + + "H" + + xRight + + "L" + + xCenter + + "," + + yTop + + "L" + + xLeft + + "," + + yBottom + + "Z" + ); + corners.attr( + "d", + "M" + + x0 + + "," + + y0 + + STARTMARKER + + "M" + + xLeft + + "," + + yBottom + + BLPATH + + "M" + + xRight + + "," + + yBottom + + BRPATH + + "M" + + xCenter + + "," + + yTop + + TOPPATH + ); } - function zoomDone(dragged, numClicks) { - if(mins === mins0) { - if(numClicks === 2) doubleClick(); - - return removeZoombox(gd); - } - - removeZoombox(gd); - - var attrs = {}; - attrs[_this.id + '.aaxis.min'] = mins.a; - attrs[_this.id + '.baxis.min'] = mins.b; - attrs[_this.id + '.caxis.min'] = mins.c; - - Plotly.relayout(gd, attrs); - - if(SHOWZOOMOUTTIP && gd.data && gd._context.showTips) { - Lib.notifier('Double-click to
zoom back out', 'long'); - SHOWZOOMOUTTIP = false; - } + if (!dimmed) { + zb + .transition() + .style("fill", lum > 0.2 ? "rgba(0,0,0,0.4)" : "rgba(255,255,255,0.3)") + .duration(200); + corners.transition().style("opacity", 1).duration(200); + dimmed = true; } + } - function panPrep() { - mins0 = { - a: _this.aaxis.range[0], - b: _this.baxis.range[1], - c: _this.caxis.range[1] - }; - mins = mins0; - } + function zoomDone(dragged, numClicks) { + if (mins === mins0) { + if (numClicks === 2) doubleClick(); - function plotDrag(dx, dy) { - var dxScaled = dx / _this.xaxis._m, - dyScaled = dy / _this.yaxis._m; - mins = { - a: mins0.a - dyScaled, - b: mins0.b + (dxScaled + dyScaled) / 2, - c: mins0.c - (dxScaled - dyScaled) / 2 - }; - var minsorted = [mins.a, mins.b, mins.c].sort(), - minindices = { - a: minsorted.indexOf(mins.a), - b: minsorted.indexOf(mins.b), - c: minsorted.indexOf(mins.c) - }; - if(minsorted[0] < 0) { - if(minsorted[1] + minsorted[0] / 2 < 0) { - minsorted[2] += minsorted[0] + minsorted[1]; - minsorted[0] = minsorted[1] = 0; - } - else { - minsorted[2] += minsorted[0] / 2; - minsorted[1] += minsorted[0] / 2; - minsorted[0] = 0; - } - mins = { - a: minsorted[minindices.a], - b: minsorted[minindices.b], - c: minsorted[minindices.c] - }; - dy = (mins0.a - mins.a) * _this.yaxis._m; - dx = (mins0.c - mins.c - mins0.b + mins.b) * _this.xaxis._m; - } - - // move the data (translate, don't redraw) - var plotTransform = 'translate(' + (_this.x0 + dx) + ',' + (_this.y0 + dy) + ')'; - _this.plotContainer.selectAll('.scatterlayer,.maplayer') - .attr('transform', plotTransform); - - // move the ticks - _this.aaxis.range = [mins.a, _this.sum - mins.b - mins.c]; - _this.baxis.range = [_this.sum - mins.a - mins.c, mins.b]; - _this.caxis.range = [_this.sum - mins.a - mins.b, mins.c]; - - _this.drawAxes(false); - _this.plotContainer.selectAll('.crisp').classed('crisp', false); + return removeZoombox(gd); } - function dragDone(dragged, numClicks) { - if(dragged) { - var attrs = {}; - attrs[_this.id + '.aaxis.min'] = mins.a; - attrs[_this.id + '.baxis.min'] = mins.b; - attrs[_this.id + '.caxis.min'] = mins.c; + removeZoombox(gd); - Plotly.relayout(gd, attrs); - } - else if(numClicks === 2) doubleClick(); - } + var attrs = {}; + attrs[_this.id + ".aaxis.min"] = mins.a; + attrs[_this.id + ".baxis.min"] = mins.b; + attrs[_this.id + ".caxis.min"] = mins.c; - 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. - _this.plotContainer.selectAll('.select-outline').remove(); - } + Plotly.relayout(gd, attrs); - function doubleClick() { - var attrs = {}; - attrs[_this.id + '.aaxis.min'] = 0; - attrs[_this.id + '.baxis.min'] = 0; - attrs[_this.id + '.caxis.min'] = 0; - gd.emit('plotly_doubleclick', null); - Plotly.relayout(gd, attrs); + if (SHOWZOOMOUTTIP && gd.data && gd._context.showTips) { + Lib.notifier("Double-click to
zoom back out", "long"); + SHOWZOOMOUTTIP = false; } + } - // finally, set up hover and click - // these event handlers must already be set before dragElement.init - // so it can stash them and override them. - dragger.onmousemove = function(evt) { - fx.hover(gd, evt, _this.id); - gd._fullLayout._lasthover = dragger; - gd._fullLayout._hoversubplot = _this.id; + function panPrep() { + mins0 = { + a: _this.aaxis.range[0], + b: _this.baxis.range[1], + c: _this.caxis.range[1] }; - - dragger.onmouseout = function(evt) { - if(gd._dragging) return; - - dragElement.unhover(gd, evt); - }; - - dragger.onclick = function(evt) { - fx.click(gd, evt); + mins = mins0; + } + + function plotDrag(dx, dy) { + var dxScaled = dx / _this.xaxis._m, dyScaled = dy / _this.yaxis._m; + mins = { + a: mins0.a - dyScaled, + b: mins0.b + (dxScaled + dyScaled) / 2, + c: mins0.c - (dxScaled - dyScaled) / 2 }; + var minsorted = [mins.a, mins.b, mins.c].sort(), + minindices = { + a: minsorted.indexOf(mins.a), + b: minsorted.indexOf(mins.b), + c: minsorted.indexOf(mins.c) + }; + if (minsorted[0] < 0) { + if (minsorted[1] + minsorted[0] / 2 < 0) { + minsorted[2] += minsorted[0] + minsorted[1]; + minsorted[0] = minsorted[1] = 0; + } else { + minsorted[2] += minsorted[0] / 2; + minsorted[1] += minsorted[0] / 2; + minsorted[0] = 0; + } + mins = { + a: minsorted[minindices.a], + b: minsorted[minindices.b], + c: minsorted[minindices.c] + }; + dy = (mins0.a - mins.a) * _this.yaxis._m; + dx = (mins0.c - mins.c - mins0.b + mins.b) * _this.xaxis._m; + } - dragElement.init(dragOptions); + // move the data (translate, don't redraw) + var plotTransform = "translate(" + + (_this.x0 + dx) + + "," + + (_this.y0 + dy) + + ")"; + _this.plotContainer + .selectAll(".scatterlayer,.maplayer") + .attr("transform", plotTransform); + + // move the ticks + _this.aaxis.range = [mins.a, _this.sum - mins.b - mins.c]; + _this.baxis.range = [_this.sum - mins.a - mins.c, mins.b]; + _this.caxis.range = [_this.sum - mins.a - mins.b, mins.c]; + + _this.drawAxes(false); + _this.plotContainer.selectAll(".crisp").classed("crisp", false); + } + + function dragDone(dragged, numClicks) { + if (dragged) { + var attrs = {}; + attrs[_this.id + ".aaxis.min"] = mins.a; + attrs[_this.id + ".baxis.min"] = mins.b; + attrs[_this.id + ".caxis.min"] = mins.c; + + Plotly.relayout(gd, attrs); + } else if (numClicks === 2) doubleClick(); + } + + 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. + _this.plotContainer.selectAll(".select-outline").remove(); + } + + function doubleClick() { + var attrs = {}; + attrs[_this.id + ".aaxis.min"] = 0; + attrs[_this.id + ".baxis.min"] = 0; + attrs[_this.id + ".caxis.min"] = 0; + gd.emit("plotly_doubleclick", null); + Plotly.relayout(gd, attrs); + } + + // finally, set up hover and click + // these event handlers must already be set before dragElement.init + // so it can stash them and override them. + dragger.onmousemove = function(evt) { + fx.hover(gd, evt, _this.id); + gd._fullLayout._lasthover = dragger; + gd._fullLayout._hoversubplot = _this.id; + }; + + dragger.onmouseout = function(evt) { + if (gd._dragging) return; + + dragElement.unhover(gd, evt); + }; + + dragger.onclick = function(evt) { + fx.click(gd, evt); + }; + + dragElement.init(dragOptions); }; function removeZoombox(gd) { - d3.select(gd) - .selectAll('.zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners') - .remove(); + d3 + .select(gd) + .selectAll( + ".zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners" + ) + .remove(); } diff --git a/src/registry.js b/src/registry.js index 5fb6f2256bd..984d1b0d423 100644 --- a/src/registry.js +++ b/src/registry.js @@ -5,12 +5,9 @@ * 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 basePlotAttributes = require('./plots/attributes'); +"use strict"; +var Lib = require("./lib"); +var basePlotAttributes = require("./plots/attributes"); exports.modules = {}; exports.allCategories = {}; @@ -30,27 +27,24 @@ exports.layoutArrayContainers = []; * @param {object} meta meta information about the trace type */ exports.register = function(_module, thisType, categoriesIn, meta) { - if(exports.modules[thisType]) { - Lib.log('Type ' + thisType + ' already registered'); - return; - } + if (exports.modules[thisType]) { + Lib.log("Type " + thisType + " already registered"); + return; + } - var categoryObj = {}; - for(var i = 0; i < categoriesIn.length; i++) { - categoryObj[categoriesIn[i]] = true; - exports.allCategories[categoriesIn[i]] = true; - } + var categoryObj = {}; + for (var i = 0; i < categoriesIn.length; i++) { + categoryObj[categoriesIn[i]] = true; + exports.allCategories[categoriesIn[i]] = true; + } - exports.modules[thisType] = { - _module: _module, - categories: categoryObj - }; + exports.modules[thisType] = { _module: _module, categories: categoryObj }; - if(meta && Object.keys(meta).length) { - exports.modules[thisType].meta = meta; - } + if (meta && Object.keys(meta).length) { + exports.modules[thisType].meta = meta; + } - exports.allTypes.push(thisType); + exports.allTypes.push(thisType); }; /** @@ -73,25 +67,25 @@ exports.register = function(_module, thisType, categoriesIn, meta) { * (the set of all valid attr names is generated below and stored in attrRegex). */ exports.registerSubplot = function(_module) { - var plotType = _module.name; + var plotType = _module.name; - if(exports.subplotsRegistry[plotType]) { - Lib.log('Plot type ' + plotType + ' already registered.'); - return; - } + if (exports.subplotsRegistry[plotType]) { + Lib.log("Plot type " + plotType + " already registered."); + return; + } - // not sure what's best for the 'cartesian' type at this point - exports.subplotsRegistry[plotType] = _module; + // not sure what's best for the 'cartesian' type at this point + exports.subplotsRegistry[plotType] = _module; }; exports.registerComponent = function(_module) { - var name = _module.name; + var name = _module.name; - exports.componentsRegistry[name] = _module; + exports.componentsRegistry[name] = _module; - if(_module.layoutAttributes && _module.layoutAttributes._isLinkedToArray) { - Lib.pushUnique(exports.layoutArrayContainers, name); - } + if (_module.layoutAttributes && _module.layoutAttributes._isLinkedToArray) { + Lib.pushUnique(exports.layoutArrayContainers, name); + } }; /** @@ -103,17 +97,19 @@ exports.registerComponent = function(_module) { * module object corresponding to trace type */ exports.getModule = function(trace) { - if(trace.r !== undefined) { - Lib.warn('Tried to put a polar trace ' + - 'on an incompatible graph of cartesian ' + - 'data. Ignoring this dataset.', trace - ); - return false; - } - - var _module = exports.modules[getTraceType(trace)]; - if(!_module) return false; - return _module._module; + if (trace.r !== undefined) { + Lib.warn( + "Tried to put a polar trace " + + "on an incompatible graph of cartesian " + + "data. Ignoring this dataset.", + trace + ); + return false; + } + + var _module = exports.modules[getTraceType(trace)]; + if (!_module) return false; + return _module._module; }; /** @@ -126,22 +122,22 @@ exports.getModule = function(trace) { * @return {boolean} */ exports.traceIs = function(traceType, category) { - traceType = getTraceType(traceType); - - // old plot.ly workspace hack, nothing to see here - if(traceType === 'various') return false; + traceType = getTraceType(traceType); - var _module = exports.modules[traceType]; + // old plot.ly workspace hack, nothing to see here + if (traceType === "various") return false; - if(!_module) { - if(traceType && traceType !== 'area') { - Lib.log('Unrecognized trace type ' + traceType + '.'); - } + var _module = exports.modules[traceType]; - _module = exports.modules[basePlotAttributes.type.dflt]; + if (!_module) { + if (traceType && traceType !== "area") { + Lib.log("Unrecognized trace type " + traceType + "."); } - return !!_module.categories[category]; + _module = exports.modules[basePlotAttributes.type.dflt]; + } + + return !!_module.categories[category]; }; /** @@ -154,13 +150,13 @@ exports.traceIs = function(traceType, category) { * @return {function} */ exports.getComponentMethod = function(name, method) { - var _module = exports.componentsRegistry[name]; + var _module = exports.componentsRegistry[name]; - if(!_module) return Lib.noop; - return _module[method]; + if (!_module) return Lib.noop; + return _module[method]; }; function getTraceType(traceType) { - if(typeof traceType === 'object') traceType = traceType.type; - return traceType; + if (typeof traceType === "object") traceType = traceType.type; + return traceType; } diff --git a/src/snapshot/cloneplot.js b/src/snapshot/cloneplot.js index bb44098caae..ab23fde7de2 100644 --- a/src/snapshot/cloneplot.js +++ b/src/snapshot/cloneplot.js @@ -5,164 +5,163 @@ * 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 Plots = require('../plots/plots'); +"use strict"; +var Lib = require("../lib"); +var Plots = require("../plots/plots"); var extendFlat = Lib.extendFlat; var extendDeep = Lib.extendDeep; // Put default plotTile layouts here function cloneLayoutOverride(tileClass) { - var override; - - switch(tileClass) { - case 'themes__thumb': - override = { - autosize: true, - width: 150, - height: 150, - title: '', - showlegend: false, - margin: {l: 5, r: 5, t: 5, b: 5, pad: 0}, - annotations: [] - }; - break; - - case 'thumbnail': - override = { - title: '', - hidesources: true, - showlegend: false, - borderwidth: 0, - bordercolor: '', - margin: {l: 1, r: 1, t: 1, b: 1, pad: 0}, - annotations: [] - }; - break; - - default: - override = {}; - } - - - return override; + var override; + + switch (tileClass) { + case "themes__thumb": + override = { + autosize: true, + width: 150, + height: 150, + title: "", + showlegend: false, + margin: { l: 5, r: 5, t: 5, b: 5, pad: 0 }, + annotations: [] + }; + break; + + case "thumbnail": + override = { + title: "", + hidesources: true, + showlegend: false, + borderwidth: 0, + bordercolor: "", + margin: { l: 1, r: 1, t: 1, b: 1, pad: 0 }, + annotations: [] + }; + break; + + default: + override = {}; + } + + return override; } function keyIsAxis(keyName) { - var types = ['xaxis', 'yaxis', 'zaxis']; - return (types.indexOf(keyName.slice(0, 5)) > -1); + var types = ["xaxis", "yaxis", "zaxis"]; + return types.indexOf(keyName.slice(0, 5)) > -1; } - module.exports = function clonePlot(graphObj, options) { - - // Polar plot compatibility - if(graphObj.framework && graphObj.framework.isPolar) { - graphObj = graphObj.framework.getConfig(); + // Polar plot compatibility + if (graphObj.framework && graphObj.framework.isPolar) { + graphObj = graphObj.framework.getConfig(); + } + + var i; + var oldData = graphObj.data; + var oldLayout = graphObj.layout; + var newData = extendDeep([], oldData); + var newLayout = extendDeep( + {}, + oldLayout, + cloneLayoutOverride(options.tileClass) + ); + + if (options.width) newLayout.width = options.width; + if (options.height) newLayout.height = options.height; + + if ( + options.tileClass === "thumbnail" || options.tileClass === "themes__thumb" + ) { + // kill annotations + newLayout.annotations = []; + var keys = Object.keys(newLayout); + + for (i = 0; i < keys.length; i++) { + if (keyIsAxis(keys[i])) { + newLayout[keys[i]].title = ""; + } } - var i; - var oldData = graphObj.data; - var oldLayout = graphObj.layout; - var newData = extendDeep([], oldData); - var newLayout = extendDeep({}, oldLayout, cloneLayoutOverride(options.tileClass)); - - if(options.width) newLayout.width = options.width; - if(options.height) newLayout.height = options.height; - - if(options.tileClass === 'thumbnail' || options.tileClass === 'themes__thumb') { - // kill annotations - newLayout.annotations = []; - var keys = Object.keys(newLayout); - - for(i = 0; i < keys.length; i++) { - if(keyIsAxis(keys[i])) { - newLayout[keys[i]].title = ''; - } - } - - // kill colorbar and pie labels - for(i = 0; i < newData.length; i++) { - var trace = newData[i]; - trace.showscale = false; - if(trace.marker) trace.marker.showscale = false; - if(trace.type === 'pie') trace.textposition = 'none'; - } + // kill colorbar and pie labels + for (i = 0; i < newData.length; i++) { + var trace = newData[i]; + trace.showscale = false; + if (trace.marker) trace.marker.showscale = false; + if (trace.type === "pie") trace.textposition = "none"; } + } - if(Array.isArray(options.annotations)) { - for(i = 0; i < options.annotations.length; i++) { - newLayout.annotations.push(options.annotations[i]); - } + if (Array.isArray(options.annotations)) { + for (i = 0; i < options.annotations.length; i++) { + newLayout.annotations.push(options.annotations[i]); } - - var sceneIds = Plots.getSubplotIds(newLayout, 'gl3d'); - - if(sceneIds.length) { - var axesImageOverride = {}; - if(options.tileClass === 'thumbnail') { - axesImageOverride = { - title: '', - showaxeslabels: false, - showticklabels: false, - linetickenable: false - }; - } - for(i = 0; i < sceneIds.length; i++) { - var scene = newLayout[sceneIds[i]]; - - if(!scene.xaxis) { - scene.xaxis = {}; - } - - if(!scene.yaxis) { - scene.yaxis = {}; - } - - if(!scene.zaxis) { - scene.zaxis = {}; - } - - extendFlat(scene.xaxis, axesImageOverride); - extendFlat(scene.yaxis, axesImageOverride); - extendFlat(scene.zaxis, axesImageOverride); - - // TODO what does this do? - scene._scene = null; - } + } + + var sceneIds = Plots.getSubplotIds(newLayout, "gl3d"); + + if (sceneIds.length) { + var axesImageOverride = {}; + if (options.tileClass === "thumbnail") { + axesImageOverride = { + title: "", + showaxeslabels: false, + showticklabels: false, + linetickenable: false + }; } + for (i = 0; i < sceneIds.length; i++) { + var scene = newLayout[sceneIds[i]]; + + if (!scene.xaxis) { + scene.xaxis = {}; + } + + if (!scene.yaxis) { + scene.yaxis = {}; + } + + if (!scene.zaxis) { + scene.zaxis = {}; + } - var gd = document.createElement('div'); - if(options.tileClass) gd.className = options.tileClass; - - var plotTile = { - gd: gd, - td: gd, // for external (image server) compatibility - layout: newLayout, - data: newData, - config: { - staticPlot: (options.staticPlot === undefined) ? - true : - options.staticPlot, - plotGlPixelRatio: (options.plotGlPixelRatio === undefined) ? - 2 : - options.plotGlPixelRatio, - displaylogo: options.displaylogo || false, - showLink: options.showLink || false, - showTips: options.showTips || false - } - }; - - if(options.setBackground !== 'transparent') { - plotTile.config.setBackground = options.setBackground || 'opaque'; + extendFlat(scene.xaxis, axesImageOverride); + extendFlat(scene.yaxis, axesImageOverride); + extendFlat(scene.zaxis, axesImageOverride); + + // TODO what does this do? + scene._scene = null; + } + } + + var gd = document.createElement("div"); + if (options.tileClass) gd.className = options.tileClass; + + var plotTile = { + gd: gd, + td: gd, + // for external (image server) compatibility + layout: newLayout, + data: newData, + config: { + staticPlot: options.staticPlot === undefined ? true : options.staticPlot, + plotGlPixelRatio: ( + options.plotGlPixelRatio === undefined ? 2 : options.plotGlPixelRatio + ), + displaylogo: options.displaylogo || false, + showLink: options.showLink || false, + showTips: options.showTips || false } + }; + + if (options.setBackground !== "transparent") { + plotTile.config.setBackground = options.setBackground || "opaque"; + } - // attaching the default Layout the gd, so you can grab it later - plotTile.gd.defaultLayout = cloneLayoutOverride(options.tileClass); + // attaching the default Layout the gd, so you can grab it later + plotTile.gd.defaultLayout = cloneLayoutOverride(options.tileClass); - return plotTile; + return plotTile; }; diff --git a/src/snapshot/download.js b/src/snapshot/download.js index aa37d95e450..67862ff97cf 100644 --- a/src/snapshot/download.js +++ b/src/snapshot/download.js @@ -5,13 +5,11 @@ * 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 toImage = require('../plot_api/to_image'); -var Lib = require('../lib'); // for isIE -var fileSaver = require('./filesaver'); +"use strict"; +var toImage = require("../plot_api/to_image"); +var Lib = require("../lib"); +// for isIE +var fileSaver = require("./filesaver"); /** * @param {object} gd figure Object @@ -22,43 +20,47 @@ var fileSaver = require('./filesaver'); * @param opts.filename name of file excluding extension */ function downloadImage(gd, opts) { + // check for undefined opts + opts = opts || {}; - // check for undefined opts - opts = opts || {}; - - // default to png - opts.format = opts.format || 'png'; + // default to png + opts.format = opts.format || "png"; - return new Promise(function(resolve, reject) { - if(gd._snapshotInProgress) { - reject(new Error('Snapshotting already in progress.')); - } + return new Promise(function(resolve, reject) { + if (gd._snapshotInProgress) { + reject(new Error("Snapshotting already in progress.")); + } - // see comments within svgtoimg for additional - // discussion of problems with IE - // can now draw to canvas, but CORS tainted canvas - // does not allow toDataURL - // svg format will work though - if(Lib.isIE() && opts.format !== 'svg') { - reject(new Error('Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.')); - } + // see comments within svgtoimg for additional + // discussion of problems with IE + // can now draw to canvas, but CORS tainted canvas + // does not allow toDataURL + // svg format will work though + if (Lib.isIE() && opts.format !== "svg") { + reject(new Error( + "Sorry IE does not support downloading from canvas. Try {format:'svg'} instead." + )); + } - gd._snapshotInProgress = true; - var promise = toImage(gd, opts); + gd._snapshotInProgress = true; + var promise = toImage(gd, opts); - var filename = opts.filename || gd.fn || 'newplot'; - filename += '.' + opts.format; + var filename = opts.filename || gd.fn || "newplot"; + filename += "." + opts.format; - promise.then(function(result) { - gd._snapshotInProgress = false; - return fileSaver(result, filename); - }).then(function(name) { - resolve(name); - }).catch(function(err) { - gd._snapshotInProgress = false; - reject(err); - }); - }); + promise + .then(function(result) { + gd._snapshotInProgress = false; + return fileSaver(result, filename); + }) + .then(function(name) { + resolve(name); + }) + .catch(function(err) { + gd._snapshotInProgress = false; + reject(err); + }); + }); } module.exports = downloadImage; diff --git a/src/snapshot/filesaver.js b/src/snapshot/filesaver.js index 88109ffe7dd..a2976156cbc 100644 --- a/src/snapshot/filesaver.js +++ b/src/snapshot/filesaver.js @@ -5,7 +5,6 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - /* * substantial portions of this code from FileSaver.js * https://github.com/eligrey/FileSaver.js @@ -18,49 +17,51 @@ * License: MIT * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md */ - -'use strict'; - +"use strict"; var fileSaver = function(url, name) { - var saveLink = document.createElement('a'); - var canUseSaveLink = 'download' in saveLink; - var isSafari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent); - var promise = new Promise(function(resolve, reject) { - // IE <10 is explicitly unsupported - if(typeof navigator !== 'undefined' && /MSIE [1-9]\./.test(navigator.userAgent)) { - reject(new Error('IE < 10 unsupported')); - } + var saveLink = document.createElement("a"); + var canUseSaveLink = "download" in saveLink; + var isSafari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent); + var promise = new Promise(function(resolve, reject) { + // IE <10 is explicitly unsupported + if ( + typeof navigator !== "undefined" && + /MSIE [1-9]\./.test(navigator.userAgent) + ) { + reject(new Error("IE < 10 unsupported")); + } - // First try a.download, then web filesystem, then object URLs - if(isSafari) { - // Safari doesn't allow downloading of blob urls - document.location.href = 'data:application/octet-stream' + url.slice(url.search(/[,;]/)); - resolve(name); - } + // First try a.download, then web filesystem, then object URLs + if (isSafari) { + // Safari doesn't allow downloading of blob urls + document.location.href = "data:application/octet-stream" + + url.slice(url.search(/[,;]/)); + resolve(name); + } - if(!name) { - name = 'download'; - } + if (!name) { + name = "download"; + } - if(canUseSaveLink) { - saveLink.href = url; - saveLink.download = name; - document.body.appendChild(saveLink); - saveLink.click(); - document.body.removeChild(saveLink); - resolve(name); - } + if (canUseSaveLink) { + saveLink.href = url; + saveLink.download = name; + document.body.appendChild(saveLink); + saveLink.click(); + document.body.removeChild(saveLink); + resolve(name); + } - // IE 10+ (native saveAs) - if(typeof navigator !== 'undefined' && navigator.msSaveBlob) { - navigator.msSaveBlob(new Blob([url]), name); - resolve(name); - } + // IE 10+ (native saveAs) + if (typeof navigator !== "undefined" && navigator.msSaveBlob) { + navigator.msSaveBlob(new Blob([url]), name); + resolve(name); + } - reject(new Error('download error')); - }); + reject(new Error("download error")); + }); - return promise; + return promise; }; module.exports = fileSaver; diff --git a/src/snapshot/helpers.js b/src/snapshot/helpers.js index 8af139fc9eb..8584fc4e3de 100644 --- a/src/snapshot/helpers.js +++ b/src/snapshot/helpers.js @@ -5,27 +5,22 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; exports.getDelay = function(fullLayout) { + // polar clears fullLayout._has for some reason + if (!fullLayout._has) return 0; - // polar clears fullLayout._has for some reason - if(!fullLayout._has) return 0; - - // maybe we should add a 'gl' (and 'svg') layoutCategory ?? - return (fullLayout._has('gl3d') || fullLayout._has('gl2d')) ? 500 : 0; + // maybe we should add a 'gl' (and 'svg') layoutCategory ?? + return fullLayout._has("gl3d") || fullLayout._has("gl2d") ? 500 : 0; }; exports.getRedrawFunc = function(gd) { - - // do not work if polar is present - if((gd.data && gd.data[0] && gd.data[0].r)) return; - - return function() { - (gd.calcdata || []).forEach(function(d) { - if(d[0] && d[0].t && d[0].t.cb) d[0].t.cb(); - }); - }; + // do not work if polar is present + if (gd.data && gd.data[0] && gd.data[0].r) return; + + return function() { + (gd.calcdata || []).forEach(function(d) { + if (d[0] && d[0].t && d[0].t.cb) d[0].t.cb(); + }); + }; }; diff --git a/src/snapshot/index.js b/src/snapshot/index.js index a8f56fbe19f..c11d146679b 100644 --- a/src/snapshot/index.js +++ b/src/snapshot/index.js @@ -5,20 +5,17 @@ * 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 helpers = require('./helpers'); +"use strict"; +var helpers = require("./helpers"); var Snapshot = { - getDelay: helpers.getDelay, - getRedrawFunc: helpers.getRedrawFunc, - clone: require('./cloneplot'), - toSVG: require('./tosvg'), - svgToImg: require('./svgtoimg'), - toImage: require('./toimage'), - downloadImage: require('./download') + getDelay: helpers.getDelay, + getRedrawFunc: helpers.getRedrawFunc, + clone: require("./cloneplot"), + toSVG: require("./tosvg"), + svgToImg: require("./svgtoimg"), + toImage: require("./toimage"), + downloadImage: require("./download") }; module.exports = Snapshot; diff --git a/src/snapshot/svgtoimg.js b/src/snapshot/svgtoimg.js index 05eddab673e..7d97cc2ed9d 100644 --- a/src/snapshot/svgtoimg.js +++ b/src/snapshot/svgtoimg.js @@ -5,125 +5,123 @@ * 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 EventEmitter = require('events').EventEmitter; +"use strict"; +var Lib = require("../lib"); +var EventEmitter = require("events").EventEmitter; function svgToImg(opts) { - - var ev = opts.emitter || new EventEmitter(); - - var promise = new Promise(function(resolve, reject) { - - var Image = window.Image; - - var svg = opts.svg; - var format = opts.format || 'png'; - - // IE is very strict, so we will need to clean - // svg with the following regex - // yes this is messy, but do not know a better way - // Even with this IE will not work due to tainted canvas - // see https://github.com/kangax/fabric.js/issues/1957 - // http://stackoverflow.com/questions/18112047/canvas-todataurl-working-in-all-browsers-except-ie10 - // Leave here just in case the CORS/tainted IE issue gets resolved - if(Lib.isIE()) { - // replace double quote with single quote - svg = svg.replace(/"/gi, '\''); - // url in svg are single quoted - // since we changed double to single - // we'll need to change these to double-quoted - svg = svg.replace(/(\('#)(.*)('\))/gi, '(\"$2\")'); - // font names with spaces will be escaped single-quoted - // we'll need to change these to double-quoted - svg = svg.replace(/(\\')/gi, '\"'); - // IE only support svg - if(format !== 'svg') { - var ieSvgError = new Error('Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.'); - reject(ieSvgError); - // eventually remove the ev - // in favor of promises - if(!opts.promise) { - return ev.emit('error', ieSvgError); - } else { - return promise; - } - } + var ev = opts.emitter || new EventEmitter(); + + var promise = new Promise(function(resolve, reject) { + var Image = window.Image; + + var svg = opts.svg; + var format = opts.format || "png"; + + // IE is very strict, so we will need to clean + // svg with the following regex + // yes this is messy, but do not know a better way + // Even with this IE will not work due to tainted canvas + // see https://github.com/kangax/fabric.js/issues/1957 + // http://stackoverflow.com/questions/18112047/canvas-todataurl-working-in-all-browsers-except-ie10 + // Leave here just in case the CORS/tainted IE issue gets resolved + if (Lib.isIE()) { + // replace double quote with single quote + svg = svg.replace(/"/gi, "'"); + // url in svg are single quoted + // since we changed double to single + // we'll need to change these to double-quoted + svg = svg.replace(/(\('#)(.*)('\))/gi, '("$2")'); + // font names with spaces will be escaped single-quoted + // we'll need to change these to double-quoted + svg = svg.replace(/(\\')/gi, '"'); + // IE only support svg + if (format !== "svg") { + var ieSvgError = new Error( + "Sorry IE does not support downloading from canvas. Try {format:'svg'} instead." + ); + reject(ieSvgError); + // eventually remove the ev + // in favor of promises + if (!opts.promise) { + return ev.emit("error", ieSvgError); + } else { + return promise; } - - var canvas = opts.canvas; - - var ctx = canvas.getContext('2d'); - var img = new Image(); - - // for Safari support, eliminate createObjectURL - // this decision could cause problems if content - // is not restricted to svg - var url = 'data:image/svg+xml,' + encodeURIComponent(svg); - - canvas.height = opts.height || 150; - canvas.width = opts.width || 300; - - img.onload = function() { - var imgData; - - // don't need to draw to canvas if svg - // save some time and also avoid failure on IE - if(format !== 'svg') { - ctx.drawImage(img, 0, 0); - } - - switch(format) { - case 'jpeg': - imgData = canvas.toDataURL('image/jpeg'); - break; - case 'png': - imgData = canvas.toDataURL('image/png'); - break; - case 'webp': - imgData = canvas.toDataURL('image/webp'); - break; - case 'svg': - imgData = url; - break; - default: - reject(new Error('Image format is not jpeg, png or svg')); - // eventually remove the ev - // in favor of promises - if(!opts.promise) { - return ev.emit('error', 'Image format is not jpeg, png or svg'); - } - } - resolve(imgData); - // eventually remove the ev - // in favor of promises - if(!opts.promise) { - ev.emit('success', imgData); - } - }; - - img.onerror = function(err) { - reject(err); - // eventually remove the ev - // in favor of promises - if(!opts.promise) { - return ev.emit('error', err); - } - }; - - img.src = url; - }); - - // temporary for backward compatibility - // move to only Promise in 2.0.0 - // and eliminate the EventEmitter - if(opts.promise) { - return promise; + } } - return ev; + var canvas = opts.canvas; + + var ctx = canvas.getContext("2d"); + var img = new Image(); + + // for Safari support, eliminate createObjectURL + // this decision could cause problems if content + // is not restricted to svg + var url = "data:image/svg+xml," + encodeURIComponent(svg); + + canvas.height = opts.height || 150; + canvas.width = opts.width || 300; + + img.onload = function() { + var imgData; + + // don't need to draw to canvas if svg + // save some time and also avoid failure on IE + if (format !== "svg") { + ctx.drawImage(img, 0, 0); + } + + switch (format) { + case "jpeg": + imgData = canvas.toDataURL("image/jpeg"); + break; + case "png": + imgData = canvas.toDataURL("image/png"); + break; + case "webp": + imgData = canvas.toDataURL("image/webp"); + break; + case "svg": + imgData = url; + break; + default: + reject(new Error("Image format is not jpeg, png or svg")); + // eventually remove the ev + // in favor of promises + if (!opts.promise) { + return ev.emit("error", "Image format is not jpeg, png or svg"); + } + } + resolve(imgData); + // eventually remove the ev + // in favor of promises + if (!opts.promise) { + ev.emit("success", imgData); + } + }; + + img.onerror = function(err) { + reject(err); + // eventually remove the ev + // in favor of promises + if (!opts.promise) { + return ev.emit("error", err); + } + }; + + img.src = url; + }); + + // temporary for backward compatibility + // move to only Promise in 2.0.0 + // and eliminate the EventEmitter + if (opts.promise) { + return promise; + } + + return ev; } module.exports = svgToImg; diff --git a/src/snapshot/toimage.js b/src/snapshot/toimage.js index db0a2a1d1ac..3fa7a0d7321 100644 --- a/src/snapshot/toimage.js +++ b/src/snapshot/toimage.js @@ -5,19 +5,16 @@ * 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 EventEmitter = require("events").EventEmitter; -'use strict'; - -var EventEmitter = require('events').EventEmitter; - -var Plotly = require('../plotly'); -var Lib = require('../lib'); - -var helpers = require('./helpers'); -var clonePlot = require('./cloneplot'); -var toSVG = require('./tosvg'); -var svgToImg = require('./svgtoimg'); +var Plotly = require("../plotly"); +var Lib = require("../lib"); +var helpers = require("./helpers"); +var clonePlot = require("./cloneplot"); +var toSVG = require("./tosvg"); +var svgToImg = require("./svgtoimg"); /** * @param {object} gd figure Object @@ -25,54 +22,54 @@ var svgToImg = require('./svgtoimg'); * @param opts.format 'jpeg' | 'png' | 'webp' | 'svg' */ function toImage(gd, opts) { + // first clone the GD so we can operate in a clean environment + var ev = new EventEmitter(); + + var clone = clonePlot(gd, { format: "png" }); + var clonedGd = clone.gd; + + // put the cloned div somewhere off screen before attaching to DOM + clonedGd.style.position = "absolute"; + clonedGd.style.left = "-5000px"; + document.body.appendChild(clonedGd); + + function wait() { + var delay = helpers.getDelay(clonedGd._fullLayout); + + setTimeout( + function() { + var svg = toSVG(clonedGd); + + var canvas = document.createElement("canvas"); + canvas.id = Lib.randstr(); + + ev = svgToImg({ + format: opts.format, + width: clonedGd._fullLayout.width, + height: clonedGd._fullLayout.height, + canvas: canvas, + emitter: ev, + svg: svg + }); - // first clone the GD so we can operate in a clean environment - var ev = new EventEmitter(); - - var clone = clonePlot(gd, {format: 'png'}); - var clonedGd = clone.gd; - - // put the cloned div somewhere off screen before attaching to DOM - clonedGd.style.position = 'absolute'; - clonedGd.style.left = '-5000px'; - document.body.appendChild(clonedGd); - - function wait() { - var delay = helpers.getDelay(clonedGd._fullLayout); - - setTimeout(function() { - var svg = toSVG(clonedGd); - - var canvas = document.createElement('canvas'); - canvas.id = Lib.randstr(); - - ev = svgToImg({ - format: opts.format, - width: clonedGd._fullLayout.width, - height: clonedGd._fullLayout.height, - canvas: canvas, - emitter: ev, - svg: svg - }); - - ev.clean = function() { - if(clonedGd) document.body.removeChild(clonedGd); - }; - - }, delay); - } - - var redrawFunc = helpers.getRedrawFunc(clonedGd); + ev.clean = function() { + if (clonedGd) document.body.removeChild(clonedGd); + }; + }, + delay + ); + } - Plotly.plot(clonedGd, clone.data, clone.layout, clone.config) - .then(redrawFunc) - .then(wait) - .catch(function(err) { - ev.emit('error', err); - }); + var redrawFunc = helpers.getRedrawFunc(clonedGd); + Plotly.plot(clonedGd, clone.data, clone.layout, clone.config) + .then(redrawFunc) + .then(wait) + .catch(function(err) { + ev.emit("error", err); + }); - return ev; + return ev; } module.exports = toImage; diff --git a/src/snapshot/tosvg.js b/src/snapshot/tosvg.js index 92188e7b3ff..cbb36f16144 100644 --- a/src/snapshot/tosvg.js +++ b/src/snapshot/tosvg.js @@ -5,113 +5,114 @@ * 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 d3 = require("d3"); +var svgTextUtils = require("../lib/svg_text_utils"); +var Drawing = require("../components/drawing"); +var Color = require("../components/color"); -'use strict'; - -var d3 = require('d3'); - -var svgTextUtils = require('../lib/svg_text_utils'); -var Drawing = require('../components/drawing'); -var Color = require('../components/color'); - -var xmlnsNamespaces = require('../constants/xmlns_namespaces'); - +var xmlnsNamespaces = require("../constants/xmlns_namespaces"); module.exports = function toSVG(gd, format) { - var fullLayout = gd._fullLayout, - svg = fullLayout._paper, - toppaper = fullLayout._toppaper, - i; - - // make background color a rect in the svg, then revert after scraping - // all other alterations have been dealt with by properly preparing the svg - // in the first place... like setting cursors with css classes so we don't - // have to remove them, and providing the right namespaces in the svg to - // begin with - svg.insert('rect', ':first-child') - .call(Drawing.setRect, 0, 0, fullLayout.width, fullLayout.height) - .call(Color.fill, fullLayout.paper_bgcolor); - - // subplot-specific to-SVG methods - // which notably add the contents of the gl-container - // into the main svg node - var basePlotModules = fullLayout._basePlotModules || []; - for(i = 0; i < basePlotModules.length; i++) { - var _module = basePlotModules[i]; - - if(_module.toSVG) _module.toSVG(gd); - } - - // add top items above them assumes everything in toppaper is either - // a group or a defs, and if it's empty (like hoverlayer) we can ignore it. - if(toppaper) { - var nodes = toppaper.node().childNodes; - - // make copy of nodes as childNodes prop gets mutated in loop below - var topGroups = Array.prototype.slice.call(nodes); - - for(i = 0; i < topGroups.length; i++) { - var topGroup = topGroups[i]; - - if(topGroup.childNodes.length) svg.node().appendChild(topGroup); - } + var fullLayout = gd._fullLayout, + svg = fullLayout._paper, + toppaper = fullLayout._toppaper, + i; + + // make background color a rect in the svg, then revert after scraping + // all other alterations have been dealt with by properly preparing the svg + // in the first place... like setting cursors with css classes so we don't + // have to remove them, and providing the right namespaces in the svg to + // begin with + svg + .insert("rect", ":first-child") + .call(Drawing.setRect, 0, 0, fullLayout.width, fullLayout.height) + .call(Color.fill, fullLayout.paper_bgcolor); + + // subplot-specific to-SVG methods + // which notably add the contents of the gl-container + // into the main svg node + var basePlotModules = fullLayout._basePlotModules || []; + for (i = 0; i < basePlotModules.length; i++) { + var _module = basePlotModules[i]; + + if (_module.toSVG) _module.toSVG(gd); + } + + // add top items above them assumes everything in toppaper is either + // a group or a defs, and if it's empty (like hoverlayer) we can ignore it. + if (toppaper) { + var nodes = toppaper.node().childNodes; + + // make copy of nodes as childNodes prop gets mutated in loop below + var topGroups = Array.prototype.slice.call(nodes); + + for (i = 0; i < topGroups.length; i++) { + var topGroup = topGroups[i]; + + if (topGroup.childNodes.length) svg.node().appendChild(topGroup); } - - // remove draglayer for Adobe Illustrator compatibility - if(fullLayout._draggers) { - fullLayout._draggers.remove(); + } + + // remove draglayer for Adobe Illustrator compatibility + if (fullLayout._draggers) { + fullLayout._draggers.remove(); + } + + // in case the svg element had an explicit background color, remove this + // we want the rect to get the color so it's the right size; svg bg will + // fill whatever container it's displayed in regardless of plot size. + svg.node().style.background = ""; + + svg.selectAll("text").attr("data-unformatted", null).each(function() { + var txt = d3.select(this); + + // hidden text is pre-formatting mathjax, + // the browser ignores it but it can still confuse batik + if (txt.style("visibility") === "hidden") { + txt.remove(); + return; + } else { + // force other visibility value to export as visible + // to not potentially confuse non-browser SVG implementations + txt.style("visibility", "visible"); } - // in case the svg element had an explicit background color, remove this - // we want the rect to get the color so it's the right size; svg bg will - // fill whatever container it's displayed in regardless of plot size. - svg.node().style.background = ''; - - svg.selectAll('text') - .attr('data-unformatted', null) - .each(function() { - var txt = d3.select(this); - - // hidden text is pre-formatting mathjax, - // the browser ignores it but it can still confuse batik - if(txt.style('visibility') === 'hidden') { - txt.remove(); - return; - } - else { - // force other visibility value to export as visible - // to not potentially confuse non-browser SVG implementations - txt.style('visibility', 'visible'); - } - - // Font family styles break things because of quotation marks, - // so we must remove them *after* the SVG DOM has been serialized - // to a string (browsers convert singles back) - var ff = txt.style('font-family'); - if(ff && ff.indexOf('"') !== -1) { - txt.style('font-family', ff.replace(/"/g, 'TOBESTRIPPED')); - } - }); - - if(format === 'pdf' || format === 'eps') { - // these formats make the extra line MathJax adds around symbols look super thick in some cases - // it looks better if this is removed entirely. - svg.selectAll('#MathJax_SVG_glyphs path') - .attr('stroke-width', 0); + // Font family styles break things because of quotation marks, + // so we must remove them *after* the SVG DOM has been serialized + // to a string (browsers convert singles back) + var ff = txt.style("font-family"); + if (ff && ff.indexOf('"') !== -1) { + txt.style("font-family", ff.replace(/"/g, "TOBESTRIPPED")); } - - // fix for IE namespacing quirk? - // http://stackoverflow.com/questions/19610089/unwanted-namespaces-on-svg-markup-when-using-xmlserializer-in-javascript-with-ie - svg.node().setAttributeNS(xmlnsNamespaces.xmlns, 'xmlns', xmlnsNamespaces.svg); - svg.node().setAttributeNS(xmlnsNamespaces.xmlns, 'xmlns:xlink', xmlnsNamespaces.xlink); - - var s = new window.XMLSerializer().serializeToString(svg.node()); - s = svgTextUtils.html_entity_decode(s); - s = svgTextUtils.xml_entity_encode(s); - - // Fix quotations around font strings - s = s.replace(/("TOBESTRIPPED)|(TOBESTRIPPED")/g, '\''); - - return s; + }); + + if (format === "pdf" || format === "eps") { + // these formats make the extra line MathJax adds around symbols look super thick in some cases + // it looks better if this is removed entirely. + svg.selectAll("#MathJax_SVG_glyphs path").attr("stroke-width", 0); + } + + // fix for IE namespacing quirk? + // http://stackoverflow.com/questions/19610089/unwanted-namespaces-on-svg-markup-when-using-xmlserializer-in-javascript-with-ie + svg + .node() + .setAttributeNS(xmlnsNamespaces.xmlns, "xmlns", xmlnsNamespaces.svg); + svg + .node() + .setAttributeNS( + xmlnsNamespaces.xmlns, + "xmlns:xlink", + xmlnsNamespaces.xlink + ); + + var s = new window.XMLSerializer().serializeToString(svg.node()); + s = svgTextUtils.html_entity_decode(s); + s = svgTextUtils.xml_entity_encode(s); + + // Fix quotations around font strings + s = s.replace(/("TOBESTRIPPED)|(TOBESTRIPPED")/g, "'"); + + return s; }; diff --git a/src/traces/bar/arrays_to_calcdata.js b/src/traces/bar/arrays_to_calcdata.js index 15ecc601d72..4196d66b552 100644 --- a/src/traces/bar/arrays_to_calcdata.js +++ b/src/traces/bar/arrays_to_calcdata.js @@ -5,26 +5,22 @@ * 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 mergeArray = require('../../lib').mergeArray; - +"use strict"; +var mergeArray = require("../../lib").mergeArray; // arrayOk attributes, merge them into calcdata array module.exports = function arraysToCalcdata(cd, trace) { - mergeArray(trace.text, cd, 'tx'); + mergeArray(trace.text, cd, "tx"); - var marker = trace.marker; - if(marker) { - mergeArray(marker.opacity, cd, 'mo'); - mergeArray(marker.color, cd, 'mc'); + var marker = trace.marker; + if (marker) { + mergeArray(marker.opacity, cd, "mo"); + mergeArray(marker.color, cd, "mc"); - var markerLine = marker.line; - if(markerLine) { - mergeArray(markerLine.color, cd, 'mlc'); - mergeArray(markerLine.width, cd, 'mlw'); - } + var markerLine = marker.line; + if (markerLine) { + mergeArray(markerLine.color, cd, "mlc"); + mergeArray(markerLine.width, cd, "mlw"); } + } }; diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js index 95141454624..33fe8424c3a 100644 --- a/src/traces/bar/attributes.js +++ b/src/traces/bar/attributes.js @@ -5,17 +5,15 @@ * 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 scatterAttrs = require("../scatter/attributes"); +var colorAttributes = require("../../components/colorscale/color_attributes"); +var errorBarAttrs = require("../../components/errorbars/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); +var fontAttrs = require("../../plots/font_attributes"); -'use strict'; - -var scatterAttrs = require('../scatter/attributes'); -var colorAttributes = require('../../components/colorscale/color_attributes'); -var errorBarAttrs = require('../../components/errorbars/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); -var fontAttrs = require('../../plots/font_attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; -var extendDeep = require('../../lib/extend').extendDeep; +var extendFlat = require("../../lib/extend").extendFlat; +var extendDeep = require("../../lib/extend").extendDeep; var textFontAttrs = extendDeep({}, fontAttrs); textFontAttrs.family.arrayOk = true; @@ -25,123 +23,106 @@ textFontAttrs.color.arrayOk = true; var scatterMarkerAttrs = scatterAttrs.marker; var scatterMarkerLineAttrs = scatterMarkerAttrs.line; -var markerLineWidth = extendFlat({}, - scatterMarkerLineAttrs.width, { dflt: 0 }); +var markerLineWidth = extendFlat({}, scatterMarkerLineAttrs.width, { dflt: 0 }); -var markerLine = extendFlat({}, { - width: markerLineWidth -}, colorAttributes('marker.line')); +var markerLine = extendFlat( + {}, + { width: markerLineWidth }, + colorAttributes("marker.line") +); -var marker = extendFlat({}, { - line: markerLine -}, colorAttributes('marker'), { - showscale: scatterMarkerAttrs.showscale, - colorbar: colorbarAttrs +var marker = extendFlat({}, { line: markerLine }, colorAttributes("marker"), { + showscale: scatterMarkerAttrs.showscale, + colorbar: colorbarAttrs }); - module.exports = { - x: scatterAttrs.x, - x0: scatterAttrs.x0, - dx: scatterAttrs.dx, - y: scatterAttrs.y, - y0: scatterAttrs.y0, - dy: scatterAttrs.dy, - - text: scatterAttrs.text, - - textposition: { - valType: 'enumerated', - role: 'info', - values: ['inside', 'outside', 'auto', 'none'], - dflt: 'none', - arrayOk: true, - description: [ - 'Specifies the location of the `text`.', - '*inside* positions `text` inside, next to the bar end', - '(rotated and scaled if needed).', - '*outside* positions `text` outside, next to the bar end', - '(scaled if needed).', - '*auto* positions `text` inside or outside', - 'so that `text` size is maximized.' - ].join(' ') - }, - - textfont: extendFlat({}, textFontAttrs, { - description: 'Sets the font used for `text`.' - }), - - insidetextfont: extendFlat({}, textFontAttrs, { - description: 'Sets the font used for `text` lying inside the bar.' - }), - - outsidetextfont: extendFlat({}, textFontAttrs, { - description: 'Sets the font used for `text` lying outside the bar.' - }), - - orientation: { - valType: 'enumerated', - role: 'info', - values: ['v', 'h'], - description: [ - 'Sets the orientation of the bars.', - 'With *v* (*h*), the value of the each bar spans', - 'along the vertical (horizontal).' - ].join(' ') - }, - - base: { - valType: 'any', - dflt: null, - arrayOk: true, - role: 'info', - description: [ - 'Sets where the bar base is drawn (in position axis units).', - 'In *stack* or *relative* barmode,', - 'traces that set *base* will be excluded', - 'and drawn in *overlay* mode instead.' - ].join(' ') - }, - - offset: { - valType: 'number', - dflt: null, - arrayOk: true, - role: 'info', - description: [ - 'Shifts the position where the bar is drawn', - '(in position axis units).', - 'In *group* barmode,', - 'traces that set *offset* will be excluded', - 'and drawn in *overlay* mode instead.' - ].join(' ') - }, - - width: { - valType: 'number', - dflt: null, - min: 0, - arrayOk: true, - role: 'info', - description: [ - 'Sets the bar width (in position axis units).' - ].join(' ') - }, - - marker: marker, - - r: scatterAttrs.r, - t: scatterAttrs.t, - - error_y: errorBarAttrs, - error_x: errorBarAttrs, - - _deprecated: { - bardir: { - valType: 'enumerated', - role: 'info', - values: ['v', 'h'], - description: 'Renamed to `orientation`.' - } + x: scatterAttrs.x, + x0: scatterAttrs.x0, + dx: scatterAttrs.dx, + y: scatterAttrs.y, + y0: scatterAttrs.y0, + dy: scatterAttrs.dy, + text: scatterAttrs.text, + textposition: { + valType: "enumerated", + role: "info", + values: ["inside", "outside", "auto", "none"], + dflt: "none", + arrayOk: true, + description: [ + "Specifies the location of the `text`.", + "*inside* positions `text` inside, next to the bar end", + "(rotated and scaled if needed).", + "*outside* positions `text` outside, next to the bar end", + "(scaled if needed).", + "*auto* positions `text` inside or outside", + "so that `text` size is maximized." + ].join(" ") + }, + textfont: extendFlat({}, textFontAttrs, { + description: "Sets the font used for `text`." + }), + insidetextfont: extendFlat({}, textFontAttrs, { + description: "Sets the font used for `text` lying inside the bar." + }), + outsidetextfont: extendFlat({}, textFontAttrs, { + description: "Sets the font used for `text` lying outside the bar." + }), + orientation: { + valType: "enumerated", + role: "info", + values: ["v", "h"], + description: [ + "Sets the orientation of the bars.", + "With *v* (*h*), the value of the each bar spans", + "along the vertical (horizontal)." + ].join(" ") + }, + base: { + valType: "any", + dflt: null, + arrayOk: true, + role: "info", + description: [ + "Sets where the bar base is drawn (in position axis units).", + "In *stack* or *relative* barmode,", + "traces that set *base* will be excluded", + "and drawn in *overlay* mode instead." + ].join(" ") + }, + offset: { + valType: "number", + dflt: null, + arrayOk: true, + role: "info", + description: [ + "Shifts the position where the bar is drawn", + "(in position axis units).", + "In *group* barmode,", + "traces that set *offset* will be excluded", + "and drawn in *overlay* mode instead." + ].join(" ") + }, + width: { + valType: "number", + dflt: null, + min: 0, + arrayOk: true, + role: "info", + description: ["Sets the bar width (in position axis units)."].join(" ") + }, + marker: marker, + r: scatterAttrs.r, + t: scatterAttrs.t, + error_y: errorBarAttrs, + error_x: errorBarAttrs, + _deprecated: { + bardir: { + valType: "enumerated", + role: "info", + values: ["v", "h"], + description: "Renamed to `orientation`." } + } }; diff --git a/src/traces/bar/calc.js b/src/traces/bar/calc.js index 24fb3352260..11e0771211e 100644 --- a/src/traces/bar/calc.js +++ b/src/traces/bar/calc.js @@ -5,101 +5,94 @@ * 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 isNumeric = require("fast-isnumeric"); +var Axes = require("../../plots/cartesian/axes"); +var hasColorscale = require("../../components/colorscale/has_colorscale"); +var colorscaleCalc = require("../../components/colorscale/calc"); -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Axes = require('../../plots/cartesian/axes'); -var hasColorscale = require('../../components/colorscale/has_colorscale'); -var colorscaleCalc = require('../../components/colorscale/calc'); - -var arraysToCalcdata = require('./arrays_to_calcdata'); - +var arraysToCalcdata = require("./arrays_to_calcdata"); module.exports = function calc(gd, trace) { - // depending on bar direction, set position and size axes - // and data ranges - // note: this logic for choosing orientation is - // duplicated in graph_obj->setstyles - - var xa = Axes.getFromId(gd, trace.xaxis || 'x'), - ya = Axes.getFromId(gd, trace.yaxis || 'y'), - orientation = trace.orientation || ((trace.x && !trace.y) ? 'h' : 'v'), - sa, pos, size, i, scalendar; - - if(orientation === 'h') { - sa = xa; - size = xa.makeCalcdata(trace, 'x'); - pos = ya.makeCalcdata(trace, 'y'); - - // not sure if it really makes sense to have dates for bar size data... - // ideally if we want to make gantt charts or something we'd treat - // the actual size (trace.x or y) as time delta but base as absolute - // time. But included here for completeness. - scalendar = trace.xcalendar; + // depending on bar direction, set position and size axes + // and data ranges + // note: this logic for choosing orientation is + // duplicated in graph_obj->setstyles + var xa = Axes.getFromId(gd, trace.xaxis || "x"), + ya = Axes.getFromId(gd, trace.yaxis || "y"), + orientation = trace.orientation || (trace.x && !trace.y ? "h" : "v"), + sa, + pos, + size, + i, + scalendar; + + if (orientation === "h") { + sa = xa; + size = xa.makeCalcdata(trace, "x"); + pos = ya.makeCalcdata(trace, "y"); + + // not sure if it really makes sense to have dates for bar size data... + // ideally if we want to make gantt charts or something we'd treat + // the actual size (trace.x or y) as time delta but base as absolute + // time. But included here for completeness. + scalendar = trace.xcalendar; + } else { + sa = ya; + size = ya.makeCalcdata(trace, "y"); + pos = xa.makeCalcdata(trace, "x"); + scalendar = trace.ycalendar; + } + + // create the "calculated data" to plot + var serieslen = Math.min(pos.length, size.length), cd = []; + + // set position + for (i = 0; i < serieslen; i++) { + // add bars with non-numeric sizes to calcdata + // so that ensure that traces with gaps are + // plotted in the correct order + if (isNumeric(pos[i])) { + cd.push({ p: pos[i] }); } - else { - sa = ya; - size = ya.makeCalcdata(trace, 'y'); - pos = xa.makeCalcdata(trace, 'x'); - scalendar = trace.ycalendar; - } - - // create the "calculated data" to plot - var serieslen = Math.min(pos.length, size.length), - cd = []; + } - // set position - for(i = 0; i < serieslen; i++) { + // set base + var base = trace.base, b; - // add bars with non-numeric sizes to calcdata - // so that ensure that traces with gaps are - // plotted in the correct order - - if(isNumeric(pos[i])) { - cd.push({p: pos[i]}); - } + if (Array.isArray(base)) { + for (i = 0; i < Math.min(base.length, cd.length); i++) { + b = sa.d2c(base[i], 0, scalendar); + cd[i].b = isNumeric(b) ? b : 0; } - - // set base - var base = trace.base, - b; - - if(Array.isArray(base)) { - for(i = 0; i < Math.min(base.length, cd.length); i++) { - b = sa.d2c(base[i], 0, scalendar); - cd[i].b = (isNumeric(b)) ? b : 0; - } - for(; i < cd.length; i++) { - cd[i].b = 0; - } + for (; i < cd.length; i++) { + cd[i].b = 0; } - else { - b = sa.d2c(base, 0, scalendar); - b = (isNumeric(b)) ? b : 0; - for(i = 0; i < cd.length; i++) { - cd[i].b = b; - } + } else { + b = sa.d2c(base, 0, scalendar); + b = isNumeric(b) ? b : 0; + for (i = 0; i < cd.length; i++) { + cd[i].b = b; } + } - // set size - for(i = 0; i < cd.length; i++) { - if(isNumeric(size[i])) { - cd[i].s = size[i]; - } + // set size + for (i = 0; i < cd.length; i++) { + if (isNumeric(size[i])) { + cd[i].s = size[i]; } + } - // auto-z and autocolorscale if applicable - if(hasColorscale(trace, 'marker')) { - colorscaleCalc(trace, trace.marker.color, 'marker', 'c'); - } - if(hasColorscale(trace, 'marker.line')) { - colorscaleCalc(trace, trace.marker.line.color, 'marker.line', 'c'); - } + // auto-z and autocolorscale if applicable + if (hasColorscale(trace, "marker")) { + colorscaleCalc(trace, trace.marker.color, "marker", "c"); + } + if (hasColorscale(trace, "marker.line")) { + colorscaleCalc(trace, trace.marker.line.color, "marker.line", "c"); + } - arraysToCalcdata(cd, trace); + arraysToCalcdata(cd, trace); - return cd; + return cd; }; diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index 614176cfc68..81f988e3574 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -5,53 +5,57 @@ * 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 Color = require('../../components/color'); - -var handleXYDefaults = require('../scatter/xy_defaults'); -var handleStyleDefaults = require('../bar/style_defaults'); -var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var coerceFont = Lib.coerceFont; - - var len = handleXYDefaults(traceIn, traceOut, layout, coerce); - if(!len) { - traceOut.visible = false; - return; - } - - coerce('orientation', (traceOut.x && !traceOut.y) ? 'h' : 'v'); - coerce('base'); - coerce('offset'); - coerce('width'); - - coerce('text'); - - var textPosition = coerce('textposition'); - - var hasBoth = Array.isArray(textPosition) || textPosition === 'auto', - hasInside = hasBoth || textPosition === 'inside', - hasOutside = hasBoth || textPosition === 'outside'; - if(hasInside || hasOutside) { - var textFont = coerceFont(coerce, 'textfont', layout.font); - if(hasInside) coerceFont(coerce, 'insidetextfont', textFont); - if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont); - } - - handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); - - // override defaultColor for error bars with defaultLine - errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'y'}); - errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'x', inherit: 'y'}); +"use strict"; +var Lib = require("../../lib"); +var Color = require("../../components/color"); + +var handleXYDefaults = require("../scatter/xy_defaults"); +var handleStyleDefaults = require("../bar/style_defaults"); +var errorBarsSupplyDefaults = require("../../components/errorbars/defaults"); +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var coerceFont = Lib.coerceFont; + + var len = handleXYDefaults(traceIn, traceOut, layout, coerce); + if (!len) { + traceOut.visible = false; + return; + } + + coerce("orientation", traceOut.x && !traceOut.y ? "h" : "v"); + coerce("base"); + coerce("offset"); + coerce("width"); + + coerce("text"); + + var textPosition = coerce("textposition"); + + var hasBoth = Array.isArray(textPosition) || textPosition === "auto", + hasInside = hasBoth || textPosition === "inside", + hasOutside = hasBoth || textPosition === "outside"; + if (hasInside || hasOutside) { + var textFont = coerceFont(coerce, "textfont", layout.font); + if (hasInside) coerceFont(coerce, "insidetextfont", textFont); + if (hasOutside) coerceFont(coerce, "outsidetextfont", textFont); + } + + handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); + + // override defaultColor for error bars with defaultLine + errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, { axis: "y" }); + errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, { + axis: "x", + inherit: "y" + }); }; diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js index f65f947461c..0ff7db67d78 100644 --- a/src/traces/bar/hover.js +++ b/src/traces/bar/hover.js @@ -5,87 +5,89 @@ * 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 Fx = require('../../plots/cartesian/graph_interact'); -var ErrorBars = require('../../components/errorbars'); -var Color = require('../../components/color'); - +"use strict"; +var Fx = require("../../plots/cartesian/graph_interact"); +var ErrorBars = require("../../components/errorbars"); +var Color = require("../../components/color"); module.exports = function hoverPoints(pointData, xval, yval, hovermode) { - var cd = pointData.cd, - trace = cd[0].trace, - t = cd[0].t, - xa = pointData.xa, - ya = pointData.ya, - barDelta = (hovermode === 'closest') ? - t.barwidth / 2 : - t.bargroupwidth / 2, - barPos; - - if(hovermode !== 'closest') barPos = function(di) { return di.p; }; - else if(trace.orientation === 'h') barPos = function(di) { return di.y; }; - else barPos = function(di) { return di.x; }; - - var dx, dy; - if(trace.orientation === 'h') { - dx = function(di) { - // add a gradient so hovering near the end of a - // bar makes it a little closer match - return Fx.inbox(di.b - xval, di.x - xval) + (di.x - xval) / (di.x - di.b); - }; - dy = function(di) { - var centerPos = barPos(di) - yval; - return Fx.inbox(centerPos - barDelta, centerPos + barDelta); - }; - } - else { - dy = function(di) { - return Fx.inbox(di.b - yval, di.y - yval) + (di.y - yval) / (di.y - di.b); - }; - dx = function(di) { - var centerPos = barPos(di) - xval; - return Fx.inbox(centerPos - barDelta, centerPos + barDelta); - }; - } - - var distfn = Fx.getDistanceFunction(hovermode, dx, dy); - Fx.getClosest(cd, distfn, pointData); - - // skip the rest (for this trace) if we didn't find a close point - if(pointData.index === false) return; - - // the closest data point - var di = cd[pointData.index], - mc = di.mcc || trace.marker.color, - mlc = di.mlcc || trace.marker.line.color, - mlw = di.mlw || trace.marker.line.width; - if(Color.opacity(mc)) pointData.color = mc; - else if(Color.opacity(mlc) && mlw) pointData.color = mlc; - - var size = (trace.base) ? di.b + di.s : di.s; - if(trace.orientation === 'h') { - pointData.x0 = pointData.x1 = xa.c2p(di.x, true); - pointData.xLabelVal = size; - - pointData.y0 = ya.c2p(barPos(di) - barDelta, true); - pointData.y1 = ya.c2p(barPos(di) + barDelta, true); - pointData.yLabelVal = di.p; - } - else { - pointData.y0 = pointData.y1 = ya.c2p(di.y, true); - pointData.yLabelVal = size; - - pointData.x0 = xa.c2p(barPos(di) - barDelta, true); - pointData.x1 = xa.c2p(barPos(di) + barDelta, true); - pointData.xLabelVal = di.p; - } - - if(di.tx) pointData.text = di.tx; - - ErrorBars.hoverInfo(di, trace, pointData); - - return [pointData]; + var cd = pointData.cd, + trace = cd[0].trace, + t = cd[0].t, + xa = pointData.xa, + ya = pointData.ya, + barDelta = hovermode === "closest" ? t.barwidth / 2 : t.bargroupwidth / 2, + barPos; + + if (hovermode !== "closest") { + barPos = function(di) { + return di.p; + }; + } else if (trace.orientation === "h") { + barPos = function(di) { + return di.y; + }; + } else { + barPos = function(di) { + return di.x; + }; + } + + var dx, dy; + if (trace.orientation === "h") { + dx = function(di) { + // add a gradient so hovering near the end of a + // bar makes it a little closer match + return Fx.inbox(di.b - xval, di.x - xval) + (di.x - xval) / (di.x - di.b); + }; + dy = function(di) { + var centerPos = barPos(di) - yval; + return Fx.inbox(centerPos - barDelta, centerPos + barDelta); + }; + } else { + dy = function(di) { + return Fx.inbox(di.b - yval, di.y - yval) + (di.y - yval) / (di.y - di.b); + }; + dx = function(di) { + var centerPos = barPos(di) - xval; + return Fx.inbox(centerPos - barDelta, centerPos + barDelta); + }; + } + + var distfn = Fx.getDistanceFunction(hovermode, dx, dy); + Fx.getClosest(cd, distfn, pointData); + + // skip the rest (for this trace) if we didn't find a close point + if (pointData.index === false) return; + + // the closest data point + var di = cd[pointData.index], + mc = di.mcc || trace.marker.color, + mlc = di.mlcc || trace.marker.line.color, + mlw = di.mlw || trace.marker.line.width; + if (Color.opacity(mc)) pointData.color = mc; + else if (Color.opacity(mlc) && mlw) pointData.color = mlc; + + var size = trace.base ? di.b + di.s : di.s; + if (trace.orientation === "h") { + pointData.x0 = pointData.x1 = xa.c2p(di.x, true); + pointData.xLabelVal = size; + + pointData.y0 = ya.c2p(barPos(di) - barDelta, true); + pointData.y1 = ya.c2p(barPos(di) + barDelta, true); + pointData.yLabelVal = di.p; + } else { + pointData.y0 = pointData.y1 = ya.c2p(di.y, true); + pointData.yLabelVal = size; + + pointData.x0 = xa.c2p(barPos(di) - barDelta, true); + pointData.x1 = xa.c2p(barPos(di) + barDelta, true); + pointData.xLabelVal = di.p; + } + + if (di.tx) pointData.text = di.tx; + + ErrorBars.hoverInfo(di, trace, pointData); + + return [pointData]; }; diff --git a/src/traces/bar/index.js b/src/traces/bar/index.js index f890fe8b673..66c229555fa 100644 --- a/src/traces/bar/index.js +++ b/src/traces/bar/index.js @@ -5,35 +5,39 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var Bar = {}; -Bar.attributes = require('./attributes'); -Bar.layoutAttributes = require('./layout_attributes'); -Bar.supplyDefaults = require('./defaults'); -Bar.supplyLayoutDefaults = require('./layout_defaults'); -Bar.calc = require('./calc'); -Bar.setPositions = require('./set_positions'); -Bar.colorbar = require('../scatter/colorbar'); -Bar.arraysToCalcdata = require('./arrays_to_calcdata'); -Bar.plot = require('./plot'); -Bar.style = require('./style'); -Bar.hoverPoints = require('./hover'); +Bar.attributes = require("./attributes"); +Bar.layoutAttributes = require("./layout_attributes"); +Bar.supplyDefaults = require("./defaults"); +Bar.supplyLayoutDefaults = require("./layout_defaults"); +Bar.calc = require("./calc"); +Bar.setPositions = require("./set_positions"); +Bar.colorbar = require("../scatter/colorbar"); +Bar.arraysToCalcdata = require("./arrays_to_calcdata"); +Bar.plot = require("./plot"); +Bar.style = require("./style"); +Bar.hoverPoints = require("./hover"); -Bar.moduleType = 'trace'; -Bar.name = 'bar'; -Bar.basePlotModule = require('../../plots/cartesian'); -Bar.categories = ['cartesian', 'bar', 'oriented', 'markerColorscale', 'errorBarsOK', 'showLegend']; +Bar.moduleType = "trace"; +Bar.name = "bar"; +Bar.basePlotModule = require("../../plots/cartesian"); +Bar.categories = [ + "cartesian", + "bar", + "oriented", + "markerColorscale", + "errorBarsOK", + "showLegend" +]; Bar.meta = { - description: [ - 'The data visualized by the span of the bars is set in `y`', - 'if `orientation` is set th *v* (the default)', - 'and the labels are set in `x`.', - 'By setting `orientation` to *h*, the roles are interchanged.' - ].join(' ') + description: [ + "The data visualized by the span of the bars is set in `y`", + "if `orientation` is set th *v* (the default)", + "and the labels are set in `x`.", + "By setting `orientation` to *h*, the roles are interchanged." + ].join(" ") }; module.exports = Bar; diff --git a/src/traces/bar/layout_attributes.js b/src/traces/bar/layout_attributes.js index 5dfb7c78191..ec310e9e3d8 100644 --- a/src/traces/bar/layout_attributes.js +++ b/src/traces/bar/layout_attributes.js @@ -5,59 +5,56 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - barmode: { - valType: 'enumerated', - values: ['stack', 'group', 'overlay', 'relative'], - dflt: 'group', - role: 'info', - description: [ - 'Determines how bars at the same location coordinate', - 'are displayed on the graph.', - 'With *stack*, the bars are stacked on top of one another', - 'With *relative*, the bars are stacked on top of one another,', - 'with negative values below the axis, positive values above', - 'With *group*, the bars are plotted next to one another', - 'centered around the shared location.', - 'With *overlay*, the bars are plotted over one another,', - 'you might need to an *opacity* to see multiple bars.' - ].join(' ') - }, - barnorm: { - valType: 'enumerated', - values: ['', 'fraction', 'percent'], - dflt: '', - role: 'info', - description: [ - 'Sets the normalization for bar traces on the graph.', - 'With *fraction*, the value of each bar is divide by the sum of the', - 'values at the location coordinate.', - 'With *percent*, the results form *fraction* are presented in percents.' - ].join(' ') - }, - bargap: { - valType: 'number', - min: 0, - max: 1, - role: 'style', - description: [ - 'Sets the gap (in plot fraction) between bars of', - 'adjacent location coordinates.' - ].join(' ') - }, - bargroupgap: { - valType: 'number', - min: 0, - max: 1, - dflt: 0, - role: 'style', - description: [ - 'Sets the gap (in plot fraction) between bars of', - 'the same location coordinate.' - ].join(' ') - } + barmode: { + valType: "enumerated", + values: ["stack", "group", "overlay", "relative"], + dflt: "group", + role: "info", + description: [ + "Determines how bars at the same location coordinate", + "are displayed on the graph.", + "With *stack*, the bars are stacked on top of one another", + "With *relative*, the bars are stacked on top of one another,", + "with negative values below the axis, positive values above", + "With *group*, the bars are plotted next to one another", + "centered around the shared location.", + "With *overlay*, the bars are plotted over one another,", + "you might need to an *opacity* to see multiple bars." + ].join(" ") + }, + barnorm: { + valType: "enumerated", + values: ["", "fraction", "percent"], + dflt: "", + role: "info", + description: [ + "Sets the normalization for bar traces on the graph.", + "With *fraction*, the value of each bar is divide by the sum of the", + "values at the location coordinate.", + "With *percent*, the results form *fraction* are presented in percents." + ].join(" ") + }, + bargap: { + valType: "number", + min: 0, + max: 1, + role: "style", + description: [ + "Sets the gap (in plot fraction) between bars of", + "adjacent location coordinates." + ].join(" ") + }, + bargroupgap: { + valType: "number", + min: 0, + max: 1, + dflt: 0, + role: "style", + description: [ + "Sets the gap (in plot fraction) between bars of", + "the same location coordinate." + ].join(" ") + } }; diff --git a/src/traces/bar/layout_defaults.js b/src/traces/bar/layout_defaults.js index 9fc2e030fe5..792129a4e17 100644 --- a/src/traces/bar/layout_defaults.js +++ b/src/traces/bar/layout_defaults.js @@ -5,52 +5,50 @@ * 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 Registry = require("../../registry"); +var Axes = require("../../plots/cartesian/axes"); +var Lib = require("../../lib"); - -'use strict'; - -var Registry = require('../../registry'); -var Axes = require('../../plots/cartesian/axes'); -var Lib = require('../../lib'); - -var layoutAttributes = require('./layout_attributes'); - +var layoutAttributes = require("./layout_attributes"); module.exports = function(layoutIn, layoutOut, fullData) { - function coerce(attr, dflt) { - return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); + function coerce(attr, dflt) { + return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); + } + + var hasBars = false, + shouldBeGapless = false, + gappedAnyway = false, + usedSubplots = {}; + + for (var i = 0; i < fullData.length; i++) { + var trace = fullData[i]; + if (Registry.traceIs(trace, "bar")) hasBars = true; + else continue; + + // if we have at least 2 grouped bar traces on the same subplot, + // we should default to a gap anyway, even if the data is histograms + if (layoutIn.barmode !== "overlay" && layoutIn.barmode !== "stack") { + var subploti = trace.xaxis + trace.yaxis; + if (usedSubplots[subploti]) gappedAnyway = true; + usedSubplots[subploti] = true; } - var hasBars = false, - shouldBeGapless = false, - gappedAnyway = false, - usedSubplots = {}; - - for(var i = 0; i < fullData.length; i++) { - var trace = fullData[i]; - if(Registry.traceIs(trace, 'bar')) hasBars = true; - else continue; - - // if we have at least 2 grouped bar traces on the same subplot, - // we should default to a gap anyway, even if the data is histograms - if(layoutIn.barmode !== 'overlay' && layoutIn.barmode !== 'stack') { - var subploti = trace.xaxis + trace.yaxis; - if(usedSubplots[subploti]) gappedAnyway = true; - usedSubplots[subploti] = true; - } - - if(trace.visible && trace.type === 'histogram') { - var pa = Axes.getFromId({_fullLayout: layoutOut}, - trace[trace.orientation === 'v' ? 'xaxis' : 'yaxis']); - if(pa.type !== 'category') shouldBeGapless = true; - } + if (trace.visible && trace.type === "histogram") { + var pa = Axes.getFromId( + { _fullLayout: layoutOut }, + trace[trace.orientation === "v" ? "xaxis" : "yaxis"] + ); + if (pa.type !== "category") shouldBeGapless = true; } + } - if(!hasBars) return; + if (!hasBars) return; - var mode = coerce('barmode'); - if(mode !== 'overlay') coerce('barnorm'); + var mode = coerce("barmode"); + if (mode !== "overlay") coerce("barnorm"); - coerce('bargap', (shouldBeGapless && !gappedAnyway) ? 0 : 0.2); - coerce('bargroupgap'); + coerce("bargap", shouldBeGapless && !gappedAnyway ? 0 : 0.2); + coerce("bargroupgap"); }; diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index ff69c21f01f..d1d391e50f5 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -5,516 +5,525 @@ * 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 d3 = require("d3"); +var isNumeric = require("fast-isnumeric"); +var tinycolor = require("tinycolor2"); +var Lib = require("../../lib"); +var svgTextUtils = require("../../lib/svg_text_utils"); -'use strict'; +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); +var ErrorBars = require("../../components/errorbars"); -var d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); -var tinycolor = require('tinycolor2'); - -var Lib = require('../../lib'); -var svgTextUtils = require('../../lib/svg_text_utils'); - -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); -var ErrorBars = require('../../components/errorbars'); - -var attributes = require('./attributes'), - attributeText = attributes.text, - attributeTextPosition = attributes.textposition, - attributeTextFont = attributes.textfont, - attributeInsideTextFont = attributes.insidetextfont, - attributeOutsideTextFont = attributes.outsidetextfont; +var attributes = require("./attributes"), + attributeText = attributes.text, + attributeTextPosition = attributes.textposition, + attributeTextFont = attributes.textfont, + attributeInsideTextFont = attributes.insidetextfont, + attributeOutsideTextFont = attributes.outsidetextfont; // padding in pixels around text var TEXTPAD = 3; module.exports = function plot(gd, plotinfo, cdbar) { - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - fullLayout = gd._fullLayout; - - var bartraces = plotinfo.plot.select('.barlayer') - .selectAll('g.trace.bars') - .data(cdbar); - - bartraces.enter().append('g') - .attr('class', 'trace bars'); - - bartraces.append('g') - .attr('class', 'points') - .each(function(d) { - var t = d[0].t, - trace = d[0].trace, - poffset = t.poffset, - poffsetIsArray = Array.isArray(poffset), - barwidth = t.barwidth, - barwidthIsArray = Array.isArray(barwidth); - - d3.select(this).selectAll('g.point') - .data(Lib.identity) - .enter().append('g').classed('point', true) - .each(function(di, i) { - // now display the bar - // clipped xf/yf (2nd arg true): non-positive - // log values go off-screen by plotwidth - // so you see them continue if you drag the plot - var p0 = di.p + ((poffsetIsArray) ? poffset[i] : poffset), - p1 = p0 + ((barwidthIsArray) ? barwidth[i] : barwidth), - s0 = di.b, - s1 = s0 + di.s; - - var x0, x1, y0, y1; - if(trace.orientation === 'h') { - y0 = ya.c2p(p0, true); - y1 = ya.c2p(p1, true); - x0 = xa.c2p(s0, true); - x1 = xa.c2p(s1, true); - } - else { - x0 = xa.c2p(p0, true); - x1 = xa.c2p(p1, true); - y0 = ya.c2p(s0, true); - y1 = ya.c2p(s1, true); - } - - if(!isNumeric(x0) || !isNumeric(x1) || - !isNumeric(y0) || !isNumeric(y1) || - x0 === x1 || y0 === y1) { - d3.select(this).remove(); - return; - } - - var lw = (di.mlw + 1 || trace.marker.line.width + 1 || - (di.trace ? di.trace.marker.line.width : 0) + 1) - 1, - offset = d3.round((lw / 2) % 1, 2); - - function roundWithLine(v) { - // if there are explicit gaps, don't round, - // it can make the gaps look crappy - return (fullLayout.bargap === 0 && fullLayout.bargroupgap === 0) ? - d3.round(Math.round(v) - offset, 2) : v; - } - - function expandToVisible(v, vc) { - // if it's not in danger of disappearing entirely, - // round more precisely - return Math.abs(v - vc) >= 2 ? roundWithLine(v) : - // but if it's very thin, expand it so it's - // necessarily visible, even if it might overlap - // its neighbor - (v > vc ? Math.ceil(v) : Math.floor(v)); - } - - if(!gd._context.staticPlot) { - // if bars are not fully opaque or they have a line - // around them, round to integer pixels, mainly for - // safari so we prevent overlaps from its expansive - // pixelation. if the bars ARE fully opaque and have - // no line, expand to a full pixel to make sure we - // can see them - var op = Color.opacity(di.mc || trace.marker.color), - fixpx = (op < 1 || lw > 0.01) ? - roundWithLine : expandToVisible; - x0 = fixpx(x0, x1); - x1 = fixpx(x1, x0); - y0 = fixpx(y0, y1); - y1 = fixpx(y1, y0); - } - - // append bar path and text - var bar = d3.select(this); - - bar.append('path').attr('d', - 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z'); - - appendBarText(gd, bar, d, i, x0, x1, y0, y1); - }); - }); - - // error bars are on the top - bartraces.call(ErrorBars.plot, plotinfo); - -}; - -function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { - function appendTextNode(bar, text, textFont) { - var textSelection = bar.append('text') - // prohibit tex interpretation until we can handle - // tex and regular text together - .attr('data-notex', 1) - .text(text) - .attr({ - 'class': 'bartext', - transform: '', - 'data-bb': '', - 'text-anchor': 'middle', - x: 0, - y: 0 - }) - .call(Drawing.font, textFont); - - textSelection.call(svgTextUtils.convertToTspans); - textSelection.selectAll('tspan.line').attr({x: 0, y: 0}); - - return textSelection; - } - - // get trace attributes - var trace = calcTrace[0].trace, - orientation = trace.orientation; - - var text = getText(trace, i); - if(!text) return; - - var textPosition = getTextPosition(trace, i); - if(textPosition === 'none') return; - - var textFont = getTextFont(trace, i, gd._fullLayout.font), - insideTextFont = getInsideTextFont(trace, i, textFont), - outsideTextFont = getOutsideTextFont(trace, i, textFont); - - // compute text position - var barmode = gd._fullLayout.barmode, - inStackMode = (barmode === 'stack'), - inRelativeMode = (barmode === 'relative'), - inStackOrRelativeMode = inStackMode || inRelativeMode, - - calcBar = calcTrace[i], - isOutmostBar = !inStackOrRelativeMode || calcBar._outmost, - - barWidth = Math.abs(x1 - x0) - 2 * TEXTPAD, // padding excluded - barHeight = Math.abs(y1 - y0) - 2 * TEXTPAD, // padding excluded - - textSelection, - textBB, - textWidth, - textHeight; - - if(textPosition === 'outside') { - if(!isOutmostBar) textPosition = 'inside'; - } - - if(textPosition === 'auto') { - if(isOutmostBar) { - // draw text using insideTextFont and check if it fits inside bar - textSelection = appendTextNode(bar, text, insideTextFont); - - textBB = Drawing.bBox(textSelection.node()), - textWidth = textBB.width, - textHeight = textBB.height; - - var textHasSize = (textWidth > 0 && textHeight > 0), - fitsInside = - (textWidth <= barWidth && textHeight <= barHeight), - fitsInsideIfRotated = - (textWidth <= barHeight && textHeight <= barWidth), - fitsInsideIfShrunk = (orientation === 'h') ? - (barWidth >= textWidth * (barHeight / textHeight)) : - (barHeight >= textHeight * (barWidth / textWidth)); - if(textHasSize && - (fitsInside || fitsInsideIfRotated || fitsInsideIfShrunk)) { - textPosition = 'inside'; - } - else { - textPosition = 'outside'; - textSelection.remove(); - textSelection = null; - } + var xa = plotinfo.xaxis, ya = plotinfo.yaxis, fullLayout = gd._fullLayout; + + var bartraces = plotinfo.plot + .select(".barlayer") + .selectAll("g.trace.bars") + .data(cdbar); + + bartraces.enter().append("g").attr("class", "trace bars"); + + bartraces.append("g").attr("class", "points").each(function(d) { + var t = d[0].t, + trace = d[0].trace, + poffset = t.poffset, + poffsetIsArray = Array.isArray(poffset), + barwidth = t.barwidth, + barwidthIsArray = Array.isArray(barwidth); + + d3 + .select(this) + .selectAll("g.point") + .data(Lib.identity) + .enter() + .append("g") + .classed("point", true) + .each(function(di, i) { + // now display the bar + // clipped xf/yf (2nd arg true): non-positive + // log values go off-screen by plotwidth + // so you see them continue if you drag the plot + var p0 = di.p + (poffsetIsArray ? poffset[i] : poffset), + p1 = p0 + (barwidthIsArray ? barwidth[i] : barwidth), + s0 = di.b, + s1 = s0 + di.s; + + var x0, x1, y0, y1; + if (trace.orientation === "h") { + y0 = ya.c2p(p0, true); + y1 = ya.c2p(p1, true); + x0 = xa.c2p(s0, true); + x1 = xa.c2p(s1, true); + } else { + x0 = xa.c2p(p0, true); + x1 = xa.c2p(p1, true); + y0 = ya.c2p(s0, true); + y1 = ya.c2p(s1, true); } - else textPosition = 'inside'; - } - - if(!textSelection) { - textSelection = appendTextNode(bar, text, - (textPosition === 'outside') ? - outsideTextFont : insideTextFont); - textBB = Drawing.bBox(textSelection.node()), - textWidth = textBB.width, - textHeight = textBB.height; + if ( + !isNumeric(x0) || + !isNumeric(x1) || + !isNumeric(y0) || + !isNumeric(y1) || + x0 === x1 || + y0 === y1 + ) { + d3.select(this).remove(); + return; + } - if(textWidth <= 0 || textHeight <= 0) { - textSelection.remove(); - return; + var lw = (di.mlw + 1 || + trace.marker.line.width + 1 || + (di.trace ? di.trace.marker.line.width : 0) + 1) - + 1, + offset = d3.round(lw / 2 % 1, 2); + + function roundWithLine(v) { + // if there are explicit gaps, don't round, + // it can make the gaps look crappy + return fullLayout.bargap === 0 && fullLayout.bargroupgap === 0 + ? d3.round(Math.round(v) - offset, 2) + : v; } - } - // compute text transform - var transform; - if(textPosition === 'outside') { - transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, - orientation); - } - else { - transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, - orientation); - } + function expandToVisible(v, vc) { + // if it's not in danger of disappearing entirely, + // round more precisely + return Math.abs(v - vc) >= 2 + ? roundWithLine(v) // but if it's very thin, expand it so it's // necessarily visible, even if it might overlap // its neighbor + : v > vc ? Math.ceil(v) : Math.floor(v); + } - textSelection.attr('transform', transform); -} + if (!gd._context.staticPlot) { + // if bars are not fully opaque or they have a line + // around them, round to integer pixels, mainly for + // safari so we prevent overlaps from its expansive + // pixelation. if the bars ARE fully opaque and have + // no line, expand to a full pixel to make sure we + // can see them + var op = Color.opacity(di.mc || trace.marker.color), + fixpx = op < 1 || lw > 0.01 ? roundWithLine : expandToVisible; + x0 = fixpx(x0, x1); + x1 = fixpx(x1, x0); + y0 = fixpx(y0, y1); + y1 = fixpx(y1, y0); + } -function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { - // compute text and target positions - var textWidth = textBB.width, - textHeight = textBB.height, - textX = (textBB.left + textBB.right) / 2, - textY = (textBB.top + textBB.bottom) / 2, - barWidth = Math.abs(x1 - x0), - barHeight = Math.abs(y1 - y0), - targetWidth, - targetHeight, - targetX, - targetY; - - // apply text padding - var textpad; - if(barWidth > (2 * TEXTPAD) && barHeight > (2 * TEXTPAD)) { - textpad = TEXTPAD; - barWidth -= 2 * textpad; - barHeight -= 2 * textpad; - } - else textpad = 0; + // append bar path and text + var bar = d3.select(this); - // compute rotation and scale - var rotate, - scale; + bar + .append("path") + .attr( + "d", + "M" + x0 + "," + y0 + "V" + y1 + "H" + x1 + "V" + y0 + "Z" + ); - if(textWidth <= barWidth && textHeight <= barHeight) { - // no scale or rotation is required - rotate = false; - scale = 1; - } - else if(textWidth <= barHeight && textHeight <= barWidth) { - // only rotation is required - rotate = true; - scale = 1; - } - else if((textWidth < textHeight) === (barWidth < barHeight)) { - // only scale is required - rotate = false; - scale = Math.min(barWidth / textWidth, barHeight / textHeight); - } - else { - // both scale and rotation are required - rotate = true; - scale = Math.min(barHeight / textWidth, barWidth / textHeight); - } + appendBarText(gd, bar, d, i, x0, x1, y0, y1); + }); + }); - if(rotate) rotate = 90; // rotate clockwise + // error bars are on the top + bartraces.call(ErrorBars.plot, plotinfo); +}; - // compute text and target positions - if(rotate) { - targetWidth = scale * textHeight; - targetHeight = scale * textWidth; +function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { + function appendTextNode(bar, text, textFont) { + var textSelection = bar + .append("text") + .attr("data-notex", 1) + .text(text) + .attr({ + class: "bartext", + transform: "", + "data-bb": "", + "text-anchor": "middle", + x: 0, + y: 0 + }) + .call(Drawing.font, textFont); + + textSelection.call(svgTextUtils.convertToTspans); + textSelection.selectAll("tspan.line").attr({ x: 0, y: 0 }); + + return textSelection; + } + + // get trace attributes + var trace = calcTrace[0].trace, orientation = trace.orientation; + + var text = getText(trace, i); + if (!text) return; + + var textPosition = getTextPosition(trace, i); + if (textPosition === "none") return; + + var textFont = getTextFont(trace, i, gd._fullLayout.font), + insideTextFont = getInsideTextFont(trace, i, textFont), + outsideTextFont = getOutsideTextFont(trace, i, textFont); + + // compute text position + var barmode = gd._fullLayout.barmode, + inStackMode = barmode === "stack", + inRelativeMode = barmode === "relative", + inStackOrRelativeMode = inStackMode || inRelativeMode, + calcBar = calcTrace[i], + isOutmostBar = !inStackOrRelativeMode || calcBar._outmost, + barWidth = Math.abs(x1 - x0) - 2 * TEXTPAD, + // padding excluded + barHeight = Math.abs(y1 - y0) - 2 * TEXTPAD, + // padding excluded + textSelection, + textBB, + textWidth, + textHeight; + + if (textPosition === "outside") { + if (!isOutmostBar) textPosition = "inside"; + } + + if (textPosition === "auto") { + if (isOutmostBar) { + // draw text using insideTextFont and check if it fits inside bar + textSelection = appendTextNode(bar, text, insideTextFont); + + textBB = Drawing.bBox( + textSelection.node() + ), textWidth = textBB.width, textHeight = textBB.height; + + var textHasSize = textWidth > 0 && textHeight > 0, + fitsInside = textWidth <= barWidth && textHeight <= barHeight, + fitsInsideIfRotated = textWidth <= barHeight && textHeight <= barWidth, + fitsInsideIfShrunk = orientation === "h" + ? barWidth >= textWidth * (barHeight / textHeight) + : barHeight >= textHeight * (barWidth / textWidth); + if ( + textHasSize && (fitsInside || fitsInsideIfRotated || fitsInsideIfShrunk) + ) { + textPosition = "inside"; + } else { + textPosition = "outside"; + textSelection.remove(); + textSelection = null; + } + } else { + textPosition = "inside"; } - else { - targetWidth = scale * textWidth; - targetHeight = scale * textHeight; + } + + if (!textSelection) { + textSelection = appendTextNode( + bar, + text, + textPosition === "outside" ? outsideTextFont : insideTextFont + ); + + textBB = Drawing.bBox( + textSelection.node() + ), textWidth = textBB.width, textHeight = textBB.height; + + if (textWidth <= 0 || textHeight <= 0) { + textSelection.remove(); + return; } + } + + // compute text transform + var transform; + if (textPosition === "outside") { + transform = getTransformToMoveOutsideBar( + x0, + x1, + y0, + y1, + textBB, + orientation + ); + } else { + transform = getTransformToMoveInsideBar( + x0, + x1, + y0, + y1, + textBB, + orientation + ); + } + + textSelection.attr("transform", transform); +} - if(orientation === 'h') { - if(x1 < x0) { - // bar end is on the left hand side - targetX = x1 + textpad + targetWidth / 2; - targetY = (y0 + y1) / 2; - } - else { - targetX = x1 - textpad - targetWidth / 2; - targetY = (y0 + y1) / 2; - } +function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { + // compute text and target positions + var textWidth = textBB.width, + textHeight = textBB.height, + textX = (textBB.left + textBB.right) / 2, + textY = (textBB.top + textBB.bottom) / 2, + barWidth = Math.abs(x1 - x0), + barHeight = Math.abs(y1 - y0), + targetWidth, + targetHeight, + targetX, + targetY; + + // apply text padding + var textpad; + if (barWidth > 2 * TEXTPAD && barHeight > 2 * TEXTPAD) { + textpad = TEXTPAD; + barWidth -= 2 * textpad; + barHeight -= 2 * textpad; + } else { + textpad = 0; + } + + // compute rotation and scale + var rotate, scale; + + if (textWidth <= barWidth && textHeight <= barHeight) { + // no scale or rotation is required + rotate = false; + scale = 1; + } else if (textWidth <= barHeight && textHeight <= barWidth) { + // only rotation is required + rotate = true; + scale = 1; + } else if (textWidth < textHeight === barWidth < barHeight) { + // only scale is required + rotate = false; + scale = Math.min(barWidth / textWidth, barHeight / textHeight); + } else { + // both scale and rotation are required + rotate = true; + scale = Math.min(barHeight / textWidth, barWidth / textHeight); + } + + if (rotate) rotate = 90; + + // rotate clockwise + // compute text and target positions + if (rotate) { + targetWidth = scale * textHeight; + targetHeight = scale * textWidth; + } else { + targetWidth = scale * textWidth; + targetHeight = scale * textHeight; + } + + if (orientation === "h") { + if (x1 < x0) { + // bar end is on the left hand side + targetX = x1 + textpad + targetWidth / 2; + targetY = (y0 + y1) / 2; + } else { + targetX = x1 - textpad - targetWidth / 2; + targetY = (y0 + y1) / 2; } - else { - if(y1 > y0) { - // bar end is on the bottom - targetX = (x0 + x1) / 2; - targetY = y1 - textpad - targetHeight / 2; - } - else { - targetX = (x0 + x1) / 2; - targetY = y1 + textpad + targetHeight / 2; - } + } else { + if (y1 > y0) { + // bar end is on the bottom + targetX = (x0 + x1) / 2; + targetY = y1 - textpad - targetHeight / 2; + } else { + targetX = (x0 + x1) / 2; + targetY = y1 + textpad + targetHeight / 2; } + } - return getTransform(textX, textY, targetX, targetY, scale, rotate); + return getTransform(textX, textY, targetX, targetY, scale, rotate); } function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { - var barWidth = (orientation === 'h') ? - Math.abs(y1 - y0) : - Math.abs(x1 - x0), - textpad; - - // apply text padding if possible - if(barWidth > 2 * TEXTPAD) { - textpad = TEXTPAD; - barWidth -= 2 * textpad; + var barWidth = orientation === "h" ? Math.abs(y1 - y0) : Math.abs(x1 - x0), + textpad; + + // apply text padding if possible + if (barWidth > 2 * TEXTPAD) { + textpad = TEXTPAD; + barWidth -= 2 * textpad; + } + + // compute rotation and scale + var rotate = false, + scale = orientation === "h" + ? Math.min(1, barWidth / textBB.height) + : Math.min(1, barWidth / textBB.width); + + // compute text and target positions + var textX = (textBB.left + textBB.right) / 2, + textY = (textBB.top + textBB.bottom) / 2, + targetWidth, + targetHeight, + targetX, + targetY; + if (rotate) { + targetWidth = scale * textBB.height; + targetHeight = scale * textBB.width; + } else { + targetWidth = scale * textBB.width; + targetHeight = scale * textBB.height; + } + + if (orientation === "h") { + if (x1 < x0) { + // bar end is on the left hand side + targetX = x1 - textpad - targetWidth / 2; + targetY = (y0 + y1) / 2; + } else { + targetX = x1 + textpad + targetWidth / 2; + targetY = (y0 + y1) / 2; } - - // compute rotation and scale - var rotate = false, - scale = (orientation === 'h') ? - Math.min(1, barWidth / textBB.height) : - Math.min(1, barWidth / textBB.width); - - // compute text and target positions - var textX = (textBB.left + textBB.right) / 2, - textY = (textBB.top + textBB.bottom) / 2, - targetWidth, - targetHeight, - targetX, - targetY; - if(rotate) { - targetWidth = scale * textBB.height; - targetHeight = scale * textBB.width; - } - else { - targetWidth = scale * textBB.width; - targetHeight = scale * textBB.height; - } - - if(orientation === 'h') { - if(x1 < x0) { - // bar end is on the left hand side - targetX = x1 - textpad - targetWidth / 2; - targetY = (y0 + y1) / 2; - } - else { - targetX = x1 + textpad + targetWidth / 2; - targetY = (y0 + y1) / 2; - } - } - else { - if(y1 > y0) { - // bar end is on the bottom - targetX = (x0 + x1) / 2; - targetY = y1 + textpad + targetHeight / 2; - } - else { - targetX = (x0 + x1) / 2; - targetY = y1 - textpad - targetHeight / 2; - } + } else { + if (y1 > y0) { + // bar end is on the bottom + targetX = (x0 + x1) / 2; + targetY = y1 + textpad + targetHeight / 2; + } else { + targetX = (x0 + x1) / 2; + targetY = y1 - textpad - targetHeight / 2; } + } - return getTransform(textX, textY, targetX, targetY, scale, rotate); + return getTransform(textX, textY, targetX, targetY, scale, rotate); } function getTransform(textX, textY, targetX, targetY, scale, rotate) { - var transformScale, - transformRotate, - transformTranslate; - - if(scale < 1) transformScale = 'scale(' + scale + ') '; - else { - scale = 1; - transformScale = ''; - } + var transformScale, transformRotate, transformTranslate; + + if (scale < 1) { + transformScale = "scale(" + scale + ") "; + } else { + scale = 1; + transformScale = ""; + } - transformRotate = (rotate) ? - 'rotate(' + rotate + ' ' + textX + ' ' + textY + ') ' : ''; + transformRotate = rotate + ? "rotate(" + rotate + " " + textX + " " + textY + ") " + : ""; - // Note that scaling also affects the center of the text box - var translateX = (targetX - scale * textX), - translateY = (targetY - scale * textY); - transformTranslate = 'translate(' + translateX + ' ' + translateY + ')'; + // Note that scaling also affects the center of the text box + var translateX = targetX - scale * textX, + translateY = targetY - scale * textY; + transformTranslate = "translate(" + translateX + " " + translateY + ")"; - return transformTranslate + transformScale + transformRotate; + return transformTranslate + transformScale + transformRotate; } function getText(trace, index) { - var value = getValue(trace.text, index); - return coerceString(attributeText, value); + var value = getValue(trace.text, index); + return coerceString(attributeText, value); } function getTextPosition(trace, index) { - var value = getValue(trace.textposition, index); - return coerceEnumerated(attributeTextPosition, value); + var value = getValue(trace.textposition, index); + return coerceEnumerated(attributeTextPosition, value); } function getTextFont(trace, index, defaultValue) { - return getFontValue( - attributeTextFont, trace.textfont, index, defaultValue); + return getFontValue(attributeTextFont, trace.textfont, index, defaultValue); } function getInsideTextFont(trace, index, defaultValue) { - return getFontValue( - attributeInsideTextFont, trace.insidetextfont, index, defaultValue); + return getFontValue( + attributeInsideTextFont, + trace.insidetextfont, + index, + defaultValue + ); } function getOutsideTextFont(trace, index, defaultValue) { - return getFontValue( - attributeOutsideTextFont, trace.outsidetextfont, index, defaultValue); + return getFontValue( + attributeOutsideTextFont, + trace.outsidetextfont, + index, + defaultValue + ); } -function getFontValue(attributeDefinition, attributeValue, index, defaultValue) { - attributeValue = attributeValue || {}; - - var familyValue = getValue(attributeValue.family, index), - sizeValue = getValue(attributeValue.size, index), - colorValue = getValue(attributeValue.color, index); - - return { - family: coerceString( - attributeDefinition.family, familyValue, defaultValue.family), - size: coerceNumber( - attributeDefinition.size, sizeValue, defaultValue.size), - color: coerceColor( - attributeDefinition.color, colorValue, defaultValue.color) - }; +function getFontValue( + attributeDefinition, + attributeValue, + index, + defaultValue +) { + attributeValue = attributeValue || {}; + + var familyValue = getValue(attributeValue.family, index), + sizeValue = getValue(attributeValue.size, index), + colorValue = getValue(attributeValue.color, index); + + return { + family: coerceString( + attributeDefinition.family, + familyValue, + defaultValue.family + ), + size: coerceNumber(attributeDefinition.size, sizeValue, defaultValue.size), + color: coerceColor( + attributeDefinition.color, + colorValue, + defaultValue.color + ) + }; } function getValue(arrayOrScalar, index) { - var value; - if(!Array.isArray(arrayOrScalar)) value = arrayOrScalar; - else if(index < arrayOrScalar.length) value = arrayOrScalar[index]; - return value; + var value; + if (!Array.isArray(arrayOrScalar)) value = arrayOrScalar; + else if (index < arrayOrScalar.length) value = arrayOrScalar[index]; + return value; } function coerceString(attributeDefinition, value, defaultValue) { - if(typeof value === 'string') { - if(value || !attributeDefinition.noBlank) return value; - } - else if(typeof value === 'number') { - if(!attributeDefinition.strict) return String(value); - } + if (typeof value === "string") { + if (value || !attributeDefinition.noBlank) return value; + } else if (typeof value === "number") { + if (!attributeDefinition.strict) return String(value); + } - return (defaultValue !== undefined) ? - defaultValue : - attributeDefinition.dflt; + return defaultValue !== undefined ? defaultValue : attributeDefinition.dflt; } function coerceEnumerated(attributeDefinition, value, defaultValue) { - if(attributeDefinition.coerceNumber) value = +value; + if (attributeDefinition.coerceNumber) value = +value; - if(attributeDefinition.values.indexOf(value) !== -1) return value; + if (attributeDefinition.values.indexOf(value) !== -1) return value; - return (defaultValue !== undefined) ? - defaultValue : - attributeDefinition.dflt; + return defaultValue !== undefined ? defaultValue : attributeDefinition.dflt; } function coerceNumber(attributeDefinition, value, defaultValue) { - if(isNumeric(value)) { - value = +value; + if (isNumeric(value)) { + value = +value; - var min = attributeDefinition.min, - max = attributeDefinition.max, - isOutOfBounds = (min !== undefined && value < min) || - (max !== undefined && value > max); + var min = attributeDefinition.min, + max = attributeDefinition.max, + isOutOfBounds = min !== undefined && value < min || + max !== undefined && value > max; - if(!isOutOfBounds) return value; - } + if (!isOutOfBounds) return value; + } - return (defaultValue !== undefined) ? - defaultValue : - attributeDefinition.dflt; + return defaultValue !== undefined ? defaultValue : attributeDefinition.dflt; } function coerceColor(attributeDefinition, value, defaultValue) { - if(tinycolor(value).isValid()) return value; + if (tinycolor(value).isValid()) return value; - return (defaultValue !== undefined) ? - defaultValue : - attributeDefinition.dflt; + return defaultValue !== undefined ? defaultValue : attributeDefinition.dflt; } diff --git a/src/traces/bar/set_positions.js b/src/traces/bar/set_positions.js index 79df94decc5..a1e3f8c65de 100644 --- a/src/traces/bar/set_positions.js +++ b/src/traces/bar/set_positions.js @@ -5,15 +5,12 @@ * 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 isNumeric = require("fast-isnumeric"); - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Registry = require('../../registry'); -var Axes = require('../../plots/cartesian/axes'); -var Sieve = require('./sieve.js'); +var Registry = require("../../registry"); +var Axes = require("../../plots/cartesian/axes"); +var Sieve = require("./sieve.js"); /* * Bar chart stacking/grouping positioning and autoscaling calculations @@ -21,588 +18,568 @@ var Sieve = require('./sieve.js'); * note that this handles histograms too * now doing this one subplot at a time */ - module.exports = function setPositions(gd, plotinfo) { - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis; - - var fullTraces = gd._fullData, - calcTraces = gd.calcdata, - calcTracesHorizontal = [], - calcTracesVertical = [], - i; - for(i = 0; i < fullTraces.length; i++) { - var fullTrace = fullTraces[i]; - if( - fullTrace.visible === true && - Registry.traceIs(fullTrace, 'bar') && - fullTrace.xaxis === xa._id && - fullTrace.yaxis === ya._id && - !calcTraces[i][0].placeholder - ) { - if(fullTrace.orientation === 'h') { - calcTracesHorizontal.push(calcTraces[i]); - } - else { - calcTracesVertical.push(calcTraces[i]); - } - } + var xa = plotinfo.xaxis, ya = plotinfo.yaxis; + + var fullTraces = gd._fullData, + calcTraces = gd.calcdata, + calcTracesHorizontal = [], + calcTracesVertical = [], + i; + for (i = 0; i < fullTraces.length; i++) { + var fullTrace = fullTraces[i]; + if ( + fullTrace.visible === true && + Registry.traceIs(fullTrace, "bar") && + fullTrace.xaxis === xa._id && + fullTrace.yaxis === ya._id && + !calcTraces[i][0].placeholder + ) { + if (fullTrace.orientation === "h") { + calcTracesHorizontal.push(calcTraces[i]); + } else { + calcTracesVertical.push(calcTraces[i]); + } } + } - setGroupPositions(gd, xa, ya, calcTracesVertical); - setGroupPositions(gd, ya, xa, calcTracesHorizontal); + setGroupPositions(gd, xa, ya, calcTracesVertical); + setGroupPositions(gd, ya, xa, calcTracesHorizontal); }; - function setGroupPositions(gd, pa, sa, calcTraces) { - if(!calcTraces.length) return; - - var barmode = gd._fullLayout.barmode, - overlay = (barmode === 'overlay'), - group = (barmode === 'group'), - excluded, - included, - i, calcTrace, fullTrace; - - if(overlay) { - setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces); + if (!calcTraces.length) return; + + var barmode = gd._fullLayout.barmode, + overlay = barmode === "overlay", + group = barmode === "group", + excluded, + included, + i, + calcTrace, + fullTrace; + + if (overlay) { + setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces); + } else if (group) { + // exclude from the group those traces for which the user set an offset + excluded = []; + included = []; + for (i = 0; i < calcTraces.length; i++) { + calcTrace = calcTraces[i]; + fullTrace = calcTrace[0].trace; + + if (fullTrace.offset === undefined) included.push(calcTrace); + else excluded.push(calcTrace); } - else if(group) { - // exclude from the group those traces for which the user set an offset - excluded = []; - included = []; - for(i = 0; i < calcTraces.length; i++) { - calcTrace = calcTraces[i]; - fullTrace = calcTrace[0].trace; - - if(fullTrace.offset === undefined) included.push(calcTrace); - else excluded.push(calcTrace); - } - if(included.length) { - setGroupPositionsInGroupMode(gd, pa, sa, included); - } - if(excluded.length) { - setGroupPositionsInOverlayMode(gd, pa, sa, excluded); - } + if (included.length) { + setGroupPositionsInGroupMode(gd, pa, sa, included); + } + if (excluded.length) { + setGroupPositionsInOverlayMode(gd, pa, sa, excluded); + } + } else { + // exclude from the stack those traces for which the user set a base + excluded = []; + included = []; + for (i = 0; i < calcTraces.length; i++) { + calcTrace = calcTraces[i]; + fullTrace = calcTrace[0].trace; + + if (fullTrace.base === undefined) included.push(calcTrace); + else excluded.push(calcTrace); } - else { - // exclude from the stack those traces for which the user set a base - excluded = []; - included = []; - for(i = 0; i < calcTraces.length; i++) { - calcTrace = calcTraces[i]; - fullTrace = calcTrace[0].trace; - - if(fullTrace.base === undefined) included.push(calcTrace); - else excluded.push(calcTrace); - } - if(included.length) { - setGroupPositionsInStackOrRelativeMode(gd, pa, sa, included); - } - if(excluded.length) { - setGroupPositionsInOverlayMode(gd, pa, sa, excluded); - } + if (included.length) { + setGroupPositionsInStackOrRelativeMode(gd, pa, sa, included); } + if (excluded.length) { + setGroupPositionsInOverlayMode(gd, pa, sa, excluded); + } + } } - function setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces) { - var barnorm = gd._fullLayout.barnorm, - separateNegativeValues = false, - dontMergeOverlappingData = !barnorm; - - // update position axis and set bar offsets and widths - for(var i = 0; i < calcTraces.length; i++) { - var calcTrace = calcTraces[i]; - - var sieve = new Sieve( - [calcTrace], separateNegativeValues, dontMergeOverlappingData - ); - - // set bar offsets and widths, and update position axis - setOffsetAndWidth(gd, pa, sieve); - - // set bar bases and sizes, and update size axis - // - // (note that `setGroupPositionsInOverlayMode` handles the case barnorm - // is defined, because this function is also invoked for traces that - // can't be grouped or stacked) - if(barnorm) { - sieveBars(gd, sa, sieve); - normalizeBars(gd, sa, sieve); - } - else { - setBaseAndTop(gd, sa, sieve); - } - } -} + var barnorm = gd._fullLayout.barnorm, + separateNegativeValues = false, + dontMergeOverlappingData = !barnorm; + // update position axis and set bar offsets and widths + for (var i = 0; i < calcTraces.length; i++) { + var calcTrace = calcTraces[i]; -function setGroupPositionsInGroupMode(gd, pa, sa, calcTraces) { - var fullLayout = gd._fullLayout, - barnorm = fullLayout.barnorm, - separateNegativeValues = false, - dontMergeOverlappingData = !barnorm, - sieve = new Sieve( - calcTraces, separateNegativeValues, dontMergeOverlappingData - ); + var sieve = new Sieve( + [calcTrace], + separateNegativeValues, + dontMergeOverlappingData + ); // set bar offsets and widths, and update position axis - setOffsetAndWidthInGroupMode(gd, pa, sieve); + setOffsetAndWidth(gd, pa, sieve); // set bar bases and sizes, and update size axis - if(barnorm) { - sieveBars(gd, sa, sieve); - normalizeBars(gd, sa, sieve); - } - else { - setBaseAndTop(gd, sa, sieve); + // + // (note that `setGroupPositionsInOverlayMode` handles the case barnorm + // is defined, because this function is also invoked for traces that + // can't be grouped or stacked) + if (barnorm) { + sieveBars(gd, sa, sieve); + normalizeBars(gd, sa, sieve); + } else { + setBaseAndTop(gd, sa, sieve); } + } } +function setGroupPositionsInGroupMode(gd, pa, sa, calcTraces) { + var fullLayout = gd._fullLayout, + barnorm = fullLayout.barnorm, + separateNegativeValues = false, + dontMergeOverlappingData = !barnorm, + sieve = new Sieve( + calcTraces, + separateNegativeValues, + dontMergeOverlappingData + ); + + // set bar offsets and widths, and update position axis + setOffsetAndWidthInGroupMode(gd, pa, sieve); + + // set bar bases and sizes, and update size axis + if (barnorm) { + sieveBars(gd, sa, sieve); + normalizeBars(gd, sa, sieve); + } else { + setBaseAndTop(gd, sa, sieve); + } +} function setGroupPositionsInStackOrRelativeMode(gd, pa, sa, calcTraces) { - var fullLayout = gd._fullLayout, - barmode = fullLayout.barmode, - stack = (barmode === 'stack'), - relative = (barmode === 'relative'), - barnorm = gd._fullLayout.barnorm, - separateNegativeValues = relative, - dontMergeOverlappingData = !(barnorm || stack || relative), - sieve = new Sieve( - calcTraces, separateNegativeValues, dontMergeOverlappingData - ); - - // set bar offsets and widths, and update position axis - setOffsetAndWidth(gd, pa, sieve); - - // set bar bases and sizes, and update size axis - stackBars(gd, sa, sieve); - - // flag the outmost bar (for text display purposes) - for(var i = 0; i < calcTraces.length; i++) { - var calcTrace = calcTraces[i]; - - for(var j = 0; j < calcTrace.length; j++) { - var bar = calcTrace[j]; - - if(!isNumeric(bar.s)) continue; - - var isOutmostBar = ((bar.b + bar.s) === sieve.get(bar.p, bar.s)); - if(isOutmostBar) bar._outmost = true; - } + var fullLayout = gd._fullLayout, + barmode = fullLayout.barmode, + stack = barmode === "stack", + relative = barmode === "relative", + barnorm = gd._fullLayout.barnorm, + separateNegativeValues = relative, + dontMergeOverlappingData = !(barnorm || stack || relative), + sieve = new Sieve( + calcTraces, + separateNegativeValues, + dontMergeOverlappingData + ); + + // set bar offsets and widths, and update position axis + setOffsetAndWidth(gd, pa, sieve); + + // set bar bases and sizes, and update size axis + stackBars(gd, sa, sieve); + + // flag the outmost bar (for text display purposes) + for (var i = 0; i < calcTraces.length; i++) { + var calcTrace = calcTraces[i]; + + for (var j = 0; j < calcTrace.length; j++) { + var bar = calcTrace[j]; + + if (!isNumeric(bar.s)) continue; + + var isOutmostBar = bar.b + bar.s === sieve.get(bar.p, bar.s); + if (isOutmostBar) bar._outmost = true; } + } - // Note that marking the outmost bars has to be done - // before `normalizeBars` changes `bar.b` and `bar.s`. - if(barnorm) normalizeBars(gd, sa, sieve); + // Note that marking the outmost bars has to be done + // before `normalizeBars` changes `bar.b` and `bar.s`. + if (barnorm) normalizeBars(gd, sa, sieve); } - function setOffsetAndWidth(gd, pa, sieve) { - var fullLayout = gd._fullLayout, - bargap = fullLayout.bargap, - bargroupgap = fullLayout.bargroupgap, - minDiff = sieve.minDiff, - calcTraces = sieve.traces, - i, calcTrace, calcTrace0, - t; - - // set bar offsets and widths - var barGroupWidth = minDiff * (1 - bargap), - barWidthPlusGap = barGroupWidth, - barWidth = barWidthPlusGap * (1 - bargroupgap); + var fullLayout = gd._fullLayout, + bargap = fullLayout.bargap, + bargroupgap = fullLayout.bargroupgap, + minDiff = sieve.minDiff, + calcTraces = sieve.traces, + i, + calcTrace, + calcTrace0, + t; + + // set bar offsets and widths + var barGroupWidth = minDiff * (1 - bargap), + barWidthPlusGap = barGroupWidth, + barWidth = barWidthPlusGap * (1 - bargroupgap); + + // computer bar group center and bar offset + var offsetFromCenter = (-barWidth) / 2; + + for (i = 0; i < calcTraces.length; i++) { + calcTrace = calcTraces[i]; + calcTrace0 = calcTrace[0]; + + // store bar width and offset for this trace + t = calcTrace0.t; + t.barwidth = barWidth; + t.poffset = offsetFromCenter; + t.bargroupwidth = barGroupWidth; + } + + // stack bars that only differ by rounding + sieve.binWidth = calcTraces[0][0].t.barwidth / 100; + + // if defined, apply trace offset and width + applyAttributes(sieve); + + // store the bar center in each calcdata item + setBarCenter(gd, pa, sieve); + + // update position axes + updatePositionAxis(gd, pa, sieve); +} - // computer bar group center and bar offset - var offsetFromCenter = -barWidth / 2; +function setOffsetAndWidthInGroupMode(gd, pa, sieve) { + var fullLayout = gd._fullLayout, + bargap = fullLayout.bargap, + bargroupgap = fullLayout.bargroupgap, + positions = sieve.positions, + distinctPositions = sieve.distinctPositions, + minDiff = sieve.minDiff, + calcTraces = sieve.traces, + i, + calcTrace, + calcTrace0, + t; + + // if there aren't any overlapping positions, + // let them have full width even if mode is group + var overlap = positions.length !== distinctPositions.length; + + var nTraces = calcTraces.length, + barGroupWidth = minDiff * (1 - bargap), + barWidthPlusGap = overlap ? barGroupWidth / nTraces : barGroupWidth, + barWidth = barWidthPlusGap * (1 - bargroupgap); + + for (i = 0; i < nTraces; i++) { + calcTrace = calcTraces[i]; + calcTrace0 = calcTrace[0]; - for(i = 0; i < calcTraces.length; i++) { - calcTrace = calcTraces[i]; - calcTrace0 = calcTrace[0]; + // computer bar group center and bar offset + var offsetFromCenter = overlap + ? ((2 * i + 1 - nTraces) * barWidthPlusGap - barWidth) / 2 + : (-barWidth) / 2; - // store bar width and offset for this trace - t = calcTrace0.t; - t.barwidth = barWidth; - t.poffset = offsetFromCenter; - t.bargroupwidth = barGroupWidth; - } + // store bar width and offset for this trace + t = calcTrace0.t; + t.barwidth = barWidth; + t.poffset = offsetFromCenter; + t.bargroupwidth = barGroupWidth; + } - // stack bars that only differ by rounding - sieve.binWidth = calcTraces[0][0].t.barwidth / 100; + // stack bars that only differ by rounding + sieve.binWidth = calcTraces[0][0].t.barwidth / 100; - // if defined, apply trace offset and width - applyAttributes(sieve); + // if defined, apply trace width + applyAttributes(sieve); - // store the bar center in each calcdata item - setBarCenter(gd, pa, sieve); + // store the bar center in each calcdata item + setBarCenter(gd, pa, sieve); - // update position axes - updatePositionAxis(gd, pa, sieve); + // update position axes + updatePositionAxis(gd, pa, sieve, overlap); } +function applyAttributes(sieve) { + var calcTraces = sieve.traces, i, calcTrace, calcTrace0, fullTrace, j, t; -function setOffsetAndWidthInGroupMode(gd, pa, sieve) { - var fullLayout = gd._fullLayout, - bargap = fullLayout.bargap, - bargroupgap = fullLayout.bargroupgap, - positions = sieve.positions, - distinctPositions = sieve.distinctPositions, - minDiff = sieve.minDiff, - calcTraces = sieve.traces, - i, calcTrace, calcTrace0, - t; - - // if there aren't any overlapping positions, - // let them have full width even if mode is group - var overlap = (positions.length !== distinctPositions.length); - - var nTraces = calcTraces.length, - barGroupWidth = minDiff * (1 - bargap), - barWidthPlusGap = (overlap) ? barGroupWidth / nTraces : barGroupWidth, - barWidth = barWidthPlusGap * (1 - bargroupgap); - - for(i = 0; i < nTraces; i++) { - calcTrace = calcTraces[i]; - calcTrace0 = calcTrace[0]; - - // computer bar group center and bar offset - var offsetFromCenter = (overlap) ? - ((2 * i + 1 - nTraces) * barWidthPlusGap - barWidth) / 2 : - -barWidth / 2; - - // store bar width and offset for this trace - t = calcTrace0.t; - t.barwidth = barWidth; - t.poffset = offsetFromCenter; - t.bargroupwidth = barGroupWidth; - } - - // stack bars that only differ by rounding - sieve.binWidth = calcTraces[0][0].t.barwidth / 100; + for (i = 0; i < calcTraces.length; i++) { + calcTrace = calcTraces[i]; + calcTrace0 = calcTrace[0]; + fullTrace = calcTrace0.trace; + t = calcTrace0.t; - // if defined, apply trace width - applyAttributes(sieve); + var offset = fullTrace.offset, initialPoffset = t.poffset, newPoffset; - // store the bar center in each calcdata item - setBarCenter(gd, pa, sieve); + if (Array.isArray(offset)) { + // if offset is an array, then clone it into t.poffset. + newPoffset = offset.slice(0, calcTrace.length); - // update position axes - updatePositionAxis(gd, pa, sieve, overlap); -} + // guard against non-numeric items + for (j = 0; j < newPoffset.length; j++) { + if (!isNumeric(newPoffset[j])) { + newPoffset[j] = initialPoffset; + } + } + // if the length of the array is too short, + // then extend it with the initial value of t.poffset + for (j = newPoffset.length; j < calcTrace.length; j++) { + newPoffset.push(initialPoffset); + } -function applyAttributes(sieve) { - var calcTraces = sieve.traces, - i, calcTrace, calcTrace0, fullTrace, - j, - t; - - for(i = 0; i < calcTraces.length; i++) { - calcTrace = calcTraces[i]; - calcTrace0 = calcTrace[0]; - fullTrace = calcTrace0.trace; - t = calcTrace0.t; - - var offset = fullTrace.offset, - initialPoffset = t.poffset, - newPoffset; - - if(Array.isArray(offset)) { - // if offset is an array, then clone it into t.poffset. - newPoffset = offset.slice(0, calcTrace.length); - - // guard against non-numeric items - for(j = 0; j < newPoffset.length; j++) { - if(!isNumeric(newPoffset[j])) { - newPoffset[j] = initialPoffset; - } - } - - // if the length of the array is too short, - // then extend it with the initial value of t.poffset - for(j = newPoffset.length; j < calcTrace.length; j++) { - newPoffset.push(initialPoffset); - } - - t.poffset = newPoffset; - } - else if(offset !== undefined) { - t.poffset = offset; - } + t.poffset = newPoffset; + } else if (offset !== undefined) { + t.poffset = offset; + } - var width = fullTrace.width, - initialBarwidth = t.barwidth; - - if(Array.isArray(width)) { - // if width is an array, then clone it into t.barwidth. - var newBarwidth = width.slice(0, calcTrace.length); - - // guard against non-numeric items - for(j = 0; j < newBarwidth.length; j++) { - if(!isNumeric(newBarwidth[j])) newBarwidth[j] = initialBarwidth; - } - - // if the length of the array is too short, - // then extend it with the initial value of t.barwidth - for(j = newBarwidth.length; j < calcTrace.length; j++) { - newBarwidth.push(initialBarwidth); - } - - t.barwidth = newBarwidth; - - // if user didn't set offset, - // then correct t.poffset to ensure bars remain centered - if(offset === undefined) { - newPoffset = []; - for(j = 0; j < calcTrace.length; j++) { - newPoffset.push( - initialPoffset + (initialBarwidth - newBarwidth[j]) / 2 - ); - } - t.poffset = newPoffset; - } - } - else if(width !== undefined) { - t.barwidth = width; - - // if user didn't set offset, - // then correct t.poffset to ensure bars remain centered - if(offset === undefined) { - t.poffset = initialPoffset + (initialBarwidth - width) / 2; - } + var width = fullTrace.width, initialBarwidth = t.barwidth; + + if (Array.isArray(width)) { + // if width is an array, then clone it into t.barwidth. + var newBarwidth = width.slice(0, calcTrace.length); + + // guard against non-numeric items + for (j = 0; j < newBarwidth.length; j++) { + if (!isNumeric(newBarwidth[j])) newBarwidth[j] = initialBarwidth; + } + + // if the length of the array is too short, + // then extend it with the initial value of t.barwidth + for (j = newBarwidth.length; j < calcTrace.length; j++) { + newBarwidth.push(initialBarwidth); + } + + t.barwidth = newBarwidth; + + // if user didn't set offset, + // then correct t.poffset to ensure bars remain centered + if (offset === undefined) { + newPoffset = []; + for (j = 0; j < calcTrace.length; j++) { + newPoffset.push( + initialPoffset + (initialBarwidth - newBarwidth[j]) / 2 + ); } + t.poffset = newPoffset; + } + } else if (width !== undefined) { + t.barwidth = width; + + // if user didn't set offset, + // then correct t.poffset to ensure bars remain centered + if (offset === undefined) { + t.poffset = initialPoffset + (initialBarwidth - width) / 2; + } } + } } - function setBarCenter(gd, pa, sieve) { - var calcTraces = sieve.traces, - pLetter = getAxisLetter(pa); - - for(var i = 0; i < calcTraces.length; i++) { - var calcTrace = calcTraces[i], - t = calcTrace[0].t, - poffset = t.poffset, - poffsetIsArray = Array.isArray(poffset), - barwidth = t.barwidth, - barwidthIsArray = Array.isArray(barwidth); - - for(var j = 0; j < calcTrace.length; j++) { - var calcBar = calcTrace[j]; - - calcBar[pLetter] = calcBar.p + - ((poffsetIsArray) ? poffset[j] : poffset) + - ((barwidthIsArray) ? barwidth[j] : barwidth) / 2; - } + var calcTraces = sieve.traces, pLetter = getAxisLetter(pa); + + for (var i = 0; i < calcTraces.length; i++) { + var calcTrace = calcTraces[i], + t = calcTrace[0].t, + poffset = t.poffset, + poffsetIsArray = Array.isArray(poffset), + barwidth = t.barwidth, + barwidthIsArray = Array.isArray(barwidth); + + for (var j = 0; j < calcTrace.length; j++) { + var calcBar = calcTrace[j]; + + calcBar[pLetter] = calcBar.p + + (poffsetIsArray ? poffset[j] : poffset) + + (barwidthIsArray ? barwidth[j] : barwidth) / 2; } + } } - function updatePositionAxis(gd, pa, sieve, allowMinDtick) { - var calcTraces = sieve.traces, - distinctPositions = sieve.distinctPositions, - distinctPositions0 = distinctPositions[0], - minDiff = sieve.minDiff, - vpad = minDiff / 2; - - Axes.minDtick(pa, minDiff, distinctPositions0, allowMinDtick); - - // If the user set the bar width or the offset, - // then bars can be shifted away from their positions - // and widths can be larger than minDiff. - // - // Here, we compute pMin and pMax to expand the position axis, - // so that all bars are fully within the axis range. - var pMin = Math.min.apply(Math, distinctPositions) - vpad, - pMax = Math.max.apply(Math, distinctPositions) + vpad; - - for(var i = 0; i < calcTraces.length; i++) { - var calcTrace = calcTraces[i], - calcTrace0 = calcTrace[0], - fullTrace = calcTrace0.trace; - - if(fullTrace.width === undefined && fullTrace.offset === undefined) { - continue; - } + var calcTraces = sieve.traces, + distinctPositions = sieve.distinctPositions, + distinctPositions0 = distinctPositions[0], + minDiff = sieve.minDiff, + vpad = minDiff / 2; + + Axes.minDtick(pa, minDiff, distinctPositions0, allowMinDtick); + + // If the user set the bar width or the offset, + // then bars can be shifted away from their positions + // and widths can be larger than minDiff. + // + // Here, we compute pMin and pMax to expand the position axis, + // so that all bars are fully within the axis range. + var pMin = Math.min.apply(Math, distinctPositions) - vpad, + pMax = Math.max.apply(Math, distinctPositions) + vpad; + + for (var i = 0; i < calcTraces.length; i++) { + var calcTrace = calcTraces[i], + calcTrace0 = calcTrace[0], + fullTrace = calcTrace0.trace; + + if (fullTrace.width === undefined && fullTrace.offset === undefined) { + continue; + } - var t = calcTrace0.t, - poffset = t.poffset, - barwidth = t.barwidth, - poffsetIsArray = Array.isArray(poffset), - barwidthIsArray = Array.isArray(barwidth); - - for(var j = 0; j < calcTrace.length; j++) { - var calcBar = calcTrace[j], - calcBarOffset = (poffsetIsArray) ? poffset[j] : poffset, - calcBarWidth = (barwidthIsArray) ? barwidth[j] : barwidth, - p = calcBar.p, - l = p + calcBarOffset, - r = l + calcBarWidth; - - pMin = Math.min(pMin, l); - pMax = Math.max(pMax, r); - } + var t = calcTrace0.t, + poffset = t.poffset, + barwidth = t.barwidth, + poffsetIsArray = Array.isArray(poffset), + barwidthIsArray = Array.isArray(barwidth); + + for (var j = 0; j < calcTrace.length; j++) { + var calcBar = calcTrace[j], + calcBarOffset = poffsetIsArray ? poffset[j] : poffset, + calcBarWidth = barwidthIsArray ? barwidth[j] : barwidth, + p = calcBar.p, + l = p + calcBarOffset, + r = l + calcBarWidth; + + pMin = Math.min(pMin, l); + pMax = Math.max(pMax, r); } + } - Axes.expand(pa, [pMin, pMax], {padded: false}); + Axes.expand(pa, [pMin, pMax], { padded: false }); } - function setBaseAndTop(gd, sa, sieve) { - // store these bar bases and tops in calcdata - // and make sure the size axis includes zero, - // along with the bases and tops of each bar. - var traces = sieve.traces, - sLetter = getAxisLetter(sa), - sMax = sa.l2c(sa.c2l(0)), - sMin = sMax; - - for(var i = 0; i < traces.length; i++) { - var trace = traces[i]; - - for(var j = 0; j < trace.length; j++) { - var bar = trace[j], - barBase = bar.b, - barTop = barBase + bar.s; - - bar[sLetter] = barTop; - - if(isNumeric(sa.c2l(barTop))) { - sMax = Math.max(sMax, barTop); - sMin = Math.min(sMin, barTop); - } - if(isNumeric(sa.c2l(barBase))) { - sMax = Math.max(sMax, barBase); - sMin = Math.min(sMin, barBase); - } - } + // store these bar bases and tops in calcdata + // and make sure the size axis includes zero, + // along with the bases and tops of each bar. + var traces = sieve.traces, + sLetter = getAxisLetter(sa), + sMax = sa.l2c(sa.c2l(0)), + sMin = sMax; + + for (var i = 0; i < traces.length; i++) { + var trace = traces[i]; + + for (var j = 0; j < trace.length; j++) { + var bar = trace[j], barBase = bar.b, barTop = barBase + bar.s; + + bar[sLetter] = barTop; + + if (isNumeric(sa.c2l(barTop))) { + sMax = Math.max(sMax, barTop); + sMin = Math.min(sMin, barTop); + } + if (isNumeric(sa.c2l(barBase))) { + sMax = Math.max(sMax, barBase); + sMin = Math.min(sMin, barBase); + } } + } - Axes.expand(sa, [sMin, sMax], {tozero: true, padded: true}); + Axes.expand(sa, [sMin, sMax], { tozero: true, padded: true }); } - function stackBars(gd, sa, sieve) { - var fullLayout = gd._fullLayout, - barnorm = fullLayout.barnorm, - sLetter = getAxisLetter(sa), - traces = sieve.traces, - i, trace, - j, bar; - - var sMax = sa.l2c(sa.c2l(0)), - sMin = sMax; - - for(i = 0; i < traces.length; i++) { - trace = traces[i]; - - for(j = 0; j < trace.length; j++) { - bar = trace[j]; - - if(!isNumeric(bar.s)) continue; - - // stack current bar and get previous sum - var barBase = sieve.put(bar.p, bar.b + bar.s), - barTop = barBase + bar.b + bar.s; - - // store the bar base and top in each calcdata item - bar.b = barBase; - bar[sLetter] = barTop; - - if(!barnorm) { - if(isNumeric(sa.c2l(barTop))) { - sMax = Math.max(sMax, barTop); - sMin = Math.min(sMin, barTop); - } - if(isNumeric(sa.c2l(barBase))) { - sMax = Math.max(sMax, barBase); - sMin = Math.min(sMin, barBase); - } - } + var fullLayout = gd._fullLayout, + barnorm = fullLayout.barnorm, + sLetter = getAxisLetter(sa), + traces = sieve.traces, + i, + trace, + j, + bar; + + var sMax = sa.l2c(sa.c2l(0)), sMin = sMax; + + for (i = 0; i < traces.length; i++) { + trace = traces[i]; + + for (j = 0; j < trace.length; j++) { + bar = trace[j]; + + if (!isNumeric(bar.s)) continue; + + // stack current bar and get previous sum + var barBase = sieve.put(bar.p, bar.b + bar.s), + barTop = barBase + bar.b + bar.s; + + // store the bar base and top in each calcdata item + bar.b = barBase; + bar[sLetter] = barTop; + + if (!barnorm) { + if (isNumeric(sa.c2l(barTop))) { + sMax = Math.max(sMax, barTop); + sMin = Math.min(sMin, barTop); } + if (isNumeric(sa.c2l(barBase))) { + sMax = Math.max(sMax, barBase); + sMin = Math.min(sMin, barBase); + } + } } + } - // if barnorm is set, let normalizeBars update the axis range - if(!barnorm) Axes.expand(sa, [sMin, sMax], {tozero: true, padded: true}); + // if barnorm is set, let normalizeBars update the axis range + if (!barnorm) Axes.expand(sa, [sMin, sMax], { tozero: true, padded: true }); } - function sieveBars(gd, sa, sieve) { - var traces = sieve.traces; + var traces = sieve.traces; - for(var i = 0; i < traces.length; i++) { - var trace = traces[i]; + for (var i = 0; i < traces.length; i++) { + var trace = traces[i]; - for(var j = 0; j < trace.length; j++) { - var bar = trace[j]; + for (var j = 0; j < trace.length; j++) { + var bar = trace[j]; - if(isNumeric(bar.s)) sieve.put(bar.p, bar.b + bar.s); - } + if (isNumeric(bar.s)) sieve.put(bar.p, bar.b + bar.s); } + } } - function normalizeBars(gd, sa, sieve) { - // Note: - // - // normalizeBars requires that either sieveBars or stackBars has been - // previously invoked. - - var traces = sieve.traces, - sLetter = getAxisLetter(sa), - sTop = (gd._fullLayout.barnorm === 'fraction') ? 1 : 100, - sTiny = sTop / 1e9, // in case of rounding error in sum - sMin = 0, - sMax = (gd._fullLayout.barmode === 'stack') ? sTop : 0, - padded = false; - - for(var i = 0; i < traces.length; i++) { - var trace = traces[i]; - - for(var j = 0; j < trace.length; j++) { - var bar = trace[j]; - - if(!isNumeric(bar.s)) continue; - - var scale = Math.abs(sTop / sieve.get(bar.p, bar.s)); - bar.b *= scale; - bar.s *= scale; - - var barBase = bar.b, - barTop = barBase + bar.s; - bar[sLetter] = barTop; - - if(isNumeric(sa.c2l(barTop))) { - if(barTop < sMin - sTiny) { - padded = true; - sMin = barTop; - } - if(barTop > sMax + sTiny) { - padded = true; - sMax = barTop; - } - } - - if(isNumeric(sa.c2l(barBase))) { - if(barBase < sMin - sTiny) { - padded = true; - sMin = barBase; - } - if(barBase > sMax + sTiny) { - padded = true; - sMax = barBase; - } - } + // Note: + // + // normalizeBars requires that either sieveBars or stackBars has been + // previously invoked. + var traces = sieve.traces, + sLetter = getAxisLetter(sa), + sTop = gd._fullLayout.barnorm === "fraction" ? 1 : 100, + sTiny = sTop / 1e9, + // in case of rounding error in sum + sMin = 0, + sMax = gd._fullLayout.barmode === "stack" ? sTop : 0, + padded = false; + + for (var i = 0; i < traces.length; i++) { + var trace = traces[i]; + + for (var j = 0; j < trace.length; j++) { + var bar = trace[j]; + + if (!isNumeric(bar.s)) continue; + + var scale = Math.abs(sTop / sieve.get(bar.p, bar.s)); + bar.b *= scale; + bar.s *= scale; + + var barBase = bar.b, barTop = barBase + bar.s; + bar[sLetter] = barTop; + + if (isNumeric(sa.c2l(barTop))) { + if (barTop < sMin - sTiny) { + padded = true; + sMin = barTop; } + if (barTop > sMax + sTiny) { + padded = true; + sMax = barTop; + } + } + + if (isNumeric(sa.c2l(barBase))) { + if (barBase < sMin - sTiny) { + padded = true; + sMin = barBase; + } + if (barBase > sMax + sTiny) { + padded = true; + sMax = barBase; + } + } } + } - // update range of size axis - Axes.expand(sa, [sMin, sMax], {tozero: true, padded: padded}); + // update range of size axis + Axes.expand(sa, [sMin, sMax], { tozero: true, padded: padded }); } - function getAxisLetter(ax) { - return ax._id.charAt(0); + return ax._id.charAt(0); } diff --git a/src/traces/bar/sieve.js b/src/traces/bar/sieve.js index 55655c743c9..413453e2930 100644 --- a/src/traces/bar/sieve.js +++ b/src/traces/bar/sieve.js @@ -5,12 +5,10 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = Sieve; -var Lib = require('../../lib'); +var Lib = require("../../lib"); /** * Helper class to sieve data from traces into bins @@ -25,27 +23,27 @@ var Lib = require('../../lib'); * If true, then don't merge overlapping bars into a single bar */ function Sieve(traces, separateNegativeValues, dontMergeOverlappingData) { - this.traces = traces; - this.separateNegativeValues = separateNegativeValues; - this.dontMergeOverlappingData = dontMergeOverlappingData; + this.traces = traces; + this.separateNegativeValues = separateNegativeValues; + this.dontMergeOverlappingData = dontMergeOverlappingData; - var positions = []; - for(var i = 0; i < traces.length; i++) { - var trace = traces[i]; - for(var j = 0; j < trace.length; j++) { - var bar = trace[j]; - positions.push(bar.p); - } + var positions = []; + for (var i = 0; i < traces.length; i++) { + var trace = traces[i]; + for (var j = 0; j < trace.length; j++) { + var bar = trace[j]; + positions.push(bar.p); } - this.positions = positions; + } + this.positions = positions; - var dv = Lib.distinctVals(this.positions); - this.distinctPositions = dv.vals; - this.minDiff = dv.minDiff; + var dv = Lib.distinctVals(this.positions); + this.distinctPositions = dv.vals; + this.minDiff = dv.minDiff; - this.binWidth = this.minDiff; + this.binWidth = this.minDiff; - this.bins = {}; + this.bins = {}; } /** @@ -57,12 +55,11 @@ function Sieve(traces, separateNegativeValues, dontMergeOverlappingData) { * @returns {number} Previous bin value */ Sieve.prototype.put = function put(position, value) { - var label = this.getLabel(position, value), - oldValue = this.bins[label] || 0; + var label = this.getLabel(position, value), oldValue = this.bins[label] || 0; - this.bins[label] = oldValue + value; + this.bins[label] = oldValue + value; - return oldValue; + return oldValue; }; /** @@ -75,8 +72,8 @@ Sieve.prototype.put = function put(position, value) { * @returns {number} Current bin value */ Sieve.prototype.get = function put(position, value) { - var label = this.getLabel(position, value); - return this.bins[label] || 0; + var label = this.getLabel(position, value); + return this.bins[label] || 0; }; /** @@ -91,9 +88,9 @@ Sieve.prototype.get = function put(position, value) { * true; otherwise prefixed with '^') */ Sieve.prototype.getLabel = function getLabel(position, value) { - var prefix = (value < 0 && this.separateNegativeValues) ? 'v' : '^', - label = (this.dontMergeOverlappingData) ? - position : - Math.round(position / this.binWidth); - return prefix + label; + var prefix = value < 0 && this.separateNegativeValues ? "v" : "^", + label = this.dontMergeOverlappingData + ? position + : Math.round(position / this.binWidth); + return prefix + label; }; diff --git a/src/traces/bar/style.js b/src/traces/bar/style.js index d0fc54e3429..83a54d68aef 100644 --- a/src/traces/bar/style.js +++ b/src/traces/bar/style.js @@ -5,72 +5,72 @@ * 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 d3 = require("d3"); - -'use strict'; - -var d3 = require('d3'); - -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); -var ErrorBars = require('../../components/errorbars'); - +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); +var ErrorBars = require("../../components/errorbars"); module.exports = function style(gd) { - var s = d3.select(gd).selectAll('g.trace.bars'), - barcount = s.size(), - fullLayout = gd._fullLayout; + var s = d3.select(gd).selectAll("g.trace.bars"), + barcount = s.size(), + fullLayout = gd._fullLayout; - // trace styling - s.style('opacity', function(d) { return d[0].trace.opacity; }) - - // for gapless (either stacked or neighboring grouped) bars use - // crispEdges to turn off antialiasing so an artificial gap - // isn't introduced. + // trace styling + s + .style("opacity", function(d) { + return d[0].trace.opacity; + }) .each(function(d) { - if((fullLayout.barmode === 'stack' && barcount > 1) || - (fullLayout.bargap === 0 && - fullLayout.bargroupgap === 0 && - !d[0].trace.marker.line.width)) { - d3.select(this).attr('shape-rendering', 'crispEdges'); - } + if ( + fullLayout.barmode === "stack" && barcount > 1 || + fullLayout.bargap === 0 && + fullLayout.bargroupgap === 0 && + !d[0].trace.marker.line.width + ) { + d3.select(this).attr("shape-rendering", "crispEdges"); + } }); - // then style the individual bars - s.selectAll('g.points').each(function(d) { - var trace = d[0].trace, - marker = trace.marker, - markerLine = marker.line, - markerScale = Drawing.tryColorscale(marker, ''), - lineScale = Drawing.tryColorscale(marker, 'line'); + // then style the individual bars + s.selectAll("g.points").each(function(d) { + var trace = d[0].trace, + marker = trace.marker, + markerLine = marker.line, + markerScale = Drawing.tryColorscale(marker, ""), + lineScale = Drawing.tryColorscale(marker, "line"); - d3.select(this).selectAll('path').each(function(d) { - // allow all marker and marker line colors to be scaled - // by given max and min to colorscales - var fillColor, - lineColor, - lineWidth = (d.mlw + 1 || markerLine.width + 1) - 1, - p = d3.select(this); + d3.select(this).selectAll("path").each(function(d) { + // allow all marker and marker line colors to be scaled + // by given max and min to colorscales + var fillColor, + lineColor, + lineWidth = (d.mlw + 1 || markerLine.width + 1) - 1, + p = d3.select(this); - if('mc' in d) fillColor = d.mcc = markerScale(d.mc); - else if(Array.isArray(marker.color)) fillColor = Color.defaultLine; - else fillColor = marker.color; + if ("mc" in d) fillColor = d.mcc = markerScale(d.mc); + else if (Array.isArray(marker.color)) fillColor = Color.defaultLine; + else fillColor = marker.color; - p.style('stroke-width', lineWidth + 'px') - .call(Color.fill, fillColor); - if(lineWidth) { - if('mlc' in d) lineColor = d.mlcc = lineScale(d.mlc); - // weird case: array wasn't long enough to apply to every point - else if(Array.isArray(markerLine.color)) lineColor = Color.defaultLine; - else lineColor = markerLine.color; + p.style("stroke-width", lineWidth + "px").call(Color.fill, fillColor); + if (lineWidth) { + if ("mlc" in d) { + lineColor = d.mlcc = lineScale(d.mlc); + } else if (Array.isArray(markerLine.color)) { + // weird case: array wasn't long enough to apply to every point + lineColor = Color.defaultLine; + } else { + lineColor = markerLine.color; + } - p.call(Color.stroke, lineColor); - } - }); - // TODO: text markers on bars, either extra text or just bar values - // d3.select(this).selectAll('text') - // .call(Drawing.textPointStyle,d.t||d[0].t); + p.call(Color.stroke, lineColor); + } }); + // TODO: text markers on bars, either extra text or just bar values + // d3.select(this).selectAll('text') + // .call(Drawing.textPointStyle,d.t||d[0].t); + }); - s.call(ErrorBars.style); + s.call(ErrorBars.style); }; diff --git a/src/traces/bar/style_defaults.js b/src/traces/bar/style_defaults.js index 3ccd7494554..4c5cb41ba0e 100644 --- a/src/traces/bar/style_defaults.js +++ b/src/traces/bar/style_defaults.js @@ -5,31 +5,35 @@ * 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 Color = require('../../components/color'); -var hasColorscale = require('../../components/colorscale/has_colorscale'); -var colorscaleDefaults = require('../../components/colorscale/defaults'); - - -module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout) { - coerce('marker.color', defaultColor); - - if(hasColorscale(traceIn, 'marker')) { - colorscaleDefaults( - traceIn, traceOut, layout, coerce, {prefix: 'marker.', cLetter: 'c'} - ); - } - - coerce('marker.line.color', Color.defaultLine); - - if(hasColorscale(traceIn, 'marker.line')) { - colorscaleDefaults( - traceIn, traceOut, layout, coerce, {prefix: 'marker.line.', cLetter: 'c'} - ); - } - - coerce('marker.line.width'); +"use strict"; +var Color = require("../../components/color"); +var hasColorscale = require("../../components/colorscale/has_colorscale"); +var colorscaleDefaults = require("../../components/colorscale/defaults"); + +module.exports = function handleStyleDefaults( + traceIn, + traceOut, + coerce, + defaultColor, + layout +) { + coerce("marker.color", defaultColor); + + if (hasColorscale(traceIn, "marker")) { + colorscaleDefaults(traceIn, traceOut, layout, coerce, { + prefix: "marker.", + cLetter: "c" + }); + } + + coerce("marker.line.color", Color.defaultLine); + + if (hasColorscale(traceIn, "marker.line")) { + colorscaleDefaults(traceIn, traceOut, layout, coerce, { + prefix: "marker.line.", + cLetter: "c" + }); + } + + coerce("marker.line.width"); }; diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index f1308538480..f03330b20bb 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -5,174 +5,174 @@ * 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 scatterAttrs = require('../scatter/attributes'); -var colorAttrs = require('../../components/color/attributes'); -var extendFlat = require('../../lib/extend').extendFlat; +"use strict"; +var scatterAttrs = require("../scatter/attributes"); +var colorAttrs = require("../../components/color/attributes"); +var extendFlat = require("../../lib/extend").extendFlat; var scatterMarkerAttrs = scatterAttrs.marker, - scatterMarkerLineAttrs = scatterMarkerAttrs.line; - + scatterMarkerLineAttrs = scatterMarkerAttrs.line; module.exports = { - y: { - valType: 'data_array', - description: [ - 'Sets the y sample data or coordinates.', - 'See overview for more info.' - ].join(' ') - }, - x: { - valType: 'data_array', - description: [ - 'Sets the x sample data or coordinates.', - 'See overview for more info.' - ].join(' ') - }, - x0: { - valType: 'any', - role: 'info', - description: [ - 'Sets the x coordinate of the box.', - 'See overview for more info.' - ].join(' ') - }, - y0: { - valType: 'any', - role: 'info', - description: [ - 'Sets the y coordinate of the box.', - 'See overview for more info.' - ].join(' ') - }, - xcalendar: scatterAttrs.xcalendar, - ycalendar: scatterAttrs.ycalendar, - whiskerwidth: { - valType: 'number', - min: 0, - max: 1, - dflt: 0.5, - role: 'style', - description: [ - 'Sets the width of the whiskers relative to', - 'the box\' width.', - 'For example, with 1, the whiskers are as wide as the box(es).' - ].join(' ') - }, - boxpoints: { - valType: 'enumerated', - values: ['all', 'outliers', 'suspectedoutliers', false], - dflt: 'outliers', - role: 'style', - description: [ - 'If *outliers*, only the sample points lying outside the whiskers', - 'are shown', - 'If *suspectedoutliers*, the outlier points are shown and', - 'points either less than 4*Q1-3*Q3 or greater than 4*Q3-3*Q1', - 'are highlighted (see `outliercolor`)', - 'If *all*, all sample points are shown', - 'If *false*, only the box(es) are shown with no sample points' - ].join(' ') + y: { + valType: "data_array", + description: [ + "Sets the y sample data or coordinates.", + "See overview for more info." + ].join(" ") + }, + x: { + valType: "data_array", + description: [ + "Sets the x sample data or coordinates.", + "See overview for more info." + ].join(" ") + }, + x0: { + valType: "any", + role: "info", + description: [ + "Sets the x coordinate of the box.", + "See overview for more info." + ].join(" ") + }, + y0: { + valType: "any", + role: "info", + description: [ + "Sets the y coordinate of the box.", + "See overview for more info." + ].join(" ") + }, + xcalendar: scatterAttrs.xcalendar, + ycalendar: scatterAttrs.ycalendar, + whiskerwidth: { + valType: "number", + min: 0, + max: 1, + dflt: 0.5, + role: "style", + description: [ + "Sets the width of the whiskers relative to", + "the box' width.", + "For example, with 1, the whiskers are as wide as the box(es)." + ].join(" ") + }, + boxpoints: { + valType: "enumerated", + values: ["all", "outliers", "suspectedoutliers", false], + dflt: "outliers", + role: "style", + description: [ + "If *outliers*, only the sample points lying outside the whiskers", + "are shown", + "If *suspectedoutliers*, the outlier points are shown and", + "points either less than 4*Q1-3*Q3 or greater than 4*Q3-3*Q1", + "are highlighted (see `outliercolor`)", + "If *all*, all sample points are shown", + "If *false*, only the box(es) are shown with no sample points" + ].join(" ") + }, + boxmean: { + valType: "enumerated", + values: [true, "sd", false], + dflt: false, + role: "style", + description: [ + "If *true*, the mean of the box(es)' underlying distribution is", + "drawn as a dashed line inside the box(es).", + "If *sd* the standard deviation is also drawn." + ].join(" ") + }, + jitter: { + valType: "number", + min: 0, + max: 1, + role: "style", + description: [ + "Sets the amount of jitter in the sample points drawn.", + "If *0*, the sample points align along the distribution axis.", + "If *1*, the sample points are drawn in a random jitter of width", + "equal to the width of the box(es)." + ].join(" ") + }, + pointpos: { + valType: "number", + min: -2, + max: 2, + role: "style", + description: [ + "Sets the position of the sample points in relation to the box(es).", + "If *0*, the sample points are places over the center of the box(es).", + "Positive (negative) values correspond to positions to the", + "right (left) for vertical boxes and above (below) for horizontal boxes" + ].join(" ") + }, + orientation: { + valType: "enumerated", + values: ["v", "h"], + role: "style", + description: [ + "Sets the orientation of the box(es).", + "If *v* (*h*), the distribution is visualized along", + "the vertical (horizontal)." + ].join(" ") + }, + marker: { + outliercolor: { + valType: "color", + dflt: "rgba(0, 0, 0, 0)", + role: "style", + description: "Sets the color of the outlier sample points." }, - boxmean: { - valType: 'enumerated', - values: [true, 'sd', false], - dflt: false, - role: 'style', + symbol: extendFlat({}, scatterMarkerAttrs.symbol, { arrayOk: false }), + opacity: extendFlat({}, scatterMarkerAttrs.opacity, { + arrayOk: false, + dflt: 1 + }), + size: extendFlat({}, scatterMarkerAttrs.size, { arrayOk: false }), + color: extendFlat({}, scatterMarkerAttrs.color, { arrayOk: false }), + line: { + color: extendFlat({}, scatterMarkerLineAttrs.color, { + arrayOk: false, + dflt: colorAttrs.defaultLine + }), + width: extendFlat({}, scatterMarkerLineAttrs.width, { + arrayOk: false, + dflt: 0 + }), + outliercolor: { + valType: "color", + role: "style", description: [ - 'If *true*, the mean of the box(es)\' underlying distribution is', - 'drawn as a dashed line inside the box(es).', - 'If *sd* the standard deviation is also drawn.' - ].join(' ') - }, - jitter: { - valType: 'number', + "Sets the border line color of the outlier sample points.", + "Defaults to marker.color" + ].join(" ") + }, + outlierwidth: { + valType: "number", min: 0, - max: 1, - role: 'style', - description: [ - 'Sets the amount of jitter in the sample points drawn.', - 'If *0*, the sample points align along the distribution axis.', - 'If *1*, the sample points are drawn in a random jitter of width', - 'equal to the width of the box(es).' - ].join(' ') - }, - pointpos: { - valType: 'number', - min: -2, - max: 2, - role: 'style', - description: [ - 'Sets the position of the sample points in relation to the box(es).', - 'If *0*, the sample points are places over the center of the box(es).', - 'Positive (negative) values correspond to positions to the', - 'right (left) for vertical boxes and above (below) for horizontal boxes' - ].join(' ') - }, - orientation: { - valType: 'enumerated', - values: ['v', 'h'], - role: 'style', + dflt: 1, + role: "style", description: [ - 'Sets the orientation of the box(es).', - 'If *v* (*h*), the distribution is visualized along', - 'the vertical (horizontal).' - ].join(' ') - }, - marker: { - outliercolor: { - valType: 'color', - dflt: 'rgba(0, 0, 0, 0)', - role: 'style', - description: 'Sets the color of the outlier sample points.' - }, - symbol: extendFlat({}, scatterMarkerAttrs.symbol, - {arrayOk: false}), - opacity: extendFlat({}, scatterMarkerAttrs.opacity, - {arrayOk: false, dflt: 1}), - size: extendFlat({}, scatterMarkerAttrs.size, - {arrayOk: false}), - color: extendFlat({}, scatterMarkerAttrs.color, - {arrayOk: false}), - line: { - color: extendFlat({}, scatterMarkerLineAttrs.color, - {arrayOk: false, dflt: colorAttrs.defaultLine}), - width: extendFlat({}, scatterMarkerLineAttrs.width, - {arrayOk: false, dflt: 0}), - outliercolor: { - valType: 'color', - role: 'style', - description: [ - 'Sets the border line color of the outlier sample points.', - 'Defaults to marker.color' - ].join(' ') - }, - outlierwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: [ - 'Sets the border line width (in px) of the outlier sample points.' - ].join(' ') - } - } - }, - line: { - color: { - valType: 'color', - role: 'style', - description: 'Sets the color of line bounding the box(es).' - }, - width: { - valType: 'number', - role: 'style', - min: 0, - dflt: 2, - description: 'Sets the width (in px) of line bounding the box(es).' - } + "Sets the border line width (in px) of the outlier sample points." + ].join(" ") + } + } + }, + line: { + color: { + valType: "color", + role: "style", + description: "Sets the color of line bounding the box(es)." }, - fillcolor: scatterAttrs.fillcolor + width: { + valType: "number", + role: "style", + min: 0, + dflt: 2, + description: "Sets the width (in px) of line bounding the box(es)." + } + }, + fillcolor: scatterAttrs.fillcolor }; diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index d6a7ca28c14..7ca43a26fed 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -5,143 +5,168 @@ * 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 isNumeric = require("fast-isnumeric"); -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Axes = require('../../plots/cartesian/axes'); - +var Lib = require("../../lib"); +var Axes = require("../../plots/cartesian/axes"); // outlier definition based on http://www.physics.csbsju.edu/stats/box2.html module.exports = function calc(gd, trace) { - var xa = Axes.getFromId(gd, trace.xaxis || 'x'), - ya = Axes.getFromId(gd, trace.yaxis || 'y'), - orientation = trace.orientation, - cd = [], - valAxis, valLetter, val, valBinned, - posAxis, posLetter, pos, posDistinct, dPos; - - // Set value (val) and position (pos) keys via orientation - if(orientation === 'h') { - valAxis = xa; - valLetter = 'x'; - posAxis = ya; - posLetter = 'y'; + var xa = Axes.getFromId(gd, trace.xaxis || "x"), + ya = Axes.getFromId(gd, trace.yaxis || "y"), + orientation = trace.orientation, + cd = [], + valAxis, + valLetter, + val, + valBinned, + posAxis, + posLetter, + pos, + posDistinct, + dPos; + + // Set value (val) and position (pos) keys via orientation + if (orientation === "h") { + valAxis = xa; + valLetter = "x"; + posAxis = ya; + posLetter = "y"; + } else { + valAxis = ya; + valLetter = "y"; + posAxis = xa; + posLetter = "x"; + } + + val = valAxis.makeCalcdata(trace, valLetter); + + // get val + // size autorange based on all source points + // position happens afterward when we know all the pos + Axes.expand(valAxis, val, { padded: true }); + + // In vertical (horizontal) box plots: + // if no x (y) data, use x0 (y0), or name + // so if you want one box + // per trace, set x0 (y0) to the x (y) value or category for this trace + // (or set x (y) to a constant array matching y (x)) + function getPos(gd, trace, posLetter, posAxis, val) { + var pos0; + if (posLetter in trace) { + pos = posAxis.makeCalcdata(trace, posLetter); } else { - valAxis = ya; - valLetter = 'y'; - posAxis = xa; - posLetter = 'x'; + if (posLetter + "0" in trace) { + pos0 = trace[posLetter + "0"]; + } else if ( + "name" in trace && + (posAxis.type === "category" || + isNumeric(trace.name) && + ["linear", "log"].indexOf(posAxis.type) !== -1 || + Lib.isDateTime(trace.name) && posAxis.type === "date") + ) { + pos0 = trace.name; + } else { + pos0 = gd.numboxes; + } + pos0 = posAxis.d2c(pos0, 0, trace[posLetter + "calendar"]); + pos = val.map(function() { + return pos0; + }); } - - val = valAxis.makeCalcdata(trace, valLetter); // get val - - // size autorange based on all source points - // position happens afterward when we know all the pos - Axes.expand(valAxis, val, {padded: true}); - - // In vertical (horizontal) box plots: - // if no x (y) data, use x0 (y0), or name - // so if you want one box - // per trace, set x0 (y0) to the x (y) value or category for this trace - // (or set x (y) to a constant array matching y (x)) - function getPos(gd, trace, posLetter, posAxis, val) { - var pos0; - if(posLetter in trace) pos = posAxis.makeCalcdata(trace, posLetter); - else { - if(posLetter + '0' in trace) pos0 = trace[posLetter + '0']; - else if('name' in trace && ( - posAxis.type === 'category' || - (isNumeric(trace.name) && - ['linear', 'log'].indexOf(posAxis.type) !== -1) || - (Lib.isDateTime(trace.name) && - posAxis.type === 'date') - )) { - pos0 = trace.name; - } - else pos0 = gd.numboxes; - pos0 = posAxis.d2c(pos0, 0, trace[posLetter + 'calendar']); - pos = val.map(function() { return pos0; }); - } - return pos; + return pos; + } + + pos = getPos(gd, trace, posLetter, posAxis, val); + + // get distinct positions and min difference + var dv = Lib.distinctVals(pos); + posDistinct = dv.vals; + dPos = dv.minDiff / 2; + + function binVal(cd, val, pos, posDistinct, dPos) { + var posDistinctLength = posDistinct.length, + valLength = val.length, + valBinned = [], + bins = [], + i, + p, + n, + v; + + // store distinct pos in cd, find bins, init. valBinned + for (i = 0; i < posDistinctLength; ++i) { + p = posDistinct[i]; + cd[i] = { pos: p }; + bins[i] = p - dPos; + valBinned[i] = []; } - - pos = getPos(gd, trace, posLetter, posAxis, val); - - // get distinct positions and min difference - var dv = Lib.distinctVals(pos); - posDistinct = dv.vals; - dPos = dv.minDiff / 2; - - function binVal(cd, val, pos, posDistinct, dPos) { - var posDistinctLength = posDistinct.length, - valLength = val.length, - valBinned = [], - bins = [], - i, p, n, v; - - // store distinct pos in cd, find bins, init. valBinned - for(i = 0; i < posDistinctLength; ++i) { - p = posDistinct[i]; - cd[i] = {pos: p}; - bins[i] = p - dPos; - valBinned[i] = []; - } - bins.push(posDistinct[posDistinctLength - 1] + dPos); - - // bin the values - for(i = 0; i < valLength; ++i) { - v = val[i]; - if(!isNumeric(v)) continue; - n = Lib.findBin(pos[i], bins); - if(n >= 0 && n < valLength) valBinned[n].push(v); - } - - return valBinned; + bins.push(posDistinct[posDistinctLength - 1] + dPos); + + // bin the values + for (i = 0; i < valLength; ++i) { + v = val[i]; + if (!isNumeric(v)) continue; + n = Lib.findBin(pos[i], bins); + if (n >= 0 && n < valLength) valBinned[n].push(v); } - valBinned = binVal(cd, val, pos, posDistinct, dPos); - - // sort the bins and calculate the stats - function calculateStats(cd, valBinned) { - var v, l, cdi, i; - - for(i = 0; i < valBinned.length; ++i) { - v = valBinned[i].sort(Lib.sorterAsc); - l = v.length; - cdi = cd[i]; - - cdi.val = v; // put all values into calcdata - cdi.min = v[0]; - cdi.max = v[l - 1]; - cdi.mean = Lib.mean(v, l); - cdi.sd = Lib.stdev(v, l, cdi.mean); - cdi.q1 = Lib.interp(v, 0.25); // first quartile - cdi.med = Lib.interp(v, 0.5); // median - cdi.q3 = Lib.interp(v, 0.75); // third quartile - // lower and upper fences - last point inside - // 1.5 interquartile ranges from quartiles - cdi.lf = Math.min(cdi.q1, v[ - Math.min(Lib.findBin(2.5 * cdi.q1 - 1.5 * cdi.q3, v, true) + 1, l - 1)]); - cdi.uf = Math.max(cdi.q3, v[ - Math.max(Lib.findBin(2.5 * cdi.q3 - 1.5 * cdi.q1, v), 0)]); - // lower and upper outliers - 3 IQR out (don't clip to max/min, - // this is only for discriminating suspected & far outliers) - cdi.lo = 4 * cdi.q1 - 3 * cdi.q3; - cdi.uo = 4 * cdi.q3 - 3 * cdi.q1; - } + return valBinned; + } + + valBinned = binVal(cd, val, pos, posDistinct, dPos); + + // sort the bins and calculate the stats + function calculateStats(cd, valBinned) { + var v, l, cdi, i; + + for (i = 0; i < valBinned.length; ++i) { + v = valBinned[i].sort(Lib.sorterAsc); + l = v.length; + cdi = cd[i]; + + cdi.val = v; + // put all values into calcdata + cdi.min = v[0]; + cdi.max = v[l - 1]; + cdi.mean = Lib.mean(v, l); + cdi.sd = Lib.stdev(v, l, cdi.mean); + cdi.q1 = Lib.interp(v, 0.25); + // first quartile + cdi.med = Lib.interp(v, 0.5); + // median + cdi.q3 = Lib.interp(v, 0.75); + // third quartile + // lower and upper fences - last point inside + // 1.5 interquartile ranges from quartiles + cdi.lf = Math.min( + cdi.q1, + v[ + Math.min(Lib.findBin(2.5 * cdi.q1 - 1.5 * cdi.q3, v, true) + 1, l - 1) + ] + ); + cdi.uf = Math.max( + cdi.q3, + v[Math.max(Lib.findBin(2.5 * cdi.q3 - 1.5 * cdi.q1, v), 0)] + ); + // lower and upper outliers - 3 IQR out (don't clip to max/min, + // this is only for discriminating suspected & far outliers) + cdi.lo = 4 * cdi.q1 - 3 * cdi.q3; + cdi.uo = 4 * cdi.q3 - 3 * cdi.q1; } + } - calculateStats(cd, valBinned); + calculateStats(cd, valBinned); - // remove empty bins - cd = cd.filter(function(cdi) { return cdi.val && cdi.val.length; }); - if(!cd.length) return [{t: {emptybox: true}}]; + // remove empty bins + cd = cd.filter(function(cdi) { + return cdi.val && cdi.val.length; + }); + if (!cd.length) return [{ t: { emptybox: true } }]; - // add numboxes and dPos to cd - cd[0].t = {boxnum: gd.numboxes, dPos: dPos}; - gd.numboxes++; - return cd; + // add numboxes and dPos to cd + cd[0].t = { boxnum: gd.numboxes, dPos: dPos }; + gd.numboxes++; + return cd; }; diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index e913a66d912..70f81986f8f 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -5,67 +5,76 @@ * 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 Registry = require("../../registry"); +var Color = require("../../components/color"); -'use strict'; +var attributes = require("./attributes"); -var Lib = require('../../lib'); -var Registry = require('../../registry'); -var Color = require('../../components/color'); +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } -var attributes = require('./attributes'); + var y = coerce("y"), x = coerce("x"), defaultOrientation; -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var y = coerce('y'), - x = coerce('x'), - defaultOrientation; - - if(y && y.length) { - defaultOrientation = 'v'; - if(!x) coerce('x0'); - } else if(x && x.length) { - defaultOrientation = 'h'; - coerce('y0'); - } else { - traceOut.visible = false; - return; - } + if (y && y.length) { + defaultOrientation = "v"; + if (!x) coerce("x0"); + } else if (x && x.length) { + defaultOrientation = "h"; + coerce("y0"); + } else { + traceOut.visible = false; + return; + } - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); - handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleTraceDefaults" + ); + handleCalendarDefaults(traceIn, traceOut, ["x", "y"], layout); - coerce('orientation', defaultOrientation); + coerce("orientation", defaultOrientation); - coerce('line.color', (traceIn.marker || {}).color || defaultColor); - coerce('line.width', 2); - coerce('fillcolor', Color.addOpacity(traceOut.line.color, 0.5)); + coerce("line.color", (traceIn.marker || {}).color || defaultColor); + coerce("line.width", 2); + coerce("fillcolor", Color.addOpacity(traceOut.line.color, 0.5)); - coerce('whiskerwidth'); - coerce('boxmean'); + coerce("whiskerwidth"); + coerce("boxmean"); - var outlierColorDflt = Lib.coerce2(traceIn, traceOut, attributes, 'marker.outliercolor'), - lineoutliercolor = coerce('marker.line.outliercolor'), - boxpoints = outlierColorDflt || - lineoutliercolor ? coerce('boxpoints', 'suspectedoutliers') : - coerce('boxpoints'); + var outlierColorDflt = Lib.coerce2( + traceIn, + traceOut, + attributes, + "marker.outliercolor" + ), + lineoutliercolor = coerce("marker.line.outliercolor"), + boxpoints = outlierColorDflt || lineoutliercolor + ? coerce("boxpoints", "suspectedoutliers") + : coerce("boxpoints"); - if(boxpoints) { - coerce('jitter', boxpoints === 'all' ? 0.3 : 0); - coerce('pointpos', boxpoints === 'all' ? -1.5 : 0); + if (boxpoints) { + coerce("jitter", boxpoints === "all" ? 0.3 : 0); + coerce("pointpos", boxpoints === "all" ? -1.5 : 0); - coerce('marker.symbol'); - coerce('marker.opacity'); - coerce('marker.size'); - coerce('marker.color', traceOut.line.color); - coerce('marker.line.color'); - coerce('marker.line.width'); + coerce("marker.symbol"); + coerce("marker.opacity"); + coerce("marker.size"); + coerce("marker.color", traceOut.line.color); + coerce("marker.line.color"); + coerce("marker.line.width"); - if(boxpoints === 'suspectedoutliers') { - coerce('marker.line.outliercolor', traceOut.marker.color); - coerce('marker.line.outlierwidth'); - } + if (boxpoints === "suspectedoutliers") { + coerce("marker.line.outliercolor", traceOut.marker.color); + coerce("marker.line.outlierwidth"); } + } }; diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js index 76e65c5104f..9755ed446df 100644 --- a/src/traces/box/hover.js +++ b/src/traces/box/hover.js @@ -5,103 +5,107 @@ * 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 Axes = require('../../plots/cartesian/axes'); -var Fx = require('../../plots/cartesian/graph_interact'); -var Lib = require('../../lib'); -var Color = require('../../components/color'); +"use strict"; +var Axes = require("../../plots/cartesian/axes"); +var Fx = require("../../plots/cartesian/graph_interact"); +var Lib = require("../../lib"); +var Color = require("../../components/color"); module.exports = function hoverPoints(pointData, xval, yval, hovermode) { - // closest mode: handicap box plots a little relative to others - var cd = pointData.cd, - trace = cd[0].trace, - t = cd[0].t, - xa = pointData.xa, - ya = pointData.ya, - closeData = [], - dx, dy, distfn, boxDelta, - posLetter, posAxis, - val, valLetter, valAxis; - - // adjust inbox w.r.t. to calculate box size - boxDelta = (hovermode === 'closest') ? 2.5 * t.bdPos : t.bdPos; - - if(trace.orientation === 'h') { - dx = function(di) { - return Fx.inbox(di.min - xval, di.max - xval); - }; - dy = function(di) { - var pos = di.pos + t.bPos - yval; - return Fx.inbox(pos - boxDelta, pos + boxDelta); - }; - posLetter = 'y'; - posAxis = ya; - valLetter = 'x'; - valAxis = xa; - } else { - dx = function(di) { - var pos = di.pos + t.bPos - xval; - return Fx.inbox(pos - boxDelta, pos + boxDelta); - }; - dy = function(di) { - return Fx.inbox(di.min - yval, di.max - yval); - }; - posLetter = 'x'; - posAxis = xa; - valLetter = 'y'; - valAxis = ya; - } - - distfn = Fx.getDistanceFunction(hovermode, dx, dy); - Fx.getClosest(cd, distfn, pointData); - - // skip the rest (for this trace) if we didn't find a close point - if(pointData.index === false) return; - - // create the item(s) in closedata for this point - - // the closest data point - var di = cd[pointData.index], - lc = trace.line.color, - mc = (trace.marker || {}).color; - if(Color.opacity(lc) && trace.line.width) pointData.color = lc; - else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc; - else pointData.color = trace.fillcolor; - - pointData[posLetter + '0'] = posAxis.c2p(di.pos + t.bPos - t.bdPos, true); - pointData[posLetter + '1'] = posAxis.c2p(di.pos + t.bPos + t.bdPos, true); - - Axes.tickText(posAxis, posAxis.c2l(di.pos), 'hover').text; - pointData[posLetter + 'LabelVal'] = di.pos; - - // box plots: each "point" gets many labels - var usedVals = {}, - attrs = ['med', 'min', 'q1', 'q3', 'max'], - attr, - pointData2; - if(trace.boxmean) attrs.push('mean'); - if(trace.boxpoints) [].push.apply(attrs, ['lf', 'uf']); - - for(var i = 0; i < attrs.length; i++) { - attr = attrs[i]; - - if(!(attr in di) || (di[attr] in usedVals)) continue; - usedVals[di[attr]] = true; - - // copy out to a new object for each value to label - val = valAxis.c2p(di[attr], true); - pointData2 = Lib.extendFlat({}, pointData); - pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = val; - pointData2[valLetter + 'LabelVal'] = di[attr]; - pointData2.attr = attr; - - if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') { - pointData2[valLetter + 'err'] = di.sd; - } - pointData.name = ''; // only keep name on the first item (median) - closeData.push(pointData2); + // closest mode: handicap box plots a little relative to others + var cd = pointData.cd, + trace = cd[0].trace, + t = cd[0].t, + xa = pointData.xa, + ya = pointData.ya, + closeData = [], + dx, + dy, + distfn, + boxDelta, + posLetter, + posAxis, + val, + valLetter, + valAxis; + + // adjust inbox w.r.t. to calculate box size + boxDelta = hovermode === "closest" ? 2.5 * t.bdPos : t.bdPos; + + if (trace.orientation === "h") { + dx = function(di) { + return Fx.inbox(di.min - xval, di.max - xval); + }; + dy = function(di) { + var pos = di.pos + t.bPos - yval; + return Fx.inbox(pos - boxDelta, pos + boxDelta); + }; + posLetter = "y"; + posAxis = ya; + valLetter = "x"; + valAxis = xa; + } else { + dx = function(di) { + var pos = di.pos + t.bPos - xval; + return Fx.inbox(pos - boxDelta, pos + boxDelta); + }; + dy = function(di) { + return Fx.inbox(di.min - yval, di.max - yval); + }; + posLetter = "x"; + posAxis = xa; + valLetter = "y"; + valAxis = ya; + } + + distfn = Fx.getDistanceFunction(hovermode, dx, dy); + Fx.getClosest(cd, distfn, pointData); + + // skip the rest (for this trace) if we didn't find a close point + if (pointData.index === false) return; + + // create the item(s) in closedata for this point + // the closest data point + var di = cd[pointData.index], + lc = trace.line.color, + mc = (trace.marker || {}).color; + if (Color.opacity(lc) && trace.line.width) pointData.color = lc; + else if (Color.opacity(mc) && trace.boxpoints) pointData.color = mc; + else pointData.color = trace.fillcolor; + + pointData[posLetter + "0"] = posAxis.c2p(di.pos + t.bPos - t.bdPos, true); + pointData[posLetter + "1"] = posAxis.c2p(di.pos + t.bPos + t.bdPos, true); + + Axes.tickText(posAxis, posAxis.c2l(di.pos), "hover").text; + pointData[posLetter + "LabelVal"] = di.pos; + + // box plots: each "point" gets many labels + var usedVals = {}, + attrs = ["med", "min", "q1", "q3", "max"], + attr, + pointData2; + if (trace.boxmean) attrs.push("mean"); + if (trace.boxpoints) [].push.apply(attrs, ["lf", "uf"]); + + for (var i = 0; i < attrs.length; i++) { + attr = attrs[i]; + + if (!(attr in di) || di[attr] in usedVals) continue; + usedVals[di[attr]] = true; + + // copy out to a new object for each value to label + val = valAxis.c2p(di[attr], true); + pointData2 = Lib.extendFlat({}, pointData); + pointData2[valLetter + "0"] = pointData2[valLetter + "1"] = val; + pointData2[valLetter + "LabelVal"] = di[attr]; + pointData2.attr = attr; + + if (attr === "mean" && "sd" in di && trace.boxmean === "sd") { + pointData2[valLetter + "err"] = di.sd; } - return closeData; + pointData.name = ""; + // only keep name on the first item (median) + closeData.push(pointData2); + } + return closeData; }; diff --git a/src/traces/box/index.js b/src/traces/box/index.js index 82ed9d23097..59ea9b83e93 100644 --- a/src/traces/box/index.js +++ b/src/traces/box/index.js @@ -5,40 +5,38 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; var Box = {}; -Box.attributes = require('./attributes'); -Box.layoutAttributes = require('./layout_attributes'); -Box.supplyDefaults = require('./defaults'); -Box.supplyLayoutDefaults = require('./layout_defaults'); -Box.calc = require('./calc'); -Box.setPositions = require('./set_positions'); -Box.plot = require('./plot'); -Box.style = require('./style'); -Box.hoverPoints = require('./hover'); +Box.attributes = require("./attributes"); +Box.layoutAttributes = require("./layout_attributes"); +Box.supplyDefaults = require("./defaults"); +Box.supplyLayoutDefaults = require("./layout_defaults"); +Box.calc = require("./calc"); +Box.setPositions = require("./set_positions"); +Box.plot = require("./plot"); +Box.style = require("./style"); +Box.hoverPoints = require("./hover"); -Box.moduleType = 'trace'; -Box.name = 'box'; -Box.basePlotModule = require('../../plots/cartesian'); -Box.categories = ['cartesian', 'symbols', 'oriented', 'box', 'showLegend']; +Box.moduleType = "trace"; +Box.name = "box"; +Box.basePlotModule = require("../../plots/cartesian"); +Box.categories = ["cartesian", "symbols", "oriented", "box", "showLegend"]; Box.meta = { - description: [ - 'In vertical (horizontal) box plots,', - 'statistics are computed using `y` (`x`) values.', - 'By supplying an `x` (`y`) array, one box per distinct x (y) value', - 'is drawn', - 'If no `x` (`y`) {array} is provided, a single box is drawn.', - 'That box position is then positioned with', - 'with `name` or with `x0` (`y0`) if provided.', - 'Each box spans from quartile 1 (Q1) to quartile 3 (Q3).', - 'The second quartile (Q2) is marked by a line inside the box.', - 'By default, the whiskers correspond to the box\' edges', - '+/- 1.5 times the interquartile range (IQR = Q3-Q1),', - 'see *boxpoints* for other options.' - ].join(' ') + description: [ + "In vertical (horizontal) box plots,", + "statistics are computed using `y` (`x`) values.", + "By supplying an `x` (`y`) array, one box per distinct x (y) value", + "is drawn", + "If no `x` (`y`) {array} is provided, a single box is drawn.", + "That box position is then positioned with", + "with `name` or with `x0` (`y0`) if provided.", + "Each box spans from quartile 1 (Q1) to quartile 3 (Q3).", + "The second quartile (Q2) is marked by a line inside the box.", + "By default, the whiskers correspond to the box' edges", + "+/- 1.5 times the interquartile range (IQR = Q3-Q1),", + "see *boxpoints* for other options." + ].join(" ") }; module.exports = Box; diff --git a/src/traces/box/layout_attributes.js b/src/traces/box/layout_attributes.js index 7e2d9f0fc75..32f878456b1 100644 --- a/src/traces/box/layout_attributes.js +++ b/src/traces/box/layout_attributes.js @@ -5,45 +5,42 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; module.exports = { - boxmode: { - valType: 'enumerated', - values: ['group', 'overlay'], - dflt: 'overlay', - role: 'info', - description: [ - 'Determines how boxes at the same location coordinate', - 'are displayed on the graph.', - 'If *group*, the boxes are plotted next to one another', - 'centered around the shared location.', - 'If *overlay*, the boxes are plotted over one another,', - 'you might need to set *opacity* to see them multiple boxes.' - ].join(' ') - }, - boxgap: { - valType: 'number', - min: 0, - max: 1, - dflt: 0.3, - role: 'style', - description: [ - 'Sets the gap (in plot fraction) between boxes of', - 'adjacent location coordinates.' - ].join(' ') - }, - boxgroupgap: { - valType: 'number', - min: 0, - max: 1, - dflt: 0.3, - role: 'style', - description: [ - 'Sets the gap (in plot fraction) between boxes of', - 'the same location coordinate.' - ].join(' ') - } + boxmode: { + valType: "enumerated", + values: ["group", "overlay"], + dflt: "overlay", + role: "info", + description: [ + "Determines how boxes at the same location coordinate", + "are displayed on the graph.", + "If *group*, the boxes are plotted next to one another", + "centered around the shared location.", + "If *overlay*, the boxes are plotted over one another,", + "you might need to set *opacity* to see them multiple boxes." + ].join(" ") + }, + boxgap: { + valType: "number", + min: 0, + max: 1, + dflt: 0.3, + role: "style", + description: [ + "Sets the gap (in plot fraction) between boxes of", + "adjacent location coordinates." + ].join(" ") + }, + boxgroupgap: { + valType: "number", + min: 0, + max: 1, + dflt: 0.3, + role: "style", + description: [ + "Sets the gap (in plot fraction) between boxes of", + "the same location coordinate." + ].join(" ") + } }; diff --git a/src/traces/box/layout_defaults.js b/src/traces/box/layout_defaults.js index 3213f703af8..f6ca32e3895 100644 --- a/src/traces/box/layout_defaults.js +++ b/src/traces/box/layout_defaults.js @@ -5,28 +5,26 @@ * 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 Registry = require('../../registry'); -var Lib = require('../../lib'); -var layoutAttributes = require('./layout_attributes'); +"use strict"; +var Registry = require("../../registry"); +var Lib = require("../../lib"); +var layoutAttributes = require("./layout_attributes"); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - function coerce(attr, dflt) { - return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); - } + function coerce(attr, dflt) { + return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); + } - var hasBoxes; - for(var i = 0; i < fullData.length; i++) { - if(Registry.traceIs(fullData[i], 'box')) { - hasBoxes = true; - break; - } + var hasBoxes; + for (var i = 0; i < fullData.length; i++) { + if (Registry.traceIs(fullData[i], "box")) { + hasBoxes = true; + break; } - if(!hasBoxes) return; + } + if (!hasBoxes) return; - coerce('boxmode'); - coerce('boxgap'); - coerce('boxgroupgap'); + coerce("boxmode"); + coerce("boxgap"); + coerce("boxgroupgap"); }; diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index f7e5b58ae7c..33c3ed5ff46 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -5,234 +5,378 @@ * 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 d3 = require("d3"); -'use strict'; - -var d3 = require('d3'); - -var Lib = require('../../lib'); -var Drawing = require('../../components/drawing'); - +var Lib = require("../../lib"); +var Drawing = require("../../components/drawing"); // repeatable pseudorandom generator var randSeed = 2000000000; function seed() { - randSeed = 2000000000; + randSeed = 2000000000; } function rand() { - var lastVal = randSeed; - randSeed = (69069 * randSeed + 1) % 4294967296; - // don't let consecutive vals be too close together - // gets away from really trying to be random, in favor of better local uniformity - if(Math.abs(randSeed - lastVal) < 429496729) return rand(); - return randSeed / 4294967296; + var lastVal = randSeed; + randSeed = (69069 * randSeed + 1) % 4294967296; + // don't let consecutive vals be too close together + // gets away from really trying to be random, in favor of better local uniformity + if (Math.abs(randSeed - lastVal) < 429496729) return rand(); + return randSeed / 4294967296; } // constants for dynamic jitter (ie less jitter for sparser points) -var JITTERCOUNT = 5, // points either side of this to include - JITTERSPREAD = 0.01; // fraction of IQR to count as "dense" - +var JITTERCOUNT = 5, + // points either side of this to include + JITTERSPREAD = 0.01; +// fraction of IQR to count as "dense" module.exports = function plot(gd, plotinfo, cdbox) { - var fullLayout = gd._fullLayout, - xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - posAxis, valAxis; - - var boxtraces = plotinfo.plot.select('.boxlayer') - .selectAll('g.trace.boxes') - .data(cdbox) - .enter().append('g') - .attr('class', 'trace boxes'); - - boxtraces.each(function(d) { - var t = d[0].t, - trace = d[0].trace, - group = (fullLayout.boxmode === 'group' && gd.numboxes > 1), - // box half width - bdPos = t.dPos * (1 - fullLayout.boxgap) * (1 - fullLayout.boxgroupgap) / (group ? gd.numboxes : 1), - // box center offset - bPos = group ? 2 * t.dPos * (-0.5 + (t.boxnum + 0.5) / gd.numboxes) * (1 - fullLayout.boxgap) : 0, - // whisker width - wdPos = bdPos * trace.whiskerwidth; - if(trace.visible !== true || t.emptybox) { - d3.select(this).remove(); - return; - } + var fullLayout = gd._fullLayout, + xa = plotinfo.xaxis, + ya = plotinfo.yaxis, + posAxis, + valAxis; + + var boxtraces = plotinfo.plot + .select(".boxlayer") + .selectAll("g.trace.boxes") + .data(cdbox) + .enter() + .append("g") + .attr("class", "trace boxes"); + + boxtraces.each(function(d) { + var t = d[0].t, + trace = d[0].trace, + group = fullLayout.boxmode === "group" && gd.numboxes > 1, + // box half width + bdPos = t.dPos * + (1 - fullLayout.boxgap) * + (1 - fullLayout.boxgroupgap) / + (group ? gd.numboxes : 1), + // box center offset + bPos = group + ? 2 * + t.dPos * + (-0.5 + (t.boxnum + 0.5) / gd.numboxes) * + (1 - fullLayout.boxgap) + : 0, + // whisker width + wdPos = bdPos * trace.whiskerwidth; + if (trace.visible !== true || t.emptybox) { + d3.select(this).remove(); + return; + } + + // set axis via orientation + if (trace.orientation === "h") { + posAxis = ya; + valAxis = xa; + } else { + posAxis = xa; + valAxis = ya; + } + + // save the box size and box position for use by hover + t.bPos = bPos; + t.bdPos = bdPos; + + // repeatable pseudorandom number generator + seed(); - // set axis via orientation - if(trace.orientation === 'h') { - posAxis = ya; - valAxis = xa; + // boxes and whiskers + d3 + .select(this) + .selectAll("path.box") + .data(Lib.identity) + .enter() + .append("path") + .attr("class", "box") + .each(function(d) { + var posc = posAxis.c2p(d.pos + bPos, true), + pos0 = posAxis.c2p(d.pos + bPos - bdPos, true), + pos1 = posAxis.c2p(d.pos + bPos + bdPos, true), + posw0 = posAxis.c2p(d.pos + bPos - wdPos, true), + posw1 = posAxis.c2p(d.pos + bPos + wdPos, true), + q1 = valAxis.c2p(d.q1, true), + q3 = valAxis.c2p(d.q3, true), + // make sure median isn't identical to either of the + // quartiles, so we can see it + m = Lib.constrain( + valAxis.c2p(d.med, true), + Math.min(q1, q3) + 1, + Math.max(q1, q3) - 1 + ), + lf = valAxis.c2p(trace.boxpoints === false ? d.min : d.lf, true), + uf = valAxis.c2p(trace.boxpoints === false ? d.max : d.uf, true); + if (trace.orientation === "h") { + d3.select(this).attr( + "d", + "M" + + m + + "," + + pos0 + + "V" + + pos1 + // median line + "M" + + q1 + + "," + + pos0 + + "V" + + pos1 + + "H" + + q3 + + "V" + + pos0 + + "Z" + // box + "M" + + q1 + + "," + + posc + + "H" + + lf + + "M" + + q3 + + "," + + posc + + "H" + + uf + + (trace.whiskerwidth === 0 // whisker caps + ? "" + : "M" + + lf + + "," + + posw0 + + "V" + + posw1 + + "M" + + uf + + "," + + posw0 + + "V" + + posw1) + ); } else { - posAxis = xa; - valAxis = ya; + d3.select(this).attr( + "d", + "M" + + pos0 + + "," + + m + + "H" + + pos1 + // median line + "M" + + pos0 + + "," + + q1 + + "H" + + pos1 + + "V" + + q3 + + "H" + + pos0 + + "Z" + // box + "M" + + posc + + "," + + q1 + + "V" + + lf + + "M" + + posc + + "," + + q3 + + "V" + + uf + + (trace.whiskerwidth === 0 // whisker caps + ? "" + : "M" + + posw0 + + "," + + lf + + "H" + + posw1 + + "M" + + posw0 + + "," + + uf + + "H" + + posw1) + ); } + }); - // save the box size and box position for use by hover - t.bPos = bPos; - t.bdPos = bdPos; - - // repeatable pseudorandom number generator - seed(); - - // boxes and whiskers - d3.select(this).selectAll('path.box') - .data(Lib.identity) - .enter().append('path') - .attr('class', 'box') - .each(function(d) { - var posc = posAxis.c2p(d.pos + bPos, true), - pos0 = posAxis.c2p(d.pos + bPos - bdPos, true), - pos1 = posAxis.c2p(d.pos + bPos + bdPos, true), - posw0 = posAxis.c2p(d.pos + bPos - wdPos, true), - posw1 = posAxis.c2p(d.pos + bPos + wdPos, true), - q1 = valAxis.c2p(d.q1, true), - q3 = valAxis.c2p(d.q3, true), - // make sure median isn't identical to either of the - // quartiles, so we can see it - m = Lib.constrain(valAxis.c2p(d.med, true), - Math.min(q1, q3) + 1, Math.max(q1, q3) - 1), - lf = valAxis.c2p(trace.boxpoints === false ? d.min : d.lf, true), - uf = valAxis.c2p(trace.boxpoints === false ? d.max : d.uf, true); - if(trace.orientation === 'h') { - d3.select(this).attr('d', - 'M' + m + ',' + pos0 + 'V' + pos1 + // median line - 'M' + q1 + ',' + pos0 + 'V' + pos1 + 'H' + q3 + 'V' + pos0 + 'Z' + // box - 'M' + q1 + ',' + posc + 'H' + lf + 'M' + q3 + ',' + posc + 'H' + uf + // whiskers - ((trace.whiskerwidth === 0) ? '' : // whisker caps - 'M' + lf + ',' + posw0 + 'V' + posw1 + 'M' + uf + ',' + posw0 + 'V' + posw1)); - } else { - d3.select(this).attr('d', - 'M' + pos0 + ',' + m + 'H' + pos1 + // median line - 'M' + pos0 + ',' + q1 + 'H' + pos1 + 'V' + q3 + 'H' + pos0 + 'Z' + // box - 'M' + posc + ',' + q1 + 'V' + lf + 'M' + posc + ',' + q3 + 'V' + uf + // whiskers - ((trace.whiskerwidth === 0) ? '' : // whisker caps - 'M' + posw0 + ',' + lf + 'H' + posw1 + 'M' + posw0 + ',' + uf + 'H' + posw1)); + // draw points, if desired + if (trace.boxpoints) { + d3 + .select(this) + .selectAll("g.points") + .data(function(d) { + d.forEach(function(v) { + v.t = t; + v.trace = trace; + }); + return d; + }) + .enter() + .append("g") + .attr("class", "points") + .selectAll("path") + .data(function(d) { + var pts = trace.boxpoints === "all" + ? d.val + : d.val.filter(function(v) { + return v < d.lf || v > d.uf; + }), + // normally use IQR, but if this is 0 or too small, use max-min + typicalSpread = Math.max((d.max - d.min) / 10, d.q3 - d.q1), + minSpread = typicalSpread * 1e-9, + spreadLimit = typicalSpread * JITTERSPREAD, + jitterFactors = [], + maxJitterFactor = 0, + i, + i0, + i1, + pmin, + pmax, + jitterFactor, + newJitter; + + // dynamic jitter + if (trace.jitter) { + if (typicalSpread === 0) { + // edge case of no spread at all: fall back to max jitter + maxJitterFactor = 1; + jitterFactors = new Array(pts.length); + for (i = 0; i < pts.length; i++) { + jitterFactors[i] = 1; + } + } else { + for (i = 0; i < pts.length; i++) { + i0 = Math.max(0, i - JITTERCOUNT); + pmin = pts[i0]; + i1 = Math.min(pts.length - 1, i + JITTERCOUNT); + pmax = pts[i1]; + + if (trace.boxpoints !== "all") { + if (pts[i] < d.lf) pmax = Math.min(pmax, d.lf); + else pmin = Math.max(pmin, d.uf); } - }); - - // draw points, if desired - if(trace.boxpoints) { - d3.select(this).selectAll('g.points') - // since box plot points get an extra level of nesting, each - // box needs the trace styling info - .data(function(d) { - d.forEach(function(v) { - v.t = t; - v.trace = trace; - }); - return d; - }) - .enter().append('g') - .attr('class', 'points') - .selectAll('path') - .data(function(d) { - var pts = (trace.boxpoints === 'all') ? d.val : - d.val.filter(function(v) { return (v < d.lf || v > d.uf); }), - // normally use IQR, but if this is 0 or too small, use max-min - typicalSpread = Math.max((d.max - d.min) / 10, d.q3 - d.q1), - minSpread = typicalSpread * 1e-9, - spreadLimit = typicalSpread * JITTERSPREAD, - jitterFactors = [], - maxJitterFactor = 0, - i, - i0, i1, - pmin, - pmax, - jitterFactor, - newJitter; - - // dynamic jitter - if(trace.jitter) { - if(typicalSpread === 0) { - // edge case of no spread at all: fall back to max jitter - maxJitterFactor = 1; - jitterFactors = new Array(pts.length); - for(i = 0; i < pts.length; i++) { - jitterFactors[i] = 1; - } - } - else { - for(i = 0; i < pts.length; i++) { - i0 = Math.max(0, i - JITTERCOUNT); - pmin = pts[i0]; - i1 = Math.min(pts.length - 1, i + JITTERCOUNT); - pmax = pts[i1]; - - if(trace.boxpoints !== 'all') { - if(pts[i] < d.lf) pmax = Math.min(pmax, d.lf); - else pmin = Math.max(pmin, d.uf); - } - - jitterFactor = Math.sqrt(spreadLimit * (i1 - i0) / (pmax - pmin + minSpread)) || 0; - jitterFactor = Lib.constrain(Math.abs(jitterFactor), 0, 1); - - jitterFactors.push(jitterFactor); - maxJitterFactor = Math.max(jitterFactor, maxJitterFactor); - } - } - newJitter = trace.jitter * 2 / maxJitterFactor; - } - - return pts.map(function(v, i) { - var posOffset = trace.pointpos, - p; - if(trace.jitter) { - posOffset += newJitter * jitterFactors[i] * (rand() - 0.5); - } - - if(trace.orientation === 'h') { - p = { - y: d.pos + posOffset * bdPos + bPos, - x: v - }; - } else { - p = { - x: d.pos + posOffset * bdPos + bPos, - y: v - }; - } - - // tag suspected outliers - if(trace.boxpoints === 'suspectedoutliers' && v < d.uo && v > d.lo) { - p.so = true; - } - return p; - }); - }) - .enter().append('path') - .call(Drawing.translatePoints, xa, ya); - } - // draw mean (and stdev diamond) if desired - if(trace.boxmean) { - d3.select(this).selectAll('path.mean') - .data(Lib.identity) - .enter().append('path') - .attr('class', 'mean') - .style('fill', 'none') - .each(function(d) { - var posc = posAxis.c2p(d.pos + bPos, true), - pos0 = posAxis.c2p(d.pos + bPos - bdPos, true), - pos1 = posAxis.c2p(d.pos + bPos + bdPos, true), - m = valAxis.c2p(d.mean, true), - sl = valAxis.c2p(d.mean - d.sd, true), - sh = valAxis.c2p(d.mean + d.sd, true); - if(trace.orientation === 'h') { - d3.select(this).attr('d', - 'M' + m + ',' + pos0 + 'V' + pos1 + - ((trace.boxmean !== 'sd') ? '' : - 'm0,0L' + sl + ',' + posc + 'L' + m + ',' + pos0 + 'L' + sh + ',' + posc + 'Z')); - } - else { - d3.select(this).attr('d', - 'M' + pos0 + ',' + m + 'H' + pos1 + - ((trace.boxmean !== 'sd') ? '' : - 'm0,0L' + posc + ',' + sl + 'L' + pos0 + ',' + m + 'L' + posc + ',' + sh + 'Z')); - } - }); - } - }); + + jitterFactor = Math.sqrt( + spreadLimit * (i1 - i0) / (pmax - pmin + minSpread) + ) || + 0; + jitterFactor = Lib.constrain(Math.abs(jitterFactor), 0, 1); + + jitterFactors.push(jitterFactor); + maxJitterFactor = Math.max(jitterFactor, maxJitterFactor); + } + } + newJitter = trace.jitter * 2 / maxJitterFactor; + } + + return pts.map(function(v, i) { + var posOffset = trace.pointpos, p; + if (trace.jitter) { + posOffset += newJitter * jitterFactors[i] * (rand() - 0.5); + } + + if (trace.orientation === "h") { + p = { y: d.pos + posOffset * bdPos + bPos, x: v }; + } else { + p = { x: d.pos + posOffset * bdPos + bPos, y: v }; + } + + // tag suspected outliers + if ( + trace.boxpoints === "suspectedoutliers" && v < d.uo && v > d.lo + ) { + p.so = true; + } + return p; + }); + }) + .enter() + .append("path") + .call(Drawing.translatePoints, xa, ya); + } + // draw mean (and stdev diamond) if desired + if (trace.boxmean) { + d3 + .select(this) + .selectAll("path.mean") + .data(Lib.identity) + .enter() + .append("path") + .attr("class", "mean") + .style("fill", "none") + .each(function(d) { + var posc = posAxis.c2p(d.pos + bPos, true), + pos0 = posAxis.c2p(d.pos + bPos - bdPos, true), + pos1 = posAxis.c2p(d.pos + bPos + bdPos, true), + m = valAxis.c2p(d.mean, true), + sl = valAxis.c2p(d.mean - d.sd, true), + sh = valAxis.c2p(d.mean + d.sd, true); + if (trace.orientation === "h") { + d3 + .select(this) + .attr( + "d", + "M" + + m + + "," + + pos0 + + "V" + + pos1 + + (trace.boxmean !== "sd" + ? "" + : "m0,0L" + + sl + + "," + + posc + + "L" + + m + + "," + + pos0 + + "L" + + sh + + "," + + posc + + "Z") + ); + } else { + d3 + .select(this) + .attr( + "d", + "M" + + pos0 + + "," + + m + + "H" + + pos1 + + (trace.boxmean !== "sd" + ? "" + : "m0,0L" + + posc + + "," + + sl + + "L" + + pos0 + + "," + + m + + "L" + + posc + + "," + + sh + + "Z") + ); + } + }); + } + }); }; diff --git a/src/traces/box/set_positions.js b/src/traces/box/set_positions.js index 30580031d4b..ed68a4a7557 100644 --- a/src/traces/box/set_positions.js +++ b/src/traces/box/set_positions.js @@ -5,88 +5,90 @@ * 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 Registry = require('../../registry'); -var Axes = require('../../plots/cartesian/axes'); -var Lib = require('../../lib'); - +"use strict"; +var Registry = require("../../registry"); +var Axes = require("../../plots/cartesian/axes"); +var Lib = require("../../lib"); module.exports = function setPositions(gd, plotinfo) { - var fullLayout = gd._fullLayout, - xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - orientations = ['v', 'h']; - var posAxis, i, j, k; + var fullLayout = gd._fullLayout, + xa = plotinfo.xaxis, + ya = plotinfo.yaxis, + orientations = ["v", "h"]; + var posAxis, i, j, k; - for(i = 0; i < orientations.length; ++i) { - var orientation = orientations[i], - boxlist = [], - boxpointlist = [], - minPad = 0, - maxPad = 0, - cd, - t, - trace; + for (i = 0; i < orientations.length; ++i) { + var orientation = orientations[i], + boxlist = [], + boxpointlist = [], + minPad = 0, + maxPad = 0, + cd, + t, + trace; - // set axis via orientation - if(orientation === 'h') posAxis = ya; - else posAxis = xa; + // set axis via orientation + if (orientation === "h") posAxis = ya; + else posAxis = xa; - // make list of boxes - for(j = 0; j < gd.calcdata.length; ++j) { - cd = gd.calcdata[j]; - t = cd[0].t; - trace = cd[0].trace; - - if(trace.visible === true && Registry.traceIs(trace, 'box') && - !t.emptybox && - trace.orientation === orientation && - trace.xaxis === xa._id && - trace.yaxis === ya._id) { - boxlist.push(j); - if(trace.boxpoints !== false) { - minPad = Math.max(minPad, trace.jitter - trace.pointpos - 1); - maxPad = Math.max(maxPad, trace.jitter + trace.pointpos - 1); - } - } - } + // make list of boxes + for (j = 0; j < gd.calcdata.length; ++j) { + cd = gd.calcdata[j]; + t = cd[0].t; + trace = cd[0].trace; - // make list of box points - for(j = 0; j < boxlist.length; j++) { - cd = gd.calcdata[boxlist[j]]; - for(k = 0; k < cd.length; k++) boxpointlist.push(cd[k].pos); + if ( + trace.visible === true && + Registry.traceIs(trace, "box") && + !t.emptybox && + trace.orientation === orientation && + trace.xaxis === xa._id && + trace.yaxis === ya._id + ) { + boxlist.push(j); + if (trace.boxpoints !== false) { + minPad = Math.max(minPad, trace.jitter - trace.pointpos - 1); + maxPad = Math.max(maxPad, trace.jitter + trace.pointpos - 1); } - if(!boxpointlist.length) continue; + } + } - // box plots - update dPos based on multiple traces - // and then use for posAxis autorange + // make list of box points + for (j = 0; j < boxlist.length; j++) { + cd = gd.calcdata[boxlist[j]]; + for (k = 0; k < cd.length; k++) { + boxpointlist.push(cd[k].pos); + } + } + if (!boxpointlist.length) continue; - var boxdv = Lib.distinctVals(boxpointlist), - dPos = boxdv.minDiff / 2; + // box plots - update dPos based on multiple traces + // and then use for posAxis autorange + var boxdv = Lib.distinctVals(boxpointlist), dPos = boxdv.minDiff / 2; - // if there's no duplication of x points, - // disable 'group' mode by setting numboxes=1 - if(boxpointlist.length === boxdv.vals.length) gd.numboxes = 1; + // if there's no duplication of x points, + // disable 'group' mode by setting numboxes=1 + if (boxpointlist.length === boxdv.vals.length) gd.numboxes = 1; - // check for forced minimum dtick - Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true); + // check for forced minimum dtick + Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true); - // set the width of all boxes - for(i = 0; i < boxlist.length; i++) { - var boxListIndex = boxlist[i]; - gd.calcdata[boxListIndex][0].t.dPos = dPos; - } - - // autoscale the x axis - including space for points if they're off the side - // TODO: this will overdo it if the outermost boxes don't have - // their points as far out as the other boxes - var padfactor = (1 - fullLayout.boxgap) * (1 - fullLayout.boxgroupgap) * - dPos / gd.numboxes; - Axes.expand(posAxis, boxdv.vals, { - vpadminus: dPos + minPad * padfactor, - vpadplus: dPos + maxPad * padfactor - }); + // set the width of all boxes + for (i = 0; i < boxlist.length; i++) { + var boxListIndex = boxlist[i]; + gd.calcdata[boxListIndex][0].t.dPos = dPos; } + + // autoscale the x axis - including space for points if they're off the side + // TODO: this will overdo it if the outermost boxes don't have + // their points as far out as the other boxes + var padfactor = (1 - fullLayout.boxgap) * + (1 - fullLayout.boxgroupgap) * + dPos / + gd.numboxes; + Axes.expand(posAxis, boxdv.vals, { + vpadminus: dPos + minPad * padfactor, + vpadplus: dPos + maxPad * padfactor + }); + } }; diff --git a/src/traces/box/style.js b/src/traces/box/style.js index cb187ebedca..559bd46beb4 100644 --- a/src/traces/box/style.js +++ b/src/traces/box/style.js @@ -5,33 +5,38 @@ * 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 d3 = require("d3"); -'use strict'; - -var d3 = require('d3'); - -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); - +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); module.exports = function style(gd) { - var s = d3.select(gd).selectAll('g.trace.boxes'); + var s = d3.select(gd).selectAll("g.trace.boxes"); - s.style('opacity', function(d) { return d[0].trace.opacity; }) - .each(function(d) { - var trace = d[0].trace, - lineWidth = trace.line.width; - d3.select(this).selectAll('path.box') - .style('stroke-width', lineWidth + 'px') - .call(Color.stroke, trace.line.color) - .call(Color.fill, trace.fillcolor); - d3.select(this).selectAll('path.mean') - .style({ - 'stroke-width': lineWidth, - 'stroke-dasharray': (2 * lineWidth) + 'px,' + lineWidth + 'px' - }) - .call(Color.stroke, trace.line.color); - d3.select(this).selectAll('g.points path') - .call(Drawing.pointStyle, trace); - }); + s + .style("opacity", function(d) { + return d[0].trace.opacity; + }) + .each(function(d) { + var trace = d[0].trace, lineWidth = trace.line.width; + d3 + .select(this) + .selectAll("path.box") + .style("stroke-width", lineWidth + "px") + .call(Color.stroke, trace.line.color) + .call(Color.fill, trace.fillcolor); + d3 + .select(this) + .selectAll("path.mean") + .style({ + "stroke-width": lineWidth, + "stroke-dasharray": 2 * lineWidth + "px," + lineWidth + "px" + }) + .call(Color.stroke, trace.line.color); + d3 + .select(this) + .selectAll("g.points path") + .call(Drawing.pointStyle, trace); + }); }; diff --git a/src/traces/candlestick/attributes.js b/src/traces/candlestick/attributes.js index c6c4e18ac3e..2b37893bd8f 100644 --- a/src/traces/candlestick/attributes.js +++ b/src/traces/candlestick/attributes.js @@ -5,52 +5,43 @@ * 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 OHLCattrs = require('../ohlc/attributes'); -var boxAttrs = require('../box/attributes'); +"use strict"; +var Lib = require("../../lib"); +var OHLCattrs = require("../ohlc/attributes"); +var boxAttrs = require("../box/attributes"); var directionAttrs = { - name: OHLCattrs.increasing.name, - showlegend: OHLCattrs.increasing.showlegend, - - line: { - color: Lib.extendFlat({}, boxAttrs.line.color), - width: Lib.extendFlat({}, boxAttrs.line.width) - }, - - fillcolor: Lib.extendFlat({}, boxAttrs.fillcolor), + name: OHLCattrs.increasing.name, + showlegend: OHLCattrs.increasing.showlegend, + line: { + color: Lib.extendFlat({}, boxAttrs.line.color), + width: Lib.extendFlat({}, boxAttrs.line.width) + }, + fillcolor: Lib.extendFlat({}, boxAttrs.fillcolor) }; module.exports = { - x: OHLCattrs.x, - open: OHLCattrs.open, - high: OHLCattrs.high, - low: OHLCattrs.low, - close: OHLCattrs.close, - - line: { - width: Lib.extendFlat({}, boxAttrs.line.width, { - description: [ - boxAttrs.line.width.description, - 'Note that this style setting can also be set per', - 'direction via `increasing.line.width` and', - '`decreasing.line.width`.' - ].join(' ') - }) - }, - - increasing: Lib.extendDeep({}, directionAttrs, { - line: { color: { dflt: OHLCattrs.increasing.line.color.dflt } } - }), - - decreasing: Lib.extendDeep({}, directionAttrs, { - line: { color: { dflt: OHLCattrs.decreasing.line.color.dflt } } - }), - - text: OHLCattrs.text, - whiskerwidth: Lib.extendFlat({}, boxAttrs.whiskerwidth, { dflt: 0 }) + x: OHLCattrs.x, + open: OHLCattrs.open, + high: OHLCattrs.high, + low: OHLCattrs.low, + close: OHLCattrs.close, + line: { + width: Lib.extendFlat({}, boxAttrs.line.width, { + description: [ + boxAttrs.line.width.description, + "Note that this style setting can also be set per", + "direction via `increasing.line.width` and", + "`decreasing.line.width`." + ].join(" ") + }) + }, + increasing: Lib.extendDeep({}, directionAttrs, { + line: { color: { dflt: OHLCattrs.increasing.line.color.dflt } } + }), + decreasing: Lib.extendDeep({}, directionAttrs, { + line: { color: { dflt: OHLCattrs.decreasing.line.color.dflt } } + }), + text: OHLCattrs.text, + whiskerwidth: Lib.extendFlat({}, boxAttrs.whiskerwidth, { dflt: 0 }) }; diff --git a/src/traces/candlestick/defaults.js b/src/traces/candlestick/defaults.js index 66213e94794..8238fa4a936 100644 --- a/src/traces/candlestick/defaults.js +++ b/src/traces/candlestick/defaults.js @@ -5,42 +5,44 @@ * 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 handleOHLC = require('../ohlc/ohlc_defaults'); -var handleDirectionDefaults = require('../ohlc/direction_defaults'); -var helpers = require('../ohlc/helpers'); -var attributes = require('./attributes'); - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - helpers.pushDummyTransformOpts(traceIn, traceOut); - - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var len = handleOHLC(traceIn, traceOut, coerce, layout); - if(len === 0) { - traceOut.visible = false; - return; - } - - coerce('line.width'); - - handleDirection(traceIn, traceOut, coerce, 'increasing'); - handleDirection(traceIn, traceOut, coerce, 'decreasing'); - - coerce('text'); - coerce('whiskerwidth'); +"use strict"; +var Lib = require("../../lib"); +var handleOHLC = require("../ohlc/ohlc_defaults"); +var handleDirectionDefaults = require("../ohlc/direction_defaults"); +var helpers = require("../ohlc/helpers"); +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + helpers.pushDummyTransformOpts(traceIn, traceOut); + + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var len = handleOHLC(traceIn, traceOut, coerce, layout); + if (len === 0) { + traceOut.visible = false; + return; + } + + coerce("line.width"); + + handleDirection(traceIn, traceOut, coerce, "increasing"); + handleDirection(traceIn, traceOut, coerce, "decreasing"); + + coerce("text"); + coerce("whiskerwidth"); }; function handleDirection(traceIn, traceOut, coerce, direction) { - handleDirectionDefaults(traceIn, traceOut, coerce, direction); + handleDirectionDefaults(traceIn, traceOut, coerce, direction); - coerce(direction + '.line.color'); - coerce(direction + '.line.width', traceOut.line.width); - coerce(direction + '.fillcolor'); + coerce(direction + ".line.color"); + coerce(direction + ".line.width", traceOut.line.width); + coerce(direction + ".fillcolor"); } diff --git a/src/traces/candlestick/index.js b/src/traces/candlestick/index.js index 13764ecbabe..aee243fdf3e 100644 --- a/src/traces/candlestick/index.js +++ b/src/traces/candlestick/index.js @@ -5,36 +5,29 @@ * 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 register = require('../../plot_api/register'); +"use strict"; +var register = require("../../plot_api/register"); module.exports = { - moduleType: 'trace', - name: 'candlestick', - basePlotModule: require('../../plots/cartesian'), - categories: ['cartesian', 'showLegend', 'candlestick'], - meta: { - description: [ - 'The candlestick is a style of financial chart describing', - 'open, high, low and close for a given `x` coordinate (most likely time).', - - 'The boxes represent the spread between the `open` and `close` values and', - 'the lines represent the spread between the `low` and `high` values', - - 'Sample points where the close value is higher (lower) then the open', - 'value are called increasing (decreasing).', - - 'By default, increasing candles are drawn in green whereas', - 'decreasing are drawn in red.' - ].join(' ') - }, - - attributes: require('./attributes'), - supplyDefaults: require('./defaults'), + moduleType: "trace", + name: "candlestick", + basePlotModule: require("../../plots/cartesian"), + categories: ["cartesian", "showLegend", "candlestick"], + meta: { + description: [ + "The candlestick is a style of financial chart describing", + "open, high, low and close for a given `x` coordinate (most likely time).", + "The boxes represent the spread between the `open` and `close` values and", + "the lines represent the spread between the `low` and `high` values", + "Sample points where the close value is higher (lower) then the open", + "value are called increasing (decreasing).", + "By default, increasing candles are drawn in green whereas", + "decreasing are drawn in red." + ].join(" ") + }, + attributes: require("./attributes"), + supplyDefaults: require("./defaults") }; -register(require('../box')); -register(require('./transform')); +register(require("../box")); +register(require("./transform")); diff --git a/src/traces/candlestick/transform.js b/src/traces/candlestick/transform.js index ce0aaeb03ad..e5d55785174 100644 --- a/src/traces/candlestick/transform.js +++ b/src/traces/candlestick/transform.js @@ -5,122 +5,111 @@ * 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 helpers = require("../ohlc/helpers"); +exports.moduleType = "transform"; -'use strict'; - -var Lib = require('../../lib'); -var helpers = require('../ohlc/helpers'); - -exports.moduleType = 'transform'; - -exports.name = 'candlestick'; +exports.name = "candlestick"; exports.attributes = {}; exports.supplyDefaults = function(transformIn, traceOut, layout, traceIn) { - helpers.clearEphemeralTransformOpts(traceIn); - helpers.copyOHLC(transformIn, traceOut); + helpers.clearEphemeralTransformOpts(traceIn); + helpers.copyOHLC(transformIn, traceOut); - return transformIn; + return transformIn; }; exports.transform = function transform(dataIn, state) { - var dataOut = []; - - for(var i = 0; i < dataIn.length; i++) { - var traceIn = dataIn[i]; + var dataOut = []; - if(traceIn.type !== 'candlestick') { - dataOut.push(traceIn); - continue; - } + for (var i = 0; i < dataIn.length; i++) { + var traceIn = dataIn[i]; - dataOut.push( - makeTrace(traceIn, state, 'increasing'), - makeTrace(traceIn, state, 'decreasing') - ); + if (traceIn.type !== "candlestick") { + dataOut.push(traceIn); + continue; } - helpers.addRangeSlider(dataOut, state.layout); + dataOut.push( + makeTrace(traceIn, state, "increasing"), + makeTrace(traceIn, state, "decreasing") + ); + } - return dataOut; + helpers.addRangeSlider(dataOut, state.layout); + + return dataOut; }; function makeTrace(traceIn, state, direction) { - var traceOut = { - type: 'box', - boxpoints: false, - - visible: traceIn.visible, - hoverinfo: traceIn.hoverinfo, - opacity: traceIn.opacity, - xaxis: traceIn.xaxis, - yaxis: traceIn.yaxis, - - transforms: helpers.makeTransform(traceIn, state, direction) - }; - - // the rest of below may not have been coerced - - var directionOpts = traceIn[direction]; - - if(directionOpts) { - Lib.extendFlat(traceOut, { - - // to make autotype catch date axes soon!! - x: traceIn.x || [0], - xcalendar: traceIn.xcalendar, - - // concat low and high to get correct autorange - y: [].concat(traceIn.low).concat(traceIn.high), - - whiskerwidth: traceIn.whiskerwidth, - text: traceIn.text, - - name: directionOpts.name, - showlegend: directionOpts.showlegend, - line: directionOpts.line, - fillcolor: directionOpts.fillcolor - }); - } - - return traceOut; + var traceOut = { + type: "box", + boxpoints: false, + visible: traceIn.visible, + hoverinfo: traceIn.hoverinfo, + opacity: traceIn.opacity, + xaxis: traceIn.xaxis, + yaxis: traceIn.yaxis, + transforms: helpers.makeTransform(traceIn, state, direction) + }; + + // the rest of below may not have been coerced + var directionOpts = traceIn[direction]; + + if (directionOpts) { + Lib.extendFlat(traceOut, { + // to make autotype catch date axes soon!! + x: ( + traceIn.x || [0] + ), + xcalendar: traceIn.xcalendar, + // concat low and high to get correct autorange + y: [].concat(traceIn.low).concat(traceIn.high), + whiskerwidth: traceIn.whiskerwidth, + text: traceIn.text, + name: directionOpts.name, + showlegend: directionOpts.showlegend, + line: directionOpts.line, + fillcolor: directionOpts.fillcolor + }); + } + + return traceOut; } exports.calcTransform = function calcTransform(gd, trace, opts) { - var direction = opts.direction, - filterFn = helpers.getFilterFn(direction); - - var open = trace.open, - high = trace.high, - low = trace.low, - close = trace.close; - - var len = open.length, - x = [], - y = []; - - var appendX = trace._fullInput.x ? - function(i) { - var v = trace.x[i]; - x.push(v, v, v, v, v, v); - } : - function(i) { - x.push(i, i, i, i, i, i); - }; - - var appendY = function(o, h, l, c) { - y.push(l, o, c, c, c, h); - }; - - for(var i = 0; i < len; i++) { - if(filterFn(open[i], close[i])) { - appendX(i); - appendY(open[i], high[i], low[i], close[i]); - } + var direction = opts.direction, filterFn = helpers.getFilterFn(direction); + + var open = trace.open, + high = trace.high, + low = trace.low, + close = trace.close; + + var len = open.length, x = [], y = []; + + var appendX = trace._fullInput.x + ? (function(i) { + var v = trace.x[i]; + x.push(v, v, v, v, v, v); + }) + : (function(i) { + x.push(i, i, i, i, i, i); + }); + + var appendY = function(o, h, l, c) { + y.push(l, o, c, c, c, h); + }; + + for (var i = 0; i < len; i++) { + if (filterFn(open[i], close[i])) { + appendX(i); + appendY(open[i], high[i], low[i], close[i]); } + } - trace.x = x; - trace.y = y; + trace.x = x; + trace.y = y; }; diff --git a/src/traces/choropleth/attributes.js b/src/traces/choropleth/attributes.js index 85523db3ed5..487cf9beb0b 100644 --- a/src/traces/choropleth/attributes.js +++ b/src/traces/choropleth/attributes.js @@ -5,45 +5,42 @@ * 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 ScatterGeoAttrs = require("../scattergeo/attributes"); +var colorscaleAttrs = require("../../components/colorscale/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); +var plotAttrs = require("../../plots/attributes"); -'use strict'; - -var ScatterGeoAttrs = require('../scattergeo/attributes'); -var colorscaleAttrs = require('../../components/colorscale/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); -var plotAttrs = require('../../plots/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; +var extendFlat = require("../../lib/extend").extendFlat; var ScatterGeoMarkerLineAttrs = ScatterGeoAttrs.marker.line; -module.exports = extendFlat({}, { +module.exports = extendFlat( + {}, + { locations: { - valType: 'data_array', - description: [ - 'Sets the coordinates via location IDs or names.', - 'See `locationmode` for more info.' - ].join(' ') + valType: "data_array", + description: [ + "Sets the coordinates via location IDs or names.", + "See `locationmode` for more info." + ].join(" ") }, locationmode: ScatterGeoAttrs.locationmode, - z: { - valType: 'data_array', - description: 'Sets the color values.' - }, + z: { valType: "data_array", description: "Sets the color values." }, text: { - valType: 'data_array', - description: 'Sets the text elements associated with each location.' + valType: "data_array", + description: "Sets the text elements associated with each location." }, marker: { - line: { - color: ScatterGeoMarkerLineAttrs.color, - width: extendFlat({}, ScatterGeoMarkerLineAttrs.width, {dflt: 1}) - } + line: { + color: ScatterGeoMarkerLineAttrs.color, + width: extendFlat({}, ScatterGeoMarkerLineAttrs.width, { dflt: 1 }) + } }, hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { - flags: ['location', 'z', 'text', 'name'] - }), -}, - colorscaleAttrs, - { colorbar: colorbarAttrs } + flags: ["location", "z", "text", "name"] + }) + }, + colorscaleAttrs, + { colorbar: colorbarAttrs } ); diff --git a/src/traces/choropleth/calc.js b/src/traces/choropleth/calc.js index 5a3eacb14a4..71356514aaf 100644 --- a/src/traces/choropleth/calc.js +++ b/src/traces/choropleth/calc.js @@ -5,13 +5,9 @@ * 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 colorscaleCalc = require('../../components/colorscale/calc'); - +"use strict"; +var colorscaleCalc = require("../../components/colorscale/calc"); module.exports = function calc(gd, trace) { - colorscaleCalc(trace, trace.z, '', 'z'); + colorscaleCalc(trace, trace.z, "", "z"); }; diff --git a/src/traces/choropleth/defaults.js b/src/traces/choropleth/defaults.js index d4dbfa057d5..1c43c0b6019 100644 --- a/src/traces/choropleth/defaults.js +++ b/src/traces/choropleth/defaults.js @@ -5,49 +5,51 @@ * 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 colorscaleDefaults = require("../../components/colorscale/defaults"); +var attributes = require("./attributes"); -'use strict'; +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } -var Lib = require('../../lib'); + var locations = coerce("locations"); -var colorscaleDefaults = require('../../components/colorscale/defaults'); -var attributes = require('./attributes'); + var len; + if (locations) len = locations.length; + if (!locations || !len) { + traceOut.visible = false; + return; + } -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } + var z = coerce("z"); + if (!Array.isArray(z)) { + traceOut.visible = false; + return; + } - var locations = coerce('locations'); + if (z.length > len) traceOut.z = z.slice(0, len); - var len; - if(locations) len = locations.length; + coerce("locationmode"); - if(!locations || !len) { - traceOut.visible = false; - return; - } + coerce("text"); - var z = coerce('z'); - if(!Array.isArray(z)) { - traceOut.visible = false; - return; - } + coerce("marker.line.color"); + coerce("marker.line.width"); - if(z.length > len) traceOut.z = z.slice(0, len); + colorscaleDefaults(traceIn, traceOut, layout, coerce, { + prefix: "", + cLetter: "z" + }); - coerce('locationmode'); - - coerce('text'); - - coerce('marker.line.color'); - coerce('marker.line.width'); - - colorscaleDefaults( - traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} - ); - - coerce('hoverinfo', (layout._dataLength === 1) ? 'location+z+text' : undefined); + coerce("hoverinfo", layout._dataLength === 1 ? "location+z+text" : undefined); }; diff --git a/src/traces/choropleth/index.js b/src/traces/choropleth/index.js index 15cfae98a54..7f8f84b1f45 100644 --- a/src/traces/choropleth/index.js +++ b/src/traces/choropleth/index.js @@ -5,32 +5,29 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var Choropleth = {}; -Choropleth.attributes = require('./attributes'); -Choropleth.supplyDefaults = require('./defaults'); -Choropleth.colorbar = require('../heatmap/colorbar'); -Choropleth.calc = require('./calc'); -Choropleth.plot = require('./plot').plot; +Choropleth.attributes = require("./attributes"); +Choropleth.supplyDefaults = require("./defaults"); +Choropleth.colorbar = require("../heatmap/colorbar"); +Choropleth.calc = require("./calc"); +Choropleth.plot = require("./plot").plot; // add dummy hover handler to skip Fx.hover w/o warnings Choropleth.hoverPoints = function() {}; -Choropleth.moduleType = 'trace'; -Choropleth.name = 'choropleth'; -Choropleth.basePlotModule = require('../../plots/geo'); -Choropleth.categories = ['geo', 'noOpacity']; +Choropleth.moduleType = "trace"; +Choropleth.name = "choropleth"; +Choropleth.basePlotModule = require("../../plots/geo"); +Choropleth.categories = ["geo", "noOpacity"]; Choropleth.meta = { - description: [ - 'The data that describes the choropleth value-to-color mapping', - 'is set in `z`.', - 'The geographic locations corresponding to each value in `z`', - 'are set in `locations`.' - ].join(' ') + description: [ + "The data that describes the choropleth value-to-color mapping", + "is set in `z`.", + "The geographic locations corresponding to each value in `z`", + "are set in `locations`." + ].join(" ") }; module.exports = Choropleth; diff --git a/src/traces/choropleth/plot.js b/src/traces/choropleth/plot.js index 86d947a6fce..451b99233a6 100644 --- a/src/traces/choropleth/plot.js +++ b/src/traces/choropleth/plot.js @@ -5,223 +5,224 @@ * 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 d3 = require('d3'); - -var Axes = require('../../plots/cartesian/axes'); -var Fx = require('../../plots/cartesian/graph_interact'); -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); -var Colorscale = require('../../components/colorscale'); - -var getTopojsonFeatures = require('../../lib/topojson_utils').getTopojsonFeatures; -var locationToFeature = require('../../lib/geo_location_utils').locationToFeature; -var arrayToCalcItem = require('../../lib/array_to_calc_item'); - -var constants = require('../../plots/geo/constants'); -var attributes = require('./attributes'); +"use strict"; +var d3 = require("d3"); + +var Axes = require("../../plots/cartesian/axes"); +var Fx = require("../../plots/cartesian/graph_interact"); +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); +var Colorscale = require("../../components/colorscale"); + +var getTopojsonFeatures = require( + "../../lib/topojson_utils" +).getTopojsonFeatures; +var locationToFeature = require( + "../../lib/geo_location_utils" +).locationToFeature; +var arrayToCalcItem = require("../../lib/array_to_calc_item"); + +var constants = require("../../plots/geo/constants"); +var attributes = require("./attributes"); var plotChoropleth = module.exports = {}; - plotChoropleth.calcGeoJSON = function(trace, topojson) { - var cdi = [], - locations = trace.locations, - len = locations.length, - features = getTopojsonFeatures(trace, topojson), - markerLine = (trace.marker || {}).line || {}; + var cdi = [], + locations = trace.locations, + len = locations.length, + features = getTopojsonFeatures(trace, topojson), + markerLine = (trace.marker || {}).line || {}; - var feature; + var feature; - for(var i = 0; i < len; i++) { - feature = locationToFeature(trace.locationmode, locations[i], features); + for (var i = 0; i < len; i++) { + feature = locationToFeature(trace.locationmode, locations[i], features); - if(!feature) continue; // filter the blank features here + if (!feature) continue; - // 'data_array' attributes - feature.z = trace.z[i]; - if(trace.text !== undefined) feature.tx = trace.text[i]; + // filter the blank features here + // 'data_array' attributes + feature.z = trace.z[i]; + if (trace.text !== undefined) feature.tx = trace.text[i]; - // 'arrayOk' attributes - arrayToCalcItem(markerLine.color, feature, 'mlc', i); - arrayToCalcItem(markerLine.width, feature, 'mlw', i); + // 'arrayOk' attributes + arrayToCalcItem(markerLine.color, feature, "mlc", i); + arrayToCalcItem(markerLine.width, feature, "mlw", i); - cdi.push(feature); - } + cdi.push(feature); + } - if(cdi.length > 0) cdi[0].trace = trace; + if (cdi.length > 0) cdi[0].trace = trace; - return cdi; + return cdi; }; plotChoropleth.plot = function(geo, calcData, geoLayout) { + function keyFunc(d) { + return d[0].trace.uid; + } - function keyFunc(d) { return d[0].trace.uid; } - - var framework = geo.framework, - gChoropleth = framework.select('g.choroplethlayer'), - gBaseLayer = framework.select('g.baselayer'), - gBaseLayerOverChoropleth = framework.select('g.baselayeroverchoropleth'), - baseLayersOverChoropleth = constants.baseLayersOverChoropleth, - layerName; - - var gChoroplethTraces = gChoropleth - .selectAll('g.trace.choropleth') - .data(calcData, keyFunc); - - gChoroplethTraces.enter().append('g') - .attr('class', 'trace choropleth'); + var framework = geo.framework, + gChoropleth = framework.select("g.choroplethlayer"), + gBaseLayer = framework.select("g.baselayer"), + gBaseLayerOverChoropleth = framework.select("g.baselayeroverchoropleth"), + baseLayersOverChoropleth = constants.baseLayersOverChoropleth, + layerName; - gChoroplethTraces.exit().remove(); + var gChoroplethTraces = gChoropleth + .selectAll("g.trace.choropleth") + .data(calcData, keyFunc); - gChoroplethTraces.each(function(calcTrace) { - var trace = calcTrace[0].trace, - cdi = plotChoropleth.calcGeoJSON(trace, geo.topojson), - cleanHoverLabelsFunc = makeCleanHoverLabelsFunc(geo, trace), - eventDataFunc = makeEventDataFunc(trace); + gChoroplethTraces.enter().append("g").attr("class", "trace choropleth"); - // keep ref to event data in this scope for plotly_unhover - var eventData = null; + gChoroplethTraces.exit().remove(); - function handleMouseOver(pt, ptIndex) { - if(!geo.showHover) return; + gChoroplethTraces.each(function(calcTrace) { + var trace = calcTrace[0].trace, + cdi = plotChoropleth.calcGeoJSON(trace, geo.topojson), + cleanHoverLabelsFunc = makeCleanHoverLabelsFunc(geo, trace), + eventDataFunc = makeEventDataFunc(trace); - var xy = geo.projection(pt.properties.ct); - cleanHoverLabelsFunc(pt); + // keep ref to event data in this scope for plotly_unhover + var eventData = null; - Fx.loneHover({ - x: xy[0], - y: xy[1], - name: pt.nameLabel, - text: pt.textLabel - }, { - container: geo.hoverContainer.node() - }); + function handleMouseOver(pt, ptIndex) { + if (!geo.showHover) return; - eventData = eventDataFunc(pt, ptIndex); + var xy = geo.projection(pt.properties.ct); + cleanHoverLabelsFunc(pt); - geo.graphDiv.emit('plotly_hover', eventData); - } - - function handleClick(pt, ptIndex) { - geo.graphDiv.emit('plotly_click', eventDataFunc(pt, ptIndex)); - } + Fx.loneHover( + { x: xy[0], y: xy[1], name: pt.nameLabel, text: pt.textLabel }, + { container: geo.hoverContainer.node() } + ); - var paths = d3.select(this).selectAll('path.choroplethlocation') - .data(cdi); - - paths.enter().append('path') - .classed('choroplethlocation', true) - .on('mouseover', handleMouseOver) - .on('click', handleClick) - .on('mouseout', function() { - Fx.loneUnhover(geo.hoverContainer); - - geo.graphDiv.emit('plotly_unhover', eventData); - }) - .on('mousedown', function() { - // to simulate the 'zoomon' event - Fx.loneUnhover(geo.hoverContainer); - }) - .on('mouseup', handleMouseOver); // ~ 'zoomend' - - paths.exit().remove(); - }); + eventData = eventDataFunc(pt, ptIndex); - // some baselayers are drawn over choropleth - gBaseLayerOverChoropleth.selectAll('*').remove(); + geo.graphDiv.emit("plotly_hover", eventData); + } - for(var i = 0; i < baseLayersOverChoropleth.length; i++) { - layerName = baseLayersOverChoropleth[i]; - gBaseLayer.select('g.' + layerName).remove(); - geo.drawTopo(gBaseLayerOverChoropleth, layerName, geoLayout); - geo.styleLayer(gBaseLayerOverChoropleth, layerName, geoLayout); + function handleClick(pt, ptIndex) { + geo.graphDiv.emit("plotly_click", eventDataFunc(pt, ptIndex)); } - plotChoropleth.style(geo); + var paths = d3.select(this).selectAll("path.choroplethlocation").data(cdi); + + paths + .enter() + .append("path") + .classed("choroplethlocation", true) + .on("mouseover", handleMouseOver) + .on("click", handleClick) + .on("mouseout", function() { + Fx.loneUnhover(geo.hoverContainer); + + geo.graphDiv.emit("plotly_unhover", eventData); + }) + .on("mousedown", function() { + // to simulate the 'zoomon' event + Fx.loneUnhover(geo.hoverContainer); + }) + .on("mouseup", handleMouseOver); + + // ~ 'zoomend' + paths.exit().remove(); + }); + + // some baselayers are drawn over choropleth + gBaseLayerOverChoropleth.selectAll("*").remove(); + + for (var i = 0; i < baseLayersOverChoropleth.length; i++) { + layerName = baseLayersOverChoropleth[i]; + gBaseLayer.select("g." + layerName).remove(); + geo.drawTopo(gBaseLayerOverChoropleth, layerName, geoLayout); + geo.styleLayer(gBaseLayerOverChoropleth, layerName, geoLayout); + } + + plotChoropleth.style(geo); }; plotChoropleth.style = function(geo) { - geo.framework.selectAll('g.trace.choropleth') - .each(function(calcTrace) { - var trace = calcTrace[0].trace, - s = d3.select(this), - marker = trace.marker || {}, - markerLine = marker.line || {}; - - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - trace.colorscale, - trace.zmin, - trace.zmax - ) - ); - - s.selectAll('path.choroplethlocation') - .each(function(pt) { - d3.select(this) - .attr('fill', function(pt) { return sclFunc(pt.z); }) - .call(Color.stroke, pt.mlc || markerLine.color) - .call(Drawing.dashLine, '', pt.mlw || markerLine.width || 0); - }); - }); + geo.framework.selectAll("g.trace.choropleth").each(function(calcTrace) { + var trace = calcTrace[0].trace, + s = d3.select(this), + marker = trace.marker || {}, + markerLine = marker.line || {}; + + var sclFunc = Colorscale.makeColorScaleFunc( + Colorscale.extractScale(trace.colorscale, trace.zmin, trace.zmax) + ); + + s.selectAll("path.choroplethlocation").each(function(pt) { + d3 + .select(this) + .attr("fill", function(pt) { + return sclFunc(pt.z); + }) + .call(Color.stroke, pt.mlc || markerLine.color) + .call(Drawing.dashLine, "", pt.mlw || markerLine.width || 0); + }); + }); }; function makeCleanHoverLabelsFunc(geo, trace) { - var hoverinfo = trace.hoverinfo; - - if(hoverinfo === 'none' || hoverinfo === 'skip') { - return function cleanHoverLabelsFunc(pt) { - delete pt.nameLabel; - delete pt.textLabel; - }; - } - - var hoverinfoParts = (hoverinfo === 'all') ? - attributes.hoverinfo.flags : - hoverinfo.split('+'); - - var hasName = (hoverinfoParts.indexOf('name') !== -1), - hasLocation = (hoverinfoParts.indexOf('location') !== -1), - hasZ = (hoverinfoParts.indexOf('z') !== -1), - hasText = (hoverinfoParts.indexOf('text') !== -1), - hasIdAsNameLabel = !hasName && hasLocation; - - function formatter(val) { - var axis = geo.mockAxis; - return Axes.tickText(axis, axis.c2l(val), 'hover').text; - } + var hoverinfo = trace.hoverinfo; + if (hoverinfo === "none" || hoverinfo === "skip") { return function cleanHoverLabelsFunc(pt) { - // put location id in name label container - // if name isn't part of hoverinfo - var thisText = []; - - if(hasIdAsNameLabel) pt.nameLabel = pt.id; - else { - if(hasName) pt.nameLabel = trace.name; - if(hasLocation) thisText.push(pt.id); - } + delete pt.nameLabel; + delete pt.textLabel; + }; + } + + var hoverinfoParts = hoverinfo === "all" + ? attributes.hoverinfo.flags + : hoverinfo.split("+"); + + var hasName = hoverinfoParts.indexOf("name") !== -1, + hasLocation = hoverinfoParts.indexOf("location") !== -1, + hasZ = hoverinfoParts.indexOf("z") !== -1, + hasText = hoverinfoParts.indexOf("text") !== -1, + hasIdAsNameLabel = !hasName && hasLocation; + + function formatter(val) { + var axis = geo.mockAxis; + return Axes.tickText(axis, axis.c2l(val), "hover").text; + } + + return function cleanHoverLabelsFunc(pt) { + // put location id in name label container + // if name isn't part of hoverinfo + var thisText = []; + + if (hasIdAsNameLabel) { + pt.nameLabel = pt.id; + } else { + if (hasName) pt.nameLabel = trace.name; + if (hasLocation) thisText.push(pt.id); + } - if(hasZ) thisText.push(formatter(pt.z)); - if(hasText) thisText.push(pt.tx); + if (hasZ) thisText.push(formatter(pt.z)); + if (hasText) thisText.push(pt.tx); - pt.textLabel = thisText.join('
'); - }; + pt.textLabel = thisText.join("
"); + }; } function makeEventDataFunc(trace) { - return function(pt, ptIndex) { - return {points: [{ - data: trace._input, - fullData: trace, - curveNumber: trace.index, - pointNumber: ptIndex, - location: pt.id, - z: pt.z - }]}; + return function(pt, ptIndex) { + return { + points: [ + { + data: trace._input, + fullData: trace, + curveNumber: trace.index, + pointNumber: ptIndex, + location: pt.id, + z: pt.z + } + ] }; + }; } diff --git a/src/traces/contour/attributes.js b/src/traces/contour/attributes.js index ee547aa5dbe..ffac49cd503 100644 --- a/src/traces/contour/attributes.js +++ b/src/traces/contour/attributes.js @@ -5,18 +5,18 @@ * 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 heatmapAttrs = require('../heatmap/attributes'); -var scatterAttrs = require('../scatter/attributes'); -var colorscaleAttrs = require('../../components/colorscale/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); -var extendFlat = require('../../lib/extend').extendFlat; +"use strict"; +var heatmapAttrs = require("../heatmap/attributes"); +var scatterAttrs = require("../scatter/attributes"); +var colorscaleAttrs = require("../../components/colorscale/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); +var extendFlat = require("../../lib/extend").extendFlat; var scatterLineAttrs = scatterAttrs.line; -module.exports = extendFlat({}, { +module.exports = extendFlat( + {}, + { z: heatmapAttrs.z, x: heatmapAttrs.x, x0: heatmapAttrs.x0, @@ -28,106 +28,106 @@ module.exports = extendFlat({}, { transpose: heatmapAttrs.transpose, xtype: heatmapAttrs.xtype, ytype: heatmapAttrs.ytype, - connectgaps: heatmapAttrs.connectgaps, - autocontour: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Determines whether or not the contour level attributes are', - 'picked by an algorithm.', - 'If *true*, the number of contour levels can be set in `ncontours`.', - 'If *false*, set the contour level attributes in `contours`.' - ].join(' ') + valType: "boolean", + dflt: true, + role: "style", + description: [ + "Determines whether or not the contour level attributes are", + "picked by an algorithm.", + "If *true*, the number of contour levels can be set in `ncontours`.", + "If *false*, set the contour level attributes in `contours`." + ].join(" ") }, ncontours: { - valType: 'integer', - dflt: 15, - min: 1, - role: 'style', - description: [ - 'Sets the maximum number of contour levels. The actual number', - 'of contours will be chosen automatically to be less than or', - 'equal to the value of `ncontours`.', - 'Has an effect only if `autocontour` is *true* or if', - '`contours.size` is missing.' - ].join(' ') + valType: "integer", + dflt: 15, + min: 1, + role: "style", + description: [ + "Sets the maximum number of contour levels. The actual number", + "of contours will be chosen automatically to be less than or", + "equal to the value of `ncontours`.", + "Has an effect only if `autocontour` is *true* or if", + "`contours.size` is missing." + ].join(" ") }, - contours: { - start: { - valType: 'number', - dflt: null, - role: 'style', - description: [ - 'Sets the starting contour level value.', - 'Must be less than `contours.end`' - ].join(' ') - }, - end: { - valType: 'number', - dflt: null, - role: 'style', - description: [ - 'Sets the end contour level value.', - 'Must be more than `contours.start`' - ].join(' ') - }, - size: { - valType: 'number', - dflt: null, - min: 0, - role: 'style', - description: [ - 'Sets the step between each contour level.', - 'Must be positive.' - ].join(' ') - }, - coloring: { - valType: 'enumerated', - values: ['fill', 'heatmap', 'lines', 'none'], - dflt: 'fill', - role: 'style', - description: [ - 'Determines the coloring method showing the contour values.', - 'If *fill*, coloring is done evenly between each contour level', - 'If *heatmap*, a heatmap gradient coloring is applied', - 'between each contour level.', - 'If *lines*, coloring is done on the contour lines.', - 'If *none*, no coloring is applied on this trace.' - ].join(' ') - }, - showlines: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Determines whether or not the contour lines are drawn.', - 'Has only an effect if `contours.coloring` is set to *fill*.' - ].join(' ') - } + start: { + valType: "number", + dflt: null, + role: "style", + description: [ + "Sets the starting contour level value.", + "Must be less than `contours.end`" + ].join(" ") + }, + end: { + valType: "number", + dflt: null, + role: "style", + description: [ + "Sets the end contour level value.", + "Must be more than `contours.start`" + ].join(" ") + }, + size: { + valType: "number", + dflt: null, + min: 0, + role: "style", + description: [ + "Sets the step between each contour level.", + "Must be positive." + ].join(" ") + }, + coloring: { + valType: "enumerated", + values: ["fill", "heatmap", "lines", "none"], + dflt: "fill", + role: "style", + description: [ + "Determines the coloring method showing the contour values.", + "If *fill*, coloring is done evenly between each contour level", + "If *heatmap*, a heatmap gradient coloring is applied", + "between each contour level.", + "If *lines*, coloring is done on the contour lines.", + "If *none*, no coloring is applied on this trace." + ].join(" ") + }, + showlines: { + valType: "boolean", + dflt: true, + role: "style", + description: [ + "Determines whether or not the contour lines are drawn.", + "Has only an effect if `contours.coloring` is set to *fill*." + ].join(" ") + } }, - line: { - color: extendFlat({}, scatterLineAttrs.color, { - description: [ - 'Sets the color of the contour level.', - 'Has no if `contours.coloring` is set to *lines*.' - ].join(' ') - }), - width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash, - smoothing: extendFlat({}, scatterLineAttrs.smoothing, { - description: [ - 'Sets the amount of smoothing for the contour lines,', - 'where *0* corresponds to no smoothing.' - ].join(' ') - }) + color: extendFlat({}, scatterLineAttrs.color, { + description: [ + "Sets the color of the contour level.", + "Has no if `contours.coloring` is set to *lines*." + ].join(" ") + }), + width: scatterLineAttrs.width, + dash: scatterLineAttrs.dash, + smoothing: extendFlat({}, scatterLineAttrs.smoothing, { + description: [ + "Sets the amount of smoothing for the contour lines,", + "where *0* corresponds to no smoothing." + ].join(" ") + }) } -}, - colorscaleAttrs, - { autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, {dflt: false}) }, - { colorbar: colorbarAttrs } + }, + colorscaleAttrs, + { + autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, { + dflt: false + }) + }, + { colorbar: colorbarAttrs } ); diff --git a/src/traces/contour/calc.js b/src/traces/contour/calc.js index 07e86c2f2fc..74b57313f02 100644 --- a/src/traces/contour/calc.js +++ b/src/traces/contour/calc.js @@ -5,75 +5,69 @@ * 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 Axes = require('../../plots/cartesian/axes'); -var extendFlat = require('../../lib').extendFlat; -var heatmapCalc = require('../heatmap/calc'); - +"use strict"; +var Axes = require("../../plots/cartesian/axes"); +var extendFlat = require("../../lib").extendFlat; +var heatmapCalc = require("../heatmap/calc"); // most is the same as heatmap calc, then adjust it // though a few things inside heatmap calc still look for // contour maps, because the makeBoundArray calls are too entangled module.exports = function calc(gd, trace) { - var cd = heatmapCalc(gd, trace), - contours = trace.contours; - - // check if we need to auto-choose contour levels - if(trace.autocontour !== false) { - var dummyAx = autoContours(trace.zmin, trace.zmax, trace.ncontours); + var cd = heatmapCalc(gd, trace), contours = trace.contours; - contours.size = dummyAx.dtick; + // check if we need to auto-choose contour levels + if (trace.autocontour !== false) { + var dummyAx = autoContours(trace.zmin, trace.zmax, trace.ncontours); - contours.start = Axes.tickFirst(dummyAx); - dummyAx.range.reverse(); - contours.end = Axes.tickFirst(dummyAx); + contours.size = dummyAx.dtick; - if(contours.start === trace.zmin) contours.start += contours.size; - if(contours.end === trace.zmax) contours.end -= contours.size; + contours.start = Axes.tickFirst(dummyAx); + dummyAx.range.reverse(); + contours.end = Axes.tickFirst(dummyAx); - // if you set a small ncontours, *and* the ends are exactly on zmin/zmax - // there's an edge case where start > end now. Make sure there's at least - // one meaningful contour, put it midway between the crossed values - if(contours.start > contours.end) { - contours.start = contours.end = (contours.start + contours.end) / 2; - } + if (contours.start === trace.zmin) contours.start += contours.size; + if (contours.end === trace.zmax) contours.end -= contours.size; - // copy auto-contour info back to the source data. - // previously we copied the whole contours object back, but that had - // other info (coloring, showlines) that should be left to supplyDefaults - if(!trace._input.contours) trace._input.contours = {}; - extendFlat(trace._input.contours, { - start: contours.start, - end: contours.end, - size: contours.size - }); - trace._input.autocontour = true; + // if you set a small ncontours, *and* the ends are exactly on zmin/zmax + // there's an edge case where start > end now. Make sure there's at least + // one meaningful contour, put it midway between the crossed values + if (contours.start > contours.end) { + contours.start = contours.end = (contours.start + contours.end) / 2; } - else { - // sanity checks on manually-supplied start/end/size - var start = contours.start, - end = contours.end, - inputContours = trace._input.contours; - if(start > end) { - contours.start = inputContours.start = end; - end = contours.end = inputContours.end = start; - start = contours.start; - } + // copy auto-contour info back to the source data. + // previously we copied the whole contours object back, but that had + // other info (coloring, showlines) that should be left to supplyDefaults + if (!trace._input.contours) trace._input.contours = {}; + extendFlat(trace._input.contours, { + start: contours.start, + end: contours.end, + size: contours.size + }); + trace._input.autocontour = true; + } else { + // sanity checks on manually-supplied start/end/size + var start = contours.start, + end = contours.end, + inputContours = trace._input.contours; + + if (start > end) { + contours.start = inputContours.start = end; + end = contours.end = inputContours.end = start; + start = contours.start; + } - if(!(contours.size > 0)) { - var sizeOut; - if(start === end) sizeOut = 1; - else sizeOut = autoContours(start, end, trace.ncontours).dtick; + if (!(contours.size > 0)) { + var sizeOut; + if (start === end) sizeOut = 1; + else sizeOut = autoContours(start, end, trace.ncontours).dtick; - inputContours.size = contours.size = sizeOut; - } + inputContours.size = contours.size = sizeOut; } + } - return cd; + return cd; }; /* @@ -88,15 +82,9 @@ module.exports = function calc(gd, trace) { * returns: an axis object */ function autoContours(start, end, ncontours) { - var dummyAx = { - type: 'linear', - range: [start, end] - }; + var dummyAx = { type: "linear", range: [start, end] }; - Axes.autoTicks( - dummyAx, - (end - start) / (ncontours || 15) - ); + Axes.autoTicks(dummyAx, (end - start) / (ncontours || 15)); - return dummyAx; + return dummyAx; } diff --git a/src/traces/contour/colorbar.js b/src/traces/contour/colorbar.js index 68be46e0234..fb729f7f941 100644 --- a/src/traces/contour/colorbar.js +++ b/src/traces/contour/colorbar.js @@ -5,56 +5,48 @@ * 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 Plots = require("../../plots/plots"); +var drawColorbar = require("../../components/colorbar/draw"); - -'use strict'; - -var Plots = require('../../plots/plots'); -var drawColorbar = require('../../components/colorbar/draw'); - -var makeColorMap = require('./make_color_map'); -var endPlus = require('./end_plus'); - +var makeColorMap = require("./make_color_map"); +var endPlus = require("./end_plus"); module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - cbId = 'cb' + trace.uid; - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - if(trace.showscale === false) { - Plots.autoMargin(gd, cbId); - return; - } - - var cb = drawColorbar(gd, cbId); - cd[0].t.cb = cb; - - var contours = trace.contours, - line = trace.line, - cs = contours.size || 1, - coloring = contours.coloring; - - var colorMap = makeColorMap(trace, {isColorbar: true}); - - if(coloring === 'heatmap') { - cb.filllevels({ - start: trace.zmin, - end: trace.zmax, - size: (trace.zmax - trace.zmin) / 254 - }); - } - - cb.fillcolor((coloring === 'fill' || coloring === 'heatmap') ? colorMap : '') - .line({ - color: coloring === 'lines' ? colorMap : line.color, - width: contours.showlines !== false ? line.width : 0, - dash: line.dash - }) - .levels({ - start: contours.start, - end: endPlus(contours), - size: cs - }) - .options(trace.colorbar)(); + var trace = cd[0].trace, cbId = "cb" + trace.uid; + + gd._fullLayout._infolayer.selectAll("." + cbId).remove(); + + if (trace.showscale === false) { + Plots.autoMargin(gd, cbId); + return; + } + + var cb = drawColorbar(gd, cbId); + cd[0].t.cb = cb; + + var contours = trace.contours, + line = trace.line, + cs = contours.size || 1, + coloring = contours.coloring; + + var colorMap = makeColorMap(trace, { isColorbar: true }); + + if (coloring === "heatmap") { + cb.filllevels({ + start: trace.zmin, + end: trace.zmax, + size: (trace.zmax - trace.zmin) / 254 + }); + } + + cb + .fillcolor(coloring === "fill" || coloring === "heatmap" ? colorMap : "") + .line({ + color: coloring === "lines" ? colorMap : line.color, + width: contours.showlines !== false ? line.width : 0, + dash: line.dash + }) + .levels({ start: contours.start, end: endPlus(contours), size: cs }) + .options(trace.colorbar)(); }; diff --git a/src/traces/contour/constants.js b/src/traces/contour/constants.js index 406c4057804..c53cc931d80 100644 --- a/src/traces/contour/constants.js +++ b/src/traces/contour/constants.js @@ -5,9 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; // some constants to help with marching squares algorithm // where does the path start for each index? module.exports.BOTTOMSTART = [1, 9, 13, 104, 713]; @@ -18,21 +16,41 @@ module.exports.RIGHTSTART = [2, 3, 11, 208, 1114]; // which way [dx,dy] do we leave a given index? // saddles are already disambiguated module.exports.NEWDELTA = [ - null, [-1, 0], [0, -1], [-1, 0], - [1, 0], null, [0, -1], [-1, 0], - [0, 1], [0, 1], null, [0, 1], - [1, 0], [1, 0], [0, -1] + null, + [-1, 0], + [0, -1], + [-1, 0], + [1, 0], + null, + [0, -1], + [-1, 0], + [0, 1], + [0, 1], + null, + [0, 1], + [1, 0], + [1, 0], + [0, -1] ]; // for each saddle, the first index here is used // for dx||dy<0, the second for dx||dy>0 module.exports.CHOOSESADDLE = { - 104: [4, 1], - 208: [2, 8], - 713: [7, 13], - 1114: [11, 14] + 104: [4, 1], + 208: [2, 8], + 713: [7, 13], + 1114: [11, 14] }; // after one index has been used for a saddle, which do we // substitute to be used up later? -module.exports.SADDLEREMAINDER = {1: 4, 2: 8, 4: 1, 7: 13, 8: 2, 11: 14, 13: 7, 14: 11}; +module.exports.SADDLEREMAINDER = { + 1: 4, + 2: 8, + 4: 1, + 7: 13, + 8: 2, + 11: 14, + 13: 7, + 14: 11 +}; diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js index 04b9debba70..2c9bdb6b7c1 100644 --- a/src/traces/contour/defaults.js +++ b/src/traces/contour/defaults.js @@ -5,47 +5,52 @@ * 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 hasColumns = require('../heatmap/has_columns'); -var handleXYZDefaults = require('../heatmap/xyz_defaults'); -var handleStyleDefaults = require('../contour/style_defaults'); -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); - if(!len) { - traceOut.visible = false; - return; - } - - coerce('text'); - coerce('connectgaps', hasColumns(traceOut)); - - var contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'), - contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'), - missingEnd = (contourStart === false) || (contourEnd === false), - - // normally we only need size if autocontour is off. But contour.calc - // pushes its calculated contour size back to the input trace, so for - // things like restyle that can call supplyDefaults without calc - // after the initial draw, we can just reuse the previous calculation - contourSize = coerce('contours.size'), - autoContour; - - if(missingEnd) autoContour = traceOut.autocontour = true; - else autoContour = coerce('autocontour', false); - - if(autoContour || !contourSize) coerce('ncontours'); - - handleStyleDefaults(traceIn, traceOut, coerce, layout); +"use strict"; +var Lib = require("../../lib"); + +var hasColumns = require("../heatmap/has_columns"); +var handleXYZDefaults = require("../heatmap/xyz_defaults"); +var handleStyleDefaults = require("../contour/style_defaults"); +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); + if (!len) { + traceOut.visible = false; + return; + } + + coerce("text"); + coerce("connectgaps", hasColumns(traceOut)); + + var contourStart = Lib.coerce2( + traceIn, + traceOut, + attributes, + "contours.start" + ), + contourEnd = Lib.coerce2(traceIn, traceOut, attributes, "contours.end"), + missingEnd = contourStart === false || contourEnd === false, + // normally we only need size if autocontour is off. But contour.calc + // pushes its calculated contour size back to the input trace, so for + // things like restyle that can call supplyDefaults without calc + // after the initial draw, we can just reuse the previous calculation + contourSize = coerce("contours.size"), + autoContour; + + if (missingEnd) autoContour = traceOut.autocontour = true; + else autoContour = coerce("autocontour", false); + + if (autoContour || !contourSize) coerce("ncontours"); + + handleStyleDefaults(traceIn, traceOut, coerce, layout); }; diff --git a/src/traces/contour/end_plus.js b/src/traces/contour/end_plus.js index 8b8e9dc3f65..7a439709bfb 100644 --- a/src/traces/contour/end_plus.js +++ b/src/traces/contour/end_plus.js @@ -5,14 +5,11 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; /* * tiny helper to move the end of the contours a little to prevent * losing the last contour to rounding errors */ module.exports = function endPlus(contours) { - return contours.end + contours.size / 1e6; + return contours.end + contours.size / 1e6; }; diff --git a/src/traces/contour/find_all_paths.js b/src/traces/contour/find_all_paths.js index 273368094e2..0ad8a69c27d 100644 --- a/src/traces/contour/find_all_paths.js +++ b/src/traces/contour/find_all_paths.js @@ -5,264 +5,273 @@ * 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 constants = require('./constants'); +"use strict"; +var Lib = require("../../lib"); +var constants = require("./constants"); module.exports = function findAllPaths(pathinfo) { - var cnt, - startLoc, - i, - pi, - j; + var cnt, startLoc, i, pi, j; - for(i = 0; i < pathinfo.length; i++) { - pi = pathinfo[i]; + for (i = 0; i < pathinfo.length; i++) { + pi = pathinfo[i]; - for(j = 0; j < pi.starts.length; j++) { - startLoc = pi.starts[j]; - makePath(pi, startLoc, 'edge'); - } + for (j = 0; j < pi.starts.length; j++) { + startLoc = pi.starts[j]; + makePath(pi, startLoc, "edge"); + } - cnt = 0; - while(Object.keys(pi.crossings).length && cnt < 10000) { - cnt++; - startLoc = Object.keys(pi.crossings)[0].split(',').map(Number); - makePath(pi, startLoc); - } - if(cnt === 10000) Lib.log('Infinite loop in contour?'); + cnt = 0; + while (Object.keys(pi.crossings).length && cnt < 10000) { + cnt++; + startLoc = Object.keys(pi.crossings)[0].split(",").map(Number); + makePath(pi, startLoc); } + if (cnt === 10000) Lib.log("Infinite loop in contour?"); + } }; function equalPts(pt1, pt2) { - return Math.abs(pt1[0] - pt2[0]) < 0.01 && - Math.abs(pt1[1] - pt2[1]) < 0.01; + return Math.abs(pt1[0] - pt2[0]) < 0.01 && Math.abs(pt1[1] - pt2[1]) < 0.01; } function ptDist(pt1, pt2) { - var dx = pt1[0] - pt2[0], - dy = pt1[1] - pt2[1]; - return Math.sqrt(dx * dx + dy * dy); + var dx = pt1[0] - pt2[0], dy = pt1[1] - pt2[1]; + return Math.sqrt(dx * dx + dy * dy); } function makePath(pi, loc, edgeflag) { - var startLocStr = loc.join(','), - locStr = startLocStr, - mi = pi.crossings[locStr], - marchStep = startStep(mi, edgeflag, loc), - // start by going backward a half step and finding the crossing point - pts = [getInterpPx(pi, loc, [-marchStep[0], -marchStep[1]])], - startStepStr = marchStep.join(','), - m = pi.z.length, - n = pi.z[0].length, - cnt; - - // now follow the path - for(cnt = 0; cnt < 10000; cnt++) { // just to avoid infinite loops - if(mi > 20) { - mi = constants.CHOOSESADDLE[mi][(marchStep[0] || marchStep[1]) < 0 ? 0 : 1]; - pi.crossings[locStr] = constants.SADDLEREMAINDER[mi]; - } - else { - delete pi.crossings[locStr]; - } - - marchStep = constants.NEWDELTA[mi]; - if(!marchStep) { - Lib.log('Found bad marching index:', mi, loc, pi.level); - break; - } - - // find the crossing a half step forward, and then take the full step - pts.push(getInterpPx(pi, loc, marchStep)); - loc[0] += marchStep[0]; - loc[1] += marchStep[1]; - - // don't include the same point multiple times - if(equalPts(pts[pts.length - 1], pts[pts.length - 2])) pts.pop(); - locStr = loc.join(','); - - var atEdge = (marchStep[0] && (loc[0] < 0 || loc[0] > n - 2)) || - (marchStep[1] && (loc[1] < 0 || loc[1] > m - 2)), - closedLoop = (locStr === startLocStr) && (marchStep.join(',') === startStepStr); - - // have we completed a loop, or reached an edge? - if((closedLoop) || (edgeflag && atEdge)) break; - - mi = pi.crossings[locStr]; - } - - if(cnt === 10000) { - Lib.log('Infinite loop in contour?'); + var startLocStr = loc.join(","), + locStr = startLocStr, + mi = pi.crossings[locStr], + marchStep = startStep(mi, edgeflag, loc), + // start by going backward a half step and finding the crossing point + pts = [getInterpPx(pi, loc, [-marchStep[0], -marchStep[1]])], + startStepStr = marchStep.join(","), + m = pi.z.length, + n = pi.z[0].length, + cnt; + + // now follow the path + for (cnt = 0; cnt < 10000; cnt++) { + // just to avoid infinite loops + if (mi > 20) { + mi = constants.CHOOSESADDLE[mi][ + (marchStep[0] || marchStep[1]) < 0 ? 0 : 1 + ]; + pi.crossings[locStr] = constants.SADDLEREMAINDER[mi]; + } else { + delete pi.crossings[locStr]; } - var closedpath = equalPts(pts[0], pts[pts.length - 1]), - totaldist = 0, - distThresholdFactor = 0.2 * pi.smoothing, - alldists = [], - cropstart = 0, - distgroup, - cnt2, - cnt3, - newpt, - ptcnt, - ptavg, - thisdist; - // check for points that are too close together (<1/5 the average dist, - // less if less smoothed) and just take the center (or avg of center 2) - // this cuts down on funny behavior when a point is very close to a contour level - for(cnt = 1; cnt < pts.length; cnt++) { - thisdist = ptDist(pts[cnt], pts[cnt - 1]); - totaldist += thisdist; - alldists.push(thisdist); + marchStep = constants.NEWDELTA[mi]; + if (!marchStep) { + Lib.log("Found bad marching index:", mi, loc, pi.level); + break; } - var distThreshold = totaldist / alldists.length * distThresholdFactor; - - function getpt(i) { return pts[i % pts.length]; } - - for(cnt = pts.length - 2; cnt >= cropstart; cnt--) { - distgroup = alldists[cnt]; - if(distgroup < distThreshold) { - cnt3 = 0; - for(cnt2 = cnt - 1; cnt2 >= cropstart; cnt2--) { - if(distgroup + alldists[cnt2] < distThreshold) { - distgroup += alldists[cnt2]; - } - else break; - } - - // closed path with close points wrapping around the boundary? - if(closedpath && cnt === pts.length - 2) { - for(cnt3 = 0; cnt3 < cnt2; cnt3++) { - if(distgroup + alldists[cnt3] < distThreshold) { - distgroup += alldists[cnt3]; - } - else break; - } - } - ptcnt = cnt - cnt2 + cnt3 + 1; - ptavg = Math.floor((cnt + cnt2 + cnt3 + 2) / 2); - - // either endpoint included: keep the endpoint - if(!closedpath && cnt === pts.length - 2) newpt = pts[pts.length - 1]; - else if(!closedpath && cnt2 === -1) newpt = pts[0]; - - // odd # of points - just take the central one - else if(ptcnt % 2) newpt = getpt(ptavg); - - // even # of pts - average central two - else { - newpt = [(getpt(ptavg)[0] + getpt(ptavg + 1)[0]) / 2, - (getpt(ptavg)[1] + getpt(ptavg + 1)[1]) / 2]; - } - - pts.splice(cnt2 + 1, cnt - cnt2 + 1, newpt); - cnt = cnt2 + 1; - if(cnt3) cropstart = cnt3; - if(closedpath) { - if(cnt === pts.length - 2) pts[cnt3] = pts[pts.length - 1]; - else if(cnt === 0) pts[pts.length - 1] = pts[0]; - } + // find the crossing a half step forward, and then take the full step + pts.push(getInterpPx(pi, loc, marchStep)); + loc[0] += marchStep[0]; + loc[1] += marchStep[1]; + + // don't include the same point multiple times + if (equalPts(pts[pts.length - 1], pts[pts.length - 2])) pts.pop(); + locStr = loc.join(","); + + var atEdge = marchStep[0] && (loc[0] < 0 || loc[0] > n - 2) || + marchStep[1] && (loc[1] < 0 || loc[1] > m - 2), + closedLoop = locStr === startLocStr && + marchStep.join(",") === startStepStr; + + // have we completed a loop, or reached an edge? + if (closedLoop || edgeflag && atEdge) break; + + mi = pi.crossings[locStr]; + } + + if (cnt === 10000) { + Lib.log("Infinite loop in contour?"); + } + var closedpath = equalPts(pts[0], pts[pts.length - 1]), + totaldist = 0, + distThresholdFactor = 0.2 * pi.smoothing, + alldists = [], + cropstart = 0, + distgroup, + cnt2, + cnt3, + newpt, + ptcnt, + ptavg, + thisdist; + + // check for points that are too close together (<1/5 the average dist, + // less if less smoothed) and just take the center (or avg of center 2) + // this cuts down on funny behavior when a point is very close to a contour level + for (cnt = 1; cnt < pts.length; cnt++) { + thisdist = ptDist(pts[cnt], pts[cnt - 1]); + totaldist += thisdist; + alldists.push(thisdist); + } + + var distThreshold = totaldist / alldists.length * distThresholdFactor; + + function getpt(i) { + return pts[i % pts.length]; + } + + for (cnt = pts.length - 2; cnt >= cropstart; cnt--) { + distgroup = alldists[cnt]; + if (distgroup < distThreshold) { + cnt3 = 0; + for (cnt2 = cnt - 1; cnt2 >= cropstart; cnt2--) { + if (distgroup + alldists[cnt2] < distThreshold) { + distgroup += alldists[cnt2]; + } else { + break; } + } + + // closed path with close points wrapping around the boundary? + if (closedpath && cnt === pts.length - 2) { + for (cnt3 = 0; cnt3 < cnt2; cnt3++) { + if (distgroup + alldists[cnt3] < distThreshold) { + distgroup += alldists[cnt3]; + } else { + break; + } + } + } + ptcnt = cnt - cnt2 + cnt3 + 1; + ptavg = Math.floor((cnt + cnt2 + cnt3 + 2) / 2); + + // either endpoint included: keep the endpoint + if (!closedpath && cnt === pts.length - 2) { + newpt = pts[pts.length - 1]; + } else if (!closedpath && cnt2 === -1) { + newpt = pts[0]; + } else if ( + ptcnt % 2 // odd # of points - just take the central one + ) { + newpt = getpt(ptavg); + } else { + // even # of pts - average central two + newpt = [ + (getpt(ptavg)[0] + getpt(ptavg + 1)[0]) / 2, + (getpt(ptavg)[1] + getpt(ptavg + 1)[1]) / 2 + ]; + } + + pts.splice(cnt2 + 1, cnt - cnt2 + 1, newpt); + cnt = cnt2 + 1; + if (cnt3) cropstart = cnt3; + if (closedpath) { + if (cnt === pts.length - 2) pts[cnt3] = pts[pts.length - 1]; + else if (cnt === 0) pts[pts.length - 1] = pts[0]; + } } - pts.splice(0, cropstart); - - // don't return single-point paths (ie all points were the same - // so they got deleted?) - if(pts.length < 2) return; - else if(closedpath) { - pts.pop(); - pi.paths.push(pts); + } + pts.splice(0, cropstart); + + // don't return single-point paths (ie all points were the same + // so they got deleted?) + if (pts.length < 2) { + return; + } else if (closedpath) { + pts.pop(); + pi.paths.push(pts); + } else { + if (!edgeflag) { + Lib.log( + "Unclosed interior contour?", + pi.level, + startLocStr, + pts.join("L") + ); } - else { - if(!edgeflag) { - Lib.log('Unclosed interior contour?', - pi.level, startLocStr, pts.join('L')); - } - - // edge path - does it start where an existing edge path ends, or vice versa? - var merged = false; - pi.edgepaths.forEach(function(edgepath, edgei) { - if(!merged && equalPts(edgepath[0], pts[pts.length - 1])) { - pts.pop(); - merged = true; - // now does it ALSO meet the end of another (or the same) path? - var doublemerged = false; - pi.edgepaths.forEach(function(edgepath2, edgei2) { - if(!doublemerged && equalPts( - edgepath2[edgepath2.length - 1], pts[0])) { - doublemerged = true; - pts.splice(0, 1); - pi.edgepaths.splice(edgei, 1); - if(edgei2 === edgei) { - // the path is now closed - pi.paths.push(pts.concat(edgepath2)); - } - else { - pi.edgepaths[edgei2] = - pi.edgepaths[edgei2].concat(pts, edgepath2); - } - } - }); - if(!doublemerged) { - pi.edgepaths[edgei] = pts.concat(edgepath); - } - } - }); - pi.edgepaths.forEach(function(edgepath, edgei) { - if(!merged && equalPts(edgepath[edgepath.length - 1], pts[0])) { - pts.splice(0, 1); - pi.edgepaths[edgei] = edgepath.concat(pts); - merged = true; + // edge path - does it start where an existing edge path ends, or vice versa? + var merged = false; + pi.edgepaths.forEach(function(edgepath, edgei) { + if (!merged && equalPts(edgepath[0], pts[pts.length - 1])) { + pts.pop(); + merged = true; + + // now does it ALSO meet the end of another (or the same) path? + var doublemerged = false; + pi.edgepaths.forEach(function(edgepath2, edgei2) { + if ( + !doublemerged && equalPts(edgepath2[edgepath2.length - 1], pts[0]) + ) { + doublemerged = true; + pts.splice(0, 1); + pi.edgepaths.splice(edgei, 1); + if (edgei2 === edgei) { + // the path is now closed + pi.paths.push(pts.concat(edgepath2)); + } else { + pi.edgepaths[edgei2] = pi.edgepaths[edgei2].concat( + pts, + edgepath2 + ); } + } }); - - if(!merged) pi.edgepaths.push(pts); - } + if (!doublemerged) { + pi.edgepaths[edgei] = pts.concat(edgepath); + } + } + }); + pi.edgepaths.forEach(function(edgepath, edgei) { + if (!merged && equalPts(edgepath[edgepath.length - 1], pts[0])) { + pts.splice(0, 1); + pi.edgepaths[edgei] = edgepath.concat(pts); + merged = true; + } + }); + + if (!merged) pi.edgepaths.push(pts); + } } // special function to get the marching step of the // first point in the path (leading to loc) function startStep(mi, edgeflag, loc) { - var dx = 0, - dy = 0; - if(mi > 20 && edgeflag) { - // these saddles start at +/- x - if(mi === 208 || mi === 1114) { - // if we're starting at the left side, we must be going right - dx = loc[0] === 0 ? 1 : -1; - } - else { - // if we're starting at the bottom, we must be going up - dy = loc[1] === 0 ? 1 : -1; - } + var dx = 0, dy = 0; + if (mi > 20 && edgeflag) { + // these saddles start at +/- x + if (mi === 208 || mi === 1114) { + // if we're starting at the left side, we must be going right + dx = loc[0] === 0 ? 1 : -1; + } else { + // if we're starting at the bottom, we must be going up + dy = loc[1] === 0 ? 1 : -1; } - else if(constants.BOTTOMSTART.indexOf(mi) !== -1) dy = 1; - else if(constants.LEFTSTART.indexOf(mi) !== -1) dx = 1; - else if(constants.TOPSTART.indexOf(mi) !== -1) dy = -1; - else dx = -1; - return [dx, dy]; + } else if (constants.BOTTOMSTART.indexOf(mi) !== -1) dy = 1; + else if (constants.LEFTSTART.indexOf(mi) !== -1) dx = 1; + else if (constants.TOPSTART.indexOf(mi) !== -1) dy = -1; + else dx = -1; + return [dx, dy]; } function getInterpPx(pi, loc, step) { - var locx = loc[0] + Math.max(step[0], 0), - locy = loc[1] + Math.max(step[1], 0), - zxy = pi.z[locy][locx], - xa = pi.xaxis, - ya = pi.yaxis; - - if(step[1]) { - var dx = (pi.level - zxy) / (pi.z[locy][locx + 1] - zxy); - return [xa.c2p((1 - dx) * pi.x[locx] + dx * pi.x[locx + 1], true), - ya.c2p(pi.y[locy], true)]; - } - else { - var dy = (pi.level - zxy) / (pi.z[locy + 1][locx] - zxy); - return [xa.c2p(pi.x[locx], true), - ya.c2p((1 - dy) * pi.y[locy] + dy * pi.y[locy + 1], true)]; - } + var locx = loc[0] + Math.max(step[0], 0), + locy = loc[1] + Math.max(step[1], 0), + zxy = pi.z[locy][locx], + xa = pi.xaxis, + ya = pi.yaxis; + + if (step[1]) { + var dx = (pi.level - zxy) / (pi.z[locy][locx + 1] - zxy); + return [ + xa.c2p((1 - dx) * pi.x[locx] + dx * pi.x[locx + 1], true), + ya.c2p(pi.y[locy], true) + ]; + } else { + var dy = (pi.level - zxy) / (pi.z[locy + 1][locx] - zxy); + return [ + xa.c2p(pi.x[locx], true), + ya.c2p((1 - dy) * pi.y[locy] + dy * pi.y[locy + 1], true) + ]; + } } diff --git a/src/traces/contour/hover.js b/src/traces/contour/hover.js index d53393d9ed8..30f0342021a 100644 --- a/src/traces/contour/hover.js +++ b/src/traces/contour/hover.js @@ -5,13 +5,9 @@ * 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 heatmapHoverPoints = require('../heatmap/hover'); - +"use strict"; +var heatmapHoverPoints = require("../heatmap/hover"); module.exports = function hoverPoints(pointData, xval, yval, hovermode) { - return heatmapHoverPoints(pointData, xval, yval, hovermode, true); + return heatmapHoverPoints(pointData, xval, yval, hovermode, true); }; diff --git a/src/traces/contour/index.js b/src/traces/contour/index.js index ee18de12422..e784294941d 100644 --- a/src/traces/contour/index.js +++ b/src/traces/contour/index.js @@ -5,35 +5,31 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var Contour = {}; -Contour.attributes = require('./attributes'); -Contour.supplyDefaults = require('./defaults'); -Contour.calc = require('./calc'); -Contour.plot = require('./plot'); -Contour.style = require('./style'); -Contour.colorbar = require('./colorbar'); -Contour.hoverPoints = require('./hover'); +Contour.attributes = require("./attributes"); +Contour.supplyDefaults = require("./defaults"); +Contour.calc = require("./calc"); +Contour.plot = require("./plot"); +Contour.style = require("./style"); +Contour.colorbar = require("./colorbar"); +Contour.hoverPoints = require("./hover"); -Contour.moduleType = 'trace'; -Contour.name = 'contour'; -Contour.basePlotModule = require('../../plots/cartesian'); -Contour.categories = ['cartesian', '2dMap', 'contour']; +Contour.moduleType = "trace"; +Contour.name = "contour"; +Contour.basePlotModule = require("../../plots/cartesian"); +Contour.categories = ["cartesian", "2dMap", "contour"]; Contour.meta = { - description: [ - 'The data from which contour lines are computed is set in `z`.', - 'Data in `z` must be a {2D array} of numbers.', - - 'Say that `z` has N rows and M columns, then by default,', - 'these N rows correspond to N y coordinates', - '(set in `y` or auto-generated) and the M columns', - 'correspond to M x coordinates (set in `x` or auto-generated).', - 'By setting `transpose` to *true*, the above behavior is flipped.' - ].join(' ') + description: [ + "The data from which contour lines are computed is set in `z`.", + "Data in `z` must be a {2D array} of numbers.", + "Say that `z` has N rows and M columns, then by default,", + "these N rows correspond to N y coordinates", + "(set in `y` or auto-generated) and the M columns", + "correspond to M x coordinates (set in `x` or auto-generated).", + "By setting `transpose` to *true*, the above behavior is flipped." + ].join(" ") }; module.exports = Contour; diff --git a/src/traces/contour/make_color_map.js b/src/traces/contour/make_color_map.js index fc54a155705..6f7fb5793f3 100644 --- a/src/traces/contour/make_color_map.js +++ b/src/traces/contour/make_color_map.js @@ -5,73 +5,68 @@ * 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 d3 = require('d3'); -var Colorscale = require('../../components/colorscale'); -var endPlus = require('./end_plus'); +"use strict"; +var d3 = require("d3"); +var Colorscale = require("../../components/colorscale"); +var endPlus = require("./end_plus"); module.exports = function makeColorMap(trace) { - var contours = trace.contours, - start = contours.start, - end = endPlus(contours), - cs = contours.size || 1, - nc = Math.floor((end - start) / cs) + 1, - extra = contours.coloring === 'lines' ? 0 : 1; + var contours = trace.contours, + start = contours.start, + end = endPlus(contours), + cs = contours.size || 1, + nc = Math.floor((end - start) / cs) + 1, + extra = contours.coloring === "lines" ? 0 : 1; - var scl = trace.colorscale, - len = scl.length; + var scl = trace.colorscale, len = scl.length; - var domain = new Array(len), - range = new Array(len); + var domain = new Array(len), range = new Array(len); - var si, i; + var si, i; - if(contours.coloring === 'heatmap') { - if(trace.zauto && trace.autocontour === false) { - trace.zmin = start - cs / 2; - trace.zmax = trace.zmin + nc * cs; - } + if (contours.coloring === "heatmap") { + if (trace.zauto && trace.autocontour === false) { + trace.zmin = start - cs / 2; + trace.zmax = trace.zmin + nc * cs; + } - for(i = 0; i < len; i++) { - si = scl[i]; + for (i = 0; i < len; i++) { + si = scl[i]; - domain[i] = si[0] * (trace.zmax - trace.zmin) + trace.zmin; - range[i] = si[1]; - } + domain[i] = si[0] * (trace.zmax - trace.zmin) + trace.zmin; + range[i] = si[1]; + } - // do the contours extend beyond the colorscale? - // if so, extend the colorscale with constants - var zRange = d3.extent([trace.zmin, trace.zmax, contours.start, - contours.start + cs * (nc - 1)]), - zmin = zRange[trace.zmin < trace.zmax ? 0 : 1], - zmax = zRange[trace.zmin < trace.zmax ? 1 : 0]; + // do the contours extend beyond the colorscale? + // if so, extend the colorscale with constants + var zRange = d3.extent([ + trace.zmin, + trace.zmax, + contours.start, + contours.start + cs * (nc - 1) + ]), + zmin = zRange[trace.zmin < trace.zmax ? 0 : 1], + zmax = zRange[trace.zmin < trace.zmax ? 1 : 0]; - if(zmin !== trace.zmin) { - domain.splice(0, 0, zmin); - range.splice(0, 0, Range[0]); - } + if (zmin !== trace.zmin) { + domain.splice(0, 0, zmin); + range.splice(0, 0, Range[0]); + } - if(zmax !== trace.zmax) { - domain.push(zmax); - range.push(range[range.length - 1]); - } + if (zmax !== trace.zmax) { + domain.push(zmax); + range.push(range[range.length - 1]); } - else { - for(i = 0; i < len; i++) { - si = scl[i]; + } else { + for (i = 0; i < len; i++) { + si = scl[i]; - domain[i] = (si[0] * (nc + extra - 1) - (extra / 2)) * cs + start; - range[i] = si[1]; - } + domain[i] = (si[0] * (nc + extra - 1) - extra / 2) * cs + start; + range[i] = si[1]; } + } - return Colorscale.makeColorScaleFunc({ - domain: domain, - range: range, - }, { - noNumericCheck: true - }); + return Colorscale.makeColorScaleFunc({ domain: domain, range: range }, { + noNumericCheck: true + }); }; diff --git a/src/traces/contour/make_crossings.js b/src/traces/contour/make_crossings.js index 7d24830f4cf..531f74d53e1 100644 --- a/src/traces/contour/make_crossings.js +++ b/src/traces/contour/make_crossings.js @@ -5,64 +5,69 @@ * 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 constants = require('./constants'); +"use strict"; +var constants = require("./constants"); // Calculate all the marching indices, for ALL levels at once. // since we want to be exhaustive we'll check for contour crossings // at every intersection, rather than just following a path // TODO: shorten the inner loop to only the relevant levels module.exports = function makeCrossings(pathinfo) { - var z = pathinfo[0].z, - m = z.length, - n = z[0].length, // we already made sure z isn't ragged in interp2d - twoWide = m === 2 || n === 2, - xi, - yi, - startIndices, - ystartIndices, - label, - corners, - mi, - pi, - i; + var z = pathinfo[0].z, + m = z.length, + n = z[0].length, + // we already made sure z isn't ragged in interp2d + twoWide = m === 2 || n === 2, + xi, + yi, + startIndices, + ystartIndices, + label, + corners, + mi, + pi, + i; - for(yi = 0; yi < m - 1; yi++) { - ystartIndices = []; - if(yi === 0) ystartIndices = ystartIndices.concat(constants.BOTTOMSTART); - if(yi === m - 2) ystartIndices = ystartIndices.concat(constants.TOPSTART); + for (yi = 0; yi < m - 1; yi++) { + ystartIndices = []; + if (yi === 0) ystartIndices = ystartIndices.concat(constants.BOTTOMSTART); + if (yi === m - 2) ystartIndices = ystartIndices.concat(constants.TOPSTART); - for(xi = 0; xi < n - 1; xi++) { - startIndices = ystartIndices.slice(); - if(xi === 0) startIndices = startIndices.concat(constants.LEFTSTART); - if(xi === n - 2) startIndices = startIndices.concat(constants.RIGHTSTART); + for (xi = 0; xi < n - 1; xi++) { + startIndices = ystartIndices.slice(); + if (xi === 0) startIndices = startIndices.concat(constants.LEFTSTART); + if (xi === n - 2) { + startIndices = startIndices.concat(constants.RIGHTSTART); + } - label = xi + ',' + yi; - corners = [[z[yi][xi], z[yi][xi + 1]], - [z[yi + 1][xi], z[yi + 1][xi + 1]]]; - for(i = 0; i < pathinfo.length; i++) { - pi = pathinfo[i]; - mi = getMarchingIndex(pi.level, corners); - if(!mi) continue; + label = xi + "," + yi; + corners = [ + [z[yi][xi], z[yi][xi + 1]], + [z[yi + 1][xi], z[yi + 1][xi + 1]] + ]; + for (i = 0; i < pathinfo.length; i++) { + pi = pathinfo[i]; + mi = getMarchingIndex(pi.level, corners); + if (!mi) continue; - pi.crossings[label] = mi; - if(startIndices.indexOf(mi) !== -1) { - pi.starts.push([xi, yi]); - if(twoWide && startIndices.indexOf(mi, - startIndices.indexOf(mi) + 1) !== -1) { - // the same square has starts from opposite sides - // it's not possible to have starts on opposite edges - // of a corner, only a start and an end... - // but if the array is only two points wide (either way) - // you can have starts on opposite sides. - pi.starts.push([xi, yi]); - } - } - } + pi.crossings[label] = mi; + if (startIndices.indexOf(mi) !== -1) { + pi.starts.push([xi, yi]); + if ( + twoWide && + startIndices.indexOf(mi, startIndices.indexOf(mi) + 1) !== -1 + ) { + // the same square has starts from opposite sides + // it's not possible to have starts on opposite edges + // of a corner, only a start and an end... + // but if the array is only two points wide (either way) + // you can have starts on opposite sides. + pi.starts.push([xi, yi]); + } } + } } + } }; // modified marching squares algorithm, @@ -74,17 +79,17 @@ module.exports = function makeCrossings(pathinfo) { // as the decimal combination of the two appropriate // non-saddle indices function getMarchingIndex(val, corners) { - var mi = (corners[0][0] > val ? 0 : 1) + - (corners[0][1] > val ? 0 : 2) + - (corners[1][1] > val ? 0 : 4) + - (corners[1][0] > val ? 0 : 8); - if(mi === 5 || mi === 10) { - var avg = (corners[0][0] + corners[0][1] + - corners[1][0] + corners[1][1]) / 4; - // two peaks with a big valley - if(val > avg) return (mi === 5) ? 713 : 1114; - // two valleys with a big ridge - return (mi === 5) ? 104 : 208; - } - return (mi === 15) ? 0 : mi; + var mi = (corners[0][0] > val ? 0 : 1) + + (corners[0][1] > val ? 0 : 2) + + (corners[1][1] > val ? 0 : 4) + + (corners[1][0] > val ? 0 : 8); + if (mi === 5 || mi === 10) { + var avg = (corners[0][0] + corners[0][1] + corners[1][0] + corners[1][1]) / + 4; + // two peaks with a big valley + if (val > avg) return mi === 5 ? 713 : 1114; + // two valleys with a big ridge + return mi === 5 ? 104 : 208; + } + return mi === 15 ? 0 : mi; } diff --git a/src/traces/contour/plot.js b/src/traces/contour/plot.js index e33beda01f1..7634b3b7399 100644 --- a/src/traces/contour/plot.js +++ b/src/traces/contour/plot.js @@ -5,352 +5,374 @@ * 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 d3 = require("d3"); +var Lib = require("../../lib"); +var Drawing = require("../../components/drawing"); -'use strict'; - -var d3 = require('d3'); - -var Lib = require('../../lib'); -var Drawing = require('../../components/drawing'); - -var heatmapPlot = require('../heatmap/plot'); -var makeCrossings = require('./make_crossings'); -var findAllPaths = require('./find_all_paths'); -var endPlus = require('./end_plus'); - +var heatmapPlot = require("../heatmap/plot"); +var makeCrossings = require("./make_crossings"); +var findAllPaths = require("./find_all_paths"); +var endPlus = require("./end_plus"); module.exports = function plot(gd, plotinfo, cdcontours) { - for(var i = 0; i < cdcontours.length; i++) { - plotOne(gd, plotinfo, cdcontours[i]); - } + for (var i = 0; i < cdcontours.length; i++) { + plotOne(gd, plotinfo, cdcontours[i]); + } }; function plotOne(gd, plotinfo, cd) { - var trace = cd[0].trace, - x = cd[0].x, - y = cd[0].y, - contours = trace.contours, - uid = trace.uid, - xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - fullLayout = gd._fullLayout, - id = 'contour' + uid, - pathinfo = emptyPathinfo(contours, plotinfo, cd[0]); - - if(trace.visible !== true) { - fullLayout._paper.selectAll('.' + id + ',.hm' + uid).remove(); - fullLayout._infolayer.selectAll('.cb' + uid).remove(); - return; + var trace = cd[0].trace, + x = cd[0].x, + y = cd[0].y, + contours = trace.contours, + uid = trace.uid, + xa = plotinfo.xaxis, + ya = plotinfo.yaxis, + fullLayout = gd._fullLayout, + id = "contour" + uid, + pathinfo = emptyPathinfo(contours, plotinfo, cd[0]); + + if (trace.visible !== true) { + fullLayout._paper.selectAll("." + id + ",.hm" + uid).remove(); + fullLayout._infolayer.selectAll(".cb" + uid).remove(); + return; + } + + // use a heatmap to fill - draw it behind the lines + if (contours.coloring === "heatmap") { + if (trace.zauto && trace.autocontour === false) { + trace._input.zmin = trace.zmin = contours.start - contours.size / 2; + trace._input.zmax = trace.zmax = trace.zmin + + pathinfo.length * contours.size; } - // use a heatmap to fill - draw it behind the lines - if(contours.coloring === 'heatmap') { - if(trace.zauto && (trace.autocontour === false)) { - trace._input.zmin = trace.zmin = - contours.start - contours.size / 2; - trace._input.zmax = trace.zmax = - trace.zmin + pathinfo.length * contours.size; - } - - heatmapPlot(gd, plotinfo, [cd]); - } + heatmapPlot(gd, plotinfo, [cd]); + } else { // in case this used to be a heatmap (or have heatmap fill) - else fullLayout._paper.selectAll('.hm' + uid).remove(); - - makeCrossings(pathinfo); - findAllPaths(pathinfo); - - var leftedge = xa.c2p(x[0], true), - rightedge = xa.c2p(x[x.length - 1], true), - bottomedge = ya.c2p(y[0], true), - topedge = ya.c2p(y[y.length - 1], true), - perimeter = [ - [leftedge, topedge], - [rightedge, topedge], - [rightedge, bottomedge], - [leftedge, bottomedge] - ]; - - // draw everything - var plotGroup = makeContourGroup(plotinfo, cd, id); - makeBackground(plotGroup, perimeter, contours); - makeFills(plotGroup, pathinfo, perimeter, contours); - makeLines(plotGroup, pathinfo, contours); - clipGaps(plotGroup, plotinfo, cd[0], perimeter); + fullLayout._paper.selectAll(".hm" + uid).remove(); + } + + makeCrossings(pathinfo); + findAllPaths(pathinfo); + + var leftedge = xa.c2p(x[0], true), + rightedge = xa.c2p(x[x.length - 1], true), + bottomedge = ya.c2p(y[0], true), + topedge = ya.c2p(y[y.length - 1], true), + perimeter = [ + [leftedge, topedge], + [rightedge, topedge], + [rightedge, bottomedge], + [leftedge, bottomedge] + ]; + + // draw everything + var plotGroup = makeContourGroup(plotinfo, cd, id); + makeBackground(plotGroup, perimeter, contours); + makeFills(plotGroup, pathinfo, perimeter, contours); + makeLines(plotGroup, pathinfo, contours); + clipGaps(plotGroup, plotinfo, cd[0], perimeter); } function emptyPathinfo(contours, plotinfo, cd0) { - var cs = contours.size, - pathinfo = [], - end = endPlus(contours); - - for(var ci = contours.start; ci < end; ci += cs) { - pathinfo.push({ - level: ci, - // all the cells with nontrivial marching index - crossings: {}, - // starting points on the edges of the lattice for each contour - starts: [], - // all unclosed paths (may have less items than starts, - // if a path is closed by rounding) - edgepaths: [], - // all closed paths - paths: [], - // store axes so we can convert to px - xaxis: plotinfo.xaxis, - yaxis: plotinfo.yaxis, - // full data arrays to use for interpolation - x: cd0.x, - y: cd0.y, - z: cd0.z, - smoothing: cd0.trace.line.smoothing - }); - - if(pathinfo.length > 1000) { - Lib.warn('Too many contours, clipping at 1000', contours); - break; - } + var cs = contours.size, pathinfo = [], end = endPlus(contours); + + for (var ci = contours.start; ci < end; ci += cs) { + pathinfo.push({ + level: ci, + // all the cells with nontrivial marching index + crossings: {}, + // starting points on the edges of the lattice for each contour + starts: [], + // all unclosed paths (may have less items than starts, + // if a path is closed by rounding) + edgepaths: [], + // all closed paths + paths: [], + // store axes so we can convert to px + xaxis: plotinfo.xaxis, + yaxis: plotinfo.yaxis, + // full data arrays to use for interpolation + x: cd0.x, + y: cd0.y, + z: cd0.z, + smoothing: cd0.trace.line.smoothing + }); + + if (pathinfo.length > 1000) { + Lib.warn("Too many contours, clipping at 1000", contours); + break; } - return pathinfo; + } + return pathinfo; } function makeContourGroup(plotinfo, cd, id) { - var plotgroup = plotinfo.plot.select('.maplayer') - .selectAll('g.contour.' + id) - .data(cd); + var plotgroup = plotinfo.plot + .select(".maplayer") + .selectAll("g.contour." + id) + .data(cd); - plotgroup.enter().append('g') - .classed('contour', true) - .classed(id, true); + plotgroup.enter().append("g").classed("contour", true).classed(id, true); - plotgroup.exit().remove(); + plotgroup.exit().remove(); - return plotgroup; + return plotgroup; } function makeBackground(plotgroup, perimeter, contours) { - var bggroup = plotgroup.selectAll('g.contourbg').data([0]); - bggroup.enter().append('g').classed('contourbg', true); - - var bgfill = bggroup.selectAll('path') - .data(contours.coloring === 'fill' ? [0] : []); - bgfill.enter().append('path'); - bgfill.exit().remove(); - bgfill - .attr('d', 'M' + perimeter.join('L') + 'Z') - .style('stroke', 'none'); + var bggroup = plotgroup.selectAll("g.contourbg").data([0]); + bggroup.enter().append("g").classed("contourbg", true); + + var bgfill = bggroup + .selectAll("path") + .data(contours.coloring === "fill" ? [0] : []); + bgfill.enter().append("path"); + bgfill.exit().remove(); + bgfill.attr("d", "M" + perimeter.join("L") + "Z").style("stroke", "none"); } function makeFills(plotgroup, pathinfo, perimeter, contours) { - var fillgroup = plotgroup.selectAll('g.contourfill') - .data([0]); - fillgroup.enter().append('g') - .classed('contourfill', true); - - var fillitems = fillgroup.selectAll('path') - .data(contours.coloring === 'fill' ? pathinfo : []); - fillitems.enter().append('path'); - fillitems.exit().remove(); - fillitems.each(function(pi) { - // join all paths for this level together into a single path - // first follow clockwise around the perimeter to close any open paths - // if the whole perimeter is above this level, start with a path - // enclosing the whole thing. With all that, the parity should mean - // that we always fill everything above the contour, nothing below - var fullpath = joinAllPaths(pi, perimeter); - - if(!fullpath) d3.select(this).remove(); - else d3.select(this).attr('d', fullpath).style('stroke', 'none'); - }); + var fillgroup = plotgroup.selectAll("g.contourfill").data([0]); + fillgroup.enter().append("g").classed("contourfill", true); + + var fillitems = fillgroup + .selectAll("path") + .data(contours.coloring === "fill" ? pathinfo : []); + fillitems.enter().append("path"); + fillitems.exit().remove(); + fillitems.each(function(pi) { + // join all paths for this level together into a single path + // first follow clockwise around the perimeter to close any open paths + // if the whole perimeter is above this level, start with a path + // enclosing the whole thing. With all that, the parity should mean + // that we always fill everything above the contour, nothing below + var fullpath = joinAllPaths(pi, perimeter); + + if (!fullpath) d3.select(this).remove(); + else d3.select(this).attr("d", fullpath).style("stroke", "none"); + }); } function joinAllPaths(pi, perimeter) { - var edgeVal2 = Math.min(pi.z[0][0], pi.z[0][1]), - fullpath = (pi.edgepaths.length || edgeVal2 <= pi.level) ? - '' : ('M' + perimeter.join('L') + 'Z'), - i = 0, - startsleft = pi.edgepaths.map(function(v, i) { return i; }), - newloop = true, - endpt, - newendpt, - cnt, - nexti, - possiblei, - addpath; - - function istop(pt) { return Math.abs(pt[1] - perimeter[0][1]) < 0.01; } - function isbottom(pt) { return Math.abs(pt[1] - perimeter[2][1]) < 0.01; } - function isleft(pt) { return Math.abs(pt[0] - perimeter[0][0]) < 0.01; } - function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < 0.01; } - - while(startsleft.length) { - addpath = Drawing.smoothopen(pi.edgepaths[i], pi.smoothing); - fullpath += newloop ? addpath : addpath.replace(/^M/, 'L'); - startsleft.splice(startsleft.indexOf(i), 1); - endpt = pi.edgepaths[i][pi.edgepaths[i].length - 1]; - nexti = -1; - - // now loop through sides, moving our endpoint until we find a new start - for(cnt = 0; cnt < 4; cnt++) { // just to prevent infinite loops - if(!endpt) { - Lib.log('Missing end?', i, pi); - break; - } - - if(istop(endpt) && !isright(endpt)) newendpt = perimeter[1]; // right top - else if(isleft(endpt)) newendpt = perimeter[0]; // left top - else if(isbottom(endpt)) newendpt = perimeter[3]; // right bottom - else if(isright(endpt)) newendpt = perimeter[2]; // left bottom - - for(possiblei = 0; possiblei < pi.edgepaths.length; possiblei++) { - var ptNew = pi.edgepaths[possiblei][0]; - // is ptNew on the (horz. or vert.) segment from endpt to newendpt? - if(Math.abs(endpt[0] - newendpt[0]) < 0.01) { - if(Math.abs(endpt[0] - ptNew[0]) < 0.01 && - (ptNew[1] - endpt[1]) * (newendpt[1] - ptNew[1]) >= 0) { - newendpt = ptNew; - nexti = possiblei; - } - } - else if(Math.abs(endpt[1] - newendpt[1]) < 0.01) { - if(Math.abs(endpt[1] - ptNew[1]) < 0.01 && - (ptNew[0] - endpt[0]) * (newendpt[0] - ptNew[0]) >= 0) { - newendpt = ptNew; - nexti = possiblei; - } - } - else { - Lib.log('endpt to newendpt is not vert. or horz.', - endpt, newendpt, ptNew); - } - } - - endpt = newendpt; - - if(nexti >= 0) break; - fullpath += 'L' + newendpt; + var edgeVal2 = Math.min(pi.z[0][0], pi.z[0][1]), + fullpath = pi.edgepaths.length || edgeVal2 <= pi.level + ? "" + : "M" + perimeter.join("L") + "Z", + i = 0, + startsleft = pi.edgepaths.map(function(v, i) { + return i; + }), + newloop = true, + endpt, + newendpt, + cnt, + nexti, + possiblei, + addpath; + + function istop(pt) { + return Math.abs(pt[1] - perimeter[0][1]) < 0.01; + } + function isbottom(pt) { + return Math.abs(pt[1] - perimeter[2][1]) < 0.01; + } + function isleft(pt) { + return Math.abs(pt[0] - perimeter[0][0]) < 0.01; + } + function isright(pt) { + return Math.abs(pt[0] - perimeter[2][0]) < 0.01; + } + + while (startsleft.length) { + addpath = Drawing.smoothopen(pi.edgepaths[i], pi.smoothing); + fullpath += newloop ? addpath : addpath.replace(/^M/, "L"); + startsleft.splice(startsleft.indexOf(i), 1); + endpt = pi.edgepaths[i][pi.edgepaths[i].length - 1]; + nexti = -1; + + // now loop through sides, moving our endpoint until we find a new start + for (cnt = 0; cnt < 4; cnt++) { + // just to prevent infinite loops + if (!endpt) { + Lib.log("Missing end?", i, pi); + break; + } + + if (istop(endpt) && !isright(endpt)) { + newendpt = perimeter[1]; + } else if (isleft(endpt)) { + // right top + newendpt = perimeter[0]; + } else if (isbottom(endpt)) { + // left top + newendpt = perimeter[3]; + } else if (isright(endpt)) newendpt = perimeter[2]; // right bottom + + // left bottom + for (possiblei = 0; possiblei < pi.edgepaths.length; possiblei++) { + var ptNew = pi.edgepaths[possiblei][0]; + // is ptNew on the (horz. or vert.) segment from endpt to newendpt? + if (Math.abs(endpt[0] - newendpt[0]) < 0.01) { + if ( + Math.abs(endpt[0] - ptNew[0]) < 0.01 && + (ptNew[1] - endpt[1]) * (newendpt[1] - ptNew[1]) >= 0 + ) { + newendpt = ptNew; + nexti = possiblei; + } + } else if (Math.abs(endpt[1] - newendpt[1]) < 0.01) { + if ( + Math.abs(endpt[1] - ptNew[1]) < 0.01 && + (ptNew[0] - endpt[0]) * (newendpt[0] - ptNew[0]) >= 0 + ) { + newendpt = ptNew; + nexti = possiblei; + } + } else { + Lib.log( + "endpt to newendpt is not vert. or horz.", + endpt, + newendpt, + ptNew + ); } + } - if(nexti === pi.edgepaths.length) { - Lib.log('unclosed perimeter path'); - break; - } + endpt = newendpt; - i = nexti; + if (nexti >= 0) break; + fullpath += "L" + newendpt; + } - // if we closed back on a loop we already included, - // close it and start a new loop - newloop = (startsleft.indexOf(i) === -1); - if(newloop) { - i = startsleft[0]; - fullpath += 'Z'; - } + if (nexti === pi.edgepaths.length) { + Lib.log("unclosed perimeter path"); + break; } - // finally add the interior paths - for(i = 0; i < pi.paths.length; i++) { - fullpath += Drawing.smoothclosed(pi.paths[i], pi.smoothing); + i = nexti; + + // if we closed back on a loop we already included, + // close it and start a new loop + newloop = startsleft.indexOf(i) === -1; + if (newloop) { + i = startsleft[0]; + fullpath += "Z"; } + } - return fullpath; + // finally add the interior paths + for (i = 0; i < pi.paths.length; i++) { + fullpath += Drawing.smoothclosed(pi.paths[i], pi.smoothing); + } + + return fullpath; } function makeLines(plotgroup, pathinfo, contours) { - var smoothing = pathinfo[0].smoothing; - - var linegroup = plotgroup.selectAll('g.contourlevel') - .data(contours.showlines === false ? [] : pathinfo); - linegroup.enter().append('g') - .classed('contourlevel', true); - linegroup.exit().remove(); - - var opencontourlines = linegroup.selectAll('path.openline') - .data(function(d) { return d.edgepaths; }); - opencontourlines.enter().append('path') - .classed('openline', true); - opencontourlines.exit().remove(); - opencontourlines - .attr('d', function(d) { - return Drawing.smoothopen(d, smoothing); - }) - .style('stroke-miterlimit', 1); - - var closedcontourlines = linegroup.selectAll('path.closedline') - .data(function(d) { return d.paths; }); - closedcontourlines.enter().append('path') - .classed('closedline', true); - closedcontourlines.exit().remove(); - closedcontourlines - .attr('d', function(d) { - return Drawing.smoothclosed(d, smoothing); - }) - .style('stroke-miterlimit', 1); + var smoothing = pathinfo[0].smoothing; + + var linegroup = plotgroup + .selectAll("g.contourlevel") + .data(contours.showlines === false ? [] : pathinfo); + linegroup.enter().append("g").classed("contourlevel", true); + linegroup.exit().remove(); + + var opencontourlines = linegroup.selectAll("path.openline").data(function(d) { + return d.edgepaths; + }); + opencontourlines.enter().append("path").classed("openline", true); + opencontourlines.exit().remove(); + opencontourlines + .attr("d", function(d) { + return Drawing.smoothopen(d, smoothing); + }) + .style("stroke-miterlimit", 1); + + var closedcontourlines = linegroup + .selectAll("path.closedline") + .data(function(d) { + return d.paths; + }); + closedcontourlines.enter().append("path").classed("closedline", true); + closedcontourlines.exit().remove(); + closedcontourlines + .attr("d", function(d) { + return Drawing.smoothclosed(d, smoothing); + }) + .style("stroke-miterlimit", 1); } function clipGaps(plotGroup, plotinfo, cd0, perimeter) { - var clipId = 'clip' + cd0.trace.uid; - - var defs = plotinfo.plot.selectAll('defs') - .data([0]); - defs.enter().append('defs'); - - var clipPath = defs.selectAll('#' + clipId) - .data(cd0.trace.connectgaps ? [] : [0]); - clipPath.enter().append('clipPath').attr('id', clipId); - clipPath.exit().remove(); - - if(cd0.trace.connectgaps === false) { - var clipPathInfo = { - // fraction of the way from missing to present point - // to draw the boundary. - // if you make this 1 (or 1-epsilon) then a point in - // a sea of missing data will disappear entirely. - level: 0.9, - crossings: {}, - starts: [], - edgepaths: [], - paths: [], - xaxis: plotinfo.xaxis, - yaxis: plotinfo.yaxis, - x: cd0.x, - y: cd0.y, - // 0 = no data, 1 = data - z: makeClipMask(cd0), - smoothing: 0 - }; - - makeCrossings([clipPathInfo]); - findAllPaths([clipPathInfo]); - var fullpath = joinAllPaths(clipPathInfo, perimeter); - - var path = clipPath.selectAll('path') - .data([0]); - path.enter().append('path'); - path.attr('d', fullpath); - } - else clipId = null; - - plotGroup.call(Drawing.setClipUrl, clipId); - plotinfo.plot.selectAll('.hm' + cd0.trace.uid) - .call(Drawing.setClipUrl, clipId); + var clipId = "clip" + cd0.trace.uid; + + var defs = plotinfo.plot.selectAll("defs").data([0]); + defs.enter().append("defs"); + + var clipPath = defs + .selectAll("#" + clipId) + .data(cd0.trace.connectgaps ? [] : [0]); + clipPath.enter().append("clipPath").attr("id", clipId); + clipPath.exit().remove(); + + if (cd0.trace.connectgaps === false) { + var clipPathInfo = { + // fraction of the way from missing to present point + // to draw the boundary. + // if you make this 1 (or 1-epsilon) then a point in + // a sea of missing data will disappear entirely. + level: 0.9, + crossings: {}, + starts: [], + edgepaths: [], + paths: [], + xaxis: plotinfo.xaxis, + yaxis: plotinfo.yaxis, + x: cd0.x, + y: cd0.y, + // 0 = no data, 1 = data + z: makeClipMask(cd0), + smoothing: 0 + }; + + makeCrossings([clipPathInfo]); + findAllPaths([clipPathInfo]); + var fullpath = joinAllPaths(clipPathInfo, perimeter); + + var path = clipPath.selectAll("path").data([0]); + path.enter().append("path"); + path.attr("d", fullpath); + } else { + clipId = null; + } + + plotGroup.call(Drawing.setClipUrl, clipId); + plotinfo.plot + .selectAll(".hm" + cd0.trace.uid) + .call(Drawing.setClipUrl, clipId); } function makeClipMask(cd0) { - var empties = cd0.trace._emptypoints, - z = [], - m = cd0.z.length, - n = cd0.z[0].length, - i, - row = [], - emptyPoint; - - for(i = 0; i < n; i++) row.push(1); - for(i = 0; i < m; i++) z.push(row.slice()); - for(i = 0; i < empties.length; i++) { - emptyPoint = empties[i]; - z[emptyPoint[0]][emptyPoint[1]] = 0; - } - // save this mask to determine whether to show this data in hover - cd0.zmask = z; - return z; + var empties = cd0.trace._emptypoints, + z = [], + m = cd0.z.length, + n = cd0.z[0].length, + i, + row = [], + emptyPoint; + + for (i = 0; i < n; i++) { + row.push(1); + } + for (i = 0; i < m; i++) { + z.push(row.slice()); + } + for (i = 0; i < empties.length; i++) { + emptyPoint = empties[i]; + z[emptyPoint[0]][emptyPoint[1]] = 0; + } + // save this mask to determine whether to show this data in hover + cd0.zmask = z; + return z; } diff --git a/src/traces/contour/style.js b/src/traces/contour/style.js index 3ce4e56c64c..f75f091f835 100644 --- a/src/traces/contour/style.js +++ b/src/traces/contour/style.js @@ -5,56 +5,56 @@ * 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 d3 = require("d3"); +var Drawing = require("../../components/drawing"); +var heatmapStyle = require("../heatmap/style"); -'use strict'; - -var d3 = require('d3'); - -var Drawing = require('../../components/drawing'); -var heatmapStyle = require('../heatmap/style'); - -var makeColorMap = require('./make_color_map'); - +var makeColorMap = require("./make_color_map"); module.exports = function style(gd) { - var contours = d3.select(gd).selectAll('g.contour'); - - contours.style('opacity', function(d) { - return d.trace.opacity; + var contours = d3.select(gd).selectAll("g.contour"); + + contours.style("opacity", function(d) { + return d.trace.opacity; + }); + + contours.each(function(d) { + var c = d3.select(this), + trace = d.trace, + contours = trace.contours, + line = trace.line, + cs = contours.size || 1, + start = contours.start; + + var colorMap = makeColorMap(trace); + + c.selectAll("g.contourlevel").each(function(d) { + d3 + .select(this) + .selectAll("path") + .call( + Drawing.lineGroupStyle, + line.width, + contours.coloring === "lines" ? colorMap(d.level) : line.color, + line.dash + ); }); - contours.each(function(d) { - var c = d3.select(this), - trace = d.trace, - contours = trace.contours, - line = trace.line, - cs = contours.size || 1, - start = contours.start; + var firstFill; - var colorMap = makeColorMap(trace); - - c.selectAll('g.contourlevel').each(function(d) { - d3.select(this).selectAll('path') - .call(Drawing.lineGroupStyle, - line.width, - contours.coloring === 'lines' ? colorMap(d.level) : line.color, - line.dash); - }); - - var firstFill; - - c.selectAll('g.contourfill path') - .style('fill', function(d) { - if(firstFill === undefined) firstFill = d.level; - return colorMap(d.level + 0.5 * cs); - }); + c.selectAll("g.contourfill path").style("fill", function(d) { + if (firstFill === undefined) firstFill = d.level; + return colorMap(d.level + 0.5 * cs); + }); - if(firstFill === undefined) firstFill = start; + if (firstFill === undefined) firstFill = start; - c.selectAll('g.contourbg path') - .style('fill', colorMap(firstFill - 0.5 * cs)); - }); + c + .selectAll("g.contourbg path") + .style("fill", colorMap(firstFill - 0.5 * cs)); + }); - heatmapStyle(gd); + heatmapStyle(gd); }; diff --git a/src/traces/contour/style_defaults.js b/src/traces/contour/style_defaults.js index cd29ec6ccbe..48ce342d1c9 100644 --- a/src/traces/contour/style_defaults.js +++ b/src/traces/contour/style_defaults.js @@ -5,30 +5,32 @@ * 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 colorscaleDefaults = require('../../components/colorscale/defaults'); - - -module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, layout) { - var coloring = coerce('contours.coloring'); - - var showLines; - if(coloring === 'fill') showLines = coerce('contours.showlines'); - - if(showLines !== false) { - if(coloring !== 'lines') coerce('line.color', '#000'); - coerce('line.width', 0.5); - coerce('line.dash'); - } - - coerce('line.smoothing'); - - if((traceOut.contours || {}).coloring !== 'none') { - colorscaleDefaults( - traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} - ); - } +"use strict"; +var colorscaleDefaults = require("../../components/colorscale/defaults"); + +module.exports = function handleStyleDefaults( + traceIn, + traceOut, + coerce, + layout +) { + var coloring = coerce("contours.coloring"); + + var showLines; + if (coloring === "fill") showLines = coerce("contours.showlines"); + + if (showLines !== false) { + if (coloring !== "lines") coerce("line.color", "#000"); + coerce("line.width", 0.5); + coerce("line.dash"); + } + + coerce("line.smoothing"); + + if ((traceOut.contours || {}).coloring !== "none") { + colorscaleDefaults(traceIn, traceOut, layout, coerce, { + prefix: "", + cLetter: "z" + }); + } }; diff --git a/src/traces/contourgl/convert.js b/src/traces/contourgl/convert.js index 2c3ef01984c..b0ac7f4d160 100644 --- a/src/traces/contourgl/convert.js +++ b/src/traces/contourgl/convert.js @@ -5,181 +5,172 @@ * 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 createContour2D = require("gl-contour2d"); +var createHeatmap2D = require("gl-heatmap2d"); - -'use strict'; - -var createContour2D = require('gl-contour2d'); -var createHeatmap2D = require('gl-heatmap2d'); - -var Axes = require('../../plots/cartesian/axes'); -var makeColorMap = require('../contour/make_color_map'); -var str2RGBArray = require('../../lib/str2rgbarray'); - +var Axes = require("../../plots/cartesian/axes"); +var makeColorMap = require("../contour/make_color_map"); +var str2RGBArray = require("../../lib/str2rgbarray"); function Contour(scene, uid) { - this.scene = scene; - this.uid = uid; - this.type = 'contourgl'; - - this.name = ''; - this.hoverinfo = 'all'; - - this.xData = []; - this.yData = []; - this.zData = []; - this.textLabels = []; - - this.idToIndex = []; - this.bounds = [0, 0, 0, 0]; - - this.contourOptions = { - z: new Float32Array(0), - x: [], - y: [], - shape: [0, 0], - levels: [0], - levelColors: [0, 0, 0, 1], - lineWidth: 1 - }; - this.contour = createContour2D(scene.glplot, this.contourOptions); - this.contour._trace = this; - - this.heatmapOptions = { - z: new Float32Array(0), - x: [], - y: [], - shape: [0, 0], - colorLevels: [0], - colorValues: [0, 0, 0, 0] - }; - this.heatmap = createHeatmap2D(scene.glplot, this.heatmapOptions); - this.heatmap._trace = this; + this.scene = scene; + this.uid = uid; + this.type = "contourgl"; + + this.name = ""; + this.hoverinfo = "all"; + + this.xData = []; + this.yData = []; + this.zData = []; + this.textLabels = []; + + this.idToIndex = []; + this.bounds = [0, 0, 0, 0]; + + this.contourOptions = { + z: new Float32Array(0), + x: [], + y: [], + shape: [0, 0], + levels: [0], + levelColors: [0, 0, 0, 1], + lineWidth: 1 + }; + this.contour = createContour2D(scene.glplot, this.contourOptions); + this.contour._trace = this; + + this.heatmapOptions = { + z: new Float32Array(0), + x: [], + y: [], + shape: [0, 0], + colorLevels: [0], + colorValues: [0, 0, 0, 0] + }; + this.heatmap = createHeatmap2D(scene.glplot, this.heatmapOptions); + this.heatmap._trace = this; } var proto = Contour.prototype; proto.handlePick = function(pickResult) { - var options = this.heatmapOptions, - shape = options.shape, - index = pickResult.pointId, - xIndex = index % shape[0], - yIndex = Math.floor(index / shape[0]), - zIndex = index; - - return { - trace: this, - dataCoord: pickResult.dataCoord, - traceCoord: [ - options.x[xIndex], - options.y[yIndex], - options.z[zIndex] - ], - textLabel: this.textLabels[index], - name: this.name, - pointIndex: [xIndex, yIndex], - hoverinfo: this.hoverinfo - }; + var options = this.heatmapOptions, + shape = options.shape, + index = pickResult.pointId, + xIndex = index % shape[0], + yIndex = Math.floor(index / shape[0]), + zIndex = index; + + return { + trace: this, + dataCoord: pickResult.dataCoord, + traceCoord: [options.x[xIndex], options.y[yIndex], options.z[zIndex]], + textLabel: this.textLabels[index], + name: this.name, + pointIndex: [xIndex, yIndex], + hoverinfo: this.hoverinfo + }; }; proto.update = function(fullTrace, calcTrace) { - var calcPt = calcTrace[0]; - - this.name = fullTrace.name; - this.hoverinfo = fullTrace.hoverinfo; - - // convert z from 2D -> 1D - var z = calcPt.z, - rowLen = z[0].length, - colLen = z.length, - colorOptions; - - this.contourOptions.z = flattenZ(z, rowLen, colLen); - this.heatmapOptions.z = [].concat.apply([], z); - - this.contourOptions.shape = this.heatmapOptions.shape = [rowLen, colLen]; - - this.contourOptions.x = this.heatmapOptions.x = calcPt.x; - this.contourOptions.y = this.heatmapOptions.y = calcPt.y; - - // pass on fill information - if(fullTrace.contours.coloring === 'fill') { - colorOptions = convertColorScale(fullTrace, {fill: true}); - this.contourOptions.levels = colorOptions.levels.slice(1); - // though gl-contour2d automatically defaults to a transparent layer for the last - // band color, it's set manually here in case the gl-contour2 API changes - this.contourOptions.fillColors = colorOptions.levelColors; - this.contourOptions.levelColors = [].concat.apply([], this.contourOptions.levels.map(function() { - return [0.25, 0.25, 0.25, 1.0]; - })); - } else { - colorOptions = convertColorScale(fullTrace, {fill: false}); - this.contourOptions.levels = colorOptions.levels; - this.contourOptions.levelColors = colorOptions.levelColors; - } - - // convert text from 2D -> 1D - this.textLabels = [].concat.apply([], fullTrace.text); - - this.contour.update(this.contourOptions); - this.heatmap.update(this.heatmapOptions); - - // expand axes - Axes.expand(this.scene.xaxis, calcPt.x); - Axes.expand(this.scene.yaxis, calcPt.y); + var calcPt = calcTrace[0]; + + this.name = fullTrace.name; + this.hoverinfo = fullTrace.hoverinfo; + + // convert z from 2D -> 1D + var z = calcPt.z, rowLen = z[0].length, colLen = z.length, colorOptions; + + this.contourOptions.z = flattenZ(z, rowLen, colLen); + this.heatmapOptions.z = [].concat.apply([], z); + + this.contourOptions.shape = this.heatmapOptions.shape = [rowLen, colLen]; + + this.contourOptions.x = this.heatmapOptions.x = calcPt.x; + this.contourOptions.y = this.heatmapOptions.y = calcPt.y; + + // pass on fill information + if (fullTrace.contours.coloring === "fill") { + colorOptions = convertColorScale(fullTrace, { fill: true }); + this.contourOptions.levels = colorOptions.levels.slice(1); + // though gl-contour2d automatically defaults to a transparent layer for the last + // band color, it's set manually here in case the gl-contour2 API changes + this.contourOptions.fillColors = colorOptions.levelColors; + this.contourOptions.levelColors = [].concat.apply( + [], + this.contourOptions.levels.map(function() { + return [0.25, 0.25, 0.25, 1.0]; + }) + ); + } else { + colorOptions = convertColorScale(fullTrace, { fill: false }); + this.contourOptions.levels = colorOptions.levels; + this.contourOptions.levelColors = colorOptions.levelColors; + } + + // convert text from 2D -> 1D + this.textLabels = [].concat.apply([], fullTrace.text); + + this.contour.update(this.contourOptions); + this.heatmap.update(this.heatmapOptions); + + // expand axes + Axes.expand(this.scene.xaxis, calcPt.x); + Axes.expand(this.scene.yaxis, calcPt.y); }; proto.dispose = function() { - this.contour.dispose(); - this.heatmap.dispose(); + this.contour.dispose(); + this.heatmap.dispose(); }; function flattenZ(zIn, rowLen, colLen) { - var zOut = new Float32Array(rowLen * colLen); - var pt = 0; + var zOut = new Float32Array(rowLen * colLen); + var pt = 0; - for(var i = 0; i < rowLen; i++) { - for(var j = 0; j < colLen; j++) { - zOut[pt++] = zIn[j][i]; - } + for (var i = 0; i < rowLen; i++) { + for (var j = 0; j < colLen; j++) { + zOut[pt++] = zIn[j][i]; } + } - return zOut; + return zOut; } function convertColorScale(fullTrace, options) { - var contours = fullTrace.contours, - start = contours.start, - end = contours.end, - cs = contours.size || 1, - fill = options.fill; - - var colorMap = makeColorMap(fullTrace); - - var N = Math.floor((end - start) / cs) + (fill ? 2 : 1), // for K thresholds (contour linees) there are K+1 areas - levels = new Array(N), - levelColors = new Array(4 * N); - - for(var i = 0; i < N; i++) { - var level = levels[i] = start + cs * (i) - (fill ? cs / 2 : 0); // in case of fill, use band midpoint - var color = str2RGBArray(colorMap(level)); - - for(var j = 0; j < 4; j++) { - levelColors[(4 * i) + j] = color[j]; - } + var contours = fullTrace.contours, + start = contours.start, + end = contours.end, + cs = contours.size || 1, + fill = options.fill; + + var colorMap = makeColorMap(fullTrace); + + var N = Math.floor((end - start) / cs) + (fill ? 2 : 1), + // for K thresholds (contour linees) there are K+1 areas + levels = new Array(N), + levelColors = new Array(4 * N); + + for (var i = 0; i < N; i++) { + var level = levels[i] = start + cs * i - (fill ? cs / 2 : 0); + // in case of fill, use band midpoint + var color = str2RGBArray(colorMap(level)); + + for (var j = 0; j < 4; j++) { + levelColors[4 * i + j] = color[j]; } + } - return { - levels: levels, - levelColors: levelColors - }; + return { levels: levels, levelColors: levelColors }; } function createContour(scene, fullTrace, calcTrace) { - var plot = new Contour(scene, fullTrace.uid); - plot.update(fullTrace, calcTrace); + var plot = new Contour(scene, fullTrace.uid); + plot.update(fullTrace, calcTrace); - return plot; + return plot; } module.exports = createContour; diff --git a/src/traces/contourgl/index.js b/src/traces/contourgl/index.js index ac4fca3b72d..3322c070919 100644 --- a/src/traces/contourgl/index.js +++ b/src/traces/contourgl/index.js @@ -5,27 +5,20 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var ContourGl = {}; -ContourGl.attributes = require('../contour/attributes'); -ContourGl.supplyDefaults = require('../contour/defaults'); -ContourGl.colorbar = require('../contour/colorbar'); +ContourGl.attributes = require("../contour/attributes"); +ContourGl.supplyDefaults = require("../contour/defaults"); +ContourGl.colorbar = require("../contour/colorbar"); -ContourGl.calc = require('../contour/calc'); -ContourGl.plot = require('./convert'); +ContourGl.calc = require("../contour/calc"); +ContourGl.plot = require("./convert"); -ContourGl.moduleType = 'trace'; -ContourGl.name = 'contourgl'; -ContourGl.basePlotModule = require('../../plots/gl2d'); -ContourGl.categories = ['gl2d', '2dMap']; -ContourGl.meta = { - description: [ - 'WebGL contour (beta)' - ].join(' ') -}; +ContourGl.moduleType = "trace"; +ContourGl.name = "contourgl"; +ContourGl.basePlotModule = require("../../plots/gl2d"); +ContourGl.categories = ["gl2d", "2dMap"]; +ContourGl.meta = { description: ["WebGL contour (beta)"].join(" ") }; module.exports = ContourGl; diff --git a/src/traces/heatmap/attributes.js b/src/traces/heatmap/attributes.js index 2b4d98ce245..74ceb88d6d5 100644 --- a/src/traces/heatmap/attributes.js +++ b/src/traces/heatmap/attributes.js @@ -5,94 +5,94 @@ * 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 scatterAttrs = require("../scatter/attributes"); +var colorscaleAttrs = require("../../components/colorscale/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); -'use strict'; +var extendFlat = require("../../lib/extend").extendFlat; -var scatterAttrs = require('../scatter/attributes'); -var colorscaleAttrs = require('../../components/colorscale/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; - -module.exports = extendFlat({}, { - z: { - valType: 'data_array', - description: 'Sets the z data.' - }, +module.exports = extendFlat( + {}, + { + z: { valType: "data_array", description: "Sets the z data." }, x: scatterAttrs.x, x0: scatterAttrs.x0, dx: scatterAttrs.dx, y: scatterAttrs.y, y0: scatterAttrs.y0, dy: scatterAttrs.dy, - text: { - valType: 'data_array', - description: 'Sets the text elements associated with each z value.' + valType: "data_array", + description: "Sets the text elements associated with each z value." }, transpose: { - valType: 'boolean', - dflt: false, - role: 'info', - description: 'Transposes the z data.' + valType: "boolean", + dflt: false, + role: "info", + description: "Transposes the z data." }, xtype: { - valType: 'enumerated', - values: ['array', 'scaled'], - role: 'info', - description: [ - 'If *array*, the heatmap\'s x coordinates are given by *x*', - '(the default behavior when `x` is provided).', - 'If *scaled*, the heatmap\'s x coordinates are given by *x0* and *dx*', - '(the default behavior when `x` is not provided).' - ].join(' ') + valType: "enumerated", + values: ["array", "scaled"], + role: "info", + description: [ + "If *array*, the heatmap's x coordinates are given by *x*", + "(the default behavior when `x` is provided).", + "If *scaled*, the heatmap's x coordinates are given by *x0* and *dx*", + "(the default behavior when `x` is not provided)." + ].join(" ") }, ytype: { - valType: 'enumerated', - values: ['array', 'scaled'], - role: 'info', - description: [ - 'If *array*, the heatmap\'s y coordinates are given by *y*', - '(the default behavior when `y` is provided)', - 'If *scaled*, the heatmap\'s y coordinates are given by *y0* and *dy*', - '(the default behavior when `y` is not provided)' - ].join(' ') + valType: "enumerated", + values: ["array", "scaled"], + role: "info", + description: [ + "If *array*, the heatmap's y coordinates are given by *y*", + "(the default behavior when `y` is provided)", + "If *scaled*, the heatmap's y coordinates are given by *y0* and *dy*", + "(the default behavior when `y` is not provided)" + ].join(" ") }, zsmooth: { - valType: 'enumerated', - values: ['fast', 'best', false], - dflt: false, - role: 'style', - description: [ - 'Picks a smoothing algorithm use to smooth `z` data.' - ].join(' ') + valType: "enumerated", + values: ["fast", "best", false], + dflt: false, + role: "style", + description: ["Picks a smoothing algorithm use to smooth `z` data."].join( + " " + ) }, connectgaps: { - valType: 'boolean', - dflt: false, - role: 'info', - description: [ - 'Determines whether or not gaps', - '(i.e. {nan} or missing values)', - 'in the `z` data are filled in.' - ].join(' ') + valType: "boolean", + dflt: false, + role: "info", + description: [ + "Determines whether or not gaps", + "(i.e. {nan} or missing values)", + "in the `z` data are filled in." + ].join(" ") }, xgap: { - valType: 'number', - dflt: 0, - min: 0, - role: 'style', - description: 'Sets the horizontal gap (in pixels) between bricks.' + valType: "number", + dflt: 0, + min: 0, + role: "style", + description: "Sets the horizontal gap (in pixels) between bricks." }, ygap: { - valType: 'number', - dflt: 0, - min: 0, - role: 'style', - description: 'Sets the vertical gap (in pixels) between bricks.' - }, -}, - colorscaleAttrs, - { autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, {dflt: false}) }, - { colorbar: colorbarAttrs } + valType: "number", + dflt: 0, + min: 0, + role: "style", + description: "Sets the vertical gap (in pixels) between bricks." + } + }, + colorscaleAttrs, + { + autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, { + dflt: false + }) + }, + { colorbar: colorbarAttrs } ); diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index 7b38ba77620..13bf5c9370a 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -5,136 +5,130 @@ * 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 Registry = require('../../registry'); -var Lib = require('../../lib'); -var Axes = require('../../plots/cartesian/axes'); - -var histogram2dCalc = require('../histogram2d/calc'); -var colorscaleCalc = require('../../components/colorscale/calc'); -var hasColumns = require('./has_columns'); -var convertColumnXYZ = require('./convert_column_xyz'); -var maxRowLength = require('./max_row_length'); -var clean2dArray = require('./clean_2d_array'); -var interp2d = require('./interp2d'); -var findEmpties = require('./find_empties'); -var makeBoundArray = require('./make_bound_array'); - +"use strict"; +var Registry = require("../../registry"); +var Lib = require("../../lib"); +var Axes = require("../../plots/cartesian/axes"); + +var histogram2dCalc = require("../histogram2d/calc"); +var colorscaleCalc = require("../../components/colorscale/calc"); +var hasColumns = require("./has_columns"); +var convertColumnXYZ = require("./convert_column_xyz"); +var maxRowLength = require("./max_row_length"); +var clean2dArray = require("./clean_2d_array"); +var interp2d = require("./interp2d"); +var findEmpties = require("./find_empties"); +var makeBoundArray = require("./make_bound_array"); module.exports = function calc(gd, trace) { - // prepare the raw data - // run makeCalcdata on x and y even for heatmaps, in case of category mappings - var xa = Axes.getFromId(gd, trace.xaxis || 'x'), - ya = Axes.getFromId(gd, trace.yaxis || 'y'), - isContour = Registry.traceIs(trace, 'contour'), - isHist = Registry.traceIs(trace, 'histogram'), - isGL2D = Registry.traceIs(trace, 'gl2d'), - zsmooth = isContour ? 'best' : trace.zsmooth, - x, - x0, - dx, - y, - y0, - dy, - z, - i; - - // cancel minimum tick spacings (only applies to bars and boxes) - xa._minDtick = 0; - ya._minDtick = 0; - - if(isHist) { - var binned = histogram2dCalc(gd, trace); - x = binned.x; - x0 = binned.x0; - dx = binned.dx; - y = binned.y; - y0 = binned.y0; - dy = binned.dy; - z = binned.z; + // prepare the raw data + // run makeCalcdata on x and y even for heatmaps, in case of category mappings + var xa = Axes.getFromId(gd, trace.xaxis || "x"), + ya = Axes.getFromId(gd, trace.yaxis || "y"), + isContour = Registry.traceIs(trace, "contour"), + isHist = Registry.traceIs(trace, "histogram"), + isGL2D = Registry.traceIs(trace, "gl2d"), + zsmooth = isContour ? "best" : trace.zsmooth, + x, + x0, + dx, + y, + y0, + dy, + z, + i; + + // cancel minimum tick spacings (only applies to bars and boxes) + xa._minDtick = 0; + ya._minDtick = 0; + + if (isHist) { + var binned = histogram2dCalc(gd, trace); + x = binned.x; + x0 = binned.x0; + dx = binned.dx; + y = binned.y; + y0 = binned.y0; + dy = binned.dy; + z = binned.z; + } else { + if (hasColumns(trace)) convertColumnXYZ(trace, xa, ya); + + x = trace.x ? xa.makeCalcdata(trace, "x") : []; + y = trace.y ? ya.makeCalcdata(trace, "y") : []; + x0 = trace.x0 || 0; + dx = trace.dx || 1; + y0 = trace.y0 || 0; + dy = trace.dy || 1; + + z = clean2dArray(trace.z, trace.transpose); + + if (isContour || trace.connectgaps) { + trace._emptypoints = findEmpties(z); + trace._interpz = interp2d(z, trace._emptypoints, trace._interpz); } - else { - if(hasColumns(trace)) convertColumnXYZ(trace, xa, ya); - - x = trace.x ? xa.makeCalcdata(trace, 'x') : []; - y = trace.y ? ya.makeCalcdata(trace, 'y') : []; - x0 = trace.x0 || 0; - dx = trace.dx || 1; - y0 = trace.y0 || 0; - dy = trace.dy || 1; - - z = clean2dArray(trace.z, trace.transpose); - - if(isContour || trace.connectgaps) { - trace._emptypoints = findEmpties(z); - trace._interpz = interp2d(z, trace._emptypoints, trace._interpz); + } + + function noZsmooth(msg) { + zsmooth = trace._input.zsmooth = trace.zsmooth = false; + Lib.notifier("cannot fast-zsmooth: " + msg); + } + + // check whether we really can smooth (ie all boxes are about the same size) + if (zsmooth === "fast") { + if (xa.type === "log" || ya.type === "log") { + noZsmooth("log axis found"); + } else if (!isHist) { + if (x.length) { + var avgdx = (x[x.length - 1] - x[0]) / (x.length - 1), + maxErrX = Math.abs(avgdx / 100); + for (i = 0; i < x.length - 1; i++) { + if (Math.abs(x[i + 1] - x[i] - avgdx) > maxErrX) { + noZsmooth("x scale is not linear"); + break; + } } - } - - function noZsmooth(msg) { - zsmooth = trace._input.zsmooth = trace.zsmooth = false; - Lib.notifier('cannot fast-zsmooth: ' + msg); - } - - // check whether we really can smooth (ie all boxes are about the same size) - if(zsmooth === 'fast') { - if(xa.type === 'log' || ya.type === 'log') { - noZsmooth('log axis found'); - } - else if(!isHist) { - if(x.length) { - var avgdx = (x[x.length - 1] - x[0]) / (x.length - 1), - maxErrX = Math.abs(avgdx / 100); - for(i = 0; i < x.length - 1; i++) { - if(Math.abs(x[i + 1] - x[i] - avgdx) > maxErrX) { - noZsmooth('x scale is not linear'); - break; - } - } - } - if(y.length && zsmooth === 'fast') { - var avgdy = (y[y.length - 1] - y[0]) / (y.length - 1), - maxErrY = Math.abs(avgdy / 100); - for(i = 0; i < y.length - 1; i++) { - if(Math.abs(y[i + 1] - y[i] - avgdy) > maxErrY) { - noZsmooth('y scale is not linear'); - break; - } - } - } + } + if (y.length && zsmooth === "fast") { + var avgdy = (y[y.length - 1] - y[0]) / (y.length - 1), + maxErrY = Math.abs(avgdy / 100); + for (i = 0; i < y.length - 1; i++) { + if (Math.abs(y[i + 1] - y[i] - avgdy) > maxErrY) { + noZsmooth("y scale is not linear"); + break; + } } + } } - - // create arrays of brick boundaries, to be used by autorange and heatmap.plot - var xlen = maxRowLength(z), - xIn = trace.xtype === 'scaled' ? '' : x, - xArray = makeBoundArray(trace, xIn, x0, dx, xlen, xa), - yIn = trace.ytype === 'scaled' ? '' : y, - yArray = makeBoundArray(trace, yIn, y0, dy, z.length, ya); - - // handled in gl2d convert step - if(!isGL2D) { - Axes.expand(xa, xArray); - Axes.expand(ya, yArray); - } - - var cd0 = {x: xArray, y: yArray, z: z}; - - // auto-z and autocolorscale if applicable - colorscaleCalc(trace, z, '', 'z'); - - if(isContour && trace.contours && trace.contours.coloring === 'heatmap') { - var dummyTrace = { - type: trace.type === 'contour' ? 'heatmap' : 'histogram2d', - xcalendar: trace.xcalendar, - ycalendar: trace.ycalendar - }; - cd0.xfill = makeBoundArray(dummyTrace, xIn, x0, dx, xlen, xa); - cd0.yfill = makeBoundArray(dummyTrace, yIn, y0, dy, z.length, ya); - } - - return [cd0]; + } + + // create arrays of brick boundaries, to be used by autorange and heatmap.plot + var xlen = maxRowLength(z), + xIn = trace.xtype === "scaled" ? "" : x, + xArray = makeBoundArray(trace, xIn, x0, dx, xlen, xa), + yIn = trace.ytype === "scaled" ? "" : y, + yArray = makeBoundArray(trace, yIn, y0, dy, z.length, ya); + + // handled in gl2d convert step + if (!isGL2D) { + Axes.expand(xa, xArray); + Axes.expand(ya, yArray); + } + + var cd0 = { x: xArray, y: yArray, z: z }; + + // auto-z and autocolorscale if applicable + colorscaleCalc(trace, z, "", "z"); + + if (isContour && trace.contours && trace.contours.coloring === "heatmap") { + var dummyTrace = { + type: trace.type === "contour" ? "heatmap" : "histogram2d", + xcalendar: trace.xcalendar, + ycalendar: trace.ycalendar + }; + cd0.xfill = makeBoundArray(dummyTrace, xIn, x0, dx, xlen, xa); + cd0.yfill = makeBoundArray(dummyTrace, yIn, y0, dy, z.length, ya); + } + + return [cd0]; }; diff --git a/src/traces/heatmap/clean_2d_array.js b/src/traces/heatmap/clean_2d_array.js index 91a37380094..29c9014aa36 100644 --- a/src/traces/heatmap/clean_2d_array.js +++ b/src/traces/heatmap/clean_2d_array.js @@ -5,39 +5,48 @@ * 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 isNumeric = require('fast-isnumeric'); +"use strict"; +var isNumeric = require("fast-isnumeric"); module.exports = function clean2dArray(zOld, transpose) { - var rowlen, collen, getCollen, old2new, i, j; + var rowlen, collen, getCollen, old2new, i, j; - function cleanZvalue(v) { - if(!isNumeric(v)) return undefined; - return +v; - } + function cleanZvalue(v) { + if (!isNumeric(v)) return undefined; + return +v; + } - if(transpose) { - rowlen = 0; - for(i = 0; i < zOld.length; i++) rowlen = Math.max(rowlen, zOld[i].length); - if(rowlen === 0) return false; - getCollen = function(zOld) { return zOld.length; }; - old2new = function(zOld, i, j) { return zOld[j][i]; }; + if (transpose) { + rowlen = 0; + for (i = 0; i < zOld.length; i++) { + rowlen = Math.max(rowlen, zOld[i].length); } - else { - rowlen = zOld.length; - getCollen = function(zOld, i) { return zOld[i].length; }; - old2new = function(zOld, i, j) { return zOld[i][j]; }; - } - - var zNew = new Array(rowlen); - - for(i = 0; i < rowlen; i++) { - collen = getCollen(zOld, i); - zNew[i] = new Array(collen); - for(j = 0; j < collen; j++) zNew[i][j] = cleanZvalue(old2new(zOld, i, j)); + if (rowlen === 0) return false; + getCollen = function(zOld) { + return zOld.length; + }; + old2new = function(zOld, i, j) { + return zOld[j][i]; + }; + } else { + rowlen = zOld.length; + getCollen = function(zOld, i) { + return zOld[i].length; + }; + old2new = function(zOld, i, j) { + return zOld[i][j]; + }; + } + + var zNew = new Array(rowlen); + + for (i = 0; i < rowlen; i++) { + collen = getCollen(zOld, i); + zNew[i] = new Array(collen); + for (j = 0; j < collen; j++) { + zNew[i][j] = cleanZvalue(old2new(zOld, i, j)); } + } - return zNew; + return zNew; }; diff --git a/src/traces/heatmap/colorbar.js b/src/traces/heatmap/colorbar.js index 147d2672b21..13d0c9c3aa0 100644 --- a/src/traces/heatmap/colorbar.js +++ b/src/traces/heatmap/colorbar.js @@ -5,45 +5,38 @@ * 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 isNumeric = require("fast-isnumeric"); - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Plots = require('../../plots/plots'); -var Colorscale = require('../../components/colorscale'); -var drawColorbar = require('../../components/colorbar/draw'); - +var Lib = require("../../lib"); +var Plots = require("../../plots/plots"); +var Colorscale = require("../../components/colorscale"); +var drawColorbar = require("../../components/colorbar/draw"); module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - cbId = 'cb' + trace.uid, - zmin = trace.zmin, - zmax = trace.zmax; - - if(!isNumeric(zmin)) zmin = Lib.aggNums(Math.min, null, trace.z); - if(!isNumeric(zmax)) zmax = Lib.aggNums(Math.max, null, trace.z); - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - if(!trace.showscale) { - Plots.autoMargin(gd, cbId); - return; - } - - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - trace.colorscale, - zmin, - zmax - ), - { noNumericCheck: true } - ); - - cb.fillcolor(sclFunc) - .filllevels({start: zmin, end: zmax, size: (zmax - zmin) / 254}) - .options(trace.colorbar)(); + var trace = cd[0].trace, + cbId = "cb" + trace.uid, + zmin = trace.zmin, + zmax = trace.zmax; + + if (!isNumeric(zmin)) zmin = Lib.aggNums(Math.min, null, trace.z); + if (!isNumeric(zmax)) zmax = Lib.aggNums(Math.max, null, trace.z); + + gd._fullLayout._infolayer.selectAll("." + cbId).remove(); + + if (!trace.showscale) { + Plots.autoMargin(gd, cbId); + return; + } + + var cb = cd[0].t.cb = drawColorbar(gd, cbId); + var sclFunc = Colorscale.makeColorScaleFunc( + Colorscale.extractScale(trace.colorscale, zmin, zmax), + { noNumericCheck: true } + ); + + cb + .fillcolor(sclFunc) + .filllevels({ start: zmin, end: zmax, size: (zmax - zmin) / 254 }) + .options(trace.colorbar)(); }; diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index c118de79f12..6901061914e 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -5,53 +5,49 @@ * 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'); - +"use strict"; +var Lib = require("../../lib"); module.exports = function convertColumnXYZ(trace, xa, ya) { - var xCol = trace.x.slice(), - yCol = trace.y.slice(), - zCol = trace.z, - textCol = trace.text, - colLen = Math.min(xCol.length, yCol.length, zCol.length), - hasColumnText = (textCol !== undefined && !Array.isArray(textCol[0])), - xcalendar = trace.xcalendar, - ycalendar = trace.ycalendar; - - var i; - - if(colLen < xCol.length) xCol = xCol.slice(0, colLen); - if(colLen < yCol.length) yCol = yCol.slice(0, colLen); - - for(i = 0; i < colLen; i++) { - xCol[i] = xa.d2c(xCol[i], 0, xcalendar); - yCol[i] = ya.d2c(yCol[i], 0, ycalendar); - } - - var xColdv = Lib.distinctVals(xCol), - x = xColdv.vals, - yColdv = Lib.distinctVals(yCol), - y = yColdv.vals, - z = Lib.init2dArray(y.length, x.length); - - var ix, iy, text; - - if(hasColumnText) text = Lib.init2dArray(y.length, x.length); - - for(i = 0; i < colLen; i++) { - ix = Lib.findBin(xCol[i] + xColdv.minDiff / 2, x); - iy = Lib.findBin(yCol[i] + yColdv.minDiff / 2, y); - - z[iy][ix] = zCol[i]; - if(hasColumnText) text[iy][ix] = textCol[i]; - } - - trace.x = x; - trace.y = y; - trace.z = z; - if(hasColumnText) trace.text = text; + var xCol = trace.x.slice(), + yCol = trace.y.slice(), + zCol = trace.z, + textCol = trace.text, + colLen = Math.min(xCol.length, yCol.length, zCol.length), + hasColumnText = textCol !== undefined && !Array.isArray(textCol[0]), + xcalendar = trace.xcalendar, + ycalendar = trace.ycalendar; + + var i; + + if (colLen < xCol.length) xCol = xCol.slice(0, colLen); + if (colLen < yCol.length) yCol = yCol.slice(0, colLen); + + for (i = 0; i < colLen; i++) { + xCol[i] = xa.d2c(xCol[i], 0, xcalendar); + yCol[i] = ya.d2c(yCol[i], 0, ycalendar); + } + + var xColdv = Lib.distinctVals(xCol), + x = xColdv.vals, + yColdv = Lib.distinctVals(yCol), + y = yColdv.vals, + z = Lib.init2dArray(y.length, x.length); + + var ix, iy, text; + + if (hasColumnText) text = Lib.init2dArray(y.length, x.length); + + for (i = 0; i < colLen; i++) { + ix = Lib.findBin(xCol[i] + xColdv.minDiff / 2, x); + iy = Lib.findBin(yCol[i] + yColdv.minDiff / 2, y); + + z[iy][ix] = zCol[i]; + if (hasColumnText) text[iy][ix] = textCol[i]; + } + + trace.x = x; + trace.y = y; + trace.z = z; + if (hasColumnText) trace.text = text; }; diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js index 3dbbaa0f380..d571fd7b958 100644 --- a/src/traces/heatmap/defaults.js +++ b/src/traces/heatmap/defaults.js @@ -5,39 +5,43 @@ * 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 hasColumns = require('./has_columns'); -var handleXYZDefaults = require('./xyz_defaults'); -var colorscaleDefaults = require('../../components/colorscale/defaults'); -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); - if(!len) { - traceOut.visible = false; - return; - } - - coerce('text'); - - var zsmooth = coerce('zsmooth'); - if(zsmooth === false) { - // ensure that xgap and ygap are coerced only when zsmooth allows them to have an effect. - coerce('xgap'); - coerce('ygap'); - } - - coerce('connectgaps', hasColumns(traceOut) && (traceOut.zsmooth !== false)); - - colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'}); +"use strict"; +var Lib = require("../../lib"); + +var hasColumns = require("./has_columns"); +var handleXYZDefaults = require("./xyz_defaults"); +var colorscaleDefaults = require("../../components/colorscale/defaults"); +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); + if (!len) { + traceOut.visible = false; + return; + } + + coerce("text"); + + var zsmooth = coerce("zsmooth"); + if (zsmooth === false) { + // ensure that xgap and ygap are coerced only when zsmooth allows them to have an effect. + coerce("xgap"); + coerce("ygap"); + } + + coerce("connectgaps", hasColumns(traceOut) && traceOut.zsmooth !== false); + + colorscaleDefaults(traceIn, traceOut, layout, coerce, { + prefix: "", + cLetter: "z" + }); }; diff --git a/src/traces/heatmap/find_empties.js b/src/traces/heatmap/find_empties.js index 243f566bc05..a3f204bb7b8 100644 --- a/src/traces/heatmap/find_empties.js +++ b/src/traces/heatmap/find_empties.js @@ -5,10 +5,8 @@ * 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 maxRowLength = require('./max_row_length'); +"use strict"; +var maxRowLength = require("./max_row_length"); /* Return a list of empty points in 2D array z * each empty point z[i][j] gives an array [i, j, neighborCount] @@ -18,87 +16,91 @@ var maxRowLength = require('./max_row_length'); * neighbors, and add a fractional neighborCount */ module.exports = function findEmpties(z) { - var empties = [], - neighborHash = {}, - noNeighborList = [], - nextRow = z[0], - row = [], - blank = [0, 0, 0], - rowLength = maxRowLength(z), - prevRow, - i, - j, - thisPt, - p, - neighborCount, - newNeighborHash, - foundNewNeighbors; + var empties = [], + neighborHash = {}, + noNeighborList = [], + nextRow = z[0], + row = [], + blank = [0, 0, 0], + rowLength = maxRowLength(z), + prevRow, + i, + j, + thisPt, + p, + neighborCount, + newNeighborHash, + foundNewNeighbors; - for(i = 0; i < z.length; i++) { - prevRow = row; - row = nextRow; - nextRow = z[i + 1] || []; - for(j = 0; j < rowLength; j++) { - if(row[j] === undefined) { - neighborCount = (row[j - 1] !== undefined ? 1 : 0) + - (row[j + 1] !== undefined ? 1 : 0) + - (prevRow[j] !== undefined ? 1 : 0) + - (nextRow[j] !== undefined ? 1 : 0); + for (i = 0; i < z.length; i++) { + prevRow = row; + row = nextRow; + nextRow = z[i + 1] || []; + for (j = 0; j < rowLength; j++) { + if (row[j] === undefined) { + neighborCount = (row[j - 1] !== undefined ? 1 : 0) + + (row[j + 1] !== undefined ? 1 : 0) + + (prevRow[j] !== undefined ? 1 : 0) + + (nextRow[j] !== undefined ? 1 : 0); - if(neighborCount) { - // for this purpose, don't count off-the-edge points - // as undefined neighbors - if(i === 0) neighborCount++; - if(j === 0) neighborCount++; - if(i === z.length - 1) neighborCount++; - if(j === row.length - 1) neighborCount++; + if (neighborCount) { + // for this purpose, don't count off-the-edge points + // as undefined neighbors + if (i === 0) neighborCount++; + if (j === 0) neighborCount++; + if (i === z.length - 1) neighborCount++; + if (j === row.length - 1) neighborCount++; - // if all neighbors that could exist do, we don't - // need this for finding farther neighbors - if(neighborCount < 4) { - neighborHash[[i, j]] = [i, j, neighborCount]; - } + // if all neighbors that could exist do, we don't + // need this for finding farther neighbors + if (neighborCount < 4) { + neighborHash[[i, j]] = [i, j, neighborCount]; + } - empties.push([i, j, neighborCount]); - } - else noNeighborList.push([i, j]); - } + empties.push([i, j, neighborCount]); + } else { + noNeighborList.push([i, j]); } + } } + } - while(noNeighborList.length) { - newNeighborHash = {}; - foundNewNeighbors = false; + while (noNeighborList.length) { + newNeighborHash = {}; + foundNewNeighbors = false; - // look for cells that now have neighbors but didn't before - for(p = noNeighborList.length - 1; p >= 0; p--) { - thisPt = noNeighborList[p]; - i = thisPt[0]; - j = thisPt[1]; + // look for cells that now have neighbors but didn't before + for (p = noNeighborList.length - 1; p >= 0; p--) { + thisPt = noNeighborList[p]; + i = thisPt[0]; + j = thisPt[1]; - neighborCount = ((neighborHash[[i - 1, j]] || blank)[2] + - (neighborHash[[i + 1, j]] || blank)[2] + - (neighborHash[[i, j - 1]] || blank)[2] + - (neighborHash[[i, j + 1]] || blank)[2]) / 20; + neighborCount = ((neighborHash[[i - 1, j]] || blank)[2] + + (neighborHash[[i + 1, j]] || blank)[2] + + (neighborHash[[i, j - 1]] || blank)[2] + + (neighborHash[[i, j + 1]] || blank)[2]) / + 20; - if(neighborCount) { - newNeighborHash[thisPt] = [i, j, neighborCount]; - noNeighborList.splice(p, 1); - foundNewNeighbors = true; - } - } + if (neighborCount) { + newNeighborHash[thisPt] = [i, j, neighborCount]; + noNeighborList.splice(p, 1); + foundNewNeighbors = true; + } + } - if(!foundNewNeighbors) { - throw 'findEmpties iterated with no new neighbors'; - } + if (!foundNewNeighbors) { + throw "findEmpties iterated with no new neighbors"; + } - // put these new cells into the main neighbor list - for(thisPt in newNeighborHash) { - neighborHash[thisPt] = newNeighborHash[thisPt]; - empties.push(newNeighborHash[thisPt]); - } + // put these new cells into the main neighbor list + for (thisPt in newNeighborHash) { + neighborHash[thisPt] = newNeighborHash[thisPt]; + empties.push(newNeighborHash[thisPt]); } + } - // sort the full list in descending order of neighbor count - return empties.sort(function(a, b) { return b[2] - a[2]; }); + // sort the full list in descending order of neighbor count + return empties.sort(function(a, b) { + return b[2] - a[2]; + }); }; diff --git a/src/traces/heatmap/has_columns.js b/src/traces/heatmap/has_columns.js index f8909d1249f..becfd6d765f 100644 --- a/src/traces/heatmap/has_columns.js +++ b/src/traces/heatmap/has_columns.js @@ -5,10 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; module.exports = function(trace) { - return !Array.isArray(trace.z[0]); + return !Array.isArray(trace.z[0]); }; diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js index 89a229a4e10..7dbf43d74a7 100644 --- a/src/traces/heatmap/hover.js +++ b/src/traces/heatmap/hover.js @@ -5,112 +5,118 @@ * 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 Fx = require("../../plots/cartesian/graph_interact"); +var Lib = require("../../lib"); +var MAXDIST = require("../../plots/cartesian/constants").MAXDIST; -'use strict'; +module.exports = function hoverPoints( + pointData, + xval, + yval, + hovermode, + contour +) { + // never let a heatmap override another type as closest point + if (pointData.distance < MAXDIST) return; -var Fx = require('../../plots/cartesian/graph_interact'); -var Lib = require('../../lib'); + var cd0 = pointData.cd[0], + trace = cd0.trace, + xa = pointData.xa, + ya = pointData.ya, + x = cd0.x, + y = cd0.y, + z = cd0.z, + zmask = cd0.zmask, + x2 = x, + y2 = y, + xl, + yl, + nx, + ny; -var MAXDIST = require('../../plots/cartesian/constants').MAXDIST; - - -module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour) { - // never let a heatmap override another type as closest point - if(pointData.distance < MAXDIST) return; - - var cd0 = pointData.cd[0], - trace = cd0.trace, - xa = pointData.xa, - ya = pointData.ya, - x = cd0.x, - y = cd0.y, - z = cd0.z, - zmask = cd0.zmask, - x2 = x, - y2 = y, - xl, - yl, - nx, - ny; - - if(pointData.index !== false) { - try { - nx = Math.round(pointData.index[1]); - ny = Math.round(pointData.index[0]); - } - catch(e) { - Lib.error('Error hovering on heatmap, ' + - 'pointNumber must be [row,col], found:', pointData.index); - return; - } - if(nx < 0 || nx >= z[0].length || ny < 0 || ny > z.length) { - return; - } + if (pointData.index !== false) { + try { + nx = Math.round(pointData.index[1]); + ny = Math.round(pointData.index[0]); + } catch (e) { + Lib.error( + "Error hovering on heatmap, " + "pointNumber must be [row,col], found:", + pointData.index + ); + return; } - else if(Fx.inbox(xval - x[0], xval - x[x.length - 1]) > MAXDIST || - Fx.inbox(yval - y[0], yval - y[y.length - 1]) > MAXDIST) { - return; + if (nx < 0 || nx >= z[0].length || ny < 0 || ny > z.length) { + return; } - else { - if(contour) { - var i2; - x2 = [2 * x[0] - x[1]]; + } else if ( + Fx.inbox(xval - x[0], xval - x[x.length - 1]) > MAXDIST || + Fx.inbox(yval - y[0], yval - y[y.length - 1]) > MAXDIST + ) { + return; + } else { + if (contour) { + var i2; + x2 = [2 * x[0] - x[1]]; - for(i2 = 1; i2 < x.length; i2++) { - x2.push((x[i2] + x[i2 - 1]) / 2); - } - x2.push([2 * x[x.length - 1] - x[x.length - 2]]); + for (i2 = 1; i2 < x.length; i2++) { + x2.push((x[i2] + x[i2 - 1]) / 2); + } + x2.push([2 * x[x.length - 1] - x[x.length - 2]]); - y2 = [2 * y[0] - y[1]]; - for(i2 = 1; i2 < y.length; i2++) { - y2.push((y[i2] + y[i2 - 1]) / 2); - } - y2.push([2 * y[y.length - 1] - y[y.length - 2]]); - } - nx = Math.max(0, Math.min(x2.length - 2, Lib.findBin(xval, x2))); - ny = Math.max(0, Math.min(y2.length - 2, Lib.findBin(yval, y2))); + y2 = [2 * y[0] - y[1]]; + for (i2 = 1; i2 < y.length; i2++) { + y2.push((y[i2] + y[i2 - 1]) / 2); + } + y2.push([2 * y[y.length - 1] - y[y.length - 2]]); } + nx = Math.max(0, Math.min(x2.length - 2, Lib.findBin(xval, x2))); + ny = Math.max(0, Math.min(y2.length - 2, Lib.findBin(yval, y2))); + } - var x0 = xa.c2p(x[nx]), - x1 = xa.c2p(x[nx + 1]), - y0 = ya.c2p(y[ny]), - y1 = ya.c2p(y[ny + 1]); + var x0 = xa.c2p(x[nx]), + x1 = xa.c2p(x[nx + 1]), + y0 = ya.c2p(y[ny]), + y1 = ya.c2p(y[ny + 1]); - if(contour) { - x1 = x0; - xl = x[nx]; - y1 = y0; - yl = y[ny]; - } - else { - xl = (x[nx] + x[nx + 1]) / 2; - yl = (y[ny] + y[ny + 1]) / 2; - if(trace.zsmooth) { - x0 = x1 = (x0 + x1) / 2; - y0 = y1 = (y0 + y1) / 2; - } + if (contour) { + x1 = x0; + xl = x[nx]; + y1 = y0; + yl = y[ny]; + } else { + xl = (x[nx] + x[nx + 1]) / 2; + yl = (y[ny] + y[ny + 1]) / 2; + if (trace.zsmooth) { + x0 = x1 = (x0 + x1) / 2; + y0 = y1 = (y0 + y1) / 2; } + } - var zVal = z[ny][nx]; - if(zmask && !zmask[ny][nx]) zVal = undefined; + var zVal = z[ny][nx]; + if (zmask && !zmask[ny][nx]) zVal = undefined; - var text; - if(Array.isArray(trace.text) && Array.isArray(trace.text[ny])) { - text = trace.text[ny][nx]; - } + var text; + if (Array.isArray(trace.text) && Array.isArray(trace.text[ny])) { + text = trace.text[ny][nx]; + } - return [Lib.extendFlat(pointData, { - index: [ny, nx], - // never let a 2D override 1D type as closest point - distance: MAXDIST + 10, - x0: x0, - x1: x1, - y0: y0, - y1: y1, - xLabelVal: xl, - yLabelVal: yl, - zLabelVal: zVal, - text: text - })]; + return [ + Lib.extendFlat(pointData, { + index: [ny, nx], + // never let a 2D override 1D type as closest point + distance: ( + MAXDIST + 10 + ), + x0: x0, + x1: x1, + y0: y0, + y1: y1, + xLabelVal: xl, + yLabelVal: yl, + zLabelVal: zVal, + text: text + }) + ]; }; diff --git a/src/traces/heatmap/index.js b/src/traces/heatmap/index.js index b3a1da2269a..12926651ee6 100644 --- a/src/traces/heatmap/index.js +++ b/src/traces/heatmap/index.js @@ -5,49 +5,44 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var Heatmap = {}; -Heatmap.attributes = require('./attributes'); -Heatmap.supplyDefaults = require('./defaults'); -Heatmap.calc = require('./calc'); -Heatmap.plot = require('./plot'); -Heatmap.colorbar = require('./colorbar'); -Heatmap.style = require('./style'); -Heatmap.hoverPoints = require('./hover'); - -Heatmap.moduleType = 'trace'; -Heatmap.name = 'heatmap'; -Heatmap.basePlotModule = require('../../plots/cartesian'); -Heatmap.categories = ['cartesian', '2dMap']; +Heatmap.attributes = require("./attributes"); +Heatmap.supplyDefaults = require("./defaults"); +Heatmap.calc = require("./calc"); +Heatmap.plot = require("./plot"); +Heatmap.colorbar = require("./colorbar"); +Heatmap.style = require("./style"); +Heatmap.hoverPoints = require("./hover"); + +Heatmap.moduleType = "trace"; +Heatmap.name = "heatmap"; +Heatmap.basePlotModule = require("../../plots/cartesian"); +Heatmap.categories = ["cartesian", "2dMap"]; Heatmap.meta = { - description: [ - 'The data that describes the heatmap value-to-color mapping', - 'is set in `z`.', - 'Data in `z` can either be a {2D array} of values (ragged or not)', - 'or a 1D array of values.', - - 'In the case where `z` is a {2D array},', - 'say that `z` has N rows and M columns.', - 'Then, by default, the resulting heatmap will have N partitions along', - 'the y axis and M partitions along the x axis.', - 'In other words, the i-th row/ j-th column cell in `z`', - 'is mapped to the i-th partition of the y axis', - '(starting from the bottom of the plot) and the j-th partition', - 'of the x-axis (starting from the left of the plot).', - 'This behavior can be flipped by using `transpose`.', - 'Moreover, `x` (`y`) can be provided with M or M+1 (N or N+1) elements.', - 'If M (N), then the coordinates correspond to the center of the', - 'heatmap cells and the cells have equal width.', - 'If M+1 (N+1), then the coordinates correspond to the edges of the', - 'heatmap cells.', - - 'In the case where `z` is a 1D {array}, the x and y coordinates must be', - 'provided in `x` and `y` respectively to form data triplets.' - ].join(' ') + description: [ + "The data that describes the heatmap value-to-color mapping", + "is set in `z`.", + "Data in `z` can either be a {2D array} of values (ragged or not)", + "or a 1D array of values.", + "In the case where `z` is a {2D array},", + "say that `z` has N rows and M columns.", + "Then, by default, the resulting heatmap will have N partitions along", + "the y axis and M partitions along the x axis.", + "In other words, the i-th row/ j-th column cell in `z`", + "is mapped to the i-th partition of the y axis", + "(starting from the bottom of the plot) and the j-th partition", + "of the x-axis (starting from the left of the plot).", + "This behavior can be flipped by using `transpose`.", + "Moreover, `x` (`y`) can be provided with M or M+1 (N or N+1) elements.", + "If M (N), then the coordinates correspond to the center of the", + "heatmap cells and the cells have equal width.", + "If M+1 (N+1), then the coordinates correspond to the edges of the", + "heatmap cells.", + "In the case where `z` is a 1D {array}, the x and y coordinates must be", + "provided in `x` and `y` respectively to form data triplets." + ].join(" ") }; module.exports = Heatmap; diff --git a/src/traces/heatmap/interp2d.js b/src/traces/heatmap/interp2d.js index 3676a9a9f7b..a5a5baa152e 100644 --- a/src/traces/heatmap/interp2d.js +++ b/src/traces/heatmap/interp2d.js @@ -5,126 +5,123 @@ * 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"); -'use strict'; - -var Lib = require('../../lib'); - -var INTERPTHRESHOLD = 1e-2, - NEIGHBORSHIFTS = [[-1, 0], [1, 0], [0, -1], [0, 1]]; +var INTERPTHRESHOLD = 1e-2, NEIGHBORSHIFTS = [[-1, 0], [1, 0], [0, -1], [0, 1]]; function correctionOvershoot(maxFractionalChange) { - // start with less overshoot, until we know it's converging, - // then ramp up the overshoot for faster convergence - return 0.5 - 0.25 * Math.min(1, maxFractionalChange * 0.5); + // start with less overshoot, until we know it's converging, + // then ramp up the overshoot for faster convergence + return 0.5 - 0.25 * Math.min(1, maxFractionalChange * 0.5); } module.exports = function interp2d(z, emptyPoints, savedInterpZ) { - // fill in any missing data in 2D array z using an iterative - // poisson equation solver with zero-derivative BC at edges - // amazingly, this just amounts to repeatedly averaging all the existing - // nearest neighbors (at least if we don't take x/y scaling into account) - var maxFractionalChange = 1, - i, - thisPt; - - if(Array.isArray(savedInterpZ)) { - for(i = 0; i < emptyPoints.length; i++) { - thisPt = emptyPoints[i]; - z[thisPt[0]][thisPt[1]] = savedInterpZ[thisPt[0]][thisPt[1]]; - } - } - else { - // one pass to fill in a starting value for all the empties - iterateInterp2d(z, emptyPoints); - } - - // we're don't need to iterate lone empties - remove them - for(i = 0; i < emptyPoints.length; i++) { - if(emptyPoints[i][2] < 4) break; - } - // but don't remove these points from the original array, - // we'll use them for masking, so make a copy. - emptyPoints = emptyPoints.slice(i); - - for(i = 0; i < 100 && maxFractionalChange > INTERPTHRESHOLD; i++) { - maxFractionalChange = iterateInterp2d(z, emptyPoints, - correctionOvershoot(maxFractionalChange)); - } - if(maxFractionalChange > INTERPTHRESHOLD) { - Lib.log('interp2d didn\'t converge quickly', maxFractionalChange); + // fill in any missing data in 2D array z using an iterative + // poisson equation solver with zero-derivative BC at edges + // amazingly, this just amounts to repeatedly averaging all the existing + // nearest neighbors (at least if we don't take x/y scaling into account) + var maxFractionalChange = 1, i, thisPt; + + if (Array.isArray(savedInterpZ)) { + for (i = 0; i < emptyPoints.length; i++) { + thisPt = emptyPoints[i]; + z[thisPt[0]][thisPt[1]] = savedInterpZ[thisPt[0]][thisPt[1]]; } - - return z; + } else { + // one pass to fill in a starting value for all the empties + iterateInterp2d(z, emptyPoints); + } + + // we're don't need to iterate lone empties - remove them + for (i = 0; i < emptyPoints.length; i++) { + if (emptyPoints[i][2] < 4) break; + } + // but don't remove these points from the original array, + // we'll use them for masking, so make a copy. + emptyPoints = emptyPoints.slice(i); + + for (i = 0; i < 100 && maxFractionalChange > INTERPTHRESHOLD; i++) { + maxFractionalChange = iterateInterp2d( + z, + emptyPoints, + correctionOvershoot(maxFractionalChange) + ); + } + if (maxFractionalChange > INTERPTHRESHOLD) { + Lib.log("interp2d didn't converge quickly", maxFractionalChange); + } + + return z; }; function iterateInterp2d(z, emptyPoints, overshoot) { - var maxFractionalChange = 0, - thisPt, - i, - j, - p, - q, - neighborShift, - neighborRow, - neighborVal, - neighborCount, - neighborSum, - initialVal, - minNeighbor, - maxNeighbor; - - for(p = 0; p < emptyPoints.length; p++) { - thisPt = emptyPoints[p]; - i = thisPt[0]; - j = thisPt[1]; - initialVal = z[i][j]; - neighborSum = 0; - neighborCount = 0; - - for(q = 0; q < 4; q++) { - neighborShift = NEIGHBORSHIFTS[q]; - neighborRow = z[i + neighborShift[0]]; - if(!neighborRow) continue; - neighborVal = neighborRow[j + neighborShift[1]]; - if(neighborVal !== undefined) { - if(neighborSum === 0) { - minNeighbor = maxNeighbor = neighborVal; - } - else { - minNeighbor = Math.min(minNeighbor, neighborVal); - maxNeighbor = Math.max(maxNeighbor, neighborVal); - } - neighborCount++; - neighborSum += neighborVal; - } - } - - if(neighborCount === 0) { - throw 'iterateInterp2d order is wrong: no defined neighbors'; + var maxFractionalChange = 0, + thisPt, + i, + j, + p, + q, + neighborShift, + neighborRow, + neighborVal, + neighborCount, + neighborSum, + initialVal, + minNeighbor, + maxNeighbor; + + for (p = 0; p < emptyPoints.length; p++) { + thisPt = emptyPoints[p]; + i = thisPt[0]; + j = thisPt[1]; + initialVal = z[i][j]; + neighborSum = 0; + neighborCount = 0; + + for (q = 0; q < 4; q++) { + neighborShift = NEIGHBORSHIFTS[q]; + neighborRow = z[i + neighborShift[0]]; + if (!neighborRow) continue; + neighborVal = neighborRow[j + neighborShift[1]]; + if (neighborVal !== undefined) { + if (neighborSum === 0) { + minNeighbor = maxNeighbor = neighborVal; + } else { + minNeighbor = Math.min(minNeighbor, neighborVal); + maxNeighbor = Math.max(maxNeighbor, neighborVal); } + neighborCount++; + neighborSum += neighborVal; + } + } - // this is the laplace equation interpolation: - // each point is just the average of its neighbors - // note that this ignores differential x/y scaling - // which I think is the right approach, since we - // don't know what that scaling means - z[i][j] = neighborSum / neighborCount; - - if(initialVal === undefined) { - if(neighborCount < 4) maxFractionalChange = 1; - } - else { - // we can make large empty regions converge faster - // if we overshoot the change vs the previous value - z[i][j] = (1 + overshoot) * z[i][j] - overshoot * initialVal; + if (neighborCount === 0) { + throw "iterateInterp2d order is wrong: no defined neighbors"; + } - if(maxNeighbor > minNeighbor) { - maxFractionalChange = Math.max(maxFractionalChange, - Math.abs(z[i][j] - initialVal) / (maxNeighbor - minNeighbor)); - } - } + // this is the laplace equation interpolation: + // each point is just the average of its neighbors + // note that this ignores differential x/y scaling + // which I think is the right approach, since we + // don't know what that scaling means + z[i][j] = neighborSum / neighborCount; + + if (initialVal === undefined) { + if (neighborCount < 4) maxFractionalChange = 1; + } else { + // we can make large empty regions converge faster + // if we overshoot the change vs the previous value + z[i][j] = (1 + overshoot) * z[i][j] - overshoot * initialVal; + + if (maxNeighbor > minNeighbor) { + maxFractionalChange = Math.max( + maxFractionalChange, + Math.abs(z[i][j] - initialVal) / (maxNeighbor - minNeighbor) + ); + } } + } - return maxFractionalChange; + return maxFractionalChange; } diff --git a/src/traces/heatmap/make_bound_array.js b/src/traces/heatmap/make_bound_array.js index 3617f342ca6..e1e9017791e 100644 --- a/src/traces/heatmap/make_bound_array.js +++ b/src/traces/heatmap/make_bound_array.js @@ -5,76 +5,79 @@ * 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 Registry = require("../../registry"); -'use strict'; +module.exports = function makeBoundArray( + trace, + arrayIn, + v0In, + dvIn, + numbricks, + ax +) { + var arrayOut = [], + isContour = Registry.traceIs(trace, "contour"), + isHist = Registry.traceIs(trace, "histogram"), + isGL2D = Registry.traceIs(trace, "gl2d"), + v0, + dv, + i; -var Registry = require('../../registry'); + var isArrayOfTwoItemsOrMore = Array.isArray(arrayIn) && arrayIn.length > 1; -module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, ax) { - var arrayOut = [], - isContour = Registry.traceIs(trace, 'contour'), - isHist = Registry.traceIs(trace, 'histogram'), - isGL2D = Registry.traceIs(trace, 'gl2d'), - v0, - dv, - i; + if (isArrayOfTwoItemsOrMore && !isHist && ax.type !== "category") { + var len = arrayIn.length; - var isArrayOfTwoItemsOrMore = Array.isArray(arrayIn) && arrayIn.length > 1; + // given vals are brick centers + // hopefully length === numbricks, but use this method even if too few are supplied + // and extend it linearly based on the last two points + if (len <= numbricks) { + // contour plots only want the centers + if (isContour || isGL2D) { + arrayOut = arrayIn.slice(0, numbricks); + } else if (numbricks === 1) { + arrayOut = [arrayIn[0] - 0.5, arrayIn[0] + 0.5]; + } else { + arrayOut = [1.5 * arrayIn[0] - 0.5 * arrayIn[1]]; - if(isArrayOfTwoItemsOrMore && !isHist && (ax.type !== 'category')) { - var len = arrayIn.length; - - // given vals are brick centers - // hopefully length === numbricks, but use this method even if too few are supplied - // and extend it linearly based on the last two points - if(len <= numbricks) { - // contour plots only want the centers - if(isContour || isGL2D) arrayOut = arrayIn.slice(0, numbricks); - else if(numbricks === 1) { - arrayOut = [arrayIn[0] - 0.5, arrayIn[0] + 0.5]; - } - else { - arrayOut = [1.5 * arrayIn[0] - 0.5 * arrayIn[1]]; - - for(i = 1; i < len; i++) { - arrayOut.push((arrayIn[i - 1] + arrayIn[i]) * 0.5); - } + for (i = 1; i < len; i++) { + arrayOut.push((arrayIn[i - 1] + arrayIn[i]) * 0.5); + } - arrayOut.push(1.5 * arrayIn[len - 1] - 0.5 * arrayIn[len - 2]); - } + arrayOut.push(1.5 * arrayIn[len - 1] - 0.5 * arrayIn[len - 2]); + } - if(len < numbricks) { - var lastPt = arrayOut[arrayOut.length - 1], - delta = lastPt - arrayOut[arrayOut.length - 2]; + if (len < numbricks) { + var lastPt = arrayOut[arrayOut.length - 1], + delta = lastPt - arrayOut[arrayOut.length - 2]; - for(i = len; i < numbricks; i++) { - lastPt += delta; - arrayOut.push(lastPt); - } - } - } - else { - // hopefully length === numbricks+1, but do something regardless: - // given vals are brick boundaries - return isContour ? - arrayIn.slice(0, numbricks) : // we must be strict for contours - arrayIn.slice(0, numbricks + 1); + for (i = len; i < numbricks; i++) { + lastPt += delta; + arrayOut.push(lastPt); } + } + } else { + // hopefully length === numbricks+1, but do something regardless: + // given vals are brick boundaries + return isContour // we must be strict for contours + ? arrayIn.slice(0, numbricks) + : arrayIn.slice(0, numbricks + 1); } - else { - dv = dvIn || 1; + } else { + dv = dvIn || 1; - var calendar = trace[ax._id.charAt(0) + 'calendar']; + var calendar = trace[ax._id.charAt(0) + "calendar"]; - if(isHist || ax.type === 'category') v0 = ax.r2c(v0In, 0, calendar) || 0; - else if(Array.isArray(arrayIn) && arrayIn.length === 1) v0 = arrayIn[0]; - else if(v0In === undefined) v0 = 0; - else v0 = ax.d2c(v0In, 0, calendar); + if (isHist || ax.type === "category") v0 = ax.r2c(v0In, 0, calendar) || 0; + else if (Array.isArray(arrayIn) && arrayIn.length === 1) v0 = arrayIn[0]; + else if (v0In === undefined) v0 = 0; + else v0 = ax.d2c(v0In, 0, calendar); - for(i = (isContour || isGL2D) ? 0 : -0.5; i < numbricks; i++) { - arrayOut.push(v0 + dv * i); - } + for (i = isContour || isGL2D ? 0 : -0.5; i < numbricks; i++) { + arrayOut.push(v0 + dv * i); } + } - return arrayOut; + return arrayOut; }; diff --git a/src/traces/heatmap/max_row_length.js b/src/traces/heatmap/max_row_length.js index d35412ca2b8..5e6388667ed 100644 --- a/src/traces/heatmap/max_row_length.js +++ b/src/traces/heatmap/max_row_length.js @@ -5,16 +5,13 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; module.exports = function maxRowLength(z) { - var len = 0; + var len = 0; - for(var i = 0; i < z.length; i++) { - len = Math.max(len, z[i].length); - } + for (var i = 0; i < z.length; i++) { + len = Math.max(len, z[i].length); + } - return len; + return len; }; diff --git a/src/traces/heatmap/plot.js b/src/traces/heatmap/plot.js index 02fbf077ee7..a718d024224 100644 --- a/src/traces/heatmap/plot.js +++ b/src/traces/heatmap/plot.js @@ -5,461 +5,466 @@ * 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 tinycolor = require("tinycolor2"); +var Registry = require("../../registry"); +var Lib = require("../../lib"); +var Colorscale = require("../../components/colorscale"); +var xmlnsNamespaces = require("../../constants/xmlns_namespaces"); -'use strict'; - -var tinycolor = require('tinycolor2'); - -var Registry = require('../../registry'); -var Lib = require('../../lib'); -var Colorscale = require('../../components/colorscale'); -var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); - -var maxRowLength = require('./max_row_length'); - +var maxRowLength = require("./max_row_length"); module.exports = function(gd, plotinfo, cdheatmaps) { - for(var i = 0; i < cdheatmaps.length; i++) { - plotOne(gd, plotinfo, cdheatmaps[i]); - } + for (var i = 0; i < cdheatmaps.length; i++) { + plotOne(gd, plotinfo, cdheatmaps[i]); + } }; // From http://www.xarg.org/2010/03/generate-client-side-png-files-using-javascript/ function plotOne(gd, plotinfo, cd) { - var trace = cd[0].trace, - uid = trace.uid, - xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - fullLayout = gd._fullLayout, - id = 'hm' + uid; - - // in case this used to be a contour map - fullLayout._paper.selectAll('.contour' + uid).remove(); - - if(trace.visible !== true) { - fullLayout._paper.selectAll('.' + id).remove(); - fullLayout._infolayer.selectAll('.cb' + uid).remove(); - return; - } - - var z = cd[0].z, - x = cd[0].x, - y = cd[0].y, - isContour = Registry.traceIs(trace, 'contour'), - zsmooth = isContour ? 'best' : trace.zsmooth, - - // get z dims - m = z.length, - n = maxRowLength(z), - xrev = false, - left, - right, - temp, - yrev = false, - top, - bottom, - i; - - // TODO: if there are multiple overlapping categorical heatmaps, - // or if we allow category sorting, then the categories may not be - // sequential... may need to reorder and/or expand z - - // Get edges of png in pixels (xa.c2p() maps axes coordinates to pixel coordinates) - // figure out if either axis is reversed (y is usually reversed, in pixel coords) - // also clip the image to maximum 50% outside the visible plot area - // bigger image lets you pan more naturally, but slows performance. - // TODO: use low-resolution images outside the visible plot for panning - // these while loops find the first and last brick bounds that are defined - // (in case of log of a negative) - i = 0; - while(left === undefined && i < x.length - 1) { - left = xa.c2p(x[i]); - i++; + var trace = cd[0].trace, + uid = trace.uid, + xa = plotinfo.xaxis, + ya = plotinfo.yaxis, + fullLayout = gd._fullLayout, + id = "hm" + uid; + + // in case this used to be a contour map + fullLayout._paper.selectAll(".contour" + uid).remove(); + + if (trace.visible !== true) { + fullLayout._paper.selectAll("." + id).remove(); + fullLayout._infolayer.selectAll(".cb" + uid).remove(); + return; + } + + var z = cd[0].z, + x = cd[0].x, + y = cd[0].y, + isContour = Registry.traceIs(trace, "contour"), + zsmooth = isContour ? "best" : trace.zsmooth, + // get z dims + m = z.length, + n = maxRowLength(z), + xrev = false, + left, + right, + temp, + yrev = false, + top, + bottom, + i; + + // TODO: if there are multiple overlapping categorical heatmaps, + // or if we allow category sorting, then the categories may not be + // sequential... may need to reorder and/or expand z + // Get edges of png in pixels (xa.c2p() maps axes coordinates to pixel coordinates) + // figure out if either axis is reversed (y is usually reversed, in pixel coords) + // also clip the image to maximum 50% outside the visible plot area + // bigger image lets you pan more naturally, but slows performance. + // TODO: use low-resolution images outside the visible plot for panning + // these while loops find the first and last brick bounds that are defined + // (in case of log of a negative) + i = 0; + while (left === undefined && i < x.length - 1) { + left = xa.c2p(x[i]); + i++; + } + i = x.length - 1; + while (right === undefined && i > 0) { + right = xa.c2p(x[i]); + i--; + } + + if (right < left) { + temp = right; + right = left; + left = temp; + xrev = true; + } + + i = 0; + while (top === undefined && i < y.length - 1) { + top = ya.c2p(y[i]); + i++; + } + i = y.length - 1; + while (bottom === undefined && i > 0) { + bottom = ya.c2p(y[i]); + i--; + } + + if (bottom < top) { + temp = top; + top = bottom; + bottom = temp; + yrev = true; + } + + // for contours with heatmap fill, we generate the boundaries based on + // brick centers but then use the brick edges for drawing the bricks + if (isContour) { + // TODO: for 'best' smoothing, we really should use the given brick + // centers as well as brick bounds in calculating values, in case of + // nonuniform brick sizes + x = cd[0].xfill; + y = cd[0].yfill; + } + + // make an image that goes at most half a screen off either side, to keep + // time reasonable when you zoom in. if zsmooth is true/fast, don't worry + // about this, because zooming doesn't increase number of pixels + // if zsmooth is best, don't include anything off screen because it takes too long + if (zsmooth !== "fast") { + var extra = zsmooth === "best" ? 0 : 0.5; + left = Math.max((-extra) * xa._length, left); + right = Math.min((1 + extra) * xa._length, right); + top = Math.max((-extra) * ya._length, top); + bottom = Math.min((1 + extra) * ya._length, bottom); + } + + var imageWidth = Math.round(right - left), + imageHeight = Math.round(bottom - top); + + // setup image nodes + // if image is entirely off-screen, don't even draw it + var isOffScreen = imageWidth <= 0 || imageHeight <= 0; + + var plotgroup = plotinfo.plot + .select(".imagelayer") + .selectAll("g.hm." + id) + .data(isOffScreen ? [] : [0]); + + plotgroup.enter().append("g").classed("hm", true).classed(id, true); + + plotgroup.exit().remove(); + + if (isOffScreen) return; + + // generate image data + var canvasW, canvasH; + if (zsmooth === "fast") { + canvasW = n; + canvasH = m; + } else { + canvasW = imageWidth; + canvasH = imageHeight; + } + + var canvas = document.createElement("canvas"); + canvas.width = canvasW; + canvas.height = canvasH; + var context = canvas.getContext("2d"); + + var sclFunc = Colorscale.makeColorScaleFunc( + Colorscale.extractScale(trace.colorscale, trace.zmin, trace.zmax), + { noNumericCheck: true, returnArray: true } + ); + + // map brick boundaries to image pixels + var xpx, ypx; + if (zsmooth === "fast") { + xpx = xrev + ? (function(index) { + return n - 1 - index; + }) + : Lib.identity; + ypx = yrev + ? (function(index) { + return m - 1 - index; + }) + : Lib.identity; + } else { + xpx = function(index) { + return Lib.constrain(Math.round(xa.c2p(x[index]) - left), 0, imageWidth); + }; + ypx = function(index) { + return Lib.constrain(Math.round(ya.c2p(y[index]) - top), 0, imageHeight); + }; + } + + // get interpolated bin value. Returns {bin0:closest bin, frac:fractional dist to next, bin1:next bin} + function findInterp(pixel, pixArray) { + var maxbin = pixArray.length - 2, + bin = Lib.constrain(Lib.findBin(pixel, pixArray), 0, maxbin), + pix0 = pixArray[bin], + pix1 = pixArray[bin + 1], + interp = Lib.constrain( + bin + (pixel - pix0) / (pix1 - pix0) - 0.5, + 0, + maxbin + ), + bin0 = Math.round(interp), + frac = Math.abs(interp - bin0); + + if (!interp || interp === maxbin || !frac) { + return { bin0: bin0, bin1: bin0, frac: 0 }; } - i = x.length - 1; - while(right === undefined && i > 0) { - right = xa.c2p(x[i]); - i--; + return { + bin0: bin0, + frac: frac, + bin1: Math.round(bin0 + frac / (interp - bin0)) + }; + } + + // build the pixel map brick-by-brick + // cruise through z-matrix row-by-row + // build a brick at each z-matrix value + var yi = ypx(0), + yb = [yi, yi], + xbi = xrev ? 0 : 1, + ybi = yrev ? 0 : 1, + // for collecting an average luminosity of the heatmap + pixcount = 0, + rcount = 0, + gcount = 0, + bcount = 0, + brickWithPadding, + xb, + j, + xi, + v, + row, + c; + + function applyBrickPadding( + trace, + x0, + x1, + y0, + y1, + xIndex, + xLength, + yIndex, + yLength + ) { + var padding = { x0: x0, x1: x1, y0: y0, y1: y1 }, + xEdgeGap = trace.xgap * 2 / 3, + yEdgeGap = trace.ygap * 2 / 3, + xCenterGap = trace.xgap / 3, + yCenterGap = trace.ygap / 3; + + if (yIndex === yLength - 1) { + // top edge brick + padding.y1 = y1 - yEdgeGap; } - if(right < left) { - temp = right; - right = left; - left = temp; - xrev = true; + if (xIndex === xLength - 1) { + // right edge brick + padding.x0 = x0 + xEdgeGap; } - i = 0; - while(top === undefined && i < y.length - 1) { - top = ya.c2p(y[i]); - i++; - } - i = y.length - 1; - while(bottom === undefined && i > 0) { - bottom = ya.c2p(y[i]); - i--; + if (yIndex === 0) { + // bottom edge brick + padding.y0 = y0 + yEdgeGap; } - if(bottom < top) { - temp = top; - top = bottom; - bottom = temp; - yrev = true; + if (xIndex === 0) { + // left edge brick + padding.x1 = x1 - xEdgeGap; } - // for contours with heatmap fill, we generate the boundaries based on - // brick centers but then use the brick edges for drawing the bricks - if(isContour) { - // TODO: for 'best' smoothing, we really should use the given brick - // centers as well as brick bounds in calculating values, in case of - // nonuniform brick sizes - x = cd[0].xfill; - y = cd[0].yfill; + if (xIndex > 0 && xIndex < xLength - 1) { + // brick in the center along x + padding.x0 = x0 + xCenterGap; + padding.x1 = x1 - xCenterGap; } - // make an image that goes at most half a screen off either side, to keep - // time reasonable when you zoom in. if zsmooth is true/fast, don't worry - // about this, because zooming doesn't increase number of pixels - // if zsmooth is best, don't include anything off screen because it takes too long - if(zsmooth !== 'fast') { - var extra = zsmooth === 'best' ? 0 : 0.5; - left = Math.max(-extra * xa._length, left); - right = Math.min((1 + extra) * xa._length, right); - top = Math.max(-extra * ya._length, top); - bottom = Math.min((1 + extra) * ya._length, bottom); + if (yIndex > 0 && yIndex < yLength - 1) { + // brick in the center along y + padding.y0 = y0 + yCenterGap; + padding.y1 = y1 - yCenterGap; } - var imageWidth = Math.round(right - left), - imageHeight = Math.round(bottom - top); - - // setup image nodes - - // if image is entirely off-screen, don't even draw it - var isOffScreen = (imageWidth <= 0 || imageHeight <= 0); - - var plotgroup = plotinfo.plot.select('.imagelayer') - .selectAll('g.hm.' + id) - .data(isOffScreen ? [] : [0]); - - plotgroup.enter().append('g') - .classed('hm', true) - .classed(id, true); - - plotgroup.exit().remove(); - - if(isOffScreen) return; - - // generate image data - - var canvasW, canvasH; - if(zsmooth === 'fast') { - canvasW = n; - canvasH = m; - } else { - canvasW = imageWidth; - canvasH = imageHeight; + return padding; + } + + function setColor(v, pixsize) { + if (v !== undefined) { + var c = sclFunc(v); + c[0] = Math.round(c[0]); + c[1] = Math.round(c[1]); + c[2] = Math.round(c[2]); + + pixcount += pixsize; + rcount += c[0] * pixsize; + gcount += c[1] * pixsize; + bcount += c[2] * pixsize; + return c; } - - var canvas = document.createElement('canvas'); - canvas.width = canvasW; - canvas.height = canvasH; - var context = canvas.getContext('2d'); - - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - trace.colorscale, - trace.zmin, - trace.zmax - ), - { noNumericCheck: true, returnArray: true } + return [0, 0, 0, 0]; + } + + function putColor(pixels, pxIndex, c) { + pixels[pxIndex] = c[0]; + pixels[pxIndex + 1] = c[1]; + pixels[pxIndex + 2] = c[2]; + pixels[pxIndex + 3] = Math.round(c[3] * 255); + } + + function interpColor(r0, r1, xinterp, yinterp) { + var z00 = r0[xinterp.bin0]; + if (z00 === undefined) return setColor(undefined, 1); + + var z01 = r0[xinterp.bin1], + z10 = r1[xinterp.bin0], + z11 = r1[xinterp.bin1], + dx = z01 - z00 || 0, + dy = z10 - z00 || 0, + dxy; + + // the bilinear interpolation term needs different calculations + // for all the different permutations of missing data + // among the neighbors of the main point, to ensure + // continuity across brick boundaries. + if (z01 === undefined) { + if (z11 === undefined) dxy = 0; + else if (z10 === undefined) dxy = 2 * (z11 - z00); + else dxy = (2 * z11 - z10 - z00) * 2 / 3; + } else if (z11 === undefined) { + if (z10 === undefined) dxy = 0; + else dxy = (2 * z00 - z01 - z10) * 2 / 3; + } else if (z10 === undefined) dxy = (2 * z11 - z01 - z00) * 2 / 3; + else dxy = z11 + z00 - z01 - z10; + + return setColor( + z00 + xinterp.frac * dx + yinterp.frac * (dy + xinterp.frac * dxy) ); + } - // map brick boundaries to image pixels - var xpx, - ypx; - if(zsmooth === 'fast') { - xpx = xrev ? - function(index) { return n - 1 - index; } : - Lib.identity; - ypx = yrev ? - function(index) { return m - 1 - index; } : - Lib.identity; - } - else { - xpx = function(index) { - return Lib.constrain(Math.round(xa.c2p(x[index]) - left), - 0, imageWidth); - }; - ypx = function(index) { - return Lib.constrain(Math.round(ya.c2p(y[index]) - top), - 0, imageHeight); - }; - } + if (zsmooth) { + // best or fast, works fastest with imageData + var pxIndex = 0, pixels; - // get interpolated bin value. Returns {bin0:closest bin, frac:fractional dist to next, bin1:next bin} - function findInterp(pixel, pixArray) { - var maxbin = pixArray.length - 2, - bin = Lib.constrain(Lib.findBin(pixel, pixArray), 0, maxbin), - pix0 = pixArray[bin], - pix1 = pixArray[bin + 1], - interp = Lib.constrain(bin + (pixel - pix0) / (pix1 - pix0) - 0.5, 0, maxbin), - bin0 = Math.round(interp), - frac = Math.abs(interp - bin0); - - if(!interp || interp === maxbin || !frac) { - return { - bin0: bin0, - bin1: bin0, - frac: 0 - }; - } - return { - bin0: bin0, - frac: frac, - bin1: Math.round(bin0 + frac / (interp - bin0)) - }; + try { + pixels = new Uint8Array(imageWidth * imageHeight * 4); + } catch (e) { + pixels = new Array(imageWidth * imageHeight * 4); } - // build the pixel map brick-by-brick - // cruise through z-matrix row-by-row - // build a brick at each z-matrix value - var yi = ypx(0), - yb = [yi, yi], - xbi = xrev ? 0 : 1, - ybi = yrev ? 0 : 1, - // for collecting an average luminosity of the heatmap - pixcount = 0, - rcount = 0, - gcount = 0, - bcount = 0, - brickWithPadding, - xb, - j, - xi, - v, - row, - c; - - function applyBrickPadding(trace, x0, x1, y0, y1, xIndex, xLength, yIndex, yLength) { - var padding = { - x0: x0, - x1: x1, - y0: y0, - y1: y1 - }, - xEdgeGap = trace.xgap * 2 / 3, - yEdgeGap = trace.ygap * 2 / 3, - xCenterGap = trace.xgap / 3, - yCenterGap = trace.ygap / 3; - - if(yIndex === yLength - 1) { // top edge brick - padding.y1 = y1 - yEdgeGap; - } - - if(xIndex === xLength - 1) { // right edge brick - padding.x0 = x0 + xEdgeGap; - } - - if(yIndex === 0) { // bottom edge brick - padding.y0 = y0 + yEdgeGap; + if (zsmooth === "best") { + var xPixArray = new Array(x.length), + yPixArray = new Array(y.length), + xinterpArray = new Array(imageWidth), + yinterp, + r0, + r1; + + // first make arrays of x and y pixel locations of brick boundaries + for (i = 0; i < x.length; i++) { + xPixArray[i] = Math.round(xa.c2p(x[i]) - left); + } + for (i = 0; i < y.length; i++) { + yPixArray[i] = Math.round(ya.c2p(y[i]) - top); + } + + // then make arrays of interpolations + // (bin0=closest, bin1=next, frac=fractional dist.) + for (i = 0; i < imageWidth; i++) { + xinterpArray[i] = findInterp(i, xPixArray); + } + + // now do the interpolations and fill the png + for (j = 0; j < imageHeight; j++) { + yinterp = findInterp(j, yPixArray); + r0 = z[yinterp.bin0]; + r1 = z[yinterp.bin1]; + for (i = 0; i < imageWidth; i++, pxIndex += 4) { + c = interpColor(r0, r1, xinterpArray[i], yinterp); + putColor(pixels, pxIndex, c); } - - if(xIndex === 0) { // left edge brick - padding.x1 = x1 - xEdgeGap; - } - - if(xIndex > 0 && xIndex < xLength - 1) { // brick in the center along x - padding.x0 = x0 + xCenterGap; - padding.x1 = x1 - xCenterGap; - } - - if(yIndex > 0 && yIndex < yLength - 1) { // brick in the center along y - padding.y0 = y0 + yCenterGap; - padding.y1 = y1 - yCenterGap; - } - - return padding; - } - - function setColor(v, pixsize) { - if(v !== undefined) { - var c = sclFunc(v); - c[0] = Math.round(c[0]); - c[1] = Math.round(c[1]); - c[2] = Math.round(c[2]); - - pixcount += pixsize; - rcount += c[0] * pixsize; - gcount += c[1] * pixsize; - bcount += c[2] * pixsize; - return c; + } + } else { + // zsmooth = fast + for (j = 0; j < m; j++) { + row = z[j]; + yb = ypx(j); + for (i = 0; i < imageWidth; i++) { + c = setColor(row[i], 1); + pxIndex = (yb * imageWidth + xpx(i)) * 4; + putColor(pixels, pxIndex, c); } - return [0, 0, 0, 0]; + } } - function putColor(pixels, pxIndex, c) { - pixels[pxIndex] = c[0]; - pixels[pxIndex + 1] = c[1]; - pixels[pxIndex + 2] = c[2]; - pixels[pxIndex + 3] = Math.round(c[3] * 255); + var imageData = context.createImageData(imageWidth, imageHeight); + try { + imageData.data.set(pixels); + } catch (e) { + var pxArray = imageData.data, dlen = pxArray.length; + for (j = 0; j < dlen; j++) { + pxArray[j] = pixels[j]; + } } - function interpColor(r0, r1, xinterp, yinterp) { - var z00 = r0[xinterp.bin0]; - if(z00 === undefined) return setColor(undefined, 1); - - var z01 = r0[xinterp.bin1], - z10 = r1[xinterp.bin0], - z11 = r1[xinterp.bin1], - dx = (z01 - z00) || 0, - dy = (z10 - z00) || 0, - dxy; - - // the bilinear interpolation term needs different calculations - // for all the different permutations of missing data - // among the neighbors of the main point, to ensure - // continuity across brick boundaries. - if(z01 === undefined) { - if(z11 === undefined) dxy = 0; - else if(z10 === undefined) dxy = 2 * (z11 - z00); - else dxy = (2 * z11 - z10 - z00) * 2 / 3; - } - else if(z11 === undefined) { - if(z10 === undefined) dxy = 0; - else dxy = (2 * z00 - z01 - z10) * 2 / 3; - } - else if(z10 === undefined) dxy = (2 * z11 - z01 - z00) * 2 / 3; - else dxy = (z11 + z00 - z01 - z10); - - return setColor(z00 + xinterp.frac * dx + yinterp.frac * (dy + xinterp.frac * dxy)); - } - - if(zsmooth) { // best or fast, works fastest with imageData - var pxIndex = 0, - pixels; - - try { - pixels = new Uint8Array(imageWidth * imageHeight * 4); - } - catch(e) { - pixels = new Array(imageWidth * imageHeight * 4); - } - - if(zsmooth === 'best') { - var xPixArray = new Array(x.length), - yPixArray = new Array(y.length), - xinterpArray = new Array(imageWidth), - yinterp, - r0, - r1; - - // first make arrays of x and y pixel locations of brick boundaries - for(i = 0; i < x.length; i++) xPixArray[i] = Math.round(xa.c2p(x[i]) - left); - for(i = 0; i < y.length; i++) yPixArray[i] = Math.round(ya.c2p(y[i]) - top); - - // then make arrays of interpolations - // (bin0=closest, bin1=next, frac=fractional dist.) - for(i = 0; i < imageWidth; i++) xinterpArray[i] = findInterp(i, xPixArray); - - // now do the interpolations and fill the png - for(j = 0; j < imageHeight; j++) { - yinterp = findInterp(j, yPixArray); - r0 = z[yinterp.bin0]; - r1 = z[yinterp.bin1]; - for(i = 0; i < imageWidth; i++, pxIndex += 4) { - c = interpColor(r0, r1, xinterpArray[i], yinterp); - putColor(pixels, pxIndex, c); - } - } - } - else { // zsmooth = fast - for(j = 0; j < m; j++) { - row = z[j]; - yb = ypx(j); - for(i = 0; i < imageWidth; i++) { - c = setColor(row[i], 1); - pxIndex = (yb * imageWidth + xpx(i)) * 4; - putColor(pixels, pxIndex, c); - } - } - } - - var imageData = context.createImageData(imageWidth, imageHeight); - try { - imageData.data.set(pixels); - } - catch(e) { - var pxArray = imageData.data, - dlen = pxArray.length; - for(j = 0; j < dlen; j ++) { - pxArray[j] = pixels[j]; - } - } - - context.putImageData(imageData, 0, 0); - } else { // zsmooth = false -> filling potentially large bricks works fastest with fillRect - for(j = 0; j < m; j++) { - row = z[j]; - yb.reverse(); - yb[ybi] = ypx(j + 1); - if(yb[0] === yb[1] || yb[0] === undefined || yb[1] === undefined) { - continue; - } - xi = xpx(0); - xb = [xi, xi]; - for(i = 0; i < n; i++) { - // build one color brick! - xb.reverse(); - xb[xbi] = xpx(i + 1); - if(xb[0] === xb[1] || xb[0] === undefined || xb[1] === undefined) { - continue; - } - v = row[i]; - c = setColor(v, (xb[1] - xb[0]) * (yb[1] - yb[0])); - context.fillStyle = 'rgba(' + c.join(',') + ')'; - - brickWithPadding = applyBrickPadding(trace, - xb[0], - xb[1], - yb[0], - yb[1], - i, - n, - j, - m); - - context.fillRect(brickWithPadding.x0, - brickWithPadding.y0, - (brickWithPadding.x1 - brickWithPadding.x0), - (brickWithPadding.y1 - brickWithPadding.y0)); - } + context.putImageData(imageData, 0, 0); + } else { + // zsmooth = false -> filling potentially large bricks works fastest with fillRect + for (j = 0; j < m; j++) { + row = z[j]; + yb.reverse(); + yb[ybi] = ypx(j + 1); + if (yb[0] === yb[1] || yb[0] === undefined || yb[1] === undefined) { + continue; + } + xi = xpx(0); + xb = [xi, xi]; + for (i = 0; i < n; i++) { + // build one color brick! + xb.reverse(); + xb[xbi] = xpx(i + 1); + if (xb[0] === xb[1] || xb[0] === undefined || xb[1] === undefined) { + continue; } + v = row[i]; + c = setColor(v, (xb[1] - xb[0]) * (yb[1] - yb[0])); + context.fillStyle = "rgba(" + c.join(",") + ")"; + + brickWithPadding = applyBrickPadding( + trace, + xb[0], + xb[1], + yb[0], + yb[1], + i, + n, + j, + m + ); + + context.fillRect( + brickWithPadding.x0, + brickWithPadding.y0, + brickWithPadding.x1 - brickWithPadding.x0, + brickWithPadding.y1 - brickWithPadding.y0 + ); + } } + } - rcount = Math.round(rcount / pixcount); - gcount = Math.round(gcount / pixcount); - bcount = Math.round(bcount / pixcount); - var avgColor = tinycolor('rgb(' + rcount + ',' + gcount + ',' + bcount + ')'); + rcount = Math.round(rcount / pixcount); + gcount = Math.round(gcount / pixcount); + bcount = Math.round(bcount / pixcount); + var avgColor = tinycolor("rgb(" + rcount + "," + gcount + "," + bcount + ")"); - gd._hmpixcount = (gd._hmpixcount||0) + pixcount; - gd._hmlumcount = (gd._hmlumcount||0) + pixcount * avgColor.getLuminance(); + gd._hmpixcount = (gd._hmpixcount || 0) + pixcount; + gd._hmlumcount = (gd._hmlumcount || 0) + pixcount * avgColor.getLuminance(); - var image3 = plotgroup.selectAll('image') - .data(cd); + var image3 = plotgroup.selectAll("image").data(cd); - image3.enter().append('svg:image').attr({ - xmlns: xmlnsNamespaces.svg, - preserveAspectRatio: 'none' - }); + image3 + .enter() + .append("svg:image") + .attr({ xmlns: xmlnsNamespaces.svg, preserveAspectRatio: "none" }); - image3.attr({ - height: imageHeight, - width: imageWidth, - x: left, - y: top, - 'xlink:href': canvas.toDataURL('image/png') - }); + image3.attr({ + height: imageHeight, + width: imageWidth, + x: left, + y: top, + "xlink:href": canvas.toDataURL("image/png") + }); - image3.exit().remove(); + image3.exit().remove(); } diff --git a/src/traces/heatmap/style.js b/src/traces/heatmap/style.js index 9eac041e073..f2f7bcb2547 100644 --- a/src/traces/heatmap/style.js +++ b/src/traces/heatmap/style.js @@ -5,15 +5,11 @@ * 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 d3 = require('d3'); +"use strict"; +var d3 = require("d3"); module.exports = function style(gd) { - d3.select(gd).selectAll('.hm image') - .style('opacity', function(d) { - return d.trace.opacity; - }); + d3.select(gd).selectAll(".hm image").style("opacity", function(d) { + return d.trace.opacity; + }); }; diff --git a/src/traces/heatmap/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js index 33d13eb8dd5..943a91d1927 100644 --- a/src/traces/heatmap/xyz_defaults.js +++ b/src/traces/heatmap/xyz_defaults.js @@ -5,87 +5,79 @@ * 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 isNumeric = require("fast-isnumeric"); - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Registry = require('../../registry'); -var hasColumns = require('./has_columns'); - +var Registry = require("../../registry"); +var hasColumns = require("./has_columns"); module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout) { - var z = coerce('z'); - var x, y; + var z = coerce("z"); + var x, y; - if(z === undefined || !z.length) return 0; + if (z === undefined || !z.length) return 0; - if(hasColumns(traceIn)) { - x = coerce('x'); - y = coerce('y'); + if (hasColumns(traceIn)) { + x = coerce("x"); + y = coerce("y"); - // column z must be accompanied by 'x' and 'y' arrays - if(!x || !y) return 0; - } - else { - x = coordDefaults('x', coerce); - y = coordDefaults('y', coerce); + // column z must be accompanied by 'x' and 'y' arrays + if (!x || !y) return 0; + } else { + x = coordDefaults("x", coerce); + y = coordDefaults("y", coerce); - // TODO put z validation elsewhere - if(!isValidZ(z)) return 0; + // TODO put z validation elsewhere + if (!isValidZ(z)) return 0; - coerce('transpose'); - } + coerce("transpose"); + } - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); - handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleTraceDefaults" + ); + handleCalendarDefaults(traceIn, traceOut, ["x", "y"], layout); - return traceOut.z.length; + return traceOut.z.length; }; function coordDefaults(coordStr, coerce) { - var coord = coerce(coordStr), - coordType = coord ? - coerce(coordStr + 'type', 'array') : - 'scaled'; + var coord = coerce(coordStr), + coordType = coord ? coerce(coordStr + "type", "array") : "scaled"; - if(coordType === 'scaled') { - coerce(coordStr + '0'); - coerce('d' + coordStr); - } + if (coordType === "scaled") { + coerce(coordStr + "0"); + coerce("d" + coordStr); + } - return coord; + return coord; } function isValidZ(z) { - var allRowsAreArrays = true, - oneRowIsFilled = false, - hasOneNumber = false, - zi; + var allRowsAreArrays = true, oneRowIsFilled = false, hasOneNumber = false, zi; - /* + /* * Without this step: * * hasOneNumber = false breaks contour but not heatmap * allRowsAreArrays = false breaks contour but not heatmap * oneRowIsFilled = false breaks both */ - - for(var i = 0; i < z.length; i++) { - zi = z[i]; - if(!Array.isArray(zi)) { - allRowsAreArrays = false; - break; - } - if(zi.length > 0) oneRowIsFilled = true; - for(var j = 0; j < zi.length; j++) { - if(isNumeric(zi[j])) { - hasOneNumber = true; - break; - } - } + for (var i = 0; i < z.length; i++) { + zi = z[i]; + if (!Array.isArray(zi)) { + allRowsAreArrays = false; + break; + } + if (zi.length > 0) oneRowIsFilled = true; + for (var j = 0; j < zi.length; j++) { + if (isNumeric(zi[j])) { + hasOneNumber = true; + break; + } } + } - return (allRowsAreArrays && oneRowIsFilled && hasOneNumber); + return allRowsAreArrays && oneRowIsFilled && hasOneNumber; } diff --git a/src/traces/heatmapgl/attributes.js b/src/traces/heatmapgl/attributes.js index d3dae984714..26b35b03e38 100644 --- a/src/traces/heatmapgl/attributes.js +++ b/src/traces/heatmapgl/attributes.js @@ -5,36 +5,43 @@ * 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 heatmapAttrs = require("../heatmap/attributes"); +var colorscaleAttrs = require("../../components/colorscale/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); -'use strict'; - - -var heatmapAttrs = require('../heatmap/attributes'); -var colorscaleAttrs = require('../../components/colorscale/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; +var extendFlat = require("../../lib/extend").extendFlat; var commonList = [ - 'z', - 'x', 'x0', 'dx', - 'y', 'y0', 'dy', - 'text', 'transpose', - 'xtype', 'ytype' + "z", + "x", + "x0", + "dx", + "y", + "y0", + "dy", + "text", + "transpose", + "xtype", + "ytype" ]; var attrs = {}; -for(var i = 0; i < commonList.length; i++) { - var k = commonList[i]; - attrs[k] = heatmapAttrs[k]; +for (var i = 0; i < commonList.length; i++) { + var k = commonList[i]; + attrs[k] = heatmapAttrs[k]; } extendFlat( - attrs, - colorscaleAttrs, - { autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, {dflt: false}) }, - { colorbar: colorbarAttrs } + attrs, + colorscaleAttrs, + { + autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, { + dflt: false + }) + }, + { colorbar: colorbarAttrs } ); module.exports = attrs; diff --git a/src/traces/heatmapgl/convert.js b/src/traces/heatmapgl/convert.js index 5bdeed828c8..161e1d83ed7 100644 --- a/src/traces/heatmapgl/convert.js +++ b/src/traces/heatmapgl/convert.js @@ -5,133 +5,117 @@ * 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 createHeatmap2D = require('gl-heatmap2d'); -var Axes = require('../../plots/cartesian/axes'); -var str2RGBArray = require('../../lib/str2rgbarray'); - +"use strict"; +var createHeatmap2D = require("gl-heatmap2d"); +var Axes = require("../../plots/cartesian/axes"); +var str2RGBArray = require("../../lib/str2rgbarray"); function Heatmap(scene, uid) { - this.scene = scene; - this.uid = uid; - this.type = 'heatmapgl'; - - this.name = ''; - this.hoverinfo = 'all'; - - this.xData = []; - this.yData = []; - this.zData = []; - this.textLabels = []; - - this.idToIndex = []; - this.bounds = [0, 0, 0, 0]; - - this.options = { - z: [], - x: [], - y: [], - shape: [0, 0], - colorLevels: [0], - colorValues: [0, 0, 0, 1] - }; - - this.heatmap = createHeatmap2D(scene.glplot, this.options); - this.heatmap._trace = this; + this.scene = scene; + this.uid = uid; + this.type = "heatmapgl"; + + this.name = ""; + this.hoverinfo = "all"; + + this.xData = []; + this.yData = []; + this.zData = []; + this.textLabels = []; + + this.idToIndex = []; + this.bounds = [0, 0, 0, 0]; + + this.options = { + z: [], + x: [], + y: [], + shape: [0, 0], + colorLevels: [0], + colorValues: [0, 0, 0, 1] + }; + + this.heatmap = createHeatmap2D(scene.glplot, this.options); + this.heatmap._trace = this; } var proto = Heatmap.prototype; proto.handlePick = function(pickResult) { - var options = this.options, - shape = options.shape, - index = pickResult.pointId, - xIndex = index % shape[0], - yIndex = Math.floor(index / shape[0]), - zIndex = index; - - return { - trace: this, - dataCoord: pickResult.dataCoord, - traceCoord: [ - options.x[xIndex], - options.y[yIndex], - options.z[zIndex] - ], - textLabel: this.textLabels[index], - name: this.name, - pointIndex: [xIndex, yIndex], - hoverinfo: this.hoverinfo - }; + var options = this.options, + shape = options.shape, + index = pickResult.pointId, + xIndex = index % shape[0], + yIndex = Math.floor(index / shape[0]), + zIndex = index; + + return { + trace: this, + dataCoord: pickResult.dataCoord, + traceCoord: [options.x[xIndex], options.y[yIndex], options.z[zIndex]], + textLabel: this.textLabels[index], + name: this.name, + pointIndex: [xIndex, yIndex], + hoverinfo: this.hoverinfo + }; }; proto.update = function(fullTrace, calcTrace) { - var calcPt = calcTrace[0]; + var calcPt = calcTrace[0]; - this.name = fullTrace.name; - this.hoverinfo = fullTrace.hoverinfo; + this.name = fullTrace.name; + this.hoverinfo = fullTrace.hoverinfo; - // convert z from 2D -> 1D - var z = calcPt.z; - this.options.z = [].concat.apply([], z); + // convert z from 2D -> 1D + var z = calcPt.z; + this.options.z = [].concat.apply([], z); - var rowLen = z[0].length, - colLen = z.length; - this.options.shape = [rowLen, colLen]; + var rowLen = z[0].length, colLen = z.length; + this.options.shape = [rowLen, colLen]; - this.options.x = calcPt.x; - this.options.y = calcPt.y; + this.options.x = calcPt.x; + this.options.y = calcPt.y; - var colorOptions = convertColorscale(fullTrace); - this.options.colorLevels = colorOptions.colorLevels; - this.options.colorValues = colorOptions.colorValues; + var colorOptions = convertColorscale(fullTrace); + this.options.colorLevels = colorOptions.colorLevels; + this.options.colorValues = colorOptions.colorValues; - // convert text from 2D -> 1D - this.textLabels = [].concat.apply([], fullTrace.text); + // convert text from 2D -> 1D + this.textLabels = [].concat.apply([], fullTrace.text); - this.heatmap.update(this.options); + this.heatmap.update(this.options); - Axes.expand(this.scene.xaxis, calcPt.x); - Axes.expand(this.scene.yaxis, calcPt.y); + Axes.expand(this.scene.xaxis, calcPt.x); + Axes.expand(this.scene.yaxis, calcPt.y); }; proto.dispose = function() { - this.heatmap.dispose(); + this.heatmap.dispose(); }; function convertColorscale(fullTrace) { - var scl = fullTrace.colorscale, - zmin = fullTrace.zmin, - zmax = fullTrace.zmax; + var scl = fullTrace.colorscale, zmin = fullTrace.zmin, zmax = fullTrace.zmax; - var N = scl.length, - domain = new Array(N), - range = new Array(4 * N); + var N = scl.length, domain = new Array(N), range = new Array(4 * N); - for(var i = 0; i < N; i++) { - var si = scl[i]; - var color = str2RGBArray(si[1]); + for (var i = 0; i < N; i++) { + var si = scl[i]; + var color = str2RGBArray(si[1]); - domain[i] = zmin + si[0] * (zmax - zmin); + domain[i] = zmin + si[0] * (zmax - zmin); - for(var j = 0; j < 4; j++) { - range[(4 * i) + j] = color[j]; - } + for (var j = 0; j < 4; j++) { + range[4 * i + j] = color[j]; } + } - return { - colorLevels: domain, - colorValues: range - }; + return { colorLevels: domain, colorValues: range }; } function createHeatmap(scene, fullTrace, calcTrace) { - var plot = new Heatmap(scene, fullTrace.uid); - plot.update(fullTrace, calcTrace); - return plot; + var plot = new Heatmap(scene, fullTrace.uid); + plot.update(fullTrace, calcTrace); + return plot; } module.exports = createHeatmap; diff --git a/src/traces/heatmapgl/index.js b/src/traces/heatmapgl/index.js index 19ac6fe15f4..b179a9dcf58 100644 --- a/src/traces/heatmapgl/index.js +++ b/src/traces/heatmapgl/index.js @@ -5,27 +5,22 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var HeatmapGl = {}; -HeatmapGl.attributes = require('./attributes'); -HeatmapGl.supplyDefaults = require('../heatmap/defaults'); -HeatmapGl.colorbar = require('../heatmap/colorbar'); +HeatmapGl.attributes = require("./attributes"); +HeatmapGl.supplyDefaults = require("../heatmap/defaults"); +HeatmapGl.colorbar = require("../heatmap/colorbar"); -HeatmapGl.calc = require('../heatmap/calc'); -HeatmapGl.plot = require('./convert'); +HeatmapGl.calc = require("../heatmap/calc"); +HeatmapGl.plot = require("./convert"); -HeatmapGl.moduleType = 'trace'; -HeatmapGl.name = 'heatmapgl'; -HeatmapGl.basePlotModule = require('../../plots/gl2d'); -HeatmapGl.categories = ['gl2d', '2dMap']; +HeatmapGl.moduleType = "trace"; +HeatmapGl.name = "heatmapgl"; +HeatmapGl.basePlotModule = require("../../plots/gl2d"); +HeatmapGl.categories = ["gl2d", "2dMap"]; HeatmapGl.meta = { - description: [ - 'WebGL version of the heatmap trace type.' - ].join(' ') + description: ["WebGL version of the heatmap trace type."].join(" ") }; module.exports = HeatmapGl; diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index 889dda231fc..13c59ab0255 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -5,206 +5,185 @@ * 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 barAttrs = require('../bar/attributes'); - +"use strict"; +var barAttrs = require("../bar/attributes"); module.exports = { - x: { - valType: 'data_array', - description: [ - 'Sets the sample data to be binned on the x axis.' - ].join(' ') - }, - y: { - valType: 'data_array', - description: [ - 'Sets the sample data to be binned on the y axis.' - ].join(' ') + x: { + valType: "data_array", + description: ["Sets the sample data to be binned on the x axis."].join(" ") + }, + y: { + valType: "data_array", + description: ["Sets the sample data to be binned on the y axis."].join(" ") + }, + text: barAttrs.text, + orientation: barAttrs.orientation, + histfunc: { + valType: "enumerated", + values: ["count", "sum", "avg", "min", "max"], + role: "style", + dflt: "count", + description: [ + "Specifies the binning function used for this histogram trace.", + "If *count*, the histogram values are computed by counting the", + "number of values lying inside each bin.", + "If *sum*, *avg*, *min*, *max*,", + "the histogram values are computed using", + "the sum, the average, the minimum or the maximum", + "of the values lying inside each bin respectively." + ].join(" ") + }, + histnorm: { + valType: "enumerated", + values: ["", "percent", "probability", "density", "probability density"], + dflt: "", + role: "style", + description: [ + "Specifies the type of normalization used for this histogram trace.", + "If **, the span of each bar corresponds to the number of", + "occurrences (i.e. the number of data points lying inside the bins).", + "If *percent* / *probability*, the span of each bar corresponds to", + "the percentage / fraction of occurrences with respect to the total", + "number of sample points", + "(here, the sum of all bin HEIGHTS equals 100% / 1).", + "If *density*, the span of each bar corresponds to the number of", + "occurrences in a bin divided by the size of the bin interval", + "(here, the sum of all bin AREAS equals the", + "total number of sample points).", + "If *probability density*, the area of each bar corresponds to the", + "probability that an event will fall into the corresponding bin", + "(here, the sum of all bin AREAS equals 1)." + ].join(" ") + }, + cumulative: { + enabled: { + valType: "boolean", + dflt: false, + role: "info", + description: [ + "If true, display the cumulative distribution by summing the", + "binned values. Use the `direction` and `centralbin` attributes", + "to tune the accumulation method.", + "Note: in this mode, the *density* `histnorm` settings behave", + "the same as their equivalents without *density*:", + "** and *density* both rise to the number of data points, and", + "*probability* and *probability density* both rise to the", + "number of sample points." + ].join(" ") }, - - text: barAttrs.text, - orientation: barAttrs.orientation, - - histfunc: { - valType: 'enumerated', - values: ['count', 'sum', 'avg', 'min', 'max'], - role: 'style', - dflt: 'count', - description: [ - 'Specifies the binning function used for this histogram trace.', - - 'If *count*, the histogram values are computed by counting the', - 'number of values lying inside each bin.', - - 'If *sum*, *avg*, *min*, *max*,', - 'the histogram values are computed using', - 'the sum, the average, the minimum or the maximum', - 'of the values lying inside each bin respectively.' - ].join(' ') + direction: { + valType: "enumerated", + values: ["increasing", "decreasing"], + dflt: "increasing", + role: "info", + description: [ + "Only applies if cumulative is enabled.", + "If *increasing* (default) we sum all prior bins, so the result", + "increases from left to right. If *decreasing* we sum later bins", + "so the result decreases from left to right." + ].join(" ") }, - histnorm: { - valType: 'enumerated', - values: ['', 'percent', 'probability', 'density', 'probability density'], - dflt: '', - role: 'style', - description: [ - 'Specifies the type of normalization used for this histogram trace.', - - 'If **, the span of each bar corresponds to the number of', - 'occurrences (i.e. the number of data points lying inside the bins).', - - 'If *percent* / *probability*, the span of each bar corresponds to', - 'the percentage / fraction of occurrences with respect to the total', - 'number of sample points', - '(here, the sum of all bin HEIGHTS equals 100% / 1).', - - 'If *density*, the span of each bar corresponds to the number of', - 'occurrences in a bin divided by the size of the bin interval', - '(here, the sum of all bin AREAS equals the', - 'total number of sample points).', - - 'If *probability density*, the area of each bar corresponds to the', - 'probability that an event will fall into the corresponding bin', - '(here, the sum of all bin AREAS equals 1).' - ].join(' ') - }, - - cumulative: { - enabled: { - valType: 'boolean', - dflt: false, - role: 'info', - description: [ - 'If true, display the cumulative distribution by summing the', - 'binned values. Use the `direction` and `centralbin` attributes', - 'to tune the accumulation method.', - 'Note: in this mode, the *density* `histnorm` settings behave', - 'the same as their equivalents without *density*:', - '** and *density* both rise to the number of data points, and', - '*probability* and *probability density* both rise to the', - 'number of sample points.' - ].join(' ') - }, - - direction: { - valType: 'enumerated', - values: ['increasing', 'decreasing'], - dflt: 'increasing', - role: 'info', - description: [ - 'Only applies if cumulative is enabled.', - 'If *increasing* (default) we sum all prior bins, so the result', - 'increases from left to right. If *decreasing* we sum later bins', - 'so the result decreases from left to right.' - ].join(' ') - }, - - currentbin: { - valType: 'enumerated', - values: ['include', 'exclude', 'half'], - dflt: 'include', - role: 'info', - description: [ - 'Only applies if cumulative is enabled.', - 'Sets whether the current bin is included, excluded, or has half', - 'of its value included in the current cumulative value.', - '*include* is the default for compatibility with various other', - 'tools, however it introduces a half-bin bias to the results.', - '*exclude* makes the opposite half-bin bias, and *half* removes', - 'it.' - ].join(' ') - } - }, - - autobinx: { - valType: 'boolean', - dflt: null, - role: 'style', - description: [ - 'Determines whether or not the x axis bin attributes are picked', - 'by an algorithm. Note that this should be set to false if you', - 'want to manually set the number of bins using the attributes in', - 'xbins.' - ].join(' ') - }, - nbinsx: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'style', - description: [ - 'Specifies the maximum number of desired bins. This value will be used', - 'in an algorithm that will decide the optimal bin size such that the', - 'histogram best visualizes the distribution of the data.' - ].join(' ') - }, - xbins: makeBinsAttr('x'), - - autobiny: { - valType: 'boolean', - dflt: null, - role: 'style', - description: [ - 'Determines whether or not the y axis bin attributes are picked', - 'by an algorithm. Note that this should be set to false if you', - 'want to manually set the number of bins using the attributes in', - 'ybins.' - ].join(' ') - }, - nbinsy: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'style', - description: [ - 'Specifies the maximum number of desired bins. This value will be used', - 'in an algorithm that will decide the optimal bin size such that the', - 'histogram best visualizes the distribution of the data.' - ].join(' ') - }, - ybins: makeBinsAttr('y'), - - marker: barAttrs.marker, - - error_y: barAttrs.error_y, - error_x: barAttrs.error_x, - - _deprecated: { - bardir: barAttrs._deprecated.bardir + currentbin: { + valType: "enumerated", + values: ["include", "exclude", "half"], + dflt: "include", + role: "info", + description: [ + "Only applies if cumulative is enabled.", + "Sets whether the current bin is included, excluded, or has half", + "of its value included in the current cumulative value.", + "*include* is the default for compatibility with various other", + "tools, however it introduces a half-bin bias to the results.", + "*exclude* makes the opposite half-bin bias, and *half* removes", + "it." + ].join(" ") } + }, + autobinx: { + valType: "boolean", + dflt: null, + role: "style", + description: [ + "Determines whether or not the x axis bin attributes are picked", + "by an algorithm. Note that this should be set to false if you", + "want to manually set the number of bins using the attributes in", + "xbins." + ].join(" ") + }, + nbinsx: { + valType: "integer", + min: 0, + dflt: 0, + role: "style", + description: [ + "Specifies the maximum number of desired bins. This value will be used", + "in an algorithm that will decide the optimal bin size such that the", + "histogram best visualizes the distribution of the data." + ].join(" ") + }, + xbins: makeBinsAttr("x"), + autobiny: { + valType: "boolean", + dflt: null, + role: "style", + description: [ + "Determines whether or not the y axis bin attributes are picked", + "by an algorithm. Note that this should be set to false if you", + "want to manually set the number of bins using the attributes in", + "ybins." + ].join(" ") + }, + nbinsy: { + valType: "integer", + min: 0, + dflt: 0, + role: "style", + description: [ + "Specifies the maximum number of desired bins. This value will be used", + "in an algorithm that will decide the optimal bin size such that the", + "histogram best visualizes the distribution of the data." + ].join(" ") + }, + ybins: makeBinsAttr("y"), + marker: barAttrs.marker, + error_y: barAttrs.error_y, + error_x: barAttrs.error_x, + _deprecated: { bardir: barAttrs._deprecated.bardir } }; function makeBinsAttr(axLetter) { - return { - start: { - valType: 'any', // for date axes - dflt: null, - role: 'style', - description: [ - 'Sets the starting value for the', axLetter, - 'axis bins.' - ].join(' ') - }, - end: { - valType: 'any', // for date axes - dflt: null, - role: 'style', - description: [ - 'Sets the end value for the', axLetter, - 'axis bins.' - ].join(' ') - }, - size: { - valType: 'any', // for date axes - dflt: null, - role: 'style', - description: [ - 'Sets the step in-between value each', axLetter, - 'axis bin.' - ].join(' ') - } - }; + return { + start: { + valType: "any", + // for date axes + dflt: null, + role: "style", + description: [ + "Sets the starting value for the", + axLetter, + "axis bins." + ].join(" ") + }, + end: { + valType: "any", + // for date axes + dflt: null, + role: "style", + description: ["Sets the end value for the", axLetter, "axis bins."].join( + " " + ) + }, + size: { + valType: "any", + // for date axes + dflt: null, + role: "style", + description: [ + "Sets the step in-between value each", + axLetter, + "axis bin." + ].join(" ") + } + }; } diff --git a/src/traces/histogram/average.js b/src/traces/histogram/average.js index ce382e3395e..81b9b6c8c36 100644 --- a/src/traces/histogram/average.js +++ b/src/traces/histogram/average.js @@ -5,20 +5,16 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; module.exports = function doAvg(size, counts) { - var nMax = size.length, - total = 0; - for(var i = 0; i < nMax; i++) { - if(counts[i]) { - size[i] /= counts[i]; - total += size[i]; - } - else size[i] = null; + var nMax = size.length, total = 0; + for (var i = 0; i < nMax; i++) { + if (counts[i]) { + size[i] /= counts[i]; + total += size[i]; + } else { + size[i] = null; } - return total; + } + return total; }; diff --git a/src/traces/histogram/bin_defaults.js b/src/traces/histogram/bin_defaults.js index 444668ac815..63d372789a0 100644 --- a/src/traces/histogram/bin_defaults.js +++ b/src/traces/histogram/bin_defaults.js @@ -5,27 +5,28 @@ * 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 = function handleBinDefaults( + traceIn, + traceOut, + coerce, + binDirections +) { + coerce("histnorm"); - -'use strict'; - - -module.exports = function handleBinDefaults(traceIn, traceOut, coerce, binDirections) { - coerce('histnorm'); - - binDirections.forEach(function(binDirection) { - /* + binDirections.forEach(function(binDirection) { + /* * Because date axes have string values for start and end, * and string options for size, we cannot validate these attributes * now. We will do this during calc (immediately prior to binning) * in ./clean_bins, and push the cleaned values back to _fullData. */ - coerce(binDirection + 'bins.start'); - coerce(binDirection + 'bins.end'); - coerce(binDirection + 'bins.size'); - coerce('autobin' + binDirection); - coerce('nbins' + binDirection); - }); + coerce(binDirection + "bins.start"); + coerce(binDirection + "bins.end"); + coerce(binDirection + "bins.size"); + coerce("autobin" + binDirection); + coerce("nbins" + binDirection); + }); - return traceOut; + return traceOut; }; diff --git a/src/traces/histogram/bin_functions.js b/src/traces/histogram/bin_functions.js index d4219667903..07d3bdd13f0 100644 --- a/src/traces/histogram/bin_functions.js +++ b/src/traces/histogram/bin_functions.js @@ -5,70 +5,60 @@ * 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 isNumeric = require('fast-isnumeric'); - +"use strict"; +var isNumeric = require("fast-isnumeric"); module.exports = { - count: function(n, i, size) { - size[n]++; - return 1; - }, - - sum: function(n, i, size, counterData) { - var v = counterData[i]; - if(isNumeric(v)) { - v = Number(v); - size[n] += v; - return v; - } - return 0; - }, - - avg: function(n, i, size, counterData, counts) { - var v = counterData[i]; - if(isNumeric(v)) { - v = Number(v); - size[n] += v; - counts[n]++; - } - return 0; - }, - - min: function(n, i, size, counterData) { - var v = counterData[i]; - if(isNumeric(v)) { - v = Number(v); - if(!isNumeric(size[n])) { - size[n] = v; - return v; - } - else if(size[n] > v) { - var delta = v - size[n]; - size[n] = v; - return delta; - } - } - return 0; - }, - - max: function(n, i, size, counterData) { - var v = counterData[i]; - if(isNumeric(v)) { - v = Number(v); - if(!isNumeric(size[n])) { - size[n] = v; - return v; - } - else if(size[n] < v) { - var delta = v - size[n]; - size[n] = v; - return delta; - } - } - return 0; + count: function(n, i, size) { + size[n]++; + return 1; + }, + sum: function(n, i, size, counterData) { + var v = counterData[i]; + if (isNumeric(v)) { + v = Number(v); + size[n] += v; + return v; + } + return 0; + }, + avg: function(n, i, size, counterData, counts) { + var v = counterData[i]; + if (isNumeric(v)) { + v = Number(v); + size[n] += v; + counts[n]++; + } + return 0; + }, + min: function(n, i, size, counterData) { + var v = counterData[i]; + if (isNumeric(v)) { + v = Number(v); + if (!isNumeric(size[n])) { + size[n] = v; + return v; + } else if (size[n] > v) { + var delta = v - size[n]; + size[n] = v; + return delta; + } + } + return 0; + }, + max: function(n, i, size, counterData) { + var v = counterData[i]; + if (isNumeric(v)) { + v = Number(v); + if (!isNumeric(size[n])) { + size[n] = v; + return v; + } else if (size[n] < v) { + var delta = v - size[n]; + size[n] = v; + return delta; + } } + return 0; + } }; diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 9b079438b98..b0924aa857d 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -5,222 +5,219 @@ * 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 isNumeric = require("fast-isnumeric"); +var Lib = require("../../lib"); +var Axes = require("../../plots/cartesian/axes"); -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Axes = require('../../plots/cartesian/axes'); - -var binFunctions = require('./bin_functions'); -var normFunctions = require('./norm_functions'); -var doAvg = require('./average'); -var cleanBins = require('./clean_bins'); - +var binFunctions = require("./bin_functions"); +var normFunctions = require("./norm_functions"); +var doAvg = require("./average"); +var cleanBins = require("./clean_bins"); module.exports = function calc(gd, trace) { - // ignore as much processing as possible (and including in autorange) if bar is not visible - if(trace.visible !== true) return; - - // depending on orientation, set position and size axes and data ranges - // note: this logic for choosing orientation is duplicated in graph_obj->setstyles - var pos = [], - size = [], - i, - pa = Axes.getFromId(gd, - trace.orientation === 'h' ? (trace.yaxis || 'y') : (trace.xaxis || 'x')), - maindata = trace.orientation === 'h' ? 'y' : 'x', - counterdata = {x: 'y', y: 'x'}[maindata], - calendar = trace[maindata + 'calendar'], - cumulativeSpec = trace.cumulative; - - cleanBins(trace, pa, maindata); - - // prepare the raw data - var pos0 = pa.makeCalcdata(trace, maindata); - - // calculate the bins - var binAttr = maindata + 'bins', - binspec; - if((trace['autobin' + maindata] !== false) || !(binAttr in trace)) { - binspec = Axes.autoBin(pos0, pa, trace['nbins' + maindata], false, calendar); - - // adjust for CDF edge cases - if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) { - if(cumulativeSpec.direction === 'decreasing') { - binspec.start = pa.c2r(pa.r2c(binspec.start) - binspec.size); - } - else { - binspec.end = pa.c2r(pa.r2c(binspec.end) + binspec.size); - } - } - - // copy bin info back to the source and full data. - trace._input[binAttr] = trace[binAttr] = binspec; - } - else { - binspec = trace[binAttr]; - } - - var nonuniformBins = typeof binspec.size === 'string', - bins = nonuniformBins ? [] : binspec, - // make the empty bin array - i2, - binend, - n, - inc = [], - counts = [], - total = 0, - norm = trace.histnorm, - func = trace.histfunc, - densitynorm = norm.indexOf('density') !== -1; - - if(cumulativeSpec.enabled && densitynorm) { - // we treat "cumulative" like it means "integral" if you use a density norm, - // which in the end means it's the same as without "density" - norm = norm.replace(/ ?density$/, ''); - densitynorm = false; - } - - var extremefunc = func === 'max' || func === 'min', - sizeinit = extremefunc ? null : 0, - binfunc = binFunctions.count, - normfunc = normFunctions[norm], - doavg = false, - pr2c = function(v) { return pa.r2c(v, 0, calendar); }, - rawCounterData; - - if(Array.isArray(trace[counterdata]) && func !== 'count') { - rawCounterData = trace[counterdata]; - doavg = func === 'avg'; - binfunc = binFunctions[func]; - } - - // create the bins (and any extra arrays needed) - // assume more than 5000 bins is an error, so we don't crash the browser - i = pr2c(binspec.start); - - // decrease end a little in case of rounding errors - binend = pr2c(binspec.end) + (i - Axes.tickIncrement(i, binspec.size, false, calendar)) / 1e6; - - while(i < binend && pos.length < 5000) { - i2 = Axes.tickIncrement(i, binspec.size, false, calendar); - pos.push((i + i2) / 2); - size.push(sizeinit); - // nonuniform bins (like months) we need to search, - // rather than straight calculate the bin we're in - if(nonuniformBins) bins.push(i); - // nonuniform bins also need nonuniform normalization factors - if(densitynorm) inc.push(1 / (i2 - i)); - if(doavg) counts.push(0); - i = i2; - } - - // for date axes we need bin bounds to be calcdata. For nonuniform bins - // we already have this, but uniform with start/end/size they're still strings. - if(!nonuniformBins && pa.type === 'date') { - bins = { - start: pr2c(bins.start), - end: pr2c(bins.end), - size: bins.size - }; + // ignore as much processing as possible (and including in autorange) if bar is not visible + if (trace.visible !== true) return; + + // depending on orientation, set position and size axes and data ranges + // note: this logic for choosing orientation is duplicated in graph_obj->setstyles + var pos = [], + size = [], + i, + pa = Axes.getFromId( + gd, + trace.orientation === "h" ? trace.yaxis || "y" : trace.xaxis || "x" + ), + maindata = trace.orientation === "h" ? "y" : "x", + counterdata = ({ x: "y", y: "x" })[maindata], + calendar = trace[maindata + "calendar"], + cumulativeSpec = trace.cumulative; + + cleanBins(trace, pa, maindata); + + // prepare the raw data + var pos0 = pa.makeCalcdata(trace, maindata); + + // calculate the bins + var binAttr = maindata + "bins", binspec; + if (trace["autobin" + maindata] !== false || !(binAttr in trace)) { + binspec = Axes.autoBin( + pos0, + pa, + trace["nbins" + maindata], + false, + calendar + ); + + // adjust for CDF edge cases + if (cumulativeSpec.enabled && cumulativeSpec.currentbin !== "include") { + if (cumulativeSpec.direction === "decreasing") { + binspec.start = pa.c2r(pa.r2c(binspec.start) - binspec.size); + } else { + binspec.end = pa.c2r(pa.r2c(binspec.end) + binspec.size); + } } - var nMax = size.length; - // bin the data - for(i = 0; i < pos0.length; i++) { - n = Lib.findBin(pos0[i], bins); - if(n >= 0 && n < nMax) total += binfunc(n, i, size, rawCounterData, counts); + // copy bin info back to the source and full data. + trace._input[binAttr] = trace[binAttr] = binspec; + } else { + binspec = trace[binAttr]; + } + + var nonuniformBins = typeof binspec.size === "string", + bins = nonuniformBins ? [] : binspec, + // make the empty bin array + i2, + binend, + n, + inc = [], + counts = [], + total = 0, + norm = trace.histnorm, + func = trace.histfunc, + densitynorm = norm.indexOf("density") !== -1; + + if (cumulativeSpec.enabled && densitynorm) { + // we treat "cumulative" like it means "integral" if you use a density norm, + // which in the end means it's the same as without "density" + norm = norm.replace(/ ?density$/, ""); + densitynorm = false; + } + + var extremefunc = func === "max" || func === "min", + sizeinit = extremefunc ? null : 0, + binfunc = binFunctions.count, + normfunc = normFunctions[norm], + doavg = false, + pr2c = function(v) { + return pa.r2c(v, 0, calendar); + }, + rawCounterData; + + if (Array.isArray(trace[counterdata]) && func !== "count") { + rawCounterData = trace[counterdata]; + doavg = func === "avg"; + binfunc = binFunctions[func]; + } + + // create the bins (and any extra arrays needed) + // assume more than 5000 bins is an error, so we don't crash the browser + i = pr2c(binspec.start); + + // decrease end a little in case of rounding errors + binend = pr2c(binspec.end) + + (i - Axes.tickIncrement(i, binspec.size, false, calendar)) / 1e6; + + while (i < binend && pos.length < 5000) { + i2 = Axes.tickIncrement(i, binspec.size, false, calendar); + pos.push((i + i2) / 2); + size.push(sizeinit); + // nonuniform bins (like months) we need to search, + // rather than straight calculate the bin we're in + if (nonuniformBins) bins.push(i); + // nonuniform bins also need nonuniform normalization factors + if (densitynorm) inc.push(1 / (i2 - i)); + if (doavg) counts.push(0); + i = i2; + } + + // for date axes we need bin bounds to be calcdata. For nonuniform bins + // we already have this, but uniform with start/end/size they're still strings. + if (!nonuniformBins && pa.type === "date") { + bins = { start: pr2c(bins.start), end: pr2c(bins.end), size: bins.size }; + } + + var nMax = size.length; + // bin the data + for (i = 0; i < pos0.length; i++) { + n = Lib.findBin(pos0[i], bins); + if (n >= 0 && n < nMax) { + total += binfunc(n, i, size, rawCounterData, counts); } - - // average and/or normalize the data, if needed - if(doavg) total = doAvg(size, counts); - if(normfunc) normfunc(size, total, inc); - - // after all normalization etc, now we can accumulate if desired - if(cumulativeSpec.enabled) cdf(size, cumulativeSpec.direction, cumulativeSpec.currentbin); - - - var serieslen = Math.min(pos.length, size.length), - cd = [], - firstNonzero = 0, - lastNonzero = serieslen - 1; - // look for empty bins at the ends to remove, so autoscale omits them - for(i = 0; i < serieslen; i++) { - if(size[i]) { - firstNonzero = i; - break; - } + } + + // average and/or normalize the data, if needed + if (doavg) total = doAvg(size, counts); + if (normfunc) normfunc(size, total, inc); + + // after all normalization etc, now we can accumulate if desired + if (cumulativeSpec.enabled) { + cdf(size, cumulativeSpec.direction, cumulativeSpec.currentbin); + } + + var serieslen = Math.min(pos.length, size.length), + cd = [], + firstNonzero = 0, + lastNonzero = serieslen - 1; + // look for empty bins at the ends to remove, so autoscale omits them + for (i = 0; i < serieslen; i++) { + if (size[i]) { + firstNonzero = i; + break; } - for(i = serieslen - 1; i > firstNonzero; i--) { - if(size[i]) { - lastNonzero = i; - break; - } + } + for (i = serieslen - 1; i > firstNonzero; i--) { + if (size[i]) { + lastNonzero = i; + break; } + } - // create the "calculated data" to plot - for(i = firstNonzero; i <= lastNonzero; i++) { - if((isNumeric(pos[i]) && isNumeric(size[i]))) { - cd.push({p: pos[i], s: size[i], b: 0}); - } + // create the "calculated data" to plot + for (i = firstNonzero; i <= lastNonzero; i++) { + if (isNumeric(pos[i]) && isNumeric(size[i])) { + cd.push({ p: pos[i], s: size[i], b: 0 }); } + } - return cd; + return cd; }; function cdf(size, direction, currentbin) { - var i, - vi, - prevSum; - - function firstHalfPoint(i) { - prevSum = size[i]; - size[i] /= 2; + var i, vi, prevSum; + + function firstHalfPoint(i) { + prevSum = size[i]; + size[i] /= 2; + } + + function nextHalfPoint(i) { + vi = size[i]; + size[i] = prevSum + vi / 2; + prevSum += vi; + } + + if (currentbin === "half") { + if (direction === "increasing") { + firstHalfPoint(0); + for (i = 1; i < size.length; i++) { + nextHalfPoint(i); + } + } else { + firstHalfPoint(size.length - 1); + for (i = size.length - 2; i >= 0; i--) { + nextHalfPoint(i); + } } - - function nextHalfPoint(i) { - vi = size[i]; - size[i] = prevSum + vi / 2; - prevSum += vi; + } else if (direction === "increasing") { + for (i = 1; i < size.length; i++) { + size[i] += size[i - 1]; } - if(currentbin === 'half') { - - if(direction === 'increasing') { - firstHalfPoint(0); - for(i = 1; i < size.length; i++) { - nextHalfPoint(i); - } - } - else { - firstHalfPoint(size.length - 1); - for(i = size.length - 2; i >= 0; i--) { - nextHalfPoint(i); - } - } + // 'exclude' is identical to 'include' just shifted one bin over + if (currentbin === "exclude") { + size.unshift(0); + size.pop(); } - else if(direction === 'increasing') { - for(i = 1; i < size.length; i++) { - size[i] += size[i - 1]; - } - - // 'exclude' is identical to 'include' just shifted one bin over - if(currentbin === 'exclude') { - size.unshift(0); - size.pop(); - } + } else { + for (i = size.length - 2; i >= 0; i--) { + size[i] += size[i + 1]; } - else { - for(i = size.length - 2; i >= 0; i--) { - size[i] += size[i + 1]; - } - - if(currentbin === 'exclude') { - size.push(0); - size.shift(); - } + + if (currentbin === "exclude") { + size.push(0); + size.shift(); } + } } diff --git a/src/traces/histogram/clean_bins.js b/src/traces/histogram/clean_bins.js index ab53f6bd88f..7db56c8ed2a 100644 --- a/src/traces/histogram/clean_bins.js +++ b/src/traces/histogram/clean_bins.js @@ -5,12 +5,10 @@ * 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 isNumeric = require('fast-isnumeric'); -var cleanDate = require('../../lib').cleanDate; -var constants = require('../../constants/numerical'); +"use strict"; +var isNumeric = require("fast-isnumeric"); +var cleanDate = require("../../lib").cleanDate; +var constants = require("../../constants/numerical"); var ONEDAY = constants.ONEDAY; var BADNUM = constants.BADNUM; @@ -24,52 +22,51 @@ var BADNUM = constants.BADNUM; * calc step, when data are inserted into bins. */ module.exports = function cleanBins(trace, ax, binDirection) { - var axType = ax.type, - binAttr = binDirection + 'bins', - bins = trace[binAttr]; + var axType = ax.type, binAttr = binDirection + "bins", bins = trace[binAttr]; - if(!bins) bins = trace[binAttr] = {}; + if (!bins) bins = trace[binAttr] = {}; - var cleanBound = (axType === 'date') ? - function(v) { return (v || v === 0) ? cleanDate(v, BADNUM, bins.calendar) : null; } : - function(v) { return isNumeric(v) ? Number(v) : null; }; + var cleanBound = axType === "date" + ? (function(v) { + return v || v === 0 ? cleanDate(v, BADNUM, bins.calendar) : null; + }) + : (function(v) { + return isNumeric(v) ? Number(v) : null; + }); - bins.start = cleanBound(bins.start); - bins.end = cleanBound(bins.end); + bins.start = cleanBound(bins.start); + bins.end = cleanBound(bins.end); - // logic for bin size is very similar to dtick (cartesian/tick_value_defaults) - // but without the extra string options for log axes - // ie the only strings we accept are M for months - var sizeDflt = (axType === 'date') ? ONEDAY : 1, - binSize = bins.size; + // logic for bin size is very similar to dtick (cartesian/tick_value_defaults) + // but without the extra string options for log axes + // ie the only strings we accept are M for months + var sizeDflt = axType === "date" ? ONEDAY : 1, binSize = bins.size; - if(isNumeric(binSize)) { - bins.size = (binSize > 0) ? Number(binSize) : sizeDflt; - } - else if(typeof binSize !== 'string') { - bins.size = sizeDflt; - } - else { - // date special case: "M" gives bins every (integer) n months - var prefix = binSize.charAt(0), - sizeNum = binSize.substr(1); + if (isNumeric(binSize)) { + bins.size = binSize > 0 ? Number(binSize) : sizeDflt; + } else if (typeof binSize !== "string") { + bins.size = sizeDflt; + } else { + // date special case: "M" gives bins every (integer) n months + var prefix = binSize.charAt(0), sizeNum = binSize.substr(1); - sizeNum = isNumeric(sizeNum) ? Number(sizeNum) : 0; - if((sizeNum <= 0) || !( - axType === 'date' && prefix === 'M' && sizeNum === Math.round(sizeNum) - )) { - bins.size = sizeDflt; - } + sizeNum = isNumeric(sizeNum) ? Number(sizeNum) : 0; + if ( + sizeNum <= 0 || + !(axType === "date" && + prefix === "M" && + sizeNum === Math.round(sizeNum)) + ) { + bins.size = sizeDflt; } + } - var autoBinAttr = 'autobin' + binDirection; + var autoBinAttr = "autobin" + binDirection; - if(typeof trace[autoBinAttr] !== 'boolean') { - trace[autoBinAttr] = !( - (bins.start || bins.start === 0) && - (bins.end || bins.end === 0) - ); - } + if (typeof trace[autoBinAttr] !== "boolean") { + trace[autoBinAttr] = !((bins.start || bins.start === 0) && + (bins.end || bins.end === 0)); + } - if(!trace[autoBinAttr]) delete trace['nbins' + binDirection]; + if (!trace[autoBinAttr]) delete trace["nbins" + binDirection]; }; diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index 534bddf3f8f..a1fc98e6143 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -5,56 +5,62 @@ * 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 Registry = require('../../registry'); -var Lib = require('../../lib'); -var Color = require('../../components/color'); - -var handleBinDefaults = require('./bin_defaults'); -var handleStyleDefaults = require('../bar/style_defaults'); -var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var x = coerce('x'), - y = coerce('y'); - - var cumulative = coerce('cumulative.enabled'); - if(cumulative) { - coerce('cumulative.direction'); - coerce('cumulative.currentbin'); - } - - coerce('text'); - - var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'), - sample = traceOut[orientation === 'v' ? 'x' : 'y']; - - if(!(sample && sample.length)) { - traceOut.visible = false; - return; - } - - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); - handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); - - var hasAggregationData = traceOut[orientation === 'h' ? 'x' : 'y']; - if(hasAggregationData) coerce('histfunc'); - - var binDirections = (orientation === 'h') ? ['y'] : ['x']; - handleBinDefaults(traceIn, traceOut, coerce, binDirections); - - handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); - - // override defaultColor for error bars with defaultLine - errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'y'}); - errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'x', inherit: 'y'}); +"use strict"; +var Registry = require("../../registry"); +var Lib = require("../../lib"); +var Color = require("../../components/color"); + +var handleBinDefaults = require("./bin_defaults"); +var handleStyleDefaults = require("../bar/style_defaults"); +var errorBarsSupplyDefaults = require("../../components/errorbars/defaults"); +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var x = coerce("x"), y = coerce("y"); + + var cumulative = coerce("cumulative.enabled"); + if (cumulative) { + coerce("cumulative.direction"); + coerce("cumulative.currentbin"); + } + + coerce("text"); + + var orientation = coerce("orientation", y && !x ? "h" : "v"), + sample = traceOut[orientation === "v" ? "x" : "y"]; + + if (!(sample && sample.length)) { + traceOut.visible = false; + return; + } + + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleTraceDefaults" + ); + handleCalendarDefaults(traceIn, traceOut, ["x", "y"], layout); + + var hasAggregationData = traceOut[orientation === "h" ? "x" : "y"]; + if (hasAggregationData) coerce("histfunc"); + + var binDirections = orientation === "h" ? ["y"] : ["x"]; + handleBinDefaults(traceIn, traceOut, coerce, binDirections); + + handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); + + // override defaultColor for error bars with defaultLine + errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, { axis: "y" }); + errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, { + axis: "x", + inherit: "y" + }); }; diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index bf743152181..18cbbdf5580 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -5,10 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; /** * Histogram has its own attribute, defaults and calc steps, * but uses bar's plot to display @@ -22,32 +19,38 @@ * to allow quadrature combination of errors in summed histograms... */ - var Histogram = {}; -Histogram.attributes = require('./attributes'); -Histogram.layoutAttributes = require('../bar/layout_attributes'); -Histogram.supplyDefaults = require('./defaults'); -Histogram.supplyLayoutDefaults = require('../bar/layout_defaults'); -Histogram.calc = require('./calc'); -Histogram.setPositions = require('../bar/set_positions'); -Histogram.plot = require('../bar/plot'); -Histogram.style = require('../bar/style'); -Histogram.colorbar = require('../scatter/colorbar'); -Histogram.hoverPoints = require('../bar/hover'); - -Histogram.moduleType = 'trace'; -Histogram.name = 'histogram'; -Histogram.basePlotModule = require('../../plots/cartesian'); -Histogram.categories = ['cartesian', 'bar', 'histogram', 'oriented', 'errorBarsOK', 'showLegend']; +Histogram.attributes = require("./attributes"); +Histogram.layoutAttributes = require("../bar/layout_attributes"); +Histogram.supplyDefaults = require("./defaults"); +Histogram.supplyLayoutDefaults = require("../bar/layout_defaults"); +Histogram.calc = require("./calc"); +Histogram.setPositions = require("../bar/set_positions"); +Histogram.plot = require("../bar/plot"); +Histogram.style = require("../bar/style"); +Histogram.colorbar = require("../scatter/colorbar"); +Histogram.hoverPoints = require("../bar/hover"); + +Histogram.moduleType = "trace"; +Histogram.name = "histogram"; +Histogram.basePlotModule = require("../../plots/cartesian"); +Histogram.categories = [ + "cartesian", + "bar", + "histogram", + "oriented", + "errorBarsOK", + "showLegend" +]; Histogram.meta = { - description: [ - 'The sample data from which statistics are computed is set in `x`', - 'for vertically spanning histograms and', - 'in `y` for horizontally spanning histograms.', - 'Binning options are set `xbins` and `ybins` respectively', - 'if no aggregation data is provided.' - ].join(' ') + description: [ + "The sample data from which statistics are computed is set in `x`", + "for vertically spanning histograms and", + "in `y` for horizontally spanning histograms.", + "Binning options are set `xbins` and `ybins` respectively", + "if no aggregation data is provided." + ].join(" ") }; module.exports = Histogram; diff --git a/src/traces/histogram/norm_functions.js b/src/traces/histogram/norm_functions.js index 81381217e68..df06e7494b4 100644 --- a/src/traces/histogram/norm_functions.js +++ b/src/traces/histogram/norm_functions.js @@ -5,29 +5,32 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; module.exports = { - percent: function(size, total) { - var nMax = size.length, - norm = 100 / total; - for(var n = 0; n < nMax; n++) size[n] *= norm; - }, - probability: function(size, total) { - var nMax = size.length; - for(var n = 0; n < nMax; n++) size[n] /= total; - }, - density: function(size, total, inc, yinc) { - var nMax = size.length; - yinc = yinc || 1; - for(var n = 0; n < nMax; n++) size[n] *= inc[n] * yinc; - }, - 'probability density': function(size, total, inc, yinc) { - var nMax = size.length; - if(yinc) total /= yinc; - for(var n = 0; n < nMax; n++) size[n] *= inc[n] / total; + percent: function(size, total) { + var nMax = size.length, norm = 100 / total; + for (var n = 0; n < nMax; n++) { + size[n] *= norm; } + }, + probability: function(size, total) { + var nMax = size.length; + for (var n = 0; n < nMax; n++) { + size[n] /= total; + } + }, + density: function(size, total, inc, yinc) { + var nMax = size.length; + yinc = yinc || 1; + for (var n = 0; n < nMax; n++) { + size[n] *= inc[n] * yinc; + } + }, + "probability density": function(size, total, inc, yinc) { + var nMax = size.length; + if (yinc) total /= yinc; + for (var n = 0; n < nMax; n++) { + size[n] *= inc[n] / total; + } + } }; diff --git a/src/traces/histogram2d/attributes.js b/src/traces/histogram2d/attributes.js index 06d6e8eacaf..4916cd41552 100644 --- a/src/traces/histogram2d/attributes.js +++ b/src/traces/histogram2d/attributes.js @@ -5,46 +5,43 @@ * 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 histogramAttrs = require("../histogram/attributes"); +var heatmapAttrs = require("../heatmap/attributes"); +var colorscaleAttrs = require("../../components/colorscale/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); -'use strict'; +var extendFlat = require("../../lib/extend").extendFlat; -var histogramAttrs = require('../histogram/attributes'); -var heatmapAttrs = require('../heatmap/attributes'); -var colorscaleAttrs = require('../../components/colorscale/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; - -module.exports = extendFlat({}, - { - x: histogramAttrs.x, - y: histogramAttrs.y, - - z: { - valType: 'data_array', - description: 'Sets the aggregation data.' - }, - marker: { - color: { - valType: 'data_array', - description: 'Sets the aggregation data.' - } - }, - - histnorm: histogramAttrs.histnorm, - histfunc: histogramAttrs.histfunc, - autobinx: histogramAttrs.autobinx, - nbinsx: histogramAttrs.nbinsx, - xbins: histogramAttrs.xbins, - autobiny: histogramAttrs.autobiny, - nbinsy: histogramAttrs.nbinsy, - ybins: histogramAttrs.ybins, - - xgap: heatmapAttrs.xgap, - ygap: heatmapAttrs.ygap, - zsmooth: heatmapAttrs.zsmooth +module.exports = extendFlat( + {}, + { + x: histogramAttrs.x, + y: histogramAttrs.y, + z: { valType: "data_array", description: "Sets the aggregation data." }, + marker: { + color: { + valType: "data_array", + description: "Sets the aggregation data." + } }, - colorscaleAttrs, - { autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, {dflt: false}) }, - { colorbar: colorbarAttrs } + histnorm: histogramAttrs.histnorm, + histfunc: histogramAttrs.histfunc, + autobinx: histogramAttrs.autobinx, + nbinsx: histogramAttrs.nbinsx, + xbins: histogramAttrs.xbins, + autobiny: histogramAttrs.autobiny, + nbinsy: histogramAttrs.nbinsy, + ybins: histogramAttrs.ybins, + xgap: heatmapAttrs.xgap, + ygap: heatmapAttrs.ygap, + zsmooth: heatmapAttrs.zsmooth + }, + colorscaleAttrs, + { + autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, { + dflt: false + }) + }, + { colorbar: colorbarAttrs } ); diff --git a/src/traces/histogram2d/calc.js b/src/traces/histogram2d/calc.js index 602c5d9a545..ee13b282f07 100644 --- a/src/traces/histogram2d/calc.js +++ b/src/traces/histogram2d/calc.js @@ -5,197 +5,231 @@ * 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 Axes = require("../../plots/cartesian/axes"); - -'use strict'; - -var Lib = require('../../lib'); -var Axes = require('../../plots/cartesian/axes'); - -var binFunctions = require('../histogram/bin_functions'); -var normFunctions = require('../histogram/norm_functions'); -var doAvg = require('../histogram/average'); -var cleanBins = require('../histogram/clean_bins'); - +var binFunctions = require("../histogram/bin_functions"); +var normFunctions = require("../histogram/norm_functions"); +var doAvg = require("../histogram/average"); +var cleanBins = require("../histogram/clean_bins"); module.exports = function calc(gd, trace) { - var xa = Axes.getFromId(gd, trace.xaxis || 'x'), - x = trace.x ? xa.makeCalcdata(trace, 'x') : [], - ya = Axes.getFromId(gd, trace.yaxis || 'y'), - y = trace.y ? ya.makeCalcdata(trace, 'y') : [], - xcalendar = trace.xcalendar, - ycalendar = trace.ycalendar, - xr2c = function(v) { return xa.r2c(v, 0, xcalendar); }, - yr2c = function(v) { return ya.r2c(v, 0, ycalendar); }, - xc2r = function(v) { return xa.c2r(v, 0, xcalendar); }, - yc2r = function(v) { return ya.c2r(v, 0, ycalendar); }, - x0, - dx, - y0, - dy, - z, - i; - - cleanBins(trace, xa, 'x'); - cleanBins(trace, ya, 'y'); - - var serieslen = Math.min(x.length, y.length); - if(x.length > serieslen) x.splice(serieslen, x.length - serieslen); - if(y.length > serieslen) y.splice(serieslen, y.length - serieslen); - - - // calculate the bins - if(trace.autobinx || !('xbins' in trace)) { - trace.xbins = Axes.autoBin(x, xa, trace.nbinsx, '2d', xcalendar); - if(trace.type === 'histogram2dcontour') { - // the "true" last argument reverses the tick direction (which we can't - // just do with a minus sign because of month bins) - trace.xbins.start = xc2r(Axes.tickIncrement( - xr2c(trace.xbins.start), trace.xbins.size, true, xcalendar)); - trace.xbins.end = xc2r(Axes.tickIncrement( - xr2c(trace.xbins.end), trace.xbins.size, false, xcalendar)); - } - - // copy bin info back to the source data. - trace._input.xbins = trace.xbins; - } - if(trace.autobiny || !('ybins' in trace)) { - trace.ybins = Axes.autoBin(y, ya, trace.nbinsy, '2d', ycalendar); - if(trace.type === 'histogram2dcontour') { - trace.ybins.start = yc2r(Axes.tickIncrement( - yr2c(trace.ybins.start), trace.ybins.size, true, ycalendar)); - trace.ybins.end = yc2r(Axes.tickIncrement( - yr2c(trace.ybins.end), trace.ybins.size, false, ycalendar)); - } - trace._input.ybins = trace.ybins; - } - - // make the empty bin array & scale the map - z = []; - var onecol = [], - zerocol = [], - nonuniformBinsX = (typeof(trace.xbins.size) === 'string'), - nonuniformBinsY = (typeof(trace.ybins.size) === 'string'), - xbins = nonuniformBinsX ? [] : trace.xbins, - ybins = nonuniformBinsY ? [] : trace.ybins, - total = 0, - n, - m, - counts = [], - norm = trace.histnorm, - func = trace.histfunc, - densitynorm = (norm.indexOf('density') !== -1), - extremefunc = (func === 'max' || func === 'min'), - sizeinit = (extremefunc ? null : 0), - binfunc = binFunctions.count, - normfunc = normFunctions[norm], - doavg = false, - xinc = [], - yinc = []; - - // set a binning function other than count? - // for binning functions: check first for 'z', - // then 'mc' in case we had a colored scatter plot - // and want to transfer these colors to the 2D histo - // TODO: this is why we need a data picker in the popover... - var rawCounterData = ('z' in trace) ? - trace.z : - (('marker' in trace && Array.isArray(trace.marker.color)) ? - trace.marker.color : ''); - if(rawCounterData && func !== 'count') { - doavg = func === 'avg'; - binfunc = binFunctions[func]; - } - - // decrease end a little in case of rounding errors - var binspec = trace.xbins, - binStart = xr2c(binspec.start), - binEnd = xr2c(binspec.end) + - (binStart - Axes.tickIncrement(binStart, binspec.size, false, xcalendar)) / 1e6; - - for(i = binStart; i < binEnd; i = Axes.tickIncrement(i, binspec.size, false, xcalendar)) { - onecol.push(sizeinit); - if(nonuniformBinsX) xbins.push(i); - if(doavg) zerocol.push(0); - } - if(nonuniformBinsX) xbins.push(i); - - var nx = onecol.length; - x0 = trace.xbins.start; - var x0c = xr2c(x0); - dx = (i - x0c) / nx; - x0 = xc2r(x0c + dx / 2); - - binspec = trace.ybins; - binStart = yr2c(binspec.start); - binEnd = yr2c(binspec.end) + - (binStart - Axes.tickIncrement(binStart, binspec.size, false, ycalendar)) / 1e6; - - for(i = binStart; i < binEnd; i = Axes.tickIncrement(i, binspec.size, false, ycalendar)) { - z.push(onecol.concat()); - if(nonuniformBinsY) ybins.push(i); - if(doavg) counts.push(zerocol.concat()); - } - if(nonuniformBinsY) ybins.push(i); - - var ny = z.length; - y0 = trace.ybins.start; - var y0c = yr2c(y0); - dy = (i - y0c) / ny; - y0 = yc2r(y0c + dy / 2); - - if(densitynorm) { - xinc = onecol.map(function(v, i) { - if(nonuniformBinsX) return 1 / (xbins[i + 1] - xbins[i]); - return 1 / dx; - }); - yinc = z.map(function(v, i) { - if(nonuniformBinsY) return 1 / (ybins[i + 1] - ybins[i]); - return 1 / dy; - }); + var xa = Axes.getFromId(gd, trace.xaxis || "x"), + x = trace.x ? xa.makeCalcdata(trace, "x") : [], + ya = Axes.getFromId(gd, trace.yaxis || "y"), + y = trace.y ? ya.makeCalcdata(trace, "y") : [], + xcalendar = trace.xcalendar, + ycalendar = trace.ycalendar, + xr2c = function(v) { + return xa.r2c(v, 0, xcalendar); + }, + yr2c = function(v) { + return ya.r2c(v, 0, ycalendar); + }, + xc2r = function(v) { + return xa.c2r(v, 0, xcalendar); + }, + yc2r = function(v) { + return ya.c2r(v, 0, ycalendar); + }, + x0, + dx, + y0, + dy, + z, + i; + + cleanBins(trace, xa, "x"); + cleanBins(trace, ya, "y"); + + var serieslen = Math.min(x.length, y.length); + if (x.length > serieslen) x.splice(serieslen, x.length - serieslen); + if (y.length > serieslen) y.splice(serieslen, y.length - serieslen); + + // calculate the bins + if (trace.autobinx || !("xbins" in trace)) { + trace.xbins = Axes.autoBin(x, xa, trace.nbinsx, "2d", xcalendar); + if (trace.type === "histogram2dcontour") { + // the "true" last argument reverses the tick direction (which we can't + // just do with a minus sign because of month bins) + trace.xbins.start = xc2r( + Axes.tickIncrement( + xr2c(trace.xbins.start), + trace.xbins.size, + true, + xcalendar + ) + ); + trace.xbins.end = xc2r( + Axes.tickIncrement( + xr2c(trace.xbins.end), + trace.xbins.size, + false, + xcalendar + ) + ); } - // for date axes we need bin bounds to be calcdata. For nonuniform bins - // we already have this, but uniform with start/end/size they're still strings. - if(!nonuniformBinsX && xa.type === 'date') { - xbins = { - start: xr2c(xbins.start), - end: xr2c(xbins.end), - size: xbins.size - }; + // copy bin info back to the source data. + trace._input.xbins = trace.xbins; + } + if (trace.autobiny || !("ybins" in trace)) { + trace.ybins = Axes.autoBin(y, ya, trace.nbinsy, "2d", ycalendar); + if (trace.type === "histogram2dcontour") { + trace.ybins.start = yc2r( + Axes.tickIncrement( + yr2c(trace.ybins.start), + trace.ybins.size, + true, + ycalendar + ) + ); + trace.ybins.end = yc2r( + Axes.tickIncrement( + yr2c(trace.ybins.end), + trace.ybins.size, + false, + ycalendar + ) + ); } - if(!nonuniformBinsY && ya.type === 'date') { - ybins = { - start: yr2c(ybins.start), - end: yr2c(ybins.end), - size: ybins.size - }; - } - - - // put data into bins - for(i = 0; i < serieslen; i++) { - n = Lib.findBin(x[i], xbins); - m = Lib.findBin(y[i], ybins); - if(n >= 0 && n < nx && m >= 0 && m < ny) { - total += binfunc(n, i, z[m], rawCounterData, counts[m]); - } + trace._input.ybins = trace.ybins; + } + + // make the empty bin array & scale the map + z = []; + var onecol = [], + zerocol = [], + nonuniformBinsX = typeof trace.xbins.size === "string", + nonuniformBinsY = typeof trace.ybins.size === "string", + xbins = nonuniformBinsX ? [] : trace.xbins, + ybins = nonuniformBinsY ? [] : trace.ybins, + total = 0, + n, + m, + counts = [], + norm = trace.histnorm, + func = trace.histfunc, + densitynorm = norm.indexOf("density") !== -1, + extremefunc = func === "max" || func === "min", + sizeinit = extremefunc ? null : 0, + binfunc = binFunctions.count, + normfunc = normFunctions[norm], + doavg = false, + xinc = [], + yinc = []; + + // set a binning function other than count? + // for binning functions: check first for 'z', + // then 'mc' in case we had a colored scatter plot + // and want to transfer these colors to the 2D histo + // TODO: this is why we need a data picker in the popover... + var rawCounterData = "z" in trace + ? trace.z + : "marker" in trace && Array.isArray(trace.marker.color) + ? trace.marker.color + : ""; + if (rawCounterData && func !== "count") { + doavg = func === "avg"; + binfunc = binFunctions[func]; + } + + // decrease end a little in case of rounding errors + var binspec = trace.xbins, + binStart = xr2c(binspec.start), + binEnd = xr2c(binspec.end) + + (binStart - + Axes.tickIncrement(binStart, binspec.size, false, xcalendar)) / + 1e6; + + for ( + i = binStart; + i < binEnd; + i = Axes.tickIncrement(i, binspec.size, false, xcalendar) + ) { + onecol.push(sizeinit); + if (nonuniformBinsX) xbins.push(i); + if (doavg) zerocol.push(0); + } + if (nonuniformBinsX) xbins.push(i); + + var nx = onecol.length; + x0 = trace.xbins.start; + var x0c = xr2c(x0); + dx = (i - x0c) / nx; + x0 = xc2r(x0c + dx / 2); + + binspec = trace.ybins; + binStart = yr2c(binspec.start); + binEnd = yr2c(binspec.end) + + (binStart - Axes.tickIncrement(binStart, binspec.size, false, ycalendar)) / + 1e6; + + for ( + i = binStart; + i < binEnd; + i = Axes.tickIncrement(i, binspec.size, false, ycalendar) + ) { + z.push(onecol.concat()); + if (nonuniformBinsY) ybins.push(i); + if (doavg) counts.push(zerocol.concat()); + } + if (nonuniformBinsY) ybins.push(i); + + var ny = z.length; + y0 = trace.ybins.start; + var y0c = yr2c(y0); + dy = (i - y0c) / ny; + y0 = yc2r(y0c + dy / 2); + + if (densitynorm) { + xinc = onecol.map(function(v, i) { + if (nonuniformBinsX) return 1 / (xbins[i + 1] - xbins[i]); + return 1 / dx; + }); + yinc = z.map(function(v, i) { + if (nonuniformBinsY) return 1 / (ybins[i + 1] - ybins[i]); + return 1 / dy; + }); + } + + // for date axes we need bin bounds to be calcdata. For nonuniform bins + // we already have this, but uniform with start/end/size they're still strings. + if (!nonuniformBinsX && xa.type === "date") { + xbins = { + start: xr2c(xbins.start), + end: xr2c(xbins.end), + size: xbins.size + }; + } + if (!nonuniformBinsY && ya.type === "date") { + ybins = { + start: yr2c(ybins.start), + end: yr2c(ybins.end), + size: ybins.size + }; + } + + // put data into bins + for (i = 0; i < serieslen; i++) { + n = Lib.findBin(x[i], xbins); + m = Lib.findBin(y[i], ybins); + if (n >= 0 && n < nx && m >= 0 && m < ny) { + total += binfunc(n, i, z[m], rawCounterData, counts[m]); } - // normalize, if needed - if(doavg) { - for(m = 0; m < ny; m++) total += doAvg(z[m], counts[m]); + } + // normalize, if needed + if (doavg) { + for (m = 0; m < ny; m++) { + total += doAvg(z[m], counts[m]); } - if(normfunc) { - for(m = 0; m < ny; m++) normfunc(z[m], total, xinc, yinc[m]); + } + if (normfunc) { + for (m = 0; m < ny; m++) { + normfunc(z[m], total, xinc, yinc[m]); } + } - return { - x: x, - x0: x0, - dx: dx, - y: y, - y0: y0, - dy: dy, - z: z - }; + return { x: x, x0: x0, dx: dx, y: y, y0: y0, dy: dy, z: z }; }; diff --git a/src/traces/histogram2d/defaults.js b/src/traces/histogram2d/defaults.js index 05b1c6ebbc4..8bb7405cc1e 100644 --- a/src/traces/histogram2d/defaults.js +++ b/src/traces/histogram2d/defaults.js @@ -5,32 +5,34 @@ * 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 handleSampleDefaults = require('./sample_defaults'); -var colorscaleDefaults = require('../../components/colorscale/defaults'); -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - handleSampleDefaults(traceIn, traceOut, coerce, layout); - - var zsmooth = coerce('zsmooth'); - if(zsmooth === false) { - // ensure that xgap and ygap are coerced only when zsmooth allows them to have an effect. - coerce('xgap'); - coerce('ygap'); - } - - colorscaleDefaults( - traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} - ); +"use strict"; +var Lib = require("../../lib"); + +var handleSampleDefaults = require("./sample_defaults"); +var colorscaleDefaults = require("../../components/colorscale/defaults"); +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + handleSampleDefaults(traceIn, traceOut, coerce, layout); + + var zsmooth = coerce("zsmooth"); + if (zsmooth === false) { + // ensure that xgap and ygap are coerced only when zsmooth allows them to have an effect. + coerce("xgap"); + coerce("ygap"); + } + + colorscaleDefaults(traceIn, traceOut, layout, coerce, { + prefix: "", + cLetter: "z" + }); }; diff --git a/src/traces/histogram2d/index.js b/src/traces/histogram2d/index.js index fb0975bf58e..e827f19d2f5 100644 --- a/src/traces/histogram2d/index.js +++ b/src/traces/histogram2d/index.js @@ -5,34 +5,31 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var Histogram2D = {}; -Histogram2D.attributes = require('./attributes'); -Histogram2D.supplyDefaults = require('./defaults'); -Histogram2D.calc = require('../heatmap/calc'); -Histogram2D.plot = require('../heatmap/plot'); -Histogram2D.colorbar = require('../heatmap/colorbar'); -Histogram2D.style = require('../heatmap/style'); -Histogram2D.hoverPoints = require('../heatmap/hover'); +Histogram2D.attributes = require("./attributes"); +Histogram2D.supplyDefaults = require("./defaults"); +Histogram2D.calc = require("../heatmap/calc"); +Histogram2D.plot = require("../heatmap/plot"); +Histogram2D.colorbar = require("../heatmap/colorbar"); +Histogram2D.style = require("../heatmap/style"); +Histogram2D.hoverPoints = require("../heatmap/hover"); -Histogram2D.moduleType = 'trace'; -Histogram2D.name = 'histogram2d'; -Histogram2D.basePlotModule = require('../../plots/cartesian'); -Histogram2D.categories = ['cartesian', '2dMap', 'histogram']; +Histogram2D.moduleType = "trace"; +Histogram2D.name = "histogram2d"; +Histogram2D.basePlotModule = require("../../plots/cartesian"); +Histogram2D.categories = ["cartesian", "2dMap", "histogram"]; Histogram2D.meta = { - hrName: 'histogram_2d', - description: [ - 'The sample data from which statistics are computed is set in `x`', - 'and `y` (where `x` and `y` represent marginal distributions,', - 'binning is set in `xbins` and `ybins` in this case)', - 'or `z` (where `z` represent the 2D distribution and binning set,', - 'binning is set by `x` and `y` in this case).', - 'The resulting distribution is visualized as a heatmap.' - ].join(' ') + hrName: "histogram_2d", + description: [ + "The sample data from which statistics are computed is set in `x`", + "and `y` (where `x` and `y` represent marginal distributions,", + "binning is set in `xbins` and `ybins` in this case)", + "or `z` (where `z` represent the 2D distribution and binning set,", + "binning is set by `x` and `y` in this case).", + "The resulting distribution is visualized as a heatmap." + ].join(" ") }; module.exports = Histogram2D; diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js index c4483bb5022..a05a514ade6 100644 --- a/src/traces/histogram2d/sample_defaults.js +++ b/src/traces/histogram2d/sample_defaults.js @@ -5,34 +5,37 @@ * 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 Registry = require('../../registry'); -var handleBinDefaults = require('../histogram/bin_defaults'); - - -module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout) { - var x = coerce('x'), - y = coerce('y'); - - // we could try to accept x0 and dx, etc... - // but that's a pretty weird use case. - // for now require both x and y explicitly specified. - if(!(x && x.length && y && y.length)) { - traceOut.visible = false; - return; - } - - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); - handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); - - // if marker.color is an array, we can use it in aggregation instead of z - var hasAggregationData = coerce('z') || coerce('marker.color'); - - if(hasAggregationData) coerce('histfunc'); - - var binDirections = ['x', 'y']; - handleBinDefaults(traceIn, traceOut, coerce, binDirections); +"use strict"; +var Registry = require("../../registry"); +var handleBinDefaults = require("../histogram/bin_defaults"); + +module.exports = function handleSampleDefaults( + traceIn, + traceOut, + coerce, + layout +) { + var x = coerce("x"), y = coerce("y"); + + // we could try to accept x0 and dx, etc... + // but that's a pretty weird use case. + // for now require both x and y explicitly specified. + if (!(x && x.length && y && y.length)) { + traceOut.visible = false; + return; + } + + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleTraceDefaults" + ); + handleCalendarDefaults(traceIn, traceOut, ["x", "y"], layout); + + // if marker.color is an array, we can use it in aggregation instead of z + var hasAggregationData = coerce("z") || coerce("marker.color"); + + if (hasAggregationData) coerce("histfunc"); + + var binDirections = ["x", "y"]; + handleBinDefaults(traceIn, traceOut, coerce, binDirections); }; diff --git a/src/traces/histogram2dcontour/attributes.js b/src/traces/histogram2dcontour/attributes.js index 0a7ba7f36c5..a250b973f64 100644 --- a/src/traces/histogram2dcontour/attributes.js +++ b/src/traces/histogram2dcontour/attributes.js @@ -5,22 +5,21 @@ * 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 histogram2dAttrs = require("../histogram2d/attributes"); +var contourAttrs = require("../contour/attributes"); +var colorscaleAttrs = require("../../components/colorscale/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); -'use strict'; +var extendFlat = require("../../lib/extend").extendFlat; -var histogram2dAttrs = require('../histogram2d/attributes'); -var contourAttrs = require('../contour/attributes'); -var colorscaleAttrs = require('../../components/colorscale/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; - -module.exports = extendFlat({}, { +module.exports = extendFlat( + {}, + { x: histogram2dAttrs.x, y: histogram2dAttrs.y, z: histogram2dAttrs.z, marker: histogram2dAttrs.marker, - histnorm: histogram2dAttrs.histnorm, histfunc: histogram2dAttrs.histfunc, autobinx: histogram2dAttrs.autobinx, @@ -29,12 +28,11 @@ module.exports = extendFlat({}, { autobiny: histogram2dAttrs.autobiny, nbinsy: histogram2dAttrs.nbinsy, ybins: histogram2dAttrs.ybins, - autocontour: contourAttrs.autocontour, ncontours: contourAttrs.ncontours, contours: contourAttrs.contours, line: contourAttrs.line -}, - colorscaleAttrs, - { colorbar: colorbarAttrs } + }, + colorscaleAttrs, + { colorbar: colorbarAttrs } ); diff --git a/src/traces/histogram2dcontour/defaults.js b/src/traces/histogram2dcontour/defaults.js index 21a2dbc0755..e1e6eefc379 100644 --- a/src/traces/histogram2dcontour/defaults.js +++ b/src/traces/histogram2dcontour/defaults.js @@ -5,30 +5,36 @@ * 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 handleSampleDefaults = require('../histogram2d/sample_defaults'); -var handleStyleDefaults = require('../contour/style_defaults'); -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - handleSampleDefaults(traceIn, traceOut, coerce, layout); - - var contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'), - contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'), - autocontour = coerce('autocontour', !(contourStart && contourEnd)); - - if(autocontour) coerce('ncontours'); - else coerce('contours.size'); - - handleStyleDefaults(traceIn, traceOut, coerce, layout); +"use strict"; +var Lib = require("../../lib"); + +var handleSampleDefaults = require("../histogram2d/sample_defaults"); +var handleStyleDefaults = require("../contour/style_defaults"); +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + handleSampleDefaults(traceIn, traceOut, coerce, layout); + + var contourStart = Lib.coerce2( + traceIn, + traceOut, + attributes, + "contours.start" + ), + contourEnd = Lib.coerce2(traceIn, traceOut, attributes, "contours.end"), + autocontour = coerce("autocontour", !(contourStart && contourEnd)); + + if (autocontour) coerce("ncontours"); + else coerce("contours.size"); + + handleStyleDefaults(traceIn, traceOut, coerce, layout); }; diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js index 1f4e8ef5d84..e397230debc 100644 --- a/src/traces/histogram2dcontour/index.js +++ b/src/traces/histogram2dcontour/index.js @@ -5,34 +5,31 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var Histogram2dContour = {}; -Histogram2dContour.attributes = require('./attributes'); -Histogram2dContour.supplyDefaults = require('./defaults'); -Histogram2dContour.calc = require('../contour/calc'); -Histogram2dContour.plot = require('../contour/plot'); -Histogram2dContour.style = require('../contour/style'); -Histogram2dContour.colorbar = require('../contour/colorbar'); -Histogram2dContour.hoverPoints = require('../contour/hover'); +Histogram2dContour.attributes = require("./attributes"); +Histogram2dContour.supplyDefaults = require("./defaults"); +Histogram2dContour.calc = require("../contour/calc"); +Histogram2dContour.plot = require("../contour/plot"); +Histogram2dContour.style = require("../contour/style"); +Histogram2dContour.colorbar = require("../contour/colorbar"); +Histogram2dContour.hoverPoints = require("../contour/hover"); -Histogram2dContour.moduleType = 'trace'; -Histogram2dContour.name = 'histogram2dcontour'; -Histogram2dContour.basePlotModule = require('../../plots/cartesian'); -Histogram2dContour.categories = ['cartesian', '2dMap', 'contour', 'histogram']; +Histogram2dContour.moduleType = "trace"; +Histogram2dContour.name = "histogram2dcontour"; +Histogram2dContour.basePlotModule = require("../../plots/cartesian"); +Histogram2dContour.categories = ["cartesian", "2dMap", "contour", "histogram"]; Histogram2dContour.meta = { - hrName: 'histogram_2d_contour', - description: [ - 'The sample data from which statistics are computed is set in `x`', - 'and `y` (where `x` and `y` represent marginal distributions,', - 'binning is set in `xbins` and `ybins` in this case)', - 'or `z` (where `z` represent the 2D distribution and binning set,', - 'binning is set by `x` and `y` in this case).', - 'The resulting distribution is visualized as a contour plot.' - ].join(' ') + hrName: "histogram_2d_contour", + description: [ + "The sample data from which statistics are computed is set in `x`", + "and `y` (where `x` and `y` represent marginal distributions,", + "binning is set in `xbins` and `ybins` in this case)", + "or `z` (where `z` represent the 2D distribution and binning set,", + "binning is set by `x` and `y` in this case).", + "The resulting distribution is visualized as a contour plot." + ].join(" ") }; module.exports = Histogram2dContour; diff --git a/src/traces/mesh3d/attributes.js b/src/traces/mesh3d/attributes.js index 3796850f51a..54317d71304 100644 --- a/src/traces/mesh3d/attributes.js +++ b/src/traces/mesh3d/attributes.js @@ -5,192 +5,180 @@ * 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 colorscaleAttrs = require("../../components/colorscale/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); +var surfaceAtts = require("../surface/attributes"); -'use strict'; - -var colorscaleAttrs = require('../../components/colorscale/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); -var surfaceAtts = require('../surface/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; - +var extendFlat = require("../../lib/extend").extendFlat; module.exports = { - x: { - valType: 'data_array', - description: [ - 'Sets the X coordinates of the vertices. The nth element of vectors `x`, `y` and `z`', - 'jointly represent the X, Y and Z coordinates of the nth vertex.' - ].join(' ') - }, - y: { - valType: 'data_array', - description: [ - 'Sets the Y coordinates of the vertices. The nth element of vectors `x`, `y` and `z`', - 'jointly represent the X, Y and Z coordinates of the nth vertex.' - ].join(' ') - }, - z: { - valType: 'data_array', - description: [ - 'Sets the Z coordinates of the vertices. The nth element of vectors `x`, `y` and `z`', - 'jointly represent the X, Y and Z coordinates of the nth vertex.' - ].join(' ') - }, - - i: { - valType: 'data_array', - description: [ - 'A vector of vertex indices, i.e. integer values between 0 and the length of the vertex', - 'vectors, representing the *first* vertex of a triangle. For example, `{i[m], j[m], k[m]}`', - 'together represent face m (triangle m) in the mesh, where `i[m] = n` points to the triplet', - '`{x[n], y[n], z[n]}` in the vertex arrays. Therefore, each element in `i` represents a', - 'point in space, which is the first vertex of a triangle.' - ].join(' ') - }, - j: { - valType: 'data_array', - description: [ - 'A vector of vertex indices, i.e. integer values between 0 and the length of the vertex', - 'vectors, representing the *second* vertex of a triangle. For example, `{i[m], j[m], k[m]}` ', - 'together represent face m (triangle m) in the mesh, where `j[m] = n` points to the triplet', - '`{x[n], y[n], z[n]}` in the vertex arrays. Therefore, each element in `j` represents a', - 'point in space, which is the second vertex of a triangle.' - ].join(' ') - - }, - k: { - valType: 'data_array', - description: [ - 'A vector of vertex indices, i.e. integer values between 0 and the length of the vertex', - 'vectors, representing the *third* vertex of a triangle. For example, `{i[m], j[m], k[m]}`', - 'together represent face m (triangle m) in the mesh, where `k[m] = n` points to the triplet ', - '`{x[n], y[n], z[n]}` in the vertex arrays. Therefore, each element in `k` represents a', - 'point in space, which is the third vertex of a triangle.' - ].join(' ') - - }, - - delaunayaxis: { - valType: 'enumerated', - role: 'info', - values: [ 'x', 'y', 'z' ], - dflt: 'z', - description: [ - 'Sets the Delaunay axis, which is the axis that is perpendicular to the surface of the', - 'Delaunay triangulation.', - 'It has an effect if `i`, `j`, `k` are not provided and `alphahull` is set to indicate', - 'Delaunay triangulation.' - ].join(' ') - }, - - alphahull: { - valType: 'number', - role: 'style', - dflt: -1, - description: [ - 'Determines how the mesh surface triangles are derived from the set of', - 'vertices (points) represented by the `x`, `y` and `z` arrays, if', - 'the `i`, `j`, `k` arrays are not supplied.', - 'For general use of `mesh3d` it is preferred that `i`, `j`, `k` are', - 'supplied.', - - 'If *-1*, Delaunay triangulation is used, which is mainly suitable if the', - 'mesh is a single, more or less layer surface that is perpendicular to `delaunayaxis`.', - 'In case the `delaunayaxis` intersects the mesh surface at more than one point', - 'it will result triangles that are very long in the dimension of `delaunayaxis`.', - - 'If *>0*, the alpha-shape algorithm is used. In this case, the positive `alphahull` value', - 'signals the use of the alpha-shape algorithm, _and_ its value', - 'acts as the parameter for the mesh fitting.', - - 'If *0*, the convex-hull algorithm is used. It is suitable for convex bodies', - 'or if the intention is to enclose the `x`, `y` and `z` point set into a convex', - 'hull.' - ].join(' ') - }, - - intensity: { - valType: 'data_array', - description: [ - 'Sets the vertex intensity values,', - 'used for plotting fields on meshes' - ].join(' ') - }, - - // Color field - color: { - valType: 'color', - role: 'style', - description: 'Sets the color of the whole mesh' - }, - vertexcolor: { - valType: 'data_array', // FIXME: this should be a color array - role: 'style', - description: [ - 'Sets the color of each vertex', - 'Overrides *color*.' - ].join(' ') - }, - facecolor: { - valType: 'data_array', - role: 'style', - description: [ - 'Sets the color of each face', - 'Overrides *color* and *vertexcolor*.' - ].join(' ') - }, - - // Opacity - opacity: extendFlat({}, surfaceAtts.opacity), - - // Flat shaded mode - flatshading: { - valType: 'boolean', - role: 'style', - dflt: false, - description: [ - 'Determines whether or not normal smoothing is applied to the meshes,', - 'creating meshes with an angular, low-poly look via flat reflections.' - ].join(' ') - }, - - contour: { - show: extendFlat({}, surfaceAtts.contours.x.show, { - description: [ - 'Sets whether or not dynamic contours are shown on hover' - ].join(' ') - }), - color: extendFlat({}, surfaceAtts.contours.x.color), - width: extendFlat({}, surfaceAtts.contours.x.width) - }, - - colorscale: colorscaleAttrs.colorscale, - reversescale: colorscaleAttrs.reversescale, - showscale: colorscaleAttrs.showscale, - colorbar: colorbarAttrs, - - lightposition: { - 'x': extendFlat({}, surfaceAtts.lightposition.x, {dflt: 1e5}), - 'y': extendFlat({}, surfaceAtts.lightposition.y, {dflt: 1e5}), - 'z': extendFlat({}, surfaceAtts.lightposition.z, {dflt: 0}) + x: { + valType: "data_array", + description: [ + "Sets the X coordinates of the vertices. The nth element of vectors `x`, `y` and `z`", + "jointly represent the X, Y and Z coordinates of the nth vertex." + ].join(" ") + }, + y: { + valType: "data_array", + description: [ + "Sets the Y coordinates of the vertices. The nth element of vectors `x`, `y` and `z`", + "jointly represent the X, Y and Z coordinates of the nth vertex." + ].join(" ") + }, + z: { + valType: "data_array", + description: [ + "Sets the Z coordinates of the vertices. The nth element of vectors `x`, `y` and `z`", + "jointly represent the X, Y and Z coordinates of the nth vertex." + ].join(" ") + }, + i: { + valType: "data_array", + description: [ + "A vector of vertex indices, i.e. integer values between 0 and the length of the vertex", + "vectors, representing the *first* vertex of a triangle. For example, `{i[m], j[m], k[m]}`", + "together represent face m (triangle m) in the mesh, where `i[m] = n` points to the triplet", + "`{x[n], y[n], z[n]}` in the vertex arrays. Therefore, each element in `i` represents a", + "point in space, which is the first vertex of a triangle." + ].join(" ") + }, + j: { + valType: "data_array", + description: [ + "A vector of vertex indices, i.e. integer values between 0 and the length of the vertex", + "vectors, representing the *second* vertex of a triangle. For example, `{i[m], j[m], k[m]}` ", + "together represent face m (triangle m) in the mesh, where `j[m] = n` points to the triplet", + "`{x[n], y[n], z[n]}` in the vertex arrays. Therefore, each element in `j` represents a", + "point in space, which is the second vertex of a triangle." + ].join(" ") + }, + k: { + valType: "data_array", + description: [ + "A vector of vertex indices, i.e. integer values between 0 and the length of the vertex", + "vectors, representing the *third* vertex of a triangle. For example, `{i[m], j[m], k[m]}`", + "together represent face m (triangle m) in the mesh, where `k[m] = n` points to the triplet ", + "`{x[n], y[n], z[n]}` in the vertex arrays. Therefore, each element in `k` represents a", + "point in space, which is the third vertex of a triangle." + ].join(" ") + }, + delaunayaxis: { + valType: "enumerated", + role: "info", + values: ["x", "y", "z"], + dflt: "z", + description: [ + "Sets the Delaunay axis, which is the axis that is perpendicular to the surface of the", + "Delaunay triangulation.", + "It has an effect if `i`, `j`, `k` are not provided and `alphahull` is set to indicate", + "Delaunay triangulation." + ].join(" ") + }, + alphahull: { + valType: "number", + role: "style", + dflt: -1, + description: [ + "Determines how the mesh surface triangles are derived from the set of", + "vertices (points) represented by the `x`, `y` and `z` arrays, if", + "the `i`, `j`, `k` arrays are not supplied.", + "For general use of `mesh3d` it is preferred that `i`, `j`, `k` are", + "supplied.", + "If *-1*, Delaunay triangulation is used, which is mainly suitable if the", + "mesh is a single, more or less layer surface that is perpendicular to `delaunayaxis`.", + "In case the `delaunayaxis` intersects the mesh surface at more than one point", + "it will result triangles that are very long in the dimension of `delaunayaxis`.", + "If *>0*, the alpha-shape algorithm is used. In this case, the positive `alphahull` value", + "signals the use of the alpha-shape algorithm, _and_ its value", + "acts as the parameter for the mesh fitting.", + "If *0*, the convex-hull algorithm is used. It is suitable for convex bodies", + "or if the intention is to enclose the `x`, `y` and `z` point set into a convex", + "hull." + ].join(" ") + }, + intensity: { + valType: "data_array", + description: [ + "Sets the vertex intensity values,", + "used for plotting fields on meshes" + ].join(" ") + }, + // Color field + color: { + valType: "color", + role: "style", + description: "Sets the color of the whole mesh" + }, + vertexcolor: { + valType: "data_array", + // FIXME: this should be a color array + role: "style", + description: ["Sets the color of each vertex", "Overrides *color*."].join( + " " + ) + }, + facecolor: { + valType: "data_array", + role: "style", + description: [ + "Sets the color of each face", + "Overrides *color* and *vertexcolor*." + ].join(" ") + }, + // Opacity + opacity: extendFlat({}, surfaceAtts.opacity), + // Flat shaded mode + flatshading: { + valType: "boolean", + role: "style", + dflt: false, + description: [ + "Determines whether or not normal smoothing is applied to the meshes,", + "creating meshes with an angular, low-poly look via flat reflections." + ].join(" ") + }, + contour: { + show: extendFlat({}, surfaceAtts.contours.x.show, { + description: [ + "Sets whether or not dynamic contours are shown on hover" + ].join(" ") + }), + color: extendFlat({}, surfaceAtts.contours.x.color), + width: extendFlat({}, surfaceAtts.contours.x.width) + }, + colorscale: colorscaleAttrs.colorscale, + reversescale: colorscaleAttrs.reversescale, + showscale: colorscaleAttrs.showscale, + colorbar: colorbarAttrs, + lightposition: { + x: extendFlat({}, surfaceAtts.lightposition.x, { dflt: 1e5 }), + y: extendFlat({}, surfaceAtts.lightposition.y, { dflt: 1e5 }), + z: extendFlat({}, surfaceAtts.lightposition.z, { dflt: 0 }) + }, + lighting: extendFlat( + {}, + { + vertexnormalsepsilon: { + valType: "number", + role: "style", + min: 0.00, + max: 1, + dflt: 1e-12, + // otherwise finely tessellated things eg. the brain will have no specular light reflection + description: "Epsilon for vertex normals calculation avoids math issues arising from degenerate geometry." + }, + facenormalsepsilon: { + valType: "number", + role: "style", + min: 0.00, + max: 1, + dflt: 1e-6, + // even the brain model doesn't appear to need finer than this + description: "Epsilon for face normals calculation avoids math issues arising from degenerate geometry." + } }, - lighting: extendFlat({}, { - vertexnormalsepsilon: { - valType: 'number', - role: 'style', - min: 0.00, - max: 1, - dflt: 1e-12, // otherwise finely tessellated things eg. the brain will have no specular light reflection - description: 'Epsilon for vertex normals calculation avoids math issues arising from degenerate geometry.' - }, - facenormalsepsilon: { - valType: 'number', - role: 'style', - min: 0.00, - max: 1, - dflt: 1e-6, // even the brain model doesn't appear to need finer than this - description: 'Epsilon for face normals calculation avoids math issues arising from degenerate geometry.' - } - }, surfaceAtts.lighting) + surfaceAtts.lighting + ) }; diff --git a/src/traces/mesh3d/convert.js b/src/traces/mesh3d/convert.js index 13768b0b62d..2731585c800 100644 --- a/src/traces/mesh3d/convert.js +++ b/src/traces/mesh3d/convert.js @@ -5,156 +5,149 @@ * 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 createMesh = require("gl-mesh3d"); +var tinycolor = require("tinycolor2"); +var triangulate = require("delaunay-triangulate"); +var alphaShape = require("alpha-shape"); +var convexHull = require("convex-hull"); - -'use strict'; - -var createMesh = require('gl-mesh3d'); -var tinycolor = require('tinycolor2'); -var triangulate = require('delaunay-triangulate'); -var alphaShape = require('alpha-shape'); -var convexHull = require('convex-hull'); - -var str2RgbaArray = require('../../lib/str2rgbarray'); - +var str2RgbaArray = require("../../lib/str2rgbarray"); function Mesh3DTrace(scene, mesh, uid) { - this.scene = scene; - this.uid = uid; - this.mesh = mesh; - this.name = ''; - this.color = '#fff'; - this.data = null; - this.showContour = false; + this.scene = scene; + this.uid = uid; + this.mesh = mesh; + this.name = ""; + this.color = "#fff"; + this.data = null; + this.showContour = false; } var proto = Mesh3DTrace.prototype; proto.handlePick = function(selection) { - if(selection.object === this.mesh) { - var selectIndex = selection.data.index; + if (selection.object === this.mesh) { + var selectIndex = selection.data.index; - selection.traceCoordinate = [ - this.data.x[selectIndex], - this.data.y[selectIndex], - this.data.z[selectIndex] - ]; + selection.traceCoordinate = [ + this.data.x[selectIndex], + this.data.y[selectIndex], + this.data.z[selectIndex] + ]; - return true; - } + return true; + } }; function parseColorScale(colorscale) { - return colorscale.map(function(elem) { - var index = elem[0]; - var color = tinycolor(elem[1]); - var rgb = color.toRgb(); - return { - index: index, - rgb: [rgb.r, rgb.g, rgb.b, 1] - }; - }); + return colorscale.map(function(elem) { + var index = elem[0]; + var color = tinycolor(elem[1]); + var rgb = color.toRgb(); + return { index: index, rgb: [rgb.r, rgb.g, rgb.b, 1] }; + }); } function parseColorArray(colors) { - return colors.map(str2RgbaArray); + return colors.map(str2RgbaArray); } function zip3(x, y, z) { - var result = new Array(x.length); - for(var i = 0; i < x.length; ++i) { - result[i] = [x[i], y[i], z[i]]; - } - return result; + var result = new Array(x.length); + for (var i = 0; i < x.length; ++i) { + result[i] = [x[i], y[i], z[i]]; + } + return result; } proto.update = function(data) { - var scene = this.scene, - layout = scene.fullSceneLayout; - - this.data = data; - - // Unpack position data - function toDataCoords(axis, coord, scale, calendar) { - return coord.map(function(x) { - return axis.d2l(x, 0, calendar) * scale; - }); - } - - var positions = zip3( - toDataCoords(layout.xaxis, data.x, scene.dataScale[0], data.xcalendar), - toDataCoords(layout.yaxis, data.y, scene.dataScale[1], data.ycalendar), - toDataCoords(layout.zaxis, data.z, scene.dataScale[2], data.zcalendar)); - - var cells; - if(data.i && data.j && data.k) { - cells = zip3(data.i, data.j, data.k); - } - else if(data.alphahull === 0) { - cells = convexHull(positions); - } - else if(data.alphahull > 0) { - cells = alphaShape(data.alphahull, positions); - } - else { - var d = ['x', 'y', 'z'].indexOf(data.delaunayaxis); - cells = triangulate(positions.map(function(c) { - return [c[(d + 1) % 3], c[(d + 2) % 3]]; - })); - } - - var config = { - positions: positions, - cells: cells, - lightPosition: [data.lightposition.x, data.lightposition.y, data.lightposition.z], - ambient: data.lighting.ambient, - diffuse: data.lighting.diffuse, - specular: data.lighting.specular, - roughness: data.lighting.roughness, - fresnel: data.lighting.fresnel, - vertexNormalsEpsilon: data.lighting.vertexnormalsepsilon, - faceNormalsEpsilon: data.lighting.facenormalsepsilon, - opacity: data.opacity, - contourEnable: data.contour.show, - contourColor: str2RgbaArray(data.contour.color).slice(0, 3), - contourWidth: data.contour.width, - useFacetNormals: data.flatshading - }; - - if(data.intensity) { - this.color = '#fff'; - config.vertexIntensity = data.intensity; - config.colormap = parseColorScale(data.colorscale); - } - else if(data.vertexcolor) { - this.color = data.vertexcolors[0]; - config.vertexColors = parseColorArray(data.vertexcolor); - } - else if(data.facecolor) { - this.color = data.facecolor[0]; - config.cellColors = parseColorArray(data.facecolor); - } - else { - this.color = data.color; - config.meshColor = str2RgbaArray(data.color); - } - - // Update mesh - this.mesh.update(config); + var scene = this.scene, layout = scene.fullSceneLayout; + + this.data = data; + + // Unpack position data + function toDataCoords(axis, coord, scale, calendar) { + return coord.map(function(x) { + return axis.d2l(x, 0, calendar) * scale; + }); + } + + var positions = zip3( + toDataCoords(layout.xaxis, data.x, scene.dataScale[0], data.xcalendar), + toDataCoords(layout.yaxis, data.y, scene.dataScale[1], data.ycalendar), + toDataCoords(layout.zaxis, data.z, scene.dataScale[2], data.zcalendar) + ); + + var cells; + if (data.i && data.j && data.k) { + cells = zip3(data.i, data.j, data.k); + } else if (data.alphahull === 0) { + cells = convexHull(positions); + } else if (data.alphahull > 0) { + cells = alphaShape(data.alphahull, positions); + } else { + var d = ["x", "y", "z"].indexOf(data.delaunayaxis); + cells = triangulate( + positions.map(function(c) { + return [c[(d + 1) % 3], c[(d + 2) % 3]]; + }) + ); + } + + var config = { + positions: positions, + cells: cells, + lightPosition: [ + data.lightposition.x, + data.lightposition.y, + data.lightposition.z + ], + ambient: data.lighting.ambient, + diffuse: data.lighting.diffuse, + specular: data.lighting.specular, + roughness: data.lighting.roughness, + fresnel: data.lighting.fresnel, + vertexNormalsEpsilon: data.lighting.vertexnormalsepsilon, + faceNormalsEpsilon: data.lighting.facenormalsepsilon, + opacity: data.opacity, + contourEnable: data.contour.show, + contourColor: str2RgbaArray(data.contour.color).slice(0, 3), + contourWidth: data.contour.width, + useFacetNormals: data.flatshading + }; + + if (data.intensity) { + this.color = "#fff"; + config.vertexIntensity = data.intensity; + config.colormap = parseColorScale(data.colorscale); + } else if (data.vertexcolor) { + this.color = data.vertexcolors[0]; + config.vertexColors = parseColorArray(data.vertexcolor); + } else if (data.facecolor) { + this.color = data.facecolor[0]; + config.cellColors = parseColorArray(data.facecolor); + } else { + this.color = data.color; + config.meshColor = str2RgbaArray(data.color); + } + + // Update mesh + this.mesh.update(config); }; proto.dispose = function() { - this.scene.glplot.remove(this.mesh); - this.mesh.dispose(); + this.scene.glplot.remove(this.mesh); + this.mesh.dispose(); }; function createMesh3DTrace(scene, data) { - var gl = scene.glplot.gl; - var mesh = createMesh({gl: gl}); - var result = new Mesh3DTrace(scene, mesh, data.uid); - result.update(data); - scene.glplot.add(mesh); - return result; + var gl = scene.glplot.gl; + var mesh = createMesh({ gl: gl }); + var result = new Mesh3DTrace(scene, mesh, data.uid); + result.update(data); + scene.glplot.add(mesh); + return result; } module.exports = createMesh3DTrace; diff --git a/src/traces/mesh3d/defaults.js b/src/traces/mesh3d/defaults.js index d731a077cf3..9a6914936f0 100644 --- a/src/traces/mesh3d/defaults.js +++ b/src/traces/mesh3d/defaults.js @@ -5,95 +5,103 @@ * 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 Registry = require('../../registry'); -var Lib = require('../../lib'); -var colorbarDefaults = require('../../components/colorbar/defaults'); -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - // read in face/vertex properties - function readComponents(array) { - var ret = array.map(function(attr) { - var result = coerce(attr); - - if(result && Array.isArray(result)) return result; - return null; - }); - - return ret.every(function(x) { - return x && x.length === ret[0].length; - }) && ret; - } - - var coords = readComponents(['x', 'y', 'z']); - var indices = readComponents(['i', 'j', 'k']); - - if(!coords) { - traceOut.visible = false; - return; - } - - if(indices) { - // otherwise, convert all face indices to ints - indices.forEach(function(index) { - for(var i = 0; i < index.length; ++i) index[i] |= 0; - }); - } - - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); - handleCalendarDefaults(traceIn, traceOut, ['x', 'y', 'z'], layout); - - // Coerce remaining properties - [ - 'lighting.ambient', - 'lighting.diffuse', - 'lighting.specular', - 'lighting.roughness', - 'lighting.fresnel', - 'lighting.vertexnormalsepsilon', - 'lighting.facenormalsepsilon', - 'lightposition.x', - 'lightposition.y', - 'lightposition.z', - 'contour.show', - 'contour.color', - 'contour.width', - 'colorscale', - 'reversescale', - 'flatshading', - 'alphahull', - 'delaunayaxis', - 'opacity' - ].forEach(function(x) { coerce(x); }); - - if('intensity' in traceIn) { - coerce('intensity'); - coerce('showscale', true); - } - else { - traceOut.showscale = false; - - if('vertexcolor' in traceIn) coerce('vertexcolor'); - else if('facecolor' in traceIn) coerce('facecolor'); - else coerce('color', defaultColor); - } - - if(traceOut.reversescale) { - traceOut.colorscale = traceOut.colorscale.map(function(si) { - return [1 - si[0], si[1]]; - }).reverse(); - } - - if(traceOut.showscale) { - colorbarDefaults(traceIn, traceOut, layout); - } +"use strict"; +var Registry = require("../../registry"); +var Lib = require("../../lib"); +var colorbarDefaults = require("../../components/colorbar/defaults"); +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + // read in face/vertex properties + function readComponents(array) { + var ret = array.map(function(attr) { + var result = coerce(attr); + + if (result && Array.isArray(result)) return result; + return null; + }); + + return ret.every(function(x) { + return x && x.length === ret[0].length; + }) && + ret; + } + + var coords = readComponents(["x", "y", "z"]); + var indices = readComponents(["i", "j", "k"]); + + if (!coords) { + traceOut.visible = false; + return; + } + + if (indices) { + // otherwise, convert all face indices to ints + indices.forEach(function(index) { + for (var i = 0; i < index.length; ++i) index[i] |= 0; + }); + } + + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleTraceDefaults" + ); + handleCalendarDefaults(traceIn, traceOut, ["x", "y", "z"], layout); + + // Coerce remaining properties + [ + "lighting.ambient", + "lighting.diffuse", + "lighting.specular", + "lighting.roughness", + "lighting.fresnel", + "lighting.vertexnormalsepsilon", + "lighting.facenormalsepsilon", + "lightposition.x", + "lightposition.y", + "lightposition.z", + "contour.show", + "contour.color", + "contour.width", + "colorscale", + "reversescale", + "flatshading", + "alphahull", + "delaunayaxis", + "opacity" + ].forEach(function(x) { + coerce(x); + }); + + if ("intensity" in traceIn) { + coerce("intensity"); + coerce("showscale", true); + } else { + traceOut.showscale = false; + + if ("vertexcolor" in traceIn) coerce("vertexcolor"); + else if ("facecolor" in traceIn) coerce("facecolor"); + else coerce("color", defaultColor); + } + + if (traceOut.reversescale) { + traceOut.colorscale = traceOut.colorscale + .map(function(si) { + return [1 - si[0], si[1]]; + }) + .reverse(); + } + + if (traceOut.showscale) { + colorbarDefaults(traceIn, traceOut, layout); + } }; diff --git a/src/traces/mesh3d/index.js b/src/traces/mesh3d/index.js index 34eed6c6aa9..7f38e26c812 100644 --- a/src/traces/mesh3d/index.js +++ b/src/traces/mesh3d/index.js @@ -5,30 +5,26 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var Mesh3D = {}; -Mesh3D.attributes = require('./attributes'); -Mesh3D.supplyDefaults = require('./defaults'); -Mesh3D.colorbar = require('../heatmap/colorbar'); -Mesh3D.plot = require('./convert'); +Mesh3D.attributes = require("./attributes"); +Mesh3D.supplyDefaults = require("./defaults"); +Mesh3D.colorbar = require("../heatmap/colorbar"); +Mesh3D.plot = require("./convert"); -Mesh3D.moduleType = 'trace'; -Mesh3D.name = 'mesh3d', -Mesh3D.basePlotModule = require('../../plots/gl3d'); -Mesh3D.categories = ['gl3d']; +Mesh3D.moduleType = "trace"; +Mesh3D.name = "mesh3d", Mesh3D.basePlotModule = require("../../plots/gl3d"); +Mesh3D.categories = ["gl3d"]; Mesh3D.meta = { - description: [ - 'Draws sets of triangles with coordinates given by', - 'three 1-dimensional arrays in `x`, `y`, `z` and', - '(1) a sets of `i`, `j`, `k` indices', - '(2) Delaunay triangulation or', - '(3) the Alpha-shape algorithm or', - '(4) the Convex-hull algorithm' - ].join(' ') + description: [ + "Draws sets of triangles with coordinates given by", + "three 1-dimensional arrays in `x`, `y`, `z` and", + "(1) a sets of `i`, `j`, `k` indices", + "(2) Delaunay triangulation or", + "(3) the Alpha-shape algorithm or", + "(4) the Convex-hull algorithm" + ].join(" ") }; module.exports = Mesh3D; diff --git a/src/traces/ohlc/attributes.js b/src/traces/ohlc/attributes.js index 02d50c76486..3ca850fd2c1 100644 --- a/src/traces/ohlc/attributes.js +++ b/src/traces/ohlc/attributes.js @@ -5,129 +5,110 @@ * 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 scatterAttrs = require("../scatter/attributes"); - -'use strict'; - -var Lib = require('../../lib'); -var scatterAttrs = require('../scatter/attributes'); - -var INCREASING_COLOR = '#3D9970'; -var DECREASING_COLOR = '#FF4136'; +var INCREASING_COLOR = "#3D9970"; +var DECREASING_COLOR = "#FF4136"; var lineAttrs = scatterAttrs.line; var directionAttrs = { - name: { - valType: 'string', - role: 'info', - description: [ - 'Sets the segment name.', - 'The segment name appear as the legend item and on hover.' - ].join(' ') - }, - - showlegend: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines whether or not an item corresponding to this', - 'segment is shown in the legend.' - ].join(' ') - }, - - line: { - color: Lib.extendFlat({}, lineAttrs.color), - width: Lib.extendFlat({}, lineAttrs.width), - dash: Lib.extendFlat({}, lineAttrs.dash), - } + name: { + valType: "string", + role: "info", + description: [ + "Sets the segment name.", + "The segment name appear as the legend item and on hover." + ].join(" ") + }, + showlegend: { + valType: "boolean", + role: "info", + dflt: true, + description: [ + "Determines whether or not an item corresponding to this", + "segment is shown in the legend." + ].join(" ") + }, + line: { + color: Lib.extendFlat({}, lineAttrs.color), + width: Lib.extendFlat({}, lineAttrs.width), + dash: Lib.extendFlat({}, lineAttrs.dash) + } }; module.exports = { - - x: { - valType: 'data_array', - description: [ - 'Sets the x coordinates.', - 'If absent, linear coordinate will be generated.' - ].join(' ') - }, - - open: { - valType: 'data_array', - dflt: [], - description: 'Sets the open values.' - }, - - high: { - valType: 'data_array', - dflt: [], - description: 'Sets the high values.' - }, - - low: { - valType: 'data_array', - dflt: [], - description: 'Sets the low values.' - }, - - close: { - valType: 'data_array', - dflt: [], - description: 'Sets the close values.' - }, - - line: { - width: Lib.extendFlat({}, lineAttrs.width, { - description: [ - lineAttrs.width, - 'Note that this style setting can also be set per', - 'direction via `increasing.line.width` and', - '`decreasing.line.width`.' - ].join(' ') - }), - dash: Lib.extendFlat({}, lineAttrs.dash, { - description: [ - lineAttrs.dash, - 'Note that this style setting can also be set per', - 'direction via `increasing.line.dash` and', - '`decreasing.line.dash`.' - ].join(' ') - }), - }, - - increasing: Lib.extendDeep({}, directionAttrs, { - line: { color: { dflt: INCREASING_COLOR } } + x: { + valType: "data_array", + description: [ + "Sets the x coordinates.", + "If absent, linear coordinate will be generated." + ].join(" ") + }, + open: { + valType: "data_array", + dflt: [], + description: "Sets the open values." + }, + high: { + valType: "data_array", + dflt: [], + description: "Sets the high values." + }, + low: { valType: "data_array", dflt: [], description: "Sets the low values." }, + close: { + valType: "data_array", + dflt: [], + description: "Sets the close values." + }, + line: { + width: Lib.extendFlat({}, lineAttrs.width, { + description: [ + lineAttrs.width, + "Note that this style setting can also be set per", + "direction via `increasing.line.width` and", + "`decreasing.line.width`." + ].join(" ") }), - - decreasing: Lib.extendDeep({}, directionAttrs, { - line: { color: { dflt: DECREASING_COLOR } } - }), - - text: { - valType: 'string', - role: 'info', - dflt: '', - arrayOk: true, - description: [ - 'Sets hover text elements associated with each sample point.', - 'If a single string, the same string appears over', - 'all the data points.', - 'If an array of string, the items are mapped in order to', - 'this trace\'s sample points.' - ].join(' ') - }, - - tickwidth: { - valType: 'number', - min: 0, - max: 0.5, - dflt: 0.3, - role: 'style', - description: [ - 'Sets the width of the open/close tick marks', - 'relative to the *x* minimal interval.' - ].join(' ') - } + dash: Lib.extendFlat({}, lineAttrs.dash, { + description: [ + lineAttrs.dash, + "Note that this style setting can also be set per", + "direction via `increasing.line.dash` and", + "`decreasing.line.dash`." + ].join(" ") + }) + }, + increasing: Lib.extendDeep({}, directionAttrs, { + line: { color: { dflt: INCREASING_COLOR } } + }), + decreasing: Lib.extendDeep({}, directionAttrs, { + line: { color: { dflt: DECREASING_COLOR } } + }), + text: { + valType: "string", + role: "info", + dflt: "", + arrayOk: true, + description: [ + "Sets hover text elements associated with each sample point.", + "If a single string, the same string appears over", + "all the data points.", + "If an array of string, the items are mapped in order to", + "this trace's sample points." + ].join(" ") + }, + tickwidth: { + valType: "number", + min: 0, + max: 0.5, + dflt: 0.3, + role: "style", + description: [ + "Sets the width of the open/close tick marks", + "relative to the *x* minimal interval." + ].join(" ") + } }; diff --git a/src/traces/ohlc/defaults.js b/src/traces/ohlc/defaults.js index da557610a49..d57810e90af 100644 --- a/src/traces/ohlc/defaults.js +++ b/src/traces/ohlc/defaults.js @@ -5,43 +5,45 @@ * 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 handleOHLC = require('./ohlc_defaults'); -var handleDirectionDefaults = require('./direction_defaults'); -var attributes = require('./attributes'); -var helpers = require('./helpers'); - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - helpers.pushDummyTransformOpts(traceIn, traceOut); - - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var len = handleOHLC(traceIn, traceOut, coerce, layout); - if(len === 0) { - traceOut.visible = false; - return; - } - - coerce('line.width'); - coerce('line.dash'); - - handleDirection(traceIn, traceOut, coerce, 'increasing'); - handleDirection(traceIn, traceOut, coerce, 'decreasing'); - - coerce('text'); - coerce('tickwidth'); +"use strict"; +var Lib = require("../../lib"); +var handleOHLC = require("./ohlc_defaults"); +var handleDirectionDefaults = require("./direction_defaults"); +var attributes = require("./attributes"); +var helpers = require("./helpers"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + helpers.pushDummyTransformOpts(traceIn, traceOut); + + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var len = handleOHLC(traceIn, traceOut, coerce, layout); + if (len === 0) { + traceOut.visible = false; + return; + } + + coerce("line.width"); + coerce("line.dash"); + + handleDirection(traceIn, traceOut, coerce, "increasing"); + handleDirection(traceIn, traceOut, coerce, "decreasing"); + + coerce("text"); + coerce("tickwidth"); }; function handleDirection(traceIn, traceOut, coerce, direction) { - handleDirectionDefaults(traceIn, traceOut, coerce, direction); + handleDirectionDefaults(traceIn, traceOut, coerce, direction); - coerce(direction + '.line.color'); - coerce(direction + '.line.width', traceOut.line.width); - coerce(direction + '.line.dash', traceOut.line.dash); + coerce(direction + ".line.color"); + coerce(direction + ".line.width", traceOut.line.width); + coerce(direction + ".line.dash", traceOut.line.dash); } diff --git a/src/traces/ohlc/direction_defaults.js b/src/traces/ohlc/direction_defaults.js index 801b4444319..595673c03a6 100644 --- a/src/traces/ohlc/direction_defaults.js +++ b/src/traces/ohlc/direction_defaults.js @@ -5,20 +5,21 @@ * 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 = function handleDirectionDefaults( + traceIn, + traceOut, + coerce, + direction +) { + coerce(direction + ".showlegend"); + // trace-wide *showlegend* overrides direction *showlegend* + if (traceIn.showlegend === false) { + traceOut[direction].showlegend = false; + } -'use strict'; + var nameDflt = traceOut.name + " - " + direction; - -module.exports = function handleDirectionDefaults(traceIn, traceOut, coerce, direction) { - coerce(direction + '.showlegend'); - - // trace-wide *showlegend* overrides direction *showlegend* - if(traceIn.showlegend === false) { - traceOut[direction].showlegend = false; - } - - var nameDflt = traceOut.name + ' - ' + direction; - - coerce(direction + '.name', nameDflt); + coerce(direction + ".name", nameDflt); }; diff --git a/src/traces/ohlc/helpers.js b/src/traces/ohlc/helpers.js index e7fca7d0d60..c314e8bf835 100644 --- a/src/traces/ohlc/helpers.js +++ b/src/traces/ohlc/helpers.js @@ -5,11 +5,8 @@ * 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'); +"use strict"; +var Lib = require("../../lib"); // This routine gets called during the trace supply-defaults step. // @@ -22,36 +19,33 @@ var Lib = require('../../lib'); // from a clear transforms container. The mutations inflicted are // cleared in exports.clearEphemeralTransformOpts. exports.pushDummyTransformOpts = function(traceIn, traceOut) { - var transformOpts = { - - // give dummy transform the same type as trace - type: traceOut.type, - - // track ephemeral transforms in user data - _ephemeral: true - }; - - if(Array.isArray(traceIn.transforms)) { - traceIn.transforms.push(transformOpts); - } - else { - traceIn.transforms = [transformOpts]; - } + var transformOpts = { + // give dummy transform the same type as trace + type: traceOut.type, + // track ephemeral transforms in user data + _ephemeral: true + }; + + if (Array.isArray(traceIn.transforms)) { + traceIn.transforms.push(transformOpts); + } else { + traceIn.transforms = [transformOpts]; + } }; // This routine gets called during the transform supply-defaults step // where it clears ephemeral transform opts in user data // and effectively put back user date in its pre-supplyDefaults state. exports.clearEphemeralTransformOpts = function(traceIn) { - var transformsIn = traceIn.transforms; + var transformsIn = traceIn.transforms; - if(!Array.isArray(transformsIn)) return; + if (!Array.isArray(transformsIn)) return; - for(var i = 0; i < transformsIn.length; i++) { - if(transformsIn[i]._ephemeral) transformsIn.splice(i, 1); - } + for (var i = 0; i < transformsIn.length; i++) { + if (transformsIn[i]._ephemeral) transformsIn.splice(i, 1); + } - if(transformsIn.length === 0) delete traceIn.transforms; + if (transformsIn.length === 0) delete traceIn.transforms; }; // This routine gets called during the transform supply-defaults step @@ -63,10 +57,10 @@ exports.clearEphemeralTransformOpts = function(traceIn) { // Note that this routine only has an effect during the // second round of transform defaults done on generated traces exports.copyOHLC = function(container, traceOut) { - if(container.open) traceOut.open = container.open; - if(container.high) traceOut.high = container.high; - if(container.low) traceOut.low = container.low; - if(container.close) traceOut.close = container.close; + if (container.open) traceOut.open = container.open; + if (container.high) traceOut.high = container.high; + if (container.low) traceOut.low = container.low; + if (container.close) traceOut.close = container.close; }; // This routine gets called during the applyTransform step. @@ -78,44 +72,47 @@ exports.copyOHLC = function(container, traceOut) { // To make sure that the attributes reach the calcTransform, // store it in the transform opts object. exports.makeTransform = function(traceIn, state, direction) { - var out = Lib.extendFlat([], traceIn.transforms); - - out[state.transformIndex] = { - type: traceIn.type, - direction: direction, - - // these are copied to traceOut during exports.copyOHLC - open: traceIn.open, - high: traceIn.high, - low: traceIn.low, - close: traceIn.close - }; - - return out; + var out = Lib.extendFlat([], traceIn.transforms); + + out[state.transformIndex] = { + type: traceIn.type, + direction: direction, + // these are copied to traceOut during exports.copyOHLC + open: traceIn.open, + high: traceIn.high, + low: traceIn.low, + close: traceIn.close + }; + + return out; }; exports.getFilterFn = function(direction) { - switch(direction) { - case 'increasing': - return function(o, c) { return o <= c; }; - - case 'decreasing': - return function(o, c) { return o > c; }; - } + switch (direction) { + case "increasing": + return function(o, c) { + return o <= c; + }; + + case "decreasing": + return function(o, c) { + return o > c; + }; + } }; exports.addRangeSlider = function(data, layout) { - var hasOneVisibleTrace = false; + var hasOneVisibleTrace = false; - for(var i = 0; i < data.length; i++) { - if(data[i].visible === true) { - hasOneVisibleTrace = true; - break; - } + for (var i = 0; i < data.length; i++) { + if (data[i].visible === true) { + hasOneVisibleTrace = true; + break; } + } - if(hasOneVisibleTrace) { - if(!layout.xaxis) layout.xaxis = {}; - if(!layout.xaxis.rangeslider) layout.xaxis.rangeslider = {}; - } + if (hasOneVisibleTrace) { + if (!layout.xaxis) layout.xaxis = {}; + if (!layout.xaxis.rangeslider) layout.xaxis.rangeslider = {}; + } }; diff --git a/src/traces/ohlc/index.js b/src/traces/ohlc/index.js index 8f03e2d2a44..5ae39d8765a 100644 --- a/src/traces/ohlc/index.js +++ b/src/traces/ohlc/index.js @@ -5,36 +5,29 @@ * 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 register = require('../../plot_api/register'); +"use strict"; +var register = require("../../plot_api/register"); module.exports = { - moduleType: 'trace', - name: 'ohlc', - basePlotModule: require('../../plots/cartesian'), - categories: ['cartesian', 'showLegend'], - meta: { - description: [ - 'The ohlc (short for Open-High-Low-Close) is a style of financial chart describing', - 'open, high, low and close for a given `x` coordinate (most likely time).', - - 'The tip of the lines represent the `low` and `high` values and', - 'the horizontal segments represent the `open` and `close` values.', - - 'Sample points where the close value is higher (lower) then the open', - 'value are called increasing (decreasing).', - - 'By default, increasing candles are drawn in green whereas', - 'decreasing are drawn in red.' - ].join(' ') - }, - - attributes: require('./attributes'), - supplyDefaults: require('./defaults'), + moduleType: "trace", + name: "ohlc", + basePlotModule: require("../../plots/cartesian"), + categories: ["cartesian", "showLegend"], + meta: { + description: [ + "The ohlc (short for Open-High-Low-Close) is a style of financial chart describing", + "open, high, low and close for a given `x` coordinate (most likely time).", + "The tip of the lines represent the `low` and `high` values and", + "the horizontal segments represent the `open` and `close` values.", + "Sample points where the close value is higher (lower) then the open", + "value are called increasing (decreasing).", + "By default, increasing candles are drawn in green whereas", + "decreasing are drawn in red." + ].join(" ") + }, + attributes: require("./attributes"), + supplyDefaults: require("./defaults") }; -register(require('../scatter')); -register(require('./transform')); +register(require("../scatter")); +register(require("./transform")); diff --git a/src/traces/ohlc/ohlc_defaults.js b/src/traces/ohlc/ohlc_defaults.js index 392dadd0d76..80ec4a6e277 100644 --- a/src/traces/ohlc/ohlc_defaults.js +++ b/src/traces/ohlc/ohlc_defaults.js @@ -5,36 +5,35 @@ * 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 Registry = require('../../registry'); - +"use strict"; +var Registry = require("../../registry"); module.exports = function handleOHLC(traceIn, traceOut, coerce, layout) { - var len; + var len; - var x = coerce('x'), - open = coerce('open'), - high = coerce('high'), - low = coerce('low'), - close = coerce('close'); + var x = coerce("x"), + open = coerce("open"), + high = coerce("high"), + low = coerce("low"), + close = coerce("close"); - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); - handleCalendarDefaults(traceIn, traceOut, ['x'], layout); + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleTraceDefaults" + ); + handleCalendarDefaults(traceIn, traceOut, ["x"], layout); - len = Math.min(open.length, high.length, low.length, close.length); + len = Math.min(open.length, high.length, low.length, close.length); - if(x) { - len = Math.min(len, x.length); - if(len < x.length) traceOut.x = x.slice(0, len); - } + if (x) { + len = Math.min(len, x.length); + if (len < x.length) traceOut.x = x.slice(0, len); + } - if(len < open.length) traceOut.open = open.slice(0, len); - if(len < high.length) traceOut.high = high.slice(0, len); - if(len < low.length) traceOut.low = low.slice(0, len); - if(len < close.length) traceOut.close = close.slice(0, len); + if (len < open.length) traceOut.open = open.slice(0, len); + if (len < high.length) traceOut.high = high.slice(0, len); + if (len < low.length) traceOut.low = low.slice(0, len); + if (len < close.length) traceOut.close = close.slice(0, len); - return len; + return len; }; diff --git a/src/traces/ohlc/transform.js b/src/traces/ohlc/transform.js index 236536056ac..a0a426a3ab7 100644 --- a/src/traces/ohlc/transform.js +++ b/src/traces/ohlc/transform.js @@ -5,88 +5,80 @@ * 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 helpers = require("./helpers"); +var Axes = require("../../plots/cartesian/axes"); +var axisIds = require("../../plots/cartesian/axis_ids"); +exports.moduleType = "transform"; -'use strict'; - -var Lib = require('../../lib'); -var helpers = require('./helpers'); -var Axes = require('../../plots/cartesian/axes'); -var axisIds = require('../../plots/cartesian/axis_ids'); - -exports.moduleType = 'transform'; - -exports.name = 'ohlc'; +exports.name = "ohlc"; exports.attributes = {}; exports.supplyDefaults = function(transformIn, traceOut, layout, traceIn) { - helpers.clearEphemeralTransformOpts(traceIn); - helpers.copyOHLC(transformIn, traceOut); + helpers.clearEphemeralTransformOpts(traceIn); + helpers.copyOHLC(transformIn, traceOut); - return transformIn; + return transformIn; }; exports.transform = function transform(dataIn, state) { - var dataOut = []; + var dataOut = []; - for(var i = 0; i < dataIn.length; i++) { - var traceIn = dataIn[i]; - - if(traceIn.type !== 'ohlc') { - dataOut.push(traceIn); - continue; - } + for (var i = 0; i < dataIn.length; i++) { + var traceIn = dataIn[i]; - dataOut.push( - makeTrace(traceIn, state, 'increasing'), - makeTrace(traceIn, state, 'decreasing') - ); + if (traceIn.type !== "ohlc") { + dataOut.push(traceIn); + continue; } - helpers.addRangeSlider(dataOut, state.layout); + dataOut.push( + makeTrace(traceIn, state, "increasing"), + makeTrace(traceIn, state, "decreasing") + ); + } - return dataOut; + helpers.addRangeSlider(dataOut, state.layout); + + return dataOut; }; function makeTrace(traceIn, state, direction) { - var traceOut = { - type: 'scatter', - mode: 'lines', - connectgaps: false, - - visible: traceIn.visible, - opacity: traceIn.opacity, - xaxis: traceIn.xaxis, - yaxis: traceIn.yaxis, - - hoverinfo: makeHoverInfo(traceIn), - transforms: helpers.makeTransform(traceIn, state, direction) - }; - - // the rest of below may not have been coerced - - var directionOpts = traceIn[direction]; - - if(directionOpts) { - Lib.extendFlat(traceOut, { - - // to make autotype catch date axes soon!! - x: traceIn.x || [0], - xcalendar: traceIn.xcalendar, - - // concat low and high to get correct autorange - y: [].concat(traceIn.low).concat(traceIn.high), - - text: traceIn.text, - - name: directionOpts.name, - showlegend: directionOpts.showlegend, - line: directionOpts.line - }); - } - - return traceOut; + var traceOut = { + type: "scatter", + mode: "lines", + connectgaps: false, + visible: traceIn.visible, + opacity: traceIn.opacity, + xaxis: traceIn.xaxis, + yaxis: traceIn.yaxis, + hoverinfo: makeHoverInfo(traceIn), + transforms: helpers.makeTransform(traceIn, state, direction) + }; + + // the rest of below may not have been coerced + var directionOpts = traceIn[direction]; + + if (directionOpts) { + Lib.extendFlat(traceOut, { + // to make autotype catch date axes soon!! + x: ( + traceIn.x || [0] + ), + xcalendar: traceIn.xcalendar, + // concat low and high to get correct autorange + y: [].concat(traceIn.low).concat(traceIn.high), + text: traceIn.text, + name: directionOpts.name, + showlegend: directionOpts.showlegend, + line: directionOpts.line + }); + } + + return traceOut; } // Let scatter hoverPoint format 'x' coordinates, if desired. @@ -99,159 +91,156 @@ function makeTrace(traceIn, state, direction) { // A future iteration should perhaps try to add a hook for transforms in // the hoverPoints handlers. function makeHoverInfo(traceIn) { - var hoverinfo = traceIn.hoverinfo; + var hoverinfo = traceIn.hoverinfo; - if(hoverinfo === 'all') return 'x+text+name'; + if (hoverinfo === "all") return "x+text+name"; - var parts = hoverinfo.split('+'), - indexOfY = parts.indexOf('y'), - indexOfText = parts.indexOf('text'); + var parts = hoverinfo.split("+"), + indexOfY = parts.indexOf("y"), + indexOfText = parts.indexOf("text"); - if(indexOfY !== -1) { - parts.splice(indexOfY, 1); + if (indexOfY !== -1) { + parts.splice(indexOfY, 1); - if(indexOfText === -1) parts.push('text'); - } + if (indexOfText === -1) parts.push("text"); + } - return parts.join('+'); + return parts.join("+"); } exports.calcTransform = function calcTransform(gd, trace, opts) { - var direction = opts.direction, - filterFn = helpers.getFilterFn(direction); - - var xa = axisIds.getFromTrace(gd, trace, 'x'), - ya = axisIds.getFromTrace(gd, trace, 'y'), - tickWidth = convertTickWidth(gd, xa, trace); - - var open = trace.open, - high = trace.high, - low = trace.low, - close = trace.close, - textIn = trace.text; - - var len = open.length, - x = [], - y = [], - textOut = []; - - var appendX; - if(trace._fullInput.x) { - appendX = function(i) { - var xi = trace.x[i], - xcalendar = trace.xcalendar, - xcalc = xa.d2c(xi, 0, xcalendar); - - x.push( - xa.c2d(xcalc - tickWidth, 0, xcalendar), - xi, xi, xi, xi, - xa.c2d(xcalc + tickWidth, 0, xcalendar), - null); - }; - } - else { - appendX = function(i) { - x.push( - i - tickWidth, - i, i, i, i, - i + tickWidth, - null); - }; - } - - var appendY = function(o, h, l, c) { - y.push(o, o, h, l, c, c, null); + var direction = opts.direction, filterFn = helpers.getFilterFn(direction); + + var xa = axisIds.getFromTrace(gd, trace, "x"), + ya = axisIds.getFromTrace(gd, trace, "y"), + tickWidth = convertTickWidth(gd, xa, trace); + + var open = trace.open, + high = trace.high, + low = trace.low, + close = trace.close, + textIn = trace.text; + + var len = open.length, x = [], y = [], textOut = []; + + var appendX; + if (trace._fullInput.x) { + appendX = function(i) { + var xi = trace.x[i], + xcalendar = trace.xcalendar, + xcalc = xa.d2c(xi, 0, xcalendar); + + x.push( + xa.c2d(xcalc - tickWidth, 0, xcalendar), + xi, + xi, + xi, + xi, + xa.c2d(xcalc + tickWidth, 0, xcalendar), + null + ); }; - - var format = function(ax, val) { - return Axes.tickText(ax, ax.c2l(val), 'hover').text; + } else { + appendX = function(i) { + x.push(i - tickWidth, i, i, i, i, i + tickWidth, null); }; + } + + var appendY = function(o, h, l, c) { + y.push(o, o, h, l, c, c, null); + }; + + var format = function(ax, val) { + return Axes.tickText(ax, ax.c2l(val), "hover").text; + }; + + var hoverinfo = trace._fullInput.hoverinfo, + hoverParts = hoverinfo.split("+"), + hasAll = hoverinfo === "all", + hasY = hasAll || hoverParts.indexOf("y") !== -1, + hasText = hasAll || hoverParts.indexOf("text") !== -1; + + var getTextItem = Array.isArray(textIn) + ? (function(i) { + return textIn[i] || ""; + }) + : (function() { + return textIn; + }); + + var appendText = function(i, o, h, l, c) { + var t = []; + + if (hasY) { + t.push("Open: " + format(ya, o)); + t.push("High: " + format(ya, h)); + t.push("Low: " + format(ya, l)); + t.push("Close: " + format(ya, c)); + } - var hoverinfo = trace._fullInput.hoverinfo, - hoverParts = hoverinfo.split('+'), - hasAll = hoverinfo === 'all', - hasY = hasAll || hoverParts.indexOf('y') !== -1, - hasText = hasAll || hoverParts.indexOf('text') !== -1; - - var getTextItem = Array.isArray(textIn) ? - function(i) { return textIn[i] || ''; } : - function() { return textIn; }; - - var appendText = function(i, o, h, l, c) { - var t = []; - - if(hasY) { - t.push('Open: ' + format(ya, o)); - t.push('High: ' + format(ya, h)); - t.push('Low: ' + format(ya, l)); - t.push('Close: ' + format(ya, c)); - } - - if(hasText) t.push(getTextItem(i)); + if (hasText) t.push(getTextItem(i)); - var _t = t.join('
'); + var _t = t.join("
"); - textOut.push(_t, _t, _t, _t, _t, _t, null); - }; + textOut.push(_t, _t, _t, _t, _t, _t, null); + }; - for(var i = 0; i < len; i++) { - if(filterFn(open[i], close[i])) { - appendX(i); - appendY(open[i], high[i], low[i], close[i]); - appendText(i, open[i], high[i], low[i], close[i]); - } + for (var i = 0; i < len; i++) { + if (filterFn(open[i], close[i])) { + appendX(i); + appendY(open[i], high[i], low[i], close[i]); + appendText(i, open[i], high[i], low[i], close[i]); } + } - trace.x = x; - trace.y = y; - trace.text = textOut; + trace.x = x; + trace.y = y; + trace.text = textOut; }; function convertTickWidth(gd, xa, trace) { - var fullInput = trace._fullInput, - tickWidth = fullInput.tickwidth, - minDiff = fullInput._minDiff; - - if(!minDiff) { - var fullData = gd._fullData, - ohlcTracesOnThisXaxis = []; - - minDiff = Infinity; - - // find min x-coordinates difference of all traces - // attached to this x-axis and stash the result - - var i; - - for(i = 0; i < fullData.length; i++) { - var _trace = fullData[i]._fullInput; - - if(_trace.type === 'ohlc' && - _trace.visible === true && - _trace.xaxis === xa._id - ) { - ohlcTracesOnThisXaxis.push(_trace); - - // - _trace.x may be undefined here, - // it is filled later in calcTransform - // - // - handle trace of length 1 separately. - - if(_trace.x && _trace.x.length > 1) { - var xcalc = Lib.simpleMap(_trace.x, xa.d2c, 0, trace.xcalendar), - _minDiff = Lib.distinctVals(xcalc).minDiff; - minDiff = Math.min(minDiff, _minDiff); - } - } + var fullInput = trace._fullInput, + tickWidth = fullInput.tickwidth, + minDiff = fullInput._minDiff; + + if (!minDiff) { + var fullData = gd._fullData, ohlcTracesOnThisXaxis = []; + + minDiff = Infinity; + + // find min x-coordinates difference of all traces + // attached to this x-axis and stash the result + var i; + + for (i = 0; i < fullData.length; i++) { + var _trace = fullData[i]._fullInput; + + if ( + _trace.type === "ohlc" && + _trace.visible === true && + _trace.xaxis === xa._id + ) { + ohlcTracesOnThisXaxis.push(_trace); + + // - _trace.x may be undefined here, + // it is filled later in calcTransform + // + // - handle trace of length 1 separately. + if (_trace.x && _trace.x.length > 1) { + var xcalc = Lib.simpleMap(_trace.x, xa.d2c, 0, trace.xcalendar), + _minDiff = Lib.distinctVals(xcalc).minDiff; + minDiff = Math.min(minDiff, _minDiff); } + } + } - // if minDiff is still Infinity here, set it to 1 - if(minDiff === Infinity) minDiff = 1; + // if minDiff is still Infinity here, set it to 1 + if (minDiff === Infinity) minDiff = 1; - for(i = 0; i < ohlcTracesOnThisXaxis.length; i++) { - ohlcTracesOnThisXaxis[i]._minDiff = minDiff; - } + for (i = 0; i < ohlcTracesOnThisXaxis.length; i++) { + ohlcTracesOnThisXaxis[i]._minDiff = minDiff; } + } - return minDiff * tickWidth; + return minDiff * tickWidth; } diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js index a5eb8aefc6e..e7a4e0e6917 100644 --- a/src/traces/pie/attributes.js +++ b/src/traces/pie/attributes.js @@ -5,224 +5,208 @@ * 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 colorAttrs = require("../../components/color/attributes"); +var fontAttrs = require("../../plots/font_attributes"); +var plotAttrs = require("../../plots/attributes"); -'use strict'; - -var colorAttrs = require('../../components/color/attributes'); -var fontAttrs = require('../../plots/font_attributes'); -var plotAttrs = require('../../plots/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; - +var extendFlat = require("../../lib/extend").extendFlat; module.exports = { - labels: { - valType: 'data_array', - description: 'Sets the sector labels.' - }, - // equivalent of x0 and dx, if label is missing - label0: { - valType: 'number', - role: 'info', - dflt: 0, - description: [ - 'Alternate to `labels`.', - 'Builds a numeric set of labels.', - 'Use with `dlabel`', - 'where `label0` is the starting label and `dlabel` the step.' - ].join(' ') + labels: { valType: "data_array", description: "Sets the sector labels." }, + // equivalent of x0 and dx, if label is missing + label0: { + valType: "number", + role: "info", + dflt: 0, + description: [ + "Alternate to `labels`.", + "Builds a numeric set of labels.", + "Use with `dlabel`", + "where `label0` is the starting label and `dlabel` the step." + ].join(" ") + }, + dlabel: { + valType: "number", + role: "info", + dflt: 1, + description: "Sets the label step. See `label0` for more info." + }, + values: { + valType: "data_array", + description: "Sets the values of the sectors of this pie chart." + }, + marker: { + colors: { + valType: "data_array", + // TODO 'color_array' ? + description: [ + "Sets the color of each sector of this pie chart.", + "If not specified, the default trace color set is used", + "to pick the sector colors." + ].join(" ") }, - dlabel: { - valType: 'number', - role: 'info', - dflt: 1, - description: 'Sets the label step. See `label0` for more info.' - }, - - values: { - valType: 'data_array', - description: 'Sets the values of the sectors of this pie chart.' - }, - - marker: { - colors: { - valType: 'data_array', // TODO 'color_array' ? - description: [ - 'Sets the color of each sector of this pie chart.', - 'If not specified, the default trace color set is used', - 'to pick the sector colors.' - ].join(' ') - }, - - line: { - color: { - valType: 'color', - role: 'style', - dflt: colorAttrs.defaultLine, - arrayOk: true, - description: [ - 'Sets the color of the line enclosing each sector.' - ].join(' ') - }, - width: { - valType: 'number', - role: 'style', - min: 0, - dflt: 0, - arrayOk: true, - description: [ - 'Sets the width (in px) of the line enclosing each sector.' - ].join(' ') - } - } - }, - - text: { - valType: 'data_array', - description: 'Sets text elements associated with each sector.' - }, - -// 'see eg:' -// 'https://www.e-education.psu.edu/natureofgeoinfo/sites/www.e-education.psu.edu.natureofgeoinfo/files/image/hisp_pies.gif', -// '(this example involves a map too - may someday be a whole trace type', -// 'of its own. but the point is the size of the whole pie is important.)' - scalegroup: { - valType: 'string', - role: 'info', - dflt: '', - description: [ - 'If there are multiple pies that should be sized according to', - 'their totals, link them by providing a non-empty group id here', - 'shared by every trace in the same group.' - ].join(' ') - }, - - // labels (legend is handled by plots.attributes.showlegend and layout.hiddenlabels) - textinfo: { - valType: 'flaglist', - role: 'info', - flags: ['label', 'text', 'value', 'percent'], - extras: ['none'], - description: [ - 'Determines which trace information appear on the graph.' - ].join(' ') - }, - hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { - flags: ['label', 'text', 'value', 'percent', 'name'] - }), - textposition: { - valType: 'enumerated', - role: 'info', - values: ['inside', 'outside', 'auto', 'none'], - dflt: 'auto', + line: { + color: { + valType: "color", + role: "style", + dflt: colorAttrs.defaultLine, arrayOk: true, - description: [ - 'Specifies the location of the `textinfo`.' - ].join(' ') - }, - // TODO make those arrayOk? - textfont: extendFlat({}, fontAttrs, { - description: 'Sets the font used for `textinfo`.' - }), - insidetextfont: extendFlat({}, fontAttrs, { - description: 'Sets the font used for `textinfo` lying inside the pie.' - }), - outsidetextfont: extendFlat({}, fontAttrs, { - description: 'Sets the font used for `textinfo` lying outside the pie.' - }), - - // position and shape - domain: { - x: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the horizontal domain of this pie trace', - '(in plot fraction).' - ].join(' ') - }, - y: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the vertical domain of this pie trace', - '(in plot fraction).' - ].join(' ') - } - }, - hole: { - valType: 'number', - role: 'style', + description: ["Sets the color of the line enclosing each sector."].join( + " " + ) + }, + width: { + valType: "number", + role: "style", min: 0, - max: 1, dflt: 0, + arrayOk: true, description: [ - 'Sets the fraction of the radius to cut out of the pie.', - 'Use this to make a donut chart.' - ].join(' ') - }, - - // ordering and direction - sort: { - valType: 'boolean', - role: 'style', - dflt: true, - description: [ - 'Determines whether or not the sectors of reordered', - 'from largest to smallest.' - ].join(' ') + "Sets the width (in px) of the line enclosing each sector." + ].join(" ") + } + } + }, + text: { + valType: "data_array", + description: "Sets text elements associated with each sector." + }, + // 'see eg:' + // 'https://www.e-education.psu.edu/natureofgeoinfo/sites/www.e-education.psu.edu.natureofgeoinfo/files/image/hisp_pies.gif', + // '(this example involves a map too - may someday be a whole trace type', + // 'of its own. but the point is the size of the whole pie is important.)' + scalegroup: { + valType: "string", + role: "info", + dflt: "", + description: [ + "If there are multiple pies that should be sized according to", + "their totals, link them by providing a non-empty group id here", + "shared by every trace in the same group." + ].join(" ") + }, + // labels (legend is handled by plots.attributes.showlegend and layout.hiddenlabels) + textinfo: { + valType: "flaglist", + role: "info", + flags: ["label", "text", "value", "percent"], + extras: ["none"], + description: [ + "Determines which trace information appear on the graph." + ].join(" ") + }, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { + flags: ["label", "text", "value", "percent", "name"] + }), + textposition: { + valType: "enumerated", + role: "info", + values: ["inside", "outside", "auto", "none"], + dflt: "auto", + arrayOk: true, + description: ["Specifies the location of the `textinfo`."].join(" ") + }, + // TODO make those arrayOk? + textfont: extendFlat({}, fontAttrs, { + description: "Sets the font used for `textinfo`." + }), + insidetextfont: extendFlat({}, fontAttrs, { + description: "Sets the font used for `textinfo` lying inside the pie." + }), + outsidetextfont: extendFlat({}, fontAttrs, { + description: "Sets the font used for `textinfo` lying outside the pie." + }), + // position and shape + domain: { + x: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: [ + "Sets the horizontal domain of this pie trace", + "(in plot fraction)." + ].join(" ") }, - direction: { - /** + y: { + valType: "info_array", + role: "info", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1], + description: [ + "Sets the vertical domain of this pie trace", + "(in plot fraction)." + ].join(" ") + } + }, + hole: { + valType: "number", + role: "style", + min: 0, + max: 1, + dflt: 0, + description: [ + "Sets the fraction of the radius to cut out of the pie.", + "Use this to make a donut chart." + ].join(" ") + }, + // ordering and direction + sort: { + valType: "boolean", + role: "style", + dflt: true, + description: [ + "Determines whether or not the sectors of reordered", + "from largest to smallest." + ].join(" ") + }, + direction: { + /** * there are two common conventions, both of which place the first * (largest, if sorted) slice with its left edge at 12 o'clock but * succeeding slices follow either cw or ccw from there. * * see http://visage.co/data-visualization-101-pie-charts/ */ - valType: 'enumerated', - values: ['clockwise', 'counterclockwise'], - role: 'style', - dflt: 'counterclockwise', - description: [ - 'Specifies the direction at which succeeding sectors follow', - 'one another.' - ].join(' ') - }, - rotation: { - valType: 'number', - role: 'style', - min: -360, - max: 360, - dflt: 0, - description: [ - 'Instead of the first slice starting at 12 o\'clock,', - 'rotate to some other angle.' - ].join(' ') - }, - - pull: { - valType: 'number', - role: 'style', - min: 0, - max: 1, - dflt: 0, - arrayOk: true, - description: [ - 'Sets the fraction of larger radius to pull the sectors', - 'out from the center. This can be a constant', - 'to pull all slices apart from each other equally', - 'or an array to highlight one or more slices.' - ].join(' ') - } + valType: "enumerated", + values: ["clockwise", "counterclockwise"], + role: "style", + dflt: "counterclockwise", + description: [ + "Specifies the direction at which succeeding sectors follow", + "one another." + ].join(" ") + }, + rotation: { + valType: "number", + role: "style", + min: -360, + max: 360, + dflt: 0, + description: [ + "Instead of the first slice starting at 12 o'clock,", + "rotate to some other angle." + ].join(" ") + }, + pull: { + valType: "number", + role: "style", + min: 0, + max: 1, + dflt: 0, + arrayOk: true, + description: [ + "Sets the fraction of larger radius to pull the sectors", + "out from the center. This can be a constant", + "to pull all slices apart from each other equally", + "or an array to highlight one or more slices." + ].join(" ") + } }; diff --git a/src/traces/pie/base_plot.js b/src/traces/pie/base_plot.js index e907f84f858..cf1d6d991b9 100644 --- a/src/traces/pie/base_plot.js +++ b/src/traces/pie/base_plot.js @@ -5,41 +5,43 @@ * 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 Registry = require("../../registry"); -'use strict'; - -var Registry = require('../../registry'); - - -exports.name = 'pie'; +exports.name = "pie"; exports.plot = function(gd) { - var Pie = Registry.getModule('pie'); - var cdPie = getCdModule(gd.calcdata, Pie); + var Pie = Registry.getModule("pie"); + var cdPie = getCdModule(gd.calcdata, Pie); - if(cdPie.length) Pie.plot(gd, cdPie); + if (cdPie.length) Pie.plot(gd, cdPie); }; -exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var hadPie = (oldFullLayout._has && oldFullLayout._has('pie')); - var hasPie = (newFullLayout._has && newFullLayout._has('pie')); - - if(hadPie && !hasPie) { - oldFullLayout._pielayer.selectAll('g.trace').remove(); - } +exports.clean = function( + newFullData, + newFullLayout, + oldFullData, + oldFullLayout +) { + var hadPie = oldFullLayout._has && oldFullLayout._has("pie"); + var hasPie = newFullLayout._has && newFullLayout._has("pie"); + + if (hadPie && !hasPie) { + oldFullLayout._pielayer.selectAll("g.trace").remove(); + } }; function getCdModule(calcdata, _module) { - var cdModule = []; + var cdModule = []; - for(var i = 0; i < calcdata.length; i++) { - var cd = calcdata[i]; - var trace = cd[0].trace; + 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); - } + if (trace._module === _module && trace.visible === true) { + cdModule.push(cd); } + } - return cdModule; + return cdModule; } diff --git a/src/traces/pie/calc.js b/src/traces/pie/calc.js index 7fd82028790..c7092d51321 100644 --- a/src/traces/pie/calc.js +++ b/src/traces/pie/calc.js @@ -5,122 +5,124 @@ * 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 isNumeric = require("fast-isnumeric"); +var tinycolor = require("tinycolor2"); -'use strict'; - -var isNumeric = require('fast-isnumeric'); -var tinycolor = require('tinycolor2'); - -var Color = require('../../components/color'); -var helpers = require('./helpers'); +var Color = require("../../components/color"); +var helpers = require("./helpers"); module.exports = function calc(gd, trace) { - var vals = trace.values, - labels = trace.labels, - cd = [], - fullLayout = gd._fullLayout, - colorMap = fullLayout._piecolormap, - allThisTraceLabels = {}, - needDefaults = false, - vTotal = 0, - hiddenLabels = fullLayout.hiddenlabels || [], - i, - v, - label, - color, - hidden, - pt; - - if(trace.dlabel) { - labels = new Array(vals.length); - for(i = 0; i < vals.length; i++) { - labels[i] = String(trace.label0 + i * trace.dlabel); - } + var vals = trace.values, + labels = trace.labels, + cd = [], + fullLayout = gd._fullLayout, + colorMap = fullLayout._piecolormap, + allThisTraceLabels = {}, + needDefaults = false, + vTotal = 0, + hiddenLabels = fullLayout.hiddenlabels || [], + i, + v, + label, + color, + hidden, + pt; + + if (trace.dlabel) { + labels = new Array(vals.length); + for (i = 0; i < vals.length; i++) { + labels[i] = String(trace.label0 + i * trace.dlabel); + } + } + + for (i = 0; i < vals.length; i++) { + v = vals[i]; + if (!isNumeric(v)) continue; + v = +v; + if (v < 0) continue; + + label = labels[i]; + if (label === undefined || label === "") label = i; + label = String(label); + // only take the first occurrence of any given label. + // TODO: perhaps (optionally?) sum values for a repeated label? + if (allThisTraceLabels[label] === undefined) { + allThisTraceLabels[label] = true; + } else { + continue; } - for(i = 0; i < vals.length; i++) { - v = vals[i]; - if(!isNumeric(v)) continue; - v = +v; - if(v < 0) continue; - - label = labels[i]; - if(label === undefined || label === '') label = i; - label = String(label); - // only take the first occurrence of any given label. - // TODO: perhaps (optionally?) sum values for a repeated label? - if(allThisTraceLabels[label] === undefined) allThisTraceLabels[label] = true; - else continue; - - color = tinycolor(trace.marker.colors[i]); - if(color.isValid()) { - color = Color.addOpacity(color, color.getAlpha()); - if(!colorMap[label]) { - colorMap[label] = color; - } - } - // have we seen this label and assigned a color to it in a previous trace? - else if(colorMap[label]) color = colorMap[label]; - // color needs a default - mark it false, come back after sorting - else { - color = false; - needDefaults = true; - } - - hidden = hiddenLabels.indexOf(label) !== -1; - - if(!hidden) vTotal += v; - - cd.push({ - v: v, - label: label, - color: color, - i: i, - hidden: hidden - }); + color = tinycolor(trace.marker.colors[i]); + if (color.isValid()) { + color = Color.addOpacity(color, color.getAlpha()); + if (!colorMap[label]) { + colorMap[label] = color; + } + } else if (colorMap[label]) { + // have we seen this label and assigned a color to it in a previous trace? + color = colorMap[label]; + } else { + // color needs a default - mark it false, come back after sorting + color = false; + needDefaults = true; } - if(trace.sort) cd.sort(function(a, b) { return b.v - a.v; }); + hidden = hiddenLabels.indexOf(label) !== -1; + + if (!hidden) vTotal += v; + + cd.push({ v: v, label: label, color: color, i: i, hidden: hidden }); + } - /** + if (trace.sort) { + cd.sort(function(a, b) { + return b.v - a.v; + }); + } + + /** * now go back and fill in colors we're still missing * this is done after sorting, so we pick defaults * in the order slices will be displayed */ - - if(needDefaults) { - for(i = 0; i < cd.length; i++) { - pt = cd[i]; - if(pt.color === false) { - colorMap[pt.label] = pt.color = nextDefaultColor(fullLayout._piedefaultcolorcount); - fullLayout._piedefaultcolorcount++; - } - } + if (needDefaults) { + for (i = 0; i < cd.length; i++) { + pt = cd[i]; + if (pt.color === false) { + colorMap[pt.label] = pt.color = nextDefaultColor( + fullLayout._piedefaultcolorcount + ); + fullLayout._piedefaultcolorcount++; + } } - - // include the sum of all values in the first point - if(cd[0]) cd[0].vTotal = vTotal; - - // now insert text - if(trace.textinfo && trace.textinfo !== 'none') { - var hasLabel = trace.textinfo.indexOf('label') !== -1, - hasText = trace.textinfo.indexOf('text') !== -1, - hasValue = trace.textinfo.indexOf('value') !== -1, - hasPercent = trace.textinfo.indexOf('percent') !== -1, - separators = fullLayout.separators, - thisText; - - for(i = 0; i < cd.length; i++) { - pt = cd[i]; - thisText = hasLabel ? [pt.label] : []; - if(hasText && trace.text[pt.i]) thisText.push(trace.text[pt.i]); - if(hasValue) thisText.push(helpers.formatPieValue(pt.v, separators)); - if(hasPercent) thisText.push(helpers.formatPiePercent(pt.v / vTotal, separators)); - pt.text = thisText.join('
'); - } + } + + // include the sum of all values in the first point + if (cd[0]) cd[0].vTotal = vTotal; + + // now insert text + if (trace.textinfo && trace.textinfo !== "none") { + var hasLabel = trace.textinfo.indexOf("label") !== -1, + hasText = trace.textinfo.indexOf("text") !== -1, + hasValue = trace.textinfo.indexOf("value") !== -1, + hasPercent = trace.textinfo.indexOf("percent") !== -1, + separators = fullLayout.separators, + thisText; + + for (i = 0; i < cd.length; i++) { + pt = cd[i]; + thisText = hasLabel ? [pt.label] : []; + if (hasText && trace.text[pt.i]) thisText.push(trace.text[pt.i]); + if (hasValue) thisText.push(helpers.formatPieValue(pt.v, separators)); + if (hasPercent) { + thisText.push(helpers.formatPiePercent(pt.v / vTotal, separators)); + } + pt.text = thisText.join("
"); } + } - return cd; + return cd; }; /** @@ -130,21 +132,25 @@ module.exports = function calc(gd, trace) { var pieDefaultColors; function nextDefaultColor(index) { - if(!pieDefaultColors) { - // generate this default set on demand (but then it gets saved in the module) - var mainDefaults = Color.defaults; - pieDefaultColors = mainDefaults.slice(); + if (!pieDefaultColors) { + // generate this default set on demand (but then it gets saved in the module) + var mainDefaults = Color.defaults; + pieDefaultColors = mainDefaults.slice(); - var i; + var i; - for(i = 0; i < mainDefaults.length; i++) { - pieDefaultColors.push(tinycolor(mainDefaults[i]).lighten(20).toHexString()); - } + for (i = 0; i < mainDefaults.length; i++) { + pieDefaultColors.push( + tinycolor(mainDefaults[i]).lighten(20).toHexString() + ); + } - for(i = 0; i < Color.defaults.length; i++) { - pieDefaultColors.push(tinycolor(mainDefaults[i]).darken(20).toHexString()); - } + for (i = 0; i < Color.defaults.length; i++) { + pieDefaultColors.push( + tinycolor(mainDefaults[i]).darken(20).toHexString() + ); } + } - return pieDefaultColors[index % pieDefaultColors.length]; + return pieDefaultColors[index % pieDefaultColors.length]; } diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index 9a21628aca9..83b16a6a061 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -5,78 +5,85 @@ * 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'); - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var coerceFont = Lib.coerceFont; - - var vals = coerce('values'); - if(!Array.isArray(vals) || !vals.length) { - traceOut.visible = false; - return; - } - - var labels = coerce('labels'); - if(!Array.isArray(labels)) { - coerce('label0'); - coerce('dlabel'); - } - - var lineWidth = coerce('marker.line.width'); - if(lineWidth) coerce('marker.line.color'); - - var colors = coerce('marker.colors'); - if(!Array.isArray(colors)) traceOut.marker.colors = []; // later this will get padded with default colors - - coerce('scalegroup'); - // TODO: tilt, depth, and hole all need to be coerced to the same values within a scaleegroup - // (ideally actually, depth would get set the same *after* scaling, ie the same absolute depth) - // and if colors aren't specified we should match these up - potentially even if separate pies - // are NOT in the same sharegroup - - - var textData = coerce('text'); - var textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent'); - - coerce('hoverinfo', (layout._dataLength === 1) ? 'label+text+value+percent' : undefined); - - if(textInfo && textInfo !== 'none') { - var textPosition = coerce('textposition'), - hasBoth = Array.isArray(textPosition) || textPosition === 'auto', - hasInside = hasBoth || textPosition === 'inside', - hasOutside = hasBoth || textPosition === 'outside'; - - if(hasInside || hasOutside) { - var dfltFont = coerceFont(coerce, 'textfont', layout.font); - if(hasInside) coerceFont(coerce, 'insidetextfont', dfltFont); - if(hasOutside) coerceFont(coerce, 'outsidetextfont', dfltFont); - } +"use strict"; +var Lib = require("../../lib"); +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var coerceFont = Lib.coerceFont; + + var vals = coerce("values"); + if (!Array.isArray(vals) || !vals.length) { + traceOut.visible = false; + return; + } + + var labels = coerce("labels"); + if (!Array.isArray(labels)) { + coerce("label0"); + coerce("dlabel"); + } + + var lineWidth = coerce("marker.line.width"); + if (lineWidth) coerce("marker.line.color"); + + var colors = coerce("marker.colors"); + if (!Array.isArray(colors)) traceOut.marker.colors = []; + + // later this will get padded with default colors + coerce("scalegroup"); + // TODO: tilt, depth, and hole all need to be coerced to the same values within a scaleegroup + // (ideally actually, depth would get set the same *after* scaling, ie the same absolute depth) + // and if colors aren't specified we should match these up - potentially even if separate pies + // are NOT in the same sharegroup + var textData = coerce("text"); + var textInfo = coerce( + "textinfo", + Array.isArray(textData) ? "text+percent" : "percent" + ); + + coerce( + "hoverinfo", + layout._dataLength === 1 ? "label+text+value+percent" : undefined + ); + + if (textInfo && textInfo !== "none") { + var textPosition = coerce("textposition"), + hasBoth = Array.isArray(textPosition) || textPosition === "auto", + hasInside = hasBoth || textPosition === "inside", + hasOutside = hasBoth || textPosition === "outside"; + + if (hasInside || hasOutside) { + var dfltFont = coerceFont(coerce, "textfont", layout.font); + if (hasInside) coerceFont(coerce, "insidetextfont", dfltFont); + if (hasOutside) coerceFont(coerce, "outsidetextfont", dfltFont); } + } - coerce('domain.x'); - coerce('domain.y'); - - // 3D attributes commented out until I finish them in a later PR - // var tilt = coerce('tilt'); - // if(tilt) { - // coerce('tiltaxis'); - // coerce('depth'); - // coerce('shading'); - // } + coerce("domain.x"); + coerce("domain.y"); - coerce('hole'); + // 3D attributes commented out until I finish them in a later PR + // var tilt = coerce('tilt'); + // if(tilt) { + // coerce('tiltaxis'); + // coerce('depth'); + // coerce('shading'); + // } + coerce("hole"); - coerce('sort'); - coerce('direction'); - coerce('rotation'); + coerce("sort"); + coerce("direction"); + coerce("rotation"); - coerce('pull'); + coerce("pull"); }; diff --git a/src/traces/pie/helpers.js b/src/traces/pie/helpers.js index ac19f6f6c1e..58543658700 100644 --- a/src/traces/pie/helpers.js +++ b/src/traces/pie/helpers.js @@ -5,23 +5,21 @@ * 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'); +"use strict"; +var Lib = require("../../lib"); exports.formatPiePercent = function formatPiePercent(v, separators) { - var vRounded = (v * 100).toPrecision(3); - if(vRounded.lastIndexOf('.') !== -1) { - vRounded = vRounded.replace(/[.]?0+$/, ''); - } - return Lib.numSeparate(vRounded, separators) + '%'; + var vRounded = (v * 100).toPrecision(3); + if (vRounded.lastIndexOf(".") !== -1) { + vRounded = vRounded.replace(/[.]?0+$/, ""); + } + return Lib.numSeparate(vRounded, separators) + "%"; }; exports.formatPieValue = function formatPieValue(v, separators) { - var vRounded = v.toPrecision(10); - if(vRounded.lastIndexOf('.') !== -1) { - vRounded = vRounded.replace(/[.]?0+$/, ''); - } - return Lib.numSeparate(vRounded, separators); + var vRounded = v.toPrecision(10); + if (vRounded.lastIndexOf(".") !== -1) { + vRounded = vRounded.replace(/[.]?0+$/, ""); + } + return Lib.numSeparate(vRounded, separators); }; diff --git a/src/traces/pie/index.js b/src/traces/pie/index.js index 87d85a0fbba..38b76101057 100644 --- a/src/traces/pie/index.js +++ b/src/traces/pie/index.js @@ -5,30 +5,28 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; var Pie = {}; -Pie.attributes = require('./attributes'); -Pie.supplyDefaults = require('./defaults'); -Pie.supplyLayoutDefaults = require('./layout_defaults'); -Pie.layoutAttributes = require('./layout_attributes'); -Pie.calc = require('./calc'); -Pie.plot = require('./plot'); -Pie.style = require('./style'); -Pie.styleOne = require('./style_one'); +Pie.attributes = require("./attributes"); +Pie.supplyDefaults = require("./defaults"); +Pie.supplyLayoutDefaults = require("./layout_defaults"); +Pie.layoutAttributes = require("./layout_attributes"); +Pie.calc = require("./calc"); +Pie.plot = require("./plot"); +Pie.style = require("./style"); +Pie.styleOne = require("./style_one"); -Pie.moduleType = 'trace'; -Pie.name = 'pie'; -Pie.basePlotModule = require('./base_plot'); -Pie.categories = ['pie', 'showLegend']; +Pie.moduleType = "trace"; +Pie.name = "pie"; +Pie.basePlotModule = require("./base_plot"); +Pie.categories = ["pie", "showLegend"]; Pie.meta = { - description: [ - 'A data visualized by the sectors of the pie is set in `values`.', - 'The sector labels are set in `labels`.', - 'The sector colors are set in `marker.colors`' - ].join(' ') + description: [ + "A data visualized by the sectors of the pie is set in `values`.", + "The sector labels are set in `labels`.", + "The sector colors are set in `marker.colors`" + ].join(" ") }; module.exports = Pie; diff --git a/src/traces/pie/layout_attributes.js b/src/traces/pie/layout_attributes.js index 29167d778c2..cfa26ab0039 100644 --- a/src/traces/pie/layout_attributes.js +++ b/src/traces/pie/layout_attributes.js @@ -5,14 +5,12 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = { - /** + /** * hiddenlabels is the pie chart analog of visible:'legendonly' * but it can contain many labels, and can hide slices * from several pies simultaneously */ - hiddenlabels: {valType: 'data_array'} + hiddenlabels: { valType: "data_array" } }; diff --git a/src/traces/pie/layout_defaults.js b/src/traces/pie/layout_defaults.js index 1f44573e03f..68141a6306d 100644 --- a/src/traces/pie/layout_defaults.js +++ b/src/traces/pie/layout_defaults.js @@ -5,16 +5,14 @@ * 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"); -'use strict'; - -var Lib = require('../../lib'); - -var layoutAttributes = require('./layout_attributes'); +var layoutAttributes = require("./layout_attributes"); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { - function coerce(attr, dflt) { - return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); - } - coerce('hiddenlabels'); + function coerce(attr, dflt) { + return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); + } + coerce("hiddenlabels"); }; diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 1d58dd2d9b4..12a75d7f92c 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -5,689 +5,817 @@ * 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 d3 = require("d3"); -'use strict'; +var Fx = require("../../plots/cartesian/graph_interact"); +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); +var svgTextUtils = require("../../lib/svg_text_utils"); -var d3 = require('d3'); - -var Fx = require('../../plots/cartesian/graph_interact'); -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); -var svgTextUtils = require('../../lib/svg_text_utils'); - -var helpers = require('./helpers'); +var helpers = require("./helpers"); module.exports = function plot(gd, cdpie) { - var fullLayout = gd._fullLayout; + var fullLayout = gd._fullLayout; + + scalePies(cdpie, fullLayout._size); + + var pieGroups = fullLayout._pielayer.selectAll("g.trace").data(cdpie); + + pieGroups.enter().append("g").attr({ + "stroke-linejoin": "round", + // TODO: miter might look better but can sometimes cause problems + // maybe miter with a small-ish stroke-miterlimit? + class: "trace" + }); + pieGroups.exit().remove(); + pieGroups.order(); + + pieGroups.each(function(cd) { + var pieGroup = d3.select(this), + cd0 = cd[0], + trace = cd0.trace, + tiltRads = 0, + // trace.tilt * Math.PI / 180, + depthLength = (trace.depth || 0) * cd0.r * Math.sin(tiltRads) / 2, + tiltAxis = trace.tiltaxis || 0, + tiltAxisRads = tiltAxis * Math.PI / 180, + depthVector = [ + depthLength * Math.sin(tiltAxisRads), + depthLength * Math.cos(tiltAxisRads) + ], + rSmall = cd0.r * Math.cos(tiltRads); + + var pieParts = pieGroup + .selectAll("g.part") + .data(trace.tilt ? ["top", "sides"] : ["top"]); + + pieParts.enter().append("g").attr("class", function(d) { + return d + " part"; + }); + pieParts.exit().remove(); + pieParts.order(); - scalePies(cdpie, fullLayout._size); + setCoords(cd); - var pieGroups = fullLayout._pielayer.selectAll('g.trace').data(cdpie); + pieGroup.selectAll(".top").each(function() { + var slices = d3.select(this).selectAll("g.slice").data(cd); - pieGroups.enter().append('g') - .attr({ - 'stroke-linejoin': 'round', // TODO: miter might look better but can sometimes cause problems - // maybe miter with a small-ish stroke-miterlimit? - 'class': 'trace' - }); - pieGroups.exit().remove(); - pieGroups.order(); - - pieGroups.each(function(cd) { - var pieGroup = d3.select(this), - cd0 = cd[0], - trace = cd0.trace, - tiltRads = 0, // trace.tilt * Math.PI / 180, - depthLength = (trace.depth||0) * cd0.r * Math.sin(tiltRads) / 2, - tiltAxis = trace.tiltaxis || 0, - tiltAxisRads = tiltAxis * Math.PI / 180, - depthVector = [ - depthLength * Math.sin(tiltAxisRads), - depthLength * Math.cos(tiltAxisRads) - ], - rSmall = cd0.r * Math.cos(tiltRads); - - var pieParts = pieGroup.selectAll('g.part') - .data(trace.tilt ? ['top', 'sides'] : ['top']); - - pieParts.enter().append('g').attr('class', function(d) { - return d + ' part'; - }); - pieParts.exit().remove(); - pieParts.order(); - - setCoords(cd); - - pieGroup.selectAll('.top').each(function() { - var slices = d3.select(this).selectAll('g.slice').data(cd); - - slices.enter().append('g') - .classed('slice', true); - slices.exit().remove(); - - var quadrants = [ - [[], []], // y<0: x<0, x>=0 - [[], []] // y>=0: x<0, x>=0 - ], - hasOutsideText = false; - - slices.each(function(pt) { - if(pt.hidden) { - d3.select(this).selectAll('path,g').remove(); - return; - } - - quadrants[pt.pxmid[1] < 0 ? 0 : 1][pt.pxmid[0] < 0 ? 0 : 1].push(pt); - - var cx = cd0.cx + depthVector[0], - cy = cd0.cy + depthVector[1], - sliceTop = d3.select(this), - slicePath = sliceTop.selectAll('path.surface').data([pt]), - hasHoverData = false; - - function handleMouseOver(evt) { - // in case fullLayout or fullData has changed without a replot - var fullLayout2 = gd._fullLayout, - trace2 = gd._fullData[trace.index], - hoverinfo = trace2.hoverinfo; - - if(hoverinfo === 'all') hoverinfo = 'label+text+value+percent+name'; - - // in case we dragged over the pie from another subplot, - // or if hover is turned off - if(gd._dragging || fullLayout2.hovermode === false || - hoverinfo === 'none' || hoverinfo === 'skip' || !hoverinfo) { - return; - } - - var rInscribed = getInscribedRadiusFraction(pt, cd0), - hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed), - hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed), - separators = fullLayout.separators, - thisText = []; - - if(hoverinfo.indexOf('label') !== -1) thisText.push(pt.label); - if(trace2.text && trace2.text[pt.i] && hoverinfo.indexOf('text') !== -1) { - thisText.push(trace2.text[pt.i]); - } - if(hoverinfo.indexOf('value') !== -1) thisText.push(helpers.formatPieValue(pt.v, separators)); - if(hoverinfo.indexOf('percent') !== -1) thisText.push(helpers.formatPiePercent(pt.v / cd0.vTotal, separators)); - - Fx.loneHover({ - x0: hoverCenterX - rInscribed * cd0.r, - x1: hoverCenterX + rInscribed * cd0.r, - y: hoverCenterY, - text: thisText.join('
'), - name: hoverinfo.indexOf('name') !== -1 ? trace2.name : undefined, - color: pt.color, - idealAlign: pt.pxmid[0] < 0 ? 'left' : 'right' - }, { - container: fullLayout2._hoverlayer.node(), - outerContainer: fullLayout2._paper.node() - }); - - Fx.hover(gd, evt, 'pie'); - - hasHoverData = true; - } - - function handleMouseOut(evt) { - gd.emit('plotly_unhover', { - points: [evt] - }); - - if(hasHoverData) { - Fx.loneUnhover(fullLayout._hoverlayer.node()); - hasHoverData = false; - } - } - - function handleClick() { - gd._hoverdata = [pt]; - gd._hoverdata.trace = cd.trace; - Fx.click(gd, { target: true }); - } - - slicePath.enter().append('path') - .classed('surface', true) - .style({'pointer-events': 'all'}); - - sliceTop.select('path.textline').remove(); - - sliceTop - .on('mouseover', handleMouseOver) - .on('mouseout', handleMouseOut) - .on('click', handleClick); - - if(trace.pull) { - var pull = +(Array.isArray(trace.pull) ? trace.pull[pt.i] : trace.pull) || 0; - if(pull > 0) { - cx += pull * pt.pxmid[0]; - cy += pull * pt.pxmid[1]; - } - } - - pt.cxFinal = cx; - pt.cyFinal = cy; - - function arc(start, finish, cw, scale) { - return 'a' + (scale * cd0.r) + ',' + (scale * rSmall) + ' ' + tiltAxis + ' ' + - pt.largeArc + (cw ? ' 1 ' : ' 0 ') + - (scale * (finish[0] - start[0])) + ',' + (scale * (finish[1] - start[1])); - } - - var hole = trace.hole; - if(pt.v === cd0.vTotal) { // 100% fails bcs arc start and end are identical - var outerCircle = 'M' + (cx + pt.px0[0]) + ',' + (cy + pt.px0[1]) + - arc(pt.px0, pt.pxmid, true, 1) + - arc(pt.pxmid, pt.px0, true, 1) + 'Z'; - if(hole) { - slicePath.attr('d', - 'M' + (cx + hole * pt.px0[0]) + ',' + (cy + hole * pt.px0[1]) + - arc(pt.px0, pt.pxmid, false, hole) + - arc(pt.pxmid, pt.px0, false, hole) + - 'Z' + outerCircle); - } - else slicePath.attr('d', outerCircle); - } else { - - var outerArc = arc(pt.px0, pt.px1, true, 1); - - if(hole) { - var rim = 1 - hole; - slicePath.attr('d', - 'M' + (cx + hole * pt.px1[0]) + ',' + (cy + hole * pt.px1[1]) + - arc(pt.px1, pt.px0, false, hole) + - 'l' + (rim * pt.px0[0]) + ',' + (rim * pt.px0[1]) + - outerArc + - 'Z'); - } else { - slicePath.attr('d', - 'M' + cx + ',' + cy + - 'l' + pt.px0[0] + ',' + pt.px0[1] + - outerArc + - 'Z'); - } - } - - // add text - var textPosition = Array.isArray(trace.textposition) ? - trace.textposition[pt.i] : trace.textposition, - sliceTextGroup = sliceTop.selectAll('g.slicetext') - .data(pt.text && (textPosition !== 'none') ? [0] : []); - - sliceTextGroup.enter().append('g') - .classed('slicetext', true); - sliceTextGroup.exit().remove(); - - sliceTextGroup.each(function() { - var sliceText = d3.select(this).selectAll('text').data([0]); - - sliceText.enter().append('text') - // prohibit tex interpretation until we can handle - // tex and regular text together - .attr('data-notex', 1); - sliceText.exit().remove(); - - sliceText.text(pt.text) - .attr({ - 'class': 'slicetext', - transform: '', - 'data-bb': '', - 'text-anchor': 'middle', - x: 0, - y: 0 - }) - .call(Drawing.font, textPosition === 'outside' ? - trace.outsidetextfont : trace.insidetextfont) - .call(svgTextUtils.convertToTspans); - sliceText.selectAll('tspan.line').attr({x: 0, y: 0}); - - // position the text relative to the slice - // TODO: so far this only accounts for flat - var textBB = Drawing.bBox(sliceText.node()), - transform; - - if(textPosition === 'outside') { - transform = transformOutsideText(textBB, pt); - } else { - transform = transformInsideText(textBB, pt, cd0); - if(textPosition === 'auto' && transform.scale < 1) { - sliceText.call(Drawing.font, trace.outsidetextfont); - if(trace.outsidetextfont.family !== trace.insidetextfont.family || - trace.outsidetextfont.size !== trace.insidetextfont.size) { - sliceText.attr({'data-bb': ''}); - textBB = Drawing.bBox(sliceText.node()); - } - transform = transformOutsideText(textBB, pt); - } - } - - var translateX = cx + pt.pxmid[0] * transform.rCenter + (transform.x || 0), - translateY = cy + pt.pxmid[1] * transform.rCenter + (transform.y || 0); - - // save some stuff to use later ensure no labels overlap - if(transform.outside) { - pt.yLabelMin = translateY - textBB.height / 2; - pt.yLabelMid = translateY; - pt.yLabelMax = translateY + textBB.height / 2; - pt.labelExtraX = 0; - pt.labelExtraY = 0; - hasOutsideText = true; - } - - sliceText.attr('transform', - 'translate(' + translateX + ',' + translateY + ')' + - (transform.scale < 1 ? ('scale(' + transform.scale + ')') : '') + - (transform.rotate ? ('rotate(' + transform.rotate + ')') : '') + - 'translate(' + - (-(textBB.left + textBB.right) / 2) + ',' + - (-(textBB.top + textBB.bottom) / 2) + - ')'); - }); - }); - - // now make sure no labels overlap (at least within one pie) - if(hasOutsideText) scootLabels(quadrants, trace); - slices.each(function(pt) { - if(pt.labelExtraX || pt.labelExtraY) { - // first move the text to its new location - var sliceTop = d3.select(this), - sliceText = sliceTop.select('g.slicetext text'); - - sliceText.attr('transform', 'translate(' + pt.labelExtraX + ',' + pt.labelExtraY + ')' + - sliceText.attr('transform')); - - // then add a line to the new location - var lineStartX = pt.cxFinal + pt.pxmid[0], - lineStartY = pt.cyFinal + pt.pxmid[1], - textLinePath = 'M' + lineStartX + ',' + lineStartY, - finalX = (pt.yLabelMax - pt.yLabelMin) * (pt.pxmid[0] < 0 ? -1 : 1) / 4; - if(pt.labelExtraX) { - var yFromX = pt.labelExtraX * pt.pxmid[1] / pt.pxmid[0], - yNet = pt.yLabelMid + pt.labelExtraY - (pt.cyFinal + pt.pxmid[1]); - - if(Math.abs(yFromX) > Math.abs(yNet)) { - textLinePath += - 'l' + (yNet * pt.pxmid[0] / pt.pxmid[1]) + ',' + yNet + - 'H' + (lineStartX + pt.labelExtraX + finalX); - } else { - textLinePath += 'l' + pt.labelExtraX + ',' + yFromX + - 'v' + (yNet - yFromX) + - 'h' + finalX; - } - } else { - textLinePath += - 'V' + (pt.yLabelMid + pt.labelExtraY) + - 'h' + finalX; - } - - sliceTop.append('path') - .classed('textline', true) - .call(Color.stroke, trace.outsidetextfont.color) - .attr({ - 'stroke-width': Math.min(2, trace.outsidetextfont.size / 8), - d: textLinePath, - fill: 'none' - }); - } - }); - }); - }); + slices.enter().append("g").classed("slice", true); + slices.exit().remove(); - // This is for a bug in Chrome (as of 2015-07-22, and does not affect FF) - // if insidetextfont and outsidetextfont are different sizes, sometimes the size - // of an "em" gets taken from the wrong element at first so lines are - // spaced wrong. You just have to tell it to try again later and it gets fixed. - // I have no idea why we haven't seen this in other contexts. Also, sometimes - // it gets the initial draw correct but on redraw it gets confused. - setTimeout(function() { - pieGroups.selectAll('tspan').each(function() { - var s = d3.select(this); - if(s.attr('dy')) s.attr('dy', s.attr('dy')); - }); - }, 0); -}; + var quadrants = [ + [[], []], + // y<0: x<0, x>=0 + // y>=0: x<0, x>=0 + [[], []] + ], + hasOutsideText = false; + slices.each(function(pt) { + if (pt.hidden) { + d3.select(this).selectAll("path,g").remove(); + return; + } -function transformInsideText(textBB, pt, cd0) { - var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height), - textAspect = textBB.width / textBB.height, - halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5), - ring = 1 - cd0.trace.hole, - rInscribed = getInscribedRadiusFraction(pt, cd0), - - // max size text can be inserted inside without rotating it - // this inscribes the text rectangle in a circle, which is then inscribed - // in the slice, so it will be an underestimate, which some day we may want - // to improve so this case can get more use - transform = { - scale: rInscribed * cd0.r * 2 / textDiameter, - - // and the center position and rotation in this case - rCenter: 1 - rInscribed, - rotate: 0 - }; - - if(transform.scale >= 1) return transform; - - // max size if text is rotated radially - var Qr = textAspect + 1 / (2 * Math.tan(halfAngle)), - maxHalfHeightRotRadial = cd0.r * Math.min( - 1 / (Math.sqrt(Qr * Qr + 0.5) + Qr), - ring / (Math.sqrt(textAspect * textAspect + ring / 2) + textAspect) - ), - radialTransform = { - scale: maxHalfHeightRotRadial * 2 / textBB.height, - rCenter: Math.cos(maxHalfHeightRotRadial / cd0.r) - - maxHalfHeightRotRadial * textAspect / cd0.r, - rotate: (180 / Math.PI * pt.midangle + 720) % 180 - 90 - }, - - // max size if text is rotated tangentially - aspectInv = 1 / textAspect, - Qt = aspectInv + 1 / (2 * Math.tan(halfAngle)), - maxHalfWidthTangential = cd0.r * Math.min( - 1 / (Math.sqrt(Qt * Qt + 0.5) + Qt), - ring / (Math.sqrt(aspectInv * aspectInv + ring / 2) + aspectInv) - ), - tangentialTransform = { - scale: maxHalfWidthTangential * 2 / textBB.width, - rCenter: Math.cos(maxHalfWidthTangential / cd0.r) - - maxHalfWidthTangential / textAspect / cd0.r, - rotate: (180 / Math.PI * pt.midangle + 810) % 180 - 90 - }, - // if we need a rotated transform, pick the biggest one - // even if both are bigger than 1 - rotatedTransform = tangentialTransform.scale > radialTransform.scale ? - tangentialTransform : radialTransform; - - if(transform.scale < 1 && rotatedTransform.scale > transform.scale) return rotatedTransform; - return transform; -} + quadrants[pt.pxmid[1] < 0 ? 0 : 1][pt.pxmid[0] < 0 ? 0 : 1].push(pt); + + var cx = cd0.cx + depthVector[0], + cy = cd0.cy + depthVector[1], + sliceTop = d3.select(this), + slicePath = sliceTop.selectAll("path.surface").data([pt]), + hasHoverData = false; + + function handleMouseOver(evt) { + // in case fullLayout or fullData has changed without a replot + var fullLayout2 = gd._fullLayout, + trace2 = gd._fullData[trace.index], + hoverinfo = trace2.hoverinfo; + + if (hoverinfo === "all") hoverinfo = "label+text+value+percent+name"; + + // in case we dragged over the pie from another subplot, + // or if hover is turned off + if ( + gd._dragging || + fullLayout2.hovermode === false || + hoverinfo === "none" || + hoverinfo === "skip" || + !hoverinfo + ) { + return; + } + + var rInscribed = getInscribedRadiusFraction(pt, cd0), + hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed), + hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed), + separators = fullLayout.separators, + thisText = []; + + if (hoverinfo.indexOf("label") !== -1) thisText.push(pt.label); + if ( + trace2.text && trace2.text[pt.i] && hoverinfo.indexOf("text") !== -1 + ) { + thisText.push(trace2.text[pt.i]); + } + if (hoverinfo.indexOf("value") !== -1) { + thisText.push(helpers.formatPieValue(pt.v, separators)); + } + if (hoverinfo.indexOf("percent") !== -1) { + thisText.push( + helpers.formatPiePercent(pt.v / cd0.vTotal, separators) + ); + } + + Fx.loneHover( + { + x0: hoverCenterX - rInscribed * cd0.r, + x1: hoverCenterX + rInscribed * cd0.r, + y: hoverCenterY, + text: thisText.join("
"), + name: hoverinfo.indexOf("name") !== -1 ? trace2.name : undefined, + color: pt.color, + idealAlign: pt.pxmid[0] < 0 ? "left" : "right" + }, + { + container: fullLayout2._hoverlayer.node(), + outerContainer: fullLayout2._paper.node() + } + ); -function getInscribedRadiusFraction(pt, cd0) { - if(pt.v === cd0.vTotal && !cd0.trace.hole) return 1;// special case of 100% with no hole + Fx.hover(gd, evt, "pie"); - var halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5); - return Math.min(1 / (1 + 1 / Math.sin(halfAngle)), (1 - cd0.trace.hole) / 2); -} + hasHoverData = true; + } -function transformOutsideText(textBB, pt) { - var x = pt.pxmid[0], - y = pt.pxmid[1], - dx = textBB.width / 2, - dy = textBB.height / 2; - - if(x < 0) dx *= -1; - if(y < 0) dy *= -1; - - return { - scale: 1, - rCenter: 1, - rotate: 0, - x: dx + Math.abs(dy) * (dx > 0 ? 1 : -1) / 2, - y: dy / (1 + x * x / (y * y)), - outside: true - }; -} + function handleMouseOut(evt) { + gd.emit("plotly_unhover", { points: [evt] }); -function scootLabels(quadrants, trace) { - var xHalf, - yHalf, - equatorFirst, - farthestX, - farthestY, - xDiffSign, - yDiffSign, - thisQuad, - oppositeQuad, - wholeSide, - i, - thisQuadOutside, - firstOppositeOutsidePt; - - function topFirst(a, b) { return a.pxmid[1] - b.pxmid[1]; } - function bottomFirst(a, b) { return b.pxmid[1] - a.pxmid[1]; } - - function scootOneLabel(thisPt, prevPt) { - if(!prevPt) prevPt = {}; - - var prevOuterY = prevPt.labelExtraY + (yHalf ? prevPt.yLabelMax : prevPt.yLabelMin), - thisInnerY = yHalf ? thisPt.yLabelMin : thisPt.yLabelMax, - thisOuterY = yHalf ? thisPt.yLabelMax : thisPt.yLabelMin, - thisSliceOuterY = thisPt.cyFinal + farthestY(thisPt.px0[1], thisPt.px1[1]), - newExtraY = prevOuterY - thisInnerY, - xBuffer, - i, - otherPt, - otherOuterY, - otherOuterX, - newExtraX; - // make sure this label doesn't overlap other labels - // this *only* has us move these labels vertically - if(newExtraY * yDiffSign > 0) thisPt.labelExtraY = newExtraY; - - // make sure this label doesn't overlap any slices - if(!Array.isArray(trace.pull)) return; // this can only happen with array pulls - - for(i = 0; i < wholeSide.length; i++) { - otherPt = wholeSide[i]; - - // overlap can only happen if the other point is pulled more than this one - if(otherPt === thisPt || ((trace.pull[thisPt.i] || 0) >= trace.pull[otherPt.i] || 0)) continue; - - if((thisPt.pxmid[1] - otherPt.pxmid[1]) * yDiffSign > 0) { - // closer to the equator - by construction all of these happen first - // move the text vertically to get away from these slices - otherOuterY = otherPt.cyFinal + farthestY(otherPt.px0[1], otherPt.px1[1]); - newExtraY = otherOuterY - thisInnerY - thisPt.labelExtraY; - - if(newExtraY * yDiffSign > 0) thisPt.labelExtraY += newExtraY; - - } else if((thisOuterY + thisPt.labelExtraY - thisSliceOuterY) * yDiffSign > 0) { - // farther from the equator - happens after we've done all the - // vertical moving we're going to do - // move horizontally to get away from these more polar slices - - // if we're moving horz. based on a slice that's several slices away from this one - // then we need some extra space for the lines to labels between them - xBuffer = 3 * xDiffSign * Math.abs(i - wholeSide.indexOf(thisPt)); - - otherOuterX = otherPt.cxFinal + farthestX(otherPt.px0[0], otherPt.px1[0]); - newExtraX = otherOuterX + xBuffer - (thisPt.cxFinal + thisPt.pxmid[0]) - thisPt.labelExtraX; - - if(newExtraX * xDiffSign > 0) thisPt.labelExtraX += newExtraX; - } + if (hasHoverData) { + Fx.loneUnhover(fullLayout._hoverlayer.node()); + hasHoverData = false; + } } - } - for(yHalf = 0; yHalf < 2; yHalf++) { - equatorFirst = yHalf ? topFirst : bottomFirst; - farthestY = yHalf ? Math.max : Math.min; - yDiffSign = yHalf ? 1 : -1; - - for(xHalf = 0; xHalf < 2; xHalf++) { - farthestX = xHalf ? Math.max : Math.min; - xDiffSign = xHalf ? 1 : -1; + function handleClick() { + gd._hoverdata = [pt]; + gd._hoverdata.trace = cd.trace; + Fx.click(gd, { target: true }); + } - // first sort the array - // note this is a copy of cd, so cd itself doesn't get sorted - // but we can still modify points in place. - thisQuad = quadrants[yHalf][xHalf]; - thisQuad.sort(equatorFirst); + slicePath + .enter() + .append("path") + .classed("surface", true) + .style({ "pointer-events": "all" }); + + sliceTop.select("path.textline").remove(); + + sliceTop + .on("mouseover", handleMouseOver) + .on("mouseout", handleMouseOut) + .on("click", handleClick); + + if (trace.pull) { + var pull = +(Array.isArray(trace.pull) + ? trace.pull[pt.i] + : trace.pull) || + 0; + if (pull > 0) { + cx += pull * pt.pxmid[0]; + cy += pull * pt.pxmid[1]; + } + } - oppositeQuad = quadrants[1 - yHalf][xHalf]; - wholeSide = oppositeQuad.concat(thisQuad); + pt.cxFinal = cx; + pt.cyFinal = cy; + + function arc(start, finish, cw, scale) { + return "a" + + scale * cd0.r + + "," + + scale * rSmall + + " " + + tiltAxis + + " " + + pt.largeArc + + (cw ? " 1 " : " 0 ") + + scale * (finish[0] - start[0]) + + "," + + scale * (finish[1] - start[1]); + } - thisQuadOutside = []; - for(i = 0; i < thisQuad.length; i++) { - if(thisQuad[i].yLabelMid !== undefined) thisQuadOutside.push(thisQuad[i]); - } + var hole = trace.hole; + if (pt.v === cd0.vTotal) { + // 100% fails bcs arc start and end are identical + var outerCircle = "M" + + (cx + pt.px0[0]) + + "," + + (cy + pt.px0[1]) + + arc(pt.px0, pt.pxmid, true, 1) + + arc(pt.pxmid, pt.px0, true, 1) + + "Z"; + if (hole) { + slicePath.attr( + "d", + "M" + + (cx + hole * pt.px0[0]) + + "," + + (cy + hole * pt.px0[1]) + + arc(pt.px0, pt.pxmid, false, hole) + + arc(pt.pxmid, pt.px0, false, hole) + + "Z" + + outerCircle + ); + } else { + slicePath.attr("d", outerCircle); + } + } else { + var outerArc = arc(pt.px0, pt.px1, true, 1); + + if (hole) { + var rim = 1 - hole; + slicePath.attr( + "d", + "M" + + (cx + hole * pt.px1[0]) + + "," + + (cy + hole * pt.px1[1]) + + arc(pt.px1, pt.px0, false, hole) + + "l" + + rim * pt.px0[0] + + "," + + rim * pt.px0[1] + + outerArc + + "Z" + ); + } else { + slicePath.attr( + "d", + "M" + + cx + + "," + + cy + + "l" + + pt.px0[0] + + "," + + pt.px0[1] + + outerArc + + "Z" + ); + } + } - firstOppositeOutsidePt = false; - for(i = 0; yHalf && i < oppositeQuad.length; i++) { - if(oppositeQuad[i].yLabelMid !== undefined) { - firstOppositeOutsidePt = oppositeQuad[i]; - break; - } + // add text + var textPosition = Array.isArray(trace.textposition) + ? trace.textposition[pt.i] + : trace.textposition, + sliceTextGroup = sliceTop + .selectAll("g.slicetext") + .data(pt.text && textPosition !== "none" ? [0] : []); + + sliceTextGroup.enter().append("g").classed("slicetext", true); + sliceTextGroup.exit().remove(); + + sliceTextGroup.each(function() { + var sliceText = d3.select(this).selectAll("text").data([0]); + + sliceText.enter().append("text").attr("data-notex", 1); + sliceText.exit().remove(); + + sliceText + .text(pt.text) + .attr({ + class: "slicetext", + transform: "", + "data-bb": "", + "text-anchor": "middle", + x: 0, + y: 0 + }) + .call( + Drawing.font, + textPosition === "outside" + ? trace.outsidetextfont + : trace.insidetextfont + ) + .call(svgTextUtils.convertToTspans); + sliceText.selectAll("tspan.line").attr({ x: 0, y: 0 }); + + // position the text relative to the slice + // TODO: so far this only accounts for flat + var textBB = Drawing.bBox(sliceText.node()), transform; + + if (textPosition === "outside") { + transform = transformOutsideText(textBB, pt); + } else { + transform = transformInsideText(textBB, pt, cd0); + if (textPosition === "auto" && transform.scale < 1) { + sliceText.call(Drawing.font, trace.outsidetextfont); + if ( + trace.outsidetextfont.family !== trace.insidetextfont.family || + trace.outsidetextfont.size !== trace.insidetextfont.size + ) { + sliceText.attr({ "data-bb": "" }); + textBB = Drawing.bBox(sliceText.node()); + } + transform = transformOutsideText(textBB, pt); } - - // each needs to avoid the previous - for(i = 0; i < thisQuadOutside.length; i++) { - var prevPt = i && thisQuadOutside[i - 1]; - // bottom half needs to avoid the first label of the top half - // top half we still need to call scootOneLabel on the first slice - // so we can avoid other slices, but we don't pass a prevPt - if(firstOppositeOutsidePt && !i) prevPt = firstOppositeOutsidePt; - scootOneLabel(thisQuadOutside[i], prevPt); + } + + var translateX = cx + + pt.pxmid[0] * transform.rCenter + + (transform.x || 0), + translateY = cy + + pt.pxmid[1] * transform.rCenter + + (transform.y || 0); + + // save some stuff to use later ensure no labels overlap + if (transform.outside) { + pt.yLabelMin = translateY - textBB.height / 2; + pt.yLabelMid = translateY; + pt.yLabelMax = translateY + textBB.height / 2; + pt.labelExtraX = 0; + pt.labelExtraY = 0; + hasOutsideText = true; + } + + sliceText.attr( + "transform", + "translate(" + + translateX + + "," + + translateY + + ")" + + (transform.scale < 1 ? "scale(" + transform.scale + ")" : "") + + (transform.rotate ? "rotate(" + transform.rotate + ")" : "") + + "translate(" + + (-(textBB.left + textBB.right)) / 2 + + "," + + (-(textBB.top + textBB.bottom)) / 2 + + ")" + ); + }); + }); + + // now make sure no labels overlap (at least within one pie) + if (hasOutsideText) scootLabels(quadrants, trace); + slices.each(function(pt) { + if (pt.labelExtraX || pt.labelExtraY) { + // first move the text to its new location + var sliceTop = d3.select(this), + sliceText = sliceTop.select("g.slicetext text"); + + sliceText.attr( + "transform", + "translate(" + + pt.labelExtraX + + "," + + pt.labelExtraY + + ")" + + sliceText.attr("transform") + ); + + // then add a line to the new location + var lineStartX = pt.cxFinal + pt.pxmid[0], + lineStartY = pt.cyFinal + pt.pxmid[1], + textLinePath = "M" + lineStartX + "," + lineStartY, + finalX = (pt.yLabelMax - pt.yLabelMin) * + (pt.pxmid[0] < 0 ? -1 : 1) / + 4; + if (pt.labelExtraX) { + var yFromX = pt.labelExtraX * pt.pxmid[1] / pt.pxmid[0], + yNet = pt.yLabelMid + pt.labelExtraY - (pt.cyFinal + pt.pxmid[1]); + + if (Math.abs(yFromX) > Math.abs(yNet)) { + textLinePath += "l" + + yNet * pt.pxmid[0] / pt.pxmid[1] + + "," + + yNet + + "H" + + (lineStartX + pt.labelExtraX + finalX); + } else { + textLinePath += "l" + + pt.labelExtraX + + "," + + yFromX + + "v" + + (yNet - yFromX) + + "h" + + finalX; } + } else { + textLinePath += "V" + + (pt.yLabelMid + pt.labelExtraY) + + "h" + + finalX; + } + + sliceTop + .append("path") + .classed("textline", true) + .call(Color.stroke, trace.outsidetextfont.color) + .attr({ + "stroke-width": Math.min(2, trace.outsidetextfont.size / 8), + d: textLinePath, + fill: "none" + }); } - } -} + }); + }); + }); + + // This is for a bug in Chrome (as of 2015-07-22, and does not affect FF) + // if insidetextfont and outsidetextfont are different sizes, sometimes the size + // of an "em" gets taken from the wrong element at first so lines are + // spaced wrong. You just have to tell it to try again later and it gets fixed. + // I have no idea why we haven't seen this in other contexts. Also, sometimes + // it gets the initial draw correct but on redraw it gets confused. + setTimeout( + function() { + pieGroups.selectAll("tspan").each(function() { + var s = d3.select(this); + if (s.attr("dy")) s.attr("dy", s.attr("dy")); + }); + }, + 0 + ); +}; -function scalePies(cdpie, plotSize) { - var pieBoxWidth, - pieBoxHeight, - i, - j, - cd0, - trace, - tiltAxisRads, - maxPull, - scaleGroups = [], - scaleGroup, - minPxPerValUnit; - - // first figure out the center and maximum radius for each pie - for(i = 0; i < cdpie.length; i++) { - cd0 = cdpie[i][0]; - trace = cd0.trace; - pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]); - pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]); - tiltAxisRads = trace.tiltaxis * Math.PI / 180; - - maxPull = trace.pull; - if(Array.isArray(maxPull)) { - maxPull = 0; - for(j = 0; j < trace.pull.length; j++) { - if(trace.pull[j] > maxPull) maxPull = trace.pull[j]; - } - } +function transformInsideText(textBB, pt, cd0) { + var textDiameter = Math.sqrt( + textBB.width * textBB.width + textBB.height * textBB.height + ), + textAspect = textBB.width / textBB.height, + halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5), + ring = 1 - cd0.trace.hole, + rInscribed = getInscribedRadiusFraction(pt, cd0), + // max size text can be inserted inside without rotating it + // this inscribes the text rectangle in a circle, which is then inscribed + // in the slice, so it will be an underestimate, which some day we may want + // to improve so this case can get more use + transform = { + scale: rInscribed * cd0.r * 2 / textDiameter, + // and the center position and rotation in this case + rCenter: ( + 1 - rInscribed + ), + rotate: 0 + }; - cd0.r = Math.min( - pieBoxWidth / maxExtent(trace.tilt, Math.sin(tiltAxisRads), trace.depth), - pieBoxHeight / maxExtent(trace.tilt, Math.cos(tiltAxisRads), trace.depth) - ) / (2 + 2 * maxPull); + if (transform.scale >= 1) return transform; + + // max size if text is rotated radially + var Qr = textAspect + 1 / (2 * Math.tan(halfAngle)), + maxHalfHeightRotRadial = cd0.r * + Math.min( + 1 / (Math.sqrt(Qr * Qr + 0.5) + Qr), + ring / (Math.sqrt(textAspect * textAspect + ring / 2) + textAspect) + ), + radialTransform = { + scale: maxHalfHeightRotRadial * 2 / textBB.height, + rCenter: ( + Math.cos(maxHalfHeightRotRadial / cd0.r) - + maxHalfHeightRotRadial * textAspect / cd0.r + ), + rotate: (180 / Math.PI * pt.midangle + 720) % 180 - 90 + }, + // max size if text is rotated tangentially + aspectInv = 1 / textAspect, + Qt = aspectInv + 1 / (2 * Math.tan(halfAngle)), + maxHalfWidthTangential = cd0.r * + Math.min( + 1 / (Math.sqrt(Qt * Qt + 0.5) + Qt), + ring / (Math.sqrt(aspectInv * aspectInv + ring / 2) + aspectInv) + ), + tangentialTransform = { + scale: maxHalfWidthTangential * 2 / textBB.width, + rCenter: ( + Math.cos(maxHalfWidthTangential / cd0.r) - + maxHalfWidthTangential / textAspect / cd0.r + ), + rotate: (180 / Math.PI * pt.midangle + 810) % 180 - 90 + }, + // if we need a rotated transform, pick the biggest one + // even if both are bigger than 1 + rotatedTransform = tangentialTransform.scale > radialTransform.scale + ? tangentialTransform + : radialTransform; + + if (transform.scale < 1 && rotatedTransform.scale > transform.scale) { + return rotatedTransform; + } + return transform; +} - cd0.cx = plotSize.l + plotSize.w * (trace.domain.x[1] + trace.domain.x[0]) / 2; - cd0.cy = plotSize.t + plotSize.h * (2 - trace.domain.y[1] - trace.domain.y[0]) / 2; +function getInscribedRadiusFraction(pt, cd0) { + if (pt.v === cd0.vTotal && !cd0.trace.hole) return 1; - if(trace.scalegroup && scaleGroups.indexOf(trace.scalegroup) === -1) { - scaleGroups.push(trace.scalegroup); - } - } + // special case of 100% with no hole + var halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5); + return Math.min(1 / (1 + 1 / Math.sin(halfAngle)), (1 - cd0.trace.hole) / 2); +} - // Then scale any pies that are grouped - for(j = 0; j < scaleGroups.length; j++) { - minPxPerValUnit = Infinity; - scaleGroup = scaleGroups[j]; +function transformOutsideText(textBB, pt) { + var x = pt.pxmid[0], + y = pt.pxmid[1], + dx = textBB.width / 2, + dy = textBB.height / 2; + + if (x < 0) dx *= -1; + if (y < 0) dy *= -1; + + return { + scale: 1, + rCenter: 1, + rotate: 0, + x: dx + Math.abs(dy) * (dx > 0 ? 1 : -1) / 2, + y: dy / (1 + x * x / (y * y)), + outside: true + }; +} - for(i = 0; i < cdpie.length; i++) { - cd0 = cdpie[i][0]; - if(cd0.trace.scalegroup === scaleGroup) { - minPxPerValUnit = Math.min(minPxPerValUnit, - cd0.r * cd0.r / cd0.vTotal); - } +function scootLabels(quadrants, trace) { + var xHalf, + yHalf, + equatorFirst, + farthestX, + farthestY, + xDiffSign, + yDiffSign, + thisQuad, + oppositeQuad, + wholeSide, + i, + thisQuadOutside, + firstOppositeOutsidePt; + + function topFirst(a, b) { + return a.pxmid[1] - b.pxmid[1]; + } + function bottomFirst(a, b) { + return b.pxmid[1] - a.pxmid[1]; + } + + function scootOneLabel(thisPt, prevPt) { + if (!prevPt) prevPt = {}; + + var prevOuterY = prevPt.labelExtraY + + (yHalf ? prevPt.yLabelMax : prevPt.yLabelMin), + thisInnerY = yHalf ? thisPt.yLabelMin : thisPt.yLabelMax, + thisOuterY = yHalf ? thisPt.yLabelMax : thisPt.yLabelMin, + thisSliceOuterY = thisPt.cyFinal + + farthestY(thisPt.px0[1], thisPt.px1[1]), + newExtraY = prevOuterY - thisInnerY, + xBuffer, + i, + otherPt, + otherOuterY, + otherOuterX, + newExtraX; + // make sure this label doesn't overlap other labels + // this *only* has us move these labels vertically + if (newExtraY * yDiffSign > 0) thisPt.labelExtraY = newExtraY; + + // make sure this label doesn't overlap any slices + if (!Array.isArray(trace.pull)) return; + + // this can only happen with array pulls + for (i = 0; i < wholeSide.length; i++) { + otherPt = wholeSide[i]; + + // overlap can only happen if the other point is pulled more than this one + if ( + otherPt === thisPt || + ((trace.pull[thisPt.i] || 0) >= trace.pull[otherPt.i] || 0) + ) { + continue; + } + + if ((thisPt.pxmid[1] - otherPt.pxmid[1]) * yDiffSign > 0) { + // closer to the equator - by construction all of these happen first + // move the text vertically to get away from these slices + otherOuterY = otherPt.cyFinal + + farthestY(otherPt.px0[1], otherPt.px1[1]); + newExtraY = otherOuterY - thisInnerY - thisPt.labelExtraY; + + if (newExtraY * yDiffSign > 0) thisPt.labelExtraY += newExtraY; + } else if ( + (thisOuterY + thisPt.labelExtraY - thisSliceOuterY) * yDiffSign > 0 + ) { + // farther from the equator - happens after we've done all the + // vertical moving we're going to do + // move horizontally to get away from these more polar slices + // if we're moving horz. based on a slice that's several slices away from this one + // then we need some extra space for the lines to labels between them + xBuffer = 3 * xDiffSign * Math.abs(i - wholeSide.indexOf(thisPt)); + + otherOuterX = otherPt.cxFinal + + farthestX(otherPt.px0[0], otherPt.px1[0]); + newExtraX = otherOuterX + + xBuffer - + (thisPt.cxFinal + thisPt.pxmid[0]) - + thisPt.labelExtraX; + + if (newExtraX * xDiffSign > 0) thisPt.labelExtraX += newExtraX; + } + } + } + + for (yHalf = 0; yHalf < 2; yHalf++) { + equatorFirst = yHalf ? topFirst : bottomFirst; + farthestY = yHalf ? Math.max : Math.min; + yDiffSign = yHalf ? 1 : -1; + + for (xHalf = 0; xHalf < 2; xHalf++) { + farthestX = xHalf ? Math.max : Math.min; + xDiffSign = xHalf ? 1 : -1; + + // first sort the array + // note this is a copy of cd, so cd itself doesn't get sorted + // but we can still modify points in place. + thisQuad = quadrants[yHalf][xHalf]; + thisQuad.sort(equatorFirst); + + oppositeQuad = quadrants[1 - yHalf][xHalf]; + wholeSide = oppositeQuad.concat(thisQuad); + + thisQuadOutside = []; + for (i = 0; i < thisQuad.length; i++) { + if (thisQuad[i].yLabelMid !== undefined) { + thisQuadOutside.push(thisQuad[i]); } + } - for(i = 0; i < cdpie.length; i++) { - cd0 = cdpie[i][0]; - if(cd0.trace.scalegroup === scaleGroup) { - cd0.r = Math.sqrt(minPxPerValUnit * cd0.vTotal); - } + firstOppositeOutsidePt = false; + for (i = 0; yHalf && i < oppositeQuad.length; i++) { + if (oppositeQuad[i].yLabelMid !== undefined) { + firstOppositeOutsidePt = oppositeQuad[i]; + break; } + } + + // each needs to avoid the previous + for (i = 0; i < thisQuadOutside.length; i++) { + var prevPt = i && thisQuadOutside[i - 1]; + // bottom half needs to avoid the first label of the top half + // top half we still need to call scootOneLabel on the first slice + // so we can avoid other slices, but we don't pass a prevPt + if (firstOppositeOutsidePt && !i) prevPt = firstOppositeOutsidePt; + scootOneLabel(thisQuadOutside[i], prevPt); + } } - + } } -function setCoords(cd) { - var cd0 = cd[0], - trace = cd0.trace, - tilt = trace.tilt, - tiltAxisRads, - tiltAxisSin, - tiltAxisCos, - tiltRads, - crossTilt, - inPlane, - currentAngle = trace.rotation * Math.PI / 180, - angleFactor = 2 * Math.PI / cd0.vTotal, - firstPt = 'px0', - lastPt = 'px1', - i, - cdi, - currentCoords; - - if(trace.direction === 'counterclockwise') { - for(i = 0; i < cd.length; i++) { - if(!cd[i].hidden) break; // find the first non-hidden slice - } - if(i === cd.length) return; // all slices hidden - - currentAngle += angleFactor * cd[i].v; - angleFactor *= -1; - firstPt = 'px1'; - lastPt = 'px0'; +function scalePies(cdpie, plotSize) { + var pieBoxWidth, + pieBoxHeight, + i, + j, + cd0, + trace, + tiltAxisRads, + maxPull, + scaleGroups = [], + scaleGroup, + minPxPerValUnit; + + // first figure out the center and maximum radius for each pie + for (i = 0; i < cdpie.length; i++) { + cd0 = cdpie[i][0]; + trace = cd0.trace; + pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]); + pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]); + tiltAxisRads = trace.tiltaxis * Math.PI / 180; + + maxPull = trace.pull; + if (Array.isArray(maxPull)) { + maxPull = 0; + for (j = 0; j < trace.pull.length; j++) { + if (trace.pull[j] > maxPull) maxPull = trace.pull[j]; + } } - if(tilt) { - tiltRads = tilt * Math.PI / 180; - tiltAxisRads = trace.tiltaxis * Math.PI / 180; - crossTilt = Math.sin(tiltAxisRads) * Math.cos(tiltAxisRads); - inPlane = 1 - Math.cos(tiltRads); - tiltAxisSin = Math.sin(tiltAxisRads); - tiltAxisCos = Math.cos(tiltAxisRads); - } + cd0.r = Math.min( + pieBoxWidth / maxExtent(trace.tilt, Math.sin(tiltAxisRads), trace.depth), + pieBoxHeight / maxExtent(trace.tilt, Math.cos(tiltAxisRads), trace.depth) + ) / + (2 + 2 * maxPull); - function getCoords(angle) { - var xFlat = cd0.r * Math.sin(angle), - yFlat = -cd0.r * Math.cos(angle); + cd0.cx = plotSize.l + + plotSize.w * (trace.domain.x[1] + trace.domain.x[0]) / 2; + cd0.cy = plotSize.t + + plotSize.h * (2 - trace.domain.y[1] - trace.domain.y[0]) / 2; - if(!tilt) return [xFlat, yFlat]; + if (trace.scalegroup && scaleGroups.indexOf(trace.scalegroup) === -1) { + scaleGroups.push(trace.scalegroup); + } + } + + // Then scale any pies that are grouped + for (j = 0; j < scaleGroups.length; j++) { + minPxPerValUnit = Infinity; + scaleGroup = scaleGroups[j]; + + for (i = 0; i < cdpie.length; i++) { + cd0 = cdpie[i][0]; + if (cd0.trace.scalegroup === scaleGroup) { + minPxPerValUnit = Math.min(minPxPerValUnit, cd0.r * cd0.r / cd0.vTotal); + } + } - return [ - xFlat * (1 - inPlane * tiltAxisSin * tiltAxisSin) + yFlat * crossTilt * inPlane, - xFlat * crossTilt * inPlane + yFlat * (1 - inPlane * tiltAxisCos * tiltAxisCos), - Math.sin(tiltRads) * (yFlat * tiltAxisCos - xFlat * tiltAxisSin) - ]; + for (i = 0; i < cdpie.length; i++) { + cd0 = cdpie[i][0]; + if (cd0.trace.scalegroup === scaleGroup) { + cd0.r = Math.sqrt(minPxPerValUnit * cd0.vTotal); + } } + } +} +function setCoords(cd) { + var cd0 = cd[0], + trace = cd0.trace, + tilt = trace.tilt, + tiltAxisRads, + tiltAxisSin, + tiltAxisCos, + tiltRads, + crossTilt, + inPlane, + currentAngle = trace.rotation * Math.PI / 180, + angleFactor = 2 * Math.PI / cd0.vTotal, + firstPt = "px0", + lastPt = "px1", + i, + cdi, + currentCoords; + + if (trace.direction === "counterclockwise") { + for (i = 0; i < cd.length; i++) { + if (!cd[i].hidden) break; // find the first non-hidden slice + } + if (i === cd.length) return; + + // all slices hidden + currentAngle += angleFactor * cd[i].v; + angleFactor *= -1; + firstPt = "px1"; + lastPt = "px0"; + } + + if (tilt) { + tiltRads = tilt * Math.PI / 180; + tiltAxisRads = trace.tiltaxis * Math.PI / 180; + crossTilt = Math.sin(tiltAxisRads) * Math.cos(tiltAxisRads); + inPlane = 1 - Math.cos(tiltRads); + tiltAxisSin = Math.sin(tiltAxisRads); + tiltAxisCos = Math.cos(tiltAxisRads); + } + + function getCoords(angle) { + var xFlat = cd0.r * Math.sin(angle), yFlat = (-cd0.r) * Math.cos(angle); + + if (!tilt) return [xFlat, yFlat]; + + return [ + xFlat * (1 - inPlane * tiltAxisSin * tiltAxisSin) + + yFlat * crossTilt * inPlane, + xFlat * crossTilt * inPlane + + yFlat * (1 - inPlane * tiltAxisCos * tiltAxisCos), + Math.sin(tiltRads) * (yFlat * tiltAxisCos - xFlat * tiltAxisSin) + ]; + } + + currentCoords = getCoords(currentAngle); + + for (i = 0; i < cd.length; i++) { + cdi = cd[i]; + if (cdi.hidden) continue; + + cdi[firstPt] = currentCoords; + + currentAngle += angleFactor * cdi.v / 2; + cdi.pxmid = getCoords(currentAngle); + cdi.midangle = currentAngle; + + currentAngle += angleFactor * cdi.v / 2; currentCoords = getCoords(currentAngle); - for(i = 0; i < cd.length; i++) { - cdi = cd[i]; - if(cdi.hidden) continue; - - cdi[firstPt] = currentCoords; + cdi[lastPt] = currentCoords; - currentAngle += angleFactor * cdi.v / 2; - cdi.pxmid = getCoords(currentAngle); - cdi.midangle = currentAngle; - - currentAngle += angleFactor * cdi.v / 2; - currentCoords = getCoords(currentAngle); - - cdi[lastPt] = currentCoords; - - cdi.largeArc = (cdi.v > cd0.vTotal / 2) ? 1 : 0; - } + cdi.largeArc = cdi.v > cd0.vTotal / 2 ? 1 : 0; + } } function maxExtent(tilt, tiltAxisFraction, depth) { - if(!tilt) return 1; - var sinTilt = Math.sin(tilt * Math.PI / 180); - return Math.max(0.01, // don't let it go crazy if you tilt the pie totally on its side - depth * sinTilt * Math.abs(tiltAxisFraction) + - 2 * Math.sqrt(1 - sinTilt * sinTilt * tiltAxisFraction * tiltAxisFraction)); + if (!tilt) return 1; + var sinTilt = Math.sin(tilt * Math.PI / 180); + return Math.max( + 0.01, + // don't let it go crazy if you tilt the pie totally on its side + depth * sinTilt * Math.abs(tiltAxisFraction) + + 2 * Math.sqrt(1 - sinTilt * sinTilt * tiltAxisFraction * tiltAxisFraction) + ); } diff --git a/src/traces/pie/style.js b/src/traces/pie/style.js index fb02933eb00..35e8e7b2938 100644 --- a/src/traces/pie/style.js +++ b/src/traces/pie/style.js @@ -5,23 +5,19 @@ * 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 d3 = require("d3"); -'use strict'; - -var d3 = require('d3'); - -var styleOne = require('./style_one'); +var styleOne = require("./style_one"); module.exports = function style(gd) { - gd._fullLayout._pielayer.selectAll('.trace').each(function(cd) { - var cd0 = cd[0], - trace = cd0.trace, - traceSelection = d3.select(this); + gd._fullLayout._pielayer.selectAll(".trace").each(function(cd) { + var cd0 = cd[0], trace = cd0.trace, traceSelection = d3.select(this); - traceSelection.style({opacity: trace.opacity}); + traceSelection.style({ opacity: trace.opacity }); - traceSelection.selectAll('.top path.surface').each(function(pt) { - d3.select(this).call(styleOne, pt, trace); - }); + traceSelection.selectAll(".top path.surface").each(function(pt) { + d3.select(this).call(styleOne, pt, trace); }); + }); }; diff --git a/src/traces/pie/style_one.js b/src/traces/pie/style_one.js index d6d66738082..8b49fed11cf 100644 --- a/src/traces/pie/style_one.js +++ b/src/traces/pie/style_one.js @@ -5,21 +5,19 @@ * 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 Color = require('../../components/color'); +"use strict"; +var Color = require("../../components/color"); module.exports = function styleOne(s, pt, trace) { - var lineColor = trace.marker.line.color; - if(Array.isArray(lineColor)) lineColor = lineColor[pt.i] || Color.defaultLine; + var lineColor = trace.marker.line.color; + if (Array.isArray(lineColor)) { + lineColor = lineColor[pt.i] || Color.defaultLine; + } - var lineWidth = trace.marker.line.width || 0; - if(Array.isArray(lineWidth)) lineWidth = lineWidth[pt.i] || 0; + var lineWidth = trace.marker.line.width || 0; + if (Array.isArray(lineWidth)) lineWidth = lineWidth[pt.i] || 0; - s.style({ - 'stroke-width': lineWidth, - fill: pt.color - }) + s + .style({ "stroke-width": lineWidth, fill: pt.color }) .call(Color.stroke, lineColor); }; diff --git a/src/traces/pointcloud/attributes.js b/src/traces/pointcloud/attributes.js index 3c3d76277c4..09b7ae418b3 100644 --- a/src/traces/pointcloud/attributes.js +++ b/src/traces/pointcloud/attributes.js @@ -5,128 +5,126 @@ * 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'); +"use strict"; +var scatterglAttrs = require("../scattergl/attributes"); module.exports = { - x: scatterglAttrs.x, - y: scatterglAttrs.y, - xy: { - valType: 'data_array', - description: [ - 'Faster alternative to specifying `x` and `y` separately.', - 'If supplied, it must be a typed `Float32Array` array that', - 'represents points such that `xy[i * 2] = x[i]` and `xy[i * 2 + 1] = y[i]`' - ].join(' ') + x: scatterglAttrs.x, + y: scatterglAttrs.y, + xy: { + valType: "data_array", + description: [ + "Faster alternative to specifying `x` and `y` separately.", + "If supplied, it must be a typed `Float32Array` array that", + "represents points such that `xy[i * 2] = x[i]` and `xy[i * 2 + 1] = y[i]`" + ].join(" ") + }, + indices: { + valType: "data_array", + description: [ + "A sequential value, 0..n, supply it to avoid creating this array inside plotting.", + "If specified, it must be a typed `Int32Array` array.", + "Its length must be equal to or greater than the number of points.", + "For the best performance and memory use, create one large `indices` typed array", + "that is guaranteed to be at least as long as the largest number of points during", + "use, and reuse it on each `Plotly.restyle()` call." + ].join(" ") + }, + xbounds: { + valType: "data_array", + description: [ + "Specify `xbounds` in the shape of `[xMin, xMax] to avoid looping through", + "the `xy` typed array. Use it in conjunction with `xy` and `ybounds` for the performance benefits." + ].join(" ") + }, + ybounds: { + valType: "data_array", + description: [ + "Specify `ybounds` in the shape of `[yMin, yMax] to avoid looping through", + "the `xy` typed array. Use it in conjunction with `xy` and `xbounds` for the performance benefits." + ].join(" ") + }, + text: scatterglAttrs.text, + marker: { + color: { + valType: "color", + arrayOk: false, + role: "style", + description: [ + "Sets the marker fill color. It accepts a specific color.", + "If the color is not fully opaque and there are hundreds of thousands", + "of points, it may cause slower zooming and panning." + ].join("") }, - indices: { - valType: 'data_array', - description: [ - 'A sequential value, 0..n, supply it to avoid creating this array inside plotting.', - 'If specified, it must be a typed `Int32Array` array.', - 'Its length must be equal to or greater than the number of points.', - 'For the best performance and memory use, create one large `indices` typed array', - 'that is guaranteed to be at least as long as the largest number of points during', - 'use, and reuse it on each `Plotly.restyle()` call.' - ].join(' ') + opacity: { + valType: "number", + min: 0, + max: 1, + dflt: 1, + arrayOk: false, + role: "style", + description: [ + "Sets the marker opacity. The default value is `1` (fully opaque).", + "If the markers are not fully opaque and there are hundreds of thousands", + "of points, it may cause slower zooming and panning.", + "Opacity fades the color even if `blend` is left on `false` even if there", + "is no translucency effect in that case." + ].join(" ") }, - xbounds: { - valType: 'data_array', - description: [ - 'Specify `xbounds` in the shape of `[xMin, xMax] to avoid looping through', - 'the `xy` typed array. Use it in conjunction with `xy` and `ybounds` for the performance benefits.' - ].join(' ') + blend: { + valType: "boolean", + dflt: null, + role: "style", + description: [ + "Determines if colors are blended together for a translucency effect", + "in case `opacity` is specified as a value less then `1`.", + "Setting `blend` to `true` reduces zoom/pan", + "speed if used with large numbers of points." + ].join(" ") }, - ybounds: { - valType: 'data_array', - description: [ - 'Specify `ybounds` in the shape of `[yMin, yMax] to avoid looping through', - 'the `xy` typed array. Use it in conjunction with `xy` and `xbounds` for the performance benefits.' - ].join(' ') + sizemin: { + valType: "number", + min: 0.1, + max: 2, + dflt: 0.5, + role: "style", + description: [ + "Sets the minimum size (in px) of the rendered marker points, effective when", + "the `pointcloud` shows a million or more points." + ].join(" ") + }, + sizemax: { + valType: "number", + min: 0.1, + dflt: 20, + role: "style", + description: [ + "Sets the maximum size (in px) of the rendered marker points.", + "Effective when the `pointcloud` shows only few points." + ].join(" ") }, - text: scatterglAttrs.text, - marker: { - color: { - valType: 'color', - arrayOk: false, - role: 'style', - description: [ - 'Sets the marker fill color. It accepts a specific color.', - 'If the color is not fully opaque and there are hundreds of thousands', - 'of points, it may cause slower zooming and panning.' - ].join('') - }, - opacity: { - valType: 'number', - min: 0, - max: 1, - dflt: 1, - arrayOk: false, - role: 'style', - description: [ - 'Sets the marker opacity. The default value is `1` (fully opaque).', - 'If the markers are not fully opaque and there are hundreds of thousands', - 'of points, it may cause slower zooming and panning.', - 'Opacity fades the color even if `blend` is left on `false` even if there', - 'is no translucency effect in that case.' - ].join(' ') - }, - blend: { - valType: 'boolean', - dflt: null, - role: 'style', - description: [ - 'Determines if colors are blended together for a translucency effect', - 'in case `opacity` is specified as a value less then `1`.', - 'Setting `blend` to `true` reduces zoom/pan', - 'speed if used with large numbers of points.' - ].join(' ') - }, - sizemin: { - valType: 'number', - min: 0.1, - max: 2, - dflt: 0.5, - role: 'style', - description: [ - 'Sets the minimum size (in px) of the rendered marker points, effective when', - 'the `pointcloud` shows a million or more points.' - ].join(' ') - }, - sizemax: { - valType: 'number', - min: 0.1, - dflt: 20, - role: 'style', - description: [ - 'Sets the maximum size (in px) of the rendered marker points.', - 'Effective when the `pointcloud` shows only few points.' - ].join(' ') - }, - border: { - color: { - valType: 'color', - arrayOk: false, - role: 'style', - description: [ - 'Sets the stroke color. It accepts a specific color.', - 'If the color is not fully opaque and there are hundreds of thousands', - 'of points, it may cause slower zooming and panning.' - ].join(' ') - }, - arearatio: { - valType: 'number', - min: 0, - max: 1, - dflt: 0, - role: 'style', - description: [ - 'Specifies what fraction of the marker area is covered with the', - 'border.' - ].join(' ') - } - } + border: { + color: { + valType: "color", + arrayOk: false, + role: "style", + description: [ + "Sets the stroke color. It accepts a specific color.", + "If the color is not fully opaque and there are hundreds of thousands", + "of points, it may cause slower zooming and panning." + ].join(" ") + }, + arearatio: { + valType: "number", + min: 0, + max: 1, + dflt: 0, + role: "style", + description: [ + "Specifies what fraction of the marker area is covered with the", + "border." + ].join(" ") + } } + } }; diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index 95ac5461cb4..06e131ef437 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -5,226 +5,209 @@ * 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 createPointCloudRenderer = require("gl-pointcloud2d"); -'use strict'; +var str2RGBArray = require("../../lib/str2rgbarray"); +var getTraceColor = require("../scatter/get_trace_color"); -var createPointCloudRenderer = require('gl-pointcloud2d'); - -var str2RGBArray = require('../../lib/str2rgbarray'); -var getTraceColor = require('../scatter/get_trace_color'); - -var AXES = ['xaxis', 'yaxis']; +var AXES = ["xaxis", "yaxis"]; function Pointcloud(scene, uid) { - this.scene = scene; - this.uid = uid; - this.type = 'pointcloud'; - - this.pickXData = []; - this.pickYData = []; - this.xData = []; - this.yData = []; - this.textLabels = []; - this.color = 'rgb(0, 0, 0)'; - this.name = ''; - this.hoverinfo = 'all'; - - this.idToIndex = new Int32Array(0); - this.bounds = [0, 0, 0, 0]; - - this.pointcloudOptions = { - positions: new Float32Array(0), - idToIndex: this.idToIndex, - sizemin: 0.5, - sizemax: 12, - color: [0, 0, 0, 1], - areaRatio: 1, - borderColor: [0, 0, 0, 1] - }; - this.pointcloud = createPointCloudRenderer(scene.glplot, this.pointcloudOptions); - this.pointcloud._trace = this; // scene2d requires this prop + this.scene = scene; + this.uid = uid; + this.type = "pointcloud"; + + this.pickXData = []; + this.pickYData = []; + this.xData = []; + this.yData = []; + this.textLabels = []; + this.color = "rgb(0, 0, 0)"; + this.name = ""; + this.hoverinfo = "all"; + + this.idToIndex = new Int32Array(0); + this.bounds = [0, 0, 0, 0]; + + this.pointcloudOptions = { + positions: new Float32Array(0), + idToIndex: this.idToIndex, + sizemin: 0.5, + sizemax: 12, + color: [0, 0, 0, 1], + areaRatio: 1, + borderColor: [0, 0, 0, 1] + }; + this.pointcloud = createPointCloudRenderer( + scene.glplot, + this.pointcloudOptions + ); + this.pointcloud._trace = this; // scene2d requires this prop } var proto = Pointcloud.prototype; proto.handlePick = function(pickResult) { - - var index = this.idToIndex[pickResult.pointId]; - - // prefer the readout from XY, if present - return { - trace: this, - dataCoord: pickResult.dataCoord, - traceCoord: this.pickXYData ? - [this.pickXYData[index * 2], this.pickXYData[index * 2 + 1]] : - [this.pickXData[index], this.pickYData[index]], - textLabel: Array.isArray(this.textLabels) ? - this.textLabels[index] : - this.textLabels, - color: this.color, - name: this.name, - pointIndex: index, - hoverinfo: this.hoverinfo - }; + var index = this.idToIndex[pickResult.pointId]; + + // prefer the readout from XY, if present + return { + trace: this, + dataCoord: pickResult.dataCoord, + traceCoord: ( + this.pickXYData + ? [this.pickXYData[index * 2], this.pickXYData[index * 2 + 1]] + : [this.pickXData[index], this.pickYData[index]] + ), + textLabel: ( + Array.isArray(this.textLabels) ? this.textLabels[index] : this.textLabels + ), + color: this.color, + name: this.name, + pointIndex: index, + hoverinfo: this.hoverinfo + }; }; proto.update = function(options) { + this.textLabels = options.text; + this.name = options.name; + this.hoverinfo = options.hoverinfo; + this.bounds = [Infinity, Infinity, -Infinity, -Infinity]; - this.textLabels = options.text; - this.name = options.name; - this.hoverinfo = options.hoverinfo; - this.bounds = [Infinity, Infinity, -Infinity, -Infinity]; - - this.updateFast(options); + this.updateFast(options); - this.color = getTraceColor(options, {}); + this.color = getTraceColor(options, {}); }; proto.updateFast = function(options) { - var x = this.xData = this.pickXData = options.x; - var y = this.yData = this.pickYData = options.y; - var xy = this.pickXYData = options.xy; - - var userBounds = options.xbounds && options.ybounds; - var index = options.indices; - - var len, - idToIndex, - positions, - bounds = this.bounds; - - var xx, yy, i; - - if(xy) { - - positions = xy; - - // dividing xy.length by 2 and truncating to integer if xy.length was not even - len = xy.length >>> 1; - - if(userBounds) { - - bounds[0] = options.xbounds[0]; - bounds[2] = options.xbounds[1]; - bounds[1] = options.ybounds[0]; - bounds[3] = options.ybounds[1]; - - } else { + var x = this.xData = this.pickXData = options.x; + var y = this.yData = this.pickYData = options.y; + var xy = this.pickXYData = options.xy; - for(i = 0; i < len; i++) { + var userBounds = options.xbounds && options.ybounds; + var index = options.indices; - xx = positions[i * 2]; - yy = positions[i * 2 + 1]; + var len, idToIndex, positions, bounds = this.bounds; - if(xx < bounds[0]) bounds[0] = xx; - if(xx > bounds[2]) bounds[2] = xx; - if(yy < bounds[1]) bounds[1] = yy; - if(yy > bounds[3]) bounds[3] = yy; - } + var xx, yy, i; - } + if (xy) { + positions = xy; - if(index) { - - idToIndex = index; - - } else { - - idToIndex = new Int32Array(len); - - for(i = 0; i < len; i++) { - - idToIndex[i] = i; - - } - - } + // dividing xy.length by 2 and truncating to integer if xy.length was not even + len = xy.length >>> 1; + if (userBounds) { + bounds[0] = options.xbounds[0]; + bounds[2] = options.xbounds[1]; + bounds[1] = options.ybounds[0]; + bounds[3] = options.ybounds[1]; } else { + for (i = 0; i < len; i++) { + xx = positions[i * 2]; + yy = positions[i * 2 + 1]; + + if (xx < bounds[0]) bounds[0] = xx; + if (xx > bounds[2]) bounds[2] = xx; + if (yy < bounds[1]) bounds[1] = yy; + if (yy > bounds[3]) bounds[3] = yy; + } + } - len = x.length; + if (index) { + idToIndex = index; + } else { + idToIndex = new Int32Array(len); - positions = new Float32Array(2 * len); - idToIndex = new Int32Array(len); + for (i = 0; i < len; i++) { + idToIndex[i] = i; + } + } + } else { + len = x.length; - for(i = 0; i < len; i++) { - xx = x[i]; - yy = y[i]; + positions = new Float32Array(2 * len); + idToIndex = new Int32Array(len); - idToIndex[i] = i; + for (i = 0; i < len; i++) { + xx = x[i]; + yy = y[i]; - positions[i * 2] = xx; - positions[i * 2 + 1] = yy; + idToIndex[i] = i; - if(xx < bounds[0]) bounds[0] = xx; - if(xx > bounds[2]) bounds[2] = xx; - if(yy < bounds[1]) bounds[1] = yy; - if(yy > bounds[3]) bounds[3] = yy; - } + positions[i * 2] = xx; + positions[i * 2 + 1] = yy; + if (xx < bounds[0]) bounds[0] = xx; + if (xx > bounds[2]) bounds[2] = xx; + if (yy < bounds[1]) bounds[1] = yy; + if (yy > bounds[3]) bounds[3] = yy; } + } - this.idToIndex = idToIndex; - this.pointcloudOptions.idToIndex = idToIndex; + this.idToIndex = idToIndex; + this.pointcloudOptions.idToIndex = idToIndex; - this.pointcloudOptions.positions = positions; + this.pointcloudOptions.positions = positions; - var markerColor = str2RGBArray(options.marker.color), - borderColor = str2RGBArray(options.marker.border.color), - opacity = options.opacity * options.marker.opacity; + var markerColor = str2RGBArray(options.marker.color), + borderColor = str2RGBArray(options.marker.border.color), + opacity = options.opacity * options.marker.opacity; - markerColor[3] *= opacity; - this.pointcloudOptions.color = markerColor; + markerColor[3] *= opacity; + this.pointcloudOptions.color = markerColor; - // detect blending from the number of points, if undefined - // because large data with blending hits performance - var blend = options.marker.blend; - if(blend === null) { - var maxPoints = 100; - blend = x.length < maxPoints || y.length < maxPoints; - } - this.pointcloudOptions.blend = blend; + // detect blending from the number of points, if undefined + // because large data with blending hits performance + var blend = options.marker.blend; + if (blend === null) { + var maxPoints = 100; + blend = x.length < maxPoints || y.length < maxPoints; + } + this.pointcloudOptions.blend = blend; - borderColor[3] *= opacity; - this.pointcloudOptions.borderColor = borderColor; + borderColor[3] *= opacity; + this.pointcloudOptions.borderColor = borderColor; - var markerSizeMin = options.marker.sizemin; - var markerSizeMax = Math.max(options.marker.sizemax, options.marker.sizemin); - this.pointcloudOptions.sizeMin = markerSizeMin; - this.pointcloudOptions.sizeMax = markerSizeMax; - this.pointcloudOptions.areaRatio = options.marker.border.arearatio; + var markerSizeMin = options.marker.sizemin; + var markerSizeMax = Math.max(options.marker.sizemax, options.marker.sizemin); + this.pointcloudOptions.sizeMin = markerSizeMin; + this.pointcloudOptions.sizeMax = markerSizeMax; + this.pointcloudOptions.areaRatio = options.marker.border.arearatio; - this.pointcloud.update(this.pointcloudOptions); + this.pointcloud.update(this.pointcloudOptions); - // add item for autorange routine - this.expandAxesFast(bounds, markerSizeMax / 2); // avoid axis reexpand just because of the adaptive point size + // add item for autorange routine + this.expandAxesFast(bounds, markerSizeMax / 2); // avoid axis reexpand just because of the adaptive point size }; proto.expandAxesFast = function(bounds, markerSize) { - var pad = markerSize || 0.5; - var ax, min, max; + var pad = markerSize || 0.5; + var ax, min, max; - for(var i = 0; i < 2; i++) { - ax = this.scene[AXES[i]]; + for (var i = 0; i < 2; i++) { + ax = this.scene[AXES[i]]; - min = ax._min; - if(!min) min = []; - min.push({ val: bounds[i], pad: pad }); + min = ax._min; + if (!min) min = []; + min.push({ val: bounds[i], pad: pad }); - max = ax._max; - if(!max) max = []; - max.push({ val: bounds[i + 2], pad: pad }); - } + max = ax._max; + if (!max) max = []; + max.push({ val: bounds[i + 2], pad: pad }); + } }; proto.dispose = function() { - this.pointcloud.dispose(); + this.pointcloud.dispose(); }; function createPointcloud(scene, data) { - var plot = new Pointcloud(scene, data.uid); - plot.update(data); - return plot; + var plot = new Pointcloud(scene, data.uid); + plot.update(data); + return plot; } module.exports = createPointcloud; diff --git a/src/traces/pointcloud/defaults.js b/src/traces/pointcloud/defaults.js index 16c40747a69..c42e0e3c908 100644 --- a/src/traces/pointcloud/defaults.js +++ b/src/traces/pointcloud/defaults.js @@ -5,39 +5,36 @@ * 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"); - -'use strict'; - -var Lib = require('../../lib'); - -var attributes = require('./attributes'); +var attributes = require("./attributes"); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - coerce('x'); - coerce('y'); - - coerce('xbounds'); - coerce('ybounds'); - - if(traceIn.xy && traceIn.xy instanceof Float32Array) { - traceOut.xy = traceIn.xy; - } - - if(traceIn.indices && traceIn.indices instanceof Int32Array) { - traceOut.indices = traceIn.indices; - } - - coerce('text'); - coerce('marker.color', defaultColor); - coerce('marker.opacity'); - coerce('marker.blend'); - coerce('marker.sizemin'); - coerce('marker.sizemax'); - coerce('marker.border.color', defaultColor); - coerce('marker.border.arearatio'); + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + coerce("x"); + coerce("y"); + + coerce("xbounds"); + coerce("ybounds"); + + if (traceIn.xy && traceIn.xy instanceof Float32Array) { + traceOut.xy = traceIn.xy; + } + + if (traceIn.indices && traceIn.indices instanceof Int32Array) { + traceOut.indices = traceIn.indices; + } + + coerce("text"); + coerce("marker.color", defaultColor); + coerce("marker.opacity"); + coerce("marker.blend"); + coerce("marker.sizemin"); + coerce("marker.sizemax"); + coerce("marker.border.color", defaultColor); + coerce("marker.border.arearatio"); }; diff --git a/src/traces/pointcloud/index.js b/src/traces/pointcloud/index.js index b5cef7bdd2c..0983aa146ab 100644 --- a/src/traces/pointcloud/index.js +++ b/src/traces/pointcloud/index.js @@ -5,27 +5,25 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; var pointcloud = {}; -pointcloud.attributes = require('./attributes'); -pointcloud.supplyDefaults = require('./defaults'); +pointcloud.attributes = require("./attributes"); +pointcloud.supplyDefaults = require("./defaults"); // reuse the Scatter3D 'dummy' calc step so that legends know what to do -pointcloud.calc = require('../scatter3d/calc'); -pointcloud.plot = require('./convert'); +pointcloud.calc = require("../scatter3d/calc"); +pointcloud.plot = require("./convert"); -pointcloud.moduleType = 'trace'; -pointcloud.name = 'pointcloud'; -pointcloud.basePlotModule = require('../../plots/gl2d'); -pointcloud.categories = ['gl2d', 'showLegend']; +pointcloud.moduleType = "trace"; +pointcloud.name = "pointcloud"; +pointcloud.basePlotModule = require("../../plots/gl2d"); +pointcloud.categories = ["gl2d", "showLegend"]; pointcloud.meta = { - description: [ - 'The data visualized as a point cloud set in `x` and `y`', - 'using the WebGl plotting engine.' - ].join(' ') + description: [ + "The data visualized as a point cloud set in `x` and `y`", + "using the WebGl plotting engine." + ].join(" ") }; module.exports = pointcloud; diff --git a/src/traces/scatter/arrays_to_calcdata.js b/src/traces/scatter/arrays_to_calcdata.js index 7cfcf57d2a3..cae597435b5 100644 --- a/src/traces/scatter/arrays_to_calcdata.js +++ b/src/traces/scatter/arrays_to_calcdata.js @@ -5,35 +5,30 @@ * 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'); - +"use strict"; +var Lib = require("../../lib"); // arrayOk attributes, merge them into calcdata array module.exports = function arraysToCalcdata(cd, trace) { - - Lib.mergeArray(trace.text, cd, 'tx'); - Lib.mergeArray(trace.textposition, cd, 'tp'); - if(trace.textfont) { - Lib.mergeArray(trace.textfont.size, cd, 'ts'); - Lib.mergeArray(trace.textfont.color, cd, 'tc'); - Lib.mergeArray(trace.textfont.family, cd, 'tf'); - } - - var marker = trace.marker; - if(marker) { - Lib.mergeArray(marker.size, cd, 'ms'); - Lib.mergeArray(marker.opacity, cd, 'mo'); - Lib.mergeArray(marker.symbol, cd, 'mx'); - Lib.mergeArray(marker.color, cd, 'mc'); - - var markerLine = marker.line; - if(marker.line) { - Lib.mergeArray(markerLine.color, cd, 'mlc'); - Lib.mergeArray(markerLine.width, cd, 'mlw'); - } + Lib.mergeArray(trace.text, cd, "tx"); + Lib.mergeArray(trace.textposition, cd, "tp"); + if (trace.textfont) { + Lib.mergeArray(trace.textfont.size, cd, "ts"); + Lib.mergeArray(trace.textfont.color, cd, "tc"); + Lib.mergeArray(trace.textfont.family, cd, "tf"); + } + + var marker = trace.marker; + if (marker) { + Lib.mergeArray(marker.size, cd, "ms"); + Lib.mergeArray(marker.opacity, cd, "mo"); + Lib.mergeArray(marker.symbol, cd, "mx"); + Lib.mergeArray(marker.color, cd, "mc"); + + var markerLine = marker.line; + if (marker.line) { + Lib.mergeArray(markerLine.color, cd, "mlc"); + Lib.mergeArray(markerLine.width, cd, "mlw"); } + } }; diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 0354006573d..07c1c1385b4 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -5,360 +5,356 @@ * 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 colorAttributes = require("../../components/colorscale/color_attributes"); +var errorBarAttrs = require("../../components/errorbars/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); -'use strict'; - -var colorAttributes = require('../../components/colorscale/color_attributes'); -var errorBarAttrs = require('../../components/errorbars/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); - -var Drawing = require('../../components/drawing'); -var constants = require('./constants'); -var extendFlat = require('../../lib/extend').extendFlat; +var Drawing = require("../../components/drawing"); +var constants = require("./constants"); +var extendFlat = require("../../lib/extend").extendFlat; module.exports = { - x: { - valType: 'data_array', - description: 'Sets the x coordinates.' + x: { valType: "data_array", description: "Sets the x coordinates." }, + x0: { + valType: "any", + dflt: 0, + role: "info", + description: [ + "Alternate to `x`.", + "Builds a linear space of x coordinates.", + "Use with `dx`", + "where `x0` is the starting coordinate and `dx` the step." + ].join(" ") + }, + dx: { + valType: "number", + dflt: 1, + role: "info", + description: [ + "Sets the x coordinate step.", + "See `x0` for more info." + ].join(" ") + }, + y: { valType: "data_array", description: "Sets the y coordinates." }, + y0: { + valType: "any", + dflt: 0, + role: "info", + description: [ + "Alternate to `y`.", + "Builds a linear space of y coordinates.", + "Use with `dy`", + "where `y0` is the starting coordinate and `dy` the step." + ].join(" ") + }, + dy: { + valType: "number", + dflt: 1, + role: "info", + description: [ + "Sets the y coordinate step.", + "See `y0` for more info." + ].join(" ") + }, + ids: { + valType: "data_array", + description: "A list of keys for object constancy of data points during animation" + }, + text: { + valType: "string", + role: "info", + dflt: "", + arrayOk: true, + description: [ + "Sets text elements associated with each (x,y) pair.", + "If a single string, the same string appears over", + "all the data points.", + "If an array of string, the items are mapped in order to the", + "this trace's (x,y) coordinates." + ].join(" ") + }, + mode: { + valType: "flaglist", + flags: ["lines", "markers", "text"], + extras: ["none"], + role: "info", + description: [ + "Determines the drawing mode for this scatter trace.", + "If the provided `mode` includes *text* then the `text` elements", + "appear at the coordinates. Otherwise, the `text` elements", + "appear on hover.", + "If there are less than " + constants.PTS_LINESONLY + " points,", + "then the default is *lines+markers*. Otherwise, *lines*." + ].join(" ") + }, + hoveron: { + valType: "flaglist", + flags: ["points", "fills"], + role: "info", + description: [ + "Do the hover effects highlight individual points (markers or", + "line points) or do they highlight filled regions?", + "If the fill is *toself* or *tonext* and there are no markers", + "or text, then the default is *fills*, otherwise it is *points*." + ].join(" ") + }, + line: { + color: { + valType: "color", + role: "style", + description: "Sets the line color." }, - x0: { - valType: 'any', - dflt: 0, - role: 'info', - description: [ - 'Alternate to `x`.', - 'Builds a linear space of x coordinates.', - 'Use with `dx`', - 'where `x0` is the starting coordinate and `dx` the step.' - ].join(' ') + width: { + valType: "number", + min: 0, + dflt: 2, + role: "style", + description: "Sets the line width (in px)." }, - dx: { - valType: 'number', - dflt: 1, - role: 'info', - description: [ - 'Sets the x coordinate step.', - 'See `x0` for more info.' - ].join(' ') + shape: { + valType: "enumerated", + values: ["linear", "spline", "hv", "vh", "hvh", "vhv"], + dflt: "linear", + role: "style", + description: [ + "Determines the line shape.", + "With *spline* the lines are drawn using spline interpolation.", + "The other available values correspond to step-wise line shapes." + ].join(" ") }, - y: { - valType: 'data_array', - description: 'Sets the y coordinates.' + smoothing: { + valType: "number", + min: 0, + max: 1.3, + dflt: 1, + role: "style", + description: [ + "Has an effect only if `shape` is set to *spline*", + "Sets the amount of smoothing.", + "*0* corresponds to no smoothing (equivalent to a *linear* shape)." + ].join(" ") }, - y0: { - valType: 'any', - dflt: 0, - role: 'info', - description: [ - 'Alternate to `y`.', - 'Builds a linear space of y coordinates.', - 'Use with `dy`', - 'where `y0` is the starting coordinate and `dy` the step.' - ].join(' ') + dash: { + valType: "string", + // string type usually doesn't take values... this one should really be + // a special type or at least a special coercion function, from the GUI + // you only get these values but elsewhere the user can supply a list of + // dash lengths in px, and it will be honored + values: ["solid", "dot", "dash", "longdash", "dashdot", "longdashdot"], + dflt: "solid", + role: "style", + description: [ + "Sets the style of the lines. Set to a dash string type", + "or a dash length in px." + ].join(" ") }, - dy: { - valType: 'number', - dflt: 1, - role: 'info', - description: [ - 'Sets the y coordinate step.', - 'See `y0` for more info.' - ].join(' ') - }, - ids: { - valType: 'data_array', - description: 'A list of keys for object constancy of data points during animation' - }, - text: { - valType: 'string', - role: 'info', - dflt: '', + simplify: { + valType: "boolean", + dflt: true, + role: "info", + description: [ + "Simplifies lines by removing nearly-collinear points. When transitioning", + "lines, it may be desirable to disable this so that the number of points", + "along the resulting SVG path is unaffected." + ].join(" ") + } + }, + connectgaps: { + valType: "boolean", + dflt: false, + role: "info", + description: [ + "Determines whether or not gaps", + "(i.e. {nan} or missing values)", + "in the provided data arrays are connected." + ].join(" ") + }, + fill: { + valType: "enumerated", + values: [ + "none", + "tozeroy", + "tozerox", + "tonexty", + "tonextx", + "toself", + "tonext" + ], + dflt: "none", + role: "style", + description: [ + "Sets the area to fill with a solid color.", + "Use with `fillcolor` if not *none*.", + "*tozerox* and *tozeroy* fill to x=0 and y=0 respectively.", + "*tonextx* and *tonexty* fill between the endpoints of this", + "trace and the endpoints of the trace before it, connecting those", + "endpoints with straight lines (to make a stacked area graph);", + "if there is no trace before it, they behave like *tozerox* and", + "*tozeroy*.", + "*toself* connects the endpoints of the trace (or each segment", + "of the trace if it has gaps) into a closed shape.", + "*tonext* fills the space between two traces if one completely", + "encloses the other (eg consecutive contour lines), and behaves like", + "*toself* if there is no trace before it. *tonext* should not be", + "used if one trace does not enclose the other." + ].join(" ") + }, + fillcolor: { + valType: "color", + role: "style", + description: [ + "Sets the fill color.", + "Defaults to a half-transparent variant of the line color,", + "marker color, or marker line color, whichever is available." + ].join(" ") + }, + marker: extendFlat( + {}, + { + symbol: { + valType: "enumerated", + values: Drawing.symbolList, + dflt: "circle", arrayOk: true, + role: "style", description: [ - 'Sets text elements associated with each (x,y) pair.', - 'If a single string, the same string appears over', - 'all the data points.', - 'If an array of string, the items are mapped in order to the', - 'this trace\'s (x,y) coordinates.' - ].join(' ') - }, - mode: { - valType: 'flaglist', - flags: ['lines', 'markers', 'text'], - extras: ['none'], - role: 'info', + "Sets the marker symbol type.", + "Adding 100 is equivalent to appending *-open* to a symbol name.", + "Adding 200 is equivalent to appending *-dot* to a symbol name.", + "Adding 300 is equivalent to appending *-open-dot*", + "or *dot-open* to a symbol name." + ].join(" ") + }, + opacity: { + valType: "number", + min: 0, + max: 1, + arrayOk: true, + role: "style", + description: "Sets the marker opacity." + }, + size: { + valType: "number", + min: 0, + dflt: 6, + arrayOk: true, + role: "style", + description: "Sets the marker size (in px)." + }, + maxdisplayed: { + valType: "number", + min: 0, + dflt: 0, + role: "style", description: [ - 'Determines the drawing mode for this scatter trace.', - 'If the provided `mode` includes *text* then the `text` elements', - 'appear at the coordinates. Otherwise, the `text` elements', - 'appear on hover.', - 'If there are less than ' + constants.PTS_LINESONLY + ' points,', - 'then the default is *lines+markers*. Otherwise, *lines*.' - ].join(' ') - }, - hoveron: { - valType: 'flaglist', - flags: ['points', 'fills'], - role: 'info', + "Sets a maximum number of points to be drawn on the graph.", + "*0* corresponds to no limit." + ].join(" ") + }, + sizeref: { + valType: "number", + dflt: 1, + role: "style", description: [ - 'Do the hover effects highlight individual points (markers or', - 'line points) or do they highlight filled regions?', - 'If the fill is *toself* or *tonext* and there are no markers', - 'or text, then the default is *fills*, otherwise it is *points*.' - ].join(' ') - }, - line: { - color: { - valType: 'color', - role: 'style', - description: 'Sets the line color.' - }, - width: { - valType: 'number', - min: 0, - dflt: 2, - role: 'style', - description: 'Sets the line width (in px).' - }, - shape: { - valType: 'enumerated', - values: ['linear', 'spline', 'hv', 'vh', 'hvh', 'vhv'], - dflt: 'linear', - role: 'style', - description: [ - 'Determines the line shape.', - 'With *spline* the lines are drawn using spline interpolation.', - 'The other available values correspond to step-wise line shapes.' - ].join(' ') - }, - smoothing: { - valType: 'number', - min: 0, - max: 1.3, - dflt: 1, - role: 'style', - description: [ - 'Has an effect only if `shape` is set to *spline*', - 'Sets the amount of smoothing.', - '*0* corresponds to no smoothing (equivalent to a *linear* shape).' - ].join(' ') - }, - dash: { - valType: 'string', - // string type usually doesn't take values... this one should really be - // a special type or at least a special coercion function, from the GUI - // you only get these values but elsewhere the user can supply a list of - // dash lengths in px, and it will be honored - values: ['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'], - dflt: 'solid', - role: 'style', - description: [ - 'Sets the style of the lines. Set to a dash string type', - 'or a dash length in px.' - ].join(' ') - }, - simplify: { - valType: 'boolean', - dflt: true, - role: 'info', - description: [ - 'Simplifies lines by removing nearly-collinear points. When transitioning', - 'lines, it may be desirable to disable this so that the number of points', - 'along the resulting SVG path is unaffected.' - ].join(' ') - } - }, - connectgaps: { - valType: 'boolean', - dflt: false, - role: 'info', + "Has an effect only if `marker.size` is set to a numerical array.", + "Sets the scale factor used to determine the rendered size of", + "marker points. Use with `sizemin` and `sizemode`." + ].join(" ") + }, + sizemin: { + valType: "number", + min: 0, + dflt: 0, + role: "style", description: [ - 'Determines whether or not gaps', - '(i.e. {nan} or missing values)', - 'in the provided data arrays are connected.' - ].join(' ') - }, - fill: { - valType: 'enumerated', - values: ['none', 'tozeroy', 'tozerox', 'tonexty', 'tonextx', 'toself', 'tonext'], - dflt: 'none', - role: 'style', + "Has an effect only if `marker.size` is set to a numerical array.", + "Sets the minimum size (in px) of the rendered marker points." + ].join(" ") + }, + sizemode: { + valType: "enumerated", + values: ["diameter", "area"], + dflt: "diameter", + role: "info", description: [ - 'Sets the area to fill with a solid color.', - 'Use with `fillcolor` if not *none*.', - '*tozerox* and *tozeroy* fill to x=0 and y=0 respectively.', - '*tonextx* and *tonexty* fill between the endpoints of this', - 'trace and the endpoints of the trace before it, connecting those', - 'endpoints with straight lines (to make a stacked area graph);', - 'if there is no trace before it, they behave like *tozerox* and', - '*tozeroy*.', - '*toself* connects the endpoints of the trace (or each segment', - 'of the trace if it has gaps) into a closed shape.', - '*tonext* fills the space between two traces if one completely', - 'encloses the other (eg consecutive contour lines), and behaves like', - '*toself* if there is no trace before it. *tonext* should not be', - 'used if one trace does not enclose the other.' - ].join(' ') - }, - fillcolor: { - valType: 'color', - role: 'style', + "Has an effect only if `marker.size` is set to a numerical array.", + "Sets the rule for which the data in `size` is converted", + "to pixels." + ].join(" ") + }, + showscale: { + valType: "boolean", + role: "info", + dflt: false, description: [ - 'Sets the fill color.', - 'Defaults to a half-transparent variant of the line color,', - 'marker color, or marker line color, whichever is available.' - ].join(' ') - }, - marker: extendFlat({}, { - symbol: { - valType: 'enumerated', - values: Drawing.symbolList, - dflt: 'circle', - arrayOk: true, - role: 'style', - description: [ - 'Sets the marker symbol type.', - 'Adding 100 is equivalent to appending *-open* to a symbol name.', - 'Adding 200 is equivalent to appending *-dot* to a symbol name.', - 'Adding 300 is equivalent to appending *-open-dot*', - 'or *dot-open* to a symbol name.' - ].join(' ') - }, - opacity: { - valType: 'number', - min: 0, - max: 1, - arrayOk: true, - role: 'style', - description: 'Sets the marker opacity.' - }, - size: { - valType: 'number', + "Has an effect only if `marker.color` is set to a numerical array.", + "Determines whether or not a colorbar is displayed." + ].join(" ") + }, + colorbar: colorbarAttrs, + line: extendFlat( + {}, + { + width: { + valType: "number", min: 0, - dflt: 6, arrayOk: true, - role: 'style', - description: 'Sets the marker size (in px).' - }, - maxdisplayed: { - valType: 'number', - min: 0, - dflt: 0, - role: 'style', - description: [ - 'Sets a maximum number of points to be drawn on the graph.', - '*0* corresponds to no limit.' - ].join(' ') - }, - sizeref: { - valType: 'number', - dflt: 1, - role: 'style', - description: [ - 'Has an effect only if `marker.size` is set to a numerical array.', - 'Sets the scale factor used to determine the rendered size of', - 'marker points. Use with `sizemin` and `sizemode`.' - ].join(' ') - }, - sizemin: { - valType: 'number', - min: 0, - dflt: 0, - role: 'style', - description: [ - 'Has an effect only if `marker.size` is set to a numerical array.', - 'Sets the minimum size (in px) of the rendered marker points.' - ].join(' ') - }, - sizemode: { - valType: 'enumerated', - values: ['diameter', 'area'], - dflt: 'diameter', - role: 'info', - description: [ - 'Has an effect only if `marker.size` is set to a numerical array.', - 'Sets the rule for which the data in `size` is converted', - 'to pixels.' - ].join(' ') - }, - - showscale: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Has an effect only if `marker.color` is set to a numerical array.', - 'Determines whether or not a colorbar is displayed.' - ].join(' ') - }, - colorbar: colorbarAttrs, - - line: extendFlat({}, { - width: { - valType: 'number', - min: 0, - arrayOk: true, - role: 'style', - description: 'Sets the width (in px) of the lines bounding the marker points.' - } + role: "style", + description: "Sets the width (in px) of the lines bounding the marker points." + } }, - colorAttributes('marker.line') - ) + colorAttributes("marker.line") + ) }, - colorAttributes('marker') - ), - textposition: { - valType: 'enumerated', - values: [ - 'top left', 'top center', 'top right', - 'middle left', 'middle center', 'middle right', - 'bottom left', 'bottom center', 'bottom right' - ], - dflt: 'middle center', - arrayOk: true, - role: 'style', - description: [ - 'Sets the positions of the `text` elements', - 'with respects to the (x,y) coordinates.' - ].join(' ') + colorAttributes("marker") + ), + textposition: { + valType: "enumerated", + values: [ + "top left", + "top center", + "top right", + "middle left", + "middle center", + "middle right", + "bottom left", + "bottom center", + "bottom right" + ], + dflt: "middle center", + arrayOk: true, + role: "style", + description: [ + "Sets the positions of the `text` elements", + "with respects to the (x,y) coordinates." + ].join(" ") + }, + textfont: { + family: { + valType: "string", + role: "style", + noBlank: true, + strict: true, + arrayOk: true }, - textfont: { - family: { - valType: 'string', - role: 'style', - noBlank: true, - strict: true, - arrayOk: true - }, - size: { - valType: 'number', - role: 'style', - min: 1, - arrayOk: true - }, - color: { - valType: 'color', - role: 'style', - arrayOk: true - }, - description: 'Sets the text font.' - }, - - r: { - valType: 'data_array', - description: [ - 'For polar chart only.', - 'Sets the radial coordinates.' - ].join('') - }, - t: { - valType: 'data_array', - description: [ - 'For polar chart only.', - 'Sets the angular coordinates.' - ].join('') - }, - - error_y: errorBarAttrs, - error_x: errorBarAttrs + size: { valType: "number", role: "style", min: 1, arrayOk: true }, + color: { valType: "color", role: "style", arrayOk: true }, + description: "Sets the text font." + }, + r: { + valType: "data_array", + description: ["For polar chart only.", "Sets the radial coordinates."].join( + "" + ) + }, + t: { + valType: "data_array", + description: [ + "For polar chart only.", + "Sets the angular coordinates." + ].join("") + }, + error_y: errorBarAttrs, + error_x: errorBarAttrs }; diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js index 1708af0144b..9d9655c159f 100644 --- a/src/traces/scatter/calc.js +++ b/src/traces/scatter/calc.js @@ -5,124 +5,113 @@ * 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 isNumeric = require("fast-isnumeric"); +var Axes = require("../../plots/cartesian/axes"); -'use strict'; +var subTypes = require("./subtypes"); +var calcColorscale = require("./colorscale_calc"); +var arraysToCalcdata = require("./arrays_to_calcdata"); -var isNumeric = require('fast-isnumeric'); +module.exports = function calc(gd, trace) { + var xa = Axes.getFromId(gd, trace.xaxis || "x"), + ya = Axes.getFromId(gd, trace.yaxis || "y"); -var Axes = require('../../plots/cartesian/axes'); + var x = xa.makeCalcdata(trace, "x"), y = ya.makeCalcdata(trace, "y"); -var subTypes = require('./subtypes'); -var calcColorscale = require('./colorscale_calc'); -var arraysToCalcdata = require('./arrays_to_calcdata'); + var serieslen = Math.min(x.length, y.length), marker, s, i; + // cancel minimum tick spacings (only applies to bars and boxes) + xa._minDtick = 0; + ya._minDtick = 0; -module.exports = function calc(gd, trace) { - var xa = Axes.getFromId(gd, trace.xaxis || 'x'), - ya = Axes.getFromId(gd, trace.yaxis || 'y'); - - var x = xa.makeCalcdata(trace, 'x'), - y = ya.makeCalcdata(trace, 'y'); - - var serieslen = Math.min(x.length, y.length), - marker, - s, - i; - - // cancel minimum tick spacings (only applies to bars and boxes) - xa._minDtick = 0; - ya._minDtick = 0; - - if(x.length > serieslen) x.splice(serieslen, x.length - serieslen); - if(y.length > serieslen) y.splice(serieslen, y.length - serieslen); - - // check whether bounds should be tight, padded, extended to zero... - // most cases both should be padded on both ends, so start with that. - var xOptions = {padded: true}, - yOptions = {padded: true}; - - if(subTypes.hasMarkers(trace)) { - - // Treat size like x or y arrays --- Run d2c - // this needs to go before ppad computation - marker = trace.marker; - s = marker.size; - - if(Array.isArray(s)) { - // I tried auto-type but category and dates dont make much sense. - var ax = {type: 'linear'}; - Axes.setConvert(ax); - s = ax.makeCalcdata(trace.marker, 'size'); - if(s.length > serieslen) s.splice(serieslen, s.length - serieslen); - } - - var sizeref = 1.6 * (trace.marker.sizeref || 1), - markerTrans; - if(trace.marker.sizemode === 'area') { - markerTrans = function(v) { - return Math.max(Math.sqrt((v || 0) / sizeref), 3); - }; - } - else { - markerTrans = function(v) { - return Math.max((v || 0) / sizeref, 3); - }; - } - xOptions.ppad = yOptions.ppad = Array.isArray(s) ? - s.map(markerTrans) : markerTrans(s); - } + if (x.length > serieslen) x.splice(serieslen, x.length - serieslen); + if (y.length > serieslen) y.splice(serieslen, y.length - serieslen); - calcColorscale(trace); + // check whether bounds should be tight, padded, extended to zero... + // most cases both should be padded on both ends, so start with that. + var xOptions = { padded: true }, yOptions = { padded: true }; - // TODO: text size + if (subTypes.hasMarkers(trace)) { + // Treat size like x or y arrays --- Run d2c + // this needs to go before ppad computation + marker = trace.marker; + s = marker.size; - // include zero (tight) and extremes (padded) if fill to zero - // (unless the shape is closed, then it's just filling the shape regardless) - if(((trace.fill === 'tozerox') || - ((trace.fill === 'tonextx') && gd.firstscatter)) && - ((x[0] !== x[serieslen - 1]) || (y[0] !== y[serieslen - 1]))) { - xOptions.tozero = true; + if (Array.isArray(s)) { + // I tried auto-type but category and dates dont make much sense. + var ax = { type: "linear" }; + Axes.setConvert(ax); + s = ax.makeCalcdata(trace.marker, "size"); + if (s.length > serieslen) s.splice(serieslen, s.length - serieslen); } - // if no error bars, markers or text, or fill to y=0 remove x padding - else if(!trace.error_y.visible && ( - ['tonexty', 'tozeroy'].indexOf(trace.fill) !== -1 || - (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace)) - )) { - xOptions.padded = false; - xOptions.ppad = 0; + var sizeref = 1.6 * (trace.marker.sizeref || 1), markerTrans; + if (trace.marker.sizemode === "area") { + markerTrans = function(v) { + return Math.max(Math.sqrt((v || 0) / sizeref), 3); + }; + } else { + markerTrans = function(v) { + return Math.max((v || 0) / sizeref, 3); + }; } - - // now check for y - rather different logic, though still mostly padded both ends - // include zero (tight) and extremes (padded) if fill to zero - // (unless the shape is closed, then it's just filling the shape regardless) - if(((trace.fill === 'tozeroy') || ((trace.fill === 'tonexty') && gd.firstscatter)) && - ((x[0] !== x[serieslen - 1]) || (y[0] !== y[serieslen - 1]))) { - yOptions.tozero = true; - } - + xOptions.ppad = yOptions.ppad = Array.isArray(s) + ? s.map(markerTrans) + : markerTrans(s); + } + + calcColorscale(trace); + + // TODO: text size + // include zero (tight) and extremes (padded) if fill to zero + // (unless the shape is closed, then it's just filling the shape regardless) + if ( + (trace.fill === "tozerox" || trace.fill === "tonextx" && gd.firstscatter) && + (x[0] !== x[serieslen - 1] || y[0] !== y[serieslen - 1]) + ) { + xOptions.tozero = true; + } else if ( + !trace.error_y.visible && + (["tonexty", "tozeroy"].indexOf(trace.fill) !== -1 || + !subTypes.hasMarkers(trace) && !subTypes.hasText(trace)) + ) { + // if no error bars, markers or text, or fill to y=0 remove x padding + xOptions.padded = false; + xOptions.ppad = 0; + } + + // now check for y - rather different logic, though still mostly padded both ends + // include zero (tight) and extremes (padded) if fill to zero + // (unless the shape is closed, then it's just filling the shape regardless) + if ( + (trace.fill === "tozeroy" || trace.fill === "tonexty" && gd.firstscatter) && + (x[0] !== x[serieslen - 1] || y[0] !== y[serieslen - 1]) + ) { + yOptions.tozero = true; + } else if (["tonextx", "tozerox"].indexOf(trace.fill) !== -1) { // tight y: any x fill - else if(['tonextx', 'tozerox'].indexOf(trace.fill) !== -1) { - yOptions.padded = false; - } + yOptions.padded = false; + } - Axes.expand(xa, x, xOptions); - Axes.expand(ya, y, yOptions); + Axes.expand(xa, x, xOptions); + Axes.expand(ya, y, yOptions); - // create the "calculated data" to plot - var cd = new Array(serieslen); - for(i = 0; i < serieslen; i++) { - cd[i] = (isNumeric(x[i]) && isNumeric(y[i])) ? - {x: x[i], y: y[i]} : {x: false, y: false}; + // create the "calculated data" to plot + var cd = new Array(serieslen); + for (i = 0; i < serieslen; i++) { + cd[i] = isNumeric(x[i]) && isNumeric(y[i]) + ? { x: x[i], y: y[i] } + : { x: false, y: false }; - if(trace.ids) { - cd[i].id = String(trace.ids[i]); - } + if (trace.ids) { + cd[i].id = String(trace.ids[i]); } + } - arraysToCalcdata(cd, trace); + arraysToCalcdata(cd, trace); - gd.firstscatter = false; - return cd; + gd.firstscatter = false; + return cd; }; diff --git a/src/traces/scatter/clean_data.js b/src/traces/scatter/clean_data.js index 8e18a13fb1e..0d2a46c0658 100644 --- a/src/traces/scatter/clean_data.js +++ b/src/traces/scatter/clean_data.js @@ -5,33 +5,31 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; // remove opacity for any trace that has a fill or is filled to module.exports = function cleanData(fullData) { - for(var i = 0; i < fullData.length; i++) { - var tracei = fullData[i]; - if(tracei.type !== 'scatter') continue; - - var filli = tracei.fill; - if(filli === 'none' || filli === 'toself') continue; - - tracei.opacity = undefined; - - if(filli === 'tonexty' || filli === 'tonextx') { - for(var j = i - 1; j >= 0; j--) { - var tracej = fullData[j]; - - if((tracej.type === 'scatter') && - (tracej.xaxis === tracei.xaxis) && - (tracej.yaxis === tracei.yaxis)) { - tracej.opacity = undefined; - break; - } - } + for (var i = 0; i < fullData.length; i++) { + var tracei = fullData[i]; + if (tracei.type !== "scatter") continue; + + var filli = tracei.fill; + if (filli === "none" || filli === "toself") continue; + + tracei.opacity = undefined; + + if (filli === "tonexty" || filli === "tonextx") { + for (var j = i - 1; j >= 0; j--) { + var tracej = fullData[j]; + + if ( + tracej.type === "scatter" && + tracej.xaxis === tracei.xaxis && + tracej.yaxis === tracei.yaxis + ) { + tracej.opacity = undefined; + break; } + } } + } }; diff --git a/src/traces/scatter/colorbar.js b/src/traces/scatter/colorbar.js index 5a6ce7b52f1..9b63fedc277 100644 --- a/src/traces/scatter/colorbar.js +++ b/src/traces/scatter/colorbar.js @@ -5,51 +5,39 @@ * 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 isNumeric = require("fast-isnumeric"); +var Lib = require("../../lib"); +var Plots = require("../../plots/plots"); +var Colorscale = require("../../components/colorscale"); +var drawColorbar = require("../../components/colorbar/draw"); -'use strict'; +module.exports = function colorbar(gd, cd) { + var trace = cd[0].trace, marker = trace.marker, cbId = "cb" + trace.uid; -var isNumeric = require('fast-isnumeric'); + gd._fullLayout._infolayer.selectAll("." + cbId).remove(); -var Lib = require('../../lib'); -var Plots = require('../../plots/plots'); -var Colorscale = require('../../components/colorscale'); -var drawColorbar = require('../../components/colorbar/draw'); + // TODO unify scatter and heatmap colorbar + // TODO make Colorbar.draw support multiple colorbar per trace + if (marker === undefined || !marker.showscale) { + Plots.autoMargin(gd, cbId); + return; + } + var vals = marker.color, cmin = marker.cmin, cmax = marker.cmax; -module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - marker = trace.marker, - cbId = 'cb' + trace.uid; - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - // TODO unify scatter and heatmap colorbar - // TODO make Colorbar.draw support multiple colorbar per trace - - if((marker === undefined) || !marker.showscale) { - Plots.autoMargin(gd, cbId); - return; - } - - var vals = marker.color, - cmin = marker.cmin, - cmax = marker.cmax; - - if(!isNumeric(cmin)) cmin = Lib.aggNums(Math.min, null, vals); - if(!isNumeric(cmax)) cmax = Lib.aggNums(Math.max, null, vals); - - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - marker.colorscale, - cmin, - cmax - ), - { noNumericCheck: true } - ); - - cb.fillcolor(sclFunc) - .filllevels({start: cmin, end: cmax, size: (cmax - cmin) / 254}) - .options(marker.colorbar)(); + if (!isNumeric(cmin)) cmin = Lib.aggNums(Math.min, null, vals); + if (!isNumeric(cmax)) cmax = Lib.aggNums(Math.max, null, vals); + + var cb = cd[0].t.cb = drawColorbar(gd, cbId); + var sclFunc = Colorscale.makeColorScaleFunc( + Colorscale.extractScale(marker.colorscale, cmin, cmax), + { noNumericCheck: true } + ); + + cb + .fillcolor(sclFunc) + .filllevels({ start: cmin, end: cmax, size: (cmax - cmin) / 254 }) + .options(marker.colorbar)(); }; diff --git a/src/traces/scatter/colorscale_calc.js b/src/traces/scatter/colorscale_calc.js index 27630c8f91b..b9813d1ee24 100644 --- a/src/traces/scatter/colorscale_calc.js +++ b/src/traces/scatter/colorscale_calc.js @@ -5,27 +5,23 @@ * 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 hasColorscale = require("../../components/colorscale/has_colorscale"); +var calcColorscale = require("../../components/colorscale/calc"); - -'use strict'; - -var hasColorscale = require('../../components/colorscale/has_colorscale'); -var calcColorscale = require('../../components/colorscale/calc'); - -var subTypes = require('./subtypes'); - +var subTypes = require("./subtypes"); module.exports = function calcMarkerColorscale(trace) { - if(subTypes.hasLines(trace) && hasColorscale(trace, 'line')) { - calcColorscale(trace, trace.line.color, 'line', 'c'); - } + if (subTypes.hasLines(trace) && hasColorscale(trace, "line")) { + calcColorscale(trace, trace.line.color, "line", "c"); + } - if(subTypes.hasMarkers(trace)) { - if(hasColorscale(trace, 'marker')) { - calcColorscale(trace, trace.marker.color, 'marker', 'c'); - } - if(hasColorscale(trace, 'marker.line')) { - calcColorscale(trace, trace.marker.line.color, 'marker.line', 'c'); - } + if (subTypes.hasMarkers(trace)) { + if (hasColorscale(trace, "marker")) { + calcColorscale(trace, trace.marker.color, "marker", "c"); + } + if (hasColorscale(trace, "marker.line")) { + calcColorscale(trace, trace.marker.line.color, "marker.line", "c"); } + } }; diff --git a/src/traces/scatter/constants.js b/src/traces/scatter/constants.js index 66eb332f109..e364c36e084 100644 --- a/src/traces/scatter/constants.js +++ b/src/traces/scatter/constants.js @@ -5,10 +5,5 @@ * 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 = { - PTS_LINESONLY: 20 -}; +"use strict"; +module.exports = { PTS_LINESONLY: 20 }; diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index 6b71dbd4f6f..72544b8c159 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -5,74 +5,80 @@ * 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 constants = require('./constants'); -var subTypes = require('./subtypes'); -var handleXYDefaults = require('./xy_defaults'); -var handleMarkerDefaults = require('./marker_defaults'); -var handleLineDefaults = require('./line_defaults'); -var handleLineShapeDefaults = require('./line_shape_defaults'); -var handleTextDefaults = require('./text_defaults'); -var handleFillColorDefaults = require('./fillcolor_defaults'); -var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); +"use strict"; +var Lib = require("../../lib"); + +var attributes = require("./attributes"); +var constants = require("./constants"); +var subTypes = require("./subtypes"); +var handleXYDefaults = require("./xy_defaults"); +var handleMarkerDefaults = require("./marker_defaults"); +var handleLineDefaults = require("./line_defaults"); +var handleLineShapeDefaults = require("./line_shape_defaults"); +var handleTextDefaults = require("./text_defaults"); +var handleFillColorDefaults = require("./fillcolor_defaults"); +var errorBarsSupplyDefaults = require("../../components/errorbars/defaults"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var len = handleXYDefaults(traceIn, traceOut, layout, coerce), + // TODO: default mode by orphan points... + defaultMode = len < constants.PTS_LINESONLY ? "lines+markers" : "lines"; + if (!len) { + traceOut.visible = false; + return; + } + + coerce("text"); + coerce("mode", defaultMode); + coerce("ids"); + + if (subTypes.hasLines(traceOut)) { + handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); + handleLineShapeDefaults(traceIn, traceOut, coerce); + coerce("connectgaps"); + coerce("line.simplify"); + } + + if (subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + if (subTypes.hasText(traceOut)) { + handleTextDefaults(traceIn, traceOut, layout, coerce); + } + + var dfltHoverOn = []; + + if (subTypes.hasMarkers(traceOut) || subTypes.hasText(traceOut)) { + coerce("marker.maxdisplayed"); + dfltHoverOn.push("points"); + } + + coerce("fill"); + if (traceOut.fill !== "none") { + handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); + if (!subTypes.hasLines(traceOut)) { + handleLineShapeDefaults(traceIn, traceOut, coerce); } - - var len = handleXYDefaults(traceIn, traceOut, layout, coerce), - // TODO: default mode by orphan points... - defaultMode = len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines'; - if(!len) { - traceOut.visible = false; - return; - } - - coerce('text'); - coerce('mode', defaultMode); - coerce('ids'); - - if(subTypes.hasLines(traceOut)) { - handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); - handleLineShapeDefaults(traceIn, traceOut, coerce); - coerce('connectgaps'); - coerce('line.simplify'); - } - - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); - } - - if(subTypes.hasText(traceOut)) { - handleTextDefaults(traceIn, traceOut, layout, coerce); - } - - var dfltHoverOn = []; - - if(subTypes.hasMarkers(traceOut) || subTypes.hasText(traceOut)) { - coerce('marker.maxdisplayed'); - dfltHoverOn.push('points'); - } - - coerce('fill'); - if(traceOut.fill !== 'none') { - handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); - if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce); - } - - if(traceOut.fill === 'tonext' || traceOut.fill === 'toself') { - dfltHoverOn.push('fills'); - } - coerce('hoveron', dfltHoverOn.join('+') || 'points'); - - errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); - errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); + } + + if (traceOut.fill === "tonext" || traceOut.fill === "toself") { + dfltHoverOn.push("fills"); + } + coerce("hoveron", dfltHoverOn.join("+") || "points"); + + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, { axis: "y" }); + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, { + axis: "x", + inherit: "y" + }); }; diff --git a/src/traces/scatter/fillcolor_defaults.js b/src/traces/scatter/fillcolor_defaults.js index b53fcb8e93d..12a5f535e87 100644 --- a/src/traces/scatter/fillcolor_defaults.js +++ b/src/traces/scatter/fillcolor_defaults.js @@ -5,32 +5,34 @@ * 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 Color = require('../../components/color'); - - -module.exports = function fillColorDefaults(traceIn, traceOut, defaultColor, coerce) { - var inheritColorFromMarker = false; - - if(traceOut.marker) { - // don't try to inherit a color array - var markerColor = traceOut.marker.color, - markerLineColor = (traceOut.marker.line || {}).color; - - if(markerColor && !Array.isArray(markerColor)) { - inheritColorFromMarker = markerColor; - } - else if(markerLineColor && !Array.isArray(markerLineColor)) { - inheritColorFromMarker = markerLineColor; - } +"use strict"; +var Color = require("../../components/color"); + +module.exports = function fillColorDefaults( + traceIn, + traceOut, + defaultColor, + coerce +) { + var inheritColorFromMarker = false; + + if (traceOut.marker) { + // don't try to inherit a color array + var markerColor = traceOut.marker.color, + markerLineColor = (traceOut.marker.line || {}).color; + + if (markerColor && !Array.isArray(markerColor)) { + inheritColorFromMarker = markerColor; + } else if (markerLineColor && !Array.isArray(markerLineColor)) { + inheritColorFromMarker = markerLineColor; } - - coerce('fillcolor', Color.addOpacity( - (traceOut.line || {}).color || - inheritColorFromMarker || - defaultColor, 0.5 - )); + } + + coerce( + "fillcolor", + Color.addOpacity( + (traceOut.line || {}).color || inheritColorFromMarker || defaultColor, + 0.5 + ) + ); }; diff --git a/src/traces/scatter/get_trace_color.js b/src/traces/scatter/get_trace_color.js index cbf0708217c..286fed0223b 100644 --- a/src/traces/scatter/get_trace_color.js +++ b/src/traces/scatter/get_trace_color.js @@ -5,47 +5,46 @@ * 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 Color = require('../../components/color'); -var subtypes = require('./subtypes'); - +"use strict"; +var Color = require("../../components/color"); +var subtypes = require("./subtypes"); module.exports = function getTraceColor(trace, di) { - var lc, tc; - - // TODO: text modes - - if(trace.mode === 'lines') { - lc = trace.line.color; - return (lc && Color.opacity(lc)) ? - lc : trace.fillcolor; - } - else if(trace.mode === 'none') { - return trace.fill ? trace.fillcolor : ''; - } - else { - var mc = di.mcc || (trace.marker || {}).color, - mlc = di.mlcc || ((trace.marker || {}).line || {}).color; - - tc = (mc && Color.opacity(mc)) ? mc : - (mlc && Color.opacity(mlc) && - (di.mlw || ((trace.marker || {}).line || {}).width)) ? mlc : ''; - - if(tc) { - // make sure the points aren't TOO transparent - if(Color.opacity(tc) < 0.3) { - return Color.addOpacity(tc, 0.3); - } - else return tc; - } - else { - lc = (trace.line || {}).color; - return (lc && Color.opacity(lc) && - subtypes.hasLines(trace) && trace.line.width) ? - lc : trace.fillcolor; - } + var lc, tc; + + // TODO: text modes + if (trace.mode === "lines") { + lc = trace.line.color; + return lc && Color.opacity(lc) ? lc : trace.fillcolor; + } else if (trace.mode === "none") { + return trace.fill ? trace.fillcolor : ""; + } else { + var mc = di.mcc || (trace.marker || {}).color, + mlc = di.mlcc || ((trace.marker || {}).line || {}).color; + + tc = mc && Color.opacity(mc) + ? mc + : mlc && + Color.opacity(mlc) && + (di.mlw || ((trace.marker || {}).line || {}).width) + ? mlc + : ""; + + if (tc) { + // make sure the points aren't TOO transparent + if (Color.opacity(tc) < 0.3) { + return Color.addOpacity(tc, 0.3); + } else { + return tc; + } + } else { + lc = (trace.line || {}).color; + return lc && + Color.opacity(lc) && + subtypes.hasLines(trace) && + trace.line.width + ? lc + : trace.fillcolor; } + } }; diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js index 2392a23bb4f..9a239c35dc7 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -5,163 +5,168 @@ * 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 Fx = require('../../plots/cartesian/graph_interact'); -var constants = require('../../plots/cartesian/constants'); -var ErrorBars = require('../../components/errorbars'); -var getTraceColor = require('./get_trace_color'); -var Color = require('../../components/color'); - +"use strict"; +var Lib = require("../../lib"); +var Fx = require("../../plots/cartesian/graph_interact"); +var constants = require("../../plots/cartesian/constants"); +var ErrorBars = require("../../components/errorbars"); +var getTraceColor = require("./get_trace_color"); +var Color = require("../../components/color"); module.exports = function hoverPoints(pointData, xval, yval, hovermode) { - var cd = pointData.cd, - trace = cd[0].trace, - xa = pointData.xa, - ya = pointData.ya, - xpx = xa.c2p(xval), - ypx = ya.c2p(yval), - pt = [xpx, ypx]; - - // look for points to hover on first, then take fills only if we - // didn't find a point - if(trace.hoveron.indexOf('points') !== -1) { - var dx = function(di) { - // scatter points: d.mrc is the calculated marker radius - // adjust the distance so if you're inside the marker it - // always will show up regardless of point size, but - // prioritize smaller points - var rad = Math.max(3, di.mrc || 0); - return Math.max(Math.abs(xa.c2p(di.x) - xpx) - rad, 1 - 3 / rad); - }, - dy = function(di) { - var rad = Math.max(3, di.mrc || 0); - return Math.max(Math.abs(ya.c2p(di.y) - ypx) - rad, 1 - 3 / rad); - }, - dxy = function(di) { - var rad = Math.max(3, di.mrc || 0), - dx = xa.c2p(di.x) - xpx, - dy = ya.c2p(di.y) - ypx; - return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad); - }, - distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); - - Fx.getClosest(cd, distfn, pointData); - - // skip the rest (for this trace) if we didn't find a close point - if(pointData.index !== false) { - - // the closest data point - var di = cd[pointData.index], - xc = xa.c2p(di.x, true), - yc = ya.c2p(di.y, true), - rad = di.mrc || 1; - - Lib.extendFlat(pointData, { - color: getTraceColor(trace, di), - - x0: xc - rad, - x1: xc + rad, - xLabelVal: di.x, - - y0: yc - rad, - y1: yc + rad, - yLabelVal: di.y - }); - - if(di.tx) pointData.text = di.tx; - else if(trace.text) pointData.text = trace.text; - - ErrorBars.hoverInfo(di, trace, pointData); - - return [pointData]; - } + var cd = pointData.cd, + trace = cd[0].trace, + xa = pointData.xa, + ya = pointData.ya, + xpx = xa.c2p(xval), + ypx = ya.c2p(yval), + pt = [xpx, ypx]; + + // look for points to hover on first, then take fills only if we + // didn't find a point + if (trace.hoveron.indexOf("points") !== -1) { + var dx = function(di) { + // scatter points: d.mrc is the calculated marker radius + // adjust the distance so if you're inside the marker it + // always will show up regardless of point size, but + // prioritize smaller points + var rad = Math.max(3, di.mrc || 0); + return Math.max(Math.abs(xa.c2p(di.x) - xpx) - rad, 1 - 3 / rad); + }, + dy = function(di) { + var rad = Math.max(3, di.mrc || 0); + return Math.max(Math.abs(ya.c2p(di.y) - ypx) - rad, 1 - 3 / rad); + }, + dxy = function(di) { + var rad = Math.max(3, di.mrc || 0), + dx = xa.c2p(di.x) - xpx, + dy = ya.c2p(di.y) - ypx; + return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad); + }, + distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); + + Fx.getClosest(cd, distfn, pointData); + + // skip the rest (for this trace) if we didn't find a close point + if (pointData.index !== false) { + // the closest data point + var di = cd[pointData.index], + xc = xa.c2p(di.x, true), + yc = ya.c2p(di.y, true), + rad = di.mrc || 1; + + Lib.extendFlat(pointData, { + color: getTraceColor(trace, di), + x0: xc - rad, + x1: xc + rad, + xLabelVal: di.x, + y0: yc - rad, + y1: yc + rad, + yLabelVal: di.y + }); + + if (di.tx) pointData.text = di.tx; + else if (trace.text) pointData.text = trace.text; + + ErrorBars.hoverInfo(di, trace, pointData); + + return [pointData]; + } + } + + // even if hoveron is 'fills', only use it if we have polygons too + if (trace.hoveron.indexOf("fills") !== -1 && trace._polygons) { + var polygons = trace._polygons, + polygonsIn = [], + inside = false, + xmin = Infinity, + xmax = -Infinity, + ymin = Infinity, + ymax = -Infinity, + i, + j, + polygon, + pts, + xCross, + x0, + x1, + y0, + y1; + + for (i = 0; i < polygons.length; i++) { + polygon = polygons[i]; + // TODO: this is not going to work right for curved edges, it will + // act as though they're straight. That's probably going to need + // the elements themselves to capture the events. Worth it? + if (polygon.contains(pt)) { + inside = !inside; + // TODO: need better than just the overall bounding box + polygonsIn.push(polygon); + ymin = Math.min(ymin, polygon.ymin); + ymax = Math.max(ymax, polygon.ymax); + } } - // even if hoveron is 'fills', only use it if we have polygons too - if(trace.hoveron.indexOf('fills') !== -1 && trace._polygons) { - var polygons = trace._polygons, - polygonsIn = [], - inside = false, - xmin = Infinity, - xmax = -Infinity, - ymin = Infinity, - ymax = -Infinity, - i, j, polygon, pts, xCross, x0, x1, y0, y1; - - for(i = 0; i < polygons.length; i++) { - polygon = polygons[i]; - // TODO: this is not going to work right for curved edges, it will - // act as though they're straight. That's probably going to need - // the elements themselves to capture the events. Worth it? - if(polygon.contains(pt)) { - inside = !inside; - // TODO: need better than just the overall bounding box - polygonsIn.push(polygon); - ymin = Math.min(ymin, polygon.ymin); - ymax = Math.max(ymax, polygon.ymax); - } - } - - if(inside) { - // constrain ymin/max to the visible plot, so the label goes - // at the middle of the piece you can see - ymin = Math.max(ymin, 0); - ymax = Math.min(ymax, ya._length); - - // find the overall left-most and right-most points of the - // polygon(s) we're inside at their combined vertical midpoint. - // This is where we will draw the hover label. - // Note that this might not be the vertical midpoint of the - // whole trace, if it's disjoint. - var yAvg = (ymin + ymax) / 2; - for(i = 0; i < polygonsIn.length; i++) { - pts = polygonsIn[i].pts; - for(j = 1; j < pts.length; j++) { - y0 = pts[j - 1][1]; - y1 = pts[j][1]; - if((y0 > yAvg) !== (y1 >= yAvg)) { - x0 = pts[j - 1][0]; - x1 = pts[j][0]; - xCross = x0 + (x1 - x0) * (yAvg - y0) / (y1 - y0); - xmin = Math.min(xmin, xCross); - xmax = Math.max(xmax, xCross); - } - } - } - - // constrain xmin/max to the visible plot now too - xmin = Math.max(xmin, 0); - xmax = Math.min(xmax, xa._length); - - // get only fill or line color for the hover color - var color = Color.defaultLine; - if(Color.opacity(trace.fillcolor)) color = trace.fillcolor; - else if(Color.opacity((trace.line || {}).color)) { - color = trace.line.color; - } - - Lib.extendFlat(pointData, { - // never let a 2D override 1D type as closest point - distance: constants.MAXDIST + 10, - x0: xmin, - x1: xmax, - y0: yAvg, - y1: yAvg, - color: color - }); - - delete pointData.index; - - if(trace.text && !Array.isArray(trace.text)) { - pointData.text = String(trace.text); - } - else pointData.text = trace.name; - - return [pointData]; + if (inside) { + // constrain ymin/max to the visible plot, so the label goes + // at the middle of the piece you can see + ymin = Math.max(ymin, 0); + ymax = Math.min(ymax, ya._length); + + // find the overall left-most and right-most points of the + // polygon(s) we're inside at their combined vertical midpoint. + // This is where we will draw the hover label. + // Note that this might not be the vertical midpoint of the + // whole trace, if it's disjoint. + var yAvg = (ymin + ymax) / 2; + for (i = 0; i < polygonsIn.length; i++) { + pts = polygonsIn[i].pts; + for (j = 1; j < pts.length; j++) { + y0 = pts[j - 1][1]; + y1 = pts[j][1]; + if (y0 > yAvg !== y1 >= yAvg) { + x0 = pts[j - 1][0]; + x1 = pts[j][0]; + xCross = x0 + (x1 - x0) * (yAvg - y0) / (y1 - y0); + xmin = Math.min(xmin, xCross); + xmax = Math.max(xmax, xCross); + } } + } + + // constrain xmin/max to the visible plot now too + xmin = Math.max(xmin, 0); + xmax = Math.min(xmax, xa._length); + + // get only fill or line color for the hover color + var color = Color.defaultLine; + if (Color.opacity(trace.fillcolor)) { + color = trace.fillcolor; + } else if (Color.opacity((trace.line || {}).color)) { + color = trace.line.color; + } + + Lib.extendFlat(pointData, { + // never let a 2D override 1D type as closest point + distance: ( + constants.MAXDIST + 10 + ), + x0: xmin, + x1: xmax, + y0: yAvg, + y1: yAvg, + color: color + }); + + delete pointData.index; + + if (trace.text && !Array.isArray(trace.text)) { + pointData.text = String(trace.text); + } else { + pointData.text = trace.name; + } + + return [pointData]; } + } }; diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js index 197b540e8d2..780540ffe0b 100644 --- a/src/traces/scatter/index.js +++ b/src/traces/scatter/index.js @@ -5,13 +5,10 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var Scatter = {}; -var subtypes = require('./subtypes'); +var subtypes = require("./subtypes"); Scatter.hasLines = subtypes.hasLines; Scatter.hasMarkers = subtypes.hasMarkers; Scatter.hasText = subtypes.hasText; @@ -19,30 +16,36 @@ Scatter.isBubble = subtypes.isBubble; // traces with < this many points are by default shown // with points and lines, > just get lines -Scatter.attributes = require('./attributes'); -Scatter.supplyDefaults = require('./defaults'); -Scatter.cleanData = require('./clean_data'); -Scatter.calc = require('./calc'); -Scatter.arraysToCalcdata = require('./arrays_to_calcdata'); -Scatter.plot = require('./plot'); -Scatter.colorbar = require('./colorbar'); -Scatter.style = require('./style'); -Scatter.hoverPoints = require('./hover'); -Scatter.selectPoints = require('./select'); +Scatter.attributes = require("./attributes"); +Scatter.supplyDefaults = require("./defaults"); +Scatter.cleanData = require("./clean_data"); +Scatter.calc = require("./calc"); +Scatter.arraysToCalcdata = require("./arrays_to_calcdata"); +Scatter.plot = require("./plot"); +Scatter.colorbar = require("./colorbar"); +Scatter.style = require("./style"); +Scatter.hoverPoints = require("./hover"); +Scatter.selectPoints = require("./select"); Scatter.animatable = true; -Scatter.moduleType = 'trace'; -Scatter.name = 'scatter'; -Scatter.basePlotModule = require('../../plots/cartesian'); -Scatter.categories = ['cartesian', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend']; +Scatter.moduleType = "trace"; +Scatter.name = "scatter"; +Scatter.basePlotModule = require("../../plots/cartesian"); +Scatter.categories = [ + "cartesian", + "symbols", + "markerColorscale", + "errorBarsOK", + "showLegend" +]; Scatter.meta = { - description: [ - 'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.', - 'The data visualized as scatter point or lines is set in `x` and `y`.', - 'Text (appearing either on the chart or on hover only) is via `text`.', - 'Bubble charts are achieved by setting `marker.size` and/or `marker.color`', - 'to numerical arrays.' - ].join(' ') + description: [ + "The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.", + "The data visualized as scatter point or lines is set in `x` and `y`.", + "Text (appearing either on the chart or on hover only) is via `text`.", + "Bubble charts are achieved by setting `marker.size` and/or `marker.color`", + "to numerical arrays." + ].join(" ") }; module.exports = Scatter; diff --git a/src/traces/scatter/line_defaults.js b/src/traces/scatter/line_defaults.js index f0fc1660492..eedcfd8d016 100644 --- a/src/traces/scatter/line_defaults.js +++ b/src/traces/scatter/line_defaults.js @@ -5,27 +5,32 @@ * 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 hasColorscale = require('../../components/colorscale/has_colorscale'); -var colorscaleDefaults = require('../../components/colorscale/defaults'); - - -module.exports = function lineDefaults(traceIn, traceOut, defaultColor, layout, coerce) { - var markerColor = (traceIn.marker || {}).color; - - coerce('line.color', defaultColor); - - if(hasColorscale(traceIn, 'line')) { - colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'line.', cLetter: 'c'}); - } - else { - var lineColorDflt = (Array.isArray(markerColor) ? false : markerColor) || defaultColor; - coerce('line.color', lineColorDflt); - } - - coerce('line.width'); - coerce('line.dash'); +"use strict"; +var hasColorscale = require("../../components/colorscale/has_colorscale"); +var colorscaleDefaults = require("../../components/colorscale/defaults"); + +module.exports = function lineDefaults( + traceIn, + traceOut, + defaultColor, + layout, + coerce +) { + var markerColor = (traceIn.marker || {}).color; + + coerce("line.color", defaultColor); + + if (hasColorscale(traceIn, "line")) { + colorscaleDefaults(traceIn, traceOut, layout, coerce, { + prefix: "line.", + cLetter: "c" + }); + } else { + var lineColorDflt = (Array.isArray(markerColor) ? false : markerColor) || + defaultColor; + coerce("line.color", lineColorDflt); + } + + coerce("line.width"); + coerce("line.dash"); }; diff --git a/src/traces/scatter/line_points.js b/src/traces/scatter/line_points.js index 03b77be8dfa..c0c23c5cf8c 100644 --- a/src/traces/scatter/line_points.js +++ b/src/traces/scatter/line_points.js @@ -5,167 +5,167 @@ * 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 BADNUM = require('../../constants/numerical').BADNUM; - +"use strict"; +var BADNUM = require("../../constants/numerical").BADNUM; module.exports = function linePoints(d, opts) { - var xa = opts.xaxis, - ya = opts.yaxis, - simplify = opts.simplify, - connectGaps = opts.connectGaps, - baseTolerance = opts.baseTolerance, - linear = opts.linear, - segments = [], - minTolerance = 0.2, // fraction of tolerance "so close we don't even consider it a new point" - pts = new Array(d.length), - pti = 0, - i, - - // pt variables are pixel coordinates [x,y] of one point - clusterStartPt, // these four are the outputs of clustering on a line - clusterEndPt, - clusterHighPt, - clusterLowPt, - thisPt, // "this" is the next point we're considering adding to the cluster - - clusterRefDist, - clusterHighFirst, // did we encounter the high point first, then a low point, or vice versa? - clusterUnitVector, // the first two points in the cluster determine its unit vector - // so the second is always in the "High" direction - thisVector, // the pixel delta from clusterStartPt - - // val variables are (signed) pixel distances along the cluster vector - clusterHighVal, - clusterLowVal, - thisVal, - - // deviation variables are (signed) pixel distances normal to the cluster vector - clusterMinDeviation, - clusterMaxDeviation, - thisDeviation; - - if(!simplify) { - baseTolerance = minTolerance = -1; - } - - // turn one calcdata point into pixel coordinates - function getPt(index) { - var x = xa.c2p(d[index].x), - y = ya.c2p(d[index].y); - if(x === BADNUM || y === BADNUM) return false; - return [x, y]; - } - - // if we're off-screen, increase tolerance over baseTolerance - function getTolerance(pt) { - var xFrac = pt[0] / xa._length, - yFrac = pt[1] / ya._length; - return (1 + 10 * Math.max(0, -xFrac, xFrac - 1, -yFrac, yFrac - 1)) * baseTolerance; - } - - function ptDist(pt1, pt2) { - var dx = pt1[0] - pt2[0], - dy = pt1[1] - pt2[1]; - return Math.sqrt(dx * dx + dy * dy); - } - - // loop over ALL points in this trace - for(i = 0; i < d.length; i++) { - clusterStartPt = getPt(i); - if(!clusterStartPt) continue; - - pti = 0; - pts[pti++] = clusterStartPt; - - // loop over one segment of the trace - for(i++; i < d.length; i++) { - clusterHighPt = getPt(i); - if(!clusterHighPt) { - if(connectGaps) continue; - else break; - } - - // can't decimate if nonlinear line shape - // TODO: we *could* decimate [hv]{2,3} shapes if we restricted clusters to horz or vert again - // but spline would be verrry awkward to decimate - if(!linear) { - pts[pti++] = clusterHighPt; - continue; - } - - clusterRefDist = ptDist(clusterHighPt, clusterStartPt); - - if(clusterRefDist < getTolerance(clusterHighPt) * minTolerance) continue; - - clusterUnitVector = [ - (clusterHighPt[0] - clusterStartPt[0]) / clusterRefDist, - (clusterHighPt[1] - clusterStartPt[1]) / clusterRefDist - ]; - - clusterLowPt = clusterStartPt; - clusterHighVal = clusterRefDist; - clusterLowVal = clusterMinDeviation = clusterMaxDeviation = 0; - clusterHighFirst = false; - clusterEndPt = clusterHighPt; - - // loop over one cluster of points that collapse onto one line - for(i++; i < d.length; i++) { - thisPt = getPt(i); - if(!thisPt) { - if(connectGaps) continue; - else break; - } - thisVector = [ - thisPt[0] - clusterStartPt[0], - thisPt[1] - clusterStartPt[1] - ]; - // cross product (or dot with normal to the cluster vector) - thisDeviation = thisVector[0] * clusterUnitVector[1] - thisVector[1] * clusterUnitVector[0]; - clusterMinDeviation = Math.min(clusterMinDeviation, thisDeviation); - clusterMaxDeviation = Math.max(clusterMaxDeviation, thisDeviation); - - if(clusterMaxDeviation - clusterMinDeviation > getTolerance(thisPt)) break; - - clusterEndPt = thisPt; - thisVal = thisVector[0] * clusterUnitVector[0] + thisVector[1] * clusterUnitVector[1]; - - if(thisVal > clusterHighVal) { - clusterHighVal = thisVal; - clusterHighPt = thisPt; - clusterHighFirst = false; - } else if(thisVal < clusterLowVal) { - clusterLowVal = thisVal; - clusterLowPt = thisPt; - clusterHighFirst = true; - } - } - - // insert this cluster into pts - // we've already inserted the start pt, now check if we have high and low pts - if(clusterHighFirst) { - pts[pti++] = clusterHighPt; - if(clusterEndPt !== clusterLowPt) pts[pti++] = clusterLowPt; - } else { - if(clusterLowPt !== clusterStartPt) pts[pti++] = clusterLowPt; - if(clusterEndPt !== clusterHighPt) pts[pti++] = clusterHighPt; - } - // and finally insert the end pt - pts[pti++] = clusterEndPt; - - // have we reached the end of this segment? - if(i >= d.length || !thisPt) break; - - // otherwise we have an out-of-cluster point to insert as next clusterStartPt - pts[pti++] = thisPt; - clusterStartPt = thisPt; + var xa = opts.xaxis, + ya = opts.yaxis, + simplify = opts.simplify, + connectGaps = opts.connectGaps, + baseTolerance = opts.baseTolerance, + linear = opts.linear, + segments = [], + minTolerance = 0.2, + // fraction of tolerance "so close we don't even consider it a new point" + pts = new Array(d.length), + pti = 0, + i, + // pt variables are pixel coordinates [x,y] of one point + clusterStartPt, + // these four are the outputs of clustering on a line + clusterEndPt, + clusterHighPt, + clusterLowPt, + thisPt, + // "this" is the next point we're considering adding to the cluster + clusterRefDist, + clusterHighFirst, + // did we encounter the high point first, then a low point, or vice versa? + clusterUnitVector, + // the first two points in the cluster determine its unit vector + // so the second is always in the "High" direction + thisVector, + // the pixel delta from clusterStartPt + // val variables are (signed) pixel distances along the cluster vector + clusterHighVal, + clusterLowVal, + thisVal, + // deviation variables are (signed) pixel distances normal to the cluster vector + clusterMinDeviation, + clusterMaxDeviation, + thisDeviation; + + if (!simplify) { + baseTolerance = minTolerance = -1; + } + + // turn one calcdata point into pixel coordinates + function getPt(index) { + var x = xa.c2p(d[index].x), y = ya.c2p(d[index].y); + if (x === BADNUM || y === BADNUM) return false; + return [x, y]; + } + + // if we're off-screen, increase tolerance over baseTolerance + function getTolerance(pt) { + var xFrac = pt[0] / xa._length, yFrac = pt[1] / ya._length; + return (1 + 10 * Math.max(0, -xFrac, xFrac - 1, -yFrac, yFrac - 1)) * + baseTolerance; + } + + function ptDist(pt1, pt2) { + var dx = pt1[0] - pt2[0], dy = pt1[1] - pt2[1]; + return Math.sqrt(dx * dx + dy * dy); + } + + // loop over ALL points in this trace + for (i = 0; i < d.length; i++) { + clusterStartPt = getPt(i); + if (!clusterStartPt) continue; + + pti = 0; + pts[pti++] = clusterStartPt; + + // loop over one segment of the trace + for (i++; i < d.length; i++) { + clusterHighPt = getPt(i); + if (!clusterHighPt) { + if (connectGaps) continue; + else break; + } + + // can't decimate if nonlinear line shape + // TODO: we *could* decimate [hv]{2,3} shapes if we restricted clusters to horz or vert again + // but spline would be verrry awkward to decimate + if (!linear) { + pts[pti++] = clusterHighPt; + continue; + } + + clusterRefDist = ptDist(clusterHighPt, clusterStartPt); + + if (clusterRefDist < getTolerance(clusterHighPt) * minTolerance) continue; + + clusterUnitVector = [ + (clusterHighPt[0] - clusterStartPt[0]) / clusterRefDist, + (clusterHighPt[1] - clusterStartPt[1]) / clusterRefDist + ]; + + clusterLowPt = clusterStartPt; + clusterHighVal = clusterRefDist; + clusterLowVal = clusterMinDeviation = clusterMaxDeviation = 0; + clusterHighFirst = false; + clusterEndPt = clusterHighPt; + + // loop over one cluster of points that collapse onto one line + for (i++; i < d.length; i++) { + thisPt = getPt(i); + if (!thisPt) { + if (connectGaps) continue; + else break; + } + thisVector = [ + thisPt[0] - clusterStartPt[0], + thisPt[1] - clusterStartPt[1] + ]; + // cross product (or dot with normal to the cluster vector) + thisDeviation = thisVector[0] * clusterUnitVector[1] - + thisVector[1] * clusterUnitVector[0]; + clusterMinDeviation = Math.min(clusterMinDeviation, thisDeviation); + clusterMaxDeviation = Math.max(clusterMaxDeviation, thisDeviation); + + if (clusterMaxDeviation - clusterMinDeviation > getTolerance(thisPt)) { + break; } - segments.push(pts.slice(0, pti)); + clusterEndPt = thisPt; + thisVal = thisVector[0] * clusterUnitVector[0] + + thisVector[1] * clusterUnitVector[1]; + + if (thisVal > clusterHighVal) { + clusterHighVal = thisVal; + clusterHighPt = thisPt; + clusterHighFirst = false; + } else if (thisVal < clusterLowVal) { + clusterLowVal = thisVal; + clusterLowPt = thisPt; + clusterHighFirst = true; + } + } + + // insert this cluster into pts + // we've already inserted the start pt, now check if we have high and low pts + if (clusterHighFirst) { + pts[pti++] = clusterHighPt; + if (clusterEndPt !== clusterLowPt) pts[pti++] = clusterLowPt; + } else { + if (clusterLowPt !== clusterStartPt) pts[pti++] = clusterLowPt; + if (clusterEndPt !== clusterHighPt) pts[pti++] = clusterHighPt; + } + // and finally insert the end pt + pts[pti++] = clusterEndPt; + + // have we reached the end of this segment? + if (i >= d.length || !thisPt) break; + + // otherwise we have an out-of-cluster point to insert as next clusterStartPt + pts[pti++] = thisPt; + clusterStartPt = thisPt; } - return segments; + segments.push(pts.slice(0, pti)); + } + + return segments; }; diff --git a/src/traces/scatter/line_shape_defaults.js b/src/traces/scatter/line_shape_defaults.js index 76758ccce7b..c94da389e34 100644 --- a/src/traces/scatter/line_shape_defaults.js +++ b/src/traces/scatter/line_shape_defaults.js @@ -5,13 +5,9 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; // common to 'scatter' and 'scatterternary' module.exports = function handleLineShapeDefaults(traceIn, traceOut, coerce) { - var shape = coerce('line.shape'); - if(shape === 'spline') coerce('line.smoothing'); + var shape = coerce("line.shape"); + if (shape === "spline") coerce("line.smoothing"); }; diff --git a/src/traces/scatter/link_traces.js b/src/traces/scatter/link_traces.js index 61400ef4c77..37225fae3df 100644 --- a/src/traces/scatter/link_traces.js +++ b/src/traces/scatter/link_traces.js @@ -5,35 +5,33 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; module.exports = function linkTraces(gd, plotinfo, cdscatter) { - var cd, trace; - var prevtrace = null; + var cd, trace; + var prevtrace = null; - for(var i = 0; i < cdscatter.length; ++i) { - cd = cdscatter[i]; - trace = cd[0].trace; + for (var i = 0; i < cdscatter.length; ++i) { + cd = cdscatter[i]; + trace = cd[0].trace; - // Note: The check which ensures all cdscatter here are for the same axis and - // are either cartesian or scatterternary has been removed. This code assumes - // the passed scattertraces have been filtered to the proper plot types and - // the proper subplots. - if(trace.visible === true) { - trace._nexttrace = null; + // Note: The check which ensures all cdscatter here are for the same axis and + // are either cartesian or scatterternary has been removed. This code assumes + // the passed scattertraces have been filtered to the proper plot types and + // the proper subplots. + if (trace.visible === true) { + trace._nexttrace = null; - if(['tonextx', 'tonexty', 'tonext'].indexOf(trace.fill) !== -1) { - trace._prevtrace = prevtrace; + if (["tonextx", "tonexty", "tonext"].indexOf(trace.fill) !== -1) { + trace._prevtrace = prevtrace; - if(prevtrace) { - prevtrace._nexttrace = trace; - } - } - - prevtrace = trace; - } else { - trace._prevtrace = trace._nexttrace = null; + if (prevtrace) { + prevtrace._nexttrace = trace; } + } + + prevtrace = trace; + } else { + trace._prevtrace = trace._nexttrace = null; } + } }; diff --git a/src/traces/scatter/make_bubble_size_func.js b/src/traces/scatter/make_bubble_size_func.js index 56a4c199b2c..e3e94a99451 100644 --- a/src/traces/scatter/make_bubble_size_func.js +++ b/src/traces/scatter/make_bubble_size_func.js @@ -5,36 +5,35 @@ * 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 isNumeric = require('fast-isnumeric'); - +"use strict"; +var isNumeric = require("fast-isnumeric"); // used in the drawing step for 'scatter' and 'scattegeo' and // in the convert step for 'scatter3d' module.exports = function makeBubbleSizeFn(trace) { - var marker = trace.marker, - sizeRef = marker.sizeref || 1, - sizeMin = marker.sizemin || 0; - - // for bubble charts, allow scaling the provided value linearly - // and by area or diameter. - // Note this only applies to the array-value sizes - - var baseFn = (marker.sizemode === 'area') ? - function(v) { return Math.sqrt(v / sizeRef); } : - function(v) { return v / sizeRef; }; - - // TODO add support for position/negative bubbles? - // TODO add 'sizeoffset' attribute? - return function(v) { - var baseSize = baseFn(v / 2); - - // don't show non-numeric and negative sizes - return (isNumeric(baseSize) && (baseSize > 0)) ? - Math.max(baseSize, sizeMin) : - 0; - }; + var marker = trace.marker, + sizeRef = marker.sizeref || 1, + sizeMin = marker.sizemin || 0; + + // for bubble charts, allow scaling the provided value linearly + // and by area or diameter. + // Note this only applies to the array-value sizes + var baseFn = marker.sizemode === "area" + ? (function(v) { + return Math.sqrt(v / sizeRef); + }) + : (function(v) { + return v / sizeRef; + }); + + // TODO add support for position/negative bubbles? + // TODO add 'sizeoffset' attribute? + return function(v) { + var baseSize = baseFn(v / 2); + + // don't show non-numeric and negative sizes + return isNumeric(baseSize) && baseSize > 0 + ? Math.max(baseSize, sizeMin) + : 0; + }; }; diff --git a/src/traces/scatter/marker_defaults.js b/src/traces/scatter/marker_defaults.js index 3211d62426e..3720ba8e745 100644 --- a/src/traces/scatter/marker_defaults.js +++ b/src/traces/scatter/marker_defaults.js @@ -5,54 +5,65 @@ * 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 Color = require("../../components/color"); +var hasColorscale = require("../../components/colorscale/has_colorscale"); +var colorscaleDefaults = require("../../components/colorscale/defaults"); +var subTypes = require("./subtypes"); -'use strict'; +module.exports = function markerDefaults( + traceIn, + traceOut, + defaultColor, + layout, + coerce +) { + var isBubble = subTypes.isBubble(traceIn), + lineColor = (traceIn.line || {}).color, + defaultMLC; -var Color = require('../../components/color'); -var hasColorscale = require('../../components/colorscale/has_colorscale'); -var colorscaleDefaults = require('../../components/colorscale/defaults'); + // marker.color inherit from line.color (even if line.color is an array) + if (lineColor) defaultColor = lineColor; -var subTypes = require('./subtypes'); + coerce("marker.symbol"); + coerce("marker.opacity", isBubble ? 0.7 : 1); + coerce("marker.size"); + coerce("marker.color", defaultColor); + if (hasColorscale(traceIn, "marker")) { + colorscaleDefaults(traceIn, traceOut, layout, coerce, { + prefix: "marker.", + cLetter: "c" + }); + } -module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce) { - var isBubble = subTypes.isBubble(traceIn), - lineColor = (traceIn.line || {}).color, - defaultMLC; + // if there's a line with a different color than the marker, use + // that line color as the default marker line color + // (except when it's an array) + // mostly this is for transparent markers to behave nicely + if ( + lineColor && + !Array.isArray(lineColor) && + traceOut.marker.color !== lineColor + ) { + defaultMLC = lineColor; + } else if (isBubble) defaultMLC = Color.background; + else defaultMLC = Color.defaultLine; - // marker.color inherit from line.color (even if line.color is an array) - if(lineColor) defaultColor = lineColor; + coerce("marker.line.color", defaultMLC); + if (hasColorscale(traceIn, "marker.line")) { + colorscaleDefaults(traceIn, traceOut, layout, coerce, { + prefix: "marker.line.", + cLetter: "c" + }); + } - coerce('marker.symbol'); - coerce('marker.opacity', isBubble ? 0.7 : 1); - coerce('marker.size'); + coerce("marker.line.width", isBubble ? 1 : 0); - coerce('marker.color', defaultColor); - if(hasColorscale(traceIn, 'marker')) { - colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.', cLetter: 'c'}); - } - - // if there's a line with a different color than the marker, use - // that line color as the default marker line color - // (except when it's an array) - // mostly this is for transparent markers to behave nicely - if(lineColor && !Array.isArray(lineColor) && (traceOut.marker.color !== lineColor)) { - defaultMLC = lineColor; - } - else if(isBubble) defaultMLC = Color.background; - else defaultMLC = Color.defaultLine; - - coerce('marker.line.color', defaultMLC); - if(hasColorscale(traceIn, 'marker.line')) { - colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.line.', cLetter: 'c'}); - } - - coerce('marker.line.width', isBubble ? 1 : 0); - - if(isBubble) { - coerce('marker.sizeref'); - coerce('marker.sizemin'); - coerce('marker.sizemode'); - } + if (isBubble) { + coerce("marker.sizeref"); + coerce("marker.sizemin"); + coerce("marker.sizemode"); + } }; diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index b9dde96cd7a..67485c4341c 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -5,529 +5,548 @@ * 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 d3 = require('d3'); - -var Lib = require('../../lib'); -var Drawing = require('../../components/drawing'); -var ErrorBars = require('../../components/errorbars'); - -var subTypes = require('./subtypes'); -var linePoints = require('./line_points'); -var linkTraces = require('./link_traces'); -var polygonTester = require('../../lib/polygon').tester; - -module.exports = function plot(gd, plotinfo, cdscatter, transitionOpts, makeOnCompleteCallback) { - var i, uids, selection, join, onComplete; - - var scatterlayer = plotinfo.plot.select('g.scatterlayer'); - - // If transition config is provided, then it is only a partial replot and traces not - // updated are removed. - var isFullReplot = !transitionOpts; - var hasTransition = !!transitionOpts && transitionOpts.duration > 0; - - selection = scatterlayer.selectAll('g.trace'); - - join = selection.data(cdscatter, function(d) { return d[0].trace.uid; }); - - // Append new traces: - join.enter().append('g') - .attr('class', function(d) { - return 'trace scatter trace' + d[0].trace.uid; - }) - .style('stroke-miterlimit', 2); - - // After the elements are created but before they've been draw, we have to perform - // this extra step of linking the traces. This allows appending of fill layers so that - // the z-order of fill layers is correct. - linkTraces(gd, plotinfo, cdscatter); - - createFills(gd, scatterlayer); - - // Sort the traces, once created, so that the ordering is preserved even when traces - // are shown and hidden. This is needed since we're not just wiping everything out - // and recreating on every update. - for(i = 0, uids = []; i < cdscatter.length; i++) { - uids[i] = cdscatter[i][0].trace.uid; +"use strict"; +var d3 = require("d3"); + +var Lib = require("../../lib"); +var Drawing = require("../../components/drawing"); +var ErrorBars = require("../../components/errorbars"); + +var subTypes = require("./subtypes"); +var linePoints = require("./line_points"); +var linkTraces = require("./link_traces"); +var polygonTester = require("../../lib/polygon").tester; + +module.exports = function plot( + gd, + plotinfo, + cdscatter, + transitionOpts, + makeOnCompleteCallback +) { + var i, uids, selection, join, onComplete; + + var scatterlayer = plotinfo.plot.select("g.scatterlayer"); + + // If transition config is provided, then it is only a partial replot and traces not + // updated are removed. + var isFullReplot = !transitionOpts; + var hasTransition = !!transitionOpts && transitionOpts.duration > 0; + + selection = scatterlayer.selectAll("g.trace"); + + join = selection.data(cdscatter, function(d) { + return d[0].trace.uid; + }); + + // Append new traces: + join + .enter() + .append("g") + .attr("class", function(d) { + return "trace scatter trace" + d[0].trace.uid; + }) + .style("stroke-miterlimit", 2); + + // After the elements are created but before they've been draw, we have to perform + // this extra step of linking the traces. This allows appending of fill layers so that + // the z-order of fill layers is correct. + linkTraces(gd, plotinfo, cdscatter); + + createFills(gd, scatterlayer); + + // Sort the traces, once created, so that the ordering is preserved even when traces + // are shown and hidden. This is needed since we're not just wiping everything out + // and recreating on every update. + for (i = 0, uids = []; i < cdscatter.length; i++) { + uids[i] = cdscatter[i][0].trace.uid; + } + + scatterlayer.selectAll("g.trace").sort(function(a, b) { + var idx1 = uids.indexOf(a[0].trace.uid); + var idx2 = uids.indexOf(b[0].trace.uid); + return idx1 > idx2 ? 1 : -1; + }); + + if (hasTransition) { + if (makeOnCompleteCallback) { + // If it was passed a callback to register completion, make a callback. If + // this is created, then it must be executed on completion, otherwise the + // pos-transition redraw will not execute: + onComplete = makeOnCompleteCallback(); } - scatterlayer.selectAll('g.trace').sort(function(a, b) { - var idx1 = uids.indexOf(a[0].trace.uid); - var idx2 = uids.indexOf(b[0].trace.uid); - return idx1 > idx2 ? 1 : -1; + var transition = d3 + .transition() + .duration(transitionOpts.duration) + .ease(transitionOpts.easing) + .each("end", function() { + onComplete && onComplete(); + }) + .each("interrupt", function() { + onComplete && onComplete(); + }); + + transition.each(function() { + // Must run the selection again since otherwise enters/updates get grouped together + // and these get executed out of order. Except we need them in order! + scatterlayer.selectAll("g.trace").each(function(d, i) { + plotOne(gd, i, plotinfo, d, cdscatter, this, transitionOpts); + }); }); + } else { + scatterlayer.selectAll("g.trace").each(function(d, i) { + plotOne(gd, i, plotinfo, d, cdscatter, this, transitionOpts); + }); + } - if(hasTransition) { - if(makeOnCompleteCallback) { - // If it was passed a callback to register completion, make a callback. If - // this is created, then it must be executed on completion, otherwise the - // pos-transition redraw will not execute: - onComplete = makeOnCompleteCallback(); - } - - var transition = d3.transition() - .duration(transitionOpts.duration) - .ease(transitionOpts.easing) - .each('end', function() { - onComplete && onComplete(); - }) - .each('interrupt', function() { - onComplete && onComplete(); - }); - - transition.each(function() { - // Must run the selection again since otherwise enters/updates get grouped together - // and these get executed out of order. Except we need them in order! - scatterlayer.selectAll('g.trace').each(function(d, i) { - plotOne(gd, i, plotinfo, d, cdscatter, this, transitionOpts); - }); - }); - } else { - scatterlayer.selectAll('g.trace').each(function(d, i) { - plotOne(gd, i, plotinfo, d, cdscatter, this, transitionOpts); - }); - } - - if(isFullReplot) { - join.exit().remove(); - } + if (isFullReplot) { + join.exit().remove(); + } - // remove paths that didn't get used - scatterlayer.selectAll('path:not([d])').remove(); + // remove paths that didn't get used + scatterlayer.selectAll("path:not([d])").remove(); }; function createFills(gd, scatterlayer) { - var trace; - - scatterlayer.selectAll('g.trace').each(function(d) { - var tr = d3.select(this); + var trace; + + scatterlayer.selectAll("g.trace").each(function(d) { + var tr = d3.select(this); + + // Loop only over the traces being redrawn: + trace = d[0].trace; + + // make the fill-to-next path now for the NEXT trace, so it shows + // behind both lines. + if (trace._nexttrace) { + trace._nextFill = tr.select(".js-fill.js-tonext"); + if (!trace._nextFill.size()) { + // If there is an existing tozero fill, we must insert this *after* that fill: + var loc = ":first-child"; + if (tr.select(".js-fill.js-tozero").size()) { + loc += " + *"; + } - // Loop only over the traces being redrawn: - trace = d[0].trace; + trace._nextFill = tr + .insert("path", loc) + .attr("class", "js-fill js-tonext"); + } + } else { + tr.selectAll(".js-fill.js-tonext").remove(); + trace._nextFill = null; + } - // make the fill-to-next path now for the NEXT trace, so it shows - // behind both lines. - if(trace._nexttrace) { - trace._nextFill = tr.select('.js-fill.js-tonext'); - if(!trace._nextFill.size()) { + if ( + trace.fill && + (trace.fill.substr(0, 6) === "tozero" || + trace.fill === "toself" || + trace.fill.substr(0, 2) === "to" && !trace._prevtrace) + ) { + trace._ownFill = tr.select(".js-fill.js-tozero"); + if (!trace._ownFill.size()) { + trace._ownFill = tr + .insert("path", ":first-child") + .attr("class", "js-fill js-tozero"); + } + } else { + tr.selectAll(".js-fill.js-tozero").remove(); + trace._ownFill = null; + } + }); +} - // If there is an existing tozero fill, we must insert this *after* that fill: - var loc = ':first-child'; - if(tr.select('.js-fill.js-tozero').size()) { - loc += ' + *'; - } +function plotOne( + gd, + idx, + plotinfo, + cdscatter, + cdscatterAll, + element, + transitionOpts +) { + var join, i; + + // Since this has been reorganized and we're executing this on individual traces, + // we need to pass it the full list of cdscatter as well as this trace's index (idx) + // since it does an internal n^2 loop over comparisons with other traces: + selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll); + + var hasTransition = !!transitionOpts && transitionOpts.duration > 0; + + function transition(selection) { + return hasTransition ? selection.transition() : selection; + } + + var xa = plotinfo.xaxis, ya = plotinfo.yaxis; + + var trace = cdscatter[0].trace, line = trace.line, tr = d3.select(element); + + // (so error bars can find them along with bars) + // error bars are at the bottom + tr.call(ErrorBars.plot, plotinfo, transitionOpts); + + if (trace.visible !== true) return; + + transition(tr).style("opacity", trace.opacity); + + // BUILD LINES AND FILLS + var ownFillEl3, tonext; + var ownFillDir = trace.fill.charAt(trace.fill.length - 1); + if (ownFillDir !== "x" && ownFillDir !== "y") ownFillDir = ""; + + // store node for tweaking by selectPoints + cdscatter[0].node3 = tr; + + var prevRevpath = ""; + var prevPolygons = []; + var prevtrace = trace._prevtrace; + + if (prevtrace) { + prevRevpath = prevtrace._prevRevpath || ""; + tonext = prevtrace._nextFill; + prevPolygons = prevtrace._polygons; + } + + var thispath, + thisrevpath, + // fullpath is all paths for this curve, joined together straight + // across gaps, for filling + fullpath = "", + // revpath is fullpath reversed, for fill-to-next + revpath = "", + // functions for converting a point array to a path + pathfn, + revpathbase, + revpathfn, + // variables used before and after the data join + pt0, + lastSegment, + pt1, + thisPolygons; + + // initialize line join data / method + var segments = [], lineSegments = [], makeUpdate = Lib.noop; + + ownFillEl3 = trace._ownFill; + + if (subTypes.hasLines(trace) || trace.fill !== "none") { + if (tonext) { + // This tells .style which trace to use for fill information: + tonext.datum(cdscatter); + } - trace._nextFill = tr.insert('path', loc).attr('class', 'js-fill js-tonext'); - } + if (["hv", "vh", "hvh", "vhv"].indexOf(line.shape) !== -1) { + pathfn = Drawing.steps(line.shape); + revpathbase = Drawing.steps(line.shape.split("").reverse().join("")); + } else if (line.shape === "spline") { + pathfn = revpathbase = function(pts) { + var pLast = pts[pts.length - 1]; + if (pts[0][0] === pLast[0] && pts[0][1] === pLast[1]) { + // identical start and end points: treat it as a + // closed curve so we don't get a kink + return Drawing.smoothclosed(pts.slice(1), line.smoothing); } else { - tr.selectAll('.js-fill.js-tonext').remove(); - trace._nextFill = null; + return Drawing.smoothopen(pts, line.smoothing); } + }; + } else { + pathfn = revpathbase = function(pts) { + return "M" + pts.join("L"); + }; + } - if(trace.fill && (trace.fill.substr(0, 6) === 'tozero' || trace.fill === 'toself' || - (trace.fill.substr(0, 2) === 'to' && !trace._prevtrace))) { - trace._ownFill = tr.select('.js-fill.js-tozero'); - if(!trace._ownFill.size()) { - trace._ownFill = tr.insert('path', ':first-child').attr('class', 'js-fill js-tozero'); - } - } else { - tr.selectAll('.js-fill.js-tozero').remove(); - trace._ownFill = null; - } + revpathfn = function(pts) { + // note: this is destructive (reverses pts in place) so can't use pts after this + return revpathbase(pts.reverse()); + }; + + segments = linePoints(cdscatter, { + xaxis: xa, + yaxis: ya, + connectGaps: trace.connectgaps, + baseTolerance: Math.max(line.width || 1, 3) / 4, + linear: line.shape === "linear", + simplify: line.simplify }); -} - -function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transitionOpts) { - var join, i; - - // Since this has been reorganized and we're executing this on individual traces, - // we need to pass it the full list of cdscatter as well as this trace's index (idx) - // since it does an internal n^2 loop over comparisons with other traces: - selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll); - var hasTransition = !!transitionOpts && transitionOpts.duration > 0; - - function transition(selection) { - return hasTransition ? selection.transition() : selection; + // since we already have the pixel segments here, use them to make + // polygons for hover on fill + // TODO: can we skip this if hoveron!=fills? That would mean we + // need to redraw when you change hoveron... + thisPolygons = trace._polygons = new Array(segments.length); + for (i = 0; i < segments.length; i++) { + trace._polygons[i] = polygonTester(segments[i]); } - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis; - - var trace = cdscatter[0].trace, - line = trace.line, - tr = d3.select(element); - - // (so error bars can find them along with bars) - // error bars are at the bottom - tr.call(ErrorBars.plot, plotinfo, transitionOpts); - - if(trace.visible !== true) return; - - transition(tr).style('opacity', trace.opacity); - - // BUILD LINES AND FILLS - var ownFillEl3, tonext; - var ownFillDir = trace.fill.charAt(trace.fill.length - 1); - if(ownFillDir !== 'x' && ownFillDir !== 'y') ownFillDir = ''; - - // store node for tweaking by selectPoints - cdscatter[0].node3 = tr; - - var prevRevpath = ''; - var prevPolygons = []; - var prevtrace = trace._prevtrace; - - if(prevtrace) { - prevRevpath = prevtrace._prevRevpath || ''; - tonext = prevtrace._nextFill; - prevPolygons = prevtrace._polygons; + if (segments.length) { + pt0 = segments[0][0]; + lastSegment = segments[segments.length - 1]; + pt1 = lastSegment[lastSegment.length - 1]; } - var thispath, - thisrevpath, - // fullpath is all paths for this curve, joined together straight - // across gaps, for filling - fullpath = '', - // revpath is fullpath reversed, for fill-to-next - revpath = '', - // functions for converting a point array to a path - pathfn, revpathbase, revpathfn, - // variables used before and after the data join - pt0, lastSegment, pt1, thisPolygons; - - // initialize line join data / method - var segments = [], - lineSegments = [], - makeUpdate = Lib.noop; - - ownFillEl3 = trace._ownFill; - - if(subTypes.hasLines(trace) || trace.fill !== 'none') { - - if(tonext) { - // This tells .style which trace to use for fill information: - tonext.datum(cdscatter); - } - - if(['hv', 'vh', 'hvh', 'vhv'].indexOf(line.shape) !== -1) { - pathfn = Drawing.steps(line.shape); - revpathbase = Drawing.steps( - line.shape.split('').reverse().join('') - ); - } - else if(line.shape === 'spline') { - pathfn = revpathbase = function(pts) { - var pLast = pts[pts.length - 1]; - if(pts[0][0] === pLast[0] && pts[0][1] === pLast[1]) { - // identical start and end points: treat it as a - // closed curve so we don't get a kink - return Drawing.smoothclosed(pts.slice(1), line.smoothing); - } - else { - return Drawing.smoothopen(pts, line.smoothing); - } - }; - } - else { - pathfn = revpathbase = function(pts) { - return 'M' + pts.join('L'); - }; - } - - revpathfn = function(pts) { - // note: this is destructive (reverses pts in place) so can't use pts after this - return revpathbase(pts.reverse()); - }; - - segments = linePoints(cdscatter, { - xaxis: xa, - yaxis: ya, - connectGaps: trace.connectgaps, - baseTolerance: Math.max(line.width || 1, 3) / 4, - linear: line.shape === 'linear', - simplify: line.simplify - }); - - // since we already have the pixel segments here, use them to make - // polygons for hover on fill - // TODO: can we skip this if hoveron!=fills? That would mean we - // need to redraw when you change hoveron... - thisPolygons = trace._polygons = new Array(segments.length); - for(i = 0; i < segments.length; i++) { - trace._polygons[i] = polygonTester(segments[i]); - } + lineSegments = segments.filter(function(s) { + return s.length > 1; + }); - if(segments.length) { - pt0 = segments[0][0]; - lastSegment = segments[segments.length - 1]; - pt1 = lastSegment[lastSegment.length - 1]; + makeUpdate = function(isEnter) { + return function(pts) { + thispath = pathfn(pts); + thisrevpath = revpathfn(pts); + if (!fullpath) { + fullpath = thispath; + revpath = thisrevpath; + } else if (ownFillDir) { + fullpath += "L" + thispath.substr(1); + revpath = thisrevpath + ("L" + revpath.substr(1)); + } else { + fullpath += "Z" + thispath; + revpath = thisrevpath + "Z" + revpath; } - lineSegments = segments.filter(function(s) { - return s.length > 1; - }); - - makeUpdate = function(isEnter) { - return function(pts) { - thispath = pathfn(pts); - thisrevpath = revpathfn(pts); - if(!fullpath) { - fullpath = thispath; - revpath = thisrevpath; - } - else if(ownFillDir) { - fullpath += 'L' + thispath.substr(1); - revpath = thisrevpath + ('L' + revpath.substr(1)); - } - else { - fullpath += 'Z' + thispath; - revpath = thisrevpath + 'Z' + revpath; - } - - if(subTypes.hasLines(trace) && pts.length > 1) { - var el = d3.select(this); - - // This makes the coloring work correctly: - el.datum(cdscatter); - - if(isEnter) { - transition(el.style('opacity', 0) - .attr('d', thispath) - .call(Drawing.lineGroupStyle)) - .style('opacity', 1); - } else { - var sel = transition(el); - sel.attr('d', thispath); - Drawing.singleLineStyle(cdscatter, sel); - } - } - }; - }; - } - - var lineJoin = tr.selectAll('.js-line').data(lineSegments); - - transition(lineJoin.exit()) - .style('opacity', 0) - .remove(); - - lineJoin.each(makeUpdate(false)); - - lineJoin.enter().append('path') - .classed('js-line', true) - .style('vector-effect', 'non-scaling-stroke') - .call(Drawing.lineGroupStyle) - .each(makeUpdate(true)); - - if(segments.length) { - if(ownFillEl3) { - if(pt0 && pt1) { - if(ownFillDir) { - if(ownFillDir === 'y') { - pt0[1] = pt1[1] = ya.c2p(0, true); - } - else if(ownFillDir === 'x') { - pt0[0] = pt1[0] = xa.c2p(0, true); - } - - // fill to zero: full trace path, plus extension of - // the endpoints to the appropriate axis - // For the sake of animations, wrap the points around so that - // the points on the axes are the first two points. Otherwise - // animations get a little crazy if the number of points changes. - transition(ownFillEl3).attr('d', 'M' + pt1 + 'L' + pt0 + 'L' + fullpath.substr(1)); - } else { - // fill to self: just join the path to itself - transition(ownFillEl3).attr('d', fullpath + 'Z'); - } - } + if (subTypes.hasLines(trace) && pts.length > 1) { + var el = d3.select(this); + + // This makes the coloring work correctly: + el.datum(cdscatter); + + if (isEnter) { + transition( + el + .style("opacity", 0) + .attr("d", thispath) + .call(Drawing.lineGroupStyle) + ).style("opacity", 1); + } else { + var sel = transition(el); + sel.attr("d", thispath); + Drawing.singleLineStyle(cdscatter, sel); + } } - else if(trace.fill.substr(0, 6) === 'tonext' && fullpath && prevRevpath) { - // fill to next: full trace path, plus the previous path reversed - if(trace.fill === 'tonext') { - // tonext: for use by concentric shapes, like manually constructed - // contours, we just add the two paths closed on themselves. - // This makes strange results if one path is *not* entirely - // inside the other, but then that is a strange usage. - transition(tonext).attr('d', fullpath + 'Z' + prevRevpath + 'Z'); - } - else { - // tonextx/y: for now just connect endpoints with lines. This is - // the correct behavior if the endpoints are at the same value of - // y/x, but if they *aren't*, we should ideally do more complicated - // things depending on whether the new endpoint projects onto the - // existing curve or off the end of it - transition(tonext).attr('d', fullpath + 'L' + prevRevpath.substr(1) + 'Z'); - } - trace._polygons = trace._polygons.concat(prevPolygons); + }; + }; + } + + var lineJoin = tr.selectAll(".js-line").data(lineSegments); + + transition(lineJoin.exit()).style("opacity", 0).remove(); + + lineJoin.each(makeUpdate(false)); + + lineJoin + .enter() + .append("path") + .classed("js-line", true) + .style("vector-effect", "non-scaling-stroke") + .call(Drawing.lineGroupStyle) + .each(makeUpdate(true)); + + if (segments.length) { + if (ownFillEl3) { + if (pt0 && pt1) { + if (ownFillDir) { + if (ownFillDir === "y") { + pt0[1] = pt1[1] = ya.c2p(0, true); + } else if (ownFillDir === "x") { + pt0[0] = pt1[0] = xa.c2p(0, true); + } + + // fill to zero: full trace path, plus extension of + // the endpoints to the appropriate axis + // For the sake of animations, wrap the points around so that + // the points on the axes are the first two points. Otherwise + // animations get a little crazy if the number of points changes. + transition(ownFillEl3).attr( + "d", + "M" + pt1 + "L" + pt0 + "L" + fullpath.substr(1) + ); + } else { + // fill to self: just join the path to itself + transition(ownFillEl3).attr("d", fullpath + "Z"); } - trace._prevRevpath = revpath; - trace._prevPolygons = thisPolygons; + } + } else if ( + trace.fill.substr(0, 6) === "tonext" && fullpath && prevRevpath + ) { + // fill to next: full trace path, plus the previous path reversed + if (trace.fill === "tonext") { + // tonext: for use by concentric shapes, like manually constructed + // contours, we just add the two paths closed on themselves. + // This makes strange results if one path is *not* entirely + // inside the other, but then that is a strange usage. + transition(tonext).attr("d", fullpath + "Z" + prevRevpath + "Z"); + } else { + // tonextx/y: for now just connect endpoints with lines. This is + // the correct behavior if the endpoints are at the same value of + // y/x, but if they *aren't*, we should ideally do more complicated + // things depending on whether the new endpoint projects onto the + // existing curve or off the end of it + transition(tonext).attr( + "d", + fullpath + "L" + prevRevpath.substr(1) + "Z" + ); + } + trace._polygons = trace._polygons.concat(prevPolygons); } + trace._prevRevpath = revpath; + trace._prevPolygons = thisPolygons; + } + function visFilter(d) { + return d.filter(function(v) { + return v.vis; + }); + } - function visFilter(d) { - return d.filter(function(v) { return v.vis; }); - } - - function keyFunc(d) { - return d.id; - } + function keyFunc(d) { + return d.id; + } - // Returns a function if the trace is keyed, otherwise returns undefined - function getKeyFunc(trace) { - if(trace.ids) { - return keyFunc; - } + // Returns a function if the trace is keyed, otherwise returns undefined + function getKeyFunc(trace) { + if (trace.ids) { + return keyFunc; } + } - function hideFilter() { - return false; - } + function hideFilter() { + return false; + } - function makePoints(d) { - var join, selection; + function makePoints(d) { + var join, selection; - var trace = d[0].trace, - s = d3.select(this), - showMarkers = subTypes.hasMarkers(trace), - showText = subTypes.hasText(trace); + var trace = d[0].trace, + s = d3.select(this), + showMarkers = subTypes.hasMarkers(trace), + showText = subTypes.hasText(trace); - var keyFunc = getKeyFunc(trace), - markerFilter = hideFilter, - textFilter = hideFilter; + var keyFunc = getKeyFunc(trace), + markerFilter = hideFilter, + textFilter = hideFilter; - if(showMarkers) { - markerFilter = trace.marker.maxdisplayed ? visFilter : Lib.identity; - } + if (showMarkers) { + markerFilter = trace.marker.maxdisplayed ? visFilter : Lib.identity; + } - if(showText) { - textFilter = trace.marker.maxdisplayed ? visFilter : Lib.identity; - } + if (showText) { + textFilter = trace.marker.maxdisplayed ? visFilter : Lib.identity; + } - // marker points + // marker points + selection = s.selectAll("path.point"); - selection = s.selectAll('path.point'); + join = selection.data(markerFilter, keyFunc); - join = selection.data(markerFilter, keyFunc); + var enter = join.enter().append("path").classed("point", true); - var enter = join.enter().append('path') - .classed('point', true); + enter + .call(Drawing.pointStyle, trace) + .call(Drawing.translatePoints, xa, ya, trace); - enter.call(Drawing.pointStyle, trace) - .call(Drawing.translatePoints, xa, ya, trace); + if (hasTransition) { + enter.style("opacity", 0).transition().style("opacity", 1); + } - if(hasTransition) { - enter.style('opacity', 0).transition() - .style('opacity', 1); - } + join.each(function(d) { + var sel = transition(d3.select(this)); + Drawing.translatePoint(d, sel, xa, ya); + Drawing.singlePointStyle(d, sel, trace); + }); - join.each(function(d) { - var sel = transition(d3.select(this)); - Drawing.translatePoint(d, sel, xa, ya); - Drawing.singlePointStyle(d, sel, trace); - }); + if (hasTransition) { + join.exit().transition().style("opacity", 0).remove(); + } else { + join.exit().remove(); + } - if(hasTransition) { - join.exit().transition() - .style('opacity', 0) - .remove(); - } else { - join.exit().remove(); - } + // text points + selection = s.selectAll("g"); + join = selection.data(textFilter, keyFunc); - // text points - selection = s.selectAll('g'); - join = selection.data(textFilter, keyFunc); + // each text needs to go in its own 'g' in case + // it gets converted to mathjax + join.enter().append("g").append("text"); - // each text needs to go in its own 'g' in case - // it gets converted to mathjax - join.enter().append('g').append('text'); + join.each(function(d) { + var sel = transition(d3.select(this).select("text")); + Drawing.translatePoint(d, sel, xa, ya); + }); - join.each(function(d) { - var sel = transition(d3.select(this).select('text')); - Drawing.translatePoint(d, sel, xa, ya); + join + .selectAll("text") + .call(Drawing.textPointStyle, trace) + .each(function(d) { + // This just *has* to be totally custom becuase of SVG text positioning :( + // It's obviously copied from translatePoint; we just can't use that + // + // put xp and yp into d if pixel scaling is already done + var x = d.xp || xa.c2p(d.x), y = d.yp || ya.c2p(d.y); + + d3.select(this).selectAll("tspan").each(function() { + transition(d3.select(this)).attr({ x: x, y: y }); }); + }); - join.selectAll('text') - .call(Drawing.textPointStyle, trace) - .each(function(d) { - - // This just *has* to be totally custom becuase of SVG text positioning :( - // It's obviously copied from translatePoint; we just can't use that - // - // put xp and yp into d if pixel scaling is already done - var x = d.xp || xa.c2p(d.x), - y = d.yp || ya.c2p(d.y); - - d3.select(this).selectAll('tspan').each(function() { - transition(d3.select(this)).attr({x: x, y: y}); - }); - }); - - join.exit().remove(); - } + join.exit().remove(); + } - // NB: selectAll is evaluated on instantiation: - var pointSelection = tr.selectAll('.points'); + // NB: selectAll is evaluated on instantiation: + var pointSelection = tr.selectAll(".points"); - // Join with new data - join = pointSelection.data([cdscatter]); + // Join with new data + join = pointSelection.data([cdscatter]); - // Transition existing, but don't defer this to an async .transition since - // there's no timing involved: - pointSelection.each(makePoints); + // Transition existing, but don't defer this to an async .transition since + // there's no timing involved: + pointSelection.each(makePoints); - join.enter().append('g') - .classed('points', true) - .each(makePoints); + join.enter().append("g").classed("points", true).each(makePoints); - join.exit().remove(); + join.exit().remove(); } function selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll) { - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - xr = d3.extent(Lib.simpleMap(xa.range, xa.r2c)), - yr = d3.extent(Lib.simpleMap(ya.range, ya.r2c)); - - var trace = cdscatter[0].trace; - if(!subTypes.hasMarkers(trace)) return; - // if marker.maxdisplayed is used, select a maximum of - // mnum markers to show, from the set that are in the viewport - var mnum = trace.marker.maxdisplayed; - - // TODO: remove some as we get away from the viewport? - if(mnum === 0) return; - - var cd = cdscatter.filter(function(v) { - return v.x >= xr[0] && v.x <= xr[1] && v.y >= yr[0] && v.y <= yr[1]; - }), - inc = Math.ceil(cd.length / mnum), - tnum = 0; - cdscatterAll.forEach(function(cdj, j) { - var tracei = cdj[0].trace; - if(subTypes.hasMarkers(tracei) && - tracei.marker.maxdisplayed > 0 && j < idx) { - tnum++; - } - }); - - // if multiple traces use maxdisplayed, stagger which markers we - // display this formula offsets successive traces by 1/3 of the - // increment, adding an extra small amount after each triplet so - // it's not quite periodic - var i0 = Math.round(tnum * inc / 3 + Math.floor(tnum / 3) * inc / 7.1); - - // for error bars: save in cd which markers to show - // so we don't have to repeat this - cdscatter.forEach(function(v) { delete v.vis; }); - cd.forEach(function(v, i) { - if(Math.round((i + i0) % inc) === 0) v.vis = true; - }); + var xa = plotinfo.xaxis, + ya = plotinfo.yaxis, + xr = d3.extent(Lib.simpleMap(xa.range, xa.r2c)), + yr = d3.extent(Lib.simpleMap(ya.range, ya.r2c)); + + var trace = cdscatter[0].trace; + if (!subTypes.hasMarkers(trace)) return; + // if marker.maxdisplayed is used, select a maximum of + // mnum markers to show, from the set that are in the viewport + var mnum = trace.marker.maxdisplayed; + + // TODO: remove some as we get away from the viewport? + if (mnum === 0) return; + + var cd = cdscatter.filter(function(v) { + return v.x >= xr[0] && v.x <= xr[1] && v.y >= yr[0] && v.y <= yr[1]; + }), + inc = Math.ceil(cd.length / mnum), + tnum = 0; + cdscatterAll.forEach(function(cdj, j) { + var tracei = cdj[0].trace; + if ( + subTypes.hasMarkers(tracei) && tracei.marker.maxdisplayed > 0 && j < idx + ) { + tnum++; + } + }); + + // if multiple traces use maxdisplayed, stagger which markers we + // display this formula offsets successive traces by 1/3 of the + // increment, adding an extra small amount after each triplet so + // it's not quite periodic + var i0 = Math.round(tnum * inc / 3 + Math.floor(tnum / 3) * inc / 7.1); + + // for error bars: save in cd which markers to show + // so we don't have to repeat this + cdscatter.forEach(function(v) { + delete v.vis; + }); + cd.forEach(function(v, i) { + if (Math.round((i + i0) % inc) === 0) v.vis = true; + }); } diff --git a/src/traces/scatter/select.js b/src/traces/scatter/select.js index 8ad3fc12030..8394b341552 100644 --- a/src/traces/scatter/select.js +++ b/src/traces/scatter/select.js @@ -5,66 +5,64 @@ * 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 subtypes = require('./subtypes'); +"use strict"; +var subtypes = require("./subtypes"); var DESELECTDIM = 0.2; module.exports = function selectPoints(searchInfo, polygon) { - var cd = searchInfo.cd, - xa = searchInfo.xaxis, - ya = searchInfo.yaxis, - selection = [], - trace = cd[0].trace, - curveNumber = trace.index, - marker = trace.marker, - i, - di, - x, - y; + var cd = searchInfo.cd, + xa = searchInfo.xaxis, + ya = searchInfo.yaxis, + selection = [], + trace = cd[0].trace, + curveNumber = trace.index, + marker = trace.marker, + i, + di, + x, + y; - // TODO: include lines? that would require per-segment line properties - var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace)); - if(trace.visible !== true || hasOnlyLines) return; + // TODO: include lines? that would require per-segment line properties + var hasOnlyLines = !subtypes.hasMarkers(trace) && !subtypes.hasText(trace); + if (trace.visible !== true || hasOnlyLines) return; - var opacity = Array.isArray(marker.opacity) ? 1 : marker.opacity; + var opacity = Array.isArray(marker.opacity) ? 1 : marker.opacity; - if(polygon === false) { // clear selection - for(i = 0; i < cd.length; i++) cd[i].dim = 0; + if (polygon === false) { + // clear selection + for (i = 0; i < cd.length; i++) { + cd[i].dim = 0; } - else { - for(i = 0; i < cd.length; i++) { - di = cd[i]; - x = xa.c2p(di.x); - y = ya.c2p(di.y); - if(polygon.contains([x, y])) { - selection.push({ - curveNumber: curveNumber, - pointNumber: i, - x: di.x, - y: di.y, - id: di.id - }); - di.dim = 0; - } - else di.dim = 1; - } + } else { + for (i = 0; i < cd.length; i++) { + di = cd[i]; + x = xa.c2p(di.x); + y = ya.c2p(di.y); + if (polygon.contains([x, y])) { + selection.push({ + curveNumber: curveNumber, + pointNumber: i, + x: di.x, + y: di.y, + id: di.id + }); + di.dim = 0; + } else { + di.dim = 1; + } } + } - // do the dimming here, as well as returning the selection - // The logic here duplicates Drawing.pointStyle, but I don't want - // d.dim in pointStyle in case something goes wrong with selection. - cd[0].node3.selectAll('path.point') - .style('opacity', function(d) { - return ((d.mo + 1 || opacity + 1) - 1) * (d.dim ? DESELECTDIM : 1); - }); - cd[0].node3.selectAll('text') - .style('opacity', function(d) { - return d.dim ? DESELECTDIM : 1; - }); + // do the dimming here, as well as returning the selection + // The logic here duplicates Drawing.pointStyle, but I don't want + // d.dim in pointStyle in case something goes wrong with selection. + cd[0].node3.selectAll("path.point").style("opacity", function(d) { + return ((d.mo + 1 || opacity + 1) - 1) * (d.dim ? DESELECTDIM : 1); + }); + cd[0].node3.selectAll("text").style("opacity", function(d) { + return d.dim ? DESELECTDIM : 1; + }); - return selection; + return selection; }; diff --git a/src/traces/scatter/style.js b/src/traces/scatter/style.js index 78339d3acf7..dc21d55fc93 100644 --- a/src/traces/scatter/style.js +++ b/src/traces/scatter/style.js @@ -5,36 +5,33 @@ * 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 d3 = require("d3"); - -'use strict'; - -var d3 = require('d3'); - -var Drawing = require('../../components/drawing'); -var ErrorBars = require('../../components/errorbars'); - +var Drawing = require("../../components/drawing"); +var ErrorBars = require("../../components/errorbars"); module.exports = function style(gd) { - var s = d3.select(gd).selectAll('g.trace.scatter'); + var s = d3.select(gd).selectAll("g.trace.scatter"); - s.style('opacity', function(d) { - return d[0].trace.opacity; - }); + s.style("opacity", function(d) { + return d[0].trace.opacity; + }); - s.selectAll('g.points') - .each(function(d) { - d3.select(this).selectAll('path.point') - .call(Drawing.pointStyle, d.trace || d[0].trace); - d3.select(this).selectAll('text') - .call(Drawing.textPointStyle, d.trace || d[0].trace); - }); + s.selectAll("g.points").each(function(d) { + d3 + .select(this) + .selectAll("path.point") + .call(Drawing.pointStyle, d.trace || d[0].trace); + d3 + .select(this) + .selectAll("text") + .call(Drawing.textPointStyle, d.trace || d[0].trace); + }); - s.selectAll('g.trace path.js-line') - .call(Drawing.lineGroupStyle); + s.selectAll("g.trace path.js-line").call(Drawing.lineGroupStyle); - s.selectAll('g.trace path.js-fill') - .call(Drawing.fillGroupStyle); + s.selectAll("g.trace path.js-fill").call(Drawing.fillGroupStyle); - s.call(ErrorBars.style); + s.call(ErrorBars.style); }; diff --git a/src/traces/scatter/subtypes.js b/src/traces/scatter/subtypes.js index 5d117eced40..49c74f54d42 100644 --- a/src/traces/scatter/subtypes.js +++ b/src/traces/scatter/subtypes.js @@ -5,30 +5,20 @@ * 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'); +"use strict"; +var Lib = require("../../lib"); module.exports = { - hasLines: function(trace) { - return trace.visible && trace.mode && - trace.mode.indexOf('lines') !== -1; - }, - - hasMarkers: function(trace) { - return trace.visible && trace.mode && - trace.mode.indexOf('markers') !== -1; - }, - - hasText: function(trace) { - return trace.visible && trace.mode && - trace.mode.indexOf('text') !== -1; - }, - - isBubble: function(trace) { - return Lib.isPlainObject(trace.marker) && - Array.isArray(trace.marker.size); - } + hasLines: function(trace) { + return trace.visible && trace.mode && trace.mode.indexOf("lines") !== -1; + }, + hasMarkers: function(trace) { + return trace.visible && trace.mode && trace.mode.indexOf("markers") !== -1; + }, + hasText: function(trace) { + return trace.visible && trace.mode && trace.mode.indexOf("text") !== -1; + }, + isBubble: function(trace) { + return Lib.isPlainObject(trace.marker) && Array.isArray(trace.marker.size); + } }; diff --git a/src/traces/scatter/text_defaults.js b/src/traces/scatter/text_defaults.js index 2860d127825..4810a115509 100644 --- a/src/traces/scatter/text_defaults.js +++ b/src/traces/scatter/text_defaults.js @@ -5,15 +5,11 @@ * 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'); - +"use strict"; +var Lib = require("../../lib"); // common to 'scatter', 'scatter3d' and 'scattergeo' module.exports = function(traceIn, traceOut, layout, coerce) { - coerce('textposition'); - Lib.coerceFont(coerce, 'textfont', layout.font); + coerce("textposition"); + Lib.coerceFont(coerce, "textfont", layout.font); }; diff --git a/src/traces/scatter/xy_defaults.js b/src/traces/scatter/xy_defaults.js index a43f04bc337..1c94ef46782 100644 --- a/src/traces/scatter/xy_defaults.js +++ b/src/traces/scatter/xy_defaults.js @@ -5,44 +5,39 @@ * 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 Registry = require('../../registry'); - +"use strict"; +var Registry = require("../../registry"); module.exports = function handleXYDefaults(traceIn, traceOut, layout, coerce) { - var len, - x = coerce('x'), - y = coerce('y'); - - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); - handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); - - if(x) { - if(y) { - len = Math.min(x.length, y.length); - // TODO: not sure we should do this here... but I think - // the way it works in calc is wrong, because it'll delete data - // which could be a problem eg in streaming / editing if x and y - // come in at different times - // so we need to revisit calc before taking this out - if(len < x.length) traceOut.x = x.slice(0, len); - if(len < y.length) traceOut.y = y.slice(0, len); - } - else { - len = x.length; - coerce('y0'); - coerce('dy'); - } - } - else { - if(!y) return 0; - - len = traceOut.y.length; - coerce('x0'); - coerce('dx'); + var len, x = coerce("x"), y = coerce("y"); + + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleTraceDefaults" + ); + handleCalendarDefaults(traceIn, traceOut, ["x", "y"], layout); + + if (x) { + if (y) { + len = Math.min(x.length, y.length); + // TODO: not sure we should do this here... but I think + // the way it works in calc is wrong, because it'll delete data + // which could be a problem eg in streaming / editing if x and y + // come in at different times + // so we need to revisit calc before taking this out + if (len < x.length) traceOut.x = x.slice(0, len); + if (len < y.length) traceOut.y = y.slice(0, len); + } else { + len = x.length; + coerce("y0"); + coerce("dy"); } - return len; + } else { + if (!y) return 0; + + len = traceOut.y.length; + coerce("x0"); + coerce("dx"); + } + return len; }; diff --git a/src/traces/scatter3d/attributes.js b/src/traces/scatter3d/attributes.js index 7c5278c25c9..d75b648b024 100644 --- a/src/traces/scatter3d/attributes.js +++ b/src/traces/scatter3d/attributes.js @@ -5,154 +5,152 @@ * 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 scatterAttrs = require("../scatter/attributes"); +var colorAttributes = require("../../components/colorscale/color_attributes"); +var errorBarAttrs = require("../../components/errorbars/attributes"); -'use strict'; - -var scatterAttrs = require('../scatter/attributes'); -var colorAttributes = require('../../components/colorscale/color_attributes'); -var errorBarAttrs = require('../../components/errorbars/attributes'); - -var MARKER_SYMBOLS = require('../../constants/gl_markers'); -var extendFlat = require('../../lib/extend').extendFlat; +var MARKER_SYMBOLS = require("../../constants/gl_markers"); +var extendFlat = require("../../lib/extend").extendFlat; var scatterLineAttrs = scatterAttrs.line, - scatterMarkerAttrs = scatterAttrs.marker, - scatterMarkerLineAttrs = scatterMarkerAttrs.line; + scatterMarkerAttrs = scatterAttrs.marker, + scatterMarkerLineAttrs = scatterMarkerAttrs.line; function makeProjectionAttr(axLetter) { - return { - show: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Sets whether or not projections are shown along the', - axLetter, 'axis.' - ].join(' ') - }, - opacity: { - valType: 'number', - role: 'style', - min: 0, - max: 1, - dflt: 1, - description: 'Sets the projection color.' - }, - scale: { - valType: 'number', - role: 'style', - min: 0, - max: 10, - dflt: 2 / 3, - description: [ - 'Sets the scale factor determining the size of the', - 'projection marker points.' - ].join(' ') - } - }; -} - -module.exports = { - x: { - valType: 'data_array', - description: 'Sets the x coordinates.' + return { + show: { + valType: "boolean", + role: "info", + dflt: false, + description: [ + "Sets whether or not projections are shown along the", + axLetter, + "axis." + ].join(" ") }, - y: { - valType: 'data_array', - description: 'Sets the y coordinates.' - }, - z: { - valType: 'data_array', - description: 'Sets the z coordinates.' + opacity: { + valType: "number", + role: "style", + min: 0, + max: 1, + dflt: 1, + description: "Sets the projection color." }, + scale: { + valType: "number", + role: "style", + min: 0, + max: 10, + dflt: 2 / 3, + description: [ + "Sets the scale factor determining the size of the", + "projection marker points." + ].join(" ") + } + }; +} - text: extendFlat({}, scatterAttrs.text, { - description: [ - 'Sets text elements associated with each (x,y,z) triplet.', - 'If a single string, the same string appears over', - 'all the data points.', - 'If an array of string, the items are mapped in order to the', - 'this trace\'s (x,y,z) coordinates.' - ].join(' ') - }), - mode: extendFlat({}, scatterAttrs.mode, // shouldn't this be on-par with 2D? - {dflt: 'lines+markers'}), - surfaceaxis: { - valType: 'enumerated', - role: 'info', - values: [-1, 0, 1, 2], - dflt: -1, +module.exports = { + x: { valType: "data_array", description: "Sets the x coordinates." }, + y: { valType: "data_array", description: "Sets the y coordinates." }, + z: { valType: "data_array", description: "Sets the z coordinates." }, + text: extendFlat({}, scatterAttrs.text, { + description: [ + "Sets text elements associated with each (x,y,z) triplet.", + "If a single string, the same string appears over", + "all the data points.", + "If an array of string, the items are mapped in order to the", + "this trace's (x,y,z) coordinates." + ].join(" ") + }), + mode: extendFlat({}, scatterAttrs.mode, { dflt: "lines+markers" }), + // shouldn't this be on-par with 2D? + surfaceaxis: { + valType: "enumerated", + role: "info", + values: [-1, 0, 1, 2], + dflt: -1, + description: [ + "If *-1*, the scatter points are not fill with a surface", + "If *0*, *1*, *2*, the scatter points are filled with", + "a Delaunay surface about the x, y, z respectively." + ].join(" ") + }, + surfacecolor: { + valType: "color", + role: "style", + description: "Sets the surface fill color." + }, + projection: { + x: makeProjectionAttr("x"), + y: makeProjectionAttr("y"), + z: makeProjectionAttr("z") + }, + connectgaps: scatterAttrs.connectgaps, + line: extendFlat( + {}, + { + width: scatterLineAttrs.width, + dash: scatterLineAttrs.dash, + showscale: { + valType: "boolean", + role: "info", + dflt: false, description: [ - 'If *-1*, the scatter points are not fill with a surface', - 'If *0*, *1*, *2*, the scatter points are filled with', - 'a Delaunay surface about the x, y, z respectively.' - ].join(' ') - }, - surfacecolor: { - valType: 'color', - role: 'style', - description: 'Sets the surface fill color.' - }, - projection: { - x: makeProjectionAttr('x'), - y: makeProjectionAttr('y'), - z: makeProjectionAttr('z') - }, - connectgaps: scatterAttrs.connectgaps, - line: extendFlat({}, { - width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash, - showscale: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Has an effect only if `line.color` is set to a numerical array.', - 'Determines whether or not a colorbar is displayed.' - ].join(' ') - } + "Has an effect only if `line.color` is set to a numerical array.", + "Determines whether or not a colorbar is displayed." + ].join(" ") + } }, - colorAttributes('line') - ), - marker: extendFlat({}, { // Parity with scatter.js? - symbol: { - valType: 'enumerated', - values: Object.keys(MARKER_SYMBOLS), - role: 'style', - dflt: 'circle', - arrayOk: true, - description: 'Sets the marker symbol type.' + colorAttributes("line") + ), + marker: extendFlat( + {}, + { + // Parity with scatter.js? + symbol: { + valType: "enumerated", + values: Object.keys(MARKER_SYMBOLS), + role: "style", + dflt: "circle", + arrayOk: true, + description: "Sets the marker symbol type." + }, + size: extendFlat({}, scatterMarkerAttrs.size, { dflt: 8 }), + sizeref: scatterMarkerAttrs.sizeref, + sizemin: scatterMarkerAttrs.sizemin, + sizemode: scatterMarkerAttrs.sizemode, + opacity: extendFlat({}, scatterMarkerAttrs.opacity, { + arrayOk: false, + description: [ + "Sets the marker opacity.", + "Note that the marker opacity for scatter3d traces", + "must be a scalar value for performance reasons.", + "To set a blending opacity value", + "(i.e. which is not transparent), set *marker.color*", + "to an rgba color and use its alpha channel." + ].join(" ") + }), + showscale: scatterMarkerAttrs.showscale, + colorbar: scatterMarkerAttrs.colorbar, + line: extendFlat( + {}, + { + width: extendFlat({}, scatterMarkerLineAttrs.width, { + arrayOk: false + }) }, - size: extendFlat({}, scatterMarkerAttrs.size, {dflt: 8}), - sizeref: scatterMarkerAttrs.sizeref, - sizemin: scatterMarkerAttrs.sizemin, - sizemode: scatterMarkerAttrs.sizemode, - opacity: extendFlat({}, scatterMarkerAttrs.opacity, { - arrayOk: false, - description: [ - 'Sets the marker opacity.', - 'Note that the marker opacity for scatter3d traces', - 'must be a scalar value for performance reasons.', - 'To set a blending opacity value', - '(i.e. which is not transparent), set *marker.color*', - 'to an rgba color and use its alpha channel.' - ].join(' ') - }), - showscale: scatterMarkerAttrs.showscale, - colorbar: scatterMarkerAttrs.colorbar, - - line: extendFlat({}, - {width: extendFlat({}, scatterMarkerLineAttrs.width, {arrayOk: false})}, - colorAttributes('marker.line') - ) + colorAttributes("marker.line") + ) }, - colorAttributes('marker') - ), - - textposition: extendFlat({}, scatterAttrs.textposition, {dflt: 'top center'}), - textfont: scatterAttrs.textfont, - - error_x: errorBarAttrs, - error_y: errorBarAttrs, - error_z: errorBarAttrs, + colorAttributes("marker") + ), + textposition: extendFlat({}, scatterAttrs.textposition, { + dflt: "top center" + }), + textfont: scatterAttrs.textfont, + error_x: errorBarAttrs, + error_y: errorBarAttrs, + error_z: errorBarAttrs }; diff --git a/src/traces/scatter3d/calc.js b/src/traces/scatter3d/calc.js index 59ab3fb5bf7..6a28bce730e 100644 --- a/src/traces/scatter3d/calc.js +++ b/src/traces/scatter3d/calc.js @@ -5,12 +5,9 @@ * 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 arraysToCalcdata = require('../scatter/arrays_to_calcdata'); -var calcColorscales = require('../scatter/colorscale_calc'); - +"use strict"; +var arraysToCalcdata = require("../scatter/arrays_to_calcdata"); +var calcColorscales = require("../scatter/colorscale_calc"); /** * This is a kludge to put the array attributes into @@ -18,10 +15,10 @@ var calcColorscales = require('../scatter/colorscale_calc'); * popovers know what to do with them. */ module.exports = function calc(gd, trace) { - var cd = [{x: false, y: false, trace: trace, t: {}}]; + var cd = [{ x: false, y: false, trace: trace, t: {} }]; - arraysToCalcdata(cd, trace); - calcColorscales(trace); + arraysToCalcdata(cd, trace); + calcColorscales(trace); - return cd; + return cd; }; diff --git a/src/traces/scatter3d/calc_errors.js b/src/traces/scatter3d/calc_errors.js index 1e77154a2be..6cbf5204c5d 100644 --- a/src/traces/scatter3d/calc_errors.js +++ b/src/traces/scatter3d/calc_errors.js @@ -5,65 +5,58 @@ * 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 makeComputeError = require('../../components/errorbars/compute_error'); - +"use strict"; +var makeComputeError = require("../../components/errorbars/compute_error"); function calculateAxisErrors(data, params, scaleFactor) { - if(!params || !params.visible) return null; + if (!params || !params.visible) return null; - var computeError = makeComputeError(params); - var result = new Array(data.length); + var computeError = makeComputeError(params); + var result = new Array(data.length); - for(var i = 0; i < data.length; i++) { - var errors = computeError(+data[i], i); + for (var i = 0; i < data.length; i++) { + var errors = computeError(+data[i], i); - result[i] = [ - -errors[0] * scaleFactor, - errors[1] * scaleFactor - ]; - } + result[i] = [(-errors[0]) * scaleFactor, errors[1] * scaleFactor]; + } - return result; + return result; } function dataLength(array) { - for(var i = 0; i < array.length; i++) { - if(array[i]) return array[i].length; - } - return 0; + for (var i = 0; i < array.length; i++) { + if (array[i]) return array[i].length; + } + return 0; } function calculateErrors(data, scaleFactor) { - var errors = [ - calculateAxisErrors(data.x, data.error_x, scaleFactor[0]), - calculateAxisErrors(data.y, data.error_y, scaleFactor[1]), - calculateAxisErrors(data.z, data.error_z, scaleFactor[2]) - ]; + var errors = [ + calculateAxisErrors(data.x, data.error_x, scaleFactor[0]), + calculateAxisErrors(data.y, data.error_y, scaleFactor[1]), + calculateAxisErrors(data.z, data.error_z, scaleFactor[2]) + ]; - var n = dataLength(errors); - if(n === 0) return null; + var n = dataLength(errors); + if (n === 0) return null; - var errorBounds = new Array(n); + var errorBounds = new Array(n); - for(var i = 0; i < n; i++) { - var bound = [[0, 0, 0], [0, 0, 0]]; + for (var i = 0; i < n; i++) { + var bound = [[0, 0, 0], [0, 0, 0]]; - for(var j = 0; j < 3; j++) { - if(errors[j]) { - for(var k = 0; k < 2; k++) { - bound[k][j] = errors[j][i][k]; - } - } + for (var j = 0; j < 3; j++) { + if (errors[j]) { + for (var k = 0; k < 2; k++) { + bound[k][j] = errors[j][i][k]; } - - errorBounds[i] = bound; + } } - return errorBounds; + errorBounds[i] = bound; + } + + return errorBounds; } module.exports = calculateErrors; diff --git a/src/traces/scatter3d/convert.js b/src/traces/scatter3d/convert.js index fa1a0a8f2dd..ff96020da5b 100644 --- a/src/traces/scatter3d/convert.js +++ b/src/traces/scatter3d/convert.js @@ -5,456 +5,475 @@ * 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 createLinePlot = require('gl-line3d'); -var createScatterPlot = require('gl-scatter3d'); -var createErrorBars = require('gl-error3d'); -var createMesh = require('gl-mesh3d'); -var triangulate = require('delaunay-triangulate'); - -var Lib = require('../../lib'); -var str2RgbaArray = require('../../lib/str2rgbarray'); -var formatColor = require('../../lib/gl_format_color'); -var makeBubbleSizeFn = require('../scatter/make_bubble_size_func'); -var DASH_PATTERNS = require('../../constants/gl3d_dashes'); -var MARKER_SYMBOLS = require('../../constants/gl_markers'); - -var calculateError = require('./calc_errors'); +"use strict"; +var createLinePlot = require("gl-line3d"); +var createScatterPlot = require("gl-scatter3d"); +var createErrorBars = require("gl-error3d"); +var createMesh = require("gl-mesh3d"); +var triangulate = require("delaunay-triangulate"); + +var Lib = require("../../lib"); +var str2RgbaArray = require("../../lib/str2rgbarray"); +var formatColor = require("../../lib/gl_format_color"); +var makeBubbleSizeFn = require("../scatter/make_bubble_size_func"); +var DASH_PATTERNS = require("../../constants/gl3d_dashes"); +var MARKER_SYMBOLS = require("../../constants/gl_markers"); + +var calculateError = require("./calc_errors"); function LineWithMarkers(scene, uid) { - this.scene = scene; - this.uid = uid; - this.linePlot = null; - this.scatterPlot = null; - this.errorBars = null; - this.textMarkers = null; - this.delaunayMesh = null; - this.color = null; - this.mode = ''; - this.dataPoints = []; - this.axesBounds = [ - [-Infinity, -Infinity, -Infinity], - [Infinity, Infinity, Infinity] - ]; - this.textLabels = null; - this.data = null; + this.scene = scene; + this.uid = uid; + this.linePlot = null; + this.scatterPlot = null; + this.errorBars = null; + this.textMarkers = null; + this.delaunayMesh = null; + this.color = null; + this.mode = ""; + this.dataPoints = []; + this.axesBounds = [ + [-Infinity, -Infinity, -Infinity], + [Infinity, Infinity, Infinity] + ]; + this.textLabels = null; + this.data = null; } var proto = LineWithMarkers.prototype; proto.handlePick = function(selection) { - if(selection.object && - (selection.object === this.linePlot || - selection.object === this.delaunayMesh || - selection.object === this.textMarkers || - selection.object === this.scatterPlot)) { - if(selection.object.highlight) { - selection.object.highlight(null); - } - if(this.scatterPlot) { - selection.object = this.scatterPlot; - this.scatterPlot.highlight(selection.data); - } - if(this.textLabels && this.textLabels[selection.data.index] !== undefined) { - selection.textLabel = this.textLabels[selection.data.index]; - } - else selection.textLabel = ''; - - var selectIndex = selection.data.index; - selection.traceCoordinate = [ - this.data.x[selectIndex], - this.data.y[selectIndex], - this.data.z[selectIndex] - ]; - - return true; + if ( + selection.object && + (selection.object === this.linePlot || + selection.object === this.delaunayMesh || + selection.object === this.textMarkers || + selection.object === this.scatterPlot) + ) { + if (selection.object.highlight) { + selection.object.highlight(null); } + if (this.scatterPlot) { + selection.object = this.scatterPlot; + this.scatterPlot.highlight(selection.data); + } + if ( + this.textLabels && this.textLabels[selection.data.index] !== undefined + ) { + selection.textLabel = this.textLabels[selection.data.index]; + } else { + selection.textLabel = ""; + } + + var selectIndex = selection.data.index; + selection.traceCoordinate = [ + this.data.x[selectIndex], + this.data.y[selectIndex], + this.data.z[selectIndex] + ]; + + return true; + } }; function constructDelaunay(points, color, axis) { - var u = (axis + 1) % 3; - var v = (axis + 2) % 3; - var filteredPoints = []; - var filteredIds = []; - var i; - - for(i = 0; i < points.length; ++i) { - var p = points[i]; - if(isNaN(p[u]) || !isFinite(p[u]) || - isNaN(p[v]) || !isFinite(p[v])) { - continue; - } - filteredPoints.push([p[u], p[v]]); - filteredIds.push(i); + var u = (axis + 1) % 3; + var v = (axis + 2) % 3; + var filteredPoints = []; + var filteredIds = []; + var i; + + for (i = 0; i < points.length; ++i) { + var p = points[i]; + if (isNaN(p[u]) || !isFinite(p[u]) || isNaN(p[v]) || !isFinite(p[v])) { + continue; } - var cells = triangulate(filteredPoints); - for(i = 0; i < cells.length; ++i) { - var c = cells[i]; - for(var j = 0; j < c.length; ++j) { - c[j] = filteredIds[c[j]]; - } + filteredPoints.push([p[u], p[v]]); + filteredIds.push(i); + } + var cells = triangulate(filteredPoints); + for (i = 0; i < cells.length; ++i) { + var c = cells[i]; + for (var j = 0; j < c.length; ++j) { + c[j] = filteredIds[c[j]]; } - return { - positions: points, - cells: cells, - meshColor: color - }; + } + return { positions: points, cells: cells, meshColor: color }; } function calculateErrorParams(errors) { - var capSize = [0.0, 0.0, 0.0], - color = [[0, 0, 0], [0, 0, 0], [0, 0, 0]], - lineWidth = [0.0, 0.0, 0.0]; - - for(var i = 0; i < 3; i++) { - var e = errors[i]; + var capSize = [0.0, 0.0, 0.0], + color = [[0, 0, 0], [0, 0, 0], [0, 0, 0]], + lineWidth = [0.0, 0.0, 0.0]; - if(e && e.copy_zstyle !== false) e = errors[2]; - if(!e) continue; + for (var i = 0; i < 3; i++) { + var e = errors[i]; - capSize[i] = e.width / 2; // ballpark rescaling - color[i] = str2RgbaArray(e.color); - lineWidth = e.thickness; + if (e && e.copy_zstyle !== false) e = errors[2]; + if (!e) continue; - } + capSize[i] = e.width / 2; + // ballpark rescaling + color[i] = str2RgbaArray(e.color); + lineWidth = e.thickness; + } - return {capSize: capSize, color: color, lineWidth: lineWidth}; + return { capSize: capSize, color: color, lineWidth: lineWidth }; } function calculateTextOffset(tp) { - // Read out text properties - var textOffset = [0, 0]; - if(Array.isArray(tp)) return [0, -1]; - if(tp.indexOf('bottom') >= 0) textOffset[1] += 1; - if(tp.indexOf('top') >= 0) textOffset[1] -= 1; - if(tp.indexOf('left') >= 0) textOffset[0] -= 1; - if(tp.indexOf('right') >= 0) textOffset[0] += 1; - return textOffset; + // Read out text properties + var textOffset = [0, 0]; + if (Array.isArray(tp)) return [0, -1]; + if (tp.indexOf("bottom") >= 0) textOffset[1] += 1; + if (tp.indexOf("top") >= 0) textOffset[1] -= 1; + if (tp.indexOf("left") >= 0) textOffset[0] -= 1; + if (tp.indexOf("right") >= 0) textOffset[0] += 1; + return textOffset; } - function calculateSize(sizeIn, sizeFn) { - // rough parity with Plotly 2D markers - return sizeFn(sizeIn * 4); + // rough parity with Plotly 2D markers + return sizeFn(sizeIn * 4); } function calculateSymbol(symbolIn) { - return MARKER_SYMBOLS[symbolIn]; + return MARKER_SYMBOLS[symbolIn]; } function formatParam(paramIn, len, calculate, dflt, extraFn) { - var paramOut = null; - - if(Array.isArray(paramIn)) { - paramOut = []; + var paramOut = null; - for(var i = 0; i < len; i++) { - if(paramIn[i] === undefined) paramOut[i] = dflt; - else paramOut[i] = calculate(paramIn[i], extraFn); - } + if (Array.isArray(paramIn)) { + paramOut = []; + for (var i = 0; i < len; i++) { + if (paramIn[i] === undefined) paramOut[i] = dflt; + else paramOut[i] = calculate(paramIn[i], extraFn); } - else paramOut = calculate(paramIn, Lib.identity); + } else { + paramOut = calculate(paramIn, Lib.identity); + } - return paramOut; + return paramOut; } - function convertPlotlyOptions(scene, data) { - var params, i, - points = [], - sceneLayout = scene.fullSceneLayout, - scaleFactor = scene.dataScale, - xaxis = sceneLayout.xaxis, - yaxis = sceneLayout.yaxis, - zaxis = sceneLayout.zaxis, - marker = data.marker, - line = data.line, - xc, x = data.x || [], - yc, y = data.y || [], - zc, z = data.z || [], - len = x.length, - xcalendar = data.xcalendar, - ycalendar = data.ycalendar, - zcalendar = data.zcalendar, - text; - - // Convert points - for(i = 0; i < len; i++) { - // sanitize numbers and apply transforms based on axes.type - xc = xaxis.d2l(x[i], 0, xcalendar) * scaleFactor[0]; - yc = yaxis.d2l(y[i], 0, ycalendar) * scaleFactor[1]; - zc = zaxis.d2l(z[i], 0, zcalendar) * scaleFactor[2]; - - points[i] = [xc, yc, zc]; - } - - // convert text - if(Array.isArray(data.text)) text = data.text; - else if(data.text !== undefined) { - text = new Array(len); - for(i = 0; i < len; i++) text[i] = data.text; + var params, + i, + points = [], + sceneLayout = scene.fullSceneLayout, + scaleFactor = scene.dataScale, + xaxis = sceneLayout.xaxis, + yaxis = sceneLayout.yaxis, + zaxis = sceneLayout.zaxis, + marker = data.marker, + line = data.line, + xc, + x = data.x || [], + yc, + y = data.y || [], + zc, + z = data.z || [], + len = x.length, + xcalendar = data.xcalendar, + ycalendar = data.ycalendar, + zcalendar = data.zcalendar, + text; + + // Convert points + for (i = 0; i < len; i++) { + // sanitize numbers and apply transforms based on axes.type + xc = xaxis.d2l(x[i], 0, xcalendar) * scaleFactor[0]; + yc = yaxis.d2l(y[i], 0, ycalendar) * scaleFactor[1]; + zc = zaxis.d2l(z[i], 0, zcalendar) * scaleFactor[2]; + + points[i] = [xc, yc, zc]; + } + + // convert text + if (Array.isArray(data.text)) { + text = data.text; + } else if (data.text !== undefined) { + text = new Array(len); + for (i = 0; i < len; i++) { + text[i] = data.text; } - - // Build object parameters - params = { - position: points, - mode: data.mode, - text: text - }; - - if('line' in data) { - params.lineColor = formatColor(line, 1, len); - params.lineWidth = line.width; - params.lineDashes = line.dash; + } + + // Build object parameters + params = { position: points, mode: data.mode, text: text }; + + if ("line" in data) { + params.lineColor = formatColor(line, 1, len); + params.lineWidth = line.width; + params.lineDashes = line.dash; + } + + if ("marker" in data) { + var sizeFn = makeBubbleSizeFn(data); + + params.scatterColor = formatColor(marker, 1, len); + params.scatterSize = formatParam( + marker.size, + len, + calculateSize, + 20, + sizeFn + ); + params.scatterMarker = formatParam( + marker.symbol, + len, + calculateSymbol, + "\u25CF" + ); + params.scatterLineWidth = marker.line.width; + // arrayOk === false + params.scatterLineColor = formatColor(marker.line, 1, len); + params.scatterAngle = 0; + } + + if ("textposition" in data) { + params.textOffset = calculateTextOffset(data.textposition); + // arrayOk === false + params.textColor = formatColor(data.textfont, 1, len); + params.textSize = formatParam(data.textfont.size, len, Lib.identity, 12); + params.textFont = data.textfont.family; + // arrayOk === false + params.textAngle = 0; + } + + var dims = ["x", "y", "z"]; + params.project = [false, false, false]; + params.projectScale = [1, 1, 1]; + params.projectOpacity = [1, 1, 1]; + for (i = 0; i < 3; ++i) { + var projection = data.projection[dims[i]]; + if (params.project[i] = projection.show) { + params.projectOpacity[i] = projection.opacity; + params.projectScale[i] = projection.scale; } + } - if('marker' in data) { - var sizeFn = makeBubbleSizeFn(data); + params.errorBounds = calculateError(data, scaleFactor); - params.scatterColor = formatColor(marker, 1, len); - params.scatterSize = formatParam(marker.size, len, calculateSize, 20, sizeFn); - params.scatterMarker = formatParam(marker.symbol, len, calculateSymbol, '●'); - params.scatterLineWidth = marker.line.width; // arrayOk === false - params.scatterLineColor = formatColor(marker.line, 1, len); - params.scatterAngle = 0; - } + var errorParams = calculateErrorParams([ + data.error_x, + data.error_y, + data.error_z + ]); + params.errorColor = errorParams.color; + params.errorLineWidth = errorParams.lineWidth; + params.errorCapSize = errorParams.capSize; - if('textposition' in data) { - params.textOffset = calculateTextOffset(data.textposition); // arrayOk === false - params.textColor = formatColor(data.textfont, 1, len); - params.textSize = formatParam(data.textfont.size, len, Lib.identity, 12); - params.textFont = data.textfont.family; // arrayOk === false - params.textAngle = 0; - } + params.delaunayAxis = data.surfaceaxis; + params.delaunayColor = str2RgbaArray(data.surfacecolor); - var dims = ['x', 'y', 'z']; - params.project = [false, false, false]; - params.projectScale = [1, 1, 1]; - params.projectOpacity = [1, 1, 1]; - for(i = 0; i < 3; ++i) { - var projection = data.projection[dims[i]]; - if((params.project[i] = projection.show)) { - params.projectOpacity[i] = projection.opacity; - params.projectScale[i] = projection.scale; - } - } - - params.errorBounds = calculateError(data, scaleFactor); - - var errorParams = calculateErrorParams([data.error_x, data.error_y, data.error_z]); - params.errorColor = errorParams.color; - params.errorLineWidth = errorParams.lineWidth; - params.errorCapSize = errorParams.capSize; - - params.delaunayAxis = data.surfaceaxis; - params.delaunayColor = str2RgbaArray(data.surfacecolor); - - return params; + return params; } function arrayToColor(color) { - if(Array.isArray(color)) { - var c = color[0]; + if (Array.isArray(color)) { + var c = color[0]; - if(Array.isArray(c)) color = c; + if (Array.isArray(c)) color = c; - return 'rgb(' + color.slice(0, 3).map(function(x) { - return Math.round(x * 255); - }) + ')'; - } + return "rgb(" + color.slice(0, 3).map(function(x) { + return Math.round(x * 255); + }) + ")"; + } - return null; + return null; } proto.update = function(data) { - var gl = this.scene.glplot.gl, - lineOptions, - scatterOptions, - errorOptions, - textOptions, - dashPattern = DASH_PATTERNS.solid; - - // Save data - this.data = data; - - // Run data conversion - var options = convertPlotlyOptions(this.scene, data); - - if('mode' in options) { - this.mode = options.mode; + var gl = this.scene.glplot.gl, + lineOptions, + scatterOptions, + errorOptions, + textOptions, + dashPattern = DASH_PATTERNS.solid; + + // Save data + this.data = data; + + // Run data conversion + var options = convertPlotlyOptions(this.scene, data); + + if ("mode" in options) { + this.mode = options.mode; + } + if ("lineDashes" in options) { + if (options.lineDashes in DASH_PATTERNS) { + dashPattern = DASH_PATTERNS[options.lineDashes]; } - if('lineDashes' in options) { - if(options.lineDashes in DASH_PATTERNS) { - dashPattern = DASH_PATTERNS[options.lineDashes]; - } + } + + this.color = arrayToColor(options.scatterColor) || + arrayToColor(options.lineColor); + + // Save data points + this.dataPoints = options.position; + + lineOptions = { + gl: gl, + position: options.position, + color: options.lineColor, + lineWidth: options.lineWidth || 1, + dashes: dashPattern[0], + dashScale: dashPattern[1], + opacity: data.opacity, + connectGaps: data.connectgaps + }; + + if (this.mode.indexOf("lines") !== -1) { + if (this.linePlot) { + this.linePlot.update(lineOptions); + } else { + this.linePlot = createLinePlot(lineOptions); + this.scene.glplot.add(this.linePlot); } - - this.color = arrayToColor(options.scatterColor) || - arrayToColor(options.lineColor); - - // Save data points - this.dataPoints = options.position; - - lineOptions = { - gl: gl, - position: options.position, - color: options.lineColor, - lineWidth: options.lineWidth || 1, - dashes: dashPattern[0], - dashScale: dashPattern[1], - opacity: data.opacity, - connectGaps: data.connectgaps - }; - - if(this.mode.indexOf('lines') !== -1) { - if(this.linePlot) this.linePlot.update(lineOptions); - else { - this.linePlot = createLinePlot(lineOptions); - this.scene.glplot.add(this.linePlot); - } - } else if(this.linePlot) { - this.scene.glplot.remove(this.linePlot); - this.linePlot.dispose(); - this.linePlot = null; - } - - // N.B. marker.opacity must be a scalar for performance - var scatterOpacity = data.opacity; - if(data.marker && data.marker.opacity) scatterOpacity *= data.marker.opacity; - - scatterOptions = { - gl: gl, - position: options.position, - color: options.scatterColor, - size: options.scatterSize, - glyph: options.scatterMarker, - opacity: scatterOpacity, - orthographic: true, - lineWidth: options.scatterLineWidth, - lineColor: options.scatterLineColor, - project: options.project, - projectScale: options.projectScale, - projectOpacity: options.projectOpacity - }; - - if(this.mode.indexOf('markers') !== -1) { - if(this.scatterPlot) this.scatterPlot.update(scatterOptions); - else { - this.scatterPlot = createScatterPlot(scatterOptions); - this.scatterPlot.highlightScale = 1; - this.scene.glplot.add(this.scatterPlot); - } - } else if(this.scatterPlot) { - this.scene.glplot.remove(this.scatterPlot); - this.scatterPlot.dispose(); - this.scatterPlot = null; + } else if (this.linePlot) { + this.scene.glplot.remove(this.linePlot); + this.linePlot.dispose(); + this.linePlot = null; + } + + // N.B. marker.opacity must be a scalar for performance + var scatterOpacity = data.opacity; + if (data.marker && data.marker.opacity) scatterOpacity *= data.marker.opacity; + + scatterOptions = { + gl: gl, + position: options.position, + color: options.scatterColor, + size: options.scatterSize, + glyph: options.scatterMarker, + opacity: scatterOpacity, + orthographic: true, + lineWidth: options.scatterLineWidth, + lineColor: options.scatterLineColor, + project: options.project, + projectScale: options.projectScale, + projectOpacity: options.projectOpacity + }; + + if (this.mode.indexOf("markers") !== -1) { + if (this.scatterPlot) { + this.scatterPlot.update(scatterOptions); + } else { + this.scatterPlot = createScatterPlot(scatterOptions); + this.scatterPlot.highlightScale = 1; + this.scene.glplot.add(this.scatterPlot); } - - textOptions = { - gl: gl, - position: options.position, - glyph: options.text, - color: options.textColor, - size: options.textSize, - angle: options.textAngle, - alignment: options.textOffset, - font: options.textFont, - orthographic: true, - lineWidth: 0, - project: false, - opacity: data.opacity - }; - - this.textLabels = options.text; - - if(this.mode.indexOf('text') !== -1) { - if(this.textMarkers) this.textMarkers.update(textOptions); - else { - this.textMarkers = createScatterPlot(textOptions); - this.textMarkers.highlightScale = 1; - this.scene.glplot.add(this.textMarkers); - } - } else if(this.textMarkers) { - this.scene.glplot.remove(this.textMarkers); - this.textMarkers.dispose(); - this.textMarkers = null; + } else if (this.scatterPlot) { + this.scene.glplot.remove(this.scatterPlot); + this.scatterPlot.dispose(); + this.scatterPlot = null; + } + + textOptions = { + gl: gl, + position: options.position, + glyph: options.text, + color: options.textColor, + size: options.textSize, + angle: options.textAngle, + alignment: options.textOffset, + font: options.textFont, + orthographic: true, + lineWidth: 0, + project: false, + opacity: data.opacity + }; + + this.textLabels = options.text; + + if (this.mode.indexOf("text") !== -1) { + if (this.textMarkers) { + this.textMarkers.update(textOptions); + } else { + this.textMarkers = createScatterPlot(textOptions); + this.textMarkers.highlightScale = 1; + this.scene.glplot.add(this.textMarkers); } - - errorOptions = { - gl: gl, - position: options.position, - color: options.errorColor, - error: options.errorBounds, - lineWidth: options.errorLineWidth, - capSize: options.errorCapSize, - opacity: data.opacity - }; - if(this.errorBars) { - if(options.errorBounds) { - this.errorBars.update(errorOptions); - } else { - this.scene.glplot.remove(this.errorBars); - this.errorBars.dispose(); - this.errorBars = null; - } - } else if(options.errorBounds) { - this.errorBars = createErrorBars(errorOptions); - this.scene.glplot.add(this.errorBars); + } else if (this.textMarkers) { + this.scene.glplot.remove(this.textMarkers); + this.textMarkers.dispose(); + this.textMarkers = null; + } + + errorOptions = { + gl: gl, + position: options.position, + color: options.errorColor, + error: options.errorBounds, + lineWidth: options.errorLineWidth, + capSize: options.errorCapSize, + opacity: data.opacity + }; + if (this.errorBars) { + if (options.errorBounds) { + this.errorBars.update(errorOptions); + } else { + this.scene.glplot.remove(this.errorBars); + this.errorBars.dispose(); + this.errorBars = null; } - - if(options.delaunayAxis >= 0) { - var delaunayOptions = constructDelaunay( - options.position, - options.delaunayColor, - options.delaunayAxis - ); - delaunayOptions.opacity = data.opacity; - - if(this.delaunayMesh) { - this.delaunayMesh.update(delaunayOptions); - } else { - delaunayOptions.gl = gl; - this.delaunayMesh = createMesh(delaunayOptions); - this.scene.glplot.add(this.delaunayMesh); - } - } else if(this.delaunayMesh) { - this.scene.glplot.remove(this.delaunayMesh); - this.delaunayMesh.dispose(); - this.delaunayMesh = null; + } else if (options.errorBounds) { + this.errorBars = createErrorBars(errorOptions); + this.scene.glplot.add(this.errorBars); + } + + if (options.delaunayAxis >= 0) { + var delaunayOptions = constructDelaunay( + options.position, + options.delaunayColor, + options.delaunayAxis + ); + delaunayOptions.opacity = data.opacity; + + if (this.delaunayMesh) { + this.delaunayMesh.update(delaunayOptions); + } else { + delaunayOptions.gl = gl; + this.delaunayMesh = createMesh(delaunayOptions); + this.scene.glplot.add(this.delaunayMesh); } + } else if (this.delaunayMesh) { + this.scene.glplot.remove(this.delaunayMesh); + this.delaunayMesh.dispose(); + this.delaunayMesh = null; + } }; proto.dispose = function() { - if(this.linePlot) { - this.scene.glplot.remove(this.linePlot); - this.linePlot.dispose(); - } - if(this.scatterPlot) { - this.scene.glplot.remove(this.scatterPlot); - this.scatterPlot.dispose(); - } - if(this.errorBars) { - this.scene.glplot.remove(this.errorBars); - this.errorBars.dispose(); - } - if(this.textMarkers) { - this.scene.glplot.remove(this.textMarkers); - this.textMarkers.dispose(); - } - if(this.delaunayMesh) { - this.scene.glplot.remove(this.delaunayMesh); - this.delaunayMesh.dispose(); - } + if (this.linePlot) { + this.scene.glplot.remove(this.linePlot); + this.linePlot.dispose(); + } + if (this.scatterPlot) { + this.scene.glplot.remove(this.scatterPlot); + this.scatterPlot.dispose(); + } + if (this.errorBars) { + this.scene.glplot.remove(this.errorBars); + this.errorBars.dispose(); + } + if (this.textMarkers) { + this.scene.glplot.remove(this.textMarkers); + this.textMarkers.dispose(); + } + if (this.delaunayMesh) { + this.scene.glplot.remove(this.delaunayMesh); + this.delaunayMesh.dispose(); + } }; function createLineWithMarkers(scene, data) { - var plot = new LineWithMarkers(scene, data.uid); - plot.update(data); - return plot; + var plot = new LineWithMarkers(scene, data.uid); + plot.update(data); + return plot; } module.exports = createLineWithMarkers; diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js index 4f62fd3c1d8..920dbcb5eaa 100644 --- a/src/traces/scatter3d/defaults.js +++ b/src/traces/scatter3d/defaults.js @@ -5,83 +5,91 @@ * 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 Registry = require('../../registry'); -var Lib = require('../../lib'); - -var subTypes = require('../scatter/subtypes'); -var handleMarkerDefaults = require('../scatter/marker_defaults'); -var handleLineDefaults = require('../scatter/line_defaults'); -var handleTextDefaults = require('../scatter/text_defaults'); -var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); - -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); - if(!len) { - traceOut.visible = false; - return; - } - - coerce('text'); - coerce('mode'); - - if(subTypes.hasLines(traceOut)) { - coerce('connectgaps'); - handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); +"use strict"; +var Registry = require("../../registry"); +var Lib = require("../../lib"); + +var subTypes = require("../scatter/subtypes"); +var handleMarkerDefaults = require("../scatter/marker_defaults"); +var handleLineDefaults = require("../scatter/line_defaults"); +var handleTextDefaults = require("../scatter/text_defaults"); +var errorBarsSupplyDefaults = require("../../components/errorbars/defaults"); + +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); + if (!len) { + traceOut.visible = false; + return; + } + + coerce("text"); + coerce("mode"); + + if (subTypes.hasLines(traceOut)) { + coerce("connectgaps"); + handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + if (subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + if (subTypes.hasText(traceOut)) { + handleTextDefaults(traceIn, traceOut, layout, coerce); + } + + var lineColor = (traceOut.line || {}).color, + markerColor = (traceOut.marker || {}).color; + if (coerce("surfaceaxis") >= 0) { + coerce("surfacecolor", lineColor || markerColor); + } + + var dims = ["x", "y", "z"]; + for (var i = 0; i < 3; ++i) { + var projection = "projection." + dims[i]; + if (coerce(projection + ".show")) { + coerce(projection + ".opacity"); + coerce(projection + ".scale"); } - - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); - } - - if(subTypes.hasText(traceOut)) { - handleTextDefaults(traceIn, traceOut, layout, coerce); - } - - var lineColor = (traceOut.line || {}).color, - markerColor = (traceOut.marker || {}).color; - if(coerce('surfaceaxis') >= 0) coerce('surfacecolor', lineColor || markerColor); - - var dims = ['x', 'y', 'z']; - for(var i = 0; i < 3; ++i) { - var projection = 'projection.' + dims[i]; - if(coerce(projection + '.show')) { - coerce(projection + '.opacity'); - coerce(projection + '.scale'); - } - } - - errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'z'}); - errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y', inherit: 'z'}); - errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'z'}); + } + + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, { axis: "z" }); + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, { + axis: "y", + inherit: "z" + }); + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, { + axis: "x", + inherit: "z" + }); }; function handleXYZDefaults(traceIn, traceOut, coerce, layout) { - var len = 0, - x = coerce('x'), - y = coerce('y'), - z = coerce('z'); - - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); - handleCalendarDefaults(traceIn, traceOut, ['x', 'y', 'z'], layout); - - if(x && y && z) { - len = Math.min(x.length, y.length, z.length); - if(len < x.length) traceOut.x = x.slice(0, len); - if(len < y.length) traceOut.y = y.slice(0, len); - if(len < z.length) traceOut.z = z.slice(0, len); - } - - return len; + var len = 0, x = coerce("x"), y = coerce("y"), z = coerce("z"); + + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleTraceDefaults" + ); + handleCalendarDefaults(traceIn, traceOut, ["x", "y", "z"], layout); + + if (x && y && z) { + len = Math.min(x.length, y.length, z.length); + if (len < x.length) traceOut.x = x.slice(0, len); + if (len < y.length) traceOut.y = y.slice(0, len); + if (len < z.length) traceOut.z = z.slice(0, len); + } + + return len; } diff --git a/src/traces/scatter3d/index.js b/src/traces/scatter3d/index.js index e1399198c77..a70cc6bbdcd 100644 --- a/src/traces/scatter3d/index.js +++ b/src/traces/scatter3d/index.js @@ -5,32 +5,30 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; var Scatter3D = {}; -Scatter3D.plot = require('./convert'); -Scatter3D.attributes = require('./attributes'); -Scatter3D.markerSymbols = require('../../constants/gl_markers'); -Scatter3D.supplyDefaults = require('./defaults'); -Scatter3D.colorbar = require('../scatter/colorbar'); -Scatter3D.calc = require('./calc'); +Scatter3D.plot = require("./convert"); +Scatter3D.attributes = require("./attributes"); +Scatter3D.markerSymbols = require("../../constants/gl_markers"); +Scatter3D.supplyDefaults = require("./defaults"); +Scatter3D.colorbar = require("../scatter/colorbar"); +Scatter3D.calc = require("./calc"); -Scatter3D.moduleType = 'trace'; -Scatter3D.name = 'scatter3d'; -Scatter3D.basePlotModule = require('../../plots/gl3d'); -Scatter3D.categories = ['gl3d', 'symbols', 'markerColorscale', 'showLegend']; +Scatter3D.moduleType = "trace"; +Scatter3D.name = "scatter3d"; +Scatter3D.basePlotModule = require("../../plots/gl3d"); +Scatter3D.categories = ["gl3d", "symbols", "markerColorscale", "showLegend"]; Scatter3D.meta = { - hrName: 'scatter_3d', - description: [ - 'The data visualized as scatter point or lines in 3D dimension', - 'is set in `x`, `y`, `z`.', - 'Text (appearing either on the chart or on hover only) is via `text`.', - 'Bubble charts are achieved by setting `marker.size` and/or `marker.color`', - 'Projections are achieved via `projection`.', - 'Surface fills are achieved via `surfaceaxis`.' - ].join(' ') + hrName: "scatter_3d", + description: [ + "The data visualized as scatter point or lines in 3D dimension", + "is set in `x`, `y`, `z`.", + "Text (appearing either on the chart or on hover only) is via `text`.", + "Bubble charts are achieved by setting `marker.size` and/or `marker.color`", + "Projections are achieved via `projection`.", + "Surface fills are achieved via `surfaceaxis`." + ].join(" ") }; module.exports = Scatter3D; diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js index 464354e35b0..99b957f9cea 100644 --- a/src/traces/scattergeo/attributes.js +++ b/src/traces/scattergeo/attributes.js @@ -5,102 +5,96 @@ * 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 scatterAttrs = require("../scatter/attributes"); +var plotAttrs = require("../../plots/attributes"); +var colorAttributes = require("../../components/colorscale/color_attributes"); -'use strict'; - -var scatterAttrs = require('../scatter/attributes'); -var plotAttrs = require('../../plots/attributes'); -var colorAttributes = require('../../components/colorscale/color_attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; +var extendFlat = require("../../lib/extend").extendFlat; var scatterMarkerAttrs = scatterAttrs.marker, - scatterLineAttrs = scatterAttrs.line, - scatterMarkerLineAttrs = scatterMarkerAttrs.line; + scatterLineAttrs = scatterAttrs.line, + scatterMarkerLineAttrs = scatterMarkerAttrs.line; module.exports = { - lon: { - valType: 'data_array', - description: 'Sets the longitude coordinates (in degrees East).' - }, - lat: { - valType: 'data_array', - description: 'Sets the latitude coordinates (in degrees North).' - }, - - locations: { - valType: 'data_array', - description: [ - 'Sets the coordinates via location IDs or names.', - 'Coordinates correspond to the centroid of each location given.', - 'See `locationmode` for more info.' - ].join(' ') - }, - locationmode: { - valType: 'enumerated', - values: ['ISO-3', 'USA-states', 'country names'], - role: 'info', - dflt: 'ISO-3', - description: [ - 'Determines the set of locations used to match entries in `locations`', - 'to regions on the map.' - ].join(' ') + lon: { + valType: "data_array", + description: "Sets the longitude coordinates (in degrees East)." + }, + lat: { + valType: "data_array", + description: "Sets the latitude coordinates (in degrees North)." + }, + locations: { + valType: "data_array", + description: [ + "Sets the coordinates via location IDs or names.", + "Coordinates correspond to the centroid of each location given.", + "See `locationmode` for more info." + ].join(" ") + }, + locationmode: { + valType: "enumerated", + values: ["ISO-3", "USA-states", "country names"], + role: "info", + dflt: "ISO-3", + description: [ + "Determines the set of locations used to match entries in `locations`", + "to regions on the map." + ].join(" ") + }, + mode: extendFlat({}, scatterAttrs.mode, { dflt: "markers" }), + text: extendFlat({}, scatterAttrs.text, { + description: [ + "Sets text elements associated with each (lon,lat) pair", + "or item in `locations`.", + "If a single string, the same string appears over", + "all the data points.", + "If an array of string, the items are mapped in order to the", + "this trace's (lon,lat) or `locations` coordinates." + ].join(" ") + }), + textfont: scatterAttrs.textfont, + textposition: scatterAttrs.textposition, + line: { + color: scatterLineAttrs.color, + width: scatterLineAttrs.width, + dash: scatterLineAttrs.dash + }, + connectgaps: scatterAttrs.connectgaps, + marker: extendFlat( + {}, + { + symbol: scatterMarkerAttrs.symbol, + opacity: scatterMarkerAttrs.opacity, + size: scatterMarkerAttrs.size, + sizeref: scatterMarkerAttrs.sizeref, + sizemin: scatterMarkerAttrs.sizemin, + sizemode: scatterMarkerAttrs.sizemode, + showscale: scatterMarkerAttrs.showscale, + colorbar: scatterMarkerAttrs.colorbar, + line: extendFlat( + {}, + { width: scatterMarkerLineAttrs.width }, + colorAttributes("marker.line") + ) }, - - mode: extendFlat({}, scatterAttrs.mode, {dflt: 'markers'}), - - text: extendFlat({}, scatterAttrs.text, { - description: [ - 'Sets text elements associated with each (lon,lat) pair', - 'or item in `locations`.', - 'If a single string, the same string appears over', - 'all the data points.', - 'If an array of string, the items are mapped in order to the', - 'this trace\'s (lon,lat) or `locations` coordinates.' - ].join(' ') - }), - textfont: scatterAttrs.textfont, - textposition: scatterAttrs.textposition, - - line: { - color: scatterLineAttrs.color, - width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash - }, - connectgaps: scatterAttrs.connectgaps, - - marker: extendFlat({}, { - symbol: scatterMarkerAttrs.symbol, - opacity: scatterMarkerAttrs.opacity, - size: scatterMarkerAttrs.size, - sizeref: scatterMarkerAttrs.sizeref, - sizemin: scatterMarkerAttrs.sizemin, - sizemode: scatterMarkerAttrs.sizemode, - showscale: scatterMarkerAttrs.showscale, - colorbar: scatterMarkerAttrs.colorbar, - line: extendFlat({}, - {width: scatterMarkerLineAttrs.width}, - colorAttributes('marker.line') - ) - }, - colorAttributes('marker') - ), - - fill: { - valType: 'enumerated', - values: ['none', 'toself'], - dflt: 'none', - role: 'style', - description: [ - 'Sets the area to fill with a solid color.', - 'Use with `fillcolor` if not *none*.', - '*toself* connects the endpoints of the trace (or each segment', - 'of the trace if it has gaps) into a closed shape.' - ].join(' ') - }, - fillcolor: scatterAttrs.fillcolor, - - hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { - flags: ['lon', 'lat', 'location', 'text', 'name'] - }) + colorAttributes("marker") + ), + fill: { + valType: "enumerated", + values: ["none", "toself"], + dflt: "none", + role: "style", + description: [ + "Sets the area to fill with a solid color.", + "Use with `fillcolor` if not *none*.", + "*toself* connects the endpoints of the trace (or each segment", + "of the trace if it has gaps) into a closed shape." + ].join(" ") + }, + fillcolor: scatterAttrs.fillcolor, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { + flags: ["lon", "lat", "location", "text", "name"] + }) }; diff --git a/src/traces/scattergeo/calc.js b/src/traces/scattergeo/calc.js index e124cf9993c..7d9c85b3b5a 100644 --- a/src/traces/scattergeo/calc.js +++ b/src/traces/scattergeo/calc.js @@ -5,51 +5,43 @@ * 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 isNumeric = require("fast-isnumeric"); - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var calcMarkerColorscale = require('../scatter/colorscale_calc'); - +var calcMarkerColorscale = require("../scatter/colorscale_calc"); module.exports = function calc(gd, trace) { - var hasLocationData = Array.isArray(trace.locations), - len = hasLocationData ? trace.locations.length : trace.lon.length; + var hasLocationData = Array.isArray(trace.locations), + len = hasLocationData ? trace.locations.length : trace.lon.length; - var calcTrace = [], - cnt = 0; + var calcTrace = [], cnt = 0; - for(var i = 0; i < len; i++) { - var calcPt = {}, - skip; + for (var i = 0; i < len; i++) { + var calcPt = {}, skip; - if(hasLocationData) { - var loc = trace.locations[i]; + if (hasLocationData) { + var loc = trace.locations[i]; - calcPt.loc = loc; - skip = (typeof loc !== 'string'); - } - else { - var lon = trace.lon[i], - lat = trace.lat[i]; + calcPt.loc = loc; + skip = typeof loc !== "string"; + } else { + var lon = trace.lon[i], lat = trace.lat[i]; - calcPt.lonlat = [+lon, +lat]; - skip = (!isNumeric(lon) || !isNumeric(lat)); - } + calcPt.lonlat = [+lon, +lat]; + skip = !isNumeric(lon) || !isNumeric(lat); + } - if(skip) { - if(cnt > 0) calcTrace[cnt - 1].gapAfter = true; - continue; - } + if (skip) { + if (cnt > 0) calcTrace[cnt - 1].gapAfter = true; + continue; + } - cnt++; + cnt++; - calcTrace.push(calcPt); - } + calcTrace.push(calcPt); + } - calcMarkerColorscale(trace); + calcMarkerColorscale(trace); - return calcTrace; + return calcTrace; }; diff --git a/src/traces/scattergeo/defaults.js b/src/traces/scattergeo/defaults.js index aa0bbdd5f45..e13a12c05fe 100644 --- a/src/traces/scattergeo/defaults.js +++ b/src/traces/scattergeo/defaults.js @@ -5,74 +5,77 @@ * 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 subTypes = require('../scatter/subtypes'); -var handleMarkerDefaults = require('../scatter/marker_defaults'); -var handleLineDefaults = require('../scatter/line_defaults'); -var handleTextDefaults = require('../scatter/text_defaults'); -var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); - -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var len = handleLonLatLocDefaults(traceIn, traceOut, coerce); - if(!len) { - traceOut.visible = false; - return; - } - - coerce('text'); - coerce('mode'); - - if(subTypes.hasLines(traceOut)) { - handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); - coerce('connectgaps'); - } - - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); - } - - if(subTypes.hasText(traceOut)) { - handleTextDefaults(traceIn, traceOut, layout, coerce); - } - - coerce('fill'); - if(traceOut.fill !== 'none') { - handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); - } - - coerce('hoverinfo', (layout._dataLength === 1) ? 'lon+lat+location+text' : undefined); +"use strict"; +var Lib = require("../../lib"); + +var subTypes = require("../scatter/subtypes"); +var handleMarkerDefaults = require("../scatter/marker_defaults"); +var handleLineDefaults = require("../scatter/line_defaults"); +var handleTextDefaults = require("../scatter/text_defaults"); +var handleFillColorDefaults = require("../scatter/fillcolor_defaults"); + +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var len = handleLonLatLocDefaults(traceIn, traceOut, coerce); + if (!len) { + traceOut.visible = false; + return; + } + + coerce("text"); + coerce("mode"); + + if (subTypes.hasLines(traceOut)) { + handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); + coerce("connectgaps"); + } + + if (subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + if (subTypes.hasText(traceOut)) { + handleTextDefaults(traceIn, traceOut, layout, coerce); + } + + coerce("fill"); + if (traceOut.fill !== "none") { + handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); + } + + coerce( + "hoverinfo", + layout._dataLength === 1 ? "lon+lat+location+text" : undefined + ); }; function handleLonLatLocDefaults(traceIn, traceOut, coerce) { - var len = 0, - locations = coerce('locations'); + var len = 0, locations = coerce("locations"); - var lon, lat; + var lon, lat; - if(locations) { - coerce('locationmode'); - len = locations.length; - return len; - } + if (locations) { + coerce("locationmode"); + len = locations.length; + return len; + } - lon = coerce('lon') || []; - lat = coerce('lat') || []; - len = Math.min(lon.length, lat.length); + lon = coerce("lon") || []; + lat = coerce("lat") || []; + len = Math.min(lon.length, lat.length); - if(len < lon.length) traceOut.lon = lon.slice(0, len); - if(len < lat.length) traceOut.lat = lat.slice(0, len); + if (len < lon.length) traceOut.lon = lon.slice(0, len); + if (len < lat.length) traceOut.lat = lat.slice(0, len); - return len; + return len; } diff --git a/src/traces/scattergeo/event_data.js b/src/traces/scattergeo/event_data.js index f43043352ec..d6f8d184fce 100644 --- a/src/traces/scattergeo/event_data.js +++ b/src/traces/scattergeo/event_data.js @@ -5,15 +5,11 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; module.exports = function eventData(out, pt) { - out.lon = pt.lon; - out.lat = pt.lat; - out.location = pt.lon ? pt.lon : null; + out.lon = pt.lon; + out.lat = pt.lat; + out.location = pt.lon ? pt.lon : null; - return out; + return out; }; diff --git a/src/traces/scattergeo/hover.js b/src/traces/scattergeo/hover.js index 3e3c54f5bd7..7835b655785 100644 --- a/src/traces/scattergeo/hover.js +++ b/src/traces/scattergeo/hover.js @@ -5,107 +5,102 @@ * 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 Fx = require("../../plots/cartesian/graph_interact"); +var Axes = require("../../plots/cartesian/axes"); - -'use strict'; - -var Fx = require('../../plots/cartesian/graph_interact'); -var Axes = require('../../plots/cartesian/axes'); - -var getTraceColor = require('../scatter/get_trace_color'); -var attributes = require('./attributes'); - +var getTraceColor = require("../scatter/get_trace_color"); +var attributes = require("./attributes"); module.exports = function hoverPoints(pointData) { - var cd = pointData.cd, - trace = cd[0].trace, - xa = pointData.xa, - ya = pointData.ya, - geo = pointData.subplot; - - if(cd[0].placeholder) return; + var cd = pointData.cd, + trace = cd[0].trace, + xa = pointData.xa, + ya = pointData.ya, + geo = pointData.subplot; - function c2p(lonlat) { - return geo.projection(lonlat); - } + if (cd[0].placeholder) return; - function distFn(d) { - var lonlat = d.lonlat; + function c2p(lonlat) { + return geo.projection(lonlat); + } - // this handles the not-found location feature case - if(lonlat[0] === null || lonlat[1] === null) return Infinity; + function distFn(d) { + var lonlat = d.lonlat; - if(geo.isLonLatOverEdges(lonlat)) return Infinity; + // this handles the not-found location feature case + if (lonlat[0] === null || lonlat[1] === null) return Infinity; - var pos = c2p(lonlat); + if (geo.isLonLatOverEdges(lonlat)) return Infinity; - var xPx = xa.c2p(), - yPx = ya.c2p(); + var pos = c2p(lonlat); - var dx = Math.abs(xPx - pos[0]), - dy = Math.abs(yPx - pos[1]), - rad = Math.max(3, d.mrc || 0); + var xPx = xa.c2p(), yPx = ya.c2p(); - // N.B. d.mrc is the calculated marker radius - // which is only set for trace with 'markers' mode. + var dx = Math.abs(xPx - pos[0]), + dy = Math.abs(yPx - pos[1]), + rad = Math.max(3, d.mrc || 0); - return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad); - } + // N.B. d.mrc is the calculated marker radius + // which is only set for trace with 'markers' mode. + return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad); + } - Fx.getClosest(cd, distFn, pointData); + Fx.getClosest(cd, distFn, pointData); - // skip the rest (for this trace) if we didn't find a close point - if(pointData.index === false) return; + // skip the rest (for this trace) if we didn't find a close point + if (pointData.index === false) return; - var di = cd[pointData.index], - lonlat = di.lonlat, - pos = c2p(lonlat), - rad = di.mrc || 1; + var di = cd[pointData.index], + lonlat = di.lonlat, + pos = c2p(lonlat), + rad = di.mrc || 1; - pointData.x0 = pos[0] - rad; - pointData.x1 = pos[0] + rad; - pointData.y0 = pos[1] - rad; - pointData.y1 = pos[1] + rad; + pointData.x0 = pos[0] - rad; + pointData.x1 = pos[0] + rad; + pointData.y0 = pos[1] - rad; + pointData.y1 = pos[1] + rad; - pointData.loc = di.loc; - pointData.lat = lonlat[0]; - pointData.lon = lonlat[1]; + pointData.loc = di.loc; + pointData.lat = lonlat[0]; + pointData.lon = lonlat[1]; - pointData.color = getTraceColor(trace, di); - pointData.extraText = getExtraText(trace, di, geo.mockAxis); + pointData.color = getTraceColor(trace, di); + pointData.extraText = getExtraText(trace, di, geo.mockAxis); - return [pointData]; + return [pointData]; }; function getExtraText(trace, pt, axis) { - var hoverinfo = trace.hoverinfo; + var hoverinfo = trace.hoverinfo; - var parts = (hoverinfo === 'all') ? - attributes.hoverinfo.flags : - hoverinfo.split('+'); + var parts = hoverinfo === "all" + ? attributes.hoverinfo.flags + : hoverinfo.split("+"); - var hasLocation = parts.indexOf('location') !== -1 && Array.isArray(trace.locations), - hasLon = (parts.indexOf('lon') !== -1), - hasLat = (parts.indexOf('lat') !== -1), - hasText = (parts.indexOf('text') !== -1); + var hasLocation = parts.indexOf("location") !== -1 && + Array.isArray(trace.locations), + hasLon = parts.indexOf("lon") !== -1, + hasLat = parts.indexOf("lat") !== -1, + hasText = parts.indexOf("text") !== -1; - var text = []; + var text = []; - function format(val) { - return Axes.tickText(axis, axis.c2l(val), 'hover').text + '\u00B0'; - } + function format(val) { + return Axes.tickText(axis, axis.c2l(val), "hover").text + "\xB0"; + } - if(hasLocation) text.push(pt.loc); - else if(hasLon && hasLat) { - text.push('(' + format(pt.lonlat[0]) + ', ' + format(pt.lonlat[1]) + ')'); - } - else if(hasLon) text.push('lon: ' + format(pt.lonlat[0])); - else if(hasLat) text.push('lat: ' + format(pt.lonlat[1])); + if (hasLocation) { + text.push(pt.loc); + } else if (hasLon && hasLat) { + text.push("(" + format(pt.lonlat[0]) + ", " + format(pt.lonlat[1]) + ")"); + } else if (hasLon) text.push("lon: " + format(pt.lonlat[0])); + else if (hasLat) text.push("lat: " + format(pt.lonlat[1])); - if(hasText) { - var tx = pt.tx || trace.text; - if(!Array.isArray(tx)) text.push(tx); - } + if (hasText) { + var tx = pt.tx || trace.text; + if (!Array.isArray(tx)) text.push(tx); + } - return text.join('
'); + return text.join("
"); } diff --git a/src/traces/scattergeo/index.js b/src/traces/scattergeo/index.js index d48f0351330..5b13923e275 100644 --- a/src/traces/scattergeo/index.js +++ b/src/traces/scattergeo/index.js @@ -5,31 +5,28 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var ScatterGeo = {}; -ScatterGeo.attributes = require('./attributes'); -ScatterGeo.supplyDefaults = require('./defaults'); -ScatterGeo.colorbar = require('../scatter/colorbar'); -ScatterGeo.calc = require('./calc'); -ScatterGeo.plot = require('./plot'); -ScatterGeo.hoverPoints = require('./hover'); -ScatterGeo.eventData = require('./event_data'); +ScatterGeo.attributes = require("./attributes"); +ScatterGeo.supplyDefaults = require("./defaults"); +ScatterGeo.colorbar = require("../scatter/colorbar"); +ScatterGeo.calc = require("./calc"); +ScatterGeo.plot = require("./plot"); +ScatterGeo.hoverPoints = require("./hover"); +ScatterGeo.eventData = require("./event_data"); -ScatterGeo.moduleType = 'trace'; -ScatterGeo.name = 'scattergeo'; -ScatterGeo.basePlotModule = require('../../plots/geo'); -ScatterGeo.categories = ['geo', 'symbols', 'markerColorscale', 'showLegend']; +ScatterGeo.moduleType = "trace"; +ScatterGeo.name = "scattergeo"; +ScatterGeo.basePlotModule = require("../../plots/geo"); +ScatterGeo.categories = ["geo", "symbols", "markerColorscale", "showLegend"]; ScatterGeo.meta = { - hrName: 'scatter_geo', - description: [ - 'The data visualized as scatter point or lines on a geographic map', - 'is provided either by longitude/latitude pairs in `lon` and `lat`', - 'respectively or by geographic location IDs or names in `locations`.' - ].join(' ') + hrName: "scatter_geo", + description: [ + "The data visualized as scatter point or lines on a geographic map", + "is provided either by longitude/latitude pairs in `lon` and `lat`", + "respectively or by geographic location IDs or names in `locations`." + ].join(" ") }; module.exports = ScatterGeo; diff --git a/src/traces/scattergeo/plot.js b/src/traces/scattergeo/plot.js index b0c8b278dbc..73d17035373 100644 --- a/src/traces/scattergeo/plot.js +++ b/src/traces/scattergeo/plot.js @@ -5,166 +5,163 @@ * 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 d3 = require("d3"); + +var Drawing = require("../../components/drawing"); +var Color = require("../../components/color"); + +var Lib = require("../../lib"); +var getTopojsonFeatures = require( + "../../lib/topojson_utils" +).getTopojsonFeatures; +var locationToFeature = require( + "../../lib/geo_location_utils" +).locationToFeature; +var geoJsonUtils = require("../../lib/geojson_utils"); +var arrayToCalcItem = require("../../lib/array_to_calc_item"); +var subTypes = require("../scatter/subtypes"); +module.exports = function plot(geo, calcData) { + function keyFunc(d) { + return d[0].trace.uid; + } -'use strict'; + var gScatterGeoTraces = geo.framework + .select(".scattergeolayer") + .selectAll("g.trace.scattergeo") + .data(calcData, keyFunc); -var d3 = require('d3'); + gScatterGeoTraces.enter().append("g").attr("class", "trace scattergeo"); -var Drawing = require('../../components/drawing'); -var Color = require('../../components/color'); + gScatterGeoTraces.exit().remove(); -var Lib = require('../../lib'); -var getTopojsonFeatures = require('../../lib/topojson_utils').getTopojsonFeatures; -var locationToFeature = require('../../lib/geo_location_utils').locationToFeature; -var geoJsonUtils = require('../../lib/geojson_utils'); -var arrayToCalcItem = require('../../lib/array_to_calc_item'); -var subTypes = require('../scatter/subtypes'); + // TODO find a way to order the inner nodes on update + gScatterGeoTraces.selectAll("*").remove(); + gScatterGeoTraces.each(function(calcTrace) { + var s = d3.select(this), + trace = calcTrace[0].trace, + convertToLonLatFn = makeConvertToLonLatFn(trace, geo.topojson); -module.exports = function plot(geo, calcData) { + // skip over placeholder traces + if (calcTrace[0].placeholder) s.remove(); - function keyFunc(d) { return d[0].trace.uid; } + // just like calcTrace but w/o not-found location datum + var _calcTrace = []; - var gScatterGeoTraces = geo.framework.select('.scattergeolayer') - .selectAll('g.trace.scattergeo') - .data(calcData, keyFunc); + for (var i = 0; i < calcTrace.length; i++) { + var _calcPt = convertToLonLatFn(calcTrace[i]); - gScatterGeoTraces.enter().append('g') - .attr('class', 'trace scattergeo'); + if (_calcPt) { + arrayItemToCalcdata(trace, calcTrace[i], i); + _calcTrace.push(_calcPt); + } + } - gScatterGeoTraces.exit().remove(); + if (subTypes.hasLines(trace) || trace.fill !== "none") { + var lineCoords = geoJsonUtils.calcTraceToLineCoords(_calcTrace); - // TODO find a way to order the inner nodes on update - gScatterGeoTraces.selectAll('*').remove(); + var lineData = trace.fill !== "none" + ? geoJsonUtils.makePolygon(lineCoords, trace) + : geoJsonUtils.makeLine(lineCoords, trace); - gScatterGeoTraces.each(function(calcTrace) { - var s = d3.select(this), - trace = calcTrace[0].trace, - convertToLonLatFn = makeConvertToLonLatFn(trace, geo.topojson); + s + .selectAll("path.js-line") + .data([lineData]) + .enter() + .append("path") + .classed("js-line", true); + } - // skip over placeholder traces - if(calcTrace[0].placeholder) s.remove(); + if (subTypes.hasMarkers(trace)) { + s + .selectAll("path.point") + .data(_calcTrace) + .enter() + .append("path") + .classed("point", true); + } - // just like calcTrace but w/o not-found location datum - var _calcTrace = []; + if (subTypes.hasText(trace)) { + s.selectAll("g").data(_calcTrace).enter().append("g").append("text"); + } + }); - for(var i = 0; i < calcTrace.length; i++) { - var _calcPt = convertToLonLatFn(calcTrace[i]); + // call style here within topojson request callback + style(geo); +}; - if(_calcPt) { - arrayItemToCalcdata(trace, calcTrace[i], i); - _calcTrace.push(_calcPt); - } - } +function makeConvertToLonLatFn(trace, topojson) { + if (!Array.isArray(trace.locations)) return Lib.identity; - if(subTypes.hasLines(trace) || trace.fill !== 'none') { - var lineCoords = geoJsonUtils.calcTraceToLineCoords(_calcTrace); + var features = getTopojsonFeatures(trace, topojson), + locationmode = trace.locationmode; - var lineData = (trace.fill !== 'none') ? - geoJsonUtils.makePolygon(lineCoords, trace) : - geoJsonUtils.makeLine(lineCoords, trace); + return function(calcPt) { + var feature = locationToFeature(locationmode, calcPt.loc, features); - s.selectAll('path.js-line') - .data([lineData]) - .enter().append('path') - .classed('js-line', true); - } + if (feature) { + calcPt.lonlat = feature.properties.ct; + return calcPt; + } else { + // mutate gd.calcdata so that hoverPoints knows to skip this datum + calcPt.lonlat = [null, null]; + return false; + } + }; +} - if(subTypes.hasMarkers(trace)) { - s.selectAll('path.point').data(_calcTrace) - .enter().append('path') - .classed('point', true); - } +function arrayItemToCalcdata(trace, calcItem, i) { + var marker = trace.marker; + + function merge(traceAttr, calcAttr) { + arrayToCalcItem(traceAttr, calcItem, calcAttr, i); + } + + merge(trace.text, "tx"); + merge(trace.textposition, "tp"); + if (trace.textfont) { + merge(trace.textfont.size, "ts"); + merge(trace.textfont.color, "tc"); + merge(trace.textfont.family, "tf"); + } + + if (marker && marker.line) { + var markerLine = marker.line; + merge(marker.opacity, "mo"); + merge(marker.symbol, "mx"); + merge(marker.color, "mc"); + merge(marker.size, "ms"); + merge(markerLine.color, "mlc"); + merge(markerLine.width, "mlw"); + } +} - if(subTypes.hasText(trace)) { - s.selectAll('g').data(_calcTrace) - .enter().append('g') - .append('text'); - } - }); +function style(geo) { + var selection = geo.framework.selectAll("g.trace.scattergeo"); - // call style here within topojson request callback - style(geo); -}; + selection.style("opacity", function(calcTrace) { + return calcTrace[0].trace.opacity; + }); -function makeConvertToLonLatFn(trace, topojson) { - if(!Array.isArray(trace.locations)) return Lib.identity; - - var features = getTopojsonFeatures(trace, topojson), - locationmode = trace.locationmode; - - return function(calcPt) { - var feature = locationToFeature(locationmode, calcPt.loc, features); - - if(feature) { - calcPt.lonlat = feature.properties.ct; - return calcPt; - } - else { - // mutate gd.calcdata so that hoverPoints knows to skip this datum - calcPt.lonlat = [null, null]; - return false; - } - }; -} + selection.each(function(calcTrace) { + var trace = calcTrace[0].trace, group = d3.select(this); -function arrayItemToCalcdata(trace, calcItem, i) { - var marker = trace.marker; + group.selectAll("path.point").call(Drawing.pointStyle, trace); + group.selectAll("text").call(Drawing.textPointStyle, trace); + }); - function merge(traceAttr, calcAttr) { - arrayToCalcItem(traceAttr, calcItem, calcAttr, i); - } + // this part is incompatible with Drawing.lineGroupStyle + selection.selectAll("path.js-line").style("fill", "none").each(function(d) { + var path = d3.select(this), trace = d.trace, line = trace.line || {}; - merge(trace.text, 'tx'); - merge(trace.textposition, 'tp'); - if(trace.textfont) { - merge(trace.textfont.size, 'ts'); - merge(trace.textfont.color, 'tc'); - merge(trace.textfont.family, 'tf'); - } + path + .call(Color.stroke, line.color) + .call(Drawing.dashLine, line.dash || "", line.width || 0); - if(marker && marker.line) { - var markerLine = marker.line; - merge(marker.opacity, 'mo'); - merge(marker.symbol, 'mx'); - merge(marker.color, 'mc'); - merge(marker.size, 'ms'); - merge(markerLine.color, 'mlc'); - merge(markerLine.width, 'mlw'); + if (trace.fill !== "none") { + path.call(Color.fill, trace.fillcolor); } -} - -function style(geo) { - var selection = geo.framework.selectAll('g.trace.scattergeo'); - - selection.style('opacity', function(calcTrace) { - return calcTrace[0].trace.opacity; - }); - - selection.each(function(calcTrace) { - var trace = calcTrace[0].trace, - group = d3.select(this); - - group.selectAll('path.point') - .call(Drawing.pointStyle, trace); - group.selectAll('text') - .call(Drawing.textPointStyle, trace); - }); - - // this part is incompatible with Drawing.lineGroupStyle - selection.selectAll('path.js-line') - .style('fill', 'none') - .each(function(d) { - var path = d3.select(this), - trace = d.trace, - line = trace.line || {}; - - path.call(Color.stroke, line.color) - .call(Drawing.dashLine, line.dash || '', line.width || 0); - - if(trace.fill !== 'none') { - path.call(Color.fill, trace.fillcolor); - } - }); + }); } diff --git a/src/traces/scattergl/attributes.js b/src/traces/scattergl/attributes.js index 2764714a5a7..4caab704877 100644 --- a/src/traces/scattergl/attributes.js +++ b/src/traces/scattergl/attributes.js @@ -5,84 +5,80 @@ * 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 scatterAttrs = require("../scatter/attributes"); +var colorAttributes = require("../../components/colorscale/color_attributes"); -'use strict'; - -var scatterAttrs = require('../scatter/attributes'); -var colorAttributes = require('../../components/colorscale/color_attributes'); - -var DASHES = require('../../constants/gl2d_dashes'); -var MARKERS = require('../../constants/gl_markers'); -var extendFlat = require('../../lib/extend').extendFlat; -var extendDeep = require('../../lib/extend').extendDeep; +var DASHES = require("../../constants/gl2d_dashes"); +var MARKERS = require("../../constants/gl_markers"); +var extendFlat = require("../../lib/extend").extendFlat; +var extendDeep = require("../../lib/extend").extendDeep; var scatterLineAttrs = scatterAttrs.line, - scatterMarkerAttrs = scatterAttrs.marker, - scatterMarkerLineAttrs = scatterMarkerAttrs.line; + scatterMarkerAttrs = scatterAttrs.marker, + scatterMarkerLineAttrs = scatterMarkerAttrs.line; module.exports = { - x: scatterAttrs.x, - x0: scatterAttrs.x0, - dx: scatterAttrs.dx, - y: scatterAttrs.y, - y0: scatterAttrs.y0, - dy: scatterAttrs.dy, - - text: extendFlat({}, scatterAttrs.text, { - description: [ - 'Sets text elements associated with each (x,y) pair to appear on hover.', - 'If a single string, the same string appears over', - 'all the data points.', - 'If an array of string, the items are mapped in order to the', - 'this trace\'s (x,y) coordinates.' - ].join(' ') - }), - mode: { - valType: 'flaglist', - flags: ['lines', 'markers'], - extras: ['none'], - role: 'info', - description: [ - 'Determines the drawing mode for this scatter trace.' - ].join(' ') + x: scatterAttrs.x, + x0: scatterAttrs.x0, + dx: scatterAttrs.dx, + y: scatterAttrs.y, + y0: scatterAttrs.y0, + dy: scatterAttrs.dy, + text: extendFlat({}, scatterAttrs.text, { + description: [ + "Sets text elements associated with each (x,y) pair to appear on hover.", + "If a single string, the same string appears over", + "all the data points.", + "If an array of string, the items are mapped in order to the", + "this trace's (x,y) coordinates." + ].join(" ") + }), + mode: { + valType: "flaglist", + flags: ["lines", "markers"], + extras: ["none"], + role: "info", + description: ["Determines the drawing mode for this scatter trace."].join( + " " + ) + }, + line: { + color: scatterLineAttrs.color, + width: scatterLineAttrs.width, + dash: { + valType: "enumerated", + values: Object.keys(DASHES), + dflt: "solid", + role: "style", + description: "Sets the style of the lines." + } + }, + marker: extendDeep({}, colorAttributes("marker"), { + symbol: { + valType: "enumerated", + values: Object.keys(MARKERS), + dflt: "circle", + arrayOk: true, + role: "style", + description: "Sets the marker symbol type." }, - line: { - color: scatterLineAttrs.color, - width: scatterLineAttrs.width, - dash: { - valType: 'enumerated', - values: Object.keys(DASHES), - dflt: 'solid', - role: 'style', - description: 'Sets the style of the lines.' - } - }, - marker: extendDeep({}, colorAttributes('marker'), { - symbol: { - valType: 'enumerated', - values: Object.keys(MARKERS), - dflt: 'circle', - arrayOk: true, - role: 'style', - description: 'Sets the marker symbol type.' - }, - size: scatterMarkerAttrs.size, - sizeref: scatterMarkerAttrs.sizeref, - sizemin: scatterMarkerAttrs.sizemin, - sizemode: scatterMarkerAttrs.sizemode, - opacity: scatterMarkerAttrs.opacity, - showscale: scatterMarkerAttrs.showscale, - colorbar: scatterMarkerAttrs.colorbar, - line: extendDeep({}, colorAttributes('marker.line'), { - width: scatterMarkerLineAttrs.width - }) - }), - connectgaps: scatterAttrs.connectgaps, - fill: extendFlat({}, scatterAttrs.fill, { - values: ['none', 'tozeroy', 'tozerox'] - }), - fillcolor: scatterAttrs.fillcolor, - - error_y: scatterAttrs.error_y, - error_x: scatterAttrs.error_x + size: scatterMarkerAttrs.size, + sizeref: scatterMarkerAttrs.sizeref, + sizemin: scatterMarkerAttrs.sizemin, + sizemode: scatterMarkerAttrs.sizemode, + opacity: scatterMarkerAttrs.opacity, + showscale: scatterMarkerAttrs.showscale, + colorbar: scatterMarkerAttrs.colorbar, + line: extendDeep({}, colorAttributes("marker.line"), { + width: scatterMarkerLineAttrs.width + }) + }), + connectgaps: scatterAttrs.connectgaps, + fill: extendFlat({}, scatterAttrs.fill, { + values: ["none", "tozeroy", "tozerox"] + }), + fillcolor: scatterAttrs.fillcolor, + error_y: scatterAttrs.error_y, + error_x: scatterAttrs.error_x }; diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index fcfa54eb6d4..7f82ba7387d 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -5,225 +5,217 @@ * 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 createScatter = require('gl-scatter2d'); -var createFancyScatter = require('gl-scatter2d-fancy'); -var createLine = require('gl-line2d'); -var createError = require('gl-error2d'); -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Axes = require('../../plots/cartesian/axes'); -var autoType = require('../../plots/cartesian/axis_autotype'); -var ErrorBars = require('../../components/errorbars'); -var str2RGBArray = require('../../lib/str2rgbarray'); -var truncate = require('../../lib/typed_array_truncate'); -var formatColor = require('../../lib/gl_format_color'); -var subTypes = require('../scatter/subtypes'); -var makeBubbleSizeFn = require('../scatter/make_bubble_size_func'); -var getTraceColor = require('../scatter/get_trace_color'); -var MARKER_SYMBOLS = require('../../constants/gl_markers'); -var DASHES = require('../../constants/gl2d_dashes'); - -var AXES = ['xaxis', 'yaxis']; - +"use strict"; +var createScatter = require("gl-scatter2d"); +var createFancyScatter = require("gl-scatter2d-fancy"); +var createLine = require("gl-line2d"); +var createError = require("gl-error2d"); +var isNumeric = require("fast-isnumeric"); + +var Lib = require("../../lib"); +var Axes = require("../../plots/cartesian/axes"); +var autoType = require("../../plots/cartesian/axis_autotype"); +var ErrorBars = require("../../components/errorbars"); +var str2RGBArray = require("../../lib/str2rgbarray"); +var truncate = require("../../lib/typed_array_truncate"); +var formatColor = require("../../lib/gl_format_color"); +var subTypes = require("../scatter/subtypes"); +var makeBubbleSizeFn = require("../scatter/make_bubble_size_func"); +var getTraceColor = require("../scatter/get_trace_color"); +var MARKER_SYMBOLS = require("../../constants/gl_markers"); +var DASHES = require("../../constants/gl2d_dashes"); + +var AXES = ["xaxis", "yaxis"]; function LineWithMarkers(scene, uid) { - this.scene = scene; - this.uid = uid; - this.type = 'scattergl'; - - this.pickXData = []; - this.pickYData = []; - this.xData = []; - this.yData = []; - this.textLabels = []; - this.color = 'rgb(0, 0, 0)'; - this.name = ''; - this.hoverinfo = 'all'; - this.connectgaps = true; - - this.idToIndex = []; - this.bounds = [0, 0, 0, 0]; - - this.hasLines = false; - this.lineOptions = { - positions: new Float64Array(0), - color: [0, 0, 0, 1], - width: 1, - fill: [false, false, false, false], - fillColor: [ - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1]], - dashes: [1] - }; - this.line = createLine(scene.glplot, this.lineOptions); - this.line._trace = this; - - this.hasErrorX = false; - this.errorXOptions = { - positions: new Float64Array(0), - errors: new Float64Array(0), - lineWidth: 1, - capSize: 0, - color: [0, 0, 0, 1] - }; - this.errorX = createError(scene.glplot, this.errorXOptions); - this.errorX._trace = this; - - this.hasErrorY = false; - this.errorYOptions = { - positions: new Float64Array(0), - errors: new Float64Array(0), - lineWidth: 1, - capSize: 0, - color: [0, 0, 0, 1] - }; - this.errorY = createError(scene.glplot, this.errorYOptions); - this.errorY._trace = this; - - this.hasMarkers = false; - this.scatterOptions = { - positions: new Float64Array(0), - sizes: [], - colors: [], - glyphs: [], - borderWidths: [], - borderColors: [], - size: 12, - color: [0, 0, 0, 1], - borderSize: 1, - borderColor: [0, 0, 0, 1] - }; - this.scatter = createScatter(scene.glplot, this.scatterOptions); - this.scatter._trace = this; - this.fancyScatter = createFancyScatter(scene.glplot, this.scatterOptions); - this.fancyScatter._trace = this; - - this.isVisible = false; + this.scene = scene; + this.uid = uid; + this.type = "scattergl"; + + this.pickXData = []; + this.pickYData = []; + this.xData = []; + this.yData = []; + this.textLabels = []; + this.color = "rgb(0, 0, 0)"; + this.name = ""; + this.hoverinfo = "all"; + this.connectgaps = true; + + this.idToIndex = []; + this.bounds = [0, 0, 0, 0]; + + this.hasLines = false; + this.lineOptions = { + positions: new Float64Array(0), + color: [0, 0, 0, 1], + width: 1, + fill: [false, false, false, false], + fillColor: [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]], + dashes: [1] + }; + this.line = createLine(scene.glplot, this.lineOptions); + this.line._trace = this; + + this.hasErrorX = false; + this.errorXOptions = { + positions: new Float64Array(0), + errors: new Float64Array(0), + lineWidth: 1, + capSize: 0, + color: [0, 0, 0, 1] + }; + this.errorX = createError(scene.glplot, this.errorXOptions); + this.errorX._trace = this; + + this.hasErrorY = false; + this.errorYOptions = { + positions: new Float64Array(0), + errors: new Float64Array(0), + lineWidth: 1, + capSize: 0, + color: [0, 0, 0, 1] + }; + this.errorY = createError(scene.glplot, this.errorYOptions); + this.errorY._trace = this; + + this.hasMarkers = false; + this.scatterOptions = { + positions: new Float64Array(0), + sizes: [], + colors: [], + glyphs: [], + borderWidths: [], + borderColors: [], + size: 12, + color: [0, 0, 0, 1], + borderSize: 1, + borderColor: [0, 0, 0, 1] + }; + this.scatter = createScatter(scene.glplot, this.scatterOptions); + this.scatter._trace = this; + this.fancyScatter = createFancyScatter(scene.glplot, this.scatterOptions); + this.fancyScatter._trace = this; + + this.isVisible = false; } var proto = LineWithMarkers.prototype; proto.handlePick = function(pickResult) { - var index = pickResult.pointId; - - if(pickResult.object !== this.line || this.connectgaps) { - index = this.idToIndex[pickResult.pointId]; - } - - var x = this.pickXData[index]; - - return { - trace: this, - dataCoord: pickResult.dataCoord, - traceCoord: [ - isNumeric(x) || !Lib.isDateTime(x) ? x : Lib.dateTime2ms(x), - this.pickYData[index] - ], - textLabel: Array.isArray(this.textLabels) ? - this.textLabels[index] : - this.textLabels, - color: Array.isArray(this.color) ? - this.color[index] : - this.color, - name: this.name, - pointIndex: index, - hoverinfo: this.hoverinfo - }; + var index = pickResult.pointId; + + if (pickResult.object !== this.line || this.connectgaps) { + index = this.idToIndex[pickResult.pointId]; + } + + var x = this.pickXData[index]; + + return { + trace: this, + dataCoord: pickResult.dataCoord, + traceCoord: [ + isNumeric(x) || !Lib.isDateTime(x) ? x : Lib.dateTime2ms(x), + this.pickYData[index] + ], + textLabel: ( + Array.isArray(this.textLabels) ? this.textLabels[index] : this.textLabels + ), + color: Array.isArray(this.color) ? this.color[index] : this.color, + name: this.name, + pointIndex: index, + hoverinfo: this.hoverinfo + }; }; // check if trace is fancy proto.isFancy = function(options) { - if(this.scene.xaxis.type !== 'linear' && this.scene.xaxis.type !== 'date') return true; - if(this.scene.yaxis.type !== 'linear') return true; - - if(!options.x || !options.y) return true; - - if(this.hasMarkers) { - var marker = options.marker || {}; - - if(Array.isArray(marker.symbol) || - marker.symbol !== 'circle' || - Array.isArray(marker.size) || - Array.isArray(marker.color) || - Array.isArray(marker.line.width) || - Array.isArray(marker.line.color) || - Array.isArray(marker.opacity) - ) return true; + if (this.scene.xaxis.type !== "linear" && this.scene.xaxis.type !== "date") { + return true; + } + if (this.scene.yaxis.type !== "linear") return true; + + if (!options.x || !options.y) return true; + + if (this.hasMarkers) { + var marker = options.marker || {}; + + if ( + Array.isArray(marker.symbol) || + marker.symbol !== "circle" || + Array.isArray(marker.size) || + Array.isArray(marker.color) || + Array.isArray(marker.line.width) || + Array.isArray(marker.line.color) || + Array.isArray(marker.opacity) + ) { + return true; } + } - if(this.hasLines && !this.connectgaps) return true; + if (this.hasLines && !this.connectgaps) return true; - if(this.hasErrorX) return true; - if(this.hasErrorY) return true; + if (this.hasErrorX) return true; + if (this.hasErrorY) return true; - return false; + return false; }; // handle the situation where values can be array-like or not array like function convertArray(convert, data, count) { - if(!Array.isArray(data)) data = [data]; + if (!Array.isArray(data)) data = [data]; - return _convertArray(convert, data, count); + return _convertArray(convert, data, count); } function _convertArray(convert, data, count) { - var result = new Array(count), - data0 = data[0]; + var result = new Array(count), data0 = data[0]; - for(var i = 0; i < count; ++i) { - result[i] = (i >= data.length) ? - convert(data0) : - convert(data[i]); - } + for (var i = 0; i < count; ++i) { + result[i] = i >= data.length ? convert(data0) : convert(data[i]); + } - return result; + return result; } -var convertNumber = convertArray.bind(null, function(x) { return +x; }); +var convertNumber = convertArray.bind(null, function(x) { + return +x; +}); var convertColorBase = convertArray.bind(null, str2RGBArray); var convertSymbol = convertArray.bind(null, function(x) { - return MARKER_SYMBOLS[x] || '●'; + return MARKER_SYMBOLS[x] || "\u25CF"; }); function convertColor(color, opacity, count) { - return _convertColor( - convertColorBase(color, count), - convertNumber(opacity, count), - count - ); + return _convertColor( + convertColorBase(color, count), + convertNumber(opacity, count), + count + ); } function convertColorScale(containerIn, markerOpacity, traceOpacity, count) { - var colors = formatColor(containerIn, markerOpacity, count); + var colors = formatColor(containerIn, markerOpacity, count); - colors = Array.isArray(colors[0]) ? - colors : - _convertArray(Lib.identity, [colors], count); + colors = Array.isArray(colors[0]) + ? colors + : _convertArray(Lib.identity, [colors], count); - return _convertColor( - colors, - convertNumber(traceOpacity, count), - count - ); + return _convertColor(colors, convertNumber(traceOpacity, count), count); } function _convertColor(colors, opacities, count) { - var result = new Array(4 * count); - - for(var i = 0; i < count; ++i) { - for(var j = 0; j < 3; ++j) result[4 * i + j] = colors[i][j]; + var result = new Array(4 * count); - result[4 * i + 3] = colors[i][3] * opacities[i]; + for (var i = 0; i < count; ++i) { + for (var j = 0; j < 3; ++j) { + result[4 * i + j] = colors[i][j]; } - return result; + result[4 * i + 3] = colors[i][3] * opacities[i]; + } + + return result; } /* Order is important here to get the correct laying: @@ -233,40 +225,37 @@ function _convertColor(colors, opacities, count) { * - markers */ proto.update = function(options) { - if(options.visible !== true) { - this.isVisible = false; - this.hasLines = false; - this.hasErrorX = false; - this.hasErrorY = false; - this.hasMarkers = false; - } - else { - this.isVisible = true; - this.hasLines = subTypes.hasLines(options); - this.hasErrorX = options.error_x.visible === true; - this.hasErrorY = options.error_y.visible === true; - this.hasMarkers = subTypes.hasMarkers(options); - } - - this.textLabels = options.text; - this.name = options.name; - this.hoverinfo = options.hoverinfo; - this.bounds = [Infinity, Infinity, -Infinity, -Infinity]; - this.connectgaps = !!options.connectgaps; - - if(!this.isVisible) { - this.clear(); - } - else if(this.isFancy(options)) { - this.updateFancy(options); - } - else { - this.updateFast(options); - } - - // not quite on-par with 'scatter', but close enough for now - // does not handle the colorscale case - this.color = getTraceColor(options, {}); + if (options.visible !== true) { + this.isVisible = false; + this.hasLines = false; + this.hasErrorX = false; + this.hasErrorY = false; + this.hasMarkers = false; + } else { + this.isVisible = true; + this.hasLines = subTypes.hasLines(options); + this.hasErrorX = options.error_x.visible === true; + this.hasErrorY = options.error_y.visible === true; + this.hasMarkers = subTypes.hasMarkers(options); + } + + this.textLabels = options.text; + this.name = options.name; + this.hoverinfo = options.hoverinfo; + this.bounds = [Infinity, Infinity, -Infinity, -Infinity]; + this.connectgaps = !!options.connectgaps; + + if (!this.isVisible) { + this.clear(); + } else if (this.isFancy(options)) { + this.updateFancy(options); + } else { + this.updateFast(options); + } + + // not quite on-par with 'scatter', but close enough for now + // does not handle the colorscale case + this.color = getTraceColor(options, {}); }; // We'd ideally know that all values are of fast types; sampling gives no certainty but faster @@ -278,359 +267,369 @@ proto.update = function(options) { // Code DRYing is not done to preserve the most direct compilation possible for speed; // also, there are quite a few differences function allFastTypesLikely(a) { - var len = a.length, - inc = Math.max(1, (len - 1) / Math.min(Math.max(len, 1), 1000)), - ai; - - for(var i = 0; i < len; i += inc) { - ai = a[Math.floor(i)]; - if(!isNumeric(ai) && !(ai instanceof Date)) { - return false; - } + var len = a.length, + inc = Math.max(1, (len - 1) / Math.min(Math.max(len, 1), 1000)), + ai; + + for (var i = 0; i < len; i += inc) { + ai = a[Math.floor(i)]; + if (!isNumeric(ai) && !(ai instanceof Date)) { + return false; } + } - return true; + return true; } proto.clear = function() { - this.lineOptions.positions = new Float64Array(0); - this.line.update(this.lineOptions); + this.lineOptions.positions = new Float64Array(0); + this.line.update(this.lineOptions); - this.errorXOptions.positions = new Float64Array(0); - this.errorX.update(this.errorXOptions); + this.errorXOptions.positions = new Float64Array(0); + this.errorX.update(this.errorXOptions); - this.errorYOptions.positions = new Float64Array(0); - this.errorY.update(this.errorYOptions); + this.errorYOptions.positions = new Float64Array(0); + this.errorY.update(this.errorYOptions); - this.scatterOptions.positions = new Float64Array(0); - this.scatterOptions.glyphs = []; - this.scatter.update(this.scatterOptions); - this.fancyScatter.update(this.scatterOptions); + this.scatterOptions.positions = new Float64Array(0); + this.scatterOptions.glyphs = []; + this.scatter.update(this.scatterOptions); + this.fancyScatter.update(this.scatterOptions); }; proto.updateFast = function(options) { - var x = this.xData = this.pickXData = options.x; - var y = this.yData = this.pickYData = options.y; - - var len = x.length, - idToIndex = new Array(len), - positions = new Float64Array(2 * len), - bounds = this.bounds, - pId = 0, - ptr = 0; - - var xx, yy; - - var xcalendar = options.xcalendar; + var x = this.xData = this.pickXData = options.x; + var y = this.yData = this.pickYData = options.y; - var fastType = allFastTypesLikely(x); - var isDateTime = !fastType && autoType(x, xcalendar) === 'date'; + var len = x.length, + idToIndex = new Array(len), + positions = new Float64Array(2 * len), + bounds = this.bounds, + pId = 0, + ptr = 0; - // TODO add 'very fast' mode that bypasses this loop - // TODO bypass this on modebar +/- zoom - if(fastType || isDateTime) { + var xx, yy; - for(var i = 0; i < len; ++i) { - xx = x[i]; - yy = y[i]; + var xcalendar = options.xcalendar; - if(isNumeric(yy)) { + var fastType = allFastTypesLikely(x); + var isDateTime = !fastType && autoType(x, xcalendar) === "date"; - if(!fastType) { - xx = Lib.dateTime2ms(xx, xcalendar); - } + // TODO add 'very fast' mode that bypasses this loop + // TODO bypass this on modebar +/- zoom + if (fastType || isDateTime) { + for (var i = 0; i < len; ++i) { + xx = x[i]; + yy = y[i]; - idToIndex[pId++] = i; - - positions[ptr++] = xx; - positions[ptr++] = yy; - - bounds[0] = Math.min(bounds[0], xx); - bounds[1] = Math.min(bounds[1], yy); - bounds[2] = Math.max(bounds[2], xx); - bounds[3] = Math.max(bounds[3], yy); - } + if (isNumeric(yy)) { + if (!fastType) { + xx = Lib.dateTime2ms(xx, xcalendar); } - } - - positions = truncate(positions, ptr); - this.idToIndex = idToIndex; - - this.updateLines(options, positions); - this.updateError('X', options); - this.updateError('Y', options); - - var markerSize; - - if(this.hasMarkers) { - this.scatterOptions.positions = positions; - - var markerColor = str2RGBArray(options.marker.color), - borderColor = str2RGBArray(options.marker.line.color), - opacity = (options.opacity) * (options.marker.opacity); - - markerColor[3] *= opacity; - this.scatterOptions.color = markerColor; - - borderColor[3] *= opacity; - this.scatterOptions.borderColor = borderColor; - - markerSize = options.marker.size; - this.scatterOptions.size = markerSize; - this.scatterOptions.borderSize = options.marker.line.width; - - this.scatter.update(this.scatterOptions); - } - else { - this.scatterOptions.positions = new Float64Array(0); - this.scatterOptions.glyphs = []; - this.scatter.update(this.scatterOptions); - } - - // turn off fancy scatter plot - this.scatterOptions.positions = new Float64Array(0); - this.scatterOptions.glyphs = []; - this.fancyScatter.update(this.scatterOptions); - - // add item for autorange routine - this.expandAxesFast(bounds, markerSize); -}; - -proto.updateFancy = function(options) { - var scene = this.scene, - xaxis = scene.xaxis, - yaxis = scene.yaxis, - bounds = this.bounds; - - // makeCalcdata runs d2c (data-to-coordinate) on every point - var x = this.pickXData = xaxis.makeCalcdata(options, 'x').slice(); - var y = this.pickYData = yaxis.makeCalcdata(options, 'y').slice(); - - this.xData = x.slice(); - this.yData = y.slice(); - - // get error values - var errorVals = ErrorBars.calcFromTrace(options, scene.fullLayout); - - var len = x.length, - idToIndex = new Array(len), - positions = new Float64Array(2 * len), - errorsX = new Float64Array(4 * len), - errorsY = new Float64Array(4 * len), - pId = 0, - ptr = 0, - ptrX = 0, - ptrY = 0; - - var getX = (xaxis.type === 'log') ? xaxis.d2l : function(x) { return x; }; - var getY = (yaxis.type === 'log') ? yaxis.d2l : function(y) { return y; }; - - var i, j, xx, yy, ex0, ex1, ey0, ey1; - - for(i = 0; i < len; ++i) { - this.xData[i] = xx = getX(x[i]); - this.yData[i] = yy = getY(y[i]); - - if(isNaN(xx) || isNaN(yy)) continue; idToIndex[pId++] = i; positions[ptr++] = xx; positions[ptr++] = yy; - ex0 = errorsX[ptrX++] = xx - errorVals[i].xs || 0; - ex1 = errorsX[ptrX++] = errorVals[i].xh - xx || 0; - errorsX[ptrX++] = 0; - errorsX[ptrX++] = 0; - - errorsY[ptrY++] = 0; - errorsY[ptrY++] = 0; - ey0 = errorsY[ptrY++] = yy - errorVals[i].ys || 0; - ey1 = errorsY[ptrY++] = errorVals[i].yh - yy || 0; - - bounds[0] = Math.min(bounds[0], xx - ex0); - bounds[1] = Math.min(bounds[1], yy - ey0); - bounds[2] = Math.max(bounds[2], xx + ex1); - bounds[3] = Math.max(bounds[3], yy + ey1); + bounds[0] = Math.min(bounds[0], xx); + bounds[1] = Math.min(bounds[1], yy); + bounds[2] = Math.max(bounds[2], xx); + bounds[3] = Math.max(bounds[3], yy); + } } + } - positions = truncate(positions, ptr); - this.idToIndex = idToIndex; + positions = truncate(positions, ptr); + this.idToIndex = idToIndex; - this.updateLines(options, positions); - this.updateError('X', options, positions, errorsX); - this.updateError('Y', options, positions, errorsY); + this.updateLines(options, positions); + this.updateError("X", options); + this.updateError("Y", options); - var sizes; + var markerSize; - if(this.hasMarkers) { - this.scatterOptions.positions = positions; + if (this.hasMarkers) { + this.scatterOptions.positions = positions; - // TODO rewrite convert function so that - // we don't have to loop through the data another time + var markerColor = str2RGBArray(options.marker.color), + borderColor = str2RGBArray(options.marker.line.color), + opacity = options.opacity * options.marker.opacity; - this.scatterOptions.sizes = new Array(pId); - this.scatterOptions.glyphs = new Array(pId); - this.scatterOptions.borderWidths = new Array(pId); - this.scatterOptions.colors = new Array(pId * 4); - this.scatterOptions.borderColors = new Array(pId * 4); + markerColor[3] *= opacity; + this.scatterOptions.color = markerColor; - var markerSizeFunc = makeBubbleSizeFn(options), - markerOpts = options.marker, - markerOpacity = markerOpts.opacity, - traceOpacity = options.opacity, - colors = convertColorScale(markerOpts, markerOpacity, traceOpacity, len), - glyphs = convertSymbol(markerOpts.symbol, len), - borderWidths = convertNumber(markerOpts.line.width, len), - borderColors = convertColorScale(markerOpts.line, markerOpacity, traceOpacity, len), - index; + borderColor[3] *= opacity; + this.scatterOptions.borderColor = borderColor; - sizes = convertArray(markerSizeFunc, markerOpts.size, len); + markerSize = options.marker.size; + this.scatterOptions.size = markerSize; + this.scatterOptions.borderSize = options.marker.line.width; - for(i = 0; i < pId; ++i) { - index = idToIndex[i]; + this.scatter.update(this.scatterOptions); + } else { + this.scatterOptions.positions = new Float64Array(0); + this.scatterOptions.glyphs = []; + this.scatter.update(this.scatterOptions); + } - this.scatterOptions.sizes[i] = 4.0 * sizes[index]; - this.scatterOptions.glyphs[i] = glyphs[index]; - this.scatterOptions.borderWidths[i] = 0.5 * borderWidths[index]; + // turn off fancy scatter plot + this.scatterOptions.positions = new Float64Array(0); + this.scatterOptions.glyphs = []; + this.fancyScatter.update(this.scatterOptions); - for(j = 0; j < 4; ++j) { - this.scatterOptions.colors[4 * i + j] = colors[4 * index + j]; - this.scatterOptions.borderColors[4 * i + j] = borderColors[4 * index + j]; - } - } + // add item for autorange routine + this.expandAxesFast(bounds, markerSize); +}; - this.fancyScatter.update(this.scatterOptions); - } - else { - this.scatterOptions.positions = new Float64Array(0); - this.scatterOptions.glyphs = []; - this.fancyScatter.update(this.scatterOptions); +proto.updateFancy = function(options) { + var scene = this.scene, + xaxis = scene.xaxis, + yaxis = scene.yaxis, + bounds = this.bounds; + + // makeCalcdata runs d2c (data-to-coordinate) on every point + var x = this.pickXData = xaxis.makeCalcdata(options, "x").slice(); + var y = this.pickYData = yaxis.makeCalcdata(options, "y").slice(); + + this.xData = x.slice(); + this.yData = y.slice(); + + // get error values + var errorVals = ErrorBars.calcFromTrace(options, scene.fullLayout); + + var len = x.length, + idToIndex = new Array(len), + positions = new Float64Array(2 * len), + errorsX = new Float64Array(4 * len), + errorsY = new Float64Array(4 * len), + pId = 0, + ptr = 0, + ptrX = 0, + ptrY = 0; + + var getX = xaxis.type === "log" + ? xaxis.d2l + : (function(x) { + return x; + }); + var getY = yaxis.type === "log" + ? yaxis.d2l + : (function(y) { + return y; + }); + + var i, j, xx, yy, ex0, ex1, ey0, ey1; + + for (i = 0; i < len; ++i) { + this.xData[i] = xx = getX(x[i]); + this.yData[i] = yy = getY(y[i]); + + if (isNaN(xx) || isNaN(yy)) continue; + + idToIndex[pId++] = i; + + positions[ptr++] = xx; + positions[ptr++] = yy; + + ex0 = errorsX[ptrX++] = xx - errorVals[i].xs || 0; + ex1 = errorsX[ptrX++] = errorVals[i].xh - xx || 0; + errorsX[ptrX++] = 0; + errorsX[ptrX++] = 0; + + errorsY[ptrY++] = 0; + errorsY[ptrY++] = 0; + ey0 = errorsY[ptrY++] = yy - errorVals[i].ys || 0; + ey1 = errorsY[ptrY++] = errorVals[i].yh - yy || 0; + + bounds[0] = Math.min(bounds[0], xx - ex0); + bounds[1] = Math.min(bounds[1], yy - ey0); + bounds[2] = Math.max(bounds[2], xx + ex1); + bounds[3] = Math.max(bounds[3], yy + ey1); + } + + positions = truncate(positions, ptr); + this.idToIndex = idToIndex; + + this.updateLines(options, positions); + this.updateError("X", options, positions, errorsX); + this.updateError("Y", options, positions, errorsY); + + var sizes; + + if (this.hasMarkers) { + this.scatterOptions.positions = positions; + + // TODO rewrite convert function so that + // we don't have to loop through the data another time + this.scatterOptions.sizes = new Array(pId); + this.scatterOptions.glyphs = new Array(pId); + this.scatterOptions.borderWidths = new Array(pId); + this.scatterOptions.colors = new Array(pId * 4); + this.scatterOptions.borderColors = new Array(pId * 4); + + var markerSizeFunc = makeBubbleSizeFn(options), + markerOpts = options.marker, + markerOpacity = markerOpts.opacity, + traceOpacity = options.opacity, + colors = convertColorScale(markerOpts, markerOpacity, traceOpacity, len), + glyphs = convertSymbol(markerOpts.symbol, len), + borderWidths = convertNumber(markerOpts.line.width, len), + borderColors = convertColorScale( + markerOpts.line, + markerOpacity, + traceOpacity, + len + ), + index; + + sizes = convertArray(markerSizeFunc, markerOpts.size, len); + + for (i = 0; i < pId; ++i) { + index = idToIndex[i]; + + this.scatterOptions.sizes[i] = 4.0 * sizes[index]; + this.scatterOptions.glyphs[i] = glyphs[index]; + this.scatterOptions.borderWidths[i] = 0.5 * borderWidths[index]; + + for (j = 0; j < 4; ++j) { + this.scatterOptions.colors[4 * i + j] = colors[4 * index + j]; + this.scatterOptions.borderColors[4 * i + j] = borderColors[ + 4 * index + j + ]; + } } - // turn off fast scatter plot + this.fancyScatter.update(this.scatterOptions); + } else { this.scatterOptions.positions = new Float64Array(0); this.scatterOptions.glyphs = []; - this.scatter.update(this.scatterOptions); + this.fancyScatter.update(this.scatterOptions); + } - // add item for autorange routine - this.expandAxesFancy(x, y, sizes); + // turn off fast scatter plot + this.scatterOptions.positions = new Float64Array(0); + this.scatterOptions.glyphs = []; + this.scatter.update(this.scatterOptions); + + // add item for autorange routine + this.expandAxesFancy(x, y, sizes); }; proto.updateLines = function(options, positions) { - var i; + var i; - if(this.hasLines) { - var linePositions = positions; + if (this.hasLines) { + var linePositions = positions; - if(!options.connectgaps) { - var p = 0; - var x = this.xData; - var y = this.yData; - linePositions = new Float64Array(2 * x.length); + if (!options.connectgaps) { + var p = 0; + var x = this.xData; + var y = this.yData; + linePositions = new Float64Array(2 * x.length); - for(i = 0; i < x.length; ++i) { - linePositions[p++] = x[i]; - linePositions[p++] = y[i]; - } - } + for (i = 0; i < x.length; ++i) { + linePositions[p++] = x[i]; + linePositions[p++] = y[i]; + } + } - this.lineOptions.positions = linePositions; + this.lineOptions.positions = linePositions; - var lineColor = convertColor(options.line.color, options.opacity, 1), - lineWidth = Math.round(0.5 * this.lineOptions.width), - dashes = (DASHES[options.line.dash] || [1]).slice(); + var lineColor = convertColor(options.line.color, options.opacity, 1), + lineWidth = Math.round(0.5 * this.lineOptions.width), + dashes = (DASHES[options.line.dash] || [1]).slice(); - for(i = 0; i < dashes.length; ++i) dashes[i] *= lineWidth; + for (i = 0; i < dashes.length; ++i) { + dashes[i] *= lineWidth; + } - switch(options.fill) { - case 'tozeroy': - this.lineOptions.fill = [false, true, false, false]; - break; - case 'tozerox': - this.lineOptions.fill = [true, false, false, false]; - break; - default: - this.lineOptions.fill = [false, false, false, false]; - break; - } + switch (options.fill) { + case "tozeroy": + this.lineOptions.fill = [false, true, false, false]; + break; + case "tozerox": + this.lineOptions.fill = [true, false, false, false]; + break; + default: + this.lineOptions.fill = [false, false, false, false]; + break; + } - var fillColor = str2RGBArray(options.fillcolor); + var fillColor = str2RGBArray(options.fillcolor); - this.lineOptions.color = lineColor; - this.lineOptions.width = 2.0 * options.line.width; - this.lineOptions.dashes = dashes; - this.lineOptions.fillColor = [fillColor, fillColor, fillColor, fillColor]; - } - else { - this.lineOptions.positions = new Float64Array(0); - } + this.lineOptions.color = lineColor; + this.lineOptions.width = 2.0 * options.line.width; + this.lineOptions.dashes = dashes; + this.lineOptions.fillColor = [fillColor, fillColor, fillColor, fillColor]; + } else { + this.lineOptions.positions = new Float64Array(0); + } - this.line.update(this.lineOptions); + this.line.update(this.lineOptions); }; proto.updateError = function(axLetter, options, positions, errors) { - var errorObj = this['error' + axLetter], - errorOptions = options['error_' + axLetter.toLowerCase()], - errorObjOptions = this['error' + axLetter + 'Options']; - - if(axLetter.toLowerCase() === 'x' && errorOptions.copy_ystyle) { - errorOptions = options.error_y; - } - - if(this['hasError' + axLetter]) { - errorObjOptions.positions = positions; - errorObjOptions.errors = errors; - errorObjOptions.capSize = errorOptions.width; - errorObjOptions.lineWidth = errorOptions.thickness / 2; // ballpark rescaling - errorObjOptions.color = convertColor(errorOptions.color, 1, 1); - } - else { - errorObjOptions.positions = new Float64Array(0); - } - - errorObj.update(errorObjOptions); + var errorObj = this["error" + axLetter], + errorOptions = options["error_" + axLetter.toLowerCase()], + errorObjOptions = this["error" + axLetter + "Options"]; + + if (axLetter.toLowerCase() === "x" && errorOptions.copy_ystyle) { + errorOptions = options.error_y; + } + + if (this["hasError" + axLetter]) { + errorObjOptions.positions = positions; + errorObjOptions.errors = errors; + errorObjOptions.capSize = errorOptions.width; + errorObjOptions.lineWidth = errorOptions.thickness / 2; + // ballpark rescaling + errorObjOptions.color = convertColor(errorOptions.color, 1, 1); + } else { + errorObjOptions.positions = new Float64Array(0); + } + + errorObj.update(errorObjOptions); }; proto.expandAxesFast = function(bounds, markerSize) { - var pad = markerSize || 10; - var ax, min, max; + var pad = markerSize || 10; + var ax, min, max; - for(var i = 0; i < 2; i++) { - ax = this.scene[AXES[i]]; + for (var i = 0; i < 2; i++) { + ax = this.scene[AXES[i]]; - min = ax._min; - if(!min) min = []; - min.push({ val: bounds[i], pad: pad }); + min = ax._min; + if (!min) min = []; + min.push({ val: bounds[i], pad: pad }); - max = ax._max; - if(!max) max = []; - max.push({ val: bounds[i + 2], pad: pad }); - } + max = ax._max; + if (!max) max = []; + max.push({ val: bounds[i + 2], pad: pad }); + } }; // not quite on-par with 'scatter' (scatter fill in several other expand options) // but close enough for now proto.expandAxesFancy = function(x, y, ppad) { - var scene = this.scene, - expandOpts = { padded: true, ppad: ppad }; + var scene = this.scene, expandOpts = { padded: true, ppad: ppad }; - Axes.expand(scene.xaxis, x, expandOpts); - Axes.expand(scene.yaxis, y, expandOpts); + Axes.expand(scene.xaxis, x, expandOpts); + Axes.expand(scene.yaxis, y, expandOpts); }; proto.dispose = function() { - this.line.dispose(); - this.errorX.dispose(); - this.errorY.dispose(); - this.scatter.dispose(); - this.fancyScatter.dispose(); + this.line.dispose(); + this.errorX.dispose(); + this.errorY.dispose(); + this.scatter.dispose(); + this.fancyScatter.dispose(); }; function createLineWithMarkers(scene, data) { - var plot = new LineWithMarkers(scene, data.uid); - plot.update(data); - return plot; + var plot = new LineWithMarkers(scene, data.uid); + plot.update(data); + return plot; } module.exports = createLineWithMarkers; diff --git a/src/traces/scattergl/defaults.js b/src/traces/scattergl/defaults.js index 442363ae113..7f4da223ddd 100644 --- a/src/traces/scattergl/defaults.js +++ b/src/traces/scattergl/defaults.js @@ -5,51 +5,55 @@ * 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 constants = require('../scatter/constants'); -var subTypes = require('../scatter/subtypes'); -var handleXYDefaults = require('../scatter/xy_defaults'); -var handleMarkerDefaults = require('../scatter/marker_defaults'); -var handleLineDefaults = require('../scatter/line_defaults'); -var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); -var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); - -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var len = handleXYDefaults(traceIn, traceOut, layout, coerce); - if(!len) { - traceOut.visible = false; - return; - } - - coerce('text'); - coerce('mode', len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines'); - - if(subTypes.hasLines(traceOut)) { - coerce('connectgaps'); - handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); - } - - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); - } - - coerce('fill'); - if(traceOut.fill !== 'none') { - handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); - } - - errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); - errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); +"use strict"; +var Lib = require("../../lib"); + +var constants = require("../scatter/constants"); +var subTypes = require("../scatter/subtypes"); +var handleXYDefaults = require("../scatter/xy_defaults"); +var handleMarkerDefaults = require("../scatter/marker_defaults"); +var handleLineDefaults = require("../scatter/line_defaults"); +var handleFillColorDefaults = require("../scatter/fillcolor_defaults"); +var errorBarsSupplyDefaults = require("../../components/errorbars/defaults"); + +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var len = handleXYDefaults(traceIn, traceOut, layout, coerce); + if (!len) { + traceOut.visible = false; + return; + } + + coerce("text"); + coerce("mode", len < constants.PTS_LINESONLY ? "lines+markers" : "lines"); + + if (subTypes.hasLines(traceOut)) { + coerce("connectgaps"); + handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + if (subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + coerce("fill"); + if (traceOut.fill !== "none") { + handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); + } + + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, { axis: "y" }); + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, { + axis: "x", + inherit: "y" + }); }; diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index d5e71241a46..a067583df25 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -5,30 +5,34 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; var ScatterGl = {}; -ScatterGl.attributes = require('./attributes'); -ScatterGl.supplyDefaults = require('./defaults'); -ScatterGl.colorbar = require('../scatter/colorbar'); +ScatterGl.attributes = require("./attributes"); +ScatterGl.supplyDefaults = require("./defaults"); +ScatterGl.colorbar = require("../scatter/colorbar"); // reuse the Scatter3D 'dummy' calc step so that legends know what to do -ScatterGl.calc = require('../scatter3d/calc'); -ScatterGl.plot = require('./convert'); +ScatterGl.calc = require("../scatter3d/calc"); +ScatterGl.plot = require("./convert"); -ScatterGl.moduleType = 'trace'; -ScatterGl.name = 'scattergl'; -ScatterGl.basePlotModule = require('../../plots/gl2d'); -ScatterGl.categories = ['gl2d', 'symbols', 'errorBarsOK', 'markerColorscale', 'showLegend']; +ScatterGl.moduleType = "trace"; +ScatterGl.name = "scattergl"; +ScatterGl.basePlotModule = require("../../plots/gl2d"); +ScatterGl.categories = [ + "gl2d", + "symbols", + "errorBarsOK", + "markerColorscale", + "showLegend" +]; ScatterGl.meta = { - description: [ - 'The data visualized as scatter point or lines is set in `x` and `y`', - 'using the WebGl plotting engine.', - 'Bubble charts are achieved by setting `marker.size` and/or `marker.color`', - 'to a numerical arrays.' - ].join(' ') + description: [ + "The data visualized as scatter point or lines is set in `x` and `y`", + "using the WebGl plotting engine.", + "Bubble charts are achieved by setting `marker.size` and/or `marker.color`", + "to a numerical arrays." + ].join(" ") }; module.exports = ScatterGl; diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js index 034f85a0b43..82d0164774c 100644 --- a/src/traces/scattermapbox/attributes.js +++ b/src/traces/scattermapbox/attributes.js @@ -5,102 +5,86 @@ * 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 scatterGeoAttrs = require("../scattergeo/attributes"); +var scatterAttrs = require("../scatter/attributes"); +var mapboxAttrs = require("../../plots/mapbox/layout_attributes"); +var plotAttrs = require("../../plots/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); -'use strict'; - -var scatterGeoAttrs = require('../scattergeo/attributes'); -var scatterAttrs = require('../scatter/attributes'); -var mapboxAttrs = require('../../plots/mapbox/layout_attributes'); -var plotAttrs = require('../../plots/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; +var extendFlat = require("../../lib/extend").extendFlat; var lineAttrs = scatterGeoAttrs.line; var markerAttrs = scatterGeoAttrs.marker; - module.exports = { - lon: scatterGeoAttrs.lon, - lat: scatterGeoAttrs.lat, - - // locations - // locationmode - - mode: { - valType: 'flaglist', - flags: ['lines', 'markers', 'text'], - dflt: 'markers', - extras: ['none'], - role: 'info', - description: [ - 'Determines the drawing mode for this scatter trace.', - 'If the provided `mode` includes *text* then the `text` elements', - 'appear at the coordinates. Otherwise, the `text` elements', - 'appear on hover.' - ].join(' ') - }, - - text: extendFlat({}, scatterAttrs.text, { - description: [ - 'Sets text elements associated with each (lon,lat) pair', - 'If a single string, the same string appears over', - 'all the data points.', - 'If an array of string, the items are mapped in order to the', - 'this trace\'s (lon,lat) coordinates.' - ].join(' ') - }), - - line: { - color: lineAttrs.color, - width: lineAttrs.width, - - // TODO - dash: lineAttrs.dash - }, - - connectgaps: scatterAttrs.connectgaps, - - marker: { - symbol: { - valType: 'string', - dflt: 'circle', - role: 'style', - arrayOk: true, - description: [ - 'Sets the marker symbol.', - 'Full list: https://www.mapbox.com/maki-icons/', - 'Note that the array `marker.color` and `marker.size`', - 'are only available for *circle* symbols.' - ].join(' ') - }, - opacity: extendFlat({}, markerAttrs.opacity, { - arrayOk: false - }), - size: markerAttrs.size, - sizeref: markerAttrs.sizeref, - sizemin: markerAttrs.sizemin, - sizemode: markerAttrs.sizemode, - color: markerAttrs.color, - colorscale: markerAttrs.colorscale, - cauto: markerAttrs.cauto, - cmax: markerAttrs.cmax, - cmin: markerAttrs.cmin, - autocolorscale: markerAttrs.autocolorscale, - reversescale: markerAttrs.reversescale, - showscale: markerAttrs.showscale, - colorbar: colorbarAttrs - - // line + lon: scatterGeoAttrs.lon, + lat: scatterGeoAttrs.lat, + // locations + // locationmode + mode: { + valType: "flaglist", + flags: ["lines", "markers", "text"], + dflt: "markers", + extras: ["none"], + role: "info", + description: [ + "Determines the drawing mode for this scatter trace.", + "If the provided `mode` includes *text* then the `text` elements", + "appear at the coordinates. Otherwise, the `text` elements", + "appear on hover." + ].join(" ") + }, + text: extendFlat({}, scatterAttrs.text, { + description: [ + "Sets text elements associated with each (lon,lat) pair", + "If a single string, the same string appears over", + "all the data points.", + "If an array of string, the items are mapped in order to the", + "this trace's (lon,lat) coordinates." + ].join(" ") + }), + line: { + color: lineAttrs.color, + width: lineAttrs.width, + // TODO + dash: lineAttrs.dash + }, + connectgaps: scatterAttrs.connectgaps, + marker: { + symbol: { + valType: "string", + dflt: "circle", + role: "style", + arrayOk: true, + description: [ + "Sets the marker symbol.", + "Full list: https://www.mapbox.com/maki-icons/", + "Note that the array `marker.color` and `marker.size`", + "are only available for *circle* symbols." + ].join(" ") }, - - fill: scatterGeoAttrs.fill, - fillcolor: scatterAttrs.fillcolor, - - textfont: mapboxAttrs.layers.symbol.textfont, - textposition: mapboxAttrs.layers.symbol.textposition, - - hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { - flags: ['lon', 'lat', 'text', 'name'] - }), + opacity: extendFlat({}, markerAttrs.opacity, { arrayOk: false }), + size: markerAttrs.size, + sizeref: markerAttrs.sizeref, + sizemin: markerAttrs.sizemin, + sizemode: markerAttrs.sizemode, + color: markerAttrs.color, + colorscale: markerAttrs.colorscale, + cauto: markerAttrs.cauto, + cmax: markerAttrs.cmax, + cmin: markerAttrs.cmin, + autocolorscale: markerAttrs.autocolorscale, + reversescale: markerAttrs.reversescale, + showscale: markerAttrs.showscale, + // line + colorbar: colorbarAttrs + }, + fill: scatterGeoAttrs.fill, + fillcolor: scatterAttrs.fillcolor, + textfont: mapboxAttrs.layers.symbol.textfont, + textposition: mapboxAttrs.layers.symbol.textposition, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { + flags: ["lon", "lat", "text", "name"] + }) }; diff --git a/src/traces/scattermapbox/calc.js b/src/traces/scattermapbox/calc.js index 6f6230851e8..2000dea36d3 100644 --- a/src/traces/scattermapbox/calc.js +++ b/src/traces/scattermapbox/calc.js @@ -5,97 +5,84 @@ * 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 isNumeric = require("fast-isnumeric"); +var Lib = require("../../lib"); +var Colorscale = require("../../components/colorscale"); -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Colorscale = require('../../components/colorscale'); - -var subtypes = require('../scatter/subtypes'); -var calcMarkerColorscale = require('../scatter/colorscale_calc'); -var makeBubbleSizeFn = require('../scatter/make_bubble_size_func'); - +var subtypes = require("../scatter/subtypes"); +var calcMarkerColorscale = require("../scatter/colorscale_calc"); +var makeBubbleSizeFn = require("../scatter/make_bubble_size_func"); module.exports = function calc(gd, trace) { - var len = trace.lon.length, - marker = trace.marker; - - var hasMarkers = subtypes.hasMarkers(trace), - hasColorArray = (hasMarkers && Array.isArray(marker.color)), - hasSizeArray = (hasMarkers && Array.isArray(marker.size)), - hasSymbolArray = (hasMarkers && Array.isArray(marker.symbol)), - hasTextArray = Array.isArray(trace.text); + var len = trace.lon.length, marker = trace.marker; - calcMarkerColorscale(trace); + var hasMarkers = subtypes.hasMarkers(trace), + hasColorArray = hasMarkers && Array.isArray(marker.color), + hasSizeArray = hasMarkers && Array.isArray(marker.size), + hasSymbolArray = hasMarkers && Array.isArray(marker.symbol), + hasTextArray = Array.isArray(trace.text); - var colorFn = Colorscale.hasColorscale(trace, 'marker') ? - Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - marker.colorscale, - marker.cmin, - marker.cmax - ) - ) : - Lib.identity; + calcMarkerColorscale(trace); - var sizeFn = subtypes.isBubble(trace) ? - makeBubbleSizeFn(trace) : - Lib.identity; + var colorFn = Colorscale.hasColorscale(trace, "marker") + ? Colorscale.makeColorScaleFunc( + Colorscale.extractScale(marker.colorscale, marker.cmin, marker.cmax) + ) + : Lib.identity; - var calcTrace = [], - cnt = 0; + var sizeFn = subtypes.isBubble(trace) + ? makeBubbleSizeFn(trace) + : Lib.identity; - // Different than cartesian calc step - // as skip over non-numeric lon, lat pairs. - // This makes the hover and convert calculations simpler. + var calcTrace = [], cnt = 0; - for(var i = 0; i < len; i++) { - var lon = trace.lon[i], - lat = trace.lat[i]; + // Different than cartesian calc step + // as skip over non-numeric lon, lat pairs. + // This makes the hover and convert calculations simpler. + for (var i = 0; i < len; i++) { + var lon = trace.lon[i], lat = trace.lat[i]; - if(!isNumeric(lon) || !isNumeric(lat)) { - if(cnt > 0) calcTrace[cnt - 1].gapAfter = true; - continue; - } - - var calcPt = {}; - cnt++; - - // coerce numeric strings into numbers - calcPt.lonlat = [+lon, +lat]; + if (!isNumeric(lon) || !isNumeric(lat)) { + if (cnt > 0) calcTrace[cnt - 1].gapAfter = true; + continue; + } - if(hasMarkers) { + var calcPt = {}; + cnt++; - if(hasColorArray) { - var mc = marker.color[i]; + // coerce numeric strings into numbers + calcPt.lonlat = [+lon, +lat]; - calcPt.mc = mc; - calcPt.mcc = colorFn(mc); - } + if (hasMarkers) { + if (hasColorArray) { + var mc = marker.color[i]; - if(hasSizeArray) { - var ms = marker.size[i]; + calcPt.mc = mc; + calcPt.mcc = colorFn(mc); + } - calcPt.ms = ms; - calcPt.mrc = sizeFn(ms); - } + if (hasSizeArray) { + var ms = marker.size[i]; - if(hasSymbolArray) { - var mx = marker.symbol[i]; - calcPt.mx = (typeof mx === 'string') ? mx : 'circle'; - } - } + calcPt.ms = ms; + calcPt.mrc = sizeFn(ms); + } - if(hasTextArray) { - var tx = trace.text[i]; - calcPt.tx = (typeof tx === 'string') ? tx : ''; - } + if (hasSymbolArray) { + var mx = marker.symbol[i]; + calcPt.mx = typeof mx === "string" ? mx : "circle"; + } + } - calcTrace.push(calcPt); + if (hasTextArray) { + var tx = trace.text[i]; + calcPt.tx = typeof tx === "string" ? tx : ""; } - return calcTrace; + calcTrace.push(calcPt); + } + + return calcTrace; }; diff --git a/src/traces/scattermapbox/convert.js b/src/traces/scattermapbox/convert.js index 3e46c8d8950..1084999f92b 100644 --- a/src/traces/scattermapbox/convert.js +++ b/src/traces/scattermapbox/convert.js @@ -5,140 +5,124 @@ * 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 geoJsonUtils = require("../../lib/geojson_utils"); +var subTypes = require("../scatter/subtypes"); +var convertTextOpts = require("../../plots/mapbox/convert_text_opts"); -'use strict'; - -var Lib = require('../../lib'); -var geoJsonUtils = require('../../lib/geojson_utils'); - -var subTypes = require('../scatter/subtypes'); -var convertTextOpts = require('../../plots/mapbox/convert_text_opts'); - -var COLOR_PROP = 'circle-color'; -var SIZE_PROP = 'circle-radius'; - +var COLOR_PROP = "circle-color"; +var SIZE_PROP = "circle-radius"; module.exports = function convert(calcTrace) { - var trace = calcTrace[0].trace; - - var isVisible = (trace.visible === true), - hasFill = (trace.fill !== 'none'), - hasLines = subTypes.hasLines(trace), - hasMarkers = subTypes.hasMarkers(trace), - hasText = subTypes.hasText(trace), - hasCircles = (hasMarkers && trace.marker.symbol === 'circle'), - hasSymbols = (hasMarkers && trace.marker.symbol !== 'circle'); - - var fill = initContainer(), - line = initContainer(), - circle = initContainer(), - symbol = initContainer(); - - var opts = { - fill: fill, - line: line, - circle: circle, - symbol: symbol - }; - - // early return if not visible or placeholder - if(!isVisible || calcTrace[0].placeholder) return opts; - - // fill layer and line layer use the same coords - var coords; - if(hasFill || hasLines) { - coords = geoJsonUtils.calcTraceToLineCoords(calcTrace); - } - - if(hasFill) { - fill.geojson = geoJsonUtils.makePolygon(coords); - fill.layout.visibility = 'visible'; - - Lib.extendFlat(fill.paint, { - 'fill-color': trace.fillcolor - }); - } - - if(hasLines) { - line.geojson = geoJsonUtils.makeLine(coords); - line.layout.visibility = 'visible'; - - Lib.extendFlat(line.paint, { - 'line-width': trace.line.width, - 'line-color': trace.line.color, - 'line-opacity': trace.opacity - }); - - // TODO convert line.dash into line-dasharray + var trace = calcTrace[0].trace; + + var isVisible = trace.visible === true, + hasFill = trace.fill !== "none", + hasLines = subTypes.hasLines(trace), + hasMarkers = subTypes.hasMarkers(trace), + hasText = subTypes.hasText(trace), + hasCircles = hasMarkers && trace.marker.symbol === "circle", + hasSymbols = hasMarkers && trace.marker.symbol !== "circle"; + + var fill = initContainer(), + line = initContainer(), + circle = initContainer(), + symbol = initContainer(); + + var opts = { fill: fill, line: line, circle: circle, symbol: symbol }; + + // early return if not visible or placeholder + if (!isVisible || calcTrace[0].placeholder) return opts; + + // fill layer and line layer use the same coords + var coords; + if (hasFill || hasLines) { + coords = geoJsonUtils.calcTraceToLineCoords(calcTrace); + } + + if (hasFill) { + fill.geojson = geoJsonUtils.makePolygon(coords); + fill.layout.visibility = "visible"; + + Lib.extendFlat(fill.paint, { "fill-color": trace.fillcolor }); + } + + if (hasLines) { + line.geojson = geoJsonUtils.makeLine(coords); + line.layout.visibility = "visible"; + + Lib.extendFlat(line.paint, { + "line-width": trace.line.width, + "line-color": trace.line.color, + "line-opacity": trace.opacity + }); + // TODO convert line.dash into line-dasharray + } + + if (hasCircles) { + var hash = {}; + hash[COLOR_PROP] = {}; + hash[SIZE_PROP] = {}; + + circle.geojson = makeCircleGeoJSON(calcTrace, hash); + circle.layout.visibility = "visible"; + + Lib.extendFlat(circle.paint, { + "circle-opacity": trace.opacity * trace.marker.opacity, + "circle-color": calcCircleColor(trace, hash), + "circle-radius": calcCircleRadius(trace, hash) + }); + } + + if (hasSymbols || hasText) { + symbol.geojson = makeSymbolGeoJSON(calcTrace); + + Lib.extendFlat(symbol.layout, { + visibility: "visible", + "icon-image": "{symbol}-15", + "text-field": "{text}" + }); + + if (hasSymbols) { + Lib.extendFlat(symbol.layout, { "icon-size": trace.marker.size / 10 }); + + Lib.extendFlat(symbol.paint, { + "icon-opacity": trace.opacity * trace.marker.opacity, + // TODO does not work ?? + "icon-color": trace.marker.color + }); } - if(hasCircles) { - var hash = {}; - hash[COLOR_PROP] = {}; - hash[SIZE_PROP] = {}; - - circle.geojson = makeCircleGeoJSON(calcTrace, hash); - circle.layout.visibility = 'visible'; - - Lib.extendFlat(circle.paint, { - 'circle-opacity': trace.opacity * trace.marker.opacity, - 'circle-color': calcCircleColor(trace, hash), - 'circle-radius': calcCircleRadius(trace, hash) - }); + if (hasText) { + var iconSize = (trace.marker || {}).size, + textOpts = convertTextOpts(trace.textposition, iconSize); + + Lib.extendFlat(symbol.layout, { + "text-size": trace.textfont.size, + "text-anchor": textOpts.anchor, + // TODO font family + // 'text-font': symbol.textfont.family.split(', '), + "text-offset": textOpts.offset + }); + + Lib.extendFlat(symbol.paint, { + "text-color": trace.textfont.color, + "text-opacity": trace.opacity + }); } + } - if(hasSymbols || hasText) { - symbol.geojson = makeSymbolGeoJSON(calcTrace); - - Lib.extendFlat(symbol.layout, { - visibility: 'visible', - 'icon-image': '{symbol}-15', - 'text-field': '{text}' - }); - - if(hasSymbols) { - Lib.extendFlat(symbol.layout, { - 'icon-size': trace.marker.size / 10 - }); - - Lib.extendFlat(symbol.paint, { - 'icon-opacity': trace.opacity * trace.marker.opacity, - - // TODO does not work ?? - 'icon-color': trace.marker.color - }); - } - - if(hasText) { - var iconSize = (trace.marker || {}).size, - textOpts = convertTextOpts(trace.textposition, iconSize); - - Lib.extendFlat(symbol.layout, { - 'text-size': trace.textfont.size, - 'text-anchor': textOpts.anchor, - 'text-offset': textOpts.offset - - // TODO font family - // 'text-font': symbol.textfont.family.split(', '), - }); - - Lib.extendFlat(symbol.paint, { - 'text-color': trace.textfont.color, - 'text-opacity': trace.opacity - }); - } - } - - return opts; + return opts; }; function initContainer() { - return { - geojson: geoJsonUtils.makeBlank(), - layout: { visibility: 'none' }, - paint: {} - }; + return { + geojson: geoJsonUtils.makeBlank(), + layout: { visibility: "none" }, + paint: {} + }; } // N.B. `hash` is mutated here @@ -156,152 +140,122 @@ function initContainer() { // The solution prove to be more robust than trying to generate // `stops` arrays from scale functions. function makeCircleGeoJSON(calcTrace, hash) { - var trace = calcTrace[0].trace; + var trace = calcTrace[0].trace; - var marker = trace.marker, - hasColorArray = Array.isArray(marker.color), - hasSizeArray = Array.isArray(marker.size); + var marker = trace.marker, + hasColorArray = Array.isArray(marker.color), + hasSizeArray = Array.isArray(marker.size); - // Translate vals in trace arrayOk containers - // into a val-to-index hash object - function translate(props, key, val, index) { - if(hash[key][val] === undefined) hash[key][val] = index; + // Translate vals in trace arrayOk containers + // into a val-to-index hash object + function translate(props, key, val, index) { + if (hash[key][val] === undefined) hash[key][val] = index; - props[key] = hash[key][val]; - } + props[key] = hash[key][val]; + } - var features = []; + var features = []; - for(var i = 0; i < calcTrace.length; i++) { - var calcPt = calcTrace[i]; + for (var i = 0; i < calcTrace.length; i++) { + var calcPt = calcTrace[i]; - var props = {}; - if(hasColorArray) translate(props, COLOR_PROP, calcPt.mcc, i); - if(hasSizeArray) translate(props, SIZE_PROP, calcPt.mrc, i); + var props = {}; + if (hasColorArray) translate(props, COLOR_PROP, calcPt.mcc, i); + if (hasSizeArray) translate(props, SIZE_PROP, calcPt.mrc, i); - features.push({ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: calcPt.lonlat - }, - properties: props - }); - } + features.push({ + type: "Feature", + geometry: { type: "Point", coordinates: calcPt.lonlat }, + properties: props + }); + } - return { - type: 'FeatureCollection', - features: features - }; + return { type: "FeatureCollection", features: features }; } function makeSymbolGeoJSON(calcTrace) { - var trace = calcTrace[0].trace; - - var marker = trace.marker || {}, - symbol = marker.symbol, - text = trace.text; - - var fillSymbol = (symbol !== 'circle') ? - getFillFunc(symbol) : - blankFillFunc; - - var fillText = subTypes.hasText(trace) ? - getFillFunc(text) : - blankFillFunc; - - var features = []; - - for(var i = 0; i < calcTrace.length; i++) { - var calcPt = calcTrace[i]; - - features.push({ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: calcPt.lonlat - }, - properties: { - symbol: fillSymbol(calcPt.mx), - text: fillText(calcPt.tx) - } - }); - } + var trace = calcTrace[0].trace; - return { - type: 'FeatureCollection', - features: features - }; -} + var marker = trace.marker || {}, symbol = marker.symbol, text = trace.text; -function calcCircleColor(trace, hash) { - var marker = trace.marker, - out; + var fillSymbol = symbol !== "circle" ? getFillFunc(symbol) : blankFillFunc; - if(Array.isArray(marker.color)) { - var vals = Object.keys(hash[COLOR_PROP]), - stops = []; + var fillText = subTypes.hasText(trace) ? getFillFunc(text) : blankFillFunc; - for(var i = 0; i < vals.length; i++) { - var val = vals[i]; + var features = []; - stops.push([ hash[COLOR_PROP][val], val ]); - } + for (var i = 0; i < calcTrace.length; i++) { + var calcPt = calcTrace[i]; - out = { - property: COLOR_PROP, - stops: stops - }; + features.push({ + type: "Feature", + geometry: { type: "Point", coordinates: calcPt.lonlat }, + properties: { symbol: fillSymbol(calcPt.mx), text: fillText(calcPt.tx) } + }); + } - } - else { - out = marker.color; + return { type: "FeatureCollection", features: features }; +} + +function calcCircleColor(trace, hash) { + var marker = trace.marker, out; + + if (Array.isArray(marker.color)) { + var vals = Object.keys(hash[COLOR_PROP]), stops = []; + + for (var i = 0; i < vals.length; i++) { + var val = vals[i]; + + stops.push([hash[COLOR_PROP][val], val]); } - return out; + out = { property: COLOR_PROP, stops: stops }; + } else { + out = marker.color; + } + + return out; } function calcCircleRadius(trace, hash) { - var marker = trace.marker, - out; + var marker = trace.marker, out; - if(Array.isArray(marker.size)) { - var vals = Object.keys(hash[SIZE_PROP]), - stops = []; + if (Array.isArray(marker.size)) { + var vals = Object.keys(hash[SIZE_PROP]), stops = []; - for(var i = 0; i < vals.length; i++) { - var val = vals[i]; + for (var i = 0; i < vals.length; i++) { + var val = vals[i]; - stops.push([ hash[SIZE_PROP][val], +val ]); - } + stops.push([hash[SIZE_PROP][val], +val]); + } - // stops indices must be sorted - stops.sort(function(a, b) { - return a[0] - b[0]; - }); + // stops indices must be sorted + stops.sort(function(a, b) { + return a[0] - b[0]; + }); - out = { - property: SIZE_PROP, - stops: stops - }; - } - else { - out = marker.size / 2; - } + out = { property: SIZE_PROP, stops: stops }; + } else { + out = marker.size / 2; + } - return out; + return out; } function getFillFunc(attr) { - if(Array.isArray(attr)) { - return function(v) { return v; }; - } - else if(attr) { - return function() { return attr; }; - } - else { - return blankFillFunc; - } + if (Array.isArray(attr)) { + return function(v) { + return v; + }; + } else if (attr) { + return function() { + return attr; + }; + } else { + return blankFillFunc; + } } -function blankFillFunc() { return ''; } +function blankFillFunc() { + return ""; +} diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js index 3de5641b76a..c9c3a69be5e 100644 --- a/src/traces/scattermapbox/defaults.js +++ b/src/traces/scattermapbox/defaults.js @@ -5,82 +5,81 @@ * 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 subTypes = require('../scatter/subtypes'); -var handleMarkerDefaults = require('../scatter/marker_defaults'); -var handleLineDefaults = require('../scatter/line_defaults'); -var handleTextDefaults = require('../scatter/text_defaults'); -var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); - -var attributes = require('./attributes'); -var scatterAttrs = require('../scatter/attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - function coerceMarker(attr, dflt) { - var attrs = (attr.indexOf('.line') === -1) ? attributes : scatterAttrs; - - // use 'scatter' attributes for 'marker.line.' attr, - // so that we can reuse the scatter marker defaults - - return Lib.coerce(traceIn, traceOut, attrs, attr, dflt); +"use strict"; +var Lib = require("../../lib"); + +var subTypes = require("../scatter/subtypes"); +var handleMarkerDefaults = require("../scatter/marker_defaults"); +var handleLineDefaults = require("../scatter/line_defaults"); +var handleTextDefaults = require("../scatter/text_defaults"); +var handleFillColorDefaults = require("../scatter/fillcolor_defaults"); + +var attributes = require("./attributes"); +var scatterAttrs = require("../scatter/attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + function coerceMarker(attr, dflt) { + var attrs = attr.indexOf(".line") === -1 ? attributes : scatterAttrs; + + // use 'scatter' attributes for 'marker.line.' attr, + // so that we can reuse the scatter marker defaults + return Lib.coerce(traceIn, traceOut, attrs, attr, dflt); + } + + var len = handleLonLatDefaults(traceIn, traceOut, coerce); + if (!len) { + traceOut.visible = false; + return; + } + + coerce("text"); + coerce("mode"); + + if (subTypes.hasLines(traceOut)) { + handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); + coerce("connectgaps"); + } + + if (subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerceMarker); + + // array marker.size and marker.color are only supported with circles + var marker = traceOut.marker; + + if (marker.symbol !== "circle") { + if (Array.isArray(marker.size)) marker.size = marker.size[0]; + if (Array.isArray(marker.color)) marker.color = marker.color[0]; } + } - var len = handleLonLatDefaults(traceIn, traceOut, coerce); - if(!len) { - traceOut.visible = false; - return; - } + if (subTypes.hasText(traceOut)) { + handleTextDefaults(traceIn, traceOut, layout, coerce); + } - coerce('text'); - coerce('mode'); - - if(subTypes.hasLines(traceOut)) { - handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); - coerce('connectgaps'); - } - - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerceMarker); - - // array marker.size and marker.color are only supported with circles - - var marker = traceOut.marker; - - if(marker.symbol !== 'circle') { - if(Array.isArray(marker.size)) marker.size = marker.size[0]; - if(Array.isArray(marker.color)) marker.color = marker.color[0]; - } - } - - if(subTypes.hasText(traceOut)) { - handleTextDefaults(traceIn, traceOut, layout, coerce); - } - - coerce('fill'); - if(traceOut.fill !== 'none') { - handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); - } + coerce("fill"); + if (traceOut.fill !== "none") { + handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); + } - coerce('hoverinfo', (layout._dataLength === 1) ? 'lon+lat+text' : undefined); + coerce("hoverinfo", layout._dataLength === 1 ? "lon+lat+text" : undefined); }; function handleLonLatDefaults(traceIn, traceOut, coerce) { - var lon = coerce('lon') || []; - var lat = coerce('lat') || []; - var len = Math.min(lon.length, lat.length); + var lon = coerce("lon") || []; + var lat = coerce("lat") || []; + var len = Math.min(lon.length, lat.length); - if(len < lon.length) traceOut.lon = lon.slice(0, len); - if(len < lat.length) traceOut.lat = lat.slice(0, len); + if (len < lon.length) traceOut.lon = lon.slice(0, len); + if (len < lat.length) traceOut.lat = lat.slice(0, len); - return len; + return len; } diff --git a/src/traces/scattermapbox/event_data.js b/src/traces/scattermapbox/event_data.js index 8581a671d78..3d19c282675 100644 --- a/src/traces/scattermapbox/event_data.js +++ b/src/traces/scattermapbox/event_data.js @@ -5,14 +5,10 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - - +"use strict"; module.exports = function eventData(out, pt) { - out.lon = pt.lon; - out.lat = pt.lat; + out.lon = pt.lon; + out.lat = pt.lat; - return out; + return out; }; diff --git a/src/traces/scattermapbox/hover.js b/src/traces/scattermapbox/hover.js index 088d35f6dc5..76238ad4e81 100644 --- a/src/traces/scattermapbox/hover.js +++ b/src/traces/scattermapbox/hover.js @@ -5,90 +5,82 @@ * 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 Fx = require('../../plots/cartesian/graph_interact'); -var getTraceColor = require('../scatter/get_trace_color'); - +"use strict"; +var Fx = require("../../plots/cartesian/graph_interact"); +var getTraceColor = require("../scatter/get_trace_color"); module.exports = function hoverPoints(pointData, xval, yval) { - var cd = pointData.cd, - trace = cd[0].trace, - xa = pointData.xa, - ya = pointData.ya; + var cd = pointData.cd, + trace = cd[0].trace, + xa = pointData.xa, + ya = pointData.ya; - if(cd[0].placeholder) return; + if (cd[0].placeholder) return; - // compute winding number about [-180, 180] globe - var winding = (xval >= 0) ? - Math.floor((xval + 180) / 360) : - Math.ceil((xval - 180) / 360); + // compute winding number about [-180, 180] globe + var winding = xval >= 0 + ? Math.floor((xval + 180) / 360) + : Math.ceil((xval - 180) / 360); - // shift longitude to [-180, 180] to determine closest point - var lonShift = winding * 360; - var xval2 = xval - lonShift; + // shift longitude to [-180, 180] to determine closest point + var lonShift = winding * 360; + var xval2 = xval - lonShift; - function distFn(d) { - var lonlat = d.lonlat, - dx = Math.abs(xa.c2p(lonlat) - xa.c2p([xval2, lonlat[1]])), - dy = Math.abs(ya.c2p(lonlat) - ya.c2p([lonlat[0], yval])), - rad = Math.max(3, d.mrc || 0); + function distFn(d) { + var lonlat = d.lonlat, + dx = Math.abs(xa.c2p(lonlat) - xa.c2p([xval2, lonlat[1]])), + dy = Math.abs(ya.c2p(lonlat) - ya.c2p([lonlat[0], yval])), + rad = Math.max(3, d.mrc || 0); - return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad); - } + return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad); + } - Fx.getClosest(cd, distFn, pointData); + Fx.getClosest(cd, distFn, pointData); - // skip the rest (for this trace) if we didn't find a close point - if(pointData.index === false) return; + // skip the rest (for this trace) if we didn't find a close point + if (pointData.index === false) return; - var di = cd[pointData.index], - lonlat = di.lonlat, - lonlatShifted = [lonlat[0] + lonShift, lonlat[1]]; + var di = cd[pointData.index], + lonlat = di.lonlat, + lonlatShifted = [lonlat[0] + lonShift, lonlat[1]]; - // shift labels back to original winded globe - var xc = xa.c2p(lonlatShifted), - yc = ya.c2p(lonlatShifted), - rad = di.mrc || 1; + // shift labels back to original winded globe + var xc = xa.c2p(lonlatShifted), yc = ya.c2p(lonlatShifted), rad = di.mrc || 1; - pointData.x0 = xc - rad; - pointData.x1 = xc + rad; - pointData.y0 = yc - rad; - pointData.y1 = yc + rad; + pointData.x0 = xc - rad; + pointData.x1 = xc + rad; + pointData.y0 = yc - rad; + pointData.y1 = yc + rad; - pointData.color = getTraceColor(trace, di); - pointData.extraText = getExtraText(trace, di); + pointData.color = getTraceColor(trace, di); + pointData.extraText = getExtraText(trace, di); - return [pointData]; + return [pointData]; }; function getExtraText(trace, di) { - var hoverinfo = trace.hoverinfo.split('+'), - isAll = (hoverinfo.indexOf('all') !== -1), - hasLon = (hoverinfo.indexOf('lon') !== -1), - hasLat = (hoverinfo.indexOf('lat') !== -1); - - var lonlat = di.lonlat, - text = []; - - // TODO should we use a mock axis to format hover? - // If so, we'll need to make precision be zoom-level dependent - function format(v) { - return v + '\u00B0'; - } - - if(isAll || (hasLon && hasLat)) { - text.push('(' + format(lonlat[0]) + ', ' + format(lonlat[1]) + ')'); - } - else if(hasLon) text.push('lon: ' + format(lonlat[0])); - else if(hasLat) text.push('lat: ' + format(lonlat[1])); - - if(isAll || hoverinfo.indexOf('text') !== -1) { - var tx = di.tx || trace.text; - if(!Array.isArray(tx)) text.push(tx); - } - - return text.join('
'); + var hoverinfo = trace.hoverinfo.split("+"), + isAll = hoverinfo.indexOf("all") !== -1, + hasLon = hoverinfo.indexOf("lon") !== -1, + hasLat = hoverinfo.indexOf("lat") !== -1; + + var lonlat = di.lonlat, text = []; + + // TODO should we use a mock axis to format hover? + // If so, we'll need to make precision be zoom-level dependent + function format(v) { + return v + "\xB0"; + } + + if (isAll || hasLon && hasLat) { + text.push("(" + format(lonlat[0]) + ", " + format(lonlat[1]) + ")"); + } else if (hasLon) text.push("lon: " + format(lonlat[0])); + else if (hasLat) text.push("lat: " + format(lonlat[1])); + + if (isAll || hoverinfo.indexOf("text") !== -1) { + var tx = di.tx || trace.text; + if (!Array.isArray(tx)) text.push(tx); + } + + return text.join("
"); } diff --git a/src/traces/scattermapbox/index.js b/src/traces/scattermapbox/index.js index fa32f9847a0..c76e322a43f 100644 --- a/src/traces/scattermapbox/index.js +++ b/src/traces/scattermapbox/index.js @@ -5,31 +5,34 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - - +"use strict"; var ScatterMapbox = {}; -ScatterMapbox.attributes = require('./attributes'); -ScatterMapbox.supplyDefaults = require('./defaults'); -ScatterMapbox.colorbar = require('../scatter/colorbar'); -ScatterMapbox.calc = require('./calc'); -ScatterMapbox.hoverPoints = require('./hover'); -ScatterMapbox.eventData = require('./event_data'); -ScatterMapbox.plot = require('./plot'); +ScatterMapbox.attributes = require("./attributes"); +ScatterMapbox.supplyDefaults = require("./defaults"); +ScatterMapbox.colorbar = require("../scatter/colorbar"); +ScatterMapbox.calc = require("./calc"); +ScatterMapbox.hoverPoints = require("./hover"); +ScatterMapbox.eventData = require("./event_data"); +ScatterMapbox.plot = require("./plot"); -ScatterMapbox.moduleType = 'trace'; -ScatterMapbox.name = 'scattermapbox'; -ScatterMapbox.basePlotModule = require('../../plots/mapbox'); -ScatterMapbox.categories = ['mapbox', 'gl', 'symbols', 'markerColorscale', 'showLegend']; +ScatterMapbox.moduleType = "trace"; +ScatterMapbox.name = "scattermapbox"; +ScatterMapbox.basePlotModule = require("../../plots/mapbox"); +ScatterMapbox.categories = [ + "mapbox", + "gl", + "symbols", + "markerColorscale", + "showLegend" +]; ScatterMapbox.meta = { - hrName: 'scatter_mapbox', - description: [ - 'The data visualized as scatter point, lines or marker symbols', - 'on a Mapbox GL geographic map', - 'is provided by longitude/latitude pairs in `lon` and `lat`.' - ].join(' ') + hrName: "scatter_mapbox", + description: [ + "The data visualized as scatter point, lines or marker symbols", + "on a Mapbox GL geographic map", + "is provided by longitude/latitude pairs in `lon` and `lat`." + ].join(" ") }; module.exports = ScatterMapbox; diff --git a/src/traces/scattermapbox/plot.js b/src/traces/scattermapbox/plot.js index f7cc4040a2f..00c0783f62c 100644 --- a/src/traces/scattermapbox/plot.js +++ b/src/traces/scattermapbox/plot.js @@ -5,118 +5,129 @@ * 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 convert = require('./convert'); - +"use strict"; +var convert = require("./convert"); function ScatterMapbox(mapbox, uid) { - this.mapbox = mapbox; - this.map = mapbox.map; - - this.uid = uid; - - this.idSourceFill = uid + '-source-fill'; - this.idSourceLine = uid + '-source-line'; - this.idSourceCircle = uid + '-source-circle'; - this.idSourceSymbol = uid + '-source-symbol'; - - this.idLayerFill = uid + '-layer-fill'; - this.idLayerLine = uid + '-layer-line'; - this.idLayerCircle = uid + '-layer-circle'; - this.idLayerSymbol = uid + '-layer-symbol'; - - this.mapbox.initSource(this.idSourceFill); - this.mapbox.initSource(this.idSourceLine); - this.mapbox.initSource(this.idSourceCircle); - this.mapbox.initSource(this.idSourceSymbol); - - this.map.addLayer({ - id: this.idLayerFill, - source: this.idSourceFill, - type: 'fill' - }); - - this.map.addLayer({ - id: this.idLayerLine, - source: this.idSourceLine, - type: 'line' - }); - - this.map.addLayer({ - id: this.idLayerCircle, - source: this.idSourceCircle, - type: 'circle' - }); - - this.map.addLayer({ - id: this.idLayerSymbol, - source: this.idSourceSymbol, - type: 'symbol' - }); - - // We could merge the 'fill' source with the 'line' source and - // the 'circle' source with the 'symbol' source if ever having - // for up-to 4 sources per 'scattermapbox' traces becomes a problem. + this.mapbox = mapbox; + this.map = mapbox.map; + + this.uid = uid; + + this.idSourceFill = uid + "-source-fill"; + this.idSourceLine = uid + "-source-line"; + this.idSourceCircle = uid + "-source-circle"; + this.idSourceSymbol = uid + "-source-symbol"; + + this.idLayerFill = uid + "-layer-fill"; + this.idLayerLine = uid + "-layer-line"; + this.idLayerCircle = uid + "-layer-circle"; + this.idLayerSymbol = uid + "-layer-symbol"; + + this.mapbox.initSource(this.idSourceFill); + this.mapbox.initSource(this.idSourceLine); + this.mapbox.initSource(this.idSourceCircle); + this.mapbox.initSource(this.idSourceSymbol); + + this.map.addLayer({ + id: this.idLayerFill, + source: this.idSourceFill, + type: "fill" + }); + + this.map.addLayer({ + id: this.idLayerLine, + source: this.idSourceLine, + type: "line" + }); + + this.map.addLayer({ + id: this.idLayerCircle, + source: this.idSourceCircle, + type: "circle" + }); + + this.map.addLayer({ + id: this.idLayerSymbol, + source: this.idSourceSymbol, + type: "symbol" + }); + // We could merge the 'fill' source with the 'line' source and + // the 'circle' source with the 'symbol' source if ever having + // for up-to 4 sources per 'scattermapbox' traces becomes a problem. } var proto = ScatterMapbox.prototype; proto.update = function update(calcTrace) { - var mapbox = this.mapbox; - var opts = convert(calcTrace); - - mapbox.setOptions(this.idLayerFill, 'setLayoutProperty', opts.fill.layout); - mapbox.setOptions(this.idLayerLine, 'setLayoutProperty', opts.line.layout); - mapbox.setOptions(this.idLayerCircle, 'setLayoutProperty', opts.circle.layout); - mapbox.setOptions(this.idLayerSymbol, 'setLayoutProperty', opts.symbol.layout); - - if(isVisible(opts.fill)) { - mapbox.setSourceData(this.idSourceFill, opts.fill.geojson); - mapbox.setOptions(this.idLayerFill, 'setPaintProperty', opts.fill.paint); - } - - if(isVisible(opts.line)) { - mapbox.setSourceData(this.idSourceLine, opts.line.geojson); - mapbox.setOptions(this.idLayerLine, 'setPaintProperty', opts.line.paint); - } - - if(isVisible(opts.circle)) { - mapbox.setSourceData(this.idSourceCircle, opts.circle.geojson); - mapbox.setOptions(this.idLayerCircle, 'setPaintProperty', opts.circle.paint); - } - - if(isVisible(opts.symbol)) { - mapbox.setSourceData(this.idSourceSymbol, opts.symbol.geojson); - mapbox.setOptions(this.idLayerSymbol, 'setPaintProperty', opts.symbol.paint); - } + var mapbox = this.mapbox; + var opts = convert(calcTrace); + + mapbox.setOptions(this.idLayerFill, "setLayoutProperty", opts.fill.layout); + mapbox.setOptions(this.idLayerLine, "setLayoutProperty", opts.line.layout); + mapbox.setOptions( + this.idLayerCircle, + "setLayoutProperty", + opts.circle.layout + ); + mapbox.setOptions( + this.idLayerSymbol, + "setLayoutProperty", + opts.symbol.layout + ); + + if (isVisible(opts.fill)) { + mapbox.setSourceData(this.idSourceFill, opts.fill.geojson); + mapbox.setOptions(this.idLayerFill, "setPaintProperty", opts.fill.paint); + } + + if (isVisible(opts.line)) { + mapbox.setSourceData(this.idSourceLine, opts.line.geojson); + mapbox.setOptions(this.idLayerLine, "setPaintProperty", opts.line.paint); + } + + if (isVisible(opts.circle)) { + mapbox.setSourceData(this.idSourceCircle, opts.circle.geojson); + mapbox.setOptions( + this.idLayerCircle, + "setPaintProperty", + opts.circle.paint + ); + } + + if (isVisible(opts.symbol)) { + mapbox.setSourceData(this.idSourceSymbol, opts.symbol.geojson); + mapbox.setOptions( + this.idLayerSymbol, + "setPaintProperty", + opts.symbol.paint + ); + } }; proto.dispose = function dispose() { - var map = this.map; + var map = this.map; - map.removeLayer(this.idLayerFill); - map.removeLayer(this.idLayerLine); - map.removeLayer(this.idLayerCircle); - map.removeLayer(this.idLayerSymbol); + map.removeLayer(this.idLayerFill); + map.removeLayer(this.idLayerLine); + map.removeLayer(this.idLayerCircle); + map.removeLayer(this.idLayerSymbol); - map.removeSource(this.idSourceFill); - map.removeSource(this.idSourceLine); - map.removeSource(this.idSourceCircle); - map.removeSource(this.idSourceSymbol); + map.removeSource(this.idSourceFill); + map.removeSource(this.idSourceLine); + map.removeSource(this.idSourceCircle); + map.removeSource(this.idSourceSymbol); }; function isVisible(layerOpts) { - return layerOpts.layout.visibility === 'visible'; + return layerOpts.layout.visibility === "visible"; } module.exports = function createScatterMapbox(mapbox, calcTrace) { - var trace = calcTrace[0].trace; + var trace = calcTrace[0].trace; - var scatterMapbox = new ScatterMapbox(mapbox, trace.uid); - scatterMapbox.update(calcTrace); + var scatterMapbox = new ScatterMapbox(mapbox, trace.uid); + scatterMapbox.update(calcTrace); - return scatterMapbox; + return scatterMapbox; }; diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js index 847cc8436fe..28773aaf1f3 100644 --- a/src/traces/scatterternary/attributes.js +++ b/src/traces/scatterternary/attributes.js @@ -5,119 +5,120 @@ * 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 scatterAttrs = require("../scatter/attributes"); +var plotAttrs = require("../../plots/attributes"); +var colorAttributes = require("../../components/colorscale/color_attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); -'use strict'; - -var scatterAttrs = require('../scatter/attributes'); -var plotAttrs = require('../../plots/attributes'); -var colorAttributes = require('../../components/colorscale/color_attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; +var extendFlat = require("../../lib/extend").extendFlat; var scatterMarkerAttrs = scatterAttrs.marker, - scatterLineAttrs = scatterAttrs.line, - scatterMarkerLineAttrs = scatterMarkerAttrs.line; + scatterLineAttrs = scatterAttrs.line, + scatterMarkerLineAttrs = scatterMarkerAttrs.line; module.exports = { - a: { - valType: 'data_array', - description: [ - 'Sets the quantity of component `a` in each data point.', - 'If `a`, `b`, and `c` are all provided, they need not be', - 'normalized, only the relative values matter. If only two', - 'arrays are provided they must be normalized to match', - '`ternary.sum`.' - ].join(' ') - }, - b: { - valType: 'data_array', - description: [ - 'Sets the quantity of component `a` in each data point.', - 'If `a`, `b`, and `c` are all provided, they need not be', - 'normalized, only the relative values matter. If only two', - 'arrays are provided they must be normalized to match', - '`ternary.sum`.' - ].join(' ') - }, - c: { - valType: 'data_array', - description: [ - 'Sets the quantity of component `a` in each data point.', - 'If `a`, `b`, and `c` are all provided, they need not be', - 'normalized, only the relative values matter. If only two', - 'arrays are provided they must be normalized to match', - '`ternary.sum`.' - ].join(' ') - }, - sum: { - valType: 'number', - role: 'info', - dflt: 0, - min: 0, - description: [ - 'The number each triplet should sum to,', - 'if only two of `a`, `b`, and `c` are provided.', - 'This overrides `ternary.sum` to normalize this specific', - 'trace, but does not affect the values displayed on the axes.', - '0 (or missing) means to use ternary.sum' - ].join(' ') - }, - mode: extendFlat({}, scatterAttrs.mode, {dflt: 'markers'}), - text: extendFlat({}, scatterAttrs.text, { - description: [ - 'Sets text elements associated with each (a,b,c) point.', - 'If a single string, the same string appears over', - 'all the data points.', - 'If an array of strings, the items are mapped in order to the', - 'the data points in (a,b,c).' - ].join(' ') + a: { + valType: "data_array", + description: [ + "Sets the quantity of component `a` in each data point.", + "If `a`, `b`, and `c` are all provided, they need not be", + "normalized, only the relative values matter. If only two", + "arrays are provided they must be normalized to match", + "`ternary.sum`." + ].join(" ") + }, + b: { + valType: "data_array", + description: [ + "Sets the quantity of component `a` in each data point.", + "If `a`, `b`, and `c` are all provided, they need not be", + "normalized, only the relative values matter. If only two", + "arrays are provided they must be normalized to match", + "`ternary.sum`." + ].join(" ") + }, + c: { + valType: "data_array", + description: [ + "Sets the quantity of component `a` in each data point.", + "If `a`, `b`, and `c` are all provided, they need not be", + "normalized, only the relative values matter. If only two", + "arrays are provided they must be normalized to match", + "`ternary.sum`." + ].join(" ") + }, + sum: { + valType: "number", + role: "info", + dflt: 0, + min: 0, + description: [ + "The number each triplet should sum to,", + "if only two of `a`, `b`, and `c` are provided.", + "This overrides `ternary.sum` to normalize this specific", + "trace, but does not affect the values displayed on the axes.", + "0 (or missing) means to use ternary.sum" + ].join(" ") + }, + mode: extendFlat({}, scatterAttrs.mode, { dflt: "markers" }), + text: extendFlat({}, scatterAttrs.text, { + description: [ + "Sets text elements associated with each (a,b,c) point.", + "If a single string, the same string appears over", + "all the data points.", + "If an array of strings, the items are mapped in order to the", + "the data points in (a,b,c)." + ].join(" ") + }), + line: { + color: scatterLineAttrs.color, + width: scatterLineAttrs.width, + dash: scatterLineAttrs.dash, + shape: extendFlat({}, scatterLineAttrs.shape, { + values: ["linear", "spline"] }), - line: { - color: scatterLineAttrs.color, - width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash, - shape: extendFlat({}, scatterLineAttrs.shape, - {values: ['linear', 'spline']}), - smoothing: scatterLineAttrs.smoothing + smoothing: scatterLineAttrs.smoothing + }, + connectgaps: scatterAttrs.connectgaps, + fill: extendFlat({}, scatterAttrs.fill, { + values: ["none", "toself", "tonext"], + description: [ + "Sets the area to fill with a solid color.", + "Use with `fillcolor` if not *none*.", + "scatterternary has a subset of the options available to scatter.", + "*toself* connects the endpoints of the trace (or each segment", + "of the trace if it has gaps) into a closed shape.", + "*tonext* fills the space between two traces if one completely", + "encloses the other (eg consecutive contour lines), and behaves like", + "*toself* if there is no trace before it. *tonext* should not be", + "used if one trace does not enclose the other." + ].join(" ") + }), + fillcolor: scatterAttrs.fillcolor, + marker: extendFlat( + {}, + { + symbol: scatterMarkerAttrs.symbol, + opacity: scatterMarkerAttrs.opacity, + maxdisplayed: scatterMarkerAttrs.maxdisplayed, + size: scatterMarkerAttrs.size, + sizeref: scatterMarkerAttrs.sizeref, + sizemin: scatterMarkerAttrs.sizemin, + sizemode: scatterMarkerAttrs.sizemode, + line: extendFlat( + {}, + { width: scatterMarkerLineAttrs.width }, + colorAttributes("marker".line) + ) }, - connectgaps: scatterAttrs.connectgaps, - fill: extendFlat({}, scatterAttrs.fill, { - values: ['none', 'toself', 'tonext'], - description: [ - 'Sets the area to fill with a solid color.', - 'Use with `fillcolor` if not *none*.', - 'scatterternary has a subset of the options available to scatter.', - '*toself* connects the endpoints of the trace (or each segment', - 'of the trace if it has gaps) into a closed shape.', - '*tonext* fills the space between two traces if one completely', - 'encloses the other (eg consecutive contour lines), and behaves like', - '*toself* if there is no trace before it. *tonext* should not be', - 'used if one trace does not enclose the other.' - ].join(' ') - }), - fillcolor: scatterAttrs.fillcolor, - marker: extendFlat({}, { - symbol: scatterMarkerAttrs.symbol, - opacity: scatterMarkerAttrs.opacity, - maxdisplayed: scatterMarkerAttrs.maxdisplayed, - size: scatterMarkerAttrs.size, - sizeref: scatterMarkerAttrs.sizeref, - sizemin: scatterMarkerAttrs.sizemin, - sizemode: scatterMarkerAttrs.sizemode, - line: extendFlat({}, - {width: scatterMarkerLineAttrs.width}, - colorAttributes('marker'.line) - ) - }, colorAttributes('marker'), { - showscale: scatterMarkerAttrs.showscale, - colorbar: colorbarAttrs - }), - - textfont: scatterAttrs.textfont, - textposition: scatterAttrs.textposition, - hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { - flags: ['a', 'b', 'c', 'text', 'name'] - }), - hoveron: scatterAttrs.hoveron, + colorAttributes("marker"), + { showscale: scatterMarkerAttrs.showscale, colorbar: colorbarAttrs } + ), + textfont: scatterAttrs.textfont, + textposition: scatterAttrs.textposition, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { + flags: ["a", "b", "c", "text", "name"] + }), + hoveron: scatterAttrs.hoveron }; diff --git a/src/traces/scatterternary/calc.js b/src/traces/scatterternary/calc.js index c0a5d958212..49463a7456c 100644 --- a/src/traces/scatterternary/calc.js +++ b/src/traces/scatterternary/calc.js @@ -5,91 +5,88 @@ * 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 isNumeric = require("fast-isnumeric"); +var Axes = require("../../plots/cartesian/axes"); -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Axes = require('../../plots/cartesian/axes'); - -var subTypes = require('../scatter/subtypes'); -var calcColorscale = require('../scatter/colorscale_calc'); -var arraysToCalcdata = require('../scatter/arrays_to_calcdata'); - -var dataArrays = ['a', 'b', 'c']; -var arraysToFill = {a: ['b', 'c'], b: ['a', 'c'], c: ['a', 'b']}; +var subTypes = require("../scatter/subtypes"); +var calcColorscale = require("../scatter/colorscale_calc"); +var arraysToCalcdata = require("../scatter/arrays_to_calcdata"); +var dataArrays = ["a", "b", "c"]; +var arraysToFill = { a: ["b", "c"], b: ["a", "c"], c: ["a", "b"] }; module.exports = function calc(gd, trace) { - var ternary = gd._fullLayout[trace.subplot], - displaySum = ternary.sum, - normSum = trace.sum || displaySum; - - var i, j, dataArray, newArray, fillArray1, fillArray2; - - // fill in one missing component - for(i = 0; i < dataArrays.length; i++) { - dataArray = dataArrays[i]; - if(trace[dataArray]) continue; - - fillArray1 = trace[arraysToFill[dataArray][0]]; - fillArray2 = trace[arraysToFill[dataArray][1]]; - newArray = new Array(fillArray1.length); - for(j = 0; j < fillArray1.length; j++) { - newArray[j] = normSum - fillArray1[j] - fillArray2[j]; - } - trace[dataArray] = newArray; + var ternary = gd._fullLayout[trace.subplot], + displaySum = ternary.sum, + normSum = trace.sum || displaySum; + + var i, j, dataArray, newArray, fillArray1, fillArray2; + + // fill in one missing component + for (i = 0; i < dataArrays.length; i++) { + dataArray = dataArrays[i]; + if (trace[dataArray]) continue; + + fillArray1 = trace[arraysToFill[dataArray][0]]; + fillArray2 = trace[arraysToFill[dataArray][1]]; + newArray = new Array(fillArray1.length); + for (j = 0; j < fillArray1.length; j++) { + newArray[j] = normSum - fillArray1[j] - fillArray2[j]; } - - // make the calcdata array - var serieslen = trace.a.length; - var cd = new Array(serieslen); - var a, b, c, norm, x, y; - for(i = 0; i < serieslen; i++) { - a = trace.a[i]; - b = trace.b[i]; - c = trace.c[i]; - if(isNumeric(a) && isNumeric(b) && isNumeric(c)) { - a = +a; - b = +b; - c = +c; - norm = displaySum / (a + b + c); - if(norm !== 1) { - a *= norm; - b *= norm; - c *= norm; - } - // map a, b, c onto x and y where the full scale of y - // is [0, sum], and x is [-sum, sum] - // TODO: this makes `a` always the top, `b` the bottom left, - // and `c` the bottom right. Do we want options to rearrange - // these? - y = a; - x = c - b; - cd[i] = {x: x, y: y, a: a, b: b, c: c}; - } - else cd[i] = {x: false, y: false}; + trace[dataArray] = newArray; + } + + // make the calcdata array + var serieslen = trace.a.length; + var cd = new Array(serieslen); + var a, b, c, norm, x, y; + for (i = 0; i < serieslen; i++) { + a = trace.a[i]; + b = trace.b[i]; + c = trace.c[i]; + if (isNumeric(a) && isNumeric(b) && isNumeric(c)) { + a = +a; + b = +b; + c = +c; + norm = displaySum / (a + b + c); + if (norm !== 1) { + a *= norm; + b *= norm; + c *= norm; + } + // map a, b, c onto x and y where the full scale of y + // is [0, sum], and x is [-sum, sum] + // TODO: this makes `a` always the top, `b` the bottom left, + // and `c` the bottom right. Do we want options to rearrange + // these? + y = a; + x = c - b; + cd[i] = { x: x, y: y, a: a, b: b, c: c }; + } else { + cd[i] = { x: false, y: false }; } - - // fill in some extras - var marker, s; - if(subTypes.hasMarkers(trace)) { - // Treat size like x or y arrays --- Run d2c - // this needs to go before ppad computation - marker = trace.marker; - s = marker.size; - - if(Array.isArray(s)) { - var ax = {type: 'linear'}; - Axes.setConvert(ax); - s = ax.makeCalcdata(trace.marker, 'size'); - if(s.length > serieslen) s.splice(serieslen, s.length - serieslen); - } + } + + // fill in some extras + var marker, s; + if (subTypes.hasMarkers(trace)) { + // Treat size like x or y arrays --- Run d2c + // this needs to go before ppad computation + marker = trace.marker; + s = marker.size; + + if (Array.isArray(s)) { + var ax = { type: "linear" }; + Axes.setConvert(ax); + s = ax.makeCalcdata(trace.marker, "size"); + if (s.length > serieslen) s.splice(serieslen, s.length - serieslen); } + } - calcColorscale(trace); - arraysToCalcdata(cd, trace); + calcColorscale(trace); + arraysToCalcdata(cd, trace); - return cd; + return cd; }; diff --git a/src/traces/scatterternary/defaults.js b/src/traces/scatterternary/defaults.js index 5095345e929..c3818a8fc35 100644 --- a/src/traces/scatterternary/defaults.js +++ b/src/traces/scatterternary/defaults.js @@ -5,99 +5,97 @@ * 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 constants = require('../scatter/constants'); -var subTypes = require('../scatter/subtypes'); -var handleMarkerDefaults = require('../scatter/marker_defaults'); -var handleLineDefaults = require('../scatter/line_defaults'); -var handleLineShapeDefaults = require('../scatter/line_shape_defaults'); -var handleTextDefaults = require('../scatter/text_defaults'); -var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); - -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var a = coerce('a'), - b = coerce('b'), - c = coerce('c'), - len; - - // allow any one array to be missing, len is the minimum length of those - // present. Note that after coerce data_array's are either Arrays (which - // are truthy even if empty) or undefined. As in scatter, an empty array - // is different from undefined, because it can signify that this data is - // not known yet but expected in the future - if(a) { - len = a.length; - if(b) { - len = Math.min(len, b.length); - if(c) len = Math.min(len, c.length); - } - else if(c) len = Math.min(len, c.length); - else len = 0; - } - else if(b && c) { - len = Math.min(b.length, c.length); - } - - if(!len) { - traceOut.visible = false; - return; - } - - // cut all data arrays down to same length - if(a && len < a.length) traceOut.a = a.slice(0, len); - if(b && len < b.length) traceOut.b = b.slice(0, len); - if(c && len < c.length) traceOut.c = c.slice(0, len); - - coerce('sum'); - - coerce('text'); - - var defaultMode = len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines'; - coerce('mode', defaultMode); - - if(subTypes.hasLines(traceOut)) { - handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); - handleLineShapeDefaults(traceIn, traceOut, coerce); - coerce('connectgaps'); - } - - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); - } - - if(subTypes.hasText(traceOut)) { - handleTextDefaults(traceIn, traceOut, layout, coerce); +"use strict"; +var Lib = require("../../lib"); + +var constants = require("../scatter/constants"); +var subTypes = require("../scatter/subtypes"); +var handleMarkerDefaults = require("../scatter/marker_defaults"); +var handleLineDefaults = require("../scatter/line_defaults"); +var handleLineShapeDefaults = require("../scatter/line_shape_defaults"); +var handleTextDefaults = require("../scatter/text_defaults"); +var handleFillColorDefaults = require("../scatter/fillcolor_defaults"); + +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var a = coerce("a"), b = coerce("b"), c = coerce("c"), len; + + // allow any one array to be missing, len is the minimum length of those + // present. Note that after coerce data_array's are either Arrays (which + // are truthy even if empty) or undefined. As in scatter, an empty array + // is different from undefined, because it can signify that this data is + // not known yet but expected in the future + if (a) { + len = a.length; + if (b) { + len = Math.min(len, b.length); + if (c) len = Math.min(len, c.length); + } else if (c) len = Math.min(len, c.length); + else len = 0; + } else if (b && c) { + len = Math.min(b.length, c.length); + } + + if (!len) { + traceOut.visible = false; + return; + } + + // cut all data arrays down to same length + if (a && len < a.length) traceOut.a = a.slice(0, len); + if (b && len < b.length) traceOut.b = b.slice(0, len); + if (c && len < c.length) traceOut.c = c.slice(0, len); + + coerce("sum"); + + coerce("text"); + + var defaultMode = len < constants.PTS_LINESONLY ? "lines+markers" : "lines"; + coerce("mode", defaultMode); + + if (subTypes.hasLines(traceOut)) { + handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); + handleLineShapeDefaults(traceIn, traceOut, coerce); + coerce("connectgaps"); + } + + if (subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + if (subTypes.hasText(traceOut)) { + handleTextDefaults(traceIn, traceOut, layout, coerce); + } + + var dfltHoverOn = []; + + if (subTypes.hasMarkers(traceOut) || subTypes.hasText(traceOut)) { + coerce("marker.maxdisplayed"); + dfltHoverOn.push("points"); + } + + coerce("fill"); + if (traceOut.fill !== "none") { + handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); + if (!subTypes.hasLines(traceOut)) { + handleLineShapeDefaults(traceIn, traceOut, coerce); } + } - var dfltHoverOn = []; - - if(subTypes.hasMarkers(traceOut) || subTypes.hasText(traceOut)) { - coerce('marker.maxdisplayed'); - dfltHoverOn.push('points'); - } + coerce("hoverinfo", layout._dataLength === 1 ? "a+b+c+text" : undefined); - coerce('fill'); - if(traceOut.fill !== 'none') { - handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); - if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce); - } - - coerce('hoverinfo', (layout._dataLength === 1) ? 'a+b+c+text' : undefined); - - if(traceOut.fill === 'tonext' || traceOut.fill === 'toself') { - dfltHoverOn.push('fills'); - } - coerce('hoveron', dfltHoverOn.join('+') || 'points'); + if (traceOut.fill === "tonext" || traceOut.fill === "toself") { + dfltHoverOn.push("fills"); + } + coerce("hoveron", dfltHoverOn.join("+") || "points"); }; diff --git a/src/traces/scatterternary/hover.js b/src/traces/scatterternary/hover.js index cdf459d2609..36ee566b7a5 100644 --- a/src/traces/scatterternary/hover.js +++ b/src/traces/scatterternary/hover.js @@ -5,65 +5,60 @@ * 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 scatterHover = require('../scatter/hover'); -var Axes = require('../../plots/cartesian/axes'); - +"use strict"; +var scatterHover = require("../scatter/hover"); +var Axes = require("../../plots/cartesian/axes"); module.exports = function hoverPoints(pointData, xval, yval, hovermode) { - var scatterPointData = scatterHover(pointData, xval, yval, hovermode); - if(!scatterPointData || scatterPointData[0].index === false) return; + var scatterPointData = scatterHover(pointData, xval, yval, hovermode); + if (!scatterPointData || scatterPointData[0].index === false) return; - var newPointData = scatterPointData[0]; + var newPointData = scatterPointData[0]; - // if hovering on a fill, we don't show any point data so the label is - // unchanged from what scatter gives us - except that it needs to - // be constrained to the trianglular plot area, not just the rectangular - // area defined by the synthetic x and y axes - // TODO: in some cases the vertical middle of the shape is not within - // the triangular viewport at all, so the label can become disconnected - // from the shape entirely. But calculating what portion of the shape - // is actually visible, as constrained by the diagonal axis lines, is not - // so easy and anyway we lost the information we would have needed to do - // this inside scatterHover. - if(newPointData.index === undefined) { - var yFracUp = 1 - (newPointData.y0 / pointData.ya._length), - xLen = pointData.xa._length, - xMin = xLen * yFracUp / 2, - xMax = xLen - xMin; - newPointData.x0 = Math.max(Math.min(newPointData.x0, xMax), xMin); - newPointData.x1 = Math.max(Math.min(newPointData.x1, xMax), xMin); - return scatterPointData; - } - - var cdi = newPointData.cd[newPointData.index]; + // if hovering on a fill, we don't show any point data so the label is + // unchanged from what scatter gives us - except that it needs to + // be constrained to the trianglular plot area, not just the rectangular + // area defined by the synthetic x and y axes + // TODO: in some cases the vertical middle of the shape is not within + // the triangular viewport at all, so the label can become disconnected + // from the shape entirely. But calculating what portion of the shape + // is actually visible, as constrained by the diagonal axis lines, is not + // so easy and anyway we lost the information we would have needed to do + // this inside scatterHover. + if (newPointData.index === undefined) { + var yFracUp = 1 - newPointData.y0 / pointData.ya._length, + xLen = pointData.xa._length, + xMin = xLen * yFracUp / 2, + xMax = xLen - xMin; + newPointData.x0 = Math.max(Math.min(newPointData.x0, xMax), xMin); + newPointData.x1 = Math.max(Math.min(newPointData.x1, xMax), xMin); + return scatterPointData; + } - newPointData.a = cdi.a; - newPointData.b = cdi.b; - newPointData.c = cdi.c; + var cdi = newPointData.cd[newPointData.index]; - newPointData.xLabelVal = undefined; - newPointData.yLabelVal = undefined; - // TODO: nice formatting, and label by axis title, for a, b, and c? + newPointData.a = cdi.a; + newPointData.b = cdi.b; + newPointData.c = cdi.c; - var trace = newPointData.trace, - ternary = trace._ternary, - hoverinfo = trace.hoverinfo.split('+'), - text = []; + newPointData.xLabelVal = undefined; + newPointData.yLabelVal = undefined; + // TODO: nice formatting, and label by axis title, for a, b, and c? + var trace = newPointData.trace, + ternary = trace._ternary, + hoverinfo = trace.hoverinfo.split("+"), + text = []; - function textPart(ax, val) { - text.push(ax._hovertitle + ': ' + Axes.tickText(ax, val, 'hover').text); - } + function textPart(ax, val) { + text.push(ax._hovertitle + ": " + Axes.tickText(ax, val, "hover").text); + } - if(hoverinfo.indexOf('all') !== -1) hoverinfo = ['a', 'b', 'c']; - if(hoverinfo.indexOf('a') !== -1) textPart(ternary.aaxis, cdi.a); - if(hoverinfo.indexOf('b') !== -1) textPart(ternary.baxis, cdi.b); - if(hoverinfo.indexOf('c') !== -1) textPart(ternary.caxis, cdi.c); + if (hoverinfo.indexOf("all") !== -1) hoverinfo = ["a", "b", "c"]; + if (hoverinfo.indexOf("a") !== -1) textPart(ternary.aaxis, cdi.a); + if (hoverinfo.indexOf("b") !== -1) textPart(ternary.baxis, cdi.b); + if (hoverinfo.indexOf("c") !== -1) textPart(ternary.caxis, cdi.c); - newPointData.extraText = text.join('
'); + newPointData.extraText = text.join("
"); - return scatterPointData; + return scatterPointData; }; diff --git a/src/traces/scatterternary/index.js b/src/traces/scatterternary/index.js index e7e75c4ddfc..672f75c77cf 100644 --- a/src/traces/scatterternary/index.js +++ b/src/traces/scatterternary/index.js @@ -5,30 +5,33 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - -'use strict'; - +"use strict"; var ScatterTernary = {}; -ScatterTernary.attributes = require('./attributes'); -ScatterTernary.supplyDefaults = require('./defaults'); -ScatterTernary.colorbar = require('../scatter/colorbar'); -ScatterTernary.calc = require('./calc'); -ScatterTernary.plot = require('./plot'); -ScatterTernary.style = require('./style'); -ScatterTernary.hoverPoints = require('./hover'); -ScatterTernary.selectPoints = require('./select'); +ScatterTernary.attributes = require("./attributes"); +ScatterTernary.supplyDefaults = require("./defaults"); +ScatterTernary.colorbar = require("../scatter/colorbar"); +ScatterTernary.calc = require("./calc"); +ScatterTernary.plot = require("./plot"); +ScatterTernary.style = require("./style"); +ScatterTernary.hoverPoints = require("./hover"); +ScatterTernary.selectPoints = require("./select"); -ScatterTernary.moduleType = 'trace'; -ScatterTernary.name = 'scatterternary'; -ScatterTernary.basePlotModule = require('../../plots/ternary'); -ScatterTernary.categories = ['ternary', 'symbols', 'markerColorscale', 'showLegend']; +ScatterTernary.moduleType = "trace"; +ScatterTernary.name = "scatterternary"; +ScatterTernary.basePlotModule = require("../../plots/ternary"); +ScatterTernary.categories = [ + "ternary", + "symbols", + "markerColorscale", + "showLegend" +]; ScatterTernary.meta = { - hrName: 'scatter_ternary', - description: [ - 'Provides similar functionality to the *scatter* type but on a ternary phase diagram.', - 'The data is provided by at least two arrays out of `a`, `b`, `c` triplets.' - ].join(' ') + hrName: "scatter_ternary", + description: [ + "Provides similar functionality to the *scatter* type but on a ternary phase diagram.", + "The data is provided by at least two arrays out of `a`, `b`, `c` triplets." + ].join(" ") }; module.exports = ScatterTernary; diff --git a/src/traces/scatterternary/plot.js b/src/traces/scatterternary/plot.js index 0ecfaa25601..ddf78fb6a9c 100644 --- a/src/traces/scatterternary/plot.js +++ b/src/traces/scatterternary/plot.js @@ -5,30 +5,26 @@ * 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 scatterPlot = require('../scatter/plot'); - +"use strict"; +var scatterPlot = require("../scatter/plot"); module.exports = function plot(ternary, moduleCalcData) { - var plotContainer = ternary.plotContainer; + var plotContainer = ternary.plotContainer; - // remove all nodes inside the scatter layer - plotContainer.select('.scatterlayer').selectAll('*').remove(); + // remove all nodes inside the scatter layer + plotContainer.select(".scatterlayer").selectAll("*").remove(); - // mimic cartesian plotinfo - var plotinfo = { - xaxis: ternary.xaxis, - yaxis: ternary.yaxis, - plot: plotContainer - }; + // mimic cartesian plotinfo + var plotinfo = { + xaxis: ternary.xaxis, + yaxis: ternary.yaxis, + plot: plotContainer + }; - // add ref to ternary subplot object in fullData traces - for(var i = 0; i < moduleCalcData.length; i++) { - moduleCalcData[i][0].trace._ternary = ternary; - } + // add ref to ternary subplot object in fullData traces + for (var i = 0; i < moduleCalcData.length; i++) { + moduleCalcData[i][0].trace._ternary = ternary; + } - scatterPlot(ternary.graphDiv, plotinfo, moduleCalcData); + scatterPlot(ternary.graphDiv, plotinfo, moduleCalcData); }; diff --git a/src/traces/scatterternary/select.js b/src/traces/scatterternary/select.js index 5682b0e1669..5138da415f5 100644 --- a/src/traces/scatterternary/select.js +++ b/src/traces/scatterternary/select.js @@ -5,29 +5,24 @@ * 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 scatterSelect = require('../scatter/select'); - +"use strict"; +var scatterSelect = require("../scatter/select"); module.exports = function selectPoints(searchInfo, polygon) { - var selection = scatterSelect(searchInfo, polygon); - if(!selection) return; - - var cd = searchInfo.cd, - pt, cdi, i; - - for(i = 0; i < selection.length; i++) { - pt = selection[i]; - cdi = cd[pt.pointNumber]; - pt.a = cdi.a; - pt.b = cdi.b; - pt.c = cdi.c; - delete pt.x; - delete pt.y; - } - - return selection; + var selection = scatterSelect(searchInfo, polygon); + if (!selection) return; + + var cd = searchInfo.cd, pt, cdi, i; + + for (i = 0; i < selection.length; i++) { + pt = selection[i]; + cdi = cd[pt.pointNumber]; + pt.a = cdi.a; + pt.b = cdi.b; + pt.c = cdi.c; + delete pt.x; + delete pt.y; + } + + return selection; }; diff --git a/src/traces/scatterternary/style.js b/src/traces/scatterternary/style.js index 8ead87cc97e..a060b4d7465 100644 --- a/src/traces/scatterternary/style.js +++ b/src/traces/scatterternary/style.js @@ -5,23 +5,19 @@ * 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 scatterStyle = require('../scatter/style'); - +"use strict"; +var scatterStyle = require("../scatter/style"); module.exports = function style(gd) { - var modules = gd._fullLayout._modules; + var modules = gd._fullLayout._modules; - // we're just going to call scatter style... if we already - // called it, don't need to redo. - // Later though we may want differences, or we may make style - // more specific in its scope, then we can remove this. - for(var i = 0; i < modules.length; i++) { - if(modules[i].name === 'scatter') return; - } + // we're just going to call scatter style... if we already + // called it, don't need to redo. + // Later though we may want differences, or we may make style + // more specific in its scope, then we can remove this. + for (var i = 0; i < modules.length; i++) { + if (modules[i].name === "scatter") return; + } - scatterStyle(gd); + scatterStyle(gd); }; diff --git a/src/traces/surface/attributes.js b/src/traces/surface/attributes.js index 6e3930a0479..cae06129773 100644 --- a/src/traces/surface/attributes.js +++ b/src/traces/surface/attributes.js @@ -5,242 +5,229 @@ * 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 Color = require("../../components/color"); +var colorscaleAttrs = require("../../components/colorscale/attributes"); +var colorbarAttrs = require("../../components/colorbar/attributes"); -'use strict'; - -var Color = require('../../components/color'); -var colorscaleAttrs = require('../../components/colorscale/attributes'); -var colorbarAttrs = require('../../components/colorbar/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; +var extendFlat = require("../../lib/extend").extendFlat; function makeContourProjAttr(axLetter) { - return { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Determines whether or not these contour lines are projected', - 'on the', axLetter, 'plane.', - 'If `highlight` is set to *true* (the default), the projected', - 'lines are shown on hover.', - 'If `show` is set to *true*, the projected lines are shown', - 'in permanence.' - ].join(' ') - }; + return { + valType: "boolean", + role: "info", + dflt: false, + description: [ + "Determines whether or not these contour lines are projected", + "on the", + axLetter, + "plane.", + "If `highlight` is set to *true* (the default), the projected", + "lines are shown on hover.", + "If `show` is set to *true*, the projected lines are shown", + "in permanence." + ].join(" ") + }; } function makeContourAttr(axLetter) { - return { - show: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Determines whether or not contour lines about the', axLetter, - 'dimension are drawn.' - ].join(' ') - }, - project: { - x: makeContourProjAttr('x'), - y: makeContourProjAttr('y'), - z: makeContourProjAttr('z') - }, - color: { - valType: 'color', - role: 'style', - dflt: Color.defaultLine, - description: 'Sets the color of the contour lines.' - }, - usecolormap: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'An alternate to *color*.', - 'Determines whether or not the contour lines are colored using', - 'the trace *colorscale*.' - ].join(' ') - }, - width: { - valType: 'number', - role: 'style', - min: 1, - max: 16, - dflt: 2, - description: 'Sets the width of the contour lines.' - }, - highlight: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines whether or not contour lines about the', axLetter, - 'dimension are highlighted on hover.' - ].join(' ') - }, - highlightcolor: { - valType: 'color', - role: 'style', - dflt: Color.defaultLine, - description: 'Sets the color of the highlighted contour lines.' - }, - highlightwidth: { - valType: 'number', - role: 'style', - min: 1, - max: 16, - dflt: 2, - description: 'Sets the width of the highlighted contour lines.' - } - }; -} - -module.exports = { - z: { - valType: 'data_array', - description: 'Sets the z coordinates.' + return { + show: { + valType: "boolean", + role: "info", + dflt: false, + description: [ + "Determines whether or not contour lines about the", + axLetter, + "dimension are drawn." + ].join(" ") }, - x: { - valType: 'data_array', - description: 'Sets the x coordinates.' + project: { + x: makeContourProjAttr("x"), + y: makeContourProjAttr("y"), + z: makeContourProjAttr("z") }, - y: { - valType: 'data_array', - description: 'Sets the y coordinates.' + color: { + valType: "color", + role: "style", + dflt: Color.defaultLine, + description: "Sets the color of the contour lines." }, - - text: { - valType: 'data_array', - description: 'Sets the text elements associated with each z value.' + usecolormap: { + valType: "boolean", + role: "info", + dflt: false, + description: [ + "An alternate to *color*.", + "Determines whether or not the contour lines are colored using", + "the trace *colorscale*." + ].join(" ") }, - surfacecolor: { - valType: 'data_array', - description: [ - 'Sets the surface color values,', - 'used for setting a color scale independent of `z`.' - ].join(' ') + width: { + valType: "number", + role: "style", + min: 1, + max: 16, + dflt: 2, + description: "Sets the width of the contour lines." }, - - // Todo this block has a structure of colorscale/attributes.js but with colorscale/color_attributes.js names - cauto: colorscaleAttrs.zauto, - cmin: colorscaleAttrs.zmin, - cmax: colorscaleAttrs.zmax, - colorscale: colorscaleAttrs.colorscale, - autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, - {dflt: false}), - reversescale: colorscaleAttrs.reversescale, - showscale: colorscaleAttrs.showscale, - colorbar: colorbarAttrs, - - contours: { - x: makeContourAttr('x'), - y: makeContourAttr('y'), - z: makeContourAttr('z') + highlight: { + valType: "boolean", + role: "info", + dflt: true, + description: [ + "Determines whether or not contour lines about the", + axLetter, + "dimension are highlighted on hover." + ].join(" ") }, - hidesurface: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Determines whether or not a surface is drawn.', - 'For example, set `hidesurface` to *false*', - '`contours.x.show` to *true* and', - '`contours.y.show` to *true* to draw a wire frame plot.' - ].join(' ') + highlightcolor: { + valType: "color", + role: "style", + dflt: Color.defaultLine, + description: "Sets the color of the highlighted contour lines." }, + highlightwidth: { + valType: "number", + role: "style", + min: 1, + max: 16, + dflt: 2, + description: "Sets the width of the highlighted contour lines." + } + }; +} - lightposition: { - x: { - valType: 'number', - role: 'style', - min: -1e5, - max: 1e5, - dflt: 10, - description: 'Numeric vector, representing the X coordinate for each vertex.' - }, - y: { - valType: 'number', - role: 'style', - min: -1e5, - max: 1e5, - dflt: 1e4, - description: 'Numeric vector, representing the Y coordinate for each vertex.' - }, - z: { - valType: 'number', - role: 'style', - min: -1e5, - max: 1e5, - dflt: 0, - description: 'Numeric vector, representing the Z coordinate for each vertex.' - } +module.exports = { + z: { valType: "data_array", description: "Sets the z coordinates." }, + x: { valType: "data_array", description: "Sets the x coordinates." }, + y: { valType: "data_array", description: "Sets the y coordinates." }, + text: { + valType: "data_array", + description: "Sets the text elements associated with each z value." + }, + surfacecolor: { + valType: "data_array", + description: [ + "Sets the surface color values,", + "used for setting a color scale independent of `z`." + ].join(" ") + }, + // Todo this block has a structure of colorscale/attributes.js but with colorscale/color_attributes.js names + cauto: colorscaleAttrs.zauto, + cmin: colorscaleAttrs.zmin, + cmax: colorscaleAttrs.zmax, + colorscale: colorscaleAttrs.colorscale, + autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, { + dflt: false + }), + reversescale: colorscaleAttrs.reversescale, + showscale: colorscaleAttrs.showscale, + colorbar: colorbarAttrs, + contours: { + x: makeContourAttr("x"), + y: makeContourAttr("y"), + z: makeContourAttr("z") + }, + hidesurface: { + valType: "boolean", + role: "info", + dflt: false, + description: [ + "Determines whether or not a surface is drawn.", + "For example, set `hidesurface` to *false*", + "`contours.x.show` to *true* and", + "`contours.y.show` to *true* to draw a wire frame plot." + ].join(" ") + }, + lightposition: { + x: { + valType: "number", + role: "style", + min: -1e5, + max: 1e5, + dflt: 10, + description: "Numeric vector, representing the X coordinate for each vertex." }, - - lighting: { - ambient: { - valType: 'number', - role: 'style', - min: 0.00, - max: 1.0, - dflt: 0.8, - description: 'Ambient light increases overall color visibility but can wash out the image.' - }, - diffuse: { - valType: 'number', - role: 'style', - min: 0.00, - max: 1.00, - dflt: 0.8, - description: 'Represents the extent that incident rays are reflected in a range of angles.' - }, - specular: { - valType: 'number', - role: 'style', - min: 0.00, - max: 2.00, - dflt: 0.05, - description: 'Represents the level that incident rays are reflected in a single direction, causing shine.' - }, - roughness: { - valType: 'number', - role: 'style', - min: 0.00, - max: 1.00, - dflt: 0.5, - description: 'Alters specular reflection; the rougher the surface, the wider and less contrasty the shine.' - }, - fresnel: { - valType: 'number', - role: 'style', - min: 0.00, - max: 5.00, - dflt: 0.2, - description: [ - 'Represents the reflectance as a dependency of the viewing angle; e.g. paper is reflective', - 'when viewing it from the edge of the paper (almost 90 degrees), causing shine.' - ].join(' ') - } + y: { + valType: "number", + role: "style", + min: -1e5, + max: 1e5, + dflt: 1e4, + description: "Numeric vector, representing the Y coordinate for each vertex." }, - - opacity: { - valType: 'number', - role: 'style', - min: 0, - max: 1, - dflt: 1, - description: 'Sets the opacity of the surface.' + z: { + valType: "number", + role: "style", + min: -1e5, + max: 1e5, + dflt: 0, + description: "Numeric vector, representing the Z coordinate for each vertex." + } + }, + lighting: { + ambient: { + valType: "number", + role: "style", + min: 0.00, + max: 1.0, + dflt: 0.8, + description: "Ambient light increases overall color visibility but can wash out the image." }, - - _deprecated: { - zauto: extendFlat({}, colorscaleAttrs.zauto, { - description: 'Obsolete. Use `cauto` instead.' - }), - zmin: extendFlat({}, colorscaleAttrs.zmin, { - description: 'Obsolete. Use `cmin` instead.' - }), - zmax: extendFlat({}, colorscaleAttrs.zmax, { - description: 'Obsolete. Use `cmax` instead.' - }) + diffuse: { + valType: "number", + role: "style", + min: 0.00, + max: 1.00, + dflt: 0.8, + description: "Represents the extent that incident rays are reflected in a range of angles." + }, + specular: { + valType: "number", + role: "style", + min: 0.00, + max: 2.00, + dflt: 0.05, + description: "Represents the level that incident rays are reflected in a single direction, causing shine." + }, + roughness: { + valType: "number", + role: "style", + min: 0.00, + max: 1.00, + dflt: 0.5, + description: "Alters specular reflection; the rougher the surface, the wider and less contrasty the shine." + }, + fresnel: { + valType: "number", + role: "style", + min: 0.00, + max: 5.00, + dflt: 0.2, + description: [ + "Represents the reflectance as a dependency of the viewing angle; e.g. paper is reflective", + "when viewing it from the edge of the paper (almost 90 degrees), causing shine." + ].join(" ") } + }, + opacity: { + valType: "number", + role: "style", + min: 0, + max: 1, + dflt: 1, + description: "Sets the opacity of the surface." + }, + _deprecated: { + zauto: extendFlat({}, colorscaleAttrs.zauto, { + description: "Obsolete. Use `cauto` instead." + }), + zmin: extendFlat({}, colorscaleAttrs.zmin, { + description: "Obsolete. Use `cmin` instead." + }), + zmax: extendFlat({}, colorscaleAttrs.zmax, { + description: "Obsolete. Use `cmax` instead." + }) + } }; diff --git a/src/traces/surface/calc.js b/src/traces/surface/calc.js index 5d52b2da316..7e8bef2b96b 100644 --- a/src/traces/surface/calc.js +++ b/src/traces/surface/calc.js @@ -5,18 +5,14 @@ * 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 colorscaleCalc = require('../../components/colorscale/calc'); - +"use strict"; +var colorscaleCalc = require("../../components/colorscale/calc"); // Compute auto-z and autocolorscale if applicable module.exports = function calc(gd, trace) { - if(trace.surfacecolor) { - colorscaleCalc(trace, trace.surfacecolor, '', 'c'); - } else { - colorscaleCalc(trace, trace.z, '', 'c'); - } + if (trace.surfacecolor) { + colorscaleCalc(trace, trace.surfacecolor, "", "c"); + } else { + colorscaleCalc(trace, trace.z, "", "c"); + } }; diff --git a/src/traces/surface/colorbar.js b/src/traces/surface/colorbar.js index e483f87df3d..855c5da2b24 100644 --- a/src/traces/surface/colorbar.js +++ b/src/traces/surface/colorbar.js @@ -5,46 +5,39 @@ * 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 isNumeric = require("fast-isnumeric"); - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Plots = require('../../plots/plots'); -var Colorscale = require('../../components/colorscale'); -var drawColorbar = require('../../components/colorbar/draw'); - +var Lib = require("../../lib"); +var Plots = require("../../plots/plots"); +var Colorscale = require("../../components/colorscale"); +var drawColorbar = require("../../components/colorbar/draw"); module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - cbId = 'cb' + trace.uid, - cmin = trace.cmin, - cmax = trace.cmax, - vals = trace.surfacecolor || trace.z; - - if(!isNumeric(cmin)) cmin = Lib.aggNums(Math.min, null, vals); - if(!isNumeric(cmax)) cmax = Lib.aggNums(Math.max, null, vals); - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - if(!trace.showscale) { - Plots.autoMargin(gd, cbId); - return; - } - - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - trace.colorscale, - cmin, - cmax - ), - { noNumericCheck: true } - ); - - cb.fillcolor(sclFunc) - .filllevels({start: cmin, end: cmax, size: (cmax - cmin) / 254}) - .options(trace.colorbar)(); + var trace = cd[0].trace, + cbId = "cb" + trace.uid, + cmin = trace.cmin, + cmax = trace.cmax, + vals = trace.surfacecolor || trace.z; + + if (!isNumeric(cmin)) cmin = Lib.aggNums(Math.min, null, vals); + if (!isNumeric(cmax)) cmax = Lib.aggNums(Math.max, null, vals); + + gd._fullLayout._infolayer.selectAll("." + cbId).remove(); + + if (!trace.showscale) { + Plots.autoMargin(gd, cbId); + return; + } + + var cb = cd[0].t.cb = drawColorbar(gd, cbId); + var sclFunc = Colorscale.makeColorScaleFunc( + Colorscale.extractScale(trace.colorscale, cmin, cmax), + { noNumericCheck: true } + ); + + cb + .fillcolor(sclFunc) + .filllevels({ start: cmin, end: cmax, size: (cmax - cmin) / 254 }) + .options(trace.colorbar)(); }; diff --git a/src/traces/surface/convert.js b/src/traces/surface/convert.js index 31fd3685e9e..cfff40a9f11 100644 --- a/src/traces/surface/convert.js +++ b/src/traces/surface/convert.js @@ -5,378 +5,385 @@ * 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 createSurface = require("gl-surface3d"); +var ndarray = require("ndarray"); +var homography = require("ndarray-homography"); +var fill = require("ndarray-fill"); +var ops = require("ndarray-ops"); +var tinycolor = require("tinycolor2"); - -'use strict'; - -var createSurface = require('gl-surface3d'); -var ndarray = require('ndarray'); -var homography = require('ndarray-homography'); -var fill = require('ndarray-fill'); -var ops = require('ndarray-ops'); -var tinycolor = require('tinycolor2'); - -var str2RgbaArray = require('../../lib/str2rgbarray'); +var str2RgbaArray = require("../../lib/str2rgbarray"); var MIN_RESOLUTION = 128; function SurfaceTrace(scene, surface, uid) { - this.scene = scene; - this.uid = uid; - this.surface = surface; - this.data = null; - this.showContour = [false, false, false]; - this.dataScale = 1.0; + this.scene = scene; + this.uid = uid; + this.surface = surface; + this.data = null; + this.showContour = [false, false, false]; + this.dataScale = 1.0; } var proto = SurfaceTrace.prototype; proto.handlePick = function(selection) { - if(selection.object === this.surface) { - var selectIndex = [ - Math.min( - Math.round(selection.data.index[0] / this.dataScale - 1)|0, - this.data.z[0].length - 1 - ), - Math.min( - Math.round(selection.data.index[1] / this.dataScale - 1)|0, - this.data.z.length - 1 - ) - ]; - var traceCoordinate = [0, 0, 0]; - - if(Array.isArray(this.data.x[0])) { - traceCoordinate[0] = this.data.x[selectIndex[1]][selectIndex[0]]; - } else { - traceCoordinate[0] = this.data.x[selectIndex[0]]; - } - if(Array.isArray(this.data.y[0])) { - traceCoordinate[1] = this.data.y[selectIndex[1]][selectIndex[0]]; - } else { - traceCoordinate[1] = this.data.y[selectIndex[1]]; - } - - traceCoordinate[2] = this.data.z[selectIndex[1]][selectIndex[0]]; - selection.traceCoordinate = traceCoordinate; - - var sceneLayout = this.scene.fullSceneLayout; - selection.dataCoordinate = [ - sceneLayout.xaxis.d2l(traceCoordinate[0], 0, this.data.xcalendar) * this.scene.dataScale[0], - sceneLayout.yaxis.d2l(traceCoordinate[1], 0, this.data.ycalendar) * this.scene.dataScale[1], - sceneLayout.zaxis.d2l(traceCoordinate[2], 0, this.data.zcalendar) * this.scene.dataScale[2] - ]; - - var text = this.data.text; - if(text && text[selectIndex[1]] && text[selectIndex[1]][selectIndex[0]] !== undefined) { - selection.textLabel = text[selectIndex[1]][selectIndex[0]]; - } - else selection.textLabel = ''; - - selection.data.dataCoordinate = selection.dataCoordinate.slice(); - - this.surface.highlight(selection.data); - - // Snap spikes to data coordinate - this.scene.glplot.spikes.position = selection.dataCoordinate; - - return true; + if (selection.object === this.surface) { + var selectIndex = [ + Math.min( + Math.round(selection.data.index[0] / this.dataScale - 1) | 0, + this.data.z[0].length - 1 + ), + Math.min( + Math.round(selection.data.index[1] / this.dataScale - 1) | 0, + this.data.z.length - 1 + ) + ]; + var traceCoordinate = [0, 0, 0]; + + if (Array.isArray(this.data.x[0])) { + traceCoordinate[0] = this.data.x[selectIndex[1]][selectIndex[0]]; + } else { + traceCoordinate[0] = this.data.x[selectIndex[0]]; + } + if (Array.isArray(this.data.y[0])) { + traceCoordinate[1] = this.data.y[selectIndex[1]][selectIndex[0]]; + } else { + traceCoordinate[1] = this.data.y[selectIndex[1]]; + } + + traceCoordinate[2] = this.data.z[selectIndex[1]][selectIndex[0]]; + selection.traceCoordinate = traceCoordinate; + + var sceneLayout = this.scene.fullSceneLayout; + selection.dataCoordinate = [ + sceneLayout.xaxis.d2l(traceCoordinate[0], 0, this.data.xcalendar) * + this.scene.dataScale[0], + sceneLayout.yaxis.d2l(traceCoordinate[1], 0, this.data.ycalendar) * + this.scene.dataScale[1], + sceneLayout.zaxis.d2l(traceCoordinate[2], 0, this.data.zcalendar) * + this.scene.dataScale[2] + ]; + + var text = this.data.text; + if ( + text && + text[selectIndex[1]] && + text[selectIndex[1]][selectIndex[0]] !== undefined + ) { + selection.textLabel = text[selectIndex[1]][selectIndex[0]]; + } else { + selection.textLabel = ""; } + + selection.data.dataCoordinate = selection.dataCoordinate.slice(); + + this.surface.highlight(selection.data); + + // Snap spikes to data coordinate + this.scene.glplot.spikes.position = selection.dataCoordinate; + + return true; + } }; function parseColorScale(colorscale, alpha) { - if(alpha === undefined) alpha = 1; - - return colorscale.map(function(elem) { - var index = elem[0]; - var color = tinycolor(elem[1]); - var rgb = color.toRgb(); - return { - index: index, - rgb: [rgb.r, rgb.g, rgb.b, alpha] - }; - }); + if (alpha === undefined) alpha = 1; + + return colorscale.map(function(elem) { + var index = elem[0]; + var color = tinycolor(elem[1]); + var rgb = color.toRgb(); + return { index: index, rgb: [rgb.r, rgb.g, rgb.b, alpha] }; + }); } function isColormapCircular(colormap) { - var first = colormap[0].rgb, - last = colormap[colormap.length - 1].rgb; - - return ( - first[0] === last[0] && - first[1] === last[1] && - first[2] === last[2] && - first[3] === last[3] - ); + var first = colormap[0].rgb, last = colormap[colormap.length - 1].rgb; + + return first[0] === last[0] && + first[1] === last[1] && + first[2] === last[2] && + first[3] === last[3]; } // Pad coords by +1 function padField(field) { - var shape = field.shape; - var nshape = [shape[0] + 2, shape[1] + 2]; - var nfield = ndarray(new Float32Array(nshape[0] * nshape[1]), nshape); - - // Center - ops.assign(nfield.lo(1, 1).hi(shape[0], shape[1]), field); - - // Edges - ops.assign(nfield.lo(1).hi(shape[0], 1), - field.hi(shape[0], 1)); - ops.assign(nfield.lo(1, nshape[1] - 1).hi(shape[0], 1), - field.lo(0, shape[1] - 1).hi(shape[0], 1)); - ops.assign(nfield.lo(0, 1).hi(1, shape[1]), - field.hi(1)); - ops.assign(nfield.lo(nshape[0] - 1, 1).hi(1, shape[1]), - field.lo(shape[0] - 1)); - - // Corners - nfield.set(0, 0, field.get(0, 0)); - nfield.set(0, nshape[1] - 1, field.get(0, shape[1] - 1)); - nfield.set(nshape[0] - 1, 0, field.get(shape[0] - 1, 0)); - nfield.set(nshape[0] - 1, nshape[1] - 1, field.get(shape[0] - 1, shape[1] - 1)); - - return nfield; + var shape = field.shape; + var nshape = [shape[0] + 2, shape[1] + 2]; + var nfield = ndarray(new Float32Array(nshape[0] * nshape[1]), nshape); + + // Center + ops.assign(nfield.lo(1, 1).hi(shape[0], shape[1]), field); + + // Edges + ops.assign(nfield.lo(1).hi(shape[0], 1), field.hi(shape[0], 1)); + ops.assign( + nfield.lo(1, nshape[1] - 1).hi(shape[0], 1), + field.lo(0, shape[1] - 1).hi(shape[0], 1) + ); + ops.assign(nfield.lo(0, 1).hi(1, shape[1]), field.hi(1)); + ops.assign( + nfield.lo(nshape[0] - 1, 1).hi(1, shape[1]), + field.lo(shape[0] - 1) + ); + + // Corners + nfield.set(0, 0, field.get(0, 0)); + nfield.set(0, nshape[1] - 1, field.get(0, shape[1] - 1)); + nfield.set(nshape[0] - 1, 0, field.get(shape[0] - 1, 0)); + nfield.set( + nshape[0] - 1, + nshape[1] - 1, + field.get(shape[0] - 1, shape[1] - 1) + ); + + return nfield; } function refine(coords) { - var minScale = Math.max(coords[0].shape[0], coords[0].shape[1]); - - if(minScale < MIN_RESOLUTION) { - var scaleF = MIN_RESOLUTION / minScale; - var nshape = [ - Math.floor((coords[0].shape[0]) * scaleF + 1)|0, - Math.floor((coords[0].shape[1]) * scaleF + 1)|0 ]; - var nsize = nshape[0] * nshape[1]; - - for(var i = 0; i < coords.length; ++i) { - var padImg = padField(coords[i]); - var scaledImg = ndarray(new Float32Array(nsize), nshape); - homography(scaledImg, padImg, [scaleF, 0, 0, - 0, scaleF, 0, - 0, 0, 1]); - coords[i] = scaledImg; - } - - return scaleF; + var minScale = Math.max(coords[0].shape[0], coords[0].shape[1]); + + if (minScale < MIN_RESOLUTION) { + var scaleF = MIN_RESOLUTION / minScale; + var nshape = [ + Math.floor(coords[0].shape[0] * scaleF + 1) | 0, + Math.floor(coords[0].shape[1] * scaleF + 1) | 0 + ]; + var nsize = nshape[0] * nshape[1]; + + for (var i = 0; i < coords.length; ++i) { + var padImg = padField(coords[i]); + var scaledImg = ndarray(new Float32Array(nsize), nshape); + homography(scaledImg, padImg, [scaleF, 0, 0, 0, scaleF, 0, 0, 0, 1]); + coords[i] = scaledImg; } - return 1.0; + return scaleF; + } + + return 1.0; } proto.setContourLevels = function() { - var nlevels = [[], [], []]; - var needsUpdate = false; - - for(var i = 0; i < 3; ++i) { - if(this.showContour[i]) { - needsUpdate = true; - nlevels[i] = this.scene.contourLevels[i]; - } - } + var nlevels = [[], [], []]; + var needsUpdate = false; - if(needsUpdate) { - this.surface.update({ levels: nlevels }); + for (var i = 0; i < 3; ++i) { + if (this.showContour[i]) { + needsUpdate = true; + nlevels[i] = this.scene.contourLevels[i]; } + } + + if (needsUpdate) { + this.surface.update({ levels: nlevels }); + } }; proto.update = function(data) { - var i, - scene = this.scene, - sceneLayout = scene.fullSceneLayout, - surface = this.surface, - alpha = data.opacity, - colormap = parseColorScale(data.colorscale, alpha), - z = data.z, - x = data.x, - y = data.y, - xaxis = sceneLayout.xaxis, - yaxis = sceneLayout.yaxis, - zaxis = sceneLayout.zaxis, - scaleFactor = scene.dataScale, - xlen = z[0].length, - ylen = z.length, - coords = [ - ndarray(new Float32Array(xlen * ylen), [xlen, ylen]), - ndarray(new Float32Array(xlen * ylen), [xlen, ylen]), - ndarray(new Float32Array(xlen * ylen), [xlen, ylen]) - ], - xc = coords[0], - yc = coords[1], - contourLevels = scene.contourLevels; - - // Save data - this.data = data; - - /* + var i, + scene = this.scene, + sceneLayout = scene.fullSceneLayout, + surface = this.surface, + alpha = data.opacity, + colormap = parseColorScale(data.colorscale, alpha), + z = data.z, + x = data.x, + y = data.y, + xaxis = sceneLayout.xaxis, + yaxis = sceneLayout.yaxis, + zaxis = sceneLayout.zaxis, + scaleFactor = scene.dataScale, + xlen = z[0].length, + ylen = z.length, + coords = [ + ndarray(new Float32Array(xlen * ylen), [xlen, ylen]), + ndarray(new Float32Array(xlen * ylen), [xlen, ylen]), + ndarray(new Float32Array(xlen * ylen), [xlen, ylen]) + ], + xc = coords[0], + yc = coords[1], + contourLevels = scene.contourLevels; + + // Save data + this.data = data; + + /* * Fill and transpose zdata. * Consistent with 'heatmap' and 'contour', plotly 'surface' * 'z' are such that sub-arrays correspond to y-coords * and that the sub-array entries correspond to a x-coords, * which is the transpose of 'gl-surface-plot'. */ - - var xcalendar = data.xcalendar, - ycalendar = data.ycalendar, - zcalendar = data.zcalendar; - - fill(coords[2], function(row, col) { - return zaxis.d2l(z[col][row], 0, zcalendar) * scaleFactor[2]; + var xcalendar = data.xcalendar, + ycalendar = data.ycalendar, + zcalendar = data.zcalendar; + + fill(coords[2], function(row, col) { + return zaxis.d2l(z[col][row], 0, zcalendar) * scaleFactor[2]; + }); + + // coords x + if (Array.isArray(x[0])) { + fill(xc, function(row, col) { + return xaxis.d2l(x[col][row], 0, xcalendar) * scaleFactor[0]; + }); + } else { + // ticks x + fill(xc, function(row) { + return xaxis.d2l(x[row], 0, xcalendar) * scaleFactor[0]; }); + } - // coords x - if(Array.isArray(x[0])) { - fill(xc, function(row, col) { - return xaxis.d2l(x[col][row], 0, xcalendar) * scaleFactor[0]; - }); - } else { - // ticks x - fill(xc, function(row) { - return xaxis.d2l(x[row], 0, xcalendar) * scaleFactor[0]; - }); - } + // coords y + if (Array.isArray(y[0])) { + fill(yc, function(row, col) { + return yaxis.d2l(y[col][row], 0, ycalendar) * scaleFactor[1]; + }); + } else { + // ticks y + fill(yc, function(row, col) { + return yaxis.d2l(y[col], 0, ycalendar) * scaleFactor[1]; + }); + } + + var params = { + colormap: colormap, + levels: [[], [], []], + showContour: [true, true, true], + showSurface: !data.hidesurface, + contourProject: [ + [false, false, false], + [false, false, false], + [false, false, false] + ], + contourWidth: [1, 1, 1], + contourColor: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]], + contourTint: [1, 1, 1], + dynamicColor: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]], + dynamicWidth: [1, 1, 1], + dynamicTint: [1, 1, 1], + opacity: 1 + }; + + params.intensityBounds = [data.cmin, data.cmax]; + + // Refine if necessary + if (data.surfacecolor) { + var intensity = ndarray(new Float32Array(xlen * ylen), [xlen, ylen]); + + fill(intensity, function(row, col) { + return data.surfacecolor[col][row]; + }); - // coords y - if(Array.isArray(y[0])) { - fill(yc, function(row, col) { - return yaxis.d2l(y[col][row], 0, ycalendar) * scaleFactor[1]; - }); - } else { - // ticks y - fill(yc, function(row, col) { - return yaxis.d2l(y[col], 0, ycalendar) * scaleFactor[1]; - }); - } + coords.push(intensity); + } else { + // when 'z' is used as 'intensity', + // we must scale its value + params.intensityBounds[0] *= scaleFactor[2]; + params.intensityBounds[1] *= scaleFactor[2]; + } - var params = { - colormap: colormap, - levels: [[], [], []], - showContour: [true, true, true], - showSurface: !data.hidesurface, - contourProject: [ - [false, false, false], - [false, false, false], - [false, false, false] - ], - contourWidth: [1, 1, 1], - contourColor: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]], - contourTint: [1, 1, 1], - dynamicColor: [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]], - dynamicWidth: [1, 1, 1], - dynamicTint: [1, 1, 1], - opacity: 1 - }; - - params.intensityBounds = [data.cmin, data.cmax]; - - // Refine if necessary - if(data.surfacecolor) { - var intensity = ndarray(new Float32Array(xlen * ylen), [xlen, ylen]); - - fill(intensity, function(row, col) { - return data.surfacecolor[col][row]; - }); - - coords.push(intensity); - } - else { - // when 'z' is used as 'intensity', - // we must scale its value - params.intensityBounds[0] *= scaleFactor[2]; - params.intensityBounds[1] *= scaleFactor[2]; - } + this.dataScale = refine(coords); - this.dataScale = refine(coords); + if (data.surfacecolor) { + params.intensity = coords.pop(); + } - if(data.surfacecolor) { - params.intensity = coords.pop(); + if ("opacity" in data) { + if (data.opacity < 1) { + params.opacity = 0.25 * data.opacity; } - - if('opacity' in data) { - if(data.opacity < 1) { - params.opacity = 0.25 * data.opacity; - } + } + + var highlightEnable = [true, true, true]; + var axis = ["x", "y", "z"]; + + for (i = 0; i < 3; ++i) { + var contourParams = data.contours[axis[i]]; + highlightEnable[i] = contourParams.highlight; + + params.showContour[i] = contourParams.show || contourParams.highlight; + if (!params.showContour[i]) continue; + + params.contourProject[i] = [ + contourParams.project.x, + contourParams.project.y, + contourParams.project.z + ]; + + if (contourParams.show) { + this.showContour[i] = true; + params.levels[i] = contourLevels[i]; + surface.highlightColor[i] = params.contourColor[i] = str2RgbaArray( + contourParams.color + ); + + if (contourParams.usecolormap) { + surface.highlightTint[i] = params.contourTint[i] = 0; + } else { + surface.highlightTint[i] = params.contourTint[i] = 1; + } + params.contourWidth[i] = contourParams.width; + } else { + this.showContour[i] = false; } - var highlightEnable = [true, true, true]; - var axis = ['x', 'y', 'z']; - - for(i = 0; i < 3; ++i) { - var contourParams = data.contours[axis[i]]; - highlightEnable[i] = contourParams.highlight; - - params.showContour[i] = contourParams.show || contourParams.highlight; - if(!params.showContour[i]) continue; - - params.contourProject[i] = [ - contourParams.project.x, - contourParams.project.y, - contourParams.project.z - ]; - - if(contourParams.show) { - this.showContour[i] = true; - params.levels[i] = contourLevels[i]; - surface.highlightColor[i] = params.contourColor[i] = str2RgbaArray(contourParams.color); - - if(contourParams.usecolormap) { - surface.highlightTint[i] = params.contourTint[i] = 0; - } - else { - surface.highlightTint[i] = params.contourTint[i] = 1; - } - params.contourWidth[i] = contourParams.width; - } else { - this.showContour[i] = false; - } - - if(contourParams.highlight) { - params.dynamicColor[i] = str2RgbaArray(contourParams.highlightcolor); - params.dynamicWidth[i] = contourParams.highlightwidth; - } + if (contourParams.highlight) { + params.dynamicColor[i] = str2RgbaArray(contourParams.highlightcolor); + params.dynamicWidth[i] = contourParams.highlightwidth; } + } - // see https://github.com/plotly/plotly.js/issues/940 - if(isColormapCircular(colormap)) { - params.vertexColor = true; - } + // see https://github.com/plotly/plotly.js/issues/940 + if (isColormapCircular(colormap)) { + params.vertexColor = true; + } - params.coords = coords; + params.coords = coords; - surface.update(params); + surface.update(params); - surface.visible = data.visible; - surface.enableDynamic = highlightEnable; + surface.visible = data.visible; + surface.enableDynamic = highlightEnable; - surface.snapToData = true; + surface.snapToData = true; - if('lighting' in data) { - surface.ambientLight = data.lighting.ambient; - surface.diffuseLight = data.lighting.diffuse; - surface.specularLight = data.lighting.specular; - surface.roughness = data.lighting.roughness; - surface.fresnel = data.lighting.fresnel; - } + if ("lighting" in data) { + surface.ambientLight = data.lighting.ambient; + surface.diffuseLight = data.lighting.diffuse; + surface.specularLight = data.lighting.specular; + surface.roughness = data.lighting.roughness; + surface.fresnel = data.lighting.fresnel; + } - if('lightposition' in data) { - surface.lightPosition = [data.lightposition.x, data.lightposition.y, data.lightposition.z]; - } + if ("lightposition" in data) { + surface.lightPosition = [ + data.lightposition.x, + data.lightposition.y, + data.lightposition.z + ]; + } - if(alpha && alpha < 1) { - surface.supportsTransparency = true; - } + if (alpha && alpha < 1) { + surface.supportsTransparency = true; + } }; proto.dispose = function() { - this.scene.glplot.remove(this.surface); - this.surface.dispose(); + this.scene.glplot.remove(this.surface); + this.surface.dispose(); }; function createSurfaceTrace(scene, data) { - var gl = scene.glplot.gl; - var surface = createSurface({ gl: gl }); - var result = new SurfaceTrace(scene, surface, data.uid); - result.update(data); - scene.glplot.add(surface); - return result; + var gl = scene.glplot.gl; + var surface = createSurface({ gl: gl }); + var result = new SurfaceTrace(scene, surface, data.uid); + result.update(data); + scene.glplot.add(surface); + return result; } module.exports = createSurfaceTrace; diff --git a/src/traces/surface/defaults.js b/src/traces/surface/defaults.js index cab5da34f98..fc8820c6504 100644 --- a/src/traces/surface/defaults.js +++ b/src/traces/surface/defaults.js @@ -5,115 +5,120 @@ * 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 Registry = require('../../registry'); -var Lib = require('../../lib'); - -var colorscaleDefaults = require('../../components/colorscale/defaults'); -var attributes = require('./attributes'); - - -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - var i, j; - - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); - } - - var z = coerce('z'); - if(!z) { - traceOut.visible = false; - return; +"use strict"; +var Registry = require("../../registry"); +var Lib = require("../../lib"); + +var colorscaleDefaults = require("../../components/colorscale/defaults"); +var attributes = require("./attributes"); + +module.exports = function supplyDefaults( + traceIn, + traceOut, + defaultColor, + layout +) { + var i, j; + + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var z = coerce("z"); + if (!z) { + traceOut.visible = false; + return; + } + + var xlen = z[0].length; + var ylen = z.length; + + coerce("x"); + coerce("y"); + + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleTraceDefaults" + ); + handleCalendarDefaults(traceIn, traceOut, ["x", "y", "z"], layout); + + if (!Array.isArray(traceOut.x)) { + // build a linearly scaled x + traceOut.x = []; + for (i = 0; i < xlen; ++i) { + traceOut.x[i] = i; } + } - var xlen = z[0].length; - var ylen = z.length; - - coerce('x'); - coerce('y'); - - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); - handleCalendarDefaults(traceIn, traceOut, ['x', 'y', 'z'], layout); - - if(!Array.isArray(traceOut.x)) { - // build a linearly scaled x - traceOut.x = []; - for(i = 0; i < xlen; ++i) { - traceOut.x[i] = i; - } + coerce("text"); + if (!Array.isArray(traceOut.y)) { + traceOut.y = []; + for (i = 0; i < ylen; ++i) { + traceOut.y[i] = i; } - - coerce('text'); - if(!Array.isArray(traceOut.y)) { - traceOut.y = []; - for(i = 0; i < ylen; ++i) { - traceOut.y[i] = i; - } + } + + // Coerce remaining properties + [ + "lighting.ambient", + "lighting.diffuse", + "lighting.specular", + "lighting.roughness", + "lighting.fresnel", + "lightposition.x", + "lightposition.y", + "lightposition.z", + "hidesurface", + "opacity" + ].forEach(function(x) { + coerce(x); + }); + + var surfaceColor = coerce("surfacecolor"); + + coerce("colorscale"); + + var dims = ["x", "y", "z"]; + for (i = 0; i < 3; ++i) { + var contourDim = "contours." + dims[i]; + var show = coerce(contourDim + ".show"); + var highlight = coerce(contourDim + ".highlight"); + + if (show || highlight) { + for (j = 0; j < 3; ++j) { + coerce(contourDim + ".project." + dims[j]); + } } - // Coerce remaining properties - [ - 'lighting.ambient', - 'lighting.diffuse', - 'lighting.specular', - 'lighting.roughness', - 'lighting.fresnel', - 'lightposition.x', - 'lightposition.y', - 'lightposition.z', - 'hidesurface', - 'opacity' - ].forEach(function(x) { coerce(x); }); - - var surfaceColor = coerce('surfacecolor'); - - coerce('colorscale'); - - var dims = ['x', 'y', 'z']; - for(i = 0; i < 3; ++i) { - - var contourDim = 'contours.' + dims[i]; - var show = coerce(contourDim + '.show'); - var highlight = coerce(contourDim + '.highlight'); - - if(show || highlight) { - for(j = 0; j < 3; ++j) { - coerce(contourDim + '.project.' + dims[j]); - } - } - - if(show) { - coerce(contourDim + '.color'); - coerce(contourDim + '.width'); - coerce(contourDim + '.usecolormap'); - } - - if(highlight) { - coerce(contourDim + '.highlightcolor'); - coerce(contourDim + '.highlightwidth'); - } + if (show) { + coerce(contourDim + ".color"); + coerce(contourDim + ".width"); + coerce(contourDim + ".usecolormap"); } - // backward compatibility block - if(!surfaceColor) { - mapLegacy(traceIn, 'zmin', 'cmin'); - mapLegacy(traceIn, 'zmax', 'cmax'); - mapLegacy(traceIn, 'zauto', 'cauto'); + if (highlight) { + coerce(contourDim + ".highlightcolor"); + coerce(contourDim + ".highlightwidth"); } - - // TODO if contours.?.usecolormap are false and hidesurface is true - // the colorbar shouldn't be shown by default - - colorscaleDefaults( - traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'c'} - ); + } + + // backward compatibility block + if (!surfaceColor) { + mapLegacy(traceIn, "zmin", "cmin"); + mapLegacy(traceIn, "zmax", "cmax"); + mapLegacy(traceIn, "zauto", "cauto"); + } + + // TODO if contours.?.usecolormap are false and hidesurface is true + // the colorbar shouldn't be shown by default + colorscaleDefaults(traceIn, traceOut, layout, coerce, { + prefix: "", + cLetter: "c" + }); }; function mapLegacy(traceIn, oldAttr, newAttr) { - if(oldAttr in traceIn && !(newAttr in traceIn)) { - traceIn[newAttr] = traceIn[oldAttr]; - } + if (oldAttr in traceIn && !(newAttr in traceIn)) { + traceIn[newAttr] = traceIn[oldAttr]; + } } diff --git a/src/traces/surface/index.js b/src/traces/surface/index.js index 8a9d1efb156..615f20c45d2 100644 --- a/src/traces/surface/index.js +++ b/src/traces/surface/index.js @@ -5,37 +5,31 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - - -'use strict'; - +"use strict"; var Surface = {}; -Surface.attributes = require('./attributes'); -Surface.supplyDefaults = require('./defaults'); -Surface.colorbar = require('./colorbar'); -Surface.calc = require('./calc'); -Surface.plot = require('./convert'); +Surface.attributes = require("./attributes"); +Surface.supplyDefaults = require("./defaults"); +Surface.colorbar = require("./colorbar"); +Surface.calc = require("./calc"); +Surface.plot = require("./convert"); -Surface.moduleType = 'trace'; -Surface.name = 'surface'; -Surface.basePlotModule = require('../../plots/gl3d'); -Surface.categories = ['gl3d', 'noOpacity']; +Surface.moduleType = "trace"; +Surface.name = "surface"; +Surface.basePlotModule = require("../../plots/gl3d"); +Surface.categories = ["gl3d", "noOpacity"]; Surface.meta = { - description: [ - 'The data the describes the coordinates of the surface is set in `z`.', - 'Data in `z` should be a {2D array}.', - - 'Coordinates in `x` and `y` can either be 1D {arrays}', - 'or {2D arrays} (e.g. to graph parametric surfaces).', - - 'If not provided in `x` and `y`, the x and y coordinates are assumed', - 'to be linear starting at 0 with a unit step.', - - 'The color scale corresponds to the `z` values by default.', - 'For custom color scales, use `surfacecolor` which should be a {2D array},', - 'where its bounds can be controlled using `cmin` and `cmax`.' - ].join(' ') + description: [ + "The data the describes the coordinates of the surface is set in `z`.", + "Data in `z` should be a {2D array}.", + "Coordinates in `x` and `y` can either be 1D {arrays}", + "or {2D arrays} (e.g. to graph parametric surfaces).", + "If not provided in `x` and `y`, the x and y coordinates are assumed", + "to be linear starting at 0 with a unit step.", + "The color scale corresponds to the `z` values by default.", + "For custom color scales, use `surfacecolor` which should be a {2D array},", + "where its bounds can be controlled using `cmin` and `cmax`." + ].join(" ") }; module.exports = Surface; diff --git a/src/transforms/filter.js b/src/transforms/filter.js index f77285b5927..159aae2f70f 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -5,331 +5,341 @@ * 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 Registry = require("../registry"); +var PlotSchema = require("../plot_api/plot_schema"); +var axisIds = require("../plots/cartesian/axis_ids"); +var autoType = require("../plots/cartesian/axis_autotype"); +var setConvert = require("../plots/cartesian/set_convert"); -'use strict'; +var INEQUALITY_OPS = ["=", "<", ">=", ">", "<="]; +var INTERVAL_OPS = ["[]", "()", "[)", "(]", "][", ")(", "](", ")["]; +var SET_OPS = ["{}", "}{"]; -var Lib = require('../lib'); -var Registry = require('../registry'); -var PlotSchema = require('../plot_api/plot_schema'); -var axisIds = require('../plots/cartesian/axis_ids'); -var autoType = require('../plots/cartesian/axis_autotype'); -var setConvert = require('../plots/cartesian/set_convert'); +exports.moduleType = "transform"; -var INEQUALITY_OPS = ['=', '<', '>=', '>', '<=']; -var INTERVAL_OPS = ['[]', '()', '[)', '(]', '][', ')(', '](', ')[']; -var SET_OPS = ['{}', '}{']; - -exports.moduleType = 'transform'; - -exports.name = 'filter'; +exports.name = "filter"; exports.attributes = { - enabled: { - valType: 'boolean', - dflt: true, - description: [ - 'Determines whether this filter transform is enabled or disabled.' - ].join(' ') - }, - target: { - valType: 'string', - strict: true, - noBlank: true, - arrayOk: true, - dflt: 'x', - description: [ - 'Sets the filter target by which the filter is applied.', - - 'If a string, *target* is assumed to be a reference to a data array', - 'in the parent trace object.', - 'To filter about nested variables, use *.* to access them.', - 'For example, set `target` to *marker.color* to filter', - 'about the marker color array.', - - 'If an array, *target* is then the data array by which the filter is applied.' - ].join(' ') - }, - operation: { - valType: 'enumerated', - values: [].concat(INEQUALITY_OPS).concat(INTERVAL_OPS).concat(SET_OPS), - dflt: '=', - description: [ - 'Sets the filter operation.', - - '*=* keeps items equal to `value`', - - '*<* keeps items less than `value`', - '*<=* keeps items less than or equal to `value`', - - '*>* keeps items greater than `value`', - '*>=* keeps items greater than or equal to `value`', - - '*[]* keeps items inside `value[0]` to value[1]` including both bounds`', - '*()* keeps items inside `value[0]` to value[1]` excluding both bounds`', - '*[)* keeps items inside `value[0]` to value[1]` including `value[0]` but excluding `value[1]', - '*(]* keeps items inside `value[0]` to value[1]` excluding `value[0]` but including `value[1]', - - '*][* keeps items outside `value[0]` to value[1]` and equal to both bounds`', - '*)(* keeps items outside `value[0]` to value[1]`', - '*](* keeps items outside `value[0]` to value[1]` and equal to `value[0]`', - '*)[* keeps items outside `value[0]` to value[1]` and equal to `value[1]`', - - '*{}* keeps items present in a set of values', - '*}{* keeps items not present in a set of values' - ].join(' ') - }, - value: { - valType: 'any', - dflt: 0, - description: [ - 'Sets the value or values by which to filter by.', - - 'Values are expected to be in the same type as the data linked', - 'to *target*.', - - 'When `operation` is set to one of the inequality values', - '(' + INEQUALITY_OPS + ')', - '*value* is expected to be a number or a string.', - - 'When `operation` is set to one of the interval value', - '(' + INTERVAL_OPS + ')', - '*value* is expected to be 2-item array where the first item', - 'is the lower bound and the second item is the upper bound.', - - 'When `operation`, is set to one of the set value', - '(' + SET_OPS + ')', - '*value* is expected to be an array with as many items as', - 'the desired set elements.' - ].join(' ') - } + enabled: { + valType: "boolean", + dflt: true, + description: [ + "Determines whether this filter transform is enabled or disabled." + ].join(" ") + }, + target: { + valType: "string", + strict: true, + noBlank: true, + arrayOk: true, + dflt: "x", + description: [ + "Sets the filter target by which the filter is applied.", + "If a string, *target* is assumed to be a reference to a data array", + "in the parent trace object.", + "To filter about nested variables, use *.* to access them.", + "For example, set `target` to *marker.color* to filter", + "about the marker color array.", + "If an array, *target* is then the data array by which the filter is applied." + ].join(" ") + }, + operation: { + valType: "enumerated", + values: [].concat(INEQUALITY_OPS).concat(INTERVAL_OPS).concat(SET_OPS), + dflt: "=", + description: [ + "Sets the filter operation.", + "*=* keeps items equal to `value`", + "*<* keeps items less than `value`", + "*<=* keeps items less than or equal to `value`", + "*>* keeps items greater than `value`", + "*>=* keeps items greater than or equal to `value`", + "*[]* keeps items inside `value[0]` to value[1]` including both bounds`", + "*()* keeps items inside `value[0]` to value[1]` excluding both bounds`", + "*[)* keeps items inside `value[0]` to value[1]` including `value[0]` but excluding `value[1]", + "*(]* keeps items inside `value[0]` to value[1]` excluding `value[0]` but including `value[1]", + "*][* keeps items outside `value[0]` to value[1]` and equal to both bounds`", + "*)(* keeps items outside `value[0]` to value[1]`", + "*](* keeps items outside `value[0]` to value[1]` and equal to `value[0]`", + "*)[* keeps items outside `value[0]` to value[1]` and equal to `value[1]`", + "*{}* keeps items present in a set of values", + "*}{* keeps items not present in a set of values" + ].join(" ") + }, + value: { + valType: "any", + dflt: 0, + description: [ + "Sets the value or values by which to filter by.", + "Values are expected to be in the same type as the data linked", + "to *target*.", + "When `operation` is set to one of the inequality values", + "(" + INEQUALITY_OPS + ")", + "*value* is expected to be a number or a string.", + "When `operation` is set to one of the interval value", + "(" + INTERVAL_OPS + ")", + "*value* is expected to be 2-item array where the first item", + "is the lower bound and the second item is the upper bound.", + "When `operation`, is set to one of the set value", + "(" + SET_OPS + ")", + "*value* is expected to be an array with as many items as", + "the desired set elements." + ].join(" ") + } }; exports.supplyDefaults = function(transformIn) { - var transformOut = {}; - - function coerce(attr, dflt) { - return Lib.coerce(transformIn, transformOut, exports.attributes, attr, dflt); - } - - var enabled = coerce('enabled'); - - if(enabled) { - coerce('operation'); - coerce('value'); - coerce('target'); - - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults'); - handleCalendarDefaults(transformIn, transformOut, 'valuecalendar', null); - handleCalendarDefaults(transformIn, transformOut, 'targetcalendar', null); - } - - return transformOut; + var transformOut = {}; + + function coerce(attr, dflt) { + return Lib.coerce( + transformIn, + transformOut, + exports.attributes, + attr, + dflt + ); + } + + var enabled = coerce("enabled"); + + if (enabled) { + coerce("operation"); + coerce("value"); + coerce("target"); + + var handleCalendarDefaults = Registry.getComponentMethod( + "calendars", + "handleDefaults" + ); + handleCalendarDefaults(transformIn, transformOut, "valuecalendar", null); + handleCalendarDefaults(transformIn, transformOut, "targetcalendar", null); + } + + return transformOut; }; exports.calcTransform = function(gd, trace, opts) { - if(!opts.enabled) return; + if (!opts.enabled) return; - var target = opts.target, - filterArray = getFilterArray(trace, target), - len = filterArray.length; + var target = opts.target, + filterArray = getFilterArray(trace, target), + len = filterArray.length; - if(!len) return; + if (!len) return; - var targetCalendar = opts.targetcalendar; + var targetCalendar = opts.targetcalendar; - // even if you provide targetcalendar, if target is a string and there - // is a calendar attribute matching target it will get used instead. - if(typeof target === 'string') { - var attrTargetCalendar = Lib.nestedProperty(trace, target + 'calendar').get(); - if(attrTargetCalendar) targetCalendar = attrTargetCalendar; - } + // even if you provide targetcalendar, if target is a string and there + // is a calendar attribute matching target it will get used instead. + if (typeof target === "string") { + var attrTargetCalendar = Lib.nestedProperty( + trace, + target + "calendar" + ).get(); + if (attrTargetCalendar) targetCalendar = attrTargetCalendar; + } - // if target points to an axis, use the type we already have for that - // axis to find the data type. Otherwise use the values to autotype. - var d2cTarget = (target === 'x' || target === 'y' || target === 'z') ? - target : filterArray; + // if target points to an axis, use the type we already have for that + // axis to find the data type. Otherwise use the values to autotype. + var d2cTarget = target === "x" || target === "y" || target === "z" + ? target + : filterArray; - var dataToCoord = getDataToCoordFunc(gd, trace, d2cTarget), - filterFunc = getFilterFunc(opts, dataToCoord, targetCalendar), - arrayAttrs = PlotSchema.findArrayAttributes(trace), - originalArrays = {}; + var dataToCoord = getDataToCoordFunc(gd, trace, d2cTarget), + filterFunc = getFilterFunc(opts, dataToCoord, targetCalendar), + arrayAttrs = PlotSchema.findArrayAttributes(trace), + originalArrays = {}; - // copy all original array attribute values, - // and clear arrays in trace - for(var k = 0; k < arrayAttrs.length; k++) { - var attr = arrayAttrs[k], - np = Lib.nestedProperty(trace, attr); + // copy all original array attribute values, + // and clear arrays in trace + for (var k = 0; k < arrayAttrs.length; k++) { + var attr = arrayAttrs[k], np = Lib.nestedProperty(trace, attr); - originalArrays[attr] = Lib.extendDeep([], np.get()); - np.set([]); - } + originalArrays[attr] = Lib.extendDeep([], np.get()); + np.set([]); + } - function fill(attr, i) { - var oldArr = originalArrays[attr], - newArr = Lib.nestedProperty(trace, attr).get(); + function fill(attr, i) { + var oldArr = originalArrays[attr], + newArr = Lib.nestedProperty(trace, attr).get(); - newArr.push(oldArr[i]); - } + newArr.push(oldArr[i]); + } - for(var i = 0; i < len; i++) { - var v = filterArray[i]; + for (var i = 0; i < len; i++) { + var v = filterArray[i]; - if(!filterFunc(v)) continue; + if (!filterFunc(v)) continue; - for(var j = 0; j < arrayAttrs.length; j++) { - fill(arrayAttrs[j], i); - } + for (var j = 0; j < arrayAttrs.length; j++) { + fill(arrayAttrs[j], i); } + } }; function getFilterArray(trace, target) { - if(typeof target === 'string' && target) { - var array = Lib.nestedProperty(trace, target).get(); + if (typeof target === "string" && target) { + var array = Lib.nestedProperty(trace, target).get(); - return Array.isArray(array) ? array : []; - } - else if(Array.isArray(target)) return target.slice(); + return Array.isArray(array) ? array : []; + } else if (Array.isArray(target)) return target.slice(); - return false; + return false; } function getDataToCoordFunc(gd, trace, target) { - var ax; - - // In the case of an array target, make a mock data array - // and call supplyDefaults to the data type and - // setup the data-to-calc method. - if(Array.isArray(target)) { - ax = { - type: autoType(target), - _categories: [] - }; - - setConvert(ax); - - if(ax.type === 'category') { - // build up ax._categories (usually done during ax.makeCalcdata() - for(var i = 0; i < target.length; i++) { - ax.d2c(target[i]); - } - } - } - else { - ax = axisIds.getFromTrace(gd, trace, target); - } + var ax; - // if 'target' has corresponding axis - // -> use setConvert method - if(ax) return ax.d2c; + // In the case of an array target, make a mock data array + // and call supplyDefaults to the data type and + // setup the data-to-calc method. + if (Array.isArray(target)) { + ax = { type: autoType(target), _categories: [] }; - // special case for 'ids' - // -> cast to String - if(target === 'ids') return function(v) { return String(v); }; + setConvert(ax); - // otherwise (e.g. numeric-array of 'marker.color' or 'marker.size') - // -> cast to Number - return function(v) { return +v; }; + if (ax.type === "category") { + // build up ax._categories (usually done during ax.makeCalcdata() + for (var i = 0; i < target.length; i++) { + ax.d2c(target[i]); + } + } + } else { + ax = axisIds.getFromTrace(gd, trace, target); + } + + // if 'target' has corresponding axis + // -> use setConvert method + if (ax) return ax.d2c; + + // special case for 'ids' + // -> cast to String + if (target === "ids") { + return function(v) { + return String(v); + }; + } + + // otherwise (e.g. numeric-array of 'marker.color' or 'marker.size') + // -> cast to Number + return function(v) { + return +v; + }; } function getFilterFunc(opts, d2c, targetCalendar) { - var operation = opts.operation, - value = opts.value, - hasArrayValue = Array.isArray(value); - - function isOperationIn(array) { - return array.indexOf(operation) !== -1; - } - - var d2cValue = function(v) { return d2c(v, 0, opts.valuecalendar); }, - d2cTarget = function(v) { return d2c(v, 0, targetCalendar); }; - - var coercedValue; - - if(isOperationIn(INEQUALITY_OPS)) { - coercedValue = hasArrayValue ? d2cValue(value[0]) : d2cValue(value); - } - else if(isOperationIn(INTERVAL_OPS)) { - coercedValue = hasArrayValue ? - [d2cValue(value[0]), d2cValue(value[1])] : - [d2cValue(value), d2cValue(value)]; - } - else if(isOperationIn(SET_OPS)) { - coercedValue = hasArrayValue ? value.map(d2cValue) : [d2cValue(value)]; - } - - switch(operation) { - - case '=': - return function(v) { return d2cTarget(v) === coercedValue; }; - - case '<': - return function(v) { return d2cTarget(v) < coercedValue; }; - - case '<=': - return function(v) { return d2cTarget(v) <= coercedValue; }; - - case '>': - return function(v) { return d2cTarget(v) > coercedValue; }; - - case '>=': - return function(v) { return d2cTarget(v) >= coercedValue; }; - - case '[]': - return function(v) { - var cv = d2cTarget(v); - return cv >= coercedValue[0] && cv <= coercedValue[1]; - }; - - case '()': - return function(v) { - var cv = d2cTarget(v); - return cv > coercedValue[0] && cv < coercedValue[1]; - }; - - case '[)': - return function(v) { - var cv = d2cTarget(v); - return cv >= coercedValue[0] && cv < coercedValue[1]; - }; - - case '(]': - return function(v) { - var cv = d2cTarget(v); - return cv > coercedValue[0] && cv <= coercedValue[1]; - }; - - case '][': - return function(v) { - var cv = d2cTarget(v); - return cv <= coercedValue[0] || cv >= coercedValue[1]; - }; - - case ')(': - return function(v) { - var cv = d2cTarget(v); - return cv < coercedValue[0] || cv > coercedValue[1]; - }; - - case '](': - return function(v) { - var cv = d2cTarget(v); - return cv <= coercedValue[0] || cv > coercedValue[1]; - }; - - case ')[': - return function(v) { - var cv = d2cTarget(v); - return cv < coercedValue[0] || cv >= coercedValue[1]; - }; - - case '{}': - return function(v) { - return coercedValue.indexOf(d2cTarget(v)) !== -1; - }; - - case '}{': - return function(v) { - return coercedValue.indexOf(d2cTarget(v)) === -1; - }; - } + var operation = opts.operation, + value = opts.value, + hasArrayValue = Array.isArray(value); + + function isOperationIn(array) { + return array.indexOf(operation) !== -1; + } + + var d2cValue = function(v) { + return d2c(v, 0, opts.valuecalendar); + }, + d2cTarget = function(v) { + return d2c(v, 0, targetCalendar); + }; + + var coercedValue; + + if (isOperationIn(INEQUALITY_OPS)) { + coercedValue = hasArrayValue ? d2cValue(value[0]) : d2cValue(value); + } else if (isOperationIn(INTERVAL_OPS)) { + coercedValue = hasArrayValue + ? [d2cValue(value[0]), d2cValue(value[1])] + : [d2cValue(value), d2cValue(value)]; + } else if (isOperationIn(SET_OPS)) { + coercedValue = hasArrayValue ? value.map(d2cValue) : [d2cValue(value)]; + } + + switch (operation) { + case "=": + return function(v) { + return d2cTarget(v) === coercedValue; + }; + + case "<": + return function(v) { + return d2cTarget(v) < coercedValue; + }; + + case "<=": + return function(v) { + return d2cTarget(v) <= coercedValue; + }; + + case ">": + return function(v) { + return d2cTarget(v) > coercedValue; + }; + + case ">=": + return function(v) { + return d2cTarget(v) >= coercedValue; + }; + + case "[]": + return function(v) { + var cv = d2cTarget(v); + return cv >= coercedValue[0] && cv <= coercedValue[1]; + }; + + case "()": + return function(v) { + var cv = d2cTarget(v); + return cv > coercedValue[0] && cv < coercedValue[1]; + }; + + case "[)": + return function(v) { + var cv = d2cTarget(v); + return cv >= coercedValue[0] && cv < coercedValue[1]; + }; + + case "(]": + return function(v) { + var cv = d2cTarget(v); + return cv > coercedValue[0] && cv <= coercedValue[1]; + }; + + case "][": + return function(v) { + var cv = d2cTarget(v); + return cv <= coercedValue[0] || cv >= coercedValue[1]; + }; + + case ")(": + return function(v) { + var cv = d2cTarget(v); + return cv < coercedValue[0] || cv > coercedValue[1]; + }; + + case "](": + return function(v) { + var cv = d2cTarget(v); + return cv <= coercedValue[0] || cv > coercedValue[1]; + }; + + case ")[": + return function(v) { + var cv = d2cTarget(v); + return cv < coercedValue[0] || cv >= coercedValue[1]; + }; + + case "{}": + return function(v) { + return coercedValue.indexOf(d2cTarget(v)) !== -1; + }; + + case "}{": + return function(v) { + return coercedValue.indexOf(d2cTarget(v)) === -1; + }; + } } diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index 0cd744529ca..9be63de7930 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -5,45 +5,43 @@ * 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 PlotSchema = require("../plot_api/plot_schema"); -'use strict'; +exports.moduleType = "transform"; -var Lib = require('../lib'); -var PlotSchema = require('../plot_api/plot_schema'); - -exports.moduleType = 'transform'; - -exports.name = 'groupby'; +exports.name = "groupby"; exports.attributes = { - enabled: { - valType: 'boolean', - dflt: true, - description: [ - 'Determines whether this group-by transform is enabled or disabled.' - ].join(' ') - }, - groups: { - valType: 'data_array', - dflt: [], - description: [ - 'Sets the groups in which the trace data will be split.', - 'For example, with `x` set to *[1, 2, 3, 4]* and', - '`groups` set to *[\'a\', \'b\', \'a\', \'b\']*,', - 'the groupby transform with split in one trace', - 'with `x` [1, 3] and one trace with `x` [2, 4].' - ].join(' ') - }, - style: { - valType: 'any', - dflt: {}, - description: [ - 'Sets each group style.', - 'For example, with `groups` set to *[\'a\', \'b\', \'a\', \'b\']*', - 'and `style` set to *{ a: { marker: { color: \'red\' } }}', - 'marker points in group *\'a\'* will be drawn in red.' - ].join(' ') - } + enabled: { + valType: "boolean", + dflt: true, + description: [ + "Determines whether this group-by transform is enabled or disabled." + ].join(" ") + }, + groups: { + valType: "data_array", + dflt: [], + description: [ + "Sets the groups in which the trace data will be split.", + "For example, with `x` set to *[1, 2, 3, 4]* and", + "`groups` set to *['a', 'b', 'a', 'b']*,", + "the groupby transform with split in one trace", + "with `x` [1, 3] and one trace with `x` [2, 4]." + ].join(" ") + }, + style: { + valType: "any", + dflt: {}, + description: [ + "Sets each group style.", + "For example, with `groups` set to *['a', 'b', 'a', 'b']*", + "and `style` set to *{ a: { marker: { color: 'red' } }}", + "marker points in group *'a'* will be drawn in red." + ].join(" ") + } }; /** @@ -60,20 +58,26 @@ exports.attributes = { * copy of transformIn that contains attribute defaults */ exports.supplyDefaults = function(transformIn) { - var transformOut = {}; - - function coerce(attr, dflt) { - return Lib.coerce(transformIn, transformOut, exports.attributes, attr, dflt); - } + var transformOut = {}; + + function coerce(attr, dflt) { + return Lib.coerce( + transformIn, + transformOut, + exports.attributes, + attr, + dflt + ); + } - var enabled = coerce('enabled'); + var enabled = coerce("enabled"); - if(!enabled) return transformOut; + if (!enabled) return transformOut; - coerce('groups'); - coerce('style'); + coerce("groups"); + coerce("style"); - return transformOut; + return transformOut; }; /** @@ -93,62 +97,62 @@ exports.supplyDefaults = function(transformIn) { * array of transformed traces */ exports.transform = function(data, state) { - var newData = []; + var newData = []; - for(var i = 0; i < data.length; i++) { - newData = newData.concat(transformOne(data[i], state)); - } + for (var i = 0; i < data.length; i++) { + newData = newData.concat(transformOne(data[i], state)); + } - return newData; + return newData; }; function initializeArray(newTrace, a) { - Lib.nestedProperty(newTrace, a).set([]); + Lib.nestedProperty(newTrace, a).set([]); } function pasteArray(newTrace, trace, j, a) { - Lib.nestedProperty(newTrace, a).set( - Lib.nestedProperty(newTrace, a).get().concat([ - Lib.nestedProperty(trace, a).get()[j] - ]) - ); + Lib.nestedProperty(newTrace, a).set( + Lib.nestedProperty(newTrace, a) + .get() + .concat([Lib.nestedProperty(trace, a).get()[j]]) + ); } function transformOne(trace, state) { - var opts = state.transform; - var groups = trace.transforms[state.transformIndex].groups; + var opts = state.transform; + var groups = trace.transforms[state.transformIndex].groups; - if(!(Array.isArray(groups)) || groups.length === 0) { - return trace; - } + if (!Array.isArray(groups) || groups.length === 0) { + return trace; + } - var groupNames = Lib.filterUnique(groups), - newData = new Array(groupNames.length), - len = groups.length; + var groupNames = Lib.filterUnique(groups), + newData = new Array(groupNames.length), + len = groups.length; - var arrayAttrs = PlotSchema.findArrayAttributes(trace); + var arrayAttrs = PlotSchema.findArrayAttributes(trace); - var style = opts.style || {}; + var style = opts.style || {}; - for(var i = 0; i < groupNames.length; i++) { - var groupName = groupNames[i]; + for (var i = 0; i < groupNames.length; i++) { + var groupName = groupNames[i]; - var newTrace = newData[i] = Lib.extendDeepNoArrays({}, trace); + var newTrace = newData[i] = Lib.extendDeepNoArrays({}, trace); - arrayAttrs.forEach(initializeArray.bind(null, newTrace)); + arrayAttrs.forEach(initializeArray.bind(null, newTrace)); - for(var j = 0; j < len; j++) { - if(groups[j] !== groupName) continue; + for (var j = 0; j < len; j++) { + if (groups[j] !== groupName) continue; - arrayAttrs.forEach(pasteArray.bind(0, newTrace, trace, j)); - } + arrayAttrs.forEach(pasteArray.bind(0, newTrace, trace, j)); + } - newTrace.name = groupName; + newTrace.name = groupName; - // there's no need to coerce style[groupName] here - // as another round of supplyDefaults is done on the transformed traces - newTrace = Lib.extendDeepNoArrays(newTrace, style[groupName] || {}); - } + // there's no need to coerce style[groupName] here + // as another round of supplyDefaults is done on the transformed traces + newTrace = Lib.extendDeepNoArrays(newTrace, style[groupName] || {}); + } - return newData; + return newData; } diff --git a/test/image/assets/get_image_paths.js b/test/image/assets/get_image_paths.js index 915bce2c2c0..7ab2beb53c4 100644 --- a/test/image/assets/get_image_paths.js +++ b/test/image/assets/get_image_paths.js @@ -1,8 +1,7 @@ -var path = require('path'); -var constants = require('../../../tasks/util/constants'); - -var DEFAULT_FORMAT = 'png'; +var path = require("path"); +var constants = require("../../../tasks/util/constants"); +var DEFAULT_FORMAT = "png"; /** * Return paths to baseline, test-image and diff images for a given mock name. @@ -15,15 +14,15 @@ var DEFAULT_FORMAT = 'png'; * diff */ module.exports = function getImagePaths(mockName, format) { - format = format || DEFAULT_FORMAT; + format = format || DEFAULT_FORMAT; - return { - baseline: join(constants.pathToTestImageBaselines, mockName, format), - test: join(constants.pathToTestImages, mockName, format), - diff: join(constants.pathToTestImagesDiff, 'diff-' + mockName, format) - }; + return { + baseline: join(constants.pathToTestImageBaselines, mockName, format), + test: join(constants.pathToTestImages, mockName, format), + diff: join(constants.pathToTestImagesDiff, "diff-" + mockName, format) + }; }; function join(basePath, fileName, format) { - return path.join(basePath, fileName) + '.' + format; + return path.join(basePath, fileName) + "." + format; } diff --git a/test/image/assets/get_image_request_options.js b/test/image/assets/get_image_request_options.js index 82589f76d16..9fb0aebf35d 100644 --- a/test/image/assets/get_image_request_options.js +++ b/test/image/assets/get_image_request_options.js @@ -1,7 +1,7 @@ -var path = require('path'); -var constants = require('../../../tasks/util/constants'); +var path = require("path"); +var constants = require("../../../tasks/util/constants"); -var DEFAULT_FORMAT = 'png'; +var DEFAULT_FORMAT = "png"; var DEFAULT_SCALE = 1; /** @@ -14,21 +14,22 @@ var DEFAULT_SCALE = 1; * url (optional): URL of image server */ module.exports = function getRequestOpts(specs) { - var pathToMock = path.join(constants.pathToTestImageMocks, specs.mockName) + '.json'; - var figure = require(pathToMock); + var pathToMock = path.join(constants.pathToTestImageMocks, specs.mockName) + + ".json"; + var figure = require(pathToMock); - var body = { - figure: figure, - format: specs.format || DEFAULT_FORMAT, - scale: specs.scale || DEFAULT_SCALE - }; + var body = { + figure: figure, + format: specs.format || DEFAULT_FORMAT, + scale: specs.scale || DEFAULT_SCALE + }; - if(specs.width) body.width = specs.width; - if(specs.height) body.height = specs.height; + if (specs.width) body.width = specs.width; + if (specs.height) body.height = specs.height; - return { - method: 'POST', - url: constants.testContainerUrl, - body: JSON.stringify(body) - }; + return { + method: "POST", + url: constants.testContainerUrl, + body: JSON.stringify(body) + }; }; diff --git a/test/image/assets/get_mock_list.js b/test/image/assets/get_mock_list.js index e0f4b39a147..8370b1bd185 100644 --- a/test/image/assets/get_mock_list.js +++ b/test/image/assets/get_mock_list.js @@ -1,8 +1,7 @@ -var path = require('path'); -var glob = require('glob'); - -var constants = require('../../../tasks/util/constants'); +var path = require("path"); +var glob = require("glob"); +var constants = require("../../../tasks/util/constants"); /** * Return array of mock name corresponding to input glob pattern @@ -11,19 +10,19 @@ var constants = require('../../../tasks/util/constants'); * @return {array} */ module.exports = function getMocks(pattern) { - // defaults to 'all' - pattern = pattern || '*'; + // defaults to 'all' + pattern = pattern || "*"; - // defaults to '.json' ext is none is provided - if(path.extname(pattern) === '') pattern += '.json'; + // defaults to '.json' ext is none is provided + if (path.extname(pattern) === "") pattern += ".json"; - var patternFull = constants.pathToTestImageMocks + '/' + pattern; - var matches = glob.sync(patternFull); + var patternFull = constants.pathToTestImageMocks + "/" + pattern; + var matches = glob.sync(patternFull); - // return only the mock name (not a full path, no ext) - var mockNames = matches.map(function(match) { - return path.basename(match).split('.')[0]; - }); + // return only the mock name (not a full path, no ext) + var mockNames = matches.map(function(match) { + return path.basename(match).split(".")[0]; + }); - return mockNames; + return mockNames; }; diff --git a/test/image/compare_pixels_test.js b/test/image/compare_pixels_test.js index ce210f88f88..0e9980e62ef 100644 --- a/test/image/compare_pixels_test.js +++ b/test/image/compare_pixels_test.js @@ -1,14 +1,14 @@ -var fs = require('fs'); +var fs = require("fs"); -var common = require('../../tasks/util/common'); -var getMockList = require('./assets/get_mock_list'); -var getRequestOpts = require('./assets/get_image_request_options'); -var getImagePaths = require('./assets/get_image_paths'); +var common = require("../../tasks/util/common"); +var getMockList = require("./assets/get_mock_list"); +var getRequestOpts = require("./assets/get_image_request_options"); +var getImagePaths = require("./assets/get_image_paths"); // packages inside the image server docker -var test = require('tape'); -var request = require('request'); -var gm = require('gm'); +var test = require("tape"); +var request = require("request"); +var gm = require("gm"); // pixel comparison tolerance var TOLERANCE = 1e-6; @@ -47,46 +47,42 @@ var QUEUE_WAIT = 10; * * npm run test-image -- gl3d_* --queue */ - var pattern = process.argv[2]; var mockList = getMockList(pattern); -var isInQueue = (process.argv[3] === '--queue'); +var isInQueue = process.argv[3] === "--queue"; var isCI = process.env.CIRCLECI; - -if(mockList.length === 0) { - throw new Error('No mocks found with pattern ' + pattern); +if (mockList.length === 0) { + throw new Error("No mocks found with pattern " + pattern); } // filter out untestable mocks if no pattern is specified -if(!pattern) { - console.log('Filtering out untestable mocks:'); - mockList = mockList.filter(untestableFilter); - console.log('\n'); +if (!pattern) { + console.log("Filtering out untestable mocks:"); + mockList = mockList.filter(untestableFilter); + console.log("\n"); } // gl2d have limited image-test support -if(pattern === 'gl2d_*') { - - if(!isInQueue) { - console.log('WARN: Running gl2d image tests in batch may lead to unwanted results\n'); - } +if (pattern === "gl2d_*") { + if (!isInQueue) { + console.log( + "WARN: Running gl2d image tests in batch may lead to unwanted results\n" + ); + } - if(isCI) { - console.log('Filtering out multiple-subplot gl2d mocks:'); - mockList = mockList - .filter(untestableGL2DonCIfilter) - .sort(sortForGL2DonCI); - console.log('\n'); - } + if (isCI) { + console.log("Filtering out multiple-subplot gl2d mocks:"); + mockList = mockList.filter(untestableGL2DonCIfilter).sort(sortForGL2DonCI); + console.log("\n"); + } } // main -if(isInQueue) { - runInQueue(mockList); -} -else { - runInBatch(mockList); +if (isInQueue) { + runInQueue(mockList); +} else { + runInBatch(mockList); } /* Test cases: @@ -100,15 +96,13 @@ else { * */ function untestableFilter(mockName) { - var cond = !( - mockName === 'font-wishlist' || - mockName.indexOf('gl2d_') !== -1 || - mockName.indexOf('mapbox_') !== -1 - ); + var cond = !(mockName === "font-wishlist" || + mockName.indexOf("gl2d_") !== -1 || + mockName.indexOf("mapbox_") !== -1); - if(!cond) console.log(' -', mockName); + if (!cond) console.log(" -", mockName); - return cond; + return cond; } /* gl2d mocks that have multiple subplots @@ -120,16 +114,17 @@ function untestableFilter(mockName) { * */ function untestableGL2DonCIfilter(mockName) { - var cond = [ - 'gl2d_multiple_subplots', - 'gl2d_simple_inset', - 'gl2d_stacked_coupled_subplots', - 'gl2d_stacked_subplots' - ].indexOf(mockName) === -1; + var cond = [ + "gl2d_multiple_subplots", + "gl2d_simple_inset", + "gl2d_stacked_coupled_subplots", + "gl2d_stacked_subplots" + ].indexOf(mockName) === + -1; - if(!cond) console.log(' -', mockName); + if (!cond) console.log(" -", mockName); - return cond; + return cond; } /* gl2d pointcloud mock(s) must be tested first @@ -145,82 +140,84 @@ function untestableGL2DonCIfilter(mockName) { * https://github.com/plotly/plotly.js/pull/1037 */ function sortForGL2DonCI(a, b) { - var root = 'gl2d_pointcloud', - ai = a.indexOf(root), - bi = b.indexOf(root); + var root = "gl2d_pointcloud", ai = a.indexOf(root), bi = b.indexOf(root); - if(ai < bi) return 1; - if(ai > bi) return -1; + if (ai < bi) return 1; + if (ai > bi) return -1; - return 0; + return 0; } function runInBatch(mockList) { - var running = 0; - - test('testing mocks in batch', function(t) { - t.plan(mockList.length); + var running = 0; - for(var i = 0; i < mockList.length; i++) { - run(mockList[i], t); - } - }); + test("testing mocks in batch", function(t) { + t.plan(mockList.length); - function run(mockName, t) { - if(running >= BATCH_SIZE) { - setTimeout(function() { - run(mockName, t); - }, BATCH_WAIT); - return; - } - running++; - - // throttle the number of tests running concurrently - - comparePixels(mockName, function(isEqual, mockName) { - running--; - t.ok(isEqual, mockName + ' should be pixel perfect'); - }); + for (var i = 0; i < mockList.length; i++) { + run(mockList[i], t); } + }); + + function run(mockName, t) { + if (running >= BATCH_SIZE) { + setTimeout( + function() { + run(mockName, t); + }, + BATCH_WAIT + ); + return; + } + running++; + + // throttle the number of tests running concurrently + comparePixels(mockName, function(isEqual, mockName) { + running--; + t.ok(isEqual, mockName + " should be pixel perfect"); + }); + } } function runInQueue(mockList) { - var index = 0; + var index = 0; - test('testing mocks in queue', function(t) { - t.plan(mockList.length); + test("testing mocks in queue", function(t) { + t.plan(mockList.length); - run(mockList[index], t); - }); + run(mockList[index], t); + }); - function run(mockName, t) { - comparePixels(mockName, function(isEqual, mockName) { - t.ok(isEqual, mockName + ' should be pixel perfect'); - - index++; - if(index < mockList.length) { - setTimeout(function() { - run(mockList[index], t); - }, QUEUE_WAIT); - } - }); - } + function run(mockName, t) { + comparePixels(mockName, function(isEqual, mockName) { + t.ok(isEqual, mockName + " should be pixel perfect"); + + index++; + if (index < mockList.length) { + setTimeout( + function() { + run(mockList[index], t); + }, + QUEUE_WAIT + ); + } + }); + } } function comparePixels(mockName, cb) { - var requestOpts = getRequestOpts({ mockName: mockName }), - imagePaths = getImagePaths(mockName), - saveImageStream = fs.createWriteStream(imagePaths.test); - - function checkImage() { - - // baseline image must be generated first - if(!common.doesFileExist(imagePaths.baseline)) { - var err = new Error('baseline image not found'); - return onEqualityCheck(err, false); - } + var requestOpts = getRequestOpts({ mockName: mockName }), + imagePaths = getImagePaths(mockName), + saveImageStream = fs.createWriteStream(imagePaths.test); + + function checkImage() { + // baseline image must be generated first + if (!common.doesFileExist(imagePaths.baseline)) { + var err = new Error("baseline image not found"); + return onEqualityCheck(err, false); + } - /* + /* * N.B. The non-zero tolerance was added in * https://github.com/plotly/plotly.js/pull/243 * where some legend mocks started generating different png outputs @@ -235,44 +232,38 @@ function comparePixels(mockName, cb) { * * Further investigation is needed. */ - - var gmOpts = { - file: imagePaths.diff, - highlightColor: 'purple', - tolerance: TOLERANCE - }; - - gm.compare( - imagePaths.test, - imagePaths.baseline, - gmOpts, - onEqualityCheck - ); + var gmOpts = { + file: imagePaths.diff, + highlightColor: "purple", + tolerance: TOLERANCE + }; + + gm.compare(imagePaths.test, imagePaths.baseline, gmOpts, onEqualityCheck); + } + + function onEqualityCheck(err, isEqual) { + if (err) { + common.touch(imagePaths.diff); + console.error(err); + return; } - - function onEqualityCheck(err, isEqual) { - if(err) { - common.touch(imagePaths.diff); - console.error(err); - return; - } - if(isEqual) { - fs.unlinkSync(imagePaths.diff); - } - - cb(isEqual, mockName); + if (isEqual) { + fs.unlinkSync(imagePaths.diff); } - // 525 means a plotly.js error - function onResponse(response) { - if(+response.statusCode === 525) { - console.error('plotly.js error while generating', mockName); - cb(false, mockName); - } + cb(isEqual, mockName); + } + + // 525 means a plotly.js error + function onResponse(response) { + if (+response.statusCode === 525) { + console.error("plotly.js error while generating", mockName); + cb(false, mockName); } + } - request(requestOpts) - .on('response', onResponse) - .pipe(saveImageStream) - .on('close', checkImage); + request(requestOpts) + .on("response", onResponse) + .pipe(saveImageStream) + .on("close", checkImage); } diff --git a/test/image/export_test.js b/test/image/export_test.js index 516178d851e..2f78eb2fb26 100644 --- a/test/image/export_test.js +++ b/test/image/export_test.js @@ -1,13 +1,13 @@ -var fs = require('fs'); -var sizeOf = require('image-size'); +var fs = require("fs"); +var sizeOf = require("image-size"); -var getMockList = require('./assets/get_mock_list'); -var getRequestOpts = require('./assets/get_image_request_options'); -var getImagePaths = require('./assets/get_image_paths'); +var getMockList = require("./assets/get_mock_list"); +var getRequestOpts = require("./assets/get_image_request_options"); +var getImagePaths = require("./assets/get_image_paths"); // packages inside the image server docker -var request = require('request'); -var test = require('tape'); +var request = require("request"); +var test = require("tape"); // image formats to test // @@ -15,12 +15,17 @@ var test = require('tape'); // // N.B. 'jpeg' and 'webp' lead to errors because of the image server code // is looking for Plotly.Color which isn't exposed anymore -var FORMATS = ['svg', 'pdf', 'eps']; +var FORMATS = ["svg", "pdf", "eps"]; // non-exhaustive list of mocks to test var DEFAULT_LIST = [ - '0', 'geo_first', 'gl3d_z-range', 'text_export', 'layout_image', 'gl2d_12', - 'range_slider_initial_valid' + "0", + "geo_first", + "gl3d_z-range", + "text_export", + "layout_image", + "gl2d_12", + "range_slider_initial_valid" ]; // return dimensions [in px] @@ -53,62 +58,58 @@ var MIN_SIZE = 100; * * npm run test-image -- gl3d_* */ - var pattern = process.argv[2]; var mockList = pattern ? getMockList(pattern) : DEFAULT_LIST; -if(mockList.length === 0) { - throw new Error('No mocks found with pattern ' + pattern); +if (mockList.length === 0) { + throw new Error("No mocks found with pattern " + pattern); } // main runInBatch(mockList); function runInBatch(mockList) { - test('testing image export formats', function(t) { - t.plan(mockList.length * FORMATS.length); - - // send all requests out at once - mockList.forEach(function(mockName) { - FORMATS.forEach(function(format) { - testExport(mockName, format, t); - }); - }); + test("testing image export formats", function(t) { + t.plan(mockList.length * FORMATS.length); + + // send all requests out at once + mockList.forEach(function(mockName) { + FORMATS.forEach(function(format) { + testExport(mockName, format, t); + }); }); + }); } // The tests below determine whether the images are properly // exported by (only) checking the file size of the generated images. function testExport(mockName, format, t) { - var specs = { - mockName: mockName, - format: format, - width: WIDTH, - height: HEIGHT - }; - - var requestOpts = getRequestOpts(specs), - imagePaths = getImagePaths(mockName, format), - saveImageStream = fs.createWriteStream(imagePaths.test); - - function checkExport(err) { - if(err) throw err; - - var didExport; - - if(format === 'svg') { - var dims = sizeOf(imagePaths.test); - didExport = (dims.width === WIDTH) && (dims.height === HEIGHT); - } - else { - var stats = fs.statSync(imagePaths.test); - didExport = stats.size > MIN_SIZE; - } - - t.ok(didExport, mockName + ' should be properly exported as a ' + format); + var specs = { + mockName: mockName, + format: format, + width: WIDTH, + height: HEIGHT + }; + + var requestOpts = getRequestOpts(specs), + imagePaths = getImagePaths(mockName, format), + saveImageStream = fs.createWriteStream(imagePaths.test); + + function checkExport(err) { + if (err) throw err; + + var didExport; + + if (format === "svg") { + var dims = sizeOf(imagePaths.test); + didExport = dims.width === WIDTH && dims.height === HEIGHT; + } else { + var stats = fs.statSync(imagePaths.test); + didExport = stats.size > MIN_SIZE; } - request(requestOpts) - .pipe(saveImageStream) - .on('close', checkExport); + t.ok(didExport, mockName + " should be properly exported as a " + format); + } + + request(requestOpts).pipe(saveImageStream).on("close", checkExport); } diff --git a/test/image/make_baseline.js b/test/image/make_baseline.js index fea40dfc082..97efa0948c1 100644 --- a/test/image/make_baseline.js +++ b/test/image/make_baseline.js @@ -1,11 +1,11 @@ -var fs = require('fs'); +var fs = require("fs"); -var getMockList = require('./assets/get_mock_list'); -var getRequestOpts = require('./assets/get_image_request_options'); -var getImagePaths = require('./assets/get_image_paths'); +var getMockList = require("./assets/get_mock_list"); +var getRequestOpts = require("./assets/get_image_request_options"); +var getImagePaths = require("./assets/get_image_paths"); // packages inside the image server docker -var request = require('request'); +var request = require("request"); // wait time between each baseline generation var QUEUE_WAIT = 10; @@ -37,45 +37,46 @@ var QUEUE_WAIT = 10; var pattern = process.argv[2]; var mockList = getMockList(pattern); -if(mockList.length === 0) { - throw new Error('No mocks found with pattern ' + pattern); +if (mockList.length === 0) { + throw new Error("No mocks found with pattern " + pattern); } // main runInQueue(mockList); function runInQueue(mockList) { - var index = 0; + var index = 0; - run(mockList[index]); + run(mockList[index]); - function run(mockName) { - makeBaseline(mockName, function() { - console.log('generated ' + mockName + ' successfully'); + function run(mockName) { + makeBaseline(mockName, function() { + console.log("generated " + mockName + " successfully"); - index++; - if(index < mockList.length) { - setTimeout(function() { - run(mockList[index]); - }, QUEUE_WAIT); - } - }); - } + index++; + if (index < mockList.length) { + setTimeout( + function() { + run(mockList[index]); + }, + QUEUE_WAIT + ); + } + }); + } } function makeBaseline(mockName, cb) { - var requestOpts = getRequestOpts({ mockName: mockName }), - imagePaths = getImagePaths(mockName), - saveImageStream = fs.createWriteStream(imagePaths.baseline); + var requestOpts = getRequestOpts({ mockName: mockName }), + imagePaths = getImagePaths(mockName), + saveImageStream = fs.createWriteStream(imagePaths.baseline); - function checkFormat(err, res) { - if(err) throw err; - if(res.headers['content-type'] !== 'image/png') { - throw new Error('Generated image is not a valid png'); - } + function checkFormat(err, res) { + if (err) throw err; + if (res.headers["content-type"] !== "image/png") { + throw new Error("Generated image is not a valid png"); } + } - request(requestOpts, checkFormat) - .pipe(saveImageStream) - .on('close', cb); + request(requestOpts, checkFormat).pipe(saveImageStream).on("close", cb); } diff --git a/test/image/strict-d3.js b/test/image/strict-d3.js index 28ba52c4e74..694261fd59a 100644 --- a/test/image/strict-d3.js +++ b/test/image/strict-d3.js @@ -5,70 +5,79 @@ /* global Plotly:false */ (function() { - 'use strict'; + "use strict"; + var selProto = Plotly.d3.selection.prototype; - var selProto = Plotly.d3.selection.prototype; + var originalSelStyle = selProto.style; - var originalSelStyle = selProto.style; - - selProto.style = function() { - var sel = this, - obj = arguments[0]; - - if(sel.size()) { - if(typeof obj === 'string') { - checkVal(obj, arguments[1]); - } - else { - Object.keys(obj).forEach(function(key) { checkVal(key, obj[key]); }); - } - } - - return originalSelStyle.apply(sel, arguments); - }; - - function checkVal(key, val) { - if(typeof val === 'string') { - // in case of multipart styles (stroke-dasharray, margins, etc) - // test each part separately - val.split(/[, ]/g).forEach(function(valPart) { - var pxSplit = valPart.length - 2; - if(valPart.substr(pxSplit) === 'px' && !isNumeric(valPart.substr(0, pxSplit))) { - throw new Error('d3 selection.style called with value: ' + val); - } - }); - } + selProto.style = function() { + var sel = this, obj = arguments[0]; + if (sel.size()) { + if (typeof obj === "string") { + checkVal(obj, arguments[1]); + } else { + Object.keys(obj).forEach(function(key) { + checkVal(key, obj[key]); + }); + } } - // below ripped from fast-isnumeric so I don't need to build this file + return originalSelStyle.apply(sel, arguments); + }; - function allBlankCharCodes(str) { - var l = str.length, - a; - for(var i = 0; i < l; i++) { - a = str.charCodeAt(i); - if((a < 9 || a > 13) && (a !== 32) && (a !== 133) && (a !== 160) && - (a !== 5760) && (a !== 6158) && (a < 8192 || a > 8205) && - (a !== 8232) && (a !== 8233) && (a !== 8239) && (a !== 8287) && - (a !== 8288) && (a !== 12288) && (a !== 65279)) { - return false; - } + function checkVal(key, val) { + if (typeof val === "string") { + // in case of multipart styles (stroke-dasharray, margins, etc) + // test each part separately + val.split(/[, ]/g).forEach(function(valPart) { + var pxSplit = valPart.length - 2; + if ( + valPart.substr(pxSplit) === "px" && + !isNumeric(valPart.substr(0, pxSplit)) + ) { + throw new Error("d3 selection.style called with value: " + val); } - return true; + }); } + } - function isNumeric(n) { - var type = typeof n; - if(type === 'string') { - var original = n; - n = +n; - // whitespace strings cast to zero - filter them out - if(n === 0 && allBlankCharCodes(original)) return false; - } - else if(type !== 'number') return false; - - return n - n < 1; + // below ripped from fast-isnumeric so I don't need to build this file + function allBlankCharCodes(str) { + var l = str.length, a; + for (var i = 0; i < l; i++) { + a = str.charCodeAt(i); + if ( + (a < 9 || a > 13) && + a !== 32 && + a !== 133 && + a !== 160 && + a !== 5760 && + a !== 6158 && + (a < 8192 || a > 8205) && + a !== 8232 && + a !== 8233 && + a !== 8239 && + a !== 8287 && + a !== 8288 && + a !== 12288 && + a !== 65279 + ) { + return false; + } } + return true; + } + + function isNumeric(n) { + var type = typeof n; + if (type === "string") { + var original = n; + n = +n; + // whitespace strings cast to zero - filter them out + if (n === 0 && allBlankCharCodes(original)) return false; + } else if (type !== "number") return false; + return n - n < 1; + } })(); diff --git a/test/jasmine/assets/assert_dims.js b/test/jasmine/assets/assert_dims.js index 2db7f297b4f..b0e1853dd4a 100644 --- a/test/jasmine/assets/assert_dims.js +++ b/test/jasmine/assets/assert_dims.js @@ -1,18 +1,21 @@ -'use strict'; - -var d3 = require('d3'); +"use strict"; +var d3 = require("d3"); module.exports = function assertDims(dims) { - var traces = d3.selectAll('.trace'); + var traces = d3.selectAll(".trace"); - expect(traces.size()) - .toEqual(dims.length, 'to have correct number of traces'); + expect(traces.size()).toEqual( + dims.length, + "to have correct number of traces" + ); - traces.each(function(_, i) { - var trace = d3.select(this); - var points = trace.selectAll('.point'); + traces.each(function(_, i) { + var trace = d3.select(this); + var points = trace.selectAll(".point"); - expect(points.size()) - .toEqual(dims[i], 'to have correct number of pts in trace ' + i); - }); + expect(points.size()).toEqual( + dims[i], + "to have correct number of pts in trace " + i + ); + }); }; diff --git a/test/jasmine/assets/assert_style.js b/test/jasmine/assets/assert_style.js index c6684da041e..5f176407d49 100644 --- a/test/jasmine/assets/assert_style.js +++ b/test/jasmine/assets/assert_style.js @@ -1,33 +1,39 @@ -'use strict'; - -var d3 = require('d3'); +"use strict"; +var d3 = require("d3"); module.exports = function assertStyle(dims, color, opacity) { - var N = dims.reduce(function(a, b) { - return a + b; - }); - - var traces = d3.selectAll('.trace'); - expect(traces.size()) - .toEqual(dims.length, 'to have correct number of traces'); - - expect(d3.selectAll('.point').size()) - .toEqual(N, 'to have correct total number of points'); - - traces.each(function(_, i) { - var trace = d3.select(this); - var points = trace.selectAll('.point'); - - expect(points.size()) - .toEqual(dims[i], 'to have correct number of pts in trace ' + i); - - points.each(function() { - var point = d3.select(this); - - expect(point.style('fill')) - .toEqual(color[i], 'to have correct pt color'); - expect(+point.style('opacity')) - .toEqual(opacity[i], 'to have correct pt opacity'); - }); + var N = dims.reduce(function(a, b) { + return a + b; + }); + + var traces = d3.selectAll(".trace"); + expect(traces.size()).toEqual( + dims.length, + "to have correct number of traces" + ); + + expect(d3.selectAll(".point").size()).toEqual( + N, + "to have correct total number of points" + ); + + traces.each(function(_, i) { + var trace = d3.select(this); + var points = trace.selectAll(".point"); + + expect(points.size()).toEqual( + dims[i], + "to have correct number of pts in trace " + i + ); + + points.each(function() { + var point = d3.select(this); + + expect(point.style("fill")).toEqual(color[i], "to have correct pt color"); + expect(+point.style("opacity")).toEqual( + opacity[i], + "to have correct pt opacity" + ); }); + }); }; diff --git a/test/jasmine/assets/click.js b/test/jasmine/assets/click.js index e2cc43e444b..ccf4952168c 100644 --- a/test/jasmine/assets/click.js +++ b/test/jasmine/assets/click.js @@ -1,7 +1,7 @@ -var mouseEvent = require('./mouse_event'); +var mouseEvent = require("./mouse_event"); module.exports = function click(x, y) { - mouseEvent('mousemove', x, y); - mouseEvent('mousedown', x, y); - mouseEvent('mouseup', x, y); + mouseEvent("mousemove", x, y); + mouseEvent("mousedown", x, y); + mouseEvent("mouseup", x, y); }; diff --git a/test/jasmine/assets/create_graph_div.js b/test/jasmine/assets/create_graph_div.js index 9791d46018c..e70a8994442 100644 --- a/test/jasmine/assets/create_graph_div.js +++ b/test/jasmine/assets/create_graph_div.js @@ -1,14 +1,13 @@ -'use strict'; - +"use strict"; module.exports = function createGraphDiv() { - var gd = document.createElement('div'); - gd.id = 'graph'; - document.body.appendChild(gd); + var gd = document.createElement("div"); + gd.id = "graph"; + document.body.appendChild(gd); - // force the graph to be at position 0,0 no matter what - gd.style.position = 'fixed'; - gd.style.left = 0; - gd.style.top = 0; + // force the graph to be at position 0,0 no matter what + gd.style.position = "fixed"; + gd.style.left = 0; + gd.style.top = 0; - return gd; + return gd; }; diff --git a/test/jasmine/assets/custom_matchers.js b/test/jasmine/assets/custom_matchers.js index 5aeb2de7764..ab867fc4523 100644 --- a/test/jasmine/assets/custom_matchers.js +++ b/test/jasmine/assets/custom_matchers.js @@ -1,159 +1,157 @@ -'use strict'; - -var isNumeric = require('fast-isnumeric'); -var Lib = require('@src/lib'); -var deepEqual = require('deep-equal'); +"use strict"; +var isNumeric = require("fast-isnumeric"); +var Lib = require("@src/lib"); +var deepEqual = require("deep-equal"); module.exports = { - // toEqual except with sparse arrays populated. This arises because: - // - // var x = new Array(2) - // expect(x).toEqual([undefined, undefined]) - // - // will fail assertion even though x[0] === undefined and x[1] === undefined. - // This is because the array elements don't exist until assigned. Of course it - // only fails on *some* platforms (old firefox, looking at you), which is why - // this is worth all the footwork. - toLooseDeepEqual: function() { - function populateUndefinedArrayEls(x) { - var i; - if(Array.isArray(x)) { - for(i = 0; i < x.length; i++) { - x[i] = x[i]; - } - } else if(Lib.isPlainObject(x)) { - var keys = Object.keys(x); - for(i = 0; i < keys.length; i++) { - populateUndefinedArrayEls(x[keys[i]]); - } - } - return x; + // toEqual except with sparse arrays populated. This arises because: + // + // var x = new Array(2) + // expect(x).toEqual([undefined, undefined]) + // + // will fail assertion even though x[0] === undefined and x[1] === undefined. + // This is because the array elements don't exist until assigned. Of course it + // only fails on *some* platforms (old firefox, looking at you), which is why + // this is worth all the footwork. + toLooseDeepEqual: function() { + function populateUndefinedArrayEls(x) { + var i; + if (Array.isArray(x)) { + for (i = 0; i < x.length; i++) { + x[i] = x[i]; } + } else if (Lib.isPlainObject(x)) { + var keys = Object.keys(x); + for (i = 0; i < keys.length; i++) { + populateUndefinedArrayEls(x[keys[i]]); + } + } + return x; + } - return { - compare: function(actual, expected, msgExtra) { - var actualExpanded = populateUndefinedArrayEls(Lib.extendDeep({}, actual)); - var expectedExpanded = populateUndefinedArrayEls(Lib.extendDeep({}, expected)); - - var passed = deepEqual(actualExpanded, expectedExpanded); - - var message = [ - 'Expected', JSON.stringify(actual), 'to be close to', JSON.stringify(expected), msgExtra - ].join(' '); - - return { - pass: passed, - message: message - }; - } - }; - }, - - // toBeCloseTo... but for arrays - toBeCloseToArray: function() { - return { - compare: function(actual, expected, precision, msgExtra) { - precision = coercePosition(precision); - - var tested = actual.map(function(element, i) { - return isClose(element, expected[i], precision); - }); - - var passed = ( - expected.length === actual.length && - tested.indexOf(false) < 0 - ); - - var message = [ - 'Expected', actual, 'to be close to', expected, msgExtra - ].join(' '); - - return { - pass: passed, - message: message - }; + return { + compare: function(actual, expected, msgExtra) { + var actualExpanded = populateUndefinedArrayEls( + Lib.extendDeep({}, actual) + ); + var expectedExpanded = populateUndefinedArrayEls( + Lib.extendDeep({}, expected) + ); + + var passed = deepEqual(actualExpanded, expectedExpanded); + + var message = [ + "Expected", + JSON.stringify(actual), + "to be close to", + JSON.stringify(expected), + msgExtra + ].join(" "); + + return { pass: passed, message: message }; + } + }; + }, + // toBeCloseTo... but for arrays + toBeCloseToArray: function() { + return { + compare: function(actual, expected, precision, msgExtra) { + precision = coercePosition(precision); + + var tested = actual.map(function(element, i) { + return isClose(element, expected[i], precision); + }); + + var passed = expected.length === actual.length && + tested.indexOf(false) < 0; + + var message = [ + "Expected", + actual, + "to be close to", + expected, + msgExtra + ].join(" "); + + return { pass: passed, message: message }; + } + }; + }, + // toBeCloseTo... but for 2D arrays + toBeCloseTo2DArray: function() { + return { + compare: function(actual, expected, precision, msgExtra) { + precision = coercePosition(precision); + + var passed = true; + + if (expected.length !== actual.length) { + passed = false; + } else { + for (var i = 0; i < expected.length; ++i) { + if (expected[i].length !== actual[i].length) { + passed = false; + break; } - }; - }, - - // toBeCloseTo... but for 2D arrays - toBeCloseTo2DArray: function() { - return { - compare: function(actual, expected, precision, msgExtra) { - precision = coercePosition(precision); - - var passed = true; - - if(expected.length !== actual.length) passed = false; - else { - for(var i = 0; i < expected.length; ++i) { - if(expected[i].length !== actual[i].length) { - passed = false; - break; - } - - for(var j = 0; j < expected[i].length; ++j) { - if(!isClose(actual[i][j], expected[i][j], precision)) { - passed = false; - break; - } - } - } - } - - var message = [ - 'Expected', - arrayToStr(actual.map(arrayToStr)), - 'to be close to', - arrayToStr(expected.map(arrayToStr)), - msgExtra - ].join(' '); - - return { - pass: passed, - message: message - }; - } - }; - }, - - toBeWithin: function() { - return { - compare: function(actual, expected, tolerance, msgExtra) { - var passed = Math.abs(actual - expected) < tolerance; - - var message = [ - 'Expected', actual, - 'to be close to', expected, - 'within', tolerance, - msgExtra - ].join(' '); - - return { - pass: passed, - message: message - }; + + for (var j = 0; j < expected[i].length; ++j) { + if (!isClose(actual[i][j], expected[i][j], precision)) { + passed = false; + break; + } } - }; - } + } + } + + var message = [ + "Expected", + arrayToStr(actual.map(arrayToStr)), + "to be close to", + arrayToStr(expected.map(arrayToStr)), + msgExtra + ].join(" "); + + return { pass: passed, message: message }; + } + }; + }, + toBeWithin: function() { + return { + compare: function(actual, expected, tolerance, msgExtra) { + var passed = Math.abs(actual - expected) < tolerance; + + var message = [ + "Expected", + actual, + "to be close to", + expected, + "within", + tolerance, + msgExtra + ].join(" "); + + return { pass: passed, message: message }; + } + }; + } }; function isClose(actual, expected, precision) { - if(isNumeric(actual) && isNumeric(expected)) { - return Math.abs(actual - expected) < precision; - } + if (isNumeric(actual) && isNumeric(expected)) { + return Math.abs(actual - expected) < precision; + } - return actual === expected; + return actual === expected; } function coercePosition(precision) { - if(precision !== 0) { - precision = Math.pow(10, -precision) / 2 || 0.005; - } + if (precision !== 0) { + precision = Math.pow(10, -precision) / 2 || 0.005; + } - return precision; + return precision; } function arrayToStr(array) { - return '[ ' + array.join(', ') + ' ]'; + return "[ " + array.join(", ") + " ]"; } diff --git a/test/jasmine/assets/delay.js b/test/jasmine/assets/delay.js index 8e57a7bf840..3bff754b8d0 100644 --- a/test/jasmine/assets/delay.js +++ b/test/jasmine/assets/delay.js @@ -1,5 +1,4 @@ -'use strict'; - +"use strict"; /** * This is a very quick and simple promise delayer. It's not full-featured * like the `delay` module. @@ -7,11 +6,14 @@ * Promise.resolve().then(delay(50)).then(...); */ module.exports = function delay(duration) { - return function(value) { - return new Promise(function(resolve) { - setTimeout(function() { - resolve(value); - }, duration || 0); - }); - }; + return function(value) { + return new Promise(function(resolve) { + setTimeout( + function() { + resolve(value); + }, + duration || 0 + ); + }); + }; }; diff --git a/test/jasmine/assets/destroy_graph_div.js b/test/jasmine/assets/destroy_graph_div.js index a1bd18b9741..f43bbc32c7a 100644 --- a/test/jasmine/assets/destroy_graph_div.js +++ b/test/jasmine/assets/destroy_graph_div.js @@ -1,7 +1,6 @@ -'use strict'; - +"use strict"; module.exports = function destroyGraphDiv() { - var gd = document.getElementById('graph'); + var gd = document.getElementById("graph"); - if(gd) document.body.removeChild(gd); + if (gd) document.body.removeChild(gd); }; diff --git a/test/jasmine/assets/double_click.js b/test/jasmine/assets/double_click.js index e92375588a8..e48697bd8f1 100644 --- a/test/jasmine/assets/double_click.js +++ b/test/jasmine/assets/double_click.js @@ -1,13 +1,21 @@ -var click = require('./click'); -var DBLCLICKDELAY = require('@src/plots/cartesian/constants').DBLCLICKDELAY; +var click = require("./click"); +var DBLCLICKDELAY = require("@src/plots/cartesian/constants").DBLCLICKDELAY; module.exports = function doubleClick(x, y) { - return new Promise(function(resolve) { - click(x, y); + return new Promise(function(resolve) { + click(x, y); - setTimeout(function() { - click(x, y); - setTimeout(function() { resolve(); }, DBLCLICKDELAY / 2); - }, DBLCLICKDELAY / 2); - }); + setTimeout( + function() { + click(x, y); + setTimeout( + function() { + resolve(); + }, + DBLCLICKDELAY / 2 + ); + }, + DBLCLICKDELAY / 2 + ); + }); }; diff --git a/test/jasmine/assets/fail_test.js b/test/jasmine/assets/fail_test.js index 32cb8a178f9..2695691c86e 100644 --- a/test/jasmine/assets/fail_test.js +++ b/test/jasmine/assets/fail_test.js @@ -1,5 +1,4 @@ -'use strict'; - +"use strict"; /** * Errors thrown in promise 'then'-s fail silently unless handled e.g. this way. A silent failure would probably * make at least some of the test assertions to be bypassed, i.e. a clean jasmine test run could result even if @@ -18,12 +17,12 @@ * See ./with_setup_teardown.js for a different example. */ module.exports = function failTest(error) { - if(error === undefined) { - expect(error).not.toBeUndefined(); - } else { - expect(error).toBeUndefined(); - } - if(error && error.stack) { - console.error(error.stack); - } + if (error === undefined) { + expect(error).not.toBeUndefined(); + } else { + expect(error).toBeUndefined(); + } + if (error && error.stack) { + console.error(error.stack); + } }; diff --git a/test/jasmine/assets/get_bbox.js b/test/jasmine/assets/get_bbox.js index 20aeb01d5f7..c8828cdb8bf 100644 --- a/test/jasmine/assets/get_bbox.js +++ b/test/jasmine/assets/get_bbox.js @@ -1,58 +1,55 @@ -'use strict'; - -var d3 = require('d3'); - -var ATTRS = ['x', 'y', 'width', 'height']; +"use strict"; +var d3 = require("d3"); +var ATTRS = ["x", "y", "width", "height"]; // In-house implementation of SVG getBBox that takes clip paths into account module.exports = function getBBox(element) { - var elementBBox = element.getBBox(); + var elementBBox = element.getBBox(); - var s = d3.select(element); - var clipPathAttr = s.attr('clip-path'); + var s = d3.select(element); + var clipPathAttr = s.attr("clip-path"); - if(!clipPathAttr) return elementBBox; + if (!clipPathAttr) return elementBBox; - // only supports 'url(#)' at the moment - var clipPathId = clipPathAttr.substring(5, clipPathAttr.length - 1); - var clipBBox = getClipBBox(clipPathId); + // only supports 'url(#)' at the moment + var clipPathId = clipPathAttr.substring(5, clipPathAttr.length - 1); + var clipBBox = getClipBBox(clipPathId); - return minBBox(elementBBox, clipBBox); + return minBBox(elementBBox, clipBBox); }; function getClipBBox(clipPathId) { - var clipPath = d3.select('#' + clipPathId); - var clipBBox; + var clipPath = d3.select("#" + clipPathId); + var clipBBox; - try { - // this line throws an error in FF (38 and 45 at least) - clipBBox = clipPath.node().getBBox(); - } - catch(e) { - // use DOM attributes as fallback - var path = d3.select(clipPath.node().firstChild); + try { + // this line throws an error in FF (38 and 45 at least) + clipBBox = clipPath.node().getBBox(); + } catch (e) { + // use DOM attributes as fallback + var path = d3.select(clipPath.node().firstChild); - clipBBox = {}; + clipBBox = {}; - ATTRS.forEach(function(attr) { - clipBBox[attr] = path.attr(attr); - }); - } + ATTRS.forEach(function(attr) { + clipBBox[attr] = path.attr(attr); + }); + } - return clipBBox; + return clipBBox; } function minBBox(bbox1, bbox2) { - var out = {}; + var out = {}; - function min(attr) { - return Math.min(bbox1[attr], bbox2[attr]); - } + function min(attr) { + return Math.min(bbox1[attr], bbox2[attr]); + } - ATTRS.forEach(function(attr) { - out[attr] = min(attr); - }); + ATTRS.forEach(function(attr) { + out[attr] = min(attr); + }); - return out; + return out; } diff --git a/test/jasmine/assets/get_rect_center.js b/test/jasmine/assets/get_rect_center.js index 51b5df0128d..b1838b9f7aa 100644 --- a/test/jasmine/assets/get_rect_center.js +++ b/test/jasmine/assets/get_rect_center.js @@ -1,6 +1,4 @@ -'use strict'; - - +"use strict"; /** * Get the screen coordinates of the center of * an SVG rectangle node. @@ -8,41 +6,40 @@ * @param {rect} rect svg node */ module.exports = function getRectCenter(rect) { - var corners = getRectScreenCoords(rect); + var corners = getRectScreenCoords(rect); - return [ - corners.nw.x + (corners.ne.x - corners.nw.x) / 2, - corners.ne.y + (corners.se.y - corners.ne.y) / 2 - ]; + return [ + corners.nw.x + (corners.ne.x - corners.nw.x) / 2, + corners.ne.y + (corners.se.y - corners.ne.y) / 2 + ]; }; // Taken from: http://stackoverflow.com/a/5835212/4068492 function getRectScreenCoords(rect) { - var svg = findParentSVG(rect); - var pt = svg.createSVGPoint(); - var corners = {}; - var matrix = rect.getScreenCTM(); - - pt.x = rect.x.animVal.value; - pt.y = rect.y.animVal.value; - corners.nw = pt.matrixTransform(matrix); - pt.x += rect.width.animVal.value; - corners.ne = pt.matrixTransform(matrix); - pt.y += rect.height.animVal.value; - corners.se = pt.matrixTransform(matrix); - pt.x -= rect.width.animVal.value; - corners.sw = pt.matrixTransform(matrix); - - return corners; + var svg = findParentSVG(rect); + var pt = svg.createSVGPoint(); + var corners = {}; + var matrix = rect.getScreenCTM(); + + pt.x = rect.x.animVal.value; + pt.y = rect.y.animVal.value; + corners.nw = pt.matrixTransform(matrix); + pt.x += rect.width.animVal.value; + corners.ne = pt.matrixTransform(matrix); + pt.y += rect.height.animVal.value; + corners.se = pt.matrixTransform(matrix); + pt.x -= rect.width.animVal.value; + corners.sw = pt.matrixTransform(matrix); + + return corners; } function findParentSVG(node) { - var parentNode = node.parentNode; + var parentNode = node.parentNode; - if(parentNode.tagName === 'svg') { - return parentNode; - } - else { - return findParentSVG(parentNode); - } + if (parentNode.tagName === "svg") { + return parentNode; + } else { + return findParentSVG(parentNode); + } } diff --git a/test/jasmine/assets/has_webgl_support.js b/test/jasmine/assets/has_webgl_support.js index c5702b53ff9..432b4293f99 100644 --- a/test/jasmine/assets/has_webgl_support.js +++ b/test/jasmine/assets/has_webgl_support.js @@ -1,18 +1,17 @@ -'use strict'; - -var getContext = require('webgl-context'); +"use strict"; +var getContext = require("webgl-context"); module.exports = function hasWebGLSupport(testName) { - var gl, canvas; + var gl, canvas; - canvas = document.createElement('canvas'); - gl = getContext({canvas: canvas}); + canvas = document.createElement("canvas"); + gl = getContext({ canvas: canvas }); - var hasSupport = !!gl; + var hasSupport = !!gl; - if(!hasSupport) { - console.warn('Cannot get WebGL context. Skip test *' + testName + '*'); - } + if (!hasSupport) { + console.warn("Cannot get WebGL context. Skip test *" + testName + "*"); + } - return hasSupport; + return hasSupport; }; diff --git a/test/jasmine/assets/hover.js b/test/jasmine/assets/hover.js index 0efa6fbb13f..960f4e5c5b7 100644 --- a/test/jasmine/assets/hover.js +++ b/test/jasmine/assets/hover.js @@ -1,5 +1,5 @@ -var mouseEvent = require('./mouse_event'); +var mouseEvent = require("./mouse_event"); module.exports = function hover(x, y) { - mouseEvent('mousemove', x, y); + mouseEvent("mousemove", x, y); }; diff --git a/test/jasmine/assets/jquery-1.8.3.min.js b/test/jasmine/assets/jquery-1.8.3.min.js index 38837795279..87ad90e478d 100644 --- a/test/jasmine/assets/jquery-1.8.3.min.js +++ b/test/jasmine/assets/jquery-1.8.3.min.js @@ -1,2 +1,4907 @@ /*! jQuery v1.8.3 jquery.com | jquery.org/license */ -(function(e,t){function _(e){var t=M[e]={};return v.each(e.split(y),function(e,n){t[n]=!0}),t}function H(e,n,r){if(r===t&&e.nodeType===1){var i="data-"+n.replace(P,"-$1").toLowerCase();r=e.getAttribute(i);if(typeof r=="string"){try{r=r==="true"?!0:r==="false"?!1:r==="null"?null:+r+""===r?+r:D.test(r)?v.parseJSON(r):r}catch(s){}v.data(e,n,r)}else r=t}return r}function B(e){var t;for(t in e){if(t==="data"&&v.isEmptyObject(e[t]))continue;if(t!=="toJSON")return!1}return!0}function et(){return!1}function tt(){return!0}function ut(e){return!e||!e.parentNode||e.parentNode.nodeType===11}function at(e,t){do e=e[t];while(e&&e.nodeType!==1);return e}function ft(e,t,n){t=t||0;if(v.isFunction(t))return v.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return v.grep(e,function(e,r){return e===t===n});if(typeof t=="string"){var r=v.grep(e,function(e){return e.nodeType===1});if(it.test(t))return v.filter(t,r,!n);t=v.filter(t,r)}return v.grep(e,function(e,r){return v.inArray(e,t)>=0===n})}function lt(e){var t=ct.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function At(e,t){if(t.nodeType!==1||!v.hasData(e))return;var n,r,i,s=v._data(e),o=v._data(t,s),u=s.events;if(u){delete o.handle,o.events={};for(n in u)for(r=0,i=u[n].length;r").appendTo(i.body),n=t.css("display");t.remove();if(n==="none"||n===""){Pt=i.body.appendChild(Pt||v.extend(i.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!Ht||!Pt.createElement)Ht=(Pt.contentWindow||Pt.contentDocument).document,Ht.write(""),Ht.close();t=Ht.body.appendChild(Ht.createElement(e)),n=Dt(t,"display"),i.body.removeChild(Pt)}return Wt[e]=n,n}function fn(e,t,n,r){var i;if(v.isArray(t))v.each(t,function(t,i){n||sn.test(e)?r(e,i):fn(e+"["+(typeof i=="object"?t:"")+"]",i,n,r)});else if(!n&&v.type(t)==="object")for(i in t)fn(e+"["+i+"]",t[i],n,r);else r(e,t)}function Cn(e){return function(t,n){typeof t!="string"&&(n=t,t="*");var r,i,s,o=t.toLowerCase().split(y),u=0,a=o.length;if(v.isFunction(n))for(;u)[^>]*$|#([\w\-]*)$)/,E=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,S=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,T=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,N=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,C=/^-ms-/,k=/-([\da-z])/gi,L=function(e,t){return(t+"").toUpperCase()},A=function(){i.addEventListener?(i.removeEventListener("DOMContentLoaded",A,!1),v.ready()):i.readyState==="complete"&&(i.detachEvent("onreadystatechange",A),v.ready())},O={};v.fn=v.prototype={constructor:v,init:function(e,n,r){var s,o,u,a;if(!e)return this;if(e.nodeType)return this.context=this[0]=e,this.length=1,this;if(typeof e=="string"){e.charAt(0)==="<"&&e.charAt(e.length-1)===">"&&e.length>=3?s=[null,e,null]:s=w.exec(e);if(s&&(s[1]||!n)){if(s[1])return n=n instanceof v?n[0]:n,a=n&&n.nodeType?n.ownerDocument||n:i,e=v.parseHTML(s[1],a,!0),E.test(s[1])&&v.isPlainObject(n)&&this.attr.call(e,n,!0),v.merge(this,e);o=i.getElementById(s[2]);if(o&&o.parentNode){if(o.id!==s[2])return r.find(e);this.length=1,this[0]=o}return this.context=i,this.selector=e,this}return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e)}return v.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),v.makeArray(e,this))},selector:"",jquery:"1.8.3",length:0,size:function(){return this.length},toArray:function(){return l.call(this)},get:function(e){return e==null?this.toArray():e<0?this[this.length+e]:this[e]},pushStack:function(e,t,n){var r=v.merge(this.constructor(),e);return r.prevObject=this,r.context=this.context,t==="find"?r.selector=this.selector+(this.selector?" ":"")+n:t&&(r.selector=this.selector+"."+t+"("+n+")"),r},each:function(e,t){return v.each(this,e,t)},ready:function(e){return v.ready.promise().done(e),this},eq:function(e){return e=+e,e===-1?this.slice(e):this.slice(e,e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(l.apply(this,arguments),"slice",l.call(arguments).join(","))},map:function(e){return this.pushStack(v.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:[].sort,splice:[].splice},v.fn.init.prototype=v.fn,v.extend=v.fn.extend=function(){var e,n,r,i,s,o,u=arguments[0]||{},a=1,f=arguments.length,l=!1;typeof u=="boolean"&&(l=u,u=arguments[1]||{},a=2),typeof u!="object"&&!v.isFunction(u)&&(u={}),f===a&&(u=this,--a);for(;a0)return;r.resolveWith(i,[v]),v.fn.trigger&&v(i).trigger("ready").off("ready")},isFunction:function(e){return v.type(e)==="function"},isArray:Array.isArray||function(e){return v.type(e)==="array"},isWindow:function(e){return e!=null&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return e==null?String(e):O[h.call(e)]||"object"},isPlainObject:function(e){if(!e||v.type(e)!=="object"||e.nodeType||v.isWindow(e))return!1;try{if(e.constructor&&!p.call(e,"constructor")&&!p.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||p.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw new Error(e)},parseHTML:function(e,t,n){var r;return!e||typeof e!="string"?null:(typeof t=="boolean"&&(n=t,t=0),t=t||i,(r=E.exec(e))?[t.createElement(r[1])]:(r=v.buildFragment([e],t,n?null:[]),v.merge([],(r.cacheable?v.clone(r.fragment):r.fragment).childNodes)))},parseJSON:function(t){if(!t||typeof t!="string")return null;t=v.trim(t);if(e.JSON&&e.JSON.parse)return e.JSON.parse(t);if(S.test(t.replace(T,"@").replace(N,"]").replace(x,"")))return(new Function("return "+t))();v.error("Invalid JSON: "+t)},parseXML:function(n){var r,i;if(!n||typeof n!="string")return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(s){r=t}return(!r||!r.documentElement||r.getElementsByTagName("parsererror").length)&&v.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&g.test(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(C,"ms-").replace(k,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,n,r){var i,s=0,o=e.length,u=o===t||v.isFunction(e);if(r){if(u){for(i in e)if(n.apply(e[i],r)===!1)break}else for(;s0&&e[0]&&e[a-1]||a===0||v.isArray(e));if(f)for(;u-1)a.splice(n,1),i&&(n<=o&&o--,n<=u&&u--)}),this},has:function(e){return v.inArray(e,a)>-1},empty:function(){return a=[],this},disable:function(){return a=f=n=t,this},disabled:function(){return!a},lock:function(){return f=t,n||c.disable(),this},locked:function(){return!f},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],a&&(!r||f)&&(i?f.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},v.extend({Deferred:function(e){var t=[["resolve","done",v.Callbacks("once memory"),"resolved"],["reject","fail",v.Callbacks("once memory"),"rejected"],["notify","progress",v.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return v.Deferred(function(n){v.each(t,function(t,r){var s=r[0],o=e[t];i[r[1]](v.isFunction(o)?function(){var e=o.apply(this,arguments);e&&v.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===i?n:this,[e])}:n[s])}),e=null}).promise()},promise:function(e){return e!=null?v.extend(e,r):r}},i={};return r.pipe=r.then,v.each(t,function(e,s){var o=s[2],u=s[3];r[s[1]]=o.add,u&&o.add(function(){n=u},t[e^1][2].disable,t[2][2].lock),i[s[0]]=o.fire,i[s[0]+"With"]=o.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=l.call(arguments),r=n.length,i=r!==1||e&&v.isFunction(e.promise)?r:0,s=i===1?e:v.Deferred(),o=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?l.call(arguments):r,n===u?s.notifyWith(t,n):--i||s.resolveWith(t,n)}},u,a,f;if(r>1){u=new Array(r),a=new Array(r),f=new Array(r);for(;t
a",n=p.getElementsByTagName("*"),r=p.getElementsByTagName("a")[0];if(!n||!r||!n.length)return{};s=i.createElement("select"),o=s.appendChild(i.createElement("option")),u=p.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:r.getAttribute("href")==="/a",opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:u.value==="on",optSelected:o.selected,getSetAttribute:p.className!=="t",enctype:!!i.createElement("form").enctype,html5Clone:i.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:i.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},u.checked=!0,t.noCloneChecked=u.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!o.disabled;try{delete p.test}catch(d){t.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",h=function(){t.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick"),p.detachEvent("onclick",h)),u=i.createElement("input"),u.value="t",u.setAttribute("type","radio"),t.radioValue=u.value==="t",u.setAttribute("checked","checked"),u.setAttribute("name","t"),p.appendChild(u),a=i.createDocumentFragment(),a.appendChild(p.lastChild),t.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,t.appendChecked=u.checked,a.removeChild(u),a.appendChild(p);if(p.attachEvent)for(l in{submit:!0,change:!0,focusin:!0})f="on"+l,c=f in p,c||(p.setAttribute(f,"return;"),c=typeof p[f]=="function"),t[l+"Bubbles"]=c;return v(function(){var n,r,s,o,u="padding:0;margin:0;border:0;display:block;overflow:hidden;",a=i.getElementsByTagName("body")[0];if(!a)return;n=i.createElement("div"),n.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",a.insertBefore(n,a.firstChild),r=i.createElement("div"),n.appendChild(r),r.innerHTML="
t
",s=r.getElementsByTagName("td"),s[0].style.cssText="padding:0;margin:0;border:0;display:none",c=s[0].offsetHeight===0,s[0].style.display="",s[1].style.display="none",t.reliableHiddenOffsets=c&&s[0].offsetHeight===0,r.innerHTML="",r.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=r.offsetWidth===4,t.doesNotIncludeMarginInBodyOffset=a.offsetTop!==1,e.getComputedStyle&&(t.pixelPosition=(e.getComputedStyle(r,null)||{}).top!=="1%",t.boxSizingReliable=(e.getComputedStyle(r,null)||{width:"4px"}).width==="4px",o=i.createElement("div"),o.style.cssText=r.style.cssText=u,o.style.marginRight=o.style.width="0",r.style.width="1px",r.appendChild(o),t.reliableMarginRight=!parseFloat((e.getComputedStyle(o,null)||{}).marginRight)),typeof r.style.zoom!="undefined"&&(r.innerHTML="",r.style.cssText=u+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=r.offsetWidth===3,r.style.display="block",r.style.overflow="visible",r.innerHTML="
",r.firstChild.style.width="5px",t.shrinkWrapBlocks=r.offsetWidth!==3,n.style.zoom=1),a.removeChild(n),n=r=s=o=null}),a.removeChild(p),n=r=s=o=u=a=p=null,t}();var D=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;v.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(v.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?v.cache[e[v.expando]]:e[v.expando],!!e&&!B(e)},data:function(e,n,r,i){if(!v.acceptData(e))return;var s,o,u=v.expando,a=typeof n=="string",f=e.nodeType,l=f?v.cache:e,c=f?e[u]:e[u]&&u;if((!c||!l[c]||!i&&!l[c].data)&&a&&r===t)return;c||(f?e[u]=c=v.deletedIds.pop()||v.guid++:c=u),l[c]||(l[c]={},f||(l[c].toJSON=v.noop));if(typeof n=="object"||typeof n=="function")i?l[c]=v.extend(l[c],n):l[c].data=v.extend(l[c].data,n);return s=l[c],i||(s.data||(s.data={}),s=s.data),r!==t&&(s[v.camelCase(n)]=r),a?(o=s[n],o==null&&(o=s[v.camelCase(n)])):o=s,o},removeData:function(e,t,n){if(!v.acceptData(e))return;var r,i,s,o=e.nodeType,u=o?v.cache:e,a=o?e[v.expando]:v.expando;if(!u[a])return;if(t){r=n?u[a]:u[a].data;if(r){v.isArray(t)||(t in r?t=[t]:(t=v.camelCase(t),t in r?t=[t]:t=t.split(" ")));for(i=0,s=t.length;i1,null,!1))},removeData:function(e){return this.each(function(){v.removeData(this,e)})}}),v.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=v._data(e,t),n&&(!r||v.isArray(n)?r=v._data(e,t,v.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=v.queue(e,t),r=n.length,i=n.shift(),s=v._queueHooks(e,t),o=function(){v.dequeue(e,t)};i==="inprogress"&&(i=n.shift(),r--),i&&(t==="fx"&&n.unshift("inprogress"),delete s.stop,i.call(e,o,s)),!r&&s&&s.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return v._data(e,n)||v._data(e,n,{empty:v.Callbacks("once memory").add(function(){v.removeData(e,t+"queue",!0),v.removeData(e,n,!0)})})}}),v.fn.extend({queue:function(e,n){var r=2;return typeof e!="string"&&(n=e,e="fx",r--),arguments.length1)},removeAttr:function(e){return this.each(function(){v.removeAttr(this,e)})},prop:function(e,t){return v.access(this,v.prop,e,t,arguments.length>1)},removeProp:function(e){return e=v.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,s,o,u;if(v.isFunction(e))return this.each(function(t){v(this).addClass(e.call(this,t,this.className))});if(e&&typeof e=="string"){t=e.split(y);for(n=0,r=this.length;n=0)r=r.replace(" "+n[s]+" "," ");i.className=e?v.trim(r):""}}}return this},toggleClass:function(e,t){var n=typeof e,r=typeof t=="boolean";return v.isFunction(e)?this.each(function(n){v(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if(n==="string"){var i,s=0,o=v(this),u=t,a=e.split(y);while(i=a[s++])u=r?u:!o.hasClass(i),o[u?"addClass":"removeClass"](i)}else if(n==="undefined"||n==="boolean")this.className&&v._data(this,"__className__",this.className),this.className=this.className||e===!1?"":v._data(this,"__className__")||""})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;n=0)return!0;return!1},val:function(e){var n,r,i,s=this[0];if(!arguments.length){if(s)return n=v.valHooks[s.type]||v.valHooks[s.nodeName.toLowerCase()],n&&"get"in n&&(r=n.get(s,"value"))!==t?r:(r=s.value,typeof r=="string"?r.replace(R,""):r==null?"":r);return}return i=v.isFunction(e),this.each(function(r){var s,o=v(this);if(this.nodeType!==1)return;i?s=e.call(this,r,o.val()):s=e,s==null?s="":typeof s=="number"?s+="":v.isArray(s)&&(s=v.map(s,function(e){return e==null?"":e+""})),n=v.valHooks[this.type]||v.valHooks[this.nodeName.toLowerCase()];if(!n||!("set"in n)||n.set(this,s,"value")===t)this.value=s})}}),v.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,s=e.type==="select-one"||i<0,o=s?null:[],u=s?i+1:r.length,a=i<0?u:s?i:0;for(;a=0}),n.length||(e.selectedIndex=-1),n}}},attrFn:{},attr:function(e,n,r,i){var s,o,u,a=e.nodeType;if(!e||a===3||a===8||a===2)return;if(i&&v.isFunction(v.fn[n]))return v(e)[n](r);if(typeof e.getAttribute=="undefined")return v.prop(e,n,r);u=a!==1||!v.isXMLDoc(e),u&&(n=n.toLowerCase(),o=v.attrHooks[n]||(X.test(n)?F:j));if(r!==t){if(r===null){v.removeAttr(e,n);return}return o&&"set"in o&&u&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r)}return o&&"get"in o&&u&&(s=o.get(e,n))!==null?s:(s=e.getAttribute(n),s===null?t:s)},removeAttr:function(e,t){var n,r,i,s,o=0;if(t&&e.nodeType===1){r=t.split(y);for(;o=0}})});var $=/^(?:textarea|input|select)$/i,J=/^([^\.]*|)(?:\.(.+)|)$/,K=/(?:^|\s)hover(\.\S+|)\b/,Q=/^key/,G=/^(?:mouse|contextmenu)|click/,Y=/^(?:focusinfocus|focusoutblur)$/,Z=function(e){return v.event.special.hover?e:e.replace(K,"mouseenter$1 mouseleave$1")};v.event={add:function(e,n,r,i,s){var o,u,a,f,l,c,h,p,d,m,g;if(e.nodeType===3||e.nodeType===8||!n||!r||!(o=v._data(e)))return;r.handler&&(d=r,r=d.handler,s=d.selector),r.guid||(r.guid=v.guid++),a=o.events,a||(o.events=a={}),u=o.handle,u||(o.handle=u=function(e){return typeof v=="undefined"||!!e&&v.event.triggered===e.type?t:v.event.dispatch.apply(u.elem,arguments)},u.elem=e),n=v.trim(Z(n)).split(" ");for(f=0;f=0&&(y=y.slice(0,-1),a=!0),y.indexOf(".")>=0&&(b=y.split("."),y=b.shift(),b.sort());if((!s||v.event.customEvent[y])&&!v.event.global[y])return;n=typeof n=="object"?n[v.expando]?n:new v.Event(y,n):new v.Event(y),n.type=y,n.isTrigger=!0,n.exclusive=a,n.namespace=b.join("."),n.namespace_re=n.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,h=y.indexOf(":")<0?"on"+y:"";if(!s){u=v.cache;for(f in u)u[f].events&&u[f].events[y]&&v.event.trigger(n,r,u[f].handle.elem,!0);return}n.result=t,n.target||(n.target=s),r=r!=null?v.makeArray(r):[],r.unshift(n),p=v.event.special[y]||{};if(p.trigger&&p.trigger.apply(s,r)===!1)return;m=[[s,p.bindType||y]];if(!o&&!p.noBubble&&!v.isWindow(s)){g=p.delegateType||y,l=Y.test(g+y)?s:s.parentNode;for(c=s;l;l=l.parentNode)m.push([l,g]),c=l;c===(s.ownerDocument||i)&&m.push([c.defaultView||c.parentWindow||e,g])}for(f=0;f=0:v.find(h,this,null,[s]).length),u[h]&&f.push(c);f.length&&w.push({elem:s,matches:f})}d.length>m&&w.push({elem:this,matches:d.slice(m)});for(r=0;r0?this.on(t,null,e,n):this.trigger(t)},Q.test(t)&&(v.event.fixHooks[t]=v.event.keyHooks),G.test(t)&&(v.event.fixHooks[t]=v.event.mouseHooks)}),function(e,t){function nt(e,t,n,r){n=n||[],t=t||g;var i,s,a,f,l=t.nodeType;if(!e||typeof e!="string")return n;if(l!==1&&l!==9)return[];a=o(t);if(!a&&!r)if(i=R.exec(e))if(f=i[1]){if(l===9){s=t.getElementById(f);if(!s||!s.parentNode)return n;if(s.id===f)return n.push(s),n}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(f))&&u(t,s)&&s.id===f)return n.push(s),n}else{if(i[2])return S.apply(n,x.call(t.getElementsByTagName(e),0)),n;if((f=i[3])&&Z&&t.getElementsByClassName)return S.apply(n,x.call(t.getElementsByClassName(f),0)),n}return vt(e.replace(j,"$1"),t,n,r,a)}function rt(e){return function(t){var n=t.nodeName.toLowerCase();return n==="input"&&t.type===e}}function it(e){return function(t){var n=t.nodeName.toLowerCase();return(n==="input"||n==="button")&&t.type===e}}function st(e){return N(function(t){return t=+t,N(function(n,r){var i,s=e([],n.length,t),o=s.length;while(o--)n[i=s[o]]&&(n[i]=!(r[i]=n[i]))})})}function ot(e,t,n){if(e===t)return n;var r=e.nextSibling;while(r){if(r===t)return-1;r=r.nextSibling}return 1}function ut(e,t){var n,r,s,o,u,a,f,l=L[d][e+" "];if(l)return t?0:l.slice(0);u=e,a=[],f=i.preFilter;while(u){if(!n||(r=F.exec(u)))r&&(u=u.slice(r[0].length)||u),a.push(s=[]);n=!1;if(r=I.exec(u))s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=r[0].replace(j," ");for(o in i.filter)(r=J[o].exec(u))&&(!f[o]||(r=f[o](r)))&&(s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=o,n.matches=r);if(!n)break}return t?u.length:u?nt.error(e):L(e,a).slice(0)}function at(e,t,r){var i=t.dir,s=r&&t.dir==="parentNode",o=w++;return t.first?function(t,n,r){while(t=t[i])if(s||t.nodeType===1)return e(t,n,r)}:function(t,r,u){if(!u){var a,f=b+" "+o+" ",l=f+n;while(t=t[i])if(s||t.nodeType===1){if((a=t[d])===l)return t.sizset;if(typeof a=="string"&&a.indexOf(f)===0){if(t.sizset)return t}else{t[d]=l;if(e(t,r,u))return t.sizset=!0,t;t.sizset=!1}}}else while(t=t[i])if(s||t.nodeType===1)if(e(t,r,u))return t}}function ft(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function lt(e,t,n,r,i){var s,o=[],u=0,a=e.length,f=t!=null;for(;u-1&&(s[f]=!(o[f]=c))}}else g=lt(g===o?g.splice(d,g.length):g),i?i(null,o,g,a):S.apply(o,g)})}function ht(e){var t,n,r,s=e.length,o=i.relative[e[0].type],u=o||i.relative[" "],a=o?1:0,f=at(function(e){return e===t},u,!0),l=at(function(e){return T.call(t,e)>-1},u,!0),h=[function(e,n,r){return!o&&(r||n!==c)||((t=n).nodeType?f(e,n,r):l(e,n,r))}];for(;a1&&ft(h),a>1&&e.slice(0,a-1).join("").replace(j,"$1"),n,a0,s=e.length>0,o=function(u,a,f,l,h){var p,d,v,m=[],y=0,w="0",x=u&&[],T=h!=null,N=c,C=u||s&&i.find.TAG("*",h&&a.parentNode||a),k=b+=N==null?1:Math.E;T&&(c=a!==g&&a,n=o.el);for(;(p=C[w])!=null;w++){if(s&&p){for(d=0;v=e[d];d++)if(v(p,a,f)){l.push(p);break}T&&(b=k,n=++o.el)}r&&((p=!v&&p)&&y--,u&&x.push(p))}y+=w;if(r&&w!==y){for(d=0;v=t[d];d++)v(x,m,a,f);if(u){if(y>0)while(w--)!x[w]&&!m[w]&&(m[w]=E.call(l));m=lt(m)}S.apply(l,m),T&&!u&&m.length>0&&y+t.length>1&&nt.uniqueSort(l)}return T&&(b=k,c=N),x};return o.el=0,r?N(o):o}function dt(e,t,n){var r=0,i=t.length;for(;r2&&(f=u[0]).type==="ID"&&t.nodeType===9&&!s&&i.relative[u[1].type]){t=i.find.ID(f.matches[0].replace($,""),t,s)[0];if(!t)return n;e=e.slice(u.shift().length)}for(o=J.POS.test(e)?-1:u.length-1;o>=0;o--){f=u[o];if(i.relative[l=f.type])break;if(c=i.find[l])if(r=c(f.matches[0].replace($,""),z.test(u[0].type)&&t.parentNode||t,s)){u.splice(o,1),e=r.length&&u.join("");if(!e)return S.apply(n,x.call(r,0)),n;break}}}return a(e,h)(r,t,s,n,z.test(e)),n}function mt(){}var n,r,i,s,o,u,a,f,l,c,h=!0,p="undefined",d=("sizcache"+Math.random()).replace(".",""),m=String,g=e.document,y=g.documentElement,b=0,w=0,E=[].pop,S=[].push,x=[].slice,T=[].indexOf||function(e){var t=0,n=this.length;for(;ti.cacheLength&&delete e[t.shift()],e[n+" "]=r},e)},k=C(),L=C(),A=C(),O="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",_=M.replace("w","w#"),D="([*^$|!~]?=)",P="\\["+O+"*("+M+")"+O+"*(?:"+D+O+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+_+")|)|)"+O+"*\\]",H=":("+M+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+P+")|[^:]|\\\\.)*|.*))\\)|)",B=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)",j=new RegExp("^"+O+"+|((?:^|[^\\\\])(?:\\\\.)*)"+O+"+$","g"),F=new RegExp("^"+O+"*,"+O+"*"),I=new RegExp("^"+O+"*([\\x20\\t\\r\\n\\f>+~])"+O+"*"),q=new RegExp(H),R=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,U=/^:not/,z=/[\x20\t\r\n\f]*[+~]/,W=/:not\($/,X=/h\d/i,V=/input|select|textarea|button/i,$=/\\(?!\\)/g,J={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),NAME:new RegExp("^\\[name=['\"]?("+M+")['\"]?\\]"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+H),POS:new RegExp(B,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),needsContext:new RegExp("^"+O+"*[>+~]|"+B,"i")},K=function(e){var t=g.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}},Q=K(function(e){return e.appendChild(g.createComment("")),!e.getElementsByTagName("*").length}),G=K(function(e){return e.innerHTML="",e.firstChild&&typeof e.firstChild.getAttribute!==p&&e.firstChild.getAttribute("href")==="#"}),Y=K(function(e){e.innerHTML="";var t=typeof e.lastChild.getAttribute("multiple");return t!=="boolean"&&t!=="string"}),Z=K(function(e){return e.innerHTML="",!e.getElementsByClassName||!e.getElementsByClassName("e").length?!1:(e.lastChild.className="e",e.getElementsByClassName("e").length===2)}),et=K(function(e){e.id=d+0,e.innerHTML="
",y.insertBefore(e,y.firstChild);var t=g.getElementsByName&&g.getElementsByName(d).length===2+g.getElementsByName(d+0).length;return r=!g.getElementById(d),y.removeChild(e),t});try{x.call(y.childNodes,0)[0].nodeType}catch(tt){x=function(e){var t,n=[];for(;t=this[e];e++)n.push(t);return n}}nt.matches=function(e,t){return nt(e,null,null,t)},nt.matchesSelector=function(e,t){return nt(t,null,null,[e]).length>0},s=nt.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(i===1||i===9||i===11){if(typeof e.textContent=="string")return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=s(e)}else if(i===3||i===4)return e.nodeValue}else for(;t=e[r];r++)n+=s(t);return n},o=nt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?t.nodeName!=="HTML":!1},u=nt.contains=y.contains?function(e,t){var n=e.nodeType===9?e.documentElement:e,r=t&&t.parentNode;return e===r||!!(r&&r.nodeType===1&&n.contains&&n.contains(r))}:y.compareDocumentPosition?function(e,t){return t&&!!(e.compareDocumentPosition(t)&16)}:function(e,t){while(t=t.parentNode)if(t===e)return!0;return!1},nt.attr=function(e,t){var n,r=o(e);return r||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):r||Y?e.getAttribute(t):(n=e.getAttributeNode(t),n?typeof e[t]=="boolean"?e[t]?t:null:n.specified?n.value:null:null)},i=nt.selectors={cacheLength:50,createPseudo:N,match:J,attrHandle:G?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},find:{ID:r?function(e,t,n){if(typeof t.getElementById!==p&&!n){var r=t.getElementById(e);return r&&r.parentNode?[r]:[]}}:function(e,n,r){if(typeof n.getElementById!==p&&!r){var i=n.getElementById(e);return i?i.id===e||typeof i.getAttributeNode!==p&&i.getAttributeNode("id").value===e?[i]:t:[]}},TAG:Q?function(e,t){if(typeof t.getElementsByTagName!==p)return t.getElementsByTagName(e)}:function(e,t){var n=t.getElementsByTagName(e);if(e==="*"){var r,i=[],s=0;for(;r=n[s];s++)r.nodeType===1&&i.push(r);return i}return n},NAME:et&&function(e,t){if(typeof t.getElementsByName!==p)return t.getElementsByName(name)},CLASS:Z&&function(e,t,n){if(typeof t.getElementsByClassName!==p&&!n)return t.getElementsByClassName(e)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace($,""),e[3]=(e[4]||e[5]||"").replace($,""),e[2]==="~="&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),e[1]==="nth"?(e[2]||nt.error(e[0]),e[3]=+(e[3]?e[4]+(e[5]||1):2*(e[2]==="even"||e[2]==="odd")),e[4]=+(e[6]+e[7]||e[2]==="odd")):e[2]&&nt.error(e[0]),e},PSEUDO:function(e){var t,n;if(J.CHILD.test(e[0]))return null;if(e[3])e[2]=e[3];else if(t=e[4])q.test(t)&&(n=ut(t,!0))&&(n=t.indexOf(")",t.length-n)-t.length)&&(t=t.slice(0,n),e[0]=e[0].slice(0,n)),e[2]=t;return e.slice(0,3)}},filter:{ID:r?function(e){return e=e.replace($,""),function(t){return t.getAttribute("id")===e}}:function(e){return e=e.replace($,""),function(t){var n=typeof t.getAttributeNode!==p&&t.getAttributeNode("id");return n&&n.value===e}},TAG:function(e){return e==="*"?function(){return!0}:(e=e.replace($,"").toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[d][e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==p&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r,i){var s=nt.attr(r,e);return s==null?t==="!=":t?(s+="",t==="="?s===n:t==="!="?s!==n:t==="^="?n&&s.indexOf(n)===0:t==="*="?n&&s.indexOf(n)>-1:t==="$="?n&&s.substr(s.length-n.length)===n:t==="~="?(" "+s+" ").indexOf(n)>-1:t==="|="?s===n||s.substr(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r){return e==="nth"?function(e){var t,i,s=e.parentNode;if(n===1&&r===0)return!0;if(s){i=0;for(t=s.firstChild;t;t=t.nextSibling)if(t.nodeType===1){i++;if(e===t)break}}return i-=r,i===n||i%n===0&&i/n>=0}:function(t){var n=t;switch(e){case"only":case"first":while(n=n.previousSibling)if(n.nodeType===1)return!1;if(e==="first")return!0;n=t;case"last":while(n=n.nextSibling)if(n.nodeType===1)return!1;return!0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||nt.error("unsupported pseudo: "+e);return r[d]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?N(function(e,n){var i,s=r(e,t),o=s.length;while(o--)i=T.call(e,s[o]),e[i]=!(n[i]=s[o])}):function(e){return r(e,0,n)}):r}},pseudos:{not:N(function(e){var t=[],n=[],r=a(e.replace(j,"$1"));return r[d]?N(function(e,t,n,i){var s,o=r(e,null,i,[]),u=e.length;while(u--)if(s=o[u])e[u]=!(t[u]=s)}):function(e,i,s){return t[0]=e,r(t,null,s,n),!n.pop()}}),has:N(function(e){return function(t){return nt(e,t).length>0}}),contains:N(function(e){return function(t){return(t.textContent||t.innerText||s(t)).indexOf(e)>-1}}),enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&!!e.checked||t==="option"&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},parent:function(e){return!i.pseudos.empty(e)},empty:function(e){var t;e=e.firstChild;while(e){if(e.nodeName>"@"||(t=e.nodeType)===3||t===4)return!1;e=e.nextSibling}return!0},header:function(e){return X.test(e.nodeName)},text:function(e){var t,n;return e.nodeName.toLowerCase()==="input"&&(t=e.type)==="text"&&((n=e.getAttribute("type"))==null||n.toLowerCase()===t)},radio:rt("radio"),checkbox:rt("checkbox"),file:rt("file"),password:rt("password"),image:rt("image"),submit:it("submit"),reset:it("reset"),button:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&e.type==="button"||t==="button"},input:function(e){return V.test(e.nodeName)},focus:function(e){var t=e.ownerDocument;return e===t.activeElement&&(!t.hasFocus||t.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},active:function(e){return e===e.ownerDocument.activeElement},first:st(function(){return[0]}),last:st(function(e,t){return[t-1]}),eq:st(function(e,t,n){return[n<0?n+t:n]}),even:st(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:st(function(e,t,n){for(var r=n<0?n+t:n;++r",e.querySelectorAll("[selected]").length||i.push("\\["+O+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||i.push(":checked")}),K(function(e){e.innerHTML="

",e.querySelectorAll("[test^='']").length&&i.push("[*^$]="+O+"*(?:\"\"|'')"),e.innerHTML="",e.querySelectorAll(":enabled").length||i.push(":enabled",":disabled")}),i=new RegExp(i.join("|")),vt=function(e,r,s,o,u){if(!o&&!u&&!i.test(e)){var a,f,l=!0,c=d,h=r,p=r.nodeType===9&&e;if(r.nodeType===1&&r.nodeName.toLowerCase()!=="object"){a=ut(e),(l=r.getAttribute("id"))?c=l.replace(n,"\\$&"):r.setAttribute("id",c),c="[id='"+c+"'] ",f=a.length;while(f--)a[f]=c+a[f].join("");h=z.test(e)&&r.parentNode||r,p=a.join(",")}if(p)try{return S.apply(s,x.call(h.querySelectorAll(p),0)),s}catch(v){}finally{l||r.removeAttribute("id")}}return t(e,r,s,o,u)},u&&(K(function(t){e=u.call(t,"div");try{u.call(t,"[test!='']:sizzle"),s.push("!=",H)}catch(n){}}),s=new RegExp(s.join("|")),nt.matchesSelector=function(t,n){n=n.replace(r,"='$1']");if(!o(t)&&!s.test(n)&&!i.test(n))try{var a=u.call(t,n);if(a||e||t.document&&t.document.nodeType!==11)return a}catch(f){}return nt(n,null,null,[t]).length>0})}(),i.pseudos.nth=i.pseudos.eq,i.filters=mt.prototype=i.pseudos,i.setFilters=new mt,nt.attr=v.attr,v.find=nt,v.expr=nt.selectors,v.expr[":"]=v.expr.pseudos,v.unique=nt.uniqueSort,v.text=nt.getText,v.isXMLDoc=nt.isXML,v.contains=nt.contains}(e);var nt=/Until$/,rt=/^(?:parents|prev(?:Until|All))/,it=/^.[^:#\[\.,]*$/,st=v.expr.match.needsContext,ot={children:!0,contents:!0,next:!0,prev:!0};v.fn.extend({find:function(e){var t,n,r,i,s,o,u=this;if(typeof e!="string")return v(e).filter(function(){for(t=0,n=u.length;t0)for(i=r;i=0:v.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,s=[],o=st.test(e)||typeof e!="string"?v(e,t||this.context):0;for(;r-1:v.find.matchesSelector(n,e)){s.push(n);break}n=n.parentNode}}return s=s.length>1?v.unique(s):s,this.pushStack(s,"closest",e)},index:function(e){return e?typeof e=="string"?v.inArray(this[0],v(e)):v.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(e,t){var n=typeof e=="string"?v(e,t):v.makeArray(e&&e.nodeType?[e]:e),r=v.merge(this.get(),n);return this.pushStack(ut(n[0])||ut(r[0])?r:v.unique(r))},addBack:function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}}),v.fn.andSelf=v.fn.addBack,v.each({parent:function(e){var t=e.parentNode;return t&&t.nodeType!==11?t:null},parents:function(e){return v.dir(e,"parentNode")},parentsUntil:function(e,t,n){return v.dir(e,"parentNode",n)},next:function(e){return at(e,"nextSibling")},prev:function(e){return at(e,"previousSibling")},nextAll:function(e){return v.dir(e,"nextSibling")},prevAll:function(e){return v.dir(e,"previousSibling")},nextUntil:function(e,t,n){return v.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return v.dir(e,"previousSibling",n)},siblings:function(e){return v.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return v.sibling(e.firstChild)},contents:function(e){return v.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:v.merge([],e.childNodes)}},function(e,t){v.fn[e]=function(n,r){var i=v.map(this,t,n);return nt.test(e)||(r=n),r&&typeof r=="string"&&(i=v.filter(r,i)),i=this.length>1&&!ot[e]?v.unique(i):i,this.length>1&&rt.test(e)&&(i=i.reverse()),this.pushStack(i,e,l.call(arguments).join(","))}}),v.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),t.length===1?v.find.matchesSelector(t[0],e)?[t[0]]:[]:v.find.matches(e,t)},dir:function(e,n,r){var i=[],s=e[n];while(s&&s.nodeType!==9&&(r===t||s.nodeType!==1||!v(s).is(r)))s.nodeType===1&&i.push(s),s=s[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)e.nodeType===1&&e!==t&&n.push(e);return n}});var ct="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",ht=/ jQuery\d+="(?:null|\d+)"/g,pt=/^\s+/,dt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,vt=/<([\w:]+)/,mt=/]","i"),Et=/^(?:checkbox|radio)$/,St=/checked\s*(?:[^=]|=\s*.checked.)/i,xt=/\/(java|ecma)script/i,Tt=/^\s*\s*$/g,Nt={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},Ct=lt(i),kt=Ct.appendChild(i.createElement("div"));Nt.optgroup=Nt.option,Nt.tbody=Nt.tfoot=Nt.colgroup=Nt.caption=Nt.thead,Nt.th=Nt.td,v.support.htmlSerialize||(Nt._default=[1,"X
","
"]),v.fn.extend({text:function(e){return v.access(this,function(e){return e===t?v.text(this):this.empty().append((this[0]&&this[0].ownerDocument||i).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(v.isFunction(e))return this.each(function(t){v(this).wrapAll(e.call(this,t))});if(this[0]){var t=v(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&e.firstChild.nodeType===1)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return v.isFunction(e)?this.each(function(t){v(this).wrapInner(e.call(this,t))}):this.each(function(){var t=v(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=v.isFunction(e);return this.each(function(n){v(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){v.nodeName(this,"body")||v(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(e,this.firstChild)})},before:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(e,this),"before",this.selector)}},after:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this.nextSibling)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(this,e),"after",this.selector)}},remove:function(e,t){var n,r=0;for(;(n=this[r])!=null;r++)if(!e||v.filter(e,[n]).length)!t&&n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),v.cleanData([n])),n.parentNode&&n.parentNode.removeChild(n);return this},empty:function(){var e,t=0;for(;(e=this[t])!=null;t++){e.nodeType===1&&v.cleanData(e.getElementsByTagName("*"));while(e.firstChild)e.removeChild(e.firstChild)}return this},clone:function(e,t){return e=e==null?!1:e,t=t==null?e:t,this.map(function(){return v.clone(this,e,t)})},html:function(e){return v.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return n.nodeType===1?n.innerHTML.replace(ht,""):t;if(typeof e=="string"&&!yt.test(e)&&(v.support.htmlSerialize||!wt.test(e))&&(v.support.leadingWhitespace||!pt.test(e))&&!Nt[(vt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(dt,"<$1>");try{for(;r1&&typeof f=="string"&&St.test(f))return this.each(function(){v(this).domManip(e,n,r)});if(v.isFunction(f))return this.each(function(i){var s=v(this);e[0]=f.call(this,i,n?s.html():t),s.domManip(e,n,r)});if(this[0]){i=v.buildFragment(e,this,l),o=i.fragment,s=o.firstChild,o.childNodes.length===1&&(o=s);if(s){n=n&&v.nodeName(s,"tr");for(u=i.cacheable||c-1;a0?this.clone(!0):this).get(),v(o[i])[t](r),s=s.concat(r);return this.pushStack(s,e,o.selector)}}),v.extend({clone:function(e,t,n){var r,i,s,o;v.support.html5Clone||v.isXMLDoc(e)||!wt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(kt.innerHTML=e.outerHTML,kt.removeChild(o=kt.firstChild));if((!v.support.noCloneEvent||!v.support.noCloneChecked)&&(e.nodeType===1||e.nodeType===11)&&!v.isXMLDoc(e)){Ot(e,o),r=Mt(e),i=Mt(o);for(s=0;r[s];++s)i[s]&&Ot(r[s],i[s])}if(t){At(e,o);if(n){r=Mt(e),i=Mt(o);for(s=0;r[s];++s)At(r[s],i[s])}}return r=i=null,o},clean:function(e,t,n,r){var s,o,u,a,f,l,c,h,p,d,m,g,y=t===i&&Ct,b=[];if(!t||typeof t.createDocumentFragment=="undefined")t=i;for(s=0;(u=e[s])!=null;s++){typeof u=="number"&&(u+="");if(!u)continue;if(typeof u=="string")if(!gt.test(u))u=t.createTextNode(u);else{y=y||lt(t),c=t.createElement("div"),y.appendChild(c),u=u.replace(dt,"<$1>"),a=(vt.exec(u)||["",""])[1].toLowerCase(),f=Nt[a]||Nt._default,l=f[0],c.innerHTML=f[1]+u+f[2];while(l--)c=c.lastChild;if(!v.support.tbody){h=mt.test(u),p=a==="table"&&!h?c.firstChild&&c.firstChild.childNodes:f[1]===""&&!h?c.childNodes:[];for(o=p.length-1;o>=0;--o)v.nodeName(p[o],"tbody")&&!p[o].childNodes.length&&p[o].parentNode.removeChild(p[o])}!v.support.leadingWhitespace&&pt.test(u)&&c.insertBefore(t.createTextNode(pt.exec(u)[0]),c.firstChild),u=c.childNodes,c.parentNode.removeChild(c)}u.nodeType?b.push(u):v.merge(b,u)}c&&(u=c=y=null);if(!v.support.appendChecked)for(s=0;(u=b[s])!=null;s++)v.nodeName(u,"input")?_t(u):typeof u.getElementsByTagName!="undefined"&&v.grep(u.getElementsByTagName("input"),_t);if(n){m=function(e){if(!e.type||xt.test(e.type))return r?r.push(e.parentNode?e.parentNode.removeChild(e):e):n.appendChild(e)};for(s=0;(u=b[s])!=null;s++)if(!v.nodeName(u,"script")||!m(u))n.appendChild(u),typeof u.getElementsByTagName!="undefined"&&(g=v.grep(v.merge([],u.getElementsByTagName("script")),m),b.splice.apply(b,[s+1,0].concat(g)),s+=g.length)}return b},cleanData:function(e,t){var n,r,i,s,o=0,u=v.expando,a=v.cache,f=v.support.deleteExpando,l=v.event.special;for(;(i=e[o])!=null;o++)if(t||v.acceptData(i)){r=i[u],n=r&&a[r];if(n){if(n.events)for(s in n.events)l[s]?v.event.remove(i,s):v.removeEvent(i,s,n.handle);a[r]&&(delete a[r],f?delete i[u]:i.removeAttribute?i.removeAttribute(u):i[u]=null,v.deletedIds.push(r))}}}}),function(){var e,t;v.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e=v.uaMatch(o.userAgent),t={},e.browser&&(t[e.browser]=!0,t.version=e.version),t.chrome?t.webkit=!0:t.webkit&&(t.safari=!0),v.browser=t,v.sub=function(){function e(t,n){return new e.fn.init(t,n)}v.extend(!0,e,this),e.superclass=this,e.fn=e.prototype=this(),e.fn.constructor=e,e.sub=this.sub,e.fn.init=function(r,i){return i&&i instanceof v&&!(i instanceof e)&&(i=e(i)),v.fn.init.call(this,r,i,t)},e.fn.init.prototype=e.fn;var t=e(i);return e}}();var Dt,Pt,Ht,Bt=/alpha\([^)]*\)/i,jt=/opacity=([^)]*)/,Ft=/^(top|right|bottom|left)$/,It=/^(none|table(?!-c[ea]).+)/,qt=/^margin/,Rt=new RegExp("^("+m+")(.*)$","i"),Ut=new RegExp("^("+m+")(?!px)[a-z%]+$","i"),zt=new RegExp("^([-+])=("+m+")","i"),Wt={BODY:"block"},Xt={position:"absolute",visibility:"hidden",display:"block"},Vt={letterSpacing:0,fontWeight:400},$t=["Top","Right","Bottom","Left"],Jt=["Webkit","O","Moz","ms"],Kt=v.fn.toggle;v.fn.extend({css:function(e,n){return v.access(this,function(e,n,r){return r!==t?v.style(e,n,r):v.css(e,n)},e,n,arguments.length>1)},show:function(){return Yt(this,!0)},hide:function(){return Yt(this)},toggle:function(e,t){var n=typeof e=="boolean";return v.isFunction(e)&&v.isFunction(t)?Kt.apply(this,arguments):this.each(function(){(n?e:Gt(this))?v(this).show():v(this).hide()})}}),v.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Dt(e,"opacity");return n===""?"1":n}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":v.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(!e||e.nodeType===3||e.nodeType===8||!e.style)return;var s,o,u,a=v.camelCase(n),f=e.style;n=v.cssProps[a]||(v.cssProps[a]=Qt(f,a)),u=v.cssHooks[n]||v.cssHooks[a];if(r===t)return u&&"get"in u&&(s=u.get(e,!1,i))!==t?s:f[n];o=typeof r,o==="string"&&(s=zt.exec(r))&&(r=(s[1]+1)*s[2]+parseFloat(v.css(e,n)),o="number");if(r==null||o==="number"&&isNaN(r))return;o==="number"&&!v.cssNumber[a]&&(r+="px");if(!u||!("set"in u)||(r=u.set(e,r,i))!==t)try{f[n]=r}catch(l){}},css:function(e,n,r,i){var s,o,u,a=v.camelCase(n);return n=v.cssProps[a]||(v.cssProps[a]=Qt(e.style,a)),u=v.cssHooks[n]||v.cssHooks[a],u&&"get"in u&&(s=u.get(e,!0,i)),s===t&&(s=Dt(e,n)),s==="normal"&&n in Vt&&(s=Vt[n]),r||i!==t?(o=parseFloat(s),r||v.isNumeric(o)?o||0:s):s},swap:function(e,t,n){var r,i,s={};for(i in t)s[i]=e.style[i],e.style[i]=t[i];r=n.call(e);for(i in t)e.style[i]=s[i];return r}}),e.getComputedStyle?Dt=function(t,n){var r,i,s,o,u=e.getComputedStyle(t,null),a=t.style;return u&&(r=u.getPropertyValue(n)||u[n],r===""&&!v.contains(t.ownerDocument,t)&&(r=v.style(t,n)),Ut.test(r)&&qt.test(n)&&(i=a.width,s=a.minWidth,o=a.maxWidth,a.minWidth=a.maxWidth=a.width=r,r=u.width,a.width=i,a.minWidth=s,a.maxWidth=o)),r}:i.documentElement.currentStyle&&(Dt=function(e,t){var n,r,i=e.currentStyle&&e.currentStyle[t],s=e.style;return i==null&&s&&s[t]&&(i=s[t]),Ut.test(i)&&!Ft.test(t)&&(n=s.left,r=e.runtimeStyle&&e.runtimeStyle.left,r&&(e.runtimeStyle.left=e.currentStyle.left),s.left=t==="fontSize"?"1em":i,i=s.pixelLeft+"px",s.left=n,r&&(e.runtimeStyle.left=r)),i===""?"auto":i}),v.each(["height","width"],function(e,t){v.cssHooks[t]={get:function(e,n,r){if(n)return e.offsetWidth===0&&It.test(Dt(e,"display"))?v.swap(e,Xt,function(){return tn(e,t,r)}):tn(e,t,r)},set:function(e,n,r){return Zt(e,n,r?en(e,t,r,v.support.boxSizing&&v.css(e,"boxSizing")==="border-box"):0)}}}),v.support.opacity||(v.cssHooks.opacity={get:function(e,t){return jt.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=v.isNumeric(t)?"alpha(opacity="+t*100+")":"",s=r&&r.filter||n.filter||"";n.zoom=1;if(t>=1&&v.trim(s.replace(Bt,""))===""&&n.removeAttribute){n.removeAttribute("filter");if(r&&!r.filter)return}n.filter=Bt.test(s)?s.replace(Bt,i):s+" "+i}}),v(function(){v.support.reliableMarginRight||(v.cssHooks.marginRight={get:function(e,t){return v.swap(e,{display:"inline-block"},function(){if(t)return Dt(e,"marginRight")})}}),!v.support.pixelPosition&&v.fn.position&&v.each(["top","left"],function(e,t){v.cssHooks[t]={get:function(e,n){if(n){var r=Dt(e,t);return Ut.test(r)?v(e).position()[t]+"px":r}}}})}),v.expr&&v.expr.filters&&(v.expr.filters.hidden=function(e){return e.offsetWidth===0&&e.offsetHeight===0||!v.support.reliableHiddenOffsets&&(e.style&&e.style.display||Dt(e,"display"))==="none"},v.expr.filters.visible=function(e){return!v.expr.filters.hidden(e)}),v.each({margin:"",padding:"",border:"Width"},function(e,t){v.cssHooks[e+t]={expand:function(n){var r,i=typeof n=="string"?n.split(" "):[n],s={};for(r=0;r<4;r++)s[e+$t[r]+t]=i[r]||i[r-2]||i[0];return s}},qt.test(e)||(v.cssHooks[e+t].set=Zt)});var rn=/%20/g,sn=/\[\]$/,on=/\r?\n/g,un=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,an=/^(?:select|textarea)/i;v.fn.extend({serialize:function(){return v.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?v.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||an.test(this.nodeName)||un.test(this.type))}).map(function(e,t){var n=v(this).val();return n==null?null:v.isArray(n)?v.map(n,function(e,n){return{name:t.name,value:e.replace(on,"\r\n")}}):{name:t.name,value:n.replace(on,"\r\n")}}).get()}}),v.param=function(e,n){var r,i=[],s=function(e,t){t=v.isFunction(t)?t():t==null?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};n===t&&(n=v.ajaxSettings&&v.ajaxSettings.traditional);if(v.isArray(e)||e.jquery&&!v.isPlainObject(e))v.each(e,function(){s(this.name,this.value)});else for(r in e)fn(r,e[r],n,s);return i.join("&").replace(rn,"+")};var ln,cn,hn=/#.*$/,pn=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,dn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,vn=/^(?:GET|HEAD)$/,mn=/^\/\//,gn=/\?/,yn=/)<[^<]*)*<\/script>/gi,bn=/([?&])_=[^&]*/,wn=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,En=v.fn.load,Sn={},xn={},Tn=["*/"]+["*"];try{cn=s.href}catch(Nn){cn=i.createElement("a"),cn.href="",cn=cn.href}ln=wn.exec(cn.toLowerCase())||[],v.fn.load=function(e,n,r){if(typeof e!="string"&&En)return En.apply(this,arguments);if(!this.length)return this;var i,s,o,u=this,a=e.indexOf(" ");return a>=0&&(i=e.slice(a,e.length),e=e.slice(0,a)),v.isFunction(n)?(r=n,n=t):n&&typeof n=="object"&&(s="POST"),v.ajax({url:e,type:s,dataType:"html",data:n,complete:function(e,t){r&&u.each(r,o||[e.responseText,t,e])}}).done(function(e){o=arguments,u.html(i?v("
").append(e.replace(yn,"")).find(i):e)}),this},v.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(e,t){v.fn[t]=function(e){return this.on(t,e)}}),v.each(["get","post"],function(e,n){v[n]=function(e,r,i,s){return v.isFunction(r)&&(s=s||i,i=r,r=t),v.ajax({type:n,url:e,data:r,success:i,dataType:s})}}),v.extend({getScript:function(e,n){return v.get(e,t,n,"script")},getJSON:function(e,t,n){return v.get(e,t,n,"json")},ajaxSetup:function(e,t){return t?Ln(e,v.ajaxSettings):(t=e,e=v.ajaxSettings),Ln(e,t),e},ajaxSettings:{url:cn,isLocal:dn.test(ln[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":Tn},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":e.String,"text html":!0,"text json":v.parseJSON,"text xml":v.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:Cn(Sn),ajaxTransport:Cn(xn),ajax:function(e,n){function T(e,n,s,a){var l,y,b,w,S,T=n;if(E===2)return;E=2,u&&clearTimeout(u),o=t,i=a||"",x.readyState=e>0?4:0,s&&(w=An(c,x,s));if(e>=200&&e<300||e===304)c.ifModified&&(S=x.getResponseHeader("Last-Modified"),S&&(v.lastModified[r]=S),S=x.getResponseHeader("Etag"),S&&(v.etag[r]=S)),e===304?(T="notmodified",l=!0):(l=On(c,w),T=l.state,y=l.data,b=l.error,l=!b);else{b=T;if(!T||e)T="error",e<0&&(e=0)}x.status=e,x.statusText=(n||T)+"",l?d.resolveWith(h,[y,T,x]):d.rejectWith(h,[x,T,b]),x.statusCode(g),g=t,f&&p.trigger("ajax"+(l?"Success":"Error"),[x,c,l?y:b]),m.fireWith(h,[x,T]),f&&(p.trigger("ajaxComplete",[x,c]),--v.active||v.event.trigger("ajaxStop"))}typeof e=="object"&&(n=e,e=t),n=n||{};var r,i,s,o,u,a,f,l,c=v.ajaxSetup({},n),h=c.context||c,p=h!==c&&(h.nodeType||h instanceof v)?v(h):v.event,d=v.Deferred(),m=v.Callbacks("once memory"),g=c.statusCode||{},b={},w={},E=0,S="canceled",x={readyState:0,setRequestHeader:function(e,t){if(!E){var n=e.toLowerCase();e=w[n]=w[n]||e,b[e]=t}return this},getAllResponseHeaders:function(){return E===2?i:null},getResponseHeader:function(e){var n;if(E===2){if(!s){s={};while(n=pn.exec(i))s[n[1].toLowerCase()]=n[2]}n=s[e.toLowerCase()]}return n===t?null:n},overrideMimeType:function(e){return E||(c.mimeType=e),this},abort:function(e){return e=e||S,o&&o.abort(e),T(0,e),this}};d.promise(x),x.success=x.done,x.error=x.fail,x.complete=m.add,x.statusCode=function(e){if(e){var t;if(E<2)for(t in e)g[t]=[g[t],e[t]];else t=e[x.status],x.always(t)}return this},c.url=((e||c.url)+"").replace(hn,"").replace(mn,ln[1]+"//"),c.dataTypes=v.trim(c.dataType||"*").toLowerCase().split(y),c.crossDomain==null&&(a=wn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===ln[1]&&a[2]===ln[2]&&(a[3]||(a[1]==="http:"?80:443))==(ln[3]||(ln[1]==="http:"?80:443)))),c.data&&c.processData&&typeof c.data!="string"&&(c.data=v.param(c.data,c.traditional)),kn(Sn,c,n,x);if(E===2)return x;f=c.global,c.type=c.type.toUpperCase(),c.hasContent=!vn.test(c.type),f&&v.active++===0&&v.event.trigger("ajaxStart");if(!c.hasContent){c.data&&(c.url+=(gn.test(c.url)?"&":"?")+c.data,delete c.data),r=c.url;if(c.cache===!1){var N=v.now(),C=c.url.replace(bn,"$1_="+N);c.url=C+(C===c.url?(gn.test(c.url)?"&":"?")+"_="+N:"")}}(c.data&&c.hasContent&&c.contentType!==!1||n.contentType)&&x.setRequestHeader("Content-Type",c.contentType),c.ifModified&&(r=r||c.url,v.lastModified[r]&&x.setRequestHeader("If-Modified-Since",v.lastModified[r]),v.etag[r]&&x.setRequestHeader("If-None-Match",v.etag[r])),x.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+(c.dataTypes[0]!=="*"?", "+Tn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)x.setRequestHeader(l,c.headers[l]);if(!c.beforeSend||c.beforeSend.call(h,x,c)!==!1&&E!==2){S="abort";for(l in{success:1,error:1,complete:1})x[l](c[l]);o=kn(xn,c,n,x);if(!o)T(-1,"No Transport");else{x.readyState=1,f&&p.trigger("ajaxSend",[x,c]),c.async&&c.timeout>0&&(u=setTimeout(function(){x.abort("timeout")},c.timeout));try{E=1,o.send(b,T)}catch(k){if(!(E<2))throw k;T(-1,k)}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var Mn=[],_n=/\?/,Dn=/(=)\?(?=&|$)|\?\?/,Pn=v.now();v.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Mn.pop()||v.expando+"_"+Pn++;return this[e]=!0,e}}),v.ajaxPrefilter("json jsonp",function(n,r,i){var s,o,u,a=n.data,f=n.url,l=n.jsonp!==!1,c=l&&Dn.test(f),h=l&&!c&&typeof a=="string"&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Dn.test(a);if(n.dataTypes[0]==="jsonp"||c||h)return s=n.jsonpCallback=v.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,o=e[s],c?n.url=f.replace(Dn,"$1"+s):h?n.data=a.replace(Dn,"$1"+s):l&&(n.url+=(_n.test(f)?"&":"?")+n.jsonp+"="+s),n.converters["script json"]=function(){return u||v.error(s+" was not called"),u[0]},n.dataTypes[0]="json",e[s]=function(){u=arguments},i.always(function(){e[s]=o,n[s]&&(n.jsonpCallback=r.jsonpCallback,Mn.push(s)),u&&v.isFunction(o)&&o(u[0]),u=o=t}),"script"}),v.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(e){return v.globalEval(e),e}}}),v.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),v.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=i.head||i.getElementsByTagName("head")[0]||i.documentElement;return{send:function(s,o){n=i.createElement("script"),n.async="async",e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,i){if(i||!n.readyState||/loaded|complete/.test(n.readyState))n.onload=n.onreadystatechange=null,r&&n.parentNode&&r.removeChild(n),n=t,i||o(200,"success")},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(0,1)}}}});var Hn,Bn=e.ActiveXObject?function(){for(var e in Hn)Hn[e](0,1)}:!1,jn=0;v.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&Fn()||In()}:Fn,function(e){v.extend(v.support,{ajax:!!e,cors:!!e&&"withCredentials"in e})}(v.ajaxSettings.xhr()),v.support.ajax&&v.ajaxTransport(function(n){if(!n.crossDomain||v.support.cors){var r;return{send:function(i,s){var o,u,a=n.xhr();n.username?a.open(n.type,n.url,n.async,n.username,n.password):a.open(n.type,n.url,n.async);if(n.xhrFields)for(u in n.xhrFields)a[u]=n.xhrFields[u];n.mimeType&&a.overrideMimeType&&a.overrideMimeType(n.mimeType),!n.crossDomain&&!i["X-Requested-With"]&&(i["X-Requested-With"]="XMLHttpRequest");try{for(u in i)a.setRequestHeader(u,i[u])}catch(f){}a.send(n.hasContent&&n.data||null),r=function(e,i){var u,f,l,c,h;try{if(r&&(i||a.readyState===4)){r=t,o&&(a.onreadystatechange=v.noop,Bn&&delete Hn[o]);if(i)a.readyState!==4&&a.abort();else{u=a.status,l=a.getAllResponseHeaders(),c={},h=a.responseXML,h&&h.documentElement&&(c.xml=h);try{c.text=a.responseText}catch(p){}try{f=a.statusText}catch(p){f=""}!u&&n.isLocal&&!n.crossDomain?u=c.text?200:404:u===1223&&(u=204)}}}catch(d){i||s(-1,d)}c&&s(u,f,c,l)},n.async?a.readyState===4?setTimeout(r,0):(o=++jn,Bn&&(Hn||(Hn={},v(e).unload(Bn)),Hn[o]=r),a.onreadystatechange=r):r()},abort:function(){r&&r(0,1)}}}});var qn,Rn,Un=/^(?:toggle|show|hide)$/,zn=new RegExp("^(?:([-+])=|)("+m+")([a-z%]*)$","i"),Wn=/queueHooks$/,Xn=[Gn],Vn={"*":[function(e,t){var n,r,i=this.createTween(e,t),s=zn.exec(t),o=i.cur(),u=+o||0,a=1,f=20;if(s){n=+s[2],r=s[3]||(v.cssNumber[e]?"":"px");if(r!=="px"&&u){u=v.css(i.elem,e,!0)||n||1;do a=a||".5",u/=a,v.style(i.elem,e,u+r);while(a!==(a=i.cur()/o)&&a!==1&&--f)}i.unit=r,i.start=u,i.end=s[1]?u+(s[1]+1)*n:n}return i}]};v.Animation=v.extend(Kn,{tweener:function(e,t){v.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;r-1,f={},l={},c,h;a?(l=i.position(),c=l.top,h=l.left):(c=parseFloat(o)||0,h=parseFloat(u)||0),v.isFunction(t)&&(t=t.call(e,n,s)),t.top!=null&&(f.top=t.top-s.top+c),t.left!=null&&(f.left=t.left-s.left+h),"using"in t?t.using.call(e,f):i.css(f)}},v.fn.extend({position:function(){if(!this[0])return;var e=this[0],t=this.offsetParent(),n=this.offset(),r=er.test(t[0].nodeName)?{top:0,left:0}:t.offset();return n.top-=parseFloat(v.css(e,"marginTop"))||0,n.left-=parseFloat(v.css(e,"marginLeft"))||0,r.top+=parseFloat(v.css(t[0],"borderTopWidth"))||0,r.left+=parseFloat(v.css(t[0],"borderLeftWidth"))||0,{top:n.top-r.top,left:n.left-r.left}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||i.body;while(e&&!er.test(e.nodeName)&&v.css(e,"position")==="static")e=e.offsetParent;return e||i.body})}}),v.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);v.fn[e]=function(i){return v.access(this,function(e,i,s){var o=tr(e);if(s===t)return o?n in o?o[n]:o.document.documentElement[i]:e[i];o?o.scrollTo(r?v(o).scrollLeft():s,r?s:v(o).scrollTop()):e[i]=s},e,i,arguments.length,null)}}),v.each({Height:"height",Width:"width"},function(e,n){v.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){v.fn[i]=function(i,s){var o=arguments.length&&(r||typeof i!="boolean"),u=r||(i===!0||s===!0?"margin":"border");return v.access(this,function(n,r,i){var s;return v.isWindow(n)?n.document.documentElement["client"+e]:n.nodeType===9?(s=n.documentElement,Math.max(n.body["scroll"+e],s["scroll"+e],n.body["offset"+e],s["offset"+e],s["client"+e])):i===t?v.css(n,r,i,u):v.style(n,r,i,u)},n,o?i:t,o,null)}})}),e.jQuery=e.$=v,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return v})})(window); \ No newline at end of file +(function(e, t) { + function _(e) { + var t = M[e] = {}; + return v.each(e.split(y), function(e, n) { + t[n] = !0; + }), t; + } + function H(e, n, r) { + if (r === t && e.nodeType === 1) { + var i = "data-" + n.replace(P, "-$1").toLowerCase(); + r = e.getAttribute(i); + if (typeof r === "string") { + try { + r = r === "true" + ? !0 + : r === "false" + ? !1 + : r === "null" + ? null + : +r + "" === r ? +r : D.test(r) ? v.parseJSON(r) : r; + } catch (s) { + } + v.data(e, n, r); + } else + r = t; + } + return r; + } + function B(e) { + var t; + for (t in e) { + if (t === "data" && v.isEmptyObject(e[t])) continue; + if (t !== "toJSON") return !1; + } + return !0; + } + function et() { + return !1; + } + function tt() { + return !0; + } + function ut(e) { + return !e || !e.parentNode || e.parentNode.nodeType === 11; + } + function at(e, t) { + do { + e = e[t]; + } while (e && e.nodeType !== 1); + return e; + } + function ft(e, t, n) { + t = t || 0; + if (v.isFunction(t)) { + return v.grep(e, function(e, r) { + var i = !!t.call(e, r, e); + return i === n; + }); + } + if (t.nodeType) { + return v.grep(e, function(e, r) { + return e === t === n; + }); + } + if (typeof t === "string") { + var r = v.grep(e, function(e) { + return e.nodeType === 1; + }); + if (it.test(t)) return v.filter(t, r, !n); + t = v.filter(t, r); + } + return v.grep(e, function(e, r) { + return v.inArray(e, t) >= 0 === n; + }); + } + function lt(e) { + var t = ct.split("|"), n = e.createDocumentFragment(); + if (n.createElement) while (t.length) n.createElement(t.pop()); + return n; + } + function Lt(e, t) { + return e.getElementsByTagName(t)[0] || + e.appendChild(e.ownerDocument.createElement(t)); + } + function At(e, t) { + if (t.nodeType !== 1 || !v.hasData(e)) return; + var n, r, i, s = v._data(e), o = v._data(t, s), u = s.events; + if (u) { + delete o.handle, o.events = {}; + for (n in u) { + for (r = 0, i = u[n].length; r < i; r++) + v.event.add(t, n, u[n][r]); + } + } + o.data && (o.data = v.extend({}, o.data)); + } + function Ot(e, t) { + var n; + if (t.nodeType !== 1) return; + t.clearAttributes && t.clearAttributes(), t.mergeAttributes && + t.mergeAttributes(e), n = t.nodeName.toLowerCase(), n === "object" + ? (t.parentNode && (t.outerHTML = e.outerHTML), v.support.html5Clone && + e.innerHTML && + !v.trim(t.innerHTML) && + (t.innerHTML = e.innerHTML)) + : n === "input" && Et.test(e.type) + ? (t.defaultChecked = t.checked = e.checked, t.value !== e.value && + (t.value = e.value)) + : n === "option" + ? t.selected = e.defaultSelected + : n === "input" || n === "textarea" + ? t.defaultValue = e.defaultValue + : n === "script" && + t.text !== e.text && + (t.text = e.text), t.removeAttribute(v.expando); + } + function Mt(e) { + return typeof e.getElementsByTagName !== "undefined" + ? e.getElementsByTagName("*") + : typeof e.querySelectorAll !== "undefined" + ? e.querySelectorAll("*") + : []; + } + function _t(e) { + Et.test(e.type) && (e.defaultChecked = e.checked); + } + function Qt(e, t) { + if (t in e) return t; + var n = t.charAt(0).toUpperCase() + t.slice(1), r = t, i = Jt.length; + while (i--) { + t = Jt[i] + n; + if (t in e) return t; + } + return r; + } + function Gt(e, t) { + return e = t || e, v.css(e, "display") === "none" || + !v.contains(e.ownerDocument, e); + } + function Yt(e, t) { + var n, r, i = [], s = 0, o = e.length; + for (; s < o; s++) { + n = e[s]; + if (!n.style) continue; + i[s] = v._data(n, "olddisplay"), t + ? (!i[s] && + n.style.display === "none" && + (n.style.display = ""), n.style.display === "" && + Gt(n) && + (i[s] = v._data(n, "olddisplay", nn(n.nodeName)))) + : (r = Dt(n, "display"), !i[s] && + r !== "none" && + v._data(n, "olddisplay", r)); + } + for (s = 0; s < o; s++) { + n = e[s]; + if (!n.style) continue; + if (!t || n.style.display === "none" || n.style.display === "") { + n.style.display = t ? i[s] || "" : "none"; + } + } + return e; + } + function Zt(e, t, n) { + var r = Rt.exec(t); + return r ? Math.max(0, r[1] - (n || 0)) + (r[2] || "px") : t; + } + function en(e, t, n, r) { + var i = n === (r ? "border" : "content") ? 4 : t === "width" ? 1 : 0, s = 0; + for (; i < 4; i += 2) { + n === "margin" && (s += v.css(e, n + $t[i], !0)), r + ? (n === "content" && + (s -= parseFloat(Dt(e, "padding" + $t[i])) || 0), n !== "margin" && + (s -= parseFloat(Dt(e, "border" + $t[i] + "Width")) || 0)) + : (s += parseFloat(Dt(e, "padding" + $t[i])) || 0, n !== "padding" && + (s += parseFloat(Dt(e, "border" + $t[i] + "Width")) || 0)); + } + return s; + } + function tn(e, t, n) { + var r = t === "width" ? e.offsetWidth : e.offsetHeight, + i = !0, + s = v.support.boxSizing && v.css(e, "boxSizing") === "border-box"; + if (r <= 0 || r == null) { + r = Dt(e, t); + if (r < 0 || r == null) r = e.style[t]; + if (Ut.test(r)) return r; + i = s && + (v.support.boxSizingReliable || r === e.style[t]), r = parseFloat(r) || + 0; + } + return r + en(e, t, n || (s ? "border" : "content"), i) + "px"; + } + function nn(e) { + if (Wt[e]) return Wt[e]; + var t = v("<" + e + ">").appendTo(i.body), n = t.css("display"); + t.remove(); + if (n === "none" || n === "") { + Pt = i.body.appendChild( + Pt || + v.extend(i.createElement("iframe"), { + frameBorder: 0, + width: 0, + height: 0 + }) + ); + if (!Ht || !Pt.createElement) { + Ht = (Pt.contentWindow || Pt.contentDocument).document, Ht.write( + "" + ), Ht.close(); + } + t = Ht.body.appendChild(Ht.createElement(e)), n = Dt( + t, + "display" + ), i.body.removeChild(Pt); + } + return Wt[e] = n, n; + } + function fn(e, t, n, r) { + var i; + if (v.isArray(t)) { + v.each(t, function(t, i) { + n || sn.test(e) + ? r(e, i) + : fn(e + "[" + (typeof i === "object" ? t : "") + "]", i, n, r); + }); + } else if (!n && v.type(t) === "object") { + for (i in t) { + fn(e + "[" + i + "]", t[i], n, r); + } + } else { + r(e, t); + } + } + function Cn(e) { + return function(t, n) { + typeof t !== "string" && (n = t, t = "*"); + var r, i, s, o = t.toLowerCase().split(y), u = 0, a = o.length; + if (v.isFunction(n)) { + for (; u < a; u++) { + r = o[u], s = /^\+/.test(r), s && (r = r.substr(1) || "*"), i = e[ + r + ] = e[r] || [], i[s ? "unshift" : "push"](n); + } + } + }; + } + function kn(e, n, r, i, s, o) { + s = s || n.dataTypes[0], o = o || {}, o[s] = !0; + var u, a = e[s], f = 0, l = a ? a.length : 0, c = e === Sn; + for (; f < l && (c || !u); f++) { + u = a[f](n, r, i), typeof u === "string" && + (!c || o[u] + ? u = t + : (n.dataTypes.unshift(u), u = kn(e, n, r, i, u, o))); + } + return (c || !u) && !o["*"] && (u = kn(e, n, r, i, "*", o)), u; + } + function Ln(e, n) { + var r, i, s = v.ajaxSettings.flatOptions || {}; + for (r in n) { + n[r] !== t && ((s[r] ? e : i || (i = {}))[r] = n[r]); + } + i && v.extend(!0, e, i); + } + function An(e, n, r) { + var i, s, o, u, a = e.contents, f = e.dataTypes, l = e.responseFields; + for (s in l) { + s in r && (n[l[s]] = r[s]); + } + while (f[0] === "*") { + f.shift(), i === t && + (i = e.mimeType || n.getResponseHeader("content-type")); + } + if (i) { + for (s in a) { + if (a[s] && a[s].test(i)) { + f.unshift(s); + break; + } + } + } + if (f[0] in r) + o = f[0]; + else { + for (s in r) { + if (!f[0] || e.converters[s + " " + f[0]]) { + o = s; + break; + } + u || (u = s); + } + o = o || u; + } + if (o) return o !== f[0] && f.unshift(o), r[o]; + } + function On(e, t) { + var n, r, i, s, o = e.dataTypes.slice(), u = o[0], a = {}, f = 0; + e.dataFilter && (t = e.dataFilter(t, e.dataType)); + if (o[1]) for (n in e.converters) a[n.toLowerCase()] = e.converters[n]; + for (; i = o[++f]; ) { + if (i !== "*") { + if (u !== "*" && u !== i) { + n = a[u + " " + i] || a["* " + i]; + if (!n) { + for (r in a) { + s = r.split(" "); + if (s[1] === i) { + n = a[u + " " + s[0]] || a["* " + s[0]]; + if (n) { + n === !0 + ? n = a[r] + : a[r] !== !0 && (i = s[0], o.splice((f--), 0, i)); + break; + } + } + } + } + if (n !== !0) { + if (n && e["throws"]) + t = n(t); + else { + try { + t = n(t); + } catch (l) { + return { + state: "parsererror", + error: n ? l : "No conversion from " + u + " to " + i + }; + } + } + } + } + u = i; + } + } + return { state: "success", data: t }; + } + function Fn() { + try { + return new e.XMLHttpRequest(); + } catch (t) { + } + } + function In() { + try { + return new e.ActiveXObject("Microsoft.XMLHTTP"); + } catch (t) { + } + } + function $n() { + return setTimeout( + function() { + qn = t; + }, + 0 + ), qn = v.now(); + } + function Jn(e, t) { + v.each(t, function(t, n) { + var r = (Vn[t] || []).concat(Vn["*"]), i = 0, s = r.length; + for (; i < s; i++) if (r[i].call(e, t, n)) return; + }); + } + function Kn(e, t, n) { + var r, + i = 0, + s = 0, + o = Xn.length, + u = v.Deferred().always(function() { + delete a.elem; + }), + a = function() { + var t = qn || $n(), + n = Math.max(0, f.startTime + f.duration - t), + r = n / f.duration || 0, + i = 1 - r, + s = 0, + o = f.tweens.length; + for (; s < o; s++) { + f.tweens[s].run(i); + } + return u.notifyWith(e, [f, i, n]), i < 1 && o + ? n + : (u.resolveWith(e, [f]), !1); + }, + f = u.promise({ + elem: e, + props: v.extend({}, t), + opts: v.extend(!0, { specialEasing: {} }, n), + originalProperties: t, + originalOptions: n, + startTime: qn || $n(), + duration: n.duration, + tweens: [], + createTween: function(t, n, r) { + var i = v.Tween( + e, + f.opts, + t, + n, + f.opts.specialEasing[t] || f.opts.easing + ); + return f.tweens.push(i), i; + }, + stop: function(t) { + var n = 0, r = t ? f.tweens.length : 0; + for (; n < r; n++) + f.tweens[n].run(1); + return t ? u.resolveWith(e, [f, t]) : u.rejectWith(e, [f, t]), this; + } + }), + l = f.props; + Qn(l, f.opts.specialEasing); + for (; i < o; i++) { + r = Xn[i].call(f, e, l, f.opts); + if (r) return r; + } + return Jn(f, l), v.isFunction(f.opts.start) && + f.opts.start.call(e, f), v.fx.timer( + v.extend(a, { anim: f, queue: f.opts.queue, elem: e }) + ), f + .progress(f.opts.progress) + .done(f.opts.done, f.opts.complete) + .fail(f.opts.fail) + .always(f.opts.always); + } + function Qn(e, t) { + var n, r, i, s, o; + for (n in e) { + r = v.camelCase(n), i = t[r], s = e[n], v.isArray(s) && + (i = s[1], s = e[n] = s[0]), n !== r && + (e[r] = s, delete e[n]), o = v.cssHooks[r]; + if (o && "expand" in o) { + s = o.expand(s), delete e[r]; + for (n in s) + n in e || (e[n] = s[n], t[n] = i); + } else + t[r] = i; + } + } + function Gn(e, t, n) { + var r, + i, + s, + o, + u, + a, + f, + l, + c, + h = this, + p = e.style, + d = {}, + m = [], + g = e.nodeType && Gt(e); + n.queue || + (l = v._queueHooks(e, "fx"), l.unqueued == null && + (l.unqueued = 0, c = l.empty.fire, l.empty.fire = function() { + l.unqueued || c(); + }), l.unqueued++, h.always(function() { + h.always(function() { + l.unqueued--, v.queue(e, "fx").length || l.empty.fire(); + }); + })), e.nodeType === 1 && + ("height" in t || "width" in t) && + (n.overflow = [p.overflow, p.overflowX, p.overflowY], v.css( + e, + "display" + ) === + "inline" && + v.css(e, "float") === "none" && + (!v.support.inlineBlockNeedsLayout || nn(e.nodeName) === "inline" + ? p.display = "inline-block" + : p.zoom = 1)), n.overflow && + (p.overflow = "hidden", v.support.shrinkWrapBlocks || + h.done(function() { + p.overflow = n.overflow[ + 0 + ], p.overflowX = n.overflow[1], p.overflowY = n.overflow[2]; + })); + for (r in t) { + s = t[r]; + if (Un.exec(s)) { + delete t[r], a = a || s === "toggle"; + if (s === (g ? "hide" : "show")) continue; + m.push(r); + } + } + o = m.length; + if (o) { + u = v._data(e, "fxshow") || v._data(e, "fxshow", {}), "hidden" in u && + (g = u.hidden), a && (u.hidden = !g), g + ? v(e).show() + : h.done(function() { + v(e).hide(); + }), h.done(function() { + var t; + v.removeData(e, "fxshow", !0); + for (t in d) v.style(e, t, d[t]); + }); + for (r = 0; r < o; r++) + i = m[r], f = h.createTween(i, g ? u[i] : 0), d[i] = u[i] || + v.style(e, i), i in u || + (u[i] = f.start, g && + (f.end = f.start, f.start = i === "width" || i === "height" + ? 1 + : 0)); + } + } + function Yn(e, t, n, r, i) { + return new Yn.prototype.init(e, t, n, r, i); + } + function Zn(e, t) { + var n, r = { height: e }, i = 0; + t = t ? 1 : 0; + for (; i < 4; i += 2 - t) { + n = $t[i], r["margin" + n] = r["padding" + n] = e; + } + return t && (r.opacity = r.width = e), r; + } + function tr(e) { + return v.isWindow(e) + ? e + : e.nodeType === 9 ? e.defaultView || e.parentWindow : !1; + } + var n, + r, + i = e.document, + s = e.location, + o = e.navigator, + u = e.jQuery, + a = e.$, + f = Array.prototype.push, + l = Array.prototype.slice, + c = Array.prototype.indexOf, + h = Object.prototype.toString, + p = Object.prototype.hasOwnProperty, + d = String.prototype.trim, + v = function(e, t) { + return new v.fn.init(e, t, n); + }, + m = /[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source, + g = /\S/, + y = /\s+/, + b = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + w = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + E = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + S = /^[\],:{}\s]*$/, + x = /(?:^|:|,)(?:\s*\[)+/g, + T = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, + N = /"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g, + C = /^-ms-/, + k = /-([\da-z])/gi, + L = function(e, t) { + return (t + "").toUpperCase(); + }, + A = function() { + i.addEventListener + ? (i.removeEventListener("DOMContentLoaded", A, !1), v.ready()) + : i.readyState === "complete" && + (i.detachEvent("onreadystatechange", A), v.ready()); + }, + O = {}; + v.fn = v.prototype = { + constructor: v, + init: function(e, n, r) { + var s, o, u, a; + if (!e) return this; + if (e.nodeType) return this.context = this[0] = e, this.length = 1, this; + if (typeof e === "string") { + e.charAt(0) === "<" && e.charAt(e.length - 1) === ">" && e.length >= 3 + ? s = [null, e, null] + : s = w.exec(e); + if (s && (s[1] || !n)) { + if (s[1]) { + return n = n instanceof v ? n[0] : n, a = n && n.nodeType + ? n.ownerDocument || n + : i, e = v.parseHTML(s[1], a, !0), E.test(s[1]) && + v.isPlainObject(n) && + this.attr.call(e, n, !0), v.merge(this, e); + } + o = i.getElementById(s[2]); + if (o && o.parentNode) { + if (o.id !== s[2]) return r.find(e); + this.length = 1, this[0] = o; + } + return this.context = i, this.selector = e, this; + } + return !n || n.jquery ? (n || r).find(e) : this.constructor(n).find(e); + } + return v.isFunction(e) + ? r.ready(e) + : (e.selector !== t && + (this.selector = e.selector, this.context = e.context), v.makeArray( + e, + this + )); + }, + selector: "", + jquery: "1.8.3", + length: 0, + size: function() { + return this.length; + }, + toArray: function() { + return l.call(this); + }, + get: function(e) { + return e == null + ? this.toArray() + : e < 0 ? this[this.length + e] : this[e]; + }, + pushStack: function(e, t, n) { + var r = v.merge(this.constructor(), e); + return r.prevObject = this, r.context = this.context, t === "find" + ? r.selector = this.selector + (this.selector ? " " : "") + n + : t && (r.selector = this.selector + "." + t + "(" + n + ")"), r; + }, + each: function(e, t) { + return v.each(this, e, t); + }, + ready: function(e) { + return v.ready.promise().done(e), this; + }, + eq: function(e) { + return e = +e, e === -1 ? this.slice(e) : this.slice(e, e + 1); + }, + first: function() { + return this.eq(0); + }, + last: function() { + return this.eq(-1); + }, + slice: function() { + return this.pushStack( + l.apply(this, arguments), + "slice", + l.call(arguments).join(",") + ); + }, + map: function(e) { + return this.pushStack( + v.map(this, function(t, n) { + return e.call(t, n, t); + }) + ); + }, + end: function() { + return this.prevObject || this.constructor(null); + }, + push: f, + sort: [].sort, + splice: [].splice + }, v.fn.init.prototype = v.fn, v.extend = v.fn.extend = function() { + var e, + n, + r, + i, + s, + o, + u = arguments[0] || {}, + a = 1, + f = arguments.length, + l = !1; + typeof u === "boolean" && + (l = u, u = arguments[1] || {}, a = 2), typeof u !== "object" && + !v.isFunction(u) && + (u = {}), f === a && (u = this, --a); + for (; a < f; a++) { + if ((e = arguments[a]) != null) { + for (n in e) { + r = u[n], i = e[n]; + if (u === i) continue; + l && i && (v.isPlainObject(i) || (s = v.isArray(i))) + ? (s + ? (s = !1, o = r && v.isArray(r) ? r : []) + : o = r && v.isPlainObject(r) ? r : {}, u[n] = v.extend( + l, + o, + i + )) + : i !== t && (u[n] = i); + } + } + } + return u; + }, v.extend({ + noConflict: function(t) { + return e.$ === v && (e.$ = a), t && e.jQuery === v && (e.jQuery = u), v; + }, + isReady: !1, + readyWait: 1, + holdReady: function(e) { + e ? v.readyWait++ : v.ready(!0); + }, + ready: function(e) { + if (e === !0 ? --v.readyWait : v.isReady) return; + if (!i.body) return setTimeout(v.ready, 1); + v.isReady = !0; + if (e !== !0 && --v.readyWait > 0) return; + r.resolveWith(i, [v]), v.fn.trigger && v(i).trigger("ready").off("ready"); + }, + isFunction: function(e) { + return v.type(e) === "function"; + }, + isArray: ( + Array.isArray || + function(e) { + return v.type(e) === "array"; + } + ), + isWindow: function(e) { + return e != null && e == e.window; + }, + isNumeric: function(e) { + return !isNaN(parseFloat(e)) && isFinite(e); + }, + type: function(e) { + return e == null ? String(e) : O[h.call(e)] || "object"; + }, + isPlainObject: function(e) { + if (!e || v.type(e) !== "object" || e.nodeType || v.isWindow(e)) { + return !1; + } + try { + if ( + e.constructor && + !p.call(e, "constructor") && + !p.call(e.constructor.prototype, "isPrototypeOf") + ) { + return !1; + } + } catch (n) { + return !1; + } + var r; + for (r in e); + return r === t || p.call(e, r); + }, + isEmptyObject: function(e) { + var t; + for (t in e) + return !1; + return !0; + }, + error: function(e) { + throw new Error(e); + }, + parseHTML: function(e, t, n) { + var r; + return !e || typeof e !== "string" + ? null + : (typeof t === "boolean" && (n = t, t = 0), t = t || i, (r = E.exec(e)) + ? [t.createElement(r[1])] + : (r = v.buildFragment([e], t, n ? null : []), v.merge( + [], + (r.cacheable ? v.clone(r.fragment) : r.fragment).childNodes + ))); + }, + parseJSON: function(t) { + if (!t || typeof t !== "string") return null; + t = v.trim(t); + if (e.JSON && e.JSON.parse) return e.JSON.parse(t); + if (S.test(t.replace(T, "@").replace(N, "]").replace(x, ""))) { + return new Function("return " + t)(); + } + v.error("Invalid JSON: " + t); + }, + parseXML: function(n) { + var r, i; + if (!n || typeof n !== "string") return null; + try { + e.DOMParser + ? (i = new DOMParser(), r = i.parseFromString(n, "text/xml")) + : (r = new ActiveXObject( + "Microsoft.XMLDOM" + ), r.async = "false", r.loadXML(n)); + } catch (s) { + r = t; + } + return (!r || + !r.documentElement || + r.getElementsByTagName("parsererror").length) && + v.error("Invalid XML: " + n), r; + }, + noop: function() {}, + globalEval: function(t) { + t && + g.test(t) && + (e.execScript || + function(t) { + e.eval.call(e, t); + })(t); + }, + camelCase: function(e) { + return e.replace(C, "ms-").replace(k, L); + }, + nodeName: function(e, t) { + return e.nodeName && e.nodeName.toLowerCase() === t.toLowerCase(); + }, + each: function(e, n, r) { + var i, s = 0, o = e.length, u = o === t || v.isFunction(e); + if (r) { + if (u) { + for (i in e) + if (n.apply(e[i], r) === !1) break; + } else + for (; s < o; ) + if (n.apply(e[s++], r) === !1) break; + } else if (u) { + for (i in e) + if (n.call(e[i], i, e[i]) === !1) break; + } else + for (; s < o; ) + if (n.call(e[s], s, e[s++]) === !1) break; + return e; + }, + trim: ( + d && !d.call("\uFEFF\xA0") + ? (function(e) { + return e == null ? "" : d.call(e); + }) + : (function(e) { + return e == null ? "" : (e + "").replace(b, ""); + }) + ), + makeArray: function(e, t) { + var n, r = t || []; + return e != null && + (n = v.type(e), e.length == null || + n === "string" || + n === "function" || + n === "regexp" || + v.isWindow(e) + ? f.call(r, e) + : v.merge(r, e)), r; + }, + inArray: function(e, t, n) { + var r; + if (t) { + if (c) return c.call(t, e, n); + r = t.length, n = n ? n < 0 ? Math.max(0, r + n) : n : 0; + for (; n < r; n++) + if (n in t && t[n] === e) return n; + } + return -1; + }, + merge: function(e, n) { + var r = n.length, i = e.length, s = 0; + if (typeof r === "number") for (; s < r; s++) e[i++] = n[s]; + else while (n[s] !== t) e[i++] = n[s++]; + return e.length = i, e; + }, + grep: function(e, t, n) { + var r, i = [], s = 0, o = e.length; + n = !!n; + for (; s < o; s++) + r = !!t(e[s], s), n !== r && i.push(e[s]); + return i; + }, + map: function(e, n, r) { + var i, + s, + o = [], + u = 0, + a = e.length, + f = e instanceof v || + a !== t && + typeof a === "number" && + (a > 0 && e[0] && e[a - 1] || a === 0 || v.isArray(e)); + if (f) { + for (; u < a; u++) { + i = n(e[u], u, r), i != null && (o[o.length] = i); + } + } else { + for (s in e) { + i = n(e[s], s, r), i != null && (o[o.length] = i); + } + } + return o.concat.apply([], o); + }, + guid: 1, + proxy: function(e, n) { + var r, i, s; + return typeof n === "string" && (r = e[n], n = e, e = r), v.isFunction(e) + ? (i = l.call(arguments, 2), s = function() { + return e.apply(n, i.concat(l.call(arguments))); + }, s.guid = e.guid = e.guid || v.guid++, s) + : t; + }, + access: function(e, n, r, i, s, o, u) { + var a, f = r == null, l = 0, c = e.length; + if (r && typeof r === "object") { + for (l in r) + v.access(e, n, l, r[l], 1, o, i); + s = 1; + } else if (i !== t) { + a = u === t && v.isFunction(i), f && + (a + ? (a = n, n = function(e, t, n) { + return a.call(v(e), n); + }) + : (n.call(e, i), n = null)); + if (n) { + for (; l < c; l++) { + n(e[l], r, a ? i.call(e[l], l, n(e[l], r)) : i, u); + } + } + s = 1; + } + return s ? e : f ? n.call(e) : c ? n(e[0], r) : o; + }, + now: function() { + return new Date().getTime(); + } + }), v.ready.promise = function(t) { + if (!r) { + r = v.Deferred(); + if (i.readyState === "complete") + setTimeout(v.ready, 1); + else if (i.addEventListener) { + i.addEventListener("DOMContentLoaded", A, !1), e.addEventListener( + "load", + v.ready, + !1 + ); + } else { + i.attachEvent("onreadystatechange", A), e.attachEvent( + "onload", + v.ready + ); + var n = !1; + try { + n = e.frameElement == null && i.documentElement; + } catch (s) { + } + n && + n.doScroll && + (function o() { + if (!v.isReady) { + try { + n.doScroll("left"); + } catch (e) { + return setTimeout(o, 50); + } + v.ready(); + } + })(); + } + } + return r.promise(t); + }, v.each( + "Boolean Number String Function Array Date RegExp Object".split(" "), + function(e, t) { + O["[object " + t + "]"] = t.toLowerCase(); + } + ), n = v(i); + var M = {}; + v.Callbacks = function(e) { + e = typeof e === "string" ? M[e] || _(e) : v.extend({}, e); + var n, + r, + i, + s, + o, + u, + a = [], + f = !e.once && [], + l = function(t) { + n = e.memory && t, r = !0, u = s || 0, s = 0, o = a.length, i = !0; + for (; a && u < o; u++) { + if (a[u].apply(t[0], t[1]) === !1 && e.stopOnFalse) { + n = !1; + break; + } + } + i = !1, a && (f ? f.length && l(f.shift()) : n ? a = [] : c.disable()); + }, + c = { + add: function() { + if (a) { + var t = a.length; + (function r(t) { + v.each(t, function(t, n) { + var i = v.type(n); + i === "function" + ? (!e.unique || !c.has(n)) && a.push(n) + : n && n.length && i !== "string" && r(n); + }); + })(arguments), i ? o = a.length : n && (s = t, l(n)); + } + return this; + }, + remove: function() { + return a && + v.each(arguments, function(e, t) { + var n; + while ( + (n = v.inArray(t, a, n)) > -1 + ) a.splice(n, 1), i && (n <= o && o--, n <= u && u--); + }), this; + }, + has: function(e) { + return v.inArray(e, a) > -1; + }, + empty: function() { + return a = [], this; + }, + disable: function() { + return a = f = n = t, this; + }, + disabled: function() { + return !a; + }, + lock: function() { + return f = t, n || c.disable(), this; + }, + locked: function() { + return !f; + }, + fireWith: function(e, t) { + return t = t || [], t = [e, t.slice ? t.slice() : t], a && + (!r || f) && + (i ? f.push(t) : l(t)), this; + }, + fire: function() { + return c.fireWith(this, arguments), this; + }, + fired: function() { + return !!r; + } + }; + return c; + }, v.extend({ + Deferred: function(e) { + var t = [ + ["resolve", "done", v.Callbacks("once memory"), "resolved"], + ["reject", "fail", v.Callbacks("once memory"), "rejected"], + ["notify", "progress", v.Callbacks("memory")] + ], + n = "pending", + r = { + state: function() { + return n; + }, + always: function() { + return i.done(arguments).fail(arguments), this; + }, + then: function() { + var e = arguments; + return v + .Deferred(function(n) { + v.each(t, function(t, r) { + var s = r[0], o = e[t]; + i[r[1]]( + v.isFunction(o) + ? (function() { + var e = o.apply(this, arguments); + e && v.isFunction(e.promise) + ? e + .promise() + .done(n.resolve) + .fail(n.reject) + .progress(n.notify) + : n[s + "With"](this === i ? n : this, [e]); + }) + : n[s] + ); + }), e = null; + }) + .promise(); + }, + promise: function(e) { + return e != null ? v.extend(e, r) : r; + } + }, + i = {}; + return r.pipe = r.then, v.each(t, function(e, s) { + var o = s[2], u = s[3]; + r[s[1]] = o.add, u && + o.add( + function() { + n = u; + }, + t[e ^ 1][2].disable, + t[2][2].lock + ), i[s[0]] = o.fire, i[s[0] + "With"] = o.fireWith; + }), r.promise(i), e && e.call(i, i), i; + }, + when: function(e) { + var t = 0, + n = l.call(arguments), + r = n.length, + i = r !== 1 || e && v.isFunction(e.promise) ? r : 0, + s = i === 1 ? e : v.Deferred(), + o = function(e, t, n) { + return function(r) { + t[e] = this, n[e] = arguments.length > 1 + ? l.call(arguments) + : r, n === u ? s.notifyWith(t, n) : --i || s.resolveWith(t, n); + }; + }, + u, + a, + f; + if (r > 1) { + u = new Array(r), a = new Array(r), f = new Array(r); + for (; t < r; t++) + n[t] && v.isFunction(n[t].promise) + ? n[t] + .promise() + .done(o(t, f, n)) + .fail(s.reject) + .progress(o(t, a, u)) + : --i; + } + return i || s.resolveWith(f, n), s.promise(); + } + }), v.support = (function() { + var t, n, r, s, o, u, a, f, l, c, h, p = i.createElement("div"); + p.setAttribute( + "className", + "t" + ), p.innerHTML = "
a", n = p.getElementsByTagName( + "*" + ), r = p.getElementsByTagName("a")[0]; + if (!n || !r || !n.length) return {}; + s = i.createElement("select"), o = s.appendChild( + i.createElement("option") + ), u = p.getElementsByTagName("input")[ + 0 + ], r.style.cssText = "top:1px;float:left;opacity:.5", t = { + leadingWhitespace: p.firstChild.nodeType === 3, + tbody: !p.getElementsByTagName("tbody").length, + htmlSerialize: !!p.getElementsByTagName("link").length, + style: /top/.test(r.getAttribute("style")), + hrefNormalized: r.getAttribute("href") === "/a", + opacity: /^0.5/.test(r.style.opacity), + cssFloat: !!r.style.cssFloat, + checkOn: u.value === "on", + optSelected: o.selected, + getSetAttribute: p.className !== "t", + enctype: !!i.createElement("form").enctype, + html5Clone: ( + i.createElement("nav").cloneNode(!0).outerHTML !== "<:nav>" + ), + boxModel: i.compatMode === "CSS1Compat", + submitBubbles: !0, + changeBubbles: !0, + focusinBubbles: !1, + deleteExpando: !0, + noCloneEvent: !0, + inlineBlockNeedsLayout: !1, + shrinkWrapBlocks: !1, + reliableMarginRight: !0, + boxSizingReliable: !0, + pixelPosition: !1 + }, u.checked = !0, t.noCloneChecked = u.cloneNode( + !0 + ).checked, s.disabled = !0, t.optDisabled = !o.disabled; + try { + delete p.test; + } catch (d) { + t.deleteExpando = !1; + } + !p.addEventListener && + p.attachEvent && + p.fireEvent && + (p.attachEvent( + "onclick", + h = function() { + t.noCloneEvent = !1; + } + ), p.cloneNode(!0).fireEvent("onclick"), p.detachEvent( + "onclick", + h + )), u = i.createElement("input"), u.value = "t", u.setAttribute( + "type", + "radio" + ), t.radioValue = u.value === "t", u.setAttribute( + "checked", + "checked" + ), u.setAttribute("name", "t"), p.appendChild( + u + ), a = i.createDocumentFragment(), a.appendChild( + p.lastChild + ), t.checkClone = a + .cloneNode(!0) + .cloneNode( + !0 + ).lastChild.checked, t.appendChecked = u.checked, a.removeChild( + u + ), a.appendChild(p); + if (p.attachEvent) { + for (l in { submit: !0, change: !0, focusin: !0 }) { + f = "on" + l, c = (f in p), c || + (p.setAttribute(f, "return;"), c = typeof p[f] === "function"), t[ + l + "Bubbles" + ] = c; + } + } + return v(function() { + var n, + r, + s, + o, + u = "padding:0;margin:0;border:0;display:block;overflow:hidden;", + a = i.getElementsByTagName("body")[0]; + if (!a) return; + n = i.createElement( + "div" + ), n.style.cssText = "visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px", a.insertBefore(n, a.firstChild), r = i.createElement("div"), n.appendChild(r), r.innerHTML = "
t
", s = r.getElementsByTagName("td"), s[0].style.cssText = "padding:0;margin:0;border:0;display:none", c = s[0].offsetHeight === 0, s[0].style.display = "", s[1].style.display = "none", t.reliableHiddenOffsets = c && s[0].offsetHeight === 0, r.innerHTML = "", r.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;", t.boxSizing = r.offsetWidth === 4, t.doesNotIncludeMarginInBodyOffset = a.offsetTop !== 1, e.getComputedStyle && (t.pixelPosition = (e.getComputedStyle(r, null) || {}).top !== "1%", t.boxSizingReliable = (e.getComputedStyle(r, null) || { width: "4px" }).width === "4px", o = i.createElement("div"), o.style.cssText = r.style.cssText = u, o.style.marginRight = o.style.width = "0", r.style.width = "1px", r.appendChild(o), t.reliableMarginRight = !parseFloat((e.getComputedStyle(o, null) || {}).marginRight)), typeof r.style.zoom !== "undefined" && (r.innerHTML = "", r.style.cssText = u + "width:1px;padding:1px;display:inline;zoom:1", t.inlineBlockNeedsLayout = r.offsetWidth === 3, r.style.display = "block", r.style.overflow = "visible", r.innerHTML = "
", r.firstChild.style.width = "5px", t.shrinkWrapBlocks = r.offsetWidth !== 3, n.style.zoom = 1), a.removeChild(n), n = r = s = o = null; + }), a.removeChild(p), n = r = s = o = u = a = p = null, t; + })(); + var D = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, P = /([A-Z])/g; + v.extend({ + cache: {}, + deletedIds: [], + uuid: 0, + expando: "jQuery" + (v.fn.jquery + Math.random()).replace(/\D/g, ""), + noData: { + embed: !0, + object: "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + applet: !0 + }, + hasData: function(e) { + return e = e.nodeType ? v.cache[e[v.expando]] : e[v.expando], !!e && + !B(e); + }, + data: function(e, n, r, i) { + if (!v.acceptData(e)) return; + var s, + o, + u = v.expando, + a = typeof n === "string", + f = e.nodeType, + l = f ? v.cache : e, + c = f ? e[u] : e[u] && u; + if ((!c || !l[c] || !i && !l[c].data) && a && r === t) return; + c || (f ? e[u] = c = v.deletedIds.pop() || v.guid++ : c = u), l[c] || + (l[c] = {}, f || (l[c].toJSON = v.noop)); + if (typeof n === "object" || typeof n === "function") { + i ? l[c] = v.extend(l[c], n) : l[c].data = v.extend(l[c].data, n); + } + return s = l[c], i || (s.data || (s.data = {}), s = s.data), r !== t && + (s[v.camelCase(n)] = r), a + ? (o = s[n], o == null && (o = s[v.camelCase(n)])) + : o = s, o; + }, + removeData: function(e, t, n) { + if (!v.acceptData(e)) return; + var r, + i, + s, + o = e.nodeType, + u = o ? v.cache : e, + a = o ? e[v.expando] : v.expando; + if (!u[a]) return; + if (t) { + r = n ? u[a] : u[a].data; + if (r) { + v.isArray(t) || + (t in r + ? t = [t] + : (t = v.camelCase(t), t in r ? t = [t] : t = t.split(" "))); + for (i = 0, s = t.length; i < s; i++) + delete r[t[i]]; + if (!(n ? B : v.isEmptyObject)(r)) return; + } + } + if (!n) { + delete u[a].data; + if (!B(u[a])) return; + } + o + ? v.cleanData([e], !0) + : v.support.deleteExpando || u != u.window ? delete u[a] : u[a] = null; + }, + _data: function(e, t, n) { + return v.data(e, t, n, !0); + }, + acceptData: function(e) { + var t = e.nodeName && v.noData[e.nodeName.toLowerCase()]; + return !t || t !== !0 && e.getAttribute("classid") === t; + } + }), v.fn.extend({ + data: function(e, n) { + var r, i, s, o, u, a = this[0], f = 0, l = null; + if (e === t) { + if (this.length) { + l = v.data(a); + if (a.nodeType === 1 && !v._data(a, "parsedAttrs")) { + s = a.attributes; + for (u = s.length; f < u; f++) + o = s[f].name, o.indexOf("data-") || + (o = v.camelCase(o.substring(5)), H(a, o, l[o])); + v._data(a, "parsedAttrs", !0); + } + } + return l; + } + return typeof e === "object" + ? this.each(function() { + v.data(this, e); + }) + : (r = e.split(".", 2), r[1] = r[1] ? "." + r[1] : "", i = r[1] + + "!", v.access( + this, + function(n) { + if (n === t) { + return l = this.triggerHandler("getData" + i, [r[0]]), l === + t && + a && + (l = v.data(a, e), l = H(a, e, l)), l === t && r[1] + ? this.data(r[0]) + : l; + } + r[1] = n, this.each(function() { + var t = v(this); + t.triggerHandler( + "setData" + i, + r + ), v.data(this, e, n), t.triggerHandler("changeData" + i, r); + }); + }, + null, + n, + arguments.length > 1, + null, + !1 + )); + }, + removeData: function(e) { + return this.each(function() { + v.removeData(this, e); + }); + } + }), v.extend({ + queue: function(e, t, n) { + var r; + if (e) { + return t = (t || "fx") + "queue", r = v._data(e, t), n && + (!r || v.isArray(n) + ? r = v._data(e, t, v.makeArray(n)) + : r.push(n)), r || []; + } + }, + dequeue: function(e, t) { + t = t || "fx"; + var n = v.queue(e, t), + r = n.length, + i = n.shift(), + s = v._queueHooks(e, t), + o = function() { + v.dequeue(e, t); + }; + i === "inprogress" && (i = n.shift(), r--), i && + (t === "fx" && n.unshift("inprogress"), delete s.stop, i.call( + e, + o, + s + )), !r && s && s.empty.fire(); + }, + _queueHooks: function(e, t) { + var n = t + "queueHooks"; + return v._data(e, n) || + v._data(e, n, { + empty: v.Callbacks("once memory").add(function() { + v.removeData(e, t + "queue", !0), v.removeData(e, n, !0); + }) + }); + } + }), v.fn.extend({ + queue: function(e, n) { + var r = 2; + return typeof e !== "string" && (n = e, e = "fx", r--), arguments.length < + r + ? v.queue(this[0], e) + : n === t + ? this + : this.each(function() { + var t = v.queue(this, e, n); + v._queueHooks( + this, + e + ), e === "fx" && t[0] !== "inprogress" && v.dequeue(this, e); + }); + }, + dequeue: function(e) { + return this.each(function() { + v.dequeue(this, e); + }); + }, + delay: function(e, t) { + return e = v.fx ? v.fx.speeds[e] || e : e, t = t || + "fx", this.queue(t, function(t, n) { + var r = setTimeout(t, e); + n.stop = function() { + clearTimeout(r); + }; + }); + }, + clearQueue: function(e) { + return this.queue(e || "fx", []); + }, + promise: function(e, n) { + var r, + i = 1, + s = v.Deferred(), + o = this, + u = this.length, + a = function() { + --i || s.resolveWith(o, [o]); + }; + typeof e !== "string" && (n = e, e = t), e = e || "fx"; + while (u--) { + r = v._data(o[u], e + "queueHooks"), r && + r.empty && + (i++, r.empty.add(a)); + } + return a(), s.promise(n); + } + }); + var j, + F, + I, + q = /[\t\r\n]/g, + R = /\r/g, + U = /^(?:button|input)$/i, + z = /^(?:button|input|object|select|textarea)$/i, + W = /^a(?:rea|)$/i, + X = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + V = v.support.getSetAttribute; + v.fn.extend({ + attr: function(e, t) { + return v.access(this, v.attr, e, t, arguments.length > 1); + }, + removeAttr: function(e) { + return this.each(function() { + v.removeAttr(this, e); + }); + }, + prop: function(e, t) { + return v.access(this, v.prop, e, t, arguments.length > 1); + }, + removeProp: function(e) { + return e = v.propFix[e] || e, this.each(function() { + try { + this[e] = t, delete this[e]; + } catch (n) { + } + }); + }, + addClass: function(e) { + var t, n, r, i, s, o, u; + if (v.isFunction(e)) { + return this.each(function(t) { + v(this).addClass(e.call(this, t, this.className)); + }); + } + if (e && typeof e === "string") { + t = e.split(y); + for (n = 0, r = this.length; n < r; n++) { + i = this[n]; + if (i.nodeType === 1) { + if (!i.className && t.length === 1) + i.className = e; + else { + s = " " + i.className + " "; + for (o = 0, u = t.length; o < u; o++) + s.indexOf(" " + t[o] + " ") < 0 && (s += t[o] + " "); + i.className = v.trim(s); + } + } + } + } + return this; + }, + removeClass: function(e) { + var n, r, i, s, o, u, a; + if (v.isFunction(e)) { + return this.each(function(t) { + v(this).removeClass(e.call(this, t, this.className)); + }); + } + if (e && typeof e === "string" || e === t) { + n = (e || "").split(y); + for (u = 0, a = this.length; u < a; u++) { + i = this[u]; + if (i.nodeType === 1 && i.className) { + r = (" " + i.className + " ").replace(q, " "); + for (s = 0, o = n.length; s < o; s++) + while (r.indexOf(" " + n[s] + " ") >= 0) + r = r.replace(" " + n[s] + " ", " "); + i.className = e ? v.trim(r) : ""; + } + } + } + return this; + }, + toggleClass: function(e, t) { + var n = typeof e, r = typeof t === "boolean"; + return v.isFunction(e) + ? this.each(function(n) { + v(this).toggleClass(e.call(this, n, this.className, t), t); + }) + : this.each(function() { + if (n === "string") { + var i, s = 0, o = v(this), u = t, a = e.split(y); + while (i = a[s++]) + u = r ? u : !o.hasClass(i), o[u ? "addClass" : "removeClass"]( + i + ); + } else if (n === "undefined" || n === "boolean") { + this.className && + v._data( + this, + "__className__", + this.className + ), this.className = this.className || e === !1 + ? "" + : v._data(this, "__className__") || ""; + } + }); + }, + hasClass: function(e) { + var t = " " + e + " ", n = 0, r = this.length; + for (; n < r; n++) + if ( + this[n].nodeType === 1 && + (" " + this[n].className + " ").replace(q, " ").indexOf(t) >= 0 + ) + return !0; + return !1; + }, + val: function(e) { + var n, r, i, s = this[0]; + if (!arguments.length) { + if (s) { + return n = v.valHooks[s.type] || + v.valHooks[s.nodeName.toLowerCase()], n && + "get" in n && + (r = n.get(s, "value")) !== t + ? r + : (r = s.value, typeof r === "string" + ? r.replace(R, "") + : r == null ? "" : r); + } + return; + } + return i = v.isFunction(e), this.each(function(r) { + var s, o = v(this); + if (this.nodeType !== 1) return; + i ? s = e.call(this, r, o.val()) : s = e, s == null + ? s = "" + : typeof s === "number" + ? s += "" + : v.isArray(s) && + (s = v.map(s, function(e) { + return e == null ? "" : e + ""; + })), n = v.valHooks[this.type] || + v.valHooks[this.nodeName.toLowerCase()]; + if (!n || !("set" in n) || n.set(this, s, "value") === t) { + this.value = s; + } + }); + } + }), v.extend({ + valHooks: { + option: { + get: function(e) { + var t = e.attributes.value; + return !t || t.specified ? e.value : e.text; + } + }, + select: { + get: function(e) { + var t, + n, + r = e.options, + i = e.selectedIndex, + s = e.type === "select-one" || i < 0, + o = s ? null : [], + u = s ? i + 1 : r.length, + a = i < 0 ? u : s ? i : 0; + for (; a < u; a++) { + n = r[a]; + if ( + (n.selected || a === i) && + (v.support.optDisabled + ? !n.disabled + : n.getAttribute("disabled") === null) && + (!n.parentNode.disabled || + !v.nodeName(n.parentNode, "optgroup")) + ) { + t = v(n).val(); + if (s) return t; + o.push(t); + } + } + return o; + }, + set: function(e, t) { + var n = v.makeArray(t); + return v(e).find("option").each(function() { + this.selected = v.inArray(v(this).val(), n) >= 0; + }), n.length || (e.selectedIndex = -1), n; + } + } + }, + attrFn: {}, + attr: function(e, n, r, i) { + var s, o, u, a = e.nodeType; + if (!e || a === 3 || a === 8 || a === 2) return; + if (i && v.isFunction(v.fn[n])) return v(e)[n](r); + if (typeof e.getAttribute === "undefined") return v.prop(e, n, r); + u = a !== 1 || !v.isXMLDoc(e), u && + (n = n.toLowerCase(), o = v.attrHooks[n] || (X.test(n) ? F : j)); + if (r !== t) { + if (r === null) { + v.removeAttr(e, n); + return; + } + return o && "set" in o && u && (s = o.set(e, r, n)) !== t + ? s + : (e.setAttribute(n, r + ""), r); + } + return o && "get" in o && u && (s = o.get(e, n)) !== null + ? s + : (s = e.getAttribute(n), s === null ? t : s); + }, + removeAttr: function(e, t) { + var n, r, i, s, o = 0; + if (t && e.nodeType === 1) { + r = t.split(y); + for (; o < r.length; o++) + i = r[o], i && + (n = v.propFix[i] || i, s = X.test(i), s || + v.attr(e, i, ""), e.removeAttribute(V ? i : n), s && + n in e && + (e[n] = !1)); + } + }, + attrHooks: { + type: { + set: function(e, t) { + if (U.test(e.nodeName) && e.parentNode) { + v.error("type property can't be changed"); + } else if ( + !v.support.radioValue && t === "radio" && v.nodeName(e, "input") + ) { + var n = e.value; + return e.setAttribute("type", t), n && (e.value = n), t; + } + } + }, + value: { + get: function(e, t) { + return j && v.nodeName(e, "button") + ? j.get(e, t) + : t in e ? e.value : null; + }, + set: function(e, t, n) { + if (j && v.nodeName(e, "button")) return j.set(e, t, n); + e.value = t; + } + } + }, + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + for: "htmlFor", + class: "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + prop: function(e, n, r) { + var i, s, o, u = e.nodeType; + if (!e || u === 3 || u === 8 || u === 2) return; + return o = u !== 1 || !v.isXMLDoc(e), o && + (n = v.propFix[n] || n, s = v.propHooks[n]), r !== t + ? s && "set" in s && (i = s.set(e, r, n)) !== t ? i : e[n] = r + : s && "get" in s && (i = s.get(e, n)) !== null ? i : e[n]; + }, + propHooks: { + tabIndex: { + get: function(e) { + var n = e.getAttributeNode("tabindex"); + return n && n.specified + ? parseInt(n.value, 10) + : z.test(e.nodeName) || W.test(e.nodeName) && e.href ? 0 : t; + } + } + } + }), F = { + get: function(e, n) { + var r, i = v.prop(e, n); + return i === !0 || + typeof i !== "boolean" && + (r = e.getAttributeNode(n)) && + r.nodeValue !== !1 + ? n.toLowerCase() + : t; + }, + set: function(e, t, n) { + var r; + return t === !1 + ? v.removeAttr(e, n) + : (r = v.propFix[n] || n, r in e && (e[r] = !0), e.setAttribute( + n, + n.toLowerCase() + )), n; + } + }, V || + (I = { name: !0, id: !0, coords: !0 }, j = v.valHooks.button = { + get: function(e, n) { + var r; + return r = e.getAttributeNode(n), r && + (I[n] ? r.value !== "" : r.specified) + ? r.value + : t; + }, + set: function(e, t, n) { + var r = e.getAttributeNode(n); + return r || + (r = i.createAttribute(n), e.setAttributeNode(r)), r.value = t + ""; + } + }, v.each(["width", "height"], function(e, t) { + v.attrHooks[t] = v.extend(v.attrHooks[t], { + set: function(e, n) { + if (n === "") return e.setAttribute(t, "auto"), n; + } + }); + }), v.attrHooks.contenteditable = { + get: j.get, + set: function(e, t, n) { + t === "" && (t = "false"), j.set(e, t, n); + } + }), v.support.hrefNormalized || + v.each(["href", "src", "width", "height"], function(e, n) { + v.attrHooks[n] = v.extend(v.attrHooks[n], { + get: function(e) { + var r = e.getAttribute(n, 2); + return r === null ? t : r; + } + }); + }), v.support.style || + (v.attrHooks.style = { + get: function(e) { + return e.style.cssText.toLowerCase() || t; + }, + set: function(e, t) { + return e.style.cssText = t + ""; + } + }), v.support.optSelected || + (v.propHooks.selected = v.extend(v.propHooks.selected, { + get: function(e) { + var t = e.parentNode; + return t && + (t.selectedIndex, t.parentNode && t.parentNode.selectedIndex), null; + } + })), v.support.enctype || + (v.propFix.enctype = "encoding"), v.support.checkOn || + v.each(["radio", "checkbox"], function() { + v.valHooks[this] = { + get: function(e) { + return e.getAttribute("value") === null ? "on" : e.value; + } + }; + }), v.each(["radio", "checkbox"], function() { + v.valHooks[this] = v.extend(v.valHooks[this], { + set: function(e, t) { + if (v.isArray(t)) return e.checked = v.inArray(v(e).val(), t) >= 0; + } + }); + }); + var $ = /^(?:textarea|input|select)$/i, + J = /^([^\.]*|)(?:\.(.+)|)$/, + K = /(?:^|\s)hover(\.\S+|)\b/, + Q = /^key/, + G = /^(?:mouse|contextmenu)|click/, + Y = /^(?:focusinfocus|focusoutblur)$/, + Z = function(e) { + return v.event.special.hover + ? e + : e.replace(K, "mouseenter$1 mouseleave$1"); + }; + v.event = { + add: function(e, n, r, i, s) { + var o, u, a, f, l, c, h, p, d, m, g; + if ( + e.nodeType === 3 || e.nodeType === 8 || !n || !r || !(o = v._data(e)) + ) { + return; + } + r.handler && (d = r, r = d.handler, s = d.selector), r.guid || + (r.guid = v.guid++), a = o.events, a || + (o.events = a = {}), u = o.handle, u || + (o.handle = u = function(e) { + return typeof v === "undefined" || !!e && v.event.triggered === e.type + ? t + : v.event.dispatch.apply(u.elem, arguments); + }, u.elem = e), n = v.trim(Z(n)).split(" "); + for (f = 0; f < n.length; f++) { + l = J.exec(n[f]) || [], c = l[1], h = (l[2] || "") + .split(".") + .sort(), g = v.event.special[c] || {}, c = (s + ? g.delegateType + : g.bindType) || + c, g = v.event.special[c] || {}, p = v.extend( + { + type: c, + origType: l[1], + data: i, + handler: r, + guid: r.guid, + selector: s, + needsContext: s && v.expr.match.needsContext.test(s), + namespace: h.join(".") + }, + d + ), m = a[c]; + if (!m) { + m = a[c] = [], m.delegateCount = 0; + if (!g.setup || g.setup.call(e, i, h, u) === !1) { + e.addEventListener + ? e.addEventListener(c, u, !1) + : e.attachEvent && e.attachEvent("on" + c, u); + } + } + g.add && + (g.add.call(e, p), p.handler.guid || (p.handler.guid = r.guid)), s + ? m.splice((m.delegateCount++), 0, p) + : m.push(p), v.event.global[c] = !0; + } + e = null; + }, + global: {}, + remove: function(e, t, n, r, i) { + var s, o, u, a, f, l, c, h, p, d, m, g = v.hasData(e) && v._data(e); + if (!g || !(h = g.events)) return; + t = v.trim(Z(t || "")).split(" "); + for (s = 0; s < t.length; s++) { + o = J.exec(t[s]) || [], u = a = o[1], f = o[2]; + if (!u) { + for (u in h) + v.event.remove(e, u + t[s], n, r, !0); + continue; + } + p = v.event.special[u] || {}, u = (r ? p.delegateType : p.bindType) || + u, d = h[u] || [], l = d.length, f = f + ? new RegExp( + "(^|\\.)" + f.split(".").sort().join("\\.(?:.*\\.|)") + "(\\.|$)" + ) + : null; + for (c = 0; c < d.length; c++) + m = d[c], (i || a === m.origType) && + (!n || n.guid === m.guid) && + (!f || f.test(m.namespace)) && + (!r || r === m.selector || r === "**" && m.selector) && + (d.splice((c--), 1), m.selector && d.delegateCount--, p.remove && + p.remove.call(e, m)); + d.length === 0 && + l !== d.length && + ((!p.teardown || p.teardown.call(e, f, g.handle) === !1) && + v.removeEvent(e, u, g.handle), delete h[u]); + } + v.isEmptyObject(h) && (delete g.handle, v.removeData(e, "events", !0)); + }, + customEvent: { getData: !0, setData: !0, changeData: !0 }, + trigger: function(n, r, s, o) { + if (!s || s.nodeType !== 3 && s.nodeType !== 8) { + var u, a, f, l, c, h, p, d, m, g, y = n.type || n, b = []; + if (Y.test(y + v.event.triggered)) return; + y.indexOf("!") >= 0 && (y = y.slice(0, -1), a = !0), y.indexOf(".") >= + 0 && + (b = y.split("."), y = b.shift(), b.sort()); + if ((!s || v.event.customEvent[y]) && !v.event.global[y]) return; + n = typeof n === "object" + ? n[v.expando] ? n : new v.Event(y, n) + : new v.Event( + y + ), n.type = y, n.isTrigger = !0, n.exclusive = a, n.namespace = b.join( + "." + ), n.namespace_re = n.namespace + ? new RegExp("(^|\\.)" + b.join("\\.(?:.*\\.|)") + "(\\.|$)") + : null, h = y.indexOf(":") < 0 ? "on" + y : ""; + if (!s) { + u = v.cache; + for (f in u) { + u[f].events && + u[f].events[y] && + v.event.trigger(n, r, u[f].handle.elem, !0); + } + return; + } + n.result = t, n.target || (n.target = s), r = r != null + ? v.makeArray(r) + : [], r.unshift(n), p = v.event.special[y] || {}; + if (p.trigger && p.trigger.apply(s, r) === !1) return; + m = [[s, p.bindType || y]]; + if (!o && !p.noBubble && !v.isWindow(s)) { + g = p.delegateType || y, l = Y.test(g + y) ? s : s.parentNode; + for (c = s; l; l = l.parentNode) + m.push([l, g]), c = l; + c === (s.ownerDocument || i) && + m.push([c.defaultView || c.parentWindow || e, g]); + } + for (f = 0; f < m.length && !n.isPropagationStopped(); f++) + l = m[f][0], n.type = m[f][1], d = (v._data(l, "events") || {})[ + n.type + ] && + v._data(l, "handle"), d && d.apply(l, r), d = h && l[h], d && + v.acceptData(l) && + d.apply && + d.apply(l, r) === !1 && + n.preventDefault(); + return n.type = y, !o && + !n.isDefaultPrevented() && + (!p._default || p._default.apply(s.ownerDocument, r) === !1) && + (y !== "click" || !v.nodeName(s, "a")) && + v.acceptData(s) && + h && + s[y] && + (y !== "focus" && y !== "blur" || n.target.offsetWidth !== 0) && + !v.isWindow(s) && + (c = s[h], c && (s[h] = null), v.event.triggered = y, s[ + y + ](), v.event.triggered = t, c && (s[h] = c)), n.result; + } + return; + }, + dispatch: function(n) { + n = v.event.fix(n || e.event); + var r, + i, + s, + o, + u, + a, + f, + c, + h, + p, + d = (v._data(this, "events") || {})[n.type] || [], + m = d.delegateCount, + g = l.call(arguments), + y = !n.exclusive && !n.namespace, + b = v.event.special[n.type] || {}, + w = []; + g[0] = n, n.delegateTarget = this; + if (b.preDispatch && b.preDispatch.call(this, n) === !1) return; + if (m && (!n.button || n.type !== "click")) { + for (s = n.target; s != this; s = s.parentNode || this) { + if (s.disabled !== !0 || n.type !== "click") { + u = {}, f = []; + for (r = 0; r < m; r++) + c = d[r], h = c.selector, u[h] === t && + (u[h] = c.needsContext + ? v(h, this).index(s) >= 0 + : v.find(h, this, null, [s]).length), u[h] && f.push(c); + f.length && w.push({ elem: s, matches: f }); + } + } + } + d.length > m && w.push({ elem: this, matches: d.slice(m) }); + for (r = 0; r < w.length && !n.isPropagationStopped(); r++) { + a = w[r], n.currentTarget = a.elem; + for ( + i = 0; + i < a.matches.length && !n.isImmediatePropagationStopped(); + i++ + ) { + c = a.matches[i]; + if ( + y || + !n.namespace && !c.namespace || + n.namespace_re && n.namespace_re.test(c.namespace) + ) { + n.data = c.data, n.handleObj = c, o = ((v.event.special[ + c.origType + ] || + {}).handle || + c.handler).apply(a.elem, g), o !== t && + (n.result = o, o === !1 && + (n.preventDefault(), n.stopPropagation())); + } + } + } + return b.postDispatch && b.postDispatch.call(this, n), n.result; + }, + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split( + " " + ), + fixHooks: {}, + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function(e, t) { + return e.which == null && + (e.which = t.charCode != null ? t.charCode : t.keyCode), e; + } + }, + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split( + " " + ), + filter: function(e, n) { + var r, s, o, u = n.button, a = n.fromElement; + return e.pageX == null && + n.clientX != null && + (r = e.target.ownerDocument || + i, s = r.documentElement, o = r.body, e.pageX = n.clientX + + (s && s.scrollLeft || o && o.scrollLeft || 0) - + (s && s.clientLeft || o && o.clientLeft || 0), e.pageY = n.clientY + + (s && s.scrollTop || o && o.scrollTop || 0) - + (s && s.clientTop || o && o.clientTop || 0)), !e.relatedTarget && + a && + (e.relatedTarget = a === e.target ? n.toElement : a), !e.which && + u !== t && + (e.which = u & 1 ? 1 : u & 2 ? 3 : u & 4 ? 2 : 0), e; + } + }, + fix: function(e) { + if (e[v.expando]) return e; + var t, + n, + r = e, + s = v.event.fixHooks[e.type] || {}, + o = s.props ? this.props.concat(s.props) : this.props; + e = v.Event(r); + for (t = o.length; t; ) + n = o[--t], e[n] = r[n]; + return e.target || (e.target = r.srcElement || i), e.target.nodeType === + 3 && + (e.target = e.target.parentNode), e.metaKey = !!e.metaKey, s.filter + ? s.filter(e, r) + : e; + }, + special: { + load: { noBubble: !0 }, + focus: { delegateType: "focusin" }, + blur: { delegateType: "focusout" }, + beforeunload: { + setup: function(e, t, n) { + v.isWindow(this) && (this.onbeforeunload = n); + }, + teardown: function(e, t) { + this.onbeforeunload === t && (this.onbeforeunload = null); + } + } + }, + simulate: function(e, t, n, r) { + var i = v.extend(new v.Event(), n, { + type: e, + isSimulated: !0, + originalEvent: {} + }); + r + ? v.event.trigger(i, null, t) + : v.event.dispatch.call(t, i), i.isDefaultPrevented() && + n.preventDefault(); + } + }, v.event.handle = v.event.dispatch, v.removeEvent = i.removeEventListener + ? (function(e, t, n) { + e.removeEventListener && e.removeEventListener(t, n, !1); + }) + : (function(e, t, n) { + var r = "on" + t; + e.detachEvent && + (typeof e[r] === "undefined" && (e[r] = null), e.detachEvent(r, n)); + }), v.Event = function(e, t) { + if (!(this instanceof v.Event)) return new v.Event(e, t); + e && e.type + ? (this.originalEvent = e, this.type = e.type, this.isDefaultPrevented = e.defaultPrevented || + e.returnValue === !1 || + e.getPreventDefault && e.getPreventDefault() + ? tt + : et) + : this.type = e, t && v.extend(this, t), this.timeStamp = e && + e.timeStamp || + v.now(), this[v.expando] = !0; + }, v.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = tt; + var e = this.originalEvent; + if (!e) return; + e.preventDefault ? e.preventDefault() : e.returnValue = !1; + }, + stopPropagation: function() { + this.isPropagationStopped = tt; + var e = this.originalEvent; + if (!e) return; + e.stopPropagation && e.stopPropagation(), e.cancelBubble = !0; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = tt, this.stopPropagation(); + }, + isDefaultPrevented: et, + isPropagationStopped: et, + isImmediatePropagationStopped: et + }, v.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( + e, + t + ) { + v.event.special[e] = { + delegateType: t, + bindType: t, + handle: function(e) { + var n, r = this, i = e.relatedTarget, s = e.handleObj, o = s.selector; + if (!i || i !== r && !v.contains(r, i)) { + e.type = s.origType, n = s.handler.apply(this, arguments), e.type = t; + } + return n; + } + }; + }), v.support.submitBubbles || + (v.event.special.submit = { + setup: function() { + if (v.nodeName(this, "form")) return !1; + v.event.add(this, "click._submit keypress._submit", function(e) { + var n = e.target, + r = v.nodeName(n, "input") || v.nodeName(n, "button") ? n.form : t; + r && + !v._data(r, "_submit_attached") && + (v.event.add(r, "submit._submit", function(e) { + e._submit_bubble = !0; + }), v._data(r, "_submit_attached", !0)); + }); + }, + postDispatch: function(e) { + e._submit_bubble && + (delete e._submit_bubble, this.parentNode && + !e.isTrigger && + v.event.simulate("submit", this.parentNode, e, !0)); + }, + teardown: function() { + if (v.nodeName(this, "form")) return !1; + v.event.remove(this, "._submit"); + } + }), v.support.changeBubbles || + (v.event.special.change = { + setup: function() { + if ($.test(this.nodeName)) { + if (this.type === "checkbox" || this.type === "radio") { + v.event.add(this, "propertychange._change", function(e) { + e.originalEvent.propertyName === "checked" && + (this._just_changed = !0); + }), v.event.add(this, "click._change", function(e) { + this._just_changed && + !e.isTrigger && + (this._just_changed = !1), v.event.simulate("change", this, e, !0); + }); + } + return !1; + } + v.event.add(this, "beforeactivate._change", function(e) { + var t = e.target; + $.test(t.nodeName) && + !v._data(t, "_change_attached") && + (v.event.add(t, "change._change", function(e) { + this.parentNode && + !e.isSimulated && + !e.isTrigger && + v.event.simulate("change", this.parentNode, e, !0); + }), v._data(t, "_change_attached", !0)); + }); + }, + handle: function(e) { + var t = e.target; + if ( + this !== t || + e.isSimulated || + e.isTrigger || + t.type !== "radio" && t.type !== "checkbox" + ) { + return e.handleObj.handler.apply(this, arguments); + } + }, + teardown: function() { + return v.event.remove(this, "._change"), !$.test(this.nodeName); + } + }), v.support.focusinBubbles || + v.each({ focus: "focusin", blur: "focusout" }, function(e, t) { + var n = 0, + r = function(e) { + v.event.simulate(t, e.target, v.event.fix(e), !0); + }; + v.event.special[t] = { + setup: function() { + n++ === 0 && i.addEventListener(e, r, !0); + }, + teardown: function() { + --n === 0 && i.removeEventListener(e, r, !0); + } + }; + }), v.fn.extend({ + on: function(e, n, r, i, s) { + var o, u; + if (typeof e === "object") { + typeof n !== "string" && (r = r || n, n = t); + for (u in e) + this.on(u, n, r, e[u], s); + return this; + } + r == null && i == null + ? (i = n, r = n = t) + : i == null && + (typeof n === "string" ? (i = r, r = t) : (i = r, r = n, n = t)); + if (i === !1) i = et; + else if (!i) return this; + return s === 1 && + (o = i, i = function(e) { + return v().off(e), o.apply(this, arguments); + }, i.guid = o.guid || (o.guid = v.guid++)), this.each(function() { + v.event.add(this, e, i, r, n); + }); + }, + one: function(e, t, n, r) { + return this.on(e, t, n, r, 1); + }, + off: function(e, n, r) { + var i, s; + if (e && e.preventDefault && e.handleObj) { + return i = e.handleObj, v(e.delegateTarget).off( + i.namespace ? i.origType + "." + i.namespace : i.origType, + i.selector, + i.handler + ), this; + } + if (typeof e === "object") { + for (s in e) + this.off(s, n, e[s]); + return this; + } + if (n === !1 || typeof n === "function") r = n, n = t; + return r === !1 && (r = et), this.each(function() { + v.event.remove(this, e, r, n); + }); + }, + bind: function(e, t, n) { + return this.on(e, null, t, n); + }, + unbind: function(e, t) { + return this.off(e, null, t); + }, + live: function(e, t, n) { + return v(this.context).on(e, this.selector, t, n), this; + }, + die: function(e, t) { + return v(this.context).off(e, this.selector || "**", t), this; + }, + delegate: function(e, t, n, r) { + return this.on(t, e, n, r); + }, + undelegate: function(e, t, n) { + return arguments.length === 1 + ? this.off(e, "**") + : this.off(t, e || "**", n); + }, + trigger: function(e, t) { + return this.each(function() { + v.event.trigger(e, t, this); + }); + }, + triggerHandler: function(e, t) { + if (this[0]) return v.event.trigger(e, t, this[0], !0); + }, + toggle: function(e) { + var t = arguments, + n = e.guid || v.guid++, + r = 0, + i = function(n) { + var i = (v._data(this, "lastToggle" + e.guid) || 0) % r; + return v._data( + this, + "lastToggle" + e.guid, + i + 1 + ), n.preventDefault(), t[i].apply(this, arguments) || !1; + }; + i.guid = n; + while (r < t.length) + t[r++].guid = n; + return this.click(i); + }, + hover: function(e, t) { + return this.mouseenter(e).mouseleave(t || e); + } + }), v.each( + "blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split( + " " + ), + function(e, t) { + v.fn[t] = function(e, n) { + return n == null && (n = e, e = null), arguments.length > 0 + ? this.on(t, null, e, n) + : this.trigger(t); + }, Q.test(t) && (v.event.fixHooks[t] = v.event.keyHooks), G.test(t) && + (v.event.fixHooks[t] = v.event.mouseHooks); + } + ), (function(e, t) { + function nt(e, t, n, r) { + n = n || [], t = t || g; + var i, s, a, f, l = t.nodeType; + if (!e || typeof e !== "string") return n; + if (l !== 1 && l !== 9) return []; + a = o(t); + if (!a && !r) { + if (i = R.exec(e)) { + if (f = i[1]) { + if (l === 9) { + s = t.getElementById(f); + if (!s || !s.parentNode) return n; + if (s.id === f) return n.push(s), n; + } else if ( + t.ownerDocument && + (s = t.ownerDocument.getElementById(f)) && + u(t, s) && + s.id === f + ) { + return n.push(s), n; + } + } else { + if (i[2]) { + return S.apply(n, x.call(t.getElementsByTagName(e), 0)), n; + } + if ((f = i[3]) && Z && t.getElementsByClassName) { + return S.apply(n, x.call(t.getElementsByClassName(f), 0)), n; + } + } + } + } + return vt(e.replace(j, "$1"), t, n, r, a); + } + function rt(e) { + return function(t) { + var n = t.nodeName.toLowerCase(); + return n === "input" && t.type === e; + }; + } + function it(e) { + return function(t) { + var n = t.nodeName.toLowerCase(); + return (n === "input" || n === "button") && t.type === e; + }; + } + function st(e) { + return N(function(t) { + return t = +t, N(function(n, r) { + var i, s = e([], n.length, t), o = s.length; + while (o--) n[i = s[o]] && (n[i] = !(r[i] = n[i])); + }); + }); + } + function ot(e, t, n) { + if (e === t) return n; + var r = e.nextSibling; + while (r) { + if (r === t) return -1; + r = r.nextSibling; + } + return 1; + } + function ut(e, t) { + var n, r, s, o, u, a, f, l = L[d][e + " "]; + if (l) return t ? 0 : l.slice(0); + u = e, a = [], f = i.preFilter; + while (u) { + if (!n || (r = F.exec(u))) { + r && (u = u.slice(r[0].length) || u), a.push(s = []); + } + n = !1; + if (r = I.exec(u)) { + s.push(n = new m(r.shift())), u = u.slice(n.length), n.type = r[ + 0 + ].replace(j, " "); + } + for (o in i.filter) { + (r = J[o].exec(u)) && + (!f[o] || (r = f[o](r))) && + (s.push(n = new m(r.shift())), u = u.slice( + n.length + ), n.type = o, n.matches = r); + } + if (!n) break; + } + return t ? u.length : u ? nt.error(e) : L(e, a).slice(0); + } + function at(e, t, r) { + var i = t.dir, s = r && t.dir === "parentNode", o = w++; + return t.first + ? (function(t, n, r) { + while (t = t[i]) + if (s || t.nodeType === 1) return e(t, n, r); + }) + : (function(t, r, u) { + if (!u) { + var a, f = b + " " + o + " ", l = f + n; + while (t = t[i]) { + if (s || t.nodeType === 1) { + if ((a = t[d]) === l) return t.sizset; + if (typeof a === "string" && a.indexOf(f) === 0) { + if (t.sizset) return t; + } else { + t[d] = l; + if (e(t, r, u)) return t.sizset = !0, t; + t.sizset = !1; + } + } + } + } else + while (t = t[i]) + if (s || t.nodeType === 1) if (e(t, r, u)) return t; + }); + } + function ft(e) { + return e.length > 1 + ? (function(t, n, r) { + var i = e.length; + while (i--) + if (!e[i](t, n, r)) return !1; + return !0; + }) + : e[0]; + } + function lt(e, t, n, r, i) { + var s, o = [], u = 0, a = e.length, f = t != null; + for (; u < a; u++) { + if (s = e[u]) if (!n || n(s, r, i)) o.push(s), f && t.push(u); + } + return o; + } + function ct(e, t, n, r, i, s) { + return r && !r[d] && (r = ct(r)), i && + !i[d] && + (i = ct(i, s)), N(function(s, o, u, a) { + var f, + l, + c, + h = [], + p = [], + d = o.length, + v = s || dt(t || "*", u.nodeType ? [u] : u, []), + m = e && (s || !t) ? lt(v, h, e, u, a) : v, + g = n ? i || (s ? e : d || r) ? [] : o : m; + n && n(m, g, u, a); + if (r) { + f = lt(g, p), r(f, [], u, a), l = f.length; + while (l--) + if (c = f[l]) g[p[l]] = !(m[p[l]] = c); + } + if (s) { + if (i || e) { + if (i) { + f = [], l = g.length; + while (l--) + (c = g[l]) && f.push(m[l] = c); + i(null, g = [], f, a); + } + l = g.length; + while (l--) { + (c = g[l]) && + (f = i ? T.call(s, c) : h[l]) > -1 && + (s[f] = !(o[f] = c)); + } + } + } else { + g = lt(g === o ? g.splice(d, g.length) : g), i + ? i(null, o, g, a) + : S.apply(o, g); + } + }); + } + function ht(e) { + var t, + n, + r, + s = e.length, + o = i.relative[e[0].type], + u = o || i.relative[" "], + a = o ? 1 : 0, + f = at( + function(e) { + return e === t; + }, + u, + !0 + ), + l = at( + function(e) { + return T.call(t, e) > -1; + }, + u, + !0 + ), + h = [ + function(e, n, r) { + return !o && (r || n !== c) || + ((t = n).nodeType ? f(e, n, r) : l(e, n, r)); + } + ]; + for (; a < s; a++) { + if (n = i.relative[e[a].type]) + h = [at(ft(h), n)]; + else { + n = i.filter[e[a].type].apply(null, e[a].matches); + if (n[d]) { + r = ++a; + for (; r < s; r++) + if (i.relative[e[r].type]) break; + return ct( + a > 1 && ft(h), + a > 1 && e.slice(0, a - 1).join("").replace(j, "$1"), + n, + a < r && ht(e.slice(a, r)), + r < s && ht(e = e.slice(r)), + r < s && e.join("") + ); + } + h.push(n); + } + } + return ft(h); + } + function pt(e, t) { + var r = t.length > 0, + s = e.length > 0, + o = function(u, a, f, l, h) { + var p, + d, + v, + m = [], + y = 0, + w = "0", + x = u && [], + T = h != null, + N = c, + C = u || s && i.find.TAG("*", h && a.parentNode || a), + k = b += N == null ? 1 : Math.E; + T && (c = a !== g && a, n = o.el); + for (; (p = C[w]) != null; w++) { + if (s && p) { + for (d = 0; v = e[d]; d++) { + if (v(p, a, f)) { + l.push(p); + break; + } + } + T && (b = k, n = ++o.el); + } + r && ((p = !v && p) && y--, u && x.push(p)); + } + y += w; + if (r && w !== y) { + for (d = 0; v = t[d]; d++) + v(x, m, a, f); + if (u) { + if (y > 0) while (w--) !x[w] && !m[w] && (m[w] = E.call(l)); + m = lt(m); + } + S.apply(l, m), T && + !u && + m.length > 0 && + y + t.length > 1 && + nt.uniqueSort(l); + } + return T && (b = k, c = N), x; + }; + return o.el = 0, r ? N(o) : o; + } + function dt(e, t, n) { + var r = 0, i = t.length; + for (; r < i; r++) { + nt(e, t[r], n); + } + return n; + } + function vt(e, t, n, r, s) { + var o, u, f, l, c, h = ut(e), p = h.length; + if (!r && h.length === 1) { + u = h[0] = h[0].slice(0); + if ( + u.length > 2 && + (f = u[0]).type === "ID" && + t.nodeType === 9 && + !s && + i.relative[u[1].type] + ) { + t = i.find.ID(f.matches[0].replace($, ""), t, s)[0]; + if (!t) return n; + e = e.slice(u.shift().length); + } + for (o = J.POS.test(e) ? -1 : u.length - 1; o >= 0; o--) { + f = u[o]; + if (i.relative[l = f.type]) break; + if (c = i.find[l]) { + if ( + r = c( + f.matches[0].replace($, ""), + z.test(u[0].type) && t.parentNode || t, + s + ) + ) { + u.splice(o, 1), e = r.length && u.join(""); + if (!e) return S.apply(n, x.call(r, 0)), n; + break; + } + } + } + } + return a(e, h)(r, t, s, n, z.test(e)), n; + } + function mt() {} + var n, + r, + i, + s, + o, + u, + a, + f, + l, + c, + h = !0, + p = "undefined", + d = ("sizcache" + Math.random()).replace(".", ""), + m = String, + g = e.document, + y = g.documentElement, + b = 0, + w = 0, + E = [].pop, + S = [].push, + x = [].slice, + T = [].indexOf || + function(e) { + var t = 0, n = this.length; + for (; t < n; t++) + if (this[t] === e) return t; + return -1; + }, + N = function(e, t) { + return e[d] = t == null || t, e; + }, + C = function() { + var e = {}, t = []; + return N( + function(n, r) { + return t.push(n) > i.cacheLength && delete e[t.shift()], e[ + n + " " + ] = r; + }, + e + ); + }, + k = C(), + L = C(), + A = C(), + O = "[\\x20\\t\\r\\n\\f]", + M = "(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+", + _ = M.replace("w", "w#"), + D = "([*^$|!~]?=)", + P = "\\[" + + O + + "*(" + + M + + ")" + + O + + "*(?:" + + D + + O + + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + + _ + + ")|)|)" + + O + + "*\\]", + H = ":(" + + M + + ")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:" + + P + + ")|[^:]|\\\\.)*|.*))\\)|)", + B = ":(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + O + + "*((?:-\\d)?\\d*)" + + O + + "*\\)|)(?=[^-]|$)", + j = new RegExp("^" + O + "+|((?:^|[^\\\\])(?:\\\\.)*)" + O + "+$", "g"), + F = new RegExp("^" + O + "*," + O + "*"), + I = new RegExp("^" + O + "*([\\x20\\t\\r\\n\\f>+~])" + O + "*"), + q = new RegExp(H), + R = /^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/, + U = /^:not/, + z = /[\x20\t\r\n\f]*[+~]/, + W = /:not\($/, + X = /h\d/i, + V = /input|select|textarea|button/i, + $ = /\\(?!\\)/g, + J = { + ID: new RegExp("^#(" + M + ")"), + CLASS: new RegExp("^\\.(" + M + ")"), + NAME: new RegExp("^\\[name=['\"]?(" + M + ")['\"]?\\]"), + TAG: new RegExp("^(" + M.replace("w", "w*") + ")"), + ATTR: new RegExp("^" + P), + PSEUDO: new RegExp("^" + H), + POS: new RegExp(B, "i"), + CHILD: new RegExp( + "^:(only|nth|first|last)-child(?:\\(" + + O + + "*(even|odd|(([+-]|)(\\d*)n|)" + + O + + "*(?:([+-]|)" + + O + + "*(\\d+)|))" + + O + + "*\\)|)", + "i" + ), + needsContext: new RegExp("^" + O + "*[>+~]|" + B, "i") + }, + K = function(e) { + var t = g.createElement("div"); + try { + return e(t); + } catch (n) { + return !1; + } finally { + t = null; + } + }, + Q = K(function(e) { + return e.appendChild( + g.createComment("") + ), !e.getElementsByTagName("*").length; + }), + G = K(function(e) { + return e.innerHTML = "", e.firstChild && typeof e.firstChild.getAttribute !== p && e.firstChild.getAttribute("href") === "#"; + }), + Y = K(function(e) { + e.innerHTML = ""; + var t = typeof e.lastChild.getAttribute("multiple"); + return t !== "boolean" && t !== "string"; + }), + Z = K(function(e) { + return e.innerHTML = "", !e.getElementsByClassName || !e.getElementsByClassName("e").length ? !1 : (e.lastChild.className = "e", e.getElementsByClassName("e").length === 2); + }), + et = K(function(e) { + e.id = d + + 0, e.innerHTML = "
", y.insertBefore(e, y.firstChild); + var t = g.getElementsByName && + g.getElementsByName(d).length === + 2 + g.getElementsByName(d + 0).length; + return r = !g.getElementById(d), y.removeChild(e), t; + }); + try { + x.call(y.childNodes, 0)[0].nodeType; + } catch (tt) { + x = function(e) { + var t, n = []; + for (; t = this[e]; e++) + n.push(t); + return n; + }; + } + nt.matches = function(e, t) { + return nt(e, null, null, t); + }, nt.matchesSelector = function(e, t) { + return nt(t, null, null, [e]).length > 0; + }, s = nt.getText = function(e) { + var t, n = "", r = 0, i = e.nodeType; + if (i) { + if (i === 1 || i === 9 || i === 11) { + if (typeof e.textContent === "string") return e.textContent; + for (e = e.firstChild; e; e = e.nextSibling) + n += s(e); + } else if (i === 3 || i === 4) return e.nodeValue; + } else + for (; t = e[r]; r++) + n += s(t); + return n; + }, o = nt.isXML = function(e) { + var t = e && (e.ownerDocument || e).documentElement; + return t ? t.nodeName !== "HTML" : !1; + }, u = nt.contains = y.contains + ? (function(e, t) { + var n = e.nodeType === 9 ? e.documentElement : e, + r = t && t.parentNode; + return e === r || + !!(r && r.nodeType === 1 && n.contains && n.contains(r)); + }) + : y.compareDocumentPosition + ? (function(e, t) { + return t && !!(e.compareDocumentPosition(t) & 16); + }) + : (function(e, t) { + while (t = t.parentNode) + if (t === e) return !0; + return !1; + }), nt.attr = function(e, t) { + var n, r = o(e); + return r || (t = t.toLowerCase()), (n = i.attrHandle[t]) + ? n(e) + : r || Y + ? e.getAttribute(t) + : (n = e.getAttributeNode(t), n + ? typeof e[t] === "boolean" + ? e[t] ? t : null + : n.specified ? n.value : null + : null); + }, i = nt.selectors = { + cacheLength: 50, + createPseudo: N, + match: J, + attrHandle: ( + G + ? {} + : { + href: function(e) { + return e.getAttribute("href", 2); + }, + type: function(e) { + return e.getAttribute("type"); + } + } + ), + find: { + ID: ( + r + ? (function(e, t, n) { + if (typeof t.getElementById !== p && !n) { + var r = t.getElementById(e); + return r && r.parentNode ? [r] : []; + } + }) + : (function(e, n, r) { + if (typeof n.getElementById !== p && !r) { + var i = n.getElementById(e); + return i + ? i.id === e || + typeof i.getAttributeNode !== p && + i.getAttributeNode("id").value === e + ? [i] + : t + : []; + } + }) + ), + TAG: ( + Q + ? (function(e, t) { + if (typeof t.getElementsByTagName !== p) { + return t.getElementsByTagName(e); + } + }) + : (function(e, t) { + var n = t.getElementsByTagName(e); + if (e === "*") { + var r, i = [], s = 0; + for (; r = n[s]; s++) + r.nodeType === 1 && i.push(r); + return i; + } + return n; + }) + ), + NAME: ( + et && + function(e, t) { + if (typeof t.getElementsByName !== p) { + return t.getElementsByName(name); + } + } + ), + CLASS: ( + Z && + function(e, t, n) { + if (typeof t.getElementsByClassName !== p && !n) { + return t.getElementsByClassName(e); + } + } + ) + }, + relative: { + ">": { dir: "parentNode", first: !0 }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: !0 }, + "~": { dir: "previousSibling" } + }, + preFilter: { + ATTR: function(e) { + return e[1] = e[1].replace($, ""), e[3] = (e[4] || + e[5] || + "").replace($, ""), e[2] === "~=" && + (e[3] = " " + e[3] + " "), e.slice(0, 4); + }, + CHILD: function(e) { + return e[1] = e[1].toLowerCase(), e[1] === "nth" + ? (e[2] || nt.error(e[0]), e[3] = +(e[3] + ? e[4] + (e[5] || 1) + : 2 * (e[2] === "even" || e[2] === "odd")), e[4] = +(e[6] + + e[7] || + e[2] === "odd")) + : e[2] && nt.error(e[0]), e; + }, + PSEUDO: function(e) { + var t, n; + if (J.CHILD.test(e[0])) return null; + if (e[3]) + e[2] = e[3]; + else if (t = e[4]) { + q.test(t) && + (n = ut(t, !0)) && + (n = t.indexOf(")", t.length - n) - t.length) && + (t = t.slice(0, n), e[0] = e[0].slice(0, n)), e[2] = t; + } + return e.slice(0, 3); + } + }, + filter: { + ID: ( + r + ? (function(e) { + return e = e.replace($, ""), function(t) { + return t.getAttribute("id") === e; + }; + }) + : (function(e) { + return e = e.replace($, ""), function(t) { + var n = typeof t.getAttributeNode !== p && + t.getAttributeNode("id"); + return n && n.value === e; + }; + }) + ), + TAG: function(e) { + return e === "*" + ? (function() { + return !0; + }) + : (e = e.replace($, "").toLowerCase(), function(t) { + return t.nodeName && t.nodeName.toLowerCase() === e; + }); + }, + CLASS: function(e) { + var t = k[d][e + " "]; + return t || + (t = new RegExp("(^|" + O + ")" + e + "(" + O + "|$)")) && + k(e, function(e) { + return t.test( + e.className || + typeof e.getAttribute !== p && e.getAttribute("class") || + "" + ); + }); + }, + ATTR: function(e, t, n) { + return function(r, i) { + var s = nt.attr(r, e); + return s == null + ? t === "!=" + : t + ? (s += "", t === "=" + ? s === n + : t === "!=" + ? s !== n + : t === "^=" + ? n && s.indexOf(n) === 0 + : t === "*=" + ? n && s.indexOf(n) > -1 + : t === "$=" + ? n && s.substr(s.length - n.length) === n + : t === "~=" + ? (" " + s + " ").indexOf(n) > -1 + : t === "|=" + ? s === n || + s.substr(0, n.length + 1) === + n + "-" + : !1) + : !0; + }; + }, + CHILD: function(e, t, n, r) { + return e === "nth" + ? (function(e) { + var t, i, s = e.parentNode; + if (n === 1 && r === 0) return !0; + if (s) { + i = 0; + for (t = s.firstChild; t; t = t.nextSibling) { + if (t.nodeType === 1) { + i++; + if (e === t) break; + } + } + } + return i -= r, i === n || i % n === 0 && i / n >= 0; + }) + : (function(t) { + var n = t; + switch (e) { + case "only": + case "first": + while (n = n.previousSibling) + if (n.nodeType === 1) return !1; + if (e === "first") return !0; + n = t; + case "last": + while (n = n.nextSibling) + if (n.nodeType === 1) return !1; + return !0; + } + }); + }, + PSEUDO: function(e, t) { + var n, + r = i.pseudos[e] || + i.setFilters[e.toLowerCase()] || + nt.error("unsupported pseudo: " + e); + return r[d] + ? r(t) + : r.length > 1 + ? (n = [e, e, "", t], i.setFilters.hasOwnProperty( + e.toLowerCase() + ) + ? N(function(e, n) { + var i, s = r(e, t), o = s.length; + while (o--) i = T.call(e, s[o]), e[i] = !(n[i] = s[o]); + }) + : (function(e) { + return r(e, 0, n); + })) + : r; + } + }, + pseudos: { + not: N(function(e) { + var t = [], n = [], r = a(e.replace(j, "$1")); + return r[d] + ? N(function(e, t, n, i) { + var s, o = r(e, null, i, []), u = e.length; + while (u--) if (s = o[u]) e[u] = !(t[u] = s); + }) + : (function(e, i, s) { + return t[0] = e, r(t, null, s, n), !n.pop(); + }); + }), + has: N(function(e) { + return function(t) { + return nt(e, t).length > 0; + }; + }), + contains: N(function(e) { + return function(t) { + return (t.textContent || t.innerText || s(t)).indexOf(e) > -1; + }; + }), + enabled: function(e) { + return e.disabled === !1; + }, + disabled: function(e) { + return e.disabled === !0; + }, + checked: function(e) { + var t = e.nodeName.toLowerCase(); + return t === "input" && !!e.checked || t === "option" && !!e.selected; + }, + selected: function(e) { + return e.parentNode && e.parentNode.selectedIndex, e.selected === !0; + }, + parent: function(e) { + return !i.pseudos.empty(e); + }, + empty: function(e) { + var t; + e = e.firstChild; + while (e) { + if (e.nodeName > "@" || (t = e.nodeType) === 3 || t === 4) { + return !1; + } + e = e.nextSibling; + } + return !0; + }, + header: function(e) { + return X.test(e.nodeName); + }, + text: function(e) { + var t, n; + return e.nodeName.toLowerCase() === "input" && + (t = e.type) === "text" && + ((n = e.getAttribute("type")) == null || n.toLowerCase() === t); + }, + radio: rt("radio"), + checkbox: rt("checkbox"), + file: rt("file"), + password: rt("password"), + image: rt("image"), + submit: it("submit"), + reset: it("reset"), + button: function(e) { + var t = e.nodeName.toLowerCase(); + return t === "input" && e.type === "button" || t === "button"; + }, + input: function(e) { + return V.test(e.nodeName); + }, + focus: function(e) { + var t = e.ownerDocument; + return e === t.activeElement && + (!t.hasFocus || t.hasFocus()) && + !!(e.type || e.href || ~e.tabIndex); + }, + active: function(e) { + return e === e.ownerDocument.activeElement; + }, + first: st(function() { + return [0]; + }), + last: st(function(e, t) { + return [t - 1]; + }), + eq: st(function(e, t, n) { + return [n < 0 ? n + t : n]; + }), + even: st(function(e, t) { + for (var n = 0; n < t; n += 2) e.push(n); + return e; + }), + odd: st(function(e, t) { + for (var n = 1; n < t; n += 2) e.push(n); + return e; + }), + lt: st(function(e, t, n) { + for (var r = n < 0 ? n + t : n; --r >= 0; ) e.push(r); + return e; + }), + gt: st(function(e, t, n) { + for (var r = n < 0 ? n + t : n; ++r < t; ) e.push(r); + return e; + }) + } + }, f = y.compareDocumentPosition + ? (function(e, t) { + return e === t + ? (l = !0, 0) + : (!e.compareDocumentPosition || !t.compareDocumentPosition + ? e.compareDocumentPosition + : e.compareDocumentPosition(t) & 4) + ? -1 + : 1; + }) + : (function(e, t) { + if (e === t) return l = !0, 0; + if (e.sourceIndex && t.sourceIndex) { + return e.sourceIndex - t.sourceIndex; + } + var n, r, i = [], s = [], o = e.parentNode, u = t.parentNode, a = o; + if (o === u) return ot(e, t); + if (!o) return -1; + if (!u) return 1; + while (a) + i.unshift(a), a = a.parentNode; + a = u; + while (a) + s.unshift(a), a = a.parentNode; + n = i.length, r = s.length; + for (var f = 0; f < n && f < r; f++) + if (i[f] !== s[f]) return ot(i[f], s[f]); + return f === n ? ot(e, s[f], -1) : ot(i[f], t, 1); + }), [0, 0].sort(f), h = !l, nt.uniqueSort = function(e) { + var t, n = [], r = 1, i = 0; + l = h, e.sort(f); + if (l) { + for (; t = e[r]; r++) + t === e[r - 1] && (i = n.push(r)); + while (i--) + e.splice(n[i], 1); + } + return e; + }, nt.error = function(e) { + throw new Error("Syntax error, unrecognized expression: " + e); + }, a = nt.compile = function(e, t) { + var n, r = [], i = [], s = A[d][e + " "]; + if (!s) { + t || (t = ut(e)), n = t.length; + while (n--) + s = ht(t[n]), s[d] ? r.push(s) : i.push(s); + s = A(e, pt(i, r)); + } + return s; + }, g.querySelectorAll && + (function() { + var e, + t = vt, + n = /'|\\/g, + r = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, + i = [":focus"], + s = [":active"], + u = y.matchesSelector || + y.mozMatchesSelector || + y.webkitMatchesSelector || + y.oMatchesSelector || + y.msMatchesSelector; + K(function(e) { + e.innerHTML = "", e.querySelectorAll("[selected]").length || i.push("\\[" + O + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)"), e.querySelectorAll(":checked").length || i.push(":checked"); + }), K(function(e) { + e.innerHTML = "

", e.querySelectorAll("[test^='']").length && i.push("[*^$]=" + O + "*(?:\"\"|'')"), e.innerHTML = "", e.querySelectorAll(":enabled").length || i.push(":enabled", ":disabled"); + }), i = new RegExp(i.join("|")), vt = function(e, r, s, o, u) { + if (!o && !u && !i.test(e)) { + var a, f, l = !0, c = d, h = r, p = r.nodeType === 9 && e; + if (r.nodeType === 1 && r.nodeName.toLowerCase() !== "object") { + a = ut(e), (l = r.getAttribute("id")) + ? c = l.replace(n, "\\$&") + : r.setAttribute("id", c), c = "[id='" + + c + + "'] ", f = a.length; + while (f--) + a[f] = c + a[f].join(""); + h = z.test(e) && r.parentNode || r, p = a.join(","); + } + if (p) { + try { + return S.apply(s, x.call(h.querySelectorAll(p), 0)), s; + } catch (v) { + } finally { + l || r.removeAttribute("id"); + } + } + } + return t(e, r, s, o, u); + }, u && + (K(function(t) { + e = u.call(t, "div"); + try { + u.call(t, "[test!='']:sizzle"), s.push("!=", H); + } catch (n) { + } + }), s = new RegExp(s.join("|")), nt.matchesSelector = function(t, n) { + n = n.replace(r, "='$1']"); + if (!o(t) && !s.test(n) && !i.test(n)) { + try { + var a = u.call(t, n); + if (a || e || t.document && t.document.nodeType !== 11) { + return a; + } + } catch (f) { + } + } + return nt(n, null, null, [t]).length > 0; + }); + })(), i.pseudos.nth = i.pseudos.eq, i.filters = mt.prototype = i.pseudos, i.setFilters = new mt(), nt.attr = v.attr, v.find = nt, v.expr = nt.selectors, v.expr[ + ":" + ] = v.expr.pseudos, v.unique = nt.uniqueSort, v.text = nt.getText, v.isXMLDoc = nt.isXML, v.contains = nt.contains; + })(e); + var nt = /Until$/, + rt = /^(?:parents|prev(?:Until|All))/, + it = /^.[^:#\[\.,]*$/, + st = v.expr.match.needsContext, + ot = { children: !0, contents: !0, next: !0, prev: !0 }; + v.fn.extend({ + find: function(e) { + var t, n, r, i, s, o, u = this; + if (typeof e !== "string") { + return v(e).filter(function() { + for ( + t = 0, n = u.length; + t < n; + t++ + ) if (v.contains(u[t], this)) return !0; + }); + } + o = this.pushStack("", "find", e); + for (t = 0, n = this.length; t < n; t++) { + r = o.length, v.find(e, this[t], o); + if (t > 0) { + for (i = r; i < o.length; i++) { + for (s = 0; s < r; s++) { + if (o[s] === o[i]) { + o.splice((i--), 1); + break; + } + } + } + } + } + return o; + }, + has: function(e) { + var t, n = v(e, this), r = n.length; + return this.filter(function() { + for (t = 0; t < r; t++) if (v.contains(this, n[t])) return !0; + }); + }, + not: function(e) { + return this.pushStack(ft(this, e, !1), "not", e); + }, + filter: function(e) { + return this.pushStack(ft(this, e, !0), "filter", e); + }, + is: function(e) { + return !!e && + (typeof e === "string" + ? st.test(e) + ? v(e, this.context).index(this[0]) >= 0 + : v.filter(e, this).length > 0 + : this.filter(e).length > 0); + }, + closest: function(e, t) { + var n, + r = 0, + i = this.length, + s = [], + o = st.test(e) || typeof e !== "string" ? v(e, t || this.context) : 0; + for (; r < i; r++) { + n = this[r]; + while (n && n.ownerDocument && n !== t && n.nodeType !== 11) { + if (o ? o.index(n) > -1 : v.find.matchesSelector(n, e)) { + s.push(n); + break; + } + n = n.parentNode; + } + } + return s = s.length > 1 ? v.unique(s) : s, this.pushStack( + s, + "closest", + e + ); + }, + index: function(e) { + return e + ? typeof e === "string" + ? v.inArray(this[0], v(e)) + : v.inArray(e.jquery ? e[0] : e, this) + : this[0] && this[0].parentNode ? this.prevAll().length : -1; + }, + add: function(e, t) { + var n = typeof e === "string" + ? v(e, t) + : v.makeArray(e && e.nodeType ? [e] : e), + r = v.merge(this.get(), n); + return this.pushStack(ut(n[0]) || ut(r[0]) ? r : v.unique(r)); + }, + addBack: function(e) { + return this.add(e == null ? this.prevObject : this.prevObject.filter(e)); + } + }), v.fn.andSelf = v.fn.addBack, v.each( + { + parent: function(e) { + var t = e.parentNode; + return t && t.nodeType !== 11 ? t : null; + }, + parents: function(e) { + return v.dir(e, "parentNode"); + }, + parentsUntil: function(e, t, n) { + return v.dir(e, "parentNode", n); + }, + next: function(e) { + return at(e, "nextSibling"); + }, + prev: function(e) { + return at(e, "previousSibling"); + }, + nextAll: function(e) { + return v.dir(e, "nextSibling"); + }, + prevAll: function(e) { + return v.dir(e, "previousSibling"); + }, + nextUntil: function(e, t, n) { + return v.dir(e, "nextSibling", n); + }, + prevUntil: function(e, t, n) { + return v.dir(e, "previousSibling", n); + }, + siblings: function(e) { + return v.sibling((e.parentNode || {}).firstChild, e); + }, + children: function(e) { + return v.sibling(e.firstChild); + }, + contents: function(e) { + return v.nodeName(e, "iframe") + ? e.contentDocument || e.contentWindow.document + : v.merge([], e.childNodes); + } + }, + function(e, t) { + v.fn[e] = function(n, r) { + var i = v.map(this, t, n); + return nt.test(e) || (r = n), r && + typeof r === "string" && + (i = v.filter(r, i)), i = this.length > 1 && !ot[e] + ? v.unique(i) + : i, this.length > 1 && + rt.test(e) && + (i = i.reverse()), this.pushStack(i, e, l.call(arguments).join(",")); + }; + } + ), v.extend({ + filter: function(e, t, n) { + return n && (e = ":not(" + e + ")"), t.length === 1 + ? v.find.matchesSelector(t[0], e) ? [t[0]] : [] + : v.find.matches(e, t); + }, + dir: function(e, n, r) { + var i = [], s = e[n]; + while ( + s && s.nodeType !== 9 && (r === t || s.nodeType !== 1 || !v(s).is(r)) + ) + s.nodeType === 1 && i.push(s), s = s[n]; + return i; + }, + sibling: function(e, t) { + var n = []; + for (; e; e = e.nextSibling) + e.nodeType === 1 && e !== t && n.push(e); + return n; + } + }); + var ct = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + ht = / jQuery\d+="(?:null|\d+)"/g, + pt = /^\s+/, + dt = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + vt = /<([\w:]+)/, + mt = /]", "i"), + Et = /^(?:checkbox|radio)$/, + St = /checked\s*(?:[^=]|=\s*.checked.)/i, + xt = /\/(java|ecma)script/i, + Tt = /^\s*\s*$/g, + Nt = { + option: [1, ""], + legend: [1, "
", "
"], + thead: [1, "", "
"], + tr: [2, "", "
"], + td: [3, "", "
"], + col: [2, "", "
"], + area: [1, "", ""], + _default: [0, "", ""] + }, + Ct = lt(i), + kt = Ct.appendChild(i.createElement("div")); + Nt.optgroup = Nt.option, Nt.tbody = Nt.tfoot = Nt.colgroup = Nt.caption = Nt.thead, Nt.th = Nt.td, v.support.htmlSerialize || + (Nt._default = [1, "X
", "
"]), v.fn.extend({ + text: function(e) { + return v.access( + this, + function(e) { + return e === t + ? v.text(this) + : this + .empty() + .append( + (this[0] && this[0].ownerDocument || i).createTextNode(e) + ); + }, + null, + e, + arguments.length + ); + }, + wrapAll: function(e) { + if (v.isFunction(e)) { + return this.each(function(t) { + v(this).wrapAll(e.call(this, t)); + }); + } + if (this[0]) { + var t = v(e, this[0].ownerDocument).eq(0).clone(!0); + this[0].parentNode && t.insertBefore(this[0]), t + .map(function() { + var e = this; + while ( + e.firstChild && e.firstChild.nodeType === 1 + ) e = e.firstChild; + return e; + }) + .append(this); + } + return this; + }, + wrapInner: function(e) { + return v.isFunction(e) + ? this.each(function(t) { + v(this).wrapInner(e.call(this, t)); + }) + : this.each(function() { + var t = v(this), n = t.contents(); + n.length ? n.wrapAll(e) : t.append(e); + }); + }, + wrap: function(e) { + var t = v.isFunction(e); + return this.each(function(n) { + v(this).wrapAll(t ? e.call(this, n) : e); + }); + }, + unwrap: function() { + return this + .parent() + .each(function() { + v.nodeName(this, "body") || v(this).replaceWith(this.childNodes); + }) + .end(); + }, + append: function() { + return this.domManip(arguments, !0, function(e) { + (this.nodeType === 1 || this.nodeType === 11) && this.appendChild(e); + }); + }, + prepend: function() { + return this.domManip(arguments, !0, function(e) { + (this.nodeType === 1 || this.nodeType === 11) && + this.insertBefore(e, this.firstChild); + }); + }, + before: function() { + if (!ut(this[0])) { + return this.domManip(arguments, !1, function(e) { + this.parentNode.insertBefore(e, this); + }); + } + if (arguments.length) { + var e = v.clean(arguments); + return this.pushStack(v.merge(e, this), "before", this.selector); + } + }, + after: function() { + if (!ut(this[0])) { + return this.domManip(arguments, !1, function(e) { + this.parentNode.insertBefore(e, this.nextSibling); + }); + } + if (arguments.length) { + var e = v.clean(arguments); + return this.pushStack(v.merge(this, e), "after", this.selector); + } + }, + remove: function(e, t) { + var n, r = 0; + for (; (n = this[r]) != null; r++) + if (!e || v.filter(e, [n]).length) + !t && + n.nodeType === 1 && + (v.cleanData(n.getElementsByTagName("*")), v.cleanData([ + n + ])), n.parentNode && n.parentNode.removeChild(n); + return this; + }, + empty: function() { + var e, t = 0; + for (; (e = this[t]) != null; t++) { + e.nodeType === 1 && v.cleanData(e.getElementsByTagName("*")); + while (e.firstChild) + e.removeChild(e.firstChild); + } + return this; + }, + clone: function(e, t) { + return e = e == null ? !1 : e, t = t == null + ? e + : t, this.map(function() { + return v.clone(this, e, t); + }); + }, + html: function(e) { + return v.access( + this, + function(e) { + var n = this[0] || {}, r = 0, i = this.length; + if (e === t) { + return n.nodeType === 1 ? n.innerHTML.replace(ht, "") : t; + } + if ( + typeof e === "string" && + !yt.test(e) && + (v.support.htmlSerialize || !wt.test(e)) && + (v.support.leadingWhitespace || !pt.test(e)) && + !Nt[(vt.exec(e) || ["", ""])[1].toLowerCase()] + ) { + e = e.replace(dt, "<$1>"); + try { + for (; r < i; r++) { + n = this[r] || {}, n.nodeType === 1 && + (v.cleanData(n.getElementsByTagName("*")), n.innerHTML = e); + } + n = 0; + } catch (s) { + } + } + n && this.empty().append(e); + }, + null, + e, + arguments.length + ); + }, + replaceWith: function(e) { + return ut(this[0]) + ? this.length + ? this.pushStack(v(v.isFunction(e) ? e() : e), "replaceWith", e) + : this + : v.isFunction(e) + ? this.each(function(t) { + var n = v(this), r = n.html(); + n.replaceWith(e.call(this, t, r)); + }) + : (typeof e !== "string" && + (e = v(e).detach()), this.each(function() { + var t = this.nextSibling, n = this.parentNode; + v(this).remove(), t ? v(t).before(e) : v(n).append(e); + })); + }, + detach: function(e) { + return this.remove(e, !0); + }, + domManip: function(e, n, r) { + e = [].concat.apply([], e); + var i, s, o, u, a = 0, f = e[0], l = [], c = this.length; + if ( + !v.support.checkClone && c > 1 && typeof f === "string" && St.test(f) + ) { + return this.each(function() { + v(this).domManip(e, n, r); + }); + } + if (v.isFunction(f)) { + return this.each(function(i) { + var s = v(this); + e[0] = f.call(this, i, n ? s.html() : t), s.domManip(e, n, r); + }); + } + if (this[0]) { + i = v.buildFragment( + e, + this, + l + ), o = i.fragment, s = o.firstChild, o.childNodes.length === 1 && + (o = s); + if (s) { + n = n && v.nodeName(s, "tr"); + for (u = i.cacheable || c - 1; a < c; a++) + r.call( + n && v.nodeName(this[a], "table") + ? Lt(this[a], "tbody") + : this[a], + a === u ? o : v.clone(o, !0, !0) + ); + } + o = s = null, l.length && + v.each(l, function(e, t) { + t.src + ? v.ajax + ? v.ajax({ + url: t.src, + type: "GET", + dataType: "script", + async: !1, + global: !1, + throws: !0 + }) + : v.error("no ajax") + : v.globalEval( + (t.text || t.textContent || t.innerHTML || "").replace(Tt, "") + ), t.parentNode && t.parentNode.removeChild(t); + }); + } + return this; + } + }), v.buildFragment = function(e, n, r) { + var s, o, u, a = e[0]; + return n = n || i, n = !n.nodeType && n[0] || n, n = n.ownerDocument || + n, e.length === 1 && + typeof a === "string" && + a.length < 512 && + n === i && + a.charAt(0) === "<" && + !bt.test(a) && + (v.support.checkClone || !St.test(a)) && + (v.support.html5Clone || !wt.test(a)) && + (o = !0, s = v.fragments[a], u = s !== t), s || + (s = n.createDocumentFragment(), v.clean(e, n, s, r), o && + (v.fragments[a] = u && s)), { fragment: s, cacheable: o }; + }, v.fragments = {}, v.each( + { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" + }, + function(e, t) { + v.fn[e] = function(n) { + var r, + i = 0, + s = [], + o = v(n), + u = o.length, + a = this.length === 1 && this[0].parentNode; + if ( + (a == null || a && a.nodeType === 11 && a.childNodes.length === 1) && + u === 1 + ) { + return o[t](this[0]), this; + } + for (; i < u; i++) + r = (i > 0 ? this.clone(!0) : this).get(), v(o[i])[t]( + r + ), s = s.concat(r); + return this.pushStack(s, e, o.selector); + }; + } + ), v.extend({ + clone: function(e, t, n) { + var r, i, s, o; + v.support.html5Clone || v.isXMLDoc(e) || !wt.test("<" + e.nodeName + ">") + ? o = e.cloneNode(!0) + : (kt.innerHTML = e.outerHTML, kt.removeChild(o = kt.firstChild)); + if ( + (!v.support.noCloneEvent || !v.support.noCloneChecked) && + (e.nodeType === 1 || e.nodeType === 11) && + !v.isXMLDoc(e) + ) { + Ot(e, o), r = Mt(e), i = Mt(o); + for (s = 0; r[s]; ++s) { + i[s] && Ot(r[s], i[s]); + } + } + if (t) { + At(e, o); + if (n) { + r = Mt(e), i = Mt(o); + for (s = 0; r[s]; ++s) + At(r[s], i[s]); + } + } + return r = i = null, o; + }, + clean: function(e, t, n, r) { + var s, o, u, a, f, l, c, h, p, d, m, g, y = t === i && Ct, b = []; + if (!t || typeof t.createDocumentFragment === "undefined") t = i; + for (s = 0; (u = e[s]) != null; s++) { + typeof u === "number" && (u += ""); + if (!u) continue; + if (typeof u === "string") { + if (!gt.test(u)) + u = t.createTextNode(u); + else { + y = y || lt(t), c = t.createElement("div"), y.appendChild( + c + ), u = u.replace(dt, "<$1>"), a = (vt.exec(u) || ["", ""])[ + 1 + ].toLowerCase(), f = Nt[a] || Nt._default, l = f[ + 0 + ], c.innerHTML = f[1] + u + f[2]; + while (l--) + c = c.lastChild; + if (!v.support.tbody) { + h = mt.test(u), p = a === "table" && !h + ? c.firstChild && c.firstChild.childNodes + : f[1] === "" && !h ? c.childNodes : []; + for (o = p.length - 1; o >= 0; --o) + v.nodeName(p[o], "tbody") && + !p[o].childNodes.length && + p[o].parentNode.removeChild(p[o]); + } + !v.support.leadingWhitespace && + pt.test(u) && + c.insertBefore( + t.createTextNode(pt.exec(u)[0]), + c.firstChild + ), u = c.childNodes, c.parentNode.removeChild(c); + } + } + u.nodeType ? b.push(u) : v.merge(b, u); + } + c && (u = c = y = null); + if (!v.support.appendChecked) { + for (s = 0; (u = b[s]) != null; s++) { + v.nodeName(u, "input") + ? _t(u) + : typeof u.getElementsByTagName !== "undefined" && + v.grep(u.getElementsByTagName("input"), _t); + } + } + if (n) { + m = function(e) { + if (!e.type || xt.test(e.type)) { + return r + ? r.push(e.parentNode ? e.parentNode.removeChild(e) : e) + : n.appendChild(e); + } + }; + for (s = 0; (u = b[s]) != null; s++) + if (!v.nodeName(u, "script") || !m(u)) + n.appendChild(u), typeof u.getElementsByTagName !== "undefined" && + (g = v.grep( + v.merge([], u.getElementsByTagName("script")), + m + ), b.splice.apply(b, [s + 1, 0].concat(g)), s += g.length); + } + return b; + }, + cleanData: function(e, t) { + var n, + r, + i, + s, + o = 0, + u = v.expando, + a = v.cache, + f = v.support.deleteExpando, + l = v.event.special; + for (; (i = e[o]) != null; o++) { + if (t || v.acceptData(i)) { + r = i[u], n = r && a[r]; + if (n) { + if (n.events) { + for (s in n.events) { + l[s] ? v.event.remove(i, s) : v.removeEvent(i, s, n.handle); + } + } + a[r] && + (delete a[r], f + ? delete i[u] + : i.removeAttribute + ? i.removeAttribute(u) + : i[u] = null, v.deletedIds.push(r)); + } + } + } + } + }), (function() { + var e, t; + v.uaMatch = function(e) { + e = e.toLowerCase(); + var t = /(chrome)[ \/]([\w.]+)/.exec(e) || + /(webkit)[ \/]([\w.]+)/.exec(e) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e) || + /(msie) ([\w.]+)/.exec(e) || + e.indexOf("compatible") < 0 && + /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e) || + []; + return { browser: t[1] || "", version: t[2] || "0" }; + }, e = v.uaMatch(o.userAgent), t = {}, e.browser && + (t[e.browser] = !0, t.version = e.version), t.chrome + ? t.webkit = !0 + : t.webkit && (t.safari = !0), v.browser = t, v.sub = function() { + function e(t, n) { + return new e.fn.init(t, n); + } + v.extend( + !0, + e, + this + ), e.superclass = this, e.fn = e.prototype = this(), e.fn.constructor = e, e.sub = this.sub, e.fn.init = function( + r, + i + ) { + return i && + i instanceof v && + !(i instanceof e) && + (i = e(i)), v.fn.init.call(this, r, i, t); + }, e.fn.init.prototype = e.fn; + var t = e(i); + return e; + }; + })(); + var Dt, + Pt, + Ht, + Bt = /alpha\([^)]*\)/i, + jt = /opacity=([^)]*)/, + Ft = /^(top|right|bottom|left)$/, + It = /^(none|table(?!-c[ea]).+)/, + qt = /^margin/, + Rt = new RegExp("^(" + m + ")(.*)$", "i"), + Ut = new RegExp("^(" + m + ")(?!px)[a-z%]+$", "i"), + zt = new RegExp("^([-+])=(" + m + ")", "i"), + Wt = { BODY: "block" }, + Xt = { position: "absolute", visibility: "hidden", display: "block" }, + Vt = { letterSpacing: 0, fontWeight: 400 }, + $t = ["Top", "Right", "Bottom", "Left"], + Jt = ["Webkit", "O", "Moz", "ms"], + Kt = v.fn.toggle; + v.fn.extend({ + css: function(e, n) { + return v.access( + this, + function(e, n, r) { + return r !== t ? v.style(e, n, r) : v.css(e, n); + }, + e, + n, + arguments.length > 1 + ); + }, + show: function() { + return Yt(this, !0); + }, + hide: function() { + return Yt(this); + }, + toggle: function(e, t) { + var n = typeof e === "boolean"; + return v.isFunction(e) && v.isFunction(t) + ? Kt.apply(this, arguments) + : this.each(function() { + (n ? e : Gt(this)) ? v(this).show() : v(this).hide(); + }); + } + }), v.extend({ + cssHooks: { + opacity: { + get: function(e, t) { + if (t) { + var n = Dt(e, "opacity"); + return n === "" ? "1" : n; + } + } + } + }, + cssNumber: { + fillOpacity: !0, + fontWeight: !0, + lineHeight: !0, + opacity: !0, + orphans: !0, + widows: !0, + zIndex: !0, + zoom: !0 + }, + cssProps: { float: v.support.cssFloat ? "cssFloat" : "styleFloat" }, + style: function(e, n, r, i) { + if (!e || e.nodeType === 3 || e.nodeType === 8 || !e.style) return; + var s, o, u, a = v.camelCase(n), f = e.style; + n = v.cssProps[a] || (v.cssProps[a] = Qt(f, a)), u = v.cssHooks[n] || + v.cssHooks[a]; + if (r === t) { + return u && "get" in u && (s = u.get(e, !1, i)) !== t ? s : f[n]; + } + o = typeof r, o === "string" && + (s = zt.exec(r)) && + (r = (s[1] + 1) * s[2] + parseFloat(v.css(e, n)), o = "number"); + if (r == null || o === "number" && isNaN(r)) return; + o === "number" && !v.cssNumber[a] && (r += "px"); + if (!u || !("set" in u) || (r = u.set(e, r, i)) !== t) { + try { + f[n] = r; + } catch (l) { + } + } + }, + css: function(e, n, r, i) { + var s, o, u, a = v.camelCase(n); + return n = v.cssProps[a] || + (v.cssProps[a] = Qt(e.style, a)), u = v.cssHooks[n] || + v.cssHooks[a], u && "get" in u && (s = u.get(e, !0, i)), s === t && + (s = Dt(e, n)), s === "normal" && n in Vt && (s = Vt[n]), r || i !== t + ? (o = parseFloat(s), r || v.isNumeric(o) ? o || 0 : s) + : s; + }, + swap: function(e, t, n) { + var r, i, s = {}; + for (i in t) + s[i] = e.style[i], e.style[i] = t[i]; + r = n.call(e); + for (i in t) + e.style[i] = s[i]; + return r; + } + }), e.getComputedStyle + ? Dt = function(t, n) { + var r, i, s, o, u = e.getComputedStyle(t, null), a = t.style; + return u && + (r = u.getPropertyValue(n) || u[n], r === "" && + !v.contains(t.ownerDocument, t) && + (r = v.style(t, n)), Ut.test(r) && + qt.test(n) && + (i = a.width, s = a.minWidth, o = a.maxWidth, a.minWidth = a.maxWidth = a.width = r, r = u.width, a.width = i, a.minWidth = s, a.maxWidth = o)), r; + } + : i.documentElement.currentStyle && + (Dt = function(e, t) { + var n, r, i = e.currentStyle && e.currentStyle[t], s = e.style; + return i == null && s && s[t] && (i = s[t]), Ut.test(i) && + !Ft.test(t) && + (n = s.left, r = e.runtimeStyle && e.runtimeStyle.left, r && + (e.runtimeStyle.left = e.currentStyle.left), s.left = t === + "fontSize" + ? "1em" + : i, i = s.pixelLeft + "px", s.left = n, r && + (e.runtimeStyle.left = r)), i === "" ? "auto" : i; + }), v.each(["height", "width"], function(e, t) { + v.cssHooks[t] = { + get: function(e, n, r) { + if (n) { + return e.offsetWidth === 0 && It.test(Dt(e, "display")) + ? v.swap(e, Xt, function() { + return tn(e, t, r); + }) + : tn(e, t, r); + } + }, + set: function(e, n, r) { + return Zt( + e, + n, + r + ? en( + e, + t, + r, + v.support.boxSizing && v.css(e, "boxSizing") === "border-box" + ) + : 0 + ); + } + }; + }), v.support.opacity || + (v.cssHooks.opacity = { + get: function(e, t) { + return jt.test( + (t && e.currentStyle ? e.currentStyle.filter : e.style.filter) || "" + ) + ? 0.01 * parseFloat(RegExp.$1) + "" + : t ? "1" : ""; + }, + set: function(e, t) { + var n = e.style, + r = e.currentStyle, + i = v.isNumeric(t) ? "alpha(opacity=" + t * 100 + ")" : "", + s = r && r.filter || n.filter || ""; + n.zoom = 1; + if (t >= 1 && v.trim(s.replace(Bt, "")) === "" && n.removeAttribute) { + n.removeAttribute("filter"); + if (r && !r.filter) return; + } + n.filter = Bt.test(s) ? s.replace(Bt, i) : s + " " + i; + } + }), v(function() { + v.support.reliableMarginRight || + (v.cssHooks.marginRight = { + get: function(e, t) { + return v.swap(e, { display: "inline-block" }, function() { + if (t) return Dt(e, "marginRight"); + }); + } + }), !v.support.pixelPosition && + v.fn.position && + v.each(["top", "left"], function(e, t) { + v.cssHooks[t] = { + get: function(e, n) { + if (n) { + var r = Dt(e, t); + return Ut.test(r) ? v(e).position()[t] + "px" : r; + } + } + }; + }); + }), v.expr && + v.expr.filters && + (v.expr.filters.hidden = function(e) { + return e.offsetWidth === 0 && e.offsetHeight === 0 || + !v.support.reliableHiddenOffsets && + (e.style && e.style.display || Dt(e, "display")) === "none"; + }, v.expr.filters.visible = function(e) { + return !v.expr.filters.hidden(e); + }), v.each({ margin: "", padding: "", border: "Width" }, function(e, t) { + v.cssHooks[e + t] = { + expand: function(n) { + var r, i = typeof n === "string" ? n.split(" ") : [n], s = {}; + for (r = 0; r < 4; r++) + s[e + $t[r] + t] = i[r] || i[r - 2] || i[0]; + return s; + } + }, qt.test(e) || (v.cssHooks[e + t].set = Zt); + }); + var rn = /%20/g, + sn = /\[\]$/, + on = /\r?\n/g, + un = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, + an = /^(?:select|textarea)/i; + v.fn.extend({ + serialize: function() { + return v.param(this.serializeArray()); + }, + serializeArray: function() { + return this + .map(function() { + return this.elements ? v.makeArray(this.elements) : this; + }) + .filter(function() { + return this.name && + !this.disabled && + (this.checked || an.test(this.nodeName) || un.test(this.type)); + }) + .map(function(e, t) { + var n = v(this).val(); + return n == null + ? null + : v.isArray(n) + ? v.map(n, function(e, n) { + return { name: t.name, value: e.replace(on, "\r\n") }; + }) + : { name: t.name, value: n.replace(on, "\r\n") }; + }) + .get(); + } + }), v.param = function(e, n) { + var r, + i = [], + s = function(e, t) { + t = v.isFunction(t) ? t() : t == null ? "" : t, i[ + i.length + ] = encodeURIComponent(e) + "=" + encodeURIComponent(t); + }; + n === t && (n = v.ajaxSettings && v.ajaxSettings.traditional); + if (v.isArray(e) || e.jquery && !v.isPlainObject(e)) { + v.each(e, function() { + s(this.name, this.value); + }); + } else + for (r in e) + fn(r, e[r], n, s); + return i.join("&").replace(rn, "+"); + }; + var ln, + cn, + hn = /#.*$/, + pn = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, + dn = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/, + vn = /^(?:GET|HEAD)$/, + mn = /^\/\//, + gn = /\?/, + yn = /)<[^<]*)*<\/script>/gi, + bn = /([?&])_=[^&]*/, + wn = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/, + En = v.fn.load, + Sn = {}, + xn = {}, + Tn = ["*/"] + ["*"]; + try { + cn = s.href; + } catch (Nn) { + cn = i.createElement("a"), cn.href = "", cn = cn.href; + } + ln = wn.exec(cn.toLowerCase()) || [], v.fn.load = function(e, n, r) { + if (typeof e !== "string" && En) return En.apply(this, arguments); + if (!this.length) return this; + var i, s, o, u = this, a = e.indexOf(" "); + return a >= 0 && + (i = e.slice(a, e.length), e = e.slice(0, a)), v.isFunction(n) + ? (r = n, n = t) + : n && typeof n === "object" && (s = "POST"), v + .ajax({ + url: e, + type: s, + dataType: "html", + data: n, + complete: function(e, t) { + r && u.each(r, o || [e.responseText, t, e]); + } + }) + .done(function(e) { + o = arguments, u.html(i ? v("
").append(e.replace(yn, "")).find(i) : e); + }), this; + }, v.each( + "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), + function(e, t) { + v.fn[t] = function(e) { + return this.on(t, e); + }; + } + ), v.each(["get", "post"], function(e, n) { + v[n] = function(e, r, i, s) { + return v.isFunction(r) && (s = s || i, i = r, r = t), v.ajax({ + type: n, + url: e, + data: r, + success: i, + dataType: s + }); + }; + }), v.extend({ + getScript: function(e, n) { + return v.get(e, t, n, "script"); + }, + getJSON: function(e, t, n) { + return v.get(e, t, n, "json"); + }, + ajaxSetup: function(e, t) { + return t ? Ln(e, v.ajaxSettings) : (t = e, e = v.ajaxSettings), Ln( + e, + t + ), e; + }, + ajaxSettings: { + url: cn, + isLocal: dn.test(ln[1]), + global: !0, + type: "GET", + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + processData: !0, + async: !0, + accepts: { + xml: "application/xml, text/xml", + html: "text/html", + text: "text/plain", + json: "application/json, text/javascript", + "*": Tn + }, + contents: { xml: /xml/, html: /html/, json: /json/ }, + responseFields: { xml: "responseXML", text: "responseText" }, + converters: { + "* text": e.String, + "text html": !0, + "text json": v.parseJSON, + "text xml": v.parseXML + }, + flatOptions: { context: !0, url: !0 } + }, + ajaxPrefilter: Cn(Sn), + ajaxTransport: Cn(xn), + ajax: function(e, n) { + function T(e, n, s, a) { + var l, y, b, w, S, T = n; + if (E === 2) return; + E = 2, u && clearTimeout(u), o = t, i = a || "", x.readyState = e > 0 + ? 4 + : 0, s && (w = An(c, x, s)); + if (e >= 200 && e < 300 || e === 304) { + c.ifModified && + (S = x.getResponseHeader("Last-Modified"), S && + (v.lastModified[r] = S), S = x.getResponseHeader("Etag"), S && + (v.etag[r] = S)), e === 304 + ? (T = "notmodified", l = !0) + : (l = On(c, w), T = l.state, y = l.data, b = l.error, l = !b); + } else { + b = T; + if (!T || e) T = "error", e < 0 && (e = 0); + } + x.status = e, x.statusText = (n || T) + "", l + ? d.resolveWith(h, [y, T, x]) + : d.rejectWith(h, [x, T, b]), x.statusCode(g), g = t, f && + p.trigger("ajax" + (l ? "Success" : "Error"), [ + x, + c, + l ? y : b + ]), m.fireWith(h, [x, T]), f && + (p.trigger("ajaxComplete", [x, c]), --v.active || + v.event.trigger("ajaxStop")); + } + typeof e === "object" && (n = e, e = t), n = n || {}; + var r, + i, + s, + o, + u, + a, + f, + l, + c = v.ajaxSetup({}, n), + h = c.context || c, + p = h !== c && (h.nodeType || h instanceof v) ? v(h) : v.event, + d = v.Deferred(), + m = v.Callbacks("once memory"), + g = c.statusCode || {}, + b = {}, + w = {}, + E = 0, + S = "canceled", + x = { + readyState: 0, + setRequestHeader: function(e, t) { + if (!E) { + var n = e.toLowerCase(); + e = w[n] = w[n] || e, b[e] = t; + } + return this; + }, + getAllResponseHeaders: function() { + return E === 2 ? i : null; + }, + getResponseHeader: function(e) { + var n; + if (E === 2) { + if (!s) { + s = {}; + while (n = pn.exec(i)) + s[n[1].toLowerCase()] = n[2]; + } + n = s[e.toLowerCase()]; + } + return n === t ? null : n; + }, + overrideMimeType: function(e) { + return E || (c.mimeType = e), this; + }, + abort: function(e) { + return e = e || S, o && o.abort(e), T(0, e), this; + } + }; + d.promise( + x + ), x.success = x.done, x.error = x.fail, x.complete = m.add, x.statusCode = function( + e + ) { + if (e) { + var t; + if (E < 2) for (t in e) g[t] = [g[t], e[t]]; + else t = e[x.status], x.always(t); + } + return this; + }, c.url = ((e || c.url) + "") + .replace(hn, "") + .replace(mn, ln[1] + "//"), c.dataTypes = v + .trim(c.dataType || "*") + .toLowerCase() + .split(y), c.crossDomain == null && + (a = wn.exec(c.url.toLowerCase()), c.crossDomain = !(!a || + a[1] === ln[1] && + a[2] === ln[2] && + (a[3] || (a[1] === "http:" ? 80 : 443)) == + (ln[3] || (ln[1] === "http:" ? 80 : 443)))), c.data && + c.processData && + typeof c.data !== "string" && + (c.data = v.param(c.data, c.traditional)), kn(Sn, c, n, x); + if (E === 2) return x; + f = c.global, c.type = c.type.toUpperCase(), c.hasContent = !vn.test( + c.type + ), f && v.active++ === 0 && v.event.trigger("ajaxStart"); + if (!c.hasContent) { + c.data && + (c.url += (gn.test(c.url) ? "&" : "?") + + c.data, delete c.data), r = c.url; + if (c.cache === !1) { + var N = v.now(), C = c.url.replace(bn, "$1_=" + N); + c.url = C + + (C === c.url ? (gn.test(c.url) ? "&" : "?") + "_=" + N : ""); + } + } + (c.data && c.hasContent && c.contentType !== !1 || n.contentType) && + x.setRequestHeader("Content-Type", c.contentType), c.ifModified && + (r = r || c.url, v.lastModified[r] && + x.setRequestHeader("If-Modified-Since", v.lastModified[r]), v.etag[ + r + ] && + x.setRequestHeader("If-None-Match", v.etag[r])), x.setRequestHeader( + "Accept", + c.dataTypes[0] && c.accepts[c.dataTypes[0]] + ? c.accepts[c.dataTypes[0]] + + (c.dataTypes[0] !== "*" ? ", " + Tn + "; q=0.01" : "") + : c.accepts["*"] + ); + for (l in c.headers) + x.setRequestHeader(l, c.headers[l]); + if (!c.beforeSend || c.beforeSend.call(h, x, c) !== !1 && E !== 2) { + S = "abort"; + for (l in { success: 1, error: 1, complete: 1 }) + x[l](c[l]); + o = kn(xn, c, n, x); + if (!o) + T(-1, "No Transport"); + else { + x.readyState = 1, f && p.trigger("ajaxSend", [x, c]), c.async && + c.timeout > 0 && + (u = setTimeout( + function() { + x.abort("timeout"); + }, + c.timeout + )); + try { + E = 1, o.send(b, T); + } catch (k) { + if (!(E < 2)) throw k; + T(-1, k); + } + } + return x; + } + return x.abort(); + }, + active: 0, + lastModified: {}, + etag: {} + }); + var Mn = [], _n = /\?/, Dn = /(=)\?(?=&|$)|\?\?/, Pn = v.now(); + v.ajaxSetup({ + jsonp: "callback", + jsonpCallback: function() { + var e = Mn.pop() || v.expando + "_" + Pn++; + return this[e] = !0, e; + } + }), v.ajaxPrefilter("json jsonp", function(n, r, i) { + var s, + o, + u, + a = n.data, + f = n.url, + l = n.jsonp !== !1, + c = l && Dn.test(f), + h = l && + !c && + typeof a === "string" && + !(n.contentType || "").indexOf("application/x-www-form-urlencoded") && + Dn.test(a); + if (n.dataTypes[0] === "jsonp" || c || h) { + return s = n.jsonpCallback = v.isFunction(n.jsonpCallback) + ? n.jsonpCallback() + : n.jsonpCallback, o = e[s], c + ? n.url = f.replace(Dn, "$1" + s) + : h + ? n.data = a.replace(Dn, "$1" + s) + : l && + (n.url += (_n.test(f) ? "&" : "?") + + n.jsonp + + "=" + + s), n.converters["script json"] = function() { + return u || v.error(s + " was not called"), u[0]; + }, n.dataTypes[0] = "json", e[s] = function() { + u = arguments; + }, i.always(function() { + e[ + s + ] = o, n[s] && (n.jsonpCallback = r.jsonpCallback, Mn.push(s)), u && v.isFunction(o) && o(u[0]), u = o = t; + }), "script"; + } + }), v.ajaxSetup({ + accepts: { + script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" + }, + contents: { script: /javascript|ecmascript/ }, + converters: { + "text script": function(e) { + return v.globalEval(e), e; + } + } + }), v.ajaxPrefilter("script", function(e) { + e.cache === t && + (e.cache = !1), e.crossDomain && (e.type = "GET", e.global = !1); + }), v.ajaxTransport("script", function(e) { + if (e.crossDomain) { + var n, + r = i.head || i.getElementsByTagName("head")[0] || i.documentElement; + return { + send: function(s, o) { + n = i.createElement("script"), n.async = "async", e.scriptCharset && + (n.charset = e.scriptCharset), n.src = e.url, n.onload = n.onreadystatechange = function( + e, + i + ) { + if (i || !n.readyState || /loaded|complete/.test(n.readyState)) { + n.onload = n.onreadystatechange = null, r && + n.parentNode && + r.removeChild(n), n = t, i || o(200, "success"); + } + }, r.insertBefore(n, r.firstChild); + }, + abort: function() { + n && n.onload(0, 1); + } + }; + } + }); + var Hn, + Bn = e.ActiveXObject + ? (function() { + for (var e in Hn) + Hn[e](0, 1); + }) + : !1, + jn = 0; + v.ajaxSettings.xhr = e.ActiveXObject + ? (function() { + return !this.isLocal && Fn() || In(); + }) + : Fn, (function(e) { + v.extend(v.support, { ajax: !!e, cors: !!e && "withCredentials" in e }); + })(v.ajaxSettings.xhr()), v.support.ajax && + v.ajaxTransport(function(n) { + if (!n.crossDomain || v.support.cors) { + var r; + return { + send: function(i, s) { + var o, u, a = n.xhr(); + n.username + ? a.open(n.type, n.url, n.async, n.username, n.password) + : a.open(n.type, n.url, n.async); + if (n.xhrFields) for (u in n.xhrFields) a[u] = n.xhrFields[u]; + n.mimeType && + a.overrideMimeType && + a.overrideMimeType(n.mimeType), !n.crossDomain && + !i["X-Requested-With"] && + (i["X-Requested-With"] = "XMLHttpRequest"); + try { + for (u in i) + a.setRequestHeader(u, i[u]); + } catch (f) { + } + a.send(n.hasContent && n.data || null), r = function(e, i) { + var u, f, l, c, h; + try { + if (r && (i || a.readyState === 4)) { + r = t, o && + (a.onreadystatechange = v.noop, Bn && delete Hn[o]); + if (i) + a.readyState !== 4 && a.abort(); + else { + u = a.status, l = a.getAllResponseHeaders(), c = { + }, h = a.responseXML, h && h.documentElement && (c.xml = h); + try { + c.text = a.responseText; + } catch (p) { + } + try { + f = a.statusText; + } catch (p) { + f = ""; + } + !u && n.isLocal && !n.crossDomain + ? u = c.text ? 200 : 404 + : u === 1223 && (u = 204); + } + } + } catch (d) { + i || s(-1, d); + } + c && s(u, f, c, l); + }, n.async + ? a.readyState === 4 + ? setTimeout(r, 0) + : (o = ++jn, Bn && + (Hn || (Hn = {}, v(e).unload(Bn)), Hn[ + o + ] = r), a.onreadystatechange = r) + : r(); + }, + abort: function() { + r && r(0, 1); + } + }; + } + }); + var qn, + Rn, + Un = /^(?:toggle|show|hide)$/, + zn = new RegExp("^(?:([-+])=|)(" + m + ")([a-z%]*)$", "i"), + Wn = /queueHooks$/, + Xn = [Gn], + Vn = { + "*": [ + function(e, t) { + var n, + r, + i = this.createTween(e, t), + s = zn.exec(t), + o = i.cur(), + u = +o || 0, + a = 1, + f = 20; + if (s) { + n = +s[2], r = s[3] || (v.cssNumber[e] ? "" : "px"); + if (r !== "px" && u) { + u = v.css(i.elem, e, !0) || n || 1; + do { + a = a || ".5", u /= a, v.style(i.elem, e, u + r); + } while (a !== (a = i.cur() / o) && a !== 1 && --f); + } + i.unit = r, i.start = u, i.end = s[1] ? u + (s[1] + 1) * n : n; + } + return i; + } + ] + }; + v.Animation = v.extend(Kn, { + tweener: function(e, t) { + v.isFunction(e) ? (t = e, e = ["*"]) : e = e.split(" "); + var n, r = 0, i = e.length; + for (; r < i; r++) + n = e[r], Vn[n] = Vn[n] || [], Vn[n].unshift(t); + }, + prefilter: function(e, t) { + t ? Xn.unshift(e) : Xn.push(e); + } + }), v.Tween = Yn, Yn.prototype = { + constructor: Yn, + init: function(e, t, n, r, i, s) { + this.elem = e, this.prop = n, this.easing = i || + "swing", this.options = t, this.start = this.now = this.cur(), this.end = r, this.unit = s || + (v.cssNumber[n] ? "" : "px"); + }, + cur: function() { + var e = Yn.propHooks[this.prop]; + return e && e.get ? e.get(this) : Yn.propHooks._default.get(this); + }, + run: function(e) { + var t, n = Yn.propHooks[this.prop]; + return this.options.duration + ? this.pos = t = v.easing[this.easing]( + e, + this.options.duration * e, + 0, + 1, + this.options.duration + ) + : this.pos = t = e, this.now = (this.end - this.start) * t + + this.start, this.options.step && + this.options.step.call(this.elem, this.now, this), n && n.set + ? n.set(this) + : Yn.propHooks._default.set(this), this; + } + }, Yn.prototype.init.prototype = Yn.prototype, Yn.propHooks = { + _default: { + get: function(e) { + var t; + return e.elem[e.prop] == null || + !!e.elem.style && e.elem.style[e.prop] != null + ? (t = v.css(e.elem, e.prop, !1, ""), !t || t === "auto" ? 0 : t) + : e.elem[e.prop]; + }, + set: function(e) { + v.fx.step[e.prop] + ? v.fx.step[e.prop](e) + : e.elem.style && + (e.elem.style[v.cssProps[e.prop]] != null || v.cssHooks[e.prop]) + ? v.style(e.elem, e.prop, e.now + e.unit) + : e.elem[e.prop] = e.now; + } + } + }, Yn.propHooks.scrollTop = Yn.propHooks.scrollLeft = { + set: function(e) { + e.elem.nodeType && e.elem.parentNode && (e.elem[e.prop] = e.now); + } + }, v.each(["toggle", "show", "hide"], function(e, t) { + var n = v.fn[t]; + v.fn[t] = function(r, i, s) { + return r == null || + typeof r === "boolean" || + !e && v.isFunction(r) && v.isFunction(i) + ? n.apply(this, arguments) + : this.animate(Zn(t, !0), r, i, s); + }; + }), v.fn.extend({ + fadeTo: function(e, t, n, r) { + return this + .filter(Gt) + .css("opacity", 0) + .show() + .end() + .animate({ opacity: t }, e, n, r); + }, + animate: function(e, t, n, r) { + var i = v.isEmptyObject(e), + s = v.speed(t, n, r), + o = function() { + var t = Kn(this, v.extend({}, e), s); + i && t.stop(!0); + }; + return i || s.queue === !1 ? this.each(o) : this.queue(s.queue, o); + }, + stop: function(e, n, r) { + var i = function(e) { + var t = e.stop; + delete e.stop, t(r); + }; + return typeof e !== "string" && (r = n, n = e, e = t), n && + e !== !1 && + this.queue(e || "fx", []), this.each(function() { + var t = !0, + n = e != null && e + "queueHooks", + s = v.timers, + o = v._data(this); + if (n) o[n] && o[n].stop && i(o[n]); + else for (n in o) o[n] && o[n].stop && Wn.test(n) && i(o[n]); + for ( + n = s.length; + n--; + + ) s[n].elem === this && (e == null || s[n].queue === e) && (s[n].anim.stop(r), t = !1, s.splice(n, 1)); + (t || !r) && v.dequeue(this, e); + }); + } + }), v.each( + { + slideDown: Zn("show"), + slideUp: Zn("hide"), + slideToggle: Zn("toggle"), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } + }, + function(e, t) { + v.fn[e] = function(e, n, r) { + return this.animate(t, e, n, r); + }; + } + ), v.speed = function(e, t, n) { + var r = e && typeof e === "object" + ? v.extend({}, e) + : { + complete: n || !n && t || v.isFunction(e) && e, + duration: e, + easing: n && t || t && !v.isFunction(t) && t + }; + r.duration = v.fx.off + ? 0 + : typeof r.duration === "number" + ? r.duration + : r.duration in v.fx.speeds + ? v.fx.speeds[r.duration] + : v.fx.speeds._default; + if (r.queue == null || r.queue === !0) r.queue = "fx"; + return r.old = r.complete, r.complete = function() { + v.isFunction(r.old) && r.old.call(this), r.queue && + v.dequeue(this, r.queue); + }, r; + }, v.easing = { + linear: function(e) { + return e; + }, + swing: function(e) { + return 0.5 - Math.cos(e * Math.PI) / 2; + } + }, v.timers = [], v.fx = Yn.prototype.init, v.fx.tick = function() { + var e, n = v.timers, r = 0; + qn = v.now(); + for (; r < n.length; r++) + e = n[r], !e() && n[r] === e && n.splice((r--), 1); + n.length || v.fx.stop(), qn = t; + }, v.fx.timer = function(e) { + e() && + v.timers.push(e) && + !Rn && + (Rn = setInterval(v.fx.tick, v.fx.interval)); + }, v.fx.interval = 13, v.fx.stop = function() { + clearInterval(Rn), Rn = null; + }, v.fx.speeds = { + slow: 600, + fast: 200, + _default: 400 + }, v.fx.step = {}, v.expr && + v.expr.filters && + (v.expr.filters.animated = function(e) { + return v.grep(v.timers, function(t) { + return e === t.elem; + }).length; + }); + var er = /^(?:body|html)$/i; + v.fn.offset = function(e) { + if (arguments.length) { + return e === t + ? this + : this.each(function(t) { + v.offset.setOffset(this, e, t); + }); + } + var n, + r, + i, + s, + o, + u, + a, + f = { top: 0, left: 0 }, + l = this[0], + c = l && l.ownerDocument; + if (!c) return; + return (r = c.body) === l + ? v.offset.bodyOffset(l) + : (n = c.documentElement, v.contains(n, l) + ? (typeof l.getBoundingClientRect !== "undefined" && + (f = l.getBoundingClientRect()), i = tr(c), s = n.clientTop || + r.clientTop || + 0, o = n.clientLeft || r.clientLeft || 0, u = i.pageYOffset || + n.scrollTop, a = i.pageXOffset || n.scrollLeft, { + top: f.top + u - s, + left: f.left + a - o + }) + : f); + }, v.offset = { + bodyOffset: function(e) { + var t = e.offsetTop, n = e.offsetLeft; + return v.support.doesNotIncludeMarginInBodyOffset && + (t += parseFloat(v.css(e, "marginTop")) || 0, n += parseFloat( + v.css(e, "marginLeft") + ) || + 0), { top: t, left: n }; + }, + setOffset: function(e, t, n) { + var r = v.css(e, "position"); + r === "static" && (e.style.position = "relative"); + var i = v(e), + s = i.offset(), + o = v.css(e, "top"), + u = v.css(e, "left"), + a = (r === "absolute" || r === "fixed") && + v.inArray("auto", [o, u]) > -1, + f = {}, + l = {}, + c, + h; + a + ? (l = i.position(), c = l.top, h = l.left) + : (c = parseFloat(o) || 0, h = parseFloat(u) || 0), v.isFunction(t) && + (t = t.call(e, n, s)), t.top != null && + (f.top = t.top - s.top + c), t.left != null && + (f.left = t.left - s.left + h), "using" in t + ? t.using.call(e, f) + : i.css(f); + } + }, v.fn.extend({ + position: function() { + if (!this[0]) return; + var e = this[0], + t = this.offsetParent(), + n = this.offset(), + r = er.test(t[0].nodeName) ? { top: 0, left: 0 } : t.offset(); + return n.top -= parseFloat(v.css(e, "marginTop")) || + 0, n.left -= parseFloat(v.css(e, "marginLeft")) || + 0, r.top += parseFloat(v.css(t[0], "borderTopWidth")) || + 0, r.left += parseFloat(v.css(t[0], "borderLeftWidth")) || 0, { + top: n.top - r.top, + left: n.left - r.left + }; + }, + offsetParent: function() { + return this.map(function() { + var e = this.offsetParent || i.body; + while ( + e && !er.test(e.nodeName) && v.css(e, "position") === "static" + ) e = e.offsetParent; + return e || i.body; + }); + } + }), v.each({ scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( + e, + n + ) { + var r = /Y/.test(n); + v.fn[e] = function(i) { + return v.access( + this, + function(e, i, s) { + var o = tr(e); + if (s === t) { + return o ? n in o ? o[n] : o.document.documentElement[i] : e[i]; + } + o + ? o.scrollTo(r ? v(o).scrollLeft() : s, r ? s : v(o).scrollTop()) + : e[i] = s; + }, + e, + i, + arguments.length, + null + ); + }; + }), v.each({ Height: "height", Width: "width" }, function(e, n) { + v.each({ padding: "inner" + e, content: n, "": "outer" + e }, function( + r, + i + ) { + v.fn[i] = function(i, s) { + var o = arguments.length && (r || typeof i !== "boolean"), + u = r || (i === !0 || s === !0 ? "margin" : "border"); + return v.access( + this, + function(n, r, i) { + var s; + return v.isWindow(n) + ? n.document.documentElement["client" + e] + : n.nodeType === 9 + ? (s = n.documentElement, Math.max( + n.body["scroll" + e], + s["scroll" + e], + n.body["offset" + e], + s["offset" + e], + s["client" + e] + )) + : i === t ? v.css(n, r, i, u) : v.style(n, r, i, u); + }, + n, + o ? i : t, + o, + null + ); + }; + }); + }), e.jQuery = e.$ = v, typeof define === "function" && + define.amd && + define.amd.jQuery && + define("jquery", [], function() { + return v; + }); +})(window); diff --git a/test/jasmine/assets/modebar_button.js b/test/jasmine/assets/modebar_button.js index 3464cf7b403..40a9c64cd72 100644 --- a/test/jasmine/assets/modebar_button.js +++ b/test/jasmine/assets/modebar_button.js @@ -1,25 +1,24 @@ -'use strict'; - -var d3 = require('d3'); - -var modeBarButtons = require('@src/components/modebar/buttons'); +"use strict"; +var d3 = require("d3"); +var modeBarButtons = require("@src/components/modebar/buttons"); module.exports = function selectButton(modeBar, name) { - var button = {}; + var button = {}; - var node = button.node = d3.select(modeBar.element) - .select('[data-title="' + modeBarButtons[name].title + '"]') - .node(); + var node = button.node = d3 + .select(modeBar.element) + .select('[data-title="' + modeBarButtons[name].title + '"]') + .node(); - button.click = function() { - var ev = new window.MouseEvent('click'); - node.dispatchEvent(ev); - }; + button.click = function() { + var ev = new window.MouseEvent("click"); + node.dispatchEvent(ev); + }; - button.isActive = function() { - return d3.select(node).classed('active'); - }; + button.isActive = function() { + return d3.select(node).classed("active"); + }; - return button; + return button; }; diff --git a/test/jasmine/assets/mouse_event.js b/test/jasmine/assets/mouse_event.js index 39101d76d35..9a674418a49 100644 --- a/test/jasmine/assets/mouse_event.js +++ b/test/jasmine/assets/mouse_event.js @@ -1,23 +1,18 @@ module.exports = function(type, x, y, opts) { - var fullOpts = { - bubbles: true, - clientX: x, - clientY: y - }; + var fullOpts = { bubbles: true, clientX: x, clientY: y }; - // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent - if(opts && opts.buttons) { - fullOpts.buttons = opts.buttons; - } + // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + if (opts && opts.buttons) { + fullOpts.buttons = opts.buttons; + } - var el = document.elementFromPoint(x, y), - ev; + var el = document.elementFromPoint(x, y), ev; - if(type === 'scroll') { - ev = new window.WheelEvent('wheel', opts); - } else { - ev = new window.MouseEvent(type, fullOpts); - } + if (type === "scroll") { + ev = new window.WheelEvent("wheel", opts); + } else { + ev = new window.MouseEvent(type, fullOpts); + } - el.dispatchEvent(ev); + el.dispatchEvent(ev); }; diff --git a/test/jasmine/assets/timed_click.js b/test/jasmine/assets/timed_click.js index b8806f2bd52..13822745fb6 100644 --- a/test/jasmine/assets/timed_click.js +++ b/test/jasmine/assets/timed_click.js @@ -1,17 +1,19 @@ -var mouseEvent = require('./mouse_event'); +var mouseEvent = require("./mouse_event"); module.exports = function click(x, y) { - mouseEvent('mousemove', x, y, {buttons: 0}); + mouseEvent("mousemove", x, y, { buttons: 0 }); - window.setTimeout(function() { + window.setTimeout( + function() { + mouseEvent("mousedown", x, y, { buttons: 1 }); - mouseEvent('mousedown', x, y, {buttons: 1}); - - window.setTimeout(function() { - - mouseEvent('mouseup', x, y, {buttons: 0}); - - }, 50); - - }, 150); + window.setTimeout( + function() { + mouseEvent("mouseup", x, y, { buttons: 0 }); + }, + 50 + ); + }, + 150 + ); }; diff --git a/test/jasmine/bundle_tests/bar_test.js b/test/jasmine/bundle_tests/bar_test.js index 714159e9c47..b5777f7680a 100644 --- a/test/jasmine/bundle_tests/bar_test.js +++ b/test/jasmine/bundle_tests/bar_test.js @@ -1,34 +1,32 @@ -var d3 = require('d3'); +var d3 = require("d3"); -var Plotly = require('@lib/core'); -var PlotlyBar = require('@lib/bar'); +var Plotly = require("@lib/core"); +var PlotlyBar = require("@lib/bar"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +describe("Bundle with bar", function() { + "use strict"; + Plotly.register(PlotlyBar); -describe('Bundle with bar', function() { - 'use strict'; + var mock = require("@mocks/bar_line.json"); - Plotly.register(PlotlyBar); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + }); - var mock = require('@mocks/bar_line.json'); + afterEach(destroyGraphDiv); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); - }); + it("should graph scatter traces", function() { + var nodes = d3.selectAll("g.trace.scatter"); - afterEach(destroyGraphDiv); + expect(nodes.size()).toEqual(1); + }); - it('should graph scatter traces', function() { - var nodes = d3.selectAll('g.trace.scatter'); + it("should graph bar traces", function() { + var nodes = d3.selectAll("g.trace.bars"); - expect(nodes.size()).toEqual(1); - }); - - it('should graph bar traces', function() { - var nodes = d3.selectAll('g.trace.bars'); - - expect(nodes.size()).toEqual(1); - }); + expect(nodes.size()).toEqual(1); + }); }); diff --git a/test/jasmine/bundle_tests/choropleth_test.js b/test/jasmine/bundle_tests/choropleth_test.js index f79094d6607..f9ba000b787 100644 --- a/test/jasmine/bundle_tests/choropleth_test.js +++ b/test/jasmine/bundle_tests/choropleth_test.js @@ -1,28 +1,26 @@ -var d3 = require('d3'); +var d3 = require("d3"); -var Plotly = require('@lib/core'); -var PlotlyChoropleth = require('@lib/choropleth'); +var Plotly = require("@lib/core"); +var PlotlyChoropleth = require("@lib/choropleth"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +describe("Bundle with choropleth", function() { + "use strict"; + Plotly.register(PlotlyChoropleth); -describe('Bundle with choropleth', function() { - 'use strict'; + var mock = require("@mocks/geo_multiple-usa-choropleths.json"); - Plotly.register(PlotlyChoropleth); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + }); - var mock = require('@mocks/geo_multiple-usa-choropleths.json'); + afterEach(destroyGraphDiv); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); - }); + it("should graph choropleth traces", function() { + var nodes = d3.selectAll("g.trace.choropleth"); - afterEach(destroyGraphDiv); - - it('should graph choropleth traces', function() { - var nodes = d3.selectAll('g.trace.choropleth'); - - expect(nodes.size()).toEqual(4); - }); + expect(nodes.size()).toEqual(4); + }); }); diff --git a/test/jasmine/bundle_tests/contour_test.js b/test/jasmine/bundle_tests/contour_test.js index 8a42ee5b479..a52eae409d9 100644 --- a/test/jasmine/bundle_tests/contour_test.js +++ b/test/jasmine/bundle_tests/contour_test.js @@ -1,34 +1,32 @@ -var d3 = require('d3'); +var d3 = require("d3"); -var Plotly = require('@lib/core'); -var PlotlyContour = require('@lib/contour'); +var Plotly = require("@lib/core"); +var PlotlyContour = require("@lib/contour"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +describe("Bundle with contour", function() { + "use strict"; + Plotly.register(PlotlyContour); -describe('Bundle with contour', function() { - 'use strict'; + var mock = require("@mocks/contour_scatter.json"); - Plotly.register(PlotlyContour); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + }); - var mock = require('@mocks/contour_scatter.json'); + afterEach(destroyGraphDiv); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); - }); + it("should graph scatter traces", function() { + var nodes = d3.selectAll("g.trace.scatter"); - afterEach(destroyGraphDiv); + expect(nodes.size()).toEqual(1); + }); - it('should graph scatter traces', function() { - var nodes = d3.selectAll('g.trace.scatter'); + it("should graph contour traces", function() { + var nodes = d3.selectAll("g.contour"); - expect(nodes.size()).toEqual(1); - }); - - it('should graph contour traces', function() { - var nodes = d3.selectAll('g.contour'); - - expect(nodes.size()).toEqual(1); - }); + expect(nodes.size()).toEqual(1); + }); }); diff --git a/test/jasmine/bundle_tests/core_test.js b/test/jasmine/bundle_tests/core_test.js index 0079548ba1c..dcb215104ab 100644 --- a/test/jasmine/bundle_tests/core_test.js +++ b/test/jasmine/bundle_tests/core_test.js @@ -1,31 +1,29 @@ -var d3 = require('d3'); +var d3 = require("d3"); -var Plotly = require('@lib/core'); +var Plotly = require("@lib/core"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +describe("Bundle with core only", function() { + "use strict"; + var mock = require("@mocks/bar_line.json"); -describe('Bundle with core only', function() { - 'use strict'; + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + }); - var mock = require('@mocks/bar_line.json'); + afterEach(destroyGraphDiv); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); - }); + it("should graph scatter traces", function() { + var nodes = d3.selectAll("g.trace.scatter"); - afterEach(destroyGraphDiv); + expect(nodes.size()).toEqual(mock.data.length); + }); - it('should graph scatter traces', function() { - var nodes = d3.selectAll('g.trace.scatter'); + it("should not graph bar traces", function() { + var nodes = d3.selectAll("g.trace.bars"); - expect(nodes.size()).toEqual(mock.data.length); - }); - - it('should not graph bar traces', function() { - var nodes = d3.selectAll('g.trace.bars'); - - expect(nodes.size()).toEqual(0); - }); + expect(nodes.size()).toEqual(0); + }); }); diff --git a/test/jasmine/bundle_tests/finance_test.js b/test/jasmine/bundle_tests/finance_test.js index b56e10e14b6..4e7250fe5bf 100644 --- a/test/jasmine/bundle_tests/finance_test.js +++ b/test/jasmine/bundle_tests/finance_test.js @@ -1,41 +1,44 @@ -var Plotly = require('@lib/core'); -var ohlc = require('@lib/ohlc'); -var candlestick = require('@lib/candlestick'); +var Plotly = require("@lib/core"); +var ohlc = require("@lib/ohlc"); +var candlestick = require("@lib/candlestick"); -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); -describe('Bundle with finance trace type', function() { - 'use strict'; +describe("Bundle with finance trace type", function() { + "use strict"; + Plotly.register([ohlc, candlestick]); - Plotly.register([ohlc, candlestick]); + var mock = require("@mocks/finance_style.json"); - var mock = require('@mocks/finance_style.json'); + it( + "should register the correct trace modules for the generated traces", + function() { + var transformModules = Object.keys(Plotly.Plots.transformsRegistry); - it('should register the correct trace modules for the generated traces', function() { - var transformModules = Object.keys(Plotly.Plots.transformsRegistry); + expect(transformModules).toEqual(["ohlc", "candlestick"]); + } + ); - expect(transformModules).toEqual(['ohlc', 'candlestick']); - }); - - it('should register the correct trace modules for the generated traces', function() { - var traceModules = Object.keys(Plotly.Plots.modules); - - expect(traceModules).toEqual(['scatter', 'box', 'ohlc', 'candlestick']); - }); - - it('should graph ohlc and candlestick traces', function(done) { + it( + "should register the correct trace modules for the generated traces", + function() { + var traceModules = Object.keys(Plotly.Plots.modules); - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(function() { - var gSubplot = d3.select('g.cartesianlayer'); + expect(traceModules).toEqual(["scatter", "box", "ohlc", "candlestick"]); + } + ); - expect(gSubplot.selectAll('g.trace.scatter').size()).toEqual(2); - expect(gSubplot.selectAll('g.trace.boxes').size()).toEqual(2); + it("should graph ohlc and candlestick traces", function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(function() { + var gSubplot = d3.select("g.cartesianlayer"); - destroyGraphDiv(); - done(); - }); + expect(gSubplot.selectAll("g.trace.scatter").size()).toEqual(2); + expect(gSubplot.selectAll("g.trace.boxes").size()).toEqual(2); + destroyGraphDiv(); + done(); }); + }); }); diff --git a/test/jasmine/bundle_tests/histogram2dcontour_test.js b/test/jasmine/bundle_tests/histogram2dcontour_test.js index 2ef3773cca2..f1c37a914d5 100644 --- a/test/jasmine/bundle_tests/histogram2dcontour_test.js +++ b/test/jasmine/bundle_tests/histogram2dcontour_test.js @@ -1,41 +1,39 @@ -var d3 = require('d3'); +var d3 = require("d3"); -var Plotly = require('@lib/core'); -var PlotlyHistogram2dContour = require('@lib/histogram2dcontour'); -var PlotlyHistogram = require('@lib/histogram'); +var Plotly = require("@lib/core"); +var PlotlyHistogram2dContour = require("@lib/histogram2dcontour"); +var PlotlyHistogram = require("@lib/histogram"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +describe("Bundle with histogram2dcontour and histogram", function() { + "use strict"; + Plotly.register([PlotlyHistogram2dContour, PlotlyHistogram]); -describe('Bundle with histogram2dcontour and histogram', function() { - 'use strict'; + var mock = require("@mocks/2dhistogram_contour_subplots.json"); - Plotly.register([PlotlyHistogram2dContour, PlotlyHistogram]); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + }); - var mock = require('@mocks/2dhistogram_contour_subplots.json'); + afterEach(destroyGraphDiv); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); - }); + it("should graph scatter traces", function() { + var nodes = d3.selectAll("g.trace.scatter"); - afterEach(destroyGraphDiv); + expect(nodes.size()).toEqual(1); + }); - it('should graph scatter traces', function() { - var nodes = d3.selectAll('g.trace.scatter'); + it("should graph contour traces", function() { + var nodes = d3.selectAll("g.contour"); - expect(nodes.size()).toEqual(1); - }); + expect(nodes.size()).toEqual(1); + }); - it('should graph contour traces', function() { - var nodes = d3.selectAll('g.contour'); + it("should graph histogram traces", function() { + var nodes = d3.selectAll("g.bars"); - expect(nodes.size()).toEqual(1); - }); - - it('should graph histogram traces', function() { - var nodes = d3.selectAll('g.bars'); - - expect(nodes.size()).toEqual(2); - }); + expect(nodes.size()).toEqual(2); + }); }); diff --git a/test/jasmine/bundle_tests/ie9_test.js b/test/jasmine/bundle_tests/ie9_test.js index d874657245b..58799ef6117 100644 --- a/test/jasmine/bundle_tests/ie9_test.js +++ b/test/jasmine/bundle_tests/ie9_test.js @@ -1,42 +1,40 @@ -var Plotly = require('@lib/core'); +var Plotly = require("@lib/core"); Plotly.register([ - require('@lib/bar'), - require('@lib/box'), - require('@lib/heatmap'), - require('@lib/histogram'), - require('@lib/histogram2d'), - require('@lib/histogram2dcontour'), - require('@lib/pie'), - require('@lib/contour'), - require('@lib/scatterternary'), - require('@lib/ohlc'), - require('@lib/candlestick') + require("@lib/bar"), + require("@lib/box"), + require("@lib/heatmap"), + require("@lib/histogram"), + require("@lib/histogram2d"), + require("@lib/histogram2dcontour"), + require("@lib/pie"), + require("@lib/contour"), + require("@lib/scatterternary"), + require("@lib/ohlc"), + require("@lib/candlestick") ]); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); - -describe('Bundle with IE9 supported trace types:', function() { - - afterEach(destroyGraphDiv); - - it(' check that ie9_mock.js did its job', function() { - expect(function() { return ArrayBuffer; }) - .toThrow(new ReferenceError('ArrayBuffer is not defined')); - expect(function() { return Uint8Array; }) - .toThrow(new ReferenceError('Uint8Array is not defined')); - }); - - it('heatmaps with smoothing should work', function(done) { - var gd = createGraphDiv(); - var data = [{ - type: 'heatmap', - z: [[1, 2, 3], [2, 1, 2]], - zsmooth: 'best' - }]; - - Plotly.plot(gd, data).then(done); - }); - +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); + +describe("Bundle with IE9 supported trace types:", function() { + afterEach(destroyGraphDiv); + + it(" check that ie9_mock.js did its job", function() { + expect(function() { + return ArrayBuffer; + }).toThrow(new ReferenceError("ArrayBuffer is not defined")); + expect(function() { + return Uint8Array; + }).toThrow(new ReferenceError("Uint8Array is not defined")); + }); + + it("heatmaps with smoothing should work", function(done) { + var gd = createGraphDiv(); + var data = [ + { type: "heatmap", z: [[1, 2, 3], [2, 1, 2]], zsmooth: "best" } + ]; + + Plotly.plot(gd, data).then(done); + }); }); diff --git a/test/jasmine/bundle_tests/requirejs_test.js b/test/jasmine/bundle_tests/requirejs_test.js index ab6228e0e83..e1ca86c3054 100644 --- a/test/jasmine/bundle_tests/requirejs_test.js +++ b/test/jasmine/bundle_tests/requirejs_test.js @@ -1,16 +1,15 @@ -describe('plotly.js + require.js', function() { - 'use strict'; +describe("plotly.js + require.js", function() { + "use strict"; + it("should preserve require.js globals", function() { + expect(window.requirejs).toBeDefined(); + expect(window.define).toBeDefined(); + expect(window.require).toBeDefined(); + }); - it('should preserve require.js globals', function() { - expect(window.requirejs).toBeDefined(); - expect(window.define).toBeDefined(); - expect(window.require).toBeDefined(); - }); - - it('should be able to import plotly.min.js', function(done) { - require(['plotly'], function(Plotly) { - expect(Plotly).toBeDefined(); - done(); - }); + it("should be able to import plotly.min.js", function(done) { + require(["plotly"], function(Plotly) { + expect(Plotly).toBeDefined(); + done(); }); + }); }); diff --git a/test/jasmine/karma.ciconf.js b/test/jasmine/karma.ciconf.js index 7b782e60cea..0cf84f3883b 100644 --- a/test/jasmine/karma.ciconf.js +++ b/test/jasmine/karma.ciconf.js @@ -1,42 +1,39 @@ // Karma configuration function func(config) { - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - func.defaultConfig.logLevel = config.LOG_INFO; - - // Note: config.LOG_DEBUG may not be verbose enough to pin down the source of failed tests. - // See the note in CONTRIBUTING.md about karma-verbose-reporter: - // func.defaultConfig.reporters = ['verbose']; - - - // Continuous Integration mode - - /* + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + func.defaultConfig.logLevel = config.LOG_INFO; + + // Note: config.LOG_DEBUG may not be verbose enough to pin down the source of failed tests. + // See the note in CONTRIBUTING.md about karma-verbose-reporter: + // func.defaultConfig.reporters = ['verbose']; + // Continuous Integration mode + /* * WebGL interaction test cases fail on the CircleCI * most likely due to a WebGL/driver issue; * exclude them from the CircleCI test bundle. * */ - func.defaultConfig.exclude = [ - 'tests/gl_plot_interact_test.js', - 'tests/gl_plot_interact_basic_test.js', - 'tests/gl2d_scatterplot_contour_test.js', - 'tests/gl2d_pointcloud_test.js' - ]; + func.defaultConfig.exclude = [ + "tests/gl_plot_interact_test.js", + "tests/gl_plot_interact_basic_test.js", + "tests/gl2d_scatterplot_contour_test.js", + "tests/gl2d_pointcloud_test.js" + ]; - // if true, Karma captures browsers, runs the tests and exits - func.defaultConfig.singleRun = true; + // if true, Karma captures browsers, runs the tests and exits + func.defaultConfig.singleRun = true; - func.defaultConfig.browserNoActivityTimeout = 30000; // 30 seconds + func.defaultConfig.browserNoActivityTimeout = 30000; - func.defaultConfig.autoWatch = false; + // 30 seconds + func.defaultConfig.autoWatch = false; - func.defaultConfig.browsers = ['Firefox_WindowSized']; + func.defaultConfig.browsers = ["Firefox_WindowSized"]; - config.set(func.defaultConfig); + config.set(func.defaultConfig); } -func.defaultConfig = require('./karma.conf').defaultConfig; +func.defaultConfig = require("./karma.conf").defaultConfig; module.exports = func; diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js index bd84e867925..e2f0a3b7c69 100644 --- a/test/jasmine/karma.conf.js +++ b/test/jasmine/karma.conf.js @@ -1,4 +1,4 @@ -/* eslint-env node*/ +/* eslint-env node */ // Karma configuration @@ -13,140 +13,110 @@ * */ -var constants = require('../../tasks/util/constants'); +var constants = require("../../tasks/util/constants"); var arg = process.argv[4]; -var testFileGlob = arg ? arg : 'tests/*_test.js'; -var isSingleSuiteRun = (arg && arg.indexOf('bundle_tests/') === -1); -var isRequireJSTest = (arg && arg.indexOf('bundle_tests/requirejs') !== -1); -var isIE9Test = (arg && arg.indexOf('bundle_tests/ie9') !== -1); - -var pathToMain = '../../lib/index.js'; -var pathToJQuery = 'assets/jquery-1.8.3.min.js'; +var testFileGlob = arg ? arg : "tests/*_test.js"; +var isSingleSuiteRun = arg && arg.indexOf("bundle_tests/") === -1; +var isRequireJSTest = arg && arg.indexOf("bundle_tests/requirejs") !== -1; +var isIE9Test = arg && arg.indexOf("bundle_tests/ie9") !== -1; +var pathToMain = "../../lib/index.js"; +var pathToJQuery = "assets/jquery-1.8.3.min.js"; function func(config) { - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - // - // NB: if you try config.LOG_DEBUG, you may actually be looking for karma-verbose-reporter. - // See CONTRIBUTING.md for additional notes on reporting. - func.defaultConfig.logLevel = config.LOG_INFO; - - config.set(func.defaultConfig); + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + // + // NB: if you try config.LOG_DEBUG, you may actually be looking for karma-verbose-reporter. + // See CONTRIBUTING.md for additional notes on reporting. + func.defaultConfig.logLevel = config.LOG_INFO; + + config.set(func.defaultConfig); } func.defaultConfig = { - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '.', - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine', 'browserify'], - - // list of files / patterns to load in the browser - // - // N.B. this field is filled below - files: [], - - exclude: [], - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - // - // N.B. this field is filled below - preprocessors: {}, - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - // - // See note in CONTRIBUTING.md about more verbose reporting via karma-verbose-reporter: - // https://www.npmjs.com/package/karma-verbose-reporter ('verbose') - // - reporters: ['progress'], - - // web server port - port: 9876, - - // enable / disable colors in the output (reporters and logs) - colors: true, - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Chrome_WindowSized'], - - // custom browser options - // window-size values came from observing default size - customLaunchers: { - Chrome_WindowSized: { - base: 'Chrome', - flags: ['--window-size=1035,617', '--ignore-gpu-blacklist'] - }, - Firefox_WindowSized: { - base: 'Firefox', - flags: ['--width=1035', '--height=617'] - } + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: ".", + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["jasmine", "browserify"], + // list of files / patterns to load in the browser + // + // N.B. this field is filled below + files: [], + exclude: [], + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + // + // N.B. this field is filled below + preprocessors: {}, + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + // + // See note in CONTRIBUTING.md about more verbose reporting via karma-verbose-reporter: + // https://www.npmjs.com/package/karma-verbose-reporter ('verbose') + // + reporters: ["progress"], + // web server port + port: 9876, + // enable / disable colors in the output (reporters and logs) + colors: true, + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ["Chrome_WindowSized"], + // custom browser options + // window-size values came from observing default size + customLaunchers: { + Chrome_WindowSized: { + base: "Chrome", + flags: ["--window-size=1035,617", "--ignore-gpu-blacklist"] }, - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: false, - - browserify: { - transform: ['../../tasks/util/shortcut_paths.js'], - extensions: ['.js'], - watch: true, - debug: true + Firefox_WindowSized: { + base: "Firefox", + flags: ["--width=1035", "--height=617"] } + }, + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false, + browserify: { + transform: ["../../tasks/util/shortcut_paths.js"], + extensions: [".js"], + watch: true, + debug: true + } }; - // Add lib/index.js to single-suite runs, // to avoid import conflicts due to plotly.js // circular dependencies. -if(isSingleSuiteRun) { - func.defaultConfig.files = [ - pathToJQuery, - pathToMain, - testFileGlob - ]; - - func.defaultConfig.preprocessors[pathToMain] = ['browserify']; - func.defaultConfig.preprocessors[testFileGlob] = ['browserify']; -} -else if(isRequireJSTest) { - func.defaultConfig.files = [ - constants.pathToRequireJS, - constants.pathToRequireJSFixture, - testFileGlob - ]; -} -else if(isIE9Test) { - // load ie9_mock.js before plotly.js+test bundle - // to catch reference errors that could occur - // when plotly.js is first loaded. - - func.defaultConfig.files = [ - './assets/ie9_mock.js', - testFileGlob - ]; - - func.defaultConfig.preprocessors[testFileGlob] = ['browserify']; -} -else { - func.defaultConfig.files = [ - pathToJQuery, - testFileGlob - ]; - - func.defaultConfig.preprocessors[testFileGlob] = ['browserify']; +if (isSingleSuiteRun) { + func.defaultConfig.files = [pathToJQuery, pathToMain, testFileGlob]; + + func.defaultConfig.preprocessors[pathToMain] = ["browserify"]; + func.defaultConfig.preprocessors[testFileGlob] = ["browserify"]; +} else if (isRequireJSTest) { + func.defaultConfig.files = [ + constants.pathToRequireJS, + constants.pathToRequireJSFixture, + testFileGlob + ]; +} else if (isIE9Test) { + // load ie9_mock.js before plotly.js+test bundle + // to catch reference errors that could occur + // when plotly.js is first loaded. + func.defaultConfig.files = ["./assets/ie9_mock.js", testFileGlob]; + + func.defaultConfig.preprocessors[testFileGlob] = ["browserify"]; +} else { + func.defaultConfig.files = [pathToJQuery, testFileGlob]; + + func.defaultConfig.preprocessors[testFileGlob] = ["browserify"]; } module.exports = func; diff --git a/test/jasmine/tests/animate_test.js b/test/jasmine/tests/animate_test.js index 53eb151c01c..b6c34511a28 100644 --- a/test/jasmine/tests/animate_test.js +++ b/test/jasmine/tests/animate_test.js @@ -1,771 +1,1031 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); var Plots = Plotly.Plots; -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); -var delay = require('../assets/delay'); - -var mock = require('@mocks/animation'); - -describe('Plots.supplyAnimationDefaults', function() { - 'use strict'; - - it('supplies transition defaults', function() { - expect(Plots.supplyAnimationDefaults({})).toEqual({ - fromcurrent: false, - mode: 'afterall', - direction: 'forward', - transition: { - duration: 500, - easing: 'cubic-in-out' - }, - frame: { - duration: 500, - redraw: true - } - }); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var fail = require("../assets/fail_test"); +var delay = require("../assets/delay"); + +var mock = require("@mocks/animation"); + +describe("Plots.supplyAnimationDefaults", function() { + "use strict"; + it("supplies transition defaults", function() { + expect(Plots.supplyAnimationDefaults({})).toEqual({ + fromcurrent: false, + mode: "afterall", + direction: "forward", + transition: { duration: 500, easing: "cubic-in-out" }, + frame: { duration: 500, redraw: true } }); - - it('uses provided values', function() { - expect(Plots.supplyAnimationDefaults({ - mode: 'next', - fromcurrent: true, - direction: 'reverse', - transition: { - duration: 600, - easing: 'elastic-in-out' - }, - frame: { - duration: 700, - redraw: false - } - })).toEqual({ - mode: 'next', - fromcurrent: true, - direction: 'reverse', - transition: { - duration: 600, - easing: 'elastic-in-out' - }, - frame: { - duration: 700, - redraw: false - } - }); + }); + + it("uses provided values", function() { + expect( + Plots.supplyAnimationDefaults({ + mode: "next", + fromcurrent: true, + direction: "reverse", + transition: { duration: 600, easing: "elastic-in-out" }, + frame: { duration: 700, redraw: false } + }) + ).toEqual({ + mode: "next", + fromcurrent: true, + direction: "reverse", + transition: { duration: 600, easing: "elastic-in-out" }, + frame: { duration: 700, redraw: false } }); + }); }); -describe('Test animate API', function() { - 'use strict'; - - var gd, mockCopy; +describe("Test animate API", function() { + "use strict"; + var gd, mockCopy; - function verifyQueueEmpty(gd) { - expect(gd._transitionData._frameQueue.length).toEqual(0); - } + function verifyQueueEmpty(gd) { + expect(gd._transitionData._frameQueue.length).toEqual(0); + } - function verifyFrameTransitionOrder(gd, expectedFrames) { - var calls = Plots.transition.calls; + function verifyFrameTransitionOrder(gd, expectedFrames) { + var calls = Plots.transition.calls; - var c1 = calls.count(); - var c2 = expectedFrames.length; - expect(c1).toEqual(c2); + var c1 = calls.count(); + var c2 = expectedFrames.length; + expect(c1).toEqual(c2); - // Prevent lots of ugly logging when it's already failed: - if(c1 !== c2) return; + // Prevent lots of ugly logging when it's already failed: + if (c1 !== c2) return; - for(var i = 0; i < calls.count(); i++) { - expect(calls.argsFor(i)[1]).toEqual( - gd._transitionData._frameHash[expectedFrames[i]].data - ); - } + for (var i = 0; i < calls.count(); i++) { + expect(calls.argsFor(i)[1]).toEqual( + gd._transitionData._frameHash[expectedFrames[i]].data + ); } + } + + beforeEach(function(done) { + gd = createGraphDiv(); + + mockCopy = Lib.extendDeep({}, mock); + + // ------------------------------------------------------------ + // NB: TRANSITION IS FAKED + // + // This means that you should not expect `.animate` to actually + // modify the plot in any way in the tests below. For tests + // involvingnon-faked transitions, see the bottom of this file. + // ------------------------------------------------------------ + spyOn(Plots, "transition").and.callFake(function() { + // Transition's fake behavior is just to delay by the duration + // and resolve: + return Promise.resolve().then(delay(arguments[5].duration)); + }); - beforeEach(function(done) { - gd = createGraphDiv(); - - mockCopy = Lib.extendDeep({}, mock); - - // ------------------------------------------------------------ - // NB: TRANSITION IS FAKED - // - // This means that you should not expect `.animate` to actually - // modify the plot in any way in the tests below. For tests - // involvingnon-faked transitions, see the bottom of this file. - // ------------------------------------------------------------ + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + return Plotly.addFrames(gd, mockCopy.frames); + }) + .then(done); + }); + + afterEach(function() { + // *must* purge between tests otherwise dangling async events might not get cleaned up properly: + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("throws an error on addFrames if gd is not a graph", function() { + var gd2 = document.createElement("div"); + gd2.id = "invalidgd"; + document.body.appendChild(gd2); + + expect(function() { + Plotly.addFrames(gd2, [{}]); + }).toThrow(new Error( + "This element is not a Plotly plot: [object HTMLDivElement]. It's likely that you've failed to create a plot before adding frames. For more details, see https://plot.ly/javascript/animations/" + )); + + document.body.removeChild(gd); + }); + + it("throws an error on animate if gd is not a graph", function() { + var gd2 = document.createElement("div"); + gd2.id = "invalidgd"; + document.body.appendChild(gd2); + + expect(function() { + Plotly.animate(gd2, { data: [{}] }); + }).toThrow(new Error( + "This element is not a Plotly plot: [object HTMLDivElement]. It's likely that you've failed to create a plot before animating it. For more details, see https://plot.ly/javascript/animations/" + )); + + document.body.removeChild(gd); + }); + + runTests(0); + runTests(30); + + function runTests(duration) { + describe("With duration = " + duration, function() { + var animOpts; + + beforeEach(function() { + animOpts = { + frame: { duration: duration }, + transition: { duration: duration * 0.5 } + }; + }); + + it("animates to a frame", function(done) { + Plotly.animate(gd, ["frame0"], { + transition: { duration: 1.2345 }, + frame: { duration: 1.5678 } + }) + .then(function() { + expect(Plots.transition).toHaveBeenCalled(); + + var args = Plots.transition.calls.mostRecent().args; + + // was called with gd, data, layout, traceIndices, transitionConfig: + expect(args.length).toEqual(6); + + // data has two traces: + expect(args[1].length).toEqual(2); + + // Verify frame config has been passed: + expect(args[4].duration).toEqual(1.5678); + + // Verify transition config has been passed: + expect(args[5].duration).toEqual(1.2345); + + // layout + expect(args[2]).toEqual({ + xaxis: { range: [0, 2] }, + yaxis: { range: [0, 10] } + }); - spyOn(Plots, 'transition').and.callFake(function() { - // Transition's fake behavior is just to delay by the duration - // and resolve: - return Promise.resolve().then(delay(arguments[5].duration)); + // traces are [0, 1]: + expect(args[3]).toEqual([0, 1]); + }) + .catch(fail) + .then(done); + }); + + it("rejects if a frame is not found", function(done) { + Plotly.animate(gd, ["foobar"], animOpts).then(fail).then(done, done); + }); + + it("treats objects as frames", function(done) { + var frame = { data: [{ x: [1, 2, 3] }] }; + Plotly.animate(gd, frame, animOpts) + .then(function() { + expect(Plots.transition.calls.count()).toEqual(1); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + + it("treats a list of objects as frames", function(done) { + var frame1 = { + data: [{ x: [1, 2, 3] }], + traces: [0], + layout: { foo: "bar" } + }; + var frame2 = { + data: [{ x: [3, 4, 5] }], + traces: [1], + layout: { foo: "baz" } + }; + Plotly.animate(gd, [frame1, frame2], animOpts) + .then(function() { + expect(Plots.transition.calls.argsFor(0)[1]).toEqual(frame1.data); + expect(Plots.transition.calls.argsFor(0)[2]).toEqual(frame1.layout); + expect(Plots.transition.calls.argsFor(0)[3]).toEqual(frame1.traces); + + expect(Plots.transition.calls.argsFor(1)[1]).toEqual(frame2.data); + expect(Plots.transition.calls.argsFor(1)[2]).toEqual(frame2.layout); + expect(Plots.transition.calls.argsFor(1)[3]).toEqual(frame2.traces); + + expect(Plots.transition.calls.count()).toEqual(2); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + + it("animates all frames if list is null", function(done) { + Plotly.animate(gd, null, animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "base", + "frame0", + "frame1", + "frame2", + "frame3" + ]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + + it("animates all frames if list is undefined", function(done) { + Plotly.animate(gd, undefined, animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "base", + "frame0", + "frame1", + "frame2", + "frame3" + ]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + + it("animates to a single frame", function(done) { + Plotly.animate(gd, ["frame0"], animOpts) + .then(function() { + expect(Plots.transition.calls.count()).toEqual(1); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + + it("animates to an empty list", function(done) { + Plotly.animate(gd, [], animOpts) + .then(function() { + expect(Plots.transition.calls.count()).toEqual(0); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + + it("animates to a list of frames", function(done) { + Plotly.animate(gd, ["frame0", "frame1"], animOpts) + .then(function() { + expect(Plots.transition.calls.count()).toEqual(2); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + + it("animates frames by group", function(done) { + Plotly.animate(gd, "even-frames", animOpts) + .then(function() { + expect(Plots.transition.calls.count()).toEqual(2); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + + it("animates frames in the correct order", function(done) { + Plotly.animate(gd, ["frame0", "frame2", "frame1", "frame3"], animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "frame0", + "frame2", + "frame1", + "frame3" + ]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + + it("accepts a single animationOpts", function(done) { + Plotly.animate(gd, ["frame0", "frame1"], { + transition: { duration: 1.12345 } + }) + .then(function() { + var calls = Plots.transition.calls; + expect(calls.argsFor(0)[5].duration).toEqual(1.12345); + expect(calls.argsFor(1)[5].duration).toEqual(1.12345); + }) + .catch(fail) + .then(done); + }); + + it("accepts an array of animationOpts", function(done) { + Plotly.animate(gd, ["frame0", "frame1"], { + transition: [{ duration: 1.123 }, { duration: 1.456 }], + frame: [{ duration: 8.7654 }, { duration: 5.4321 }] + }) + .then(function() { + var calls = Plots.transition.calls; + expect(calls.argsFor(0)[4].duration).toEqual(8.7654); + expect(calls.argsFor(1)[4].duration).toEqual(5.4321); + expect(calls.argsFor(0)[5].duration).toEqual(1.123); + expect(calls.argsFor(1)[5].duration).toEqual(1.456); + }) + .catch(fail) + .then(done); + }); + + it( + "falls back to animationOpts[0] if not enough supplied in array", + function(done) { + Plotly.animate(gd, ["frame0", "frame1"], { + transition: [{ duration: 1.123 }], + frame: [{ duration: 2.345 }] + }) + .then(function() { + var calls = Plots.transition.calls; + expect(calls.argsFor(0)[4].duration).toEqual(2.345); + expect(calls.argsFor(1)[4].duration).toEqual(2.345); + expect(calls.argsFor(0)[5].duration).toEqual(1.123); + expect(calls.argsFor(1)[5].duration).toEqual(1.123); + }) + .catch(fail) + .then(done); + } + ); + + it("chains animations as promises", function(done) { + Plotly.animate(gd, ["frame0", "frame1"], animOpts) + .then(function() { + return Plotly.animate(gd, ["frame2", "frame3"], animOpts); + }) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "frame0", + "frame1", + "frame2", + "frame3" + ]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + + it("emits plotly_animated before the promise is resolved", function( + done + ) { + var animated = false; + gd.on("plotly_animated", function() { + animated = true; }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - return Plotly.addFrames(gd, mockCopy.frames); - }).then(done); - }); - - afterEach(function() { - // *must* purge between tests otherwise dangling async events might not get cleaned up properly: - Plotly.purge(gd); - destroyGraphDiv(); + Plotly.animate(gd, ["frame0"], animOpts) + .then(function() { + expect(animated).toBe(true); + }) + .catch(fail) + .then(done); + }); + + it( + "emits plotly_animated as each animation in a sequence completes", + function(done) { + var completed = 0; + var test1 = 0, test2 = 0; + gd.on("plotly_animated", function() { + completed++; + if (completed === 1) { + // Verify that after the first plotly_animated, precisely frame0 and frame1 + // have been transitioned to: + verifyFrameTransitionOrder(gd, ["frame0", "frame1"]); + test1++; + } else { + // Verify that after the second plotly_animated, precisely all frames + // have been transitioned to: + verifyFrameTransitionOrder(gd, [ + "frame0", + "frame1", + "frame2", + "frame3" + ]); + test2++; + } + }); + + Plotly.animate(gd, ["frame0", "frame1"], animOpts) + .then(function() { + return Plotly.animate(gd, ["frame2", "frame3"], animOpts); + }) + .then(function() { + expect(test1).toBe(1); + expect(test2).toBe(1); + }) + .catch(fail) + .then(done); + } + ); + + it("resolves at the end of each animation sequence", function(done) { + Plotly.animate(gd, "even-frames", animOpts) + .then(function() { + return Plotly.animate( + gd, + ["frame0", "frame2", "frame1", "frame3"], + animOpts + ); + }) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "frame0", + "frame2", + "frame0", + "frame2", + "frame1", + "frame3" + ]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); }); + } - it('throws an error on addFrames if gd is not a graph', function() { - var gd2 = document.createElement('div'); - gd2.id = 'invalidgd'; - document.body.appendChild(gd2); + describe("Animation direction", function() { + var animOpts; - expect(function() { - Plotly.addFrames(gd2, [{}]); - }).toThrow(new Error('This element is not a Plotly plot: [object HTMLDivElement]. It\'s likely that you\'ve failed to create a plot before adding frames. For more details, see https://plot.ly/javascript/animations/')); - - document.body.removeChild(gd); + beforeEach(function() { + animOpts = { frame: { duration: 0 }, transition: { duration: 0 } }; }); - it('throws an error on animate if gd is not a graph', function() { - var gd2 = document.createElement('div'); - gd2.id = 'invalidgd'; - document.body.appendChild(gd2); - - expect(function() { - Plotly.animate(gd2, {data: [{}]}); - }).toThrow(new Error('This element is not a Plotly plot: [object HTMLDivElement]. It\'s likely that you\'ve failed to create a plot before animating it. For more details, see https://plot.ly/javascript/animations/')); - - document.body.removeChild(gd); + it("animates frames by name in reverse", function(done) { + animOpts.direction = "reverse"; + + Plotly.animate(gd, ["frame0", "frame2", "frame1", "frame3"], animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "frame3", + "frame1", + "frame2", + "frame0" + ]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); }); - runTests(0); - runTests(30); - - function runTests(duration) { - describe('With duration = ' + duration, function() { - var animOpts; - - beforeEach(function() { - animOpts = {frame: {duration: duration}, transition: {duration: duration * 0.5}}; - }); - - it('animates to a frame', function(done) { - Plotly.animate(gd, ['frame0'], {transition: {duration: 1.2345}, frame: {duration: 1.5678}}).then(function() { - expect(Plots.transition).toHaveBeenCalled(); - - var args = Plots.transition.calls.mostRecent().args; - - // was called with gd, data, layout, traceIndices, transitionConfig: - expect(args.length).toEqual(6); - - // data has two traces: - expect(args[1].length).toEqual(2); - - // Verify frame config has been passed: - expect(args[4].duration).toEqual(1.5678); - - // Verify transition config has been passed: - expect(args[5].duration).toEqual(1.2345); - - // layout - expect(args[2]).toEqual({ - xaxis: {range: [0, 2]}, - yaxis: {range: [0, 10]} - }); - - // traces are [0, 1]: - expect(args[3]).toEqual([0, 1]); - }).catch(fail).then(done); - }); - - it('rejects if a frame is not found', function(done) { - Plotly.animate(gd, ['foobar'], animOpts).then(fail).then(done, done); - }); - - it('treats objects as frames', function(done) { - var frame = {data: [{x: [1, 2, 3]}]}; - Plotly.animate(gd, frame, animOpts).then(function() { - expect(Plots.transition.calls.count()).toEqual(1); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('treats a list of objects as frames', function(done) { - var frame1 = {data: [{x: [1, 2, 3]}], traces: [0], layout: {foo: 'bar'}}; - var frame2 = {data: [{x: [3, 4, 5]}], traces: [1], layout: {foo: 'baz'}}; - Plotly.animate(gd, [frame1, frame2], animOpts).then(function() { - expect(Plots.transition.calls.argsFor(0)[1]).toEqual(frame1.data); - expect(Plots.transition.calls.argsFor(0)[2]).toEqual(frame1.layout); - expect(Plots.transition.calls.argsFor(0)[3]).toEqual(frame1.traces); - - expect(Plots.transition.calls.argsFor(1)[1]).toEqual(frame2.data); - expect(Plots.transition.calls.argsFor(1)[2]).toEqual(frame2.layout); - expect(Plots.transition.calls.argsFor(1)[3]).toEqual(frame2.traces); - - expect(Plots.transition.calls.count()).toEqual(2); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('animates all frames if list is null', function(done) { - Plotly.animate(gd, null, animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['base', 'frame0', 'frame1', 'frame2', 'frame3']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('animates all frames if list is undefined', function(done) { - Plotly.animate(gd, undefined, animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['base', 'frame0', 'frame1', 'frame2', 'frame3']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('animates to a single frame', function(done) { - Plotly.animate(gd, ['frame0'], animOpts).then(function() { - expect(Plots.transition.calls.count()).toEqual(1); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('animates to an empty list', function(done) { - Plotly.animate(gd, [], animOpts).then(function() { - expect(Plots.transition.calls.count()).toEqual(0); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('animates to a list of frames', function(done) { - Plotly.animate(gd, ['frame0', 'frame1'], animOpts).then(function() { - expect(Plots.transition.calls.count()).toEqual(2); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('animates frames by group', function(done) { - Plotly.animate(gd, 'even-frames', animOpts).then(function() { - expect(Plots.transition.calls.count()).toEqual(2); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('animates frames in the correct order', function(done) { - Plotly.animate(gd, ['frame0', 'frame2', 'frame1', 'frame3'], animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['frame0', 'frame2', 'frame1', 'frame3']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('accepts a single animationOpts', function(done) { - Plotly.animate(gd, ['frame0', 'frame1'], {transition: {duration: 1.12345}}).then(function() { - var calls = Plots.transition.calls; - expect(calls.argsFor(0)[5].duration).toEqual(1.12345); - expect(calls.argsFor(1)[5].duration).toEqual(1.12345); - }).catch(fail).then(done); - }); - - it('accepts an array of animationOpts', function(done) { - Plotly.animate(gd, ['frame0', 'frame1'], { - transition: [{duration: 1.123}, {duration: 1.456}], - frame: [{duration: 8.7654}, {duration: 5.4321}] - }).then(function() { - var calls = Plots.transition.calls; - expect(calls.argsFor(0)[4].duration).toEqual(8.7654); - expect(calls.argsFor(1)[4].duration).toEqual(5.4321); - expect(calls.argsFor(0)[5].duration).toEqual(1.123); - expect(calls.argsFor(1)[5].duration).toEqual(1.456); - }).catch(fail).then(done); - }); - - it('falls back to animationOpts[0] if not enough supplied in array', function(done) { - Plotly.animate(gd, ['frame0', 'frame1'], { - transition: [{duration: 1.123}], - frame: [{duration: 2.345}] - }).then(function() { - var calls = Plots.transition.calls; - expect(calls.argsFor(0)[4].duration).toEqual(2.345); - expect(calls.argsFor(1)[4].duration).toEqual(2.345); - expect(calls.argsFor(0)[5].duration).toEqual(1.123); - expect(calls.argsFor(1)[5].duration).toEqual(1.123); - }).catch(fail).then(done); - }); - - it('chains animations as promises', function(done) { - Plotly.animate(gd, ['frame0', 'frame1'], animOpts).then(function() { - return Plotly.animate(gd, ['frame2', 'frame3'], animOpts); - }).then(function() { - verifyFrameTransitionOrder(gd, ['frame0', 'frame1', 'frame2', 'frame3']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('emits plotly_animated before the promise is resolved', function(done) { - var animated = false; - gd.on('plotly_animated', function() { - animated = true; - }); - - Plotly.animate(gd, ['frame0'], animOpts).then(function() { - expect(animated).toBe(true); - }).catch(fail).then(done); - }); - - it('emits plotly_animated as each animation in a sequence completes', function(done) { - var completed = 0; - var test1 = 0, test2 = 0; - gd.on('plotly_animated', function() { - completed++; - if(completed === 1) { - // Verify that after the first plotly_animated, precisely frame0 and frame1 - // have been transitioned to: - verifyFrameTransitionOrder(gd, ['frame0', 'frame1']); - test1++; - } else { - // Verify that after the second plotly_animated, precisely all frames - // have been transitioned to: - verifyFrameTransitionOrder(gd, ['frame0', 'frame1', 'frame2', 'frame3']); - test2++; - } - }); - - Plotly.animate(gd, ['frame0', 'frame1'], animOpts).then(function() { - return Plotly.animate(gd, ['frame2', 'frame3'], animOpts); - }).then(function() { - expect(test1).toBe(1); - expect(test2).toBe(1); - }).catch(fail).then(done); - }); - - it('resolves at the end of each animation sequence', function(done) { - Plotly.animate(gd, 'even-frames', animOpts).then(function() { - return Plotly.animate(gd, ['frame0', 'frame2', 'frame1', 'frame3'], animOpts); - }).then(function() { - verifyFrameTransitionOrder(gd, ['frame0', 'frame2', 'frame0', 'frame2', 'frame1', 'frame3']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - }); - } - - describe('Animation direction', function() { - var animOpts; - - beforeEach(function() { - animOpts = { - frame: {duration: 0}, - transition: {duration: 0} - }; - }); - - it('animates frames by name in reverse', function(done) { - animOpts.direction = 'reverse'; - - Plotly.animate(gd, ['frame0', 'frame2', 'frame1', 'frame3'], animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['frame3', 'frame1', 'frame2', 'frame0']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('animates a group in reverse', function(done) { - animOpts.direction = 'reverse'; - Plotly.animate(gd, 'even-frames', animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['frame2', 'frame0']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); + it("animates a group in reverse", function(done) { + animOpts.direction = "reverse"; + Plotly.animate(gd, "even-frames", animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, ["frame2", "frame0"]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); }); + }); - describe('Animation fromcurrent', function() { - var animOpts; - - beforeEach(function() { - animOpts = { - frame: {duration: 0}, - transition: {duration: 0}, - fromcurrent: true - }; - }); - - it('animates starting at the current frame', function(done) { - Plotly.animate(gd, ['frame1'], animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['frame1']); - verifyQueueEmpty(gd); - - return Plotly.animate(gd, null, animOpts); - }).then(function() { - verifyFrameTransitionOrder(gd, ['frame1', 'frame2', 'frame3']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('plays from the start when current frame = last frame', function(done) { - Plotly.animate(gd, null, animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['base', 'frame0', 'frame1', 'frame2', 'frame3']); - verifyQueueEmpty(gd); - - return Plotly.animate(gd, null, animOpts); - }).then(function() { - verifyFrameTransitionOrder(gd, [ - 'base', 'frame0', 'frame1', 'frame2', 'frame3', - 'base', 'frame0', 'frame1', 'frame2', 'frame3' - ]); - - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('animates in reverse starting at the current frame', function(done) { - animOpts.direction = 'reverse'; - - Plotly.animate(gd, ['frame1'], animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['frame1']); - verifyQueueEmpty(gd); - return Plotly.animate(gd, null, animOpts); - }).then(function() { - verifyFrameTransitionOrder(gd, ['frame1', 'frame0', 'base']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('plays in reverse from the end when current frame = first frame', function(done) { - animOpts.direction = 'reverse'; - - Plotly.animate(gd, ['base'], animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['base']); - verifyQueueEmpty(gd); - - return Plotly.animate(gd, null, animOpts); - }).then(function() { - verifyFrameTransitionOrder(gd, [ - 'base', 'frame3', 'frame2', 'frame1', 'frame0', 'base' - ]); + describe("Animation fromcurrent", function() { + var animOpts; - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); + beforeEach(function() { + animOpts = { + frame: { duration: 0 }, + transition: { duration: 0 }, + fromcurrent: true + }; }); - // The tests above use promises to ensure ordering, but the tests below this call Plotly.animate - // without chaining promises which would result in race conditions. This is not invalid behavior, - // but it doesn't ensure proper ordering and completion, so these must be performed with finite - // duration. Stricly speaking, these tests *do* involve race conditions, but the finite duration - // prevents that from causing problems. - describe('Calling Plotly.animate synchronously in series', function() { - var animOpts; - - beforeEach(function() { - animOpts = {frame: {duration: 30}}; - }); - - it('emits plotly_animationinterrupted when an animation is interrupted', function(done) { - var interrupted = false; - gd.on('plotly_animationinterrupted', function() { - interrupted = true; - }); - - Plotly.animate(gd, ['frame0', 'frame1'], animOpts); - - Plotly.animate(gd, ['frame2'], Lib.extendFlat(animOpts, {mode: 'immediate'})).then(function() { - expect(interrupted).toBe(true); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); - - it('queues successive animations', function(done) { - var starts = 0; - var ends = 0; + it("animates starting at the current frame", function(done) { + Plotly.animate(gd, ["frame1"], animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, ["frame1"]); + verifyQueueEmpty(gd); + + return Plotly.animate(gd, null, animOpts); + }) + .then(function() { + verifyFrameTransitionOrder(gd, ["frame1", "frame2", "frame3"]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); - gd.on('plotly_animating', function() { - starts++; - }).on('plotly_animated', function() { - ends++; - expect(Plots.transition.calls.count()).toEqual(4); - expect(starts).toEqual(1); - }); + it("plays from the start when current frame = last frame", function(done) { + Plotly.animate(gd, null, animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "base", + "frame0", + "frame1", + "frame2", + "frame3" + ]); + verifyQueueEmpty(gd); + + return Plotly.animate(gd, null, animOpts); + }) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "base", + "frame0", + "frame1", + "frame2", + "frame3", + "base", + "frame0", + "frame1", + "frame2", + "frame3" + ]); + + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); - Plotly.animate(gd, 'even-frames', {transition: {duration: 16}}); - Plotly.animate(gd, 'odd-frames', {transition: {duration: 16}}).then(delay(10)).then(function() { - expect(ends).toEqual(1); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); + it("animates in reverse starting at the current frame", function(done) { + animOpts.direction = "reverse"; + + Plotly.animate(gd, ["frame1"], animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, ["frame1"]); + verifyQueueEmpty(gd); + return Plotly.animate(gd, null, animOpts); + }) + .then(function() { + verifyFrameTransitionOrder(gd, ["frame1", "frame0", "base"]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); - it('an empty list with immediate dumps previous frames', function(done) { - Plotly.animate(gd, ['frame0', 'frame1'], {frame: {duration: 50}}); - Plotly.animate(gd, [], {mode: 'immediate'}).then(function() { - expect(Plots.transition.calls.count()).toEqual(1); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); + it( + "plays in reverse from the end when current frame = first frame", + function(done) { + animOpts.direction = "reverse"; + + Plotly.animate(gd, ["base"], animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, ["base"]); + verifyQueueEmpty(gd); + + return Plotly.animate(gd, null, animOpts); + }) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "base", + "frame3", + "frame2", + "frame1", + "frame0", + "base" + ]); + + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + } + ); + }); + + // The tests above use promises to ensure ordering, but the tests below this call Plotly.animate + // without chaining promises which would result in race conditions. This is not invalid behavior, + // but it doesn't ensure proper ordering and completion, so these must be performed with finite + // duration. Stricly speaking, these tests *do* involve race conditions, but the finite duration + // prevents that from causing problems. + describe("Calling Plotly.animate synchronously in series", function() { + var animOpts; - it('animates groups in the correct order', function(done) { - Plotly.animate(gd, 'even-frames', animOpts); - Plotly.animate(gd, 'odd-frames', animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['frame0', 'frame2', 'frame1', 'frame3']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); + beforeEach(function() { + animOpts = { frame: { duration: 30 } }; + }); - it('drops queued frames when immediate = true', function(done) { - Plotly.animate(gd, 'even-frames', animOpts); - Plotly.animate(gd, 'odd-frames', Lib.extendFlat(animOpts, {mode: 'immediate'})).then(function() { - verifyFrameTransitionOrder(gd, ['frame0', 'frame1', 'frame3']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); + it( + "emits plotly_animationinterrupted when an animation is interrupted", + function(done) { + var interrupted = false; + gd.on("plotly_animationinterrupted", function() { + interrupted = true; }); - it('animates frames and groups in sequence', function(done) { - Plotly.animate(gd, 'even-frames', animOpts); - Plotly.animate(gd, ['frame0', 'frame2', 'frame1', 'frame3'], animOpts).then(function() { - verifyFrameTransitionOrder(gd, ['frame0', 'frame2', 'frame0', 'frame2', 'frame1', 'frame3']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); + Plotly.animate(gd, ["frame0", "frame1"], animOpts); + + Plotly.animate( + gd, + ["frame2"], + Lib.extendFlat(animOpts, { mode: "immediate" }) + ) + .then(function() { + expect(interrupted).toBe(true); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + } + ); + + it("queues successive animations", function(done) { + var starts = 0; + var ends = 0; + + gd + .on("plotly_animating", function() { + starts++; + }) + .on("plotly_animated", function() { + ends++; + expect(Plots.transition.calls.count()).toEqual(4); + expect(starts).toEqual(1); }); - it('rejects when an animation is interrupted', function(done) { - var interrupted = false; - Plotly.animate(gd, ['frame0', 'frame1'], animOpts).then(fail, function() { - interrupted = true; - }); - - Plotly.animate(gd, ['frame2'], Lib.extendFlat(animOpts, {mode: 'immediate'})).then(function() { - expect(interrupted).toBe(true); - verifyFrameTransitionOrder(gd, ['frame0', 'frame2']); - verifyQueueEmpty(gd); - }).catch(fail).then(done); - }); + Plotly.animate(gd, "even-frames", { transition: { duration: 16 } }); + Plotly.animate(gd, "odd-frames", { transition: { duration: 16 } }) + .then(delay(10)) + .then(function() { + expect(ends).toEqual(1); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); }); - describe('frame events', function() { - it('emits an event when a frame is transitioned to', function(done) { - var frames = []; - gd.on('plotly_animatingframe', function(data) { - frames.push(data.name); - expect(data.frame).not.toBe(undefined); - expect(data.animation.frame).not.toBe(undefined); - expect(data.animation.transition).not.toBe(undefined); - }); - - Plotly.animate(gd, ['frame0', 'frame1', {name: 'test'}, {data: []}], { - transition: {duration: 1}, - frame: {duration: 1} - }).then(function() { - expect(frames).toEqual(['frame0', 'frame1', null, null]); - }).catch(fail).then(done); - - }); + it("an empty list with immediate dumps previous frames", function(done) { + Plotly.animate(gd, ["frame0", "frame1"], { frame: { duration: 50 } }); + Plotly.animate(gd, [], { mode: "immediate" }) + .then(function() { + expect(Plots.transition.calls.count()).toEqual(1); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); }); - describe('frame vs. transition timing', function() { - it('limits the transition duration to <= frame duration', function(done) { - Plotly.animate(gd, ['frame0'], { - transition: {duration: 100000}, - frame: {duration: 50} - }).then(function() { - // Frame timing: - expect(Plots.transition.calls.argsFor(0)[4].duration).toEqual(50); - - // Transition timing: - expect(Plots.transition.calls.argsFor(0)[5].duration).toEqual(50); - - }).catch(fail).then(done); - }); - - it('limits the transition duration to <= frame duration (matching per-config)', function(done) { - Plotly.animate(gd, ['frame0', 'frame1'], { - transition: [{duration: 100000}, {duration: 123456}], - frame: [{duration: 50}, {duration: 40}] - }).then(function() { - // Frame timing: - expect(Plots.transition.calls.argsFor(0)[4].duration).toEqual(50); - expect(Plots.transition.calls.argsFor(1)[4].duration).toEqual(40); - - // Transition timing: - expect(Plots.transition.calls.argsFor(0)[5].duration).toEqual(50); - expect(Plots.transition.calls.argsFor(1)[5].duration).toEqual(40); - - }).catch(fail).then(done); - }); + it("animates groups in the correct order", function(done) { + Plotly.animate(gd, "even-frames", animOpts); + Plotly.animate(gd, "odd-frames", animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "frame0", + "frame2", + "frame1", + "frame3" + ]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); }); -}); - -describe('Animate API details', function() { - 'use strict'; - var gd; - var dur = 30; - var mockCopy; + it("drops queued frames when immediate = true", function(done) { + Plotly.animate(gd, "even-frames", animOpts); + Plotly.animate( + gd, + "odd-frames", + Lib.extendFlat(animOpts, { mode: "immediate" }) + ) + .then(function() { + verifyFrameTransitionOrder(gd, ["frame0", "frame1", "frame3"]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); - beforeEach(function(done) { - gd = createGraphDiv(); - mockCopy = Lib.extendDeep({}, mock); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + it("animates frames and groups in sequence", function(done) { + Plotly.animate(gd, "even-frames", animOpts); + Plotly.animate(gd, ["frame0", "frame2", "frame1", "frame3"], animOpts) + .then(function() { + verifyFrameTransitionOrder(gd, [ + "frame0", + "frame2", + "frame0", + "frame2", + "frame1", + "frame3" + ]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); }); - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); + it("rejects when an animation is interrupted", function(done) { + var interrupted = false; + Plotly.animate(gd, ["frame0", "frame1"], animOpts).then(fail, function() { + interrupted = true; + }); + + Plotly.animate( + gd, + ["frame2"], + Lib.extendFlat(animOpts, { mode: "immediate" }) + ) + .then(function() { + expect(interrupted).toBe(true); + verifyFrameTransitionOrder(gd, ["frame0", "frame2"]); + verifyQueueEmpty(gd); + }) + .catch(fail) + .then(done); + }); + }); + + describe("frame events", function() { + it("emits an event when a frame is transitioned to", function(done) { + var frames = []; + gd.on("plotly_animatingframe", function(data) { + frames.push(data.name); + expect(data.frame).not.toBe(undefined); + expect(data.animation.frame).not.toBe(undefined); + expect(data.animation.transition).not.toBe(undefined); + }); + + Plotly.animate(gd, ["frame0", "frame1", { name: "test" }, { data: [] }], { + transition: { duration: 1 }, + frame: { duration: 1 } + }) + .then(function() { + expect(frames).toEqual(["frame0", "frame1", null, null]); + }) + .catch(fail) + .then(done); + }); + }); + + describe("frame vs. transition timing", function() { + it("limits the transition duration to <= frame duration", function(done) { + Plotly.animate(gd, ["frame0"], { + transition: { duration: 100000 }, + frame: { duration: 50 } + }) + .then(function() { + // Frame timing: + expect(Plots.transition.calls.argsFor(0)[4].duration).toEqual(50); + + // Transition timing: + expect(Plots.transition.calls.argsFor(0)[5].duration).toEqual(50); + }) + .catch(fail) + .then(done); }); - it('redraws after a layout animation', function(done) { - var redraws = 0; - gd.on('plotly_redraw', function() {redraws++;}); + it( + "limits the transition duration to <= frame duration (matching per-config)", + function(done) { + Plotly.animate(gd, ["frame0", "frame1"], { + transition: [{ duration: 100000 }, { duration: 123456 }], + frame: [{ duration: 50 }, { duration: 40 }] + }) + .then(function() { + // Frame timing: + expect(Plots.transition.calls.argsFor(0)[4].duration).toEqual(50); + expect(Plots.transition.calls.argsFor(1)[4].duration).toEqual(40); + + // Transition timing: + expect(Plots.transition.calls.argsFor(0)[5].duration).toEqual(50); + expect(Plots.transition.calls.argsFor(1)[5].duration).toEqual(40); + }) + .catch(fail) + .then(done); + } + ); + }); +}); - Plotly.animate(gd, - {layout: {'xaxis.range': [0, 1]}}, - {frame: {redraw: true, duration: dur}, transition: {duration: dur}} - ).then(function() { - expect(redraws).toBe(1); - }).catch(fail).then(done); +describe("Animate API details", function() { + "use strict"; + var gd; + var dur = 30; + var mockCopy; + + beforeEach(function(done) { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("redraws after a layout animation", function(done) { + var redraws = 0; + gd.on("plotly_redraw", function() { + redraws++; }); - it('forces a relayout after layout animations', function(done) { - var relayouts = 0; - var restyles = 0; - var redraws = 0; - gd.on('plotly_relayout', function() {relayouts++;}); - gd.on('plotly_restyle', function() {restyles++;}); - gd.on('plotly_redraw', function() {redraws++;}); - - Plotly.animate(gd, - {layout: {'xaxis.range': [0, 1]}}, - {frame: {redraw: false, duration: dur}, transition: {duration: dur}} - ).then(function() { - expect(relayouts).toBe(1); - expect(restyles).toBe(0); - expect(redraws).toBe(0); - }).catch(fail).then(done); + Plotly.animate(gd, { layout: { "xaxis.range": [0, 1] } }, { + frame: { redraw: true, duration: dur }, + transition: { duration: dur } + }) + .then(function() { + expect(redraws).toBe(1); + }) + .catch(fail) + .then(done); + }); + + it("forces a relayout after layout animations", function(done) { + var relayouts = 0; + var restyles = 0; + var redraws = 0; + gd.on("plotly_relayout", function() { + relayouts++; }); - - it('triggers plotly_animated after a single layout animation', function(done) { - var animateds = 0; - gd.on('plotly_animated', function() {animateds++;}); - - Plotly.animate(gd, [ - {layout: {'xaxis.range': [0, 1]}}, - ], {frame: {redraw: false, duration: dur}, transition: {duration: dur}} - ).then(function() { - // Wait a bit just to be sure: - setTimeout(function() { - expect(animateds).toBe(1); - done(); - }, dur); - }); + gd.on("plotly_restyle", function() { + restyles++; }); - - it('triggers plotly_animated after a multi-step layout animation', function(done) { - var animateds = 0; - gd.on('plotly_animated', function() {animateds++;}); - - Plotly.animate(gd, [ - {layout: {'xaxis.range': [0, 1]}}, - {layout: {'xaxis.range': [2, 4]}}, - ], {frame: {redraw: false, duration: dur}, transition: {duration: dur}} - ).then(function() { - // Wait a bit just to be sure: - setTimeout(function() { - expect(animateds).toBe(1); - done(); - }, dur); - }); + gd.on("plotly_redraw", function() { + redraws++; }); - it('does not fail if strings are not used', function(done) { - Plotly.addFrames(gd, [{name: 8, data: [{x: [8, 7, 6]}]}]).then(function() { - // Verify it was added as a string name: - expect(gd._transitionData._frameHash['8']).not.toBeUndefined(); - - // Transition using a number: - return Plotly.animate(gd, [8], {transition: {duration: 0}, frame: {duration: 0}}); - }).then(function() { - // Confirm the result: - expect(gd.data[0].x).toEqual([8, 7, 6]); - }).catch(fail).then(done); + Plotly.animate(gd, { layout: { "xaxis.range": [0, 1] } }, { + frame: { redraw: false, duration: dur }, + transition: { duration: dur } + }) + .then(function() { + expect(relayouts).toBe(1); + expect(restyles).toBe(0); + expect(redraws).toBe(0); + }) + .catch(fail) + .then(done); + }); + + it("triggers plotly_animated after a single layout animation", function( + done + ) { + var animateds = 0; + gd.on("plotly_animated", function() { + animateds++; }); - it('ignores null and undefined frames', function(done) { - var cnt = 0; - gd.on('plotly_animatingframe', function() {cnt++;}); - - Plotly.addFrames(gd, mockCopy.frames).then(function() { - return Plotly.animate(gd, ['frame0', null, undefined], {transition: {duration: 0}, frame: {duration: 0}}); - }).then(function() { - // Check only one animating was fired: - expect(cnt).toEqual(1); - - // Check unused frames did not affect the current frame: - expect(gd._fullLayout._currentFrame).toEqual('frame0'); - }).catch(fail).then(done); + Plotly.animate(gd, [{ layout: { "xaxis.range": [0, 1] } }], { + frame: { redraw: false, duration: dur }, + transition: { duration: dur } + }).then(function() { + // Wait a bit just to be sure: + setTimeout( + function() { + expect(animateds).toBe(1); + done(); + }, + dur + ); }); - - it('null frames should not break everything', function(done) { - gd._transitionData._frames.push(null); - - Plotly.animate(gd, null, { - frame: {duration: 0}, - transition: {duration: 0} - }).catch(fail).then(done); + }); + + it("triggers plotly_animated after a multi-step layout animation", function( + done + ) { + var animateds = 0; + gd.on("plotly_animated", function() { + animateds++; }); -}); -describe('non-animatable fallback', function() { - 'use strict'; - var gd; - - beforeEach(function() { - gd = createGraphDiv(); + Plotly.animate( + gd, + [ + { layout: { "xaxis.range": [0, 1] } }, + { layout: { "xaxis.range": [2, 4] } } + ], + { + frame: { redraw: false, duration: dur }, + transition: { duration: dur } + } + ).then(function() { + // Wait a bit just to be sure: + setTimeout( + function() { + expect(animateds).toBe(1); + done(); + }, + dur + ); }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); + }); + + it("does not fail if strings are not used", function(done) { + Plotly.addFrames(gd, [{ name: 8, data: [{ x: [8, 7, 6] }] }]) + .then(function() { + // Verify it was added as a string name: + expect(gd._transitionData._frameHash["8"]).not.toBeUndefined(); + + // Transition using a number: + return Plotly.animate(gd, [8], { + transition: { duration: 0 }, + frame: { duration: 0 } + }); + }) + .then(function() { + // Confirm the result: + expect(gd.data[0].x).toEqual([8, 7, 6]); + }) + .catch(fail) + .then(done); + }); + + it("ignores null and undefined frames", function(done) { + var cnt = 0; + gd.on("plotly_animatingframe", function() { + cnt++; }); - it('falls back to a simple update for bar graphs', function(done) { - Plotly.plot(gd, [{ - x: [1, 2, 3], - y: [4, 5, 6], - type: 'bar' - }]).then(function() { - expect(gd.data[0].y).toEqual([4, 5, 6]); - - return Plotly.animate(gd, [{ - data: [{y: [6, 4, 5]}] - }], {frame: {duration: 0}}); - }).then(function() { - expect(gd.data[0].y).toEqual([6, 4, 5]); - }).catch(fail).then(done); - - }); + Plotly.addFrames(gd, mockCopy.frames) + .then(function() { + return Plotly.animate(gd, ["frame0", null, undefined], { + transition: { duration: 0 }, + frame: { duration: 0 } + }); + }) + .then(function() { + // Check only one animating was fired: + expect(cnt).toEqual(1); + + // Check unused frames did not affect the current frame: + expect(gd._fullLayout._currentFrame).toEqual("frame0"); + }) + .catch(fail) + .then(done); + }); + + it("null frames should not break everything", function(done) { + gd._transitionData._frames.push(null); + + Plotly.animate(gd, null, { + frame: { duration: 0 }, + transition: { duration: 0 } + }) + .catch(fail) + .then(done); + }); }); -describe('animating scatter traces', function() { - 'use strict'; - var gd; +describe("non-animatable fallback", function() { + "use strict"; + var gd; - beforeEach(function() { - gd = createGraphDiv(); - }); + beforeEach(function() { + gd = createGraphDiv(); + }); - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - it('animates trace opacity', function(done) { - var trace; - Plotly.plot(gd, [{ - x: [1, 2, 3], - y: [4, 5, 6], - opacity: 1 - }]).then(function() { - trace = Plotly.d3.selectAll('g.scatter.trace'); - expect(trace.style('opacity')).toEqual('1'); - - return Plotly.animate(gd, [{ - data: [{opacity: 0.1}] - }], {transition: {duration: 0}, frame: {duration: 0, redraw: false}}); - }).then(function() { - expect(trace.style('opacity')).toEqual('0.1'); - }).catch(fail).then(done); - }); + it("falls back to a simple update for bar graphs", function(done) { + Plotly.plot(gd, [{ x: [1, 2, 3], y: [4, 5, 6], type: "bar" }]) + .then(function() { + expect(gd.data[0].y).toEqual([4, 5, 6]); + + return Plotly.animate(gd, [{ data: [{ y: [6, 4, 5] }] }], { + frame: { duration: 0 } + }); + }) + .then(function() { + expect(gd.data[0].y).toEqual([6, 4, 5]); + }) + .catch(fail) + .then(done); + }); +}); + +describe("animating scatter traces", function() { + "use strict"; + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("animates trace opacity", function(done) { + var trace; + Plotly.plot(gd, [{ x: [1, 2, 3], y: [4, 5, 6], opacity: 1 }]) + .then(function() { + trace = Plotly.d3.selectAll("g.scatter.trace"); + expect(trace.style("opacity")).toEqual("1"); + + return Plotly.animate(gd, [{ data: [{ opacity: 0.1 }] }], { + transition: { duration: 0 }, + frame: { duration: 0, redraw: false } + }); + }) + .then(function() { + expect(trace.style("opacity")).toEqual("0.1"); + }) + .catch(fail) + .then(done); + }); }); diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index 880f273ed70..7321165d517 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -1,427 +1,557 @@ -var Annotations = require('@src/components/annotations'); +var Annotations = require("@src/components/annotations"); -var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); -var Axes = require('@src/plots/cartesian/axes'); +var Plotly = require("@lib/index"); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); +var Axes = require("@src/plots/cartesian/axes"); -var d3 = require('d3'); -var customMatchers = require('../assets/custom_matchers'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var d3 = require("d3"); +var customMatchers = require("../assets/custom_matchers"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +describe("Test annotations", function() { + "use strict"; + describe("supplyLayoutDefaults", function() { + function _supply(layoutIn, layoutOut) { + layoutOut = layoutOut || {}; + layoutOut._has = Plots._hasPlotType.bind(layoutOut); -describe('Test annotations', function() { - 'use strict'; + Annotations.supplyLayoutDefaults(layoutIn, layoutOut); - describe('supplyLayoutDefaults', function() { - - function _supply(layoutIn, layoutOut) { - layoutOut = layoutOut || {}; - layoutOut._has = Plots._hasPlotType.bind(layoutOut); - - Annotations.supplyLayoutDefaults(layoutIn, layoutOut); - - return layoutOut.annotations; - } - - it('should skip non-array containers', function() { - [null, undefined, {}, 'str', 0, false, true].forEach(function(cont) { - var msg = '- ' + JSON.stringify(cont); - var layoutIn = { annotations: cont }; - var out = _supply(layoutIn); - - expect(layoutIn.annotations).toBe(cont, msg); - expect(out).toEqual([], msg); - }); - }); - - it('should make non-object item visible: false', function() { - var annotations = [null, undefined, [], 'str', 0, false, true]; - var layoutIn = { annotations: annotations }; - var out = _supply(layoutIn); - - expect(layoutIn.annotations).toEqual(annotations); - - out.forEach(function(item, i) { - expect(item).toEqual({ - visible: false, - _input: {}, - _index: i, - clicktoshow: false - }); - }); - }); + return layoutOut.annotations; + } - it('should default to pixel for axref/ayref', function() { - var layoutIn = { - annotations: [{ showarrow: true, arrowhead: 2 }] - }; + it("should skip non-array containers", function() { + [null, undefined, {}, "str", 0, false, true].forEach(function(cont) { + var msg = "- " + JSON.stringify(cont); + var layoutIn = { annotations: cont }; + var out = _supply(layoutIn); - var out = _supply(layoutIn); + expect(layoutIn.annotations).toBe(cont, msg); + expect(out).toEqual([], msg); + }); + }); - expect(out[0].axref).toEqual('pixel'); - expect(out[0].ayref).toEqual('pixel'); - }); + it("should make non-object item visible: false", function() { + var annotations = [null, undefined, [], "str", 0, false, true]; + var layoutIn = { annotations: annotations }; + var out = _supply(layoutIn); - it('should convert ax/ay date coordinates to date string if tail is in milliseconds and axis is a date', function() { - var layoutIn = { - annotations: [{ - showarrow: true, - axref: 'x', - ayref: 'y', - x: '2008-07-01', - // note this is not portable: this generates ms in the local - // timezone, so will work correctly where it was created but - // not if the milliseconds number is moved to another TZ - ax: +(new Date(2004, 6, 1)), - y: 0, - ay: 50 - }] - }; - - var layoutOut = { - xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] } - }; - Axes.setConvert(layoutOut.xaxis); - - _supply(layoutIn, layoutOut); - - expect(layoutOut.annotations[0].x).toEqual('2008-07-01'); - expect(layoutOut.annotations[0].ax).toEqual('2004-07-01'); - }); + expect(layoutIn.annotations).toEqual(annotations); - it('should convert ax/ay category coordinates to linear coords', function() { - var layoutIn = { - annotations: [{ - showarrow: true, - axref: 'x', - ayref: 'y', - x: 'c', - ax: 1, - y: 'A', - ay: 3 - }] - }; - - var layoutOut = { - xaxis: { - type: 'category', - _categories: ['a', 'b', 'c'], - range: [-0.5, 2.5] }, - yaxis: { - type: 'category', - _categories: ['A', 'B', 'C'], - range: [-0.5, 3] - } - }; - Axes.setConvert(layoutOut.xaxis); - Axes.setConvert(layoutOut.yaxis); - - _supply(layoutIn, layoutOut); - - expect(layoutOut.annotations[0].x).toEqual(2); - expect(layoutOut.annotations[0].ax).toEqual(1); - expect(layoutOut.annotations[0].y).toEqual(0); - expect(layoutOut.annotations[0].ay).toEqual(3); + out.forEach(function(item, i) { + expect(item).toEqual({ + visible: false, + _input: {}, + _index: i, + clicktoshow: false }); + }); }); -}); -describe('annotations relayout', function() { - 'use strict'; + it("should default to pixel for axref/ayref", function() { + var layoutIn = { annotations: [{ showarrow: true, arrowhead: 2 }] }; - var mock = require('@mocks/annotations.json'); - var gd; + var out = _supply(layoutIn); - // there is 1 visible: false item - var len = mock.layout.annotations.length - 1; + expect(out[0].axref).toEqual("pixel"); + expect(out[0].ayref).toEqual("pixel"); + }); - beforeEach(function(done) { - gd = createGraphDiv(); + it( + "should convert ax/ay date coordinates to date string if tail is in milliseconds and axis is a date", + function() { + var layoutIn = { + annotations: [ + { + showarrow: true, + axref: "x", + ayref: "y", + x: "2008-07-01", + // note this is not portable: this generates ms in the local + // timezone, so will work correctly where it was created but + // not if the milliseconds number is moved to another TZ + ax: +new Date(2004, 6, 1), + y: 0, + ay: 50 + } + ] + }; - var mockData = Lib.extendDeep([], mock.data), - mockLayout = Lib.extendDeep({}, mock.layout); + var layoutOut = { + xaxis: { type: "date", range: ["2000-01-01", "2016-01-01"] } + }; + Axes.setConvert(layoutOut.xaxis); + + _supply(layoutIn, layoutOut); + + expect(layoutOut.annotations[0].x).toEqual("2008-07-01"); + expect(layoutOut.annotations[0].ax).toEqual("2004-07-01"); + } + ); + + it( + "should convert ax/ay category coordinates to linear coords", + function() { + var layoutIn = { + annotations: [ + { + showarrow: true, + axref: "x", + ayref: "y", + x: "c", + ax: 1, + y: "A", + ay: 3 + } + ] + }; - Plotly.plot(gd, mockData, mockLayout).then(done); - }); + var layoutOut = { + xaxis: { + type: "category", + _categories: ["a", "b", "c"], + range: [-0.5, 2.5] + }, + yaxis: { + type: "category", + _categories: ["A", "B", "C"], + range: [-0.5, 3] + } + }; + Axes.setConvert(layoutOut.xaxis); + Axes.setConvert(layoutOut.yaxis); + + _supply(layoutIn, layoutOut); + + expect(layoutOut.annotations[0].x).toEqual(2); + expect(layoutOut.annotations[0].ax).toEqual(1); + expect(layoutOut.annotations[0].y).toEqual(0); + expect(layoutOut.annotations[0].ay).toEqual(3); + } + ); + }); +}); - afterEach(destroyGraphDiv); +describe("annotations relayout", function() { + "use strict"; + var mock = require("@mocks/annotations.json"); + var gd; - function countAnnotations() { - return d3.selectAll('g.annotation').size(); - } + // there is 1 visible: false item + var len = mock.layout.annotations.length - 1; - it('should be able to add /remove annotations', function(done) { - expect(countAnnotations()).toEqual(len); + beforeEach(function(done) { + gd = createGraphDiv(); - var ann = { text: '' }; + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); - Plotly.relayout(gd, 'annotations[' + len + ']', ann).then(function() { - expect(countAnnotations()).toEqual(len + 1); + Plotly.plot(gd, mockData, mockLayout).then(done); + }); - return Plotly.relayout(gd, 'annotations[0]', 'remove'); - }) - .then(function() { - expect(countAnnotations()).toEqual(len); + afterEach(destroyGraphDiv); - return Plotly.relayout(gd, 'annotations[0]', null); - }) - .then(function() { - expect(countAnnotations()).toEqual(len - 1); + function countAnnotations() { + return d3.selectAll("g.annotation").size(); + } - return Plotly.relayout(gd, 'annotations[0].visible', false); - }) - .then(function() { - expect(countAnnotations()).toEqual(len - 2); + it("should be able to add /remove annotations", function(done) { + expect(countAnnotations()).toEqual(len); - return Plotly.relayout(gd, { annotations: [] }); - }) - .then(function() { - expect(countAnnotations()).toEqual(0); + var ann = { text: "" }; - done(); - }); - }); + Plotly.relayout(gd, "annotations[" + len + "]", ann) + .then(function() { + expect(countAnnotations()).toEqual(len + 1); - it('should be able update annotations', function(done) { + return Plotly.relayout(gd, "annotations[0]", "remove"); + }) + .then(function() { + expect(countAnnotations()).toEqual(len); - function assertText(index, expected) { - var query = '.annotation[data-index="' + index + '"]', - actual = d3.select(query).select('text').text(); + return Plotly.relayout(gd, "annotations[0]", null); + }) + .then(function() { + expect(countAnnotations()).toEqual(len - 1); - expect(actual).toEqual(expected); - } + return Plotly.relayout(gd, "annotations[0].visible", false); + }) + .then(function() { + expect(countAnnotations()).toEqual(len - 2); - assertText(0, 'left top'); + return Plotly.relayout(gd, { annotations: [] }); + }) + .then(function() { + expect(countAnnotations()).toEqual(0); - Plotly.relayout(gd, 'annotations[0].text', 'hello').then(function() { - assertText(0, 'hello'); + done(); + }); + }); - return Plotly.relayout(gd, 'annotations[0].text', null); - }) - .then(function() { - assertText(0, 'new text'); - }) - .then(done); + it("should be able update annotations", function(done) { + function assertText(index, expected) { + var query = '.annotation[data-index="' + index + '"]', + actual = d3.select(query).select("text").text(); - }); -}); + expect(actual).toEqual(expected); + } -describe('annotations autosize', function() { - 'use strict'; + assertText(0, "left top"); - var mock = Lib.extendDeep({}, require('@mocks/annotations-autorange.json')); - var gd; + Plotly.relayout(gd, "annotations[0].text", "hello") + .then(function() { + assertText(0, "hello"); - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - afterEach(destroyGraphDiv); - - it('should adapt to relayout calls', function(done) { - gd = createGraphDiv(); - - function assertRanges(x, y, x2, y2, x3, y3) { - var fullLayout = gd._fullLayout; - var PREC = 1; - - // xaxis2 need a bit more tolerance to pass on CI - // this most likely due to the different text bounding box values - // on headfull vs headless browsers. - // but also because it's a date axis that we've converted to ms - var PRECX2 = -10; - // yaxis2 needs a bit more now too... - var PRECY2 = 0.2; - var dateAx = fullLayout.xaxis2; - - expect(fullLayout.xaxis.range).toBeCloseToArray(x, PREC, '- xaxis'); - expect(fullLayout.yaxis.range).toBeCloseToArray(y, PREC, '- yaxis'); - expect(Lib.simpleMap(dateAx.range, dateAx.r2l)) - .toBeCloseToArray(Lib.simpleMap(x2, dateAx.r2l), PRECX2, 'xaxis2 ' + dateAx.range); - expect(fullLayout.yaxis2.range).toBeCloseToArray(y2, PRECY2, 'yaxis2'); - expect(fullLayout.xaxis3.range).toBeCloseToArray(x3, PREC, 'xaxis3'); - expect(fullLayout.yaxis3.range).toBeCloseToArray(y3, PREC, 'yaxis3'); - } - - Plotly.plot(gd, mock).then(function() { - assertRanges( - [0.97, 2.03], [0.97, 2.03], - ['2000-10-01 08:23:18.0583', '2001-06-05 19:20:23.301'], [-0.245, 4.245], - [0.9, 2.1], [0.86, 2.14] - ); - - return Plotly.relayout(gd, { - 'annotations[0].visible': false, - 'annotations[4].visible': false, - 'annotations[8].visible': false - }); - }) - .then(function() { - assertRanges( - [1.44, 2.02], [0.97, 2.03], - ['2001-01-18 15:06:04.0449', '2001-03-27 14:01:20.8989'], [-0.245, 4.245], - [1.44, 2.1], [0.86, 2.14] - ); - - return Plotly.relayout(gd, { - 'annotations[2].visible': false, - 'annotations[5].visible': false, - 'annotations[9].visible': false - }); - }) - .then(function() { - assertRanges( - [1.44, 2.02], [0.99, 1.52], - ['2001-01-31 23:59:59.999', '2001-02-01 00:00:00.001'], [-0.245, 4.245], - [0.5, 2.5], [0.86, 2.14] - ); - - return Plotly.relayout(gd, { - 'annotations[0].visible': true, - 'annotations[2].visible': true, - 'annotations[4].visible': true, - 'annotations[5].visible': true, - 'annotations[8].visible': true, - 'annotations[9].visible': true - }); - }) - .then(function() { - assertRanges( - [0.97, 2.03], [0.97, 2.03], - ['2000-10-01 08:23:18.0583', '2001-06-05 19:20:23.301'], [-0.245, 4.245], - [0.9, 2.1], [0.86, 2.14] - ); - }) - .then(done); - }); + return Plotly.relayout(gd, "annotations[0].text", null); + }) + .then(function() { + assertText(0, "new text"); + }) + .then(done); + }); }); -describe('annotation clicktoshow', function() { - var gd; - - afterEach(destroyGraphDiv); - - function layout() { - return { - xaxis: {domain: [0, 0.5]}, - xaxis2: {domain: [0.5, 1], anchor: 'y2'}, - yaxis2: {anchor: 'x2'}, - annotations: [ - {x: 1, y: 2, xref: 'x', yref: 'y', text: 'index0'}, // (1,2) selects - {x: 1, y: 3, xref: 'x', yref: 'y', text: 'index1'}, - {x: 2, y: 3, xref: 'x', yref: 'y', text: 'index2'}, // ** (2,3) selects - {x: 4, y: 2, xref: 'x', yref: 'y', text: 'index3'}, - {x: 1, y: 2, xref: 'x2', yref: 'y', text: 'index4'}, - {x: 1, y: 2, xref: 'x', yref: 'y2', text: 'index5'}, - {x: 1, xclick: 5, y: 2, xref: 'x', yref: 'y', text: 'index6'}, - {x: 1, y: 2, yclick: 6, xref: 'x', yref: 'y', text: 'index7'}, - {x: 1, y: 2.0000001, xref: 'x', yref: 'y', text: 'index8'}, - {x: 1, y: 2, xref: 'x', yref: 'y', text: 'index9'}, // (1,2) selects - {x: 7, xclick: 1, y: 2, xref: 'x', yref: 'y', text: 'index10'}, // (1,2) selects - {x: 1, y: 8, yclick: 2, xref: 'x', yref: 'y', text: 'index11'}, // (1,2) selects - {x: 1, y: 2, xref: 'paper', yref: 'y', text: 'index12'}, - {x: 1, y: 2, xref: 'x', yref: 'paper', text: 'index13'}, - {x: 1, y: 2, xref: 'paper', yref: 'paper', text: 'index14'} - ] - }; +describe("annotations autosize", function() { + "use strict"; + var mock = Lib.extendDeep({}, require("@mocks/annotations-autorange.json")); + var gd; + + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + afterEach(destroyGraphDiv); + + it("should adapt to relayout calls", function(done) { + gd = createGraphDiv(); + + function assertRanges(x, y, x2, y2, x3, y3) { + var fullLayout = gd._fullLayout; + var PREC = 1; + + // xaxis2 need a bit more tolerance to pass on CI + // this most likely due to the different text bounding box values + // on headfull vs headless browsers. + // but also because it's a date axis that we've converted to ms + var PRECX2 = -10; + // yaxis2 needs a bit more now too... + var PRECY2 = 0.2; + var dateAx = fullLayout.xaxis2; + + expect(fullLayout.xaxis.range).toBeCloseToArray(x, PREC, "- xaxis"); + expect(fullLayout.yaxis.range).toBeCloseToArray(y, PREC, "- yaxis"); + expect(Lib.simpleMap(dateAx.range, dateAx.r2l)).toBeCloseToArray( + Lib.simpleMap(x2, dateAx.r2l), + PRECX2, + "xaxis2 " + dateAx.range + ); + expect(fullLayout.yaxis2.range).toBeCloseToArray(y2, PRECY2, "yaxis2"); + expect(fullLayout.xaxis3.range).toBeCloseToArray(x3, PREC, "xaxis3"); + expect(fullLayout.yaxis3.range).toBeCloseToArray(y3, PREC, "yaxis3"); } - var data = [ - {x: [0, 1, 2], y: [1, 2, 3]}, - {x: [0, 1, 2], y: [1, 2, 3], xaxis: 'x2', yaxis: 'y2'} - ]; - - function hoverData(xyPairs) { - // hovering on nothing can have undefined hover data - must be supported - if(!xyPairs.length) return; - - return xyPairs.map(function(xy) { - return { - x: xy[0], - y: xy[1], - xaxis: gd._fullLayout.xaxis, - yaxis: gd._fullLayout.yaxis - }; + Plotly.plot(gd, mock) + .then(function() { + assertRanges( + [0.97, 2.03], + [0.97, 2.03], + ["2000-10-01 08:23:18.0583", "2001-06-05 19:20:23.301"], + [-0.245, 4.245], + [0.9, 2.1], + [0.86, 2.14] + ); + + return Plotly.relayout(gd, { + "annotations[0].visible": false, + "annotations[4].visible": false, + "annotations[8].visible": false }); - } - - function checkVisible(opts) { - gd._fullLayout.annotations.forEach(function(ann, i) { - expect(ann.visible).toBe(opts.on.indexOf(i) !== -1, 'i: ' + i + ', step: ' + opts.step); + }) + .then(function() { + assertRanges( + [1.44, 2.02], + [0.97, 2.03], + ["2001-01-18 15:06:04.0449", "2001-03-27 14:01:20.8989"], + [-0.245, 4.245], + [1.44, 2.1], + [0.86, 2.14] + ); + + return Plotly.relayout(gd, { + "annotations[2].visible": false, + "annotations[5].visible": false, + "annotations[9].visible": false }); - } - - function allAnnotations(attr, value) { - var update = {}; - for(var i = 0; i < gd.layout.annotations.length; i++) { - update['annotations[' + i + '].' + attr] = value; - } - return update; - } - - function clickAndCheck(opts) { - return function() { - expect(Annotations.hasClickToShow(gd, hoverData(opts.newPts))) - .toBe(opts.newCTS, 'step: ' + opts.step); + }) + .then(function() { + assertRanges( + [1.44, 2.02], + [0.99, 1.52], + ["2001-01-31 23:59:59.999", "2001-02-01 00:00:00.001"], + [-0.245, 4.245], + [0.5, 2.5], + [0.86, 2.14] + ); + + return Plotly.relayout(gd, { + "annotations[0].visible": true, + "annotations[2].visible": true, + "annotations[4].visible": true, + "annotations[5].visible": true, + "annotations[8].visible": true, + "annotations[9].visible": true + }); + }) + .then(function() { + assertRanges( + [0.97, 2.03], + [0.97, 2.03], + ["2000-10-01 08:23:18.0583", "2001-06-05 19:20:23.301"], + [-0.245, 4.245], + [0.9, 2.1], + [0.86, 2.14] + ); + }) + .then(done); + }); +}); - var clickResult = Annotations.onClick(gd, hoverData(opts.newPts)); - if(clickResult && clickResult.then) { - return clickResult.then(function() { checkVisible(opts); }); - } - else { - checkVisible(opts); - } - }; - } +describe("annotation clicktoshow", function() { + var gd; + + afterEach(destroyGraphDiv); + + function layout() { + return { + xaxis: { domain: [0, 0.5] }, + xaxis2: { domain: [0.5, 1], anchor: "y2" }, + yaxis2: { anchor: "x2" }, + annotations: [ + { x: 1, y: 2, xref: "x", yref: "y", text: "index0" }, + // (1,2) selects + { x: 1, y: 3, xref: "x", yref: "y", text: "index1" }, + { x: 2, y: 3, xref: "x", yref: "y", text: "index2" }, + // ** (2,3) selects + { x: 4, y: 2, xref: "x", yref: "y", text: "index3" }, + { x: 1, y: 2, xref: "x2", yref: "y", text: "index4" }, + { x: 1, y: 2, xref: "x", yref: "y2", text: "index5" }, + { x: 1, xclick: 5, y: 2, xref: "x", yref: "y", text: "index6" }, + { x: 1, y: 2, yclick: 6, xref: "x", yref: "y", text: "index7" }, + { x: 1, y: 2.0000001, xref: "x", yref: "y", text: "index8" }, + { x: 1, y: 2, xref: "x", yref: "y", text: "index9" }, + // (1,2) selects + { x: 7, xclick: 1, y: 2, xref: "x", yref: "y", text: "index10" }, + // (1,2) selects + { x: 1, y: 8, yclick: 2, xref: "x", yref: "y", text: "index11" }, + // (1,2) selects + { x: 1, y: 2, xref: "paper", yref: "y", text: "index12" }, + { x: 1, y: 2, xref: "x", yref: "paper", text: "index13" }, + { x: 1, y: 2, xref: "paper", yref: "paper", text: "index14" } + ] + }; + } + + var data = [ + { x: [0, 1, 2], y: [1, 2, 3] }, + { x: [0, 1, 2], y: [1, 2, 3], xaxis: "x2", yaxis: "y2" } + ]; + + function hoverData(xyPairs) { + // hovering on nothing can have undefined hover data - must be supported + if (!xyPairs.length) return; + + return xyPairs.map(function(xy) { + return { + x: xy[0], + y: xy[1], + xaxis: gd._fullLayout.xaxis, + yaxis: gd._fullLayout.yaxis + }; + }); + } + + function checkVisible(opts) { + gd._fullLayout.annotations.forEach(function(ann, i) { + expect(ann.visible).toBe( + opts.on.indexOf(i) !== -1, + "i: " + i + ", step: " + opts.step + ); + }); + } - function updateAndCheck(opts) { - return function() { - return Plotly.update(gd, {}, opts.update).then(function() { - checkVisible(opts); - }); - }; + function allAnnotations(attr, value) { + var update = {}; + for (var i = 0; i < gd.layout.annotations.length; i++) { + update["annotations[" + i + "]." + attr] = value; } - - var allIndices = layout().annotations.map(function(v, i) { return i; }); - - it('should select only clicktoshow annotations matching x, y, and axes of any point', function(done) { - gd = createGraphDiv(); - - // first try to select without adding clicktoshow, both visible and invisible - Plotly.plot(gd, data, layout()) - // clicktoshow is off initially, so it doesn't *expect* clicking will - // do anything, and it doesn't *actually* do anything. - .then(clickAndCheck({newPts: [[1, 2]], newCTS: false, on: allIndices, step: 1})) - .then(updateAndCheck({update: allAnnotations('visible', false), on: [], step: 2})) - // still nothing happens with hidden annotations - .then(clickAndCheck({newPts: [[1, 2]], newCTS: false, on: [], step: 3})) - - // turn on clicktoshow (onout mode) and we see some action! - .then(updateAndCheck({update: allAnnotations('clicktoshow', 'onout'), on: [], step: 4})) - .then(clickAndCheck({newPts: [[1, 2]], newCTS: true, on: [0, 9, 10, 11], step: 5})) - .then(clickAndCheck({newPts: [[2, 3]], newCTS: true, on: [2], step: 6})) - // clicking the same point again will close all, but in onout mode hasClickToShow - // is false because closing notes is kind of passive - .then(clickAndCheck({newPts: [[2, 3]], newCTS: false, on: [], step: 7})) - // now click two points (as if in compare hovermode) - .then(clickAndCheck({newPts: [[1, 2], [2, 3]], newCTS: true, on: [0, 2, 9, 10, 11], step: 8})) - // close all by clicking somewhere else - .then(clickAndCheck({newPts: [[0, 1]], newCTS: false, on: [], step: 9})) - - // now switch to onoff mode - .then(updateAndCheck({update: allAnnotations('clicktoshow', 'onoff'), on: [], step: 10})) - // again, clicking a point turns those annotations on - .then(clickAndCheck({newPts: [[1, 2]], newCTS: true, on: [0, 9, 10, 11], step: 11})) - // clicking a different point (or no point at all) leaves open annotations the same - .then(clickAndCheck({newPts: [[0, 1]], newCTS: false, on: [0, 9, 10, 11], step: 12})) - .then(clickAndCheck({newPts: [], newCTS: false, on: [0, 9, 10, 11], step: 13})) - // clicking another point turns it on too, without turning off the original - .then(clickAndCheck({newPts: [[0, 1], [2, 3]], newCTS: true, on: [0, 2, 9, 10, 11], step: 14})) - // finally click each one off - .then(clickAndCheck({newPts: [[1, 2]], newCTS: true, on: [2], step: 15})) - .then(clickAndCheck({newPts: [[2, 3]], newCTS: true, on: [], step: 16})) + return update; + } + + function clickAndCheck(opts) { + return function() { + expect(Annotations.hasClickToShow(gd, hoverData(opts.newPts))).toBe( + opts.newCTS, + "step: " + opts.step + ); + + var clickResult = Annotations.onClick(gd, hoverData(opts.newPts)); + if (clickResult && clickResult.then) { + return clickResult.then(function() { + checkVisible(opts); + }); + } else { + checkVisible(opts); + } + }; + } + + function updateAndCheck(opts) { + return function() { + return Plotly.update(gd, {}, opts.update).then(function() { + checkVisible(opts); + }); + }; + } + + var allIndices = layout().annotations.map(function(v, i) { + return i; + }); + + it( + "should select only clicktoshow annotations matching x, y, and axes of any point", + function(done) { + gd = createGraphDiv(); + + // first try to select without adding clicktoshow, both visible and invisible + Plotly.plot(gd, data, layout()) + .then( + clickAndCheck({ + newPts: [[1, 2]], + newCTS: false, + on: allIndices, + step: 1 + }) + ) + .then( + updateAndCheck({ + update: allAnnotations("visible", false), + on: [], + step: 2 + }) + ) + .then( + clickAndCheck({ + newPts: [[1, 2]], + newCTS: false, + on: [], + step: 3 + }) + ) + .then( + updateAndCheck({ + update: allAnnotations("clicktoshow", "onout"), + on: [], + step: 4 + }) + ) + .then( + clickAndCheck({ + newPts: [[1, 2]], + newCTS: true, + on: [0, 9, 10, 11], + step: 5 + }) + ) + .then( + clickAndCheck({ + newPts: [[2, 3]], + newCTS: true, + on: [2], + step: 6 + }) + ) + .then( + clickAndCheck({ + newPts: [[2, 3]], + newCTS: false, + on: [], + step: 7 + }) + ) + .then( + clickAndCheck({ + newPts: [[1, 2], [2, 3]], + newCTS: true, + on: [0, 2, 9, 10, 11], + step: 8 + }) + ) + .then( + clickAndCheck({ + newPts: [[0, 1]], + newCTS: false, + on: [], + step: 9 + }) + ) + .then( + updateAndCheck({ + update: allAnnotations("clicktoshow", "onoff"), + on: [], + step: 10 + }) + ) + .then( + clickAndCheck({ + newPts: [[1, 2]], + newCTS: true, + on: [0, 9, 10, 11], + step: 11 + }) + ) + .then( + clickAndCheck({ + newPts: [[0, 1]], + newCTS: false, + on: [0, 9, 10, 11], + step: 12 + }) + ) + .then( + clickAndCheck({ + newPts: [], + newCTS: false, + on: [0, 9, 10, 11], + step: 13 + }) + ) + .then( + clickAndCheck({ + newPts: [[0, 1], [2, 3]], + newCTS: true, + on: [0, 2, 9, 10, 11], + step: 14 + }) + ) + .then( + clickAndCheck({ + newPts: [[1, 2]], + newCTS: true, + on: [2], + step: 15 + }) + ) + .then( + clickAndCheck({ + newPts: [[2, 3]], + newCTS: true, + on: [], + step: 16 + }) + ) .then(done); - }); + } + ); }); diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 3883cedb617..10fd433ab71 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1,1835 +1,1925 @@ -var PlotlyInternal = require('@src/plotly'); +var PlotlyInternal = require("@src/plotly"); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); -var Color = require('@src/components/color'); -var tinycolor = require('tinycolor2'); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); +var Color = require("@src/components/color"); +var tinycolor = require("tinycolor2"); -var handleTickValueDefaults = require('@src/plots/cartesian/tick_value_defaults'); +var handleTickValueDefaults = require( + "@src/plots/cartesian/tick_value_defaults" +); var Axes = PlotlyInternal.Axes; -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); - - -describe('Test axes', function() { - 'use strict'; - - describe('swap', function() { - it('should swap most attributes and fix placeholder titles', function() { - var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], - layout: { - xaxis: { - title: 'A Title!!!', - type: 'log', - autorange: 'reversed', - rangemode: 'tozero', - tickmode: 'auto', - nticks: 23, - ticks: 'outside', - mirror: 'ticks', - ticklen: 12, - tickwidth: 4, - tickcolor: '#f00' - }, - yaxis: { - title: 'Click to enter Y axis title', - type: 'date' - } - } - }; - var expectedYaxis = Lib.extendDeep({}, gd.layout.xaxis), - expectedXaxis = { - title: 'Click to enter X axis title', - type: 'date' - }; - - Plots.supplyDefaults(gd); - - Axes.swap(gd, [0]); - - expect(gd.layout.xaxis).toEqual(expectedXaxis); - expect(gd.layout.yaxis).toEqual(expectedYaxis); - }); - - it('should not swap noSwapAttrs', function() { - // for reference: - // noSwapAttrs = ['anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle']; - var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], - layout: { - xaxis: { - anchor: 'free', - domain: [0, 1], - overlaying: false, - position: 0.2, - tickangle: 60 - }, - yaxis: { - anchor: 'x', - domain: [0.1, 0.9] - } - } - }; - var expectedLayoutAfter = Lib.extendDeep({}, gd.layout); - expectedLayoutAfter.xaxis.type = 'linear'; - expectedLayoutAfter.yaxis.type = 'linear'; +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); + +describe("Test axes", function() { + "use strict"; + describe("swap", function() { + it("should swap most attributes and fix placeholder titles", function() { + var gd = { + data: [{ x: [1, 2, 3], y: [1, 2, 3] }], + layout: { + xaxis: { + title: "A Title!!!", + type: "log", + autorange: "reversed", + rangemode: "tozero", + tickmode: "auto", + nticks: 23, + ticks: "outside", + mirror: "ticks", + ticklen: 12, + tickwidth: 4, + tickcolor: "#f00" + }, + yaxis: { title: "Click to enter Y axis title", type: "date" } + } + }; + var expectedYaxis = Lib.extendDeep({}, gd.layout.xaxis), + expectedXaxis = { title: "Click to enter X axis title", type: "date" }; - Plots.supplyDefaults(gd); + Plots.supplyDefaults(gd); - Axes.swap(gd, [0]); + Axes.swap(gd, [0]); - expect(gd.layout.xaxis).toEqual(expectedLayoutAfter.xaxis); - expect(gd.layout.yaxis).toEqual(expectedLayoutAfter.yaxis); - }); - - it('should swap shared attributes, combine linear/log, and move annotations', function() { - var gd = { - data: [ - {x: [1, 2, 3], y: [1, 2, 3]}, - {x: [1, 2, 3], y: [1, 2, 3], xaxis: 'x2'} - ], - layout: { - xaxis: { - type: 'linear', // combine linear/log - ticks: 'outside', // same as x2 - ticklen: 5, // default value - tickwidth: 2, // different - side: 'top', // noSwap - domain: [0, 0.45] // noSwap - }, - xaxis2: { - type: 'log', - ticks: 'outside', - tickcolor: '#444', // default value in 2nd axis - tickwidth: 3, - side: 'top', - domain: [0.55, 1] - }, - yaxis: { - type: 'category', - ticks: 'inside', - ticklen: 10, - tickcolor: '#f00', - tickwidth: 4, - showline: true, // not present in either x - side: 'right' - }, - annotations: [ - {x: 2, y: 3}, // xy referenced by default - {x: 3, y: 4, xref: 'x2', yref: 'y'}, - {x: 5, y: 0.5, xref: 'x', yref: 'paper'} // any paper ref -> don't swap - ] - } - }; - var expectedXaxis = { - type: 'category', - ticks: 'inside', - ticklen: 10, - tickcolor: '#f00', - tickwidth: 2, - showline: true, - side: 'top', - domain: [0, 0.45] - }, - expectedXaxis2 = { - type: 'category', - ticks: 'inside', - ticklen: 10, - tickcolor: '#f00', - tickwidth: 3, - showline: true, - side: 'top', - domain: [0.55, 1] - }, - expectedYaxis = { - type: 'linear', - ticks: 'outside', - ticklen: 5, - tickwidth: 4, - side: 'right' - }, - expectedAnnotations = [ - {x: 3, y: 2}, - {x: 4, y: 3, xref: 'x2', yref: 'y'}, - {x: 5, y: 0.5, xref: 'x', yref: 'paper'} - ]; - - Plots.supplyDefaults(gd); - - Axes.swap(gd, [0, 1]); - - expect(gd.layout.xaxis).toEqual(expectedXaxis); - expect(gd.layout.xaxis2).toEqual(expectedXaxis2); - expect(gd.layout.yaxis).toEqual(expectedYaxis); - expect(gd.layout.annotations).toEqual(expectedAnnotations); - }); + expect(gd.layout.xaxis).toEqual(expectedXaxis); + expect(gd.layout.yaxis).toEqual(expectedYaxis); }); - describe('supplyLayoutDefaults', function() { - var layoutIn, layoutOut, fullData; - - beforeEach(function() { - layoutOut = { - _has: Plots._hasPlotType, - _basePlotModules: [] - }; - fullData = []; - }); - - var supplyLayoutDefaults = Axes.supplyLayoutDefaults; - - it('should set undefined linewidth/linecolor if linewidth, linecolor or showline is not supplied', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.xaxis.linewidth).toBe(undefined); - expect(layoutOut.xaxis.linecolor).toBe(undefined); - expect(layoutOut.yaxis.linewidth).toBe(undefined); - expect(layoutOut.yaxis.linecolor).toBe(undefined); - }); - - it('should set default linewidth and linecolor if showline is true', function() { - layoutIn = { - xaxis: {showline: true} - }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.xaxis.linewidth).toBe(1); - expect(layoutOut.xaxis.linecolor).toBe(Color.defaultLine); - }); - - it('should set linewidth to default if linecolor is supplied and valid', function() { - layoutIn = { - xaxis: { linecolor: 'black' } - }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.xaxis.linecolor).toBe('black'); - expect(layoutOut.xaxis.linewidth).toBe(1); - }); - - it('should set linecolor to default if linewidth is supplied and valid', function() { - layoutIn = { - yaxis: { linewidth: 2 } - }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.yaxis.linewidth).toBe(2); - expect(layoutOut.yaxis.linecolor).toBe(Color.defaultLine); - }); - - it('should set default gridwidth and gridcolor', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - var lightLine = tinycolor(Color.lightLine).toRgbString(); - expect(layoutOut.xaxis.gridwidth).toBe(1); - expect(tinycolor(layoutOut.xaxis.gridcolor).toRgbString()).toBe(lightLine); - expect(layoutOut.yaxis.gridwidth).toBe(1); - expect(tinycolor(layoutOut.yaxis.gridcolor).toRgbString()).toBe(lightLine); - }); - - it('should set gridcolor/gridwidth to undefined if showgrid is false', function() { - layoutIn = { - yaxis: {showgrid: false} - }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.yaxis.gridwidth).toBe(undefined); - expect(layoutOut.yaxis.gridcolor).toBe(undefined); - }); - - it('should set default zerolinecolor/zerolinewidth', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.xaxis.zerolinewidth).toBe(1); - expect(layoutOut.xaxis.zerolinecolor).toBe(Color.defaultLine); - expect(layoutOut.yaxis.zerolinewidth).toBe(1); - expect(layoutOut.yaxis.zerolinecolor).toBe(Color.defaultLine); - }); - - it('should set zerolinecolor/zerolinewidth to undefined if zeroline is false', function() { - layoutIn = { - xaxis: {zeroline: false} - }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.xaxis.zerolinewidth).toBe(undefined); - expect(layoutOut.xaxis.zerolinecolor).toBe(undefined); - }); - - it('should detect orphan axes (lone axes case)', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - fullData = []; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules[0].name).toEqual('cartesian'); - }); - - it('should detect orphan axes (gl2d trace conflict case)', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - fullData = [{ - type: 'scattergl', - xaxis: 'x', - yaxis: 'y' - }]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules).toEqual([]); - }); - - it('should detect orphan axes (gl2d + cartesian case)', function() { - layoutIn = { - xaxis2: {}, - yaxis2: {} - }; - fullData = [{ - type: 'scattergl', - xaxis: 'x', - yaxis: 'y' - }]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules[0].name).toEqual('cartesian'); - }); - - it('should detect orphan axes (gl3d present case)', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - layoutOut._basePlotModules = [ { name: 'gl3d' }]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules).toEqual([ { name: 'gl3d' }]); - }); - - it('should detect orphan axes (geo present case)', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - layoutOut._basePlotModules = [ { name: 'geo' }]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules).toEqual([ { name: 'geo' }]); - }); - - it('should use \'axis.color\' as default for \'axis.titlefont.color\'', function() { - layoutIn = { - xaxis: { color: 'red' }, - yaxis: {}, - yaxis2: { titlefont: { color: 'yellow' } } - }; - - layoutOut.font = { color: 'blue' }, - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.xaxis.titlefont.color).toEqual('red'); - expect(layoutOut.yaxis.titlefont.color).toEqual('blue'); - expect(layoutOut.yaxis2.titlefont.color).toEqual('yellow'); - }); - - it('should use \'axis.color\' as default for \'axis.linecolor\'', function() { - layoutIn = { - xaxis: { showline: true, color: 'red' }, - yaxis: { linecolor: 'blue' }, - yaxis2: { showline: true } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.xaxis.linecolor).toEqual('red'); - expect(layoutOut.yaxis.linecolor).toEqual('blue'); - expect(layoutOut.yaxis2.linecolor).toEqual('#444'); - }); - - it('should use \'axis.color\' as default for \'axis.zerolinecolor\'', function() { - layoutIn = { - xaxis: { showzeroline: true, color: 'red' }, - yaxis: { zerolinecolor: 'blue' }, - yaxis2: { showzeroline: true } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.xaxis.zerolinecolor).toEqual('red'); - expect(layoutOut.yaxis.zerolinecolor).toEqual('blue'); - expect(layoutOut.yaxis2.zerolinecolor).toEqual('#444'); - }); - - it('should use combo of \'axis.color\', bgcolor and lightFraction as default for \'axis.gridcolor\'', function() { - layoutIn = { - paper_bgcolor: 'green', - plot_bgcolor: 'yellow', - xaxis: { showgrid: true, color: 'red' }, - yaxis: { gridcolor: 'blue' }, - yaxis2: { showgrid: true } - }; - - var bgColor = Color.combine('yellow', 'green'), - frac = 100 * (0xe - 0x4) / (0xf - 0x4); - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.xaxis.gridcolor) - .toEqual(tinycolor.mix('red', bgColor, frac).toRgbString()); - expect(layoutOut.yaxis.gridcolor).toEqual('blue'); - expect(layoutOut.yaxis2.gridcolor) - .toEqual(tinycolor.mix('#444', bgColor, frac).toRgbString()); - }); - - it('should inherit calendar from the layout', function() { - layoutOut.calendar = 'nepali'; - layoutIn = { - calendar: 'nepali', - xaxis: {type: 'date'}, - yaxis: {type: 'date'} - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - - expect(layoutOut.xaxis.calendar).toBe('nepali'); - expect(layoutOut.yaxis.calendar).toBe('nepali'); - }); + it("should not swap noSwapAttrs", function() { + // for reference: + // noSwapAttrs = ['anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle']; + var gd = { + data: [{ x: [1, 2, 3], y: [1, 2, 3] }], + layout: { + xaxis: { + anchor: "free", + domain: [0, 1], + overlaying: false, + position: 0.2, + tickangle: 60 + }, + yaxis: { anchor: "x", domain: [0.1, 0.9] } + } + }; + var expectedLayoutAfter = Lib.extendDeep({}, gd.layout); + expectedLayoutAfter.xaxis.type = "linear"; + expectedLayoutAfter.yaxis.type = "linear"; - it('should allow its own calendar', function() { - layoutOut.calendar = 'nepali'; - layoutIn = { - calendar: 'nepali', - xaxis: {type: 'date', calendar: 'coptic'}, - yaxis: {type: 'date', calendar: 'thai'} - }; + Plots.supplyDefaults(gd); - supplyLayoutDefaults(layoutIn, layoutOut, fullData); + Axes.swap(gd, [0]); - expect(layoutOut.xaxis.calendar).toBe('coptic'); - expect(layoutOut.yaxis.calendar).toBe('thai'); - }); + expect(gd.layout.xaxis).toEqual(expectedLayoutAfter.xaxis); + expect(gd.layout.yaxis).toEqual(expectedLayoutAfter.yaxis); }); - describe('categoryorder', function() { - - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - describe('setting, or not setting categoryorder if it is not explicitly declared', function() { - - it('should set categoryorder to default if categoryorder and categoryarray are not supplied', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], {xaxis: {type: 'category'}}); - expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); - expect(gd._fullLayout.xaxis.categorarray).toBe(undefined); - }); - - it('should set categoryorder to default even if type is not set to category explicitly', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}]); - expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); - expect(gd._fullLayout.xaxis.categorarray).toBe(undefined); - }); - - it('should NOT set categoryorder to default if type is not category', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}]); - expect(gd._fullLayout.yaxis.categoryorder).toBe(undefined); - expect(gd._fullLayout.xaxis.categorarray).toBe(undefined); - }); + it( + "should swap shared attributes, combine linear/log, and move annotations", + function() { + var gd = { + data: [ + { x: [1, 2, 3], y: [1, 2, 3] }, + { x: [1, 2, 3], y: [1, 2, 3], xaxis: "x2" } + ], + layout: { + xaxis: { + type: "linear", + // combine linear/log + ticks: "outside", + // same as x2 + ticklen: 5, + // default value + tickwidth: 2, + // different + side: "top", + // noSwap + // noSwap + domain: [0, 0.45] + }, + xaxis2: { + type: "log", + ticks: "outside", + tickcolor: "#444", + // default value in 2nd axis + tickwidth: 3, + side: "top", + domain: [0.55, 1] + }, + yaxis: { + type: "category", + ticks: "inside", + ticklen: 10, + tickcolor: "#f00", + tickwidth: 4, + showline: true, + // not present in either x + side: "right" + }, + annotations: [ + { x: 2, y: 3 }, + // xy referenced by default + { x: 3, y: 4, xref: "x2", yref: "y" }, + // any paper ref -> don't swap + { x: 5, y: 0.5, xref: "x", yref: "paper" } + ] + } + }; + var expectedXaxis = { + type: "category", + ticks: "inside", + ticklen: 10, + tickcolor: "#f00", + tickwidth: 2, + showline: true, + side: "top", + domain: [0, 0.45] + }, + expectedXaxis2 = { + type: "category", + ticks: "inside", + ticklen: 10, + tickcolor: "#f00", + tickwidth: 3, + showline: true, + side: "top", + domain: [0.55, 1] + }, + expectedYaxis = { + type: "linear", + ticks: "outside", + ticklen: 5, + tickwidth: 4, + side: "right" + }, + expectedAnnotations = [ + { x: 3, y: 2 }, + { x: 4, y: 3, xref: "x2", yref: "y" }, + { x: 5, y: 0.5, xref: "x", yref: "paper" } + ]; + + Plots.supplyDefaults(gd); + + Axes.swap(gd, [0, 1]); + + expect(gd.layout.xaxis).toEqual(expectedXaxis); + expect(gd.layout.xaxis2).toEqual(expectedXaxis2); + expect(gd.layout.yaxis).toEqual(expectedYaxis); + expect(gd.layout.annotations).toEqual(expectedAnnotations); + } + ); + }); + + describe("supplyLayoutDefaults", function() { + var layoutIn, layoutOut, fullData; + + beforeEach(function() { + layoutOut = { _has: Plots._hasPlotType, _basePlotModules: [] }; + fullData = []; + }); - it('should set categoryorder to default if type is overridden to be category', function() { - PlotlyInternal.plot(gd, [{x: [1, 2, 3, 4, 5], y: [15, 11, 12, 13, 14]}], {yaxis: {type: 'category'}}); - expect(gd._fullLayout.xaxis.categoryorder).toBe(undefined); - expect(gd._fullLayout.yaxis.categorarray).toBe(undefined); - expect(gd._fullLayout.yaxis.categoryorder).toBe('trace'); - expect(gd._fullLayout.yaxis.categorarray).toBe(undefined); - }); + var supplyLayoutDefaults = Axes.supplyLayoutDefaults; + + it( + "should set undefined linewidth/linecolor if linewidth, linecolor or showline is not supplied", + function() { + layoutIn = { xaxis: {}, yaxis: {} }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.linewidth).toBe(undefined); + expect(layoutOut.xaxis.linecolor).toBe(undefined); + expect(layoutOut.yaxis.linewidth).toBe(undefined); + expect(layoutOut.yaxis.linecolor).toBe(undefined); + } + ); + + it( + "should set default linewidth and linecolor if showline is true", + function() { + layoutIn = { xaxis: { showline: true } }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.linewidth).toBe(1); + expect(layoutOut.xaxis.linecolor).toBe(Color.defaultLine); + } + ); + + it( + "should set linewidth to default if linecolor is supplied and valid", + function() { + layoutIn = { xaxis: { linecolor: "black" } }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.linecolor).toBe("black"); + expect(layoutOut.xaxis.linewidth).toBe(1); + } + ); + + it( + "should set linecolor to default if linewidth is supplied and valid", + function() { + layoutIn = { yaxis: { linewidth: 2 } }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.yaxis.linewidth).toBe(2); + expect(layoutOut.yaxis.linecolor).toBe(Color.defaultLine); + } + ); + + it("should set default gridwidth and gridcolor", function() { + layoutIn = { xaxis: {}, yaxis: {} }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + var lightLine = tinycolor(Color.lightLine).toRgbString(); + expect(layoutOut.xaxis.gridwidth).toBe(1); + expect(tinycolor(layoutOut.xaxis.gridcolor).toRgbString()).toBe( + lightLine + ); + expect(layoutOut.yaxis.gridwidth).toBe(1); + expect(tinycolor(layoutOut.yaxis.gridcolor).toRgbString()).toBe( + lightLine + ); + }); - }); + it( + "should set gridcolor/gridwidth to undefined if showgrid is false", + function() { + layoutIn = { yaxis: { showgrid: false } }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.yaxis.gridwidth).toBe(undefined); + expect(layoutOut.yaxis.gridcolor).toBe(undefined); + } + ); + + it("should set default zerolinecolor/zerolinewidth", function() { + layoutIn = { xaxis: {}, yaxis: {} }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.zerolinewidth).toBe(1); + expect(layoutOut.xaxis.zerolinecolor).toBe(Color.defaultLine); + expect(layoutOut.yaxis.zerolinewidth).toBe(1); + expect(layoutOut.yaxis.zerolinecolor).toBe(Color.defaultLine); + }); - describe('setting categoryorder to "array"', function() { - - it('should leave categoryorder on "array" if it is supplied', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { - xaxis: {type: 'category', categoryorder: 'array', categoryarray: ['b', 'a', 'd', 'e', 'c']} - }); - expect(gd._fullLayout.xaxis.categoryorder).toBe('array'); - expect(gd._fullLayout.xaxis.categoryarray).toEqual(['b', 'a', 'd', 'e', 'c']); - }); - - it('should switch categoryorder on "array" if it is not supplied but categoryarray is supplied', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { - xaxis: {type: 'category', categoryarray: ['b', 'a', 'd', 'e', 'c']} - }); - expect(gd._fullLayout.xaxis.categoryorder).toBe('array'); - expect(gd._fullLayout.xaxis.categoryarray).toEqual(['b', 'a', 'd', 'e', 'c']); - }); - - it('should revert categoryorder to "trace" if "array" is supplied but there is no list', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { - xaxis: {type: 'category', categoryorder: 'array'} - }); - expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); - expect(gd._fullLayout.xaxis.categorarray).toBe(undefined); - }); + it( + "should set zerolinecolor/zerolinewidth to undefined if zeroline is false", + function() { + layoutIn = { xaxis: { zeroline: false } }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.zerolinewidth).toBe(undefined); + expect(layoutOut.xaxis.zerolinecolor).toBe(undefined); + } + ); + + it("should detect orphan axes (lone axes case)", function() { + layoutIn = { xaxis: {}, yaxis: {} }; + fullData = []; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._basePlotModules[0].name).toEqual("cartesian"); + }); - }); + it("should detect orphan axes (gl2d trace conflict case)", function() { + layoutIn = { xaxis: {}, yaxis: {} }; + fullData = [{ type: "scattergl", xaxis: "x", yaxis: "y" }]; - describe('do not set categoryorder to "array" if list exists but empty', function() { - - it('should switch categoryorder to default if list is not supplied', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { - xaxis: {type: 'category', categoryorder: 'array', categoryarray: []} - }); - expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); - expect(gd._fullLayout.xaxis.categoryarray).toEqual([]); - }); - - it('should not switch categoryorder on "array" if categoryarray is supplied but empty', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { - xaxis: {type: 'category', categoryarray: []} - }); - expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); - expect(gd._fullLayout.xaxis.categoryarray).toEqual(undefined); - }); - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._basePlotModules).toEqual([]); + }); - describe('do NOT set categoryorder to "array" if it has some other proper value', function() { - - it('should use specified categoryorder if it is supplied even if categoryarray exists', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { - xaxis: {type: 'category', categoryorder: 'trace', categoryarray: ['b', 'a', 'd', 'e', 'c']} - }); - expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); - expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined); - }); - - it('should use specified categoryorder if it is supplied even if categoryarray exists', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { - xaxis: {type: 'category', categoryorder: 'category ascending', categoryarray: ['b', 'a', 'd', 'e', 'c']} - }); - expect(gd._fullLayout.xaxis.categoryorder).toBe('category ascending'); - expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined); - }); - - it('should use specified categoryorder if it is supplied even if categoryarray exists', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { - xaxis: {type: 'category', categoryorder: 'category descending', categoryarray: ['b', 'a', 'd', 'e', 'c']} - }); - expect(gd._fullLayout.xaxis.categoryorder).toBe('category descending'); - expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined); - }); + it("should detect orphan axes (gl2d + cartesian case)", function() { + layoutIn = { xaxis2: {}, yaxis2: {} }; + fullData = [{ type: "scattergl", xaxis: "x", yaxis: "y" }]; - }); - - describe('setting categoryorder to the default if the value is unexpected', function() { + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._basePlotModules[0].name).toEqual("cartesian"); + }); - it('should switch categoryorder to "trace" if mode is supplied but invalid', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { - xaxis: {type: 'category', categoryorder: 'invalid value'} - }); - expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); - expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined); - }); + it("should detect orphan axes (gl3d present case)", function() { + layoutIn = { xaxis: {}, yaxis: {} }; + layoutOut._basePlotModules = [{ name: "gl3d" }]; - it('should switch categoryorder to "array" if mode is supplied but invalid and list is supplied', function() { - PlotlyInternal.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { - xaxis: {type: 'category', categoryorder: 'invalid value', categoryarray: ['b', 'a', 'd', 'e', 'c']} - }); - expect(gd._fullLayout.xaxis.categoryorder).toBe('array'); - expect(gd._fullLayout.xaxis.categoryarray).toEqual(['b', 'a', 'd', 'e', 'c']); - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._basePlotModules).toEqual([{ name: "gl3d" }]); + }); - }); + it("should detect orphan axes (geo present case)", function() { + layoutIn = { xaxis: {}, yaxis: {} }; + layoutOut._basePlotModules = [{ name: "geo" }]; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut._basePlotModules).toEqual([{ name: "geo" }]); }); - describe('handleTickDefaults', function() { - var data = [{ x: [1, 2, 3], y: [3, 4, 5] }], - gd; + it( + "should use 'axis.color' as default for 'axis.titlefont.color'", + function() { + layoutIn = { + xaxis: { color: "red" }, + yaxis: {}, + yaxis2: { titlefont: { color: "yellow" } } + }; + + layoutOut.font = { color: "blue" }, supplyLayoutDefaults( + layoutIn, + layoutOut, + fullData + ); + expect(layoutOut.xaxis.titlefont.color).toEqual("red"); + expect(layoutOut.yaxis.titlefont.color).toEqual("blue"); + expect(layoutOut.yaxis2.titlefont.color).toEqual("yellow"); + } + ); + + it("should use 'axis.color' as default for 'axis.linecolor'", function() { + layoutIn = { + xaxis: { showline: true, color: "red" }, + yaxis: { linecolor: "blue" }, + yaxis2: { showline: true } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.linecolor).toEqual("red"); + expect(layoutOut.yaxis.linecolor).toEqual("blue"); + expect(layoutOut.yaxis2.linecolor).toEqual("#444"); + }); - beforeEach(function() { - gd = createGraphDiv(); - }); + it( + "should use 'axis.color' as default for 'axis.zerolinecolor'", + function() { + layoutIn = { + xaxis: { showzeroline: true, color: "red" }, + yaxis: { zerolinecolor: "blue" }, + yaxis2: { showzeroline: true } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.zerolinecolor).toEqual("red"); + expect(layoutOut.yaxis.zerolinecolor).toEqual("blue"); + expect(layoutOut.yaxis2.zerolinecolor).toEqual("#444"); + } + ); + + it( + "should use combo of 'axis.color', bgcolor and lightFraction as default for 'axis.gridcolor'", + function() { + layoutIn = { + paper_bgcolor: "green", + plot_bgcolor: "yellow", + xaxis: { showgrid: true, color: "red" }, + yaxis: { gridcolor: "blue" }, + yaxis2: { showgrid: true } + }; + + var bgColor = Color.combine("yellow", "green"), + frac = 100 * (0xe - 0x4) / (0xf - 0x4); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.gridcolor).toEqual( + tinycolor.mix("red", bgColor, frac).toRgbString() + ); + expect(layoutOut.yaxis.gridcolor).toEqual("blue"); + expect(layoutOut.yaxis2.gridcolor).toEqual( + tinycolor.mix("#444", bgColor, frac).toRgbString() + ); + } + ); + + it("should inherit calendar from the layout", function() { + layoutOut.calendar = "nepali"; + layoutIn = { + calendar: "nepali", + xaxis: { type: "date" }, + yaxis: { type: "date" } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.calendar).toBe("nepali"); + expect(layoutOut.yaxis.calendar).toBe("nepali"); + }); - afterEach(destroyGraphDiv); - - it('should set defaults on bad inputs', function() { - var layout = { - yaxis: { - ticklen: 'invalid', - tickwidth: 'invalid', - tickcolor: 'invalid', - showticklabels: 'invalid', - tickfont: 'invalid', - tickangle: 'invalid' - } - }; - - PlotlyInternal.plot(gd, data, layout); - - var yaxis = gd._fullLayout.yaxis; - expect(yaxis.ticklen).toBe(5); - expect(yaxis.tickwidth).toBe(1); - expect(yaxis.tickcolor).toBe('#444'); - expect(yaxis.ticks).toBe('outside'); - expect(yaxis.showticklabels).toBe(true); - expect(yaxis.tickfont).toEqual({ family: '"Open Sans", verdana, arial, sans-serif', size: 12, color: '#444' }); - expect(yaxis.tickangle).toBe('auto'); - }); + it("should allow its own calendar", function() { + layoutOut.calendar = "nepali"; + layoutIn = { + calendar: "nepali", + xaxis: { type: "date", calendar: "coptic" }, + yaxis: { type: "date", calendar: "thai" } + }; - it('should use valid inputs', function() { - var layout = { - yaxis: { - ticklen: 10, - tickwidth: 5, - tickcolor: '#F00', - showticklabels: true, - tickfont: { family: 'Garamond', size: 72, color: '#0FF' }, - tickangle: -20 - } - }; - - PlotlyInternal.plot(gd, data, layout); - - var yaxis = gd._fullLayout.yaxis; - expect(yaxis.ticklen).toBe(10); - expect(yaxis.tickwidth).toBe(5); - expect(yaxis.tickcolor).toBe('#F00'); - expect(yaxis.ticks).toBe('outside'); - expect(yaxis.showticklabels).toBe(true); - expect(yaxis.tickfont).toEqual({ family: 'Garamond', size: 72, color: '#0FF' }); - expect(yaxis.tickangle).toBe(-20); - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); - it('should conditionally coerce based on showticklabels', function() { - var layout = { - yaxis: { - showticklabels: false, - tickangle: -90 - } - }; + expect(layoutOut.xaxis.calendar).toBe("coptic"); + expect(layoutOut.yaxis.calendar).toBe("thai"); + }); + }); - PlotlyInternal.plot(gd, data, layout); + describe("categoryorder", function() { + var gd; - var yaxis = gd._fullLayout.yaxis; - expect(yaxis.tickangle).toBeUndefined(); - }); + beforeEach(function() { + gd = createGraphDiv(); }); - describe('handleTickValueDefaults', function() { - function mockSupplyDefaults(axIn, axOut, axType) { - function coerce(attr, dflt) { - return Lib.coerce(axIn, axOut, Axes.layoutAttributes, attr, dflt); + afterEach(destroyGraphDiv); + + describe( + "setting, or not setting categoryorder if it is not explicitly declared", + function() { + it( + "should set categoryorder to default if categoryorder and categoryarray are not supplied", + function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { xaxis: { type: "category" } } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe("trace"); + expect(gd._fullLayout.xaxis.categorarray).toBe(undefined); + } + ); + + it( + "should set categoryorder to default even if type is not set to category explicitly", + function() { + PlotlyInternal.plot(gd, [ + { x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] } + ]); + expect(gd._fullLayout.xaxis.categoryorder).toBe("trace"); + expect(gd._fullLayout.xaxis.categorarray).toBe(undefined); + } + ); + + it( + "should NOT set categoryorder to default if type is not category", + function() { + PlotlyInternal.plot(gd, [ + { x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] } + ]); + expect(gd._fullLayout.yaxis.categoryorder).toBe(undefined); + expect(gd._fullLayout.xaxis.categorarray).toBe(undefined); + } + ); + + it( + "should set categoryorder to default if type is overridden to be category", + function() { + PlotlyInternal.plot( + gd, + [{ x: [1, 2, 3, 4, 5], y: [15, 11, 12, 13, 14] }], + { yaxis: { type: "category" } } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe(undefined); + expect(gd._fullLayout.yaxis.categorarray).toBe(undefined); + expect(gd._fullLayout.yaxis.categoryorder).toBe("trace"); + expect(gd._fullLayout.yaxis.categorarray).toBe(undefined); + } + ); + } + ); + + describe('setting categoryorder to "array"', function() { + it('should leave categoryorder on "array" if it is supplied', function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "array", + categoryarray: ["b", "a", "d", "e", "c"] } - - handleTickValueDefaults(axIn, axOut, coerce, axType); + } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe("array"); + expect(gd._fullLayout.xaxis.categoryarray).toEqual([ + "b", + "a", + "d", + "e", + "c" + ]); + }); + + it( + 'should switch categoryorder on "array" if it is not supplied but categoryarray is supplied', + function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryarray: ["b", "a", "d", "e", "c"] + } + } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe("array"); + expect(gd._fullLayout.xaxis.categoryarray).toEqual([ + "b", + "a", + "d", + "e", + "c" + ]); } - - it('should set default tickmode correctly', function() { - var axIn = {}, - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.tickmode).toBe('auto'); - - axIn = {tickmode: 'array', tickvals: 'stuff'}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.tickmode).toBe('auto'); - - axIn = {tickmode: 'array', tickvals: [1, 2, 3]}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'date'); - expect(axOut.tickmode).toBe('auto'); - - axIn = {tickvals: [1, 2, 3]}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.tickmode).toBe('array'); - - axIn = {dtick: 1}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.tickmode).toBe('linear'); - }); - - it('should set nticks iff tickmode=auto', function() { - var axIn = {}, - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.nticks).toBe(0); - - axIn = {tickmode: 'auto', nticks: 5}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.nticks).toBe(5); - - axIn = {tickmode: 'linear', nticks: 15}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.nticks).toBe(undefined); - }); - - it('should set tick0 and dtick iff tickmode=linear', function() { - var axIn = {tickmode: 'auto', tick0: 1, dtick: 1}, - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.tick0).toBe(undefined); - expect(axOut.dtick).toBe(undefined); - - axIn = {tickvals: [1, 2, 3], tick0: 1, dtick: 1}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.tick0).toBe(undefined); - expect(axOut.dtick).toBe(undefined); - - axIn = {tick0: 2.71, dtick: 0.00828}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.tick0).toBe(2.71); - expect(axOut.dtick).toBe(0.00828); - - axIn = {tickmode: 'linear', tick0: 3.14, dtick: 0.00159}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.tick0).toBe(3.14); - expect(axOut.dtick).toBe(0.00159); - }); - - it('should handle tick0 and dtick for date axes', function() { - var someMs = 123456789, - someMsDate = Lib.ms2DateTimeLocal(someMs), - oneDay = 24 * 3600 * 1000, - axIn = {tick0: someMs, dtick: String(3 * oneDay)}, - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'date'); - expect(axOut.tick0).toBe(someMsDate); - expect(axOut.dtick).toBe(3 * oneDay); - - var someDate = '2011-12-15 13:45:56'; - axIn = {tick0: someDate, dtick: 'M15'}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'date'); - expect(axOut.tick0).toBe(someDate); - expect(axOut.dtick).toBe('M15'); - - // dtick without tick0: get the right default - axIn = {dtick: 'M12'}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'date'); - expect(axOut.tick0).toBe('2000-01-01'); - expect(axOut.dtick).toBe('M12'); - - // now some stuff that shouldn't work, should give defaults - [ - ['next thursday', -1], - ['123-45', 'L1'], - ['', 'M0.5'], - ['', 'M-1'], - ['', '2000-01-01'] - ].forEach(function(v) { - axIn = {tick0: v[0], dtick: v[1]}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'date'); - expect(axOut.tick0).toBe('2000-01-01'); - expect(axOut.dtick).toBe(oneDay); - }); - }); - - it('should handle tick0 and dtick for log axes', function() { - var axIn = {tick0: '0.2', dtick: 0.3}, - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'log'); - expect(axOut.tick0).toBe(0.2); - expect(axOut.dtick).toBe(0.3); - - ['D1', 'D2'].forEach(function(v) { - axIn = {tick0: -1, dtick: v}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'log'); - // tick0 gets ignored for D - expect(axOut.tick0).toBe(0); - expect(axOut.dtick).toBe(v); - }); - - [ - [-1, 'L3'], - ['0.2', 'L0.3'], - [-1, 3], - ['0.1234', '0.69238473'] - ].forEach(function(v) { - axIn = {tick0: v[0], dtick: v[1]}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'log'); - expect(axOut.tick0).toBe(Number(v[0])); - expect(axOut.dtick).toBe((+v[1]) ? Number(v[1]) : v[1]); - }); - - // now some stuff that should not work, should give defaults - [ - ['', -1], - ['D1', 'D3'], - ['', 'D0'], - ['2011-01-01', 'L0'], - ['', 'L-1'] - ].forEach(function(v) { - axIn = {tick0: v[0], dtick: v[1]}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'log'); - expect(axOut.tick0).toBe(0); - expect(axOut.dtick).toBe(1); - }); - - }); - - it('should set tickvals and ticktext iff tickmode=array', function() { - var axIn = {tickmode: 'auto', tickvals: [1, 2, 3], ticktext: ['4', '5', '6']}, - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.tickvals).toBe(undefined); - expect(axOut.ticktext).toBe(undefined); - - axIn = {tickvals: [2, 4, 6, 8], ticktext: ['who', 'do', 'we', 'appreciate']}; - axOut = {}; - mockSupplyDefaults(axIn, axOut, 'linear'); - expect(axOut.tickvals).toEqual([2, 4, 6, 8]); - expect(axOut.ticktext).toEqual(['who', 'do', 'we', 'appreciate']); - }); + ); + + it( + 'should revert categoryorder to "trace" if "array" is supplied but there is no list', + function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { xaxis: { type: "category", categoryorder: "array" } } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe("trace"); + expect(gd._fullLayout.xaxis.categorarray).toBe(undefined); + } + ); }); - describe('saveRangeInitial', function() { - var saveRangeInitial = Axes.saveRangeInitial; - var gd, hasOneAxisChanged; - - beforeEach(function() { - gd = { - _fullLayout: { - xaxis: { range: [0, 0.5] }, - yaxis: { range: [0, 0.5] }, - xaxis2: { range: [0.5, 1] }, - yaxis2: { range: [0.5, 1] } + describe( + 'do not set categoryorder to "array" if list exists but empty', + function() { + it( + "should switch categoryorder to default if list is not supplied", + function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "array", + categoryarray: [] } - }; - }); - - it('should save range when autosize turned off and rangeInitial isn\'t defined', function() { - ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(ax) { - gd._fullLayout[ax].autorange = false; - }); - - hasOneAxisChanged = saveRangeInitial(gd); - - expect(hasOneAxisChanged).toBe(true); - expect(gd._fullLayout.xaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.yaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.xaxis2._rangeInitial).toEqual([0.5, 1]); - expect(gd._fullLayout.yaxis2._rangeInitial).toEqual([0.5, 1]); - }); - - it('should not overwrite saved range if rangeInitial is defined', function() { - ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(ax) { - gd._fullLayout[ax]._rangeInitial = gd._fullLayout[ax].range.slice(); - gd._fullLayout[ax].range = [0, 1]; - }); - - hasOneAxisChanged = saveRangeInitial(gd); - - expect(hasOneAxisChanged).toBe(false); - expect(gd._fullLayout.xaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.yaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.xaxis2._rangeInitial).toEqual([0.5, 1]); - expect(gd._fullLayout.yaxis2._rangeInitial).toEqual([0.5, 1]); - }); - - it('should save range when overwrite option is on and range has changed', function() { - ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(ax) { - gd._fullLayout[ax]._rangeInitial = gd._fullLayout[ax].range.slice(); - }); - gd._fullLayout.xaxis2.range = [0.2, 0.4]; - - hasOneAxisChanged = saveRangeInitial(gd, true); - expect(hasOneAxisChanged).toBe(true); - expect(gd._fullLayout.xaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.yaxis._rangeInitial).toEqual([0, 0.5]); - expect(gd._fullLayout.xaxis2._rangeInitial).toEqual([0.2, 0.4]); - expect(gd._fullLayout.yaxis2._rangeInitial).toEqual([0.5, 1]); - }); - }); - - describe('list', function() { - var listFunc = Axes.list; - var gd; - - it('returns empty array when no fullLayout is present', function() { - gd = {}; - - expect(listFunc(gd)).toEqual([]); - }); - - it('returns array of axes in fullLayout', function() { - gd = { - _fullLayout: { - xaxis: { _id: 'x' }, - yaxis: { _id: 'y' }, - yaxis2: { _id: 'y2' } + } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe("trace"); + expect(gd._fullLayout.xaxis.categoryarray).toEqual([]); + } + ); + + it( + 'should not switch categoryorder on "array" if categoryarray is supplied but empty', + function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { xaxis: { type: "category", categoryarray: [] } } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe("trace"); + expect(gd._fullLayout.xaxis.categoryarray).toEqual(undefined); + } + ); + } + ); + + describe( + 'do NOT set categoryorder to "array" if it has some other proper value', + function() { + it( + "should use specified categoryorder if it is supplied even if categoryarray exists", + function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "trace", + categoryarray: ["b", "a", "d", "e", "c"] } - }; - - expect(listFunc(gd)) - .toEqual([{ _id: 'x' }, { _id: 'y' }, { _id: 'y2' }]); - }); - - it('returns array of axes, including the ones in scenes', function() { - gd = { - _fullLayout: { - scene: { - xaxis: { _id: 'x' }, - yaxis: { _id: 'y' }, - zaxis: { _id: 'z' } - }, - scene2: { - xaxis: { _id: 'x' }, - yaxis: { _id: 'y' }, - zaxis: { _id: 'z' } - } + } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe("trace"); + expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined); + } + ); + + it( + "should use specified categoryorder if it is supplied even if categoryarray exists", + function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "category ascending", + categoryarray: ["b", "a", "d", "e", "c"] } - }; - - expect(listFunc(gd)) - .toEqual([ - { _id: 'x' }, { _id: 'y' }, { _id: 'z' }, - { _id: 'x' }, { _id: 'y' }, { _id: 'z' } - ]); - }); - - it('returns array of axes, excluding the ones in scenes with only2d option', function() { - gd = { - _fullLayout: { - scene: { - xaxis: { _id: 'x' }, - yaxis: { _id: 'y' }, - zaxis: { _id: 'z' } - }, - xaxis2: { _id: 'x2' }, - yaxis2: { _id: 'y2' } + } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe( + "category ascending" + ); + expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined); + } + ); + + it( + "should use specified categoryorder if it is supplied even if categoryarray exists", + function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "category descending", + categoryarray: ["b", "a", "d", "e", "c"] } - }; - - expect(listFunc(gd, '', true)) - .toEqual([{ _id: 'x2' }, { _id: 'y2' }]); - }); - - it('returns array of axes, of particular ax letter with axLetter option', function() { - gd = { - _fullLayout: { - scene: { - xaxis: { _id: 'x' }, - yaxis: { _id: 'y' }, - zaxis: { _id: 'z' - } - }, - xaxis2: { _id: 'x2' }, - yaxis2: { _id: 'y2' } + } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe( + "category descending" + ); + expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined); + } + ); + } + ); + + describe( + "setting categoryorder to the default if the value is unexpected", + function() { + it( + 'should switch categoryorder to "trace" if mode is supplied but invalid', + function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { xaxis: { type: "category", categoryorder: "invalid value" } } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe("trace"); + expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined); + } + ); + + it( + 'should switch categoryorder to "array" if mode is supplied but invalid and list is supplied', + function() { + PlotlyInternal.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "invalid value", + categoryarray: ["b", "a", "d", "e", "c"] } - }; + } + ); + expect(gd._fullLayout.xaxis.categoryorder).toBe("array"); + expect(gd._fullLayout.xaxis.categoryarray).toEqual([ + "b", + "a", + "d", + "e", + "c" + ]); + } + ); + } + ); + }); - expect(listFunc(gd, 'x')) - .toEqual([{ _id: 'x2' }, { _id: 'x' }]); - }); + describe("handleTickDefaults", function() { + var data = [{ x: [1, 2, 3], y: [3, 4, 5] }], gd; + beforeEach(function() { + gd = createGraphDiv(); }); - describe('getSubplots', function() { - var getSubplots = Axes.getSubplots; - var gd; - - it('returns list of subplots ids (from data only)', function() { - gd = { - data: [ - { type: 'scatter' }, - { type: 'scattergl', xaxis: 'x2', yaxis: 'y2' } - ] - }; - - expect(getSubplots(gd)) - .toEqual(['xy', 'x2y2']); - }); - - it('returns list of subplots ids (from fullLayout only)', function() { - gd = { - _fullLayout: { - xaxis: { _id: 'x', anchor: 'y' }, - yaxis: { _id: 'y', anchor: 'x' }, - xaxis2: { _id: 'x2', anchor: 'y2' }, - yaxis2: { _id: 'y2', anchor: 'x2' } - } - }; - - expect(getSubplots(gd)) - .toEqual(['xy', 'x2y2']); - }); - - it('returns list of subplots ids of particular axis with ax option', function() { - gd = { - data: [ - { type: 'scatter' }, - { type: 'scattergl', xaxis: 'x3', yaxis: 'y3' } - ], - _fullLayout: { - xaxis2: { _id: 'x2', anchor: 'y2' }, - yaxis2: { _id: 'y2', anchor: 'x2' }, - yaxis3: { _id: 'y3', anchor: 'free' } - } - }; - - expect(getSubplots(gd, { _id: 'x' })) - .toEqual(['xy']); - }); + afterEach(destroyGraphDiv); + + it("should set defaults on bad inputs", function() { + var layout = { + yaxis: { + ticklen: "invalid", + tickwidth: "invalid", + tickcolor: "invalid", + showticklabels: "invalid", + tickfont: "invalid", + tickangle: "invalid" + } + }; + + PlotlyInternal.plot(gd, data, layout); + + var yaxis = gd._fullLayout.yaxis; + expect(yaxis.ticklen).toBe(5); + expect(yaxis.tickwidth).toBe(1); + expect(yaxis.tickcolor).toBe("#444"); + expect(yaxis.ticks).toBe("outside"); + expect(yaxis.showticklabels).toBe(true); + expect(yaxis.tickfont).toEqual({ + family: '"Open Sans", verdana, arial, sans-serif', + size: 12, + color: "#444" + }); + expect(yaxis.tickangle).toBe("auto"); }); - describe('getAutoRange', function() { - var getAutoRange = Axes.getAutoRange; - var ax; - - it('returns reasonable range without explicit rangemode or autorange', function() { - ax = { - _min: [ - {val: 1, pad: 20}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], - type: 'linear', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([-0.5, 7]); - }); - - it('reverses axes', function() { - ax = { - _min: [ - {val: 1, pad: 20}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], - type: 'linear', - autorange: 'reversed', - rangemode: 'normal', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([7, -0.5]); - }); - - it('expands empty range', function() { - ax = { - _min: [ - {val: 2, pad: 0} - ], - _max: [ - {val: 2, pad: 0} - ], - type: 'linear', - rangemode: 'normal', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([1, 3]); - }); + it("should use valid inputs", function() { + var layout = { + yaxis: { + ticklen: 10, + tickwidth: 5, + tickcolor: "#F00", + showticklabels: true, + tickfont: { family: "Garamond", size: 72, color: "#0FF" }, + tickangle: -20 + } + }; + + PlotlyInternal.plot(gd, data, layout); + + var yaxis = gd._fullLayout.yaxis; + expect(yaxis.ticklen).toBe(10); + expect(yaxis.tickwidth).toBe(5); + expect(yaxis.tickcolor).toBe("#F00"); + expect(yaxis.ticks).toBe("outside"); + expect(yaxis.showticklabels).toBe(true); + expect(yaxis.tickfont).toEqual({ + family: "Garamond", + size: 72, + color: "#0FF" + }); + expect(yaxis.tickangle).toBe(-20); + }); - it('returns a lower bound of 0 on rangemode tozero with positive points', function() { - ax = { - _min: [ - {val: 1, pad: 20}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([0, 7]); - }); + it("should conditionally coerce based on showticklabels", function() { + var layout = { yaxis: { showticklabels: false, tickangle: -90 } }; - it('returns an upper bound of 0 on rangemode tozero with negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: -5, pad: 20}, - {val: -4, pad: 0}, - {val: -6, pad: 10}, - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([-12.5, 0]); - }); + PlotlyInternal.plot(gd, data, layout); - it('returns a positive and negative range on rangemode tozero with positive and negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: 6, pad: 10}, - {val: 7, pad: 0}, - {val: 5, pad: 20}, - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([-15, 10]); - }); + var yaxis = gd._fullLayout.yaxis; + expect(yaxis.tickangle).toBeUndefined(); + }); + }); + + describe("handleTickValueDefaults", function() { + function mockSupplyDefaults(axIn, axOut, axType) { + function coerce(attr, dflt) { + return Lib.coerce(axIn, axOut, Axes.layoutAttributes, attr, dflt); + } + + handleTickValueDefaults(axIn, axOut, coerce, axType); + } + + it("should set default tickmode correctly", function() { + var axIn = {}, axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.tickmode).toBe("auto"); + + axIn = { tickmode: "array", tickvals: "stuff" }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.tickmode).toBe("auto"); + + axIn = { tickmode: "array", tickvals: [1, 2, 3] }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "date"); + expect(axOut.tickmode).toBe("auto"); + + axIn = { tickvals: [1, 2, 3] }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.tickmode).toBe("array"); + + axIn = { dtick: 1 }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.tickmode).toBe("linear"); + }); - it('reverses range after applying rangemode tozero', function() { - ax = { - _min: [ - {val: 1, pad: 20}, - {val: 3, pad: 0}, - {val: 2, pad: 10} - ], - _max: [ - {val: 6, pad: 20}, - {val: 7, pad: 0}, - {val: 5, pad: 10}, - ], - type: 'linear', - autorange: 'reversed', - rangemode: 'tozero', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([7.5, 0]); - }); + it("should set nticks iff tickmode=auto", function() { + var axIn = {}, axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.nticks).toBe(0); - it('expands empty positive range to something including 0 with rangemode tozero', function() { - ax = { - _min: [ - {val: 5, pad: 0} - ], - _max: [ - {val: 5, pad: 0} - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([0, 6]); - }); + axIn = { tickmode: "auto", nticks: 5 }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.nticks).toBe(5); - it('expands empty negative range to something including 0 with rangemode tozero', function() { - ax = { - _min: [ - {val: -5, pad: 0} - ], - _max: [ - {val: -5, pad: 0} - ], - type: 'linear', - rangemode: 'tozero', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([-6, 0]); - }); + axIn = { tickmode: "linear", nticks: 15 }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.nticks).toBe(undefined); + }); - it('never returns a negative range when rangemode nonnegative is set with positive and negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: 6, pad: 20}, - {val: 7, pad: 0}, - {val: 5, pad: 10}, - ], - type: 'linear', - rangemode: 'nonnegative', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([0, 7.5]); - }); + it("should set tick0 and dtick iff tickmode=linear", function() { + var axIn = { tickmode: "auto", tick0: 1, dtick: 1 }, axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.tick0).toBe(undefined); + expect(axOut.dtick).toBe(undefined); + + axIn = { tickvals: [1, 2, 3], tick0: 1, dtick: 1 }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.tick0).toBe(undefined); + expect(axOut.dtick).toBe(undefined); + + axIn = { tick0: 2.71, dtick: 0.00828 }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.tick0).toBe(2.71); + expect(axOut.dtick).toBe(0.00828); + + axIn = { tickmode: "linear", tick0: 3.14, dtick: 0.00159 }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.tick0).toBe(3.14); + expect(axOut.dtick).toBe(0.00159); + }); - it('never returns a negative range when rangemode nonnegative is set with only negative points', function() { - ax = { - _min: [ - {val: -10, pad: 20}, - {val: -8, pad: 0}, - {val: -9, pad: 10} - ], - _max: [ - {val: -5, pad: 20}, - {val: -4, pad: 0}, - {val: -6, pad: 10}, - ], - type: 'linear', - rangemode: 'nonnegative', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([0, 1]); - }); + it("should handle tick0 and dtick for date axes", function() { + var someMs = 123456789, + someMsDate = Lib.ms2DateTimeLocal(someMs), + oneDay = 24 * 3600 * 1000, + axIn = { tick0: someMs, dtick: String(3 * oneDay) }, + axOut = {}; + mockSupplyDefaults(axIn, axOut, "date"); + expect(axOut.tick0).toBe(someMsDate); + expect(axOut.dtick).toBe(3 * oneDay); + + var someDate = "2011-12-15 13:45:56"; + axIn = { tick0: someDate, dtick: "M15" }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "date"); + expect(axOut.tick0).toBe(someDate); + expect(axOut.dtick).toBe("M15"); + + // dtick without tick0: get the right default + axIn = { dtick: "M12" }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "date"); + expect(axOut.tick0).toBe("2000-01-01"); + expect(axOut.dtick).toBe("M12"); + + // now some stuff that shouldn't work, should give defaults + [ + ["next thursday", -1], + ["123-45", "L1"], + ["", "M0.5"], + ["", "M-1"], + ["", "2000-01-01"] + ].forEach(function(v) { + axIn = { tick0: v[0], dtick: v[1] }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "date"); + expect(axOut.tick0).toBe("2000-01-01"); + expect(axOut.dtick).toBe(oneDay); + }); + }); - it('expands empty range to something nonnegative with rangemode nonnegative', function() { - ax = { - _min: [ - {val: -5, pad: 0} - ], - _max: [ - {val: -5, pad: 0} - ], - type: 'linear', - rangemode: 'nonnegative', - _length: 100 - }; - - expect(getAutoRange(ax)).toEqual([0, 1]); - }); + it("should handle tick0 and dtick for log axes", function() { + var axIn = { tick0: "0.2", dtick: 0.3 }, axOut = {}; + mockSupplyDefaults(axIn, axOut, "log"); + expect(axOut.tick0).toBe(0.2); + expect(axOut.dtick).toBe(0.3); + + ["D1", "D2"].forEach(function(v) { + axIn = { tick0: -1, dtick: v }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "log"); + // tick0 gets ignored for D + expect(axOut.tick0).toBe(0); + expect(axOut.dtick).toBe(v); + }); + + [ + [-1, "L3"], + ["0.2", "L0.3"], + [-1, 3], + ["0.1234", "0.69238473"] + ].forEach(function(v) { + axIn = { tick0: v[0], dtick: v[1] }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "log"); + expect(axOut.tick0).toBe(Number(v[0])); + expect(axOut.dtick).toBe(+v[1] ? Number(v[1]) : v[1]); + }); + + // now some stuff that should not work, should give defaults + [ + ["", -1], + ["D1", "D3"], + ["", "D0"], + ["2011-01-01", "L0"], + ["", "L-1"] + ].forEach(function(v) { + axIn = { tick0: v[0], dtick: v[1] }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "log"); + expect(axOut.tick0).toBe(0); + expect(axOut.dtick).toBe(1); + }); }); - describe('expand', function() { - var expand = Axes.expand; - var ax, data, options; - - // Axes.expand modifies ax, so this provides a simple - // way of getting a new clean copy each time. - function getDefaultAx() { - return { - c2l: Number, - type: 'linear', - _length: 100, - _m: 1, - _needsExpand: true - }; + it("should set tickvals and ticktext iff tickmode=array", function() { + var axIn = { + tickmode: "auto", + tickvals: [1, 2, 3], + ticktext: ["4", "5", "6"] + }, + axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.tickvals).toBe(undefined); + expect(axOut.ticktext).toBe(undefined); + + axIn = { + tickvals: [2, 4, 6, 8], + ticktext: ["who", "do", "we", "appreciate"] + }; + axOut = {}; + mockSupplyDefaults(axIn, axOut, "linear"); + expect(axOut.tickvals).toEqual([2, 4, 6, 8]); + expect(axOut.ticktext).toEqual(["who", "do", "we", "appreciate"]); + }); + }); + + describe("saveRangeInitial", function() { + var saveRangeInitial = Axes.saveRangeInitial; + var gd, hasOneAxisChanged; + + beforeEach(function() { + gd = { + _fullLayout: { + xaxis: { range: [0, 0.5] }, + yaxis: { range: [0, 0.5] }, + xaxis2: { range: [0.5, 1] }, + yaxis2: { range: [0.5, 1] } } + }; + }); - it('constructs simple ax._min and ._max correctly', function() { - ax = getDefaultAx(); - data = [1, 4, 7, 2]; - - expand(ax, data); - - expect(ax._min).toEqual([{val: 1, pad: 0}]); - expect(ax._max).toEqual([{val: 7, pad: 0}]); - }); - - it('calls ax.setScale if necessary', function() { - ax = { - c2l: Number, - type: 'linear', - setScale: function() {}, - _needsExpand: true - }; - spyOn(ax, 'setScale'); - data = [1]; - - expand(ax, data); - - expect(ax.setScale).toHaveBeenCalled(); - }); - - it('handles symmetric pads as numbers', function() { - ax = getDefaultAx(); - data = [1, 4, 2, 7]; - options = { - vpad: 2, - ppad: 10 - }; - - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -1, pad: 10}]); - expect(ax._max).toEqual([{val: 9, pad: 10}]); - }); - - it('handles symmetric pads as number arrays', function() { - ax = getDefaultAx(); - data = [1, 4, 2, 7]; - options = { - vpad: [1, 10, 6, 3], - ppad: [0, 15, 20, 10] - }; - - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -6, pad: 15}, {val: -4, pad: 20}]); - expect(ax._max).toEqual([{val: 14, pad: 15}, {val: 8, pad: 20}]); - }); - - it('handles separate pads as numbers', function() { - ax = getDefaultAx(); - data = [1, 4, 2, 7]; - options = { - vpadminus: 5, - vpadplus: 4, - ppadminus: 10, - ppadplus: 20 - }; - - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -4, pad: 10}]); - expect(ax._max).toEqual([{val: 11, pad: 20}]); - }); - - it('handles separate pads as number arrays', function() { - ax = getDefaultAx(); - data = [1, 4, 2, 7]; - options = { - vpadminus: [0, 3, 5, 1], - vpadplus: [8, 2, 1, 1], - ppadminus: [0, 30, 10, 20], - ppadplus: [0, 0, 40, 20] - }; - - expand(ax, data, options); + it( + "should save range when autosize turned off and rangeInitial isn't defined", + function() { + ["xaxis", "yaxis", "xaxis2", "yaxis2"].forEach(function(ax) { + gd._fullLayout[ax].autorange = false; + }); + + hasOneAxisChanged = saveRangeInitial(gd); + + expect(hasOneAxisChanged).toBe(true); + expect(gd._fullLayout.xaxis._rangeInitial).toEqual([0, 0.5]); + expect(gd._fullLayout.yaxis._rangeInitial).toEqual([0, 0.5]); + expect(gd._fullLayout.xaxis2._rangeInitial).toEqual([0.5, 1]); + expect(gd._fullLayout.yaxis2._rangeInitial).toEqual([0.5, 1]); + } + ); + + it( + "should not overwrite saved range if rangeInitial is defined", + function() { + ["xaxis", "yaxis", "xaxis2", "yaxis2"].forEach(function(ax) { + gd._fullLayout[ax]._rangeInitial = gd._fullLayout[ax].range.slice(); + gd._fullLayout[ax].range = [0, 1]; + }); + + hasOneAxisChanged = saveRangeInitial(gd); + + expect(hasOneAxisChanged).toBe(false); + expect(gd._fullLayout.xaxis._rangeInitial).toEqual([0, 0.5]); + expect(gd._fullLayout.yaxis._rangeInitial).toEqual([0, 0.5]); + expect(gd._fullLayout.xaxis2._rangeInitial).toEqual([0.5, 1]); + expect(gd._fullLayout.yaxis2._rangeInitial).toEqual([0.5, 1]); + } + ); + + it( + "should save range when overwrite option is on and range has changed", + function() { + ["xaxis", "yaxis", "xaxis2", "yaxis2"].forEach(function(ax) { + gd._fullLayout[ax]._rangeInitial = gd._fullLayout[ax].range.slice(); + }); + gd._fullLayout.xaxis2.range = [0.2, 0.4]; + + hasOneAxisChanged = saveRangeInitial(gd, true); + expect(hasOneAxisChanged).toBe(true); + expect(gd._fullLayout.xaxis._rangeInitial).toEqual([0, 0.5]); + expect(gd._fullLayout.yaxis._rangeInitial).toEqual([0, 0.5]); + expect(gd._fullLayout.xaxis2._rangeInitial).toEqual([0.2, 0.4]); + expect(gd._fullLayout.yaxis2._rangeInitial).toEqual([0.5, 1]); + } + ); + }); + + describe("list", function() { + var listFunc = Axes.list; + var gd; + + it("returns empty array when no fullLayout is present", function() { + gd = {}; + + expect(listFunc(gd)).toEqual([]); + }); - expect(ax._min).toEqual([{val: 1, pad: 30}, {val: -3, pad: 10}]); - expect(ax._max).toEqual([{val: 9, pad: 0}, {val: 3, pad: 40}, {val: 8, pad: 20}]); - }); + it("returns array of axes in fullLayout", function() { + gd = { + _fullLayout: { + xaxis: { _id: "x" }, + yaxis: { _id: "y" }, + yaxis2: { _id: "y2" } + } + }; - it('overrides symmetric pads with separate pads', function() { - ax = getDefaultAx(); - data = [1, 5]; - options = { - vpad: 1, - ppad: 10, - vpadminus: 2, - vpadplus: 4, - ppadminus: 20, - ppadplus: 40 - }; - - expand(ax, data, options); - - expect(ax._min).toEqual([{val: -1, pad: 20}]); - expect(ax._max).toEqual([{val: 9, pad: 40}]); - }); + expect(listFunc(gd)).toEqual([{ _id: "x" }, { _id: "y" }, { _id: "y2" }]); + }); - it('adds 5% padding if specified by flag', function() { - ax = getDefaultAx(); - data = [1, 5]; - options = { - vpad: 1, - ppad: 10, - padded: true - }; + it("returns array of axes, including the ones in scenes", function() { + gd = { + _fullLayout: { + scene: { + xaxis: { _id: "x" }, + yaxis: { _id: "y" }, + zaxis: { _id: "z" } + }, + scene2: { + xaxis: { _id: "x" }, + yaxis: { _id: "y" }, + zaxis: { _id: "z" } + } + } + }; + + expect(listFunc(gd)).toEqual([ + { _id: "x" }, + { _id: "y" }, + { _id: "z" }, + { _id: "x" }, + { _id: "y" }, + { _id: "z" } + ]); + }); - expand(ax, data, options); + it( + "returns array of axes, excluding the ones in scenes with only2d option", + function() { + gd = { + _fullLayout: { + scene: { + xaxis: { _id: "x" }, + yaxis: { _id: "y" }, + zaxis: { _id: "z" } + }, + xaxis2: { _id: "x2" }, + yaxis2: { _id: "y2" } + } + }; + + expect(listFunc(gd, "", true)).toEqual([{ _id: "x2" }, { _id: "y2" }]); + } + ); + + it( + "returns array of axes, of particular ax letter with axLetter option", + function() { + gd = { + _fullLayout: { + scene: { + xaxis: { _id: "x" }, + yaxis: { _id: "y" }, + zaxis: { _id: "z" } + }, + xaxis2: { _id: "x2" }, + yaxis2: { _id: "y2" } + } + }; + + expect(listFunc(gd, "x")).toEqual([{ _id: "x2" }, { _id: "x" }]); + } + ); + }); + + describe("getSubplots", function() { + var getSubplots = Axes.getSubplots; + var gd; + + it("returns list of subplots ids (from data only)", function() { + gd = { + data: [ + { type: "scatter" }, + { type: "scattergl", xaxis: "x2", yaxis: "y2" } + ] + }; + + expect(getSubplots(gd)).toEqual(["xy", "x2y2"]); + }); - expect(ax._min).toEqual([{val: 0, pad: 15}]); - expect(ax._max).toEqual([{val: 6, pad: 15}]); - }); + it("returns list of subplots ids (from fullLayout only)", function() { + gd = { + _fullLayout: { + xaxis: { _id: "x", anchor: "y" }, + yaxis: { _id: "y", anchor: "x" }, + xaxis2: { _id: "x2", anchor: "y2" }, + yaxis2: { _id: "y2", anchor: "x2" } + } + }; - it('has lower bound zero with all positive data if tozero is sset', function() { - ax = getDefaultAx(); - data = [2, 5]; - options = { - vpad: 1, - ppad: 10, - tozero: true - }; + expect(getSubplots(gd)).toEqual(["xy", "x2y2"]); + }); - expand(ax, data, options); + it( + "returns list of subplots ids of particular axis with ax option", + function() { + gd = { + data: [ + { type: "scatter" }, + { type: "scattergl", xaxis: "x3", yaxis: "y3" } + ], + _fullLayout: { + xaxis2: { _id: "x2", anchor: "y2" }, + yaxis2: { _id: "y2", anchor: "x2" }, + yaxis3: { _id: "y3", anchor: "free" } + } + }; + + expect(getSubplots(gd, { _id: "x" })).toEqual(["xy"]); + } + ); + }); + + describe("getAutoRange", function() { + var getAutoRange = Axes.getAutoRange; + var ax; + + it( + "returns reasonable range without explicit rangemode or autorange", + function() { + ax = { + _min: [{ val: 1, pad: 20 }, { val: 3, pad: 0 }, { val: 2, pad: 10 }], + _max: [{ val: 6, pad: 10 }, { val: 7, pad: 0 }, { val: 5, pad: 20 }], + type: "linear", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([-0.5, 7]); + } + ); + + it("reverses axes", function() { + ax = { + _min: [{ val: 1, pad: 20 }, { val: 3, pad: 0 }, { val: 2, pad: 10 }], + _max: [{ val: 6, pad: 10 }, { val: 7, pad: 0 }, { val: 5, pad: 20 }], + type: "linear", + autorange: "reversed", + rangemode: "normal", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([7, -0.5]); + }); - expect(ax._min).toEqual([{val: 0, pad: 0}]); - expect(ax._max).toEqual([{val: 6, pad: 10}]); - }); + it("expands empty range", function() { + ax = { + _min: [{ val: 2, pad: 0 }], + _max: [{ val: 2, pad: 0 }], + type: "linear", + rangemode: "normal", + _length: 100 + }; - it('has upper bound zero with all negative data if tozero is set', function() { - ax = getDefaultAx(); - data = [-7, -4]; - options = { - vpad: 1, - ppad: 10, - tozero: true - }; + expect(getAutoRange(ax)).toEqual([1, 3]); + }); - expand(ax, data, options); + it( + "returns a lower bound of 0 on rangemode tozero with positive points", + function() { + ax = { + _min: [{ val: 1, pad: 20 }, { val: 3, pad: 0 }, { val: 2, pad: 10 }], + _max: [{ val: 6, pad: 10 }, { val: 7, pad: 0 }, { val: 5, pad: 20 }], + type: "linear", + rangemode: "tozero", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([0, 7]); + } + ); + + it( + "returns an upper bound of 0 on rangemode tozero with negative points", + function() { + ax = { + _min: [ + { val: -10, pad: 20 }, + { val: -8, pad: 0 }, + { val: -9, pad: 10 } + ], + _max: [ + { val: -5, pad: 20 }, + { val: -4, pad: 0 }, + { val: -6, pad: 10 } + ], + type: "linear", + rangemode: "tozero", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([-12.5, 0]); + } + ); + + it( + "returns a positive and negative range on rangemode tozero with positive and negative points", + function() { + ax = { + _min: [ + { val: -10, pad: 20 }, + { val: -8, pad: 0 }, + { val: -9, pad: 10 } + ], + _max: [{ val: 6, pad: 10 }, { val: 7, pad: 0 }, { val: 5, pad: 20 }], + type: "linear", + rangemode: "tozero", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([-15, 10]); + } + ); + + it("reverses range after applying rangemode tozero", function() { + ax = { + _min: [{ val: 1, pad: 20 }, { val: 3, pad: 0 }, { val: 2, pad: 10 }], + _max: [{ val: 6, pad: 20 }, { val: 7, pad: 0 }, { val: 5, pad: 10 }], + type: "linear", + autorange: "reversed", + rangemode: "tozero", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([7.5, 0]); + }); - expect(ax._min).toEqual([{val: -8, pad: 10}]); - expect(ax._max).toEqual([{val: 0, pad: 0}]); - }); + it( + "expands empty positive range to something including 0 with rangemode tozero", + function() { + ax = { + _min: [{ val: 5, pad: 0 }], + _max: [{ val: 5, pad: 0 }], + type: "linear", + rangemode: "tozero", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([0, 6]); + } + ); + + it( + "expands empty negative range to something including 0 with rangemode tozero", + function() { + ax = { + _min: [{ val: -5, pad: 0 }], + _max: [{ val: -5, pad: 0 }], + type: "linear", + rangemode: "tozero", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([-6, 0]); + } + ); + + it( + "never returns a negative range when rangemode nonnegative is set with positive and negative points", + function() { + ax = { + _min: [ + { val: -10, pad: 20 }, + { val: -8, pad: 0 }, + { val: -9, pad: 10 } + ], + _max: [{ val: 6, pad: 20 }, { val: 7, pad: 0 }, { val: 5, pad: 10 }], + type: "linear", + rangemode: "nonnegative", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([0, 7.5]); + } + ); + + it( + "never returns a negative range when rangemode nonnegative is set with only negative points", + function() { + ax = { + _min: [ + { val: -10, pad: 20 }, + { val: -8, pad: 0 }, + { val: -9, pad: 10 } + ], + _max: [ + { val: -5, pad: 20 }, + { val: -4, pad: 0 }, + { val: -6, pad: 10 } + ], + type: "linear", + rangemode: "nonnegative", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([0, 1]); + } + ); + + it( + "expands empty range to something nonnegative with rangemode nonnegative", + function() { + ax = { + _min: [{ val: -5, pad: 0 }], + _max: [{ val: -5, pad: 0 }], + type: "linear", + rangemode: "nonnegative", + _length: 100 + }; + + expect(getAutoRange(ax)).toEqual([0, 1]); + } + ); + }); + + describe("expand", function() { + var expand = Axes.expand; + var ax, data, options; + + // Axes.expand modifies ax, so this provides a simple + // way of getting a new clean copy each time. + function getDefaultAx() { + return { + c2l: Number, + type: "linear", + _length: 100, + _m: 1, + _needsExpand: true + }; + } + + it("constructs simple ax._min and ._max correctly", function() { + ax = getDefaultAx(); + data = [1, 4, 7, 2]; + + expand(ax, data); + + expect(ax._min).toEqual([{ val: 1, pad: 0 }]); + expect(ax._max).toEqual([{ val: 7, pad: 0 }]); + }); - it('sets neither bound to zero with positive and negative data if tozero is set', function() { - ax = getDefaultAx(); - data = [-7, 4]; - options = { - vpad: 1, - ppad: 10, - tozero: true - }; + it("calls ax.setScale if necessary", function() { + ax = { + c2l: Number, + type: "linear", + setScale: function() {}, + _needsExpand: true + }; + spyOn(ax, "setScale"); + data = [1]; - expand(ax, data, options); + expand(ax, data); - expect(ax._min).toEqual([{val: -8, pad: 10}]); - expect(ax._max).toEqual([{val: 5, pad: 10}]); - }); + expect(ax.setScale).toHaveBeenCalled(); + }); - it('overrides padded with tozero', function() { - ax = getDefaultAx(); - data = [2, 5]; - options = { - vpad: 1, - ppad: 10, - tozero: true, - padded: true - }; + it("handles symmetric pads as numbers", function() { + ax = getDefaultAx(); + data = [1, 4, 2, 7]; + options = { vpad: 2, ppad: 10 }; - expand(ax, data, options); + expand(ax, data, options); - expect(ax._min).toEqual([{val: 0, pad: 0}]); - expect(ax._max).toEqual([{val: 6, pad: 15}]); - }); + expect(ax._min).toEqual([{ val: -1, pad: 10 }]); + expect(ax._max).toEqual([{ val: 9, pad: 10 }]); }); - describe('calcTicks and tickText', function() { - function mockCalc(ax) { - Axes.setConvert(ax); - ax.tickfont = {}; - ax._gd = {_fullLayout: {separators: '.,'}}; - return Axes.calcTicks(ax).map(function(v) { return v.text; }); - } + it("handles symmetric pads as number arrays", function() { + ax = getDefaultAx(); + data = [1, 4, 2, 7]; + options = { vpad: [1, 10, 6, 3], ppad: [0, 15, 20, 10] }; - function mockHoverText(ax, x) { - var xCalc = (ax.d2l_noadd || ax.d2l)(x); - var tickTextObj = Axes.tickText(ax, xCalc, true); - return tickTextObj.text; - } + expand(ax, data, options); - function checkHovers(ax, specArray) { - specArray.forEach(function(v) { - expect(mockHoverText(ax, v[0])) - .toBe(v[1], ax.dtick + ' - ' + v[0]); - }); - } + expect(ax._min).toEqual([{ val: -6, pad: 15 }, { val: -4, pad: 20 }]); + expect(ax._max).toEqual([{ val: 14, pad: 15 }, { val: 8, pad: 20 }]); + }); - it('provides a new date suffix whenever the suffix changes', function() { - var ax = { - type: 'date', - tickmode: 'linear', - tick0: '2000-01-01', - dtick: 14 * 24 * 3600 * 1000, // 14 days - range: ['1999-12-01', '2000-02-15'] - }; - var textOut = mockCalc(ax); - - var expectedText = [ - 'Dec 4
1999', - 'Dec 18', - 'Jan 1
2000', - 'Jan 15', - 'Jan 29', - 'Feb 12' - ]; - expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, '1999-12-18 15:34:33.3')) - .toBe('Dec 18, 1999, 15:34'); - - ax = { - type: 'date', - tickmode: 'linear', - tick0: '2000-01-01', - dtick: 12 * 3600 * 1000, // 12 hours - range: ['2000-01-03 11:00', '2000-01-06'] - }; - textOut = mockCalc(ax); - - expectedText = [ - '12:00
Jan 3, 2000', - '00:00
Jan 4, 2000', - '12:00', - '00:00
Jan 5, 2000', - '12:00', - '00:00
Jan 6, 2000' - ]; - expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, '2000-01-04 15:34:33.3')) - .toBe('Jan 4, 2000, 15:34:33'); - - ax = { - type: 'date', - tickmode: 'linear', - tick0: '2000-01-01', - dtick: 1000, // 1 sec - range: ['2000-02-03 23:59:57', '2000-02-04 00:00:02'] - }; - textOut = mockCalc(ax); - - expectedText = [ - '23:59:57
Feb 3, 2000', - '23:59:58', - '23:59:59', - '00:00:00
Feb 4, 2000', - '00:00:01', - '00:00:02' - ]; - expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, '2000-02-04 00:00:00.123456')) - .toBe('Feb 4, 2000, 00:00:00.1235'); - expect(mockHoverText(ax, '2000-02-04 00:00:00')) - .toBe('Feb 4, 2000'); - }); + it("handles separate pads as numbers", function() { + ax = getDefaultAx(); + data = [1, 4, 2, 7]; + options = { vpadminus: 5, vpadplus: 4, ppadminus: 10, ppadplus: 20 }; - it('should give dates extra precision if tick0 is weird', function() { - var ax = { - type: 'date', - tickmode: 'linear', - tick0: '2000-01-01 00:05', - dtick: 14 * 24 * 3600 * 1000, // 14 days - range: ['1999-12-01', '2000-02-15'] - }; - var textOut = mockCalc(ax); - - var expectedText = [ - '00:05
Dec 4, 1999', - '00:05
Dec 18, 1999', - '00:05
Jan 1, 2000', - '00:05
Jan 15, 2000', - '00:05
Jan 29, 2000', - '00:05
Feb 12, 2000' - ]; - expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, '2000-02-04 00:00:00.123456')) - .toBe('Feb 4, 2000'); - expect(mockHoverText(ax, '2000-02-04 00:00:05.123456')) - .toBe('Feb 4, 2000, 00:00:05'); - }); + expand(ax, data, options); - it('should never give dates more than 100 microsecond precision', function() { - var ax = { - type: 'date', - tickmode: 'linear', - tick0: '2000-01-01', - dtick: 1.1333, - range: ['2000-01-01', '2000-01-01 00:00:00.01'] - }; - var textOut = mockCalc(ax); - - var expectedText = [ - '00:00:00
Jan 1, 2000', - '00:00:00.0011', - '00:00:00.0023', - '00:00:00.0034', - '00:00:00.0045', - '00:00:00.0057', - '00:00:00.0068', - '00:00:00.0079', - '00:00:00.0091' - ]; - expect(textOut).toEqual(expectedText); - }); + expect(ax._min).toEqual([{ val: -4, pad: 10 }]); + expect(ax._max).toEqual([{ val: 11, pad: 20 }]); + }); - it('should handle edge cases with dates and tickvals', function() { - var ax = { - type: 'date', - tickmode: 'array', - tickvals: [ - '2012-01-01', - new Date(2012, 2, 1).getTime(), - '2012-08-01 00:00:00', - '2012-10-01 12:00:00', - new Date(2013, 0, 1, 0, 0, 1).getTime(), - '2010-01-01', '2014-01-01' // off the axis - ], - // only the first two have text - ticktext: ['New year', 'February'], - - // required to get calcTicks to run - range: ['2011-12-10', '2013-01-23'], - nticks: 10 - }; - var textOut = mockCalc(ax); - - var expectedText = [ - 'New year', - 'February', - 'Aug 1, 2012', - '12:00
Oct 1, 2012', - '00:00:01
Jan 1, 2013' - ]; - expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, '2012-01-01')) - .toBe('New year'); - expect(mockHoverText(ax, '2012-01-01 12:34:56.1234')) - .toBe('Jan 1, 2012, 12:34:56'); - }); + it("handles separate pads as number arrays", function() { + ax = getDefaultAx(); + data = [1, 4, 2, 7]; + options = { + vpadminus: [0, 3, 5, 1], + vpadplus: [8, 2, 1, 1], + ppadminus: [0, 30, 10, 20], + ppadplus: [0, 0, 40, 20] + }; + + expand(ax, data, options); + + expect(ax._min).toEqual([{ val: 1, pad: 30 }, { val: -3, pad: 10 }]); + expect(ax._max).toEqual([ + { val: 9, pad: 0 }, + { val: 3, pad: 40 }, + { val: 8, pad: 20 } + ]); + }); - it('should handle tickvals edge cases with linear and log axes', function() { - ['linear', 'log'].forEach(function(axType) { - var ax = { - type: axType, - tickmode: 'array', - tickvals: [1, 1.5, 2.6999999, 30, 39.999, 100, 0.1], - ticktext: ['One', '...and a half'], - // I'll be so happy when I can finally get rid of this switch! - range: axType === 'log' ? [-0.2, 1.8] : [0.5, 50], - nticks: 10 - }; - var textOut = mockCalc(ax); - - var expectedText = [ - 'One', - '...and a half', // the first two get explicit labels - '2.7', // 2.6999999 gets rounded to 2.7 - '30', - '39.999' // 39.999 does not get rounded - // 10 and 0.1 are off scale - ]; - expect(textOut).toEqual(expectedText, axType); - expect(mockHoverText(ax, 1)).toBe('One'); - expect(mockHoverText(ax, 19.999)).toBe('19.999'); - }); - }); + it("overrides symmetric pads with separate pads", function() { + ax = getDefaultAx(); + data = [1, 5]; + options = { + vpad: 1, + ppad: 10, + vpadminus: 2, + vpadplus: 4, + ppadminus: 20, + ppadplus: 40 + }; + + expand(ax, data, options); + + expect(ax._min).toEqual([{ val: -1, pad: 20 }]); + expect(ax._max).toEqual([{ val: 9, pad: 40 }]); + }); - it('should handle tickvals edge cases with category axes', function() { - var ax = { - type: 'category', - _categories: ['a', 'b', 'c', 'd'], - tickmode: 'array', - tickvals: ['a', 1, 1.5, 'c', 2.7, 3, 'e', 4, 5, -2], - ticktext: ['A!', 'B?', 'B->C'], - range: [-0.5, 4.5], - nticks: 10 - }; - var textOut = mockCalc(ax); - - var expectedText = [ - 'A!', // category position, explicit text - 'B?', // integer position, explicit text - 'B->C', // non-integer position, explicit text - 'c', // category position, no text: use category - 'd', // non-integer position, no text: use closest category - 'd', // integer position, no text: use category - '' // 4: number with no close category: leave blank - // but still include it so we get a tick mark & grid - // 'e', 5, -2: bad category and numbers out of range: omitted - ]; - expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, 0)).toBe('A!'); - expect(mockHoverText(ax, 2)).toBe('c'); - expect(mockHoverText(ax, 4)).toBe(''); - - // make sure we didn't add any more categories accidentally - expect(ax._categories).toEqual(['a', 'b', 'c', 'd']); - }); + it("adds 5% padding if specified by flag", function() { + ax = getDefaultAx(); + data = [1, 5]; + options = { vpad: 1, ppad: 10, padded: true }; - it('should always start at year for date axis hover', function() { - var ax = { - type: 'date', - tickmode: 'linear', - tick0: '2000-01-01', - dtick: 'M1200', - range: ['1000-01-01', '3000-01-01'], - nticks: 10 - }; - mockCalc(ax); - - checkHovers(ax, [ - ['2000-01-01', 'Jan 2000'], - ['2000-01-01 11:00', 'Jan 2000'], - ['2000-01-01 11:14', 'Jan 2000'], - ['2000-01-01 11:00:15', 'Jan 2000'], - ['2000-01-01 11:00:00.1', 'Jan 2000'], - ['2000-01-01 11:00:00.0001', 'Jan 2000'] - ]); + expand(ax, data, options); - ax.dtick = 'M1'; - ax.range = ['1999-06-01', '2000-06-01']; - mockCalc(ax); - - checkHovers(ax, [ - ['2000-01-01', 'Jan 1, 2000'], - ['2000-01-01 11:00', 'Jan 1, 2000'], - ['2000-01-01 11:14', 'Jan 1, 2000'], - ['2000-01-01 11:00:15', 'Jan 1, 2000'], - ['2000-01-01 11:00:00.1', 'Jan 1, 2000'], - ['2000-01-01 11:00:00.0001', 'Jan 1, 2000'] - ]); + expect(ax._min).toEqual([{ val: 0, pad: 15 }]); + expect(ax._max).toEqual([{ val: 6, pad: 15 }]); + }); - ax.dtick = 24 * 3600000; // one day - ax.range = ['1999-12-15', '2000-01-15']; - mockCalc(ax); - - checkHovers(ax, [ - ['2000-01-01', 'Jan 1, 2000'], - ['2000-01-01 11:00', 'Jan 1, 2000, 11:00'], - ['2000-01-01 11:14', 'Jan 1, 2000, 11:14'], - ['2000-01-01 11:00:15', 'Jan 1, 2000, 11:00'], - ['2000-01-01 11:00:00.1', 'Jan 1, 2000, 11:00'], - ['2000-01-01 11:00:00.0001', 'Jan 1, 2000, 11:00'] - ]); + it( + "has lower bound zero with all positive data if tozero is sset", + function() { + ax = getDefaultAx(); + data = [2, 5]; + options = { vpad: 1, ppad: 10, tozero: true }; + + expand(ax, data, options); + + expect(ax._min).toEqual([{ val: 0, pad: 0 }]); + expect(ax._max).toEqual([{ val: 6, pad: 10 }]); + } + ); + + it( + "has upper bound zero with all negative data if tozero is set", + function() { + ax = getDefaultAx(); + data = [-7, -4]; + options = { vpad: 1, ppad: 10, tozero: true }; + + expand(ax, data, options); + + expect(ax._min).toEqual([{ val: -8, pad: 10 }]); + expect(ax._max).toEqual([{ val: 0, pad: 0 }]); + } + ); + + it( + "sets neither bound to zero with positive and negative data if tozero is set", + function() { + ax = getDefaultAx(); + data = [-7, 4]; + options = { vpad: 1, ppad: 10, tozero: true }; + + expand(ax, data, options); + + expect(ax._min).toEqual([{ val: -8, pad: 10 }]); + expect(ax._max).toEqual([{ val: 5, pad: 10 }]); + } + ); + + it("overrides padded with tozero", function() { + ax = getDefaultAx(); + data = [2, 5]; + options = { vpad: 1, ppad: 10, tozero: true, padded: true }; + + expand(ax, data, options); + + expect(ax._min).toEqual([{ val: 0, pad: 0 }]); + expect(ax._max).toEqual([{ val: 6, pad: 15 }]); + }); + }); + + describe("calcTicks and tickText", function() { + function mockCalc(ax) { + Axes.setConvert(ax); + ax.tickfont = {}; + ax._gd = { _fullLayout: { separators: ".," } }; + return Axes.calcTicks(ax).map(function(v) { + return v.text; + }); + } + + function mockHoverText(ax, x) { + var xCalc = (ax.d2l_noadd || ax.d2l)(x); + var tickTextObj = Axes.tickText(ax, xCalc, true); + return tickTextObj.text; + } + + function checkHovers(ax, specArray) { + specArray.forEach(function(v) { + expect(mockHoverText(ax, v[0])).toBe(v[1], ax.dtick + " - " + v[0]); + }); + } + + it("provides a new date suffix whenever the suffix changes", function() { + var ax = { + type: "date", + tickmode: "linear", + tick0: "2000-01-01", + dtick: 14 * 24 * 3600 * 1000, + // 14 days + range: ["1999-12-01", "2000-02-15"] + }; + var textOut = mockCalc(ax); + + var expectedText = [ + "Dec 4
1999", + "Dec 18", + "Jan 1
2000", + "Jan 15", + "Jan 29", + "Feb 12" + ]; + expect(textOut).toEqual(expectedText); + expect(mockHoverText(ax, "1999-12-18 15:34:33.3")).toBe( + "Dec 18, 1999, 15:34" + ); + + ax = { + type: "date", + tickmode: "linear", + tick0: "2000-01-01", + dtick: 12 * 3600 * 1000, + // 12 hours + range: ["2000-01-03 11:00", "2000-01-06"] + }; + textOut = mockCalc(ax); + + expectedText = [ + "12:00
Jan 3, 2000", + "00:00
Jan 4, 2000", + "12:00", + "00:00
Jan 5, 2000", + "12:00", + "00:00
Jan 6, 2000" + ]; + expect(textOut).toEqual(expectedText); + expect(mockHoverText(ax, "2000-01-04 15:34:33.3")).toBe( + "Jan 4, 2000, 15:34:33" + ); + + ax = { + type: "date", + tickmode: "linear", + tick0: "2000-01-01", + dtick: 1000, + // 1 sec + range: ["2000-02-03 23:59:57", "2000-02-04 00:00:02"] + }; + textOut = mockCalc(ax); + + expectedText = [ + "23:59:57
Feb 3, 2000", + "23:59:58", + "23:59:59", + "00:00:00
Feb 4, 2000", + "00:00:01", + "00:00:02" + ]; + expect(textOut).toEqual(expectedText); + expect(mockHoverText(ax, "2000-02-04 00:00:00.123456")).toBe( + "Feb 4, 2000, 00:00:00.1235" + ); + expect(mockHoverText(ax, "2000-02-04 00:00:00")).toBe("Feb 4, 2000"); + }); - ax.dtick = 3600000; // one hour - ax.range = ['1999-12-31', '2000-01-02']; - mockCalc(ax); - - checkHovers(ax, [ - ['2000-01-01', 'Jan 1, 2000'], - ['2000-01-01 11:00', 'Jan 1, 2000, 11:00'], - ['2000-01-01 11:14', 'Jan 1, 2000, 11:14'], - ['2000-01-01 11:00:15', 'Jan 1, 2000, 11:00:15'], - ['2000-01-01 11:00:00.1', 'Jan 1, 2000, 11:00'], - ['2000-01-01 11:00:00.0001', 'Jan 1, 2000, 11:00'] - ]); + it("should give dates extra precision if tick0 is weird", function() { + var ax = { + type: "date", + tickmode: "linear", + tick0: "2000-01-01 00:05", + dtick: 14 * 24 * 3600 * 1000, + // 14 days + range: ["1999-12-01", "2000-02-15"] + }; + var textOut = mockCalc(ax); + + var expectedText = [ + "00:05
Dec 4, 1999", + "00:05
Dec 18, 1999", + "00:05
Jan 1, 2000", + "00:05
Jan 15, 2000", + "00:05
Jan 29, 2000", + "00:05
Feb 12, 2000" + ]; + expect(textOut).toEqual(expectedText); + expect(mockHoverText(ax, "2000-02-04 00:00:00.123456")).toBe( + "Feb 4, 2000" + ); + expect(mockHoverText(ax, "2000-02-04 00:00:05.123456")).toBe( + "Feb 4, 2000, 00:00:05" + ); + }); - ax.dtick = 60000; // one minute - ax.range = ['1999-12-31 23:00', '2000-01-01 01:00']; - mockCalc(ax); - - checkHovers(ax, [ - ['2000-01-01', 'Jan 1, 2000'], - ['2000-01-01 11:00', 'Jan 1, 2000, 11:00'], - ['2000-01-01 11:14', 'Jan 1, 2000, 11:14'], - ['2000-01-01 11:00:15', 'Jan 1, 2000, 11:00:15'], - ['2000-01-01 11:00:00.1', 'Jan 1, 2000, 11:00'], - ['2000-01-01 11:00:00.0001', 'Jan 1, 2000, 11:00'] - ]); + it( + "should never give dates more than 100 microsecond precision", + function() { + var ax = { + type: "date", + tickmode: "linear", + tick0: "2000-01-01", + dtick: 1.1333, + range: ["2000-01-01", "2000-01-01 00:00:00.01"] + }; + var textOut = mockCalc(ax); + + var expectedText = [ + "00:00:00
Jan 1, 2000", + "00:00:00.0011", + "00:00:00.0023", + "00:00:00.0034", + "00:00:00.0045", + "00:00:00.0057", + "00:00:00.0068", + "00:00:00.0079", + "00:00:00.0091" + ]; + expect(textOut).toEqual(expectedText); + } + ); + + it("should handle edge cases with dates and tickvals", function() { + var ax = { + type: "date", + tickmode: "array", + tickvals: [ + "2012-01-01", + new Date(2012, 2, 1).getTime(), + "2012-08-01 00:00:00", + "2012-10-01 12:00:00", + new Date(2013, 0, 1, 0, 0, 1).getTime(), + "2010-01-01", + // off the axis + "2014-01-01" + ], + // only the first two have text + ticktext: ["New year", "February"], + // required to get calcTicks to run + range: ["2011-12-10", "2013-01-23"], + nticks: 10 + }; + var textOut = mockCalc(ax); + + var expectedText = [ + "New year", + "February", + "Aug 1, 2012", + "12:00
Oct 1, 2012", + "00:00:01
Jan 1, 2013" + ]; + expect(textOut).toEqual(expectedText); + expect(mockHoverText(ax, "2012-01-01")).toBe("New year"); + expect(mockHoverText(ax, "2012-01-01 12:34:56.1234")).toBe( + "Jan 1, 2012, 12:34:56" + ); + }); - ax.dtick = 1000; // one second - ax.range = ['1999-12-31 23:59', '2000-01-01 00:01']; - mockCalc(ax); - - checkHovers(ax, [ - ['2000-01-01', 'Jan 1, 2000'], - ['2000-01-01 11:00', 'Jan 1, 2000, 11:00'], - ['2000-01-01 11:14', 'Jan 1, 2000, 11:14'], - ['2000-01-01 11:00:15', 'Jan 1, 2000, 11:00:15'], - ['2000-01-01 11:00:00.1', 'Jan 1, 2000, 11:00:00.1'], - ['2000-01-01 11:00:00.0001', 'Jan 1, 2000, 11:00:00.0001'] - ]); - }); + it( + "should handle tickvals edge cases with linear and log axes", + function() { + ["linear", "log"].forEach(function(axType) { + var ax = { + type: axType, + tickmode: "array", + tickvals: [1, 1.5, 2.6999999, 30, 39.999, 100, 0.1], + ticktext: ["One", "...and a half"], + // I'll be so happy when I can finally get rid of this switch! + range: ( + axType === "log" ? [-0.2, 1.8] : [0.5, 50] + ), + nticks: 10 + }; + var textOut = mockCalc(ax); + + var expectedText = [ + "One", + "...and a half", + // the first two get explicit labels + "2.7", + // 2.6999999 gets rounded to 2.7 + "30", + // 39.999 does not get rounded + // 10 and 0.1 are off scale + "39.999" + ]; + expect(textOut).toEqual(expectedText, axType); + expect(mockHoverText(ax, 1)).toBe("One"); + expect(mockHoverText(ax, 19.999)).toBe("19.999"); + }); + } + ); + + it("should handle tickvals edge cases with category axes", function() { + var ax = { + type: "category", + _categories: ["a", "b", "c", "d"], + tickmode: "array", + tickvals: ["a", 1, 1.5, "c", 2.7, 3, "e", 4, 5, -2], + ticktext: ["A!", "B?", "B->C"], + range: [-0.5, 4.5], + nticks: 10 + }; + var textOut = mockCalc(ax); + + var expectedText = [ + "A!", + // category position, explicit text + "B?", + // integer position, explicit text + "B->C", + // non-integer position, explicit text + "c", + // category position, no text: use category + "d", + // non-integer position, no text: use closest category + "d", + // integer position, no text: use category + // 4: number with no close category: leave blank + // but still include it so we get a tick mark & grid + // 'e', 5, -2: bad category and numbers out of range: omitted + "" + ]; + expect(textOut).toEqual(expectedText); + expect(mockHoverText(ax, 0)).toBe("A!"); + expect(mockHoverText(ax, 2)).toBe("c"); + expect(mockHoverText(ax, 4)).toBe(""); + + // make sure we didn't add any more categories accidentally + expect(ax._categories).toEqual(["a", "b", "c", "d"]); }); - describe('autoBin', function() { + it("should always start at year for date axis hover", function() { + var ax = { + type: "date", + tickmode: "linear", + tick0: "2000-01-01", + dtick: "M1200", + range: ["1000-01-01", "3000-01-01"], + nticks: 10 + }; + mockCalc(ax); + + checkHovers(ax, [ + ["2000-01-01", "Jan 2000"], + ["2000-01-01 11:00", "Jan 2000"], + ["2000-01-01 11:14", "Jan 2000"], + ["2000-01-01 11:00:15", "Jan 2000"], + ["2000-01-01 11:00:00.1", "Jan 2000"], + ["2000-01-01 11:00:00.0001", "Jan 2000"] + ]); + + ax.dtick = "M1"; + ax.range = ["1999-06-01", "2000-06-01"]; + mockCalc(ax); + + checkHovers(ax, [ + ["2000-01-01", "Jan 1, 2000"], + ["2000-01-01 11:00", "Jan 1, 2000"], + ["2000-01-01 11:14", "Jan 1, 2000"], + ["2000-01-01 11:00:15", "Jan 1, 2000"], + ["2000-01-01 11:00:00.1", "Jan 1, 2000"], + ["2000-01-01 11:00:00.0001", "Jan 1, 2000"] + ]); + + ax.dtick = 24 * 3600000; + // one day + ax.range = ["1999-12-15", "2000-01-15"]; + mockCalc(ax); + + checkHovers(ax, [ + ["2000-01-01", "Jan 1, 2000"], + ["2000-01-01 11:00", "Jan 1, 2000, 11:00"], + ["2000-01-01 11:14", "Jan 1, 2000, 11:14"], + ["2000-01-01 11:00:15", "Jan 1, 2000, 11:00"], + ["2000-01-01 11:00:00.1", "Jan 1, 2000, 11:00"], + ["2000-01-01 11:00:00.0001", "Jan 1, 2000, 11:00"] + ]); + + ax.dtick = 3600000; + // one hour + ax.range = ["1999-12-31", "2000-01-02"]; + mockCalc(ax); + + checkHovers(ax, [ + ["2000-01-01", "Jan 1, 2000"], + ["2000-01-01 11:00", "Jan 1, 2000, 11:00"], + ["2000-01-01 11:14", "Jan 1, 2000, 11:14"], + ["2000-01-01 11:00:15", "Jan 1, 2000, 11:00:15"], + ["2000-01-01 11:00:00.1", "Jan 1, 2000, 11:00"], + ["2000-01-01 11:00:00.0001", "Jan 1, 2000, 11:00"] + ]); + + ax.dtick = 60000; + // one minute + ax.range = ["1999-12-31 23:00", "2000-01-01 01:00"]; + mockCalc(ax); + + checkHovers(ax, [ + ["2000-01-01", "Jan 1, 2000"], + ["2000-01-01 11:00", "Jan 1, 2000, 11:00"], + ["2000-01-01 11:14", "Jan 1, 2000, 11:14"], + ["2000-01-01 11:00:15", "Jan 1, 2000, 11:00:15"], + ["2000-01-01 11:00:00.1", "Jan 1, 2000, 11:00"], + ["2000-01-01 11:00:00.0001", "Jan 1, 2000, 11:00"] + ]); + + ax.dtick = 1000; + // one second + ax.range = ["1999-12-31 23:59", "2000-01-01 00:01"]; + mockCalc(ax); + + checkHovers(ax, [ + ["2000-01-01", "Jan 1, 2000"], + ["2000-01-01 11:00", "Jan 1, 2000, 11:00"], + ["2000-01-01 11:14", "Jan 1, 2000, 11:14"], + ["2000-01-01 11:00:15", "Jan 1, 2000, 11:00:15"], + ["2000-01-01 11:00:00.1", "Jan 1, 2000, 11:00:00.1"], + ["2000-01-01 11:00:00.0001", "Jan 1, 2000, 11:00:00.0001"] + ]); + }); + }); - function _autoBin(x, ax, nbins) { - ax._categories = []; - Axes.setConvert(ax); + describe("autoBin", function() { + function _autoBin(x, ax, nbins) { + ax._categories = []; + Axes.setConvert(ax); - var d = ax.makeCalcdata({ x: x }, 'x'); + var d = ax.makeCalcdata({ x: x }, "x"); - return Axes.autoBin(d, ax, nbins, false, 'gregorian'); - } + return Axes.autoBin(d, ax, nbins, false, "gregorian"); + } - it('should auto bin categories', function() { - var out = _autoBin( - ['apples', 'oranges', 'bananas'], - { type: 'category' } - ); + it("should auto bin categories", function() { + var out = _autoBin(["apples", "oranges", "bananas"], { + type: "category" + }); - expect(out).toEqual({ - start: -0.5, - end: 2.5, - size: 1 - }); - }); + expect(out).toEqual({ start: -0.5, end: 2.5, size: 1 }); + }); - it('should not error out for categories on linear axis', function() { - var out = _autoBin( - ['apples', 'oranges', 'bananas'], - { type: 'linear' } - ); + it("should not error out for categories on linear axis", function() { + var out = _autoBin(["apples", "oranges", "bananas"], { + type: "linear" + }); - expect(out).toEqual({ - start: undefined, - end: undefined, - size: 2 - }); - }); + expect(out).toEqual({ start: undefined, end: undefined, size: 2 }); + }); - it('should not error out for categories on log axis', function() { - var out = _autoBin( - ['apples', 'oranges', 'bananas'], - { type: 'log' } - ); + it("should not error out for categories on log axis", function() { + var out = _autoBin(["apples", "oranges", "bananas"], { type: "log" }); - expect(out).toEqual({ - start: undefined, - end: undefined, - size: 2 - }); - }); + expect(out).toEqual({ start: undefined, end: undefined, size: 2 }); + }); - it('should not error out for categories on date axis', function() { - var out = _autoBin( - ['apples', 'oranges', 'bananas'], - { type: 'date' } - ); + it("should not error out for categories on date axis", function() { + var out = _autoBin(["apples", "oranges", "bananas"], { type: "date" }); - expect(out).toEqual({ - start: undefined, - end: undefined, - size: 2 - }); - }); + expect(out).toEqual({ start: undefined, end: undefined, size: 2 }); + }); - it('should auto bin linear data', function() { - var out = _autoBin( - [1, 1, 2, 2, 3, 3, 4, 4], - { type: 'linear' } - ); + it("should auto bin linear data", function() { + var out = _autoBin([1, 1, 2, 2, 3, 3, 4, 4], { type: "linear" }); - expect(out).toEqual({ - start: 0.5, - end: 4.5, - size: 1 - }); - }); + expect(out).toEqual({ start: 0.5, end: 4.5, size: 1 }); + }); - it('should auto bin linear data with nbins constraint', function() { - var out = _autoBin( - [1, 1, 2, 2, 3, 3, 4, 4], - { type: 'linear' }, - 2 - ); + it("should auto bin linear data with nbins constraint", function() { + var out = _autoBin([1, 1, 2, 2, 3, 3, 4, 4], { type: "linear" }, 2); - // when size > 1 with all integers, we want the starting point to be - // a half integer below the round number a tick would be at (in this case 0) - // to approximate the half-open interval [) that's commonly used. - expect(out).toEqual({ - start: -0.5, - end: 5.5, - size: 2 - }); - }); + // when size > 1 with all integers, we want the starting point to be + // a half integer below the round number a tick would be at (in this case 0) + // to approximate the half-open interval [) that's commonly used. + expect(out).toEqual({ start: -0.5, end: 5.5, size: 2 }); }); + }); }); diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index cd7b44674b5..492ec5904e7 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -1,1298 +1,1266 @@ -var Plotly = require('@lib/index'); +var Plotly = require("@lib/index"); -var Bar = require('@src/traces/bar'); -var Lib = require('@src/lib'); -var Plots = require('@src/plots/plots'); +var Bar = require("@src/traces/bar"); +var Lib = require("@src/lib"); +var Plots = require("@src/plots/plots"); -var PlotlyInternal = require('@src/plotly'); +var PlotlyInternal = require("@src/plotly"); var Axes = PlotlyInternal.Axes; -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var customMatchers = require('../assets/custom_matchers'); - -describe('Bar.supplyDefaults', function() { - 'use strict'; - - var traceIn, - traceOut; - - var defaultColor = '#444'; - - var supplyDefaults = Bar.supplyDefaults; - - beforeEach(function() { - traceOut = {}; - }); - - it('should set visible to false when x and y are empty', function() { - traceIn = {}; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [], - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - }); - - it('should set visible to false when x or y is empty', function() { - traceIn = { - x: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [], - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [1, 2, 3], - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - }); - - it('should not set base, offset or width', function() { - traceIn = { - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.base).toBeUndefined(); - expect(traceOut.offset).toBeUndefined(); - expect(traceOut.width).toBeUndefined(); - }); - - it('should coerce a non-negative width', function() { - traceIn = { - width: -1, - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.width).toBeUndefined(); - }); - - it('should coerce textposition to none', function() { - traceIn = { - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.textposition).toBe('none'); - expect(traceOut.texfont).toBeUndefined(); - expect(traceOut.insidetexfont).toBeUndefined(); - expect(traceOut.outsidetexfont).toBeUndefined(); - }); - - it('should default textfont to layout.font', function() { - traceIn = { - textposition: 'inside', - y: [1, 2, 3] - }; - - var layout = { - font: {family: 'arial', color: '#AAA', size: 13} - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - - expect(traceOut.textposition).toBe('inside'); - expect(traceOut.textfont).toEqual(layout.font); - expect(traceOut.textfont).not.toBe(layout.font); - expect(traceOut.insidetextfont).toEqual(layout.font); - expect(traceOut.insidetextfont).not.toBe(layout.font); - expect(traceOut.insidetextfont).not.toBe(traceOut.textfont); - expect(traceOut.outsidetexfont).toBeUndefined(); - }); - - it('should inherit layout.calendar', function() { - traceIn = { - x: [1, 2, 3], - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('islamic'); - expect(traceOut.ycalendar).toBe('islamic'); - }); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var customMatchers = require("../assets/custom_matchers"); + +describe("Bar.supplyDefaults", function() { + "use strict"; + var traceIn, traceOut; + + var defaultColor = "#444"; + + var supplyDefaults = Bar.supplyDefaults; + + beforeEach(function() { + traceOut = {}; + }); + + it("should set visible to false when x and y are empty", function() { + traceIn = {}; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + + traceIn = { x: [], y: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + }); + + it("should set visible to false when x or y is empty", function() { + traceIn = { x: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + + traceIn = { x: [], y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + + traceIn = { y: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + + traceIn = { x: [1, 2, 3], y: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + }); + + it("should not set base, offset or width", function() { + traceIn = { y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.base).toBeUndefined(); + expect(traceOut.offset).toBeUndefined(); + expect(traceOut.width).toBeUndefined(); + }); + + it("should coerce a non-negative width", function() { + traceIn = { width: -1, y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.width).toBeUndefined(); + }); + + it("should coerce textposition to none", function() { + traceIn = { y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.textposition).toBe("none"); + expect(traceOut.texfont).toBeUndefined(); + expect(traceOut.insidetexfont).toBeUndefined(); + expect(traceOut.outsidetexfont).toBeUndefined(); + }); + + it("should default textfont to layout.font", function() { + traceIn = { textposition: "inside", y: [1, 2, 3] }; + + var layout = { font: { family: "arial", color: "#AAA", size: 13 } }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + + expect(traceOut.textposition).toBe("inside"); + expect(traceOut.textfont).toEqual(layout.font); + expect(traceOut.textfont).not.toBe(layout.font); + expect(traceOut.insidetextfont).toEqual(layout.font); + expect(traceOut.insidetextfont).not.toBe(layout.font); + expect(traceOut.insidetextfont).not.toBe(traceOut.textfont); + expect(traceOut.outsidetexfont).toBeUndefined(); + }); + + it("should inherit layout.calendar", function() { + traceIn = { x: [1, 2, 3], y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("islamic"); + expect(traceOut.ycalendar).toBe("islamic"); + }); + + it("should take its own calendars", function() { + traceIn = { + x: [1, 2, 3], + y: [1, 2, 3], + xcalendar: "coptic", + ycalendar: "ethiopian" + }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); - it('should take its own calendars', function() { - traceIn = { - x: [1, 2, 3], - y: [1, 2, 3], - xcalendar: 'coptic', - ycalendar: 'ethiopian' - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - expect(traceOut.xcalendar).toBe('coptic'); - expect(traceOut.ycalendar).toBe('ethiopian'); - }); + expect(traceOut.xcalendar).toBe("coptic"); + expect(traceOut.ycalendar).toBe("ethiopian"); + }); }); -describe('heatmap calc / setPositions', function() { - 'use strict'; - - beforeAll(function() { - jasmine.addMatchers(customMatchers); +describe("heatmap calc / setPositions", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + it("should fill in calc pt fields (stack case)", function() { + var gd = mockBarPlot( + [{ y: [2, 1, 2] }, { y: [3, 1, 2] }, { y: [null, null, 2] }], + { barmode: "stack" } + ); + + var cd = gd.calcdata; + assertPointField(cd, "x", [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertPointField(cd, "y", [ + [2, 1, 2], + [5, 2, 4], + [undefined, undefined, 6] + ]); + assertPointField(cd, "b", [[0, 0, 0], [2, 1, 2], [0, 0, 4]]); + assertPointField(cd, "s", [ + [2, 1, 2], + [3, 1, 2], + [undefined, undefined, 2] + ]); + assertPointField(cd, "p", [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertTraceField(cd, "t.barwidth", [0.8, 0.8, 0.8]); + assertTraceField(cd, "t.poffset", [-0.4, -0.4, -0.4]); + assertTraceField(cd, "t.bargroupwidth", [0.8, 0.8, 0.8]); + }); + + it("should fill in calc pt fields (overlay case)", function() { + var gd = mockBarPlot([{ y: [2, 1, 2] }, { y: [3, 1, 2] }], { + barmode: "overlay" }); - it('should fill in calc pt fields (stack case)', function() { - var gd = mockBarPlot([{ - y: [2, 1, 2] - }, { - y: [3, 1, 2] - }, { - y: [null, null, 2] - }], { - barmode: 'stack' - }); - - var cd = gd.calcdata; - assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); - assertPointField(cd, 'y', [[2, 1, 2], [5, 2, 4], [undefined, undefined, 6]]); - assertPointField(cd, 'b', [[0, 0, 0], [2, 1, 2], [0, 0, 4]]); - assertPointField(cd, 's', [[2, 1, 2], [3, 1, 2], [undefined, undefined, 2]]); - assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); - assertTraceField(cd, 't.barwidth', [0.8, 0.8, 0.8]); - assertTraceField(cd, 't.poffset', [-0.4, -0.4, -0.4]); - assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8]); + var cd = gd.calcdata; + assertPointField(cd, "x", [[0, 1, 2], [0, 1, 2]]); + assertPointField(cd, "y", [[2, 1, 2], [3, 1, 2]]); + assertPointField(cd, "b", [[0, 0, 0], [0, 0, 0]]); + assertPointField(cd, "s", [[2, 1, 2], [3, 1, 2]]); + assertPointField(cd, "p", [[0, 1, 2], [0, 1, 2]]); + assertTraceField(cd, "t.barwidth", [0.8, 0.8]); + assertTraceField(cd, "t.poffset", [-0.4, -0.4]); + assertTraceField(cd, "t.bargroupwidth", [0.8, 0.8]); + }); + + it("should fill in calc pt fields (group case)", function() { + var gd = mockBarPlot([{ y: [2, 1, 2] }, { y: [3, 1, 2] }], { + barmode: "group", + // asumming default bargap is 0.2 + bargroupgap: 0.1 }); - it('should fill in calc pt fields (overlay case)', function() { - var gd = mockBarPlot([{ - y: [2, 1, 2] - }, { - y: [3, 1, 2] - }], { - barmode: 'overlay' - }); - - var cd = gd.calcdata; - assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]); - assertPointField(cd, 'y', [[2, 1, 2], [3, 1, 2]]); - assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]); - assertPointField(cd, 's', [[2, 1, 2], [3, 1, 2]]); - assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2]]); - assertTraceField(cd, 't.barwidth', [0.8, 0.8]); - assertTraceField(cd, 't.poffset', [-0.4, -0.4]); - assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8]); - }); - - it('should fill in calc pt fields (group case)', function() { - var gd = mockBarPlot([{ - y: [2, 1, 2] - }, { - y: [3, 1, 2] - }], { - barmode: 'group', - // asumming default bargap is 0.2 - bargroupgap: 0.1 - }); - - var cd = gd.calcdata; - assertPointField(cd, 'x', [[-0.2, 0.8, 1.8], [0.2, 1.2, 2.2]]); - assertPointField(cd, 'y', [[2, 1, 2], [3, 1, 2]]); - assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]); - assertPointField(cd, 's', [[2, 1, 2], [3, 1, 2]]); - assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2]]); - assertTraceField(cd, 't.barwidth', [0.36, 0.36]); - assertTraceField(cd, 't.poffset', [-0.38, 0.02]); - assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8]); - }); - - it('should fill in calc pt fields (relative case)', function() { - var gd = mockBarPlot([{ - y: [20, 14, -23] - }, { - y: [-12, -18, -29] - }], { - barmode: 'relative' - }); - - var cd = gd.calcdata; - assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]); - assertPointField(cd, 'y', [[20, 14, -23], [-12, -18, -52]]); - assertPointField(cd, 'b', [[0, 0, 0], [0, 0, -23]]); - assertPointField(cd, 's', [[20, 14, -23], [-12, -18, -29]]); - assertPointField(cd, 'p', [[0, 1, 2], [0, 1, 2]]); - assertTraceField(cd, 't.barwidth', [0.8, 0.8]); - assertTraceField(cd, 't.poffset', [-0.4, -0.4]); - assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8]); + var cd = gd.calcdata; + assertPointField(cd, "x", [[-0.2, 0.8, 1.8], [0.2, 1.2, 2.2]]); + assertPointField(cd, "y", [[2, 1, 2], [3, 1, 2]]); + assertPointField(cd, "b", [[0, 0, 0], [0, 0, 0]]); + assertPointField(cd, "s", [[2, 1, 2], [3, 1, 2]]); + assertPointField(cd, "p", [[0, 1, 2], [0, 1, 2]]); + assertTraceField(cd, "t.barwidth", [0.36, 0.36]); + assertTraceField(cd, "t.poffset", [-0.38, 0.02]); + assertTraceField(cd, "t.bargroupwidth", [0.8, 0.8]); + }); + + it("should fill in calc pt fields (relative case)", function() { + var gd = mockBarPlot([{ y: [20, 14, -23] }, { y: [-12, -18, -29] }], { + barmode: "relative" }); - it('should fill in calc pt fields (relative / percent case)', function() { - var gd = mockBarPlot([{ - x: ['A', 'B', 'C', 'D'], - y: [20, 14, 40, -60] - }, { - x: ['A', 'B', 'C', 'D'], - y: [-12, -18, 60, -40] - }], { - barmode: 'relative', - barnorm: 'percent' - }); - - var cd = gd.calcdata; - assertPointField(cd, 'x', [[0, 1, 2, 3], [0, 1, 2, 3]]); - assertPointField(cd, 'y', [[100, 100, 40, -60], [-100, -100, 100, -100]]); - assertPointField(cd, 'b', [[0, 0, 0, 0], [0, 0, 40, -60]]); - assertPointField(cd, 's', [[100, 100, 40, -60], [-100, -100, 60, -40]]); - assertPointField(cd, 'p', [[0, 1, 2, 3], [0, 1, 2, 3]]); - assertTraceField(cd, 't.barwidth', [0.8, 0.8]); - assertTraceField(cd, 't.poffset', [-0.4, -0.4]); - assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8]); - }); + var cd = gd.calcdata; + assertPointField(cd, "x", [[0, 1, 2], [0, 1, 2]]); + assertPointField(cd, "y", [[20, 14, -23], [-12, -18, -52]]); + assertPointField(cd, "b", [[0, 0, 0], [0, 0, -23]]); + assertPointField(cd, "s", [[20, 14, -23], [-12, -18, -29]]); + assertPointField(cd, "p", [[0, 1, 2], [0, 1, 2]]); + assertTraceField(cd, "t.barwidth", [0.8, 0.8]); + assertTraceField(cd, "t.poffset", [-0.4, -0.4]); + assertTraceField(cd, "t.bargroupwidth", [0.8, 0.8]); + }); + + it("should fill in calc pt fields (relative / percent case)", function() { + var gd = mockBarPlot( + [ + { x: ["A", "B", "C", "D"], y: [20, 14, 40, -60] }, + { x: ["A", "B", "C", "D"], y: [-12, -18, 60, -40] } + ], + { barmode: "relative", barnorm: "percent" } + ); + + var cd = gd.calcdata; + assertPointField(cd, "x", [[0, 1, 2, 3], [0, 1, 2, 3]]); + assertPointField(cd, "y", [[100, 100, 40, -60], [-100, -100, 100, -100]]); + assertPointField(cd, "b", [[0, 0, 0, 0], [0, 0, 40, -60]]); + assertPointField(cd, "s", [[100, 100, 40, -60], [-100, -100, 60, -40]]); + assertPointField(cd, "p", [[0, 1, 2, 3], [0, 1, 2, 3]]); + assertTraceField(cd, "t.barwidth", [0.8, 0.8]); + assertTraceField(cd, "t.poffset", [-0.4, -0.4]); + assertTraceField(cd, "t.bargroupwidth", [0.8, 0.8]); + }); }); -describe('Bar.calc', function() { - 'use strict'; - - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - it('should guard against invalid base items', function() { - var gd = mockBarPlot([{ - base: [null, 1, 2], - y: [1, 2, 3] - }, { - base: [null, 1], - y: [1, 2, 3] - }, { - base: null, - y: [1, 2] - }], { - barmode: 'overlay' - }); - - var cd = gd.calcdata; - assertPointField(cd, 'b', [[0, 1, 2], [0, 1, 0], [0, 0]]); - }); +describe("Bar.calc", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + it("should guard against invalid base items", function() { + var gd = mockBarPlot( + [ + { base: [null, 1, 2], y: [1, 2, 3] }, + { base: [null, 1], y: [1, 2, 3] }, + { base: null, y: [1, 2] } + ], + { barmode: "overlay" } + ); + + var cd = gd.calcdata; + assertPointField(cd, "b", [[0, 1, 2], [0, 1, 0], [0, 0]]); + }); }); -describe('Bar.setPositions', function() { - 'use strict'; - - beforeAll(function() { - jasmine.addMatchers(customMatchers); +describe("Bar.setPositions", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + it("should guard against invalid offset items", function() { + var gd = mockBarPlot( + [ + { offset: [null, 0, 1], y: [1, 2, 3] }, + { offset: [null, 1], y: [1, 2, 3] }, + { offset: null, y: [1] } + ], + { bargap: 0.2, barmode: "overlay" } + ); + + var cd = gd.calcdata; + assertArrayField(cd[0][0], "t.poffset", [-0.4, 0, 1]); + assertArrayField(cd[1][0], "t.poffset", [-0.4, 1, -0.4]); + assertArrayField(cd[2][0], "t.poffset", [-0.4]); + }); + + it("should guard against invalid width items", function() { + var gd = mockBarPlot( + [ + { width: [null, 1, 0.8], y: [1, 2, 3] }, + { width: [null, 1], y: [1, 2, 3] }, + { width: null, y: [1] } + ], + { bargap: 0.2, barmode: "overlay" } + ); + + var cd = gd.calcdata; + assertArrayField(cd[0][0], "t.barwidth", [0.8, 1, 0.8]); + assertArrayField(cd[1][0], "t.barwidth", [0.8, 1, 0.8]); + assertArrayField(cd[2][0], "t.barwidth", [0.8]); + }); + + it("should guard against invalid width items (group case)", function() { + var gd = mockBarPlot( + [ + { width: [null, 0.1, 0.2], y: [1, 2, 3] }, + { width: [null, 0.1], y: [1, 2, 3] }, + { width: null, y: [1] } + ], + { bargap: 0, barmode: "group" } + ); + + var cd = gd.calcdata; + assertArrayField(cd[0][0], "t.barwidth", [0.33, 0.1, 0.2]); + assertArrayField(cd[1][0], "t.barwidth", [0.33, 0.1, 0.33]); + assertArrayField(cd[2][0], "t.barwidth", [0.33]); + }); + + it("should stack vertical and horizontal traces separately", function() { + var gd = mockBarPlot( + [ + { y: [1, 2, 3] }, + { y: [10, 20, 30] }, + { x: [-1, -2, -3] }, + { x: [-10, -20, -30] } + ], + { barmode: "stack" } + ); + + var cd = gd.calcdata; + assertPointField(cd, "b", [[0, 0, 0], [1, 2, 3], [0, 0, 0], [-1, -2, -3]]); + assertPointField(cd, "s", [ + [1, 2, 3], + [10, 20, 30], + [-1, -2, -3], + [-10, -20, -30] + ]); + assertPointField(cd, "x", [ + [0, 1, 2], + [0, 1, 2], + [-1, -2, -3], + [-11, -22, -33] + ]); + assertPointField(cd, "y", [[1, 2, 3], [11, 22, 33], [0, 1, 2], [0, 1, 2]]); + }); + + it("should not group traces that set offset", function() { + var gd = mockBarPlot( + [{ y: [1, 2, 3] }, { y: [10, 20, 30] }, { offset: -1, y: [-1, -2, -3] }], + { bargap: 0, barmode: "group" } + ); + + var cd = gd.calcdata; + assertPointField(cd, "b", [[0, 0, 0], [0, 0, 0], [0, 0, 0]]); + assertPointField(cd, "s", [[1, 2, 3], [10, 20, 30], [-1, -2, -3]]); + assertPointField(cd, "x", [ + [-0.25, 0.75, 1.75], + [0.25, 1.25, 2.25], + [-0.5, 0.5, 1.5] + ]); + assertPointField(cd, "y", [[1, 2, 3], [10, 20, 30], [-1, -2, -3]]); + }); + + it("should not stack traces that set base", function() { + var gd = mockBarPlot( + [{ y: [1, 2, 3] }, { y: [10, 20, 30] }, { base: -1, y: [-1, -2, -3] }], + { bargap: 0, barmode: "stack" } + ); + + var cd = gd.calcdata; + assertPointField(cd, "b", [[0, 0, 0], [1, 2, 3], [-1, -1, -1]]); + assertPointField(cd, "s", [[1, 2, 3], [10, 20, 30], [-1, -2, -3]]); + assertPointField(cd, "x", [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertPointField(cd, "y", [[1, 2, 3], [11, 22, 33], [-2, -3, -4]]); + }); + + it("should draw traces separately in overlay mode", function() { + var gd = mockBarPlot([{ y: [1, 2, 3] }, { y: [10, 20, 30] }], { + bargap: 0, + barmode: "overlay", + barnorm: false }); - it('should guard against invalid offset items', function() { - var gd = mockBarPlot([{ - offset: [null, 0, 1], - y: [1, 2, 3] - }, { - offset: [null, 1], - y: [1, 2, 3] - }, { - offset: null, - y: [1] - }], { - bargap: 0.2, - barmode: 'overlay' - }); - - var cd = gd.calcdata; - assertArrayField(cd[0][0], 't.poffset', [-0.4, 0, 1]); - assertArrayField(cd[1][0], 't.poffset', [-0.4, 1, -0.4]); - assertArrayField(cd[2][0], 't.poffset', [-0.4]); + var cd = gd.calcdata; + assertPointField(cd, "b", [[0, 0, 0], [0, 0, 0]]); + assertPointField(cd, "s", [[1, 2, 3], [10, 20, 30]]); + assertPointField(cd, "x", [[0, 1, 2], [0, 1, 2]]); + assertPointField(cd, "y", [[1, 2, 3], [10, 20, 30]]); + }); + + it("should ignore barnorm in overlay mode", function() { + var gd = mockBarPlot([{ y: [1, 2, 3] }, { y: [10, 20, 30] }], { + bargap: 0, + barmode: "overlay", + barnorm: "percent" }); - it('should guard against invalid width items', function() { - var gd = mockBarPlot([{ - width: [null, 1, 0.8], - y: [1, 2, 3] - }, { - width: [null, 1], - y: [1, 2, 3] - }, { - width: null, - y: [1] - }], { - bargap: 0.2, - barmode: 'overlay' - }); - - var cd = gd.calcdata; - assertArrayField(cd[0][0], 't.barwidth', [0.8, 1, 0.8]); - assertArrayField(cd[1][0], 't.barwidth', [0.8, 1, 0.8]); - assertArrayField(cd[2][0], 't.barwidth', [0.8]); - }); + expect(gd._fullLayout.barnorm).toBeUndefined(); - it('should guard against invalid width items (group case)', function() { - var gd = mockBarPlot([{ - width: [null, 0.1, 0.2], - y: [1, 2, 3] - }, { - width: [null, 0.1], - y: [1, 2, 3] - }, { - width: null, - y: [1] - }], { - bargap: 0, - barmode: 'group' - }); + var cd = gd.calcdata; + assertPointField(cd, "b", [[0, 0, 0], [0, 0, 0]]); + assertPointField(cd, "s", [[1, 2, 3], [10, 20, 30]]); + assertPointField(cd, "x", [[0, 1, 2], [0, 1, 2]]); + assertPointField(cd, "y", [[1, 2, 3], [10, 20, 30]]); + }); - var cd = gd.calcdata; - assertArrayField(cd[0][0], 't.barwidth', [0.33, 0.1, 0.2]); - assertArrayField(cd[1][0], 't.barwidth', [0.33, 0.1, 0.33]); - assertArrayField(cd[2][0], 't.barwidth', [0.33]); + it("should honor barnorm for traces that cannot be grouped", function() { + var gd = mockBarPlot([{ offset: 0, y: [1, 2, 3] }], { + bargap: 0, + barmode: "group", + barnorm: "percent" }); - it('should stack vertical and horizontal traces separately', function() { - var gd = mockBarPlot([{ - y: [1, 2, 3] - }, { - y: [10, 20, 30] - }, { - x: [-1, -2, -3] - }, { - x: [-10, -20, -30] - }], { - barmode: 'stack' - }); - - var cd = gd.calcdata; - assertPointField(cd, 'b', [[0, 0, 0], [1, 2, 3], [0, 0, 0], [-1, -2, -3]]); - assertPointField(cd, 's', [[1, 2, 3], [10, 20, 30], [-1, -2, -3], [-10, -20, -30]]); - assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2], [-1, -2, -3], [-11, -22, -33]]); - assertPointField(cd, 'y', [[1, 2, 3], [11, 22, 33], [0, 1, 2], [0, 1, 2]]); - }); + expect(gd._fullLayout.barnorm).toBe("percent"); - it('should not group traces that set offset', function() { - var gd = mockBarPlot([{ - y: [1, 2, 3] - }, { - y: [10, 20, 30] - }, { - offset: -1, - y: [-1, -2, -3] - }], { - bargap: 0, - barmode: 'group' - }); + var cd = gd.calcdata; + assertPointField(cd, "b", [[0, 0, 0]]); + assertPointField(cd, "s", [[100, 100, 100]]); + assertPointField(cd, "x", [[0.5, 1.5, 2.5]]); + assertPointField(cd, "y", [[100, 100, 100]]); + }); - var cd = gd.calcdata; - assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0], [0, 0, 0]]); - assertPointField(cd, 's', [[1, 2, 3], [10, 20, 30], [-1, -2, -3]]); - assertPointField(cd, 'x', [[-0.25, 0.75, 1.75], [0.25, 1.25, 2.25], [-0.5, 0.5, 1.5]]); - assertPointField(cd, 'y', [[1, 2, 3], [10, 20, 30], [-1, -2, -3]]); + it("should honor barnorm for traces that cannot be stacked", function() { + var gd = mockBarPlot([{ offset: 0, y: [1, 2, 3] }], { + bargap: 0, + barmode: "stack", + barnorm: "percent" }); - it('should not stack traces that set base', function() { - var gd = mockBarPlot([{ - y: [1, 2, 3] - }, { - y: [10, 20, 30] - }, { - base: -1, - y: [-1, -2, -3] - }], { - bargap: 0, - barmode: 'stack' - }); + expect(gd._fullLayout.barnorm).toBe("percent"); - var cd = gd.calcdata; - assertPointField(cd, 'b', [[0, 0, 0], [1, 2, 3], [-1, -1, -1]]); - assertPointField(cd, 's', [[1, 2, 3], [10, 20, 30], [-1, -2, -3]]); - assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2], [0, 1, 2]]); - assertPointField(cd, 'y', [[1, 2, 3], [11, 22, 33], [-2, -3, -4]]); - }); + var cd = gd.calcdata; + assertPointField(cd, "b", [[0, 0, 0]]); + assertPointField(cd, "s", [[100, 100, 100]]); + assertPointField(cd, "x", [[0.5, 1.5, 2.5]]); + assertPointField(cd, "y", [[100, 100, 100]]); + }); - it('should draw traces separately in overlay mode', function() { - var gd = mockBarPlot([{ - y: [1, 2, 3] - }, { - y: [10, 20, 30] - }], { - bargap: 0, - barmode: 'overlay', - barnorm: false - }); - - var cd = gd.calcdata; - assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]); - assertPointField(cd, 's', [[1, 2, 3], [10, 20, 30]]); - assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]); - assertPointField(cd, 'y', [[1, 2, 3], [10, 20, 30]]); + it("should honor barnorm (group case)", function() { + var gd = mockBarPlot([{ y: [3, 2, 1] }, { y: [1, 2, 3] }], { + bargap: 0, + barmode: "group", + barnorm: "fraction" }); - it('should ignore barnorm in overlay mode', function() { - var gd = mockBarPlot([{ - y: [1, 2, 3] - }, { - y: [10, 20, 30] - }], { - bargap: 0, - barmode: 'overlay', - barnorm: 'percent' - }); - - expect(gd._fullLayout.barnorm).toBeUndefined(); - - var cd = gd.calcdata; - assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]); - assertPointField(cd, 's', [[1, 2, 3], [10, 20, 30]]); - assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]); - assertPointField(cd, 'y', [[1, 2, 3], [10, 20, 30]]); + expect(gd._fullLayout.barnorm).toBe("fraction"); + + var cd = gd.calcdata; + assertPointField(cd, "b", [[0, 0, 0], [0, 0, 0]]); + assertPointField(cd, "s", [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); + assertPointField(cd, "x", [[-0.25, 0.75, 1.75], [0.25, 1.25, 2.25]]); + assertPointField(cd, "y", [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); + }); + + it("should honor barnorm (group+base case)", function() { + var gd = mockBarPlot( + [{ base: [3, 2, 1], y: [0, 0, 0] }, { y: [1, 2, 3] }], + { bargap: 0, barmode: "group", barnorm: "fraction" } + ); + + expect(gd._fullLayout.barnorm).toBe("fraction"); + + var cd = gd.calcdata; + assertPointField(cd, "b", [[0.75, 0.50, 0.25], [0, 0, 0]]); + assertPointField(cd, "s", [[0, 0, 0], [0.25, 0.50, 0.75]]); + assertPointField(cd, "x", [[-0.25, 0.75, 1.75], [0.25, 1.25, 2.25]]); + assertPointField(cd, "y", [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); + }); + + it("should honor barnorm (stack case)", function() { + var gd = mockBarPlot([{ y: [3, 2, 1] }, { y: [1, 2, 3] }], { + bargap: 0, + barmode: "stack", + barnorm: "fraction" }); - it('should honor barnorm for traces that cannot be grouped', function() { - var gd = mockBarPlot([{ - offset: 0, - y: [1, 2, 3] - }], { - bargap: 0, - barmode: 'group', - barnorm: 'percent' - }); - - expect(gd._fullLayout.barnorm).toBe('percent'); + expect(gd._fullLayout.barnorm).toBe("fraction"); + + var cd = gd.calcdata; + assertPointField(cd, "b", [[0, 0, 0], [0.75, 0.50, 0.25]]); + assertPointField(cd, "s", [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); + assertPointField(cd, "x", [[0, 1, 2], [0, 1, 2]]); + assertPointField(cd, "y", [[0.75, 0.50, 0.25], [1, 1, 1]]); + }); + + it("should honor barnorm (relative case)", function() { + var gd = mockBarPlot( + [ + { y: [3, 2, 1] }, + { y: [1, 2, 3] }, + { y: [-3, -2, -1] }, + { y: [-1, -2, -3] } + ], + { bargap: 0, barmode: "relative", barnorm: "fraction" } + ); + + expect(gd._fullLayout.barnorm).toBe("fraction"); + + var cd = gd.calcdata; + assertPointField(cd, "b", [ + [0, 0, 0], + [0.75, 0.50, 0.25], + [0, 0, 0], + [-0.75, -0.50, -0.25] + ]); + assertPointField(cd, "s", [ + [0.75, 0.50, 0.25], + [0.25, 0.50, 0.75], + [-0.75, -0.50, -0.25], + [-0.25, -0.50, -0.75] + ]); + assertPointField(cd, "x", [[0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2]]); + assertPointField(cd, "y", [ + [0.75, 0.50, 0.25], + [1, 1, 1], + [-0.75, -0.50, -0.25], + [-1, -1, -1] + ]); + }); + + it("should expand position axis", function() { + var gd = mockBarPlot( + [ + { offset: 10, width: 2, y: [3, 2, 1] }, + { offset: -5, width: 2, y: [-1, -2, -3] } + ], + { bargap: 0, barmode: "overlay", barnorm: false } + ); + + expect(gd._fullLayout.barnorm).toBeUndefined(); + + var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; + expect(Axes.getAutoRange(xa)).toBeCloseToArray( + [-5, 14], + undefined, + "(xa.range)" + ); + expect(Axes.getAutoRange(ya)).toBeCloseToArray( + [-3.33, 3.33], + undefined, + "(ya.range)" + ); + }); + + it("should expand size axis (overlay case)", function() { + var gd = mockBarPlot( + [ + { base: 7, y: [3, 2, 1] }, + { base: 2, y: [1, 2, 3] }, + { base: -2, y: [-3, -2, -1] }, + { base: -7, y: [-1, -2, -3] } + ], + { bargap: 0, barmode: "overlay", barnorm: false } + ); + + expect(gd._fullLayout.barnorm).toBeUndefined(); + + var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; + expect(Axes.getAutoRange(xa)).toBeCloseToArray( + [-0.5, 2.5], + undefined, + "(xa.range)" + ); + expect(Axes.getAutoRange(ya)).toBeCloseToArray( + [-11.11, 11.11], + undefined, + "(ya.range)" + ); + }); + + it("should expand size axis (relative case)", function() { + var gd = mockBarPlot( + [ + { y: [3, 2, 1] }, + { y: [1, 2, 3] }, + { y: [-3, -2, -1] }, + { y: [-1, -2, -3] } + ], + { bargap: 0, barmode: "relative", barnorm: false } + ); + + expect(gd._fullLayout.barnorm).toBe(""); + + var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; + expect(Axes.getAutoRange(xa)).toBeCloseToArray( + [-0.5, 2.5], + undefined, + "(xa.range)" + ); + expect(Axes.getAutoRange(ya)).toBeCloseToArray( + [-4.44, 4.44], + undefined, + "(ya.range)" + ); + }); + + it("should expand size axis (barnorm case)", function() { + var gd = mockBarPlot( + [ + { y: [3, 2, 1] }, + { y: [1, 2, 3] }, + { y: [-3, -2, -1] }, + { y: [-1, -2, -3] } + ], + { bargap: 0, barmode: "relative", barnorm: "fraction" } + ); + + expect(gd._fullLayout.barnorm).toBe("fraction"); + + var xa = gd._fullLayout.xaxis, ya = gd._fullLayout.yaxis; + expect(Axes.getAutoRange(xa)).toBeCloseToArray( + [-0.5, 2.5], + undefined, + "(xa.range)" + ); + expect(Axes.getAutoRange(ya)).toBeCloseToArray( + [-1.11, 1.11], + undefined, + "(ya.range)" + ); + }); + + it("should skip placeholder trace in position computations", function() { + var gd = mockBarPlot([ + { x: [1, 2, 3], y: [2, 1, 2] }, + { x: [null], y: [null] } + ]); + + expect(gd.calcdata[0][0].t.barwidth).toEqual(0.8); + + expect(gd.calcdata[1][0].x).toBe(false); + expect(gd.calcdata[1][0].y).toBe(false); + expect(gd.calcdata[1][0].placeholder).toBe(true); + expect(gd.calcdata[1][0].t.barwidth).toBeUndefined(); + }); +}); - var cd = gd.calcdata; - assertPointField(cd, 'b', [[0, 0, 0]]); - assertPointField(cd, 's', [[100, 100, 100]]); - assertPointField(cd, 'x', [[0.5, 1.5, 2.5]]); - assertPointField(cd, 'y', [[100, 100, 100]]); +describe("A bar plot", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + afterEach(destroyGraphDiv); + + function getAllTraceNodes(node) { + return node.querySelectorAll("g.points"); + } + + function getAllBarNodes(node) { + return node.querySelectorAll("g.point"); + } + + function assertTextIsInsidePath(textNode, pathNode) { + var textBB = textNode.getBoundingClientRect(), + pathBB = pathNode.getBoundingClientRect(); + + expect(pathBB.left).not.toBeGreaterThan(textBB.left); + expect(textBB.right).not.toBeGreaterThan(pathBB.right); + expect(pathBB.top).not.toBeGreaterThan(textBB.top); + expect(textBB.bottom).not.toBeGreaterThan(pathBB.bottom); + } + + function assertTextIsAbovePath(textNode, pathNode) { + var textBB = textNode.getBoundingClientRect(), + pathBB = pathNode.getBoundingClientRect(); + + expect(textBB.bottom).not.toBeGreaterThan(pathBB.top); + } + + function assertTextIsBelowPath(textNode, pathNode) { + var textBB = textNode.getBoundingClientRect(), + pathBB = pathNode.getBoundingClientRect(); + + expect(pathBB.bottom).not.toBeGreaterThan(textBB.top); + } + + function assertTextIsAfterPath(textNode, pathNode) { + var textBB = textNode.getBoundingClientRect(), + pathBB = pathNode.getBoundingClientRect(); + + expect(pathBB.right).not.toBeGreaterThan(textBB.left); + } + + var colorMap = { + "rgb(0, 0, 0)": "black", + "rgb(255, 0, 0)": "red", + "rgb(0, 128, 0)": "green", + "rgb(0, 0, 255)": "blue" + }; + function assertTextFont(textNode, textFont, index) { + expect(textNode.style.fontFamily).toBe(textFont.family[index]); + expect(textNode.style.fontSize).toBe(textFont.size[index] + "px"); + + var color = textNode.style.fill; + if (!colorMap[color]) colorMap[color] = color; + expect(colorMap[color]).toBe(textFont.color[index]); + } + + function assertTextIsBeforePath(textNode, pathNode) { + var textBB = textNode.getBoundingClientRect(), + pathBB = pathNode.getBoundingClientRect(); + + expect(textBB.right).not.toBeGreaterThan(pathBB.left); + } + + it("should show bar texts (inside case)", function(done) { + var gd = createGraphDiv(), + data = [ + { + y: [10, 20, 30], + type: "bar", + text: ["1", "Very very very very very long bar text"], + textposition: "inside" + } + ], + layout = {}; + + Plotly.plot(gd, data, layout).then(function() { + var traceNodes = getAllTraceNodes(gd), + barNodes = getAllBarNodes(traceNodes[0]), + foundTextNodes; + + for (var i = 0; i < barNodes.length; i++) { + var barNode = barNodes[i], + pathNode = barNode.querySelector("path"), + textNode = barNode.querySelector("text"); + if (textNode) { + foundTextNodes = true; + assertTextIsInsidePath(textNode, pathNode); + } + } + + expect(foundTextNodes).toBe(true); + + done(); }); - - it('should honor barnorm for traces that cannot be stacked', function() { - var gd = mockBarPlot([{ - offset: 0, - y: [1, 2, 3] - }], { - bargap: 0, - barmode: 'stack', - barnorm: 'percent' - }); - - expect(gd._fullLayout.barnorm).toBe('percent'); - - var cd = gd.calcdata; - assertPointField(cd, 'b', [[0, 0, 0]]); - assertPointField(cd, 's', [[100, 100, 100]]); - assertPointField(cd, 'x', [[0.5, 1.5, 2.5]]); - assertPointField(cd, 'y', [[100, 100, 100]]); + }); + + it("should show bar texts (outside case)", function(done) { + var gd = createGraphDiv(), + data = [ + { + y: [10, -20, 30], + type: "bar", + text: ["1", "Very very very very very long bar text"], + textposition: "outside" + } + ], + layout = { barmode: "relative" }; + + Plotly.plot(gd, data, layout).then(function() { + var traceNodes = getAllTraceNodes(gd), + barNodes = getAllBarNodes(traceNodes[0]), + foundTextNodes; + + for (var i = 0; i < barNodes.length; i++) { + var barNode = barNodes[i], + pathNode = barNode.querySelector("path"), + textNode = barNode.querySelector("text"); + if (textNode) { + foundTextNodes = true; + if (data[0].y[i] > 0) assertTextIsAbovePath(textNode, pathNode); + else assertTextIsBelowPath(textNode, pathNode); + } + } + + expect(foundTextNodes).toBe(true); + + done(); }); - - it('should honor barnorm (group case)', function() { - var gd = mockBarPlot([{ - y: [3, 2, 1] - }, { - y: [1, 2, 3] - }], { - bargap: 0, - barmode: 'group', - barnorm: 'fraction' - }); - - expect(gd._fullLayout.barnorm).toBe('fraction'); - - var cd = gd.calcdata; - assertPointField(cd, 'b', [[0, 0, 0], [0, 0, 0]]); - assertPointField(cd, 's', [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); - assertPointField(cd, 'x', [[-0.25, 0.75, 1.75], [0.25, 1.25, 2.25]]); - assertPointField(cd, 'y', [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); + }); + + it("should show bar texts (horizontal case)", function(done) { + var gd = createGraphDiv(), + data = [ + { + x: [10, -20, 30], + type: "bar", + text: ["Very very very very very long bar text", -20], + textposition: "outside" + } + ], + layout = {}; + + Plotly.plot(gd, data, layout).then(function() { + var traceNodes = getAllTraceNodes(gd), + barNodes = getAllBarNodes(traceNodes[0]), + foundTextNodes; + + for (var i = 0; i < barNodes.length; i++) { + var barNode = barNodes[i], + pathNode = barNode.querySelector("path"), + textNode = barNode.querySelector("text"); + if (textNode) { + foundTextNodes = true; + if (data[0].x[i] > 0) assertTextIsAfterPath(textNode, pathNode); + else assertTextIsBeforePath(textNode, pathNode); + } + } + + expect(foundTextNodes).toBe(true); + + done(); }); - - it('should honor barnorm (group+base case)', function() { - var gd = mockBarPlot([{ - base: [3, 2, 1], - y: [0, 0, 0] - }, { - y: [1, 2, 3] - }], { - bargap: 0, - barmode: 'group', - barnorm: 'fraction' - }); - - expect(gd._fullLayout.barnorm).toBe('fraction'); - - var cd = gd.calcdata; - assertPointField(cd, 'b', [[0.75, 0.50, 0.25], [0, 0, 0]]); - assertPointField(cd, 's', [[0, 0, 0], [0.25, 0.50, 0.75]]); - assertPointField(cd, 'x', [[-0.25, 0.75, 1.75], [0.25, 1.25, 2.25]]); - assertPointField(cd, 'y', [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); + }); + + it("should show bar texts (barnorm case)", function(done) { + var gd = createGraphDiv(), + data = [ + { + x: [100, -100, 100], + type: "bar", + text: [100, -100, 100], + textposition: "outside" + } + ], + layout = { barmode: "relative", barnorm: "percent" }; + + Plotly.plot(gd, data, layout).then(function() { + var traceNodes = getAllTraceNodes(gd), + barNodes = getAllBarNodes(traceNodes[0]), + foundTextNodes; + + for (var i = 0; i < barNodes.length; i++) { + var barNode = barNodes[i], + pathNode = barNode.querySelector("path"), + textNode = barNode.querySelector("text"); + if (textNode) { + foundTextNodes = true; + if (data[0].x[i] > 0) assertTextIsAfterPath(textNode, pathNode); + else assertTextIsBeforePath(textNode, pathNode); + } + } + + expect(foundTextNodes).toBe(true); + + done(); }); + }); - it('should honor barnorm (stack case)', function() { - var gd = mockBarPlot([{ - y: [3, 2, 1] - }, { - y: [1, 2, 3] - }], { - bargap: 0, - barmode: 'stack', - barnorm: 'fraction' - }); - - expect(gd._fullLayout.barnorm).toBe('fraction'); + it("should be able to restyle", function(done) { + var gd = createGraphDiv(), + mock = Lib.extendDeep({}, require("@mocks/bar_attrs_relative")); + Plotly.plot(gd, mock.data, mock.layout) + .then(function() { var cd = gd.calcdata; - assertPointField(cd, 'b', [[0, 0, 0], [0.75, 0.50, 0.25]]); - assertPointField(cd, 's', [[0.75, 0.50, 0.25], [0.25, 0.50, 0.75]]); - assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2]]); - assertPointField(cd, 'y', [[0.75, 0.50, 0.25], [1, 1, 1]]); - }); - - it('should honor barnorm (relative case)', function() { - var gd = mockBarPlot([{ - y: [3, 2, 1] - }, { - y: [1, 2, 3] - }, { - y: [-3, -2, -1] - }, { - y: [-1, -2, -3] - }], { - bargap: 0, - barmode: 'relative', - barnorm: 'fraction' - }); - - expect(gd._fullLayout.barnorm).toBe('fraction'); - + assertPointField(cd, "x", [ + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3, 4] + ]); + assertPointField(cd, "y", [ + [1, 2, 3, 4], + [4, 4, 4, 4], + [-1, -3, -2, -4], + [4, -3.25, -5, -6] + ]); + assertPointField(cd, "b", [ + [0, 0, 0, 0], + [1, 2, 3, 4], + [0, 0, 0, 0], + [4, -3, -2, -4] + ]); + assertPointField(cd, "s", [ + [1, 2, 3, 4], + [3, 2, 1, 0], + [-1, -3, -2, -4], + [0, -0.25, -3, -2] + ]); + assertPointField(cd, "p", [ + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3, 4] + ]); + assertArrayField(cd[0][0], "t.barwidth", [1, 0.8, 0.6, 0.4]); + assertArrayField(cd[1][0], "t.barwidth", [0.4, 0.6, 0.8, 1]); + expect(cd[2][0].t.barwidth).toBe(1); + expect(cd[3][0].t.barwidth).toBe(0.8); + assertArrayField(cd[0][0], "t.poffset", [-0.5, -0.4, -0.3, -0.2]); + assertArrayField(cd[1][0], "t.poffset", [-0.2, -0.3, -0.4, -0.5]); + expect(cd[2][0].t.poffset).toBe(-0.5); + expect(cd[3][0].t.poffset).toBe(-0.4); + assertTraceField(cd, "t.bargroupwidth", [0.8, 0.8, 0.8, 0.8]); + + return Plotly.restyle(gd, "offset", 0); + }) + .then(function() { var cd = gd.calcdata; - assertPointField(cd, 'b', [ - [0, 0, 0], [0.75, 0.50, 0.25], - [0, 0, 0], [-0.75, -0.50, -0.25] + assertPointField(cd, "x", [ + [1.5, 2.4, 3.3, 4.2], + [1.2, 2.3, 3.4, 4.5], + [1.5, 2.5, 3.5, 4.5], + [1.4, 2.4, 3.4, 4.4] ]); - assertPointField(cd, 's', [ - [0.75, 0.50, 0.25], [0.25, 0.50, 0.75], - [-0.75, -0.50, -0.25], [-0.25, -0.50, -0.75], + assertPointField(cd, "y", [ + [1, 2, 3, 4], + [4, 4, 4, 4], + [-1, -3, -2, -4], + [4, -3.25, -5, -6] ]); - assertPointField(cd, 'x', [[0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2]]); - assertPointField(cd, 'y', [ - [0.75, 0.50, 0.25], [1, 1, 1], - [-0.75, -0.50, -0.25], [-1, -1, -1], + assertPointField(cd, "b", [ + [0, 0, 0, 0], + [1, 2, 3, 4], + [0, 0, 0, 0], + [4, -3, -2, -4] ]); - }); - - it('should expand position axis', function() { - var gd = mockBarPlot([{ - offset: 10, - width: 2, - y: [3, 2, 1] - }, { - offset: -5, - width: 2, - y: [-1, -2, -3] - }], { - bargap: 0, - barmode: 'overlay', - barnorm: false - }); - - expect(gd._fullLayout.barnorm).toBeUndefined(); - - var xa = gd._fullLayout.xaxis, - ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-5, 14], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-3.33, 3.33], undefined, '(ya.range)'); - }); - - it('should expand size axis (overlay case)', function() { - var gd = mockBarPlot([{ - base: 7, - y: [3, 2, 1] - }, { - base: 2, - y: [1, 2, 3] - }, { - base: -2, - y: [-3, -2, -1] - }, { - base: -7, - y: [-1, -2, -3] - }], { - bargap: 0, - barmode: 'overlay', - barnorm: false - }); - - expect(gd._fullLayout.barnorm).toBeUndefined(); - - var xa = gd._fullLayout.xaxis, - ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-11.11, 11.11], undefined, '(ya.range)'); - }); - - it('should expand size axis (relative case)', function() { - var gd = mockBarPlot([{ - y: [3, 2, 1] - }, { - y: [1, 2, 3] - }, { - y: [-3, -2, -1] - }, { - y: [-1, -2, -3] - }], { - bargap: 0, - barmode: 'relative', - barnorm: false - }); - - expect(gd._fullLayout.barnorm).toBe(''); - - var xa = gd._fullLayout.xaxis, - ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-4.44, 4.44], undefined, '(ya.range)'); - }); - - it('should expand size axis (barnorm case)', function() { - var gd = mockBarPlot([{ - y: [3, 2, 1] - }, { - y: [1, 2, 3] - }, { - y: [-3, -2, -1] - }, { - y: [-1, -2, -3] - }], { - bargap: 0, - barmode: 'relative', - barnorm: 'fraction' - }); - - expect(gd._fullLayout.barnorm).toBe('fraction'); - - var xa = gd._fullLayout.xaxis, - ya = gd._fullLayout.yaxis; - expect(Axes.getAutoRange(xa)).toBeCloseToArray([-0.5, 2.5], undefined, '(xa.range)'); - expect(Axes.getAutoRange(ya)).toBeCloseToArray([-1.11, 1.11], undefined, '(ya.range)'); - }); + assertPointField(cd, "s", [ + [1, 2, 3, 4], + [3, 2, 1, 0], + [-1, -3, -2, -4], + [0, -0.25, -3, -2] + ]); + assertPointField(cd, "p", [ + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3, 4] + ]); + assertArrayField(cd[0][0], "t.barwidth", [1, 0.8, 0.6, 0.4]); + assertArrayField(cd[1][0], "t.barwidth", [0.4, 0.6, 0.8, 1]); + expect(cd[2][0].t.barwidth).toBe(1); + expect(cd[3][0].t.barwidth).toBe(0.8); + expect(cd[0][0].t.poffset).toBe(0); + expect(cd[1][0].t.poffset).toBe(0); + expect(cd[2][0].t.poffset).toBe(0); + expect(cd[3][0].t.poffset).toBe(0); + assertTraceField(cd, "t.bargroupwidth", [0.8, 0.8, 0.8, 0.8]); + + var traceNodes = getAllTraceNodes(gd), + trace0Bar3 = getAllBarNodes(traceNodes[0])[3], + path03 = trace0Bar3.querySelector("path"), + text03 = trace0Bar3.querySelector("text"), + trace1Bar2 = getAllBarNodes(traceNodes[1])[2], + path12 = trace1Bar2.querySelector("path"), + text12 = trace1Bar2.querySelector("text"), + trace2Bar0 = getAllBarNodes(traceNodes[2])[0], + path20 = trace2Bar0.querySelector("path"), + text20 = trace2Bar0.querySelector("text"), + trace3Bar0 = getAllBarNodes(traceNodes[3])[0], + path30 = trace3Bar0.querySelector("path"), + text30 = trace3Bar0.querySelector("text"); + + expect(text03.textContent).toBe("4"); + expect(text12.textContent).toBe("inside text"); + expect(text20.textContent).toBe("-1"); + expect(text30.textContent).toBe("outside text"); + + assertTextIsAbovePath(text03, path03); + // outside + assertTextIsInsidePath(text12, path12); + // inside + assertTextIsInsidePath(text20, path20); + // inside + assertTextIsBelowPath(text30, path30); + + // outside + return Plotly.restyle(gd, "textposition", "inside"); + }) + .then(function() { + var cd = gd.calcdata; + assertPointField(cd, "x", [ + [1.5, 2.4, 3.3, 4.2], + [1.2, 2.3, 3.4, 4.5], + [1.5, 2.5, 3.5, 4.5], + [1.4, 2.4, 3.4, 4.4] + ]); + assertPointField(cd, "y", [ + [1, 2, 3, 4], + [4, 4, 4, 4], + [-1, -3, -2, -4], + [4, -3.25, -5, -6] + ]); + assertPointField(cd, "b", [ + [0, 0, 0, 0], + [1, 2, 3, 4], + [0, 0, 0, 0], + [4, -3, -2, -4] + ]); + assertPointField(cd, "s", [ + [1, 2, 3, 4], + [3, 2, 1, 0], + [-1, -3, -2, -4], + [0, -0.25, -3, -2] + ]); + assertPointField(cd, "p", [ + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3, 4] + ]); + assertArrayField(cd[0][0], "t.barwidth", [1, 0.8, 0.6, 0.4]); + assertArrayField(cd[1][0], "t.barwidth", [0.4, 0.6, 0.8, 1]); + expect(cd[2][0].t.barwidth).toBe(1); + expect(cd[3][0].t.barwidth).toBe(0.8); + expect(cd[0][0].t.poffset).toBe(0); + expect(cd[1][0].t.poffset).toBe(0); + expect(cd[2][0].t.poffset).toBe(0); + expect(cd[3][0].t.poffset).toBe(0); + assertTraceField(cd, "t.bargroupwidth", [0.8, 0.8, 0.8, 0.8]); + + var traceNodes = getAllTraceNodes(gd), + trace0Bar3 = getAllBarNodes(traceNodes[0])[3], + path03 = trace0Bar3.querySelector("path"), + text03 = trace0Bar3.querySelector("text"), + trace1Bar2 = getAllBarNodes(traceNodes[1])[2], + path12 = trace1Bar2.querySelector("path"), + text12 = trace1Bar2.querySelector("text"), + trace2Bar0 = getAllBarNodes(traceNodes[2])[0], + path20 = trace2Bar0.querySelector("path"), + text20 = trace2Bar0.querySelector("text"), + trace3Bar0 = getAllBarNodes(traceNodes[3])[0], + path30 = trace3Bar0.querySelector("path"), + text30 = trace3Bar0.querySelector("text"); + + expect(text03.textContent).toBe("4"); + expect(text12.textContent).toBe("inside text"); + expect(text20.textContent).toBe("-1"); + expect(text30.textContent).toBe("outside text"); + + assertTextIsInsidePath(text03, path03); + // inside + assertTextIsInsidePath(text12, path12); + // inside + assertTextIsInsidePath(text20, path20); + // inside + assertTextIsInsidePath(text30, path30); + + // inside + done(); + }); + }); + + it("should coerce text-related attributes", function(done) { + var gd = createGraphDiv(), + data = [ + { + y: [10, 20, 30, 40], + type: "bar", + text: ["T1P1", "T1P2", 13, 14], + textposition: ["inside", "outside", "auto", "BADVALUE"], + textfont: { family: ['"comic sans"'], color: ["red", "green"] }, + insidetextfont: { size: [8, 12, 16], color: ["black"] }, + outsidetextfont: { size: [null, 24, 32] } + } + ], + layout = { font: { family: "arial", color: "blue", size: 13 } }; + + var expected = { + y: [10, 20, 30, 40], + type: "bar", + text: ["T1P1", "T1P2", "13", "14"], + textposition: ["inside", "outside", "none"], + textfont: { + family: ['"comic sans"', "arial"], + color: ["red", "green"], + size: [13, 13] + }, + insidetextfont: { + family: ['"comic sans"', "arial", "arial"], + color: ["black", "green", "blue"], + size: [8, 12, 16] + }, + outsidetextfont: { + family: ['"comic sans"', "arial", "arial"], + color: ["red", "green", "blue"], + size: [13, 24, 32] + } + }; - it('should skip placeholder trace in position computations', function() { - var gd = mockBarPlot([{ - x: [1, 2, 3], - y: [2, 1, 2] - }, { - x: [null], - y: [null] - }]); - - expect(gd.calcdata[0][0].t.barwidth).toEqual(0.8); - - expect(gd.calcdata[1][0].x).toBe(false); - expect(gd.calcdata[1][0].y).toBe(false); - expect(gd.calcdata[1][0].placeholder).toBe(true); - expect(gd.calcdata[1][0].t.barwidth).toBeUndefined(); + Plotly.plot(gd, data, layout).then(function() { + var traceNodes = getAllTraceNodes(gd), + barNodes = getAllBarNodes(traceNodes[0]), + pathNodes = [ + barNodes[0].querySelector("path"), + barNodes[1].querySelector("path"), + barNodes[2].querySelector("path"), + barNodes[3].querySelector("path") + ], + textNodes = [ + barNodes[0].querySelector("text"), + barNodes[1].querySelector("text"), + barNodes[2].querySelector("text"), + barNodes[3].querySelector("text") + ], + i; + + // assert bar texts + for (i = 0; i < 3; i++) { + expect(textNodes[i].textContent).toBe(expected.text[i]); + } + + // assert bar positions + assertTextIsInsidePath(textNodes[0], pathNodes[0]); + // inside + assertTextIsAbovePath(textNodes[1], pathNodes[1]); + // outside + assertTextIsInsidePath(textNodes[2], pathNodes[2]); + // auto -> inside + expect(textNodes[3]).toBe(null); + + // BADVALUE -> none + // assert fonts + assertTextFont(textNodes[0], expected.insidetextfont, 0); + assertTextFont(textNodes[1], expected.outsidetextfont, 1); + assertTextFont(textNodes[2], expected.insidetextfont, 2); + + done(); }); + }); }); -describe('A bar plot', function() { - 'use strict'; +describe("bar hover", function() { + "use strict"; + var gd; - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - afterEach(destroyGraphDiv); - - function getAllTraceNodes(node) { - return node.querySelectorAll('g.points'); - } - - function getAllBarNodes(node) { - return node.querySelectorAll('g.point'); - } - - function assertTextIsInsidePath(textNode, pathNode) { - var textBB = textNode.getBoundingClientRect(), - pathBB = pathNode.getBoundingClientRect(); + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - expect(pathBB.left).not.toBeGreaterThan(textBB.left); - expect(textBB.right).not.toBeGreaterThan(pathBB.right); - expect(pathBB.top).not.toBeGreaterThan(textBB.top); - expect(textBB.bottom).not.toBeGreaterThan(pathBB.bottom); - } + afterEach(destroyGraphDiv); - function assertTextIsAbovePath(textNode, pathNode) { - var textBB = textNode.getBoundingClientRect(), - pathBB = pathNode.getBoundingClientRect(); + function getPointData(gd) { + var cd = gd.calcdata, subplot = gd._fullLayout._plots.xy; - expect(textBB.bottom).not.toBeGreaterThan(pathBB.top); - } - - function assertTextIsBelowPath(textNode, pathNode) { - var textBB = textNode.getBoundingClientRect(), - pathBB = pathNode.getBoundingClientRect(); - - expect(pathBB.bottom).not.toBeGreaterThan(textBB.top); - } - - function assertTextIsAfterPath(textNode, pathNode) { - var textBB = textNode.getBoundingClientRect(), - pathBB = pathNode.getBoundingClientRect(); + return { + index: false, + distance: 20, + cd: cd[0], + trace: cd[0][0].trace, + xa: subplot.xaxis, + ya: subplot.yaxis + }; + } - expect(pathBB.right).not.toBeGreaterThan(textBB.left); - } + function _hover(gd, xval, yval, closest) { + var pointData = getPointData(gd); + var pt = Bar.hoverPoints(pointData, xval, yval, closest)[0]; - var colorMap = { - 'rgb(0, 0, 0)': 'black', - 'rgb(255, 0, 0)': 'red', - 'rgb(0, 128, 0)': 'green', - 'rgb(0, 0, 255)': 'blue' + return { + style: [pt.index, pt.color, pt.xLabelVal, pt.yLabelVal], + pos: [pt.x0, pt.x1, pt.y0, pt.y1] }; - function assertTextFont(textNode, textFont, index) { - expect(textNode.style.fontFamily).toBe(textFont.family[index]); - expect(textNode.style.fontSize).toBe(textFont.size[index] + 'px'); - - var color = textNode.style.fill; - if(!colorMap[color]) colorMap[color] = color; - expect(colorMap[color]).toBe(textFont.color[index]); - } - - function assertTextIsBeforePath(textNode, pathNode) { - var textBB = textNode.getBoundingClientRect(), - pathBB = pathNode.getBoundingClientRect(); - - expect(textBB.right).not.toBeGreaterThan(pathBB.left); - } - - it('should show bar texts (inside case)', function(done) { - var gd = createGraphDiv(), - data = [{ - y: [10, 20, 30], - type: 'bar', - text: ['1', 'Very very very very very long bar text'], - textposition: 'inside', - }], - layout = { - }; - - Plotly.plot(gd, data, layout).then(function() { - var traceNodes = getAllTraceNodes(gd), - barNodes = getAllBarNodes(traceNodes[0]), - foundTextNodes; - - for(var i = 0; i < barNodes.length; i++) { - var barNode = barNodes[i], - pathNode = barNode.querySelector('path'), - textNode = barNode.querySelector('text'); - if(textNode) { - foundTextNodes = true; - assertTextIsInsidePath(textNode, pathNode); - } - } - - expect(foundTextNodes).toBe(true); - - done(); - }); - }); + } - it('should show bar texts (outside case)', function(done) { - var gd = createGraphDiv(), - data = [{ - y: [10, -20, 30], - type: 'bar', - text: ['1', 'Very very very very very long bar text'], - textposition: 'outside', - }], - layout = { - barmode: 'relative' - }; - - Plotly.plot(gd, data, layout).then(function() { - var traceNodes = getAllTraceNodes(gd), - barNodes = getAllBarNodes(traceNodes[0]), - foundTextNodes; - - for(var i = 0; i < barNodes.length; i++) { - var barNode = barNodes[i], - pathNode = barNode.querySelector('path'), - textNode = barNode.querySelector('text'); - if(textNode) { - foundTextNodes = true; - if(data[0].y[i] > 0) assertTextIsAbovePath(textNode, pathNode); - else assertTextIsBelowPath(textNode, pathNode); - } - } - - expect(foundTextNodes).toBe(true); - - done(); - }); - }); + function assertPos(actual, expected) { + var TOL = 5; - it('should show bar texts (horizontal case)', function(done) { - var gd = createGraphDiv(), - data = [{ - x: [10, -20, 30], - type: 'bar', - text: ['Very very very very very long bar text', -20], - textposition: 'outside', - }], - layout = { - }; - - Plotly.plot(gd, data, layout).then(function() { - var traceNodes = getAllTraceNodes(gd), - barNodes = getAllBarNodes(traceNodes[0]), - foundTextNodes; - - for(var i = 0; i < barNodes.length; i++) { - var barNode = barNodes[i], - pathNode = barNode.querySelector('path'), - textNode = barNode.querySelector('text'); - if(textNode) { - foundTextNodes = true; - if(data[0].x[i] > 0) assertTextIsAfterPath(textNode, pathNode); - else assertTextIsBeforePath(textNode, pathNode); - } - } - - expect(foundTextNodes).toBe(true); - - done(); - }); + actual.forEach(function(p, i) { + expect(p).toBeWithin(expected[i], TOL); }); + } - it('should show bar texts (barnorm case)', function(done) { - var gd = createGraphDiv(), - data = [{ - x: [100, -100, 100], - type: 'bar', - text: [100, -100, 100], - textposition: 'outside', - }], - layout = { - barmode: 'relative', - barnorm: 'percent' - }; - - Plotly.plot(gd, data, layout).then(function() { - var traceNodes = getAllTraceNodes(gd), - barNodes = getAllBarNodes(traceNodes[0]), - foundTextNodes; - - for(var i = 0; i < barNodes.length; i++) { - var barNode = barNodes[i], - pathNode = barNode.querySelector('path'), - textNode = barNode.querySelector('text'); - if(textNode) { - foundTextNodes = true; - if(data[0].x[i] > 0) assertTextIsAfterPath(textNode, pathNode); - else assertTextIsBeforePath(textNode, pathNode); - } - } - - expect(foundTextNodes).toBe(true); - - done(); - }); - }); + describe("with orientation *v*", function() { + beforeAll(function(done) { + gd = createGraphDiv(); - it('should be able to restyle', function(done) { - var gd = createGraphDiv(), - mock = Lib.extendDeep({}, require('@mocks/bar_attrs_relative')); - - Plotly.plot(gd, mock.data, mock.layout).then(function() { - var cd = gd.calcdata; - assertPointField(cd, 'x', [ - [1, 2, 3, 4], [1, 2, 3, 4], - [1, 2, 3, 4], [1, 2, 3, 4]]); - assertPointField(cd, 'y', [ - [1, 2, 3, 4], [4, 4, 4, 4], - [-1, -3, -2, -4], [4, -3.25, -5, -6]]); - assertPointField(cd, 'b', [ - [0, 0, 0, 0], [1, 2, 3, 4], - [0, 0, 0, 0], [4, -3, -2, -4]]); - assertPointField(cd, 's', [ - [1, 2, 3, 4], [3, 2, 1, 0], - [-1, -3, -2, -4], [0, -0.25, -3, -2]]); - assertPointField(cd, 'p', [ - [1, 2, 3, 4], [1, 2, 3, 4], - [1, 2, 3, 4], [1, 2, 3, 4]]); - assertArrayField(cd[0][0], 't.barwidth', [1, 0.8, 0.6, 0.4]); - assertArrayField(cd[1][0], 't.barwidth', [0.4, 0.6, 0.8, 1]); - expect(cd[2][0].t.barwidth).toBe(1); - expect(cd[3][0].t.barwidth).toBe(0.8); - assertArrayField(cd[0][0], 't.poffset', [-0.5, -0.4, -0.3, -0.2]); - assertArrayField(cd[1][0], 't.poffset', [-0.2, -0.3, -0.4, -0.5]); - expect(cd[2][0].t.poffset).toBe(-0.5); - expect(cd[3][0].t.poffset).toBe(-0.4); - assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8, 0.8]); - - return Plotly.restyle(gd, 'offset', 0); - }).then(function() { - var cd = gd.calcdata; - assertPointField(cd, 'x', [ - [1.5, 2.4, 3.3, 4.2], [1.2, 2.3, 3.4, 4.5], - [1.5, 2.5, 3.5, 4.5], [1.4, 2.4, 3.4, 4.4]]); - assertPointField(cd, 'y', [ - [1, 2, 3, 4], [4, 4, 4, 4], - [-1, -3, -2, -4], [4, -3.25, -5, -6]]); - assertPointField(cd, 'b', [ - [0, 0, 0, 0], [1, 2, 3, 4], - [0, 0, 0, 0], [4, -3, -2, -4]]); - assertPointField(cd, 's', [ - [1, 2, 3, 4], [3, 2, 1, 0], - [-1, -3, -2, -4], [0, -0.25, -3, -2]]); - assertPointField(cd, 'p', [ - [1, 2, 3, 4], [1, 2, 3, 4], - [1, 2, 3, 4], [1, 2, 3, 4]]); - assertArrayField(cd[0][0], 't.barwidth', [1, 0.8, 0.6, 0.4]); - assertArrayField(cd[1][0], 't.barwidth', [0.4, 0.6, 0.8, 1]); - expect(cd[2][0].t.barwidth).toBe(1); - expect(cd[3][0].t.barwidth).toBe(0.8); - expect(cd[0][0].t.poffset).toBe(0); - expect(cd[1][0].t.poffset).toBe(0); - expect(cd[2][0].t.poffset).toBe(0); - expect(cd[3][0].t.poffset).toBe(0); - assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8, 0.8]); - - var traceNodes = getAllTraceNodes(gd), - trace0Bar3 = getAllBarNodes(traceNodes[0])[3], - path03 = trace0Bar3.querySelector('path'), - text03 = trace0Bar3.querySelector('text'), - trace1Bar2 = getAllBarNodes(traceNodes[1])[2], - path12 = trace1Bar2.querySelector('path'), - text12 = trace1Bar2.querySelector('text'), - trace2Bar0 = getAllBarNodes(traceNodes[2])[0], - path20 = trace2Bar0.querySelector('path'), - text20 = trace2Bar0.querySelector('text'), - trace3Bar0 = getAllBarNodes(traceNodes[3])[0], - path30 = trace3Bar0.querySelector('path'), - text30 = trace3Bar0.querySelector('text'); - - expect(text03.textContent).toBe('4'); - expect(text12.textContent).toBe('inside text'); - expect(text20.textContent).toBe('-1'); - expect(text30.textContent).toBe('outside text'); - - assertTextIsAbovePath(text03, path03); // outside - assertTextIsInsidePath(text12, path12); // inside - assertTextIsInsidePath(text20, path20); // inside - assertTextIsBelowPath(text30, path30); // outside - - return Plotly.restyle(gd, 'textposition', 'inside'); - }).then(function() { - var cd = gd.calcdata; - assertPointField(cd, 'x', [ - [1.5, 2.4, 3.3, 4.2], [1.2, 2.3, 3.4, 4.5], - [1.5, 2.5, 3.5, 4.5], [1.4, 2.4, 3.4, 4.4]]); - assertPointField(cd, 'y', [ - [1, 2, 3, 4], [4, 4, 4, 4], - [-1, -3, -2, -4], [4, -3.25, -5, -6]]); - assertPointField(cd, 'b', [ - [0, 0, 0, 0], [1, 2, 3, 4], - [0, 0, 0, 0], [4, -3, -2, -4]]); - assertPointField(cd, 's', [ - [1, 2, 3, 4], [3, 2, 1, 0], - [-1, -3, -2, -4], [0, -0.25, -3, -2]]); - assertPointField(cd, 'p', [ - [1, 2, 3, 4], [1, 2, 3, 4], - [1, 2, 3, 4], [1, 2, 3, 4]]); - assertArrayField(cd[0][0], 't.barwidth', [1, 0.8, 0.6, 0.4]); - assertArrayField(cd[1][0], 't.barwidth', [0.4, 0.6, 0.8, 1]); - expect(cd[2][0].t.barwidth).toBe(1); - expect(cd[3][0].t.barwidth).toBe(0.8); - expect(cd[0][0].t.poffset).toBe(0); - expect(cd[1][0].t.poffset).toBe(0); - expect(cd[2][0].t.poffset).toBe(0); - expect(cd[3][0].t.poffset).toBe(0); - assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8, 0.8]); - - var traceNodes = getAllTraceNodes(gd), - trace0Bar3 = getAllBarNodes(traceNodes[0])[3], - path03 = trace0Bar3.querySelector('path'), - text03 = trace0Bar3.querySelector('text'), - trace1Bar2 = getAllBarNodes(traceNodes[1])[2], - path12 = trace1Bar2.querySelector('path'), - text12 = trace1Bar2.querySelector('text'), - trace2Bar0 = getAllBarNodes(traceNodes[2])[0], - path20 = trace2Bar0.querySelector('path'), - text20 = trace2Bar0.querySelector('text'), - trace3Bar0 = getAllBarNodes(traceNodes[3])[0], - path30 = trace3Bar0.querySelector('path'), - text30 = trace3Bar0.querySelector('text'); - - expect(text03.textContent).toBe('4'); - expect(text12.textContent).toBe('inside text'); - expect(text20.textContent).toBe('-1'); - expect(text30.textContent).toBe('outside text'); - - assertTextIsInsidePath(text03, path03); // inside - assertTextIsInsidePath(text12, path12); // inside - assertTextIsInsidePath(text20, path20); // inside - assertTextIsInsidePath(text30, path30); // inside - - done(); - }); - }); + var mock = Lib.extendDeep({}, require("@mocks/11.json")); - it('should coerce text-related attributes', function(done) { - var gd = createGraphDiv(), - data = [{ - y: [10, 20, 30, 40], - type: 'bar', - text: ['T1P1', 'T1P2', 13, 14], - textposition: ['inside', 'outside', 'auto', 'BADVALUE'], - textfont: { - family: ['"comic sans"'], - color: ['red', 'green'], - }, - insidetextfont: { - size: [8, 12, 16], - color: ['black'], - }, - outsidetextfont: { - size: [null, 24, 32] - } - }], - layout = { - font: {family: 'arial', color: 'blue', size: 13} - }; - - var expected = { - y: [10, 20, 30, 40], - type: 'bar', - text: ['T1P1', 'T1P2', '13', '14'], - textposition: ['inside', 'outside', 'none'], - textfont: { - family: ['"comic sans"', 'arial'], - color: ['red', 'green'], - size: [13, 13] - }, - insidetextfont: { - family: ['"comic sans"', 'arial', 'arial'], - color: ['black', 'green', 'blue'], - size: [8, 12, 16] - }, - outsidetextfont: { - family: ['"comic sans"', 'arial', 'arial'], - color: ['red', 'green', 'blue'], - size: [13, 24, 32] - } - }; - - Plotly.plot(gd, data, layout).then(function() { - var traceNodes = getAllTraceNodes(gd), - barNodes = getAllBarNodes(traceNodes[0]), - pathNodes = [ - barNodes[0].querySelector('path'), - barNodes[1].querySelector('path'), - barNodes[2].querySelector('path'), - barNodes[3].querySelector('path') - ], - textNodes = [ - barNodes[0].querySelector('text'), - barNodes[1].querySelector('text'), - barNodes[2].querySelector('text'), - barNodes[3].querySelector('text') - ], - i; - - // assert bar texts - for(i = 0; i < 3; i++) { - expect(textNodes[i].textContent).toBe(expected.text[i]); - } - - // assert bar positions - assertTextIsInsidePath(textNodes[0], pathNodes[0]); // inside - assertTextIsAbovePath(textNodes[1], pathNodes[1]); // outside - assertTextIsInsidePath(textNodes[2], pathNodes[2]); // auto -> inside - expect(textNodes[3]).toBe(null); // BADVALUE -> none - - // assert fonts - assertTextFont(textNodes[0], expected.insidetextfont, 0); - assertTextFont(textNodes[1], expected.outsidetextfont, 1); - assertTextFont(textNodes[2], expected.insidetextfont, 2); - - done(); - }); + Plotly.plot(gd, mock.data, mock.layout).then(done); }); -}); - -describe('bar hover', function() { - 'use strict'; - var gd; + it("should return the correct hover point data (case x)", function() { + var out = _hover(gd, 0, 0, "x"); - beforeAll(function() { - jasmine.addMatchers(customMatchers); + expect(out.style).toEqual([0, "rgb(255, 102, 97)", 0, 13.23]); + assertPos(out.pos, [11.87, 106.8, 152.76, 152.76]); }); - afterEach(destroyGraphDiv); - - function getPointData(gd) { - var cd = gd.calcdata, - subplot = gd._fullLayout._plots.xy; - - return { - index: false, - distance: 20, - cd: cd[0], - trace: cd[0][0].trace, - xa: subplot.xaxis, - ya: subplot.yaxis - }; - } + it("should return the correct hover point data (case closest)", function() { + var out = _hover(gd, -0.2, 12, "closest"); - function _hover(gd, xval, yval, closest) { - var pointData = getPointData(gd); - var pt = Bar.hoverPoints(pointData, xval, yval, closest)[0]; - - return { - style: [pt.index, pt.color, pt.xLabelVal, pt.yLabelVal], - pos: [pt.x0, pt.x1, pt.y0, pt.y1] - }; - } - - function assertPos(actual, expected) { - var TOL = 5; - - actual.forEach(function(p, i) { - expect(p).toBeWithin(expected[i], TOL); - }); - } - - describe('with orientation *v*', function() { - beforeAll(function(done) { - gd = createGraphDiv(); - - var mock = Lib.extendDeep({}, require('@mocks/11.json')); - - Plotly.plot(gd, mock.data, mock.layout).then(done); - }); - - it('should return the correct hover point data (case x)', function() { - var out = _hover(gd, 0, 0, 'x'); + expect(out.style).toEqual([0, "rgb(255, 102, 97)", 0, 13.23]); + assertPos(out.pos, [11.87, 59.33, 152.76, 152.76]); + }); + }); - expect(out.style).toEqual([0, 'rgb(255, 102, 97)', 0, 13.23]); - assertPos(out.pos, [11.87, 106.8, 152.76, 152.76]); - }); + describe("with orientation *h*", function() { + beforeAll(function(done) { + gd = createGraphDiv(); - it('should return the correct hover point data (case closest)', function() { - var out = _hover(gd, -0.2, 12, 'closest'); + var mock = Lib.extendDeep( + {}, + require("@mocks/bar_attrs_group_norm.json") + ); - expect(out.style).toEqual([0, 'rgb(255, 102, 97)', 0, 13.23]); - assertPos(out.pos, [11.87, 59.33, 152.76, 152.76]); - }); + Plotly.plot(gd, mock.data, mock.layout).then(done); }); - describe('with orientation *h*', function() { - beforeAll(function(done) { - gd = createGraphDiv(); - - var mock = Lib.extendDeep({}, require('@mocks/bar_attrs_group_norm.json')); - - Plotly.plot(gd, mock.data, mock.layout).then(done); - }); - - it('should return the correct hover point data (case y)', function() { - var out = _hover(gd, 0.75, 0.15, 'y'), - subplot = gd._fullLayout._plots.xy, - xa = subplot.xaxis, - ya = subplot.yaxis, - barDelta = 1 * 0.8 / 2, - x0 = xa.c2p(0.5, true), - x1 = x0, - y0 = ya.c2p(0 - barDelta, true), - y1 = ya.c2p(0 + barDelta, true); - - expect(out.style).toEqual([0, '#1f77b4', 0.5, 0]); - assertPos(out.pos, [x0, x1, y0, y1]); - }); - - it('should return the correct hover point data (case closest)', function() { - var out = _hover(gd, 0.75, -0.15, 'closest'), - subplot = gd._fullLayout._plots.xy, - xa = subplot.xaxis, - ya = subplot.yaxis, - barDelta = 1 * 0.8 / 2 / 2, - barPos = 0 - 1 * 0.8 / 2 + barDelta, - x0 = xa.c2p(0.5, true), - x1 = x0, - y0 = ya.c2p(barPos - barDelta, true), - y1 = ya.c2p(barPos + barDelta, true); - - expect(out.style).toEqual([0, '#1f77b4', 0.5, 0]); - assertPos(out.pos, [x0, x1, y0, y1]); - }); + it("should return the correct hover point data (case y)", function() { + var out = _hover(gd, 0.75, 0.15, "y"), + subplot = gd._fullLayout._plots.xy, + xa = subplot.xaxis, + ya = subplot.yaxis, + barDelta = 1 * 0.8 / 2, + x0 = xa.c2p(0.5, true), + x1 = x0, + y0 = ya.c2p(0 - barDelta, true), + y1 = ya.c2p(0 + barDelta, true); + + expect(out.style).toEqual([0, "#1f77b4", 0.5, 0]); + assertPos(out.pos, [x0, x1, y0, y1]); }); + it("should return the correct hover point data (case closest)", function() { + var out = _hover(gd, 0.75, -0.15, "closest"), + subplot = gd._fullLayout._plots.xy, + xa = subplot.xaxis, + ya = subplot.yaxis, + barDelta = 1 * 0.8 / 2 / 2, + barPos = 0 - 1 * 0.8 / 2 + barDelta, + x0 = xa.c2p(0.5, true), + x1 = x0, + y0 = ya.c2p(barPos - barDelta, true), + y1 = ya.c2p(barPos + barDelta, true); + + expect(out.style).toEqual([0, "#1f77b4", 0.5, 0]); + assertPos(out.pos, [x0, x1, y0, y1]); + }); + }); }); function mockBarPlot(dataWithoutTraceType, layout) { - var traceTemplate = { type: 'bar' }; + var traceTemplate = { type: "bar" }; - var dataWithTraceType = dataWithoutTraceType.map(function(trace) { - return Lib.extendFlat({}, traceTemplate, trace); - }); + var dataWithTraceType = dataWithoutTraceType.map(function(trace) { + return Lib.extendFlat({}, traceTemplate, trace); + }); - var gd = { - data: dataWithTraceType, - layout: layout, - calcdata: [] - }; + var gd = { data: dataWithTraceType, layout: layout, calcdata: [] }; - Plots.supplyDefaults(gd); - Plots.doCalcdata(gd); + Plots.supplyDefaults(gd); + Plots.doCalcdata(gd); - var plotinfo = { - xaxis: gd._fullLayout.xaxis, - yaxis: gd._fullLayout.yaxis - }; + var plotinfo = { xaxis: gd._fullLayout.xaxis, yaxis: gd._fullLayout.yaxis }; - // call Bar.setPositions - Bar.setPositions(gd, plotinfo); + // call Bar.setPositions + Bar.setPositions(gd, plotinfo); - return gd; + return gd; } function assertArrayField(calcData, prop, expectation) { - // Note that this functions requires to add `customMatchers` to jasmine - // matchers; i.e: `jasmine.addMatchers(customMatchers);`. - var values = Lib.nestedProperty(calcData, prop).get(); - if(!Array.isArray(values)) values = [values]; - - expect(values).toBeCloseToArray(expectation, undefined, '(field ' + prop + ')'); + // Note that this functions requires to add `customMatchers` to jasmine + // matchers; i.e: `jasmine.addMatchers(customMatchers);`. + var values = Lib.nestedProperty(calcData, prop).get(); + if (!Array.isArray(values)) values = [values]; + + expect(values).toBeCloseToArray( + expectation, + undefined, + "(field " + prop + ")" + ); } function assertPointField(calcData, prop, expectation) { - // Note that this functions requires to add `customMatchers` to jasmine - // matchers; i.e: `jasmine.addMatchers(customMatchers);`. - var values = []; + // Note that this functions requires to add `customMatchers` to jasmine + // matchers; i.e: `jasmine.addMatchers(customMatchers);`. + var values = []; - calcData.forEach(function(calcTrace) { - var vals = calcTrace.map(function(pt) { - return Lib.nestedProperty(pt, prop).get(); - }); - - values.push(vals); + calcData.forEach(function(calcTrace) { + var vals = calcTrace.map(function(pt) { + return Lib.nestedProperty(pt, prop).get(); }); - expect(values).toBeCloseTo2DArray(expectation, undefined, '(field ' + prop + ')'); + values.push(vals); + }); + + expect(values).toBeCloseTo2DArray( + expectation, + undefined, + "(field " + prop + ")" + ); } function assertTraceField(calcData, prop, expectation) { - // Note that this functions requires to add `customMatchers` to jasmine - // matchers; i.e: `jasmine.addMatchers(customMatchers);`. - var values = calcData.map(function(calcTrace) { - return Lib.nestedProperty(calcTrace[0], prop).get(); - }); - - expect(values).toBeCloseToArray(expectation, undefined, '(field ' + prop + ')'); + // Note that this functions requires to add `customMatchers` to jasmine + // matchers; i.e: `jasmine.addMatchers(customMatchers);`. + var values = calcData.map(function(calcTrace) { + return Lib.nestedProperty(calcTrace[0], prop).get(); + }); + + expect(values).toBeCloseToArray( + expectation, + undefined, + "(field " + prop + ")" + ); } diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index c292fc356c9..03fecde9c18 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -1,112 +1,80 @@ -var Box = require('@src/traces/box'); - - -describe('Test boxes', function() { - 'use strict'; - - describe('supplyDefaults', function() { - var traceIn, - traceOut; - - var defaultColor = '#444'; - - var supplyDefaults = Box.supplyDefaults; - - beforeEach(function() { - traceOut = {}; - }); - - it('should set visible to false when x and y are empty', function() { - traceIn = {}; - supplyDefaults(traceIn, traceOut, defaultColor); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [], - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - }); - - it('should set visible to false when x or y is empty', function() { - traceIn = { - x: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [], - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [1, 2, 3], - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.visible).toBe(false); - }); - - it('should set orientation to v by default', function() { - traceIn = { - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.orientation).toBe('v'); - - traceIn = { - x: [1, 1, 1], - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.orientation).toBe('v'); - }); - - it('should set orientation to h when only x is supplied', function() { - traceIn = { - x: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {}); - expect(traceOut.orientation).toBe('h'); - - }); - - it('should inherit layout.calendar', function() { - traceIn = { - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('islamic'); - expect(traceOut.ycalendar).toBe('islamic'); - }); - - it('should take its own calendars', function() { - traceIn = { - y: [1, 2, 3], - xcalendar: 'coptic', - ycalendar: 'ethiopian' - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('coptic'); - expect(traceOut.ycalendar).toBe('ethiopian'); - }); +var Box = require("@src/traces/box"); +describe("Test boxes", function() { + "use strict"; + describe("supplyDefaults", function() { + var traceIn, traceOut; + + var defaultColor = "#444"; + + var supplyDefaults = Box.supplyDefaults; + + beforeEach(function() { + traceOut = {}; + }); + + it("should set visible to false when x and y are empty", function() { + traceIn = {}; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.visible).toBe(false); + + traceIn = { x: [], y: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + }); + + it("should set visible to false when x or y is empty", function() { + traceIn = { x: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + + traceIn = { x: [], y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + + traceIn = { y: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); + + traceIn = { x: [1, 2, 3], y: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(false); }); + it("should set orientation to v by default", function() { + traceIn = { y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.orientation).toBe("v"); + + traceIn = { x: [1, 1, 1], y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.orientation).toBe("v"); + }); + + it("should set orientation to h when only x is supplied", function() { + traceIn = { x: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.orientation).toBe("h"); + }); + + it("should inherit layout.calendar", function() { + traceIn = { y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("islamic"); + expect(traceOut.ycalendar).toBe("islamic"); + }); + + it("should take its own calendars", function() { + traceIn = { y: [1, 2, 3], xcalendar: "coptic", ycalendar: "ethiopian" }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("coptic"); + expect(traceOut.ycalendar).toBe("ethiopian"); + }); + }); }); diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 9ac9dda0e98..5ea19b48897 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -1,865 +1,2068 @@ -var Plotly = require('@lib/index'); +var Plotly = require("@lib/index"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); -describe('calculated data and points', function() { +describe("calculated data and points", function() { + var gd; - var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - describe('connectGaps', function() { - - it('should exclude null and undefined points when false', function() { - Plotly.plot(gd, [{ x: [1, 2, 3, undefined, 5], y: [1, null, 3, 4, 5]}], {}); + afterEach(destroyGraphDiv); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({ x: false, y: false})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({ x: false, y: false})); - }); - - it('should exclude null and undefined points as categories when false', function() { - Plotly.plot(gd, [{ x: [1, 2, 3, undefined, 5], y: [1, null, 3, 4, 5] }], { xaxis: { type: 'category' }}); + describe("connectGaps", function() { + it("should exclude null and undefined points when false", function() { + Plotly.plot(gd, [{ x: [1, 2, 3, undefined, 5], y: [1, null, 3, 4, 5] }], { + }); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({ x: false, y: false})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({ x: false, y: false})); - }); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: false, y: false }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: false, y: false }) + ); }); - describe('category ordering', function() { - - describe('default category ordering reified', function() { - - it('should output categories in the given order by default', function() { - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category' - }}); - - expect(gd.calcdata[0][0].y).toEqual(15); - expect(gd.calcdata[0][1].y).toEqual(11); - expect(gd.calcdata[0][2].y).toEqual(12); - expect(gd.calcdata[0][3].y).toEqual(13); - expect(gd.calcdata[0][4].y).toEqual(14); - }); - - it('should output categories in the given order if `trace` order is explicitly specified', function() { - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'trace' - // Also, if axis tick order is made configurable, shouldn't we make trace order configurable? - // Trace order as in, if a line or curve is drawn through points, what's the trace sequence. - // These are two orthogonal concepts. Currently, the trace order is implied - // by the order the {x,y} arrays are specified. - }}); - - expect(gd.calcdata[0][0].y).toEqual(15); - expect(gd.calcdata[0][1].y).toEqual(11); - expect(gd.calcdata[0][2].y).toEqual(12); - expect(gd.calcdata[0][3].y).toEqual(13); - expect(gd.calcdata[0][4].y).toEqual(14); - }); - }); - - describe('domain alphanumerical category ordering', function() { - - it('should output categories in ascending domain alphanumerical order', function() { - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'category ascending' - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); - }); - - it('should output categories in descending domain alphanumerical order', function() { - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'category descending' - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 4, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 0, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 3, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 1, y: 14})); - }); - - it('should output categories in ascending domain alphanumerical order even if categories are all numbers', function() { - - Plotly.plot(gd, [{x: [3, 1, 5, 2, 4], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'category ascending' - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); - }); - - it('should output categories in categoryorder order even if category array is defined', function() { - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'category ascending', - categoryarray: ['b', 'a', 'd', 'e', 'c'] // These must be ignored. Alternative: error? - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); - }); - - it('should output categories in ascending domain alphanumerical order, excluding undefined', function() { - - Plotly.plot(gd, [{x: ['c', undefined, 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'category ascending' - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 15})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 3, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 2, y: 14})); - }); - - it('should combine duplicate categories', function() { - Plotly.plot(gd, [{x: [ '1', '1'], y: [10, 20]}], { xaxis: { - type: 'category', - categoryorder: 'category ascending' - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + it( + "should exclude null and undefined points as categories when false", + function() { + Plotly.plot( + gd, + [{ x: [1, 2, 3, undefined, 5], y: [1, null, 3, 4, 5] }], + { xaxis: { type: "category" } } + ); + + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: false, y: false }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: false, y: false }) + ); + } + ); + }); + + describe("category ordering", function() { + describe("default category ordering reified", function() { + it("should output categories in the given order by default", function() { + Plotly.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { xaxis: { type: "category" } } + ); + + expect(gd.calcdata[0][0].y).toEqual(15); + expect(gd.calcdata[0][1].y).toEqual(11); + expect(gd.calcdata[0][2].y).toEqual(12); + expect(gd.calcdata[0][3].y).toEqual(13); + expect(gd.calcdata[0][4].y).toEqual(14); + }); + + it( + "should output categories in the given order if `trace` order is explicitly specified", + function() { + Plotly.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + // Also, if axis tick order is made configurable, shouldn't we make trace order configurable? + // Trace order as in, if a line or curve is drawn through points, what's the trace sequence. + // These are two orthogonal concepts. Currently, the trace order is implied + // by the order the {x,y} arrays are specified. + categoryorder: "trace" + } + } + ); + + expect(gd.calcdata[0][0].y).toEqual(15); + expect(gd.calcdata[0][1].y).toEqual(11); + expect(gd.calcdata[0][2].y).toEqual(12); + expect(gd.calcdata[0][3].y).toEqual(13); + expect(gd.calcdata[0][4].y).toEqual(14); + } + ); + }); - expect(gd._fullLayout.xaxis._categories).toEqual(['1']); - }); + describe("domain alphanumerical category ordering", function() { + it( + "should output categories in ascending domain alphanumerical order", + function() { + Plotly.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { xaxis: { type: "category", categoryorder: "category ascending" } } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 4, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 1, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 3, y: 14 }) + ); + } + ); + + it( + "should output categories in descending domain alphanumerical order", + function() { + Plotly.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { type: "category", categoryorder: "category descending" } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 4, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 0, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 3, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 1, y: 14 }) + ); + } + ); + + it( + "should output categories in ascending domain alphanumerical order even if categories are all numbers", + function() { + Plotly.plot(gd, [{ x: [3, 1, 5, 2, 4], y: [15, 11, 12, 13, 14] }], { + xaxis: { type: "category", categoryorder: "category ascending" } + }); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 4, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 1, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 3, y: 14 }) + ); + } + ); + + it( + "should output categories in categoryorder order even if category array is defined", + function() { + Plotly.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "category ascending", + // These must be ignored. Alternative: error? + categoryarray: ["b", "a", "d", "e", "c"] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 4, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 1, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 3, y: 14 }) + ); + } + ); + + it( + "should output categories in ascending domain alphanumerical order, excluding undefined", + function() { + Plotly.plot( + gd, + [ + { + x: ["c", undefined, "e", "b", "d"], + y: [15, 11, 12, 13, 14] + } + ], + { xaxis: { type: "category", categoryorder: "category ascending" } } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 1, y: 15 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 3, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 0, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 2, y: 14 }) + ); + } + ); + + it("should combine duplicate categories", function() { + Plotly.plot(gd, [{ x: ["1", "1"], y: [10, 20] }], { + xaxis: { type: "category", categoryorder: "category ascending" } }); - describe('explicit category ordering', function() { - - it('should output categories in explicitly supplied order, independent of trace order', function() { - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'array', - categoryarray: ['b', 'a', 'd', 'e', 'c'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 3, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 2, y: 14})); - }); - - it('should output categories in explicitly supplied order even if category values are all numbers', function() { - - Plotly.plot(gd, [{x: [3, 1, 5, 2, 4], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'array', - categoryarray: [2, 1, 4, 5, 3] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 3, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 2, y: 14})); - }); - - it('should output categories in explicitly supplied order, independent of trace order, pruned', function() { - - Plotly.plot(gd, [{x: ['c', undefined, 'e', 'b', 'd'], y: [15, 11, 12, null, 14]}], { xaxis: { - type: 'category', - categoryorder: 'array', - categoryarray: ['b', 'a', 'd', 'e', 'c'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({ x: false, y: false})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 3, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({ x: false, y: false})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 2, y: 14})); - }); - - it('should output categories in explicitly supplied order even if not all categories are present', function() { - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'array', - categoryarray: ['b', 'x', 'a', 'd', 'z', 'e', 'c'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 2, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 5, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); - }); - - it('should output categories in explicitly supplied order even if some missing categories were at the beginning or end of categoryarray', function() { - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'array', - categoryarray: ['y', 'b', 'x', 'a', 'd', 'z', 'e', 'c', 'q', 'k'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 7, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 3, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 4, y: 14})); - - // The auto-range feature currently eliminates unused category ticks on the left/right axis tails. - // The below test case reifies this current behavior, and checks proper order of categories kept. - - var domTickTexts = Array.prototype.slice.call(document.querySelectorAll('g.xtick')) - .map(function(e) {return e.__data__.text;}); - - expect(domTickTexts).toEqual(['b', 'x', 'a', 'd', 'z', 'e', 'c']); // y, q and k has no data points - }); - - it('should output categories in explicitly supplied order even if some missing categories were at the beginning or end of categoryarray', function() { + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 20 }) + ); - // The auto-range feature currently eliminates unutilized category ticks on the left/right edge - // BUT keeps it if a data point with null is added; test is almost identical to the one above - // except that it explicitly adds an axis tick for y - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd', 'y'], y: [15, 11, 12, 13, 14, null]}], { xaxis: { - type: 'category', - categoryorder: 'array', - categoryarray: ['y', 'b', 'x', 'a', 'd', 'z', 'e', 'c', 'q', 'k'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 7, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 3, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 4, y: 14})); - - var domTickTexts = Array.prototype.slice.call(document.querySelectorAll('g.xtick')) - .map(function(e) {return e.__data__.text;}); + expect(gd._fullLayout.xaxis._categories).toEqual(["1"]); + }); + }); - expect(domTickTexts).toEqual(['y', 'b', 'x', 'a', 'd', 'z', 'e', 'c']); // q, k has no data; y is null + describe("explicit category ordering", function() { + it( + "should output categories in explicitly supplied order, independent of trace order", + function() { + Plotly.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "array", + categoryarray: ["b", "a", "d", "e", "c"] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 4, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 3, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 0, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 2, y: 14 }) + ); + } + ); + + it( + "should output categories in explicitly supplied order even if category values are all numbers", + function() { + Plotly.plot(gd, [{ x: [3, 1, 5, 2, 4], y: [15, 11, 12, 13, 14] }], { + xaxis: { + type: "category", + categoryorder: "array", + categoryarray: [2, 1, 4, 5, 3] + } + }); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 4, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 3, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 0, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 2, y: 14 }) + ); + } + ); + + it( + "should output categories in explicitly supplied order, independent of trace order, pruned", + function() { + Plotly.plot( + gd, + [ + { + x: ["c", undefined, "e", "b", "d"], + y: [15, 11, 12, null, 14] + } + ], + { + xaxis: { + type: "category", + categoryorder: "array", + categoryarray: ["b", "a", "d", "e", "c"] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 4, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: false, y: false }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 3, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: false, y: false }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 2, y: 14 }) + ); + } + ); + + it( + "should output categories in explicitly supplied order even if not all categories are present", + function() { + Plotly.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "array", + categoryarray: ["b", "x", "a", "d", "z", "e", "c"] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 6, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 2, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 5, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 0, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 3, y: 14 }) + ); + } + ); + + it( + "should output categories in explicitly supplied order even if some missing categories were at the beginning or end of categoryarray", + function() { + Plotly.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "array", + categoryarray: [ + "y", + "b", + "x", + "a", + "d", + "z", + "e", + "c", + "q", + "k" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 7, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 3, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 6, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 1, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 4, y: 14 }) + ); + + // The auto-range feature currently eliminates unused category ticks on the left/right axis tails. + // The below test case reifies this current behavior, and checks proper order of categories kept. + var domTickTexts = Array.prototype.slice + .call(document.querySelectorAll("g.xtick")) + .map(function(e) { + return e.__data__.text; }); - it('should output categories in explicitly supplied order even if not all categories are present, and should interact with a null value orthogonally', function() { - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, null, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'array', - categoryarray: ['b', 'x', 'a', 'd', 'z', 'e', 'c'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: false, y: false})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 5, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); + expect(domTickTexts).toEqual(["b", "x", "a", "d", "z", "e", "c"]); // y, q and k has no data points + } + ); + + it( + "should output categories in explicitly supplied order even if some missing categories were at the beginning or end of categoryarray", + function() { + // The auto-range feature currently eliminates unutilized category ticks on the left/right edge + // BUT keeps it if a data point with null is added; test is almost identical to the one above + // except that it explicitly adds an axis tick for y + Plotly.plot( + gd, + [ + { + x: ["c", "a", "e", "b", "d", "y"], + y: [15, 11, 12, 13, 14, null] + } + ], + { + xaxis: { + type: "category", + categoryorder: "array", + categoryarray: [ + "y", + "b", + "x", + "a", + "d", + "z", + "e", + "c", + "q", + "k" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 7, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 3, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 6, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 1, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 4, y: 14 }) + ); + + var domTickTexts = Array.prototype.slice + .call(document.querySelectorAll("g.xtick")) + .map(function(e) { + return e.__data__.text; }); - it('should output categories in explicitly supplied order first, if not all categories are covered', function() { - - Plotly.plot(gd, [{x: ['c', 'a', 'e', 'b', 'd'], y: [15, 11, 12, 13, 14]}], { xaxis: { - type: 'category', - categoryorder: 'array', - categoryarray: ['b', 'a', 'x', 'c'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 3, y: 15})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); - expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); - expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 5, y: 14})); + expect(domTickTexts).toEqual([ + "y", + "b", + "x", + "a", + "d", + "z", + "e", + "c" + ]); // q, k has no data; y is null + } + ); + + it( + "should output categories in explicitly supplied order even if not all categories are present, and should interact with a null value orthogonally", + function() { + Plotly.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, null, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "array", + categoryarray: ["b", "x", "a", "d", "z", "e", "c"] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 6, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: false, y: false }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 5, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 0, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 3, y: 14 }) + ); + } + ); + + it( + "should output categories in explicitly supplied order first, if not all categories are covered", + function() { + Plotly.plot( + gd, + [{ x: ["c", "a", "e", "b", "d"], y: [15, 11, 12, 13, 14] }], + { + xaxis: { + type: "category", + categoryorder: "array", + categoryarray: ["b", "a", "x", "c"] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 3, y: 15 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 4, y: 12 }) + ); + expect(gd.calcdata[0][3]).toEqual( + jasmine.objectContaining({ x: 0, y: 13 }) + ); + expect(gd.calcdata[0][4]).toEqual( + jasmine.objectContaining({ x: 5, y: 14 }) + ); + // The order of the rest is unspecified, no need to check. Alternative: make _both_ categoryorder and + // categories effective; categories would take precedence and the remaining items would be sorted + // based on the categoryorder. This of course means that the mere presence of categories triggers this + // behavior, rather than an explicit 'explicit' categoryorder. + } + ); + }); - // The order of the rest is unspecified, no need to check. Alternative: make _both_ categoryorder and - // categories effective; categories would take precedence and the remaining items would be sorted - // based on the categoryorder. This of course means that the mere presence of categories triggers this - // behavior, rather than an explicit 'explicit' categoryorder. - }); + describe( + "ordering tests in the presence of multiple traces - mutually exclusive", + function() { + it("baseline testing for the unordered, disjunct case", function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Seals"]; + + Plotly.plot(gd, [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ]); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 3, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 4, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 5, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 6, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 7, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 8, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 9, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 10, y: 32 }) + ); }); - describe('ordering tests in the presence of multiple traces - mutually exclusive', function() { - - it('baseline testing for the unordered, disjunct case', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ]); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 10, y: 32})); - }); - - it('category order follows the trace order (even if categoryarray is specified)', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'trace', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 10, y: 32})); - }); - - it('category order is category ascending (even if categoryarray is specified)', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'category ascending', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 10, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 7, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 3, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 1, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 9, y: 32})); - }); - - it('category order is category descending (even if categoryarray is specified)', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'category descending', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 10, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 3, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 8, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 7, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 9, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); - }); - - it('category order follows categoryarray', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'array', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 10, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 3, y: 32})); - }); + it( + "category order follows the trace order (even if categoryarray is specified)", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Seals"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "trace", + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 3, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 4, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 5, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 6, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 7, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 8, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 9, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 10, y: 32 }) + ); + } + ); + + it( + "category order is category ascending (even if categoryarray is specified)", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Seals"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "category ascending", + // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 4, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 6, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 10, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 7, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 3, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 1, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 8, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 5, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 9, y: 32 }) + ); + } + ); + + it( + "category order is category descending (even if categoryarray is specified)", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Seals"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "category descending", + // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 6, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 10, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 4, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 3, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 8, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 7, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 9, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 5, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 32 }) + ); + } + ); + + it("category order follows categoryarray", function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Seals"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "array", + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 9, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 6, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 5, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 8, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 7, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 4, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 10, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 3, y: 32 }) + ); }); - - describe('ordering tests in the presence of multiple traces - partially overlapping', function() { - - it('baseline testing for the unordered, partially overlapping case', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ]); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); - expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 10, y: 33})); - }); - - it('category order follows the trace order (even if categoryarray is specified)', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'trace', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); - expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 10, y: 33})); - }); - - it('category order is category ascending (even if categoryarray is specified)', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'category ascending', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 10, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 7, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 3, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 1, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); - expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 9, y: 33})); - }); - - it('category order is category descending (even if categoryarray is specified)', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'category descending', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 10, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 3, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 8, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 7, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 9, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 10, y: 32})); - expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 1, y: 33})); - }); - - it('category order follows categoryarray', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'array', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 10, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); - expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 3, y: 33})); - }); + } + ); + + describe( + "ordering tests in the presence of multiple traces - partially overlapping", + function() { + it( + "baseline testing for the unordered, partially overlapping case", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Bearing", "Seals"]; + + Plotly.plot(gd, [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ]); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 3, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 4, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 5, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 6, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 7, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 8, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 9, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 32 }) + ); + expect(gd.calcdata[2][3]).toEqual( + jasmine.objectContaining({ x: 10, y: 33 }) + ); + } + ); + + it( + "category order follows the trace order (even if categoryarray is specified)", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Bearing", "Seals"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "trace", + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 3, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 4, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 5, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 6, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 7, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 8, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 9, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 32 }) + ); + expect(gd.calcdata[2][3]).toEqual( + jasmine.objectContaining({ x: 10, y: 33 }) + ); + } + ); + + it( + "category order is category ascending (even if categoryarray is specified)", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Bearing", "Seals"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "category ascending", + // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 4, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 6, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 10, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 7, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 3, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 1, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 8, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 5, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 0, y: 32 }) + ); + expect(gd.calcdata[2][3]).toEqual( + jasmine.objectContaining({ x: 9, y: 33 }) + ); + } + ); + + it( + "category order is category descending (even if categoryarray is specified)", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Bearing", "Seals"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "category descending", + // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 6, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 10, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 4, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 3, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 8, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 7, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 9, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 5, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 10, y: 32 }) + ); + expect(gd.calcdata[2][3]).toEqual( + jasmine.objectContaining({ x: 1, y: 33 }) + ); + } + ); + + it("category order follows categoryarray", function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Bearing", "Seals"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "array", + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 9, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 6, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 5, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 8, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 7, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 4, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 10, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 32 }) + ); + expect(gd.calcdata[2][3]).toEqual( + jasmine.objectContaining({ x: 3, y: 33 }) + ); }); - - describe('ordering tests in the presence of multiple traces - fully overlapping', function() { - - it('baseline testing for the unordered, fully overlapping case', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Bearing', 'Gear', 'Motor']; - var x3 = ['Motor', 'Gear', 'Bearing']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ]); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 0, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 0, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); - }); - - it('category order follows the trace order (even if categoryarray is specified)', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Bearing', 'Gear', 'Motor']; - var x3 = ['Motor', 'Gear', 'Bearing']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'trace', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 0, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 0, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); - }); - - it('category order is category ascending (even if categoryarray is specified)', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Bearing', 'Gear', 'Motor']; - var x3 = ['Motor', 'Gear', 'Bearing']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'category ascending', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 1, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 1, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); - }); - - it('category order is category descending (even if categoryarray is specified)', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Bearing', 'Gear', 'Motor']; - var x3 = ['Motor', 'Gear', 'Bearing']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'category descending', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] - }}); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 2, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 0, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 2, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 1, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 0, y: 22})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 0, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 1, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 2, y: 32})); - }); - - it('category order follows categoryarray', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Bearing', 'Gear', 'Motor']; - var x3 = ['Motor', 'Gear', 'Bearing']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { - xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'array', - categoryarray: ['Bearing', 'Motor', 'Gear'] - } - }); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 1, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 2, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 1, y: 22})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 1, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 2, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); - }); - - it('category order follows categoryarray even if data is sparse', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Bearing', 'Gear', 'Motor']; - var x3 = ['Motor', 'Gear', 'Bearing']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;})}, - {x: x2, y: x2.map(function(d, i) {return i + 20;})}, - {x: x3, y: x3.map(function(d, i) {return i + 30;})} - ], { - xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'array', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - } - }); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 9, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); - }); + } + ); + + describe( + "ordering tests in the presence of multiple traces - fully overlapping", + function() { + it( + "baseline testing for the unordered, fully overlapping case", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Bearing", "Gear", "Motor"]; + var x3 = ["Motor", "Gear", "Bearing"]; + + Plotly.plot(gd, [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ]); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 1, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 22 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 32 }) + ); + } + ); + + it( + "category order follows the trace order (even if categoryarray is specified)", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Bearing", "Gear", "Motor"]; + var x3 = ["Motor", "Gear", "Bearing"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "trace", + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 1, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 22 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 32 }) + ); + } + ); + + it( + "category order is category ascending (even if categoryarray is specified)", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Bearing", "Gear", "Motor"]; + var x3 = ["Motor", "Gear", "Bearing"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "category ascending", + // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 1, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 22 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 0, y: 32 }) + ); + } + ); + + it( + "category order is category descending (even if categoryarray is specified)", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Bearing", "Gear", "Motor"]; + var x3 = ["Motor", "Gear", "Bearing"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "category descending", + // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 1, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 2, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 0, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 0, y: 22 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 32 }) + ); + } + ); + + it("category order follows categoryarray", function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Bearing", "Gear", "Motor"]; + var x3 = ["Motor", "Gear", "Bearing"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "array", + categoryarray: ["Bearing", "Motor", "Gear"] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 2, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 22 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 1, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 2, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 0, y: 32 }) + ); }); - describe('ordering and stacking combined', function() { - - it('partially overlapping category order follows categoryarray and stacking produces expected results', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; - var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;}), type: 'bar'}, - {x: x2, y: x2.map(function(d, i) {return i + 20;}), type: 'bar'}, - {x: x3, y: x3.map(function(d, i) {return i + 30;}), type: 'bar'} - ], { - barmode: 'stack', - xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'array', - categoryarray: ['Switch', 'Bearing', 'Motor', 'Seals', 'Pump', 'Cord', 'Plug', 'Bulb', 'Fuse', 'Gear', 'Leak'] - } - }); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 10, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 11 + 32})); - expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 3, y: 33})); - }); - - it('fully overlapping - category order follows categoryarray and stacking produces expected results', function() { - - var x1 = ['Gear', 'Bearing', 'Motor']; - var x2 = ['Bearing', 'Gear', 'Motor']; - var x3 = ['Motor', 'Gear', 'Bearing']; - - Plotly.plot(gd, [ - {x: x1, y: x1.map(function(d, i) {return i + 10;}), type: 'bar'}, - {x: x2, y: x2.map(function(d, i) {return i + 20;}), type: 'bar'}, - {x: x3, y: x3.map(function(d, i) {return i + 30;}), type: 'bar'} - ], { - barmode: 'stack', - xaxis: { - // type: 'category', // commented out to rely on autotyping for added realism - categoryorder: 'array', - categoryarray: ['Bearing', 'Motor', 'Gear'] - } - }); - - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 1, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 11 + 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 2, y: 10 + 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 1, y: 12 + 22})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 1, y: 12 + 22 + 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 2, y: 10 + 21 + 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 11 + 20 + 32})); - }); - }); + it( + "category order follows categoryarray even if data is sparse", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Bearing", "Gear", "Motor"]; + var x3 = ["Motor", "Gear", "Bearing"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }) + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }) + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }) + } + ], + { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "array", + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 9, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 1, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 9, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 22 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 9, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 32 }) + ); + } + ); + } + ); + + describe("ordering and stacking combined", function() { + it( + "partially overlapping category order follows categoryarray and stacking produces expected results", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Switch", "Plug", "Cord", "Fuse", "Bulb"]; + var x3 = ["Pump", "Leak", "Bearing", "Seals"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }), + type: "bar" + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }), + type: "bar" + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }), + type: "bar" + } + ], + { + barmode: "stack", + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "array", + categoryarray: [ + "Switch", + "Bearing", + "Motor", + "Seals", + "Pump", + "Cord", + "Plug", + "Bulb", + "Fuse", + "Gear", + "Leak" + ] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 9, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 2, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 6, y: 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 5, y: 22 }) + ); + expect(gd.calcdata[1][3]).toEqual( + jasmine.objectContaining({ x: 8, y: 23 }) + ); + expect(gd.calcdata[1][4]).toEqual( + jasmine.objectContaining({ x: 7, y: 24 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 4, y: 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 10, y: 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 11 + 32 }) + ); + expect(gd.calcdata[2][3]).toEqual( + jasmine.objectContaining({ x: 3, y: 33 }) + ); + } + ); + + it( + "fully overlapping - category order follows categoryarray and stacking produces expected results", + function() { + var x1 = ["Gear", "Bearing", "Motor"]; + var x2 = ["Bearing", "Gear", "Motor"]; + var x3 = ["Motor", "Gear", "Bearing"]; + + Plotly.plot( + gd, + [ + { + x: x1, + y: x1.map(function(d, i) { + return i + 10; + }), + type: "bar" + }, + { + x: x2, + y: x2.map(function(d, i) { + return i + 20; + }), + type: "bar" + }, + { + x: x3, + y: x3.map(function(d, i) { + return i + 30; + }), + type: "bar" + } + ], + { + barmode: "stack", + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categoryorder: "array", + categoryarray: ["Bearing", "Motor", "Gear"] + } + } + ); + + expect(gd.calcdata[0][0]).toEqual( + jasmine.objectContaining({ x: 2, y: 10 }) + ); + expect(gd.calcdata[0][1]).toEqual( + jasmine.objectContaining({ x: 0, y: 11 }) + ); + expect(gd.calcdata[0][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 12 }) + ); + + expect(gd.calcdata[1][0]).toEqual( + jasmine.objectContaining({ x: 0, y: 11 + 20 }) + ); + expect(gd.calcdata[1][1]).toEqual( + jasmine.objectContaining({ x: 2, y: 10 + 21 }) + ); + expect(gd.calcdata[1][2]).toEqual( + jasmine.objectContaining({ x: 1, y: 12 + 22 }) + ); + + expect(gd.calcdata[2][0]).toEqual( + jasmine.objectContaining({ x: 1, y: 12 + 22 + 30 }) + ); + expect(gd.calcdata[2][1]).toEqual( + jasmine.objectContaining({ x: 2, y: 10 + 21 + 31 }) + ); + expect(gd.calcdata[2][2]).toEqual( + jasmine.objectContaining({ x: 0, y: 11 + 20 + 32 }) + ); + } + ); }); + }); }); diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js index d3977090fd2..4ea8afa5cee 100644 --- a/test/jasmine/tests/cartesian_test.js +++ b/test/jasmine/tests/cartesian_test.js @@ -1,308 +1,316 @@ -var d3 = require('d3'); +var d3 = require("d3"); -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); -var Drawing = require('@src/components/drawing'); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); +var Drawing = require("@src/components/drawing"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); +describe("zoom box element", function() { + var mock = require("@mocks/14.json"); -describe('zoom box element', function() { - var mock = require('@mocks/14.json'); + var gd; + beforeEach(function(done) { + gd = createGraphDiv(); - var gd; - beforeEach(function(done) { - gd = createGraphDiv(); - - var mockCopy = Lib.extendDeep({}, mock); - mockCopy.layout.dragmode = 'zoom'; + var mockCopy = Lib.extendDeep({}, mock); + mockCopy.layout.dragmode = "zoom"; - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - afterEach(destroyGraphDiv); + afterEach(destroyGraphDiv); - it('should be appended to the zoom layer', function() { - var x0 = 100; - var y0 = 200; - var x1 = 150; - var y1 = 200; - - mouseEvent('mousemove', x0, y0); - expect(d3.selectAll('.zoomlayer > .zoombox').size()) - .toEqual(0); - expect(d3.selectAll('.zoomlayer > .zoombox-corners').size()) - .toEqual(0); - - mouseEvent('mousedown', x0, y0); - mouseEvent('mousemove', x1, y1); - expect(d3.selectAll('.zoomlayer > .zoombox').size()) - .toEqual(1); - expect(d3.selectAll('.zoomlayer > .zoombox-corners').size()) - .toEqual(1); - - mouseEvent('mouseup', x1, y1); - expect(d3.selectAll('.zoomlayer > .zoombox').size()) - .toEqual(0); - expect(d3.selectAll('.zoomlayer > .zoombox-corners').size()) - .toEqual(0); - }); -}); + it("should be appended to the zoom layer", function() { + var x0 = 100; + var y0 = 200; + var x1 = 150; + var y1 = 200; -describe('restyle', function() { - describe('scatter traces', function() { - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - it('reuses SVG fills', function(done) { - var fills, firstToZero, secondToZero, firstToNext, secondToNext; - var mock = Lib.extendDeep({}, require('@mocks/basic_area.json')); - - Plotly.plot(gd, mock.data, mock.layout).then(function() { - // Assert there are two fills: - fills = d3.selectAll('g.trace.scatter .js-fill')[0]; - - // First is tozero, second is tonext: - expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(2); - expect(fills[0].classList.contains('js-tozero')).toBe(true); - expect(fills[0].classList.contains('js-tonext')).toBe(false); - expect(fills[1].classList.contains('js-tozero')).toBe(false); - expect(fills[1].classList.contains('js-tonext')).toBe(true); - - firstToZero = fills[0]; - firstToNext = fills[1]; - }).then(function() { - return Plotly.restyle(gd, {visible: [false]}, [1]); - }).then(function() { - // Trace 1 hidden leaves only trace zero's tozero fill: - expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(1); - expect(fills[0].classList.contains('js-tozero')).toBe(true); - expect(fills[0].classList.contains('js-tonext')).toBe(false); - }).then(function() { - return Plotly.restyle(gd, {visible: [true]}, [1]); - }).then(function() { - // Reshow means two fills again AND order is preserved: - fills = d3.selectAll('g.trace.scatter .js-fill')[0]; - - // First is tozero, second is tonext: - expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(2); - expect(fills[0].classList.contains('js-tozero')).toBe(true); - expect(fills[0].classList.contains('js-tonext')).toBe(false); - expect(fills[1].classList.contains('js-tozero')).toBe(false); - expect(fills[1].classList.contains('js-tonext')).toBe(true); - - secondToZero = fills[0]; - secondToNext = fills[1]; - - // The identity of the first is retained: - expect(firstToZero).toBe(secondToZero); - - // The second has been recreated so is different: - expect(firstToNext).not.toBe(secondToNext); - - return Plotly.restyle(gd, 'visible', false); - }).then(function() { - expect(d3.selectAll('g.trace.scatter').size()).toEqual(0); - - }).then(done); - }); - - it('reuses SVG lines', function(done) { - var lines, firstLine1, secondLine1, firstLine2, secondLine2; - var mock = Lib.extendDeep({}, require('@mocks/basic_line.json')); - - Plotly.plot(gd, mock.data, mock.layout).then(function() { - lines = d3.selectAll('g.scatter.trace .js-line'); - - firstLine1 = lines[0][0]; - firstLine2 = lines[0][1]; - - // One line for each trace: - expect(lines.size()).toEqual(2); - }).then(function() { - return Plotly.restyle(gd, {visible: [false]}, [0]); - }).then(function() { - lines = d3.selectAll('g.scatter.trace .js-line'); - - // Only one line now and it's equal to the second trace's line from above: - expect(lines.size()).toEqual(1); - expect(lines[0][0]).toBe(firstLine2); - }).then(function() { - return Plotly.restyle(gd, {visible: [true]}, [0]); - }).then(function() { - lines = d3.selectAll('g.scatter.trace .js-line'); - secondLine1 = lines[0][0]; - secondLine2 = lines[0][1]; - - // Two lines once again: - expect(lines.size()).toEqual(2); - - // First line has been removed and recreated: - expect(firstLine1).not.toBe(secondLine1); - - // Second line was persisted: - expect(firstLine2).toBe(secondLine2); - }).then(done); - }); - - it('can change scatter mode', function(done) { - var mock = Lib.extendDeep({}, require('@mocks/text_chart_basic.json')); - - function assertScatterModeSizes(lineSize, pointSize, textSize) { - var gd3 = d3.select(gd), - lines = gd3.selectAll('g.scatter.trace .js-line'), - points = gd3.selectAll('g.scatter.trace path.point'), - texts = gd3.selectAll('g.scatter.trace text'); - - expect(lines.size()).toEqual(lineSize); - expect(points.size()).toEqual(pointSize); - expect(texts.size()).toEqual(textSize); - } - - Plotly.plot(gd, mock.data, mock.layout).then(function() { - assertScatterModeSizes(2, 6, 9); - - return Plotly.restyle(gd, 'mode', 'lines'); - }) - .then(function() { - assertScatterModeSizes(3, 0, 0); - - return Plotly.restyle(gd, 'mode', 'markers'); - }) - .then(function() { - assertScatterModeSizes(0, 9, 0); - - return Plotly.restyle(gd, 'mode', 'markers+text'); - }) - .then(function() { - assertScatterModeSizes(0, 9, 9); - - return Plotly.restyle(gd, 'mode', 'text'); - }) - .then(function() { - assertScatterModeSizes(0, 0, 9); - - return Plotly.restyle(gd, 'mode', 'markers+text+lines'); - }) - .then(function() { - assertScatterModeSizes(3, 9, 9); - }) - .then(done); - - }); - }); -}); + mouseEvent("mousemove", x0, y0); + expect(d3.selectAll(".zoomlayer > .zoombox").size()).toEqual(0); + expect(d3.selectAll(".zoomlayer > .zoombox-corners").size()).toEqual(0); -describe('relayout', function() { + mouseEvent("mousedown", x0, y0); + mouseEvent("mousemove", x1, y1); + expect(d3.selectAll(".zoomlayer > .zoombox").size()).toEqual(1); + expect(d3.selectAll(".zoomlayer > .zoombox-corners").size()).toEqual(1); - describe('axis category attributes', function() { - var mock = require('@mocks/basic_bar.json'); + mouseEvent("mouseup", x1, y1); + expect(d3.selectAll(".zoomlayer > .zoombox").size()).toEqual(0); + expect(d3.selectAll(".zoomlayer > .zoombox-corners").size()).toEqual(0); + }); +}); - var gd, mockCopy; +describe("restyle", function() { + describe("scatter traces", function() { + var gd; - beforeEach(function() { - mockCopy = Lib.extendDeep({}, mock); - gd = createGraphDiv(); - }); + beforeEach(function() { + gd = createGraphDiv(); + }); - afterEach(destroyGraphDiv); + afterEach(destroyGraphDiv); - it('should response to \'categoryarray\' and \'categoryorder\' updates', function(done) { - function assertCategories(list) { - d3.selectAll('g.xtick').each(function(_, i) { - var tick = d3.select(this).select('text'); - expect(tick.html()).toEqual(list[i]); - }); - } + it("reuses SVG fills", function(done) { + var fills, firstToZero, secondToZero, firstToNext, secondToNext; + var mock = Lib.extendDeep({}, require("@mocks/basic_area.json")); + + Plotly.plot(gd, mock.data, mock.layout) + .then(function() { + // Assert there are two fills: + fills = d3.selectAll("g.trace.scatter .js-fill")[0]; + + // First is tozero, second is tonext: + expect(d3.selectAll("g.trace.scatter .js-fill").size()).toEqual(2); + expect(fills[0].classList.contains("js-tozero")).toBe(true); + expect(fills[0].classList.contains("js-tonext")).toBe(false); + expect(fills[1].classList.contains("js-tozero")).toBe(false); + expect(fills[1].classList.contains("js-tonext")).toBe(true); + + firstToZero = fills[0]; + firstToNext = fills[1]; + }) + .then(function() { + return Plotly.restyle(gd, { visible: [false] }, [1]); + }) + .then(function() { + // Trace 1 hidden leaves only trace zero's tozero fill: + expect(d3.selectAll("g.trace.scatter .js-fill").size()).toEqual(1); + expect(fills[0].classList.contains("js-tozero")).toBe(true); + expect(fills[0].classList.contains("js-tonext")).toBe(false); + }) + .then(function() { + return Plotly.restyle(gd, { visible: [true] }, [1]); + }) + .then(function() { + // Reshow means two fills again AND order is preserved: + fills = d3.selectAll("g.trace.scatter .js-fill")[0]; + + // First is tozero, second is tonext: + expect(d3.selectAll("g.trace.scatter .js-fill").size()).toEqual(2); + expect(fills[0].classList.contains("js-tozero")).toBe(true); + expect(fills[0].classList.contains("js-tonext")).toBe(false); + expect(fills[1].classList.contains("js-tozero")).toBe(false); + expect(fills[1].classList.contains("js-tonext")).toBe(true); + + secondToZero = fills[0]; + secondToNext = fills[1]; + + // The identity of the first is retained: + expect(firstToZero).toBe(secondToZero); + + // The second has been recreated so is different: + expect(firstToNext).not.toBe(secondToNext); + + return Plotly.restyle(gd, "visible", false); + }) + .then(function() { + expect(d3.selectAll("g.trace.scatter").size()).toEqual(0); + }) + .then(done); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - assertCategories(['giraffes', 'orangutans', 'monkeys']); + it("reuses SVG lines", function(done) { + var lines, firstLine1, secondLine1, firstLine2, secondLine2; + var mock = Lib.extendDeep({}, require("@mocks/basic_line.json")); + + Plotly.plot(gd, mock.data, mock.layout) + .then(function() { + lines = d3.selectAll("g.scatter.trace .js-line"); + + firstLine1 = lines[0][0]; + firstLine2 = lines[0][1]; + + // One line for each trace: + expect(lines.size()).toEqual(2); + }) + .then(function() { + return Plotly.restyle(gd, { visible: [false] }, [0]); + }) + .then(function() { + lines = d3.selectAll("g.scatter.trace .js-line"); + + // Only one line now and it's equal to the second trace's line from above: + expect(lines.size()).toEqual(1); + expect(lines[0][0]).toBe(firstLine2); + }) + .then(function() { + return Plotly.restyle(gd, { visible: [true] }, [0]); + }) + .then(function() { + lines = d3.selectAll("g.scatter.trace .js-line"); + secondLine1 = lines[0][0]; + secondLine2 = lines[0][1]; + + // Two lines once again: + expect(lines.size()).toEqual(2); + + // First line has been removed and recreated: + expect(firstLine1).not.toBe(secondLine1); + + // Second line was persisted: + expect(firstLine2).toBe(secondLine2); + }) + .then(done); + }); - return Plotly.relayout(gd, 'xaxis.categoryorder', 'category descending'); - }).then(function() { - var list = ['orangutans', 'monkeys', 'giraffes']; + it("can change scatter mode", function(done) { + var mock = Lib.extendDeep({}, require("@mocks/text_chart_basic.json")); + + function assertScatterModeSizes(lineSize, pointSize, textSize) { + var gd3 = d3.select(gd), + lines = gd3.selectAll("g.scatter.trace .js-line"), + points = gd3.selectAll("g.scatter.trace path.point"), + texts = gd3.selectAll("g.scatter.trace text"); + + expect(lines.size()).toEqual(lineSize); + expect(points.size()).toEqual(pointSize); + expect(texts.size()).toEqual(textSize); + } + + Plotly.plot(gd, mock.data, mock.layout) + .then(function() { + assertScatterModeSizes(2, 6, 9); + + return Plotly.restyle(gd, "mode", "lines"); + }) + .then(function() { + assertScatterModeSizes(3, 0, 0); + + return Plotly.restyle(gd, "mode", "markers"); + }) + .then(function() { + assertScatterModeSizes(0, 9, 0); + + return Plotly.restyle(gd, "mode", "markers+text"); + }) + .then(function() { + assertScatterModeSizes(0, 9, 9); + + return Plotly.restyle(gd, "mode", "text"); + }) + .then(function() { + assertScatterModeSizes(0, 0, 9); + + return Plotly.restyle(gd, "mode", "markers+text+lines"); + }) + .then(function() { + assertScatterModeSizes(3, 9, 9); + }) + .then(done); + }); + }); +}); - expect(gd._fullLayout.xaxis._initialCategories).toEqual(list); - assertCategories(list); +describe("relayout", function() { + describe("axis category attributes", function() { + var mock = require("@mocks/basic_bar.json"); - return Plotly.relayout(gd, 'xaxis.categoryorder', null); - }).then(function() { - assertCategories(['giraffes', 'orangutans', 'monkeys']); + var gd, mockCopy; - return Plotly.relayout(gd, { - 'xaxis.categoryarray': ['monkeys', 'giraffes', 'orangutans'] - }); - }).then(function() { - var list = ['monkeys', 'giraffes', 'orangutans']; + beforeEach(function() { + mockCopy = Lib.extendDeep({}, mock); + gd = createGraphDiv(); + }); - expect(gd.layout.xaxis.categoryarray).toEqual(list); - expect(gd._fullLayout.xaxis.categoryarray).toEqual(list); - expect(gd._fullLayout.xaxis._initialCategories).toEqual(list); - assertCategories(list); + afterEach(destroyGraphDiv); - done(); + it( + "should response to 'categoryarray' and 'categoryorder' updates", + function(done) { + function assertCategories(list) { + d3.selectAll("g.xtick").each(function(_, i) { + var tick = d3.select(this).select("text"); + expect(tick.html()).toEqual(list[i]); + }); + } + + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + assertCategories(["giraffes", "orangutans", "monkeys"]); + + return Plotly.relayout( + gd, + "xaxis.categoryorder", + "category descending" + ); + }) + .then(function() { + var list = ["orangutans", "monkeys", "giraffes"]; + + expect(gd._fullLayout.xaxis._initialCategories).toEqual(list); + assertCategories(list); + + return Plotly.relayout(gd, "xaxis.categoryorder", null); + }) + .then(function() { + assertCategories(["giraffes", "orangutans", "monkeys"]); + + return Plotly.relayout(gd, { + "xaxis.categoryarray": ["monkeys", "giraffes", "orangutans"] }); - }); + }) + .then(function() { + var list = ["monkeys", "giraffes", "orangutans"]; + + expect(gd.layout.xaxis.categoryarray).toEqual(list); + expect(gd._fullLayout.xaxis.categoryarray).toEqual(list); + expect(gd._fullLayout.xaxis._initialCategories).toEqual(list); + assertCategories(list); + + done(); + }); + } + ); + }); + + describe("axis ranges", function() { + var gd; + beforeEach(function() { + gd = createGraphDiv(); }); - describe('axis ranges', function() { - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - it('should translate points and text element', function(done) { - var mockData = [{ - x: [1], - y: [1], - text: ['A'], - mode: 'markers+text' - }]; + afterEach(destroyGraphDiv); - function assertPointTranslate(pointT, textT) { - var TOLERANCE = 10; + it("should translate points and text element", function(done) { + var mockData = [{ x: [1], y: [1], text: ["A"], mode: "markers+text" }]; - var gd3 = d3.select(gd), - points = gd3.selectAll('g.scatter.trace path.point'), - texts = gd3.selectAll('g.scatter.trace text'); + function assertPointTranslate(pointT, textT) { + var TOLERANCE = 10; - expect(points.size()).toEqual(1); - expect(texts.size()).toEqual(1); + var gd3 = d3.select(gd), + points = gd3.selectAll("g.scatter.trace path.point"), + texts = gd3.selectAll("g.scatter.trace text"); - expect(points.attr('x')).toBe(null); - expect(points.attr('y')).toBe(null); - expect(texts.attr('transform')).toBe(null); + expect(points.size()).toEqual(1); + expect(texts.size()).toEqual(1); - var translate = Drawing.getTranslate(points); - expect(Math.abs(translate.x - pointT[0])).toBeLessThan(TOLERANCE); - expect(Math.abs(translate.y - pointT[1])).toBeLessThan(TOLERANCE); + expect(points.attr("x")).toBe(null); + expect(points.attr("y")).toBe(null); + expect(texts.attr("transform")).toBe(null); - expect(Math.abs(texts.attr('x') - textT[0])).toBeLessThan(TOLERANCE); - expect(Math.abs(texts.attr('y') - textT[1])).toBeLessThan(TOLERANCE); - } + var translate = Drawing.getTranslate(points); + expect(Math.abs(translate.x - pointT[0])).toBeLessThan(TOLERANCE); + expect(Math.abs(translate.y - pointT[1])).toBeLessThan(TOLERANCE); - Plotly.plot(gd, mockData).then(function() { - assertPointTranslate([270, 135], [270, 135]); + expect(Math.abs(texts.attr("x") - textT[0])).toBeLessThan(TOLERANCE); + expect(Math.abs(texts.attr("y") - textT[1])).toBeLessThan(TOLERANCE); + } - return Plotly.relayout(gd, 'xaxis.range', [2, 3]); - }) - .then(function() { - assertPointTranslate([-540, 135], [-540, 135]); - }) - .then(done); - }); + Plotly.plot(gd, mockData) + .then(function() { + assertPointTranslate([270, 135], [270, 135]); + return Plotly.relayout(gd, "xaxis.range", [2, 3]); + }) + .then(function() { + assertPointTranslate([-540, 135], [-540, 135]); + }) + .then(done); }); - + }); }); diff --git a/test/jasmine/tests/choropleth_test.js b/test/jasmine/tests/choropleth_test.js index 1cdcdcd28aa..9e5c7a5a852 100644 --- a/test/jasmine/tests/choropleth_test.js +++ b/test/jasmine/tests/choropleth_test.js @@ -1,51 +1,36 @@ -var Choropleth = require('@src/traces/choropleth'); -var Plots = require('@src/plots/plots'); +var Choropleth = require("@src/traces/choropleth"); +var Plots = require("@src/plots/plots"); +describe("Test choropleth", function() { + "use strict"; + describe("supplyDefaults", function() { + var traceIn, traceOut; -describe('Test choropleth', function() { - 'use strict'; + var defaultColor = "#444", layout = { font: Plots.layoutAttributes.font }; - describe('supplyDefaults', function() { - var traceIn, - traceOut; - - var defaultColor = '#444', - layout = { - font: Plots.layoutAttributes.font - }; - - beforeEach(function() { - traceOut = {}; - }); - - it('should slice z if it is longer than locations', function() { - traceIn = { - locations: ['CAN', 'USA'], - z: [1, 2, 3] - }; + beforeEach(function() { + traceOut = {}; + }); - Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.z).toEqual([1, 2]); - }); + it("should slice z if it is longer than locations", function() { + traceIn = { locations: ["CAN", "USA"], z: [1, 2, 3] }; - it('should make trace invisible if locations is not defined', function() { - traceIn = { - z: [1, 2, 3] - }; + Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.z).toEqual([1, 2]); + }); - Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); + it("should make trace invisible if locations is not defined", function() { + traceIn = { z: [1, 2, 3] }; - it('should make trace invisible if z is not an array', function() { - traceIn = { - z: 'no gonna work' - }; + Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); - Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); + it("should make trace invisible if z is not an array", function() { + traceIn = { z: "no gonna work" }; + Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); }); - + }); }); diff --git a/test/jasmine/tests/click_test.js b/test/jasmine/tests/click_test.js index 88a32102dcd..baecb4aeab6 100644 --- a/test/jasmine/tests/click_test.js +++ b/test/jasmine/tests/click_test.js @@ -1,857 +1,1078 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); -var Drawing = require('@src/components/drawing'); -var DBLCLICKDELAY = require('@src/plots/cartesian/constants').DBLCLICKDELAY; - -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); -var getRectCenter = require('../assets/get_rect_center'); -var customMatchers = require('../assets/custom_matchers'); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); +var Drawing = require("@src/components/drawing"); +var DBLCLICKDELAY = require("@src/plots/cartesian/constants").DBLCLICKDELAY; + +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); +var getRectCenter = require("../assets/get_rect_center"); +var customMatchers = require("../assets/custom_matchers"); // cartesian click events events use the hover data // from the mousemove events and then simulate // a click event on mouseup -var click = require('../assets/click'); -var doubleClick = require('../assets/double_click'); +var click = require("../assets/click"); +var doubleClick = require("../assets/double_click"); +describe("Test click interactions:", function() { + var mock = require("@mocks/14.json"); -describe('Test click interactions:', function() { - var mock = require('@mocks/14.json'); + var mockCopy, gd; - var mockCopy, gd; + var pointPos = [344, 216], blankPos = [63, 356]; - var pointPos = [344, 216], - blankPos = [63, 356]; + var autoRangeX = [-3.011967491973726, 2.1561305597186564], + autoRangeY = [-0.9910086301469277, 1.389382716298284]; - var autoRangeX = [-3.011967491973726, 2.1561305597186564], - autoRangeY = [-0.9910086301469277, 1.389382716298284]; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - beforeEach(function() { - gd = createGraphDiv(); - mockCopy = Lib.extendDeep({}, mock); - }); + beforeEach(function() { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + }); - afterEach(destroyGraphDiv); + afterEach(destroyGraphDiv); - function drag(fromX, fromY, toX, toY, delay) { - return new Promise(function(resolve) { - mouseEvent('mousemove', fromX, fromY); - mouseEvent('mousedown', fromX, fromY); - mouseEvent('mousemove', toX, toY); - - setTimeout(function() { - mouseEvent('mouseup', toX, toY); - resolve(); - }, delay || DBLCLICKDELAY / 4); - }); - } + function drag(fromX, fromY, toX, toY, delay) { + return new Promise(function(resolve) { + mouseEvent("mousemove", fromX, fromY); + mouseEvent("mousedown", fromX, fromY); + mouseEvent("mousemove", toX, toY); - describe('click events', function() { - var futureData; + setTimeout( + function() { + mouseEvent("mouseup", toX, toY); + resolve(); + }, + delay || DBLCLICKDELAY / 4 + ); + }); + } - beforeEach(function(done) { - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + describe("click events", function() { + var futureData; - gd.on('plotly_click', function(data) { - futureData = data; - }); - }); + beforeEach(function(done) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - it('should not be trigged when not on data points', function() { - click(blankPos[0], blankPos[1]); - expect(futureData).toBe(undefined); - }); + gd.on("plotly_click", function(data) { + futureData = data; + }); + }); - it('should contain the correct fields', function() { - click(pointPos[0], pointPos[1]); - expect(futureData.points.length).toEqual(1); + it("should not be trigged when not on data points", function() { + click(blankPos[0], blankPos[1]); + expect(futureData).toBe(undefined); + }); - var pt = futureData.points[0]; - expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'x', 'y', 'xaxis', 'yaxis' - ]); - expect(pt.curveNumber).toEqual(0); - expect(pt.pointNumber).toEqual(11); - expect(pt.x).toEqual(0.125); - expect(pt.y).toEqual(2.125); - }); + it("should contain the correct fields", function() { + click(pointPos[0], pointPos[1]); + expect(futureData.points.length).toEqual(1); + + var pt = futureData.points[0]; + expect(Object.keys(pt)).toEqual([ + "data", + "fullData", + "curveNumber", + "pointNumber", + "x", + "y", + "xaxis", + "yaxis" + ]); + expect(pt.curveNumber).toEqual(0); + expect(pt.pointNumber).toEqual(11); + expect(pt.x).toEqual(0.125); + expect(pt.y).toEqual(2.125); }); + }); - describe('click event with hoverinfo set to skip - plotly_click', function() { - var futureData = null; + describe("click event with hoverinfo set to skip - plotly_click", function() { + var futureData = null; - beforeEach(function(done) { + beforeEach(function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + modifiedMockCopy.data[0].hoverinfo = "skip"; + Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout).then( + done + ); - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - modifiedMockCopy.data[0].hoverinfo = 'skip'; - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - .then(done); + gd.on("plotly_click", function(data) { + futureData = data; + }); + }); - gd.on('plotly_click', function(data) { - futureData = data; - }); + it("should not register the click", function() { + click(pointPos[0], pointPos[1]); + expect(futureData).toEqual(null); + }); + }); + + describe( + "click events with hoverinfo set to skip - plotly_hover", + function() { + var futureData = null; + + beforeEach(function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + modifiedMockCopy.data[0].hoverinfo = "skip"; + Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout).then( + done + ); + + gd.on("plotly_hover", function(data) { + futureData = data; }); + }); - it('should not register the click', function() { - click(pointPos[0], pointPos[1]); - expect(futureData).toEqual(null); - }); - }); + it("should not register the hover", function() { + click(pointPos[0], pointPos[1]); + expect(futureData).toEqual(null); + }); + } + ); - describe('click events with hoverinfo set to skip - plotly_hover', function() { - var futureData = null; + describe("click event with hoverinfo set to none - plotly_click", function() { + var futureData; - beforeEach(function(done) { + beforeEach(function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + modifiedMockCopy.data[0].hoverinfo = "none"; + Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout).then( + done + ); - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - modifiedMockCopy.data[0].hoverinfo = 'skip'; - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - .then(done); + gd.on("plotly_click", function(data) { + futureData = data; + }); + }); - gd.on('plotly_hover', function(data) { - futureData = data; - }); + it( + 'should contain the correct fields despite hoverinfo: "none"', + function() { + click(pointPos[0], pointPos[1]); + expect(futureData.points.length).toEqual(1); + + var pt = futureData.points[0]; + expect(Object.keys(pt)).toEqual([ + "data", + "fullData", + "curveNumber", + "pointNumber", + "x", + "y", + "xaxis", + "yaxis" + ]); + expect(pt.curveNumber).toEqual(0); + expect(pt.pointNumber).toEqual(11); + expect(pt.x).toEqual(0.125); + expect(pt.y).toEqual(2.125); + } + ); + }); + + describe( + "click events with hoverinfo set to none - plotly_hover", + function() { + var futureData; + + beforeEach(function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + modifiedMockCopy.data[0].hoverinfo = "none"; + Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout).then( + done + ); + + gd.on("plotly_hover", function(data) { + futureData = data; }); + }); + + it( + 'should contain the correct fields despite hoverinfo: "none"', + function() { + click(pointPos[0], pointPos[1]); + expect(futureData.points.length).toEqual(1); + + var pt = futureData.points[0]; + expect(Object.keys(pt)).toEqual([ + "data", + "fullData", + "curveNumber", + "pointNumber", + "x", + "y", + "xaxis", + "yaxis" + ]); + expect(pt.curveNumber).toEqual(0); + expect(pt.pointNumber).toEqual(11); + expect(pt.x).toEqual(0.125); + expect(pt.y).toEqual(2.125); + } + ); + } + ); - it('should not register the hover', function() { - click(pointPos[0], pointPos[1]); - expect(futureData).toEqual(null); - }); - }); + describe("double click events", function() { + var futureData; - describe('click event with hoverinfo set to none - plotly_click', function() { - var futureData; + beforeEach(function(done) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - beforeEach(function(done) { + gd.on("plotly_doubleclick", function(data) { + futureData = data; + }); + }); - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - modifiedMockCopy.data[0].hoverinfo = 'none'; - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - .then(done); + it("should return null", function(done) { + doubleClick(pointPos[0], pointPos[1]).then(function() { + expect(futureData).toBe(null); + done(); + }); + }); + }); - gd.on('plotly_click', function(data) { - futureData = data; - }); - }); + describe("drag interactions", function() { + beforeEach(function(done) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + // Do not let the notifier hide the drag elements + var tooltip = document.querySelector(".notifier-note"); + if (tooltip) tooltip.style.display = "None"; - it('should contain the correct fields despite hoverinfo: "none"', function() { - click(pointPos[0], pointPos[1]); - expect(futureData.points.length).toEqual(1); + done(); + }); + }); - var pt = futureData.points[0]; - expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'x', 'y', 'xaxis', 'yaxis' - ]); - expect(pt.curveNumber).toEqual(0); - expect(pt.pointNumber).toEqual(11); - expect(pt.x).toEqual(0.125); - expect(pt.y).toEqual(2.125); + it("on nw dragbox should update the axis ranges", function(done) { + var node = document.querySelector("rect.nwdrag"); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe("drag"); + expect(node.classList[1]).toBe("nwdrag"); + expect(node.classList[2]).toBe("cursor-nw-resize"); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.08579746, + 2.156130559 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.99100863, + 1.86546098 + ]); + + return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.99100863, + 1.10938115 + ]); + + done(); }); }); - describe('click events with hoverinfo set to none - plotly_hover', function() { - var futureData; - - beforeEach(function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - modifiedMockCopy.data[0].hoverinfo = 'none'; - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - .then(done); - - gd.on('plotly_hover', function(data) { - futureData = data; - }); + it("on ne dragbox should update the axis ranges", function(done) { + var node = document.querySelector("rect.nedrag"); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe("drag"); + expect(node.classList[1]).toBe("nedrag"); + expect(node.classList[2]).toBe("cursor-ne-resize"); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.01196749, + 1.72466470 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.99100863, + 1.86546098 + ]); + + return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.01196749, + 2.08350047 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.99100863, + 1.10938115 + ]); + + done(); }); + }); - it('should contain the correct fields despite hoverinfo: "none"', function() { - click(pointPos[0], pointPos[1]); - expect(futureData.points.length).toEqual(1); - - var pt = futureData.points[0]; - expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'x', 'y', 'xaxis', 'yaxis' - ]); - expect(pt.curveNumber).toEqual(0); - expect(pt.pointNumber).toEqual(11); - expect(pt.x).toEqual(0.125); - expect(pt.y).toEqual(2.125); + it("on sw dragbox should update the axis ranges", function(done) { + var node = document.querySelector("rect.swdrag"); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe("drag"); + expect(node.classList[1]).toBe("swdrag"); + expect(node.classList[2]).toBe("cursor-sw-resize"); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.08579746, + 2.15613055 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.36094210, + 1.38938271 + ]); + + return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.00958227, + 2.15613055 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.71100706, + 1.38938271 + ]); + + done(); }); }); - describe('double click events', function() { - var futureData; - - beforeEach(function(done) { - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - - gd.on('plotly_doubleclick', function(data) { - futureData = data; - }); - + it("on se dragbox should update the axis ranges", function(done) { + var node = document.querySelector("rect.sedrag"); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe("drag"); + expect(node.classList[1]).toBe("sedrag"); + expect(node.classList[2]).toBe("cursor-se-resize"); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.01196749, + 1.72466470 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.36094210, + 1.38938271 + ]); + + return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.01196749, + 2.08350047 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.71100706, + 1.38938271 + ]); + + done(); }); + }); - it('should return null', function(done) { - doubleClick(pointPos[0], pointPos[1]).then(function() { - expect(futureData).toBe(null); - done(); - }); + it("on ew dragbox should update the xaxis range", function(done) { + var node = document.querySelector("rect.ewdrag"); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe("drag"); + expect(node.classList[1]).toBe("ewdrag"); + expect(node.classList[2]).toBe("cursor-ew-resize"); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.375918058, + 1.792179992 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.01196749, + 2.15613055 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + done(); }); }); - describe('drag interactions', function() { - beforeEach(function(done) { - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - // Do not let the notifier hide the drag elements - var tooltip = document.querySelector('.notifier-note'); - if(tooltip) tooltip.style.display = 'None'; - - done(); - }); + it("on w dragbox should update the xaxis range", function(done) { + var node = document.querySelector("rect.wdrag"); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe("drag"); + expect(node.classList[1]).toBe("wdrag"); + expect(node.classList[2]).toBe("cursor-w-resize"); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.40349007, + 2.15613055 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -2.93933740, + 2.15613055 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + done(); }); + }); - it('on nw dragbox should update the axis ranges', function(done) { - var node = document.querySelector('rect.nwdrag'); - var pos = getRectCenter(node); - - expect(node.classList[0]).toBe('drag'); - expect(node.classList[1]).toBe('nwdrag'); - expect(node.classList[2]).toBe('cursor-nw-resize'); - - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.08579746, 2.156130559]); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.99100863, 1.86546098]); - - return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.99100863, 1.10938115]); - - done(); - }); + it("on e dragbox should update the xaxis range", function(done) { + var node = document.querySelector("rect.edrag"); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe("drag"); + expect(node.classList[1]).toBe("edrag"); + expect(node.classList[2]).toBe("cursor-e-resize"); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.01196749, + 1.7246647 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -3.01196749, + 2.0835004 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + done(); }); + }); - it('on ne dragbox should update the axis ranges', function(done) { - var node = document.querySelector('rect.nedrag'); - var pos = getRectCenter(node); + it("on ns dragbox should update the yaxis range", function(done) { + var node = document.querySelector("rect.nsdrag"); + var pos = getRectCenter(node); - expect(node.classList[0]).toBe('drag'); - expect(node.classList[1]).toBe('nedrag'); - expect(node.classList[2]).toBe('cursor-ne-resize'); + expect(node.classList[0]).toBe("drag"); + expect(node.classList[1]).toBe("nsdrag"); + expect(node.classList[2]).toBe("cursor-ns-resize"); - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 1.72466470]); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.99100863, 1.86546098]); + drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.59427673, + 1.78611460 + ]); - return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 2.08350047]); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.99100863, 1.10938115]); + return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - done(); - }); + done(); }); + }); - it('on sw dragbox should update the axis ranges', function(done) { - var node = document.querySelector('rect.swdrag'); - var pos = getRectCenter(node); - - expect(node.classList[0]).toBe('drag'); - expect(node.classList[1]).toBe('swdrag'); - expect(node.classList[2]).toBe('cursor-sw-resize'); - - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.08579746, 2.15613055]); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.36094210, 1.38938271]); - - return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.00958227, 2.15613055]); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.71100706, 1.38938271]); - - done(); - }); + it("on s dragbox should update the yaxis range", function(done) { + var node = document.querySelector("rect.sdrag"); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe("drag"); + expect(node.classList[1]).toBe("sdrag"); + expect(node.classList[2]).toBe("cursor-s-resize"); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.3609421011, + 1.3893827 + ]); + + return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.7110070646, + 1.3893827 + ]); + + done(); }); + }); - it('on se dragbox should update the axis ranges', function(done) { - var node = document.querySelector('rect.sedrag'); - var pos = getRectCenter(node); - - expect(node.classList[0]).toBe('drag'); - expect(node.classList[1]).toBe('sedrag'); - expect(node.classList[2]).toBe('cursor-se-resize'); - - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 1.72466470]); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.36094210, 1.38938271]); - - return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 2.08350047]); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.71100706, 1.38938271]); - - done(); - }); + it("on n dragbox should update the yaxis range", function(done) { + var node = document.querySelector("rect.ndrag"); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe("drag"); + expect(node.classList[1]).toBe("ndrag"); + expect(node.classList[2]).toBe("cursor-n-resize"); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.991008630, + 1.86546098 + ]); + + return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.991008630, + 1.10938115 + ]); + + done(); }); + }); + }); - it('on ew dragbox should update the xaxis range', function(done) { - var node = document.querySelector('rect.ewdrag'); - var pos = getRectCenter(node); - - expect(node.classList[0]).toBe('drag'); - expect(node.classList[1]).toBe('ewdrag'); - expect(node.classList[2]).toBe('cursor-ew-resize'); - - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + describe("double click interactions", function() { + var setRangeX = [-3, 1], setRangeY = [-0.5, 1]; - drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.375918058, 1.792179992]); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + var zoomRangeX = [-2, 0], zoomRangeY = [0, 0.5]; - return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 2.15613055]); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + var update = { + "xaxis.range[0]": zoomRangeX[0], + "xaxis.range[1]": zoomRangeX[1], + "yaxis.range[0]": zoomRangeY[0], + "yaxis.range[1]": zoomRangeY[1] + }; - done(); - }); - }); + function setRanges(mockCopy) { + mockCopy.layout.xaxis.autorange = false; + mockCopy.layout.xaxis.range = setRangeX.slice(); - it('on w dragbox should update the xaxis range', function(done) { - var node = document.querySelector('rect.wdrag'); - var pos = getRectCenter(node); + mockCopy.layout.yaxis.autorange = false; + mockCopy.layout.yaxis.range = setRangeY.slice(); - expect(node.classList[0]).toBe('drag'); - expect(node.classList[1]).toBe('wdrag'); - expect(node.classList[2]).toBe('cursor-w-resize'); + return mockCopy; + } + it( + "when set to 'reset+autorange' (the default) should work when 'autorange' is on", + function(done) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.40349007, 2.15613055]); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-2.93933740, 2.15613055]); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - done(); - }); - }); - - it('on e dragbox should update the xaxis range', function(done) { - var node = document.querySelector('rect.edrag'); - var pos = getRectCenter(node); - - expect(node.classList[0]).toBe('drag'); - expect(node.classList[1]).toBe('edrag'); - expect(node.classList[2]).toBe('cursor-e-resize'); + return Plotly.relayout(gd, update); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 1.7246647]); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 2.0835004]); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - done(); - }); - }); - - it('on ns dragbox should update the yaxis range', function(done) { - var node = document.querySelector('rect.nsdrag'); - var pos = getRectCenter(node); - - expect(node.classList[0]).toBe('drag'); - expect(node.classList[1]).toBe('nsdrag'); - expect(node.classList[2]).toBe('cursor-ns-resize'); - + done(); + }); + } + ); + + it( + "when set to 'reset+autorange' (the default) should reset to set range on double click", + function(done) { + mockCopy = setRanges(mockCopy); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + return Plotly.relayout(gd, update); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + done(); + }); + } + ); + + it( + "when set to 'reset+autorange' (the default) should autosize on 1st double click and reset on 2nd", + function(done) { + mockCopy = setRanges(mockCopy); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.59427673, 1.78611460]); - - return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - done(); - }); - }); - - it('on s dragbox should update the yaxis range', function(done) { - var node = document.querySelector('rect.sdrag'); - var pos = getRectCenter(node); - - expect(node.classList[0]).toBe('drag'); - expect(node.classList[1]).toBe('sdrag'); - expect(node.classList[2]).toBe('cursor-s-resize'); - + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + done(); + }); + } + ); + + it( + "when set to 'reset+autorange' (the default) should autosize on 1st double click and zoom when immediately dragged", + function(done) { + mockCopy = setRanges(mockCopy); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.3609421011, 1.3893827]); - - return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.7110070646, 1.3893827]); + return drag(100, 100, 200, 200, DBLCLICKDELAY / 2); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -2.6480169249531356, + -1.920115790911955 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + 0.4372261777201992, + 1.2306899598686027 + ]); - done(); - }); - }); + done(); + }); + } + ); - it('on n dragbox should update the yaxis range', function(done) { - var node = document.querySelector('rect.ndrag'); - var pos = getRectCenter(node); + it( + "when set to 'reset+autorange' (the default) should follow updated auto ranges", + function(done) { + var updateData = { x: [[1e-4, 0, 1e3]], y: [[30, 0, 30]] }; - expect(node.classList[0]).toBe('drag'); - expect(node.classList[1]).toBe('ndrag'); - expect(node.classList[2]).toBe('cursor-n-resize'); + var newAutoRangeX = [-4.482371794871794, 3.4823717948717943], + newAutoRangeY = [-0.8892256657741471, 1.6689872212461876]; + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.991008630, 1.86546098]); - - return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.991008630, 1.10938115]); - - done(); - }); + return Plotly.relayout(gd, update); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); + + return Plotly.restyle(gd, updateData); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(newAutoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(newAutoRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(newAutoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(newAutoRangeY); + + done(); + }); + } + ); + + it("when set to 'reset' should work when 'autorange' is on", function( + done + ) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout, { doubleClick: "reset" }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + return Plotly.relayout(gd, update); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + done(); }); - }); - describe('double click interactions', function() { - var setRangeX = [-3, 1], - setRangeY = [-0.5, 1]; - - var zoomRangeX = [-2, 0], - zoomRangeY = [0, 0.5]; - - var update = { - 'xaxis.range[0]': zoomRangeX[0], - 'xaxis.range[1]': zoomRangeX[1], - 'yaxis.range[0]': zoomRangeY[0], - 'yaxis.range[1]': zoomRangeY[1] - }; - - function setRanges(mockCopy) { - mockCopy.layout.xaxis.autorange = false; - mockCopy.layout.xaxis.range = setRangeX.slice(); - - mockCopy.layout.yaxis.autorange = false; - mockCopy.layout.yaxis.range = setRangeY.slice(); - - return mockCopy; - } - - it('when set to \'reset+autorange\' (the default) should work when \'autorange\' is on', function(done) { - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - return Plotly.relayout(gd, update); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - done(); - }); - }); - - it('when set to \'reset+autorange\' (the default) should reset to set range on double click', function(done) { - mockCopy = setRanges(mockCopy); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - return Plotly.relayout(gd, update); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - done(); - }); - }); - - it('when set to \'reset+autorange\' (the default) should autosize on 1st double click and reset on 2nd', function(done) { - mockCopy = setRanges(mockCopy); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - done(); - }); - }); - - it('when set to \'reset+autorange\' (the default) should autosize on 1st double click and zoom when immediately dragged', function(done) { - mockCopy = setRanges(mockCopy); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - return drag(100, 100, 200, 200, DBLCLICKDELAY / 2); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-2.6480169249531356, -1.920115790911955]); - expect(gd.layout.yaxis.range).toBeCloseToArray([0.4372261777201992, 1.2306899598686027]); - - done(); - }); - }); - - it('when set to \'reset+autorange\' (the default) should follow updated auto ranges', function(done) { - var updateData = { - x: [[1e-4, 0, 1e3]], - y: [[30, 0, 30]] - }; - - var newAutoRangeX = [-4.482371794871794, 3.4823717948717943], - newAutoRangeY = [-0.8892256657741471, 1.6689872212461876]; - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - return Plotly.relayout(gd, update); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); - - return Plotly.restyle(gd, updateData); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(newAutoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(newAutoRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(newAutoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(newAutoRangeY); - - done(); - }); - }); - - it('when set to \'reset\' should work when \'autorange\' is on', function(done) { - Plotly.plot(gd, mockCopy.data, mockCopy.layout, { doubleClick: 'reset' }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - return Plotly.relayout(gd, update); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - done(); - }); - }); - - it('when set to \'reset\' should reset to set range on double click', function(done) { - mockCopy = setRanges(mockCopy); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout, { doubleClick: 'reset' }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - return Plotly.relayout(gd, update); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - done(); - }); + it( + "when set to 'reset' should reset to set range on double click", + function(done) { + mockCopy = setRanges(mockCopy); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout, { + doubleClick: "reset" + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + return Plotly.relayout(gd, update); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + done(); + }); + } + ); + + it("when set to 'reset' should reset on all double clicks", function(done) { + mockCopy = setRanges(mockCopy); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout, { doubleClick: "reset" }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + done(); }); - - it('when set to \'reset\' should reset on all double clicks', function(done) { - mockCopy = setRanges(mockCopy); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout, { doubleClick: 'reset' }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - done(); - }); - }); - - it('when set to \'autosize\' should work when \'autorange\' is on', function(done) { - Plotly.plot(gd, mockCopy.data, mockCopy.layout, { doubleClick: 'autosize' }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - return Plotly.relayout(gd, update); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - done(); - }); - }); - - it('when set to \'autosize\' should set to autorange on double click', function(done) { - mockCopy = setRanges(mockCopy); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout, { doubleClick: 'autosize' }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - return Plotly.relayout(gd, update); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - done(); - }); - }); - - it('when set to \'autosize\' should reset on all double clicks', function(done) { - mockCopy = setRanges(mockCopy); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout, { doubleClick: 'autosize' }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); - - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - done(); - }); - }); - }); - describe('zoom interactions', function() { - beforeEach(function(done) { - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + it("when set to 'autosize' should work when 'autorange' is on", function( + done + ) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout, { + doubleClick: "autosize" + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + return Plotly.relayout(gd, update); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + done(); }); + }); - it('on main dragbox should update the axis ranges', function(done) { + it( + "when set to 'autosize' should set to autorange on double click", + function(done) { + mockCopy = setRanges(mockCopy); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout, { + doubleClick: "autosize" + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + return Plotly.relayout(gd, update); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - drag(93, 93, 393, 293).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-2.69897000, -0.515266602]); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.30069513, 1.2862324246]); - - return drag(93, 93, 393, 293); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-2.56671754, -1.644025966]); - expect(gd.layout.yaxis.range).toBeCloseToArray([0.159513853, 1.2174655634]); - - done(); - }); + done(); + }); + } + ); + + it("when set to 'autosize' should reset on all double clicks", function( + done + ) { + mockCopy = setRanges(mockCopy); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout, { + doubleClick: "autosize" + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + done(); }); }); + }); - describe('scroll zoom interactions', function() { + describe("zoom interactions", function() { + beforeEach(function(done) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - beforeEach(function(done) { - Plotly.plot(gd, mockCopy.data, mockCopy.layout, { scrollZoom: true }).then(done); + it("on main dragbox should update the axis ranges", function(done) { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(93, 93, 393, 293) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -2.69897000, + -0.515266602 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + -0.30069513, + 1.2862324246 + ]); + + return drag(93, 93, 393, 293); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -2.56671754, + -1.644025966 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + 0.159513853, + 1.2174655634 + ]); + + done(); }); + }); + }); - it('zooms in on scroll up', function() { + describe("scroll zoom interactions", function() { + beforeEach(function(done) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout, { + scrollZoom: true + }).then(done); + }); - var plot = gd._fullLayout._plots.xy.plot; + it("zooms in on scroll up", function() { + var plot = gd._fullLayout._plots.xy.plot; - mouseEvent('mousemove', 393, 243); - mouseEvent('scroll', 393, 243, { deltaX: 0, deltaY: -1000 }); + mouseEvent("mousemove", 393, 243); + mouseEvent("scroll", 393, 243, { deltaX: 0, deltaY: -1000 }); - var transform = plot.attr('transform'); + var transform = plot.attr("transform"); - var mockEl = { - attr: function() { - return transform; - } - }; + var mockEl = { + attr: function() { + return transform; + } + }; - var translate = Drawing.getTranslate(mockEl), - scale = Drawing.getScale(mockEl); + var translate = Drawing.getTranslate(mockEl), + scale = Drawing.getScale(mockEl); - expect([translate.x, translate.y]).toBeCloseToArray([61.070, 97.712]); - expect([scale.x, scale.y]).toBeCloseToArray([1.221, 1.221]); - }); + expect([translate.x, translate.y]).toBeCloseToArray([61.070, 97.712]); + expect([scale.x, scale.y]).toBeCloseToArray([1.221, 1.221]); }); + }); - describe('pan interactions', function() { - beforeEach(function(done) { - mockCopy.layout.dragmode = 'pan'; - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); - - it('on main dragbox should update the axis ranges', function(done) { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - - drag(93, 93, 393, 293).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-5.19567089, -0.02757284]); - expect(gd.layout.yaxis.range).toBeCloseToArray([0.595918934, 2.976310280]); + describe("pan interactions", function() { + beforeEach(function(done) { + mockCopy.layout.dragmode = "pan"; - return drag(93, 93, 393, 293); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-7.37937429, -2.21127624]); - expect(gd.layout.yaxis.range).toBeCloseToArray([2.182846498, 4.563237844]); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - done(); - }); + it("on main dragbox should update the axis ranges", function(done) { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(93, 93, 393, 293) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -5.19567089, + -0.02757284 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + 0.595918934, + 2.976310280 + ]); + + return drag(93, 93, 393, 293); + }) + .then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([ + -7.37937429, + -2.21127624 + ]); + expect(gd.layout.yaxis.range).toBeCloseToArray([ + 2.182846498, + 4.563237844 + ]); + + done(); }); + }); + it("should move the plot when panning", function() { + var start = 100, end = 300, plot = gd._fullLayout._plots.xy.plot; - it('should move the plot when panning', function() { - var start = 100, - end = 300, - plot = gd._fullLayout._plots.xy.plot; - - mouseEvent('mousemove', start, start); - mouseEvent('mousedown', start, start); - mouseEvent('mousemove', end, end); + mouseEvent("mousemove", start, start); + mouseEvent("mousedown", start, start); + mouseEvent("mousemove", end, end); - expect(plot.attr('transform')).toBe('translate(250, 280) scale(1, 1)'); + expect(plot.attr("transform")).toBe("translate(250, 280) scale(1, 1)"); - mouseEvent('mouseup', end, end); - }); + mouseEvent("mouseup", end, end); }); + }); }); -describe('dragbox', function() { +describe("dragbox", function() { + afterEach(destroyGraphDiv); - afterEach(destroyGraphDiv); + it("should scale subplot and inverse scale scatter points", function(done) { + var mock = Lib.extendDeep({}, require("@mocks/bar_line.json")); - it('should scale subplot and inverse scale scatter points', function(done) { - var mock = Lib.extendDeep({}, require('@mocks/bar_line.json')); - - function assertScale(node, x, y) { - var scale = Drawing.getScale(node); - expect(scale.x).toBeCloseTo(x, 1); - expect(scale.y).toBeCloseTo(y, 1); - } + function assertScale(node, x, y) { + var scale = Drawing.getScale(node); + expect(scale.x).toBeCloseTo(x, 1); + expect(scale.y).toBeCloseTo(y, 1); + } - Plotly.plot(createGraphDiv(), mock).then(function() { - var node = d3.select('rect.nedrag').node(); - var pos = getRectCenter(node); + Plotly.plot(createGraphDiv(), mock).then(function() { + var node = d3.select("rect.nedrag").node(); + var pos = getRectCenter(node); - assertScale(d3.select('.plot').node(), 1, 1); + assertScale(d3.select(".plot").node(), 1, 1); - d3.selectAll('.point').each(function() { - assertScale(this, 1, 1); - }); + d3.selectAll(".point").each(function() { + assertScale(this, 1, 1); + }); - mouseEvent('mousemove', pos[0], pos[1]); - mouseEvent('mousedown', pos[0], pos[1]); - mouseEvent('mousemove', pos[0] + 50, pos[1]); + mouseEvent("mousemove", pos[0], pos[1]); + mouseEvent("mousedown", pos[0], pos[1]); + mouseEvent("mousemove", pos[0] + 50, pos[1]); - setTimeout(function() { - assertScale(d3.select('.plot').node(), 1.14, 1); + setTimeout( + function() { + assertScale(d3.select(".plot").node(), 1.14, 1); - d3.select('.scatterlayer').selectAll('.point').each(function() { - assertScale(this, 0.87, 1); - }); - d3.select('.barlayer').selectAll('.point').each(function() { - assertScale(this, 1, 1); - }); + d3.select(".scatterlayer").selectAll(".point").each(function() { + assertScale(this, 0.87, 1); + }); + d3.select(".barlayer").selectAll(".point").each(function() { + assertScale(this, 1, 1); + }); - mouseEvent('mouseup', pos[0] + 50, pos[1]); - done(); - }, DBLCLICKDELAY / 4); - }); + mouseEvent("mouseup", pos[0] + 50, pos[1]); + done(); + }, + DBLCLICKDELAY / 4 + ); }); - + }); }); diff --git a/test/jasmine/tests/color_test.js b/test/jasmine/tests/color_test.js index 3f776337bcb..9c16343efef 100644 --- a/test/jasmine/tests/color_test.js +++ b/test/jasmine/tests/color_test.js @@ -1,199 +1,182 @@ -var Color = require('@src/components/color'); - - -describe('Test color:', function() { - 'use strict'; - - describe('clean', function() { - it('should turn rgb and rgba fractions into 0-255 values', function() { - var container = { - rgbcolor: 'rgb(0.3, 0.6, 0.9)', - rgbacolor: 'rgba(0.2, 0.4, 0.6, 0.8)' - }; - var expectedContainer = { - rgbcolor: 'rgb(77, 153, 230)', - rgbacolor: 'rgba(51, 102, 153, 0.8)' - }; - - Color.clean(container); - expect(container).toEqual(expectedContainer); - }); - - it('should dive into objects, arrays, and colorscales', function() { - var container = { - color: ['rgb(0.3, 0.6, 0.9)', 'rgba(0.2, 0.4, 0.6, 0.8)'], - nest: { - acolor: 'rgb(0.1, 0.2, 0.3)', - astring: 'rgb(0.1, 0.2, 0.3)' - }, - objarray: [ - {color: 'rgb(0.1, 0.2, 0.3)'}, - {color: 'rgb(0.3, 0.6, 0.9)'} - ], - somecolorscale: [ - [0, 'rgb(0.1, 0.2, 0.3)'], - [1, 'rgb(0.3, 0.6, 0.9)'] - ] - }; - var expectedContainer = { - color: ['rgb(77, 153, 230)', 'rgba(51, 102, 153, 0.8)'], - nest: { - acolor: 'rgb(26, 51, 77)', - astring: 'rgb(0.1, 0.2, 0.3)' - }, - objarray: [ - {color: 'rgb(26, 51, 77)'}, - {color: 'rgb(77, 153, 230)'} - ], - somecolorscale: [ - [0, 'rgb(26, 51, 77)'], - [1, 'rgb(77, 153, 230)'] - ] - }; - - Color.clean(container); - expect(container).toEqual(expectedContainer); - }); - - it('should count 0 as a fraction but not 1, except in alpha', function() { - // this is weird... but old tinycolor actually breaks - // if you pass in a 1, while in some cases a 1 here - // could be ambiguous - so we treat it as a real 1. - var container = { - fractioncolor: 'rgb(0, 0.4, 0.8)', - regularcolor: 'rgb(1, 0.5, 0.5)', - fractionrgbacolor: 'rgba(0, 0.4, 0.8, 1)' - }; - var expectedContainer = { - fractioncolor: 'rgb(0, 102, 204)', - regularcolor: 'rgb(1, 0.5, 0.5)', - fractionrgbacolor: 'rgba(0, 102, 204, 1)' - }; - - Color.clean(container); - expect(container).toEqual(expectedContainer); - }); - - it('should allow extra whitespace or space instead of commas', function() { - var container = { - rgbcolor: ' \t\r\n rgb \r\t\n ( 0.3\t\n,\t 0.6\n\n,\n 0.9\n\n)\r\t\n\t ', - rgb2color: 'rgb(0.3 0.6 0.9)', - rgbacolor: ' \t\r\n rgba \r\t\n ( 0.2\t\n,\t 0.4\n\n,\n 0.6\n\n , 0.8 )\r\t\n\t ' - }; - var expectedContainer = { - rgbcolor: 'rgb(77, 153, 230)', - rgb2color: 'rgb(77, 153, 230)', - rgbacolor: 'rgba(51, 102, 153, 0.8)' - }; - - Color.clean(container); - expect(container).toEqual(expectedContainer); - }); - - it('should not change if r, g, b >= 1 but clip alpha > 1', function() { - var container = { - rgbcolor: 'rgb(0.1, 1.0, 0.5)', - rgbacolor: 'rgba(0.1, 1.0, 0.5, 1234)', - rgba2color: 'rgba(0.1, 0.2, 0.5, 1234)' - }; - var expectedContainer = { - rgbcolor: 'rgb(0.1, 1.0, 0.5)', - rgbacolor: 'rgba(0.1, 1.0, 0.5, 1234)', - rgba2color: 'rgba(26, 51, 128, 1)' - }; - - Color.clean(container); - expect(container).toEqual(expectedContainer); - }); - - it('should not alter malformed strings or non-color keys', function() { - var container = { - color2: 'rgb(0.1, 0.1, 0.1)', - acolor: 'rgbb(0.1, 0.1, 0.1)', - bcolor: 'rgb(0.1, ,0.1)', - ccolor: 'rgb(0.1, 0.1, 0.1', - dcolor: 'rgb(0.1, 0.1, 0.1);' - }; - var expectedContainer = {}; - Object.keys(container).forEach(function(k) { expectedContainer[k] = container[k]; }); - - Color.clean(container); - expect(container).toEqual(expectedContainer); - }); - - it('should not barf on nulls', function() { - var container1 = null; - var expectedContainer1 = null; - - Color.clean(container1); - expect(container1).toEqual(expectedContainer1); - - var container2 = { - anull: null, - anundefined: undefined, - color: null, - anarray: [null, {color: 'rgb(0.1, 0.1, 0.1)'}] - }; - var expectedContainer2 = { - anull: null, - anundefined: undefined, - color: null, - anarray: [null, {color: 'rgb(0.1, 0.1, 0.1)'}] - }; - - Color.clean(container2); - expect(container2).toEqual(expectedContainer2); - }); +var Color = require("@src/components/color"); + +describe("Test color:", function() { + "use strict"; + describe("clean", function() { + it("should turn rgb and rgba fractions into 0-255 values", function() { + var container = { + rgbcolor: "rgb(0.3, 0.6, 0.9)", + rgbacolor: "rgba(0.2, 0.4, 0.6, 0.8)" + }; + var expectedContainer = { + rgbcolor: "rgb(77, 153, 230)", + rgbacolor: "rgba(51, 102, 153, 0.8)" + }; + + Color.clean(container); + expect(container).toEqual(expectedContainer); }); - describe('fill', function() { - - it('should call style with both fill and fill-opacity', function() { - var mockElement = { - style: function(object) { - expect(object.fill).toBe('rgb(255, 255, 0)'); - expect(object['fill-opacity']).toBe(0.5); - } - }; - - Color.fill(mockElement, 'rgba(255,255,0,0.5'); - }); - + it("should dive into objects, arrays, and colorscales", function() { + var container = { + color: ["rgb(0.3, 0.6, 0.9)", "rgba(0.2, 0.4, 0.6, 0.8)"], + nest: { acolor: "rgb(0.1, 0.2, 0.3)", astring: "rgb(0.1, 0.2, 0.3)" }, + objarray: [ + { color: "rgb(0.1, 0.2, 0.3)" }, + { color: "rgb(0.3, 0.6, 0.9)" } + ], + somecolorscale: [[0, "rgb(0.1, 0.2, 0.3)"], [1, "rgb(0.3, 0.6, 0.9)"]] + }; + var expectedContainer = { + color: ["rgb(77, 153, 230)", "rgba(51, 102, 153, 0.8)"], + nest: { acolor: "rgb(26, 51, 77)", astring: "rgb(0.1, 0.2, 0.3)" }, + objarray: [ + { color: "rgb(26, 51, 77)" }, + { color: "rgb(77, 153, 230)" } + ], + somecolorscale: [[0, "rgb(26, 51, 77)"], [1, "rgb(77, 153, 230)"]] + }; + + Color.clean(container); + expect(container).toEqual(expectedContainer); }); - describe('stroke', function() { + it("should count 0 as a fraction but not 1, except in alpha", function() { + // this is weird... but old tinycolor actually breaks + // if you pass in a 1, while in some cases a 1 here + // could be ambiguous - so we treat it as a real 1. + var container = { + fractioncolor: "rgb(0, 0.4, 0.8)", + regularcolor: "rgb(1, 0.5, 0.5)", + fractionrgbacolor: "rgba(0, 0.4, 0.8, 1)" + }; + var expectedContainer = { + fractioncolor: "rgb(0, 102, 204)", + regularcolor: "rgb(1, 0.5, 0.5)", + fractionrgbacolor: "rgba(0, 102, 204, 1)" + }; + + Color.clean(container); + expect(container).toEqual(expectedContainer); + }); - it('should call style with both fill and fill-opacity', function() { - var mockElement = { - style: function(object) { - expect(object.stroke).toBe('rgb(255, 255, 0)'); - expect(object['stroke-opacity']).toBe(0.5); - } - }; + it("should allow extra whitespace or space instead of commas", function() { + var container = { + rgbcolor: " \t\r\n rgb \r\t\n ( 0.3\t\n,\t 0.6\n\n,\n 0.9\n\n)\r\t\n\t ", + rgb2color: "rgb(0.3 0.6 0.9)", + rgbacolor: " \t\r\n rgba \r\t\n ( 0.2\t\n,\t 0.4\n\n,\n 0.6\n\n , 0.8 )\r\t\n\t " + }; + var expectedContainer = { + rgbcolor: "rgb(77, 153, 230)", + rgb2color: "rgb(77, 153, 230)", + rgbacolor: "rgba(51, 102, 153, 0.8)" + }; + + Color.clean(container); + expect(container).toEqual(expectedContainer); + }); - Color.stroke(mockElement, 'rgba(255,255,0,0.5'); - }); + it("should not change if r, g, b >= 1 but clip alpha > 1", function() { + var container = { + rgbcolor: "rgb(0.1, 1.0, 0.5)", + rgbacolor: "rgba(0.1, 1.0, 0.5, 1234)", + rgba2color: "rgba(0.1, 0.2, 0.5, 1234)" + }; + var expectedContainer = { + rgbcolor: "rgb(0.1, 1.0, 0.5)", + rgbacolor: "rgba(0.1, 1.0, 0.5, 1234)", + rgba2color: "rgba(26, 51, 128, 1)" + }; + + Color.clean(container); + expect(container).toEqual(expectedContainer); + }); + it("should not alter malformed strings or non-color keys", function() { + var container = { + color2: "rgb(0.1, 0.1, 0.1)", + acolor: "rgbb(0.1, 0.1, 0.1)", + bcolor: "rgb(0.1, ,0.1)", + ccolor: "rgb(0.1, 0.1, 0.1", + dcolor: "rgb(0.1, 0.1, 0.1);" + }; + var expectedContainer = {}; + Object.keys(container).forEach(function(k) { + expectedContainer[k] = container[k]; + }); + + Color.clean(container); + expect(container).toEqual(expectedContainer); }); - describe('contrast', function() { + it("should not barf on nulls", function() { + var container1 = null; + var expectedContainer1 = null; + + Color.clean(container1); + expect(container1).toEqual(expectedContainer1); + + var container2 = { + anull: null, + anundefined: undefined, + color: null, + anarray: [null, { color: "rgb(0.1, 0.1, 0.1)" }] + }; + var expectedContainer2 = { + anull: null, + anundefined: undefined, + color: null, + anarray: [null, { color: "rgb(0.1, 0.1, 0.1)" }] + }; + + Color.clean(container2); + expect(container2).toEqual(expectedContainer2); + }); + }); + + describe("fill", function() { + it("should call style with both fill and fill-opacity", function() { + var mockElement = { + style: function(object) { + expect(object.fill).toBe("rgb(255, 255, 0)"); + expect(object["fill-opacity"]).toBe(0.5); + } + }; + + Color.fill(mockElement, "rgba(255,255,0,0.5"); + }); + }); + + describe("stroke", function() { + it("should call style with both fill and fill-opacity", function() { + var mockElement = { + style: function(object) { + expect(object.stroke).toBe("rgb(255, 255, 0)"); + expect(object["stroke-opacity"]).toBe(0.5); + } + }; + + Color.stroke(mockElement, "rgba(255,255,0,0.5"); + }); + }); - it('should darken light colors', function() { - var out = Color.contrast('#eee', 10, 20); + describe("contrast", function() { + it("should darken light colors", function() { + var out = Color.contrast("#eee", 10, 20); - expect(out).toEqual('#bbbbbb'); - }); + expect(out).toEqual("#bbbbbb"); + }); - it('should darken light colors (2)', function() { - var out = Color.contrast('#fdae61', 10, 20); + it("should darken light colors (2)", function() { + var out = Color.contrast("#fdae61", 10, 20); - expect(out).toEqual('#f57a03'); - }); + expect(out).toEqual("#f57a03"); + }); - it('should lighten dark colors', function() { - var out = Color.contrast('#2b83ba', 10, 20); + it("should lighten dark colors", function() { + var out = Color.contrast("#2b83ba", 10, 20); - expect(out).toEqual('#449dd4'); - }); + expect(out).toEqual("#449dd4"); }); + }); }); diff --git a/test/jasmine/tests/colorbar_test.js b/test/jasmine/tests/colorbar_test.js index 0b6591f0e5c..e788e5690b5 100644 --- a/test/jasmine/tests/colorbar_test.js +++ b/test/jasmine/tests/colorbar_test.js @@ -1,32 +1,18 @@ -var Colorbar = require('@src/components/colorbar'); +var Colorbar = require("@src/components/colorbar"); +describe("Test colorbar:", function() { + "use strict"; + describe("hasColorbar", function() { + var hasColorbar = Colorbar.hasColorbar, trace; -describe('Test colorbar:', function() { - 'use strict'; + it("should return true when marker colorbar is defined", function() { + trace = { marker: { colorbar: {}, line: { colorbar: {} } } }; + expect(hasColorbar(trace.marker)).toBe(true); + expect(hasColorbar(trace.marker.line)).toBe(true); - describe('hasColorbar', function() { - var hasColorbar = Colorbar.hasColorbar, - trace; - - it('should return true when marker colorbar is defined', function() { - trace = { - marker: { - colorbar: {}, - line: { - colorbar: {} - } - } - }; - expect(hasColorbar(trace.marker)).toBe(true); - expect(hasColorbar(trace.marker.line)).toBe(true); - - trace = { - marker: { - line: {} - } - }; - expect(hasColorbar(trace.marker)).toBe(false); - expect(hasColorbar(trace.marker.line)).toBe(false); - }); + trace = { marker: { line: {} } }; + expect(hasColorbar(trace.marker)).toBe(false); + expect(hasColorbar(trace.marker.line)).toBe(false); }); + }); }); diff --git a/test/jasmine/tests/colorscale_test.js b/test/jasmine/tests/colorscale_test.js index 462d53c2a08..e8e64d3a324 100644 --- a/test/jasmine/tests/colorscale_test.js +++ b/test/jasmine/tests/colorscale_test.js @@ -1,406 +1,360 @@ -var Colorscale = require('@src/components/colorscale'); -var Lib = require('@src/lib'); -var Plots = require('@src/plots/plots'); -var Heatmap = require('@src/traces/heatmap'); -var Scatter = require('@src/traces/scatter'); - - -describe('Test colorscale:', function() { - 'use strict'; - - describe('isValidScale', function() { - var isValidScale = Colorscale.isValidScale, - scl; - - it('should accept colorscale strings', function() { - expect(isValidScale('Earth')).toBe(true); - expect(isValidScale('Greens')).toBe(true); - expect(isValidScale('Nop')).toBe(false); - }); +var Colorscale = require("@src/components/colorscale"); +var Lib = require("@src/lib"); +var Plots = require("@src/plots/plots"); +var Heatmap = require("@src/traces/heatmap"); +var Scatter = require("@src/traces/scatter"); + +describe("Test colorscale:", function() { + "use strict"; + describe("isValidScale", function() { + var isValidScale = Colorscale.isValidScale, scl; + + it("should accept colorscale strings", function() { + expect(isValidScale("Earth")).toBe(true); + expect(isValidScale("Greens")).toBe(true); + expect(isValidScale("Nop")).toBe(false); + }); - it('should accept only array of 2-item arrays', function() { - expect(isValidScale('a')).toBe(false); - expect(isValidScale([])).toBe(false); - expect(isValidScale([null, undefined])).toBe(false); - expect(isValidScale([{}, [1, 'rgb(0, 0, 200']])).toBe(false); - expect(isValidScale([[0, 'rgb(200, 0, 0)'], {}])).toBe(false); - expect(isValidScale([[0, 'rgb(0, 0, 200)'], undefined])).toBe(false); - expect(isValidScale([null, [1, 'rgb(0, 0, 200)']])).toBe(false); - expect(isValidScale(['a', 'b'])).toBe(false); - expect(isValidScale(['a'])).toBe(false); - expect(isValidScale([['a'], ['b']])).toBe(false); - - scl = [[0, 'rgb(0, 0, 200)'], [1, 'rgb(200, 0, 0)']]; - expect(isValidScale(scl)).toBe(true); - }); + it("should accept only array of 2-item arrays", function() { + expect(isValidScale("a")).toBe(false); + expect(isValidScale([])).toBe(false); + expect(isValidScale([null, undefined])).toBe(false); + expect(isValidScale([{}, [1, "rgb(0, 0, 200"]])).toBe(false); + expect(isValidScale([[0, "rgb(200, 0, 0)"], {}])).toBe(false); + expect(isValidScale([[0, "rgb(0, 0, 200)"], undefined])).toBe(false); + expect(isValidScale([null, [1, "rgb(0, 0, 200)"]])).toBe(false); + expect(isValidScale(["a", "b"])).toBe(false); + expect(isValidScale(["a"])).toBe(false); + expect(isValidScale([["a"], ["b"]])).toBe(false); + + scl = [[0, "rgb(0, 0, 200)"], [1, "rgb(200, 0, 0)"]]; + expect(isValidScale(scl)).toBe(true); + }); - it('should accept only arrays with 1st val = 0 and last val = 1', function() { - scl = [[0.2, 'rgb(0, 0, 200)'], [1, 'rgb(200, 0, 0)']]; - expect(isValidScale(scl)).toBe(false); + it( + "should accept only arrays with 1st val = 0 and last val = 1", + function() { + scl = [[0.2, "rgb(0, 0, 200)"], [1, "rgb(200, 0, 0)"]]; + expect(isValidScale(scl)).toBe(false); - scl = [['0', 'rgb(0, 0, 200)'], [1, 'rgb(200, 0, 0)']]; - expect(isValidScale(scl)).toBe(true); + scl = [["0", "rgb(0, 0, 200)"], [1, "rgb(200, 0, 0)"]]; + expect(isValidScale(scl)).toBe(true); - scl = [[0, 'rgb(0, 0, 200)'], [1.2, 'rgb(200, 0, 0)']]; - expect(isValidScale(scl)).toBe(false); + scl = [[0, "rgb(0, 0, 200)"], [1.2, "rgb(200, 0, 0)"]]; + expect(isValidScale(scl)).toBe(false); - scl = [[0, 'rgb(0, 0, 200)'], ['1.0', 'rgb(200, 0, 0)']]; - expect(isValidScale(scl)).toBe(true); - }); + scl = [[0, "rgb(0, 0, 200)"], ["1.0", "rgb(200, 0, 0)"]]; + expect(isValidScale(scl)).toBe(true); + } + ); - it('should accept ascending order number-color items', function() { - scl = [['rgb(0, 0, 200)', 0], ['rgb(200, 0, 0)', 1]]; - expect(isValidScale(scl)).toBe(false); + it("should accept ascending order number-color items", function() { + scl = [["rgb(0, 0, 200)", 0], ["rgb(200, 0, 0)", 1]]; + expect(isValidScale(scl)).toBe(false); - scl = [[0, 0], [1, 1]]; - expect(isValidScale(scl)).toBe(false); + scl = [[0, 0], [1, 1]]; + expect(isValidScale(scl)).toBe(false); - scl = [[0, 'a'], [1, 'b']]; - expect(isValidScale()).toBe(false); + scl = [[0, "a"], [1, "b"]]; + expect(isValidScale()).toBe(false); - scl = [[0, 'rgb(0, 0, 200)'], [0.6, 'rgb(200, 200, 0)'], - [0.3, 'rgb(0, 200, 0)'], [1, 'rgb(200, 0, 0)']]; - expect(isValidScale(scl)).toBe(false); - }); + scl = [ + [0, "rgb(0, 0, 200)"], + [0.6, "rgb(200, 200, 0)"], + [0.3, "rgb(0, 200, 0)"], + [1, "rgb(200, 0, 0)"] + ]; + expect(isValidScale(scl)).toBe(false); }); + }); + + describe("flipScale", function() { + var flipScale = Colorscale.flipScale, scl; + + it("should flip a colorscale", function() { + scl = [ + [0, "rgb(0, 0, 200)"], + ["0.5", "rgb(0, 0, 0)"], + ["1.0", "rgb(200, 0, 0)"] + ]; + expect(flipScale(scl)).toEqual([ + [0, "rgb(200, 0, 0)"], + [0.5, "rgb(0, 0, 0)"], + [1, "rgb(0, 0, 200)"] + ]); + }); + }); - describe('flipScale', function() { - var flipScale = Colorscale.flipScale, - scl; - - it('should flip a colorscale', function() { - scl = [[0, 'rgb(0, 0, 200)'], ['0.5', 'rgb(0, 0, 0)'], ['1.0', 'rgb(200, 0, 0)']]; - expect(flipScale(scl)).toEqual( - [[0, 'rgb(200, 0, 0)'], [0.5, 'rgb(0, 0, 0)'], [1, 'rgb(0, 0, 200)']] - ); + describe("hasColorscale", function() { + var hasColorscale = Colorscale.hasColorscale, trace; - }); + it("should return false when marker is not defined", function() { + var shouldBeFalse = [{}, { marker: null }]; + shouldBeFalse.forEach(function(trace) { + expect(hasColorscale(trace, "marker")).toBe(false); + }); }); - describe('hasColorscale', function() { - var hasColorscale = Colorscale.hasColorscale, - trace; - - it('should return false when marker is not defined', function() { - var shouldBeFalse = [ - {}, - {marker: null} - ]; - shouldBeFalse.forEach(function(trace) { - expect(hasColorscale(trace, 'marker')).toBe(false); - }); + it( + "should return false when marker is not defined (nested version)", + function() { + var shouldBeFalse = [{}, { marker: null }, { marker: { line: null } }]; + shouldBeFalse.forEach(function(trace) { + expect(hasColorscale(trace, "marker.line")).toBe(false); }); + } + ); + + it( + "should return true when marker color is an Array with at least one number", + function() { + trace = { + marker: { color: [1, 2, 3], line: { color: [2, 3, 4] } } + }; + expect(hasColorscale(trace, "marker")).toBe(true); + expect(hasColorscale(trace, "marker.line")).toBe(true); + + trace = { + marker: { + color: ["1", "red", "#d0d0d0"], + line: { color: ["blue", "3", "#fff"] } + } + }; + expect(hasColorscale(trace, "marker")).toBe(true); + expect(hasColorscale(trace, "marker.line")).toBe(true); + + trace = { + marker: { + color: ["green", "red", "blue"], + line: { color: ["rgb(100, 100, 100)", "#d0d0d0", "#fff"] } + } + }; + expect(hasColorscale(trace, "marker")).toBe(false); + expect(hasColorscale(trace, "marker.line")).toBe(false); + } + ); + + it("should return true when marker showscale is true", function() { + trace = { marker: { showscale: true, line: { showscale: true } } }; + expect(hasColorscale(trace, "marker")).toBe(true); + expect(hasColorscale(trace, "marker.line")).toBe(true); + }); - it('should return false when marker is not defined (nested version)', function() { - var shouldBeFalse = [ - {}, - {marker: null}, - {marker: {line: null}} - ]; - shouldBeFalse.forEach(function(trace) { - expect(hasColorscale(trace, 'marker.line')).toBe(false); - }); - }); + it("should return true when marker colorscale is valid", function() { + trace = { + marker: { + colorscale: "Greens", + line: { colorscale: [[0, "rgb(0,0,0)"], [1, "rgb(0,0,0)"]] } + } + }; + expect(hasColorscale(trace, "marker")).toBe(true); + expect(hasColorscale(trace, "marker.line")).toBe(true); + }); - it('should return true when marker color is an Array with at least one number', function() { - trace = { - marker: { - color: [1, 2, 3], - line: { - color: [2, 3, 4] - } - } - }; - expect(hasColorscale(trace, 'marker')).toBe(true); - expect(hasColorscale(trace, 'marker.line')).toBe(true); - - trace = { - marker: { - color: ['1', 'red', '#d0d0d0'], - line: { - color: ['blue', '3', '#fff'] - } - } - }; - expect(hasColorscale(trace, 'marker')).toBe(true); - expect(hasColorscale(trace, 'marker.line')).toBe(true); - - trace = { - marker: { - color: ['green', 'red', 'blue'], - line: { - color: ['rgb(100, 100, 100)', '#d0d0d0', '#fff'] - } - } - }; - expect(hasColorscale(trace, 'marker')).toBe(false); - expect(hasColorscale(trace, 'marker.line')).toBe(false); - }); + it("should return true when marker cmin & cmax are numbers", function() { + trace = { marker: { cmin: 10, cmax: 20, line: { cmin: 10, cmax: 20 } } }; + expect(hasColorscale(trace, "marker")).toBe(true); + expect(hasColorscale(trace, "marker.line")).toBe(true); + }); - it('should return true when marker showscale is true', function() { - trace = { - marker: { - showscale: true, - line: { - showscale: true - } - } - }; - expect(hasColorscale(trace, 'marker')).toBe(true); - expect(hasColorscale(trace, 'marker.line')).toBe(true); - }); + it("should return true when marker colorbar is defined", function() { + trace = { marker: { colorbar: {}, line: { colorbar: {} } } }; + expect(hasColorscale(trace, "marker")).toBe(true); + expect(hasColorscale(trace, "marker.line")).toBe(true); + }); + }); - it('should return true when marker colorscale is valid', function() { - trace = { - marker: { - colorscale: 'Greens', - line: { - colorscale: [[0, 'rgb(0,0,0)'], [1, 'rgb(0,0,0)']] - } - } - }; - expect(hasColorscale(trace, 'marker')).toBe(true); - expect(hasColorscale(trace, 'marker.line')).toBe(true); - }); + describe("handleDefaults (heatmap-like version)", function() { + var handleDefaults = Colorscale.handleDefaults, + layout = { font: Plots.layoutAttributes.font }, + opts = { prefix: "", cLetter: "z" }; + var traceIn, traceOut; - it('should return true when marker cmin & cmax are numbers', function() { - trace = { - marker: { - cmin: 10, - cmax: 20, - line: { - cmin: 10, - cmax: 20 - } - } - }; - expect(hasColorscale(trace, 'marker')).toBe(true); - expect(hasColorscale(trace, 'marker.line')).toBe(true); - }); + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, Heatmap.attributes, attr, dflt); + } - it('should return true when marker colorbar is defined', function() { - trace = { - marker: { - colorbar: {}, - line: { - colorbar: {} - } - } - }; - expect(hasColorscale(trace, 'marker')).toBe(true); - expect(hasColorscale(trace, 'marker.line')).toBe(true); - }); + beforeEach(function() { + traceOut = {}; }); - describe('handleDefaults (heatmap-like version)', function() { - var handleDefaults = Colorscale.handleDefaults, - layout = { - font: Plots.layoutAttributes.font - }, - opts = {prefix: '', cLetter: 'z'}; - var traceIn, traceOut; - - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, Heatmap.attributes, attr, dflt); - } - - beforeEach(function() { - traceOut = {}; - }); + it("should set auto to true when min/max are valid", function() { + traceIn = { zmin: -10, zmax: 10 }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.zauto).toBe(false); + }); - it('should set auto to true when min/max are valid', function() { - traceIn = { - zmin: -10, - zmax: 10 - }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.zauto).toBe(false); - }); + it("should fall back to auto true when min/max are invalid", function() { + traceIn = { zmin: "dsa", zmax: null }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.zauto).toBe(true); - it('should fall back to auto true when min/max are invalid', function() { - traceIn = { - zmin: 'dsa', - zmax: null - }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.zauto).toBe(true); - - traceIn = { - zmin: 10, - zmax: -10 - }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.zauto).toBe(true); - }); + traceIn = { zmin: 10, zmax: -10 }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.zauto).toBe(true); + }); - it('should coerce autocolorscale to false unless set to true', function() { - traceIn = {}; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.autocolorscale).toBe(false); - - traceIn = { - colorscale: 'Greens' - }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.autocolorscale).toBe(false); - - traceIn = { - autocolorscale: true - }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.autocolorscale).toBe(true); - }); + it("should coerce autocolorscale to false unless set to true", function() { + traceIn = {}; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.autocolorscale).toBe(false); - it('should coerce showscale to true unless set to false', function() { - traceIn = {}; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.showscale).toBe(true); + traceIn = { colorscale: "Greens" }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.autocolorscale).toBe(false); - traceIn = { showscale: false }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.showscale).toBe(false); - }); + traceIn = { autocolorscale: true }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.autocolorscale).toBe(true); }); - describe('handleDefaults (scatter-like version)', function() { - var handleDefaults = Colorscale.handleDefaults, - layout = { - font: Plots.layoutAttributes.font - }, - opts = {prefix: 'marker.', cLetter: 'c'}; - var traceIn, traceOut; - - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, Scatter.attributes, attr, dflt); - } + it("should coerce showscale to true unless set to false", function() { + traceIn = {}; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.showscale).toBe(true); - beforeEach(function() { - traceOut = { marker: {} }; - }); + traceIn = { showscale: false }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.showscale).toBe(false); + }); + }); - it('should coerce autocolorscale to true by default', function() { - traceIn = { marker: { line: {} } }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.marker.autocolorscale).toBe(true); - }); + describe("handleDefaults (scatter-like version)", function() { + var handleDefaults = Colorscale.handleDefaults, + layout = { font: Plots.layoutAttributes.font }, + opts = { prefix: "marker.", cLetter: "c" }; + var traceIn, traceOut; - it('should coerce autocolorscale to false when valid colorscale is given', function() { - traceIn = { - marker: { colorscale: 'Greens' } - }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.marker.autocolorscale).toBe(false); - - traceIn = { - marker: { colorscale: 'nope' } - }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.marker.autocolorscale).toBe(true); - }); + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, Scatter.attributes, attr, dflt); + } - it('should coerce showscale to true if colorbar is specified', function() { - traceIn = { marker: {} }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.marker.showscale).toBe(false); - - traceIn = { - marker: { - colorbar: {} - } - }; - handleDefaults(traceIn, traceOut, layout, coerce, opts); - expect(traceOut.marker.showscale).toBe(true); - }); + beforeEach(function() { + traceOut = { marker: {} }; }); - describe('calc', function() { - var calcColorscale = Colorscale.calc; - var trace, z; + it("should coerce autocolorscale to true by default", function() { + traceIn = { marker: { line: {} } }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.marker.autocolorscale).toBe(true); + }); - beforeEach(function() { - trace = {}; - z = {}; - }); + it( + "should coerce autocolorscale to false when valid colorscale is given", + function() { + traceIn = { marker: { colorscale: "Greens" } }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.marker.autocolorscale).toBe(false); + + traceIn = { marker: { colorscale: "nope" } }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.marker.autocolorscale).toBe(true); + } + ); + + it("should coerce showscale to true if colorbar is specified", function() { + traceIn = { marker: {} }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.marker.showscale).toBe(false); + + traceIn = { marker: { colorbar: {} } }; + handleDefaults(traceIn, traceOut, layout, coerce, opts); + expect(traceOut.marker.showscale).toBe(true); + }); + }); - it('should be RdBuNeg when autocolorscale and z <= 0', function() { - trace = { - type: 'heatmap', - z: [[0, -1.5], [-2, -10]], - autocolorscale: true, - _input: {} - }; - z = [[0, -1.5], [-2, -10]]; - calcColorscale(trace, z, '', 'z'); - expect(trace.autocolorscale).toBe(true); - expect(trace.colorscale[5]).toEqual([1, 'rgb(220,220,220)']); - }); + describe("calc", function() { + var calcColorscale = Colorscale.calc; + var trace, z; - it('should be Blues when the only numerical z <= -0.5', function() { - trace = { - type: 'heatmap', - z: [['a', 'b'], [-0.5, 'd']], - autocolorscale: true, - _input: {} - }; - z = [[undefined, undefined], [-0.5, undefined]]; - calcColorscale(trace, z, '', 'z'); - expect(trace.autocolorscale).toBe(true); - expect(trace.colorscale[5]).toEqual([1, 'rgb(220,220,220)']); - }); + beforeEach(function() { + trace = {}; + z = {}; + }); - it('should be Reds when the only numerical z >= 0.5', function() { - trace = { - type: 'heatmap', - z: [['a', 'b'], [0.5, 'd']], - autocolorscale: true, - _input: {} - }; - z = [[undefined, undefined], [0.5, undefined]]; - calcColorscale(trace, z, '', 'z'); - expect(trace.autocolorscale).toBe(true); - expect(trace.colorscale[0]).toEqual([0, 'rgb(220,220,220)']); - }); + it("should be RdBuNeg when autocolorscale and z <= 0", function() { + trace = { + type: "heatmap", + z: [[0, -1.5], [-2, -10]], + autocolorscale: true, + _input: {} + }; + z = [[0, -1.5], [-2, -10]]; + calcColorscale(trace, z, "", "z"); + expect(trace.autocolorscale).toBe(true); + expect(trace.colorscale[5]).toEqual([1, "rgb(220,220,220)"]); + }); - it('should be reverse the auto scale when reversescale is true', function() { - trace = { - type: 'heatmap', - z: [['a', 'b'], [0.5, 'd']], - autocolorscale: true, - reversescale: true, - _input: {} - }; - z = [[undefined, undefined], [0.5, undefined]]; - calcColorscale(trace, z, '', 'z'); - expect(trace.autocolorscale).toBe(true); - expect(trace.colorscale[trace.colorscale.length - 1]) - .toEqual([1, 'rgb(220,220,220)']); - }); + it("should be Blues when the only numerical z <= -0.5", function() { + trace = { + type: "heatmap", + z: [["a", "b"], [-0.5, "d"]], + autocolorscale: true, + _input: {} + }; + z = [[undefined, undefined], [-0.5, undefined]]; + calcColorscale(trace, z, "", "z"); + expect(trace.autocolorscale).toBe(true); + expect(trace.colorscale[5]).toEqual([1, "rgb(220,220,220)"]); + }); + it("should be Reds when the only numerical z >= 0.5", function() { + trace = { + type: "heatmap", + z: [["a", "b"], [0.5, "d"]], + autocolorscale: true, + _input: {} + }; + z = [[undefined, undefined], [0.5, undefined]]; + calcColorscale(trace, z, "", "z"); + expect(trace.autocolorscale).toBe(true); + expect(trace.colorscale[0]).toEqual([0, "rgb(220,220,220)"]); }); - describe('extractScale + makeColorScaleFunc', function() { - var scale = [ - [0, 'rgb(5,10,172)'], - [0.35, 'rgb(106,137,247)'], - [0.5, 'rgb(190,190,190)'], - [0.6, 'rgb(220,170,132)'], - [0.7, 'rgb(230,145,90)'], - [1, 'rgb(178,10,28)'] - ]; - - var specs = Colorscale.extractScale(scale, 2, 3); - var sclFunc = Colorscale.makeColorScaleFunc(specs); - - it('should constrain color array values between cmin and cmax', function() { - var color1 = sclFunc(1), - color2 = sclFunc(2), - color3 = sclFunc(3), - color4 = sclFunc(4); - - expect(color1).toEqual(color2); - expect(color1).toEqual('rgb(5, 10, 172)'); - expect(color3).toEqual(color4); - expect(color4).toEqual('rgb(178, 10, 28)'); - }); + it( + "should be reverse the auto scale when reversescale is true", + function() { + trace = { + type: "heatmap", + z: [["a", "b"], [0.5, "d"]], + autocolorscale: true, + reversescale: true, + _input: {} + }; + z = [[undefined, undefined], [0.5, undefined]]; + calcColorscale(trace, z, "", "z"); + expect(trace.autocolorscale).toBe(true); + expect(trace.colorscale[trace.colorscale.length - 1]).toEqual([ + 1, + "rgb(220,220,220)" + ]); + } + ); + }); + + describe("extractScale + makeColorScaleFunc", function() { + var scale = [ + [0, "rgb(5,10,172)"], + [0.35, "rgb(106,137,247)"], + [0.5, "rgb(190,190,190)"], + [0.6, "rgb(220,170,132)"], + [0.7, "rgb(230,145,90)"], + [1, "rgb(178,10,28)"] + ]; + + var specs = Colorscale.extractScale(scale, 2, 3); + var sclFunc = Colorscale.makeColorScaleFunc(specs); + + it("should constrain color array values between cmin and cmax", function() { + var color1 = sclFunc(1), + color2 = sclFunc(2), + color3 = sclFunc(3), + color4 = sclFunc(4); + + expect(color1).toEqual(color2); + expect(color1).toEqual("rgb(5, 10, 172)"); + expect(color3).toEqual(color4); + expect(color4).toEqual("rgb(178, 10, 28)"); }); + }); }); diff --git a/test/jasmine/tests/command_test.js b/test/jasmine/tests/command_test.js index 808fe17dfc5..c525a9ac735 100644 --- a/test/jasmine/tests/command_test.js +++ b/test/jasmine/tests/command_test.js @@ -1,660 +1,873 @@ -var Plotly = require('@lib/index'); -var PlotlyInternal = require('@src/plotly'); +var Plotly = require("@lib/index"); +var PlotlyInternal = require("@src/plotly"); var Plots = Plotly.Plots; -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); -var Lib = require('@src/lib'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var fail = require("../assets/fail_test"); +var Lib = require("@src/lib"); -describe('Plots.executeAPICommand', function() { - 'use strict'; +describe("Plots.executeAPICommand", function() { + "use strict"; + var gd; - var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(function() { - destroyGraphDiv(gd); - }); - - describe('with a successful API command', function() { - beforeEach(function() { - spyOn(PlotlyInternal, 'restyle').and.callFake(function() { - return Promise.resolve('resolution'); - }); - }); - - it('calls the API method and resolves', function(done) { - Plots.executeAPICommand(gd, 'restyle', ['foo', 'bar']).then(function(value) { - var m = PlotlyInternal.restyle; - expect(m).toHaveBeenCalled(); - expect(m.calls.count()).toEqual(1); - expect(m.calls.argsFor(0)).toEqual([gd, 'foo', 'bar']); - - expect(value).toEqual('resolution'); - }).catch(fail).then(done); - }); - - }); - - describe('with an unsuccessful command', function() { - beforeEach(function() { - spyOn(PlotlyInternal, 'restyle').and.callFake(function() { - return Promise.reject('rejection'); - }); - }); - - it('calls the API method and rejects', function(done) { - Plots.executeAPICommand(gd, 'restyle', ['foo', 'bar']).then(fail, function(value) { - var m = PlotlyInternal.restyle; - expect(m).toHaveBeenCalled(); - expect(m.calls.count()).toEqual(1); - expect(m.calls.argsFor(0)).toEqual([gd, 'foo', 'bar']); - - expect(value).toEqual('rejection'); - }).catch(fail).then(done); - }); - - }); -}); + afterEach(function() { + destroyGraphDiv(gd); + }); -describe('Plots.hasSimpleAPICommandBindings', function() { - 'use strict'; - var gd; + describe("with a successful API command", function() { beforeEach(function() { - gd = createGraphDiv(); - - Plotly.plot(gd, [ - {x: [1, 2, 3], y: [1, 2, 3]}, - {x: [1, 2, 3], y: [4, 5, 6]}, - ]); + spyOn(PlotlyInternal, "restyle").and.callFake(function() { + return Promise.resolve("resolution"); + }); }); - afterEach(function() { - destroyGraphDiv(gd); + it("calls the API method and resolves", function(done) { + Plots.executeAPICommand(gd, "restyle", ["foo", "bar"]) + .then(function(value) { + var m = PlotlyInternal.restyle; + expect(m).toHaveBeenCalled(); + expect(m.calls.count()).toEqual(1); + expect(m.calls.argsFor(0)).toEqual([gd, "foo", "bar"]); + + expect(value).toEqual("resolution"); + }) + .catch(fail) + .then(done); }); + }); - it('return the binding when bindings are simple', function() { - var isSimple = Plots.hasSimpleAPICommandBindings(gd, [{ - method: 'restyle', - args: [{'marker.size': 10}] - }, { - method: 'restyle', - args: [{'marker.size': 20}] - }]); - - expect(isSimple).toEqual({ - type: 'data', - prop: 'marker.size', - traces: null, - value: 10 - }); + describe("with an unsuccessful command", function() { + beforeEach(function() { + spyOn(PlotlyInternal, "restyle").and.callFake(function() { + return Promise.reject("rejection"); + }); }); - it('return false when properties are not the same', function() { - var isSimple = Plots.hasSimpleAPICommandBindings(gd, [{ - method: 'restyle', - args: [{'marker.size': 10}] - }, { - method: 'restyle', - args: [{'marker.color': 20}] - }]); - - expect(isSimple).toBe(false); + it("calls the API method and rejects", function(done) { + Plots.executeAPICommand(gd, "restyle", ["foo", "bar"]) + .then(fail, function(value) { + var m = PlotlyInternal.restyle; + expect(m).toHaveBeenCalled(); + expect(m.calls.count()).toEqual(1); + expect(m.calls.argsFor(0)).toEqual([gd, "foo", "bar"]); + + expect(value).toEqual("rejection"); + }) + .catch(fail) + .then(done); }); + }); +}); - it('return false when a command binds to more than one property', function() { - var isSimple = Plots.hasSimpleAPICommandBindings(gd, [{ - method: 'restyle', - args: [{'marker.color': 10, 'marker.size': 12}] - }, { - method: 'restyle', - args: [{'marker.color': 20}] - }]); - - expect(isSimple).toBe(false); +describe("Plots.hasSimpleAPICommandBindings", function() { + "use strict"; + var gd; + beforeEach(function() { + gd = createGraphDiv(); + + Plotly.plot(gd, [ + { x: [1, 2, 3], y: [1, 2, 3] }, + { x: [1, 2, 3], y: [4, 5, 6] } + ]); + }); + + afterEach(function() { + destroyGraphDiv(gd); + }); + + it("return the binding when bindings are simple", function() { + var isSimple = Plots.hasSimpleAPICommandBindings(gd, [ + { method: "restyle", args: [{ "marker.size": 10 }] }, + { method: "restyle", args: [{ "marker.size": 20 }] } + ]); + + expect(isSimple).toEqual({ + type: "data", + prop: "marker.size", + traces: null, + value: 10 }); - - it('return false when commands affect different traces', function() { - var isSimple = Plots.hasSimpleAPICommandBindings(gd, [{ - method: 'restyle', - args: [{'marker.color': 10}, [0]] - }, { - method: 'restyle', - args: [{'marker.color': 20}, [1]] - }]); - - expect(isSimple).toBe(false); + }); + + it("return false when properties are not the same", function() { + var isSimple = Plots.hasSimpleAPICommandBindings(gd, [ + { method: "restyle", args: [{ "marker.size": 10 }] }, + { method: "restyle", args: [{ "marker.color": 20 }] } + ]); + + expect(isSimple).toBe(false); + }); + + it("return false when a command binds to more than one property", function() { + var isSimple = Plots.hasSimpleAPICommandBindings(gd, [ + { + method: "restyle", + args: [{ "marker.color": 10, "marker.size": 12 }] + }, + { method: "restyle", args: [{ "marker.color": 20 }] } + ]); + + expect(isSimple).toBe(false); + }); + + it("return false when commands affect different traces", function() { + var isSimple = Plots.hasSimpleAPICommandBindings(gd, [ + { method: "restyle", args: [{ "marker.color": 10 }, [0]] }, + { method: "restyle", args: [{ "marker.color": 20 }, [1]] } + ]); + + expect(isSimple).toBe(false); + }); + + it("return the binding when commands affect the same traces", function() { + var isSimple = Plots.hasSimpleAPICommandBindings(gd, [ + { method: "restyle", args: [{ "marker.color": 10 }, [1]] }, + { method: "restyle", args: [{ "marker.color": 20 }, [1]] } + ]); + + expect(isSimple).toEqual({ + type: "data", + prop: "marker.color", + traces: [1], + value: [10] }); - - it('return the binding when commands affect the same traces', function() { - var isSimple = Plots.hasSimpleAPICommandBindings(gd, [{ - method: 'restyle', - args: [{'marker.color': 10}, [1]] - }, { - method: 'restyle', - args: [{'marker.color': 20}, [1]] - }]); - - expect(isSimple).toEqual({ - type: 'data', - prop: 'marker.color', - traces: [ 1 ], - value: [ 10 ] - }); - }); - - it('return the binding when commands affect the same traces in different order', function() { - var isSimple = Plots.hasSimpleAPICommandBindings(gd, [{ - method: 'restyle', - args: [{'marker.color': 10}, [1, 2]] - }, { - method: 'restyle', - args: [{'marker.color': 20}, [2, 1]] - }]); - - // See https://github.com/plotly/plotly.js/issues/1169 for an example of where - // this logic was a little too sophisticated. It's better to bail out and omit - // functionality than to get it wrong. - expect(isSimple).toEqual(false); - - /* expect(isSimple).toEqual({ + }); + + it( + "return the binding when commands affect the same traces in different order", + function() { + var isSimple = Plots.hasSimpleAPICommandBindings(gd, [ + { method: "restyle", args: [{ "marker.color": 10 }, [1, 2]] }, + { method: "restyle", args: [{ "marker.color": 20 }, [2, 1]] } + ]); + + // See https://github.com/plotly/plotly.js/issues/1169 for an example of where + // this logic was a little too sophisticated. It's better to bail out and omit + // functionality than to get it wrong. + expect(isSimple).toEqual(false); + /* expect(isSimple).toEqual({ type: 'data', prop: 'marker.color', traces: [ 1, 2 ], value: [ 10, 10 ] - });*/ - }); + }); */ + } + ); }); -describe('Plots.computeAPICommandBindings', function() { - 'use strict'; - - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - - Plotly.plot(gd, [ - {x: [1, 2, 3], y: [1, 2, 3]}, - {x: [1, 2, 3], y: [4, 5, 6]}, - ]); - }); - - afterEach(function() { - destroyGraphDiv(gd); +describe("Plots.computeAPICommandBindings", function() { + "use strict"; + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + + Plotly.plot(gd, [ + { x: [1, 2, 3], y: [1, 2, 3] }, + { x: [1, 2, 3], y: [4, 5, 6] } + ]); + }); + + afterEach(function() { + destroyGraphDiv(gd); + }); + + describe("restyle", function() { + describe("with invalid notation", function() { + it("with a scalar value", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [["x"]]); + expect(result).toEqual([]); + }); }); - describe('restyle', function() { - describe('with invalid notation', function() { - it('with a scalar value', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [['x']]); - expect(result).toEqual([]); - }); + describe("with astr + val notation", function() { + describe("and a single attribute", function() { + it("with a scalar value", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + "marker.size", + 7 + ]); + expect(result).toEqual([ + { prop: "marker.size", traces: null, type: "data", value: 7 } + ]); }); - describe('with astr + val notation', function() { - describe('and a single attribute', function() { - it('with a scalar value', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', ['marker.size', 7]); - expect(result).toEqual([{prop: 'marker.size', traces: null, type: 'data', value: 7}]); - }); - - it('with an array value and no trace specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', ['marker.size', [7]]); - expect(result).toEqual([{prop: 'marker.size', traces: [0], type: 'data', value: [7]}]); - }); - - it('with trace specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', ['marker.size', 7, [0]]); - expect(result).toEqual([{prop: 'marker.size', traces: [0], type: 'data', value: [7]}]); - }); - - it('with a different trace specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', ['marker.size', 7, [0]]); - expect(result).toEqual([{prop: 'marker.size', traces: [0], type: 'data', value: [7]}]); - }); - - it('with an array value', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', ['marker.size', [7], [1]]); - expect(result).toEqual([{prop: 'marker.size', traces: [1], type: 'data', value: [7]}]); - }); - - it('with two array values and two traces specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', ['marker.size', [7, 5], [0, 1]]); - expect(result).toEqual([{prop: 'marker.size', traces: [0, 1], type: 'data', value: [7, 5]}]); - }); - - it('with traces specified in reverse order', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', ['marker.size', [7, 5], [1, 0]]); - expect(result).toEqual([{prop: 'marker.size', traces: [1, 0], type: 'data', value: [7, 5]}]); - }); - - it('with two values and a single trace specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', ['marker.size', [7, 5], [0]]); - expect(result).toEqual([{prop: 'marker.size', traces: [0], type: 'data', value: [7]}]); - }); - - it('with two values and a different trace specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', ['marker.size', [7, 5], [1]]); - expect(result).toEqual([{prop: 'marker.size', traces: [1], type: 'data', value: [7]}]); - }); - }); + it("with an array value and no trace specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + "marker.size", + [7] + ]); + expect(result).toEqual([ + { prop: "marker.size", traces: [0], type: "data", value: [7] } + ]); }); - - describe('with aobj notation', function() { - describe('and a single attribute', function() { - it('with a scalar value', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{'marker.size': 7}]); - expect(result).toEqual([{type: 'data', prop: 'marker.size', traces: null, value: 7}]); - }); - - it('with trace specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{'marker.size': 7}, [0]]); - expect(result).toEqual([{type: 'data', prop: 'marker.size', traces: [0], value: [7]}]); - }); - - it('with a different trace specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{'marker.size': 7}, [1]]); - expect(result).toEqual([{type: 'data', prop: 'marker.size', traces: [1], value: [7]}]); - }); - - it('with an array value', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{'marker.size': [7]}, [1]]); - expect(result).toEqual([{type: 'data', prop: 'marker.size', traces: [1], value: [7]}]); - }); - - it('with two array values and two traces specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{'marker.size': [7, 5]}, [0, 1]]); - expect(result).toEqual([{type: 'data', prop: 'marker.size', traces: [0, 1], value: [7, 5]}]); - }); - - it('with traces specified in reverse order', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{'marker.size': [7, 5]}, [1, 0]]); - expect(result).toEqual([{type: 'data', prop: 'marker.size', traces: [1, 0], value: [7, 5]}]); - }); - - it('with two values and a single trace specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{'marker.size': [7, 5]}, [0]]); - expect(result).toEqual([{type: 'data', prop: 'marker.size', traces: [0], value: [7]}]); - }); - - it('with two values and a different trace specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{'marker.size': [7, 5]}, [1]]); - expect(result).toEqual([{type: 'data', prop: 'marker.size', traces: [1], value: [7]}]); - }); - }); - - describe('and multiple attributes', function() { - it('with a scalar value', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{'marker.size': 7, 'text.color': 'blue'}]); - expect(result).toEqual([ - {type: 'data', prop: 'marker.size', traces: null, value: 7}, - {type: 'data', prop: 'text.color', traces: null, value: 'blue'} - ]); - }); - }); + it("with trace specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + "marker.size", + 7, + [0] + ]); + expect(result).toEqual([ + { prop: "marker.size", traces: [0], type: "data", value: [7] } + ]); }); - describe('with mixed notation', function() { - it('and nested object and nested attr', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{ - y: [[3, 4, 5]], - 'marker.size': [10, 20, 25], - 'line.color': 'red', - line: { - width: [2, 8] - } - }]); - - // The results are definitely not completely intuitive, so this - // is based upon empirical results with a codepen example: - expect(result).toEqual([ - {type: 'data', prop: 'y', traces: [0], value: [[3, 4, 5]]}, - {type: 'data', prop: 'marker.size', traces: [0, 1], value: [10, 20]}, - {type: 'data', prop: 'line.color', traces: null, value: 'red'}, - {type: 'data', prop: 'line.width', traces: [0, 1], value: [2, 8]} - ]); - }); - - it('and traces specified', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{ - y: [[3, 4, 5]], - 'marker.size': [10, 20, 25], - 'line.color': 'red', - line: { - width: [2, 8] - } - }, [1, 0]]); - - expect(result).toEqual([ - {type: 'data', prop: 'y', traces: [1], value: [[3, 4, 5]]}, - {type: 'data', prop: 'marker.size', traces: [1, 0], value: [10, 20]}, - - // This result is actually not quite correct. Setting `line` should override - // this—or actually it's technically undefined since the iteration order of - // objects is not strictly defined but is at least consistent across browsers. - // The worst-case scenario right now isn't too bad though since it's an obscure - // case that will definitely cause bailout anyway before any bindings would - // happen. - {type: 'data', prop: 'line.color', traces: [1, 0], value: ['red', 'red']}, - - {type: 'data', prop: 'line.width', traces: [1, 0], value: [2, 8]} - ]); - }); - - it('and more data than traces', function() { - var result = Plots.computeAPICommandBindings(gd, 'restyle', [{ - y: [[3, 4, 5]], - 'marker.size': [10, 20, 25], - 'line.color': 'red', - line: { - width: [2, 8] - } - }, [1]]); - - expect(result).toEqual([ - {type: 'data', prop: 'y', traces: [1], value: [[3, 4, 5]]}, - {type: 'data', prop: 'marker.size', traces: [1], value: [10]}, - {type: 'data', prop: 'line.color', traces: [1], value: ['red']}, - {type: 'data', prop: 'line.width', traces: [1], value: [2]} - ]); - }); + it("with a different trace specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + "marker.size", + 7, + [0] + ]); + expect(result).toEqual([ + { prop: "marker.size", traces: [0], type: "data", value: [7] } + ]); }); - }); - describe('relayout', function() { - describe('with invalid notation', function() { - it('and a scalar value', function() { - var result = Plots.computeAPICommandBindings(gd, 'relayout', [['x']]); - expect(result).toEqual([]); - }); + it("with an array value", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + "marker.size", + [7], + [1] + ]); + expect(result).toEqual([ + { prop: "marker.size", traces: [1], type: "data", value: [7] } + ]); }); - describe('with aobj notation', function() { - it('and a single attribute', function() { - var result = Plots.computeAPICommandBindings(gd, 'relayout', [{height: 500}]); - expect(result).toEqual([{type: 'layout', prop: 'height', value: 500}]); - }); - - it('and two attributes', function() { - var result = Plots.computeAPICommandBindings(gd, 'relayout', [{height: 500, width: 100}]); - expect(result).toEqual([{type: 'layout', prop: 'height', value: 500}, {type: 'layout', prop: 'width', value: 100}]); - }); + it("with two array values and two traces specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + "marker.size", + [7, 5], + [0, 1] + ]); + expect(result).toEqual([ + { prop: "marker.size", traces: [0, 1], type: "data", value: [7, 5] } + ]); }); - describe('with astr + val notation', function() { - it('and an attribute', function() { - var result = Plots.computeAPICommandBindings(gd, 'relayout', ['width', 100]); - expect(result).toEqual([{type: 'layout', prop: 'width', value: 100}]); - }); + it("with traces specified in reverse order", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + "marker.size", + [7, 5], + [1, 0] + ]); + expect(result).toEqual([ + { prop: "marker.size", traces: [1, 0], type: "data", value: [7, 5] } + ]); + }); - it('and nested atributes', function() { - var result = Plots.computeAPICommandBindings(gd, 'relayout', ['margin.l', 10]); - expect(result).toEqual([{type: 'layout', prop: 'margin.l', value: 10}]); - }); + it("with two values and a single trace specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + "marker.size", + [7, 5], + [0] + ]); + expect(result).toEqual([ + { prop: "marker.size", traces: [0], type: "data", value: [7] } + ]); }); - describe('with mixed notation', function() { - it('containing aob + astr', function() { - var result = Plots.computeAPICommandBindings(gd, 'relayout', [{ - 'width': 100, - 'margin.l': 10 - }]); - expect(result).toEqual([ - {type: 'layout', prop: 'width', value: 100}, - {type: 'layout', prop: 'margin.l', value: 10} - ]); - }); + it("with two values and a different trace specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + "marker.size", + [7, 5], + [1] + ]); + expect(result).toEqual([ + { prop: "marker.size", traces: [1], type: "data", value: [7] } + ]); }); + }); }); - describe('update', function() { - it('computes bindings', function() { - var result = Plots.computeAPICommandBindings(gd, 'update', [{ - y: [[3, 4, 5]], - 'marker.size': [10, 20, 25], - 'line.color': 'red', - line: { - width: [2, 8] - } - }, { - 'margin.l': 50, - width: 10 - }, [1]]); - - expect(result).toEqual([ - {type: 'data', prop: 'y', traces: [1], value: [[3, 4, 5]]}, - {type: 'data', prop: 'marker.size', traces: [1], value: [10]}, - {type: 'data', prop: 'line.color', traces: [1], value: ['red']}, - {type: 'data', prop: 'line.width', traces: [1], value: [2]}, - {type: 'layout', prop: 'margin.l', value: 50}, - {type: 'layout', prop: 'width', value: 10} - ]); + describe("with aobj notation", function() { + describe("and a single attribute", function() { + it("with a scalar value", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { "marker.size": 7 } + ]); + expect(result).toEqual([ + { type: "data", prop: "marker.size", traces: null, value: 7 } + ]); }); - }); - describe('animate', function() { - it('binds to the frame for a simple animate command', function() { - var result = Plots.computeAPICommandBindings(gd, 'animate', [['framename']]); + it("with trace specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { "marker.size": 7 }, + [0] + ]); + expect(result).toEqual([ + { type: "data", prop: "marker.size", traces: [0], value: [7] } + ]); + }); + + it("with a different trace specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { "marker.size": 7 }, + [1] + ]); + expect(result).toEqual([ + { type: "data", prop: "marker.size", traces: [1], value: [7] } + ]); + }); - expect(result).toEqual([{type: 'layout', prop: '_currentFrame', value: 'framename'}]); + it("with an array value", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { "marker.size": [7] }, + [1] + ]); + expect(result).toEqual([ + { type: "data", prop: "marker.size", traces: [1], value: [7] } + ]); }); - it('treats numeric frame names as strings', function() { - var result = Plots.computeAPICommandBindings(gd, 'animate', [[8]]); + it("with two array values and two traces specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { "marker.size": [7, 5] }, + [0, 1] + ]); + expect(result).toEqual([ + { type: "data", prop: "marker.size", traces: [0, 1], value: [7, 5] } + ]); + }); - expect(result).toEqual([{type: 'layout', prop: '_currentFrame', value: '8'}]); + it("with traces specified in reverse order", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { "marker.size": [7, 5] }, + [1, 0] + ]); + expect(result).toEqual([ + { type: "data", prop: "marker.size", traces: [1, 0], value: [7, 5] } + ]); }); - it('binds to nothing for a multi-frame animate command', function() { - var result = Plots.computeAPICommandBindings(gd, 'animate', [['frame1', 'frame2']]); + it("with two values and a single trace specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { "marker.size": [7, 5] }, + [0] + ]); + expect(result).toEqual([ + { type: "data", prop: "marker.size", traces: [0], value: [7] } + ]); + }); - expect(result).toEqual([]); + it("with two values and a different trace specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { "marker.size": [7, 5] }, + [1] + ]); + expect(result).toEqual([ + { type: "data", prop: "marker.size", traces: [1], value: [7] } + ]); + }); + }); + + describe("and multiple attributes", function() { + it("with a scalar value", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { "marker.size": 7, "text.color": "blue" } + ]); + expect(result).toEqual([ + { type: "data", prop: "marker.size", traces: null, value: 7 }, + { type: "data", prop: "text.color", traces: null, value: "blue" } + ]); }); + }); }); -}); -describe('component bindings', function() { - 'use strict'; + describe("with mixed notation", function() { + it("and nested object and nested attr", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { + y: [[3, 4, 5]], + "marker.size": [10, 20, 25], + "line.color": "red", + line: { width: [2, 8] } + } + ]); - var gd; - var mock = require('@mocks/binding.json'); + // The results are definitely not completely intuitive, so this + // is based upon empirical results with a codepen example: + expect(result).toEqual([ + { type: "data", prop: "y", traces: [0], value: [[3, 4, 5]] }, + { + type: "data", + prop: "marker.size", + traces: [0, 1], + value: [10, 20] + }, + { type: "data", prop: "line.color", traces: null, value: "red" }, + { type: "data", prop: "line.width", traces: [0, 1], value: [2, 8] } + ]); + }); + + it("and traces specified", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { + y: [[3, 4, 5]], + "marker.size": [10, 20, 25], + "line.color": "red", + line: { width: [2, 8] } + }, + [1, 0] + ]); - beforeEach(function(done) { - var mockCopy = Lib.extendDeep({}, mock); - gd = createGraphDiv(); + expect(result).toEqual([ + { type: "data", prop: "y", traces: [1], value: [[3, 4, 5]] }, + { + type: "data", + prop: "marker.size", + traces: [1, 0], + value: [10, 20] + }, + // This result is actually not quite correct. Setting `line` should override + // this—or actually it's technically undefined since the iteration order of + // objects is not strictly defined but is at least consistent across browsers. + // The worst-case scenario right now isn't too bad though since it's an obscure + // case that will definitely cause bailout anyway before any bindings would + // happen. + { + type: "data", + prop: "line.color", + traces: [1, 0], + value: ["red", "red"] + }, + { + type: "data", + prop: "line.width", + traces: [1, 0], + value: [2, 8] + } + ]); + }); + + it("and more data than traces", function() { + var result = Plots.computeAPICommandBindings(gd, "restyle", [ + { + y: [[3, 4, 5]], + "marker.size": [10, 20, 25], + "line.color": "red", + line: { width: [2, 8] } + }, + [1] + ]); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + expect(result).toEqual([ + { type: "data", prop: "y", traces: [1], value: [[3, 4, 5]] }, + { type: "data", prop: "marker.size", traces: [1], value: [10] }, + { type: "data", prop: "line.color", traces: [1], value: ["red"] }, + { type: "data", prop: "line.width", traces: [1], value: [2] } + ]); + }); }); - - afterEach(function() { - destroyGraphDiv(gd); + }); + + describe("relayout", function() { + describe("with invalid notation", function() { + it("and a scalar value", function() { + var result = Plots.computeAPICommandBindings(gd, "relayout", [["x"]]); + expect(result).toEqual([]); + }); }); - it('creates an observer', function(done) { - var count = 0; - Plots.manageCommandObserver(gd, {}, [ - { method: 'restyle', args: ['marker.color', 'red'] }, - { method: 'restyle', args: ['marker.color', 'green'] } - ], function(data) { - count++; - expect(data.index).toEqual(1); - }); + describe("with aobj notation", function() { + it("and a single attribute", function() { + var result = Plots.computeAPICommandBindings(gd, "relayout", [ + { height: 500 } + ]); + expect(result).toEqual([ + { type: "layout", prop: "height", value: 500 } + ]); + }); - // Doesn't trigger the callback: - Plotly.relayout(gd, 'width', 400).then(function() { - // Triggers the callback: - return Plotly.restyle(gd, 'marker.color', 'green'); - }).then(function() { - // Doesn't trigger a callback: - return Plotly.restyle(gd, 'marker.width', 8); - }).then(function() { - expect(count).toEqual(1); - }).catch(fail).then(done); + it("and two attributes", function() { + var result = Plots.computeAPICommandBindings(gd, "relayout", [ + { height: 500, width: 100 } + ]); + expect(result).toEqual([ + { type: "layout", prop: "height", value: 500 }, + { type: "layout", prop: "width", value: 100 } + ]); + }); }); - it('logs a warning if unable to create an observer', function() { - var warnings = 0; - spyOn(Lib, 'warn').and.callFake(function() { - warnings++; - }); + describe("with astr + val notation", function() { + it("and an attribute", function() { + var result = Plots.computeAPICommandBindings(gd, "relayout", [ + "width", + 100 + ]); + expect(result).toEqual([{ type: "layout", prop: "width", value: 100 }]); + }); - Plots.manageCommandObserver(gd, {}, [ - { method: 'restyle', args: ['marker.color', 'red'] }, - { method: 'restyle', args: [{'line.color': 'green', 'marker.color': 'green'}] } + it("and nested atributes", function() { + var result = Plots.computeAPICommandBindings(gd, "relayout", [ + "margin.l", + 10 + ]); + expect(result).toEqual([ + { type: "layout", prop: "margin.l", value: 10 } ]); + }); + }); - expect(warnings).toEqual(1); + describe("with mixed notation", function() { + it("containing aob + astr", function() { + var result = Plots.computeAPICommandBindings(gd, "relayout", [ + { width: 100, "margin.l": 10 } + ]); + expect(result).toEqual([ + { type: "layout", prop: "width", value: 100 }, + { type: "layout", prop: "margin.l", value: 10 } + ]); + }); + }); + }); + + describe("update", function() { + it("computes bindings", function() { + var result = Plots.computeAPICommandBindings(gd, "update", [ + { + y: [[3, 4, 5]], + "marker.size": [10, 20, 25], + "line.color": "red", + line: { width: [2, 8] } + }, + { "margin.l": 50, width: 10 }, + [1] + ]); + + expect(result).toEqual([ + { type: "data", prop: "y", traces: [1], value: [[3, 4, 5]] }, + { type: "data", prop: "marker.size", traces: [1], value: [10] }, + { type: "data", prop: "line.color", traces: [1], value: ["red"] }, + { type: "data", prop: "line.width", traces: [1], value: [2] }, + { type: "layout", prop: "margin.l", value: 50 }, + { type: "layout", prop: "width", value: 10 } + ]); }); + }); - it('udpates bound components when the value changes', function(done) { - expect(gd.layout.sliders[0].active).toBe(0); + describe("animate", function() { + it("binds to the frame for a simple animate command", function() { + var result = Plots.computeAPICommandBindings(gd, "animate", [ + ["framename"] + ]); - Plotly.restyle(gd, 'marker.color', 'blue').then(function() { - expect(gd.layout.sliders[0].active).toBe(4); - }).catch(fail).then(done); + expect(result).toEqual([ + { type: "layout", prop: "_currentFrame", value: "framename" } + ]); }); - it('does not update the component if the value is not present', function(done) { - expect(gd.layout.sliders[0].active).toBe(0); + it("treats numeric frame names as strings", function() { + var result = Plots.computeAPICommandBindings(gd, "animate", [[8]]); - Plotly.restyle(gd, 'marker.color', 'black').then(function() { - expect(gd.layout.sliders[0].active).toBe(0); - }).catch(fail).then(done); + expect(result).toEqual([ + { type: "layout", prop: "_currentFrame", value: "8" } + ]); }); - it('udpates bound components when the computed value changes', function(done) { - expect(gd.layout.sliders[0].active).toBe(0); + it("binds to nothing for a multi-frame animate command", function() { + var result = Plots.computeAPICommandBindings(gd, "animate", [ + ["frame1", "frame2"] + ]); - // The default line color comes from the marker color, if specified. - // That is, the fact that the marker color changes is just incidental, but - // nonetheless is bound by value to the component. - Plotly.restyle(gd, 'line.color', 'blue').then(function() { - expect(gd.layout.sliders[0].active).toBe(4); - }).catch(fail).then(done); + expect(result).toEqual([]); }); + }); }); -describe('attaching component bindings', function() { - 'use strict'; - var gd; - - beforeEach(function(done) { - gd = createGraphDiv(); - Plotly.plot(gd, [{x: [1, 2, 3], y: [1, 2, 3]}]).then(done); +describe("component bindings", function() { + "use strict"; + var gd; + var mock = require("@mocks/binding.json"); + + beforeEach(function(done) { + var mockCopy = Lib.extendDeep({}, mock); + gd = createGraphDiv(); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + afterEach(function() { + destroyGraphDiv(gd); + }); + + it("creates an observer", function(done) { + var count = 0; + Plots.manageCommandObserver( + gd, + {}, + [ + { method: "restyle", args: ["marker.color", "red"] }, + { method: "restyle", args: ["marker.color", "green"] } + ], + function(data) { + count++; + expect(data.index).toEqual(1); + } + ); + + // Doesn't trigger the callback: + Plotly.relayout(gd, "width", 400) + .then(function() { + // Triggers the callback: + return Plotly.restyle(gd, "marker.color", "green"); + }) + .then(function() { + // Doesn't trigger a callback: + return Plotly.restyle(gd, "marker.width", 8); + }) + .then(function() { + expect(count).toEqual(1); + }) + .catch(fail) + .then(done); + }); + + it("logs a warning if unable to create an observer", function() { + var warnings = 0; + spyOn(Lib, "warn").and.callFake(function() { + warnings++; }); - afterEach(function() { - destroyGraphDiv(gd); - }); + Plots.manageCommandObserver(gd, {}, [ + { method: "restyle", args: ["marker.color", "red"] }, + { + method: "restyle", + args: [{ "line.color": "green", "marker.color": "green" }] + } + ]); + + expect(warnings).toEqual(1); + }); + + it("udpates bound components when the value changes", function(done) { + expect(gd.layout.sliders[0].active).toBe(0); + + Plotly.restyle(gd, "marker.color", "blue") + .then(function() { + expect(gd.layout.sliders[0].active).toBe(4); + }) + .catch(fail) + .then(done); + }); + + it("does not update the component if the value is not present", function( + done + ) { + expect(gd.layout.sliders[0].active).toBe(0); + + Plotly.restyle(gd, "marker.color", "black") + .then(function() { + expect(gd.layout.sliders[0].active).toBe(0); + }) + .catch(fail) + .then(done); + }); + + it("udpates bound components when the computed value changes", function( + done + ) { + expect(gd.layout.sliders[0].active).toBe(0); + + // The default line color comes from the marker color, if specified. + // That is, the fact that the marker color changes is just incidental, but + // nonetheless is bound by value to the component. + Plotly.restyle(gd, "line.color", "blue") + .then(function() { + expect(gd.layout.sliders[0].active).toBe(4); + }) + .catch(fail) + .then(done); + }); +}); - it('attaches and updates bindings for sliders', function(done) { - expect(gd._internalEv._events.plotly_animatingframe).toBeUndefined(); +describe("attaching component bindings", function() { + "use strict"; + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]).then(done); + }); + + afterEach(function() { + destroyGraphDiv(gd); + }); + + it("attaches and updates bindings for sliders", function(done) { + expect(gd._internalEv._events.plotly_animatingframe).toBeUndefined(); + + Plotly.relayout(gd, { + sliders: [ + { + // This one gets bindings: + steps: [ + { + label: "first", + method: "restyle", + args: ["marker.color", "red"] + }, + { + label: "second", + method: "restyle", + args: ["marker.color", "blue"] + } + ] + }, + { + // This one does *not*: + steps: [ + { + label: "first", + method: "restyle", + args: ["line.color", "red"] + }, + { + label: "second", + method: "restyle", + args: ["marker.color", "blue"] + } + ] + } + ] + }) + .then(function() { + // Check that it has attached a listener: + expect(typeof gd._internalEv._events.plotly_animatingframe).toBe( + "function" + ); + + // Confirm the first position is selected: + expect(gd.layout.sliders[0].active).toBeUndefined(); + + // Modify the plot + return Plotly.restyle(gd, { "marker.color": "blue" }); + }) + .then(function() { + // Confirm that this has changed the slider position: + expect(gd.layout.sliders[0].active).toBe(1); + + // Swap the values of the components: + return Plotly.relayout(gd, { + "sliders[0].steps[0].args[1]": "green", + "sliders[0].steps[1].args[1]": "red" + }); + }) + .then(function() { + return Plotly.restyle(gd, { "marker.color": "green" }); + }) + .then(function() { + // Confirm that the lookup table has been updated: + expect(gd.layout.sliders[0].active).toBe(0); - Plotly.relayout(gd, { - sliders: [{ - // This one gets bindings: - steps: [ - {label: 'first', method: 'restyle', args: ['marker.color', 'red']}, - {label: 'second', method: 'restyle', args: ['marker.color', 'blue']}, - ] - }, { - // This one does *not*: - steps: [ - {label: 'first', method: 'restyle', args: ['line.color', 'red']}, - {label: 'second', method: 'restyle', args: ['marker.color', 'blue']}, - ] - }] - }).then(function() { - // Check that it has attached a listener: - expect(typeof gd._internalEv._events.plotly_animatingframe).toBe('function'); - - // Confirm the first position is selected: - expect(gd.layout.sliders[0].active).toBeUndefined(); - - // Modify the plot - return Plotly.restyle(gd, {'marker.color': 'blue'}); - }).then(function() { - // Confirm that this has changed the slider position: - expect(gd.layout.sliders[0].active).toBe(1); - - // Swap the values of the components: - return Plotly.relayout(gd, { - 'sliders[0].steps[0].args[1]': 'green', - 'sliders[0].steps[1].args[1]': 'red' - }); - }).then(function() { - return Plotly.restyle(gd, {'marker.color': 'green'}); - }).then(function() { - // Confirm that the lookup table has been updated: - expect(gd.layout.sliders[0].active).toBe(0); - - // Check that it still has one attached listener: - expect(typeof gd._internalEv._events.plotly_animatingframe).toBe('function'); - - // Change this to a non-simple binding: - return Plotly.relayout(gd, {'sliders[0].steps[0].args[0]': 'line.color'}); - }).then(function() { - // Bindings are no longer simple, so check to ensure they have - // been removed - expect(gd._internalEv._events.plotly_animatingframe).toBeUndefined(); - }).catch(fail).then(done); - }); + // Check that it still has one attached listener: + expect(typeof gd._internalEv._events.plotly_animatingframe).toBe( + "function" + ); - it('attaches and updates bindings for updatemenus', function(done) { + // Change this to a non-simple binding: + return Plotly.relayout(gd, { + "sliders[0].steps[0].args[0]": "line.color" + }); + }) + .then(function() { + // Bindings are no longer simple, so check to ensure they have + // been removed expect(gd._internalEv._events.plotly_animatingframe).toBeUndefined(); - - Plotly.relayout(gd, { - updatemenus: [{ - // This one gets bindings: - buttons: [ - {label: 'first', method: 'restyle', args: ['marker.color', 'red']}, - {label: 'second', method: 'restyle', args: ['marker.color', 'blue']}, - ] - }, { - // This one does *not*: - buttons: [ - {label: 'first', method: 'restyle', args: ['line.color', 'red']}, - {label: 'second', method: 'restyle', args: ['marker.color', 'blue']}, - ] - }] - }).then(function() { - // Check that it has attached a listener: - expect(typeof gd._internalEv._events.plotly_animatingframe).toBe('function'); - - // Confirm the first position is selected: - expect(gd.layout.updatemenus[0].active).toBeUndefined(); - - // Modify the plot - return Plotly.restyle(gd, {'marker.color': 'blue'}); - }).then(function() { - // Confirm that this has changed the slider position: - expect(gd.layout.updatemenus[0].active).toBe(1); - - // Swap the values of the components: - return Plotly.relayout(gd, { - 'updatemenus[0].buttons[0].args[1]': 'green', - 'updatemenus[0].buttons[1].args[1]': 'red' - }); - }).then(function() { - return Plotly.restyle(gd, {'marker.color': 'green'}); - }).then(function() { - // Confirm that the lookup table has been updated: - expect(gd.layout.updatemenus[0].active).toBe(0); - - // Check that it still has one attached listener: - expect(typeof gd._internalEv._events.plotly_animatingframe).toBe('function'); - - // Change this to a non-simple binding: - return Plotly.relayout(gd, {'updatemenus[0].buttons[0].args[0]': 'line.color'}); - }).then(function() { - // Bindings are no longer simple, so check to ensure they have - // been removed - expect(gd._internalEv._events.plotly_animatingframe).toBeUndefined(); - }).catch(fail).then(done); - }); + }) + .catch(fail) + .then(done); + }); + + it("attaches and updates bindings for updatemenus", function(done) { + expect(gd._internalEv._events.plotly_animatingframe).toBeUndefined(); + + Plotly.relayout(gd, { + updatemenus: [ + { + // This one gets bindings: + buttons: [ + { + label: "first", + method: "restyle", + args: ["marker.color", "red"] + }, + { + label: "second", + method: "restyle", + args: ["marker.color", "blue"] + } + ] + }, + { + // This one does *not*: + buttons: [ + { + label: "first", + method: "restyle", + args: ["line.color", "red"] + }, + { + label: "second", + method: "restyle", + args: ["marker.color", "blue"] + } + ] + } + ] + }) + .then(function() { + // Check that it has attached a listener: + expect(typeof gd._internalEv._events.plotly_animatingframe).toBe( + "function" + ); + + // Confirm the first position is selected: + expect(gd.layout.updatemenus[0].active).toBeUndefined(); + + // Modify the plot + return Plotly.restyle(gd, { "marker.color": "blue" }); + }) + .then(function() { + // Confirm that this has changed the slider position: + expect(gd.layout.updatemenus[0].active).toBe(1); + + // Swap the values of the components: + return Plotly.relayout(gd, { + "updatemenus[0].buttons[0].args[1]": "green", + "updatemenus[0].buttons[1].args[1]": "red" + }); + }) + .then(function() { + return Plotly.restyle(gd, { "marker.color": "green" }); + }) + .then(function() { + // Confirm that the lookup table has been updated: + expect(gd.layout.updatemenus[0].active).toBe(0); + + // Check that it still has one attached listener: + expect(typeof gd._internalEv._events.plotly_animatingframe).toBe( + "function" + ); + + // Change this to a non-simple binding: + return Plotly.relayout(gd, { + "updatemenus[0].buttons[0].args[0]": "line.color" + }); + }) + .then(function() { + // Bindings are no longer simple, so check to ensure they have + // been removed + expect(gd._internalEv._events.plotly_animatingframe).toBeUndefined(); + }) + .catch(fail) + .then(done); + }); }); diff --git a/test/jasmine/tests/compute_frame_test.js b/test/jasmine/tests/compute_frame_test.js index 9010838ee5a..6a800464d83 100644 --- a/test/jasmine/tests/compute_frame_test.js +++ b/test/jasmine/tests/compute_frame_test.js @@ -1,236 +1,217 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var computeFrame = require('@src/plots/plots').computeFrame; +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var computeFrame = require("@src/plots/plots").computeFrame; function clone(obj) { - return Lib.extendDeep({}, obj); + return Lib.extendDeep({}, obj); } -describe('Test mergeFrames', function() { - 'use strict'; +describe("Test mergeFrames", function() { + "use strict"; + var gd, mock; - var gd, mock; + beforeEach(function(done) { + mock = [{ x: [1, 2, 3], y: [2, 1, 3] }, { x: [1, 2, 3], y: [6, 4, 5] }]; + gd = createGraphDiv(); + Plotly.plot(gd, mock).then(done); + }); + + afterEach(destroyGraphDiv); + + describe("computing a single frame", function() { + var frame1, input; beforeEach(function(done) { - mock = [{x: [1, 2, 3], y: [2, 1, 3]}, {x: [1, 2, 3], y: [6, 4, 5]}]; - gd = createGraphDiv(); - Plotly.plot(gd, mock).then(done); - }); - - afterEach(destroyGraphDiv); - - describe('computing a single frame', function() { - var frame1, input; - - beforeEach(function(done) { - frame1 = { - name: 'frame1', - data: [{ - x: [1, 2, 3], - 'marker.size': 8, - marker: {color: 'red'} - }] - }; - - input = clone(frame1); - Plotly.addFrames(gd, [input]).then(done); - }); - - it('returns false if the frame does not exist', function() { - expect(computeFrame(gd, 'frame8')).toBe(false); - }); - - it('returns a new object', function() { - var result = computeFrame(gd, 'frame1'); - expect(result).not.toBe(input); - }); - - it('copies objects', function() { - var result = computeFrame(gd, 'frame1'); - expect(result.data).not.toBe(input.data); - expect(result.data[0].marker).not.toBe(input.data[0].marker); - }); - - it('does NOT copy arrays', function() { - var result = computeFrame(gd, 'frame1'); - expect(result.data[0].x).toBe(input.data[0].x); - }); - - it('computes a single frame', function() { - var computed = computeFrame(gd, 'frame1'); - var expected = {data: [{x: [1, 2, 3], marker: {size: 8, color: 'red'}}], traces: [0]}; - expect(computed).toEqual(expected); - }); - - it('leaves the frame unaffected', function() { - computeFrame(gd, 'frame1'); - expect(gd._transitionData._frameHash.frame1).toEqual(frame1); - }); - }); - - describe('circularly defined frames', function() { - var frames, results; - - beforeEach(function(done) { - frames = [ - {name: 'frame0', baseframe: 'frame1', data: [{'marker.size': 0}]}, - {name: 'frame1', baseframe: 'frame2', data: [{'marker.size': 1}]}, - {name: 'frame2', baseframe: 'frame0', data: [{'marker.size': 2}]} - ]; - - results = [ - {traces: [0], data: [{marker: {size: 0}}]}, - {traces: [0], data: [{marker: {size: 1}}]}, - {traces: [0], data: [{marker: {size: 2}}]} - ]; - - Plotly.addFrames(gd, frames).then(done); - }); - - function doTest(i) { - it('avoid infinite recursion (starting point = ' + i + ')', function() { - var result = computeFrame(gd, 'frame' + i); - expect(result).toEqual(results[i]); - }); - } + frame1 = { + name: "frame1", + data: [{ x: [1, 2, 3], "marker.size": 8, marker: { color: "red" } }] + }; + + input = clone(frame1); + Plotly.addFrames(gd, [input]).then(done); + }); + + it("returns false if the frame does not exist", function() { + expect(computeFrame(gd, "frame8")).toBe(false); + }); + + it("returns a new object", function() { + var result = computeFrame(gd, "frame1"); + expect(result).not.toBe(input); + }); + + it("copies objects", function() { + var result = computeFrame(gd, "frame1"); + expect(result.data).not.toBe(input.data); + expect(result.data[0].marker).not.toBe(input.data[0].marker); + }); + + it("does NOT copy arrays", function() { + var result = computeFrame(gd, "frame1"); + expect(result.data[0].x).toBe(input.data[0].x); + }); - for(var ii = 0; ii < 3; ii++) { - doTest(ii); + it("computes a single frame", function() { + var computed = computeFrame(gd, "frame1"); + var expected = { + data: [{ x: [1, 2, 3], marker: { size: 8, color: "red" } }], + traces: [0] + }; + expect(computed).toEqual(expected); + }); + + it("leaves the frame unaffected", function() { + computeFrame(gd, "frame1"); + expect(gd._transitionData._frameHash.frame1).toEqual(frame1); + }); + }); + + describe("circularly defined frames", function() { + var frames, results; + + beforeEach(function(done) { + frames = [ + { name: "frame0", baseframe: "frame1", data: [{ "marker.size": 0 }] }, + { name: "frame1", baseframe: "frame2", data: [{ "marker.size": 1 }] }, + { name: "frame2", baseframe: "frame0", data: [{ "marker.size": 2 }] } + ]; + + results = [ + { traces: [0], data: [{ marker: { size: 0 } }] }, + { traces: [0], data: [{ marker: { size: 1 } }] }, + { traces: [0], data: [{ marker: { size: 2 } }] } + ]; + + Plotly.addFrames(gd, frames).then(done); + }); + + function doTest(i) { + it("avoid infinite recursion (starting point = " + i + ")", function() { + var result = computeFrame(gd, "frame" + i); + expect(result).toEqual(results[i]); + }); + } + + for (var ii = 0; ii < 3; ii++) { + doTest(ii); + } + }); + + describe("computing trace data", function() { + var frames; + + beforeEach(function() { + frames = [ + { name: "frame0", data: [{ "marker.size": 0 }], traces: [2] }, + { name: "frame1", data: [{ "marker.size": 1 }], traces: [8] }, + { name: "frame2", data: [{ "marker.size": 2 }], traces: [2] }, + { + name: "frame3", + data: [{ "marker.size": 3 }, { "marker.size": 4 }], + traces: [2, 8] + }, + { + name: "frame4", + data: [ + { "marker.size": 5 }, + { "marker.size": 6 }, + { "marker.size": 7 } + ] } + ]; + }); + + it("merges orthogonal traces", function() { + frames[0].baseframe = frames[1].name; + + // This technically returns a promise, but it's not actually asynchronous so + // that we'll just keep this synchronous: + Plotly.addFrames(gd, frames.map(clone)); + + expect(computeFrame(gd, "frame0")).toEqual({ + traces: [8, 2], + data: [{ marker: { size: 1 } }, { marker: { size: 0 } }] + }); + + // Verify that the frames are untouched (by value, at least, but they should + // also be unmodified by identity too) by the computation: + expect(gd._transitionData._frames).toEqual(frames); + }); + + it("merges overlapping traces", function() { + frames[0].baseframe = frames[2].name; + + Plotly.addFrames(gd, frames.map(clone)); + + expect(computeFrame(gd, "frame0")).toEqual({ + traces: [2], + data: [{ marker: { size: 0 } }] + }); + + expect(gd._transitionData._frames).toEqual(frames); + }); + + it("merges partially overlapping traces", function() { + frames[0].baseframe = frames[1].name; + frames[1].baseframe = frames[2].name; + frames[2].baseframe = frames[3].name; + + Plotly.addFrames(gd, frames.map(clone)); + + expect(computeFrame(gd, "frame0")).toEqual({ + traces: [2, 8], + data: [{ marker: { size: 0 } }, { marker: { size: 1 } }] + }); + + expect(gd._transitionData._frames).toEqual(frames); + }); + + it("assumes serial order without traces specified", function() { + frames[4].baseframe = frames[3].name; + + Plotly.addFrames(gd, frames.map(clone)); + + expect(computeFrame(gd, "frame4")).toEqual({ + traces: [2, 8, 0, 1], + data: [ + { marker: { size: 7 } }, + { marker: { size: 4 } }, + { marker: { size: 5 } }, + { marker: { size: 6 } } + ] + }); + + expect(gd._transitionData._frames).toEqual(frames); + }); + }); + + describe("computing trace layout", function() { + var frames, frameCopies; + + beforeEach(function(done) { + frames = [ + { name: "frame0", layout: { "margin.l": 40 } }, + { name: "frame1", layout: { "margin.l": 80 } } + ]; + + frameCopies = frames.map(clone); + + Plotly.addFrames(gd, frames).then(done); + }); + + it("merges layouts", function() { + frames[0].baseframe = frames[1].name; + var result = computeFrame(gd, "frame0"); + + expect(result).toEqual({ layout: { margin: { l: 40 } } }); }); - describe('computing trace data', function() { - var frames; - - beforeEach(function() { - frames = [{ - name: 'frame0', - data: [{'marker.size': 0}], - traces: [2] - }, { - name: 'frame1', - data: [{'marker.size': 1}], - traces: [8] - }, { - name: 'frame2', - data: [{'marker.size': 2}], - traces: [2] - }, { - name: 'frame3', - data: [{'marker.size': 3}, {'marker.size': 4}], - traces: [2, 8] - }, { - name: 'frame4', - data: [ - {'marker.size': 5}, - {'marker.size': 6}, - {'marker.size': 7} - ] - }]; - }); - - it('merges orthogonal traces', function() { - frames[0].baseframe = frames[1].name; - - // This technically returns a promise, but it's not actually asynchronous so - // that we'll just keep this synchronous: - Plotly.addFrames(gd, frames.map(clone)); - - expect(computeFrame(gd, 'frame0')).toEqual({ - traces: [8, 2], - data: [ - {marker: {size: 1}}, - {marker: {size: 0}} - ] - }); - - // Verify that the frames are untouched (by value, at least, but they should - // also be unmodified by identity too) by the computation: - expect(gd._transitionData._frames).toEqual(frames); - }); - - it('merges overlapping traces', function() { - frames[0].baseframe = frames[2].name; - - Plotly.addFrames(gd, frames.map(clone)); - - expect(computeFrame(gd, 'frame0')).toEqual({ - traces: [2], - data: [{marker: {size: 0}}] - }); - - expect(gd._transitionData._frames).toEqual(frames); - }); - - it('merges partially overlapping traces', function() { - frames[0].baseframe = frames[1].name; - frames[1].baseframe = frames[2].name; - frames[2].baseframe = frames[3].name; - - Plotly.addFrames(gd, frames.map(clone)); - - expect(computeFrame(gd, 'frame0')).toEqual({ - traces: [2, 8], - data: [ - {marker: {size: 0}}, - {marker: {size: 1}} - ] - }); - - expect(gd._transitionData._frames).toEqual(frames); - }); - - it('assumes serial order without traces specified', function() { - frames[4].baseframe = frames[3].name; - - Plotly.addFrames(gd, frames.map(clone)); - - expect(computeFrame(gd, 'frame4')).toEqual({ - traces: [2, 8, 0, 1], - data: [ - {marker: {size: 7}}, - {marker: {size: 4}}, - {marker: {size: 5}}, - {marker: {size: 6}} - ] - }); - - expect(gd._transitionData._frames).toEqual(frames); - }); - }); - - describe('computing trace layout', function() { - var frames, frameCopies; - - beforeEach(function(done) { - frames = [{ - name: 'frame0', - layout: {'margin.l': 40} - }, { - name: 'frame1', - layout: {'margin.l': 80} - }]; - - frameCopies = frames.map(clone); - - Plotly.addFrames(gd, frames).then(done); - }); - - it('merges layouts', function() { - frames[0].baseframe = frames[1].name; - var result = computeFrame(gd, 'frame0'); - - expect(result).toEqual({ - layout: {margin: {l: 40}} - }); - }); - - it('leaves the frame unaffected', function() { - computeFrame(gd, 'frame0'); - expect(gd._transitionData._frames).toEqual(frameCopies); - }); + it("leaves the frame unaffected", function() { + computeFrame(gd, "frame0"); + expect(gd._transitionData._frames).toEqual(frameCopies); }); + }); }); diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index f066d4a1f07..649031426f9 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -1,272 +1,266 @@ -var Plotly = require('@lib/index'); +var Plotly = require("@lib/index"); var Plots = Plotly.Plots; -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); - -describe('config argument', function() { - - describe('attribute layout.autosize', function() { - var layoutWidth = 1111, - relayoutWidth = 555, - containerWidthBeforePlot = 888, - containerWidthBeforeRelayout = 666, - containerHeightBeforePlot = 543, - containerHeightBeforeRelayout = 321, - data = [], - gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - function checkLayoutSize(width, height) { - expect(gd._fullLayout.width).toBe(width); - expect(gd._fullLayout.height).toBe(height); - - var svg = document.getElementsByClassName('main-svg')[0]; - expect(+svg.getAttribute('width')).toBe(width); - expect(+svg.getAttribute('height')).toBe(height); - } - - function compareLayoutAndFullLayout(gd) { - expect(gd.layout.width).toBe(gd._fullLayout.width); - expect(gd.layout.height).toBe(gd._fullLayout.height); - } - - function testAutosize(autosize, config, layoutHeight, relayoutHeight, done) { - var layout = { - autosize: autosize, - width: layoutWidth - - }, - relayout = { - width: relayoutWidth - }; - - var container = document.getElementById('graph'); - container.style.width = containerWidthBeforePlot + 'px'; - container.style.height = containerHeightBeforePlot + 'px'; - - Plotly.plot(gd, data, layout, config).then(function() { - checkLayoutSize(layoutWidth, layoutHeight); - if(!autosize) compareLayoutAndFullLayout(gd); - - container.style.width = containerWidthBeforeRelayout + 'px'; - container.style.height = containerHeightBeforeRelayout + 'px'; - - Plotly.relayout(gd, relayout).then(function() { - checkLayoutSize(relayoutWidth, relayoutHeight); - if(!autosize) compareLayoutAndFullLayout(gd); - done(); - }); - }); - } - - it('should fill the frame when autosize: false, fillFrame: true, frameMargins: undefined', function(done) { - var autosize = false, - config = { - autosizable: true, - fillFrame: true - }, - layoutHeight = window.innerHeight, - relayoutHeight = layoutHeight; - testAutosize(autosize, config, layoutHeight, relayoutHeight, done); - }); - - it('should fill the frame when autosize: true, fillFrame: true and frameMargins: undefined', function(done) { - var autosize = true, - config = { - fillFrame: true - }, - layoutHeight = window.innerHeight, - relayoutHeight = window.innerHeight; - testAutosize(autosize, config, layoutHeight, relayoutHeight, done); - }); - - it('should fill the container when autosize: false, fillFrame: false and frameMargins: undefined', function(done) { - var autosize = false, - config = { - autosizable: true, - fillFrame: false - }, - layoutHeight = containerHeightBeforePlot, - relayoutHeight = layoutHeight; - testAutosize(autosize, config, layoutHeight, relayoutHeight, done); - }); - - it('should fill the container when autosize: true, fillFrame: false and frameMargins: undefined', function(done) { - var autosize = true, - config = { - fillFrame: false - }, - layoutHeight = containerHeightBeforePlot, - relayoutHeight = containerHeightBeforeRelayout; - testAutosize(autosize, config, layoutHeight, relayoutHeight, done); - }); - - it('should fill the container when autosize: false, fillFrame: false and frameMargins: 0.1', function(done) { - var autosize = false, - config = { - autosizable: true, - fillFrame: false, - frameMargins: 0.1 - }, - layoutHeight = 360, - relayoutHeight = layoutHeight; - testAutosize(autosize, config, layoutHeight, relayoutHeight, done); - }); - - it('should fill the container when autosize: true, fillFrame: false and frameMargins: 0.1', function(done) { - var autosize = true, - config = { - fillFrame: false, - frameMargins: 0.1 - }, - layoutHeight = 360, - relayoutHeight = 288; - testAutosize(autosize, config, layoutHeight, relayoutHeight, done); - }); - - it('should respect attribute autosizable: false', function(done) { - var autosize = false, - config = { - autosizable: false, - fillFrame: true - }, - layoutHeight = Plots.layoutAttributes.height.dflt, - relayoutHeight = layoutHeight; - testAutosize(autosize, config, layoutHeight, relayoutHeight, done); - }); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); + +describe("config argument", function() { + describe("attribute layout.autosize", function() { + var layoutWidth = 1111, + relayoutWidth = 555, + containerWidthBeforePlot = 888, + containerWidthBeforeRelayout = 666, + containerHeightBeforePlot = 543, + containerHeightBeforeRelayout = 321, + data = [], + gd; + + beforeEach(function() { + gd = createGraphDiv(); }); - describe('showLink attribute', function() { - - var gd; - - beforeEach(function(done) { - gd = createGraphDiv(); - done(); + afterEach(destroyGraphDiv); + + function checkLayoutSize(width, height) { + expect(gd._fullLayout.width).toBe(width); + expect(gd._fullLayout.height).toBe(height); + + var svg = document.getElementsByClassName("main-svg")[0]; + expect(+svg.getAttribute("width")).toBe(width); + expect(+svg.getAttribute("height")).toBe(height); + } + + function compareLayoutAndFullLayout(gd) { + expect(gd.layout.width).toBe(gd._fullLayout.width); + expect(gd.layout.height).toBe(gd._fullLayout.height); + } + + function testAutosize( + autosize, + config, + layoutHeight, + relayoutHeight, + done + ) { + var layout = { autosize: autosize, width: layoutWidth }, + relayout = { width: relayoutWidth }; + + var container = document.getElementById("graph"); + container.style.width = containerWidthBeforePlot + "px"; + container.style.height = containerHeightBeforePlot + "px"; + + Plotly.plot(gd, data, layout, config).then(function() { + checkLayoutSize(layoutWidth, layoutHeight); + if (!autosize) compareLayoutAndFullLayout(gd); + + container.style.width = containerWidthBeforeRelayout + "px"; + container.style.height = containerHeightBeforeRelayout + "px"; + + Plotly.relayout(gd, relayout).then(function() { + checkLayoutSize(relayoutWidth, relayoutHeight); + if (!autosize) compareLayoutAndFullLayout(gd); + done(); }); + }); + } + + it( + "should fill the frame when autosize: false, fillFrame: true, frameMargins: undefined", + function(done) { + var autosize = false, + config = { autosizable: true, fillFrame: true }, + layoutHeight = window.innerHeight, + relayoutHeight = layoutHeight; + testAutosize(autosize, config, layoutHeight, relayoutHeight, done); + } + ); + + it( + "should fill the frame when autosize: true, fillFrame: true and frameMargins: undefined", + function(done) { + var autosize = true, + config = { fillFrame: true }, + layoutHeight = window.innerHeight, + relayoutHeight = window.innerHeight; + testAutosize(autosize, config, layoutHeight, relayoutHeight, done); + } + ); + + it( + "should fill the container when autosize: false, fillFrame: false and frameMargins: undefined", + function(done) { + var autosize = false, + config = { autosizable: true, fillFrame: false }, + layoutHeight = containerHeightBeforePlot, + relayoutHeight = layoutHeight; + testAutosize(autosize, config, layoutHeight, relayoutHeight, done); + } + ); + + it( + "should fill the container when autosize: true, fillFrame: false and frameMargins: undefined", + function(done) { + var autosize = true, + config = { fillFrame: false }, + layoutHeight = containerHeightBeforePlot, + relayoutHeight = containerHeightBeforeRelayout; + testAutosize(autosize, config, layoutHeight, relayoutHeight, done); + } + ); + + it( + "should fill the container when autosize: false, fillFrame: false and frameMargins: 0.1", + function(done) { + var autosize = false, + config = { autosizable: true, fillFrame: false, frameMargins: 0.1 }, + layoutHeight = 360, + relayoutHeight = layoutHeight; + testAutosize(autosize, config, layoutHeight, relayoutHeight, done); + } + ); + + it( + "should fill the container when autosize: true, fillFrame: false and frameMargins: 0.1", + function(done) { + var autosize = true, + config = { fillFrame: false, frameMargins: 0.1 }, + layoutHeight = 360, + relayoutHeight = 288; + testAutosize(autosize, config, layoutHeight, relayoutHeight, done); + } + ); + + it("should respect attribute autosizable: false", function(done) { + var autosize = false, + config = { autosizable: false, fillFrame: true }, + layoutHeight = Plots.layoutAttributes.height.dflt, + relayoutHeight = layoutHeight; + testAutosize(autosize, config, layoutHeight, relayoutHeight, done); + }); + }); - afterEach(destroyGraphDiv); - - it('should not display the edit link by default', function() { - Plotly.plot(gd, [], {}); - - var link = document.getElementsByClassName('js-plot-link-container')[0]; + describe("showLink attribute", function() { + var gd; - expect(link.textContent).toBe(''); + beforeEach(function(done) { + gd = createGraphDiv(); + done(); + }); - var bBox = link.getBoundingClientRect(); - expect(bBox.width).toBe(0); - expect(bBox.height).toBe(0); - }); + afterEach(destroyGraphDiv); - it('should display a link when true', function() { - Plotly.plot(gd, [], {}, { showLink: true }); + it("should not display the edit link by default", function() { + Plotly.plot(gd, [], {}); - var link = document.getElementsByClassName('js-plot-link-container')[0]; + var link = document.getElementsByClassName("js-plot-link-container")[0]; - expect(link.textContent).toBe('Edit chart »'); + expect(link.textContent).toBe(""); - var bBox = link.getBoundingClientRect(); - expect(bBox.width).toBeGreaterThan(0); - expect(bBox.height).toBeGreaterThan(0); - }); + var bBox = link.getBoundingClientRect(); + expect(bBox.width).toBe(0); + expect(bBox.height).toBe(0); }); + it("should display a link when true", function() { + Plotly.plot(gd, [], {}, { showLink: true }); - describe('editable attribute', function() { - - var gd; + var link = document.getElementsByClassName("js-plot-link-container")[0]; - beforeEach(function(done) { - gd = createGraphDiv(); + expect(link.textContent).toBe("Edit chart \xBB"); - Plotly.plot(gd, [ - { x: [1, 2, 3], y: [1, 2, 3] }, - { x: [1, 2, 3], y: [3, 2, 1] } - ], { - width: 600, - height: 400, - annotations: [ - { text: 'testing', x: 1, y: 1, showarrow: true } - ] - }, { editable: true }) - .then(done); - }); + var bBox = link.getBoundingClientRect(); + expect(bBox.width).toBeGreaterThan(0); + expect(bBox.height).toBeGreaterThan(0); + }); + }); + + describe("editable attribute", function() { + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + + Plotly.plot( + gd, + [{ x: [1, 2, 3], y: [1, 2, 3] }, { x: [1, 2, 3], y: [3, 2, 1] }], + { + width: 600, + height: 400, + annotations: [{ text: "testing", x: 1, y: 1, showarrow: true }] + }, + { editable: true } + ).then(done); + }); - afterEach(destroyGraphDiv); + afterEach(destroyGraphDiv); - function checkIfEditable(elClass, text) { - var label = document.getElementsByClassName(elClass)[0]; + function checkIfEditable(elClass, text) { + var label = document.getElementsByClassName(elClass)[0]; - expect(label.textContent).toBe(text); + expect(label.textContent).toBe(text); - var labelBox = label.getBoundingClientRect(), - labelX = labelBox.left + labelBox.width / 2, - labelY = labelBox.top + labelBox.height / 2; + var labelBox = label.getBoundingClientRect(), + labelX = labelBox.left + labelBox.width / 2, + labelY = labelBox.top + labelBox.height / 2; - mouseEvent('click', labelX, labelY); + mouseEvent("click", labelX, labelY); - var editBox = document.getElementsByClassName('plugin-editable editable')[0]; - expect(editBox).toBeDefined(); - expect(editBox.textContent).toBe(text); - expect(editBox.getAttribute('contenteditable')).toBe('true'); - } + var editBox = document.getElementsByClassName("plugin-editable editable")[ + 0 + ]; + expect(editBox).toBeDefined(); + expect(editBox.textContent).toBe(text); + expect(editBox.getAttribute("contenteditable")).toBe("true"); + } - function checkIfDraggable(elClass) { - var el = document.getElementsByClassName(elClass)[0]; + function checkIfDraggable(elClass) { + var el = document.getElementsByClassName(elClass)[0]; - var elBox = el.getBoundingClientRect(), - elX = elBox.left + elBox.width / 2, - elY = elBox.top + elBox.height / 2; + var elBox = el.getBoundingClientRect(), + elX = elBox.left + elBox.width / 2, + elY = elBox.top + elBox.height / 2; - mouseEvent('mousedown', elX, elY); - mouseEvent('mousemove', elX - 20, elY + 20); + mouseEvent("mousedown", elX, elY); + mouseEvent("mousemove", elX - 20, elY + 20); - var movedBox = el.getBoundingClientRect(); + var movedBox = el.getBoundingClientRect(); - expect(movedBox.left).toBe(elBox.left - 20); - expect(movedBox.top).toBe(elBox.top + 20); + expect(movedBox.left).toBe(elBox.left - 20); + expect(movedBox.top).toBe(elBox.top + 20); - mouseEvent('mouseup', elX - 20, elY + 20); - } + mouseEvent("mouseup", elX - 20, elY + 20); + } - it('should make titles editable', function() { - checkIfEditable('gtitle', 'Click to enter Plot title'); - }); - - it('should make x axes labels editable', function() { - checkIfEditable('g-xtitle', 'Click to enter X axis title'); - }); + it("should make titles editable", function() { + checkIfEditable("gtitle", "Click to enter Plot title"); + }); - it('should make y axes labels editable', function() { - checkIfEditable('g-ytitle', 'Click to enter Y axis title'); - }); + it("should make x axes labels editable", function() { + checkIfEditable("g-xtitle", "Click to enter X axis title"); + }); - it('should make legend labels editable', function() { - checkIfEditable('legendtext', 'trace 0'); - }); + it("should make y axes labels editable", function() { + checkIfEditable("g-ytitle", "Click to enter Y axis title"); + }); - it('should make annotation labels editable', function() { - checkIfEditable('annotation-text-g', 'testing'); - }); + it("should make legend labels editable", function() { + checkIfEditable("legendtext", "trace 0"); + }); - it('should make annotation labels draggable', function() { - checkIfDraggable('annotation-text-g'); - }); + it("should make annotation labels editable", function() { + checkIfEditable("annotation-text-g", "testing"); + }); - it('should make annotation arrows draggable', function() { - checkIfDraggable('annotation-arrow-g'); - }); + it("should make annotation labels draggable", function() { + checkIfDraggable("annotation-text-g"); + }); - it('should make legends draggable', function() { - checkIfDraggable('legend'); - }); + it("should make annotation arrows draggable", function() { + checkIfDraggable("annotation-arrow-g"); + }); + it("should make legends draggable", function() { + checkIfDraggable("legend"); }); + }); }); diff --git a/test/jasmine/tests/contour_test.js b/test/jasmine/tests/contour_test.js index 764a5d94218..ec0eeaa3344 100644 --- a/test/jasmine/tests/contour_test.js +++ b/test/jasmine/tests/contour_test.js @@ -1,346 +1,334 @@ -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); - -var Contour = require('@src/traces/contour'); -var makeColorMap = require('@src/traces/contour/make_color_map'); -var colorScales = require('@src/components/colorscale/scales'); - -var customMatchers = require('../assets/custom_matchers'); - - -describe('contour defaults', function() { - 'use strict'; - - var traceIn, - traceOut; - - var defaultColor = '#444', - layout = { - font: Plots.layoutAttributes.font - }; - - var supplyDefaults = Contour.supplyDefaults; - - beforeEach(function() { - traceOut = {}; - }); - - it('should set autocontour to false when contours is supplied', function() { - traceIn = { - type: 'contour', - z: [[10, 10.625, 12.5, 15.625], - [5.625, 6.25, 8.125, 11.25], - [2.5, 3.125, 5.0, 8.125], - [0.625, 1.25, 3.125, 6.25]], - contours: { - start: 4, - end: 14 - // missing size does NOT set autocontour true - // even though in calc we set an autosize. - } - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.autocontour).toBe(false); - - traceIn = { - type: 'contour', - z: [[10, 10.625, 12.5, 15.625], - [5.625, 6.25, 8.125, 11.25], - [2.5, 3.125, 5.0, 8.125], - [0.625, 1.25, 3.125, 6.25]], - contours: {start: 4} // you need at least start and end - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.autocontour).toBe(true); - }); - - it('should inherit layout.calendar', function() { - traceIn = { - x: [1, 2], - y: [1, 2], - z: [[1, 2], [3, 4]] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('islamic'); - expect(traceOut.ycalendar).toBe('islamic'); - }); - - it('should take its own calendars', function() { - traceIn = { - x: [1, 2], - y: [1, 2], - z: [[1, 2], [3, 4]], - xcalendar: 'coptic', - ycalendar: 'ethiopian' - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('coptic'); - expect(traceOut.ycalendar).toBe('ethiopian'); - }); -}); - -describe('contour makeColorMap', function() { - 'use strict'; - - it('should make correct color map function (\'fill\' coloring case)', function() { - var trace = { - contours: { - coloring: 'fill', - start: -1.5, - size: 0.5, - end: 2.005 - }, - colorscale: [[ - 0, 'rgb(12,51,131)' - ], [ - 0.25, 'rgb(10,136,186)' - ], [ - 0.5, 'rgb(242,211,56)' - ], [ - 0.75, 'rgb(242,143,56)' - ], [ - 1, 'rgb(217,30,30)' - ]] - }; - - var colorMap = makeColorMap(trace); - - expect(colorMap.domain()).toEqual( - [-1.75, -0.75, 0.25, 1.25, 2.25] - ); - - expect(colorMap.range()).toEqual([ - 'rgb(12,51,131)', 'rgb(10,136,186)', 'rgb(242,211,56)', - 'rgb(242,143,56)', 'rgb(217,30,30)' - ]); - }); - - it('should make correct color map function (\'heatmap\' coloring case)', function() { - var trace = { - contours: { - coloring: 'heatmap', - start: 1.5, - size: 0.5, - end: 5.505 - }, - colorscale: colorScales.RdBu, - zmin: 1, - zmax: 6 - }; - - var colorMap = makeColorMap(trace); - - expect(colorMap.domain()).toEqual( - [1, 2.75, 3.5, 4, 4.5, 6] - ); - - expect(colorMap.range()).toEqual([ - 'rgb(5,10,172)', 'rgb(106,137,247)', 'rgb(190,190,190)', - 'rgb(220,170,132)', 'rgb(230,145,90)', 'rgb(178,10,28)' - ]); - }); - - it('should make correct color map function (\'lines\' coloring case)', function() { - var trace = { - contours: { - coloring: 'lines', - start: 1.5, - size: 0.5, - end: 5.505 - }, - colorscale: colorScales.RdBu - }; - - var colorMap = makeColorMap(trace); - - expect(colorMap.domain()).toEqual( - [1.5, 2.9, 3.5, 3.9, 4.3, 5.5] - ); - - expect(colorMap.range()).toEqual([ - 'rgb(5,10,172)', 'rgb(106,137,247)', 'rgb(190,190,190)', - 'rgb(220,170,132)', 'rgb(230,145,90)', 'rgb(178,10,28)' - ]); - }); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); + +var Contour = require("@src/traces/contour"); +var makeColorMap = require("@src/traces/contour/make_color_map"); +var colorScales = require("@src/components/colorscale/scales"); + +var customMatchers = require("../assets/custom_matchers"); + +describe("contour defaults", function() { + "use strict"; + var traceIn, traceOut; + + var defaultColor = "#444", layout = { font: Plots.layoutAttributes.font }; + + var supplyDefaults = Contour.supplyDefaults; + + beforeEach(function() { + traceOut = {}; + }); + + it("should set autocontour to false when contours is supplied", function() { + traceIn = { + type: "contour", + z: [ + [10, 10.625, 12.5, 15.625], + [5.625, 6.25, 8.125, 11.25], + [2.5, 3.125, 5.0, 8.125], + [0.625, 1.25, 3.125, 6.25] + ], + contours: { + start: 4, + // missing size does NOT set autocontour true + // even though in calc we set an autosize. + end: 14 + } + }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.autocontour).toBe(false); + + traceIn = { + type: "contour", + z: [ + [10, 10.625, 12.5, 15.625], + [5.625, 6.25, 8.125, 11.25], + [2.5, 3.125, 5.0, 8.125], + [0.625, 1.25, 3.125, 6.25] + ], + // you need at least start and end + contours: { start: 4 } + }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.autocontour).toBe(true); + }); + + it("should inherit layout.calendar", function() { + traceIn = { x: [1, 2], y: [1, 2], z: [[1, 2], [3, 4]] }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("islamic"); + expect(traceOut.ycalendar).toBe("islamic"); + }); + + it("should take its own calendars", function() { + traceIn = { + x: [1, 2], + y: [1, 2], + z: [[1, 2], [3, 4]], + xcalendar: "coptic", + ycalendar: "ethiopian" + }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("coptic"); + expect(traceOut.ycalendar).toBe("ethiopian"); + }); }); -describe('contour calc', function() { - 'use strict'; - - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - function _calc(opts) { - var base = { type: 'contour' }, - trace = Lib.extendFlat({}, base, opts), - gd = { data: [trace] }; - - Plots.supplyDefaults(gd); - var fullTrace = gd._fullData[0]; - - var out = Contour.calc(gd, fullTrace)[0]; - out.trace = fullTrace; - return out; +describe("contour makeColorMap", function() { + "use strict"; + it( + "should make correct color map function ('fill' coloring case)", + function() { + var trace = { + contours: { coloring: "fill", start: -1.5, size: 0.5, end: 2.005 }, + colorscale: [ + [0, "rgb(12,51,131)"], + [0.25, "rgb(10,136,186)"], + [0.5, "rgb(242,211,56)"], + [0.75, "rgb(242,143,56)"], + [1, "rgb(217,30,30)"] + ] + }; + + var colorMap = makeColorMap(trace); + + expect(colorMap.domain()).toEqual([-1.75, -0.75, 0.25, 1.25, 2.25]); + + expect(colorMap.range()).toEqual([ + "rgb(12,51,131)", + "rgb(10,136,186)", + "rgb(242,211,56)", + "rgb(242,143,56)", + "rgb(217,30,30)" + ]); } + ); + + it( + "should make correct color map function ('heatmap' coloring case)", + function() { + var trace = { + contours: { coloring: "heatmap", start: 1.5, size: 0.5, end: 5.505 }, + colorscale: colorScales.RdBu, + zmin: 1, + zmax: 6 + }; + + var colorMap = makeColorMap(trace); + + expect(colorMap.domain()).toEqual([1, 2.75, 3.5, 4, 4.5, 6]); + + expect(colorMap.range()).toEqual([ + "rgb(5,10,172)", + "rgb(106,137,247)", + "rgb(190,190,190)", + "rgb(220,170,132)", + "rgb(230,145,90)", + "rgb(178,10,28)" + ]); + } + ); + + it( + "should make correct color map function ('lines' coloring case)", + function() { + var trace = { + contours: { coloring: "lines", start: 1.5, size: 0.5, end: 5.505 }, + colorscale: colorScales.RdBu + }; + + var colorMap = makeColorMap(trace); + + expect(colorMap.domain()).toEqual([1.5, 2.9, 3.5, 3.9, 4.3, 5.5]); + + expect(colorMap.range()).toEqual([ + "rgb(5,10,172)", + "rgb(106,137,247)", + "rgb(190,190,190)", + "rgb(220,170,132)", + "rgb(230,145,90)", + "rgb(178,10,28)" + ]); + } + ); +}); - it('should fill in bricks if x/y not given', function() { - var out = _calc({ - z: [[1, 2, 3], [3, 1, 2]] - }); - - expect(out.x).toBeCloseToArray([0, 1, 2]); - expect(out.y).toBeCloseToArray([0, 1]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); - }); - - it('should fill in bricks with x0/dx + y0/dy', function() { - var out = _calc({ - z: [[1, 2, 3], [3, 1, 2]], - x0: 10, - dx: 0.5, - y0: -2, - dy: -2 - }); - - expect(out.x).toBeCloseToArray([10, 10.5, 11]); - expect(out.y).toBeCloseToArray([-2, -4]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); - }); - - it('should convert x/y coordinates into bricks', function() { - var out = _calc({ - x: [1, 2, 3], - y: [2, 6], - z: [[1, 2, 3], [3, 1, 2]] - }); - - expect(out.x).toBeCloseToArray([1, 2, 3]); - expect(out.y).toBeCloseToArray([2, 6]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); - }); - - it('should trim brick-link /y coordinates', function() { - var out = _calc({ - x: [1, 2, 3, 4], - y: [2, 6, 10], - z: [[1, 2, 3], [3, 1, 2]] - }); - - expect(out.x).toBeCloseToArray([1, 2, 3]); - expect(out.y).toBeCloseToArray([2, 6]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); +describe("contour calc", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + function _calc(opts) { + var base = { type: "contour" }, + trace = Lib.extendFlat({}, base, opts), + gd = { data: [trace] }; + + Plots.supplyDefaults(gd); + var fullTrace = gd._fullData[0]; + + var out = Contour.calc(gd, fullTrace)[0]; + out.trace = fullTrace; + return out; + } + + it("should fill in bricks if x/y not given", function() { + var out = _calc({ z: [[1, 2, 3], [3, 1, 2]] }); + + expect(out.x).toBeCloseToArray([0, 1, 2]); + expect(out.y).toBeCloseToArray([0, 1]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); + + it("should fill in bricks with x0/dx + y0/dy", function() { + var out = _calc({ + z: [[1, 2, 3], [3, 1, 2]], + x0: 10, + dx: 0.5, + y0: -2, + dy: -2 }); - it('should handle 1-xy + 1-brick case', function() { - var out = _calc({ - x: [2], - y: [3], - z: [[1]] - }); + expect(out.x).toBeCloseToArray([10, 10.5, 11]); + expect(out.y).toBeCloseToArray([-2, -4]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); - expect(out.x).toBeCloseToArray([2]); - expect(out.y).toBeCloseToArray([3]); - expect(out.z).toBeCloseTo2DArray([[1]]); + it("should convert x/y coordinates into bricks", function() { + var out = _calc({ + x: [1, 2, 3], + y: [2, 6], + z: [[1, 2, 3], [3, 1, 2]] }); - it('should handle 1-xy + multi-brick case', function() { - var out = _calc({ - x: [2], - y: [3], - z: [[1, 2, 3], [3, 1, 2]] - }); + expect(out.x).toBeCloseToArray([1, 2, 3]); + expect(out.y).toBeCloseToArray([2, 6]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); - expect(out.x).toBeCloseToArray([2, 3, 4]); - expect(out.y).toBeCloseToArray([3, 4]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + it("should trim brick-link /y coordinates", function() { + var out = _calc({ + x: [1, 2, 3, 4], + y: [2, 6, 10], + z: [[1, 2, 3], [3, 1, 2]] }); - it('should handle 0-xy + multi-brick case', function() { + expect(out.x).toBeCloseToArray([1, 2, 3]); + expect(out.y).toBeCloseToArray([2, 6]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); + + it("should handle 1-xy + 1-brick case", function() { + var out = _calc({ x: [2], y: [3], z: [[1]] }); + + expect(out.x).toBeCloseToArray([2]); + expect(out.y).toBeCloseToArray([3]); + expect(out.z).toBeCloseTo2DArray([[1]]); + }); + + it("should handle 1-xy + multi-brick case", function() { + var out = _calc({ x: [2], y: [3], z: [[1, 2, 3], [3, 1, 2]] }); + + expect(out.x).toBeCloseToArray([2, 3, 4]); + expect(out.y).toBeCloseToArray([3, 4]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); + + it("should handle 0-xy + multi-brick case", function() { + var out = _calc({ x: [], y: [], z: [[1, 2, 3], [3, 1, 2]] }); + + expect(out.x).toBeCloseToArray([0, 1, 2]); + expect(out.y).toBeCloseToArray([0, 1]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); + + it("should make nice autocontour values", function() { + var incompleteContours = [ + undefined, + { start: 12 }, + { end: 45 }, + // size gets ignored + { start: 2, size: 2 } + ]; + + var contoursFinal = [ + // fully auto. These are *not* exactly the output contours objects, + // I put the input ncontours in here too. + { inputNcontours: undefined, start: 0.5, end: 4.5, size: 0.5 }, + // explicit ncontours + { inputNcontours: 6, start: 1, end: 4, size: 1 }, + // edge case where low ncontours makes start and end cross + { inputNcontours: 2, start: 2.5, end: 2.5, size: 5 } + ]; + + incompleteContours.forEach(function(contoursIn) { + contoursFinal.forEach(function(spec) { var out = _calc({ - x: [], - y: [], - z: [[1, 2, 3], [3, 1, 2]] + z: [[0, 2], [3, 5]], + contours: Lib.extendFlat({}, contoursIn), + ncontours: spec.inputNcontours + }).trace; + + ["start", "end", "size"].forEach(function(attr) { + expect(out.contours[attr]).toBe(spec[attr], [ + contoursIn, + spec.inputNcontours, + attr + ]); + // all these get copied back to the input trace + expect(out._input.contours[attr]).toBe(spec[attr], [ + contoursIn, + spec.inputNcontours, + attr + ]); }); - expect(out.x).toBeCloseToArray([0, 1, 2]); - expect(out.y).toBeCloseToArray([0, 1]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + expect(out._input.autocontour).toBe(true); + expect(out._input.zauto).toBe(true); + expect(out._input.zmin).toBe(0); + expect(out._input.zmax).toBe(5); + }); }); - - it('should make nice autocontour values', function() { - var incompleteContours = [ - undefined, - {start: 12}, - {end: 45}, - {start: 2, size: 2} // size gets ignored - ]; - - var contoursFinal = [ - // fully auto. These are *not* exactly the output contours objects, - // I put the input ncontours in here too. - {inputNcontours: undefined, start: 0.5, end: 4.5, size: 0.5}, - // explicit ncontours - {inputNcontours: 6, start: 1, end: 4, size: 1}, - // edge case where low ncontours makes start and end cross - {inputNcontours: 2, start: 2.5, end: 2.5, size: 5} - ]; - - incompleteContours.forEach(function(contoursIn) { - contoursFinal.forEach(function(spec) { - var out = _calc({ - z: [[0, 2], [3, 5]], - contours: Lib.extendFlat({}, contoursIn), - ncontours: spec.inputNcontours - }).trace; - - ['start', 'end', 'size'].forEach(function(attr) { - expect(out.contours[attr]).toBe(spec[attr], [contoursIn, spec.inputNcontours, attr]); - // all these get copied back to the input trace - expect(out._input.contours[attr]).toBe(spec[attr], [contoursIn, spec.inputNcontours, attr]); - }); - - expect(out._input.autocontour).toBe(true); - expect(out._input.zauto).toBe(true); - expect(out._input.zmin).toBe(0); - expect(out._input.zmax).toBe(5); - }); - }); - }); - - it('should supply size and reorder start/end if autocontour is off', function() { - var specs = [ - {start: 1, end: 100, ncontours: undefined, size: 10}, - {start: 1, end: 100, ncontours: 5, size: 20}, - {start: 10, end: 10, ncontours: 10, size: 1} - ]; - - specs.forEach(function(spec) { - [ - [spec.start, spec.end, 'normal'], - [spec.end, spec.start, 'reversed'] - ].forEach(function(v) { - var startIn = v[0], - endIn = v[1], - order = v[2]; - - var out = _calc({ - z: [[1, 2], [3, 4]], - contours: {start: startIn, end: endIn}, - ncontours: spec.ncontours - }).trace; - - ['start', 'end', 'size'].forEach(function(attr) { - expect(out.contours[attr]).toBe(spec[attr], [spec, order, attr]); - expect(out._input.contours[attr]).toBe(spec[attr], [spec, order, attr]); - }); - }); + }); + + it( + "should supply size and reorder start/end if autocontour is off", + function() { + var specs = [ + { start: 1, end: 100, ncontours: undefined, size: 10 }, + { start: 1, end: 100, ncontours: 5, size: 20 }, + { start: 10, end: 10, ncontours: 10, size: 1 } + ]; + + specs.forEach(function(spec) { + [ + [spec.start, spec.end, "normal"], + [spec.end, spec.start, "reversed"] + ].forEach(function(v) { + var startIn = v[0], endIn = v[1], order = v[2]; + + var out = _calc({ + z: [[1, 2], [3, 4]], + contours: { start: startIn, end: endIn }, + ncontours: spec.ncontours + }).trace; + + ["start", "end", "size"].forEach(function(attr) { + expect(out.contours[attr]).toBe(spec[attr], [spec, order, attr]); + expect(out._input.contours[attr]).toBe(spec[attr], [ + spec, + order, + attr + ]); + }); }); - }); + }); + } + ); }); diff --git a/test/jasmine/tests/download_test.js b/test/jasmine/tests/download_test.js index bcb17a6c354..0f041b20a76 100644 --- a/test/jasmine/tests/download_test.js +++ b/test/jasmine/tests/download_test.js @@ -1,121 +1,144 @@ -var Plotly = require('@lib/index'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var textchartMock = require('@mocks/text_chart_arrays.json'); +var Plotly = require("@lib/index"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var textchartMock = require("@mocks/text_chart_arrays.json"); var LONG_TIMEOUT_INTERVAL = 2 * jasmine.DEFAULT_TIMEOUT_INTERVAL; -describe('Plotly.downloadImage', function() { - 'use strict'; - var gd; - - // override click handler on createElement - // so these tests will not actually - // download an image each time they are run - // full credit goes to @etpinard; thanks - var createElement = document.createElement; - beforeAll(function() { - document.createElement = function(args) { - var el = createElement.call(document, args); - el.click = function() {}; - return el; - }; - }); - - afterAll(function() { - document.createElement = createElement; - }); - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(function() { - destroyGraphDiv(); - }); - - it('should be attached to Plotly', function() { - expect(Plotly.downloadImage).toBeDefined(); - }); - - it('should create link, remove link, accept options', function(done) { - downloadTest(gd, 'jpeg', done); - }, LONG_TIMEOUT_INTERVAL); - - it('should create link, remove link, accept options', function(done) { - downloadTest(gd, 'png', done); - }, LONG_TIMEOUT_INTERVAL); - - it('should create link, remove link, accept options', function(done) { - checkWebp(function(supported) { - if(supported) { - downloadTest(gd, 'webp', done); - } else { - done(); - } - }); - }, LONG_TIMEOUT_INTERVAL); - - it('should create link, remove link, accept options', function(done) { - downloadTest(gd, 'svg', done); - }, LONG_TIMEOUT_INTERVAL); +describe("Plotly.downloadImage", function() { + "use strict"; + var gd; + + // override click handler on createElement + // so these tests will not actually + // download an image each time they are run + // full credit goes to @etpinard; thanks + var createElement = document.createElement; + beforeAll(function() { + document.createElement = function(args) { + var el = createElement.call(document, args); + el.click = function() {}; + return el; + }; + }); + + afterAll(function() { + document.createElement = createElement; + }); + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + destroyGraphDiv(); + }); + + it("should be attached to Plotly", function() { + expect(Plotly.downloadImage).toBeDefined(); + }); + + it( + "should create link, remove link, accept options", + function(done) { + downloadTest(gd, "jpeg", done); + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should create link, remove link, accept options", + function(done) { + downloadTest(gd, "png", done); + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should create link, remove link, accept options", + function(done) { + checkWebp(function(supported) { + if (supported) { + downloadTest(gd, "webp", done); + } else { + done(); + } + }); + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should create link, remove link, accept options", + function(done) { + downloadTest(gd, "svg", done); + }, + LONG_TIMEOUT_INTERVAL + ); }); - function downloadTest(gd, format, done) { - // use MutationObserver to monitor the DOM - // for changes - // code modeled after - // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver - // select the target node - var target = document.body; - var domchanges = []; - - // create an observer instance - var observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - domchanges.push(mutation); - }); + // use MutationObserver to monitor the DOM + // for changes + // code modeled after + // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver + // select the target node + var target = document.body; + var domchanges = []; + + // create an observer instance + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + domchanges.push(mutation); }); - - Plotly.plot(gd, textchartMock.data, textchartMock.layout).then(function(gd) { - // start observing dom - // configuration of the observer: - var config = { childList: true }; - - // pass in the target node and observer options - observer.observe(target, config); - - return Plotly.downloadImage(gd, {format: format, height: 300, width: 300, filename: 'plotly_download'}); - }).then(function(filename) { - // stop observing - observer.disconnect(); - // look for an added and removed link - var linkadded = domchanges[domchanges.length - 2].addedNodes[0]; - var linkdeleted = domchanges[domchanges.length - 1].removedNodes[0]; - - // check for a 2/3, y < 1/3', function() { - var cursor = getCursor(0.9, 0.1); - expect(cursor).toEqual('se-resize'); + mouseEvent("mousedown", this.x, this.y); + mouseEvent("mousemove", this.x + 10, this.y + 10); + mouseEvent("mouseup", this.x, this.y); - cursor = getCursor(0, 0, 'right'); - expect(cursor).toEqual('se-resize', 'with right xanchor'); - - cursor = getCursor(0.63, 1, null, 'bottom'); - expect(cursor).toEqual('s-resize', 'with bottom yanchor'); - }); + expect(mockObj.dummy).not.toHaveBeenCalled(); + }); +}); - it('should return w-resize when x < 1/3, 1/3 < y < 2/3', function() { - var cursor = getCursor(0.1, 0.4); - expect(cursor).toEqual('w-resize'); +describe("dragElement.getCursor", function() { + "use strict"; + var getCursor = dragElement.getCursor; - cursor = getCursor(0.9, 0.5, 'left'); - expect(cursor).toEqual('w-resize', 'with left xanchor'); + it("should return sw-resize when x < 1/3, y < 1/3", function() { + var cursor = getCursor(0.2, 0); + expect(cursor).toEqual("sw-resize"); - cursor = getCursor(0.1, 0.1, null, 'middle'); - expect(cursor).toEqual('w-resize', 'with middle yanchor'); - }); + cursor = getCursor(1, 0, "left"); + expect(cursor).toEqual("sw-resize", "with left xanchor"); - it('should return move when 1/3 < x < 2/3, 1/3 < y < 2/3', function() { - var cursor = getCursor(0.4, 0.4); - expect(cursor).toEqual('move'); + cursor = getCursor(0.3, 1, null, "bottom"); + expect(cursor).toEqual("sw-resize", "with bottom yanchor"); + }); - cursor = getCursor(0.9, 0.5, 'center'); - expect(cursor).toEqual('move', 'with center xanchor'); + it("should return s-resize when 1/3 < x < 2/3, y < 1/3", function() { + var cursor = getCursor(0.4, 0.3); + expect(cursor).toEqual("s-resize"); - cursor = getCursor(0.4, 0.1, null, 'middle'); - expect(cursor).toEqual('move', 'with middle yanchor'); - }); + cursor = getCursor(0, 0, "center"); + expect(cursor).toEqual("s-resize", "with center xanchor"); - it('should return e-resize when x > 1/3, 1/3 < y < 2/3', function() { - var cursor = getCursor(0.8, 0.4); - expect(cursor).toEqual('e-resize'); + cursor = getCursor(0.63, 1, null, "bottom"); + expect(cursor).toEqual("s-resize", "with bottom yanchor"); + }); - cursor = getCursor(0.09, 0.5, 'right'); - expect(cursor).toEqual('e-resize', 'with right xanchor'); + it("should return se-resize when x > 2/3, y < 1/3", function() { + var cursor = getCursor(0.9, 0.1); + expect(cursor).toEqual("se-resize"); - cursor = getCursor(0.9, 0.1, null, 'middle'); - expect(cursor).toEqual('e-resize', 'with middle yanchor'); - }); + cursor = getCursor(0, 0, "right"); + expect(cursor).toEqual("se-resize", "with right xanchor"); - it('should return nw-resize when x > 1/3, y > 2/3', function() { - var cursor = getCursor(0.2, 0.7); - expect(cursor).toEqual('nw-resize'); + cursor = getCursor(0.63, 1, null, "bottom"); + expect(cursor).toEqual("s-resize", "with bottom yanchor"); + }); - cursor = getCursor(0.9, 0.9, 'left'); - expect(cursor).toEqual('nw-resize', 'with left xanchor'); + it("should return w-resize when x < 1/3, 1/3 < y < 2/3", function() { + var cursor = getCursor(0.1, 0.4); + expect(cursor).toEqual("w-resize"); - cursor = getCursor(0.1, 0.1, null, 'top'); - expect(cursor).toEqual('nw-resize', 'with top yanchor'); - }); + cursor = getCursor(0.9, 0.5, "left"); + expect(cursor).toEqual("w-resize", "with left xanchor"); - it('should return nw-resize when 1/3 < x < 2/3, y > 2/3', function() { - var cursor = getCursor(0.4, 0.7); - expect(cursor).toEqual('n-resize'); + cursor = getCursor(0.1, 0.1, null, "middle"); + expect(cursor).toEqual("w-resize", "with middle yanchor"); + }); - cursor = getCursor(0.9, 0.9, 'center'); - expect(cursor).toEqual('n-resize', 'with center xanchor'); + it("should return move when 1/3 < x < 2/3, 1/3 < y < 2/3", function() { + var cursor = getCursor(0.4, 0.4); + expect(cursor).toEqual("move"); - cursor = getCursor(0.5, 0.1, null, 'top'); - expect(cursor).toEqual('n-resize', 'with top yanchor'); - }); + cursor = getCursor(0.9, 0.5, "center"); + expect(cursor).toEqual("move", "with center xanchor"); - it('should return nw-resize when x > 2/3, y > 2/3', function() { - var cursor = getCursor(0.7, 0.7); - expect(cursor).toEqual('ne-resize'); + cursor = getCursor(0.4, 0.1, null, "middle"); + expect(cursor).toEqual("move", "with middle yanchor"); + }); - cursor = getCursor(0.09, 0.9, 'right'); - expect(cursor).toEqual('ne-resize', 'with right xanchor'); + it("should return e-resize when x > 1/3, 1/3 < y < 2/3", function() { + var cursor = getCursor(0.8, 0.4); + expect(cursor).toEqual("e-resize"); - cursor = getCursor(0.8, 0.1, null, 'top'); - expect(cursor).toEqual('ne-resize', 'with top yanchor'); - }); -}); + cursor = getCursor(0.09, 0.5, "right"); + expect(cursor).toEqual("e-resize", "with right xanchor"); -describe('dragElement.align', function() { - 'use strict'; + cursor = getCursor(0.9, 0.1, null, "middle"); + expect(cursor).toEqual("e-resize", "with middle yanchor"); + }); - var align = dragElement.align; + it("should return nw-resize when x > 1/3, y > 2/3", function() { + var cursor = getCursor(0.2, 0.7); + expect(cursor).toEqual("nw-resize"); - it('should return min value if anchor is set to \'bottom\' or \'left\'', function() { - var al = align(0, 1, 0, 1, 'bottom'); - expect(al).toEqual(0); + cursor = getCursor(0.9, 0.9, "left"); + expect(cursor).toEqual("nw-resize", "with left xanchor"); - al = align(0, 1, 0, 1, 'left'); - expect(al).toEqual(0); - }); + cursor = getCursor(0.1, 0.1, null, "top"); + expect(cursor).toEqual("nw-resize", "with top yanchor"); + }); - it('should return max value if anchor is set to \'top\' or \'right\'', function() { - var al = align(0, 1, 0, 1, 'top'); - expect(al).toEqual(1); + it("should return nw-resize when 1/3 < x < 2/3, y > 2/3", function() { + var cursor = getCursor(0.4, 0.7); + expect(cursor).toEqual("n-resize"); - al = align(0, 1, 0, 1, 'right'); - expect(al).toEqual(1); - }); + cursor = getCursor(0.9, 0.9, "center"); + expect(cursor).toEqual("n-resize", "with center xanchor"); - it('should return center value if anchor is set to \'middle\' or \'center\'', function() { - var al = align(0, 1, 0, 1, 'middle'); - expect(al).toEqual(0.5); + cursor = getCursor(0.5, 0.1, null, "top"); + expect(cursor).toEqual("n-resize", "with top yanchor"); + }); - al = align(0, 1, 0, 1, 'center'); - expect(al).toEqual(0.5); - }); + it("should return nw-resize when x > 2/3, y > 2/3", function() { + var cursor = getCursor(0.7, 0.7); + expect(cursor).toEqual("ne-resize"); - it('should return center value if anchor is set to \'middle\' or \'center\'', function() { - var al = align(0, 1, 0, 1, 'middle'); - expect(al).toEqual(0.5); + cursor = getCursor(0.09, 0.9, "right"); + expect(cursor).toEqual("ne-resize", "with right xanchor"); - al = align(0, 1, 0, 1, 'center'); - expect(al).toEqual(0.5); - }); - - it('should return min value ', function() { - var al = align(0, 1, 0, 1); - expect(al).toEqual(0); - }); + cursor = getCursor(0.8, 0.1, null, "top"); + expect(cursor).toEqual("ne-resize", "with top yanchor"); + }); +}); - it('should return max value ', function() { - var al = align(1, 1, 0, 1); - expect(al).toEqual(2); - }); +describe("dragElement.align", function() { + "use strict"; + var align = dragElement.align; + + it( + "should return min value if anchor is set to 'bottom' or 'left'", + function() { + var al = align(0, 1, 0, 1, "bottom"); + expect(al).toEqual(0); + + al = align(0, 1, 0, 1, "left"); + expect(al).toEqual(0); + } + ); + + it( + "should return max value if anchor is set to 'top' or 'right'", + function() { + var al = align(0, 1, 0, 1, "top"); + expect(al).toEqual(1); + + al = align(0, 1, 0, 1, "right"); + expect(al).toEqual(1); + } + ); + + it( + "should return center value if anchor is set to 'middle' or 'center'", + function() { + var al = align(0, 1, 0, 1, "middle"); + expect(al).toEqual(0.5); + + al = align(0, 1, 0, 1, "center"); + expect(al).toEqual(0.5); + } + ); + + it( + "should return center value if anchor is set to 'middle' or 'center'", + function() { + var al = align(0, 1, 0, 1, "middle"); + expect(al).toEqual(0.5); + + al = align(0, 1, 0, 1, "center"); + expect(al).toEqual(0.5); + } + ); + + it("should return min value ", function() { + var al = align(0, 1, 0, 1); + expect(al).toEqual(0); + }); + + it("should return max value ", function() { + var al = align(1, 1, 0, 1); + expect(al).toEqual(2); + }); }); diff --git a/test/jasmine/tests/drawing_test.js b/test/jasmine/tests/drawing_test.js index 3c20e719a1a..7b2ea502fb1 100644 --- a/test/jasmine/tests/drawing_test.js +++ b/test/jasmine/tests/drawing_test.js @@ -1,311 +1,311 @@ -var Drawing = require('@src/components/drawing'); +var Drawing = require("@src/components/drawing"); -var d3 = require('d3'); +var d3 = require("d3"); -describe('Drawing', function() { - 'use strict'; - - describe('setClipUrl', function() { - - beforeEach(function() { - this.svg = d3.select('body').append('svg'); - this.g = this.svg.append('g'); - }); - - afterEach(function() { - this.svg.remove(); - this.g.remove(); - }); - - it('should set the clip-path attribute', function() { - expect(this.g.attr('clip-path')).toBe(null); +describe("Drawing", function() { + "use strict"; + describe("setClipUrl", function() { + beforeEach(function() { + this.svg = d3.select("body").append("svg"); + this.g = this.svg.append("g"); + }); - Drawing.setClipUrl(this.g, 'id1'); + afterEach(function() { + this.svg.remove(); + this.g.remove(); + }); - expect(this.g.attr('clip-path')).toEqual('url(#id1)'); - }); + it("should set the clip-path attribute", function() { + expect(this.g.attr("clip-path")).toBe(null); - it('should unset the clip-path if arg is falsy', function() { - this.g.attr('clip-path', 'url(#id2)'); + Drawing.setClipUrl(this.g, "id1"); - Drawing.setClipUrl(this.g, false); + expect(this.g.attr("clip-path")).toEqual("url(#id1)"); + }); - expect(this.g.attr('clip-path')).toBe(null); - }); + it("should unset the clip-path if arg is falsy", function() { + this.g.attr("clip-path", "url(#id2)"); - it('should append window URL to clip-path if is present', function() { + Drawing.setClipUrl(this.g, false); - // append with href - var base = d3.select('body') - .append('base') - .attr('href', 'https://plot.ly'); + expect(this.g.attr("clip-path")).toBe(null); + }); - // grab window URL - var href = window.location.href; + it( + "should append window URL to clip-path if is present", + function() { + // append with href + var base = d3 + .select("body") + .append("base") + .attr("href", "https://plot.ly"); - Drawing.setClipUrl(this.g, 'id3'); + // grab window URL + var href = window.location.href; - expect(this.g.attr('clip-path')) - .toEqual('url(' + href + '#id3)'); + Drawing.setClipUrl(this.g, "id3"); - base.remove(); - }); + expect(this.g.attr("clip-path")).toEqual("url(" + href + "#id3)"); - it('should append window URL w/o hash to clip-path if is present', function() { - var base = d3.select('body') - .append('base') - .attr('href', 'https://plot.ly/#hash'); + base.remove(); + } + ); - window.location.hash = 'hash'; + it( + "should append window URL w/o hash to clip-path if is present", + function() { + var base = d3 + .select("body") + .append("base") + .attr("href", "https://plot.ly/#hash"); - Drawing.setClipUrl(this.g, 'id4'); + window.location.hash = "hash"; - var expected = 'url(' + window.location.href.split('#')[0] + '#id4)'; + Drawing.setClipUrl(this.g, "id4"); - expect(this.g.attr('clip-path')).toEqual(expected); + var expected = "url(" + window.location.href.split("#")[0] + "#id4)"; - base.remove(); - window.location.hash = ''; - }); - }); + expect(this.g.attr("clip-path")).toEqual(expected); - describe('getTranslate', function() { + base.remove(); + window.location.hash = ""; + } + ); + }); - it('should work with regular DOM elements', function() { - var el = document.createElement('div'); + describe("getTranslate", function() { + it("should work with regular DOM elements", function() { + var el = document.createElement("div"); - expect(Drawing.getTranslate(el)).toEqual({ x: 0, y: 0 }); + expect(Drawing.getTranslate(el)).toEqual({ x: 0, y: 0 }); - el.setAttribute('transform', 'translate(123.45px, 67)'); - expect(Drawing.getTranslate(el)).toEqual({ x: 123.45, y: 67 }); + el.setAttribute("transform", "translate(123.45px, 67)"); + expect(Drawing.getTranslate(el)).toEqual({ x: 123.45, y: 67 }); - el.setAttribute('transform', 'translate(123.45)'); - expect(Drawing.getTranslate(el)).toEqual({ x: 123.45, y: 0 }); + el.setAttribute("transform", "translate(123.45)"); + expect(Drawing.getTranslate(el)).toEqual({ x: 123.45, y: 0 }); - el.setAttribute('transform', 'translate(1 2)'); - expect(Drawing.getTranslate(el)).toEqual({ x: 1, y: 2 }); + el.setAttribute("transform", "translate(1 2)"); + expect(Drawing.getTranslate(el)).toEqual({ x: 1, y: 2 }); - el.setAttribute('transform', 'translate(1 2); rotate(20deg)'); - expect(Drawing.getTranslate(el)).toEqual({ x: 1, y: 2 }); + el.setAttribute("transform", "translate(1 2); rotate(20deg)"); + expect(Drawing.getTranslate(el)).toEqual({ x: 1, y: 2 }); - el.setAttribute('transform', 'rotate(20deg) translate(1 2);'); - expect(Drawing.getTranslate(el)).toEqual({ x: 1, y: 2 }); + el.setAttribute("transform", "rotate(20deg) translate(1 2);"); + expect(Drawing.getTranslate(el)).toEqual({ x: 1, y: 2 }); - el.setAttribute('transform', 'rotate(20deg)'); - expect(Drawing.getTranslate(el)).toEqual({ x: 0, y: 0 }); - }); + el.setAttribute("transform", "rotate(20deg)"); + expect(Drawing.getTranslate(el)).toEqual({ x: 0, y: 0 }); + }); - it('should work with d3 elements', function() { - var el = d3.select(document.createElement('div')); + it("should work with d3 elements", function() { + var el = d3.select(document.createElement("div")); - el.attr('transform', 'translate(123.45px, 67)'); - expect(Drawing.getTranslate(el)).toEqual({ x: 123.45, y: 67 }); + el.attr("transform", "translate(123.45px, 67)"); + expect(Drawing.getTranslate(el)).toEqual({ x: 123.45, y: 67 }); - el.attr('transform', 'translate(123.45)'); - expect(Drawing.getTranslate(el)).toEqual({ x: 123.45, y: 0 }); + el.attr("transform", "translate(123.45)"); + expect(Drawing.getTranslate(el)).toEqual({ x: 123.45, y: 0 }); - el.attr('transform', 'translate(1 2)'); - expect(Drawing.getTranslate(el)).toEqual({ x: 1, y: 2 }); + el.attr("transform", "translate(1 2)"); + expect(Drawing.getTranslate(el)).toEqual({ x: 1, y: 2 }); - el.attr('transform', 'translate(1 2); rotate(20)'); - expect(Drawing.getTranslate(el)).toEqual({ x: 1, y: 2 }); + el.attr("transform", "translate(1 2); rotate(20)"); + expect(Drawing.getTranslate(el)).toEqual({ x: 1, y: 2 }); - el.attr('transform', 'rotate(20)'); - expect(Drawing.getTranslate(el)).toEqual({ x: 0, y: 0 }); - }); + el.attr("transform", "rotate(20)"); + expect(Drawing.getTranslate(el)).toEqual({ x: 0, y: 0 }); + }); - it('should work with negative values', function() { - var el = document.createElement('div'), - el3 = d3.select(document.createElement('div')); + it("should work with negative values", function() { + var el = document.createElement("div"), + el3 = d3.select(document.createElement("div")); + + expect(Drawing.getTranslate(el)).toEqual({ x: 0, y: 0 }); + + var testCases = [ + { transform: "translate(-123.45px, -67)", x: -123.45, y: -67 }, + { transform: "translate(-123.45px, 67)", x: -123.45, y: 67 }, + { transform: "translate(123.45px, -67)", x: 123.45, y: -67 }, + { transform: "translate(-123.45)", x: -123.45, y: 0 }, + { transform: "translate(-1 -2)", x: -1, y: -2 }, + { transform: "translate(-1 2)", x: -1, y: 2 }, + { transform: "translate(1 -2)", x: 1, y: -2 }, + { transform: "translate(-1 -2); rotate(20deg)", x: -1, y: -2 }, + { transform: "translate(-1 2); rotate(20deg)", x: -1, y: 2 }, + { transform: "translate(1 -2); rotate(20deg)", x: 1, y: -2 }, + { transform: "rotate(20deg) translate(-1 -2);", x: -1, y: -2 }, + { transform: "rotate(20deg) translate(-1 2);", x: -1, y: 2 }, + { transform: "rotate(20deg) translate(1 -2);", x: 1, y: -2 } + ]; + + for (var i = 0; i < testCases.length; i++) { + var testCase = testCases[i], + transform = testCase.transform, + x = testCase.x, + y = testCase.y; + + el.setAttribute("transform", transform); + expect(Drawing.getTranslate(el)).toEqual({ x: x, y: y }); + + el3.attr("transform", transform); + expect(Drawing.getTranslate(el)).toEqual({ x: x, y: y }); + } + }); + }); - expect(Drawing.getTranslate(el)).toEqual({ x: 0, y: 0 }); + describe("setTranslate", function() { + it("should work with regular DOM elements", function() { + var el = document.createElement("div"); - var testCases = [ - { transform: 'translate(-123.45px, -67)', x: -123.45, y: -67 }, - { transform: 'translate(-123.45px, 67)', x: -123.45, y: 67 }, - { transform: 'translate(123.45px, -67)', x: 123.45, y: -67 }, - { transform: 'translate(-123.45)', x: -123.45, y: 0 }, - { transform: 'translate(-1 -2)', x: -1, y: -2 }, - { transform: 'translate(-1 2)', x: -1, y: 2 }, - { transform: 'translate(1 -2)', x: 1, y: -2 }, - { transform: 'translate(-1 -2); rotate(20deg)', x: -1, y: -2 }, - { transform: 'translate(-1 2); rotate(20deg)', x: -1, y: 2 }, - { transform: 'translate(1 -2); rotate(20deg)', x: 1, y: -2 }, - { transform: 'rotate(20deg) translate(-1 -2);', x: -1, y: -2 }, - { transform: 'rotate(20deg) translate(-1 2);', x: -1, y: 2 }, - { transform: 'rotate(20deg) translate(1 -2);', x: 1, y: -2 } - ]; + Drawing.setTranslate(el, 5); + expect(el.getAttribute("transform")).toBe("translate(5, 0)"); - for(var i = 0; i < testCases.length; i++) { - var testCase = testCases[i], - transform = testCase.transform, - x = testCase.x, - y = testCase.y; + Drawing.setTranslate(el, 10, 20); + expect(el.getAttribute("transform")).toBe("translate(10, 20)"); - el.setAttribute('transform', transform); - expect(Drawing.getTranslate(el)).toEqual({ x: x, y: y }); + Drawing.setTranslate(el); + expect(el.getAttribute("transform")).toBe("translate(0, 0)"); - el3.attr('transform', transform); - expect(Drawing.getTranslate(el)).toEqual({ x: x, y: y }); - } - }); + el.setAttribute("transform", "translate(0, 0); rotate(30)"); + Drawing.setTranslate(el, 30, 40); + expect(el.getAttribute("transform")).toBe("rotate(30) translate(30, 40)"); }); - describe('setTranslate', function() { + it("should work with d3 elements", function() { + var el = d3.select(document.createElement("div")); - it('should work with regular DOM elements', function() { - var el = document.createElement('div'); + Drawing.setTranslate(el, 5); + expect(el.attr("transform")).toBe("translate(5, 0)"); - Drawing.setTranslate(el, 5); - expect(el.getAttribute('transform')).toBe('translate(5, 0)'); + Drawing.setTranslate(el, 30, 40); + expect(el.attr("transform")).toBe("translate(30, 40)"); - Drawing.setTranslate(el, 10, 20); - expect(el.getAttribute('transform')).toBe('translate(10, 20)'); + Drawing.setTranslate(el); + expect(el.attr("transform")).toBe("translate(0, 0)"); - Drawing.setTranslate(el); - expect(el.getAttribute('transform')).toBe('translate(0, 0)'); - - el.setAttribute('transform', 'translate(0, 0); rotate(30)'); - Drawing.setTranslate(el, 30, 40); - expect(el.getAttribute('transform')).toBe('rotate(30) translate(30, 40)'); - }); - - it('should work with d3 elements', function() { - var el = d3.select(document.createElement('div')); + el.attr("transform", "translate(0, 0); rotate(30)"); + Drawing.setTranslate(el, 30, 40); + expect(el.attr("transform")).toBe("rotate(30) translate(30, 40)"); + }); + }); - Drawing.setTranslate(el, 5); - expect(el.attr('transform')).toBe('translate(5, 0)'); + describe("getScale", function() { + it("should work with regular DOM elements", function() { + var el = document.createElement("div"); - Drawing.setTranslate(el, 30, 40); - expect(el.attr('transform')).toBe('translate(30, 40)'); + expect(Drawing.getScale(el)).toEqual({ x: 1, y: 1 }); - Drawing.setTranslate(el); - expect(el.attr('transform')).toBe('translate(0, 0)'); + el.setAttribute("transform", "scale(1.23, 45)"); + expect(Drawing.getScale(el)).toEqual({ x: 1.23, y: 45 }); - el.attr('transform', 'translate(0, 0); rotate(30)'); - Drawing.setTranslate(el, 30, 40); - expect(el.attr('transform')).toBe('rotate(30) translate(30, 40)'); - }); - }); + el.setAttribute("transform", "scale(123.45)"); + expect(Drawing.getScale(el)).toEqual({ x: 123.45, y: 1 }); - describe('getScale', function() { + el.setAttribute("transform", "scale(0.1 2)"); + expect(Drawing.getScale(el)).toEqual({ x: 0.1, y: 2 }); - it('should work with regular DOM elements', function() { - var el = document.createElement('div'); + el.setAttribute("transform", "scale(0.1 2); rotate(20deg)"); + expect(Drawing.getScale(el)).toEqual({ x: 0.1, y: 2 }); - expect(Drawing.getScale(el)).toEqual({ x: 1, y: 1 }); + el.setAttribute("transform", "rotate(20deg) scale(0.1 2);"); + expect(Drawing.getScale(el)).toEqual({ x: 0.1, y: 2 }); - el.setAttribute('transform', 'scale(1.23, 45)'); - expect(Drawing.getScale(el)).toEqual({ x: 1.23, y: 45 }); + el.setAttribute("transform", "rotate(20deg)"); + expect(Drawing.getScale(el)).toEqual({ x: 1, y: 1 }); + }); - el.setAttribute('transform', 'scale(123.45)'); - expect(Drawing.getScale(el)).toEqual({ x: 123.45, y: 1 }); + it("should work with d3 elements", function() { + var el = d3.select(document.createElement("div")); - el.setAttribute('transform', 'scale(0.1 2)'); - expect(Drawing.getScale(el)).toEqual({ x: 0.1, y: 2 }); + el.attr("transform", "scale(1.23, 45)"); + expect(Drawing.getScale(el)).toEqual({ x: 1.23, y: 45 }); - el.setAttribute('transform', 'scale(0.1 2); rotate(20deg)'); - expect(Drawing.getScale(el)).toEqual({ x: 0.1, y: 2 }); + el.attr("transform", "scale(123.45)"); + expect(Drawing.getScale(el)).toEqual({ x: 123.45, y: 1 }); - el.setAttribute('transform', 'rotate(20deg) scale(0.1 2);'); - expect(Drawing.getScale(el)).toEqual({ x: 0.1, y: 2 }); + el.attr("transform", "scale(0.1 2)"); + expect(Drawing.getScale(el)).toEqual({ x: 0.1, y: 2 }); - el.setAttribute('transform', 'rotate(20deg)'); - expect(Drawing.getScale(el)).toEqual({ x: 1, y: 1 }); - }); + el.attr("transform", "scale(0.1 2); rotate(20)"); + expect(Drawing.getScale(el)).toEqual({ x: 0.1, y: 2 }); - it('should work with d3 elements', function() { - var el = d3.select(document.createElement('div')); + el.attr("transform", "rotate(20)"); + expect(Drawing.getScale(el)).toEqual({ x: 1, y: 1 }); + }); + }); - el.attr('transform', 'scale(1.23, 45)'); - expect(Drawing.getScale(el)).toEqual({ x: 1.23, y: 45 }); + describe("setScale", function() { + it("should work with regular DOM elements", function() { + var el = document.createElement("div"); - el.attr('transform', 'scale(123.45)'); - expect(Drawing.getScale(el)).toEqual({ x: 123.45, y: 1 }); + Drawing.setScale(el, 5); + expect(el.getAttribute("transform")).toBe("scale(5, 1)"); - el.attr('transform', 'scale(0.1 2)'); - expect(Drawing.getScale(el)).toEqual({ x: 0.1, y: 2 }); + Drawing.setScale(el, 30, 40); + expect(el.getAttribute("transform")).toBe("scale(30, 40)"); - el.attr('transform', 'scale(0.1 2); rotate(20)'); - expect(Drawing.getScale(el)).toEqual({ x: 0.1, y: 2 }); + Drawing.setScale(el); + expect(el.getAttribute("transform")).toBe("scale(1, 1)"); - el.attr('transform', 'rotate(20)'); - expect(Drawing.getScale(el)).toEqual({ x: 1, y: 1 }); - }); + el.setAttribute("transform", "scale(1, 1); rotate(30)"); + Drawing.setScale(el, 30, 40); + expect(el.getAttribute("transform")).toBe("rotate(30) scale(30, 40)"); }); - describe('setScale', function() { + it("should work with d3 elements", function() { + var el = d3.select(document.createElement("div")); - it('should work with regular DOM elements', function() { - var el = document.createElement('div'); + Drawing.setScale(el, 5); + expect(el.attr("transform")).toBe("scale(5, 1)"); - Drawing.setScale(el, 5); - expect(el.getAttribute('transform')).toBe('scale(5, 1)'); + Drawing.setScale(el, 30, 40); + expect(el.attr("transform")).toBe("scale(30, 40)"); - Drawing.setScale(el, 30, 40); - expect(el.getAttribute('transform')).toBe('scale(30, 40)'); + Drawing.setScale(el); + expect(el.attr("transform")).toBe("scale(1, 1)"); - Drawing.setScale(el); - expect(el.getAttribute('transform')).toBe('scale(1, 1)'); + el.attr("transform", "scale(0, 0); rotate(30)"); + Drawing.setScale(el, 30, 40); + expect(el.attr("transform")).toBe("rotate(30) scale(30, 40)"); + }); + }); - el.setAttribute('transform', 'scale(1, 1); rotate(30)'); - Drawing.setScale(el, 30, 40); - expect(el.getAttribute('transform')).toBe('rotate(30) scale(30, 40)'); - }); + describe("setPointGroupScale", function() { + var el, sel; - it('should work with d3 elements', function() { - var el = d3.select(document.createElement('div')); + beforeEach(function() { + el = document.createElement("div"); + sel = d3.select(el); + }); - Drawing.setScale(el, 5); - expect(el.attr('transform')).toBe('scale(5, 1)'); + it("sets the scale of a point", function() { + Drawing.setPointGroupScale(sel, 2, 2); + expect(el.getAttribute("transform")).toBe("scale(2,2)"); + }); - Drawing.setScale(el, 30, 40); - expect(el.attr('transform')).toBe('scale(30, 40)'); + it("appends the scale of a point", function() { + el.setAttribute("transform", "translate(1,2)"); + Drawing.setPointGroupScale(sel, 2, 2); + expect(el.getAttribute("transform")).toBe("translate(1,2) scale(2,2)"); + }); - Drawing.setScale(el); - expect(el.attr('transform')).toBe('scale(1, 1)'); + it("modifies the scale of a point", function() { + el.setAttribute("transform", "translate(1,2) scale(3,4)"); + Drawing.setPointGroupScale(sel, 2, 2); + expect(el.getAttribute("transform")).toBe("translate(1,2) scale(2,2)"); + }); - el.attr('transform', 'scale(0, 0); rotate(30)'); - Drawing.setScale(el, 30, 40); - expect(el.attr('transform')).toBe('rotate(30) scale(30, 40)'); - }); + it("does not apply the scale of a point if scale (1, 1)", function() { + el.setAttribute("transform", "translate(1,2)"); + Drawing.setPointGroupScale(sel, 1, 1); + expect(el.getAttribute("transform")).toBe("translate(1,2)"); }); - describe('setPointGroupScale', function() { - var el, sel; - - beforeEach(function() { - el = document.createElement('div'); - sel = d3.select(el); - }); - - it('sets the scale of a point', function() { - Drawing.setPointGroupScale(sel, 2, 2); - expect(el.getAttribute('transform')).toBe('scale(2,2)'); - }); - - it('appends the scale of a point', function() { - el.setAttribute('transform', 'translate(1,2)'); - Drawing.setPointGroupScale(sel, 2, 2); - expect(el.getAttribute('transform')).toBe('translate(1,2) scale(2,2)'); - }); - - it('modifies the scale of a point', function() { - el.setAttribute('transform', 'translate(1,2) scale(3,4)'); - Drawing.setPointGroupScale(sel, 2, 2); - expect(el.getAttribute('transform')).toBe('translate(1,2) scale(2,2)'); - }); - - it('does not apply the scale of a point if scale (1, 1)', function() { - el.setAttribute('transform', 'translate(1,2)'); - Drawing.setPointGroupScale(sel, 1, 1); - expect(el.getAttribute('transform')).toBe('translate(1,2)'); - }); - - it('removes the scale of a point if scale (1, 1)', function() { - el.setAttribute('transform', 'translate(1,2) scale(3,4)'); - Drawing.setPointGroupScale(sel, 1, 1); - expect(el.getAttribute('transform')).toBe('translate(1,2)'); - }); + it("removes the scale of a point if scale (1, 1)", function() { + el.setAttribute("transform", "translate(1,2) scale(3,4)"); + Drawing.setPointGroupScale(sel, 1, 1); + expect(el.getAttribute("transform")).toBe("translate(1,2)"); }); + }); }); diff --git a/test/jasmine/tests/events_test.js b/test/jasmine/tests/events_test.js index d4cc5bf423a..4b10e9ca0f9 100644 --- a/test/jasmine/tests/events_test.js +++ b/test/jasmine/tests/events_test.js @@ -6,332 +6,341 @@ * compatibility with JQuery events. */ -var Events = require('@src/lib/events'); - -describe('Events', function() { - 'use strict'; - - var plotObj; - var plotDiv; - - beforeEach(function() { - plotObj = {}; - plotDiv = document.createElement('div'); +var Events = require("@src/lib/events"); + +describe("Events", function() { + "use strict"; + var plotObj; + var plotDiv; + + beforeEach(function() { + plotObj = {}; + plotDiv = document.createElement("div"); + }); + + describe("init", function() { + it("instantiates an emitter on incoming plot object", function() { + expect(plotObj._ev).not.toBeDefined(); + expect(Events.init(plotObj)._ev).toBeDefined(); }); - describe('init', function() { + it("maps function onto incoming plot object", function() { + Events.init(plotObj); - it('instantiates an emitter on incoming plot object', function() { - expect(plotObj._ev).not.toBeDefined(); - expect(Events.init(plotObj)._ev).toBeDefined(); - }); + expect(typeof plotObj.on).toBe("function"); + expect(typeof plotObj.once).toBe("function"); + expect(typeof plotObj.removeListener).toBe("function"); + expect(typeof plotObj.removeAllListeners).toBe("function"); + }); - it('maps function onto incoming plot object', function() { - Events.init(plotObj); + it("is idempotent", function() { + Events.init(plotObj); + plotObj.emit = function() { + return "initial"; + }; - expect(typeof plotObj.on).toBe('function'); - expect(typeof plotObj.once).toBe('function'); - expect(typeof plotObj.removeListener).toBe('function'); - expect(typeof plotObj.removeAllListeners).toBe('function'); - }); + Events.init(plotObj); + expect(plotObj.emit()).toBe("initial"); + }); - it('is idempotent', function() { - Events.init(plotObj); - plotObj.emit = function() { - return 'initial'; - }; + it("triggers node style events", function(done) { + Events.init(plotObj); - Events.init(plotObj); - expect(plotObj.emit()).toBe('initial'); - }); + plotObj.on("ping", function(data) { + expect(data).toBe("pong"); + done(); + }); - it('triggers node style events', function(done) { - Events.init(plotObj); + setTimeout(function() { + plotObj.emit("ping", "pong"); + }); + }); - plotObj.on('ping', function(data) { - expect(data).toBe('pong'); - done(); - }); + it("triggers jquery events", function(done) { + Events.init(plotDiv); - setTimeout(function() { - plotObj.emit('ping', 'pong'); - }); - }); + $(plotDiv).bind("ping", function(event, data) { + expect(data).toBe("pong"); + done(); + }); - it('triggers jquery events', function(done) { - Events.init(plotDiv); + setTimeout(function() { + $(plotDiv).trigger("ping", "pong"); + }); + }); - $(plotDiv).bind('ping', function(event, data) { - expect(data).toBe('pong'); - done(); - }); + it("mirrors events on an internal handler", function(done) { + Events.init(plotDiv); - setTimeout(function() { - $(plotDiv).trigger('ping', 'pong'); - }); - }); + plotDiv._internalOn("ping", function(data) { + expect(data).toBe("pong"); + done(); + }); - it('mirrors events on an internal handler', function(done) { - Events.init(plotDiv); + setTimeout(function() { + plotDiv.emit("ping", "pong"); + }); + }); + }); - plotDiv._internalOn('ping', function(data) { - expect(data).toBe('pong'); - done(); - }); + describe("triggerHandler", function() { + it("triggers node handlers and returns last value", function() { + var eventBaton = 0; - setTimeout(function() { - plotDiv.emit('ping', 'pong'); - }); - }); - }); + Events.init(plotDiv); - describe('triggerHandler', function() { + plotDiv.on("ping", function() { + eventBaton++; + return "ping"; + }); - it('triggers node handlers and returns last value', function() { - var eventBaton = 0; + plotDiv.on("ping", function() { + eventBaton++; + return "ping"; + }); - Events.init(plotDiv); + plotDiv.on("ping", function() { + eventBaton++; + return "pong"; + }); - plotDiv.on('ping', function() { - eventBaton++; - return 'ping'; - }); + var result = Events.triggerHandler(plotDiv, "ping"); - plotDiv.on('ping', function() { - eventBaton++; - return 'ping'; - }); + expect(eventBaton).toBe(3); + expect(result).toBe("pong"); + }); - plotDiv.on('ping', function() { - eventBaton++; - return 'pong'; - }); + it( + "does *not* mirror triggerHandler events on the internal handler", + function() { + var eventBaton = 0; + var internalEventBaton = 0; - var result = Events.triggerHandler(plotDiv, 'ping'); + Events.init(plotDiv); - expect(eventBaton).toBe(3); - expect(result).toBe('pong'); + plotDiv.on("ping", function() { + eventBaton++; + return "ping"; }); - it('does *not* mirror triggerHandler events on the internal handler', function() { - var eventBaton = 0; - var internalEventBaton = 0; + plotDiv._internalOn("ping", function() { + internalEventBaton++; + return "foo"; + }); - Events.init(plotDiv); + plotDiv.on("ping", function() { + eventBaton++; + return "pong"; + }); - plotDiv.on('ping', function() { - eventBaton++; - return 'ping'; - }); + var result = Events.triggerHandler(plotDiv, "ping"); - plotDiv._internalOn('ping', function() { - internalEventBaton++; - return 'foo'; - }); + expect(eventBaton).toBe(2); + expect(internalEventBaton).toBe(0); + expect(result).toBe("pong"); + } + ); - plotDiv.on('ping', function() { - eventBaton++; - return 'pong'; - }); + it( + "triggers jQuery handlers when no matching node events bound", + function() { + var eventBaton = 0; - var result = Events.triggerHandler(plotDiv, 'ping'); + Events.init(plotDiv); - expect(eventBaton).toBe(2); - expect(internalEventBaton).toBe(0); - expect(result).toBe('pong'); + $(plotDiv).bind("ping", function() { + eventBaton++; + return "ping"; }); - it('triggers jQuery handlers when no matching node events bound', function() { - var eventBaton = 0; - - Events.init(plotDiv); - - $(plotDiv).bind('ping', function() { - eventBaton++; - return 'ping'; - }); - - /* + /* * This will not be called */ - plotDiv.on('pong', function() { - eventBaton++; - return 'ping'; - }); - - $(plotDiv).bind('ping', function() { - eventBaton++; - return 'pong'; - }); - - var result = Events.triggerHandler(plotDiv, 'ping'); - - expect(eventBaton).toBe(2); - expect(result).toBe('pong'); + plotDiv.on("pong", function() { + eventBaton++; + return "ping"; }); - it('triggers jQuery handlers when no node events initialized', function() { - var eventBaton = 0; + $(plotDiv).bind("ping", function() { + eventBaton++; + return "pong"; + }); - $(plotDiv).bind('ping', function() { - eventBaton++; - return 'ping'; - }); + var result = Events.triggerHandler(plotDiv, "ping"); - $(plotDiv).bind('ping', function() { - eventBaton++; - return 'ping'; - }); + expect(eventBaton).toBe(2); + expect(result).toBe("pong"); + } + ); - $(plotDiv).bind('ping', function() { - eventBaton++; - return 'pong'; - }); + it("triggers jQuery handlers when no node events initialized", function() { + var eventBaton = 0; - var result = Events.triggerHandler(plotDiv, 'ping'); + $(plotDiv).bind("ping", function() { + eventBaton++; + return "ping"; + }); - expect(eventBaton).toBe(3); - expect(result).toBe('pong'); - }); + $(plotDiv).bind("ping", function() { + eventBaton++; + return "ping"; + }); + $(plotDiv).bind("ping", function() { + eventBaton++; + return "pong"; + }); - it('triggers jQuery + nodejs handlers and returns last jQuery value', function() { - var eventBaton = 0; + var result = Events.triggerHandler(plotDiv, "ping"); - Events.init(plotDiv); + expect(eventBaton).toBe(3); + expect(result).toBe("pong"); + }); - $(plotDiv).bind('ping', function() { - eventBaton++; - return 'ping'; - }); + it( + "triggers jQuery + nodejs handlers and returns last jQuery value", + function() { + var eventBaton = 0; - plotDiv.on('ping', function() { - eventBaton++; - return 'ping'; - }); + Events.init(plotDiv); - $(plotDiv).bind('ping', function() { - eventBaton++; - return 'pong'; - }); + $(plotDiv).bind("ping", function() { + eventBaton++; + return "ping"; + }); - var result = Events.triggerHandler(plotDiv, 'ping'); + plotDiv.on("ping", function() { + eventBaton++; + return "ping"; + }); - expect(eventBaton).toBe(3); - expect(result).toBe('pong'); + $(plotDiv).bind("ping", function() { + eventBaton++; + return "pong"; }); - }); - describe('purge', function() { - it('should remove all method from the plotObj', function() { - Events.init(plotObj); - Events.purge(plotObj); + var result = Events.triggerHandler(plotDiv, "ping"); - expect(plotObj).toEqual({}); - }); - }); + expect(eventBaton).toBe(3); + expect(result).toBe("pong"); + } + ); + }); - describe('when jQuery.noConflict is set, ', function() { + describe("purge", function() { + it("should remove all method from the plotObj", function() { + Events.init(plotObj); + Events.purge(plotObj); - beforeEach(function() { - $.noConflict(); - }); + expect(plotObj).toEqual({}); + }); + }); - afterEach(function() { - window.$ = jQuery; - }); + describe("when jQuery.noConflict is set, ", function() { + beforeEach(function() { + $.noConflict(); + }); - it('triggers jquery events', function(done) { + afterEach(function() { + window.$ = jQuery; + }); - Events.init(plotDiv); + it("triggers jquery events", function(done) { + Events.init(plotDiv); - jQuery(plotDiv).bind('ping', function(event, data) { - expect(data).toBe('pong'); - done(); - }); + jQuery(plotDiv).bind("ping", function(event, data) { + expect(data).toBe("pong"); + done(); + }); - setTimeout(function() { - jQuery(plotDiv).trigger('ping', 'pong'); - }); - }); + setTimeout(function() { + jQuery(plotDiv).trigger("ping", "pong"); + }); + }); - it('triggers jQuery handlers when no matching node events bound', function() { - var eventBaton = 0; + it( + "triggers jQuery handlers when no matching node events bound", + function() { + var eventBaton = 0; - Events.init(plotDiv); + Events.init(plotDiv); - jQuery(plotDiv).bind('ping', function() { - eventBaton++; - return 'ping'; - }); + jQuery(plotDiv).bind("ping", function() { + eventBaton++; + return "ping"; + }); - /* + /* * This will not be called */ - plotDiv.on('pong', function() { - eventBaton++; - return 'ping'; - }); - - jQuery(plotDiv).bind('ping', function() { - eventBaton++; - return 'pong'; - }); - - var result = Events.triggerHandler(plotDiv, 'ping'); + plotDiv.on("pong", function() { + eventBaton++; + return "ping"; + }); - expect(eventBaton).toBe(2); - expect(result).toBe('pong'); + jQuery(plotDiv).bind("ping", function() { + eventBaton++; + return "pong"; }); - it('triggers jQuery handlers when no node events initialized', function() { - var eventBaton = 0; + var result = Events.triggerHandler(plotDiv, "ping"); - jQuery(plotDiv).bind('ping', function() { - eventBaton++; - return 'ping'; - }); + expect(eventBaton).toBe(2); + expect(result).toBe("pong"); + } + ); - jQuery(plotDiv).bind('ping', function() { - eventBaton++; - return 'ping'; - }); + it("triggers jQuery handlers when no node events initialized", function() { + var eventBaton = 0; - jQuery(plotDiv).bind('ping', function() { - eventBaton++; - return 'pong'; - }); + jQuery(plotDiv).bind("ping", function() { + eventBaton++; + return "ping"; + }); - var result = Events.triggerHandler(plotDiv, 'ping'); + jQuery(plotDiv).bind("ping", function() { + eventBaton++; + return "ping"; + }); - expect(eventBaton).toBe(3); - expect(result).toBe('pong'); - }); + jQuery(plotDiv).bind("ping", function() { + eventBaton++; + return "pong"; + }); - it('triggers jQuery + nodejs handlers and returns last jQuery value', function() { - var eventBaton = 0; + var result = Events.triggerHandler(plotDiv, "ping"); - Events.init(plotDiv); + expect(eventBaton).toBe(3); + expect(result).toBe("pong"); + }); - jQuery(plotDiv).bind('ping', function() { - eventBaton++; - return 'ping'; - }); + it( + "triggers jQuery + nodejs handlers and returns last jQuery value", + function() { + var eventBaton = 0; - plotDiv.on('ping', function() { - eventBaton++; - return 'ping'; - }); + Events.init(plotDiv); - jQuery(plotDiv).bind('ping', function() { - eventBaton++; - return 'pong'; - }); + jQuery(plotDiv).bind("ping", function() { + eventBaton++; + return "ping"; + }); - var result = Events.triggerHandler(plotDiv, 'ping'); + plotDiv.on("ping", function() { + eventBaton++; + return "ping"; + }); - expect(eventBaton).toBe(3); - expect(result).toBe('pong'); + jQuery(plotDiv).bind("ping", function() { + eventBaton++; + return "pong"; }); - }); + + var result = Events.triggerHandler(plotDiv, "ping"); + + expect(eventBaton).toBe(3); + expect(result).toBe("pong"); + } + ); + }); }); diff --git a/test/jasmine/tests/extend_test.js b/test/jasmine/tests/extend_test.js index 7c4b50ce0a1..d800d3cdf11 100644 --- a/test/jasmine/tests/extend_test.js +++ b/test/jasmine/tests/extend_test.js @@ -1,524 +1,510 @@ -var extendModule = require('@src/lib/extend.js'); +var extendModule = require("@src/lib/extend.js"); var extendFlat = extendModule.extendFlat; var extendDeep = extendModule.extendDeep; var extendDeepAll = extendModule.extendDeepAll; var extendDeepNoArrays = extendModule.extendDeepNoArrays; -var str = 'me a test', - integer = 10, - arr = [1, 'what', new Date(81, 8, 4)], - date = new Date(81, 4, 13); +var str = "me a test", + integer = 10, + arr = [1, "what", new Date(81, 8, 4)], + date = new Date(81, 4, 13); var Foo = function() {}; var obj = { - str: str, - integer: integer, - arr: arr, - date: date, - constructor: 'fake', - isPrototypeOf: 'not a function', - foo: new Foo() + str: str, + integer: integer, + arr: arr, + date: date, + constructor: "fake", + isPrototypeOf: "not a function", + foo: new Foo() }; var deep = { - ori: obj, - layer: { - integer: 10, - str: 'str', - date: new Date(84, 5, 12), - arr: [101, 'dude', new Date(82, 10, 4)], - deep: { - str: obj.str, - integer: integer, - arr: obj.arr, - date: new Date(81, 7, 4) - } + ori: obj, + layer: { + integer: 10, + str: "str", + date: new Date(84, 5, 12), + arr: [101, "dude", new Date(82, 10, 4)], + deep: { + str: obj.str, + integer: integer, + arr: obj.arr, + date: new Date(81, 7, 4) } + } }; var undef = { - str: undefined, - layer: { - date: undefined - }, - arr: [1, 2, undefined] + str: undefined, + layer: { date: undefined }, + arr: [1, 2, undefined] }; var undef2 = { - str: undefined, - layer: { - date: undefined - }, - arr: [1, undefined, 2] + str: undefined, + layer: { date: undefined }, + arr: [1, undefined, 2] }; - -describe('extendFlat', function() { - 'use strict'; - - var ori, target; - - it('extends an array with an array', function() { - ori = [1, 2, 3, 4, 5, 6]; - target = extendFlat(ori, arr); - - expect(ori).toEqual([1, 'what', new Date(81, 8, 4), 4, 5, 6]); - expect(arr).toEqual([1, 'what', new Date(81, 8, 4)]); - expect(target).toEqual([1, 'what', new Date(81, 8, 4), 4, 5, 6]); - +describe("extendFlat", function() { + "use strict"; + var ori, target; + + it("extends an array with an array", function() { + ori = [1, 2, 3, 4, 5, 6]; + target = extendFlat(ori, arr); + + expect(ori).toEqual([1, "what", new Date(81, 8, 4), 4, 5, 6]); + expect(arr).toEqual([1, "what", new Date(81, 8, 4)]); + expect(target).toEqual([1, "what", new Date(81, 8, 4), 4, 5, 6]); + }); + + it("extends an array with an array into a clone", function() { + ori = [1, 2, 3, 4, 5, 6]; + target = extendFlat([], ori, arr); + + expect(ori).toEqual([1, 2, 3, 4, 5, 6]); + expect(arr).toEqual([1, "what", new Date(81, 8, 4)]); + expect(target).toEqual([1, "what", new Date(81, 8, 4), 4, 5, 6]); + }); + + it("extends an array with an object", function() { + ori = [1, 2, 3, 4, 5, 6]; + target = extendFlat(ori, obj); + + expect(obj).toEqual({ + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: "fake", + isPrototypeOf: "not a function", + foo: new Foo() }); - it('extends an array with an array into a clone', function() { - ori = [1, 2, 3, 4, 5, 6]; - target = extendFlat([], ori, arr); - - expect(ori).toEqual([1, 2, 3, 4, 5, 6]); - expect(arr).toEqual([1, 'what', new Date(81, 8, 4)]); - expect(target).toEqual([1, 'what', new Date(81, 8, 4), 4, 5, 6]); + expect(ori.length).toEqual(6); + expect(ori.str).toEqual("me a test"); + expect(ori.integer).toEqual(10); + expect(ori.arr).toEqual([1, "what", new Date(81, 8, 4)]); + expect(ori.date).toEqual(new Date(81, 4, 13)); + + expect(target.length).toEqual(6); + expect(target.str).toEqual("me a test"); + expect(target.integer).toEqual(10); + expect(target.arr).toEqual([1, "what", new Date(81, 8, 4)]); + expect(target.date).toEqual(new Date(81, 4, 13)); + }); + + it("extends an object with an array", function() { + ori = { + str: "no shit", + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26) + }; + target = extendFlat(ori, arr); + + expect(ori).toEqual({ + 0: 1, + 1: "what", + 2: new Date(81, 8, 4), + str: "no shit", + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26) }); - - it('extends an array with an object', function() { - ori = [1, 2, 3, 4, 5, 6]; - target = extendFlat(ori, obj); - - expect(obj).toEqual({ - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 4, 13), - constructor: 'fake', - isPrototypeOf: 'not a function', - foo: new Foo() - }); - - expect(ori.length).toEqual(6); - expect(ori.str).toEqual('me a test'); - expect(ori.integer).toEqual(10); - expect(ori.arr).toEqual([1, 'what', new Date(81, 8, 4)]); - expect(ori.date).toEqual(new Date(81, 4, 13)); - - expect(target.length).toEqual(6); - expect(target.str).toEqual('me a test'); - expect(target.integer).toEqual(10); - expect(target.arr).toEqual([1, 'what', new Date(81, 8, 4)]); - expect(target.date).toEqual(new Date(81, 4, 13)); + expect(arr).toEqual([1, "what", new Date(81, 8, 4)]); + expect(target).toEqual({ + 0: 1, + 1: "what", + 2: new Date(81, 8, 4), + str: "no shit", + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26) }); - - it('extends an object with an array', function() { - ori = { - str: 'no shit', - integer: 76, - arr: [1, 2, 3, 4], - date: new Date(81, 7, 26) - }; - target = extendFlat(ori, arr); - - expect(ori).toEqual({ - 0: 1, - 1: 'what', - 2: new Date(81, 8, 4), - str: 'no shit', - integer: 76, - arr: [1, 2, 3, 4], - date: new Date(81, 7, 26) - }); - expect(arr).toEqual([1, 'what', new Date(81, 8, 4)]); - expect(target).toEqual({ - 0: 1, - 1: 'what', - 2: new Date(81, 8, 4), - str: 'no shit', - integer: 76, - arr: [1, 2, 3, 4], - date: new Date(81, 7, 26) - }); + }); + + it("extends an object with another object", function() { + ori = { + str: "no shit", + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26), + foo: "bar" + }; + target = extendFlat(ori, obj); + + expect(ori).toEqual({ + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: "fake", + isPrototypeOf: "not a function", + foo: new Foo() }); - - it('extends an object with another object', function() { - ori = { - str: 'no shit', - integer: 76, - arr: [1, 2, 3, 4], - date: new Date(81, 7, 26), - foo: 'bar' - }; - target = extendFlat(ori, obj); - - expect(ori).toEqual({ - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 4, 13), - constructor: 'fake', - isPrototypeOf: 'not a function', - foo: new Foo() - }); - expect(obj).toEqual({ - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 4, 13), - constructor: 'fake', - isPrototypeOf: 'not a function', - foo: new Foo() - }); - expect(target).toEqual({ - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 4, 13), - constructor: 'fake', - isPrototypeOf: 'not a function', - foo: new Foo() - }); + expect(obj).toEqual({ + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: "fake", + isPrototypeOf: "not a function", + foo: new Foo() + }); + expect(target).toEqual({ + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: "fake", + isPrototypeOf: "not a function", + foo: new Foo() }); + }); - it('merges array keys', function() { - var defaults = { - arr: [1, 2, 3] - }; + it("merges array keys", function() { + var defaults = { arr: [1, 2, 3] }; - var override = { - arr: ['x'] - }; + var override = { arr: ["x"] }; - target = extendFlat(defaults, override); + target = extendFlat(defaults, override); - expect(defaults).toEqual({arr: ['x']}); - expect(override).toEqual({arr: ['x']}); - expect(target).toEqual({arr: ['x']}); - }); + expect(defaults).toEqual({ arr: ["x"] }); + expect(override).toEqual({ arr: ["x"] }); + expect(target).toEqual({ arr: ["x"] }); + }); - it('ignores keys with undefined values', function() { - ori = {}; - target = extendFlat(ori, undef); - - expect(ori).toEqual({ - layer: { date: undefined }, - arr: [1, 2, undefined] - }); - expect(undef).toEqual({ - str: undefined, - layer: { - date: undefined - }, - arr: [1, 2, undefined] - }); - expect(target).toEqual({ - layer: { date: undefined }, - arr: [1, 2, undefined] - }); - }); + it("ignores keys with undefined values", function() { + ori = {}; + target = extendFlat(ori, undef); - it('does not handle null inputs', function() { - expect(function() { - extendFlat(null, obj); - }).toThrowError(TypeError); + expect(ori).toEqual({ layer: { date: undefined }, arr: [1, 2, undefined] }); + expect(undef).toEqual({ + str: undefined, + layer: { date: undefined }, + arr: [1, 2, undefined] }); - - it('does not handle string targets', function() { - expect(function() { - extendFlat(null, obj); - }).toThrowError(TypeError); + expect(target).toEqual({ + layer: { date: undefined }, + arr: [1, 2, undefined] }); + }); + + it("does not handle null inputs", function() { + expect(function() { + extendFlat(null, obj); + }).toThrowError(TypeError); + }); + + it("does not handle string targets", function() { + expect(function() { + extendFlat(null, obj); + }).toThrowError(TypeError); + }); }); -describe('extendDeep', function() { - 'use strict'; - - var ori, target; - - it('extends nested object with another nested object', function() { - ori = { - str: 'no shit', - integer: 76, - arr: [1, 2, 3, 4], - date: new Date(81, 7, 26), - layer: { - deep: { - integer: 42 - } - } - }; - target = extendDeep(ori, deep); - - expect(ori).toEqual({ - str: 'no shit', - integer: 76, - arr: [1, 2, 3, 4], - date: new Date(81, 7, 26), - ori: { - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 4, 13), - constructor: 'fake', - isPrototypeOf: 'not a function', - foo: new Foo() - }, - layer: { - integer: 10, - str: 'str', - date: new Date(84, 5, 12), - arr: [101, 'dude', new Date(82, 10, 4)], - deep: { - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 7, 4) - } - } - }); - expect(deep).toEqual({ - ori: { - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 4, 13), - constructor: 'fake', - isPrototypeOf: 'not a function', - foo: new Foo() - }, - layer: { - integer: 10, - str: 'str', - date: new Date(84, 5, 12), - arr: [101, 'dude', new Date(82, 10, 4)], - deep: { - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 7, 4) - } - } - }); - expect(target).toEqual({ - str: 'no shit', - integer: 76, - arr: [1, 2, 3, 4], - date: new Date(81, 7, 26), - ori: { - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 4, 13), - constructor: 'fake', - isPrototypeOf: 'not a function', - foo: new Foo() - }, - layer: { - integer: 10, - str: 'str', - date: new Date(84, 5, 12), - arr: [101, 'dude', new Date(82, 10, 4)], - deep: { - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 7, 4) - } - } - }); - }); - - it('doesn\'t modify source objects after setting the target', function() { - ori = { - str: 'no shit', - integer: 76, - arr: [1, 2, 3, 4], - date: new Date(81, 7, 26), - layer: { - deep: { - integer: 42 - } - } - }; - target = extendDeep(ori, deep); - target.layer.deep.integer = 100; - - expect(ori.layer.deep.integer).toEqual(100); - expect(deep).toEqual({ - ori: { - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 4, 13), - constructor: 'fake', - isPrototypeOf: 'not a function', - foo: new Foo() - }, - layer: { - integer: 10, - str: 'str', - date: new Date(84, 5, 12), - arr: [101, 'dude', new Date(82, 10, 4)], - deep: { - str: 'me a test', - integer: 10, - arr: [1, 'what', new Date(81, 8, 4)], - date: new Date(81, 7, 4) - } - } - }); +describe("extendDeep", function() { + "use strict"; + var ori, target; + + it("extends nested object with another nested object", function() { + ori = { + str: "no shit", + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26), + layer: { deep: { integer: 42 } } + }; + target = extendDeep(ori, deep); + + expect(ori).toEqual({ + str: "no shit", + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26), + ori: { + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: "fake", + isPrototypeOf: "not a function", + foo: new Foo() + }, + layer: { + integer: 10, + str: "str", + date: new Date(84, 5, 12), + arr: [101, "dude", new Date(82, 10, 4)], + deep: { + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 7, 4) + } + } }); - - it('merges array items', function() { - var defaults = { - arr: [1, 2, 3] - }; - - var override = { - arr: ['x'] - }; - - target = extendDeep(defaults, override); - - expect(defaults).toEqual({arr: ['x', 2, 3]}); - expect(override).toEqual({arr: ['x']}); - expect(target).toEqual({arr: ['x', 2, 3]}); + expect(deep).toEqual({ + ori: { + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: "fake", + isPrototypeOf: "not a function", + foo: new Foo() + }, + layer: { + integer: 10, + str: "str", + date: new Date(84, 5, 12), + arr: [101, "dude", new Date(82, 10, 4)], + deep: { + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 7, 4) + } + } }); - - it('ignores keys with undefined values', function() { - ori = {}; - target = extendDeep(ori, undef); - - expect(ori).toEqual({ - layer: { }, - arr: [1, 2] - }); - expect(undef).toEqual({ - str: undefined, - layer: { - date: undefined - }, - arr: [1, 2, undefined] - }); - expect(target).toEqual({ - layer: { }, - arr: [1, 2] - }); + expect(target).toEqual({ + str: "no shit", + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26), + ori: { + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: "fake", + isPrototypeOf: "not a function", + foo: new Foo() + }, + layer: { + integer: 10, + str: "str", + date: new Date(84, 5, 12), + arr: [101, "dude", new Date(82, 10, 4)], + deep: { + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 7, 4) + } + } }); - - it('leaves a gap in the array for undefined of lower index than that of the highest defined value', function() { - ori = {}; - target = extendDeep(ori, undef2); - - var compare = []; - compare[0] = 1; - // compare[1] left undefined - compare[2] = 2; - - expect(ori).toEqual({ - layer: { }, - arr: compare - }); - expect(undef2).toEqual({ - str: undefined, - layer: { - date: undefined - }, - arr: [1, undefined, 2] - }); - expect(target).toEqual({ - layer: { }, - arr: compare - }); + }); + + it("doesn't modify source objects after setting the target", function() { + ori = { + str: "no shit", + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26), + layer: { deep: { integer: 42 } } + }; + target = extendDeep(ori, deep); + target.layer.deep.integer = 100; + + expect(ori.layer.deep.integer).toEqual(100); + expect(deep).toEqual({ + ori: { + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: "fake", + isPrototypeOf: "not a function", + foo: new Foo() + }, + layer: { + integer: 10, + str: "str", + date: new Date(84, 5, 12), + arr: [101, "dude", new Date(82, 10, 4)], + deep: { + str: "me a test", + integer: 10, + arr: [1, "what", new Date(81, 8, 4)], + date: new Date(81, 7, 4) + } + } }); + }); - it('does not handle circular structure', function() { - var circ = { a: {b: null} }; - circ.a.b = circ; - - expect(function() { - extendDeep({}, circ); - }).toThrow(); + it("merges array items", function() { + var defaults = { arr: [1, 2, 3] }; - // results in an InternalError on Chrome and - // a RangeError on Firefox - }); -}); + var override = { arr: ["x"] }; -describe('extendDeepAll', function() { - 'use strict'; + target = extendDeep(defaults, override); - var ori; + expect(defaults).toEqual({ arr: ["x", 2, 3] }); + expect(override).toEqual({ arr: ["x"] }); + expect(target).toEqual({ arr: ["x", 2, 3] }); + }); - it('extends object with another other containing keys undefined values', function() { - ori = {}; - extendDeepAll(ori, deep, undef); + it("ignores keys with undefined values", function() { + ori = {}; + target = extendDeep(ori, undef); - expect(ori.str).toBe(undefined); - expect(ori.layer.date).toBe(undefined); - expect(ori.arr[2]).toBe(undefined); + expect(ori).toEqual({ layer: {}, arr: [1, 2] }); + expect(undef).toEqual({ + str: undefined, + layer: { date: undefined }, + arr: [1, 2, undefined] }); + expect(target).toEqual({ layer: {}, arr: [1, 2] }); + }); + + it( + "leaves a gap in the array for undefined of lower index than that of the highest defined value", + function() { + ori = {}; + target = extendDeep(ori, undef2); + + var compare = []; + compare[0] = 1; + // compare[1] left undefined + compare[2] = 2; + + expect(ori).toEqual({ layer: {}, arr: compare }); + expect(undef2).toEqual({ + str: undefined, + layer: { date: undefined }, + arr: [1, undefined, 2] + }); + expect(target).toEqual({ layer: {}, arr: compare }); + } + ); + + it("does not handle circular structure", function() { + var circ = { a: { b: null } }; + circ.a.b = circ; + + expect(function() { + extendDeep({}, circ); + }).toThrow(); + // results in an InternalError on Chrome and + // a RangeError on Firefox + }); }); -describe('array by reference vs deep-copy', function() { - 'use strict'; - - it('extendDeep DOES deep-copy untyped source arrays', function() { - var src = {foo: {bar: [1, 2, 3], baz: [5, 4, 3]}}; - var tar = {foo: {bar: [4, 5, 6], bop: [8, 2, 1]}}; - var ext = extendDeep(tar, src); +describe("extendDeepAll", function() { + "use strict"; + var ori; - expect(ext).not.toBe(src); - expect(ext).toBe(tar); + it( + "extends object with another other containing keys undefined values", + function() { + ori = {}; + extendDeepAll(ori, deep, undef); - expect(ext.foo).not.toBe(src.foo); - expect(ext.foo).toBe(tar.foo); + expect(ori.str).toBe(undefined); + expect(ori.layer.date).toBe(undefined); + expect(ori.arr[2]).toBe(undefined); + } + ); +}); - expect(ext.foo.bar).not.toBe(src.foo.bar); - expect(ext.foo.baz).not.toBe(src.foo.baz); - expect(ext.foo.bop).toBe(tar.foo.bop); // what comes from the target isn't deep copied - }); +describe("array by reference vs deep-copy", function() { + "use strict"; + it("extendDeep DOES deep-copy untyped source arrays", function() { + var src = { foo: { bar: [1, 2, 3], baz: [5, 4, 3] } }; + var tar = { foo: { bar: [4, 5, 6], bop: [8, 2, 1] } }; + var ext = extendDeep(tar, src); - it('extendDeepNoArrays includes by reference untyped arrays from source', function() { - var src = {foo: {bar: [1, 2, 3], baz: [5, 4, 3]}}; - var tar = {foo: {bar: [4, 5, 6], bop: [8, 2, 1]}}; - var ext = extendDeepNoArrays(tar, src); + expect(ext).not.toBe(src); + expect(ext).toBe(tar); - expect(ext).not.toBe(src); - expect(ext).toBe(tar); + expect(ext.foo).not.toBe(src.foo); + expect(ext.foo).toBe(tar.foo); - expect(ext.foo).not.toBe(src.foo); - expect(ext.foo).toBe(tar.foo); + expect(ext.foo.bar).not.toBe(src.foo.bar); + expect(ext.foo.baz).not.toBe(src.foo.baz); + expect(ext.foo.bop).toBe(tar.foo.bop); // what comes from the target isn't deep copied + }); - expect(ext.foo.bar).toBe(src.foo.bar); - expect(ext.foo.baz).toBe(src.foo.baz); - expect(ext.foo.bop).toBe(tar.foo.bop); - }); + it( + "extendDeepNoArrays includes by reference untyped arrays from source", + function() { + var src = { foo: { bar: [1, 2, 3], baz: [5, 4, 3] } }; + var tar = { foo: { bar: [4, 5, 6], bop: [8, 2, 1] } }; + var ext = extendDeepNoArrays(tar, src); - it('extendDeepNoArrays includes by reference typed arrays from source', function() { - var src = {foo: {bar: new Int32Array([1, 2, 3]), baz: new Float32Array([5, 4, 3])}}; - var tar = {foo: {bar: new Int16Array([4, 5, 6]), bop: new Float64Array([8, 2, 1])}}; - var ext = extendDeepNoArrays(tar, src); + expect(ext).not.toBe(src); + expect(ext).toBe(tar); - expect(ext).not.toBe(src); - expect(ext).toBe(tar); + expect(ext.foo).not.toBe(src.foo); + expect(ext.foo).toBe(tar.foo); - expect(ext.foo).not.toBe(src.foo); - expect(ext.foo).toBe(tar.foo); + expect(ext.foo.bar).toBe(src.foo.bar); + expect(ext.foo.baz).toBe(src.foo.baz); + expect(ext.foo.bop).toBe(tar.foo.bop); + } + ); + + it( + "extendDeepNoArrays includes by reference typed arrays from source", + function() { + var src = { + foo: { + bar: new Int32Array([1, 2, 3]), + baz: new Float32Array([5, 4, 3]) + } + }; + var tar = { + foo: { + bar: new Int16Array([4, 5, 6]), + bop: new Float64Array([8, 2, 1]) + } + }; + var ext = extendDeepNoArrays(tar, src); - expect(ext.foo.bar).toBe(src.foo.bar); - expect(ext.foo.baz).toBe(src.foo.baz); - expect(ext.foo.bop).toBe(tar.foo.bop); - }); + expect(ext).not.toBe(src); + expect(ext).toBe(tar); - it('extendDeep ALSO includes by reference typed arrays from source', function() { - var src = {foo: {bar: new Int32Array([1, 2, 3]), baz: new Float32Array([5, 4, 3])}}; - var tar = {foo: {bar: new Int16Array([4, 5, 6]), bop: new Float64Array([8, 2, 1])}}; - var ext = extendDeep(tar, src); + expect(ext.foo).not.toBe(src.foo); + expect(ext.foo).toBe(tar.foo); - expect(ext).not.toBe(src); - expect(ext).toBe(tar); + expect(ext.foo.bar).toBe(src.foo.bar); + expect(ext.foo.baz).toBe(src.foo.baz); + expect(ext.foo.bop).toBe(tar.foo.bop); + } + ); + + it( + "extendDeep ALSO includes by reference typed arrays from source", + function() { + var src = { + foo: { + bar: new Int32Array([1, 2, 3]), + baz: new Float32Array([5, 4, 3]) + } + }; + var tar = { + foo: { + bar: new Int16Array([4, 5, 6]), + bop: new Float64Array([8, 2, 1]) + } + }; + var ext = extendDeep(tar, src); - expect(ext.foo).not.toBe(src.foo); - expect(ext.foo).toBe(tar.foo); + expect(ext).not.toBe(src); + expect(ext).toBe(tar); - expect(ext.foo.bar).toBe(src.foo.bar); - expect(ext.foo.baz).toBe(src.foo.baz); - expect(ext.foo.bop).toBe(tar.foo.bop); - }); + expect(ext.foo).not.toBe(src.foo); + expect(ext.foo).toBe(tar.foo); + expect(ext.foo.bar).toBe(src.foo.bar); + expect(ext.foo.baz).toBe(src.foo.baz); + expect(ext.foo.bop).toBe(tar.foo.bop); + } + ); }); diff --git a/test/jasmine/tests/finance_test.js b/test/jasmine/tests/finance_test.js index ef217bb9c0e..edd6e0ae60c 100644 --- a/test/jasmine/tests/finance_test.js +++ b/test/jasmine/tests/finance_test.js @@ -1,1096 +1,1461 @@ -var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); +var Plotly = require("@lib/index"); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); var mock0 = { - open: [33.01, 33.31, 33.50, 32.06, 34.12, 33.05, 33.31, 33.50], - high: [34.20, 34.37, 33.62, 34.25, 35.18, 33.25, 35.37, 34.62], - low: [31.70, 30.75, 32.87, 31.62, 30.81, 32.75, 32.75, 32.87], - close: [34.10, 31.93, 33.37, 33.18, 31.18, 33.10, 32.93, 33.70] + open: [33.01, 33.31, 33.50, 32.06, 34.12, 33.05, 33.31, 33.50], + high: [34.20, 34.37, 33.62, 34.25, 35.18, 33.25, 35.37, 34.62], + low: [31.70, 30.75, 32.87, 31.62, 30.81, 32.75, 32.75, 32.87], + close: [34.10, 31.93, 33.37, 33.18, 31.18, 33.10, 32.93, 33.70] }; var mock1 = Lib.extendDeep({}, mock0, { - x: [ - '2016-09-01', '2016-09-02', '2016-09-03', '2016-09-04', - '2016-09-05', '2016-09-06', '2016-09-07', '2016-09-10' - ] + x: [ + "2016-09-01", + "2016-09-02", + "2016-09-03", + "2016-09-04", + "2016-09-05", + "2016-09-06", + "2016-09-07", + "2016-09-10" + ] }); -describe('finance charts defaults:', function() { - 'use strict'; +describe("finance charts defaults:", function() { + "use strict"; + function _supply(data, layout) { + var gd = { data: data, layout: layout }; - function _supply(data, layout) { - var gd = { - data: data, - layout: layout - }; + Plots.supplyDefaults(gd); - Plots.supplyDefaults(gd); + return gd; + } - return gd; - } + it("should generated the correct number of full traces", function() { + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc" }); - it('should generated the correct number of full traces', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc' - }); + var trace1 = Lib.extendDeep({}, mock1, { type: "candlestick" }); - var trace1 = Lib.extendDeep({}, mock1, { - type: 'candlestick' - }); + var out = _supply([trace0, trace1]); - var out = _supply([trace0, trace1]); + expect(out.data.length).toEqual(2); + expect(out._fullData.length).toEqual(4); - expect(out.data.length).toEqual(2); - expect(out._fullData.length).toEqual(4); - - var directions = out._fullData.map(function(fullTrace) { - return fullTrace.transforms[0].direction; - }); - - expect(directions).toEqual(['increasing', 'decreasing', 'increasing', 'decreasing']); + var directions = out._fullData.map(function(fullTrace) { + return fullTrace.transforms[0].direction; }); - it('should not mutate user data', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc' - }); - - var trace1 = Lib.extendDeep({}, mock1, { - type: 'candlestick' - }); - - var out = _supply([trace0, trace1]); - expect(out.data[0]).toBe(trace0); - expect(out.data[0].transforms).toBeUndefined(); - expect(out.data[1]).toBe(trace1); - expect(out.data[1].transforms).toBeUndefined(); - - // ... and in an idempotent way - - var out2 = _supply(out.data); - expect(out2.data[0]).toBe(trace0); - expect(out2.data[0].transforms).toBeUndefined(); - expect(out2.data[1]).toBe(trace1); - expect(out2.data[1].transforms).toBeUndefined(); + expect(directions).toEqual([ + "increasing", + "decreasing", + "increasing", + "decreasing" + ]); + }); + + it("should not mutate user data", function() { + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc" }); + + var trace1 = Lib.extendDeep({}, mock1, { type: "candlestick" }); + + var out = _supply([trace0, trace1]); + expect(out.data[0]).toBe(trace0); + expect(out.data[0].transforms).toBeUndefined(); + expect(out.data[1]).toBe(trace1); + expect(out.data[1].transforms).toBeUndefined(); + + // ... and in an idempotent way + var out2 = _supply(out.data); + expect(out2.data[0]).toBe(trace0); + expect(out2.data[0].transforms).toBeUndefined(); + expect(out2.data[1]).toBe(trace1); + expect(out2.data[1].transforms).toBeUndefined(); + }); + + it("should work with transforms", function() { + var trace0 = Lib.extendDeep({}, mock1, { + type: "ohlc", + transforms: [{ type: "filter" }] }); - it('should work with transforms', function() { - var trace0 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - transforms: [{ - type: 'filter' - }] - }); - - var trace1 = Lib.extendDeep({}, mock0, { - type: 'candlestick', - transforms: [{ - type: 'filter' - }] - }); - - var out = _supply([trace0, trace1]); - - expect(out.data.length).toEqual(2); - expect(out._fullData.length).toEqual(4); - - var transformTypesIn = out.data.map(function(trace) { - return trace.transforms.map(function(opts) { - return opts.type; - }); - }); - - expect(transformTypesIn).toEqual([ ['filter'], ['filter'] ]); - - var transformTypesOut = out._fullData.map(function(fullTrace) { - return fullTrace.transforms.map(function(opts) { - return opts.type; - }); - }); - - // dummy 'ohlc' and 'candlestick' transforms are pushed at the end - // of the 'transforms' array container - - expect(transformTypesOut).toEqual([ - ['filter', 'ohlc'], ['filter', 'ohlc'], - ['filter', 'candlestick'], ['filter', 'candlestick'] - ]); + var trace1 = Lib.extendDeep({}, mock0, { + type: "candlestick", + transforms: [{ type: "filter" }] }); - it('should slice data array according to minimum supplied length', function() { + var out = _supply([trace0, trace1]); - function assertDataLength(fullTrace, len) { - expect(fullTrace.visible).toBe(true); - - expect(fullTrace.open.length).toEqual(len); - expect(fullTrace.high.length).toEqual(len); - expect(fullTrace.low.length).toEqual(len); - expect(fullTrace.close.length).toEqual(len); - } + expect(out.data.length).toEqual(2); + expect(out._fullData.length).toEqual(4); - var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); - trace0.open = [33.01, 33.31, 33.50, 32.06, 34.12]; - - var trace1 = Lib.extendDeep({}, mock1, { type: 'candlestick' }); - trace1.x = ['2016-09-01', '2016-09-02', '2016-09-03', '2016-09-04']; - - var out = _supply([trace0, trace1]); - - assertDataLength(out._fullData[0], 5); - assertDataLength(out._fullData[1], 5); - assertDataLength(out._fullData[2], 4); - assertDataLength(out._fullData[3], 4); - - expect(out._fullData[0]._fullInput.x).toBeUndefined(); - expect(out._fullData[1]._fullInput.x).toBeUndefined(); - expect(out._fullData[2]._fullInput.x.length).toEqual(4); - expect(out._fullData[3]._fullInput.x.length).toEqual(4); + var transformTypesIn = out.data.map(function(trace) { + return trace.transforms.map(function(opts) { + return opts.type; + }); }); - it('should set visible to *false* when minimum supplied length is 0', function() { - var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); - trace0.close = undefined; + expect(transformTypesIn).toEqual([["filter"], ["filter"]]); - var trace1 = Lib.extendDeep({}, mock1, { type: 'candlestick' }); - trace1.high = null; - - var out = _supply([trace0, trace1]); - - expect(out.data.length).toEqual(2); - expect(out._fullData.length).toEqual(4); - - var visibilities = out._fullData.map(function(fullTrace) { - return fullTrace.visible; - }); - - expect(visibilities).toEqual([false, false, false, false]); + var transformTypesOut = out._fullData.map(function(fullTrace) { + return fullTrace.transforms.map(function(opts) { + return opts.type; + }); }); - it('direction *showlegend* should be inherited from trace-wide *showlegend*', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - showlegend: false, - }); + // dummy 'ohlc' and 'candlestick' transforms are pushed at the end + // of the 'transforms' array container + expect(transformTypesOut).toEqual([ + ["filter", "ohlc"], + ["filter", "ohlc"], + ["filter", "candlestick"], + ["filter", "candlestick"] + ]); + }); + + it( + "should slice data array according to minimum supplied length", + function() { + function assertDataLength(fullTrace, len) { + expect(fullTrace.visible).toBe(true); + + expect(fullTrace.open.length).toEqual(len); + expect(fullTrace.high.length).toEqual(len); + expect(fullTrace.low.length).toEqual(len); + expect(fullTrace.close.length).toEqual(len); + } + + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc" }); + trace0.open = [33.01, 33.31, 33.50, 32.06, 34.12]; + + var trace1 = Lib.extendDeep({}, mock1, { type: "candlestick" }); + trace1.x = ["2016-09-01", "2016-09-02", "2016-09-03", "2016-09-04"]; + + var out = _supply([trace0, trace1]); + + assertDataLength(out._fullData[0], 5); + assertDataLength(out._fullData[1], 5); + assertDataLength(out._fullData[2], 4); + assertDataLength(out._fullData[3], 4); + + expect(out._fullData[0]._fullInput.x).toBeUndefined(); + expect(out._fullData[1]._fullInput.x).toBeUndefined(); + expect(out._fullData[2]._fullInput.x.length).toEqual(4); + expect(out._fullData[3]._fullInput.x.length).toEqual(4); + } + ); - var trace1 = Lib.extendDeep({}, mock1, { - type: 'candlestick', - showlegend: false, - increasing: { showlegend: true }, - decreasing: { showlegend: true } - }); + it( + "should set visible to *false* when minimum supplied length is 0", + function() { + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc" }); + trace0.close = undefined; - var out = _supply([trace0, trace1]); + var trace1 = Lib.extendDeep({}, mock1, { type: "candlestick" }); + trace1.high = null; - var visibilities = out._fullData.map(function(fullTrace) { - return fullTrace.showlegend; - }); + var out = _supply([trace0, trace1]); - expect(visibilities).toEqual([false, false, false, false]); - }); - - it('direction *name* should be inherited from trace-wide *name*', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - name: 'Company A' - }); + expect(out.data.length).toEqual(2); + expect(out._fullData.length).toEqual(4); - var trace1 = Lib.extendDeep({}, mock1, { - type: 'candlestick', - name: 'Company B', - increasing: { name: 'B - UP' }, - decreasing: { name: 'B - DOWN' } - }); + var visibilities = out._fullData.map(function(fullTrace) { + return fullTrace.visible; + }); - var out = _supply([trace0, trace1]); + expect(visibilities).toEqual([false, false, false, false]); + } + ); + + it( + "direction *showlegend* should be inherited from trace-wide *showlegend*", + function() { + var trace0 = Lib.extendDeep({}, mock0, { + type: "ohlc", + showlegend: false + }); + + var trace1 = Lib.extendDeep({}, mock1, { + type: "candlestick", + showlegend: false, + increasing: { showlegend: true }, + decreasing: { showlegend: true } + }); + + var out = _supply([trace0, trace1]); + + var visibilities = out._fullData.map(function(fullTrace) { + return fullTrace.showlegend; + }); + + expect(visibilities).toEqual([false, false, false, false]); + } + ); - var names = out._fullData.map(function(fullTrace) { - return fullTrace.name; - }); + it("direction *name* should be inherited from trace-wide *name*", function() { + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc", name: "Company A" }); - expect(names).toEqual([ - 'Company A - increasing', - 'Company A - decreasing', - 'B - UP', - 'B - DOWN' - ]); + var trace1 = Lib.extendDeep({}, mock1, { + type: "candlestick", + name: "Company B", + increasing: { name: "B - UP" }, + decreasing: { name: "B - DOWN" } }); - it('trace *name* default should make reference to user data trace indices', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc' - }); - - var trace1 = { type: 'scatter' }; - - var trace2 = Lib.extendDeep({}, mock1, { - type: 'candlestick', - }); - - var trace3 = { type: 'bar' }; - - var out = _supply([trace0, trace1, trace2, trace3]); + var out = _supply([trace0, trace1]); - var names = out._fullData.map(function(fullTrace) { - return fullTrace.name; - }); - - expect(names).toEqual([ - 'trace 0 - increasing', - 'trace 0 - decreasing', - 'trace 1', - 'trace 2 - increasing', - 'trace 2 - decreasing', - 'trace 3' - ]); + var names = out._fullData.map(function(fullTrace) { + return fullTrace.name; }); - it('trace-wide styling should set default for corresponding per-direction styling', function() { - function assertLine(cont, width, dash) { - expect(cont.line.width).toEqual(width); - if(dash) expect(cont.line.dash).toEqual(dash); - } - - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - line: { width: 1, dash: 'dash' }, - decreasing: { line: { dash: 'dot' } } - }); + expect(names).toEqual([ + "Company A - increasing", + "Company A - decreasing", + "B - UP", + "B - DOWN" + ]); + }); - var trace1 = Lib.extendDeep({}, mock1, { - type: 'candlestick', - line: { width: 3 }, - increasing: { line: { width: 0 } } - }); + it( + "trace *name* default should make reference to user data trace indices", + function() { + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc" }); - var out = _supply([trace0, trace1]); + var trace1 = { type: "scatter" }; + var trace2 = Lib.extendDeep({}, mock1, { type: "candlestick" }); - var fullData = out._fullData; - var fullInput = fullData.map(function(fullTrace) { return fullTrace._fullInput; }); + var trace3 = { type: "bar" }; - assertLine(fullInput[0].increasing, 1, 'dash'); - assertLine(fullInput[0].decreasing, 1, 'dot'); - assertLine(fullInput[2].increasing, 0); - assertLine(fullInput[2].decreasing, 3); + var out = _supply([trace0, trace1, trace2, trace3]); - assertLine(fullData[0], 1, 'dash'); - assertLine(fullData[1], 1, 'dot'); - assertLine(fullData[2], 0); - assertLine(fullData[3], 3); - }); - - it('trace-wide *visible* should be passed to generated traces', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - visible: 'legendonly' - }); - - var trace1 = Lib.extendDeep({}, mock1, { - type: 'candlestick', - visible: false - }); - - var out = _supply([trace0, trace1]); - - var visibilities = out._fullData.map(function(fullTrace) { - return fullTrace.visible; - }); + var names = out._fullData.map(function(fullTrace) { + return fullTrace.name; + }); - // only three items here as visible: false traces are not transformed + expect(names).toEqual([ + "trace 0 - increasing", + "trace 0 - decreasing", + "trace 1", + "trace 2 - increasing", + "trace 2 - decreasing", + "trace 3" + ]); + } + ); + + it( + "trace-wide styling should set default for corresponding per-direction styling", + function() { + function assertLine(cont, width, dash) { + expect(cont.line.width).toEqual(width); + if (dash) expect(cont.line.dash).toEqual(dash); + } + + var trace0 = Lib.extendDeep({}, mock0, { + type: "ohlc", + line: { width: 1, dash: "dash" }, + decreasing: { line: { dash: "dot" } } + }); + + var trace1 = Lib.extendDeep({}, mock1, { + type: "candlestick", + line: { width: 3 }, + increasing: { line: { width: 0 } } + }); + + var out = _supply([trace0, trace1]); + + var fullData = out._fullData; + var fullInput = fullData.map(function(fullTrace) { + return fullTrace._fullInput; + }); + + assertLine(fullInput[0].increasing, 1, "dash"); + assertLine(fullInput[0].decreasing, 1, "dot"); + assertLine(fullInput[2].increasing, 0); + assertLine(fullInput[2].decreasing, 3); + + assertLine(fullData[0], 1, "dash"); + assertLine(fullData[1], 1, "dot"); + assertLine(fullData[2], 0); + assertLine(fullData[3], 3); + } + ); - expect(visibilities).toEqual(['legendonly', 'legendonly', false]); + it("trace-wide *visible* should be passed to generated traces", function() { + var trace0 = Lib.extendDeep({}, mock0, { + type: "ohlc", + visible: "legendonly" }); - it('should add a few layout settings by default', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc' - }); - - var layout0 = {}; - - var out0 = _supply([trace0], layout0); - - expect(out0.layout.xaxis.rangeslider).toBeDefined(); - expect(out0._fullLayout.xaxis.rangeslider.visible).toBe(true); - - var trace1 = Lib.extendDeep({}, mock0, { - type: 'candlestick' - }); - - var layout1 = { - xaxis: { rangeslider: { visible: false }} - }; - - var out1 = _supply([trace1], layout1); - - expect(out1.layout.xaxis.rangeslider).toBeDefined(); - expect(out1._fullLayout.xaxis.rangeslider.visible).toBe(false); + var trace1 = Lib.extendDeep({}, mock1, { + type: "candlestick", + visible: false }); - it('pushes layout.calendar to all output traces', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc' - }); - - var trace1 = Lib.extendDeep({}, mock1, { - type: 'candlestick' - }); - - var out = _supply([trace0, trace1], {calendar: 'nanakshahi'}); + var out = _supply([trace0, trace1]); - - out._fullData.forEach(function(fullTrace) { - expect(fullTrace.xcalendar).toBe('nanakshahi'); - }); + var visibilities = out._fullData.map(function(fullTrace) { + return fullTrace.visible; }); - it('accepts a calendar per input trace', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - xcalendar: 'hebrew' - }); + // only three items here as visible: false traces are not transformed + expect(visibilities).toEqual(["legendonly", "legendonly", false]); + }); - var trace1 = Lib.extendDeep({}, mock1, { - type: 'candlestick', - xcalendar: 'julian' - }); + it("should add a few layout settings by default", function() { + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc" }); - var out = _supply([trace0, trace1], {calendar: 'nanakshahi'}); + var layout0 = {}; + var out0 = _supply([trace0], layout0); - out._fullData.forEach(function(fullTrace, i) { - expect(fullTrace.xcalendar).toBe(i < 2 ? 'hebrew' : 'julian'); - }); - }); + expect(out0.layout.xaxis.rangeslider).toBeDefined(); + expect(out0._fullLayout.xaxis.rangeslider.visible).toBe(true); - it('should make empty candlestick traces autotype to *linear* (as opposed to real box traces)', function() { - var trace0 = { type: 'candlestick' }; - var out = _supply([trace0], { xaxis: {} }); + var trace1 = Lib.extendDeep({}, mock0, { type: "candlestick" }); - expect(out._fullLayout.xaxis.type).toEqual('linear'); - }); -}); + var layout1 = { xaxis: { rangeslider: { visible: false } } }; -describe('finance charts calc transforms:', function() { - 'use strict'; + var out1 = _supply([trace1], layout1); - function calcDatatoTrace(calcTrace) { - return calcTrace[0].trace; - } + expect(out1.layout.xaxis.rangeslider).toBeDefined(); + expect(out1._fullLayout.xaxis.rangeslider.visible).toBe(false); + }); - function _calc(data, layout) { - var gd = { - data: data, - layout: layout || {} - }; + it("pushes layout.calendar to all output traces", function() { + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc" }); - Plots.supplyDefaults(gd); - Plots.doCalcdata(gd); + var trace1 = Lib.extendDeep({}, mock1, { type: "candlestick" }); - return gd.calcdata.map(calcDatatoTrace); - } + var out = _supply([trace0, trace1], { calendar: "nanakshahi" }); - it('should fill when *x* is not present', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - }); - - var trace1 = Lib.extendDeep({}, mock0, { - type: 'candlestick', - }); - - var out = _calc([trace0, trace1]); - - expect(out[0].x).toEqual([ - -0.3, 0, 0, 0, 0, 0.3, null, - 2.7, 3, 3, 3, 3, 3.3, null, - 4.7, 5, 5, 5, 5, 5.3, null, - 6.7, 7, 7, 7, 7, 7.3, null - ]); - expect(out[1].x).toEqual([ - 0.7, 1, 1, 1, 1, 1.3, null, - 1.7, 2, 2, 2, 2, 2.3, null, - 3.7, 4, 4, 4, 4, 4.3, null, - 5.7, 6, 6, 6, 6, 6.3, null - ]); - expect(out[2].x).toEqual([ - 0, 0, 0, 0, 0, 0, - 3, 3, 3, 3, 3, 3, - 5, 5, 5, 5, 5, 5, - 7, 7, 7, 7, 7, 7 - ]); - expect(out[3].x).toEqual([ - 1, 1, 1, 1, 1, 1, - 2, 2, 2, 2, 2, 2, - 4, 4, 4, 4, 4, 4, - 6, 6, 6, 6, 6, 6 - ]); + out._fullData.forEach(function(fullTrace) { + expect(fullTrace.xcalendar).toBe("nanakshahi"); }); + }); - it('should fill *text* for OHLC hover labels', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - text: ['A', 'B', 'C', 'D'] - }); - - var trace1 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - text: 'IMPORTANT', - hoverinfo: 'x+text', - xaxis: 'x2' - }); - - var trace2 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - hoverinfo: 'y', - xaxis: 'x2' - }); - - var trace3 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - hoverinfo: 'x', - }); - - var out = _calc([trace0, trace1, trace2, trace3]); - - expect(out[0].hoverinfo).toEqual('x+text+name'); - expect(out[0].text[0]) - .toEqual('Open: 33.01
High: 34.2
Low: 31.7
Close: 34.1
A'); - expect(out[0].hoverinfo).toEqual('x+text+name'); - expect(out[1].text[0]) - .toEqual('Open: 33.31
High: 34.37
Low: 30.75
Close: 31.93
B'); - - expect(out[2].hoverinfo).toEqual('x+text'); - expect(out[2].text[0]).toEqual('IMPORTANT'); - - expect(out[3].hoverinfo).toEqual('x+text'); - expect(out[3].text[0]).toEqual('IMPORTANT'); - - expect(out[4].hoverinfo).toEqual('text'); - expect(out[4].text[0]) - .toEqual('Open: 33.01
High: 34.2
Low: 31.7
Close: 34.1'); - expect(out[5].hoverinfo).toEqual('text'); - expect(out[5].text[0]) - .toEqual('Open: 33.31
High: 34.37
Low: 30.75
Close: 31.93'); - - expect(out[6].hoverinfo).toEqual('x'); - expect(out[6].text[0]).toEqual(''); - expect(out[7].hoverinfo).toEqual('x'); - expect(out[7].text[0]).toEqual(''); + it("accepts a calendar per input trace", function() { + var trace0 = Lib.extendDeep({}, mock0, { + type: "ohlc", + xcalendar: "hebrew" }); - it('should work with *filter* transforms', function() { - var trace0 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - tickwidth: 0.05, - transforms: [{ - type: 'filter', - operation: '>', - target: 'open', - value: 33 - }] - }); - - var trace1 = Lib.extendDeep({}, mock1, { - type: 'candlestick', - transforms: [{ - type: 'filter', - operation: '{}', - target: 'x', - value: ['2016-09-01', '2016-09-10'] - }] - }); - - var out = _calc([trace0, trace1]); - - expect(out.length).toEqual(4); - - expect(out[0].x).toEqual([ - '2016-08-31 22:48', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01 01:12', null, - '2016-09-05 22:48', '2016-09-06', '2016-09-06', '2016-09-06', '2016-09-06', '2016-09-06 01:12', null, - '2016-09-09 22:48', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10 01:12', null - ]); - expect(out[0].y).toEqual([ - 33.01, 33.01, 34.2, 31.7, 34.1, 34.1, null, - 33.05, 33.05, 33.25, 32.75, 33.1, 33.1, null, - 33.5, 33.5, 34.62, 32.87, 33.7, 33.7, null - ]); - expect(out[1].x).toEqual([ - '2016-09-01 22:48', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02 01:12', null, - '2016-09-02 22:48', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03 01:12', null, - '2016-09-04 22:48', '2016-09-05', '2016-09-05', '2016-09-05', '2016-09-05', '2016-09-05 01:12', null, - '2016-09-06 22:48', '2016-09-07', '2016-09-07', '2016-09-07', '2016-09-07', '2016-09-07 01:12', null - ]); - expect(out[1].y).toEqual([ - 33.31, 33.31, 34.37, 30.75, 31.93, 31.93, null, - 33.5, 33.5, 33.62, 32.87, 33.37, 33.37, null, - 34.12, 34.12, 35.18, 30.81, 31.18, 31.18, null, - 33.31, 33.31, 35.37, 32.75, 32.93, 32.93, null - ]); - - expect(out[2].x).toEqual([ - '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', - '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10' - ]); - expect(out[2].y).toEqual([ - 31.7, 33.01, 34.1, 34.1, 34.1, 34.2, - 32.87, 33.5, 33.7, 33.7, 33.7, 34.62 - ]); - - expect(out[3].x).toEqual([]); - expect(out[3].y).toEqual([]); + var trace1 = Lib.extendDeep({}, mock1, { + type: "candlestick", + xcalendar: "julian" }); - it('should work with *groupby* transforms (ohlc)', function() { - var opts = { - type: 'groupby', - groups: ['b', 'b', 'b', 'a'], - }; - - var trace0 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - tickwidth: 0.05, - transforms: [opts] - }); - - var out = _calc([trace0]); + var out = _supply([trace0, trace1], { calendar: "nanakshahi" }); - expect(out[0].name).toEqual('trace 0 - increasing'); - expect(out[0].x).toEqual([ - '2016-08-31 22:48', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01 01:12', null - ]); - expect(out[0].y).toEqual([ - 33.01, 33.01, 34.2, 31.7, 34.1, 34.1, null, - ]); - - expect(out[1].name).toEqual('trace 0 - decreasing'); - expect(out[1].x).toEqual([ - '2016-09-01 22:48', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02 01:12', null, - '2016-09-02 22:48', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03 01:12', null - ]); - expect(out[1].y).toEqual([ - 33.31, 33.31, 34.37, 30.75, 31.93, 31.93, null, - 33.5, 33.5, 33.62, 32.87, 33.37, 33.37, null - ]); - - expect(out[2].name).toEqual('trace 0 - increasing'); - expect(out[2].x).toEqual([ - '2016-09-03 22:48', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04 01:12', null - ]); - expect(out[2].y).toEqual([ - 32.06, 32.06, 34.25, 31.62, 33.18, 33.18, null - ]); - - expect(out[3].name).toEqual('trace 0 - decreasing'); - expect(out[3].x).toEqual([]); - expect(out[3].y).toEqual([]); + out._fullData.forEach(function(fullTrace, i) { + expect(fullTrace.xcalendar).toBe(i < 2 ? "hebrew" : "julian"); }); + }); - it('should work with *groupby* transforms (candlestick)', function() { - var opts = { - type: 'groupby', - groups: ['a', 'b', 'b', 'a'], - }; + it( + "should make empty candlestick traces autotype to *linear* (as opposed to real box traces)", + function() { + var trace0 = { type: "candlestick" }; + var out = _supply([trace0], { xaxis: {} }); - var trace0 = Lib.extendDeep({}, mock1, { - type: 'candlestick', - transforms: [opts] - }); - - var out = _calc([trace0]); - - expect(out[0].name).toEqual('trace 0 - increasing'); - expect(out[0].x).toEqual([ - '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', - '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04' - ]); - expect(out[0].y).toEqual([ - 31.7, 33.01, 34.1, 34.1, 34.1, 34.2, - 31.62, 32.06, 33.18, 33.18, 33.18, 34.25 - ]); - - expect(out[1].name).toEqual('trace 0 - decreasing'); - expect(out[1].x).toEqual([]); - expect(out[1].y).toEqual([]); - - expect(out[2].name).toEqual('trace 0 - increasing'); - expect(out[2].x).toEqual([]); - expect(out[2].y).toEqual([]); + expect(out._fullLayout.xaxis.type).toEqual("linear"); + } + ); +}); - expect(out[3].name).toEqual('trace 0 - decreasing'); - expect(out[3].x).toEqual([ - '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', - '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03' - ]); - expect(out[3].y).toEqual([ - 30.75, 33.31, 31.93, 31.93, 31.93, 34.37, - 32.87, 33.5, 33.37, 33.37, 33.37, 33.62 - ]); +describe("finance charts calc transforms:", function() { + "use strict"; + function calcDatatoTrace(calcTrace) { + return calcTrace[0].trace; + } + + function _calc(data, layout) { + var gd = { data: data, layout: layout || {} }; + + Plots.supplyDefaults(gd); + Plots.doCalcdata(gd); + + return gd.calcdata.map(calcDatatoTrace); + } + + it("should fill when *x* is not present", function() { + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc" }); + + var trace1 = Lib.extendDeep({}, mock0, { type: "candlestick" }); + + var out = _calc([trace0, trace1]); + + expect(out[0].x).toEqual([ + -0.3, + 0, + 0, + 0, + 0, + 0.3, + null, + 2.7, + 3, + 3, + 3, + 3, + 3.3, + null, + 4.7, + 5, + 5, + 5, + 5, + 5.3, + null, + 6.7, + 7, + 7, + 7, + 7, + 7.3, + null + ]); + expect(out[1].x).toEqual([ + 0.7, + 1, + 1, + 1, + 1, + 1.3, + null, + 1.7, + 2, + 2, + 2, + 2, + 2.3, + null, + 3.7, + 4, + 4, + 4, + 4, + 4.3, + null, + 5.7, + 6, + 6, + 6, + 6, + 6.3, + null + ]); + expect(out[2].x).toEqual([ + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 3, + 3, + 3, + 3, + 3, + 5, + 5, + 5, + 5, + 5, + 5, + 7, + 7, + 7, + 7, + 7, + 7 + ]); + expect(out[3].x).toEqual([ + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 4, + 4, + 6, + 6, + 6, + 6, + 6, + 6 + ]); + }); + + it("should fill *text* for OHLC hover labels", function() { + var trace0 = Lib.extendDeep({}, mock0, { + type: "ohlc", + text: ["A", "B", "C", "D"] }); - it('should use the smallest trace minimum x difference to convert *tickwidth* to data coords for all traces attached to a given x-axis', function() { - var trace0 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - tickwidth: 0.5 - }); - - var trace1 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - tickwidth: 0.5 - }); - - // shift time coordinates by 10 hours - trace1.x = trace1.x.map(function(d) { - return d + ' 10:00'; - }); - - var out = _calc([trace0, trace1]); - - expect(out[0].x).toEqual([ - '2016-08-31 12:00', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01 12:00', null, - '2016-09-03 12:00', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04 12:00', null, - '2016-09-05 12:00', '2016-09-06', '2016-09-06', '2016-09-06', '2016-09-06', '2016-09-06 12:00', null, - '2016-09-09 12:00', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10 12:00', null - ]); - - expect(out[1].x).toEqual([ - '2016-09-01 12:00', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02 12:00', null, - '2016-09-02 12:00', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03 12:00', null, - '2016-09-04 12:00', '2016-09-05', '2016-09-05', '2016-09-05', '2016-09-05', '2016-09-05 12:00', null, - '2016-09-06 12:00', '2016-09-07', '2016-09-07', '2016-09-07', '2016-09-07', '2016-09-07 12:00', null - ]); - - expect(out[2].x).toEqual([ - '2016-08-31 22:00', '2016-09-01 10:00', '2016-09-01 10:00', '2016-09-01 10:00', '2016-09-01 10:00', '2016-09-01 22:00', null, - '2016-09-03 22:00', '2016-09-04 10:00', '2016-09-04 10:00', '2016-09-04 10:00', '2016-09-04 10:00', '2016-09-04 22:00', null, - '2016-09-05 22:00', '2016-09-06 10:00', '2016-09-06 10:00', '2016-09-06 10:00', '2016-09-06 10:00', '2016-09-06 22:00', null, - '2016-09-09 22:00', '2016-09-10 10:00', '2016-09-10 10:00', '2016-09-10 10:00', '2016-09-10 10:00', '2016-09-10 22:00', null - ]); - - expect(out[3].x).toEqual([ - '2016-09-01 22:00', '2016-09-02 10:00', '2016-09-02 10:00', '2016-09-02 10:00', '2016-09-02 10:00', '2016-09-02 22:00', null, - '2016-09-02 22:00', '2016-09-03 10:00', '2016-09-03 10:00', '2016-09-03 10:00', '2016-09-03 10:00', '2016-09-03 22:00', null, - '2016-09-04 22:00', '2016-09-05 10:00', '2016-09-05 10:00', '2016-09-05 10:00', '2016-09-05 10:00', '2016-09-05 22:00', null, - '2016-09-06 22:00', '2016-09-07 10:00', '2016-09-07 10:00', '2016-09-07 10:00', '2016-09-07 10:00', '2016-09-07 22:00', null - ]); + var trace1 = Lib.extendDeep({}, mock1, { + type: "ohlc", + text: "IMPORTANT", + hoverinfo: "x+text", + xaxis: "x2" }); - it('should fallback to a minimum x difference of 0.5 in one-item traces', function() { - var trace0 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - tickwidth: 0.5 - }); - trace0.x = [ '2016-01-01' ]; - - var trace1 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - tickwidth: 0.5 - }); - trace1.x = [ 10 ]; - - var out = _calc([trace0, trace1]); - - var x0 = Lib.simpleMap(out[0].x, Lib.dateTime2ms); - expect(x0[x0.length - 2] - x0[0]).toEqual(1); - - var x2 = Lib.simpleMap(out[2].x, Lib.dateTime2ms); - expect(x2[x2.length - 2] - x2[0]).toEqual(1); - - expect(out[1].x).toEqual([]); - expect(out[3].x).toEqual([]); + var trace2 = Lib.extendDeep({}, mock1, { + type: "ohlc", + hoverinfo: "y", + xaxis: "x2" }); -}); - -describe('finance charts updates:', function() { - 'use strict'; - var gd; - - beforeEach(function() { - gd = createGraphDiv(); + var trace3 = Lib.extendDeep({}, mock0, { type: "ohlc", hoverinfo: "x" }); + + var out = _calc([trace0, trace1, trace2, trace3]); + + expect(out[0].hoverinfo).toEqual("x+text+name"); + expect(out[0].text[0]).toEqual( + "Open: 33.01
High: 34.2
Low: 31.7
Close: 34.1
A" + ); + expect(out[0].hoverinfo).toEqual("x+text+name"); + expect(out[1].text[0]).toEqual( + "Open: 33.31
High: 34.37
Low: 30.75
Close: 31.93
B" + ); + + expect(out[2].hoverinfo).toEqual("x+text"); + expect(out[2].text[0]).toEqual("IMPORTANT"); + + expect(out[3].hoverinfo).toEqual("x+text"); + expect(out[3].text[0]).toEqual("IMPORTANT"); + + expect(out[4].hoverinfo).toEqual("text"); + expect(out[4].text[0]).toEqual( + "Open: 33.01
High: 34.2
Low: 31.7
Close: 34.1" + ); + expect(out[5].hoverinfo).toEqual("text"); + expect(out[5].text[0]).toEqual( + "Open: 33.31
High: 34.37
Low: 30.75
Close: 31.93" + ); + + expect(out[6].hoverinfo).toEqual("x"); + expect(out[6].text[0]).toEqual(""); + expect(out[7].hoverinfo).toEqual("x"); + expect(out[7].text[0]).toEqual(""); + }); + + it("should work with *filter* transforms", function() { + var trace0 = Lib.extendDeep({}, mock1, { + type: "ohlc", + tickwidth: 0.05, + transforms: [ + { type: "filter", operation: ">", target: "open", value: 33 } + ] }); - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); + var trace1 = Lib.extendDeep({}, mock1, { + type: "candlestick", + transforms: [ + { + type: "filter", + operation: "{}", + target: "x", + value: ["2016-09-01", "2016-09-10"] + } + ] }); - function countScatterTraces() { - return d3.select('g.cartesianlayer').selectAll('g.trace.scatter').size(); - } + var out = _calc([trace0, trace1]); + + expect(out.length).toEqual(4); + + expect(out[0].x).toEqual([ + "2016-08-31 22:48", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01 01:12", + null, + "2016-09-05 22:48", + "2016-09-06", + "2016-09-06", + "2016-09-06", + "2016-09-06", + "2016-09-06 01:12", + null, + "2016-09-09 22:48", + "2016-09-10", + "2016-09-10", + "2016-09-10", + "2016-09-10", + "2016-09-10 01:12", + null + ]); + expect(out[0].y).toEqual([ + 33.01, + 33.01, + 34.2, + 31.7, + 34.1, + 34.1, + null, + 33.05, + 33.05, + 33.25, + 32.75, + 33.1, + 33.1, + null, + 33.5, + 33.5, + 34.62, + 32.87, + 33.7, + 33.7, + null + ]); + expect(out[1].x).toEqual([ + "2016-09-01 22:48", + "2016-09-02", + "2016-09-02", + "2016-09-02", + "2016-09-02", + "2016-09-02 01:12", + null, + "2016-09-02 22:48", + "2016-09-03", + "2016-09-03", + "2016-09-03", + "2016-09-03", + "2016-09-03 01:12", + null, + "2016-09-04 22:48", + "2016-09-05", + "2016-09-05", + "2016-09-05", + "2016-09-05", + "2016-09-05 01:12", + null, + "2016-09-06 22:48", + "2016-09-07", + "2016-09-07", + "2016-09-07", + "2016-09-07", + "2016-09-07 01:12", + null + ]); + expect(out[1].y).toEqual([ + 33.31, + 33.31, + 34.37, + 30.75, + 31.93, + 31.93, + null, + 33.5, + 33.5, + 33.62, + 32.87, + 33.37, + 33.37, + null, + 34.12, + 34.12, + 35.18, + 30.81, + 31.18, + 31.18, + null, + 33.31, + 33.31, + 35.37, + 32.75, + 32.93, + 32.93, + null + ]); + + expect(out[2].x).toEqual([ + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-10", + "2016-09-10", + "2016-09-10", + "2016-09-10", + "2016-09-10", + "2016-09-10" + ]); + expect(out[2].y).toEqual([ + 31.7, + 33.01, + 34.1, + 34.1, + 34.1, + 34.2, + 32.87, + 33.5, + 33.7, + 33.7, + 33.7, + 34.62 + ]); + + expect(out[3].x).toEqual([]); + expect(out[3].y).toEqual([]); + }); + + it("should work with *groupby* transforms (ohlc)", function() { + var opts = { type: "groupby", groups: ["b", "b", "b", "a"] }; + + var trace0 = Lib.extendDeep({}, mock1, { + type: "ohlc", + tickwidth: 0.05, + transforms: [opts] + }); - function countBoxTraces() { - return d3.select('g.cartesianlayer').selectAll('g.trace.boxes').size(); - } + var out = _calc([trace0]); + + expect(out[0].name).toEqual("trace 0 - increasing"); + expect(out[0].x).toEqual([ + "2016-08-31 22:48", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01 01:12", + null + ]); + expect(out[0].y).toEqual([33.01, 33.01, 34.2, 31.7, 34.1, 34.1, null]); + + expect(out[1].name).toEqual("trace 0 - decreasing"); + expect(out[1].x).toEqual([ + "2016-09-01 22:48", + "2016-09-02", + "2016-09-02", + "2016-09-02", + "2016-09-02", + "2016-09-02 01:12", + null, + "2016-09-02 22:48", + "2016-09-03", + "2016-09-03", + "2016-09-03", + "2016-09-03", + "2016-09-03 01:12", + null + ]); + expect(out[1].y).toEqual([ + 33.31, + 33.31, + 34.37, + 30.75, + 31.93, + 31.93, + null, + 33.5, + 33.5, + 33.62, + 32.87, + 33.37, + 33.37, + null + ]); + + expect(out[2].name).toEqual("trace 0 - increasing"); + expect(out[2].x).toEqual([ + "2016-09-03 22:48", + "2016-09-04", + "2016-09-04", + "2016-09-04", + "2016-09-04", + "2016-09-04 01:12", + null + ]); + expect(out[2].y).toEqual([32.06, 32.06, 34.25, 31.62, 33.18, 33.18, null]); + + expect(out[3].name).toEqual("trace 0 - decreasing"); + expect(out[3].x).toEqual([]); + expect(out[3].y).toEqual([]); + }); + + it("should work with *groupby* transforms (candlestick)", function() { + var opts = { type: "groupby", groups: ["a", "b", "b", "a"] }; + + var trace0 = Lib.extendDeep({}, mock1, { + type: "candlestick", + transforms: [opts] + }); - function countRangeSliders() { - return d3.select('g.rangeslider-rangeplot').size(); + var out = _calc([trace0]); + + expect(out[0].name).toEqual("trace 0 - increasing"); + expect(out[0].x).toEqual([ + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-04", + "2016-09-04", + "2016-09-04", + "2016-09-04", + "2016-09-04", + "2016-09-04" + ]); + expect(out[0].y).toEqual([ + 31.7, + 33.01, + 34.1, + 34.1, + 34.1, + 34.2, + 31.62, + 32.06, + 33.18, + 33.18, + 33.18, + 34.25 + ]); + + expect(out[1].name).toEqual("trace 0 - decreasing"); + expect(out[1].x).toEqual([]); + expect(out[1].y).toEqual([]); + + expect(out[2].name).toEqual("trace 0 - increasing"); + expect(out[2].x).toEqual([]); + expect(out[2].y).toEqual([]); + + expect(out[3].name).toEqual("trace 0 - decreasing"); + expect(out[3].x).toEqual([ + "2016-09-02", + "2016-09-02", + "2016-09-02", + "2016-09-02", + "2016-09-02", + "2016-09-02", + "2016-09-03", + "2016-09-03", + "2016-09-03", + "2016-09-03", + "2016-09-03", + "2016-09-03" + ]); + expect(out[3].y).toEqual([ + 30.75, + 33.31, + 31.93, + 31.93, + 31.93, + 34.37, + 32.87, + 33.5, + 33.37, + 33.37, + 33.37, + 33.62 + ]); + }); + + it( + "should use the smallest trace minimum x difference to convert *tickwidth* to data coords for all traces attached to a given x-axis", + function() { + var trace0 = Lib.extendDeep({}, mock1, { type: "ohlc", tickwidth: 0.5 }); + + var trace1 = Lib.extendDeep({}, mock1, { type: "ohlc", tickwidth: 0.5 }); + + // shift time coordinates by 10 hours + trace1.x = trace1.x.map(function(d) { + return d + " 10:00"; + }); + + var out = _calc([trace0, trace1]); + + expect(out[0].x).toEqual([ + "2016-08-31 12:00", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01", + "2016-09-01 12:00", + null, + "2016-09-03 12:00", + "2016-09-04", + "2016-09-04", + "2016-09-04", + "2016-09-04", + "2016-09-04 12:00", + null, + "2016-09-05 12:00", + "2016-09-06", + "2016-09-06", + "2016-09-06", + "2016-09-06", + "2016-09-06 12:00", + null, + "2016-09-09 12:00", + "2016-09-10", + "2016-09-10", + "2016-09-10", + "2016-09-10", + "2016-09-10 12:00", + null + ]); + + expect(out[1].x).toEqual([ + "2016-09-01 12:00", + "2016-09-02", + "2016-09-02", + "2016-09-02", + "2016-09-02", + "2016-09-02 12:00", + null, + "2016-09-02 12:00", + "2016-09-03", + "2016-09-03", + "2016-09-03", + "2016-09-03", + "2016-09-03 12:00", + null, + "2016-09-04 12:00", + "2016-09-05", + "2016-09-05", + "2016-09-05", + "2016-09-05", + "2016-09-05 12:00", + null, + "2016-09-06 12:00", + "2016-09-07", + "2016-09-07", + "2016-09-07", + "2016-09-07", + "2016-09-07 12:00", + null + ]); + + expect(out[2].x).toEqual([ + "2016-08-31 22:00", + "2016-09-01 10:00", + "2016-09-01 10:00", + "2016-09-01 10:00", + "2016-09-01 10:00", + "2016-09-01 22:00", + null, + "2016-09-03 22:00", + "2016-09-04 10:00", + "2016-09-04 10:00", + "2016-09-04 10:00", + "2016-09-04 10:00", + "2016-09-04 22:00", + null, + "2016-09-05 22:00", + "2016-09-06 10:00", + "2016-09-06 10:00", + "2016-09-06 10:00", + "2016-09-06 10:00", + "2016-09-06 22:00", + null, + "2016-09-09 22:00", + "2016-09-10 10:00", + "2016-09-10 10:00", + "2016-09-10 10:00", + "2016-09-10 10:00", + "2016-09-10 22:00", + null + ]); + + expect(out[3].x).toEqual([ + "2016-09-01 22:00", + "2016-09-02 10:00", + "2016-09-02 10:00", + "2016-09-02 10:00", + "2016-09-02 10:00", + "2016-09-02 22:00", + null, + "2016-09-02 22:00", + "2016-09-03 10:00", + "2016-09-03 10:00", + "2016-09-03 10:00", + "2016-09-03 10:00", + "2016-09-03 22:00", + null, + "2016-09-04 22:00", + "2016-09-05 10:00", + "2016-09-05 10:00", + "2016-09-05 10:00", + "2016-09-05 10:00", + "2016-09-05 22:00", + null, + "2016-09-06 22:00", + "2016-09-07 10:00", + "2016-09-07 10:00", + "2016-09-07 10:00", + "2016-09-07 10:00", + "2016-09-07 22:00", + null + ]); } + ); - it('Plotly.restyle should work', function(done) { - var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); - - var path0; - - Plotly.plot(gd, [trace0]).then(function() { - expect(gd.calcdata[0][0].x).toEqual(-0.3); - expect(gd.calcdata[0][0].y).toEqual(33.01); - - return Plotly.restyle(gd, 'tickwidth', 0.5); - }) - .then(function() { - expect(gd.calcdata[0][0].x).toEqual(-0.5); - - return Plotly.restyle(gd, 'open', [[0, 30.75, 32.87, 31.62, 30.81, 32.75, 32.75, 32.87]]); - }) - .then(function() { - expect(gd.calcdata[0][0].y).toEqual(0); + it( + "should fallback to a minimum x difference of 0.5 in one-item traces", + function() { + var trace0 = Lib.extendDeep({}, mock1, { type: "ohlc", tickwidth: 0.5 }); + trace0.x = ["2016-01-01"]; - return Plotly.restyle(gd, { - type: 'candlestick', - open: [[33.01, 33.31, 33.50, 32.06, 34.12, 33.05, 33.31, 33.50]] - }); - }) - .then(function() { - path0 = d3.select('path.box').attr('d'); + var trace1 = Lib.extendDeep({}, mock0, { type: "ohlc", tickwidth: 0.5 }); + trace1.x = [10]; - return Plotly.restyle(gd, 'whiskerwidth', 0.2); - }) - .then(function() { - expect(d3.select('path.box').attr('d')).not.toEqual(path0); + var out = _calc([trace0, trace1]); - done(); - }); + var x0 = Lib.simpleMap(out[0].x, Lib.dateTime2ms); + expect(x0[x0.length - 2] - x0[0]).toEqual(1); - }); + var x2 = Lib.simpleMap(out[2].x, Lib.dateTime2ms); + expect(x2[x2.length - 2] - x2[0]).toEqual(1); - it('should be able to toggle visibility', function(done) { - var data = [ - Lib.extendDeep({}, mock0, { type: 'ohlc' }), - Lib.extendDeep({}, mock0, { type: 'candlestick' }), - ]; + expect(out[1].x).toEqual([]); + expect(out[3].x).toEqual([]); + } + ); +}); - Plotly.plot(gd, data).then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); +describe("finance charts updates:", function() { + "use strict"; + var gd; - return Plotly.restyle(gd, 'visible', false); - }) - .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(0); + beforeEach(function() { + gd = createGraphDiv(); + }); - return Plotly.restyle(gd, 'visible', 'legendonly', [1]); - }) - .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(0); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - return Plotly.restyle(gd, 'visible', true, [1]); - }) - .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(2); + function countScatterTraces() { + return d3.select("g.cartesianlayer").selectAll("g.trace.scatter").size(); + } - return Plotly.restyle(gd, 'visible', true, [0]); - }) - .then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); + function countBoxTraces() { + return d3.select("g.cartesianlayer").selectAll("g.trace.boxes").size(); + } - return Plotly.restyle(gd, 'visible', 'legendonly', [0]); - }) - .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(2); + function countRangeSliders() { + return d3.select("g.rangeslider-rangeplot").size(); + } - return Plotly.restyle(gd, 'visible', true); - }) - .then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); + it("Plotly.restyle should work", function(done) { + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc" }); - done(); - }); - }); + var path0; - it('Plotly.relayout should work', function(done) { - var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); + Plotly.plot(gd, [trace0]) + .then(function() { + expect(gd.calcdata[0][0].x).toEqual(-0.3); + expect(gd.calcdata[0][0].y).toEqual(33.01); - Plotly.plot(gd, [trace0]).then(function() { - expect(countRangeSliders()).toEqual(1); + return Plotly.restyle(gd, "tickwidth", 0.5); + }) + .then(function() { + expect(gd.calcdata[0][0].x).toEqual(-0.5); - return Plotly.relayout(gd, 'xaxis.rangeslider.visible', false); - }) - .then(function() { - expect(countRangeSliders()).toEqual(0); + return Plotly.restyle(gd, "open", [ + [0, 30.75, 32.87, 31.62, 30.81, 32.75, 32.75, 32.87] + ]); + }) + .then(function() { + expect(gd.calcdata[0][0].y).toEqual(0); - done(); + return Plotly.restyle(gd, { + type: "candlestick", + open: [[33.01, 33.31, 33.50, 32.06, 34.12, 33.05, 33.31, 33.50]] }); - - }); - - it('Plotly.extendTraces should work', function(done) { - var data = [ - Lib.extendDeep({}, mock0, { type: 'ohlc' }), - Lib.extendDeep({}, mock0, { type: 'candlestick' }), - ]; - - // ohlc have 7 calc pts per 'x' coords - - Plotly.plot(gd, data).then(function() { - expect(gd.calcdata[0].length).toEqual(28); - expect(gd.calcdata[1].length).toEqual(28); - expect(gd.calcdata[2].length).toEqual(4); - expect(gd.calcdata[3].length).toEqual(4); - - return Plotly.extendTraces(gd, { - open: [[ 34, 35 ]], - high: [[ 40, 41 ]], - low: [[ 32, 33 ]], - close: [[ 38, 39 ]] - }, [1]); - }) - .then(function() { - expect(gd.calcdata[0].length).toEqual(28); - expect(gd.calcdata[1].length).toEqual(28); - expect(gd.calcdata[2].length).toEqual(6); - expect(gd.calcdata[3].length).toEqual(4); - - return Plotly.extendTraces(gd, { - open: [[ 34, 35 ]], - high: [[ 40, 41 ]], - low: [[ 32, 33 ]], - close: [[ 38, 39 ]] - }, [0]); - }) - .then(function() { - expect(gd.calcdata[0].length).toEqual(42); - expect(gd.calcdata[1].length).toEqual(28); - expect(gd.calcdata[2].length).toEqual(6); - expect(gd.calcdata[3].length).toEqual(4); - - done(); + }) + .then(function() { + path0 = d3.select("path.box").attr("d"); + + return Plotly.restyle(gd, "whiskerwidth", 0.2); + }) + .then(function() { + expect(d3.select("path.box").attr("d")).not.toEqual(path0); + + done(); + }); + }); + + it("should be able to toggle visibility", function(done) { + var data = [ + Lib.extendDeep({}, mock0, { type: "ohlc" }), + Lib.extendDeep({}, mock0, { type: "candlestick" }) + ]; + + Plotly.plot(gd, data) + .then(function() { + expect(countScatterTraces()).toEqual(2); + expect(countBoxTraces()).toEqual(2); + + return Plotly.restyle(gd, "visible", false); + }) + .then(function() { + expect(countScatterTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(0); + + return Plotly.restyle(gd, "visible", "legendonly", [1]); + }) + .then(function() { + expect(countScatterTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(0); + + return Plotly.restyle(gd, "visible", true, [1]); + }) + .then(function() { + expect(countScatterTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(2); + + return Plotly.restyle(gd, "visible", true, [0]); + }) + .then(function() { + expect(countScatterTraces()).toEqual(2); + expect(countBoxTraces()).toEqual(2); + + return Plotly.restyle(gd, "visible", "legendonly", [0]); + }) + .then(function() { + expect(countScatterTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(2); + + return Plotly.restyle(gd, "visible", true); + }) + .then(function() { + expect(countScatterTraces()).toEqual(2); + expect(countBoxTraces()).toEqual(2); + + done(); + }); + }); + + it("Plotly.relayout should work", function(done) { + var trace0 = Lib.extendDeep({}, mock0, { type: "ohlc" }); + + Plotly.plot(gd, [trace0]) + .then(function() { + expect(countRangeSliders()).toEqual(1); + + return Plotly.relayout(gd, "xaxis.rangeslider.visible", false); + }) + .then(function() { + expect(countRangeSliders()).toEqual(0); + + done(); + }); + }); + + it("Plotly.extendTraces should work", function(done) { + var data = [ + Lib.extendDeep({}, mock0, { type: "ohlc" }), + Lib.extendDeep({}, mock0, { type: "candlestick" }) + ]; + + // ohlc have 7 calc pts per 'x' coords + Plotly.plot(gd, data) + .then(function() { + expect(gd.calcdata[0].length).toEqual(28); + expect(gd.calcdata[1].length).toEqual(28); + expect(gd.calcdata[2].length).toEqual(4); + expect(gd.calcdata[3].length).toEqual(4); + + return Plotly.extendTraces( + gd, + { + open: [[34, 35]], + high: [[40, 41]], + low: [[32, 33]], + close: [[38, 39]] + }, + [1] + ); + }) + .then(function() { + expect(gd.calcdata[0].length).toEqual(28); + expect(gd.calcdata[1].length).toEqual(28); + expect(gd.calcdata[2].length).toEqual(6); + expect(gd.calcdata[3].length).toEqual(4); + + return Plotly.extendTraces( + gd, + { + open: [[34, 35]], + high: [[40, 41]], + low: [[32, 33]], + close: [[38, 39]] + }, + [0] + ); + }) + .then(function() { + expect(gd.calcdata[0].length).toEqual(42); + expect(gd.calcdata[1].length).toEqual(28); + expect(gd.calcdata[2].length).toEqual(6); + expect(gd.calcdata[3].length).toEqual(4); + + done(); + }); + }); + + it("Plotly.deleteTraces / addTraces should work", function(done) { + var data = [ + Lib.extendDeep({}, mock0, { type: "ohlc" }), + Lib.extendDeep({}, mock0, { type: "candlestick" }) + ]; + + Plotly.plot(gd, data) + .then(function() { + expect(countScatterTraces()).toEqual(2); + expect(countBoxTraces()).toEqual(2); + + return Plotly.deleteTraces(gd, [1]); + }) + .then(function() { + expect(countScatterTraces()).toEqual(2); + expect(countBoxTraces()).toEqual(0); + + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + expect(countScatterTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(0); + + var trace = Lib.extendDeep({}, mock0, { type: "candlestick" }); + + return Plotly.addTraces(gd, [trace]); + }) + .then(function() { + expect(countScatterTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(2); + + var trace = Lib.extendDeep({}, mock0, { type: "ohlc" }); + + return Plotly.addTraces(gd, [trace]); + }) + .then(function() { + expect(countScatterTraces()).toEqual(2); + expect(countBoxTraces()).toEqual(2); + + done(); + }); + }); + + it( + "Plotly.addTraces + Plotly.relayout should update candlestick box position values", + function(done) { + function assertBoxPosFields(dPos) { + expect(gd.calcdata.length).toEqual(dPos.length); + + gd.calcdata.forEach(function(calcTrace, i) { + if (dPos[i] === undefined) { + expect(calcTrace[0].t.dPos).toBeUndefined(); + } else { + expect(calcTrace[0].t.dPos).toEqual(dPos[i]); + } }); - }); - - it('Plotly.deleteTraces / addTraces should work', function(done) { - var data = [ - Lib.extendDeep({}, mock0, { type: 'ohlc' }), - Lib.extendDeep({}, mock0, { type: 'candlestick' }), - ]; - - Plotly.plot(gd, data).then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); - - return Plotly.deleteTraces(gd, [1]); - }) + } + + var trace0 = { + type: "candlestick", + x: ["2011-01-01"], + open: [0], + high: [3], + low: [1], + close: [3] + }; + + Plotly.plot(gd, [trace0]) .then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(0); + assertBoxPosFields([0.5, undefined]); - return Plotly.deleteTraces(gd, [0]); + return Plotly.addTraces(gd, {}); }) .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(0); - - var trace = Lib.extendDeep({}, mock0, { type: 'candlestick' }); - - return Plotly.addTraces(gd, [trace]); + var update = { + type: "candlestick", + x: [["2011-02-02"]], + open: [[0]], + high: [[3]], + low: [[1]], + close: [[3]] + }; + + return Plotly.restyle(gd, update); }) .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(2); - - var trace = Lib.extendDeep({}, mock0, { type: 'ohlc' }); + assertBoxPosFields([0.5, undefined, 0.5, undefined]); - return Plotly.addTraces(gd, [trace]); - }) - .then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); - - done(); + done(); }); - }); - - it('Plotly.addTraces + Plotly.relayout should update candlestick box position values', function(done) { - - function assertBoxPosFields(dPos) { - expect(gd.calcdata.length).toEqual(dPos.length); - - gd.calcdata.forEach(function(calcTrace, i) { - if(dPos[i] === undefined) { - expect(calcTrace[0].t.dPos).toBeUndefined(); - } - else { - expect(calcTrace[0].t.dPos).toEqual(dPos[i]); - } - }); - } - - var trace0 = { - type: 'candlestick', - x: ['2011-01-01'], - open: [0], - high: [3], - low: [1], - close: [3] - }; - - Plotly.plot(gd, [trace0]).then(function() { - assertBoxPosFields([0.5, undefined]); - - return Plotly.addTraces(gd, {}); - - }) + } + ); + + it( + "Plotly.plot with data-less trace and adding with Plotly.restyle", + function(done) { + var data = [ + { type: "candlestick" }, + { type: "ohlc" }, + { type: "bar", y: [2, 1, 2] } + ]; + + Plotly.plot(gd, data) .then(function() { - var update = { - type: 'candlestick', - x: [['2011-02-02']], - open: [[0]], - high: [[3]], - low: [[1]], - close: [[3]] - }; - - return Plotly.restyle(gd, update); + expect(countScatterTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(0); + expect(countRangeSliders()).toEqual(0); + + return Plotly.restyle( + gd, + { + open: [mock0.open], + high: [mock0.high], + low: [mock0.low], + close: [mock0.close] + }, + [0] + ); }) .then(function() { - assertBoxPosFields([0.5, undefined, 0.5, undefined]); - - done(); - }); - }); - - it('Plotly.plot with data-less trace and adding with Plotly.restyle', function(done) { - var data = [ - { type: 'candlestick' }, - { type: 'ohlc' }, - { type: 'bar', y: [2, 1, 2] } - ]; - - Plotly.plot(gd, data).then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(0); - expect(countRangeSliders()).toEqual(0); - - return Plotly.restyle(gd, { - open: [mock0.open], - high: [mock0.high], - low: [mock0.low], - close: [mock0.close] - }, [0]); - }) - .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(2); - expect(countRangeSliders()).toEqual(1); - - return Plotly.restyle(gd, { - open: [mock0.open], - high: [mock0.high], - low: [mock0.low], - close: [mock0.close] - }, [1]); + expect(countScatterTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(2); + expect(countRangeSliders()).toEqual(1); + + return Plotly.restyle( + gd, + { + open: [mock0.open], + high: [mock0.high], + low: [mock0.low], + close: [mock0.close] + }, + [1] + ); }) .then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); - expect(countRangeSliders()).toEqual(1); + expect(countScatterTraces()).toEqual(2); + expect(countBoxTraces()).toEqual(2); + expect(countRangeSliders()).toEqual(1); }) .then(done); - }); - + } + ); }); -describe('finance charts *special* handlers:', function() { - - afterEach(destroyGraphDiv); - - it('`editable: true` handlers should work', function(done) { +describe("finance charts *special* handlers:", function() { + afterEach(destroyGraphDiv); - var gd = createGraphDiv(); + it("`editable: true` handlers should work", function(done) { + var gd = createGraphDiv(); - function editText(itemNumber, newText) { - var textNode = d3.selectAll('text.legendtext') - .filter(function(_, i) { return i === itemNumber; }).node(); - textNode.dispatchEvent(new window.MouseEvent('click')); - - var editNode = d3.select('.plugin-editable.editable').node(); - editNode.dispatchEvent(new window.FocusEvent('focus')); + function editText(itemNumber, newText) { + var textNode = d3 + .selectAll("text.legendtext") + .filter(function(_, i) { + return i === itemNumber; + }) + .node(); + textNode.dispatchEvent(new window.MouseEvent("click")); - editNode.textContent = newText; - editNode.dispatchEvent(new window.FocusEvent('focus')); - editNode.dispatchEvent(new window.FocusEvent('blur')); - } + var editNode = d3.select(".plugin-editable.editable").node(); + editNode.dispatchEvent(new window.FocusEvent("focus")); - // makeEditable in svg_text_utils clears the edit
in - // a 0-second transition, so push the resolve call at the back - // of the rendering queue to make sure the edit
is properly - // cleared after each mocked text edits. - function delayedResolve(resolve) { - setTimeout(function() { return resolve(gd); }, 0); - } + editNode.textContent = newText; + editNode.dispatchEvent(new window.FocusEvent("focus")); + editNode.dispatchEvent(new window.FocusEvent("blur")); + } - Plotly.plot(gd, [ - Lib.extendDeep({}, mock0, { type: 'ohlc' }), - Lib.extendDeep({}, mock0, { type: 'candlestick' }) - ], {}, { - editable: true - }) - .then(function(gd) { - return new Promise(function(resolve) { - gd.once('plotly_restyle', function(eventData) { - expect(eventData[0]['increasing.name']).toEqual('0'); - expect(eventData[1]).toEqual([0]); - delayedResolve(resolve); - }); - - editText(0, '0'); - }); - }) - .then(function(gd) { - return new Promise(function(resolve) { - gd.once('plotly_restyle', function(eventData) { - expect(eventData[0]['decreasing.name']).toEqual('1'); - expect(eventData[1]).toEqual([0]); - delayedResolve(resolve); - }); - - editText(1, '1'); - }); - }) - .then(function(gd) { - return new Promise(function(resolve) { - gd.once('plotly_restyle', function(eventData) { - expect(eventData[0]['decreasing.name']).toEqual('2'); - expect(eventData[1]).toEqual([1]); - delayedResolve(resolve); - }); - - editText(3, '2'); - }); - }) - .then(function(gd) { - return new Promise(function(resolve) { - gd.once('plotly_restyle', function(eventData) { - expect(eventData[0]['increasing.name']).toEqual('3'); - expect(eventData[1]).toEqual([1]); - delayedResolve(resolve); - }); - - editText(2, '3'); - }); - }) - .then(done); - }); + // makeEditable in svg_text_utils clears the edit
in + // a 0-second transition, so push the resolve call at the back + // of the rendering queue to make sure the edit
is properly + // cleared after each mocked text edits. + function delayedResolve(resolve) { + setTimeout( + function() { + return resolve(gd); + }, + 0 + ); + } + Plotly.plot( + gd, + [ + Lib.extendDeep({}, mock0, { type: "ohlc" }), + Lib.extendDeep({}, mock0, { type: "candlestick" }) + ], + {}, + { editable: true } + ) + .then(function(gd) { + return new Promise(function(resolve) { + gd.once("plotly_restyle", function(eventData) { + expect(eventData[0]["increasing.name"]).toEqual("0"); + expect(eventData[1]).toEqual([0]); + delayedResolve(resolve); + }); + + editText(0, "0"); + }); + }) + .then(function(gd) { + return new Promise(function(resolve) { + gd.once("plotly_restyle", function(eventData) { + expect(eventData[0]["decreasing.name"]).toEqual("1"); + expect(eventData[1]).toEqual([0]); + delayedResolve(resolve); + }); + + editText(1, "1"); + }); + }) + .then(function(gd) { + return new Promise(function(resolve) { + gd.once("plotly_restyle", function(eventData) { + expect(eventData[0]["decreasing.name"]).toEqual("2"); + expect(eventData[1]).toEqual([1]); + delayedResolve(resolve); + }); + + editText(3, "2"); + }); + }) + .then(function(gd) { + return new Promise(function(resolve) { + gd.once("plotly_restyle", function(eventData) { + expect(eventData[0]["increasing.name"]).toEqual("3"); + expect(eventData[1]).toEqual([1]); + delayedResolve(resolve); + }); + + editText(2, "3"); + }); + }) + .then(done); + }); }); diff --git a/test/jasmine/tests/frame_api_test.js b/test/jasmine/tests/frame_api_test.js index 16c253e06e8..10f3708357a 100644 --- a/test/jasmine/tests/frame_api_test.js +++ b/test/jasmine/tests/frame_api_test.js @@ -1,267 +1,403 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); - -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); - -describe('Test frame api', function() { - 'use strict'; - - var gd, mock, f, h; - - beforeEach(function(done) { - mock = [{x: [1, 2, 3], y: [2, 1, 3]}, {x: [1, 2, 3], y: [6, 4, 5]}]; - gd = createGraphDiv(); - Plotly.plot(gd, mock).then(function() { - f = gd._transitionData._frames; - h = gd._transitionData._frameHash; - }).then(function() { - Plotly.setPlotConfig({ queueLength: 10 }); - }).then(done); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); + +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var fail = require("../assets/fail_test"); + +describe("Test frame api", function() { + "use strict"; + var gd, mock, f, h; + + beforeEach(function(done) { + mock = [{ x: [1, 2, 3], y: [2, 1, 3] }, { x: [1, 2, 3], y: [6, 4, 5] }]; + gd = createGraphDiv(); + Plotly.plot(gd, mock) + .then(function() { + f = gd._transitionData._frames; + h = gd._transitionData._frameHash; + }) + .then(function() { + Plotly.setPlotConfig({ queueLength: 10 }); + }) + .then(done); + }); + + afterEach(function() { + destroyGraphDiv(); + Plotly.setPlotConfig({ queueLength: 0 }); + }); + + describe("gd initialization", function() { + it("creates an empty list for frames", function() { + expect(gd._transitionData._frames).toEqual([]); }); - afterEach(function() { - destroyGraphDiv(); - Plotly.setPlotConfig({queueLength: 0}); + it("creates an empty lookup table for frames", function() { + expect(gd._transitionData._counter).toEqual(0); }); - - describe('gd initialization', function() { - it('creates an empty list for frames', function() { - expect(gd._transitionData._frames).toEqual([]); - }); - - it('creates an empty lookup table for frames', function() { - expect(gd._transitionData._counter).toEqual(0); - }); + }); + + describe("#addFrames", function() { + it("treats an undefined list as a noop", function(done) { + Plotly.addFrames(gd, undefined) + .then(function() { + expect(Object.keys(h)).toEqual([]); + }) + .catch(fail) + .then(done); }); - describe('#addFrames', function() { - it('treats an undefined list as a noop', function(done) { - Plotly.addFrames(gd, undefined).then(function() { - expect(Object.keys(h)).toEqual([]); - }).catch(fail).then(done); - }); - - it('compresses garbage when adding frames', function(done) { - Plotly.addFrames(gd, [null, 'garbage', 14, true, false, {name: 'test'}, null]).then(function() { - expect(Object.keys(h)).toEqual(['test']); - expect(f).toEqual([{name: 'test'}]); - }).catch(fail).then(done); - }); - - it('treats a null list as a noop', function(done) { - Plotly.addFrames(gd, null).then(function() { - expect(Object.keys(h)).toEqual([]); - }).catch(fail).then(done); - }); - - it('treats an empty list as a noop', function(done) { - Plotly.addFrames(gd, []).then(function() { - expect(Object.keys(h)).toEqual([]); - }).catch(fail).then(done); - }); - - it('names an unnamed frame', function(done) { - Plotly.addFrames(gd, [{}]).then(function() { - expect(Object.keys(h)).toEqual(['frame 0']); - }).catch(fail).then(done); - }); - - it('casts names to strings', function(done) { - Plotly.addFrames(gd, [{name: 5}]).then(function() { - expect(Object.keys(h)).toEqual(['5']); - }).catch(fail).then(done); - }); + it("compresses garbage when adding frames", function(done) { + Plotly.addFrames(gd, [ + null, + "garbage", + 14, + true, + false, + { name: "test" }, + null + ]) + .then(function() { + expect(Object.keys(h)).toEqual(["test"]); + expect(f).toEqual([{ name: "test" }]); + }) + .catch(fail) + .then(done); + }); - it('creates multiple unnamed frames at the same time', function(done) { - Plotly.addFrames(gd, [{}, {}]).then(function() { - expect(f).toEqual([{name: 'frame 0'}, {name: 'frame 1'}]); - }).catch(fail).then(done); - }); + it("treats a null list as a noop", function(done) { + Plotly.addFrames(gd, null) + .then(function() { + expect(Object.keys(h)).toEqual([]); + }) + .catch(fail) + .then(done); + }); - it('creates multiple unnamed frames in series', function(done) { - Plotly.addFrames(gd, [{}]).then(function() { - return Plotly.addFrames(gd, [{}]); - }).then(function() { - expect(f).toEqual([{name: 'frame 0'}, {name: 'frame 1'}]); - }).catch(fail).then(done); - }); + it("treats an empty list as a noop", function(done) { + Plotly.addFrames(gd, []) + .then(function() { + expect(Object.keys(h)).toEqual([]); + }) + .catch(fail) + .then(done); + }); - it('casts number names to strings on insertion', function(done) { - Plotly.addFrames(gd, [{name: 2}]).then(function() { - expect(f).toEqual([{name: '2'}]); - }).catch(fail).then(done); - }); + it("names an unnamed frame", function(done) { + Plotly.addFrames(gd, [{}]) + .then(function() { + expect(Object.keys(h)).toEqual(["frame 0"]); + }) + .catch(fail) + .then(done); + }); - it('updates frames referenced by number', function(done) { - Plotly.addFrames(gd, [{name: 2}]).then(function() { - return Plotly.addFrames(gd, [{name: 2, layout: {foo: 'bar'}}]); - }).then(function() { - expect(f).toEqual([{name: '2', layout: {foo: 'bar'}}]); - }).catch(fail).then(done); - }); + it("casts names to strings", function(done) { + Plotly.addFrames(gd, [{ name: 5 }]) + .then(function() { + expect(Object.keys(h)).toEqual(["5"]); + }) + .catch(fail) + .then(done); + }); - it('issues a warning if a number-named frame would overwrite a frame', function(done) { - var warnings = []; - spyOn(Lib, 'warn').and.callFake(function(msg) { - warnings.push(msg); - }); - - Plotly.addFrames(gd, [{name: 2}]).then(function() { - return Plotly.addFrames(gd, [{name: 2, layout: {foo: 'bar'}}]); - }).then(function() { - expect(warnings.length).toEqual(1); - expect(warnings[0]).toMatch(/overwriting/); - }).catch(fail).then(done); - }); + it("creates multiple unnamed frames at the same time", function(done) { + Plotly.addFrames(gd, [{}, {}]) + .then(function() { + expect(f).toEqual([{ name: "frame 0" }, { name: "frame 1" }]); + }) + .catch(fail) + .then(done); + }); - it('avoids name collisions', function(done) { - Plotly.addFrames(gd, [{name: 'frame 0'}, {name: 'frame 2'}]).then(function() { - expect(f).toEqual([{name: 'frame 0'}, {name: 'frame 2'}]); + it("creates multiple unnamed frames in series", function(done) { + Plotly.addFrames(gd, [{}]) + .then(function() { + return Plotly.addFrames(gd, [{}]); + }) + .then(function() { + expect(f).toEqual([{ name: "frame 0" }, { name: "frame 1" }]); + }) + .catch(fail) + .then(done); + }); - return Plotly.addFrames(gd, [{}, {name: 'foobar'}, {}]); - }).then(function() { - expect(f).toEqual([{name: 'frame 0'}, {name: 'frame 2'}, {name: 'frame 1'}, {name: 'foobar'}, {name: 'frame 3'}]); - }).catch(fail).then(done); - }); + it("casts number names to strings on insertion", function(done) { + Plotly.addFrames(gd, [{ name: 2 }]) + .then(function() { + expect(f).toEqual([{ name: "2" }]); + }) + .catch(fail) + .then(done); + }); - it('inserts frames at specific indices', function(done) { - var i; - var frames = []; - for(i = 0; i < 10; i++) { - frames.push({name: 'frame' + i}); - } - - function validate() { - for(i = 0; i < f.length; i++) { - expect(f[i].name).toEqual('frame' + i); - } - } - - Plotly.addFrames(gd, frames).then(validate).then(function() { - return Plotly.addFrames(gd, [{name: 'frame5', data: [1]}, {name: 'frame7', data: [2]}, {name: 'frame10', data: [3]}], [5, 7, undefined]); - }).then(function() { - expect(f[5]).toEqual({name: 'frame5', data: [1]}); - expect(f[7]).toEqual({name: 'frame7', data: [2]}); - expect(f[10]).toEqual({name: 'frame10', data: [3]}); - - return Plotly.Queue.undo(gd); - }).then(validate).catch(fail).then(done); - }); + it("updates frames referenced by number", function(done) { + Plotly.addFrames(gd, [{ name: 2 }]) + .then(function() { + return Plotly.addFrames(gd, [{ name: 2, layout: { foo: "bar" } }]); + }) + .then(function() { + expect(f).toEqual([{ name: "2", layout: { foo: "bar" } }]); + }) + .catch(fail) + .then(done); + }); - it('inserts frames at specific indices (reversed)', function(done) { - var i; - var frames = []; - for(i = 0; i < 10; i++) { - frames.push({name: 'frame' + i}); - } - - function validate() { - for(i = 0; i < f.length; i++) { - expect(f[i].name).toEqual('frame' + i); - } - } - - Plotly.addFrames(gd, frames).then(validate).then(function() { - return Plotly.addFrames(gd, [{name: 'frame10', data: [3]}, {name: 'frame7', data: [2]}, {name: 'frame5', data: [1]}], [undefined, 7, 5]); - }).then(function() { - expect(f[5]).toEqual({name: 'frame5', data: [1]}); - expect(f[7]).toEqual({name: 'frame7', data: [2]}); - expect(f[10]).toEqual({name: 'frame10', data: [3]}); - - return Plotly.Queue.undo(gd); - }).then(validate).catch(fail).then(done); + it( + "issues a warning if a number-named frame would overwrite a frame", + function(done) { + var warnings = []; + spyOn(Lib, "warn").and.callFake(function(msg) { + warnings.push(msg); }); - it('implements undo/redo', function(done) { - function validate() { - expect(f).toEqual([{name: 'frame 0'}, {name: 'frame 1'}]); - expect(h).toEqual({'frame 0': {name: 'frame 0'}, 'frame 1': {name: 'frame 1'}}); - } + Plotly.addFrames(gd, [{ name: 2 }]) + .then(function() { + return Plotly.addFrames(gd, [{ name: 2, layout: { foo: "bar" } }]); + }) + .then(function() { + expect(warnings.length).toEqual(1); + expect(warnings[0]).toMatch(/overwriting/); + }) + .catch(fail) + .then(done); + } + ); + + it("avoids name collisions", function(done) { + Plotly.addFrames(gd, [{ name: "frame 0" }, { name: "frame 2" }]) + .then(function() { + expect(f).toEqual([{ name: "frame 0" }, { name: "frame 2" }]); + + return Plotly.addFrames(gd, [{}, { name: "foobar" }, {}]); + }) + .then(function() { + expect(f).toEqual([ + { name: "frame 0" }, + { name: "frame 2" }, + { name: "frame 1" }, + { name: "foobar" }, + { name: "frame 3" } + ]); + }) + .catch(fail) + .then(done); + }); - Plotly.addFrames(gd, [{name: 'frame 0'}, {name: 'frame 1'}]).then(validate).then(function() { - return Plotly.Queue.undo(gd); - }).then(function() { - expect(f).toEqual([]); - expect(h).toEqual({}); + it("inserts frames at specific indices", function(done) { + var i; + var frames = []; + for (i = 0; i < 10; i++) { + frames.push({ name: "frame" + i }); + } + + function validate() { + for (i = 0; i < f.length; i++) { + expect(f[i].name).toEqual("frame" + i); + } + } + + Plotly.addFrames(gd, frames) + .then(validate) + .then(function() { + return Plotly.addFrames( + gd, + [ + { name: "frame5", data: [1] }, + { name: "frame7", data: [2] }, + { name: "frame10", data: [3] } + ], + [5, 7, undefined] + ); + }) + .then(function() { + expect(f[5]).toEqual({ name: "frame5", data: [1] }); + expect(f[7]).toEqual({ name: "frame7", data: [2] }); + expect(f[10]).toEqual({ name: "frame10", data: [3] }); + + return Plotly.Queue.undo(gd); + }) + .then(validate) + .catch(fail) + .then(done); + }); - return Plotly.Queue.redo(gd); - }).then(validate).catch(fail).then(done); - }); + it("inserts frames at specific indices (reversed)", function(done) { + var i; + var frames = []; + for (i = 0; i < 10; i++) { + frames.push({ name: "frame" + i }); + } + + function validate() { + for (i = 0; i < f.length; i++) { + expect(f[i].name).toEqual("frame" + i); + } + } + + Plotly.addFrames(gd, frames) + .then(validate) + .then(function() { + return Plotly.addFrames( + gd, + [ + { name: "frame10", data: [3] }, + { name: "frame7", data: [2] }, + { name: "frame5", data: [1] } + ], + [undefined, 7, 5] + ); + }) + .then(function() { + expect(f[5]).toEqual({ name: "frame5", data: [1] }); + expect(f[7]).toEqual({ name: "frame7", data: [2] }); + expect(f[10]).toEqual({ name: "frame10", data: [3] }); + + return Plotly.Queue.undo(gd); + }) + .then(validate) + .catch(fail) + .then(done); + }); - it('overwrites frames', function(done) { - // The whole shebang. This hits insertion + replacements + deletion + undo + redo: - Plotly.addFrames(gd, [{name: 'test1', data: ['y']}, {name: 'test2'}]).then(function() { - expect(f).toEqual([{name: 'test1', data: ['y']}, {name: 'test2'}]); - expect(Object.keys(h)).toEqual(['test1', 'test2']); - - return Plotly.addFrames(gd, [{name: 'test1'}, {name: 'test3'}]); - }).then(function() { - expect(f).toEqual([{name: 'test1'}, {name: 'test2'}, {name: 'test3'}]); - expect(Object.keys(h)).toEqual(['test1', 'test2', 'test3']); - - return Plotly.Queue.undo(gd); - }).then(function() { - expect(f).toEqual([{name: 'test1', data: ['y']}, {name: 'test2'}]); - expect(Object.keys(h)).toEqual(['test1', 'test2']); - - return Plotly.Queue.redo(gd); - }).then(function() { - expect(f).toEqual([{name: 'test1'}, {name: 'test2'}, {name: 'test3'}]); - expect(Object.keys(h)).toEqual(['test1', 'test2', 'test3']); - }).catch(fail).then(done); + it("implements undo/redo", function(done) { + function validate() { + expect(f).toEqual([{ name: "frame 0" }, { name: "frame 1" }]); + expect(h).toEqual({ + "frame 0": { name: "frame 0" }, + "frame 1": { name: "frame 1" } }); + } + + Plotly.addFrames(gd, [{ name: "frame 0" }, { name: "frame 1" }]) + .then(validate) + .then(function() { + return Plotly.Queue.undo(gd); + }) + .then(function() { + expect(f).toEqual([]); + expect(h).toEqual({}); + + return Plotly.Queue.redo(gd); + }) + .then(validate) + .catch(fail) + .then(done); }); - describe('#deleteFrames', function() { - it('deletes a frame', function(done) { - Plotly.addFrames(gd, [{name: 'frame1'}]).then(function() { - expect(f).toEqual([{name: 'frame1'}]); - expect(Object.keys(h)).toEqual(['frame1']); - - return Plotly.deleteFrames(gd, [0]); - }).then(function() { - expect(f).toEqual([]); - expect(Object.keys(h)).toEqual([]); - - return Plotly.Queue.undo(gd); - }).then(function() { - expect(f).toEqual([{name: 'frame1'}]); - - return Plotly.Queue.redo(gd); - }).then(function() { - expect(f).toEqual([]); - expect(Object.keys(h)).toEqual([]); - }).catch(fail).then(done); - }); + it("overwrites frames", function(done) { + // The whole shebang. This hits insertion + replacements + deletion + undo + redo: + Plotly.addFrames(gd, [{ name: "test1", data: ["y"] }, { name: "test2" }]) + .then(function() { + expect(f).toEqual([ + { name: "test1", data: ["y"] }, + { name: "test2" } + ]); + expect(Object.keys(h)).toEqual(["test1", "test2"]); + + return Plotly.addFrames(gd, [{ name: "test1" }, { name: "test3" }]); + }) + .then(function() { + expect(f).toEqual([ + { name: "test1" }, + { name: "test2" }, + { name: "test3" } + ]); + expect(Object.keys(h)).toEqual(["test1", "test2", "test3"]); + + return Plotly.Queue.undo(gd); + }) + .then(function() { + expect(f).toEqual([ + { name: "test1", data: ["y"] }, + { name: "test2" } + ]); + expect(Object.keys(h)).toEqual(["test1", "test2"]); + + return Plotly.Queue.redo(gd); + }) + .then(function() { + expect(f).toEqual([ + { name: "test1" }, + { name: "test2" }, + { name: "test3" } + ]); + expect(Object.keys(h)).toEqual(["test1", "test2", "test3"]); + }) + .catch(fail) + .then(done); + }); + }); + + describe("#deleteFrames", function() { + it("deletes a frame", function(done) { + Plotly.addFrames(gd, [{ name: "frame1" }]) + .then(function() { + expect(f).toEqual([{ name: "frame1" }]); + expect(Object.keys(h)).toEqual(["frame1"]); + + return Plotly.deleteFrames(gd, [0]); + }) + .then(function() { + expect(f).toEqual([]); + expect(Object.keys(h)).toEqual([]); + + return Plotly.Queue.undo(gd); + }) + .then(function() { + expect(f).toEqual([{ name: "frame1" }]); + + return Plotly.Queue.redo(gd); + }) + .then(function() { + expect(f).toEqual([]); + expect(Object.keys(h)).toEqual([]); + }) + .catch(fail) + .then(done); + }); - it('deletes multiple frames', function(done) { - var i; - var frames = []; - for(i = 0; i < 10; i++) { - frames.push({name: 'frame' + i}); - } - - function validate() { - var expected = ['frame0', 'frame1', 'frame3', 'frame5', 'frame7', 'frame9']; - expect(f.length).toEqual(expected.length); - for(i = 0; i < expected.length; i++) { - expect(f[i].name).toEqual(expected[i]); - } - } - - Plotly.addFrames(gd, frames).then(function() { - return Plotly.deleteFrames(gd, [2, 8, 4, 6]); - }).then(validate).then(function() { - return Plotly.Queue.undo(gd); - }).then(function() { - for(i = 0; i < 10; i++) { - expect(f[i]).toEqual({name: 'frame' + i}); - } - - return Plotly.Queue.redo(gd); - }).then(validate).catch(fail).then(done); - }); + it("deletes multiple frames", function(done) { + var i; + var frames = []; + for (i = 0; i < 10; i++) { + frames.push({ name: "frame" + i }); + } + + function validate() { + var expected = [ + "frame0", + "frame1", + "frame3", + "frame5", + "frame7", + "frame9" + ]; + expect(f.length).toEqual(expected.length); + for (i = 0; i < expected.length; i++) { + expect(f[i].name).toEqual(expected[i]); + } + } + + Plotly.addFrames(gd, frames) + .then(function() { + return Plotly.deleteFrames(gd, [2, 8, 4, 6]); + }) + .then(validate) + .then(function() { + return Plotly.Queue.undo(gd); + }) + .then(function() { + for (i = 0; i < 10; i++) { + expect(f[i]).toEqual({ name: "frame" + i }); + } + + return Plotly.Queue.redo(gd); + }) + .then(validate) + .catch(fail) + .then(done); }); + }); }); diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js index 4a89b547472..2a8011abb17 100644 --- a/test/jasmine/tests/fx_test.js +++ b/test/jasmine/tests/fx_test.js @@ -1,135 +1,142 @@ -var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); - -var Fx = require('@src/plots/cartesian/graph_interact'); - -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); - - -describe('Fx defaults', function() { - 'use strict'; - - var layoutIn, layoutOut, fullData; - - beforeEach(function() { - layoutIn = {}; - layoutOut = { - _has: Plots._hasPlotType - }; - fullData = [{}]; - }); - - it('should default (blank version)', function() { - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest'); - expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); - }); - - it('should default (cartesian version)', function() { - layoutOut._basePlotModules = [{ name: 'cartesian' }]; - - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.hovermode).toBe('x', 'hovermode to x'); - expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); - expect(layoutOut._isHoriz).toBe(false, 'isHoriz to false'); - }); - - it('should default (cartesian horizontal version)', function() { - layoutOut._basePlotModules = [{ name: 'cartesian' }]; - fullData[0] = { orientation: 'h' }; - - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.hovermode).toBe('y', 'hovermode to y'); - expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); - expect(layoutOut._isHoriz).toBe(true, 'isHoriz to true'); - }); - - it('should default (gl3d version)', function() { - layoutOut._basePlotModules = [{ name: 'gl3d' }]; - - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest'); - expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); - }); - - it('should default (geo version)', function() { - layoutOut._basePlotModules = [{ name: 'geo' }]; - - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest'); - expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); - }); - - it('should default (multi plot type version)', function() { - layoutOut._basePlotModules = [{ name: 'cartesian' }, { name: 'gl3d' }]; - - Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.hovermode).toBe('x', 'hovermode to x'); - expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); - }); +var Plotly = require("@lib/index"); +var Plots = require("@src/plots/plots"); + +var Fx = require("@src/plots/cartesian/graph_interact"); + +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); + +describe("Fx defaults", function() { + "use strict"; + var layoutIn, layoutOut, fullData; + + beforeEach(function() { + layoutIn = {}; + layoutOut = { _has: Plots._hasPlotType }; + fullData = [{}]; + }); + + it("should default (blank version)", function() { + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe("closest", "hovermode to closest"); + expect(layoutOut.dragmode).toBe("zoom", "dragmode to zoom"); + }); + + it("should default (cartesian version)", function() { + layoutOut._basePlotModules = [{ name: "cartesian" }]; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe("x", "hovermode to x"); + expect(layoutOut.dragmode).toBe("zoom", "dragmode to zoom"); + expect(layoutOut._isHoriz).toBe(false, "isHoriz to false"); + }); + + it("should default (cartesian horizontal version)", function() { + layoutOut._basePlotModules = [{ name: "cartesian" }]; + fullData[0] = { orientation: "h" }; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe("y", "hovermode to y"); + expect(layoutOut.dragmode).toBe("zoom", "dragmode to zoom"); + expect(layoutOut._isHoriz).toBe(true, "isHoriz to true"); + }); + + it("should default (gl3d version)", function() { + layoutOut._basePlotModules = [{ name: "gl3d" }]; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe("closest", "hovermode to closest"); + expect(layoutOut.dragmode).toBe("zoom", "dragmode to zoom"); + }); + + it("should default (geo version)", function() { + layoutOut._basePlotModules = [{ name: "geo" }]; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe("closest", "hovermode to closest"); + expect(layoutOut.dragmode).toBe("zoom", "dragmode to zoom"); + }); + + it("should default (multi plot type version)", function() { + layoutOut._basePlotModules = [{ name: "cartesian" }, { name: "gl3d" }]; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe("x", "hovermode to x"); + expect(layoutOut.dragmode).toBe("zoom", "dragmode to zoom"); + }); }); -describe('relayout', function() { - 'use strict'; - - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - 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(); - - expect(mainDrag.classed('cursor-' + cursor)).toBe(true, 'cursor ' + cursor); - expect(mainDrag.style('pointer-events')).toEqual('all', 'pointer event'); - expect(!!node.onmousedown).toBe(isActive, 'mousedown handler'); - } - - Plotly.plot(gd, [{ - y: [2, 1, 2] - }]).then(function() { - assertMainDrag('crosshair', true); - - return Plotly.relayout(gd, 'dragmode', 'pan'); - }).then(function() { - assertMainDrag('move', true); - - return Plotly.relayout(gd, 'dragmode', 'drag'); - }).then(function() { - assertMainDrag('crosshair', true); - - return Plotly.relayout(gd, 'xaxis.fixedrange', true); - }).then(function() { - assertMainDrag('ns-resize', true); - - return Plotly.relayout(gd, 'yaxis.fixedrange', true); - }).then(function() { - assertMainDrag('pointer', false); - - return Plotly.relayout(gd, 'dragmode', 'drag'); - }).then(function() { - assertMainDrag('pointer', false); - - return Plotly.relayout(gd, 'dragmode', 'lasso'); - }).then(function() { - assertMainDrag('pointer', true); - - return Plotly.relayout(gd, 'dragmode', 'select'); - }).then(function() { - assertMainDrag('pointer', true); - - return Plotly.relayout(gd, 'xaxis.fixedrange', false); - }).then(function() { - assertMainDrag('ew-resize', true); - }).then(done); - }); +describe("relayout", function() { + "use strict"; + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + 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(); + + expect(mainDrag.classed("cursor-" + cursor)).toBe( + true, + "cursor " + cursor + ); + expect(mainDrag.style("pointer-events")).toEqual("all", "pointer event"); + expect(!!node.onmousedown).toBe(isActive, "mousedown handler"); + } + + Plotly.plot(gd, [{ y: [2, 1, 2] }]) + .then(function() { + assertMainDrag("crosshair", true); + + return Plotly.relayout(gd, "dragmode", "pan"); + }) + .then(function() { + assertMainDrag("move", true); + + return Plotly.relayout(gd, "dragmode", "drag"); + }) + .then(function() { + assertMainDrag("crosshair", true); + + return Plotly.relayout(gd, "xaxis.fixedrange", true); + }) + .then(function() { + assertMainDrag("ns-resize", true); + + return Plotly.relayout(gd, "yaxis.fixedrange", true); + }) + .then(function() { + assertMainDrag("pointer", false); + + return Plotly.relayout(gd, "dragmode", "drag"); + }) + .then(function() { + assertMainDrag("pointer", false); + + return Plotly.relayout(gd, "dragmode", "lasso"); + }) + .then(function() { + assertMainDrag("pointer", true); + + return Plotly.relayout(gd, "dragmode", "select"); + }) + .then(function() { + assertMainDrag("pointer", true); + + return Plotly.relayout(gd, "xaxis.fixedrange", false); + }) + .then(function() { + assertMainDrag("ew-resize", true); + }) + .then(done); + }); }); diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index 244db755d2c..33ddc7cdb04 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -1,989 +1,1035 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); - -var Geo = require('@src/plots/geo'); -var GeoAssets = require('@src/assets/geo_assets'); -var params = require('@src/plots/geo/constants'); -var supplyLayoutDefaults = require('@src/plots/geo/layout/axis_defaults'); -var geoLocationUtils = require('@src/lib/geo_location_utils'); -var topojsonUtils = require('@src/lib/topojson_utils'); - -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); - -var HOVERMINTIME = require('@src/plots/cartesian/constants').HOVERMINTIME; - - -describe('Test geoaxes', function() { - 'use strict'; - - describe('supplyLayoutDefaults', function() { - var geoLayoutIn, - geoLayoutOut; - - beforeEach(function() { - geoLayoutOut = {}; - }); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); + +var Geo = require("@src/plots/geo"); +var GeoAssets = require("@src/assets/geo_assets"); +var params = require("@src/plots/geo/constants"); +var supplyLayoutDefaults = require("@src/plots/geo/layout/axis_defaults"); +var geoLocationUtils = require("@src/lib/geo_location_utils"); +var topojsonUtils = require("@src/lib/topojson_utils"); + +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); + +var HOVERMINTIME = require("@src/plots/cartesian/constants").HOVERMINTIME; + +describe("Test geoaxes", function() { + "use strict"; + describe("supplyLayoutDefaults", function() { + var geoLayoutIn, geoLayoutOut; + + beforeEach(function() { + geoLayoutOut = {}; + }); - it('should default to lon(lat)range to params non-world scopes', function() { - var scopeDefaults = params.scopeDefaults, - scopes = Object.keys(scopeDefaults), - customLonaxisRange = [-42.21313312, 40.321321], - customLataxisRange = [-42.21313312, 40.321321]; - - var dfltLonaxisRange, dfltLataxisRange; - - scopes.forEach(function(scope) { - if(scope === 'world') return; - - dfltLonaxisRange = scopeDefaults[scope].lonaxisRange; - dfltLataxisRange = scopeDefaults[scope].lataxisRange; - - geoLayoutIn = {}; - geoLayoutOut = {scope: scope}; - - supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); - expect(geoLayoutOut.lonaxis.range).toEqual(dfltLonaxisRange); - expect(geoLayoutOut.lataxis.range).toEqual(dfltLataxisRange); - expect(geoLayoutOut.lonaxis.tick0).toEqual(dfltLonaxisRange[0]); - expect(geoLayoutOut.lataxis.tick0).toEqual(dfltLataxisRange[0]); - - geoLayoutIn = { - lonaxis: {range: customLonaxisRange}, - lataxis: {range: customLataxisRange} - }; - geoLayoutOut = {scope: scope}; - - supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); - expect(geoLayoutOut.lonaxis.range).toEqual(customLonaxisRange); - expect(geoLayoutOut.lataxis.range).toEqual(customLataxisRange); - expect(geoLayoutOut.lonaxis.tick0).toEqual(customLonaxisRange[0]); - expect(geoLayoutOut.lataxis.tick0).toEqual(customLataxisRange[0]); - }); + it( + "should default to lon(lat)range to params non-world scopes", + function() { + var scopeDefaults = params.scopeDefaults, + scopes = Object.keys(scopeDefaults), + customLonaxisRange = [-42.21313312, 40.321321], + customLataxisRange = [-42.21313312, 40.321321]; + + var dfltLonaxisRange, dfltLataxisRange; + + scopes.forEach(function(scope) { + if (scope === "world") return; + + dfltLonaxisRange = scopeDefaults[scope].lonaxisRange; + dfltLataxisRange = scopeDefaults[scope].lataxisRange; + + geoLayoutIn = {}; + geoLayoutOut = { scope: scope }; + + supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); + expect(geoLayoutOut.lonaxis.range).toEqual(dfltLonaxisRange); + expect(geoLayoutOut.lataxis.range).toEqual(dfltLataxisRange); + expect(geoLayoutOut.lonaxis.tick0).toEqual(dfltLonaxisRange[0]); + expect(geoLayoutOut.lataxis.tick0).toEqual(dfltLataxisRange[0]); + + geoLayoutIn = { + lonaxis: { range: customLonaxisRange }, + lataxis: { range: customLataxisRange } + }; + geoLayoutOut = { scope: scope }; + + supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); + expect(geoLayoutOut.lonaxis.range).toEqual(customLonaxisRange); + expect(geoLayoutOut.lataxis.range).toEqual(customLataxisRange); + expect(geoLayoutOut.lonaxis.tick0).toEqual(customLonaxisRange[0]); + expect(geoLayoutOut.lataxis.tick0).toEqual(customLataxisRange[0]); }); + } + ); + + it( + "should adjust default lon(lat)range to projection.rotation in world scopes", + function() { + var expectedLonaxisRange, expectedLataxisRange; + + function testOne() { + supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); + expect(geoLayoutOut.lonaxis.range).toEqual(expectedLonaxisRange); + expect(geoLayoutOut.lataxis.range).toEqual(expectedLataxisRange); + } - it('should adjust default lon(lat)range to projection.rotation in world scopes', function() { - var expectedLonaxisRange, expectedLataxisRange; - - function testOne() { - supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); - expect(geoLayoutOut.lonaxis.range).toEqual(expectedLonaxisRange); - expect(geoLayoutOut.lataxis.range).toEqual(expectedLataxisRange); - } - - geoLayoutIn = {}; - geoLayoutOut = { - scope: 'world', - projection: { - type: 'equirectangular', - rotation: { - lon: -75, - lat: 45 - } - } - }; - expectedLonaxisRange = [-255, 105]; // => -75 +/- 180 - expectedLataxisRange = [-45, 135]; // => 45 +/- 90 - testOne(); - - geoLayoutIn = {}; - geoLayoutOut = { - scope: 'world', - projection: { - type: 'orthographic', - rotation: { - lon: -75, - lat: 45 - } - } - }; - expectedLonaxisRange = [-165, 15]; // => -75 +/- 90 - expectedLataxisRange = [-45, 135]; // => 45 +/- 90 - testOne(); - - geoLayoutIn = { - lonaxis: {range: [-42.21313312, 40.321321]}, - lataxis: {range: [-42.21313312, 40.321321]} - }; - expectedLonaxisRange = [-42.21313312, 40.321321]; - expectedLataxisRange = [-42.21313312, 40.321321]; - testOne(); - }); - }); + geoLayoutIn = {}; + geoLayoutOut = { + scope: "world", + projection: { + type: "equirectangular", + rotation: { lon: -75, lat: 45 } + } + }; + expectedLonaxisRange = [-255, 105]; + // => -75 +/- 180 + expectedLataxisRange = [-45, 135]; + // => 45 +/- 90 + testOne(); + + geoLayoutIn = {}; + geoLayoutOut = { + scope: "world", + projection: { type: "orthographic", rotation: { lon: -75, lat: 45 } } + }; + expectedLonaxisRange = [-165, 15]; + // => -75 +/- 90 + expectedLataxisRange = [-45, 135]; + // => 45 +/- 90 + testOne(); + + geoLayoutIn = { + lonaxis: { range: [-42.21313312, 40.321321] }, + lataxis: { range: [-42.21313312, 40.321321] } + }; + expectedLonaxisRange = [-42.21313312, 40.321321]; + expectedLataxisRange = [-42.21313312, 40.321321]; + testOne(); + } + ); + }); }); -describe('Test Geo layout defaults', function() { - 'use strict'; - - var layoutAttributes = Geo.layoutAttributes; - var supplyLayoutDefaults = Geo.supplyLayoutDefaults; +describe("Test Geo layout defaults", function() { + "use strict"; + var layoutAttributes = Geo.layoutAttributes; + var supplyLayoutDefaults = Geo.supplyLayoutDefaults; - describe('supplyLayoutDefaults', function() { - var layoutIn, layoutOut, fullData; + describe("supplyLayoutDefaults", function() { + var layoutIn, layoutOut, fullData; - beforeEach(function() { - layoutOut = {}; + beforeEach(function() { + layoutOut = {}; - // needs a geo-ref in a trace in order to be detected - fullData = [{ type: 'scattergeo', geo: 'geo' }]; - }); - - var seaFields = [ - 'showcoastlines', 'coastlinecolor', 'coastlinewidth', - 'showocean', 'oceancolor' - ]; - - var subunitFields = [ - 'showsubunits', 'subunitcolor', 'subunitwidth' - ]; - - var frameFields = [ - 'showframe', 'framecolor', 'framewidth' - ]; - - it('should not coerce projection.rotation if type is albers usa', function() { - layoutIn = { - geo: { - projection: { - type: 'albers usa', - rotation: { - lon: 10, - lat: 10 - } - } - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.geo.projection.rotation).toBeUndefined(); - }); + // needs a geo-ref in a trace in order to be detected + fullData = [{ type: "scattergeo", geo: "geo" }]; + }); - it('should not coerce projection.rotation if type is albers usa (converse)', function() { - layoutIn = { - geo: { - projection: { - rotation: { - lon: 10, - lat: 10 - } - } - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.geo.projection.rotation).toBeDefined(); + var seaFields = [ + "showcoastlines", + "coastlinecolor", + "coastlinewidth", + "showocean", + "oceancolor" + ]; + + var subunitFields = ["showsubunits", "subunitcolor", "subunitwidth"]; + + var frameFields = ["showframe", "framecolor", "framewidth"]; + + it( + "should not coerce projection.rotation if type is albers usa", + function() { + layoutIn = { + geo: { + projection: { type: "albers usa", rotation: { lon: 10, lat: 10 } } + } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.geo.projection.rotation).toBeUndefined(); + } + ); + + it( + "should not coerce projection.rotation if type is albers usa (converse)", + function() { + layoutIn = { geo: { projection: { rotation: { lon: 10, lat: 10 } } } }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.geo.projection.rotation).toBeDefined(); + } + ); + + it( + "should not coerce coastlines and ocean if type is albers usa", + function() { + layoutIn = { + geo: { + projection: { type: "albers usa" }, + showcoastlines: true, + showocean: true + } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + seaFields.forEach(function(field) { + expect(layoutOut.geo[field]).toBeUndefined(); }); + } + ); - it('should not coerce coastlines and ocean if type is albers usa', function() { - layoutIn = { - geo: { - projection: { - type: 'albers usa' - }, - showcoastlines: true, - showocean: true - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - seaFields.forEach(function(field) { - expect(layoutOut.geo[field]).toBeUndefined(); - }); - }); + it( + "should not coerce coastlines and ocean if type is albers usa (converse)", + function() { + layoutIn = { geo: { showcoastlines: true, showocean: true } }; - it('should not coerce coastlines and ocean if type is albers usa (converse)', function() { - layoutIn = { - geo: { - showcoastlines: true, - showocean: true - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - seaFields.forEach(function(field) { - expect(layoutOut.geo[field]).toBeDefined(); - }); - }); - - it('should not coerce projection.parallels if type is conic', function() { - var projTypes = layoutAttributes.projection.type.values; - - function testOne(projType) { - layoutIn = { - geo: { - projection: { - type: projType, - parallels: [10, 10] - } - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - } - - projTypes.forEach(function(projType) { - testOne(projType); - if(projType.indexOf('conic') !== -1) { - expect(layoutOut.geo.projection.parallels).toBeDefined(); - } - else { - expect(layoutOut.geo.projection.parallels).toBeUndefined(); - } - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + seaFields.forEach(function(field) { + expect(layoutOut.geo[field]).toBeDefined(); }); + } + ); + + it("should not coerce projection.parallels if type is conic", function() { + var projTypes = layoutAttributes.projection.type.values; + + function testOne(projType) { + layoutIn = { + geo: { projection: { type: projType, parallels: [10, 10] } } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + } + + projTypes.forEach(function(projType) { + testOne(projType); + if (projType.indexOf("conic") !== -1) { + expect(layoutOut.geo.projection.parallels).toBeDefined(); + } else { + expect(layoutOut.geo.projection.parallels).toBeUndefined(); + } + }); + }); - it('should coerce subunits only when available (usa case)', function() { - layoutIn = { - geo: { scope: 'usa' } - }; + it("should coerce subunits only when available (usa case)", function() { + layoutIn = { geo: { scope: "usa" } }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - subunitFields.forEach(function(field) { - expect(layoutOut.geo[field]).toBeDefined(); - }); - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + subunitFields.forEach(function(field) { + expect(layoutOut.geo[field]).toBeDefined(); + }); + }); - it('should coerce subunits only when available (default case)', function() { - layoutIn = { geo: {} }; + it("should coerce subunits only when available (default case)", function() { + layoutIn = { geo: {} }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - subunitFields.forEach(function(field) { - expect(layoutOut.geo[field]).toBeUndefined(); - }); - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + subunitFields.forEach(function(field) { + expect(layoutOut.geo[field]).toBeUndefined(); + }); + }); - it('should coerce subunits only when available (NA case)', function() { - layoutIn = { - geo: { - scope: 'north america', - resolution: 50 - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - subunitFields.forEach(function(field) { - expect(layoutOut.geo[field]).toBeDefined(); - }); - }); + it("should coerce subunits only when available (NA case)", function() { + layoutIn = { geo: { scope: "north america", resolution: 50 } }; - it('should coerce subunits only when available (NA case 2)', function() { - layoutIn = { - geo: { - scope: 'north america', - resolution: '50' - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - subunitFields.forEach(function(field) { - expect(layoutOut.geo[field]).toBeDefined(); - }); - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + subunitFields.forEach(function(field) { + expect(layoutOut.geo[field]).toBeDefined(); + }); + }); - it('should coerce subunits only when available (NA case 2)', function() { - layoutIn = { - geo: { - scope: 'north america' - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - subunitFields.forEach(function(field) { - expect(layoutOut.geo[field]).toBeUndefined(); - }); - }); + it("should coerce subunits only when available (NA case 2)", function() { + layoutIn = { geo: { scope: "north america", resolution: "50" } }; - it('should not coerce frame unless for world scope', function() { - var scopes = layoutAttributes.scope.values; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + subunitFields.forEach(function(field) { + expect(layoutOut.geo[field]).toBeDefined(); + }); + }); - function testOne(scope) { - layoutIn = { - geo: { scope: scope } - }; + it("should coerce subunits only when available (NA case 2)", function() { + layoutIn = { geo: { scope: "north america" } }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - } + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + subunitFields.forEach(function(field) { + expect(layoutOut.geo[field]).toBeUndefined(); + }); + }); - scopes.forEach(function(scope) { - testOne(scope); - if(scope === 'world') { - frameFields.forEach(function(field) { - expect(layoutOut.geo[field]).toBeDefined(); - }); - } - else { - frameFields.forEach(function(field) { - expect(layoutOut.geo[field]).toBeUndefined(); - }); - } - }); - }); + it("should not coerce frame unless for world scope", function() { + var scopes = layoutAttributes.scope.values; + + function testOne(scope) { + layoutIn = { geo: { scope: scope } }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + } + + scopes.forEach(function(scope) { + testOne(scope); + if (scope === "world") { + frameFields.forEach(function(field) { + expect(layoutOut.geo[field]).toBeDefined(); + }); + } else { + frameFields.forEach(function(field) { + expect(layoutOut.geo[field]).toBeUndefined(); + }); + } + }); + }); - it('should add geo data-only geos into layoutIn', function() { - layoutIn = {}; - fullData = [{ type: 'scattergeo', geo: 'geo' }]; + it("should add geo data-only geos into layoutIn", function() { + layoutIn = {}; + fullData = [{ type: "scattergeo", geo: "geo" }]; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutIn.geo).toEqual({}); - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn.geo).toEqual({}); + }); - it('should add geo data-only geos into layoutIn (converse)', function() { - layoutIn = {}; - fullData = [{ type: 'scatter' }]; + it("should add geo data-only geos into layoutIn (converse)", function() { + layoutIn = {}; + fullData = [{ type: "scatter" }]; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutIn.geo).toBe(undefined); - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn.geo).toBe(undefined); }); + }); }); -describe('geojson / topojson utils', function() { - 'use strict'; +describe("geojson / topojson utils", function() { + "use strict"; + function _locationToFeature(topojson, loc, locationmode) { + var trace = { locationmode: locationmode }; + var features = topojsonUtils.getTopojsonFeatures(trace, topojson); + + var feature = geoLocationUtils.locationToFeature( + locationmode, + loc, + features + ); + return feature; + } + + describe( + "should be able to extract topojson feature from *locations* items", + function() { + var topojsonName = "world_110m"; + var topojson = GeoAssets.topojson[topojsonName]; + + it("with *ISO-3* locationmode", function() { + var out = _locationToFeature(topojson, "CAN", "ISO-3"); + + expect(Object.keys(out)).toEqual([ + "type", + "id", + "properties", + "geometry" + ]); + expect(out.id).toEqual("CAN"); + }); + + it("with *ISO-3* locationmode (not-found case)", function() { + var out = _locationToFeature(topojson, "XXX", "ISO-3"); + + expect(out).toEqual(false); + }); + + it("with *country names* locationmode", function() { + var out = _locationToFeature( + topojson, + "United States", + "country names" + ); + + expect(Object.keys(out)).toEqual([ + "type", + "id", + "properties", + "geometry" + ]); + expect(out.id).toEqual("USA"); + }); + + it("with *country names* locationmode (not-found case)", function() { + var out = _locationToFeature(topojson, "XXX", "country names"); + + expect(out).toEqual(false); + }); + } + ); +}); - function _locationToFeature(topojson, loc, locationmode) { - var trace = { locationmode: locationmode }; - var features = topojsonUtils.getTopojsonFeatures(trace, topojson); +describe("Test geo interactions", function() { + "use strict"; + afterEach(destroyGraphDiv); - var feature = geoLocationUtils.locationToFeature(locationmode, loc, features); - return feature; - } + describe("mock geo_first.json", function() { + var mock = require("@mocks/geo_first.json"); + var gd; - describe('should be able to extract topojson feature from *locations* items', function() { - var topojsonName = 'world_110m'; - var topojson = GeoAssets.topojson[topojsonName]; + function mouseEventScatterGeo(type) { + mouseEvent(type, 300, 235); + } - it('with *ISO-3* locationmode', function() { - var out = _locationToFeature(topojson, 'CAN', 'ISO-3'); + function mouseEventChoropleth(type) { + mouseEvent(type, 400, 160); + } - expect(Object.keys(out)).toEqual(['type', 'id', 'properties', 'geometry']); - expect(out.id).toEqual('CAN'); - }); + function countTraces(type) { + return d3.selectAll("g.trace." + type).size(); + } - it('with *ISO-3* locationmode (not-found case)', function() { - var out = _locationToFeature(topojson, 'XXX', 'ISO-3'); + function countGeos() { + return d3.select("div.geo-container").selectAll("div").size(); + } - expect(out).toEqual(false); - }); + function countColorBars() { + return d3.select("g.infolayer").selectAll(".cbbg").size(); + } - it('with *country names* locationmode', function() { - var out = _locationToFeature(topojson, 'United States', 'country names'); + beforeEach(function(done) { + gd = createGraphDiv(); - expect(Object.keys(out)).toEqual(['type', 'id', 'properties', 'geometry']); - expect(out.id).toEqual('USA'); - }); + var mockCopy = Lib.extendDeep({}, mock); - it('with *country names* locationmode (not-found case)', function() { - var out = _locationToFeature(topojson, 'XXX', 'country names'); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - expect(out).toEqual(false); - }); + describe("scattergeo hover labels", function() { + it("should show one hover text group", function() { + mouseEventScatterGeo("mousemove"); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); + }); + + it("should show longitude and latitude values", function() { + mouseEventScatterGeo("mousemove"); + + var node = d3.selectAll("g.hovertext").selectAll("tspan")[0][0]; + expect(node.innerHTML).toEqual("(0\xB0, 0\xB0)"); + }); + + it("should show the trace name", function() { + mouseEventScatterGeo("mousemove"); + + var node = d3.selectAll("g.hovertext").selectAll("text")[0][0]; + expect(node.innerHTML).toEqual("trace 0"); + }); + + it("should show *text* (case 1)", function(done) { + Plotly.restyle(gd, "text", [["A", "B"]]) + .then(function() { + mouseEventScatterGeo("mousemove"); + + var node = d3.selectAll("g.hovertext").selectAll("tspan")[0][1]; + expect(node.innerHTML).toEqual("A"); + }) + .then(done); + }); + + it("should show *text* (case 2)", function(done) { + Plotly.restyle(gd, "text", [[null, "B"]]) + .then(function() { + mouseEventScatterGeo("mousemove"); + + var node = d3.selectAll("g.hovertext").selectAll("tspan")[0][1]; + expect(node).toBeUndefined(); + }) + .then(done); + }); + + it("should show *text* (case 3)", function(done) { + Plotly.restyle(gd, "text", [["", "B"]]) + .then(function() { + mouseEventScatterGeo("mousemove"); + + var node = d3.selectAll("g.hovertext").selectAll("tspan")[0][1]; + expect(node).toBeUndefined(); + }) + .then(done); + }); }); -}); -describe('Test geo interactions', function() { - 'use strict'; + describe("scattergeo hover events", function() { + var ptData, cnt; - afterEach(destroyGraphDiv); + beforeEach(function() { + cnt = 0; - describe('mock geo_first.json', function() { - var mock = require('@mocks/geo_first.json'); - var gd; + gd.on("plotly_hover", function(eventData) { + ptData = eventData.points[0]; + cnt++; + }); - function mouseEventScatterGeo(type) { - mouseEvent(type, 300, 235); - } + mouseEventScatterGeo("mousemove"); + }); + + it("should contain the correct fields", function() { + expect(Object.keys(ptData)).toEqual([ + "data", + "fullData", + "curveNumber", + "pointNumber", + "lon", + "lat", + "location" + ]); + expect(cnt).toEqual(1); + }); + + it("should show the correct point data", function() { + expect(ptData.lon).toEqual(0); + expect(ptData.lat).toEqual(0); + expect(ptData.location).toBe(null); + expect(ptData.curveNumber).toEqual(0); + expect(ptData.pointNumber).toEqual(0); + expect(cnt).toEqual(1); + }); + + it( + "should not be triggered when pt over on the other side of the globe", + function(done) { + var update = { + "geo.projection.type": "orthographic", + "geo.projection.rotation": { lon: 82, lat: -19 } + }; + + Plotly.relayout(gd, update).then(function() { + setTimeout( + function() { + mouseEvent("mousemove", 288, 170); - function mouseEventChoropleth(type) { - mouseEvent(type, 400, 160); - } + expect(cnt).toEqual(1); - function countTraces(type) { - return d3.selectAll('g.trace.' + type).size(); + done(); + }, + HOVERMINTIME + 10 + ); + }); } + ); - function countGeos() { - return d3.select('div.geo-container').selectAll('div').size(); - } + it( + "should not be triggered when pt *location* does not have matching feature", + function(done) { + var update = { locations: [["CAN", "AAA", "USA"]] }; - function countColorBars() { - return d3.select('g.infolayer').selectAll('.cbbg').size(); - } + Plotly.restyle(gd, update).then(function() { + setTimeout( + function() { + mouseEvent("mousemove", 300, 230); - beforeEach(function(done) { - gd = createGraphDiv(); + expect(cnt).toEqual(1); - var mockCopy = Lib.extendDeep({}, mock); + done(); + }, + HOVERMINTIME + 10 + ); + }); + } + ); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); + describe("scattergeo click events", function() { + var ptData; - describe('scattergeo hover labels', function() { - it('should show one hover text group', function() { - mouseEventScatterGeo('mousemove'); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - }); - - it('should show longitude and latitude values', function() { - mouseEventScatterGeo('mousemove'); - - var node = d3.selectAll('g.hovertext').selectAll('tspan')[0][0]; - expect(node.innerHTML).toEqual('(0°, 0°)'); - }); - - it('should show the trace name', function() { - mouseEventScatterGeo('mousemove'); - - var node = d3.selectAll('g.hovertext').selectAll('text')[0][0]; - expect(node.innerHTML).toEqual('trace 0'); - }); - - it('should show *text* (case 1)', function(done) { - Plotly.restyle(gd, 'text', [['A', 'B']]).then(function() { - mouseEventScatterGeo('mousemove'); - - var node = d3.selectAll('g.hovertext').selectAll('tspan')[0][1]; - expect(node.innerHTML).toEqual('A'); - }) - .then(done); - }); - - it('should show *text* (case 2)', function(done) { - Plotly.restyle(gd, 'text', [[null, 'B']]).then(function() { - mouseEventScatterGeo('mousemove'); - - var node = d3.selectAll('g.hovertext').selectAll('tspan')[0][1]; - expect(node).toBeUndefined(); - }) - .then(done); - }); - - it('should show *text* (case 3)', function(done) { - Plotly.restyle(gd, 'text', [['', 'B']]).then(function() { - mouseEventScatterGeo('mousemove'); - - var node = d3.selectAll('g.hovertext').selectAll('tspan')[0][1]; - expect(node).toBeUndefined(); - }) - .then(done); - }); + beforeEach(function() { + gd.on("plotly_click", function(eventData) { + ptData = eventData.points[0]; }); - describe('scattergeo hover events', function() { - var ptData, cnt; - - beforeEach(function() { - cnt = 0; - - gd.on('plotly_hover', function(eventData) { - ptData = eventData.points[0]; - cnt++; - }); + mouseEventScatterGeo("mousemove"); + mouseEventScatterGeo("click"); + }); + + it("should contain the correct fields", function() { + expect(Object.keys(ptData)).toEqual([ + "data", + "fullData", + "curveNumber", + "pointNumber", + "lon", + "lat", + "location" + ]); + }); + + it("should show the correct point data", function() { + expect(ptData.lon).toEqual(0); + expect(ptData.lat).toEqual(0); + expect(ptData.location).toBe(null); + expect(ptData.curveNumber).toEqual(0); + expect(ptData.pointNumber).toEqual(0); + }); + }); - mouseEventScatterGeo('mousemove'); - }); + describe("scattergeo unhover events", function() { + var ptData; - it('should contain the correct fields', function() { - expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'lon', 'lat', 'location' - ]); - expect(cnt).toEqual(1); - }); - - it('should show the correct point data', function() { - expect(ptData.lon).toEqual(0); - expect(ptData.lat).toEqual(0); - expect(ptData.location).toBe(null); - expect(ptData.curveNumber).toEqual(0); - expect(ptData.pointNumber).toEqual(0); - expect(cnt).toEqual(1); - }); - - it('should not be triggered when pt over on the other side of the globe', function(done) { - var update = { - 'geo.projection.type': 'orthographic', - 'geo.projection.rotation': { lon: 82, lat: -19 } - }; + beforeEach(function(done) { + gd.on("plotly_unhover", function(eventData) { + ptData = eventData.points[0]; + }); - Plotly.relayout(gd, update).then(function() { - setTimeout(function() { - mouseEvent('mousemove', 288, 170); + mouseEventScatterGeo("mousemove"); + setTimeout( + function() { + mouseEvent("mousemove", 400, 200); + done(); + }, + HOVERMINTIME + 10 + ); + }); + + it("should contain the correct fields", function() { + expect(Object.keys(ptData)).toEqual([ + "data", + "fullData", + "curveNumber", + "pointNumber", + "lon", + "lat", + "location" + ]); + }); + + it("should show the correct point data", function() { + expect(ptData.lon).toEqual(0); + expect(ptData.lat).toEqual(0); + expect(ptData.location).toBe(null); + expect(ptData.curveNumber).toEqual(0); + expect(ptData.pointNumber).toEqual(0); + }); + }); - expect(cnt).toEqual(1); + describe("choropleth hover labels", function() { + beforeEach(function() { + mouseEventChoropleth("mouseover"); + }); - done(); - }, HOVERMINTIME + 10); - }); - }); + it("should show one hover text group", function() { + expect(d3.selectAll("g.hovertext").size()).toEqual(1); + }); - it('should not be triggered when pt *location* does not have matching feature', function(done) { - var update = { - 'locations': [['CAN', 'AAA', 'USA']] - }; + it("should show location and z values", function() { + var node = d3.selectAll("g.hovertext").selectAll("tspan")[0]; - Plotly.restyle(gd, update).then(function() { - setTimeout(function() { mouseEvent('mousemove', 300, 230); + expect(node[0].innerHTML).toEqual("RUS"); + expect(node[1].innerHTML).toEqual("10"); + }); - expect(cnt).toEqual(1); + it("should show the trace name", function() { + var node = d3.selectAll("g.hovertext").selectAll("text")[0][0]; - done(); - }, HOVERMINTIME + 10); - }); - }); - }); + expect(node.innerHTML).toEqual("trace 1"); + }); + }); - describe('scattergeo click events', function() { - var ptData; - - beforeEach(function() { - gd.on('plotly_click', function(eventData) { - ptData = eventData.points[0]; - }); - - mouseEventScatterGeo('mousemove'); - mouseEventScatterGeo('click'); - }); - - it('should contain the correct fields', function() { - expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'lon', 'lat', 'location' - ]); - }); - - it('should show the correct point data', function() { - expect(ptData.lon).toEqual(0); - expect(ptData.lat).toEqual(0); - expect(ptData.location).toBe(null); - expect(ptData.curveNumber).toEqual(0); - expect(ptData.pointNumber).toEqual(0); - }); - }); + describe("choropleth hover events", function() { + var ptData; - describe('scattergeo unhover events', function() { - var ptData; - - beforeEach(function(done) { - gd.on('plotly_unhover', function(eventData) { - ptData = eventData.points[0]; - }); - - mouseEventScatterGeo('mousemove'); - setTimeout(function() { - mouseEvent('mousemove', 400, 200); - done(); - }, HOVERMINTIME + 10); - }); - - it('should contain the correct fields', function() { - expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'lon', 'lat', 'location' - ]); - }); - - it('should show the correct point data', function() { - expect(ptData.lon).toEqual(0); - expect(ptData.lat).toEqual(0); - expect(ptData.location).toBe(null); - expect(ptData.curveNumber).toEqual(0); - expect(ptData.pointNumber).toEqual(0); - }); + beforeEach(function() { + gd.on("plotly_hover", function(eventData) { + ptData = eventData.points[0]; }); - describe('choropleth hover labels', function() { - beforeEach(function() { - mouseEventChoropleth('mouseover'); - }); - - it('should show one hover text group', function() { - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - }); - - it('should show location and z values', function() { - var node = d3.selectAll('g.hovertext').selectAll('tspan')[0]; - - expect(node[0].innerHTML).toEqual('RUS'); - expect(node[1].innerHTML).toEqual('10'); - }); + mouseEventChoropleth("mouseover"); + }); + + it("should contain the correct fields", function() { + expect(Object.keys(ptData)).toEqual([ + "data", + "fullData", + "curveNumber", + "pointNumber", + "location", + "z" + ]); + }); + + it("should show the correct point data", function() { + expect(ptData.location).toBe("RUS"); + expect(ptData.z).toEqual(10); + expect(ptData.curveNumber).toEqual(1); + expect(ptData.pointNumber).toEqual(2); + }); + }); - it('should show the trace name', function() { - var node = d3.selectAll('g.hovertext').selectAll('text')[0][0]; + describe("choropleth click events", function() { + var ptData; - expect(node.innerHTML).toEqual('trace 1'); - }); + beforeEach(function() { + gd.on("plotly_click", function(eventData) { + ptData = eventData.points[0]; }); - describe('choropleth hover events', function() { - var ptData; - - beforeEach(function() { - gd.on('plotly_hover', function(eventData) { - ptData = eventData.points[0]; - }); - - mouseEventChoropleth('mouseover'); - }); - - it('should contain the correct fields', function() { - expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'location', 'z' - ]); - }); - - it('should show the correct point data', function() { - expect(ptData.location).toBe('RUS'); - expect(ptData.z).toEqual(10); - expect(ptData.curveNumber).toEqual(1); - expect(ptData.pointNumber).toEqual(2); - }); - }); + mouseEventChoropleth("click"); + }); + + it("should contain the correct fields", function() { + expect(Object.keys(ptData)).toEqual([ + "data", + "fullData", + "curveNumber", + "pointNumber", + "location", + "z" + ]); + }); + + it("should show the correct point data", function() { + expect(ptData.location).toBe("RUS"); + expect(ptData.z).toEqual(10); + expect(ptData.curveNumber).toEqual(1); + expect(ptData.pointNumber).toEqual(2); + }); + }); - describe('choropleth click events', function() { - var ptData; - - beforeEach(function() { - gd.on('plotly_click', function(eventData) { - ptData = eventData.points[0]; - }); - - mouseEventChoropleth('click'); - }); - - it('should contain the correct fields', function() { - expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'location', 'z' - ]); - }); - - it('should show the correct point data', function() { - expect(ptData.location).toBe('RUS'); - expect(ptData.z).toEqual(10); - expect(ptData.curveNumber).toEqual(1); - expect(ptData.pointNumber).toEqual(2); - }); - }); + describe("choropleth unhover events", function() { + var ptData; - describe('choropleth unhover events', function() { - var ptData; - - beforeEach(function() { - gd.on('plotly_unhover', function(eventData) { - ptData = eventData.points[0]; - }); - - mouseEventChoropleth('mouseover'); - mouseEventChoropleth('mouseout'); - }); - - it('should contain the correct fields', function() { - expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'location', 'z' - ]); - }); - - it('should show the correct point data', function() { - expect(ptData.location).toBe('RUS'); - expect(ptData.z).toEqual(10); - expect(ptData.curveNumber).toEqual(1); - expect(ptData.pointNumber).toEqual(2); - }); + beforeEach(function() { + gd.on("plotly_unhover", function(eventData) { + ptData = eventData.points[0]; }); - describe('trace visibility toggle', function() { - it('should toggle scattergeo elements', function(done) { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - - Plotly.restyle(gd, 'visible', false, [0]).then(function() { - expect(countTraces('scattergeo')).toBe(0); - expect(countTraces('choropleth')).toBe(1); - - return Plotly.restyle(gd, 'visible', true, [0]); - }).then(function() { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - - done(); - }); - }); - - it('should toggle choropleth elements', function(done) { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - - Plotly.restyle(gd, 'visible', false, [1]).then(function() { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(0); - - return Plotly.restyle(gd, 'visible', true, [1]); - }).then(function() { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); + mouseEventChoropleth("mouseover"); + mouseEventChoropleth("mouseout"); + }); + + it("should contain the correct fields", function() { + expect(Object.keys(ptData)).toEqual([ + "data", + "fullData", + "curveNumber", + "pointNumber", + "location", + "z" + ]); + }); + + it("should show the correct point data", function() { + expect(ptData.location).toBe("RUS"); + expect(ptData.z).toEqual(10); + expect(ptData.curveNumber).toEqual(1); + expect(ptData.pointNumber).toEqual(2); + }); + }); - done(); - }); - }); + describe("trace visibility toggle", function() { + it("should toggle scattergeo elements", function(done) { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(1); + + Plotly.restyle(gd, "visible", false, [0]) + .then(function() { + expect(countTraces("scattergeo")).toBe(0); + expect(countTraces("choropleth")).toBe(1); + + return Plotly.restyle(gd, "visible", true, [0]); + }) + .then(function() { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(1); + + done(); + }); + }); + + it("should toggle choropleth elements", function(done) { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(1); + + Plotly.restyle(gd, "visible", false, [1]) + .then(function() { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(0); + + return Plotly.restyle(gd, "visible", true, [1]); + }) + .then(function() { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(1); + + done(); + }); + }); + }); - }); + describe("deleting traces and geos", function() { + it("should delete traces in succession", function(done) { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(1); + expect(countGeos()).toBe(1); + expect(countColorBars()).toBe(1); + + Plotly.deleteTraces(gd, [0]) + .then(function() { + expect(countTraces("scattergeo")).toBe(0); + expect(countTraces("choropleth")).toBe(1); + expect(countGeos()).toBe(1); + expect(countColorBars()).toBe(1); + + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + expect(countTraces("scattergeo")).toBe(0); + expect(countTraces("choropleth")).toBe(0); + expect(countGeos()).toBe(0, "- trace-less geo subplot are deleted"); + expect(countColorBars()).toBe(0); + + return Plotly.relayout(gd, "geo", null); + }) + .then(function() { + expect(countTraces("scattergeo")).toBe(0); + expect(countTraces("choropleth")).toBe(0); + expect(countGeos()).toBe(0); + expect(countColorBars()).toBe(0); + + done(); + }); + }); + }); - describe('deleting traces and geos', function() { - it('should delete traces in succession', function(done) { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - expect(countGeos()).toBe(1); - expect(countColorBars()).toBe(1); - - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countTraces('scattergeo')).toBe(0); - expect(countTraces('choropleth')).toBe(1); - expect(countGeos()).toBe(1); - expect(countColorBars()).toBe(1); - - return Plotly.deleteTraces(gd, [0]); - }).then(function() { - expect(countTraces('scattergeo')).toBe(0); - expect(countTraces('choropleth')).toBe(0); - expect(countGeos()).toBe(0, '- trace-less geo subplot are deleted'); - expect(countColorBars()).toBe(0); - - return Plotly.relayout(gd, 'geo', null); - }).then(function() { - expect(countTraces('scattergeo')).toBe(0); - expect(countTraces('choropleth')).toBe(0); - expect(countGeos()).toBe(0); - expect(countColorBars()).toBe(0); - - done(); - }); - }); + describe("streaming calls", function() { + var INTERVAL = 10; + + var N_MARKERS_AT_START = Math.min( + mock.data[0].lat.length, + mock.data[0].lon.length + ); + + var N_LOCATIONS_AT_START = mock.data[1].locations.length; + + var lonQueue = [45, -45, 12, 20], + latQueue = [-75, 80, 5, 10], + textQueue = ["c", "d", "e", "f"], + locationsQueue = ["AUS", "FRA", "DEU", "MEX"], + zQueue = [100, 20, 30, 12]; + + beforeEach(function(done) { + var update = { + mode: "lines+markers+text", + text: [["a", "b"]], + "marker.size": 10 + }; + + Plotly.restyle(gd, update, [0]).then(done); + }); + + function countScatterGeoLines() { + return d3 + .selectAll("g.trace.scattergeo") + .selectAll("path.js-line") + .size(); + } + + function countScatterGeoMarkers() { + return d3 + .selectAll("g.trace.scattergeo") + .selectAll("path.point") + .size(); + } + + function countScatterGeoTextGroups() { + return d3.selectAll("g.trace.scattergeo").selectAll("g").size(); + } + + function countScatterGeoTextNodes() { + return d3 + .selectAll("g.trace.scattergeo") + .selectAll("g") + .select("text") + .size(); + } + + function checkScatterGeoOrder() { + var order = ["js-path", "point", null]; + var nodes = d3.selectAll("g.trace.scattergeo"); + + nodes.each(function() { + var list = []; + + d3.select(this).selectAll("*").each(function() { + var className = d3.select(this).attr("class"); + list.push(className); + }); + + var listSorted = list.slice().sort(function(a, b) { + return order.indexOf(a) - order.indexOf(b); + }); + + expect(list).toEqual(listSorted); }); - - describe('streaming calls', function() { - var INTERVAL = 10; - - var N_MARKERS_AT_START = Math.min( - mock.data[0].lat.length, - mock.data[0].lon.length - ); - - var N_LOCATIONS_AT_START = mock.data[1].locations.length; - - var lonQueue = [45, -45, 12, 20], - latQueue = [-75, 80, 5, 10], - textQueue = ['c', 'd', 'e', 'f'], - locationsQueue = ['AUS', 'FRA', 'DEU', 'MEX'], - zQueue = [100, 20, 30, 12]; - - beforeEach(function(done) { - var update = { - mode: 'lines+markers+text', - text: [['a', 'b']], - 'marker.size': 10 - }; - - Plotly.restyle(gd, update, [0]).then(done); - }); - - function countScatterGeoLines() { - return d3.selectAll('g.trace.scattergeo') - .selectAll('path.js-line') - .size(); - } - - function countScatterGeoMarkers() { - return d3.selectAll('g.trace.scattergeo') - .selectAll('path.point') - .size(); + } + + function countChoroplethPaths() { + return d3 + .selectAll("g.trace.choropleth") + .selectAll("path.choroplethlocation") + .size(); + } + + it("should be able to add line/marker/text nodes", function(done) { + var i = 0; + + var interval = setInterval( + function() { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(1); + expect(countScatterGeoLines()).toBe(1); + expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START + i); + expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START + i); + expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START + i); + checkScatterGeoOrder(); + + var trace = gd.data[0]; + trace.lon.push(lonQueue[i]); + trace.lat.push(latQueue[i]); + trace.text.push(textQueue[i]); + + if (i === lonQueue.length - 1) { + clearInterval(interval); + done(); } - function countScatterGeoTextGroups() { - return d3.selectAll('g.trace.scattergeo') - .selectAll('g') - .size(); + gd.calcdata = undefined; + Plotly.plot(gd); + i++; + }, + INTERVAL + ); + }); + + it("should be able to shift line/marker/text nodes", function(done) { + var i = 0; + + var interval = setInterval( + function() { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(1); + expect(countScatterGeoLines()).toBe(1); + expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START); + expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START); + expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START); + checkScatterGeoOrder(); + + var trace = gd.data[0]; + trace.lon.push(lonQueue[i]); + trace.lat.push(latQueue[i]); + trace.text.push(textQueue[i]); + trace.lon.shift(); + trace.lat.shift(); + trace.text.shift(); + + if (i === lonQueue.length - 1) { + clearInterval(interval); + done(); } - function countScatterGeoTextNodes() { - return d3.selectAll('g.trace.scattergeo') - .selectAll('g') - .select('text') - .size(); + gd.calcdata = undefined; + Plotly.plot(gd); + i++; + }, + INTERVAL + ); + }); + + it("should be able to update line/marker/text nodes", function(done) { + var i = 0; + + var interval = setInterval( + function() { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(1); + expect(countScatterGeoLines()).toBe(1); + expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START); + expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START); + expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START); + checkScatterGeoOrder(); + + var trace = gd.data[0]; + trace.lon.push(lonQueue[i]); + trace.lat.push(latQueue[i]); + trace.text.push(textQueue[i]); + trace.lon.shift(); + trace.lat.shift(); + trace.text.shift(); + + if (i === lonQueue.length - 1) { + clearInterval(interval); + done(); } - function checkScatterGeoOrder() { - var order = ['js-path', 'point', null]; - var nodes = d3.selectAll('g.trace.scattergeo'); - - nodes.each(function() { - var list = []; - - d3.select(this).selectAll('*').each(function() { - var className = d3.select(this).attr('class'); - list.push(className); - }); - - var listSorted = list.slice().sort(function(a, b) { - return order.indexOf(a) - order.indexOf(b); - }); - - expect(list).toEqual(listSorted); - }); - } - - function countChoroplethPaths() { - return d3.selectAll('g.trace.choropleth') - .selectAll('path.choroplethlocation') - .size(); - } - - it('should be able to add line/marker/text nodes', function(done) { - var i = 0; - - var interval = setInterval(function() { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - expect(countScatterGeoLines()).toBe(1); - expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START + i); - expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START + i); - expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START + i); - checkScatterGeoOrder(); - - var trace = gd.data[0]; - trace.lon.push(lonQueue[i]); - trace.lat.push(latQueue[i]); - trace.text.push(textQueue[i]); - - if(i === lonQueue.length - 1) { - clearInterval(interval); - done(); - } - - gd.calcdata = undefined; - Plotly.plot(gd); - i++; - }, INTERVAL); - }); - - it('should be able to shift line/marker/text nodes', function(done) { - var i = 0; - - var interval = setInterval(function() { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - expect(countScatterGeoLines()).toBe(1); - expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START); - expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START); - expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START); - checkScatterGeoOrder(); - - var trace = gd.data[0]; - trace.lon.push(lonQueue[i]); - trace.lat.push(latQueue[i]); - trace.text.push(textQueue[i]); - trace.lon.shift(); - trace.lat.shift(); - trace.text.shift(); - - if(i === lonQueue.length - 1) { - clearInterval(interval); - done(); - } - - gd.calcdata = undefined; - Plotly.plot(gd); - i++; - }, INTERVAL); - }); - - it('should be able to update line/marker/text nodes', function(done) { - var i = 0; - - var interval = setInterval(function() { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - expect(countScatterGeoLines()).toBe(1); - expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START); - expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START); - expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START); - checkScatterGeoOrder(); - - var trace = gd.data[0]; - trace.lon.push(lonQueue[i]); - trace.lat.push(latQueue[i]); - trace.text.push(textQueue[i]); - trace.lon.shift(); - trace.lat.shift(); - trace.text.shift(); - - if(i === lonQueue.length - 1) { - clearInterval(interval); - done(); - } - - gd.calcdata = undefined; - Plotly.plot(gd); - i++; - }, INTERVAL); - }); - - it('should be able to delete line/marker/text nodes and choropleth paths', function(done) { - var trace0 = gd.data[0]; - trace0.lon.shift(); - trace0.lat.shift(); - trace0.text.shift(); - - var trace1 = gd.data[1]; - trace1.locations.shift(); - - gd.calcdata = undefined; - Plotly.plot(gd).then(function() { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - - expect(countScatterGeoLines()).toBe(1); - expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START - 1); - expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START - 1); - expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START - 1); - checkScatterGeoOrder(); - - expect(countChoroplethPaths()).toBe(N_LOCATIONS_AT_START - 1); - - done(); - }); - }); - - it('should be able to update line/marker/text nodes and choropleth paths', function(done) { - var trace0 = gd.data[0]; - trace0.lon = lonQueue; - trace0.lat = latQueue; - trace0.text = textQueue; - - var trace1 = gd.data[1]; - trace1.locations = locationsQueue; - trace1.z = zQueue; - - gd.calcdata = undefined; - Plotly.plot(gd).then(function() { - expect(countTraces('scattergeo')).toBe(1); - expect(countTraces('choropleth')).toBe(1); - - expect(countScatterGeoLines()).toBe(1); - expect(countScatterGeoMarkers()).toBe(lonQueue.length); - expect(countScatterGeoTextGroups()).toBe(textQueue.length); - expect(countScatterGeoTextNodes()).toBe(textQueue.length); - checkScatterGeoOrder(); - - expect(countChoroplethPaths()).toBe(locationsQueue.length); - - done(); - }); - }); - - }); + gd.calcdata = undefined; + Plotly.plot(gd); + i++; + }, + INTERVAL + ); + }); + + it( + "should be able to delete line/marker/text nodes and choropleth paths", + function(done) { + var trace0 = gd.data[0]; + trace0.lon.shift(); + trace0.lat.shift(); + trace0.text.shift(); + + var trace1 = gd.data[1]; + trace1.locations.shift(); + + gd.calcdata = undefined; + Plotly.plot(gd).then(function() { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(1); + + expect(countScatterGeoLines()).toBe(1); + expect(countScatterGeoMarkers()).toBe(N_MARKERS_AT_START - 1); + expect(countScatterGeoTextGroups()).toBe(N_MARKERS_AT_START - 1); + expect(countScatterGeoTextNodes()).toBe(N_MARKERS_AT_START - 1); + checkScatterGeoOrder(); + + expect(countChoroplethPaths()).toBe(N_LOCATIONS_AT_START - 1); + + done(); + }); + } + ); + + it( + "should be able to update line/marker/text nodes and choropleth paths", + function(done) { + var trace0 = gd.data[0]; + trace0.lon = lonQueue; + trace0.lat = latQueue; + trace0.text = textQueue; + + var trace1 = gd.data[1]; + trace1.locations = locationsQueue; + trace1.z = zQueue; + + gd.calcdata = undefined; + Plotly.plot(gd).then(function() { + expect(countTraces("scattergeo")).toBe(1); + expect(countTraces("choropleth")).toBe(1); + + expect(countScatterGeoLines()).toBe(1); + expect(countScatterGeoMarkers()).toBe(lonQueue.length); + expect(countScatterGeoTextGroups()).toBe(textQueue.length); + expect(countScatterGeoTextNodes()).toBe(textQueue.length); + checkScatterGeoOrder(); + + expect(countChoroplethPaths()).toBe(locationsQueue.length); + + done(); + }); + } + ); }); + }); }); diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index 8e001d55873..4633e448f1c 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -1,591 +1,612 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var customMatchers = require('../assets/custom_matchers'); -var hasWebGLSupport = require('../assets/has_webgl_support'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var customMatchers = require("../assets/custom_matchers"); +var hasWebGLSupport = require("../assets/has_webgl_support"); // cartesian click events events use the hover data // from the mousemove events and then simulate // a click event on mouseup -var click = require('../assets/timed_click'); -var hover = require('../assets/hover'); +var click = require("../assets/timed_click"); +var hover = require("../assets/hover"); // contourgl is not part of the dist plotly.js bundle initially -Plotly.register([ - require('@lib/contourgl') -]); - -describe('Test hover and click interactions', function() { - - if(!hasWebGLSupport('gl2d_click_test')) return; - - var mock = require('@mocks/gl2d_14.json'); - var mock2 = require('@mocks/gl2d_pointcloud-basic.json'); - var mock3 = { - 'data': [ - { - 'type': 'contourgl', - 'z': [ - [ - 10, - 10.625, - 12.5, - 15.625, - 20 - ], - [ - 5.625, - 6.25, - 8.125, - 11.25, - 15.625 - ], - [ - 2.5, - 3.125, - 5, - 8.125, - 12.5 - ], - [ - 0.625, - 1.25, - 3.125, - 6.25, - 10.625 - ], - [ - 0, - 0.625, - 2.5, - 5.625, - 10 - ] - ], - 'colorscale': 'Jet', -/* 'contours': { +Plotly.register([require("@lib/contourgl")]); + +describe("Test hover and click interactions", function() { + if (!hasWebGLSupport("gl2d_click_test")) return; + + var mock = require("@mocks/gl2d_14.json"); + var mock2 = require("@mocks/gl2d_pointcloud-basic.json"); + var mock3 = { + data: [ + { + type: "contourgl", + z: [ + [10, 10.625, 12.5, 15.625, 20], + [5.625, 6.25, 8.125, 11.25, 15.625], + [2.5, 3.125, 5, 8.125, 12.5], + [0.625, 1.25, 3.125, 6.25, 10.625], + [0, 0.625, 2.5, 5.625, 10] + ], + colorscale: "Jet", + /* 'contours': { 'start': 2, 'end': 10, 'size': 1 - },*/ - 'uid': 'ad5624', - 'zmin': 0, - 'zmax': 20 - } - ], - 'layout': { - 'xaxis': { - 'range': [ - 0, - 4 - ], - 'autorange': true - }, - 'yaxis': { - 'range': [ - 0, - 4 - ], - 'autorange': true - }, - 'height': 450, - 'width': 1000, - 'autosize': true - } - }; - var mock4 = { - data: [ - { - x: [1, 2, 3, 4], - y: [12, 3, 14, 4], - type: 'scattergl', - mode: 'markers' - }, - { - x: [4, 5, 6, 7], - y: [1, 31, 24, 14], - type: 'scattergl', - mode: 'markers' - }, - { - x: [8, 9, 10, 11], - y: [18, 13, 10, 3], - type: 'scattergl', - mode: 'markers' - }], - layout: {} - }; - - var mockCopy, gd; - - function check(pt) { - expect(Object.keys(pt)).toEqual([ - 'x', 'y', 'curveNumber', 'pointNumber', 'data', 'fullData', 'xaxis', 'yaxis' - ]); - - expect(pt.x).toEqual(15.772); - expect(pt.y).toEqual(0.387); - expect(pt.curveNumber).toEqual(0); - expect(pt.pointNumber).toEqual(33); - expect(pt.fullData.length).toEqual(1); - expect(typeof pt.data.uid).toEqual('string'); - expect(pt.xaxis.domain.length).toEqual(2); - expect(pt.yaxis.domain.length).toEqual(2); + }, */ + uid: "ad5624", + zmin: 0, + zmax: 20 + } + ], + layout: { + xaxis: { range: [0, 4], autorange: true }, + yaxis: { range: [0, 4], autorange: true }, + height: 450, + width: 1000, + autosize: true } - - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - beforeEach(function() { - gd = createGraphDiv(); - mockCopy = Lib.extendDeep({}, mock); - }); - - afterEach(destroyGraphDiv); - - describe('hover event is fired on hover', function() { - var futureData; - - it('in general', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - gd.on('plotly_hover', function(data) { - futureData = data; - }); - - hover(654.7712871743302, 316.97670766680994); - - window.setTimeout(function() { - - expect(futureData.points.length).toEqual(1); - - var pt = futureData.points[0]; - - check(pt); - - done(); - }, 250); - })); - - + }; + var mock4 = { + data: [ + { + x: [1, 2, 3, 4], + y: [12, 3, 14, 4], + type: "scattergl", + mode: "markers" + }, + { + x: [4, 5, 6, 7], + y: [1, 31, 24, 14], + type: "scattergl", + mode: "markers" + }, + { + x: [8, 9, 10, 11], + y: [18, 13, 10, 3], + type: "scattergl", + mode: "markers" + } + ], + layout: {} + }; + + var mockCopy, gd; + + function check(pt) { + expect(Object.keys(pt)).toEqual([ + "x", + "y", + "curveNumber", + "pointNumber", + "data", + "fullData", + "xaxis", + "yaxis" + ]); + + expect(pt.x).toEqual(15.772); + expect(pt.y).toEqual(0.387); + expect(pt.curveNumber).toEqual(0); + expect(pt.pointNumber).toEqual(33); + expect(pt.fullData.length).toEqual(1); + expect(typeof pt.data.uid).toEqual("string"); + expect(pt.xaxis.domain.length).toEqual(2); + expect(pt.yaxis.domain.length).toEqual(2); + } + + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + beforeEach(function() { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + }); + + afterEach(destroyGraphDiv); + + describe("hover event is fired on hover", function() { + var futureData; + + it("in general", function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + gd.on("plotly_hover", function(data) { + futureData = data; }); - it('even when hoverinfo (== plotly tooltip) is set to none', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - modifiedMockCopy.data[0].hoverinfo = 'none'; - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - gd.on('plotly_hover', function(data) { - futureData = data; - }); + hover(654.7712871743302, 316.97670766680994); - hover(654.7712871743302, 316.97670766680994); + window.setTimeout( + function() { + expect(futureData.points.length).toEqual(1); - window.setTimeout(function() { + var pt = futureData.points[0]; - expect(futureData.points.length).toEqual(1); - - var pt = futureData.points[0]; - - check(pt); - - done(); - }, 250); - })); + check(pt); + done(); + }, + 250 + ); + })); + }); + it("even when hoverinfo (== plotly tooltip) is set to none", function( + done + ) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + modifiedMockCopy.data[0].hoverinfo = "none"; + + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + gd.on("plotly_hover", function(data) { + futureData = data; }); - it('event happens even on a click interaction', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - modifiedMockCopy.data[0].hoverinfo = 'none'; - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - gd.on('plotly_hover', function(data) { - futureData = data; - }); + hover(654.7712871743302, 316.97670766680994); - click(654.7712871743302, 316.97670766680994); + window.setTimeout( + function() { + expect(futureData.points.length).toEqual(1); - window.setTimeout(function() { + var pt = futureData.points[0]; - expect(futureData.points.length).toEqual(1); - - var pt = futureData.points[0]; - - check(pt); - - done(); - }, 250); - })); + check(pt); + done(); + }, + 250 + ); + })); + }); + it("event happens even on a click interaction", function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + modifiedMockCopy.data[0].hoverinfo = "none"; + + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + gd.on("plotly_hover", function(data) { + futureData = data; }); - it('unhover happens', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - modifiedMockCopy.data[0].hoverinfo = 'none'; - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - futureData = undefined; - - gd.on('plotly_unhover', function() { - futureData = 'emitted plotly_unhover'; - }); + click(654.7712871743302, 316.97670766680994); - hover(654.7712871743302, 316.97670766680994); + window.setTimeout( + function() { + expect(futureData.points.length).toEqual(1); - // fairly realistic simulation of moving with the cursor - window.setTimeout(function() { + var pt = futureData.points[0]; - var x = 654, y = 316; // we start here - var canceler = window.setInterval(function() { - hover(x--, y++); // move the cursor - }, 10); + check(pt); - window.setTimeout(function() { - window.clearInterval(canceler); // stop the mouse at some point - }, 250); - - window.setTimeout(function() { - - expect(futureData).toEqual('emitted plotly_unhover'); - - done(); + done(); + }, + 250 + ); + })); + }); - }, 250); + it("unhover happens", function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + modifiedMockCopy.data[0].hoverinfo = "none"; - }, 250); - })); + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + futureData = undefined; + gd.on("plotly_unhover", function() { + futureData = "emitted plotly_unhover"; }); - + hover(654.7712871743302, 316.97670766680994); + + // fairly realistic simulation of moving with the cursor + window.setTimeout( + function() { + var x = 654, y = 316; + // we start here + var canceler = window.setInterval( + function() { + hover((x--), (y++)); // move the cursor + }, + 10 + ); + + window.setTimeout( + function() { + window.clearInterval(canceler); // stop the mouse at some point + }, + 250 + ); + + window.setTimeout( + function() { + expect(futureData).toEqual("emitted plotly_unhover"); + + done(); + }, + 250 + ); + }, + 250 + ); + })); }); + }); - describe('hover event is fired for other gl2d plot types', function() { - var futureData; - - it('pointcloud', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mock2); - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - gd.on('plotly_hover', function(data) { - futureData = data; - }); - - hover(540, 150); - - window.setTimeout(function() { - - expect(futureData.points.length).toEqual(1); - - var pt = futureData.points[0]; - - expect(Object.keys(pt)).toEqual([ - 'x', 'y', 'curveNumber', 'pointNumber', 'data', 'fullData', 'xaxis', 'yaxis' - ]); - - expect(pt.x).toEqual(4.5); - expect(pt.y).toEqual(9); - expect(pt.curveNumber).toEqual(2); - expect(pt.pointNumber).toEqual(1); - expect(pt.fullData.length).toEqual(3); - expect(typeof pt.data.uid).toEqual('string'); - expect(pt.xaxis.domain.length).toEqual(2); - expect(pt.yaxis.domain.length).toEqual(2); - - done(); - }, 350); - })); + describe("hover event is fired for other gl2d plot types", function() { + var futureData; + it("pointcloud", function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mock2); + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + gd.on("plotly_hover", function(data) { + futureData = data; }); - it('heatmapgl', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mock3); - modifiedMockCopy.data[0].type = 'heatmapgl'; - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - gd.on('plotly_hover', function(data) { - futureData = data; - }); - - hover(540, 150); - - window.setTimeout(function() { - - expect(futureData.points.length).toEqual(1); - - var pt = futureData.points[0]; - - expect(Object.keys(pt)).toEqual([ - 'x', 'y', 'curveNumber', 'pointNumber', 'data', 'fullData', 'xaxis', 'yaxis' - ]); - - expect(pt.x).toEqual(2); - expect(pt.y).toEqual(3); - expect(pt.curveNumber).toEqual(0); - expect(pt.pointNumber).toEqual([2, 3]); - expect(pt.fullData.length).toEqual(1); - expect(typeof pt.data.uid).toEqual('string'); - expect(pt.xaxis.domain.length).toEqual(2); - expect(pt.yaxis.domain.length).toEqual(2); - - done(); - }, 350); - })); - + hover(540, 150); + + window.setTimeout( + function() { + expect(futureData.points.length).toEqual(1); + + var pt = futureData.points[0]; + + expect(Object.keys(pt)).toEqual([ + "x", + "y", + "curveNumber", + "pointNumber", + "data", + "fullData", + "xaxis", + "yaxis" + ]); + + expect(pt.x).toEqual(4.5); + expect(pt.y).toEqual(9); + expect(pt.curveNumber).toEqual(2); + expect(pt.pointNumber).toEqual(1); + expect(pt.fullData.length).toEqual(3); + expect(typeof pt.data.uid).toEqual("string"); + expect(pt.xaxis.domain.length).toEqual(2); + expect(pt.yaxis.domain.length).toEqual(2); + + done(); + }, + 350 + ); + })); + }); + it("heatmapgl", function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mock3); + modifiedMockCopy.data[0].type = "heatmapgl"; + + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + gd.on("plotly_hover", function(data) { + futureData = data; }); - it('scattergl', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mock4); - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - gd.on('plotly_hover', function(data) { - futureData = data; - }); - - hover(435, 216); - - window.setTimeout(function() { - - expect(futureData.points.length).toEqual(1); - - var pt = futureData.points[0]; - - expect(Object.keys(pt)).toEqual([ - 'x', 'y', 'curveNumber', 'pointNumber', 'data', 'fullData', 'xaxis', 'yaxis' - ]); + hover(540, 150); + + window.setTimeout( + function() { + expect(futureData.points.length).toEqual(1); + + var pt = futureData.points[0]; + + expect(Object.keys(pt)).toEqual([ + "x", + "y", + "curveNumber", + "pointNumber", + "data", + "fullData", + "xaxis", + "yaxis" + ]); + + expect(pt.x).toEqual(2); + expect(pt.y).toEqual(3); + expect(pt.curveNumber).toEqual(0); + expect(pt.pointNumber).toEqual([2, 3]); + expect(pt.fullData.length).toEqual(1); + expect(typeof pt.data.uid).toEqual("string"); + expect(pt.xaxis.domain.length).toEqual(2); + expect(pt.yaxis.domain.length).toEqual(2); + + done(); + }, + 350 + ); + })); + }); - expect(pt.x).toEqual(8); - expect(pt.y).toEqual(18); - expect(pt.curveNumber).toEqual(2); - expect(pt.pointNumber).toEqual(0); - expect(pt.fullData.length).toEqual(3); - expect(typeof pt.data.uid).toEqual('string'); - expect(pt.xaxis.domain.length).toEqual(2); - expect(pt.yaxis.domain.length).toEqual(2); + it("scattergl", function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mock4); - done(); - }, 350); - })); + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + gd.on("plotly_hover", function(data) { + futureData = data; }); - it('scattergl-fancy', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mock4); - modifiedMockCopy.data[0].mode = 'markers+lines'; - modifiedMockCopy.data[1].mode = 'markers+lines'; - modifiedMockCopy.data[2].mode = 'markers+lines'; - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - gd.on('plotly_hover', function(data) { - futureData = data; - }); - - hover(435, 216); - - window.setTimeout(function() { - - expect(futureData.points.length).toEqual(1); - - var pt = futureData.points[0]; - - expect(Object.keys(pt)).toEqual([ - 'x', 'y', 'curveNumber', 'pointNumber', 'data', 'fullData', 'xaxis', 'yaxis' - ]); - - expect(pt.x).toEqual(8); - expect(pt.y).toEqual(18); - expect(pt.curveNumber).toEqual(2); - expect(pt.pointNumber).toEqual(0); - expect(pt.fullData.length).toEqual(3); - expect(typeof pt.data.uid).toEqual('string'); - expect(pt.xaxis.domain.length).toEqual(2); - expect(pt.yaxis.domain.length).toEqual(2); + hover(435, 216); + + window.setTimeout( + function() { + expect(futureData.points.length).toEqual(1); + + var pt = futureData.points[0]; + + expect(Object.keys(pt)).toEqual([ + "x", + "y", + "curveNumber", + "pointNumber", + "data", + "fullData", + "xaxis", + "yaxis" + ]); + + expect(pt.x).toEqual(8); + expect(pt.y).toEqual(18); + expect(pt.curveNumber).toEqual(2); + expect(pt.pointNumber).toEqual(0); + expect(pt.fullData.length).toEqual(3); + expect(typeof pt.data.uid).toEqual("string"); + expect(pt.xaxis.domain.length).toEqual(2); + expect(pt.yaxis.domain.length).toEqual(2); + + done(); + }, + 350 + ); + })); + }); - done(); - }, 350); - })); + it("scattergl-fancy", function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mock4); + modifiedMockCopy.data[0].mode = "markers+lines"; + modifiedMockCopy.data[1].mode = "markers+lines"; + modifiedMockCopy.data[2].mode = "markers+lines"; + + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + gd.on("plotly_hover", function(data) { + futureData = data; }); - it('contourgl', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mock3); - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - gd.on('plotly_hover', function(data) { - futureData = data; - }); - - hover(540, 150); - - window.setTimeout(function() { - - expect(futureData.points.length).toEqual(1); - - var pt = futureData.points[0]; - - expect(Object.keys(pt)).toEqual([ - 'x', 'y', 'curveNumber', 'pointNumber', 'data', 'fullData', 'xaxis', 'yaxis' - ]); - - expect(pt.x).toEqual(2); - expect(pt.y).toEqual(3); - expect(pt.curveNumber).toEqual(0); - expect(pt.pointNumber).toEqual([2, 3]); - expect(pt.fullData.length).toEqual(1); - expect(typeof pt.data.uid).toEqual('string'); - expect(pt.xaxis.domain.length).toEqual(2); - expect(pt.yaxis.domain.length).toEqual(2); - - done(); - }, 350); - })); - }); + hover(435, 216); + + window.setTimeout( + function() { + expect(futureData.points.length).toEqual(1); + + var pt = futureData.points[0]; + + expect(Object.keys(pt)).toEqual([ + "x", + "y", + "curveNumber", + "pointNumber", + "data", + "fullData", + "xaxis", + "yaxis" + ]); + + expect(pt.x).toEqual(8); + expect(pt.y).toEqual(18); + expect(pt.curveNumber).toEqual(2); + expect(pt.pointNumber).toEqual(0); + expect(pt.fullData.length).toEqual(3); + expect(typeof pt.data.uid).toEqual("string"); + expect(pt.xaxis.domain.length).toEqual(2); + expect(pt.yaxis.domain.length).toEqual(2); + + done(); + }, + 350 + ); + })); }); - describe('click event is fired on click', function() { - var futureData; - - it('in general', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - gd.on('plotly_click', function(data) { - futureData = data; - }); - - click(654.7712871743302, 316.97670766680994); - - window.setTimeout(function() { - - var pt = futureData.points[0]; - - check(pt); - - done(); - - }, 350); - })); + it("contourgl", function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mock3); + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + gd.on("plotly_hover", function(data) { + futureData = data; }); - it('even when hoverinfo (== plotly tooltip) is set to none', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - modifiedMockCopy.data[0].hoverinfo = 'none'; - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) - - .then(new Promise(function() { - - gd.on('plotly_hover', function(data) { - futureData = data; - }); - - hover(654.7712871743302, 316.97670766680994); - - window.setTimeout(function() { - - expect(futureData.points.length).toEqual(1); - - var pt = futureData.points[0]; - - check(pt); + hover(540, 150); + + window.setTimeout( + function() { + expect(futureData.points.length).toEqual(1); + + var pt = futureData.points[0]; + + expect(Object.keys(pt)).toEqual([ + "x", + "y", + "curveNumber", + "pointNumber", + "data", + "fullData", + "xaxis", + "yaxis" + ]); + + expect(pt.x).toEqual(2); + expect(pt.y).toEqual(3); + expect(pt.curveNumber).toEqual(0); + expect(pt.pointNumber).toEqual([2, 3]); + expect(pt.fullData.length).toEqual(1); + expect(typeof pt.data.uid).toEqual("string"); + expect(pt.xaxis.domain.length).toEqual(2); + expect(pt.yaxis.domain.length).toEqual(2); + + done(); + }, + 350 + ); + })); + }); + }); - done(); - }, 250); - })); + describe("click event is fired on click", function() { + var futureData; + it("in general", function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + gd.on("plotly_click", function(data) { + futureData = data; }); - it('unhover happens', function(done) { - - var modifiedMockCopy = Lib.extendDeep({}, mockCopy); - modifiedMockCopy.data[0].hoverinfo = 'none'; - - Plotly.plot(gd, modifiedMockCopy.data, modifiedMockCopy.layout) + click(654.7712871743302, 316.97670766680994); - .then(new Promise(function() { + window.setTimeout( + function() { + var pt = futureData.points[0]; - futureData = undefined; + check(pt); - gd.on('plotly_unhover', function() { - futureData = 'emitted plotly_unhover'; - }); - - hover(654.7712871743302, 316.97670766680994); + done(); + }, + 350 + ); + })); + }); - // fairly realistic simulation of moving with the cursor - window.setTimeout(function() { + it("even when hoverinfo (== plotly tooltip) is set to none", function( + done + ) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + modifiedMockCopy.data[0].hoverinfo = "none"; + + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + gd.on("plotly_hover", function(data) { + futureData = data; + }); - var x = 654, y = 316; // we start here - var canceler = window.setInterval(function() { - hover(x--, y++); // move the cursor - }, 10); + hover(654.7712871743302, 316.97670766680994); - window.setTimeout(function() { - window.clearInterval(canceler); // stop the mouse at some point - }, 250); + window.setTimeout( + function() { + expect(futureData.points.length).toEqual(1); - window.setTimeout(function() { + var pt = futureData.points[0]; - expect(futureData).toEqual('emitted plotly_unhover'); + check(pt); - done(); + done(); + }, + 250 + ); + })); + }); - }, 250); + it("unhover happens", function(done) { + var modifiedMockCopy = Lib.extendDeep({}, mockCopy); + modifiedMockCopy.data[0].hoverinfo = "none"; - }, 250); - })); + Plotly.plot( + gd, + modifiedMockCopy.data, + modifiedMockCopy.layout + ).then(new Promise(function() { + futureData = undefined; + gd.on("plotly_unhover", function() { + futureData = "emitted plotly_unhover"; }); + hover(654.7712871743302, 316.97670766680994); + + // fairly realistic simulation of moving with the cursor + window.setTimeout( + function() { + var x = 654, y = 316; + // we start here + var canceler = window.setInterval( + function() { + hover((x--), (y++)); // move the cursor + }, + 10 + ); + + window.setTimeout( + function() { + window.clearInterval(canceler); // stop the mouse at some point + }, + 250 + ); + + window.setTimeout( + function() { + expect(futureData).toEqual("emitted plotly_unhover"); + + done(); + }, + 250 + ); + }, + 250 + ); + })); }); + }); }); diff --git a/test/jasmine/tests/gl2d_date_axis_render_test.js b/test/jasmine/tests/gl2d_date_axis_render_test.js index 53392de3e76..4ce251c7c06 100644 --- a/test/jasmine/tests/gl2d_date_axis_render_test.js +++ b/test/jasmine/tests/gl2d_date_axis_render_test.js @@ -1,99 +1,113 @@ -var PlotlyInternal = require('@src/plotly'); - -var hasWebGLSupport = require('../assets/has_webgl_support'); - -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); - -describe('date axis', function() { - - if(!hasWebGLSupport('axes_test date axis')) return; - - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - it('should use the fancy gl-vis/gl-scatter2d', function() { - PlotlyInternal.plot(gd, [{ - type: 'scattergl', - 'marker': { - 'color': 'rgb(31, 119, 180)', - 'size': 18, - 'symbol': [ - 'diamond', - 'cross' - ] - }, - x: [new Date('2016-10-10'), new Date('2016-10-12')], - y: [15, 16] - }]); - - expect(gd._fullLayout.xaxis.type).toBe('date'); - expect(gd._fullLayout.yaxis.type).toBe('linear'); - expect(gd._fullData[0].type).toBe('scattergl'); - expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); - - // one way of check which renderer - fancy vs not - we're using - expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe(0); - }); - - it('should use the fancy gl-vis/gl-scatter2d once again', function() { - PlotlyInternal.plot(gd, [{ - type: 'scattergl', - 'marker': { - 'color': 'rgb(31, 119, 180)', - 'size': 36, - 'symbol': [ - 'circle', - 'cross' - ] - }, - x: [new Date('2016-10-10'), new Date('2016-10-11')], - y: [15, 16] - }]); - - expect(gd._fullLayout.xaxis.type).toBe('date'); - expect(gd._fullLayout.yaxis.type).toBe('linear'); - expect(gd._fullData[0].type).toBe('scattergl'); - expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); - - // one way of check which renderer - fancy vs not - we're using - expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe(0); - }); - - it('should now use the non-fancy gl-vis/gl-scatter2d', function() { - PlotlyInternal.plot(gd, [{ - type: 'scattergl', - mode: 'markers', // important, as otherwise lines are assumed (which needs fancy) - x: [new Date('2016-10-10'), new Date('2016-10-11')], - y: [15, 16] - }]); - - expect(gd._fullLayout.xaxis.type).toBe('date'); - expect(gd._fullLayout.yaxis.type).toBe('linear'); - expect(gd._fullData[0].type).toBe('scattergl'); - expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); - - expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe(2); - }); - - it('should use the non-fancy gl-vis/gl-scatter2d with string dates', function() { - PlotlyInternal.plot(gd, [{ - type: 'scattergl', - mode: 'markers', // important, as otherwise lines are assumed (which needs fancy) - x: ['2016-10-10', '2016-10-11'], - y: [15, 16] - }]); - - expect(gd._fullLayout.xaxis.type).toBe('date'); - expect(gd._fullLayout.yaxis.type).toBe('linear'); - expect(gd._fullData[0].type).toBe('scattergl'); - expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); - - expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe(2); - }); +var PlotlyInternal = require("@src/plotly"); + +var hasWebGLSupport = require("../assets/has_webgl_support"); + +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); + +describe("date axis", function() { + if (!hasWebGLSupport("axes_test date axis")) return; + + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + it("should use the fancy gl-vis/gl-scatter2d", function() { + PlotlyInternal.plot(gd, [ + { + type: "scattergl", + marker: { + color: "rgb(31, 119, 180)", + size: 18, + symbol: ["diamond", "cross"] + }, + x: [new Date("2016-10-10"), new Date("2016-10-12")], + y: [15, 16] + } + ]); + + expect(gd._fullLayout.xaxis.type).toBe("date"); + expect(gd._fullLayout.yaxis.type).toBe("linear"); + expect(gd._fullData[0].type).toBe("scattergl"); + expect(gd._fullData[0]._module.basePlotModule.name).toBe("gl2d"); + + // one way of check which renderer - fancy vs not - we're using + expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe( + 0 + ); + }); + + it("should use the fancy gl-vis/gl-scatter2d once again", function() { + PlotlyInternal.plot(gd, [ + { + type: "scattergl", + marker: { + color: "rgb(31, 119, 180)", + size: 36, + symbol: ["circle", "cross"] + }, + x: [new Date("2016-10-10"), new Date("2016-10-11")], + y: [15, 16] + } + ]); + + expect(gd._fullLayout.xaxis.type).toBe("date"); + expect(gd._fullLayout.yaxis.type).toBe("linear"); + expect(gd._fullData[0].type).toBe("scattergl"); + expect(gd._fullData[0]._module.basePlotModule.name).toBe("gl2d"); + + // one way of check which renderer - fancy vs not - we're using + expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe( + 0 + ); + }); + + it("should now use the non-fancy gl-vis/gl-scatter2d", function() { + PlotlyInternal.plot(gd, [ + { + type: "scattergl", + mode: "markers", + // important, as otherwise lines are assumed (which needs fancy) + x: [new Date("2016-10-10"), new Date("2016-10-11")], + y: [15, 16] + } + ]); + + expect(gd._fullLayout.xaxis.type).toBe("date"); + expect(gd._fullLayout.yaxis.type).toBe("linear"); + expect(gd._fullData[0].type).toBe("scattergl"); + expect(gd._fullData[0]._module.basePlotModule.name).toBe("gl2d"); + + expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe( + 2 + ); + }); + + it( + "should use the non-fancy gl-vis/gl-scatter2d with string dates", + function() { + PlotlyInternal.plot(gd, [ + { + type: "scattergl", + mode: "markers", + // important, as otherwise lines are assumed (which needs fancy) + x: ["2016-10-10", "2016-10-11"], + y: [15, 16] + } + ]); + + expect(gd._fullLayout.xaxis.type).toBe("date"); + expect(gd._fullLayout.yaxis.type).toBe("linear"); + expect(gd._fullData[0].type).toBe("scattergl"); + expect(gd._fullData[0]._module.basePlotModule.name).toBe("gl2d"); + + expect( + gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount + ).toBe(2); + } + ); }); diff --git a/test/jasmine/tests/gl2d_pointcloud_test.js b/test/jasmine/tests/gl2d_pointcloud_test.js index 6cc0017ad21..ba0546af37f 100644 --- a/test/jasmine/tests/gl2d_pointcloud_test.js +++ b/test/jasmine/tests/gl2d_pointcloud_test.js @@ -1,211 +1,230 @@ -'use strict'; - -var Plotly = require('@lib/index'); +"use strict"; +var Plotly = require("@lib/index"); // Test utilities -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var failTest = require('../assets/fail_test'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var failTest = require("../assets/fail_test"); var plotData = { - 'data': [ - { - 'type': 'pointcloud', - 'mode': 'markers', - 'marker': { - 'sizemin': 0.5, - 'sizemax': 100, - 'arearatio': 0, - 'color': 'rgba(255, 0, 0, 0.6)' - }, - 'x': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - 'y': [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] - }, - { - 'type': 'pointcloud', - 'mode': 'markers', - 'marker': { - 'sizemin': 0.5, - 'sizemax': 100, - 'arearatio': 0, - 'color': 'rgba(0, 0, 255, 0.9)', - 'opacity': 0.8, - 'blend': true - }, - 'opacity': 0.7, - 'x': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - 'y': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - { - 'type': 'pointcloud', - 'mode': 'markers', - 'marker': { - 'sizemin': 0.5, - 'sizemax': 100, - 'border': { - 'color': 'rgb(0, 0, 0)', - 'arearatio': 0.7071 - }, - 'color': 'green', - 'opacity': 0.8, - 'blend': true - }, - 'opacity': 0.7, - 'x': [3, 4.5, 6], - 'y': [9, 9, 9] - }, - { - 'type': 'pointcloud', - 'mode': 'markers', - 'marker': { - 'sizemin': 0.5, - 'sizemax': 100, - 'color': 'yellow', - 'opacity': 0.8, - 'blend': true - }, - 'opacity': 0.7, - 'xy': new Float32Array([1, 3, 9, 3]), - 'indices': new Int32Array([0, 1]), - 'xbounds': [1, 9], - 'ybounds': [3, 3] - }, - { - 'type': 'pointcloud', - 'mode': 'markers', - 'marker': { - 'sizemin': 0.5, - 'sizemax': 100, - 'color': 'orange', - 'opacity': 0.8, - 'blend': true - }, - 'opacity': 0.7, - 'xy': new Float32Array([1, 4, 9, 4]), - 'indices': new Int32Array([0, 1]) - }, - { - 'type': 'pointcloud', - 'mode': 'markers', - 'marker': { - 'sizemin': 0.5, - 'sizemax': 100, - 'color': 'darkorange', - 'opacity': 0.8, - 'blend': true - }, - 'opacity': 0.7, - 'xy': new Float32Array([1, 5, 9, 5]), - 'xbounds': [1, 9], - 'ybounds': [5, 5] - }, - { - 'type': 'pointcloud', - 'mode': 'markers', - 'marker': { - 'sizemin': 0.5, - 'sizemax': 100, - 'color': 'red', - 'opacity': 0.8, - 'blend': true - }, - 'opacity': 0.7, - 'xy': new Float32Array([1, 6, 9, 6]) - } - ], - 'layout': { - 'title': 'Point Cloud - basic', - 'xaxis': { - 'type': 'linear', - 'range': [ - -2.501411175139456, - 43.340777299865266 - ], - 'autorange': true - }, - 'yaxis': { - 'type': 'linear', - 'range': [ - 4, - 6 - ], - 'autorange': true - }, - 'height': 598, - 'width': 1080, - 'autosize': true, - 'showlegend': false + data: [ + { + type: "pointcloud", + mode: "markers", + marker: { + sizemin: 0.5, + sizemax: 100, + arearatio: 0, + color: "rgba(255, 0, 0, 0.6)" + }, + x: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + y: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + }, + { + type: "pointcloud", + mode: "markers", + marker: { + sizemin: 0.5, + sizemax: 100, + arearatio: 0, + color: "rgba(0, 0, 255, 0.9)", + opacity: 0.8, + blend: true + }, + opacity: 0.7, + x: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + y: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + { + type: "pointcloud", + mode: "markers", + marker: { + sizemin: 0.5, + sizemax: 100, + border: { color: "rgb(0, 0, 0)", arearatio: 0.7071 }, + color: "green", + opacity: 0.8, + blend: true + }, + opacity: 0.7, + x: [3, 4.5, 6], + y: [9, 9, 9] + }, + { + type: "pointcloud", + mode: "markers", + marker: { + sizemin: 0.5, + sizemax: 100, + color: "yellow", + opacity: 0.8, + blend: true + }, + opacity: 0.7, + xy: new Float32Array([1, 3, 9, 3]), + indices: new Int32Array([0, 1]), + xbounds: [1, 9], + ybounds: [3, 3] + }, + { + type: "pointcloud", + mode: "markers", + marker: { + sizemin: 0.5, + sizemax: 100, + color: "orange", + opacity: 0.8, + blend: true + }, + opacity: 0.7, + xy: new Float32Array([1, 4, 9, 4]), + indices: new Int32Array([0, 1]) + }, + { + type: "pointcloud", + mode: "markers", + marker: { + sizemin: 0.5, + sizemax: 100, + color: "darkorange", + opacity: 0.8, + blend: true + }, + opacity: 0.7, + xy: new Float32Array([1, 5, 9, 5]), + xbounds: [1, 9], + ybounds: [5, 5] + }, + { + type: "pointcloud", + mode: "markers", + marker: { + sizemin: 0.5, + sizemax: 100, + color: "red", + opacity: 0.8, + blend: true + }, + opacity: 0.7, + xy: new Float32Array([1, 6, 9, 6]) } + ], + layout: { + title: "Point Cloud - basic", + xaxis: { + type: "linear", + range: [-2.501411175139456, 43.340777299865266], + autorange: true + }, + yaxis: { type: "linear", range: [4, 6], autorange: true }, + height: 598, + width: 1080, + autosize: true, + showlegend: false + } }; function makePlot(gd, mock, done) { - return Plotly.plot(gd, mock.data, mock.layout) - .then(null, failTest) - .then(done); + return Plotly.plot(gd, mock.data, mock.layout) + .then(null, failTest) + .then(done); } -describe('contourgl plots', function() { - - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); - - it('render without raising an error', function(done) { - makePlot(gd, plotData, done); - }); - - it('should update properly', function(done) { - var mock = plotData; - var scene2d; - - var xBaselineMins = [{'val': 0, 'pad': 50}, {'val': 0, 'pad': 50}, {'val': 3, 'pad': 50}, {'val': 1, 'pad': 50}, {'val': 1, 'pad': 50}, {'val': 1, 'pad': 50}, {'val': 1, 'pad': 50}]; - var xBaselineMaxes = [{'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 6, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}]; - - var yBaselineMins = [{'val': 0, 'pad': 50}, {'val': 0, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 3, 'pad': 50}, {'val': 4, 'pad': 50}, {'val': 5, 'pad': 50}, {'val': 6, 'pad': 50}]; - var yBaselineMaxes = [{'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 3, 'pad': 50}, {'val': 4, 'pad': 50}, {'val': 5, 'pad': 50}, {'val': 6, 'pad': 50}]; - - Plotly.plot(gd, mock.data, mock.layout).then(function() { - scene2d = gd._fullLayout._plots.xy._scene2d; - - expect(scene2d.traces[mock.data[0].uid].type).toEqual('pointcloud'); - - expect(scene2d.xaxis._min).toEqual(xBaselineMins); - expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); - - expect(scene2d.yaxis._min).toEqual(yBaselineMins); - expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); - - return Plotly.relayout(gd, 'xaxis.range', [3, 6]); - }).then(function() { - - expect(scene2d.xaxis._min).toEqual(xBaselineMins); - expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); - - return Plotly.relayout(gd, 'xaxis.autorange', true); - }).then(function() { - - expect(scene2d.xaxis._min).toEqual(xBaselineMins); - expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); - - return Plotly.relayout(gd, 'yaxis.range', [8, 20]); - }).then(function() { - - expect(scene2d.yaxis._min).toEqual(yBaselineMins); - expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); - - return Plotly.relayout(gd, 'yaxis.autorange', true); - }).then(function() { - expect(scene2d.yaxis._min).toEqual(yBaselineMins); - expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); - - done(); - }); - }); +describe("contourgl plots", function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("render without raising an error", function(done) { + makePlot(gd, plotData, done); + }); + + it("should update properly", function(done) { + var mock = plotData; + var scene2d; + + var xBaselineMins = [ + { val: 0, pad: 50 }, + { val: 0, pad: 50 }, + { val: 3, pad: 50 }, + { val: 1, pad: 50 }, + { val: 1, pad: 50 }, + { val: 1, pad: 50 }, + { val: 1, pad: 50 } + ]; + var xBaselineMaxes = [ + { val: 9, pad: 50 }, + { val: 9, pad: 50 }, + { val: 6, pad: 50 }, + { val: 9, pad: 50 }, + { val: 9, pad: 50 }, + { val: 9, pad: 50 }, + { val: 9, pad: 50 } + ]; + + var yBaselineMins = [ + { val: 0, pad: 50 }, + { val: 0, pad: 50 }, + { val: 9, pad: 50 }, + { val: 3, pad: 50 }, + { val: 4, pad: 50 }, + { val: 5, pad: 50 }, + { val: 6, pad: 50 } + ]; + var yBaselineMaxes = [ + { val: 9, pad: 50 }, + { val: 9, pad: 50 }, + { val: 9, pad: 50 }, + { val: 3, pad: 50 }, + { val: 4, pad: 50 }, + { val: 5, pad: 50 }, + { val: 6, pad: 50 } + ]; + + Plotly.plot(gd, mock.data, mock.layout) + .then(function() { + scene2d = gd._fullLayout._plots.xy._scene2d; + + expect(scene2d.traces[mock.data[0].uid].type).toEqual("pointcloud"); + + expect(scene2d.xaxis._min).toEqual(xBaselineMins); + expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); + + expect(scene2d.yaxis._min).toEqual(yBaselineMins); + expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); + + return Plotly.relayout(gd, "xaxis.range", [3, 6]); + }) + .then(function() { + expect(scene2d.xaxis._min).toEqual(xBaselineMins); + expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); + + return Plotly.relayout(gd, "xaxis.autorange", true); + }) + .then(function() { + expect(scene2d.xaxis._min).toEqual(xBaselineMins); + expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); + + return Plotly.relayout(gd, "yaxis.range", [8, 20]); + }) + .then(function() { + expect(scene2d.yaxis._min).toEqual(yBaselineMins); + expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); + + return Plotly.relayout(gd, "yaxis.autorange", true); + }) + .then(function() { + expect(scene2d.yaxis._min).toEqual(yBaselineMins); + expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); + + done(); + }); + }); }); diff --git a/test/jasmine/tests/gl2d_scatterplot_contour_test.js b/test/jasmine/tests/gl2d_scatterplot_contour_test.js index b0c65b9fda2..dcc3912b739 100644 --- a/test/jasmine/tests/gl2d_scatterplot_contour_test.js +++ b/test/jasmine/tests/gl2d_scatterplot_contour_test.js @@ -1,257 +1,210 @@ -'use strict'; - -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); -var d3 = require('d3'); +"use strict"; +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); +var d3 = require("d3"); // contourgl is not part of the dist plotly.js bundle initially -Plotly.register([ - require('@lib/contourgl') -]); +Plotly.register([require("@lib/contourgl")]); // Test utilities -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var failTest = require('../assets/fail_test'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var failTest = require("../assets/fail_test"); var plotData = { - 'data': [ - { - 'type': 'contourgl', - 'z': [ - [ - 10, - 10.625, - 12.5, - 15.625, - 20 - ], - [ - 5.625, - 6.25, - 8.125, - 11.25, - 15.625 - ], - [ - 2.5, - 3.125, - 5, - 8.125, - 12.5 - ], - [ - 0.625, - 1.25, - 3.125, - 6.25, - 10.625 - ], - [ - 0, - 0.625, - 2.5, - 5.625, - 10 - ] - ], - 'colorscale': 'Jet', - 'contours': { - 'start': 2, - 'end': 10, - 'size': 1 - }, - 'uid': 'ad5624', - 'zmin': 0, - 'zmax': 20 - } - ], - 'layout': { - 'xaxis': { - 'range': [ - 0, - 4 - ], - 'autorange': true - }, - 'yaxis': { - 'range': [ - 0, - 4 - ], - 'autorange': true - }, - 'height': 450, - 'width': 1000, - 'autosize': true + data: [ + { + type: "contourgl", + z: [ + [10, 10.625, 12.5, 15.625, 20], + [5.625, 6.25, 8.125, 11.25, 15.625], + [2.5, 3.125, 5, 8.125, 12.5], + [0.625, 1.25, 3.125, 6.25, 10.625], + [0, 0.625, 2.5, 5.625, 10] + ], + colorscale: "Jet", + contours: { start: 2, end: 10, size: 1 }, + uid: "ad5624", + zmin: 0, + zmax: 20 } + ], + layout: { + xaxis: { range: [0, 4], autorange: true }, + yaxis: { range: [0, 4], autorange: true }, + height: 450, + width: 1000, + autosize: true + } }; function transpose(a) { - return a[0].map(function(ignore, columnIndex) {return a.map(function(row) {return row[columnIndex];});}); + return a[0].map(function(ignore, columnIndex) { + return a.map(function(row) { + return row[columnIndex]; + }); + }); } function jitter(maxJitterRatio, n) { - return n * (1 + maxJitterRatio * (2 * Math.random() - 1)); + return n * (1 + maxJitterRatio * (2 * Math.random() - 1)); } function rotate(rad, point) { - return { - x: point.x * Math.cos(rad) - point.y * Math.sin(rad), - y: point.x * Math.sin(rad) + point.y * Math.cos(rad) - }; + return { + x: point.x * Math.cos(rad) - point.y * Math.sin(rad), + y: point.x * Math.sin(rad) + point.y * Math.cos(rad) + }; } function generate(maxJitter) { - var x = d3.range(-1, 1.5, 0.5); // left closed, right open interval - var y = d3.range(-1, 1.5, 0.5); // left closed, right open interval - var i, j, p, z = new Array(x.length); - for(i = 0; i < x.length; i++) { - z[i] = new Array(y.length); - for(j = 0; j < y.length; j++) { - p = rotate(Math.PI / 4, {x: x[i], y: -y[j]}); - z[i][j] = jitter(maxJitter, Math.pow(p.x, 2) + Math.pow(p.y, 2)); - } + var x = d3.range(-1, 1.5, 0.5); + // left closed, right open interval + var y = d3.range(-1, 1.5, 0.5); + // left closed, right open interval + var i, j, p, z = new Array(x.length); + for (i = 0; i < x.length; i++) { + z[i] = new Array(y.length); + for (j = 0; j < y.length; j++) { + p = rotate(Math.PI / 4, { x: x[i], y: -y[j] }); + z[i][j] = jitter(maxJitter, Math.pow(p.x, 2) + Math.pow(p.y, 2)); } - return {x: x, y: y, z: z}; // looking forward to the ES2015 return {x, y, z} + } + return { x: x, y: y, z: z }; // looking forward to the ES2015 return {x, y, z} } // equivalent to the new example case in gl-contour2d var plotDataElliptical = function(maxJitter) { - var model = generate(maxJitter); - return { - 'data': [ - { - 'type': 'contourgl', - 'x': model.x, - 'y': model.y, - 'z': transpose(model.z), // gl-vis is column-major order while ploly is row-major order - 'colorscale': 'Jet', - 'contours': { - 'start': 0, - 'end': 2, - 'size': 0.1, - 'coloring': 'fill' - }, - 'uid': 'ad5624', - 'zmin': 0, - 'zmax': 2 - } - ], - 'layout': { - 'xaxis': { - 'range': [ - -10, - 10 - ], - 'autorange': true - }, - 'yaxis': { - 'range': [ - -10, - 10 - ], - 'autorange': true - }, - 'height': 600, - 'width': 600, - 'autosize': true - } - }; + var model = generate(maxJitter); + return { + data: [ + { + type: "contourgl", + x: model.x, + y: model.y, + z: transpose(model.z), + // gl-vis is column-major order while ploly is row-major order + colorscale: "Jet", + contours: { start: 0, end: 2, size: 0.1, coloring: "fill" }, + uid: "ad5624", + zmin: 0, + zmax: 2 + } + ], + layout: { + xaxis: { range: [-10, 10], autorange: true }, + yaxis: { range: [-10, 10], autorange: true }, + height: 600, + width: 600, + autosize: true + } + }; }; - function makePlot(gd, mock, done) { - return Plotly.plot(gd, mock.data, mock.layout) - .then(null, failTest) - .then(done); + return Plotly.plot(gd, mock.data, mock.layout) + .then(null, failTest) + .then(done); } -describe('contourgl plots', function() { - - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); - - // this first dataset is a special case, very forgiving to the contour renderer, as it's convex, - // contains no inflexion points etc. - it('render without raising an error', function(done) { - makePlot(gd, plotData, done); - }); - - it('render without raising an error', function(done) { - var mock = require('@mocks/simple_contour.json'), - mockCopy = Lib.extendDeep({}, mock); - - mockCopy.data[0].type = 'contourgl'; - mockCopy.data[0].contours = { coloring: 'fill' }; - - makePlot(gd, mockCopy, done); - }); - - it('render without raising an error (coloring: "lines")', function(done) { - var mock = Lib.extendDeep({}, plotDataElliptical(0)); - mock.data[0].contours.coloring = 'lines'; // 'fill' is the default - makePlot(gd, mock, done); - }); - - it('render smooth, regular ellipses without raising an error (coloring: "fill")', function(done) { - var mock = plotDataElliptical(0); - makePlot(gd, mock, done); - }); - - it('render ellipses with added noise without raising an error (coloring: "fill")', function(done) { - var mock = plotDataElliptical(0.5); - mock.data[0].contours.coloring = 'fill'; // 'fill' is the default - mock.data[0].line = {smoothing: 0}; - makePlot(gd, mock, done); - }); - - it('should update properly', function(done) { - var mock = plotDataElliptical(0); - var scene2d; - - Plotly.plot(gd, mock.data, mock.layout).then(function() { - scene2d = gd._fullLayout._plots.xy._scene2d; - - expect(scene2d.traces[mock.data[0].uid].type).toEqual('contourgl'); - expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0}]); - expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0}]); - - return Plotly.relayout(gd, 'xaxis.range', [0, -10]); - }).then(function() { - expect(scene2d.xaxis._min).toEqual([]); - expect(scene2d.xaxis._max).toEqual([]); - - return Plotly.relayout(gd, 'xaxis.autorange', true); - }).then(function() { - expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0}]); - expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0}]); - - return Plotly.restyle(gd, 'type', 'heatmapgl'); - }).then(function() { - expect(scene2d.traces[mock.data[0].uid].type).toEqual('heatmapgl'); - expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0}]); - expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0}]); - - return Plotly.relayout(gd, 'xaxis.range', [0, -10]); - }).then(function() { - expect(scene2d.xaxis._min).toEqual([]); - expect(scene2d.xaxis._max).toEqual([]); - - return Plotly.relayout(gd, 'xaxis.autorange', true); - }).then(function() { - expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0}]); - expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0}]); - - done(); - }); - }); +describe("contourgl plots", function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + // this first dataset is a special case, very forgiving to the contour renderer, as it's convex, + // contains no inflexion points etc. + it("render without raising an error", function(done) { + makePlot(gd, plotData, done); + }); + + it("render without raising an error", function(done) { + var mock = require("@mocks/simple_contour.json"), + mockCopy = Lib.extendDeep({}, mock); + + mockCopy.data[0].type = "contourgl"; + mockCopy.data[0].contours = { coloring: "fill" }; + + makePlot(gd, mockCopy, done); + }); + + it('render without raising an error (coloring: "lines")', function(done) { + var mock = Lib.extendDeep({}, plotDataElliptical(0)); + mock.data[0].contours.coloring = "lines"; + // 'fill' is the default + makePlot(gd, mock, done); + }); + + it( + 'render smooth, regular ellipses without raising an error (coloring: "fill")', + function(done) { + var mock = plotDataElliptical(0); + makePlot(gd, mock, done); + } + ); + + it( + 'render ellipses with added noise without raising an error (coloring: "fill")', + function(done) { + var mock = plotDataElliptical(0.5); + mock.data[0].contours.coloring = "fill"; + // 'fill' is the default + mock.data[0].line = { smoothing: 0 }; + makePlot(gd, mock, done); + } + ); + + it("should update properly", function(done) { + var mock = plotDataElliptical(0); + var scene2d; + + Plotly.plot(gd, mock.data, mock.layout) + .then(function() { + scene2d = gd._fullLayout._plots.xy._scene2d; + + expect(scene2d.traces[mock.data[0].uid].type).toEqual("contourgl"); + expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0 }]); + expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0 }]); + + return Plotly.relayout(gd, "xaxis.range", [0, -10]); + }) + .then(function() { + expect(scene2d.xaxis._min).toEqual([]); + expect(scene2d.xaxis._max).toEqual([]); + + return Plotly.relayout(gd, "xaxis.autorange", true); + }) + .then(function() { + expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0 }]); + expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0 }]); + + return Plotly.restyle(gd, "type", "heatmapgl"); + }) + .then(function() { + expect(scene2d.traces[mock.data[0].uid].type).toEqual("heatmapgl"); + expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0 }]); + expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0 }]); + + return Plotly.relayout(gd, "xaxis.range", [0, -10]); + }) + .then(function() { + expect(scene2d.xaxis._min).toEqual([]); + expect(scene2d.xaxis._max).toEqual([]); + + return Plotly.relayout(gd, "xaxis.autorange", true); + }) + .then(function() { + expect(scene2d.xaxis._min).toEqual([{ val: -1, pad: 0 }]); + expect(scene2d.xaxis._max).toEqual([{ val: 1, pad: 0 }]); + + done(); + }); + }); }); diff --git a/test/jasmine/tests/gl3daxes_test.js b/test/jasmine/tests/gl3daxes_test.js index fad1f8c222b..de9602ceb6c 100644 --- a/test/jasmine/tests/gl3daxes_test.js +++ b/test/jasmine/tests/gl3daxes_test.js @@ -1,109 +1,109 @@ -var supplyLayoutDefaults = require('@src/plots/gl3d/layout/axis_defaults'); +var supplyLayoutDefaults = require("@src/plots/gl3d/layout/axis_defaults"); +describe("Test Gl3dAxes", function() { + "use strict"; + describe("supplyLayoutDefaults supplies defaults", function() { + var layoutIn, layoutOut; -describe('Test Gl3dAxes', function() { - 'use strict'; + var options = { + font: "Open Sans", + scene: { id: "scene" }, + data: [{ x: [], y: [] }], + bgColor: "#fff" + }; - describe('supplyLayoutDefaults supplies defaults', function() { - var layoutIn, - layoutOut; - - var options = { - font: 'Open Sans', - scene: {id: 'scene'}, - data: [{x: [], y: []}], - bgColor: '#fff' - }; - - beforeEach(function() { - layoutOut = {}; - }); + beforeEach(function() { + layoutOut = {}; + }); - it('should define specific default set with empty initial layout', function() { - layoutIn = {}; + it( + "should define specific default set with empty initial layout", + function() { + layoutIn = {}; - var expected = { - 'xaxis': { - 'showline': false, - 'showgrid': true, - 'gridcolor': 'rgb(204, 204, 204)', - 'gridwidth': 1, - 'showspikes': true, - 'spikesides': true, - 'spikethickness': 2, - 'spikecolor': '#444', - 'showbackground': false, - 'showaxeslabels': true - }, - 'yaxis': { - 'showline': false, - 'showgrid': true, - 'gridcolor': 'rgb(204, 204, 204)', - 'gridwidth': 1, - 'showspikes': true, - 'spikesides': true, - 'spikethickness': 2, - 'spikecolor': '#444', - 'showbackground': false, - 'showaxeslabels': true - }, - 'zaxis': { - 'showline': false, - 'showgrid': true, - 'gridcolor': 'rgb(204, 204, 204)', - 'gridwidth': 1, - 'showspikes': true, - 'spikesides': true, - 'spikethickness': 2, - 'spikecolor': '#444', - 'showbackground': false, - 'showaxeslabels': true - } - }; + var expected = { + xaxis: { + showline: false, + showgrid: true, + gridcolor: "rgb(204, 204, 204)", + gridwidth: 1, + showspikes: true, + spikesides: true, + spikethickness: 2, + spikecolor: "#444", + showbackground: false, + showaxeslabels: true + }, + yaxis: { + showline: false, + showgrid: true, + gridcolor: "rgb(204, 204, 204)", + gridwidth: 1, + showspikes: true, + spikesides: true, + spikethickness: 2, + spikecolor: "#444", + showbackground: false, + showaxeslabels: true + }, + zaxis: { + showline: false, + showgrid: true, + gridcolor: "rgb(204, 204, 204)", + gridwidth: 1, + showspikes: true, + spikesides: true, + spikethickness: 2, + spikecolor: "#444", + showbackground: false, + showaxeslabels: true + } + }; - function checkKeys(validObject, testObject) { - var keys = Object.keys(validObject); - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; - expect(validObject[k]).toBe(testObject[k]); - } - return true; - } + function checkKeys(validObject, testObject) { + var keys = Object.keys(validObject); + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + expect(validObject[k]).toBe(testObject[k]); + } + return true; + } - supplyLayoutDefaults(layoutIn, layoutOut, options); - ['xaxis', 'yaxis', 'zaxis'].forEach(function(axis) { - checkKeys(expected[axis], layoutOut[axis]); - }); + supplyLayoutDefaults(layoutIn, layoutOut, options); + ["xaxis", "yaxis", "zaxis"].forEach(function(axis) { + checkKeys(expected[axis], layoutOut[axis]); }); + } + ); - it('should inherit layout.calendar', function() { - layoutIn = { - xaxis: {type: 'date'}, - yaxis: {type: 'date'}, - zaxis: {type: 'date'} - }; - options.calendar = 'taiwan'; + it("should inherit layout.calendar", function() { + layoutIn = { + xaxis: { type: "date" }, + yaxis: { type: "date" }, + zaxis: { type: "date" } + }; + options.calendar = "taiwan"; - supplyLayoutDefaults(layoutIn, layoutOut, options); + supplyLayoutDefaults(layoutIn, layoutOut, options); - expect(layoutOut.xaxis.calendar).toBe('taiwan'); - expect(layoutOut.yaxis.calendar).toBe('taiwan'); - expect(layoutOut.zaxis.calendar).toBe('taiwan'); - }); + expect(layoutOut.xaxis.calendar).toBe("taiwan"); + expect(layoutOut.yaxis.calendar).toBe("taiwan"); + expect(layoutOut.zaxis.calendar).toBe("taiwan"); + }); - it('should accept its own calendar', function() { - layoutIn = { - xaxis: {type: 'date', calendar: 'hebrew'}, - yaxis: {type: 'date', calendar: 'ummalqura'}, - zaxis: {type: 'date', calendar: 'discworld'} - }; - options.calendar = 'taiwan'; + it("should accept its own calendar", function() { + layoutIn = { + xaxis: { type: "date", calendar: "hebrew" }, + yaxis: { type: "date", calendar: "ummalqura" }, + zaxis: { type: "date", calendar: "discworld" } + }; + options.calendar = "taiwan"; - supplyLayoutDefaults(layoutIn, layoutOut, options); + supplyLayoutDefaults(layoutIn, layoutOut, options); - expect(layoutOut.xaxis.calendar).toBe('hebrew'); - expect(layoutOut.yaxis.calendar).toBe('ummalqura'); - expect(layoutOut.zaxis.calendar).toBe('discworld'); - }); + expect(layoutOut.xaxis.calendar).toBe("hebrew"); + expect(layoutOut.yaxis.calendar).toBe("ummalqura"); + expect(layoutOut.zaxis.calendar).toBe("discworld"); }); + }); }); diff --git a/test/jasmine/tests/gl3dlayout_test.js b/test/jasmine/tests/gl3dlayout_test.js index f6e2fd24d07..a6694757bd5 100644 --- a/test/jasmine/tests/gl3dlayout_test.js +++ b/test/jasmine/tests/gl3dlayout_test.js @@ -1,257 +1,226 @@ -var Gl3d = require('@src/plots/gl3d'); -var Plots = require('@src/plots/plots'); - -var tinycolor = require('tinycolor2'); -var Color = require('@src/components/color'); - - -describe('Test Gl3d layout defaults', function() { - 'use strict'; - - describe('supplyLayoutDefaults', function() { - var layoutIn, layoutOut, fullData; - - var supplyLayoutDefaults = Gl3d.supplyLayoutDefaults; - - beforeEach(function() { - layoutOut = { - _has: Plots._hasPlotType - }; - - // needs a scene-ref in a trace in order to be detected - fullData = [ { type: 'scatter3d', scene: 'scene' }]; - }); - - it('should coerce aspectmode=ratio when ratio data is valid', function() { - var aspectratio = { - x: 1, - y: 2, - z: 1 - }; - - layoutIn = { - scene: { - aspectmode: 'manual', - aspectratio: aspectratio - } - }; - - var expected = { - scene: { - aspectmode: 'manual', - aspectratio: aspectratio, - bgcolor: 'rgba(0,0,0,0)' - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); - expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); - expect(layoutOut.scene.bgcolor).toBe(expected.scene.bgcolor); - }); - - - it('should coerce aspectmode=auto when aspect ratio data is invalid', function() { - var aspectratio = { - x: 'g', - y: 2, - z: 1 - }; - - layoutIn = { - scene: { - aspectmode: 'manual', - aspectratio: aspectratio - } - }; - - var expected = { - scene: { - aspectmode: 'auto', - aspectratio: {x: 1, y: 1, z: 1} - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); - expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); - }); - - - it('should coerce manual when valid ratio data but invalid aspectmode', function() { - var aspectratio = { - x: 1, - y: 2, - z: 1 - }; - - layoutIn = { - scene: { - aspectmode: {}, - aspectratio: aspectratio - } - }; - - var expected = { - scene: { - aspectmode: 'manual', - aspectratio: {x: 1, y: 2, z: 1} - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); - expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); - }); - - - it('should not coerce manual when invalid ratio data but invalid aspectmode', function() { - var aspectratio = { - x: 'g', - y: 2, - z: 1 - }; - - layoutIn = { - scene: { - aspectmode: {}, - aspectratio: aspectratio - } - }; - - var expected = { - scene: { - aspectmode: 'auto', - aspectratio: {x: 1, y: 1, z: 1} - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); - expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); - }); - - - it('should not coerce manual when valid ratio data and valid non-manual aspectmode', function() { - var aspectratio = { - x: 1, - y: 2, - z: 1 - }; - - layoutIn = { - scene: { - aspectmode: 'cube', - aspectratio: aspectratio - } - }; - - var expected = { - scene: { - aspectmode: 'cube', - aspectratio: {x: 1, y: 2, z: 1} - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); - expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); - }); - - it('should coerce dragmode', function() { - layoutIn = { scene: {} }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.dragmode) - .toBe('turntable', 'to turntable by default'); - - layoutIn = { scene: { dragmode: 'orbit' } }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.dragmode) - .toBe('orbit', 'to user val if valid'); - - layoutIn = { scene: {}, dragmode: 'orbit' }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.dragmode) - .toBe('orbit', 'to user layout val if valid and 3d only'); - - layoutIn = { scene: {}, dragmode: 'orbit' }; - layoutOut._basePlotModules = [{ name: 'cartesian' }]; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.dragmode) - .toBe('turntable', 'to default if not 3d only'); - - layoutIn = { scene: {}, dragmode: 'not gonna work' }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.dragmode) - .toBe('turntable', 'to default if not valid'); - }); - - it('should coerce hovermode', function() { - layoutIn = { scene: {} }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.hovermode) - .toBe('closest', 'to closest by default'); - - layoutIn = { scene: { hovermode: false } }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.hovermode) - .toBe(false, 'to user val if valid'); - - layoutIn = { scene: {}, hovermode: false }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.hovermode) - .toBe(false, 'to user layout val if valid and 3d only'); - - layoutIn = { scene: {}, hovermode: false }; - layoutOut._basePlotModules = [{ name: 'cartesian' }]; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.hovermode) - .toBe('closest', 'to default if not 3d only'); - - layoutIn = { scene: {}, hovermode: 'not gonna work' }; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.hovermode) - .toBe('closest', 'to default if not valid'); - }); - - it('should add data-only scenes into layoutIn', function() { - layoutIn = {}; - fullData = [{ type: 'scatter3d', scene: 'scene' }]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutIn.scene).toEqual({ - aspectratio: { x: 1, y: 1, z: 1 } - }); - }); - - it('should add scene data-only scenes into layoutIn (converse)', function() { - layoutIn = {}; - fullData = [{ type: 'scatter' }]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutIn.scene).toBe(undefined); - }); - - it('should use combo of \'axis.color\', bgcolor and lightFraction as default for \'axis.gridcolor\'', function() { - layoutIn = { - paper_bgcolor: 'green', - scene: { - bgcolor: 'yellow', - xaxis: { showgrid: true, color: 'red' }, - yaxis: { gridcolor: 'blue' }, - zaxis: { showgrid: true } - } - }; - - var bgColor = Color.combine('yellow', 'green'), - frac = 100 * (204 - 0x44) / (255 - 0x44); - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.scene.xaxis.gridcolor) - .toEqual(tinycolor.mix('red', bgColor, frac).toRgbString()); - expect(layoutOut.scene.yaxis.gridcolor).toEqual('blue'); - expect(layoutOut.scene.zaxis.gridcolor) - .toEqual(tinycolor.mix('#444', bgColor, frac).toRgbString()); - }); +var Gl3d = require("@src/plots/gl3d"); +var Plots = require("@src/plots/plots"); + +var tinycolor = require("tinycolor2"); +var Color = require("@src/components/color"); + +describe("Test Gl3d layout defaults", function() { + "use strict"; + describe("supplyLayoutDefaults", function() { + var layoutIn, layoutOut, fullData; + + var supplyLayoutDefaults = Gl3d.supplyLayoutDefaults; + + beforeEach(function() { + layoutOut = { _has: Plots._hasPlotType }; + + // needs a scene-ref in a trace in order to be detected + fullData = [{ type: "scatter3d", scene: "scene" }]; + }); + + it("should coerce aspectmode=ratio when ratio data is valid", function() { + var aspectratio = { x: 1, y: 2, z: 1 }; + + layoutIn = { scene: { aspectmode: "manual", aspectratio: aspectratio } }; + + var expected = { + scene: { + aspectmode: "manual", + aspectratio: aspectratio, + bgcolor: "rgba(0,0,0,0)" + } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); + expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); + expect(layoutOut.scene.bgcolor).toBe(expected.scene.bgcolor); + }); + + it( + "should coerce aspectmode=auto when aspect ratio data is invalid", + function() { + var aspectratio = { x: "g", y: 2, z: 1 }; + + layoutIn = { + scene: { aspectmode: "manual", aspectratio: aspectratio } + }; + + var expected = { + scene: { aspectmode: "auto", aspectratio: { x: 1, y: 1, z: 1 } } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); + expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); + } + ); + + it( + "should coerce manual when valid ratio data but invalid aspectmode", + function() { + var aspectratio = { x: 1, y: 2, z: 1 }; + + layoutIn = { scene: { aspectmode: {}, aspectratio: aspectratio } }; + + var expected = { + scene: { aspectmode: "manual", aspectratio: { x: 1, y: 2, z: 1 } } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); + expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); + } + ); + + it( + "should not coerce manual when invalid ratio data but invalid aspectmode", + function() { + var aspectratio = { x: "g", y: 2, z: 1 }; + + layoutIn = { scene: { aspectmode: {}, aspectratio: aspectratio } }; + + var expected = { + scene: { aspectmode: "auto", aspectratio: { x: 1, y: 1, z: 1 } } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); + expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); + } + ); + + it( + "should not coerce manual when valid ratio data and valid non-manual aspectmode", + function() { + var aspectratio = { x: 1, y: 2, z: 1 }; + + layoutIn = { scene: { aspectmode: "cube", aspectratio: aspectratio } }; + + var expected = { + scene: { aspectmode: "cube", aspectratio: { x: 1, y: 2, z: 1 } } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); + expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); + } + ); + + it("should coerce dragmode", function() { + layoutIn = { scene: {} }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.dragmode).toBe( + "turntable", + "to turntable by default" + ); + + layoutIn = { scene: { dragmode: "orbit" } }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.dragmode).toBe("orbit", "to user val if valid"); + + layoutIn = { scene: {}, dragmode: "orbit" }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.dragmode).toBe( + "orbit", + "to user layout val if valid and 3d only" + ); + + layoutIn = { scene: {}, dragmode: "orbit" }; + layoutOut._basePlotModules = [{ name: "cartesian" }]; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.dragmode).toBe( + "turntable", + "to default if not 3d only" + ); + + layoutIn = { scene: {}, dragmode: "not gonna work" }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.dragmode).toBe( + "turntable", + "to default if not valid" + ); + }); + + it("should coerce hovermode", function() { + layoutIn = { scene: {} }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.hovermode).toBe( + "closest", + "to closest by default" + ); + + layoutIn = { scene: { hovermode: false } }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.hovermode).toBe(false, "to user val if valid"); + + layoutIn = { scene: {}, hovermode: false }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.hovermode).toBe( + false, + "to user layout val if valid and 3d only" + ); + + layoutIn = { scene: {}, hovermode: false }; + layoutOut._basePlotModules = [{ name: "cartesian" }]; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.hovermode).toBe( + "closest", + "to default if not 3d only" + ); + + layoutIn = { scene: {}, hovermode: "not gonna work" }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.hovermode).toBe( + "closest", + "to default if not valid" + ); }); + + it("should add data-only scenes into layoutIn", function() { + layoutIn = {}; + fullData = [{ type: "scatter3d", scene: "scene" }]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn.scene).toEqual({ aspectratio: { x: 1, y: 1, z: 1 } }); + }); + + it( + "should add scene data-only scenes into layoutIn (converse)", + function() { + layoutIn = {}; + fullData = [{ type: "scatter" }]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn.scene).toBe(undefined); + } + ); + + it( + "should use combo of 'axis.color', bgcolor and lightFraction as default for 'axis.gridcolor'", + function() { + layoutIn = { + paper_bgcolor: "green", + scene: { + bgcolor: "yellow", + xaxis: { showgrid: true, color: "red" }, + yaxis: { gridcolor: "blue" }, + zaxis: { showgrid: true } + } + }; + + var bgColor = Color.combine("yellow", "green"), + frac = 100 * (204 - 0x44) / (255 - 0x44); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.xaxis.gridcolor).toEqual( + tinycolor.mix("red", bgColor, frac).toRgbString() + ); + expect(layoutOut.scene.yaxis.gridcolor).toEqual("blue"); + expect(layoutOut.scene.zaxis.gridcolor).toEqual( + tinycolor.mix("#444", bgColor, frac).toRgbString() + ); + } + ); + }); }); diff --git a/test/jasmine/tests/gl_plot_interact_basic_test.js b/test/jasmine/tests/gl_plot_interact_basic_test.js index 0397084d1a6..11aa25b1be7 100644 --- a/test/jasmine/tests/gl_plot_interact_basic_test.js +++ b/test/jasmine/tests/gl_plot_interact_basic_test.js @@ -1,83 +1,98 @@ -'use strict'; - -var Plotly = require('@lib/index'); -var mouseEvent = require('../assets/mouse_event'); +"use strict"; +var Plotly = require("@lib/index"); +var mouseEvent = require("../assets/mouse_event"); // Test utilities -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var failTest = require('../assets/fail_test'); - +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var failTest = require("../assets/fail_test"); // Expected shape of projection-related data var cameraStructure = { - up: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, - center: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, - eye: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)} + up: { + x: jasmine.any(Number), + y: jasmine.any(Number), + z: jasmine.any(Number) + }, + center: { + x: jasmine.any(Number), + y: jasmine.any(Number), + z: jasmine.any(Number) + }, + eye: { + x: jasmine.any(Number), + y: jasmine.any(Number), + z: jasmine.any(Number) + } }; function makePlot(gd, mock) { - return Plotly.plot(gd, mock.data, mock.layout); + return Plotly.plot(gd, mock.data, mock.layout); } function addEventCallback(graphDiv) { - var relayoutCallback = jasmine.createSpy('relayoutCallback'); - graphDiv.on('plotly_relayout', relayoutCallback); - return {graphDiv: graphDiv, relayoutCallback: relayoutCallback}; + var relayoutCallback = jasmine.createSpy("relayoutCallback"); + graphDiv.on("plotly_relayout", relayoutCallback); + return { graphDiv: graphDiv, relayoutCallback: relayoutCallback }; } function verifyInteractionEffects(tuple) { + // One 'drag': simulating fairly thoroughly as the mouseup event is also needed here + mouseEvent("mousemove", 400, 200); + mouseEvent("mousedown", 400, 200); + mouseEvent("mousemove", 320, 320, { buttons: 1 }); + mouseEvent("mouseup", 320, 320); - // One 'drag': simulating fairly thoroughly as the mouseup event is also needed here - mouseEvent('mousemove', 400, 200); - mouseEvent('mousedown', 400, 200); - mouseEvent('mousemove', 320, 320, {buttons: 1}); - mouseEvent('mouseup', 320, 320); + // Check event emission count + expect(tuple.relayoutCallback).toHaveBeenCalledTimes(1); - // Check event emission count - expect(tuple.relayoutCallback).toHaveBeenCalledTimes(1); + // Check structure of event callback value contents + expect(tuple.relayoutCallback).toHaveBeenCalledWith( + jasmine.objectContaining({ scene: cameraStructure }) + ); - // Check structure of event callback value contents - expect(tuple.relayoutCallback).toHaveBeenCalledWith(jasmine.objectContaining({scene: cameraStructure})); + // Check camera contents on the DIV layout + var divCamera = tuple.graphDiv.layout.scene.camera; - // Check camera contents on the DIV layout - var divCamera = tuple.graphDiv.layout.scene.camera; + expect(divCamera).toEqual(cameraStructure); - expect(divCamera).toEqual(cameraStructure); - - return tuple.graphDiv; + return tuple.graphDiv; } function testEvents(plot) { - return plot - .then(function(graphDiv) { - var tuple = addEventCallback(graphDiv); // TODO disuse tuple with ES6 - verifyInteractionEffects(tuple); - }); + return plot.then(function(graphDiv) { + var tuple = addEventCallback(graphDiv); + // TODO disuse tuple with ES6 + verifyInteractionEffects(tuple); + }); } -describe('gl3d plots', function() { - - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); - - it('should respond to drag interactions with mock of unset camera', function(done) { - testEvents(makePlot(gd, require('@mocks/gl3d_scatter3d-connectgaps.json'))) - .then(null, failTest) // current linter balks on .catch with 'dot-notation'; fixme a linter - .then(done); - }); - - it('should respond to drag interactions with mock of partially set camera', function(done) { - testEvents(makePlot(gd, require('@mocks/gl3d_errorbars_zx.json'))) - .then(null, failTest) - .then(done); - }); +describe("gl3d plots", function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("should respond to drag interactions with mock of unset camera", function( + done + ) { + testEvents(makePlot(gd, require("@mocks/gl3d_scatter3d-connectgaps.json"))) + .then(null, failTest) + .then(done); + }); + + it( + "should respond to drag interactions with mock of partially set camera", + function(done) { + testEvents(makePlot(gd, require("@mocks/gl3d_errorbars_zx.json"))) + .then(null, failTest) + .then(done); + } + ); }); diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 2a7960b93e3..aaf0d1029ff 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -1,905 +1,926 @@ -var d3 = require('d3'); +var d3 = require("d3"); -var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); -var Drawing = require('@src/components/drawing'); +var Plotly = require("@lib/index"); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); +var Drawing = require("@src/components/drawing"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); -var selectButton = require('../assets/modebar_button'); -var customMatchers = require('../assets/custom_matchers'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); +var selectButton = require("../assets/modebar_button"); +var customMatchers = require("../assets/custom_matchers"); /* * WebGL interaction test cases fail on the CircleCI * most likely due to a WebGL/driver issue * */ - var MODEBAR_DELAY = 500; +describe("Test gl plot interactions", function() { + "use strict"; + var gd; -describe('Test gl plot interactions', function() { - 'use strict'; + beforeEach(function() { + jasmine.addMatchers(customMatchers); + }); - var gd; + afterEach(function() { + var fullLayout = gd._fullLayout, sceneIds; - beforeEach(function() { - jasmine.addMatchers(customMatchers); - }); + sceneIds = Plots.getSubplotIds(fullLayout, "gl3d"); + sceneIds.forEach(function(id) { + var scene = fullLayout[id]._scene; - afterEach(function() { - var fullLayout = gd._fullLayout, - sceneIds; + if (scene.glplot) scene.destroy(); + }); - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'); - sceneIds.forEach(function(id) { - var scene = fullLayout[id]._scene; + sceneIds = Plots.getSubplotIds(fullLayout, "gl2d"); + sceneIds.forEach(function(id) { + var scene2d = fullLayout._plots[id]._scene2d; - if(scene.glplot) scene.destroy(); - }); + if (scene2d.glplot) { + scene2d.stopped = true; + scene2d.destroy(); + } + }); - sceneIds = Plots.getSubplotIds(fullLayout, 'gl2d'); - sceneIds.forEach(function(id) { - var scene2d = fullLayout._plots[id]._scene2d; + destroyGraphDiv(); + }); - if(scene2d.glplot) { - scene2d.stopped = true; - scene2d.destroy(); - } - }); + // put callback in the event queue + function delay(done) { + setTimeout(done, 0); + } - destroyGraphDiv(); - }); + describe("gl3d plots", function() { + var mock = require("@mocks/gl3d_marker-arrays.json"); - // put callback in the event queue - function delay(done) { - setTimeout(done, 0); + function mouseEventScatter3d(type, opts) { + mouseEvent(type, 605, 271, opts); } - describe('gl3d plots', function() { - var mock = require('@mocks/gl3d_marker-arrays.json'); + function countCanvases() { + return d3.selectAll("canvas").size(); + } - function mouseEventScatter3d(type, opts) { - mouseEvent(type, 605, 271, opts); - } + beforeEach(function(done) { + gd = createGraphDiv(); - function countCanvases() { - return d3.selectAll('canvas').size(); - } + var mockCopy = Lib.extendDeep({}, mock); - beforeEach(function(done) { - gd = createGraphDiv(); + // lines, markers, text, error bars and surfaces each + // correspond to one glplot object + mockCopy.data[0].mode = "lines+markers+text"; + mockCopy.data[0].error_z = { value: 10 }; + mockCopy.data[0].surfaceaxis = 2; + mockCopy.layout.showlegend = true; - var mockCopy = Lib.extendDeep({}, mock); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + delay(done); + }); + }); - // lines, markers, text, error bars and surfaces each - // correspond to one glplot object - mockCopy.data[0].mode = 'lines+markers+text'; - mockCopy.data[0].error_z = { value: 10 }; - mockCopy.data[0].surfaceaxis = 2; - mockCopy.layout.showlegend = true; + describe("scatter3d hover", function() { + var node, ptData; - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - delay(done); - }); + beforeEach(function(done) { + gd.on("plotly_hover", function(eventData) { + ptData = eventData.points[0]; }); - describe('scatter3d hover', function() { - - var node, ptData; - - beforeEach(function(done) { - gd.on('plotly_hover', function(eventData) { - ptData = eventData.points[0]; - }); - - mouseEventScatter3d('mouseover'); - - delay(done); - }); - - it('should have', function() { - node = d3.selectAll('g.hovertext'); - expect(node.size()).toEqual(1, 'one hover text group'); - - node = d3.selectAll('g.hovertext').selectAll('tspan')[0]; - expect(node[0].innerHTML).toEqual('x: 140.72', 'x val on hover'); - expect(node[1].innerHTML).toEqual('y: −96.97', 'y val on hover'); - expect(node[2].innerHTML).toEqual('z: −96.97', 'z val on hover'); - - expect(Object.keys(ptData)).toEqual([ - 'x', 'y', 'z', - 'data', 'fullData', 'curveNumber', 'pointNumber' - ], 'correct hover data fields'); - - expect(ptData.x).toBe('140.72', 'x val hover data'); - expect(ptData.y).toBe('−96.97', 'y val hover data'); - expect(ptData.z).toEqual('−96.97', 'z val hover data'); - expect(ptData.curveNumber).toEqual(0, 'curveNumber hover data'); - expect(ptData.pointNumber).toEqual(2, 'pointNumber hover data'); - }); - - }); + mouseEventScatter3d("mouseover"); - describe('scatter3d click events', function() { - var ptData; + delay(done); + }); - beforeEach(function(done) { - gd.on('plotly_click', function(eventData) { - ptData = eventData.points[0]; - }); + it("should have", function() { + node = d3.selectAll("g.hovertext"); + expect(node.size()).toEqual(1, "one hover text group"); - // N.B. gl3d click events are 'mouseover' events - // with button 1 pressed - mouseEventScatter3d('mouseover', {buttons: 1}); + node = d3.selectAll("g.hovertext").selectAll("tspan")[0]; + expect(node[0].innerHTML).toEqual("x: 140.72", "x val on hover"); + expect(node[1].innerHTML).toEqual("y: \u221296.97", "y val on hover"); + expect(node[2].innerHTML).toEqual("z: \u221296.97", "z val on hover"); - delay(done); - }); + expect(Object.keys(ptData)).toEqual( + ["x", "y", "z", "data", "fullData", "curveNumber", "pointNumber"], + "correct hover data fields" + ); - it('should have', function() { - expect(Object.keys(ptData)).toEqual([ - 'x', 'y', 'z', - 'data', 'fullData', 'curveNumber', 'pointNumber' - ], 'correct hover data fields'); + expect(ptData.x).toBe("140.72", "x val hover data"); + expect(ptData.y).toBe("\u221296.97", "y val hover data"); + expect(ptData.z).toEqual("\u221296.97", "z val hover data"); + expect(ptData.curveNumber).toEqual(0, "curveNumber hover data"); + expect(ptData.pointNumber).toEqual(2, "pointNumber hover data"); + }); + }); + describe("scatter3d click events", function() { + var ptData; - expect(ptData.x).toBe('140.72', 'x val click data'); - expect(ptData.y).toBe('−96.97', 'y val click data'); - expect(ptData.z).toEqual('−96.97', 'z val click data'); - expect(ptData.curveNumber).toEqual(0, 'curveNumber click data'); - expect(ptData.pointNumber).toEqual(2, 'pointNumber click data'); - }); + beforeEach(function(done) { + gd.on("plotly_click", function(eventData) { + ptData = eventData.points[0]; }); - it('should be able to reversibly change trace type', function(done) { - var sceneLayout = { aspectratio: { x: 1, y: 1, z: 1 } }; - - expect(countCanvases()).toEqual(1); - expect(gd.layout.scene).toEqual(sceneLayout); - expect(gd.layout.xaxis).toBeUndefined(); - expect(gd.layout.yaxis).toBeUndefined(); - expect(gd._fullLayout._has('gl3d')).toBe(true); - expect(gd._fullLayout.scene._scene).toBeDefined(); - - Plotly.restyle(gd, 'type', 'scatter').then(function() { - expect(countCanvases()).toEqual(0); - expect(gd.layout.scene).toEqual(sceneLayout); - expect(gd.layout.xaxis).toBeDefined(); - expect(gd.layout.yaxis).toBeDefined(); - expect(gd._fullLayout._has('gl3d')).toBe(false); - expect(gd._fullLayout.scene).toBeUndefined(); - - return Plotly.restyle(gd, 'type', 'scatter3d'); - }).then(function() { - expect(countCanvases()).toEqual(1); - expect(gd.layout.scene).toEqual(sceneLayout); - expect(gd.layout.xaxis).toBeDefined(); - expect(gd.layout.yaxis).toBeDefined(); - expect(gd._fullLayout._has('gl3d')).toBe(true); - expect(gd._fullLayout.scene._scene).toBeDefined(); + // N.B. gl3d click events are 'mouseover' events + // with button 1 pressed + mouseEventScatter3d("mouseover", { buttons: 1 }); + + delay(done); + }); + + it("should have", function() { + expect(Object.keys(ptData)).toEqual( + ["x", "y", "z", "data", "fullData", "curveNumber", "pointNumber"], + "correct hover data fields" + ); + + expect(ptData.x).toBe("140.72", "x val click data"); + expect(ptData.y).toBe("\u221296.97", "y val click data"); + expect(ptData.z).toEqual("\u221296.97", "z val click data"); + expect(ptData.curveNumber).toEqual(0, "curveNumber click data"); + expect(ptData.pointNumber).toEqual(2, "pointNumber click data"); + }); + }); - done(); - }); - }); + it("should be able to reversibly change trace type", function(done) { + var sceneLayout = { aspectratio: { x: 1, y: 1, z: 1 } }; - it('should be able to delete the last trace', function(done) { - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countCanvases()).toEqual(0); - expect(gd._fullLayout._has('gl3d')).toBe(false); - expect(gd._fullLayout.scene).toBeUndefined(); + expect(countCanvases()).toEqual(1); + expect(gd.layout.scene).toEqual(sceneLayout); + expect(gd.layout.xaxis).toBeUndefined(); + expect(gd.layout.yaxis).toBeUndefined(); + expect(gd._fullLayout._has("gl3d")).toBe(true); + expect(gd._fullLayout.scene._scene).toBeDefined(); - done(); - }); + Plotly.restyle(gd, "type", "scatter") + .then(function() { + expect(countCanvases()).toEqual(0); + expect(gd.layout.scene).toEqual(sceneLayout); + expect(gd.layout.xaxis).toBeDefined(); + expect(gd.layout.yaxis).toBeDefined(); + expect(gd._fullLayout._has("gl3d")).toBe(false); + expect(gd._fullLayout.scene).toBeUndefined(); + + return Plotly.restyle(gd, "type", "scatter3d"); + }) + .then(function() { + expect(countCanvases()).toEqual(1); + expect(gd.layout.scene).toEqual(sceneLayout); + expect(gd.layout.xaxis).toBeDefined(); + expect(gd.layout.yaxis).toBeDefined(); + expect(gd._fullLayout._has("gl3d")).toBe(true); + expect(gd._fullLayout.scene._scene).toBeDefined(); + + done(); }); + }); - it('should be able to toggle visibility', function(done) { - var objects = gd._fullLayout.scene._scene.glplot.objects; - - expect(objects.length).toEqual(5); - - Plotly.restyle(gd, 'visible', 'legendonly').then(function() { - expect(objects.length).toEqual(0); - - return Plotly.restyle(gd, 'visible', true); - }).then(function() { - expect(objects.length).toEqual(5); - - done(); - }); - }); + it("should be able to delete the last trace", function(done) { + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countCanvases()).toEqual(0); + expect(gd._fullLayout._has("gl3d")).toBe(false); + expect(gd._fullLayout.scene).toBeUndefined(); + done(); + }); }); - describe('gl2d plots', function() { - var mock = require('@mocks/gl2d_10.json'), - modeBar, relayoutCallback; - - beforeEach(function(done) { - gd = createGraphDiv(); + it("should be able to toggle visibility", function(done) { + var objects = gd._fullLayout.scene._scene.glplot.objects; - Plotly.plot(gd, mock.data, mock.layout).then(function() { + expect(objects.length).toEqual(5); - modeBar = gd._fullLayout._modeBar; - relayoutCallback = jasmine.createSpy('relayoutCallback'); + Plotly.restyle(gd, "visible", "legendonly") + .then(function() { + expect(objects.length).toEqual(0); - gd.on('plotly_relayout', relayoutCallback); + return Plotly.restyle(gd, "visible", true); + }) + .then(function() { + expect(objects.length).toEqual(5); - delay(done); - }); + done(); }); + }); + }); - it('has one *canvas* node', function() { - var nodes = d3.selectAll('canvas'); - expect(nodes[0].length).toEqual(1); - }); + describe("gl2d plots", function() { + var mock = require("@mocks/gl2d_10.json"), modeBar, relayoutCallback; - it('should respond to drag interactions', function(done) { + beforeEach(function(done) { + gd = createGraphDiv(); - function mouseTo(p0, p1) { - mouseEvent('mousemove', p0[0], p0[1]); - mouseEvent('mousedown', p0[0], p0[1], { buttons: 1 }); - mouseEvent('mousemove', p1[0], p1[1], { buttons: 1 }); - mouseEvent('mouseup', p1[0], p1[1]); - } + Plotly.plot(gd, mock.data, mock.layout).then(function() { + modeBar = gd._fullLayout._modeBar; + relayoutCallback = jasmine.createSpy("relayoutCallback"); - jasmine.addMatchers(customMatchers); + gd.on("plotly_relayout", relayoutCallback); - var precision = 5; + delay(done); + }); + }); - var buttonPan = selectButton(modeBar, 'pan2d'); + it("has one *canvas* node", function() { + var nodes = d3.selectAll("canvas"); + expect(nodes[0].length).toEqual(1); + }); - var originalX = [-0.022068095838587643, 5.022068095838588]; - var originalY = [-0.21331533513634046, 5.851205650049042]; + it("should respond to drag interactions", function(done) { + function mouseTo(p0, p1) { + mouseEvent("mousemove", p0[0], p0[1]); + mouseEvent("mousedown", p0[0], p0[1], { buttons: 1 }); + mouseEvent("mousemove", p1[0], p1[1], { buttons: 1 }); + mouseEvent("mouseup", p1[0], p1[1]); + } - var newX = [-0.23224043715846995, 4.811895754518705]; - var newY = [-1.2962655110623016, 4.768255474123081]; + jasmine.addMatchers(customMatchers); - expect(gd.layout.xaxis.autorange).toBe(true); - expect(gd.layout.yaxis.autorange).toBe(true); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + var precision = 5; - // Switch to pan mode - expect(buttonPan.isActive()).toBe(false); // initially, zoom is active - buttonPan.click(); - expect(buttonPan.isActive()).toBe(true); // switched on dragmode + var buttonPan = selectButton(modeBar, "pan2d"); - // Switching mode must not change visible range - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + var originalX = [-0.022068095838587643, 5.022068095838588]; + var originalY = [-0.21331533513634046, 5.851205650049042]; - setTimeout(function() { - relayoutCallback.calls.reset(); + var newX = [-0.23224043715846995, 4.811895754518705]; + var newY = [-1.2962655110623016, 4.768255474123081]; - // Drag scene along the X axis - mouseTo([200, 200], [220, 200]); + expect(gd.layout.xaxis.autorange).toBe(true); + expect(gd.layout.yaxis.autorange).toBe(true); + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - expect(gd.layout.xaxis.autorange).toBe(false); - expect(gd.layout.yaxis.autorange).toBe(false); + // Switch to pan mode + expect(buttonPan.isActive()).toBe(false); + // initially, zoom is active + buttonPan.click(); + expect(buttonPan.isActive()).toBe(true); - expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + // switched on dragmode + // Switching mode must not change visible range + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - // Drag scene back along the X axis - mouseTo([220, 200], [200, 200]); + setTimeout( + function() { + relayoutCallback.calls.reset(); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + // Drag scene along the X axis + mouseTo([200, 200], [220, 200]); - // Drag scene along the Y axis - mouseTo([200, 200], [200, 150]); + expect(gd.layout.xaxis.autorange).toBe(false); + expect(gd.layout.yaxis.autorange).toBe(false); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); + expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - // Drag scene back along the Y axis - mouseTo([200, 150], [200, 200]); + // Drag scene back along the X axis + mouseTo([220, 200], [200, 200]); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - // Drag scene along both the X and Y axis - mouseTo([200, 200], [220, 150]); + // Drag scene along the Y axis + mouseTo([200, 200], [200, 150]); - expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); - // Drag scene back along the X and Y axis - mouseTo([220, 150], [200, 200]); + // Drag scene back along the Y axis + mouseTo([200, 150], [200, 200]); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - setTimeout(function() { + // Drag scene along both the X and Y axis + mouseTo([200, 200], [220, 150]); - // callback count expectation: X and back; Y and back; XY and back - expect(relayoutCallback).toHaveBeenCalledTimes(6); + expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); - // a callback value structure and contents check - expect(relayoutCallback).toHaveBeenCalledWith(jasmine.objectContaining({ - lastInputTime: jasmine.any(Number), - xaxis: [jasmine.any(Number), jasmine.any(Number)], - yaxis: [jasmine.any(Number), jasmine.any(Number)] - })); + // Drag scene back along the X and Y axis + mouseTo([220, 150], [200, 200]); - done(); + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - }, MODEBAR_DELAY); + setTimeout( + function() { + // callback count expectation: X and back; Y and back; XY and back + expect(relayoutCallback).toHaveBeenCalledTimes(6); - }, MODEBAR_DELAY); - }); + // a callback value structure and contents check + expect(relayoutCallback).toHaveBeenCalledWith( + jasmine.objectContaining({ + lastInputTime: jasmine.any(Number), + xaxis: [jasmine.any(Number), jasmine.any(Number)], + yaxis: [jasmine.any(Number), jasmine.any(Number)] + }) + ); + + done(); + }, + MODEBAR_DELAY + ); + }, + MODEBAR_DELAY + ); + }); - it('should be able to toggle visibility', function(done) { - var OBJECT_PER_TRACE = 5; + it("should be able to toggle visibility", function(done) { + var OBJECT_PER_TRACE = 5; - var objects = function() { - return gd._fullLayout._plots.xy._scene2d.glplot.objects; - }; + var objects = function() { + return gd._fullLayout._plots.xy._scene2d.glplot.objects; + }; - expect(objects().length).toEqual(OBJECT_PER_TRACE); + expect(objects().length).toEqual(OBJECT_PER_TRACE); - Plotly.restyle(gd, 'visible', 'legendonly').then(function() { - expect(objects().length).toEqual(OBJECT_PER_TRACE); - expect(objects()[0].data.length).toEqual(0); + Plotly.restyle(gd, "visible", "legendonly") + .then(function() { + expect(objects().length).toEqual(OBJECT_PER_TRACE); + expect(objects()[0].data.length).toEqual(0); - return Plotly.restyle(gd, 'visible', true); - }) - .then(function() { - expect(objects().length).toEqual(OBJECT_PER_TRACE); - expect(objects()[0].data.length).not.toEqual(0); + return Plotly.restyle(gd, "visible", true); + }) + .then(function() { + expect(objects().length).toEqual(OBJECT_PER_TRACE); + expect(objects()[0].data.length).not.toEqual(0); - return Plotly.restyle(gd, 'visible', false); - }) - .then(function() { - expect(gd._fullLayout._plots.xy._scene2d).toBeUndefined(); + return Plotly.restyle(gd, "visible", false); + }) + .then(function() { + expect(gd._fullLayout._plots.xy._scene2d).toBeUndefined(); - return Plotly.restyle(gd, 'visible', true); - }) - .then(function() { - expect(objects().length).toEqual(OBJECT_PER_TRACE); - expect(objects()[0].data.length).not.toEqual(0); - }) - .then(done); - }); + return Plotly.restyle(gd, "visible", true); + }) + .then(function() { + expect(objects().length).toEqual(OBJECT_PER_TRACE); + expect(objects()[0].data.length).not.toEqual(0); + }) + .then(done); }); + }); - describe('gl3d event handlers', function() { - var modeBar, relayoutCallback; + describe("gl3d event handlers", function() { + var modeBar, relayoutCallback; - beforeEach(function(done) { - var mockData = [{ - type: 'scatter3d' - }, { - type: 'surface', scene: 'scene2' - }]; + beforeEach(function(done) { + var mockData = [ + { type: "scatter3d" }, + { type: "surface", scene: "scene2" } + ]; - var mockLayout = { - scene: { camera: { eye: { x: 0.1, y: 0.1, z: 1 }}}, - scene2: { camera: { eye: { x: 2.5, y: 2.5, z: 2.5 }}} - }; + var mockLayout = { + scene: { camera: { eye: { x: 0.1, y: 0.1, z: 1 } } }, + scene2: { camera: { eye: { x: 2.5, y: 2.5, z: 2.5 } } } + }; - gd = createGraphDiv(); - Plotly.plot(gd, mockData, mockLayout).then(function() { + gd = createGraphDiv(); + Plotly.plot(gd, mockData, mockLayout).then(function() { + modeBar = gd._fullLayout._modeBar; - modeBar = gd._fullLayout._modeBar; + relayoutCallback = jasmine.createSpy("relayoutCallback"); - relayoutCallback = jasmine.createSpy('relayoutCallback'); + gd.on("plotly_relayout", relayoutCallback); - gd.on('plotly_relayout', relayoutCallback); + delay(done); + }); + }); - delay(done); - }); - }); + function assertScenes(cont, attr, val) { + var sceneIds = Plots.getSubplotIds(cont, "gl3d"); - function assertScenes(cont, attr, val) { - var sceneIds = Plots.getSubplotIds(cont, 'gl3d'); + sceneIds.forEach(function(sceneId) { + var thisVal = Lib.nestedProperty(cont[sceneId], attr).get(); + expect(thisVal).toEqual(val); + }); + } - sceneIds.forEach(function(sceneId) { - var thisVal = Lib.nestedProperty(cont[sceneId], attr).get(); - expect(thisVal).toEqual(val); - }); + describe("modebar click handlers", function() { + it( + "button zoom3d should updates the scene dragmode and dragmode button", + function() { + var buttonTurntable = selectButton(modeBar, "tableRotation"), + buttonZoom3d = selectButton(modeBar, "zoom3d"); + + assertScenes(gd._fullLayout, "dragmode", "turntable"); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonZoom3d.isActive()).toBe(false); + + buttonZoom3d.click(); + assertScenes(gd.layout, "dragmode", "zoom"); + expect(gd.layout.dragmode).toBe(undefined); + expect(gd._fullLayout.dragmode).toBe("zoom"); + expect(buttonTurntable.isActive()).toBe(false); + expect(buttonZoom3d.isActive()).toBe(true); + + buttonTurntable.click(); + assertScenes(gd._fullLayout, "dragmode", "turntable"); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonZoom3d.isActive()).toBe(false); + } + ); + + it( + "button pan3d should updates the scene dragmode and dragmode button", + function() { + var buttonTurntable = selectButton(modeBar, "tableRotation"), + buttonPan3d = selectButton(modeBar, "pan3d"); + + assertScenes(gd._fullLayout, "dragmode", "turntable"); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonPan3d.isActive()).toBe(false); + + buttonPan3d.click(); + assertScenes(gd.layout, "dragmode", "pan"); + expect(gd.layout.dragmode).toBe(undefined); + expect(gd._fullLayout.dragmode).toBe("zoom"); + expect(buttonTurntable.isActive()).toBe(false); + expect(buttonPan3d.isActive()).toBe(true); + + buttonTurntable.click(); + assertScenes(gd._fullLayout, "dragmode", "turntable"); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonPan3d.isActive()).toBe(false); + } + ); + + it( + "button orbitRotation should updates the scene dragmode and dragmode button", + function() { + var buttonTurntable = selectButton(modeBar, "tableRotation"), + buttonOrbit = selectButton(modeBar, "orbitRotation"); + + assertScenes(gd._fullLayout, "dragmode", "turntable"); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonOrbit.isActive()).toBe(false); + + buttonOrbit.click(); + assertScenes(gd.layout, "dragmode", "orbit"); + expect(gd.layout.dragmode).toBe(undefined); + expect(gd._fullLayout.dragmode).toBe("zoom"); + expect(buttonTurntable.isActive()).toBe(false); + expect(buttonOrbit.isActive()).toBe(true); + + buttonTurntable.click(); + assertScenes(gd._fullLayout, "dragmode", "turntable"); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonOrbit.isActive()).toBe(false); } + ); + + it( + "button hoverClosest3d should update the scene hovermode and spikes", + function() { + var buttonHover = selectButton(modeBar, "hoverClosest3d"); + + assertScenes(gd._fullLayout, "hovermode", "closest"); + expect(buttonHover.isActive()).toBe(true); + + buttonHover.click(); + assertScenes(gd._fullLayout, "hovermode", false); + assertScenes(gd._fullLayout, "xaxis.showspikes", false); + assertScenes(gd._fullLayout, "yaxis.showspikes", false); + assertScenes(gd._fullLayout, "zaxis.showspikes", false); + expect(buttonHover.isActive()).toBe(false); + + buttonHover.click(); + assertScenes(gd._fullLayout, "hovermode", "closest"); + assertScenes(gd._fullLayout, "xaxis.showspikes", true); + assertScenes(gd._fullLayout, "yaxis.showspikes", true); + assertScenes(gd._fullLayout, "zaxis.showspikes", true); + expect(buttonHover.isActive()).toBe(true); + } + ); - describe('modebar click handlers', function() { - - it('button zoom3d should updates the scene dragmode and dragmode button', function() { - var buttonTurntable = selectButton(modeBar, 'tableRotation'), - buttonZoom3d = selectButton(modeBar, 'zoom3d'); - - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonZoom3d.isActive()).toBe(false); - - buttonZoom3d.click(); - assertScenes(gd.layout, 'dragmode', 'zoom'); - expect(gd.layout.dragmode).toBe(undefined); - expect(gd._fullLayout.dragmode).toBe('zoom'); - expect(buttonTurntable.isActive()).toBe(false); - expect(buttonZoom3d.isActive()).toBe(true); - - buttonTurntable.click(); - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonZoom3d.isActive()).toBe(false); - }); - - it('button pan3d should updates the scene dragmode and dragmode button', function() { - var buttonTurntable = selectButton(modeBar, 'tableRotation'), - buttonPan3d = selectButton(modeBar, 'pan3d'); - - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonPan3d.isActive()).toBe(false); - - buttonPan3d.click(); - assertScenes(gd.layout, 'dragmode', 'pan'); - expect(gd.layout.dragmode).toBe(undefined); - expect(gd._fullLayout.dragmode).toBe('zoom'); - expect(buttonTurntable.isActive()).toBe(false); - expect(buttonPan3d.isActive()).toBe(true); - - buttonTurntable.click(); - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonPan3d.isActive()).toBe(false); - }); - - it('button orbitRotation should updates the scene dragmode and dragmode button', function() { - var buttonTurntable = selectButton(modeBar, 'tableRotation'), - buttonOrbit = selectButton(modeBar, 'orbitRotation'); - - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonOrbit.isActive()).toBe(false); - - buttonOrbit.click(); - assertScenes(gd.layout, 'dragmode', 'orbit'); - expect(gd.layout.dragmode).toBe(undefined); - expect(gd._fullLayout.dragmode).toBe('zoom'); - expect(buttonTurntable.isActive()).toBe(false); - expect(buttonOrbit.isActive()).toBe(true); - - buttonTurntable.click(); - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonOrbit.isActive()).toBe(false); - }); - - it('button hoverClosest3d should update the scene hovermode and spikes', function() { - var buttonHover = selectButton(modeBar, 'hoverClosest3d'); - - assertScenes(gd._fullLayout, 'hovermode', 'closest'); - expect(buttonHover.isActive()).toBe(true); - - buttonHover.click(); - assertScenes(gd._fullLayout, 'hovermode', false); - assertScenes(gd._fullLayout, 'xaxis.showspikes', false); - assertScenes(gd._fullLayout, 'yaxis.showspikes', false); - assertScenes(gd._fullLayout, 'zaxis.showspikes', false); - expect(buttonHover.isActive()).toBe(false); - - buttonHover.click(); - assertScenes(gd._fullLayout, 'hovermode', 'closest'); - assertScenes(gd._fullLayout, 'xaxis.showspikes', true); - assertScenes(gd._fullLayout, 'yaxis.showspikes', true); - assertScenes(gd._fullLayout, 'zaxis.showspikes', true); - expect(buttonHover.isActive()).toBe(true); - }); - - it('button resetCameraDefault3d should reset camera to default', function(done) { - var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); - - expect(gd._fullLayout.scene._scene.cameraInitial.eye).toEqual({ x: 0.1, y: 0.1, z: 1 }); - expect(gd._fullLayout.scene2._scene.cameraInitial.eye).toEqual({ x: 2.5, y: 2.5, z: 2.5 }); - - gd.once('plotly_relayout', function() { - assertScenes(gd._fullLayout, 'camera.eye.x', 1.25); - assertScenes(gd._fullLayout, 'camera.eye.y', 1.25); - assertScenes(gd._fullLayout, 'camera.eye.z', 1.25); - - expect(gd._fullLayout.scene._scene.getCamera().eye.z).toBeCloseTo(1.25); - expect(gd._fullLayout.scene2._scene.getCamera().eye.z).toBeCloseTo(1.25); - - done(); - }); + it("button resetCameraDefault3d should reset camera to default", function( + done + ) { + var buttonDefault = selectButton(modeBar, "resetCameraDefault3d"); - buttonDefault.click(); - }); - - it('button resetCameraLastSave3d should reset camera to default', function(done) { - var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); - var buttonLastSave = selectButton(modeBar, 'resetCameraLastSave3d'); - - function assertCameraEye(sceneLayout, eyeX, eyeY, eyeZ) { - expect(sceneLayout.camera.eye.x).toEqual(eyeX); - expect(sceneLayout.camera.eye.y).toEqual(eyeY); - expect(sceneLayout.camera.eye.z).toEqual(eyeZ); - - var camera = sceneLayout._scene.getCamera(); - expect(camera.eye.x).toBeCloseTo(eyeX); - expect(camera.eye.y).toBeCloseTo(eyeY); - expect(camera.eye.z).toBeCloseTo(eyeZ); - } - - Plotly.relayout(gd, { - 'scene.camera.eye.z': 4, - 'scene2.camera.eye.z': 5 - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); - assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); - - return new Promise(function(resolve) { - gd.once('plotly_relayout', resolve); - buttonLastSave.click(); - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 1); - assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 2.5); - - return new Promise(function(resolve) { - gd.once('plotly_relayout', resolve); - buttonDefault.click(); - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 1.25, 1.25, 1.25); - assertCameraEye(gd._fullLayout.scene2, 1.25, 1.25, 1.25); - - return new Promise(function(resolve) { - gd.once('plotly_relayout', resolve); - buttonLastSave.click(); - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 1); - assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 2.5); - - delete gd._fullLayout.scene._scene.cameraInitial; - delete gd._fullLayout.scene2._scene.cameraInitial; - - Plotly.relayout(gd, { - 'scene.bgcolor': '#d3d3d3', - 'scene.camera.eye.z': 4, - 'scene2.camera.eye.z': 5 - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); - assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); - - return new Promise(function(resolve) { - gd.once('plotly_relayout', resolve); - buttonDefault.click(); - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 1.25, 1.25, 1.25); - assertCameraEye(gd._fullLayout.scene2, 1.25, 1.25, 1.25); - - return new Promise(function(resolve) { - gd.once('plotly_relayout', resolve); - buttonLastSave.click(); - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); - assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); - }) - .then(done); - - }); + expect(gd._fullLayout.scene._scene.cameraInitial.eye).toEqual({ + x: 0.1, + y: 0.1, + z: 1 + }); + expect(gd._fullLayout.scene2._scene.cameraInitial.eye).toEqual({ + x: 2.5, + y: 2.5, + z: 2.5 }); - describe('drag and wheel interactions', function() { - it('should update the scene camera', function(done) { - var sceneLayout = gd._fullLayout.scene, - sceneLayout2 = gd._fullLayout.scene2, - sceneTarget = gd.querySelector('.svg-container .gl-container #scene canvas'), - sceneTarget2 = gd.querySelector('.svg-container .gl-container #scene2 canvas'); - - expect(sceneLayout.camera.eye) - .toEqual({x: 0.1, y: 0.1, z: 1}); - expect(sceneLayout2.camera.eye) - .toEqual({x: 2.5, y: 2.5, z: 2.5}); - - // Wheel scene 1 - sceneTarget.dispatchEvent(new WheelEvent('wheel', {deltaY: 1})); - - // Wheel scene 2 - sceneTarget2.dispatchEvent(new WheelEvent('wheel', {deltaY: 1})); + gd.once("plotly_relayout", function() { + assertScenes(gd._fullLayout, "camera.eye.x", 1.25); + assertScenes(gd._fullLayout, "camera.eye.y", 1.25); + assertScenes(gd._fullLayout, "camera.eye.z", 1.25); - setTimeout(function() { + expect(gd._fullLayout.scene._scene.getCamera().eye.z).toBeCloseTo( + 1.25 + ); + expect(gd._fullLayout.scene2._scene.getCamera().eye.z).toBeCloseTo( + 1.25 + ); - expect(relayoutCallback).toHaveBeenCalledTimes(2); + done(); + }); - relayoutCallback.calls.reset(); + buttonDefault.click(); + }); + + it( + "button resetCameraLastSave3d should reset camera to default", + function(done) { + var buttonDefault = selectButton(modeBar, "resetCameraDefault3d"); + var buttonLastSave = selectButton(modeBar, "resetCameraLastSave3d"); + + function assertCameraEye(sceneLayout, eyeX, eyeY, eyeZ) { + expect(sceneLayout.camera.eye.x).toEqual(eyeX); + expect(sceneLayout.camera.eye.y).toEqual(eyeY); + expect(sceneLayout.camera.eye.z).toEqual(eyeZ); + + var camera = sceneLayout._scene.getCamera(); + expect(camera.eye.x).toBeCloseTo(eyeX); + expect(camera.eye.y).toBeCloseTo(eyeY); + expect(camera.eye.z).toBeCloseTo(eyeZ); + } + + Plotly.relayout(gd, { + "scene.camera.eye.z": 4, + "scene2.camera.eye.z": 5 + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); - // Drag scene 1 - sceneTarget.dispatchEvent(new MouseEvent('mousedown', {x: 0, y: 0})); - sceneTarget.dispatchEvent(new MouseEvent('mousemove', { x: 100, y: 100})); - sceneTarget.dispatchEvent(new MouseEvent('mouseup', { x: 100, y: 100})); + return new Promise(function(resolve) { + gd.once("plotly_relayout", resolve); + buttonLastSave.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 1); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 2.5); - // Drag scene 2 - sceneTarget2.dispatchEvent(new MouseEvent('mousedown', {x: 0, y: 0 })); - sceneTarget2.dispatchEvent(new MouseEvent('mousemove', {x: 100, y: 100})); - sceneTarget2.dispatchEvent(new MouseEvent('mouseup', {x: 100, y: 100})); + return new Promise(function(resolve) { + gd.once("plotly_relayout", resolve); + buttonDefault.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 1.25, 1.25, 1.25); + assertCameraEye(gd._fullLayout.scene2, 1.25, 1.25, 1.25); - setTimeout(function() { + return new Promise(function(resolve) { + gd.once("plotly_relayout", resolve); + buttonLastSave.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 1); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 2.5); - expect(relayoutCallback).toHaveBeenCalledTimes(2); + delete gd._fullLayout.scene._scene.cameraInitial; + delete gd._fullLayout.scene2._scene.cameraInitial; - done(); + Plotly.relayout(gd, { + "scene.bgcolor": "#d3d3d3", + "scene.camera.eye.z": 4, + "scene2.camera.eye.z": 5 + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); - }, MODEBAR_DELAY); + return new Promise(function(resolve) { + gd.once("plotly_relayout", resolve); + buttonDefault.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 1.25, 1.25, 1.25); + assertCameraEye(gd._fullLayout.scene2, 1.25, 1.25, 1.25); - }, MODEBAR_DELAY); - }); - }); + return new Promise(function(resolve) { + gd.once("plotly_relayout", resolve); + buttonLastSave.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); + }) + .then(done); + } + ); }); - describe('Removal of gl contexts', function() { - - var mockData2d = [{ - type: 'scattergl', - x: [1, 2, 3], - y: [2, 1, 3] - }]; - - - var mockData3d = [{ - type: 'scatter3d', - x: [1, 2, 3], - y: [2, 1, 3], - z: [3, 2, 1] - }]; - - describe('Plots.cleanPlot', function() { - - it('should remove gl context from the graph div of a gl3d plot', function(done) { - gd = createGraphDiv(); + describe("drag and wheel interactions", function() { + it("should update the scene camera", function(done) { + var sceneLayout = gd._fullLayout.scene, + sceneLayout2 = gd._fullLayout.scene2, + sceneTarget = gd.querySelector( + ".svg-container .gl-container #scene canvas" + ), + sceneTarget2 = gd.querySelector( + ".svg-container .gl-container #scene2 canvas" + ); + + expect(sceneLayout.camera.eye).toEqual({ x: 0.1, y: 0.1, z: 1 }); + expect(sceneLayout2.camera.eye).toEqual({ x: 2.5, y: 2.5, z: 2.5 }); + + // Wheel scene 1 + sceneTarget.dispatchEvent(new WheelEvent("wheel", { deltaY: 1 })); + + // Wheel scene 2 + sceneTarget2.dispatchEvent(new WheelEvent("wheel", { deltaY: 1 })); + + setTimeout( + function() { + expect(relayoutCallback).toHaveBeenCalledTimes(2); + + relayoutCallback.calls.reset(); + + // Drag scene 1 + sceneTarget.dispatchEvent(new MouseEvent("mousedown", { + x: 0, + y: 0 + })); + sceneTarget.dispatchEvent(new MouseEvent("mousemove", { + x: 100, + y: 100 + })); + sceneTarget.dispatchEvent(new MouseEvent("mouseup", { + x: 100, + y: 100 + })); + + // Drag scene 2 + sceneTarget2.dispatchEvent(new MouseEvent("mousedown", { + x: 0, + y: 0 + })); + sceneTarget2.dispatchEvent(new MouseEvent("mousemove", { + x: 100, + y: 100 + })); + sceneTarget2.dispatchEvent(new MouseEvent("mouseup", { + x: 100, + y: 100 + })); + + setTimeout( + function() { + expect(relayoutCallback).toHaveBeenCalledTimes(2); - Plotly.plot(gd, mockData3d).then(function() { - expect(gd._fullLayout.scene._scene.glplot).toBeDefined(); + done(); + }, + MODEBAR_DELAY + ); + }, + MODEBAR_DELAY + ); + }); + }); + }); - Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); - expect(gd._fullLayout.scene._scene.glplot).toBe(null); + describe("Removal of gl contexts", function() { + var mockData2d = [{ type: "scattergl", x: [1, 2, 3], y: [2, 1, 3] }]; - done(); - }); - }); + var mockData3d = [ + { type: "scatter3d", x: [1, 2, 3], y: [2, 1, 3], z: [3, 2, 1] } + ]; - it('should remove gl context from the graph div of a gl2d plot', function(done) { - gd = createGraphDiv(); + describe("Plots.cleanPlot", function() { + it("should remove gl context from the graph div of a gl3d plot", function( + done + ) { + gd = createGraphDiv(); - Plotly.plot(gd, mockData2d).then(function() { - expect(gd._fullLayout._plots.xy._scene2d.glplot).toBeDefined(); + Plotly.plot(gd, mockData3d).then(function() { + expect(gd._fullLayout.scene._scene.glplot).toBeDefined(); - Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); - expect(gd._fullLayout._plots).toEqual({}); + Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); + expect(gd._fullLayout.scene._scene.glplot).toBe(null); - done(); - }); - }); + done(); }); + }); - describe('Plotly.newPlot', function() { - - var mockData2dNew = [{ - type: 'scattergl', - x: [1, 3, 2], - y: [2, 3, 1] - }]; - - - var mockData3dNew = [{ - type: 'scatter3d', - x: [2, 1, 3], - y: [1, 2, 3], - z: [2, 1, 3] - }]; - - - it('should remove gl context from the graph div of a gl3d plot', function(done) { - gd = createGraphDiv(); - - Plotly.plot(gd, mockData3d).then(function() { - - var firstGlplotObject = gd._fullLayout.scene._scene.glplot; - var firstGlContext = firstGlplotObject.gl; - var firstCanvas = firstGlContext.canvas; - - expect(firstGlplotObject).toBeDefined(); - - Plotly.newPlot(gd, mockData3dNew, {}).then(function() { - - var secondGlplotObject = gd._fullLayout.scene._scene.glplot; - var secondGlContext = secondGlplotObject.gl; - var secondCanvas = secondGlContext.canvas; - - expect(secondGlplotObject).not.toBe(firstGlplotObject); - expect(firstGlplotObject.gl === null); - expect(secondGlContext instanceof WebGLRenderingContext); - expect(secondGlContext).not.toBe(firstGlContext); - - // The same canvas can't possibly be reassinged a new WebGL context, but let's leave room - // for the implementation to make the context get lost and have the old canvas stick around - // in a disused state. - expect(firstCanvas.parentNode === null || - firstCanvas !== secondCanvas && firstGlContext.isContextLost()); - - done(); + it("should remove gl context from the graph div of a gl2d plot", function( + done + ) { + gd = createGraphDiv(); - }); - }); - }); + Plotly.plot(gd, mockData2d).then(function() { + expect(gd._fullLayout._plots.xy._scene2d.glplot).toBeDefined(); - it('should remove gl context from the graph div of a gl2d plot', function(done) { - gd = createGraphDiv(); + Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); + expect(gd._fullLayout._plots).toEqual({}); - Plotly.plot(gd, mockData2d).then(function() { + done(); + }); + }); + }); - var firstGlplotObject = gd._fullLayout._plots.xy._scene2d.glplot; - var firstGlContext = firstGlplotObject.gl; - var firstCanvas = firstGlContext.canvas; + describe("Plotly.newPlot", function() { + var mockData2dNew = [{ type: "scattergl", x: [1, 3, 2], y: [2, 3, 1] }]; - expect(firstGlplotObject).toBeDefined(); - expect(firstGlContext).toBeDefined(); - expect(firstGlContext instanceof WebGLRenderingContext); + var mockData3dNew = [ + { type: "scatter3d", x: [2, 1, 3], y: [1, 2, 3], z: [2, 1, 3] } + ]; - Plotly.newPlot(gd, mockData2dNew, {}).then(function() { + it("should remove gl context from the graph div of a gl3d plot", function( + done + ) { + gd = createGraphDiv(); - var secondGlplotObject = gd._fullLayout._plots.xy._scene2d.glplot; - var secondGlContext = secondGlplotObject.gl; - var secondCanvas = secondGlContext.canvas; + Plotly.plot(gd, mockData3d).then(function() { + var firstGlplotObject = gd._fullLayout.scene._scene.glplot; + var firstGlContext = firstGlplotObject.gl; + var firstCanvas = firstGlContext.canvas; + + expect(firstGlplotObject).toBeDefined(); + + Plotly.newPlot(gd, mockData3dNew, {}).then(function() { + var secondGlplotObject = gd._fullLayout.scene._scene.glplot; + var secondGlContext = secondGlplotObject.gl; + var secondCanvas = secondGlContext.canvas; + + expect(secondGlplotObject).not.toBe(firstGlplotObject); + expect(firstGlplotObject.gl === null); + expect(secondGlContext instanceof WebGLRenderingContext); + expect(secondGlContext).not.toBe(firstGlContext); + + // The same canvas can't possibly be reassinged a new WebGL context, but let's leave room + // for the implementation to make the context get lost and have the old canvas stick around + // in a disused state. + expect( + firstCanvas.parentNode === null || + firstCanvas !== secondCanvas && firstGlContext.isContextLost() + ); + + done(); + }); + }); + }); - expect(Object.keys(gd._fullLayout._plots).length === 1); - expect(secondGlplotObject).not.toBe(firstGlplotObject); - expect(firstGlplotObject.gl === null); - expect(secondGlContext instanceof WebGLRenderingContext); - expect(secondGlContext).not.toBe(firstGlContext); - expect(firstCanvas.parentNode === null || - firstCanvas !== secondCanvas && firstGlContext.isContextLost()); + it("should remove gl context from the graph div of a gl2d plot", function( + done + ) { + gd = createGraphDiv(); - done(); - }); - }); - }); + Plotly.plot(gd, mockData2d).then(function() { + var firstGlplotObject = gd._fullLayout._plots.xy._scene2d.glplot; + var firstGlContext = firstGlplotObject.gl; + var firstCanvas = firstGlContext.canvas; + + expect(firstGlplotObject).toBeDefined(); + expect(firstGlContext).toBeDefined(); + expect(firstGlContext instanceof WebGLRenderingContext); + + Plotly.newPlot(gd, mockData2dNew, {}).then(function() { + var secondGlplotObject = gd._fullLayout._plots.xy._scene2d.glplot; + var secondGlContext = secondGlplotObject.gl; + var secondCanvas = secondGlContext.canvas; + + expect(Object.keys(gd._fullLayout._plots).length === 1); + expect(secondGlplotObject).not.toBe(firstGlplotObject); + expect(firstGlplotObject.gl === null); + expect(secondGlContext instanceof WebGLRenderingContext); + expect(secondGlContext).not.toBe(firstGlContext); + expect( + firstCanvas.parentNode === null || + firstCanvas !== secondCanvas && firstGlContext.isContextLost() + ); + + done(); + }); }); + }); }); + }); }); -describe('Test gl plot side effects', function() { - var gd; +describe("Test gl plot side effects", function() { + var gd; - beforeEach(function() { - gd = createGraphDiv(); - }); + beforeEach(function() { + gd = createGraphDiv(); + }); - afterEach(destroyGraphDiv); - - describe('when present with rangeslider', function() { - it('should not draw the rangeslider', function(done) { - var data = [{ - x: [1, 2, 3], - y: [2, 3, 4], - type: 'scattergl' - }, { - x: [1, 2, 3], - y: [2, 3, 4], - type: 'scatter' - }]; - - var layout = { - xaxis: { rangeslider: { visible: true } } - }; - - Plotly.plot(gd, data, layout).then(function() { - var rangeSlider = document.getElementsByClassName('range-slider')[0]; - expect(rangeSlider).not.toBeDefined(); - done(); - }); - }); + afterEach(destroyGraphDiv); + + describe("when present with rangeslider", function() { + it("should not draw the rangeslider", function(done) { + var data = [ + { x: [1, 2, 3], y: [2, 3, 4], type: "scattergl" }, + { x: [1, 2, 3], y: [2, 3, 4], type: "scatter" } + ]; + + var layout = { xaxis: { rangeslider: { visible: true } } }; + + Plotly.plot(gd, data, layout).then(function() { + var rangeSlider = document.getElementsByClassName("range-slider")[0]; + expect(rangeSlider).not.toBeDefined(); + done(); + }); }); + }); - it('should be able to replot from a blank graph', function(done) { - function countCanvases(cnt) { - var nodes = d3.selectAll('canvas'); - expect(nodes.size()).toEqual(cnt); - } + it("should be able to replot from a blank graph", function(done) { + function countCanvases(cnt) { + var nodes = d3.selectAll("canvas"); + expect(nodes.size()).toEqual(cnt); + } - var data = [{ - type: 'scattergl', - x: [1, 2, 3], - y: [2, 1, 2] - }]; + var data = [{ type: "scattergl", x: [1, 2, 3], y: [2, 1, 2] }]; - Plotly.plot(gd, []).then(function() { - countCanvases(0); + Plotly.plot(gd, []) + .then(function() { + countCanvases(0); - return Plotly.plot(gd, data); - }).then(function() { - countCanvases(1); + return Plotly.plot(gd, data); + }) + .then(function() { + countCanvases(1); - return Plotly.purge(gd); - }).then(function() { - countCanvases(0); + return Plotly.purge(gd); + }) + .then(function() { + countCanvases(0); - return Plotly.plot(gd, data); - }).then(function() { - countCanvases(1); + return Plotly.plot(gd, data); + }) + .then(function() { + countCanvases(1); - return Plotly.deleteTraces(gd, [0]); - }).then(function() { - countCanvases(0); + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + countCanvases(0); - return Plotly.purge(gd); - }).then(done); - }); + return Plotly.purge(gd); + }) + .then(done); + }); }); -describe('gl2d interaction', function() { - var gd; +describe("gl2d interaction", function() { + var gd; - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - beforeEach(function() { - gd = createGraphDiv(); - }); + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); + beforeEach(function() { + gd = createGraphDiv(); + }); - it('data-referenced annotations should update on drag', function(done) { + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - function drag(start, end) { - mouseEvent('mousemove', start[0], start[1]); - mouseEvent('mousedown', start[0], start[1], { buttons: 1 }); - mouseEvent('mousemove', end[0], end[1], { buttons: 1 }); - mouseEvent('mouseup', end[0], end[1]); - } + it("data-referenced annotations should update on drag", function(done) { + function drag(start, end) { + mouseEvent("mousemove", start[0], start[1]); + mouseEvent("mousedown", start[0], start[1], { buttons: 1 }); + mouseEvent("mousemove", end[0], end[1], { buttons: 1 }); + mouseEvent("mouseup", end[0], end[1]); + } - function assertAnnotation(xy) { - var ann = d3.select('g.annotation-text-g').select('g'); - var translate = Drawing.getTranslate(ann); + function assertAnnotation(xy) { + var ann = d3.select("g.annotation-text-g").select("g"); + var translate = Drawing.getTranslate(ann); - expect(translate.x).toBeWithin(xy[0], 1.5); - expect(translate.y).toBeWithin(xy[1], 1.5); - } + expect(translate.x).toBeWithin(xy[0], 1.5); + expect(translate.y).toBeWithin(xy[1], 1.5); + } - Plotly.plot(gd, [{ - type: 'scattergl', - x: [1, 2, 3], - y: [2, 1, 2] - }], { - annotations: [{ - x: 2, - y: 1, - text: 'text' - }], - dragmode: 'pan' - }) - .then(function() { - assertAnnotation([327, 325]); + Plotly.plot(gd, [{ type: "scattergl", x: [1, 2, 3], y: [2, 1, 2] }], { + annotations: [{ x: 2, y: 1, text: "text" }], + dragmode: "pan" + }) + .then(function() { + assertAnnotation([327, 325]); - drag([250, 200], [200, 150]); - assertAnnotation([277, 275]); + drag([250, 200], [200, 150]); + assertAnnotation([277, 275]); - return Plotly.relayout(gd, { - 'xaxis.range': [1.5, 2.5], - 'yaxis.range': [1, 1.5] - }); - }) - .then(function() { - assertAnnotation([327, 331]); - }) - .then(done); - }); + return Plotly.relayout(gd, { + "xaxis.range": [1.5, 2.5], + "yaxis.range": [1, 1.5] + }); + }) + .then(function() { + assertAnnotation([327, 331]); + }) + .then(done); + }); }); diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index 9141ff5edf1..5475bb5d7d4 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -1,586 +1,728 @@ -var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); - -var convertColumnXYZ = require('@src/traces/heatmap/convert_column_xyz'); -var Heatmap = require('@src/traces/heatmap'); - -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var customMatchers = require('../assets/custom_matchers'); - - -describe('heatmap supplyDefaults', function() { - 'use strict'; - - var traceIn, - traceOut; - - var defaultColor = '#444', - layout = { - font: Plots.layoutAttributes.font - }; - - var supplyDefaults = Heatmap.supplyDefaults; - - beforeEach(function() { - traceOut = {}; - }); - - it('should set visible to false when z is empty', function() { - traceIn = { - z: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - - traceIn = { - z: [[]] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - - traceIn = { - z: [[], [], []] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - - traceIn = { - type: 'heatmap', - z: [[1, 2], []] - }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); - - traceIn = { - type: 'heatmap', - z: [[], [1, 2], [1, 2, 3]] - }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); - expect(traceOut.visible).toBe(true); - expect(traceOut.visible).toBe(true); - }); - - it('should set visible to false when z is non-numeric', function() { - traceIn = { - type: 'heatmap', - z: [['a', 'b'], ['c', 'd']] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); - - it('should set visible to false when z isn\'t column not a 2d array', function() { - traceIn = { - x: [1, 1, 1, 2, 2], - y: [1, 2, 3, 1, 2], - z: [1, ['this is considered a column'], 1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).not.toBe(false); - - traceIn = { - x: [1, 1, 1, 2, 2], - y: [1, 2, 3, 1, 2], - z: [[0], ['this is not considered a column'], 1, ['nor 2d']] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); - - it('should set paddings to 0 when not defined', function() { - traceIn = { - type: 'heatmap', - z: [[1, 2], [3, 4]] - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.xgap).toBe(0); - expect(traceOut.ygap).toBe(0); - }); - - it('should not step on defined paddings', function() { - traceIn = { - xgap: 10, - type: 'heatmap', - z: [[1, 2], [3, 4]] - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.xgap).toBe(10); - expect(traceOut.ygap).toBe(0); - }); - - it('should not coerce gap if zsmooth is set', function() { - traceIn = { - xgap: 10, - zsmooth: 'best', - type: 'heatmap', - z: [[1, 2], [3, 4]] - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.xgap).toBe(undefined); - expect(traceOut.ygap).toBe(undefined); - }); - - it('should inherit layout.calendar', function() { - traceIn = { - x: [1, 2], - y: [1, 2], - z: [[1, 2], [3, 4]] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('islamic'); - expect(traceOut.ycalendar).toBe('islamic'); - }); - - it('should take its own calendars', function() { - traceIn = { - x: [1, 2], - y: [1, 2], - z: [[1, 2], [3, 4]], - xcalendar: 'coptic', - ycalendar: 'ethiopian' - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('coptic'); - expect(traceOut.ycalendar).toBe('ethiopian'); - }); +var Plotly = require("@lib/index"); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); + +var convertColumnXYZ = require("@src/traces/heatmap/convert_column_xyz"); +var Heatmap = require("@src/traces/heatmap"); + +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var customMatchers = require("../assets/custom_matchers"); + +describe("heatmap supplyDefaults", function() { + "use strict"; + var traceIn, traceOut; + + var defaultColor = "#444", layout = { font: Plots.layoutAttributes.font }; + + var supplyDefaults = Heatmap.supplyDefaults; + + beforeEach(function() { + traceOut = {}; + }); + + it("should set visible to false when z is empty", function() { + traceIn = { z: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + + traceIn = { z: [[]] }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + + traceIn = { z: [[], [], []] }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + + traceIn = { type: "heatmap", z: [[1, 2], []] }; + traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); + + traceIn = { type: "heatmap", z: [[], [1, 2], [1, 2, 3]] }; + traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); + expect(traceOut.visible).toBe(true); + expect(traceOut.visible).toBe(true); + }); + + it("should set visible to false when z is non-numeric", function() { + traceIn = { type: "heatmap", z: [["a", "b"], ["c", "d"]] }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); + + it( + "should set visible to false when z isn't column not a 2d array", + function() { + traceIn = { + x: [1, 1, 1, 2, 2], + y: [1, 2, 3, 1, 2], + z: [1, ["this is considered a column"], 1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).not.toBe(false); + + traceIn = { + x: [1, 1, 1, 2, 2], + y: [1, 2, 3, 1, 2], + z: [[0], ["this is not considered a column"], 1, ["nor 2d"]] + }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + } + ); + + it("should set paddings to 0 when not defined", function() { + traceIn = { type: "heatmap", z: [[1, 2], [3, 4]] }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.xgap).toBe(0); + expect(traceOut.ygap).toBe(0); + }); + + it("should not step on defined paddings", function() { + traceIn = { xgap: 10, type: "heatmap", z: [[1, 2], [3, 4]] }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.xgap).toBe(10); + expect(traceOut.ygap).toBe(0); + }); + + it("should not coerce gap if zsmooth is set", function() { + traceIn = { + xgap: 10, + zsmooth: "best", + type: "heatmap", + z: [[1, 2], [3, 4]] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.xgap).toBe(undefined); + expect(traceOut.ygap).toBe(undefined); + }); + + it("should inherit layout.calendar", function() { + traceIn = { x: [1, 2], y: [1, 2], z: [[1, 2], [3, 4]] }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("islamic"); + expect(traceOut.ycalendar).toBe("islamic"); + }); + + it("should take its own calendars", function() { + traceIn = { + x: [1, 2], + y: [1, 2], + z: [[1, 2], [3, 4]], + xcalendar: "coptic", + ycalendar: "ethiopian" + }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("coptic"); + expect(traceOut.ycalendar).toBe("ethiopian"); + }); }); -describe('heatmap convertColumnXYZ', function() { - 'use strict'; - - var trace; - - function makeMockAxis() { - return { - d2c: function(v) { return v; } - }; +describe("heatmap convertColumnXYZ", function() { + "use strict"; + var trace; + + function makeMockAxis() { + return { + d2c: function(v) { + return v; + } + }; + } + + var xa = makeMockAxis(), ya = makeMockAxis(); + + it("should convert x/y/z columns to z(x,y)", function() { + trace = { + x: [1, 1, 1, 2, 2, 2], + y: [1, 2, 3, 1, 2, 3], + z: [1, 2, 3, 4, 5, 6] + }; + + convertColumnXYZ(trace, xa, ya); + expect(trace.x).toEqual([1, 2]); + expect(trace.y).toEqual([1, 2, 3]); + expect(trace.z).toEqual([[1, 4], [2, 5], [3, 6]]); + }); + + it( + "should convert x/y/z columns to z(x,y) with uneven dimensions", + function() { + trace = { + x: [1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3], + y: [1, 2, 1, 2, 3], + z: [1, 2, 4, 5, 6] + }; + + convertColumnXYZ(trace, xa, ya); + expect(trace.x).toEqual([1, 2]); + expect(trace.y).toEqual([1, 2, 3]); + expect(trace.z).toEqual([[1, 4], [2, 5], [, 6]]); } - - var xa = makeMockAxis(), - ya = makeMockAxis(); - - it('should convert x/y/z columns to z(x,y)', function() { - trace = { - x: [1, 1, 1, 2, 2, 2], - y: [1, 2, 3, 1, 2, 3], - z: [1, 2, 3, 4, 5, 6] - }; - - convertColumnXYZ(trace, xa, ya); - expect(trace.x).toEqual([1, 2]); - expect(trace.y).toEqual([1, 2, 3]); - expect(trace.z).toEqual([[1, 4], [2, 5], [3, 6]]); - }); - - it('should convert x/y/z columns to z(x,y) with uneven dimensions', function() { - trace = { - x: [1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3], - y: [1, 2, 1, 2, 3], - z: [1, 2, 4, 5, 6] - }; - - convertColumnXYZ(trace, xa, ya); - expect(trace.x).toEqual([1, 2]); - expect(trace.y).toEqual([1, 2, 3]); - expect(trace.z).toEqual([[1, 4], [2, 5], [, 6]]); - }); - - it('should convert x/y/z columns to z(x,y) with missing values', function() { - trace = { - x: [1, 1, 2, 2, 2], - y: [1, 2, 1, 2, 3], - z: [1, null, 4, 5, 6] - }; - - convertColumnXYZ(trace, xa, ya); - expect(trace.x).toEqual([1, 2]); - expect(trace.y).toEqual([1, 2, 3]); - expect(trace.z).toEqual([[1, 4], [null, 5], [, 6]]); - }); - - it('should convert x/y/z/text columns to z(x,y) and text(x,y)', function() { - trace = { - x: [1, 1, 1, 2, 2, 2], - y: [1, 2, 3, 1, 2, 3], - z: [1, 2, 3, 4, 5, 6], - text: ['a', 'b', 'c', 'd', 'e', 'f', 'g'] - }; - - convertColumnXYZ(trace, xa, ya); - expect(trace.text).toEqual([['a', 'd'], ['b', 'e'], ['c', 'f']]); - }); - - it('should convert x/y/z columns to z(x,y) with out-of-order data', function() { - /* eslint no-sparse-arrays: 0*/ - - trace = { - x: [ - 50076, -42372, -19260, 3852, 26964, -65484, -42372, -19260, - 3852, 26964, -88596, -65484, -42372, -19260, 3852, 26964, 50076, 73188, - -65484, -42372, -19260, 3852, 26964, 50076, -42372, -19260, 3852, 26964, - -88596, -65484, -42372, -19260, 3852, 26964, 50076, 73188, -88596, -65484, - -42372, -19260, 3852, 26964, 50076, 73188 - ], - y: [ - 51851.8, 77841.4, 77841.4, 77841.4, 77841.4, 51851.8, 51851.8, 51851.8, - 51851.8, 51851.8, -26117, -26117, -26117, -26117, -26117, -26117, -26117, -26117, - -52106.6, -52106.6, -52106.6, -52106.6, -52106.6, -52106.6, -78096.2, -78096.2, - -78096.2, -78096.2, -127.4, -127.4, -127.4, -127.4, -127.4, -127.4, -127.4, -127.4, - 25862.2, 25862.2, 25862.2, 25862.2, 25862.2, 25862.2, 25862.2, 25862.2 - ], - z: [ - 4.361856, 4.234497, 4.321701, 4.450315, 4.416136, 4.210373, - 4.32009, 4.246728, 4.293992, 4.316364, 3.908434, 4.433257, 4.364234, 4.308714, 4.275516, - 4.126979, 4.296483, 4.320471, 4.339848, 4.39907, 4.345006, 4.315032, 4.295618, 4.262052, - 4.154291, 4.404264, 4.33847, 4.270931, 4.032226, 4.381492, 4.328922, 4.24046, 4.349151, - 4.202861, 4.256402, 4.28972, 3.956225, 4.337909, 4.31226, 4.259435, 4.146854, 4.235799, - 4.238752, 4.299876 - ] - }; - - convertColumnXYZ(trace, xa, ya); - expect(trace.x).toEqual( - [-88596, -65484, -42372, -19260, 3852, 26964, 50076, 73188]); - expect(trace.y).toEqual( - [-78096.2, -52106.6, -26117, -127.4, 25862.2, 51851.8, 77841.4]); - expect(trace.z).toEqual([ - [,, 4.154291, 4.404264, 4.33847, 4.270931,,, ], - [, 4.339848, 4.39907, 4.345006, 4.315032, 4.295618, 4.262052,, ], - [3.908434, 4.433257, 4.364234, 4.308714, 4.275516, 4.126979, 4.296483, 4.320471], - [4.032226, 4.381492, 4.328922, 4.24046, 4.349151, 4.202861, 4.256402, 4.28972], - [3.956225, 4.337909, 4.31226, 4.259435, 4.146854, 4.235799, 4.238752, 4.299876], - [, 4.210373, 4.32009, 4.246728, 4.293992, 4.316364, 4.361856,, ], - [,, 4.234497, 4.321701, 4.450315, 4.416136,,, ] - ]); - }); + ); + + it("should convert x/y/z columns to z(x,y) with missing values", function() { + trace = { + x: [1, 1, 2, 2, 2], + y: [1, 2, 1, 2, 3], + z: [1, null, 4, 5, 6] + }; + + convertColumnXYZ(trace, xa, ya); + expect(trace.x).toEqual([1, 2]); + expect(trace.y).toEqual([1, 2, 3]); + expect(trace.z).toEqual([[1, 4], [null, 5], [, 6]]); + }); + + it("should convert x/y/z/text columns to z(x,y) and text(x,y)", function() { + trace = { + x: [1, 1, 1, 2, 2, 2], + y: [1, 2, 3, 1, 2, 3], + z: [1, 2, 3, 4, 5, 6], + text: ["a", "b", "c", "d", "e", "f", "g"] + }; + + convertColumnXYZ(trace, xa, ya); + expect(trace.text).toEqual([["a", "d"], ["b", "e"], ["c", "f"]]); + }); + + it( + "should convert x/y/z columns to z(x,y) with out-of-order data", + function() { + /* eslint no-sparse-arrays: 0 */ + trace = { + x: [ + 50076, + -42372, + -19260, + 3852, + 26964, + -65484, + -42372, + -19260, + 3852, + 26964, + -88596, + -65484, + -42372, + -19260, + 3852, + 26964, + 50076, + 73188, + -65484, + -42372, + -19260, + 3852, + 26964, + 50076, + -42372, + -19260, + 3852, + 26964, + -88596, + -65484, + -42372, + -19260, + 3852, + 26964, + 50076, + 73188, + -88596, + -65484, + -42372, + -19260, + 3852, + 26964, + 50076, + 73188 + ], + y: [ + 51851.8, + 77841.4, + 77841.4, + 77841.4, + 77841.4, + 51851.8, + 51851.8, + 51851.8, + 51851.8, + 51851.8, + -26117, + -26117, + -26117, + -26117, + -26117, + -26117, + -26117, + -26117, + -52106.6, + -52106.6, + -52106.6, + -52106.6, + -52106.6, + -52106.6, + -78096.2, + -78096.2, + -78096.2, + -78096.2, + -127.4, + -127.4, + -127.4, + -127.4, + -127.4, + -127.4, + -127.4, + -127.4, + 25862.2, + 25862.2, + 25862.2, + 25862.2, + 25862.2, + 25862.2, + 25862.2, + 25862.2 + ], + z: [ + 4.361856, + 4.234497, + 4.321701, + 4.450315, + 4.416136, + 4.210373, + 4.32009, + 4.246728, + 4.293992, + 4.316364, + 3.908434, + 4.433257, + 4.364234, + 4.308714, + 4.275516, + 4.126979, + 4.296483, + 4.320471, + 4.339848, + 4.39907, + 4.345006, + 4.315032, + 4.295618, + 4.262052, + 4.154291, + 4.404264, + 4.33847, + 4.270931, + 4.032226, + 4.381492, + 4.328922, + 4.24046, + 4.349151, + 4.202861, + 4.256402, + 4.28972, + 3.956225, + 4.337909, + 4.31226, + 4.259435, + 4.146854, + 4.235799, + 4.238752, + 4.299876 + ] + }; + + convertColumnXYZ(trace, xa, ya); + expect(trace.x).toEqual([ + -88596, + -65484, + -42372, + -19260, + 3852, + 26964, + 50076, + 73188 + ]); + expect(trace.y).toEqual([ + -78096.2, + -52106.6, + -26117, + -127.4, + 25862.2, + 51851.8, + 77841.4 + ]); + expect(trace.z).toEqual([ + [, , 4.154291, 4.404264, 4.33847, 4.270931, , ,], + [, 4.339848, 4.39907, 4.345006, 4.315032, 4.295618, 4.262052, ,], + [ + 3.908434, + 4.433257, + 4.364234, + 4.308714, + 4.275516, + 4.126979, + 4.296483, + 4.320471 + ], + [ + 4.032226, + 4.381492, + 4.328922, + 4.24046, + 4.349151, + 4.202861, + 4.256402, + 4.28972 + ], + [ + 3.956225, + 4.337909, + 4.31226, + 4.259435, + 4.146854, + 4.235799, + 4.238752, + 4.299876 + ], + [, 4.210373, 4.32009, 4.246728, 4.293992, 4.316364, 4.361856, ,], + [, , 4.234497, 4.321701, 4.450315, 4.416136, , ,] + ]); + } + ); }); -describe('heatmap calc', function() { - 'use strict'; - - beforeAll(function() { - jasmine.addMatchers(customMatchers); +describe("heatmap calc", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + function _calc(opts) { + var base = { type: "heatmap" }, + trace = Lib.extendFlat({}, base, opts), + gd = { data: [trace] }; + + Plots.supplyDefaults(gd); + var fullTrace = gd._fullData[0]; + + return Heatmap.calc(gd, fullTrace)[0]; + } + + it("should fill in bricks if x/y not given", function() { + var out = _calc({ z: [[1, 2, 3], [3, 1, 2]] }); + + expect(out.x).toBeCloseToArray([-0.5, 0.5, 1.5, 2.5]); + expect(out.y).toBeCloseToArray([-0.5, 0.5, 1.5]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); + + it("should fill in bricks with x0/dx + y0/dy", function() { + var out = _calc({ + z: [[1, 2, 3], [3, 1, 2]], + x0: 10, + dx: 0.5, + y0: -2, + dy: -2 }); - function _calc(opts) { - var base = { type: 'heatmap' }, - trace = Lib.extendFlat({}, base, opts), - gd = { data: [trace] }; - - Plots.supplyDefaults(gd); - var fullTrace = gd._fullData[0]; - - return Heatmap.calc(gd, fullTrace)[0]; - } + expect(out.x).toBeCloseToArray([9.75, 10.25, 10.75, 11.25]); + expect(out.y).toBeCloseToArray([-1, -3, -5]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); - it('should fill in bricks if x/y not given', function() { - var out = _calc({ - z: [[1, 2, 3], [3, 1, 2]] - }); - - expect(out.x).toBeCloseToArray([-0.5, 0.5, 1.5, 2.5]); - expect(out.y).toBeCloseToArray([-0.5, 0.5, 1.5]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); - }); - - it('should fill in bricks with x0/dx + y0/dy', function() { - var out = _calc({ - z: [[1, 2, 3], [3, 1, 2]], - x0: 10, - dx: 0.5, - y0: -2, - dy: -2 - }); - - expect(out.x).toBeCloseToArray([9.75, 10.25, 10.75, 11.25]); - expect(out.y).toBeCloseToArray([-1, -3, -5]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + it("should convert x/y coordinates into bricks", function() { + var out = _calc({ + x: [1, 2, 3], + y: [2, 6], + z: [[1, 2, 3], [3, 1, 2]] }); - it('should convert x/y coordinates into bricks', function() { - var out = _calc({ - x: [1, 2, 3], - y: [2, 6], - z: [[1, 2, 3], [3, 1, 2]] - }); + expect(out.x).toBeCloseToArray([0.5, 1.5, 2.5, 3.5]); + expect(out.y).toBeCloseToArray([0, 4, 8]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); - expect(out.x).toBeCloseToArray([0.5, 1.5, 2.5, 3.5]); - expect(out.y).toBeCloseToArray([0, 4, 8]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + it("should respect brick-link /y coordinates", function() { + var out = _calc({ + x: [1, 2, 3, 4], + y: [2, 6, 10], + z: [[1, 2, 3], [3, 1, 2]] }); - it('should respect brick-link /y coordinates', function() { - var out = _calc({ - x: [1, 2, 3, 4], - y: [2, 6, 10], - z: [[1, 2, 3], [3, 1, 2]] - }); + expect(out.x).toBeCloseToArray([1, 2, 3, 4]); + expect(out.y).toBeCloseToArray([2, 6, 10]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); - expect(out.x).toBeCloseToArray([1, 2, 3, 4]); - expect(out.y).toBeCloseToArray([2, 6, 10]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); - }); + it("should handle 1-xy + 1-brick case", function() { + var out = _calc({ x: [2], y: [3], z: [[1]] }); - it('should handle 1-xy + 1-brick case', function() { - var out = _calc({ - x: [2], - y: [3], - z: [[1]] - }); + expect(out.x).toBeCloseToArray([1.5, 2.5]); + expect(out.y).toBeCloseToArray([2.5, 3.5]); + expect(out.z).toBeCloseTo2DArray([[1]]); + }); - expect(out.x).toBeCloseToArray([1.5, 2.5]); - expect(out.y).toBeCloseToArray([2.5, 3.5]); - expect(out.z).toBeCloseTo2DArray([[1]]); - }); + it("should handle 1-xy + multi-brick case", function() { + var out = _calc({ x: [2], y: [3], z: [[1, 2, 3], [3, 1, 2]] }); - it('should handle 1-xy + multi-brick case', function() { - var out = _calc({ - x: [2], - y: [3], - z: [[1, 2, 3], [3, 1, 2]] - }); + expect(out.x).toBeCloseToArray([1.5, 2.5, 3.5, 4.5]); + expect(out.y).toBeCloseToArray([2.5, 3.5, 4.5]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); - expect(out.x).toBeCloseToArray([1.5, 2.5, 3.5, 4.5]); - expect(out.y).toBeCloseToArray([2.5, 3.5, 4.5]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); - }); + it("should handle 0-xy + multi-brick case", function() { + var out = _calc({ x: [], y: [], z: [[1, 2, 3], [3, 1, 2]] }); - it('should handle 0-xy + multi-brick case', function() { - var out = _calc({ - x: [], - y: [], - z: [[1, 2, 3], [3, 1, 2]] - }); + expect(out.x).toBeCloseToArray([-0.5, 0.5, 1.5, 2.5]); + expect(out.y).toBeCloseToArray([-0.5, 0.5, 1.5]); + expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + }); - expect(out.x).toBeCloseToArray([-0.5, 0.5, 1.5, 2.5]); - expect(out.y).toBeCloseToArray([-0.5, 0.5, 1.5]); - expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); + it("should handle the category case", function() { + var out = _calc({ + x: ["a", "b", "c"], + y: ["z"], + z: [[17, 18, 19]] }); - it('should handle the category case', function() { - var out = _calc({ - x: ['a', 'b', 'c'], - y: ['z'], - z: [[17, 18, 19]] - }); - - expect(out.x).toBeCloseToArray([-0.5, 0.5, 1.5, 2.5]); - expect(out.y).toBeCloseToArray([-0.5, 0.5]); - expect(out.z).toBeCloseTo2DArray([[17, 18, 19]]); - }); + expect(out.x).toBeCloseToArray([-0.5, 0.5, 1.5, 2.5]); + expect(out.y).toBeCloseToArray([-0.5, 0.5]); + expect(out.z).toBeCloseTo2DArray([[17, 18, 19]]); + }); }); -describe('heatmap plot', function() { - 'use strict'; +describe("heatmap plot", function() { + "use strict"; + afterEach(destroyGraphDiv); - afterEach(destroyGraphDiv); + it("should not draw traces that are off-screen", function(done) { + var mock = require("@mocks/heatmap_multi-trace.json"), + mockCopy = Lib.extendDeep({}, mock), + gd = createGraphDiv(); - it('should not draw traces that are off-screen', function(done) { - var mock = require('@mocks/heatmap_multi-trace.json'), - mockCopy = Lib.extendDeep({}, mock), - gd = createGraphDiv(); + function assertImageCnt(cnt) { + var images = d3.selectAll(".hm").select("image"); - function assertImageCnt(cnt) { - var images = d3.selectAll('.hm').select('image'); + expect(images.size()).toEqual(cnt); + } - expect(images.size()).toEqual(cnt); - } + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + assertImageCnt(5); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - assertImageCnt(5); + return Plotly.relayout(gd, "xaxis.range", [2, 3]); + }) + .then(function() { + assertImageCnt(2); - return Plotly.relayout(gd, 'xaxis.range', [2, 3]); - }).then(function() { - assertImageCnt(2); + return Plotly.relayout(gd, "xaxis.autorange", true); + }) + .then(function() { + assertImageCnt(5); - return Plotly.relayout(gd, 'xaxis.autorange', true); - }).then(function() { - assertImageCnt(5); + done(); + }); + }); - done(); - }); - }); + it("should be able to restyle", function(done) { + var mock = require("@mocks/13.json"), + mockCopy = Lib.extendDeep({}, mock), + gd = createGraphDiv(); - it('should be able to restyle', function(done) { - var mock = require('@mocks/13.json'), - mockCopy = Lib.extendDeep({}, mock), - gd = createGraphDiv(); + function getImageURL() { + return d3.select(".hm > image").attr("href"); + } - function getImageURL() { - return d3.select('.hm > image').attr('href'); - } + var imageURLs = []; - var imageURLs = []; + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + imageURLs.push(getImageURL()); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - imageURLs.push(getImageURL()); + return Plotly.restyle(gd, "colorscale", "Greens"); + }) + .then(function() { + imageURLs.push(getImageURL()); - return Plotly.restyle(gd, 'colorscale', 'Greens'); - }).then(function() { - imageURLs.push(getImageURL()); + expect(imageURLs[0]).not.toEqual(imageURLs[1]); - expect(imageURLs[0]).not.toEqual(imageURLs[1]); + return Plotly.restyle(gd, "colorscale", "Reds"); + }) + .then(function() { + imageURLs.push(getImageURL()); - return Plotly.restyle(gd, 'colorscale', 'Reds'); - }).then(function() { - imageURLs.push(getImageURL()); + expect(imageURLs[1]).not.toEqual(imageURLs[2]); - expect(imageURLs[1]).not.toEqual(imageURLs[2]); + return Plotly.restyle(gd, "colorscale", "Greens"); + }) + .then(function() { + imageURLs.push(getImageURL()); - return Plotly.restyle(gd, 'colorscale', 'Greens'); - }).then(function() { - imageURLs.push(getImageURL()); + expect(imageURLs[1]).toEqual(imageURLs[3]); - expect(imageURLs[1]).toEqual(imageURLs[3]); + done(); + }); + }); - done(); - }); - }); + it("draws canvas with correct margins", function(done) { + var mockWithPadding = require("@mocks/heatmap_brick_padding.json"), + mockWithoutPadding = Lib.extendDeep({}, mockWithPadding), + gd = createGraphDiv(), + getContextStub = { fillRect: jasmine.createSpy() }, + originalCreateElement = document.createElement; - it('draws canvas with correct margins', function(done) { - var mockWithPadding = require('@mocks/heatmap_brick_padding.json'), - mockWithoutPadding = Lib.extendDeep({}, mockWithPadding), - gd = createGraphDiv(), - getContextStub = { - fillRect: jasmine.createSpy() - }, - originalCreateElement = document.createElement; - - mockWithoutPadding.data[0].xgap = 0; - mockWithoutPadding.data[0].ygap = 0; - - spyOn(document, 'createElement').and.callFake(function(elementType) { - var element = originalCreateElement.call(document, elementType); - if(elementType === 'canvas') { - spyOn(element, 'getContext').and.returnValue(getContextStub); - } - return element; - }); - - var argumentsWithoutPadding = [], - argumentsWithPadding = []; - Plotly.plot(gd, mockWithoutPadding.data, mockWithoutPadding.layout).then(function() { - argumentsWithoutPadding = getContextStub.fillRect.calls.allArgs().slice(0); - return Plotly.plot(gd, mockWithPadding.data, mockWithPadding.layout); - }).then(function() { - var centerXGap = mockWithPadding.data[0].xgap / 3; - var centerYGap = mockWithPadding.data[0].ygap / 3; - var edgeXGap = mockWithPadding.data[0].xgap * 2 / 3; - var edgeYGap = mockWithPadding.data[0].ygap * 2 / 3; - - argumentsWithPadding = getContextStub.fillRect.calls.allArgs().slice(getContextStub.fillRect.calls.allArgs().length - 9); - expect(argumentsWithPadding).toEqual([ - [argumentsWithoutPadding[0][0], - argumentsWithoutPadding[0][1] + edgeYGap, - argumentsWithoutPadding[0][2] - edgeXGap, - argumentsWithoutPadding[0][3] - edgeYGap], - [argumentsWithoutPadding[1][0] + centerXGap, - argumentsWithoutPadding[1][1] + edgeYGap, - argumentsWithoutPadding[1][2] - edgeXGap, - argumentsWithoutPadding[1][3] - edgeYGap], - [argumentsWithoutPadding[2][0] + edgeXGap, - argumentsWithoutPadding[2][1] + edgeYGap, - argumentsWithoutPadding[2][2] - edgeXGap, - argumentsWithoutPadding[2][3] - edgeYGap], - [argumentsWithoutPadding[3][0], - argumentsWithoutPadding[3][1] + centerYGap, - argumentsWithoutPadding[3][2] - edgeXGap, - argumentsWithoutPadding[3][3] - edgeYGap], - [argumentsWithoutPadding[4][0] + centerXGap, - argumentsWithoutPadding[4][1] + centerYGap, - argumentsWithoutPadding[4][2] - edgeXGap, - argumentsWithoutPadding[4][3] - edgeYGap], - [argumentsWithoutPadding[5][0] + edgeXGap, - argumentsWithoutPadding[5][1] + centerYGap, - argumentsWithoutPadding[5][2] - edgeXGap, - argumentsWithoutPadding[5][3] - edgeYGap], - [argumentsWithoutPadding[6][0], - argumentsWithoutPadding[6][1], - argumentsWithoutPadding[6][2] - edgeXGap, - argumentsWithoutPadding[6][3] - edgeYGap], - [argumentsWithoutPadding[7][0] + centerXGap, - argumentsWithoutPadding[7][1], - argumentsWithoutPadding[7][2] - edgeXGap, - argumentsWithoutPadding[7][3] - edgeYGap], - [argumentsWithoutPadding[8][0] + edgeXGap, - argumentsWithoutPadding[8][1], - argumentsWithoutPadding[8][2] - edgeXGap, - argumentsWithoutPadding[8][3] - edgeYGap - ]]); - done(); - }); - }); -}); + mockWithoutPadding.data[0].xgap = 0; + mockWithoutPadding.data[0].ygap = 0; -describe('heatmap hover', function() { - 'use strict'; + spyOn(document, "createElement").and.callFake(function(elementType) { + var element = originalCreateElement.call(document, elementType); + if (elementType === "canvas") { + spyOn(element, "getContext").and.returnValue(getContextStub); + } + return element; + }); - var gd; + var argumentsWithoutPadding = [], argumentsWithPadding = []; + Plotly.plot(gd, mockWithoutPadding.data, mockWithoutPadding.layout) + .then(function() { + argumentsWithoutPadding = getContextStub.fillRect.calls + .allArgs() + .slice(0); + return Plotly.plot(gd, mockWithPadding.data, mockWithPadding.layout); + }) + .then(function() { + var centerXGap = mockWithPadding.data[0].xgap / 3; + var centerYGap = mockWithPadding.data[0].ygap / 3; + var edgeXGap = mockWithPadding.data[0].xgap * 2 / 3; + var edgeYGap = mockWithPadding.data[0].ygap * 2 / 3; + + argumentsWithPadding = getContextStub.fillRect.calls + .allArgs() + .slice(getContextStub.fillRect.calls.allArgs().length - 9); + expect(argumentsWithPadding).toEqual([ + [ + argumentsWithoutPadding[0][0], + argumentsWithoutPadding[0][1] + edgeYGap, + argumentsWithoutPadding[0][2] - edgeXGap, + argumentsWithoutPadding[0][3] - edgeYGap + ], + [ + argumentsWithoutPadding[1][0] + centerXGap, + argumentsWithoutPadding[1][1] + edgeYGap, + argumentsWithoutPadding[1][2] - edgeXGap, + argumentsWithoutPadding[1][3] - edgeYGap + ], + [ + argumentsWithoutPadding[2][0] + edgeXGap, + argumentsWithoutPadding[2][1] + edgeYGap, + argumentsWithoutPadding[2][2] - edgeXGap, + argumentsWithoutPadding[2][3] - edgeYGap + ], + [ + argumentsWithoutPadding[3][0], + argumentsWithoutPadding[3][1] + centerYGap, + argumentsWithoutPadding[3][2] - edgeXGap, + argumentsWithoutPadding[3][3] - edgeYGap + ], + [ + argumentsWithoutPadding[4][0] + centerXGap, + argumentsWithoutPadding[4][1] + centerYGap, + argumentsWithoutPadding[4][2] - edgeXGap, + argumentsWithoutPadding[4][3] - edgeYGap + ], + [ + argumentsWithoutPadding[5][0] + edgeXGap, + argumentsWithoutPadding[5][1] + centerYGap, + argumentsWithoutPadding[5][2] - edgeXGap, + argumentsWithoutPadding[5][3] - edgeYGap + ], + [ + argumentsWithoutPadding[6][0], + argumentsWithoutPadding[6][1], + argumentsWithoutPadding[6][2] - edgeXGap, + argumentsWithoutPadding[6][3] - edgeYGap + ], + [ + argumentsWithoutPadding[7][0] + centerXGap, + argumentsWithoutPadding[7][1], + argumentsWithoutPadding[7][2] - edgeXGap, + argumentsWithoutPadding[7][3] - edgeYGap + ], + [ + argumentsWithoutPadding[8][0] + edgeXGap, + argumentsWithoutPadding[8][1], + argumentsWithoutPadding[8][2] - edgeXGap, + argumentsWithoutPadding[8][3] - edgeYGap + ] + ]); + done(); + }); + }); +}); - beforeAll(function(done) { - jasmine.addMatchers(customMatchers); +describe("heatmap hover", function() { + "use strict"; + var gd; - gd = createGraphDiv(); + beforeAll(function(done) { + jasmine.addMatchers(customMatchers); - var mock = require('@mocks/heatmap_multi-trace.json'), - mockCopy = Lib.extendDeep({}, mock); + gd = createGraphDiv(); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); + var mock = require("@mocks/heatmap_multi-trace.json"), + mockCopy = Lib.extendDeep({}, mock); - afterAll(destroyGraphDiv); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - function _hover(gd, xval, yval) { - var fullLayout = gd._fullLayout, - calcData = gd.calcdata, - hoverData = []; + afterAll(destroyGraphDiv); - for(var i = 0; i < calcData.length; i++) { - var pointData = { - index: false, - distance: 20, - cd: calcData[i], - trace: calcData[i][0].trace, - xa: fullLayout.xaxis, - ya: fullLayout.yaxis - }; + function _hover(gd, xval, yval) { + var fullLayout = gd._fullLayout, calcData = gd.calcdata, hoverData = []; - var hoverPoint = Heatmap.hoverPoints(pointData, xval, yval); - if(hoverPoint) hoverData.push(hoverPoint[0]); - } + for (var i = 0; i < calcData.length; i++) { + var pointData = { + index: false, + distance: 20, + cd: calcData[i], + trace: calcData[i][0].trace, + xa: fullLayout.xaxis, + ya: fullLayout.yaxis + }; - return hoverData; + var hoverPoint = Heatmap.hoverPoints(pointData, xval, yval); + if (hoverPoint) hoverData.push(hoverPoint[0]); } - function assertLabels(hoverPoint, xLabel, yLabel, zLabel) { - expect(hoverPoint.xLabelVal).toEqual(xLabel, 'have correct x label'); - expect(hoverPoint.yLabelVal).toEqual(yLabel, 'have correct y label'); - expect(hoverPoint.zLabelVal).toEqual(zLabel, 'have correct z label'); - } + return hoverData; + } - it('should find closest point (case 1) and should', function() { - var pt = _hover(gd, 0.5, 0.5)[0]; + function assertLabels(hoverPoint, xLabel, yLabel, zLabel) { + expect(hoverPoint.xLabelVal).toEqual(xLabel, "have correct x label"); + expect(hoverPoint.yLabelVal).toEqual(yLabel, "have correct y label"); + expect(hoverPoint.zLabelVal).toEqual(zLabel, "have correct z label"); + } - expect(pt.index).toEqual([1, 0], 'have correct index'); - assertLabels(pt, 1, 1, 4); - }); + it("should find closest point (case 1) and should", function() { + var pt = _hover(gd, 0.5, 0.5)[0]; - it('should find closest point (case 2) and should', function() { - var pt = _hover(gd, 1.5, 0.5)[0]; + expect(pt.index).toEqual([1, 0], "have correct index"); + assertLabels(pt, 1, 1, 4); + }); - expect(pt.index).toEqual([0, 0], 'have correct index'); - assertLabels(pt, 2, 0.2, 6); - }); + it("should find closest point (case 2) and should", function() { + var pt = _hover(gd, 1.5, 0.5)[0]; + expect(pt.index).toEqual([0, 0], "have correct index"); + assertLabels(pt, 2, 0.2, 6); + }); }); diff --git a/test/jasmine/tests/histogram2d_test.js b/test/jasmine/tests/histogram2d_test.js index e6e421fad84..d99128128b5 100644 --- a/test/jasmine/tests/histogram2d_test.js +++ b/test/jasmine/tests/histogram2d_test.js @@ -1,135 +1,121 @@ -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); - -var supplyDefaults = require('@src/traces/histogram2d/defaults'); -var calc = require('@src/traces/histogram2d/calc'); - - -describe('Test histogram2d', function() { - 'use strict'; - - describe('supplyDefaults', function() { - var traceIn, - traceOut; - - beforeEach(function() { - traceOut = {}; - }); - - it('should set zsmooth to false when zsmooth is empty', function() { - traceIn = {}; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.zsmooth).toBe(false); - }); - - it('doesnt step on zsmooth when zsmooth is set', function() { - traceIn = { - zsmooth: 'fast' - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.zsmooth).toBe('fast'); - }); - - it('should set xgap and ygap to 0 when xgap and ygap are empty', function() { - traceIn = {}; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.xgap).toBe(0); - expect(traceOut.ygap).toBe(0); - }); - - it('shouldnt step on xgap and ygap when xgap and ygap are set', function() { - traceIn = { - xgap: 10, - ygap: 5 - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.xgap).toBe(10); - expect(traceOut.ygap).toBe(5); - }); - - it('shouldnt coerce gap when zsmooth is set', function() { - traceIn = { - xgap: 10, - ygap: 5, - zsmooth: 'best' - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.xgap).toBe(undefined); - expect(traceOut.ygap).toBe(undefined); - }); - - - it('should inherit layout.calendar', function() { - traceIn = { - x: [1, 2, 3], - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, '', {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('islamic'); - expect(traceOut.ycalendar).toBe('islamic'); - }); - - it('should take its own calendars', function() { - traceIn = { - x: [1, 2, 3], - y: [1, 2, 3], - xcalendar: 'coptic', - ycalendar: 'ethiopian' - }; - supplyDefaults(traceIn, traceOut, '', {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('coptic'); - expect(traceOut.ycalendar).toBe('ethiopian'); - }); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); + +var supplyDefaults = require("@src/traces/histogram2d/defaults"); +var calc = require("@src/traces/histogram2d/calc"); + +describe("Test histogram2d", function() { + "use strict"; + describe("supplyDefaults", function() { + var traceIn, traceOut; + + beforeEach(function() { + traceOut = {}; + }); + + it("should set zsmooth to false when zsmooth is empty", function() { + traceIn = {}; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.zsmooth).toBe(false); + }); + + it("doesnt step on zsmooth when zsmooth is set", function() { + traceIn = { zsmooth: "fast" }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.zsmooth).toBe("fast"); }); + it( + "should set xgap and ygap to 0 when xgap and ygap are empty", + function() { + traceIn = {}; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.xgap).toBe(0); + expect(traceOut.ygap).toBe(0); + } + ); + + it("shouldnt step on xgap and ygap when xgap and ygap are set", function() { + traceIn = { xgap: 10, ygap: 5 }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.xgap).toBe(10); + expect(traceOut.ygap).toBe(5); + }); + + it("shouldnt coerce gap when zsmooth is set", function() { + traceIn = { xgap: 10, ygap: 5, zsmooth: "best" }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.xgap).toBe(undefined); + expect(traceOut.ygap).toBe(undefined); + }); + + it("should inherit layout.calendar", function() { + traceIn = { x: [1, 2, 3], y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, "", { calendar: "islamic" }); - describe('calc', function() { - function _calc(opts) { - var base = { type: 'histogram2d' }, - trace = Lib.extendFlat({}, base, opts), - gd = { data: [trace] }; - - Plots.supplyDefaults(gd); - var fullTrace = gd._fullData[0]; - - var out = calc(gd, fullTrace); - delete out.trace; - return out; - } - - // remove tzJan/tzJuly when we move to UTC - var oneDay = 24 * 3600000; - - it('should handle both uniform and nonuniform date bins', function() { - var out = _calc({ - x: ['1970-01-01', '1970-01-01', '1970-01-02', '1970-01-04'], - nbinsx: 4, - y: ['1970-01-01', '1970-01-01', '1971-01-01', '1973-01-01'], - nbinsy: 4 - }); - - expect(out.x0).toBe('1970-01-01'); - expect(out.dx).toBe(oneDay); - - // TODO: even though the binning is done on non-uniform bins, - // the display makes them linear (using only y0 and dy) - // Can we also make it display the bins with nonuniform size? - // see https://github.com/plotly/plotly.js/issues/360 - expect(out.y0).toBe('1970-01-01 03:00'); - expect(out.dy).toBe(365.25 * oneDay); - - expect(out.z).toEqual([ - [2, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 1] - ]); - }); + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("islamic"); + expect(traceOut.ycalendar).toBe("islamic"); + }); + + it("should take its own calendars", function() { + traceIn = { + x: [1, 2, 3], + y: [1, 2, 3], + xcalendar: "coptic", + ycalendar: "ethiopian" + }; + supplyDefaults(traceIn, traceOut, "", { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("coptic"); + expect(traceOut.ycalendar).toBe("ethiopian"); + }); + }); + + describe("calc", function() { + function _calc(opts) { + var base = { type: "histogram2d" }, + trace = Lib.extendFlat({}, base, opts), + gd = { data: [trace] }; + + Plots.supplyDefaults(gd); + var fullTrace = gd._fullData[0]; + + var out = calc(gd, fullTrace); + delete out.trace; + return out; + } + + // remove tzJan/tzJuly when we move to UTC + var oneDay = 24 * 3600000; + + it("should handle both uniform and nonuniform date bins", function() { + var out = _calc({ + x: ["1970-01-01", "1970-01-01", "1970-01-02", "1970-01-04"], + nbinsx: 4, + y: ["1970-01-01", "1970-01-01", "1971-01-01", "1973-01-01"], + nbinsy: 4 + }); + + expect(out.x0).toBe("1970-01-01"); + expect(out.dx).toBe(oneDay); + + // TODO: even though the binning is done on non-uniform bins, + // the display makes them linear (using only y0 and dy) + // Can we also make it display the bins with nonuniform size? + // see https://github.com/plotly/plotly.js/issues/360 + expect(out.y0).toBe("1970-01-01 03:00"); + expect(out.dy).toBe(365.25 * oneDay); + + expect(out.z).toEqual([ + [2, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 1] + ]); }); + }); }); diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index a9fed476675..0a9ac56bef3 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -1,355 +1,331 @@ -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); - -var supplyDefaults = require('@src/traces/histogram/defaults'); -var calc = require('@src/traces/histogram/calc'); - - -describe('Test histogram', function() { - 'use strict'; - - describe('supplyDefaults', function() { - var traceIn, - traceOut; - - beforeEach(function() { - traceOut = {}; - }); - - it('should set visible to false when x or y is empty', function() { - traceIn = { - x: [] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - y: [] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.visible).toBe(false); - }); - - it('should set visible to false when type is histogram2d and x or y are empty', function() { - traceIn = { - type: 'histogram2d', - x: [], - y: [1, 2, 2] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - type: 'histogram2d', - x: [1, 2, 2], - y: [] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - type: 'histogram2d', - x: [], - y: [] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.visible).toBe(false); - - traceIn = { - type: 'histogram2dcontour', - x: [], - y: [1, 2, 2] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.visible).toBe(false); - }); - - it('should set orientation to v by default', function() { - traceIn = { - x: [1, 2, 2] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.orientation).toBe('v'); - - traceIn = { - x: [1, 2, 2], - y: [1, 2, 2] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.orientation).toBe('v'); - }); - - it('should set orientation to h when only y is supplied', function() { - traceIn = { - y: [1, 2, 2] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.orientation).toBe('h'); - - }); - - // coercing bin attributes got moved to calc because it needs - // axis type - so here we just test that it's NOT happening - - it('should not coerce autobinx regardless of xbins', function() { - traceIn = { - x: [1, 2, 2], - xbins: { - start: 1, - end: 3, - size: 1 - } - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.autobinx).toBeUndefined(); - - traceIn = { - x: [1, 2, 2] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.autobinx).toBeUndefined(); - }); - - it('should not coerce autobiny regardless of ybins', function() { - traceIn = { - y: [1, 2, 2], - ybins: { - start: 1, - end: 3, - size: 1 - } - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.autobiny).toBeUndefined(); - - traceIn = { - y: [1, 2, 2] - }; - supplyDefaults(traceIn, traceOut, '', {}); - expect(traceOut.autobiny).toBeUndefined(); - }); - - it('should inherit layout.calendar', function() { - traceIn = { - x: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, '', {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - // size axis calendar is weird, but *might* be able to happen if - // we're using histfunc=min or max (does this work?) - expect(traceOut.xcalendar).toBe('islamic'); - expect(traceOut.ycalendar).toBe('islamic'); - }); - - it('should take its own calendars', function() { - traceIn = { - x: [1, 2, 3], - xcalendar: 'coptic', - ycalendar: 'nepali' - }; - supplyDefaults(traceIn, traceOut, '', {calendar: 'islamic'}); - - expect(traceOut.xcalendar).toBe('coptic'); - expect(traceOut.ycalendar).toBe('nepali'); - }); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); + +var supplyDefaults = require("@src/traces/histogram/defaults"); +var calc = require("@src/traces/histogram/calc"); + +describe("Test histogram", function() { + "use strict"; + describe("supplyDefaults", function() { + var traceIn, traceOut; + + beforeEach(function() { + traceOut = {}; }); + it("should set visible to false when x or y is empty", function() { + traceIn = { x: [] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.visible).toBe(false); - describe('calc', function() { - function _calc(opts) { - var base = { type: 'histogram' }, - trace = Lib.extendFlat({}, base, opts), - gd = { data: [trace] }; + traceIn = { y: [] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.visible).toBe(false); + }); - Plots.supplyDefaults(gd); - var fullTrace = gd._fullData[0]; + it( + "should set visible to false when type is histogram2d and x or y are empty", + function() { + traceIn = { type: "histogram2d", x: [], y: [1, 2, 2] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.visible).toBe(false); + + traceIn = { type: "histogram2d", x: [1, 2, 2], y: [] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.visible).toBe(false); + + traceIn = { type: "histogram2d", x: [], y: [] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.visible).toBe(false); + + traceIn = { type: "histogram2dcontour", x: [], y: [1, 2, 2] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.visible).toBe(false); + } + ); + + it("should set orientation to v by default", function() { + traceIn = { x: [1, 2, 2] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.orientation).toBe("v"); + + traceIn = { x: [1, 2, 2], y: [1, 2, 2] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.orientation).toBe("v"); + }); - var out = calc(gd, fullTrace); - delete out[0].trace; - return out; - } + it("should set orientation to h when only y is supplied", function() { + traceIn = { y: [1, 2, 2] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.orientation).toBe("h"); + }); - var oneDay = 24 * 3600000; + // coercing bin attributes got moved to calc because it needs + // axis type - so here we just test that it's NOT happening + it("should not coerce autobinx regardless of xbins", function() { + traceIn = { x: [1, 2, 2], xbins: { start: 1, end: 3, size: 1 } }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.autobinx).toBeUndefined(); - it('should handle auto dates with nonuniform (month) bins', function() { - // All data on exact years: shift so bin center is an - // exact year, except on leap years - var out = _calc({ - x: ['1970-01-01', '1970-01-01', '1971-01-01', '1973-01-01'], - nbinsx: 4 - }); + traceIn = { x: [1, 2, 2] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.autobinx).toBeUndefined(); + }); - // TODO: this gives half-day gaps between all but the first two - // bars. Now that we have explicit per-bar positioning, perhaps - // we should fill the space, rather than insisting on equal-width - // bars? - expect(out).toEqual([ - // full calcdata has x and y too (and t in the first one), - // but those come later from setPositions. - {b: 0, p: Date.UTC(1970, 0, 1), s: 2}, - {b: 0, p: Date.UTC(1971, 0, 1), s: 1}, - {b: 0, p: Date.UTC(1972, 0, 1, 12), s: 0}, - {b: 0, p: Date.UTC(1973, 0, 1), s: 1} - ]); - - // All data on exact months: shift so bin center is on (31-day months) - // or in (shorter months) that month - out = _calc({ - x: ['1970-01-01', '1970-01-01', '1970-02-01', '1970-04-01'], - nbinsx: 4 - }); + it("should not coerce autobiny regardless of ybins", function() { + traceIn = { y: [1, 2, 2], ybins: { start: 1, end: 3, size: 1 } }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.autobiny).toBeUndefined(); - expect(out).toEqual([ - {b: 0, p: Date.UTC(1970, 0, 1), s: 2}, - {b: 0, p: Date.UTC(1970, 1, 1), s: 1}, - {b: 0, p: Date.UTC(1970, 2, 2, 12), s: 0}, - {b: 0, p: Date.UTC(1970, 3, 1), s: 1} - ]); - - // data on exact days: shift so each bin goes from noon to noon - // even though this gives kind of odd bin centers since the bins - // are months... but the important thing is it's unambiguous which - // bin any given day is in. - out = _calc({ - x: ['1970-01-02', '1970-01-31', '1970-02-13', '1970-04-19'], - nbinsx: 4 - }); + traceIn = { y: [1, 2, 2] }; + supplyDefaults(traceIn, traceOut, "", {}); + expect(traceOut.autobiny).toBeUndefined(); + }); - expect(out).toEqual([ - // dec 31 12:00 -> jan 31 12:00, middle is jan 16 - {b: 0, p: Date.UTC(1970, 0, 16), s: 2}, - // jan 31 12:00 -> feb 28 12:00, middle is feb 14 12:00 - {b: 0, p: Date.UTC(1970, 1, 14, 12), s: 1}, - {b: 0, p: Date.UTC(1970, 2, 16), s: 0}, - {b: 0, p: Date.UTC(1970, 3, 15, 12), s: 1} - ]); - }); - - it('should handle auto dates with uniform (day) bins', function() { - var out = _calc({ - x: ['1970-01-01', '1970-01-01', '1970-01-02', '1970-01-04'], - nbinsx: 4 - }); + it("should inherit layout.calendar", function() { + traceIn = { x: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, "", { calendar: "islamic" }); - var x0 = 0, - x1 = x0 + oneDay, - x2 = x1 + oneDay, - x3 = x2 + oneDay; - - expect(out).toEqual([ - {b: 0, p: x0, s: 2}, - {b: 0, p: x1, s: 1}, - {b: 0, p: x2, s: 0}, - {b: 0, p: x3, s: 1} - ]); - }); - - describe('cumulative distribution functions', function() { - var base = { - x: [0, 5, 10, 15, 5, 10, 15, 10, 15, 15], - y: [2, 2, 2, 14, 6, 6, 6, 10, 10, 2] - }; - - it('makes the right base histogram', function() { - var baseOut = _calc(base); - expect(baseOut).toEqual([ - {b: 0, p: 2, s: 1}, - {b: 0, p: 7, s: 2}, - {b: 0, p: 12, s: 3}, - {b: 0, p: 17, s: 4}, - ]); - }); + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + // size axis calendar is weird, but *might* be able to happen if + // we're using histfunc=min or max (does this work?) + expect(traceOut.xcalendar).toBe("islamic"); + expect(traceOut.ycalendar).toBe("islamic"); + }); + + it("should take its own calendars", function() { + traceIn = { x: [1, 2, 3], xcalendar: "coptic", ycalendar: "nepali" }; + supplyDefaults(traceIn, traceOut, "", { calendar: "islamic" }); + + expect(traceOut.xcalendar).toBe("coptic"); + expect(traceOut.ycalendar).toBe("nepali"); + }); + }); + + describe("calc", function() { + function _calc(opts) { + var base = { type: "histogram" }, + trace = Lib.extendFlat({}, base, opts), + gd = { data: [trace] }; + + Plots.supplyDefaults(gd); + var fullTrace = gd._fullData[0]; + + var out = calc(gd, fullTrace); + delete out[0].trace; + return out; + } + + var oneDay = 24 * 3600000; + + it("should handle auto dates with nonuniform (month) bins", function() { + // All data on exact years: shift so bin center is an + // exact year, except on leap years + var out = _calc({ + x: ["1970-01-01", "1970-01-01", "1971-01-01", "1973-01-01"], + nbinsx: 4 + }); + + // TODO: this gives half-day gaps between all but the first two + // bars. Now that we have explicit per-bar positioning, perhaps + // we should fill the space, rather than insisting on equal-width + // bars? + expect(out).toEqual([ + // full calcdata has x and y too (and t in the first one), + // but those come later from setPositions. + { b: 0, p: Date.UTC(1970, 0, 1), s: 2 }, + { b: 0, p: Date.UTC(1971, 0, 1), s: 1 }, + { b: 0, p: Date.UTC(1972, 0, 1, 12), s: 0 }, + { b: 0, p: Date.UTC(1973, 0, 1), s: 1 } + ]); + + // All data on exact months: shift so bin center is on (31-day months) + // or in (shorter months) that month + out = _calc({ + x: ["1970-01-01", "1970-01-01", "1970-02-01", "1970-04-01"], + nbinsx: 4 + }); + + expect(out).toEqual([ + { b: 0, p: Date.UTC(1970, 0, 1), s: 2 }, + { b: 0, p: Date.UTC(1970, 1, 1), s: 1 }, + { b: 0, p: Date.UTC(1970, 2, 2, 12), s: 0 }, + { b: 0, p: Date.UTC(1970, 3, 1), s: 1 } + ]); + + // data on exact days: shift so each bin goes from noon to noon + // even though this gives kind of odd bin centers since the bins + // are months... but the important thing is it's unambiguous which + // bin any given day is in. + out = _calc({ + x: ["1970-01-02", "1970-01-31", "1970-02-13", "1970-04-19"], + nbinsx: 4 + }); + + expect(out).toEqual([ + // dec 31 12:00 -> jan 31 12:00, middle is jan 16 + { b: 0, p: Date.UTC(1970, 0, 16), s: 2 }, + // jan 31 12:00 -> feb 28 12:00, middle is feb 14 12:00 + { b: 0, p: Date.UTC(1970, 1, 14, 12), s: 1 }, + { b: 0, p: Date.UTC(1970, 2, 16), s: 0 }, + { b: 0, p: Date.UTC(1970, 3, 15, 12), s: 1 } + ]); + }); - var CDFs = [ - {p: [2, 7, 12, 17], s: [1, 3, 6, 10]}, - { - direction: 'decreasing', - p: [2, 7, 12, 17], s: [10, 9, 7, 4] - }, - { - currentbin: 'exclude', - p: [7, 12, 17, 22], s: [1, 3, 6, 10] - }, - { - direction: 'decreasing', currentbin: 'exclude', - p: [-3, 2, 7, 12], s: [10, 9, 7, 4] - }, - { - currentbin: 'half', - p: [2, 7, 12, 17, 22], s: [0.5, 2, 4.5, 8, 10] - }, - { - direction: 'decreasing', currentbin: 'half', - p: [-3, 2, 7, 12, 17], s: [10, 9.5, 8, 5.5, 2] - }, - { - direction: 'decreasing', currentbin: 'half', histnorm: 'percent', - p: [-3, 2, 7, 12, 17], s: [100, 95, 80, 55, 20] - }, - { - currentbin: 'exclude', histnorm: 'probability', - p: [7, 12, 17, 22], s: [0.1, 0.3, 0.6, 1] - }, - { - // behaves the same as without *density* - direction: 'decreasing', currentbin: 'half', histnorm: 'density', - p: [-3, 2, 7, 12, 17], s: [10, 9.5, 8, 5.5, 2] - }, - { - // behaves the same as without *density*, only *probability* - direction: 'decreasing', currentbin: 'half', histnorm: 'probability density', - p: [-3, 2, 7, 12, 17], s: [1, 0.95, 0.8, 0.55, 0.2] - }, - { - currentbin: 'half', histfunc: 'sum', - p: [2, 7, 12, 17, 22], s: [1, 6, 19, 44, 60] - }, - { - currentbin: 'half', histfunc: 'sum', histnorm: 'probability', - p: [2, 7, 12, 17, 22], s: [0.5 / 30, 0.1, 9.5 / 30, 22 / 30, 1] - }, - { - direction: 'decreasing', currentbin: 'half', histfunc: 'max', histnorm: 'percent', - p: [-3, 2, 7, 12, 17], s: [100, 3100 / 32, 2700 / 32, 1900 / 32, 700 / 32] - }, - { - direction: 'decreasing', currentbin: 'half', histfunc: 'min', histnorm: 'density', - p: [-3, 2, 7, 12, 17], s: [8, 7, 5, 3, 1] - }, - { - currentbin: 'exclude', histfunc: 'avg', histnorm: 'probability density', - p: [7, 12, 17, 22], s: [0.1, 0.3, 0.6, 1] - } - ]; - - CDFs.forEach(function(CDF) { - var p = CDF.p, - s = CDF.s; - - it('handles direction=' + CDF.direction + ', currentbin=' + CDF.currentbin + - ', histnorm=' + CDF.histnorm + ', histfunc=' + CDF.histfunc, function() { - var traceIn = Lib.extendFlat({}, base, { - cumulative: { - enabled: true, - direction: CDF.direction, - currentbin: CDF.currentbin - }, - histnorm: CDF.histnorm, - histfunc: CDF.histfunc - }); - var out = _calc(traceIn); - - expect(out.length).toBe(p.length); - out.forEach(function(outi, i) { - expect(outi.p).toBe(p[i]); - expect(outi.s).toBeCloseTo(s[i], 6); - expect(outi.b).toBe(0); - }); - }); + it("should handle auto dates with uniform (day) bins", function() { + var out = _calc({ + x: ["1970-01-01", "1970-01-01", "1970-01-02", "1970-01-04"], + nbinsx: 4 + }); + + var x0 = 0, x1 = x0 + oneDay, x2 = x1 + oneDay, x3 = x2 + oneDay; + + expect(out).toEqual([ + { b: 0, p: x0, s: 2 }, + { b: 0, p: x1, s: 1 }, + { b: 0, p: x2, s: 0 }, + { b: 0, p: x3, s: 1 } + ]); + }); + + describe("cumulative distribution functions", function() { + var base = { + x: [0, 5, 10, 15, 5, 10, 15, 10, 15, 15], + y: [2, 2, 2, 14, 6, 6, 6, 10, 10, 2] + }; + + it("makes the right base histogram", function() { + var baseOut = _calc(base); + expect(baseOut).toEqual([ + { b: 0, p: 2, s: 1 }, + { b: 0, p: 7, s: 2 }, + { b: 0, p: 12, s: 3 }, + { b: 0, p: 17, s: 4 } + ]); + }); + + var CDFs = [ + { p: [2, 7, 12, 17], s: [1, 3, 6, 10] }, + { direction: "decreasing", p: [2, 7, 12, 17], s: [10, 9, 7, 4] }, + { currentbin: "exclude", p: [7, 12, 17, 22], s: [1, 3, 6, 10] }, + { + direction: "decreasing", + currentbin: "exclude", + p: [-3, 2, 7, 12], + s: [10, 9, 7, 4] + }, + { + currentbin: "half", + p: [2, 7, 12, 17, 22], + s: [0.5, 2, 4.5, 8, 10] + }, + { + direction: "decreasing", + currentbin: "half", + p: [-3, 2, 7, 12, 17], + s: [10, 9.5, 8, 5.5, 2] + }, + { + direction: "decreasing", + currentbin: "half", + histnorm: "percent", + p: [-3, 2, 7, 12, 17], + s: [100, 95, 80, 55, 20] + }, + { + currentbin: "exclude", + histnorm: "probability", + p: [7, 12, 17, 22], + s: [0.1, 0.3, 0.6, 1] + }, + { + // behaves the same as without *density* + direction: "decreasing", + currentbin: "half", + histnorm: "density", + p: [-3, 2, 7, 12, 17], + s: [10, 9.5, 8, 5.5, 2] + }, + { + // behaves the same as without *density*, only *probability* + direction: "decreasing", + currentbin: "half", + histnorm: "probability density", + p: [-3, 2, 7, 12, 17], + s: [1, 0.95, 0.8, 0.55, 0.2] + }, + { + currentbin: "half", + histfunc: "sum", + p: [2, 7, 12, 17, 22], + s: [1, 6, 19, 44, 60] + }, + { + currentbin: "half", + histfunc: "sum", + histnorm: "probability", + p: [2, 7, 12, 17, 22], + s: [0.5 / 30, 0.1, 9.5 / 30, 22 / 30, 1] + }, + { + direction: "decreasing", + currentbin: "half", + histfunc: "max", + histnorm: "percent", + p: [-3, 2, 7, 12, 17], + s: [100, 3100 / 32, 2700 / 32, 1900 / 32, 700 / 32] + }, + { + direction: "decreasing", + currentbin: "half", + histfunc: "min", + histnorm: "density", + p: [-3, 2, 7, 12, 17], + s: [8, 7, 5, 3, 1] + }, + { + currentbin: "exclude", + histfunc: "avg", + histnorm: "probability density", + p: [7, 12, 17, 22], + s: [0.1, 0.3, 0.6, 1] + } + ]; + + CDFs.forEach(function(CDF) { + var p = CDF.p, s = CDF.s; + + it( + "handles direction=" + + CDF.direction + + ", currentbin=" + + CDF.currentbin + + ", histnorm=" + + CDF.histnorm + + ", histfunc=" + + CDF.histfunc, + function() { + var traceIn = Lib.extendFlat({}, base, { + cumulative: { + enabled: true, + direction: CDF.direction, + currentbin: CDF.currentbin + }, + histnorm: CDF.histnorm, + histfunc: CDF.histfunc }); - }); + var out = _calc(traceIn); + expect(out.length).toBe(p.length); + out.forEach(function(outi, i) { + expect(outi.p).toBe(p[i]); + expect(outi.s).toBeCloseTo(s[i], 6); + expect(outi.b).toBe(0); + }); + } + ); + }); }); + }); }); diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 2e76a3ab0e5..6b61fc807de 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1,813 +1,893 @@ -var d3 = require('d3'); +var d3 = require("d3"); -var Plotly = require('@lib/index'); -var Fx = require('@src/plots/cartesian/graph_interact'); -var constants = require('@src/plots/cartesian/constants'); -var Lib = require('@src/lib'); +var Plotly = require("@lib/index"); +var Fx = require("@src/plots/cartesian/graph_interact"); +var constants = require("@src/plots/cartesian/constants"); +var Lib = require("@src/lib"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); -var click = require('../assets/click'); -var doubleClick = require('../assets/double_click'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); +var click = require("../assets/click"); +var doubleClick = require("../assets/double_click"); -describe('hover info', function() { - 'use strict'; +describe("hover info", function() { + "use strict"; + var mock = require("@mocks/14.json"), + evt = { clientX: mock.layout.width / 2, clientY: mock.layout.height / 2 }; - var mock = require('@mocks/14.json'), - evt = { - clientX: mock.layout.width / 2, - clientY: mock.layout.height / 2 - }; + afterEach(destroyGraphDiv); - afterEach(destroyGraphDiv); + describe("hover info", function() { + var mockCopy = Lib.extendDeep({}, mock); - describe('hover info', function() { - var mockCopy = Lib.extendDeep({}, mock); - - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); - - it('responds to hover', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); - - var hoverTrace = gd._hoverdata[0]; - - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(17); - expect(hoverTrace.x).toEqual(0.388); - expect(hoverTrace.y).toEqual(1); - - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual('0.388'); - expect(d3.selectAll('g.hovertext').select('text').html()).toEqual('1'); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); }); - describe('hover info x', function() { - var mockCopy = Lib.extendDeep({}, mock); - - mockCopy.data[0].hoverinfo = 'x'; + it("responds to hover", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); - - it('responds to hover x', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); + var hoverTrace = gd._hoverdata[0]; - var hoverTrace = gd._hoverdata[0]; + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(17); + expect(hoverTrace.x).toEqual(0.388); + expect(hoverTrace.y).toEqual(1); - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(17); - expect(hoverTrace.x).toEqual(0.388); - expect(hoverTrace.y).toEqual(1); - - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').size()).toEqual(0); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual('0.388'); - }); + expect(d3.selectAll("g.axistext").size()).toEqual(1); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); + expect(d3.selectAll("g.axistext").select("text").html()).toEqual("0.388"); + expect(d3.selectAll("g.hovertext").select("text").html()).toEqual("1"); }); + }); - describe('hover info y', function() { - var mockCopy = Lib.extendDeep({}, mock); - - mockCopy.data[0].hoverinfo = 'y'; - - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); - - it('responds to hover y', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); + describe("hover info x", function() { + var mockCopy = Lib.extendDeep({}, mock); - var hoverTrace = gd._hoverdata[0]; + mockCopy.data[0].hoverinfo = "x"; - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(17); - expect(hoverTrace.x).toEqual(0.388); - expect(hoverTrace.y).toEqual(1); - - expect(d3.selectAll('g.axistext').size()).toEqual(0); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').select('text').html()).toEqual('1'); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); }); - describe('hover info text', function() { - var mockCopy = Lib.extendDeep({}, mock); - - mockCopy.data[0].text = []; - // we convert newlines to spaces - // see https://github.com/plotly/plotly.js/issues/746 - mockCopy.data[0].text[17] = 'hover\ntext\n\rwith\r\nspaces\n\nnot\rnewlines'; - mockCopy.data[0].hoverinfo = 'text'; - - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); - - it('responds to hover text', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); + it("responds to hover x", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); - var hoverTrace = gd._hoverdata[0]; + var hoverTrace = gd._hoverdata[0]; - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(17); - expect(hoverTrace.x).toEqual(0.388); - expect(hoverTrace.y).toEqual(1); + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(17); + expect(hoverTrace.x).toEqual(0.388); + expect(hoverTrace.y).toEqual(1); - expect(d3.selectAll('g.axistext').size()).toEqual(0); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').select('text').html()) - .toEqual('hover text with spaces not newlines'); - }); + expect(d3.selectAll("g.axistext").size()).toEqual(1); + expect(d3.selectAll("g.hovertext").size()).toEqual(0); + expect(d3.selectAll("g.axistext").select("text").html()).toEqual("0.388"); }); + }); - describe('hover info all', function() { - var mockCopy = Lib.extendDeep({}, mock); - - mockCopy.data[0].text = []; - mockCopy.data[0].text[17] = 'hover text'; - mockCopy.data[0].hoverinfo = 'all'; + describe("hover info y", function() { + var mockCopy = Lib.extendDeep({}, mock); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); - - it('responds to hover all', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); - - var hoverTrace = gd._hoverdata[0]; - - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(17); - expect(hoverTrace.x).toEqual(0.388); - expect(hoverTrace.y).toEqual(1); + mockCopy.data[0].hoverinfo = "y"; - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual('0.388'); - expect(d3.selectAll('g.hovertext').select('text').selectAll('tspan').size()).toEqual(2); - expect(d3.selectAll('g.hovertext').selectAll('tspan')[0][0].innerHTML).toEqual('1'); - expect(d3.selectAll('g.hovertext').selectAll('tspan')[0][1].innerHTML).toEqual('hover text'); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); }); - describe('hover info with bad name', function() { - var mockCopy = Lib.extendDeep({}, mock); + it("responds to hover y", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); - mockCopy.data[0].text = []; - mockCopy.data[0].text[17] = 'hover text'; - mockCopy.data[0].hoverinfo = 'all'; - mockCopy.data[0].name = ''; - mockCopy.data.push({ - x: [0.002, 0.004], - y: [12.5, 16.25], - mode: 'lines+markers', - name: 'another trace' - }); + var hoverTrace = gd._hoverdata[0]; - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(17); + expect(hoverTrace.x).toEqual(0.388); + expect(hoverTrace.y).toEqual(1); - it('cleans the name', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); + expect(d3.selectAll("g.axistext").size()).toEqual(0); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); + expect(d3.selectAll("g.hovertext").select("text").html()).toEqual("1"); + }); + }); - var hoverTrace = gd._hoverdata[0]; + describe("hover info text", function() { + var mockCopy = Lib.extendDeep({}, mock); - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(17); - expect(hoverTrace.x).toEqual(0.388); - expect(hoverTrace.y).toEqual(1); + mockCopy.data[0].text = []; + // we convert newlines to spaces + // see https://github.com/plotly/plotly.js/issues/746 + mockCopy.data[0].text[ + 17 + ] = "hover\ntext\n\rwith\r\nspaces\n\nnot\rnewlines"; + mockCopy.data[0].hoverinfo = "text"; - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual('0.388'); - expect(d3.selectAll('g.hovertext').select('text.nums').selectAll('tspan').size()).toEqual(2); - expect(d3.selectAll('g.hovertext').selectAll('tspan')[0][0].innerHTML).toEqual('1'); - expect(d3.selectAll('g.hovertext').selectAll('tspan')[0][1].innerHTML).toEqual('hover text'); - expect(d3.selectAll('g.hovertext').selectAll('text.name').node().innerHTML).toEqual('<img src=x o...'); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); }); - describe('hover info y+text', function() { - var mockCopy = Lib.extendDeep({}, mock); + it("responds to hover text", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); - mockCopy.data[0].text = []; - mockCopy.data[0].text[17] = 'hover text'; - mockCopy.data[0].hoverinfo = 'y+text'; + var hoverTrace = gd._hoverdata[0]; - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(17); + expect(hoverTrace.x).toEqual(0.388); + expect(hoverTrace.y).toEqual(1); - it('responds to hover y+text', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); + expect(d3.selectAll("g.axistext").size()).toEqual(0); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); + expect(d3.selectAll("g.hovertext").select("text").html()).toEqual( + "hover text with spaces not newlines" + ); + }); + }); - var hoverTrace = gd._hoverdata[0]; + describe("hover info all", function() { + var mockCopy = Lib.extendDeep({}, mock); - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(17); - expect(hoverTrace.x).toEqual(0.388); - expect(hoverTrace.y).toEqual(1); + mockCopy.data[0].text = []; + mockCopy.data[0].text[17] = "hover text"; + mockCopy.data[0].hoverinfo = "all"; - expect(d3.selectAll('g.axistext').size()).toEqual(0); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').selectAll('tspan').size()).toEqual(2); - expect(d3.selectAll('g.hovertext').selectAll('tspan')[0][0].innerHTML).toEqual('1'); - expect(d3.selectAll('g.hovertext').selectAll('tspan')[0][1].innerHTML).toEqual('hover text'); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); }); - describe('hover info x+text', function() { - var mockCopy = Lib.extendDeep({}, mock); - - mockCopy.data[0].text = []; - mockCopy.data[0].text[17] = 'hover text'; - mockCopy.data[0].hoverinfo = 'x+text'; + it("responds to hover all", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); + + var hoverTrace = gd._hoverdata[0]; + + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(17); + expect(hoverTrace.x).toEqual(0.388); + expect(hoverTrace.y).toEqual(1); + + expect(d3.selectAll("g.axistext").size()).toEqual(1); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); + expect(d3.selectAll("g.axistext").select("text").html()).toEqual("0.388"); + expect( + d3.selectAll("g.hovertext").select("text").selectAll("tspan").size() + ).toEqual(2); + expect( + d3.selectAll("g.hovertext").selectAll("tspan")[0][0].innerHTML + ).toEqual("1"); + expect( + d3.selectAll("g.hovertext").selectAll("tspan")[0][1].innerHTML + ).toEqual("hover text"); + }); + }); + + describe("hover info with bad name", function() { + var mockCopy = Lib.extendDeep({}, mock); + + mockCopy.data[0].text = []; + mockCopy.data[0].text[17] = "hover text"; + mockCopy.data[0].hoverinfo = "all"; + mockCopy.data[0].name = ""; + mockCopy.data.push({ + x: [0.002, 0.004], + y: [12.5, 16.25], + mode: "lines+markers", + name: "another trace" + }); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); + }); - it('responds to hover x+text', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); + it("cleans the name", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); + + var hoverTrace = gd._hoverdata[0]; + + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(17); + expect(hoverTrace.x).toEqual(0.388); + expect(hoverTrace.y).toEqual(1); + + expect(d3.selectAll("g.axistext").size()).toEqual(1); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); + expect(d3.selectAll("g.axistext").select("text").html()).toEqual("0.388"); + expect( + d3 + .selectAll("g.hovertext") + .select("text.nums") + .selectAll("tspan") + .size() + ).toEqual(2); + expect( + d3.selectAll("g.hovertext").selectAll("tspan")[0][0].innerHTML + ).toEqual("1"); + expect( + d3.selectAll("g.hovertext").selectAll("tspan")[0][1].innerHTML + ).toEqual("hover text"); + expect( + d3.selectAll("g.hovertext").selectAll("text.name").node().innerHTML + ).toEqual("<img src=x o..."); + }); + }); - var hoverTrace = gd._hoverdata[0]; + describe("hover info y+text", function() { + var mockCopy = Lib.extendDeep({}, mock); - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(17); - expect(hoverTrace.x).toEqual(0.388); - expect(hoverTrace.y).toEqual(1); + mockCopy.data[0].text = []; + mockCopy.data[0].text[17] = "hover text"; + mockCopy.data[0].hoverinfo = "y+text"; - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual('0.388'); - expect(d3.selectAll('g.hovertext').select('text').html()).toEqual('hover text'); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); }); - describe('hover error x text (log axis positive)', function() { - var mockCopy = Lib.extendDeep({}, mock); - - mockCopy.data[0].error_x = { array: [] }; - mockCopy.data[0].error_x.array[17] = 1; + it("responds to hover y+text", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); + + var hoverTrace = gd._hoverdata[0]; + + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(17); + expect(hoverTrace.x).toEqual(0.388); + expect(hoverTrace.y).toEqual(1); + + expect(d3.selectAll("g.axistext").size()).toEqual(0); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); + expect(d3.selectAll("g.hovertext").selectAll("tspan").size()).toEqual(2); + expect( + d3.selectAll("g.hovertext").selectAll("tspan")[0][0].innerHTML + ).toEqual("1"); + expect( + d3.selectAll("g.hovertext").selectAll("tspan")[0][1].innerHTML + ).toEqual("hover text"); + }); + }); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); + describe("hover info x+text", function() { + var mockCopy = Lib.extendDeep({}, mock); - it('responds to hover x+text', function() { - Fx.hover('graph', evt, 'xy'); + mockCopy.data[0].text = []; + mockCopy.data[0].text[17] = "hover text"; + mockCopy.data[0].hoverinfo = "x+text"; - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual('0.388 ± 1'); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); }); - describe('hover error text (log axis 0)', function() { - var mockCopy = Lib.extendDeep({}, mock); - - mockCopy.data[0].error_x = { array: [] }; - mockCopy.data[0].error_x.array[17] = 0; + it("responds to hover x+text", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); + var hoverTrace = gd._hoverdata[0]; - it('responds to hover x+text', function() { - Fx.hover('graph', evt, 'xy'); + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(17); + expect(hoverTrace.x).toEqual(0.388); + expect(hoverTrace.y).toEqual(1); - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual('0.388'); - }); + expect(d3.selectAll("g.axistext").size()).toEqual(1); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); + expect(d3.selectAll("g.axistext").select("text").html()).toEqual("0.388"); + expect(d3.selectAll("g.hovertext").select("text").html()).toEqual( + "hover text" + ); }); + }); - describe('hover error text (log axis negative)', function() { - var mockCopy = Lib.extendDeep({}, mock); - - mockCopy.data[0].error_x = { array: [] }; - mockCopy.data[0].error_x.array[17] = -1; - - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); + describe("hover error x text (log axis positive)", function() { + var mockCopy = Lib.extendDeep({}, mock); - it('responds to hover x+text', function() { - Fx.hover('graph', evt, 'xy'); + mockCopy.data[0].error_x = { array: [] }; + mockCopy.data[0].error_x.array[17] = 1; - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual('0.388'); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); }); - describe('hover info text with html', function() { - var mockCopy = Lib.extendDeep({}, mock); + it("responds to hover x+text", function() { + Fx.hover("graph", evt, "xy"); - mockCopy.data[0].text = []; - mockCopy.data[0].text[17] = 'hover
text'; - mockCopy.data[0].hoverinfo = 'text'; + expect(d3.selectAll("g.axistext").size()).toEqual(1); + expect(d3.selectAll("g.axistext").select("text").html()).toEqual( + "0.388 \xB1 1" + ); + }); + }); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); + describe("hover error text (log axis 0)", function() { + var mockCopy = Lib.extendDeep({}, mock); - it('responds to hover text with html', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); + mockCopy.data[0].error_x = { array: [] }; + mockCopy.data[0].error_x.array[17] = 0; - var hoverTrace = gd._hoverdata[0]; + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); + }); - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(17); - expect(hoverTrace.x).toEqual(0.388); - expect(hoverTrace.y).toEqual(1); + it("responds to hover x+text", function() { + Fx.hover("graph", evt, "xy"); - expect(d3.selectAll('g.axistext').size()).toEqual(0); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').selectAll('tspan')[0][0].innerHTML).toEqual('hover'); - expect(d3.selectAll('g.hovertext').selectAll('tspan')[0][1].innerHTML).toEqual('text'); - expect(d3.selectAll('g.hovertext').select('text').selectAll('tspan').size()).toEqual(2); - }); + expect(d3.selectAll("g.axistext").size()).toEqual(1); + expect(d3.selectAll("g.axistext").select("text").html()).toEqual("0.388"); }); + }); - describe('hover info skip', function() { - var mockCopy = Lib.extendDeep({}, mock); + describe("hover error text (log axis negative)", function() { + var mockCopy = Lib.extendDeep({}, mock); - mockCopy.data[0].hoverinfo = 'skip'; + mockCopy.data[0].error_x = { array: [] }; + mockCopy.data[0].error_x.array[17] = -1; - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); + }); - it('does not hover if hover info is set to skip', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); + it("responds to hover x+text", function() { + Fx.hover("graph", evt, "xy"); - expect(gd._hoverdata, undefined); - }); + expect(d3.selectAll("g.axistext").size()).toEqual(1); + expect(d3.selectAll("g.axistext").select("text").html()).toEqual("0.388"); }); + }); - describe('hover info none', function() { - var mockCopy = Lib.extendDeep({}, mock); + describe("hover info text with html", function() { + var mockCopy = Lib.extendDeep({}, mock); - mockCopy.data[0].hoverinfo = 'none'; + mockCopy.data[0].text = []; + mockCopy.data[0].text[17] = "hover
text"; + mockCopy.data[0].hoverinfo = "text"; - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); + }); - it('does not render if hover is set to none', function() { - var gd = document.getElementById('graph'); - Fx.hover('graph', evt, 'xy'); + it("responds to hover text with html", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); + + var hoverTrace = gd._hoverdata[0]; + + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(17); + expect(hoverTrace.x).toEqual(0.388); + expect(hoverTrace.y).toEqual(1); + + expect(d3.selectAll("g.axistext").size()).toEqual(0); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); + expect( + d3.selectAll("g.hovertext").selectAll("tspan")[0][0].innerHTML + ).toEqual("hover"); + expect( + d3.selectAll("g.hovertext").selectAll("tspan")[0][1].innerHTML + ).toEqual("text"); + expect( + d3.selectAll("g.hovertext").select("text").selectAll("tspan").size() + ).toEqual(2); + }); + }); - var hoverTrace = gd._hoverdata[0]; + describe("hover info skip", function() { + var mockCopy = Lib.extendDeep({}, mock); - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(17); - expect(hoverTrace.x).toEqual(0.388); - expect(hoverTrace.y).toEqual(1); + mockCopy.data[0].hoverinfo = "skip"; - expect(d3.selectAll('g.axistext').size()).toEqual(0); - expect(d3.selectAll('g.hovertext').size()).toEqual(0); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); }); - describe('\'closest\' hover info (superimposed case)', function() { - var mockCopy = Lib.extendDeep({}, mock); + it("does not hover if hover info is set to skip", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); - // superimposed traces - mockCopy.data.push(Lib.extendDeep({}, mockCopy.data[0])); - mockCopy.layout.hovermode = 'closest'; + expect(gd._hoverdata, undefined); + }); + }); - var gd; + describe("hover info none", function() { + var mockCopy = Lib.extendDeep({}, mock); - beforeEach(function(done) { - gd = createGraphDiv(); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); + mockCopy.data[0].hoverinfo = "none"; - it('render hover labels of the above trace', function() { - Fx.hover('graph', evt, 'xy'); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); + }); - expect(gd._hoverdata.length).toEqual(1); + it("does not render if hover is set to none", function() { + var gd = document.getElementById("graph"); + Fx.hover("graph", evt, "xy"); - var hoverTrace = gd._hoverdata[0]; + var hoverTrace = gd._hoverdata[0]; - expect(hoverTrace.fullData.index).toEqual(1); - expect(hoverTrace.curveNumber).toEqual(1); - expect(hoverTrace.pointNumber).toEqual(16); - expect(hoverTrace.x).toEqual(0.33); - expect(hoverTrace.y).toEqual(1.25); + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(17); + expect(hoverTrace.x).toEqual(0.388); + expect(hoverTrace.y).toEqual(1); - expect(d3.selectAll('g.axistext').size()).toEqual(0); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); + expect(d3.selectAll("g.axistext").size()).toEqual(0); + expect(d3.selectAll("g.hovertext").size()).toEqual(0); + }); + }); - var expectations = ['PV learning ...', '(0.33, 1.25)']; - d3.selectAll('g.hovertext').selectAll('text').each(function(_, i) { - expect(d3.select(this).html()).toEqual(expectations[i]); - }); - }); + describe("'closest' hover info (superimposed case)", function() { + var mockCopy = Lib.extendDeep({}, mock); - it('render only non-hoverinfo \'none\' hover labels', function(done) { + // superimposed traces + mockCopy.data.push(Lib.extendDeep({}, mockCopy.data[0])); + mockCopy.layout.hovermode = "closest"; - Plotly.restyle(gd, 'hoverinfo', ['none', 'name']).then(function() { - Fx.hover('graph', evt, 'xy'); + var gd; - expect(gd._hoverdata.length).toEqual(1); + beforeEach(function(done) { + gd = createGraphDiv(); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - var hoverTrace = gd._hoverdata[0]; + it("render hover labels of the above trace", function() { + Fx.hover("graph", evt, "xy"); - expect(hoverTrace.fullData.index).toEqual(1); - expect(hoverTrace.curveNumber).toEqual(1); - expect(hoverTrace.pointNumber).toEqual(16); - expect(hoverTrace.x).toEqual(0.33); - expect(hoverTrace.y).toEqual(1.25); + expect(gd._hoverdata.length).toEqual(1); - expect(d3.selectAll('g.axistext').size()).toEqual(0); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); + var hoverTrace = gd._hoverdata[0]; - var text = d3.selectAll('g.hovertext').select('text'); - expect(text.size()).toEqual(1); - expect(text.html()).toEqual('PV learning ...'); + expect(hoverTrace.fullData.index).toEqual(1); + expect(hoverTrace.curveNumber).toEqual(1); + expect(hoverTrace.pointNumber).toEqual(16); + expect(hoverTrace.x).toEqual(0.33); + expect(hoverTrace.y).toEqual(1.25); - done(); - }); + expect(d3.selectAll("g.axistext").size()).toEqual(0); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); - }); + var expectations = ["PV learning ...", "(0.33, 1.25)"]; + d3.selectAll("g.hovertext").selectAll("text").each(function(_, i) { + expect(d3.select(this).html()).toEqual(expectations[i]); + }); }); - describe('hoverformat', function() { + it("render only non-hoverinfo 'none' hover labels", function(done) { + Plotly.restyle(gd, "hoverinfo", ["none", "name"]).then(function() { + Fx.hover("graph", evt, "xy"); - var data = [{ - x: [1, 2, 3], - y: [0.12345, 0.23456, 0.34567] - }], - layout = { - yaxis: { showticklabels: true, hoverformat: ',.2r' }, - width: 600, - height: 400 - }; + expect(gd._hoverdata.length).toEqual(1); - beforeEach(function() { - this.gd = createGraphDiv(); - }); - - it('should display the correct format when ticklabels true', function() { - Plotly.plot(this.gd, data, layout); - mouseEvent('mousemove', 303, 213); + var hoverTrace = gd._hoverdata[0]; - var hovers = d3.selectAll('g.hovertext'); - - expect(hovers.size()).toEqual(1); - expect(hovers.select('text')[0][0].textContent).toEqual('0.23'); - }); + expect(hoverTrace.fullData.index).toEqual(1); + expect(hoverTrace.curveNumber).toEqual(1); + expect(hoverTrace.pointNumber).toEqual(16); + expect(hoverTrace.x).toEqual(0.33); + expect(hoverTrace.y).toEqual(1.25); - it('should display the correct format when ticklabels false', function() { - layout.yaxis.showticklabels = false; - Plotly.plot(this.gd, data, layout); - mouseEvent('mousemove', 303, 213); + expect(d3.selectAll("g.axistext").size()).toEqual(0); + expect(d3.selectAll("g.hovertext").size()).toEqual(1); - var hovers = d3.selectAll('g.hovertext'); + var text = d3.selectAll("g.hovertext").select("text"); + expect(text.size()).toEqual(1); + expect(text.html()).toEqual("PV learning ..."); - expect(hovers.size()).toEqual(1); - expect(hovers.select('text')[0][0].textContent).toEqual('0.23'); - }); + done(); + }); + }); + }); + + describe("hoverformat", function() { + var data = [{ x: [1, 2, 3], y: [0.12345, 0.23456, 0.34567] }], + layout = { + yaxis: { showticklabels: true, hoverformat: ",.2r" }, + width: 600, + height: 400 + }; + + beforeEach(function() { + this.gd = createGraphDiv(); }); - describe('textmode', function() { - - var data = [{ - x: [1, 2, 3, 4], - y: [2, 3, 4, 5], - mode: 'text', - hoverinfo: 'text', - text: ['test', null, 42, undefined] - }], - layout = { - width: 600, - height: 400 - }; - - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), data, layout).then(done); - }); - - it('should show text labels', function() { - mouseEvent('mousemove', 108, 303); - var hovers = d3.selectAll('g.hovertext'); - expect(hovers.size()).toEqual(1); - expect(hovers.select('text')[0][0].textContent).toEqual('test'); - }); - - it('should show number labels', function() { - mouseEvent('mousemove', 363, 173); - var hovers = d3.selectAll('g.hovertext'); - expect(hovers.size()).toEqual(1); - expect(hovers.select('text')[0][0].textContent).toEqual('42'); - }); + it("should display the correct format when ticklabels true", function() { + Plotly.plot(this.gd, data, layout); + mouseEvent("mousemove", 303, 213); - it('should not show null text labels', function() { - mouseEvent('mousemove', 229, 239); - var hovers = d3.selectAll('g.hovertext'); - expect(hovers.size()).toEqual(0); - }); + var hovers = d3.selectAll("g.hovertext"); - it('should not show undefined text labels', function() { - mouseEvent('mousemove', 493, 108); - var hovers = d3.selectAll('g.hovertext'); - expect(hovers.size()).toEqual(0); - }); + expect(hovers.size()).toEqual(1); + expect(hovers.select("text")[0][0].textContent).toEqual("0.23"); }); -}); -describe('hover info on stacked subplots', function() { - 'use strict'; + it("should display the correct format when ticklabels false", function() { + layout.yaxis.showticklabels = false; + Plotly.plot(this.gd, data, layout); + mouseEvent("mousemove", 303, 213); - afterEach(destroyGraphDiv); + var hovers = d3.selectAll("g.hovertext"); - describe('hover info on stacked subplots with shared x-axis', function() { - var mock = require('@mocks/stacked_coupled_subplots.json'); - - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); - }); + expect(hovers.size()).toEqual(1); + expect(hovers.select("text")[0][0].textContent).toEqual("0.23"); + }); + }); + + describe("textmode", function() { + var data = [ + { + x: [1, 2, 3, 4], + y: [2, 3, 4, 5], + mode: "text", + hoverinfo: "text", + text: ["test", null, 42, undefined] + } + ], + layout = { width: 600, height: 400 }; + + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), data, layout).then(done); + }); - it('responds to hover', function() { - var gd = document.getElementById('graph'); - Plotly.Fx.hover(gd, {xval: 3}, ['xy', 'xy2', 'xy3']); - - expect(gd._hoverdata.length).toEqual(2); - - expect(gd._hoverdata[0]).toEqual(jasmine.objectContaining( - { - curveNumber: 1, - pointNumber: 1, - x: 3, - y: 110 - })); - - expect(gd._hoverdata[1]).toEqual(jasmine.objectContaining( - { - curveNumber: 2, - pointNumber: 0, - x: 3, - y: 1000 - })); - - // There should be a single label on the x-axis with the shared x value, 3. - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual('3'); - - // There should be two points being hovered over, in two different traces, one in each plot. - expect(d3.selectAll('g.hovertext').size()).toEqual(2); - var textNodes = d3.selectAll('g.hovertext').selectAll('text'); - - expect(textNodes[0][0].innerHTML).toEqual('trace 1'); - expect(textNodes[0][1].innerHTML).toEqual('110'); - expect(textNodes[1][0].innerHTML).toEqual('trace 2'); - expect(textNodes[1][1].innerHTML).toEqual('1000'); - }); + it("should show text labels", function() { + mouseEvent("mousemove", 108, 303); + var hovers = d3.selectAll("g.hovertext"); + expect(hovers.size()).toEqual(1); + expect(hovers.select("text")[0][0].textContent).toEqual("test"); }); - describe('hover info on stacked subplots with shared y-axis', function() { - var mock = require('@mocks/stacked_subplots_shared_yaxis.json'); + it("should show number labels", function() { + mouseEvent("mousemove", 363, 173); + var hovers = d3.selectAll("g.hovertext"); + expect(hovers.size()).toEqual(1); + expect(hovers.select("text")[0][0].textContent).toEqual("42"); + }); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); - }); + it("should not show null text labels", function() { + mouseEvent("mousemove", 229, 239); + var hovers = d3.selectAll("g.hovertext"); + expect(hovers.size()).toEqual(0); + }); - it('responds to hover', function() { - var gd = document.getElementById('graph'); - Plotly.Fx.hover(gd, {yval: 0}, ['xy', 'x2y', 'x3y']); - - expect(gd._hoverdata.length).toEqual(3); - - expect(gd._hoverdata[0]).toEqual(jasmine.objectContaining( - { - curveNumber: 0, - pointNumber: 0, - x: 1, - y: 0 - })); - - expect(gd._hoverdata[1]).toEqual(jasmine.objectContaining( - { - curveNumber: 1, - pointNumber: 0, - x: 2.1, - y: 0 - })); - - expect(gd._hoverdata[2]).toEqual(jasmine.objectContaining( - { - curveNumber: 2, - pointNumber: 0, - x: 3, - y: 0 - })); - - // There should be a single label on the y-axis with the shared y value, 0. - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual('0'); - - // There should be three points being hovered over, in three different traces, one in each plot. - expect(d3.selectAll('g.hovertext').size()).toEqual(3); - var textNodes = d3.selectAll('g.hovertext').selectAll('text'); - - expect(textNodes[0][0].innerHTML).toEqual('trace 0'); - expect(textNodes[0][1].innerHTML).toEqual('1'); - expect(textNodes[1][0].innerHTML).toEqual('trace 1'); - expect(textNodes[1][1].innerHTML).toEqual('2.1'); - expect(textNodes[2][0].innerHTML).toEqual('trace 2'); - expect(textNodes[2][1].innerHTML).toEqual('3'); - }); + it("should not show undefined text labels", function() { + mouseEvent("mousemove", 493, 108); + var hovers = d3.selectAll("g.hovertext"); + expect(hovers.size()).toEqual(0); }); + }); }); +describe("hover info on stacked subplots", function() { + "use strict"; + afterEach(destroyGraphDiv); -describe('hover info on overlaid subplots', function() { - 'use strict'; + describe("hover info on stacked subplots with shared x-axis", function() { + var mock = require("@mocks/stacked_coupled_subplots.json"); - afterEach(destroyGraphDiv); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + }); - it('should respond to hover', function(done) { - var mock = require('@mocks/autorange-tozero-rangemode.json'); + it("responds to hover", function() { + var gd = document.getElementById("graph"); + Plotly.Fx.hover(gd, { xval: 3 }, ["xy", "xy2", "xy3"]); - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(function() { - mouseEvent('mousemove', 768, 345); + expect(gd._hoverdata.length).toEqual(2); - var axisText = d3.selectAll('g.axistext'), - hoverText = d3.selectAll('g.hovertext'); + expect(gd._hoverdata[0]).toEqual( + jasmine.objectContaining({ + curveNumber: 1, + pointNumber: 1, + x: 3, + y: 110 + }) + ); + + expect(gd._hoverdata[1]).toEqual( + jasmine.objectContaining({ + curveNumber: 2, + pointNumber: 0, + x: 3, + y: 1000 + }) + ); - expect(axisText.size()).toEqual(1, 'with 1 label on axis'); - expect(hoverText.size()).toEqual(2, 'with 2 labels on the overlaid pts'); + // There should be a single label on the x-axis with the shared x value, 3. + expect(d3.selectAll("g.axistext").size()).toEqual(1); + expect(d3.selectAll("g.axistext").select("text").html()).toEqual("3"); - expect(axisText.select('text').html()).toEqual('1', 'with correct axis label'); + // There should be two points being hovered over, in two different traces, one in each plot. + expect(d3.selectAll("g.hovertext").size()).toEqual(2); + var textNodes = d3.selectAll("g.hovertext").selectAll("text"); - var textNodes = hoverText.selectAll('text'); + expect(textNodes[0][0].innerHTML).toEqual("trace 1"); + expect(textNodes[0][1].innerHTML).toEqual("110"); + expect(textNodes[1][0].innerHTML).toEqual("trace 2"); + expect(textNodes[1][1].innerHTML).toEqual("1000"); + }); + }); - expect(textNodes[0][0].innerHTML).toEqual('Take Rate', 'with correct hover labels'); - expect(textNodes[0][1].innerHTML).toEqual('0.35', 'with correct hover labels'); - expect(textNodes[1][0].innerHTML).toEqual('Revenue', 'with correct hover labels'); - expect(textNodes[1][1].innerHTML).toEqual('2,352.5', 'with correct hover labels'); + describe("hover info on stacked subplots with shared y-axis", function() { + var mock = require("@mocks/stacked_subplots_shared_yaxis.json"); - }).then(done); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); }); -}); -describe('hover after resizing', function() { - 'use strict'; + it("responds to hover", function() { + var gd = document.getElementById("graph"); + Plotly.Fx.hover(gd, { yval: 0 }, ["xy", "x2y", "x3y"]); - afterEach(destroyGraphDiv); + expect(gd._hoverdata.length).toEqual(3); - function _click(pos) { - return new Promise(function(resolve) { - click(pos[0], pos[1]); - - setTimeout(function() { - resolve(); - }, constants.HOVERMINTIME); - }); - } + expect(gd._hoverdata[0]).toEqual( + jasmine.objectContaining({ curveNumber: 0, pointNumber: 0, x: 1, y: 0 }) + ); - function assertLabelCount(pos, cnt, msg) { - return new Promise(function(resolve) { - mouseEvent('mousemove', pos[0], pos[1]); + expect(gd._hoverdata[1]).toEqual( + jasmine.objectContaining({ + curveNumber: 1, + pointNumber: 0, + x: 2.1, + y: 0 + }) + ); + + expect(gd._hoverdata[2]).toEqual( + jasmine.objectContaining({ curveNumber: 2, pointNumber: 0, x: 3, y: 0 }) + ); + + // There should be a single label on the y-axis with the shared y value, 0. + expect(d3.selectAll("g.axistext").size()).toEqual(1); + expect(d3.selectAll("g.axistext").select("text").html()).toEqual("0"); + + // There should be three points being hovered over, in three different traces, one in each plot. + expect(d3.selectAll("g.hovertext").size()).toEqual(3); + var textNodes = d3.selectAll("g.hovertext").selectAll("text"); + + expect(textNodes[0][0].innerHTML).toEqual("trace 0"); + expect(textNodes[0][1].innerHTML).toEqual("1"); + expect(textNodes[1][0].innerHTML).toEqual("trace 1"); + expect(textNodes[1][1].innerHTML).toEqual("2.1"); + expect(textNodes[2][0].innerHTML).toEqual("trace 2"); + expect(textNodes[2][1].innerHTML).toEqual("3"); + }); + }); +}); - setTimeout(function() { - var hoverText = d3.selectAll('g.hovertext'); - expect(hoverText.size()).toEqual(cnt, msg); +describe("hover info on overlaid subplots", function() { + "use strict"; + afterEach(destroyGraphDiv); + + it("should respond to hover", function(done) { + var mock = require("@mocks/autorange-tozero-rangemode.json"); + + Plotly.plot(createGraphDiv(), mock.data, mock.layout) + .then(function() { + mouseEvent("mousemove", 768, 345); + + var axisText = d3.selectAll("g.axistext"), + hoverText = d3.selectAll("g.hovertext"); + + expect(axisText.size()).toEqual(1, "with 1 label on axis"); + expect(hoverText.size()).toEqual( + 2, + "with 2 labels on the overlaid pts" + ); + + expect(axisText.select("text").html()).toEqual( + "1", + "with correct axis label" + ); + + var textNodes = hoverText.selectAll("text"); + + expect(textNodes[0][0].innerHTML).toEqual( + "Take Rate", + "with correct hover labels" + ); + expect(textNodes[0][1].innerHTML).toEqual( + "0.35", + "with correct hover labels" + ); + expect(textNodes[1][0].innerHTML).toEqual( + "Revenue", + "with correct hover labels" + ); + expect(textNodes[1][1].innerHTML).toEqual( + "2,352.5", + "with correct hover labels" + ); + }) + .then(done); + }); +}); - resolve(); - }, constants.HOVERMINTIME); - }); - } +describe("hover after resizing", function() { + "use strict"; + afterEach(destroyGraphDiv); - it('should work', function(done) { - var data = [{ y: [2, 1, 2] }], - layout = { width: 600, height: 500 }, - gd = createGraphDiv(); + function _click(pos) { + return new Promise(function(resolve) { + click(pos[0], pos[1]); - var pos0 = [305, 403], - pos1 = [401, 122]; + setTimeout( + function() { + resolve(); + }, + constants.HOVERMINTIME + ); + }); + } - Plotly.plot(gd, data, layout).then(function() { + function assertLabelCount(pos, cnt, msg) { + return new Promise(function(resolve) { + mouseEvent("mousemove", pos[0], pos[1]); - // to test https://github.com/plotly/plotly.js/issues/1044 + setTimeout( + function() { + var hoverText = d3.selectAll("g.hovertext"); + expect(hoverText.size()).toEqual(cnt, msg); - return _click(pos0); - }) - .then(function() { - return assertLabelCount(pos0, 1, 'before resize, showing pt label'); - }) - .then(function() { - return assertLabelCount(pos1, 0, 'before resize, not showing blank spot'); - }) - .then(function() { - return Plotly.relayout(gd, 'width', 500); - }) - .then(function() { - return assertLabelCount(pos0, 0, 'after resize, not showing blank spot'); - }) - .then(function() { - return assertLabelCount(pos1, 1, 'after resize, showing pt label'); - }) - .then(function() { - return Plotly.relayout(gd, 'width', 600); - }) - .then(function() { - return assertLabelCount(pos0, 1, 'back to initial, showing pt label'); - }) - .then(function() { - return assertLabelCount(pos1, 0, 'back to initial, not showing blank spot'); - }) - .then(done); + resolve(); + }, + constants.HOVERMINTIME + ); }); + } + + it("should work", function(done) { + var data = [{ y: [2, 1, 2] }], + layout = { width: 600, height: 500 }, + gd = createGraphDiv(); + + var pos0 = [305, 403], pos1 = [401, 122]; + + Plotly.plot(gd, data, layout) + .then(function() { + // to test https://github.com/plotly/plotly.js/issues/1044 + return _click(pos0); + }) + .then(function() { + return assertLabelCount(pos0, 1, "before resize, showing pt label"); + }) + .then(function() { + return assertLabelCount( + pos1, + 0, + "before resize, not showing blank spot" + ); + }) + .then(function() { + return Plotly.relayout(gd, "width", 500); + }) + .then(function() { + return assertLabelCount( + pos0, + 0, + "after resize, not showing blank spot" + ); + }) + .then(function() { + return assertLabelCount(pos1, 1, "after resize, showing pt label"); + }) + .then(function() { + return Plotly.relayout(gd, "width", 600); + }) + .then(function() { + return assertLabelCount(pos0, 1, "back to initial, showing pt label"); + }) + .then(function() { + return assertLabelCount( + pos1, + 0, + "back to initial, not showing blank spot" + ); + }) + .then(done); + }); }); -describe('hover on fill', function() { - 'use strict'; - - afterEach(destroyGraphDiv); - - function assertLabelsCorrect(mousePos, labelPos, labelText) { - return new Promise(function(resolve) { - mouseEvent('mousemove', mousePos[0], mousePos[1]); - - setTimeout(function() { - var hoverText = d3.selectAll('g.hovertext'); - expect(hoverText.size()).toEqual(1); - expect(hoverText.text()).toEqual(labelText); - - var transformParts = hoverText.attr('transform').split('('); - expect(transformParts[0]).toEqual('translate'); - var transformCoords = transformParts[1].split(')')[0].split(','); - expect(+transformCoords[0]).toBeCloseTo(labelPos[0], -1, labelText + ':x'); - expect(+transformCoords[1]).toBeCloseTo(labelPos[1], -1, labelText + ':y'); - - resolve(); - }, constants.HOVERMINTIME); - }); - } - - it('should always show one label in the right place', function(done) { - var mock = Lib.extendDeep({}, require('@mocks/scatter_fill_self_next.json')); - mock.data.forEach(function(trace) { trace.hoveron = 'fills'; }); - - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(function() { - return assertLabelsCorrect([242, 142], [252, 133.8], 'trace 2'); - }).then(function() { - return assertLabelsCorrect([242, 292], [233, 210], 'trace 1'); - }).then(function() { - return assertLabelsCorrect([147, 252], [158.925, 248.1], 'trace 0'); - }).then(done); - }); - - it('should work for scatterternary too', function(done) { - var mock = Lib.extendDeep({}, require('@mocks/ternary_fill.json')); - var gd = createGraphDiv(); - - Plotly.plot(gd, mock.data, mock.layout).then(function() { - // hover over a point when that's closest, even if you're over - // a fill, because by default we have hoveron='points+fills' - return assertLabelsCorrect([237, 150], [240.0, 144], - 'trace 2Component A: 0.8Component B: 0.1Component C: 0.1'); - }).then(function() { - // the rest are hovers over fills - return assertLabelsCorrect([237, 170], [247.7, 166], 'trace 2'); - }).then(function() { - return assertLabelsCorrect([237, 218], [266.75, 265], 'trace 1'); - }).then(function() { - return assertLabelsCorrect([237, 240], [247.7, 254], 'trace 0'); - }).then(function() { - // zoom in to test clipping of large out-of-viewport shapes - return Plotly.relayout(gd, { - 'ternary.aaxis.min': 0.5, - 'ternary.baxis.min': 0.25 - }); - }).then(function() { - // this particular one has a hover label disconnected from the shape itself - // so if we ever fix this, the test will have to be fixed too. - return assertLabelsCorrect([295, 218], [275.1, 166], 'trace 2'); - }).then(function() { - // trigger an autoscale redraw, which goes through dragElement - return doubleClick(237, 251); - }).then(function() { - // then make sure we can still select a *different* item afterward - return assertLabelsCorrect([237, 218], [266.75, 265], 'trace 1'); - }).then(done); +describe("hover on fill", function() { + "use strict"; + afterEach(destroyGraphDiv); + + function assertLabelsCorrect(mousePos, labelPos, labelText) { + return new Promise(function(resolve) { + mouseEvent("mousemove", mousePos[0], mousePos[1]); + + setTimeout( + function() { + var hoverText = d3.selectAll("g.hovertext"); + expect(hoverText.size()).toEqual(1); + expect(hoverText.text()).toEqual(labelText); + + var transformParts = hoverText.attr("transform").split("("); + expect(transformParts[0]).toEqual("translate"); + var transformCoords = transformParts[1].split(")")[0].split(","); + expect(+transformCoords[0]).toBeCloseTo( + labelPos[0], + -1, + labelText + ":x" + ); + expect(+transformCoords[1]).toBeCloseTo( + labelPos[1], + -1, + labelText + ":y" + ); + + resolve(); + }, + constants.HOVERMINTIME + ); + }); + } + + it("should always show one label in the right place", function(done) { + var mock = Lib.extendDeep( + {}, + require("@mocks/scatter_fill_self_next.json") + ); + mock.data.forEach(function(trace) { + trace.hoveron = "fills"; }); + + Plotly.plot(createGraphDiv(), mock.data, mock.layout) + .then(function() { + return assertLabelsCorrect([242, 142], [252, 133.8], "trace 2"); + }) + .then(function() { + return assertLabelsCorrect([242, 292], [233, 210], "trace 1"); + }) + .then(function() { + return assertLabelsCorrect([147, 252], [158.925, 248.1], "trace 0"); + }) + .then(done); + }); + + it("should work for scatterternary too", function(done) { + var mock = Lib.extendDeep({}, require("@mocks/ternary_fill.json")); + var gd = createGraphDiv(); + + Plotly.plot(gd, mock.data, mock.layout) + .then(function() { + // hover over a point when that's closest, even if you're over + // a fill, because by default we have hoveron='points+fills' + return assertLabelsCorrect( + [237, 150], + [240.0, 144], + "trace 2Component A: 0.8Component B: 0.1Component C: 0.1" + ); + }) + .then(function() { + // the rest are hovers over fills + return assertLabelsCorrect([237, 170], [247.7, 166], "trace 2"); + }) + .then(function() { + return assertLabelsCorrect([237, 218], [266.75, 265], "trace 1"); + }) + .then(function() { + return assertLabelsCorrect([237, 240], [247.7, 254], "trace 0"); + }) + .then(function() { + // zoom in to test clipping of large out-of-viewport shapes + return Plotly.relayout(gd, { + "ternary.aaxis.min": 0.5, + "ternary.baxis.min": 0.25 + }); + }) + .then(function() { + // this particular one has a hover label disconnected from the shape itself + // so if we ever fix this, the test will have to be fixed too. + return assertLabelsCorrect([295, 218], [275.1, 166], "trace 2"); + }) + .then(function() { + // trigger an autoscale redraw, which goes through dragElement + return doubleClick(237, 251); + }) + .then(function() { + // then make sure we can still select a *different* item afterward + return assertLabelsCorrect([237, 218], [266.75, 265], "trace 1"); + }) + .then(done); + }); }); diff --git a/test/jasmine/tests/hover_pie_test.js b/test/jasmine/tests/hover_pie_test.js index 75610e36834..9d5fd8b2513 100644 --- a/test/jasmine/tests/hover_pie_test.js +++ b/test/jasmine/tests/hover_pie_test.js @@ -1,31 +1,29 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); -describe('pie hovering', function() { - var mock = require('@mocks/pie_simple.json'); +describe("pie hovering", function() { + var mock = require("@mocks/pie_simple.json"); - describe('event data', function() { - var mockCopy = Lib.extendDeep({}, mock), - width = mockCopy.layout.width, - height = mockCopy.layout.height, - gd; + describe("event data", function() { + var mockCopy = Lib.extendDeep({}, mock), + width = mockCopy.layout.width, + height = mockCopy.layout.height, + gd; - beforeEach(function(done) { - gd = createGraphDiv(); + beforeEach(function(done) { + gd = createGraphDiv(); - Plotly.plot(gd, mockCopy.data, mockCopy.layout) - .then(done); - }); - - afterEach(destroyGraphDiv); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - it('should contain the correct fields', function() { + afterEach(destroyGraphDiv); - /* + it("should contain the correct fields", function() { + /* * expected = [{ * v: 4, * label: '3', @@ -42,126 +40,141 @@ describe('pie hovering', function() { * cyFinal: 160 * }]; */ - var hoverData, - unhoverData; - - - gd.on('plotly_hover', function(data) { - hoverData = data; - }); - - gd.on('plotly_unhover', function(data) { - unhoverData = data; - }); - - mouseEvent('mouseover', width / 2 - 7, height / 2 - 7); - mouseEvent('mouseout', width / 2 - 7, height / 2 - 7); - - expect(hoverData.points.length).toEqual(1); - expect(unhoverData.points.length).toEqual(1); - - var fields = [ - 'v', 'label', 'color', 'i', 'hidden', - 'text', 'px1', 'pxmid', 'midangle', - 'px0', 'largeArc', 'cxFinal', 'cyFinal' - ]; - - expect(Object.keys(hoverData.points[0])).toEqual(fields); - expect(hoverData.points[0].i).toEqual(3); - - expect(Object.keys(unhoverData.points[0])).toEqual(fields); - expect(unhoverData.points[0].i).toEqual(3); - }); - - it('should fire hover event when moving from one slice to another', function(done) { - var count = 0, - hoverData = []; - - gd.on('plotly_hover', function(data) { - count++; - hoverData.push(data); - }); - - mouseEvent('mouseover', 173, 133); - setTimeout(function() { - mouseEvent('mouseover', 233, 193); - expect(count).toEqual(2); - expect(hoverData[0]).not.toEqual(hoverData[1]); - done(); - }, 100); - }); - - it('should fire unhover event when the mouse moves off the graph', function(done) { - var count = 0, - unhoverData = []; - - gd.on('plotly_unhover', function(data) { - count++; - unhoverData.push(data); - }); - - mouseEvent('mouseover', 173, 133); - mouseEvent('mouseout', 173, 133); - setTimeout(function() { - mouseEvent('mouseover', 233, 193); - mouseEvent('mouseout', 233, 193); - expect(count).toEqual(2); - expect(unhoverData[0]).not.toEqual(unhoverData[1]); - done(); - }, 100); - }); + var hoverData, unhoverData; + + gd.on("plotly_hover", function(data) { + hoverData = data; + }); + + gd.on("plotly_unhover", function(data) { + unhoverData = data; + }); + + mouseEvent("mouseover", width / 2 - 7, height / 2 - 7); + mouseEvent("mouseout", width / 2 - 7, height / 2 - 7); + + expect(hoverData.points.length).toEqual(1); + expect(unhoverData.points.length).toEqual(1); + + var fields = [ + "v", + "label", + "color", + "i", + "hidden", + "text", + "px1", + "pxmid", + "midangle", + "px0", + "largeArc", + "cxFinal", + "cyFinal" + ]; + + expect(Object.keys(hoverData.points[0])).toEqual(fields); + expect(hoverData.points[0].i).toEqual(3); + + expect(Object.keys(unhoverData.points[0])).toEqual(fields); + expect(unhoverData.points[0].i).toEqual(3); }); - describe('labels', function() { - - var gd, - mockCopy; + it( + "should fire hover event when moving from one slice to another", + function(done) { + var count = 0, hoverData = []; - beforeEach(function() { - gd = createGraphDiv(); - mockCopy = Lib.extendDeep({}, mock); + gd.on("plotly_hover", function(data) { + count++; + hoverData.push(data); }); - afterEach(destroyGraphDiv); - - it('should show the default selected values', function(done) { + mouseEvent("mouseover", 173, 133); + setTimeout( + function() { + mouseEvent("mouseover", 233, 193); + expect(count).toEqual(2); + expect(hoverData[0]).not.toEqual(hoverData[1]); + done(); + }, + 100 + ); + } + ); + + it("should fire unhover event when the mouse moves off the graph", function( + done + ) { + var count = 0, unhoverData = []; + + gd.on("plotly_unhover", function(data) { + count++; + unhoverData.push(data); + }); + + mouseEvent("mouseover", 173, 133); + mouseEvent("mouseout", 173, 133); + setTimeout( + function() { + mouseEvent("mouseover", 233, 193); + mouseEvent("mouseout", 233, 193); + expect(count).toEqual(2); + expect(unhoverData[0]).not.toEqual(unhoverData[1]); + done(); + }, + 100 + ); + }); + }); - var expected = ['4', '5', '33.3%']; + describe("labels", function() { + var gd, mockCopy; - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + beforeEach(function() { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + }); - mouseEvent('mouseover', 223, 143); + afterEach(destroyGraphDiv); - var labels = Plotly.d3.selectAll('.hovertext .nums .line'); + it("should show the default selected values", function(done) { + var expected = ["4", "5", "33.3%"]; - expect(labels[0].length).toBe(3); + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + mouseEvent("mouseover", 223, 143); - labels.each(function(_, i) { - expect(Plotly.d3.select(this).text()).toBe(expected[i]); - }); - }).then(done); - }); + var labels = Plotly.d3.selectAll(".hovertext .nums .line"); - it('should show the correct separators for values', function(done) { + expect(labels[0].length).toBe(3); - var expected = ['0', '12|345|678@91', '99@9%']; + labels.each(function(_, i) { + expect(Plotly.d3.select(this).text()).toBe(expected[i]); + }); + }) + .then(done); + }); - mockCopy.layout.separators = '@|'; - mockCopy.data[0].values[0] = 12345678.912; - mockCopy.data[0].values[1] = 10000; + it("should show the correct separators for values", function(done) { + var expected = ["0", "12|345|678@91", "99@9%"]; - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + mockCopy.layout.separators = "@|"; + mockCopy.data[0].values[0] = 12345678.912; + mockCopy.data[0].values[1] = 10000; - mouseEvent('mouseover', 223, 143); + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + mouseEvent("mouseover", 223, 143); - var labels = Plotly.d3.selectAll('.hovertext .nums .line'); + var labels = Plotly.d3.selectAll(".hovertext .nums .line"); - expect(labels[0].length).toBe(3); + expect(labels[0].length).toBe(3); - labels.each(function(_, i) { - expect(Plotly.d3.select(this).text()).toBe(expected[i]); - }); - }).then(done); - }); + labels.each(function(_, i) { + expect(Plotly.d3.select(this).text()).toBe(expected[i]); + }); + }) + .then(done); }); + }); }); diff --git a/test/jasmine/tests/is_array_test.js b/test/jasmine/tests/is_array_test.js index bea361516db..1b7bc6612ed 100644 --- a/test/jasmine/tests/is_array_test.js +++ b/test/jasmine/tests/is_array_test.js @@ -1,47 +1,51 @@ -var Lib = require('@src/lib'); +var Lib = require("@src/lib"); -describe('isArray', function() { - 'use strict'; +describe("isArray", function() { + "use strict"; + var isArray = Lib.isArray; - var isArray = Lib.isArray; + function A() {} - function A() {} + var shouldPass = [ + [], + new Array(10), + new Float32Array(1), + new Int32Array([1, 2, 3]) + ]; - var shouldPass = [ - [], - new Array(10), - new Float32Array(1), - new Int32Array([1, 2, 3]) - ]; + var shouldFail = [ + A, + new A(), + document, + window, + null, + undefined, + "string", + true, + false, + NaN, + Infinity, + /foo/, + "\n", + new Date(), + new RegExp("foo"), + new String("string") + ]; - var shouldFail = [ - A, - new A(), - document, - window, - null, - undefined, - 'string', - true, - false, - NaN, - Infinity, - /foo/, - '\n', - new Date(), - new RegExp('foo'), - new String('string') - ]; - - shouldPass.forEach(function(obj) { - it('treats ' + JSON.stringify(obj) + ' as an array', function() { - expect(isArray(obj)).toBe(true); - }); + shouldPass.forEach(function(obj) { + it("treats " + JSON.stringify(obj) + " as an array", function() { + expect(isArray(obj)).toBe(true); }); + }); - shouldFail.forEach(function(obj) { - it('treats ' + JSON.stringify(obj !== window ? obj : 'window') + ' as NOT an array', function() { - expect(isArray(obj)).toBe(false); - }); - }); + shouldFail.forEach(function(obj) { + it( + "treats " + + JSON.stringify(obj !== window ? obj : "window") + + " as NOT an array", + function() { + expect(isArray(obj)).toBe(false); + } + ); + }); }); diff --git a/test/jasmine/tests/is_plain_object_test.js b/test/jasmine/tests/is_plain_object_test.js index cf2ba311f25..5f504c0bd22 100644 --- a/test/jasmine/tests/is_plain_object_test.js +++ b/test/jasmine/tests/is_plain_object_test.js @@ -1,48 +1,49 @@ -var Lib = require('@src/lib'); +var Lib = require("@src/lib"); -describe('isPlainObject', function() { - 'use strict'; +describe("isPlainObject", function() { + "use strict"; + var isPlainObject = Lib.isPlainObject; - var isPlainObject = Lib.isPlainObject; + function A() {} - function A() {} + var shouldPass = [{}, { a: "A", B: "b" }]; - var shouldPass = [ - {}, - {a: 'A', 'B': 'b'} - ]; + var shouldFail = [ + A, + new A(), + document, + window, + null, + undefined, + [], + new Float32Array(1), + "string", + true, + false, + NaN, + Infinity, + /foo/, + "\n", + new Array(10), + new Date(), + new RegExp("foo"), + new String("string") + ]; - var shouldFail = [ - A, - new A(), - document, - window, - null, - undefined, - [], - new Float32Array(1), - 'string', - true, - false, - NaN, - Infinity, - /foo/, - '\n', - new Array(10), - new Date(), - new RegExp('foo'), - new String('string') - ]; - - shouldPass.forEach(function(obj) { - it('treats ' + JSON.stringify(obj) + ' as a plain object', function() { - expect(isPlainObject(obj)).toBe(true); - }); + shouldPass.forEach(function(obj) { + it("treats " + JSON.stringify(obj) + " as a plain object", function() { + expect(isPlainObject(obj)).toBe(true); }); + }); - shouldFail.forEach(function(obj) { - it('treats ' + JSON.stringify(obj !== window ? obj : 'window') + ' as NOT a plain object', function() { - expect(isPlainObject(obj)).toBe(false); - }); - }); + shouldFail.forEach(function(obj) { + it( + "treats " + + JSON.stringify(obj !== window ? obj : "window") + + " as NOT a plain object", + function() { + expect(isPlainObject(obj)).toBe(false); + } + ); + }); }); diff --git a/test/jasmine/tests/layout_images_test.js b/test/jasmine/tests/layout_images_test.js index 71d7c2c8692..24c80eb2770 100644 --- a/test/jasmine/tests/layout_images_test.js +++ b/test/jasmine/tests/layout_images_test.js @@ -1,382 +1,366 @@ -var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); -var Images = require('@src/components/images'); +var Plotly = require("@lib/index"); +var Plots = require("@src/plots/plots"); +var Images = require("@src/components/images"); -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); -var jsLogo = 'https://images.plot.ly/language-icons/api-home/js-logo.png'; -var pythonLogo = 'https://images.plot.ly/language-icons/api-home/python-logo.png'; +var jsLogo = "https://images.plot.ly/language-icons/api-home/js-logo.png"; +var pythonLogo = "https://images.plot.ly/language-icons/api-home/python-logo.png"; -describe('Layout images', function() { +describe("Layout images", function() { + describe("supplyLayoutDefaults", function() { + var layoutIn, layoutOut; - describe('supplyLayoutDefaults', function() { - - var layoutIn, - layoutOut; - - beforeEach(function() { - layoutIn = { images: [] }; - layoutOut = { _has: Plots._hasPlotType }; - }); - - it('should reject when there is no `source`', function() { - layoutIn.images[0] = { opacity: 0.5, sizex: 0.2, sizey: 0.2 }; - - Images.supplyLayoutDefaults(layoutIn, layoutOut); - - expect(layoutOut.images).toEqual([{ - visible: false, - _index: 0, - _input: layoutIn.images[0] - }]); - }); - - it('should reject when not an array', function() { - layoutIn.images = { - source: jsLogo, - opacity: 0.5, - sizex: 0.2, - sizey: 0.2 - }; - - Images.supplyLayoutDefaults(layoutIn, layoutOut); + beforeEach(function() { + layoutIn = { images: [] }; + layoutOut = { _has: Plots._hasPlotType }; + }); - expect(layoutOut.images).toEqual([]); - }); + it("should reject when there is no `source`", function() { + layoutIn.images[0] = { opacity: 0.5, sizex: 0.2, sizey: 0.2 }; - it('should coerce the correct defaults', function() { - var image = { source: jsLogo }; - - layoutIn.images[0] = image; - - var expected = { - source: jsLogo, - visible: true, - layer: 'above', - x: 0, - y: 0, - xanchor: 'left', - yanchor: 'top', - sizex: 0, - sizey: 0, - sizing: 'contain', - opacity: 1, - xref: 'paper', - yref: 'paper', - _input: image, - _index: 0 - }; - - Images.supplyLayoutDefaults(layoutIn, layoutOut); - - expect(layoutOut.images[0]).toEqual(expected); - }); + Images.supplyLayoutDefaults(layoutIn, layoutOut); + expect(layoutOut.images).toEqual([ + { visible: false, _index: 0, _input: layoutIn.images[0] } + ]); }); - describe('drawing', function() { + it("should reject when not an array", function() { + layoutIn.images = { + source: jsLogo, + opacity: 0.5, + sizex: 0.2, + sizey: 0.2 + }; - var gd, - data = [{ x: [1, 2, 3], y: [1, 2, 3] }]; + Images.supplyLayoutDefaults(layoutIn, layoutOut); - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); + expect(layoutOut.images).toEqual([]); + }); - it('should draw images on the right layers', function() { + it("should coerce the correct defaults", function() { + var image = { source: jsLogo }; + + layoutIn.images[0] = image; + + var expected = { + source: jsLogo, + visible: true, + layer: "above", + x: 0, + y: 0, + xanchor: "left", + yanchor: "top", + sizex: 0, + sizey: 0, + sizing: "contain", + opacity: 1, + xref: "paper", + yref: "paper", + _input: image, + _index: 0 + }; + + Images.supplyLayoutDefaults(layoutIn, layoutOut); + + expect(layoutOut.images[0]).toEqual(expected); + }); + }); - var layer; + describe("drawing", function() { + var gd, data = [{ x: [1, 2, 3], y: [1, 2, 3] }]; - Plotly.plot(gd, data, { images: [{ - source: 'imageabove', - layer: 'above' - }]}); + beforeEach(function() { + gd = createGraphDiv(); + }); - layer = gd._fullLayout._imageUpperLayer; - expect(layer.length).toBe(1); + afterEach(destroyGraphDiv); - destroyGraphDiv(); - gd = createGraphDiv(); - Plotly.plot(gd, data, { images: [{ - source: 'imagebelow', - layer: 'below' - }]}); + it("should draw images on the right layers", function() { + var layer; - layer = gd._fullLayout._imageLowerLayer; - expect(layer.length).toBe(1); + Plotly.plot(gd, data, { + images: [{ source: "imageabove", layer: "above" }] + }); - destroyGraphDiv(); - gd = createGraphDiv(); - Plotly.plot(gd, data, { images: [{ - source: 'imagesubplot', - layer: 'below', - xref: 'x', - yref: 'y' - }]}); + layer = gd._fullLayout._imageUpperLayer; + expect(layer.length).toBe(1); - layer = gd._fullLayout._imageSubplotLayer; - expect(layer.length).toBe(1); - }); + destroyGraphDiv(); + gd = createGraphDiv(); + Plotly.plot(gd, data, { + images: [{ source: "imagebelow", layer: "below" }] + }); - describe('with anchors and sizing', function() { + layer = gd._fullLayout._imageLowerLayer; + expect(layer.length).toBe(1); - function testAspectRatio(xAnchor, yAnchor, sizing, expected) { - var anchorName = xAnchor + yAnchor; - Plotly.plot(gd, data, { images: [{ - source: anchorName, - xanchor: xAnchor, - yanchor: yAnchor, - sizing: sizing - }]}); + destroyGraphDiv(); + gd = createGraphDiv(); + Plotly.plot(gd, data, { + images: [ + { source: "imagesubplot", layer: "below", xref: "x", yref: "y" } + ] + }); - var image = Plotly.d3.select('image'), - parValue = image.attr('preserveAspectRatio'); + layer = gd._fullLayout._imageSubplotLayer; + expect(layer.length).toBe(1); + }); - expect(parValue).toBe(expected); + describe("with anchors and sizing", function() { + function testAspectRatio(xAnchor, yAnchor, sizing, expected) { + var anchorName = xAnchor + yAnchor; + Plotly.plot(gd, data, { + images: [ + { + source: anchorName, + xanchor: xAnchor, + yanchor: yAnchor, + sizing: sizing } + ] + }); - it('should work for center middle', function() { - testAspectRatio('center', 'middle', undefined, 'xMidYMid'); - }); + var image = Plotly.d3.select("image"), + parValue = image.attr("preserveAspectRatio"); - it('should work for left top', function() { - testAspectRatio('left', 'top', undefined, 'xMinYMin'); - }); + expect(parValue).toBe(expected); + } - it('should work for right bottom', function() { - testAspectRatio('right', 'bottom', undefined, 'xMaxYMax'); - }); + it("should work for center middle", function() { + testAspectRatio("center", "middle", undefined, "xMidYMid"); + }); - it('should work for stretch sizing', function() { - testAspectRatio('middle', 'center', 'stretch', 'none'); - }); + it("should work for left top", function() { + testAspectRatio("left", "top", undefined, "xMinYMin"); + }); - it('should work for fill sizing', function() { - testAspectRatio('invalid', 'invalid', 'fill', 'xMinYMin slice'); - }); + it("should work for right bottom", function() { + testAspectRatio("right", "bottom", undefined, "xMaxYMax"); + }); - }); + it("should work for stretch sizing", function() { + testAspectRatio("middle", "center", "stretch", "none"); + }); + it("should work for fill sizing", function() { + testAspectRatio("invalid", "invalid", "fill", "xMinYMin slice"); + }); }); + }); - describe('when the plot is dragged', function() { - var gd, - data = [{ x: [1, 2, 3], y: [1, 2, 3] }]; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - it('should not move when referencing the paper', function(done) { - var image = { - source: jsLogo, - xref: 'paper', - yref: 'paper', - x: 0, - y: 0, - sizex: 0.1, - sizey: 0.1 - }; - - Plotly.plot(gd, data, { - images: [image], - dragmode: 'pan', - width: 600, - height: 400 - }).then(function() { - var img = Plotly.d3.select('image').node(), - oldPos = img.getBoundingClientRect(); - - mouseEvent('mousedown', 250, 200); - mouseEvent('mousemove', 300, 250); - - var newPos = img.getBoundingClientRect(); - - expect(newPos.left).toBe(oldPos.left); - expect(newPos.top).toBe(oldPos.top); - - mouseEvent('mouseup', 300, 250); - }).then(done); - }); - - it('should move when referencing axes', function(done) { - var image = { - source: jsLogo, - xref: 'x', - yref: 'y', - x: 2, - y: 2, - sizex: 1, - sizey: 1 - }; - - Plotly.plot(gd, data, { - images: [image], - dragmode: 'pan', - width: 600, - height: 400 - }).then(function() { - var img = Plotly.d3.select('image').node(), - oldPos = img.getBoundingClientRect(); - - mouseEvent('mousedown', 250, 200); - mouseEvent('mousemove', 300, 250); - - var newPos = img.getBoundingClientRect(); - - expect(newPos.left).toBe(oldPos.left + 50); - expect(newPos.top).toBe(oldPos.top + 50); - - mouseEvent('mouseup', 300, 250); - }).then(done); - }); + describe("when the plot is dragged", function() { + var gd, data = [{ x: [1, 2, 3], y: [1, 2, 3] }]; + beforeEach(function() { + gd = createGraphDiv(); }); - describe('when relayout', function() { - - var gd, - data = [{ x: [1, 2, 3], y: [1, 2, 3] }]; - - beforeEach(function(done) { - gd = createGraphDiv(); - Plotly.plot(gd, data, { - images: [{ - source: jsLogo, - x: 2, - y: 2, - sizex: 1, - sizey: 1 - }], - width: 500, - height: 400 - }).then(done); - }); - - afterEach(destroyGraphDiv); - - it('should update the image if changed', function(done) { - var img = Plotly.d3.select('image'), - url = img.attr('xlink:href'); - - Plotly.relayout(gd, 'images[0].source', pythonLogo).then(function() { - var newImg = Plotly.d3.select('image'), - newUrl = newImg.attr('xlink:href'); - expect(url).not.toBe(newUrl); - }).then(done); - }); - - it('should update the image position if changed', function(done) { - var update = { - 'images[0].x': 0, - 'images[0].y': 1 - }; - - var img = Plotly.d3.select('image'); - - expect([+img.attr('x'), +img.attr('y')]).toEqual([760, -120]); + afterEach(destroyGraphDiv); + + it("should not move when referencing the paper", function(done) { + var image = { + source: jsLogo, + xref: "paper", + yref: "paper", + x: 0, + y: 0, + sizex: 0.1, + sizey: 0.1 + }; + + Plotly.plot(gd, data, { + images: [image], + dragmode: "pan", + width: 600, + height: 400 + }) + .then(function() { + var img = Plotly.d3.select("image").node(), + oldPos = img.getBoundingClientRect(); + + mouseEvent("mousedown", 250, 200); + mouseEvent("mousemove", 300, 250); + + var newPos = img.getBoundingClientRect(); + + expect(newPos.left).toBe(oldPos.left); + expect(newPos.top).toBe(oldPos.top); + + mouseEvent("mouseup", 300, 250); + }) + .then(done); + }); - Plotly.relayout(gd, update).then(function() { - var newImg = Plotly.d3.select('image'); - expect([+newImg.attr('x'), +newImg.attr('y')]).toEqual([80, 100]); - }).then(done); - }); + it("should move when referencing axes", function(done) { + var image = { + source: jsLogo, + xref: "x", + yref: "y", + x: 2, + y: 2, + sizex: 1, + sizey: 1 + }; + + Plotly.plot(gd, data, { + images: [image], + dragmode: "pan", + width: 600, + height: 400 + }) + .then(function() { + var img = Plotly.d3.select("image").node(), + oldPos = img.getBoundingClientRect(); + + mouseEvent("mousedown", 250, 200); + mouseEvent("mousemove", 300, 250); + + var newPos = img.getBoundingClientRect(); + + expect(newPos.left).toBe(oldPos.left + 50); + expect(newPos.top).toBe(oldPos.top + 50); + + mouseEvent("mouseup", 300, 250); + }) + .then(done); + }); + }); + + describe("when relayout", function() { + var gd, data = [{ x: [1, 2, 3], y: [1, 2, 3] }]; + + beforeEach(function(done) { + gd = createGraphDiv(); + Plotly.plot(gd, data, { + images: [{ source: jsLogo, x: 2, y: 2, sizex: 1, sizey: 1 }], + width: 500, + height: 400 + }).then(done); + }); - it('should remove the image tag if an invalid source', function(done) { + afterEach(destroyGraphDiv); - var selection = Plotly.d3.select('image'); - expect(selection.size()).toBe(1); + it("should update the image if changed", function(done) { + var img = Plotly.d3.select("image"), url = img.attr("xlink:href"); - Plotly.relayout(gd, 'images[0].source', 'invalidUrl').then(function() { - var newSelection = Plotly.d3.select('image'); - expect(newSelection.size()).toBe(0); - }).then(done); - }); + Plotly.relayout(gd, "images[0].source", pythonLogo) + .then(function() { + var newImg = Plotly.d3.select("image"), + newUrl = newImg.attr("xlink:href"); + expect(url).not.toBe(newUrl); + }) + .then(done); }); - describe('when adding/removing images', function() { + it("should update the image position if changed", function(done) { + var update = { "images[0].x": 0, "images[0].y": 1 }; - afterEach(destroyGraphDiv); + var img = Plotly.d3.select("image"); - it('should properly add and removing image', function(done) { - var gd = createGraphDiv(), - data = [{ x: [1, 2, 3], y: [1, 2, 3] }], - layout = { width: 500, height: 400 }; + expect([+img.attr("x"), +img.attr("y")]).toEqual([760, -120]); - function makeImage(source, x, y) { - return { - source: source, - x: x, - y: y, - sizex: 1, - sizey: 1 - }; - } + Plotly.relayout(gd, update) + .then(function() { + var newImg = Plotly.d3.select("image"); + expect([+newImg.attr("x"), +newImg.attr("y")]).toEqual([80, 100]); + }) + .then(done); + }); - function assertImages(cnt) { - expect(d3.selectAll('image').size()).toEqual(cnt); - } + it("should remove the image tag if an invalid source", function(done) { + var selection = Plotly.d3.select("image"); + expect(selection.size()).toBe(1); - Plotly.plot(gd, data, layout).then(function() { - assertImages(0); - - return Plotly.relayout(gd, 'images[0]', makeImage(jsLogo, 0.1, 0.1)); - }) - .then(function() { - assertImages(1); - - return Plotly.relayout(gd, 'images[1]', makeImage(pythonLogo, 0.9, 0.9)); - }) - .then(function() { - assertImages(2); - - return Plotly.relayout(gd, 'images[2]', makeImage(pythonLogo, 0.2, 0.5)); - }) - .then(function() { - assertImages(3); - expect(gd.layout.images.length).toEqual(3); - - return Plotly.relayout(gd, 'images[1].visible', false); - }) - .then(function() { - assertImages(2); - expect(gd.layout.images.length).toEqual(3); - - return Plotly.relayout(gd, 'images[1].visible', true); - }) - .then(function() { - assertImages(3); - expect(gd.layout.images.length).toEqual(3); - - return Plotly.relayout(gd, 'images[2]', null); - }) - .then(function() { - assertImages(2); - expect(gd.layout.images.length).toEqual(2); - - return Plotly.relayout(gd, 'images[1]', null); - }) - .then(function() { - assertImages(1); - expect(gd.layout.images.length).toEqual(1); - - return Plotly.relayout(gd, 'images[0]', null); - }) - .then(function() { - assertImages(0); - expect(gd.layout.images).toEqual([]); - - done(); - }); + Plotly.relayout(gd, "images[0].source", "invalidUrl") + .then(function() { + var newSelection = Plotly.d3.select("image"); + expect(newSelection.size()).toBe(0); + }) + .then(done); + }); + }); + + describe("when adding/removing images", function() { + afterEach(destroyGraphDiv); + + it("should properly add and removing image", function(done) { + var gd = createGraphDiv(), + data = [{ x: [1, 2, 3], y: [1, 2, 3] }], + layout = { width: 500, height: 400 }; + + function makeImage(source, x, y) { + return { source: source, x: x, y: y, sizex: 1, sizey: 1 }; + } + + function assertImages(cnt) { + expect(d3.selectAll("image").size()).toEqual(cnt); + } + + Plotly.plot(gd, data, layout) + .then(function() { + assertImages(0); + + return Plotly.relayout(gd, "images[0]", makeImage(jsLogo, 0.1, 0.1)); + }) + .then(function() { + assertImages(1); + + return Plotly.relayout( + gd, + "images[1]", + makeImage(pythonLogo, 0.9, 0.9) + ); + }) + .then(function() { + assertImages(2); + + return Plotly.relayout( + gd, + "images[2]", + makeImage(pythonLogo, 0.2, 0.5) + ); + }) + .then(function() { + assertImages(3); + expect(gd.layout.images.length).toEqual(3); + + return Plotly.relayout(gd, "images[1].visible", false); + }) + .then(function() { + assertImages(2); + expect(gd.layout.images.length).toEqual(3); + + return Plotly.relayout(gd, "images[1].visible", true); + }) + .then(function() { + assertImages(3); + expect(gd.layout.images.length).toEqual(3); + + return Plotly.relayout(gd, "images[2]", null); + }) + .then(function() { + assertImages(2); + expect(gd.layout.images.length).toEqual(2); + + return Plotly.relayout(gd, "images[1]", null); + }) + .then(function() { + assertImages(1); + expect(gd.layout.images.length).toEqual(1); + + return Plotly.relayout(gd, "images[0]", null); + }) + .then(function() { + assertImages(0); + expect(gd.layout.images).toEqual([]); + + done(); }); - }); - + }); }); diff --git a/test/jasmine/tests/legend_scroll_test.js b/test/jasmine/tests/legend_scroll_test.js index b93610ac5f7..1dcb7d0b528 100644 --- a/test/jasmine/tests/legend_scroll_test.js +++ b/test/jasmine/tests/legend_scroll_test.js @@ -1,258 +1,271 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); -var constants = require('@src/components/legend/constants'); - -var d3 = require('d3'); -var createGraph = require('../assets/create_graph_div'); -var destroyGraph = require('../assets/destroy_graph_div'); -var getBBox = require('../assets/get_bbox'); -var mock = require('../../image/mocks/legend_scroll.json'); - - -describe('The legend', function() { - 'use strict'; - - function countLegendGroups(gd) { - return gd._fullLayout._toppaper.selectAll('g.legend').size(); - } - - function countLegendClipPaths(gd) { - var uid = gd._fullLayout._uid; - - return gd._fullLayout._topdefs.selectAll('#legend' + uid).size(); - } - - function getPlotHeight(gd) { - return gd._fullLayout.height - gd._fullLayout.margin.t - gd._fullLayout.margin.b; - } - - function getLegendHeight(gd) { - var bg = d3.select('g.legend').select('.bg').node(); - return gd._fullLayout.legend.borderwidth + getBBox(bg).height; - } - - function getLegend() { - return d3.select('g.legend').node(); - } - - function getScrollBox() { - return d3.select('g.legend').select('.scrollbox').node(); - } - - function getScrollBar() { - return d3.select('g.legend').select('.scrollbar').node(); - } - - function getToggle() { - return d3.select('g.legend').select('.legendtoggle').node(); - } - - describe('when plotted with many traces', function() { - var gd; - - beforeEach(function(done) { - gd = createGraph(); - - var mockCopy = Lib.extendDeep({}, mock); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - done(); - }); - }); - - afterEach(destroyGraph); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); +var constants = require("@src/components/legend/constants"); + +var d3 = require("d3"); +var createGraph = require("../assets/create_graph_div"); +var destroyGraph = require("../assets/destroy_graph_div"); +var getBBox = require("../assets/get_bbox"); +var mock = require("../../image/mocks/legend_scroll.json"); + +describe("The legend", function() { + "use strict"; + function countLegendGroups(gd) { + return gd._fullLayout._toppaper.selectAll("g.legend").size(); + } + + function countLegendClipPaths(gd) { + var uid = gd._fullLayout._uid; + + return gd._fullLayout._topdefs.selectAll("#legend" + uid).size(); + } + + function getPlotHeight(gd) { + return gd._fullLayout.height - + gd._fullLayout.margin.t - + gd._fullLayout.margin.b; + } + + function getLegendHeight(gd) { + var bg = d3.select("g.legend").select(".bg").node(); + return gd._fullLayout.legend.borderwidth + getBBox(bg).height; + } + + function getLegend() { + return d3.select("g.legend").node(); + } + + function getScrollBox() { + return d3.select("g.legend").select(".scrollbox").node(); + } + + function getScrollBar() { + return d3.select("g.legend").select(".scrollbar").node(); + } + + function getToggle() { + return d3.select("g.legend").select(".legendtoggle").node(); + } + + describe("when plotted with many traces", function() { + var gd; + + beforeEach(function(done) { + gd = createGraph(); + + var mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + done(); + }); + }); - it('should not exceed plot height', function() { - var legendHeight = getLegendHeight(gd); + afterEach(destroyGraph); - expect(+legendHeight).toBe(getPlotHeight(gd)); - }); + it("should not exceed plot height", function() { + var legendHeight = getLegendHeight(gd); - it('should insert a scrollbar', function() { - var scrollBar = getScrollBar(); - - expect(scrollBar).toBeDefined(); - expect(scrollBar.getAttribute('x')).not.toBe(null); - }); - - it('should scroll when there\'s a wheel event', function() { - var legend = getLegend(), - scrollBox = getScrollBox(), - legendHeight = getLegendHeight(gd), - scrollBoxYMax = gd._fullLayout.legend.height - legendHeight, - scrollBarYMax = legendHeight - - constants.scrollBarHeight - - 2 * constants.scrollBarMargin, - initialDataScroll = scrollBox.getAttribute('data-scroll'), - wheelDeltaY = 100, - finalDataScroll = '' + Lib.constrain(initialDataScroll - - wheelDeltaY / scrollBarYMax * scrollBoxYMax, - -scrollBoxYMax, 0); - - legend.dispatchEvent(scrollTo(wheelDeltaY)); - - expect(scrollBox.getAttribute('data-scroll')).toBe(finalDataScroll); - expect(scrollBox.getAttribute('transform')).toBe( - 'translate(0, ' + finalDataScroll + ')'); - }); + expect(+legendHeight).toBe(getPlotHeight(gd)); + }); - it('should keep the scrollbar position after a toggle event', function() { - var legend = getLegend(), - scrollBox = getScrollBox(), - toggle = getToggle(), - wheelDeltaY = 100; + it("should insert a scrollbar", function() { + var scrollBar = getScrollBar(); - legend.dispatchEvent(scrollTo(wheelDeltaY)); + expect(scrollBar).toBeDefined(); + expect(scrollBar.getAttribute("x")).not.toBe(null); + }); - var dataScroll = scrollBox.getAttribute('data-scroll'); - toggle.dispatchEvent(new MouseEvent('click')); - expect(+toggle.parentNode.style.opacity).toBeLessThan(1); - expect(scrollBox.getAttribute('data-scroll')).toBe(dataScroll); - expect(scrollBox.getAttribute('transform')).toBe( - 'translate(0, ' + dataScroll + ')'); - }); + it("should scroll when there's a wheel event", function() { + var legend = getLegend(), + scrollBox = getScrollBox(), + legendHeight = getLegendHeight(gd), + scrollBoxYMax = gd._fullLayout.legend.height - legendHeight, + scrollBarYMax = legendHeight - + constants.scrollBarHeight - + 2 * constants.scrollBarMargin, + initialDataScroll = scrollBox.getAttribute("data-scroll"), + wheelDeltaY = 100, + finalDataScroll = "" + + Lib.constrain( + initialDataScroll - wheelDeltaY / scrollBarYMax * scrollBoxYMax, + -scrollBoxYMax, + 0 + ); + + legend.dispatchEvent(scrollTo(wheelDeltaY)); + + expect(scrollBox.getAttribute("data-scroll")).toBe(finalDataScroll); + expect(scrollBox.getAttribute("transform")).toBe( + "translate(0, " + finalDataScroll + ")" + ); + }); - it('should be restored and functional after relayout', function() { - var wheelDeltaY = 100, - legend = getLegend(), - scrollBox, - scrollBar, - scrollBarX, - scrollBarY, - toggle; - - legend.dispatchEvent(scrollTo(wheelDeltaY)); - scrollBar = legend.getElementsByClassName('scrollbar')[0]; - scrollBarX = scrollBar.getAttribute('x'), - scrollBarY = scrollBar.getAttribute('y'); - - Plotly.relayout(gd, 'showlegend', false); - Plotly.relayout(gd, 'showlegend', true); - - legend = getLegend(); - scrollBox = getScrollBox(); - scrollBar = getScrollBar(); - toggle = getToggle(); - - legend.dispatchEvent(scrollTo(wheelDeltaY)); - expect(scrollBar.getAttribute('x')).toBe(scrollBarX); - expect(scrollBar.getAttribute('y')).toBe(scrollBarY); - - var dataScroll = scrollBox.getAttribute('data-scroll'); - toggle.dispatchEvent(new MouseEvent('click')); - expect(+toggle.parentNode.style.opacity).toBeLessThan(1); - expect(scrollBox.getAttribute('data-scroll')).toBe(dataScroll); - expect(scrollBox.getAttribute('transform')).toBe( - 'translate(0, ' + dataScroll + ')'); - expect(scrollBar.getAttribute('width')).toBeGreaterThan(0); - expect(scrollBar.getAttribute('height')).toBeGreaterThan(0); - }); + it("should keep the scrollbar position after a toggle event", function() { + var legend = getLegend(), + scrollBox = getScrollBox(), + toggle = getToggle(), + wheelDeltaY = 100; + + legend.dispatchEvent(scrollTo(wheelDeltaY)); + + var dataScroll = scrollBox.getAttribute("data-scroll"); + toggle.dispatchEvent(new MouseEvent("click")); + expect(+toggle.parentNode.style.opacity).toBeLessThan(1); + expect(scrollBox.getAttribute("data-scroll")).toBe(dataScroll); + expect(scrollBox.getAttribute("transform")).toBe( + "translate(0, " + dataScroll + ")" + ); + }); - it('should constrain scrolling to the contents', function() { - var legend = getLegend(), - scrollBox = getScrollBox(); + it("should be restored and functional after relayout", function() { + var wheelDeltaY = 100, + legend = getLegend(), + scrollBox, + scrollBar, + scrollBarX, + scrollBarY, + toggle; + + legend.dispatchEvent(scrollTo(wheelDeltaY)); + scrollBar = legend.getElementsByClassName("scrollbar")[0]; + scrollBarX = scrollBar.getAttribute( + "x" + ), scrollBarY = scrollBar.getAttribute("y"); + + Plotly.relayout(gd, "showlegend", false); + Plotly.relayout(gd, "showlegend", true); + + legend = getLegend(); + scrollBox = getScrollBox(); + scrollBar = getScrollBar(); + toggle = getToggle(); + + legend.dispatchEvent(scrollTo(wheelDeltaY)); + expect(scrollBar.getAttribute("x")).toBe(scrollBarX); + expect(scrollBar.getAttribute("y")).toBe(scrollBarY); + + var dataScroll = scrollBox.getAttribute("data-scroll"); + toggle.dispatchEvent(new MouseEvent("click")); + expect(+toggle.parentNode.style.opacity).toBeLessThan(1); + expect(scrollBox.getAttribute("data-scroll")).toBe(dataScroll); + expect(scrollBox.getAttribute("transform")).toBe( + "translate(0, " + dataScroll + ")" + ); + expect(scrollBar.getAttribute("width")).toBeGreaterThan(0); + expect(scrollBar.getAttribute("height")).toBeGreaterThan(0); + }); - legend.dispatchEvent(scrollTo(-100)); - expect(scrollBox.getAttribute('transform')).toBe('translate(0, 0)'); + it("should constrain scrolling to the contents", function() { + var legend = getLegend(), scrollBox = getScrollBox(); - legend.dispatchEvent(scrollTo(100000)); - expect(scrollBox.getAttribute('transform')).toBe('translate(0, -179)'); - }); + legend.dispatchEvent(scrollTo(-100)); + expect(scrollBox.getAttribute("transform")).toBe("translate(0, 0)"); - it('should scale the scrollbar movement from top to bottom', function() { - var legend = getLegend(), - scrollBar = getScrollBar(), - legendHeight = getLegendHeight(gd); + legend.dispatchEvent(scrollTo(100000)); + expect(scrollBox.getAttribute("transform")).toBe("translate(0, -179)"); + }); - // The scrollbar is 20px tall and has 4px margins + it("should scale the scrollbar movement from top to bottom", function() { + var legend = getLegend(), + scrollBar = getScrollBar(), + legendHeight = getLegendHeight(gd); - legend.dispatchEvent(scrollTo(-1000)); - expect(+scrollBar.getAttribute('y')).toBe(4); + // The scrollbar is 20px tall and has 4px margins + legend.dispatchEvent(scrollTo(-1000)); + expect(+scrollBar.getAttribute("y")).toBe(4); - legend.dispatchEvent(scrollTo(10000)); - expect(+scrollBar.getAttribute('y')).toBe(legendHeight - 4 - 20); - }); + legend.dispatchEvent(scrollTo(10000)); + expect(+scrollBar.getAttribute("y")).toBe(legendHeight - 4 - 20); + }); - it('should be removed from DOM when \'showlegend\' is relayout\'ed to false', function(done) { - expect(countLegendGroups(gd)).toBe(1); - expect(countLegendClipPaths(gd)).toBe(1); + it( + "should be removed from DOM when 'showlegend' is relayout'ed to false", + function(done) { + expect(countLegendGroups(gd)).toBe(1); + expect(countLegendClipPaths(gd)).toBe(1); - Plotly.relayout(gd, 'showlegend', false).then(function() { - expect(countLegendGroups(gd)).toBe(0); - expect(countLegendClipPaths(gd)).toBe(0); + Plotly.relayout(gd, "showlegend", false).then(function() { + expect(countLegendGroups(gd)).toBe(0); + expect(countLegendClipPaths(gd)).toBe(0); - done(); - }); + done(); }); + } + ); - it('should resize when relayout\'ed with new height', function(done) { - var origLegendHeight = getLegendHeight(gd); + it("should resize when relayout'ed with new height", function(done) { + var origLegendHeight = getLegendHeight(gd); - Plotly.relayout(gd, 'height', gd._fullLayout.height / 2).then(function() { - var legendHeight = getLegendHeight(gd); + Plotly.relayout(gd, "height", gd._fullLayout.height / 2).then(function() { + var legendHeight = getLegendHeight(gd); - // legend still exists and not duplicated - expect(countLegendGroups(gd)).toBe(1); - expect(countLegendClipPaths(gd)).toBe(1); + // legend still exists and not duplicated + expect(countLegendGroups(gd)).toBe(1); + expect(countLegendClipPaths(gd)).toBe(1); - // clippath resized to new height less than new plot height - expect(+legendHeight).toBe(getPlotHeight(gd)); - expect(+legendHeight).toBeLessThan(+origLegendHeight); + // clippath resized to new height less than new plot height + expect(+legendHeight).toBe(getPlotHeight(gd)); + expect(+legendHeight).toBeLessThan(+origLegendHeight); - done(); - }); - }); + done(); + }); }); + }); - describe('when plotted with few traces', function() { - var gd; + describe("when plotted with few traces", function() { + var gd; - beforeEach(function() { - gd = createGraph(); + beforeEach(function() { + gd = createGraph(); - var data = [{ x: [1, 2, 3], y: [2, 3, 4], name: 'Test' }]; - var layout = { showlegend: true }; + var data = [{ x: [1, 2, 3], y: [2, 3, 4], name: "Test" }]; + var layout = { showlegend: true }; - Plotly.plot(gd, data, layout); - }); + Plotly.plot(gd, data, layout); + }); - afterEach(destroyGraph); + afterEach(destroyGraph); - it('should not display the scrollbar', function() { - var scrollBar = document.getElementsByClassName('scrollbar')[0]; + it("should not display the scrollbar", function() { + var scrollBar = document.getElementsByClassName("scrollbar")[0]; - expect(+scrollBar.getAttribute('width')).toBe(0); - expect(+scrollBar.getAttribute('height')).toBe(0); - }); + expect(+scrollBar.getAttribute("width")).toBe(0); + expect(+scrollBar.getAttribute("height")).toBe(0); + }); - it('should be removed from DOM when \'showlegend\' is relayout\'ed to false', function(done) { - expect(countLegendGroups(gd)).toBe(1); - expect(countLegendClipPaths(gd)).toBe(1); + it( + "should be removed from DOM when 'showlegend' is relayout'ed to false", + function(done) { + expect(countLegendGroups(gd)).toBe(1); + expect(countLegendClipPaths(gd)).toBe(1); - Plotly.relayout(gd, 'showlegend', false).then(function() { - expect(countLegendGroups(gd)).toBe(0); - expect(countLegendClipPaths(gd)).toBe(0); + Plotly.relayout(gd, "showlegend", false).then(function() { + expect(countLegendGroups(gd)).toBe(0); + expect(countLegendClipPaths(gd)).toBe(0); - done(); - }); + done(); }); + } + ); - it('should resize when traces added', function(done) { - var origLegendHeight = getLegendHeight(gd); - - Plotly.addTraces(gd, { x: [1, 2, 3], y: [4, 3, 2], name: 'Test2' }).then(function() { - var legendHeight = getLegendHeight(gd); + it("should resize when traces added", function(done) { + var origLegendHeight = getLegendHeight(gd); - expect(+legendHeight).toBeCloseTo(+origLegendHeight + 19, 0); + Plotly.addTraces(gd, { + x: [1, 2, 3], + y: [4, 3, 2], + name: "Test2" + }).then(function() { + var legendHeight = getLegendHeight(gd); - done(); - }); + expect(+legendHeight).toBeCloseTo(+origLegendHeight + 19, 0); - }); + done(); + }); }); + }); }); - function scrollTo(delta) { - return new WheelEvent('wheel', { deltaY: delta }); + return new WheelEvent("wheel", { deltaY: delta }); } diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index c832da02dcd..8566a923379 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -1,630 +1,739 @@ -var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); - -var Legend = require('@src/components/legend'); -var getLegendData = require('@src/components/legend/get_legend_data'); -var helpers = require('@src/components/legend/helpers'); -var anchorUtils = require('@src/components/legend/anchor_utils'); - -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var customMatchers = require('../assets/custom_matchers'); - - -describe('legend defaults', function() { - 'use strict'; - - var supplyLayoutDefaults = Legend.supplyLayoutDefaults; - - var layoutIn, layoutOut, fullData; +var Plotly = require("@lib/index"); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); + +var Legend = require("@src/components/legend"); +var getLegendData = require("@src/components/legend/get_legend_data"); +var helpers = require("@src/components/legend/helpers"); +var anchorUtils = require("@src/components/legend/anchor_utils"); + +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var customMatchers = require("../assets/custom_matchers"); + +describe("legend defaults", function() { + "use strict"; + var supplyLayoutDefaults = Legend.supplyLayoutDefaults; + + var layoutIn, layoutOut, fullData; + + beforeEach(function() { + layoutIn = { showlegend: true }; + layoutOut = { + font: Plots.layoutAttributes.font, + bg_color: Plots.layoutAttributes.bg_color + }; + }); + + it("should default traceorder to reversed for stack bar charts", function() { + fullData = [{ type: "bar" }, { type: "bar" }, { type: "scatter" }]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.legend.traceorder).toEqual("normal"); + + layoutOut.barmode = "stack"; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.legend.traceorder).toEqual("reversed"); + }); + + it( + "should default traceorder to reversed for filled tonext scatter charts", + function() { + fullData = [{ type: "scatter" }, { type: "scatter", fill: "tonexty" }]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.legend.traceorder).toEqual("reversed"); + } + ); + + it( + "should default traceorder to grouped when a group is present", + function() { + fullData = [ + { type: "scatter", legendgroup: "group" }, + { type: "scatter" } + ]; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.legend.traceorder).toEqual("grouped"); + + fullData[1].fill = "tonextx"; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.legend.traceorder).toEqual("grouped+reversed"); + } + ); + + it("should default orientation to vertical", function() { + supplyLayoutDefaults(layoutIn, layoutOut, []); + expect(layoutOut.legend.orientation).toEqual("v"); + }); + + describe("for horizontal legends", function() { + var layoutInForHorizontalLegends; beforeEach(function() { - layoutIn = { - showlegend: true - }; - layoutOut = { - font: Plots.layoutAttributes.font, - bg_color: Plots.layoutAttributes.bg_color - }; - }); - - it('should default traceorder to reversed for stack bar charts', function() { - fullData = [ - { type: 'bar' }, - { type: 'bar' }, - { type: 'scatter' } - ]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.legend.traceorder).toEqual('normal'); - - layoutOut.barmode = 'stack'; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.legend.traceorder).toEqual('reversed'); - }); - - it('should default traceorder to reversed for filled tonext scatter charts', function() { - fullData = [ - { type: 'scatter' }, - { type: 'scatter', fill: 'tonexty' } - ]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.legend.traceorder).toEqual('reversed'); - }); - - it('should default traceorder to grouped when a group is present', function() { - fullData = [ - { type: 'scatter', legendgroup: 'group' }, - { type: 'scatter'} - ]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.legend.traceorder).toEqual('grouped'); - - fullData[1].fill = 'tonextx'; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.legend.traceorder).toEqual('grouped+reversed'); + layoutInForHorizontalLegends = Lib.extendDeep( + { + legend: { orientation: "h" }, + xaxis: { rangeslider: { visible: false } } + }, + layoutIn + ); }); - it('should default orientation to vertical', function() { - supplyLayoutDefaults(layoutIn, layoutOut, []); - expect(layoutOut.legend.orientation).toEqual('v'); + it("should default position to bottom left", function() { + supplyLayoutDefaults(layoutInForHorizontalLegends, layoutOut, []); + expect(layoutOut.legend.x).toEqual(0); + expect(layoutOut.legend.xanchor).toEqual("left"); + expect(layoutOut.legend.y).toEqual(-0.1); + expect(layoutOut.legend.yanchor).toEqual("top"); }); - describe('for horizontal legends', function() { - var layoutInForHorizontalLegends; - - beforeEach(function() { - layoutInForHorizontalLegends = Lib.extendDeep({ - legend: { - orientation: 'h' - }, - xaxis: { - rangeslider: { - visible: false - } - } - }, layoutIn); - }); - - it('should default position to bottom left', function() { - supplyLayoutDefaults(layoutInForHorizontalLegends, layoutOut, []); - expect(layoutOut.legend.x).toEqual(0); - expect(layoutOut.legend.xanchor).toEqual('left'); - expect(layoutOut.legend.y).toEqual(-0.1); - expect(layoutOut.legend.yanchor).toEqual('top'); - }); - - it('should default position to top left if a range slider present', function() { - var mockLayoutIn = Lib.extendDeep({}, layoutInForHorizontalLegends); - mockLayoutIn.xaxis.rangeslider.visible = true; - - supplyLayoutDefaults(mockLayoutIn, layoutOut, []); - expect(layoutOut.legend.x).toEqual(0); - expect(layoutOut.legend.xanchor).toEqual('left'); - expect(layoutOut.legend.y).toEqual(1.1); - expect(layoutOut.legend.yanchor).toEqual('bottom'); - }); - }); + it( + "should default position to top left if a range slider present", + function() { + var mockLayoutIn = Lib.extendDeep({}, layoutInForHorizontalLegends); + mockLayoutIn.xaxis.rangeslider.visible = true; + + supplyLayoutDefaults(mockLayoutIn, layoutOut, []); + expect(layoutOut.legend.x).toEqual(0); + expect(layoutOut.legend.xanchor).toEqual("left"); + expect(layoutOut.legend.y).toEqual(1.1); + expect(layoutOut.legend.yanchor).toEqual("bottom"); + } + ); + }); }); -describe('legend getLegendData', function() { - 'use strict'; - - var calcdata, opts, legendData, expected; - - it('should group legendgroup traces', function() { - calcdata = [ - [{trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true - - }}], - [{trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - }}], - [{trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true - }}] - ]; - opts = { - traceorder: 'grouped' - }; - - legendData = getLegendData(calcdata, opts); - - expected = [ - [ - [{trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true - - }}], - [{trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true - }}] - ], - [ - [{trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - }}] - ] - ]; - - expect(legendData).toEqual(expected); - expect(opts._lgroupsLength).toEqual(2); - }); - - it('should collapse when data has only one group', function() { - calcdata = [ - [{trace: { - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true - - }}], - [{trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - }}], - [{trace: { - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true - }}] - ]; - opts = { - traceorder: 'grouped' - }; - - legendData = getLegendData(calcdata, opts); - - expected = [ - [ - [{trace: { - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true - - }}], - [{trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - }}], - [{trace: { - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true - }}] - ] - ]; - - expect(legendData).toEqual(expected); - expect(opts._lgroupsLength).toEqual(1); - }); - - it('should return empty array when legend data has no traces', function() { - calcdata = [ - [{trace: { - type: 'histogram', - visible: true, - legendgroup: '', - showlegend: false - - }}], - [{trace: { - type: 'box', - visible: 'legendonly', - legendgroup: '', - showlegend: false - }}], - [{trace: { - type: 'heatmap', - visible: true, - legendgroup: '' - }}] - ]; - opts = { - traceorder: 'normal' - }; - - legendData = getLegendData(calcdata, opts); - expect(legendData).toEqual([]); - }); - - it('should reverse the order when legend.traceorder is set', function() { - calcdata = [ - [{trace: { - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true - - }}], - [{trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - }}], - [{trace: { - type: 'box', +describe("legend getLegendData", function() { + "use strict"; + var calcdata, opts, legendData, expected; + + it("should group legendgroup traces", function() { + calcdata = [ + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "group", + showlegend: true + } + } + ], + [ + { + trace: { + type: "bar", + visible: "legendonly", + legendgroup: "", + showlegend: true + } + } + ], + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "group", + showlegend: true + } + } + ] + ]; + opts = { traceorder: "grouped" }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "group", + showlegend: true + } + } + ], + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "group", + showlegend: true + } + } + ] + ], + [ + [ + { + trace: { + type: "bar", + visible: "legendonly", + legendgroup: "", + showlegend: true + } + } + ] + ] + ]; + + expect(legendData).toEqual(expected); + expect(opts._lgroupsLength).toEqual(2); + }); + + it("should collapse when data has only one group", function() { + calcdata = [ + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "", + showlegend: true + } + } + ], + [ + { + trace: { + type: "bar", + visible: "legendonly", + legendgroup: "", + showlegend: true + } + } + ], + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "", + showlegend: true + } + } + ] + ]; + opts = { traceorder: "grouped" }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "", + showlegend: true + } + } + ], + [ + { + trace: { + type: "bar", + visible: "legendonly", + legendgroup: "", + showlegend: true + } + } + ], + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "", + showlegend: true + } + } + ] + ] + ]; + + expect(legendData).toEqual(expected); + expect(opts._lgroupsLength).toEqual(1); + }); + + it("should return empty array when legend data has no traces", function() { + calcdata = [ + [ + { + trace: { + type: "histogram", + visible: true, + legendgroup: "", + showlegend: false + } + } + ], + [ + { + trace: { + type: "box", + visible: "legendonly", + legendgroup: "", + showlegend: false + } + } + ], + [{ trace: { type: "heatmap", visible: true, legendgroup: "" } }] + ]; + opts = { traceorder: "normal" }; + + legendData = getLegendData(calcdata, opts); + expect(legendData).toEqual([]); + }); + + it("should reverse the order when legend.traceorder is set", function() { + calcdata = [ + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "", + showlegend: true + } + } + ], + [ + { + trace: { + type: "bar", + visible: "legendonly", + legendgroup: "", + showlegend: true + } + } + ], + [ + { + trace: { + type: "box", + visible: true, + legendgroup: "", + showlegend: true + } + } + ] + ]; + opts = { traceorder: "reversed" }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [ + { + trace: { + type: "box", + visible: true, + legendgroup: "", + showlegend: true + } + } + ], + [ + { + trace: { + type: "bar", + visible: "legendonly", + legendgroup: "", + showlegend: true + } + } + ], + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "", + showlegend: true + } + } + ] + ] + ]; + + expect(legendData).toEqual(expected); + expect(opts._lgroupsLength).toEqual(1); + }); + + it( + "should reverse the trace order within groups when reversed+grouped", + function() { + calcdata = [ + [ + { + trace: { + type: "scatter", + visible: true, + legendgroup: "group", + showlegend: true + } + } + ], + [ + { + trace: { + type: "bar", + visible: "legendonly", + legendgroup: "", + showlegend: true + } + } + ], + [ + { + trace: { + type: "box", + visible: true, + legendgroup: "group", + showlegend: true + } + } + ] + ]; + opts = { traceorder: "reversed+grouped" }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [ + { + trace: { + type: "box", visible: true, - legendgroup: '', + legendgroup: "group", showlegend: true - }}] - ]; - opts = { - traceorder: 'reversed' - }; - - legendData = getLegendData(calcdata, opts); - - expected = [ - [ - [{trace: { - type: 'box', - visible: true, - legendgroup: '', - showlegend: true - - }}], - [{trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - }}], - [{trace: { - type: 'scatter', - visible: true, - legendgroup: '', - showlegend: true - }}] - ] - ]; - - expect(legendData).toEqual(expected); - expect(opts._lgroupsLength).toEqual(1); - }); - - it('should reverse the trace order within groups when reversed+grouped', function() { - calcdata = [ - [{trace: { - type: 'scatter', + } + } + ], + [ + { + trace: { + type: "scatter", visible: true, - legendgroup: 'group', + legendgroup: "group", showlegend: true - - }}], - [{trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', + } + } + ] + ], + [ + [ + { + trace: { + type: "bar", + visible: "legendonly", + legendgroup: "", showlegend: true - }}], - [{trace: { - type: 'box', - visible: true, - legendgroup: 'group', - showlegend: true - }}] - ]; - opts = { - traceorder: 'reversed+grouped' - }; - - legendData = getLegendData(calcdata, opts); - - expected = [ - [ - [{trace: { - type: 'box', - visible: true, - legendgroup: 'group', - showlegend: true - - }}], - [{trace: { - type: 'scatter', - visible: true, - legendgroup: 'group', - showlegend: true - }}] - ], - [ - [{trace: { - type: 'bar', - visible: 'legendonly', - legendgroup: '', - showlegend: true - }}] - ] - ]; - - expect(legendData).toEqual(expected); - expect(opts._lgroupsLength).toEqual(2); - }); + } + } + ] + ] + ]; + + expect(legendData).toEqual(expected); + expect(opts._lgroupsLength).toEqual(2); + } + ); }); -describe('legend helpers:', function() { - 'use strict'; - - describe('legendGetsTraces', function() { - var legendGetsTrace = helpers.legendGetsTrace; - - it('should return true when trace is visible and supports legend', function() { - expect(legendGetsTrace({ visible: true, type: 'bar' })).toBe(true); - expect(legendGetsTrace({ visible: false, type: 'bar' })).toBe(false); - expect(legendGetsTrace({ visible: true, type: 'contour' })).toBe(false); - expect(legendGetsTrace({ visible: false, type: 'contour' })).toBe(false); - }); - }); - - describe('isGrouped', function() { - var isGrouped = helpers.isGrouped; - - it('should return true when trace is visible and supports legend', function() { - expect(isGrouped({ traceorder: 'normal' })).toBe(false); - expect(isGrouped({ traceorder: 'grouped' })).toBe(true); - expect(isGrouped({ traceorder: 'reversed+grouped' })).toBe(true); - expect(isGrouped({ traceorder: 'grouped+reversed' })).toBe(true); - expect(isGrouped({ traceorder: 'reversed' })).toBe(false); - }); - }); - - describe('isReversed', function() { - var isReversed = helpers.isReversed; - - it('should return true when trace is visible and supports legend', function() { - expect(isReversed({ traceorder: 'normal' })).toBe(false); - expect(isReversed({ traceorder: 'grouped' })).toBe(false); - expect(isReversed({ traceorder: 'reversed+grouped' })).toBe(true); - expect(isReversed({ traceorder: 'grouped+reversed' })).toBe(true); - expect(isReversed({ traceorder: 'reversed' })).toBe(true); - }); - }); +describe("legend helpers:", function() { + "use strict"; + describe("legendGetsTraces", function() { + var legendGetsTrace = helpers.legendGetsTrace; + + it( + "should return true when trace is visible and supports legend", + function() { + expect(legendGetsTrace({ visible: true, type: "bar" })).toBe(true); + expect(legendGetsTrace({ visible: false, type: "bar" })).toBe(false); + expect(legendGetsTrace({ visible: true, type: "contour" })).toBe(false); + expect(legendGetsTrace({ visible: false, type: "contour" })).toBe( + false + ); + } + ); + }); + + describe("isGrouped", function() { + var isGrouped = helpers.isGrouped; + + it( + "should return true when trace is visible and supports legend", + function() { + expect(isGrouped({ traceorder: "normal" })).toBe(false); + expect(isGrouped({ traceorder: "grouped" })).toBe(true); + expect(isGrouped({ traceorder: "reversed+grouped" })).toBe(true); + expect(isGrouped({ traceorder: "grouped+reversed" })).toBe(true); + expect(isGrouped({ traceorder: "reversed" })).toBe(false); + } + ); + }); + + describe("isReversed", function() { + var isReversed = helpers.isReversed; + + it( + "should return true when trace is visible and supports legend", + function() { + expect(isReversed({ traceorder: "normal" })).toBe(false); + expect(isReversed({ traceorder: "grouped" })).toBe(false); + expect(isReversed({ traceorder: "reversed+grouped" })).toBe(true); + expect(isReversed({ traceorder: "grouped+reversed" })).toBe(true); + expect(isReversed({ traceorder: "reversed" })).toBe(true); + } + ); + }); }); -describe('legend anchor utils:', function() { - 'use strict'; - - describe('isRightAnchor', function() { - var isRightAnchor = anchorUtils.isRightAnchor; - var threshold = 2 / 3; +describe("legend anchor utils:", function() { + "use strict"; + describe("isRightAnchor", function() { + var isRightAnchor = anchorUtils.isRightAnchor; + var threshold = 2 / 3; - it('should return true when \'xanchor\' is set to \'right\'', function() { - expect(isRightAnchor({ xanchor: 'left' })).toBe(false); - expect(isRightAnchor({ xanchor: 'center' })).toBe(false); - expect(isRightAnchor({ xanchor: 'right' })).toBe(true); - }); - - it('should return true when \'xanchor\' is set to \'auto\' and \'x\' >= 2/3', function() { - var opts = { xanchor: 'auto' }; - - [0, 0.4, 0.7, 1].forEach(function(v) { - opts.x = v; - expect(isRightAnchor(opts)) - .toBe(v > threshold, 'case ' + v); - }); - }); + it("should return true when 'xanchor' is set to 'right'", function() { + expect(isRightAnchor({ xanchor: "left" })).toBe(false); + expect(isRightAnchor({ xanchor: "center" })).toBe(false); + expect(isRightAnchor({ xanchor: "right" })).toBe(true); }); - describe('isCenterAnchor', function() { - var isCenterAnchor = anchorUtils.isCenterAnchor; - var threshold0 = 1 / 3; - var threshold1 = 2 / 3; + it( + "should return true when 'xanchor' is set to 'auto' and 'x' >= 2/3", + function() { + var opts = { xanchor: "auto" }; - it('should return true when \'xanchor\' is set to \'center\'', function() { - expect(isCenterAnchor({ xanchor: 'left' })).toBe(false); - expect(isCenterAnchor({ xanchor: 'center' })).toBe(true); - expect(isCenterAnchor({ xanchor: 'right' })).toBe(false); - }); - - it('should return true when \'xanchor\' is set to \'auto\' and 1/3 < \'x\' < 2/3', function() { - var opts = { xanchor: 'auto' }; - - [0, 0.4, 0.7, 1].forEach(function(v) { - opts.x = v; - expect(isCenterAnchor(opts)) - .toBe(v > threshold0 && v < threshold1, 'case ' + v); - }); + [0, 0.4, 0.7, 1].forEach(function(v) { + opts.x = v; + expect(isRightAnchor(opts)).toBe(v > threshold, "case " + v); }); + } + ); + }); + + describe("isCenterAnchor", function() { + var isCenterAnchor = anchorUtils.isCenterAnchor; + var threshold0 = 1 / 3; + var threshold1 = 2 / 3; + + it("should return true when 'xanchor' is set to 'center'", function() { + expect(isCenterAnchor({ xanchor: "left" })).toBe(false); + expect(isCenterAnchor({ xanchor: "center" })).toBe(true); + expect(isCenterAnchor({ xanchor: "right" })).toBe(false); }); - describe('isBottomAnchor', function() { - var isBottomAnchor = anchorUtils.isBottomAnchor; - var threshold = 1 / 3; - - it('should return true when \'yanchor\' is set to \'right\'', function() { - expect(isBottomAnchor({ yanchor: 'top' })).toBe(false); - expect(isBottomAnchor({ yanchor: 'middle' })).toBe(false); - expect(isBottomAnchor({ yanchor: 'bottom' })).toBe(true); - }); - - it('should return true when \'yanchor\' is set to \'auto\' and \'y\' <= 1/3', function() { - var opts = { yanchor: 'auto' }; - - [0, 0.4, 0.7, 1].forEach(function(v) { - opts.y = v; - expect(isBottomAnchor(opts)) - .toBe(v < threshold, 'case ' + v); - }); + it( + "should return true when 'xanchor' is set to 'auto' and 1/3 < 'x' < 2/3", + function() { + var opts = { xanchor: "auto" }; + + [0, 0.4, 0.7, 1].forEach(function(v) { + opts.x = v; + expect(isCenterAnchor(opts)).toBe( + v > threshold0 && v < threshold1, + "case " + v + ); }); + } + ); + }); + + describe("isBottomAnchor", function() { + var isBottomAnchor = anchorUtils.isBottomAnchor; + var threshold = 1 / 3; + + it("should return true when 'yanchor' is set to 'right'", function() { + expect(isBottomAnchor({ yanchor: "top" })).toBe(false); + expect(isBottomAnchor({ yanchor: "middle" })).toBe(false); + expect(isBottomAnchor({ yanchor: "bottom" })).toBe(true); }); - describe('isMiddleAnchor', function() { - var isMiddleAnchor = anchorUtils.isMiddleAnchor; - var threshold0 = 1 / 3; - var threshold1 = 2 / 3; + it( + "should return true when 'yanchor' is set to 'auto' and 'y' <= 1/3", + function() { + var opts = { yanchor: "auto" }; - it('should return true when \'yanchor\' is set to \'center\'', function() { - expect(isMiddleAnchor({ yanchor: 'top' })).toBe(false); - expect(isMiddleAnchor({ yanchor: 'middle' })).toBe(true); - expect(isMiddleAnchor({ yanchor: 'bottom' })).toBe(false); + [0, 0.4, 0.7, 1].forEach(function(v) { + opts.y = v; + expect(isBottomAnchor(opts)).toBe(v < threshold, "case " + v); }); + } + ); + }); + + describe("isMiddleAnchor", function() { + var isMiddleAnchor = anchorUtils.isMiddleAnchor; + var threshold0 = 1 / 3; + var threshold1 = 2 / 3; + + it("should return true when 'yanchor' is set to 'center'", function() { + expect(isMiddleAnchor({ yanchor: "top" })).toBe(false); + expect(isMiddleAnchor({ yanchor: "middle" })).toBe(true); + expect(isMiddleAnchor({ yanchor: "bottom" })).toBe(false); + }); - it('should return true when \'yanchor\' is set to \'auto\' and 1/3 < \'y\' < 2/3', function() { - var opts = { yanchor: 'auto' }; - - [0, 0.4, 0.7, 1].forEach(function(v) { - opts.y = v; - expect(isMiddleAnchor(opts)) - .toBe(v > threshold0 && v < threshold1, 'case ' + v); - }); + it( + "should return true when 'yanchor' is set to 'auto' and 1/3 < 'y' < 2/3", + function() { + var opts = { yanchor: "auto" }; + + [0, 0.4, 0.7, 1].forEach(function(v) { + opts.y = v; + expect(isMiddleAnchor(opts)).toBe( + v > threshold0 && v < threshold1, + "case " + v + ); }); - }); + } + ); + }); }); -describe('legend relayout update', function() { - 'use strict'; - - afterEach(destroyGraphDiv); +describe("legend relayout update", function() { + "use strict"; + afterEach(destroyGraphDiv); - it('should update border styling', function(done) { - var mock = require('@mocks/0.json'), - mockCopy = Lib.extendDeep({}, mock), - gd = createGraphDiv(); + it("should update border styling", function(done) { + var mock = require("@mocks/0.json"), + mockCopy = Lib.extendDeep({}, mock), + gd = createGraphDiv(); - function assertLegendStyle(bgColor, borderColor, borderWidth) { - var node = d3.select('g.legend').select('rect'); - - expect(node.style('fill')).toEqual(bgColor); - expect(node.style('stroke')).toEqual(borderColor); - expect(node.style('stroke-width')).toEqual(borderWidth + 'px'); - } + function assertLegendStyle(bgColor, borderColor, borderWidth) { + var node = d3.select("g.legend").select("rect"); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - assertLegendStyle('rgb(255, 255, 255)', 'rgb(0, 0, 0)', 1); + expect(node.style("fill")).toEqual(bgColor); + expect(node.style("stroke")).toEqual(borderColor); + expect(node.style("stroke-width")).toEqual(borderWidth + "px"); + } - return Plotly.relayout(gd, { - 'legend.bordercolor': 'red', - 'legend.bgcolor': 'blue' - }); - }).then(function() { - assertLegendStyle('rgb(0, 0, 255)', 'rgb(255, 0, 0)', 1); + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + assertLegendStyle("rgb(255, 255, 255)", "rgb(0, 0, 0)", 1); - return Plotly.relayout(gd, 'legend.borderwidth', 10); - }).then(function() { - assertLegendStyle('rgb(0, 0, 255)', 'rgb(255, 0, 0)', 10); - - return Plotly.relayout(gd, 'legend.bgcolor', null); - }).then(function() { - assertLegendStyle('rgb(255, 255, 255)', 'rgb(255, 0, 0)', 10); - - return Plotly.relayout(gd, 'paper_bgcolor', 'blue'); - }).then(function() { - assertLegendStyle('rgb(0, 0, 255)', 'rgb(255, 0, 0)', 10); - - done(); + return Plotly.relayout(gd, { + "legend.bordercolor": "red", + "legend.bgcolor": "blue" }); - }); + }) + .then(function() { + assertLegendStyle("rgb(0, 0, 255)", "rgb(255, 0, 0)", 1); + + return Plotly.relayout(gd, "legend.borderwidth", 10); + }) + .then(function() { + assertLegendStyle("rgb(0, 0, 255)", "rgb(255, 0, 0)", 10); + + return Plotly.relayout(gd, "legend.bgcolor", null); + }) + .then(function() { + assertLegendStyle("rgb(255, 255, 255)", "rgb(255, 0, 0)", 10); + + return Plotly.relayout(gd, "paper_bgcolor", "blue"); + }) + .then(function() { + assertLegendStyle("rgb(0, 0, 255)", "rgb(255, 0, 0)", 10); + + done(); + }); + }); }); -describe('legend orientation change:', function() { - 'use strict'; - - afterEach(destroyGraphDiv); - - it('should update plot background', function(done) { - var mock = require('@mocks/legend_horizontal_autowrap.json'), - gd = createGraphDiv(), - initialLegendBGColor; - - Plotly.plot(gd, mock.data, mock.layout).then(function() { - initialLegendBGColor = gd._fullLayout.legend.bgcolor; - return Plotly.relayout(gd, 'legend.bgcolor', '#000000'); - }).then(function() { - expect(gd._fullLayout.legend.bgcolor).toBe('#000000'); - return Plotly.relayout(gd, 'legend.bgcolor', initialLegendBGColor); - }).then(function() { - expect(gd._fullLayout.legend.bgcolor).toBe(initialLegendBGColor); - done(); - }); - }); +describe("legend orientation change:", function() { + "use strict"; + afterEach(destroyGraphDiv); + + it("should update plot background", function(done) { + var mock = require("@mocks/legend_horizontal_autowrap.json"), + gd = createGraphDiv(), + initialLegendBGColor; + + Plotly.plot(gd, mock.data, mock.layout) + .then(function() { + initialLegendBGColor = gd._fullLayout.legend.bgcolor; + return Plotly.relayout(gd, "legend.bgcolor", "#000000"); + }) + .then(function() { + expect(gd._fullLayout.legend.bgcolor).toBe("#000000"); + return Plotly.relayout(gd, "legend.bgcolor", initialLegendBGColor); + }) + .then(function() { + expect(gd._fullLayout.legend.bgcolor).toBe(initialLegendBGColor); + done(); + }); + }); }); -describe('legend restyle update', function() { - 'use strict'; - - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - afterEach(destroyGraphDiv); - - it('should update trace toggle background rectangle', function(done) { - var mock = require('@mocks/0.json'), - mockCopy = Lib.extendDeep({}, mock), - gd = createGraphDiv(); - - mockCopy.data[0].visible = false; - mockCopy.data[0].showlegend = false; - mockCopy.data[1].visible = false; - mockCopy.data[1].showlegend = false; - - function countLegendItems() { - return d3.select(gd).selectAll('rect.legendtoggle').size(); - } - - function assertTraceToggleRect() { - var nodes = d3.selectAll('rect.legendtoggle'); - - nodes.each(function() { - var node = d3.select(this); - - expect(node.attr('x')).toEqual('0'); - expect(node.attr('y')).toEqual('-9.5'); - expect(node.attr('height')).toEqual('19'); - - var w = +node.attr('width'); - expect(Math.abs(w - 160)).toBeLessThan(10); - }); - } - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - expect(countLegendItems()).toEqual(1); - assertTraceToggleRect(); - - return Plotly.restyle(gd, 'visible', [true, false, false]); - }).then(function() { - expect(countLegendItems()).toEqual(0); - - return Plotly.restyle(gd, 'showlegend', [true, false, false]); - }).then(function() { - expect(countLegendItems()).toEqual(1); - assertTraceToggleRect(); - - done(); - }); - }); +describe("legend restyle update", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + afterEach(destroyGraphDiv); + + it("should update trace toggle background rectangle", function(done) { + var mock = require("@mocks/0.json"), + mockCopy = Lib.extendDeep({}, mock), + gd = createGraphDiv(); + + mockCopy.data[0].visible = false; + mockCopy.data[0].showlegend = false; + mockCopy.data[1].visible = false; + mockCopy.data[1].showlegend = false; + + function countLegendItems() { + return d3.select(gd).selectAll("rect.legendtoggle").size(); + } + + function assertTraceToggleRect() { + var nodes = d3.selectAll("rect.legendtoggle"); + + nodes.each(function() { + var node = d3.select(this); + + expect(node.attr("x")).toEqual("0"); + expect(node.attr("y")).toEqual("-9.5"); + expect(node.attr("height")).toEqual("19"); + + var w = +node.attr("width"); + expect(Math.abs(w - 160)).toBeLessThan(10); + }); + } + + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + expect(countLegendItems()).toEqual(1); + assertTraceToggleRect(); + + return Plotly.restyle(gd, "visible", [true, false, false]); + }) + .then(function() { + expect(countLegendItems()).toEqual(0); + + return Plotly.restyle(gd, "showlegend", [true, false, false]); + }) + .then(function() { + expect(countLegendItems()).toEqual(1); + assertTraceToggleRect(); + + done(); + }); + }); }); diff --git a/test/jasmine/tests/lib_date_test.js b/test/jasmine/tests/lib_date_test.js index 2c5cd93e615..7b869970750 100644 --- a/test/jasmine/tests/lib_date_test.js +++ b/test/jasmine/tests/lib_date_test.js @@ -1,605 +1,749 @@ -var isNumeric = require('fast-isnumeric'); +var isNumeric = require("fast-isnumeric"); -var Lib = require('@src/lib'); -var calComponent = require('@src/components/calendars'); +var Lib = require("@src/lib"); +var calComponent = require("@src/components/calendars"); // use only the parts of world-calendars that we've imported for our tests -var calendars = require('@src/components/calendars/calendars'); - -describe('dates', function() { - 'use strict'; - - var d1c = new Date(2000, 0, 1, 1, 0, 0, 600); - // first-century years must be set separately as Date constructor maps 2-digit years - // to near the present, but we accept them as part of 4-digit years - d1c.setFullYear(13); - - var thisYear = new Date().getFullYear(), - thisYear_2 = thisYear % 100, - nowMinus70 = thisYear - 70, - nowMinus70_2 = nowMinus70 % 100, - nowPlus29 = thisYear + 29, - nowPlus29_2 = nowPlus29 % 100; - - describe('dateTime2ms', function() { - it('should accept valid date strings', function() { - var tzOffset; - - [ - ['2016', new Date(2016, 0, 1)], - ['2016-05', new Date(2016, 4, 1)], - // leap year, and whitespace - ['\r\n\t 2016-02-29\r\n\t ', new Date(2016, 1, 29)], - ['9814-08-23', new Date(9814, 7, 23)], - ['1564-03-14 12', new Date(1564, 2, 14, 12)], - ['0122-04-08 08:22', new Date(122, 3, 8, 8, 22)], - ['-0098-11-19 23:59:59', new Date(-98, 10, 19, 23, 59, 59)], - ['-9730-12-01 12:34:56.789', new Date(-9730, 11, 1, 12, 34, 56, 789)], - // random whitespace before and after gets stripped - ['\r\n\t -9730-12-01 12:34:56.789\r\n\t ', new Date(-9730, 11, 1, 12, 34, 56, 789)], - // first century, also allow month, day, and hour to be 1-digit, and not all - // three digits of milliseconds - ['0013-1-1 1:00:00.6', d1c], - // we support tenths of msec too, though Date objects don't. Smaller than that - // and we hit the precision limit of js numbers unless we're close to the epoch. - // It won't break though. - ['0013-1-1 1:00:00.6001', +d1c + 0.1], - ['0013-1-1 1:00:00.60011111111', +d1c + 0.11111111], - - // 2-digit years get mapped to now-70 -> now+29 - [thisYear_2 + '-05', new Date(thisYear, 4, 1)], - [nowMinus70_2 + '-10-18', new Date(nowMinus70, 9, 18)], - [nowPlus29_2 + '-02-12 14:29:32', new Date(nowPlus29, 1, 12, 14, 29, 32)], - - // including timezone info (that we discard) - ['2014-03-04 08:15Z', new Date(2014, 2, 4, 8, 15)], - ['2014-03-04 08:15:00.00z', new Date(2014, 2, 4, 8, 15)], - ['2014-03-04 08:15:34+1200', new Date(2014, 2, 4, 8, 15, 34)], - ['2014-03-04 08:15:34.567-05:45', new Date(2014, 2, 4, 8, 15, 34, 567)], - ].forEach(function(v) { - // just for sub-millisecond precision tests, use timezoneoffset - // from the previous date object - if(v[1].getTimezoneOffset) tzOffset = v[1].getTimezoneOffset(); - - var expected = +v[1] - (tzOffset * 60000); - expect(Lib.dateTime2ms(v[0])).toBe(expected, v[0]); - - // ISO-8601: all the same stuff with t or T as the separator - expect(Lib.dateTime2ms(v[0].trim().replace(' ', 't'))).toBe(expected, v[0].trim().replace(' ', 't')); - expect(Lib.dateTime2ms('\r\n\t ' + v[0].trim().replace(' ', 'T') + '\r\n\t ')).toBe(expected, v[0].trim().replace(' ', 'T')); - }); - }); - - it('should accept 4-digit and 2-digit numbers', function() { - // not sure if we really *want* this behavior, but it's what we have. - // especially since the number 0 is *not* allowed it seems pretty unlikely - // to cause problems for people using milliseconds as dates, since the only - // values to get mistaken are between 1 and 10 seconds before and after - // the epoch, and between 10 and 99 milliseconds after the epoch - // (note that millisecond numbers are not handled by dateTime2ms directly, - // but in ax.d2c if dateTime2ms fails.) - [ - 1000, 9999, -1000, -9999 - ].forEach(function(v) { - expect(Lib.dateTime2ms(v)).toBe(Date.UTC(v, 0, 1), v); - }); - - [ - [10, 2010], - [nowPlus29_2, nowPlus29], - [nowMinus70_2, nowMinus70], - [99, 1999] - ].forEach(function(v) { - expect(Lib.dateTime2ms(v[0])).toBe(Date.UTC(v[1], 0, 1), v[0]); - }); - }); - - it('should accept Date objects within year +/-9999', function() { - [ - new Date(), - new Date(-9999, 0, 1), - new Date(9999, 11, 31, 23, 59, 59, 999), - new Date(-1, 0, 1), - new Date(323, 11, 30), - new Date(-456, 1, 2), - d1c, - new Date(2015, 8, 7, 23, 34, 45, 567) - ].forEach(function(v) { - expect(Lib.dateTime2ms(v)).toBe(+v - v.getTimezoneOffset() * 60000); - }); - }); - - it('should not accept Date objects beyond our limits', function() { - [ - new Date(10000, 0, 1), - new Date(-10000, 11, 31, 23, 59, 59, 999) - ].forEach(function(v) { - expect(Lib.dateTime2ms(v)).toBeUndefined(v); - }); - }); - - it('should not accept invalid strings or other objects', function() { - [ - '', 0, 1, 9, -1, -10, -99, 100, 999, -100, -999, 10000, -10000, - 1.2, -1.2, 2015.1, -1023.4, NaN, null, undefined, Infinity, -Infinity, - {}, {1: '2014-01-01'}, [], [2016], ['2015-11-23'], - '123-01-01', '-756-01-01', // 3-digit year - '10000-01-01', '-10000-01-01', // 5-digit year - '2015-00-01', '2015-13-01', '2015-001-01', // bad month - '2015-01-00', '2015-01-32', '2015-02-29', '2015-04-31', '2015-01-001', // bad day (incl non-leap year) - '2015-01-01 24:00', '2015-01-01 -1:00', '2015-01-01 001:00', // bad hour - '2015-01-01 12:60', '2015-01-01 12:-1', '2015-01-01 12:001', '2015-01-01 12:1', // bad minute - '2015-01-01 12:00:60', '2015-01-01 12:00:-1', '2015-01-01 12:00:001', '2015-01-01 12:00:1', // bad second - '2015-01-01T', '2015-01-01TT12:34', // bad ISO separators - '2015-01-01Z', '2015-01-01T12Z', '2015-01-01T12:34Z05:00', '2015-01-01 12:34+500', '2015-01-01 12:34-5:00' // bad TZ info - ].forEach(function(v) { - expect(Lib.dateTime2ms(v)).toBeUndefined(v); - }); - }); - - var JULY1MS = 181 * 24 * 3600 * 1000; - - it('should use UTC with no timezone offset or daylight saving time', function() { - expect(Lib.dateTime2ms('1970-01-01')).toBe(0); - - // 181 days (and no DST hours) between jan 1 and july 1 in a non-leap-year - // 31 + 28 + 31 + 30 + 31 + 30 - expect(Lib.dateTime2ms('1970-07-01')).toBe(JULY1MS); - }); - - it('should interpret JS dates by local time, not by its getTime()', function() { - // not really part of the test, just to make sure the test is meaningful - // the test should NOT be run in a UTC environment - var local0 = Number(new Date(1970, 0, 1)), - localjuly1 = Number(new Date(1970, 6, 1)); - expect([local0, localjuly1]).not.toEqual([0, JULY1MS], - 'test must not run in UTC'); - // verify that there *is* daylight saving time in the test environment - expect(localjuly1 - local0).not.toEqual(JULY1MS - 0, - 'test must run in a timezone with DST'); - - // now repeat the previous test and show that we throw away - // timezone info from js dates - expect(Lib.dateTime2ms(new Date(1970, 0, 1))).toBe(0); - expect(Lib.dateTime2ms(new Date(1970, 6, 1))).toBe(JULY1MS); - }); +var calendars = require("@src/components/calendars/calendars"); + +describe("dates", function() { + "use strict"; + var d1c = new Date(2000, 0, 1, 1, 0, 0, 600); + // first-century years must be set separately as Date constructor maps 2-digit years + // to near the present, but we accept them as part of 4-digit years + d1c.setFullYear(13); + + var thisYear = new Date().getFullYear(), + thisYear_2 = thisYear % 100, + nowMinus70 = thisYear - 70, + nowMinus70_2 = nowMinus70 % 100, + nowPlus29 = thisYear + 29, + nowPlus29_2 = nowPlus29 % 100; + + describe("dateTime2ms", function() { + it("should accept valid date strings", function() { + var tzOffset; + + [ + ["2016", new Date(2016, 0, 1)], + ["2016-05", new Date(2016, 4, 1)], + // leap year, and whitespace + ["\r\n\t 2016-02-29\r\n\t ", new Date(2016, 1, 29)], + ["9814-08-23", new Date(9814, 7, 23)], + ["1564-03-14 12", new Date(1564, 2, 14, 12)], + ["0122-04-08 08:22", new Date(122, 3, 8, 8, 22)], + ["-0098-11-19 23:59:59", new Date(-98, 10, 19, 23, 59, 59)], + ["-9730-12-01 12:34:56.789", new Date(-9730, 11, 1, 12, 34, 56, 789)], + // random whitespace before and after gets stripped + [ + "\r\n\t -9730-12-01 12:34:56.789\r\n\t ", + new Date(-9730, 11, 1, 12, 34, 56, 789) + ], + // first century, also allow month, day, and hour to be 1-digit, and not all + // three digits of milliseconds + ["0013-1-1 1:00:00.6", d1c], + // we support tenths of msec too, though Date objects don't. Smaller than that + // and we hit the precision limit of js numbers unless we're close to the epoch. + // It won't break though. + ["0013-1-1 1:00:00.6001", +d1c + 0.1], + ["0013-1-1 1:00:00.60011111111", +d1c + 0.11111111], + // 2-digit years get mapped to now-70 -> now+29 + [thisYear_2 + "-05", new Date(thisYear, 4, 1)], + [nowMinus70_2 + "-10-18", new Date(nowMinus70, 9, 18)], + [ + nowPlus29_2 + "-02-12 14:29:32", + new Date(nowPlus29, 1, 12, 14, 29, 32) + ], + // including timezone info (that we discard) + ["2014-03-04 08:15Z", new Date(2014, 2, 4, 8, 15)], + ["2014-03-04 08:15:00.00z", new Date(2014, 2, 4, 8, 15)], + ["2014-03-04 08:15:34+1200", new Date(2014, 2, 4, 8, 15, 34)], + ["2014-03-04 08:15:34.567-05:45", new Date(2014, 2, 4, 8, 15, 34, 567)] + ].forEach(function(v) { + // just for sub-millisecond precision tests, use timezoneoffset + // from the previous date object + if (v[1].getTimezoneOffset) tzOffset = v[1].getTimezoneOffset(); + + var expected = +v[1] - tzOffset * 60000; + expect(Lib.dateTime2ms(v[0])).toBe(expected, v[0]); + + // ISO-8601: all the same stuff with t or T as the separator + expect(Lib.dateTime2ms(v[0].trim().replace(" ", "t"))).toBe( + expected, + v[0].trim().replace(" ", "t") + ); + expect( + Lib.dateTime2ms("\r\n\t " + v[0].trim().replace(" ", "T") + "\r\n\t ") + ).toBe(expected, v[0].trim().replace(" ", "T")); + }); }); - describe('ms2DateTime', function() { - it('should report the minimum fields with nonzero values, except minutes', function() { - [ - '2016-01-01', // we'll never report less than this bcs month and day are never zero - '2016-01-01 01:00', // we won't report hours without minutes - '2016-01-01 01:01', - '2016-01-01 01:01:01', - '2016-01-01 01:01:01.1', - '2016-01-01 01:01:01.01', - '2016-01-01 01:01:01.001', - '2016-01-01 01:01:01.0001' - ].forEach(function(v) { - expect(Lib.ms2DateTime(Lib.dateTime2ms(v))).toBe(v); - }); - }); - - it('should accept Date objects within year +/-9999', function() { - [ - '-9999-01-01', - '-9999-01-01 00:00:00.0001', - '9999-12-31 23:59:59.9999', - '0123-01-01', - '0042-01-01', - '-0016-01-01', - '-0016-01-01 12:34:56.7891', - '-0456-07-23 16:22' - ].forEach(function(v) { - expect(Lib.ms2DateTime(Lib.dateTime2ms(v))).toBe(v); - }); - }); - - it('should not accept Date objects beyond our limits or other objects', function() { - [ - Date.UTC(10000, 0, 1), - Date.UTC(-10000, 11, 31, 23, 59, 59, 999), - '', - '2016-01-01', - '0', - [], [0], {}, {1: 2} - ].forEach(function(v) { - expect(Lib.ms2DateTime(v)).toBeUndefined(v); - }); - }); - - it('should drop the right pieces if rounding is specified', function() { - [ - ['2016-01-01 00:00:00.0001', 0, '2016-01-01 00:00:00.0001'], - ['2016-01-01 00:00:00.0001', 299999, '2016-01-01 00:00:00.0001'], - ['2016-01-01 00:00:00.0001', 300000, '2016-01-01'], - ['2016-01-01 00:00:00.0001', 7776000000, '2016-01-01'], - ['2016-01-01 12:34:56.7891', 0, '2016-01-01 12:34:56.7891'], - ['2016-01-01 12:34:56.7891', 299999, '2016-01-01 12:34:56.7891'], - ['2016-01-01 12:34:56.7891', 300000, '2016-01-01 12:34:56'], - ['2016-01-01 12:34:56.7891', 10799999, '2016-01-01 12:34:56'], - ['2016-01-01 12:34:56.7891', 10800000, '2016-01-01 12:34'], - ['2016-01-01 12:34:56.7891', 7775999999, '2016-01-01 12:34'], - ['2016-01-01 12:34:56.7891', 7776000000, '2016-01-01'], - ['2016-01-01 12:34:56.7891', 1e300, '2016-01-01'] - ].forEach(function(v) { - expect(Lib.ms2DateTime(Lib.dateTime2ms(v[0]), v[1])).toBe(v[2], v); - }); - }); - - it('should work right with inputs beyond our precision', function() { - for(var i = -1; i <= 1; i += 0.001) { - var tenths = Math.round(i * 10), - base = i < -0.05 ? '1969-12-31 23:59:59.99' : '1970-01-01 00:00:00.00', - expected = (base + String(tenths + 200).substr(1)) - .replace(/0+$/, '') - .replace(/ 00:00:00[\.]$/, ''); - expect(Lib.ms2DateTime(i)).toBe(expected, i); - } - }); + it("should accept 4-digit and 2-digit numbers", function() { + // not sure if we really *want* this behavior, but it's what we have. + // especially since the number 0 is *not* allowed it seems pretty unlikely + // to cause problems for people using milliseconds as dates, since the only + // values to get mistaken are between 1 and 10 seconds before and after + // the epoch, and between 10 and 99 milliseconds after the epoch + // (note that millisecond numbers are not handled by dateTime2ms directly, + // but in ax.d2c if dateTime2ms fails.) + [1000, 9999, -1000, -9999].forEach(function(v) { + expect(Lib.dateTime2ms(v)).toBe(Date.UTC(v, 0, 1), v); + }); + + [ + [10, 2010], + [nowPlus29_2, nowPlus29], + [nowMinus70_2, nowMinus70], + [99, 1999] + ].forEach(function(v) { + expect(Lib.dateTime2ms(v[0])).toBe(Date.UTC(v[1], 0, 1), v[0]); + }); }); - describe('world calendar inputs', function() { - it('should give the right values near epoch zero', function() { - [ - [undefined, '1970-01-01'], - ['gregorian', '1970-01-01'], - ['chinese', '1969-11-24'], - ['coptic', '1686-04-23'], - ['discworld', '1798-12-27'], - ['ethiopian', '1962-04-23'], - ['hebrew', '5730-10-23'], - ['islamic', '1389-10-22'], - ['julian', '1969-12-19'], - ['mayan', '5156-07-05'], - ['nanakshahi', '0501-10-19'], - ['nepali', '2026-09-17'], - ['persian', '1348-10-11'], - ['jalali', '1348-10-11'], - ['taiwan', '0059-01-01'], - ['thai', '2513-01-01'], - ['ummalqura', '1389-10-23'] - ].forEach(function(v) { - var calendar = v[0], - dateStr = v[1]; - expect(Lib.ms2DateTime(0, 0, calendar)).toBe(dateStr, calendar); - expect(Lib.dateTime2ms(dateStr, calendar)).toBe(0, calendar); - - var expected_p1ms = dateStr + ' 00:00:00.0001', - expected_1s = dateStr + ' 00:00:01', - expected_1m = dateStr + ' 00:01', - expected_1h = dateStr + ' 01:00', - expected_lastinstant = dateStr + ' 23:59:59.9999'; - - var oneSec = 1000, - oneMin = 60 * oneSec, - oneHour = 60 * oneMin, - lastInstant = 24 * oneHour - 0.1; - - expect(Lib.ms2DateTime(0.1, 0, calendar)).toBe(expected_p1ms, calendar); - expect(Lib.ms2DateTime(oneSec, 0, calendar)).toBe(expected_1s, calendar); - expect(Lib.ms2DateTime(oneMin, 0, calendar)).toBe(expected_1m, calendar); - expect(Lib.ms2DateTime(oneHour, 0, calendar)).toBe(expected_1h, calendar); - expect(Lib.ms2DateTime(lastInstant, 0, calendar)).toBe(expected_lastinstant, calendar); - - expect(Lib.dateTime2ms(expected_p1ms, calendar)).toBe(0.1, calendar); - expect(Lib.dateTime2ms(expected_1s, calendar)).toBe(oneSec, calendar); - expect(Lib.dateTime2ms(expected_1m, calendar)).toBe(oneMin, calendar); - expect(Lib.dateTime2ms(expected_1h, calendar)).toBe(oneHour, calendar); - expect(Lib.dateTime2ms(expected_lastinstant, calendar)).toBe(lastInstant, calendar); - }); - }); + it("should accept Date objects within year +/-9999", function() { + [ + new Date(), + new Date(-9999, 0, 1), + new Date(9999, 11, 31, 23, 59, 59, 999), + new Date(-1, 0, 1), + new Date(323, 11, 30), + new Date(-456, 1, 2), + d1c, + new Date(2015, 8, 7, 23, 34, 45, 567) + ].forEach(function(v) { + expect(Lib.dateTime2ms(v)).toBe(+v - v.getTimezoneOffset() * 60000); + }); + }); - it('should contain canonical ticks sundays, ranges for all calendars', function() { - var calList = Object.keys(calendars.calendars).filter(function(v) { - return v !== 'gregorian'; - }); - - var canonicalTick = calComponent.CANONICAL_TICK, - canonicalSunday = calComponent.CANONICAL_SUNDAY, - dfltRange = calComponent.DFLTRANGE; - expect(Object.keys(canonicalTick).length).toBe(calList.length); - expect(Object.keys(canonicalSunday).length).toBe(calList.length); - expect(Object.keys(dfltRange).length).toBe(calList.length); - - calList.forEach(function(calendar) { - expect(Lib.dateTime2ms(canonicalTick[calendar], calendar)).toBeDefined(calendar); - var sunday = Lib.dateTime2ms(canonicalSunday[calendar], calendar); - // convert back implicitly with gregorian calendar - expect(Lib.formatDate(sunday, '%A')).toBe('Sunday', calendar); - - expect(Lib.dateTime2ms(dfltRange[calendar][0], calendar)).toBeDefined(calendar); - expect(Lib.dateTime2ms(dfltRange[calendar][1], calendar)).toBeDefined(calendar); - }); - }); + it("should not accept Date objects beyond our limits", function() { + [ + new Date(10000, 0, 1), + new Date(-10000, 11, 31, 23, 59, 59, 999) + ].forEach(function(v) { + expect(Lib.dateTime2ms(v)).toBeUndefined(v); + }); + }); - it('should handle Chinese intercalary months correctly', function() { - var intercalaryDates = [ - '1995-08i-01', - '1995-08i-29', - '1984-10i-15', - '2023-02i-29' - ]; - intercalaryDates.forEach(function(v) { - var ms = Lib.dateTime2ms(v, 'chinese'); - expect(Lib.ms2DateTime(ms, 0, 'chinese')).toBe(v); - - // should also work without leading zeros - var vShort = v.replace(/-0/g, '-'); - expect(Lib.dateTime2ms(vShort, 'chinese')).toBe(ms, vShort); - }); - - var badIntercalaryDates = [ - '1995-07i-01', - '1995-08i-30', - '1995-09i-01' - ]; - badIntercalaryDates.forEach(function(v) { - expect(Lib.dateTime2ms(v, 'chinese')).toBeUndefined(v); - }); - }); + it("should not accept invalid strings or other objects", function() { + [ + "", + 0, + 1, + 9, + -1, + -10, + -99, + 100, + 999, + -100, + -999, + 10000, + -10000, + 1.2, + -1.2, + 2015.1, + -1023.4, + NaN, + null, + undefined, + Infinity, + -Infinity, + {}, + { 1: "2014-01-01" }, + [], + [2016], + ["2015-11-23"], + "123-01-01", + "-756-01-01", + // 3-digit year + "10000-01-01", + "-10000-01-01", + // 5-digit year + "2015-00-01", + "2015-13-01", + "2015-001-01", + // bad month + "2015-01-00", + "2015-01-32", + "2015-02-29", + "2015-04-31", + "2015-01-001", + // bad day (incl non-leap year) + "2015-01-01 24:00", + "2015-01-01 -1:00", + "2015-01-01 001:00", + // bad hour + "2015-01-01 12:60", + "2015-01-01 12:-1", + "2015-01-01 12:001", + "2015-01-01 12:1", + // bad minute + "2015-01-01 12:00:60", + "2015-01-01 12:00:-1", + "2015-01-01 12:00:001", + "2015-01-01 12:00:1", + // bad second + "2015-01-01T", + "2015-01-01TT12:34", + // bad ISO separators + "2015-01-01Z", + "2015-01-01T12Z", + "2015-01-01T12:34Z05:00", + "2015-01-01 12:34+500", + // bad TZ info + "2015-01-01 12:34-5:00" + ].forEach(function(v) { + expect(Lib.dateTime2ms(v)).toBeUndefined(v); + }); }); - describe('cleanDate', function() { - it('should convert numbers or js Dates to strings based on local TZ', function() { - [ - new Date(0), - new Date(2000), - new Date(2000, 0, 1), - new Date(), - new Date(-9999, 0, 3), // we lose one day of range +/- tzoffset this way - new Date(9999, 11, 29, 23, 59, 59, 999) - ].forEach(function(v) { - var expected = Lib.ms2DateTime(Lib.dateTime2ms(v)); - expect(typeof expected).toBe('string'); - expect(Lib.cleanDate(v)).toBe(expected); - expect(Lib.cleanDate(+v)).toBe(expected); - expect(Lib.cleanDate(v, '2000-01-01')).toBe(expected); - }); + var JULY1MS = 181 * 24 * 3600 * 1000; + + it( + "should use UTC with no timezone offset or daylight saving time", + function() { + expect(Lib.dateTime2ms("1970-01-01")).toBe(0); + + // 181 days (and no DST hours) between jan 1 and july 1 in a non-leap-year + // 31 + 28 + 31 + 30 + 31 + 30 + expect(Lib.dateTime2ms("1970-07-01")).toBe(JULY1MS); + } + ); + + it( + "should interpret JS dates by local time, not by its getTime()", + function() { + // not really part of the test, just to make sure the test is meaningful + // the test should NOT be run in a UTC environment + var local0 = Number(new Date(1970, 0, 1)), + localjuly1 = Number(new Date(1970, 6, 1)); + expect([local0, localjuly1]).not.toEqual( + [0, JULY1MS], + "test must not run in UTC" + ); + // verify that there *is* daylight saving time in the test environment + expect(localjuly1 - local0).not.toEqual( + JULY1MS - 0, + "test must run in a timezone with DST" + ); + + // now repeat the previous test and show that we throw away + // timezone info from js dates + expect(Lib.dateTime2ms(new Date(1970, 0, 1))).toBe(0); + expect(Lib.dateTime2ms(new Date(1970, 6, 1))).toBe(JULY1MS); + } + ); + }); + + describe("ms2DateTime", function() { + it( + "should report the minimum fields with nonzero values, except minutes", + function() { + [ + "2016-01-01", + // we'll never report less than this bcs month and day are never zero + "2016-01-01 01:00", + // we won't report hours without minutes + "2016-01-01 01:01", + "2016-01-01 01:01:01", + "2016-01-01 01:01:01.1", + "2016-01-01 01:01:01.01", + "2016-01-01 01:01:01.001", + "2016-01-01 01:01:01.0001" + ].forEach(function(v) { + expect(Lib.ms2DateTime(Lib.dateTime2ms(v))).toBe(v); }); + } + ); + + it("should accept Date objects within year +/-9999", function() { + [ + "-9999-01-01", + "-9999-01-01 00:00:00.0001", + "9999-12-31 23:59:59.9999", + "0123-01-01", + "0042-01-01", + "-0016-01-01", + "-0016-01-01 12:34:56.7891", + "-0456-07-23 16:22" + ].forEach(function(v) { + expect(Lib.ms2DateTime(Lib.dateTime2ms(v))).toBe(v); + }); + }); - it('should fail numbers & js Dates out of range, and other bad objects', function() { - [ - new Date(-20000, 0, 1), - new Date(20000, 0, 1), - new Date('fail'), - undefined, null, NaN, - [], {}, [0], {1: 2}, '', - '2001-02-29' // not a leap year - ].forEach(function(v) { - expect(Lib.cleanDate(v)).toBeUndefined(); - if(!isNumeric(+v)) expect(Lib.cleanDate(+v)).toBeUndefined(); - expect(Lib.cleanDate(v, '2000-01-01')).toBe('2000-01-01'); - }); + it( + "should not accept Date objects beyond our limits or other objects", + function() { + [ + Date.UTC(10000, 0, 1), + Date.UTC(-10000, 11, 31, 23, 59, 59, 999), + "", + "2016-01-01", + "0", + [], + [0], + {}, + { 1: 2 } + ].forEach(function(v) { + expect(Lib.ms2DateTime(v)).toBeUndefined(v); }); + } + ); + + it("should drop the right pieces if rounding is specified", function() { + [ + ["2016-01-01 00:00:00.0001", 0, "2016-01-01 00:00:00.0001"], + ["2016-01-01 00:00:00.0001", 299999, "2016-01-01 00:00:00.0001"], + ["2016-01-01 00:00:00.0001", 300000, "2016-01-01"], + ["2016-01-01 00:00:00.0001", 7776000000, "2016-01-01"], + ["2016-01-01 12:34:56.7891", 0, "2016-01-01 12:34:56.7891"], + ["2016-01-01 12:34:56.7891", 299999, "2016-01-01 12:34:56.7891"], + ["2016-01-01 12:34:56.7891", 300000, "2016-01-01 12:34:56"], + ["2016-01-01 12:34:56.7891", 10799999, "2016-01-01 12:34:56"], + ["2016-01-01 12:34:56.7891", 10800000, "2016-01-01 12:34"], + ["2016-01-01 12:34:56.7891", 7775999999, "2016-01-01 12:34"], + ["2016-01-01 12:34:56.7891", 7776000000, "2016-01-01"], + ["2016-01-01 12:34:56.7891", 1e300, "2016-01-01"] + ].forEach(function(v) { + expect(Lib.ms2DateTime(Lib.dateTime2ms(v[0]), v[1])).toBe(v[2], v); + }); + }); - it('should not alter valid date strings, even to truncate them', function() { - [ - '2000', - '2000-01', - '2000-01-01', - '2000-01-01 00', - '2000-01-01 00:00', - '2000-01-01 00:00:00', - '2000-01-01 00:00:00.0', - '2000-01-01 00:00:00.00', - '2000-01-01 00:00:00.000', - '2000-01-01 00:00:00.0000', - '9999-12-31 23:59:59.9999', - '-9999-01-01 00:00:00.0000', - '99-01-01', - '00-01-01' - ].forEach(function(v) { - expect(Lib.cleanDate(v)).toBe(v); - }); - }); + it("should work right with inputs beyond our precision", function() { + for (var i = -1; i <= 1; i += 0.001) { + var tenths = Math.round(i * 10), + base = i < -0.05 + ? "1969-12-31 23:59:59.99" + : "1970-01-01 00:00:00.00", + expected = (base + String(tenths + 200).substr(1)) + .replace(/0+$/, "") + .replace(/ 00:00:00[\.]$/, ""); + expect(Lib.ms2DateTime(i)).toBe(expected, i); + } + }); + }); + + describe("world calendar inputs", function() { + it("should give the right values near epoch zero", function() { + [ + [undefined, "1970-01-01"], + ["gregorian", "1970-01-01"], + ["chinese", "1969-11-24"], + ["coptic", "1686-04-23"], + ["discworld", "1798-12-27"], + ["ethiopian", "1962-04-23"], + ["hebrew", "5730-10-23"], + ["islamic", "1389-10-22"], + ["julian", "1969-12-19"], + ["mayan", "5156-07-05"], + ["nanakshahi", "0501-10-19"], + ["nepali", "2026-09-17"], + ["persian", "1348-10-11"], + ["jalali", "1348-10-11"], + ["taiwan", "0059-01-01"], + ["thai", "2513-01-01"], + ["ummalqura", "1389-10-23"] + ].forEach(function(v) { + var calendar = v[0], dateStr = v[1]; + expect(Lib.ms2DateTime(0, 0, calendar)).toBe(dateStr, calendar); + expect(Lib.dateTime2ms(dateStr, calendar)).toBe(0, calendar); + + var expected_p1ms = dateStr + " 00:00:00.0001", + expected_1s = dateStr + " 00:00:01", + expected_1m = dateStr + " 00:01", + expected_1h = dateStr + " 01:00", + expected_lastinstant = dateStr + " 23:59:59.9999"; + + var oneSec = 1000, + oneMin = 60 * oneSec, + oneHour = 60 * oneMin, + lastInstant = 24 * oneHour - 0.1; + + expect(Lib.ms2DateTime(0.1, 0, calendar)).toBe(expected_p1ms, calendar); + expect(Lib.ms2DateTime(oneSec, 0, calendar)).toBe( + expected_1s, + calendar + ); + expect(Lib.ms2DateTime(oneMin, 0, calendar)).toBe( + expected_1m, + calendar + ); + expect(Lib.ms2DateTime(oneHour, 0, calendar)).toBe( + expected_1h, + calendar + ); + expect(Lib.ms2DateTime(lastInstant, 0, calendar)).toBe( + expected_lastinstant, + calendar + ); + + expect(Lib.dateTime2ms(expected_p1ms, calendar)).toBe(0.1, calendar); + expect(Lib.dateTime2ms(expected_1s, calendar)).toBe(oneSec, calendar); + expect(Lib.dateTime2ms(expected_1m, calendar)).toBe(oneMin, calendar); + expect(Lib.dateTime2ms(expected_1h, calendar)).toBe(oneHour, calendar); + expect(Lib.dateTime2ms(expected_lastinstant, calendar)).toBe( + lastInstant, + calendar + ); + }); }); - describe('incrementMonth', function() { - it('should include Chinese intercalary months', function() { - var start = '1995-06-01'; - var expected = [ - '1995-07-01', - '1995-08-01', - '1995-08i-01', - '1995-09-01', - '1995-10-01', - '1995-11-01', - '1995-12-01', - '1996-01-01' - ]; - var tick = Lib.dateTime2ms(start, 'chinese'); - expected.forEach(function(v) { - tick = Lib.incrementMonth(tick, 1, 'chinese'); - expect(tick).toBe(Lib.dateTime2ms(v, 'chinese'), v); - }); + it( + "should contain canonical ticks sundays, ranges for all calendars", + function() { + var calList = Object.keys(calendars.calendars).filter(function(v) { + return v !== "gregorian"; }); - it('should increment years even over leap years', function() { - var start = '1995-06-01'; - var expected = [ - '1996-06-01', - '1997-06-01', - '1998-06-01', - '1999-06-01', - '2000-06-01', - '2001-06-01', - '2002-06-01', - '2003-06-01', - '2004-06-01', - '2005-06-01', - '2006-06-01', - '2007-06-01', - '2008-06-01' - ]; - var tick = Lib.dateTime2ms(start, 'chinese'); - expected.forEach(function(v) { - tick = Lib.incrementMonth(tick, 12, 'chinese'); - expect(tick).toBe(Lib.dateTime2ms(v, 'chinese'), v); - }); + var canonicalTick = calComponent.CANONICAL_TICK, + canonicalSunday = calComponent.CANONICAL_SUNDAY, + dfltRange = calComponent.DFLTRANGE; + expect(Object.keys(canonicalTick).length).toBe(calList.length); + expect(Object.keys(canonicalSunday).length).toBe(calList.length); + expect(Object.keys(dfltRange).length).toBe(calList.length); + + calList.forEach(function(calendar) { + expect( + Lib.dateTime2ms(canonicalTick[calendar], calendar) + ).toBeDefined(calendar); + var sunday = Lib.dateTime2ms(canonicalSunday[calendar], calendar); + // convert back implicitly with gregorian calendar + expect(Lib.formatDate(sunday, "%A")).toBe("Sunday", calendar); + + expect(Lib.dateTime2ms(dfltRange[calendar][0], calendar)).toBeDefined( + calendar + ); + expect(Lib.dateTime2ms(dfltRange[calendar][1], calendar)).toBeDefined( + calendar + ); }); + } + ); + + it("should handle Chinese intercalary months correctly", function() { + var intercalaryDates = [ + "1995-08i-01", + "1995-08i-29", + "1984-10i-15", + "2023-02i-29" + ]; + intercalaryDates.forEach(function(v) { + var ms = Lib.dateTime2ms(v, "chinese"); + expect(Lib.ms2DateTime(ms, 0, "chinese")).toBe(v); + + // should also work without leading zeros + var vShort = v.replace(/-0/g, "-"); + expect(Lib.dateTime2ms(vShort, "chinese")).toBe(ms, vShort); + }); + + var badIntercalaryDates = ["1995-07i-01", "1995-08i-30", "1995-09i-01"]; + badIntercalaryDates.forEach(function(v) { + expect(Lib.dateTime2ms(v, "chinese")).toBeUndefined(v); + }); }); - - describe('isJSDate', function() { - it('should return true for any Date object but not the equivalent numbers', function() { - [ - new Date(), - new Date(0), - new Date(-9900, 1, 2, 3, 4, 5, 6), - new Date(9900, 1, 2, 3, 4, 5, 6), - new Date(-20000, 0, 1), new Date(20000, 0, 1), // outside our range, still true - new Date('fail') // `Invalid Date` is still a Date - ].forEach(function(v) { - expect(Lib.isJSDate(v)).toBe(true); - expect(Lib.isJSDate(+v)).toBe(false); - }); + }); + + describe("cleanDate", function() { + it( + "should convert numbers or js Dates to strings based on local TZ", + function() { + [ + new Date(0), + new Date(2000), + new Date(2000, 0, 1), + new Date(), + new Date(-9999, 0, 3), + // we lose one day of range +/- tzoffset this way + new Date(9999, 11, 29, 23, 59, 59, 999) + ].forEach(function(v) { + var expected = Lib.ms2DateTime(Lib.dateTime2ms(v)); + expect(typeof expected).toBe("string"); + expect(Lib.cleanDate(v)).toBe(expected); + expect(Lib.cleanDate(+v)).toBe(expected); + expect(Lib.cleanDate(v, "2000-01-01")).toBe(expected); }); - - it('should return false for anything thats not explicitly a JS Date', function() { - [ - 0, NaN, null, undefined, '', {}, [], [0], [2016, 0, 1], - '2016-01-01', '2016-01-01 12:34:56', '2016-01-01 12:34:56.789', - 'Thu Oct 20 2016 15:35:14 GMT-0400 (EDT)', - // getting really close to a hack of our test... we look for getTime to be a function - {getTime: 4} - ].forEach(function(v) { - expect(Lib.isJSDate(v)).toBe(false); - }); + } + ); + + it( + "should fail numbers & js Dates out of range, and other bad objects", + function() { + [ + new Date(-20000, 0, 1), + new Date(20000, 0, 1), + new Date("fail"), + undefined, + null, + NaN, + [], + {}, + [0], + { 1: 2 }, + "", + // not a leap year + "2001-02-29" + ].forEach(function(v) { + expect(Lib.cleanDate(v)).toBeUndefined(); + if (!isNumeric(+v)) expect(Lib.cleanDate(+v)).toBeUndefined(); + expect(Lib.cleanDate(v, "2000-01-01")).toBe("2000-01-01"); }); - }); - - describe('formatDate', function() { - function assertFormatRounds(ms, calendar, results) { - ['y', 'm', 'd', 'M', 'S', 1, 2, 3, 4].forEach(function(tr, i) { - expect(Lib.formatDate(ms, '', tr, calendar)) - .toBe(results[i], calendar); - }); - } - - it('should pick a format based on tickround if no format is provided', function() { - var ms = Lib.dateTime2ms('2012-08-13 06:19:34.5678'); - assertFormatRounds(ms, 'gregorian', [ - '2012', - 'Aug 2012', - 'Aug 13\n2012', - '06:19\nAug 13, 2012', - '06:19:35\nAug 13, 2012', - '06:19:34.6\nAug 13, 2012', - '06:19:34.57\nAug 13, 2012', - '06:19:34.568\nAug 13, 2012', - '06:19:34.5678\nAug 13, 2012' - ]); - - // and for world calendars - in coptic this is 1728-12-07 (month=Meso) - assertFormatRounds(ms, 'coptic', [ - '1728', - 'Meso 1728', - 'Meso 7\n1728', - '06:19\nMeso 7, 1728', - '06:19:35\nMeso 7, 1728', - '06:19:34.6\nMeso 7, 1728', - '06:19:34.57\nMeso 7, 1728', - '06:19:34.568\nMeso 7, 1728', - '06:19:34.5678\nMeso 7, 1728' - ]); + } + ); + + it( + "should not alter valid date strings, even to truncate them", + function() { + [ + "2000", + "2000-01", + "2000-01-01", + "2000-01-01 00", + "2000-01-01 00:00", + "2000-01-01 00:00:00", + "2000-01-01 00:00:00.0", + "2000-01-01 00:00:00.00", + "2000-01-01 00:00:00.000", + "2000-01-01 00:00:00.0000", + "9999-12-31 23:59:59.9999", + "-9999-01-01 00:00:00.0000", + "99-01-01", + "00-01-01" + ].forEach(function(v) { + expect(Lib.cleanDate(v)).toBe(v); }); + } + ); + }); + + describe("incrementMonth", function() { + it("should include Chinese intercalary months", function() { + var start = "1995-06-01"; + var expected = [ + "1995-07-01", + "1995-08-01", + "1995-08i-01", + "1995-09-01", + "1995-10-01", + "1995-11-01", + "1995-12-01", + "1996-01-01" + ]; + var tick = Lib.dateTime2ms(start, "chinese"); + expected.forEach(function(v) { + tick = Lib.incrementMonth(tick, 1, "chinese"); + expect(tick).toBe(Lib.dateTime2ms(v, "chinese"), v); + }); + }); - it('should accept custom formats using d3 specs even for world cals', function() { - var ms = Lib.dateTime2ms('2012-08-13 06:19:34.5678'); - [ - // some common formats (plotly workspace options) - ['%Y-%m-%d', '2012-08-13', '1728-12-07'], - ['%H:%M:%S', '06:19:34', '06:19:34'], - ['%Y-%m-%e %H:%M:%S', '2012-08-13 06:19:34', '1728-12-7 06:19:34'], - ['%A, %b %e', 'Monday, Aug 13', 'Pesnau, Meso 7'], - - // test padding behavior - // world doesn't support space-padded (yet?) - ['%Y-%_m-%_d', '2012- 8-13', '1728-12-7'], - ['%Y-%-m-%-d', '2012-8-13', '1728-12-7'], - - // and some strange ones to cover all fields - ['%a%j!%-j', 'Mon226!226', 'Pes337!337'], - [ - '%W or un or space padded-> %-W,%_W', - '33 or un or space padded-> 33,33', - '48 or un or space padded-> 48,48' - ], - [ - '%B \'%y WOY:%U DOW:%w', - 'August \'12 WOY:32 DOW:1', - 'Mesori \'28 WOY:## DOW:##' // world-cals doesn't support U or w - ], - [ - '%c && %x && .%2f .%f', // %f is our addition - 'Mon Aug 13 06:19:34 2012 && 08/13/2012 && .57 .5678', - 'Pes Meso 7 06:19:34 1728 && 12/07/1728 && .57 .5678' - ] - - ].forEach(function(v) { - var fmt = v[0], - expectedGregorian = v[1], - expectedCoptic = v[2]; - - // tickround is irrelevant here... - expect(Lib.formatDate(ms, fmt, 'y')) - .toBe(expectedGregorian, fmt); - expect(Lib.formatDate(ms, fmt, 4, 'gregorian')) - .toBe(expectedGregorian, fmt); - expect(Lib.formatDate(ms, fmt, 'y', 'coptic')) - .toBe(expectedCoptic, fmt); - }); + it("should increment years even over leap years", function() { + var start = "1995-06-01"; + var expected = [ + "1996-06-01", + "1997-06-01", + "1998-06-01", + "1999-06-01", + "2000-06-01", + "2001-06-01", + "2002-06-01", + "2003-06-01", + "2004-06-01", + "2005-06-01", + "2006-06-01", + "2007-06-01", + "2008-06-01" + ]; + var tick = Lib.dateTime2ms(start, "chinese"); + expected.forEach(function(v) { + tick = Lib.incrementMonth(tick, 12, "chinese"); + expect(tick).toBe(Lib.dateTime2ms(v, "chinese"), v); + }); + }); + }); + + describe("isJSDate", function() { + it( + "should return true for any Date object but not the equivalent numbers", + function() { + [ + new Date(), + new Date(0), + new Date(-9900, 1, 2, 3, 4, 5, 6), + new Date(9900, 1, 2, 3, 4, 5, 6), + new Date(-20000, 0, 1), + new Date(20000, 0, 1), + // outside our range, still true + // `Invalid Date` is still a Date + new Date("fail") + ].forEach(function(v) { + expect(Lib.isJSDate(v)).toBe(true); + expect(Lib.isJSDate(+v)).toBe(false); }); - - it('should not round up to 60 seconds', function() { - // see note in dates.js -> formatTime about this rounding - assertFormatRounds(-0.1, 'gregorian', [ - '1969', - 'Dec 1969', - 'Dec 31\n1969', - '23:59\nDec 31, 1969', - '23:59:59\nDec 31, 1969', - '23:59:59.9\nDec 31, 1969', - '23:59:59.99\nDec 31, 1969', - '23:59:59.999\nDec 31, 1969', - '23:59:59.9999\nDec 31, 1969' - ]); - - // in coptic this is Koi 22, 1686 - assertFormatRounds(-0.1, 'coptic', [ - '1686', - 'Koi 1686', - 'Koi 22\n1686', - '23:59\nKoi 22, 1686', - '23:59:59\nKoi 22, 1686', - '23:59:59.9\nKoi 22, 1686', - '23:59:59.99\nKoi 22, 1686', - '23:59:59.999\nKoi 22, 1686', - '23:59:59.9999\nKoi 22, 1686' - ]); - - // and using the custom format machinery - expect(Lib.formatDate(-0.1, '%Y-%m-%d %H:%M:%S.%f')) - .toBe('1969-12-31 23:59:59.9999'); - expect(Lib.formatDate(-0.1, '%Y-%m-%d %H:%M:%S.%f', null, 'coptic')) - .toBe('1686-04-22 23:59:59.9999'); - + } + ); + + it( + "should return false for anything thats not explicitly a JS Date", + function() { + [ + 0, + NaN, + null, + undefined, + "", + {}, + [], + [0], + [2016, 0, 1], + "2016-01-01", + "2016-01-01 12:34:56", + "2016-01-01 12:34:56.789", + "Thu Oct 20 2016 15:35:14 GMT-0400 (EDT)", + // getting really close to a hack of our test... we look for getTime to be a function + { getTime: 4 } + ].forEach(function(v) { + expect(Lib.isJSDate(v)).toBe(false); }); - - it('should remove extra fractional second zeros', function() { - expect(Lib.formatDate(0.1, '', 4)).toBe('00:00:00.0001\nJan 1, 1970'); - expect(Lib.formatDate(0.1, '', 3)).toBe('00:00:00\nJan 1, 1970'); - expect(Lib.formatDate(0.1, '', 0)).toBe('00:00:00\nJan 1, 1970'); - expect(Lib.formatDate(0.1, '', 'S')).toBe('00:00:00\nJan 1, 1970'); - expect(Lib.formatDate(0.1, '', 3, 'coptic')) - .toBe('00:00:00\nKoi 23, 1686'); - - // because the decimal point is explicitly part of the format - // string here, we can't remove it OR the very first zero after it. - expect(Lib.formatDate(0.1, '%S.%f')).toBe('00.0001'); - expect(Lib.formatDate(0.1, '%S.%3f')).toBe('00.0'); + } + ); + }); + + describe("formatDate", function() { + function assertFormatRounds(ms, calendar, results) { + ["y", "m", "d", "M", "S", 1, 2, 3, 4].forEach(function(tr, i) { + expect(Lib.formatDate(ms, "", tr, calendar)).toBe(results[i], calendar); + }); + } + + it( + "should pick a format based on tickround if no format is provided", + function() { + var ms = Lib.dateTime2ms("2012-08-13 06:19:34.5678"); + assertFormatRounds(ms, "gregorian", [ + "2012", + "Aug 2012", + "Aug 13\n2012", + "06:19\nAug 13, 2012", + "06:19:35\nAug 13, 2012", + "06:19:34.6\nAug 13, 2012", + "06:19:34.57\nAug 13, 2012", + "06:19:34.568\nAug 13, 2012", + "06:19:34.5678\nAug 13, 2012" + ]); + + // and for world calendars - in coptic this is 1728-12-07 (month=Meso) + assertFormatRounds(ms, "coptic", [ + "1728", + "Meso 1728", + "Meso 7\n1728", + "06:19\nMeso 7, 1728", + "06:19:35\nMeso 7, 1728", + "06:19:34.6\nMeso 7, 1728", + "06:19:34.57\nMeso 7, 1728", + "06:19:34.568\nMeso 7, 1728", + "06:19:34.5678\nMeso 7, 1728" + ]); + } + ); + + it( + "should accept custom formats using d3 specs even for world cals", + function() { + var ms = Lib.dateTime2ms("2012-08-13 06:19:34.5678"); + [ + // some common formats (plotly workspace options) + ["%Y-%m-%d", "2012-08-13", "1728-12-07"], + ["%H:%M:%S", "06:19:34", "06:19:34"], + ["%Y-%m-%e %H:%M:%S", "2012-08-13 06:19:34", "1728-12-7 06:19:34"], + ["%A, %b %e", "Monday, Aug 13", "Pesnau, Meso 7"], + // test padding behavior + // world doesn't support space-padded (yet?) + ["%Y-%_m-%_d", "2012- 8-13", "1728-12-7"], + ["%Y-%-m-%-d", "2012-8-13", "1728-12-7"], + // and some strange ones to cover all fields + ["%a%j!%-j", "Mon226!226", "Pes337!337"], + [ + "%W or un or space padded-> %-W,%_W", + "33 or un or space padded-> 33,33", + "48 or un or space padded-> 48,48" + ], + [ + "%B '%y WOY:%U DOW:%w", + "August '12 WOY:32 DOW:1", + // world-cals doesn't support U or w + "Mesori '28 WOY:## DOW:##" + ], + [ + "%c && %x && .%2f .%f", + // %f is our addition + "Mon Aug 13 06:19:34 2012 && 08/13/2012 && .57 .5678", + "Pes Meso 7 06:19:34 1728 && 12/07/1728 && .57 .5678" + ] + ].forEach(function(v) { + var fmt = v[0], expectedGregorian = v[1], expectedCoptic = v[2]; + + // tickround is irrelevant here... + expect(Lib.formatDate(ms, fmt, "y")).toBe(expectedGregorian, fmt); + expect(Lib.formatDate(ms, fmt, 4, "gregorian")).toBe( + expectedGregorian, + fmt + ); + expect(Lib.formatDate(ms, fmt, "y", "coptic")).toBe( + expectedCoptic, + fmt + ); }); + } + ); + + it("should not round up to 60 seconds", function() { + // see note in dates.js -> formatTime about this rounding + assertFormatRounds(-0.1, "gregorian", [ + "1969", + "Dec 1969", + "Dec 31\n1969", + "23:59\nDec 31, 1969", + "23:59:59\nDec 31, 1969", + "23:59:59.9\nDec 31, 1969", + "23:59:59.99\nDec 31, 1969", + "23:59:59.999\nDec 31, 1969", + "23:59:59.9999\nDec 31, 1969" + ]); + + // in coptic this is Koi 22, 1686 + assertFormatRounds(-0.1, "coptic", [ + "1686", + "Koi 1686", + "Koi 22\n1686", + "23:59\nKoi 22, 1686", + "23:59:59\nKoi 22, 1686", + "23:59:59.9\nKoi 22, 1686", + "23:59:59.99\nKoi 22, 1686", + "23:59:59.999\nKoi 22, 1686", + "23:59:59.9999\nKoi 22, 1686" + ]); + + // and using the custom format machinery + expect(Lib.formatDate(-0.1, "%Y-%m-%d %H:%M:%S.%f")).toBe( + "1969-12-31 23:59:59.9999" + ); + expect(Lib.formatDate(-0.1, "%Y-%m-%d %H:%M:%S.%f", null, "coptic")).toBe( + "1686-04-22 23:59:59.9999" + ); + }); + it("should remove extra fractional second zeros", function() { + expect(Lib.formatDate(0.1, "", 4)).toBe("00:00:00.0001\nJan 1, 1970"); + expect(Lib.formatDate(0.1, "", 3)).toBe("00:00:00\nJan 1, 1970"); + expect(Lib.formatDate(0.1, "", 0)).toBe("00:00:00\nJan 1, 1970"); + expect(Lib.formatDate(0.1, "", "S")).toBe("00:00:00\nJan 1, 1970"); + expect(Lib.formatDate(0.1, "", 3, "coptic")).toBe( + "00:00:00\nKoi 23, 1686" + ); + + // because the decimal point is explicitly part of the format + // string here, we can't remove it OR the very first zero after it. + expect(Lib.formatDate(0.1, "%S.%f")).toBe("00.0001"); + expect(Lib.formatDate(0.1, "%S.%3f")).toBe("00.0"); }); + }); }); diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 7181510d4cc..ef45629d6c6 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1,606 +1,602 @@ -var Lib = require('@src/lib'); -var setCursor = require('@src/lib/setcursor'); -var overrideCursor = require('@src/lib/override_cursor'); -var config = require('@src/plot_api/plot_config'); - -var d3 = require('d3'); -var Plotly = require('@lib'); -var PlotlyInternal = require('@src/plotly'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var Lib = require("@src/lib"); +var setCursor = require("@src/lib/setcursor"); +var overrideCursor = require("@src/lib/override_cursor"); +var config = require("@src/plot_api/plot_config"); + +var d3 = require("d3"); +var Plotly = require("@lib"); +var PlotlyInternal = require("@src/plotly"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); var Plots = PlotlyInternal.Plots; -var customMatchers = require('../assets/custom_matchers'); - -describe('Test lib.js:', function() { - 'use strict'; - - describe('interp() should', function() { - it('return 1.75 as Q1 of [1, 2, 3, 4, 5]:', function() { - var input = [1, 2, 3, 4, 5], - res = Lib.interp(input, 0.25), - res0 = 1.75; - expect(res).toEqual(res0); - }); - it('return 4.25 as Q3 of [1, 2, 3, 4, 5]:', function() { - var input = [1, 2, 3, 4, 5], - res = Lib.interp(input, 0.75), - res0 = 4.25; - expect(res).toEqual(res0); - }); - it('error if second input argument is a string:', function() { - var input = [1, 2, 3, 4, 5]; - expect(function() { - Lib.interp(input, 'apple'); - }).toThrow('n should be a finite number'); - }); - it('error if second input argument is a date:', function() { - var in1 = [1, 2, 3, 4, 5], - in2 = new Date(2014, 11, 1); - expect(function() { - Lib.interp(in1, in2); - }).toThrow('n should be a finite number'); - }); - it('return the right boundary on input [-Inf, Inf]:', function() { - var input = [-Infinity, Infinity], - res = Lib.interp(input, 1), - res0 = Infinity; - expect(res).toEqual(res0); - }); +var customMatchers = require("../assets/custom_matchers"); + +describe("Test lib.js:", function() { + "use strict"; + describe("interp() should", function() { + it("return 1.75 as Q1 of [1, 2, 3, 4, 5]:", function() { + var input = [1, 2, 3, 4, 5], res = Lib.interp(input, 0.25), res0 = 1.75; + expect(res).toEqual(res0); }); - - describe('transposeRagged()', function() { - it('should transpose and return a rectangular array', function() { - var input = [ - [1], - [2, 3, 4], - [5, 6], - [7]], - output = [ - [1, 2, 5, 7], - [undefined, 3, 6, undefined], - [undefined, 4, undefined, undefined] - ]; - - expect(Lib.transposeRagged(input)).toEqual(output); - }); + it("return 4.25 as Q3 of [1, 2, 3, 4, 5]:", function() { + var input = [1, 2, 3, 4, 5], res = Lib.interp(input, 0.75), res0 = 4.25; + expect(res).toEqual(res0); }); - - describe('dot()', function() { - var dot = Lib.dot; - - it('should return null for empty or unequal-length inputs', function() { - expect(dot([], [])).toBeNull(); - expect(dot([1], [2, 3])).toBeNull(); - }); - - it('should dot vectors to a scalar', function() { - expect(dot([1, 2, 3], [4, 5, 6])).toEqual(32); - }); - - it('should dot a vector and a matrix to a vector', function() { - expect(dot([1, 2], [[3, 4], [5, 6]])).toEqual([13, 16]); - expect(dot([[3, 4], [5, 6]], [1, 2])).toEqual([11, 17]); - }); - - it('should dot two matrices to a matrix', function() { - expect(dot([[1, 2], [3, 4]], [[5, 6], [7, 8]])) - .toEqual([[19, 22], [43, 50]]); - }); + it("error if second input argument is a string:", function() { + var input = [1, 2, 3, 4, 5]; + expect(function() { + Lib.interp(input, "apple"); + }).toThrow("n should be a finite number"); }); + it("error if second input argument is a date:", function() { + var in1 = [1, 2, 3, 4, 5], in2 = new Date(2014, 11, 1); + expect(function() { + Lib.interp(in1, in2); + }).toThrow("n should be a finite number"); + }); + it("return the right boundary on input [-Inf, Inf]:", function() { + var input = [-Infinity, Infinity], + res = Lib.interp(input, 1), + res0 = Infinity; + expect(res).toEqual(res0); + }); + }); + + describe("transposeRagged()", function() { + it("should transpose and return a rectangular array", function() { + var input = [[1], [2, 3, 4], [5, 6], [7]], + output = [ + [1, 2, 5, 7], + [undefined, 3, 6, undefined], + [undefined, 4, undefined, undefined] + ]; + + expect(Lib.transposeRagged(input)).toEqual(output); + }); + }); - describe('aggNums()', function() { - var aggNums = Lib.aggNums; - - function summation(a, b) { return a + b; } - - it('should work with 1D and 2D inputs and ignore non-numerics', function() { - var in1D = [1, 2, 3, 4, 'goose!', 5, 6], - in2D = [[1, 2, 3], ['', 4], [5, 'hi!', 6]]; - - expect(aggNums(Math.min, null, in1D)).toEqual(1); - expect(aggNums(Math.min, null, in2D)).toEqual(1); - - expect(aggNums(Math.max, null, in1D)).toEqual(6); - expect(aggNums(Math.max, null, in2D)).toEqual(6); + describe("dot()", function() { + var dot = Lib.dot; - expect(aggNums(summation, 0, in1D)).toEqual(21); - expect(aggNums(summation, 0, in2D)).toEqual(21); - }); + it("should return null for empty or unequal-length inputs", function() { + expect(dot([], [])).toBeNull(); + expect(dot([1], [2, 3])).toBeNull(); }); - describe('mean() should', function() { - it('toss out non-numerics (strings):', function() { - var input = [1, 2, 'apple', 'orange'], - res = Lib.mean(input); - expect(res).toEqual(1.5); - }); - it('toss out non-numerics (NaN):', function() { - var input = [1, 2, NaN], - res = Lib.mean(input); - expect(res).toEqual(1.5); - }); - it('evaluate numbers which are passed around as text strings:', function() { - var input = ['1', '2'], - res = Lib.mean(input); - expect(res).toEqual(1.5); - }); + it("should dot vectors to a scalar", function() { + expect(dot([1, 2, 3], [4, 5, 6])).toEqual(32); }); - describe('variance() should', function() { - it('return 0 on input [2, 2, 2, 2, 2]:', function() { - var input = [2, 2, 2, 2], - res = Lib.variance(input); - expect(res).toEqual(0); - }); - it('return 2/3 on input [-1, 0, 1]:', function() { - var input = [-1, 0, 1], - res = Lib.variance(input); - expect(res).toEqual(2 / 3); - }); - it('toss out non-numerics (strings):', function() { - var input = [1, 2, 'apple', 'orange'], - res = Lib.variance(input); - expect(res).toEqual(0.25); - }); - it('toss out non-numerics (NaN):', function() { - var input = [1, 2, NaN], - res = Lib.variance(input); - expect(res).toEqual(0.25); - }); + it("should dot a vector and a matrix to a vector", function() { + expect(dot([1, 2], [[3, 4], [5, 6]])).toEqual([13, 16]); + expect(dot([[3, 4], [5, 6]], [1, 2])).toEqual([11, 17]); }); - describe('stdev() should', function() { - it('return 0 on input [2, 2, 2, 2, 2]:', function() { - var input = [2, 2, 2, 2], - res = Lib.stdev(input); - expect(res).toEqual(0); - }); - it('return sqrt(2/3) on input [-1, 0, 1]:', function() { - var input = [-1, 0, 1], - res = Lib.stdev(input); - expect(res).toEqual(Math.sqrt(2 / 3)); - }); - it('toss out non-numerics (strings):', function() { - var input = [1, 2, 'apple', 'orange'], - res = Lib.stdev(input); - expect(res).toEqual(0.5); - }); - it('toss out non-numerics (NaN):', function() { - var input = [1, 2, NaN], - res = Lib.stdev(input); - expect(res).toEqual(0.5); - }); + it("should dot two matrices to a matrix", function() { + expect(dot([[1, 2], [3, 4]], [[5, 6], [7, 8]])).toEqual([ + [19, 22], + [43, 50] + ]); }); + }); - describe('smooth()', function() { - it('should not alter the input for FWHM < 1.5', function() { - var input = [1, 2, 1, 2, 1], - output = Lib.smooth(input.slice(), 1.49); + describe("aggNums()", function() { + var aggNums = Lib.aggNums; - expect(output).toEqual(input); + function summation(a, b) { + return a + b; + } - output = Lib.smooth(input.slice(), 'like butter'); + it("should work with 1D and 2D inputs and ignore non-numerics", function() { + var in1D = [1, 2, 3, 4, "goose!", 5, 6], + in2D = [[1, 2, 3], ["", 4], [5, "hi!", 6]]; - expect(output).toEqual(input); - }); + expect(aggNums(Math.min, null, in1D)).toEqual(1); + expect(aggNums(Math.min, null, in2D)).toEqual(1); - it('should preserve the length and integral even with multiple bounces', function() { - var input = [1, 2, 4, 8, 16, 8, 10, 12], - output2 = Lib.smooth(input.slice(), 2), - output30 = Lib.smooth(input.slice(), 30), - sumIn = 0, - sum2 = 0, - sum30 = 0; - - for(var i = 0; i < input.length; i++) { - sumIn += input[i]; - sum2 += output2[i]; - sum30 += output30[i]; - } + expect(aggNums(Math.max, null, in1D)).toEqual(6); + expect(aggNums(Math.max, null, in2D)).toEqual(6); - expect(output2.length).toEqual(input.length); - expect(output30.length).toEqual(input.length); - expect(sum2).toBeCloseTo(sumIn, 6); - expect(sum30).toBeCloseTo(sumIn, 6); - }); - - it('should use a hann window and bounce', function() { - var input = [0, 0, 0, 7, 0, 0, 0], - out4 = Lib.smooth(input, 4), - out7 = Lib.smooth(input, 7), - expected4 = [ - 0.2562815664617711, 0.875, 1.4937184335382292, 1.75, - 1.493718433538229, 0.875, 0.25628156646177086 - ], - expected7 = [1, 1, 1, 1, 1, 1, 1], - i; - - for(i = 0; i < input.length; i++) { - expect(out4[i]).toBeCloseTo(expected4[i], 6); - expect(out7[i]).toBeCloseTo(expected7[i], 6); - } - }); + expect(aggNums(summation, 0, in1D)).toEqual(21); + expect(aggNums(summation, 0, in2D)).toEqual(21); }); + }); - describe('nestedProperty', function() { - var np = Lib.nestedProperty; - - it('should access simple objects', function() { - var obj = {a: 'b', c: 'd'}, - propA = np(obj, 'a'), - propB = np(obj, 'b'); - - expect(propA.get()).toBe('b'); - // making and reading nestedProperties shouldn't change anything - expect(obj).toEqual({a: 'b', c: 'd'}); - // only setting them should - propA.set('cats'); - expect(obj).toEqual({a: 'cats', c: 'd'}); - expect(propA.get()).toBe('cats'); - propA.set('b'); - - expect(propB.get()).toBe(undefined); - expect(obj).toEqual({a: 'b', c: 'd'}); - propB.set({cats: true, dogs: false}); - expect(obj).toEqual({a: 'b', c: 'd', b: {cats: true, dogs: false}}); - }); - - it('should access arrays', function() { - var arr = [1, 2, 3], - prop1 = np(arr, 1), - prop5 = np(arr, '5'); - - expect(prop1.get()).toBe(2); - expect(arr).toEqual([1, 2, 3]); - - prop1.set('cats'); - expect(prop1.get()).toBe('cats'); - - prop1.set(2); - expect(prop5.get()).toBe(undefined); - expect(arr).toEqual([1, 2, 3]); - - prop5.set(5); - var localArr = [1, 2, 3]; - localArr[5] = 5; - expect(arr).toEqual(localArr); - - prop5.set(null); - expect(arr).toEqual([1, 2, 3]); - expect(arr.length).toBe(3); - }); - - it('should not access whole array elements with index -1', function() { - // for a lot of cases we could make this work, - // but deleting the value is a mess, and anyway - // we don't need this, it's better just to set the whole - // array, ie np(obj, 'arr') - var obj = {arr: [1, 2, 3]}; - expect(function() { np(obj, 'arr[-1]'); }).toThrow('bad property string'); - }); - - it('should access properties of objects in an array with index -1', function() { - var obj = {arr: [{a: 1}, {a: 2}, {b: 3}]}, - prop = np(obj, 'arr[-1].a'); + describe("mean() should", function() { + it("toss out non-numerics (strings):", function() { + var input = [1, 2, "apple", "orange"], res = Lib.mean(input); + expect(res).toEqual(1.5); + }); + it("toss out non-numerics (NaN):", function() { + var input = [1, 2, NaN], res = Lib.mean(input); + expect(res).toEqual(1.5); + }); + it("evaluate numbers which are passed around as text strings:", function() { + var input = ["1", "2"], res = Lib.mean(input); + expect(res).toEqual(1.5); + }); + }); - expect(prop.get()).toEqual([1, 2, undefined]); - expect(obj).toEqual({arr: [{a: 1}, {a: 2}, {b: 3}]}); + describe("variance() should", function() { + it("return 0 on input [2, 2, 2, 2, 2]:", function() { + var input = [2, 2, 2, 2], res = Lib.variance(input); + expect(res).toEqual(0); + }); + it("return 2/3 on input [-1, 0, 1]:", function() { + var input = [-1, 0, 1], res = Lib.variance(input); + expect(res).toEqual(2 / 3); + }); + it("toss out non-numerics (strings):", function() { + var input = [1, 2, "apple", "orange"], res = Lib.variance(input); + expect(res).toEqual(0.25); + }); + it("toss out non-numerics (NaN):", function() { + var input = [1, 2, NaN], res = Lib.variance(input); + expect(res).toEqual(0.25); + }); + }); - prop.set(5); - expect(prop.get()).toBe(5); - expect(obj).toEqual({arr: [{a: 5}, {a: 5}, {a: 5, b: 3}]}); + describe("stdev() should", function() { + it("return 0 on input [2, 2, 2, 2, 2]:", function() { + var input = [2, 2, 2, 2], res = Lib.stdev(input); + expect(res).toEqual(0); + }); + it("return sqrt(2/3) on input [-1, 0, 1]:", function() { + var input = [-1, 0, 1], res = Lib.stdev(input); + expect(res).toEqual(Math.sqrt(2 / 3)); + }); + it("toss out non-numerics (strings):", function() { + var input = [1, 2, "apple", "orange"], res = Lib.stdev(input); + expect(res).toEqual(0.5); + }); + it("toss out non-numerics (NaN):", function() { + var input = [1, 2, NaN], res = Lib.stdev(input); + expect(res).toEqual(0.5); + }); + }); - prop.set(null); - expect(prop.get()).toBe(undefined); - expect(obj).toEqual({arr: [undefined, undefined, {b: 3}]}); + describe("smooth()", function() { + it("should not alter the input for FWHM < 1.5", function() { + var input = [1, 2, 1, 2, 1], output = Lib.smooth(input.slice(), 1.49); - prop.set([2, 3, 4]); - expect(prop.get()).toEqual([2, 3, 4]); - expect(obj).toEqual({arr: [{a: 2}, {a: 3}, {a: 4, b: 3}]}); + expect(output).toEqual(input); - prop.set([6, 7, undefined]); - expect(prop.get()).toEqual([6, 7, undefined]); - expect(obj).toEqual({arr: [{a: 6}, {a: 7}, {b: 3}]}); + output = Lib.smooth(input.slice(), "like butter"); - // too short an array: wrap around - prop.set([9, 10]); - expect(prop.get()).toEqual([9, 10, 9]); - expect(obj).toEqual({arr: [{a: 9}, {a: 10}, {a: 9, b: 3}]}); + expect(output).toEqual(input); + }); - // too long an array: ignore extras - prop.set([11, 12, 13, 14]); - expect(prop.get()).toEqual([11, 12, 13]); - expect(obj).toEqual({arr: [{a: 11}, {a: 12}, {a: 13, b: 3}]}); - }); + it( + "should preserve the length and integral even with multiple bounces", + function() { + var input = [1, 2, 4, 8, 16, 8, 10, 12], + output2 = Lib.smooth(input.slice(), 2), + output30 = Lib.smooth(input.slice(), 30), + sumIn = 0, + sum2 = 0, + sum30 = 0; + + for (var i = 0; i < input.length; i++) { + sumIn += input[i]; + sum2 += output2[i]; + sum30 += output30[i]; + } - it('should remove a property only with undefined or null', function() { - var obj = {a: 'b', c: 'd'}, - propA = np(obj, 'a'), - propC = np(obj, 'c'); + expect(output2.length).toEqual(input.length); + expect(output30.length).toEqual(input.length); + expect(sum2).toBeCloseTo(sumIn, 6); + expect(sum30).toBeCloseTo(sumIn, 6); + } + ); + + it("should use a hann window and bounce", function() { + var input = [0, 0, 0, 7, 0, 0, 0], + out4 = Lib.smooth(input, 4), + out7 = Lib.smooth(input, 7), + expected4 = [ + 0.2562815664617711, + 0.875, + 1.4937184335382292, + 1.75, + 1.493718433538229, + 0.875, + 0.25628156646177086 + ], + expected7 = [1, 1, 1, 1, 1, 1, 1], + i; + + for (i = 0; i < input.length; i++) { + expect(out4[i]).toBeCloseTo(expected4[i], 6); + expect(out7[i]).toBeCloseTo(expected7[i], 6); + } + }); + }); + + describe("nestedProperty", function() { + var np = Lib.nestedProperty; + + it("should access simple objects", function() { + var obj = { a: "b", c: "d" }, propA = np(obj, "a"), propB = np(obj, "b"); + + expect(propA.get()).toBe("b"); + // making and reading nestedProperties shouldn't change anything + expect(obj).toEqual({ a: "b", c: "d" }); + // only setting them should + propA.set("cats"); + expect(obj).toEqual({ a: "cats", c: "d" }); + expect(propA.get()).toBe("cats"); + propA.set("b"); + + expect(propB.get()).toBe(undefined); + expect(obj).toEqual({ a: "b", c: "d" }); + propB.set({ cats: true, dogs: false }); + expect(obj).toEqual({ a: "b", c: "d", b: { cats: true, dogs: false } }); + }); - propA.set(null); - propC.set(undefined); - expect(obj).toEqual({}); + it("should access arrays", function() { + var arr = [1, 2, 3], prop1 = np(arr, 1), prop5 = np(arr, "5"); - propA.set(false); - np(obj, 'b').set(''); - propC.set(0); - np(obj, 'd').set(NaN); - expect(obj).toEqual({a: false, b: '', c: 0, d: NaN}); - }); + expect(prop1.get()).toBe(2); + expect(arr).toEqual([1, 2, 3]); - it('should remove containers but not data arrays', function() { - var obj = { - annotations: [{a: [1, 2, 3]}], - c: [1, 2, 3], - domain: [1, 2], - range: [2, 3], - shapes: ['elephant'] - }, - propA = np(obj, 'annotations[-1].a'), - propC = np(obj, 'c'), - propD0 = np(obj, 'domain[0]'), - propD1 = np(obj, 'domain[1]'), - propR = np(obj, 'range'), - propS = np(obj, 'shapes[0]'); - - propA.set([]); - propC.set([]); - propD0.set(undefined); - propD1.set(undefined); - propR.set([]); - propS.set(null); - - expect(obj).toEqual({c: []}); - }); + prop1.set("cats"); + expect(prop1.get()).toBe("cats"); + prop1.set(2); + expect(prop5.get()).toBe(undefined); + expect(arr).toEqual([1, 2, 3]); - it('should have no empty object sub-containers but contain empty data arrays', function() { - var obj = {}, - prop = np(obj, 'a[1].b.c'), - expectedArr = []; + prop5.set(5); + var localArr = [1, 2, 3]; + localArr[5] = 5; + expect(arr).toEqual(localArr); - expectedArr[1] = {b: {c: 'pizza'}}; + prop5.set(null); + expect(arr).toEqual([1, 2, 3]); + expect(arr.length).toBe(3); + }); - expect(prop.get()).toBe(undefined); - expect(obj).toEqual({}); + it("should not access whole array elements with index -1", function() { + // for a lot of cases we could make this work, + // but deleting the value is a mess, and anyway + // we don't need this, it's better just to set the whole + // array, ie np(obj, 'arr') + var obj = { arr: [1, 2, 3] }; + expect(function() { + np(obj, "arr[-1]"); + }).toThrow("bad property string"); + }); - prop.set('pizza'); - expect(obj).toEqual({a: expectedArr}); - expect(prop.get()).toBe('pizza'); + it( + "should access properties of objects in an array with index -1", + function() { + var obj = { arr: [{ a: 1 }, { a: 2 }, { b: 3 }] }, + prop = np(obj, "arr[-1].a"); + + expect(prop.get()).toEqual([1, 2, undefined]); + expect(obj).toEqual({ arr: [{ a: 1 }, { a: 2 }, { b: 3 }] }); + + prop.set(5); + expect(prop.get()).toBe(5); + expect(obj).toEqual({ arr: [{ a: 5 }, { a: 5 }, { a: 5, b: 3 }] }); + + prop.set(null); + expect(prop.get()).toBe(undefined); + expect(obj).toEqual({ arr: [undefined, undefined, { b: 3 }] }); + + prop.set([2, 3, 4]); + expect(prop.get()).toEqual([2, 3, 4]); + expect(obj).toEqual({ arr: [{ a: 2 }, { a: 3 }, { a: 4, b: 3 }] }); + + prop.set([6, 7, undefined]); + expect(prop.get()).toEqual([6, 7, undefined]); + expect(obj).toEqual({ arr: [{ a: 6 }, { a: 7 }, { b: 3 }] }); + + // too short an array: wrap around + prop.set([9, 10]); + expect(prop.get()).toEqual([9, 10, 9]); + expect(obj).toEqual({ arr: [{ a: 9 }, { a: 10 }, { a: 9, b: 3 }] }); + + // too long an array: ignore extras + prop.set([11, 12, 13, 14]); + expect(prop.get()).toEqual([11, 12, 13]); + expect(obj).toEqual({ arr: [{ a: 11 }, { a: 12 }, { a: 13, b: 3 }] }); + } + ); + + it("should remove a property only with undefined or null", function() { + var obj = { a: "b", c: "d" }, propA = np(obj, "a"), propC = np(obj, "c"); + + propA.set(null); + propC.set(undefined); + expect(obj).toEqual({}); + + propA.set(false); + np(obj, "b").set(""); + propC.set(0); + np(obj, "d").set(NaN); + expect(obj).toEqual({ a: false, b: "", c: 0, d: NaN }); + }); - prop.set(null); - expect(prop.get()).toBe(undefined); - expect(obj).toEqual({a: []}); - }); + it("should remove containers but not data arrays", function() { + var obj = { + annotations: [{ a: [1, 2, 3] }], + c: [1, 2, 3], + domain: [1, 2], + range: [2, 3], + shapes: ["elephant"] + }, + propA = np(obj, "annotations[-1].a"), + propC = np(obj, "c"), + propD0 = np(obj, "domain[0]"), + propD1 = np(obj, "domain[1]"), + propR = np(obj, "range"), + propS = np(obj, "shapes[0]"); + + propA.set([]); + propC.set([]); + propD0.set(undefined); + propD1.set(undefined); + propR.set([]); + propS.set(null); + + expect(obj).toEqual({ c: [] }); + }); - it('should get empty, and fail on set, with a bad input object', function() { - var badProps = [ - np(5, 'a'), - np(undefined, 'a'), - np('cats', 'a'), - np(true, 'a') - ]; - - function badSetter(i) { - return function() { - badProps[i].set('cats'); - }; - } + it( + "should have no empty object sub-containers but contain empty data arrays", + function() { + var obj = {}, prop = np(obj, "a[1].b.c"), expectedArr = []; + + expectedArr[1] = { b: { c: "pizza" } }; + + expect(prop.get()).toBe(undefined); + expect(obj).toEqual({}); + + prop.set("pizza"); + expect(obj).toEqual({ a: expectedArr }); + expect(prop.get()).toBe("pizza"); + + prop.set(null); + expect(prop.get()).toBe(undefined); + expect(obj).toEqual({ a: [] }); + } + ); + + it( + "should get empty, and fail on set, with a bad input object", + function() { + var badProps = [ + np(5, "a"), + np(undefined, "a"), + np("cats", "a"), + np(true, "a") + ]; + + function badSetter(i) { + return function() { + badProps[i].set("cats"); + }; + } - for(var i = 0; i < badProps.length; i++) { - expect(badProps[i].get()).toBe(undefined); - expect(badSetter(i)).toThrow('bad container'); - } - }); + for (var i = 0; i < badProps.length; i++) { + expect(badProps[i].get()).toBe(undefined); + expect(badSetter(i)).toThrow("bad container"); + } + } + ); - it('should fail on a bad property string', function() { - var badStr = [ - [], {}, false, undefined, null, NaN, Infinity - ]; + it("should fail on a bad property string", function() { + var badStr = [[], {}, false, undefined, null, NaN, Infinity]; - function badProp(i) { - return function() { - np({}, badStr[i]); - }; - } + function badProp(i) { + return function() { + np({}, badStr[i]); + }; + } - for(var i = 0; i < badStr.length; i++) { - expect(badProp(i)).toThrow('bad property string'); - } - }); + for (var i = 0; i < badStr.length; i++) { + expect(badProp(i)).toThrow("bad property string"); + } }); + }); - describe('objectFromPath', function() { + describe("objectFromPath", function() { + it("should return an object", function() { + var obj = Lib.objectFromPath("test", "object"); - it('should return an object', function() { - var obj = Lib.objectFromPath('test', 'object'); - - expect(obj).toEqual({ test: 'object' }); - }); + expect(obj).toEqual({ test: "object" }); + }); - it('should work for deep objects', function() { - var obj = Lib.objectFromPath('deep.nested.test', 'object'); + it("should work for deep objects", function() { + var obj = Lib.objectFromPath("deep.nested.test", "object"); - expect(obj).toEqual({ deep: { nested: { test: 'object' }}}); - }); + expect(obj).toEqual({ deep: { nested: { test: "object" } } }); + }); - it('should work for arrays', function() { - var obj = Lib.objectFromPath('nested[2].array', 'object'); + it("should work for arrays", function() { + var obj = Lib.objectFromPath("nested[2].array", "object"); - expect(Object.keys(obj)).toEqual(['nested']); - expect(Array.isArray(obj.nested)).toBe(true); - expect(obj.nested[0]).toBe(undefined); - expect(obj.nested[2]).toEqual({ array: 'object' }); - }); + expect(Object.keys(obj)).toEqual(["nested"]); + expect(Array.isArray(obj.nested)).toBe(true); + expect(obj.nested[0]).toBe(undefined); + expect(obj.nested[2]).toEqual({ array: "object" }); + }); - it('should work for any given value', function() { - var obj = Lib.objectFromPath('test.type', { an: 'object' }); + it("should work for any given value", function() { + var obj = Lib.objectFromPath("test.type", { an: "object" }); - expect(obj).toEqual({ test: { type: { an: 'object' }}}); + expect(obj).toEqual({ test: { type: { an: "object" } } }); - obj = Lib.objectFromPath('test.type', [42]); + obj = Lib.objectFromPath("test.type", [42]); - expect(obj).toEqual({ test: { type: [42] }}); - }); + expect(obj).toEqual({ test: { type: [42] } }); }); + }); - describe('expandObjectPaths', function() { - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - it('returns the original object', function() { - var x = {}; - expect(Lib.expandObjectPaths(x)).toBe(x); - }); + describe("expandObjectPaths", function() { + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - it('unpacks top-level paths', function() { - var input = {'marker.color': 'red', 'marker.size': [1, 2, 3]}; - var expected = {marker: {color: 'red', size: [1, 2, 3]}}; - expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); - }); + it("returns the original object", function() { + var x = {}; + expect(Lib.expandObjectPaths(x)).toBe(x); + }); - it('unpacks recursively', function() { - var input = {'marker.color': {'red.certainty': 'definitely'}}; - var expected = {marker: {color: {red: {certainty: 'definitely'}}}}; - expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); - }); + it("unpacks top-level paths", function() { + var input = { "marker.color": "red", "marker.size": [1, 2, 3] }; + var expected = { marker: { color: "red", size: [1, 2, 3] } }; + expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); + }); - it('unpacks deep paths', function() { - var input = {'foo.bar.baz': 'red'}; - var expected = {foo: {bar: {baz: 'red'}}}; - expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); - }); + it("unpacks recursively", function() { + var input = { "marker.color": { "red.certainty": "definitely" } }; + var expected = { + marker: { color: { red: { certainty: "definitely" } } } + }; + expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); + }); - it('unpacks non-top-level deep paths', function() { - var input = {color: {'foo.bar.baz': 'red'}}; - var expected = {color: {foo: {bar: {baz: 'red'}}}}; - expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); - }); + it("unpacks deep paths", function() { + var input = { "foo.bar.baz": "red" }; + var expected = { foo: { bar: { baz: "red" } } }; + expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); + }); - it('merges dotted properties into objects', function() { - var input = {marker: {color: 'red'}, 'marker.size': 8}; - var expected = {marker: {color: 'red', size: 8}}; - expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); - }); + it("unpacks non-top-level deep paths", function() { + var input = { color: { "foo.bar.baz": "red" } }; + var expected = { color: { foo: { bar: { baz: "red" } } } }; + expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); + }); - it('merges objects into dotted properties', function() { - var input = {'marker.size': 8, marker: {color: 'red'}}; - var expected = {marker: {color: 'red', size: 8}}; - expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); - }); + it("merges dotted properties into objects", function() { + var input = { marker: { color: "red" }, "marker.size": 8 }; + var expected = { marker: { color: "red", size: 8 } }; + expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); + }); - it('retains the identity of nested objects', function() { - var input = {marker: {size: 8}}; - var origNested = input.marker; - var expanded = Lib.expandObjectPaths(input); - var newNested = expanded.marker; + it("merges objects into dotted properties", function() { + var input = { "marker.size": 8, marker: { color: "red" } }; + var expected = { marker: { color: "red", size: 8 } }; + expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); + }); - expect(input).toBe(expanded); - expect(origNested).toBe(newNested); - }); + it("retains the identity of nested objects", function() { + var input = { marker: { size: 8 } }; + var origNested = input.marker; + var expanded = Lib.expandObjectPaths(input); + var newNested = expanded.marker; - it('retains the identity of nested arrays', function() { - var input = {'marker.size': [1, 2, 3]}; - var origArray = input['marker.size']; - var expanded = Lib.expandObjectPaths(input); - var newArray = expanded.marker.size; + expect(input).toBe(expanded); + expect(origNested).toBe(newNested); + }); - expect(input).toBe(expanded); - expect(origArray).toBe(newArray); - }); + it("retains the identity of nested arrays", function() { + var input = { "marker.size": [1, 2, 3] }; + var origArray = input["marker.size"]; + var expanded = Lib.expandObjectPaths(input); + var newArray = expanded.marker.size; - it('expands bracketed array notation', function() { - var input = {'marker[1]': {color: 'red'}}; - var expected = {marker: [undefined, {color: 'red'}]}; - expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); - }); + expect(input).toBe(expanded); + expect(origArray).toBe(newArray); + }); - it('expands nested arrays', function() { - var input = {'marker[1].range[1]': 5}; - var expected = {marker: [undefined, {range: [undefined, 5]}]}; - var computed = Lib.expandObjectPaths(input); - expect(computed).toLooseDeepEqual(expected); - }); + it("expands bracketed array notation", function() { + var input = { "marker[1]": { color: "red" } }; + var expected = { marker: [undefined, { color: "red" }] }; + expect(Lib.expandObjectPaths(input)).toLooseDeepEqual(expected); + }); - it('expands bracketed array with more nested attributes', function() { - var input = {'marker[1]': {'color.alpha': 2}}; - var expected = {marker: [undefined, {color: {alpha: 2}}]}; - var computed = Lib.expandObjectPaths(input); - expect(computed).toLooseDeepEqual(expected); - }); + it("expands nested arrays", function() { + var input = { "marker[1].range[1]": 5 }; + var expected = { marker: [undefined, { range: [undefined, 5] }] }; + var computed = Lib.expandObjectPaths(input); + expect(computed).toLooseDeepEqual(expected); + }); - it('expands bracketed array notation without further nesting', function() { - var input = {'marker[1]': 8}; - var expected = {marker: [undefined, 8]}; - var computed = Lib.expandObjectPaths(input); - expect(computed).toLooseDeepEqual(expected); - }); + it("expands bracketed array with more nested attributes", function() { + var input = { "marker[1]": { "color.alpha": 2 } }; + var expected = { marker: [undefined, { color: { alpha: 2 } }] }; + var computed = Lib.expandObjectPaths(input); + expect(computed).toLooseDeepEqual(expected); + }); - it('expands bracketed array notation with further nesting', function() { - var input = {'marker[1].size': 8}; - var expected = {marker: [undefined, {size: 8}]}; - var computed = Lib.expandObjectPaths(input); - expect(computed).toLooseDeepEqual(expected); - }); + it("expands bracketed array notation without further nesting", function() { + var input = { "marker[1]": 8 }; + var expected = { marker: [undefined, 8] }; + var computed = Lib.expandObjectPaths(input); + expect(computed).toLooseDeepEqual(expected); + }); - it('expands bracketed array notation with further nesting', function() { - var input = {'marker[1].size.magnitude': 8}; - var expected = {marker: [undefined, {size: {magnitude: 8}}]}; - var computed = Lib.expandObjectPaths(input); - expect(computed).toLooseDeepEqual(expected); - }); + it("expands bracketed array notation with further nesting", function() { + var input = { "marker[1].size": 8 }; + var expected = { marker: [undefined, { size: 8 }] }; + var computed = Lib.expandObjectPaths(input); + expect(computed).toLooseDeepEqual(expected); + }); - it('combines changes with single array nesting', function() { - var input = {'marker[1].foo': 5, 'marker[0].foo': 4}; - var expected = {marker: [{foo: 4}, {foo: 5}]}; - var computed = Lib.expandObjectPaths(input); - expect(computed).toLooseDeepEqual(expected); - }); + it("expands bracketed array notation with further nesting", function() { + var input = { "marker[1].size.magnitude": 8 }; + var expected = { marker: [undefined, { size: { magnitude: 8 } }] }; + var computed = Lib.expandObjectPaths(input); + expect(computed).toLooseDeepEqual(expected); + }); - it('does not skip over array container set to null values', function() { - var input = {title: 'clear annotations', annotations: null}; - var expected = {title: 'clear annotations', annotations: null}; - var computed = Lib.expandObjectPaths(input); - expect(computed).toLooseDeepEqual(expected); - }); + it("combines changes with single array nesting", function() { + var input = { "marker[1].foo": 5, "marker[0].foo": 4 }; + var expected = { marker: [{ foo: 4 }, { foo: 5 }] }; + var computed = Lib.expandObjectPaths(input); + expect(computed).toLooseDeepEqual(expected); + }); - it('expands array containers', function() { - var input = {title: 'clear annotation 1', 'annotations[1]': { title: 'new' }}; - var expected = {title: 'clear annotation 1', annotations: [null, { title: 'new' }]}; - var computed = Lib.expandObjectPaths(input); - expect(computed).toLooseDeepEqual(expected); - }); + it("does not skip over array container set to null values", function() { + var input = { title: "clear annotations", annotations: null }; + var expected = { title: "clear annotations", annotations: null }; + var computed = Lib.expandObjectPaths(input); + expect(computed).toLooseDeepEqual(expected); + }); - // TODO: This test is unimplemented since it's a currently-unused corner case. - // Getting the test to pass requires some extension (pun?) to extendDeepNoArrays - // that's intelligent enough to only selectively merge *some* arrays, in particular - // not data arrays but yes on arrays that were previously expanded. This is a bit - // tricky to get to work just right and currently doesn't have any known use since - // container arrays are not multiply nested. - // - // Additional notes on what works or what doesn't work. This case does *not* work - // because the two nested arrays that would result from the expansion need to be - // deep merged. - // - // Lib.expandObjectPaths({'marker.range[0]': 5, 'marker.range[1]': 2}) - // - // // => {marker: {range: [null, 2]}} - // - // This case *does* work becuase the array merging does not require a deep extend: - // - // Lib.expandObjectPaths({'range[0]': 5, 'range[1]': 2} - // - // // => {range: [5, 2]} - // - // Finally note that this case works fine becuase there's no merge necessary: - // - // Lib.expandObjectPaths({'marker.range[1]': 2}) - // - // // => {marker: {range: [null, 2]}} - // - /* + it("expands array containers", function() { + var input = { + title: "clear annotation 1", + "annotations[1]": { title: "new" } + }; + var expected = { + title: "clear annotation 1", + annotations: [null, { title: "new" }] + }; + var computed = Lib.expandObjectPaths(input); + expect(computed).toLooseDeepEqual(expected); + }); + // TODO: This test is unimplemented since it's a currently-unused corner case. + // Getting the test to pass requires some extension (pun?) to extendDeepNoArrays + // that's intelligent enough to only selectively merge *some* arrays, in particular + // not data arrays but yes on arrays that were previously expanded. This is a bit + // tricky to get to work just right and currently doesn't have any known use since + // container arrays are not multiply nested. + // + // Additional notes on what works or what doesn't work. This case does *not* work + // because the two nested arrays that would result from the expansion need to be + // deep merged. + // + // Lib.expandObjectPaths({'marker.range[0]': 5, 'marker.range[1]': 2}) + // + // // => {marker: {range: [null, 2]}} + // + // This case *does* work becuase the array merging does not require a deep extend: + // + // Lib.expandObjectPaths({'range[0]': 5, 'range[1]': 2} + // + // // => {range: [5, 2]} + // + // Finally note that this case works fine becuase there's no merge necessary: + // + // Lib.expandObjectPaths({'marker.range[1]': 2}) + // + // // => {marker: {range: [null, 2]}} + // + /* it('combines changes', function() { var input = {'marker[1].range[1]': 5, 'marker[1].range[0]': 4}; var expected = {marker: [undefined, {range: [4, 5]}]}; @@ -608,1064 +604,1112 @@ describe('Test lib.js:', function() { expect(computed).toEqual(expected); }); */ + }); + + describe("coerce", function() { + var coerce = Lib.coerce, out; + + // TODO: I tested font and string because I changed them, but all the other types need tests still + it("should set a value and return the value it sets", function() { + var aVal = "aaaaah!", + cVal = { 1: 2, 3: 4 }, + attrs = { + a: { valType: "any", dflt: aVal }, + b: { c: { valType: "any" } } + }, + obj = { b: { c: cVal } }, + outObj = {}, + aOut = coerce(obj, outObj, attrs, "a"), + cOut = coerce(obj, outObj, attrs, "b.c"); + + expect(aOut).toBe(aVal); + expect(aOut).toBe(outObj.a); + expect(cOut).toBe(cVal); + expect(cOut).toBe(outObj.b.c); }); - describe('coerce', function() { - var coerce = Lib.coerce, - out; - - // TODO: I tested font and string because I changed them, but all the other types need tests still - - it('should set a value and return the value it sets', function() { - var aVal = 'aaaaah!', - cVal = {1: 2, 3: 4}, - attrs = {a: {valType: 'any', dflt: aVal}, b: {c: {valType: 'any'}}}, - obj = {b: {c: cVal}}, - outObj = {}, - - aOut = coerce(obj, outObj, attrs, 'a'), - cOut = coerce(obj, outObj, attrs, 'b.c'); - - expect(aOut).toBe(aVal); - expect(aOut).toBe(outObj.a); - expect(cOut).toBe(cVal); - expect(cOut).toBe(outObj.b.c); - }); - - describe('string valType', function() { - var dflt = 'Jabberwock', - stringAttrs = { - s: {valType: 'string', dflt: dflt}, - noBlank: {valType: 'string', dflt: dflt, noBlank: true} - }; - - it('should insert the default if input is missing, or blank with noBlank', function() { - out = coerce(undefined, {}, stringAttrs, 's'); - expect(out).toEqual(dflt); - - out = coerce({}, {}, stringAttrs, 's'); - expect(out).toEqual(dflt); - - out = coerce({s: ''}, {}, stringAttrs, 's'); - expect(out).toEqual(''); + describe("string valType", function() { + var dflt = "Jabberwock", + stringAttrs = { + s: { valType: "string", dflt: dflt }, + noBlank: { valType: "string", dflt: dflt, noBlank: true } + }; - out = coerce({noBlank: ''}, {}, stringAttrs, 'noBlank'); - expect(out).toEqual(dflt); - }); + it( + "should insert the default if input is missing, or blank with noBlank", + function() { + out = coerce(undefined, {}, stringAttrs, "s"); + expect(out).toEqual(dflt); - it('should always return a string for any input', function() { - expect(coerce({s: 'a string!!'}, {}, stringAttrs, 's')) - .toEqual('a string!!'); + out = coerce({}, {}, stringAttrs, "s"); + expect(out).toEqual(dflt); - expect(coerce({s: 42}, {}, stringAttrs, 's')) - .toEqual('42'); + out = coerce({ s: "" }, {}, stringAttrs, "s"); + expect(out).toEqual(""); - expect(coerce({s: [1, 2, 3]}, {}, stringAttrs, 's')) - .toEqual(dflt); + out = coerce({ noBlank: "" }, {}, stringAttrs, "noBlank"); + expect(out).toEqual(dflt); + } + ); - expect(coerce({s: true}, {}, stringAttrs, 's')) - .toEqual(dflt); + it("should always return a string for any input", function() { + expect(coerce({ s: "a string!!" }, {}, stringAttrs, "s")).toEqual( + "a string!!" + ); - expect(coerce({s: {1: 2}}, {}, stringAttrs, 's')) - .toEqual(dflt); - }); - }); + expect(coerce({ s: 42 }, {}, stringAttrs, "s")).toEqual("42"); - describe('coerce2', function() { - var coerce2 = Lib.coerce2; - - it('should set a value and return the value it sets when user input is valid', function() { - var colVal = 'red', - sizeVal = 0, // 0 is valid but falsey - attrs = {testMarker: {testColor: {valType: 'color', dflt: 'rgba(0, 0, 0, 0)'}, - testSize: {valType: 'number', dflt: 20}}}, - obj = {testMarker: {testColor: colVal, testSize: sizeVal}}, - outObj = {}, - colOut = coerce2(obj, outObj, attrs, 'testMarker.testColor'), - sizeOut = coerce2(obj, outObj, attrs, 'testMarker.testSize'); - - expect(colOut).toBe(colVal); - expect(colOut).toBe(outObj.testMarker.testColor); - expect(sizeOut).toBe(sizeVal); - expect(sizeOut).toBe(outObj.testMarker.testSize); - }); - - it('should set and return the default if the user input is not valid', function() { - var colVal = 'r', - sizeVal = 'aaaaah!', - attrs = {testMarker: {testColor: {valType: 'color', dflt: 'rgba(0, 0, 0, 0)'}, - testSize: {valType: 'number', dflt: 20}}}, - obj = {testMarker: {testColor: colVal, testSize: sizeVal}}, - outObj = {}, - colOut = coerce2(obj, outObj, attrs, 'testMarker.testColor'), - sizeOut = coerce2(obj, outObj, attrs, 'testMarker.testSize'); - - expect(colOut).toBe('rgba(0, 0, 0, 0)'); - expect(sizeOut).toBe(outObj.testMarker.testSize); - expect(sizeOut).toBe(20); - expect(sizeOut).toBe(outObj.testMarker.testSize); - }); - - it('should return false if there is no user input', function() { - var colVal = null, - sizeVal, // undefined - attrs = {testMarker: {testColor: {valType: 'color', dflt: 'rgba(0, 0, 0, 0)'}, - testSize: {valType: 'number', dflt: 20}}}, - obj = {testMarker: {testColor: colVal, testSize: sizeVal}}, - outObj = {}, - colOut = coerce2(obj, outObj, attrs, 'testMarker.testColor'), - sizeOut = coerce2(obj, outObj, attrs, 'testMarker.testSize'); - - expect(colOut).toBe(false); - expect(sizeOut).toBe(false); - }); - }); + expect(coerce({ s: [1, 2, 3] }, {}, stringAttrs, "s")).toEqual(dflt); - describe('info_array valType', function() { - var infoArrayAttrs = { - range: { - valType: 'info_array', - items: [ - { valType: 'number' }, - { valType: 'number' } - ] - }, - domain: { - valType: 'info_array', - items: [ - { valType: 'number', min: 0, max: 1 }, - { valType: 'number', min: 0, max: 1 } - ], - dflt: [0, 1] - } - }; - - it('should insert the default if input is missing', function() { - expect(coerce(undefined, {}, infoArrayAttrs, 'domain')) - .toEqual([0, 1]); - expect(coerce(undefined, {}, infoArrayAttrs, 'domain', [0, 0.5])) - .toEqual([0, 0.5]); - }); - - it('should dive into the items and coerce accordingly', function() { - expect(coerce({range: ['-10', 100]}, {}, infoArrayAttrs, 'range')) - .toEqual([-10, 100]); - - expect(coerce({domain: [0, 0.5]}, {}, infoArrayAttrs, 'domain')) - .toEqual([0, 0.5]); - - expect(coerce({domain: [-5, 0.5]}, {}, infoArrayAttrs, 'domain')) - .toEqual([0, 0.5]); - - expect(coerce({domain: [0.5, 4.5]}, {}, infoArrayAttrs, 'domain')) - .toEqual([0.5, 1]); - }); - - it('should coerce unexpected input as best as it can', function() { - expect(coerce({range: [12]}, {}, infoArrayAttrs, 'range')) - .toEqual([12]); - - expect(coerce({range: [12]}, {}, infoArrayAttrs, 'range', [-1, 20])) - .toEqual([12, 20]); - - expect(coerce({domain: [0.5]}, {}, infoArrayAttrs, 'domain')) - .toEqual([0.5, 1]); - - expect(coerce({range: ['-10', 100, 12]}, {}, infoArrayAttrs, 'range')) - .toEqual([-10, 100]); - - expect(coerce({domain: [0, 0.5, 1]}, {}, infoArrayAttrs, 'domain')) - .toEqual([0, 0.5]); - }); - }); + expect(coerce({ s: true }, {}, stringAttrs, "s")).toEqual(dflt); - describe('subplotid valtype', function() { - var dflt = 'slice'; - var idAttrs = { - pizza: { - valType: 'subplotid', - dflt: dflt - } - }; - - var goodVals = ['slice', 'slice2', 'slice1492']; - - goodVals.forEach(function(goodVal) { - it('should allow "' + goodVal + '"', function() { - expect(coerce({pizza: goodVal}, {}, idAttrs, 'pizza')) - .toEqual(goodVal); - }); - }); - - var badVals = [ - 'slice0', - 'slice1', - 'Slice2', - '2slice', - '2', - 2, - 'slice2 ', - 'slice2.0', - ' slice2', - 'slice 2', - 'slice01' - ]; - - badVals.forEach(function(badVal) { - it('should not allow "' + badVal + '"', function() { - expect(coerce({pizza: badVal}, {}, idAttrs, 'pizza')) - .toEqual(dflt); - }); - }); - }); + expect(coerce({ s: { 1: 2 } }, {}, stringAttrs, "s")).toEqual(dflt); + }); }); - describe('coerceFont', function() { - var fontAttrs = Plots.fontAttrs, - extendFlat = Lib.extendFlat, - coerceFont = Lib.coerceFont; - - var defaultFont = { - family: '"Open sans", verdana, arial, sans-serif, DEFAULT', - size: 314159, - color: 'neon pink with sparkles' - }; - - var attributes = { - fontWithDefault: { - family: extendFlat({}, fontAttrs.family, {dflt: defaultFont.family}), - size: extendFlat({}, fontAttrs.size, {dflt: defaultFont.size}), - color: extendFlat({}, fontAttrs.color, {dflt: defaultFont.color}) + describe("coerce2", function() { + var coerce2 = Lib.coerce2; + + it( + "should set a value and return the value it sets when user input is valid", + function() { + var colVal = "red", + sizeVal = 0, + // 0 is valid but falsey + attrs = { + testMarker: { + testColor: { valType: "color", dflt: "rgba(0, 0, 0, 0)" }, + testSize: { valType: "number", dflt: 20 } + } }, - fontNoDefault: fontAttrs - }; - - var containerIn; - - function coerce(attr, dflt) { - return Lib.coerce(containerIn, {}, attributes, attr, dflt); + obj = { testMarker: { testColor: colVal, testSize: sizeVal } }, + outObj = {}, + colOut = coerce2(obj, outObj, attrs, "testMarker.testColor"), + sizeOut = coerce2(obj, outObj, attrs, "testMarker.testSize"); + + expect(colOut).toBe(colVal); + expect(colOut).toBe(outObj.testMarker.testColor); + expect(sizeOut).toBe(sizeVal); + expect(sizeOut).toBe(outObj.testMarker.testSize); } - - it('should insert the full default if no or empty input', function() { - containerIn = undefined; - expect(coerceFont(coerce, 'fontWithDefault')) - .toEqual(defaultFont); - - containerIn = {}; - expect(coerceFont(coerce, 'fontNoDefault', defaultFont)) - .toEqual(defaultFont); - - containerIn = {fontWithDefault: {}}; - expect(coerceFont(coerce, 'fontWithDefault')) - .toEqual(defaultFont); - }); - - it('should fill in defaults for bad inputs', function() { - containerIn = { - fontWithDefault: {family: '', size: 'a million', color: 42} - }; - expect(coerceFont(coerce, 'fontWithDefault')) - .toEqual(defaultFont); - }); - - it('should pass through individual valid pieces', function() { - var goodFamily = 'A fish', // for now any non-blank string is OK - badFamily = 42, - goodSize = 123.456, - badSize = 'ginormous', - goodColor = 'red', - badColor = 'a dark and stormy night'; - - containerIn = { - fontWithDefault: {family: goodFamily, size: badSize, color: badColor} - }; - expect(coerceFont(coerce, 'fontWithDefault')) - .toEqual({family: goodFamily, size: defaultFont.size, color: defaultFont.color}); - - containerIn = { - fontWithDefault: {family: badFamily, size: goodSize, color: badColor} - }; - expect(coerceFont(coerce, 'fontWithDefault')) - .toEqual({family: defaultFont.family, size: goodSize, color: defaultFont.color}); - - containerIn = { - fontWithDefault: {family: badFamily, size: badSize, color: goodColor} - }; - expect(coerceFont(coerce, 'fontWithDefault')) - .toEqual({family: defaultFont.family, size: defaultFont.size, color: goodColor}); - }); - }); - - describe('init2dArray', function() { - it('should initialize a 2d array with the correct dimenstions', function() { - var array = Lib.init2dArray(4, 5); - expect(array.length).toEqual(4); - expect(array[0].length).toEqual(5); - expect(array[3].length).toEqual(5); - }); + ); + + it( + "should set and return the default if the user input is not valid", + function() { + var colVal = "r", + sizeVal = "aaaaah!", + attrs = { + testMarker: { + testColor: { valType: "color", dflt: "rgba(0, 0, 0, 0)" }, + testSize: { valType: "number", dflt: 20 } + } + }, + obj = { testMarker: { testColor: colVal, testSize: sizeVal } }, + outObj = {}, + colOut = coerce2(obj, outObj, attrs, "testMarker.testColor"), + sizeOut = coerce2(obj, outObj, attrs, "testMarker.testSize"); + + expect(colOut).toBe("rgba(0, 0, 0, 0)"); + expect(sizeOut).toBe(outObj.testMarker.testSize); + expect(sizeOut).toBe(20); + expect(sizeOut).toBe(outObj.testMarker.testSize); + } + ); + + it("should return false if there is no user input", function() { + var colVal = null, + sizeVal, + // undefined + attrs = { + testMarker: { + testColor: { valType: "color", dflt: "rgba(0, 0, 0, 0)" }, + testSize: { valType: "number", dflt: 20 } + } + }, + obj = { testMarker: { testColor: colVal, testSize: sizeVal } }, + outObj = {}, + colOut = coerce2(obj, outObj, attrs, "testMarker.testColor"), + sizeOut = coerce2(obj, outObj, attrs, "testMarker.testSize"); + + expect(colOut).toBe(false); + expect(sizeOut).toBe(false); + }); }); - describe('validate', function() { - - function assert(shouldPass, shouldFail, valObject) { - shouldPass.forEach(function(v) { - var res = Lib.validate(v, valObject); - expect(res).toBe(true, JSON.stringify(v) + ' should pass'); - }); - - shouldFail.forEach(function(v) { - var res = Lib.validate(v, valObject); - expect(res).toBe(false, JSON.stringify(v) + ' should fail'); - }); + describe("info_array valType", function() { + var infoArrayAttrs = { + range: { + valType: "info_array", + items: [{ valType: "number" }, { valType: "number" }] + }, + domain: { + valType: "info_array", + items: [ + { valType: "number", min: 0, max: 1 }, + { valType: "number", min: 0, max: 1 } + ], + dflt: [0, 1] } + }; + + it("should insert the default if input is missing", function() { + expect(coerce(undefined, {}, infoArrayAttrs, "domain")).toEqual([0, 1]); + expect( + coerce(undefined, {}, infoArrayAttrs, "domain", [0, 0.5]) + ).toEqual([0, 0.5]); + }); + + it("should dive into the items and coerce accordingly", function() { + expect( + coerce({ range: ["-10", 100] }, {}, infoArrayAttrs, "range") + ).toEqual([-10, 100]); + + expect( + coerce({ domain: [0, 0.5] }, {}, infoArrayAttrs, "domain") + ).toEqual([0, 0.5]); + + expect( + coerce({ domain: [-5, 0.5] }, {}, infoArrayAttrs, "domain") + ).toEqual([0, 0.5]); + + expect( + coerce({ domain: [0.5, 4.5] }, {}, infoArrayAttrs, "domain") + ).toEqual([0.5, 1]); + }); + + it("should coerce unexpected input as best as it can", function() { + expect(coerce({ range: [12] }, {}, infoArrayAttrs, "range")).toEqual([ + 12 + ]); + + expect( + coerce({ range: [12] }, {}, infoArrayAttrs, "range", [-1, 20]) + ).toEqual([12, 20]); + + expect( + coerce({ domain: [0.5] }, {}, infoArrayAttrs, "domain") + ).toEqual([0.5, 1]); + + expect( + coerce({ range: ["-10", 100, 12] }, {}, infoArrayAttrs, "range") + ).toEqual([-10, 100]); + + expect( + coerce({ domain: [0, 0.5, 1] }, {}, infoArrayAttrs, "domain") + ).toEqual([0, 0.5]); + }); + }); - it('should work for valType \'data_array\' where', function() { - var shouldPass = [[20], []], - shouldFail = ['a', {}, 20, undefined, null]; - - assert(shouldPass, shouldFail, { - valType: 'data_array' - }); - - assert(shouldPass, shouldFail, { - valType: 'data_array', - dflt: [1, 2] - }); - }); - - it('should work for valType \'enumerated\' where', function() { - assert(['a', 'b'], ['c', 1, null, undefined, ''], { - valType: 'enumerated', - values: ['a', 'b'], - dflt: 'a' - }); - - assert([1, '1', 2, '2'], ['c', 3, null, undefined, ''], { - valType: 'enumerated', - values: [1, 2], - coerceNumber: true, - dflt: 1 - }); - - assert(['a', 'b', [1, 2]], ['c', 1, null, undefined, ''], { - valType: 'enumerated', - values: ['a', 'b'], - arrayOk: true, - dflt: 'a' - }); - }); - - it('should work for valType \'boolean\' where', function() { - var shouldPass = [true, false], - shouldFail = ['a', 1, {}, [], null, undefined, '']; - - assert(shouldPass, shouldFail, { - valType: 'boolean', - dflt: true - }); - - assert(shouldPass, shouldFail, { - valType: 'boolean', - dflt: false - }); - }); - - it('should work for valType \'number\' where', function() { - var shouldPass = [20, '20', 1e6], - shouldFail = ['a', [], {}, null, undefined, '']; - - assert(shouldPass, shouldFail, { - valType: 'number' - }); - - assert(shouldPass, shouldFail, { - valType: 'number', - dflt: null - }); - - assert([20, '20'], [-10, '-10', 25, '25'], { - valType: 'number', - dflt: 20, - min: 0, - max: 21 - }); - - assert([20, '20', [1, 2]], ['a', {}], { - valType: 'number', - dflt: 20, - arrayOk: true - }); - }); - - it('should work for valType \'integer\' where', function() { - assert([1, 2, '3', '4'], ['a', 1.321321, {}, [], null, 2 / 3, undefined, null], { - valType: 'integer', - dflt: 1 - }); - - assert([1, 2, '3', '4'], [-1, '-2', 2.121, null, undefined, [], {}], { - valType: 'integer', - min: 0, - dflt: 1 - }); - }); - - it('should work for valType \'string\' where', function() { - var date = new Date(2016, 1, 1); - - assert(['3', '4', 'a', 3, 1.2113, ''], [undefined, {}, [], null, date, false], { - valType: 'string', - dflt: 'a' - }); - - assert(['3', '4', 'a', 3, 1.2113], ['', undefined, {}, [], null, date, true], { - valType: 'string', - dflt: 'a', - noBlank: true - }); - - assert(['3', '4', ''], [undefined, 1, {}, [], null, date, true, false], { - valType: 'string', - dflt: 'a', - strict: true - }); - - assert(['3', '4'], [undefined, 1, {}, [], null, date, '', true, false], { - valType: 'string', - dflt: 'a', - strict: true, - noBlank: true - }); - }); - - it('should work for valType \'color\' where', function() { - var shouldPass = ['red', '#d3d3d3', 'rgba(0,255,255,0.1)'], - shouldFail = [1, {}, [], 'rgq(233,122,332,1)', null, undefined]; - - assert(shouldPass, shouldFail, { - valType: 'color' - }); - }); - - it('should work for valType \'colorscale\' where', function() { - var good = [ [0, 'red'], [1, 'blue'] ], - bad = [ [0.1, 'red'], [1, 'blue'] ], - bad2 = [ [0], [1] ], - bad3 = [ ['red'], ['blue']], - bad4 = ['red', 'blue']; - - var shouldPass = ['Viridis', 'Greens', good], - shouldFail = ['red', 1, undefined, null, {}, [], bad, bad2, bad3, bad4]; + describe("subplotid valtype", function() { + var dflt = "slice"; + var idAttrs = { pizza: { valType: "subplotid", dflt: dflt } }; + + var goodVals = ["slice", "slice2", "slice1492"]; + + goodVals.forEach(function(goodVal) { + it('should allow "' + goodVal + '"', function() { + expect(coerce({ pizza: goodVal }, {}, idAttrs, "pizza")).toEqual( + goodVal + ); + }); + }); + + var badVals = [ + "slice0", + "slice1", + "Slice2", + "2slice", + "2", + 2, + "slice2 ", + "slice2.0", + " slice2", + "slice 2", + "slice01" + ]; + + badVals.forEach(function(badVal) { + it('should not allow "' + badVal + '"', function() { + expect(coerce({ pizza: badVal }, {}, idAttrs, "pizza")).toEqual(dflt); + }); + }); + }); + }); + + describe("coerceFont", function() { + var fontAttrs = Plots.fontAttrs, + extendFlat = Lib.extendFlat, + coerceFont = Lib.coerceFont; + + var defaultFont = { + family: '"Open sans", verdana, arial, sans-serif, DEFAULT', + size: 314159, + color: "neon pink with sparkles" + }; + + var attributes = { + fontWithDefault: { + family: extendFlat({}, fontAttrs.family, { dflt: defaultFont.family }), + size: extendFlat({}, fontAttrs.size, { dflt: defaultFont.size }), + color: extendFlat({}, fontAttrs.color, { dflt: defaultFont.color }) + }, + fontNoDefault: fontAttrs + }; + + var containerIn; + + function coerce(attr, dflt) { + return Lib.coerce(containerIn, {}, attributes, attr, dflt); + } + + it("should insert the full default if no or empty input", function() { + containerIn = undefined; + expect(coerceFont(coerce, "fontWithDefault")).toEqual(defaultFont); + + containerIn = {}; + expect(coerceFont(coerce, "fontNoDefault", defaultFont)).toEqual( + defaultFont + ); + + containerIn = { fontWithDefault: {} }; + expect(coerceFont(coerce, "fontWithDefault")).toEqual(defaultFont); + }); - assert(shouldPass, shouldFail, { - valType: 'colorscale' - }); - }); + it("should fill in defaults for bad inputs", function() { + containerIn = { + fontWithDefault: { family: "", size: "a million", color: 42 } + }; + expect(coerceFont(coerce, "fontWithDefault")).toEqual(defaultFont); + }); - it('should work for valType \'angle\' where', function() { - var shouldPass = ['auto', '120', 270], - shouldFail = [{}, [], 'red', null, undefined, '']; + it("should pass through individual valid pieces", function() { + var goodFamily = "A fish", + // for now any non-blank string is OK + badFamily = 42, + goodSize = 123.456, + badSize = "ginormous", + goodColor = "red", + badColor = "a dark and stormy night"; + + containerIn = { + fontWithDefault: { family: goodFamily, size: badSize, color: badColor } + }; + expect(coerceFont(coerce, "fontWithDefault")).toEqual({ + family: goodFamily, + size: defaultFont.size, + color: defaultFont.color + }); + + containerIn = { + fontWithDefault: { family: badFamily, size: goodSize, color: badColor } + }; + expect(coerceFont(coerce, "fontWithDefault")).toEqual({ + family: defaultFont.family, + size: goodSize, + color: defaultFont.color + }); + + containerIn = { + fontWithDefault: { family: badFamily, size: badSize, color: goodColor } + }; + expect(coerceFont(coerce, "fontWithDefault")).toEqual({ + family: defaultFont.family, + size: defaultFont.size, + color: goodColor + }); + }); + }); + + describe("init2dArray", function() { + it("should initialize a 2d array with the correct dimenstions", function() { + var array = Lib.init2dArray(4, 5); + expect(array.length).toEqual(4); + expect(array[0].length).toEqual(5); + expect(array[3].length).toEqual(5); + }); + }); - assert(shouldPass, shouldFail, { - valType: 'angle', - dflt: 0 - }); - }); + describe("validate", function() { + function assert(shouldPass, shouldFail, valObject) { + shouldPass.forEach(function(v) { + var res = Lib.validate(v, valObject); + expect(res).toBe(true, JSON.stringify(v) + " should pass"); + }); - it('should work for valType \'subplotid\' where', function() { - var shouldPass = ['sp', 'sp4', 'sp10'], - shouldFail = [{}, [], 'sp1', 'sp0', 'spee1', null, undefined, true]; + shouldFail.forEach(function(v) { + var res = Lib.validate(v, valObject); + expect(res).toBe(false, JSON.stringify(v) + " should fail"); + }); + } - assert(shouldPass, shouldFail, { - valType: 'subplotid', - dflt: 'sp' - }); - }); + it("should work for valType 'data_array' where", function() { + var shouldPass = [[20], []], shouldFail = ["a", {}, 20, undefined, null]; - it('should work for valType \'flaglist\' where', function() { - var shouldPass = ['a', 'b', 'a+b', 'b+a', 'c'], - shouldFail = [{}, [], 'red', null, undefined, '', 'a + b']; + assert(shouldPass, shouldFail, { valType: "data_array" }); - assert(shouldPass, shouldFail, { - valType: 'flaglist', - flags: ['a', 'b'], - extras: ['c'] - }); - }); + assert(shouldPass, shouldFail, { valType: "data_array", dflt: [1, 2] }); + }); - it('should work for valType \'any\' where', function() { - var shouldPass = ['', '120', null, false, {}, []], - shouldFail = [undefined]; + it("should work for valType 'enumerated' where", function() { + assert(["a", "b"], ["c", 1, null, undefined, ""], { + valType: "enumerated", + values: ["a", "b"], + dflt: "a" + }); + + assert([1, "1", 2, "2"], ["c", 3, null, undefined, ""], { + valType: "enumerated", + values: [1, 2], + coerceNumber: true, + dflt: 1 + }); + + assert(["a", "b", [1, 2]], ["c", 1, null, undefined, ""], { + valType: "enumerated", + values: ["a", "b"], + arrayOk: true, + dflt: "a" + }); + }); - assert(shouldPass, shouldFail, { - valType: 'any' - }); - }); + it("should work for valType 'boolean' where", function() { + var shouldPass = [true, false], + shouldFail = ["a", 1, {}, [], null, undefined, ""]; - it('should work for valType \'info_array\' where', function() { - var shouldPass = [[1, 2], [-20, '20']], - shouldFail = [ - {}, [], [10], [null, 10], ['aads', null], - 'red', null, undefined, '', - [1, 10, null] - ]; - - assert(shouldPass, shouldFail, { - valType: 'info_array', - items: [{ - valType: 'number', dflt: -20 - }, { - valType: 'number', dflt: 20 - }] - }); - }); + assert(shouldPass, shouldFail, { valType: "boolean", dflt: true }); - it('should work for valType \'info_array\' (freeLength case)', function() { - var shouldPass = [ - ['marker.color', 'red'], - [{ 'marker.color': 'red' }, [1, 2]] - ]; - var shouldFail = [ - ['marker.color', 'red', 'red'], - [{ 'marker.color': 'red' }, [1, 2], 'blue'] - ]; - - assert(shouldPass, shouldFail, { - valType: 'info_array', - freeLength: true, - items: [{ - valType: 'any' - }, { - valType: 'any' - }, { - valType: 'number' - }] - }); - }); + assert(shouldPass, shouldFail, { valType: "boolean", dflt: false }); }); - describe('setCursor', function() { + it("should work for valType 'number' where", function() { + var shouldPass = [20, "20", 1e6], + shouldFail = ["a", [], {}, null, undefined, ""]; - beforeEach(function() { - this.el3 = d3.select(createGraphDiv()); - }); + assert(shouldPass, shouldFail, { valType: "number" }); - afterEach(destroyGraphDiv); + assert(shouldPass, shouldFail, { valType: "number", dflt: null }); - it('should assign cursor- class', function() { - setCursor(this.el3, 'one'); + assert([20, "20"], [-10, "-10", 25, "25"], { + valType: "number", + dflt: 20, + min: 0, + max: 21 + }); - expect(this.el3.attr('class')).toEqual('cursor-one'); - }); - - it('should assign cursor- class while present non-cursor- classes', function() { - this.el3.classed('one', true); - this.el3.classed('two', true); - this.el3.classed('three', true); - setCursor(this.el3, 'one'); + assert([20, "20", [1, 2]], ["a", {}], { + valType: "number", + dflt: 20, + arrayOk: true + }); + }); - expect(this.el3.attr('class')).toEqual('one two three cursor-one'); - }); + it("should work for valType 'integer' where", function() { + assert( + [1, 2, "3", "4"], + ["a", 1.321321, {}, [], null, 2 / 3, undefined, null], + { valType: "integer", dflt: 1 } + ); + + assert([1, 2, "3", "4"], [-1, "-2", 2.121, null, undefined, [], {}], { + valType: "integer", + min: 0, + dflt: 1 + }); + }); - it('should update class from one cursor- class to another', function() { - this.el3.classed('cursor-one', true); - setCursor(this.el3, 'two'); + it("should work for valType 'string' where", function() { + var date = new Date(2016, 1, 1); + + assert( + ["3", "4", "a", 3, 1.2113, ""], + [undefined, {}, [], null, date, false], + { valType: "string", dflt: "a" } + ); + + assert( + ["3", "4", "a", 3, 1.2113], + ["", undefined, {}, [], null, date, true], + { valType: "string", dflt: "a", noBlank: true } + ); + + assert(["3", "4", ""], [undefined, 1, {}, [], null, date, true, false], { + valType: "string", + dflt: "a", + strict: true + }); + + assert(["3", "4"], [undefined, 1, {}, [], null, date, "", true, false], { + valType: "string", + dflt: "a", + strict: true, + noBlank: true + }); + }); - expect(this.el3.attr('class')).toEqual('cursor-two'); - }); + it("should work for valType 'color' where", function() { + var shouldPass = ["red", "#d3d3d3", "rgba(0,255,255,0.1)"], + shouldFail = [1, {}, [], "rgq(233,122,332,1)", null, undefined]; - it('should update multiple cursor- classes', function() { - this.el3.classed('cursor-one', true); - this.el3.classed('cursor-two', true); - this.el3.classed('cursor-three', true); - setCursor(this.el3, 'four'); + assert(shouldPass, shouldFail, { valType: "color" }); + }); - expect(this.el3.attr('class')).toEqual('cursor-four'); - }); + it("should work for valType 'colorscale' where", function() { + var good = [[0, "red"], [1, "blue"]], + bad = [[0.1, "red"], [1, "blue"]], + bad2 = [[0], [1]], + bad3 = [["red"], ["blue"]], + bad4 = ["red", "blue"]; - it('should remove cursor- if no new class is given', function() { - this.el3.classed('cursor-one', true); - this.el3.classed('cursor-two', true); - this.el3.classed('cursor-three', true); - setCursor(this.el3); + var shouldPass = ["Viridis", "Greens", good], + shouldFail = ["red", 1, undefined, null, {}, [], bad, bad2, bad3, bad4]; - expect(this.el3.attr('class')).toEqual(''); - }); + assert(shouldPass, shouldFail, { valType: "colorscale" }); }); - describe('overrideCursor', function() { + it("should work for valType 'angle' where", function() { + var shouldPass = ["auto", "120", 270], + shouldFail = [{}, [], "red", null, undefined, ""]; - beforeEach(function() { - this.el3 = d3.select(createGraphDiv()); - }); + assert(shouldPass, shouldFail, { valType: "angle", dflt: 0 }); + }); - afterEach(destroyGraphDiv); + it("should work for valType 'subplotid' where", function() { + var shouldPass = ["sp", "sp4", "sp10"], + shouldFail = [{}, [], "sp1", "sp0", "spee1", null, undefined, true]; - it('should apply the new cursor(s) and revert to the original when removed', function() { - this.el3 - .classed('cursor-before', true) - .classed('not-a-cursor', true) - .classed('another', true); + assert(shouldPass, shouldFail, { valType: "subplotid", dflt: "sp" }); + }); - overrideCursor(this.el3, 'after'); - expect(this.el3.attr('class')).toBe('not-a-cursor another cursor-after'); + it("should work for valType 'flaglist' where", function() { + var shouldPass = ["a", "b", "a+b", "b+a", "c"], + shouldFail = [{}, [], "red", null, undefined, "", "a + b"]; - overrideCursor(this.el3, 'later'); - expect(this.el3.attr('class')).toBe('not-a-cursor another cursor-later'); + assert(shouldPass, shouldFail, { + valType: "flaglist", + flags: ["a", "b"], + extras: ["c"] + }); + }); - overrideCursor(this.el3); - expect(this.el3.attr('class')).toBe('not-a-cursor another cursor-before'); - }); + it("should work for valType 'any' where", function() { + var shouldPass = ["", "120", null, false, {}, []], + shouldFail = [undefined]; - it('should apply the new cursor(s) and revert to the none when removed', function() { - this.el3 - .classed('not-a-cursor', true) - .classed('another', true); + assert(shouldPass, shouldFail, { valType: "any" }); + }); - overrideCursor(this.el3, 'after'); - expect(this.el3.attr('class')).toBe('not-a-cursor another cursor-after'); + it("should work for valType 'info_array' where", function() { + var shouldPass = [[1, 2], [-20, "20"]], + shouldFail = [ + {}, + [], + [10], + [null, 10], + ["aads", null], + "red", + null, + undefined, + "", + [1, 10, null] + ]; + + assert(shouldPass, shouldFail, { + valType: "info_array", + items: [ + { valType: "number", dflt: -20 }, + { valType: "number", dflt: 20 } + ] + }); + }); - overrideCursor(this.el3, 'later'); - expect(this.el3.attr('class')).toBe('not-a-cursor another cursor-later'); + it("should work for valType 'info_array' (freeLength case)", function() { + var shouldPass = [ + ["marker.color", "red"], + [{ "marker.color": "red" }, [1, 2]] + ]; + var shouldFail = [ + ["marker.color", "red", "red"], + [{ "marker.color": "red" }, [1, 2], "blue"] + ]; + + assert(shouldPass, shouldFail, { + valType: "info_array", + freeLength: true, + items: [{ valType: "any" }, { valType: "any" }, { valType: "number" }] + }); + }); + }); - overrideCursor(this.el3); - expect(this.el3.attr('class')).toBe('not-a-cursor another'); - }); + describe("setCursor", function() { + beforeEach(function() { + this.el3 = d3.select(createGraphDiv()); + }); - it('should do nothing if no existing or new override is present', function() { - this.el3 - .classed('cursor-before', true) - .classed('not-a-cursor', true); + afterEach(destroyGraphDiv); - overrideCursor(this.el3); + it("should assign cursor- class", function() { + setCursor(this.el3, "one"); - expect(this.el3.attr('class')).toBe('cursor-before not-a-cursor'); - }); + expect(this.el3.attr("class")).toEqual("cursor-one"); }); - describe('pushUnique', function() { + it( + "should assign cursor- class while present non-cursor- classes", + function() { + this.el3.classed("one", true); + this.el3.classed("two", true); + this.el3.classed("three", true); + setCursor(this.el3, "one"); - beforeEach(function() { - this.obj = { a: 'A' }; - this.array = ['a', 'b', 'c', this.obj]; - }); + expect(this.el3.attr("class")).toEqual("one two three cursor-one"); + } + ); - it('should fill new items in array', function() { - var out = Lib.pushUnique(this.array, 'd'); + it("should update class from one cursor- class to another", function() { + this.el3.classed("cursor-one", true); + setCursor(this.el3, "two"); - expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }, 'd']); - expect(this.array).toBe(out); - }); + expect(this.el3.attr("class")).toEqual("cursor-two"); + }); - it('should ignore falsy items', function() { - Lib.pushUnique(this.array, false); - expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); + it("should update multiple cursor- classes", function() { + this.el3.classed("cursor-one", true); + this.el3.classed("cursor-two", true); + this.el3.classed("cursor-three", true); + setCursor(this.el3, "four"); - Lib.pushUnique(this.array, undefined); - expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); + expect(this.el3.attr("class")).toEqual("cursor-four"); + }); - Lib.pushUnique(this.array, 0); - expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); + it("should remove cursor- if no new class is given", function() { + this.el3.classed("cursor-one", true); + this.el3.classed("cursor-two", true); + this.el3.classed("cursor-three", true); + setCursor(this.el3); - Lib.pushUnique(this.array, null); - expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); + expect(this.el3.attr("class")).toEqual(""); + }); + }); - Lib.pushUnique(this.array, ''); - expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); - }); + describe("overrideCursor", function() { + beforeEach(function() { + this.el3 = d3.select(createGraphDiv()); + }); - it('should ignore item already in array', function() { - Lib.pushUnique(this.array, 'a'); - expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); + afterEach(destroyGraphDiv); + + it( + "should apply the new cursor(s) and revert to the original when removed", + function() { + this.el3 + .classed("cursor-before", true) + .classed("not-a-cursor", true) + .classed("another", true); + + overrideCursor(this.el3, "after"); + expect(this.el3.attr("class")).toBe( + "not-a-cursor another cursor-after" + ); + + overrideCursor(this.el3, "later"); + expect(this.el3.attr("class")).toBe( + "not-a-cursor another cursor-later" + ); + + overrideCursor(this.el3); + expect(this.el3.attr("class")).toBe( + "not-a-cursor another cursor-before" + ); + } + ); + + it( + "should apply the new cursor(s) and revert to the none when removed", + function() { + this.el3.classed("not-a-cursor", true).classed("another", true); + + overrideCursor(this.el3, "after"); + expect(this.el3.attr("class")).toBe( + "not-a-cursor another cursor-after" + ); + + overrideCursor(this.el3, "later"); + expect(this.el3.attr("class")).toBe( + "not-a-cursor another cursor-later" + ); + + overrideCursor(this.el3); + expect(this.el3.attr("class")).toBe("not-a-cursor another"); + } + ); + + it( + "should do nothing if no existing or new override is present", + function() { + this.el3.classed("cursor-before", true).classed("not-a-cursor", true); + + overrideCursor(this.el3); + + expect(this.el3.attr("class")).toBe("cursor-before not-a-cursor"); + } + ); + }); + + describe("pushUnique", function() { + beforeEach(function() { + this.obj = { a: "A" }; + this.array = ["a", "b", "c", this.obj]; + }); - Lib.pushUnique(this.array, this.obj); - expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); + it("should fill new items in array", function() { + var out = Lib.pushUnique(this.array, "d"); - }); + expect(this.array).toEqual(["a", "b", "c", { a: "A" }, "d"]); + expect(this.array).toBe(out); }); - describe('filterUnique', function() { + it("should ignore falsy items", function() { + Lib.pushUnique(this.array, false); + expect(this.array).toEqual(["a", "b", "c", { a: "A" }]); - it('should return array containing unique values', function() { - expect( - Lib.filterUnique(['a', 'a', 'b', 'b']) - ) - .toEqual(['a', 'b']); + Lib.pushUnique(this.array, undefined); + expect(this.array).toEqual(["a", "b", "c", { a: "A" }]); - expect( - Lib.filterUnique(['1', ['1'], 1]) - ) - .toEqual(['1']); + Lib.pushUnique(this.array, 0); + expect(this.array).toEqual(["a", "b", "c", { a: "A" }]); - expect( - Lib.filterUnique([1, '1', [1]]) - ) - .toEqual([1]); + Lib.pushUnique(this.array, null); + expect(this.array).toEqual(["a", "b", "c", { a: "A" }]); - expect( - Lib.filterUnique([ { a: 1 }, { b: 2 }]) - ) - .toEqual([{ a: 1 }]); + Lib.pushUnique(this.array, ""); + expect(this.array).toEqual(["a", "b", "c", { a: "A" }]); + }); - expect( - Lib.filterUnique([null, undefined, null, null, undefined]) - ) - .toEqual([null, undefined]); - }); + it("should ignore item already in array", function() { + Lib.pushUnique(this.array, "a"); + expect(this.array).toEqual(["a", "b", "c", { a: "A" }]); + Lib.pushUnique(this.array, this.obj); + expect(this.array).toEqual(["a", "b", "c", { a: "A" }]); }); + }); - describe('numSeparate', function() { + describe("filterUnique", function() { + it("should return array containing unique values", function() { + expect(Lib.filterUnique(["a", "a", "b", "b"])).toEqual(["a", "b"]); - it('should work on numbers and strings', function() { - expect(Lib.numSeparate(12345.67, '.,')).toBe('12,345.67'); - expect(Lib.numSeparate('12345.67', '.,')).toBe('12,345.67'); - }); + expect(Lib.filterUnique(["1", ["1"], 1])).toEqual(["1"]); - it('should ignore years', function() { - expect(Lib.numSeparate(2016, '.,')).toBe('2016'); - }); + expect(Lib.filterUnique([1, "1", [1]])).toEqual([1]); - it('should work even for 4-digit integer if third argument is true', function() { - expect(Lib.numSeparate(3000, '.,', true)).toBe('3,000'); - }); + expect(Lib.filterUnique([{ a: 1 }, { b: 2 }])).toEqual([{ a: 1 }]); - it('should work for multiple thousands', function() { - expect(Lib.numSeparate(1000000000, '.,')).toBe('1,000,000,000'); - }); - - it('should work when there\'s only one separator', function() { - expect(Lib.numSeparate(12.34, '|')).toBe('12|34'); - expect(Lib.numSeparate(1234.56, '|')).toBe('1234|56'); - }); - - it('should throw an error when no separator is provided', function() { - expect(function() { - Lib.numSeparate(1234); - }).toThrowError('Separator string required for formatting!'); + expect( + Lib.filterUnique([null, undefined, null, null, undefined]) + ).toEqual([null, undefined]); + }); + }); - expect(function() { - Lib.numSeparate(1234, ''); - }).toThrowError('Separator string required for formatting!'); - }); + describe("numSeparate", function() { + it("should work on numbers and strings", function() { + expect(Lib.numSeparate(12345.67, ".,")).toBe("12,345.67"); + expect(Lib.numSeparate("12345.67", ".,")).toBe("12,345.67"); }); - describe('cleanNumber', function() { - it('should return finite numbers untouched', function() { - [ - 0, 1, 2, 1234.567, - -1, -100, -999.999, - Number.MAX_VALUE, Number.MIN_VALUE, Number.EPSILON, - -Number.MAX_VALUE, -Number.MIN_VALUE, -Number.EPSILON - ].forEach(function(v) { - expect(Lib.cleanNumber(v)).toBe(v); - }); - }); + it("should ignore years", function() { + expect(Lib.numSeparate(2016, ".,")).toBe("2016"); + }); - it('should accept number strings with arbitrary cruft on the outside', function() { - [ - ['0', 0], - ['1', 1], - ['1.23', 1.23], - ['-100.001', -100.001], - [' $4.325 #%\t', 4.325], - [' " #1" ', 1], - [' \'\n \r -9.2e7 \t\' ', -9.2e7], - ['1,690,000', 1690000], - ['1 690 000', 1690000], - ['2 2', 22], - ['$5,162,000.00', 5162000], - [' $1,410,000.00 ', 1410000], - ].forEach(function(v) { - expect(Lib.cleanNumber(v[0])).toBe(v[1], v[0]); - }); - }); + it( + "should work even for 4-digit integer if third argument is true", + function() { + expect(Lib.numSeparate(3000, ".,", true)).toBe("3,000"); + } + ); - it('should not accept other objects or cruft in the middle', function() { - [ - NaN, Infinity, -Infinity, null, undefined, new Date(), '', - ' ', '\t', '2\t2', '2%2', '2$2', {1: 2}, [1], ['1'], {}, [] - ].forEach(function(v) { - expect(Lib.cleanNumber(v)).toBeUndefined(v); - }); - }); + it("should work for multiple thousands", function() { + expect(Lib.numSeparate(1000000000, ".,")).toBe("1,000,000,000"); }); - describe('isPlotDiv', function() { - it('should work on plain objects', function() { - expect(Lib.isPlotDiv({})).toBe(false); - }); + it("should work when there's only one separator", function() { + expect(Lib.numSeparate(12.34, "|")).toBe("12|34"); + expect(Lib.numSeparate(1234.56, "|")).toBe("1234|56"); }); - describe('isD3Selection', function() { - var gd; + it("should throw an error when no separator is provided", function() { + expect(function() { + Lib.numSeparate(1234); + }).toThrowError("Separator string required for formatting!"); - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(function() { - destroyGraphDiv(); - Plotly.setPlotConfig({ queueLength: 0 }); - }); + expect(function() { + Lib.numSeparate(1234, ""); + }).toThrowError("Separator string required for formatting!"); + }); + }); + + describe("cleanNumber", function() { + it("should return finite numbers untouched", function() { + [ + 0, + 1, + 2, + 1234.567, + -1, + -100, + -999.999, + Number.MAX_VALUE, + Number.MIN_VALUE, + Number.EPSILON, + -Number.MAX_VALUE, + -Number.MIN_VALUE, + -Number.EPSILON + ].forEach(function(v) { + expect(Lib.cleanNumber(v)).toBe(v); + }); + }); - it('recognizes real and duck typed selections', function() { - var yesSelections = [ - d3.select(gd), - // this is what got us into trouble actually - d3 selections can - // contain non-nodes - say for example d3 selections! then they - // don't work correctly. But it makes a convenient test! - d3.select(1), - // just showing what we actually do in this function: duck type - // using the `classed` method. - {classed: function(v) { return !!v; }} - ]; - - yesSelections.forEach(function(v) { - expect(Lib.isD3Selection(v)).toBe(true, v); - }); - }); + it( + "should accept number strings with arbitrary cruft on the outside", + function() { + [ + ["0", 0], + ["1", 1], + ["1.23", 1.23], + ["-100.001", -100.001], + [" $4.325 #%\t", 4.325], + [' " #1" ', 1], + [" '\n \r -9.2e7 \t' ", -9.2e7], + ["1,690,000", 1690000], + ["1 690 000", 1690000], + ["2 2", 22], + ["$5,162,000.00", 5162000], + [" $1,410,000.00 ", 1410000] + ].forEach(function(v) { + expect(Lib.cleanNumber(v[0])).toBe(v[1], v[0]); + }); + } + ); + + it("should not accept other objects or cruft in the middle", function() { + [ + NaN, + Infinity, + -Infinity, + null, + undefined, + new Date(), + "", + " ", + "\t", + "2\t2", + "2%2", + "2$2", + { 1: 2 }, + [1], + ["1"], + {}, + [] + ].forEach(function(v) { + expect(Lib.cleanNumber(v)).toBeUndefined(v); + }); + }); + }); - it('rejects non-selections', function() { - var notSelections = [ - 1, - 'path', - [1, 2], - [[1, 2]], - {classed: 1}, - gd - ]; - - notSelections.forEach(function(v) { - expect(Lib.isD3Selection(v)).toBe(false, v); - }); - }); + describe("isPlotDiv", function() { + it("should work on plain objects", function() { + expect(Lib.isPlotDiv({})).toBe(false); }); + }); - describe('loggers', function() { - var stashConsole, - stashLogLevel; + describe("isD3Selection", function() { + var gd; - function consoleFn(name, hasApply, messages) { - var out = function() { - var args = []; - for(var i = 0; i < arguments.length; i++) args.push(arguments[i]); - messages.push([name, args]); - }; + beforeEach(function() { + gd = createGraphDiv(); + }); - if(!hasApply) out.apply = undefined; + afterEach(function() { + destroyGraphDiv(); + Plotly.setPlotConfig({ queueLength: 0 }); + }); - return out; + it("recognizes real and duck typed selections", function() { + var yesSelections = [ + d3.select(gd), + // this is what got us into trouble actually - d3 selections can + // contain non-nodes - say for example d3 selections! then they + // don't work correctly. But it makes a convenient test! + d3.select(1), + // just showing what we actually do in this function: duck type + // using the `classed` method. + { + classed: function(v) { + return !!v; + } } + ]; - function mockConsole(hasApply, hasTrace) { - var out = { - MESSAGES: [] - }; - out.log = consoleFn('log', hasApply, out.MESSAGES); - out.error = consoleFn('error', hasApply, out.MESSAGES); + yesSelections.forEach(function(v) { + expect(Lib.isD3Selection(v)).toBe(true, v); + }); + }); - if(hasTrace) out.trace = consoleFn('trace', hasApply, out.MESSAGES); + it("rejects non-selections", function() { + var notSelections = [1, "path", [1, 2], [[1, 2]], { classed: 1 }, gd]; - return out; - } + notSelections.forEach(function(v) { + expect(Lib.isD3Selection(v)).toBe(false, v); + }); + }); + }); - beforeEach(function() { - stashConsole = window.console; - stashLogLevel = config.logging; - }); + describe("loggers", function() { + var stashConsole, stashLogLevel; - afterEach(function() { - window.console = stashConsole; - config.logging = stashLogLevel; - }); + function consoleFn(name, hasApply, messages) { + var out = function() { + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); + } + messages.push([name, args]); + }; - it('emits one console message if apply is available', function() { - var c = window.console = mockConsole(true, true); - config.logging = 2; + if (!hasApply) out.apply = undefined; - Lib.log('tick', 'tock', 'tick', 'tock', 1); - Lib.warn('I\'m', 'a', 'little', 'cuckoo', 'clock', [1, 2]); - Lib.error('cuckoo!', 'cuckoo!!!', {a: 1, b: 2}); + return out; + } - expect(c.MESSAGES).toEqual([ - ['trace', ['LOG:', 'tick', 'tock', 'tick', 'tock', 1]], - ['trace', ['WARN:', 'I\'m', 'a', 'little', 'cuckoo', 'clock', [1, 2]]], - ['error', ['ERROR:', 'cuckoo!', 'cuckoo!!!', {a: 1, b: 2}]] - ]); - }); + function mockConsole(hasApply, hasTrace) { + var out = { MESSAGES: [] }; + out.log = consoleFn("log", hasApply, out.MESSAGES); + out.error = consoleFn("error", hasApply, out.MESSAGES); - it('falls back on console.log if no trace', function() { - var c = window.console = mockConsole(true, false); - config.logging = 2; + if (hasTrace) out.trace = consoleFn("trace", hasApply, out.MESSAGES); - Lib.log('Hi'); - Lib.warn(42); + return out; + } - expect(c.MESSAGES).toEqual([ - ['log', ['LOG:', 'Hi']], - ['log', ['WARN:', 42]] - ]); - }); + beforeEach(function() { + stashConsole = window.console; + stashLogLevel = config.logging; + }); - it('falls back on separate calls if no apply', function() { - var c = window.console = mockConsole(false, false); - config.logging = 2; - - Lib.log('tick', 'tock', 'tick', 'tock', 1); - Lib.warn('I\'m', 'a', 'little', 'cuckoo', 'clock', [1, 2]); - Lib.error('cuckoo!', 'cuckoo!!!', {a: 1, b: 2}); - - expect(c.MESSAGES).toEqual([ - ['log', ['LOG:']], - ['log', ['tick']], - ['log', ['tock']], - ['log', ['tick']], - ['log', ['tock']], - ['log', [1]], - ['log', ['WARN:']], - ['log', ['I\'m']], - ['log', ['a']], - ['log', ['little']], - ['log', ['cuckoo']], - ['log', ['clock']], - ['log', [[1, 2]]], - ['error', ['ERROR:']], - ['error', ['cuckoo!']], - ['error', ['cuckoo!!!']], - ['error', [{a: 1, b: 2}]] - ]); - }); + afterEach(function() { + window.console = stashConsole; + config.logging = stashLogLevel; + }); - it('omits .log at log level 1', function() { - var c = window.console = mockConsole(true, true); - config.logging = 1; + it("emits one console message if apply is available", function() { + var c = window.console = mockConsole(true, true); + config.logging = 2; - Lib.log(1); - Lib.warn(2); - Lib.error(3); + Lib.log("tick", "tock", "tick", "tock", 1); + Lib.warn("I'm", "a", "little", "cuckoo", "clock", [1, 2]); + Lib.error("cuckoo!", "cuckoo!!!", { a: 1, b: 2 }); - expect(c.MESSAGES).toEqual([ - ['trace', ['WARN:', 2]], - ['error', ['ERROR:', 3]] - ]); - }); + expect(c.MESSAGES).toEqual([ + ["trace", ["LOG:", "tick", "tock", "tick", "tock", 1]], + ["trace", ["WARN:", "I'm", "a", "little", "cuckoo", "clock", [1, 2]]], + ["error", ["ERROR:", "cuckoo!", "cuckoo!!!", { a: 1, b: 2 }]] + ]); + }); - it('logs nothing at log level 0', function() { - var c = window.console = mockConsole(true, true); - config.logging = 0; + it("falls back on console.log if no trace", function() { + var c = window.console = mockConsole(true, false); + config.logging = 2; - Lib.log(1); - Lib.warn(2); - Lib.error(3); + Lib.log("Hi"); + Lib.warn(42); - expect(c.MESSAGES).toEqual([]); - }); + expect(c.MESSAGES).toEqual([ + ["log", ["LOG:", "Hi"]], + ["log", ["WARN:", 42]] + ]); }); -}); -describe('Queue', function() { - 'use strict'; + it("falls back on separate calls if no apply", function() { + var c = window.console = mockConsole(false, false); + config.logging = 2; + + Lib.log("tick", "tock", "tick", "tock", 1); + Lib.warn("I'm", "a", "little", "cuckoo", "clock", [1, 2]); + Lib.error("cuckoo!", "cuckoo!!!", { a: 1, b: 2 }); + + expect(c.MESSAGES).toEqual([ + ["log", ["LOG:"]], + ["log", ["tick"]], + ["log", ["tock"]], + ["log", ["tick"]], + ["log", ["tock"]], + ["log", [1]], + ["log", ["WARN:"]], + ["log", ["I'm"]], + ["log", ["a"]], + ["log", ["little"]], + ["log", ["cuckoo"]], + ["log", ["clock"]], + ["log", [[1, 2]]], + ["error", ["ERROR:"]], + ["error", ["cuckoo!"]], + ["error", ["cuckoo!!!"]], + ["error", [{ a: 1, b: 2 }]] + ]); + }); - var gd; + it("omits .log at log level 1", function() { + var c = window.console = mockConsole(true, true); + config.logging = 1; - beforeEach(function() { - gd = createGraphDiv(); - }); + Lib.log(1); + Lib.warn(2); + Lib.error(3); - afterEach(function() { - destroyGraphDiv(); - Plotly.setPlotConfig({ queueLength: 0 }); + expect(c.MESSAGES).toEqual([ + ["trace", ["WARN:", 2]], + ["error", ["ERROR:", 3]] + ]); }); - it('should not fill in undoQueue by default', function(done) { - Plotly.plot(gd, [{ - y: [2, 1, 2] - }]).then(function() { - expect(gd.undoQueue).toBeUndefined(); - - return Plotly.restyle(gd, 'marker.color', 'red'); - }).then(function() { - expect(gd.undoQueue.index).toEqual(0); - expect(gd.undoQueue.queue).toEqual([]); + it("logs nothing at log level 0", function() { + var c = window.console = mockConsole(true, true); + config.logging = 0; - return Plotly.relayout(gd, 'title', 'A title'); - }).then(function() { - expect(gd.undoQueue.index).toEqual(0); - expect(gd.undoQueue.queue).toEqual([]); + Lib.log(1); + Lib.warn(2); + Lib.error(3); - done(); - }); + expect(c.MESSAGES).toEqual([]); }); + }); +}); - it('should fill in undoQueue up to value found in *queueLength* config', function(done) { - Plotly.setPlotConfig({ queueLength: 2 }); - - Plotly.plot(gd, [{ - y: [2, 1, 2] - }]) +describe("Queue", function() { + "use strict"; + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + destroyGraphDiv(); + Plotly.setPlotConfig({ queueLength: 0 }); + }); + + it("should not fill in undoQueue by default", function(done) { + Plotly.plot(gd, [{ y: [2, 1, 2] }]) + .then(function() { + expect(gd.undoQueue).toBeUndefined(); + + return Plotly.restyle(gd, "marker.color", "red"); + }) + .then(function() { + expect(gd.undoQueue.index).toEqual(0); + expect(gd.undoQueue.queue).toEqual([]); + + return Plotly.relayout(gd, "title", "A title"); + }) + .then(function() { + expect(gd.undoQueue.index).toEqual(0); + expect(gd.undoQueue.queue).toEqual([]); + + done(); + }); + }); + + it( + "should fill in undoQueue up to value found in *queueLength* config", + function(done) { + Plotly.setPlotConfig({ queueLength: 2 }); + + Plotly.plot(gd, [{ y: [2, 1, 2] }]) .then(function() { - expect(gd.undoQueue).toBeUndefined(); + expect(gd.undoQueue).toBeUndefined(); - return Plotly.restyle(gd, 'marker.color', 'red'); + return Plotly.restyle(gd, "marker.color", "red"); }) .then(function() { - expect(gd.undoQueue.index).toEqual(1); - expect(gd.undoQueue.queue[0].undo.args[0][1]['marker.color']).toEqual([undefined]); - expect(gd.undoQueue.queue[0].redo.args[0][1]['marker.color']).toEqual('red'); - - return Plotly.relayout(gd, 'title', 'A title'); + expect(gd.undoQueue.index).toEqual(1); + expect( + gd.undoQueue.queue[0].undo.args[0][1]["marker.color"] + ).toEqual([undefined]); + expect(gd.undoQueue.queue[0].redo.args[0][1]["marker.color"]).toEqual( + "red" + ); + + return Plotly.relayout(gd, "title", "A title"); }) .then(function() { - expect(gd.undoQueue.index).toEqual(2); - expect(gd.undoQueue.queue[1].undo.args[0][1].title).toEqual(undefined); - expect(gd.undoQueue.queue[1].redo.args[0][1].title).toEqual('A title'); - - return Plotly.restyle(gd, 'mode', 'markers'); + expect(gd.undoQueue.index).toEqual(2); + expect(gd.undoQueue.queue[1].undo.args[0][1].title).toEqual( + undefined + ); + expect(gd.undoQueue.queue[1].redo.args[0][1].title).toEqual( + "A title" + ); + + return Plotly.restyle(gd, "mode", "markers"); }) .then(function() { - expect(gd.undoQueue.index).toEqual(2); - expect(gd.undoQueue.queue[2]).toBeUndefined(); - - expect(gd.undoQueue.queue[1].undo.args[0][1].mode).toEqual([undefined]); - expect(gd.undoQueue.queue[1].redo.args[0][1].mode).toEqual('markers'); - - expect(gd.undoQueue.queue[0].undo.args[0][1].title).toEqual(undefined); - expect(gd.undoQueue.queue[0].redo.args[0][1].title).toEqual('A title'); - - return Plotly.restyle(gd, 'transforms[0]', { type: 'filter' }); + expect(gd.undoQueue.index).toEqual(2); + expect(gd.undoQueue.queue[2]).toBeUndefined(); + + expect(gd.undoQueue.queue[1].undo.args[0][1].mode).toEqual([ + undefined + ]); + expect(gd.undoQueue.queue[1].redo.args[0][1].mode).toEqual("markers"); + + expect(gd.undoQueue.queue[0].undo.args[0][1].title).toEqual( + undefined + ); + expect(gd.undoQueue.queue[0].redo.args[0][1].title).toEqual( + "A title" + ); + + return Plotly.restyle(gd, "transforms[0]", { type: "filter" }); }) .then(function() { - expect(gd.undoQueue.queue[1].undo.args[0][1]) - .toEqual({ 'transforms[0]': null }); - expect(gd.undoQueue.queue[1].redo.args[0][1]) - .toEqual({ 'transforms[0]': { type: 'filter' } }); - - return Plotly.relayout(gd, 'updatemenus[0]', { buttons: [] }); + expect(gd.undoQueue.queue[1].undo.args[0][1]).toEqual({ + "transforms[0]": null + }); + expect(gd.undoQueue.queue[1].redo.args[0][1]).toEqual({ + "transforms[0]": { type: "filter" } + }); + + return Plotly.relayout(gd, "updatemenus[0]", { buttons: [] }); }) .then(function() { - expect(gd.undoQueue.queue[1].undo.args[0][1]) - .toEqual({ 'updatemenus[0]': null }); - expect(gd.undoQueue.queue[1].redo.args[0][1]) - .toEqual({ 'updatemenus[0]': { buttons: [] } }); - - return Plotly.relayout(gd, 'updatemenus[0]', null); + expect(gd.undoQueue.queue[1].undo.args[0][1]).toEqual({ + "updatemenus[0]": null + }); + expect(gd.undoQueue.queue[1].redo.args[0][1]).toEqual({ + "updatemenus[0]": { buttons: [] } + }); + + return Plotly.relayout(gd, "updatemenus[0]", null); }) .then(function() { - expect(gd.undoQueue.queue[1].undo.args[0][1]) - .toEqual({ 'updatemenus[0]': { buttons: []} }); - expect(gd.undoQueue.queue[1].redo.args[0][1]) - .toEqual({ 'updatemenus[0]': null }); - - return Plotly.restyle(gd, 'transforms[0]', null); + expect(gd.undoQueue.queue[1].undo.args[0][1]).toEqual({ + "updatemenus[0]": { buttons: [] } + }); + expect(gd.undoQueue.queue[1].redo.args[0][1]).toEqual({ + "updatemenus[0]": null + }); + + return Plotly.restyle(gd, "transforms[0]", null); }) .then(function() { - expect(gd.undoQueue.queue[1].undo.args[0][1]) - .toEqual({ 'transforms[0]': [ { type: 'filter' } ]}); - expect(gd.undoQueue.queue[1].redo.args[0][1]) - .toEqual({ 'transforms[0]': null }); + expect(gd.undoQueue.queue[1].undo.args[0][1]).toEqual({ + "transforms[0]": [{ type: "filter" }] + }); + expect(gd.undoQueue.queue[1].redo.args[0][1]).toEqual({ + "transforms[0]": null + }); - done(); + done(); }); - }); + } + ); }); diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index b9146242e3c..5b947112c67 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -1,960 +1,1012 @@ -var Plotly = require('@lib'); -var Lib = require('@src/lib'); - -var constants = require('@src/plots/mapbox/constants'); -var supplyLayoutDefaults = require('@src/plots/mapbox/layout_defaults'); - -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var hasWebGLSupport = require('../assets/has_webgl_support'); -var mouseEvent = require('../assets/mouse_event'); -var customMatchers = require('../assets/custom_matchers'); - -var MAPBOX_ACCESS_TOKEN = require('@build/credentials.json').MAPBOX_ACCESS_TOKEN; +var Plotly = require("@lib"); +var Lib = require("@src/lib"); + +var constants = require("@src/plots/mapbox/constants"); +var supplyLayoutDefaults = require("@src/plots/mapbox/layout_defaults"); + +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var hasWebGLSupport = require("../assets/has_webgl_support"); +var mouseEvent = require("../assets/mouse_event"); +var customMatchers = require("../assets/custom_matchers"); + +var MAPBOX_ACCESS_TOKEN = require( + "@build/credentials.json" +).MAPBOX_ACCESS_TOKEN; var TRANSITION_DELAY = 500; var MOUSE_DELAY = 100; var LONG_TIMEOUT_INTERVAL = 5 * jasmine.DEFAULT_TIMEOUT_INTERVAL; var noop = function() {}; -Plotly.setPlotConfig({ - mapboxAccessToken: MAPBOX_ACCESS_TOKEN -}); - +Plotly.setPlotConfig({ mapboxAccessToken: MAPBOX_ACCESS_TOKEN }); -describe('mapbox defaults', function() { - 'use strict'; +describe("mapbox defaults", function() { + "use strict"; + var layoutIn, layoutOut, fullData; - var layoutIn, layoutOut, fullData; + beforeEach(function() { + layoutOut = { font: { color: "red" } }; - beforeEach(function() { - layoutOut = { font: { color: 'red' } }; - - // needs a ternary-ref in a trace in order to be detected - fullData = [{ type: 'scattermapbox', subplot: 'mapbox' }]; - }); - - it('should fill empty containers', function() { - layoutIn = {}; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutIn).toEqual({ mapbox: {} }); - }); - - it('should copy ref to input container in full (for updating on map move)', function() { - var mapbox = { style: 'light '}; - - layoutIn = { mapbox: mapbox }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.mapbox._input).toBe(mapbox); - }); - - it('should accept both string and object style', function() { - var mapboxStyleJSON = { - id: 'cdsa213wqdsa', - owner: 'johnny' - }; - - layoutIn = { - mapbox: { style: 'light' }, - mapbox2: { style: mapboxStyleJSON } - }; - - fullData.push({ type: 'scattermapbox', subplot: 'mapbox2' }); - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.mapbox.style).toEqual('light'); - expect(layoutOut.mapbox2.style).toBe(mapboxStyleJSON); - }); + // needs a ternary-ref in a trace in order to be detected + fullData = [{ type: "scattermapbox", subplot: "mapbox" }]; + }); - it('should fill layer containers', function() { - layoutIn = { - mapbox: { - layers: [{}, {}] - } - }; + it("should fill empty containers", function() { + layoutIn = {}; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.mapbox.layers[0].sourcetype).toEqual('geojson'); - expect(layoutOut.mapbox.layers[1].sourcetype).toEqual('geojson'); - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn).toEqual({ mapbox: {} }); + }); - it('should skip over non-object layer containers', function() { - layoutIn = { - mapbox: { - layers: [{}, null, 'remove', {}] - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.mapbox.layers[0].sourcetype).toEqual('geojson'); - expect(layoutOut.mapbox.layers[0]._index).toEqual(0); - expect(layoutOut.mapbox.layers[1].sourcetype).toEqual('geojson'); - expect(layoutOut.mapbox.layers[1]._index).toEqual(3); - }); + it( + "should copy ref to input container in full (for updating on map move)", + function() { + var mapbox = { style: "light " }; - it('should coerce \'sourcelayer\' only for *vector* \'sourcetype\'', function() { - layoutIn = { - mapbox: { - layers: [{ - sourcetype: 'vector', - sourcelayer: 'layer0' - }, { - sourcetype: 'geojson', - sourcelayer: 'layer0' - }] - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.mapbox.layers[0].sourcelayer).toEqual('layer0'); - expect(layoutOut.mapbox.layers[1].sourcelayer).toBeUndefined(); - }); + layoutIn = { mapbox: mapbox }; - it('should only coerce relevant layer style attributes', function() { - var base = { - line: { width: 3 }, - fill: { outlinecolor: '#d3d3d3' }, - circle: { radius: 20 }, - symbol: { icon: 'monument' } - }; - - layoutIn = { - mapbox: { - layers: [ - Lib.extendFlat({}, base, { - type: 'line', - color: 'red' - }), - Lib.extendFlat({}, base, { - type: 'fill', - color: 'blue' - }), - Lib.extendFlat({}, base, { - type: 'circle', - color: 'green' - }), - Lib.extendFlat({}, base, { - type: 'symbol', - color: 'yellow' - }) - ] - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - - expect(layoutOut.mapbox.layers[0].color).toEqual('red'); - expect(layoutOut.mapbox.layers[0].line.width).toEqual(3); - expect(layoutOut.mapbox.layers[0].fill).toBeUndefined(); - expect(layoutOut.mapbox.layers[0].circle).toBeUndefined(); - expect(layoutOut.mapbox.layers[0].symbol).toBeUndefined(); - - expect(layoutOut.mapbox.layers[1].color).toEqual('blue'); - expect(layoutOut.mapbox.layers[1].fill.outlinecolor).toEqual('#d3d3d3'); - expect(layoutOut.mapbox.layers[1].line).toBeUndefined(); - expect(layoutOut.mapbox.layers[1].circle).toBeUndefined(); - expect(layoutOut.mapbox.layers[1].symbol).toBeUndefined(); - - expect(layoutOut.mapbox.layers[2].color).toEqual('green'); - expect(layoutOut.mapbox.layers[2].circle.radius).toEqual(20); - expect(layoutOut.mapbox.layers[2].line).toBeUndefined(); - expect(layoutOut.mapbox.layers[2].fill).toBeUndefined(); - expect(layoutOut.mapbox.layers[2].symbol).toBeUndefined(); - - expect(layoutOut.mapbox.layers[3].color).toEqual('yellow'); - expect(layoutOut.mapbox.layers[3].symbol.icon).toEqual('monument'); - expect(layoutOut.mapbox.layers[3].line).toBeUndefined(); - expect(layoutOut.mapbox.layers[3].fill).toBeUndefined(); - expect(layoutOut.mapbox.layers[3].circle).toBeUndefined(); - }); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.mapbox._input).toBe(mapbox); + } + ); + + it("should accept both string and object style", function() { + var mapboxStyleJSON = { id: "cdsa213wqdsa", owner: "johnny" }; + + layoutIn = { + mapbox: { style: "light" }, + mapbox2: { style: mapboxStyleJSON } + }; + + fullData.push({ type: "scattermapbox", subplot: "mapbox2" }); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.mapbox.style).toEqual("light"); + expect(layoutOut.mapbox2.style).toBe(mapboxStyleJSON); + }); + + it("should fill layer containers", function() { + layoutIn = { mapbox: { layers: [{}, {}] } }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.mapbox.layers[0].sourcetype).toEqual("geojson"); + expect(layoutOut.mapbox.layers[1].sourcetype).toEqual("geojson"); + }); + + it("should skip over non-object layer containers", function() { + layoutIn = { mapbox: { layers: [{}, null, "remove", {}] } }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.mapbox.layers[0].sourcetype).toEqual("geojson"); + expect(layoutOut.mapbox.layers[0]._index).toEqual(0); + expect(layoutOut.mapbox.layers[1].sourcetype).toEqual("geojson"); + expect(layoutOut.mapbox.layers[1]._index).toEqual(3); + }); + + it("should coerce 'sourcelayer' only for *vector* 'sourcetype'", function() { + layoutIn = { + mapbox: { + layers: [ + { sourcetype: "vector", sourcelayer: "layer0" }, + { sourcetype: "geojson", sourcelayer: "layer0" } + ] + } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.mapbox.layers[0].sourcelayer).toEqual("layer0"); + expect(layoutOut.mapbox.layers[1].sourcelayer).toBeUndefined(); + }); + + it("should only coerce relevant layer style attributes", function() { + var base = { + line: { width: 3 }, + fill: { outlinecolor: "#d3d3d3" }, + circle: { radius: 20 }, + symbol: { icon: "monument" } + }; + + layoutIn = { + mapbox: { + layers: [ + Lib.extendFlat({}, base, { type: "line", color: "red" }), + Lib.extendFlat({}, base, { type: "fill", color: "blue" }), + Lib.extendFlat({}, base, { type: "circle", color: "green" }), + Lib.extendFlat({}, base, { type: "symbol", color: "yellow" }) + ] + } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.mapbox.layers[0].color).toEqual("red"); + expect(layoutOut.mapbox.layers[0].line.width).toEqual(3); + expect(layoutOut.mapbox.layers[0].fill).toBeUndefined(); + expect(layoutOut.mapbox.layers[0].circle).toBeUndefined(); + expect(layoutOut.mapbox.layers[0].symbol).toBeUndefined(); + + expect(layoutOut.mapbox.layers[1].color).toEqual("blue"); + expect(layoutOut.mapbox.layers[1].fill.outlinecolor).toEqual("#d3d3d3"); + expect(layoutOut.mapbox.layers[1].line).toBeUndefined(); + expect(layoutOut.mapbox.layers[1].circle).toBeUndefined(); + expect(layoutOut.mapbox.layers[1].symbol).toBeUndefined(); + + expect(layoutOut.mapbox.layers[2].color).toEqual("green"); + expect(layoutOut.mapbox.layers[2].circle.radius).toEqual(20); + expect(layoutOut.mapbox.layers[2].line).toBeUndefined(); + expect(layoutOut.mapbox.layers[2].fill).toBeUndefined(); + expect(layoutOut.mapbox.layers[2].symbol).toBeUndefined(); + + expect(layoutOut.mapbox.layers[3].color).toEqual("yellow"); + expect(layoutOut.mapbox.layers[3].symbol.icon).toEqual("monument"); + expect(layoutOut.mapbox.layers[3].line).toBeUndefined(); + expect(layoutOut.mapbox.layers[3].fill).toBeUndefined(); + expect(layoutOut.mapbox.layers[3].circle).toBeUndefined(); + }); }); -describe('mapbox credentials', function() { - 'use strict'; - - if(!hasWebGLSupport('mapbox credentials')) return; - - var dummyToken = 'asfdsa124331wersdsa1321q3'; - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - - Plotly.setPlotConfig({ - mapboxAccessToken: null - }); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - - Plotly.setPlotConfig({ - mapboxAccessToken: MAPBOX_ACCESS_TOKEN - }); - }); - - it('should throw error if token is not registered', function() { - expect(function() { - Plotly.plot(gd, [{ - type: 'scattermapbox', - lon: [10, 20, 30], - lat: [10, 20, 30] - }]); - }).toThrow(new Error(constants.noAccessTokenErrorMsg)); - }, LONG_TIMEOUT_INTERVAL); - - it('should throw error if token is invalid', function(done) { - var cnt = 0; - - Plotly.plot(gd, [{ - type: 'scattermapbox', - lon: [10, 20, 30], - lat: [10, 20, 30] - }], {}, { - mapboxAccessToken: dummyToken - }) +describe("mapbox credentials", function() { + "use strict"; + if (!hasWebGLSupport("mapbox credentials")) return; + + var dummyToken = "asfdsa124331wersdsa1321q3"; + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + + Plotly.setPlotConfig({ mapboxAccessToken: null }); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + + Plotly.setPlotConfig({ mapboxAccessToken: MAPBOX_ACCESS_TOKEN }); + }); + + it( + "should throw error if token is not registered", + function() { + expect(function() { + Plotly.plot(gd, [ + { type: "scattermapbox", lon: [10, 20, 30], lat: [10, 20, 30] } + ]); + }).toThrow(new Error(constants.noAccessTokenErrorMsg)); + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should throw error if token is invalid", + function(done) { + var cnt = 0; + + Plotly.plot( + gd, + [{ type: "scattermapbox", lon: [10, 20, 30], lat: [10, 20, 30] }], + {}, + { mapboxAccessToken: dummyToken } + ) .catch(function(err) { - cnt++; - expect(err).toEqual(new Error(constants.mapOnErrorMsg)); + cnt++; + expect(err).toEqual(new Error(constants.mapOnErrorMsg)); }) .then(function() { - expect(cnt).toEqual(1); - done(); + expect(cnt).toEqual(1); + done(); }); - }, LONG_TIMEOUT_INTERVAL); - - it('should use access token in mapbox layout options if present', function(done) { - var cnt = 0; - - Plotly.plot(gd, [{ - type: 'scattermapbox', - lon: [10, 20, 30], - lat: [10, 20, 30] - }], { - mapbox: { - accesstoken: MAPBOX_ACCESS_TOKEN - } - }, { - mapboxAccessToken: dummyToken - }).catch(function() { - cnt++; - }).then(function() { - expect(cnt).toEqual(0); - expect(gd._fullLayout.mapbox.accesstoken).toEqual(MAPBOX_ACCESS_TOKEN); - done(); + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should use access token in mapbox layout options if present", + function(done) { + var cnt = 0; + + Plotly.plot( + gd, + [{ type: "scattermapbox", lon: [10, 20, 30], lat: [10, 20, 30] }], + { mapbox: { accesstoken: MAPBOX_ACCESS_TOKEN } }, + { mapboxAccessToken: dummyToken } + ) + .catch(function() { + cnt++; + }) + .then(function() { + expect(cnt).toEqual(0); + expect(gd._fullLayout.mapbox.accesstoken).toEqual( + MAPBOX_ACCESS_TOKEN + ); + done(); }); - }, LONG_TIMEOUT_INTERVAL); - - it('should bypass access token in mapbox layout options when config points to an Atlas server', function(done) { - var cnt = 0; - var msg = [ - 'An API access token is required to use Mapbox GL.', - 'See https://www.mapbox.com/developers/api/#access-tokens' - ].join(' '); - - Plotly.plot(gd, [{ - type: 'scattermapbox', - lon: [10, 20, 30], - lat: [10, 20, 30] - }], { - mapbox: { - accesstoken: MAPBOX_ACCESS_TOKEN - } - }, { - mapboxAccessToken: '' - }) + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should bypass access token in mapbox layout options when config points to an Atlas server", + function(done) { + var cnt = 0; + var msg = [ + "An API access token is required to use Mapbox GL.", + "See https://www.mapbox.com/developers/api/#access-tokens" + ].join(" "); + + Plotly.plot( + gd, + [{ type: "scattermapbox", lon: [10, 20, 30], lat: [10, 20, 30] }], + { mapbox: { accesstoken: MAPBOX_ACCESS_TOKEN } }, + { mapboxAccessToken: "" } + ) .catch(function(err) { - cnt++; - expect(err).toEqual(new Error(msg)); + cnt++; + expect(err).toEqual(new Error(msg)); }) .then(function() { - expect(cnt).toEqual(1); - done(); + expect(cnt).toEqual(1); + done(); }); - }, LONG_TIMEOUT_INTERVAL); + }, + LONG_TIMEOUT_INTERVAL + ); }); -describe('mapbox plots', function() { - 'use strict'; +describe("mapbox plots", function() { + "use strict"; + if (!hasWebGLSupport("mapbox plots")) return; - if(!hasWebGLSupport('mapbox plots')) return; + var mock = require("@mocks/mapbox_0.json"), gd; - var mock = require('@mocks/mapbox_0.json'), - gd; + var pointPos = [579, 276], blankPos = [650, 120]; - var pointPos = [579, 276], - blankPos = [650, 120]; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); + beforeEach(function(done) { + gd = createGraphDiv(); - beforeEach(function(done) { - gd = createGraphDiv(); + var mockCopy = Lib.extendDeep({}, mock); - var mockCopy = Lib.extendDeep({}, mock); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - it('should be able to toggle trace visibility', function(done) { - var modes = ['line', 'circle']; + it( + "should be able to toggle trace visibility", + function(done) { + var modes = ["line", "circle"]; - expect(countVisibleTraces(gd, modes)).toEqual(2); + expect(countVisibleTraces(gd, modes)).toEqual(2); - Plotly.restyle(gd, 'visible', false).then(function() { - expect(gd._fullLayout.mapbox).toBeUndefined(); + Plotly.restyle(gd, "visible", false) + .then(function() { + expect(gd._fullLayout.mapbox).toBeUndefined(); - return Plotly.restyle(gd, 'visible', true); + return Plotly.restyle(gd, "visible", true); }) .then(function() { - expect(countVisibleTraces(gd, modes)).toEqual(2); + expect(countVisibleTraces(gd, modes)).toEqual(2); - return Plotly.restyle(gd, 'visible', 'legendonly', [1]); + return Plotly.restyle(gd, "visible", "legendonly", [1]); }) .then(function() { - expect(countVisibleTraces(gd, modes)).toEqual(1); + expect(countVisibleTraces(gd, modes)).toEqual(1); - return Plotly.restyle(gd, 'visible', true); + return Plotly.restyle(gd, "visible", true); }) .then(function() { - expect(countVisibleTraces(gd, modes)).toEqual(2); + expect(countVisibleTraces(gd, modes)).toEqual(2); - var mockCopy = Lib.extendDeep({}, mock); - mockCopy.data[0].visible = false; + var mockCopy = Lib.extendDeep({}, mock); + mockCopy.data[0].visible = false; - return Plotly.newPlot(gd, mockCopy.data, mockCopy.layout); + return Plotly.newPlot(gd, mockCopy.data, mockCopy.layout); }) .then(function() { - expect(countVisibleTraces(gd, modes)).toEqual(1); + expect(countVisibleTraces(gd, modes)).toEqual(1); - done(); + done(); }); - }, LONG_TIMEOUT_INTERVAL); + }, + LONG_TIMEOUT_INTERVAL + ); - it('should be able to delete and add traces', function(done) { - var modes = ['line', 'circle']; + it( + "should be able to delete and add traces", + function(done) { + var modes = ["line", "circle"]; - expect(countVisibleTraces(gd, modes)).toEqual(2); + expect(countVisibleTraces(gd, modes)).toEqual(2); - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countVisibleTraces(gd, modes)).toEqual(1); + Plotly.deleteTraces(gd, [0]) + .then(function() { + expect(countVisibleTraces(gd, modes)).toEqual(1); - var trace = { - type: 'scattermapbox', - mode: 'markers+lines', - lon: [-10, -20, -10], - lat: [-10, 20, -10] - }; + var trace = { + type: "scattermapbox", + mode: "markers+lines", + lon: [-10, -20, -10], + lat: [-10, 20, -10] + }; - return Plotly.addTraces(gd, [trace]); + return Plotly.addTraces(gd, [trace]); }) .then(function() { - expect(countVisibleTraces(gd, modes)).toEqual(2); + expect(countVisibleTraces(gd, modes)).toEqual(2); - var trace = { - type: 'scattermapbox', - mode: 'markers+lines', - lon: [10, 20, 10], - lat: [10, -20, 10] - }; + var trace = { + type: "scattermapbox", + mode: "markers+lines", + lon: [10, 20, 10], + lat: [10, -20, 10] + }; - return Plotly.addTraces(gd, [trace]); + return Plotly.addTraces(gd, [trace]); }) .then(function() { - expect(countVisibleTraces(gd, modes)).toEqual(3); + expect(countVisibleTraces(gd, modes)).toEqual(3); - return Plotly.deleteTraces(gd, [0, 1, 2]); + return Plotly.deleteTraces(gd, [0, 1, 2]); }) .then(function() { - expect(gd._fullLayout.mapbox).toBeUndefined(); + expect(gd._fullLayout.mapbox).toBeUndefined(); - done(); + done(); }); - }, LONG_TIMEOUT_INTERVAL); + }, + LONG_TIMEOUT_INTERVAL + ); - it('should be able to restyle', function(done) { - var restyleCnt = 0, - relayoutCnt = 0; + it( + "should be able to restyle", + function(done) { + var restyleCnt = 0, relayoutCnt = 0; - gd.on('plotly_restyle', function() { - restyleCnt++; - }); - - gd.on('plotly_relayout', function() { - relayoutCnt++; - }); + gd.on("plotly_restyle", function() { + restyleCnt++; + }); - function assertMarkerColor(expectations) { - return new Promise(function(resolve) { - setTimeout(function() { - var colors = getStyle(gd, 'circle', 'circle-color'); + gd.on("plotly_relayout", function() { + relayoutCnt++; + }); - expectations.forEach(function(expected, i) { - expect(colors[i]).toBeCloseToArray(expected); - }); - - resolve(); - }, TRANSITION_DELAY); - }); - } + function assertMarkerColor(expectations) { + return new Promise(function(resolve) { + setTimeout( + function() { + var colors = getStyle(gd, "circle", "circle-color"); + + expectations.forEach(function(expected, i) { + expect(colors[i]).toBeCloseToArray(expected); + }); + + resolve(); + }, + TRANSITION_DELAY + ); + }); + } - assertMarkerColor([ - [0.121, 0.466, 0.705, 1], - [1, 0.498, 0.0549, 1] - ]) + assertMarkerColor([[0.121, 0.466, 0.705, 1], [1, 0.498, 0.0549, 1]]) .then(function() { - return Plotly.restyle(gd, 'marker.color', 'green'); + return Plotly.restyle(gd, "marker.color", "green"); }) .then(function() { - expect(restyleCnt).toEqual(1); - expect(relayoutCnt).toEqual(0); + expect(restyleCnt).toEqual(1); + expect(relayoutCnt).toEqual(0); - return assertMarkerColor([ - [0, 0.5019, 0, 1], - [0, 0.5019, 0, 1] - ]); + return assertMarkerColor([[0, 0.5019, 0, 1], [0, 0.5019, 0, 1]]); }) .then(function() { - return Plotly.restyle(gd, 'marker.color', 'red', [1]); + return Plotly.restyle(gd, "marker.color", "red", [1]); }) .then(function() { - expect(restyleCnt).toEqual(2); - expect(relayoutCnt).toEqual(0); + expect(restyleCnt).toEqual(2); + expect(relayoutCnt).toEqual(0); - return assertMarkerColor([ - [0, 0.5019, 0, 1], - [1, 0, 0, 1] - ]); + return assertMarkerColor([[0, 0.5019, 0, 1], [1, 0, 0, 1]]); }) .then(done); - }, LONG_TIMEOUT_INTERVAL); - - it('should be able to relayout', function(done) { - var restyleCnt = 0, - relayoutCnt = 0; - - gd.on('plotly_restyle', function() { - restyleCnt++; + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should be able to relayout", + function(done) { + var restyleCnt = 0, relayoutCnt = 0; + + gd.on("plotly_restyle", function() { + restyleCnt++; + }); + + gd.on("plotly_relayout", function() { + relayoutCnt++; + }); + + function assertLayout(style, center, zoom, dims) { + var mapInfo = getMapInfo(gd); + + expect(mapInfo.style.name).toEqual(style); + expect([mapInfo.center.lng, mapInfo.center.lat]).toBeCloseToArray( + center + ); + expect(mapInfo.zoom).toBeCloseTo(zoom); + + var divStyle = mapInfo.div.style; + ["left", "top", "width", "height"].forEach(function(p, i) { + expect(parseFloat(divStyle[p])).toBeWithin(dims[i], 8); }); + } - gd.on('plotly_relayout', function() { - relayoutCnt++; - }); - - function assertLayout(style, center, zoom, dims) { - var mapInfo = getMapInfo(gd); - - expect(mapInfo.style.name).toEqual(style); - expect([mapInfo.center.lng, mapInfo.center.lat]) - .toBeCloseToArray(center); - expect(mapInfo.zoom).toBeCloseTo(zoom); - - var divStyle = mapInfo.div.style; - ['left', 'top', 'width', 'height'].forEach(function(p, i) { - expect(parseFloat(divStyle[p])).toBeWithin(dims[i], 8); - }); - } + assertLayout("Mapbox Dark", [-4.710, 19.475], 1.234, [80, 100, 908, 270]); - assertLayout('Mapbox Dark', [-4.710, 19.475], 1.234, [80, 100, 908, 270]); - - Plotly.relayout(gd, 'mapbox.center', { lon: 0, lat: 0 }).then(function() { - expect(restyleCnt).toEqual(0); - expect(relayoutCnt).toEqual(1); + Plotly.relayout(gd, "mapbox.center", { lon: 0, lat: 0 }) + .then(function() { + expect(restyleCnt).toEqual(0); + expect(relayoutCnt).toEqual(1); - assertLayout('Mapbox Dark', [0, 0], 1.234, [80, 100, 908, 270]); + assertLayout("Mapbox Dark", [0, 0], 1.234, [80, 100, 908, 270]); - return Plotly.relayout(gd, 'mapbox.zoom', '6'); + return Plotly.relayout(gd, "mapbox.zoom", "6"); }) .then(function() { - expect(restyleCnt).toEqual(0); - expect(relayoutCnt).toEqual(2); + expect(restyleCnt).toEqual(0); + expect(relayoutCnt).toEqual(2); - assertLayout('Mapbox Dark', [0, 0], 6, [80, 100, 908, 270]); + assertLayout("Mapbox Dark", [0, 0], 6, [80, 100, 908, 270]); - return Plotly.relayout(gd, 'mapbox.style', 'light'); + return Plotly.relayout(gd, "mapbox.style", "light"); }) .then(function() { - expect(restyleCnt).toEqual(0); - expect(relayoutCnt).toEqual(3); + expect(restyleCnt).toEqual(0); + expect(relayoutCnt).toEqual(3); - assertLayout('Mapbox Light', [0, 0], 6, [80, 100, 908, 270]); + assertLayout("Mapbox Light", [0, 0], 6, [80, 100, 908, 270]); - return Plotly.relayout(gd, 'mapbox.domain.x', [0, 0.5]); + return Plotly.relayout(gd, "mapbox.domain.x", [0, 0.5]); }) .then(function() { - expect(restyleCnt).toEqual(0); - expect(relayoutCnt).toEqual(4); + expect(restyleCnt).toEqual(0); + expect(relayoutCnt).toEqual(4); - assertLayout('Mapbox Light', [0, 0], 6, [80, 100, 454, 270]); + assertLayout("Mapbox Light", [0, 0], 6, [80, 100, 454, 270]); - return Plotly.relayout(gd, 'mapbox.domain.y[0]', 0.5); + return Plotly.relayout(gd, "mapbox.domain.y[0]", 0.5); }) .then(function() { - expect(restyleCnt).toEqual(0); - expect(relayoutCnt).toEqual(5); + expect(restyleCnt).toEqual(0); + expect(relayoutCnt).toEqual(5); - assertLayout('Mapbox Light', [0, 0], 6, [80, 100, 454, 135]); + assertLayout("Mapbox Light", [0, 0], 6, [80, 100, 454, 135]); - done(); + done(); }); - }, LONG_TIMEOUT_INTERVAL); + }, + LONG_TIMEOUT_INTERVAL + ); - it('should be able to add, update and remove layers', function(done) { - var mockWithLayers = require('@mocks/mapbox_layers'); + it( + "should be able to add, update and remove layers", + function(done) { + var mockWithLayers = require("@mocks/mapbox_layers"); - var layer0 = Lib.extendDeep({}, mockWithLayers.layout.mapbox.layers[0]), - layer1 = Lib.extendDeep({}, mockWithLayers.layout.mapbox.layers[1]); + var layer0 = Lib.extendDeep({}, mockWithLayers.layout.mapbox.layers[0]), + layer1 = Lib.extendDeep({}, mockWithLayers.layout.mapbox.layers[1]); - var mapUpdate = { - 'mapbox.zoom': mockWithLayers.layout.mapbox.zoom, - 'mapbox.center.lon': mockWithLayers.layout.mapbox.center.lon, - 'mapbox.center.lat': mockWithLayers.layout.mapbox.center.lat - }; + var mapUpdate = { + "mapbox.zoom": mockWithLayers.layout.mapbox.zoom, + "mapbox.center.lon": mockWithLayers.layout.mapbox.center.lon, + "mapbox.center.lat": mockWithLayers.layout.mapbox.center.lat + }; - var styleUpdate0 = { - 'mapbox.layers[0].color': 'red', - 'mapbox.layers[0].fill.outlinecolor': 'blue', - 'mapbox.layers[0].opacity': 0.3 - }; + var styleUpdate0 = { + "mapbox.layers[0].color": "red", + "mapbox.layers[0].fill.outlinecolor": "blue", + "mapbox.layers[0].opacity": 0.3 + }; - var styleUpdate1 = { - 'mapbox.layers[1].color': 'blue', - 'mapbox.layers[1].line.width': 3, - 'mapbox.layers[1].opacity': 0.6 - }; + var styleUpdate1 = { + "mapbox.layers[1].color": "blue", + "mapbox.layers[1].line.width": 3, + "mapbox.layers[1].opacity": 0.6 + }; - function countVisibleLayers(gd) { - var mapInfo = getMapInfo(gd); + function countVisibleLayers(gd) { + var mapInfo = getMapInfo(gd); - var sourceLen = mapInfo.layoutSources.length, - layerLen = mapInfo.layoutLayers.length; + var sourceLen = mapInfo.layoutSources.length, + layerLen = mapInfo.layoutLayers.length; - if(sourceLen !== layerLen) return null; + if (sourceLen !== layerLen) return null; - return layerLen; - } + return layerLen; + } - function assertLayerStyle(gd, expectations, index) { - var mapInfo = getMapInfo(gd), - layers = mapInfo.layers, - layerNames = mapInfo.layoutLayers; - - var layer = layers[layerNames[index]]; - - return new Promise(function(resolve) { - setTimeout(function() { - Object.keys(expectations).forEach(function(k) { - expect(layer.paint[k]).toEqual(expectations[k]); - }); - resolve(); - }, TRANSITION_DELAY); - }); - } + function assertLayerStyle(gd, expectations, index) { + var mapInfo = getMapInfo(gd), + layers = mapInfo.layers, + layerNames = mapInfo.layoutLayers; - expect(countVisibleLayers(gd)).toEqual(0); + var layer = layers[layerNames[index]]; - Plotly.relayout(gd, 'mapbox.layers[0]', layer0).then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(1); - expect(countVisibleLayers(gd)).toEqual(1); + return new Promise(function(resolve) { + setTimeout( + function() { + Object.keys(expectations).forEach(function(k) { + expect(layer.paint[k]).toEqual(expectations[k]); + }); + resolve(); + }, + TRANSITION_DELAY + ); + }); + } - return Plotly.relayout(gd, 'mapbox.layers[1]', layer1); - }) + expect(countVisibleLayers(gd)).toEqual(0); + + Plotly.relayout(gd, "mapbox.layers[0]", layer0) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); - expect(countVisibleLayers(gd)).toEqual(2); + expect(gd.layout.mapbox.layers.length).toEqual(1); + expect(countVisibleLayers(gd)).toEqual(1); - return Plotly.relayout(gd, mapUpdate); + return Plotly.relayout(gd, "mapbox.layers[1]", layer1); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); - expect(countVisibleLayers(gd)).toEqual(2); + expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(countVisibleLayers(gd)).toEqual(2); - return Plotly.relayout(gd, styleUpdate0); + return Plotly.relayout(gd, mapUpdate); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); - expect(countVisibleLayers(gd)).toEqual(2); + expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(countVisibleLayers(gd)).toEqual(2); - return assertLayerStyle(gd, { - 'fill-color': [1, 0, 0, 1], - 'fill-outline-color': [0, 0, 1, 1], - 'fill-opacity': 0.3 - }, 0); + return Plotly.relayout(gd, styleUpdate0); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); - expect(countVisibleLayers(gd)).toEqual(2); + expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(countVisibleLayers(gd)).toEqual(2); - return Plotly.relayout(gd, styleUpdate1); + return assertLayerStyle( + gd, + { + "fill-color": [1, 0, 0, 1], + "fill-outline-color": [0, 0, 1, 1], + "fill-opacity": 0.3 + }, + 0 + ); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); - expect(countVisibleLayers(gd)).toEqual(2); + expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(countVisibleLayers(gd)).toEqual(2); - return assertLayerStyle(gd, { - 'line-width': 3, - 'line-color': [0, 0, 1, 1], - 'line-opacity': 0.6 - }, 1); + return Plotly.relayout(gd, styleUpdate1); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); - expect(countVisibleLayers(gd)).toEqual(2); + expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(countVisibleLayers(gd)).toEqual(2); - return Plotly.relayout(gd, 'mapbox.layers[1]', null); + return assertLayerStyle( + gd, + { + "line-width": 3, + "line-color": [0, 0, 1, 1], + "line-opacity": 0.6 + }, + 1 + ); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(1); - expect(countVisibleLayers(gd)).toEqual(1); + expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(countVisibleLayers(gd)).toEqual(2); - return Plotly.relayout(gd, 'mapbox.layers[0]', null); + return Plotly.relayout(gd, "mapbox.layers[1]", null); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(0); - expect(countVisibleLayers(gd)).toEqual(0); + expect(gd.layout.mapbox.layers.length).toEqual(1); + expect(countVisibleLayers(gd)).toEqual(1); - return Plotly.relayout(gd, 'mapbox.layers[0]', {}); + return Plotly.relayout(gd, "mapbox.layers[0]", null); }) .then(function() { - expect(gd.layout.mapbox.layers).toEqual([]); - expect(countVisibleLayers(gd)).toEqual(0); + expect(gd.layout.mapbox.layers.length).toEqual(0); + expect(countVisibleLayers(gd)).toEqual(0); - // layer with no source are not drawn + return Plotly.relayout(gd, "mapbox.layers[0]", {}); + }) + .then(function() { + expect(gd.layout.mapbox.layers).toEqual([]); + expect(countVisibleLayers(gd)).toEqual(0); - return Plotly.relayout(gd, 'mapbox.layers[0].source', layer0.source); + // layer with no source are not drawn + return Plotly.relayout(gd, "mapbox.layers[0].source", layer0.source); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(1); - expect(countVisibleLayers(gd)).toEqual(1); + expect(gd.layout.mapbox.layers.length).toEqual(1); + expect(countVisibleLayers(gd)).toEqual(1); - done(); - }); - }, LONG_TIMEOUT_INTERVAL); - - it('should be able to update the access token', function(done) { - Plotly.relayout(gd, 'mapbox.accesstoken', 'wont-work').catch(function(err) { - expect(gd._fullLayout.mapbox.accesstoken).toEqual('wont-work'); - expect(err).toEqual(new Error(constants.mapOnErrorMsg)); - expect(gd._promises.length).toEqual(1); - - return Plotly.relayout(gd, 'mapbox.accesstoken', MAPBOX_ACCESS_TOKEN); - }).then(function() { - expect(gd._fullLayout.mapbox.accesstoken).toEqual(MAPBOX_ACCESS_TOKEN); - expect(gd._promises.length).toEqual(0); - done(); + done(); }); - }, LONG_TIMEOUT_INTERVAL); - - it('should be able to update traces', function(done) { - function assertDataPts(lengths) { - var lines = getGeoJsonData(gd, 'lines'), - markers = getGeoJsonData(gd, 'markers'); - - lines.forEach(function(obj, i) { - expect(obj.coordinates[0].length).toEqual(lengths[i]); - }); - - markers.forEach(function(obj, i) { - expect(obj.features.length).toEqual(lengths[i]); - }); - } - - assertDataPts([3, 3]); - - var update = { - lon: [[10, 20]], - lat: [[-45, -20]] - }; - - Plotly.restyle(gd, update, [1]).then(function() { - assertDataPts([3, 2]); - - var update = { - lon: [ [10, 20], [30, 40, 20] ], - lat: [ [-10, 20], [10, 20, 30] ] - }; + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should be able to update the access token", + function(done) { + Plotly.relayout(gd, "mapbox.accesstoken", "wont-work") + .catch(function(err) { + expect(gd._fullLayout.mapbox.accesstoken).toEqual("wont-work"); + expect(err).toEqual(new Error(constants.mapOnErrorMsg)); + expect(gd._promises.length).toEqual(1); - return Plotly.extendTraces(gd, update, [0, 1]); + return Plotly.relayout(gd, "mapbox.accesstoken", MAPBOX_ACCESS_TOKEN); }) .then(function() { - assertDataPts([5, 5]); - - done(); + expect(gd._fullLayout.mapbox.accesstoken).toEqual( + MAPBOX_ACCESS_TOKEN + ); + expect(gd._promises.length).toEqual(0); + done(); + }); + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should be able to update traces", + function(done) { + function assertDataPts(lengths) { + var lines = getGeoJsonData(gd, "lines"), + markers = getGeoJsonData(gd, "markers"); + + lines.forEach(function(obj, i) { + expect(obj.coordinates[0].length).toEqual(lengths[i]); }); - }, LONG_TIMEOUT_INTERVAL); - - it('should display to hover labels on mouse over', function(done) { - function assertMouseMove(pos, len) { - return _mouseEvent('mousemove', pos, function() { - var hoverLabels = d3.select('.hoverlayer').selectAll('g'); - - expect(hoverLabels.size()).toEqual(len); - }); - } - assertMouseMove(blankPos, 0).then(function() { - return assertMouseMove(pointPos, 1); - }).then(done); - }, LONG_TIMEOUT_INTERVAL); + markers.forEach(function(obj, i) { + expect(obj.features.length).toEqual(lengths[i]); + }); + } - it('should respond to hover interactions by', function(done) { - var hoverCnt = 0, - unhoverCnt = 0; + assertDataPts([3, 3]); - var hoverData, unhoverData; + var update = { lon: [[10, 20]], lat: [[-45, -20]] }; - gd.on('plotly_hover', function(eventData) { - hoverCnt++; - hoverData = eventData.points[0]; - }); + Plotly.restyle(gd, update, [1]) + .then(function() { + assertDataPts([3, 2]); - gd.on('plotly_unhover', function(eventData) { - unhoverCnt++; - unhoverData = eventData.points[0]; - }); + var update = { + lon: [[10, 20], [30, 40, 20]], + lat: [[-10, 20], [10, 20, 30]] + }; - _mouseEvent('mousemove', blankPos, function() { - expect(hoverData).toBe(undefined, 'not firing on blank points'); - expect(unhoverData).toBe(undefined, 'not firing on blank points'); + return Plotly.extendTraces(gd, update, [0, 1]); }) .then(function() { - return _mouseEvent('mousemove', pointPos, function() { - expect(hoverData).not.toBe(undefined, 'firing on data points'); - expect(Object.keys(hoverData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat' - ], 'returning the correct event data keys'); - expect(hoverData.curveNumber).toEqual(0, 'returning the correct curve number'); - expect(hoverData.pointNumber).toEqual(0, 'returning the correct point number'); - }); - }) - .then(function() { - return _mouseEvent('mousemove', blankPos, function() { - expect(unhoverData).not.toBe(undefined, 'firing on data points'); - expect(Object.keys(unhoverData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat' - ], 'returning the correct event data keys'); - expect(unhoverData.curveNumber).toEqual(0, 'returning the correct curve number'); - expect(unhoverData.pointNumber).toEqual(0, 'returning the correct point number'); - }); - }) - .then(function() { - expect(hoverCnt).toEqual(1); - expect(unhoverCnt).toEqual(1); + assertDataPts([5, 5]); - done(); + done(); }); - }, LONG_TIMEOUT_INTERVAL); - - it('should respond drag / scroll interactions', function(done) { - var relayoutCnt = 0, - updateData; - - gd.on('plotly_relayout', function(eventData) { - relayoutCnt++; - updateData = eventData; + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should display to hover labels on mouse over", + function(done) { + function assertMouseMove(pos, len) { + return _mouseEvent("mousemove", pos, function() { + var hoverLabels = d3.select(".hoverlayer").selectAll("g"); + + expect(hoverLabels.size()).toEqual(len); }); + } - function _drag(p0, p1, cb) { - var promise = _mouseEvent('mousemove', p0, noop).then(function() { - return _mouseEvent('mousedown', p0, noop); - }).then(function() { - return _mouseEvent('mousemove', p1, noop); - }).then(function() { - // repeat mousemove to simulate long dragging motion - return _mouseEvent('mousemove', p1, noop); - }).then(function() { - return _mouseEvent('mouseup', p1, noop); - }).then(function() { - return _mouseEvent('mouseup', p1, noop); - }).then(cb); - - return promise; - } - - function assertLayout(center, zoom, opts) { - var mapInfo = getMapInfo(gd), - layout = gd.layout.mapbox; - - expect([mapInfo.center.lng, mapInfo.center.lat]).toBeCloseToArray(center); - expect(mapInfo.zoom).toBeCloseTo(zoom); - - expect([layout.center.lon, layout.center.lat]).toBeCloseToArray(center); - expect(layout.zoom).toBeCloseTo(zoom); - - if(opts && opts.withUpdateData) { - var mapboxUpdate = updateData.mapbox; - - expect([mapboxUpdate.center.lon, mapboxUpdate.center.lat]).toBeCloseToArray(center); - expect(mapboxUpdate.zoom).toBeCloseTo(zoom); - } - } - - assertLayout([-4.710, 19.475], 1.234); - - var p1 = [pointPos[0] + 50, pointPos[1] - 20]; - - _drag(pointPos, p1, function() { - expect(relayoutCnt).toEqual(1); - assertLayout([-19.651, 13.751], 1.234, { withUpdateData: true }); - + assertMouseMove(blankPos, 0) + .then(function() { + return assertMouseMove(pointPos, 1); }) .then(done); - - // TODO test scroll - - }, LONG_TIMEOUT_INTERVAL); - - it('should respond to click interactions by', function(done) { - var ptData; - - gd.on('plotly_click', function(eventData) { - ptData = eventData.points[0]; + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should respond to hover interactions by", + function(done) { + var hoverCnt = 0, unhoverCnt = 0; + + var hoverData, unhoverData; + + gd.on("plotly_hover", function(eventData) { + hoverCnt++; + hoverData = eventData.points[0]; + }); + + gd.on("plotly_unhover", function(eventData) { + unhoverCnt++; + unhoverData = eventData.points[0]; + }); + + _mouseEvent("mousemove", blankPos, function() { + expect(hoverData).toBe(undefined, "not firing on blank points"); + expect(unhoverData).toBe(undefined, "not firing on blank points"); + }) + .then(function() { + return _mouseEvent("mousemove", pointPos, function() { + expect(hoverData).not.toBe(undefined, "firing on data points"); + expect(Object.keys(hoverData)).toEqual( + ["data", "fullData", "curveNumber", "pointNumber", "lon", "lat"], + "returning the correct event data keys" + ); + expect(hoverData.curveNumber).toEqual( + 0, + "returning the correct curve number" + ); + expect(hoverData.pointNumber).toEqual( + 0, + "returning the correct point number" + ); + }); + }) + .then(function() { + return _mouseEvent("mousemove", blankPos, function() { + expect(unhoverData).not.toBe(undefined, "firing on data points"); + expect(Object.keys(unhoverData)).toEqual( + ["data", "fullData", "curveNumber", "pointNumber", "lon", "lat"], + "returning the correct event data keys" + ); + expect(unhoverData.curveNumber).toEqual( + 0, + "returning the correct curve number" + ); + expect(unhoverData.pointNumber).toEqual( + 0, + "returning the correct point number" + ); + }); + }) + .then(function() { + expect(hoverCnt).toEqual(1); + expect(unhoverCnt).toEqual(1); + + done(); }); - - function _click(pos, cb) { - var promise = _mouseEvent('mousemove', pos, noop).then(function() { - return _mouseEvent('mousedown', pos, noop); - }).then(function() { - return _mouseEvent('click', pos, cb); - }); - - return promise; + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should respond drag / scroll interactions", + function(done) { + var relayoutCnt = 0, updateData; + + gd.on("plotly_relayout", function(eventData) { + relayoutCnt++; + updateData = eventData; + }); + + function _drag(p0, p1, cb) { + var promise = _mouseEvent("mousemove", p0, noop) + .then(function() { + return _mouseEvent("mousedown", p0, noop); + }) + .then(function() { + return _mouseEvent("mousemove", p1, noop); + }) + .then(function() { + // repeat mousemove to simulate long dragging motion + return _mouseEvent("mousemove", p1, noop); + }) + .then(function() { + return _mouseEvent("mouseup", p1, noop); + }) + .then(function() { + return _mouseEvent("mouseup", p1, noop); + }) + .then(cb); + + return promise; + } + + function assertLayout(center, zoom, opts) { + var mapInfo = getMapInfo(gd), layout = gd.layout.mapbox; + + expect([mapInfo.center.lng, mapInfo.center.lat]).toBeCloseToArray( + center + ); + expect(mapInfo.zoom).toBeCloseTo(zoom); + + expect([layout.center.lon, layout.center.lat]).toBeCloseToArray(center); + expect(layout.zoom).toBeCloseTo(zoom); + + if (opts && opts.withUpdateData) { + var mapboxUpdate = updateData.mapbox; + + expect([ + mapboxUpdate.center.lon, + mapboxUpdate.center.lat + ]).toBeCloseToArray(center); + expect(mapboxUpdate.zoom).toBeCloseTo(zoom); } - - _click(blankPos, function() { - expect(ptData).toBe(undefined, 'not firing on blank points'); - }) - .then(function() { - return _click(pointPos, function() { - expect(ptData).not.toBe(undefined, 'firing on data points'); - expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat' - ], 'returning the correct event data keys'); - expect(ptData.curveNumber).toEqual(0, 'returning the correct curve number'); - expect(ptData.pointNumber).toEqual(0, 'returning the correct point number'); - }); + } + + assertLayout([-4.710, 19.475], 1.234); + + var p1 = [pointPos[0] + 50, pointPos[1] - 20]; + + _drag(pointPos, p1, function() { + expect(relayoutCnt).toEqual(1); + assertLayout([-19.651, 13.751], 1.234, { withUpdateData: true }); + }).then(done); + // TODO test scroll + }, + LONG_TIMEOUT_INTERVAL + ); + + it( + "should respond to click interactions by", + function(done) { + var ptData; + + gd.on("plotly_click", function(eventData) { + ptData = eventData.points[0]; + }); + + function _click(pos, cb) { + var promise = _mouseEvent("mousemove", pos, noop) + .then(function() { + return _mouseEvent("mousedown", pos, noop); + }) + .then(function() { + return _mouseEvent("click", pos, cb); + }); + + return promise; + } + + _click(blankPos, function() { + expect(ptData).toBe(undefined, "not firing on blank points"); + }) + .then(function() { + return _click(pointPos, function() { + expect(ptData).not.toBe(undefined, "firing on data points"); + expect(Object.keys(ptData)).toEqual( + ["data", "fullData", "curveNumber", "pointNumber", "lon", "lat"], + "returning the correct event data keys" + ); + expect(ptData.curveNumber).toEqual( + 0, + "returning the correct curve number" + ); + expect(ptData.pointNumber).toEqual( + 0, + "returning the correct point number" + ); + }); }) .then(done); - }, LONG_TIMEOUT_INTERVAL); - - function getMapInfo(gd) { - var subplot = gd._fullLayout.mapbox._subplot, - map = subplot.map; - - var sources = map.style.sources, - layers = map.style._layers, - uid = subplot.uid; - - var traceSources = Object.keys(sources).filter(function(k) { - return k.indexOf('-source-') !== -1; - }); + }, + LONG_TIMEOUT_INTERVAL + ); - var traceLayers = Object.keys(layers).filter(function(k) { - return k.indexOf('-layer-') !== -1; - }); + function getMapInfo(gd) { + var subplot = gd._fullLayout.mapbox._subplot, map = subplot.map; - var layoutSources = Object.keys(sources).filter(function(k) { - return k.indexOf(uid) !== -1; - }); - - var layoutLayers = Object.keys(layers).filter(function(k) { - return k.indexOf(uid) !== -1; - }); + var sources = map.style.sources, + layers = map.style._layers, + uid = subplot.uid; - return { - map: map, - div: subplot.div, - sources: sources, - layers: layers, - traceSources: traceSources, - traceLayers: traceLayers, - layoutSources: layoutSources, - layoutLayers: layoutLayers, - center: map.getCenter(), - zoom: map.getZoom(), - style: map.getStyle() - }; - } - - function countVisibleTraces(gd, modes) { - var mapInfo = getMapInfo(gd), - cnts = []; - - // 'modes' are the ScatterMapbox layers names - // e.g. 'fill', 'line', 'circle', 'symbol' - - modes.forEach(function(mode) { - var cntPerMode = 0; - - mapInfo.traceLayers.forEach(function(l) { - var info = mapInfo.layers[l]; + var traceSources = Object.keys(sources).filter(function(k) { + return k.indexOf("-source-") !== -1; + }); - if(l.indexOf(mode) === -1) return; - if(info.layout.visibility === 'visible') cntPerMode++; - }); + var traceLayers = Object.keys(layers).filter(function(k) { + return k.indexOf("-layer-") !== -1; + }); - cnts.push(cntPerMode); - }); + var layoutSources = Object.keys(sources).filter(function(k) { + return k.indexOf(uid) !== -1; + }); - var cnt = cnts.reduce(function(a, b) { - return (a === b) ? a : null; - }); + var layoutLayers = Object.keys(layers).filter(function(k) { + return k.indexOf(uid) !== -1; + }); - // returns null if not all counter per mode are the same, - // returns the counter if all are the same. + return { + map: map, + div: subplot.div, + sources: sources, + layers: layers, + traceSources: traceSources, + traceLayers: traceLayers, + layoutSources: layoutSources, + layoutLayers: layoutLayers, + center: map.getCenter(), + zoom: map.getZoom(), + style: map.getStyle() + }; + } + + function countVisibleTraces(gd, modes) { + var mapInfo = getMapInfo(gd), cnts = []; + + // 'modes' are the ScatterMapbox layers names + // e.g. 'fill', 'line', 'circle', 'symbol' + modes.forEach(function(mode) { + var cntPerMode = 0; + + mapInfo.traceLayers.forEach(function(l) { + var info = mapInfo.layers[l]; + + if (l.indexOf(mode) === -1) return; + if (info.layout.visibility === "visible") cntPerMode++; + }); + + cnts.push(cntPerMode); + }); - return cnt; - } + var cnt = cnts.reduce(function(a, b) { + return a === b ? a : null; + }); - function getStyle(gd, mode, prop) { - var mapInfo = getMapInfo(gd), - values = []; + // returns null if not all counter per mode are the same, + // returns the counter if all are the same. + return cnt; + } - mapInfo.traceLayers.forEach(function(l) { - var info = mapInfo.layers[l]; + function getStyle(gd, mode, prop) { + var mapInfo = getMapInfo(gd), values = []; - if(l.indexOf(mode) === -1) return; + mapInfo.traceLayers.forEach(function(l) { + var info = mapInfo.layers[l]; - values.push(info.paint[prop]); - }); + if (l.indexOf(mode) === -1) return; - return values; - } + values.push(info.paint[prop]); + }); - function getGeoJsonData(gd, mode) { - var mapInfo = getMapInfo(gd), - out = []; + return values; + } - mapInfo.traceSources.forEach(function(s) { - var info = mapInfo.sources[s]; + function getGeoJsonData(gd, mode) { + var mapInfo = getMapInfo(gd), out = []; - if(s.indexOf(mode) === -1) return; + mapInfo.traceSources.forEach(function(s) { + var info = mapInfo.sources[s]; - out.push(info._data); - }); + if (s.indexOf(mode) === -1) return; - return out; - } + out.push(info._data); + }); - function _mouseEvent(type, pos, cb) { - return new Promise(function(resolve) { - mouseEvent(type, pos[0], pos[1]); + return out; + } - setTimeout(function() { - cb(); - resolve(); - }, MOUSE_DELAY); - }); - } + function _mouseEvent(type, pos, cb) { + return new Promise(function(resolve) { + mouseEvent(type, pos[0], pos[1]); + setTimeout( + function() { + cb(); + resolve(); + }, + MOUSE_DELAY + ); + }); + } }); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index b3be9a51b43..e3723e9c851 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -1,850 +1,897 @@ -var d3 = require('d3'); - -var createModeBar = require('@src/components/modebar/modebar'); -var manageModeBar = require('@src/components/modebar/manage'); -var customMatchers = require('../assets/custom_matchers'); - -var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var selectButton = require('../assets/modebar_button'); - - -describe('ModeBar', function() { - 'use strict'; - - function noop() {} - - function getMockContainerTree() { - var root = document.createElement('div'); - root.className = 'plot-container'; - var parent = document.createElement('div'); - parent.className = 'svg-container'; - root.appendChild(parent); +var d3 = require("d3"); + +var createModeBar = require("@src/components/modebar/modebar"); +var manageModeBar = require("@src/components/modebar/manage"); +var customMatchers = require("../assets/custom_matchers"); + +var Plotly = require("@lib/index"); +var Plots = require("@src/plots/plots"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var selectButton = require("../assets/modebar_button"); + +describe("ModeBar", function() { + "use strict"; + function noop() {} + + function getMockContainerTree() { + var root = document.createElement("div"); + root.className = "plot-container"; + var parent = document.createElement("div"); + parent.className = "svg-container"; + root.appendChild(parent); + + return parent; + } + + function getMockGraphInfo() { + return { + _fullLayout: { + dragmode: "zoom", + _paperdiv: d3.select(getMockContainerTree()), + _has: Plots._hasPlotType + }, + _fullData: [], + _context: { + displaylogo: true, + displayModeBar: true, + modeBarButtonsToRemove: [], + modeBarButtonsToAdd: [] + } + }; + } + + function countGroups(modeBar) { + return d3.select(modeBar.element).selectAll("div.modebar-group")[0].length; + } + + function countButtons(modeBar) { + return d3.select(modeBar.element).selectAll("a.modebar-btn")[0].length; + } + + function countLogo(modeBar) { + return d3.select(modeBar.element).selectAll("a.plotlyjsicon")[0].length; + } + + function checkBtnAttr(modeBar, index, attr) { + var buttons = d3.select(modeBar.element).selectAll("a.modebar-btn"); + return d3.select(buttons[0][index]).attr(attr); + } + + var buttons = [ + [{ name: "button 1", click: noop }, { name: "button 2", click: noop }] + ]; + + var modeBar = createModeBar(getMockGraphInfo(), buttons); + + describe("createModebar", function() { + it("creates a mode bar", function() { + expect(countGroups(modeBar)).toEqual(2); + expect(countButtons(modeBar)).toEqual(3); + expect(countLogo(modeBar)).toEqual(1); + }); - return parent; - } + it("throws when button config does not have name", function() { + expect(function() { + createModeBar(getMockGraphInfo(), [ + [ + { + click: function() { + console.log("not gonna work"); + } + } + ] + ]); + }).toThrowError(); + }); - function getMockGraphInfo() { - return { - _fullLayout: { - dragmode: 'zoom', - _paperdiv: d3.select(getMockContainerTree()), - _has: Plots._hasPlotType + it("throws when button name is not unique", function() { + expect(function() { + createModeBar(getMockGraphInfo(), [ + [ + { + name: "A", + click: function() { + console.log("not gonna"); + } }, - _fullData: [], - _context: { - displaylogo: true, - displayModeBar: true, - modeBarButtonsToRemove: [], - modeBarButtonsToAdd: [] + { + name: "A", + click: function() { + console.log("... work"); + } } - }; - } - - function countGroups(modeBar) { - return d3.select(modeBar.element).selectAll('div.modebar-group')[0].length; - } - - function countButtons(modeBar) { - return d3.select(modeBar.element).selectAll('a.modebar-btn')[0].length; - } - - function countLogo(modeBar) { - return d3.select(modeBar.element).selectAll('a.plotlyjsicon')[0].length; - } - - function checkBtnAttr(modeBar, index, attr) { - var buttons = d3.select(modeBar.element).selectAll('a.modebar-btn'); - return d3.select(buttons[0][index]).attr(attr); - } - - var buttons = [[{ - name: 'button 1', - click: noop - }, { - name: 'button 2', - click: noop - }]]; - - var modeBar = createModeBar(getMockGraphInfo(), buttons); - - describe('createModebar', function() { - it('creates a mode bar', function() { - expect(countGroups(modeBar)).toEqual(2); - expect(countButtons(modeBar)).toEqual(3); - expect(countLogo(modeBar)).toEqual(1); - }); - - it('throws when button config does not have name', function() { - expect(function() { - createModeBar(getMockGraphInfo(), [[ - { click: function() { console.log('not gonna work'); } } - ]]); - }).toThrowError(); - }); - - it('throws when button name is not unique', function() { - expect(function() { - createModeBar(getMockGraphInfo(), [[ - { name: 'A', click: function() { console.log('not gonna'); } }, - { name: 'A', click: function() { console.log('... work'); } } - ]]); - }).toThrowError(); - }); + ] + ]); + }).toThrowError(); + }); - it('throws when button config does not have a click handler', function() { - expect(function() { - createModeBar(getMockGraphInfo(), [[ - { name: 'not gonna work' } - ]]); - }).toThrowError(); - }); + it("throws when button config does not have a click handler", function() { + expect(function() { + createModeBar(getMockGraphInfo(), [[{ name: "not gonna work" }]]); + }).toThrowError(); + }); - it('defaults title to name when missing', function() { - var modeBar = createModeBar(getMockGraphInfo(), [[ - { name: 'the title too', click: noop } - ]]); + it("defaults title to name when missing", function() { + var modeBar = createModeBar(getMockGraphInfo(), [ + [{ name: "the title too", click: noop }] + ]); - expect(checkBtnAttr(modeBar, 0, 'data-title')).toEqual('the title too'); - }); + expect(checkBtnAttr(modeBar, 0, "data-title")).toEqual("the title too"); + }); - it('hides title to when title is falsy but not 0', function() { - var modeBar; + it("hides title to when title is falsy but not 0", function() { + var modeBar; - modeBar = createModeBar(getMockGraphInfo(), [[ - { name: 'button', title: null, click: noop } - ]]); - expect(checkBtnAttr(modeBar, 0, 'data-title')).toBe(null); + modeBar = createModeBar(getMockGraphInfo(), [ + [{ name: "button", title: null, click: noop }] + ]); + expect(checkBtnAttr(modeBar, 0, "data-title")).toBe(null); - modeBar = createModeBar(getMockGraphInfo(), [[ - { name: 'button', title: '', click: noop } - ]]); - expect(checkBtnAttr(modeBar, 0, 'data-title')).toBe(null); + modeBar = createModeBar(getMockGraphInfo(), [ + [{ name: "button", title: "", click: noop }] + ]); + expect(checkBtnAttr(modeBar, 0, "data-title")).toBe(null); - modeBar = createModeBar(getMockGraphInfo(), [[ - { name: 'button', title: false, click: noop } - ]]); - expect(checkBtnAttr(modeBar, 0, 'data-title')).toBe(null); + modeBar = createModeBar(getMockGraphInfo(), [ + [{ name: "button", title: false, click: noop }] + ]); + expect(checkBtnAttr(modeBar, 0, "data-title")).toBe(null); - modeBar = createModeBar(getMockGraphInfo(), [[ - { name: 'button', title: 0, click: noop } - ]]); - expect(checkBtnAttr(modeBar, 0, 'data-title')).toEqual('0'); - }); + modeBar = createModeBar(getMockGraphInfo(), [ + [{ name: "button", title: 0, click: noop }] + ]); + expect(checkBtnAttr(modeBar, 0, "data-title")).toEqual("0"); }); + }); - describe('modeBar.removeAllButtons', function() { - it('removes all mode bar buttons', function() { - modeBar.removeAllButtons(); + describe("modeBar.removeAllButtons", function() { + it("removes all mode bar buttons", function() { + modeBar.removeAllButtons(); - expect(modeBar.element.innerHTML).toEqual(''); - expect(modeBar.hasLogo).toBe(false); - }); + expect(modeBar.element.innerHTML).toEqual(""); + expect(modeBar.hasLogo).toBe(false); }); + }); - describe('modeBar.destroy', function() { - it('removes the mode bar entirely', function() { - var modeBarParent = modeBar.element.parentNode; + describe("modeBar.destroy", function() { + it("removes the mode bar entirely", function() { + var modeBarParent = modeBar.element.parentNode; - modeBar.destroy(); + modeBar.destroy(); - expect(modeBarParent.querySelector('.modebar')).toBeNull(); - }); + expect(modeBarParent.querySelector(".modebar")).toBeNull(); }); - - describe('manageModeBar', function() { - - function getButtons(list) { - for(var i = 0; i < list.length; i++) { - for(var j = 0; j < list[i].length; j++) { - - // minimal button config object - list[i][j] = { name: list[i][j], click: noop }; - } - } - return list; + }); + + describe("manageModeBar", function() { + function getButtons(list) { + for (var i = 0; i < list.length; i++) { + for (var j = 0; j < list[i].length; j++) { + // minimal button config object + list[i][j] = { name: list[i][j], click: noop }; } + } + return list; + } - function checkButtons(modeBar, buttons, logos) { - var expectedGroupCount = buttons.length + logos; - var expectedButtonCount = logos; - buttons.forEach(function(group) { - expectedButtonCount += group.length; - }); - - expect(modeBar.hasButtons(buttons)).toBe(true); - expect(countGroups(modeBar)).toEqual(expectedGroupCount); - expect(countButtons(modeBar)).toEqual(expectedButtonCount); - expect(countLogo(modeBar)).toEqual(1); - } + function checkButtons(modeBar, buttons, logos) { + var expectedGroupCount = buttons.length + logos; + var expectedButtonCount = logos; + buttons.forEach(function(group) { + expectedButtonCount += group.length; + }); + + expect(modeBar.hasButtons(buttons)).toBe(true); + expect(countGroups(modeBar)).toEqual(expectedGroupCount); + expect(countButtons(modeBar)).toEqual(expectedButtonCount); + expect(countLogo(modeBar)).toEqual(1); + } - it('creates mode bar (unselectable cartesian version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['zoom2d', 'pan2d'], - ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], - ['hoverClosestCartesian', 'hoverCompareCartesian'] - ]); + it("creates mode bar (unselectable cartesian version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["zoom2d", "pan2d"], + ["zoomIn2d", "zoomOut2d", "autoScale2d", "resetScale2d"], + ["hoverClosestCartesian", "hoverCompareCartesian"] + ]); - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; - gd._fullLayout.xaxis = {fixedrange: false}; + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "cartesian" }]; + gd._fullLayout.xaxis = { fixedrange: false }; - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - checkButtons(modeBar, buttons, 1); - }); + checkButtons(modeBar, buttons, 1); + }); - it('creates mode bar (selectable cartesian version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['zoom2d', 'pan2d', 'select2d', 'lasso2d'], - ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], - ['hoverClosestCartesian', 'hoverCompareCartesian'] - ]); + it("creates mode bar (selectable cartesian version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["zoom2d", "pan2d", "select2d", "lasso2d"], + ["zoomIn2d", "zoomOut2d", "autoScale2d", "resetScale2d"], + ["hoverClosestCartesian", "hoverCompareCartesian"] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "cartesian" }]; + gd._fullLayout.xaxis = { fixedrange: false }; + gd._fullData = [ + { + type: "scatter", + visible: true, + mode: "markers", + _module: { selectPoints: true } + } + ]; - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; - gd._fullLayout.xaxis = {fixedrange: false}; - gd._fullData = [{ - type: 'scatter', - visible: true, - mode: 'markers', - _module: {selectPoints: true} - }]; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + checkButtons(modeBar, buttons, 1); + }); - checkButtons(modeBar, buttons, 1); - }); + it("creates mode bar (cartesian fixed-axes version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["hoverClosestCartesian", "hoverCompareCartesian"] + ]); - it('creates mode bar (cartesian fixed-axes version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['hoverClosestCartesian', 'hoverCompareCartesian'] - ]); + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "cartesian" }]; - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + checkButtons(modeBar, buttons, 1); + }); - checkButtons(modeBar, buttons, 1); - }); + it("creates mode bar (gl3d version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["zoom3d", "pan3d", "orbitRotation", "tableRotation"], + ["resetCameraDefault3d", "resetCameraLastSave3d"], + ["hoverClosest3d"] + ]); - it('creates mode bar (gl3d version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation'], - ['resetCameraDefault3d', 'resetCameraLastSave3d'], - ['hoverClosest3d'] - ]); + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "gl3d" }]; - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'gl3d' }]; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + checkButtons(modeBar, buttons, 1); + }); - checkButtons(modeBar, buttons, 1); - }); + it("creates mode bar (geo version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["zoomInGeo", "zoomOutGeo", "resetGeo"], + ["hoverClosestGeo"] + ]); - it('creates mode bar (geo version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['zoomInGeo', 'zoomOutGeo', 'resetGeo'], - ['hoverClosestGeo'] - ]); + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "geo" }]; - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'geo' }]; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + checkButtons(modeBar, buttons, 1); + }); - checkButtons(modeBar, buttons, 1); - }); + it("creates mode bar (gl2d version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["zoom2d", "pan2d"], + ["zoomIn2d", "zoomOut2d", "autoScale2d", "resetScale2d"], + ["hoverClosestGl2d"] + ]); - it('creates mode bar (gl2d version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['zoom2d', 'pan2d'], - ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], - ['hoverClosestGl2d'] - ]); + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "gl2d" }]; + gd._fullLayout.xaxis = { fixedrange: false }; - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'gl2d' }]; - gd._fullLayout.xaxis = {fixedrange: false}; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + checkButtons(modeBar, buttons, 1); + }); - checkButtons(modeBar, buttons, 1); - }); + it("creates mode bar (pie version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["hoverClosestPie"] + ]); - it('creates mode bar (pie version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['hoverClosestPie'] - ]); + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "pie" }]; - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'pie' }]; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + checkButtons(modeBar, buttons, 1); + }); - checkButtons(modeBar, buttons, 1); - }); + it("creates mode bar (cartesian + gl3d version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["resetViews", "toggleHover"] + ]); - it('creates mode bar (cartesian + gl3d version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['resetViews', 'toggleHover'] - ]); + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [ + { name: "cartesian" }, + { name: "gl3d" } + ]; - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'cartesian' }, { name: 'gl3d' }]; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + checkButtons(modeBar, buttons, 1); + }); - checkButtons(modeBar, buttons, 1); - }); + it("creates mode bar (cartesian + geo version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["resetViews", "toggleHover"] + ]); - it('creates mode bar (cartesian + geo version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['resetViews', 'toggleHover'] - ]); + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [ + { name: "cartesian" }, + { name: "geo" } + ]; - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'cartesian' }, { name: 'geo' }]; + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + checkButtons(modeBar, buttons, 1); + }); - checkButtons(modeBar, buttons, 1); - }); + it("creates mode bar (cartesian + pie version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["zoom2d", "pan2d", "select2d", "lasso2d"], + ["zoomIn2d", "zoomOut2d", "autoScale2d", "resetScale2d"], + ["toggleHover"] + ]); + + var gd = getMockGraphInfo(); + gd._fullData = [ + { + type: "scatter", + visible: true, + mode: "markers", + _module: { selectPoints: true } + } + ]; + gd._fullLayout.xaxis = { fixedrange: false }; + gd._fullLayout._basePlotModules = [ + { name: "cartesian" }, + { name: "pie" } + ]; - it('creates mode bar (cartesian + pie version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['zoom2d', 'pan2d', 'select2d', 'lasso2d'], - ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], - ['toggleHover'] - ]); + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - var gd = getMockGraphInfo(); - gd._fullData = [{ - type: 'scatter', - visible: true, - mode: 'markers', - _module: {selectPoints: true} - }]; - gd._fullLayout.xaxis = {fixedrange: false}; - gd._fullLayout._basePlotModules = [{ name: 'cartesian' }, { name: 'pie' }]; + checkButtons(modeBar, buttons, 1); + }); - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + it("creates mode bar (gl3d + geo version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["resetViews", "toggleHover"] + ]); - checkButtons(modeBar, buttons, 1); - }); + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "geo" }, { name: "gl3d" }]; - it('creates mode bar (gl3d + geo version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['resetViews', 'toggleHover'] - ]); + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'geo' }, { name: 'gl3d' }]; + checkButtons(modeBar, buttons, 1); + }); - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + it("creates mode bar (un-selectable ternary version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["zoom2d", "pan2d"] + ]); - checkButtons(modeBar, buttons, 1); - }); + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "ternary" }]; - it('creates mode bar (un-selectable ternary version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['zoom2d', 'pan2d'] - ]); + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'ternary' }]; + checkButtons(modeBar, buttons, 1); + }); - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + it("creates mode bar (selectable ternary version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["zoom2d", "pan2d", "select2d", "lasso2d"] + ]); + + var gd = getMockGraphInfo(); + gd._fullData = [ + { + type: "scatterternary", + visible: true, + mode: "markers", + _module: { selectPoints: true } + } + ]; + gd._fullLayout._basePlotModules = [{ name: "ternary" }]; - checkButtons(modeBar, buttons, 1); - }); + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - it('creates mode bar (selectable ternary version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['zoom2d', 'pan2d', 'select2d', 'lasso2d'] - ]); + checkButtons(modeBar, buttons, 1); + }); - var gd = getMockGraphInfo(); - gd._fullData = [{ - type: 'scatterternary', - visible: true, - mode: 'markers', - _module: {selectPoints: true} - }]; - gd._fullLayout._basePlotModules = [{ name: 'ternary' }]; + it("creates mode bar (ternary + cartesian version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["zoom2d", "pan2d"], + ["hoverClosestCartesian", "hoverCompareCartesian"] + ]); - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [ + { name: "ternary" }, + { name: "cartesian" } + ]; - checkButtons(modeBar, buttons, 1); - }); + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - it('creates mode bar (ternary + cartesian version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['zoom2d', 'pan2d'], - ['hoverClosestCartesian', 'hoverCompareCartesian'] - ]); + checkButtons(modeBar, buttons, 1); + }); - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'ternary' }, { name: 'cartesian' }]; + it("creates mode bar (ternary + gl3d version)", function() { + var buttons = getButtons([ + ["toImage", "sendDataToCloud"], + ["resetViews", "toggleHover"] + ]); - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "ternary" }, { name: "gl3d" }]; - checkButtons(modeBar, buttons, 1); - }); + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; - it('creates mode bar (ternary + gl3d version)', function() { - var buttons = getButtons([ - ['toImage', 'sendDataToCloud'], - ['resetViews', 'toggleHover'] - ]); + checkButtons(modeBar, buttons, 1); + }); - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'ternary' }, { name: 'gl3d' }]; + it("throws an error if modeBarButtonsToRemove isn't an array", function() { + var gd = getMockGraphInfo(); + gd._context.modeBarButtonsToRemove = "not gonna work"; - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; + expect(function() { + manageModeBar(gd); + }).toThrowError(); + }); - checkButtons(modeBar, buttons, 1); - }); + it("throws an error if modeBarButtonsToAdd isn't an array", function() { + var gd = getMockGraphInfo(); + gd._context.modeBarButtonsToAdd = "not gonna work"; - it('throws an error if modeBarButtonsToRemove isn\'t an array', function() { - var gd = getMockGraphInfo(); - gd._context.modeBarButtonsToRemove = 'not gonna work'; + expect(function() { + manageModeBar(gd); + }).toThrowError(); + }); - expect(function() { manageModeBar(gd); }).toThrowError(); - }); + it( + "displays or not mode bar according to displayModeBar config arg", + function() { + var gd = getMockGraphInfo(); + gd._context.displayModeBar = false; + + manageModeBar(gd); + expect(gd._fullLayout._modeBar).not.toBeDefined(); + } + ); + + it("updates mode bar according to displayModeBar config arg", function() { + var gd = getMockGraphInfo(); + manageModeBar(gd); + expect(gd._fullLayout._modeBar).toBeDefined(); + + gd._context.displayModeBar = false; + manageModeBar(gd); + expect(gd._fullLayout._modeBar).not.toBeDefined(); + }); - it('throws an error if modeBarButtonsToAdd isn\'t an array', function() { - var gd = getMockGraphInfo(); - gd._context.modeBarButtonsToAdd = 'not gonna work'; + it("displays or not logo according to displaylogo config arg", function() { + var gd = getMockGraphInfo(); + manageModeBar(gd); + expect(countLogo(gd._fullLayout._modeBar)).toEqual(1); - expect(function() { manageModeBar(gd); }).toThrowError(); - }); + gd._context.displaylogo = false; + manageModeBar(gd); + expect(countLogo(gd._fullLayout._modeBar)).toEqual(0); + }); - it('displays or not mode bar according to displayModeBar config arg', function() { - var gd = getMockGraphInfo(); - gd._context.displayModeBar = false; + // gives 11 buttons in 5 groups by default + function setupGraphInfo() { + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: "cartesian" }]; + gd._fullLayout.xaxis = { fixedrange: false }; + return gd; + } - manageModeBar(gd); - expect(gd._fullLayout._modeBar).not.toBeDefined(); - }); + it("updates mode bar buttons if plot type changes", function() { + var gd = setupGraphInfo(); + manageModeBar(gd); - it('updates mode bar according to displayModeBar config arg', function() { - var gd = getMockGraphInfo(); - manageModeBar(gd); - expect(gd._fullLayout._modeBar).toBeDefined(); + gd._fullLayout._basePlotModules = [{ name: "gl3d" }]; + manageModeBar(gd); - gd._context.displayModeBar = false; - manageModeBar(gd); - expect(gd._fullLayout._modeBar).not.toBeDefined(); - }); + expect(countButtons(gd._fullLayout._modeBar)).toEqual(10); + }); - it('displays or not logo according to displaylogo config arg', function() { - var gd = getMockGraphInfo(); - manageModeBar(gd); - expect(countLogo(gd._fullLayout._modeBar)).toEqual(1); + it( + "updates mode bar buttons if modeBarButtonsToRemove changes", + function() { + var gd = setupGraphInfo(); + manageModeBar(gd); + var initialButtonCount = countButtons(gd._fullLayout._modeBar); + + gd._context.modeBarButtonsToRemove = ["toImage", "sendDataToCloud"]; + manageModeBar(gd); + + expect(countButtons(gd._fullLayout._modeBar)).toEqual( + initialButtonCount - 2 + ); + } + ); + + it("updates mode bar buttons if modeBarButtonsToAdd changes", function() { + var gd = setupGraphInfo(); + manageModeBar(gd); + + var initialGroupCount = countGroups(gd._fullLayout._modeBar), + initialButtonCount = countButtons(gd._fullLayout._modeBar); + + gd._context.modeBarButtonsToAdd = [{ name: "some button", click: noop }]; + manageModeBar(gd); + + expect(countGroups(gd._fullLayout._modeBar)).toEqual( + initialGroupCount + 1 + ); + expect(countButtons(gd._fullLayout._modeBar)).toEqual( + initialButtonCount + 1 + ); + }); - gd._context.displaylogo = false; - manageModeBar(gd); - expect(countLogo(gd._fullLayout._modeBar)).toEqual(0); - }); + it( + "sets up buttons with modeBarButtonsToAdd and modeBarButtonToRemove", + function() { + var gd = setupGraphInfo(); + gd._context.modeBarButtonsToRemove = [ + "toImage", + "pan2d", + "hoverCompareCartesian" + ]; + gd._context.modeBarButtonsToAdd = [ + { name: "some button", click: noop }, + { name: "some other button", click: noop } + ]; + + manageModeBar(gd); + + var modeBar = gd._fullLayout._modeBar; + expect(countGroups(modeBar)).toEqual(6); + expect(countButtons(modeBar)).toEqual(10); + } + ); + + it( + "sets up buttons with modeBarButtonsToAdd and modeBarButtonToRemove (2)", + function() { + var gd = setupGraphInfo(); + gd._context.modeBarButtonsToRemove = [ + "toImage", + "pan2d", + "hoverCompareCartesian" + ]; + gd._context.modeBarButtonsToAdd = [ + [ + { name: "some button", click: noop }, + { name: "some other button", click: noop } + ], + [ + { name: "some button 2", click: noop }, + { name: "some other button 2", click: noop } + ] + ]; + + manageModeBar(gd); + + var modeBar = gd._fullLayout._modeBar; + expect(countGroups(modeBar)).toEqual(7); + expect(countButtons(modeBar)).toEqual(12); + } + ); + + it("sets up buttons with fully custom modeBarButtons", function() { + var gd = setupGraphInfo(); + gd._context.modeBarButtons = [ + [ + { name: "some button", click: noop }, + { name: "some other button", click: noop } + ], + [ + { name: "some button in another group", click: noop }, + { name: "some other button in another group", click: noop } + ] + ]; + + manageModeBar(gd); + + var modeBar = gd._fullLayout._modeBar; + expect(countGroups(modeBar)).toEqual(3); + expect(countButtons(modeBar)).toEqual(5); + }); - // gives 11 buttons in 5 groups by default - function setupGraphInfo() { - var gd = getMockGraphInfo(); - gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; - gd._fullLayout.xaxis = {fixedrange: false}; - return gd; - } + it("sets up buttons with custom modeBarButtons + default name", function() { + var gd = setupGraphInfo(); + gd._context.modeBarButtons = [ + [ + { name: "some button", click: noop }, + { name: "some other button", click: noop } + ], + ["toImage", "pan2d", "hoverCompareCartesian"] + ]; + + manageModeBar(gd); + + var modeBar = gd._fullLayout._modeBar; + expect(countGroups(modeBar)).toEqual(3); + expect(countButtons(modeBar)).toEqual(6); + }); - it('updates mode bar buttons if plot type changes', function() { - var gd = setupGraphInfo(); - manageModeBar(gd); + it("throw error when modeBarButtons contains invalid name", function() { + var gd = setupGraphInfo(); + gd._context.modeBarButtons = [["toImage", "pan2d", "no gonna work"]]; - gd._fullLayout._basePlotModules = [{ name: 'gl3d' }]; - manageModeBar(gd); + expect(function() { + manageModeBar(gd); + }).toThrowError(); + }); + }); - expect(countButtons(gd._fullLayout._modeBar)).toEqual(10); - }); + describe("modebar on clicks", function() { + var gd, modeBar; - it('updates mode bar buttons if modeBarButtonsToRemove changes', function() { - var gd = setupGraphInfo(); - manageModeBar(gd); - var initialButtonCount = countButtons(gd._fullLayout._modeBar); + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - gd._context.modeBarButtonsToRemove = ['toImage', 'sendDataToCloud']; - manageModeBar(gd); + afterEach(destroyGraphDiv); - expect(countButtons(gd._fullLayout._modeBar)) - .toEqual(initialButtonCount - 2); - }); + function assertRange(axName, expected) { + var PRECISION = 2; - it('updates mode bar buttons if modeBarButtonsToAdd changes', function() { - var gd = setupGraphInfo(); - manageModeBar(gd); + var ax = gd._fullLayout[axName]; + var actual = ax.range; - var initialGroupCount = countGroups(gd._fullLayout._modeBar), - initialButtonCount = countButtons(gd._fullLayout._modeBar); + if (ax.type === "date") { + var truncate = function(v) { + return v.substr(0, 10); + }; + expect(actual.map(truncate)).toEqual(expected.map(truncate), axName); + } else { + expect(actual).toBeCloseToArray(expected, PRECISION, axName); + } + } - gd._context.modeBarButtonsToAdd = [{ - name: 'some button', - click: noop - }]; - manageModeBar(gd); + function assertActive(buttons, activeButton) { + for (var i = 0; i < buttons.length; i++) { + expect(buttons[i].isActive()).toBe(buttons[i] === activeButton); + } + } - expect(countGroups(gd._fullLayout._modeBar)) - .toEqual(initialGroupCount + 1); - expect(countButtons(gd._fullLayout._modeBar)) - .toEqual(initialButtonCount + 1); - }); + describe("cartesian handlers", function() { + beforeEach(function(done) { + var mockData = [ + { + type: "scatter", + x: ["2016-01-01", "2016-02-01", "2016-03-01"], + y: [10, 100, 1000] + }, + { + type: "bar", + x: ["a", "b", "c"], + y: [2, 1, 2], + xaxis: "x2", + yaxis: "y2" + } + ]; + + var mockLayout = { + xaxis: { + anchor: "y", + domain: [0, 0.5], + range: ["2016-01-01", "2016-04-01"] + }, + yaxis: { anchor: "x", type: "log", range: [1, 3] }, + xaxis2: { anchor: "y2", domain: [0.5, 1], range: [-1, 4] }, + yaxis2: { anchor: "x2", range: [0, 4] }, + width: 600, + height: 500 + }; - it('sets up buttons with modeBarButtonsToAdd and modeBarButtonToRemove', function() { - var gd = setupGraphInfo(); - gd._context.modeBarButtonsToRemove = [ - 'toImage', 'pan2d', 'hoverCompareCartesian' - ]; - gd._context.modeBarButtonsToAdd = [ - { name: 'some button', click: noop }, - { name: 'some other button', click: noop } - ]; - - manageModeBar(gd); - - var modeBar = gd._fullLayout._modeBar; - expect(countGroups(modeBar)).toEqual(6); - expect(countButtons(modeBar)).toEqual(10); - }); + gd = createGraphDiv(); + Plotly.plot(gd, mockData, mockLayout).then(function() { + modeBar = gd._fullLayout._modeBar; + done(); + }); + }); + + describe( + "buttons zoomIn2d, zoomOut2d, autoScale2d and resetScale2d", + function() { + it("should update axis ranges", function() { + var buttonZoomIn = selectButton(modeBar, "zoomIn2d"), + buttonZoomOut = selectButton(modeBar, "zoomOut2d"), + buttonAutoScale = selectButton(modeBar, "autoScale2d"), + buttonResetScale = selectButton(modeBar, "resetScale2d"); + + assertRange("xaxis", ["2016-01-01", "2016-04-01"]); + assertRange("yaxis", [1, 3]); + assertRange("xaxis2", [-1, 4]); + assertRange("yaxis2", [0, 4]); + + buttonZoomIn.click(); + assertRange("xaxis", ["2016-01-23 17:45", "2016-03-09 05:15"]); + assertRange("yaxis", [1.5, 2.5]); + assertRange("xaxis2", [0.25, 2.75]); + assertRange("yaxis2", [1, 3]); + + buttonZoomOut.click(); + assertRange("xaxis", ["2016-01-01", "2016-04-01"]); + assertRange("yaxis", [1, 3]); + assertRange("xaxis2", [-1, 4]); + assertRange("yaxis2", [0, 4]); + + buttonZoomIn.click(); + buttonAutoScale.click(); + assertRange("xaxis", [ + "2015-12-27 06:36:39.6661", + "2016-03-05 17:23:20.3339" + ]); + assertRange("yaxis", [0.8591, 3.1408]); + assertRange("xaxis2", [-0.5, 2.5]); + assertRange("yaxis2", [0, 2.105263]); + + buttonResetScale.click(); + assertRange("xaxis", ["2016-01-01", "2016-04-01"]); + assertRange("yaxis", [1, 3]); + assertRange("xaxis2", [-1, 4]); + assertRange("yaxis2", [0, 4]); + }); + } + ); + + describe("buttons zoom2d, pan2d, select2d and lasso2d", function() { + it("should update the layout dragmode", function() { + var zoom2d = selectButton(modeBar, "zoom2d"), + pan2d = selectButton(modeBar, "pan2d"), + select2d = selectButton(modeBar, "select2d"), + lasso2d = selectButton(modeBar, "lasso2d"), + buttons = [zoom2d, pan2d, select2d, lasso2d]; + + expect(gd._fullLayout.dragmode).toBe("zoom"); + assertActive(buttons, zoom2d); + + pan2d.click(); + expect(gd._fullLayout.dragmode).toBe("pan"); + assertActive(buttons, pan2d); + + select2d.click(); + expect(gd._fullLayout.dragmode).toBe("select"); + assertActive(buttons, select2d); + + lasso2d.click(); + expect(gd._fullLayout.dragmode).toBe("lasso"); + assertActive(buttons, lasso2d); + + zoom2d.click(); + expect(gd._fullLayout.dragmode).toBe("zoom"); + assertActive(buttons, zoom2d); + }); + }); + + describe( + "buttons hoverCompareCartesian and hoverClosestCartesian ", + function() { + it("should update layout hovermode", function() { + var buttonCompare = selectButton(modeBar, "hoverCompareCartesian"), + buttonClosest = selectButton(modeBar, "hoverClosestCartesian"), + buttons = [buttonCompare, buttonClosest]; + + expect(gd._fullLayout.hovermode).toBe("x"); + assertActive(buttons, buttonCompare); + + buttonClosest.click(); + expect(gd._fullLayout.hovermode).toBe("closest"); + assertActive(buttons, buttonClosest); + + buttonCompare.click(); + expect(gd._fullLayout.hovermode).toBe("x"); + assertActive(buttons, buttonCompare); + }); + } + ); + }); - it('sets up buttons with modeBarButtonsToAdd and modeBarButtonToRemove (2)', function() { - var gd = setupGraphInfo(); - gd._context.modeBarButtonsToRemove = [ - 'toImage', 'pan2d', 'hoverCompareCartesian' - ]; - gd._context.modeBarButtonsToAdd = [[ - { name: 'some button', click: noop }, - { name: 'some other button', click: noop } - ], [ - { name: 'some button 2', click: noop }, - { name: 'some other button 2', click: noop } - ]]; - - manageModeBar(gd); - - var modeBar = gd._fullLayout._modeBar; - expect(countGroups(modeBar)).toEqual(7); - expect(countButtons(modeBar)).toEqual(12); - }); + describe("pie handlers", function() { + beforeEach(function(done) { + var mockData = [ + { + type: "pie", + labels: ["apples", "bananas", "grapes"], + values: [10, 20, 30] + } + ]; - it('sets up buttons with fully custom modeBarButtons', function() { - var gd = setupGraphInfo(); - gd._context.modeBarButtons = [[ - { name: 'some button', click: noop }, - { name: 'some other button', click: noop } - ], [ - { name: 'some button in another group', click: noop }, - { name: 'some other button in another group', click: noop } - ]]; - - manageModeBar(gd); - - var modeBar = gd._fullLayout._modeBar; - expect(countGroups(modeBar)).toEqual(3); - expect(countButtons(modeBar)).toEqual(5); + gd = createGraphDiv(); + Plotly.plot(gd, mockData).then(function() { + modeBar = gd._fullLayout._modeBar; + done(); }); + }); - it('sets up buttons with custom modeBarButtons + default name', function() { - var gd = setupGraphInfo(); - gd._context.modeBarButtons = [[ - { name: 'some button', click: noop }, - { name: 'some other button', click: noop } - ], [ - 'toImage', 'pan2d', 'hoverCompareCartesian' - ]]; + describe("buttons hoverClosestPie", function() { + it("should update layout hovermode", function() { + var button = selectButton(modeBar, "hoverClosestPie"); - manageModeBar(gd); + expect(gd._fullLayout.hovermode).toBe("closest"); + expect(button.isActive()).toBe(true); - var modeBar = gd._fullLayout._modeBar; - expect(countGroups(modeBar)).toEqual(3); - expect(countButtons(modeBar)).toEqual(6); - }); + button.click(); + expect(gd._fullLayout.hovermode).toBe(false); + expect(button.isActive()).toBe(false); - it('throw error when modeBarButtons contains invalid name', function() { - var gd = setupGraphInfo(); - gd._context.modeBarButtons = [[ - 'toImage', 'pan2d', 'no gonna work' - ]]; - - expect(function() { manageModeBar(gd); }).toThrowError(); + button.click(); + expect(gd._fullLayout.hovermode).toBe("closest"); + expect(button.isActive()).toBe(true); }); - + }); }); - describe('modebar on clicks', function() { - var gd, modeBar; + describe("geo handlers", function() { + beforeEach(function(done) { + var mockData = [ + { type: "scattergeo", lon: [10, 20, 30], lat: [10, 20, 30] } + ]; - beforeAll(function() { - jasmine.addMatchers(customMatchers); + gd = createGraphDiv(); + Plotly.plot(gd, mockData).then(function() { + modeBar = gd._fullLayout._modeBar; + done(); }); + }); - afterEach(destroyGraphDiv); + describe("buttons hoverClosestGeo", function() { + it("should update layout hovermode", function() { + var button = selectButton(modeBar, "hoverClosestGeo"); - function assertRange(axName, expected) { - var PRECISION = 2; + expect(gd._fullLayout.hovermode).toBe("closest"); + expect(button.isActive()).toBe(true); - var ax = gd._fullLayout[axName]; - var actual = ax.range; + button.click(); + expect(gd._fullLayout.hovermode).toBe(false); + expect(button.isActive()).toBe(false); - if(ax.type === 'date') { - var truncate = function(v) { return v.substr(0, 10); }; - expect(actual.map(truncate)).toEqual(expected.map(truncate), axName); - } - else { - expect(actual).toBeCloseToArray(expected, PRECISION, axName); - } - } - - function assertActive(buttons, activeButton) { - for(var i = 0; i < buttons.length; i++) { - expect(buttons[i].isActive()).toBe( - buttons[i] === activeButton - ); - } - } - - describe('cartesian handlers', function() { - - beforeEach(function(done) { - var mockData = [{ - type: 'scatter', - x: ['2016-01-01', '2016-02-01', '2016-03-01'], - y: [10, 100, 1000], - }, { - type: 'bar', - x: ['a', 'b', 'c'], - y: [2, 1, 2], - xaxis: 'x2', - yaxis: 'y2' - }]; - - var mockLayout = { - xaxis: { - anchor: 'y', - domain: [0, 0.5], - range: ['2016-01-01', '2016-04-01'] - }, - yaxis: { - anchor: 'x', - type: 'log', - range: [1, 3] - }, - xaxis2: { - anchor: 'y2', - domain: [0.5, 1], - range: [-1, 4] - }, - yaxis2: { - anchor: 'x2', - range: [0, 4] - }, - width: 600, - height: 500 - }; - - gd = createGraphDiv(); - Plotly.plot(gd, mockData, mockLayout).then(function() { - modeBar = gd._fullLayout._modeBar; - done(); - }); - }); - - describe('buttons zoomIn2d, zoomOut2d, autoScale2d and resetScale2d', function() { - it('should update axis ranges', function() { - var buttonZoomIn = selectButton(modeBar, 'zoomIn2d'), - buttonZoomOut = selectButton(modeBar, 'zoomOut2d'), - buttonAutoScale = selectButton(modeBar, 'autoScale2d'), - buttonResetScale = selectButton(modeBar, 'resetScale2d'); - - assertRange('xaxis', ['2016-01-01', '2016-04-01']); - assertRange('yaxis', [1, 3]); - assertRange('xaxis2', [-1, 4]); - assertRange('yaxis2', [0, 4]); - - buttonZoomIn.click(); - assertRange('xaxis', ['2016-01-23 17:45', '2016-03-09 05:15']); - assertRange('yaxis', [1.5, 2.5]); - assertRange('xaxis2', [0.25, 2.75]); - assertRange('yaxis2', [1, 3]); - - buttonZoomOut.click(); - assertRange('xaxis', ['2016-01-01', '2016-04-01']); - assertRange('yaxis', [1, 3]); - assertRange('xaxis2', [-1, 4]); - assertRange('yaxis2', [0, 4]); - - buttonZoomIn.click(); - buttonAutoScale.click(); - assertRange('xaxis', ['2015-12-27 06:36:39.6661', '2016-03-05 17:23:20.3339']); - assertRange('yaxis', [0.8591, 3.1408]); - assertRange('xaxis2', [-0.5, 2.5]); - assertRange('yaxis2', [0, 2.105263]); - - buttonResetScale.click(); - assertRange('xaxis', ['2016-01-01', '2016-04-01']); - assertRange('yaxis', [1, 3]); - assertRange('xaxis2', [-1, 4]); - assertRange('yaxis2', [0, 4]); - }); - }); - - describe('buttons zoom2d, pan2d, select2d and lasso2d', function() { - it('should update the layout dragmode', function() { - var zoom2d = selectButton(modeBar, 'zoom2d'), - pan2d = selectButton(modeBar, 'pan2d'), - select2d = selectButton(modeBar, 'select2d'), - lasso2d = selectButton(modeBar, 'lasso2d'), - buttons = [zoom2d, pan2d, select2d, lasso2d]; - - expect(gd._fullLayout.dragmode).toBe('zoom'); - assertActive(buttons, zoom2d); - - pan2d.click(); - expect(gd._fullLayout.dragmode).toBe('pan'); - assertActive(buttons, pan2d); - - select2d.click(); - expect(gd._fullLayout.dragmode).toBe('select'); - assertActive(buttons, select2d); - - lasso2d.click(); - expect(gd._fullLayout.dragmode).toBe('lasso'); - assertActive(buttons, lasso2d); - - zoom2d.click(); - expect(gd._fullLayout.dragmode).toBe('zoom'); - assertActive(buttons, zoom2d); - }); - }); - - describe('buttons hoverCompareCartesian and hoverClosestCartesian ', function() { - it('should update layout hovermode', function() { - var buttonCompare = selectButton(modeBar, 'hoverCompareCartesian'), - buttonClosest = selectButton(modeBar, 'hoverClosestCartesian'), - buttons = [buttonCompare, buttonClosest]; - - expect(gd._fullLayout.hovermode).toBe('x'); - assertActive(buttons, buttonCompare); - - buttonClosest.click(); - expect(gd._fullLayout.hovermode).toBe('closest'); - assertActive(buttons, buttonClosest); - - buttonCompare.click(); - expect(gd._fullLayout.hovermode).toBe('x'); - assertActive(buttons, buttonCompare); - }); - }); + button.click(); + expect(gd._fullLayout.hovermode).toBe("closest"); + expect(button.isActive()).toBe(true); }); - - describe('pie handlers', function() { - - beforeEach(function(done) { - var mockData = [{ - type: 'pie', - labels: ['apples', 'bananas', 'grapes'], - values: [10, 20, 30] - }]; - - gd = createGraphDiv(); - Plotly.plot(gd, mockData).then(function() { - modeBar = gd._fullLayout._modeBar; - done(); - }); - }); - - describe('buttons hoverClosestPie', function() { - it('should update layout hovermode', function() { - var button = selectButton(modeBar, 'hoverClosestPie'); - - expect(gd._fullLayout.hovermode).toBe('closest'); - expect(button.isActive()).toBe(true); - - button.click(); - expect(gd._fullLayout.hovermode).toBe(false); - expect(button.isActive()).toBe(false); - - button.click(); - expect(gd._fullLayout.hovermode).toBe('closest'); - expect(button.isActive()).toBe(true); - }); - }); - }); - - describe('geo handlers', function() { - - beforeEach(function(done) { - var mockData = [{ - type: 'scattergeo', - lon: [10, 20, 30], - lat: [10, 20, 30] - }]; - - gd = createGraphDiv(); - Plotly.plot(gd, mockData).then(function() { - modeBar = gd._fullLayout._modeBar; - done(); - }); - }); - - describe('buttons hoverClosestGeo', function() { - it('should update layout hovermode', function() { - var button = selectButton(modeBar, 'hoverClosestGeo'); - - expect(gd._fullLayout.hovermode).toBe('closest'); - expect(button.isActive()).toBe(true); - - button.click(); - expect(gd._fullLayout.hovermode).toBe(false); - expect(button.isActive()).toBe(false); - - button.click(); - expect(gd._fullLayout.hovermode).toBe('closest'); - expect(button.isActive()).toBe(true); - }); - }); - - }); - + }); }); + }); }); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index e6deca855b0..084638b4823 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -1,1311 +1,1334 @@ -var Plotly = require('@lib/index'); -var PlotlyInternal = require('@src/plotly'); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); -var Scatter = require('@src/traces/scatter'); -var Bar = require('@src/traces/bar'); -var Legend = require('@src/components/legend'); -var pkg = require('../../../package.json'); -var subroutines = require('@src/plot_api/subroutines'); - -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); - - -describe('Test plot api', function() { - 'use strict'; - - describe('Plotly.version', function() { - it('should be the same as in the package.json', function() { - expect(Plotly.version).toEqual(pkg.version); - }); +var Plotly = require("@lib/index"); +var PlotlyInternal = require("@src/plotly"); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); +var Scatter = require("@src/traces/scatter"); +var Bar = require("@src/traces/bar"); +var Legend = require("@src/components/legend"); +var pkg = require("../../../package.json"); +var subroutines = require("@src/plot_api/subroutines"); + +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var fail = require("../assets/fail_test"); + +describe("Test plot api", function() { + "use strict"; + describe("Plotly.version", function() { + it("should be the same as in the package.json", function() { + expect(Plotly.version).toEqual(pkg.version); }); + }); - describe('Plotly.plot', function() { - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - it('accepts gd, data, layout, and config as args', function(done) { - Plotly.plot(gd, - [{x: [1, 2, 3], y: [1, 2, 3]}], - {width: 500, height: 500}, - {editable: true} - ).then(function() { - expect(gd.layout.width).toEqual(500); - expect(gd.layout.height).toEqual(500); - expect(gd.data.length).toEqual(1); - expect(gd._context.editable).toBe(true); - }).catch(fail).then(done); - }); - - it('accepts gd and an object as args', function(done) { - Plotly.plot(gd, { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], - layout: {width: 500, height: 500}, - config: {editable: true}, - frames: [{y: [2, 1, 0], name: 'frame1'}] - }).then(function() { - expect(gd.layout.width).toEqual(500); - expect(gd.layout.height).toEqual(500); - expect(gd.data.length).toEqual(1); - expect(gd._transitionData._frames.length).toEqual(1); - expect(gd._context.editable).toBe(true); - }).catch(fail).then(done); - }); + describe("Plotly.plot", function() { + var gd; - it('allows adding more frames to the initial set', function(done) { - Plotly.plot(gd, { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], - layout: {width: 500, height: 500}, - config: {editable: true}, - frames: [{y: [7, 7, 7], name: 'frame1'}] - }).then(function() { - expect(gd.layout.width).toEqual(500); - expect(gd.layout.height).toEqual(500); - expect(gd.data.length).toEqual(1); - expect(gd._transitionData._frames.length).toEqual(1); - expect(gd._context.editable).toBe(true); - - return Plotly.addFrames(gd, [ - {y: [8, 8, 8], name: 'frame2'}, - {y: [9, 9, 9], name: 'frame3'} - ]); - }).then(function() { - expect(gd._transitionData._frames.length).toEqual(3); - expect(gd._transitionData._frames[0].name).toEqual('frame1'); - expect(gd._transitionData._frames[1].name).toEqual('frame2'); - expect(gd._transitionData._frames[2].name).toEqual('frame3'); - }).catch(fail).then(done); - }); - - it('should emit afterplot event after plotting is done', function(done) { - var afterPlot = false; - - var promise = Plotly.plot(gd, [{ y: [2, 1, 2]}]); - - gd.on('plotly_afterplot', function() { - afterPlot = true; - }); + beforeEach(function() { + gd = createGraphDiv(); + }); - promise.then(function() { - expect(afterPlot).toBe(true); - }) - .then(done); - }); + afterEach(destroyGraphDiv); + + it("accepts gd, data, layout, and config as args", function(done) { + Plotly.plot( + gd, + [{ x: [1, 2, 3], y: [1, 2, 3] }], + { width: 500, height: 500 }, + { editable: true } + ) + .then(function() { + expect(gd.layout.width).toEqual(500); + expect(gd.layout.height).toEqual(500); + expect(gd.data.length).toEqual(1); + expect(gd._context.editable).toBe(true); + }) + .catch(fail) + .then(done); }); - describe('Plotly.relayout', function() { - var gd; + it("accepts gd and an object as args", function(done) { + Plotly.plot(gd, { + data: [{ x: [1, 2, 3], y: [1, 2, 3] }], + layout: { width: 500, height: 500 }, + config: { editable: true }, + frames: [{ y: [2, 1, 0], name: "frame1" }] + }) + .then(function() { + expect(gd.layout.width).toEqual(500); + expect(gd.layout.height).toEqual(500); + expect(gd.data.length).toEqual(1); + expect(gd._transitionData._frames.length).toEqual(1); + expect(gd._context.editable).toBe(true); + }) + .catch(fail) + .then(done); + }); - beforeEach(function() { - gd = createGraphDiv(); - }); + it("allows adding more frames to the initial set", function(done) { + Plotly.plot(gd, { + data: [{ x: [1, 2, 3], y: [1, 2, 3] }], + layout: { width: 500, height: 500 }, + config: { editable: true }, + frames: [{ y: [7, 7, 7], name: "frame1" }] + }) + .then(function() { + expect(gd.layout.width).toEqual(500); + expect(gd.layout.height).toEqual(500); + expect(gd.data.length).toEqual(1); + expect(gd._transitionData._frames.length).toEqual(1); + expect(gd._context.editable).toBe(true); + + return Plotly.addFrames(gd, [ + { y: [8, 8, 8], name: "frame2" }, + { y: [9, 9, 9], name: "frame3" } + ]); + }) + .then(function() { + expect(gd._transitionData._frames.length).toEqual(3); + expect(gd._transitionData._frames[0].name).toEqual("frame1"); + expect(gd._transitionData._frames[1].name).toEqual("frame2"); + expect(gd._transitionData._frames[2].name).toEqual("frame3"); + }) + .catch(fail) + .then(done); + }); - afterEach(destroyGraphDiv); + it("should emit afterplot event after plotting is done", function(done) { + var afterPlot = false; - it('should update the plot clipPath if the plot is resized', function(done) { + var promise = Plotly.plot(gd, [{ y: [2, 1, 2] }]); - Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }], { width: 500, height: 500 }) - .then(function() { - return Plotly.relayout(gd, { width: 400, height: 400 }); - }) - .then(function() { - var uid = gd._fullLayout._uid; + gd.on("plotly_afterplot", function() { + afterPlot = true; + }); - var plotClip = document.getElementById('clip' + uid + 'xyplot'), - clipRect = plotClip.children[0], - clipWidth = +clipRect.getAttribute('width'), - clipHeight = +clipRect.getAttribute('height'); + promise + .then(function() { + expect(afterPlot).toBe(true); + }) + .then(done); + }); + }); - expect(clipWidth).toBe(240); - expect(clipHeight).toBe(220); - }) - .then(done); - }); + describe("Plotly.relayout", function() { + var gd; - it('sets null values to their default', function(done) { - var defaultWidth; - Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) - .then(function() { - defaultWidth = gd._fullLayout.width; - return Plotly.relayout(gd, { width: defaultWidth - 25}); - }) - .then(function() { - expect(gd._fullLayout.width).toBe(defaultWidth - 25); - return Plotly.relayout(gd, { width: null }); - }) - .then(function() { - expect(gd._fullLayout.width).toBe(defaultWidth); - }) - .then(done); - }); + beforeEach(function() { + gd = createGraphDiv(); + }); - it('ignores undefined values', function(done) { - var defaultWidth; - Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) - .then(function() { - defaultWidth = gd._fullLayout.width; - return Plotly.relayout(gd, { width: defaultWidth - 25}); - }) - .then(function() { - expect(gd._fullLayout.width).toBe(defaultWidth - 25); - return Plotly.relayout(gd, { width: undefined }); - }) - .then(function() { - expect(gd._fullLayout.width).toBe(defaultWidth - 25); - }) - .then(done); - }); + afterEach(destroyGraphDiv); + + it("should update the plot clipPath if the plot is resized", function( + done + ) { + Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }], { + width: 500, + height: 500 + }) + .then(function() { + return Plotly.relayout(gd, { width: 400, height: 400 }); + }) + .then(function() { + var uid = gd._fullLayout._uid; + + var plotClip = document.getElementById("clip" + uid + "xyplot"), + clipRect = plotClip.children[0], + clipWidth = +clipRect.getAttribute("width"), + clipHeight = +clipRect.getAttribute("height"); + + expect(clipWidth).toBe(240); + expect(clipHeight).toBe(220); + }) + .then(done); + }); - it('can set items in array objects', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) - .then(function() { - return Plotly.relayout(gd, {rando: [1, 2, 3]}); - }) - .then(function() { - expect(gd.layout.rando).toEqual([1, 2, 3]); - return Plotly.relayout(gd, {'rando[1]': 45}); - }) - .then(function() { - expect(gd.layout.rando).toEqual([1, 45, 3]); - }) - .then(done); - }); + it("sets null values to their default", function(done) { + var defaultWidth; + Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) + .then(function() { + defaultWidth = gd._fullLayout.width; + return Plotly.relayout(gd, { width: defaultWidth - 25 }); + }) + .then(function() { + expect(gd._fullLayout.width).toBe(defaultWidth - 25); + return Plotly.relayout(gd, { width: null }); + }) + .then(function() { + expect(gd._fullLayout.width).toBe(defaultWidth); + }) + .then(done); + }); - it('can set empty text nodes', function(done) { - var data = [{ - x: [1, 2, 3], - y: [0, 0, 0], - text: ['', 'Text', ''], - mode: 'lines+text' - }]; - var scatter = null; - var oldHeight = 0; - Plotly.plot(gd, data) - .then(function() { - scatter = document.getElementsByClassName('scatter')[0]; - oldHeight = scatter.getBoundingClientRect().height; - return Plotly.relayout(gd, 'yaxis.range', [0.5, 0.5, 0.5]); - }) - .then(function() { - var newHeight = scatter.getBoundingClientRect().height; - expect(newHeight).toEqual(oldHeight); - }) - .then(done); - }); + it("ignores undefined values", function(done) { + var defaultWidth; + Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) + .then(function() { + defaultWidth = gd._fullLayout.width; + return Plotly.relayout(gd, { width: defaultWidth - 25 }); + }) + .then(function() { + expect(gd._fullLayout.width).toBe(defaultWidth - 25); + return Plotly.relayout(gd, { width: undefined }); + }) + .then(function() { + expect(gd._fullLayout.width).toBe(defaultWidth - 25); + }) + .then(done); }); - describe('Plotly.restyle', function() { - beforeEach(function() { - spyOn(PlotlyInternal, 'plot'); - spyOn(Plots, 'previousPromises'); - spyOn(Scatter, 'arraysToCalcdata'); - spyOn(Bar, 'arraysToCalcdata'); - spyOn(Plots, 'style'); - spyOn(Legend, 'draw'); - }); + it("can set items in array objects", function(done) { + Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) + .then(function() { + return Plotly.relayout(gd, { rando: [1, 2, 3] }); + }) + .then(function() { + expect(gd.layout.rando).toEqual([1, 2, 3]); + return Plotly.relayout(gd, { "rando[1]": 45 }); + }) + .then(function() { + expect(gd.layout.rando).toEqual([1, 45, 3]); + }) + .then(done); + }); - function mockDefaultsAndCalc(gd) { - Plots.supplyDefaults(gd); - gd.calcdata = gd._fullData.map(function(trace) { - return [{x: 1, y: 1, trace: trace}]; - }); + it("can set empty text nodes", function(done) { + var data = [ + { + x: [1, 2, 3], + y: [0, 0, 0], + text: ["", "Text", ""], + mode: "lines+text" } - - it('calls Scatter.arraysToCalcdata and Plots.style on scatter styling', function() { - var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], - layout: {} - }; - mockDefaultsAndCalc(gd); - Plotly.restyle(gd, {'marker.color': 'red'}); - expect(Scatter.arraysToCalcdata).toHaveBeenCalled(); - expect(Bar.arraysToCalcdata).not.toHaveBeenCalled(); - expect(Plots.style).toHaveBeenCalled(); - expect(PlotlyInternal.plot).not.toHaveBeenCalled(); - // "docalc" deletes gd.calcdata - make sure this didn't happen - expect(gd.calcdata).toBeDefined(); - }); - - it('calls Bar.arraysToCalcdata and Plots.style on bar styling', function() { - var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3], type: 'bar'}], - layout: {} - }; - mockDefaultsAndCalc(gd); - Plotly.restyle(gd, {'marker.color': 'red'}); - expect(Scatter.arraysToCalcdata).not.toHaveBeenCalled(); - expect(Bar.arraysToCalcdata).toHaveBeenCalled(); - expect(Plots.style).toHaveBeenCalled(); - expect(PlotlyInternal.plot).not.toHaveBeenCalled(); - expect(gd.calcdata).toBeDefined(); - }); - - it('calls plot on xgap and ygap styling', function() { - var gd = { - data: [{z: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], showscale: false, type: 'heatmap'}], - layout: {} - }; - - mockDefaultsAndCalc(gd); - Plotly.restyle(gd, {'xgap': 2}); - expect(PlotlyInternal.plot).toHaveBeenCalled(); - - Plotly.restyle(gd, {'ygap': 2}); - expect(PlotlyInternal.plot.calls.count()).toEqual(2); - }); - - it('ignores undefined values', function() { - var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3], type: 'scatter'}], - layout: {} - }; - - mockDefaultsAndCalc(gd); - - // Check to see that the color is updated: - Plotly.restyle(gd, {'marker.color': 'blue'}); - expect(gd._fullData[0].marker.color).toBe('blue'); - - // Check to see that the color is unaffected: - Plotly.restyle(gd, {'marker.color': undefined}); - expect(gd._fullData[0].marker.color).toBe('blue'); - }); - - it('restores null values to defaults', function() { - var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3], type: 'scatter'}], - layout: {} - }; - - mockDefaultsAndCalc(gd); - var colorDflt = gd._fullData[0].marker.color; - - // Check to see that the color is updated: - Plotly.restyle(gd, {'marker.color': 'blue'}); - expect(gd._fullData[0].marker.color).toBe('blue'); - - // Check to see that the color is restored to the original default: - Plotly.restyle(gd, {'marker.color': null}); - expect(gd._fullData[0].marker.color).toBe(colorDflt); - }); - - it('can target specific traces by leaving properties undefined', function() { - var gd = { - data: [ - {x: [1, 2, 3], y: [1, 2, 3], type: 'scatter'}, - {x: [1, 2, 3], y: [3, 4, 5], type: 'scatter'} - ], - layout: {} - }; - - mockDefaultsAndCalc(gd); - var colorDflt = [gd._fullData[0].marker.color, gd._fullData[1].marker.color]; - - // Check only second trace's color has been changed: - Plotly.restyle(gd, {'marker.color': [undefined, 'green']}); - expect(gd._fullData[0].marker.color).toBe(colorDflt[0]); - expect(gd._fullData[1].marker.color).toBe('green'); - - // Check both colors restored to the original default: - Plotly.restyle(gd, {'marker.color': [null, null]}); - expect(gd._fullData[0].marker.color).toBe(colorDflt[0]); - expect(gd._fullData[1].marker.color).toBe(colorDflt[1]); - }); + ]; + var scatter = null; + var oldHeight = 0; + Plotly.plot(gd, data) + .then(function() { + scatter = document.getElementsByClassName("scatter")[0]; + oldHeight = scatter.getBoundingClientRect().height; + return Plotly.relayout(gd, "yaxis.range", [0.5, 0.5, 0.5]); + }) + .then(function() { + var newHeight = scatter.getBoundingClientRect().height; + expect(newHeight).toEqual(oldHeight); + }) + .then(done); }); - - describe('Plotly.restyle unmocked', function() { - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(function() { - destroyGraphDiv(); - }); - - it('should redo auto z/contour when editing z array', function() { - Plotly.plot(gd, [{type: 'contour', z: [[1, 2], [3, 4]]}]).then(function() { - expect(gd.data[0].zauto).toBe(true, gd.data[0]); - expect(gd.data[0].zmin).toBe(1); - expect(gd.data[0].zmax).toBe(4); - - expect(gd.data[0].autocontour).toBe(true); - expect(gd.data[0].contours).toEqual({start: 1.5, end: 3.5, size: 0.5}); - - return Plotly.restyle(gd, {'z[0][0]': 10}); - }).then(function() { - expect(gd.data[0].zmin).toBe(2); - expect(gd.data[0].zmax).toBe(10); - - expect(gd.data[0].contours).toEqual({start: 3, end: 9, size: 1}); - }); - }); + }); + + describe("Plotly.restyle", function() { + beforeEach(function() { + spyOn(PlotlyInternal, "plot"); + spyOn(Plots, "previousPromises"); + spyOn(Scatter, "arraysToCalcdata"); + spyOn(Bar, "arraysToCalcdata"); + spyOn(Plots, "style"); + spyOn(Legend, "draw"); }); - describe('Plotly.deleteTraces', function() { - var gd; - - beforeEach(function() { - gd = { - data: [ - {'name': 'a'}, - {'name': 'b'}, - {'name': 'c'}, - {'name': 'd'} - ] - }; - spyOn(PlotlyInternal, 'redraw'); - }); - - it('should throw an error when indices are omitted', function() { - - expect(function() { - Plotly.deleteTraces(gd); - }).toThrow(new Error('indices must be an integer or array of integers.')); - - }); - - it('should throw an error when indices are out of bounds', function() { - - expect(function() { - Plotly.deleteTraces(gd, 10); - }).toThrow(new Error('indices must be valid indices for gd.data.')); - - }); - - it('should throw an error when indices are repeated', function() { - - expect(function() { - Plotly.deleteTraces(gd, [0, 0]); - }).toThrow(new Error('each index in indices must be unique.')); - - }); - - it('should work when indices are negative', function() { - var expectedData = [ - {'name': 'a'}, - {'name': 'b'}, - {'name': 'c'} - ]; - - Plotly.deleteTraces(gd, -1); - expect(gd.data).toEqual(expectedData); - expect(PlotlyInternal.redraw).toHaveBeenCalled(); - - }); - - it('should work when multiple traces are deleted', function() { - var expectedData = [ - {'name': 'b'}, - {'name': 'c'} - ]; - - Plotly.deleteTraces(gd, [0, 3]); - expect(gd.data).toEqual(expectedData); - expect(PlotlyInternal.redraw).toHaveBeenCalled(); - - }); - - it('should work when indices are not sorted', function() { - var expectedData = [ - {'name': 'b'}, - {'name': 'c'} - ]; - - Plotly.deleteTraces(gd, [3, 0]); - expect(gd.data).toEqual(expectedData); - expect(PlotlyInternal.redraw).toHaveBeenCalled(); - - }); - - it('should work with more than 10 indices', function() { - gd.data = []; - - for(var i = 0; i < 20; i++) { - gd.data.push({ - name: 'trace #' + i - }); - } - - var expectedData = [ - {name: 'trace #12'}, - {name: 'trace #13'}, - {name: 'trace #14'}, - {name: 'trace #15'}, - {name: 'trace #16'}, - {name: 'trace #17'}, - {name: 'trace #18'}, - {name: 'trace #19'} - ]; - - Plotly.deleteTraces(gd, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - expect(gd.data).toEqual(expectedData); - expect(PlotlyInternal.redraw).toHaveBeenCalled(); - - }); - + function mockDefaultsAndCalc(gd) { + Plots.supplyDefaults(gd); + gd.calcdata = gd._fullData.map(function(trace) { + return [{ x: 1, y: 1, trace: trace }]; + }); + } + + it( + "calls Scatter.arraysToCalcdata and Plots.style on scatter styling", + function() { + var gd = { data: [{ x: [1, 2, 3], y: [1, 2, 3] }], layout: {} }; + mockDefaultsAndCalc(gd); + Plotly.restyle(gd, { "marker.color": "red" }); + expect(Scatter.arraysToCalcdata).toHaveBeenCalled(); + expect(Bar.arraysToCalcdata).not.toHaveBeenCalled(); + expect(Plots.style).toHaveBeenCalled(); + expect(PlotlyInternal.plot).not.toHaveBeenCalled(); + // "docalc" deletes gd.calcdata - make sure this didn't happen + expect(gd.calcdata).toBeDefined(); + } + ); + + it("calls Bar.arraysToCalcdata and Plots.style on bar styling", function() { + var gd = { + data: [{ x: [1, 2, 3], y: [1, 2, 3], type: "bar" }], + layout: {} + }; + mockDefaultsAndCalc(gd); + Plotly.restyle(gd, { "marker.color": "red" }); + expect(Scatter.arraysToCalcdata).not.toHaveBeenCalled(); + expect(Bar.arraysToCalcdata).toHaveBeenCalled(); + expect(Plots.style).toHaveBeenCalled(); + expect(PlotlyInternal.plot).not.toHaveBeenCalled(); + expect(gd.calcdata).toBeDefined(); }); - describe('Plotly.addTraces', function() { - var gd; - - beforeEach(function() { - gd = { data: [{'name': 'a'}, {'name': 'b'}] }; - spyOn(PlotlyInternal, 'redraw'); - spyOn(PlotlyInternal, 'moveTraces'); - }); - - it('should throw an error when traces is not an object or an array of objects', function() { - var expected = JSON.parse(JSON.stringify(gd)); - expect(function() { - Plotly.addTraces(gd, 1, 2); - }).toThrowError(Error, 'all values in traces array must be non-array objects'); - - expect(function() { - Plotly.addTraces(gd, [{}, 4], 2); - }).toThrowError(Error, 'all values in traces array must be non-array objects'); - - expect(function() { - Plotly.addTraces(gd, [{}, []], 2); - }).toThrowError(Error, 'all values in traces array must be non-array objects'); - - // make sure we didn't muck with gd.data if things failed! - expect(gd).toEqual(expected); - }); - - it('should throw an error when traces and newIndices arrays are unequal', function() { - - expect(function() { - Plotly.addTraces(gd, [{}, {}], 2); - }).toThrowError(Error, 'if indices is specified, traces.length must equal indices.length'); - - }); - - it('should throw an error when newIndices are out of bounds', function() { - var expected = JSON.parse(JSON.stringify(gd)); - - expect(function() { - Plotly.addTraces(gd, [{}, {}], [0, 10]); - }).toThrow(new Error('newIndices must be valid indices for gd.data.')); - - // make sure we didn't muck with gd.data if things failed! - expect(gd).toEqual(expected); - }); - - it('should work when newIndices is undefined', function() { - Plotly.addTraces(gd, [{'name': 'c'}, {'name': 'd'}]); - expect(gd.data[2].name).toBeDefined(); - expect(gd.data[2].uid).toBeDefined(); - expect(gd.data[3].name).toBeDefined(); - expect(gd.data[3].uid).toBeDefined(); - expect(PlotlyInternal.redraw).toHaveBeenCalled(); - expect(PlotlyInternal.moveTraces).not.toHaveBeenCalled(); - }); - - it('should work when newIndices is defined', function() { - Plotly.addTraces(gd, [{'name': 'c'}, {'name': 'd'}], [1, 3]); - expect(gd.data[2].name).toBeDefined(); - expect(gd.data[2].uid).toBeDefined(); - expect(gd.data[3].name).toBeDefined(); - expect(gd.data[3].uid).toBeDefined(); - expect(PlotlyInternal.redraw).not.toHaveBeenCalled(); - expect(PlotlyInternal.moveTraces).toHaveBeenCalledWith(gd, [-2, -1], [1, 3]); - }); - - it('should work when newIndices has negative indices', function() { - Plotly.addTraces(gd, [{'name': 'c'}, {'name': 'd'}], [-3, -1]); - expect(gd.data[2].name).toBeDefined(); - expect(gd.data[2].uid).toBeDefined(); - expect(gd.data[3].name).toBeDefined(); - expect(gd.data[3].uid).toBeDefined(); - expect(PlotlyInternal.redraw).not.toHaveBeenCalled(); - expect(PlotlyInternal.moveTraces).toHaveBeenCalledWith(gd, [-2, -1], [-3, -1]); - }); - - it('should work when newIndices is an integer', function() { - Plotly.addTraces(gd, {'name': 'c'}, 0); - expect(gd.data[2].name).toBeDefined(); - expect(gd.data[2].uid).toBeDefined(); - expect(PlotlyInternal.redraw).not.toHaveBeenCalled(); - expect(PlotlyInternal.moveTraces).toHaveBeenCalledWith(gd, [-1], [0]); - }); - - it('should work when adding an existing trace', function() { - Plotly.addTraces(gd, gd.data[0]); - - expect(gd.data.length).toEqual(3); - expect(gd.data[0]).not.toBe(gd.data[2]); - }); - - it('should work when duplicating the existing data', function() { - Plotly.addTraces(gd, gd.data); - - expect(gd.data.length).toEqual(4); - expect(gd.data[0]).not.toBe(gd.data[2]); - expect(gd.data[1]).not.toBe(gd.data[3]); - }); + it("calls plot on xgap and ygap styling", function() { + var gd = { + data: [ + { + z: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + showscale: false, + type: "heatmap" + } + ], + layout: {} + }; + + mockDefaultsAndCalc(gd); + Plotly.restyle(gd, { xgap: 2 }); + expect(PlotlyInternal.plot).toHaveBeenCalled(); + + Plotly.restyle(gd, { ygap: 2 }); + expect(PlotlyInternal.plot.calls.count()).toEqual(2); }); - describe('Plotly.moveTraces should', function() { - var gd; - beforeEach(function() { - gd = { - data: [ - {'name': 'a'}, - {'name': 'b'}, - {'name': 'c'}, - {'name': 'd'} - ] - }; - spyOn(PlotlyInternal, 'redraw'); - }); - - it('throw an error when index arrays are unequal', function() { - expect(function() { - Plotly.moveTraces(gd, [1], [2, 1]); - }).toThrow(new Error('current and new indices must be of equal length.')); - }); - - it('throw an error when gd.data isn\'t an array.', function() { - expect(function() { - Plotly.moveTraces({}, [0], [0]); - }).toThrow(new Error('gd.data must be an array.')); - expect(function() { - Plotly.moveTraces({data: 'meow'}, [0], [0]); - }).toThrow(new Error('gd.data must be an array.')); - }); - - it('thow an error when a current index is out of bounds', function() { - expect(function() { - Plotly.moveTraces(gd, [-gd.data.length - 1], [0]); - }).toThrow(new Error('currentIndices must be valid indices for gd.data.')); - expect(function() { - Plotly.moveTraces(gd, [gd.data.length], [0]); - }).toThrow(new Error('currentIndices must be valid indices for gd.data.')); - }); - - it('thow an error when a new index is out of bounds', function() { - expect(function() { - Plotly.moveTraces(gd, [0], [-gd.data.length - 1]); - }).toThrow(new Error('newIndices must be valid indices for gd.data.')); - expect(function() { - Plotly.moveTraces(gd, [0], [gd.data.length]); - }).toThrow(new Error('newIndices must be valid indices for gd.data.')); - }); - - it('thow an error when current indices are repeated', function() { - expect(function() { - Plotly.moveTraces(gd, [0, 0], [0, 1]); - }).toThrow(new Error('each index in currentIndices must be unique.')); - - // note that both positive and negative indices are accepted! - expect(function() { - Plotly.moveTraces(gd, [0, -gd.data.length], [0, 1]); - }).toThrow(new Error('each index in currentIndices must be unique.')); - }); - - it('thow an error when new indices are repeated', function() { - expect(function() { - Plotly.moveTraces(gd, [0, 1], [0, 0]); - }).toThrow(new Error('each index in newIndices must be unique.')); - - // note that both positive and negative indices are accepted! - expect(function() { - Plotly.moveTraces(gd, [0, 1], [-gd.data.length, 0]); - }).toThrow(new Error('each index in newIndices must be unique.')); - }); - - it('accept integers in place of arrays', function() { - var expectedData = [ - {'name': 'b'}, - {'name': 'a'}, - {'name': 'c'}, - {'name': 'd'} - ]; - - Plotly.moveTraces(gd, 0, 1); - expect(gd.data).toEqual(expectedData); - expect(PlotlyInternal.redraw).toHaveBeenCalled(); - - }); - - it('handle unsorted currentIndices', function() { - var expectedData = [ - {'name': 'd'}, - {'name': 'a'}, - {'name': 'c'}, - {'name': 'b'} - ]; + it("ignores undefined values", function() { + var gd = { + data: [{ x: [1, 2, 3], y: [1, 2, 3], type: "scatter" }], + layout: {} + }; - Plotly.moveTraces(gd, [3, 1], [0, 3]); - expect(gd.data).toEqual(expectedData); - expect(PlotlyInternal.redraw).toHaveBeenCalled(); + mockDefaultsAndCalc(gd); - }); + // Check to see that the color is updated: + Plotly.restyle(gd, { "marker.color": "blue" }); + expect(gd._fullData[0].marker.color).toBe("blue"); - it('work when newIndices are undefined.', function() { - var expectedData = [ - {'name': 'b'}, - {'name': 'c'}, - {'name': 'd'}, - {'name': 'a'} - ]; + // Check to see that the color is unaffected: + Plotly.restyle(gd, { "marker.color": undefined }); + expect(gd._fullData[0].marker.color).toBe("blue"); + }); - Plotly.moveTraces(gd, [3, 0]); - expect(gd.data).toEqual(expectedData); - expect(PlotlyInternal.redraw).toHaveBeenCalled(); + it("restores null values to defaults", function() { + var gd = { + data: [{ x: [1, 2, 3], y: [1, 2, 3], type: "scatter" }], + layout: {} + }; - }); + mockDefaultsAndCalc(gd); + var colorDflt = gd._fullData[0].marker.color; - it('accept negative indices.', function() { - var expectedData = [ - {'name': 'a'}, - {'name': 'c'}, - {'name': 'b'}, - {'name': 'd'} - ]; + // Check to see that the color is updated: + Plotly.restyle(gd, { "marker.color": "blue" }); + expect(gd._fullData[0].marker.color).toBe("blue"); - Plotly.moveTraces(gd, 1, -2); - expect(gd.data).toEqual(expectedData); - expect(PlotlyInternal.redraw).toHaveBeenCalled(); + // Check to see that the color is restored to the original default: + Plotly.restyle(gd, { "marker.color": null }); + expect(gd._fullData[0].marker.color).toBe(colorDflt); + }); - }); + it( + "can target specific traces by leaving properties undefined", + function() { + var gd = { + data: [ + { x: [1, 2, 3], y: [1, 2, 3], type: "scatter" }, + { x: [1, 2, 3], y: [3, 4, 5], type: "scatter" } + ], + layout: {} + }; + + mockDefaultsAndCalc(gd); + var colorDflt = [ + gd._fullData[0].marker.color, + gd._fullData[1].marker.color + ]; + + // Check only second trace's color has been changed: + Plotly.restyle(gd, { "marker.color": [undefined, "green"] }); + expect(gd._fullData[0].marker.color).toBe(colorDflt[0]); + expect(gd._fullData[1].marker.color).toBe("green"); + + // Check both colors restored to the original default: + Plotly.restyle(gd, { "marker.color": [null, null] }); + expect(gd._fullData[0].marker.color).toBe(colorDflt[0]); + expect(gd._fullData[1].marker.color).toBe(colorDflt[1]); + } + ); + }); + + describe("Plotly.restyle unmocked", function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); }); + afterEach(function() { + destroyGraphDiv(); + }); - describe('Plotly.ExtendTraces', function() { - var gd; + it("should redo auto z/contour when editing z array", function() { + Plotly.plot(gd, [{ type: "contour", z: [[1, 2], [3, 4]] }]) + .then(function() { + expect(gd.data[0].zauto).toBe(true, gd.data[0]); + expect(gd.data[0].zmin).toBe(1); + expect(gd.data[0].zmax).toBe(4); - beforeEach(function() { - gd = { - data: [ - {x: [0, 1, 2], marker: {size: [3, 2, 1]}}, - {x: [1, 2, 3], marker: {size: [2, 3, 4]}} - ] - }; + expect(gd.data[0].autocontour).toBe(true); + expect(gd.data[0].contours).toEqual({ + start: 1.5, + end: 3.5, + size: 0.5 + }); - if(!Plotly.Queue) { - Plotly.Queue = { - add: function() {}, - startSequence: function() {}, - endSequence: function() {} - }; - } + return Plotly.restyle(gd, { "z[0][0]": 10 }); + }) + .then(function() { + expect(gd.data[0].zmin).toBe(2); + expect(gd.data[0].zmax).toBe(10); - spyOn(PlotlyInternal, 'redraw'); - spyOn(Plotly.Queue, 'add'); + expect(gd.data[0].contours).toEqual({ start: 3, end: 9, size: 1 }); }); + }); + }); - it('should throw an error when gd.data isn\'t an array.', function() { - - expect(function() { - Plotly.extendTraces({}, {x: [[1]]}, [0]); - }).toThrow(new Error('gd.data must be an array')); + describe("Plotly.deleteTraces", function() { + var gd; - expect(function() { - Plotly.extendTraces({data: 'meow'}, {x: [[1]]}, [0]); - }).toThrow(new Error('gd.data must be an array')); + beforeEach(function() { + gd = { + data: [{ name: "a" }, { name: "b" }, { name: "c" }, { name: "d" }] + }; + spyOn(PlotlyInternal, "redraw"); + }); - }); + it("should throw an error when indices are omitted", function() { + expect(function() { + Plotly.deleteTraces(gd); + }).toThrow(new Error("indices must be an integer or array of integers.")); + }); - it('should throw an error when update is not an object', function() { + it("should throw an error when indices are out of bounds", function() { + expect(function() { + Plotly.deleteTraces(gd, 10); + }).toThrow(new Error("indices must be valid indices for gd.data.")); + }); - expect(function() { - Plotly.extendTraces(gd, undefined, [0], 8); - }).toThrow(new Error('update must be a key:value object')); + it("should throw an error when indices are repeated", function() { + expect(function() { + Plotly.deleteTraces(gd, [0, 0]); + }).toThrow(new Error("each index in indices must be unique.")); + }); - expect(function() { - Plotly.extendTraces(gd, null, [0]); - }).toThrow(new Error('update must be a key:value object')); + it("should work when indices are negative", function() { + var expectedData = [{ name: "a" }, { name: "b" }, { name: "c" }]; - }); + Plotly.deleteTraces(gd, -1); + expect(gd.data).toEqual(expectedData); + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + }); + it("should work when multiple traces are deleted", function() { + var expectedData = [{ name: "b" }, { name: "c" }]; - it('should throw an error when indices are omitted', function() { + Plotly.deleteTraces(gd, [0, 3]); + expect(gd.data).toEqual(expectedData); + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + }); - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}); - }).toThrow(new Error('indices must be an integer or array of integers')); + it("should work when indices are not sorted", function() { + var expectedData = [{ name: "b" }, { name: "c" }]; - }); + Plotly.deleteTraces(gd, [3, 0]); + expect(gd.data).toEqual(expectedData); + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + }); - it('should throw an error when a current index is out of bounds', function() { + it("should work with more than 10 indices", function() { + gd.data = []; + + for (var i = 0; i < 20; i++) { + gd.data.push({ name: "trace #" + i }); + } + + var expectedData = [ + { name: "trace #12" }, + { name: "trace #13" }, + { name: "trace #14" }, + { name: "trace #15" }, + { name: "trace #16" }, + { name: "trace #17" }, + { name: "trace #18" }, + { name: "trace #19" } + ]; + + Plotly.deleteTraces(gd, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + expect(gd.data).toEqual(expectedData); + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + }); + }); - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [-gd.data.length - 1]); - }).toThrow(new Error('indices must be valid indices for gd.data.')); + describe("Plotly.addTraces", function() { + var gd; - }); + beforeEach(function() { + gd = { data: [{ name: "a" }, { name: "b" }] }; + spyOn(PlotlyInternal, "redraw"); + spyOn(PlotlyInternal, "moveTraces"); + }); - it('should not throw an error when negative index wraps to positive', function() { + it( + "should throw an error when traces is not an object or an array of objects", + function() { + var expected = JSON.parse(JSON.stringify(gd)); + expect(function() { + Plotly.addTraces(gd, 1, 2); + }).toThrowError( + Error, + "all values in traces array must be non-array objects" + ); + + expect(function() { + Plotly.addTraces(gd, [{}, 4], 2); + }).toThrowError( + Error, + "all values in traces array must be non-array objects" + ); + + expect(function() { + Plotly.addTraces(gd, [{}, []], 2); + }).toThrowError( + Error, + "all values in traces array must be non-array objects" + ); + + // make sure we didn't muck with gd.data if things failed! + expect(gd).toEqual(expected); + } + ); + + it( + "should throw an error when traces and newIndices arrays are unequal", + function() { + expect(function() { + Plotly.addTraces(gd, [{}, {}], 2); + }).toThrowError( + Error, + "if indices is specified, traces.length must equal indices.length" + ); + } + ); + + it("should throw an error when newIndices are out of bounds", function() { + var expected = JSON.parse(JSON.stringify(gd)); + + expect(function() { + Plotly.addTraces(gd, [{}, {}], [0, 10]); + }).toThrow(new Error("newIndices must be valid indices for gd.data.")); + + // make sure we didn't muck with gd.data if things failed! + expect(gd).toEqual(expected); + }); - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [-1]); - }).not.toThrow(); + it("should work when newIndices is undefined", function() { + Plotly.addTraces(gd, [{ name: "c" }, { name: "d" }]); + expect(gd.data[2].name).toBeDefined(); + expect(gd.data[2].uid).toBeDefined(); + expect(gd.data[3].name).toBeDefined(); + expect(gd.data[3].uid).toBeDefined(); + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + expect(PlotlyInternal.moveTraces).not.toHaveBeenCalled(); + }); - }); + it("should work when newIndices is defined", function() { + Plotly.addTraces(gd, [{ name: "c" }, { name: "d" }], [1, 3]); + expect(gd.data[2].name).toBeDefined(); + expect(gd.data[2].uid).toBeDefined(); + expect(gd.data[3].name).toBeDefined(); + expect(gd.data[3].uid).toBeDefined(); + expect(PlotlyInternal.redraw).not.toHaveBeenCalled(); + expect(PlotlyInternal.moveTraces).toHaveBeenCalledWith(gd, [-2, -1], [ + 1, + 3 + ]); + }); - it('should throw an error when number of Indices does not match Update arrays', function() { + it("should work when newIndices has negative indices", function() { + Plotly.addTraces(gd, [{ name: "c" }, { name: "d" }], [-3, -1]); + expect(gd.data[2].name).toBeDefined(); + expect(gd.data[2].uid).toBeDefined(); + expect(gd.data[3].name).toBeDefined(); + expect(gd.data[3].uid).toBeDefined(); + expect(PlotlyInternal.redraw).not.toHaveBeenCalled(); + expect(PlotlyInternal.moveTraces).toHaveBeenCalledWith(gd, [-2, -1], [ + -3, + -1 + ]); + }); - expect(function() { - Plotly.extendTraces(gd, {x: [[1, 2], [2, 3]] }, [0]); - }).toThrow(new Error('attribute x must be an array of length equal to indices array length')); + it("should work when newIndices is an integer", function() { + Plotly.addTraces(gd, { name: "c" }, 0); + expect(gd.data[2].name).toBeDefined(); + expect(gd.data[2].uid).toBeDefined(); + expect(PlotlyInternal.redraw).not.toHaveBeenCalled(); + expect(PlotlyInternal.moveTraces).toHaveBeenCalledWith(gd, [-1], [0]); + }); - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [0, 1]); - }).toThrow(new Error('attribute x must be an array of length equal to indices array length')); + it("should work when adding an existing trace", function() { + Plotly.addTraces(gd, gd.data[0]); - }); + expect(gd.data.length).toEqual(3); + expect(gd.data[0]).not.toBe(gd.data[2]); + }); - it('should throw an error when maxPoints is an Object but does not match Update', function() { + it("should work when duplicating the existing data", function() { + Plotly.addTraces(gd, gd.data); - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [0], {y: [1]}); - }).toThrow(new Error('when maxPoints is set as a key:value object it must contain a 1:1 ' + - 'corrispondence with the keys and number of traces in the update object')); + expect(gd.data.length).toEqual(4); + expect(gd.data[0]).not.toBe(gd.data[2]); + expect(gd.data[1]).not.toBe(gd.data[3]); + }); + }); + + describe("Plotly.moveTraces should", function() { + var gd; + beforeEach(function() { + gd = { + data: [{ name: "a" }, { name: "b" }, { name: "c" }, { name: "d" }] + }; + spyOn(PlotlyInternal, "redraw"); + }); - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [0], {x: [1, 2]}); - }).toThrow(new Error('when maxPoints is set as a key:value object it must contain a 1:1 ' + - 'corrispondence with the keys and number of traces in the update object')); + it("throw an error when index arrays are unequal", function() { + expect(function() { + Plotly.moveTraces(gd, [1], [2, 1]); + }).toThrow(new Error("current and new indices must be of equal length.")); + }); - }); + it("throw an error when gd.data isn't an array.", function() { + expect(function() { + Plotly.moveTraces({}, [0], [0]); + }).toThrow(new Error("gd.data must be an array.")); + expect(function() { + Plotly.moveTraces({ data: "meow" }, [0], [0]); + }).toThrow(new Error("gd.data must be an array.")); + }); - it('should throw an error when update keys mismatch trace keys', function() { + it("thow an error when a current index is out of bounds", function() { + expect(function() { + Plotly.moveTraces(gd, [-gd.data.length - 1], [0]); + }).toThrow(new Error( + "currentIndices must be valid indices for gd.data." + )); + expect(function() { + Plotly.moveTraces(gd, [gd.data.length], [0]); + }).toThrow(new Error( + "currentIndices must be valid indices for gd.data." + )); + }); - // lets update y on both traces, but only 1 trace has "y" - gd.data[1].y = [1, 2, 3]; + it("thow an error when a new index is out of bounds", function() { + expect(function() { + Plotly.moveTraces(gd, [0], [-gd.data.length - 1]); + }).toThrow(new Error("newIndices must be valid indices for gd.data.")); + expect(function() { + Plotly.moveTraces(gd, [0], [gd.data.length]); + }).toThrow(new Error("newIndices must be valid indices for gd.data.")); + }); - expect(function() { - Plotly.extendTraces(gd, { - y: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1]); - }).toThrow(new Error('cannot extend missing or non-array attribute: y')); + it("thow an error when current indices are repeated", function() { + expect(function() { + Plotly.moveTraces(gd, [0, 0], [0, 1]); + }).toThrow(new Error("each index in currentIndices must be unique.")); - }); + // note that both positive and negative indices are accepted! + expect(function() { + Plotly.moveTraces(gd, [0, -gd.data.length], [0, 1]); + }).toThrow(new Error("each index in currentIndices must be unique.")); + }); - it('should extend traces with update keys', function() { + it("thow an error when new indices are repeated", function() { + expect(function() { + Plotly.moveTraces(gd, [0, 1], [0, 0]); + }).toThrow(new Error("each index in newIndices must be unique.")); - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1]); + // note that both positive and negative indices are accepted! + expect(function() { + Plotly.moveTraces(gd, [0, 1], [-gd.data.length, 0]); + }).toThrow(new Error("each index in newIndices must be unique.")); + }); - expect(gd.data).toEqual([ - {x: [0, 1, 2, 3, 4], marker: {size: [3, 2, 1, 0, -1]}}, - {x: [1, 2, 3, 4, 5], marker: {size: [2, 3, 4, 5, 6]}} - ]); + it("accept integers in place of arrays", function() { + var expectedData = [ + { name: "b" }, + { name: "a" }, + { name: "c" }, + { name: "d" } + ]; + + Plotly.moveTraces(gd, 0, 1); + expect(gd.data).toEqual(expectedData); + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + }); - expect(PlotlyInternal.redraw).toHaveBeenCalled(); - }); + it("handle unsorted currentIndices", function() { + var expectedData = [ + { name: "d" }, + { name: "a" }, + { name: "c" }, + { name: "b" } + ]; + + Plotly.moveTraces(gd, [3, 1], [0, 3]); + expect(gd.data).toEqual(expectedData); + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + }); - it('should extend and window traces with update keys', function() { - var maxPoints = 3; + it("work when newIndices are undefined.", function() { + var expectedData = [ + { name: "b" }, + { name: "c" }, + { name: "d" }, + { name: "a" } + ]; + + Plotly.moveTraces(gd, [3, 0]); + expect(gd.data).toEqual(expectedData); + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + }); - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1], maxPoints); + it("accept negative indices.", function() { + var expectedData = [ + { name: "a" }, + { name: "c" }, + { name: "b" }, + { name: "d" } + ]; + + Plotly.moveTraces(gd, 1, -2); + expect(gd.data).toEqual(expectedData); + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + }); + }); + + describe("Plotly.ExtendTraces", function() { + var gd; + + beforeEach(function() { + gd = { + data: [ + { x: [0, 1, 2], marker: { size: [3, 2, 1] } }, + { x: [1, 2, 3], marker: { size: [2, 3, 4] } } + ] + }; + + if (!Plotly.Queue) { + Plotly.Queue = { + add: function() {}, + startSequence: function() {}, + endSequence: function() {} + }; + } + + spyOn(PlotlyInternal, "redraw"); + spyOn(Plotly.Queue, "add"); + }); - expect(gd.data).toEqual([ - {x: [2, 3, 4], marker: {size: [1, 0, -1]}}, - {x: [3, 4, 5], marker: {size: [4, 5, 6]}} - ]); - }); + it("should throw an error when gd.data isn't an array.", function() { + expect(function() { + Plotly.extendTraces({}, { x: [[1]] }, [0]); + }).toThrow(new Error("gd.data must be an array")); - it('should extend and window traces with update keys', function() { - var maxPoints = 3; + expect(function() { + Plotly.extendTraces({ data: "meow" }, { x: [[1]] }, [0]); + }).toThrow(new Error("gd.data must be an array")); + }); - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1], maxPoints); + it("should throw an error when update is not an object", function() { + expect(function() { + Plotly.extendTraces(gd, undefined, [0], 8); + }).toThrow(new Error("update must be a key:value object")); - expect(gd.data).toEqual([ - {x: [2, 3, 4], marker: {size: [1, 0, -1]}}, - {x: [3, 4, 5], marker: {size: [4, 5, 6]}} - ]); - }); + expect(function() { + Plotly.extendTraces(gd, null, [0]); + }).toThrow(new Error("update must be a key:value object")); + }); - it('should extend and window traces using full maxPoint object', function() { - var maxPoints = {x: [2, 3], 'marker.size': [1, 2]}; + it("should throw an error when indices are omitted", function() { + expect(function() { + Plotly.extendTraces(gd, { x: [[1]] }); + }).toThrow(new Error("indices must be an integer or array of integers")); + }); - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1], maxPoints); + it( + "should throw an error when a current index is out of bounds", + function() { + expect(function() { + Plotly.extendTraces(gd, { x: [[1]] }, [-gd.data.length - 1]); + }).toThrow(new Error("indices must be valid indices for gd.data.")); + } + ); + + it( + "should not throw an error when negative index wraps to positive", + function() { + expect(function() { + Plotly.extendTraces(gd, { x: [[1]] }, [-1]); + }).not.toThrow(); + } + ); + + it( + "should throw an error when number of Indices does not match Update arrays", + function() { + expect(function() { + Plotly.extendTraces(gd, { x: [[1, 2], [2, 3]] }, [0]); + }).toThrow(new Error( + "attribute x must be an array of length equal to indices array length" + )); + + expect(function() { + Plotly.extendTraces(gd, { x: [[1]] }, [0, 1]); + }).toThrow(new Error( + "attribute x must be an array of length equal to indices array length" + )); + } + ); + + it( + "should throw an error when maxPoints is an Object but does not match Update", + function() { + expect(function() { + Plotly.extendTraces(gd, { x: [[1]] }, [0], { y: [1] }); + }).toThrow(new Error( + "when maxPoints is set as a key:value object it must contain a 1:1 " + + "corrispondence with the keys and number of traces in the update object" + )); + + expect(function() { + Plotly.extendTraces(gd, { x: [[1]] }, [0], { x: [1, 2] }); + }).toThrow(new Error( + "when maxPoints is set as a key:value object it must contain a 1:1 " + + "corrispondence with the keys and number of traces in the update object" + )); + } + ); + + it( + "should throw an error when update keys mismatch trace keys", + function() { + // lets update y on both traces, but only 1 trace has "y" + gd.data[1].y = [1, 2, 3]; + + expect(function() { + Plotly.extendTraces( + gd, + { + y: [[3, 4], [4, 5]], + "marker.size": [[0, -1], [5, 6]] + }, + [0, 1] + ); + }).toThrow(new Error( + "cannot extend missing or non-array attribute: y" + )); + } + ); + + it("should extend traces with update keys", function() { + Plotly.extendTraces( + gd, + { x: [[3, 4], [4, 5]], "marker.size": [[0, -1], [5, 6]] }, + [0, 1] + ); + + expect(gd.data).toEqual([ + { x: [0, 1, 2, 3, 4], marker: { size: [3, 2, 1, 0, -1] } }, + { x: [1, 2, 3, 4, 5], marker: { size: [2, 3, 4, 5, 6] } } + ]); + + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + }); - expect(gd.data).toEqual([ - {x: [3, 4], marker: {size: [-1]}}, - {x: [3, 4, 5], marker: {size: [5, 6]}} - ]); - }); + it("should extend and window traces with update keys", function() { + var maxPoints = 3; - it('should truncate arrays when maxPoints is zero', function() { + Plotly.extendTraces( + gd, + { x: [[3, 4], [4, 5]], "marker.size": [[0, -1], [5, 6]] }, + [0, 1], + maxPoints + ); - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1], 0); + expect(gd.data).toEqual([ + { x: [2, 3, 4], marker: { size: [1, 0, -1] } }, + { x: [3, 4, 5], marker: { size: [4, 5, 6] } } + ]); + }); - expect(gd.data).toEqual([ - {x: [], marker: {size: []}}, - {x: [], marker: {size: []}} - ]); + it("should extend and window traces with update keys", function() { + var maxPoints = 3; - expect(PlotlyInternal.redraw).toHaveBeenCalled(); - }); + Plotly.extendTraces( + gd, + { x: [[3, 4], [4, 5]], "marker.size": [[0, -1], [5, 6]] }, + [0, 1], + maxPoints + ); - it('prepend is the inverse of extend - no maxPoints', function() { - var cachedData = Lib.extendDeep([], gd.data); + expect(gd.data).toEqual([ + { x: [2, 3, 4], marker: { size: [1, 0, -1] } }, + { x: [3, 4, 5], marker: { size: [4, 5, 6] } } + ]); + }); - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1]); + it( + "should extend and window traces using full maxPoint object", + function() { + var maxPoints = { x: [2, 3], "marker.size": [1, 2] }; + + Plotly.extendTraces( + gd, + { x: [[3, 4], [4, 5]], "marker.size": [[0, -1], [5, 6]] }, + [0, 1], + maxPoints + ); + + expect(gd.data).toEqual([ + { x: [3, 4], marker: { size: [-1] } }, + { x: [3, 4, 5], marker: { size: [5, 6] } } + ]); + } + ); + + it("should truncate arrays when maxPoints is zero", function() { + Plotly.extendTraces( + gd, + { x: [[3, 4], [4, 5]], "marker.size": [[0, -1], [5, 6]] }, + [0, 1], + 0 + ); + + expect(gd.data).toEqual([ + { x: [], marker: { size: [] } }, + { x: [], marker: { size: [] } } + ]); + + expect(PlotlyInternal.redraw).toHaveBeenCalled(); + }); - expect(gd.data).not.toEqual(cachedData); - expect(Plotly.Queue.add).toHaveBeenCalled(); + it("prepend is the inverse of extend - no maxPoints", function() { + var cachedData = Lib.extendDeep([], gd.data); - var undoArgs = Plotly.Queue.add.calls.first().args[2]; + Plotly.extendTraces( + gd, + { x: [[3, 4], [4, 5]], "marker.size": [[0, -1], [5, 6]] }, + [0, 1] + ); - Plotly.prependTraces.apply(null, undoArgs); + expect(gd.data).not.toEqual(cachedData); + expect(Plotly.Queue.add).toHaveBeenCalled(); - expect(gd.data).toEqual(cachedData); - }); + var undoArgs = Plotly.Queue.add.calls.first().args[2]; + Plotly.prependTraces.apply(null, undoArgs); - it('extend is the inverse of prepend - no maxPoints', function() { - var cachedData = Lib.extendDeep([], gd.data); + expect(gd.data).toEqual(cachedData); + }); - Plotly.prependTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1]); + it("extend is the inverse of prepend - no maxPoints", function() { + var cachedData = Lib.extendDeep([], gd.data); - expect(gd.data).not.toEqual(cachedData); - expect(Plotly.Queue.add).toHaveBeenCalled(); + Plotly.prependTraces( + gd, + { x: [[3, 4], [4, 5]], "marker.size": [[0, -1], [5, 6]] }, + [0, 1] + ); - var undoArgs = Plotly.Queue.add.calls.first().args[2]; + expect(gd.data).not.toEqual(cachedData); + expect(Plotly.Queue.add).toHaveBeenCalled(); - Plotly.extendTraces.apply(null, undoArgs); + var undoArgs = Plotly.Queue.add.calls.first().args[2]; - expect(gd.data).toEqual(cachedData); - }); + Plotly.extendTraces.apply(null, undoArgs); + expect(gd.data).toEqual(cachedData); + }); - it('prepend is the inverse of extend - with maxPoints', function() { - var maxPoints = 3; - var cachedData = Lib.extendDeep([], gd.data); + it("prepend is the inverse of extend - with maxPoints", function() { + var maxPoints = 3; + var cachedData = Lib.extendDeep([], gd.data); - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1], maxPoints); + Plotly.extendTraces( + gd, + { x: [[3, 4], [4, 5]], "marker.size": [[0, -1], [5, 6]] }, + [0, 1], + maxPoints + ); - expect(gd.data).not.toEqual(cachedData); - expect(Plotly.Queue.add).toHaveBeenCalled(); + expect(gd.data).not.toEqual(cachedData); + expect(Plotly.Queue.add).toHaveBeenCalled(); - var undoArgs = Plotly.Queue.add.calls.first().args[2]; + var undoArgs = Plotly.Queue.add.calls.first().args[2]; - Plotly.prependTraces.apply(null, undoArgs); + Plotly.prependTraces.apply(null, undoArgs); - expect(gd.data).toEqual(cachedData); - }); + expect(gd.data).toEqual(cachedData); }); + }); - describe('Plotly.purge', function() { - - afterEach(destroyGraphDiv); + describe("Plotly.purge", function() { + afterEach(destroyGraphDiv); - it('should return the graph div in its original state', function(done) { - var gd = createGraphDiv(); - var initialKeys = Object.keys(gd); - var intialHTML = gd.innerHTML; - var mockData = [{ x: [1, 2, 3], y: [2, 3, 4] }]; + it("should return the graph div in its original state", function(done) { + var gd = createGraphDiv(); + var initialKeys = Object.keys(gd); + var intialHTML = gd.innerHTML; + var mockData = [{ x: [1, 2, 3], y: [2, 3, 4] }]; - Plotly.plot(gd, mockData).then(function() { - Plotly.purge(gd); + Plotly.plot(gd, mockData).then(function() { + Plotly.purge(gd); - expect(Object.keys(gd)).toEqual(initialKeys); - expect(gd.innerHTML).toEqual(intialHTML); + expect(Object.keys(gd)).toEqual(initialKeys); + expect(gd.innerHTML).toEqual(intialHTML); - done(); - }); - }); + done(); + }); }); - - describe('Plotly.redraw', function() { - - afterEach(destroyGraphDiv); - - it('', function(done) { - var gd = createGraphDiv(), - initialData = [], - layout = { title: 'Redraw' }; - - Plotly.newPlot(gd, initialData, layout); - - var trace1 = { - x: [1, 2, 3, 4], - y: [4, 1, 5, 3], - name: 'First Trace' - }; - var trace2 = { - x: [1, 2, 3, 4], - y: [14, 11, 15, 13], - name: 'Second Trace' - }; - var trace3 = { - x: [1, 2, 3, 4], - y: [5, 3, 7, 1], - name: 'Third Trace' - }; - - var newData = [trace1, trace2, trace3]; - gd.data = newData; - - Plotly.redraw(gd).then(function() { - expect(d3.selectAll('g.trace.scatter').size()).toEqual(3); - }) - .then(done); - }); + }); + + describe("Plotly.redraw", function() { + afterEach(destroyGraphDiv); + + it("", function(done) { + var gd = createGraphDiv(), initialData = [], layout = { title: "Redraw" }; + + Plotly.newPlot(gd, initialData, layout); + + var trace1 = { + x: [1, 2, 3, 4], + y: [4, 1, 5, 3], + name: "First Trace" + }; + var trace2 = { + x: [1, 2, 3, 4], + y: [14, 11, 15, 13], + name: "Second Trace" + }; + var trace3 = { + x: [1, 2, 3, 4], + y: [5, 3, 7, 1], + name: "Third Trace" + }; + + var newData = [trace1, trace2, trace3]; + gd.data = newData; + + Plotly.redraw(gd) + .then(function() { + expect(d3.selectAll("g.trace.scatter").size()).toEqual(3); + }) + .then(done); }); + }); - describe('cleanData & cleanLayout', function() { - var gd; + describe("cleanData & cleanLayout", function() { + var gd; - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - it('should rename \'YIGnBu\' colorscales YlGnBu (2dMap case)', function() { - var data = [{ - type: 'heatmap', - colorscale: 'YIGnBu' - }]; - - Plotly.plot(gd, data); - expect(gd.data[0].colorscale).toBe('YlGnBu'); - }); - - it('should rename \'YIGnBu\' colorscales YlGnBu (markerColorscale case)', function() { - var data = [{ - type: 'scattergeo', - marker: { colorscale: 'YIGnBu' } - }]; - - Plotly.plot(gd, data); - expect(gd.data[0].marker.colorscale).toBe('YlGnBu'); - }); + beforeEach(function() { + gd = createGraphDiv(); + }); - it('should rename \'YIOrRd\' colorscales YlOrRd (2dMap case)', function() { - var data = [{ - type: 'contour', - colorscale: 'YIOrRd' - }]; + afterEach(destroyGraphDiv); - Plotly.plot(gd, data); - expect(gd.data[0].colorscale).toBe('YlOrRd'); - }); + it("should rename 'YIGnBu' colorscales YlGnBu (2dMap case)", function() { + var data = [{ type: "heatmap", colorscale: "YIGnBu" }]; - it('should rename \'YIOrRd\' colorscales YlOrRd (markerColorscale case)', function() { - var data = [{ - type: 'scattergeo', - marker: { colorscale: 'YIOrRd' } - }]; + Plotly.plot(gd, data); + expect(gd.data[0].colorscale).toBe("YlGnBu"); + }); - Plotly.plot(gd, data); - expect(gd.data[0].marker.colorscale).toBe('YlOrRd'); - }); + it( + "should rename 'YIGnBu' colorscales YlGnBu (markerColorscale case)", + function() { + var data = [{ type: "scattergeo", marker: { colorscale: "YIGnBu" } }]; - it('should rename \'highlightColor\' to \'highlightcolor\')', function() { - var data = [{ - type: 'surface', - contours: { - x: { highlightColor: 'red' }, - y: { highlightcolor: 'blue' } - } - }, { - type: 'surface' - }, { - type: 'surface', - contours: false - }, { - type: 'surface', - contours: { - stuff: {}, - x: false, - y: [] - } - }]; - - spyOn(Plots.subplotsRegistry.gl3d, 'plot'); - - Plotly.plot(gd, data); - - expect(Plots.subplotsRegistry.gl3d.plot).toHaveBeenCalled(); - - var contours = gd.data[0].contours; - - expect(contours.x.highlightColor).toBeUndefined(); - expect(contours.x.highlightcolor).toEqual('red'); - expect(contours.y.highlightcolor).toEqual('blue'); - expect(contours.z).toBeUndefined(); - - expect(gd.data[1].contours).toBeUndefined(); - expect(gd.data[2].contours).toBe(false); - expect(gd.data[3].contours).toEqual({ stuff: {}, x: false, y: [] }); - }); + Plotly.plot(gd, data); + expect(gd.data[0].marker.colorscale).toBe("YlGnBu"); + } + ); - it('should rename \'highlightWidth\' to \'highlightwidth\')', function() { - var data = [{ - type: 'surface', - contours: { - z: { highlightwidth: 'red' }, - y: { highlightWidth: 'blue' } - } - }, { - type: 'surface' - }]; + it("should rename 'YIOrRd' colorscales YlOrRd (2dMap case)", function() { + var data = [{ type: "contour", colorscale: "YIOrRd" }]; - spyOn(Plots.subplotsRegistry.gl3d, 'plot'); + Plotly.plot(gd, data); + expect(gd.data[0].colorscale).toBe("YlOrRd"); + }); - Plotly.plot(gd, data); + it( + "should rename 'YIOrRd' colorscales YlOrRd (markerColorscale case)", + function() { + var data = [{ type: "scattergeo", marker: { colorscale: "YIOrRd" } }]; + + Plotly.plot(gd, data); + expect(gd.data[0].marker.colorscale).toBe("YlOrRd"); + } + ); + + it("should rename 'highlightColor' to 'highlightcolor')", function() { + var data = [ + { + type: "surface", + contours: { + x: { highlightColor: "red" }, + y: { highlightcolor: "blue" } + } + }, + { type: "surface" }, + { type: "surface", contours: false }, + { type: "surface", contours: { stuff: {}, x: false, y: [] } } + ]; + + spyOn(Plots.subplotsRegistry.gl3d, "plot"); + + Plotly.plot(gd, data); + + expect(Plots.subplotsRegistry.gl3d.plot).toHaveBeenCalled(); + + var contours = gd.data[0].contours; + + expect(contours.x.highlightColor).toBeUndefined(); + expect(contours.x.highlightcolor).toEqual("red"); + expect(contours.y.highlightcolor).toEqual("blue"); + expect(contours.z).toBeUndefined(); + + expect(gd.data[1].contours).toBeUndefined(); + expect(gd.data[2].contours).toBe(false); + expect(gd.data[3].contours).toEqual({ stuff: {}, x: false, y: [] }); + }); - expect(Plots.subplotsRegistry.gl3d.plot).toHaveBeenCalled(); + it("should rename 'highlightWidth' to 'highlightwidth')", function() { + var data = [ + { + type: "surface", + contours: { + z: { highlightwidth: "red" }, + y: { highlightWidth: "blue" } + } + }, + { type: "surface" } + ]; - var contours = gd.data[0].contours; + spyOn(Plots.subplotsRegistry.gl3d, "plot"); - expect(contours.x).toBeUndefined(); - expect(contours.y.highlightwidth).toEqual('blue'); - expect(contours.z.highlightWidth).toBeUndefined(); - expect(contours.z.highlightwidth).toEqual('red'); + Plotly.plot(gd, data); - expect(gd.data[1].contours).toBeUndefined(); - }); + expect(Plots.subplotsRegistry.gl3d.plot).toHaveBeenCalled(); - it('should rename *filtersrc* to *target* in filter transforms', function() { - var data = [{ - transforms: [{ - type: 'filter', - filtersrc: 'y' - }, { - type: 'filter', - operation: '<' - }] - }, { - transforms: [{ - type: 'filter', - target: 'y' - }] - }]; - - Plotly.plot(gd, data); - - var trace0 = gd.data[0], - trace1 = gd.data[1]; - - expect(trace0.transforms.length).toEqual(2); - expect(trace0.transforms[0].filtersrc).toBeUndefined(); - expect(trace0.transforms[0].target).toEqual('y'); - - expect(trace1.transforms.length).toEqual(1); - expect(trace1.transforms[0].target).toEqual('y'); - }); - - it('should rename *calendar* to *valuecalendar* in filter transforms', function() { - var data = [{ - transforms: [{ - type: 'filter', - target: 'y', - calendar: 'hebrew' - }, { - type: 'filter', - operation: '<' - }] - }, { - transforms: [{ - type: 'filter', - valuecalendar: 'jalali' - }] - }]; - - Plotly.plot(gd, data); - - var trace0 = gd.data[0], - trace1 = gd.data[1]; - - expect(trace0.transforms.length).toEqual(2); - expect(trace0.transforms[0].calendar).toBeUndefined(); - expect(trace0.transforms[0].valuecalendar).toEqual('hebrew'); - - expect(trace1.transforms.length).toEqual(1); - expect(trace1.transforms[0].valuecalendar).toEqual('jalali'); - }); + var contours = gd.data[0].contours; - it('should cleanup annotations / shapes refs', function() { - var data = [{}]; - - var layout = { - annotations: [ - { ref: 'paper' }, - null, - { xref: 'x02', yref: 'y1' } - ], - shapes: [ - { xref: 'y', yref: 'x' }, - null, - { xref: 'x03', yref: 'y1' } - ] - }; - - Plotly.plot(gd, data, layout); - - expect(gd.layout.annotations[0]).toEqual({ xref: 'paper', yref: 'paper' }); - expect(gd.layout.annotations[1]).toEqual(null); - expect(gd.layout.annotations[2]).toEqual({ xref: 'x2', yref: 'y' }); - - expect(gd.layout.shapes[0].xref).toBeUndefined(); - expect(gd.layout.shapes[0].yref).toBeUndefined(); - expect(gd.layout.shapes[1]).toEqual(null); - expect(gd.layout.shapes[2].xref).toEqual('x3'); - expect(gd.layout.shapes[2].yref).toEqual('y'); + expect(contours.x).toBeUndefined(); + expect(contours.y.highlightwidth).toEqual("blue"); + expect(contours.z.highlightWidth).toBeUndefined(); + expect(contours.z.highlightwidth).toEqual("red"); - }); + expect(gd.data[1].contours).toBeUndefined(); }); - describe('Plotly.newPlot', function() { - var gd; + it( + "should rename *filtersrc* to *target* in filter transforms", + function() { + var data = [ + { + transforms: [ + { type: "filter", filtersrc: "y" }, + { type: "filter", operation: "<" } + ] + }, + { transforms: [{ type: "filter", target: "y" }] } + ]; + + Plotly.plot(gd, data); + + var trace0 = gd.data[0], trace1 = gd.data[1]; + + expect(trace0.transforms.length).toEqual(2); + expect(trace0.transforms[0].filtersrc).toBeUndefined(); + expect(trace0.transforms[0].target).toEqual("y"); + + expect(trace1.transforms.length).toEqual(1); + expect(trace1.transforms[0].target).toEqual("y"); + } + ); + + it( + "should rename *calendar* to *valuecalendar* in filter transforms", + function() { + var data = [ + { + transforms: [ + { type: "filter", target: "y", calendar: "hebrew" }, + { type: "filter", operation: "<" } + ] + }, + { transforms: [{ type: "filter", valuecalendar: "jalali" }] } + ]; + + Plotly.plot(gd, data); + + var trace0 = gd.data[0], trace1 = gd.data[1]; + + expect(trace0.transforms.length).toEqual(2); + expect(trace0.transforms[0].calendar).toBeUndefined(); + expect(trace0.transforms[0].valuecalendar).toEqual("hebrew"); + + expect(trace1.transforms.length).toEqual(1); + expect(trace1.transforms[0].valuecalendar).toEqual("jalali"); + } + ); + + it("should cleanup annotations / shapes refs", function() { + var data = [{}]; + + var layout = { + annotations: [{ ref: "paper" }, null, { xref: "x02", yref: "y1" }], + shapes: [{ xref: "y", yref: "x" }, null, { xref: "x03", yref: "y1" }] + }; + + Plotly.plot(gd, data, layout); + + expect(gd.layout.annotations[0]).toEqual({ + xref: "paper", + yref: "paper" + }); + expect(gd.layout.annotations[1]).toEqual(null); + expect(gd.layout.annotations[2]).toEqual({ xref: "x2", yref: "y" }); + + expect(gd.layout.shapes[0].xref).toBeUndefined(); + expect(gd.layout.shapes[0].yref).toBeUndefined(); + expect(gd.layout.shapes[1]).toEqual(null); + expect(gd.layout.shapes[2].xref).toEqual("x3"); + expect(gd.layout.shapes[2].yref).toEqual("y"); + }); + }); - beforeEach(function() { - gd = createGraphDiv(); - }); + describe("Plotly.newPlot", function() { + var gd; - afterEach(destroyGraphDiv); + beforeEach(function() { + gd = createGraphDiv(); + }); - it('should respect layout.width and layout.height', function(done) { + afterEach(destroyGraphDiv); - // See issue https://github.com/plotly/plotly.js/issues/537 - var data = [{ - x: [1, 2], - y: [1, 2] - }]; + it("should respect layout.width and layout.height", function(done) { + // See issue https://github.com/plotly/plotly.js/issues/537 + var data = [{ x: [1, 2], y: [1, 2] }]; - Plotly.plot(gd, data).then(function() { - var height = 50; + Plotly.plot(gd, data).then(function() { + var height = 50; - Plotly.newPlot(gd, data, { height: height }).then(function() { - var fullLayout = gd._fullLayout, - svg = document.getElementsByClassName('main-svg')[0]; + Plotly.newPlot(gd, data, { height: height }) + .then(function() { + var fullLayout = gd._fullLayout, + svg = document.getElementsByClassName("main-svg")[0]; - expect(fullLayout.height).toBe(height); - expect(+svg.getAttribute('height')).toBe(height); - }).then(done); - }); - }); + expect(fullLayout.height).toBe(height); + expect(+svg.getAttribute("height")).toBe(height); + }) + .then(done); + }); }); + }); - describe('Plotly.update should', function() { - var gd, data, layout, calcdata; + describe("Plotly.update should", function() { + var gd, data, layout, calcdata; - beforeAll(function() { - Object.keys(subroutines).forEach(function(k) { - spyOn(subroutines, k).and.callThrough(); - }); - }); + beforeAll(function() { + Object.keys(subroutines).forEach(function(k) { + spyOn(subroutines, k).and.callThrough(); + }); + }); - beforeEach(function(done) { - gd = createGraphDiv(); - Plotly.plot(gd, [{ y: [2, 1, 2] }]).then(function() { - data = gd.data; - layout = gd.layout; - calcdata = gd.calcdata; - done(); - }); - }); + beforeEach(function(done) { + gd = createGraphDiv(); + Plotly.plot(gd, [{ y: [2, 1, 2] }]).then(function() { + data = gd.data; + layout = gd.layout; + calcdata = gd.calcdata; + done(); + }); + }); - afterEach(destroyGraphDiv); + afterEach(destroyGraphDiv); - it('call doTraceStyle on trace style updates', function(done) { - expect(subroutines.doTraceStyle).not.toHaveBeenCalled(); + it("call doTraceStyle on trace style updates", function(done) { + expect(subroutines.doTraceStyle).not.toHaveBeenCalled(); - Plotly.update(gd, { 'marker.color': 'blue' }).then(function() { - expect(subroutines.doTraceStyle).toHaveBeenCalledTimes(1); - expect(calcdata).toBe(gd.calcdata); - done(); - }); - }); + Plotly.update(gd, { "marker.color": "blue" }).then(function() { + expect(subroutines.doTraceStyle).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + done(); + }); + }); - it('clear calcdata on data updates', function(done) { - Plotly.update(gd, { x: [[3, 1, 3]] }).then(function() { - expect(data).toBe(gd.data); - expect(layout).toBe(gd.layout); - expect(calcdata).not.toBe(gd.calcdata); - done(); - }); - }); + it("clear calcdata on data updates", function(done) { + Plotly.update(gd, { x: [[3, 1, 3]] }).then(function() { + expect(data).toBe(gd.data); + expect(layout).toBe(gd.layout); + expect(calcdata).not.toBe(gd.calcdata); + done(); + }); + }); - it('clear calcdata on data + axis updates w/o extending current gd.data', function(done) { - var traceUpdate = { - x: [[3, 1, 3]] - }; + it( + "clear calcdata on data + axis updates w/o extending current gd.data", + function(done) { + var traceUpdate = { x: [[3, 1, 3]] }; - var layoutUpdate = { - xaxis: {title: 'A', type: '-'} - }; + var layoutUpdate = { xaxis: { title: "A", type: "-" } }; - Plotly.update(gd, traceUpdate, layoutUpdate).then(function() { - expect(data).toBe(gd.data); - expect(layout).toBe(gd.layout); - expect(calcdata).not.toBe(gd.calcdata); + Plotly.update(gd, traceUpdate, layoutUpdate).then(function() { + expect(data).toBe(gd.data); + expect(layout).toBe(gd.layout); + expect(calcdata).not.toBe(gd.calcdata); - expect(gd.data.length).toEqual(1); + expect(gd.data.length).toEqual(1); - done(); - }); + done(); }); + } + ); - it('call doLegend on legend updates', function(done) { - expect(subroutines.doLegend).not.toHaveBeenCalled(); + it("call doLegend on legend updates", function(done) { + expect(subroutines.doLegend).not.toHaveBeenCalled(); - Plotly.update(gd, {}, { 'showlegend': true }).then(function() { - expect(subroutines.doLegend).toHaveBeenCalledTimes(1); - expect(calcdata).toBe(gd.calcdata); - done(); - }); - }); + Plotly.update(gd, {}, { showlegend: true }).then(function() { + expect(subroutines.doLegend).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + done(); + }); + }); - it('call layoutReplot when adding update menu', function(done) { - expect(subroutines.layoutReplot).not.toHaveBeenCalled(); - - var layoutUpdate = { - updatemenus: [{ - buttons: [{ - method: 'relayout', - args: ['title', 'Hello World'] - }] - }] - }; - - Plotly.update(gd, {}, layoutUpdate).then(function() { - expect(subroutines.doLegend).toHaveBeenCalledTimes(1); - expect(calcdata).toBe(gd.calcdata); - done(); - }); - }); + it("call layoutReplot when adding update menu", function(done) { + expect(subroutines.layoutReplot).not.toHaveBeenCalled(); + + var layoutUpdate = { + updatemenus: [ + { + buttons: [{ method: "relayout", args: ["title", "Hello World"] }] + } + ] + }; + + Plotly.update(gd, {}, layoutUpdate).then(function() { + expect(subroutines.doLegend).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + done(); + }); + }); - it('call doModeBar when updating \'dragmode\'', function(done) { - expect(subroutines.doModeBar).not.toHaveBeenCalled(); + it("call doModeBar when updating 'dragmode'", function(done) { + expect(subroutines.doModeBar).not.toHaveBeenCalled(); - Plotly.update(gd, {}, { 'dragmode': 'pan' }).then(function() { - expect(subroutines.doModeBar).toHaveBeenCalledTimes(1); - expect(calcdata).toBe(gd.calcdata); - done(); - }); - }); + Plotly.update(gd, {}, { dragmode: "pan" }).then(function() { + expect(subroutines.doModeBar).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + done(); + }); }); + }); }); diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index d6a0efda994..c7335b61adb 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -1,675 +1,721 @@ -var d3 = require('d3'); +var d3 = require("d3"); -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var customMatchers = require('../assets/custom_matchers'); -var mouseEvent = require('../assets/mouse_event'); -var selectButton = require('../assets/modebar_button'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var customMatchers = require("../assets/custom_matchers"); +var mouseEvent = require("../assets/mouse_event"); +var selectButton = require("../assets/modebar_button"); var MODEBAR_DELAY = 500; -describe('Test plot structure', function() { - 'use strict'; +describe("Test plot structure", function() { + "use strict"; + function assertNamespaces(node) { + expect(node.getAttribute("xmlns")).toEqual("http://www.w3.org/2000/svg"); + expect(node.getAttribute("xmlns:xlink")).toEqual( + "http://www.w3.org/1999/xlink" + ); + } - function assertNamespaces(node) { - expect(node.getAttribute('xmlns')) - .toEqual('http://www.w3.org/2000/svg'); - expect(node.getAttribute('xmlns:xlink')) - .toEqual('http://www.w3.org/1999/xlink'); - } - - afterEach(destroyGraphDiv); - - describe('cartesian plots', function() { - - function countSubplots() { - return d3.selectAll('g.subplot').size(); - } + afterEach(destroyGraphDiv); - function countScatterTraces() { - return d3.selectAll('g.trace.scatter').size(); - } + describe("cartesian plots", function() { + function countSubplots() { + return d3.selectAll("g.subplot").size(); + } - function countColorBars() { - return d3.selectAll('rect.cbbg').size(); - } + function countScatterTraces() { + return d3.selectAll("g.trace.scatter").size(); + } - function countClipPaths() { - return d3.selectAll('defs').selectAll('.axesclip,.plotclip').size(); - } + function countColorBars() { + return d3.selectAll("rect.cbbg").size(); + } - function countDraggers() { - return d3.selectAll('g.draglayer').selectAll('g').size(); - } + function countClipPaths() { + return d3.selectAll("defs").selectAll(".axesclip,.plotclip").size(); + } - describe('scatter traces', function() { - var mock = require('@mocks/14.json'); - var gd; + function countDraggers() { + return d3.selectAll("g.draglayer").selectAll("g").size(); + } - beforeEach(function(done) { - gd = createGraphDiv(); + describe("scatter traces", function() { + var mock = require("@mocks/14.json"); + var gd; - var mockData = Lib.extendDeep([], mock.data), - mockLayout = Lib.extendDeep({}, mock.layout); + beforeEach(function(done) { + gd = createGraphDiv(); - Plotly.plot(gd, mockData, mockLayout).then(done); - }); + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); - it('has one *subplot xy* node', function() { - expect(countSubplots()).toEqual(1); - }); + Plotly.plot(gd, mockData, mockLayout).then(done); + }); - it('has four clip paths', function() { - expect(countClipPaths()).toEqual(4); - }); + it("has one *subplot xy* node", function() { + expect(countSubplots()).toEqual(1); + }); - it('has one dragger group', function() { - expect(countDraggers()).toEqual(1); - }); + it("has four clip paths", function() { + expect(countClipPaths()).toEqual(4); + }); - it('has one *scatterlayer* node', function() { - var nodes = d3.selectAll('g.scatterlayer'); - expect(nodes.size()).toEqual(1); - }); + it("has one dragger group", function() { + expect(countDraggers()).toEqual(1); + }); - it('has as many *trace scatter* nodes as there are traces', function() { - expect(countScatterTraces()).toEqual(mock.data.length); - }); + it("has one *scatterlayer* node", function() { + var nodes = d3.selectAll("g.scatterlayer"); + expect(nodes.size()).toEqual(1); + }); - it('has as many *point* nodes as there are traces', function() { - var nodes = d3.selectAll('path.point'); + it("has as many *trace scatter* nodes as there are traces", function() { + expect(countScatterTraces()).toEqual(mock.data.length); + }); - var Npts = 0; - mock.data.forEach(function(trace) { - Npts += trace.x.length; - }); + it("has as many *point* nodes as there are traces", function() { + var nodes = d3.selectAll("path.point"); - expect(nodes.size()).toEqual(Npts); - }); - - it('has the correct name spaces', function() { - var mainSVGs = d3.selectAll('.main-svg'); - - mainSVGs.each(function() { - var node = this; - assertNamespaces(node); - }); - }); - - it('should be able to get deleted', function(done) { - expect(countScatterTraces()).toEqual(mock.data.length); - expect(countSubplots()).toEqual(1); - - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countSubplots()).toEqual(1); - expect(countClipPaths()).toEqual(4); - expect(countDraggers()).toEqual(1); - - return Plotly.relayout(gd, {xaxis: null, yaxis: null}); - }).then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countSubplots()).toEqual(0); - expect(countClipPaths()).toEqual(0); - expect(countDraggers()).toEqual(0); - - done(); - }); - }); - - it('should restore layout axes when they get deleted', function(done) { - jasmine.addMatchers(customMatchers); - - expect(countScatterTraces()).toEqual(mock.data.length); - expect(countSubplots()).toEqual(1); - - Plotly.relayout(gd, {xaxis: null, yaxis: null}).then(function() { - expect(countScatterTraces()).toEqual(1); - expect(countSubplots()).toEqual(1); - expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); - expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); - - return Plotly.relayout(gd, 'xaxis', null); - }).then(function() { - expect(countScatterTraces()).toEqual(1); - expect(countSubplots()).toEqual(1); - expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); - expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); - - return Plotly.relayout(gd, 'xaxis', {}); - }).then(function() { - expect(countScatterTraces()).toEqual(1); - expect(countSubplots()).toEqual(1); - expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); - expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); - - return Plotly.relayout(gd, 'yaxis', null); - }).then(function() { - expect(countScatterTraces()).toEqual(1); - expect(countSubplots()).toEqual(1); - expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); - expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); - - return Plotly.relayout(gd, 'yaxis', {}); - }).then(function() { - expect(countScatterTraces()).toEqual(1); - expect(countSubplots()).toEqual(1); - expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); - expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); - - done(); - }); - }); + var Npts = 0; + mock.data.forEach(function(trace) { + Npts += trace.x.length; }); - describe('scatter drag', function() { - - var mock = require('@mocks/10.json'), - gd, modeBar, relayoutCallback; - - beforeEach(function(done) { - gd = createGraphDiv(); - - Plotly.plot(gd, mock.data, mock.layout).then(function() { - - modeBar = gd._fullLayout._modeBar; - relayoutCallback = jasmine.createSpy('relayoutCallback'); - - gd.on('plotly_relayout', relayoutCallback); - - done(); - }); - }); - - it('scatter plot should respond to drag interactions', function(done) { - - jasmine.addMatchers(customMatchers); - - var precision = 5; - - var buttonPan = selectButton(modeBar, 'pan2d'); - - var originalX = [-0.6225, 5.5]; - var originalY = [-1.6340975059013805, 7.166241526218911]; - - var newX = [-2.0255729166666665, 4.096927083333333]; - var newY = [-0.3769062155984817, 8.42343281652181]; - - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - - // Switch to pan mode - expect(buttonPan.isActive()).toBe(false); // initially, zoom is active - buttonPan.click(); - expect(buttonPan.isActive()).toBe(true); // switched on dragmode - - // Switching mode must not change visible range - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - - setTimeout(function() { - - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - - // Drag scene along the X axis - - mouseEvent('mousedown', 110, 150); - mouseEvent('mousemove', 220, 150); - mouseEvent('mouseup', 220, 150); - - expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + expect(nodes.size()).toEqual(Npts); + }); - // Drag scene back along the X axis (not from the same starting point but same X delta) + it("has the correct name spaces", function() { + var mainSVGs = d3.selectAll(".main-svg"); - mouseEvent('mousedown', 280, 150); - mouseEvent('mousemove', 170, 150); - mouseEvent('mouseup', 170, 150); + mainSVGs.each(function() { + var node = this; + assertNamespaces(node); + }); + }); + + it("should be able to get deleted", function(done) { + expect(countScatterTraces()).toEqual(mock.data.length); + expect(countSubplots()).toEqual(1); + + Plotly.deleteTraces(gd, [0]) + .then(function() { + expect(countScatterTraces()).toEqual(0); + expect(countSubplots()).toEqual(1); + expect(countClipPaths()).toEqual(4); + expect(countDraggers()).toEqual(1); + + return Plotly.relayout(gd, { xaxis: null, yaxis: null }); + }) + .then(function() { + expect(countScatterTraces()).toEqual(0); + expect(countSubplots()).toEqual(0); + expect(countClipPaths()).toEqual(0); + expect(countDraggers()).toEqual(0); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + done(); + }); + }); + + it("should restore layout axes when they get deleted", function(done) { + jasmine.addMatchers(customMatchers); + + expect(countScatterTraces()).toEqual(mock.data.length); + expect(countSubplots()).toEqual(1); + + Plotly.relayout(gd, { xaxis: null, yaxis: null }) + .then(function() { + expect(countScatterTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + expect(gd.layout.xaxis.range).toBeCloseToArray( + [-4.79980, 74.48580], + 4 + ); + expect(gd.layout.yaxis.range).toBeCloseToArray( + [-1.2662, 17.67023], + 4 + ); + + return Plotly.relayout(gd, "xaxis", null); + }) + .then(function() { + expect(countScatterTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + expect(gd.layout.xaxis.range).toBeCloseToArray( + [-4.79980, 74.48580], + 4 + ); + expect(gd.layout.yaxis.range).toBeCloseToArray( + [-1.2662, 17.67023], + 4 + ); + + return Plotly.relayout(gd, "xaxis", {}); + }) + .then(function() { + expect(countScatterTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + expect(gd.layout.xaxis.range).toBeCloseToArray( + [-4.79980, 74.48580], + 4 + ); + expect(gd.layout.yaxis.range).toBeCloseToArray( + [-1.2662, 17.67023], + 4 + ); + + return Plotly.relayout(gd, "yaxis", null); + }) + .then(function() { + expect(countScatterTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + expect(gd.layout.xaxis.range).toBeCloseToArray( + [-4.79980, 74.48580], + 4 + ); + expect(gd.layout.yaxis.range).toBeCloseToArray( + [-1.2662, 17.67023], + 4 + ); + + return Plotly.relayout(gd, "yaxis", {}); + }) + .then(function() { + expect(countScatterTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + expect(gd.layout.xaxis.range).toBeCloseToArray( + [-4.79980, 74.48580], + 4 + ); + expect(gd.layout.yaxis.range).toBeCloseToArray( + [-1.2662, 17.67023], + 4 + ); - // Drag scene along the Y axis + done(); + }); + }); + }); - mouseEvent('mousedown', 110, 150); - mouseEvent('mousemove', 110, 190); - mouseEvent('mouseup', 110, 190); + describe("scatter drag", function() { + var mock = require("@mocks/10.json"), gd, modeBar, relayoutCallback; - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); + beforeEach(function(done) { + gd = createGraphDiv(); - // Drag scene back along the Y axis (not from the same starting point but same Y delta) + Plotly.plot(gd, mock.data, mock.layout).then(function() { + modeBar = gd._fullLayout._modeBar; + relayoutCallback = jasmine.createSpy("relayoutCallback"); - mouseEvent('mousedown', 280, 130); - mouseEvent('mousemove', 280, 90); - mouseEvent('mouseup', 280, 90); + gd.on("plotly_relayout", relayoutCallback); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + done(); + }); + }); + + it("scatter plot should respond to drag interactions", function(done) { + jasmine.addMatchers(customMatchers); + + var precision = 5; + + var buttonPan = selectButton(modeBar, "pan2d"); + + var originalX = [-0.6225, 5.5]; + var originalY = [-1.6340975059013805, 7.166241526218911]; + + var newX = [-2.0255729166666665, 4.096927083333333]; + var newY = [-0.3769062155984817, 8.42343281652181]; + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + // Switch to pan mode + expect(buttonPan.isActive()).toBe(false); + // initially, zoom is active + buttonPan.click(); + expect(buttonPan.isActive()).toBe(true); + + // switched on dragmode + // Switching mode must not change visible range + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + setTimeout( + function() { + expect(relayoutCallback).toHaveBeenCalledTimes(1); + relayoutCallback.calls.reset(); + + // Drag scene along the X axis + mouseEvent("mousedown", 110, 150); + mouseEvent("mousemove", 220, 150); + mouseEvent("mouseup", 220, 150); + + expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray( + originalY, + precision + ); + + // Drag scene back along the X axis (not from the same starting point but same X delta) + mouseEvent("mousedown", 280, 150); + mouseEvent("mousemove", 170, 150); + mouseEvent("mouseup", 170, 150); + + expect(gd.layout.xaxis.range).toBeCloseToArray( + originalX, + precision + ); + expect(gd.layout.yaxis.range).toBeCloseToArray( + originalY, + precision + ); + + // Drag scene along the Y axis + mouseEvent("mousedown", 110, 150); + mouseEvent("mousemove", 110, 190); + mouseEvent("mouseup", 110, 190); + + expect(gd.layout.xaxis.range).toBeCloseToArray( + originalX, + precision + ); + expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); + + // Drag scene back along the Y axis (not from the same starting point but same Y delta) + mouseEvent("mousedown", 280, 130); + mouseEvent("mousemove", 280, 90); + mouseEvent("mouseup", 280, 90); + + expect(gd.layout.xaxis.range).toBeCloseToArray( + originalX, + precision + ); + expect(gd.layout.yaxis.range).toBeCloseToArray( + originalY, + precision + ); + + // Drag scene along both the X and Y axis + mouseEvent("mousedown", 110, 150); + mouseEvent("mousemove", 220, 190); + mouseEvent("mouseup", 220, 190); + + expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); + + // Drag scene back along the X and Y axis (not from the same starting point but same delta vector) + mouseEvent("mousedown", 280, 130); + mouseEvent("mousemove", 170, 90); + mouseEvent("mouseup", 170, 90); + + expect(gd.layout.xaxis.range).toBeCloseToArray( + originalX, + precision + ); + expect(gd.layout.yaxis.range).toBeCloseToArray( + originalY, + precision + ); + + setTimeout( + function() { + expect(relayoutCallback).toHaveBeenCalledTimes(6); + + // X and back; Y and back; XY and back + done(); + }, + MODEBAR_DELAY + ); + }, + MODEBAR_DELAY + ); + }); + }); - // Drag scene along both the X and Y axis + describe("contour/heatmap traces", function() { + var mock = require("@mocks/connectgaps_2d.json"); + var gd; - mouseEvent('mousedown', 110, 150); - mouseEvent('mousemove', 220, 190); - mouseEvent('mouseup', 220, 190); + function extendMock() { + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); - expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); + // add a colorbar for testing + mockData[0].showscale = true; - // Drag scene back along the X and Y axis (not from the same starting point but same delta vector) + return { data: mockData, layout: mockLayout }; + } - mouseEvent('mousedown', 280, 130); - mouseEvent('mousemove', 170, 90); - mouseEvent('mouseup', 170, 90); + function assertHeatmapNodes(expectedCnt) { + var hmNodes = d3.selectAll("g.hm"); + expect(hmNodes.size()).toEqual(expectedCnt); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + var imageNodes = d3.selectAll("image"); + expect(imageNodes.size()).toEqual(expectedCnt); + } - setTimeout(function() { + function assertContourNodes(expectedCnt) { + var nodes = d3.selectAll("g.contour"); + expect(nodes.size()).toEqual(expectedCnt); + } - expect(relayoutCallback).toHaveBeenCalledTimes(6); // X and back; Y and back; XY and back + describe("initial structure", function() { + beforeEach(function(done) { + var mockCopy = extendMock(); + var gd = createGraphDiv(); - done(); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - }, MODEBAR_DELAY); + it("has four *subplot* nodes", function() { + expect(countSubplots()).toEqual(4); + expect(countClipPaths()).toEqual(12); + expect(countDraggers()).toEqual(4); + }); - }, MODEBAR_DELAY); - }); + it("has four heatmap image nodes", function() { + // N.B. the contour traces both have a heatmap fill + assertHeatmapNodes(4); }); - describe('contour/heatmap traces', function() { - var mock = require('@mocks/connectgaps_2d.json'); - var gd; - - function extendMock() { - var mockData = Lib.extendDeep([], mock.data), - mockLayout = Lib.extendDeep({}, mock.layout); - - // add a colorbar for testing - mockData[0].showscale = true; - - return { - data: mockData, - layout: mockLayout - }; - } - - function assertHeatmapNodes(expectedCnt) { - var hmNodes = d3.selectAll('g.hm'); - expect(hmNodes.size()).toEqual(expectedCnt); - - var imageNodes = d3.selectAll('image'); - expect(imageNodes.size()).toEqual(expectedCnt); - } - - function assertContourNodes(expectedCnt) { - var nodes = d3.selectAll('g.contour'); - expect(nodes.size()).toEqual(expectedCnt); - } - - describe('initial structure', function() { - beforeEach(function(done) { - var mockCopy = extendMock(); - var gd = createGraphDiv(); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout) - .then(done); - }); - - it('has four *subplot* nodes', function() { - expect(countSubplots()).toEqual(4); - expect(countClipPaths()).toEqual(12); - expect(countDraggers()).toEqual(4); - }); - - it('has four heatmap image nodes', function() { - // N.B. the contour traces both have a heatmap fill - assertHeatmapNodes(4); - }); - - it('has two contour nodes', function() { - assertContourNodes(2); - }); - - it('has one colorbar nodes', function() { - expect(countColorBars()).toEqual(1); - }); - }); + it("has two contour nodes", function() { + assertContourNodes(2); + }); - describe('structure after restyle', function() { - beforeEach(function(done) { - var mockCopy = extendMock(); - var gd = createGraphDiv(); + it("has one colorbar nodes", function() { + expect(countColorBars()).toEqual(1); + }); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout); + describe("structure after restyle", function() { + beforeEach(function(done) { + var mockCopy = extendMock(); + var gd = createGraphDiv(); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout); + + Plotly.restyle( + gd, + { + type: "scatter", + x: [[1, 2, 3]], + y: [[2, 1, 2]], + z: null + }, + 0 + ); - Plotly.restyle(gd, { - type: 'scatter', - x: [[1, 2, 3]], - y: [[2, 1, 2]], - z: null - }, 0); + Plotly.restyle(gd, "type", "contour", 1); - Plotly.restyle(gd, 'type', 'contour', 1); + Plotly.restyle(gd, "type", "heatmap", 2).then(done); + }); - Plotly.restyle(gd, 'type', 'heatmap', 2) - .then(done); - }); + it("has four *subplot* nodes", function() { + expect(countSubplots()).toEqual(4); + expect(countClipPaths()).toEqual(12); + expect(countDraggers()).toEqual(4); + }); - it('has four *subplot* nodes', function() { - expect(countSubplots()).toEqual(4); - expect(countClipPaths()).toEqual(12); - expect(countDraggers()).toEqual(4); - }); + it("has two heatmap image nodes", function() { + assertHeatmapNodes(2); + }); - it('has two heatmap image nodes', function() { - assertHeatmapNodes(2); - }); + it("has two contour nodes", function() { + assertContourNodes(2); + }); - it('has two contour nodes', function() { - assertContourNodes(2); - }); + it("has one scatter node", function() { + expect(countScatterTraces()).toEqual(1); + }); - it('has one scatter node', function() { - expect(countScatterTraces()).toEqual(1); - }); + it("has no colorbar node", function() { + expect(countColorBars()).toEqual(0); + }); + }); - it('has no colorbar node', function() { - expect(countColorBars()).toEqual(0); - }); - }); + describe("structure after deleteTraces", function() { + beforeEach(function(done) { + gd = createGraphDiv(); - describe('structure after deleteTraces', function() { - beforeEach(function(done) { - gd = createGraphDiv(); - - var mockCopy = extendMock(); - Plotly.plot(gd, mockCopy.data, mockCopy.layout) - .then(done); - }); - - it('should be removed of traces in sequence', function(done) { - expect(countSubplots()).toEqual(4); - assertHeatmapNodes(4); - assertContourNodes(2); - expect(countColorBars()).toEqual(1); - - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countSubplots()).toEqual(4); - expect(countClipPaths()).toEqual(12); - expect(countDraggers()).toEqual(4); - assertHeatmapNodes(3); - assertContourNodes(2); - expect(countColorBars()).toEqual(0); - - return Plotly.deleteTraces(gd, [0]); - }).then(function() { - expect(countSubplots()).toEqual(4); - expect(countClipPaths()).toEqual(12); - expect(countDraggers()).toEqual(4); - assertHeatmapNodes(2); - assertContourNodes(2); - expect(countColorBars()).toEqual(0); - - return Plotly.deleteTraces(gd, [0]); - }).then(function() { - expect(countSubplots()).toEqual(4); - expect(countClipPaths()).toEqual(12); - expect(countDraggers()).toEqual(4); - assertHeatmapNodes(1); - assertContourNodes(1); - expect(countColorBars()).toEqual(0); - - return Plotly.deleteTraces(gd, [0]); - }).then(function() { - expect(countSubplots()).toEqual(3); - expect(countClipPaths()).toEqual(11); - expect(countDraggers()).toEqual(3); - assertHeatmapNodes(0); - assertContourNodes(0); - expect(countColorBars()).toEqual(0); - - var update = { - xaxis: null, - yaxis: null, - xaxis2: null, - yaxis2: null - }; - - return Plotly.relayout(gd, update); - }).then(function() { - expect(countSubplots()).toEqual(0); - expect(countClipPaths()).toEqual(0); - expect(countDraggers()).toEqual(0); - assertHeatmapNodes(0); - assertContourNodes(0); - expect(countColorBars()).toEqual(0); - - done(); - }); - }); + var mockCopy = extendMock(); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + it("should be removed of traces in sequence", function(done) { + expect(countSubplots()).toEqual(4); + assertHeatmapNodes(4); + assertContourNodes(2); + expect(countColorBars()).toEqual(1); + + Plotly.deleteTraces(gd, [0]) + .then(function() { + expect(countSubplots()).toEqual(4); + expect(countClipPaths()).toEqual(12); + expect(countDraggers()).toEqual(4); + assertHeatmapNodes(3); + assertContourNodes(2); + expect(countColorBars()).toEqual(0); + + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + expect(countSubplots()).toEqual(4); + expect(countClipPaths()).toEqual(12); + expect(countDraggers()).toEqual(4); + assertHeatmapNodes(2); + assertContourNodes(2); + expect(countColorBars()).toEqual(0); + + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + expect(countSubplots()).toEqual(4); + expect(countClipPaths()).toEqual(12); + expect(countDraggers()).toEqual(4); + assertHeatmapNodes(1); + assertContourNodes(1); + expect(countColorBars()).toEqual(0); + + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + expect(countSubplots()).toEqual(3); + expect(countClipPaths()).toEqual(11); + expect(countDraggers()).toEqual(3); + assertHeatmapNodes(0); + assertContourNodes(0); + expect(countColorBars()).toEqual(0); + + var update = { + xaxis: null, + yaxis: null, + xaxis2: null, + yaxis2: null + }; + + return Plotly.relayout(gd, update); + }) + .then(function() { + expect(countSubplots()).toEqual(0); + expect(countClipPaths()).toEqual(0); + expect(countDraggers()).toEqual(0); + assertHeatmapNodes(0); + assertContourNodes(0); + expect(countColorBars()).toEqual(0); + + done(); }); - }); + }); + }); - describe('pie traces', function() { - var mock = require('@mocks/pie_simple.json'); - var gd; - - function countPieTraces() { - return d3.select('g.pielayer').selectAll('g.trace').size(); - } + describe("pie traces", function() { + var mock = require("@mocks/pie_simple.json"); + var gd; - function countBarTraces() { - return d3.selectAll('g.trace.bars').size(); - } + function countPieTraces() { + return d3.select("g.pielayer").selectAll("g.trace").size(); + } - beforeEach(function(done) { - gd = createGraphDiv(); + function countBarTraces() { + return d3.selectAll("g.trace.bars").size(); + } - var mockData = Lib.extendDeep([], mock.data), - mockLayout = Lib.extendDeep({}, mock.layout); + beforeEach(function(done) { + gd = createGraphDiv(); - Plotly.plot(gd, mockData, mockLayout).then(done); - }); + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); - it('has as many *slice* nodes as there are pie items', function() { - var nodes = d3.selectAll('g.slice'); + Plotly.plot(gd, mockData, mockLayout).then(done); + }); - var Npts = 0; - mock.data.forEach(function(trace) { - Npts += trace.values.length; - }); + it("has as many *slice* nodes as there are pie items", function() { + var nodes = d3.selectAll("g.slice"); - expect(nodes.size()).toEqual(Npts); - }); + var Npts = 0; + mock.data.forEach(function(trace) { + Npts += trace.values.length; + }); - it('has the correct name spaces', function() { - var mainSVGs = d3.selectAll('.main-svg'); + expect(nodes.size()).toEqual(Npts); + }); - mainSVGs.each(function() { - var node = this; - assertNamespaces(node); - }); + it("has the correct name spaces", function() { + var mainSVGs = d3.selectAll(".main-svg"); - var testerSVG = d3.selectAll('#js-plotly-tester'); - assertNamespaces(testerSVG.node()); - }); + mainSVGs.each(function() { + var node = this; + assertNamespaces(node); + }); - it('should be able to get deleted', function(done) { - expect(countPieTraces()).toEqual(1); - expect(countSubplots()).toEqual(0); + var testerSVG = d3.selectAll("#js-plotly-tester"); + assertNamespaces(testerSVG.node()); + }); - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countPieTraces()).toEqual(0); - expect(countSubplots()).toEqual(0); + it("should be able to get deleted", function(done) { + expect(countPieTraces()).toEqual(1); + expect(countSubplots()).toEqual(0); - done(); - }); - }); + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countPieTraces()).toEqual(0); + expect(countSubplots()).toEqual(0); - it('should be able to be restyled to a bar chart and back', function(done) { - expect(countPieTraces()).toEqual(1); - expect(countBarTraces()).toEqual(0); - expect(countSubplots()).toEqual(0); - - Plotly.restyle(gd, 'type', 'bar').then(function() { - expect(countPieTraces()).toEqual(0); - expect(countBarTraces()).toEqual(1); - expect(countSubplots()).toEqual(1); + done(); + }); + }); + + it("should be able to be restyled to a bar chart and back", function( + done + ) { + expect(countPieTraces()).toEqual(1); + expect(countBarTraces()).toEqual(0); + expect(countSubplots()).toEqual(0); + + Plotly.restyle(gd, "type", "bar") + .then(function() { + expect(countPieTraces()).toEqual(0); + expect(countBarTraces()).toEqual(1); + expect(countSubplots()).toEqual(1); + + return Plotly.restyle(gd, "type", "pie"); + }) + .then(function() { + expect(countPieTraces()).toEqual(1); + expect(countBarTraces()).toEqual(0); + expect(countSubplots()).toEqual(0); - return Plotly.restyle(gd, 'type', 'pie'); - }).then(function() { - expect(countPieTraces()).toEqual(1); - expect(countBarTraces()).toEqual(0); - expect(countSubplots()).toEqual(0); + done(); + }); + }); + }); + }); - done(); - }); + describe("geo plots", function() { + var mock = require("@mocks/geo_first.json"); - }); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); }); - describe('geo plots', function() { - var mock = require('@mocks/geo_first.json'); + it( + "has as many *choroplethlocation* nodes as there are choropleth locations", + function() { + var nodes = d3.selectAll("path.choroplethlocation"); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + var Npts = 0; + mock.data.forEach(function(trace) { + var items = trace.locations; + if (items) Npts += items.length; }); - it('has as many *choroplethlocation* nodes as there are choropleth locations', function() { - var nodes = d3.selectAll('path.choroplethlocation'); - - var Npts = 0; - mock.data.forEach(function(trace) { - var items = trace.locations; - if(items) Npts += items.length; - }); - - expect(nodes.size()).toEqual(Npts); - }); + expect(nodes.size()).toEqual(Npts); + } + ); - it('has as many *point* nodes as there are marker points', function() { - var nodes = d3.selectAll('path.point'); + it("has as many *point* nodes as there are marker points", function() { + var nodes = d3.selectAll("path.point"); - var Npts = 0; - mock.data.forEach(function(trace) { - var items = trace.lat; - if(items) Npts += items.length; - }); + var Npts = 0; + mock.data.forEach(function(trace) { + var items = trace.lat; + if (items) Npts += items.length; + }); - expect(nodes.size()).toEqual(Npts); - }); + expect(nodes.size()).toEqual(Npts); + }); - it('has the correct name spaces', function() { - var mainSVGs = d3.selectAll('.main-svg'); + it("has the correct name spaces", function() { + var mainSVGs = d3.selectAll(".main-svg"); - mainSVGs.each(function() { - var node = this; - assertNamespaces(node); - }); + mainSVGs.each(function() { + var node = this; + assertNamespaces(node); + }); - var geoSVGs = d3.select('#geo').selectAll('svg'); + var geoSVGs = d3.select("#geo").selectAll("svg"); - geoSVGs.each(function() { - var node = this; - assertNamespaces(node); - }); - }); + geoSVGs.each(function() { + var node = this; + assertNamespaces(node); + }); }); + }); - describe('polar plots', function() { - var mock = require('@mocks/polar_scatter.json'); + describe("polar plots", function() { + var mock = require("@mocks/polar_scatter.json"); - beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); - }); + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + }); - it('has as many *mark dot* nodes as there are points', function() { - var nodes = d3.selectAll('path.mark.dot'); + it("has as many *mark dot* nodes as there are points", function() { + var nodes = d3.selectAll("path.mark.dot"); - var Npts = 0; - mock.data.forEach(function(trace) { - Npts += trace.r.length; - }); + var Npts = 0; + mock.data.forEach(function(trace) { + Npts += trace.r.length; + }); - expect(nodes.size()).toEqual(Npts); - }); + expect(nodes.size()).toEqual(Npts); }); + }); }); -describe('plot svg clip paths', function() { - - // plot with all features that rely on clip paths - function plot() { - return Plotly.plot(createGraphDiv(), [{ - type: 'contour', - z: [[1, 2, 3], [2, 3, 1]] - }, { - type: 'scatter', - y: [2, 1, 2] - }], { - showlegend: true, - xaxis: { - rangeslider: {} - }, - shapes: [{ - xref: 'x', - yref: 'y', - x0: 0, - y0: 0, - x1: 3, - y1: 3 - }] - }); - } - - afterEach(destroyGraphDiv); - - it('should set clip path url to ids (base case)', function(done) { - plot().then(function() { - - d3.selectAll('[clip-path]').each(function() { - var cp = d3.select(this).attr('clip-path'); - - expect(cp.substring(0, 5)).toEqual('url(#'); - expect(cp.substring(cp.length - 1)).toEqual(')'); - }); - - done(); - }); +describe("plot svg clip paths", function() { + // plot with all features that rely on clip paths + function plot() { + return Plotly.plot( + createGraphDiv(), + [ + { type: "contour", z: [[1, 2, 3], [2, 3, 1]] }, + { type: "scatter", y: [2, 1, 2] } + ], + { + showlegend: true, + xaxis: { rangeslider: {} }, + shapes: [{ xref: "x", yref: "y", x0: 0, y0: 0, x1: 3, y1: 3 }] + } + ); + } + + afterEach(destroyGraphDiv); + + it("should set clip path url to ids (base case)", function(done) { + plot().then(function() { + d3.selectAll("[clip-path]").each(function() { + var cp = d3.select(this).attr("clip-path"); + + expect(cp.substring(0, 5)).toEqual("url(#"); + expect(cp.substring(cp.length - 1)).toEqual(")"); + }); + + done(); }); + }); - it('should set clip path url to ids appended to window url', function(done) { - - // this case occurs in some past versions of AngularJS - // https://github.com/angular/angular.js/issues/8934 + it("should set clip path url to ids appended to window url", function(done) { + // this case occurs in some past versions of AngularJS + // https://github.com/angular/angular.js/issues/8934 + // append with href + var base = d3.select("body").append("base").attr("href", "https://plot.ly"); - // append with href - var base = d3.select('body') - .append('base') - .attr('href', 'https://plot.ly'); + // grab window URL + var href = window.location.href.split("#")[0]; - // grab window URL - var href = window.location.href.split('#')[0]; + plot().then(function() { + d3.selectAll("[clip-path]").each(function() { + var cp = d3.select(this).attr("clip-path"); - plot().then(function() { + expect(cp.substring(0, 5 + href.length)).toEqual("url(" + href + "#"); + expect(cp.substring(cp.length - 1)).toEqual(")"); + }); - d3.selectAll('[clip-path]').each(function() { - var cp = d3.select(this).attr('clip-path'); - - expect(cp.substring(0, 5 + href.length)).toEqual('url(' + href + '#'); - expect(cp.substring(cp.length - 1)).toEqual(')'); - }); - - base.remove(); - done(); - }); + base.remove(); + done(); }); + }); }); diff --git a/test/jasmine/tests/plot_promise_test.js b/test/jasmine/tests/plot_promise_test.js index 5fc8285e350..e918873f091 100644 --- a/test/jasmine/tests/plot_promise_test.js +++ b/test/jasmine/tests/plot_promise_test.js @@ -1,480 +1,470 @@ -var Plotly = require('@lib/index'); -var Events = require('@src/lib/events'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var Plotly = require("@lib/index"); +var Events = require("@src/lib/events"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +describe("Plotly.___ methods", function() { + "use strict"; + afterEach(destroyGraphDiv); -describe('Plotly.___ methods', function() { - 'use strict'; + describe("Plotly.plot promise", function() { + var promise, promiseGd; - afterEach(destroyGraphDiv); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; - describe('Plotly.plot promise', function() { - var promise, - promiseGd; + promise = Plotly.plot(createGraphDiv(), data, {}); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; - - promise = Plotly.plot(createGraphDiv(), data, {}); - - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); - - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.plot promise', function() { - var gd, - promise, - promiseRejected = false; - - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - gd = createGraphDiv(); + describe("Plotly.plot promise", function() { + var gd, promise, promiseRejected = false; - Events.init(gd); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; - gd.on('plotly_beforeplot', function() { - return false; - }); + gd = createGraphDiv(); - promise = Plotly.plot(gd, data, {}); + Events.init(gd); - promise.then(null, function() { - promiseRejected = true; - done(); - }); - }); + gd.on("plotly_beforeplot", function() { + return false; + }); + promise = Plotly.plot(gd, data, {}); - it('should be rejected when plotly_beforeplot event handlers return false', function() { - expect(promiseRejected).toBe(true); - }); + promise.then(null, function() { + promiseRejected = true; + done(); + }); }); - describe('Plotly.plot promise', function() { - var gd, - promise, - promiseRejected = false; - - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; + it( + "should be rejected when plotly_beforeplot event handlers return false", + function() { + expect(promiseRejected).toBe(true); + } + ); + }); - gd = createGraphDiv(); + describe("Plotly.plot promise", function() { + var gd, promise, promiseRejected = false; - gd._dragging = true; + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; - promise = Plotly.plot(gd, data, {}); + gd = createGraphDiv(); - promise.then(null, function() { - promiseRejected = true; - done(); - }); - }); + gd._dragging = true; + promise = Plotly.plot(gd, data, {}); - it('should reject the promise when graph is being dragged', function() { - expect(promiseRejected).toBe(true); - }); + promise.then(null, function() { + promiseRejected = true; + done(); + }); }); - describe('Plotly.redraw promise', function() { - var promise, - promiseGd; + it("should reject the promise when graph is being dragged", function() { + expect(promiseRejected).toBe(true); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - initialDiv = createGraphDiv(); + describe("Plotly.redraw promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, {}); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + initialDiv = createGraphDiv(); - promise = Plotly.redraw(initialDiv); + Plotly.plot(initialDiv, data, {}); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.redraw(initialDiv); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.newPlot promise', function() { - var promise, - promiseGd; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; + describe("Plotly.newPlot promise", function() { + var promise, promiseGd; - promise = Plotly.newPlot(createGraphDiv(), data, {}); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.newPlot(createGraphDiv(), data, {}); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.extendTraces promise', function() { - var promise, - promiseGd; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - initialDiv = createGraphDiv(); + describe("Plotly.extendTraces promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, {}); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + initialDiv = createGraphDiv(); - promise = Plotly.extendTraces(initialDiv, { y: [[2]] }, [0], 3); + Plotly.plot(initialDiv, data, {}); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.extendTraces(initialDiv, { y: [[2]] }, [0], 3); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.prependTraces promise', function() { - var promise, - promiseGd; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - initialDiv = createGraphDiv(); + describe("Plotly.prependTraces promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, {}); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + initialDiv = createGraphDiv(); - promise = Plotly.prependTraces(initialDiv, { y: [[2]] }, [0], 3); + Plotly.plot(initialDiv, data, {}); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.prependTraces(initialDiv, { y: [[2]] }, [0], 3); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.addTraces promise', function() { - var promise, - promiseGd; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - initialDiv = createGraphDiv(); + describe("Plotly.addTraces promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, {}); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + initialDiv = createGraphDiv(); - promise = Plotly.addTraces(initialDiv, [{ x: [1, 2, 3], y: [1, 2, 3] }], [1]); + Plotly.plot(initialDiv, data, {}); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.addTraces(initialDiv, [{ x: [1, 2, 3], y: [1, 2, 3] }], [ + 1 + ]); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.deleteTraces promise', function() { - var promise, - promiseGd; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - initialDiv = createGraphDiv(); + describe("Plotly.deleteTraces promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, {}); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + initialDiv = createGraphDiv(); - promise = Plotly.deleteTraces(initialDiv, [0]); + Plotly.plot(initialDiv, data, {}); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.deleteTraces(initialDiv, [0]); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.deleteTraces promise', function() { - var promise, - promiseGd; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - initialDiv = createGraphDiv(); + describe("Plotly.deleteTraces promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, {}); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + initialDiv = createGraphDiv(); - promise = Plotly.deleteTraces(initialDiv, [0]); + Plotly.plot(initialDiv, data, {}); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.deleteTraces(initialDiv, [0]); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.moveTraces promise', function() { - var promise, - promiseGd; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [ - { x: [1, 2, 3], y: [4, 5, 6] }, - { x: [1, 2, 3], y: [6, 5, 4] } - ], - initialDiv = createGraphDiv(); + describe("Plotly.moveTraces promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, {}); + beforeEach(function(done) { + var data = [ + { x: [1, 2, 3], y: [4, 5, 6] }, + { x: [1, 2, 3], y: [6, 5, 4] } + ], + initialDiv = createGraphDiv(); - promise = Plotly.moveTraces(initialDiv, 0, 1); + Plotly.plot(initialDiv, data, {}); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.moveTraces(initialDiv, 0, 1); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.restyle promise', function() { - var promise, - promiseGd; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - initialDiv = createGraphDiv(); + describe("Plotly.restyle promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, {}); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + initialDiv = createGraphDiv(); - promise = Plotly.restyle(initialDiv, 'marker.color', 'rgb(255,0,0)'); + Plotly.plot(initialDiv, data, {}); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.restyle(initialDiv, "marker.color", "rgb(255,0,0)"); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.restyle promise', function() { - var promise, - promiseRejected = false; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - initialDiv = createGraphDiv(); + describe("Plotly.restyle promise", function() { + var promise, promiseRejected = false; - Plotly.plot(initialDiv, data, {}); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + initialDiv = createGraphDiv(); - promise = Plotly.restyle(initialDiv, undefined, ''); + Plotly.plot(initialDiv, data, {}); - promise.then(null, function() { - promiseRejected = true; - done(); - }); - }); + promise = Plotly.restyle(initialDiv, undefined, ""); - it('should be rejected when the attribute is missing', function() { - expect(promiseRejected).toBe(true); - }); + promise.then(null, function() { + promiseRejected = true; + done(); + }); }); - describe('Plotly.relayout promise', function() { - var promise, - promiseGd; + it("should be rejected when the attribute is missing", function() { + expect(promiseRejected).toBe(true); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - layout = {hovermode: 'closest'}, - initialDiv = createGraphDiv(); + describe("Plotly.relayout promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, layout); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + layout = { hovermode: "closest" }, + initialDiv = createGraphDiv(); - promise = Plotly.relayout(initialDiv, 'hovermode', false); + Plotly.plot(initialDiv, data, layout); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.relayout(initialDiv, "hovermode", false); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.relayout promise', function() { - var promise, - promiseGd; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - layout = {hovermode: 'closest'}, - initialDiv = createGraphDiv(); + describe("Plotly.relayout promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, layout); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + layout = { hovermode: "closest" }, + initialDiv = createGraphDiv(); - promise = Plotly.relayout(initialDiv, 'hovermode', false); + Plotly.plot(initialDiv, data, layout); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + promise = Plotly.relayout(initialDiv, "hovermode", false); - it('should be returned with the graph div as an argument', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.data).toBeDefined(); - expect(promiseGd.layout).toBeDefined(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.relayout promise', function() { - var promise, - promiseGd; + it("should be returned with the graph div as an argument", function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.data).toBeDefined(); + expect(promiseGd.layout).toBeDefined(); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - layout = {hovermode: 'closest'}, - initialDiv = createGraphDiv(); + describe("Plotly.relayout promise", function() { + var promise, promiseGd; - Plotly.plot(initialDiv, data, layout); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + layout = { hovermode: "closest" }, + initialDiv = createGraphDiv(); - initialDiv.framework = { isPolar: true }; - promise = Plotly.relayout(initialDiv, 'hovermode', false); + Plotly.plot(initialDiv, data, layout); - promise.then(function(gd) { - promiseGd = gd; - done(); - }); - }); + initialDiv.framework = { isPolar: true }; + promise = Plotly.relayout(initialDiv, "hovermode", false); - it('should be returned with the graph div unchanged when the framework is polar', function() { - expect(promiseGd).toBeDefined(); - expect(typeof promiseGd).toBe('object'); - expect(promiseGd.changed).toBeFalsy(); - }); + promise.then(function(gd) { + promiseGd = gd; + done(); + }); }); - describe('Plotly.relayout promise', function() { - var promise, - promiseRejected = false; + it( + "should be returned with the graph div unchanged when the framework is polar", + function() { + expect(promiseGd).toBeDefined(); + expect(typeof promiseGd).toBe("object"); + expect(promiseGd.changed).toBeFalsy(); + } + ); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], - layout = {hovermode: 'closest'}, - initialDiv = createGraphDiv(); + describe("Plotly.relayout promise", function() { + var promise, promiseRejected = false; - Plotly.plot(initialDiv, data, layout); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }], + layout = { hovermode: "closest" }, + initialDiv = createGraphDiv(); - promise = Plotly.relayout(initialDiv, undefined, false); + Plotly.plot(initialDiv, data, layout); - promise.then(null, function() { - promiseRejected = true; - done(); - }); - }); + promise = Plotly.relayout(initialDiv, undefined, false); - it('should be rejected when the attribute is missing', function() { - expect(promiseRejected).toBe(true); - }); + promise.then(null, function() { + promiseRejected = true; + done(); + }); }); - describe('Plotly.Plots.resize promise', function() { - var initialDiv; + it("should be rejected when the attribute is missing", function() { + expect(promiseRejected).toBe(true); + }); + }); - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; + describe("Plotly.Plots.resize promise", function() { + var initialDiv; - initialDiv = createGraphDiv(); + beforeEach(function(done) { + var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; - Plotly.plot(initialDiv, data, {}).then(done); - }); + initialDiv = createGraphDiv(); - it('should return a resolved promise of the gd', function(done) { - Plotly.Plots.resize(initialDiv).then(function(gd) { - expect(gd).toBeDefined(); - expect(typeof gd).toBe('object'); - expect(gd.layout).toBeDefined(); - }).then(done); - }); + Plotly.plot(initialDiv, data, {}).then(done); + }); - it('should return a rejected promise with no argument', function(done) { - Plotly.Plots.resize().then(null, function(err) { - expect(err).toBeDefined(); - expect(err.message).toBe('Resize must be passed a plot div element.'); - }).then(done); - }); + it("should return a resolved promise of the gd", function(done) { + Plotly.Plots + .resize(initialDiv) + .then(function(gd) { + expect(gd).toBeDefined(); + expect(typeof gd).toBe("object"); + expect(gd.layout).toBeDefined(); + }) + .then(done); }); + it("should return a rejected promise with no argument", function(done) { + Plotly.Plots + .resize() + .then(null, function(err) { + expect(err).toBeDefined(); + expect(err.message).toBe("Resize must be passed a plot div element."); + }) + .then(done); + }); + }); }); diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 0e702ec5b89..b5b1d906b21 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -1,715 +1,648 @@ -var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); +var Plotly = require("@lib/index"); +var Plots = require("@src/plots/plots"); + +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); + +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 = { layout: { height: height } }; + + Plots.supplyDefaults(gd); + expect(gd.layout.height).toBe(height); + expect(gd._fullLayout).toBeDefined(); + expect(gd._fullLayout.height).toBe(height); + expect(gd._fullLayout.width).toBe(Plots.layoutAttributes.width.dflt); + expect(gd._fullData).toBeDefined(); + }); -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); + it("should relink private keys", function() { + var oldFullData = [ + { type: "scatter3d", z: [1, 2, 3] }, + { type: "contour", _empties: [1, 2, 3] } + ]; + + var oldFullLayout = { + _plots: { xy: { plot: {} } }, + xaxis: { + c2p: function() {} + }, + yaxis: { _m: 20 }, + scene: { _scene: {} }, + annotations: [{ _min: 10 }, { _max: 20 }], + someFunc: function() {} + }; + + var newData = [ + { type: "scatter3d", z: [1, 2, 3, 4] }, + { type: "contour", z: [[1, 2, 3], [2, 3, 4]] } + ]; + + var newLayout = { annotations: [{}, {}, {}] }; + + var gd = { + _fullData: oldFullData, + _fullLayout: oldFullLayout, + data: newData, + layout: newLayout + }; + + Plots.supplyDefaults(gd); + + expect(gd._fullData[0].z).toBe(newData[0].z); + expect(gd._fullData[1].z).toBe(newData[1].z); + expect(gd._fullData[1]._empties).toBe(oldFullData[1]._empties); + expect(gd._fullLayout.scene._scene).toBe(oldFullLayout.scene._scene); + expect(gd._fullLayout._plots.plot).toBe(oldFullLayout._plots.plot); + expect(gd._fullLayout.annotations[0]._min).toBe( + oldFullLayout.annotations[0]._min + ); + expect(gd._fullLayout.annotations[1]._max).toBe( + oldFullLayout.annotations[1]._max + ); + expect(gd._fullLayout.someFunc).toBe(oldFullLayout.someFunc); + + expect(gd._fullLayout.xaxis.c2p).not.toBe( + oldFullLayout.xaxis.c2p, + "(set during ax.setScale" + ); + expect(gd._fullLayout.yaxis._m).not.toBe( + oldFullLayout.yaxis._m, + "(set during ax.setScale" + ); + }); + it("should include the correct reference to user data", function() { + var trace0 = { y: [1, 2, 3] }; + var trace1 = { y: [5, 2, 3] }; -describe('Test Plots', function() { - 'use strict'; + var data = [trace0, trace1]; + var gd = { data: data }; - describe('Plots.supplyDefaults', function() { + Plots.supplyDefaults(gd); - it('should not throw an error when gd is a plain object', function() { - var height = 100, - gd = { - layout: { - height: height - } - }; + expect(gd.data).toBe(data); - Plots.supplyDefaults(gd); - expect(gd.layout.height).toBe(height); - expect(gd._fullLayout).toBeDefined(); - expect(gd._fullLayout.height).toBe(height); - expect(gd._fullLayout.width).toBe(Plots.layoutAttributes.width.dflt); - expect(gd._fullData).toBeDefined(); - }); + expect(gd._fullData[0].index).toEqual(0); + expect(gd._fullData[1].index).toEqual(1); - it('should relink private keys', function() { - var oldFullData = [{ - type: 'scatter3d', - z: [1, 2, 3] - }, { - type: 'contour', - _empties: [1, 2, 3] - }]; - - var oldFullLayout = { - _plots: { xy: { plot: {} } }, - xaxis: { c2p: function() {} }, - yaxis: { _m: 20 }, - scene: { _scene: {} }, - annotations: [{ _min: 10, }, { _max: 20 }], - someFunc: function() {} - }; - - var newData = [{ - type: 'scatter3d', - z: [1, 2, 3, 4] - }, { - type: 'contour', - z: [[1, 2, 3], [2, 3, 4]] - }]; - - var newLayout = { - annotations: [{}, {}, {}] - }; - - var gd = { - _fullData: oldFullData, - _fullLayout: oldFullLayout, - data: newData, - layout: newLayout - }; - - Plots.supplyDefaults(gd); - - expect(gd._fullData[0].z).toBe(newData[0].z); - expect(gd._fullData[1].z).toBe(newData[1].z); - expect(gd._fullData[1]._empties).toBe(oldFullData[1]._empties); - expect(gd._fullLayout.scene._scene).toBe(oldFullLayout.scene._scene); - expect(gd._fullLayout._plots.plot).toBe(oldFullLayout._plots.plot); - expect(gd._fullLayout.annotations[0]._min).toBe(oldFullLayout.annotations[0]._min); - expect(gd._fullLayout.annotations[1]._max).toBe(oldFullLayout.annotations[1]._max); - expect(gd._fullLayout.someFunc).toBe(oldFullLayout.someFunc); - - expect(gd._fullLayout.xaxis.c2p) - .not.toBe(oldFullLayout.xaxis.c2p, '(set during ax.setScale'); - expect(gd._fullLayout.yaxis._m) - .not.toBe(oldFullLayout.yaxis._m, '(set during ax.setScale'); - }); + expect(gd._fullData[0]._expandedIndex).toEqual(0); + expect(gd._fullData[1]._expandedIndex).toEqual(1); - it('should include the correct reference to user data', function() { - var trace0 = { y: [1, 2, 3] }; - var trace1 = { y: [5, 2, 3] }; + expect(gd._fullData[0]._input).toBe(trace0); + expect(gd._fullData[1]._input).toBe(trace1); - var data = [trace0, trace1]; - var gd = { data: data }; + expect(gd._fullData[0]._fullInput).toBe(gd._fullData[0]); + expect(gd._fullData[1]._fullInput).toBe(gd._fullData[1]); - Plots.supplyDefaults(gd); - - expect(gd.data).toBe(data); + expect(gd._fullData[0]._expandedInput).toBe(gd._fullData[0]); + expect(gd._fullData[1]._expandedInput).toBe(gd._fullData[1]); + }); - expect(gd._fullData[0].index).toEqual(0); - expect(gd._fullData[1].index).toEqual(1); + function testSanitizeMarginsHasBeenCalledOnlyOnce(gd) { + spyOn(Plots, "sanitizeMargins").and.callThrough(); + Plots.supplyDefaults(gd); + expect(Plots.sanitizeMargins).toHaveBeenCalledTimes(1); + } - expect(gd._fullData[0]._expandedIndex).toEqual(0); - expect(gd._fullData[1]._expandedIndex).toEqual(1); + it( + "should call sanitizeMargins only once when both width and height are defined", + function() { + var gd = { layout: { width: 100, height: 100 } }; - expect(gd._fullData[0]._input).toBe(trace0); - expect(gd._fullData[1]._input).toBe(trace1); + testSanitizeMarginsHasBeenCalledOnlyOnce(gd); + } + ); - expect(gd._fullData[0]._fullInput).toBe(gd._fullData[0]); - expect(gd._fullData[1]._fullInput).toBe(gd._fullData[1]); + it( + "should call sanitizeMargins only once when autosize is false", + function() { + var gd = { layout: { autosize: false, height: 100 } }; - expect(gd._fullData[0]._expandedInput).toBe(gd._fullData[0]); - expect(gd._fullData[1]._expandedInput).toBe(gd._fullData[1]); - }); + testSanitizeMarginsHasBeenCalledOnlyOnce(gd); + } + ); - function testSanitizeMarginsHasBeenCalledOnlyOnce(gd) { - spyOn(Plots, 'sanitizeMargins').and.callThrough(); - Plots.supplyDefaults(gd); - expect(Plots.sanitizeMargins).toHaveBeenCalledTimes(1); - } - - it('should call sanitizeMargins only once when both width and height are defined', function() { - var gd = { - layout: { - width: 100, - height: 100 - } - }; - - testSanitizeMarginsHasBeenCalledOnlyOnce(gd); - }); + it( + "should call sanitizeMargins only once when autosize is true", + function() { + var gd = { layout: { autosize: true, height: 100 } }; - it('should call sanitizeMargins only once when autosize is false', function() { - var gd = { - layout: { - autosize: false, - height: 100 - } - }; + testSanitizeMarginsHasBeenCalledOnlyOnce(gd); + } + ); + }); - testSanitizeMarginsHasBeenCalledOnlyOnce(gd); - }); + describe("Plots.supplyLayoutGlobalDefaults should", function() { + var layoutIn, layoutOut, expected; - it('should call sanitizeMargins only once when autosize is true', function() { - var gd = { - layout: { - autosize: true, - height: 100 - } - }; + var supplyLayoutDefaults = Plots.supplyLayoutGlobalDefaults; - testSanitizeMarginsHasBeenCalledOnlyOnce(gd); - }); + beforeEach(function() { + layoutOut = {}; }); - describe('Plots.supplyLayoutGlobalDefaults should', function() { - var layoutIn, - layoutOut, - expected; - - var supplyLayoutDefaults = Plots.supplyLayoutGlobalDefaults; - - beforeEach(function() { - layoutOut = {}; - }); - - it('should sanitize margins when they are wider than the plot', function() { - layoutIn = { - width: 500, - height: 500, - margin: { - l: 400, - r: 200 - } - }; - expected = { - l: 332, - r: 166, - t: 100, - b: 80, - pad: 0, - autoexpand: true - }; - - supplyLayoutDefaults(layoutIn, layoutOut); - expect(layoutOut.margin).toEqual(expected); - }); - - it('should sanitize margins when they are taller than the plot', function() { - layoutIn = { - width: 500, - height: 500, - margin: { - l: 400, - r: 200, - t: 300, - b: 500 - } - }; - expected = { - l: 332, - r: 166, - t: 187, - b: 311, - pad: 0, - autoexpand: true - }; - - supplyLayoutDefaults(layoutIn, layoutOut); - expect(layoutOut.margin).toEqual(expected); - }); + it("should sanitize margins when they are wider than the plot", function() { + layoutIn = { width: 500, height: 500, margin: { l: 400, r: 200 } }; + expected = { l: 332, r: 166, t: 100, b: 80, pad: 0, autoexpand: true }; + supplyLayoutDefaults(layoutIn, layoutOut); + expect(layoutOut.margin).toEqual(expected); }); - describe('Plots.supplyTraceDefaults', function() { - var supplyTraceDefaults = Plots.supplyTraceDefaults, - layout = {}; + it( + "should sanitize margins when they are taller than the plot", + function() { + layoutIn = { + width: 500, + height: 500, + margin: { l: 400, r: 200, t: 300, b: 500 } + }; + expected = { l: 332, r: 166, t: 187, b: 311, pad: 0, autoexpand: true }; + + supplyLayoutDefaults(layoutIn, layoutOut); + expect(layoutOut.margin).toEqual(expected); + } + ); + }); + + describe("Plots.supplyTraceDefaults", function() { + var supplyTraceDefaults = Plots.supplyTraceDefaults, layout = {}; + + var traceIn, traceOut; + + describe("should coerce hoverinfo", function() { + it("without *name* for single-trace graphs by default", function() { + layout._dataLength = 1; + + traceIn = {}; + traceOut = supplyTraceDefaults(traceIn, 0, layout); + expect(traceOut.hoverinfo).toEqual("x+y+z+text"); + + traceIn = { hoverinfo: "name" }; + traceOut = supplyTraceDefaults(traceIn, 0, layout); + expect(traceOut.hoverinfo).toEqual("name"); + }); + + it("without *name* for single-trace graphs by default", function() { + layout._dataLength = 2; + + traceIn = {}; + traceOut = supplyTraceDefaults(traceIn, 0, layout); + expect(traceOut.hoverinfo).toEqual("all"); + + traceIn = { hoverinfo: "name" }; + traceOut = supplyTraceDefaults(traceIn, 0, layout); + expect(traceOut.hoverinfo).toEqual("name"); + }); + }); + }); - var traceIn, traceOut; + describe("Plots.getSubplotIds", function() { + var getSubplotIds = Plots.getSubplotIds; - describe('should coerce hoverinfo', function() { - it('without *name* for single-trace graphs by default', function() { - layout._dataLength = 1; + it("returns scene ids in order", function() { + var layout = { scene2: {}, scene: {}, scene3: {} }; - traceIn = {}; - traceOut = supplyTraceDefaults(traceIn, 0, layout); - expect(traceOut.hoverinfo).toEqual('x+y+z+text'); + expect(getSubplotIds(layout, "gl3d")).toEqual([ + "scene", + "scene2", + "scene3" + ]); - traceIn = { hoverinfo: 'name' }; - traceOut = supplyTraceDefaults(traceIn, 0, layout); - expect(traceOut.hoverinfo).toEqual('name'); - }); + expect(getSubplotIds(layout, "cartesian")).toEqual([]); + expect(getSubplotIds(layout, "geo")).toEqual([]); + expect(getSubplotIds(layout, "no-valid-subplot-type")).toEqual([]); + }); - it('without *name* for single-trace graphs by default', function() { - layout._dataLength = 2; + it("returns geo ids in order", function() { + var layout = { geo2: {}, geo: {}, geo3: {} }; - traceIn = {}; - traceOut = supplyTraceDefaults(traceIn, 0, layout); - expect(traceOut.hoverinfo).toEqual('all'); + expect(getSubplotIds(layout, "geo")).toEqual(["geo", "geo2", "geo3"]); - traceIn = { hoverinfo: 'name' }; - traceOut = supplyTraceDefaults(traceIn, 0, layout); - expect(traceOut.hoverinfo).toEqual('name'); - }); - }); + expect(getSubplotIds(layout, "cartesian")).toEqual([]); + expect(getSubplotIds(layout, "gl3d")).toEqual([]); + expect(getSubplotIds(layout, "no-valid-subplot-type")).toEqual([]); }); - describe('Plots.getSubplotIds', function() { - var getSubplotIds = Plots.getSubplotIds; - - it('returns scene ids in order', function() { - var layout = { - scene2: {}, - scene: {}, - scene3: {} - }; - - expect(getSubplotIds(layout, 'gl3d')) - .toEqual(['scene', 'scene2', 'scene3']); - - expect(getSubplotIds(layout, 'cartesian')) - .toEqual([]); - expect(getSubplotIds(layout, 'geo')) - .toEqual([]); - expect(getSubplotIds(layout, 'no-valid-subplot-type')) - .toEqual([]); - }); + it("returns cartesian ids", function() { + var layout = { _has: Plots._hasPlotType, _plots: { xy: {}, x2y2: {} } }; - it('returns geo ids in order', function() { - var layout = { - geo2: {}, - geo: {}, - geo3: {} - }; - - expect(getSubplotIds(layout, 'geo')) - .toEqual(['geo', 'geo2', 'geo3']); - - expect(getSubplotIds(layout, 'cartesian')) - .toEqual([]); - expect(getSubplotIds(layout, 'gl3d')) - .toEqual([]); - expect(getSubplotIds(layout, 'no-valid-subplot-type')) - .toEqual([]); - }); + expect(getSubplotIds(layout, "cartesian")).toEqual([]); - it('returns cartesian ids', function() { - var layout = { - _has: Plots._hasPlotType, - _plots: { xy: {}, x2y2: {} } - }; + layout._basePlotModules = [{ name: "cartesian" }]; + expect(getSubplotIds(layout, "cartesian")).toEqual(["xy", "x2y2"]); + expect(getSubplotIds(layout, "gl2d")).toEqual([]); - expect(getSubplotIds(layout, 'cartesian')) - .toEqual([]); + layout._basePlotModules = [{ name: "gl2d" }]; + expect(getSubplotIds(layout, "gl2d")).toEqual(["xy", "x2y2"]); + expect(getSubplotIds(layout, "cartesian")).toEqual([]); + }); + }); - layout._basePlotModules = [{ name: 'cartesian' }]; - expect(getSubplotIds(layout, 'cartesian')) - .toEqual(['xy', 'x2y2']); - expect(getSubplotIds(layout, 'gl2d')) - .toEqual([]); + describe("Plots.findSubplotIds", function() { + var findSubplotIds = Plots.findSubplotIds; + var ids; - layout._basePlotModules = [{ name: 'gl2d' }]; - expect(getSubplotIds(layout, 'gl2d')) - .toEqual(['xy', 'x2y2']); - expect(getSubplotIds(layout, 'cartesian')) - .toEqual([]); + it("should return subplots ids found in the data", function() { + var data = [ + { type: "scatter3d", scene: "scene" }, + { type: "surface", scene: "scene2" }, + { type: "choropleth", geo: "geo" } + ]; - }); - }); + ids = findSubplotIds(data, "geo"); + expect(ids).toEqual(["geo"]); - describe('Plots.findSubplotIds', function() { - var findSubplotIds = Plots.findSubplotIds; - var ids; - - it('should return subplots ids found in the data', function() { - var data = [{ - type: 'scatter3d', - scene: 'scene' - }, { - type: 'surface', - scene: 'scene2' - }, { - type: 'choropleth', - geo: 'geo' - }]; - - ids = findSubplotIds(data, 'geo'); - expect(ids).toEqual(['geo']); - - ids = findSubplotIds(data, 'gl3d'); - expect(ids).toEqual(['scene', 'scene2']); - }); + ids = findSubplotIds(data, "gl3d"); + expect(ids).toEqual(["scene", "scene2"]); }); + }); - describe('Plots.resize', function() { - var gd; + describe("Plots.resize", function() { + var gd; - beforeAll(function(done) { - gd = createGraphDiv(); + beforeAll(function(done) { + gd = createGraphDiv(); - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }]) - .then(function() { - gd.style.width = '400px'; - gd.style.height = '400px'; + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }]) + .then(function() { + gd.style.width = "400px"; + gd.style.height = "400px"; - return Plotly.Plots.resize(gd); - }) - .then(done); - }); + return Plotly.Plots.resize(gd); + }) + .then(done); + }); - afterEach(destroyGraphDiv); + afterEach(destroyGraphDiv); - it('should resize the plot clip', function() { - var uid = gd._fullLayout._uid; + it("should resize the plot clip", function() { + var uid = gd._fullLayout._uid; - var plotClip = document.getElementById('clip' + uid + 'xyplot'), - clipRect = plotClip.children[0], - clipWidth = +clipRect.getAttribute('width'), - clipHeight = +clipRect.getAttribute('height'); + var plotClip = document.getElementById("clip" + uid + "xyplot"), + clipRect = plotClip.children[0], + clipWidth = +clipRect.getAttribute("width"), + clipHeight = +clipRect.getAttribute("height"); - expect(clipWidth).toBe(240); - expect(clipHeight).toBe(220); - }); + expect(clipWidth).toBe(240); + expect(clipHeight).toBe(220); + }); - it('should resize the main svgs', function() { - var mainSvgs = document.getElementsByClassName('main-svg'); + it("should resize the main svgs", function() { + var mainSvgs = document.getElementsByClassName("main-svg"); - for(var i = 0; i < mainSvgs.length; i++) { - var svg = mainSvgs[i], - svgWidth = +svg.getAttribute('width'), - svgHeight = +svg.getAttribute('height'); + for (var i = 0; i < mainSvgs.length; i++) { + var svg = mainSvgs[i], + svgWidth = +svg.getAttribute("width"), + svgHeight = +svg.getAttribute("height"); - expect(svgWidth).toBe(400); - expect(svgHeight).toBe(400); - } - }); + expect(svgWidth).toBe(400); + expect(svgHeight).toBe(400); + } + }); - it('should update the axis scales', function() { - var fullLayout = gd._fullLayout, - plotinfo = fullLayout._plots.xy; + it("should update the axis scales", function() { + var fullLayout = gd._fullLayout, plotinfo = fullLayout._plots.xy; - expect(fullLayout.xaxis._length).toEqual(240); - expect(fullLayout.yaxis._length).toEqual(220); + expect(fullLayout.xaxis._length).toEqual(240); + expect(fullLayout.yaxis._length).toEqual(220); - expect(plotinfo.xaxis._length).toEqual(240); - expect(plotinfo.yaxis._length).toEqual(220); - }); + expect(plotinfo.xaxis._length).toEqual(240); + expect(plotinfo.yaxis._length).toEqual(220); }); + }); - describe('Plots.purge', function() { - var gd; - - beforeEach(function(done) { - gd = createGraphDiv(); - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}).then(done); - }); + describe("Plots.purge", function() { + var gd; - afterEach(destroyGraphDiv); - - it('should unset everything in the gd except _context', function() { - var expectedKeys = [ - '_ev', '_internalEv', 'on', 'once', 'removeListener', 'removeAllListeners', - '_internalOn', '_internalOnce', '_removeInternalListener', - '_removeAllInternalListeners', 'emit', '_context', '_replotPending', - '_hmpixcount', '_hmlumcount', '_mouseDownTime' - ]; - - Plots.purge(gd); - expect(Object.keys(gd)).toEqual(expectedKeys); - expect(gd.data).toBeUndefined(); - expect(gd.layout).toBeUndefined(); - expect(gd._fullData).toBeUndefined(); - expect(gd._fullLayout).toBeUndefined(); - expect(gd.calcdata).toBeUndefined(); - expect(gd.framework).toBeUndefined(); - expect(gd.empty).toBeUndefined(); - expect(gd.fid).toBeUndefined(); - expect(gd.undoqueue).toBeUndefined(); - expect(gd.undonum).toBeUndefined(); - expect(gd.autoplay).toBeUndefined(); - expect(gd.changed).toBeUndefined(); - expect(gd._tester).toBeUndefined(); - expect(gd._testref).toBeUndefined(); - expect(gd._promises).toBeUndefined(); - expect(gd._redrawTimer).toBeUndefined(); - expect(gd._replotting).toBeUndefined(); - expect(gd.firstscatter).toBeUndefined(); - expect(gd.hmlumcount).toBeUndefined(); - expect(gd.hmpixcount).toBeUndefined(); - expect(gd.numboxes).toBeUndefined(); - expect(gd._hoverTimer).toBeUndefined(); - expect(gd._lastHoverTime).toBeUndefined(); - expect(gd._transitionData).toBeUndefined(); - expect(gd._transitioning).toBeUndefined(); - }); + beforeEach(function(done) { + gd = createGraphDiv(); + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}).then(done); }); - describe('extendObjectWithContainers', function() { - - function assert(dest, src, expected) { - Plots.extendObjectWithContainers(dest, src, ['container']); - expect(dest).toEqual(expected); - } - - it('extend each container items', function() { - var dest = { - container: [ - { text: '1', x: 1, y: 1 }, - { text: '2', x: 2, y: 2 } - ] - }; - - var src = { - container: [ - { text: '1-new' }, - { text: '2-new' } - ] - }; - - var expected = { - container: [ - { text: '1-new', x: 1, y: 1 }, - { text: '2-new', x: 2, y: 2 } - ] - }; - - assert(dest, src, expected); - }); + afterEach(destroyGraphDiv); + + it("should unset everything in the gd except _context", function() { + var expectedKeys = [ + "_ev", + "_internalEv", + "on", + "once", + "removeListener", + "removeAllListeners", + "_internalOn", + "_internalOnce", + "_removeInternalListener", + "_removeAllInternalListeners", + "emit", + "_context", + "_replotPending", + "_hmpixcount", + "_hmlumcount", + "_mouseDownTime" + ]; + + Plots.purge(gd); + expect(Object.keys(gd)).toEqual(expectedKeys); + expect(gd.data).toBeUndefined(); + expect(gd.layout).toBeUndefined(); + expect(gd._fullData).toBeUndefined(); + expect(gd._fullLayout).toBeUndefined(); + expect(gd.calcdata).toBeUndefined(); + expect(gd.framework).toBeUndefined(); + expect(gd.empty).toBeUndefined(); + expect(gd.fid).toBeUndefined(); + expect(gd.undoqueue).toBeUndefined(); + expect(gd.undonum).toBeUndefined(); + expect(gd.autoplay).toBeUndefined(); + expect(gd.changed).toBeUndefined(); + expect(gd._tester).toBeUndefined(); + expect(gd._testref).toBeUndefined(); + expect(gd._promises).toBeUndefined(); + expect(gd._redrawTimer).toBeUndefined(); + expect(gd._replotting).toBeUndefined(); + expect(gd.firstscatter).toBeUndefined(); + expect(gd.hmlumcount).toBeUndefined(); + expect(gd.hmpixcount).toBeUndefined(); + expect(gd.numboxes).toBeUndefined(); + expect(gd._hoverTimer).toBeUndefined(); + expect(gd._lastHoverTime).toBeUndefined(); + expect(gd._transitionData).toBeUndefined(); + expect(gd._transitioning).toBeUndefined(); + }); + }); - it('clears container items when applying null src items', function() { - var dest = { - container: [ - { text: '1', x: 1, y: 1 }, - { text: '2', x: 2, y: 2 } - ] - }; + describe("extendObjectWithContainers", function() { + function assert(dest, src, expected) { + Plots.extendObjectWithContainers(dest, src, ["container"]); + expect(dest).toEqual(expected); + } - var src = { - container: [null, null] - }; + it("extend each container items", function() { + var dest = { + container: [{ text: "1", x: 1, y: 1 }, { text: "2", x: 2, y: 2 }] + }; - var expected = { - container: [null, null] - }; + var src = { container: [{ text: "1-new" }, { text: "2-new" }] }; - assert(dest, src, expected); - }); + var expected = { + container: [ + { text: "1-new", x: 1, y: 1 }, + { text: "2-new", x: 2, y: 2 } + ] + }; - it('clears container applying null src', function() { - var dest = { - container: [ - { text: '1', x: 1, y: 1 }, - { text: '2', x: 2, y: 2 } - ] - }; + assert(dest, src, expected); + }); - var src = { container: null }; + it("clears container items when applying null src items", function() { + var dest = { + container: [{ text: "1", x: 1, y: 1 }, { text: "2", x: 2, y: 2 }] + }; - var expected = { container: null }; + var src = { container: [null, null] }; - assert(dest, src, expected); - }); - }); + var expected = { container: [null, null] }; - describe('Plots.graphJson', function() { - - it('should serialize data, layout and frames', function(done) { - var mock = { - data: [{ - x: [1, 2, 3], - y: [2, 1, 2] - }], - layout: { - title: 'base' - }, - frames: [{ - data: [{ - y: [1, 2, 1], - }], - layout: { - title: 'frame A' - }, - name: 'A' - }, null, { - data: [{ - y: [1, 2, 3], - }], - layout: { - title: 'frame B' - }, - name: 'B' - }, { - data: [null, false, undefined], - layout: 'garbage', - name: 'garbage' - }] - }; - - Plotly.plot(createGraphDiv(), mock).then(function(gd) { - var str = Plots.graphJson(gd, false, 'keepdata'); - var obj = JSON.parse(str); - - expect(obj.data).toEqual(mock.data); - expect(obj.layout).toEqual(mock.layout); - expect(obj.frames[0]).toEqual(mock.frames[0]); - expect(obj.frames[1]).toEqual(mock.frames[2]); - expect(obj.frames[2]).toEqual({ - data: [null, false, null], - layout: 'garbage', - name: 'garbage' - }); - }) - .then(function() { - destroyGraphDiv(); - done(); - }); - }); + assert(dest, src, expected); }); - describe('Plots.getSubplotCalcData', function() { - var trace0 = { geo: 'geo2' }; - var trace1 = { subplot: 'ternary10' }; - var trace2 = { subplot: 'ternary10' }; - - var cd = [ - [{ trace: trace0 }], - [{ trace: trace1 }], - [{ trace: trace2}] - ]; + it("clears container applying null src", function() { + var dest = { + container: [{ text: "1", x: 1, y: 1 }, { text: "2", x: 2, y: 2 }] + }; - it('should extract calcdata traces associated with subplot (1)', function() { - var out = Plots.getSubplotCalcData(cd, 'geo', 'geo2'); - expect(out).toEqual([[{ trace: trace0 }]]); - }); + var src = { container: null }; - it('should extract calcdata traces associated with subplot (2)', function() { - var out = Plots.getSubplotCalcData(cd, 'ternary', 'ternary10'); - expect(out).toEqual([[{ trace: trace1 }], [{ trace: trace2 }]]); - }); + var expected = { container: null }; - it('should return [] when no calcdata traces where found', function() { - var out = Plots.getSubplotCalcData(cd, 'geo', 'geo'); - expect(out).toEqual([]); - }); - - it('should return [] when subplot type is invalid', function() { - var out = Plots.getSubplotCalcData(cd, 'non-sense', 'geo2'); - expect(out).toEqual([]); - }); + assert(dest, src, expected); }); - - describe('Plots.generalUpdatePerTraceModule', function() { - - function _update(subplotCalcData, traceHashOld) { - var subplot = { traceHash: traceHashOld || {} }; - var calcDataPerModule = []; - - var plot = function(_, moduleCalcData) { - calcDataPerModule.push(moduleCalcData); - }; - - subplotCalcData.forEach(function(calcTrace) { - calcTrace[0].trace._module = { plot: plot }; - }); - - Plots.generalUpdatePerTraceModule(subplot, subplotCalcData, {}); - - return { - traceHash: subplot.traceHash, - calcDataPerModule: calcDataPerModule - }; - } - - it('should update subplot trace hash and call module plot method with correct calcdata traces', function() { - var out = _update([ - [ { trace: { type: 'A', visible: false } } ], - [ { trace: { type: 'A', visible: true } } ], - [ { trace: { type: 'B', visible: false } } ], - [ { trace: { type: 'C', visible: true } } ] - ]); - - expect(Object.keys(out.traceHash)).toEqual(['A', 'C']); - expect(out.traceHash.A.length).toEqual(1); - expect(out.traceHash.C.length).toEqual(1); - - expect(out.calcDataPerModule.length).toEqual(2); - expect(out.calcDataPerModule[0].length).toEqual(1); - expect(out.calcDataPerModule[1].length).toEqual(1); - - var out2 = _update([ - [ { trace: { type: 'A', visible: false } } ], - [ { trace: { type: 'A', visible: false } } ], - [ { trace: { type: 'B', visible: true } } ], - [ { trace: { type: 'C', visible: false } } ] - ], out.traceHash); - - expect(Object.keys(out2.traceHash)).toEqual(['B', 'A', 'C']); - expect(out2.traceHash.B.length).toEqual(1); - expect(out2.traceHash.A.length).toEqual(1); - expect(out2.traceHash.A[0][0].trace.visible).toBe(false); - expect(out2.traceHash.C.length).toEqual(1); - expect(out2.traceHash.C[0][0].trace.visible).toBe(false); - - expect(out2.calcDataPerModule.length).toEqual(1); - expect(out2.calcDataPerModule[0].length).toEqual(1); - - var out3 = _update([ - [ { trace: { type: 'A', visible: false } } ], - [ { trace: { type: 'A', visible: false } } ], - [ { trace: { type: 'B', visible: false } } ], - [ { trace: { type: 'C', visible: false } } ] - ], out2.traceHash); - - expect(Object.keys(out3.traceHash)).toEqual(['B', 'A', 'C']); - expect(out3.traceHash.B.length).toEqual(1); - expect(out3.traceHash.B[0][0].trace.visible).toBe(false); - expect(out3.traceHash.A.length).toEqual(1); - expect(out3.traceHash.A[0][0].trace.visible).toBe(false); - expect(out3.traceHash.C.length).toEqual(1); - expect(out3.traceHash.C[0][0].trace.visible).toBe(false); - - expect(out3.calcDataPerModule.length).toEqual(0); - - var out4 = _update([ - [ { trace: { type: 'A', visible: true } } ], - [ { trace: { type: 'A', visible: true } } ], - [ { trace: { type: 'B', visible: true } } ], - [ { trace: { type: 'C', visible: true } } ] - ], out3.traceHash); - - expect(Object.keys(out4.traceHash)).toEqual(['A', 'B', 'C']); - expect(out4.traceHash.A.length).toEqual(2); - expect(out4.traceHash.B.length).toEqual(1); - expect(out4.traceHash.C.length).toEqual(1); - - expect(out4.calcDataPerModule.length).toEqual(3); - expect(out4.calcDataPerModule[0].length).toEqual(2); - expect(out4.calcDataPerModule[1].length).toEqual(1); - expect(out4.calcDataPerModule[2].length).toEqual(1); + }); + + describe("Plots.graphJson", function() { + it("should serialize data, layout and frames", function(done) { + var mock = { + data: [{ x: [1, 2, 3], y: [2, 1, 2] }], + layout: { title: "base" }, + frames: [ + { + data: [{ y: [1, 2, 1] }], + layout: { title: "frame A" }, + name: "A" + }, + null, + { + data: [{ y: [1, 2, 3] }], + layout: { title: "frame B" }, + name: "B" + }, + { + data: [null, false, undefined], + layout: "garbage", + name: "garbage" + } + ] + }; + + Plotly.plot(createGraphDiv(), mock) + .then(function(gd) { + var str = Plots.graphJson(gd, false, "keepdata"); + var obj = JSON.parse(str); + + expect(obj.data).toEqual(mock.data); + expect(obj.layout).toEqual(mock.layout); + expect(obj.frames[0]).toEqual(mock.frames[0]); + expect(obj.frames[1]).toEqual(mock.frames[2]); + expect(obj.frames[2]).toEqual({ + data: [null, false, null], + layout: "garbage", + name: "garbage" + }); + }) + .then(function() { + destroyGraphDiv(); + done(); }); + }); + }); + + describe("Plots.getSubplotCalcData", function() { + var trace0 = { geo: "geo2" }; + var trace1 = { subplot: "ternary10" }; + var trace2 = { subplot: "ternary10" }; + + var cd = [[{ trace: trace0 }], [{ trace: trace1 }], [{ trace: trace2 }]]; + + it( + "should extract calcdata traces associated with subplot (1)", + function() { + var out = Plots.getSubplotCalcData(cd, "geo", "geo2"); + expect(out).toEqual([[{ trace: trace0 }]]); + } + ); + + it( + "should extract calcdata traces associated with subplot (2)", + function() { + var out = Plots.getSubplotCalcData(cd, "ternary", "ternary10"); + expect(out).toEqual([[{ trace: trace1 }], [{ trace: trace2 }]]); + } + ); + + it("should return [] when no calcdata traces where found", function() { + var out = Plots.getSubplotCalcData(cd, "geo", "geo"); + expect(out).toEqual([]); + }); - it('should handle cases when module plot is not set (geo case)', function(done) { - Plotly.plot(createGraphDiv(), [{ - type: 'scattergeo', - visible: false, - lon: [10, 20], - lat: [20, 10] - }, { - type: 'scattergeo', - lon: [10, 20], - lat: [20, 10] - }]) - .then(function() { - expect(d3.selectAll('g.trace.scattergeo').size()).toEqual(1); - - destroyGraphDiv(); - done(); - }); - }); + it("should return [] when subplot type is invalid", function() { + var out = Plots.getSubplotCalcData(cd, "non-sense", "geo2"); + expect(out).toEqual([]); + }); + }); + + describe("Plots.generalUpdatePerTraceModule", function() { + function _update(subplotCalcData, traceHashOld) { + var subplot = { traceHash: traceHashOld || {} }; + var calcDataPerModule = []; + + var plot = function(_, moduleCalcData) { + calcDataPerModule.push(moduleCalcData); + }; + + subplotCalcData.forEach(function(calcTrace) { + calcTrace[0].trace._module = { plot: plot }; + }); + + Plots.generalUpdatePerTraceModule(subplot, subplotCalcData, {}); + + return { + traceHash: subplot.traceHash, + calcDataPerModule: calcDataPerModule + }; + } + + it( + "should update subplot trace hash and call module plot method with correct calcdata traces", + function() { + var out = _update([ + [{ trace: { type: "A", visible: false } }], + [{ trace: { type: "A", visible: true } }], + [{ trace: { type: "B", visible: false } }], + [{ trace: { type: "C", visible: true } }] + ]); + + expect(Object.keys(out.traceHash)).toEqual(["A", "C"]); + expect(out.traceHash.A.length).toEqual(1); + expect(out.traceHash.C.length).toEqual(1); + + expect(out.calcDataPerModule.length).toEqual(2); + expect(out.calcDataPerModule[0].length).toEqual(1); + expect(out.calcDataPerModule[1].length).toEqual(1); + + var out2 = _update( + [ + [{ trace: { type: "A", visible: false } }], + [{ trace: { type: "A", visible: false } }], + [{ trace: { type: "B", visible: true } }], + [{ trace: { type: "C", visible: false } }] + ], + out.traceHash + ); + + expect(Object.keys(out2.traceHash)).toEqual(["B", "A", "C"]); + expect(out2.traceHash.B.length).toEqual(1); + expect(out2.traceHash.A.length).toEqual(1); + expect(out2.traceHash.A[0][0].trace.visible).toBe(false); + expect(out2.traceHash.C.length).toEqual(1); + expect(out2.traceHash.C[0][0].trace.visible).toBe(false); + + expect(out2.calcDataPerModule.length).toEqual(1); + expect(out2.calcDataPerModule[0].length).toEqual(1); + + var out3 = _update( + [ + [{ trace: { type: "A", visible: false } }], + [{ trace: { type: "A", visible: false } }], + [{ trace: { type: "B", visible: false } }], + [{ trace: { type: "C", visible: false } }] + ], + out2.traceHash + ); + + expect(Object.keys(out3.traceHash)).toEqual(["B", "A", "C"]); + expect(out3.traceHash.B.length).toEqual(1); + expect(out3.traceHash.B[0][0].trace.visible).toBe(false); + expect(out3.traceHash.A.length).toEqual(1); + expect(out3.traceHash.A[0][0].trace.visible).toBe(false); + expect(out3.traceHash.C.length).toEqual(1); + expect(out3.traceHash.C[0][0].trace.visible).toBe(false); + + expect(out3.calcDataPerModule.length).toEqual(0); + + var out4 = _update( + [ + [{ trace: { type: "A", visible: true } }], + [{ trace: { type: "A", visible: true } }], + [{ trace: { type: "B", visible: true } }], + [{ trace: { type: "C", visible: true } }] + ], + out3.traceHash + ); + + expect(Object.keys(out4.traceHash)).toEqual(["A", "B", "C"]); + expect(out4.traceHash.A.length).toEqual(2); + expect(out4.traceHash.B.length).toEqual(1); + expect(out4.traceHash.C.length).toEqual(1); + + expect(out4.calcDataPerModule.length).toEqual(3); + expect(out4.calcDataPerModule[0].length).toEqual(2); + expect(out4.calcDataPerModule[1].length).toEqual(1); + expect(out4.calcDataPerModule[2].length).toEqual(1); + } + ); + + it("should handle cases when module plot is not set (geo case)", function( + done + ) { + Plotly.plot(createGraphDiv(), [ + { + type: "scattergeo", + visible: false, + lon: [10, 20], + lat: [20, 10] + }, + { type: "scattergeo", lon: [10, 20], lat: [20, 10] } + ]).then(function() { + expect(d3.selectAll("g.trace.scattergeo").size()).toEqual(1); + + destroyGraphDiv(); + done(); + }); + }); - it('should handle cases when module plot is not set (ternary case)', function(done) { - Plotly.plot(createGraphDiv(), [{ - type: 'scatterternary', - visible: false, - a: [0.1, 0.2], - b: [0.2, 0.1] - }, { - type: 'scatterternary', - a: [0.1, 0.2], - b: [0.2, 0.1] - }]) - .then(function() { - expect(d3.selectAll('g.trace.scatter').size()).toEqual(1); - - destroyGraphDiv(); - done(); - }); + it( + "should handle cases when module plot is not set (ternary case)", + function(done) { + Plotly.plot(createGraphDiv(), [ + { + type: "scatterternary", + visible: false, + a: [0.1, 0.2], + b: [0.2, 0.1] + }, + { type: "scatterternary", a: [0.1, 0.2], b: [0.2, 0.1] } + ]).then(function() { + expect(d3.selectAll("g.trace.scatter").size()).toEqual(1); + + destroyGraphDiv(); + done(); }); - }); + } + ); + }); }); diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index b7b227bc8a0..490f3a295ad 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -1,216 +1,215 @@ -var Plotly = require('@lib/index'); +var Plotly = require("@lib/index"); -var Lib = require('@src/lib'); +var Lib = require("@src/lib"); -describe('plot schema', function() { - 'use strict'; +describe("plot schema", function() { + "use strict"; + var plotSchema = Plotly.PlotSchema.get(), + valObjects = plotSchema.defs.valObjects; - var plotSchema = Plotly.PlotSchema.get(), - valObjects = plotSchema.defs.valObjects; + var isValObject = Plotly.PlotSchema.isValObject, + isPlainObject = Lib.isPlainObject; - var isValObject = Plotly.PlotSchema.isValObject, - isPlainObject = Lib.isPlainObject; + var VALTYPES = Object.keys(valObjects), ROLES = ["info", "style", "data"]; - var VALTYPES = Object.keys(valObjects), - ROLES = ['info', 'style', 'data']; - - function assertPlotSchema(callback) { - var traces = plotSchema.traces; - - Object.keys(traces).forEach(function(traceName) { - Plotly.PlotSchema.crawl(traces[traceName].attributes, callback); - }); - - Plotly.PlotSchema.crawl(plotSchema.layout.layoutAttributes, callback); - } - - it('all attributes should have a valid `valType`', function() { - assertPlotSchema( - function(attr) { - if(isValObject(attr)) { - expect(VALTYPES.indexOf(attr.valType) !== -1).toBe(true); - } - } - ); + function assertPlotSchema(callback) { + var traces = plotSchema.traces; + Object.keys(traces).forEach(function(traceName) { + Plotly.PlotSchema.crawl(traces[traceName].attributes, callback); }); - it('all attributes should only have valid `role`', function() { - assertPlotSchema( - function(attr) { - if(isValObject(attr)) { - expect(ROLES.indexOf(attr.role) !== -1).toBe(true, attr); - } - } - ); - }); + Plotly.PlotSchema.crawl(plotSchema.layout.layoutAttributes, callback); + } - it('all nested objects should have the *object* `role`', function() { - assertPlotSchema( - function(attr, attrName) { - if(!isValObject(attr) && isPlainObject(attr) && attrName !== 'items') { - expect(attr.role === 'object').toBe(true); - } - } - ); + it("all attributes should have a valid `valType`", function() { + assertPlotSchema(function(attr) { + if (isValObject(attr)) { + expect(VALTYPES.indexOf(attr.valType) !== -1).toBe(true); + } }); + }); - it('all attributes should have the required options', function() { - assertPlotSchema( - function(attr) { - if(isValObject(attr)) { - var keys = Object.keys(attr); - - valObjects[attr.valType].requiredOpts.forEach(function(opt) { - expect(keys.indexOf(opt) !== -1).toBe(true); - }); - } - } - ); + it("all attributes should only have valid `role`", function() { + assertPlotSchema(function(attr) { + if (isValObject(attr)) { + expect(ROLES.indexOf(attr.role) !== -1).toBe(true, attr); + } }); + }); - it('all attributes should only have compatible options', function() { - assertPlotSchema( - function(attr) { - if(isValObject(attr)) { - var valObject = valObjects[attr.valType], - opts = valObject.requiredOpts - .concat(valObject.otherOpts) - .concat(['valType', 'description', 'role']); - - Object.keys(attr).forEach(function(key) { - expect(opts.indexOf(key) !== -1).toBe(true, key, attr); - }); - } - } - ); + it("all nested objects should have the *object* `role`", function() { + assertPlotSchema(function(attr, attrName) { + if (!isValObject(attr) && isPlainObject(attr) && attrName !== "items") { + expect(attr.role === "object").toBe(true); + } }); + }); - it('all subplot objects should contain _isSubplotObj', function() { - var IS_SUBPLOT_OBJ = '_isSubplotObj', - astrs = ['xaxis', 'yaxis', 'scene', 'geo', 'ternary', 'mapbox'], - cnt = 0; - - // check if the subplot objects have '_isSubplotObj' - astrs.forEach(function(astr) { - expect( - Lib.nestedProperty( - plotSchema.layout.layoutAttributes, - astr + '.' + IS_SUBPLOT_OBJ - ).get() - ).toBe(true); - }); + it("all attributes should have the required options", function() { + assertPlotSchema(function(attr) { + if (isValObject(attr)) { + var keys = Object.keys(attr); - // check that no other object has '_isSubplotObj' - assertPlotSchema( - function(attr, attrName) { - if(attr[IS_SUBPLOT_OBJ] === true) { - expect(astrs.indexOf(attrName)).not.toEqual(-1); - cnt++; - } - } - ); - - expect(cnt).toEqual(astrs.length); + valObjects[attr.valType].requiredOpts.forEach(function(opt) { + expect(keys.indexOf(opt) !== -1).toBe(true); + }); + } }); - - it('should convert _isLinkedToArray attributes to items object', function() { - var astrs = [ - 'annotations', 'shapes', 'images', - 'xaxis.rangeselector.buttons', - 'updatemenus', - 'sliders', - 'mapbox.layers' - ]; - - astrs.forEach(function(astr) { - var np = Lib.nestedProperty( - plotSchema.layout.layoutAttributes, astr - ); - - var name = np.parts[np.parts.length - 1], - itemName = name.substr(0, name.length - 1); - - var itemsObj = np.get().items, - itemObj = itemsObj[itemName]; - - // N.B. the specs below must be satisfied for plotly.py - expect(isPlainObject(itemsObj)).toBe(true); - expect(itemsObj.role).toBeUndefined(); - expect(Object.keys(itemsObj).length).toEqual(1); - expect(isPlainObject(itemObj)).toBe(true); - expect(itemObj.role).toBe('object'); - - var role = np.get().role; - expect(role).toEqual('object'); + }); + + it("all attributes should only have compatible options", function() { + assertPlotSchema(function(attr) { + if (isValObject(attr)) { + var valObject = valObjects[attr.valType], + opts = valObject.requiredOpts + .concat(valObject.otherOpts) + .concat(["valType", "description", "role"]); + + Object.keys(attr).forEach(function(key) { + expect(opts.indexOf(key) !== -1).toBe(true, key, attr); }); + } }); - - it('valObjects descriptions should be strings', function() { - assertPlotSchema( - function(attr) { - var isValid; - - if(isValObject(attr)) { - // attribute don't have to have a description (for now) - isValid = (typeof attr.description === 'string') || - (attr.description === undefined); - - expect(isValid).toBe(true); - } - } - ); + }); + + it("all subplot objects should contain _isSubplotObj", function() { + var IS_SUBPLOT_OBJ = "_isSubplotObj", + astrs = ["xaxis", "yaxis", "scene", "geo", "ternary", "mapbox"], + cnt = 0; + + // check if the subplot objects have '_isSubplotObj' + astrs.forEach(function(astr) { + expect( + Lib.nestedProperty( + plotSchema.layout.layoutAttributes, + astr + "." + IS_SUBPLOT_OBJ + ).get() + ).toBe(true); }); - it('deprecated attributes should have a `valType` and `role`', function() { - var DEPRECATED = '_deprecated'; - - assertPlotSchema( - function(attr) { - if(isPlainObject(attr[DEPRECATED])) { - Object.keys(attr[DEPRECATED]).forEach(function(dAttrName) { - var dAttr = attr[DEPRECATED][dAttrName]; - - expect(VALTYPES.indexOf(dAttr.valType) !== -1).toBe(true); - expect(ROLES.indexOf(dAttr.role) !== -1).toBe(true); - }); - } - } - ); + // check that no other object has '_isSubplotObj' + assertPlotSchema(function(attr, attrName) { + if (attr[IS_SUBPLOT_OBJ] === true) { + expect(astrs.indexOf(attrName)).not.toEqual(-1); + cnt++; + } }); - it('should work with registered transforms', function() { - var valObjects = plotSchema.transforms.filter.attributes, - attrNames = Object.keys(valObjects); - - ['operation', 'value', 'target'].forEach(function(k) { - expect(attrNames).toContain(k); - }); + expect(cnt).toEqual(astrs.length); + }); + + it("should convert _isLinkedToArray attributes to items object", function() { + var astrs = [ + "annotations", + "shapes", + "images", + "xaxis.rangeselector.buttons", + "updatemenus", + "sliders", + "mapbox.layers" + ]; + + astrs.forEach(function(astr) { + var np = Lib.nestedProperty(plotSchema.layout.layoutAttributes, astr); + + var name = np.parts[np.parts.length - 1], + itemName = name.substr(0, name.length - 1); + + var itemsObj = np.get().items, itemObj = itemsObj[itemName]; + + // N.B. the specs below must be satisfied for plotly.py + expect(isPlainObject(itemsObj)).toBe(true); + expect(itemsObj.role).toBeUndefined(); + expect(Object.keys(itemsObj).length).toEqual(1); + expect(isPlainObject(itemObj)).toBe(true); + expect(itemObj.role).toBe("object"); + + var role = np.get().role; + expect(role).toEqual("object"); }); + }); - it('should work with registered components', function() { - expect(plotSchema.traces.scatter.attributes.xcalendar.valType).toEqual('enumerated'); - expect(plotSchema.traces.scatter3d.attributes.zcalendar.valType).toEqual('enumerated'); + it("valObjects descriptions should be strings", function() { + assertPlotSchema(function(attr) { + var isValid; - expect(plotSchema.layout.layoutAttributes.calendar.valType).toEqual('enumerated'); - expect(plotSchema.layout.layoutAttributes.xaxis.calendar.valType).toEqual('enumerated'); - expect(plotSchema.layout.layoutAttributes.scene.xaxis.calendar.valType).toEqual('enumerated'); + if (isValObject(attr)) { + // attribute don't have to have a description (for now) + isValid = typeof attr.description === "string" || + attr.description === undefined; - expect(plotSchema.transforms.filter.attributes.valuecalendar.valType).toEqual('enumerated'); - expect(plotSchema.transforms.filter.attributes.targetcalendar.valType).toEqual('enumerated'); + expect(isValid).toBe(true); + } }); + }); + + it("deprecated attributes should have a `valType` and `role`", function() { + var DEPRECATED = "_deprecated"; - it('should list correct defs', function() { - expect(plotSchema.defs.valObjects).toBeDefined(); + assertPlotSchema(function(attr) { + if (isPlainObject(attr[DEPRECATED])) { + Object.keys(attr[DEPRECATED]).forEach(function(dAttrName) { + var dAttr = attr[DEPRECATED][dAttrName]; - expect(plotSchema.defs.metaKeys) - .toEqual(['_isSubplotObj', '_isLinkedToArray', '_deprecated', 'description', 'role']); + expect(VALTYPES.indexOf(dAttr.valType) !== -1).toBe(true); + expect(ROLES.indexOf(dAttr.role) !== -1).toBe(true); + }); + } }); + }); + + it("should work with registered transforms", function() { + var valObjects = plotSchema.transforms.filter.attributes, + attrNames = Object.keys(valObjects); - it('should list the correct frame attributes', function() { - expect(plotSchema.frames).toBeDefined(); - expect(plotSchema.frames.role).toEqual('object'); - expect(plotSchema.frames.items.frames_entry).toBeDefined(); - expect(plotSchema.frames.items.frames_entry.role).toEqual('object'); + ["operation", "value", "target"].forEach(function(k) { + expect(attrNames).toContain(k); }); + }); + + it("should work with registered components", function() { + expect(plotSchema.traces.scatter.attributes.xcalendar.valType).toEqual( + "enumerated" + ); + expect(plotSchema.traces.scatter3d.attributes.zcalendar.valType).toEqual( + "enumerated" + ); + + expect(plotSchema.layout.layoutAttributes.calendar.valType).toEqual( + "enumerated" + ); + expect(plotSchema.layout.layoutAttributes.xaxis.calendar.valType).toEqual( + "enumerated" + ); + expect( + plotSchema.layout.layoutAttributes.scene.xaxis.calendar.valType + ).toEqual("enumerated"); + + expect( + plotSchema.transforms.filter.attributes.valuecalendar.valType + ).toEqual("enumerated"); + expect( + plotSchema.transforms.filter.attributes.targetcalendar.valType + ).toEqual("enumerated"); + }); + + it("should list correct defs", function() { + expect(plotSchema.defs.valObjects).toBeDefined(); + + expect(plotSchema.defs.metaKeys).toEqual([ + "_isSubplotObj", + "_isLinkedToArray", + "_deprecated", + "description", + "role" + ]); + }); + + it("should list the correct frame attributes", function() { + expect(plotSchema.frames).toBeDefined(); + expect(plotSchema.frames.role).toEqual("object"); + expect(plotSchema.frames.items.frames_entry).toBeDefined(); + expect(plotSchema.frames.items.frames_entry.role).toEqual("object"); + }); }); diff --git a/test/jasmine/tests/polygon_test.js b/test/jasmine/tests/polygon_test.js index f9fc5536fd6..6ae3d832a38 100644 --- a/test/jasmine/tests/polygon_test.js +++ b/test/jasmine/tests/polygon_test.js @@ -1,214 +1,340 @@ -var polygon = require('@src/lib/polygon'), - polygonTester = polygon.tester, - isBent = polygon.isSegmentBent, - filter = polygon.filter; - -describe('polygon.tester', function() { - 'use strict'; - - var squareCW = [[0, 0], [0, 1], [1, 1], [1, 0]], - squareCCW = [[0, 0], [1, 0], [1, 1], [0, 1]], - bowtie = [[0, 0], [0, 1], [1, 0], [1, 1]], - squareish = [ - [-0.123, -0.0456], - [0.12345, 1.2345], - [1.3456, 1.4567], - [1.5678, 0.21345]], - equilateralTriangle = [ - [0, Math.sqrt(3) / 3], - [-0.5, -Math.sqrt(3) / 6], - [0.5, -Math.sqrt(3) / 6]], - - zigzag = [ // 4 * - [0, 0], [2, 1], // \-. - [0, 1], [2, 2], // 3 * * - [1, 2], [3, 3], // ,-' | - [2, 4], [4, 3], // 2 *-* | - [4, 0]], // ,-' | - // 1 *---* | - // ,-' | - // 0 *-------* - // 0 1 2 3 4 - inZigzag = [ - [0.5, 0.01], [1, 0.49], [1.5, 0.5], [2, 0.5], [2.5, 0.5], [3, 0.5], - [3.5, 0.5], [0.5, 1.01], [1, 1.49], [1.5, 1.5], [2, 1.5], [2.5, 1.5], - [3, 1.5], [3.5, 1.5], [1.5, 2.01], [2, 2.49], [2.5, 2.5], [3, 2.5], - [3.5, 2.5], [2.5, 3.51], [3, 3.49]], - notInZigzag = [ - [0, -0.01], [0, 0.01], [0, 0.99], [0, 1.01], [0.5, -0.01], [0.5, 0.26], - [0.5, 0.99], [0.5, 1.26], [1, -0.01], [1, 0.51], [1, 0.99], [1, 1.51], - [1, 1.99], [1, 2.01], [2, -0.01], [2, 2.51], [2, 3.99], [2, 4.01], - [3, -0.01], [2.99, 3], [3, 3.51], [4, -0.01], [4, 3.01]], - - donut = [ // inner CCW, outer CW // 3 *-----* - [3, 0], [0, 0], [0, 1], [2, 1], [2, 2], // | | - [1, 2], [1, 1], [0, 1], [0, 3], [3, 3]], // 2 | *-* | - donut2 = [ // inner CCW, outer CCW // | | | | - [3, 3], [0, 3], [0, 1], [2, 1], [2, 2], // 1 *-*-* | - [1, 2], [1, 1], [0, 1], [0, 0], [3, 0]], // | | - // 0 *-----* - // 0 1 2 3 - inDonut = [[0.5, 0.5], [1, 0.5], [1.5, 0.5], [2, 0.5], [2.5, 0.5], - [2.5, 1], [2.5, 1.5], [2.5, 2], [2.5, 2.5], [2, 2.5], [1.5, 2.5], - [1, 2.5], [0.5, 2.5], [0.5, 2], [0.5, 1.5], [0.5, 1]], - notInDonut = [[1.5, -0.5], [1.5, 1.5], [1.5, 3.5], [-0.5, 1.5], [3.5, 1.5]]; - - it('should exclude points outside the bounding box', function() { - var poly = polygonTester([[1, 2], [3, 4]]); - var pts = [[0, 3], [4, 3], [2, 1], [2, 5]]; - pts.forEach(function(pt) { - expect(poly.contains(pt)).toBe(false); - expect(poly.contains(pt, true)).toBe(false); - expect(poly.contains(pt, false)).toBe(false); - }); - }); +var polygon = require("@src/lib/polygon"), + polygonTester = polygon.tester, + isBent = polygon.isSegmentBent, + filter = polygon.filter; - it('should prepare a polygon object correctly', function() { - var polyPts = [squareCW, squareCCW, bowtie, squareish, equilateralTriangle, - zigzag, donut, donut2]; - - polyPts.forEach(function(polyPt) { - var poly = polygonTester(polyPt), - xArray = polyPt.map(function(pt) { return pt[0]; }), - yArray = polyPt.map(function(pt) { return pt[1]; }); - - expect(poly.pts.length).toEqual(polyPt.length + 1); - polyPt.forEach(function(pt, i) { - expect(poly.pts[i]).toEqual(pt); - }); - expect(poly.pts[poly.pts.length - 1]).toEqual(polyPt[0]); - expect(poly.xmin).toEqual(Math.min.apply(null, xArray)); - expect(poly.xmax).toEqual(Math.max.apply(null, xArray)); - expect(poly.ymin).toEqual(Math.min.apply(null, yArray)); - expect(poly.ymax).toEqual(Math.max.apply(null, yArray)); - }); - }); +describe("polygon.tester", function() { + "use strict"; + var squareCW = [[0, 0], [0, 1], [1, 1], [1, 0]], + squareCCW = [[0, 0], [1, 0], [1, 1], [0, 1]], + bowtie = [[0, 0], [0, 1], [1, 0], [1, 1]], + squareish = [ + [-0.123, -0.0456], + [0.12345, 1.2345], + [1.3456, 1.4567], + [1.5678, 0.21345] + ], + equilateralTriangle = [ + [0, Math.sqrt(3) / 3], + [-0.5, (-Math.sqrt(3)) / 6], + [0.5, (-Math.sqrt(3)) / 6] + ], + zigzag = [ + // 4 * + [0, 0], + [2, 1], + // \-. + [0, 1], + [2, 2], + // 3 * * + [1, 2], + [3, 3], + // ,-' | + [2, 4], + [4, 3], + // 2 *-* | + [4, 0] + ], + // ,-' | + // 1 *---* | + // ,-' | + // 0 *-------* + // 0 1 2 3 4 + inZigzag = [ + [0.5, 0.01], + [1, 0.49], + [1.5, 0.5], + [2, 0.5], + [2.5, 0.5], + [3, 0.5], + [3.5, 0.5], + [0.5, 1.01], + [1, 1.49], + [1.5, 1.5], + [2, 1.5], + [2.5, 1.5], + [3, 1.5], + [3.5, 1.5], + [1.5, 2.01], + [2, 2.49], + [2.5, 2.5], + [3, 2.5], + [3.5, 2.5], + [2.5, 3.51], + [3, 3.49] + ], + notInZigzag = [ + [0, -0.01], + [0, 0.01], + [0, 0.99], + [0, 1.01], + [0.5, -0.01], + [0.5, 0.26], + [0.5, 0.99], + [0.5, 1.26], + [1, -0.01], + [1, 0.51], + [1, 0.99], + [1, 1.51], + [1, 1.99], + [1, 2.01], + [2, -0.01], + [2, 2.51], + [2, 3.99], + [2, 4.01], + [3, -0.01], + [2.99, 3], + [3, 3.51], + [4, -0.01], + [4, 3.01] + ], + donut = [ + // inner CCW, outer CW // 3 *-----* + [3, 0], + [0, 0], + [0, 1], + [2, 1], + [2, 2], + // | | + [1, 2], + [1, 1], + [0, 1], + [0, 3], + [3, 3] + ], + // 2 | *-* | + donut2 = [ + // inner CCW, outer CCW // | | | | + [3, 3], + [0, 3], + [0, 1], + [2, 1], + [2, 2], + // 1 *-*-* | + [1, 2], + [1, 1], + [0, 1], + [0, 0], + [3, 0] + ], + // | | + // 0 *-----* + // 0 1 2 3 + inDonut = [ + [0.5, 0.5], + [1, 0.5], + [1.5, 0.5], + [2, 0.5], + [2.5, 0.5], + [2.5, 1], + [2.5, 1.5], + [2.5, 2], + [2.5, 2.5], + [2, 2.5], + [1.5, 2.5], + [1, 2.5], + [0.5, 2.5], + [0.5, 2], + [0.5, 1.5], + [0.5, 1] + ], + notInDonut = [[1.5, -0.5], [1.5, 1.5], [1.5, 3.5], [-0.5, 1.5], [3.5, 1.5]]; - it('should include the whole boundary, except as per omitFirstEdge', function() { - var polyPts = [squareCW, squareCCW, bowtie, squareish, equilateralTriangle, - zigzag, donut, donut2]; - var np = 6; // number of intermediate points on each edge to test - - polyPts.forEach(function(polyPt) { - var poly = polygonTester(polyPt); - - var isRect = polyPt === squareCW || polyPt === squareCCW; - expect(poly.isRect).toBe(isRect); - // to make sure we're only using the bounds and first pt, delete the rest - if(isRect) poly.pts.splice(1, poly.pts.length); - - poly.pts.forEach(function(pt1, i) { - if(!i) return; - var pt0 = poly.pts[i - 1], - j; - - var testPts = [pt0, pt1]; - for(j = 1; j < np; j++) { - if(pt0[0] === pt1[0]) { - testPts.push([pt0[0], pt0[1] + (pt1[1] - pt0[1]) * j / np]); - } - else { - var x = pt0[0] + (pt1[0] - pt0[0]) * j / np; - // calculated the same way as in the pt_in_polygon source, - // so we know rounding errors will apply the same and this pt - // *really* appears on the boundary - testPts.push([x, pt0[1] + (x - pt0[0]) * (pt1[1] - pt0[1]) / - (pt1[0] - pt0[0])]); - } - } - testPts.forEach(function(pt, j) { - expect(poly.contains(pt)) - .toBe(true, 'poly: ' + polyPt.join(';') + ', pt: ' + pt); - var isFirstEdge = (i === 1) || (i === 2 && j === 0) || - (i === poly.pts.length - 1 && j === 1); - expect(poly.contains(pt, true)) - .toBe(!isFirstEdge, 'omit: ' + !isFirstEdge + ', poly: ' + - polyPt.join(';') + ', pt: ' + pt); - }); - }); - }); + it("should exclude points outside the bounding box", function() { + var poly = polygonTester([[1, 2], [3, 4]]); + var pts = [[0, 3], [4, 3], [2, 1], [2, 5]]; + pts.forEach(function(pt) { + expect(poly.contains(pt)).toBe(false); + expect(poly.contains(pt, true)).toBe(false); + expect(poly.contains(pt, false)).toBe(false); }); + }); - it('should find only the right interior points', function() { - var zzpoly = polygonTester(zigzag); - inZigzag.forEach(function(pt) { - expect(zzpoly.contains(pt)).toBe(true); - }); - notInZigzag.forEach(function(pt) { - expect(zzpoly.contains(pt)).toBe(false); - }); + it("should prepare a polygon object correctly", function() { + var polyPts = [ + squareCW, + squareCCW, + bowtie, + squareish, + equilateralTriangle, + zigzag, + donut, + donut2 + ]; - var donutpoly = polygonTester(donut), - donut2poly = polygonTester(donut2); - inDonut.forEach(function(pt) { - expect(donutpoly.contains(pt)).toBe(true); - expect(donut2poly.contains(pt)).toBe(true); - }); - notInDonut.forEach(function(pt) { - expect(donutpoly.contains(pt)).toBe(false); - expect(donut2poly.contains(pt)).toBe(false); + polyPts.forEach(function(polyPt) { + var poly = polygonTester(polyPt), + xArray = polyPt.map(function(pt) { + return pt[0]; + }), + yArray = polyPt.map(function(pt) { + return pt[1]; }); + + expect(poly.pts.length).toEqual(polyPt.length + 1); + polyPt.forEach(function(pt, i) { + expect(poly.pts[i]).toEqual(pt); + }); + expect(poly.pts[poly.pts.length - 1]).toEqual(polyPt[0]); + expect(poly.xmin).toEqual(Math.min.apply(null, xArray)); + expect(poly.xmax).toEqual(Math.max.apply(null, xArray)); + expect(poly.ymin).toEqual(Math.min.apply(null, yArray)); + expect(poly.ymax).toEqual(Math.max.apply(null, yArray)); }); -}); + }); -describe('polygon.isSegmentBent', function() { - 'use strict'; + it( + "should include the whole boundary, except as per omitFirstEdge", + function() { + var polyPts = [ + squareCW, + squareCCW, + bowtie, + squareish, + equilateralTriangle, + zigzag, + donut, + donut2 + ]; + var np = 6; - var pts = [[0, 0], [1, 1], [2, 0], [1, 0], [100, -37]]; + // number of intermediate points on each edge to test + polyPts.forEach(function(polyPt) { + var poly = polygonTester(polyPt); - it('should treat any two points as straight', function() { - for(var i = 0; i < pts.length - 1; i++) { - expect(isBent(pts, i, i + 1, 0)).toBe(false); - } - }); + var isRect = polyPt === squareCW || polyPt === squareCCW; + expect(poly.isRect).toBe(isRect); + // to make sure we're only using the bounds and first pt, delete the rest + if (isRect) poly.pts.splice(1, poly.pts.length); + + poly.pts.forEach(function(pt1, i) { + if (!i) return; + var pt0 = poly.pts[i - 1], j; - function rotatePt(theta) { - return function(pt) { - return [ - pt[0] * Math.cos(theta) - pt[1] * Math.sin(theta), - pt[0] * Math.sin(theta) + pt[1] * Math.cos(theta)]; - }; + var testPts = [pt0, pt1]; + for (j = 1; j < np; j++) { + if (pt0[0] === pt1[0]) { + testPts.push([pt0[0], pt0[1] + (pt1[1] - pt0[1]) * j / np]); + } else { + var x = pt0[0] + (pt1[0] - pt0[0]) * j / np; + // calculated the same way as in the pt_in_polygon source, + // so we know rounding errors will apply the same and this pt + // *really* appears on the boundary + testPts.push([ + x, + pt0[1] + (x - pt0[0]) * (pt1[1] - pt0[1]) / (pt1[0] - pt0[0]) + ]); + } + } + testPts.forEach(function(pt, j) { + expect(poly.contains(pt)).toBe( + true, + "poly: " + polyPt.join(";") + ", pt: " + pt + ); + var isFirstEdge = i === 1 || + i === 2 && j === 0 || + i === poly.pts.length - 1 && j === 1; + expect(poly.contains(pt, true)).toBe( + !isFirstEdge, + "omit: " + + !isFirstEdge + + ", poly: " + + polyPt.join(";") + + ", pt: " + + pt + ); + }); + }); + }); } + ); - it('should find a bent line at the right tolerance', function() { - for(var theta = 0; theta < 6; theta += 0.3) { - var pts2 = pts.map(rotatePt(theta)); - expect(isBent(pts2, 0, 2, 0.99)).toBe(true); - expect(isBent(pts2, 0, 2, 1.01)).toBe(false); - } + it("should find only the right interior points", function() { + var zzpoly = polygonTester(zigzag); + inZigzag.forEach(function(pt) { + expect(zzpoly.contains(pt)).toBe(true); + }); + notInZigzag.forEach(function(pt) { + expect(zzpoly.contains(pt)).toBe(false); }); - it('should treat any backward motion as bent', function() { - expect(isBent([[0, 0], [2, 0], [1, 0]], 0, 2, 10)).toBe(true); + var donutpoly = polygonTester(donut), donut2poly = polygonTester(donut2); + inDonut.forEach(function(pt) { + expect(donutpoly.contains(pt)).toBe(true); + expect(donut2poly.contains(pt)).toBe(true); + }); + notInDonut.forEach(function(pt) { + expect(donutpoly.contains(pt)).toBe(false); + expect(donut2poly.contains(pt)).toBe(false); }); + }); }); -describe('polygon.filter', function() { - 'use strict'; +describe("polygon.isSegmentBent", function() { + "use strict"; + var pts = [[0, 0], [1, 1], [2, 0], [1, 0], [100, -37]]; - var pts = [ - [0, 0], [1, 0], [2, 0], [3, 0], - [3, 1], [3, 2], [3, 3], - [2, 3], [1, 3], [0, 3], - [0, 2], [0, 1], [0, 0]]; + it("should treat any two points as straight", function() { + for (var i = 0; i < pts.length - 1; i++) { + expect(isBent(pts, i, i + 1, 0)).toBe(false); + } + }); - var ptsOut = [[0, 0], [3, 0], [3, 3], [0, 3], [0, 0]]; + function rotatePt(theta) { + return function(pt) { + return [ + pt[0] * Math.cos(theta) - pt[1] * Math.sin(theta), + pt[0] * Math.sin(theta) + pt[1] * Math.cos(theta) + ]; + }; + } - it('should give the right result if points are provided upfront', function() { - expect(filter(pts, 0.5).filtered).toEqual(ptsOut); - }); + it("should find a bent line at the right tolerance", function() { + for (var theta = 0; theta < 6; theta += 0.3) { + var pts2 = pts.map(rotatePt(theta)); + expect(isBent(pts2, 0, 2, 0.99)).toBe(true); + expect(isBent(pts2, 0, 2, 1.01)).toBe(false); + } + }); - it('should give the right result if points are added one-by-one', function() { - var p = filter([pts[0]], 0.5), - i; + it("should treat any backward motion as bent", function() { + expect(isBent([[0, 0], [2, 0], [1, 0]], 0, 2, 10)).toBe(true); + }); +}); - // intermediate result (the last point isn't in the final) - for(i = 1; i < 6; i++) p.addPt(pts[i]); - expect(p.filtered).toEqual([[0, 0], [3, 0], [3, 2]]); +describe("polygon.filter", function() { + "use strict"; + var pts = [ + [0, 0], + [1, 0], + [2, 0], + [3, 0], + [3, 1], + [3, 2], + [3, 3], + [2, 3], + [1, 3], + [0, 3], + [0, 2], + [0, 1], + [0, 0] + ]; - // final result - for(i = 6; i < pts.length; i++) p.addPt(pts[i]); - expect(p.filtered).toEqual(ptsOut); - }); + var ptsOut = [[0, 0], [3, 0], [3, 3], [0, 3], [0, 0]]; + + it("should give the right result if points are provided upfront", function() { + expect(filter(pts, 0.5).filtered).toEqual(ptsOut); + }); + + it("should give the right result if points are added one-by-one", function() { + var p = filter([pts[0]], 0.5), i; + + // intermediate result (the last point isn't in the final) + for (i = 1; i < 6; i++) p.addPt(pts[i]); + expect(p.filtered).toEqual([[0, 0], [3, 0], [3, 2]]); + // final result + for (i = 6; i < pts.length; i++) p.addPt(pts[i]); + expect(p.filtered).toEqual(ptsOut); + }); }); diff --git a/test/jasmine/tests/range_selector_test.js b/test/jasmine/tests/range_selector_test.js index 1a306e4b24d..c481b7eabb8 100644 --- a/test/jasmine/tests/range_selector_test.js +++ b/test/jasmine/tests/range_selector_test.js @@ -1,596 +1,538 @@ -var RangeSelector = require('@src/components/rangeselector'); -var getUpdateObject = require('@src/components/rangeselector/get_update_object'); - -var d3 = require('d3'); -var Plotly = require('@lib'); -var Lib = require('@src/lib'); -var Color = require('@src/components/color'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var getRectCenter = require('../assets/get_rect_center'); -var mouseEvent = require('../assets/mouse_event'); -var setConvert = require('@src/plots/cartesian/set_convert'); - - -describe('range selector defaults:', function() { - 'use strict'; - - var handleDefaults = RangeSelector.handleDefaults; - - function supply(containerIn, containerOut, calendar) { - containerOut.domain = [0, 1]; - - var layout = { - yaxis: { domain: [0, 1] } - }; - - var counterAxes = ['yaxis']; - - handleDefaults(containerIn, containerOut, layout, counterAxes, calendar); +var RangeSelector = require("@src/components/rangeselector"); +var getUpdateObject = require( + "@src/components/rangeselector/get_update_object" +); + +var d3 = require("d3"); +var Plotly = require("@lib"); +var Lib = require("@src/lib"); +var Color = require("@src/components/color"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var getRectCenter = require("../assets/get_rect_center"); +var mouseEvent = require("../assets/mouse_event"); +var setConvert = require("@src/plots/cartesian/set_convert"); + +describe("range selector defaults:", function() { + "use strict"; + var handleDefaults = RangeSelector.handleDefaults; + + function supply(containerIn, containerOut, calendar) { + containerOut.domain = [0, 1]; + + var layout = { yaxis: { domain: [0, 1] } }; + + var counterAxes = ["yaxis"]; + + handleDefaults(containerIn, containerOut, layout, counterAxes, calendar); + } + + it("should set 'visible' to false when no buttons are present", function() { + var containerIn = {}; + var containerOut = {}; + + supply(containerIn, containerOut); + + expect(containerOut.rangeselector).toEqual({ visible: false, buttons: [] }); + }); + + it("should coerce an empty button object", function() { + var containerIn = { rangeselector: { buttons: [{}] } }; + var containerOut = {}; + + supply(containerIn, containerOut); + + expect(containerIn.rangeselector.buttons).toEqual([{}]); + expect(containerOut.rangeselector.buttons).toEqual([ + { step: "month", stepmode: "backward", count: 1, _index: 0 } + ]); + }); + + it("should skip over non-object buttons", function() { + var containerIn = { + rangeselector: { + buttons: [ + { label: "button 0" }, + null, + { label: "button 2" }, + "remove", + { label: "button 4" } + ] + } + }; + var containerOut = {}; + + supply(containerIn, containerOut); + + expect(containerIn.rangeselector.buttons.length).toEqual(5); + expect(containerOut.rangeselector.buttons.length).toEqual(3); + }); + + it("should coerce all buttons present", function() { + var containerIn = { + rangeselector: { buttons: [{ step: "year", count: 10 }, { count: 6 }] } + }; + var containerOut = {}; + + supply(containerIn, containerOut); + + expect(containerOut.rangeselector.visible).toBe(true); + expect(containerOut.rangeselector.buttons).toEqual([ + { step: "year", stepmode: "backward", count: 10, _index: 0 }, + { step: "month", stepmode: "backward", count: 6, _index: 1 } + ]); + }); + + it( + "should not coerce 'stepmode' and 'count', for 'step' all buttons", + function() { + var containerIn = { + rangeselector: { buttons: [{ step: "all", label: "full range" }] } + }; + var containerOut = {}; + + supply(containerIn, containerOut); + + expect(containerOut.rangeselector.buttons).toEqual([ + { step: "all", label: "full range", _index: 0 } + ]); } - - it('should set \'visible\' to false when no buttons are present', function() { - var containerIn = {}; - var containerOut = {}; - - supply(containerIn, containerOut); - - expect(containerOut.rangeselector) - .toEqual({ - visible: false, - buttons: [] - }); - }); - - it('should coerce an empty button object', function() { - var containerIn = { - rangeselector: { - buttons: [{}] - } - }; - var containerOut = {}; - - supply(containerIn, containerOut); - - expect(containerIn.rangeselector.buttons).toEqual([{}]); - expect(containerOut.rangeselector.buttons).toEqual([{ - step: 'month', - stepmode: 'backward', - count: 1, - _index: 0 - }]); - }); - - it('should skip over non-object buttons', function() { - var containerIn = { - rangeselector: { - buttons: [{ - label: 'button 0' - }, null, { - label: 'button 2' - }, 'remove', { - label: 'button 4' - }] - } - }; - var containerOut = {}; - - supply(containerIn, containerOut); - - expect(containerIn.rangeselector.buttons.length).toEqual(5); - expect(containerOut.rangeselector.buttons.length).toEqual(3); - }); - - it('should coerce all buttons present', function() { - var containerIn = { - rangeselector: { - buttons: [{ - step: 'year', - count: 10 - }, { - count: 6 - }] - } - }; - var containerOut = {}; - - supply(containerIn, containerOut); - - expect(containerOut.rangeselector.visible).toBe(true); - expect(containerOut.rangeselector.buttons).toEqual([ - { step: 'year', stepmode: 'backward', count: 10, _index: 0 }, - { step: 'month', stepmode: 'backward', count: 6, _index: 1 } - ]); - }); - - it('should not coerce \'stepmode\' and \'count\', for \'step\' all buttons', function() { - var containerIn = { - rangeselector: { - buttons: [{ - step: 'all', - label: 'full range' - }] - } - }; - var containerOut = {}; - - supply(containerIn, containerOut); - - expect(containerOut.rangeselector.buttons).toEqual([{ - step: 'all', - label: 'full range', - _index: 0 - }]); - }); - - it('should use axis and counter axis to determine \'x\' and \'y\' defaults (case 1 y)', function() { - var containerIn = { - rangeselector: { buttons: [{}] } - }; - var containerOut = { - _id: 'x', - domain: [0, 0.5] - }; - var layout = { - xaxis: containerIn, - yaxis: { - anchor: 'x', - domain: [0, 0.45] - } - }; - var counterAxes = ['yaxis']; - - handleDefaults(containerIn, containerOut, layout, counterAxes); - - expect(containerOut.rangeselector.x).toEqual(0); - expect(containerOut.rangeselector.y).toBeCloseTo(0.47); - }); - - it('should use axis and counter axis to determine \'x\' and \'y\' defaults (case multi y)', function() { - var containerIn = { - rangeselector: { buttons: [{}] } - }; - var containerOut = { - _id: 'x', - domain: [0.5, 1] - }; - var layout = { - xaxis: containerIn, - yaxis: { - anchor: 'x', - domain: [0, 0.25] - }, - yaxis2: { - anchor: 'x', - overlaying: 'y' - }, - yaxis3: { - anchor: 'x', - domain: [0.6, 0.85] - } - }; - var counterAxes = ['yaxis', 'yaxis2', 'yaxis3']; - - handleDefaults(containerIn, containerOut, layout, counterAxes); - - expect(containerOut.rangeselector.x).toEqual(0.5); - expect(containerOut.rangeselector.y).toBeCloseTo(0.87); - }); - - it('should not allow month/year todate with calendars other than Gregorian', function() { - var containerIn = { - rangeselector: { - buttons: [{ - step: 'year', - count: 1, - stepmode: 'todate' - }, { - step: 'month', - count: 6, - stepmode: 'todate' - }, { - step: 'day', - count: 1, - stepmode: 'todate' - }, { - step: 'hour', - count: 1, - stepmode: 'todate' - }] - } - }; - var containerOut; - function getStepmode(button) { return button.stepmode; } - - containerOut = {}; - supply(containerIn, containerOut); - - expect(containerOut.rangeselector.buttons.map(getStepmode)).toEqual([ - 'todate', 'todate', 'todate', 'todate' - ]); - - containerOut = {}; - supply(containerIn, containerOut, 'gregorian'); - - expect(containerOut.rangeselector.buttons.map(getStepmode)).toEqual([ - 'todate', 'todate', 'todate', 'todate' - ]); - - containerOut = {}; - supply(containerIn, containerOut, 'chinese'); - - expect(containerOut.rangeselector.buttons.map(getStepmode)).toEqual([ - 'backward', 'backward', 'todate', 'todate' - ]); - }); -}); - -describe('range selector getUpdateObject:', function() { - 'use strict'; - - function assertRanges(update, range0, range1) { - expect(update['xaxis.range[0]']).toEqual(range0); - expect(update['xaxis.range[1]']).toEqual(range1); + ); + + it( + "should use axis and counter axis to determine 'x' and 'y' defaults (case 1 y)", + function() { + var containerIn = { rangeselector: { buttons: [{}] } }; + var containerOut = { _id: "x", domain: [0, 0.5] }; + var layout = { + xaxis: containerIn, + yaxis: { anchor: "x", domain: [0, 0.45] } + }; + var counterAxes = ["yaxis"]; + + handleDefaults(containerIn, containerOut, layout, counterAxes); + + expect(containerOut.rangeselector.x).toEqual(0); + expect(containerOut.rangeselector.y).toBeCloseTo(0.47); } - - function setupAxis(opts) { - var axisOut = Lib.extendFlat({type: 'date'}, opts); - setConvert(axisOut); - return axisOut; + ); + + it( + "should use axis and counter axis to determine 'x' and 'y' defaults (case multi y)", + function() { + var containerIn = { rangeselector: { buttons: [{}] } }; + var containerOut = { _id: "x", domain: [0.5, 1] }; + var layout = { + xaxis: containerIn, + yaxis: { anchor: "x", domain: [0, 0.25] }, + yaxis2: { anchor: "x", overlaying: "y" }, + yaxis3: { anchor: "x", domain: [0.6, 0.85] } + }; + var counterAxes = ["yaxis", "yaxis2", "yaxis3"]; + + handleDefaults(containerIn, containerOut, layout, counterAxes); + + expect(containerOut.rangeselector.x).toEqual(0.5); + expect(containerOut.rangeselector.y).toBeCloseTo(0.87); } - - // buttonLayout: {step, stepmode, count} - // range0out: expected resulting range[0] (input is always '1948-01-01') - // range1: input range[1], expected to also be the output - function assertUpdateCase(buttonLayout, range0out, range1) { - var axisLayout = setupAxis({ - _name: 'xaxis', - range: ['1948-01-01', range1] - }); - - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, range0out, range1); + ); + + it( + "should not allow month/year todate with calendars other than Gregorian", + function() { + var containerIn = { + rangeselector: { + buttons: [ + { step: "year", count: 1, stepmode: "todate" }, + { step: "month", count: 6, stepmode: "todate" }, + { step: "day", count: 1, stepmode: "todate" }, + { step: "hour", count: 1, stepmode: "todate" } + ] + } + }; + var containerOut; + function getStepmode(button) { + return button.stepmode; + } + + containerOut = {}; + supply(containerIn, containerOut); + + expect(containerOut.rangeselector.buttons.map(getStepmode)).toEqual([ + "todate", + "todate", + "todate", + "todate" + ]); + + containerOut = {}; + supply(containerIn, containerOut, "gregorian"); + + expect(containerOut.rangeselector.buttons.map(getStepmode)).toEqual([ + "todate", + "todate", + "todate", + "todate" + ]); + + containerOut = {}; + supply(containerIn, containerOut, "chinese"); + + expect(containerOut.rangeselector.buttons.map(getStepmode)).toEqual([ + "backward", + "backward", + "todate", + "todate" + ]); } - - it('should return update object (1 month backward case)', function() { - var buttonLayout = { - step: 'month', - stepmode: 'backward', - count: 1 - }; - - assertUpdateCase(buttonLayout, '2015-10-30', '2015-11-30'); - assertUpdateCase(buttonLayout, '2015-10-30 12:34:56', '2015-11-30 12:34:56'); - }); - - it('should return update object (3 months backward case)', function() { - var buttonLayout = { - step: 'month', - stepmode: 'backward', - count: 3 - }; - - assertUpdateCase(buttonLayout, '2015-08-30', '2015-11-30'); - assertUpdateCase(buttonLayout, '2015-08-30 12:34:56', '2015-11-30 12:34:56'); - }); - - it('should return update object (6 months backward case)', function() { - var buttonLayout = { - step: 'month', - stepmode: 'backward', - count: 6 - }; - - assertUpdateCase(buttonLayout, '2015-05-30', '2015-11-30'); - assertUpdateCase(buttonLayout, '2015-05-30 12:34:56', '2015-11-30 12:34:56'); - }); - - it('should return update object (5 months to-date case)', function() { - var buttonLayout = { - step: 'month', - stepmode: 'todate', - count: 5 - }; - - assertUpdateCase(buttonLayout, '2015-07-01', '2015-11-30'); - assertUpdateCase(buttonLayout, '2015-07-01', '2015-12-01'); - assertUpdateCase(buttonLayout, '2015-08-01', '2015-12-01 00:00:01'); - }); - - it('should return update object (1 year to-date case)', function() { - var buttonLayout = { - step: 'year', - stepmode: 'todate', - count: 1 - }; - - assertUpdateCase(buttonLayout, '2015-01-01', '2015-11-30'); - assertUpdateCase(buttonLayout, '2015-01-01', '2016-01-01'); - assertUpdateCase(buttonLayout, '2016-01-01', '2016-01-01 00:00:01'); - }); - - it('should return update object (10 year to-date case)', function() { - var buttonLayout = { - step: 'year', - stepmode: 'todate', - count: 10 - }; - - assertUpdateCase(buttonLayout, '2006-01-01', '2015-11-30'); - assertUpdateCase(buttonLayout, '2006-01-01', '2016-01-01'); - assertUpdateCase(buttonLayout, '2007-01-01', '2016-01-01 00:00:01'); - }); - - it('should return update object (1 year backward case)', function() { - var buttonLayout = { - step: 'year', - stepmode: 'backward', - count: 1 - }; - - assertUpdateCase(buttonLayout, '2014-11-30', '2015-11-30'); - assertUpdateCase(buttonLayout, '2014-11-30 12:34:56', '2015-11-30 12:34:56'); - }); - - it('should return update object (reset case)', function() { - var axisLayout = setupAxis({ - _name: 'xaxis', - range: ['1948-01-01', '2015-11-30'] - }); - - var buttonLayout = { - step: 'all' - }; - - var update = getUpdateObject(axisLayout, buttonLayout); - - expect(update).toEqual({'xaxis.autorange': true}); - }); - - it('should return update object (10 day backward case)', function() { - var buttonLayout = { - step: 'day', - stepmode: 'backward', - count: 10 - }; - - assertUpdateCase(buttonLayout, '2015-11-20', '2015-11-30'); - assertUpdateCase(buttonLayout, '2015-11-20 12:34:56', '2015-11-30 12:34:56'); - }); - - it('should return update object (5 hour backward case)', function() { - var buttonLayout = { - step: 'hour', - stepmode: 'backward', - count: 5 - }; - - assertUpdateCase(buttonLayout, '2015-11-29 19:00', '2015-11-30'); - assertUpdateCase(buttonLayout, '2015-11-30 07:34:56', '2015-11-30 12:34:56'); - }); - - it('should return update object (15 minute backward case)', function() { - var buttonLayout = { - step: 'minute', - stepmode: 'backward', - count: 15 - }; - - assertUpdateCase(buttonLayout, '2015-11-29 23:45', '2015-11-30'); - assertUpdateCase(buttonLayout, '2015-11-30 12:19:56', '2015-11-30 12:34:56'); - }); - - it('should return update object (10 second backward case)', function() { - var buttonLayout = { - step: 'second', - stepmode: 'backward', - count: 10 - }; - - assertUpdateCase(buttonLayout, '2015-11-29 23:59:50', '2015-11-30'); - assertUpdateCase(buttonLayout, '2015-11-30 12:34:46', '2015-11-30 12:34:56'); - }); - - it('should return update object (12 hour to-date case)', function() { - var buttonLayout = { - step: 'hour', - stepmode: 'todate', - count: 12 - }; - - assertUpdateCase(buttonLayout, '2015-11-30', '2015-11-30 12'); - assertUpdateCase(buttonLayout, '2015-11-30 01:00', '2015-11-30 12:00:01'); - assertUpdateCase(buttonLayout, '2015-11-30 01:00', '2015-11-30 13'); - }); - - it('should return update object (20 minute to-date case)', function() { - var buttonLayout = { - step: 'minute', - stepmode: 'todate', - count: 20 - }; - - assertUpdateCase(buttonLayout, '2015-11-30 12:00', '2015-11-30 12:20'); - assertUpdateCase(buttonLayout, '2015-11-30 12:01', '2015-11-30 12:20:01'); - assertUpdateCase(buttonLayout, '2015-11-30 12:01', '2015-11-30 12:21'); - }); - - it('should return update object (2 second to-date case)', function() { - var buttonLayout = { - step: 'second', - stepmode: 'todate', - count: 2 - }; - - assertUpdateCase(buttonLayout, '2015-11-30 12:20', '2015-11-30 12:20:02'); - assertUpdateCase(buttonLayout, '2015-11-30 12:20:01', '2015-11-30 12:20:02.001'); - assertUpdateCase(buttonLayout, '2015-11-30 12:20:01', '2015-11-30 12:20:03'); - }); - - it('should return update object with correct axis names', function() { - var axisLayout = setupAxis({ - _name: 'xaxis5', - range: ['1948-01-01', '2015-11-30'] - }); - - var buttonLayout = { - step: 'month', - stepmode: 'backward', - count: 1 - }; - - var update = getUpdateObject(axisLayout, buttonLayout); - - expect(update).toEqual({ - 'xaxis5.range[0]': '2015-10-30', - 'xaxis5.range[1]': '2015-11-30' - }); - - }); + ); }); -describe('range selector interactions:', function() { - 'use strict'; - - var mock = require('@mocks/range_selector.json'); - - var gd, mockCopy; - - beforeEach(function(done) { - gd = createGraphDiv(); - mockCopy = Lib.extendDeep({}, mock); +describe("range selector getUpdateObject:", function() { + "use strict"; + function assertRanges(update, range0, range1) { + expect(update["xaxis.range[0]"]).toEqual(range0); + expect(update["xaxis.range[1]"]).toEqual(range1); + } + + function setupAxis(opts) { + var axisOut = Lib.extendFlat({ type: "date" }, opts); + setConvert(axisOut); + return axisOut; + } + + // buttonLayout: {step, stepmode, count} + // range0out: expected resulting range[0] (input is always '1948-01-01') + // range1: input range[1], expected to also be the output + function assertUpdateCase(buttonLayout, range0out, range1) { + var axisLayout = setupAxis({ + _name: "xaxis", + range: ["1948-01-01", range1] + }); + + var update = getUpdateObject(axisLayout, buttonLayout); + + assertRanges(update, range0out, range1); + } + + it("should return update object (1 month backward case)", function() { + var buttonLayout = { step: "month", stepmode: "backward", count: 1 }; + + assertUpdateCase(buttonLayout, "2015-10-30", "2015-11-30"); + assertUpdateCase( + buttonLayout, + "2015-10-30 12:34:56", + "2015-11-30 12:34:56" + ); + }); + + it("should return update object (3 months backward case)", function() { + var buttonLayout = { step: "month", stepmode: "backward", count: 3 }; + + assertUpdateCase(buttonLayout, "2015-08-30", "2015-11-30"); + assertUpdateCase( + buttonLayout, + "2015-08-30 12:34:56", + "2015-11-30 12:34:56" + ); + }); + + it("should return update object (6 months backward case)", function() { + var buttonLayout = { step: "month", stepmode: "backward", count: 6 }; + + assertUpdateCase(buttonLayout, "2015-05-30", "2015-11-30"); + assertUpdateCase( + buttonLayout, + "2015-05-30 12:34:56", + "2015-11-30 12:34:56" + ); + }); + + it("should return update object (5 months to-date case)", function() { + var buttonLayout = { step: "month", stepmode: "todate", count: 5 }; + + assertUpdateCase(buttonLayout, "2015-07-01", "2015-11-30"); + assertUpdateCase(buttonLayout, "2015-07-01", "2015-12-01"); + assertUpdateCase(buttonLayout, "2015-08-01", "2015-12-01 00:00:01"); + }); + + it("should return update object (1 year to-date case)", function() { + var buttonLayout = { step: "year", stepmode: "todate", count: 1 }; + + assertUpdateCase(buttonLayout, "2015-01-01", "2015-11-30"); + assertUpdateCase(buttonLayout, "2015-01-01", "2016-01-01"); + assertUpdateCase(buttonLayout, "2016-01-01", "2016-01-01 00:00:01"); + }); + + it("should return update object (10 year to-date case)", function() { + var buttonLayout = { step: "year", stepmode: "todate", count: 10 }; + + assertUpdateCase(buttonLayout, "2006-01-01", "2015-11-30"); + assertUpdateCase(buttonLayout, "2006-01-01", "2016-01-01"); + assertUpdateCase(buttonLayout, "2007-01-01", "2016-01-01 00:00:01"); + }); + + it("should return update object (1 year backward case)", function() { + var buttonLayout = { step: "year", stepmode: "backward", count: 1 }; + + assertUpdateCase(buttonLayout, "2014-11-30", "2015-11-30"); + assertUpdateCase( + buttonLayout, + "2014-11-30 12:34:56", + "2015-11-30 12:34:56" + ); + }); + + it("should return update object (reset case)", function() { + var axisLayout = setupAxis({ + _name: "xaxis", + range: ["1948-01-01", "2015-11-30"] + }); + + var buttonLayout = { step: "all" }; + + var update = getUpdateObject(axisLayout, buttonLayout); + + expect(update).toEqual({ "xaxis.autorange": true }); + }); + + it("should return update object (10 day backward case)", function() { + var buttonLayout = { step: "day", stepmode: "backward", count: 10 }; + + assertUpdateCase(buttonLayout, "2015-11-20", "2015-11-30"); + assertUpdateCase( + buttonLayout, + "2015-11-20 12:34:56", + "2015-11-30 12:34:56" + ); + }); + + it("should return update object (5 hour backward case)", function() { + var buttonLayout = { step: "hour", stepmode: "backward", count: 5 }; + + assertUpdateCase(buttonLayout, "2015-11-29 19:00", "2015-11-30"); + assertUpdateCase( + buttonLayout, + "2015-11-30 07:34:56", + "2015-11-30 12:34:56" + ); + }); + + it("should return update object (15 minute backward case)", function() { + var buttonLayout = { step: "minute", stepmode: "backward", count: 15 }; + + assertUpdateCase(buttonLayout, "2015-11-29 23:45", "2015-11-30"); + assertUpdateCase( + buttonLayout, + "2015-11-30 12:19:56", + "2015-11-30 12:34:56" + ); + }); + + it("should return update object (10 second backward case)", function() { + var buttonLayout = { step: "second", stepmode: "backward", count: 10 }; + + assertUpdateCase(buttonLayout, "2015-11-29 23:59:50", "2015-11-30"); + assertUpdateCase( + buttonLayout, + "2015-11-30 12:34:46", + "2015-11-30 12:34:56" + ); + }); + + it("should return update object (12 hour to-date case)", function() { + var buttonLayout = { step: "hour", stepmode: "todate", count: 12 }; + + assertUpdateCase(buttonLayout, "2015-11-30", "2015-11-30 12"); + assertUpdateCase(buttonLayout, "2015-11-30 01:00", "2015-11-30 12:00:01"); + assertUpdateCase(buttonLayout, "2015-11-30 01:00", "2015-11-30 13"); + }); + + it("should return update object (20 minute to-date case)", function() { + var buttonLayout = { step: "minute", stepmode: "todate", count: 20 }; + + assertUpdateCase(buttonLayout, "2015-11-30 12:00", "2015-11-30 12:20"); + assertUpdateCase(buttonLayout, "2015-11-30 12:01", "2015-11-30 12:20:01"); + assertUpdateCase(buttonLayout, "2015-11-30 12:01", "2015-11-30 12:21"); + }); + + it("should return update object (2 second to-date case)", function() { + var buttonLayout = { step: "second", stepmode: "todate", count: 2 }; + + assertUpdateCase(buttonLayout, "2015-11-30 12:20", "2015-11-30 12:20:02"); + assertUpdateCase( + buttonLayout, + "2015-11-30 12:20:01", + "2015-11-30 12:20:02.001" + ); + assertUpdateCase( + buttonLayout, + "2015-11-30 12:20:01", + "2015-11-30 12:20:03" + ); + }); + + it("should return update object with correct axis names", function() { + var axisLayout = setupAxis({ + _name: "xaxis5", + range: ["1948-01-01", "2015-11-30"] + }); + + var buttonLayout = { step: "month", stepmode: "backward", count: 1 }; + + var update = getUpdateObject(axisLayout, buttonLayout); + + expect(update).toEqual({ + "xaxis5.range[0]": "2015-10-30", + "xaxis5.range[1]": "2015-11-30" + }); + }); +}); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); +describe("range selector interactions:", function() { + "use strict"; + var mock = require("@mocks/range_selector.json"); - afterEach(destroyGraphDiv); + var gd, mockCopy; - function assertNodeCount(query, cnt) { - expect(d3.selectAll(query).size()).toEqual(cnt); - } + beforeEach(function(done) { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); - function checkActiveButton(activeIndex, msg) { - d3.selectAll('.button').each(function(d, i) { - expect(d.isActive).toBe(activeIndex === i, msg + ': button #' + i); - }); - } + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - function checkButtonColor(bgColor, activeColor) { - d3.selectAll('.button').each(function(d) { - var rect = d3.select(this).select('rect'); + afterEach(destroyGraphDiv); - expect(rect.style('fill')).toEqual( - d.isActive ? activeColor : bgColor - ); - }); - } + function assertNodeCount(query, cnt) { + expect(d3.selectAll(query).size()).toEqual(cnt); + } - it('should display the correct nodes', function() { - assertNodeCount('.rangeselector', 1); - assertNodeCount('.button', mockCopy.layout.xaxis.rangeselector.buttons.length); + function checkActiveButton(activeIndex, msg) { + d3.selectAll(".button").each(function(d, i) { + expect(d.isActive).toBe(activeIndex === i, msg + ": button #" + i); }); + } - it('should be able to be removed by `relayout`', function(done) { - Plotly.relayout(gd, 'xaxis.rangeselector.visible', false).then(function() { - assertNodeCount('.rangeselector', 0); - assertNodeCount('.button', 0); - done(); - }); + function checkButtonColor(bgColor, activeColor) { + d3.selectAll(".button").each(function(d) { + var rect = d3.select(this).select("rect"); + expect(rect.style("fill")).toEqual(d.isActive ? activeColor : bgColor); }); + } - it('should be able to remove button(s) on `relayout`', function(done) { - var len = mockCopy.layout.xaxis.rangeselector.buttons.length; + it("should display the correct nodes", function() { + assertNodeCount(".rangeselector", 1); + assertNodeCount( + ".button", + mockCopy.layout.xaxis.rangeselector.buttons.length + ); + }); - assertNodeCount('.button', len); + it("should be able to be removed by `relayout`", function(done) { + Plotly.relayout(gd, "xaxis.rangeselector.visible", false).then(function() { + assertNodeCount(".rangeselector", 0); + assertNodeCount(".button", 0); + done(); + }); + }); - Plotly.relayout(gd, 'xaxis.rangeselector.buttons[0]', null).then(function() { - assertNodeCount('.button', len - 1); + it("should be able to remove button(s) on `relayout`", function(done) { + var len = mockCopy.layout.xaxis.rangeselector.buttons.length; - return Plotly.relayout(gd, 'xaxis.rangeselector.buttons[1]', 'remove'); - }).then(function() { - assertNodeCount('.button', len - 2); + assertNodeCount(".button", len); - done(); - }); - }); + Plotly.relayout(gd, "xaxis.rangeselector.buttons[0]", null) + .then(function() { + assertNodeCount(".button", len - 1); - it('should be able to change its style on `relayout`', function(done) { - var prefix = 'xaxis.rangeselector.'; + return Plotly.relayout(gd, "xaxis.rangeselector.buttons[1]", "remove"); + }) + .then(function() { + assertNodeCount(".button", len - 2); - checkButtonColor('rgb(238, 238, 238)', 'rgb(212, 212, 212)'); + done(); + }); + }); - Plotly.relayout(gd, prefix + 'bgcolor', 'red').then(function() { - checkButtonColor('rgb(255, 0, 0)', 'rgb(255, 128, 128)'); + it("should be able to change its style on `relayout`", function(done) { + var prefix = "xaxis.rangeselector."; - return Plotly.relayout(gd, prefix + 'activecolor', 'blue'); - }).then(function() { - checkButtonColor('rgb(255, 0, 0)', 'rgb(0, 0, 255)'); + checkButtonColor("rgb(238, 238, 238)", "rgb(212, 212, 212)"); - done(); - }); - }); + Plotly.relayout(gd, prefix + "bgcolor", "red") + .then(function() { + checkButtonColor("rgb(255, 0, 0)", "rgb(255, 128, 128)"); - it('should update range and active button when clicked', function() { - var range0 = gd.layout.xaxis.range[0]; - var buttons = d3.selectAll('.button').select('rect'); + return Plotly.relayout(gd, prefix + "activecolor", "blue"); + }) + .then(function() { + checkButtonColor("rgb(255, 0, 0)", "rgb(0, 0, 255)"); - checkActiveButton(buttons.size() - 1); + done(); + }); + }); - var pos0 = getRectCenter(buttons[0][0]); - var posReset = getRectCenter(buttons[0][buttons.size() - 1]); + it("should update range and active button when clicked", function() { + var range0 = gd.layout.xaxis.range[0]; + var buttons = d3.selectAll(".button").select("rect"); - mouseEvent('click', pos0[0], pos0[1]); - expect(gd.layout.xaxis.range[0]).toBeGreaterThan(range0); + checkActiveButton(buttons.size() - 1); - checkActiveButton(0); + var pos0 = getRectCenter(buttons[0][0]); + var posReset = getRectCenter(buttons[0][buttons.size() - 1]); - mouseEvent('click', posReset[0], posReset[1]); - expect(gd.layout.xaxis.range[0]).toEqual(range0); + mouseEvent("click", pos0[0], pos0[1]); + expect(gd.layout.xaxis.range[0]).toBeGreaterThan(range0); - checkActiveButton(buttons.size() - 1); - }); + checkActiveButton(0); - it('should change color on mouse over', function() { - var button = d3.select('.button').select('rect'); - var pos = getRectCenter(button.node()); + mouseEvent("click", posReset[0], posReset[1]); + expect(gd.layout.xaxis.range[0]).toEqual(range0); - var fillColor = Color.rgb(gd._fullLayout.xaxis.rangeselector.bgcolor); - var activeColor = 'rgb(212, 212, 212)'; + checkActiveButton(buttons.size() - 1); + }); - expect(button.style('fill')).toEqual(fillColor); + it("should change color on mouse over", function() { + var button = d3.select(".button").select("rect"); + var pos = getRectCenter(button.node()); - mouseEvent('mouseover', pos[0], pos[1]); - expect(button.style('fill')).toEqual(activeColor); + var fillColor = Color.rgb(gd._fullLayout.xaxis.rangeselector.bgcolor); + var activeColor = "rgb(212, 212, 212)"; - mouseEvent('mouseout', pos[0], pos[1]); - expect(button.style('fill')).toEqual(fillColor); - }); + expect(button.style("fill")).toEqual(fillColor); - it('should update is active relayout calls', function(done) { - var buttons = d3.selectAll('.button').select('rect'); + mouseEvent("mouseover", pos[0], pos[1]); + expect(button.style("fill")).toEqual(activeColor); - // 'all' should be active at first - checkActiveButton(buttons.size() - 1, 'initial'); + mouseEvent("mouseout", pos[0], pos[1]); + expect(button.style("fill")).toEqual(fillColor); + }); - var update = { - 'xaxis.range[0]': '2015-10-30', - 'xaxis.range[1]': '2015-11-30' - }; + it("should update is active relayout calls", function(done) { + var buttons = d3.selectAll(".button").select("rect"); - Plotly.relayout(gd, update).then(function() { + // 'all' should be active at first + checkActiveButton(buttons.size() - 1, "initial"); - // '1m' should be active after the relayout - checkActiveButton(0, '1m'); + var update = { + "xaxis.range[0]": "2015-10-30", + "xaxis.range[1]": "2015-11-30" + }; - return Plotly.relayout(gd, 'xaxis.autorange', true); - }).then(function() { + Plotly.relayout(gd, update) + .then(function() { + // '1m' should be active after the relayout + checkActiveButton(0, "1m"); - // 'all' should be after an autoscale - checkActiveButton(buttons.size() - 1, 'back to all'); + return Plotly.relayout(gd, "xaxis.autorange", true); + }) + .then(function() { + // 'all' should be after an autoscale + checkActiveButton(buttons.size() - 1, "back to all"); - done(); - }); - }); + done(); + }); + }); }); diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 0348c8cfaac..ae3d0b122dd 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -1,579 +1,602 @@ -var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); -var setConvert = require('@src/plots/cartesian/set_convert'); +var Plotly = require("@lib/index"); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); +var setConvert = require("@src/plots/cartesian/set_convert"); -var RangeSlider = require('@src/components/rangeslider'); -var constants = require('@src/components/rangeslider/constants'); -var mock = require('../../image/mocks/range_slider.json'); +var RangeSlider = require("@src/components/rangeslider"); +var constants = require("@src/components/rangeslider/constants"); +var mock = require("../../image/mocks/range_slider.json"); -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); -var customMatchers = require('../assets/custom_matchers'); +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); +var customMatchers = require("../assets/custom_matchers"); var TOL = 6; +describe("the range slider", function() { + var gd, rangeSlider, children; -describe('the range slider', function() { + var sliderY = 393; - var gd, - rangeSlider, - children; + function getRangeSlider() { + var className = constants.containerClassName; + return document.getElementsByClassName(className)[0]; + } - var sliderY = 393; + function countRangeSliderClipPaths() { + return d3 + .selectAll("defs") + .selectAll("*") + .filter(function() { + return this.id.indexOf("rangeslider") !== -1; + }) + .size(); + } - function getRangeSlider() { - var className = constants.containerClassName; - return document.getElementsByClassName(className)[0]; - } + function testTranslate1D(node, val) { + var transformParts = node.getAttribute("transform").split("("); - function countRangeSliderClipPaths() { - return d3.selectAll('defs').selectAll('*').filter(function() { - return this.id.indexOf('rangeslider') !== -1; - }).size(); - } + expect(transformParts[0]).toEqual("translate"); + expect(+transformParts[1].split(",0)")[0]).toBeWithin(val, TOL); + } - function testTranslate1D(node, val) { - var transformParts = node.getAttribute('transform').split('('); + describe("when specified as visible", function() { + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - expect(transformParts[0]).toEqual('translate'); - expect(+transformParts[1].split(',0)')[0]).toBeWithin(val, TOL); - } + beforeEach(function(done) { + gd = createGraphDiv(); - describe('when specified as visible', function() { + var mockCopy = Lib.extendDeep({}, mock); - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + rangeSlider = getRangeSlider(); + children = rangeSlider.children; - beforeEach(function(done) { - gd = createGraphDiv(); + done(); + }); + }); - var mockCopy = Lib.extendDeep({}, mock); + afterEach(destroyGraphDiv); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - rangeSlider = getRangeSlider(); - children = rangeSlider.children; + it("should be added to the DOM when specified", function() { + expect(rangeSlider).toBeDefined(); + }); - done(); - }); - }); + it("should have the correct width and height", function() { + var bg = children[0]; - afterEach(destroyGraphDiv); + var options = mock.layout.xaxis.rangeslider, + expectedWidth = gd._fullLayout._size.w + options.borderwidth; - it('should be added to the DOM when specified', function() { - expect(rangeSlider).toBeDefined(); - }); + // width incorporates border widths + expect(+bg.getAttribute("width")).toEqual(expectedWidth); + expect(+bg.getAttribute("height")).toEqual(66); + }); - it('should have the correct width and height', function() { - var bg = children[0]; + it("should have the correct style", function() { + var bg = children[0]; - var options = mock.layout.xaxis.rangeslider, - expectedWidth = gd._fullLayout._size.w + options.borderwidth; + expect(bg.getAttribute("fill")).toBe("#fafafa"); + expect(bg.getAttribute("stroke")).toBe("black"); + expect(bg.getAttribute("stroke-width")).toBe("2"); + }); - // width incorporates border widths - expect(+bg.getAttribute('width')).toEqual(expectedWidth); - expect(+bg.getAttribute('height')).toEqual(66); - }); + it("should react to resizing the minimum handle", function(done) { + var start = 85, end = 140, diff = end - start; - it('should have the correct style', function() { - var bg = children[0]; + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); - expect(bg.getAttribute('fill')).toBe('#fafafa'); - expect(bg.getAttribute('stroke')).toBe('black'); - expect(bg.getAttribute('stroke-width')).toBe('2'); - }); + slide(start, sliderY, end, sliderY) + .then(function() { + var maskMin = children[2], handleMin = children[5]; - it('should react to resizing the minimum handle', function(done) { - var start = 85, - end = 140, - diff = end - start; - - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); - - slide(start, sliderY, end, sliderY).then(function() { - var maskMin = children[2], - handleMin = children[5]; - - expect(gd.layout.xaxis.range).toBeCloseToArray([4, 49], -0.5); - expect(maskMin.getAttribute('width')).toEqual(String(diff)); - expect(handleMin.getAttribute('transform')).toBe('translate(' + (diff - 3) + ',0)'); - }).then(done); - }); - - it('should react to resizing the maximum handle', function(done) { - var start = 695, - end = 490, - dataMaxStart = gd._fullLayout.xaxis.rangeslider.d2p(49), - diff = end - start; - - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); - - slide(start, sliderY, end, sliderY).then(function() { - var maskMax = children[3], - handleMax = children[6]; - - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 32.77], -0.5); - expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); - - testTranslate1D(handleMax, dataMaxStart + diff); - }).then(done); - }); - - it('should react to moving the slidebox left to right', function(done) { - var start = 250, - end = 300, - dataMinStart = gd._fullLayout.xaxis.rangeslider.d2p(0), - diff = end - start; - - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); - - slide(start, sliderY, end, sliderY).then(function() { - var maskMin = children[2], - handleMin = children[5]; - - expect(gd.layout.xaxis.range).toBeCloseToArray([3.96, 49], -0.5); - expect(+maskMin.getAttribute('width')).toBeCloseTo(String(diff)); - testTranslate1D(handleMin, dataMinStart + diff - 3); - }).then(done); - }); - - it('should react to moving the slidebox right to left', function(done) { - var start = 300, - end = 250, - dataMaxStart = gd._fullLayout.xaxis.rangeslider.d2p(49), - diff = end - start; - - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); - - slide(start, sliderY, end, sliderY).then(function() { - var maskMax = children[3], - handleMax = children[6]; - - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 45.04], -0.5); - expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); - testTranslate1D(handleMax, dataMaxStart + diff); - }).then(done); - }); - - it('should resize the main plot when rangeslider has moved', function(done) { - var start = 300, - end = 400, - rangeDiff1 = gd._fullLayout.xaxis.range[1] - gd._fullLayout.xaxis.range[0], - rangeDiff2, - rangeDiff3; - - slide(start, sliderY, end, sliderY).then(function() { - rangeDiff2 = gd._fullLayout.xaxis.range[1] - gd._fullLayout.xaxis.range[0]; - expect(rangeDiff2).toBeLessThan(rangeDiff1); - }).then(function() { - start = 400; - end = 200; - - return slide(start, sliderY, end, sliderY); - }).then(function() { - rangeDiff3 = gd._fullLayout.xaxis.range[1] - gd._fullLayout.xaxis.range[0]; - expect(rangeDiff3).toBeLessThan(rangeDiff2); - }).then(done); - }); - - it('should relayout with relayout "array syntax"', function(done) { - Plotly.relayout(gd, 'xaxis.range', [10, 20]).then(function() { - var maskMin = children[2], - maskMax = children[3], - handleMin = children[5], - handleMax = children[6]; - - expect(+maskMin.getAttribute('width')).toBeWithin(125, TOL); - expect(+maskMax.getAttribute('width')).toBeWithin(365, TOL); - testTranslate1D(handleMin, 123.32); - testTranslate1D(handleMax, 252.65); - }) - .then(done); - }); - - it('should relayout with relayout "element syntax"', function(done) { - Plotly.relayout(gd, 'xaxis.range[0]', 10).then(function() { - var maskMin = children[2], - maskMax = children[3], - handleMin = children[5], - handleMax = children[6]; - - expect(+maskMin.getAttribute('width')).toBeWithin(126, TOL); - expect(+maskMax.getAttribute('width')).toEqual(0); - testTranslate1D(handleMin, 123.32); - testTranslate1D(handleMax, 619); - }) - .then(done); - }); - - it('should relayout with style options', function(done) { - var bg = children[0], - maskMin = children[2], - maskMax = children[3]; - - var maskMinWidth, maskMaxWidth; - - Plotly.relayout(gd, 'xaxis.range', [5, 10]).then(function() { - maskMinWidth = +maskMin.getAttribute('width'), - maskMaxWidth = +maskMax.getAttribute('width'); - - return Plotly.relayout(gd, 'xaxis.rangeslider.bgcolor', 'red'); - }) - .then(function() { - expect(+maskMin.getAttribute('width')).toEqual(maskMinWidth); - expect(+maskMax.getAttribute('width')).toEqual(maskMaxWidth); - - expect(bg.getAttribute('fill')).toBe('red'); - expect(bg.getAttribute('stroke')).toBe('black'); - expect(bg.getAttribute('stroke-width')).toBe('2'); - - return Plotly.relayout(gd, 'xaxis.rangeslider.bordercolor', 'blue'); - }) - .then(function() { - expect(+maskMin.getAttribute('width')).toEqual(maskMinWidth); - expect(+maskMax.getAttribute('width')).toEqual(maskMaxWidth); - - expect(bg.getAttribute('fill')).toBe('red'); - expect(bg.getAttribute('stroke')).toBe('blue'); - expect(bg.getAttribute('stroke-width')).toBe('2'); - - return Plotly.relayout(gd, 'xaxis.rangeslider.borderwidth', 3); - }) - .then(function() { - expect(+maskMin.getAttribute('width')).toEqual(maskMinWidth); - expect(+maskMax.getAttribute('width')).toEqual(maskMaxWidth); - - expect(bg.getAttribute('fill')).toBe('red'); - expect(bg.getAttribute('stroke')).toBe('blue'); - expect(bg.getAttribute('stroke-width')).toBe('3'); - }) - .then(done); - }); - - it('should relayout on size / domain udpate', function(done) { - var maskMin = children[2], - maskMax = children[3]; - - Plotly.relayout(gd, 'xaxis.range', [5, 10]).then(function() { - expect(+maskMin.getAttribute('width')).toBeWithin(63.16, TOL); - expect(+maskMax.getAttribute('width')).toBeWithin(492.67, TOL); - - return Plotly.relayout(gd, 'xaxis.domain', [0.3, 0.7]); - }) - .then(function() { - var maskMin = children[2], - maskMax = children[3]; - - expect(+maskMin.getAttribute('width')).toBeWithin(25.26, TOL); - expect(+maskMax.getAttribute('width')).toBeWithin(197.06, TOL); - - return Plotly.relayout(gd, 'width', 400); - }) - .then(function() { - var maskMin = children[2], - maskMax = children[3]; - - expect(+maskMin.getAttribute('width')).toBeWithin(9.22, TOL); - expect(+maskMax.getAttribute('width')).toBeWithin(71.95, TOL); - - }) - .then(done); - }); + expect(gd.layout.xaxis.range).toBeCloseToArray([4, 49], -0.5); + expect(maskMin.getAttribute("width")).toEqual(String(diff)); + expect(handleMin.getAttribute("transform")).toBe( + "translate(" + (diff - 3) + ",0)" + ); + }) + .then(done); }); + it("should react to resizing the maximum handle", function(done) { + var start = 695, + end = 490, + dataMaxStart = gd._fullLayout.xaxis.rangeslider.d2p(49), + diff = end - start; + + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); + + slide(start, sliderY, end, sliderY) + .then(function() { + var maskMax = children[3], handleMax = children[6]; + + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 32.77], -0.5); + expect(+maskMax.getAttribute("width")).toBeCloseTo(-diff); - describe('visibility property', function() { - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - it('should not add the slider to the DOM by default', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) - .then(function() { - var rangeSlider = getRangeSlider(); - expect(rangeSlider).not.toBeDefined(); - }) - .then(done); - }); - - it('should add the slider if rangeslider is set to anything', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) - .then(function() { Plotly.relayout(gd, 'xaxis.rangeslider', 'exists'); }) - .then(function() { - var rangeSlider = getRangeSlider(); - expect(rangeSlider).toBeDefined(); - }) - .then(done); - }); - - it('should add the slider if visible changed to `true`', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) - .then(function() { Plotly.relayout(gd, 'xaxis.rangeslider.visible', true); }) - .then(function() { - var rangeSlider = getRangeSlider(); - expect(rangeSlider).toBeDefined(); - expect(countRangeSliderClipPaths()).toEqual(1); - }) - .then(done); - }); - - it('should remove the slider if changed to `false` or `undefined`', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], { xaxis: { rangeslider: { visible: true }}}) - .then(function() { Plotly.relayout(gd, 'xaxis.rangeslider.visible', false); }) - .then(function() { - var rangeSlider = getRangeSlider(); - expect(rangeSlider).not.toBeDefined(); - expect(countRangeSliderClipPaths()).toEqual(0); - }) - .then(done); - }); + testTranslate1D(handleMax, dataMaxStart + diff); + }) + .then(done); }); - describe('handleDefaults function', function() { - - it('should not coerce anything if rangeslider isn\'t set', function() { - var layoutIn = { xaxis: {} }, - layoutOut = { xaxis: {} }, - expected = { xaxis: {} }; - - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - expect(layoutIn).toEqual(expected); - }); - - it('should not mutate layoutIn', function() { - var layoutIn = { xaxis: { rangeslider: { visible: true }} }, - layoutOut = { xaxis: { rangeslider: {}} }, - expected = { xaxis: { rangeslider: { visible: true }} }; - - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - expect(layoutIn).toEqual(expected); - }); - - it('should set defaults if rangeslider is set to anything truthy', function() { - var layoutIn = { xaxis: { rangeslider: {} }}, - layoutOut = { xaxis: {} }, - expected = { - xaxis: { - rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }, - _needsExpand: true - } - }; - - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - expect(layoutOut).toEqual(expected); - }); - - it('should set defaults if rangeslider.visible is true', function() { - var layoutIn = { xaxis: { rangeslider: { visible: true }} }, - layoutOut = { xaxis: { rangeslider: {}} }, - expected = { - xaxis: { - rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }, - _needsExpand: true - } - }; - - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - expect(layoutOut).toEqual(expected); - }); - - it('should set defaults if properties are invalid', function() { - var layoutIn = { xaxis: { rangeslider: { - visible: 'invalid', - thickness: 'invalid', - bgcolor: 42, - bordercolor: 42, - borderwidth: 'superfat' - }}}, - layoutOut = { xaxis: {} }, - expected = { - xaxis: { - rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }, - _needsExpand: true - } - }; - - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - expect(layoutOut).toEqual(expected); - }); - - it('should expand the rangeslider range to axis range', function() { - var layoutIn = { xaxis: { rangeslider: { range: [5, 6] } } }, - layoutOut = { xaxis: { range: [1, 10], type: 'linear'} }, - expected = { - xaxis: { - rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - range: [1, 10], - _input: layoutIn.xaxis.rangeslider - }, - range: [1, 10] - } - }; - - setConvert(layoutOut.xaxis); - - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - // don't compare the whole layout, because we had to run setConvert which - // attaches all sorts of other stuff to xaxis - expect(layoutOut.xaxis.rangeslider).toEqual(expected.xaxis.rangeslider); - }); - - it('should set _needsExpand when an axis range is set', function() { - var layoutIn = { xaxis: { rangeslider: true } }, - layoutOut = { xaxis: { range: [2, 40]} }, - expected = { - xaxis: { - rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: {} - }, - range: [2, 40], - _needsExpand: true - }, - }; - - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - expect(layoutOut).toEqual(expected); - }); - - it('should default \'bgcolor\' to layout \'plot_bgcolor\'', function() { - var layoutIn = { - xaxis: { rangeslider: true } - }; - - var layoutOut = { - xaxis: { range: [2, 40]}, - plot_bgcolor: 'blue' - }; - - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - expect(layoutOut.xaxis.rangeslider.bgcolor).toEqual('blue'); - }); + it("should react to moving the slidebox left to right", function(done) { + var start = 250, + end = 300, + dataMinStart = gd._fullLayout.xaxis.rangeslider.d2p(0), + diff = end - start; + + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); + + slide(start, sliderY, end, sliderY) + .then(function() { + var maskMin = children[2], handleMin = children[5]; + + expect(gd.layout.xaxis.range).toBeCloseToArray([3.96, 49], -0.5); + expect(+maskMin.getAttribute("width")).toBeCloseTo(String(diff)); + testTranslate1D(handleMin, dataMinStart + diff - 3); + }) + .then(done); }); - describe('anchored axes fixedrange', function() { - - it('should default to *true* when range slider is visible', function() { - var mock = { - layout: { - xaxis: { rangeslider: {} }, - yaxis: { anchor: 'x' }, - yaxis2: { anchor: 'x' }, - yaxis3: { anchor: 'free' } - } - }; - - Plots.supplyDefaults(mock); - - expect(mock._fullLayout.xaxis.rangeslider.visible).toBe(true); - expect(mock._fullLayout.yaxis.fixedrange).toBe(true); - expect(mock._fullLayout.yaxis2.fixedrange).toBe(true); - expect(mock._fullLayout.yaxis3.fixedrange).toBe(false); - }); - - it('should honor user settings', function() { - var mock = { - layout: { - xaxis: { rangeslider: {} }, - yaxis: { anchor: 'x', fixedrange: false }, - yaxis2: { anchor: 'x', fixedrange: false }, - yaxis3: { anchor: 'free' } - } - }; - - Plots.supplyDefaults(mock); - - expect(mock._fullLayout.xaxis.rangeslider.visible).toBe(true); - expect(mock._fullLayout.yaxis.fixedrange).toBe(false); - expect(mock._fullLayout.yaxis2.fixedrange).toBe(false); - expect(mock._fullLayout.yaxis3.fixedrange).toBe(false); - }); + it("should react to moving the slidebox right to left", function(done) { + var start = 300, + end = 250, + dataMaxStart = gd._fullLayout.xaxis.rangeslider.d2p(49), + diff = end - start; + + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); + slide(start, sliderY, end, sliderY) + .then(function() { + var maskMax = children[3], handleMax = children[6]; + + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 45.04], -0.5); + expect(+maskMax.getAttribute("width")).toBeCloseTo(-diff); + testTranslate1D(handleMax, dataMaxStart + diff); + }) + .then(done); }); - describe('in general', function() { + it("should resize the main plot when rangeslider has moved", function( + done + ) { + var start = 300, + end = 400, + rangeDiff1 = gd._fullLayout.xaxis.range[1] - + gd._fullLayout.xaxis.range[0], + rangeDiff2, + rangeDiff3; + + slide(start, sliderY, end, sliderY) + .then(function() { + rangeDiff2 = gd._fullLayout.xaxis.range[1] - + gd._fullLayout.xaxis.range[0]; + expect(rangeDiff2).toBeLessThan(rangeDiff1); + }) + .then(function() { + start = 400; + end = 200; + + return slide(start, sliderY, end, sliderY); + }) + .then(function() { + rangeDiff3 = gd._fullLayout.xaxis.range[1] - + gd._fullLayout.xaxis.range[0]; + expect(rangeDiff3).toBeLessThan(rangeDiff2); + }) + .then(done); + }); - beforeEach(function() { - gd = createGraphDiv(); - }); + it('should relayout with relayout "array syntax"', function(done) { + Plotly.relayout(gd, "xaxis.range", [10, 20]) + .then(function() { + var maskMin = children[2], + maskMax = children[3], + handleMin = children[5], + handleMax = children[6]; + + expect(+maskMin.getAttribute("width")).toBeWithin(125, TOL); + expect(+maskMax.getAttribute("width")).toBeWithin(365, TOL); + testTranslate1D(handleMin, 123.32); + testTranslate1D(handleMax, 252.65); + }) + .then(done); + }); - afterEach(destroyGraphDiv); + it('should relayout with relayout "element syntax"', function(done) { + Plotly.relayout(gd, "xaxis.range[0]", 10) + .then(function() { + var maskMin = children[2], + maskMax = children[3], + handleMin = children[5], + handleMax = children[6]; + + expect(+maskMin.getAttribute("width")).toBeWithin(126, TOL); + expect(+maskMax.getAttribute("width")).toEqual(0); + testTranslate1D(handleMin, 123.32); + testTranslate1D(handleMax, 619); + }) + .then(done); + }); - it('should plot when only x data is provided', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3] }], { xaxis: { rangeslider: {} }}) - .then(function() { - var rangeSlider = getRangeSlider(); + it("should relayout with style options", function(done) { + var bg = children[0], maskMin = children[2], maskMax = children[3]; + + var maskMinWidth, maskMaxWidth; + + Plotly.relayout(gd, "xaxis.range", [5, 10]) + .then(function() { + maskMinWidth = +maskMin.getAttribute( + "width" + ), maskMaxWidth = +maskMax.getAttribute("width"); + + return Plotly.relayout(gd, "xaxis.rangeslider.bgcolor", "red"); + }) + .then(function() { + expect(+maskMin.getAttribute("width")).toEqual(maskMinWidth); + expect(+maskMax.getAttribute("width")).toEqual(maskMaxWidth); + + expect(bg.getAttribute("fill")).toBe("red"); + expect(bg.getAttribute("stroke")).toBe("black"); + expect(bg.getAttribute("stroke-width")).toBe("2"); + + return Plotly.relayout(gd, "xaxis.rangeslider.bordercolor", "blue"); + }) + .then(function() { + expect(+maskMin.getAttribute("width")).toEqual(maskMinWidth); + expect(+maskMax.getAttribute("width")).toEqual(maskMaxWidth); + + expect(bg.getAttribute("fill")).toBe("red"); + expect(bg.getAttribute("stroke")).toBe("blue"); + expect(bg.getAttribute("stroke-width")).toBe("2"); + + return Plotly.relayout(gd, "xaxis.rangeslider.borderwidth", 3); + }) + .then(function() { + expect(+maskMin.getAttribute("width")).toEqual(maskMinWidth); + expect(+maskMax.getAttribute("width")).toEqual(maskMaxWidth); + + expect(bg.getAttribute("fill")).toBe("red"); + expect(bg.getAttribute("stroke")).toBe("blue"); + expect(bg.getAttribute("stroke-width")).toBe("3"); + }) + .then(done); + }); + + it("should relayout on size / domain udpate", function(done) { + var maskMin = children[2], maskMax = children[3]; + + Plotly.relayout(gd, "xaxis.range", [5, 10]) + .then(function() { + expect(+maskMin.getAttribute("width")).toBeWithin(63.16, TOL); + expect(+maskMax.getAttribute("width")).toBeWithin(492.67, TOL); + + return Plotly.relayout(gd, "xaxis.domain", [0.3, 0.7]); + }) + .then(function() { + var maskMin = children[2], maskMax = children[3]; - expect(rangeSlider).toBeDefined(); - }) - .then(done); - }); + expect(+maskMin.getAttribute("width")).toBeWithin(25.26, TOL); + expect(+maskMax.getAttribute("width")).toBeWithin(197.06, TOL); - it('should plot when only y data is provided', function(done) { - Plotly.plot(gd, [{ y: [1, 2, 3] }], { xaxis: { rangeslider: {} }}) - .then(function() { - var rangeSlider = getRangeSlider(); + return Plotly.relayout(gd, "width", 400); + }) + .then(function() { + var maskMin = children[2], maskMax = children[3]; - expect(rangeSlider).toBeDefined(); - }) - .then(done); - }); + expect(+maskMin.getAttribute("width")).toBeWithin(9.22, TOL); + expect(+maskMax.getAttribute("width")).toBeWithin(71.95, TOL); + }) + .then(done); + }); + }); + + describe("visibility property", function() { + beforeEach(function() { + gd = createGraphDiv(); }); -}); + afterEach(destroyGraphDiv); -function slide(fromX, fromY, toX, toY) { - return new Promise(function(resolve) { - mouseEvent('mousemove', fromX, fromY); - mouseEvent('mousedown', fromX, fromY); - mouseEvent('mousemove', toX, toY); - mouseEvent('mouseup', toX, toY); - - setTimeout(function() { - return resolve(); - }, 20); + it("should not add the slider to the DOM by default", function(done) { + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) + .then(function() { + var rangeSlider = getRangeSlider(); + expect(rangeSlider).not.toBeDefined(); + }) + .then(done); + }); + + it("should add the slider if rangeslider is set to anything", function( + done + ) { + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) + .then(function() { + Plotly.relayout(gd, "xaxis.rangeslider", "exists"); + }) + .then(function() { + var rangeSlider = getRangeSlider(); + expect(rangeSlider).toBeDefined(); + }) + .then(done); + }); + + it("should add the slider if visible changed to `true`", function(done) { + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) + .then(function() { + Plotly.relayout(gd, "xaxis.rangeslider.visible", true); + }) + .then(function() { + var rangeSlider = getRangeSlider(); + expect(rangeSlider).toBeDefined(); + expect(countRangeSliderClipPaths()).toEqual(1); + }) + .then(done); + }); + + it( + "should remove the slider if changed to `false` or `undefined`", + function(done) { + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], { + xaxis: { rangeslider: { visible: true } } + }) + .then(function() { + Plotly.relayout(gd, "xaxis.rangeslider.visible", false); + }) + .then(function() { + var rangeSlider = getRangeSlider(); + expect(rangeSlider).not.toBeDefined(); + expect(countRangeSliderClipPaths()).toEqual(0); + }) + .then(done); + } + ); + }); + + describe("handleDefaults function", function() { + it("should not coerce anything if rangeslider isn't set", function() { + var layoutIn = { xaxis: {} }, + layoutOut = { xaxis: {} }, + expected = { xaxis: {} }; + + RangeSlider.handleDefaults(layoutIn, layoutOut, "xaxis"); + + expect(layoutIn).toEqual(expected); + }); + + it("should not mutate layoutIn", function() { + var layoutIn = { xaxis: { rangeslider: { visible: true } } }, + layoutOut = { xaxis: { rangeslider: {} } }, + expected = { xaxis: { rangeslider: { visible: true } } }; + + RangeSlider.handleDefaults(layoutIn, layoutOut, "xaxis"); + + expect(layoutIn).toEqual(expected); + }); + + it( + "should set defaults if rangeslider is set to anything truthy", + function() { + var layoutIn = { xaxis: { rangeslider: {} } }, + layoutOut = { xaxis: {} }, + expected = { + xaxis: { + rangeslider: { + visible: true, + thickness: 0.15, + bgcolor: "#fff", + borderwidth: 0, + bordercolor: "#444", + _input: layoutIn.xaxis.rangeslider + }, + _needsExpand: true + } + }; + + RangeSlider.handleDefaults(layoutIn, layoutOut, "xaxis"); + + expect(layoutOut).toEqual(expected); + } + ); + + it("should set defaults if rangeslider.visible is true", function() { + var layoutIn = { xaxis: { rangeslider: { visible: true } } }, + layoutOut = { xaxis: { rangeslider: {} } }, + expected = { + xaxis: { + rangeslider: { + visible: true, + thickness: 0.15, + bgcolor: "#fff", + borderwidth: 0, + bordercolor: "#444", + _input: layoutIn.xaxis.rangeslider + }, + _needsExpand: true + } + }; + + RangeSlider.handleDefaults(layoutIn, layoutOut, "xaxis"); + + expect(layoutOut).toEqual(expected); + }); + + it("should set defaults if properties are invalid", function() { + var layoutIn = { + xaxis: { + rangeslider: { + visible: "invalid", + thickness: "invalid", + bgcolor: 42, + bordercolor: 42, + borderwidth: "superfat" + } + } + }, + layoutOut = { xaxis: {} }, + expected = { + xaxis: { + rangeslider: { + visible: true, + thickness: 0.15, + bgcolor: "#fff", + borderwidth: 0, + bordercolor: "#444", + _input: layoutIn.xaxis.rangeslider + }, + _needsExpand: true + } + }; + + RangeSlider.handleDefaults(layoutIn, layoutOut, "xaxis"); + + expect(layoutOut).toEqual(expected); + }); + + it("should expand the rangeslider range to axis range", function() { + var layoutIn = { xaxis: { rangeslider: { range: [5, 6] } } }, + layoutOut = { xaxis: { range: [1, 10], type: "linear" } }, + expected = { + xaxis: { + rangeslider: { + visible: true, + thickness: 0.15, + bgcolor: "#fff", + borderwidth: 0, + bordercolor: "#444", + range: [1, 10], + _input: layoutIn.xaxis.rangeslider + }, + range: [1, 10] + } + }; + + setConvert(layoutOut.xaxis); + + RangeSlider.handleDefaults(layoutIn, layoutOut, "xaxis"); + + // don't compare the whole layout, because we had to run setConvert which + // attaches all sorts of other stuff to xaxis + expect(layoutOut.xaxis.rangeslider).toEqual(expected.xaxis.rangeslider); + }); + + it("should set _needsExpand when an axis range is set", function() { + var layoutIn = { xaxis: { rangeslider: true } }, + layoutOut = { xaxis: { range: [2, 40] } }, + expected = { + xaxis: { + rangeslider: { + visible: true, + thickness: 0.15, + bgcolor: "#fff", + borderwidth: 0, + bordercolor: "#444", + _input: {} + }, + range: [2, 40], + _needsExpand: true + } + }; + + RangeSlider.handleDefaults(layoutIn, layoutOut, "xaxis"); + + expect(layoutOut).toEqual(expected); + }); + + it("should default 'bgcolor' to layout 'plot_bgcolor'", function() { + var layoutIn = { xaxis: { rangeslider: true } }; + + var layoutOut = { xaxis: { range: [2, 40] }, plot_bgcolor: "blue" }; + + RangeSlider.handleDefaults(layoutIn, layoutOut, "xaxis"); + + expect(layoutOut.xaxis.rangeslider.bgcolor).toEqual("blue"); }); + }); + + describe("anchored axes fixedrange", function() { + it("should default to *true* when range slider is visible", function() { + var mock = { + layout: { + xaxis: { rangeslider: {} }, + yaxis: { anchor: "x" }, + yaxis2: { anchor: "x" }, + yaxis3: { anchor: "free" } + } + }; + + Plots.supplyDefaults(mock); + + expect(mock._fullLayout.xaxis.rangeslider.visible).toBe(true); + expect(mock._fullLayout.yaxis.fixedrange).toBe(true); + expect(mock._fullLayout.yaxis2.fixedrange).toBe(true); + expect(mock._fullLayout.yaxis3.fixedrange).toBe(false); + }); + + it("should honor user settings", function() { + var mock = { + layout: { + xaxis: { rangeslider: {} }, + yaxis: { anchor: "x", fixedrange: false }, + yaxis2: { anchor: "x", fixedrange: false }, + yaxis3: { anchor: "free" } + } + }; + + Plots.supplyDefaults(mock); + + expect(mock._fullLayout.xaxis.rangeslider.visible).toBe(true); + expect(mock._fullLayout.yaxis.fixedrange).toBe(false); + expect(mock._fullLayout.yaxis2.fixedrange).toBe(false); + expect(mock._fullLayout.yaxis3.fixedrange).toBe(false); + }); + }); + + describe("in general", function() { + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + it("should plot when only x data is provided", function(done) { + Plotly.plot(gd, [{ x: [1, 2, 3] }], { xaxis: { rangeslider: {} } }) + .then(function() { + var rangeSlider = getRangeSlider(); + + expect(rangeSlider).toBeDefined(); + }) + .then(done); + }); + + it("should plot when only y data is provided", function(done) { + Plotly.plot(gd, [{ y: [1, 2, 3] }], { xaxis: { rangeslider: {} } }) + .then(function() { + var rangeSlider = getRangeSlider(); + + expect(rangeSlider).toBeDefined(); + }) + .then(done); + }); + }); +}); + +function slide(fromX, fromY, toX, toY) { + return new Promise(function(resolve) { + mouseEvent("mousemove", fromX, fromY); + mouseEvent("mousedown", fromX, fromY); + mouseEvent("mousemove", toX, toY); + mouseEvent("mouseup", toX, toY); + + setTimeout( + function() { + return resolve(); + }, + 20 + ); + }); } diff --git a/test/jasmine/tests/register_test.js b/test/jasmine/tests/register_test.js index 6fa534a2e42..7f06b187ff0 100644 --- a/test/jasmine/tests/register_test.js +++ b/test/jasmine/tests/register_test.js @@ -1,280 +1,309 @@ -var Plotly = require('@lib/index'); -var Registry = require('@src/registry'); +var Plotly = require("@lib/index"); +var Registry = require("@src/registry"); -describe('Test Registry', function() { - 'use strict'; - - describe('register, getModule, and traceIs', function() { - beforeEach(function() { - this.modulesKeys = Object.keys(Registry.modules); - this.allTypesKeys = Object.keys(Registry.allTypes); - this.allCategoriesKeys = Object.keys(Registry.allCategories); - - this.fakeModule = { - calc: function() { return 42; }, - plot: function() { return 1000000; } - }; - this.fakeModule2 = { - plot: function() { throw new Error('nope!'); } - }; - - Registry.register(this.fakeModule, 'newtype', ['red', 'green']); - - spyOn(console, 'warn'); - }); - - afterEach(function() { - function revertObj(obj, initialKeys) { - Object.keys(obj).forEach(function(k) { - if(initialKeys.indexOf(k) === -1) delete obj[k]; - }); - } - - revertObj(Registry.modules, this.modulesKeys); - revertObj(Registry.allTypes, this.allTypesKeys); - revertObj(Registry.allCategories, this.allCategoriesKeys); - }); - - it('should not reregister a type', function() { - Registry.register(this.fakeModule2, 'newtype', ['yellow', 'blue']); - expect(Registry.allCategories.yellow).toBeUndefined(); - }); - - it('should find the module for a type', function() { - expect(Registry.getModule('newtype')).toBe(this.fakeModule); - expect(Registry.getModule({type: 'newtype'})).toBe(this.fakeModule); - }); - - it('should return false for types it doesn\'t know', function() { - expect(Registry.getModule('notatype')).toBe(false); - expect(Registry.getModule({type: 'notatype'})).toBe(false); - expect(Registry.getModule({type: 'newtype', r: 'this is polar'})).toBe(false); - }); - - it('should find the categories for this type', function() { - expect(Registry.traceIs('newtype', 'red')).toBe(true); - expect(Registry.traceIs({type: 'newtype'}, 'red')).toBe(true); - }); - - it('should not find other real categories', function() { - expect(Registry.traceIs('newtype', 'cartesian')).toBe(false); - expect(Registry.traceIs({type: 'newtype'}, 'cartesian')).toBe(false); - expect(console.warn).not.toHaveBeenCalled(); - }); - }); - - describe('Registry.registerSubplot', function() { - var fake = { - name: 'fake', - attr: 'abc', - idRoot: 'cba', - attrRegex: /^abc([2-9]|[1-9][0-9]+)?$/, - idRegex: /^cba([2-9]|[1-9][0-9]+)?$/, - attributes: { stuff: { 'more stuff': 102102 } } - }; - - Registry.registerSubplot(fake); - - var subplotsRegistry = Registry.subplotsRegistry; - - it('should register attr, idRoot and attributes', function() { - expect(subplotsRegistry.fake.attr).toEqual('abc'); - expect(subplotsRegistry.fake.idRoot).toEqual('cba'); - expect(subplotsRegistry.fake.attributes) - .toEqual({stuff: { 'more stuff': 102102 }}); - }); - - describe('registered subplot type attribute regex', function() { - it('should compile to correct attribute regex string', function() { - expect(subplotsRegistry.fake.attrRegex.toString()) - .toEqual('/^abc([2-9]|[1-9][0-9]+)?$/'); - }); - - var shouldPass = [ - 'abc', 'abc2', 'abc3', 'abc10', 'abc9', 'abc100', 'abc2002' - ]; - var shouldFail = [ - '0abc', 'abc0', 'abc1', 'abc021321', 'abc00021321' - ]; - - shouldPass.forEach(function(s) { - it('considers ' + JSON.stringify(s) + 'as a correct attribute name', function() { - expect(subplotsRegistry.fake.attrRegex.test(s)).toBe(true); - }); - }); - - shouldFail.forEach(function(s) { - it('considers ' + JSON.stringify(s) + 'as an incorrect attribute name', function() { - expect(subplotsRegistry.fake.attrRegex.test(s)).toBe(false); - }); - }); - }); - - describe('registered subplot type id regex', function() { - it('should compile to correct id regular expression', function() { - expect(subplotsRegistry.fake.idRegex.toString()) - .toEqual('/^cba([2-9]|[1-9][0-9]+)?$/'); - }); - - var shouldPass = [ - 'cba', 'cba2', 'cba3', 'cba10', 'cba9', 'cba100', 'cba2002' - ]; - var shouldFail = [ - '0cba', 'cba0', 'cba1', 'cba021321', 'cba00021321' - ]; - - shouldPass.forEach(function(s) { - it('considers ' + JSON.stringify(s) + 'as a correct attribute name', function() { - expect(subplotsRegistry.fake.idRegex.test(s)).toBe(true); - }); - }); - - shouldFail.forEach(function(s) { - it('considers ' + JSON.stringify(s) + 'as an incorrect attribute name', function() { - expect(subplotsRegistry.fake.idRegex.test(s)).toBe(false); - }); - }); - }); - - }); -}); +describe("Test Registry", function() { + "use strict"; + describe("register, getModule, and traceIs", function() { + beforeEach(function() { + this.modulesKeys = Object.keys(Registry.modules); + this.allTypesKeys = Object.keys(Registry.allTypes); + this.allCategoriesKeys = Object.keys(Registry.allCategories); + + this.fakeModule = { + calc: function() { + return 42; + }, + plot: function() { + return 1000000; + } + }; + this.fakeModule2 = { + plot: function() { + throw new Error("nope!"); + } + }; -describe('the register function', function() { - 'use strict'; + Registry.register(this.fakeModule, "newtype", ["red", "green"]); - beforeEach(function() { - this.modulesKeys = Object.keys(Registry.modules); - this.allTypesKeys = Object.keys(Registry.allTypes); - this.allCategoriesKeys = Object.keys(Registry.allCategories); - this.allTransformsKeys = Object.keys(Registry.transformsRegistry); + spyOn(console, "warn"); }); afterEach(function() { - function revertObj(obj, initialKeys) { - Object.keys(obj).forEach(function(k) { - if(initialKeys.indexOf(k) === -1) delete obj[k]; - }); - } + function revertObj(obj, initialKeys) { + Object.keys(obj).forEach(function(k) { + if (initialKeys.indexOf(k) === -1) delete obj[k]; + }); + } - revertObj(Registry.modules, this.modulesKeys); - revertObj(Registry.allTypes, this.allTypesKeys); - revertObj(Registry.allCategories, this.allCategoriesKeys); - revertObj(Registry.transformsRegistry, this.allTransformsKeys); + revertObj(Registry.modules, this.modulesKeys); + revertObj(Registry.allTypes, this.allTypesKeys); + revertObj(Registry.allCategories, this.allCategoriesKeys); }); - it('should throw an error when no argument is given', function() { - expect(function() { - Plotly.register(); - }).toThrowError(Error, 'No argument passed to Plotly.register.'); + it("should not reregister a type", function() { + Registry.register(this.fakeModule2, "newtype", ["yellow", "blue"]); + expect(Registry.allCategories.yellow).toBeUndefined(); }); - it('should work with a single module', function() { - var mockTrace1 = { - moduleType: 'trace', - name: 'mockTrace1', - meta: 'Meta string', - basePlotModule: { name: 'plotModule' }, - categories: ['categories', 'array'] - }; - - expect(function() { - Plotly.register(mockTrace1); - }).not.toThrow(); - - expect(Registry.getModule('mockTrace1')).toBe(mockTrace1); + it("should find the module for a type", function() { + expect(Registry.getModule("newtype")).toBe(this.fakeModule); + expect(Registry.getModule({ type: "newtype" })).toBe(this.fakeModule); }); - it('should work with an array of modules', function() { - var mockTrace2 = { - moduleType: 'trace', - name: 'mockTrace2', - meta: 'Meta string', - basePlotModule: { name: 'plotModule' }, - categories: ['categories', 'array'] - }; - - expect(function() { - Plotly.register([mockTrace2]); - }).not.toThrow(); - - expect(Registry.getModule('mockTrace2')).toBe(mockTrace2); + it("should return false for types it doesn't know", function() { + expect(Registry.getModule("notatype")).toBe(false); + expect(Registry.getModule({ type: "notatype" })).toBe(false); + expect(Registry.getModule({ type: "newtype", r: "this is polar" })).toBe( + false + ); }); - it('should throw an error when an invalid module is given', function() { - var invalidTrace = { moduleType: 'invalid' }; - - expect(function() { - Plotly.register([invalidTrace]); - }).toThrowError(Error, 'Invalid module was attempted to be registered!'); - - expect(Registry.transformsRegistry['mah-transform']).toBeUndefined(); + it("should find the categories for this type", function() { + expect(Registry.traceIs("newtype", "red")).toBe(true); + expect(Registry.traceIs({ type: "newtype" }, "red")).toBe(true); }); - it('should throw when if transform module is invalid (1)', function() { - var missingTransformName = { - moduleType: 'transform' - }; - - expect(function() { - Plotly.register(missingTransformName); - }).toThrowError(Error, 'Transform module *name* must be a string.'); - - expect(Registry.transformsRegistry['mah-transform']).toBeUndefined(); + it("should not find other real categories", function() { + expect(Registry.traceIs("newtype", "cartesian")).toBe(false); + expect(Registry.traceIs({ type: "newtype" }, "cartesian")).toBe(false); + expect(console.warn).not.toHaveBeenCalled(); }); - - it('should throw when if transform module is invalid (2)', function() { - var missingTransformFunc = { - moduleType: 'transform', - name: 'mah-transform' - }; - - expect(function() { - Plotly.register(missingTransformFunc); - }).toThrowError(Error, 'Transform module mah-transform is missing a *transform* or *calcTransform* method.'); - - expect(Registry.transformsRegistry['mah-transform']).toBeUndefined(); + }); + + describe("Registry.registerSubplot", function() { + var fake = { + name: "fake", + attr: "abc", + idRoot: "cba", + attrRegex: /^abc([2-9]|[1-9][0-9]+)?$/, + idRegex: /^cba([2-9]|[1-9][0-9]+)?$/, + attributes: { stuff: { "more stuff": 102102 } } + }; + + Registry.registerSubplot(fake); + + var subplotsRegistry = Registry.subplotsRegistry; + + it("should register attr, idRoot and attributes", function() { + expect(subplotsRegistry.fake.attr).toEqual("abc"); + expect(subplotsRegistry.fake.idRoot).toEqual("cba"); + expect(subplotsRegistry.fake.attributes).toEqual({ + stuff: { "more stuff": 102102 } + }); }); - it('should not throw when transform module is valid (1)', function() { - var transformModule = { - moduleType: 'transform', - name: 'mah-transform', - transform: function() {} - }; - - expect(function() { - Plotly.register(transformModule); - }).not.toThrow(); - - expect(Registry.transformsRegistry['mah-transform']).toBeDefined(); + describe("registered subplot type attribute regex", function() { + it("should compile to correct attribute regex string", function() { + expect(subplotsRegistry.fake.attrRegex.toString()).toEqual( + "/^abc([2-9]|[1-9][0-9]+)?$/" + ); + }); + + var shouldPass = [ + "abc", + "abc2", + "abc3", + "abc10", + "abc9", + "abc100", + "abc2002" + ]; + var shouldFail = ["0abc", "abc0", "abc1", "abc021321", "abc00021321"]; + + shouldPass.forEach(function(s) { + it( + "considers " + JSON.stringify(s) + "as a correct attribute name", + function() { + expect(subplotsRegistry.fake.attrRegex.test(s)).toBe(true); + } + ); + }); + + shouldFail.forEach(function(s) { + it( + "considers " + JSON.stringify(s) + "as an incorrect attribute name", + function() { + expect(subplotsRegistry.fake.attrRegex.test(s)).toBe(false); + } + ); + }); }); - it('should not throw when transform module is valid (2)', function() { - var transformModule = { - moduleType: 'transform', - name: 'mah-transform', - calcTransform: function() {} - }; - - expect(function() { - Plotly.register(transformModule); - }).not.toThrow(); - - expect(Registry.transformsRegistry['mah-transform']).toBeDefined(); + describe("registered subplot type id regex", function() { + it("should compile to correct id regular expression", function() { + expect(subplotsRegistry.fake.idRegex.toString()).toEqual( + "/^cba([2-9]|[1-9][0-9]+)?$/" + ); + }); + + var shouldPass = [ + "cba", + "cba2", + "cba3", + "cba10", + "cba9", + "cba100", + "cba2002" + ]; + var shouldFail = ["0cba", "cba0", "cba1", "cba021321", "cba00021321"]; + + shouldPass.forEach(function(s) { + it( + "considers " + JSON.stringify(s) + "as a correct attribute name", + function() { + expect(subplotsRegistry.fake.idRegex.test(s)).toBe(true); + } + ); + }); + + shouldFail.forEach(function(s) { + it( + "considers " + JSON.stringify(s) + "as an incorrect attribute name", + function() { + expect(subplotsRegistry.fake.idRegex.test(s)).toBe(false); + } + ); + }); }); + }); +}); - it('should not throw when transform module is valid (3)', function() { - var transformModule = { - moduleType: 'transform', - name: 'mah-transform', - transform: function() {}, - calcTransform: function() {} - }; - - expect(function() { - Plotly.register(transformModule); - }).not.toThrow(); - - expect(Registry.transformsRegistry['mah-transform']).toBeDefined(); - }); +describe("the register function", function() { + "use strict"; + beforeEach(function() { + this.modulesKeys = Object.keys(Registry.modules); + this.allTypesKeys = Object.keys(Registry.allTypes); + this.allCategoriesKeys = Object.keys(Registry.allCategories); + this.allTransformsKeys = Object.keys(Registry.transformsRegistry); + }); + + afterEach(function() { + function revertObj(obj, initialKeys) { + Object.keys(obj).forEach(function(k) { + if (initialKeys.indexOf(k) === -1) delete obj[k]; + }); + } + + revertObj(Registry.modules, this.modulesKeys); + revertObj(Registry.allTypes, this.allTypesKeys); + revertObj(Registry.allCategories, this.allCategoriesKeys); + revertObj(Registry.transformsRegistry, this.allTransformsKeys); + }); + + it("should throw an error when no argument is given", function() { + expect(function() { + Plotly.register(); + }).toThrowError(Error, "No argument passed to Plotly.register."); + }); + + it("should work with a single module", function() { + var mockTrace1 = { + moduleType: "trace", + name: "mockTrace1", + meta: "Meta string", + basePlotModule: { name: "plotModule" }, + categories: ["categories", "array"] + }; + + expect(function() { + Plotly.register(mockTrace1); + }).not.toThrow(); + + expect(Registry.getModule("mockTrace1")).toBe(mockTrace1); + }); + + it("should work with an array of modules", function() { + var mockTrace2 = { + moduleType: "trace", + name: "mockTrace2", + meta: "Meta string", + basePlotModule: { name: "plotModule" }, + categories: ["categories", "array"] + }; + + expect(function() { + Plotly.register([mockTrace2]); + }).not.toThrow(); + + expect(Registry.getModule("mockTrace2")).toBe(mockTrace2); + }); + + it("should throw an error when an invalid module is given", function() { + var invalidTrace = { moduleType: "invalid" }; + + expect(function() { + Plotly.register([invalidTrace]); + }).toThrowError(Error, "Invalid module was attempted to be registered!"); + + expect(Registry.transformsRegistry["mah-transform"]).toBeUndefined(); + }); + + it("should throw when if transform module is invalid (1)", function() { + var missingTransformName = { moduleType: "transform" }; + + expect(function() { + Plotly.register(missingTransformName); + }).toThrowError(Error, "Transform module *name* must be a string."); + + expect(Registry.transformsRegistry["mah-transform"]).toBeUndefined(); + }); + + it("should throw when if transform module is invalid (2)", function() { + var missingTransformFunc = { + moduleType: "transform", + name: "mah-transform" + }; + + expect(function() { + Plotly.register(missingTransformFunc); + }).toThrowError( + Error, + "Transform module mah-transform is missing a *transform* or *calcTransform* method." + ); + + expect(Registry.transformsRegistry["mah-transform"]).toBeUndefined(); + }); + + it("should not throw when transform module is valid (1)", function() { + var transformModule = { + moduleType: "transform", + name: "mah-transform", + transform: function() {} + }; + + expect(function() { + Plotly.register(transformModule); + }).not.toThrow(); + + expect(Registry.transformsRegistry["mah-transform"]).toBeDefined(); + }); + + it("should not throw when transform module is valid (2)", function() { + var transformModule = { + moduleType: "transform", + name: "mah-transform", + calcTransform: function() {} + }; + + expect(function() { + Plotly.register(transformModule); + }).not.toThrow(); + + expect(Registry.transformsRegistry["mah-transform"]).toBeDefined(); + }); + + it("should not throw when transform module is valid (3)", function() { + var transformModule = { + moduleType: "transform", + name: "mah-transform", + transform: function() {}, + calcTransform: function() {} + }; + + expect(function() { + Plotly.register(transformModule); + }).not.toThrow(); + + expect(Registry.transformsRegistry["mah-transform"]).toBeDefined(); + }); }); diff --git a/test/jasmine/tests/scatter3d_test.js b/test/jasmine/tests/scatter3d_test.js index 7518da16166..05e002a1984 100644 --- a/test/jasmine/tests/scatter3d_test.js +++ b/test/jasmine/tests/scatter3d_test.js @@ -1,91 +1,92 @@ -var Scatter3D = require('@src/traces/scatter3d'); -var Lib = require('@src/lib'); -var Color = require('@src/components/color'); - - -describe('Scatter3D defaults', function() { - 'use strict'; - - var defaultColor = '#d3d3d3'; - - function _supply(traceIn, layoutEdits) { - var traceOut = { visible: true }, - layout = Lib.extendFlat({ _dataLength: 1 }, layoutEdits); - - Scatter3D.supplyDefaults(traceIn, traceOut, defaultColor, layout); - return traceOut; +var Scatter3D = require("@src/traces/scatter3d"); +var Lib = require("@src/lib"); +var Color = require("@src/components/color"); + +describe("Scatter3D defaults", function() { + "use strict"; + var defaultColor = "#d3d3d3"; + + function _supply(traceIn, layoutEdits) { + var traceOut = { visible: true }, + layout = Lib.extendFlat({ _dataLength: 1 }, layoutEdits); + + Scatter3D.supplyDefaults(traceIn, traceOut, defaultColor, layout); + return traceOut; + } + + var base = { x: [1, 2, 3], y: [1, 2, 3], z: [1, 2, 1] }; + + it( + "should make marker.color inherit from line.color (scalar case)", + function() { + var out = _supply(Lib.extendFlat({}, base, { line: { color: "red" } })); + + expect(out.line.color).toEqual("red"); + expect(out.marker.color).toEqual("red"); + expect(out.marker.line.color).toBe( + Color.defaultLine, + "but not marker.line.color" + ); } + ); - var base = { - x: [1, 2, 3], - y: [1, 2, 3], - z: [1, 2, 1] - }; - - it('should make marker.color inherit from line.color (scalar case)', function() { - var out = _supply(Lib.extendFlat({}, base, { - line: { color: 'red' } - })); - - expect(out.line.color).toEqual('red'); - expect(out.marker.color).toEqual('red'); - expect(out.marker.line.color).toBe(Color.defaultLine, 'but not marker.line.color'); - }); - - it('should make marker.color inherit from line.color (array case)', function() { - var color = [1, 2, 3]; + it( + "should make marker.color inherit from line.color (array case)", + function() { + var color = [1, 2, 3]; - var out = _supply(Lib.extendFlat({}, base, { - line: { color: color } - })); + var out = _supply(Lib.extendFlat({}, base, { line: { color: color } })); - expect(out.line.color).toBe(color); - expect(out.marker.color).toBe(color); - expect(out.marker.line.color).toBe(Color.defaultLine, 'but not marker.line.color'); - }); - - it('should make line.color inherit from marker.color if scalar)', function() { - var out = _supply(Lib.extendFlat({}, base, { - marker: { color: 'red' } - })); - - expect(out.line.color).toEqual('red'); - expect(out.marker.color).toEqual('red'); - expect(out.marker.line.color).toBe(Color.defaultLine); - }); + expect(out.line.color).toBe(color); + expect(out.marker.color).toBe(color); + expect(out.marker.line.color).toBe( + Color.defaultLine, + "but not marker.line.color" + ); + } + ); - it('should not make line.color inherit from marker.color if array', function() { - var color = [1, 2, 3]; + it("should make line.color inherit from marker.color if scalar)", function() { + var out = _supply(Lib.extendFlat({}, base, { marker: { color: "red" } })); - var out = _supply(Lib.extendFlat({}, base, { - marker: { color: color } - })); + expect(out.line.color).toEqual("red"); + expect(out.marker.color).toEqual("red"); + expect(out.marker.line.color).toBe(Color.defaultLine); + }); - expect(out.line.color).toBe(defaultColor); - expect(out.marker.color).toBe(color); - expect(out.marker.line.color).toBe(Color.defaultLine); - }); + it( + "should not make line.color inherit from marker.color if array", + function() { + var color = [1, 2, 3]; - it('should inherit layout.calendar', function() { - var out = _supply(base, {calendar: 'islamic'}); + var out = _supply(Lib.extendFlat({}, base, { marker: { color: color } })); - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(out.xcalendar).toBe('islamic'); - expect(out.ycalendar).toBe('islamic'); - expect(out.zcalendar).toBe('islamic'); + expect(out.line.color).toBe(defaultColor); + expect(out.marker.color).toBe(color); + expect(out.marker.line.color).toBe(Color.defaultLine); + } + ); + + it("should inherit layout.calendar", function() { + var out = _supply(base, { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(out.xcalendar).toBe("islamic"); + expect(out.ycalendar).toBe("islamic"); + expect(out.zcalendar).toBe("islamic"); + }); + + it("should take its own calendars", function() { + var traceIn = Lib.extendFlat({}, base, { + xcalendar: "coptic", + ycalendar: "ethiopian", + zcalendar: "mayan" }); + var out = _supply(traceIn, { calendar: "islamic" }); - it('should take its own calendars', function() { - var traceIn = Lib.extendFlat({}, base, { - xcalendar: 'coptic', - ycalendar: 'ethiopian', - zcalendar: 'mayan' - }); - var out = _supply(traceIn, {calendar: 'islamic'}); - - expect(out.xcalendar).toBe('coptic'); - expect(out.ycalendar).toBe('ethiopian'); - expect(out.zcalendar).toBe('mayan'); - }); + expect(out.xcalendar).toBe("coptic"); + expect(out.ycalendar).toBe("ethiopian"); + expect(out.zcalendar).toBe("mayan"); + }); }); diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index 1dcb3abbee3..f48a205b487 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -1,328 +1,398 @@ -var Scatter = require('@src/traces/scatter'); -var makeBubbleSizeFn = require('@src/traces/scatter/make_bubble_size_func'); -var linePoints = require('@src/traces/scatter/line_points'); -var Lib = require('@src/lib'); - -describe('Test scatter', function() { - 'use strict'; - - describe('supplyDefaults', function() { - var traceIn, - traceOut; - - var defaultColor = '#444', - layout = {}; - - var supplyDefaults = Scatter.supplyDefaults; - - beforeEach(function() { - traceOut = {}; - }); - - it('should set visible to false when x and y are empty', function() { - traceIn = {}; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [], - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); - - it('should set visible to false when x or y is empty', function() { - traceIn = { - x: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [], - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - - traceIn = { - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - - traceIn = { - x: [1, 2, 3], - y: [] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); - - it('should correctly assign \'hoveron\' default', function() { - traceIn = { - x: [1, 2, 3], - y: [1, 2, 3], - mode: 'lines+markers', - fill: 'tonext' - }; - - // fills and markers, you get both hover types - // you need visible: true here, as that normally gets set - // outside of the module supplyDefaults - traceOut = {visible: true}; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.hoveron).toBe('points+fills'); - - // but with only lines (or just fill) and fill tonext or toself - // you get fills - traceIn.mode = 'lines'; - traceOut = {visible: true}; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.hoveron).toBe('fills'); - - // with the wrong fill you always get points - // only area fills default to hoveron points. Vertical or - // horizontal fills don't have the same physical meaning, - // they're generally just filling their own slice, so they - // default to hoveron points. - traceIn.fill = 'tonexty'; - traceOut = {visible: true}; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.hoveron).toBe('points'); - }); - - it('should inherit layout.calendar', function() { - traceIn = { - x: [1, 2, 3], - y: [1, 2, 3] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('islamic'); - expect(traceOut.ycalendar).toBe('islamic'); - }); - - it('should take its own calendars', function() { - traceIn = { - x: [1, 2, 3], - y: [1, 2, 3], - xcalendar: 'coptic', - ycalendar: 'ethiopian' - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - expect(traceOut.xcalendar).toBe('coptic'); - expect(traceOut.ycalendar).toBe('ethiopian'); - }); +var Scatter = require("@src/traces/scatter"); +var makeBubbleSizeFn = require("@src/traces/scatter/make_bubble_size_func"); +var linePoints = require("@src/traces/scatter/line_points"); +var Lib = require("@src/lib"); + +describe("Test scatter", function() { + "use strict"; + describe("supplyDefaults", function() { + var traceIn, traceOut; + + var defaultColor = "#444", layout = {}; + + var supplyDefaults = Scatter.supplyDefaults; + + beforeEach(function() { + traceOut = {}; + }); + + it("should set visible to false when x and y are empty", function() { + traceIn = {}; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + + traceIn = { x: [], y: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); + + it("should set visible to false when x or y is empty", function() { + traceIn = { x: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + + traceIn = { x: [], y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + + traceIn = { y: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + + traceIn = { x: [1, 2, 3], y: [] }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); + + it("should correctly assign 'hoveron' default", function() { + traceIn = { + x: [1, 2, 3], + y: [1, 2, 3], + mode: "lines+markers", + fill: "tonext" + }; + + // fills and markers, you get both hover types + // you need visible: true here, as that normally gets set + // outside of the module supplyDefaults + traceOut = { visible: true }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.hoveron).toBe("points+fills"); + + // but with only lines (or just fill) and fill tonext or toself + // you get fills + traceIn.mode = "lines"; + traceOut = { visible: true }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.hoveron).toBe("fills"); + + // with the wrong fill you always get points + // only area fills default to hoveron points. Vertical or + // horizontal fills don't have the same physical meaning, + // they're generally just filling their own slice, so they + // default to hoveron points. + traceIn.fill = "tonexty"; + traceOut = { visible: true }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.hoveron).toBe("points"); + }); + + it("should inherit layout.calendar", function() { + traceIn = { x: [1, 2, 3], y: [1, 2, 3] }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("islamic"); + expect(traceOut.ycalendar).toBe("islamic"); + }); + + it("should take its own calendars", function() { + traceIn = { + x: [1, 2, 3], + y: [1, 2, 3], + xcalendar: "coptic", + ycalendar: "ethiopian" + }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); + + expect(traceOut.xcalendar).toBe("coptic"); + expect(traceOut.ycalendar).toBe("ethiopian"); + }); + }); + + describe("isBubble", function() { + it("should return true when marker.size is an Array", function() { + var trace = { marker: { size: [1, 4, 2, 10] } }, + isBubble = Scatter.isBubble(trace); + + expect(isBubble).toBe(true); + }); + + it("should return false when marker.size is an number", function() { + var trace = { marker: { size: 10 } }, isBubble = Scatter.isBubble(trace); + + expect(isBubble).toBe(false); + }); + + it("should return false when marker.size is not defined", function() { + var trace = { marker: { color: "red" } }, + isBubble = Scatter.isBubble(trace); + + expect(isBubble).toBe(false); + }); + + it("should return false when marker is not defined", function() { + var trace = { line: { color: "red" } }, + isBubble = Scatter.isBubble(trace); + + expect(isBubble).toBe(false); + }); + }); + + describe("makeBubbleSizeFn", function() { + var markerSizes = [ + 0, + "1", + 2.21321321, + "not-a-number", + 100, + 1000.213213, + 1e7, + undefined, + null, + -100 + ], + trace = { marker: {} }; + + var sizeFn, expected; + + it( + "should scale w.r.t. bubble diameter when sizemode=diameter", + function() { + trace.marker.sizemode = "diameter"; + sizeFn = makeBubbleSizeFn(trace); + + expected = [0, 0.5, 1.106606605, 0, 50, 500.1066065, 5000000, 0, 0, 0]; + expect(markerSizes.map(sizeFn)).toEqual(expected); + } + ); + + it("should scale w.r.t. bubble area when sizemode=area", function() { + trace.marker.sizemode = "area"; + sizeFn = makeBubbleSizeFn(trace); + + expected = [ + 0, + 0.7071067811865476, + 1.051953708582274, + 0, + 7.0710678118654755, + 22.363063441755916, + 2236.06797749979, + 0, + 0, + 0 + ]; + expect(markerSizes.map(sizeFn)).toEqual(expected); }); - describe('isBubble', function() { - it('should return true when marker.size is an Array', function() { - var trace = { - marker: { - size: [1, 4, 2, 10] - } - }, - isBubble = Scatter.isBubble(trace); - - expect(isBubble).toBe(true); - }); - - it('should return false when marker.size is an number', function() { - var trace = { - marker: { - size: 10 - } - }, - isBubble = Scatter.isBubble(trace); - - expect(isBubble).toBe(false); - }); - - it('should return false when marker.size is not defined', function() { - var trace = { - marker: { - color: 'red' - } - }, - isBubble = Scatter.isBubble(trace); - - expect(isBubble).toBe(false); - }); - - it('should return false when marker is not defined', function() { - var trace = { - line: { - color: 'red' - } - }, - isBubble = Scatter.isBubble(trace); - - expect(isBubble).toBe(false); - }); + it("should adjust scaling according to sizeref", function() { + trace.marker.sizemode = "diameter"; + trace.marker.sizeref = 0.1; + sizeFn = makeBubbleSizeFn(trace); + + expected = [0, 5, 11.06606605, 0, 500, 5001.066065, 50000000, 0, 0, 0]; + expect(markerSizes.map(sizeFn)).toEqual(expected); + }); + + it("should adjust the small sizes according to sizemin", function() { + trace.marker.sizemode = "diameter"; + trace.marker.sizeref = 10; + trace.marker.sizemin = 5; + sizeFn = makeBubbleSizeFn(trace); + + expected = [0, 5, 5, 0, 5, 50.01066065, 500000, 0, 0, 0]; + expect(markerSizes.map(sizeFn)).toEqual(expected); + }); + }); + + describe("linePoints", function() { + // test axes are unit-scaled and 100 units long + var ax = { _length: 100, c2p: Lib.identity }, + baseOpts = { + xaxis: ax, + yaxis: ax, + connectGaps: false, + baseTolerance: 1, + linear: true, + simplify: true + }; + + function makeCalcData(ptsIn) { + return ptsIn.map(function(pt) { + return { x: pt[0], y: pt[1] }; + }); + } + + function callLinePoints(ptsIn, opts) { + var thisOpts = {}; + if (!opts) opts = {}; + Object.keys(baseOpts).forEach(function(key) { + if (opts[key] !== undefined) thisOpts[key] = opts[key]; + else thisOpts[key] = baseOpts[key]; + }); + return linePoints(makeCalcData(ptsIn), thisOpts); + } + + it("should pass along well-separated non-linear points", function() { + var ptsIn = [[0, 0], [10, 20], [20, 10], [30, 40], [40, 60], [50, 30]]; + var ptsOut = callLinePoints(ptsIn); + + expect(ptsOut).toEqual([ptsIn]); + }); + + it("should collapse straight lines to just their endpoints", function() { + var ptsIn = [ + [0, 0], + [5, 10], + [13, 26], + [15, 30], + [22, 16], + [28, 4], + [30, 0] + ]; + var ptsOut = callLinePoints(ptsIn); + // TODO: [22,16] should not appear here. This is ok but not optimal. + expect(ptsOut).toEqual([[[0, 0], [15, 30], [22, 16], [30, 0]]]); + }); + + it("should not collapse straight lines if simplify is false", function() { + var ptsIn = [ + [0, 0], + [5, 10], + [13, 26], + [15, 30], + [22, 16], + [28, 4], + [30, 0] + ]; + var ptsOut = callLinePoints(ptsIn, { simplify: false }); + expect(ptsOut).toEqual([ptsIn]); + }); + it("should separate out blanks, unless connectgaps is true", function() { + var ptsIn = [ + [0, 0], + [10, 20], + [20, 10], + [undefined, undefined], + [30, 40], + [undefined, undefined], + [40, 60], + [50, 30] + ]; + var ptsDisjoint = callLinePoints(ptsIn); + var ptsConnected = callLinePoints(ptsIn, { connectGaps: true }); + + expect(ptsDisjoint).toEqual([ + [[0, 0], [10, 20], [20, 10]], + [[30, 40]], + [[40, 60], [50, 30]] + ]); + expect(ptsConnected).toEqual([ + [[0, 0], [10, 20], [20, 10], [30, 40], [40, 60], [50, 30]] + ]); }); - describe('makeBubbleSizeFn', function() { - var markerSizes = [ - 0, '1', 2.21321321, 'not-a-number', - 100, 1000.213213, 1e7, undefined, null, -100 - ], - trace = { marker: {} }; - - var sizeFn, expected; - - it('should scale w.r.t. bubble diameter when sizemode=diameter', function() { - trace.marker.sizemode = 'diameter'; - sizeFn = makeBubbleSizeFn(trace); - - expected = [ - 0, 0.5, 1.106606605, 0, 50, 500.1066065, 5000000, 0, 0, 0 - ]; - expect(markerSizes.map(sizeFn)).toEqual(expected); - }); - - it('should scale w.r.t. bubble area when sizemode=area', function() { - trace.marker.sizemode = 'area'; - sizeFn = makeBubbleSizeFn(trace); - - expected = [ - 0, 0.7071067811865476, 1.051953708582274, 0, 7.0710678118654755, - 22.363063441755916, 2236.06797749979, 0, 0, 0 - ]; - expect(markerSizes.map(sizeFn)).toEqual(expected); - }); - - it('should adjust scaling according to sizeref', function() { - trace.marker.sizemode = 'diameter'; - trace.marker.sizeref = 0.1; - sizeFn = makeBubbleSizeFn(trace); - - expected = [ - 0, 5, 11.06606605, 0, 500, 5001.066065, 50000000, 0, 0, 0 - ]; - expect(markerSizes.map(sizeFn)).toEqual(expected); - }); - - it('should adjust the small sizes according to sizemin', function() { - trace.marker.sizemode = 'diameter'; - trace.marker.sizeref = 10; - trace.marker.sizemin = 5; - sizeFn = makeBubbleSizeFn(trace); - - expected = [ - 0, 5, 5, 0, 5, 50.01066065, 500000, 0, 0, 0 - ]; - expect(markerSizes.map(sizeFn)).toEqual(expected); - }); + it("should collapse a vertical cluster into 4 points", function() { + // the four being initial, high, low, and final if the high is before the low + var ptsIn = [ + [-10, 0], + [0, 0], + [0, 10], + [0, 20], + [0, -10], + [0, 15], + [0, -25], + [0, 10], + [0, 5], + [10, 10] + ]; + var ptsOut = callLinePoints(ptsIn); + + // TODO: [0, 10] should not appear in either of these results - this is OK but not optimal. + expect(ptsOut).toEqual([ + [[-10, 0], [0, 0], [0, 10], [0, 20], [0, -25], [0, 5], [10, 10]] + ]); + + // or initial, low, high, final if the low is before the high + ptsIn = [ + [-10, 0], + [0, 0], + [0, 10], + [0, -25], + [0, -10], + [0, 15], + [0, 20], + [0, 10], + [0, 5], + [10, 10] + ]; + ptsOut = callLinePoints(ptsIn); + + expect(ptsOut).toEqual([ + [[-10, 0], [0, 0], [0, 10], [0, -25], [0, 20], [0, 5], [10, 10]] + ]); }); - describe('linePoints', function() { - // test axes are unit-scaled and 100 units long - var ax = {_length: 100, c2p: Lib.identity}, - baseOpts = { - xaxis: ax, - yaxis: ax, - connectGaps: false, - baseTolerance: 1, - linear: true, - simplify: true - }; - - function makeCalcData(ptsIn) { - return ptsIn.map(function(pt) { - return {x: pt[0], y: pt[1]}; - }); - } - - function callLinePoints(ptsIn, opts) { - var thisOpts = {}; - if(!opts) opts = {}; - Object.keys(baseOpts).forEach(function(key) { - if(opts[key] !== undefined) thisOpts[key] = opts[key]; - else thisOpts[key] = baseOpts[key]; - }); - return linePoints(makeCalcData(ptsIn), thisOpts); - } - - it('should pass along well-separated non-linear points', function() { - var ptsIn = [[0, 0], [10, 20], [20, 10], [30, 40], [40, 60], [50, 30]]; - var ptsOut = callLinePoints(ptsIn); - - expect(ptsOut).toEqual([ptsIn]); - }); - - it('should collapse straight lines to just their endpoints', function() { - var ptsIn = [[0, 0], [5, 10], [13, 26], [15, 30], [22, 16], [28, 4], [30, 0]]; - var ptsOut = callLinePoints(ptsIn); - // TODO: [22,16] should not appear here. This is ok but not optimal. - expect(ptsOut).toEqual([[[0, 0], [15, 30], [22, 16], [30, 0]]]); - }); - - it('should not collapse straight lines if simplify is false', function() { - var ptsIn = [[0, 0], [5, 10], [13, 26], [15, 30], [22, 16], [28, 4], [30, 0]]; - var ptsOut = callLinePoints(ptsIn, {simplify: false}); - expect(ptsOut).toEqual([ptsIn]); - }); - - it('should separate out blanks, unless connectgaps is true', function() { - var ptsIn = [ - [0, 0], [10, 20], [20, 10], [undefined, undefined], - [30, 40], [undefined, undefined], - [40, 60], [50, 30]]; - var ptsDisjoint = callLinePoints(ptsIn); - var ptsConnected = callLinePoints(ptsIn, {connectGaps: true}); - - expect(ptsDisjoint).toEqual([[[0, 0], [10, 20], [20, 10]], [[30, 40]], [[40, 60], [50, 30]]]); - expect(ptsConnected).toEqual([[[0, 0], [10, 20], [20, 10], [30, 40], [40, 60], [50, 30]]]); - }); - - it('should collapse a vertical cluster into 4 points', function() { - // the four being initial, high, low, and final if the high is before the low - var ptsIn = [[-10, 0], [0, 0], [0, 10], [0, 20], [0, -10], [0, 15], [0, -25], [0, 10], [0, 5], [10, 10]]; - var ptsOut = callLinePoints(ptsIn); - - // TODO: [0, 10] should not appear in either of these results - this is OK but not optimal. - expect(ptsOut).toEqual([[[-10, 0], [0, 0], [0, 10], [0, 20], [0, -25], [0, 5], [10, 10]]]); - - // or initial, low, high, final if the low is before the high - ptsIn = [[-10, 0], [0, 0], [0, 10], [0, -25], [0, -10], [0, 15], [0, 20], [0, 10], [0, 5], [10, 10]]; - ptsOut = callLinePoints(ptsIn); - - expect(ptsOut).toEqual([[[-10, 0], [0, 0], [0, 10], [0, -25], [0, 20], [0, 5], [10, 10]]]); - }); - - it('should collapse a horizontal cluster into 4 points', function() { - // same deal - var ptsIn = [[0, -10], [0, 0], [10, 0], [20, 0], [-10, 0], [15, 0], [-25, 0], [10, 0], [5, 0], [10, 10]]; - var ptsOut = callLinePoints(ptsIn); - - // TODO: [10, 0] should not appear in either of these results - this is OK but not optimal. - // same problem as the test above - expect(ptsOut).toEqual([[[0, -10], [0, 0], [10, 0], [20, 0], [-25, 0], [5, 0], [10, 10]]]); - - ptsIn = [[0, -10], [0, 0], [10, 0], [-25, 0], [-10, 0], [15, 0], [20, 0], [10, 0], [5, 0], [10, 10]]; - ptsOut = callLinePoints(ptsIn); - - expect(ptsOut).toEqual([[[0, -10], [0, 0], [10, 0], [-25, 0], [20, 0], [5, 0], [10, 10]]]); - }); - - it('should use lineWidth to determine whether a cluster counts', function() { - var ptsIn = [[0, 0], [20, 0], [21, 10], [22, 20], [23, -10], [24, 15], [25, -25], [26, 10], [27, 5], [100, 10]]; - var ptsThin = callLinePoints(ptsIn); - var ptsThick = callLinePoints(ptsIn, {baseTolerance: 8}); - - // thin line, no decimation. thick line yes. - expect(ptsThin).toEqual([ptsIn]); - // TODO: [21,10] should not appear in this result (same issue again) - expect(ptsThick).toEqual([[[0, 0], [20, 0], [21, 10], [22, 20], [25, -25], [27, 5], [100, 10]]]); - }); - - // TODO: test coarser decimation outside plot, and removing very near duplicates from the four of a cluster + it("should collapse a horizontal cluster into 4 points", function() { + // same deal + var ptsIn = [ + [0, -10], + [0, 0], + [10, 0], + [20, 0], + [-10, 0], + [15, 0], + [-25, 0], + [10, 0], + [5, 0], + [10, 10] + ]; + var ptsOut = callLinePoints(ptsIn); + + // TODO: [10, 0] should not appear in either of these results - this is OK but not optimal. + // same problem as the test above + expect(ptsOut).toEqual([ + [[0, -10], [0, 0], [10, 0], [20, 0], [-25, 0], [5, 0], [10, 10]] + ]); + + ptsIn = [ + [0, -10], + [0, 0], + [10, 0], + [-25, 0], + [-10, 0], + [15, 0], + [20, 0], + [10, 0], + [5, 0], + [10, 10] + ]; + ptsOut = callLinePoints(ptsIn); + + expect(ptsOut).toEqual([ + [[0, -10], [0, 0], [10, 0], [-25, 0], [20, 0], [5, 0], [10, 10]] + ]); }); + it( + "should use lineWidth to determine whether a cluster counts", + function() { + var ptsIn = [ + [0, 0], + [20, 0], + [21, 10], + [22, 20], + [23, -10], + [24, 15], + [25, -25], + [26, 10], + [27, 5], + [100, 10] + ]; + var ptsThin = callLinePoints(ptsIn); + var ptsThick = callLinePoints(ptsIn, { baseTolerance: 8 }); + + // thin line, no decimation. thick line yes. + expect(ptsThin).toEqual([ptsIn]); + // TODO: [21,10] should not appear in this result (same issue again) + expect(ptsThick).toEqual([ + [[0, 0], [20, 0], [21, 10], [22, 20], [25, -25], [27, 5], [100, 10]] + ]); + } + ); + // TODO: test coarser decimation outside plot, and removing very near duplicates from the four of a cluster + }); }); diff --git a/test/jasmine/tests/scattergeo_test.js b/test/jasmine/tests/scattergeo_test.js index 2391cdfb512..f7b3718dccc 100644 --- a/test/jasmine/tests/scattergeo_test.js +++ b/test/jasmine/tests/scattergeo_test.js @@ -1,74 +1,63 @@ -var ScatterGeo = require('@src/traces/scattergeo'); +var ScatterGeo = require("@src/traces/scattergeo"); -describe('Test scattergeo', function() { - 'use strict'; +describe("Test scattergeo", function() { + "use strict"; + describe("supplyDefaults", function() { + var traceIn, traceOut; - describe('supplyDefaults', function() { - var traceIn, - traceOut; + var defaultColor = "#444", layout = {}; - var defaultColor = '#444', - layout = {}; - - beforeEach(function() { - traceOut = {}; - }); - - it('should slice lat if it it longer than lon', function() { - traceIn = { - lon: [-75], - lat: [45, 45, 45] - }; - - ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.lat).toEqual([45]); - expect(traceOut.lon).toEqual([-75]); - }); - - it('should slice lon if it it longer than lat', function() { - traceIn = { - lon: [-75, -75, -75], - lat: [45] - }; - - ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.lat).toEqual([45]); - expect(traceOut.lon).toEqual([-75]); - }); + beforeEach(function() { + traceOut = {}; + }); - it('should not coerce lat and lon if locations is valid', function() { - traceIn = { - locations: ['CAN', 'USA'], - lon: [20, 40], - lat: [20, 40] - }; + it("should slice lat if it it longer than lon", function() { + traceIn = { lon: [-75], lat: [45, 45, 45] }; - ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.lon).toBeUndefined(); - expect(traceOut.lat).toBeUndefined(); - }); + ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.lat).toEqual([45]); + expect(traceOut.lon).toEqual([-75]); + }); - it('should make trace invisible if lon or lat is omitted and locations not given', function() { - function testOne() { - ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - } + it("should slice lon if it it longer than lat", function() { + traceIn = { lon: [-75, -75, -75], lat: [45] }; - traceIn = { - lat: [45, 45, 45] - }; - testOne(); + ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.lat).toEqual([45]); + expect(traceOut.lon).toEqual([-75]); + }); - traceIn = { - lon: [-75, -75, -75] - }; - traceOut = {}; - testOne(); + it("should not coerce lat and lon if locations is valid", function() { + traceIn = { + locations: ["CAN", "USA"], + lon: [20, 40], + lat: [20, 40] + }; - traceIn = {}; - traceOut = {}; - testOne(); - }); + ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.lon).toBeUndefined(); + expect(traceOut.lat).toBeUndefined(); }); + it( + "should make trace invisible if lon or lat is omitted and locations not given", + function() { + function testOne() { + ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + } + + traceIn = { lat: [45, 45, 45] }; + testOne(); + + traceIn = { lon: [-75, -75, -75] }; + traceOut = {}; + testOne(); + + traceIn = {}; + traceOut = {}; + testOne(); + } + ); + }); }); diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js index 1e9556f44cd..02b8942b874 100644 --- a/test/jasmine/tests/scattermapbox_test.js +++ b/test/jasmine/tests/scattermapbox_test.js @@ -1,590 +1,653 @@ -var Plotly = require('@lib'); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); +var Plotly = require("@lib"); +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); -var ScatterMapbox = require('@src/traces/scattermapbox'); -var convert = require('@src/traces/scattermapbox/convert'); +var ScatterMapbox = require("@src/traces/scattermapbox"); +var convert = require("@src/traces/scattermapbox/convert"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var hasWebGLSupport = require('../assets/has_webgl_support'); -var customMatchers = require('../assets/custom_matchers'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var hasWebGLSupport = require("../assets/has_webgl_support"); +var customMatchers = require("../assets/custom_matchers"); Plotly.setPlotConfig({ - mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN + mapboxAccessToken: require("@build/credentials.json").MAPBOX_ACCESS_TOKEN }); +describe("scattermapbox defaults", function() { + "use strict"; + function _supply(traceIn) { + var traceOut = { visible: true }, + defaultColor = "#444", + layout = { _dataLength: 1 }; -describe('scattermapbox defaults', function() { - 'use strict'; + ScatterMapbox.supplyDefaults(traceIn, traceOut, defaultColor, layout); - function _supply(traceIn) { - var traceOut = { visible: true }, - defaultColor = '#444', - layout = { _dataLength: 1 }; + return traceOut; + } - ScatterMapbox.supplyDefaults(traceIn, traceOut, defaultColor, layout); + it("should truncate 'lon' if longer than 'lat'", function() { + var fullTrace = _supply({ lon: [1, 2, 3], lat: [2, 3] }); - return traceOut; - } - - it('should truncate \'lon\' if longer than \'lat\'', function() { - var fullTrace = _supply({ - lon: [1, 2, 3], - lat: [2, 3] - }); - - expect(fullTrace.lon).toEqual([1, 2]); - expect(fullTrace.lat).toEqual([2, 3]); - }); - - it('should truncate \'lat\' if longer than \'lon\'', function() { - var fullTrace = _supply({ - lon: [1, 2, 3], - lat: [2, 3, 3, 5] - }); + expect(fullTrace.lon).toEqual([1, 2]); + expect(fullTrace.lat).toEqual([2, 3]); + }); - expect(fullTrace.lon).toEqual([1, 2, 3]); - expect(fullTrace.lat).toEqual([2, 3, 3]); - }); + it("should truncate 'lat' if longer than 'lon'", function() { + var fullTrace = _supply({ lon: [1, 2, 3], lat: [2, 3, 3, 5] }); - it('should set \'visible\' to false if \'lat\' and/or \'lon\' has zero length', function() { - var fullTrace = _supply({ - lon: [1, 2, 3], - lat: [] - }); + expect(fullTrace.lon).toEqual([1, 2, 3]); + expect(fullTrace.lat).toEqual([2, 3, 3]); + }); - expect(fullTrace.visible).toEqual(false); + it( + "should set 'visible' to false if 'lat' and/or 'lon' has zero length", + function() { + var fullTrace = _supply({ lon: [1, 2, 3], lat: [] }); - fullTrace = _supply({ - lon: null, - lat: [1, 2, 3] - }); - - expect(fullTrace.visible).toEqual(false); - }); - - it('should set \'marker.color\' and \'marker.size\' to first item if symbol is set to \'circle\'', function() { - var base = { - mode: 'markers', - lon: [1, 2, 3], - lat: [2, 3, 3], - marker: { - color: ['red', 'green', 'blue'], - size: [10, 20, 30] - } - }; - - var fullTrace = _supply(Lib.extendDeep({}, base, { - marker: { symbol: 'monument' } - })); - - expect(fullTrace.marker.color).toEqual('red'); - expect(fullTrace.marker.size).toEqual(10); - - fullTrace = _supply(Lib.extendDeep({}, base, { - marker: { symbol: ['monument', 'music', 'harbor'] } - })); - - expect(fullTrace.marker.color).toEqual('red'); - expect(fullTrace.marker.size).toEqual(10); - - fullTrace = _supply(Lib.extendDeep({}, base, { - marker: { symbol: 'circle' } - })); - - expect(fullTrace.marker.color).toEqual(['red', 'green', 'blue']); - expect(fullTrace.marker.size).toEqual([10, 20, 30]); - }); -}); + expect(fullTrace.visible).toEqual(false); -describe('scattermapbox calc', function() { - 'use strict'; + fullTrace = _supply({ lon: null, lat: [1, 2, 3] }); - function _calc(trace) { - var gd = { data: [trace] }; - - Plots.supplyDefaults(gd); - - var fullTrace = gd._fullData[0]; - return ScatterMapbox.calc(gd, fullTrace); + expect(fullTrace.visible).toEqual(false); } + ); + + it( + "should set 'marker.color' and 'marker.size' to first item if symbol is set to 'circle'", + function() { + var base = { + mode: "markers", + lon: [1, 2, 3], + lat: [2, 3, 3], + marker: { color: ["red", "green", "blue"], size: [10, 20, 30] } + }; + + var fullTrace = _supply( + Lib.extendDeep({}, base, { marker: { symbol: "monument" } }) + ); + + expect(fullTrace.marker.color).toEqual("red"); + expect(fullTrace.marker.size).toEqual(10); + + fullTrace = _supply( + Lib.extendDeep({}, base, { + marker: { symbol: ["monument", "music", "harbor"] } + }) + ); - var base = { type: 'scattermapbox' }; - - it('should place lon/lat data in lonlat pairs', function() { - var calcTrace = _calc(Lib.extendFlat({}, base, { - lon: [10, 20, 30], - lat: [20, 30, 10] - })); - - expect(calcTrace).toEqual([ - { lonlat: [10, 20] }, - { lonlat: [20, 30] }, - { lonlat: [30, 10] } - ]); - }); - - it('should coerce numeric strings lon/lat data into numbers', function() { - var calcTrace = _calc(Lib.extendFlat({}, base, { - lon: [10, 20, '30', '40'], - lat: [20, '30', 10, '50'] - })); - - expect(calcTrace).toEqual([ - { lonlat: [10, 20] }, - { lonlat: [20, 30] }, - { lonlat: [30, 10] }, - { lonlat: [40, 50] } - ]); - }); - - it('should keep track of gaps in data', function() { - var calcTrace = _calc(Lib.extendFlat({}, base, { - lon: [null, 10, null, null, 20, '30', null, '40', null, 10], - lat: [10, 20, '30', null, 10, '50', null, 60, null, null] - })); - - expect(calcTrace).toEqual([ - { lonlat: [10, 20], gapAfter: true }, - { lonlat: [20, 10] }, - { lonlat: [30, 50], gapAfter: true }, - { lonlat: [40, 60], gapAfter: true } - ]); - }); - - it('should fill array text (base case)', function() { - var calcTrace = _calc(Lib.extendFlat({}, base, { - lon: [10, 20, 30], - lat: [20, 30, 10], - text: ['A', 'B', 'C'] - })); - - expect(calcTrace).toEqual([ - { lonlat: [10, 20], tx: 'A' }, - { lonlat: [20, 30], tx: 'B' }, - { lonlat: [30, 10], tx: 'C' } - ]); - }); - - it('should fill array text (invalid entry case)', function() { - var calcTrace = _calc(Lib.extendFlat({}, base, { - lon: [10, 20, 30], - lat: [20, 30, 10], - text: ['A', 'B', null] - })); - - expect(calcTrace).toEqual([ - { lonlat: [10, 20], tx: 'A' }, - { lonlat: [20, 30], tx: 'B' }, - { lonlat: [30, 10], tx: '' } - ]); - }); - - it('should fill array marker attributes (base case)', function() { - var calcTrace = _calc(Lib.extendFlat({}, base, { - lon: [10, 20, null, 30], - lat: [20, 30, null, 10], - marker: { - color: ['red', 'blue', 'green', 'yellow'], - size: [10, 20, 8, 10] - } - })); - - expect(calcTrace).toEqual([ - { lonlat: [10, 20], mc: 'red', ms: 10, mcc: 'red', mrc: 5 }, - { lonlat: [20, 30], mc: 'blue', ms: 20, mcc: 'blue', mrc: 10, gapAfter: true }, - { lonlat: [30, 10], mc: 'yellow', ms: 10, mcc: 'yellow', mrc: 5 } - ]); - }); - - it('should fill array marker attributes (invalid scale case)', function() { - var calcTrace = _calc(Lib.extendFlat({}, base, { - lon: [10, 20, null, 30], - lat: [20, 30, null, 10], - marker: { - color: [0, null, 5, 10], - size: [10, NaN, 8, 10], - colorscale: [ - [0, 'blue'], [0.5, 'red'], [1, 'green'] - ] - } - })); - - expect(calcTrace).toEqual([ - { lonlat: [10, 20], mc: 0, ms: 10, mcc: 'rgb(0, 0, 255)', mrc: 5 }, - { lonlat: [20, 30], mc: null, ms: NaN, mcc: '#444', mrc: 0, gapAfter: true }, - { lonlat: [30, 10], mc: 10, ms: 10, mcc: 'rgb(0, 128, 0)', mrc: 5 } - ]); - }); - - it('should fill marker attributes (symbol case)', function() { - var calcTrace = _calc(Lib.extendFlat({}, base, { - lon: [10, 20, null, 30], - lat: [20, 30, null, 10], - marker: { - symbol: ['monument', 'music', 'harbor', null] - } - })); - - expect(calcTrace).toEqual([ - { lonlat: [10, 20], mx: 'monument' }, - { lonlat: [20, 30], mx: 'music', gapAfter: true }, - { lonlat: [30, 10], mx: 'circle' } - ]); - }); -}); - -describe('scattermapbox convert', function() { - 'use strict'; - - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - function _convert(trace) { - var gd = { data: [trace] }; - Plots.supplyDefaults(gd); + expect(fullTrace.marker.color).toEqual("red"); + expect(fullTrace.marker.size).toEqual(10); - var fullTrace = gd._fullData[0]; - Plots.doCalcdata(gd, fullTrace); + fullTrace = _supply( + Lib.extendDeep({}, base, { marker: { symbol: "circle" } }) + ); - var calcTrace = gd.calcdata[0]; - return convert(calcTrace); + expect(fullTrace.marker.color).toEqual(["red", "green", "blue"]); + expect(fullTrace.marker.size).toEqual([10, 20, 30]); } + ); +}); - var base = { - type: 'scattermapbox', - lon: [10, '20', 30, 20, null, 20, 10], - lat: [20, 20, '10', null, 10, 10, 20] - }; - - it('for markers + circle bubbles traces, should', function() { - var opts = _convert(Lib.extendFlat({}, base, { - mode: 'markers', - marker: { - symbol: 'circle', - size: [10, 20, null, 10, '10'], - color: [10, null, '30', 20, 10] - } - })); - - assertVisibility(opts, ['none', 'none', 'visible', 'none']); - - expect(opts.circle.paint['circle-color']).toEqual({ - property: 'circle-color', - stops: [ - [0, 'rgb(220, 220, 220)'], [1, '#444'], [2, 'rgb(178, 10, 28)'] - ] - }, 'have correct circle-color stops'); - - expect(opts.circle.paint['circle-radius']).toEqual({ - property: 'circle-radius', - stops: [ [0, 5], [1, 10], [2, 0] ] - }, 'have correct circle-radius stops'); - - var circleProps = opts.circle.geojson.features.map(function(f) { - return f.properties; - }); - - // N.B repeated values have same geojson props - expect(circleProps).toEqual([ - { 'circle-color': 0, 'circle-radius': 0 }, - { 'circle-color': 1, 'circle-radius': 1 }, - { 'circle-color': 2, 'circle-radius': 2 }, - { 'circle-color': 1, 'circle-radius': 2 }, - { 'circle-color': 1, 'circle-radius': 2 } - ], 'have correct geojson feature properties'); - }); - - it('fill + markers + lines traces, should', function() { - var opts = _convert(Lib.extendFlat({}, base, { - mode: 'markers+lines', - marker: { symbol: 'circle' }, - fill: 'toself' - })); - - assertVisibility(opts, ['visible', 'visible', 'visible', 'none']); - - var segment1 = [[10, 20], [20, 20], [30, 10]], - segment2 = [[20, 10], [10, 20]]; - - var lineCoords = [segment1, segment2], - fillCoords = [[segment1], [segment2]]; - - expect(opts.line.geojson.coordinates).toEqual(lineCoords, 'have correct line coords'); - expect(opts.fill.geojson.coordinates).toEqual(fillCoords, 'have correct fill coords'); - - var circleCoords = opts.circle.geojson.features.map(function(f) { - return f.geometry.coordinates; - }); +describe("scattermapbox calc", function() { + "use strict"; + function _calc(trace) { + var gd = { data: [trace] }; + + Plots.supplyDefaults(gd); + + var fullTrace = gd._fullData[0]; + return ScatterMapbox.calc(gd, fullTrace); + } + + var base = { type: "scattermapbox" }; + + it("should place lon/lat data in lonlat pairs", function() { + var calcTrace = _calc( + Lib.extendFlat({}, base, { lon: [10, 20, 30], lat: [20, 30, 10] }) + ); + + expect(calcTrace).toEqual([ + { lonlat: [10, 20] }, + { lonlat: [20, 30] }, + { lonlat: [30, 10] } + ]); + }); + + it("should coerce numeric strings lon/lat data into numbers", function() { + var calcTrace = _calc( + Lib.extendFlat({}, base, { + lon: [10, 20, "30", "40"], + lat: [20, "30", 10, "50"] + }) + ); + + expect(calcTrace).toEqual([ + { lonlat: [10, 20] }, + { lonlat: [20, 30] }, + { lonlat: [30, 10] }, + { lonlat: [40, 50] } + ]); + }); + + it("should keep track of gaps in data", function() { + var calcTrace = _calc( + Lib.extendFlat({}, base, { + lon: [null, 10, null, null, 20, "30", null, "40", null, 10], + lat: [10, 20, "30", null, 10, "50", null, 60, null, null] + }) + ); + + expect(calcTrace).toEqual([ + { lonlat: [10, 20], gapAfter: true }, + { lonlat: [20, 10] }, + { lonlat: [30, 50], gapAfter: true }, + { lonlat: [40, 60], gapAfter: true } + ]); + }); + + it("should fill array text (base case)", function() { + var calcTrace = _calc( + Lib.extendFlat({}, base, { + lon: [10, 20, 30], + lat: [20, 30, 10], + text: ["A", "B", "C"] + }) + ); + + expect(calcTrace).toEqual([ + { lonlat: [10, 20], tx: "A" }, + { lonlat: [20, 30], tx: "B" }, + { lonlat: [30, 10], tx: "C" } + ]); + }); + + it("should fill array text (invalid entry case)", function() { + var calcTrace = _calc( + Lib.extendFlat({}, base, { + lon: [10, 20, 30], + lat: [20, 30, 10], + text: ["A", "B", null] + }) + ); + + expect(calcTrace).toEqual([ + { lonlat: [10, 20], tx: "A" }, + { lonlat: [20, 30], tx: "B" }, + { lonlat: [30, 10], tx: "" } + ]); + }); + + it("should fill array marker attributes (base case)", function() { + var calcTrace = _calc( + Lib.extendFlat({}, base, { + lon: [10, 20, null, 30], + lat: [20, 30, null, 10], + marker: { + color: ["red", "blue", "green", "yellow"], + size: [10, 20, 8, 10] + } + }) + ); + + expect(calcTrace).toEqual([ + { lonlat: [10, 20], mc: "red", ms: 10, mcc: "red", mrc: 5 }, + { + lonlat: [20, 30], + mc: "blue", + ms: 20, + mcc: "blue", + mrc: 10, + gapAfter: true + }, + { lonlat: [30, 10], mc: "yellow", ms: 10, mcc: "yellow", mrc: 5 } + ]); + }); + + it("should fill array marker attributes (invalid scale case)", function() { + var calcTrace = _calc( + Lib.extendFlat({}, base, { + lon: [10, 20, null, 30], + lat: [20, 30, null, 10], + marker: { + color: [0, null, 5, 10], + size: [10, NaN, 8, 10], + colorscale: [[0, "blue"], [0.5, "red"], [1, "green"]] + } + }) + ); + + expect(calcTrace).toEqual([ + { lonlat: [10, 20], mc: 0, ms: 10, mcc: "rgb(0, 0, 255)", mrc: 5 }, + { + lonlat: [20, 30], + mc: null, + ms: NaN, + mcc: "#444", + mrc: 0, + gapAfter: true + }, + { lonlat: [30, 10], mc: 10, ms: 10, mcc: "rgb(0, 128, 0)", mrc: 5 } + ]); + }); + + it("should fill marker attributes (symbol case)", function() { + var calcTrace = _calc( + Lib.extendFlat({}, base, { + lon: [10, 20, null, 30], + lat: [20, 30, null, 10], + marker: { symbol: ["monument", "music", "harbor", null] } + }) + ); + + expect(calcTrace).toEqual([ + { lonlat: [10, 20], mx: "monument" }, + { lonlat: [20, 30], mx: "music", gapAfter: true }, + { lonlat: [30, 10], mx: "circle" } + ]); + }); +}); - expect(circleCoords).toEqual([ - [10, 20], [20, 20], [30, 10], [20, 10], [10, 20] - ], 'have correct circle coords'); +describe("scattermapbox convert", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + function _convert(trace) { + var gd = { data: [trace] }; + Plots.supplyDefaults(gd); + + var fullTrace = gd._fullData[0]; + Plots.doCalcdata(gd, fullTrace); + + var calcTrace = gd.calcdata[0]; + return convert(calcTrace); + } + + var base = { + type: "scattermapbox", + lon: [10, "20", 30, 20, null, 20, 10], + lat: [20, 20, "10", null, 10, 10, 20] + }; + + it("for markers + circle bubbles traces, should", function() { + var opts = _convert( + Lib.extendFlat({}, base, { + mode: "markers", + marker: { + symbol: "circle", + size: [10, 20, null, 10, "10"], + color: [10, null, "30", 20, 10] + } + }) + ); + + assertVisibility(opts, ["none", "none", "visible", "none"]); + + expect(opts.circle.paint["circle-color"]).toEqual( + { + property: "circle-color", + stops: [[0, "rgb(220, 220, 220)"], [1, "#444"], [2, "rgb(178, 10, 28)"]] + }, + "have correct circle-color stops" + ); + + expect(opts.circle.paint["circle-radius"]).toEqual( + { property: "circle-radius", stops: [[0, 5], [1, 10], [2, 0]] }, + "have correct circle-radius stops" + ); + + var circleProps = opts.circle.geojson.features.map(function(f) { + return f.properties; }); - it('for markers + non-circle traces, should', function() { - var opts = _convert(Lib.extendFlat({}, base, { - mode: 'markers', - marker: { symbol: 'monument' } - })); - - assertVisibility(opts, ['none', 'none', 'none', 'visible']); - - var symbolProps = opts.symbol.geojson.features.map(function(f) { - return [f.properties.symbol, f.properties.text]; - }); - - var expected = opts.symbol.geojson.features.map(function() { - return ['monument', '']; - }); - - expect(symbolProps).toEqual(expected, 'have correct geojson properties'); + // N.B repeated values have same geojson props + expect(circleProps).toEqual( + [ + { "circle-color": 0, "circle-radius": 0 }, + { "circle-color": 1, "circle-radius": 1 }, + { "circle-color": 2, "circle-radius": 2 }, + { "circle-color": 1, "circle-radius": 2 }, + { "circle-color": 1, "circle-radius": 2 } + ], + "have correct geojson feature properties" + ); + }); + + it("fill + markers + lines traces, should", function() { + var opts = _convert( + Lib.extendFlat({}, base, { + mode: "markers+lines", + marker: { symbol: "circle" }, + fill: "toself" + }) + ); + + assertVisibility(opts, ["visible", "visible", "visible", "none"]); + + var segment1 = [[10, 20], [20, 20], [30, 10]], + segment2 = [[20, 10], [10, 20]]; + + var lineCoords = [segment1, segment2], + fillCoords = [[segment1], [segment2]]; + + expect(opts.line.geojson.coordinates).toEqual( + lineCoords, + "have correct line coords" + ); + expect(opts.fill.geojson.coordinates).toEqual( + fillCoords, + "have correct fill coords" + ); + + var circleCoords = opts.circle.geojson.features.map(function(f) { + return f.geometry.coordinates; }); - it('for text + lines traces, should', function() { - var opts = _convert(Lib.extendFlat({}, base, { - mode: 'lines+text', - connectgaps: true, - text: ['A', 'B', 'C', 'D', 'E', 'F'] - })); - - assertVisibility(opts, ['none', 'visible', 'none', 'visible']); + expect(circleCoords).toEqual( + [[10, 20], [20, 20], [30, 10], [20, 10], [10, 20]], + "have correct circle coords" + ); + }); - var lineCoords = [ - [10, 20], [20, 20], [30, 10], [20, 10], [10, 20] - ]; + it("for markers + non-circle traces, should", function() { + var opts = _convert( + Lib.extendFlat({}, base, { + mode: "markers", + marker: { symbol: "monument" } + }) + ); - expect(opts.line.geojson.coordinates).toEqual(lineCoords, 'have correct line coords'); + assertVisibility(opts, ["none", "none", "none", "visible"]); - var actualText = opts.symbol.geojson.features.map(function(f) { - return f.properties.text; - }); - - expect(actualText).toEqual(['A', 'B', 'C', 'F', '']); + var symbolProps = opts.symbol.geojson.features.map(function(f) { + return [f.properties.symbol, f.properties.text]; }); - it('should correctly convert \'textposition\' to \'text-anchor\' and \'text-offset\'', function() { - var specs = { - 'top left': ['top-right', [-0.65, -1.65]], - 'top center': ['top', [0, -1.65]], - 'top right': ['top-left', [0.65, -1.65]], - 'middle left': ['right', [-0.65, 0]], - 'middle center': ['center', [0, 0]], - 'middle right': ['left', [0.65, 0]], - 'bottom left': ['bottom-right', [-0.65, 1.65]], - 'bottom center': ['bottom', [0, 1.65]], - 'bottom right': ['bottom-left', [0.65, 1.65]] - }; - - Object.keys(specs).forEach(function(k) { - var spec = specs[k]; - - var opts = _convert(Lib.extendFlat({}, base, { - textposition: k, - mode: 'text+markers', - marker: { size: 15 }, - text: ['A', 'B', 'C'] - })); - - expect([ - opts.symbol.layout['text-anchor'], - opts.symbol.layout['text-offset'] - ]).toEqual(spec, '(case ' + k + ')'); - }); + var expected = opts.symbol.geojson.features.map(function() { + return ["monument", ""]; }); - it('for markers + circle bubbles traces with repeated values, should', function() { - var opts = _convert(Lib.extendFlat({}, base, { - lon: ['-96.796988', '-81.379236', '-85.311819', ''], - lat: ['32.776664', '28.538335', '35.047157', '' ], - marker: { size: ['5', '49', '5', ''] } - })); - - expect(opts.circle.paint['circle-radius'].stops) - .toBeCloseTo2DArray([[0, 2.5], [1, 24.5]], 'not replicate stops'); + expect(symbolProps).toEqual(expected, "have correct geojson properties"); + }); - var radii = opts.circle.geojson.features.map(function(f) { - return f.properties['circle-radius']; - }); + it("for text + lines traces, should", function() { + var opts = _convert( + Lib.extendFlat({}, base, { + mode: "lines+text", + connectgaps: true, + text: ["A", "B", "C", "D", "E", "F"] + }) + ); - expect(radii).toBeCloseToArray([0, 1, 0], 'link features to correct stops'); - }); + assertVisibility(opts, ["none", "visible", "none", "visible"]); - it('for input only blank pts', function() { - var opts = _convert(Lib.extendFlat({}, base, { - mode: 'lines', - lon: ['', null], - lat: [null, ''], - fill: 'toself' - })); + var lineCoords = [[10, 20], [20, 20], [30, 10], [20, 10], [10, 20]]; - assertVisibility(opts, ['none', 'none', 'none', 'none']); + expect(opts.line.geojson.coordinates).toEqual( + lineCoords, + "have correct line coords" + ); - expect(opts.line.geojson.coordinates).toEqual([], 'have correct line coords'); - expect(opts.fill.geojson.coordinates).toEqual([], 'have correct fill coords'); + var actualText = opts.symbol.geojson.features.map(function(f) { + return f.properties.text; }); - function assertVisibility(opts, expectations) { - var actual = ['fill', 'line', 'circle', 'symbol'].map(function(l) { - return opts[l].layout.visibility; - }); - - var msg = 'set layer visibility properly'; - - expect(actual).toEqual(expectations, msg); + expect(actualText).toEqual(["A", "B", "C", "F", ""]); + }); + + it( + "should correctly convert 'textposition' to 'text-anchor' and 'text-offset'", + function() { + var specs = { + "top left": ["top-right", [-0.65, -1.65]], + "top center": ["top", [0, -1.65]], + "top right": ["top-left", [0.65, -1.65]], + "middle left": ["right", [-0.65, 0]], + "middle center": ["center", [0, 0]], + "middle right": ["left", [0.65, 0]], + "bottom left": ["bottom-right", [-0.65, 1.65]], + "bottom center": ["bottom", [0, 1.65]], + "bottom right": ["bottom-left", [0.65, 1.65]] + }; + + Object.keys(specs).forEach(function(k) { + var spec = specs[k]; + + var opts = _convert( + Lib.extendFlat({}, base, { + textposition: k, + mode: "text+markers", + marker: { size: 15 }, + text: ["A", "B", "C"] + }) + ); + + expect([ + opts.symbol.layout["text-anchor"], + opts.symbol.layout["text-offset"] + ]).toEqual(spec, "(case " + k + ")"); + }); } -}); - -describe('scattermapbox hover', function() { - 'use strict'; - - if(!hasWebGLSupport('scattermapbox hover')) return; - - var hoverPoints = ScatterMapbox.hoverPoints; - - var gd; - - beforeAll(function(done) { - jasmine.addMatchers(customMatchers); - - gd = createGraphDiv(); - - var data = [{ - type: 'scattermapbox', - lon: [10, 20, 30], - lat: [10, 20, 30], - text: ['A', 'B', 'C'] - }]; + ); + + it( + "for markers + circle bubbles traces with repeated values, should", + function() { + var opts = _convert( + Lib.extendFlat({}, base, { + lon: ["-96.796988", "-81.379236", "-85.311819", ""], + lat: ["32.776664", "28.538335", "35.047157", ""], + marker: { size: ["5", "49", "5", ""] } + }) + ); - Plotly.plot(gd, data, { autosize: true }).then(done); - }); + expect(opts.circle.paint["circle-radius"].stops).toBeCloseTo2DArray( + [[0, 2.5], [1, 24.5]], + "not replicate stops" + ); - afterAll(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); + var radii = opts.circle.geojson.features.map(function(f) { + return f.properties["circle-radius"]; + }); - function getPointData(gd) { - var cd = gd.calcdata, - mapbox = gd._fullLayout.mapbox._subplot; - - return { - index: false, - distance: 20, - cd: cd[0], - trace: cd[0][0].trace, - xa: mapbox.xaxis, - ya: mapbox.yaxis - }; + expect(radii).toBeCloseToArray( + [0, 1, 0], + "link features to correct stops" + ); } - - it('should generate hover label info (base case)', function() { - var xval = 11, - yval = 11; - - var out = hoverPoints(getPointData(gd), xval, yval)[0]; - - expect(out.index).toEqual(0); - expect([out.x0, out.x1, out.y0, out.y1]).toBeCloseToArray([ - 297.444, 299.444, 105.410, 107.410 - ]); - expect(out.extraText).toEqual('(10°, 10°)
A'); - expect(out.color).toEqual('#1f77b4'); + ); + + it("for input only blank pts", function() { + var opts = _convert( + Lib.extendFlat({}, base, { + mode: "lines", + lon: ["", null], + lat: [null, ""], + fill: "toself" + }) + ); + + assertVisibility(opts, ["none", "none", "none", "none"]); + + expect(opts.line.geojson.coordinates).toEqual( + [], + "have correct line coords" + ); + expect(opts.fill.geojson.coordinates).toEqual( + [], + "have correct fill coords" + ); + }); + + function assertVisibility(opts, expectations) { + var actual = ["fill", "line", "circle", "symbol"].map(function(l) { + return opts[l].layout.visibility; }); - it('should skip over blank and non-string text items', function(done) { - var xval = 11, - yval = 11, - out; - - Plotly.restyle(gd, 'text', [['', 'B', 'C']]).then(function() { - out = hoverPoints(getPointData(gd), xval, yval)[0]; - expect(out.extraText).toEqual('(10°, 10°)'); + var msg = "set layer visibility properly"; - return Plotly.restyle(gd, 'text', [[null, 'B', 'C']]); - }) - .then(function() { - out = hoverPoints(getPointData(gd), xval, yval)[0]; - expect(out.extraText).toEqual('(10°, 10°)'); - - return Plotly.restyle(gd, 'text', [[false, 'B', 'C']]); - }) - .then(function() { - out = hoverPoints(getPointData(gd), xval, yval)[0]; - expect(out.extraText).toEqual('(10°, 10°)'); + expect(actual).toEqual(expectations, msg); + } +}); - return Plotly.restyle(gd, 'text', [['A', 'B', 'C']]); - }) - .then(function() { - out = hoverPoints(getPointData(gd), xval, yval)[0]; - expect(out.extraText).toEqual('(10°, 10°)
A'); - }) - .then(done); - }); +describe("scattermapbox hover", function() { + "use strict"; + if (!hasWebGLSupport("scattermapbox hover")) return; - it('should generate hover label info (positive winding case)', function() { - var xval = 11 + 720, - yval = 11; + var hoverPoints = ScatterMapbox.hoverPoints; - var out = hoverPoints(getPointData(gd), xval, yval)[0]; + var gd; - expect(out.index).toEqual(0); - expect([out.x0, out.x1, out.y0, out.y1]).toBeCloseToArray([ - 2345.444, 2347.444, 105.410, 107.410 - ]); - expect(out.extraText).toEqual('(10°, 10°)
A'); - expect(out.color).toEqual('#1f77b4'); - }); + beforeAll(function(done) { + jasmine.addMatchers(customMatchers); - it('should generate hover label info (negative winding case)', function() { - var xval = 11 - 1080, - yval = 11; + gd = createGraphDiv(); - var out = hoverPoints(getPointData(gd), xval, yval)[0]; + var data = [ + { + type: "scattermapbox", + lon: [10, 20, 30], + lat: [10, 20, 30], + text: ["A", "B", "C"] + } + ]; - expect(out.index).toEqual(0); - expect([out.x0, out.x1, out.y0, out.y1]).toBeCloseToArray([ - -2774.555, -2772.555, 105.410, 107.410 - ]); - expect(out.extraText).toEqual('(10°, 10°)
A'); - expect(out.color).toEqual('#1f77b4'); - }); + Plotly.plot(gd, data, { autosize: true }).then(done); + }); - it('should generate hover label info (hoverinfo: \'lon\' case)', function(done) { - Plotly.restyle(gd, 'hoverinfo', 'lon').then(function() { - var xval = 11, - yval = 11; + afterAll(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - var out = hoverPoints(getPointData(gd), xval, yval)[0]; + function getPointData(gd) { + var cd = gd.calcdata, mapbox = gd._fullLayout.mapbox._subplot; - expect(out.extraText).toEqual('lon: 10°'); - done(); - }); + return { + index: false, + distance: 20, + cd: cd[0], + trace: cd[0][0].trace, + xa: mapbox.xaxis, + ya: mapbox.yaxis + }; + } + + it("should generate hover label info (base case)", function() { + var xval = 11, yval = 11; + + var out = hoverPoints(getPointData(gd), xval, yval)[0]; + + expect(out.index).toEqual(0); + expect([out.x0, out.x1, out.y0, out.y1]).toBeCloseToArray([ + 297.444, + 299.444, + 105.410, + 107.410 + ]); + expect(out.extraText).toEqual("(10\xB0, 10\xB0)
A"); + expect(out.color).toEqual("#1f77b4"); + }); + + it("should skip over blank and non-string text items", function(done) { + var xval = 11, yval = 11, out; + + Plotly.restyle(gd, "text", [["", "B", "C"]]) + .then(function() { + out = hoverPoints(getPointData(gd), xval, yval)[0]; + expect(out.extraText).toEqual("(10\xB0, 10\xB0)"); + + return Plotly.restyle(gd, "text", [[null, "B", "C"]]); + }) + .then(function() { + out = hoverPoints(getPointData(gd), xval, yval)[0]; + expect(out.extraText).toEqual("(10\xB0, 10\xB0)"); + + return Plotly.restyle(gd, "text", [[false, "B", "C"]]); + }) + .then(function() { + out = hoverPoints(getPointData(gd), xval, yval)[0]; + expect(out.extraText).toEqual("(10\xB0, 10\xB0)"); + + return Plotly.restyle(gd, "text", [["A", "B", "C"]]); + }) + .then(function() { + out = hoverPoints(getPointData(gd), xval, yval)[0]; + expect(out.extraText).toEqual("(10\xB0, 10\xB0)
A"); + }) + .then(done); + }); + + it("should generate hover label info (positive winding case)", function() { + var xval = 11 + 720, yval = 11; + + var out = hoverPoints(getPointData(gd), xval, yval)[0]; + + expect(out.index).toEqual(0); + expect([out.x0, out.x1, out.y0, out.y1]).toBeCloseToArray([ + 2345.444, + 2347.444, + 105.410, + 107.410 + ]); + expect(out.extraText).toEqual("(10\xB0, 10\xB0)
A"); + expect(out.color).toEqual("#1f77b4"); + }); + + it("should generate hover label info (negative winding case)", function() { + var xval = 11 - 1080, yval = 11; + + var out = hoverPoints(getPointData(gd), xval, yval)[0]; + + expect(out.index).toEqual(0); + expect([out.x0, out.x1, out.y0, out.y1]).toBeCloseToArray([ + -2774.555, + -2772.555, + 105.410, + 107.410 + ]); + expect(out.extraText).toEqual("(10\xB0, 10\xB0)
A"); + expect(out.color).toEqual("#1f77b4"); + }); + + it("should generate hover label info (hoverinfo: 'lon' case)", function( + done + ) { + Plotly.restyle(gd, "hoverinfo", "lon").then(function() { + var xval = 11, yval = 11; + + var out = hoverPoints(getPointData(gd), xval, yval)[0]; + + expect(out.extraText).toEqual("lon: 10\xB0"); + done(); }); + }); - it('should generate hover label info (hoverinfo: \'lat\' case)', function(done) { - Plotly.restyle(gd, 'hoverinfo', 'lat').then(function() { - var xval = 11, - yval = 11; + it("should generate hover label info (hoverinfo: 'lat' case)", function( + done + ) { + Plotly.restyle(gd, "hoverinfo", "lat").then(function() { + var xval = 11, yval = 11; - var out = hoverPoints(getPointData(gd), xval, yval)[0]; + var out = hoverPoints(getPointData(gd), xval, yval)[0]; - expect(out.extraText).toEqual('lat: 10°'); - done(); - }); + expect(out.extraText).toEqual("lat: 10\xB0"); + done(); }); + }); - it('should generate hover label info (hoverinfo: \'text\' case)', function(done) { - Plotly.restyle(gd, 'hoverinfo', 'text').then(function() { - var xval = 11, - yval = 11; + it("should generate hover label info (hoverinfo: 'text' case)", function( + done + ) { + Plotly.restyle(gd, "hoverinfo", "text").then(function() { + var xval = 11, yval = 11; - var out = hoverPoints(getPointData(gd), xval, yval)[0]; + var out = hoverPoints(getPointData(gd), xval, yval)[0]; - expect(out.extraText).toEqual('A'); - done(); - }); + expect(out.extraText).toEqual("A"); + done(); }); + }); }); diff --git a/test/jasmine/tests/scatterternary_test.js b/test/jasmine/tests/scatterternary_test.js index 21da09876f0..bba0003b4d4 100644 --- a/test/jasmine/tests/scatterternary_test.js +++ b/test/jasmine/tests/scatterternary_test.js @@ -1,351 +1,338 @@ -var Plotly = require('@lib'); -var Lib = require('@src/lib'); -var ScatterTernary = require('@src/traces/scatterternary'); +var Plotly = require("@lib"); +var Lib = require("@src/lib"); +var ScatterTernary = require("@src/traces/scatterternary"); -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var customMatchers = require('../assets/custom_matchers'); +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var customMatchers = require("../assets/custom_matchers"); +describe("scatterternary defaults", function() { + "use strict"; + var supplyDefaults = ScatterTernary.supplyDefaults; -describe('scatterternary defaults', function() { - 'use strict'; + var traceIn, traceOut; - var supplyDefaults = ScatterTernary.supplyDefaults; + var defaultColor = "#444", layout = {}; - var traceIn, traceOut; + beforeEach(function() { + traceOut = {}; + }); - var defaultColor = '#444', - layout = {}; + it( + "should allow one of 'a', 'b' or 'c' to be missing (base case)", + function() { + traceIn = { a: [1, 2, 3], b: [1, 2, 3], c: [1, 2, 3] }; - beforeEach(function() { - traceOut = {}; - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).not.toBe(true); + } + ); - it('should allow one of \'a\', \'b\' or \'c\' to be missing (base case)', function() { - traceIn = { - a: [1, 2, 3], - b: [1, 2, 3], - c: [1, 2, 3] - }; + it( + "should allow one of 'a', 'b' or 'c' to be missing ('c' is missing case)", + function() { + traceIn = { a: [1, 2, 3], b: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).not.toBe(true); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).not.toBe(true); + } + ); - it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'c\' is missing case)', function() { - traceIn = { - a: [1, 2, 3], - b: [1, 2, 3] - }; + it( + "should allow one of 'a', 'b' or 'c' to be missing ('b' is missing case)", + function() { + traceIn = { a: [1, 2, 3], c: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).not.toBe(true); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).not.toBe(true); + } + ); - it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'b\' is missing case)', function() { - traceIn = { - a: [1, 2, 3], - c: [1, 2, 3] - }; + it( + "should allow one of 'a', 'b' or 'c' to be missing ('a' is missing case)", + function() { + traceIn = { b: [1, 2, 3], c: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).not.toBe(true); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).not.toBe(true); + } + ); - it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'a\' is missing case)', function() { - traceIn = { - b: [1, 2, 3], - c: [1, 2, 3] - }; + it( + "should allow one of 'a', 'b' or 'c' to be missing ('b and 'c' are missing case)", + function() { + traceIn = { a: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).not.toBe(true); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + } + ); - it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'b\ and \'c\' are missing case)', function() { - traceIn = { - a: [1, 2, 3] - }; + it( + "should allow one of 'a', 'b' or 'c' to be missing ('a and 'c' are missing case)", + function() { + traceIn = { b: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + } + ); - it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'a\ and \'c\' are missing case)', function() { - traceIn = { - b: [1, 2, 3] - }; + it( + "should allow one of 'a', 'b' or 'c' to be missing ('a and 'b' are missing case)", + function() { + traceIn = { c: [1, 2, 3] }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + } + ); - it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'a\ and \'b\' are missing case)', function() { - traceIn = { - c: [1, 2, 3] - }; + it( + "should allow one of 'a', 'b' or 'c' to be missing (all are missing case)", + function() { + traceIn = {}; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + } + ); - it('should allow one of \'a\', \'b\' or \'c\' to be missing (all are missing case)', function() { - traceIn = {}; + it( + "should truncate data arrays to the same length ('c' is shortest case)", + function() { + traceIn = { a: [1, 2, 3], b: [1, 2], c: [1] }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.a).toEqual([1]); + expect(traceOut.b).toEqual([1]); + expect(traceOut.c).toEqual([1]); + } + ); - it('should truncate data arrays to the same length (\'c\' is shortest case)', function() { - traceIn = { - a: [1, 2, 3], - b: [1, 2], - c: [1] - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.a).toEqual([1]); - expect(traceOut.b).toEqual([1]); - expect(traceOut.c).toEqual([1]); - }); + it( + "should truncate data arrays to the same length ('a' is shortest case)", + function() { + traceIn = { a: [1], b: [1, 2, 3], c: [1, 2] }; - it('should truncate data arrays to the same length (\'a\' is shortest case)', function() { - traceIn = { - a: [1], - b: [1, 2, 3], - c: [1, 2] - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.a).toEqual([1]); - expect(traceOut.b).toEqual([1]); - expect(traceOut.c).toEqual([1]); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.a).toEqual([1]); + expect(traceOut.b).toEqual([1]); + expect(traceOut.c).toEqual([1]); + } + ); - it('should truncate data arrays to the same length (\'a\' is shortest case)', function() { - traceIn = { - a: [1, 2], - b: [1], - c: [1, 2, 3] - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.a).toEqual([1]); - expect(traceOut.b).toEqual([1]); - expect(traceOut.c).toEqual([1]); - }); - it('should include \'name\' in \'hoverinfo\' default if multi trace graph', function() { - traceIn = { - a: [1, 2, 3], - b: [1, 2, 3], - c: [1, 2, 3] - }; - layout._dataLength = 2; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.hoverinfo).toBe('all'); - }); + it( + "should truncate data arrays to the same length ('a' is shortest case)", + function() { + traceIn = { a: [1, 2], b: [1], c: [1, 2, 3] }; - it('should not include \'name\' in \'hoverinfo\' default if single trace graph', function() { - traceIn = { - a: [1, 2, 3], - b: [1, 2, 3], - c: [1, 2, 3] - }; - layout._dataLength = 1; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.a).toEqual([1]); + expect(traceOut.b).toEqual([1]); + expect(traceOut.c).toEqual([1]); + } + ); + it( + "should include 'name' in 'hoverinfo' default if multi trace graph", + function() { + traceIn = { a: [1, 2, 3], b: [1, 2, 3], c: [1, 2, 3] }; + layout._dataLength = 2; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.hoverinfo).toBe("all"); + } + ); - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.hoverinfo).toBe('a+b+c+text'); - }); + it( + "should not include 'name' in 'hoverinfo' default if single trace graph", + function() { + traceIn = { a: [1, 2, 3], b: [1, 2, 3], c: [1, 2, 3] }; + layout._dataLength = 1; - it('should correctly assign \'hoveron\' default', function() { - traceIn = { - a: [1, 2, 3], - b: [1, 2, 3], - c: [1, 2, 3], - mode: 'lines+markers', - fill: 'tonext' - }; - - // fills and markers, you get both hover types - // you need visible: true here, as that normally gets set - // outside of the module supplyDefaults - traceOut = {visible: true}; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.hoveron).toBe('points+fills'); - - // but with only lines (or just fill) and fill tonext or toself - // you get fills - traceIn.mode = 'lines'; - traceOut = {visible: true}; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.hoveron).toBe('fills'); - - // without a fill you always get points. For scatterternary, unlike - // scatter, every allowed fill but 'none' is an area fill (rather than - // a vertical / horizontal fill) so they all should default to - // hoveron points. - traceIn.fill = 'none'; - traceOut = {visible: true}; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.hoveron).toBe('points'); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.hoverinfo).toBe("a+b+c+text"); + } + ); + + it("should correctly assign 'hoveron' default", function() { + traceIn = { + a: [1, 2, 3], + b: [1, 2, 3], + c: [1, 2, 3], + mode: "lines+markers", + fill: "tonext" + }; + + // fills and markers, you get both hover types + // you need visible: true here, as that normally gets set + // outside of the module supplyDefaults + traceOut = { visible: true }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.hoveron).toBe("points+fills"); + + // but with only lines (or just fill) and fill tonext or toself + // you get fills + traceIn.mode = "lines"; + traceOut = { visible: true }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.hoveron).toBe("fills"); + + // without a fill you always get points. For scatterternary, unlike + // scatter, every allowed fill but 'none' is an area fill (rather than + // a vertical / horizontal fill) so they all should default to + // hoveron points. + traceIn.fill = "none"; + traceOut = { visible: true }; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.hoveron).toBe("points"); + }); }); -describe('scatterternary calc', function() { - 'use strict'; +describe("scatterternary calc", function() { + "use strict"; + var calc = ScatterTernary.calc; - var calc = ScatterTernary.calc; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); + var gd, trace, cd; - var gd, trace, cd; + beforeEach(function() { + gd = { _fullLayout: { ternary: { sum: 1 } } }; - beforeEach(function() { - gd = { - _fullLayout: { - ternary: { sum: 1 } - } - }; + trace = { subplot: "ternary", sum: 1 }; + }); - trace = { - subplot: 'ternary', - sum: 1 - }; - }); + it("should fill in missing component (case 'c')", function() { + trace.a = [0.1, 0.3, 0.6]; + trace.b = [0.3, 0.6, 0.1]; - it('should fill in missing component (case \'c\')', function() { - trace.a = [0.1, 0.3, 0.6]; - trace.b = [0.3, 0.6, 0.1]; + calc(gd, trace); + expect(trace.c).toBeCloseToArray([0.6, 0.1, 0.3]); + }); - calc(gd, trace); - expect(trace.c).toBeCloseToArray([0.6, 0.1, 0.3]); - }); + it("should fill in missing component (case 'b')", function() { + trace.a = [0.1, 0.3, 0.6]; + trace.c = [0.1, 0.3, 0.2]; - it('should fill in missing component (case \'b\')', function() { - trace.a = [0.1, 0.3, 0.6]; - trace.c = [0.1, 0.3, 0.2]; + calc(gd, trace); + expect(trace.b).toBeCloseToArray([0.8, 0.4, 0.2]); + }); - calc(gd, trace); - expect(trace.b).toBeCloseToArray([0.8, 0.4, 0.2]); - }); + it("should fill in missing component (case 'a')", function() { + trace.b = [0.1, 0.3, 0.6]; + trace.c = [0.8, 0.4, 0.1]; - it('should fill in missing component (case \'a\')', function() { - trace.b = [0.1, 0.3, 0.6]; - trace.c = [0.8, 0.4, 0.1]; + calc(gd, trace); + expect(trace.a).toBeCloseToArray([0.1, 0.3, 0.3]); + }); - calc(gd, trace); - expect(trace.a).toBeCloseToArray([0.1, 0.3, 0.3]); - }); + it("should skip over non-numeric values", function() { + trace.a = [0.1, "a", 0.6]; + trace.b = [0.1, 0.3, null]; + trace.c = [8, 0.4, 0.1]; - it('should skip over non-numeric values', function() { - trace.a = [0.1, 'a', 0.6]; - trace.b = [0.1, 0.3, null]; - trace.c = [8, 0.4, 0.1]; + cd = calc(gd, trace); - cd = calc(gd, trace); + expect(objectToArray(cd[0])).toBeCloseToArray([ + 0.963414634, + 0.012195121, + 0.012195121, + 0.012195121, + 0.975609756 + ]); + expect(cd[1]).toEqual({ x: false, y: false }); + expect(cd[2]).toEqual({ x: false, y: false }); + }); - expect(objectToArray(cd[0])).toBeCloseToArray([ - 0.963414634, 0.012195121, 0.012195121, 0.012195121, 0.975609756 - ]); - expect(cd[1]).toEqual({ x: false, y: false }); - expect(cd[2]).toEqual({ x: false, y: false }); + function objectToArray(obj) { + return Object.keys(obj).map(function(k) { + return obj[k]; }); - - function objectToArray(obj) { - return Object.keys(obj).map(function(k) { - return obj[k]; - }); - } - + } }); -describe('scatterternary plot and hover', function() { - 'use strict'; +describe("scatterternary plot and hover", function() { + "use strict"; + var mock = require("@mocks/ternary_simple.json"); - var mock = require('@mocks/ternary_simple.json'); + afterAll(destroyGraphDiv); - afterAll(destroyGraphDiv); + beforeAll(function(done) { + var gd = createGraphDiv(); + var mockCopy = Lib.extendDeep({}, mock); - beforeAll(function(done) { - var gd = createGraphDiv(); - var mockCopy = Lib.extendDeep({}, mock); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); - - it('should put scatterternary trace in \'frontplot\' node', function() { - var nodes = d3.select('.frontplot').selectAll('.scatter'); + it("should put scatterternary trace in 'frontplot' node", function() { + var nodes = d3.select(".frontplot").selectAll(".scatter"); - expect(nodes.size()).toEqual(1); - }); + expect(nodes.size()).toEqual(1); + }); - it('should generate one line path per trace', function() { - var nodes = d3.selectAll('path.js-line'); + it("should generate one line path per trace", function() { + var nodes = d3.selectAll("path.js-line"); - expect(nodes.size()).toEqual(mock.data.length); - }); + expect(nodes.size()).toEqual(mock.data.length); + }); - it('should generate as many points as there are data points', function() { - var nodes = d3.selectAll('path.point'); + it("should generate as many points as there are data points", function() { + var nodes = d3.selectAll("path.point"); - expect(nodes.size()).toEqual(mock.data[0].a.length); - }); + expect(nodes.size()).toEqual(mock.data[0].a.length); + }); }); -describe('scatterternary hover', function() { - 'use strict'; +describe("scatterternary hover", function() { + "use strict"; + var hoverPoints = ScatterTernary.hoverPoints; - var hoverPoints = ScatterTernary.hoverPoints; + var gd, pointData; - var gd, pointData; + beforeAll(function(done) { + gd = createGraphDiv(); - beforeAll(function(done) { - gd = createGraphDiv(); + var data = [ + { + type: "scatterternary", + a: [0.1, 0.2, 0.3], + b: [0.3, 0.2, 0.1], + c: [0.1, 0.4, 0.5] + } + ]; - var data = [{ - type: 'scatterternary', - a: [0.1, 0.2, 0.3], - b: [0.3, 0.2, 0.1], - c: [0.1, 0.4, 0.5] - }]; + Plotly.plot(gd, data).then(done); + }); - Plotly.plot(gd, data).then(done); - }); + beforeEach(function() { + var cd = gd.calcdata, ternary = gd._fullLayout.ternary._subplot; - beforeEach(function() { - var cd = gd.calcdata, - ternary = gd._fullLayout.ternary._subplot; + pointData = { + index: false, + distance: 20, + cd: cd[0], + trace: cd[0][0].trace, + xa: ternary.xaxis, + ya: ternary.yaxis + }; + }); - pointData = { - index: false, - distance: 20, - cd: cd[0], - trace: cd[0][0].trace, - xa: ternary.xaxis, - ya: ternary.yaxis - }; + afterAll(destroyGraphDiv); - }); - - afterAll(destroyGraphDiv); - - it('should generate extra text field on hover', function() { - var xval = 0.42, - yval = 0.37, - hovermode = 'closest'; + it("should generate extra text field on hover", function() { + var xval = 0.42, yval = 0.37, hovermode = "closest"; - var scatterPointData = hoverPoints(pointData, xval, yval, hovermode); + var scatterPointData = hoverPoints(pointData, xval, yval, hovermode); - expect(scatterPointData[0].extraText).toEqual( - 'Component A: 0.3333333
Component B: 0.1111111
Component C: 0.5555556' - ); - - expect(scatterPointData[0].xLabelVal).toBeUndefined(); - expect(scatterPointData[0].yLabelVal).toBeUndefined(); - }); + expect(scatterPointData[0].extraText).toEqual( + "Component A: 0.3333333
Component B: 0.1111111
Component C: 0.5555556" + ); + expect(scatterPointData[0].xLabelVal).toBeUndefined(); + expect(scatterPointData[0].yLabelVal).toBeUndefined(); + }); }); diff --git a/test/jasmine/tests/search_test.js b/test/jasmine/tests/search_test.js index 6b6b122c117..e44c3793439 100644 --- a/test/jasmine/tests/search_test.js +++ b/test/jasmine/tests/search_test.js @@ -1,37 +1,36 @@ -var Lib = require('@src/lib'); +var Lib = require("@src/lib"); -describe('Test search.js:', function() { - 'use strict'; - - describe('findBin', function() { - it('should work on ascending arrays', function() { - expect(Lib.findBin(-10000, [0, 1, 3])).toBe(-1); - expect(Lib.findBin(0.5, [0, 1, 3])).toBe(0); - expect(Lib.findBin(2, [0, 1, 3])).toBe(1); - expect(Lib.findBin(10000, [0, 1, 3])).toBe(2); - // default: linelow falsey, so the line is in the higher bin - expect(Lib.findBin(1, [0, 1, 3])).toBe(1); - // linelow truthy, so the line is in the lower bin - expect(Lib.findBin(1, [0, 1, 3], true)).toBe(0); - }); +describe("Test search.js:", function() { + "use strict"; + describe("findBin", function() { + it("should work on ascending arrays", function() { + expect(Lib.findBin(-10000, [0, 1, 3])).toBe(-1); + expect(Lib.findBin(0.5, [0, 1, 3])).toBe(0); + expect(Lib.findBin(2, [0, 1, 3])).toBe(1); + expect(Lib.findBin(10000, [0, 1, 3])).toBe(2); + // default: linelow falsey, so the line is in the higher bin + expect(Lib.findBin(1, [0, 1, 3])).toBe(1); + // linelow truthy, so the line is in the lower bin + expect(Lib.findBin(1, [0, 1, 3], true)).toBe(0); + }); - it('should work on decending arrays', function() { - expect(Lib.findBin(-10000, [3, 1, 0])).toBe(2); - expect(Lib.findBin(0.5, [3, 1, 0])).toBe(1); - expect(Lib.findBin(2, [3, 1, 0])).toBe(0); - expect(Lib.findBin(10000, [3, 1, 0])).toBe(-1); + it("should work on decending arrays", function() { + expect(Lib.findBin(-10000, [3, 1, 0])).toBe(2); + expect(Lib.findBin(0.5, [3, 1, 0])).toBe(1); + expect(Lib.findBin(2, [3, 1, 0])).toBe(0); + expect(Lib.findBin(10000, [3, 1, 0])).toBe(-1); - expect(Lib.findBin(1, [3, 1, 0])).toBe(0); - expect(Lib.findBin(1, [3, 1, 0], true)).toBe(1); - }); + expect(Lib.findBin(1, [3, 1, 0])).toBe(0); + expect(Lib.findBin(1, [3, 1, 0], true)).toBe(1); + }); - it('should treat a length-1 array as ascending', function() { - expect(Lib.findBin(-1, [0])).toBe(-1); - expect(Lib.findBin(1, [0])).toBe(0); + it("should treat a length-1 array as ascending", function() { + expect(Lib.findBin(-1, [0])).toBe(-1); + expect(Lib.findBin(1, [0])).toBe(0); - expect(Lib.findBin(0, [0])).toBe(0); - expect(Lib.findBin(0, [0], true)).toBe(-1); - }); - // TODO: didn't test bins as objects {start, stop, size} + expect(Lib.findBin(0, [0])).toBe(0); + expect(Lib.findBin(0, [0], true)).toBe(-1); }); + // TODO: didn't test bins as objects {start, stop, size} + }); }); diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index f2be564a41d..917c55f5105 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -1,325 +1,289 @@ -var d3 = require('d3'); +var d3 = require("d3"); -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); -var doubleClick = require('../assets/double_click'); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); +var doubleClick = require("../assets/double_click"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); -var customMatchers = require('../assets/custom_matchers'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); +var customMatchers = require("../assets/custom_matchers"); +describe("select box and lasso", function() { + var mock = require("@mocks/14.json"); -describe('select box and lasso', function() { - var mock = require('@mocks/14.json'); + var selectPath = [[93, 193], [143, 193]]; + var lassoPath = [[316, 171], [318, 239], [335, 243], [328, 169]]; - var selectPath = [[93, 193], [143, 193]]; - var lassoPath = [[316, 171], [318, 239], [335, 243], [328, 169]]; + beforeEach(function() { + jasmine.addMatchers(customMatchers); + }); - beforeEach(function() { - jasmine.addMatchers(customMatchers); - }); + afterEach(destroyGraphDiv); - afterEach(destroyGraphDiv); + function drag(path) { + var len = path.length; - function drag(path) { - var len = path.length; + mouseEvent("mousemove", path[0][0], path[0][1]); + mouseEvent("mousedown", path[0][0], path[0][1]); - mouseEvent('mousemove', path[0][0], path[0][1]); - mouseEvent('mousedown', path[0][0], path[0][1]); + path.slice(1, len).forEach(function(pt) { + mouseEvent("mousemove", pt[0], pt[1]); + }); - path.slice(1, len).forEach(function(pt) { - mouseEvent('mousemove', pt[0], pt[1]); - }); + mouseEvent("mouseup", path[len - 1][0], path[len - 1][1]); + } - mouseEvent('mouseup', path[len - 1][0], path[len - 1][1]); - } + function assertRange(actual, expected) { + var PRECISION = 4; - function assertRange(actual, expected) { - var PRECISION = 4; + expect(actual.x).toBeCloseToArray(expected.x, PRECISION); + expect(actual.y).toBeCloseToArray(expected.y, PRECISION); + } - expect(actual.x).toBeCloseToArray(expected.x, PRECISION); - expect(actual.y).toBeCloseToArray(expected.y, PRECISION); - } + describe("select elements", function() { + var mockCopy = Lib.extendDeep({}, mock); + mockCopy.layout.dragmode = "select"; - describe('select elements', function() { - var mockCopy = Lib.extendDeep({}, mock); - mockCopy.layout.dragmode = 'select'; + var gd; + beforeEach(function(done) { + gd = createGraphDiv(); - var gd; - beforeEach(function(done) { - gd = createGraphDiv(); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout) - .then(done); - }); + it("should be appended to the zoom layer", function(done) { + var x0 = 100, y0 = 200, x1 = 150, y1 = 250, x2 = 50, y2 = 50; - it('should be appended to the zoom layer', function(done) { - var x0 = 100, - y0 = 200, - x1 = 150, - y1 = 250, - x2 = 50, - y2 = 50; - - gd.once('plotly_selecting', function() { - expect(d3.selectAll('.zoomlayer > .zoombox-corners').size()) - .toEqual(1); - expect(d3.selectAll('.zoomlayer > .select-outline').size()) - .toEqual(2); - }); - - gd.once('plotly_selected', function() { - expect(d3.selectAll('.zoomlayer > .zoombox-corners').size()) - .toEqual(0); - expect(d3.selectAll('.zoomlayer > .select-outline').size()) - .toEqual(2); - }); - - gd.once('plotly_deselect', function() { - expect(d3.selectAll('.zoomlayer > .select-outline').size()) - .toEqual(0); - }); - - mouseEvent('mousemove', x0, y0); - expect(d3.selectAll('.zoomlayer > .zoombox-corners').size()) - .toEqual(0); - - drag([[x0, y0], [x1, y1]]); - - doubleClick(x2, y2).then(done); - }); - }); + gd.once("plotly_selecting", function() { + expect(d3.selectAll(".zoomlayer > .zoombox-corners").size()).toEqual(1); + expect(d3.selectAll(".zoomlayer > .select-outline").size()).toEqual(2); + }); - describe('lasso elements', function() { - var mockCopy = Lib.extendDeep({}, mock); - mockCopy.layout.dragmode = 'lasso'; + gd.once("plotly_selected", function() { + expect(d3.selectAll(".zoomlayer > .zoombox-corners").size()).toEqual(0); + expect(d3.selectAll(".zoomlayer > .select-outline").size()).toEqual(2); + }); - var gd; - beforeEach(function(done) { - gd = createGraphDiv(); + gd.once("plotly_deselect", function() { + expect(d3.selectAll(".zoomlayer > .select-outline").size()).toEqual(0); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout) - .then(done); - }); + mouseEvent("mousemove", x0, y0); + expect(d3.selectAll(".zoomlayer > .zoombox-corners").size()).toEqual(0); - it('should be appended to the zoom layer', function(done) { - var x0 = 100, - y0 = 200, - x1 = 150, - y1 = 250, - x2 = 50, - y2 = 50; - - gd.once('plotly_selecting', function() { - expect(d3.selectAll('.zoomlayer > .zoombox-corners').size()) - .toEqual(1); - expect(d3.selectAll('.zoomlayer > .select-outline').size()) - .toEqual(2); - }); - - gd.once('plotly_selected', function() { - expect(d3.selectAll('.zoomlayer > .zoombox-corners').size()) - .toEqual(0); - expect(d3.selectAll('.zoomlayer > .select-outline').size()) - .toEqual(2); - }); - - gd.once('plotly_deselect', function() { - expect(d3.selectAll('.zoomlayer > .select-outline').size()) - .toEqual(0); - }); - - mouseEvent('mousemove', x0, y0); - expect(d3.selectAll('.zoomlayer > .zoombox-corners').size()) - .toEqual(0); - - drag([[x0, y0], [x1, y1]]); - - doubleClick(x2, y2).then(done); - }); + drag([[x0, y0], [x1, y1]]); + + doubleClick(x2, y2).then(done); }); + }); - describe('select events', function() { - var mockCopy = Lib.extendDeep({}, mock); - mockCopy.layout.dragmode = 'select'; + describe("lasso elements", function() { + var mockCopy = Lib.extendDeep({}, mock); + mockCopy.layout.dragmode = "lasso"; - var gd; - beforeEach(function(done) { - gd = createGraphDiv(); + var gd; + beforeEach(function(done) { + gd = createGraphDiv(); - Plotly.plot(gd, mockCopy.data, mockCopy.layout) - .then(done); - }); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - it('should trigger selecting/selected/deselect events', function(done) { - var selectingCnt = 0, - selectingData; - gd.on('plotly_selecting', function(data) { - selectingCnt++; - selectingData = data; - }); - - var selectedCnt = 0, - selectedData; - gd.on('plotly_selected', function(data) { - selectedCnt++; - selectedData = data; - }); - - var doubleClickData; - gd.on('plotly_deselect', function(data) { - doubleClickData = data; - }); - - drag(selectPath); - - expect(selectingCnt).toEqual(1, 'with the correct selecting count'); - expect(selectingData.points).toEqual([{ - curveNumber: 0, - pointNumber: 0, - x: 0.002, - y: 16.25, - id: undefined - }, { - curveNumber: 0, - pointNumber: 1, - x: 0.004, - y: 12.5, - id: undefined - }], 'with the correct selecting points'); - assertRange(selectingData.range, { - x: [0.002000, 0.0046236], - y: [0.10209191961595454, 24.512223978291406] - }, 'with the correct selecting range'); - - expect(selectedCnt).toEqual(1, 'with the correct selected count'); - expect(selectedData.points).toEqual([{ - curveNumber: 0, - pointNumber: 0, - x: 0.002, - y: 16.25, - id: undefined - }, { - curveNumber: 0, - pointNumber: 1, - x: 0.004, - y: 12.5, - id: undefined - }], 'with the correct selected points'); - assertRange(selectedData.range, { - x: [0.002000, 0.0046236], - y: [0.10209191961595454, 24.512223978291406] - }, 'with the correct selected range'); - - doubleClick(250, 200).then(function() { - expect(doubleClickData).toBe(null, 'with the correct deselect data'); - done(); - }); - }); + it("should be appended to the zoom layer", function(done) { + var x0 = 100, y0 = 200, x1 = 150, y1 = 250, x2 = 50, y2 = 50; - }); + gd.once("plotly_selecting", function() { + expect(d3.selectAll(".zoomlayer > .zoombox-corners").size()).toEqual(1); + expect(d3.selectAll(".zoomlayer > .select-outline").size()).toEqual(2); + }); - describe('lasso events', function() { - var mockCopy = Lib.extendDeep({}, mock); - mockCopy.layout.dragmode = 'lasso'; + gd.once("plotly_selected", function() { + expect(d3.selectAll(".zoomlayer > .zoombox-corners").size()).toEqual(0); + expect(d3.selectAll(".zoomlayer > .select-outline").size()).toEqual(2); + }); - var gd; - beforeEach(function(done) { - gd = createGraphDiv(); + gd.once("plotly_deselect", function() { + expect(d3.selectAll(".zoomlayer > .select-outline").size()).toEqual(0); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout) - .then(done); - }); + mouseEvent("mousemove", x0, y0); + expect(d3.selectAll(".zoomlayer > .zoombox-corners").size()).toEqual(0); - it('should trigger selecting/selected/deselect events', function(done) { - var selectingCnt = 0, - selectingData; - gd.on('plotly_selecting', function(data) { - selectingCnt++; - selectingData = data; - }); - - var selectedCnt = 0, - selectedData; - gd.on('plotly_selected', function(data) { - selectedCnt++; - selectedData = data; - }); - - var doubleClickData; - gd.on('plotly_deselect', function(data) { - doubleClickData = data; - }); - - drag(lassoPath); - - expect(selectingCnt).toEqual(3, 'with the correct selecting count'); - expect(selectingData.points).toEqual([{ - curveNumber: 0, - pointNumber: 10, - x: 0.099, - y: 2.75, - id: undefined - }], 'with the correct selecting points'); - - expect(selectedCnt).toEqual(1, 'with the correct selected count'); - expect(selectedData.points).toEqual([{ - curveNumber: 0, - pointNumber: 10, - x: 0.099, - y: 2.75, - id: undefined - }], 'with the correct selected points'); - - doubleClick(250, 200).then(function() { - expect(doubleClickData).toBe(null, 'with the correct deselect data'); - done(); - }); - }); + drag([[x0, y0], [x1, y1]]); + + doubleClick(x2, y2).then(done); }); + }); + + describe("select events", function() { + var mockCopy = Lib.extendDeep({}, mock); + mockCopy.layout.dragmode = "select"; - it('should skip over non-visible traces', function(done) { - var mockCopy = Lib.extendDeep({}, mock); - mockCopy.layout.dragmode = 'select'; + var gd; + beforeEach(function(done) { + gd = createGraphDiv(); - var gd = createGraphDiv(); - var selectedPtLength; + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - gd.on('plotly_selected', function(data) { - selectedPtLength = data.points.length; - }); + it("should trigger selecting/selected/deselect events", function(done) { + var selectingCnt = 0, selectingData; + gd.on("plotly_selecting", function(data) { + selectingCnt++; + selectingData = data; + }); + + var selectedCnt = 0, selectedData; + gd.on("plotly_selected", function(data) { + selectedCnt++; + selectedData = data; + }); + + var doubleClickData; + gd.on("plotly_deselect", function(data) { + doubleClickData = data; + }); + + drag(selectPath); + + expect(selectingCnt).toEqual(1, "with the correct selecting count"); + expect(selectingData.points).toEqual( + [ + { curveNumber: 0, pointNumber: 0, x: 0.002, y: 16.25, id: undefined }, + { curveNumber: 0, pointNumber: 1, x: 0.004, y: 12.5, id: undefined } + ], + "with the correct selecting points" + ); + assertRange( + selectingData.range, + { + x: [0.002000, 0.0046236], + y: [0.10209191961595454, 24.512223978291406] + }, + "with the correct selecting range" + ); + + expect(selectedCnt).toEqual(1, "with the correct selected count"); + expect(selectedData.points).toEqual( + [ + { curveNumber: 0, pointNumber: 0, x: 0.002, y: 16.25, id: undefined }, + { curveNumber: 0, pointNumber: 1, x: 0.004, y: 12.5, id: undefined } + ], + "with the correct selected points" + ); + assertRange( + selectedData.range, + { + x: [0.002000, 0.0046236], + y: [0.10209191961595454, 24.512223978291406] + }, + "with the correct selected range" + ); + + doubleClick(250, 200).then(function() { + expect(doubleClickData).toBe(null, "with the correct deselect data"); + done(); + }); + }); + }); - drag(selectPath); - expect(selectedPtLength).toEqual(2, '(case 0)'); + describe("lasso events", function() { + var mockCopy = Lib.extendDeep({}, mock); + mockCopy.layout.dragmode = "lasso"; - return Plotly.restyle(gd, 'visible', 'legendonly'); - }).then(function() { - drag(selectPath); - expect(selectedPtLength).toEqual(0, '(legendonly case)'); + var gd; + beforeEach(function(done) { + gd = createGraphDiv(); - return Plotly.restyle(gd, 'visible', true); - }).then(function() { - drag(selectPath); - expect(selectedPtLength).toEqual(2, '(back to case 0)'); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - return Plotly.relayout(gd, 'dragmode', 'lasso'); - }).then(function() { - drag(lassoPath); - expect(selectedPtLength).toEqual(1, '(case 0 lasso)'); + it("should trigger selecting/selected/deselect events", function(done) { + var selectingCnt = 0, selectingData; + gd.on("plotly_selecting", function(data) { + selectingCnt++; + selectingData = data; + }); + + var selectedCnt = 0, selectedData; + gd.on("plotly_selected", function(data) { + selectedCnt++; + selectedData = data; + }); + + var doubleClickData; + gd.on("plotly_deselect", function(data) { + doubleClickData = data; + }); + + drag(lassoPath); + + expect(selectingCnt).toEqual(3, "with the correct selecting count"); + expect(selectingData.points).toEqual( + [{ curveNumber: 0, pointNumber: 10, x: 0.099, y: 2.75, id: undefined }], + "with the correct selecting points" + ); + + expect(selectedCnt).toEqual(1, "with the correct selected count"); + expect(selectedData.points).toEqual( + [{ curveNumber: 0, pointNumber: 10, x: 0.099, y: 2.75, id: undefined }], + "with the correct selected points" + ); + + doubleClick(250, 200).then(function() { + expect(doubleClickData).toBe(null, "with the correct deselect data"); + done(); + }); + }); + }); - return Plotly.restyle(gd, 'visible', 'legendonly'); - }).then(function() { - drag(lassoPath); - expect(selectedPtLength).toEqual(0, '(lasso legendonly case)'); + it("should skip over non-visible traces", function(done) { + var mockCopy = Lib.extendDeep({}, mock); + mockCopy.layout.dragmode = "select"; - return Plotly.restyle(gd, 'visible', true); - }).then(function() { - drag(lassoPath); - expect(selectedPtLength).toEqual(1, '(back to lasso case 0)'); + var gd = createGraphDiv(); + var selectedPtLength; - done(); + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + gd.on("plotly_selected", function(data) { + selectedPtLength = data.points.length; }); - }); + + drag(selectPath); + expect(selectedPtLength).toEqual(2, "(case 0)"); + + return Plotly.restyle(gd, "visible", "legendonly"); + }) + .then(function() { + drag(selectPath); + expect(selectedPtLength).toEqual(0, "(legendonly case)"); + + return Plotly.restyle(gd, "visible", true); + }) + .then(function() { + drag(selectPath); + expect(selectedPtLength).toEqual(2, "(back to case 0)"); + + return Plotly.relayout(gd, "dragmode", "lasso"); + }) + .then(function() { + drag(lassoPath); + expect(selectedPtLength).toEqual(1, "(case 0 lasso)"); + + return Plotly.restyle(gd, "visible", "legendonly"); + }) + .then(function() { + drag(lassoPath); + expect(selectedPtLength).toEqual(0, "(lasso legendonly case)"); + + return Plotly.restyle(gd, "visible", true); + }) + .then(function() { + drag(lassoPath); + expect(selectedPtLength).toEqual(1, "(back to lasso case 0)"); + + done(); + }); + }); }); diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 43714ebd8df..9df3a7829b9 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -1,893 +1,884 @@ -var Shapes = require('@src/components/shapes'); -var helpers = require('@src/components/shapes/helpers'); -var constants = require('@src/components/shapes/constants'); +var Shapes = require("@src/components/shapes"); +var helpers = require("@src/components/shapes/helpers"); +var constants = require("@src/components/shapes/constants"); -var Plotly = require('@lib/index'); -var PlotlyInternal = require('@src/plotly'); -var Lib = require('@src/lib'); +var Plotly = require("@lib/index"); +var PlotlyInternal = require("@src/plotly"); +var Lib = require("@src/lib"); var Plots = PlotlyInternal.Plots; var Axes = PlotlyInternal.Axes; -var d3 = require('d3'); -var customMatchers = require('../assets/custom_matchers'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var d3 = require("d3"); +var customMatchers = require("../assets/custom_matchers"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +describe("Test shapes defaults:", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); -describe('Test shapes defaults:', function() { - 'use strict'; + function _supply(layoutIn, layoutOut) { + layoutOut = layoutOut || {}; + layoutOut._has = Plots._hasPlotType.bind(layoutOut); - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - function _supply(layoutIn, layoutOut) { - layoutOut = layoutOut || {}; - layoutOut._has = Plots._hasPlotType.bind(layoutOut); + Shapes.supplyLayoutDefaults(layoutIn, layoutOut); - Shapes.supplyLayoutDefaults(layoutIn, layoutOut); + return layoutOut.shapes; + } - return layoutOut.shapes; - } + it("should skip non-array containers", function() { + [null, undefined, {}, "str", 0, false, true].forEach(function(cont) { + var msg = "- " + JSON.stringify(cont); + var layoutIn = { shapes: cont }; + var out = _supply(layoutIn); - it('should skip non-array containers', function() { - [null, undefined, {}, 'str', 0, false, true].forEach(function(cont) { - var msg = '- ' + JSON.stringify(cont); - var layoutIn = { shapes: cont }; - var out = _supply(layoutIn); - - expect(layoutIn.shapes).toBe(cont, msg); - expect(out).toEqual([], msg); - }); + expect(layoutIn.shapes).toBe(cont, msg); + expect(out).toEqual([], msg); }); + }); - it('should make non-object item visible: false', function() { - var shapes = [null, undefined, [], 'str', 0, false, true]; - var layoutIn = { shapes: shapes }; - var out = _supply(layoutIn); + it("should make non-object item visible: false", function() { + var shapes = [null, undefined, [], "str", 0, false, true]; + var layoutIn = { shapes: shapes }; + var out = _supply(layoutIn); - expect(layoutIn.shapes).toEqual(shapes); + expect(layoutIn.shapes).toEqual(shapes); - out.forEach(function(item, i) { - expect(item).toEqual({ - visible: false, - _input: {}, - _index: i - }); - }); + out.forEach(function(item, i) { + expect(item).toEqual({ visible: false, _input: {}, _index: i }); }); + }); - it('should provide the right defaults on all axis types', function() { - var fullLayout = { - xaxis: {type: 'linear', range: [0, 20]}, - yaxis: {type: 'log', range: [1, 5]}, - xaxis2: {type: 'date', range: ['2006-06-05', '2006-06-09']}, - yaxis2: {type: 'category', range: [-0.5, 7.5]} - }; + it("should provide the right defaults on all axis types", function() { + var fullLayout = { + xaxis: { type: "linear", range: [0, 20] }, + yaxis: { type: "log", range: [1, 5] }, + xaxis2: { type: "date", range: ["2006-06-05", "2006-06-09"] }, + yaxis2: { type: "category", range: [-0.5, 7.5] } + }; - Axes.setConvert(fullLayout.xaxis); - Axes.setConvert(fullLayout.yaxis); - Axes.setConvert(fullLayout.xaxis2); - Axes.setConvert(fullLayout.yaxis2); + Axes.setConvert(fullLayout.xaxis); + Axes.setConvert(fullLayout.yaxis); + Axes.setConvert(fullLayout.xaxis2); + Axes.setConvert(fullLayout.yaxis2); - var shape1In = {type: 'rect'}, - shape2In = {type: 'circle', xref: 'x2', yref: 'y2'}; + var shape1In = { type: "rect" }, + shape2In = { type: "circle", xref: "x2", yref: "y2" }; - var layoutIn = { - shapes: [shape1In, shape2In] - }; + var layoutIn = { shapes: [shape1In, shape2In] }; - _supply(layoutIn, fullLayout); + _supply(layoutIn, fullLayout); - var shape1Out = fullLayout.shapes[0], - shape2Out = fullLayout.shapes[1]; + var shape1Out = fullLayout.shapes[0], shape2Out = fullLayout.shapes[1]; - // default positions are 1/4 and 3/4 of the full range of that axis - expect(shape1Out.x0).toBe(5); - expect(shape1Out.x1).toBe(15); + // default positions are 1/4 and 3/4 of the full range of that axis + expect(shape1Out.x0).toBe(5); + expect(shape1Out.x1).toBe(15); - // shapes use data values for log axes (like everyone will in V2.0) - expect(shape1Out.y0).toBeWithin(100, 0.001); - expect(shape1Out.y1).toBeWithin(10000, 0.001); + // shapes use data values for log axes (like everyone will in V2.0) + expect(shape1Out.y0).toBeWithin(100, 0.001); + expect(shape1Out.y1).toBeWithin(10000, 0.001); - // date strings also interpolate - expect(shape2Out.x0).toBe('2006-06-06'); - expect(shape2Out.x1).toBe('2006-06-08'); + // date strings also interpolate + expect(shape2Out.x0).toBe("2006-06-06"); + expect(shape2Out.x1).toBe("2006-06-08"); - // categories must use serial numbers to get continuous values - expect(shape2Out.y0).toBeWithin(1.5, 0.001); - expect(shape2Out.y1).toBeWithin(5.5, 0.001); - }); + // categories must use serial numbers to get continuous values + expect(shape2Out.y0).toBeWithin(1.5, 0.001); + expect(shape2Out.y1).toBeWithin(5.5, 0.001); + }); }); -describe('Test shapes:', function() { - 'use strict'; +describe("Test shapes:", function() { + "use strict"; + var mock = require("@mocks/shapes.json"); + var gd; - var mock = require('@mocks/shapes.json'); - var gd; + beforeEach(function(done) { + gd = createGraphDiv(); - beforeEach(function(done) { - gd = createGraphDiv(); + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); - var mockData = Lib.extendDeep([], mock.data), - mockLayout = Lib.extendDeep({}, mock.layout); + Plotly.plot(gd, mockData, mockLayout).then(done); + }); - Plotly.plot(gd, mockData, mockLayout).then(done); - }); + afterEach(destroyGraphDiv); - afterEach(destroyGraphDiv); + function countShapesInLowerLayer() { + return gd._fullLayout.shapes.filter(isShapeInLowerLayer).length; + } - function countShapesInLowerLayer() { - return gd._fullLayout.shapes.filter(isShapeInLowerLayer).length; - } + function countShapesInUpperLayer() { + return gd._fullLayout.shapes.filter(isShapeInUpperLayer).length; + } - function countShapesInUpperLayer() { - return gd._fullLayout.shapes.filter(isShapeInUpperLayer).length; - } + function countShapesInSubplots() { + return gd._fullLayout.shapes.filter(isShapeInSubplot).length; + } - function countShapesInSubplots() { - return gd._fullLayout.shapes.filter(isShapeInSubplot).length; - } + function isShapeInUpperLayer(shape) { + return shape.layer !== "below"; + } - function isShapeInUpperLayer(shape) { - return shape.layer !== 'below'; - } + function isShapeInLowerLayer(shape) { + return shape.xref === "paper" && + shape.yref === "paper" && + !isShapeInUpperLayer(shape); + } - function isShapeInLowerLayer(shape) { - return (shape.xref === 'paper' && shape.yref === 'paper') && - !isShapeInUpperLayer(shape); - } + function isShapeInSubplot(shape) { + return !isShapeInUpperLayer(shape) && !isShapeInLowerLayer(shape); + } - function isShapeInSubplot(shape) { - return !isShapeInUpperLayer(shape) && !isShapeInLowerLayer(shape); - } + function countShapeLowerLayerNodes() { + return d3.selectAll(".layer-below > .shapelayer").size(); + } - function countShapeLowerLayerNodes() { - return d3.selectAll('.layer-below > .shapelayer').size(); - } + function countShapeUpperLayerNodes() { + return d3.selectAll(".layer-above > .shapelayer").size(); + } - function countShapeUpperLayerNodes() { - return d3.selectAll('.layer-above > .shapelayer').size(); - } + function countShapeLayerNodesInSubplots() { + return d3.selectAll(".layer-subplot").size(); + } - function countShapeLayerNodesInSubplots() { - return d3.selectAll('.layer-subplot').size(); - } + function countSubplots(gd) { + return Object.keys(gd._fullLayout._plots || {}).length; + } - function countSubplots(gd) { - return Object.keys(gd._fullLayout._plots || {}).length; - } + function countShapePathsInLowerLayer() { + return d3.selectAll(".layer-below > .shapelayer > path").size(); + } - function countShapePathsInLowerLayer() { - return d3.selectAll('.layer-below > .shapelayer > path').size(); - } + function countShapePathsInUpperLayer() { + return d3.selectAll(".layer-above > .shapelayer > path").size(); + } - function countShapePathsInUpperLayer() { - return d3.selectAll('.layer-above > .shapelayer > path').size(); - } + function countShapePathsInSubplots() { + return d3.selectAll(".layer-subplot > .shapelayer > path").size(); + } - function countShapePathsInSubplots() { - return d3.selectAll('.layer-subplot > .shapelayer > path').size(); - } - - describe('*shapeLowerLayer*', function() { - it('has one node', function() { - expect(countShapeLowerLayerNodes()).toEqual(1); - }); - - it('has as many *path* nodes as shapes in the lower layer', function() { - expect(countShapePathsInLowerLayer()) - .toEqual(countShapesInLowerLayer()); - }); - - it('should be able to get relayout', function(done) { - Plotly.relayout(gd, {height: 200, width: 400}).then(function() { - expect(countShapeLowerLayerNodes()).toEqual(1); - expect(countShapePathsInLowerLayer()) - .toEqual(countShapesInLowerLayer()); - }).then(done); - }); + describe("*shapeLowerLayer*", function() { + it("has one node", function() { + expect(countShapeLowerLayerNodes()).toEqual(1); }); - describe('*shapeUpperLayer*', function() { - it('has one node', function() { - expect(countShapeUpperLayerNodes()).toEqual(1); - }); - - it('has as many *path* nodes as shapes in the upper layer', function() { - expect(countShapePathsInUpperLayer()) - .toEqual(countShapesInUpperLayer()); - }); - - it('should be able to get relayout', function(done) { - Plotly.relayout(gd, {height: 200, width: 400}).then(function() { - expect(countShapeUpperLayerNodes()).toEqual(1); - expect(countShapePathsInUpperLayer()) - .toEqual(countShapesInUpperLayer()); - }).then(done); - }); + it("has as many *path* nodes as shapes in the lower layer", function() { + expect(countShapePathsInLowerLayer()).toEqual(countShapesInLowerLayer()); }); - describe('each *subplot*', function() { - it('has one *shapelayer*', function() { - expect(countShapeLayerNodesInSubplots()) - .toEqual(countSubplots(gd)); - }); - - it('has as many *path* nodes as shapes in the subplot', function() { - expect(countShapePathsInSubplots()) - .toEqual(countShapesInSubplots()); - }); - - it('should be able to get relayout', function(done) { - Plotly.relayout(gd, {height: 200, width: 400}).then(function() { - expect(countShapeLayerNodesInSubplots()) - .toEqual(countSubplots(gd)); - expect(countShapePathsInSubplots()) - .toEqual(countShapesInSubplots()); - }).then(done); - }); + it("should be able to get relayout", function(done) { + Plotly.relayout(gd, { height: 200, width: 400 }) + .then(function() { + expect(countShapeLowerLayerNodes()).toEqual(1); + expect(countShapePathsInLowerLayer()).toEqual( + countShapesInLowerLayer() + ); + }) + .then(done); }); + }); - function countShapes(gd) { - return gd.layout.shapes ? - gd.layout.shapes.length : - 0; - } - - function getLastShape(gd) { - return gd.layout.shapes ? - gd.layout.shapes[gd.layout.shapes.length - 1] : - null; - } - - function getRandomShape() { - return { - x0: Math.random(), - y0: Math.random(), - x1: Math.random(), - y1: Math.random() - }; - } - - describe('Plotly.relayout', function() { - it('should be able to add a shape', function(done) { - var pathCount = countShapePathsInUpperLayer(); - var index = countShapes(gd); - var shape = getRandomShape(); - - Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 1); - }).then(done); - }); - - it('should be able to remove a shape', function(done) { - var pathCount = countShapePathsInUpperLayer(); - var index = countShapes(gd); - var shape = getRandomShape(); - - Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 1); - - return Plotly.relayout(gd, 'shapes[' + index + ']', 'remove'); - }) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount); - expect(countShapes(gd)).toEqual(index); - - return Plotly.relayout(gd, 'shapes[2].visible', false); - }) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount - 1); - expect(countShapes(gd)).toEqual(index); - - return Plotly.relayout(gd, 'shapes[1]', null); - }) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount - 2); - expect(countShapes(gd)).toEqual(index - 1); - }) - .then(done); - }); + describe("*shapeUpperLayer*", function() { + it("has one node", function() { + expect(countShapeUpperLayerNodes()).toEqual(1); + }); - it('should be able to remove all shapes', function(done) { - Plotly.relayout(gd, { shapes: [] }).then(function() { - expect(countShapePathsInUpperLayer()).toEqual(0); - expect(countShapePathsInLowerLayer()).toEqual(0); - expect(countShapePathsInSubplots()).toEqual(0); - }).then(done); - }); + it("has as many *path* nodes as shapes in the upper layer", function() { + expect(countShapePathsInUpperLayer()).toEqual(countShapesInUpperLayer()); + }); - it('should be able to update a shape layer', function(done) { - var index = countShapes(gd), - astr = 'shapes[' + index + ']', - shape = getRandomShape(), - shapesInLowerLayer = countShapePathsInLowerLayer(), - shapesInUpperLayer = countShapePathsInUpperLayer(); - - shape.xref = 'paper'; - shape.yref = 'paper'; - - Plotly.relayout(gd, astr, shape).then(function() { - expect(countShapePathsInLowerLayer()) - .toEqual(shapesInLowerLayer); - expect(countShapePathsInUpperLayer()) - .toEqual(shapesInUpperLayer + 1); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 1); - }).then(function() { - shape.layer = 'below'; - Plotly.relayout(gd, astr + '.layer', shape.layer); - }).then(function() { - expect(countShapePathsInLowerLayer()) - .toEqual(shapesInLowerLayer + 1); - expect(countShapePathsInUpperLayer()) - .toEqual(shapesInUpperLayer); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 1); - }).then(function() { - shape.layer = 'above'; - Plotly.relayout(gd, astr + '.layer', shape.layer); - }).then(function() { - expect(countShapePathsInLowerLayer()) - .toEqual(shapesInLowerLayer); - expect(countShapePathsInUpperLayer()) - .toEqual(shapesInUpperLayer + 1); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 1); - }).then(done); - }); + it("should be able to get relayout", function(done) { + Plotly.relayout(gd, { height: 200, width: 400 }) + .then(function() { + expect(countShapeUpperLayerNodes()).toEqual(1); + expect(countShapePathsInUpperLayer()).toEqual( + countShapesInUpperLayer() + ); + }) + .then(done); }); -}); + }); -describe('shapes autosize', function() { - 'use strict'; + describe("each *subplot*", function() { + it("has one *shapelayer*", function() { + expect(countShapeLayerNodesInSubplots()).toEqual(countSubplots(gd)); + }); - var gd; + it("has as many *path* nodes as shapes in the subplot", function() { + expect(countShapePathsInSubplots()).toEqual(countShapesInSubplots()); + }); - beforeAll(function() { - jasmine.addMatchers(customMatchers); + it("should be able to get relayout", function(done) { + Plotly.relayout(gd, { height: 200, width: 400 }) + .then(function() { + expect(countShapeLayerNodesInSubplots()).toEqual(countSubplots(gd)); + expect(countShapePathsInSubplots()).toEqual(countShapesInSubplots()); + }) + .then(done); + }); + }); + + function countShapes(gd) { + return gd.layout.shapes ? gd.layout.shapes.length : 0; + } + + function getLastShape(gd) { + return gd.layout.shapes + ? gd.layout.shapes[gd.layout.shapes.length - 1] + : null; + } + + function getRandomShape() { + return { + x0: Math.random(), + y0: Math.random(), + x1: Math.random(), + y1: Math.random() + }; + } + + describe("Plotly.relayout", function() { + it("should be able to add a shape", function(done) { + var pathCount = countShapePathsInUpperLayer(); + var index = countShapes(gd); + var shape = getRandomShape(); + + Plotly.relayout(gd, "shapes[" + index + "]", shape) + .then(function() { + expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 1); + }) + .then(done); }); - afterEach(destroyGraphDiv); - - it('should adapt to relayout calls', function(done) { - gd = createGraphDiv(); - - var mock = { - data: [{}], - layout: { - shapes: [{ - type: 'line', - x0: 0, - y0: 0, - x1: 1, - y1: 1 - }, { - type: 'line', - x0: 0, - y0: 0, - x1: 2, - y1: 2 - }] - } - }; - - function assertRanges(x, y) { - var fullLayout = gd._fullLayout; - var PREC = 1; - - expect(fullLayout.xaxis.range).toBeCloseToArray(x, PREC, '- xaxis'); - expect(fullLayout.yaxis.range).toBeCloseToArray(y, PREC, '- yaxis'); - } + it("should be able to remove a shape", function(done) { + var pathCount = countShapePathsInUpperLayer(); + var index = countShapes(gd); + var shape = getRandomShape(); - Plotly.plot(gd, mock).then(function() { - assertRanges([0, 2], [0, 2]); + Plotly.relayout(gd, "shapes[" + index + "]", shape) + .then(function() { + expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 1); - return Plotly.relayout(gd, { 'shapes[1].visible': false }); + return Plotly.relayout(gd, "shapes[" + index + "]", "remove"); }) .then(function() { - assertRanges([0, 1], [0, 1]); + expect(countShapePathsInUpperLayer()).toEqual(pathCount); + expect(countShapes(gd)).toEqual(index); - return Plotly.relayout(gd, { 'shapes[1].visible': true }); + return Plotly.relayout(gd, "shapes[2].visible", false); }) .then(function() { - assertRanges([0, 2], [0, 2]); + expect(countShapePathsInUpperLayer()).toEqual(pathCount - 1); + expect(countShapes(gd)).toEqual(index); - return Plotly.relayout(gd, { 'shapes[0].x1': 3 }); + return Plotly.relayout(gd, "shapes[1]", null); }) .then(function() { - assertRanges([0, 3], [0, 2]); + expect(countShapePathsInUpperLayer()).toEqual(pathCount - 2); + expect(countShapes(gd)).toEqual(index - 1); }) .then(done); }); -}); -describe('Test shapes: a plot with shapes and an overlaid axis', function() { - 'use strict'; - - var gd, data, layout; - - beforeEach(function() { - gd = createGraphDiv(); - - data = [{ - 'y': [1934.5, 1932.3, 1930.3], - 'x': ['1947-01-01', '1947-04-01', '1948-07-01'], - 'type': 'scatter' - }]; - - layout = { - 'yaxis': { - 'type': 'linear' - }, - 'xaxis': { - 'type': 'date' - }, - 'yaxis2': { - 'side': 'right', - 'overlaying': 'y' - }, - 'shapes': [{ - 'fillcolor': '#ccc', - 'type': 'rect', - 'x0': '1947-01-01', - 'x1': '1947-04-01', - 'xref': 'x', - 'y0': 0, - 'y1': 1, - 'yref': 'paper', - 'layer': 'below' - }] - }; + it("should be able to remove all shapes", function(done) { + Plotly.relayout(gd, { shapes: [] }) + .then(function() { + expect(countShapePathsInUpperLayer()).toEqual(0); + expect(countShapePathsInLowerLayer()).toEqual(0); + expect(countShapePathsInSubplots()).toEqual(0); + }) + .then(done); }); - afterEach(destroyGraphDiv); + it("should be able to update a shape layer", function(done) { + var index = countShapes(gd), + astr = "shapes[" + index + "]", + shape = getRandomShape(), + shapesInLowerLayer = countShapePathsInLowerLayer(), + shapesInUpperLayer = countShapePathsInUpperLayer(); - it('should not throw an exception', function(done) { - Plotly.plot(gd, data, layout).then(done); + shape.xref = "paper"; + shape.yref = "paper"; + + Plotly.relayout(gd, astr, shape) + .then(function() { + expect(countShapePathsInLowerLayer()).toEqual(shapesInLowerLayer); + expect(countShapePathsInUpperLayer()).toEqual(shapesInUpperLayer + 1); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 1); + }) + .then(function() { + shape.layer = "below"; + Plotly.relayout(gd, astr + ".layer", shape.layer); + }) + .then(function() { + expect(countShapePathsInLowerLayer()).toEqual(shapesInLowerLayer + 1); + expect(countShapePathsInUpperLayer()).toEqual(shapesInUpperLayer); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 1); + }) + .then(function() { + shape.layer = "above"; + Plotly.relayout(gd, astr + ".layer", shape.layer); + }) + .then(function() { + expect(countShapePathsInLowerLayer()).toEqual(shapesInLowerLayer); + expect(countShapePathsInUpperLayer()).toEqual(shapesInUpperLayer + 1); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 1); + }) + .then(done); }); + }); }); -describe('Test shapes', function() { - 'use strict'; +describe("shapes autosize", function() { + "use strict"; + var gd; - var gd, data, layout, config; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - beforeEach(function() { - gd = createGraphDiv(); - data = [{}]; - layout = {}; - config = { - editable: true, - displayModeBar: false - }; - }); + afterEach(destroyGraphDiv); - afterEach(destroyGraphDiv); + it("should adapt to relayout calls", function(done) { + gd = createGraphDiv(); - var testCases = [ - // xref: 'paper', yref: 'paper' - { - title: 'linked to paper' - }, + var mock = { + data: [{}], + layout: { + shapes: [ + { type: "line", x0: 0, y0: 0, x1: 1, y1: 1 }, + { type: "line", x0: 0, y0: 0, x1: 2, y1: 2 } + ] + } + }; - // xaxis.type: 'linear', yaxis.type: 'log' - { - title: 'linked to linear and log axes', - xaxis: { type: 'linear', range: [0, 10] }, - yaxis: { type: 'log', range: [Math.log10(1), Math.log10(1000)] } - }, + function assertRanges(x, y) { + var fullLayout = gd._fullLayout; + var PREC = 1; - // xaxis.type: 'date', yaxis.type: 'category' - { - title: 'linked to date and category axes', - xaxis: { - type: 'date', - range: ['2000-01-01', '2000-02-02'] - }, - yaxis: { type: 'category', range: ['a', 'b'] } - } - ]; + expect(fullLayout.xaxis.range).toBeCloseToArray(x, PREC, "- xaxis"); + expect(fullLayout.yaxis.range).toBeCloseToArray(y, PREC, "- yaxis"); + } - testCases.forEach(function(testCase) { - it(testCase.title + 'should be draggable', function(done) { - setupLayout(testCase); - testDragEachShape(done); - }); - }); + Plotly.plot(gd, mock) + .then(function() { + assertRanges([0, 2], [0, 2]); + + return Plotly.relayout(gd, { "shapes[1].visible": false }); + }) + .then(function() { + assertRanges([0, 1], [0, 1]); + + return Plotly.relayout(gd, { "shapes[1].visible": true }); + }) + .then(function() { + assertRanges([0, 2], [0, 2]); + + return Plotly.relayout(gd, { "shapes[0].x1": 3 }); + }) + .then(function() { + assertRanges([0, 3], [0, 2]); + }) + .then(done); + }); +}); - testCases.forEach(function(testCase) { - ['n', 's', 'w', 'e', 'nw', 'se', 'ne', 'sw'].forEach(function(direction) { - var testTitle = testCase.title + - 'should be resizeable over direction ' + - direction; - it(testTitle, function(done) { - setupLayout(testCase); - testResizeEachShape(direction, done); - }); - }); - }); +describe("Test shapes: a plot with shapes and an overlaid axis", function() { + "use strict"; + var gd, data, layout; - function setupLayout(testCase) { - Lib.extendDeep(layout, testCase); - - var xrange = testCase.xaxis ? testCase.xaxis.range : [0.25, 0.75], - yrange = testCase.yaxis ? testCase.yaxis.range : [0.25, 0.75], - xref = testCase.xaxis ? 'x' : 'paper', - yref = testCase.yaxis ? 'y' : 'paper', - x0 = xrange[0], - x1 = xrange[1], - y0 = yrange[0], - y1 = yrange[1]; - - if(testCase.xaxis && testCase.xaxis.type === 'log') { - x0 = Math.pow(10, x0); - x1 = Math.pow(10, x1); - } + beforeEach(function() { + gd = createGraphDiv(); - if(testCase.yaxis && testCase.yaxis.type === 'log') { - y0 = Math.pow(10, y0); - y1 = Math.pow(10, y1); - } + data = [ + { + y: [1934.5, 1932.3, 1930.3], + x: ["1947-01-01", "1947-04-01", "1948-07-01"], + type: "scatter" + } + ]; - if(testCase.xaxis && testCase.xaxis.type === 'category') { - x0 = 0; - x1 = 1; + layout = { + yaxis: { type: "linear" }, + xaxis: { type: "date" }, + yaxis2: { side: "right", overlaying: "y" }, + shapes: [ + { + fillcolor: "#ccc", + type: "rect", + x0: "1947-01-01", + x1: "1947-04-01", + xref: "x", + y0: 0, + y1: 1, + yref: "paper", + layer: "below" } + ] + }; + }); - if(testCase.yaxis && testCase.yaxis.type === 'category') { - y0 = 0; - y1 = 1; - } + afterEach(destroyGraphDiv); - var x0y0 = x0 + ',' + y0, - x1y1 = x1 + ',' + y1, - x1y0 = x1 + ',' + y0; - - var layoutShapes = [ - { type: 'line' }, - { type: 'rect' }, - { type: 'circle' }, - {} // path - ]; - - layoutShapes.forEach(function(s) { - s.xref = xref; - s.yref = yref; - - if(s.type) { - s.x0 = x0; - s.x1 = x1; - s.y0 = y0; - s.y1 = y1; - } - else { - s.path = 'M' + x0y0 + 'L' + x1y1 + 'L' + x1y0 + 'Z'; - } - }); + it("should not throw an exception", function(done) { + Plotly.plot(gd, data, layout).then(done); + }); +}); - layout.shapes = layoutShapes; +describe("Test shapes", function() { + "use strict"; + var gd, data, layout, config; + + beforeEach(function() { + gd = createGraphDiv(); + data = [{}]; + layout = {}; + config = { editable: true, displayModeBar: false }; + }); + + afterEach(destroyGraphDiv); + + var testCases = [ + // xref: 'paper', yref: 'paper' + { title: "linked to paper" }, + // xaxis.type: 'linear', yaxis.type: 'log' + { + title: "linked to linear and log axes", + xaxis: { type: "linear", range: [0, 10] }, + yaxis: { type: "log", range: [Math.log10(1), Math.log10(1000)] } + }, + // xaxis.type: 'date', yaxis.type: 'category' + { + title: "linked to date and category axes", + xaxis: { type: "date", range: ["2000-01-01", "2000-02-02"] }, + yaxis: { type: "category", range: ["a", "b"] } } + ]; - function testDragEachShape(done) { - var promise = Plotly.plot(gd, data, layout, config); - - var layoutShapes = gd.layout.shapes; - - expect(layoutShapes.length).toBe(4); // line, rect, circle and path - - layoutShapes.forEach(function(layoutShape, index) { - var dx = 100, - dy = 100; - promise = promise.then(function() { - var node = getShapeNode(index); - expect(node).not.toBe(null); - - return (layoutShape.path) ? - testPathDrag(dx, dy, layoutShape, node) : - testShapeDrag(dx, dy, layoutShape, node); - }); - }); - - return promise.then(done); + testCases.forEach(function(testCase) { + it(testCase.title + "should be draggable", function(done) { + setupLayout(testCase); + testDragEachShape(done); + }); + }); + + testCases.forEach(function(testCase) { + ["n", "s", "w", "e", "nw", "se", "ne", "sw"].forEach(function(direction) { + var testTitle = testCase.title + + "should be resizeable over direction " + + direction; + it(testTitle, function(done) { + setupLayout(testCase); + testResizeEachShape(direction, done); + }); + }); + }); + + function setupLayout(testCase) { + Lib.extendDeep(layout, testCase); + + var xrange = testCase.xaxis ? testCase.xaxis.range : [0.25, 0.75], + yrange = testCase.yaxis ? testCase.yaxis.range : [0.25, 0.75], + xref = testCase.xaxis ? "x" : "paper", + yref = testCase.yaxis ? "y" : "paper", + x0 = xrange[0], + x1 = xrange[1], + y0 = yrange[0], + y1 = yrange[1]; + + if (testCase.xaxis && testCase.xaxis.type === "log") { + x0 = Math.pow(10, x0); + x1 = Math.pow(10, x1); } - function testResizeEachShape(direction, done) { - var promise = Plotly.plot(gd, data, layout, config); - - var layoutShapes = gd.layout.shapes; - - expect(layoutShapes.length).toBe(4); // line, rect, circle and path - - var dxToShrinkWidth = { - n: 0, s: 0, w: 10, e: -10, nw: 10, se: -10, ne: -10, sw: 10 - }, - dyToShrinkHeight = { - n: 10, s: -10, w: 0, e: 0, nw: 10, se: -10, ne: 10, sw: -10 - }; - layoutShapes.forEach(function(layoutShape, index) { - if(layoutShape.path) return; - - var dx = dxToShrinkWidth[direction], - dy = dyToShrinkHeight[direction]; - - promise = promise.then(function() { - var node = getShapeNode(index); - expect(node).not.toBe(null); - - return testShapeResize(direction, dx, dy, layoutShape, node); - }); - - promise = promise.then(function() { - var node = getShapeNode(index); - expect(node).not.toBe(null); - - return testShapeResize(direction, -dx, -dy, layoutShape, node); - }); - }); - - return promise.then(done); + if (testCase.yaxis && testCase.yaxis.type === "log") { + y0 = Math.pow(10, y0); + y1 = Math.pow(10, y1); } - function getShapeNode(index) { - return d3.selectAll('.shapelayer path').filter(function() { - return +this.getAttribute('data-index') === index; - }).node(); + if (testCase.xaxis && testCase.xaxis.type === "category") { + x0 = 0; + x1 = 1; } - function testShapeDrag(dx, dy, layoutShape, node) { - var xa = Axes.getFromId(gd, layoutShape.xref), - ya = Axes.getFromId(gd, layoutShape.yref), - x2p = helpers.getDataToPixel(gd, xa), - y2p = helpers.getDataToPixel(gd, ya, true); - - var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); - - return drag(node, dx, dy).then(function() { - var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); - - expect(finalCoordinates.x0 - initialCoordinates.x0).toBeCloseTo(dx); - expect(finalCoordinates.x1 - initialCoordinates.x1).toBeCloseTo(dx); - expect(finalCoordinates.y0 - initialCoordinates.y0).toBeCloseTo(dy); - expect(finalCoordinates.y1 - initialCoordinates.y1).toBeCloseTo(dy); - }); + if (testCase.yaxis && testCase.yaxis.type === "category") { + y0 = 0; + y1 = 1; } - function getShapeCoordinates(layoutShape, x2p, y2p) { - return { - x0: x2p(layoutShape.x0), - x1: x2p(layoutShape.x1), - y0: y2p(layoutShape.y0), - y1: y2p(layoutShape.y1) - }; - } + var x0y0 = x0 + "," + y0, x1y1 = x1 + "," + y1, x1y0 = x1 + "," + y0; - function testPathDrag(dx, dy, layoutShape, node) { - var xa = Axes.getFromId(gd, layoutShape.xref), - ya = Axes.getFromId(gd, layoutShape.yref), - x2p = helpers.getDataToPixel(gd, xa), - y2p = helpers.getDataToPixel(gd, ya, true); + var layoutShapes = [ + { type: "line" }, + { type: "rect" }, + { type: "circle" }, + // path + {} + ]; - var initialPath = layoutShape.path, - initialCoordinates = getPathCoordinates(initialPath, x2p, y2p); + layoutShapes.forEach(function(s) { + s.xref = xref; + s.yref = yref; + + if (s.type) { + s.x0 = x0; + s.x1 = x1; + s.y0 = y0; + s.y1 = y1; + } else { + s.path = "M" + x0y0 + "L" + x1y1 + "L" + x1y0 + "Z"; + } + }); - expect(initialCoordinates.length).toBe(6); + layout.shapes = layoutShapes; + } - return drag(node, dx, dy).then(function() { - var finalPath = layoutShape.path, - finalCoordinates = getPathCoordinates(finalPath, x2p, y2p); + function testDragEachShape(done) { + var promise = Plotly.plot(gd, data, layout, config); - expect(finalCoordinates.length).toBe(initialCoordinates.length); + var layoutShapes = gd.layout.shapes; - for(var i = 0; i < initialCoordinates.length; i++) { - var initialCoordinate = initialCoordinates[i], - finalCoordinate = finalCoordinates[i]; + expect(layoutShapes.length).toBe(4); - if(initialCoordinate.x) { - expect(finalCoordinate.x - initialCoordinate.x) - .toBeCloseTo(dx); - } - else { - expect(finalCoordinate.y - initialCoordinate.y) - .toBeCloseTo(dy); - } - } - }); - } + // line, rect, circle and path + layoutShapes.forEach(function(layoutShape, index) { + var dx = 100, dy = 100; + promise = promise.then(function() { + var node = getShapeNode(index); + expect(node).not.toBe(null); - function testShapeResize(direction, dx, dy, layoutShape, node) { - var xa = Axes.getFromId(gd, layoutShape.xref), - ya = Axes.getFromId(gd, layoutShape.yref), - x2p = helpers.getDataToPixel(gd, xa), - y2p = helpers.getDataToPixel(gd, ya, true); - - var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); - - return resize(direction, node, dx, dy).then(function() { - var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); - - var keyN, keyS, keyW, keyE; - if(initialCoordinates.y0 < initialCoordinates.y1) { - keyN = 'y0'; keyS = 'y1'; - } - else { - keyN = 'y1'; keyS = 'y0'; - } - if(initialCoordinates.x0 < initialCoordinates.x1) { - keyW = 'x0'; keyE = 'x1'; - } - else { - keyW = 'x1'; keyE = 'x0'; - } - - if(~direction.indexOf('n')) { - expect(finalCoordinates[keyN] - initialCoordinates[keyN]) - .toBeCloseTo(dy); - } - else if(~direction.indexOf('s')) { - expect(finalCoordinates[keyS] - initialCoordinates[keyS]) - .toBeCloseTo(dy); - } - - if(~direction.indexOf('w')) { - expect(finalCoordinates[keyW] - initialCoordinates[keyW]) - .toBeCloseTo(dx); - } - else if(~direction.indexOf('e')) { - expect(finalCoordinates[keyE] - initialCoordinates[keyE]) - .toBeCloseTo(dx); - } - }); - } + return layoutShape.path + ? testPathDrag(dx, dy, layoutShape, node) + : testShapeDrag(dx, dy, layoutShape, node); + }); + }); + + return promise.then(done); + } + + function testResizeEachShape(direction, done) { + var promise = Plotly.plot(gd, data, layout, config); + + var layoutShapes = gd.layout.shapes; + + expect(layoutShapes.length).toBe(4); + + // line, rect, circle and path + var dxToShrinkWidth = { + n: 0, + s: 0, + w: 10, + e: -10, + nw: 10, + se: -10, + ne: -10, + sw: 10 + }, + dyToShrinkHeight = { + n: 10, + s: -10, + w: 0, + e: 0, + nw: 10, + se: -10, + ne: 10, + sw: -10 + }; + layoutShapes.forEach(function(layoutShape, index) { + if (layoutShape.path) return; + + var dx = dxToShrinkWidth[direction], dy = dyToShrinkHeight[direction]; + + promise = promise.then(function() { + var node = getShapeNode(index); + expect(node).not.toBe(null); + + return testShapeResize(direction, dx, dy, layoutShape, node); + }); + + promise = promise.then(function() { + var node = getShapeNode(index); + expect(node).not.toBe(null); + + return testShapeResize(direction, -dx, -dy, layoutShape, node); + }); + }); - function getPathCoordinates(pathString, x2p, y2p) { - var coordinates = []; - - pathString.match(constants.segmentRE).forEach(function(segment) { - var paramNumber = 0, - segmentType = segment.charAt(0), - xParams = constants.paramIsX[segmentType], - yParams = constants.paramIsY[segmentType], - nParams = constants.numParams[segmentType], - params = segment.substr(1).match(constants.paramRE); - - if(params) { - params.forEach(function(param) { - if(paramNumber >= nParams) return; - - if(xParams[paramNumber]) { - coordinates.push({ x: x2p(param) }); - } - else if(yParams[paramNumber]) { - coordinates.push({ y: y2p(param) }); - } - - paramNumber++; - }); - } + return promise.then(done); + } + + function getShapeNode(index) { + return d3 + .selectAll(".shapelayer path") + .filter(function() { + return +this.getAttribute("data-index") === index; + }) + .node(); + } + + function testShapeDrag(dx, dy, layoutShape, node) { + var xa = Axes.getFromId(gd, layoutShape.xref), + ya = Axes.getFromId(gd, layoutShape.yref), + x2p = helpers.getDataToPixel(gd, xa), + y2p = helpers.getDataToPixel(gd, ya, true); + + var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); + + return drag(node, dx, dy).then(function() { + var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); + + expect(finalCoordinates.x0 - initialCoordinates.x0).toBeCloseTo(dx); + expect(finalCoordinates.x1 - initialCoordinates.x1).toBeCloseTo(dx); + expect(finalCoordinates.y0 - initialCoordinates.y0).toBeCloseTo(dy); + expect(finalCoordinates.y1 - initialCoordinates.y1).toBeCloseTo(dy); + }); + } + + function getShapeCoordinates(layoutShape, x2p, y2p) { + return { + x0: x2p(layoutShape.x0), + x1: x2p(layoutShape.x1), + y0: y2p(layoutShape.y0), + y1: y2p(layoutShape.y1) + }; + } + + function testPathDrag(dx, dy, layoutShape, node) { + var xa = Axes.getFromId(gd, layoutShape.xref), + ya = Axes.getFromId(gd, layoutShape.yref), + x2p = helpers.getDataToPixel(gd, xa), + y2p = helpers.getDataToPixel(gd, ya, true); + + var initialPath = layoutShape.path, + initialCoordinates = getPathCoordinates(initialPath, x2p, y2p); + + expect(initialCoordinates.length).toBe(6); + + return drag(node, dx, dy).then(function() { + var finalPath = layoutShape.path, + finalCoordinates = getPathCoordinates(finalPath, x2p, y2p); + + expect(finalCoordinates.length).toBe(initialCoordinates.length); + + for (var i = 0; i < initialCoordinates.length; i++) { + var initialCoordinate = initialCoordinates[i], + finalCoordinate = finalCoordinates[i]; + + if (initialCoordinate.x) { + expect(finalCoordinate.x - initialCoordinate.x).toBeCloseTo(dx); + } else { + expect(finalCoordinate.y - initialCoordinate.y).toBeCloseTo(dy); + } + } + }); + } + + function testShapeResize(direction, dx, dy, layoutShape, node) { + var xa = Axes.getFromId(gd, layoutShape.xref), + ya = Axes.getFromId(gd, layoutShape.yref), + x2p = helpers.getDataToPixel(gd, xa), + y2p = helpers.getDataToPixel(gd, ya, true); + + var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); + + return resize(direction, node, dx, dy).then(function() { + var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); + + var keyN, keyS, keyW, keyE; + if (initialCoordinates.y0 < initialCoordinates.y1) { + keyN = "y0"; + keyS = "y1"; + } else { + keyN = "y1"; + keyS = "y0"; + } + if (initialCoordinates.x0 < initialCoordinates.x1) { + keyW = "x0"; + keyE = "x1"; + } else { + keyW = "x1"; + keyE = "x0"; + } + + if (~direction.indexOf("n")) { + expect(finalCoordinates[keyN] - initialCoordinates[keyN]).toBeCloseTo( + dy + ); + } else if (~direction.indexOf("s")) { + expect(finalCoordinates[keyS] - initialCoordinates[keyS]).toBeCloseTo( + dy + ); + } + + if (~direction.indexOf("w")) { + expect(finalCoordinates[keyW] - initialCoordinates[keyW]).toBeCloseTo( + dx + ); + } else if (~direction.indexOf("e")) { + expect(finalCoordinates[keyE] - initialCoordinates[keyE]).toBeCloseTo( + dx + ); + } + }); + } + + function getPathCoordinates(pathString, x2p, y2p) { + var coordinates = []; + + pathString.match(constants.segmentRE).forEach(function(segment) { + var paramNumber = 0, + segmentType = segment.charAt(0), + xParams = constants.paramIsX[segmentType], + yParams = constants.paramIsY[segmentType], + nParams = constants.numParams[segmentType], + params = segment.substr(1).match(constants.paramRE); + + if (params) { + params.forEach(function(param) { + if (paramNumber >= nParams) return; + + if (xParams[paramNumber]) { + coordinates.push({ x: x2p(param) }); + } else if (yParams[paramNumber]) { + coordinates.push({ y: y2p(param) }); + } + + paramNumber++; }); + } + }); - return coordinates; - } + return coordinates; + } }); -var DBLCLICKDELAY = require('@src/plots/cartesian/constants').DBLCLICKDELAY; +var DBLCLICKDELAY = require("@src/plots/cartesian/constants").DBLCLICKDELAY; function mouseDown(node, x, y) { - node.dispatchEvent(new MouseEvent('mousedown', { - bubbles: true, - clientX: x, - clientY: y - })); + node.dispatchEvent(new MouseEvent("mousedown", { + bubbles: true, + clientX: x, + clientY: y + })); } function mouseMove(node, x, y) { - node.dispatchEvent(new MouseEvent('mousemove', { - bubbles: true, - clientX: x, - clientY: y - })); + node.dispatchEvent(new MouseEvent("mousemove", { + bubbles: true, + clientX: x, + clientY: y + })); } function mouseUp(node, x, y) { - node.dispatchEvent(new MouseEvent('mouseup', { - bubbles: true, - clientX: x, - clientY: y - })); + node.dispatchEvent(new MouseEvent("mouseup", { + bubbles: true, + clientX: x, + clientY: y + })); } function drag(node, dx, dy) { - var bbox = node.getBoundingClientRect(), - fromX = (bbox.left + bbox.right) / 2, - fromY = (bbox.bottom + bbox.top) / 2, - toX = fromX + dx, - toY = fromY + dy; - - mouseMove(node, fromX, fromY); - mouseDown(node, fromX, fromY); - - var promise = waitForDragCover().then(function(dragCoverNode) { - mouseMove(dragCoverNode, toX, toY); - mouseUp(dragCoverNode, toX, toY); - return waitForDragCoverRemoval(); - }); + var bbox = node.getBoundingClientRect(), + fromX = (bbox.left + bbox.right) / 2, + fromY = (bbox.bottom + bbox.top) / 2, + toX = fromX + dx, + toY = fromY + dy; + + mouseMove(node, fromX, fromY); + mouseDown(node, fromX, fromY); - return promise; + var promise = waitForDragCover().then(function(dragCoverNode) { + mouseMove(dragCoverNode, toX, toY); + mouseUp(dragCoverNode, toX, toY); + return waitForDragCoverRemoval(); + }); + + return promise; } function resize(direction, node, dx, dy) { - var bbox = node.getBoundingClientRect(); + var bbox = node.getBoundingClientRect(); - var fromX, fromY, toX, toY; + var fromX, fromY, toX, toY; - if(~direction.indexOf('n')) fromY = bbox.top; - else if(~direction.indexOf('s')) fromY = bbox.bottom; - else fromY = (bbox.bottom + bbox.top) / 2; + if (~direction.indexOf("n")) fromY = bbox.top; + else if (~direction.indexOf("s")) fromY = bbox.bottom; + else fromY = (bbox.bottom + bbox.top) / 2; - if(~direction.indexOf('w')) fromX = bbox.left; - else if(~direction.indexOf('e')) fromX = bbox.right; - else fromX = (bbox.left + bbox.right) / 2; + if (~direction.indexOf("w")) fromX = bbox.left; + else if (~direction.indexOf("e")) fromX = bbox.right; + else fromX = (bbox.left + bbox.right) / 2; - toX = fromX + dx; - toY = fromY + dy; + toX = fromX + dx; + toY = fromY + dy; - mouseMove(node, fromX, fromY); - mouseDown(node, fromX, fromY); + mouseMove(node, fromX, fromY); + mouseDown(node, fromX, fromY); - var promise = waitForDragCover().then(function(dragCoverNode) { - mouseMove(dragCoverNode, toX, toY); - mouseUp(dragCoverNode, toX, toY); - return waitForDragCoverRemoval(); - }); + var promise = waitForDragCover().then(function(dragCoverNode) { + mouseMove(dragCoverNode, toX, toY); + mouseUp(dragCoverNode, toX, toY); + return waitForDragCoverRemoval(); + }); - return promise; + return promise; } function waitForDragCover() { - return new Promise(function(resolve) { - var interval = DBLCLICKDELAY / 4, - timeout = 5000; - - var id = setInterval(function() { - var dragCoverNode = d3.selectAll('.dragcover').node(); - if(dragCoverNode) { - clearInterval(id); - resolve(dragCoverNode); - } - - timeout -= interval; - if(timeout < 0) { - clearInterval(id); - throw new Error('waitForDragCover: timeout'); - } - }, interval); - }); + return new Promise(function(resolve) { + var interval = DBLCLICKDELAY / 4, timeout = 5000; + + var id = setInterval( + function() { + var dragCoverNode = d3.selectAll(".dragcover").node(); + if (dragCoverNode) { + clearInterval(id); + resolve(dragCoverNode); + } + + timeout -= interval; + if (timeout < 0) { + clearInterval(id); + throw new Error("waitForDragCover: timeout"); + } + }, + interval + ); + }); } function waitForDragCoverRemoval() { - return new Promise(function(resolve) { - var interval = DBLCLICKDELAY / 4, - timeout = 5000; - - var id = setInterval(function() { - var dragCoverNode = d3.selectAll('.dragcover').node(); - if(!dragCoverNode) { - clearInterval(id); - resolve(dragCoverNode); - } - - timeout -= interval; - if(timeout < 0) { - clearInterval(id); - throw new Error('waitForDragCoverRemoval: timeout'); - } - }, interval); - }); + return new Promise(function(resolve) { + var interval = DBLCLICKDELAY / 4, timeout = 5000; + + var id = setInterval( + function() { + var dragCoverNode = d3.selectAll(".dragcover").node(); + if (!dragCoverNode) { + clearInterval(id); + resolve(dragCoverNode); + } + + timeout -= interval; + if (timeout < 0) { + clearInterval(id); + throw new Error("waitForDragCoverRemoval: timeout"); + } + }, + interval + ); + }); } diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 5a1274769a2..42a0695f32e 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -1,443 +1,467 @@ -var Sliders = require('@src/components/sliders'); -var constants = require('@src/components/sliders/constants'); - -var d3 = require('d3'); -var Plotly = require('@lib'); -var Lib = require('@src/lib'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); - -describe('sliders defaults', function() { - 'use strict'; - - var supply = Sliders.supplyLayoutDefaults; - - var layoutIn, layoutOut; - - beforeEach(function() { - layoutIn = {}; - layoutOut = {}; - }); - - it('should set \'visible\' to false when no steps are present', function() { - layoutIn.sliders = [{ - steps: [{ - method: 'relayout', - args: ['title', 'Hello World'] - }, { - method: 'update', - args: [ { 'marker.size': 20 }, { 'xaxis.range': [0, 10] }, [0, 1] ] - }, { - method: 'animate', - args: [ 'frame1', { transition: { duration: 500, ease: 'cubic-in-out' }}] - }] - }, { - bgcolor: 'red' - }, { - visible: false, - steps: [{ - method: 'relayout', - args: ['title', 'Hello World'] - }] - }]; - - supply(layoutIn, layoutOut); - - expect(layoutOut.sliders[0].visible).toBe(true); - expect(layoutOut.sliders[0].active).toEqual(0); - expect(layoutOut.sliders[0].steps[0].args.length).toEqual(2); - expect(layoutOut.sliders[0].steps[1].args.length).toEqual(3); - expect(layoutOut.sliders[0].steps[2].args.length).toEqual(2); - - expect(layoutOut.sliders[1].visible).toBe(false); - expect(layoutOut.sliders[1].active).toBeUndefined(); - - expect(layoutOut.sliders[2].visible).toBe(false); - expect(layoutOut.sliders[2].active).toBeUndefined(); - }); - - it('should not coerce currentvalue defaults unless currentvalue is visible', function() { - layoutIn.sliders = [{ - currentvalue: { - visible: false, - xanchor: 'left' - }, - steps: [ - {method: 'restyle', args: [], label: 'step0'}, - {method: 'restyle', args: [], label: 'step1'} - ] - }]; - - supply(layoutIn, layoutOut); - - expect(layoutOut.sliders[0].currentvalue.xanchor).toBeUndefined(); - expect(layoutOut.sliders[0].currentvalue.prefix).toBeUndefined(); - expect(layoutOut.sliders[0].currentvalue.suffix).toBeUndefined(); - expect(layoutOut.sliders[0].currentvalue.offset).toBeUndefined(); - expect(layoutOut.sliders[0].currentvalue.font).toBeUndefined(); - }); - - it('should set the default values equal to the labels', function() { - layoutIn.sliders = [{ - steps: [{ - method: 'relayout', args: [], - label: 'Label #1', - value: 'label-1' - }, { - method: 'update', args: [], - label: 'Label #2' - }, { - method: 'animate', args: [], - value: 'lacks-label' - }] - }]; - - supply(layoutIn, layoutOut); - - expect(layoutOut.sliders[0].steps.length).toEqual(3); - expect(layoutOut.sliders[0].steps).toEqual([{ - method: 'relayout', args: [], - label: 'Label #1', - value: 'label-1' - }, { - method: 'update', args: [], - label: 'Label #2', - value: 'Label #2' - }, { - method: 'animate', args: [], - label: 'step-2', - value: 'lacks-label' - }]); - }); - - it('should skip over non-object steps', function() { - layoutIn.sliders = [{ - steps: [ - null, - { - method: 'relayout', - args: ['title', 'Hello World'] - }, - 'remove' +var Sliders = require("@src/components/sliders"); +var constants = require("@src/components/sliders/constants"); + +var d3 = require("d3"); +var Plotly = require("@lib"); +var Lib = require("@src/lib"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var fail = require("../assets/fail_test"); + +describe("sliders defaults", function() { + "use strict"; + var supply = Sliders.supplyLayoutDefaults; + + var layoutIn, layoutOut; + + beforeEach(function() { + layoutIn = {}; + layoutOut = {}; + }); + + it("should set 'visible' to false when no steps are present", function() { + layoutIn.sliders = [ + { + steps: [ + { method: "relayout", args: ["title", "Hello World"] }, + { + method: "update", + args: [{ "marker.size": 20 }, { "xaxis.range": [0, 10] }, [0, 1]] + }, + { + method: "animate", + args: [ + "frame1", + { transition: { duration: 500, ease: "cubic-in-out" } } ] - }]; - - supply(layoutIn, layoutOut); + } + ] + }, + { bgcolor: "red" }, + { + visible: false, + steps: [{ method: "relayout", args: ["title", "Hello World"] }] + } + ]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0].visible).toBe(true); + expect(layoutOut.sliders[0].active).toEqual(0); + expect(layoutOut.sliders[0].steps[0].args.length).toEqual(2); + expect(layoutOut.sliders[0].steps[1].args.length).toEqual(3); + expect(layoutOut.sliders[0].steps[2].args.length).toEqual(2); + + expect(layoutOut.sliders[1].visible).toBe(false); + expect(layoutOut.sliders[1].active).toBeUndefined(); + + expect(layoutOut.sliders[2].visible).toBe(false); + expect(layoutOut.sliders[2].active).toBeUndefined(); + }); + + it( + "should not coerce currentvalue defaults unless currentvalue is visible", + function() { + layoutIn.sliders = [ + { + currentvalue: { visible: false, xanchor: "left" }, + steps: [ + { method: "restyle", args: [], label: "step0" }, + { method: "restyle", args: [], label: "step1" } + ] + } + ]; - expect(layoutOut.sliders[0].steps.length).toEqual(1); - expect(layoutOut.sliders[0].steps[0]).toEqual({ - method: 'relayout', - args: ['title', 'Hello World'], - label: 'step-1', - value: 'step-1', - }); - }); + supply(layoutIn, layoutOut); - it('should skip over steps with non-array \'args\' field', function() { - layoutIn.sliders = [{ - steps: [{ - method: 'restyle', - }, { - method: 'relayout', - args: ['title', 'Hello World'] - }, { - method: 'relayout', - args: null - }, {}] - }]; - - supply(layoutIn, layoutOut); - - expect(layoutOut.sliders[0].steps.length).toEqual(1); - expect(layoutOut.sliders[0].steps[0]).toEqual({ - method: 'relayout', - args: ['title', 'Hello World'], - label: 'step-1', - value: 'step-1', - }); + expect(layoutOut.sliders[0].currentvalue.xanchor).toBeUndefined(); + expect(layoutOut.sliders[0].currentvalue.prefix).toBeUndefined(); + expect(layoutOut.sliders[0].currentvalue.suffix).toBeUndefined(); + expect(layoutOut.sliders[0].currentvalue.offset).toBeUndefined(); + expect(layoutOut.sliders[0].currentvalue.font).toBeUndefined(); + } + ); + + it("should set the default values equal to the labels", function() { + layoutIn.sliders = [ + { + steps: [ + { method: "relayout", args: [], label: "Label #1", value: "label-1" }, + { method: "update", args: [], label: "Label #2" }, + { method: "animate", args: [], value: "lacks-label" } + ] + } + ]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0].steps.length).toEqual(3); + expect(layoutOut.sliders[0].steps).toEqual([ + { method: "relayout", args: [], label: "Label #1", value: "label-1" }, + { method: "update", args: [], label: "Label #2", value: "Label #2" }, + { method: "animate", args: [], label: "step-2", value: "lacks-label" } + ]); + }); + + it("should skip over non-object steps", function() { + layoutIn.sliders = [ + { + steps: [ + null, + { method: "relayout", args: ["title", "Hello World"] }, + "remove" + ] + } + ]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0].steps.length).toEqual(1); + expect(layoutOut.sliders[0].steps[0]).toEqual({ + method: "relayout", + args: ["title", "Hello World"], + label: "step-1", + value: "step-1" }); - - it('should keep ref to input update menu container', function() { - layoutIn.sliders = [{ - steps: [{ - method: 'relayout', - args: ['title', 'Hello World'] - }] - }, { - bgcolor: 'red' - }, { - visible: false, - steps: [{ - method: 'relayout', - args: ['title', 'Hello World'] - }] - }]; - - supply(layoutIn, layoutOut); - - expect(layoutOut.sliders[0]._input).toBe(layoutIn.sliders[0]); - expect(layoutOut.sliders[1]._input).toBe(layoutIn.sliders[1]); - expect(layoutOut.sliders[2]._input).toBe(layoutIn.sliders[2]); + }); + + it("should skip over steps with non-array 'args' field", function() { + layoutIn.sliders = [ + { + steps: [ + { method: "restyle" }, + { method: "relayout", args: ["title", "Hello World"] }, + { method: "relayout", args: null }, + {} + ] + } + ]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0].steps.length).toEqual(1); + expect(layoutOut.sliders[0].steps[0]).toEqual({ + method: "relayout", + args: ["title", "Hello World"], + label: "step-1", + value: "step-1" }); + }); + + it("should keep ref to input update menu container", function() { + layoutIn.sliders = [ + { steps: [{ method: "relayout", args: ["title", "Hello World"] }] }, + { bgcolor: "red" }, + { + visible: false, + steps: [{ method: "relayout", args: ["title", "Hello World"] }] + } + ]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0]._input).toBe(layoutIn.sliders[0]); + expect(layoutOut.sliders[1]._input).toBe(layoutIn.sliders[1]); + expect(layoutOut.sliders[2]._input).toBe(layoutIn.sliders[2]); + }); }); -describe('sliders initialization', function() { - 'use strict'; - var gd; - - beforeEach(function(done) { - gd = createGraphDiv(); - - Plotly.plot(gd, [{x: [1, 2, 3]}], { - sliders: [{ - transition: {duration: 0}, - steps: [ - {method: 'restyle', args: [], label: 'first'}, - {method: 'restyle', args: [], label: 'second'}, - ] - }] - }).then(done); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); - - it('does not set active on initial plot', function() { - expect(gd.layout.sliders[0].active).toBeUndefined(); - }); +describe("sliders initialization", function() { + "use strict"; + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + + Plotly.plot(gd, [{ x: [1, 2, 3] }], { + sliders: [ + { + transition: { duration: 0 }, + steps: [ + { method: "restyle", args: [], label: "first" }, + { method: "restyle", args: [], label: "second" } + ] + } + ] + }).then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("does not set active on initial plot", function() { + expect(gd.layout.sliders[0].active).toBeUndefined(); + }); }); -describe('ugly internal manipulation of steps', function() { - 'use strict'; - var gd; - - beforeEach(function(done) { - gd = createGraphDiv(); - - Plotly.plot(gd, [{x: [1, 2, 3]}], { - sliders: [{ - transition: {duration: 0}, - steps: [ - {method: 'restyle', args: [], label: 'first'}, - {method: 'restyle', args: [], label: 'second'}, - ] - }] - }).then(done); - }); +describe("ugly internal manipulation of steps", function() { + "use strict"; + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + + Plotly.plot(gd, [{ x: [1, 2, 3] }], { + sliders: [ + { + transition: { duration: 0 }, + steps: [ + { method: "restyle", args: [], label: "first" }, + { method: "restyle", args: [], label: "second" } + ] + } + ] + }).then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("adds and removes slider steps gracefully", function(done) { + expect(gd._fullLayout.sliders[0].active).toEqual(0); + + // Set the active index higher than it can go: + Plotly.relayout(gd, { "sliders[0].active": 2 }) + .then(function() { + // Confirm nothing changed + expect(gd._fullLayout.sliders[0].active).toEqual(0); - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); + // Add an option manually without calling API functions: + gd.layout.sliders[0].steps.push({ + method: "restyle", + args: [], + label: "first" + }); - it('adds and removes slider steps gracefully', function(done) { + // Now that it's been added, restyle and try again: + return Plotly.relayout(gd, { "sliders[0].active": 2 }); + }) + .then(function() { + // Confirm it's been changed: + expect(gd._fullLayout.sliders[0].active).toEqual(2); + + // Remove the option: + gd.layout.sliders[0].steps.pop(); + + // And redraw the plot: + return Plotly.redraw(gd); + }) + .then(function() { + // The selected option no longer exists, so confirm it's + // been fixed during the process of updating/drawing it: expect(gd._fullLayout.sliders[0].active).toEqual(0); - - // Set the active index higher than it can go: - Plotly.relayout(gd, {'sliders[0].active': 2}).then(function() { - // Confirm nothing changed - expect(gd._fullLayout.sliders[0].active).toEqual(0); - - // Add an option manually without calling API functions: - gd.layout.sliders[0].steps.push({method: 'restyle', args: [], label: 'first'}); - - // Now that it's been added, restyle and try again: - return Plotly.relayout(gd, {'sliders[0].active': 2}); - }).then(function() { - // Confirm it's been changed: - expect(gd._fullLayout.sliders[0].active).toEqual(2); - - // Remove the option: - gd.layout.sliders[0].steps.pop(); - - // And redraw the plot: - return Plotly.redraw(gd); - }).then(function() { - // The selected option no longer exists, so confirm it's - // been fixed during the process of updating/drawing it: - expect(gd._fullLayout.sliders[0].active).toEqual(0); - }).catch(fail).then(done); - }); + }) + .catch(fail) + .then(done); + }); }); -describe('sliders interactions', function() { - 'use strict'; +describe("sliders interactions", function() { + "use strict"; + var mock = require("@mocks/sliders.json"); + var mockCopy; - var mock = require('@mocks/sliders.json'); - var mockCopy; + var gd; - var gd; + beforeEach(function(done) { + gd = createGraphDiv(); - beforeEach(function(done) { - gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); - mockCopy = Lib.extendDeep({}, mock); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); - - it('should draw only visible sliders', function(done) { - expect(gd._fullLayout._pushmargin['slider-0']).toBeDefined(); - expect(gd._fullLayout._pushmargin['slider-1']).toBeDefined(); - - Plotly.relayout(gd, 'sliders[0].visible', false).then(function() { - assertNodeCount('.' + constants.groupClassName, 1); - expect(gd._fullLayout._pushmargin['slider-0']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['slider-1']).toBeDefined(); - expect(gd.layout.sliders.length).toEqual(2); - - return Plotly.relayout(gd, 'sliders[1]', null); - }) - .then(function() { - assertNodeCount('.' + constants.groupClassName, 0); - expect(gd._fullLayout._pushmargin['slider-0']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['slider-1']).toBeUndefined(); - expect(gd.layout.sliders.length).toEqual(1); - - return Plotly.relayout(gd, { - 'sliders[0].visible': true, - 'sliders[1].visible': true - }); - }).then(function() { - assertNodeCount('.' + constants.groupClassName, 1); - expect(gd._fullLayout._pushmargin['slider-0']).toBeDefined(); - expect(gd._fullLayout._pushmargin['slider-1']).toBeUndefined(); - - return Plotly.relayout(gd, { - 'sliders[1]': { - steps: [{ - method: 'relayout', - args: ['title', 'new title'], - label: '1970' - }, { - method: 'relayout', - args: ['title', 'new title'], - label: '1971' - }] - } - }); - }) - .then(function() { - assertNodeCount('.' + constants.groupClassName, 2); - expect(gd._fullLayout._pushmargin['slider-0']).toBeDefined(); - expect(gd._fullLayout._pushmargin['slider-1']).toBeDefined(); - }) - .catch(fail).then(done); - }); - - it('should respond to mouse clicks', function(done) { - var firstGroup = gd._fullLayout._infolayer.select('.' + constants.railTouchRectClass); - var firstGrip = gd._fullLayout._infolayer.select('.' + constants.gripRectClass); - var railNode = firstGroup.node(); - var touchRect = railNode.getBoundingClientRect(); - - var originalFill = firstGrip.style('fill'); - - // Dispatch a click on the right side of the bar: - railNode.dispatchEvent(new MouseEvent('mousedown', { - clientY: touchRect.top + 5, - clientX: touchRect.left + touchRect.width - 5, - })); - - expect(mockCopy.layout.sliders[0].active).toEqual(5); - var mousedownFill = firstGrip.style('fill'); - expect(mousedownFill).not.toEqual(originalFill); - - // Drag to the left side: - gd.dispatchEvent(new MouseEvent('mousemove', { - clientY: touchRect.top + 5, - clientX: touchRect.left + 5, - })); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - var mousemoveFill = firstGrip.style('fill'); - expect(mousemoveFill).toEqual(mousedownFill); + it("should draw only visible sliders", function(done) { + expect(gd._fullLayout._pushmargin["slider-0"]).toBeDefined(); + expect(gd._fullLayout._pushmargin["slider-1"]).toBeDefined(); - setTimeout(function() { - expect(mockCopy.layout.sliders[0].active).toEqual(0); + Plotly.relayout(gd, "sliders[0].visible", false) + .then(function() { + assertNodeCount("." + constants.groupClassName, 1); + expect(gd._fullLayout._pushmargin["slider-0"]).toBeUndefined(); + expect(gd._fullLayout._pushmargin["slider-1"]).toBeDefined(); + expect(gd.layout.sliders.length).toEqual(2); - gd.dispatchEvent(new MouseEvent('mouseup')); + return Plotly.relayout(gd, "sliders[1]", null); + }) + .then(function() { + assertNodeCount("." + constants.groupClassName, 0); + expect(gd._fullLayout._pushmargin["slider-0"]).toBeUndefined(); + expect(gd._fullLayout._pushmargin["slider-1"]).toBeUndefined(); + expect(gd.layout.sliders.length).toEqual(1); - var mouseupFill = firstGrip.style('fill'); - expect(mouseupFill).toEqual(originalFill); - expect(mockCopy.layout.sliders[0].active).toEqual(0); - - done(); - }, 100); - }); - - it('should issue events on interaction', function(done) { - var cntStart = 0; - var cntInteraction = 0; - var cntNonInteraction = 0; - var cntEnd = 0; - - gd.on('plotly_sliderstart', function() { - cntStart++; - }).on('plotly_sliderchange', function(datum) { - if(datum.interaction) { - cntInteraction++; - } else { - cntNonInteraction++; - } - }).on('plotly_sliderend', function() { - cntEnd++; + return Plotly.relayout(gd, { + "sliders[0].visible": true, + "sliders[1].visible": true }); - - function assertEventCounts(starts, interactions, noninteractions, ends) { - expect( - [cntStart, cntInteraction, cntNonInteraction, cntEnd] - ).toEqual( - [starts, interactions, noninteractions, ends] - ); + }) + .then(function() { + assertNodeCount("." + constants.groupClassName, 1); + expect(gd._fullLayout._pushmargin["slider-0"]).toBeDefined(); + expect(gd._fullLayout._pushmargin["slider-1"]).toBeUndefined(); + + return Plotly.relayout(gd, { + "sliders[1]": { + steps: [ + { + method: "relayout", + args: ["title", "new title"], + label: "1970" + }, + { + method: "relayout", + args: ["title", "new title"], + label: "1971" + } + ] + } + }); + }) + .then(function() { + assertNodeCount("." + constants.groupClassName, 2); + expect(gd._fullLayout._pushmargin["slider-0"]).toBeDefined(); + expect(gd._fullLayout._pushmargin["slider-1"]).toBeDefined(); + }) + .catch(fail) + .then(done); + }); + + it("should respond to mouse clicks", function(done) { + var firstGroup = gd._fullLayout._infolayer.select( + "." + constants.railTouchRectClass + ); + var firstGrip = gd._fullLayout._infolayer.select( + "." + constants.gripRectClass + ); + var railNode = firstGroup.node(); + var touchRect = railNode.getBoundingClientRect(); + + var originalFill = firstGrip.style("fill"); + + // Dispatch a click on the right side of the bar: + railNode.dispatchEvent(new MouseEvent("mousedown", { + clientY: touchRect.top + 5, + clientX: touchRect.left + touchRect.width - 5 + })); + + expect(mockCopy.layout.sliders[0].active).toEqual(5); + var mousedownFill = firstGrip.style("fill"); + expect(mousedownFill).not.toEqual(originalFill); + + // Drag to the left side: + gd.dispatchEvent(new MouseEvent("mousemove", { + clientY: touchRect.top + 5, + clientX: touchRect.left + 5 + })); + + var mousemoveFill = firstGrip.style("fill"); + expect(mousemoveFill).toEqual(mousedownFill); + + setTimeout( + function() { + expect(mockCopy.layout.sliders[0].active).toEqual(0); + + gd.dispatchEvent(new MouseEvent("mouseup")); + + var mouseupFill = firstGrip.style("fill"); + expect(mouseupFill).toEqual(originalFill); + expect(mockCopy.layout.sliders[0].active).toEqual(0); + + done(); + }, + 100 + ); + }); + + it("should issue events on interaction", function(done) { + var cntStart = 0; + var cntInteraction = 0; + var cntNonInteraction = 0; + var cntEnd = 0; + + gd + .on("plotly_sliderstart", function() { + cntStart++; + }) + .on("plotly_sliderchange", function(datum) { + if (datum.interaction) { + cntInteraction++; + } else { + cntNonInteraction++; } + }) + .on("plotly_sliderend", function() { + cntEnd++; + }); + + function assertEventCounts(starts, interactions, noninteractions, ends) { + expect([cntStart, cntInteraction, cntNonInteraction, cntEnd]).toEqual([ + starts, + interactions, + noninteractions, + ends + ]); + } - assertEventCounts(0, 0, 0, 0); - - var firstGroup = gd._fullLayout._infolayer.select('.' + constants.railTouchRectClass); - var railNode = firstGroup.node(); - var touchRect = railNode.getBoundingClientRect(); + assertEventCounts(0, 0, 0, 0); - // Dispatch a click on the right side of the bar: - railNode.dispatchEvent(new MouseEvent('mousedown', { - clientY: touchRect.top + 5, - clientX: touchRect.left + touchRect.width - 5, - })); + var firstGroup = gd._fullLayout._infolayer.select( + "." + constants.railTouchRectClass + ); + var railNode = firstGroup.node(); + var touchRect = railNode.getBoundingClientRect(); - setTimeout(function() { - // One slider received a mousedown, one received an interaction, and one received a change: - assertEventCounts(1, 1, 1, 0); + // Dispatch a click on the right side of the bar: + railNode.dispatchEvent(new MouseEvent("mousedown", { + clientY: touchRect.top + 5, + clientX: touchRect.left + touchRect.width - 5 + })); - // Drag to the left side: - gd.dispatchEvent(new MouseEvent('mousemove', { - clientY: touchRect.top + 5, - clientX: touchRect.left + 5, - })); + setTimeout( + function() { + // One slider received a mousedown, one received an interaction, and one received a change: + assertEventCounts(1, 1, 1, 0); - setTimeout(function() { - // On move, now to changes for the each slider, and no ends: - assertEventCounts(1, 2, 2, 0); + // Drag to the left side: + gd.dispatchEvent(new MouseEvent("mousemove", { + clientY: touchRect.top + 5, + clientX: touchRect.left + 5 + })); - gd.dispatchEvent(new MouseEvent('mouseup')); + setTimeout( + function() { + // On move, now to changes for the each slider, and no ends: + assertEventCounts(1, 2, 2, 0); - setTimeout(function() { - // Now an end: - assertEventCounts(1, 2, 2, 1); + gd.dispatchEvent(new MouseEvent("mouseup")); - done(); - }, 50); - }, 50); - }, 50); - }); + setTimeout( + function() { + // Now an end: + assertEventCounts(1, 2, 2, 1); - function assertNodeCount(query, cnt) { - expect(d3.selectAll(query).size()).toEqual(cnt); - } + done(); + }, + 50 + ); + }, + 50 + ); + }, + 50 + ); + }); + + function assertNodeCount(query, cnt) { + expect(d3.selectAll(query).size()).toEqual(cnt); + } }); diff --git a/test/jasmine/tests/snapshot_test.js b/test/jasmine/tests/snapshot_test.js index 72c01d2a8ab..485b5b64a3e 100644 --- a/test/jasmine/tests/snapshot_test.js +++ b/test/jasmine/tests/snapshot_test.js @@ -1,248 +1,255 @@ -var Plotly = require('@lib/index'); - -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); - -var subplotMock = require('../../image/mocks/multiple_subplots.json'); -var annotationMock = require('../../image/mocks/annotations.json'); - -describe('Plotly.Snapshot', function() { - 'use strict'; - - describe('clone', function() { - - var data, - layout, - dummyTrace1, dummyTrace2, - dummyGraphObj; - - dummyTrace1 = { - x: ['0', '1', '2', '3', '4', '5'], - y: ['2', '4', '6', '8', '6', '4'], - mode: 'markers', - name: 'Col2', - type: 'scatter' - }; - dummyTrace2 = { - x: ['0', '1', '2', '3', '4', '5'], - y: ['4', '6', '8', '10', '8', '6'], - mode: 'markers', - name: 'Col3', - type: 'scatter' +var Plotly = require("@lib/index"); + +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); + +var subplotMock = require("../../image/mocks/multiple_subplots.json"); +var annotationMock = require("../../image/mocks/annotations.json"); + +describe("Plotly.Snapshot", function() { + "use strict"; + describe("clone", function() { + var data, layout, dummyTrace1, dummyTrace2, dummyGraphObj; + + dummyTrace1 = { + x: ["0", "1", "2", "3", "4", "5"], + y: ["2", "4", "6", "8", "6", "4"], + mode: "markers", + name: "Col2", + type: "scatter" + }; + dummyTrace2 = { + x: ["0", "1", "2", "3", "4", "5"], + y: ["4", "6", "8", "10", "8", "6"], + mode: "markers", + name: "Col3", + type: "scatter" + }; + + data = [dummyTrace1, dummyTrace2]; + layout = { + title: "Chart Title", + showlegend: true, + autosize: true, + width: 688, + height: 460, + xaxis: { + title: "xaxis title", + range: [-0.323374917925, 5.32337491793], + type: "linear", + autorange: true + }, + yaxis: { + title: "yaxis title", + range: [1.41922290389, 10.5807770961], + type: "linear", + autorange: true + } + }; + + dummyGraphObj = { data: data, layout: layout }; + + it( + "should create a themeTile, with width certain things stripped out", + function() { + var themeOptions = { tileClass: "themes__thumb" }; + + // Defaults from clone() + var THEMETILE_DEFAULT_LAYOUT = { + autosize: true, + width: 150, + height: 150, + title: "", + showlegend: false, + margin: { l: 5, r: 5, t: 5, b: 5, pad: 0 }, + annotations: [] }; - data = [dummyTrace1, dummyTrace2]; - layout = { - title: 'Chart Title', - showlegend: true, - autosize: true, - width: 688, - height: 460, - xaxis: { - title: 'xaxis title', - range: [-0.323374917925, 5.32337491793], - type: 'linear', - autorange: true - }, - yaxis: { - title: 'yaxis title', - range: [1.41922290389, 10.5807770961], - type: 'linear', - autorange: true - } + var config = { + staticPlot: true, + plotGlPixelRatio: 2, + displaylogo: false, + showLink: false, + showTips: false, + setBackground: "opaque" }; - dummyGraphObj = { - data: data, - layout: layout + var themeTile = Plotly.Snapshot.clone(dummyGraphObj, themeOptions); + expect(themeTile.layout.height).toEqual( + THEMETILE_DEFAULT_LAYOUT.height + ); + expect(themeTile.layout.width).toEqual(THEMETILE_DEFAULT_LAYOUT.width); + expect(themeTile.gd.defaultLayout).toEqual(THEMETILE_DEFAULT_LAYOUT); + expect(themeTile.gd).toBe(themeTile.td); + // image server compatibility + expect(themeTile.config).toEqual(config); + } + ); + + it( + "should create a thumbnail for image export to the filewell", + function() { + var thumbnailOptions = { tileClass: "thumbnail" }; + + var THUMBNAIL_DEFAULT_LAYOUT = { + title: "", + hidesources: true, + showlegend: false, + hovermode: false, + dragmode: false, + zoom: false, + borderwidth: 0, + bordercolor: "", + margin: { l: 1, r: 1, t: 1, b: 1, pad: 0 }, + annotations: [] }; - it('should create a themeTile, with width certain things stripped out', function() { - var themeOptions = { - tileClass: 'themes__thumb' - }; - - // Defaults from clone() - var THEMETILE_DEFAULT_LAYOUT = { - autosize: true, - width: 150, - height: 150, - title: '', - showlegend: false, - margin: {'l': 5, 'r': 5, 't': 5, 'b': 5, 'pad': 0}, - annotations: [] - }; - - var config = { - staticPlot: true, - plotGlPixelRatio: 2, - displaylogo: false, - showLink: false, - showTips: false, - setBackground: 'opaque' - }; - - var themeTile = Plotly.Snapshot.clone(dummyGraphObj, themeOptions); - expect(themeTile.layout.height).toEqual(THEMETILE_DEFAULT_LAYOUT.height); - expect(themeTile.layout.width).toEqual(THEMETILE_DEFAULT_LAYOUT.width); - expect(themeTile.gd.defaultLayout).toEqual(THEMETILE_DEFAULT_LAYOUT); - expect(themeTile.gd).toBe(themeTile.td); // image server compatibility - expect(themeTile.config).toEqual(config); - }); - - it('should create a thumbnail for image export to the filewell', function() { - var thumbnailOptions = { - tileClass: 'thumbnail' - }; - - var THUMBNAIL_DEFAULT_LAYOUT = { - 'title': '', - 'hidesources': true, - 'showlegend': false, - 'hovermode': false, - 'dragmode': false, - 'zoom': false, - 'borderwidth': 0, - 'bordercolor': '', - 'margin': {'l': 1, 'r': 1, 't': 1, 'b': 1, 'pad': 0}, - 'annotations': [] - }; - - var thumbTile = Plotly.Snapshot.clone(dummyGraphObj, thumbnailOptions); - expect(thumbTile.layout.hidesources).toEqual(THUMBNAIL_DEFAULT_LAYOUT.hidesources); - expect(thumbTile.layout.showlegend).toEqual(THUMBNAIL_DEFAULT_LAYOUT.showlegend); - expect(thumbTile.layout.borderwidth).toEqual(THUMBNAIL_DEFAULT_LAYOUT.borderwidth); - expect(thumbTile.layout.annotations).toEqual(THUMBNAIL_DEFAULT_LAYOUT.annotations); - }); - - it('should create a 3D thumbnail with limited attributes', function() { - - var figure = { - data: [{ - type: 'scatter', - mode: 'markers', - y: [2, 4, 6, 5, 7, 4], - x: [1, 3, 4, 6, 3, 1], - name: 'C' - }], - layout: { - autosize: true, - scene: { - aspectratio: {y: 1, x: 1, z: 1} - } - }}; - - - var thumbnailOptions = { - tileClass: 'thumbnail' - }; - - var AXIS_OVERRIDE = { - title: '', - showaxeslabels: false, - showticklabels: false, - linetickenable: false - }; - - var thumbTile = Plotly.Snapshot.clone(figure, thumbnailOptions); - expect(thumbTile.layout.scene.xaxis).toEqual(AXIS_OVERRIDE); - expect(thumbTile.layout.scene.yaxis).toEqual(AXIS_OVERRIDE); - expect(thumbTile.layout.scene.zaxis).toEqual(AXIS_OVERRIDE); - }); - - - it('should create a custom sized Tile based on options', function() { - var customOptions = { - tileClass: 'notarealclass', - height: 888, - width: 888 - }; - - var customTile = Plotly.Snapshot.clone(dummyGraphObj, customOptions); - expect(customTile.layout.height).toEqual(customOptions.height); - expect(customTile.layout.width).toEqual(customOptions.width); - }); - - it('should not touch the data or layout if you do not specify an existing tileClass', function() { - var vanillaOptions = { - tileClass: 'notarealclass' - }; - - var vanillaPlotTile = Plotly.Snapshot.clone(dummyGraphObj, vanillaOptions); - expect(vanillaPlotTile.data[0].x).toEqual(data[0].x); - expect(vanillaPlotTile.layout).toEqual(layout); - expect(vanillaPlotTile.layout.height).toEqual(layout.height); - expect(vanillaPlotTile.layout.width).toEqual(layout.width); - }); - - it('should set the background parameter appropriately', function() { - var pt = Plotly.Snapshot.clone(dummyGraphObj, { - setBackground: 'transparent' - }); - expect(pt.config.setBackground).not.toBeDefined(); - - pt = Plotly.Snapshot.clone(dummyGraphObj, { - setBackground: 'blue' - }); - expect(pt.config.setBackground).toEqual('blue'); - }); + var thumbTile = Plotly.Snapshot.clone(dummyGraphObj, thumbnailOptions); + expect(thumbTile.layout.hidesources).toEqual( + THUMBNAIL_DEFAULT_LAYOUT.hidesources + ); + expect(thumbTile.layout.showlegend).toEqual( + THUMBNAIL_DEFAULT_LAYOUT.showlegend + ); + expect(thumbTile.layout.borderwidth).toEqual( + THUMBNAIL_DEFAULT_LAYOUT.borderwidth + ); + expect(thumbTile.layout.annotations).toEqual( + THUMBNAIL_DEFAULT_LAYOUT.annotations + ); + } + ); + + it("should create a 3D thumbnail with limited attributes", function() { + var figure = { + data: [ + { + type: "scatter", + mode: "markers", + y: [2, 4, 6, 5, 7, 4], + x: [1, 3, 4, 6, 3, 1], + name: "C" + } + ], + layout: { autosize: true, scene: { aspectratio: { y: 1, x: 1, z: 1 } } } + }; + + var thumbnailOptions = { tileClass: "thumbnail" }; + + var AXIS_OVERRIDE = { + title: "", + showaxeslabels: false, + showticklabels: false, + linetickenable: false + }; + + var thumbTile = Plotly.Snapshot.clone(figure, thumbnailOptions); + expect(thumbTile.layout.scene.xaxis).toEqual(AXIS_OVERRIDE); + expect(thumbTile.layout.scene.yaxis).toEqual(AXIS_OVERRIDE); + expect(thumbTile.layout.scene.zaxis).toEqual(AXIS_OVERRIDE); }); - describe('toSVG', function() { - var parser = new DOMParser(), - gd; + it("should create a custom sized Tile based on options", function() { + var customOptions = { + tileClass: "notarealclass", + height: 888, + width: 888 + }; - beforeEach(function() { - gd = createGraphDiv(); - }); + var customTile = Plotly.Snapshot.clone(dummyGraphObj, customOptions); + expect(customTile.layout.height).toEqual(customOptions.height); + expect(customTile.layout.width).toEqual(customOptions.width); + }); - afterEach(destroyGraphDiv); + it( + "should not touch the data or layout if you do not specify an existing tileClass", + function() { + var vanillaOptions = { tileClass: "notarealclass" }; + + var vanillaPlotTile = Plotly.Snapshot.clone( + dummyGraphObj, + vanillaOptions + ); + expect(vanillaPlotTile.data[0].x).toEqual(data[0].x); + expect(vanillaPlotTile.layout).toEqual(layout); + expect(vanillaPlotTile.layout.height).toEqual(layout.height); + expect(vanillaPlotTile.layout.width).toEqual(layout.width); + } + ); + + it("should set the background parameter appropriately", function() { + var pt = Plotly.Snapshot.clone(dummyGraphObj, { + setBackground: "transparent" + }); + expect(pt.config.setBackground).not.toBeDefined(); + + pt = Plotly.Snapshot.clone(dummyGraphObj, { setBackground: "blue" }); + expect(pt.config.setBackground).toEqual("blue"); + }); + }); + describe("toSVG", function() { + var parser = new DOMParser(), gd; - it('should not return any nested svg tags of plots', function(done) { - Plotly.plot(gd, subplotMock.data, subplotMock.layout).then(function() { - return Plotly.Snapshot.toSVG(gd); - }).then(function(svg) { - var svgDOM = parser.parseFromString(svg, 'image/svg+xml'), - svgElements = svgDOM.getElementsByTagName('svg'); + beforeEach(function() { + gd = createGraphDiv(); + }); - expect(svgElements.length).toBe(1); - }).then(done); - }); + afterEach(destroyGraphDiv); - it('should not return any nested svg tags of annotations', function(done) { - Plotly.plot(gd, annotationMock.data, annotationMock.layout).then(function() { - return Plotly.Snapshot.toSVG(gd); - }).then(function(svg) { - var svgDOM = parser.parseFromString(svg, 'image/svg+xml'), - svgElements = svgDOM.getElementsByTagName('svg'); + it("should not return any nested svg tags of plots", function(done) { + Plotly.plot(gd, subplotMock.data, subplotMock.layout) + .then(function() { + return Plotly.Snapshot.toSVG(gd); + }) + .then(function(svg) { + var svgDOM = parser.parseFromString(svg, "image/svg+xml"), + svgElements = svgDOM.getElementsByTagName("svg"); - expect(svgElements.length).toBe(1); - }).then(done); - }); + expect(svgElements.length).toBe(1); + }) + .then(done); + }); - it('should force *visibility: visible* for text elements with *visibility: inherit*', function(done) { - d3.select(gd).style('visibility', 'inherit'); + it("should not return any nested svg tags of annotations", function(done) { + Plotly.plot(gd, annotationMock.data, annotationMock.layout) + .then(function() { + return Plotly.Snapshot.toSVG(gd); + }) + .then(function(svg) { + var svgDOM = parser.parseFromString(svg, "image/svg+xml"), + svgElements = svgDOM.getElementsByTagName("svg"); + + expect(svgElements.length).toBe(1); + }) + .then(done); + }); - Plotly.plot(gd, subplotMock.data, subplotMock.layout).then(function() { + it( + "should force *visibility: visible* for text elements with *visibility: inherit*", + function(done) { + d3.select(gd).style("visibility", "inherit"); - d3.select(gd).selectAll('text').each(function() { - expect(d3.select(this).style('visibility')).toEqual('visible'); - }); + Plotly.plot(gd, subplotMock.data, subplotMock.layout) + .then(function() { + d3.select(gd).selectAll("text").each(function() { + expect(d3.select(this).style("visibility")).toEqual("visible"); + }); - return Plotly.Snapshot.toSVG(gd); - }) - .then(function(svg) { - var svgDOM = parser.parseFromString(svg, 'image/svg+xml'), - textElements = svgDOM.getElementsByTagName('text'); + return Plotly.Snapshot.toSVG(gd); + }) + .then(function(svg) { + var svgDOM = parser.parseFromString(svg, "image/svg+xml"), + textElements = svgDOM.getElementsByTagName("text"); - for(var i = 0; i < textElements.length; i++) { - expect(textElements[i].style.visibility).toEqual('visible'); - } + for (var i = 0; i < textElements.length; i++) { + expect(textElements[i].style.visibility).toEqual("visible"); + } - done(); - }); - }); - }); + done(); + }); + } + ); + }); }); diff --git a/test/jasmine/tests/surface_test.js b/test/jasmine/tests/surface_test.js index 96aa26f0193..02361ee2586 100644 --- a/test/jasmine/tests/surface_test.js +++ b/test/jasmine/tests/surface_test.js @@ -1,181 +1,187 @@ -var Surface = require('@src/traces/surface'); +var Surface = require("@src/traces/surface"); -var Lib = require('@src/lib'); +var Lib = require("@src/lib"); +describe("Test surface", function() { + "use strict"; + describe("supplyDefaults", function() { + var supplyDefaults = Surface.supplyDefaults; -describe('Test surface', function() { - 'use strict'; + var defaultColor = "#444", layout = {}; - describe('supplyDefaults', function() { - var supplyDefaults = Surface.supplyDefaults; + var traceIn, traceOut; - var defaultColor = '#444', - layout = {}; - - var traceIn, traceOut; - - beforeEach(function() { - traceOut = {}; - }); - - it('should set \'visible\' to false if \'z\' isn\'t provided', function() { - traceIn = {}; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); - - it('should fill \'x\' and \'y\' if not provided', function() { - traceIn = { - z: [[1, 2, 3], [2, 1, 2]] - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.x).toEqual([0, 1, 2]); - expect(traceOut.y).toEqual([0, 1]); - }); - - it('should coerce \'project\' if contours or highlight lines are enabled', function() { - traceIn = { - z: [[1, 2, 3], [2, 1, 2]], - contours: { - x: {}, - y: { show: true }, - z: { show: false, highlight: false } - } - }; - - var fullOpts = { - show: false, - highlight: true, - project: { x: false, y: false, z: false }, - highlightcolor: '#444', - highlightwidth: 2 - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.contours.x).toEqual(fullOpts); - expect(traceOut.contours.y).toEqual(Lib.extendDeep({}, fullOpts, { - show: true, - color: '#444', - width: 2, - usecolormap: false - })); - expect(traceOut.contours.z).toEqual({ show: false, highlight: false }); - }); + beforeEach(function() { + traceOut = {}; + }); - it('should coerce contour style attributes if contours lines are enabled', function() { - traceIn = { - z: [[1, 2, 3], [2, 1, 2]], - contours: { - x: { show: true } - } - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.contours.x.color).toEqual('#444'); - expect(traceOut.contours.x.width).toEqual(2); - expect(traceOut.contours.x.usecolormap).toEqual(false); - - ['y', 'z'].forEach(function(ax) { - expect(traceOut.contours[ax].color).toBeUndefined(); - expect(traceOut.contours[ax].width).toBeUndefined(); - expect(traceOut.contours[ax].usecolormap).toBeUndefined(); - }); - }); + it("should set 'visible' to false if 'z' isn't provided", function() { + traceIn = {}; - it('should coerce colorscale and colorbar attributes', function() { - traceIn = { - z: [[1, 2, 3], [2, 1, 2]] - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.cauto).toBe(true); - expect(traceOut.cmin).toBeUndefined(); - expect(traceOut.cmax).toBeUndefined(); - expect(traceOut.colorscale).toEqual([ - [0, 'rgb(5,10,172)'], - [0.35, 'rgb(106,137,247)'], - [0.5, 'rgb(190,190,190)'], - [0.6, 'rgb(220,170,132)'], - [0.7, 'rgb(230,145,90)'], - [1, 'rgb(178,10,28)'] - ]); - expect(traceOut.showscale).toBe(true); - expect(traceOut.colorbar).toBeDefined(); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); - it('should coerce \'c\' attributes with \'z\' if \'c\' isn\'t present', function() { - traceIn = { - z: [[1, 2, 3], [2, 1, 2]], - zauto: false, - zmin: 0, - zmax: 10 - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.cauto).toEqual(false); - expect(traceOut.cmin).toEqual(0); - expect(traceOut.cmax).toEqual(10); - }); + it("should fill 'x' and 'y' if not provided", function() { + traceIn = { z: [[1, 2, 3], [2, 1, 2]] }; - it('should coerce \'c\' attributes with \'c\' values regardless of `\'z\' if \'c\' is present', function() { - traceIn = { - z: [[1, 2, 3], [2, 1, 2]], - zauto: false, - zmin: 0, - zmax: 10, - cauto: true, - cmin: -10, - cmax: 20 - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.cauto).toEqual(true); - expect(traceOut.cmin).toEqual(-10); - expect(traceOut.cmax).toEqual(20); - }); + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.x).toEqual([0, 1, 2]); + expect(traceOut.y).toEqual([0, 1]); + }); - it('should default \'c\' attributes with if \'surfacecolor\' is present', function() { - traceIn = { - z: [[1, 2, 3], [2, 1, 2]], - surfacecolor: [[2, 1, 2], [1, 2, 3]], - zauto: false, - zmin: 0, - zmax: 10 - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.cauto).toEqual(true); - expect(traceOut.cmin).toBeUndefined(); - expect(traceOut.cmax).toBeUndefined(); + it( + "should coerce 'project' if contours or highlight lines are enabled", + function() { + traceIn = { + z: [[1, 2, 3], [2, 1, 2]], + contours: { + x: {}, + y: { show: true }, + z: { show: false, highlight: false } + } + }; + + var fullOpts = { + show: false, + highlight: true, + project: { x: false, y: false, z: false }, + highlightcolor: "#444", + highlightwidth: 2 + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.contours.x).toEqual(fullOpts); + expect(traceOut.contours.y).toEqual( + Lib.extendDeep({}, fullOpts, { + show: true, + color: "#444", + width: 2, + usecolormap: false + }) + ); + expect(traceOut.contours.z).toEqual({ show: false, highlight: false }); + } + ); + + it( + "should coerce contour style attributes if contours lines are enabled", + function() { + traceIn = { + z: [[1, 2, 3], [2, 1, 2]], + contours: { x: { show: true } } + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.contours.x.color).toEqual("#444"); + expect(traceOut.contours.x.width).toEqual(2); + expect(traceOut.contours.x.usecolormap).toEqual(false); + + ["y", "z"].forEach(function(ax) { + expect(traceOut.contours[ax].color).toBeUndefined(); + expect(traceOut.contours[ax].width).toBeUndefined(); + expect(traceOut.contours[ax].usecolormap).toBeUndefined(); }); + } + ); + + it("should coerce colorscale and colorbar attributes", function() { + traceIn = { z: [[1, 2, 3], [2, 1, 2]] }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.cauto).toBe(true); + expect(traceOut.cmin).toBeUndefined(); + expect(traceOut.cmax).toBeUndefined(); + expect(traceOut.colorscale).toEqual([ + [0, "rgb(5,10,172)"], + [0.35, "rgb(106,137,247)"], + [0.5, "rgb(190,190,190)"], + [0.6, "rgb(220,170,132)"], + [0.7, "rgb(230,145,90)"], + [1, "rgb(178,10,28)"] + ]); + expect(traceOut.showscale).toBe(true); + expect(traceOut.colorbar).toBeDefined(); + }); - it('should inherit layout.calendar', function() { - traceIn = { - z: [[1, 2, 3], [2, 1, 2]] - }; - supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'}); - - // we always fill calendar attributes, because it's hard to tell if - // we're on a date axis at this point. - expect(traceOut.xcalendar).toBe('islamic'); - expect(traceOut.ycalendar).toBe('islamic'); - expect(traceOut.zcalendar).toBe('islamic'); - }); + it( + "should coerce 'c' attributes with 'z' if 'c' isn't present", + function() { + traceIn = { + z: [[1, 2, 3], [2, 1, 2]], + zauto: false, + zmin: 0, + zmax: 10 + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.cauto).toEqual(false); + expect(traceOut.cmin).toEqual(0); + expect(traceOut.cmax).toEqual(10); + } + ); + + it( + "should coerce 'c' attributes with 'c' values regardless of `'z' if 'c' is present", + function() { + traceIn = { + z: [[1, 2, 3], [2, 1, 2]], + zauto: false, + zmin: 0, + zmax: 10, + cauto: true, + cmin: -10, + cmax: 20 + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.cauto).toEqual(true); + expect(traceOut.cmin).toEqual(-10); + expect(traceOut.cmax).toEqual(20); + } + ); + + it( + "should default 'c' attributes with if 'surfacecolor' is present", + function() { + traceIn = { + z: [[1, 2, 3], [2, 1, 2]], + surfacecolor: [[2, 1, 2], [1, 2, 3]], + zauto: false, + zmin: 0, + zmax: 10 + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.cauto).toEqual(true); + expect(traceOut.cmin).toBeUndefined(); + expect(traceOut.cmax).toBeUndefined(); + } + ); + + it("should inherit layout.calendar", function() { + traceIn = { z: [[1, 2, 3], [2, 1, 2]] }; + supplyDefaults(traceIn, traceOut, defaultColor, { calendar: "islamic" }); + + // we always fill calendar attributes, because it's hard to tell if + // we're on a date axis at this point. + expect(traceOut.xcalendar).toBe("islamic"); + expect(traceOut.ycalendar).toBe("islamic"); + expect(traceOut.zcalendar).toBe("islamic"); + }); - it('should take its own calendars', function() { - var traceIn = { - z: [[1, 2, 3], [2, 1, 2]], - xcalendar: 'coptic', - ycalendar: 'ethiopian', - zcalendar: 'mayan' - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.xcalendar).toBe('coptic'); - expect(traceOut.ycalendar).toBe('ethiopian'); - expect(traceOut.zcalendar).toBe('mayan'); - }); + it("should take its own calendars", function() { + var traceIn = { + z: [[1, 2, 3], [2, 1, 2]], + xcalendar: "coptic", + ycalendar: "ethiopian", + zcalendar: "mayan" + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.xcalendar).toBe("coptic"); + expect(traceOut.ycalendar).toBe("ethiopian"); + expect(traceOut.zcalendar).toBe("mayan"); }); + }); }); diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js index 3468f65317c..a13ba24b5fb 100644 --- a/test/jasmine/tests/svg_text_utils_test.js +++ b/test/jasmine/tests/svg_text_utils_test.js @@ -1,207 +1,205 @@ -var d3 = require('d3'); +var d3 = require("d3"); + +var util = require("@src/lib/svg_text_utils"); + +describe("svg+text utils", function() { + "use strict"; + describe("convertToTspans should", function() { + function mockTextSVGElement(txt) { + return d3 + .select("body") + .append("svg") + .attr("id", "text") + .append("text") + .text(txt) + .call(util.convertToTspans) + .attr("transform", "translate(50,50)"); + } + + function assertAnchorLink(node, href) { + var a = node.select("a"); + + expect(a.attr("xlink:href")).toBe(href); + expect(a.attr("xlink:show")).toBe(href === null ? null : "new"); + } + + function assertTspanStyle(node, style) { + var tspan = node.select("tspan"); + expect(tspan.attr("style")).toBe(style); + } + + function assertAnchorAttrs(node) { + var a = node.select("a"); + + var WHITE_LIST = ["xlink:href", "xlink:show", "style"], + attrs = listAttributes(a.node()); + + // check that no other attribute are found in anchor, + // which can be lead to XSS attacks. + var hasWrongAttr = attrs.some(function(attr) { + return WHITE_LIST.indexOf(attr) === -1; + }); + + expect(hasWrongAttr).toBe(false); + } + + function listAttributes(node) { + var items = Array.prototype.slice.call(node.attributes); + + var attrs = items.map(function(item) { + return item.name; + }); + + return attrs; + } + + afterEach(function() { + d3.select("#text").remove(); + }); + + it("check for XSS attack in href", function() { + var node = mockTextSVGElement( + "
XSS" + ); + + expect(node.text()).toEqual("XSS"); + assertAnchorAttrs(node); + assertAnchorLink(node, null); + }); + + it( + "check for XSS attack in href (with plenty of white spaces)", + function() { + var node = mockTextSVGElement( + "XSS" + ); + + expect(node.text()).toEqual("XSS"); + assertAnchorAttrs(node); + assertAnchorLink(node, null); + } + ); + + it("whitelist relative hrefs (interpreted as http)", function() { + var node = mockTextSVGElement('mylink'); + + expect(node.text()).toEqual("mylink"); + assertAnchorAttrs(node); + assertAnchorLink(node, "/mylink"); + }); + + it("whitelist http hrefs", function() { + var node = mockTextSVGElement( + 'bl.ocks.org' + ); + + expect(node.text()).toEqual("bl.ocks.org"); + assertAnchorAttrs(node); + assertAnchorLink(node, "http://bl.ocks.org/"); + }); + + it("whitelist https hrefs", function() { + var node = mockTextSVGElement('plot.ly'); + + expect(node.text()).toEqual("plot.ly"); + assertAnchorAttrs(node); + assertAnchorLink(node, "https://plot.ly"); + }); + + it("whitelist mailto hrefs", function() { + var node = mockTextSVGElement( + 'support' + ); + + expect(node.text()).toEqual("support"); + assertAnchorAttrs(node); + assertAnchorLink(node, "mailto:support@plot.ly"); + }); -var util = require('@src/lib/svg_text_utils'); + it("wrap XSS attacks in href", function() { + var textCases = [ + 'Subtitle', + 'Subtitle' + ]; + + textCases.forEach(function(textCase) { + var node = mockTextSVGElement(textCase); + + expect(node.text()).toEqual("Subtitle"); + assertAnchorAttrs(node); + assertAnchorLink( + node, + "XSS onmouseover=alert(1) style=font-size:300px" + ); + }); + }); + + it("should keep query parameters in href", function() { + var textCases = [ + 'abc.com?shared-key', + 'abc.com?shared-key' + ]; + + textCases.forEach(function(textCase) { + var node = mockTextSVGElement(textCase); + + assertAnchorAttrs(node); + expect(node.text()).toEqual("abc.com?shared-key"); + assertAnchorLink( + node, + "https://abc.com/myFeature.jsp?name=abc&pwd=def" + ); + }); + }); + + it("allow basic spans", function() { + var node = mockTextSVGElement("text"); + expect(node.text()).toEqual("text"); + assertTspanStyle(node, null); + }); + + it("ignore unquoted styles in spans", function() { + var node = mockTextSVGElement("text"); + + expect(node.text()).toEqual("text"); + assertTspanStyle(node, null); + }); -describe('svg+text utils', function() { - 'use strict'; + it("allow quoted styles in spans", function() { + var node = mockTextSVGElement('text'); - describe('convertToTspans should', function() { + expect(node.text()).toEqual("text"); + assertTspanStyle(node, "quoted: yeah;"); + }); + + it("ignore extra stuff after span styles", function() { + var node = mockTextSVGElement( + 'text' + ); + + expect(node.text()).toEqual("text"); + assertTspanStyle(node, "quoted: yeah;"); + }); + + it("escapes HTML entities in span styles", function() { + var node = mockTextSVGElement( + 'text' + ); + + expect(node.text()).toEqual("text"); + assertTspanStyle(node, "quoted: yeah&';;"); + }); - function mockTextSVGElement(txt) { - return d3.select('body') - .append('svg') - .attr('id', 'text') - .append('text') - .text(txt) - .call(util.convertToTspans) - .attr('transform', 'translate(50,50)'); - } - - function assertAnchorLink(node, href) { - var a = node.select('a'); + it("decode some HTML entities in text", function() { + var node = mockTextSVGElement( + "100μ & < 10 > 0  " + + "100 × 20 ± 0.5 °" + ); - expect(a.attr('xlink:href')).toBe(href); - expect(a.attr('xlink:show')).toBe(href === null ? null : 'new'); - } - - function assertTspanStyle(node, style) { - var tspan = node.select('tspan'); - expect(tspan.attr('style')).toBe(style); - } - - function assertAnchorAttrs(node) { - var a = node.select('a'); - - var WHITE_LIST = ['xlink:href', 'xlink:show', 'style'], - attrs = listAttributes(a.node()); - - // check that no other attribute are found in anchor, - // which can be lead to XSS attacks. - - var hasWrongAttr = attrs.some(function(attr) { - return WHITE_LIST.indexOf(attr) === -1; - }); - - expect(hasWrongAttr).toBe(false); - } - - function listAttributes(node) { - var items = Array.prototype.slice.call(node.attributes); - - var attrs = items.map(function(item) { - return item.name; - }); - - return attrs; - } - - afterEach(function() { - d3.select('#text').remove(); - }); - - it('check for XSS attack in href', function() { - var node = mockTextSVGElement( - 'XSS' - ); - - expect(node.text()).toEqual('XSS'); - assertAnchorAttrs(node); - assertAnchorLink(node, null); - }); - - it('check for XSS attack in href (with plenty of white spaces)', function() { - var node = mockTextSVGElement( - 'XSS' - ); - - expect(node.text()).toEqual('XSS'); - assertAnchorAttrs(node); - assertAnchorLink(node, null); - }); - - it('whitelist relative hrefs (interpreted as http)', function() { - var node = mockTextSVGElement( - 'mylink' - ); - - expect(node.text()).toEqual('mylink'); - assertAnchorAttrs(node); - assertAnchorLink(node, '/mylink'); - }); - - it('whitelist http hrefs', function() { - var node = mockTextSVGElement( - 'bl.ocks.org' - ); - - expect(node.text()).toEqual('bl.ocks.org'); - assertAnchorAttrs(node); - assertAnchorLink(node, 'http://bl.ocks.org/'); - }); - - it('whitelist https hrefs', function() { - var node = mockTextSVGElement( - 'plot.ly' - ); - - expect(node.text()).toEqual('plot.ly'); - assertAnchorAttrs(node); - assertAnchorLink(node, 'https://plot.ly'); - }); - - it('whitelist mailto hrefs', function() { - var node = mockTextSVGElement( - 'support' - ); - - expect(node.text()).toEqual('support'); - assertAnchorAttrs(node); - assertAnchorLink(node, 'mailto:support@plot.ly'); - }); - - it('wrap XSS attacks in href', function() { - var textCases = [ - 'Subtitle', - 'Subtitle' - ]; - - textCases.forEach(function(textCase) { - var node = mockTextSVGElement(textCase); - - expect(node.text()).toEqual('Subtitle'); - assertAnchorAttrs(node); - assertAnchorLink(node, 'XSS onmouseover=alert(1) style=font-size:300px'); - }); - }); - - it('should keep query parameters in href', function() { - var textCases = [ - 'abc.com?shared-key', - 'abc.com?shared-key' - ]; - - textCases.forEach(function(textCase) { - var node = mockTextSVGElement(textCase); - - assertAnchorAttrs(node); - expect(node.text()).toEqual('abc.com?shared-key'); - assertAnchorLink(node, 'https://abc.com/myFeature.jsp?name=abc&pwd=def'); - }); - }); - - it('allow basic spans', function() { - var node = mockTextSVGElement( - 'text' - ); - - expect(node.text()).toEqual('text'); - assertTspanStyle(node, null); - }); - - it('ignore unquoted styles in spans', function() { - var node = mockTextSVGElement( - 'text' - ); - - expect(node.text()).toEqual('text'); - assertTspanStyle(node, null); - }); - - it('allow quoted styles in spans', function() { - var node = mockTextSVGElement( - 'text' - ); - - expect(node.text()).toEqual('text'); - assertTspanStyle(node, 'quoted: yeah;'); - }); - - it('ignore extra stuff after span styles', function() { - var node = mockTextSVGElement( - 'text' - ); - - expect(node.text()).toEqual('text'); - assertTspanStyle(node, 'quoted: yeah;'); - }); - - it('escapes HTML entities in span styles', function() { - var node = mockTextSVGElement( - 'text' - ); - - expect(node.text()).toEqual('text'); - assertTspanStyle(node, 'quoted: yeah&\';;'); - }); - - it('decode some HTML entities in text', function() { - var node = mockTextSVGElement( - '100μ & < 10 > 0  ' + - '100 × 20 ± 0.5 °' - ); - - expect(node.text()).toEqual('100μ & < 10 > 0  100 × 20 ± 0.5 °'); - }); + expect(node.text()).toEqual( + "100\u03BC & < 10 > 0 \xA0100 \xD7 20 \xB1 0.5 \xB0" + ); }); + }); }); diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js index c563a500b1b..5393002cb0e 100644 --- a/test/jasmine/tests/ternary_test.js +++ b/test/jasmine/tests/ternary_test.js @@ -1,336 +1,390 @@ -var Plotly = require('@lib'); -var Lib = require('@src/lib'); +var Plotly = require("@lib"); +var Lib = require("@src/lib"); -var supplyLayoutDefaults = require('@src/plots/ternary/layout/defaults'); +var supplyLayoutDefaults = require("@src/plots/ternary/layout/defaults"); -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); -var click = require('../assets/click'); -var doubleClick = require('../assets/double_click'); -var customMatchers = require('../assets/custom_matchers'); +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); +var click = require("../assets/click"); +var doubleClick = require("../assets/double_click"); +var customMatchers = require("../assets/custom_matchers"); +describe("ternary plots", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); -describe('ternary plots', function() { - 'use strict'; + afterEach(destroyGraphDiv); - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - afterEach(destroyGraphDiv); - - describe('with scatterternary trace(s)', function() { - var mock = require('@mocks/ternary_simple.json'); - var gd; - - var pointPos = [391, 219]; - var blankPos = [200, 200]; - - beforeEach(function(done) { - gd = createGraphDiv(); - - var mockCopy = Lib.extendDeep({}, mock); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); - - it('should be able to toggle trace visibility', function(done) { - expect(countTraces('scatter')).toEqual(1); - - Plotly.restyle(gd, 'visible', false).then(function() { - expect(countTraces('scatter')).toEqual(0); - - return Plotly.restyle(gd, 'visible', true); - }).then(function() { - expect(countTraces('scatter')).toEqual(1); - - return Plotly.restyle(gd, 'visible', 'legendonly'); - }).then(function() { - expect(countTraces('scatter')).toEqual(0); + describe("with scatterternary trace(s)", function() { + var mock = require("@mocks/ternary_simple.json"); + var gd; - return Plotly.restyle(gd, 'visible', true); - }).then(function() { - expect(countTraces('scatter')).toEqual(1); + var pointPos = [391, 219]; + var blankPos = [200, 200]; - done(); - }); - }); - - it('should be able to delete and add traces', function(done) { - expect(countTernarySubplot()).toEqual(1); - expect(countTraces('scatter')).toEqual(1); + beforeEach(function(done) { + gd = createGraphDiv(); - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countTernarySubplot()).toEqual(0); - expect(countTraces('scatter')).toEqual(0); + var mockCopy = Lib.extendDeep({}, mock); - var trace = Lib.extendDeep({}, mock.data[0]); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - return Plotly.addTraces(gd, [trace]); - }).then(function() { - expect(countTernarySubplot()).toEqual(1); - expect(countTraces('scatter')).toEqual(1); + it("should be able to toggle trace visibility", function(done) { + expect(countTraces("scatter")).toEqual(1); - var trace = Lib.extendDeep({}, mock.data[0]); + Plotly.restyle(gd, "visible", false) + .then(function() { + expect(countTraces("scatter")).toEqual(0); - return Plotly.addTraces(gd, [trace]); - }).then(function() { - expect(countTernarySubplot()).toEqual(1); - expect(countTraces('scatter')).toEqual(2); + return Plotly.restyle(gd, "visible", true); + }) + .then(function() { + expect(countTraces("scatter")).toEqual(1); - return Plotly.deleteTraces(gd, [0]); - }).then(function() { - expect(countTernarySubplot()).toEqual(1); - expect(countTraces('scatter')).toEqual(1); + return Plotly.restyle(gd, "visible", "legendonly"); + }) + .then(function() { + expect(countTraces("scatter")).toEqual(0); - done(); - }); - }); + return Plotly.restyle(gd, "visible", true); + }) + .then(function() { + expect(countTraces("scatter")).toEqual(1); - it('should be able to restyle', function(done) { - Plotly.restyle(gd, { a: [[1, 2, 3]]}, 0).then(function() { - var transforms = []; - d3.selectAll('.ternary .point').each(function() { - var point = d3.select(this); - transforms.push(point.attr('transform')); - }); - - expect(transforms).toEqual([ - 'translate(186.45,209.8)', - 'translate(118.53,170.59)', - 'translate(248.76,117.69)' - ]); - }).then(done); + done(); }); + }); - it('should display to hover labels', function() { - var hoverLabels; - - mouseEvent('mousemove', blankPos[0], blankPos[1]); - hoverLabels = findHoverLabels(); - expect(hoverLabels.size()).toEqual(0, 'only on data points'); + it("should be able to delete and add traces", function(done) { + expect(countTernarySubplot()).toEqual(1); + expect(countTraces("scatter")).toEqual(1); - mouseEvent('mousemove', pointPos[0], pointPos[1]); - hoverLabels = findHoverLabels(); - expect(hoverLabels.size()).toEqual(1, 'one per data point'); + Plotly.deleteTraces(gd, [0]) + .then(function() { + expect(countTernarySubplot()).toEqual(0); + expect(countTraces("scatter")).toEqual(0); - var rows = hoverLabels.selectAll('tspan'); - expect(rows[0][0].innerHTML).toEqual('Component A: 0.5', 'with correct text'); - expect(rows[0][1].innerHTML).toEqual('B: 0.25', 'with correct text'); - expect(rows[0][2].innerHTML).toEqual('Component C: 0.25', 'with correct text'); - }); + var trace = Lib.extendDeep({}, mock.data[0]); - it('should respond to hover interactions by', function() { - var hoverCnt = 0, - unhoverCnt = 0; - - var hoverData, unhoverData; - - gd.on('plotly_hover', function(eventData) { - hoverCnt++; - hoverData = eventData.points[0]; - }); - - gd.on('plotly_unhover', function(eventData) { - unhoverCnt++; - unhoverData = eventData.points[0]; - }); - - mouseEvent('mousemove', blankPos[0], blankPos[1]); - expect(hoverData).toBe(undefined, 'not firing on blank points'); - expect(unhoverData).toBe(undefined, 'not firing on blank points'); - - mouseEvent('mousemove', pointPos[0], pointPos[1]); - expect(hoverData).not.toBe(undefined, 'firing on data points'); - expect(Object.keys(hoverData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'x', 'y', 'xaxis', 'yaxis' - ], 'returning the correct event data keys'); - expect(hoverData.curveNumber).toEqual(0, 'returning the correct curve number'); - expect(hoverData.pointNumber).toEqual(0, 'returning the correct point number'); - - mouseEvent('mouseout', pointPos[0], pointPos[1]); - expect(unhoverData).not.toBe(undefined, 'firing on data points'); - expect(Object.keys(unhoverData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'x', 'y', 'xaxis', 'yaxis' - ], 'returning the correct event data keys'); - expect(unhoverData.curveNumber).toEqual(0, 'returning the correct curve number'); - expect(unhoverData.pointNumber).toEqual(0, 'returning the correct point number'); - - expect(hoverCnt).toEqual(1); - expect(unhoverCnt).toEqual(1); - }); + return Plotly.addTraces(gd, [trace]); + }) + .then(function() { + expect(countTernarySubplot()).toEqual(1); + expect(countTraces("scatter")).toEqual(1); - it('should respond to click interactions by', function() { - var ptData; + var trace = Lib.extendDeep({}, mock.data[0]); - gd.on('plotly_click', function(eventData) { - ptData = eventData.points[0]; - }); + return Plotly.addTraces(gd, [trace]); + }) + .then(function() { + expect(countTernarySubplot()).toEqual(1); + expect(countTraces("scatter")).toEqual(2); - click(blankPos[0], blankPos[1]); - expect(ptData).toBe(undefined, 'not firing on blank points'); + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + expect(countTernarySubplot()).toEqual(1); + expect(countTraces("scatter")).toEqual(1); - click(pointPos[0], pointPos[1]); - expect(ptData).not.toBe(undefined, 'firing on data points'); - expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'x', 'y', 'xaxis', 'yaxis' - ], 'returning the correct event data keys'); - expect(ptData.curveNumber).toEqual(0, 'returning the correct curve number'); - expect(ptData.pointNumber).toEqual(0, 'returning the correct point number'); - }); - - it('should respond zoom drag interactions', function(done) { - assertRange(gd, [0.231, 0.2, 0.11]); - - drag([[383, 213], [293, 243]]); - assertRange(gd, [0.4435, 0.2462, 0.1523]); - - doubleClick(pointPos[0], pointPos[1]).then(function() { - assertRange(gd, [0, 0, 0]); - - done(); - }); + done(); }); }); - describe('static plots', function() { - var mock = require('@mocks/ternary_simple.json'); - var gd; - - beforeEach(function(done) { - gd = createGraphDiv(); + it("should be able to restyle", function(done) { + Plotly.restyle(gd, { a: [[1, 2, 3]] }, 0) + .then(function() { + var transforms = []; + d3.selectAll(".ternary .point").each(function() { + var point = d3.select(this); + transforms.push(point.attr("transform")); + }); + + expect(transforms).toEqual([ + "translate(186.45,209.8)", + "translate(118.53,170.59)", + "translate(248.76,117.69)" + ]); + }) + .then(done); + }); - var mockCopy = Lib.extendDeep({}, mock); - var config = { staticPlot: true }; + it("should display to hover labels", function() { + var hoverLabels; + + mouseEvent("mousemove", blankPos[0], blankPos[1]); + hoverLabels = findHoverLabels(); + expect(hoverLabels.size()).toEqual(0, "only on data points"); + + mouseEvent("mousemove", pointPos[0], pointPos[1]); + hoverLabels = findHoverLabels(); + expect(hoverLabels.size()).toEqual(1, "one per data point"); + + var rows = hoverLabels.selectAll("tspan"); + expect(rows[0][0].innerHTML).toEqual( + "Component A: 0.5", + "with correct text" + ); + expect(rows[0][1].innerHTML).toEqual("B: 0.25", "with correct text"); + expect(rows[0][2].innerHTML).toEqual( + "Component C: 0.25", + "with correct text" + ); + }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout, config).then(done); - }); + it("should respond to hover interactions by", function() { + var hoverCnt = 0, unhoverCnt = 0; + + var hoverData, unhoverData; + + gd.on("plotly_hover", function(eventData) { + hoverCnt++; + hoverData = eventData.points[0]; + }); + + gd.on("plotly_unhover", function(eventData) { + unhoverCnt++; + unhoverData = eventData.points[0]; + }); + + mouseEvent("mousemove", blankPos[0], blankPos[1]); + expect(hoverData).toBe(undefined, "not firing on blank points"); + expect(unhoverData).toBe(undefined, "not firing on blank points"); + + mouseEvent("mousemove", pointPos[0], pointPos[1]); + expect(hoverData).not.toBe(undefined, "firing on data points"); + expect(Object.keys(hoverData)).toEqual( + [ + "data", + "fullData", + "curveNumber", + "pointNumber", + "x", + "y", + "xaxis", + "yaxis" + ], + "returning the correct event data keys" + ); + expect(hoverData.curveNumber).toEqual( + 0, + "returning the correct curve number" + ); + expect(hoverData.pointNumber).toEqual( + 0, + "returning the correct point number" + ); + + mouseEvent("mouseout", pointPos[0], pointPos[1]); + expect(unhoverData).not.toBe(undefined, "firing on data points"); + expect(Object.keys(unhoverData)).toEqual( + [ + "data", + "fullData", + "curveNumber", + "pointNumber", + "x", + "y", + "xaxis", + "yaxis" + ], + "returning the correct event data keys" + ); + expect(unhoverData.curveNumber).toEqual( + 0, + "returning the correct curve number" + ); + expect(unhoverData.pointNumber).toEqual( + 0, + "returning the correct point number" + ); + + expect(hoverCnt).toEqual(1); + expect(unhoverCnt).toEqual(1); + }); - it('should not respond to drag', function(done) { - var range = [0.231, 0.2, 0.11]; + it("should respond to click interactions by", function() { + var ptData; + + gd.on("plotly_click", function(eventData) { + ptData = eventData.points[0]; + }); + + click(blankPos[0], blankPos[1]); + expect(ptData).toBe(undefined, "not firing on blank points"); + + click(pointPos[0], pointPos[1]); + expect(ptData).not.toBe(undefined, "firing on data points"); + expect(Object.keys(ptData)).toEqual( + [ + "data", + "fullData", + "curveNumber", + "pointNumber", + "x", + "y", + "xaxis", + "yaxis" + ], + "returning the correct event data keys" + ); + expect(ptData.curveNumber).toEqual( + 0, + "returning the correct curve number" + ); + expect(ptData.pointNumber).toEqual( + 0, + "returning the correct point number" + ); + }); - assertRange(gd, range); + it("should respond zoom drag interactions", function(done) { + assertRange(gd, [0.231, 0.2, 0.11]); - drag([[390, 220], [300, 250]]); - assertRange(gd, range); + drag([[383, 213], [293, 243]]); + assertRange(gd, [0.4435, 0.2462, 0.1523]); - doubleClick(390, 220).then(function() { - assertRange(gd, range); + doubleClick(pointPos[0], pointPos[1]).then(function() { + assertRange(gd, [0, 0, 0]); - done(); - }); - }); + done(); + }); }); + }); - function countTernarySubplot() { - return d3.selectAll('.ternary').size(); - } + describe("static plots", function() { + var mock = require("@mocks/ternary_simple.json"); + var gd; - function countTraces(type) { - return d3.selectAll('.ternary').selectAll('g.trace.' + type).size(); - } + beforeEach(function(done) { + gd = createGraphDiv(); - function findHoverLabels() { - return d3.select('.hoverlayer').selectAll('g'); - } + var mockCopy = Lib.extendDeep({}, mock); + var config = { staticPlot: true }; - function drag(path) { - var len = path.length; + Plotly.plot(gd, mockCopy.data, mockCopy.layout, config).then(done); + }); - mouseEvent('mousemove', path[0][0], path[0][1]); - mouseEvent('mousedown', path[0][0], path[0][1]); + it("should not respond to drag", function(done) { + var range = [0.231, 0.2, 0.11]; - path.slice(1, len).forEach(function(pt) { - mouseEvent('mousemove', pt[0], pt[1]); - }); + assertRange(gd, range); - mouseEvent('mouseup', path[len - 1][0], path[len - 1][1]); - } + drag([[390, 220], [300, 250]]); + assertRange(gd, range); - function assertRange(gd, expected) { - var ternary = gd._fullLayout.ternary; - var actual = [ - ternary.aaxis.min, - ternary.baxis.min, - ternary.caxis.min - ]; + doubleClick(390, 220).then(function() { + assertRange(gd, range); - expect(actual).toBeCloseToArray(expected); - } -}); + done(); + }); + }); + }); -describe('ternary defaults', function() { - 'use strict'; + function countTernarySubplot() { + return d3.selectAll(".ternary").size(); + } - var layoutIn, layoutOut, fullData; + function countTraces(type) { + return d3.selectAll(".ternary").selectAll("g.trace." + type).size(); + } - beforeEach(function() { - layoutOut = { - font: { color: 'red' } - }; + function findHoverLabels() { + return d3.select(".hoverlayer").selectAll("g"); + } - // needs a ternary-ref in a trace in order to be detected - fullData = [{ type: 'scatterternary', subplot: 'ternary' }]; - }); + function drag(path) { + var len = path.length; - it('should fill empty containers', function() { - layoutIn = {}; + mouseEvent("mousemove", path[0][0], path[0][1]); + mouseEvent("mousedown", path[0][0], path[0][1]); - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutIn).toEqual({ ternary: {} }); - expect(layoutOut.ternary.aaxis.type).toEqual('linear'); - expect(layoutOut.ternary.baxis.type).toEqual('linear'); - expect(layoutOut.ternary.caxis.type).toEqual('linear'); + path.slice(1, len).forEach(function(pt) { + mouseEvent("mousemove", pt[0], pt[1]); }); - it('should coerce \'min\' values to 0 and delete them for user data if they contradict', function() { - layoutIn = { - ternary: { - aaxis: { min: 1 }, - baxis: { min: 1 }, - caxis: { min: 1 }, - sum: 2 - } - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.ternary.aaxis.min).toEqual(0); - expect(layoutOut.ternary.baxis.min).toEqual(0); - expect(layoutOut.ternary.caxis.min).toEqual(0); - expect(layoutOut.ternary.sum).toEqual(2); - expect(layoutIn.ternary.aaxis.min).toBeUndefined(); - expect(layoutIn.ternary.baxis.min).toBeUndefined(); - expect(layoutIn.ternary.caxis.min).toBeUndefined(); - }); + mouseEvent("mouseup", path[len - 1][0], path[len - 1][1]); + } - it('should default \'title\' to Component + _name', function() { - layoutIn = {}; + function assertRange(gd, expected) { + var ternary = gd._fullLayout.ternary; + var actual = [ternary.aaxis.min, ternary.baxis.min, ternary.caxis.min]; - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.ternary.aaxis.title).toEqual('Component A'); - expect(layoutOut.ternary.baxis.title).toEqual('Component B'); - expect(layoutOut.ternary.caxis.title).toEqual('Component C'); - }); + expect(actual).toBeCloseToArray(expected); + } +}); - it('should default \'gricolor\' to 60% dark', function() { - layoutIn = { - ternary: { - aaxis: { showgrid: true, color: 'red' }, - baxis: { showgrid: true }, - caxis: { gridcolor: 'black' }, - bgcolor: 'blue' - }, - paper_bgcolor: 'green' - }; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.ternary.aaxis.gridcolor).toEqual('rgb(102, 0, 153)'); - expect(layoutOut.ternary.baxis.gridcolor).toEqual('rgb(27, 27, 180)'); - expect(layoutOut.ternary.caxis.gridcolor).toEqual('black'); - }); +describe("ternary defaults", function() { + "use strict"; + var layoutIn, layoutOut, fullData; + + beforeEach(function() { + layoutOut = { font: { color: "red" } }; + + // needs a ternary-ref in a trace in order to be detected + fullData = [{ type: "scatterternary", subplot: "ternary" }]; + }); + + it("should fill empty containers", function() { + layoutIn = {}; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn).toEqual({ ternary: {} }); + expect(layoutOut.ternary.aaxis.type).toEqual("linear"); + expect(layoutOut.ternary.baxis.type).toEqual("linear"); + expect(layoutOut.ternary.caxis.type).toEqual("linear"); + }); + + it( + "should coerce 'min' values to 0 and delete them for user data if they contradict", + function() { + layoutIn = { + ternary: { + aaxis: { min: 1 }, + baxis: { min: 1 }, + caxis: { min: 1 }, + sum: 2 + } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.ternary.aaxis.min).toEqual(0); + expect(layoutOut.ternary.baxis.min).toEqual(0); + expect(layoutOut.ternary.caxis.min).toEqual(0); + expect(layoutOut.ternary.sum).toEqual(2); + expect(layoutIn.ternary.aaxis.min).toBeUndefined(); + expect(layoutIn.ternary.baxis.min).toBeUndefined(); + expect(layoutIn.ternary.caxis.min).toBeUndefined(); + } + ); + + it("should default 'title' to Component + _name", function() { + layoutIn = {}; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.ternary.aaxis.title).toEqual("Component A"); + expect(layoutOut.ternary.baxis.title).toEqual("Component B"); + expect(layoutOut.ternary.caxis.title).toEqual("Component C"); + }); + + it("should default 'gricolor' to 60% dark", function() { + layoutIn = { + ternary: { + aaxis: { showgrid: true, color: "red" }, + baxis: { showgrid: true }, + caxis: { gridcolor: "black" }, + bgcolor: "blue" + }, + paper_bgcolor: "green" + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.ternary.aaxis.gridcolor).toEqual("rgb(102, 0, 153)"); + expect(layoutOut.ternary.baxis.gridcolor).toEqual("rgb(27, 27, 180)"); + expect(layoutOut.ternary.caxis.gridcolor).toEqual("black"); + }); }); diff --git a/test/jasmine/tests/titles_test.js b/test/jasmine/tests/titles_test.js index 7e6b58e64d5..e837b2d17cc 100644 --- a/test/jasmine/tests/titles_test.js +++ b/test/jasmine/tests/titles_test.js @@ -1,123 +1,131 @@ -var d3 = require('d3'); - -var Plotly = require('@lib/index'); -var interactConstants = require('@src/constants/interactions'); - -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mouseEvent = require('../assets/mouse_event'); - -describe('editable titles', function() { - 'use strict'; - - var data = [{x: [1, 2, 3], y: [1, 2, 3]}]; - - var gd; - - afterEach(destroyGraphDiv); - - beforeEach(function() { - gd = createGraphDiv(); +var d3 = require("d3"); + +var Plotly = require("@lib/index"); +var interactConstants = require("@src/constants/interactions"); + +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var mouseEvent = require("../assets/mouse_event"); + +describe("editable titles", function() { + "use strict"; + var data = [{ x: [1, 2, 3], y: [1, 2, 3] }]; + + var gd; + + afterEach(destroyGraphDiv); + + beforeEach(function() { + gd = createGraphDiv(); + }); + + function checkTitle(letter, text, opacityOut, opacityIn) { + var titleEl = d3.select("." + letter + "title"); + expect(titleEl.text()).toBe(text); + expect(+titleEl.style("opacity")).toBe(opacityOut); + + var bb = titleEl.node().getBoundingClientRect(), + xCenter = (bb.left + bb.right) / 2, + yCenter = (bb.top + bb.bottom) / 2, + done, + promise = new Promise(function(resolve) { + done = resolve; + }); + + mouseEvent("mouseover", xCenter, yCenter); + setTimeout( + function() { + expect(+titleEl.style("opacity")).toBe(opacityIn); + + mouseEvent("mouseout", xCenter, yCenter); + setTimeout( + function() { + expect(+titleEl.style("opacity")).toBe(opacityOut); + done(); + }, + interactConstants.HIDE_PLACEHOLDER + 50 + ); + }, + interactConstants.SHOW_PLACEHOLDER + 50 + ); + + return promise; + } + + function editTitle(letter, attr, text) { + return new Promise(function(resolve) { + gd.once("plotly_relayout", function(eventData) { + expect(eventData[attr]).toEqual(text, [letter, attr, eventData]); + setTimeout(resolve, 10); + }); + + var textNode = document.querySelector("." + letter + "title"); + textNode.dispatchEvent(new window.MouseEvent("click")); + + var editNode = document.querySelector(".plugin-editable.editable"); + editNode.dispatchEvent(new window.FocusEvent("focus")); + editNode.textContent = text; + editNode.dispatchEvent(new window.FocusEvent("focus")); + editNode.dispatchEvent(new window.FocusEvent("blur")); }); - - function checkTitle(letter, text, opacityOut, opacityIn) { - var titleEl = d3.select('.' + letter + 'title'); - expect(titleEl.text()).toBe(text); - expect(+titleEl.style('opacity')).toBe(opacityOut); - - var bb = titleEl.node().getBoundingClientRect(), - xCenter = (bb.left + bb.right) / 2, - yCenter = (bb.top + bb.bottom) / 2, - done, - promise = new Promise(function(resolve) { done = resolve; }); - - mouseEvent('mouseover', xCenter, yCenter); - setTimeout(function() { - expect(+titleEl.style('opacity')).toBe(opacityIn); - - mouseEvent('mouseout', xCenter, yCenter); - setTimeout(function() { - expect(+titleEl.style('opacity')).toBe(opacityOut); - done(); - }, interactConstants.HIDE_PLACEHOLDER + 50); - }, interactConstants.SHOW_PLACEHOLDER + 50); - - return promise; - } - - function editTitle(letter, attr, text) { - return new Promise(function(resolve) { - gd.once('plotly_relayout', function(eventData) { - expect(eventData[attr]).toEqual(text, [letter, attr, eventData]); - setTimeout(resolve, 10); - }); - - var textNode = document.querySelector('.' + letter + 'title'); - textNode.dispatchEvent(new window.MouseEvent('click')); - - var editNode = document.querySelector('.plugin-editable.editable'); - editNode.dispatchEvent(new window.FocusEvent('focus')); - editNode.textContent = text; - editNode.dispatchEvent(new window.FocusEvent('focus')); - editNode.dispatchEvent(new window.FocusEvent('blur')); - }); - } - - it('shows default titles semi-opaque with no hover effects', function(done) { - Plotly.plot(gd, data, {}, {editable: true}) - .then(function() { - return Promise.all([ - // Check all three titles in parallel. This only works because - // we're using synthetic events, not a real mouse. It's a big - // win though because the test takes 1.2 seconds with the - // animations... - checkTitle('x', 'Click to enter X axis title', 0.2, 0.2), - checkTitle('y', 'Click to enter Y axis title', 0.2, 0.2), - checkTitle('g', 'Click to enter Plot title', 0.2, 0.2) - ]); - }) - .then(done); - }); - - it('has hover effects for blank titles', function(done) { - Plotly.plot(gd, data, { - xaxis: {title: ''}, - yaxis: {title: ''}, - title: '' - }, {editable: true}) - .then(function() { - return Promise.all([ - checkTitle('x', 'Click to enter X axis title', 0, 1), - checkTitle('y', 'Click to enter Y axis title', 0, 1), - checkTitle('g', 'Click to enter Plot title', 0, 1) - ]); - }) - .then(done); - }); - - it('has no hover effects for titles that used to be blank', function(done) { - Plotly.plot(gd, data, { - xaxis: {title: ''}, - yaxis: {title: ''}, - title: '' - }, {editable: true}) - .then(function() { - return editTitle('x', 'xaxis.title', 'XXX'); - }) - .then(function() { - return editTitle('y', 'yaxis.title', 'YYY'); - }) - .then(function() { - return editTitle('g', 'title', 'TTT'); - }) - .then(function() { - return Promise.all([ - checkTitle('x', 'XXX', 1, 1), - checkTitle('y', 'YYY', 1, 1), - checkTitle('g', 'TTT', 1, 1) - ]); - }) - .then(done); - }); - + } + + it("shows default titles semi-opaque with no hover effects", function(done) { + Plotly.plot(gd, data, {}, { editable: true }) + .then(function() { + return Promise.all([ + // Check all three titles in parallel. This only works because + // we're using synthetic events, not a real mouse. It's a big + // win though because the test takes 1.2 seconds with the + // animations... + checkTitle("x", "Click to enter X axis title", 0.2, 0.2), + checkTitle("y", "Click to enter Y axis title", 0.2, 0.2), + checkTitle("g", "Click to enter Plot title", 0.2, 0.2) + ]); + }) + .then(done); + }); + + it("has hover effects for blank titles", function(done) { + Plotly.plot( + gd, + data, + { xaxis: { title: "" }, yaxis: { title: "" }, title: "" }, + { editable: true } + ) + .then(function() { + return Promise.all([ + checkTitle("x", "Click to enter X axis title", 0, 1), + checkTitle("y", "Click to enter Y axis title", 0, 1), + checkTitle("g", "Click to enter Plot title", 0, 1) + ]); + }) + .then(done); + }); + + it("has no hover effects for titles that used to be blank", function(done) { + Plotly.plot( + gd, + data, + { xaxis: { title: "" }, yaxis: { title: "" }, title: "" }, + { editable: true } + ) + .then(function() { + return editTitle("x", "xaxis.title", "XXX"); + }) + .then(function() { + return editTitle("y", "yaxis.title", "YYY"); + }) + .then(function() { + return editTitle("g", "title", "TTT"); + }) + .then(function() { + return Promise.all([ + checkTitle("x", "XXX", 1, 1), + checkTitle("y", "YYY", 1, 1), + checkTitle("g", "TTT", 1, 1) + ]); + }) + .then(done); + }); }); diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index 6cde6567067..1e79ebdf4bf 100644 --- a/test/jasmine/tests/toimage_test.js +++ b/test/jasmine/tests/toimage_test.js @@ -1,122 +1,131 @@ // move toimage to plot_api_test.js // once established and confirmed? -var Plotly = require('@lib/index'); - -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var subplotMock = require('@mocks/multiple_subplots.json'); - - -describe('Plotly.toImage', function() { - 'use strict'; - - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(function() { - - // make sure ALL graph divs are deleted, - // even the ones generated by Plotly.toImage - d3.selectAll('.js-plotly-plot').remove(); - d3.selectAll('#graph').remove(); - }); - - it('should be attached to Plotly', function() { - expect(Plotly.toImage).toBeDefined(); - }); - - it('should return a promise', function(done) { - function isPromise(x) { - return !!x.then && typeof x.then === 'function'; - } - - var returnValue = Plotly.plot(gd, subplotMock.data, subplotMock.layout) - .then(Plotly.toImage); - - expect(isPromise(returnValue)).toBe(true); - - returnValue.then(done); +var Plotly = require("@lib/index"); + +var d3 = require("d3"); +var createGraphDiv = require("../assets/create_graph_div"); +var subplotMock = require("@mocks/multiple_subplots.json"); + +describe("Plotly.toImage", function() { + "use strict"; + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + // make sure ALL graph divs are deleted, + // even the ones generated by Plotly.toImage + d3.selectAll(".js-plotly-plot").remove(); + d3.selectAll("#graph").remove(); + }); + + it("should be attached to Plotly", function() { + expect(Plotly.toImage).toBeDefined(); + }); + + it("should return a promise", function(done) { + function isPromise(x) { + return !!x.then && typeof x.then === "function"; + } + + var returnValue = Plotly.plot( + gd, + subplotMock.data, + subplotMock.layout + ).then(Plotly.toImage); + + expect(isPromise(returnValue)).toBe(true); + + returnValue.then(done); + }); + + it("should throw error with unsupported file type", function(done) { + // error should actually come in the svgToImg step + Plotly.plot(gd, subplotMock.data, subplotMock.layout).then(function(gd) { + Plotly.toImage(gd, { format: "x" }).catch(function(err) { + expect(err.message).toEqual("Image format is not jpeg, png or svg"); + done(); + }); }); - - it('should throw error with unsupported file type', function(done) { - // error should actually come in the svgToImg step - - Plotly.plot(gd, subplotMock.data, subplotMock.layout) - .then(function(gd) { - Plotly.toImage(gd, {format: 'x'}).catch(function(err) { - expect(err.message).toEqual('Image format is not jpeg, png or svg'); - done(); - }); - }); - - }); - - it('should throw error with height and/or width < 1', function(done) { - // let user know that Plotly expects pixel values - Plotly.plot(gd, subplotMock.data, subplotMock.layout) - .then(function(gd) { - return Plotly.toImage(gd, {height: 0.5}).catch(function(err) { - expect(err.message).toEqual('Height and width should be pixel values.'); - }); - }).then(function() { - Plotly.toImage(gd, {width: 0.5}).catch(function(err) { - expect(err.message).toEqual('Height and width should be pixel values.'); - done(); - }); - }); - }); - - it('should create img with proper height and width', function(done) { - var img = document.createElement('img'); - - // specify height and width - subplotMock.layout.height = 600; - subplotMock.layout.width = 700; - - Plotly.plot(gd, subplotMock.data, subplotMock.layout).then(function(gd) { - expect(gd.layout.height).toBe(600); - expect(gd.layout.width).toBe(700); - return Plotly.toImage(gd); - }).then(function(url) { - return new Promise(function(resolve) { - img.src = url; - img.onload = function() { - expect(img.height).toBe(600); - expect(img.width).toBe(700); - }; - // now provide height and width in opts - resolve(Plotly.toImage(gd, {height: 400, width: 400})); - }); - }).then(function(url) { - img.src = url; - img.onload = function() { - expect(img.height).toBe(400); - expect(img.width).toBe(400); - done(); - }; + }); + + it("should throw error with height and/or width < 1", function(done) { + // let user know that Plotly expects pixel values + Plotly.plot(gd, subplotMock.data, subplotMock.layout) + .then(function(gd) { + return Plotly.toImage(gd, { height: 0.5 }).catch(function(err) { + expect(err.message).toEqual( + "Height and width should be pixel values." + ); }); - }); - - it('should create proper file type', function(done) { - var plot = Plotly.plot(gd, subplotMock.data, subplotMock.layout); - - plot.then(function(gd) { - return Plotly.toImage(gd, {format: 'png'}); - }).then(function(url) { - expect(url.split('png')[0]).toBe('data:image/'); - // now do jpeg - return Plotly.toImage(gd, {format: 'jpeg'}); - }).then(function(url) { - expect(url.split('jpeg')[0]).toBe('data:image/'); - // now do svg - return Plotly.toImage(gd, {format: 'svg'}); - }).then(function(url) { - expect(url.split('svg')[0]).toBe('data:image/'); - done(); + }) + .then(function() { + Plotly.toImage(gd, { width: 0.5 }).catch(function(err) { + expect(err.message).toEqual( + "Height and width should be pixel values." + ); + done(); }); - }); + }); + }); + + it("should create img with proper height and width", function(done) { + var img = document.createElement("img"); + + // specify height and width + subplotMock.layout.height = 600; + subplotMock.layout.width = 700; + + Plotly.plot(gd, subplotMock.data, subplotMock.layout) + .then(function(gd) { + expect(gd.layout.height).toBe(600); + expect(gd.layout.width).toBe(700); + return Plotly.toImage(gd); + }) + .then(function(url) { + return new Promise(function(resolve) { + img.src = url; + img.onload = function() { + expect(img.height).toBe(600); + expect(img.width).toBe(700); + }; + // now provide height and width in opts + resolve(Plotly.toImage(gd, { height: 400, width: 400 })); + }); + }) + .then(function(url) { + img.src = url; + img.onload = function() { + expect(img.height).toBe(400); + expect(img.width).toBe(400); + done(); + }; + }); + }); + + it("should create proper file type", function(done) { + var plot = Plotly.plot(gd, subplotMock.data, subplotMock.layout); + + plot + .then(function(gd) { + return Plotly.toImage(gd, { format: "png" }); + }) + .then(function(url) { + expect(url.split("png")[0]).toBe("data:image/"); + // now do jpeg + return Plotly.toImage(gd, { format: "jpeg" }); + }) + .then(function(url) { + expect(url.split("jpeg")[0]).toBe("data:image/"); + // now do svg + return Plotly.toImage(gd, { format: "svg" }); + }) + .then(function(url) { + expect(url.split("svg")[0]).toBe("data:image/"); + done(); + }); + }); }); diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index 9bbea9678c6..c41036ed1f8 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -1,1074 +1,1122 @@ -var Plotly = require('@lib/index'); -var Filter = require('@lib/filter'); +var Plotly = require("@lib/index"); +var Filter = require("@lib/filter"); + +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); + +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var assertDims = require("../assets/assert_dims"); +var assertStyle = require("../assets/assert_style"); +var customMatchers = require("../assets/custom_matchers"); + +describe("filter transforms defaults:", function() { + var fullLayout = { _transformModules: [] }; + + var traceIn, traceOut; + + it("supplyTraceDefaults should coerce all attributes", function() { + traceIn = { x: [1, 2, 3], transforms: [{ type: "filter", value: 0 }] }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + + expect(traceOut.transforms).toEqual([ + { + type: "filter", + enabled: true, + operation: "=", + value: 0, + target: "x", + _module: Filter + } + ]); + }); + + it( + "supplyTraceDefaults should not coerce attributes if enabled: false", + function() { + traceIn = { + x: [1, 2, 3], + transforms: [{ enabled: false, type: "filter", value: 0 }] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + + expect(traceOut.transforms).toEqual([ + { type: "filter", enabled: false, _module: Filter } + ]); + } + ); + + it( + "supplyTraceDefaults should coerce *target* as a strict / noBlank string", + function() { + traceIn = { + x: [1, 2, 3], + transforms: [ + { type: "filter" }, + { type: "filter", target: 0 }, + { type: "filter", target: "" }, + { type: "filter", target: "marker.color" } + ] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + + expect(traceOut.transforms[0].target).toEqual("x"); + expect(traceOut.transforms[1].target).toEqual("x"); + expect(traceOut.transforms[2].target).toEqual("x"); + expect(traceOut.transforms[3].target).toEqual("marker.color"); + } + ); +}); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); +describe("filter transforms calc:", function() { + "use strict"; + function calcDatatoTrace(calcTrace) { + return calcTrace[0].trace; + } + + function _transform(data, layout) { + var gd = { data: data, layout: layout || {} }; + + Plots.supplyDefaults(gd); + Plots.doCalcdata(gd); + + return gd.calcdata.map(calcDatatoTrace); + } + + var base = { + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + ids: ["n0", "n1", "n2", "z", "p1", "p2", "p3"], + marker: { color: [0.1, 0.2, 0.3, 0.1, 0.2, 0.3, 0.4], size: 20 }, + transforms: [{ type: "filter" }] + }; + + it("filters should skip if *target* isn't present in trace", function() { + var out = _transform([ + Lib.extendDeep({}, base, { + transforms: [{ type: "filter", operation: ">", value: 0, target: "z" }] + }) + ]); + + expect(out[0].x).toEqual(base.x); + expect(out[0].y).toEqual(base.y); + }); + + it("filters should handle 3D *z* data", function() { + var out = _transform([ + Lib.extendDeep({}, base, { + type: "scatter3d", + z: [ + "2015-07-20", + "2016-08-01", + "2016-09-01", + "2016-10-21", + "2016-12-02" + ], + transforms: [ + { type: "filter", operation: ">", value: "2016-10-01", target: "z" } + ] + }) + ]); + + expect(out[0].x).toEqual([0, 1]); + expect(out[0].y).toEqual([1, 2]); + expect(out[0].z).toEqual(["2016-10-21", "2016-12-02"]); + }); + + it( + "should use the calendar from the target attribute if target is a string", + function() { + // this is the same data as in "filters should handle 3D *z* data" + // but with different calendars + var out = _transform([ + Lib.extendDeep({}, base, { + type: "scatter3d", + // the same array as above but in nanakshahi dates + z: [ + "0547-05-05", + "0548-05-17", + "0548-06-17", + "0548-08-07", + "0548-09-19" + ], + zcalendar: "nanakshahi", + transforms: [ + { + type: "filter", + operation: ">", + value: "5776-06-28", + valuecalendar: "hebrew", + target: "z", + // targetcalendar is ignored! + targetcalendar: "taiwan" + } + ] + }) + ]); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var assertDims = require('../assets/assert_dims'); -var assertStyle = require('../assets/assert_style'); -var customMatchers = require('../assets/custom_matchers'); + expect(out[0].x).toEqual([0, 1]); + expect(out[0].y).toEqual([1, 2]); + expect(out[0].z).toEqual(["0548-08-07", "0548-09-19"]); + } + ); + + it( + "should use targetcalendar anyway if there is no matching calendar attribute", + function() { + // this is the same data as in "filters should handle 3D *z* data" + // but with different calendars + var out = _transform([ + Lib.extendDeep({}, base, { + type: "scatter", + // the same array as above but in taiwanese dates + text: [ + "0104-07-20", + "0105-08-01", + "0105-09-01", + "0105-10-21", + "0105-12-02" + ], + transforms: [ + { + type: "filter", + operation: ">", + value: "5776-06-28", + valuecalendar: "hebrew", + target: "text", + targetcalendar: "taiwan" + } + ] + }) + ]); -describe('filter transforms defaults:', function() { + expect(out[0].x).toEqual([0, 1]); + expect(out[0].y).toEqual([1, 2]); + expect(out[0].text).toEqual(["0105-10-21", "0105-12-02"]); + } + ); + + it("should use targetcalendar if target is an array", function() { + // this is the same data as in "filters should handle 3D *z* data" + // but with different calendars + var out = _transform([ + Lib.extendDeep({}, base, { + type: "scatter3d", + // the same array as above but in nanakshahi dates + z: [ + "0547-05-05", + "0548-05-17", + "0548-06-17", + "0548-08-07", + "0548-09-19" + ], + zcalendar: "nanakshahi", + transforms: [ + { + type: "filter", + operation: ">", + value: "5776-06-28", + valuecalendar: "hebrew", + target: [ + "0104-07-20", + "0105-08-01", + "0105-09-01", + "0105-10-21", + "0105-12-02" + ], + targetcalendar: "taiwan" + } + ] + }) + ]); + + expect(out[0].x).toEqual([0, 1]); + expect(out[0].y).toEqual([1, 2]); + expect(out[0].z).toEqual(["0548-08-07", "0548-09-19"]); + }); + + it("filters should handle geographical *lon* data", function() { + var trace0 = { + type: "scattergeo", + lon: [-90, -40, 100, 120, 130], + lat: [-50, -40, 10, 20, 30], + transforms: [{ type: "filter", operation: ">", value: 0, target: "lon" }] + }; - var fullLayout = { _transformModules: [] }; + var trace1 = { + type: "scattermapbox", + lon: [-90, -40, 100, 120, 130], + lat: [-50, -40, 10, 20, 30], + transforms: [{ type: "filter", operation: "<", value: 0, target: "lat" }] + }; - var traceIn, traceOut; + var out = _transform([trace0, trace1]); + + expect(out[0].lon).toEqual([100, 120, 130]); + expect(out[0].lat).toEqual([10, 20, 30]); + + expect(out[1].lon).toEqual([-90, -40]); + expect(out[1].lat).toEqual([-50, -40]); + }); + + it("filters should handle nested attributes", function() { + var out = _transform([ + Lib.extendDeep({}, base, { + transforms: [ + { type: "filter", operation: ">", value: 0.2, target: "marker.color" } + ] + }) + ]); + + expect(out[0].x).toEqual([-2, 2, 3]); + expect(out[0].y).toEqual([3, 3, 1]); + expect(out[0].marker.color).toEqual([0.3, 0.3, 0.4]); + }); + + it("filters should skip if *enabled* is false", function() { + var out = _transform([ + Lib.extendDeep({}, base, { + transforms: [ + { + type: "filter", + enabled: false, + operation: ">", + value: 0, + target: "x" + } + ] + }) + ]); + + expect(out[0].x).toEqual(base.x); + expect(out[0].y).toEqual(base.y); + }); + + it("filters should chain as AND (case 1)", function() { + var out = _transform([ + Lib.extendDeep({}, base, { + transforms: [ + { type: "filter", operation: ">", value: 0, target: "x" }, + { type: "filter", operation: "<", value: 3, target: "x" } + ] + }) + ]); + + expect(out[0].x).toEqual([1, 2]); + expect(out[0].y).toEqual([2, 3]); + }); + + it("filters should chain as AND (case 2)", function() { + var out = _transform([ + Lib.extendDeep({}, base, { + transforms: [ + { type: "filter", operation: ">", value: 0, target: "x" }, + { + type: "filter", + enabled: false, + operation: ">", + value: 2, + target: "y" + }, + { type: "filter", operation: "<", value: 2, target: "y" } + ] + }) + ]); + + expect(out[0].x).toEqual([3]); + expect(out[0].y).toEqual([1]); + }); + + describe("filters should handle numeric values", function() { + var _base = Lib.extendDeep({}, base); + + function _assert(out, x, y, markerColor) { + expect(out[0].x).toEqual(x, "- x coords"); + expect(out[0].y).toEqual(y, "- y coords"); + expect(out[0].marker.color).toEqual( + markerColor, + "- marker.color arrayOk" + ); + expect(out[0].marker.size).toEqual(20, "- marker.size style"); + } - it('supplyTraceDefaults should coerce all attributes', function() { - traceIn = { - x: [1, 2, 3], - transforms: [{ - type: 'filter', - value: 0 - }] - }; + it("with operation *[]*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "[]", value: [-1, 1], target: "x" }] + }) + ]); - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + _assert(out, [-1, 0, 1], [2, 1, 2], [0.2, 0.1, 0.2]); + }); - expect(traceOut.transforms).toEqual([{ - type: 'filter', - enabled: true, - operation: '=', - value: 0, - target: 'x', - _module: Filter - }]); + it("with operation *[)*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "[)", value: [-1, 1], target: "x" }] + }) + ]); + + _assert(out, [-1, 0], [2, 1], [0.2, 0.1]); }); - it('supplyTraceDefaults should not coerce attributes if enabled: false', function() { - traceIn = { - x: [1, 2, 3], - transforms: [{ - enabled: false, - type: 'filter', - value: 0 - }] - }; + it("with operation *(]*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "(]", value: [-1, 1], target: "x" }] + }) + ]); - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + _assert(out, [0, 1], [1, 2], [0.1, 0.2]); + }); - expect(traceOut.transforms).toEqual([{ - type: 'filter', - enabled: false, - _module: Filter - }]); + it("with operation *()*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "()", value: [-1, 1], target: "x" }] + }) + ]); + + _assert(out, [0], [1], [0.1]); }); - it('supplyTraceDefaults should coerce *target* as a strict / noBlank string', function() { - traceIn = { - x: [1, 2, 3], - transforms: [{ - type: 'filter', - }, { - type: 'filter', - target: 0 - }, { - type: 'filter', - target: '' - }, { - type: 'filter', - target: 'marker.color' - }] - }; - - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); - - expect(traceOut.transforms[0].target).toEqual('x'); - expect(traceOut.transforms[1].target).toEqual('x'); - expect(traceOut.transforms[2].target).toEqual('x'); - expect(traceOut.transforms[3].target).toEqual('marker.color'); + it("with operation *)(*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: ")(", value: [-1, 1], target: "x" }] + }) + ]); + + _assert(out, [-2, -2, 2, 3], [1, 3, 3, 1], [0.1, 0.3, 0.3, 0.4]); + }); + + it("with operation *)[*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: ")[", value: [-1, 1], target: "x" }] + }) + ]); + + _assert(out, [-2, -2, 1, 2, 3], [1, 3, 2, 3, 1], [ + 0.1, + 0.3, + 0.2, + 0.3, + 0.4 + ]); }); -}); -describe('filter transforms calc:', function() { - 'use strict'; + it("with operation *](*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "](", value: [-1, 1], target: "x" }] + }) + ]); + + _assert(out, [-2, -1, -2, 2, 3], [1, 2, 3, 3, 1], [ + 0.1, + 0.2, + 0.3, + 0.3, + 0.4 + ]); + }); - function calcDatatoTrace(calcTrace) { - return calcTrace[0].trace; - } + it("with operation *][*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "][", value: [-1, 1], target: "x" }] + }) + ]); + + _assert(out, [-2, -1, -2, 1, 2, 3], [1, 2, 3, 2, 3, 1], [ + 0.1, + 0.2, + 0.3, + 0.2, + 0.3, + 0.4 + ]); + }); - function _transform(data, layout) { - var gd = { - data: data, - layout: layout || {} - }; + it("with operation *{}*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "{}", value: [-2, 0], target: "x" }] + }) + ]); - Plots.supplyDefaults(gd); - Plots.doCalcdata(gd); + _assert(out, [-2, -2, 0], [1, 3, 1], [0.1, 0.3, 0.1]); + }); - return gd.calcdata.map(calcDatatoTrace); - } + it("with operation *}{*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "}{", value: [-2, 0], target: "x" }] + }) + ]); - var base = { - x: [-2, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - ids: ['n0', 'n1', 'n2', 'z', 'p1', 'p2', 'p3'], - marker: { - color: [0.1, 0.2, 0.3, 0.1, 0.2, 0.3, 0.4], - size: 20 - }, - transforms: [{ type: 'filter' }] - }; + _assert(out, [-1, 1, 2, 3], [2, 2, 3, 1], [0.2, 0.2, 0.3, 0.4]); + }); - it('filters should skip if *target* isn\'t present in trace', function() { - var out = _transform([Lib.extendDeep({}, base, { - transforms: [{ - type: 'filter', - operation: '>', - value: 0, - target: 'z' - }] - })]); - - expect(out[0].x).toEqual(base.x); - expect(out[0].y).toEqual(base.y); + it("should honored set axis type", function() { + var out = _transform( + [ + Lib.extendDeep({}, _base, { + x: [1, 2, 3, 0, -1, -2, -3], + transforms: [{ operation: ">", value: -1, target: "x" }] + }) + ], + { xaxis: { type: "category" } } + ); + + _assert(out, [-2, -3], [3, 1], [0.3, 0.4]); }); + }); + + describe("filters should handle categories", function() { + var _base = { + x: ["a", "b", "c", "d"], + y: [1, 2, 3, 4], + marker: { color: "red", size: ["0", "1", "2", "0"] }, + transforms: [{ type: "filter" }] + }; + + function _assert(out, x, y, markerSize) { + expect(out[0].x).toEqual(x, "- x coords"); + expect(out[0].y).toEqual(y, "- y coords"); + expect(out[0].marker.size).toEqual(markerSize, "- marker.size arrayOk"); + expect(out[0].marker.color).toEqual("red", "- marker.color style"); + } - it('filters should handle 3D *z* data', function() { - var out = _transform([Lib.extendDeep({}, base, { - type: 'scatter3d', - z: ['2015-07-20', '2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], - transforms: [{ - type: 'filter', - operation: '>', - value: '2016-10-01', - target: 'z' - }] - })]); - - expect(out[0].x).toEqual([0, 1]); - expect(out[0].y).toEqual([1, 2]); - expect(out[0].z).toEqual(['2016-10-21', '2016-12-02']); + it("with operation *()*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "()", value: ["a", "c"], target: "x" }] + }) + ]); + + _assert(out, ["b"], [2], ["1"]); }); - it('should use the calendar from the target attribute if target is a string', function() { - // this is the same data as in "filters should handle 3D *z* data" - // but with different calendars - var out = _transform([Lib.extendDeep({}, base, { - type: 'scatter3d', - // the same array as above but in nanakshahi dates - z: ['0547-05-05', '0548-05-17', '0548-06-17', '0548-08-07', '0548-09-19'], - zcalendar: 'nanakshahi', - transforms: [{ - type: 'filter', - operation: '>', - value: '5776-06-28', - valuecalendar: 'hebrew', - target: 'z', - // targetcalendar is ignored! - targetcalendar: 'taiwan' - }] - })]); - - expect(out[0].x).toEqual([0, 1]); - expect(out[0].y).toEqual([1, 2]); - expect(out[0].z).toEqual(['0548-08-07', '0548-09-19']); + it("with operation *)(*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: ")(", value: ["a", "c"], target: "x" }] + }) + ]); + + _assert(out, ["d"], [4], ["0"]); }); - it('should use targetcalendar anyway if there is no matching calendar attribute', function() { - // this is the same data as in "filters should handle 3D *z* data" - // but with different calendars - var out = _transform([Lib.extendDeep({}, base, { - type: 'scatter', - // the same array as above but in taiwanese dates - text: ['0104-07-20', '0105-08-01', '0105-09-01', '0105-10-21', '0105-12-02'], - transforms: [{ - type: 'filter', - operation: '>', - value: '5776-06-28', - valuecalendar: 'hebrew', - target: 'text', - targetcalendar: 'taiwan' - }] - })]); - - expect(out[0].x).toEqual([0, 1]); - expect(out[0].y).toEqual([1, 2]); - expect(out[0].text).toEqual(['0105-10-21', '0105-12-02']); + it("with operation *{}*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "{}", value: ["b", "d"], target: "x" }] + }) + ]); + + _assert(out, ["b", "d"], [2, 4], ["1", "0"]); }); - it('should use targetcalendar if target is an array', function() { - // this is the same data as in "filters should handle 3D *z* data" - // but with different calendars - var out = _transform([Lib.extendDeep({}, base, { - type: 'scatter3d', - // the same array as above but in nanakshahi dates - z: ['0547-05-05', '0548-05-17', '0548-06-17', '0548-08-07', '0548-09-19'], - zcalendar: 'nanakshahi', - transforms: [{ - type: 'filter', - operation: '>', - value: '5776-06-28', - valuecalendar: 'hebrew', - target: ['0104-07-20', '0105-08-01', '0105-09-01', '0105-10-21', '0105-12-02'], - targetcalendar: 'taiwan' - }] - })]); - - expect(out[0].x).toEqual([0, 1]); - expect(out[0].y).toEqual([1, 2]); - expect(out[0].z).toEqual(['0548-08-07', '0548-09-19']); + it("with operation *}{*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "}{", value: ["b", "d"], target: "x" }] + }) + ]); + + _assert(out, ["a", "c"], [1, 3], ["0", "2"]); }); + }); + + describe("filters should handle dates", function() { + var _base = { + x: ["2015-07-20", "2016-08-01", "2016-09-01", "2016-10-21", "2016-12-02"], + y: [1, 2, 3, 1, 5], + marker: { line: { color: [0.1, 0.2, 0.3, 0.1, 0.2], width: 2.5 } }, + transforms: [{ type: "filter" }] + }; + + function _assert(out, x, y, markerLineColor) { + expect(out[0].x).toEqual(x, "- x coords"); + expect(out[0].y).toEqual(y, "- y coords"); + expect(out[0].marker.line.color).toEqual( + markerLineColor, + "- marker.line.color arrayOk" + ); + expect(out[0].marker.line.width).toEqual( + 2.5, + "- marker.line.width style" + ); + } + + it("with operation *=*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "=", value: ["2015-07-20"], target: "x" }] + }) + ]); - it('filters should handle geographical *lon* data', function() { - var trace0 = { - type: 'scattergeo', - lon: [-90, -40, 100, 120, 130], - lat: [-50, -40, 10, 20, 30], - transforms: [{ - type: 'filter', - operation: '>', - value: 0, - target: 'lon' - }] - }; - - var trace1 = { - type: 'scattermapbox', - lon: [-90, -40, 100, 120, 130], - lat: [-50, -40, 10, 20, 30], - transforms: [{ - type: 'filter', - operation: '<', - value: 0, - target: 'lat' - }] - }; - - var out = _transform([trace0, trace1]); - - expect(out[0].lon).toEqual([100, 120, 130]); - expect(out[0].lat).toEqual([10, 20, 30]); - - expect(out[1].lon).toEqual([-90, -40]); - expect(out[1].lat).toEqual([-50, -40]); + _assert(out, ["2015-07-20"], [1], [0.1]); }); - it('filters should handle nested attributes', function() { - var out = _transform([Lib.extendDeep({}, base, { - transforms: [{ - type: 'filter', - operation: '>', - value: 0.2, - target: 'marker.color' - }] - })]); - - expect(out[0].x).toEqual([-2, 2, 3]); - expect(out[0].y).toEqual([3, 3, 1]); - expect(out[0].marker.color).toEqual([0.3, 0.3, 0.4]); + it("with operation *<*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "<", value: "2016-01-01", target: "x" }] + }) + ]); + + _assert(out, ["2015-07-20"], [1], [0.1]); }); - it('filters should skip if *enabled* is false', function() { - var out = _transform([Lib.extendDeep({}, base, { - transforms: [{ - type: 'filter', - enabled: false, - operation: '>', - value: 0, - target: 'x' - }] - })]); - - expect(out[0].x).toEqual(base.x); - expect(out[0].y).toEqual(base.y); + it("with operation *>*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: ">=", value: "2016-08-01", target: "x" }] + }) + ]); + + _assert( + out, + ["2016-08-01", "2016-09-01", "2016-10-21", "2016-12-02"], + [2, 3, 1, 5], + [0.2, 0.3, 0.1, 0.2] + ); }); - it('filters should chain as AND (case 1)', function() { - var out = _transform([Lib.extendDeep({}, base, { - transforms: [{ - type: 'filter', - operation: '>', - value: 0, - target: 'x' - }, { - type: 'filter', - operation: '<', - value: 3, - target: 'x' - }] - })]); - - expect(out[0].x).toEqual([1, 2]); - expect(out[0].y).toEqual([2, 3]); + it("with operation *[]*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [ + { + operation: "[]", + value: ["2016-08-01", "2016-10-01"], + target: "x" + } + ] + }) + ]); + + _assert(out, ["2016-08-01", "2016-09-01"], [2, 3], [0.2, 0.3]); }); - it('filters should chain as AND (case 2)', function() { - var out = _transform([Lib.extendDeep({}, base, { - transforms: [{ - type: 'filter', - operation: '>', - value: 0, - target: 'x' - }, { - type: 'filter', - enabled: false, - operation: '>', - value: 2, - target: 'y' - }, { - type: 'filter', - operation: '<', - value: 2, - target: 'y' - }] - })]); - - expect(out[0].x).toEqual([3]); - expect(out[0].y).toEqual([1]); + it("with operation *)(*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [ + { + operation: ")(", + value: ["2016-08-01", "2016-10-01"], + target: "x" + } + ] + }) + ]); + + _assert(out, ["2015-07-20", "2016-10-21", "2016-12-02"], [1, 1, 5], [ + 0.1, + 0.1, + 0.2 + ]); }); - describe('filters should handle numeric values', function() { - var _base = Lib.extendDeep({}, base); + it("with operation *{}*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [{ operation: "{}", value: "2015-07-20", target: "x" }] + }) + ]); - function _assert(out, x, y, markerColor) { - expect(out[0].x).toEqual(x, '- x coords'); - expect(out[0].y).toEqual(y, '- y coords'); - expect(out[0].marker.color).toEqual(markerColor, '- marker.color arrayOk'); - expect(out[0].marker.size).toEqual(20, '- marker.size style'); - } + _assert(out, ["2015-07-20"], [1], [0.1]); + }); - it('with operation *[]*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '[]', - value: [-1, 1], - target: 'x' - }] - })]); - - _assert(out, - [-1, 0, 1], - [2, 1, 2], - [0.2, 0.1, 0.2] - ); - }); - - it('with operation *[)*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '[)', - value: [-1, 1], - target: 'x' - }] - })]); - - _assert(out, [-1, 0], [2, 1], [0.2, 0.1]); - }); - - it('with operation *(]*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '(]', - value: [-1, 1], - target: 'x' - }] - })]); - - _assert(out, [0, 1], [1, 2], [0.1, 0.2]); - }); - - it('with operation *()*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '()', - value: [-1, 1], - target: 'x' - }] - })]); - - _assert(out, [0], [1], [0.1]); - }); - - it('with operation *)(*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: ')(', - value: [-1, 1], - target: 'x' - }] - })]); - - _assert(out, - [-2, -2, 2, 3], - [1, 3, 3, 1], - [0.1, 0.3, 0.3, 0.4] - ); - }); - - it('with operation *)[*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: ')[', - value: [-1, 1], - target: 'x' - }] - })]); - - _assert(out, - [-2, -2, 1, 2, 3], - [1, 3, 2, 3, 1], - [0.1, 0.3, 0.2, 0.3, 0.4] - ); - }); - - it('with operation *](*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '](', - value: [-1, 1], - target: 'x' - }] - })]); - - _assert(out, - [-2, -1, -2, 2, 3], - [1, 2, 3, 3, 1], - [0.1, 0.2, 0.3, 0.3, 0.4] - ); - }); - - it('with operation *][*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '][', - value: [-1, 1], - target: 'x' - }] - })]); - - _assert(out, - [-2, -1, -2, 1, 2, 3], - [1, 2, 3, 2, 3, 1], - [0.1, 0.2, 0.3, 0.2, 0.3, 0.4] - ); - }); - - it('with operation *{}*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '{}', - value: [-2, 0], - target: 'x' - }] - })]); - - _assert(out, - [-2, -2, 0], - [1, 3, 1], - [0.1, 0.3, 0.1] - ); - }); - - it('with operation *}{*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '}{', - value: [-2, 0], - target: 'x' - }] - })]); - - _assert(out, - [-1, 1, 2, 3], - [2, 2, 3, 1], - [0.2, 0.2, 0.3, 0.4] - ); - }); - - it('should honored set axis type', function() { - var out = _transform([Lib.extendDeep({}, _base, { - x: [1, 2, 3, 0, -1, -2, -3], - transforms: [{ - operation: '>', - value: -1, - target: 'x' - }] - })], { - xaxis: { type: 'category' } - }); - - _assert(out, [-2, -3], [3, 1], [0.3, 0.4]); - }); + it("with operation *}{*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [ + { + operation: "}{", + value: ["2016-08-01", "2016-09-01", "2016-10-21", "2016-12-02"], + target: "x" + } + ] + }) + ]); + _assert(out, ["2015-07-20"], [1], [0.1]); }); + }); + + it("filters should handle ids", function() { + var out = _transform([ + Lib.extendDeep({}, base, { + transforms: [ + { operation: "{}", value: ["p1", "p2", "n1"], target: "ids" } + ] + }) + ]); + + expect(out[0].x).toEqual([-1, 1, 2]); + expect(out[0].y).toEqual([2, 2, 3]); + expect(out[0].ids).toEqual(["n1", "p1", "p2"]); + }); + + describe("filters should handle array *target* values", function() { + var _base = Lib.extendDeep({}, base); + + function _assert(out, x, y, markerColor) { + expect(out[0].x).toEqual(x, "- x coords"); + expect(out[0].y).toEqual(y, "- y coords"); + expect(out[0].marker.color).toEqual( + markerColor, + "- marker.color arrayOk" + ); + expect(out[0].marker.size).toEqual(20, "- marker.size style"); + } - describe('filters should handle categories', function() { - var _base = { - x: ['a', 'b', 'c', 'd'], - y: [1, 2, 3, 4], - marker: { - color: 'red', - size: ['0', '1', '2', '0'] - }, - transforms: [{ type: 'filter' }] - }; - - function _assert(out, x, y, markerSize) { - expect(out[0].x).toEqual(x, '- x coords'); - expect(out[0].y).toEqual(y, '- y coords'); - expect(out[0].marker.size).toEqual(markerSize, '- marker.size arrayOk'); - expect(out[0].marker.color).toEqual('red', '- marker.color style'); - } + it("with numeric items", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [ + { target: [1, 1, 0, 0, 1, 0, 1], operation: "{}", value: 0 } + ] + }) + ]); - it('with operation *()*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '()', - value: ['a', 'c'], - target: 'x' - }] - })]); - - _assert(out, ['b'], [2], ['1']); - }); - - it('with operation *)(*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: ')(', - value: ['a', 'c'], - target: 'x' - }] - })]); - - _assert(out, ['d'], [4], ['0']); - }); - - it('with operation *{}*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '{}', - value: ['b', 'd'], - target: 'x' - }] - })]); - - _assert(out, ['b', 'd'], [2, 4], ['1', '0']); - }); - - it('with operation *}{*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '}{', - value: ['b', 'd'], - target: 'x' - }] - })]); - - _assert(out, ['a', 'c'], [1, 3], ['0', '2']); - }); + _assert(out, [-2, 0, 2], [3, 1, 3], [0.3, 0.1, 0.3]); + expect(out[0].transforms[0].target).toEqual([0, 0, 0]); + }); + + it("with categorical items and *{}*", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [ + { + target: ["a", "a", "b", "b", "a", "b", "a"], + operation: "{}", + value: "b" + } + ] + }) + ]); + _assert(out, [-2, 0, 2], [3, 1, 3], [0.3, 0.1, 0.3]); + expect(out[0].transforms[0].target).toEqual(["b", "b", "b"]); }); - describe('filters should handle dates', function() { - var _base = { - x: ['2015-07-20', '2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], - y: [1, 2, 3, 1, 5], - marker: { - line: { - color: [0.1, 0.2, 0.3, 0.1, 0.2], - width: 2.5 - } - }, - transforms: [{ type: 'filter' }] - }; - - function _assert(out, x, y, markerLineColor) { - expect(out[0].x).toEqual(x, '- x coords'); - expect(out[0].y).toEqual(y, '- y coords'); - expect(out[0].marker.line.color).toEqual(markerLineColor, '- marker.line.color arrayOk'); - expect(out[0].marker.line.width).toEqual(2.5, '- marker.line.width style'); + it("with categorical items and *<* and *>=*", function() { + var out = _transform([ + { + x: [1, 2, 3], + y: [10, 20, 30], + transforms: [ + { + type: "filter", + operation: "<", + target: ["a", "b", "c"], + value: "c" + } + ] + }, + { + x: [1, 2, 3], + y: [30, 20, 10], + transforms: [ + { + type: "filter", + operation: ">=", + target: ["a", "b", "c"], + value: "b" + } + ] } + ]); - it('with operation *=*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '=', - value: ['2015-07-20'], - target: 'x' - }] - })]); - - _assert(out, ['2015-07-20'], [1], [0.1]); - }); - - it('with operation *<*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '<', - value: '2016-01-01', - target: 'x' - }] - })]); - - _assert(out, ['2015-07-20'], [1], [0.1]); - }); - - it('with operation *>*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '>=', - value: '2016-08-01', - target: 'x' - }] - })]); - - _assert(out, - ['2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], - [2, 3, 1, 5], - [0.2, 0.3, 0.1, 0.2] - ); - }); - - it('with operation *[]*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '[]', - value: ['2016-08-01', '2016-10-01'], - target: 'x' - }] - })]); - - _assert(out, ['2016-08-01', '2016-09-01'], [2, 3], [0.2, 0.3]); - }); - - it('with operation *)(*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: ')(', - value: ['2016-08-01', '2016-10-01'], - target: 'x' - }] - })]); - - _assert(out, ['2015-07-20', '2016-10-21', '2016-12-02'], [1, 1, 5], [0.1, 0.1, 0.2]); - }); - - it('with operation *{}*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '{}', - value: '2015-07-20', - target: 'x' - }] - })]); - - _assert(out, ['2015-07-20'], [1], [0.1]); - }); - - it('with operation *}{*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - operation: '}{', - value: ['2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], - target: 'x' - }] - })]); - - _assert(out, ['2015-07-20'], [1], [0.1]); - }); + expect(out[0].x).toEqual([1, 2]); + expect(out[0].y).toEqual([10, 20]); + expect(out[0].transforms[0].target).toEqual(["a", "b"]); + expect(out[1].x).toEqual([2, 3]); + expect(out[1].y).toEqual([20, 10]); + expect(out[1].transforms[0].target).toEqual(["b", "c"]); }); - it('filters should handle ids', function() { - var out = _transform([Lib.extendDeep({}, base, { - transforms: [{ - operation: '{}', - value: ['p1', 'p2', 'n1'], - target: 'ids' - }] - })]); - - expect(out[0].x).toEqual([-1, 1, 2]); - expect(out[0].y).toEqual([2, 2, 3]); - expect(out[0].ids).toEqual(['n1', 'p1', 'p2']); - }); + it("with categorical items and *[]*, *][*, *()* and *)(*", function() { + var out = _transform([ + { + x: [1, 2, 3], + y: [10, 20, 30], + transforms: [ + { + type: "filter", + operation: "[]", + target: ["a", "b", "c"], + value: ["a", "b"] + } + ] + }, + { + x: [1, 2, 3], + y: [10, 20, 30], + transforms: [ + { + type: "filter", + operation: "()", + target: ["a", "b", "c"], + value: ["a", "b"] + } + ] + }, + { + x: [1, 2, 3], + y: [30, 20, 10], + transforms: [ + { + type: "filter", + operation: "][", + target: ["a", "b", "c"], + value: ["a", "b"] + } + ] + }, + { + x: [1, 2, 3], + y: [30, 20, 10], + transforms: [ + { + type: "filter", + operation: ")(", + target: ["a", "b", "c"], + value: ["a", "b"] + } + ] + } + ]); - describe('filters should handle array *target* values', function() { - var _base = Lib.extendDeep({}, base); + expect(out[0].x).toEqual([1, 2]); + expect(out[0].y).toEqual([10, 20]); + expect(out[0].transforms[0].target).toEqual(["a", "b"]); - function _assert(out, x, y, markerColor) { - expect(out[0].x).toEqual(x, '- x coords'); - expect(out[0].y).toEqual(y, '- y coords'); - expect(out[0].marker.color).toEqual(markerColor, '- marker.color arrayOk'); - expect(out[0].marker.size).toEqual(20, '- marker.size style'); - } + expect(out[1].x).toEqual([]); + expect(out[1].y).toEqual([]); + expect(out[1].transforms[0].target).toEqual([]); - it('with numeric items', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - target: [1, 1, 0, 0, 1, 0, 1], - operation: '{}', - value: 0 - }] - })]); - - _assert(out, [-2, 0, 2], [3, 1, 3], [0.3, 0.1, 0.3]); - expect(out[0].transforms[0].target).toEqual([0, 0, 0]); - }); - - it('with categorical items and *{}*', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - target: ['a', 'a', 'b', 'b', 'a', 'b', 'a'], - operation: '{}', - value: 'b' - }] - })]); - - _assert(out, [-2, 0, 2], [3, 1, 3], [0.3, 0.1, 0.3]); - expect(out[0].transforms[0].target).toEqual(['b', 'b', 'b']); - }); - - it('with categorical items and *<* and *>=*', function() { - var out = _transform([{ - x: [1, 2, 3], - y: [10, 20, 30], - transforms: [{ - type: 'filter', - operation: '<', - target: ['a', 'b', 'c'], - value: 'c' - }] - }, { - x: [1, 2, 3], - y: [30, 20, 10], - transforms: [{ - type: 'filter', - operation: '>=', - target: ['a', 'b', 'c'], - value: 'b' - }] - }]); - - expect(out[0].x).toEqual([1, 2]); - expect(out[0].y).toEqual([10, 20]); - expect(out[0].transforms[0].target).toEqual(['a', 'b']); - - expect(out[1].x).toEqual([2, 3]); - expect(out[1].y).toEqual([20, 10]); - expect(out[1].transforms[0].target).toEqual(['b', 'c']); - }); - - it('with categorical items and *[]*, *][*, *()* and *)(*', function() { - var out = _transform([{ - x: [1, 2, 3], - y: [10, 20, 30], - transforms: [{ - type: 'filter', - operation: '[]', - target: ['a', 'b', 'c'], - value: ['a', 'b'] - }] - }, { - x: [1, 2, 3], - y: [10, 20, 30], - transforms: [{ - type: 'filter', - operation: '()', - target: ['a', 'b', 'c'], - value: ['a', 'b'] - }] - }, { - x: [1, 2, 3], - y: [30, 20, 10], - transforms: [{ - type: 'filter', - operation: '][', - target: ['a', 'b', 'c'], - value: ['a', 'b'] - }] - }, { - x: [1, 2, 3], - y: [30, 20, 10], - transforms: [{ - type: 'filter', - operation: ')(', - target: ['a', 'b', 'c'], - value: ['a', 'b'] - }] - }]); - - expect(out[0].x).toEqual([1, 2]); - expect(out[0].y).toEqual([10, 20]); - expect(out[0].transforms[0].target).toEqual(['a', 'b']); - - expect(out[1].x).toEqual([]); - expect(out[1].y).toEqual([]); - expect(out[1].transforms[0].target).toEqual([]); - - expect(out[2].x).toEqual([1, 2, 3]); - expect(out[2].y).toEqual([30, 20, 10]); - expect(out[2].transforms[0].target).toEqual(['a', 'b', 'c']); - - expect(out[3].x).toEqual([3]); - expect(out[3].y).toEqual([10]); - expect(out[3].transforms[0].target).toEqual(['c']); - }); - - it('with dates items', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - target: ['2015-07-20', '2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], - operation: '<', - value: '2016-01-01' - }] - })]); - - _assert(out, [-2], [1], [0.1]); - expect(out[0].transforms[0].target).toEqual(['2015-07-20']); - }); - - it('with multiple transforms (dates) ', function() { - var out = _transform([Lib.extendDeep({}, _base, { - transforms: [{ - target: ['2015-07-20', '2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], - operation: '>', - value: '2016-01-01' - }, { - type: 'filter', - target: ['2015-07-20', '2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], - operation: '<', - value: '2016-09-01' - }] - })]); - - _assert(out, [-1], [2], [0.2]); - expect(out[0].transforms[0].target).toEqual(['2016-08-01']); - }); + expect(out[2].x).toEqual([1, 2, 3]); + expect(out[2].y).toEqual([30, 20, 10]); + expect(out[2].transforms[0].target).toEqual(["a", "b", "c"]); + + expect(out[3].x).toEqual([3]); + expect(out[3].y).toEqual([10]); + expect(out[3].transforms[0].target).toEqual(["c"]); }); -}); -describe('filter transforms interactions', function() { - 'use strict'; + it("with dates items", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [ + { + target: [ + "2015-07-20", + "2016-08-01", + "2016-09-01", + "2016-10-21", + "2016-12-02" + ], + operation: "<", + value: "2016-01-01" + } + ] + }) + ]); - beforeAll(function() { - jasmine.addMatchers(customMatchers); + _assert(out, [-2], [1], [0.1]); + expect(out[0].transforms[0].target).toEqual(["2015-07-20"]); }); - var mockData0 = [{ - x: [-2, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - text: ['a', 'b', 'c', 'd', 'e', 'f', 'g'], - transforms: [{ - type: 'filter', - operation: '>' - }] - }]; - - var mockData1 = [Lib.extendDeep({}, mockData0[0]), { - x: [20, 11, 12, 0, 1, 2, 3], - y: [1, 2, 3, 2, 5, 2, 0], - text: ['A', 'B', 'C', 'D', 'E', 'F', 'G'], - transforms: [{ - type: 'filter', - operation: '<', - value: 10 - }] - }]; - - afterEach(destroyGraphDiv); - - it('Plotly.plot should plot the transform trace', function(done) { - var data = Lib.extendDeep([], mockData0); - - Plotly.plot(createGraphDiv(), data).then(function(gd) { - assertDims([3]); - - var uid = data[0].uid; - expect(gd._fullData[0].uid).toEqual(uid + '0'); - - done(); - }); + it("with multiple transforms (dates) ", function() { + var out = _transform([ + Lib.extendDeep({}, _base, { + transforms: [ + { + target: [ + "2015-07-20", + "2016-08-01", + "2016-09-01", + "2016-10-21", + "2016-12-02" + ], + operation: ">", + value: "2016-01-01" + }, + { + type: "filter", + target: [ + "2015-07-20", + "2016-08-01", + "2016-09-01", + "2016-10-21", + "2016-12-02" + ], + operation: "<", + value: "2016-09-01" + } + ] + }) + ]); + + _assert(out, [-1], [2], [0.2]); + expect(out[0].transforms[0].target).toEqual(["2016-08-01"]); }); + }); +}); - it('Plotly.restyle should work', function(done) { - var data = Lib.extendDeep([], mockData0); - data[0].marker = { color: 'red' }; +describe("filter transforms interactions", function() { + "use strict"; + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + var mockData0 = [ + { + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + text: ["a", "b", "c", "d", "e", "f", "g"], + transforms: [{ type: "filter", operation: ">" }] + } + ]; + + var mockData1 = [ + Lib.extendDeep({}, mockData0[0]), + { + x: [20, 11, 12, 0, 1, 2, 3], + y: [1, 2, 3, 2, 5, 2, 0], + text: ["A", "B", "C", "D", "E", "F", "G"], + transforms: [{ type: "filter", operation: "<", value: 10 }] + } + ]; - var gd = createGraphDiv(); - var dims = [3]; + afterEach(destroyGraphDiv); - var uid; - function assertUid(gd) { - expect(gd._fullData[0].uid) - .toEqual(uid + '0', 'should preserve uid on restyle'); - } + it("Plotly.plot should plot the transform trace", function(done) { + var data = Lib.extendDeep([], mockData0); - Plotly.plot(gd, data).then(function() { - uid = gd.data[0].uid; + Plotly.plot(createGraphDiv(), data).then(function(gd) { + assertDims([3]); - expect(gd._fullData[0].marker.color).toEqual('red'); - assertUid(gd); - assertStyle(dims, ['rgb(255, 0, 0)'], [1]); + var uid = data[0].uid; + expect(gd._fullData[0].uid).toEqual(uid + "0"); - expect(gd._fullLayout.xaxis.range).toBeCloseToArray([0.87, 3.13]); - expect(gd._fullLayout.yaxis.range).toBeCloseToArray([0.85, 3.15]); + done(); + }); + }); - return Plotly.restyle(gd, 'marker.color', 'blue'); - }).then(function() { - expect(gd._fullData[0].marker.color).toEqual('blue'); - assertUid(gd); - assertStyle(dims, ['rgb(0, 0, 255)'], [1]); + it("Plotly.restyle should work", function(done) { + var data = Lib.extendDeep([], mockData0); + data[0].marker = { color: "red" }; - return Plotly.restyle(gd, 'marker.color', 'red'); - }).then(function() { - expect(gd._fullData[0].marker.color).toEqual('red'); - assertUid(gd); - assertStyle(dims, ['rgb(255, 0, 0)'], [1]); + var gd = createGraphDiv(); + var dims = [3]; - return Plotly.restyle(gd, 'transforms[0].value', 2.5); - }).then(function() { - assertUid(gd); - assertStyle([1], ['rgb(255, 0, 0)'], [1]); + var uid; + function assertUid(gd) { + expect(gd._fullData[0].uid).toEqual( + uid + "0", + "should preserve uid on restyle" + ); + } - expect(gd._fullLayout.xaxis.range).toBeCloseToArray([2, 4]); - expect(gd._fullLayout.yaxis.range).toBeCloseToArray([0, 2]); + Plotly.plot(gd, data) + .then(function() { + uid = gd.data[0].uid; - done(); - }); - }); + expect(gd._fullData[0].marker.color).toEqual("red"); + assertUid(gd); + assertStyle(dims, ["rgb(255, 0, 0)"], [1]); - it('Plotly.extendTraces should work', function(done) { - var data = Lib.extendDeep([], mockData0); + expect(gd._fullLayout.xaxis.range).toBeCloseToArray([0.87, 3.13]); + expect(gd._fullLayout.yaxis.range).toBeCloseToArray([0.85, 3.15]); - var gd = createGraphDiv(); + return Plotly.restyle(gd, "marker.color", "blue"); + }) + .then(function() { + expect(gd._fullData[0].marker.color).toEqual("blue"); + assertUid(gd); + assertStyle(dims, ["rgb(0, 0, 255)"], [1]); - Plotly.plot(gd, data).then(function() { - expect(gd.data[0].x.length).toEqual(7); - expect(gd._fullData[0].x.length).toEqual(3); + return Plotly.restyle(gd, "marker.color", "red"); + }) + .then(function() { + expect(gd._fullData[0].marker.color).toEqual("red"); + assertUid(gd); + assertStyle(dims, ["rgb(255, 0, 0)"], [1]); - assertDims([3]); + return Plotly.restyle(gd, "transforms[0].value", 2.5); + }) + .then(function() { + assertUid(gd); + assertStyle([1], ["rgb(255, 0, 0)"], [1]); - return Plotly.extendTraces(gd, { - x: [ [-3, 4, 5] ], - y: [ [1, -2, 3] ] - }, [0]); - }).then(function() { - expect(gd.data[0].x.length).toEqual(10); - expect(gd._fullData[0].x.length).toEqual(5); + expect(gd._fullLayout.xaxis.range).toBeCloseToArray([2, 4]); + expect(gd._fullLayout.yaxis.range).toBeCloseToArray([0, 2]); - assertDims([5]); + done(); + }); + }); - done(); - }); - }); + it("Plotly.extendTraces should work", function(done) { + var data = Lib.extendDeep([], mockData0); - it('Plotly.deleteTraces should work', function(done) { - var data = Lib.extendDeep([], mockData1); + var gd = createGraphDiv(); - var gd = createGraphDiv(); + Plotly.plot(gd, data) + .then(function() { + expect(gd.data[0].x.length).toEqual(7); + expect(gd._fullData[0].x.length).toEqual(3); - Plotly.plot(gd, data).then(function() { - assertDims([3, 4]); + assertDims([3]); - return Plotly.deleteTraces(gd, [1]); - }).then(function() { - assertDims([3]); + return Plotly.extendTraces(gd, { x: [[-3, 4, 5]], y: [[1, -2, 3]] }, [ + 0 + ]); + }) + .then(function() { + expect(gd.data[0].x.length).toEqual(10); + expect(gd._fullData[0].x.length).toEqual(5); - return Plotly.deleteTraces(gd, [0]); - }).then(function() { - assertDims([]); + assertDims([5]); - done(); - }); + done(); + }); + }); - }); + it("Plotly.deleteTraces should work", function(done) { + var data = Lib.extendDeep([], mockData1); - it('toggling trace visibility should work', function(done) { - var data = Lib.extendDeep([], mockData1); + var gd = createGraphDiv(); - var gd = createGraphDiv(); + Plotly.plot(gd, data) + .then(function() { + assertDims([3, 4]); - Plotly.plot(gd, data).then(function() { - assertDims([3, 4]); + return Plotly.deleteTraces(gd, [1]); + }) + .then(function() { + assertDims([3]); - return Plotly.restyle(gd, 'visible', 'legendonly', [1]); - }).then(function() { - assertDims([3]); + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + assertDims([]); - return Plotly.restyle(gd, 'visible', false, [0]); - }).then(function() { - assertDims([]); + done(); + }); + }); - return Plotly.restyle(gd, 'visible', [true, true], [0, 1]); - }).then(function() { - assertDims([3, 4]); + it("toggling trace visibility should work", function(done) { + var data = Lib.extendDeep([], mockData1); - done(); - }); - }); + var gd = createGraphDiv(); - it('zooming in/out should not change filtered data', function(done) { - var data = Lib.extendDeep([], mockData1); + Plotly.plot(gd, data) + .then(function() { + assertDims([3, 4]); - var gd = createGraphDiv(); + return Plotly.restyle(gd, "visible", "legendonly", [1]); + }) + .then(function() { + assertDims([3]); - function getTx(p) { return p.tx; } + return Plotly.restyle(gd, "visible", false, [0]); + }) + .then(function() { + assertDims([]); - Plotly.plot(gd, data).then(function() { - expect(gd.calcdata[0].map(getTx)).toEqual(['e', 'f', 'g']); - expect(gd.calcdata[1].map(getTx)).toEqual(['D', 'E', 'F', 'G']); + return Plotly.restyle(gd, "visible", [true, true], [0, 1]); + }) + .then(function() { + assertDims([3, 4]); - return Plotly.relayout(gd, 'xaxis.range', [-1, 1]); - }) - .then(function() { - expect(gd.calcdata[0].map(getTx)).toEqual(['e', 'f', 'g']); - expect(gd.calcdata[1].map(getTx)).toEqual(['D', 'E', 'F', 'G']); + done(); + }); + }); - return Plotly.relayout(gd, 'xaxis.autorange', true); - }) - .then(function() { - expect(gd.calcdata[0].map(getTx)).toEqual(['e', 'f', 'g']); - expect(gd.calcdata[1].map(getTx)).toEqual(['D', 'E', 'F', 'G']); - }) - .then(done); - }); + it("zooming in/out should not change filtered data", function(done) { + var data = Lib.extendDeep([], mockData1); - it('should update axis categories', function(done) { - var data = [{ - type: 'bar', - x: ['a', 'b', 'c', 'd', 'e', 'f', 'g'], - y: [1, 10, 100, 25, 50, -25, 100], - transforms: [{ - type: 'filter', - operation: '<', - value: 10, - target: [1, 10, 100, 25, 50, -25, 100] - }] - }]; - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - expect(gd._fullLayout.xaxis._categories).toEqual(['a', 'f']); - expect(gd._fullLayout.yaxis._categories).toEqual([]); - - return Plotly.addTraces(gd, [{ - type: 'bar', - x: ['h', 'i'], - y: [2, 1], - transforms: [{ - type: 'filter', - operation: '=', - value: 'i' - }] - }]); - }) - .then(function() { - expect(gd._fullLayout.xaxis._categories).toEqual(['a', 'f', 'i']); - expect(gd._fullLayout.yaxis._categories).toEqual([]); + var gd = createGraphDiv(); - return Plotly.deleteTraces(gd, [0]); - }) - .then(function() { - expect(gd._fullLayout.xaxis._categories).toEqual(['i']); - expect(gd._fullLayout.yaxis._categories).toEqual([]); - }) - .then(done); - }); + function getTx(p) { + return p.tx; + } + Plotly.plot(gd, data) + .then(function() { + expect(gd.calcdata[0].map(getTx)).toEqual(["e", "f", "g"]); + expect(gd.calcdata[1].map(getTx)).toEqual(["D", "E", "F", "G"]); + + return Plotly.relayout(gd, "xaxis.range", [-1, 1]); + }) + .then(function() { + expect(gd.calcdata[0].map(getTx)).toEqual(["e", "f", "g"]); + expect(gd.calcdata[1].map(getTx)).toEqual(["D", "E", "F", "G"]); + + return Plotly.relayout(gd, "xaxis.autorange", true); + }) + .then(function() { + expect(gd.calcdata[0].map(getTx)).toEqual(["e", "f", "g"]); + expect(gd.calcdata[1].map(getTx)).toEqual(["D", "E", "F", "G"]); + }) + .then(done); + }); + + it("should update axis categories", function(done) { + var data = [ + { + type: "bar", + x: ["a", "b", "c", "d", "e", "f", "g"], + y: [1, 10, 100, 25, 50, -25, 100], + transforms: [ + { + type: "filter", + operation: "<", + value: 10, + target: [1, 10, 100, 25, 50, -25, 100] + } + ] + } + ]; + + var gd = createGraphDiv(); + + Plotly.plot(gd, data) + .then(function() { + expect(gd._fullLayout.xaxis._categories).toEqual(["a", "f"]); + expect(gd._fullLayout.yaxis._categories).toEqual([]); + + return Plotly.addTraces(gd, [ + { + type: "bar", + x: ["h", "i"], + y: [2, 1], + transforms: [{ type: "filter", operation: "=", value: "i" }] + } + ]); + }) + .then(function() { + expect(gd._fullLayout.xaxis._categories).toEqual(["a", "f", "i"]); + expect(gd._fullLayout.yaxis._categories).toEqual([]); + + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + expect(gd._fullLayout.xaxis._categories).toEqual(["i"]); + expect(gd._fullLayout.yaxis._categories).toEqual([]); + }) + .then(done); + }); }); diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index bb2ea0f607e..02ecff850a3 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -1,586 +1,708 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); - -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var assertDims = require('../assets/assert_dims'); -var assertStyle = require('../assets/assert_style'); - - -describe('groupby', function() { - - describe('one-to-many transforms:', function() { - 'use strict'; - - var mockData0 = [{ - mode: 'markers', - x: [1, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - transforms: [{ - type: 'groupby', - groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } - }] - }]; - - var mockData1 = [Lib.extendDeep({}, mockData0[0]), { - mode: 'markers', - x: [20, 11, 12, 0, 1, 2, 3], - y: [1, 2, 3, 2, 5, 2, 0], - transforms: [{ - type: 'groupby', - groups: ['b', 'a', 'b', 'b', 'b', 'a', 'a'], - style: { a: {marker: {color: 'green'}}, b: {marker: {color: 'black'}} } - }] - }]; - - afterEach(destroyGraphDiv); - - it('Plotly.plot should plot the transform traces', function(done) { - var data = Lib.extendDeep([], mockData0); - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - expect(gd.data.length).toEqual(1); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - - expect(gd._fullData.length).toEqual(2); - expect(gd._fullData[0].x).toEqual([1, -1, 0, 3]); - expect(gd._fullData[0].y).toEqual([1, 2, 1, 1]); - expect(gd._fullData[1].x).toEqual([-2, 1, 2]); - expect(gd._fullData[1].y).toEqual([3, 2, 3]); - - assertDims([4, 3]); - - done(); - }); - }); - - it('Plotly.restyle should work', function(done) { - var data = Lib.extendDeep([], mockData0); - data[0].marker = { size: 20 }; - - var gd = createGraphDiv(); - var dims = [4, 3]; - - Plotly.plot(gd, data).then(function() { - assertStyle(dims, - ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], - [1, 1] - ); - - return Plotly.restyle(gd, 'marker.opacity', 0.4); - }).then(function() { - assertStyle(dims, - ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], - [0.4, 0.4] - ); - - expect(gd._fullData[0].marker.opacity).toEqual(0.4); - expect(gd._fullData[1].marker.opacity).toEqual(0.4); - - return Plotly.restyle(gd, 'marker.opacity', 1); - }).then(function() { - assertStyle(dims, - ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], - [1, 1] - ); - - expect(gd._fullData[0].marker.opacity).toEqual(1); - expect(gd._fullData[1].marker.opacity).toEqual(1); - - return Plotly.restyle(gd, { - 'transforms[0].style': { a: {marker: {color: 'green'}}, b: {marker: {color: 'red'}} }, - 'marker.opacity': 0.4 - }); - }).then(function() { - assertStyle(dims, - ['rgb(0, 128, 0)', 'rgb(255, 0, 0)'], - [0.4, 0.4] - ); - - done(); - }); - }); - - it('Plotly.extendTraces should work', function(done) { - var data = Lib.extendDeep([], mockData0); - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - expect(gd.data[0].x.length).toEqual(7); - expect(gd._fullData[0].x.length).toEqual(4); - expect(gd._fullData[1].x.length).toEqual(3); - - assertDims([4, 3]); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); + +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var assertDims = require("../assets/assert_dims"); +var assertStyle = require("../assets/assert_style"); + +describe("groupby", function() { + describe("one-to-many transforms:", function() { + "use strict"; + var mockData0 = [ + { + mode: "markers", + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [ + { + type: "groupby", + groups: ["a", "a", "b", "a", "b", "b", "a"], + style: { + a: { marker: { color: "red" } }, + b: { marker: { color: "blue" } } + } + } + ] + } + ]; + + var mockData1 = [ + Lib.extendDeep({}, mockData0[0]), + { + mode: "markers", + x: [20, 11, 12, 0, 1, 2, 3], + y: [1, 2, 3, 2, 5, 2, 0], + transforms: [ + { + type: "groupby", + groups: ["b", "a", "b", "b", "b", "a", "a"], + style: { + a: { marker: { color: "green" } }, + b: { marker: { color: "black" } } + } + } + ] + } + ]; + + afterEach(destroyGraphDiv); + + it("Plotly.plot should plot the transform traces", function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + + expect(gd._fullData.length).toEqual(2); + expect(gd._fullData[0].x).toEqual([1, -1, 0, 3]); + expect(gd._fullData[0].y).toEqual([1, 2, 1, 1]); + expect(gd._fullData[1].x).toEqual([-2, 1, 2]); + expect(gd._fullData[1].y).toEqual([3, 2, 3]); + + assertDims([4, 3]); + + done(); + }); + }); - return Plotly.extendTraces(gd, { - x: [ [-3, 4, 5] ], - y: [ [1, -2, 3] ], - 'transforms[0].groups': [ ['b', 'a', 'b'] ] - }, [0]); - }).then(function() { - expect(gd.data[0].x.length).toEqual(10); - expect(gd._fullData[0].x.length).toEqual(5); - expect(gd._fullData[1].x.length).toEqual(5); + it("Plotly.restyle should work", function(done) { + var data = Lib.extendDeep([], mockData0); + data[0].marker = { size: 20 }; - assertDims([5, 5]); + var gd = createGraphDiv(); + var dims = [4, 3]; - done(); - }); - }); + Plotly.plot(gd, data) + .then(function() { + assertStyle(dims, ["rgb(255, 0, 0)", "rgb(0, 0, 255)"], [1, 1]); - it('Plotly.deleteTraces should work', function(done) { - var data = Lib.extendDeep([], mockData1); + return Plotly.restyle(gd, "marker.opacity", 0.4); + }) + .then(function() { + assertStyle(dims, ["rgb(255, 0, 0)", "rgb(0, 0, 255)"], [0.4, 0.4]); - var gd = createGraphDiv(); + expect(gd._fullData[0].marker.opacity).toEqual(0.4); + expect(gd._fullData[1].marker.opacity).toEqual(0.4); - Plotly.plot(gd, data).then(function() { - assertDims([4, 3, 4, 3]); + return Plotly.restyle(gd, "marker.opacity", 1); + }) + .then(function() { + assertStyle(dims, ["rgb(255, 0, 0)", "rgb(0, 0, 255)"], [1, 1]); - return Plotly.deleteTraces(gd, [1]); - }).then(function() { - assertDims([4, 3]); + expect(gd._fullData[0].marker.opacity).toEqual(1); + expect(gd._fullData[1].marker.opacity).toEqual(1); - return Plotly.deleteTraces(gd, [0]); - }).then(function() { - assertDims([]); + return Plotly.restyle(gd, { + "transforms[0].style": { + a: { marker: { color: "green" } }, + b: { marker: { color: "red" } } + }, + "marker.opacity": 0.4 + }); + }) + .then(function() { + assertStyle(dims, ["rgb(0, 128, 0)", "rgb(255, 0, 0)"], [0.4, 0.4]); - done(); - }); + done(); }); + }); - it('toggling trace visibility should work', function(done) { - var data = Lib.extendDeep([], mockData1); + it("Plotly.extendTraces should work", function(done) { + var data = Lib.extendDeep([], mockData0); - var gd = createGraphDiv(); + var gd = createGraphDiv(); - Plotly.plot(gd, data).then(function() { - assertDims([4, 3, 4, 3]); + Plotly.plot(gd, data) + .then(function() { + expect(gd.data[0].x.length).toEqual(7); + expect(gd._fullData[0].x.length).toEqual(4); + expect(gd._fullData[1].x.length).toEqual(3); - return Plotly.restyle(gd, 'visible', 'legendonly', [1]); - }).then(function() { - assertDims([4, 3]); + assertDims([4, 3]); - return Plotly.restyle(gd, 'visible', false, [0]); - }).then(function() { - assertDims([]); + return Plotly.extendTraces( + gd, + { + x: [[-3, 4, 5]], + y: [[1, -2, 3]], + "transforms[0].groups": [["b", "a", "b"]] + }, + [0] + ); + }) + .then(function() { + expect(gd.data[0].x.length).toEqual(10); + expect(gd._fullData[0].x.length).toEqual(5); + expect(gd._fullData[1].x.length).toEqual(5); - return Plotly.restyle(gd, 'visible', [true, true], [0, 1]); - }).then(function() { - assertDims([4, 3, 4, 3]); + assertDims([5, 5]); - done(); - }); + done(); }); - }); - // these tests can be shortened, once the meaning of edge cases gets clarified - describe('symmetry/degeneracy testing of one-to-many transforms on arbitrary arrays where there is no grouping (implicit 1):', function() { - 'use strict'; - - var mockData = [{ - mode: 'markers', - x: [1, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - - // everything is present: - transforms: [{ - type: 'groupby', - groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } - }] - }]; - - var mockData0 = [{ - mode: 'markers', - x: [1, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - - // groups, styles not present - transforms: [{ - type: 'groupby' - // groups not present - // styles not present - }] - }]; - - // transform attribute with empty list - var mockData1 = [{ - mode: 'markers', - x: [1, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - - // transforms is present but there are no items in it - transforms: [ /* list is empty */ ] - }]; - - // transform attribute with null value - var mockData2 = [{ - mode: 'markers', - x: [1, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - transforms: null - }]; - - // no transform is present at all - var mockData3 = [{ - mode: 'markers', - x: [1, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1] - }]; - - afterEach(destroyGraphDiv); - - it('Plotly.plot should plot the transform traces', function(done) { - var data = Lib.extendDeep([], mockData); - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - expect(gd.data.length).toEqual(1); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - - expect(gd._fullData.length).toEqual(2); // two groups - expect(gd._fullData[0].x).toEqual([1, -1, 0, 3]); - expect(gd._fullData[0].y).toEqual([1, 2, 1, 1]); - expect(gd._fullData[1].x).toEqual([-2, 1, 2]); - expect(gd._fullData[1].y).toEqual([3, 2, 3]); - - assertDims([4, 3]); - - done(); - }); - }); + it("Plotly.deleteTraces should work", function(done) { + var data = Lib.extendDeep([], mockData1); - it('Plotly.plot should plot the transform traces', function(done) { - var data = Lib.extendDeep([], mockData0); + var gd = createGraphDiv(); - var gd = createGraphDiv(); + Plotly.plot(gd, data) + .then(function() { + assertDims([4, 3, 4, 3]); - Plotly.plot(gd, data).then(function() { - expect(gd.data.length).toEqual(1); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + return Plotly.deleteTraces(gd, [1]); + }) + .then(function() { + assertDims([4, 3]); - expect(gd._fullData.length).toEqual(1); - assertDims([7]); + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + assertDims([]); - done(); - }); - }); - - it('Plotly.plot should plot the transform traces', function(done) { - var data = Lib.extendDeep([], mockData1); - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - expect(gd.data.length).toEqual(1); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - - expect(gd._fullData.length).toEqual(1); - expect(gd._fullData[0].x).toEqual([ 1, -1, -2, 0, 1, 2, 3 ]); - expect(gd._fullData[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - - assertDims([7]); - - done(); - }); + done(); }); + }); - it('Plotly.plot should plot the transform traces', function(done) { - var data = Lib.extendDeep([], mockData2); + it("toggling trace visibility should work", function(done) { + var data = Lib.extendDeep([], mockData1); - var gd = createGraphDiv(); + var gd = createGraphDiv(); - Plotly.plot(gd, data).then(function() { - expect(gd.data.length).toEqual(1); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + Plotly.plot(gd, data) + .then(function() { + assertDims([4, 3, 4, 3]); - expect(gd._fullData.length).toEqual(1); + return Plotly.restyle(gd, "visible", "legendonly", [1]); + }) + .then(function() { + assertDims([4, 3]); - expect(gd._fullData[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd._fullData[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + return Plotly.restyle(gd, "visible", false, [0]); + }) + .then(function() { + assertDims([]); - assertDims([7]); + return Plotly.restyle(gd, "visible", [true, true], [0, 1]); + }) + .then(function() { + assertDims([4, 3, 4, 3]); - done(); - }); + done(); }); + }); + }); + + // these tests can be shortened, once the meaning of edge cases gets clarified + describe( + "symmetry/degeneracy testing of one-to-many transforms on arbitrary arrays where there is no grouping (implicit 1):", + function() { + "use strict"; + var mockData = [ + { + mode: "markers", + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + // everything is present: + transforms: [ + { + type: "groupby", + groups: ["a", "a", "b", "a", "b", "b", "a"], + style: { + a: { marker: { color: "red" } }, + b: { marker: { color: "blue" } } + } + } + ] + } + ]; + + var mockData0 = [ + { + mode: "markers", + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + // groups, styles not present + transforms: [ + { + // groups not present + // styles not present + type: "groupby" + } + ] + } + ]; + + // transform attribute with empty list + var mockData1 = [ + { + mode: "markers", + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + // transforms is present but there are no items in it + transforms: [] + } + ]; + + // transform attribute with null value + var mockData2 = [ + { + mode: "markers", + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: null + } + ]; + + // no transform is present at all + var mockData3 = [ + { + mode: "markers", + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1] + } + ]; - it('Plotly.plot should plot the transform traces', function(done) { - var data = Lib.extendDeep([], mockData3); + afterEach(destroyGraphDiv); - var gd = createGraphDiv(); + it("Plotly.plot should plot the transform traces", function(done) { + var data = Lib.extendDeep([], mockData); - Plotly.plot(gd, data).then(function() { - expect(gd.data.length).toEqual(1); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + var gd = createGraphDiv(); - expect(gd._fullData.length).toEqual(1); + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - expect(gd._fullData[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd._fullData[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + expect(gd._fullData.length).toEqual(2); + // two groups + expect(gd._fullData[0].x).toEqual([1, -1, 0, 3]); + expect(gd._fullData[0].y).toEqual([1, 2, 1, 1]); + expect(gd._fullData[1].x).toEqual([-2, 1, 2]); + expect(gd._fullData[1].y).toEqual([3, 2, 3]); - assertDims([7]); + assertDims([4, 3]); - done(); - }); + done(); }); - }); - - describe('grouping with basic, heterogenous and overridden attributes', function() { - 'use strict'; + }); - afterEach(destroyGraphDiv); + it("Plotly.plot should plot the transform traces", function(done) { + var data = Lib.extendDeep([], mockData0); - function test(mockData) { + var gd = createGraphDiv(); - return function(done) { - var data = Lib.extendDeep([], mockData); + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - var gd = createGraphDiv(); + expect(gd._fullData.length).toEqual(1); + assertDims([7]); - Plotly.plot(gd, data).then(function() { - - expect(gd.data.length).toEqual(1); - expect(gd.data[0].ids).toEqual(['q', 'w', 'r', 't', 'y', 'u', 'i']); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([0, 1, 2, 3, 5, 4, 6]); - expect(gd.data[0].marker.line.width).toEqual([4, 2, 4, 2, 2, 3, 3]); + done(); + }); + }); - expect(gd._fullData.length).toEqual(2); + it("Plotly.plot should plot the transform traces", function(done) { + var data = Lib.extendDeep([], mockData1); - expect(gd._fullData[0].ids).toEqual(['q', 'w', 't', 'i']); - expect(gd._fullData[0].x).toEqual([1, -1, 0, 3]); - expect(gd._fullData[0].y).toEqual([0, 1, 3, 6]); - expect(gd._fullData[0].marker.line.width).toEqual([4, 2, 2, 3]); + var gd = createGraphDiv(); - expect(gd._fullData[1].ids).toEqual(['r', 'y', 'u']); - expect(gd._fullData[1].x).toEqual([-2, 1, 2]); - expect(gd._fullData[1].y).toEqual([2, 5, 4]); - expect(gd._fullData[1].marker.line.width).toEqual([4, 2, 3]); + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - assertDims([4, 3]); + expect(gd._fullData.length).toEqual(1); + expect(gd._fullData[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd._fullData[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - done(); - }); - }; - } + assertDims([7]); - // basic test - var mockData1 = [{ - mode: 'markers', - ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'], - x: [1, -1, -2, 0, 1, 2, 3], - y: [0, 1, 2, 3, 5, 4, 6], - marker: {line: {width: [4, 2, 4, 2, 2, 3, 3]}}, - transforms: [{ - type: 'groupby', - groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } - }] - }]; - - // heterogenously present attributes - var mockData2 = [{ - mode: 'markers', - ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'], - x: [1, -1, -2, 0, 1, 2, 3], - y: [0, 1, 2, 3, 5, 4, 6], - marker: {line: {width: [4, 2, 4, 2, 2, 3, 3]}}, - transforms: [{ - type: 'groupby', - groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { - a: { - marker: { - color: 'orange', - size: 20, - line: { - color: 'red' - } - } - }, - b: { - mode: 'markers+lines', // heterogeonos attributes are OK: group 'a' doesn't need to define this - marker: { - color: 'cyan', - size: 15, - line: { - color: 'purple' - }, - opacity: 0.5, - symbol: 'triangle-up' - }, - line: { - color: 'purple' - } - } - } - }] - }]; - - // attributes set at top level and partially overridden in the group item level - var mockData3 = [{ - mode: 'markers+lines', - ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'], - x: [1, -1, -2, 0, 1, 2, 3], - y: [0, 1, 2, 3, 5, 4, 6], - marker: { - color: 'darkred', // general 'default' color - line: { - width: [4, 2, 4, 2, 2, 3, 3], - color: ['orange', 'red', 'green', 'cyan', 'magenta', 'blue', 'pink'] - } - }, - line: {color: 'red'}, - transforms: [{ - type: 'groupby', - groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { - a: {marker: {size: 30}}, - // override general color: - b: {marker: {size: 15, line: {color: 'yellow'}}, line: {color: 'purple'}} - } - }] - }]; - - var mockData4 = [{ - mode: 'markers+lines', - ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'], - x: [1, -1, -2, 0, 1, 2, 3], - y: [0, 1, 2, 3, 5, 4, 6], - marker: {line: {width: [4, 2, 4, 2, 2, 3, 3]}}, - transforms: [{ - type: 'groupby', - groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: {/* can be empty, or of partial group id coverage */} - }] - }]; - - var mockData5 = [{ - mode: 'markers+lines', - ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'], - x: [1, -1, -2, 0, 1, 2, 3], - y: [0, 1, 2, 3, 5, 4, 6], - marker: { - line: {width: [4, 2, 4, 2, 2, 3, 3]}, - size: 10, - color: ['red', '#eee', 'lightgreen', 'blue', 'red', '#eee', 'lightgreen'] - }, - transforms: [{ - type: 'groupby', - groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'] - }] - }]; - - it('`data` preserves user supplied input but `gd._fullData` reflects the grouping', test(mockData1)); - - it('passes with lots of attributes and heterogenous attrib presence', test(mockData2)); - - it('passes with group styles partially overriding top level aesthetics', test(mockData3)); - it('passes extended tests with group styles partially overriding top level aesthetics', function(done) { - var data = Lib.extendDeep([], mockData3); - var gd = createGraphDiv(); - Plotly.plot(gd, data).then(function() { - expect(gd._fullData[0].marker.line.color).toEqual(['orange', 'red', 'cyan', 'pink']); - expect(gd._fullData[1].marker.line.color).toEqual('yellow'); - done(); - }); + done(); }); + }); - it('passes with no explicit styling for the individual group', test(mockData4)); + it("Plotly.plot should plot the transform traces", function(done) { + var data = Lib.extendDeep([], mockData2); - it('passes with no explicit styling in the group transform at all', test(mockData5)); + var gd = createGraphDiv(); - }); + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - describe('passes with no `groups`', function() { - 'use strict'; + expect(gd._fullData.length).toEqual(1); - afterEach(destroyGraphDiv); + expect(gd._fullData[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd._fullData[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - function test(mockData) { + assertDims([7]); - return function(done) { - var data = Lib.extendDeep([], mockData); + done(); + }); + }); - var gd = createGraphDiv(); + it("Plotly.plot should plot the transform traces", function(done) { + var data = Lib.extendDeep([], mockData3); - Plotly.plot(gd, data).then(function() { + var gd = createGraphDiv(); - expect(gd.data.length).toEqual(1); - expect(gd.data[0].ids).toEqual(['q', 'w', 'r', 't', 'y', 'u', 'i']); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([0, 1, 2, 3, 5, 4, 6]); - expect(gd.data[0].marker.line.width).toEqual([4, 2, 4, 2, 2, 3, 3]); + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - expect(gd._fullData.length).toEqual(1); + expect(gd._fullData.length).toEqual(1); - expect(gd._fullData[0].ids).toEqual(['q', 'w', 'r', 't', 'y', 'u', 'i']); - expect(gd._fullData[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd._fullData[0].y).toEqual([0, 1, 2, 3, 5, 4, 6]); - expect(gd._fullData[0].marker.line.width).toEqual([4, 2, 4, 2, 2, 3, 3]); + expect(gd._fullData[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd._fullData[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - assertDims([7]); + assertDims([7]); - done(); - }); - }; + done(); + }); + }); + } + ); + + describe( + "grouping with basic, heterogenous and overridden attributes", + function() { + "use strict"; + afterEach(destroyGraphDiv); + + function test(mockData) { + return function(done) { + var data = Lib.extendDeep([], mockData); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].ids).toEqual(["q", "w", "r", "t", "y", "u", "i"]); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([0, 1, 2, 3, 5, 4, 6]); + expect(gd.data[0].marker.line.width).toEqual([4, 2, 4, 2, 2, 3, 3]); + + expect(gd._fullData.length).toEqual(2); + + expect(gd._fullData[0].ids).toEqual(["q", "w", "t", "i"]); + expect(gd._fullData[0].x).toEqual([1, -1, 0, 3]); + expect(gd._fullData[0].y).toEqual([0, 1, 3, 6]); + expect(gd._fullData[0].marker.line.width).toEqual([4, 2, 2, 3]); + + expect(gd._fullData[1].ids).toEqual(["r", "y", "u"]); + expect(gd._fullData[1].x).toEqual([-2, 1, 2]); + expect(gd._fullData[1].y).toEqual([2, 5, 4]); + expect(gd._fullData[1].marker.line.width).toEqual([4, 2, 3]); + + assertDims([4, 3]); + + done(); + }); + }; + } + + // basic test + var mockData1 = [ + { + mode: "markers", + ids: ["q", "w", "r", "t", "y", "u", "i"], + x: [1, -1, -2, 0, 1, 2, 3], + y: [0, 1, 2, 3, 5, 4, 6], + marker: { line: { width: [4, 2, 4, 2, 2, 3, 3] } }, + transforms: [ + { + type: "groupby", + groups: ["a", "a", "b", "a", "b", "b", "a"], + style: { + a: { marker: { color: "red" } }, + b: { marker: { color: "blue" } } + } + } + ] } - - var mockData0 = [{ - mode: 'markers+lines', - ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'], - x: [1, -1, -2, 0, 1, 2, 3], - y: [0, 1, 2, 3, 5, 4, 6], - marker: {size: 20, line: {width: [4, 2, 4, 2, 2, 3, 3]}}, - transforms: [{ - type: 'groupby', - // groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } - }] - }]; - - var mockData1 = [{ - mode: 'markers+lines', - ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'], - x: [1, -1, -2, 0, 1, 2, 3], - y: [0, 1, 2, 3, 5, 4, 6], - marker: {size: 20, line: {width: [4, 2, 4, 2, 2, 3, 3]}}, - transforms: [{ - type: 'groupby', - groups: [], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } - }] - }]; - - var mockData2 = [{ - mode: 'markers+lines', - ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'], - x: [1, -1, -2, 0, 1, 2, 3], - y: [0, 1, 2, 3, 5, 4, 6], - marker: {size: 20, line: {width: [4, 2, 4, 2, 2, 3, 3]}}, - transforms: [{ - type: 'groupby', - groups: null, - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } - }] - }]; - - it('passes with no groups', test(mockData0)); - it('passes with empty groups', test(mockData1)); - it('passes with falsey groups', test(mockData2)); - - }); + ]; + + // heterogenously present attributes + var mockData2 = [ + { + mode: "markers", + ids: ["q", "w", "r", "t", "y", "u", "i"], + x: [1, -1, -2, 0, 1, 2, 3], + y: [0, 1, 2, 3, 5, 4, 6], + marker: { line: { width: [4, 2, 4, 2, 2, 3, 3] } }, + transforms: [ + { + type: "groupby", + groups: ["a", "a", "b", "a", "b", "b", "a"], + style: { + a: { + marker: { color: "orange", size: 20, line: { color: "red" } } + }, + b: { + mode: "markers+lines", + // heterogeonos attributes are OK: group 'a' doesn't need to define this + marker: { + color: "cyan", + size: 15, + line: { color: "purple" }, + opacity: 0.5, + symbol: "triangle-up" + }, + line: { color: "purple" } + } + } + } + ] + } + ]; + + // attributes set at top level and partially overridden in the group item level + var mockData3 = [ + { + mode: "markers+lines", + ids: ["q", "w", "r", "t", "y", "u", "i"], + x: [1, -1, -2, 0, 1, 2, 3], + y: [0, 1, 2, 3, 5, 4, 6], + marker: { + color: "darkred", + // general 'default' color + line: { + width: [4, 2, 4, 2, 2, 3, 3], + color: [ + "orange", + "red", + "green", + "cyan", + "magenta", + "blue", + "pink" + ] + } + }, + line: { color: "red" }, + transforms: [ + { + type: "groupby", + groups: ["a", "a", "b", "a", "b", "b", "a"], + style: { + a: { marker: { size: 30 } }, + // override general color: + b: { + marker: { size: 15, line: { color: "yellow" } }, + line: { color: "purple" } + } + } + } + ] + } + ]; + + var mockData4 = [ + { + mode: "markers+lines", + ids: ["q", "w", "r", "t", "y", "u", "i"], + x: [1, -1, -2, 0, 1, 2, 3], + y: [0, 1, 2, 3, 5, 4, 6], + marker: { line: { width: [4, 2, 4, 2, 2, 3, 3] } }, + transforms: [ + { + type: "groupby", + groups: ["a", "a", "b", "a", "b", "b", "a"], + style: {} + } + ] + } + ]; + + var mockData5 = [ + { + mode: "markers+lines", + ids: ["q", "w", "r", "t", "y", "u", "i"], + x: [1, -1, -2, 0, 1, 2, 3], + y: [0, 1, 2, 3, 5, 4, 6], + marker: { + line: { width: [4, 2, 4, 2, 2, 3, 3] }, + size: 10, + color: [ + "red", + "#eee", + "lightgreen", + "blue", + "red", + "#eee", + "lightgreen" + ] + }, + transforms: [ + { type: "groupby", groups: ["a", "a", "b", "a", "b", "b", "a"] } + ] + } + ]; + + it( + "`data` preserves user supplied input but `gd._fullData` reflects the grouping", + test(mockData1) + ); + + it( + "passes with lots of attributes and heterogenous attrib presence", + test(mockData2) + ); + + it( + "passes with group styles partially overriding top level aesthetics", + test(mockData3) + ); + it( + "passes extended tests with group styles partially overriding top level aesthetics", + function(done) { + var data = Lib.extendDeep([], mockData3); + var gd = createGraphDiv(); + Plotly.plot(gd, data).then(function() { + expect(gd._fullData[0].marker.line.color).toEqual([ + "orange", + "red", + "cyan", + "pink" + ]); + expect(gd._fullData[1].marker.line.color).toEqual("yellow"); + done(); + }); + } + ); + + it( + "passes with no explicit styling for the individual group", + test(mockData4) + ); + + it( + "passes with no explicit styling in the group transform at all", + test(mockData5) + ); + } + ); + + describe("passes with no `groups`", function() { + "use strict"; + afterEach(destroyGraphDiv); + + function test(mockData) { + return function(done) { + var data = Lib.extendDeep([], mockData); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].ids).toEqual(["q", "w", "r", "t", "y", "u", "i"]); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([0, 1, 2, 3, 5, 4, 6]); + expect(gd.data[0].marker.line.width).toEqual([4, 2, 4, 2, 2, 3, 3]); + + expect(gd._fullData.length).toEqual(1); + + expect(gd._fullData[0].ids).toEqual([ + "q", + "w", + "r", + "t", + "y", + "u", + "i" + ]); + expect(gd._fullData[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd._fullData[0].y).toEqual([0, 1, 2, 3, 5, 4, 6]); + expect(gd._fullData[0].marker.line.width).toEqual([ + 4, + 2, + 4, + 2, + 2, + 3, + 3 + ]); + + assertDims([7]); + + done(); + }); + }; + } + + var mockData0 = [ + { + mode: "markers+lines", + ids: ["q", "w", "r", "t", "y", "u", "i"], + x: [1, -1, -2, 0, 1, 2, 3], + y: [0, 1, 2, 3, 5, 4, 6], + marker: { size: 20, line: { width: [4, 2, 4, 2, 2, 3, 3] } }, + transforms: [ + { + type: "groupby", + // groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], + style: { + a: { marker: { color: "red" } }, + b: { marker: { color: "blue" } } + } + } + ] + } + ]; + + var mockData1 = [ + { + mode: "markers+lines", + ids: ["q", "w", "r", "t", "y", "u", "i"], + x: [1, -1, -2, 0, 1, 2, 3], + y: [0, 1, 2, 3, 5, 4, 6], + marker: { size: 20, line: { width: [4, 2, 4, 2, 2, 3, 3] } }, + transforms: [ + { + type: "groupby", + groups: [], + style: { + a: { marker: { color: "red" } }, + b: { marker: { color: "blue" } } + } + } + ] + } + ]; + + var mockData2 = [ + { + mode: "markers+lines", + ids: ["q", "w", "r", "t", "y", "u", "i"], + x: [1, -1, -2, 0, 1, 2, 3], + y: [0, 1, 2, 3, 5, 4, 6], + marker: { size: 20, line: { width: [4, 2, 4, 2, 2, 3, 3] } }, + transforms: [ + { + type: "groupby", + groups: null, + style: { + a: { marker: { color: "red" } }, + b: { marker: { color: "blue" } } + } + } + ] + } + ]; + + it("passes with no groups", test(mockData0)); + it("passes with empty groups", test(mockData1)); + it("passes with falsey groups", test(mockData2)); + }); }); diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 6930effbd8e..15c17750a39 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -1,665 +1,702 @@ -var Plotly = require('@lib/index'); -var Filter = require('@lib/filter'); - -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); - -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var assertDims = require('../assets/assert_dims'); -var assertStyle = require('../assets/assert_style'); - - -describe('general transforms:', function() { - 'use strict'; - - var fullLayout = { _transformModules: [] }; - - var traceIn, traceOut; - - it('supplyTraceDefaults should supply the transform defaults', function() { - traceIn = { - y: [2, 1, 2], - transforms: [{ type: 'filter' }] - }; - - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); - - expect(traceOut.transforms).toEqual([{ - type: 'filter', - enabled: true, - operation: '=', - value: 0, - target: 'x', - _module: Filter - }]); - }); - - it('supplyTraceDefaults should not bail if transform module is not found', function() { - traceIn = { - y: [2, 1, 2], - transforms: [{ type: 'invalid' }] - }; - - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); - - expect(traceOut.y).toBe(traceIn.y); - }); - - it('supplyTraceDefaults should honored global transforms', function() { - traceIn = { - y: [2, 1, 2], - transforms: [{ - type: 'filter', - operation: '>', - value: 0, - target: 'x' - }] - }; - - var layout = { - _transformModules: [], - _globalTransforms: [{ - type: 'filter' - }] - }; - - traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); - - expect(traceOut.transforms[0]).toEqual({ - type: 'filter', - enabled: true, - operation: '=', - value: 0, - target: 'x', - _module: Filter - }, '- global first'); - - expect(traceOut.transforms[1]).toEqual({ - type: 'filter', - enabled: true, - operation: '>', - value: 0, - target: 'x', - _module: Filter - }, '- trace second'); - - expect(layout._transformModules).toEqual([Filter]); - }); - - it('supplyDataDefaults should apply the transform while', function() { - var dataIn = [{ - x: [-2, -2, 1, 2, 3], - y: [1, 2, 2, 3, 1] - }, { - x: [-2, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - transforms: [{ - type: 'filter', - operation: '>', - value: 0, - target: 'x' - }] - }]; - - var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); - - var msg; - - msg = 'does not mutate user data'; - expect(dataIn[1].x).toEqual([-2, -1, -2, 0, 1, 2, 3], msg); - expect(dataIn[1].y).toEqual([1, 2, 3, 1, 2, 3, 1], msg); - expect(dataIn[1].transforms).toEqual([{ - type: 'filter', - operation: '>', - value: 0, - target: 'x' - }], msg); - - msg = 'supplying the transform defaults'; - expect(dataOut[1].transforms[0]).toEqual({ - type: 'filter', - enabled: true, - operation: '>', - value: 0, - target: 'x', - _module: Filter - }, msg); - - msg = 'keeping refs to user data'; - expect(dataOut[1]._input.x).toEqual([-2, -1, -2, 0, 1, 2, 3], msg); - expect(dataOut[1]._input.y).toEqual([1, 2, 3, 1, 2, 3, 1], msg); - expect(dataOut[1]._input.transforms).toEqual([{ - type: 'filter', - operation: '>', - value: 0, - target: 'x', - }], msg); - - msg = 'keeping refs to full transforms array'; - expect(dataOut[1]._fullInput.transforms).toEqual([{ - type: 'filter', - enabled: true, - operation: '>', - value: 0, - target: 'x', - _module: Filter - }], msg); - - msg = 'setting index w.r.t user data'; - expect(dataOut[0].index).toEqual(0, msg); - expect(dataOut[1].index).toEqual(1, msg); - - msg = 'setting _expandedIndex w.r.t full data'; - expect(dataOut[0]._expandedIndex).toEqual(0, msg); - expect(dataOut[1]._expandedIndex).toEqual(1, msg); - }); - -}); - -describe('user-defined transforms:', function() { - 'use strict'; - - it('should pass correctly arguments to transform methods', function() { - var transformIn = { type: 'fake' }; - var transformOut = {}; - - var dataIn = [{ - transforms: [transformIn] - }]; - - var fullData = [], - layout = {}, - fullLayout = { _has: function() {} }, - transitionData = {}; - - function assertSupplyDefaultsArgs(_transformIn, traceOut, _layout) { - expect(_transformIn).toBe(transformIn); - expect(_layout).toBe(fullLayout); - - return transformOut; - } - - function assertTransformArgs(dataOut, opts) { - expect(dataOut[0]._input).toBe(dataIn[0]); - expect(opts.transform).toBe(transformOut); - expect(opts.fullTrace._input).toBe(dataIn[0]); - expect(opts.layout).toBe(layout); - expect(opts.fullLayout).toBe(fullLayout); - - return dataOut; - } - - function assertSupplyLayoutDefaultsArgs(_layout, _fullLayout, _fullData, _transitionData) { - expect(_layout).toBe(layout); - expect(_fullLayout).toBe(fullLayout); - expect(_fullData).toBe(fullData); - expect(_transitionData).toBe(transitionData); +var Plotly = require("@lib/index"); +var Filter = require("@lib/filter"); + +var Plots = require("@src/plots/plots"); +var Lib = require("@src/lib"); + +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var assertDims = require("../assets/assert_dims"); +var assertStyle = require("../assets/assert_style"); + +describe("general transforms:", function() { + "use strict"; + var fullLayout = { _transformModules: [] }; + + var traceIn, traceOut; + + it("supplyTraceDefaults should supply the transform defaults", function() { + traceIn = { y: [2, 1, 2], transforms: [{ type: "filter" }] }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + + expect(traceOut.transforms).toEqual([ + { + type: "filter", + enabled: true, + operation: "=", + value: 0, + target: "x", + _module: Filter + } + ]); + }); + + it( + "supplyTraceDefaults should not bail if transform module is not found", + function() { + traceIn = { y: [2, 1, 2], transforms: [{ type: "invalid" }] }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + + expect(traceOut.y).toBe(traceIn.y); + } + ); + + it("supplyTraceDefaults should honored global transforms", function() { + traceIn = { + y: [2, 1, 2], + transforms: [{ type: "filter", operation: ">", value: 0, target: "x" }] + }; + + var layout = { + _transformModules: [], + _globalTransforms: [{ type: "filter" }] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); + + expect(traceOut.transforms[0]).toEqual( + { + type: "filter", + enabled: true, + operation: "=", + value: 0, + target: "x", + _module: Filter + }, + "- global first" + ); + + expect(traceOut.transforms[1]).toEqual( + { + type: "filter", + enabled: true, + operation: ">", + value: 0, + target: "x", + _module: Filter + }, + "- trace second" + ); + + expect(layout._transformModules).toEqual([Filter]); + }); + + it("supplyDataDefaults should apply the transform while", function() { + var dataIn = [ + { x: [-2, -2, 1, 2, 3], y: [1, 2, 2, 3, 1] }, + { + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [{ type: "filter", operation: ">", value: 0, target: "x" }] + } + ]; + + var dataOut = []; + Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + + var msg; + + msg = "does not mutate user data"; + expect(dataIn[1].x).toEqual([-2, -1, -2, 0, 1, 2, 3], msg); + expect(dataIn[1].y).toEqual([1, 2, 3, 1, 2, 3, 1], msg); + expect(dataIn[1].transforms).toEqual( + [{ type: "filter", operation: ">", value: 0, target: "x" }], + msg + ); + + msg = "supplying the transform defaults"; + expect(dataOut[1].transforms[0]).toEqual( + { + type: "filter", + enabled: true, + operation: ">", + value: 0, + target: "x", + _module: Filter + }, + msg + ); + + msg = "keeping refs to user data"; + expect(dataOut[1]._input.x).toEqual([-2, -1, -2, 0, 1, 2, 3], msg); + expect(dataOut[1]._input.y).toEqual([1, 2, 3, 1, 2, 3, 1], msg); + expect(dataOut[1]._input.transforms).toEqual( + [{ type: "filter", operation: ">", value: 0, target: "x" }], + msg + ); + + msg = "keeping refs to full transforms array"; + expect(dataOut[1]._fullInput.transforms).toEqual( + [ + { + type: "filter", + enabled: true, + operation: ">", + value: 0, + target: "x", + _module: Filter } - - var fakeTransformModule = { - moduleType: 'transform', - name: 'fake', - attributes: {}, - supplyDefaults: assertSupplyDefaultsArgs, - transform: assertTransformArgs, - supplyLayoutDefaults: assertSupplyLayoutDefaultsArgs - }; - - Plotly.register(fakeTransformModule); - Plots.supplyDataDefaults(dataIn, fullData, layout, fullLayout); - Plots.supplyLayoutModuleDefaults(layout, fullLayout, fullData, transitionData); - delete Plots.transformsRegistry.fake; - }); - + ], + msg + ); + + msg = "setting index w.r.t user data"; + expect(dataOut[0].index).toEqual(0, msg); + expect(dataOut[1].index).toEqual(1, msg); + + msg = "setting _expandedIndex w.r.t full data"; + expect(dataOut[0]._expandedIndex).toEqual(0, msg); + expect(dataOut[1]._expandedIndex).toEqual(1, msg); + }); }); -describe('multiple transforms:', function() { - 'use strict'; - - var mockData0 = [{ - mode: 'markers', - x: [1, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - transforms: [{ - type: 'groupby', - groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } - }, { - type: 'filter', - operation: '>' - }] - }]; - - var mockData1 = [Lib.extendDeep({}, mockData0[0]), { - mode: 'markers', - x: [20, 11, 12, 0, 1, 2, 3], - y: [1, 2, 3, 2, 5, 2, 0], - transforms: [{ - type: 'groupby', - groups: ['b', 'a', 'b', 'b', 'b', 'a', 'a'], - style: { a: {marker: {color: 'green'}}, b: {marker: {color: 'black'}} } - }, { - type: 'filter', - operation: '<', - value: 10 - }] - }]; - - afterEach(destroyGraphDiv); - - it('Plotly.plot should plot the transform traces', function(done) { - var data = Lib.extendDeep([], mockData0); - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - expect(gd.data.length).toEqual(1); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - - expect(gd._fullData.length).toEqual(2); - expect(gd._fullData[0].x).toEqual([1, 3]); - expect(gd._fullData[0].y).toEqual([1, 1]); - expect(gd._fullData[1].x).toEqual([1, 2]); - expect(gd._fullData[1].y).toEqual([2, 3]); - - assertDims([2, 2]); - - done(); - }); - }); - - it('Plotly.plot should plot the transform traces (reverse case)', function(done) { - var data = Lib.extendDeep([], mockData0); - - data[0].transforms.slice().reverse(); - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - expect(gd.data.length).toEqual(1); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - - expect(gd._fullData.length).toEqual(2); - expect(gd._fullData[0].x).toEqual([1, 3]); - expect(gd._fullData[0].y).toEqual([1, 1]); - expect(gd._fullData[1].x).toEqual([1, 2]); - expect(gd._fullData[1].y).toEqual([2, 3]); - - assertDims([2, 2]); - - done(); - }); - }); +describe("user-defined transforms:", function() { + "use strict"; + it("should pass correctly arguments to transform methods", function() { + var transformIn = { type: "fake" }; + var transformOut = {}; + + var dataIn = [{ transforms: [transformIn] }]; + + var fullData = [], + layout = {}, + fullLayout = { + _has: function() {} + }, + transitionData = {}; + + function assertSupplyDefaultsArgs(_transformIn, traceOut, _layout) { + expect(_transformIn).toBe(transformIn); + expect(_layout).toBe(fullLayout); + + return transformOut; + } + + function assertTransformArgs(dataOut, opts) { + expect(dataOut[0]._input).toBe(dataIn[0]); + expect(opts.transform).toBe(transformOut); + expect(opts.fullTrace._input).toBe(dataIn[0]); + expect(opts.layout).toBe(layout); + expect(opts.fullLayout).toBe(fullLayout); + + return dataOut; + } + + function assertSupplyLayoutDefaultsArgs( + _layout, + _fullLayout, + _fullData, + _transitionData + ) { + expect(_layout).toBe(layout); + expect(_fullLayout).toBe(fullLayout); + expect(_fullData).toBe(fullData); + expect(_transitionData).toBe(transitionData); + } + + var fakeTransformModule = { + moduleType: "transform", + name: "fake", + attributes: {}, + supplyDefaults: assertSupplyDefaultsArgs, + transform: assertTransformArgs, + supplyLayoutDefaults: assertSupplyLayoutDefaultsArgs + }; + + Plotly.register(fakeTransformModule); + Plots.supplyDataDefaults(dataIn, fullData, layout, fullLayout); + Plots.supplyLayoutModuleDefaults( + layout, + fullLayout, + fullData, + transitionData + ); + delete Plots.transformsRegistry.fake; + }); +}); - it('Plotly.restyle should work', function(done) { - var data = Lib.extendDeep([], mockData0); - data[0].marker = { size: 20 }; - - var gd = createGraphDiv(); - var dims = [2, 2]; - - Plotly.plot(gd, data).then(function() { - assertStyle(dims, - ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], - [1, 1] - ); - - return Plotly.restyle(gd, 'marker.opacity', 0.4); - }).then(function() { - assertStyle(dims, - ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], - [0.4, 0.4] - ); - - expect(gd._fullData[0].marker.opacity).toEqual(0.4); - expect(gd._fullData[1].marker.opacity).toEqual(0.4); - - return Plotly.restyle(gd, 'marker.opacity', 1); - }).then(function() { - assertStyle(dims, - ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], - [1, 1] - ); - - expect(gd._fullData[0].marker.opacity).toEqual(1); - expect(gd._fullData[1].marker.opacity).toEqual(1); - - return Plotly.restyle(gd, { - 'transforms[0].style': { a: {marker: {color: 'green'}}, b: {marker: {color: 'red'}} }, - 'marker.opacity': 0.4 - }); - }).then(function() { - assertStyle(dims, - ['rgb(0, 128, 0)', 'rgb(255, 0, 0)'], - [0.4, 0.4] - ); - - done(); - }); +describe("multiple transforms:", function() { + "use strict"; + var mockData0 = [ + { + mode: "markers", + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [ + { + type: "groupby", + groups: ["a", "a", "b", "a", "b", "b", "a"], + style: { + a: { marker: { color: "red" } }, + b: { marker: { color: "blue" } } + } + }, + { type: "filter", operation: ">" } + ] + } + ]; + + var mockData1 = [ + Lib.extendDeep({}, mockData0[0]), + { + mode: "markers", + x: [20, 11, 12, 0, 1, 2, 3], + y: [1, 2, 3, 2, 5, 2, 0], + transforms: [ + { + type: "groupby", + groups: ["b", "a", "b", "b", "b", "a", "a"], + style: { + a: { marker: { color: "green" } }, + b: { marker: { color: "black" } } + } + }, + { type: "filter", operation: "<", value: 10 } + ] + } + ]; + + afterEach(destroyGraphDiv); + + it("Plotly.plot should plot the transform traces", function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + + expect(gd._fullData.length).toEqual(2); + expect(gd._fullData[0].x).toEqual([1, 3]); + expect(gd._fullData[0].y).toEqual([1, 1]); + expect(gd._fullData[1].x).toEqual([1, 2]); + expect(gd._fullData[1].y).toEqual([2, 3]); + + assertDims([2, 2]); + + done(); }); + }); - it('Plotly.extendTraces should work', function(done) { - var data = Lib.extendDeep([], mockData0); + it("Plotly.plot should plot the transform traces (reverse case)", function( + done + ) { + var data = Lib.extendDeep([], mockData0); - var gd = createGraphDiv(); + data[0].transforms.slice().reverse(); - Plotly.plot(gd, data).then(function() { - expect(gd.data[0].x.length).toEqual(7); - expect(gd._fullData[0].x.length).toEqual(2); - expect(gd._fullData[1].x.length).toEqual(2); + var gd = createGraphDiv(); - assertDims([2, 2]); + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - return Plotly.extendTraces(gd, { - x: [ [-3, 4, 5] ], - y: [ [1, -2, 3] ], - 'transforms[0].groups': [ ['b', 'a', 'b'] ] - }, [0]); - }).then(function() { - expect(gd.data[0].x.length).toEqual(10); - expect(gd._fullData[0].x.length).toEqual(3); - expect(gd._fullData[1].x.length).toEqual(3); + expect(gd._fullData.length).toEqual(2); + expect(gd._fullData[0].x).toEqual([1, 3]); + expect(gd._fullData[0].y).toEqual([1, 1]); + expect(gd._fullData[1].x).toEqual([1, 2]); + expect(gd._fullData[1].y).toEqual([2, 3]); - assertDims([3, 3]); + assertDims([2, 2]); - done(); - }); + done(); }); + }); - it('Plotly.deleteTraces should work', function(done) { - var data = Lib.extendDeep([], mockData1); + it("Plotly.restyle should work", function(done) { + var data = Lib.extendDeep([], mockData0); + data[0].marker = { size: 20 }; - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - assertDims([2, 2, 2, 2]); - - return Plotly.deleteTraces(gd, [1]); - }).then(function() { - assertDims([2, 2]); - - return Plotly.deleteTraces(gd, [0]); - }).then(function() { - assertDims([]); - - done(); - }); - }); + var gd = createGraphDiv(); + var dims = [2, 2]; - it('toggling trace visibility should work', function(done) { - var data = Lib.extendDeep([], mockData1); + Plotly.plot(gd, data) + .then(function() { + assertStyle(dims, ["rgb(255, 0, 0)", "rgb(0, 0, 255)"], [1, 1]); - var gd = createGraphDiv(); + return Plotly.restyle(gd, "marker.opacity", 0.4); + }) + .then(function() { + assertStyle(dims, ["rgb(255, 0, 0)", "rgb(0, 0, 255)"], [0.4, 0.4]); - Plotly.plot(gd, data).then(function() { - assertDims([2, 2, 2, 2]); + expect(gd._fullData[0].marker.opacity).toEqual(0.4); + expect(gd._fullData[1].marker.opacity).toEqual(0.4); - return Plotly.restyle(gd, 'visible', 'legendonly', [1]); - }).then(function() { - assertDims([2, 2]); + return Plotly.restyle(gd, "marker.opacity", 1); + }) + .then(function() { + assertStyle(dims, ["rgb(255, 0, 0)", "rgb(0, 0, 255)"], [1, 1]); - return Plotly.restyle(gd, 'visible', false, [0]); - }).then(function() { - assertDims([]); + expect(gd._fullData[0].marker.opacity).toEqual(1); + expect(gd._fullData[1].marker.opacity).toEqual(1); - return Plotly.restyle(gd, 'visible', [true, true]); - }).then(function() { - assertDims([2, 2, 2, 2]); - - done(); + return Plotly.restyle(gd, { + "transforms[0].style": { + a: { marker: { color: "green" } }, + b: { marker: { color: "red" } } + }, + "marker.opacity": 0.4 }); - }); - + }) + .then(function() { + assertStyle(dims, ["rgb(0, 128, 0)", "rgb(255, 0, 0)"], [0.4, 0.4]); + + done(); + }); + }); + + it("Plotly.extendTraces should work", function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data) + .then(function() { + expect(gd.data[0].x.length).toEqual(7); + expect(gd._fullData[0].x.length).toEqual(2); + expect(gd._fullData[1].x.length).toEqual(2); + + assertDims([2, 2]); + + return Plotly.extendTraces( + gd, + { + x: [[-3, 4, 5]], + y: [[1, -2, 3]], + "transforms[0].groups": [["b", "a", "b"]] + }, + [0] + ); + }) + .then(function() { + expect(gd.data[0].x.length).toEqual(10); + expect(gd._fullData[0].x.length).toEqual(3); + expect(gd._fullData[1].x.length).toEqual(3); + + assertDims([3, 3]); + + done(); + }); + }); + + it("Plotly.deleteTraces should work", function(done) { + var data = Lib.extendDeep([], mockData1); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data) + .then(function() { + assertDims([2, 2, 2, 2]); + + return Plotly.deleteTraces(gd, [1]); + }) + .then(function() { + assertDims([2, 2]); + + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + assertDims([]); + + done(); + }); + }); + + it("toggling trace visibility should work", function(done) { + var data = Lib.extendDeep([], mockData1); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data) + .then(function() { + assertDims([2, 2, 2, 2]); + + return Plotly.restyle(gd, "visible", "legendonly", [1]); + }) + .then(function() { + assertDims([2, 2]); + + return Plotly.restyle(gd, "visible", false, [0]); + }) + .then(function() { + assertDims([]); + + return Plotly.restyle(gd, "visible", [true, true]); + }) + .then(function() { + assertDims([2, 2, 2, 2]); + + done(); + }); + }); }); -describe('multiple traces with transforms:', function() { - 'use strict'; - - var mockData0 = [{ - mode: 'markers', - x: [1, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - marker: { color: 'green' }, - name: 'filtered', - transforms: [{ - type: 'filter', - operation: '>', - value: 1 - }] - }, { - mode: 'markers', - x: [20, 11, 12, 0, 1, 2, 3], - y: [1, 2, 3, 2, 5, 2, 0], - transforms: [{ - type: 'groupby', - groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } - }, { - type: 'filter', - operation: '>' - }] - }]; - - afterEach(destroyGraphDiv); - - it('Plotly.plot should plot the transform traces', function(done) { - var data = Lib.extendDeep([], mockData0); - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - expect(gd.data.length).toEqual(2); - expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); - expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); - expect(gd.data[1].x).toEqual([20, 11, 12, 0, 1, 2, 3]); - expect(gd.data[1].y).toEqual([1, 2, 3, 2, 5, 2, 0]); - - expect(gd._fullData.length).toEqual(3); - expect(gd._fullData[0].x).toEqual([2, 3]); - expect(gd._fullData[0].y).toEqual([3, 1]); - expect(gd._fullData[1].x).toEqual([20, 11, 3]); - expect(gd._fullData[1].y).toEqual([1, 2, 0]); - expect(gd._fullData[2].x).toEqual([12, 1, 2]); - expect(gd._fullData[2].y).toEqual([3, 5, 2]); - - assertDims([2, 3, 3]); - - done(); - }); +describe("multiple traces with transforms:", function() { + "use strict"; + var mockData0 = [ + { + mode: "markers", + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + marker: { color: "green" }, + name: "filtered", + transforms: [{ type: "filter", operation: ">", value: 1 }] + }, + { + mode: "markers", + x: [20, 11, 12, 0, 1, 2, 3], + y: [1, 2, 3, 2, 5, 2, 0], + transforms: [ + { + type: "groupby", + groups: ["a", "a", "b", "a", "b", "b", "a"], + style: { + a: { marker: { color: "red" } }, + b: { marker: { color: "blue" } } + } + }, + { type: "filter", operation: ">" } + ] + } + ]; + + afterEach(destroyGraphDiv); + + it("Plotly.plot should plot the transform traces", function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(2); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + expect(gd.data[1].x).toEqual([20, 11, 12, 0, 1, 2, 3]); + expect(gd.data[1].y).toEqual([1, 2, 3, 2, 5, 2, 0]); + + expect(gd._fullData.length).toEqual(3); + expect(gd._fullData[0].x).toEqual([2, 3]); + expect(gd._fullData[0].y).toEqual([3, 1]); + expect(gd._fullData[1].x).toEqual([20, 11, 3]); + expect(gd._fullData[1].y).toEqual([1, 2, 0]); + expect(gd._fullData[2].x).toEqual([12, 1, 2]); + expect(gd._fullData[2].y).toEqual([3, 5, 2]); + + assertDims([2, 3, 3]); + + done(); }); - - it('Plotly.restyle should work', function(done) { - var data = Lib.extendDeep([], mockData0); - data[0].marker.size = 20; - - var gd = createGraphDiv(); - var dims = [2, 3, 3]; - - Plotly.plot(gd, data).then(function() { - assertStyle(dims, - ['rgb(0, 128, 0)', 'rgb(255, 0, 0)', 'rgb(0, 0, 255)'], - [1, 1, 1] - ); - - return Plotly.restyle(gd, 'marker.opacity', 0.4); - }).then(function() { - assertStyle(dims, - ['rgb(0, 128, 0)', 'rgb(255, 0, 0)', 'rgb(0, 0, 255)'], - [0.4, 0.4, 0.4] - ); - - gd._fullData.forEach(function(trace) { - expect(trace.marker.opacity).toEqual(0.4); - }); - - return Plotly.restyle(gd, 'marker.opacity', 1); - }).then(function() { - assertStyle(dims, - ['rgb(0, 128, 0)', 'rgb(255, 0, 0)', 'rgb(0, 0, 255)'], - [1, 1, 1] - ); - - gd._fullData.forEach(function(trace) { - expect(trace.marker.opacity).toEqual(1); - }); - - return Plotly.restyle(gd, { - 'transforms[0].style': { a: {marker: {color: 'green'}}, b: {marker: {color: 'red'}} }, - 'marker.opacity': [0.4, 0.6] - }); - }).then(function() { - assertStyle(dims, - ['rgb(0, 128, 0)', 'rgb(0, 128, 0)', 'rgb(255, 0, 0)'], - [0.4, 0.6, 0.6] - ); - - done(); + }); + + it("Plotly.restyle should work", function(done) { + var data = Lib.extendDeep([], mockData0); + data[0].marker.size = 20; + + var gd = createGraphDiv(); + var dims = [2, 3, 3]; + + Plotly.plot(gd, data) + .then(function() { + assertStyle( + dims, + ["rgb(0, 128, 0)", "rgb(255, 0, 0)", "rgb(0, 0, 255)"], + [1, 1, 1] + ); + + return Plotly.restyle(gd, "marker.opacity", 0.4); + }) + .then(function() { + assertStyle( + dims, + ["rgb(0, 128, 0)", "rgb(255, 0, 0)", "rgb(0, 0, 255)"], + [0.4, 0.4, 0.4] + ); + + gd._fullData.forEach(function(trace) { + expect(trace.marker.opacity).toEqual(0.4); }); - }); - - it('Plotly.extendTraces should work', function(done) { - var data = Lib.extendDeep([], mockData0); - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - assertDims([2, 3, 3]); - return Plotly.extendTraces(gd, { - x: [ [-3, 4, 5] ], - y: [ [1, -2, 3] ], - 'transforms[0].groups': [ ['b', 'a', 'b'] ] - }, [1]); - }).then(function() { - assertDims([2, 4, 4]); - - return Plotly.extendTraces(gd, { - x: [ [5, 7, 10] ], - y: [ [1, -2, 3] ] - }, [0]); - }).then(function() { - assertDims([5, 4, 4]); - - done(); + return Plotly.restyle(gd, "marker.opacity", 1); + }) + .then(function() { + assertStyle( + dims, + ["rgb(0, 128, 0)", "rgb(255, 0, 0)", "rgb(0, 0, 255)"], + [1, 1, 1] + ); + + gd._fullData.forEach(function(trace) { + expect(trace.marker.opacity).toEqual(1); }); - }); - - it('Plotly.deleteTraces should work', function(done) { - var data = Lib.extendDeep([], mockData0); - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - assertDims([2, 3, 3]); - - return Plotly.deleteTraces(gd, [1]); - }).then(function() { - assertDims([2]); - - return Plotly.deleteTraces(gd, [0]); - }).then(function() { - assertDims([]); - - done(); - }); - }); - it('toggling trace visibility should work', function(done) { - var data = Lib.extendDeep([], mockData0); - - var gd = createGraphDiv(); - - Plotly.plot(gd, data).then(function() { - assertDims([2, 3, 3]); - - return Plotly.restyle(gd, 'visible', 'legendonly', [1]); - }).then(function() { - assertDims([2]); - - return Plotly.restyle(gd, 'visible', false, [0]); - }).then(function() { - assertDims([]); - - return Plotly.restyle(gd, 'visible', [true, true]); - }).then(function() { - assertDims([2, 3, 3]); - - return Plotly.restyle(gd, 'visible', 'legendonly', [0]); - }).then(function() { - assertDims([3, 3]); - - done(); + return Plotly.restyle(gd, { + "transforms[0].style": { + a: { marker: { color: "green" } }, + b: { marker: { color: "red" } } + }, + "marker.opacity": [0.4, 0.6] }); - }); + }) + .then(function() { + assertStyle( + dims, + ["rgb(0, 128, 0)", "rgb(0, 128, 0)", "rgb(255, 0, 0)"], + [0.4, 0.6, 0.6] + ); + + done(); + }); + }); + + it("Plotly.extendTraces should work", function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data) + .then(function() { + assertDims([2, 3, 3]); + + return Plotly.extendTraces( + gd, + { + x: [[-3, 4, 5]], + y: [[1, -2, 3]], + "transforms[0].groups": [["b", "a", "b"]] + }, + [1] + ); + }) + .then(function() { + assertDims([2, 4, 4]); + + return Plotly.extendTraces(gd, { x: [[5, 7, 10]], y: [[1, -2, 3]] }, [ + 0 + ]); + }) + .then(function() { + assertDims([5, 4, 4]); + + done(); + }); + }); + + it("Plotly.deleteTraces should work", function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data) + .then(function() { + assertDims([2, 3, 3]); + + return Plotly.deleteTraces(gd, [1]); + }) + .then(function() { + assertDims([2]); + + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + assertDims([]); + + done(); + }); + }); + + it("toggling trace visibility should work", function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data) + .then(function() { + assertDims([2, 3, 3]); + + return Plotly.restyle(gd, "visible", "legendonly", [1]); + }) + .then(function() { + assertDims([2]); + + return Plotly.restyle(gd, "visible", false, [0]); + }) + .then(function() { + assertDims([]); + + return Plotly.restyle(gd, "visible", [true, true]); + }) + .then(function() { + assertDims([2, 3, 3]); + + return Plotly.restyle(gd, "visible", "legendonly", [0]); + }) + .then(function() { + assertDims([3, 3]); + + done(); + }); + }); }); -describe('restyle applied on transforms:', function() { - 'use strict'; - - afterEach(destroyGraphDiv); +describe("restyle applied on transforms:", function() { + "use strict"; + afterEach(destroyGraphDiv); - it('should be able', function(done) { - var gd = createGraphDiv(); + it("should be able", function(done) { + var gd = createGraphDiv(); - var data = [{ y: [2, 1, 2] }]; + var data = [{ y: [2, 1, 2] }]; - var transform0 = { - type: 'filter', - target: 'y', - operation: '>', - value: 1 - }; + var transform0 = { type: "filter", target: "y", operation: ">", value: 1 }; - var transform1 = { - type: 'groupby', - groups: ['a', 'b', 'b'] - }; + var transform1 = { type: "groupby", groups: ["a", "b", "b"] }; - Plotly.plot(gd, data).then(function() { - expect(gd.data.transforms).toBeUndefined(); + Plotly.plot(gd, data) + .then(function() { + expect(gd.data.transforms).toBeUndefined(); - return Plotly.restyle(gd, 'transforms[0]', transform0); - }) - .then(function() { - var msg = 'to generate blank transform objects'; + return Plotly.restyle(gd, "transforms[0]", transform0); + }) + .then(function() { + var msg = "to generate blank transform objects"; - expect(gd.data[0].transforms[0]).toBe(transform0, msg); + expect(gd.data[0].transforms[0]).toBe(transform0, msg); - // make sure transform actually works - expect(gd._fullData[0].y).toEqual([2, 2], msg); + // make sure transform actually works + expect(gd._fullData[0].y).toEqual([2, 2], msg); - return Plotly.restyle(gd, 'transforms[1]', transform1); - }) - .then(function() { - var msg = 'to generate blank transform objects (2)'; + return Plotly.restyle(gd, "transforms[1]", transform1); + }) + .then(function() { + var msg = "to generate blank transform objects (2)"; - expect(gd.data[0].transforms[0]).toBe(transform0, msg); - expect(gd.data[0].transforms[1]).toBe(transform1, msg); - expect(gd._fullData[0].y).toEqual([2], msg); + expect(gd.data[0].transforms[0]).toBe(transform0, msg); + expect(gd.data[0].transforms[1]).toBe(transform1, msg); + expect(gd._fullData[0].y).toEqual([2], msg); - return Plotly.restyle(gd, 'transforms[0]', null); - }) - .then(function() { - var msg = 'to remove transform objects'; + return Plotly.restyle(gd, "transforms[0]", null); + }) + .then(function() { + var msg = "to remove transform objects"; - expect(gd.data[0].transforms[0]).toBe(transform1, msg); - expect(gd.data[0].transforms[1]).toBeUndefined(msg); - expect(gd._fullData[0].y).toEqual([2], msg); - expect(gd._fullData[1].y).toEqual([1, 2], msg); + expect(gd.data[0].transforms[0]).toBe(transform1, msg); + expect(gd.data[0].transforms[1]).toBeUndefined(msg); + expect(gd._fullData[0].y).toEqual([2], msg); + expect(gd._fullData[1].y).toEqual([1, 2], msg); - return Plotly.restyle(gd, 'transforms', null); - }) - .then(function() { - var msg = 'to remove all transform objects'; - - expect(gd.data[0].transforms).toBeUndefined(msg); - expect(gd._fullData[0].y).toEqual([2, 1, 2], msg); - }) - .then(done); - }); + return Plotly.restyle(gd, "transforms", null); + }) + .then(function() { + var msg = "to remove all transform objects"; + expect(gd.data[0].transforms).toBeUndefined(msg); + expect(gd._fullData[0].y).toEqual([2, 1, 2], msg); + }) + .then(done); + }); }); diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index 949aaaa14c3..a76f082ceef 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -1,268 +1,350 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); var Plots = Plotly.Plots; -var plotApiHelpers = require('@src/plot_api/helpers'); +var plotApiHelpers = require("@src/plot_api/helpers"); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); -var delay = require('../assets/delay'); -var mock = require('@mocks/animation'); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); +var fail = require("../assets/fail_test"); +var delay = require("../assets/delay"); +var mock = require("@mocks/animation"); function runTests(transitionDuration) { - describe('Plots.transition (duration = ' + transitionDuration + ')', function() { - 'use strict'; - - var gd; - - beforeEach(function(done) { - gd = createGraphDiv(); - - var mockCopy = Lib.extendDeep({}, mock); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); - - it('resolves only once the transition has completed', function(done) { - var t1 = Date.now(); - var traces = plotApiHelpers.coerceTraceIndices(gd, null); - - Plots.transition(gd, null, {'xaxis.range': [0.2, 0.3]}, traces, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}) - .then(delay(20)) - .then(function() { - expect(Date.now() - t1).toBeGreaterThan(transitionDuration); - }).catch(fail).then(done); + describe( + "Plots.transition (duration = " + transitionDuration + ")", + function() { + "use strict"; + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + + var mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("resolves only once the transition has completed", function(done) { + var t1 = Date.now(); + var traces = plotApiHelpers.coerceTraceIndices(gd, null); + + Plots.transition( + gd, + null, + { "xaxis.range": [0.2, 0.3] }, + traces, + { redraw: true }, + { duration: transitionDuration, easing: "cubic-in-out" } + ) + .then(delay(20)) + .then(function() { + expect(Date.now() - t1).toBeGreaterThan(transitionDuration); + }) + .catch(fail) + .then(done); + }); + + it("emits plotly_transitioning on transition start", function(done) { + var beginTransitionCnt = 0; + var traces = plotApiHelpers.coerceTraceIndices(gd, null); + + gd.on("plotly_transitioning", function() { + beginTransitionCnt++; }); - it('emits plotly_transitioning on transition start', function(done) { - var beginTransitionCnt = 0; - var traces = plotApiHelpers.coerceTraceIndices(gd, null); - - gd.on('plotly_transitioning', function() { beginTransitionCnt++; }); - - Plots.transition(gd, null, {'xaxis.range': [0.2, 0.3]}, traces, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}) - .then(delay(20)) - .then(function() { - expect(beginTransitionCnt).toBe(1); - }).catch(fail).then(done); + Plots.transition( + gd, + null, + { "xaxis.range": [0.2, 0.3] }, + traces, + { redraw: true }, + { duration: transitionDuration, easing: "cubic-in-out" } + ) + .then(delay(20)) + .then(function() { + expect(beginTransitionCnt).toBe(1); + }) + .catch(fail) + .then(done); + }); + + it("emits plotly_transitioned on transition end", function(done) { + var trEndCnt = 0; + var traces = plotApiHelpers.coerceTraceIndices(gd, null); + + gd.on("plotly_transitioned", function() { + trEndCnt++; }); - it('emits plotly_transitioned on transition end', function(done) { - var trEndCnt = 0; - var traces = plotApiHelpers.coerceTraceIndices(gd, null); - - gd.on('plotly_transitioned', function() { trEndCnt++; }); - - Plots.transition(gd, null, {'xaxis.range': [0.2, 0.3]}, traces, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}) - .then(delay(20)) - .then(function() { - expect(trEndCnt).toEqual(1); - }).catch(fail).then(done); - }); - - it('transitions an annotation', function(done) { - function annotationPosition() { - var g = gd._fullLayout._infolayer.select('.annotation').select('.annotation-text-g'); - var bBox = g.node().getBoundingClientRect(); - return [bBox.left, bBox.top]; + Plots.transition( + gd, + null, + { "xaxis.range": [0.2, 0.3] }, + traces, + { redraw: true }, + { duration: transitionDuration, easing: "cubic-in-out" } + ) + .then(delay(20)) + .then(function() { + expect(trEndCnt).toEqual(1); + }) + .catch(fail) + .then(done); + }); + + it("transitions an annotation", function(done) { + function annotationPosition() { + var g = gd._fullLayout._infolayer + .select(".annotation") + .select(".annotation-text-g"); + var bBox = g.node().getBoundingClientRect(); + return [bBox.left, bBox.top]; + } + var p1, p2; + + Plotly.relayout(gd, { annotations: [{ x: 0, y: 0, text: "test" }] }) + .then(function() { + p1 = annotationPosition(); + + return Plots.transition( + gd, + null, + { "annotations[0].x": 1, "annotations[0].y": 1 }, + [], + { redraw: true, duration: transitionDuration }, + { duration: transitionDuration, easing: "cubic-in-out" } + ); + }) + .then(function() { + p2 = annotationPosition(); + + // Ensure both coordinates have moved, i.e. that the annotation has transitioned: + expect(p1[0]).not.toEqual(p2[0]); + expect(p1[1]).not.toEqual(p2[1]); + }) + .catch(fail) + .then(done); + }); + + it("transitions an image", function(done) { + var jsLogo = "https://images.plot.ly/language-icons/api-home/js-logo.png"; + var pythonLogo = "https://images.plot.ly/language-icons/api-home/python-logo.png"; + + function imageel() { + return gd._fullLayout._imageUpperLayer.select("image").node(); + } + function imagesrc() { + return imageel().getAttribute("href"); + } + var p1, p2, e1, e2; + + Plotly.relayout(gd, { images: [{ x: 0, y: 0, source: jsLogo }] }) + .then(function() { + p1 = imagesrc(); + e1 = imageel(); + + return Plots.transition( + gd, + null, + { "images[0].source": pythonLogo }, + [], + { redraw: true, duration: transitionDuration }, + { duration: transitionDuration, easing: "cubic-in-out" } + ); + }) + .then(function() { + p2 = imagesrc(); + e2 = imageel(); + + // Test that the image src has changed: + expect(p1).not.toEqual(p2); + + // Test that the image element identity has not: + expect(e1).toBe(e2); + }) + .catch(fail) + .then(done); + }); + + it("transitions a shape", function(done) { + function getPath() { + return gd._fullLayout._shapeUpperLayer.select("path").node(); + } + var p1, p2, p3, d1, d2, d3, s1, s2, s3; + + Plotly.relayout(gd, { + shapes: [ + { + type: "circle", + xref: "x", + yref: "y", + x0: 0, + y0: 0, + x1: 2, + y1: 2, + opacity: 0.2, + fillcolor: "blue", + line: { color: "blue" } } - var p1, p2; - - Plotly.relayout(gd, {annotations: [{x: 0, y: 0, text: 'test'}]}).then(function() { - p1 = annotationPosition(); - - return Plots.transition(gd, null, { - 'annotations[0].x': 1, - 'annotations[0].y': 1 - }, [], - {redraw: true, duration: transitionDuration}, - {duration: transitionDuration, easing: 'cubic-in-out'} - ); - }).then(function() { - p2 = annotationPosition(); - - // Ensure both coordinates have moved, i.e. that the annotation has transitioned: - expect(p1[0]).not.toEqual(p2[0]); - expect(p1[1]).not.toEqual(p2[1]); - - }).catch(fail).then(done); - }); - - it('transitions an image', function(done) { - var jsLogo = 'https://images.plot.ly/language-icons/api-home/js-logo.png'; - var pythonLogo = 'https://images.plot.ly/language-icons/api-home/python-logo.png'; - - function imageel() { - return gd._fullLayout._imageUpperLayer.select('image').node(); + ] + }) + .then(function() { + p1 = getPath(); + d1 = p1.getAttribute("d"); + s1 = p1.getAttribute("style"); + + return Plots.transition( + gd, + null, + { "shapes[0].x0": 1, "shapes[0].y0": 1 }, + [], + { redraw: true, duration: transitionDuration }, + { duration: transitionDuration, easing: "cubic-in-out" } + ); + }) + .then(function() { + p2 = getPath(); + d2 = p2.getAttribute("d"); + s2 = p2.getAttribute("style"); + + // If object constancy is implemented, this will then be *equal*: + expect(p1).not.toBe(p2); + expect(d1).not.toEqual(d2); + expect(s1).toEqual(s2); + + return Plots.transition( + gd, + null, + { "shapes[0].color": "red" }, + [], + { redraw: true, duration: transitionDuration }, + { duration: transitionDuration, easing: "cubic-in-out" } + ); + }) + .then(function() { + p3 = getPath(); + d3 = p3.getAttribute("d"); + s3 = p3.getAttribute("d"); + + expect(d3).toEqual(d2); + expect(s3).not.toEqual(s2); + }) + .catch(fail) + .then(done); + }); + + it("transitions a transform", function(done) { + Plotly.restyle( + gd, + { + "transforms[0]": { + enabled: true, + type: "filter", + operation: "<", + target: "x", + value: 10 } - function imagesrc() { - return imageel().getAttribute('href'); - } - var p1, p2, e1, e2; - - Plotly.relayout(gd, {images: [{x: 0, y: 0, source: jsLogo}]}).then(function() { - p1 = imagesrc(); - e1 = imageel(); - - return Plots.transition(gd, null, { - 'images[0].source': pythonLogo, - }, [], - {redraw: true, duration: transitionDuration}, - {duration: transitionDuration, easing: 'cubic-in-out'} - ); - }).then(function() { - p2 = imagesrc(); - e2 = imageel(); - - // Test that the image src has changed: - expect(p1).not.toEqual(p2); - - // Test that the image element identity has not: - expect(e1).toBe(e2); - - }).catch(fail).then(done); + }, + [0] + ) + .then(function() { + expect(gd._fullData[0].transforms).toEqual([ + jasmine.objectContaining({ + enabled: true, + type: "filter", + operation: "<", + target: "x", + value: 10 + }) + ]); + + return Plots.transition( + gd, + [{ "transforms[0].operation": ">" }], + null, + [0], + { redraw: true, duration: transitionDuration }, + { duration: transitionDuration, easing: "cubic-in-out" } + ); + }) + .then(function() { + expect(gd._fullData[0].transforms).toEqual([ + jasmine.objectContaining({ + enabled: true, + type: "filter", + operation: ">", + target: "x", + value: 10 + }) + ]); + }) + .catch(fail) + .then(done); + }); + + // This doesn't really test anything that the above tests don't cover, but it combines + // the behavior and attempts to ensure chaining and events happen in the correct order. + it("transitions may be chained", function(done) { + var currentlyRunning = 0; + var beginCnt = 0; + var endCnt = 0; + + gd.on("plotly_transitioning", function() { + currentlyRunning++; + beginCnt++; }); + gd.on("plotly_transitioned", function() { + currentlyRunning--; - it('transitions a shape', function(done) { - function getPath() { - return gd._fullLayout._shapeUpperLayer.select('path').node(); - } - var p1, p2, p3, d1, d2, d3, s1, s2, s3; - - Plotly.relayout(gd, { - shapes: [{ - type: 'circle', - xref: 'x', - yref: 'y', - x0: 0, - y0: 0, - x1: 2, - y1: 2, - opacity: 0.2, - fillcolor: 'blue', - line: {color: 'blue'} - }] - }).then(function() { - p1 = getPath(); - d1 = p1.getAttribute('d'); - s1 = p1.getAttribute('style'); - - return Plots.transition(gd, null, { - 'shapes[0].x0': 1, - 'shapes[0].y0': 1, - }, [], - {redraw: true, duration: transitionDuration}, - {duration: transitionDuration, easing: 'cubic-in-out'} - ); - }).then(function() { - p2 = getPath(); - d2 = p2.getAttribute('d'); - s2 = p2.getAttribute('style'); - - // If object constancy is implemented, this will then be *equal*: - expect(p1).not.toBe(p2); - expect(d1).not.toEqual(d2); - expect(s1).toEqual(s2); - - return Plots.transition(gd, null, { - 'shapes[0].color': 'red' - }, [], - {redraw: true, duration: transitionDuration}, - {duration: transitionDuration, easing: 'cubic-in-out'} - ); - }).then(function() { - p3 = getPath(); - d3 = p3.getAttribute('d'); - s3 = p3.getAttribute('d'); - - expect(d3).toEqual(d2); - expect(s3).not.toEqual(s2); - }).catch(fail).then(done); + endCnt++; }); - - it('transitions a transform', function(done) { - Plotly.restyle(gd, { - 'transforms[0]': { - enabled: true, - type: 'filter', - operation: '<', - target: 'x', - value: 10 - } - }, [0]).then(function() { - expect(gd._fullData[0].transforms).toEqual([jasmine.objectContaining({ - enabled: true, - type: 'filter', - operation: '<', - target: 'x', - value: 10 - })]); - - return Plots.transition(gd, [{ - 'transforms[0].operation': '>' - }], null, [0], - {redraw: true, duration: transitionDuration}, - {duration: transitionDuration, easing: 'cubic-in-out'} - ); - }).then(function() { - expect(gd._fullData[0].transforms).toEqual([jasmine.objectContaining({ - enabled: true, - type: 'filter', - operation: '>', - target: 'x', - value: 10 - })]); - }).catch(fail).then(done); - }); - - // This doesn't really test anything that the above tests don't cover, but it combines - // the behavior and attempts to ensure chaining and events happen in the correct order. - it('transitions may be chained', function(done) { - var currentlyRunning = 0; - var beginCnt = 0; - var endCnt = 0; - - gd.on('plotly_transitioning', function() { currentlyRunning++; beginCnt++; }); - gd.on('plotly_transitioned', function() { currentlyRunning--; endCnt++; }); - - function doTransition() { - var traces = plotApiHelpers.coerceTraceIndices(gd, null); - return Plots.transition(gd, [{x: [1, 2]}], null, traces, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}); - } - - function checkNoneRunning() { - expect(currentlyRunning).toEqual(0); - } - - doTransition() - .then(checkNoneRunning) - .then(doTransition) - .then(checkNoneRunning) - .then(doTransition) - .then(checkNoneRunning) - .then(delay(10)) - .then(function() { - expect(beginCnt).toEqual(3); - expect(endCnt).toEqual(3); - }) - .then(checkNoneRunning) - .catch(fail).then(done); - }); - }); + function doTransition() { + var traces = plotApiHelpers.coerceTraceIndices(gd, null); + return Plots.transition( + gd, + [{ x: [1, 2] }], + null, + traces, + { redraw: true }, + { duration: transitionDuration, easing: "cubic-in-out" } + ); + } + + function checkNoneRunning() { + expect(currentlyRunning).toEqual(0); + } + + doTransition() + .then(checkNoneRunning) + .then(doTransition) + .then(checkNoneRunning) + .then(doTransition) + .then(checkNoneRunning) + .then(delay(10)) + .then(function() { + expect(beginCnt).toEqual(3); + expect(endCnt).toEqual(3); + }) + .then(checkNoneRunning) + .catch(fail) + .then(done); + }); + } + ); } -for(var i = 0; i < 2; i++) { - var duration = i * 20; - // Run the whole set of tests twice: once with zero duration and once with - // nonzero duration since the behavior should be identical, but there's a - // very real possibility of race conditions or other timing issues. - // - // And of course, remember to put the async loop in a closure: - runTests(duration); +for (var i = 0; i < 2; i++) { + var duration = i * 20; + // Run the whole set of tests twice: once with zero duration and once with + // nonzero duration since the behavior should be identical, but there's a + // very real possibility of race conditions or other timing issues. + // + // And of course, remember to put the async loop in a closure: + runTests(duration); } diff --git a/test/jasmine/tests/updatemenus_test.js b/test/jasmine/tests/updatemenus_test.js index f57635e0d3f..f08ef7844bd 100644 --- a/test/jasmine/tests/updatemenus_test.js +++ b/test/jasmine/tests/updatemenus_test.js @@ -1,1207 +1,1248 @@ -var UpdateMenus = require('@src/components/updatemenus'); -var constants = require('@src/components/updatemenus/constants'); - -var d3 = require('d3'); -var Plotly = require('@lib'); -var Lib = require('@src/lib'); -var Events = require('@src/lib/events'); -var Drawing = require('@src/components/drawing'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); +var UpdateMenus = require("@src/components/updatemenus"); +var constants = require("@src/components/updatemenus/constants"); + +var d3 = require("d3"); +var Plotly = require("@lib"); +var Lib = require("@src/lib"); +var Events = require("@src/lib/events"); +var Drawing = require("@src/components/drawing"); +var createGraphDiv = require("../assets/create_graph_div"); +var destroyGraphDiv = require("../assets/destroy_graph_div"); var TRANSITION_DELAY = 100; -var fail = require('../assets/fail_test'); -var getBBox = require('../assets/get_bbox'); +var fail = require("../assets/fail_test"); +var getBBox = require("../assets/get_bbox"); -describe('update menus defaults', function() { - 'use strict'; +describe("update menus defaults", function() { + "use strict"; + var supply = UpdateMenus.supplyLayoutDefaults; - var supply = UpdateMenus.supplyLayoutDefaults; + var layoutIn, layoutOut; - var layoutIn, layoutOut; + beforeEach(function() { + layoutIn = {}; + layoutOut = {}; + }); - beforeEach(function() { - layoutIn = {}; - layoutOut = {}; - }); - - it('should skip non-array containers', function() { - [null, undefined, {}, 'str', 0, false, true].forEach(function(cont) { - var msg = '- ' + JSON.stringify(cont); + it("should skip non-array containers", function() { + [null, undefined, {}, "str", 0, false, true].forEach(function(cont) { + var msg = "- " + JSON.stringify(cont); - layoutIn = { updatemenus: cont }; - layoutOut = {}; - supply(layoutIn, layoutOut); + layoutIn = { updatemenus: cont }; + layoutOut = {}; + supply(layoutIn, layoutOut); - expect(layoutIn.updatemenus).toBe(cont, msg); - expect(layoutOut.updatemenus).toEqual([], msg); - }); + expect(layoutIn.updatemenus).toBe(cont, msg); + expect(layoutOut.updatemenus).toEqual([], msg); }); + }); - it('should make non-object item visible: false', function() { - var updatemenus = [null, undefined, [], 'str', 0, false, true]; + it("should make non-object item visible: false", function() { + var updatemenus = [null, undefined, [], "str", 0, false, true]; - layoutIn = { updatemenus: updatemenus }; - layoutOut = {}; - supply(layoutIn, layoutOut); + layoutIn = { updatemenus: updatemenus }; + layoutOut = {}; + supply(layoutIn, layoutOut); - expect(layoutIn.updatemenus).toEqual(updatemenus); - - layoutOut.updatemenus.forEach(function(item, i) { - expect(item).toEqual({ - visible: false, - buttons: [], - _input: {}, - _index: i - }); - }); - }); + expect(layoutIn.updatemenus).toEqual(updatemenus); - it('should set \'visible\' to false when no buttons are present', function() { - layoutIn.updatemenus = [{ - buttons: [{ - method: 'relayout', - args: ['title', 'Hello World'] - }, { - method: 'update', - args: [ { 'marker.size': 20 }, { 'xaxis.range': [0, 10] }, [0, 1] ] - }, { - method: 'animate', - args: [ 'frame1', { transition: { duration: 500, ease: 'cubic-in-out' }}] - }] - }, { - bgcolor: 'red' - }, { - visible: false, - buttons: [{ - method: 'relayout', - args: ['title', 'Hello World'] - }] - }]; - - supply(layoutIn, layoutOut); - - expect(layoutOut.updatemenus[0].visible).toBe(true); - expect(layoutOut.updatemenus[0].active).toEqual(0); - expect(layoutOut.updatemenus[0].buttons[0].args.length).toEqual(2); - expect(layoutOut.updatemenus[0].buttons[1].args.length).toEqual(3); - expect(layoutOut.updatemenus[0].buttons[2].args.length).toEqual(2); - - expect(layoutOut.updatemenus[1].visible).toBe(false); - expect(layoutOut.updatemenus[1].active).toBeUndefined(); - - expect(layoutOut.updatemenus[2].visible).toBe(false); - expect(layoutOut.updatemenus[2].active).toBeUndefined(); + layoutOut.updatemenus.forEach(function(item, i) { + expect(item).toEqual({ + visible: false, + buttons: [], + _input: {}, + _index: i + }); }); - - it('should skip over non-object buttons', function() { - layoutIn.updatemenus = [{ - buttons: [ - null, - { - method: 'relayout', - args: ['title', 'Hello World'] - }, - 'remove' + }); + + it("should set 'visible' to false when no buttons are present", function() { + layoutIn.updatemenus = [ + { + buttons: [ + { method: "relayout", args: ["title", "Hello World"] }, + { + method: "update", + args: [{ "marker.size": 20 }, { "xaxis.range": [0, 10] }, [0, 1]] + }, + { + method: "animate", + args: [ + "frame1", + { transition: { duration: 500, ease: "cubic-in-out" } } ] - }]; - - supply(layoutIn, layoutOut); - - expect(layoutOut.updatemenus[0].buttons.length).toEqual(1); - expect(layoutOut.updatemenus[0].buttons[0]).toEqual({ - method: 'relayout', - args: ['title', 'Hello World'], - label: '', - _index: 1 - }); + } + ] + }, + { bgcolor: "red" }, + { + visible: false, + buttons: [{ method: "relayout", args: ["title", "Hello World"] }] + } + ]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.updatemenus[0].visible).toBe(true); + expect(layoutOut.updatemenus[0].active).toEqual(0); + expect(layoutOut.updatemenus[0].buttons[0].args.length).toEqual(2); + expect(layoutOut.updatemenus[0].buttons[1].args.length).toEqual(3); + expect(layoutOut.updatemenus[0].buttons[2].args.length).toEqual(2); + + expect(layoutOut.updatemenus[1].visible).toBe(false); + expect(layoutOut.updatemenus[1].active).toBeUndefined(); + + expect(layoutOut.updatemenus[2].visible).toBe(false); + expect(layoutOut.updatemenus[2].active).toBeUndefined(); + }); + + it("should skip over non-object buttons", function() { + layoutIn.updatemenus = [ + { + buttons: [ + null, + { method: "relayout", args: ["title", "Hello World"] }, + "remove" + ] + } + ]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.updatemenus[0].buttons.length).toEqual(1); + expect(layoutOut.updatemenus[0].buttons[0]).toEqual({ + method: "relayout", + args: ["title", "Hello World"], + label: "", + _index: 1 }); - - it('should skip over buttons with array \'args\' field', function() { - layoutIn.updatemenus = [{ - buttons: [{ - method: 'restyle', - }, { - method: 'relayout', - args: ['title', 'Hello World'] - }, { - method: 'relayout', - args: null - }, {}] - }]; - - supply(layoutIn, layoutOut); - - expect(layoutOut.updatemenus[0].buttons.length).toEqual(1); - expect(layoutOut.updatemenus[0].buttons[0]).toEqual({ - method: 'relayout', - args: ['title', 'Hello World'], - label: '', - _index: 1 - }); + }); + + it("should skip over buttons with array 'args' field", function() { + layoutIn.updatemenus = [ + { + buttons: [ + { method: "restyle" }, + { method: "relayout", args: ["title", "Hello World"] }, + { method: "relayout", args: null }, + {} + ] + } + ]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.updatemenus[0].buttons.length).toEqual(1); + expect(layoutOut.updatemenus[0].buttons[0]).toEqual({ + method: "relayout", + args: ["title", "Hello World"], + label: "", + _index: 1 }); + }); - it('should keep ref to input update menu container', function() { - layoutIn.updatemenus = [{ - buttons: [{ - method: 'relayout', - args: ['title', 'Hello World'] - }] - }, { - bgcolor: 'red' - }, { - visible: false, - buttons: [{ - method: 'relayout', - args: ['title', 'Hello World'] - }] - }]; - - supply(layoutIn, layoutOut); - - expect(layoutOut.updatemenus[0]._input).toBe(layoutIn.updatemenus[0]); - expect(layoutOut.updatemenus[1]._input).toBe(layoutIn.updatemenus[1]); - expect(layoutOut.updatemenus[2]._input).toBe(layoutIn.updatemenus[2]); - }); + it("should keep ref to input update menu container", function() { + layoutIn.updatemenus = [ + { buttons: [{ method: "relayout", args: ["title", "Hello World"] }] }, + { bgcolor: "red" }, + { + visible: false, + buttons: [{ method: "relayout", args: ["title", "Hello World"] }] + } + ]; - it('should default \'bgcolor\' to layout \'paper_bgcolor\'', function() { - var buttons = [{ - method: 'relayout', - args: ['title', 'Hello World'] - }]; + supply(layoutIn, layoutOut); - layoutIn.updatemenus = [{ - buttons: buttons, - }, { - bgcolor: 'red', - buttons: buttons - }]; + expect(layoutOut.updatemenus[0]._input).toBe(layoutIn.updatemenus[0]); + expect(layoutOut.updatemenus[1]._input).toBe(layoutIn.updatemenus[1]); + expect(layoutOut.updatemenus[2]._input).toBe(layoutIn.updatemenus[2]); + }); - layoutOut.paper_bgcolor = 'blue'; + it("should default 'bgcolor' to layout 'paper_bgcolor'", function() { + var buttons = [{ method: "relayout", args: ["title", "Hello World"] }]; - supply(layoutIn, layoutOut); + layoutIn.updatemenus = [ + { buttons: buttons }, + { bgcolor: "red", buttons: buttons } + ]; - expect(layoutOut.updatemenus[0].bgcolor).toEqual('blue'); - expect(layoutOut.updatemenus[1].bgcolor).toEqual('red'); - }); + layoutOut.paper_bgcolor = "blue"; - it('should default \'type\' to \'dropdown\'', function() { - layoutIn.updatemenus = [{ - buttons: [{method: 'relayout', args: ['title', 'Hello World']}] - }]; + supply(layoutIn, layoutOut); - supply(layoutIn, layoutOut); + expect(layoutOut.updatemenus[0].bgcolor).toEqual("blue"); + expect(layoutOut.updatemenus[1].bgcolor).toEqual("red"); + }); - expect(layoutOut.updatemenus[0].type).toEqual('dropdown'); - }); + it("should default 'type' to 'dropdown'", function() { + layoutIn.updatemenus = [ + { buttons: [{ method: "relayout", args: ["title", "Hello World"] }] } + ]; - it('should default \'direction\' to \'down\'', function() { - layoutIn.updatemenus = [{ - buttons: [{method: 'relayout', args: ['title', 'Hello World']}] - }]; + supply(layoutIn, layoutOut); - supply(layoutIn, layoutOut); + expect(layoutOut.updatemenus[0].type).toEqual("dropdown"); + }); - expect(layoutOut.updatemenus[0].direction).toEqual('down'); - }); + it("should default 'direction' to 'down'", function() { + layoutIn.updatemenus = [ + { buttons: [{ method: "relayout", args: ["title", "Hello World"] }] } + ]; - it('should default \'showactive\' to true', function() { - layoutIn.updatemenus = [{ - buttons: [{method: 'relayout', args: ['title', 'Hello World']}] - }]; + supply(layoutIn, layoutOut); - supply(layoutIn, layoutOut); + expect(layoutOut.updatemenus[0].direction).toEqual("down"); + }); - expect(layoutOut.updatemenus[0].showactive).toEqual(true); - }); -}); + it("should default 'showactive' to true", function() { + layoutIn.updatemenus = [ + { buttons: [{ method: "relayout", args: ["title", "Hello World"] }] } + ]; -describe('update menus buttons', function() { - var mock = require('@mocks/updatemenus_positioning.json'); - var gd; - var allMenus, buttonMenus, dropdownMenus; + supply(layoutIn, layoutOut); - beforeEach(function(done) { - gd = createGraphDiv(); + expect(layoutOut.updatemenus[0].showactive).toEqual(true); + }); +}); - // bump event max listeners to remove console warnings - Events.init(gd); - gd._internalEv.setMaxListeners(20); +describe("update menus buttons", function() { + var mock = require("@mocks/updatemenus_positioning.json"); + var gd; + var allMenus, buttonMenus, dropdownMenus; - // move update menu #2 to click on them separately - var mockCopy = Lib.extendDeep({}, mock); - mockCopy.layout.updatemenus[1].x = 1; + beforeEach(function(done) { + gd = createGraphDiv(); - allMenus = mockCopy.layout.updatemenus; - buttonMenus = allMenus.filter(function(opts) { return opts.type === 'buttons'; }); - dropdownMenus = allMenus.filter(function(opts) { return opts.type !== 'buttons'; }); + // bump event max listeners to remove console warnings + Events.init(gd); + gd._internalEv.setMaxListeners(20); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); + // move update menu #2 to click on them separately + var mockCopy = Lib.extendDeep({}, mock); + mockCopy.layout.updatemenus[1].x = 1; - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); + allMenus = mockCopy.layout.updatemenus; + buttonMenus = allMenus.filter(function(opts) { + return opts.type === "buttons"; + }); + dropdownMenus = allMenus.filter(function(opts) { + return opts.type !== "buttons"; }); - it('creates button menus', function(done) { - assertNodeCount('.' + constants.containerClassName, 1); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); - // 12 menus, but button menus don't have headers, so there are only six headers: - assertNodeCount('.' + constants.headerClassName, dropdownMenus.length); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - // Count the *total* number of buttons we expect for this mock: - var buttonCount = 0; - buttonMenus.forEach(function(menu) { buttonCount += menu.buttons.length; }); + it("creates button menus", function(done) { + assertNodeCount("." + constants.containerClassName, 1); - assertNodeCount('.' + constants.buttonClassName, buttonCount); + // 12 menus, but button menus don't have headers, so there are only six headers: + assertNodeCount("." + constants.headerClassName, dropdownMenus.length); - done(); + // Count the *total* number of buttons we expect for this mock: + var buttonCount = 0; + buttonMenus.forEach(function(menu) { + buttonCount += menu.buttons.length; }); - function assertNodeCount(query, cnt) { - expect(d3.selectAll(query).size()).toEqual(cnt); - } -}); - -describe('update menus initialization', function() { - 'use strict'; - var gd; - - beforeEach(function(done) { - gd = createGraphDiv(); - - Plotly.plot(gd, [{x: [1, 2, 3]}], { - updatemenus: [{ - buttons: [ - {method: 'restyle', args: [], label: 'first'}, - {method: 'restyle', args: [], label: 'second'}, - ] - }] - }).then(done); - }); + assertNodeCount("." + constants.buttonClassName, buttonCount); - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); + done(); + }); - it('does not set active on initial plot', function() { - expect(gd.layout.updatemenus[0].active).toBeUndefined(); - }); + function assertNodeCount(query, cnt) { + expect(d3.selectAll(query).size()).toEqual(cnt); + } }); -describe('update menus interactions', function() { - 'use strict'; - - var mock = require('@mocks/updatemenus.json'), - bgColor = 'rgb(255, 255, 255)', - activeColor = 'rgb(244, 250, 255)'; +describe("update menus initialization", function() { + "use strict"; + var gd; - var gd; + beforeEach(function(done) { + gd = createGraphDiv(); - beforeEach(function(done) { - gd = createGraphDiv(); - - // move update menu #2 to click on them separately - var mockCopy = Lib.extendDeep({}, mock); - mockCopy.layout.updatemenus[1].x = 1; - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); + Plotly.plot(gd, [{ x: [1, 2, 3] }], { + updatemenus: [ + { + buttons: [ + { method: "restyle", args: [], label: "first" }, + { method: "restyle", args: [], label: "second" } + ] + } + ] + }).then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("does not set active on initial plot", function() { + expect(gd.layout.updatemenus[0].active).toBeUndefined(); + }); +}); - it('should draw only visible menus', function(done) { +describe("update menus interactions", function() { + "use strict"; + var mock = require("@mocks/updatemenus.json"), + bgColor = "rgb(255, 255, 255)", + activeColor = "rgb(244, 250, 255)"; + + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + + // move update menu #2 to click on them separately + var mockCopy = Lib.extendDeep({}, mock); + mockCopy.layout.updatemenus[1].x = 1; + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("should draw only visible menus", function(done) { + assertMenus([0, 0]); + expect(gd._fullLayout._pushmargin["updatemenu-0"]).toBeDefined(); + expect(gd._fullLayout._pushmargin["updatemenu-1"]).toBeDefined(); + + Plotly.relayout(gd, "updatemenus[0].visible", false) + .then(function() { + assertMenus([0]); + expect(gd._fullLayout._pushmargin["updatemenu-0"]).toBeUndefined(); + expect(gd._fullLayout._pushmargin["updatemenu-1"]).toBeDefined(); + + return Plotly.relayout(gd, "updatemenus[1]", null); + }) + .then(function() { + assertNodeCount("." + constants.containerClassName, 0); + expect(gd._fullLayout._pushmargin["updatemenu-0"]).toBeUndefined(); + expect(gd._fullLayout._pushmargin["updatemenu-1"]).toBeUndefined(); + + return Plotly.relayout(gd, { + "updatemenus[0].visible": true, + "updatemenus[1].visible": true + }); + }) + .then(function() { assertMenus([0, 0]); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeDefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeDefined(); - - Plotly.relayout(gd, 'updatemenus[0].visible', false).then(function() { - assertMenus([0]); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeDefined(); - - return Plotly.relayout(gd, 'updatemenus[1]', null); - }) - .then(function() { - assertNodeCount('.' + constants.containerClassName, 0); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeUndefined(); - - return Plotly.relayout(gd, { - 'updatemenus[0].visible': true, - 'updatemenus[1].visible': true - }); - }) - .then(function() { - assertMenus([0, 0]); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeDefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeDefined(); - - return Plotly.relayout(gd, { - 'updatemenus[0].visible': false, - 'updatemenus[1].visible': false - }); - }) - .then(function() { - assertNodeCount('.' + constants.containerClassName, 0); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeUndefined(); - - return Plotly.relayout(gd, { - 'updatemenus[2]': { - buttons: [{ - method: 'relayout', - args: ['title', 'new title'] - }] - } - }); - }) - .then(function() { - assertMenus([0]); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-2']).toBeDefined(); - - return Plotly.relayout(gd, 'updatemenus[0].visible', true); - }) - .then(function() { - assertMenus([0, 0]); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeDefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-2']).toBeDefined(); - expect(gd.layout.updatemenus.length).toEqual(3); - - return Plotly.relayout(gd, 'updatemenus[0]', null); - }) - .then(function() { - assertMenus([0]); - expect(gd.layout.updatemenus.length).toEqual(2); - - return Plotly.relayout(gd, 'updatemenus', null); - }) - .then(function() { - expect(gd.layout.updatemenus).toBeUndefined(); - - }) - .then(done); - }); + expect(gd._fullLayout._pushmargin["updatemenu-0"]).toBeDefined(); + expect(gd._fullLayout._pushmargin["updatemenu-1"]).toBeDefined(); - it('should drop/fold buttons when clicking on header', function(done) { - var header0 = selectHeader(0), - header1 = selectHeader(1); - - click(header0).then(function() { - assertMenus([3, 0]); - return click(header0); - }).then(function() { - assertMenus([0, 0]); - return click(header1); - }).then(function() { - assertMenus([0, 4]); - return click(header1); - }).then(function() { - assertMenus([0, 0]); - return click(header0); - }).then(function() { - assertMenus([3, 0]); - return click(header1); - }).then(function() { - assertMenus([0, 4]); - return click(header0); - }).then(function() { - assertMenus([3, 0]); - done(); + return Plotly.relayout(gd, { + "updatemenus[0].visible": false, + "updatemenus[1].visible": false }); - }); - - it('should emit an event on button click', function(done) { - var clickCnt = 0; - var data = []; - gd.on('plotly_buttonclicked', function(datum) { - data.push(datum); - clickCnt++; + }) + .then(function() { + assertNodeCount("." + constants.containerClassName, 0); + expect(gd._fullLayout._pushmargin["updatemenu-0"]).toBeUndefined(); + expect(gd._fullLayout._pushmargin["updatemenu-1"]).toBeUndefined(); + + return Plotly.relayout(gd, { + "updatemenus[2]": { + buttons: [{ method: "relayout", args: ["title", "new title"] }] + } }); - - click(selectHeader(0)).then(function() { - expect(clickCnt).toEqual(0); - - return click(selectButton(2)); - }).then(function() { - expect(clickCnt).toEqual(1); - expect(data.length).toEqual(1); - expect(data[0].active).toEqual(2); - - return click(selectButton(1)); - }).then(function() { - expect(clickCnt).toEqual(2); - expect(data.length).toEqual(2); - expect(data[1].active).toEqual(1); - }).catch(fail).then(done); + }) + .then(function() { + assertMenus([0]); + expect(gd._fullLayout._pushmargin["updatemenu-0"]).toBeUndefined(); + expect(gd._fullLayout._pushmargin["updatemenu-1"]).toBeUndefined(); + expect(gd._fullLayout._pushmargin["updatemenu-2"]).toBeDefined(); + + return Plotly.relayout(gd, "updatemenus[0].visible", true); + }) + .then(function() { + assertMenus([0, 0]); + expect(gd._fullLayout._pushmargin["updatemenu-0"]).toBeDefined(); + expect(gd._fullLayout._pushmargin["updatemenu-1"]).toBeUndefined(); + expect(gd._fullLayout._pushmargin["updatemenu-2"]).toBeDefined(); + expect(gd.layout.updatemenus.length).toEqual(3); + + return Plotly.relayout(gd, "updatemenus[0]", null); + }) + .then(function() { + assertMenus([0]); + expect(gd.layout.updatemenus.length).toEqual(2); + + return Plotly.relayout(gd, "updatemenus", null); + }) + .then(function() { + expect(gd.layout.updatemenus).toBeUndefined(); + }) + .then(done); + }); + + it("should drop/fold buttons when clicking on header", function(done) { + var header0 = selectHeader(0), header1 = selectHeader(1); + + click(header0) + .then(function() { + assertMenus([3, 0]); + return click(header0); + }) + .then(function() { + assertMenus([0, 0]); + return click(header1); + }) + .then(function() { + assertMenus([0, 4]); + return click(header1); + }) + .then(function() { + assertMenus([0, 0]); + return click(header0); + }) + .then(function() { + assertMenus([3, 0]); + return click(header1); + }) + .then(function() { + assertMenus([0, 4]); + return click(header0); + }) + .then(function() { + assertMenus([3, 0]); + done(); + }); + }); + + it("should emit an event on button click", function(done) { + var clickCnt = 0; + var data = []; + gd.on("plotly_buttonclicked", function(datum) { + data.push(datum); + clickCnt++; }); - it('should apply update on button click', function(done) { - var header0 = selectHeader(0), - header1 = selectHeader(1); + click(selectHeader(0)) + .then(function() { + expect(clickCnt).toEqual(0); + + return click(selectButton(2)); + }) + .then(function() { + expect(clickCnt).toEqual(1); + expect(data.length).toEqual(1); + expect(data[0].active).toEqual(2); + + return click(selectButton(1)); + }) + .then(function() { + expect(clickCnt).toEqual(2); + expect(data.length).toEqual(2); + expect(data[1].active).toEqual(1); + }) + .catch(fail) + .then(done); + }); + + it("should apply update on button click", function(done) { + var header0 = selectHeader(0), header1 = selectHeader(1); + + assertActive(gd, [1, 2]); + + click(header0) + .then(function() { + assertItemColor(selectButton(1), activeColor); + + return click(selectButton(0)); + }) + .then(function() { + assertActive(gd, [0, 2]); + + return click(header1); + }) + .then(function() { + assertItemColor(selectButton(2), activeColor); + + return click(selectButton(0)); + }) + .then(function() { + assertActive(gd, [0, 0]); - assertActive(gd, [1, 2]); - - click(header0).then(function() { - assertItemColor(selectButton(1), activeColor); - - return click(selectButton(0)); - }).then(function() { - assertActive(gd, [0, 2]); + done(); + }); + }); + + it("should update correctly on failed binding comparisons", function(done) { + // See https://github.com/plotly/plotly.js/issues/1169 + // for more info. + var data = [ + { y: [1, 2, 3], visible: true }, + { y: [2, 3, 1], visible: false }, + { y: [3, 1, 2], visible: false } + ]; + + var layout = { + updatemenus: [ + { + buttons: [ + { + label: "a", + method: "restyle", + args: ["visible", [true, false, false]] + }, + { + label: "b", + method: "restyle", + args: ["visible", [false, true, false]] + }, + { + label: "c", + method: "restyle", + args: ["visible", [false, false, true]] + } + ] + } + ] + }; - return click(header1); - }).then(function() { - assertItemColor(selectButton(2), activeColor); + Plotly.newPlot(gd, data, layout) + .then(function() { + return click(selectHeader(0)); + }) + .then(function() { + return click(selectButton(1)); + }) + .then(function() { + assertActive(gd, [1]); + }) + .then(done); + }); + + it("should change color on mouse over", function(done) { + var INDEX_0 = 2, INDEX_1 = gd.layout.updatemenus[1].active; + + var header0 = selectHeader(0); + + assertItemColor(header0, bgColor); + mouseEvent("mouseover", header0); + assertItemColor(header0, activeColor); + mouseEvent("mouseout", header0); + assertItemColor(header0, bgColor); + + click(header0) + .then(function() { + var button = selectButton(INDEX_0); + + assertItemColor(button, bgColor); + mouseEvent("mouseover", button); + assertItemColor(button, activeColor); + mouseEvent("mouseout", button); + assertItemColor(button, bgColor); + + return click(selectHeader(1)); + }) + .then(function() { + var button = selectButton(INDEX_1); + + assertItemColor(button, activeColor); + mouseEvent("mouseover", button); + assertItemColor(button, activeColor); + mouseEvent("mouseout", button); + assertItemColor(button, activeColor); - return click(selectButton(0)); - }).then(function() { - assertActive(gd, [0, 0]); + done(); + }); + }); + + it("should relayout", function(done) { + assertItemColor(selectHeader(0), "rgb(255, 255, 255)"); + assertItemDims(selectHeader(1), 95, 33); + + Plotly.relayout(gd, "updatemenus[0].bgcolor", "red") + .then(function() { + assertItemColor(selectHeader(0), "rgb(255, 0, 0)"); + + return click(selectHeader(0)); + }) + .then(function() { + assertMenus([3, 0]); + + return Plotly.relayout(gd, "updatemenus[0].bgcolor", "blue"); + }) + .then(function() { + // and keep menu dropped + assertMenus([3, 0]); + assertItemColor(selectHeader(0), "rgb(0, 0, 255)"); + + return Plotly.relayout( + gd, + "updatemenus[1].buttons[1].label", + "a looooooooooooong
label" + ); + }) + .then(function() { + assertItemDims(selectHeader(1), 179, 35); + + return click(selectHeader(1)); + }) + .then(function() { + assertMenus([0, 4]); + + return Plotly.relayout(gd, "updatemenus[1].visible", false); + }) + .then(function() { + // and delete buttons + assertMenus([0]); + + return click(selectHeader(0)); + }) + .then(function() { + assertMenus([3]); + + return Plotly.relayout(gd, "updatemenus[1].visible", true); + }) + .then(function() { + // fold up buttons whenever new menus are added + assertMenus([0, 0]); - done(); + return Plotly.relayout(gd, { + "updatemenus[0].bgcolor": null, + paper_bgcolor: "black" }); - }); + }) + .then(function() { + assertItemColor(selectHeader(0), "rgb(0, 0, 0)"); + assertItemColor(selectHeader(1), "rgb(0, 0, 0)"); - it('should update correctly on failed binding comparisons', function(done) { - - // See https://github.com/plotly/plotly.js/issues/1169 - // for more info. - - var data = [{ - y: [1, 2, 3], - visible: true - }, { - y: [2, 3, 1], - visible: false - }, { - y: [3, 1, 2], - visible: false - }]; - - var layout = { - updatemenus: [{ - buttons: [{ - label: 'a', - method: 'restyle', - args: ['visible', [true, false, false]] - }, { - label: 'b', - method: 'restyle', - args: ['visible', [false, true, false]] - }, { - label: 'c', - method: 'restyle', - args: ['visible', [false, false, true]] - }] - }] - }; - - Plotly.newPlot(gd, data, layout).then(function() { - return click(selectHeader(0)); - }) - .then(function() { - return click(selectButton(1)); - }) - .then(function() { - assertActive(gd, [1]); - }) - .then(done); - }); - - it('should change color on mouse over', function(done) { - var INDEX_0 = 2, - INDEX_1 = gd.layout.updatemenus[1].active; - - var header0 = selectHeader(0); - - assertItemColor(header0, bgColor); - mouseEvent('mouseover', header0); - assertItemColor(header0, activeColor); - mouseEvent('mouseout', header0); - assertItemColor(header0, bgColor); - - click(header0).then(function() { - var button = selectButton(INDEX_0); - - assertItemColor(button, bgColor); - mouseEvent('mouseover', button); - assertItemColor(button, activeColor); - mouseEvent('mouseout', button); - assertItemColor(button, bgColor); - - return click(selectHeader(1)); - }).then(function() { - var button = selectButton(INDEX_1); - - assertItemColor(button, activeColor); - mouseEvent('mouseover', button); - assertItemColor(button, activeColor); - mouseEvent('mouseout', button); - assertItemColor(button, activeColor); - - done(); + done(); + }); + }); + + it("applies padding on all sides", function(done) { + var xy1, xy2; + var firstMenu = d3.select("." + constants.headerGroupClassName); + var xpad = 80; + var ypad = 60; + + // Position it center-anchored and in the middle of the plot: + Plotly.relayout(gd, { + "updatemenus[0].x": 0.2, + "updatemenus[0].y": 0.5, + "updatemenus[0].xanchor": "center", + "updatemenus[0].yanchor": "middle" + }) + .then(function() { + // Convert to xy: + xy1 = firstMenu + .attr("transform") + .match(/translate\(([^,]*),\s*([^\)]*)\)/) + .slice(1) + .map(parseFloat); + + // Set three of four paddings. This should move it. + return Plotly.relayout(gd, { + "updatemenus[0].pad.t": ypad, + "updatemenus[0].pad.r": xpad, + "updatemenus[0].pad.b": ypad, + "updatemenus[0].pad.l": xpad }); + }) + .then(function() { + xy2 = firstMenu + .attr("transform") + .match(/translate\(([^,]*),\s*([^\)]*)\)/) + .slice(1) + .map(parseFloat); + + expect(xy1[0] - xy2[0]).toEqual(xpad); + expect(xy1[1] - xy2[1]).toEqual(ypad); + }) + .catch(fail) + .then(done); + }); + + it("applies y padding on relayout", function(done) { + var x1, x2; + var firstMenu = d3.select("." + constants.headerGroupClassName); + var padShift = 40; + + // Position the menu in the center of the plot horizontal so that + // we can test padding updates without worrying about margin pushing. + Plotly.relayout(gd, { "updatemenus[0].x": 0.5, "updatemenus[0].pad.r": 0 }) + .then(function() { + // Extract the x-component of the translation: + x1 = parseInt( + firstMenu.attr("transform").match(/translate\(([^,]*).*/)[1] + ); + + return Plotly.relayout(gd, "updatemenus[0].pad.r", 40); + }) + .then(function() { + // Extract the x-component of the translation: + x2 = parseInt( + firstMenu.attr("transform").match(/translate\(([^,]*).*/)[1] + ); + + expect(x1 - x2).toBeCloseTo(padShift, 1); + }) + .catch(fail) + .then(done); + }); + + function assertNodeCount(query, cnt) { + expect(d3.selectAll(query).size()).toEqual(cnt); + } + + // call assertMenus([0, 3]); to check that the 2nd update menu is dropped + // and showing 3 buttons. + function assertMenus(expectedMenus) { + assertNodeCount("." + constants.containerClassName, 1); + assertNodeCount("." + constants.headerClassName, expectedMenus.length); + + var gButton = d3.select("." + constants.dropdownButtonGroupClassName), + actualActiveIndex = +gButton.attr(constants.menuIndexAttrName), + hasActive = false; + + expectedMenus.forEach(function(expected, i) { + if (expected) { + expect(actualActiveIndex).toEqual(i); + assertNodeCount("." + constants.dropdownButtonClassName, expected); + hasActive = true; + } }); - it('should relayout', function(done) { - assertItemColor(selectHeader(0), 'rgb(255, 255, 255)'); - assertItemDims(selectHeader(1), 95, 33); - - Plotly.relayout(gd, 'updatemenus[0].bgcolor', 'red').then(function() { - assertItemColor(selectHeader(0), 'rgb(255, 0, 0)'); - - return click(selectHeader(0)); - }).then(function() { - assertMenus([3, 0]); - - return Plotly.relayout(gd, 'updatemenus[0].bgcolor', 'blue'); - }).then(function() { - // and keep menu dropped - assertMenus([3, 0]); - assertItemColor(selectHeader(0), 'rgb(0, 0, 255)'); - - return Plotly.relayout(gd, 'updatemenus[1].buttons[1].label', 'a looooooooooooong
label'); - }).then(function() { - assertItemDims(selectHeader(1), 179, 35); - - return click(selectHeader(1)); - }).then(function() { - assertMenus([0, 4]); - - return Plotly.relayout(gd, 'updatemenus[1].visible', false); - }).then(function() { - // and delete buttons - assertMenus([0]); - - return click(selectHeader(0)); - }).then(function() { - assertMenus([3]); - - return Plotly.relayout(gd, 'updatemenus[1].visible', true); - }).then(function() { - // fold up buttons whenever new menus are added - assertMenus([0, 0]); - - return Plotly.relayout(gd, { - 'updatemenus[0].bgcolor': null, - 'paper_bgcolor': 'black' - }); - }).then(function() { - assertItemColor(selectHeader(0), 'rgb(0, 0, 0)'); - assertItemColor(selectHeader(1), 'rgb(0, 0, 0)'); - - done(); - }); - }); + if (!hasActive) { + expect(actualActiveIndex).toEqual(-1); + assertNodeCount("." + constants.dropdownButtonClassName, 0); + } + } - it('applies padding on all sides', function(done) { - var xy1, xy2; - var firstMenu = d3.select('.' + constants.headerGroupClassName); - var xpad = 80; - var ypad = 60; - - // Position it center-anchored and in the middle of the plot: - Plotly.relayout(gd, { - 'updatemenus[0].x': 0.2, - 'updatemenus[0].y': 0.5, - 'updatemenus[0].xanchor': 'center', - 'updatemenus[0].yanchor': 'middle', - }).then(function() { - // Convert to xy: - xy1 = firstMenu.attr('transform').match(/translate\(([^,]*),\s*([^\)]*)\)/).slice(1).map(parseFloat); - - // Set three of four paddings. This should move it. - return Plotly.relayout(gd, { - 'updatemenus[0].pad.t': ypad, - 'updatemenus[0].pad.r': xpad, - 'updatemenus[0].pad.b': ypad, - 'updatemenus[0].pad.l': xpad, - }); - }).then(function() { - xy2 = firstMenu.attr('transform').match(/translate\(([^,]*),\s*([^\)]*)\)/).slice(1).map(parseFloat); - - expect(xy1[0] - xy2[0]).toEqual(xpad); - expect(xy1[1] - xy2[1]).toEqual(ypad); - }).catch(fail).then(done); + function assertActive(gd, expectedMenus) { + expectedMenus.forEach(function(expected, i) { + expect(gd.layout.updatemenus[i].active).toEqual(expected); + expect(gd._fullLayout.updatemenus[i].active).toEqual(expected); }); - - it('applies y padding on relayout', function(done) { - var x1, x2; - var firstMenu = d3.select('.' + constants.headerGroupClassName); - var padShift = 40; - - // Position the menu in the center of the plot horizontal so that - // we can test padding updates without worrying about margin pushing. - Plotly.relayout(gd, { - 'updatemenus[0].x': 0.5, - 'updatemenus[0].pad.r': 0, - }).then(function() { - // Extract the x-component of the translation: - x1 = parseInt(firstMenu.attr('transform').match(/translate\(([^,]*).*/)[1]); - - return Plotly.relayout(gd, 'updatemenus[0].pad.r', 40); - }).then(function() { - // Extract the x-component of the translation: - x2 = parseInt(firstMenu.attr('transform').match(/translate\(([^,]*).*/)[1]); - - expect(x1 - x2).toBeCloseTo(padShift, 1); - }).catch(fail).then(done); + } + + function assertItemColor(node, color) { + var rect = node.select("rect"); + expect(rect.style("fill")).toEqual(color); + } + + function assertItemDims(node, width, height) { + var rect = node.select("rect"), actualWidth = +rect.attr("width"); + + // must compare with a tolerance as the exact result + // is browser/font dependent (via getBBox) + expect(Math.abs(actualWidth - width)).toBeLessThan(16); + + // height is determined by 'fontsize', + // so no such tolerance is needed + expect(+rect.attr("height")).toEqual(height); + } + + function click(selection) { + return new Promise(function(resolve) { + setTimeout( + function() { + mouseEvent("click", selection); + resolve(); + }, + TRANSITION_DELAY + ); }); + } + + // For some reason, ../assets/mouse_event.js fails + // to detect the button elements in FF38 (like on CircleCI 2016/08/02), + // so dispatch the mouse event directly about the nodes instead. + function mouseEvent(type, selection) { + var ev = new window.MouseEvent(type, { bubbles: true }); + selection.node().dispatchEvent(ev); + } + + function selectHeader(menuIndex) { + var headers = d3.selectAll("." + constants.headerClassName), + header = d3.select(headers[0][menuIndex]); + return header; + } + + function selectButton(buttonIndex) { + var buttons = d3.selectAll("." + constants.dropdownButtonClassName), + button = d3.select(buttons[0][buttonIndex]); + return button; + } +}); - function assertNodeCount(query, cnt) { - expect(d3.selectAll(query).size()).toEqual(cnt); - } - - // call assertMenus([0, 3]); to check that the 2nd update menu is dropped - // and showing 3 buttons. - function assertMenus(expectedMenus) { - assertNodeCount('.' + constants.containerClassName, 1); - assertNodeCount('.' + constants.headerClassName, expectedMenus.length); - - var gButton = d3.select('.' + constants.dropdownButtonGroupClassName), - actualActiveIndex = +gButton.attr(constants.menuIndexAttrName), - hasActive = false; - - expectedMenus.forEach(function(expected, i) { - if(expected) { - expect(actualActiveIndex).toEqual(i); - assertNodeCount('.' + constants.dropdownButtonClassName, expected); - hasActive = true; +describe("update menus interaction with other components:", function() { + "use strict"; + afterEach(destroyGraphDiv); + + it("buttons show be drawn above sliders", function(done) { + Plotly.plot(createGraphDiv(), [{ x: [1, 2, 3], y: [1, 2, 1] }], { + sliders: [ + { + xanchor: "right", + x: -0.05, + y: 0.9, + len: 0.3, + steps: [ + { + label: "red", + method: "restyle", + args: [{ "line.color": "red" }] + }, + { + label: "orange", + method: "restyle", + args: [{ "line.color": "orange" }] + }, + { + label: "yellow", + method: "restyle", + args: [{ "line.color": "yellow" }] } - }); - - if(!hasActive) { - expect(actualActiveIndex).toEqual(-1); - assertNodeCount('.' + constants.dropdownButtonClassName, 0); + ] } - } - - function assertActive(gd, expectedMenus) { - expectedMenus.forEach(function(expected, i) { - expect(gd.layout.updatemenus[i].active).toEqual(expected); - expect(gd._fullLayout.updatemenus[i].active).toEqual(expected); - }); - } - - function assertItemColor(node, color) { - var rect = node.select('rect'); - expect(rect.style('fill')).toEqual(color); - } - - function assertItemDims(node, width, height) { - var rect = node.select('rect'), - actualWidth = +rect.attr('width'); - - // must compare with a tolerance as the exact result - // is browser/font dependent (via getBBox) - expect(Math.abs(actualWidth - width)).toBeLessThan(16); - - // height is determined by 'fontsize', - // so no such tolerance is needed - expect(+rect.attr('height')).toEqual(height); - } - - function click(selection) { - return new Promise(function(resolve) { - setTimeout(function() { - mouseEvent('click', selection); - resolve(); - }, TRANSITION_DELAY); + ], + updatemenus: [ + { + buttons: [ + { + label: "markers and lines", + method: "restyle", + args: [{ mode: "markers+lines" }] + }, + { + label: "markers", + method: "restyle", + args: [{ mode: "markers" }] + }, + { label: "lines", method: "restyle", args: [{ mode: "lines" }] } + ] + } + ] + }) + .then(function() { + var infoLayer = d3.select("g.infolayer"); + var containerClassNames = ["slider-container", "updatemenu-container"]; + var list = []; + + infoLayer.selectAll("*").each(function() { + var className = d3.select(this).attr("class"); + + if (containerClassNames.indexOf(className) !== -1) { + list.push(className); + } }); - } - - // For some reason, ../assets/mouse_event.js fails - // to detect the button elements in FF38 (like on CircleCI 2016/08/02), - // so dispatch the mouse event directly about the nodes instead. - function mouseEvent(type, selection) { - var ev = new window.MouseEvent(type, { bubbles: true }); - selection.node().dispatchEvent(ev); - } - - function selectHeader(menuIndex) { - var headers = d3.selectAll('.' + constants.headerClassName), - header = d3.select(headers[0][menuIndex]); - return header; - } - function selectButton(buttonIndex) { - var buttons = d3.selectAll('.' + constants.dropdownButtonClassName), - button = d3.select(buttons[0][buttonIndex]); - return button; - } -}); - - -describe('update menus interaction with other components:', function() { - 'use strict'; - - afterEach(destroyGraphDiv); - - it('buttons show be drawn above sliders', function(done) { - - Plotly.plot(createGraphDiv(), [{ - x: [1, 2, 3], - y: [1, 2, 1] - }], { - sliders: [{ - xanchor: 'right', - x: -0.05, - y: 0.9, - len: 0.3, - steps: [{ - label: 'red', - method: 'restyle', - args: [{'line.color': 'red'}] - }, { - label: 'orange', - method: 'restyle', - args: [{'line.color': 'orange'}] - }, { - label: 'yellow', - method: 'restyle', - args: [{'line.color': 'yellow'}] - }] - }], - updatemenus: [{ - buttons: [{ - label: 'markers and lines', - method: 'restyle', - args: [{ 'mode': 'markers+lines' }] - }, { - label: 'markers', - method: 'restyle', - args: [{ 'mode': 'markers' }] - }, { - label: 'lines', - method: 'restyle', - args: [{ 'mode': 'lines' }] - }] - }] - }) - .then(function() { - var infoLayer = d3.select('g.infolayer'); - var containerClassNames = ['slider-container', 'updatemenu-container']; - var list = []; - - infoLayer.selectAll('*').each(function() { - var className = d3.select(this).attr('class'); - - if(containerClassNames.indexOf(className) !== -1) { - list.push(className); - } - }); - - expect(list).toEqual(containerClassNames); - }) - .then(done); - }); + expect(list).toEqual(containerClassNames); + }) + .then(done); + }); }); - -describe('update menus interaction with scrollbox:', function() { - 'use strict'; - - var gd, - mock, - menuDown, - menuLeft, - menuRight, - menuUp; - - // Adapted from https://github.com/plotly/plotly.js/pull/770#issuecomment-234669383 - mock = { - data: [], - layout: { - width: 1100, - height: 450, - updatemenus: [{ - buttons: [{ - method: 'restyle', - args: ['line.color', 'red'], - label: 'red' - }, { - method: 'restyle', - args: ['line.color', 'blue'], - label: 'blue' - }, { - method: 'restyle', - args: ['line.color', 'green'], - label: 'green' - }] - }, { - x: 0.5, - xanchor: 'left', - y: 0.5, - yanchor: 'top', - direction: 'down', - buttons: [] - }, { - x: 0.5, - xanchor: 'right', - y: 0.5, - yanchor: 'top', - direction: 'left', - buttons: [] - }, { - x: 0.5, - xanchor: 'left', - y: 0.5, - yanchor: 'bottom', - direction: 'right', - buttons: [] - }, { - x: 0.5, - xanchor: 'right', - y: 0.5, - yanchor: 'bottom', - direction: 'up', - buttons: [] - }] - } - }; - - for(var i = 0, n = 20; i < n; i++) { - var j; - - var y; - for(j = 0, y = []; j < 10; j++) y.push(Math.random); - - mock.data.push({ - y: y, - line: { - shape: 'spline', - color: 'red' +describe("update menus interaction with scrollbox:", function() { + "use strict"; + var gd, mock, menuDown, menuLeft, menuRight, menuUp; + + // Adapted from https://github.com/plotly/plotly.js/pull/770#issuecomment-234669383 + mock = { + data: [], + layout: { + width: 1100, + height: 450, + updatemenus: [ + { + buttons: [ + { method: "restyle", args: ["line.color", "red"], label: "red" }, + { + method: "restyle", + args: ["line.color", "blue"], + label: "blue" }, - visible: i === 0, - name: 'Data set ' + i, - }); - - var visible; - for(j = 0, visible = []; j < n; j++) visible.push(i === j); - - for(j = 1; j <= 4; j++) { - mock.layout.updatemenus[j].buttons.push({ - method: 'restyle', - args: ['visible', visible], - label: 'Data set ' + i - }); + { + method: "restyle", + args: ["line.color", "green"], + label: "green" + } + ] + }, + { + x: 0.5, + xanchor: "left", + y: 0.5, + yanchor: "top", + direction: "down", + buttons: [] + }, + { + x: 0.5, + xanchor: "right", + y: 0.5, + yanchor: "top", + direction: "left", + buttons: [] + }, + { + x: 0.5, + xanchor: "left", + y: 0.5, + yanchor: "bottom", + direction: "right", + buttons: [] + }, + { + x: 0.5, + xanchor: "right", + y: 0.5, + yanchor: "bottom", + direction: "up", + buttons: [] } + ] } + }; - beforeEach(function(done) { - gd = createGraphDiv(); - - var mockCopy = Lib.extendDeep({}, mock); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - var menus = document.getElementsByClassName('updatemenu-header'); + for (var i = 0, n = 20; i < n; i++) { + var j; - expect(menus.length).toBe(5); + var y; + for (j = 0, y = []; j < 10; j++) y.push(Math.random); - menuDown = menus[1]; - menuLeft = menus[2]; - menuRight = menus[3]; - menuUp = menus[4]; - }).catch(fail).then(done); + mock.data.push({ + y: y, + line: { shape: "spline", color: "red" }, + visible: i === 0, + name: "Data set " + i }); - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); - - it('scrollbox can be dragged', function() { - var deltaX = -50, - deltaY = -100, - scrollBox, - scrollBar, - scrollBoxTranslate0, - scrollBarTranslate0, - scrollBoxTranslate1, - scrollBarTranslate1; - - scrollBox = getScrollBox(); - expect(scrollBox).toBeDefined(); - - // down menu - click(menuDown); - - scrollBar = getVerticalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); - scrollBarTranslate0 = Drawing.getTranslate(scrollBar); - dragScrollBox(scrollBox, 0, deltaY); - scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); - scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - - expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y + deltaY); - expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); - - // left menu - click(menuLeft); - - scrollBar = getHorizontalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); - scrollBarTranslate0 = Drawing.getTranslate(scrollBar); - dragScrollBox(scrollBox, deltaX, 0); - scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); - scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - - expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x + deltaX); - expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); - - // right menu - click(menuRight); - - scrollBar = getHorizontalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); - scrollBarTranslate0 = Drawing.getTranslate(scrollBar); - dragScrollBox(scrollBox, deltaX, 0); - scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); - scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - - expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x + deltaX); - expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); - - // up menu - click(menuUp); - - scrollBar = getVerticalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); - scrollBarTranslate0 = Drawing.getTranslate(scrollBar); - dragScrollBox(scrollBox, 0, deltaY); - scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); - scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - - expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y + deltaY); - expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); - }); - - it('scrollbox handles wheel events', function() { - var deltaY = 100, - scrollBox, - scrollBar, - scrollBoxTranslate0, - scrollBarTranslate0, - scrollBoxTranslate1, - scrollBarTranslate1; - - scrollBox = getScrollBox(); - expect(scrollBox).toBeDefined(); + var visible; + for (j = 0, visible = []; j < n; j++) visible.push(i === j); - // down menu - click(menuDown); - - scrollBar = getVerticalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); - scrollBarTranslate0 = Drawing.getTranslate(scrollBar); - wheel(scrollBox, deltaY); - scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); - scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - - expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y - deltaY); - expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); - - // left menu - click(menuLeft); - - scrollBar = getHorizontalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); - scrollBarTranslate0 = Drawing.getTranslate(scrollBar); - wheel(scrollBox, deltaY); - scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); - scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - - expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x - deltaY); - expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); - - // right menu - click(menuRight); - - scrollBar = getHorizontalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); - scrollBarTranslate0 = Drawing.getTranslate(scrollBar); - wheel(scrollBox, deltaY); - scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); - scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - - expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x - deltaY); - expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); - - // up menu - click(menuUp); - - scrollBar = getVerticalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); - scrollBarTranslate0 = Drawing.getTranslate(scrollBar); - wheel(scrollBox, deltaY); - scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); - scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - - expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y - deltaY); - expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); - }); - - it('scrollbar can be dragged', function() { - var deltaX = 20, - deltaY = 10, - scrollBox, - scrollBar, - scrollBoxPosition0, - scrollBarPosition0, - scrollBoxPosition1, - scrollBarPosition1; - - scrollBox = getScrollBox(); - expect(scrollBox).toBeDefined(); - - // down menu - click(menuDown); - - scrollBar = getVerticalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxPosition0 = Drawing.getTranslate(scrollBox); - scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); - dragScrollBar(scrollBar, scrollBarPosition0, 0, deltaY); - scrollBoxPosition1 = Drawing.getTranslate(scrollBox); - scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); - - expect(scrollBoxPosition1.y).toBeLessThan(scrollBoxPosition0.y); - expect(scrollBarPosition1.y).toEqual(scrollBarPosition0.y + deltaY); - - // left menu - click(menuLeft); - - scrollBar = getHorizontalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxPosition0 = Drawing.getTranslate(scrollBox); - scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); - dragScrollBar(scrollBar, scrollBarPosition0, deltaX, 0); - scrollBoxPosition1 = Drawing.getTranslate(scrollBox); - scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); - - expect(scrollBoxPosition1.x).toBeLessThan(scrollBoxPosition0.x); - expect(scrollBarPosition1.x).toEqual(scrollBarPosition0.x + deltaX); - - // right menu - click(menuRight); - - scrollBar = getHorizontalScrollBar(); - expect(scrollBar).toBeDefined(); - - scrollBoxPosition0 = Drawing.getTranslate(scrollBox); - scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); - dragScrollBar(scrollBar, scrollBarPosition0, deltaX, 0); - scrollBoxPosition1 = Drawing.getTranslate(scrollBox); - scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); - - expect(scrollBoxPosition1.x).toBeLessThan(scrollBoxPosition0.x); - expect(scrollBarPosition1.x).toEqual(scrollBarPosition0.x + deltaX); - - // up menu - click(menuUp); + for (j = 1; j <= 4; j++) { + mock.layout.updatemenus[j].buttons.push({ + method: "restyle", + args: ["visible", visible], + label: "Data set " + i + }); + } + } - scrollBar = getVerticalScrollBar(); - expect(scrollBar).toBeDefined(); + beforeEach(function(done) { + gd = createGraphDiv(); + + var mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + var menus = document.getElementsByClassName("updatemenu-header"); + + expect(menus.length).toBe(5); + + menuDown = menus[1]; + menuLeft = menus[2]; + menuRight = menus[3]; + menuUp = menus[4]; + }) + .catch(fail) + .then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it("scrollbox can be dragged", function() { + var deltaX = -50, + deltaY = -100, + scrollBox, + scrollBar, + scrollBoxTranslate0, + scrollBarTranslate0, + scrollBoxTranslate1, + scrollBarTranslate1; + + scrollBox = getScrollBox(); + expect(scrollBox).toBeDefined(); + + // down menu + click(menuDown); - scrollBoxPosition0 = Drawing.getTranslate(scrollBox); - scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); - dragScrollBar(scrollBar, scrollBarPosition0, 0, deltaY); - scrollBoxPosition1 = Drawing.getTranslate(scrollBox); - scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); - expect(scrollBoxPosition1.y).toBeLessThan(scrollBoxPosition0.y); - expect(scrollBarPosition1.y).toEqual(scrollBarPosition0.y + deltaY); - }); + scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); + scrollBarTranslate0 = Drawing.getTranslate(scrollBar); + dragScrollBox(scrollBox, 0, deltaY); + scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); + scrollBarTranslate1 = Drawing.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y + deltaY); + expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); - function getScrollBox() { - return document.getElementsByClassName('updatemenu-dropdown-button-group')[0]; - } + // left menu + click(menuLeft); - function getHorizontalScrollBar() { - return document.getElementsByClassName('scrollbar-horizontal')[0]; - } + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); - function getVerticalScrollBar() { - return document.getElementsByClassName('scrollbar-vertical')[0]; - } + scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); + scrollBarTranslate0 = Drawing.getTranslate(scrollBar); + dragScrollBox(scrollBox, deltaX, 0); + scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); + scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - function getCenter(node) { - var bbox = getBBox(node), - x = bbox.x + 0.5 * bbox.width, - y = bbox.y + 0.5 * bbox.height; + expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x + deltaX); + expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); - return { x: x, y: y }; - } + // right menu + click(menuRight); + + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); + scrollBarTranslate0 = Drawing.getTranslate(scrollBar); + dragScrollBox(scrollBox, deltaX, 0); + scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); + scrollBarTranslate1 = Drawing.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x + deltaX); + expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); + + // up menu + click(menuUp); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); + scrollBarTranslate0 = Drawing.getTranslate(scrollBar); + dragScrollBox(scrollBox, 0, deltaY); + scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); + scrollBarTranslate1 = Drawing.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y + deltaY); + expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); + }); + + it("scrollbox handles wheel events", function() { + var deltaY = 100, + scrollBox, + scrollBar, + scrollBoxTranslate0, + scrollBarTranslate0, + scrollBoxTranslate1, + scrollBarTranslate1; + + scrollBox = getScrollBox(); + expect(scrollBox).toBeDefined(); + + // down menu + click(menuDown); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); + scrollBarTranslate0 = Drawing.getTranslate(scrollBar); + wheel(scrollBox, deltaY); + scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); + scrollBarTranslate1 = Drawing.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y - deltaY); + expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); + + // left menu + click(menuLeft); + + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); - function getScrollBarCenter(scrollBox, scrollBar) { - var scrollBoxTranslate = Drawing.getTranslate(scrollBox), - scrollBarTranslate = Drawing.getTranslate(scrollBar), - translateX = scrollBoxTranslate.x + scrollBarTranslate.x, - translateY = scrollBoxTranslate.y + scrollBarTranslate.y, - center = getCenter(scrollBar), - x = center.x + translateX, - y = center.y + translateY; + scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); + scrollBarTranslate0 = Drawing.getTranslate(scrollBar); + wheel(scrollBox, deltaY); + scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); + scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - return { x: x, y: y }; - } + expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x - deltaY); + expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); - function click(node) { - node.dispatchEvent(new MouseEvent('click'), { - bubbles: true - }); - } + // right menu + click(menuRight); - function drag(node, x, y, deltaX, deltaY) { - node.dispatchEvent(new MouseEvent('mousedown', { - bubbles: true, - clientX: x, - clientY: y - })); - node.dispatchEvent(new MouseEvent('mousemove', { - bubbles: true, - clientX: x + deltaX, - clientY: y + deltaY - })); - } + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); - function dragScrollBox(node, deltaX, deltaY) { - var position = getCenter(node); + scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); + scrollBarTranslate0 = Drawing.getTranslate(scrollBar); + wheel(scrollBox, deltaY); + scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); + scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - drag(node, position.x, position.y, deltaX, deltaY); - } + expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x - deltaY); + expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); - function dragScrollBar(node, position, deltaX, deltaY) { - drag(node, position.x, position.y, deltaX, deltaY); - } + // up menu + click(menuUp); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Drawing.getTranslate(scrollBox); + scrollBarTranslate0 = Drawing.getTranslate(scrollBar); + wheel(scrollBox, deltaY); + scrollBoxTranslate1 = Drawing.getTranslate(scrollBox); + scrollBarTranslate1 = Drawing.getTranslate(scrollBar); - function wheel(node, deltaY) { - node.dispatchEvent(new WheelEvent('wheel', { - bubbles: true, - deltaY: deltaY - })); - } + expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y - deltaY); + expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); + }); + + it("scrollbar can be dragged", function() { + var deltaX = 20, + deltaY = 10, + scrollBox, + scrollBar, + scrollBoxPosition0, + scrollBarPosition0, + scrollBoxPosition1, + scrollBarPosition1; + + scrollBox = getScrollBox(); + expect(scrollBox).toBeDefined(); + + // down menu + click(menuDown); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxPosition0 = Drawing.getTranslate(scrollBox); + scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); + dragScrollBar(scrollBar, scrollBarPosition0, 0, deltaY); + scrollBoxPosition1 = Drawing.getTranslate(scrollBox); + scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); + + expect(scrollBoxPosition1.y).toBeLessThan(scrollBoxPosition0.y); + expect(scrollBarPosition1.y).toEqual(scrollBarPosition0.y + deltaY); + + // left menu + click(menuLeft); + + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxPosition0 = Drawing.getTranslate(scrollBox); + scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); + dragScrollBar(scrollBar, scrollBarPosition0, deltaX, 0); + scrollBoxPosition1 = Drawing.getTranslate(scrollBox); + scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); + + expect(scrollBoxPosition1.x).toBeLessThan(scrollBoxPosition0.x); + expect(scrollBarPosition1.x).toEqual(scrollBarPosition0.x + deltaX); + + // right menu + click(menuRight); + + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxPosition0 = Drawing.getTranslate(scrollBox); + scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); + dragScrollBar(scrollBar, scrollBarPosition0, deltaX, 0); + scrollBoxPosition1 = Drawing.getTranslate(scrollBox); + scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); + + expect(scrollBoxPosition1.x).toBeLessThan(scrollBoxPosition0.x); + expect(scrollBarPosition1.x).toEqual(scrollBarPosition0.x + deltaX); + + // up menu + click(menuUp); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxPosition0 = Drawing.getTranslate(scrollBox); + scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); + dragScrollBar(scrollBar, scrollBarPosition0, 0, deltaY); + scrollBoxPosition1 = Drawing.getTranslate(scrollBox); + scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); + + expect(scrollBoxPosition1.y).toBeLessThan(scrollBoxPosition0.y); + expect(scrollBarPosition1.y).toEqual(scrollBarPosition0.y + deltaY); + }); + + function getScrollBox() { + return document.getElementsByClassName( + "updatemenu-dropdown-button-group" + )[0]; + } + + function getHorizontalScrollBar() { + return document.getElementsByClassName("scrollbar-horizontal")[0]; + } + + function getVerticalScrollBar() { + return document.getElementsByClassName("scrollbar-vertical")[0]; + } + + function getCenter(node) { + var bbox = getBBox(node), + x = bbox.x + 0.5 * bbox.width, + y = bbox.y + 0.5 * bbox.height; + + return { x: x, y: y }; + } + + function getScrollBarCenter(scrollBox, scrollBar) { + var scrollBoxTranslate = Drawing.getTranslate(scrollBox), + scrollBarTranslate = Drawing.getTranslate(scrollBar), + translateX = scrollBoxTranslate.x + scrollBarTranslate.x, + translateY = scrollBoxTranslate.y + scrollBarTranslate.y, + center = getCenter(scrollBar), + x = center.x + translateX, + y = center.y + translateY; + + return { x: x, y: y }; + } + + function click(node) { + node.dispatchEvent(new MouseEvent("click"), { bubbles: true }); + } + + function drag(node, x, y, deltaX, deltaY) { + node.dispatchEvent(new MouseEvent("mousedown", { + bubbles: true, + clientX: x, + clientY: y + })); + node.dispatchEvent(new MouseEvent("mousemove", { + bubbles: true, + clientX: x + deltaX, + clientY: y + deltaY + })); + } + + function dragScrollBox(node, deltaX, deltaY) { + var position = getCenter(node); + + drag(node, position.x, position.y, deltaX, deltaY); + } + + function dragScrollBar(node, position, deltaX, deltaY) { + drag(node, position.x, position.y, deltaX, deltaY); + } + + function wheel(node, deltaY) { + node.dispatchEvent(new WheelEvent("wheel", { + bubbles: true, + deltaY: deltaY + })); + } }); diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index 10771bc154a..e208564e4f3 100644 --- a/test/jasmine/tests/validate_test.js +++ b/test/jasmine/tests/validate_test.js @@ -1,367 +1,444 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); +var Plotly = require("@lib/index"); +var Lib = require("@src/lib"); +describe("Plotly.validate", function() { + function assertErrorContent(obj, code, cont, trace, path, astr, msg) { + expect(obj.code).toEqual(code); + expect(obj.container).toEqual(cont); + expect(obj.trace).toEqual(trace); + expect(obj.path).toEqual(path); + expect(obj.astr).toEqual(astr); + expect(obj.msg).toEqual(msg); + } -describe('Plotly.validate', function() { - - function assertErrorContent(obj, code, cont, trace, path, astr, msg) { - expect(obj.code).toEqual(code); - expect(obj.container).toEqual(cont); - expect(obj.trace).toEqual(trace); - expect(obj.path).toEqual(path); - expect(obj.astr).toEqual(astr); - expect(obj.msg).toEqual(msg); - } - - it('should return undefined when no errors are found', function() { - var out = Plotly.validate([{ - type: 'scatter', - x: [1, 2, 3] - }], { - title: 'my simple graph' - }); - - expect(out).toBeUndefined(); + it("should return undefined when no errors are found", function() { + var out = Plotly.validate([{ type: "scatter", x: [1, 2, 3] }], { + title: "my simple graph" }); - it('should report when data is not an array', function() { - var out = Plotly.validate({ - type: 'scatter', - x: [1, 2, 3] - }); + expect(out).toBeUndefined(); + }); - expect(out.length).toEqual(1); - assertErrorContent( - out[0], 'array', 'data', null, '', '', - 'The data argument must be linked to an array container' - ); - }); + it("should report when data is not an array", function() { + var out = Plotly.validate({ type: "scatter", x: [1, 2, 3] }); - it('should report when a data trace is not an object', function() { - var out = Plotly.validate([{ - type: 'bar', - x: [1, 2, 3] - }, [1, 2, 3]]); + expect(out.length).toEqual(1); + assertErrorContent( + out[0], + "array", + "data", + null, + "", + "", + "The data argument must be linked to an array container" + ); + }); - expect(out.length).toEqual(1); - assertErrorContent( - out[0], 'object', 'data', 1, '', '', - 'Trace 1 in the data argument must be linked to an object container' - ); - }); + it("should report when a data trace is not an object", function() { + var out = Plotly.validate([{ type: "bar", x: [1, 2, 3] }, [1, 2, 3]]); - it('should report when layout is not an object', function() { - var out = Plotly.validate([], [1, 2, 3]); + expect(out.length).toEqual(1); + assertErrorContent( + out[0], + "object", + "data", + 1, + "", + "", + "Trace 1 in the data argument must be linked to an object container" + ); + }); - expect(out.length).toEqual(1); - assertErrorContent( - out[0], 'object', 'layout', null, '', '', - 'The layout argument must be linked to an object container' - ); - }); + it("should report when layout is not an object", function() { + var out = Plotly.validate([], [1, 2, 3]); - it('should report when trace is defaulted to not be visible', function() { - var out = Plotly.validate([{ - type: 'scattergeo' - // missing 'x' and 'y - }], {}); + expect(out.length).toEqual(1); + assertErrorContent( + out[0], + "object", + "layout", + null, + "", + "", + "The layout argument must be linked to an object container" + ); + }); - expect(out.length).toEqual(1); - assertErrorContent( - out[0], 'invisible', 'data', 0, '', '', - 'Trace 0 got defaulted to be not visible' - ); - }); + it("should report when trace is defaulted to not be visible", function() { + var out = Plotly.validate( + [ + { + // missing 'x' and 'y + type: "scattergeo" + } + ], + {} + ); - it('should report when trace contains keys not part of the schema', function() { - var out = Plotly.validate([{ - x: [1, 2, 3], - markerColor: 'blue' - }], {}); + expect(out.length).toEqual(1); + assertErrorContent( + out[0], + "invisible", + "data", + 0, + "", + "", + "Trace 0 got defaulted to be not visible" + ); + }); - expect(out.length).toEqual(1); - assertErrorContent( - out[0], 'schema', 'data', 0, ['markerColor'], 'markerColor', - 'In data trace 0, key markerColor is not part of the schema' - ); - }); + it( + "should report when trace contains keys not part of the schema", + function() { + var out = Plotly.validate([{ x: [1, 2, 3], markerColor: "blue" }], {}); - it('should report when trace contains keys that are not coerced', function() { - var out = Plotly.validate([{ - x: [1, 2, 3], - mode: 'lines', - marker: { color: 'blue' } - }, { - x: [1, 2, 3], - mode: 'markers', - marker: { - color: 'blue', - cmin: 10 - } - }], {}); + expect(out.length).toEqual(1); + assertErrorContent( + out[0], + "schema", + "data", + 0, + ["markerColor"], + "markerColor", + "In data trace 0, key markerColor is not part of the schema" + ); + } + ); - expect(out.length).toEqual(2); - assertErrorContent( - out[0], 'unused', 'data', 0, ['marker'], 'marker', - 'In data trace 0, container marker did not get coerced' - ); - assertErrorContent( - out[1], 'unused', 'data', 1, ['marker', 'cmin'], 'marker.cmin', - 'In data trace 1, key marker.cmin did not get coerced' - ); - }); + it("should report when trace contains keys that are not coerced", function() { + var out = Plotly.validate( + [ + { x: [1, 2, 3], mode: "lines", marker: { color: "blue" } }, + { x: [1, 2, 3], mode: "markers", marker: { color: "blue", cmin: 10 } } + ], + {} + ); - it('should report when trace contains keys set to invalid values', function() { - var out = Plotly.validate([{ - x: [1, 2, 3], - mode: 'lines', - line: { width: 'a big number' } - }, { - x: [1, 2, 3], - mode: 'markers', - marker: { color: 10 } - }], {}); + expect(out.length).toEqual(2); + assertErrorContent( + out[0], + "unused", + "data", + 0, + ["marker"], + "marker", + "In data trace 0, container marker did not get coerced" + ); + assertErrorContent( + out[1], + "unused", + "data", + 1, + ["marker", "cmin"], + "marker.cmin", + "In data trace 1, key marker.cmin did not get coerced" + ); + }); - expect(out.length).toEqual(2); - assertErrorContent( - out[0], 'value', 'data', 0, ['line', 'width'], 'line.width', - 'In data trace 0, key line.width is set to an invalid value (a big number)' - ); - assertErrorContent( - out[1], 'value', 'data', 1, ['marker', 'color'], 'marker.color', - 'In data trace 1, key marker.color is set to an invalid value (10)' - ); - }); + it( + "should report when trace contains keys set to invalid values", + function() { + var out = Plotly.validate( + [ + { x: [1, 2, 3], mode: "lines", line: { width: "a big number" } }, + { x: [1, 2, 3], mode: "markers", marker: { color: 10 } } + ], + {} + ); - it('should work with info arrays', function() { - var out = Plotly.validate([{ - y: [1, 2, 2] - }], { - xaxis: { range: [0, 10] }, - yaxis: { range: 'not-gonna-work' }, - }); + expect(out.length).toEqual(2); + assertErrorContent( + out[0], + "value", + "data", + 0, + ["line", "width"], + "line.width", + "In data trace 0, key line.width is set to an invalid value (a big number)" + ); + assertErrorContent( + out[1], + "value", + "data", + 1, + ["marker", "color"], + "marker.color", + "In data trace 1, key marker.color is set to an invalid value (10)" + ); + } + ); - expect(out.length).toEqual(1); - assertErrorContent( - out[0], 'value', 'layout', null, ['yaxis', 'range'], 'yaxis.range', - 'In layout, key yaxis.range is set to an invalid value (not-gonna-work)' - ); + it("should work with info arrays", function() { + var out = Plotly.validate([{ y: [1, 2, 2] }], { + xaxis: { range: [0, 10] }, + yaxis: { range: "not-gonna-work" } }); - it('should work with isLinkedToArray attributes', function() { - var out = Plotly.validate([], { - annotations: [{ - text: 'some text' - }, { - arrowSymbol: 'cat' - }, { - font: { color: 'wont-work' } - }], - xaxis: { - type: 'date', - rangeselector: { - buttons: [{ - label: '1 month', - step: 'all', - count: 10 - }, 'wont-work', { - title: '1 month' - }] - } - }, - xaxis2: { - type: 'date', - rangeselector: { - buttons: [{ - title: '1 month' - }] - } - }, - xaxis3: { - type: 'date', - rangeselector: { - buttons: 'wont-work' - } - }, - shapes: [{ - opacity: 'none' - }], - updatemenus: [{ - buttons: [{ - method: 'restyle', - args: ['marker.color', 'red'] - }] - }, 'wont-work', { - buttons: [{ - method: 'restyle', - args: null - }, { - method: 'relayout', - args: ['marker.color', 'red'], - title: 'not-gonna-work' - }, 'wont-work'] - }] - }); + expect(out.length).toEqual(1); + assertErrorContent( + out[0], + "value", + "layout", + null, + ["yaxis", "range"], + "yaxis.range", + "In layout, key yaxis.range is set to an invalid value (not-gonna-work)" + ); + }); - expect(out.length).toEqual(12); - assertErrorContent( - out[0], 'schema', 'layout', null, - ['annotations', 1, 'arrowSymbol'], 'annotations[1].arrowSymbol', - 'In layout, key annotations[1].arrowSymbol is not part of the schema' - ); - assertErrorContent( - out[1], 'value', 'layout', null, - ['annotations', 2, 'font', 'color'], 'annotations[2].font.color', - 'In layout, key annotations[2].font.color is set to an invalid value (wont-work)' - ); - assertErrorContent( - out[2], 'unused', 'layout', null, - ['xaxis', 'rangeselector', 'buttons', 0, 'count'], - 'xaxis.rangeselector.buttons[0].count', - 'In layout, key xaxis.rangeselector.buttons[0].count did not get coerced' - ); - assertErrorContent( - out[3], 'schema', 'layout', null, - ['xaxis', 'rangeselector', 'buttons', 2, 'title'], - 'xaxis.rangeselector.buttons[2].title', - 'In layout, key xaxis.rangeselector.buttons[2].title is not part of the schema' - ); - assertErrorContent( - out[4], 'object', 'layout', null, - ['xaxis', 'rangeselector', 'buttons', 1], - 'xaxis.rangeselector.buttons[1]', - 'In layout, key xaxis.rangeselector.buttons[1] must be linked to an object container' - ); - assertErrorContent( - out[5], 'schema', 'layout', null, - ['xaxis2', 'rangeselector', 'buttons', 0, 'title'], - 'xaxis2.rangeselector.buttons[0].title', - 'In layout, key xaxis2.rangeselector.buttons[0].title is not part of the schema' - ); - assertErrorContent( - out[6], 'array', 'layout', null, - ['xaxis3', 'rangeselector', 'buttons'], - 'xaxis3.rangeselector.buttons', - 'In layout, key xaxis3.rangeselector.buttons must be linked to an array container' - ); - assertErrorContent( - out[7], 'value', 'layout', null, - ['shapes', 0, 'opacity'], 'shapes[0].opacity', - 'In layout, key shapes[0].opacity is set to an invalid value (none)' - ); - assertErrorContent( - out[8], 'schema', 'layout', null, - ['updatemenus', 2, 'buttons', 1, 'title'], 'updatemenus[2].buttons[1].title', - 'In layout, key updatemenus[2].buttons[1].title is not part of the schema' - ); - assertErrorContent( - out[9], 'unused', 'layout', null, - ['updatemenus', 2, 'buttons', 0], 'updatemenus[2].buttons[0]', - 'In layout, key updatemenus[2].buttons[0] did not get coerced' - ); - assertErrorContent( - out[10], 'object', 'layout', null, - ['updatemenus', 2, 'buttons', 2], 'updatemenus[2].buttons[2]', - 'In layout, key updatemenus[2].buttons[2] must be linked to an object container' - ); + it("should work with isLinkedToArray attributes", function() { + var out = Plotly.validate([], { + annotations: [ + { text: "some text" }, + { arrowSymbol: "cat" }, + { font: { color: "wont-work" } } + ], + xaxis: { + type: "date", + rangeselector: { + buttons: [ + { label: "1 month", step: "all", count: 10 }, + "wont-work", + { title: "1 month" } + ] + } + }, + xaxis2: { + type: "date", + rangeselector: { buttons: [{ title: "1 month" }] } + }, + xaxis3: { type: "date", rangeselector: { buttons: "wont-work" } }, + shapes: [{ opacity: "none" }], + updatemenus: [ + { buttons: [{ method: "restyle", args: ["marker.color", "red"] }] }, + "wont-work", + { + buttons: [ + { method: "restyle", args: null }, + { + method: "relayout", + args: ["marker.color", "red"], + title: "not-gonna-work" + }, + "wont-work" + ] + } + ] }); - it('should work with isSubplotObj attributes', function() { - var out = Plotly.validate([], { - xaxis2: { - range: 30 - }, - scene10: { - bgcolor: 'red' - }, - geo0: {}, - yaxis5: 'sup' - }); + expect(out.length).toEqual(12); + assertErrorContent( + out[0], + "schema", + "layout", + null, + ["annotations", 1, "arrowSymbol"], + "annotations[1].arrowSymbol", + "In layout, key annotations[1].arrowSymbol is not part of the schema" + ); + assertErrorContent( + out[1], + "value", + "layout", + null, + ["annotations", 2, "font", "color"], + "annotations[2].font.color", + "In layout, key annotations[2].font.color is set to an invalid value (wont-work)" + ); + assertErrorContent( + out[2], + "unused", + "layout", + null, + ["xaxis", "rangeselector", "buttons", 0, "count"], + "xaxis.rangeselector.buttons[0].count", + "In layout, key xaxis.rangeselector.buttons[0].count did not get coerced" + ); + assertErrorContent( + out[3], + "schema", + "layout", + null, + ["xaxis", "rangeselector", "buttons", 2, "title"], + "xaxis.rangeselector.buttons[2].title", + "In layout, key xaxis.rangeselector.buttons[2].title is not part of the schema" + ); + assertErrorContent( + out[4], + "object", + "layout", + null, + ["xaxis", "rangeselector", "buttons", 1], + "xaxis.rangeselector.buttons[1]", + "In layout, key xaxis.rangeselector.buttons[1] must be linked to an object container" + ); + assertErrorContent( + out[5], + "schema", + "layout", + null, + ["xaxis2", "rangeselector", "buttons", 0, "title"], + "xaxis2.rangeselector.buttons[0].title", + "In layout, key xaxis2.rangeselector.buttons[0].title is not part of the schema" + ); + assertErrorContent( + out[6], + "array", + "layout", + null, + ["xaxis3", "rangeselector", "buttons"], + "xaxis3.rangeselector.buttons", + "In layout, key xaxis3.rangeselector.buttons must be linked to an array container" + ); + assertErrorContent( + out[7], + "value", + "layout", + null, + ["shapes", 0, "opacity"], + "shapes[0].opacity", + "In layout, key shapes[0].opacity is set to an invalid value (none)" + ); + assertErrorContent( + out[8], + "schema", + "layout", + null, + ["updatemenus", 2, "buttons", 1, "title"], + "updatemenus[2].buttons[1].title", + "In layout, key updatemenus[2].buttons[1].title is not part of the schema" + ); + assertErrorContent( + out[9], + "unused", + "layout", + null, + ["updatemenus", 2, "buttons", 0], + "updatemenus[2].buttons[0]", + "In layout, key updatemenus[2].buttons[0] did not get coerced" + ); + assertErrorContent( + out[10], + "object", + "layout", + null, + ["updatemenus", 2, "buttons", 2], + "updatemenus[2].buttons[2]", + "In layout, key updatemenus[2].buttons[2] must be linked to an object container" + ); + }); - expect(out.length).toEqual(4); - assertErrorContent( - out[0], 'value', 'layout', null, - ['xaxis2', 'range'], 'xaxis2.range', - 'In layout, key xaxis2.range is set to an invalid value (30)' - ); - assertErrorContent( - out[1], 'unused', 'layout', null, - ['scene10'], 'scene10', - 'In layout, container scene10 did not get coerced' - ); - assertErrorContent( - out[2], 'schema', 'layout', null, - ['geo0'], 'geo0', - 'In layout, key geo0 is not part of the schema' - ); - assertErrorContent( - out[3], 'object', 'layout', null, - ['yaxis5'], 'yaxis5', - 'In layout, key yaxis5 must be linked to an object container' - ); + it("should work with isSubplotObj attributes", function() { + var out = Plotly.validate([], { + xaxis2: { range: 30 }, + scene10: { bgcolor: "red" }, + geo0: {}, + yaxis5: "sup" }); - it('should work with attributes in registered transforms', function() { - var base = { - x: [-2, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - }; + expect(out.length).toEqual(4); + assertErrorContent( + out[0], + "value", + "layout", + null, + ["xaxis2", "range"], + "xaxis2.range", + "In layout, key xaxis2.range is set to an invalid value (30)" + ); + assertErrorContent( + out[1], + "unused", + "layout", + null, + ["scene10"], + "scene10", + "In layout, container scene10 did not get coerced" + ); + assertErrorContent( + out[2], + "schema", + "layout", + null, + ["geo0"], + "geo0", + "In layout, key geo0 is not part of the schema" + ); + assertErrorContent( + out[3], + "object", + "layout", + null, + ["yaxis5"], + "yaxis5", + "In layout, key yaxis5 must be linked to an object container" + ); + }); - var out = Plotly.validate([ - Lib.extendFlat({}, base, { - transforms: [{ - type: 'filter', - operation: '=' - }, { - type: 'filter', - operation: '=', - wrongKey: 'sup?' - }, { - type: 'filter', - operation: 'wrongVal' - }, - 'wont-work' - ] - }), - Lib.extendFlat({}, base, { - transforms: { - type: 'filter' - } - }), - Lib.extendFlat({}, base, { - transforms: [{ - type: 'no gonna work' - }] - }), - ], { - title: 'my transformed graph' - }); + it("should work with attributes in registered transforms", function() { + var base = { x: [-2, -1, -2, 0, 1, 2, 3], y: [1, 2, 3, 1, 2, 3, 1] }; - expect(out.length).toEqual(5); - assertErrorContent( - out[0], 'schema', 'data', 0, - ['transforms', 1, 'wrongKey'], 'transforms[1].wrongKey', - 'In data trace 0, key transforms[1].wrongKey is not part of the schema' - ); - assertErrorContent( - out[1], 'value', 'data', 0, - ['transforms', 2, 'operation'], 'transforms[2].operation', - 'In data trace 0, key transforms[2].operation is set to an invalid value (wrongVal)' - ); - assertErrorContent( - out[2], 'object', 'data', 0, - ['transforms', 3], 'transforms[3]', - 'In data trace 0, key transforms[3] must be linked to an object container' - ); - assertErrorContent( - out[3], 'array', 'data', 1, - ['transforms'], 'transforms', - 'In data trace 1, key transforms must be linked to an array container' - ); - assertErrorContent( - out[4], 'value', 'data', 2, - ['transforms', 0, 'type'], 'transforms[0].type', - 'In data trace 2, key transforms[0].type is set to an invalid value (no gonna work)' - ); - }); + var out = Plotly.validate( + [ + Lib.extendFlat({}, base, { + transforms: [ + { type: "filter", operation: "=" }, + { type: "filter", operation: "=", wrongKey: "sup?" }, + { type: "filter", operation: "wrongVal" }, + "wont-work" + ] + }), + Lib.extendFlat({}, base, { transforms: { type: "filter" } }), + Lib.extendFlat({}, base, { transforms: [{ type: "no gonna work" }] }) + ], + { title: "my transformed graph" } + ); + + expect(out.length).toEqual(5); + assertErrorContent( + out[0], + "schema", + "data", + 0, + ["transforms", 1, "wrongKey"], + "transforms[1].wrongKey", + "In data trace 0, key transforms[1].wrongKey is not part of the schema" + ); + assertErrorContent( + out[1], + "value", + "data", + 0, + ["transforms", 2, "operation"], + "transforms[2].operation", + "In data trace 0, key transforms[2].operation is set to an invalid value (wrongVal)" + ); + assertErrorContent( + out[2], + "object", + "data", + 0, + ["transforms", 3], + "transforms[3]", + "In data trace 0, key transforms[3] must be linked to an object container" + ); + assertErrorContent( + out[3], + "array", + "data", + 1, + ["transforms"], + "transforms", + "In data trace 1, key transforms must be linked to an array container" + ); + assertErrorContent( + out[4], + "value", + "data", + 2, + ["transforms", 0, "type"], + "transforms[0].type", + "In data trace 2, key transforms[0].type is set to an invalid value (no gonna work)" + ); + }); });