From 881c41791f59fb9699380c522bdc7d497666a0c9 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 11 Jan 2021 14:46:47 -0500 Subject: [PATCH 1/3] drop problematic gl2d plots and traces - namely pointcloud, contourgl and heatmapgl - one could use image & scattergl instaed of heatmapgl & pointcloud - this very much cleans the gl2d bundle from bugs & function constructors - with the exception of binary-search-bounds v2 used in scattergl now --- lib/contourgl.js | 11 - lib/heatmapgl.js | 11 - lib/index-gl2d.js | 3 - lib/index.js | 3 - lib/pointcloud.js | 11 - package-lock.json | 386 ---------- package.json | 5 - src/components/images/draw.js | 3 +- src/components/modebar/buttons.js | 11 - src/components/modebar/manage.js | 9 +- src/components/rangeslider/helpers.js | 18 +- src/plot_api/plot_api.js | 2 - src/plot_api/plot_schema.js | 3 +- src/plot_api/subroutines.js | 3 - src/plots/cartesian/axes.js | 7 +- src/plots/cartesian/include_components.js | 4 +- src/plots/cartesian/index.js | 2 +- src/plots/cartesian/layout_defaults.js | 2 +- src/plots/get_data.js | 17 +- src/plots/gl2d/camera.js | 293 ------- src/plots/gl2d/convert.js | 241 ------ src/plots/gl2d/index.js | 149 ---- src/plots/gl2d/scene2d.js | 718 ------------------ src/plots/plots.js | 39 +- src/snapshot/helpers.js | 1 - src/traces/contourgl/convert.js | 187 ----- src/traces/contourgl/index.js | 30 - src/traces/heatmap/calc.js | 8 +- src/traces/heatmap/make_bound_array.js | 5 +- src/traces/heatmap/xyz_defaults.js | 5 - src/traces/heatmapgl/attributes.js | 46 -- src/traces/heatmapgl/convert.js | 150 ---- src/traces/heatmapgl/defaults.js | 34 - src/traces/heatmapgl/index.js | 28 - src/traces/pointcloud/attributes.js | 146 ---- src/traces/pointcloud/convert.js | 200 ----- src/traces/pointcloud/defaults.js | 46 -- src/traces/pointcloud/index.js | 29 - test/image/baselines/gl2d_heatmapgl.png | Bin 69201 -> 0 bytes .../baselines/gl2d_heatmapgl_discrete.png | Bin 18173 -> 0 bytes .../image/baselines/gl2d_pointcloud-basic.png | Bin 30504 -> 0 bytes test/image/compare_pixels_test.js | 30 - test/image/mocks/gl2d_heatmapgl.json | 134 ---- test/image/mocks/gl2d_heatmapgl_discrete.json | 135 ---- test/image/mocks/gl2d_pointcloud-basic.json | 72 -- test/jasmine/assets/mock_lists.js | 2 - test/jasmine/bundle_tests/no_webgl_test.js | 12 - test/jasmine/tests/axes_test.js | 3 +- test/jasmine/tests/contourgl_test.js | 288 ------- test/jasmine/tests/gl2d_click_test.js | 150 +--- test/jasmine/tests/heatmap_test.js | 2 +- test/jasmine/tests/heatmapgl_test.js | 99 --- test/jasmine/tests/mock_test.js | 6 - test/jasmine/tests/modebar_test.js | 19 - test/jasmine/tests/plot_api_react_test.js | 4 - test/jasmine/tests/pointcloud_test.js | 255 ------- 56 files changed, 45 insertions(+), 4032 deletions(-) delete mode 100644 lib/contourgl.js delete mode 100644 lib/heatmapgl.js delete mode 100644 lib/pointcloud.js delete mode 100644 src/plots/gl2d/camera.js delete mode 100644 src/plots/gl2d/convert.js delete mode 100644 src/plots/gl2d/index.js delete mode 100644 src/plots/gl2d/scene2d.js delete mode 100644 src/traces/contourgl/convert.js delete mode 100644 src/traces/contourgl/index.js delete mode 100644 src/traces/heatmapgl/attributes.js delete mode 100644 src/traces/heatmapgl/convert.js delete mode 100644 src/traces/heatmapgl/defaults.js delete mode 100644 src/traces/heatmapgl/index.js delete mode 100644 src/traces/pointcloud/attributes.js delete mode 100644 src/traces/pointcloud/convert.js delete mode 100644 src/traces/pointcloud/defaults.js delete mode 100644 src/traces/pointcloud/index.js delete mode 100644 test/image/baselines/gl2d_heatmapgl.png delete mode 100644 test/image/baselines/gl2d_heatmapgl_discrete.png delete mode 100644 test/image/baselines/gl2d_pointcloud-basic.png delete mode 100644 test/image/mocks/gl2d_heatmapgl.json delete mode 100644 test/image/mocks/gl2d_heatmapgl_discrete.json delete mode 100644 test/image/mocks/gl2d_pointcloud-basic.json delete mode 100644 test/jasmine/tests/contourgl_test.js delete mode 100644 test/jasmine/tests/heatmapgl_test.js delete mode 100644 test/jasmine/tests/pointcloud_test.js diff --git a/lib/contourgl.js b/lib/contourgl.js deleted file mode 100644 index 7bbbae74a15..00000000000 --- a/lib/contourgl.js +++ /dev/null @@ -1,11 +0,0 @@ -/** -* Copyright 2012-2021, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - -module.exports = require('../src/traces/contourgl'); diff --git a/lib/heatmapgl.js b/lib/heatmapgl.js deleted file mode 100644 index 8554aad30ef..00000000000 --- a/lib/heatmapgl.js +++ /dev/null @@ -1,11 +0,0 @@ -/** -* Copyright 2012-2021, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - -module.exports = require('../src/traces/heatmapgl'); diff --git a/lib/index-gl2d.js b/lib/index-gl2d.js index 9f27ddf3167..478f1d617bc 100644 --- a/lib/index-gl2d.js +++ b/lib/index-gl2d.js @@ -13,9 +13,6 @@ var Plotly = require('./core'); Plotly.register([ require('./scattergl'), require('./splom'), - require('./pointcloud'), - require('./heatmapgl'), - require('./contourgl'), require('./parcoords') ]); diff --git a/lib/index.js b/lib/index.js index 97e0734862f..4b1be68f8db 100644 --- a/lib/index.js +++ b/lib/index.js @@ -44,9 +44,6 @@ Plotly.register([ require('./scattergl'), require('./splom'), - require('./pointcloud'), - require('./heatmapgl'), - require('./parcoords'), require('./parcats'), diff --git a/lib/pointcloud.js b/lib/pointcloud.js deleted file mode 100644 index 15d851007e8..00000000000 --- a/lib/pointcloud.js +++ /dev/null @@ -1,11 +0,0 @@ -/** -* Copyright 2012-2021, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - -module.exports = require('../src/traces/pointcloud'); diff --git a/package-lock.json b/package-lock.json index 9712fe17db8..7fb7d2a01a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4994,102 +4994,6 @@ "resolved": "https://registry.npmjs.org/gl-constants/-/gl-constants-1.0.0.tgz", "integrity": "sha1-WXpQTjZHUP9QJTqjX43qevSl0jM=" }, - "gl-contour2d": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/gl-contour2d/-/gl-contour2d-1.1.7.tgz", - "integrity": "sha512-GdebvJ9DtT3pJDpoE+eU2q+Wo9S3MijPpPz5arZbhK85w2bARmpFpVfPaDlZqWkB644W3BlH8TVyvAo1KE4Bhw==", - "requires": { - "binary-search-bounds": "^2.0.4", - "cdt2d": "^1.0.0", - "clean-pslg": "^1.1.2", - "gl-buffer": "^2.1.2", - "gl-shader": "^4.2.1", - "glslify": "^7.0.0", - "iota-array": "^1.0.0", - "ndarray": "^1.0.18", - "surface-nets": "^1.0.2" - }, - "dependencies": { - "glslify": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", - "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", - "requires": { - "bl": "^2.2.1", - "concat-stream": "^1.5.2", - "duplexify": "^3.4.5", - "falafel": "^2.1.0", - "from2": "^2.3.0", - "glsl-resolve": "0.0.1", - "glsl-token-whitespace-trim": "^1.0.0", - "glslify-bundle": "^5.0.0", - "glslify-deps": "^1.2.5", - "minimist": "^1.2.5", - "resolve": "^1.1.5", - "stack-trace": "0.0.9", - "static-eval": "^2.0.5", - "through2": "^2.0.1", - "xtend": "^4.0.0" - } - }, - "glslify-deps": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.2.tgz", - "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", - "requires": { - "@choojs/findup": "^0.2.0", - "events": "^3.2.0", - "glsl-resolve": "0.0.1", - "glsl-tokenizer": "^2.0.0", - "graceful-fs": "^4.1.2", - "inherits": "^2.0.1", - "map-limit": "0.0.1", - "resolve": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, "gl-error3d": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/gl-error3d/-/gl-error3d-1.0.16.tgz", @@ -5201,99 +5105,6 @@ "sprintf-js": "^1.0.3" } }, - "gl-heatmap2d": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/gl-heatmap2d/-/gl-heatmap2d-1.1.0.tgz", - "integrity": "sha512-0FLXyxv6UBCzzhi4Q2u+9fUs6BX1+r5ZztFe27VikE9FUVw7hZiuSHmgDng92EpydogcSYHXCIK8+58RagODug==", - "requires": { - "binary-search-bounds": "^2.0.4", - "gl-buffer": "^2.1.2", - "gl-shader": "^4.2.1", - "glslify": "^7.0.0", - "iota-array": "^1.0.0", - "typedarray-pool": "^1.2.0" - }, - "dependencies": { - "glslify": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", - "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", - "requires": { - "bl": "^2.2.1", - "concat-stream": "^1.5.2", - "duplexify": "^3.4.5", - "falafel": "^2.1.0", - "from2": "^2.3.0", - "glsl-resolve": "0.0.1", - "glsl-token-whitespace-trim": "^1.0.0", - "glslify-bundle": "^5.0.0", - "glslify-deps": "^1.2.5", - "minimist": "^1.2.5", - "resolve": "^1.1.5", - "stack-trace": "0.0.9", - "static-eval": "^2.0.5", - "through2": "^2.0.1", - "xtend": "^4.0.0" - } - }, - "glslify-deps": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.2.tgz", - "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", - "requires": { - "@choojs/findup": "^0.2.0", - "events": "^3.2.0", - "glsl-resolve": "0.0.1", - "glsl-tokenizer": "^2.0.0", - "graceful-fs": "^4.1.2", - "inherits": "^2.0.1", - "map-limit": "0.0.1", - "resolve": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, "gl-line3d": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/gl-line3d/-/gl-line3d-1.2.1.tgz", @@ -5501,100 +5312,6 @@ } } }, - "gl-plot2d": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/gl-plot2d/-/gl-plot2d-1.4.5.tgz", - "integrity": "sha512-6GmCN10SWtV+qHFQ1gjdnVubeHFVsm6P4zmo0HrPIl9TcdePCUHDlBKWAuE6XtFhiMKMj7R8rApOX8O8uXUYog==", - "requires": { - "binary-search-bounds": "^2.0.4", - "gl-buffer": "^2.1.2", - "gl-select-static": "^2.0.7", - "gl-shader": "^4.2.1", - "glsl-inverse": "^1.0.0", - "glslify": "^7.0.0", - "text-cache": "^4.2.2" - }, - "dependencies": { - "glslify": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", - "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", - "requires": { - "bl": "^2.2.1", - "concat-stream": "^1.5.2", - "duplexify": "^3.4.5", - "falafel": "^2.1.0", - "from2": "^2.3.0", - "glsl-resolve": "0.0.1", - "glsl-token-whitespace-trim": "^1.0.0", - "glslify-bundle": "^5.0.0", - "glslify-deps": "^1.2.5", - "minimist": "^1.2.5", - "resolve": "^1.1.5", - "stack-trace": "0.0.9", - "static-eval": "^2.0.5", - "through2": "^2.0.1", - "xtend": "^4.0.0" - } - }, - "glslify-deps": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.2.tgz", - "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", - "requires": { - "@choojs/findup": "^0.2.0", - "events": "^3.2.0", - "glsl-resolve": "0.0.1", - "glsl-tokenizer": "^2.0.0", - "graceful-fs": "^4.1.2", - "inherits": "^2.0.1", - "map-limit": "0.0.1", - "resolve": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, "gl-plot3d": { "version": "2.4.7", "resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.7.tgz", @@ -5902,96 +5619,6 @@ } } }, - "gl-select-box": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/gl-select-box/-/gl-select-box-1.0.4.tgz", - "integrity": "sha512-mKsCnglraSKyBbQiGq0Ila0WF+m6Tr+EWT2yfaMn/Sh9aMHq5Wt0F/l6Cf/Ed3CdERq5jHWAY5yxLviZteYu2w==", - "requires": { - "gl-buffer": "^2.1.2", - "gl-shader": "^4.2.1", - "glslify": "^7.0.0" - }, - "dependencies": { - "glslify": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", - "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", - "requires": { - "bl": "^2.2.1", - "concat-stream": "^1.5.2", - "duplexify": "^3.4.5", - "falafel": "^2.1.0", - "from2": "^2.3.0", - "glsl-resolve": "0.0.1", - "glsl-token-whitespace-trim": "^1.0.0", - "glslify-bundle": "^5.0.0", - "glslify-deps": "^1.2.5", - "minimist": "^1.2.5", - "resolve": "^1.1.5", - "stack-trace": "0.0.9", - "static-eval": "^2.0.5", - "through2": "^2.0.1", - "xtend": "^4.0.0" - } - }, - "glslify-deps": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.2.tgz", - "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", - "requires": { - "@choojs/findup": "^0.2.0", - "events": "^3.2.0", - "glsl-resolve": "0.0.1", - "glsl-tokenizer": "^2.0.0", - "graceful-fs": "^4.1.2", - "inherits": "^2.0.1", - "map-limit": "0.0.1", - "resolve": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, "gl-select-static": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/gl-select-static/-/gl-select-static-2.0.7.tgz", @@ -6012,11 +5639,6 @@ "weakmap-shim": "^1.1.0" } }, - "gl-spikes2d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/gl-spikes2d/-/gl-spikes2d-1.0.2.tgz", - "integrity": "sha512-QVeOZsi9nQuJJl7NB3132CCv5KA10BWxAY2QgJNsKqbLsG53B/TrGJpjIAohnJftdZ4fT6b3ZojWgeaXk8bOOA==" - }, "gl-spikes3d": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/gl-spikes3d/-/gl-spikes3d-1.0.10.tgz", @@ -12267,14 +11889,6 @@ "source-map-support": "~0.5.12" } }, - "text-cache": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/text-cache/-/text-cache-4.2.2.tgz", - "integrity": "sha512-zky+UDYiX0a/aPw/YTBD+EzKMlCTu1chFuCMZeAkgoRiceySdROu1V2kJXhCbtEdBhiOviYnAdGiSYl58HW0ZQ==", - "requires": { - "vectorize-text": "^3.2.1" - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index a5dd0e4cbe1..d02c0b49e6e 100644 --- a/package.json +++ b/package.json @@ -79,18 +79,13 @@ "delaunay-triangulate": "^1.1.6", "fast-isnumeric": "^1.1.4", "gl-cone3d": "^1.5.2", - "gl-contour2d": "^1.1.7", "gl-error3d": "^1.0.16", - "gl-heatmap2d": "^1.1.0", "gl-line3d": "1.2.1", "gl-mat4": "^1.2.0", "gl-mesh3d": "^2.3.1", - "gl-plot2d": "^1.4.5", "gl-plot3d": "^2.4.7", "gl-pointcloud2d": "^1.0.3", "gl-scatter3d": "^1.2.3", - "gl-select-box": "^1.0.4", - "gl-spikes2d": "^1.0.2", "gl-streamtube3d": "^1.4.1", "gl-surface3d": "^1.6.0", "gl-text": "^1.1.8", diff --git a/src/components/images/draw.js b/src/components/images/draw.js index 4393ad07214..c97c6524147 100644 --- a/src/components/images/draw.js +++ b/src/components/images/draw.js @@ -236,8 +236,7 @@ module.exports = function draw(gd) { subplot = allSubplots[i]; var subplotObj = fullLayout._plots[subplot]; - // filter out overlaid plots (which havd their images on the main plot) - // and gl2d plots (which don't support below images, at least not yet) + // filter out overlaid plots (which have their images on the main plot) if(!subplotObj.imagelayer) continue; var imagesOnSubplot = subplotObj.imagelayer.selectAll('image') diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index af0ef757fcd..55f42e84b76 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -544,17 +544,6 @@ function handleGeo(gd, ev) { } } -modeBarButtons.hoverClosestGl2d = { - name: 'hoverClosestGl2d', - title: function(gd) { return _(gd, '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: function(gd) { return _(gd, 'Toggle show closest data on hover'); }, diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 6743ba5aae5..971d98629cd 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -89,7 +89,6 @@ function getButtonGroups(gd) { var hasGeo = fullLayout._has('geo'); var hasPie = fullLayout._has('pie'); var hasFunnelarea = fullLayout._has('funnelarea'); - var hasGL2D = fullLayout._has('gl2d'); var hasTernary = fullLayout._has('ternary'); var hasMapbox = fullLayout._has('mapbox'); var hasPolar = fullLayout._has('polar'); @@ -124,7 +123,7 @@ function getButtonGroups(gd) { var resetGroup = []; var dragModeGroup = []; - if((hasCartesian || hasGL2D || hasPie || hasFunnelarea || hasTernary) + hasGeo + hasGL3D + hasMapbox + hasPolar > 1) { + if((hasCartesian || hasPie || hasFunnelarea || hasTernary) + hasGeo + hasGL3D + hasMapbox + hasPolar > 1) { // graphs with more than one plot types get 'union buttons' // which reset the view or toggle hover labels across all subplots. hoverGroup = ['toggleHover']; @@ -140,8 +139,6 @@ function getButtonGroups(gd) { zoomGroup = ['zoomInMapbox', 'zoomOutMapbox']; hoverGroup = ['toggleHover']; resetGroup = ['resetViewMapbox']; - } else if(hasGL2D) { - hoverGroup = ['hoverClosestGl2d']; } else if(hasPie) { hoverGroup = ['hoverClosestPie']; } else if(hasSankey) { @@ -161,14 +158,14 @@ function getButtonGroups(gd) { hoverGroup = []; } - if((hasCartesian || hasGL2D) && !allAxesFixed) { + if((hasCartesian) && !allAxesFixed) { zoomGroup = ['zoomIn2d', 'zoomOut2d', 'autoScale2d']; if(resetGroup[0] !== 'resetViews') resetGroup = ['resetScale2d']; } if(hasGL3D) { dragModeGroup = ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']; - } else if(((hasCartesian || hasGL2D) && !allAxesFixed) || hasTernary) { + } else if(((hasCartesian) && !allAxesFixed) || hasTernary) { dragModeGroup = ['zoom2d', 'pan2d']; } else if(hasMapbox || hasGeo) { dragModeGroup = ['pan2d']; diff --git a/src/components/rangeslider/helpers.js b/src/components/rangeslider/helpers.js index 4f32a3320c0..abbb0a04667 100644 --- a/src/components/rangeslider/helpers.js +++ b/src/components/rangeslider/helpers.js @@ -25,18 +25,16 @@ exports.makeData = function(fullLayout) { var margin = fullLayout.margin; var rangeSliderData = []; - if(!fullLayout._has('gl2d')) { - for(var i = 0; i < axes.length; i++) { - var ax = axes[i]; + for(var i = 0; i < axes.length; i++) { + var ax = axes[i]; - if(isVisible(ax)) { - rangeSliderData.push(ax); + if(isVisible(ax)) { + rangeSliderData.push(ax); - var opts = ax[name]; - opts._id = name + ax._id; - opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness; - opts._offsetShift = Math.floor(opts.borderwidth / 2); - } + var opts = ax[name]; + opts._id = name + ax._id; + opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness; + opts._offsetShift = Math.floor(opts.borderwidth / 2); } } diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 3ae3bcf42d6..89a20696dd1 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2191,8 +2191,6 @@ function _relayout(gd, aobj) { !(vOld === 'lasso' || vOld === 'select')) ) { flags.plot = true; - } else if(fullLayout._has('gl2d')) { - flags.plot = true; } else if(valObject) editTypes.update(flags, valObject); else flags.calc = true; diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index a67c8909f28..2502bb313a8 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -332,7 +332,7 @@ function layoutHeadAttr(fullLayout, head) { _module = basePlotModules[i]; if(_module.attrRegex && _module.attrRegex.test(head)) { // if a module defines overrides, these take precedence - // initially this is to allow gl2d different editTypes from svg cartesian + // initially this was to allow gl2d different editTypes from svg cartesian if(_module.layoutAttrOverrides) return _module.layoutAttrOverrides; // otherwise take the first attributes we find @@ -340,7 +340,6 @@ function layoutHeadAttr(fullLayout, head) { } // a module can also override the behavior of base (and component) module layout attrs - // again see gl2d for initial use case var baseOverrides = _module.baseLayoutAttrOverrides; if(baseOverrides && head in baseOverrides) return baseOverrides[head]; } diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 334815d0595..b6a92c332fd 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -68,9 +68,6 @@ function lsInner(gd) { exports.drawMainTitle(gd); ModeBar.manage(gd); - // _has('cartesian') means SVG specifically, not GL2D - but GL2D - // can still get here because it makes some of the SVG structure - // for shared features like selections. if(!fullLayout._has('cartesian')) { return Plots.previousPromises(gd); } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index bd53e4364ff..a1d3f2956d2 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -102,8 +102,6 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption if(!extraOption) extraOption = dflt; axlist = axlist.concat(axlist.map(function(x) { return x + ' domain'; })); - // data-ref annotations are not supported in gl2d yet - attrDef[refAttr] = { valType: 'enumerated', values: axlist.concat(extraOption ? @@ -1840,7 +1838,7 @@ axes.getTickFormat = function(ax) { // ideally we get rid of it there (or just copy this there) and remove it here axes.getSubplots = function(gd, ax) { var subplotObj = gd._fullLayout._subplots; - var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []); + var allSubplots = subplotObj.cartesian; var out = ax ? axes.findSubplotsWithAxis(allSubplots, ax) : allSubplots; @@ -1856,8 +1854,7 @@ axes.getSubplots = function(gd, ax) { }; // find all subplots with axis 'ax' -// NOTE: this is only used in axes.getSubplots (only used outside plotly.js) and -// gl2d/convert (where it restricts axis subplots to only those with gl2d) +// NOTE: this is only used in axes.getSubplots (only used outside plotly.js) axes.findSubplotsWithAxis = function(subplots, ax) { var axMatch = new RegExp( (ax._id.charAt(0) === 'x') ? ('^' + ax._id + 'y') : (ax._id + '$') diff --git a/src/plots/cartesian/include_components.js b/src/plots/cartesian/include_components.js index c699716b5ff..2c8677bc46c 100644 --- a/src/plots/cartesian/include_components.js +++ b/src/plots/cartesian/include_components.js @@ -35,7 +35,7 @@ module.exports = function makeIncludeComponents(containerArrayName) { var xaList = subplots.xaxis; var yaList = subplots.yaxis; var cartesianList = subplots.cartesian; - var hasCartesianOrGL2D = layoutOut._has('cartesian') || layoutOut._has('gl2d'); + var hasCartesian = layoutOut._has('cartesian'); for(var i = 0; i < array.length; i++) { var itemi = array[i]; @@ -49,7 +49,7 @@ module.exports = function makeIncludeComponents(containerArrayName) { var hasXref = idRegex.x.test(xref); var hasYref = idRegex.y.test(yref); if(hasXref || hasYref) { - if(!hasCartesianOrGL2D) Lib.pushUnique(layoutOut._basePlotModules, Cartesian); + if(!hasCartesian) Lib.pushUnique(layoutOut._basePlotModules, Cartesian); var newAxis = false; if(hasXref && xaList.indexOf(xref) === -1) { diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index c1e3b607989..d77e6f511c4 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -52,7 +52,7 @@ exports.finalizeSubplots = function(layoutIn, layoutOut) { var xList = subplots.xaxis; var yList = subplots.yaxis; var spSVG = subplots.cartesian; - var spAll = spSVG.concat(subplots.gl2d || []); + var spAll = spSVG; var allX = {}; var allY = {}; var i, xi, yi; diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index f3a23402048..c42580cbe2b 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -55,7 +55,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { // look for axes in the data for(i = 0; i < fullData.length; i++) { var trace = fullData[i]; - if(!traceIs(trace, 'cartesian') && !traceIs(trace, 'gl2d')) continue; + if(!traceIs(trace, 'cartesian')) continue; var xaName; if(trace.xaxis) { diff --git a/src/plots/get_data.js b/src/plots/get_data.js index 78fcb7d2149..e255c2aff0a 100644 --- a/src/plots/get_data.js +++ b/src/plots/get_data.js @@ -9,7 +9,6 @@ 'use strict'; var Registry = require('../registry'); -var SUBPLOT_PATTERN = require('./cartesian/constants').SUBPLOT_PATTERN; /** * Get calcdata trace(s) associated with a given subplot @@ -103,24 +102,12 @@ exports.getSubplotData = function getSubplotData(data, type, subplotId) { var attr = Registry.subplotsRegistry[type].attr; var subplotData = []; - var trace, subplotX, subplotY; - - if(type === 'gl2d') { - var spmatch = subplotId.match(SUBPLOT_PATTERN); - subplotX = 'x' + spmatch[1]; - subplotY = 'y' + spmatch[2]; - } + var trace; for(var i = 0; i < data.length; i++) { trace = data[i]; - if(type === 'gl2d' && Registry.traceIs(trace, 'gl2d')) { - if(trace[attr[0]] === subplotX && trace[attr[1]] === subplotY) { - subplotData.push(trace); - } - } else { - if(trace[attr] === subplotId) subplotData.push(trace); - } + if(trace[attr] === subplotId) subplotData.push(trace); } return subplotData; diff --git a/src/plots/gl2d/camera.js b/src/plots/gl2d/camera.js deleted file mode 100644 index 60f21f0fcec..00000000000 --- a/src/plots/gl2d/camera.js +++ /dev/null @@ -1,293 +0,0 @@ -/** -* Copyright 2012-2021, 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 mouseChange = require('mouse-change'); -var mouseWheel = require('mouse-wheel'); -var mouseOffset = require('mouse-event-offset'); -var cartesianConstants = require('../cartesian/constants'); -var hasPassive = require('has-passive-events'); - -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.boxInited = false; - this.boxStart = [0, 0]; - this.boxEnd = [0, 0]; - this.dragStart = [0, 0]; -} - - -function createCamera(scene) { - var element = scene.mouseContainer; - var plot = scene.glplot; - var result = new Camera2D(element, plot); - - function unSetAutoRange() { - scene.xaxis.autorange = false; - scene.yaxis.autorange = false; - } - - function getSubplotConstraint() { - // note: this assumes we only have one x and one y axis on this subplot - // when this constraint is lifted this block won't make sense - var constraints = scene.graphDiv._fullLayout._axisConstraintGroups; - var xaId = scene.xaxis._id; - var yaId = scene.yaxis._id; - for(var i = 0; i < constraints.length; i++) { - if(constraints[i][xaId] !== -1) { - if(constraints[i][yaId] !== -1) return true; - break; - } - } - return false; - } - - result.mouseListener = mouseChange(element, handleInteraction); - - // enable simple touch interactions - element.addEventListener('touchstart', function(ev) { - var xy = mouseOffset(ev.changedTouches[0], element); - handleInteraction(0, xy[0], xy[1]); - handleInteraction(1, xy[0], xy[1]); - - ev.preventDefault(); - }, hasPassive ? {passive: false} : false); - element.addEventListener('touchmove', function(ev) { - ev.preventDefault(); - var xy = mouseOffset(ev.changedTouches[0], element); - handleInteraction(1, xy[0], xy[1]); - - ev.preventDefault(); - }, hasPassive ? {passive: false} : false); - element.addEventListener('touchend', function(ev) { - handleInteraction(0, result.lastPos[0], result.lastPos[1]); - - ev.preventDefault(); - }, hasPassive ? {passive: false} : false); - - function handleInteraction(buttons, x, y) { - var dataBox = scene.calcDataBox(); - var viewBox = plot.viewBox; - - var lastX = result.lastPos[0]; - var lastY = result.lastPos[1]; - - var MINDRAG = cartesianConstants.MINDRAG * plot.pixelRatio; - var MINZOOM = cartesianConstants.MINZOOM * plot.pixelRatio; - - var dx, dy; - - 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); - var 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.boxInited) { - result.boxStart[0] = dataX; - result.boxStart[1] = dataY; - result.dragStart[0] = x; - result.dragStart[1] = y; - } - - result.boxEnd[0] = dataX; - result.boxEnd[1] = dataY; - - // we need to mark the box as initialized right away - // so that we can tell the start and end points apart - result.boxInited = true; - - // but don't actually enable the box until the cursor moves - if(!result.boxEnabled && ( - result.boxStart[0] !== result.boxEnd[0] || - result.boxStart[1] !== result.boxEnd[1]) - ) { - result.boxEnabled = true; - } - - // constrain aspect ratio if the axes require it - var smallDx = Math.abs(result.dragStart[0] - x) < MINZOOM; - var smallDy = Math.abs(result.dragStart[1] - y) < MINZOOM; - if(getSubplotConstraint() && !(smallDx && smallDy)) { - dx = result.boxEnd[0] - result.boxStart[0]; - dy = result.boxEnd[1] - result.boxStart[1]; - var dydx = (dataBox[3] - dataBox[1]) / (dataBox[2] - dataBox[0]); - - if(Math.abs(dx * dydx) > Math.abs(dy)) { - result.boxEnd[1] = result.boxStart[1] + - Math.abs(dx) * dydx * (dy >= 0 ? 1 : -1); - - // gl-select-box clips to the plot area bounds, - // which breaks the axis constraint, so don't allow - // this box to go out of bounds - if(result.boxEnd[1] < dataBox[1]) { - result.boxEnd[1] = dataBox[1]; - result.boxEnd[0] = result.boxStart[0] + - (dataBox[1] - result.boxStart[1]) / Math.abs(dydx); - } else if(result.boxEnd[1] > dataBox[3]) { - result.boxEnd[1] = dataBox[3]; - result.boxEnd[0] = result.boxStart[0] + - (dataBox[3] - result.boxStart[1]) / Math.abs(dydx); - } - } else { - result.boxEnd[0] = result.boxStart[0] + - Math.abs(dy) / dydx * (dx >= 0 ? 1 : -1); - - if(result.boxEnd[0] < dataBox[0]) { - result.boxEnd[0] = dataBox[0]; - result.boxEnd[1] = result.boxStart[1] + - (dataBox[0] - result.boxStart[0]) * Math.abs(dydx); - } else if(result.boxEnd[0] > dataBox[2]) { - result.boxEnd[0] = dataBox[2]; - result.boxEnd[1] = result.boxStart[1] + - (dataBox[2] - result.boxStart[0]) * Math.abs(dydx); - } - } - } else { - // otherwise clamp small changes to the origin so we get 1D zoom - - if(smallDx) result.boxEnd[0] = result.boxStart[0]; - if(smallDy) result.boxEnd[1] = result.boxStart[1]; - } - } else if(result.boxEnabled) { - dx = result.boxStart[0] !== result.boxEnd[0]; - dy = result.boxStart[1] !== result.boxEnd[1]; - if(dx || dy) { - if(dx) { - updateRange(0, result.boxStart[0], result.boxEnd[0]); - scene.xaxis.autorange = false; - } - if(dy) { - updateRange(1, result.boxStart[1], result.boxEnd[1]); - scene.yaxis.autorange = false; - } - scene.relayoutCallback(); - } else { - scene.glplot.setDirty(); - } - result.boxEnabled = false; - result.boxInited = false; - } else if(result.boxInited) { - // if box was inited but button released then - reset the box - - result.boxInited = false; - } - break; - - case 'pan': - result.boxEnabled = false; - result.boxInited = false; - - if(buttons) { - if(!result.panning) { - result.dragStart[0] = x; - result.dragStart[1] = y; - } - - if(Math.abs(result.dragStart[0] - x) < MINDRAG) x = result.dragStart[0]; - if(Math.abs(result.dragStart[1] - y) < MINDRAG) y = result.dragStart[1]; - - dx = (lastX - x) * (dataBox[2] - dataBox[0]) / - (plot.viewBox[2] - plot.viewBox[0]); - 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.wheelListener = mouseWheel(element, function(dx, dy) { - if(!scene.scrollZoom) return false; - - var dataBox = scene.calcDataBox(); - var viewBox = plot.viewBox; - - var lastX = result.lastPos[0]; - var lastY = result.lastPos[1]; - - var scale = Math.exp(5.0 * 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]; - - 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); - - result.lastInputTime = Date.now(); - unSetAutoRange(); - scene.cameraChanged(); - scene.handleAnnotations(); - scene.relayoutCallback(); - - return true; - }, true); - - return result; -} diff --git a/src/plots/gl2d/convert.js b/src/plots/gl2d/convert.js deleted file mode 100644 index 937790c718d..00000000000 --- a/src/plots/gl2d/convert.js +++ /dev/null @@ -1,241 +0,0 @@ -/** -* Copyright 2012-2021, 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 Axes = require('../cartesian/axes'); - -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; -} - -var proto = Axes2DOptions.prototype; - -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]; - var axisLetter = axisName.charAt(0); - - // get options relevant to this subplot, - // '_name' is e.g. xaxis, xaxis2, yaxis, yaxis4 ... - ax = options[this.scene[axisName]._name]; - - axTitle = ax.title.text === this.scene.fullLayout._dfltTitle[axisLetter] ? '' : ax.title.text; - - for(j = 0; j <= 2; j += 2) { - this.labelEnable[i + j] = false; - this.labels[i + j] = axTitle; - this.labelColor[i + j] = str2RGBArray(ax.title.font.color); - this.labelFont[i + j] = ax.title.font.family; - this.labelSize[i + j] = ax.title.font.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; - - 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.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; - var subplotIds = scene.fullLayout._subplots.gl2d; - var 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); -}; - -// has an axis in default position (i.e. bottom/left) ? -proto.hasAxisInDfltPos = function(axisName, ax) { - var axSide = ax.side; - - 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; - - if(axisName === 'xaxis') return (axSide === 'top'); - else if(axisName === 'yaxis') return (axSide === 'right'); -}; - -proto.getLabelPad = function(axisName, ax) { - var offsetBase = 1.5; - var fontSize = ax.title.font.size; - var 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; -}; - -proto.getTickMarkLength = function(ax) { - if(!ax.ticks) return 0; - - var ticklen = ax.ticklen; - - return (ax.ticks === 'inside') ? -ticklen : ticklen; -}; - - -function createAxes2D(scene) { - return new Axes2DOptions(scene); -} - -module.exports = createAxes2D; diff --git a/src/plots/gl2d/index.js b/src/plots/gl2d/index.js deleted file mode 100644 index 83b465e5dfd..00000000000 --- a/src/plots/gl2d/index.js +++ /dev/null @@ -1,149 +0,0 @@ -/** -* Copyright 2012-2021, 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 overrideAll = require('../../plot_api/edit_types').overrideAll; - -var Scene2D = require('./scene2d'); -var layoutGlobalAttrs = require('../layout_attributes'); -var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); -var constants = require('../cartesian/constants'); -var Cartesian = require('../cartesian'); -var fxAttrs = require('../../components/fx/layout_attributes'); -var getSubplotData = require('../get_data').getSubplotData; - -exports.name = 'gl2d'; - -exports.attr = ['xaxis', 'yaxis']; - -exports.idRoot = ['x', 'y']; - -exports.idRegex = constants.idRegex; - -exports.attrRegex = constants.attrRegex; - -exports.attributes = require('../cartesian/attributes'); - -exports.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { - if(!layoutOut._has('cartesian')) { - Cartesian.supplyLayoutDefaults(layoutIn, layoutOut, fullData); - } -}; - -// gl2d uses svg axis attributes verbatim, but overrides editType -// this could potentially be just `layoutAttributes` but it would -// still need special handling somewhere to give it precedence over -// the svg version when both are in use on one plot -exports.layoutAttrOverrides = overrideAll(Cartesian.layoutAttributes, 'plot', 'from-root'); - -// similar overrides for base plot attributes (and those added by components) -exports.baseLayoutAttrOverrides = overrideAll({ - plot_bgcolor: layoutGlobalAttrs.plot_bgcolor, - hoverlabel: fxAttrs.hoverlabel - // dragmode needs calc but only when transitioning TO lasso or select - // so for now it's left inside _relayout - // dragmode: fxAttrs.dragmode -}, 'plot', 'nested'); - -exports.plot = function plot(gd) { - var fullLayout = gd._fullLayout; - var fullData = gd._fullData; - var subplotIds = fullLayout._subplots.gl2d; - - for(var i = 0; i < subplotIds.length; i++) { - var subplotId = subplotIds[i]; - var subplotObj = fullLayout._plots[subplotId]; - var fullSubplotData = 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); - } -}; - -exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldSceneKeys = oldFullLayout._subplots.gl2d || []; - - for(var i = 0; i < oldSceneKeys.length; i++) { - var id = oldSceneKeys[i]; - var 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 = getSubplotData(newFullData, 'gl2d', id); - if(subplotData.length === 0) { - oldSubplot._scene2d.destroy(); - delete oldFullLayout._plots[id]; - } - } - - // since we use cartesian interactions, do cartesian clean - Cartesian.clean.apply(this, arguments); -}; - -exports.drawFramework = function(gd) { - if(!gd._context.staticPlot) { - Cartesian.drawFramework(gd); - } -}; - -exports.toSVG = function(gd) { - var fullLayout = gd._fullLayout; - var subplotIds = fullLayout._subplots.gl2d; - - for(var i = 0; i < subplotIds.length; i++) { - var subplot = fullLayout._plots[subplotIds[i]]; - var 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(); - } -}; - -exports.updateFx = function(gd) { - var fullLayout = gd._fullLayout; - var subplotIds = fullLayout._subplots.gl2d; - - for(var i = 0; i < subplotIds.length; i++) { - var subplotObj = fullLayout._plots[subplotIds[i]]._scene2d; - subplotObj.updateFx(fullLayout.dragmode); - } -}; diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js deleted file mode 100644 index a4ecc52cfbf..00000000000 --- a/src/plots/gl2d/scene2d.js +++ /dev/null @@ -1,718 +0,0 @@ -/** -* Copyright 2012-2021, 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 Registry = require('../../registry'); -var Axes = require('../../plots/cartesian/axes'); -var Fx = require('../../components/fx'); - -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 showNoWebGlMsg = require('../../lib/show_no_webgl_msg'); -var axisConstraints = require('../cartesian/constraints'); -var enforceAxisConstraints = axisConstraints.enforce; -var cleanAxisConstraints = axisConstraints.clean; -var doAutoRange = require('../cartesian/autorange').doAutoRange; - -var dragHelpers = require('../../components/dragelement/helpers'); -var drawMode = dragHelpers.drawMode; -var selectMode = dragHelpers.selectMode; - -var AXES = ['xaxis', 'yaxis']; -var STATIC_CANVAS, STATIC_CONTEXT; - -var SUBPLOT_PATTERN = require('../cartesian/constants').SUBPLOT_PATTERN; - - -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.scrollZoom = this.graphDiv._context._scrollZoom.cartesian; - - this.fullData = null; - this.updateRefs(fullLayout); - - this.makeFramework(); - if(this.stopped) return; - - // update options - this.glplotOptions = createOptions(this); - this.glplotOptions.merge(fullLayout); - - // create the plot - this.glplot = createPlot2D(this.glplotOptions); - - // create camera - this.camera = createCamera(this); - - // trace set - this.traces = {}; - - // create axes spikes - this.spikes = createSpikes(this.glplot); - - this.selectBox = createSelectBox(this.glplot, { - innerFill: false, - outerFill: true - }); - - // last button state - this.lastButtonState = 0; - - // last pick result - this.pickResult = null; - - // is the mouse over the plot? - // it's OK if this says true when it's not, so long as - // when we get a mouseout we set it to false before handling - this.isMouseOver = true; - - // flag to stop render loop - this.stopped = false; - - // redraw the plot - this.redraw = this.draw.bind(this); - this.redraw(); -} - -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 = this.container.querySelector('.gl-canvas-focus'); - - var gl = getContext({ - canvas: liveCanvas, - preserveDrawingBuffer: true, - premultipliedAlpha: true - }); - - if(!gl) { - showNoWebGlMsg(this); - this.stopped = true; - return; - } - - 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); - - // 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'; - mouseContainer.style['pointer-events'] = 'auto'; - - this.pickCanvas = this.container.querySelector('.gl-canvas-pick'); - - - // append canvas, hover svg and mouse div to container - var container = this.container; - container.appendChild(svgContainer); - container.appendChild(mouseContainer); - - var self = this; - mouseContainer.addEventListener('mouseout', function() { - self.isMouseOver = false; - self.unhover(); - }); - mouseContainer.addEventListener('mouseover', function() { - self.isMouseOver = true; - }); -}; - -proto.toImage = function(format) { - if(!format) format = 'png'; - - this.stopped = true; - - if(this.staticPlot) this.container.appendChild(STATIC_CANVAS); - - // update canvas size - this.updateSize(this.canvas); - - - // grab context and yank out pixels - var gl = this.glplot.gl; - var w = gl.drawingBufferWidth; - var h = gl.drawingBufferHeight; - - // force redraw - gl.clearColor(1, 1, 1, 0); - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - this.glplot.setDirty(); - this.glplot.draw(); - - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - - 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; - } - } - } - - 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); - - return dataURL; -}; - -proto.updateSize = function(canvas) { - if(!canvas) canvas = this.canvas; - - var pixelRatio = this.pixelRatio; - var fullLayout = this.fullLayout; - - var width = fullLayout.width; - var height = fullLayout.height; - var pixelWidth = Math.ceil(pixelRatio * width) |0; - var pixelHeight = Math.ceil(pixelRatio * height) |0; - - // check for resize - if(canvas.width !== pixelWidth || canvas.height !== pixelHeight) { - canvas.width = pixelWidth; - canvas.height = pixelHeight; - } - - return canvas; -}; - -proto.computeTickMarks = function() { - this.xaxis.setScale(); - this.yaxis.setScale(); - - 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 = nextTicks[j][i].text + ''; - } - } - - return nextTicks; -}; - -function compareTicks(a, b) { - for(var i = 0; i < 2; ++i) { - var aticks = a[i]; - var bticks = b[i]; - - if(aticks.length !== bticks.length) return true; - - for(var j = 0; j < aticks.length; ++j) { - if(aticks[j].x !== bticks[j].x) return true; - } - } - - return false; -} - -proto.updateRefs = function(newFullLayout) { - this.fullLayout = newFullLayout; - - var spmatch = this.id.match(SUBPLOT_PATTERN); - var xaxisName = 'xaxis' + spmatch[1]; - var yaxisName = 'yaxis' + spmatch[2]; - - this.xaxis = this.fullLayout[xaxisName]; - this.yaxis = this.fullLayout[yaxisName]; -}; - -proto.relayoutCallback = function() { - var graphDiv = this.graphDiv; - var xaxis = this.xaxis; - var yaxis = this.yaxis; - var layout = graphDiv.layout; - - // make a meaningful value to be passed on to possible 'plotly_relayout' subscriber(s) - var update = {}; - var xrange = update[xaxis._name + '.range'] = xaxis.range.slice(); - var yrange = update[yaxis._name + '.range'] = yaxis.range.slice(); - update[xaxis._name + '.autorange'] = xaxis.autorange; - update[yaxis._name + '.autorange'] = yaxis.autorange; - - Registry.call('_storeDirectGUIEdit', graphDiv.layout, graphDiv._fullLayout._preGUI, update); - - // update the input layout - var xaIn = layout[xaxis._name]; - xaIn.range = xrange; - xaIn.autorange = xaxis.autorange; - - var yaIn = layout[yaxis._name]; - yaIn.range = yrange; - yaIn.autorange = yaxis.autorange; - - // lastInputTime helps determine which one is the latest input (if async) - update.lastInputTime = this.camera.lastInputTime; - graphDiv.emit('plotly_relayout', update); -}; - -proto.cameraChanged = function() { - var camera = this.camera; - - this.glplot.setDataBox(this.calcDataBox()); - - 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(); - } -}; - -proto.handleAnnotations = function() { - var gd = this.graphDiv; - var annotations = this.fullLayout.annotations; - - 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); - } - } -}; - -proto.destroy = function() { - if(!this.glplot) return; - - var traces = this.traces; - - if(traces) { - Object.keys(traces).map(function(key) { - traces[key].dispose(); - delete traces[key]; - }); - } - - this.glplot.dispose(); - - this.container.removeChild(this.svgContainer); - this.container.removeChild(this.mouseContainer); - - this.fullData = null; - this.glplot = null; - this.stopped = true; - this.camera.mouseListener.enabled = false; - this.mouseContainer.removeEventListener('wheel', this.camera.wheelListener); - this.camera = null; -}; - -proto.plot = function(fullData, calcData, fullLayout) { - var glplot = this.glplot; - - this.updateRefs(fullLayout); - this.xaxis.clearCalc(); - this.yaxis.clearCalc(); - this.updateTraces(fullData, calcData); - this.updateFx(fullLayout.dragmode); - - var width = fullLayout.width; - var height = fullLayout.height; - - this.updateSize(this.canvas); - - var options = this.glplotOptions; - options.merge(fullLayout); - options.screenBox = [0, 0, width, height]; - - var mockGraphDiv = {_fullLayout: { - _axisConstraintGroups: this.graphDiv._fullLayout._axisConstraintGroups, - xaxis: this.xaxis, - yaxis: this.yaxis - }}; - - cleanAxisConstraints(mockGraphDiv, this.xaxis); - cleanAxisConstraints(mockGraphDiv, this.yaxis); - - var size = fullLayout._size; - var domainX = this.xaxis.domain; - var 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 - ]; - - 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 ax, i; - - for(i = 0; i < 2; ++i) { - ax = this[AXES[i]]; - ax._length = options.viewBox[i + 2] - options.viewBox[i]; - - doAutoRange(this.graphDiv, ax); - ax.setScale(); - } - - enforceAxisConstraints(mockGraphDiv); - - options.ticks = this.computeTickMarks(); - - options.dataBox = this.calcDataBox(); - - options.merge(fullLayout); - glplot.update(options); - - // force redraw so that promise is returned when rendering is completed - this.glplot.draw(); -}; - -proto.calcDataBox = function() { - var xaxis = this.xaxis; - var yaxis = this.yaxis; - var xrange = xaxis.range; - var yrange = yaxis.range; - var xr2l = xaxis.r2l; - var yr2l = yaxis.r2l; - - return [xr2l(xrange[0]), yr2l(yrange[0]), xr2l(xrange[1]), yr2l(yrange[1])]; -}; - -proto.setRanges = function(dataBox) { - var xaxis = this.xaxis; - var yaxis = this.yaxis; - var xl2r = xaxis.l2r; - var yl2r = yaxis.l2r; - - 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; - - this.fullData = fullData; - - // remove empty traces - traceIdLoop: - for(i = 0; i < traceIds.length; i++) { - var oldUid = traceIds[i]; - var oldTrace = this.traces[oldUid]; - - for(j = 0; j < fullData.length; j++) { - fullTrace = fullData[j]; - - if(fullTrace.uid === oldUid && fullTrace.type === oldTrace.type) { - continue traceIdLoop; - } - } - - oldTrace.dispose(); - delete this.traces[oldUid]; - } - - // update / create trace objects - for(i = 0; i < fullData.length; i++) { - fullTrace = fullData[i]; - var calcTrace = calcData[i]; - var traceObj = this.traces[fullTrace.uid]; - - if(traceObj) traceObj.update(fullTrace, calcTrace); - else { - traceObj = fullTrace._module.plot(this, fullTrace, calcTrace); - this.traces[fullTrace.uid] = traceObj; - } - } - - // order object per traces - this.glplot.objects.sort(function(a, b) { - return a._trace.index - b._trace.index; - }); -}; - -proto.updateFx = function(dragmode) { - // switch to svg interactions in lasso/select mode & shape drawing - if(selectMode(dragmode) || drawMode(dragmode)) { - this.pickCanvas.style['pointer-events'] = 'none'; - this.mouseContainer.style['pointer-events'] = 'none'; - } else { - this.pickCanvas.style['pointer-events'] = 'auto'; - this.mouseContainer.style['pointer-events'] = 'auto'; - } - - // set proper cursor - if(dragmode === 'pan') { - this.mouseContainer.style.cursor = 'move'; - } else if(dragmode === 'zoom') { - this.mouseContainer.style.cursor = 'crosshair'; - } else { - this.mouseContainer.style.cursor = null; - } -}; - -proto.emitPointAction = function(nextSelection, eventType) { - var uid = nextSelection.trace.uid; - var ptNumber = nextSelection.pointIndex; - var trace; - - for(var i = 0; i < this.fullData.length; i++) { - if(this.fullData[i].uid === uid) { - trace = this.fullData[i]; - } - } - - var pointData = { - x: nextSelection.traceCoord[0], - y: nextSelection.traceCoord[1], - curveNumber: trace.index, - pointNumber: ptNumber, - data: trace._input, - fullData: this.fullData, - xaxis: this.xaxis, - yaxis: this.yaxis - }; - - Fx.appendArrayPointValue(pointData, trace, ptNumber); - - this.graphDiv.emit(eventType, {points: [pointData]}); -}; - -proto.draw = function() { - if(this.stopped) return; - - requestAnimationFrame(this.redraw); - - var glplot = this.glplot; - var camera = this.camera; - var mouseListener = camera.mouseListener; - var mouseUp = this.lastButtonState === 1 && mouseListener.buttons === 0; - var fullLayout = this.fullLayout; - - this.lastButtonState = mouseListener.buttons; - - this.cameraChanged(); - - var x = mouseListener.x * glplot.pixelRatio; - var y = this.canvas.height - glplot.pixelRatio * mouseListener.y; - - var result; - - if(camera.boxEnabled && fullLayout.dragmode === 'zoom') { - this.selectBox.enabled = true; - - var selectBox = 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]) - ]; - - // 1D zoom - for(var i = 0; i < 2; i++) { - if(camera.boxStart[i] === camera.boxEnd[i]) { - selectBox[i] = glplot.dataBox[i]; - selectBox[i + 2] = glplot.dataBox[i + 2]; - } - } - - glplot.setDirty(); - } else if(!camera.panning && this.isMouseOver) { - this.selectBox.enabled = false; - - var size = fullLayout._size; - var domainX = this.xaxis.domain; - var domainY = this.yaxis.domain; - - 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); - - 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 trace = this.fullData[selection.trace.index] || {}; - var ptNumber = selection.pointIndex; - var hoverinfo = Fx.castHoverinfo(trace, fullLayout, ptNumber); - - if(hoverinfo && 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: Fx.castHoverOption(trace, ptNumber, 'bgcolor') || selection.color, - borderColor: Fx.castHoverOption(trace, ptNumber, 'bordercolor'), - fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'), - fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'), - fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color'), - nameLength: Fx.castHoverOption(trace, ptNumber, 'namelength'), - textAlign: Fx.castHoverOption(trace, ptNumber, 'align') - }, { - container: this.svgContainer, - gd: this.graphDiv - }); - } - } - } - - // Remove hover effects if we're not over a point OR - // if we're zooming or panning (in which case result is not set) - if(!result) { - this.unhover(); - } - - glplot.draw(); -}; - -proto.unhover = function() { - if(this.lastPickResult) { - this.spikes.update({}); - this.lastPickResult = null; - this.graphDiv.emit('plotly_unhover'); - Fx.loneUnhover(this.svgContainer); - } -}; - -proto.hoverFormatter = function(axisName, val) { - if(val === undefined) return undefined; - - var axis = this[axisName]; - return Axes.tickText(axis, axis.c2l(val), 'hover').text; -}; diff --git a/src/plots/plots.js b/src/plots/plots.js index e0ec0575abd..92c86505d42 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -466,7 +466,6 @@ plots.supplyDefaults = function(gd, opts) { 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'); @@ -476,12 +475,10 @@ plots.supplyDefaults = function(gd, opts) { // clean subplots and other artifacts from previous plot calls plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); - var hadGL2D = !!(oldFullLayout._has && oldFullLayout._has('gl2d')); - var hasGL2D = !!(newFullLayout._has && newFullLayout._has('gl2d')); var hadCartesian = !!(oldFullLayout._has && oldFullLayout._has('cartesian')); var hasCartesian = !!(newFullLayout._has && newFullLayout._has('cartesian')); - var hadBgLayer = hadCartesian || hadGL2D; - var hasBgLayer = hasCartesian || hasGL2D; + var hadBgLayer = hadCartesian; + var hasBgLayer = hasCartesian; if(hadBgLayer && !hasBgLayer) { // remove bgLayer oldFullLayout._bgLayer.remove(); @@ -859,7 +856,7 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa _fullLayout: newFullLayout }; - var ids = newSubplotList.cartesian.concat(newSubplotList.gl2d || []); + var ids = newSubplotList.cartesian; for(i = 0; i < ids.length; i++) { var id = ids[i]; @@ -1287,28 +1284,20 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac var subplots = layout._subplots; var subplotId = ''; - if( - visible || - basePlotModule.name !== 'gl2d' // for now just drop empty gl2d subplots - // TODO - currently if we draw an empty gl2d subplot, it draws - // nothing then gets stuck and you can't get it back without newPlot - // sort this out in the regl refactor? - ) { - if(Array.isArray(subplotAttr)) { - for(i = 0; i < subplotAttr.length; i++) { - var attri = subplotAttr[i]; - var vali = Lib.coerce(traceIn, traceOut, subplotAttrs, attri); + if(Array.isArray(subplotAttr)) { + for(i = 0; i < subplotAttr.length; i++) { + var attri = subplotAttr[i]; + var vali = Lib.coerce(traceIn, traceOut, subplotAttrs, attri); - if(subplots[attri]) Lib.pushUnique(subplots[attri], vali); - subplotId += vali; - } - } else { - subplotId = Lib.coerce(traceIn, traceOut, subplotAttrs, subplotAttr); + if(subplots[attri]) Lib.pushUnique(subplots[attri], vali); + subplotId += vali; } + } else { + subplotId = Lib.coerce(traceIn, traceOut, subplotAttrs, subplotAttr); + } - if(subplots[basePlotModule.name]) { - Lib.pushUnique(subplots[basePlotModule.name], subplotId); - } + if(subplots[basePlotModule.name]) { + Lib.pushUnique(subplots[basePlotModule.name], subplotId); } } } diff --git a/src/snapshot/helpers.js b/src/snapshot/helpers.js index ede584c7e06..0ac671d48c6 100644 --- a/src/snapshot/helpers.js +++ b/src/snapshot/helpers.js @@ -15,7 +15,6 @@ exports.getDelay = function(fullLayout) { return ( fullLayout._has('gl3d') || - fullLayout._has('gl2d') || fullLayout._has('mapbox') ) ? 500 : 0; }; diff --git a/src/traces/contourgl/convert.js b/src/traces/contourgl/convert.js deleted file mode 100644 index d6f830d0f49..00000000000 --- a/src/traces/contourgl/convert.js +++ /dev/null @@ -1,187 +0,0 @@ -/** -* Copyright 2012-2021, 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 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'); - - -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; -} - -var proto = Contour.prototype; - -proto.handlePick = function(pickResult) { - var options = this.heatmapOptions; - var shape = options.shape; - var index = pickResult.pointId; - var xIndex = index % shape[0]; - var yIndex = Math.floor(index / shape[0]); - var 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: [yIndex, xIndex], - hoverinfo: this.hoverinfo - }; -}; - -proto.update = function(fullTrace, calcTrace) { - var calcPt = calcTrace[0]; - - this.index = fullTrace.index; - this.name = fullTrace.name; - this.hoverinfo = fullTrace.hoverinfo; - - // convert z from 2D -> 1D - var z = calcPt.z; - var rowLen = z[0].length; - var colLen = z.length; - var 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); - - var xa = this.scene.xaxis; - var ya = this.scene.yaxis; - fullTrace._extremes[xa._id] = Axes.findExtremes(xa, calcPt.x); - fullTrace._extremes[ya._id] = Axes.findExtremes(ya, calcPt.y); -}; - -proto.dispose = function() { - this.contour.dispose(); - this.heatmap.dispose(); -}; - -function flattenZ(zIn, rowLen, colLen) { - 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]; - } - } - - return zOut; -} - -function convertColorScale(fullTrace, options) { - var contours = fullTrace.contours; - var start = contours.start; - var end = contours.end; - var cs = contours.size || 1; - var 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 - var levels = new Array(N); - var 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 - }; -} - -function createContour(scene, fullTrace, calcTrace) { - var plot = new Contour(scene, fullTrace.uid); - plot.update(fullTrace, calcTrace); - - return plot; -} - -module.exports = createContour; diff --git a/src/traces/contourgl/index.js b/src/traces/contourgl/index.js deleted file mode 100644 index faef6353ab5..00000000000 --- a/src/traces/contourgl/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/** -* Copyright 2012-2021, 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 overrideAll = require('../../plot_api/edit_types').overrideAll; - -module.exports = { - attributes: overrideAll(require('../contour/attributes'), 'calc', 'nested'), - supplyDefaults: require('../contour/defaults'), - colorbar: require('../contour/colorbar'), - - calc: require('../contour/calc'), - plot: require('./convert'), - - moduleType: 'trace', - name: 'contourgl', - basePlotModule: require('../../plots/gl2d'), - categories: ['gl', 'gl2d', '2dMap'], - meta: { - description: [ - 'WebGL contour (beta)' - ].join(' ') - } -}; diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index 7d62fb2d849..38a1811a723 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -29,7 +29,6 @@ module.exports = function calc(gd, trace) { var ya = Axes.getFromId(gd, trace.yaxis || 'y'); var isContour = Registry.traceIs(trace, 'contour'); var isHist = Registry.traceIs(trace, 'histogram'); - var isGL2D = Registry.traceIs(trace, 'gl2d'); var zsmooth = isContour ? 'best' : trace.zsmooth; var x, x0, dx, origX; var y, y0, dy, origY; @@ -133,11 +132,8 @@ module.exports = function calc(gd, trace) { var yIn = trace.ytype === 'scaled' ? '' : y; var yArray = makeBoundArray(trace, yIn, y0, dy, z.length, ya); - // handled in gl2d convert step - if(!isGL2D) { - trace._extremes[xa._id] = Axes.findExtremes(xa, xArray); - trace._extremes[ya._id] = Axes.findExtremes(ya, yArray); - } + trace._extremes[xa._id] = Axes.findExtremes(xa, xArray); + trace._extremes[ya._id] = Axes.findExtremes(ya, yArray); var cd0 = { x: xArray, diff --git a/src/traces/heatmap/make_bound_array.js b/src/traces/heatmap/make_bound_array.js index aad299e565d..0e035646770 100644 --- a/src/traces/heatmap/make_bound_array.js +++ b/src/traces/heatmap/make_bound_array.js @@ -15,7 +15,6 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, var arrayOut = []; var isContour = Registry.traceIs(trace, 'contour'); var isHist = Registry.traceIs(trace, 'histogram'); - var isGL2D = Registry.traceIs(trace, 'gl2d'); var v0; var dv; var i; @@ -30,7 +29,7 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, // 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); + if(isContour) arrayOut = arrayIn.slice(0, numbricks); else if(numbricks === 1) { arrayOut = [arrayIn[0] - 0.5, arrayIn[0] + 0.5]; } else { @@ -77,7 +76,7 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, dv = dvIn || 1; - for(i = (isContour || isGL2D) ? 0 : -0.5; i < numbricks; i++) { + for(i = (isContour) ? 0 : -0.5; i < numbricks; i++) { arrayOut.push(v0 + dv * i); } } diff --git a/src/traces/heatmap/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js index 66ea1874e28..c3308698e8d 100644 --- a/src/traces/heatmap/xyz_defaults.js +++ b/src/traces/heatmap/xyz_defaults.js @@ -44,11 +44,6 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x traceOut._length = null; } - if( - traceIn.type === 'heatmapgl' || - traceIn.type === 'contourgl' - ) return true; // skip calendars until we handle them in those traces - var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); handleCalendarDefaults(traceIn, traceOut, [xName, yName], layout); diff --git a/src/traces/heatmapgl/attributes.js b/src/traces/heatmapgl/attributes.js deleted file mode 100644 index b78f17ab812..00000000000 --- a/src/traces/heatmapgl/attributes.js +++ /dev/null @@ -1,46 +0,0 @@ -/** -* Copyright 2012-2021, 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 heatmapAttrs = require('../heatmap/attributes'); -var colorScaleAttrs = require('../../components/colorscale/attributes'); - -var extendFlat = require('../../lib/extend').extendFlat; -var overrideAll = require('../../plot_api/edit_types').overrideAll; - -var commonList = [ - '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]; -} - -attrs.zsmooth = { - valType: 'enumerated', - values: ['fast', false], - dflt: 'fast', - role: 'style', - editType: 'calc', - description: 'Picks a smoothing algorithm use to smooth `z` data.' -}; - -extendFlat( - attrs, - colorScaleAttrs('', {cLetter: 'z', autoColorDflt: false}) -); - -module.exports = overrideAll(attrs, 'calc', 'nested'); diff --git a/src/traces/heatmapgl/convert.js b/src/traces/heatmapgl/convert.js deleted file mode 100644 index 0bc3ca9f554..00000000000 --- a/src/traces/heatmapgl/convert.js +++ /dev/null @@ -1,150 +0,0 @@ -/** -* Copyright 2012-2021, 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 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 = { - zsmooth: 'fast', - 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; - var shape = options.shape; - var index = pickResult.pointId; - var xIndex = index % shape[0]; - var yIndex = Math.floor(index / shape[0]); - var 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: [yIndex, xIndex], - hoverinfo: this.hoverinfo - }; -}; - -proto.update = function(fullTrace, calcTrace) { - var calcPt = calcTrace[0]; - - this.index = fullTrace.index; - this.name = fullTrace.name; - this.hoverinfo = fullTrace.hoverinfo; - - // convert z from 2D -> 1D - var z = calcPt.z; - this.options.z = [].concat.apply([], z); - - var rowLen = z[0].length; - var colLen = z.length; - this.options.shape = [rowLen, colLen]; - - this.options.x = calcPt.x; - this.options.y = calcPt.y; - this.options.zsmooth = fullTrace.zsmooth; - - 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); - - this.heatmap.update(this.options); - - var xa = this.scene.xaxis; - var ya = this.scene.yaxis; - - var xOpts, yOpts; - if(fullTrace.zsmooth === false) { - // increase padding for discretised heatmap as suggested by Louise Ord - xOpts = { ppad: calcPt.x[1] - calcPt.x[0] }; - yOpts = { ppad: calcPt.y[1] - calcPt.y[0] }; - } - - fullTrace._extremes[xa._id] = Axes.findExtremes(xa, calcPt.x, xOpts); - fullTrace._extremes[ya._id] = Axes.findExtremes(ya, calcPt.y, yOpts); -}; - -proto.dispose = function() { - this.heatmap.dispose(); -}; - -function convertColorscale(fullTrace) { - var scl = fullTrace.colorscale; - var zmin = fullTrace.zmin; - var zmax = fullTrace.zmax; - - var N = scl.length; - var domain = new Array(N); - var range = new Array(4 * N); - - for(var i = 0; i < N; i++) { - var si = scl[i]; - var color = str2RGBArray(si[1]); - - domain[i] = zmin + si[0] * (zmax - zmin); - - for(var j = 0; j < 4; j++) { - range[(4 * i) + j] = color[j]; - } - } - - return { - colorLevels: domain, - colorValues: range - }; -} - -function createHeatmap(scene, fullTrace, calcTrace) { - var plot = new Heatmap(scene, fullTrace.uid); - plot.update(fullTrace, calcTrace); - return plot; -} - -module.exports = createHeatmap; diff --git a/src/traces/heatmapgl/defaults.js b/src/traces/heatmapgl/defaults.js deleted file mode 100644 index e95c90491a1..00000000000 --- a/src/traces/heatmapgl/defaults.js +++ /dev/null @@ -1,34 +0,0 @@ -/** -* Copyright 2012-2021, 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 handleXYZDefaults = require('../heatmap/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 validData = handleXYZDefaults(traceIn, traceOut, coerce, layout); - if(!validData) { - traceOut.visible = false; - return; - } - - coerce('text'); - coerce('zsmooth'); - - colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'}); -}; diff --git a/src/traces/heatmapgl/index.js b/src/traces/heatmapgl/index.js deleted file mode 100644 index efabb704dbe..00000000000 --- a/src/traces/heatmapgl/index.js +++ /dev/null @@ -1,28 +0,0 @@ -/** -* Copyright 2012-2021, 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 = { - attributes: require('./attributes'), - supplyDefaults: require('./defaults'), - colorbar: require('../heatmap/colorbar'), - - calc: require('../heatmap/calc'), - plot: require('./convert'), - - moduleType: 'trace', - name: 'heatmapgl', - basePlotModule: require('../../plots/gl2d'), - categories: ['gl', 'gl2d', '2dMap'], - meta: { - description: [ - 'WebGL version of the heatmap trace type.' - ].join(' ') - } -}; diff --git a/src/traces/pointcloud/attributes.js b/src/traces/pointcloud/attributes.js deleted file mode 100644 index 88cc8e81452..00000000000 --- a/src/traces/pointcloud/attributes.js +++ /dev/null @@ -1,146 +0,0 @@ -/** -* Copyright 2012-2021, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - -var scatterglAttrs = require('../scatter/attributes'); - -module.exports = { - x: scatterglAttrs.x, - y: scatterglAttrs.y, - xy: { - valType: 'data_array', - editType: 'calc', - 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', - editType: 'calc', - 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', - editType: 'calc', - 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', - editType: 'calc', - 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', - editType: 'calc', - 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', - editType: 'calc', - 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', - editType: 'calc', - 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', - editType: 'calc', - 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', - editType: 'calc', - 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', - editType: 'calc', - 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', - editType: 'calc', - description: [ - 'Specifies what fraction of the marker area is covered with the', - 'border.' - ].join(' ') - }, - editType: 'calc' - }, - editType: 'calc' - }, - transforms: undefined -}; diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js deleted file mode 100644 index 9735071ba54..00000000000 --- a/src/traces/pointcloud/convert.js +++ /dev/null @@ -1,200 +0,0 @@ -/** -* Copyright 2012-2021, 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 createPointCloudRenderer = require('gl-pointcloud2d'); - -var str2RGBArray = require('../../lib/str2rgbarray'); -var findExtremes = require('../../plots/cartesian/autorange').findExtremes; -var getTraceColor = require('../scatter/get_trace_color'); - -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 -} - -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 - }; -}; - -proto.update = function(options) { - this.index = options.index; - this.textLabels = options.text; - this.name = options.name; - this.hoverinfo = options.hoverinfo; - this.bounds = [Infinity, Infinity, -Infinity, -Infinity]; - - this.updateFast(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; - var idToIndex; - var positions; - var 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 { - 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; - } - } - - if(index) { - idToIndex = index; - } else { - idToIndex = new Int32Array(len); - - for(i = 0; i < len; i++) { - idToIndex[i] = i; - } - } - } else { - len = x.length; - - positions = new Float32Array(2 * len); - idToIndex = new Int32Array(len); - - for(i = 0; i < len; i++) { - xx = x[i]; - yy = y[i]; - - idToIndex[i] = i; - - 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.pointcloudOptions.positions = positions; - - var markerColor = str2RGBArray(options.marker.color); - var borderColor = str2RGBArray(options.marker.border.color); - var opacity = options.opacity * options.marker.opacity; - - 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; - - 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; - - this.pointcloud.update(this.pointcloudOptions); - - // add item for autorange routine - var xa = this.scene.xaxis; - var ya = this.scene.yaxis; - var pad = markerSizeMax / 2 || 0.5; - options._extremes[xa._id] = findExtremes(xa, [bounds[0], bounds[2]], {ppad: pad}); - options._extremes[ya._id] = findExtremes(ya, [bounds[1], bounds[3]], {ppad: pad}); -}; - -proto.dispose = function() { - this.pointcloud.dispose(); -}; - -function createPointcloud(scene, data) { - 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 deleted file mode 100644 index 0aedf577504..00000000000 --- a/src/traces/pointcloud/defaults.js +++ /dev/null @@ -1,46 +0,0 @@ -/** -* Copyright 2012-2021, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var Lib = require('../../lib'); - -var attributes = require('./attributes'); - -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'); - - // disable 1D transforms - that would defeat the purpose of this trace type, performance! - traceOut._length = null; -}; diff --git a/src/traces/pointcloud/index.js b/src/traces/pointcloud/index.js deleted file mode 100644 index 868e1cce512..00000000000 --- a/src/traces/pointcloud/index.js +++ /dev/null @@ -1,29 +0,0 @@ -/** -* Copyright 2012-2021, 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 = { - attributes: require('./attributes'), - supplyDefaults: require('./defaults'), - - // reuse the Scatter3D 'dummy' calc step so that legends know what to do - calc: require('../scatter3d/calc'), - plot: require('./convert'), - - moduleType: 'trace', - name: 'pointcloud', - basePlotModule: require('../../plots/gl2d'), - categories: ['gl', 'gl2d', 'showLegend'], - meta: { - description: [ - 'The data visualized as a point cloud set in `x` and `y`', - 'using the WebGl plotting engine.' - ].join(' ') - } -}; diff --git a/test/image/baselines/gl2d_heatmapgl.png b/test/image/baselines/gl2d_heatmapgl.png deleted file mode 100644 index df8694bf2fda8782c646fad3a28e73f42241d15a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69201 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 LEyaQx_a6TrG&>aS diff --git a/test/image/compare_pixels_test.js b/test/image/compare_pixels_test.js index 9ec29d774ae..ebffb45d3aa 100644 --- a/test/image/compare_pixels_test.js +++ b/test/image/compare_pixels_test.js @@ -121,35 +121,6 @@ if(argv['skip-flaky']) { }); } -/* gl2d pointcloud and other non-regl gl2d mock(s) - * must be tested first on in order to work; - * sort them here. - * - * gl-shader appears to conflict with regl. - * We suspect that the lone gl context on CircleCI is - * having issues with dealing with the two different - * program binding algorithm. - * - * The problem will be solved by switching all our - * WebGL-based trace types to regl. - * - * More info here: - * https://github.com/plotly/plotly.js/pull/1037 - */ -function sortGl2dMockList(mockList) { - var mockNames = ['gl2d_pointcloud-basic', 'gl2d_heatmapgl']; - var pos = 0; - - mockNames.forEach(function(m) { - var ind = mockList.indexOf(m); - if(ind === -1) return; - var tmp = mockList[pos]; - mockList[pos] = m; - mockList[ind] = tmp; - pos++; - }); -} - function runInBatch(mockList) { var running = 0; @@ -283,7 +254,6 @@ function comparePixels(mockName, cb) { .on('close', checkImage); } -sortGl2dMockList(allMockList); console.log(''); // main diff --git a/test/image/mocks/gl2d_heatmapgl.json b/test/image/mocks/gl2d_heatmapgl.json deleted file mode 100644 index 768a74dead4..00000000000 --- a/test/image/mocks/gl2d_heatmapgl.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "data": [ - { - "type": "heatmapgl", - "z": [ - [ - 125, - 106, - 89, - 74, - 61, - 50, - 41, - 34, - 29, - 26 - ], - [ - 116, - 97, - 80, - 65, - 52, - 41, - 32, - 25, - 20, - 17 - ], - [ - 109, - 90, - 73, - 58, - 45, - 34, - 25, - 18, - 13, - 10 - ], - [ - 104, - 85, - 68, - 53, - 40, - 29, - 20, - 13, - 8, - 5 - ], - [ - 101, - 82, - 65, - 50, - 37, - 26, - 17, - 10, - 5, - 2 - ], - [ - 100, - 81, - 64, - 49, - 36, - 25, - 16, - 9, - 4, - 1 - ], - [ - 101, - 82, - 65, - 50, - 37, - 26, - 17, - 10, - 5, - 2 - ], - [ - 104, - 85, - 68, - 53, - 40, - 29, - 20, - 13, - 8, - 5 - ], - [ - 109, - 90, - 73, - 58, - 45, - 34, - 25, - 18, - 13, - 10 - ], - [ - 116, - 97, - 80, - 65, - 52, - 41, - 32, - 25, - 20, - 17 - ] - ], - "colorscale": "Viridis" - } - ], - "layout": { - "height": 450, - "width": 550 - } -} diff --git a/test/image/mocks/gl2d_heatmapgl_discrete.json b/test/image/mocks/gl2d_heatmapgl_discrete.json deleted file mode 100644 index cdd21d6e92c..00000000000 --- a/test/image/mocks/gl2d_heatmapgl_discrete.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "data": [ - { - "type": "heatmapgl", - "zsmooth": false, - "z": [ - [ - 125, - 106, - 89, - 74, - 61, - 50, - 41, - 34, - 29, - 26 - ], - [ - 116, - 97, - 80, - 65, - 52, - 41, - 32, - 25, - 20, - 17 - ], - [ - 109, - 90, - 73, - 58, - 45, - 34, - 25, - 18, - 13, - 10 - ], - [ - 104, - 85, - 68, - 53, - 40, - 29, - 20, - 13, - 8, - 5 - ], - [ - 101, - 82, - 65, - 50, - 37, - 26, - 17, - 10, - 5, - 2 - ], - [ - 100, - 81, - 64, - 49, - 36, - 25, - 16, - 9, - 4, - 1 - ], - [ - 101, - 82, - 65, - 50, - 37, - 26, - 17, - 10, - 5, - 2 - ], - [ - 104, - 85, - 68, - 53, - 40, - 29, - 20, - 13, - 8, - 5 - ], - [ - 109, - 90, - 73, - 58, - 45, - 34, - 25, - 18, - 13, - 10 - ], - [ - 116, - 97, - 80, - 65, - 52, - 41, - 32, - 25, - 20, - 17 - ] - ], - "colorscale": "Viridis" - } - ], - "layout": { - "height": 450, - "width": 550 - } -} diff --git a/test/image/mocks/gl2d_pointcloud-basic.json b/test/image/mocks/gl2d_pointcloud-basic.json deleted file mode 100644 index bdab3ae5510..00000000000 --- a/test/image/mocks/gl2d_pointcloud-basic.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "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] - } - ], - "layout": { - "title": {"text": "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 - } -} diff --git a/test/jasmine/assets/mock_lists.js b/test/jasmine/assets/mock_lists.js index 59af1a73109..1b5f4fc678f 100644 --- a/test/jasmine/assets/mock_lists.js +++ b/test/jasmine/assets/mock_lists.js @@ -55,10 +55,8 @@ var svgMockList = [ ]; var glMockList = [ - ['gl2d_heatmapgl', require('@mocks/gl2d_heatmapgl.json')], ['gl2d_line_dash', require('@mocks/gl2d_line_dash.json')], ['gl2d_parcoords_2', require('@mocks/gl2d_parcoords_2.json')], - ['gl2d_pointcloud-basic', require('@mocks/gl2d_pointcloud-basic.json')], ['gl3d_annotations', require('@mocks/gl3d_annotations.json')], ['gl3d_set-ranges', require('@mocks/gl3d_set-ranges.json')], ['gl3d_world-cals', require('@mocks/gl3d_world-cals.json')], diff --git a/test/jasmine/bundle_tests/no_webgl_test.js b/test/jasmine/bundle_tests/no_webgl_test.js index 10a22e858c5..ea4d3979b1c 100644 --- a/test/jasmine/bundle_tests/no_webgl_test.js +++ b/test/jasmine/bundle_tests/no_webgl_test.js @@ -37,18 +37,6 @@ describe('Plotly w/o WebGL support:', function() { .then(done, done.fail); }); - it('gl2d subplots', function(done) { - Plotly.react(gd, require('@mocks/gl2d_pointcloud-basic.json')) - .then(function() { - checkNoWebGLMsg(true); - return Plotly.react(gd, require('@mocks/10.json')); - }) - .then(function() { - checkNoWebGLMsg(false); - }) - .then(done, done.fail); - }); - it('scattergl subplots', function(done) { Plotly.react(gd, require('@mocks/gl2d_12.json')) .then(function() { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 88113fa191b..87d63e8f4f7 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -2544,8 +2544,7 @@ describe('Test axes', function() { var gd = { _fullLayout: { _subplots: { - cartesian: ['x2y2'], - gl2d: ['xy'] + cartesian: ['xy', 'x2y2'], } } }; diff --git a/test/jasmine/tests/contourgl_test.js b/test/jasmine/tests/contourgl_test.js deleted file mode 100644 index 580c834a7a5..00000000000 --- a/test/jasmine/tests/contourgl_test.js +++ /dev/null @@ -1,288 +0,0 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); -var d3Range = require('../../strict-d3').range; -var supplyDefaults = require('@src/traces/heatmapgl').supplyDefaults; -var Plots = require('@src/plots/plots'); - -// contourgl is not part of the dist plotly.js bundle initially -Plotly.register([ - require('@lib/contourgl') -]); - -var schema = Plotly.PlotSchema.get(); -var attributeList = Object.getOwnPropertyNames(schema.traces.heatmapgl.attributes); - -// Test utilities -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); - - -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 - } -}; - -function transpose(a) { - return a[0].map(function(ignore, columnIndex) {return a.map(function(row) {return row[columnIndex];});}); -} - -Lib.seedPseudoRandom(); - -function jitter(maxJitterRatio, n) { - return n * (1 + maxJitterRatio * (2 * Lib.pseudoRandom() - 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) - }; -} - -function generate(maxJitter) { - var x = d3Range(-1, 1.5, 0.5); // left closed, right open interval - var y = d3Range(-1, 1.5, 0.5); // left closed, right open interval - var z = new Array(x.length); - var i, j, p; - - 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} -} - -// 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 - } - }; -}; - - -function makePlot(gd, mock, done) { - return Plotly.newPlot(gd, mock.data, mock.layout) - .then(done, done.fail); -} - -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('@gl render without raising an error', function(done) { - makePlot(gd, plotData, done); - }); - - it('@gl render without raising an error', function(done) { - var mock = require('@mocks/simple_contour.json'); - var mockCopy = Lib.extendDeep({}, mock); - - mockCopy.data[0].type = 'contourgl'; - mockCopy.data[0].contours = { coloring: 'fill' }; - - makePlot(gd, mockCopy, done); - }); - - it('@gl 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('@gl render smooth, regular ellipses without raising an error (coloring: "fill")', function(done) { - var mock = plotDataElliptical(0); - makePlot(gd, mock, done); - }); - - it('@gl 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('@gl should update properly', function(done) { - var mock = plotDataElliptical(0); - var scene2d; - - Plotly.newPlot(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.range).toEqual([-1, 1]); - - return Plotly.relayout(gd, 'xaxis.range', [0, -10]); - }).then(function() { - expect(scene2d.xaxis.range).toEqual([0, -10]); - - return Plotly.relayout(gd, 'xaxis.autorange', true); - }).then(function() { - expect(scene2d.xaxis.range).toEqual([1, -1]); // autorange keeps its reversal - - return Plotly.restyle(gd, 'type', 'heatmapgl'); - }).then(function() { - expect(scene2d.traces[mock.data[0].uid].type).toEqual('heatmapgl'); - expect(scene2d.xaxis.range).toEqual([1, -1]); - - return Plotly.relayout(gd, 'xaxis.range', [0, -10]); - }).then(function() { - expect(scene2d.xaxis.range).toEqual([0, -10]); - - return Plotly.relayout(gd, 'xaxis.autorange', true); - }).then(function() { - expect(scene2d.xaxis.range).toEqual([1, -1]); - }) - .then(done, done.fail); - }); -}); - -describe('heatmapgl supplyDefaults', function() { - 'use strict'; - - var traceIn; - var traceOut; - - var defaultColor = '#444'; - var layout = { - font: Plots.layoutAttributes.font, - _dfltTitle: {colorbar: 'cb'}, - _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']} - }; - - beforeEach(function() { - traceOut = {}; - }); - - it('should only coerce attributes that are part of scheme', function() { - traceIn = { - type: 'contourgl', - z: [[0, 1], [1, 0]] - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - var allKeys = Object.getOwnPropertyNames(traceOut); - allKeys.forEach(function(key) { - if(key[0] !== '_') { - expect(attributeList.indexOf(key)).not.toBe(-1, key); - } - }); - }); -}); diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index 14c61c0db42..85dd9355309 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -18,34 +18,10 @@ var hover = require('../assets/hover'); var delay = require('../assets/delay'); var mouseEvent = require('../assets/mouse_event'); -// contourgl is not part of the dist plotly.js bundle initially -Plotly.register([ - require('@lib/contourgl') -]); - var mock0 = require('@mocks/gl2d_scatter-continuous-clustering.json'); var mock1 = 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, 20, 10.625], - [0, 0.625, 2.5, 5.625, 10] - ], - colorscale: 'Jet', - // contours: { start: 2, end: 10, size: 1 }, - zmin: 0, - zmax: 20 - }], - layout: {} -}; -var mock4 = { +var mock2 = { data: [{ x: [1, 2, 3, 4], y: [12, 3, 14, 4], @@ -411,33 +387,6 @@ describe('Test hover and click interactions', function() { .then(done, done.fail); }); - it('@gl should output correct event data for pointcloud', function(done) { - var _mock = Lib.extendDeep({}, mock2); - - _mock.layout.hoverlabel = { font: {size: 8} }; - _mock.data[2].hoverlabel = { - bgcolor: ['red', 'green', 'blue'] - }; - - var run = makeRunner([540, 150], { - x: 4.5, - y: 9, - curveNumber: 2, - pointNumber: 1, - bgcolor: 'rgb(0, 128, 0)', - bordercolor: 'rgb(255, 255, 255)', - fontSize: 8, - fontFamily: 'Arial', - fontColor: 'rgb(255, 255, 255)' - }, { - msg: 'pointcloud' - }); - - Plotly.newPlot(gd, _mock) - .then(run) - .then(done, done.fail); - }); - it('@gl scatter3d should propagate marker colors to hover labels', function(done) { var _mock = Lib.extendDeep({}, mock0); _mock.layout.width = 800; @@ -454,71 +403,7 @@ describe('Test hover and click interactions', function() { fontFamily: 'Arial', fontColor: 'rgb(68, 68, 68)' }, { - msg: 'pointcloud' - }); - - Plotly.newPlot(gd, _mock) - .then(run) - .then(done, done.fail); - }); - - it('@gl should output correct event data for heatmapgl', function(done) { - var _mock = Lib.extendDeep({}, mock3); - _mock.data[0].type = 'heatmapgl'; - - _mock.data[0].hoverlabel = { - font: { size: _mock.data[0].z } - }; - - _mock.layout.hoverlabel = { - font: { family: 'Roboto' } - }; - - var run = makeRunner([540, 150], { - x: 3, - y: 3, - curveNumber: 0, - pointNumber: [3, 3], - bgcolor: 'rgb(68, 68, 68)', - bordercolor: 'rgb(255, 255, 255)', - fontSize: 20, - fontFamily: 'Roboto', - fontColor: 'rgb(255, 255, 255)' - }, { - noUnHover: true, - msg: 'heatmapgl' - }); - - Plotly.newPlot(gd, _mock) - .then(run) - .then(done, done.fail); - }); - - it('@gl should output correct event data for heatmapgl (asymmetric case) ', function(done) { - var _mock = { - data: [{ - type: 'heatmapgl', - z: [[1, 2, 0], [2, 3, 1]], - text: [['a', 'b', 'c'], ['D', 'E', 'F']], - hoverlabel: { - bgcolor: [['red', 'blue', 'green'], ['cyan', 'pink', 'black']] - } - }] - }; - - var run = makeRunner([540, 150], { - x: 2, - y: 1, - curveNumber: 0, - pointNumber: [1, 2], - bgcolor: 'rgb(0, 0, 0)', - bordercolor: 'rgb(255, 255, 255)', - fontSize: 13, - fontFamily: 'Arial', - fontColor: 'rgb(255, 255, 255)' - }, { - noUnHover: true, - msg: 'heatmapgl' + msg: 'scattergl marker colors' }); Plotly.newPlot(gd, _mock) @@ -527,7 +412,7 @@ describe('Test hover and click interactions', function() { }); it('@gl should output correct event data for scattergl after visibility restyle', function(done) { - var _mock = Lib.extendDeep({}, mock4); + var _mock = Lib.extendDeep({}, mock2); var run = makeRunner([435, 216], { x: 8, @@ -568,7 +453,7 @@ describe('Test hover and click interactions', function() { }); it('@gl should output correct event data for scattergl-fancy', function(done) { - var _mock = Lib.extendDeep({}, mock4); + var _mock = Lib.extendDeep({}, mock2); _mock.data[0].mode = 'markers+lines'; _mock.data[1].mode = 'markers+lines'; _mock.data[2].mode = 'markers+lines'; @@ -613,33 +498,6 @@ describe('Test hover and click interactions', function() { .then(run2) .then(done, done.fail); }); - - it('@gl should output correct event data contourgl', function(done) { - var _mock = Lib.extendDeep({}, mock3); - - _mock.data[0].hoverlabel = { - font: { size: _mock.data[0].z } - }; - - var run = makeRunner([540, 150], { - x: 3, - y: 3, - curveNumber: 0, - pointNumber: [3, 3], - bgcolor: 'rgb(68, 68, 68)', - bordercolor: 'rgb(255, 255, 255)', - fontSize: 20, - fontFamily: 'Arial', - fontColor: 'rgb(255, 255, 255)' - }, { - noUnHover: true, - msg: 'contourgl' - }); - - Plotly.newPlot(gd, _mock) - .then(run) - .then(done, done.fail); - }); }); describe('hover with (x|y)period positioning', function() { diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index 5325c55753e..e28a5f4bc32 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -528,7 +528,7 @@ describe('heatmap calc', function() { expect(out.z).toBeCloseTo2DArray([[1, 2, 3], [3, 1, 2]]); }); - ['heatmap', 'heatmapgl'].forEach(function(traceType) { + ['heatmap'].forEach(function(traceType) { it('should sort z data based on axis categoryorder for ' + traceType, function() { var mock = require('@mocks/heatmap_categoryorder'); var mockCopy = Lib.extendDeep({}, mock); diff --git a/test/jasmine/tests/heatmapgl_test.js b/test/jasmine/tests/heatmapgl_test.js deleted file mode 100644 index c4e7a5b7478..00000000000 --- a/test/jasmine/tests/heatmapgl_test.js +++ /dev/null @@ -1,99 +0,0 @@ -var supplyDefaults = require('@src/traces/heatmapgl').supplyDefaults; -var Plots = require('@src/plots/plots'); -var Plotly = require('@lib/index'); -var schema = Plotly.PlotSchema.get(); -var attributeList = Object.getOwnPropertyNames(schema.traces.heatmapgl.attributes); - -describe('heatmapgl supplyDefaults', function() { - 'use strict'; - - var traceIn; - var traceOut; - - var defaultColor = '#444'; - var layout = { - font: Plots.layoutAttributes.font, - _dfltTitle: {colorbar: 'cb'}, - _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']} - }; - - 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: 'heatmapgl', - z: [[1, 2], []] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - - traceIn = { - type: 'heatmapgl', - z: [[], [1, 2], [1, 2, 3]] - }; - supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.visible).toBe(false); - }); - - it('should set visible to false when z is non-numeric', function() { - traceIn = { - type: 'heatmapgl', - 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 only coerce attributes that are part of scheme', function() { - traceIn = { - type: 'heatmapgl', - z: [[0, 1], [1, 0]] - }; - - supplyDefaults(traceIn, traceOut, defaultColor, layout); - var allKeys = Object.getOwnPropertyNames(traceOut); - allKeys.forEach(function(key) { - if(key[0] !== '_') { - expect(attributeList.indexOf(key)).not.toBe(-1, key); - } - }); - }); -}); diff --git a/test/jasmine/tests/mock_test.js b/test/jasmine/tests/mock_test.js index 44811150551..c242130a2ec 100644 --- a/test/jasmine/tests/mock_test.js +++ b/test/jasmine/tests/mock_test.js @@ -387,8 +387,6 @@ var list = [ 'gl2d_fill_trace_tozero_order', 'gl2d_fill-ordering', 'gl2d_fonts', - 'gl2d_heatmapgl', - 'gl2d_heatmapgl_discrete', 'gl2d_horiz-lines', 'gl2d_layout_image', 'gl2d_line_aligned', @@ -426,7 +424,6 @@ var list = [ 'gl2d_parcoords_tick_format', 'gl2d_period_positioning', 'gl2d_point-selection', - 'gl2d_pointcloud-basic', 'gl2d_rgb_dont_accept_alpha_scattergl', 'gl2d_scatter_fill_self_next', 'gl2d_scatter_fill_self_next_vs_nogl', @@ -1475,8 +1472,6 @@ figs['gl2d_error_bars_log'] = require('@mocks/gl2d_error_bars_log'); // figs['gl2d_fill_trace_tozero_order'] = require('@mocks/gl2d_fill_trace_tozero_order'); figs['gl2d_fill-ordering'] = require('@mocks/gl2d_fill-ordering'); // figs['gl2d_fonts'] = require('@mocks/gl2d_fonts'); -figs['gl2d_heatmapgl'] = require('@mocks/gl2d_heatmapgl'); -figs['gl2d_heatmapgl_discrete'] = require('@mocks/gl2d_heatmapgl_discrete'); figs['gl2d_horiz-lines'] = require('@mocks/gl2d_horiz-lines'); // figs['gl2d_layout_image'] = require('@mocks/gl2d_layout_image'); figs['gl2d_line_aligned'] = require('@mocks/gl2d_line_aligned'); @@ -1514,7 +1509,6 @@ figs['gl2d_parcoords_style_labels'] = require('@mocks/gl2d_parcoords_style_label figs['gl2d_parcoords_tick_format'] = require('@mocks/gl2d_parcoords_tick_format'); figs['gl2d_period_positioning'] = require('@mocks/gl2d_period_positioning'); figs['gl2d_point-selection'] = require('@mocks/gl2d_point-selection'); -// figs['gl2d_pointcloud-basic'] = require('@mocks/gl2d_pointcloud-basic'); // figs['gl2d_rgb_dont_accept_alpha_scattergl'] = require('@mocks/gl2d_rgb_dont_accept_alpha_scattergl'); figs['gl2d_scatter_fill_self_next'] = require('@mocks/gl2d_scatter_fill_self_next'); figs['gl2d_scatter_fill_self_next_vs_nogl'] = require('@mocks/gl2d_scatter_fill_self_next_vs_nogl'); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index a6b52bd45b1..a8410f9f031 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -521,25 +521,6 @@ describe('ModeBar', function() { checkButtons(modeBar, buttons, 1); }); - it('creates mode bar (gl2d version)', function() { - var buttons = getButtons([ - ['toImage'], - ['zoom2d', 'pan2d'], - ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], - ['hoverClosestGl2d'] - ]); - - var gd = getMockGraphInfo(['x'], ['y']); - gd._fullLayout._basePlotModules = [{ name: 'gl2d' }]; - gd._fullLayout.xaxis = {fixedrange: false}; - gd._fullData = [{type: 'scattergl'}]; - - manageModeBar(gd); - var modeBar = gd._fullLayout._modeBar; - - checkButtons(modeBar, buttons, 1); - }); - it('creates mode bar (pie version)', function() { var buttons = getButtons([ ['toImage'], diff --git a/test/jasmine/tests/plot_api_react_test.js b/test/jasmine/tests/plot_api_react_test.js index 51bc4f07a35..4338cbd6097 100644 --- a/test/jasmine/tests/plot_api_react_test.js +++ b/test/jasmine/tests/plot_api_react_test.js @@ -761,10 +761,6 @@ describe('@noCIdep Plotly.react', function() { for(itemType in Registry.modules) { typesTested[itemType] = 0; } for(itemType in Registry.transformsRegistry) { typesTested[itemType] = 0; } - // Not really being supported... This isn't part of the main bundle, and it's pretty broken, - // but it gets registered and used by a couple of the gl2d tests. - delete typesTested.contourgl; - function _runReactMock(mockSpec, done) { var mock = mockSpec[1]; var initialJson; diff --git a/test/jasmine/tests/pointcloud_test.js b/test/jasmine/tests/pointcloud_test.js deleted file mode 100644 index 3fba5e52381..00000000000 --- a/test/jasmine/tests/pointcloud_test.js +++ /dev/null @@ -1,255 +0,0 @@ -'use strict'; - -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); -var d3Select = require('../../strict-d3').select; - -// Test utilities -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); - -var delay = require('../assets/delay'); -var mouseEvent = require('../assets/mouse_event'); -var readPixel = require('../assets/read_pixel'); - -var multipleScatter2dMock = require('@mocks/gl2d_scatter2d-multiple-colors.json'); - -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 - } -}; - -describe('pointcloud traces', function() { - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); - - it('@gl renders without raising an error', function(done) { - Plotly.newPlot(gd, Lib.extendDeep({}, plotData)) - .then(done, done.fail); - }); - - it('@gl should update properly', function(done) { - var scene2d; - - Plotly.newPlot(gd, Lib.extendDeep({}, plotData)) - .then(function() { - scene2d = gd._fullLayout._plots.xy._scene2d; - expect(scene2d.traces[gd._fullData[0].uid].type).toBe('pointcloud'); - - return Plotly.relayout(gd, 'xaxis.range', [3, 6]); - }).then(function() { - expect(scene2d.xaxis.range).toEqual([3, 6]); - expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); - return Plotly.relayout(gd, 'xaxis.autorange', true); - }).then(function() { - expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); - expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); - return Plotly.relayout(gd, 'yaxis.range', [8, 20]); - }).then(function() { - expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); - expect(scene2d.yaxis.range).toEqual([8, 20]); - return Plotly.relayout(gd, 'yaxis.autorange', true); - }).then(function() { - expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); - expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); - }) - .then(done, done.fail); - }); - - it('@gl should not change other traces colors', function(done) { - var _mock = Lib.extendDeep({}, multipleScatter2dMock); - Plotly.newPlot(gd, _mock) - .then(delay(20)) - .then(function() { - var canvas = d3Select('.gl-canvas-context').node(); - - var RGBA = readPixel(canvas, canvas.width / 2 - 1, canvas.height / 2 - 1, 1, 1); - - expect(RGBA[0] === 255).toBe(true, 'be red'); - expect(RGBA[1] === 0).toBe(true, 'no green'); - expect(RGBA[2] === 0).toBe(true, 'no blue'); - expect(RGBA[3] === 255).toBe(true, 'no transparent'); - }) - .then(done, done.fail); - }); - - it('@gl should respond to drag', function(done) { - function _drag(p0, p1) { - mouseEvent('mousemove', p0[0], p0[1], {buttons: 1}); - mouseEvent('mousedown', p0[0], p0[1], {buttons: 1}); - mouseEvent('mousemove', (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2, {buttons: 1}); - mouseEvent('mousemove', p1[0], p1[1], {buttons: 0}); - mouseEvent('mouseup', p1[0], p1[1], {buttons: 0}); - } - - function _assertRange(msg, xrng, yrng) { - expect(gd._fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg); - expect(gd._fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg); - } - - Plotly.newPlot(gd, Lib.extendDeep({}, plotData)) - .then(delay(20)) - .then(function() { - _assertRange('base', [-0.548, 9.548], [-1.415, 10.415]); - }) - .then(delay(20)) - .then(function() { _drag([200, 200], [350, 350]); }) - .then(delay(20)) - .then(function() { - _assertRange('after zoombox drag', [0.768, 1.591], [5.462, 7.584]); - }) - .then(function() { - return Plotly.relayout(gd, { - 'xaxis.autorange': true, - 'yaxis.autorange': true - }); - }) - .then(function() { - _assertRange('back to base', [-0.548, 9.548], [-1.415, 10.415]); - }) - .then(function() { - return Plotly.relayout(gd, 'dragmode', 'pan'); - }) - .then(delay(20)) - .then(function() { _drag([200, 200], [350, 350]); }) - .then(delay(20)) - .then(function() { - _assertRange('after pan drag', [0.2743, 10.3719], [-3.537, 8.292]); - }) - .then(done, done.fail); - }); -}); From 530281a439a51896167b3c9a01c9ec86b5ff7a83 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 14 Jan 2021 17:17:57 -0500 Subject: [PATCH 2/3] reimplement heatmapgl --- lib/heatmapgl.js | 11 + lib/index-gl2d.js | 1 + lib/index.js | 2 + package-lock.json | 245 +++++- package.json | 5 +- src/components/images/draw.js | 1 + src/components/modebar/buttons.js | 11 + src/components/modebar/manage.js | 9 +- src/components/rangeslider/helpers.js | 18 +- src/plot_api/plot_api.js | 2 + src/plot_api/plot_schema.js | 3 +- src/plot_api/subroutines.js | 3 + src/plots/cartesian/axes.js | 7 +- src/plots/cartesian/include_components.js | 4 +- src/plots/cartesian/index.js | 2 +- src/plots/cartesian/layout_defaults.js | 2 +- src/plots/get_data.js | 17 +- src/plots/gl2d/camera.js | 293 +++++++ src/plots/gl2d/convert.js | 241 ++++++ src/plots/gl2d/index.js | 149 ++++ src/plots/gl2d/scene2d.js | 718 ++++++++++++++++++ src/plots/plots.js | 39 +- src/snapshot/helpers.js | 1 + src/traces/heatmap/calc.js | 8 +- src/traces/heatmap/make_bound_array.js | 5 +- src/traces/heatmap/xyz_defaults.js | 2 + src/traces/heatmapgl/attributes.js | 46 ++ src/traces/heatmapgl/convert.js | 150 ++++ src/traces/heatmapgl/defaults.js | 34 + src/traces/heatmapgl/index.js | 28 + test/image/baselines/gl2d_heatmapgl.png | Bin 0 -> 69201 bytes .../baselines/gl2d_heatmapgl_discrete.png | Bin 0 -> 18173 bytes test/image/compare_pixels_test.js | 30 + test/image/mocks/gl2d_heatmapgl.json | 134 ++++ test/image/mocks/gl2d_heatmapgl_discrete.json | 135 ++++ test/jasmine/assets/mock_lists.js | 1 + test/jasmine/bundle_tests/no_webgl_test.js | 12 + test/jasmine/tests/axes_test.js | 3 +- test/jasmine/tests/gl2d_click_test.js | 82 +- test/jasmine/tests/heatmap_test.js | 2 +- test/jasmine/tests/heatmapgl_test.js | 99 +++ test/jasmine/tests/mock_test.js | 4 + test/jasmine/tests/modebar_test.js | 19 + 43 files changed, 2513 insertions(+), 65 deletions(-) create mode 100644 lib/heatmapgl.js create mode 100644 src/plots/gl2d/camera.js create mode 100644 src/plots/gl2d/convert.js create mode 100644 src/plots/gl2d/index.js create mode 100644 src/plots/gl2d/scene2d.js create mode 100644 src/traces/heatmapgl/attributes.js create mode 100644 src/traces/heatmapgl/convert.js create mode 100644 src/traces/heatmapgl/defaults.js create mode 100644 src/traces/heatmapgl/index.js create mode 100644 test/image/baselines/gl2d_heatmapgl.png create mode 100644 test/image/baselines/gl2d_heatmapgl_discrete.png create mode 100644 test/image/mocks/gl2d_heatmapgl.json create mode 100644 test/image/mocks/gl2d_heatmapgl_discrete.json create mode 100644 test/jasmine/tests/heatmapgl_test.js diff --git a/lib/heatmapgl.js b/lib/heatmapgl.js new file mode 100644 index 00000000000..8554aad30ef --- /dev/null +++ b/lib/heatmapgl.js @@ -0,0 +1,11 @@ +/** +* Copyright 2012-2021, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = require('../src/traces/heatmapgl'); diff --git a/lib/index-gl2d.js b/lib/index-gl2d.js index 478f1d617bc..a6d010cd910 100644 --- a/lib/index-gl2d.js +++ b/lib/index-gl2d.js @@ -13,6 +13,7 @@ var Plotly = require('./core'); Plotly.register([ require('./scattergl'), require('./splom'), + require('./heatmapgl'), require('./parcoords') ]); diff --git a/lib/index.js b/lib/index.js index 4b1be68f8db..443fa207259 100644 --- a/lib/index.js +++ b/lib/index.js @@ -44,6 +44,8 @@ Plotly.register([ require('./scattergl'), require('./splom'), + require('./heatmapgl'), + require('./parcoords'), require('./parcats'), diff --git a/package-lock.json b/package-lock.json index 7fb7d2a01a4..8f24af22d2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5105,6 +5105,99 @@ "sprintf-js": "^1.0.3" } }, + "gl-heatmap2d": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gl-heatmap2d/-/gl-heatmap2d-1.1.0.tgz", + "integrity": "sha512-0FLXyxv6UBCzzhi4Q2u+9fUs6BX1+r5ZztFe27VikE9FUVw7hZiuSHmgDng92EpydogcSYHXCIK8+58RagODug==", + "requires": { + "binary-search-bounds": "^2.0.4", + "gl-buffer": "^2.1.2", + "gl-shader": "^4.2.1", + "glslify": "^7.0.0", + "iota-array": "^1.0.0", + "typedarray-pool": "^1.2.0" + }, + "dependencies": { + "glslify": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", + "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", + "requires": { + "bl": "^2.2.1", + "concat-stream": "^1.5.2", + "duplexify": "^3.4.5", + "falafel": "^2.1.0", + "from2": "^2.3.0", + "glsl-resolve": "0.0.1", + "glsl-token-whitespace-trim": "^1.0.0", + "glslify-bundle": "^5.0.0", + "glslify-deps": "^1.2.5", + "minimist": "^1.2.5", + "resolve": "^1.1.5", + "stack-trace": "0.0.9", + "static-eval": "^2.0.5", + "through2": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "glslify-deps": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.2.tgz", + "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", + "requires": { + "@choojs/findup": "^0.2.0", + "events": "^3.2.0", + "glsl-resolve": "0.0.1", + "glsl-tokenizer": "^2.0.0", + "graceful-fs": "^4.1.2", + "inherits": "^2.0.1", + "map-limit": "0.0.1", + "resolve": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, "gl-line3d": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/gl-line3d/-/gl-line3d-1.2.1.tgz", @@ -5312,27 +5405,18 @@ } } }, - "gl-plot3d": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.7.tgz", - "integrity": "sha512-mLDVWrl4Dj0O0druWyHUK5l7cBQrRIJRn2oROEgrRuOgbbrLAzsREKefwMO0bA0YqkiZMFMnV5VvPA9j57X5Xg==", + "gl-plot2d": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/gl-plot2d/-/gl-plot2d-1.4.5.tgz", + "integrity": "sha512-6GmCN10SWtV+qHFQ1gjdnVubeHFVsm6P4zmo0HrPIl9TcdePCUHDlBKWAuE6XtFhiMKMj7R8rApOX8O8uXUYog==", "requires": { - "3d-view": "^2.0.0", - "a-big-triangle": "^1.0.3", - "gl-axes3d": "^1.5.3", - "gl-fbo": "^2.0.5", - "gl-mat4": "^1.2.0", + "binary-search-bounds": "^2.0.4", + "gl-buffer": "^2.1.2", "gl-select-static": "^2.0.7", "gl-shader": "^4.2.1", - "gl-spikes3d": "^1.0.10", + "glsl-inverse": "^1.0.0", "glslify": "^7.0.0", - "has-passive-events": "^1.0.0", - "is-mobile": "^2.2.1", - "mouse-change": "^1.4.0", - "mouse-event-offset": "^3.0.2", - "mouse-wheel": "^1.2.0", - "ndarray": "^1.0.19", - "right-now": "^1.0.0" + "text-cache": "^4.2.2" }, "dependencies": { "glslify": { @@ -5415,15 +5499,27 @@ } } }, - "gl-pointcloud2d": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/gl-pointcloud2d/-/gl-pointcloud2d-1.0.3.tgz", - "integrity": "sha512-OS2e1irvJXVRpg/GziXj10xrFJm9kkRfFoB6BLUvkjCQV7ZRNNcs2CD+YSK1r0gvMwTg2T3lfLM3UPwNtz+4Xw==", + "gl-plot3d": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-2.4.7.tgz", + "integrity": "sha512-mLDVWrl4Dj0O0druWyHUK5l7cBQrRIJRn2oROEgrRuOgbbrLAzsREKefwMO0bA0YqkiZMFMnV5VvPA9j57X5Xg==", "requires": { - "gl-buffer": "^2.1.2", + "3d-view": "^2.0.0", + "a-big-triangle": "^1.0.3", + "gl-axes3d": "^1.5.3", + "gl-fbo": "^2.0.5", + "gl-mat4": "^1.2.0", + "gl-select-static": "^2.0.7", "gl-shader": "^4.2.1", + "gl-spikes3d": "^1.0.10", "glslify": "^7.0.0", - "typedarray-pool": "^1.1.0" + "has-passive-events": "^1.0.0", + "is-mobile": "^2.2.1", + "mouse-change": "^1.4.0", + "mouse-event-offset": "^3.0.2", + "mouse-wheel": "^1.2.0", + "ndarray": "^1.0.19", + "right-now": "^1.0.0" }, "dependencies": { "glslify": { @@ -5619,6 +5715,96 @@ } } }, + "gl-select-box": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/gl-select-box/-/gl-select-box-1.0.4.tgz", + "integrity": "sha512-mKsCnglraSKyBbQiGq0Ila0WF+m6Tr+EWT2yfaMn/Sh9aMHq5Wt0F/l6Cf/Ed3CdERq5jHWAY5yxLviZteYu2w==", + "requires": { + "gl-buffer": "^2.1.2", + "gl-shader": "^4.2.1", + "glslify": "^7.0.0" + }, + "dependencies": { + "glslify": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", + "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", + "requires": { + "bl": "^2.2.1", + "concat-stream": "^1.5.2", + "duplexify": "^3.4.5", + "falafel": "^2.1.0", + "from2": "^2.3.0", + "glsl-resolve": "0.0.1", + "glsl-token-whitespace-trim": "^1.0.0", + "glslify-bundle": "^5.0.0", + "glslify-deps": "^1.2.5", + "minimist": "^1.2.5", + "resolve": "^1.1.5", + "stack-trace": "0.0.9", + "static-eval": "^2.0.5", + "through2": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "glslify-deps": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.2.tgz", + "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", + "requires": { + "@choojs/findup": "^0.2.0", + "events": "^3.2.0", + "glsl-resolve": "0.0.1", + "glsl-tokenizer": "^2.0.0", + "graceful-fs": "^4.1.2", + "inherits": "^2.0.1", + "map-limit": "0.0.1", + "resolve": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, "gl-select-static": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/gl-select-static/-/gl-select-static-2.0.7.tgz", @@ -5639,6 +5825,11 @@ "weakmap-shim": "^1.1.0" } }, + "gl-spikes2d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/gl-spikes2d/-/gl-spikes2d-1.0.2.tgz", + "integrity": "sha512-QVeOZsi9nQuJJl7NB3132CCv5KA10BWxAY2QgJNsKqbLsG53B/TrGJpjIAohnJftdZ4fT6b3ZojWgeaXk8bOOA==" + }, "gl-spikes3d": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/gl-spikes3d/-/gl-spikes3d-1.0.10.tgz", @@ -11889,6 +12080,14 @@ "source-map-support": "~0.5.12" } }, + "text-cache": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/text-cache/-/text-cache-4.2.2.tgz", + "integrity": "sha512-zky+UDYiX0a/aPw/YTBD+EzKMlCTu1chFuCMZeAkgoRiceySdROu1V2kJXhCbtEdBhiOviYnAdGiSYl58HW0ZQ==", + "requires": { + "vectorize-text": "^3.2.1" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index d02c0b49e6e..ad0bf2d7816 100644 --- a/package.json +++ b/package.json @@ -80,12 +80,15 @@ "fast-isnumeric": "^1.1.4", "gl-cone3d": "^1.5.2", "gl-error3d": "^1.0.16", + "gl-heatmap2d": "^1.1.0", "gl-line3d": "1.2.1", "gl-mat4": "^1.2.0", "gl-mesh3d": "^2.3.1", + "gl-plot2d": "^1.4.5", "gl-plot3d": "^2.4.7", - "gl-pointcloud2d": "^1.0.3", "gl-scatter3d": "^1.2.3", + "gl-select-box": "^1.0.4", + "gl-spikes2d": "^1.0.2", "gl-streamtube3d": "^1.4.1", "gl-surface3d": "^1.6.0", "gl-text": "^1.1.8", diff --git a/src/components/images/draw.js b/src/components/images/draw.js index c97c6524147..61dcfe5757e 100644 --- a/src/components/images/draw.js +++ b/src/components/images/draw.js @@ -237,6 +237,7 @@ module.exports = function draw(gd) { var subplotObj = fullLayout._plots[subplot]; // filter out overlaid plots (which have their images on the main plot) + // and heatmapgl plots (which don't support below images, at least not yet) if(!subplotObj.imagelayer) continue; var imagesOnSubplot = subplotObj.imagelayer.selectAll('image') diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 55f42e84b76..af0ef757fcd 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -544,6 +544,17 @@ function handleGeo(gd, ev) { } } +modeBarButtons.hoverClosestGl2d = { + name: 'hoverClosestGl2d', + title: function(gd) { return _(gd, '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: function(gd) { return _(gd, 'Toggle show closest data on hover'); }, diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 971d98629cd..264e8f0062b 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -89,6 +89,7 @@ function getButtonGroups(gd) { var hasGeo = fullLayout._has('geo'); var hasPie = fullLayout._has('pie'); var hasFunnelarea = fullLayout._has('funnelarea'); + var hasHeatmapgl = fullLayout._has('gl2d'); var hasTernary = fullLayout._has('ternary'); var hasMapbox = fullLayout._has('mapbox'); var hasPolar = fullLayout._has('polar'); @@ -123,7 +124,7 @@ function getButtonGroups(gd) { var resetGroup = []; var dragModeGroup = []; - if((hasCartesian || hasPie || hasFunnelarea || hasTernary) + hasGeo + hasGL3D + hasMapbox + hasPolar > 1) { + if((hasCartesian || hasHeatmapgl || hasPie || hasFunnelarea || hasTernary) + hasGeo + hasGL3D + hasMapbox + hasPolar > 1) { // graphs with more than one plot types get 'union buttons' // which reset the view or toggle hover labels across all subplots. hoverGroup = ['toggleHover']; @@ -139,6 +140,8 @@ function getButtonGroups(gd) { zoomGroup = ['zoomInMapbox', 'zoomOutMapbox']; hoverGroup = ['toggleHover']; resetGroup = ['resetViewMapbox']; + } else if(hasHeatmapgl) { + hoverGroup = ['hoverClosestGl2d']; } else if(hasPie) { hoverGroup = ['hoverClosestPie']; } else if(hasSankey) { @@ -158,14 +161,14 @@ function getButtonGroups(gd) { hoverGroup = []; } - if((hasCartesian) && !allAxesFixed) { + if((hasCartesian || hasHeatmapgl) && !allAxesFixed) { zoomGroup = ['zoomIn2d', 'zoomOut2d', 'autoScale2d']; if(resetGroup[0] !== 'resetViews') resetGroup = ['resetScale2d']; } if(hasGL3D) { dragModeGroup = ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']; - } else if(((hasCartesian) && !allAxesFixed) || hasTernary) { + } else if(((hasCartesian || hasHeatmapgl) && !allAxesFixed) || hasTernary) { dragModeGroup = ['zoom2d', 'pan2d']; } else if(hasMapbox || hasGeo) { dragModeGroup = ['pan2d']; diff --git a/src/components/rangeslider/helpers.js b/src/components/rangeslider/helpers.js index abbb0a04667..4f32a3320c0 100644 --- a/src/components/rangeslider/helpers.js +++ b/src/components/rangeslider/helpers.js @@ -25,16 +25,18 @@ exports.makeData = function(fullLayout) { var margin = fullLayout.margin; var rangeSliderData = []; - for(var i = 0; i < axes.length; i++) { - var ax = axes[i]; + if(!fullLayout._has('gl2d')) { + for(var i = 0; i < axes.length; i++) { + var ax = axes[i]; - if(isVisible(ax)) { - rangeSliderData.push(ax); + if(isVisible(ax)) { + rangeSliderData.push(ax); - var opts = ax[name]; - opts._id = name + ax._id; - opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness; - opts._offsetShift = Math.floor(opts.borderwidth / 2); + var opts = ax[name]; + opts._id = name + ax._id; + opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness; + opts._offsetShift = Math.floor(opts.borderwidth / 2); + } } } diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 89a20696dd1..3ae3bcf42d6 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2191,6 +2191,8 @@ function _relayout(gd, aobj) { !(vOld === 'lasso' || vOld === 'select')) ) { flags.plot = true; + } else if(fullLayout._has('gl2d')) { + flags.plot = true; } else if(valObject) editTypes.update(flags, valObject); else flags.calc = true; diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 2502bb313a8..1fa14250aaa 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -332,7 +332,7 @@ function layoutHeadAttr(fullLayout, head) { _module = basePlotModules[i]; if(_module.attrRegex && _module.attrRegex.test(head)) { // if a module defines overrides, these take precedence - // initially this was to allow gl2d different editTypes from svg cartesian + // initially this was to allow heatmapgl different editTypes from svg cartesian if(_module.layoutAttrOverrides) return _module.layoutAttrOverrides; // otherwise take the first attributes we find @@ -340,6 +340,7 @@ function layoutHeadAttr(fullLayout, head) { } // a module can also override the behavior of base (and component) module layout attrs + // again see heatmapgl for initial use case var baseOverrides = _module.baseLayoutAttrOverrides; if(baseOverrides && head in baseOverrides) return baseOverrides[head]; } diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index b6a92c332fd..d0b0fe6ee6e 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -68,6 +68,9 @@ function lsInner(gd) { exports.drawMainTitle(gd); ModeBar.manage(gd); + // _has('cartesian') means SVG specifically, not heatmapgl - but heatmapgl + // can still get here because it makes some of the SVG structure + // for shared features like selections. if(!fullLayout._has('cartesian')) { return Plots.previousPromises(gd); } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index a1d3f2956d2..0f7da3b3716 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -102,6 +102,8 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption if(!extraOption) extraOption = dflt; axlist = axlist.concat(axlist.map(function(x) { return x + ' domain'; })); + // data-ref annotations are not supported in heatmapgl yet + attrDef[refAttr] = { valType: 'enumerated', values: axlist.concat(extraOption ? @@ -1838,7 +1840,7 @@ axes.getTickFormat = function(ax) { // ideally we get rid of it there (or just copy this there) and remove it here axes.getSubplots = function(gd, ax) { var subplotObj = gd._fullLayout._subplots; - var allSubplots = subplotObj.cartesian; + var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []); var out = ax ? axes.findSubplotsWithAxis(allSubplots, ax) : allSubplots; @@ -1854,7 +1856,8 @@ axes.getSubplots = function(gd, ax) { }; // find all subplots with axis 'ax' -// NOTE: this is only used in axes.getSubplots (only used outside plotly.js) +// NOTE: this is only used in axes.getSubplots (only used outside plotly.js) and +// gl2d/convert (where it restricts axis subplots to only those with gl2d) axes.findSubplotsWithAxis = function(subplots, ax) { var axMatch = new RegExp( (ax._id.charAt(0) === 'x') ? ('^' + ax._id + 'y') : (ax._id + '$') diff --git a/src/plots/cartesian/include_components.js b/src/plots/cartesian/include_components.js index 2c8677bc46c..2ca206406ac 100644 --- a/src/plots/cartesian/include_components.js +++ b/src/plots/cartesian/include_components.js @@ -35,7 +35,7 @@ module.exports = function makeIncludeComponents(containerArrayName) { var xaList = subplots.xaxis; var yaList = subplots.yaxis; var cartesianList = subplots.cartesian; - var hasCartesian = layoutOut._has('cartesian'); + var hasCartesianOrHeatmapgl = layoutOut._has('cartesian') || layoutOut._has('gl2d'); for(var i = 0; i < array.length; i++) { var itemi = array[i]; @@ -49,7 +49,7 @@ module.exports = function makeIncludeComponents(containerArrayName) { var hasXref = idRegex.x.test(xref); var hasYref = idRegex.y.test(yref); if(hasXref || hasYref) { - if(!hasCartesian) Lib.pushUnique(layoutOut._basePlotModules, Cartesian); + if(!hasCartesianOrHeatmapgl) Lib.pushUnique(layoutOut._basePlotModules, Cartesian); var newAxis = false; if(hasXref && xaList.indexOf(xref) === -1) { diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index d77e6f511c4..c1e3b607989 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -52,7 +52,7 @@ exports.finalizeSubplots = function(layoutIn, layoutOut) { var xList = subplots.xaxis; var yList = subplots.yaxis; var spSVG = subplots.cartesian; - var spAll = spSVG; + var spAll = spSVG.concat(subplots.gl2d || []); var allX = {}; var allY = {}; var i, xi, yi; diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index c42580cbe2b..f3a23402048 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -55,7 +55,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { // look for axes in the data for(i = 0; i < fullData.length; i++) { var trace = fullData[i]; - if(!traceIs(trace, 'cartesian')) continue; + if(!traceIs(trace, 'cartesian') && !traceIs(trace, 'gl2d')) continue; var xaName; if(trace.xaxis) { diff --git a/src/plots/get_data.js b/src/plots/get_data.js index e255c2aff0a..78fcb7d2149 100644 --- a/src/plots/get_data.js +++ b/src/plots/get_data.js @@ -9,6 +9,7 @@ 'use strict'; var Registry = require('../registry'); +var SUBPLOT_PATTERN = require('./cartesian/constants').SUBPLOT_PATTERN; /** * Get calcdata trace(s) associated with a given subplot @@ -102,12 +103,24 @@ exports.getSubplotData = function getSubplotData(data, type, subplotId) { var attr = Registry.subplotsRegistry[type].attr; var subplotData = []; - var trace; + var trace, subplotX, subplotY; + + if(type === 'gl2d') { + var spmatch = subplotId.match(SUBPLOT_PATTERN); + subplotX = 'x' + spmatch[1]; + subplotY = 'y' + spmatch[2]; + } for(var i = 0; i < data.length; i++) { trace = data[i]; - if(trace[attr] === subplotId) subplotData.push(trace); + if(type === 'gl2d' && Registry.traceIs(trace, 'gl2d')) { + if(trace[attr[0]] === subplotX && trace[attr[1]] === subplotY) { + subplotData.push(trace); + } + } else { + if(trace[attr] === subplotId) subplotData.push(trace); + } } return subplotData; diff --git a/src/plots/gl2d/camera.js b/src/plots/gl2d/camera.js new file mode 100644 index 00000000000..60f21f0fcec --- /dev/null +++ b/src/plots/gl2d/camera.js @@ -0,0 +1,293 @@ +/** +* Copyright 2012-2021, 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 mouseChange = require('mouse-change'); +var mouseWheel = require('mouse-wheel'); +var mouseOffset = require('mouse-event-offset'); +var cartesianConstants = require('../cartesian/constants'); +var hasPassive = require('has-passive-events'); + +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.boxInited = false; + this.boxStart = [0, 0]; + this.boxEnd = [0, 0]; + this.dragStart = [0, 0]; +} + + +function createCamera(scene) { + var element = scene.mouseContainer; + var plot = scene.glplot; + var result = new Camera2D(element, plot); + + function unSetAutoRange() { + scene.xaxis.autorange = false; + scene.yaxis.autorange = false; + } + + function getSubplotConstraint() { + // note: this assumes we only have one x and one y axis on this subplot + // when this constraint is lifted this block won't make sense + var constraints = scene.graphDiv._fullLayout._axisConstraintGroups; + var xaId = scene.xaxis._id; + var yaId = scene.yaxis._id; + for(var i = 0; i < constraints.length; i++) { + if(constraints[i][xaId] !== -1) { + if(constraints[i][yaId] !== -1) return true; + break; + } + } + return false; + } + + result.mouseListener = mouseChange(element, handleInteraction); + + // enable simple touch interactions + element.addEventListener('touchstart', function(ev) { + var xy = mouseOffset(ev.changedTouches[0], element); + handleInteraction(0, xy[0], xy[1]); + handleInteraction(1, xy[0], xy[1]); + + ev.preventDefault(); + }, hasPassive ? {passive: false} : false); + element.addEventListener('touchmove', function(ev) { + ev.preventDefault(); + var xy = mouseOffset(ev.changedTouches[0], element); + handleInteraction(1, xy[0], xy[1]); + + ev.preventDefault(); + }, hasPassive ? {passive: false} : false); + element.addEventListener('touchend', function(ev) { + handleInteraction(0, result.lastPos[0], result.lastPos[1]); + + ev.preventDefault(); + }, hasPassive ? {passive: false} : false); + + function handleInteraction(buttons, x, y) { + var dataBox = scene.calcDataBox(); + var viewBox = plot.viewBox; + + var lastX = result.lastPos[0]; + var lastY = result.lastPos[1]; + + var MINDRAG = cartesianConstants.MINDRAG * plot.pixelRatio; + var MINZOOM = cartesianConstants.MINZOOM * plot.pixelRatio; + + var dx, dy; + + 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); + var 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.boxInited) { + result.boxStart[0] = dataX; + result.boxStart[1] = dataY; + result.dragStart[0] = x; + result.dragStart[1] = y; + } + + result.boxEnd[0] = dataX; + result.boxEnd[1] = dataY; + + // we need to mark the box as initialized right away + // so that we can tell the start and end points apart + result.boxInited = true; + + // but don't actually enable the box until the cursor moves + if(!result.boxEnabled && ( + result.boxStart[0] !== result.boxEnd[0] || + result.boxStart[1] !== result.boxEnd[1]) + ) { + result.boxEnabled = true; + } + + // constrain aspect ratio if the axes require it + var smallDx = Math.abs(result.dragStart[0] - x) < MINZOOM; + var smallDy = Math.abs(result.dragStart[1] - y) < MINZOOM; + if(getSubplotConstraint() && !(smallDx && smallDy)) { + dx = result.boxEnd[0] - result.boxStart[0]; + dy = result.boxEnd[1] - result.boxStart[1]; + var dydx = (dataBox[3] - dataBox[1]) / (dataBox[2] - dataBox[0]); + + if(Math.abs(dx * dydx) > Math.abs(dy)) { + result.boxEnd[1] = result.boxStart[1] + + Math.abs(dx) * dydx * (dy >= 0 ? 1 : -1); + + // gl-select-box clips to the plot area bounds, + // which breaks the axis constraint, so don't allow + // this box to go out of bounds + if(result.boxEnd[1] < dataBox[1]) { + result.boxEnd[1] = dataBox[1]; + result.boxEnd[0] = result.boxStart[0] + + (dataBox[1] - result.boxStart[1]) / Math.abs(dydx); + } else if(result.boxEnd[1] > dataBox[3]) { + result.boxEnd[1] = dataBox[3]; + result.boxEnd[0] = result.boxStart[0] + + (dataBox[3] - result.boxStart[1]) / Math.abs(dydx); + } + } else { + result.boxEnd[0] = result.boxStart[0] + + Math.abs(dy) / dydx * (dx >= 0 ? 1 : -1); + + if(result.boxEnd[0] < dataBox[0]) { + result.boxEnd[0] = dataBox[0]; + result.boxEnd[1] = result.boxStart[1] + + (dataBox[0] - result.boxStart[0]) * Math.abs(dydx); + } else if(result.boxEnd[0] > dataBox[2]) { + result.boxEnd[0] = dataBox[2]; + result.boxEnd[1] = result.boxStart[1] + + (dataBox[2] - result.boxStart[0]) * Math.abs(dydx); + } + } + } else { + // otherwise clamp small changes to the origin so we get 1D zoom + + if(smallDx) result.boxEnd[0] = result.boxStart[0]; + if(smallDy) result.boxEnd[1] = result.boxStart[1]; + } + } else if(result.boxEnabled) { + dx = result.boxStart[0] !== result.boxEnd[0]; + dy = result.boxStart[1] !== result.boxEnd[1]; + if(dx || dy) { + if(dx) { + updateRange(0, result.boxStart[0], result.boxEnd[0]); + scene.xaxis.autorange = false; + } + if(dy) { + updateRange(1, result.boxStart[1], result.boxEnd[1]); + scene.yaxis.autorange = false; + } + scene.relayoutCallback(); + } else { + scene.glplot.setDirty(); + } + result.boxEnabled = false; + result.boxInited = false; + } else if(result.boxInited) { + // if box was inited but button released then - reset the box + + result.boxInited = false; + } + break; + + case 'pan': + result.boxEnabled = false; + result.boxInited = false; + + if(buttons) { + if(!result.panning) { + result.dragStart[0] = x; + result.dragStart[1] = y; + } + + if(Math.abs(result.dragStart[0] - x) < MINDRAG) x = result.dragStart[0]; + if(Math.abs(result.dragStart[1] - y) < MINDRAG) y = result.dragStart[1]; + + dx = (lastX - x) * (dataBox[2] - dataBox[0]) / + (plot.viewBox[2] - plot.viewBox[0]); + 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.wheelListener = mouseWheel(element, function(dx, dy) { + if(!scene.scrollZoom) return false; + + var dataBox = scene.calcDataBox(); + var viewBox = plot.viewBox; + + var lastX = result.lastPos[0]; + var lastY = result.lastPos[1]; + + var scale = Math.exp(5.0 * 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]; + + 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); + + result.lastInputTime = Date.now(); + unSetAutoRange(); + scene.cameraChanged(); + scene.handleAnnotations(); + scene.relayoutCallback(); + + return true; + }, true); + + return result; +} diff --git a/src/plots/gl2d/convert.js b/src/plots/gl2d/convert.js new file mode 100644 index 00000000000..937790c718d --- /dev/null +++ b/src/plots/gl2d/convert.js @@ -0,0 +1,241 @@ +/** +* Copyright 2012-2021, 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 Axes = require('../cartesian/axes'); + +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; +} + +var proto = Axes2DOptions.prototype; + +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]; + var axisLetter = axisName.charAt(0); + + // get options relevant to this subplot, + // '_name' is e.g. xaxis, xaxis2, yaxis, yaxis4 ... + ax = options[this.scene[axisName]._name]; + + axTitle = ax.title.text === this.scene.fullLayout._dfltTitle[axisLetter] ? '' : ax.title.text; + + for(j = 0; j <= 2; j += 2) { + this.labelEnable[i + j] = false; + this.labels[i + j] = axTitle; + this.labelColor[i + j] = str2RGBArray(ax.title.font.color); + this.labelFont[i + j] = ax.title.font.family; + this.labelSize[i + j] = ax.title.font.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; + + 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.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; + var subplotIds = scene.fullLayout._subplots.gl2d; + var 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); +}; + +// has an axis in default position (i.e. bottom/left) ? +proto.hasAxisInDfltPos = function(axisName, ax) { + var axSide = ax.side; + + 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; + + if(axisName === 'xaxis') return (axSide === 'top'); + else if(axisName === 'yaxis') return (axSide === 'right'); +}; + +proto.getLabelPad = function(axisName, ax) { + var offsetBase = 1.5; + var fontSize = ax.title.font.size; + var 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; +}; + +proto.getTickMarkLength = function(ax) { + if(!ax.ticks) return 0; + + var ticklen = ax.ticklen; + + return (ax.ticks === 'inside') ? -ticklen : ticklen; +}; + + +function createAxes2D(scene) { + return new Axes2DOptions(scene); +} + +module.exports = createAxes2D; diff --git a/src/plots/gl2d/index.js b/src/plots/gl2d/index.js new file mode 100644 index 00000000000..83b465e5dfd --- /dev/null +++ b/src/plots/gl2d/index.js @@ -0,0 +1,149 @@ +/** +* Copyright 2012-2021, 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 overrideAll = require('../../plot_api/edit_types').overrideAll; + +var Scene2D = require('./scene2d'); +var layoutGlobalAttrs = require('../layout_attributes'); +var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); +var constants = require('../cartesian/constants'); +var Cartesian = require('../cartesian'); +var fxAttrs = require('../../components/fx/layout_attributes'); +var getSubplotData = require('../get_data').getSubplotData; + +exports.name = 'gl2d'; + +exports.attr = ['xaxis', 'yaxis']; + +exports.idRoot = ['x', 'y']; + +exports.idRegex = constants.idRegex; + +exports.attrRegex = constants.attrRegex; + +exports.attributes = require('../cartesian/attributes'); + +exports.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { + if(!layoutOut._has('cartesian')) { + Cartesian.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + } +}; + +// gl2d uses svg axis attributes verbatim, but overrides editType +// this could potentially be just `layoutAttributes` but it would +// still need special handling somewhere to give it precedence over +// the svg version when both are in use on one plot +exports.layoutAttrOverrides = overrideAll(Cartesian.layoutAttributes, 'plot', 'from-root'); + +// similar overrides for base plot attributes (and those added by components) +exports.baseLayoutAttrOverrides = overrideAll({ + plot_bgcolor: layoutGlobalAttrs.plot_bgcolor, + hoverlabel: fxAttrs.hoverlabel + // dragmode needs calc but only when transitioning TO lasso or select + // so for now it's left inside _relayout + // dragmode: fxAttrs.dragmode +}, 'plot', 'nested'); + +exports.plot = function plot(gd) { + var fullLayout = gd._fullLayout; + var fullData = gd._fullData; + var subplotIds = fullLayout._subplots.gl2d; + + for(var i = 0; i < subplotIds.length; i++) { + var subplotId = subplotIds[i]; + var subplotObj = fullLayout._plots[subplotId]; + var fullSubplotData = 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); + } +}; + +exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { + var oldSceneKeys = oldFullLayout._subplots.gl2d || []; + + for(var i = 0; i < oldSceneKeys.length; i++) { + var id = oldSceneKeys[i]; + var 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 = getSubplotData(newFullData, 'gl2d', id); + if(subplotData.length === 0) { + oldSubplot._scene2d.destroy(); + delete oldFullLayout._plots[id]; + } + } + + // since we use cartesian interactions, do cartesian clean + Cartesian.clean.apply(this, arguments); +}; + +exports.drawFramework = function(gd) { + if(!gd._context.staticPlot) { + Cartesian.drawFramework(gd); + } +}; + +exports.toSVG = function(gd) { + var fullLayout = gd._fullLayout; + var subplotIds = fullLayout._subplots.gl2d; + + for(var i = 0; i < subplotIds.length; i++) { + var subplot = fullLayout._plots[subplotIds[i]]; + var 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(); + } +}; + +exports.updateFx = function(gd) { + var fullLayout = gd._fullLayout; + var subplotIds = fullLayout._subplots.gl2d; + + for(var i = 0; i < subplotIds.length; i++) { + var subplotObj = fullLayout._plots[subplotIds[i]]._scene2d; + subplotObj.updateFx(fullLayout.dragmode); + } +}; diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js new file mode 100644 index 00000000000..a4ecc52cfbf --- /dev/null +++ b/src/plots/gl2d/scene2d.js @@ -0,0 +1,718 @@ +/** +* Copyright 2012-2021, 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 Registry = require('../../registry'); +var Axes = require('../../plots/cartesian/axes'); +var Fx = require('../../components/fx'); + +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 showNoWebGlMsg = require('../../lib/show_no_webgl_msg'); +var axisConstraints = require('../cartesian/constraints'); +var enforceAxisConstraints = axisConstraints.enforce; +var cleanAxisConstraints = axisConstraints.clean; +var doAutoRange = require('../cartesian/autorange').doAutoRange; + +var dragHelpers = require('../../components/dragelement/helpers'); +var drawMode = dragHelpers.drawMode; +var selectMode = dragHelpers.selectMode; + +var AXES = ['xaxis', 'yaxis']; +var STATIC_CANVAS, STATIC_CONTEXT; + +var SUBPLOT_PATTERN = require('../cartesian/constants').SUBPLOT_PATTERN; + + +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.scrollZoom = this.graphDiv._context._scrollZoom.cartesian; + + this.fullData = null; + this.updateRefs(fullLayout); + + this.makeFramework(); + if(this.stopped) return; + + // update options + this.glplotOptions = createOptions(this); + this.glplotOptions.merge(fullLayout); + + // create the plot + this.glplot = createPlot2D(this.glplotOptions); + + // create camera + this.camera = createCamera(this); + + // trace set + this.traces = {}; + + // create axes spikes + this.spikes = createSpikes(this.glplot); + + this.selectBox = createSelectBox(this.glplot, { + innerFill: false, + outerFill: true + }); + + // last button state + this.lastButtonState = 0; + + // last pick result + this.pickResult = null; + + // is the mouse over the plot? + // it's OK if this says true when it's not, so long as + // when we get a mouseout we set it to false before handling + this.isMouseOver = true; + + // flag to stop render loop + this.stopped = false; + + // redraw the plot + this.redraw = this.draw.bind(this); + this.redraw(); +} + +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 = this.container.querySelector('.gl-canvas-focus'); + + var gl = getContext({ + canvas: liveCanvas, + preserveDrawingBuffer: true, + premultipliedAlpha: true + }); + + if(!gl) { + showNoWebGlMsg(this); + this.stopped = true; + return; + } + + 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); + + // 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'; + mouseContainer.style['pointer-events'] = 'auto'; + + this.pickCanvas = this.container.querySelector('.gl-canvas-pick'); + + + // append canvas, hover svg and mouse div to container + var container = this.container; + container.appendChild(svgContainer); + container.appendChild(mouseContainer); + + var self = this; + mouseContainer.addEventListener('mouseout', function() { + self.isMouseOver = false; + self.unhover(); + }); + mouseContainer.addEventListener('mouseover', function() { + self.isMouseOver = true; + }); +}; + +proto.toImage = function(format) { + if(!format) format = 'png'; + + this.stopped = true; + + if(this.staticPlot) this.container.appendChild(STATIC_CANVAS); + + // update canvas size + this.updateSize(this.canvas); + + + // grab context and yank out pixels + var gl = this.glplot.gl; + var w = gl.drawingBufferWidth; + var h = gl.drawingBufferHeight; + + // force redraw + gl.clearColor(1, 1, 1, 0); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + this.glplot.setDirty(); + this.glplot.draw(); + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + 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; + } + } + } + + 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); + + return dataURL; +}; + +proto.updateSize = function(canvas) { + if(!canvas) canvas = this.canvas; + + var pixelRatio = this.pixelRatio; + var fullLayout = this.fullLayout; + + var width = fullLayout.width; + var height = fullLayout.height; + var pixelWidth = Math.ceil(pixelRatio * width) |0; + var pixelHeight = Math.ceil(pixelRatio * height) |0; + + // check for resize + if(canvas.width !== pixelWidth || canvas.height !== pixelHeight) { + canvas.width = pixelWidth; + canvas.height = pixelHeight; + } + + return canvas; +}; + +proto.computeTickMarks = function() { + this.xaxis.setScale(); + this.yaxis.setScale(); + + 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 = nextTicks[j][i].text + ''; + } + } + + return nextTicks; +}; + +function compareTicks(a, b) { + for(var i = 0; i < 2; ++i) { + var aticks = a[i]; + var bticks = b[i]; + + if(aticks.length !== bticks.length) return true; + + for(var j = 0; j < aticks.length; ++j) { + if(aticks[j].x !== bticks[j].x) return true; + } + } + + return false; +} + +proto.updateRefs = function(newFullLayout) { + this.fullLayout = newFullLayout; + + var spmatch = this.id.match(SUBPLOT_PATTERN); + var xaxisName = 'xaxis' + spmatch[1]; + var yaxisName = 'yaxis' + spmatch[2]; + + this.xaxis = this.fullLayout[xaxisName]; + this.yaxis = this.fullLayout[yaxisName]; +}; + +proto.relayoutCallback = function() { + var graphDiv = this.graphDiv; + var xaxis = this.xaxis; + var yaxis = this.yaxis; + var layout = graphDiv.layout; + + // make a meaningful value to be passed on to possible 'plotly_relayout' subscriber(s) + var update = {}; + var xrange = update[xaxis._name + '.range'] = xaxis.range.slice(); + var yrange = update[yaxis._name + '.range'] = yaxis.range.slice(); + update[xaxis._name + '.autorange'] = xaxis.autorange; + update[yaxis._name + '.autorange'] = yaxis.autorange; + + Registry.call('_storeDirectGUIEdit', graphDiv.layout, graphDiv._fullLayout._preGUI, update); + + // update the input layout + var xaIn = layout[xaxis._name]; + xaIn.range = xrange; + xaIn.autorange = xaxis.autorange; + + var yaIn = layout[yaxis._name]; + yaIn.range = yrange; + yaIn.autorange = yaxis.autorange; + + // lastInputTime helps determine which one is the latest input (if async) + update.lastInputTime = this.camera.lastInputTime; + graphDiv.emit('plotly_relayout', update); +}; + +proto.cameraChanged = function() { + var camera = this.camera; + + this.glplot.setDataBox(this.calcDataBox()); + + 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(); + } +}; + +proto.handleAnnotations = function() { + var gd = this.graphDiv; + var annotations = this.fullLayout.annotations; + + 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); + } + } +}; + +proto.destroy = function() { + if(!this.glplot) return; + + var traces = this.traces; + + if(traces) { + Object.keys(traces).map(function(key) { + traces[key].dispose(); + delete traces[key]; + }); + } + + this.glplot.dispose(); + + this.container.removeChild(this.svgContainer); + this.container.removeChild(this.mouseContainer); + + this.fullData = null; + this.glplot = null; + this.stopped = true; + this.camera.mouseListener.enabled = false; + this.mouseContainer.removeEventListener('wheel', this.camera.wheelListener); + this.camera = null; +}; + +proto.plot = function(fullData, calcData, fullLayout) { + var glplot = this.glplot; + + this.updateRefs(fullLayout); + this.xaxis.clearCalc(); + this.yaxis.clearCalc(); + this.updateTraces(fullData, calcData); + this.updateFx(fullLayout.dragmode); + + var width = fullLayout.width; + var height = fullLayout.height; + + this.updateSize(this.canvas); + + var options = this.glplotOptions; + options.merge(fullLayout); + options.screenBox = [0, 0, width, height]; + + var mockGraphDiv = {_fullLayout: { + _axisConstraintGroups: this.graphDiv._fullLayout._axisConstraintGroups, + xaxis: this.xaxis, + yaxis: this.yaxis + }}; + + cleanAxisConstraints(mockGraphDiv, this.xaxis); + cleanAxisConstraints(mockGraphDiv, this.yaxis); + + var size = fullLayout._size; + var domainX = this.xaxis.domain; + var 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 + ]; + + 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 ax, i; + + for(i = 0; i < 2; ++i) { + ax = this[AXES[i]]; + ax._length = options.viewBox[i + 2] - options.viewBox[i]; + + doAutoRange(this.graphDiv, ax); + ax.setScale(); + } + + enforceAxisConstraints(mockGraphDiv); + + options.ticks = this.computeTickMarks(); + + options.dataBox = this.calcDataBox(); + + options.merge(fullLayout); + glplot.update(options); + + // force redraw so that promise is returned when rendering is completed + this.glplot.draw(); +}; + +proto.calcDataBox = function() { + var xaxis = this.xaxis; + var yaxis = this.yaxis; + var xrange = xaxis.range; + var yrange = yaxis.range; + var xr2l = xaxis.r2l; + var yr2l = yaxis.r2l; + + return [xr2l(xrange[0]), yr2l(yrange[0]), xr2l(xrange[1]), yr2l(yrange[1])]; +}; + +proto.setRanges = function(dataBox) { + var xaxis = this.xaxis; + var yaxis = this.yaxis; + var xl2r = xaxis.l2r; + var yl2r = yaxis.l2r; + + 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; + + this.fullData = fullData; + + // remove empty traces + traceIdLoop: + for(i = 0; i < traceIds.length; i++) { + var oldUid = traceIds[i]; + var oldTrace = this.traces[oldUid]; + + for(j = 0; j < fullData.length; j++) { + fullTrace = fullData[j]; + + if(fullTrace.uid === oldUid && fullTrace.type === oldTrace.type) { + continue traceIdLoop; + } + } + + oldTrace.dispose(); + delete this.traces[oldUid]; + } + + // update / create trace objects + for(i = 0; i < fullData.length; i++) { + fullTrace = fullData[i]; + var calcTrace = calcData[i]; + var traceObj = this.traces[fullTrace.uid]; + + if(traceObj) traceObj.update(fullTrace, calcTrace); + else { + traceObj = fullTrace._module.plot(this, fullTrace, calcTrace); + this.traces[fullTrace.uid] = traceObj; + } + } + + // order object per traces + this.glplot.objects.sort(function(a, b) { + return a._trace.index - b._trace.index; + }); +}; + +proto.updateFx = function(dragmode) { + // switch to svg interactions in lasso/select mode & shape drawing + if(selectMode(dragmode) || drawMode(dragmode)) { + this.pickCanvas.style['pointer-events'] = 'none'; + this.mouseContainer.style['pointer-events'] = 'none'; + } else { + this.pickCanvas.style['pointer-events'] = 'auto'; + this.mouseContainer.style['pointer-events'] = 'auto'; + } + + // set proper cursor + if(dragmode === 'pan') { + this.mouseContainer.style.cursor = 'move'; + } else if(dragmode === 'zoom') { + this.mouseContainer.style.cursor = 'crosshair'; + } else { + this.mouseContainer.style.cursor = null; + } +}; + +proto.emitPointAction = function(nextSelection, eventType) { + var uid = nextSelection.trace.uid; + var ptNumber = nextSelection.pointIndex; + var trace; + + for(var i = 0; i < this.fullData.length; i++) { + if(this.fullData[i].uid === uid) { + trace = this.fullData[i]; + } + } + + var pointData = { + x: nextSelection.traceCoord[0], + y: nextSelection.traceCoord[1], + curveNumber: trace.index, + pointNumber: ptNumber, + data: trace._input, + fullData: this.fullData, + xaxis: this.xaxis, + yaxis: this.yaxis + }; + + Fx.appendArrayPointValue(pointData, trace, ptNumber); + + this.graphDiv.emit(eventType, {points: [pointData]}); +}; + +proto.draw = function() { + if(this.stopped) return; + + requestAnimationFrame(this.redraw); + + var glplot = this.glplot; + var camera = this.camera; + var mouseListener = camera.mouseListener; + var mouseUp = this.lastButtonState === 1 && mouseListener.buttons === 0; + var fullLayout = this.fullLayout; + + this.lastButtonState = mouseListener.buttons; + + this.cameraChanged(); + + var x = mouseListener.x * glplot.pixelRatio; + var y = this.canvas.height - glplot.pixelRatio * mouseListener.y; + + var result; + + if(camera.boxEnabled && fullLayout.dragmode === 'zoom') { + this.selectBox.enabled = true; + + var selectBox = 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]) + ]; + + // 1D zoom + for(var i = 0; i < 2; i++) { + if(camera.boxStart[i] === camera.boxEnd[i]) { + selectBox[i] = glplot.dataBox[i]; + selectBox[i + 2] = glplot.dataBox[i + 2]; + } + } + + glplot.setDirty(); + } else if(!camera.panning && this.isMouseOver) { + this.selectBox.enabled = false; + + var size = fullLayout._size; + var domainX = this.xaxis.domain; + var domainY = this.yaxis.domain; + + 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); + + 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 trace = this.fullData[selection.trace.index] || {}; + var ptNumber = selection.pointIndex; + var hoverinfo = Fx.castHoverinfo(trace, fullLayout, ptNumber); + + if(hoverinfo && 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: Fx.castHoverOption(trace, ptNumber, 'bgcolor') || selection.color, + borderColor: Fx.castHoverOption(trace, ptNumber, 'bordercolor'), + fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'), + fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'), + fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color'), + nameLength: Fx.castHoverOption(trace, ptNumber, 'namelength'), + textAlign: Fx.castHoverOption(trace, ptNumber, 'align') + }, { + container: this.svgContainer, + gd: this.graphDiv + }); + } + } + } + + // Remove hover effects if we're not over a point OR + // if we're zooming or panning (in which case result is not set) + if(!result) { + this.unhover(); + } + + glplot.draw(); +}; + +proto.unhover = function() { + if(this.lastPickResult) { + this.spikes.update({}); + this.lastPickResult = null; + this.graphDiv.emit('plotly_unhover'); + Fx.loneUnhover(this.svgContainer); + } +}; + +proto.hoverFormatter = function(axisName, val) { + if(val === undefined) return undefined; + + var axis = this[axisName]; + return Axes.tickText(axis, axis.c2l(val), 'hover').text; +}; diff --git a/src/plots/plots.js b/src/plots/plots.js index 92c86505d42..09d241a0d13 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -466,6 +466,7 @@ plots.supplyDefaults = function(gd, opts) { 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'); @@ -475,10 +476,12 @@ plots.supplyDefaults = function(gd, opts) { // clean subplots and other artifacts from previous plot calls plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); + var hadHeatmapgl = !!(oldFullLayout._has && oldFullLayout._has('gl2d')); + var hasHeatmapgl = !!(newFullLayout._has && newFullLayout._has('gl2d')); var hadCartesian = !!(oldFullLayout._has && oldFullLayout._has('cartesian')); var hasCartesian = !!(newFullLayout._has && newFullLayout._has('cartesian')); - var hadBgLayer = hadCartesian; - var hasBgLayer = hasCartesian; + var hadBgLayer = hadCartesian || hadHeatmapgl; + var hasBgLayer = hasCartesian || hasHeatmapgl; if(hadBgLayer && !hasBgLayer) { // remove bgLayer oldFullLayout._bgLayer.remove(); @@ -856,7 +859,7 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa _fullLayout: newFullLayout }; - var ids = newSubplotList.cartesian; + var ids = newSubplotList.cartesian.concat(newSubplotList.gl2d || []); for(i = 0; i < ids.length; i++) { var id = ids[i]; @@ -1284,20 +1287,28 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac var subplots = layout._subplots; var subplotId = ''; - if(Array.isArray(subplotAttr)) { - for(i = 0; i < subplotAttr.length; i++) { - var attri = subplotAttr[i]; - var vali = Lib.coerce(traceIn, traceOut, subplotAttrs, attri); + if( + visible || + basePlotModule.name !== 'gl2d' // for now just drop empty gl2d subplots + // TODO - currently if we draw an empty gl2d subplot, it draws + // nothing then gets stuck and you can't get it back without newPlot + // sort this out in the regl refactor? + ) { + if(Array.isArray(subplotAttr)) { + for(i = 0; i < subplotAttr.length; i++) { + var attri = subplotAttr[i]; + var vali = Lib.coerce(traceIn, traceOut, subplotAttrs, attri); - if(subplots[attri]) Lib.pushUnique(subplots[attri], vali); - subplotId += vali; + if(subplots[attri]) Lib.pushUnique(subplots[attri], vali); + subplotId += vali; + } + } else { + subplotId = Lib.coerce(traceIn, traceOut, subplotAttrs, subplotAttr); } - } else { - subplotId = Lib.coerce(traceIn, traceOut, subplotAttrs, subplotAttr); - } - if(subplots[basePlotModule.name]) { - Lib.pushUnique(subplots[basePlotModule.name], subplotId); + if(subplots[basePlotModule.name]) { + Lib.pushUnique(subplots[basePlotModule.name], subplotId); + } } } } diff --git a/src/snapshot/helpers.js b/src/snapshot/helpers.js index 0ac671d48c6..ede584c7e06 100644 --- a/src/snapshot/helpers.js +++ b/src/snapshot/helpers.js @@ -15,6 +15,7 @@ exports.getDelay = function(fullLayout) { return ( fullLayout._has('gl3d') || + fullLayout._has('gl2d') || fullLayout._has('mapbox') ) ? 500 : 0; }; diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index 38a1811a723..7d62fb2d849 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -29,6 +29,7 @@ module.exports = function calc(gd, trace) { var ya = Axes.getFromId(gd, trace.yaxis || 'y'); var isContour = Registry.traceIs(trace, 'contour'); var isHist = Registry.traceIs(trace, 'histogram'); + var isGL2D = Registry.traceIs(trace, 'gl2d'); var zsmooth = isContour ? 'best' : trace.zsmooth; var x, x0, dx, origX; var y, y0, dy, origY; @@ -132,8 +133,11 @@ module.exports = function calc(gd, trace) { var yIn = trace.ytype === 'scaled' ? '' : y; var yArray = makeBoundArray(trace, yIn, y0, dy, z.length, ya); - trace._extremes[xa._id] = Axes.findExtremes(xa, xArray); - trace._extremes[ya._id] = Axes.findExtremes(ya, yArray); + // handled in gl2d convert step + if(!isGL2D) { + trace._extremes[xa._id] = Axes.findExtremes(xa, xArray); + trace._extremes[ya._id] = Axes.findExtremes(ya, yArray); + } var cd0 = { x: xArray, diff --git a/src/traces/heatmap/make_bound_array.js b/src/traces/heatmap/make_bound_array.js index 0e035646770..4356e8c9d80 100644 --- a/src/traces/heatmap/make_bound_array.js +++ b/src/traces/heatmap/make_bound_array.js @@ -15,6 +15,7 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, var arrayOut = []; var isContour = Registry.traceIs(trace, 'contour'); var isHist = Registry.traceIs(trace, 'histogram'); + var isHeatmapgl = Registry.traceIs(trace, 'gl2d'); var v0; var dv; var i; @@ -29,7 +30,7 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, // and extend it linearly based on the last two points if(len <= numbricks) { // contour plots only want the centers - if(isContour) arrayOut = arrayIn.slice(0, numbricks); + if(isContour || isHeatmapgl) arrayOut = arrayIn.slice(0, numbricks); else if(numbricks === 1) { arrayOut = [arrayIn[0] - 0.5, arrayIn[0] + 0.5]; } else { @@ -76,7 +77,7 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, dv = dvIn || 1; - for(i = (isContour) ? 0 : -0.5; i < numbricks; i++) { + for(i = (isContour || isHeatmapgl) ? 0 : -0.5; i < numbricks; i++) { arrayOut.push(v0 + dv * i); } } diff --git a/src/traces/heatmap/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js index c3308698e8d..1ee9e49c05c 100644 --- a/src/traces/heatmap/xyz_defaults.js +++ b/src/traces/heatmap/xyz_defaults.js @@ -44,6 +44,8 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x traceOut._length = null; } + if(traceIn.type === 'heatmapgl') return true; // skip calendars until we handle them in those traces + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); handleCalendarDefaults(traceIn, traceOut, [xName, yName], layout); diff --git a/src/traces/heatmapgl/attributes.js b/src/traces/heatmapgl/attributes.js new file mode 100644 index 00000000000..b78f17ab812 --- /dev/null +++ b/src/traces/heatmapgl/attributes.js @@ -0,0 +1,46 @@ +/** +* Copyright 2012-2021, 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 heatmapAttrs = require('../heatmap/attributes'); +var colorScaleAttrs = require('../../components/colorscale/attributes'); + +var extendFlat = require('../../lib/extend').extendFlat; +var overrideAll = require('../../plot_api/edit_types').overrideAll; + +var commonList = [ + '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]; +} + +attrs.zsmooth = { + valType: 'enumerated', + values: ['fast', false], + dflt: 'fast', + role: 'style', + editType: 'calc', + description: 'Picks a smoothing algorithm use to smooth `z` data.' +}; + +extendFlat( + attrs, + colorScaleAttrs('', {cLetter: 'z', autoColorDflt: false}) +); + +module.exports = overrideAll(attrs, 'calc', 'nested'); diff --git a/src/traces/heatmapgl/convert.js b/src/traces/heatmapgl/convert.js new file mode 100644 index 00000000000..0bc3ca9f554 --- /dev/null +++ b/src/traces/heatmapgl/convert.js @@ -0,0 +1,150 @@ +/** +* Copyright 2012-2021, 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 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 = { + zsmooth: 'fast', + 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; + var shape = options.shape; + var index = pickResult.pointId; + var xIndex = index % shape[0]; + var yIndex = Math.floor(index / shape[0]); + var 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: [yIndex, xIndex], + hoverinfo: this.hoverinfo + }; +}; + +proto.update = function(fullTrace, calcTrace) { + var calcPt = calcTrace[0]; + + this.index = fullTrace.index; + this.name = fullTrace.name; + this.hoverinfo = fullTrace.hoverinfo; + + // convert z from 2D -> 1D + var z = calcPt.z; + this.options.z = [].concat.apply([], z); + + var rowLen = z[0].length; + var colLen = z.length; + this.options.shape = [rowLen, colLen]; + + this.options.x = calcPt.x; + this.options.y = calcPt.y; + this.options.zsmooth = fullTrace.zsmooth; + + 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); + + this.heatmap.update(this.options); + + var xa = this.scene.xaxis; + var ya = this.scene.yaxis; + + var xOpts, yOpts; + if(fullTrace.zsmooth === false) { + // increase padding for discretised heatmap as suggested by Louise Ord + xOpts = { ppad: calcPt.x[1] - calcPt.x[0] }; + yOpts = { ppad: calcPt.y[1] - calcPt.y[0] }; + } + + fullTrace._extremes[xa._id] = Axes.findExtremes(xa, calcPt.x, xOpts); + fullTrace._extremes[ya._id] = Axes.findExtremes(ya, calcPt.y, yOpts); +}; + +proto.dispose = function() { + this.heatmap.dispose(); +}; + +function convertColorscale(fullTrace) { + var scl = fullTrace.colorscale; + var zmin = fullTrace.zmin; + var zmax = fullTrace.zmax; + + var N = scl.length; + var domain = new Array(N); + var range = new Array(4 * N); + + for(var i = 0; i < N; i++) { + var si = scl[i]; + var color = str2RGBArray(si[1]); + + domain[i] = zmin + si[0] * (zmax - zmin); + + for(var j = 0; j < 4; j++) { + range[(4 * i) + j] = color[j]; + } + } + + return { + colorLevels: domain, + colorValues: range + }; +} + +function createHeatmap(scene, fullTrace, calcTrace) { + var plot = new Heatmap(scene, fullTrace.uid); + plot.update(fullTrace, calcTrace); + return plot; +} + +module.exports = createHeatmap; diff --git a/src/traces/heatmapgl/defaults.js b/src/traces/heatmapgl/defaults.js new file mode 100644 index 00000000000..e95c90491a1 --- /dev/null +++ b/src/traces/heatmapgl/defaults.js @@ -0,0 +1,34 @@ +/** +* Copyright 2012-2021, 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 handleXYZDefaults = require('../heatmap/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 validData = handleXYZDefaults(traceIn, traceOut, coerce, layout); + if(!validData) { + traceOut.visible = false; + return; + } + + coerce('text'); + coerce('zsmooth'); + + colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'}); +}; diff --git a/src/traces/heatmapgl/index.js b/src/traces/heatmapgl/index.js new file mode 100644 index 00000000000..efabb704dbe --- /dev/null +++ b/src/traces/heatmapgl/index.js @@ -0,0 +1,28 @@ +/** +* Copyright 2012-2021, 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 = { + attributes: require('./attributes'), + supplyDefaults: require('./defaults'), + colorbar: require('../heatmap/colorbar'), + + calc: require('../heatmap/calc'), + plot: require('./convert'), + + moduleType: 'trace', + name: 'heatmapgl', + basePlotModule: require('../../plots/gl2d'), + categories: ['gl', 'gl2d', '2dMap'], + meta: { + description: [ + 'WebGL version of the heatmap trace type.' + ].join(' ') + } +}; diff --git a/test/image/baselines/gl2d_heatmapgl.png b/test/image/baselines/gl2d_heatmapgl.png new file mode 100644 index 0000000000000000000000000000000000000000..df8694bf2fda8782c646fad3a28e73f42241d15a GIT binary patch literal 69201 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 Date: Thu, 14 Jan 2021 19:06:31 -0500 Subject: [PATCH 3/3] improve transition test --- test/jasmine/tests/transition_test.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index a5c5f24d9fe..c106aa952c7 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -1196,25 +1196,21 @@ describe('Plotly.react transitions:', function() { gd.layout.xaxis.range = ['2018-06-01', '2019-06-01']; gd.layout.xaxis2.range = [0.5, 1.5]; - var promise = Plotly.react(gd, gd.data, gd.layout); - setTimeout(function() { - var fullLayout = gd._fullLayout; + return Plotly.react(gd, gd.data, gd.layout); + }).then(function() { + var fullLayout = gd._fullLayout; - var xa = fullLayout.xaxis; - var xr = xa.range.slice(); - expect(xa.r2l(xr[0])).toBeGreaterThan(xa.r2l('2018-01-01')); - expect(xa.r2l(xr[1])).toBeLessThan(xa.r2l('2020-01-01')); + var xa = fullLayout.xaxis; + var xr = xa.range.slice(); + expect(xa.r2l(xr[0])).toBeGreaterThan(xa.r2l('2018-01-01')); + expect(xa.r2l(xr[1])).toBeLessThan(xa.r2l('2020-01-01')); - var xa2 = fullLayout.xaxis2; - var xr2 = xa2.range.slice(); - expect(xr2[0]).toBeGreaterThan(0); - expect(xr2[1]).toBeLessThan(2); - }, 15); + var xa2 = fullLayout.xaxis2; + var xr2 = xa2.range.slice(); + expect(xr2[0]).toBeGreaterThan(0); + expect(xr2[1]).toBeLessThan(2); - return promise; - }) - .then(function() { expect(gd._fullLayout.xaxis.range).toEqual(['2018-06-01', '2019-06-01']); expect(gd._fullLayout.xaxis2.range).toEqual([0.5, 1.5]); })