Chart.tsx 31 KB

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