Chart.tsx 28 KB

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