/* * 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;