diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e20e664a4..4171da9b58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,18 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#1729](https://github.com/plotly/dash/pull/1729) Include F#, C#, and MATLAB in markdown code highlighting, for the upcoming .NET and MATLAB flavors of dash. +- [#1735](https://github.com/plotly/dash/pull/1735) Upgrade Plotly.js to v2.4.2. This includes: + - [Feature release 2.3.0](https://github.com/plotly/plotly.js/releases/tag/v2.3.0): + - More number formatting options due to `d3-format` upgrade. + - Many new `geo` projections. + - Improved rendering and performance of `scattergl`, `splom` and `parcoords` traces. + - [Feature release 2.4.0](https://github.com/plotly/plotly.js/releases/tag/v2.4.0): + - `legend.groupclick` + - `bbox` of hover items in event data, to support custom dash-driven hover effects + - Patch releases [2.3.1](https://github.com/plotly/plotly.js/releases/tag/v2.3.1), [2.4.1](https://github.com/plotly/plotly.js/releases/tag/v2.4.1), and [2.4.2](https://github.com/plotly/plotly.js/releases/tag/v2.4.2) containing various bug fixes. + +- [#1735](https://github.com/plotly/dash/pull/1735) New `dcc.Tooltip` component. This is particularly useful for rich hover information on `dcc.Graph` charts, using the `bbox` information included in the event data in plotly.js v2.4.0 + ## Dash Table ### Added diff --git a/components/dash-core-components/package-lock.json b/components/dash-core-components/package-lock.json index db8f0f9b67..1b28de8d4e 100644 --- a/components/dash-core-components/package-lock.json +++ b/components/dash-core-components/package-lock.json @@ -19,7 +19,7 @@ "highlight.js": "^11.0.1", "moment": "^2.29.1", "node-polyfill-webpack-plugin": "^1.1.4", - "plotly.js": "2.2.1", + "plotly.js": "2.4.2", "prop-types": "^15.7.2", "ramda": "^0.27.1", "rc-slider": "^9.7.2", @@ -1930,9 +1930,9 @@ } }, "node_modules/@plotly/d3": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.6.1.tgz", - "integrity": "sha512-lM2dmUqRX1qGtrWczC7QNbQ4Bdgp9sII9i7NV6Hokw06kLH1++x0Ehlj193+PkLYvi1us1IcYjC7IIw+h6GywA==" + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.0.tgz", + "integrity": "sha512-L10iHgzvw3uSic/nQpYehlNzxUQvImwms5U7S95pJAEhrllzkrdQNy1Mc5DW9ab881Yr4fh300gJztKXWZDfkQ==" }, "node_modules/@plotly/d3-sankey": { "version": "0.7.2", @@ -4502,6 +4502,42 @@ "d3-timer": "1" } }, + "node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + }, + "node_modules/d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/d3-geo-projection": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", + "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", + "dependencies": { + "commander": "2", + "d3-array": "1", + "d3-geo": "^1.12.0", + "resolve": "^1.1.10" + }, + "bin": { + "geo2svg": "bin/geo2svg", + "geograticule": "bin/geograticule", + "geoproject": "bin/geoproject", + "geoquantize": "bin/geoquantize", + "geostitch": "bin/geostitch" + } + }, + "node_modules/d3-geo-projection/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/d3-hierarchy": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", @@ -6782,9 +6818,9 @@ } }, "node_modules/gl-text": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/gl-text/-/gl-text-1.1.8.tgz", - "integrity": "sha512-whnq9DEFYbW92C4ONwk2eT0YkzmVPHoADnEtuzMOmit87XhgAhBrNs3lK9EgGjU/MoWYvlF6RkI8Kl7Yuo1hUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gl-text/-/gl-text-1.2.0.tgz", + "integrity": "sha512-1X5yL8wKjyVMPenPKe7UvDAgyAOstuQ9gRfDBI3OK7ZHqHEnatTT5NaHWoY+II2ZHE35DvePxnOuayukowF0Ow==", "dependencies": { "bit-twiddle": "^1.0.2", "color-normalize": "^1.5.0", @@ -6800,7 +6836,7 @@ "parse-rect": "^1.2.0", "parse-unit": "^1.0.1", "pick-by-alias": "^1.2.0", - "regl": "^1.3.11", + "regl": "^2.0.0", "to-px": "^1.0.1", "typedarray-pool": "^1.1.0" } @@ -10174,11 +10210,11 @@ } }, "node_modules/plotly.js": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.2.1.tgz", - "integrity": "sha512-9kUkGnUmFxHrU7MUkmptKceBVyjSIXEwXpAsXpWtbhzzEstm5gsyajV1YbMwF+G6sBvxkeLM9X2JbaiqHGooSg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.4.2.tgz", + "integrity": "sha512-V/UtLtYxdc/+tPSZzL+WBnZ5x4NPhPNKNh49uV0TZccKSmL4QIDlz2jIAnY1yYYLu5ubsiTou+yMwk/+TTiUsw==", "dependencies": { - "@plotly/d3": "3.6.1", + "@plotly/d3": "3.8.0", "@plotly/d3-sankey": "0.7.2", "@plotly/d3-sankey-circular": "0.33.1", "@plotly/point-cluster": "^3.1.9", @@ -10194,8 +10230,12 @@ "convex-hull": "^1.0.3", "country-regex": "^1.1.0", "d3-force": "^1.2.1", + "d3-format": "^1.4.5", + "d3-geo": "^1.12.1", + "d3-geo-projection": "^2.9.0", "d3-hierarchy": "^1.1.9", "d3-interpolate": "^1.4.0", + "d3-time": "^1.1.0", "d3-time-format": "^2.2.3", "delaunay-triangulate": "^1.1.6", "fast-isnumeric": "^1.1.4", @@ -10213,7 +10253,7 @@ "gl-spikes2d": "^1.0.2", "gl-streamtube3d": "^1.4.1", "gl-surface3d": "^1.6.0", - "gl-text": "^1.1.8", + "gl-text": "^1.2.0", "glslify": "^7.1.1", "has-hover": "^1.0.1", "has-passive-events": "^1.0.0", @@ -10229,10 +10269,10 @@ "parse-svg-path": "^0.1.2", "polybooljs": "^1.2.0", "probe-image-size": "^7.2.1", - "regl": "^1.6.1", - "regl-error2d": "^2.0.11", - "regl-line2d": "^3.1.0", - "regl-scatter2d": "^3.2.3", + "regl": "^2.1.0", + "regl-error2d": "^2.0.12", + "regl-line2d": "^3.1.1", + "regl-scatter2d": "^3.2.6", "regl-splom": "^1.0.14", "right-now": "^1.0.0", "robust-orientation": "^1.1.3", @@ -11309,9 +11349,9 @@ } }, "node_modules/regl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/regl/-/regl-1.7.0.tgz", - "integrity": "sha512-bEAtp/qrtKucxXSJkD4ebopFZYP0q1+3Vb2WECWv/T8yQEgKxDxJ7ztO285tAMaYZVR6mM1GgI6CCn8FROtL1w==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/regl/-/regl-2.1.0.tgz", + "integrity": "sha512-oWUce/aVoEvW5l2V0LK7O5KJMzUSKeiOwFuJehzpSFd43dO5spP9r+sSUfhKtsky4u6MCqWJaRL+abzExynfTg==" }, "node_modules/regl-error2d": { "version": "2.0.12", @@ -15729,9 +15769,9 @@ } }, "@plotly/d3": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.6.1.tgz", - "integrity": "sha512-lM2dmUqRX1qGtrWczC7QNbQ4Bdgp9sII9i7NV6Hokw06kLH1++x0Ehlj193+PkLYvi1us1IcYjC7IIw+h6GywA==" + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.0.tgz", + "integrity": "sha512-L10iHgzvw3uSic/nQpYehlNzxUQvImwms5U7S95pJAEhrllzkrdQNy1Mc5DW9ab881Yr4fh300gJztKXWZDfkQ==" }, "@plotly/d3-sankey": { "version": "0.7.2", @@ -17918,6 +17958,37 @@ "d3-timer": "1" } }, + "d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + }, + "d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "requires": { + "d3-array": "1" + } + }, + "d3-geo-projection": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", + "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", + "requires": { + "commander": "2", + "d3-array": "1", + "d3-geo": "^1.12.0", + "resolve": "^1.1.10" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, "d3-hierarchy": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", @@ -19818,9 +19889,9 @@ } }, "gl-text": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/gl-text/-/gl-text-1.1.8.tgz", - "integrity": "sha512-whnq9DEFYbW92C4ONwk2eT0YkzmVPHoADnEtuzMOmit87XhgAhBrNs3lK9EgGjU/MoWYvlF6RkI8Kl7Yuo1hUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gl-text/-/gl-text-1.2.0.tgz", + "integrity": "sha512-1X5yL8wKjyVMPenPKe7UvDAgyAOstuQ9gRfDBI3OK7ZHqHEnatTT5NaHWoY+II2ZHE35DvePxnOuayukowF0Ow==", "requires": { "bit-twiddle": "^1.0.2", "color-normalize": "^1.5.0", @@ -19836,7 +19907,7 @@ "parse-rect": "^1.2.0", "parse-unit": "^1.0.1", "pick-by-alias": "^1.2.0", - "regl": "^1.3.11", + "regl": "^2.0.0", "to-px": "^1.0.1", "typedarray-pool": "^1.1.0" } @@ -22549,11 +22620,11 @@ } }, "plotly.js": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.2.1.tgz", - "integrity": "sha512-9kUkGnUmFxHrU7MUkmptKceBVyjSIXEwXpAsXpWtbhzzEstm5gsyajV1YbMwF+G6sBvxkeLM9X2JbaiqHGooSg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.4.2.tgz", + "integrity": "sha512-V/UtLtYxdc/+tPSZzL+WBnZ5x4NPhPNKNh49uV0TZccKSmL4QIDlz2jIAnY1yYYLu5ubsiTou+yMwk/+TTiUsw==", "requires": { - "@plotly/d3": "3.6.1", + "@plotly/d3": "3.8.0", "@plotly/d3-sankey": "0.7.2", "@plotly/d3-sankey-circular": "0.33.1", "@plotly/point-cluster": "^3.1.9", @@ -22569,8 +22640,12 @@ "convex-hull": "^1.0.3", "country-regex": "^1.1.0", "d3-force": "^1.2.1", + "d3-format": "^1.4.5", + "d3-geo": "^1.12.1", + "d3-geo-projection": "^2.9.0", "d3-hierarchy": "^1.1.9", "d3-interpolate": "^1.4.0", + "d3-time": "^1.1.0", "d3-time-format": "^2.2.3", "delaunay-triangulate": "^1.1.6", "fast-isnumeric": "^1.1.4", @@ -22588,7 +22663,7 @@ "gl-spikes2d": "^1.0.2", "gl-streamtube3d": "^1.4.1", "gl-surface3d": "^1.6.0", - "gl-text": "^1.1.8", + "gl-text": "^1.2.0", "glslify": "^7.1.1", "has-hover": "^1.0.1", "has-passive-events": "^1.0.0", @@ -22604,10 +22679,10 @@ "parse-svg-path": "^0.1.2", "polybooljs": "^1.2.0", "probe-image-size": "^7.2.1", - "regl": "^1.6.1", - "regl-error2d": "^2.0.11", - "regl-line2d": "^3.1.0", - "regl-scatter2d": "^3.2.3", + "regl": "^2.1.0", + "regl-error2d": "^2.0.12", + "regl-line2d": "^3.1.1", + "regl-scatter2d": "^3.2.6", "regl-splom": "^1.0.14", "right-now": "^1.0.0", "robust-orientation": "^1.1.3", @@ -23452,9 +23527,9 @@ } }, "regl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/regl/-/regl-1.7.0.tgz", - "integrity": "sha512-bEAtp/qrtKucxXSJkD4ebopFZYP0q1+3Vb2WECWv/T8yQEgKxDxJ7ztO285tAMaYZVR6mM1GgI6CCn8FROtL1w==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/regl/-/regl-2.1.0.tgz", + "integrity": "sha512-oWUce/aVoEvW5l2V0LK7O5KJMzUSKeiOwFuJehzpSFd43dO5spP9r+sSUfhKtsky4u6MCqWJaRL+abzExynfTg==" }, "regl-error2d": { "version": "2.0.12", diff --git a/components/dash-core-components/package.json b/components/dash-core-components/package.json index c622d9d8da..e304f71d7c 100644 --- a/components/dash-core-components/package.json +++ b/components/dash-core-components/package.json @@ -48,7 +48,7 @@ "highlight.js": "^11.0.1", "moment": "^2.29.1", "node-polyfill-webpack-plugin": "^1.1.4", - "plotly.js": "2.2.1", + "plotly.js": "2.4.2", "prop-types": "^15.7.2", "ramda": "^0.27.1", "rc-slider": "^9.7.2", diff --git a/components/dash-core-components/src/components/Tooltip.react.js b/components/dash-core-components/src/components/Tooltip.react.js new file mode 100644 index 0000000000..51e1d38baf --- /dev/null +++ b/components/dash-core-components/src/components/Tooltip.react.js @@ -0,0 +1,276 @@ +import PropTypes from 'prop-types'; + +import _JSXStyle from 'styled-jsx/style'; // eslint-disable-line no-unused-vars + +/** + * A tooltip with an absolute position. + */ +const Tooltip = props => { + const {bbox, border_color, background_color, id, loading_state} = props; + const is_loading = loading_state?.is_loading; + const show = props.show && bbox; + + return ( + <> +
+ + + {is_loading ? ( + {props.loading_text} + ) : ( + props.children + )} + + +
+ + + ); +}; + +Tooltip.defaultProps = { + show: true, + targetable: false, + direction: 'right', + border_color: '#d6d6d6', + background_color: 'white', + className: '', + zindex: 1, + loading_text: 'Loading...', +}; + +Tooltip.propTypes = { + /** + * The contents of the tooltip + */ + children: PropTypes.node, + + /** + * The ID of this component, used to identify dash components + * in callbacks. The ID needs to be unique across all of the + * components in an app. + */ + id: PropTypes.string, + + /** + * The class of the tooltip + */ + className: PropTypes.string, + + /** + * The style of the tooltip + */ + style: PropTypes.object, + + /** + * The bounding box coordinates of the item to label, in px relative to + * the positioning parent of the Tooltip component. + */ + bbox: PropTypes.exact({ + x0: PropTypes.number, + y0: PropTypes.number, + x1: PropTypes.number, + y1: PropTypes.number, + }), + + /** + * Whether to show the tooltip + */ + show: PropTypes.bool, + + /** + * The side of the `bbox` on which the tooltip should open. + */ + direction: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + + /** + * Color of the tooltip border, as a CSS color string. + */ + border_color: PropTypes.string, + + /** + * Color of the tooltip background, as a CSS color string. + */ + background_color: PropTypes.string, + + /** + * The text displayed in the tooltip while loading + */ + loading_text: PropTypes.string, + + /** + * The `z-index` CSS property to assign to the tooltip. Components with + * higher values will be displayed on top of components with lower values. + */ + zindex: PropTypes.number, + + /** + * Whether the tooltip itself can be targeted by pointer events. + * For tooltips triggered by hover events, typically this should be left + * `false` to avoid the tooltip interfering with those same events. + */ + targetable: PropTypes.bool, + + /** + * Dash-assigned callback that gets fired when the value changes. + */ + setProps: PropTypes.func, + + /** + * Object that holds the loading state object coming from dash-renderer + */ + loading_state: PropTypes.shape({ + /** + * Determines if the component is loading or not + */ + is_loading: PropTypes.bool, + /** + * Holds which property is loading + */ + prop_name: PropTypes.string, + /** + * Holds the name of the component that is loading + */ + component_name: PropTypes.string, + }), +}; + +export default Tooltip; diff --git a/components/dash-core-components/src/fragments/Graph.react.js b/components/dash-core-components/src/fragments/Graph.react.js index f846dd710f..7cb415fd13 100644 --- a/components/dash-core-components/src/fragments/Graph.react.js +++ b/components/dash-core-components/src/fragments/Graph.react.js @@ -74,6 +74,12 @@ const filterEventData = (gd, eventData, event) => { const pointData = filter(function (o) { return !includes(type(o), ['Object', 'Array']); }, fullPoint); + + // permit a bounding box to pass through, if present + if (has('bbox', fullPoint)) { + pointData.bbox = fullPoint.bbox; + } + if ( has('curveNumber', fullPoint) && has('pointNumber', fullPoint) && diff --git a/components/dash-core-components/src/index.js b/components/dash-core-components/src/index.js index 5ec4bca0e2..a1635e029c 100644 --- a/components/dash-core-components/src/index.js +++ b/components/dash-core-components/src/index.js @@ -1,55 +1,57 @@ /* eslint-disable import/prefer-default-export */ +import Checklist from './components/Checklist.react'; +import Clipboard from './components/Clipboard.react'; import ConfirmDialog from './components/ConfirmDialog.react'; import ConfirmDialogProvider from './components/ConfirmDialogProvider.react'; +import DatePickerRange from './components/DatePickerRange.react'; +import DatePickerSingle from './components/DatePickerSingle.react'; +import Download from './components/Download.react'; import Dropdown from './components/Dropdown.react'; -import Input from './components/Input.react'; import Graph from './components/Graph.react'; -import RangeSlider from './components/RangeSlider.react'; -import Slider from './components/Slider.react'; -import RadioItems from './components/RadioItems.react'; -import Checklist from './components/Checklist.react'; +import Input from './components/Input.react'; import Interval from './components/Interval.react'; -import Markdown from './components/Markdown.react'; +import Link from './components/Link.react'; import Loading from './components/Loading.react'; import Location from './components/Location.react'; -import Link from './components/Link.react'; +import LogoutButton from './components/LogoutButton.react'; +import Markdown from './components/Markdown.react'; +import RadioItems from './components/RadioItems.react'; +import RangeSlider from './components/RangeSlider.react'; +import Slider from './components/Slider.react'; +import Store from './components/Store.react'; +import Tab from './components/Tab.react'; +import Tabs from './components/Tabs.react'; import Textarea from './components/Textarea.react'; -import DatePickerSingle from './components/DatePickerSingle.react'; -import DatePickerRange from './components/DatePickerRange.react'; +import Tooltip from './components/Tooltip.react'; import Upload from './components/Upload.react'; -import Download from './components/Download.react'; -import Tabs from './components/Tabs.react'; -import Tab from './components/Tab.react'; -import Store from './components/Store.react'; -import LogoutButton from './components/LogoutButton.react'; -import Clipboard from './components/Clipboard.react'; import 'react-dates/lib/css/_datepicker.css'; import './components/css/react-dates@20.1.0-fix.css'; export { Checklist, + Clipboard, ConfirmDialog, ConfirmDialogProvider, + DatePickerRange, + DatePickerSingle, + Download, Dropdown, Graph, Input, + Interval, + Link, + Loading, + Location, + LogoutButton, + Markdown, RadioItems, RangeSlider, Slider, - Tabs, + Store, Tab, - Interval, - Markdown, - Loading, - Location, - Link, + Tabs, Textarea, - DatePickerSingle, - DatePickerRange, + Tooltip, Upload, - Store, - LogoutButton, - Download, - Clipboard }; diff --git a/components/dash-core-components/tests/integration/tooltip/test_tooltip.py b/components/dash-core-components/tests/integration/tooltip/test_tooltip.py new file mode 100644 index 0000000000..ace269a058 --- /dev/null +++ b/components/dash-core-components/tests/integration/tooltip/test_tooltip.py @@ -0,0 +1,85 @@ +from multiprocessing import Lock +from selenium.webdriver.common.action_chains import ActionChains +from dash.testing.wait import until + +from dash import Dash, Input, Output, dcc, html, no_update + + +def test_ttbs001_canonical_behavior(dash_dcc): + lock = Lock() + + loading_text = "Waiting for Godot" + + fig = dict( + data=[ + dict( + x=[11, 22, 33], y=[333, 222, 111], mode="markers", marker=dict(size=40) + ) + ], + layout=dict(width=400, height=400, margin=dict(l=100, r=100, t=100, b=100)), + ) + app = Dash(__name__) + + app.layout = html.Div( + className="container", + children=[ + dcc.Graph(id="graph", figure=fig, clear_on_unhover=True), + dcc.Tooltip(id="graph-tooltip", loading_text=loading_text), + ], + style=dict(position="relative"), + ) + + # This callback is executed very quickly + app.clientside_callback( + """ + function show_tooltip(hoverData) { + if(!hoverData) { + return [false, dash_clientside.no_update]; + } + var pt = hoverData.points[0]; + return [true, pt.bbox]; + } + """, + Output("graph-tooltip", "show"), + Output("graph-tooltip", "bbox"), + Input("graph", "hoverData"), + ) + + # This callback is executed after 1s to simulate a long-running process + @app.callback( + Output("graph-tooltip", "children"), + Input("graph", "hoverData"), + ) + def update_tooltip_content(hoverData): + if hoverData is None: + return no_update + + with lock: + # Display the x0 and y0 coordinate + bbox = hoverData["points"][0]["bbox"] + return [ + html.P(f"x0={bbox['x0']}, y0={bbox['y0']}"), + ] + + dash_dcc.start_server(app) + + until(lambda: not dash_dcc.find_element("#graph-tooltip").is_displayed(), 3) + + elem = dash_dcc.find_element("#graph .nsewdrag") + + with lock: + # hover on the center of the graph + ActionChains(dash_dcc.driver).move_to_element_with_offset( + elem, elem.size["width"] / 2, elem.size["height"] / 2 + ).click().perform() + dash_dcc.wait_for_text_to_equal("#graph-tooltip", loading_text) + + dash_dcc.wait_for_contains_text("#graph-tooltip", "x0=") + tt_text = dash_dcc.find_element("#graph-tooltip").text + coords = [float(part.split("=")[1]) for part in tt_text.split(",")] + 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" + + ActionChains(dash_dcc.driver).move_to_element_with_offset(elem, 0, 0).perform() + + until(lambda: not dash_dcc.find_element("#graph-tooltip").is_displayed(), 3) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index 3b6bbf0f4a..7359c55671 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -6,8 +6,8 @@ import dash from dash import Dash, Input, Output, State, ALL, ALLSMALLER, MATCH, html, dcc -from assets.todo_app import todo_app -from assets.grouping_app import grouping_app +from tests.assets.todo_app import todo_app +from tests.assets.grouping_app import grouping_app def css_escape(s): diff --git a/tests/integration/devtools/test_devtools_ui.py b/tests/integration/devtools/test_devtools_ui.py index 395d1f5e38..ae9d7910ec 100644 --- a/tests/integration/devtools/test_devtools_ui.py +++ b/tests/integration/devtools/test_devtools_ui.py @@ -5,7 +5,7 @@ import dash.testing.wait as wait from dash_test_components import WidthComponent -from assets.todo_app import todo_app +from tests.assets.todo_app import todo_app def test_dvui001_disable_props_check_config(dash_duo): diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 7d86701a5b..57662f0c18 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -11,6 +11,8 @@ from dash import Dash, html, dcc, Input, Output from dash.exceptions import PreventUpdate +from dash.testing.wait import until + def test_inin004_wildcard_data_attributes(dash_duo): app = Dash() @@ -328,42 +330,10 @@ def render_content(tab): dash_duo.find_element("#graph1:not(.dash-graph--pending)").click() - graph_1_expected_clickdata = { - "points": [ - { - "curveNumber": 0, - "pointNumber": 1, - "pointIndex": 1, - "x": 2, - "y": 10, - "label": 2, - "value": 10, - } - ] - } - - graph_2_expected_clickdata = { - "points": [ - { - "curveNumber": 0, - "pointNumber": 1, - "pointIndex": 1, - "x": 3, - "y": 10, - "label": 3, - "value": 10, - } - ] - } - - dash_duo.wait_for_text_to_equal( - "#graph1_info", json.dumps(graph_1_expected_clickdata) - ) + until(lambda: '"label": 2' in dash_duo.find_element("#graph1_info").text, timeout=3) dash_duo.find_element("#tab2").click() dash_duo.find_element("#graph2:not(.dash-graph--pending)").click() - dash_duo.wait_for_text_to_equal( - "#graph2_info", json.dumps(graph_2_expected_clickdata) - ) + until(lambda: '"label": 3' in dash_duo.find_element("#graph2_info").text, timeout=3)