diff --git a/frontend/src/lib/components/Plot/index.ts b/frontend/src/lib/components/Plot/index.ts new file mode 100644 index 000000000..994a24096 --- /dev/null +++ b/frontend/src/lib/components/Plot/index.ts @@ -0,0 +1 @@ +export { Plot } from "./plot"; diff --git a/frontend/src/lib/components/Plot/plot.tsx b/frontend/src/lib/components/Plot/plot.tsx new file mode 100644 index 000000000..4eee97bf2 --- /dev/null +++ b/frontend/src/lib/components/Plot/plot.tsx @@ -0,0 +1,148 @@ +import React from "react"; +import PlotlyPlot, { Figure, PlotParams } from "react-plotly.js"; + +import { cloneDeep, isEqual } from "lodash"; +import Plotly from "plotly.js"; +import { v4 } from "uuid"; + +export const Plot: React.FC = (props) => { + const { + data: propsData, + layout: propsLayout, + frames: propsFrames, + onHover: propsOnHover, + onUnhover: propsOnUnhover, + ...rest + } = props; + + const [data, setData] = React.useState(propsData); + const [layout, setLayout] = React.useState>(propsLayout); + const [frames, setFrames] = React.useState(propsFrames || null); + const [prevData, setPrevData] = React.useState(propsData); + const [prevLayout, setPrevLayout] = React.useState>(propsLayout); + const [prevFrames, setPrevFrames] = React.useState(propsFrames || null); + const id = React.useRef(`plot-${v4()}`); + + const timeout = React.useRef | null>(null); + const eventsDisabled = React.useRef(false); + + React.useEffect(() => { + // When zooming in, the relayout function is only called once there hasn't been a wheel event in a certain time. + // However, the hover event would still be sent and might update the data, causing a layout change and, hence, + // a jump in the plot's zoom. To prevent this, we disable the hover event for a certain time after a wheel event. + function handleWheel() { + if (timeout.current) { + clearTimeout(timeout.current); + } + eventsDisabled.current = true; + timeout.current = setTimeout(() => { + eventsDisabled.current = false; + }, 500); + } + + function handleTouchZoom(e: TouchEvent) { + if (e.touches.length === 2) { + handleWheel(); + } + } + + const element = document.getElementById(id.current); + if (element) { + element.addEventListener("wheel", handleWheel); + element.addEventListener("touchmove", handleTouchZoom); + } + + return () => { + if (element) { + element.removeEventListener("wheel", handleWheel); + element.removeEventListener("touchmove", handleTouchZoom); + } + + if (timeout.current) { + clearTimeout(timeout.current); + } + }; + }, []); + + if (!isEqual(propsData, prevData)) { + setData(cloneDeep(propsData)); + setPrevData(cloneDeep(propsData)); + } + + if (!isEqual(prevLayout, propsLayout)) { + const clone = cloneDeep(propsLayout); + setLayout({ + ...layout, + ...clone, + }); + setPrevLayout(cloneDeep(propsLayout)); + } + + if (!isEqual(prevFrames, propsFrames || null)) { + setFrames(cloneDeep(propsFrames || null)); + setPrevFrames(cloneDeep(propsFrames || null)); + } + + const handleInitialized = React.useCallback(function handleInitialized(figure: Figure) { + console.debug("initialized"); + setLayout(figure.layout); + setData(figure.data); + setFrames(figure.frames || null); + }, []); + + const handleRelayout = React.useCallback(function handleRelayout(e: Plotly.PlotRelayoutEvent) { + setLayout({ + ...layout, + xaxis: { + ...layout.xaxis, + range: [e["xaxis.range[0]"], e["xaxis.range[1]"]], + autorange: e["xaxis.autorange"], + }, + yaxis: { + ...layout.yaxis, + range: [e["yaxis.range[0]"], e["yaxis.range[1]"]], + autorange: e["yaxis.autorange"], + }, + }); + }, []); + + function handleHover(event: Readonly) { + if (propsOnHover && !eventsDisabled.current) { + propsOnHover(event); + } + return; + } + + function handleUnhover(event: Readonly) { + if (propsOnUnhover && !eventsDisabled.current) { + propsOnUnhover(event); + } + return; + } + + function handleUpdate(figure: Readonly
, graphDiv: Readonly) { + console.debug("update"); + } + + return ( + + ); +}; + +Plot.displayName = "Plot"; diff --git a/frontend/src/modules/DistributionPlot/components/barChart.tsx b/frontend/src/modules/DistributionPlot/components/barChart.tsx index 9fe39265d..bbf0eda35 100644 --- a/frontend/src/modules/DistributionPlot/components/barChart.tsx +++ b/frontend/src/modules/DistributionPlot/components/barChart.tsx @@ -1,5 +1,6 @@ import React from "react"; -import Plot from "react-plotly.js"; + +import { Plot } from "@lib/components/Plot"; import { Layout, PlotData, PlotHoverEvent } from "plotly.js"; @@ -59,8 +60,8 @@ export const BarChart: React.FC = (props) => { const layout: Partial = { width: props.width, height: props.height, - xaxis: { zeroline: false, title: props.xAxisTitle }, - yaxis: { zeroline: false, title: props.yAxisTitle }, + xaxis: { zeroline: false, title: { text: props.xAxisTitle } }, + yaxis: { zeroline: false, title: { text: props.yAxisTitle } }, margin: { t: 0, r: 0, l: 40, b: 40 }, }; return ( diff --git a/frontend/src/modules/DistributionPlot/components/histogram.tsx b/frontend/src/modules/DistributionPlot/components/histogram.tsx index 64459262f..e3199b5a5 100644 --- a/frontend/src/modules/DistributionPlot/components/histogram.tsx +++ b/frontend/src/modules/DistributionPlot/components/histogram.tsx @@ -1,5 +1,6 @@ import React from "react"; -import Plot from "react-plotly.js"; + +import { Plot } from "@lib/components/Plot"; import { Layout, PlotData, PlotHoverEvent } from "plotly.js"; diff --git a/frontend/src/modules/DistributionPlot/components/scatterPlot.tsx b/frontend/src/modules/DistributionPlot/components/scatterPlot.tsx index a47ccb5f6..543f655c7 100644 --- a/frontend/src/modules/DistributionPlot/components/scatterPlot.tsx +++ b/frontend/src/modules/DistributionPlot/components/scatterPlot.tsx @@ -1,5 +1,6 @@ import React from "react"; -import Plot from "react-plotly.js"; + +import { Plot } from "@lib/components/Plot"; import { Layout, PlotData, PlotHoverEvent } from "plotly.js"; diff --git a/frontend/src/modules/DistributionPlot/components/scatterPlotWithColorMapping.tsx b/frontend/src/modules/DistributionPlot/components/scatterPlotWithColorMapping.tsx index 7bcf376c1..f22d02985 100644 --- a/frontend/src/modules/DistributionPlot/components/scatterPlotWithColorMapping.tsx +++ b/frontend/src/modules/DistributionPlot/components/scatterPlotWithColorMapping.tsx @@ -1,5 +1,6 @@ import React from "react"; -import Plot from "react-plotly.js"; + +import { Plot } from "@lib/components/Plot"; import { Layout, PlotData, PlotHoverEvent } from "plotly.js"; diff --git a/frontend/src/modules/InplaceVolumetrics/view.tsx b/frontend/src/modules/InplaceVolumetrics/view.tsx index fb491b3dd..d8bd6a9dc 100644 --- a/frontend/src/modules/InplaceVolumetrics/view.tsx +++ b/frontend/src/modules/InplaceVolumetrics/view.tsx @@ -1,5 +1,4 @@ import React from "react"; -import Plot from "react-plotly.js"; import { Body_get_realizations_response_api } from "@api"; import { BroadcastChannelMeta } from "@framework/Broadcaster"; @@ -7,6 +6,7 @@ import { ModuleFCProps } from "@framework/Module"; import { useSubscribedValue } from "@framework/WorkbenchServices"; import { ApiStateWrapper } from "@lib/components/ApiStateWrapper"; import { CircularProgress } from "@lib/components/CircularProgress"; +import { Plot } from "@lib/components/Plot"; import { useElementSize } from "@lib/hooks/useElementSize"; import { Layout, PlotData, PlotHoverEvent } from "plotly.js"; diff --git a/frontend/src/modules/SimulationTimeSeries/view.tsx b/frontend/src/modules/SimulationTimeSeries/view.tsx index de96a2dda..a2fa58fa5 100644 --- a/frontend/src/modules/SimulationTimeSeries/view.tsx +++ b/frontend/src/modules/SimulationTimeSeries/view.tsx @@ -1,10 +1,10 @@ import React from "react"; -import Plot from "react-plotly.js"; import { VectorHistoricalData_api, VectorRealizationData_api, VectorStatisticData_api } from "@api"; import { BroadcastChannelMeta } from "@framework/Broadcaster"; import { ModuleFCProps } from "@framework/Module"; import { useSubscribedValue } from "@framework/WorkbenchServices"; +import { Plot } from "@lib/components/Plot"; import { useElementSize } from "@lib/hooks/useElementSize"; import { Layout, PlotData, PlotHoverEvent } from "plotly.js"; @@ -123,10 +123,8 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod const tracesDataArr: MyPlotData[] = []; if (showRealizations && vectorQuery.data && vectorQuery.data.length > 0) { - let highlightedTrace: MyPlotData | null = null; for (let i = 0; i < vectorQuery.data.length; i++) { const vec = vectorQuery.data[i]; - const isHighlighted = vec.realization === subscribedPlotlyRealization?.realization ? true : false; const curveColor = vec.realization === subscribedPlotlyRealization?.realization ? "red" : "green"; const lineWidth = vec.realization === subscribedPlotlyRealization?.realization ? 3 : 1; const lineShape = vec.is_rate ? "vh" : "linear"; @@ -140,16 +138,7 @@ export const view = ({ moduleContext, workbenchSession, workbenchServices }: Mod mode: "lines", line: { color: curveColor, width: lineWidth, shape: lineShape }, }; - - if (isHighlighted) { - highlightedTrace = trace; - } else { - tracesDataArr.push(trace); - } - } - - if (highlightedTrace) { - tracesDataArr.push(highlightedTrace); + tracesDataArr.push(trace); } }