/*
* Copyright 2021-2025 Avaiga Private Limited
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
import React, { CSSProperties, lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTheme } from "@mui/material";
import Box from "@mui/material/Box";
import Skeleton from "@mui/material/Skeleton";
import Tooltip from "@mui/material/Tooltip";
import isEqual from "lodash/isEqual";
import merge from "lodash/merge";
import { nanoid } from "nanoid";
import {
AnimationOpts,
Config,
Data,
Frame,
Layout,
ModeBarButtonAny,
PlotDatum,
PlotData,
PlotlyHTMLElement,
PlotMarker,
PlotRelayoutEvent,
PlotSelectionEvent,
Root,
ScatterLine,
} from "plotly.js";
import { Figure } from "react-plotly.js";
import {
createRequestChartUpdateAction,
createSendActionNameAction,
createSendUpdateAction,
} from "../../context/taipyReducers";
import { lightenPayload } from "../../context/wsUtils";
import { darkThemeTemplate } from "../../themes/darkThemeTemplate";
import {
useClassNames,
useDispatch,
useDispatchRequestUpdateOnFirstRender,
useDynamicJsonProperty,
useDynamicProperty,
useModule,
} from "../../utils/hooks";
import { ColumnDesc } from "./tableUtils";
import { getComponentClassName } from "./TaipyStyle";
import { getArrayValue, getUpdateVar, TaipyActiveProps, TaipyChangeProps } from "./utils";
const Plot = lazy(() => import("react-plotly.js"));
interface PlotlyObject {
animate: (
root: Root,
frameOrGroupNameOrFrameList?: string | string[] | Partial | Array>,
opts?: Partial
) => Promise;
}
interface ChartProp extends TaipyActiveProps, TaipyChangeProps {
title?: string;
defaultTitle?: string;
width?: string | number;
height?: string | number;
defaultConfig: string;
config?: string;
data?: Record;
animationData?: Record;
//data${number}?: Record;
defaultLayout?: string;
layout?: string;
plotConfig?: string;
onRangeChange?: string;
render?: boolean;
defaultRender?: boolean;
template?: string;
template_Dark_?: string;
template_Light_?: string;
//[key: `selected${number}`]: number[];
figure?: Array>;
onClick?: string;
dataVarNames?: string;
}
interface ChartConfig {
columns: Array>;
labels: string[];
modes: string[];
types: string[];
traces: string[][];
xaxis: string[];
yaxis: string[];
markers: Partial[];
selectedMarkers: Partial[];
orientations: string[];
names: string[];
lines: Partial[];
texts: string[];
textAnchors: string[];
options: Record[];
axisNames: Array;
addIndex: Array;
decimators?: string[];
}
export type TraceValueType = Record;
const defaultStyle = { position: "relative", display: "inline-block" };
const indexedData = /^(\d+)\/(.*)/;
export const getColNameFromIndexed = (colName: string): string => {
if (colName) {
const reRes = indexedData.exec(colName);
if (reRes && reRes.length > 2) {
return reRes[2] || colName;
}
}
return colName;
};
export const getValue = (
values: TraceValueType | undefined,
arr: T[],
idx: number,
returnUndefined = false
): (string | number)[] | undefined => {
const value = getValueFromCol(values, getArrayValue(arr, idx) as unknown as string);
if (!returnUndefined || value.length) {
return value;
}
return undefined;
};
export const getValueFromCol = (values: TraceValueType | undefined, col: string): (string | number)[] => {
if (values) {
if (col) {
// TODO: Re-review the logic here
if (Array.isArray(values)) {
const reRes = indexedData.exec(col);
if (reRes && reRes.length > 2) {
return values[parseInt(reRes[1], 10) || 0][reRes[2] || col] || [];
}
} else {
return values[col] || [];
}
}
}
return [];
};
export const getAxis = (traces: string[][], idx: number, columns: Record, axis: number) => {
if (traces.length > idx && traces[idx].length > axis && traces[idx][axis] && columns[traces[idx][axis]])
return columns[traces[idx][axis]].dfid;
return undefined;
};
const getDecimatorsPayload = (
decimators: string[] | undefined,
plotDiv: HTMLDivElement | null,
modes: string[],
columns: Record,
traces: string[][],
relayoutData?: PlotRelayoutEvent
) => {
return decimators
? {
width: plotDiv?.clientWidth,
height: plotDiv?.clientHeight,
decimators: decimators.map((d, i) =>
d
? {
decimator: d,
xAxis: getAxis(traces, i, columns, 0),
yAxis: getAxis(traces, i, columns, 1),
zAxis: getAxis(traces, i, columns, 2),
chartMode: modes[i],
}
: {
xAxis: getAxis(traces, i, columns, 0),
yAxis: getAxis(traces, i, columns, 1),
zAxis: getAxis(traces, i, columns, 2),
chartMode: modes[i],
}
),
relayoutData: relayoutData,
}
: undefined;
};
const selectedPropRe = /selected(\d+)/;
const MARKER_TO_COL = ["color", "size", "symbol", "opacity", "colors"];
const DEFAULT_ANIMATION_SETTINGS: Partial = {
transition: {
duration: 500,
easing: "cubic-in-out",
},
frame: {
duration: 500,
},
mode: "immediate",
};
const isOnClick = (types: string[]) => (types?.length ? types.every((t) => t === "pie") : false);
interface Axis {
p2c: () => number;
p2d: (a: number) => number;
}
interface PlotlyMap {
_subplot?: { xaxis: Axis; yaxis: Axis };
}
interface PlotlyDiv extends HTMLDivElement {
_fullLayout?: {
map?: PlotlyMap;
geo?: PlotlyMap;
mapbox?: PlotlyMap;
xaxis?: Axis;
yaxis?: Axis;
};
}
interface ExtendedPlotData extends PlotData {
meta?: {
xAxisName?: string;
yAxisName?: string;
};
}
interface WithPointNumbers {
pointNumbers: number[];
}
export const getPlotIndex = (pt: PlotDatum) =>
pt.pointIndex === undefined
? pt.pointNumber === undefined
? (pt as unknown as WithPointNumbers).pointNumbers?.length
? (pt as unknown as WithPointNumbers).pointNumbers[0]
: 0
: pt.pointNumber
: pt.pointIndex;
const defaultConfig = {
columns: [] as Array>,
labels: [],
modes: [],
types: [],
traces: [],
xaxis: [],
yaxis: [],
markers: [],
selectedMarkers: [],
orientations: [],
names: [],
lines: [],
texts: [],
textAnchors: [],
options: [],
axisNames: [],
addIndex: [],
} as ChartConfig;
const emptyLayout = {} as Partial;
const emptyData = {} as Record;
export const TaipyPlotlyButtons: ModeBarButtonAny[] = [
{
name: "Full screen",
title: "Full screen",
icon: {
height: 24,
width: 24,
path: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z",
},
click: function (gd: HTMLElement, evt: Event) {
const div = gd.querySelector("div.svg-container") as HTMLDivElement;
if (!div) {
return;
}
const { height, width } = gd.dataset;
if (!height) {
const st = getComputedStyle(div);
gd.setAttribute("data-height", st.height);
gd.setAttribute("data-width", st.width);
}
const fs = gd.classList.toggle("full-screen");
(evt.currentTarget as HTMLElement).setAttribute("data-title", fs ? "Exit Full screen" : "Full screen");
if (!fs) {
// height && div.attributeStyleMap.set("height", height);
height && (div.style.height = height);
// width && div.attributeStyleMap.set("width", width);
width && (div.style.width = width);
}
window.dispatchEvent(new Event("resize"));
},
},
];
const updateArrays = (sel: number[][], val: number[], idx: number) => {
if (idx >= sel.length || val.length !== sel[idx].length || val.some((v, i) => sel[idx][i] != v)) {
sel = sel.concat(); // shallow copy
sel[idx] = val;
}
return sel;
};
const getDataKey = (columns?: Record, decimators?: string[]): [string[], string] => {
const backCols = columns ? Object.values(columns).map((col) => col.dfid) : [];
return [backCols, backCols.join("-") + (decimators ? `--${decimators.join("")}` : "")];
};
const isDataRefresh = (data?: Record) => data?.__taipy_refresh !== undefined;
const getDataVarName = (updateVarName: string | undefined, dataVarNames: string[], idx: number) =>
idx === 0 ? updateVarName : dataVarNames[idx - 1];
const getData = (
data: Record,
additionalDatas: Array>,
idx: number
) => (idx === 0 ? data : idx <= additionalDatas.length ? additionalDatas[idx - 1] : undefined);
const Chart = (props: ChartProp) => {
const {
width = "100%",
height,
updateVarName,
updateVars,
id,
data = emptyData,
onRangeChange,
propagate = true,
onClick,
animationData,
} = props;
const dispatch = useDispatch();
const [selected, setSelected] = useState([]);
const plotRef = useRef(null);
const plotlyRef = useRef(null);
const [dataKeys, setDataKeys] = useState([]);
// animation
const [toFrame, setToFrame] = useState>({
data: [],
traces: [],
});
const lastDataPl = useRef([]);
const theme = useTheme();
const module = useModule();
const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
const active = useDynamicProperty(props.active, props.defaultActive, true);
const render = useDynamicProperty(props.render, props.defaultRender, true);
const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
const baseLayout = useDynamicJsonProperty(props.layout, props.defaultLayout || "", emptyLayout);
const title = useDynamicProperty(props.title, props.defaultTitle, "");
const dataVarNames = useMemo(() => (props.dataVarNames ? props.dataVarNames.split(";") : []), [props.dataVarNames]);
const oldAdditionalDatas = useRef>>([]);
const additionalDatas = useMemo(() => {
const newAdditionalDatas = dataVarNames.map(
(_, idx) => (props as unknown as Record>)[`data${idx + 1}`]
);
if (newAdditionalDatas.length !== oldAdditionalDatas.current.length) {
oldAdditionalDatas.current = newAdditionalDatas;
} else if (!newAdditionalDatas.every((d, idx) => d === oldAdditionalDatas.current[idx])) {
oldAdditionalDatas.current = newAdditionalDatas;
}
return oldAdditionalDatas.current;
}, [dataVarNames, props]);
const refresh = useMemo(
() => (isDataRefresh(data) || additionalDatas.some((d) => isDataRefresh(d)) ? nanoid() : false),
[data, additionalDatas]
);
// get props.selected[i] values
useEffect(() => {
if (props.figure) {
return;
}
setSelected((sel) => {
Object.keys(props).forEach((key) => {
const res = selectedPropRe.exec(key);
if (res && res.length == 2) {
const idx = parseInt(res[1], 10);
let val = (props as unknown as Record)[key];
if (val !== undefined) {
if (typeof val === "string") {
try {
val = JSON.parse(val) as number[];
} catch {
// too bad
val = [];
}
}
if (!Array.isArray(val)) {
val = [];
}
if (idx === 0 && val.length && Array.isArray(val[0])) {
for (let i = 0; i < val.length; i++) {
sel = updateArrays(sel, val[i] as unknown as number[], i);
}
} else {
sel = updateArrays(sel, val, idx);
}
}
}
});
return sel;
});
}, [props]);
const config = useDynamicJsonProperty(props.config, props.defaultConfig, defaultConfig);
useEffect(() => {
setDataKeys((oldDtKeys) => {
let changed = false;
const newDtKeys = (config.columns || []).map((columns, idx) => {
const varName = getDataVarName(updateVarName, dataVarNames, idx);
if (varName) {
const [backCols, dtKey] = getDataKey(columns, config.decimators);
changed = changed || idx > oldDtKeys.length || oldDtKeys[idx] !== dtKey;
const lData = getData(data, additionalDatas, idx);
if (lData === undefined || isDataRefresh(lData) || !lData[dtKey]) {
Promise.resolve().then(() =>
dispatch(
createRequestChartUpdateAction(
varName,
id,
module,
backCols,
dtKey,
getDecimatorsPayload(
config.decimators,
plotRef.current,
config.modes,
columns,
config.traces
)
)
)
);
}
return dtKey;
}
return "";
});
return changed ? newDtKeys : oldDtKeys;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
refresh,
dispatch,
config.columns,
config.traces,
config.modes,
config.decimators,
updateVarName,
dataVarNames,
id,
module,
]);
useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
const layout = useMemo(() => {
const layout = { ...baseLayout };
let template = undefined;
try {
const tpl = props.template && JSON.parse(props.template);
const tplTheme =
theme.palette.mode === "dark"
? props.template_Dark_
? JSON.parse(props.template_Dark_)
: darkThemeTemplate
: props.template_Light_ && JSON.parse(props.template_Light_);
template = tpl ? (tplTheme ? { ...tpl, ...tplTheme } : tpl) : tplTheme ? tplTheme : undefined;
} catch (e) {
console.info(`Error while parsing Chart.template\n${(e as Error).message || e}`);
}
if (template) {
layout.template = template;
}
if (props.figure) {
return merge({}, props.figure[0].layout as Partial, layout, {
title: title || layout.title || (props.figure[0].layout as Partial).title,
clickmode: "event+select",
});
}
return {
...layout,
autosize: true,
title: title || layout.title,
xaxis: {
title:
config.traces.length && config.traces[0].length && config.traces[0][0]
? getColNameFromIndexed(config.columns[0][config.traces[0][0]]?.dfid)
: undefined,
...layout.xaxis,
},
yaxis: {
title:
config.traces.length == 1 && config.traces[0].length > 1 && config.columns[0][config.traces[0][1]]
? getColNameFromIndexed(config.columns[0][config.traces[0][1]]?.dfid)
: undefined,
...layout.yaxis,
},
clickmode: "event+select",
} as Layout;
}, [
theme.palette.mode,
title,
config.columns,
config.traces,
baseLayout,
props.template,
props.template_Dark_,
props.template_Light_,
props.figure,
]);
useEffect(() => {
if (animationData?.__taipy_refresh) {
const animationDataVar = getUpdateVar(updateVars || "", "animationData");
animationDataVar &&
dispatch(createRequestChartUpdateAction(animationDataVar, id, module, [], "", undefined));
}
}, [animationData?.__taipy_refresh, dispatch, id, module, updateVars]);
const runAnimation = useCallback(() => {
return (
plotRef.current &&
plotlyRef.current &&
toFrame.data &&
(toFrame.traces && toFrame.traces.length > 0 ? true : null) &&
plotlyRef.current.animate(
plotRef.current as unknown as PlotlyHTMLElement,
{
...toFrame,
layout: layout,
},
DEFAULT_ANIMATION_SETTINGS
)
);
}, [layout, toFrame]);
const style = useMemo(
() =>
height === undefined
? ({ ...defaultStyle, width: width } as CSSProperties)
: ({ ...defaultStyle, width: width, height: height } as CSSProperties),
[width, height]
);
const skelStyle = useMemo(() => ({ ...style, minHeight: "7em" }), [style]);
const dataPl = useMemo(() => {
if (props.figure) {
return lastDataPl.current || [];
}
const dataList = dataKeys.map((_, idx) => getData(data, additionalDatas, idx));
if (!dataList.length || dataList.every((d) => !d || isDataRefresh(d) || !Object.keys(d).length)) {
return lastDataPl.current || [];
}
let changed = false;
let baseDataPl = (lastDataPl.current.length && lastDataPl.current[0]) || {};
const newDataPl = config.traces.map((trace, idx) => {
const currentData = (idx < lastDataPl.current.length && lastDataPl.current[idx]) || baseDataPl;
const dataKey = idx < dataKeys.length ? dataKeys[idx] : dataKeys[0];
const lData = (idx < dataList.length && dataList[idx]) || dataList[0];
if (!lData || isDataRefresh(lData) || !Object.keys(lData).length) {
return currentData;
}
const dtKey = getDataKey(
idx < config.columns?.length ? config.columns[idx] : undefined,
config.decimators
)[1];
if (!dataKey.startsWith(dtKey)) {
return currentData;
}
changed = true;
const datum = lData[dataKey];
const columns = config.columns[idx] || config.columns[0];
const ret = {
...getArrayValue(config.options, idx, {}),
type: config.types[idx],
mode: config.modes[idx],
name:
getArrayValue(config.names, idx) ||
(columns[trace[1]] ? getColNameFromIndexed(columns[trace[1]].dfid) : undefined),
} as Record;
ret.marker = { ...getArrayValue(config.markers, idx, ret.marker || {}) };
if (Object.keys(ret.marker as object).length) {
MARKER_TO_COL.forEach((prop) => {
const val = (ret.marker as Record)[prop];
if (typeof val === "string") {
const arr = getValueFromCol(datum, val as string);
if (arr.length) {
(ret.marker as Record)[prop] = arr;
}
}
});
} else {
delete ret.marker;
}
const xs = getValue(datum, trace, 0) || [];
const ys = getValue(datum, trace, 1) || [];
const addIndex = getArrayValue(config.addIndex, idx, true) && !ys.length;
const baseX = addIndex ? Array.from(Array(xs.length).keys()) : xs;
const baseY = addIndex ? xs : ys;
const axisNames = config.axisNames.length > idx ? config.axisNames[idx] : ([] as string[]);
if (baseX.length) {
if (axisNames.length > 0) {
ret[axisNames[0]] = baseX;
} else {
ret.x = baseX;
}
}
if (baseY.length) {
if (axisNames.length > 1) {
ret[axisNames[1]] = baseY;
} else {
ret.y = baseY;
}
}
const baseZ = getValue(datum, trace, 2, true);
if (baseZ) {
if (axisNames.length > 2) {
ret[axisNames[2]] = baseZ;
} else {
ret.z = baseZ;
}
}
// Hack for treemap charts: create a fallback 'parents' column if needed
// This works ONLY because 'parents' is the third named axis
// (see __CHART_AXIS in gui/utils/chart_config_builder.py)
else if (config.types[idx] === "treemap" && Array.isArray(ret.labels)) {
ret.parents = Array(ret.labels.length).fill("");
}
// Other axis
for (let i = 3; i < axisNames.length; i++) {
ret[axisNames[i]] = getValue(datum, trace, i, true);
}
ret.text = getValue(datum, config.texts, idx, true);
ret.xaxis = config.xaxis[idx];
ret.yaxis = config.yaxis[idx];
ret.hovertext = getValue(datum, config.labels, idx, true);
ret.meta = {
xAxisName: config.traces[idx][0],
yAxisName: config.traces[idx][1],
};
const selPoints = getArrayValue(selected, idx, []);
if (selPoints?.length) {
ret.selectedpoints = selPoints;
}
ret.orientation = getArrayValue(config.orientations, idx);
ret.line = getArrayValue(config.lines, idx);
ret.textposition = getArrayValue(config.textAnchors, idx);
const selectedMarker = getArrayValue(config.selectedMarkers, idx);
if (selectedMarker) {
ret.selected = { marker: selectedMarker };
}
if (idx == 0) {
baseDataPl = ret;
}
return ret;
});
if (changed) {
lastDataPl.current = newDataPl as ExtendedPlotData[];
}
return lastDataPl.current;
}, [props.figure, selected, data, additionalDatas, config, dataKeys]);
const plotConfig = useMemo(() => {
let plConf: Partial = {};
if (props.plotConfig) {
try {
plConf = JSON.parse(props.plotConfig);
} catch (e) {
console.info(`Error while parsing Chart.plot_config\n${(e as Error).message || e}`);
}
if (typeof plConf !== "object" || plConf === null || Array.isArray(plConf)) {
console.info("Error Chart.plot_config is not a dictionary");
plConf = {};
}
}
plConf.displaylogo = !!plConf.displaylogo;
plConf.modeBarButtonsToAdd = TaipyPlotlyButtons;
// plConf.responsive = true; // this is the source of the on/off height ...
plConf.autosizable = true;
if (!active) {
plConf.staticPlot = true;
}
return plConf;
}, [active, props.plotConfig]);
const onRelayout = useCallback(
(eventData: PlotRelayoutEvent) => {
onRangeChange && dispatch(createSendActionNameAction(id, module, { action: onRangeChange, ...eventData }));
if (config.decimators && !config.types.includes("scatter3d")) {
const [backCols, dtKeyBase] = getDataKey(
config.columns?.length ? config.columns[0] : undefined,
config.decimators
);
const dtKey = `${dtKeyBase}--${Object.entries(eventData)
.map(([k, v]) => `${k}=${v}`)
.join("-")}`;
setDataKeys((oldDataKeys) => {
if (oldDataKeys.length === 0) {
return [dtKey];
}
if (oldDataKeys[0] !== dtKey) {
Promise.resolve().then(() =>
dispatch(
createRequestChartUpdateAction(
updateVarName,
id,
module,
backCols,
dtKey,
getDecimatorsPayload(
config.decimators,
plotRef.current,
config.modes,
config.columns?.length ? config.columns[0] : {},
config.traces,
eventData
)
)
)
);
return [dtKey, ...oldDataKeys.slice(1)];
}
return oldDataKeys;
});
}
},
[
dispatch,
onRangeChange,
id,
config.modes,
config.columns,
config.traces,
config.types,
config.decimators,
updateVarName,
module,
]
);
const clickHandler = useCallback(
(evt?: MouseEvent) => {
const map =
(evt?.currentTarget as PlotlyDiv)?._fullLayout?.map ||
(evt?.currentTarget as PlotlyDiv)?._fullLayout?.geo ||
(evt?.currentTarget as PlotlyDiv)?._fullLayout?.mapbox;
const xaxis = map ? map._subplot?.xaxis : (evt?.currentTarget as PlotlyDiv)?._fullLayout?.xaxis;
const yaxis = map ? map._subplot?.yaxis : (evt?.currentTarget as PlotlyDiv)?._fullLayout?.yaxis;
if (!xaxis || !yaxis) {
console.info("clickHandler: Plotly div does not have an xaxis object", evt);
return;
}
const transform = (axis: Axis, delta: keyof DOMRect) => {
const bb = (evt?.target as HTMLDivElement).getBoundingClientRect();
return (pos?: number) => axis.p2d((pos || 0) - (bb[delta] as number));
};
dispatch(
createSendActionNameAction(
id,
module,
lightenPayload({
action: onClick,
lat: map ? yaxis.p2c() : undefined,
y: map ? undefined : transform(yaxis, "top")(evt?.clientY),
lon: map ? xaxis.p2c() : undefined,
x: map ? undefined : transform(xaxis, "left")(evt?.clientX),
})
)
);
},
[dispatch, module, id, onClick]
);
const onInitialized = useCallback(
(figure: Readonly, graphDiv: Readonly) => {
onClick && graphDiv.addEventListener("click", clickHandler);
plotRef.current = graphDiv as HTMLDivElement;
plotlyRef.current = window.Plotly as unknown as PlotlyObject;
if (animationData) {
runAnimation()?.catch(console.error);
}
},
[onClick, clickHandler, animationData, runAnimation]
);
const getRealIndex = useCallback(
(dataIdx: number, index?: number) => {
const lData = getData(data, additionalDatas, dataIdx);
if (!lData) {
return index || 0;
}
const dtKey = dataKeys[dataIdx];
return typeof index === "number"
? props.figure
? index
: lData[dtKey].tp_index
? (lData[dtKey].tp_index[index] as number)
: index
: 0;
},
[data, additionalDatas, dataKeys, props.figure]
);
const onSelect = useCallback(
(evt?: PlotSelectionEvent) => {
if (updateVars) {
const traces = (evt?.points || []).reduce((tr, pt) => {
tr[pt.curveNumber] = tr[pt.curveNumber] || [];
tr[pt.curveNumber].push(getRealIndex(pt.curveNumber, getPlotIndex(pt)));
return tr;
}, [] as number[][]);
if (config.traces.length === 0) {
// figure
const theVar = getUpdateVar(updateVars, "selected");
theVar && dispatch(createSendUpdateAction(theVar, traces, module, props.onChange, propagate));
return;
}
if (traces.length) {
const upvars = traces.map((_, idx) => getUpdateVar(updateVars, `selected${idx}`));
const setVars = new Set(upvars.filter((v) => v));
if (traces.length > 1 && setVars.size === 1) {
dispatch(
createSendUpdateAction(
setVars.values().next().value,
traces,
module,
props.onChange,
propagate
)
);
return;
}
traces.forEach((tr, idx) => {
if (upvars[idx] && tr && tr.length) {
dispatch(createSendUpdateAction(upvars[idx], tr, module, props.onChange, propagate));
}
});
} else if (config.traces.length === 1) {
const upVar = getUpdateVar(updateVars, "selected0");
if (upVar) {
dispatch(createSendUpdateAction(upVar, [], module, props.onChange, propagate));
}
}
}
},
[getRealIndex, dispatch, updateVars, propagate, props.onChange, config.traces.length, module]
);
useEffect(() => {
if (!dataPl.length || !animationData || isDataRefresh(animationData)) {
return;
}
const animationKeys = Object.keys(animationData) as Array;
let found = false;
const toFramesData = dataPl
.map((trace) => {
const traceAnimationKeys = animationKeys.filter(
(key) => trace.hasOwnProperty(key) && Array.isArray(trace[key]) && Array.isArray(animationData[key])
);
if (!traceAnimationKeys.length) {
return undefined;
}
return traceAnimationKeys.reduce(
(tr, key) => {
if (!isEqual(trace[key], animationData[key])) {
found = true;
tr[key] = animationData[key];
}
return tr;
},
{ ...trace } as Record
) as unknown as ExtendedPlotData;
})
.filter((t) => t);
if (!found) {
return;
}
if (toFramesData.length) {
setToFrame({
data: toFramesData as Data[],
traces: dataPl.map((_, idx) => idx),
});
}
}, [dataPl, animationData]);
useEffect(() => {
if (!plotRef.current || !toFrame.data?.length) {
return;
}
const plotElement = plotRef.current as unknown as PlotlyHTMLElement;
if (!plotElement?.data) {
return;
}
const timer = setTimeout(() => {
if (plotRef.current) {
runAnimation()?.catch(console.error);
}
}, 100);
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [toFrame.data?.length, runAnimation]);
return render ? (
}>
{Array.isArray(props.figure) && props.figure.length && props.figure[0].data !== undefined ? (
) : (
)}
{props.children}
) : null;
};
export default Chart;