import React, {Component, memo, Suspense} from 'react'; import PropTypes from 'prop-types'; import {asyncDecorator} from '@plotly/dash-component-plugins'; import graph from '../utils/LazyLoader/graph'; import plotly from '../utils/LazyLoader/plotly'; import { privatePropTypes, privateDefaultProps, } from '../fragments/Graph.privateprops'; const EMPTY_DATA = []; /** * Graph can be used to render any plotly.js-powered data visualization. * * You can define callbacks based on user interaction with Graphs such as * hovering, clicking or selecting */ class PlotlyGraph extends Component { constructor(props) { super(props); this.state = { prependData: [], extendData: [], }; this.clearState = this.clearState.bind(this); } componentDidMount() { if (this.props.prependData) { this.setState({ prependData: [this.props.prependData], }); } if (this.props.extendData) { this.setState({ extendData: [this.props.extendData], }); } } componentWillUnmount() { this.setState({ prependData: [], extendData: [], }); } UNSAFE_componentWillReceiveProps(nextProps) { let prependData = this.state.prependData.slice(0); if (this.props.figure !== nextProps.figure) { prependData = EMPTY_DATA; } if ( nextProps.prependData && this.props.prependData !== nextProps.prependData ) { prependData.push(nextProps.prependData); } else { prependData = EMPTY_DATA; } if (prependData !== EMPTY_DATA) { this.setState({ prependData, }); } let extendData = this.state.extendData.slice(0); if (this.props.figure !== nextProps.figure) { extendData = EMPTY_DATA; } if ( nextProps.extendData && this.props.extendData !== nextProps.extendData ) { extendData.push(nextProps.extendData); } else { extendData = EMPTY_DATA; } if (extendData !== EMPTY_DATA) { this.setState({ extendData, }); } } clearState(dataKey) { this.setState(props => { var data = props[dataKey]; const res = data && data.length ? { [dataKey]: EMPTY_DATA, } : undefined; return res; }); } render() { return ( ); } } const RealPlotlyGraph = asyncDecorator(PlotlyGraph, () => Promise.all([plotly(), graph()]).then(([, graph]) => graph) ); const ControlledPlotlyGraph = memo(props => { const {className, id} = props; const extendedClassName = className ? 'dash-graph ' + className : 'dash-graph'; return ( } > ); }); PlotlyGraph.propTypes = { ...privatePropTypes, /** * 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, /** * If True, the Plotly.js plot will be fully responsive to window resize * and parent element resize event. This is achieved by overriding * `config.responsive` to True, `figure.layout.autosize` to True and unsetting * `figure.layout.height` and `figure.layout.width`. * If False, the Plotly.js plot not be responsive to window resize and * parent element resize event. This is achieved by overriding `config.responsive` * to False and `figure.layout.autosize` to False. * If 'auto' (default), the Graph will determine if the Plotly.js plot can be made fully * responsive (True) or not (False) based on the values in `config.responsive`, * `figure.layout.autosize`, `figure.layout.height`, `figure.layout.width`. * This is the legacy behavior of the Graph component. * * Needs to be combined with appropriate dimension / styling through the `style` prop * to fully take effect. */ responsive: PropTypes.oneOf([true, false, 'auto']), /** * Data from latest click event. Read-only. */ clickData: PropTypes.object, /** * Data from latest click annotation event. Read-only. */ clickAnnotationData: PropTypes.object, /** * Data from latest hover event. Read-only. */ hoverData: PropTypes.object, /** * If True, `clear_on_unhover` will clear the `hoverData` property * when the user "unhovers" from a point. * If False, then the `hoverData` property will be equal to the * data from the last point that was hovered over. */ clear_on_unhover: PropTypes.bool, /** * Data from latest select event. Read-only. */ selectedData: PropTypes.object, /** * Data from latest relayout event which occurs * when the user zooms or pans on the plot or other * layout-level edits. Has the form `{: }` * describing the changes made. Read-only. */ relayoutData: PropTypes.object, /** * Data that should be appended to existing traces. Has the form * `[updateData, traceIndices, maxPoints]`, where `updateData` is an object * containing the data to extend, `traceIndices` (optional) is an array of * trace indices that should be extended, and `maxPoints` (optional) is * either an integer defining the maximum number of points allowed or an * object with key:value pairs matching `updateData` * Reference the Plotly.extendTraces API for full usage: * https://plotly.com/javascript/plotlyjs-function-reference/#plotlyextendtraces */ extendData: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), /** * Data that should be prepended to existing traces. Has the form * `[updateData, traceIndices, maxPoints]`, where `updateData` is an object * containing the data to prepend, `traceIndices` (optional) is an array of * trace indices that should be prepended, and `maxPoints` (optional) is * either an integer defining the maximum number of points allowed or an * object with key:value pairs matching `updateData` * Reference the Plotly.prependTraces API for full usage: * https://plotly.com/javascript/plotlyjs-function-reference/#plotlyprependtraces */ prependData: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), /** * Data from latest restyle event which occurs * when the user toggles a legend item, changes * parcoords selections, or other trace-level edits. * Has the form `[edits, indices]`, where `edits` is an object * `{: }` describing the changes made, * and `indices` is an array of trace indices that were edited. * Read-only. */ restyleData: PropTypes.array, /** * Plotly `figure` object. See schema: * https://plotly.com/javascript/reference * * `config` is set separately by the `config` property */ figure: PropTypes.exact({ data: PropTypes.arrayOf(PropTypes.object), layout: PropTypes.object, frames: PropTypes.arrayOf(PropTypes.object), }), /** * Generic style overrides on the plot div */ style: PropTypes.object, /** * className of the parent div */ className: PropTypes.string, /** * Beta: If true, animate between updates using * plotly.js's `animate` function */ animate: PropTypes.bool, /** * Beta: Object containing animation settings. * Only applies if `animate` is `true` */ animation_options: PropTypes.object, /** * Plotly.js config options. * See https://plotly.com/javascript/configuration-options/ * for more info. */ config: PropTypes.exact({ /** * No interactivity, for export or image generation */ staticPlot: PropTypes.bool, /** * Base URL for a Plotly cloud instance, if `showSendToCloud` is enabled */ plotlyServerURL: PropTypes.string, /** * We can edit titles, move annotations, etc - sets all pieces of `edits` * unless a separate `edits` config item overrides individual parts */ editable: PropTypes.bool, /** * A set of editable properties */ edits: PropTypes.exact({ /** * The main anchor of the annotation, which is the * text (if no arrow) or the arrow (which drags the whole thing leaving * the arrow length & direction unchanged) */ annotationPosition: PropTypes.bool, /** * Just for annotations with arrows, change the length and direction of the arrow */ annotationTail: PropTypes.bool, annotationText: PropTypes.bool, axisTitleText: PropTypes.bool, colorbarPosition: PropTypes.bool, colorbarTitleText: PropTypes.bool, legendPosition: PropTypes.bool, /** * Edit the trace name fields from the legend */ legendText: PropTypes.bool, shapePosition: PropTypes.bool, /** * The global `layout.title` */ titleText: PropTypes.bool, }), /** * DO autosize once regardless of layout.autosize * (use default width or height values otherwise) */ autosizable: PropTypes.bool, /** * Whether to change layout size when the window size changes */ responsive: PropTypes.bool, /** * Set the length of the undo/redo queue */ queueLength: PropTypes.number, /** * If we DO autosize, do we fill the container or the screen? */ fillFrame: PropTypes.bool, /** * If we DO autosize, set the frame margins in percents of plot size */ frameMargins: PropTypes.number, /** * Mousewheel or two-finger scroll zooms the plot */ scrollZoom: PropTypes.bool, /** * Double click interaction (false, 'reset', 'autosize' or 'reset+autosize') */ doubleClick: PropTypes.oneOf([ false, 'reset', 'autosize', 'reset+autosize', ]), /** * Delay for registering a double-click event in ms. The * minimum value is 100 and the maximum value is 1000. By * default this is 300. */ doubleClickDelay: PropTypes.number, /** * New users see some hints about interactivity */ showTips: PropTypes.bool, /** * Enable axis pan/zoom drag handles */ showAxisDragHandles: PropTypes.bool, /** * Enable direct range entry at the pan/zoom drag points * (drag handles must be enabled above) */ showAxisRangeEntryBoxes: PropTypes.bool, /** * Link to open this plot in plotly */ showLink: PropTypes.bool, /** * If we show a link, does it contain data or just link to a plotly file? */ sendData: PropTypes.bool, /** * Text appearing in the sendData link */ linkText: PropTypes.string, /** * Display the mode bar (true, false, or 'hover') */ displayModeBar: PropTypes.oneOf([true, false, 'hover']), /** * Should we include a modebar button to send this data to a * Plotly Cloud instance, linked by `plotlyServerURL`. * By default this is false. */ showSendToCloud: PropTypes.bool, /** * Should we show a modebar button to send this data to a * Plotly Chart Studio plot. If both this and showSendToCloud * are selected, only showEditInChartStudio will be * honored. By default this is false. */ showEditInChartStudio: PropTypes.bool, /** * Remove mode bar button by name. * All modebar button names at https://github.com/plotly/plotly.js/blob/master/src/components/modebar/buttons.js * Common names include: * sendDataToCloud; * (2D) zoom2d, pan2d, select2d, lasso2d, zoomIn2d, zoomOut2d, autoScale2d, resetScale2d; * (Cartesian) hoverClosestCartesian, hoverCompareCartesian; * (3D) zoom3d, pan3d, orbitRotation, tableRotation, handleDrag3d, resetCameraDefault3d, resetCameraLastSave3d, hoverClosest3d; * (Geo) zoomInGeo, zoomOutGeo, resetGeo, hoverClosestGeo; * hoverClosestGl2d, hoverClosestPie, toggleHover, resetViews. */ modeBarButtonsToRemove: PropTypes.array, /** * Add mode bar button using config objects */ modeBarButtonsToAdd: PropTypes.array, /** * Fully custom mode bar buttons as nested array, * where the outer arrays represents button groups, and * the inner arrays have buttons config objects or names of default buttons */ modeBarButtons: PropTypes.any, /** * Modifications to how the toImage modebar button works */ toImageButtonOptions: PropTypes.exact({ /** * The file format to create */ format: PropTypes.oneOf(['jpeg', 'png', 'webp', 'svg']), /** * The name given to the downloaded file */ filename: PropTypes.string, /** * Width of the downloaded file, in px */ width: PropTypes.number, /** * Height of the downloaded file, in px */ height: PropTypes.number, /** * Extra resolution to give the file after * rendering it with the given width and height */ scale: PropTypes.number, }), /** * Add the plotly logo on the end of the mode bar */ displaylogo: PropTypes.bool, /** * Add the plotly logo even with no modebar */ watermark: PropTypes.bool, /** * Increase the pixel ratio for Gl plot images */ plotGlPixelRatio: PropTypes.number, /** * URL to topojson files used in geo charts */ topojsonURL: PropTypes.string, /** * Mapbox access token (required to plot mapbox trace types) * If using an Mapbox Atlas server, set this option to '', * so that plotly.js won't attempt to authenticate to the public Mapbox server. */ mapboxAccessToken: PropTypes.any, /** * The locale to use. Locales may be provided with the plot * (`locales` below) or by loading them on the page, see: * https://github.com/plotly/plotly.js/blob/master/dist/README.md#to-include-localization */ locale: PropTypes.string, /** * Localization definitions, if you choose to provide them with the * plot rather than registering them globally. */ locales: PropTypes.object, }), /** * Function that updates the state tree. */ 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, }), }; ControlledPlotlyGraph.propTypes = PlotlyGraph.propTypes; PlotlyGraph.defaultProps = { ...privateDefaultProps, clickData: null, clickAnnotationData: null, hoverData: null, selectedData: null, relayoutData: null, prependData: null, extendData: null, restyleData: null, figure: { data: [], layout: {}, frames: [], }, responsive: 'auto', animate: false, animation_options: { frame: { redraw: false, }, transition: { duration: 750, ease: 'cubic-in-out', }, }, clear_on_unhover: false, config: {}, }; export const graphPropTypes = PlotlyGraph.propTypes; export const graphDefaultProps = PlotlyGraph.defaultProps; export default PlotlyGraph;