Chart.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. /*
  2. * Copyright 2021-2024 Avaiga Private Limited
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  5. * the License. You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  10. * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  11. * specific language governing permissions and limitations under the License.
  12. */
  13. import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState, lazy, Suspense } from "react";
  14. import {
  15. Config,
  16. Data,
  17. Layout,
  18. ModeBarButtonAny,
  19. PlotDatum,
  20. PlotMarker,
  21. PlotRelayoutEvent,
  22. PlotSelectionEvent,
  23. ScatterLine,
  24. } from "plotly.js";
  25. import Skeleton from "@mui/material/Skeleton";
  26. import Box from "@mui/material/Box";
  27. import Tooltip from "@mui/material/Tooltip";
  28. import { useTheme } from "@mui/material";
  29. import { getArrayValue, getUpdateVar, TaipyActiveProps, TaipyChangeProps } from "./utils";
  30. import {
  31. createRequestChartUpdateAction,
  32. createSendActionNameAction,
  33. createSendUpdateAction,
  34. } from "../../context/taipyReducers";
  35. import { ColumnDesc } from "./tableUtils";
  36. import {
  37. useClassNames,
  38. useDispatch,
  39. useDispatchRequestUpdateOnFirstRender,
  40. useDynamicJsonProperty,
  41. useDynamicProperty,
  42. useModule,
  43. } from "../../utils/hooks";
  44. import { darkThemeTemplate } from "../../themes/darkThemeTemplate";
  45. import { Figure } from "react-plotly.js";
  46. import { lightenPayload } from "../../context/wsUtils";
  47. const Plot = lazy(() => import("react-plotly.js"));
  48. interface ChartProp extends TaipyActiveProps, TaipyChangeProps {
  49. title?: string;
  50. width?: string | number;
  51. height?: string | number;
  52. defaultConfig: string;
  53. config?: string;
  54. data?: Record<string, TraceValueType>;
  55. defaultLayout?: string;
  56. layout?: string;
  57. plotConfig?: string;
  58. onRangeChange?: string;
  59. testId?: string;
  60. render?: boolean;
  61. defaultRender?: boolean;
  62. template?: string;
  63. template_Dark_?: string;
  64. template_Light_?: string;
  65. //[key: `selected_${number}`]: number[];
  66. figure?: Array<Record<string, unknown>>;
  67. onClick?: string;
  68. }
  69. interface ChartConfig {
  70. columns: Record<string, ColumnDesc>;
  71. labels: string[];
  72. modes: string[];
  73. types: string[];
  74. traces: string[][];
  75. xaxis: string[];
  76. yaxis: string[];
  77. markers: Partial<PlotMarker>[];
  78. selectedMarkers: Partial<PlotMarker>[];
  79. orientations: string[];
  80. names: string[];
  81. lines: Partial<ScatterLine>[];
  82. texts: string[];
  83. textAnchors: string[];
  84. options: Record<string, unknown>[];
  85. axisNames: Array<string[]>;
  86. addIndex: Array<boolean>;
  87. decimators?: string[];
  88. }
  89. export type TraceValueType = Record<string, (string | number)[]>;
  90. const defaultStyle = { position: "relative", display: "inline-block" };
  91. const indexedData = /^(\d+)\/(.*)/;
  92. export const getColNameFromIndexed = (colName: string): string => {
  93. if (colName) {
  94. const reRes = indexedData.exec(colName);
  95. if (reRes && reRes.length > 2) {
  96. return reRes[2] || colName;
  97. }
  98. }
  99. return colName;
  100. };
  101. export const getValue = <T,>(
  102. values: TraceValueType | undefined,
  103. arr: T[],
  104. idx: number,
  105. returnUndefined = false,
  106. ): (string | number)[] | undefined => {
  107. const value = getValueFromCol(values, getArrayValue(arr, idx) as unknown as string);
  108. if (!returnUndefined || value.length) {
  109. return value;
  110. }
  111. return undefined;
  112. };
  113. export const getValueFromCol = (values: TraceValueType | undefined, col: string): (string | number)[] => {
  114. if (values) {
  115. if (col) {
  116. // TODO: Re-review the logic here
  117. if (Array.isArray(values)) {
  118. const reRes = indexedData.exec(col);
  119. if (reRes && reRes.length > 2) {
  120. return values[parseInt(reRes[1], 10) || 0][reRes[2] || col] || [];
  121. }
  122. } else {
  123. return values[col] || [];
  124. }
  125. }
  126. }
  127. return [];
  128. };
  129. export const getAxis = (traces: string[][], idx: number, columns: Record<string, ColumnDesc>, axis: number) => {
  130. if (traces.length > idx && traces[idx].length > axis && traces[idx][axis] && columns[traces[idx][axis]])
  131. return columns[traces[idx][axis]].dfid;
  132. return undefined;
  133. };
  134. const getDecimatorsPayload = (
  135. decimators: string[] | undefined,
  136. plotDiv: HTMLDivElement | null,
  137. modes: string[],
  138. columns: Record<string, ColumnDesc>,
  139. traces: string[][],
  140. relayoutData?: PlotRelayoutEvent,
  141. ) => {
  142. return decimators
  143. ? {
  144. width: plotDiv?.clientWidth,
  145. height: plotDiv?.clientHeight,
  146. decimators: decimators.map((d, i) =>
  147. d
  148. ? {
  149. decimator: d,
  150. xAxis: getAxis(traces, i, columns, 0),
  151. yAxis: getAxis(traces, i, columns, 1),
  152. zAxis: getAxis(traces, i, columns, 2),
  153. chartMode: modes[i],
  154. }
  155. : {
  156. xAxis: getAxis(traces, i, columns, 0),
  157. yAxis: getAxis(traces, i, columns, 1),
  158. zAxis: getAxis(traces, i, columns, 2),
  159. chartMode: modes[i],
  160. },
  161. ),
  162. relayoutData: relayoutData,
  163. }
  164. : undefined;
  165. };
  166. const selectedPropRe = /selected(\d+)/;
  167. const MARKER_TO_COL = ["color", "size", "symbol", "opacity", "colors"];
  168. const isOnClick = (types: string[]) => (types?.length ? types.every((t) => t === "pie") : false);
  169. interface Axis {
  170. p2c: () => number;
  171. p2d: (a: number) => number;
  172. }
  173. interface PlotlyMap {
  174. _subplot?: { xaxis: Axis; yaxis: Axis };
  175. }
  176. interface PlotlyDiv extends HTMLDivElement {
  177. _fullLayout?: {
  178. map?: PlotlyMap;
  179. geo?: PlotlyMap;
  180. mapbox?: PlotlyMap;
  181. xaxis?: Axis;
  182. yaxis?: Axis;
  183. };
  184. }
  185. interface WithpointNumbers {
  186. pointNumbers: number[];
  187. }
  188. export const getPlotIndex = (pt: PlotDatum) =>
  189. pt.pointIndex === undefined
  190. ? pt.pointNumber === undefined
  191. ? (pt as unknown as WithpointNumbers).pointNumbers?.length
  192. ? (pt as unknown as WithpointNumbers).pointNumbers[0]
  193. : 0
  194. : pt.pointNumber
  195. : pt.pointIndex;
  196. const defaultConfig = {
  197. columns: {} as Record<string, ColumnDesc>,
  198. labels: [],
  199. modes: [],
  200. types: [],
  201. traces: [],
  202. xaxis: [],
  203. yaxis: [],
  204. markers: [],
  205. selectedMarkers: [],
  206. orientations: [],
  207. names: [],
  208. lines: [],
  209. texts: [],
  210. textAnchors: [],
  211. options: [],
  212. axisNames: [],
  213. addIndex: [],
  214. } as ChartConfig;
  215. const emptyLayout = {} as Partial<Layout>;
  216. const emptyData = {} as Record<string, TraceValueType>;
  217. export const TaipyPlotlyButtons: ModeBarButtonAny[] = [
  218. {
  219. name: "Full screen",
  220. title: "Full screen",
  221. icon: {
  222. height: 24,
  223. width: 24,
  224. path: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z",
  225. },
  226. click: function (gd: HTMLElement, evt: Event) {
  227. const div = gd.querySelector("div.svg-container") as HTMLDivElement;
  228. if (!div) {
  229. return;
  230. }
  231. const { height, width } = gd.dataset;
  232. if (!height) {
  233. const st = getComputedStyle(div);
  234. gd.setAttribute("data-height", st.height);
  235. gd.setAttribute("data-width", st.width);
  236. }
  237. const fs = gd.classList.toggle("full-screen");
  238. (evt.currentTarget as HTMLElement).setAttribute("data-title", fs ? "Exit Full screen" : "Full screen");
  239. if (!fs) {
  240. // height && div.attributeStyleMap.set("height", height);
  241. height && (div.style.height = height);
  242. // width && div.attributeStyleMap.set("width", width);
  243. width && (div.style.width = width);
  244. }
  245. window.dispatchEvent(new Event("resize"));
  246. },
  247. },
  248. ];
  249. const updateArrays = (sel: number[][], val: number[], idx: number) => {
  250. if (idx >= sel.length || val.length !== sel[idx].length || val.some((v, i) => sel[idx][i] != v)) {
  251. sel = sel.concat(); // shallow copy
  252. sel[idx] = val;
  253. }
  254. return sel;
  255. };
  256. const Chart = (props: ChartProp) => {
  257. const {
  258. title = "",
  259. width = "100%",
  260. height,
  261. updateVarName,
  262. updateVars,
  263. id,
  264. data = emptyData,
  265. onRangeChange,
  266. propagate = true,
  267. onClick,
  268. } = props;
  269. const dispatch = useDispatch();
  270. const [selected, setSelected] = useState<number[][]>([]);
  271. const plotRef = useRef<HTMLDivElement>(null);
  272. const [dataKey, setDataKey] = useState("__default__");
  273. const lastDataPl = useRef<Data[]>([]);
  274. const theme = useTheme();
  275. const module = useModule();
  276. const refresh = typeof data.__taipy_refresh === "boolean";
  277. const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
  278. const active = useDynamicProperty(props.active, props.defaultActive, true);
  279. const render = useDynamicProperty(props.render, props.defaultRender, true);
  280. const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
  281. const baseLayout = useDynamicJsonProperty(props.layout, props.defaultLayout || "", emptyLayout);
  282. // get props.selected[i] values
  283. useEffect(() => {
  284. if (props.figure) {
  285. return;
  286. }
  287. setSelected((sel) => {
  288. Object.keys(props).forEach((key) => {
  289. const res = selectedPropRe.exec(key);
  290. if (res && res.length == 2) {
  291. const idx = parseInt(res[1], 10);
  292. let val = (props as unknown as Record<string, number[]>)[key];
  293. if (val !== undefined) {
  294. if (typeof val === "string") {
  295. try {
  296. val = JSON.parse(val) as number[];
  297. } catch {
  298. // too bad
  299. val = [];
  300. }
  301. }
  302. if (!Array.isArray(val)) {
  303. val = [];
  304. }
  305. if (idx === 0 && val.length && Array.isArray(val[0])) {
  306. for (let i = 0; i < val.length; i++) {
  307. sel = updateArrays(sel, val[i] as unknown as number[], i);
  308. }
  309. } else {
  310. sel = updateArrays(sel, val, idx);
  311. }
  312. }
  313. }
  314. });
  315. return sel;
  316. });
  317. }, [props]);
  318. const config = useDynamicJsonProperty(props.config, props.defaultConfig, defaultConfig);
  319. useEffect(() => {
  320. if (updateVarName && (refresh || !data[dataKey])) {
  321. const backCols = Object.values(config.columns).map((col) => col.dfid);
  322. const dtKey = backCols.join("-") + (config.decimators ? `--${config.decimators.join("")}` : "");
  323. setDataKey(dtKey);
  324. if (refresh || !data[dtKey]) {
  325. dispatch(
  326. createRequestChartUpdateAction(
  327. updateVarName,
  328. id,
  329. module,
  330. backCols,
  331. dtKey,
  332. getDecimatorsPayload(
  333. config.decimators,
  334. plotRef.current,
  335. config.modes,
  336. config.columns,
  337. config.traces,
  338. ),
  339. ),
  340. );
  341. }
  342. }
  343. // eslint-disable-next-line react-hooks/exhaustive-deps
  344. }, [refresh, dispatch, config.columns, config.traces, config.modes, config.decimators, updateVarName, id, module]);
  345. useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
  346. const layout = useMemo(() => {
  347. const layout = { ...baseLayout };
  348. let template = undefined;
  349. try {
  350. const tpl = props.template && JSON.parse(props.template);
  351. const tplTheme =
  352. theme.palette.mode === "dark"
  353. ? props.template_Dark_
  354. ? JSON.parse(props.template_Dark_)
  355. : darkThemeTemplate
  356. : props.template_Light_ && JSON.parse(props.template_Light_);
  357. template = tpl ? (tplTheme ? { ...tpl, ...tplTheme } : tpl) : tplTheme ? tplTheme : undefined;
  358. } catch (e) {
  359. console.info(`Error while parsing Chart.template\n${(e as Error).message || e}`);
  360. }
  361. if (template) {
  362. layout.template = template;
  363. }
  364. if (props.figure) {
  365. return {
  366. ...(props.figure[0].layout as Partial<Layout>),
  367. ...layout,
  368. title: title || layout.title || (props.figure[0].layout as Partial<Layout>).title,
  369. clickmode: "event+select",
  370. } as Layout;
  371. }
  372. return {
  373. ...layout,
  374. autosize: true,
  375. title: title || layout.title,
  376. xaxis: {
  377. title:
  378. config.traces.length && config.traces[0].length && config.traces[0][0]
  379. ? getColNameFromIndexed(config.columns[config.traces[0][0]]?.dfid)
  380. : undefined,
  381. ...layout.xaxis,
  382. },
  383. yaxis: {
  384. title:
  385. config.traces.length == 1 && config.traces[0].length > 1 && config.columns[config.traces[0][1]]
  386. ? getColNameFromIndexed(config.columns[config.traces[0][1]]?.dfid)
  387. : undefined,
  388. ...layout.yaxis,
  389. },
  390. clickmode: "event+select",
  391. } as Layout;
  392. }, [
  393. theme.palette.mode,
  394. title,
  395. config.columns,
  396. config.traces,
  397. baseLayout,
  398. props.template,
  399. props.template_Dark_,
  400. props.template_Light_,
  401. props.figure,
  402. ]);
  403. const style = useMemo(
  404. () =>
  405. height === undefined
  406. ? ({ ...defaultStyle, width: width } as CSSProperties)
  407. : ({ ...defaultStyle, width: width, height: height } as CSSProperties),
  408. [width, height],
  409. );
  410. const skelStyle = useMemo(() => ({ ...style, minHeight: "7em" }), [style]);
  411. const dataPl = useMemo(() => {
  412. if (props.figure) {
  413. return lastDataPl.current;
  414. }
  415. if (typeof data === "number" && lastDataPl.current) {
  416. return lastDataPl.current;
  417. }
  418. const datum = data[dataKey];
  419. lastDataPl.current = datum
  420. ? config.traces.map((trace, idx) => {
  421. const ret = {
  422. ...getArrayValue(config.options, idx, {}),
  423. type: config.types[idx],
  424. mode: config.modes[idx],
  425. name:
  426. getArrayValue(config.names, idx) ||
  427. (config.columns[trace[1]] ? getColNameFromIndexed(config.columns[trace[1]].dfid) : undefined),
  428. } as Record<string, unknown>;
  429. ret.marker = { ...getArrayValue(config.markers, idx, ret.marker || {}) };
  430. if (Object.keys(ret.marker as object).length) {
  431. MARKER_TO_COL.forEach((prop) => {
  432. const val = (ret.marker as Record<string, unknown>)[prop];
  433. if (typeof val === "string") {
  434. const arr = getValueFromCol(datum, val as string);
  435. if (arr.length) {
  436. (ret.marker as Record<string, unknown>)[prop] = arr;
  437. }
  438. }
  439. });
  440. } else {
  441. delete ret.marker;
  442. }
  443. const xs = getValue(datum, trace, 0) || [];
  444. const ys = getValue(datum, trace, 1) || [];
  445. const addIndex = getArrayValue(config.addIndex, idx, true) && !ys.length;
  446. const baseX = addIndex ? Array.from(Array(xs.length).keys()) : xs;
  447. const baseY = addIndex ? xs : ys;
  448. const axisNames = config.axisNames.length > idx ? config.axisNames[idx] : ([] as string[]);
  449. if (baseX.length) {
  450. if (axisNames.length > 0) {
  451. ret[axisNames[0]] = baseX;
  452. } else {
  453. ret.x = baseX;
  454. }
  455. }
  456. if (baseY.length) {
  457. if (axisNames.length > 1) {
  458. ret[axisNames[1]] = baseY;
  459. } else {
  460. ret.y = baseY;
  461. }
  462. }
  463. const baseZ = getValue(datum, trace, 2, true);
  464. if (baseZ) {
  465. if (axisNames.length > 2) {
  466. ret[axisNames[2]] = baseZ;
  467. } else {
  468. ret.z = baseZ;
  469. }
  470. }
  471. // Hack for treemap charts: create a fallback 'parents' column if needed
  472. // This works ONLY because 'parents' is the third named axis
  473. // (see __CHART_AXIS in gui/utils/chart_config_builder.py)
  474. else if (config.types[idx] === "treemap" && Array.isArray(ret.labels)) {
  475. ret.parents = Array(ret.labels.length).fill("");
  476. }
  477. // Other axis
  478. for (let i = 3; i < axisNames.length; i++) {
  479. ret[axisNames[i]] = getValue(datum, trace, i, true);
  480. }
  481. ret.text = getValue(datum, config.texts, idx, true);
  482. ret.xaxis = config.xaxis[idx];
  483. ret.yaxis = config.yaxis[idx];
  484. ret.hovertext = getValue(datum, config.labels, idx, true);
  485. const selPoints = getArrayValue(selected, idx, []);
  486. if (selPoints?.length) {
  487. ret.selectedpoints = selPoints;
  488. }
  489. ret.orientation = getArrayValue(config.orientations, idx);
  490. ret.line = getArrayValue(config.lines, idx);
  491. ret.textposition = getArrayValue(config.textAnchors, idx);
  492. const selectedMarker = getArrayValue(config.selectedMarkers, idx);
  493. if (selectedMarker) {
  494. ret.selected = { marker: selectedMarker };
  495. }
  496. return ret as Data;
  497. })
  498. : [];
  499. return lastDataPl.current;
  500. }, [props.figure, selected, data, config, dataKey]);
  501. const plotConfig = useMemo(() => {
  502. let plconf: Partial<Config> = {};
  503. if (props.plotConfig) {
  504. try {
  505. plconf = JSON.parse(props.plotConfig);
  506. } catch (e) {
  507. console.info(`Error while parsing Chart.plot_config\n${(e as Error).message || e}`);
  508. }
  509. if (typeof plconf !== "object" || plconf === null || Array.isArray(plconf)) {
  510. console.info("Error Chart.plot_config is not a dictionary");
  511. plconf = {};
  512. }
  513. }
  514. plconf.displaylogo = !!plconf.displaylogo;
  515. plconf.modeBarButtonsToAdd = TaipyPlotlyButtons;
  516. // plconf.responsive = true; // this is the source of the on/off height ...
  517. plconf.autosizable = true;
  518. if (!active) {
  519. plconf.staticPlot = true;
  520. }
  521. return plconf;
  522. }, [active, props.plotConfig]);
  523. const onRelayout = useCallback(
  524. (eventData: PlotRelayoutEvent) => {
  525. onRangeChange && dispatch(createSendActionNameAction(id, module, { action: onRangeChange, ...eventData }));
  526. if (config.decimators && !config.types.includes("scatter3d")) {
  527. const backCols = Object.values(config.columns).map((col) => col.dfid);
  528. const eventDataKey = Object.entries(eventData)
  529. .map(([k, v]) => `${k}=${v}`)
  530. .join("-");
  531. const dtKey =
  532. backCols.join("-") +
  533. (config.decimators ? `--${config.decimators.join("")}` : "") +
  534. "--" +
  535. eventDataKey;
  536. setDataKey(dtKey);
  537. dispatch(
  538. createRequestChartUpdateAction(
  539. updateVarName,
  540. id,
  541. module,
  542. backCols,
  543. dtKey,
  544. getDecimatorsPayload(
  545. config.decimators,
  546. plotRef.current,
  547. config.modes,
  548. config.columns,
  549. config.traces,
  550. eventData,
  551. ),
  552. ),
  553. );
  554. }
  555. },
  556. [
  557. dispatch,
  558. onRangeChange,
  559. id,
  560. config.modes,
  561. config.columns,
  562. config.traces,
  563. config.types,
  564. config.decimators,
  565. updateVarName,
  566. module,
  567. ],
  568. );
  569. const clickHandler = useCallback(
  570. (evt?: MouseEvent) => {
  571. const map =
  572. (evt?.currentTarget as PlotlyDiv)?._fullLayout?.map ||
  573. (evt?.currentTarget as PlotlyDiv)?._fullLayout?.geo ||
  574. (evt?.currentTarget as PlotlyDiv)?._fullLayout?.mapbox;
  575. const xaxis = map ? map._subplot?.xaxis : (evt?.currentTarget as PlotlyDiv)?._fullLayout?.xaxis;
  576. const yaxis = map ? map._subplot?.xaxis : (evt?.currentTarget as PlotlyDiv)?._fullLayout?.yaxis;
  577. if (!xaxis || !yaxis) {
  578. console.info("clickHandler: Plotly div does not have an xaxis object", evt);
  579. return;
  580. }
  581. const transform = (axis: Axis, delta: keyof DOMRect) => {
  582. const bb = (evt?.target as HTMLDivElement).getBoundingClientRect();
  583. return (pos?: number) => axis.p2d((pos || 0) - (bb[delta] as number));
  584. };
  585. dispatch(
  586. createSendActionNameAction(
  587. id,
  588. module,
  589. lightenPayload({
  590. action: onClick,
  591. lat: map ? yaxis.p2c() : undefined,
  592. y: map ? undefined : transform(yaxis, "top")(evt?.clientY),
  593. lon: map ? xaxis.p2c() : undefined,
  594. x: map ? undefined : transform(xaxis, "left")(evt?.clientX),
  595. }),
  596. ),
  597. );
  598. },
  599. [dispatch, module, id, onClick],
  600. );
  601. const onInitialized = useCallback(
  602. (figure: Readonly<Figure>, graphDiv: Readonly<HTMLElement>) => {
  603. onClick && graphDiv.addEventListener("click", clickHandler);
  604. },
  605. [onClick, clickHandler],
  606. );
  607. const getRealIndex = useCallback(
  608. (index?: number) =>
  609. typeof index === "number"
  610. ? props.figure
  611. ? index
  612. : data[dataKey].tp_index
  613. ? (data[dataKey].tp_index[index] as number)
  614. : index
  615. : 0,
  616. [data, dataKey, props.figure],
  617. );
  618. const onSelect = useCallback(
  619. (evt?: PlotSelectionEvent) => {
  620. if (updateVars) {
  621. const traces = (evt?.points || []).reduce((tr, pt) => {
  622. tr[pt.curveNumber] = tr[pt.curveNumber] || [];
  623. tr[pt.curveNumber].push(getRealIndex(getPlotIndex(pt)));
  624. return tr;
  625. }, [] as number[][]);
  626. if (config.traces.length === 0) {
  627. // figure
  628. const theVar = getUpdateVar(updateVars, "selected");
  629. theVar && dispatch(createSendUpdateAction(theVar, traces, module, props.onChange, propagate));
  630. return;
  631. }
  632. if (traces.length) {
  633. const upvars = traces.map((_, idx) => getUpdateVar(updateVars, `selected${idx}`));
  634. const setVars = new Set(upvars.filter((v) => v));
  635. if (traces.length > 1 && setVars.size === 1) {
  636. dispatch(
  637. createSendUpdateAction(
  638. setVars.values().next().value,
  639. traces,
  640. module,
  641. props.onChange,
  642. propagate,
  643. ),
  644. );
  645. return;
  646. }
  647. traces.forEach((tr, idx) => {
  648. if (upvars[idx] && tr && tr.length) {
  649. dispatch(createSendUpdateAction(upvars[idx], tr, module, props.onChange, propagate));
  650. }
  651. });
  652. } else if (config.traces.length === 1) {
  653. const upvar = getUpdateVar(updateVars, "selected0");
  654. if (upvar) {
  655. dispatch(createSendUpdateAction(upvar, [], module, props.onChange, propagate));
  656. }
  657. }
  658. }
  659. },
  660. [getRealIndex, dispatch, updateVars, propagate, props.onChange, config.traces.length, module],
  661. );
  662. return render ? (
  663. <Tooltip title={hover || ""}>
  664. <Box id={id} data-testid={props.testId} className={className} ref={plotRef}>
  665. <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle} />}>
  666. {Array.isArray(props.figure) && props.figure.length && props.figure[0].data !== undefined ? (
  667. <Plot
  668. data={props.figure[0].data as Data[]}
  669. layout={layout}
  670. style={style}
  671. onRelayout={onRelayout}
  672. onSelected={onSelect}
  673. onDeselect={onSelect}
  674. config={plotConfig}
  675. useResizeHandler
  676. onInitialized={onInitialized}
  677. />
  678. ) : (
  679. <Plot
  680. data={dataPl}
  681. layout={layout}
  682. style={style}
  683. onRelayout={onRelayout}
  684. onSelected={isOnClick(config.types) ? undefined : onSelect}
  685. onDeselect={isOnClick(config.types) ? undefined : onSelect}
  686. onClick={isOnClick(config.types) ? onSelect : undefined}
  687. config={plotConfig}
  688. useResizeHandler
  689. onInitialized={onInitialized}
  690. />
  691. )}
  692. </Suspense>
  693. </Box>
  694. </Tooltip>
  695. ) : null;
  696. };
  697. export default Chart;