Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plotly plot state wrapper #231

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions frontend/src/lib/components/Plot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Plot } from "./plot";
148 changes: 148 additions & 0 deletions frontend/src/lib/components/Plot/plot.tsx
Original file line number Diff line number Diff line change
@@ -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<PlotParams> = (props) => {
const {
data: propsData,
layout: propsLayout,
frames: propsFrames,
onHover: propsOnHover,
onUnhover: propsOnUnhover,
...rest
} = props;

const [data, setData] = React.useState<Plotly.Data[]>(propsData);
const [layout, setLayout] = React.useState<Partial<Plotly.Layout>>(propsLayout);
const [frames, setFrames] = React.useState<Plotly.Frame[] | null>(propsFrames || null);
const [prevData, setPrevData] = React.useState<Plotly.Data[]>(propsData);
const [prevLayout, setPrevLayout] = React.useState<Partial<Plotly.Layout>>(propsLayout);
const [prevFrames, setPrevFrames] = React.useState<Plotly.Frame[] | null>(propsFrames || null);
const id = React.useRef<string>(`plot-${v4()}`);

const timeout = React.useRef<ReturnType<typeof setTimeout> | 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<Plotly.PlotHoverEvent>) {
if (propsOnHover && !eventsDisabled.current) {
propsOnHover(event);
}
return;
}

function handleUnhover(event: Readonly<Plotly.PlotMouseEvent>) {
if (propsOnUnhover && !eventsDisabled.current) {
propsOnUnhover(event);
}
return;
}

function handleUpdate(figure: Readonly<Figure>, graphDiv: Readonly<HTMLElement>) {
console.debug("update");
}

return (
<PlotlyPlot
{...rest}
divId={id.current}
config={{
modeBarButtons: [["pan2d", "autoScale2d", "zoomIn2d", "zoomOut2d", "toImage"]],
displaylogo: false,
...props.config,
}}
data={data}
layout={layout}
frames={frames || undefined}
onInitialized={handleInitialized}
onRelayout={handleRelayout}
onHover={handleHover}
onUnhover={handleUnhover}
onUpdate={handleUpdate}
/>
);
};

Plot.displayName = "Plot";
7 changes: 4 additions & 3 deletions frontend/src/modules/DistributionPlot/components/barChart.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -59,8 +60,8 @@ export const BarChart: React.FC<BarChartProps> = (props) => {
const layout: Partial<Layout> = {
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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/modules/InplaceVolumetrics/view.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from "react";
import Plot from "react-plotly.js";

import { Body_get_realizations_response_api } from "@api";
import { BroadcastChannelMeta } from "@framework/Broadcaster";
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";
Expand Down
15 changes: 2 additions & 13 deletions frontend/src/modules/SimulationTimeSeries/view.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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";
Expand All @@ -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);
}
}

Expand Down