diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d4a9e2c9..95a1e4c17b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## [UNRELEASED] +## Changed + +- [#2610](https://github.com/plotly/dash/pull/2610) Load plotly.js bundle/version from plotly.py ## [2.12.1] - 2023-08-16 diff --git a/components/dash-core-components/dash_core_components_base/__init__.py b/components/dash-core-components/dash_core_components_base/__init__.py index 32d28f866e..2072e1b1a2 100644 --- a/components/dash-core-components/dash_core_components_base/__init__.py +++ b/components/dash-core-components/dash_core_components_base/__init__.py @@ -1,6 +1,7 @@ import json import os as _os import sys as _sys + import dash as _dash from ._imports_ import * # noqa: F401, F403, E402 @@ -121,33 +122,6 @@ "namespace": "dash", "dynamic": True, }, - { - "relative_package_path": "dcc/plotly.min.js", - "external_url": ( - "https://unpkg.com/dash-core-components@{}" - "/dash_core_components/plotly.min.js" - ).format(__version__), - "namespace": "dash", - "async": "eager", - }, - { - "relative_package_path": "dcc/async-plotlyjs.js", - "external_url": ( - "https://unpkg.com/dash-core-components@{}" - "/dash_core_components/async-plotlyjs.js" - ).format(__version__), - "namespace": "dash", - "async": "lazy", - }, - { - "relative_package_path": "dcc/async-plotlyjs.js.map", - "external_url": ( - "https://unpkg.com/dash-core-components@{}" - "/dash_core_components/async-plotlyjs.js.map" - ).format(__version__), - "namespace": "dash", - "dynamic": True, - }, ] ) diff --git a/components/dash-core-components/package-lock.json b/components/dash-core-components/package-lock.json index 980a750725..83bc0e76b9 100644 --- a/components/dash-core-components/package-lock.json +++ b/components/dash-core-components/package-lock.json @@ -22,7 +22,6 @@ "mathjax": "^3.2.2", "moment": "^2.29.4", "node-polyfill-webpack-plugin": "^2.0.1", - "plotly.js-dist-min": "2.25.2", "prop-types": "^15.8.1", "ramda": "^0.29.0", "rc-slider": "^9.7.5", @@ -6898,11 +6897,6 @@ "node": ">=8" } }, - "node_modules/plotly.js-dist-min": { - "version": "2.25.2", - "resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-2.25.2.tgz", - "integrity": "sha512-ktajQ3QcbKSOhQ3Oin5igm0JYtaJstOrWXP7KFG9XbgAOmLFX4mcm1NytbIxSStRNvEZSey/5M0uxL3zARG51Q==" - }, "node_modules/postcss": { "version": "8.4.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", @@ -14515,11 +14509,6 @@ } } }, - "plotly.js-dist-min": { - "version": "2.25.2", - "resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-2.25.2.tgz", - "integrity": "sha512-ktajQ3QcbKSOhQ3Oin5igm0JYtaJstOrWXP7KFG9XbgAOmLFX4mcm1NytbIxSStRNvEZSey/5M0uxL3zARG51Q==" - }, "postcss": { "version": "8.4.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", diff --git a/components/dash-core-components/package.json b/components/dash-core-components/package.json index 4e23b41940..9284d33b6c 100644 --- a/components/dash-core-components/package.json +++ b/components/dash-core-components/package.json @@ -23,7 +23,6 @@ "test": "run-s -c lint test:intg test:pyimport", "test:intg": "pytest --nopercyfinalize --headless tests/integration ", "test:pyimport": "python -m unittest tests/test_dash_import.py", - "prebuild:js": "cp node_modules/plotly.js-dist-min/plotly.min.js dash_core_components_base/plotly.min.js", "build:js": "webpack --mode production", "build:backends": "dash-generate-components ./src/components dash_core_components -p package-info.json && cp dash_core_components_base/** dash_core_components/ && dash-generate-components ./src/components dash_core_components -p package-info.json -k RangeSlider,Slider,Dropdown,RadioItems,Checklist,DatePickerSingle,DatePickerRange,Input,Link --r-prefix 'dcc' --r-suggests 'dash,dashHtmlComponents,jsonlite,plotly' --jl-prefix 'dcc' && black dash_core_components", "build": "run-s prepublishOnly build:js build:backends", @@ -49,7 +48,6 @@ "mathjax": "^3.2.2", "moment": "^2.29.4", "node-polyfill-webpack-plugin": "^2.0.1", - "plotly.js-dist-min": "2.25.2", "prop-types": "^15.8.1", "ramda": "^0.29.0", "rc-slider": "^9.7.5", diff --git a/components/dash-core-components/src/utils/LazyLoader/plotly.js b/components/dash-core-components/src/utils/LazyLoader/plotly.js index 2bdb34b776..8de65c9b66 100644 --- a/components/dash-core-components/src/utils/LazyLoader/plotly.js +++ b/components/dash-core-components/src/utils/LazyLoader/plotly.js @@ -1,6 +1,25 @@ -export default () => Promise.resolve(window.Plotly || - import(/* webpackChunkName: "plotlyjs" */ 'plotly.js-dist-min').then(({ default: Plotly }) => { - window.Plotly = Plotly; - return Plotly; - })); +export default () => { + return Promise.resolve(window.Plotly || new Promise((resolve, reject) => { + /* eslint-disable prefer-const */ + let timeoutId; + + const element = document.createElement('script'); + element.src = window._dashPlotlyJSURL; + element.async = true; + element.onload = () => { + clearTimeout(timeoutId); + resolve(); + }; + element.onerror = (error) => { + clearTimeout(timeoutId); + reject(error); + }; + timeoutId = setTimeout(() => { + element.src = ''; + reject(new Error(`plotly.js did not load after 30 seconds`)); + }, 3 * 10 * 1000); + + document.querySelector('body').appendChild(element); + })); +} diff --git a/components/dash-core-components/tests/integration/graph/test_graph_varia.py b/components/dash-core-components/tests/integration/graph/test_graph_varia.py index ffd3f93517..00455e182d 100644 --- a/components/dash-core-components/tests/integration/graph/test_graph_varia.py +++ b/components/dash-core-components/tests/integration/graph/test_graph_varia.py @@ -128,6 +128,8 @@ def show_relayout_data(data): dash_dcc.start_server(app) + time.sleep(1) + # use this opportunity to test restyleData, since there are multiple # traces on this graph legendToggle = dash_dcc.find_element( diff --git a/components/dash-core-components/tests/integration/tooltip/test_tooltip.py b/components/dash-core-components/tests/integration/tooltip/test_tooltip.py index 3460b2a74a..8ecb1e8a4e 100644 --- a/components/dash-core-components/tests/integration/tooltip/test_tooltip.py +++ b/components/dash-core-components/tests/integration/tooltip/test_tooltip.py @@ -80,6 +80,8 @@ def update_tooltip_content(hoverData): assert 175 < coords[0] < 185, "x0 is about 200 minus half a marker size" assert 175 < coords[1] < 185, "y0 is about 200 minus half a marker size" + elem = dash_dcc.find_element("#graph .nsewdrag") + ActionChains(dash_dcc.driver).move_to_element_with_offset( elem, 5, elem.size["height"] - 5 ).perform() diff --git a/dash/dash-renderer/src/APIController.react.js b/dash/dash-renderer/src/APIController.react.js index 852d750a59..f27a48a90a 100644 --- a/dash/dash-renderer/src/APIController.react.js +++ b/dash/dash-renderer/src/APIController.react.js @@ -72,6 +72,14 @@ const UnconnectedContainer = props => { } }); + useEffect(() => { + if (config.serve_locally) { + window._dashPlotlyJSURL = `${config.requests_pathname_prefix}_dash-component-suites/plotly/package_data/plotly.min.js`; + } else { + window._dashPlotlyJSURL = config.plotlyjs_url; + } + }, []); + let content; if ( layoutRequest.status && diff --git a/dash/dash-renderer/src/config.ts b/dash/dash-renderer/src/config.ts index 60dfd01670..00db747aec 100644 --- a/dash/dash-renderer/src/config.ts +++ b/dash/dash-renderer/src/config.ts @@ -19,6 +19,8 @@ type Config = { 'Content-Type': string; }; }; + serve_locally?: boolean; + plotlyjs_url?: string; }; export default function getConfigFromDOM(): Config { diff --git a/dash/dash.py b/dash/dash.py index 5ffca55dbd..22bc7f74bd 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -341,6 +341,8 @@ class Dash: if not added previously. """ + _plotlyjs_url: str + def __init__( # pylint: disable=too-many-statements self, name=None, @@ -588,6 +590,8 @@ def _handle_error(_): _get_app.APP = self self.enable_pages() + self._setup_plotlyjs() + def _add_url(self, name, view_func, methods=("GET",)): full_name = self.config.routes_pathname_prefix + name @@ -619,6 +623,25 @@ def _setup_routes(self): # catch-all for front-end routes, used by dcc.Location self._add_url("", self.index) + def _setup_plotlyjs(self): + # pylint: disable=import-outside-toplevel + from plotly.offline import get_plotlyjs_version + + url = f"https://cdn.plot.ly/plotly-{get_plotlyjs_version()}.min.js" + + # pylint: disable=protected-access + dcc._js_dist.extend( + [ + { + "relative_package_path": "package_data/plotly.min.js", + "external_url": url, + "namespace": "plotly", + "async": "eager", + } + ] + ) + self._plotlyjs_url = url + @property def layout(self): return self._layout @@ -702,7 +725,10 @@ def _config(self): "suppress_callback_exceptions": self.config.suppress_callback_exceptions, "update_title": self.config.update_title, "children_props": ComponentRegistry.children_props, + "serve_locally": self.config.serve_locally, } + if not self.config.serve_locally: + config["plotlyjs_url"] = self._plotlyjs_url if self._dev_tools.hot_reload: config["hot_reload"] = { # convert from seconds to msec as used by js `setInterval` diff --git a/tests/integration/test_scripts.py b/tests/integration/test_scripts.py index a073216c60..6d9ff3d103 100644 --- a/tests/integration/test_scripts.py +++ b/tests/integration/test_scripts.py @@ -9,14 +9,6 @@ def get_script_sources(dash_duo): return [s.get_attribute("src") for s in dash_duo.find_elements("script")] -def hasSyncPlotlyJs(dash_duo): - return any("dash/dcc/plotly" in s for s in get_script_sources(dash_duo)) - - -def hasAsyncPlotlyJs(dash_duo): - return any("dash/dcc/async-plotlyjs" in s for s in get_script_sources(dash_duo)) - - def hasWindowPlotly(dash_duo): return dash_duo.driver.execute_script("return !!window.Plotly") @@ -34,14 +26,11 @@ def test_scri001_scripts(dash_duo, is_eager): dev_tools_hot_reload=False, ) + assert hasWindowPlotly(dash_duo) is is_eager + # Wait for the graph to appear dash_duo.find_element(".js-plotly-plot") - assert hasSyncPlotlyJs(dash_duo) is is_eager - - # Webpack 5 deletes the script tag immediately after evaluating it - # https://github.com/plotly/dash/pull/1685#issuecomment-877199466 - assert hasAsyncPlotlyJs(dash_duo) is False assert hasWindowPlotly(dash_duo) is True @@ -67,8 +56,6 @@ def load_chart(n_clicks): # Give time for the async dependency to be requested (if any) time.sleep(2) - assert hasSyncPlotlyJs(dash_duo) is False - assert hasAsyncPlotlyJs(dash_duo) is False assert hasWindowPlotly(dash_duo) is False dash_duo.find_element("#btn").click() @@ -76,7 +63,4 @@ def load_chart(n_clicks): # Wait for the graph to appear dash_duo.find_element(".js-plotly-plot") - assert hasSyncPlotlyJs(dash_duo) is False - # Again, webpack 5 deletes the script tag immediately after evaluating it - assert hasAsyncPlotlyJs(dash_duo) is False assert hasWindowPlotly(dash_duo) is True diff --git a/tests/unit/test_old_imports.py b/tests/unit/test_old_imports.py index 08d7b14a23..90eaf4d0e7 100644 --- a/tests/unit/test_old_imports.py +++ b/tests/unit/test_old_imports.py @@ -12,6 +12,7 @@ def filter_dir(package): "package_name", "f", "express", + "get_plotlyjs_version", ] return sorted( [