DataNodeChart.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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, { useEffect, useState, useCallback, useMemo, MouseEvent, Fragment, ChangeEvent } from "react";
  14. import Add from "@mui/icons-material/Add";
  15. import BarChartOutlined from "@mui/icons-material/BarChartOutlined";
  16. import DeleteOutline from "@mui/icons-material/DeleteOutline";
  17. import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
  18. import TableChartOutlined from "@mui/icons-material/TableChartOutlined";
  19. import Box from "@mui/material/Box";
  20. import Button from "@mui/material/Button";
  21. import FormControl from "@mui/material/FormControl";
  22. import FormControlLabel from "@mui/material/FormControlLabel";
  23. import Grid from "@mui/material/Grid2";
  24. import IconButton from "@mui/material/IconButton";
  25. import InputLabel from "@mui/material/InputLabel";
  26. import OutlinedInput from "@mui/material/OutlinedInput";
  27. import ListItemText from "@mui/material/ListItemText";
  28. import MenuItem from "@mui/material/MenuItem";
  29. import Paper from "@mui/material/Paper";
  30. import Select, { SelectChangeEvent } from "@mui/material/Select";
  31. import Switch from "@mui/material/Switch";
  32. import ToggleButton from "@mui/material/ToggleButton";
  33. import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
  34. import Tooltip from "@mui/material/Tooltip";
  35. import { Chart, ColumnDesc, TraceValueType } from "taipy-gui";
  36. import { ChartViewType, MenuProps, TableViewType, tabularHeaderSx } from "./utils";
  37. interface DataNodeChartProps {
  38. active: boolean;
  39. configId?: string;
  40. tabularData?: Record<string, TraceValueType>;
  41. columns?: Record<string, ColumnDesc>;
  42. defaultConfig?: string;
  43. updateVarName?: string;
  44. uniqId: string;
  45. chartConfigs?: string;
  46. onViewTypeChange: (e: MouseEvent, value?: string) => void;
  47. }
  48. const TraceSx = { pl: 2 };
  49. const DefaultAxis = ["x", "y"];
  50. const chartTypes: Record<string, { name: string; addIndex: boolean; axisNames: string[]; [prop: string]: unknown }> = {
  51. scatter: { name: "Cartesian", addIndex: true, axisNames: DefaultAxis },
  52. pie: { name: "Pie", addIndex: false, axisNames: ["values", "labels"] },
  53. scatterpolargl: { name: "Polar", addIndex: true, axisNames: ["r", "theta"] },
  54. };
  55. const getChartTypeConfig = (...types: string[]) => {
  56. const ret: Record<string, Array<unknown>> = {};
  57. types.forEach((t, i) => {
  58. if (t && chartTypes[t]) {
  59. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  60. const { name, ...cfg } = chartTypes[t];
  61. Object.entries(cfg).forEach(([k, v]) => {
  62. ret[k] = ret[k] || Array(types.length);
  63. ret[k][i] = v;
  64. });
  65. }
  66. });
  67. return ret;
  68. };
  69. interface ChartConfig {
  70. traces?: Array<[string, string]>;
  71. types?: string[];
  72. columns?: Record<string, ColumnDesc>;
  73. options?: Array<Record<string, unknown>>;
  74. axisNames?: Array<string[]>;
  75. cumulative?: boolean;
  76. }
  77. const addCumulative = (config: ChartConfig) => {
  78. const types = config.types || [];
  79. const options: Array<Record<string, unknown>> = config.options || Array(types.length);
  80. types.forEach(
  81. (_, i) =>
  82. (options[i] = {
  83. ...(i < options.length ? options[i] || {} : {}),
  84. fill: i == 0 ? "tozeroy" : "tonexty",
  85. })
  86. );
  87. config.options = options;
  88. };
  89. const getAxisNames = (conf: ChartConfig, trace: number) =>
  90. conf?.axisNames && trace < conf?.axisNames.length
  91. ? conf.axisNames[trace] && conf.axisNames[trace].length
  92. ? conf.axisNames[trace]
  93. : DefaultAxis
  94. : DefaultAxis;
  95. interface ColSelectProps {
  96. labelId: string;
  97. label: string;
  98. trace: number;
  99. axis: number;
  100. traceConf: Array<[string, string]>;
  101. setColConf: (trace: number, axis: number, col: string) => void;
  102. columns: Array<[string, string]>;
  103. withNone?: boolean;
  104. }
  105. const ColSelect = (props: ColSelectProps) => {
  106. const { labelId, trace, axis, columns, label, setColConf, traceConf, withNone = false } = props;
  107. const [col, setCol] = useState("");
  108. const onColChange = useCallback(
  109. (e: SelectChangeEvent<string>) => {
  110. setCol(e.target.value);
  111. setColConf(trace, axis, e.target.value);
  112. },
  113. [trace, axis, setColConf]
  114. );
  115. useEffect(() => setCol(getTraceCol(traceConf, trace, axis)), [traceConf, trace, axis]);
  116. return (
  117. <FormControl>
  118. <InputLabel id={labelId}>{label}</InputLabel>
  119. <Select
  120. labelId={labelId}
  121. value={col}
  122. onChange={onColChange}
  123. input={<OutlinedInput label={label} />}
  124. MenuProps={MenuProps}
  125. >
  126. {withNone ? (
  127. <MenuItem value="">
  128. <ListItemText primary="- None -" />
  129. </MenuItem>
  130. ) : null}
  131. {columns.map(([dfid, k]) => (
  132. <MenuItem key={dfid} value={k}>
  133. <ListItemText primary={dfid} />
  134. </MenuItem>
  135. ))}
  136. </Select>
  137. </FormControl>
  138. );
  139. };
  140. interface TypeSelectProps {
  141. labelId: string;
  142. label: string;
  143. trace: number;
  144. setTypeConf: (trace: number, cType: string) => void;
  145. value: string;
  146. }
  147. const TypeSelect = (props: TypeSelectProps) => {
  148. const { labelId, trace, label, setTypeConf, value } = props;
  149. const [cType, setType] = useState("");
  150. const onTypeChange = useCallback(
  151. (e: SelectChangeEvent<string>) => {
  152. setType(e.target.value);
  153. setTypeConf(trace, e.target.value);
  154. },
  155. [trace, setTypeConf]
  156. );
  157. useEffect(() => setType(value), [value]);
  158. return (
  159. <FormControl>
  160. <InputLabel id={labelId}>{label}</InputLabel>
  161. <Select
  162. labelId={labelId}
  163. value={cType}
  164. onChange={onTypeChange}
  165. input={<OutlinedInput label={label} />}
  166. MenuProps={MenuProps}
  167. >
  168. {Object.entries(chartTypes).map(([k, v]) => (
  169. <MenuItem key={k} value={k}>
  170. <ListItemText primary={v.name} />
  171. </MenuItem>
  172. ))}
  173. </Select>
  174. </FormControl>
  175. );
  176. };
  177. const getTraceCol = (traceConf: Array<[string, string]>, trace: number, axis: number) => {
  178. return trace < traceConf.length ? traceConf[trace][axis] : "";
  179. };
  180. const storeConf = (configId?: string, config?: ChartConfig) => {
  181. localStorage && localStorage.setItem(`${configId}-chart-config`, JSON.stringify(config));
  182. return config;
  183. };
  184. const getBaseConfig = (defaultConfig?: string, chartConfigs?: string, configId?: string) => {
  185. if (defaultConfig) {
  186. try {
  187. const baseConfig = JSON.parse(defaultConfig) as ChartConfig;
  188. if (baseConfig) {
  189. if (configId && chartConfigs) {
  190. try {
  191. const conf: Record<string, ChartConfig> = JSON.parse(chartConfigs);
  192. if (conf[configId]) {
  193. const config = {
  194. ...baseConfig,
  195. ...getChartTypeConfig(...(conf[configId].types || [])),
  196. ...conf[configId],
  197. };
  198. config.cumulative && addCumulative(config);
  199. return config;
  200. }
  201. } catch (e) {
  202. console.warn(`chart_configs property is not a valid config.\n${e}`);
  203. }
  204. }
  205. return baseConfig;
  206. }
  207. } catch {
  208. // Do nothing
  209. }
  210. }
  211. return undefined;
  212. };
  213. const DataNodeChart = (props: DataNodeChartProps) => {
  214. const { defaultConfig = "", uniqId, configId, chartConfigs = "", onViewTypeChange } = props;
  215. const [config, setConfig] = useState<ChartConfig | undefined>(undefined);
  216. useEffect(() => {
  217. let localConf: ChartConfig | undefined = undefined;
  218. const localItem = localStorage && localStorage.getItem(`${configId}-chart-config`);
  219. if (localItem) {
  220. try {
  221. localConf = JSON.parse(localItem);
  222. } catch {
  223. // do nothing
  224. }
  225. }
  226. const conf = getBaseConfig(defaultConfig, chartConfigs, configId);
  227. if (localConf && localConf.traces) {
  228. if (
  229. conf &&
  230. conf.columns &&
  231. localConf.traces.some((tr) => tr.some((id) => (conf.columns || {})[id] === undefined))
  232. ) {
  233. setConfig(conf);
  234. } else {
  235. localConf.cumulative && addCumulative(localConf);
  236. setConfig(localConf);
  237. }
  238. } else if (conf) {
  239. setConfig(conf);
  240. }
  241. }, [defaultConfig, configId, chartConfigs]);
  242. const resetConfig = useCallback(() => {
  243. localStorage && localStorage.removeItem(`${configId}-chart-config`);
  244. const conf = getBaseConfig(defaultConfig, chartConfigs, configId);
  245. if (conf) {
  246. setConfig(conf);
  247. }
  248. }, [defaultConfig, chartConfigs, configId]);
  249. const columns: Array<[string, string]> = useMemo(
  250. () => Object.entries(props.columns || {}).map(([k, c]) => [c.dfid, k]),
  251. [props.columns]
  252. );
  253. const setTypeChange = useCallback(
  254. (trace: number, cType: string) =>
  255. setConfig((cfg) => {
  256. if (!cfg) {
  257. return cfg;
  258. }
  259. const nts = (cfg.types || []).map((ct, i) => (i == trace ? cType : ct));
  260. return storeConf(configId, { ...cfg, types: nts, ...getChartTypeConfig(...nts) });
  261. }),
  262. [configId]
  263. );
  264. const setColConf = useCallback(
  265. (trace: number, axis: number, col: string) =>
  266. setConfig((cfg) =>
  267. cfg
  268. ? storeConf(configId, {
  269. ...cfg,
  270. traces: (cfg.traces || []).map((axes, idx) =>
  271. idx == trace ? (axes.map((a, j) => (j == axis ? col : a)) as [string, string]) : axes
  272. ),
  273. })
  274. : cfg
  275. ),
  276. [configId]
  277. );
  278. const onAddTrace = useCallback(
  279. () =>
  280. setConfig((cfg) => {
  281. if (!cfg || !columns || !columns.length) {
  282. return cfg;
  283. }
  284. const nt = cfg.types?.length ? cfg.types[0] : "scatter";
  285. const nts = [...(cfg.types || []), nt];
  286. const conf: ChartConfig = {
  287. ...cfg,
  288. types: nts,
  289. traces: [...(cfg.traces || []), [columns[0][1], columns[columns.length > 1 ? 1 : 0][1]]],
  290. ...getChartTypeConfig(...nts),
  291. };
  292. cfg.cumulative && addCumulative(conf);
  293. return storeConf(configId, conf);
  294. }),
  295. [columns, configId]
  296. );
  297. const onRemoveTrace = useCallback(
  298. (e: MouseEvent<HTMLElement>) => {
  299. const { idx } = e.currentTarget.dataset;
  300. const i = Number(idx);
  301. setConfig((cfg) => {
  302. if (!cfg || !cfg.traces || isNaN(i) || i >= cfg.traces.length) {
  303. return cfg;
  304. }
  305. const nts = (cfg.types || []).filter((c, j) => j != i);
  306. return storeConf(configId, {
  307. ...cfg,
  308. types: nts,
  309. traces: (cfg.traces || []).filter((t, j) => j != i),
  310. ...getChartTypeConfig(...nts),
  311. });
  312. });
  313. },
  314. [configId]
  315. );
  316. const onCumulativeChange = useCallback(
  317. (e: ChangeEvent<HTMLInputElement>, check: boolean) => {
  318. setConfig((cfg) => {
  319. if (!cfg || !cfg.types) {
  320. return cfg;
  321. }
  322. cfg.cumulative = check;
  323. if (check) {
  324. addCumulative(cfg);
  325. } else {
  326. cfg.options?.forEach((o) => delete o.fill);
  327. }
  328. return storeConf(configId, { ...cfg });
  329. });
  330. },
  331. [configId]
  332. );
  333. return (
  334. <>
  335. <Grid container sx={tabularHeaderSx}>
  336. <Grid>
  337. <Box className="taipy-toggle">
  338. <ToggleButtonGroup onChange={onViewTypeChange} exclusive value={ChartViewType} color="primary">
  339. <ToggleButton value={TableViewType}>
  340. <TableChartOutlined />
  341. </ToggleButton>
  342. <ToggleButton value={ChartViewType}>
  343. <BarChartOutlined />
  344. </ToggleButton>
  345. </ToggleButtonGroup>
  346. </Box>
  347. </Grid>
  348. <Grid>
  349. <FormControlLabel
  350. control={
  351. <Switch checked={!!config?.cumulative} onChange={onCumulativeChange} color="primary" />
  352. }
  353. label="Cumulative"
  354. />
  355. </Grid>
  356. <Grid>
  357. <Button
  358. onClick={resetConfig}
  359. variant="text"
  360. color="primary"
  361. className="taipy-button"
  362. startIcon={<RefreshOutlined />}
  363. >
  364. Reset View
  365. </Button>
  366. </Grid>
  367. </Grid>
  368. <Paper>
  369. <Grid container alignItems="center">
  370. {config?.traces && config?.types
  371. ? config?.traces.map((tc, idx) => {
  372. const baseLabelId = `${uniqId}-trace${idx}-"`;
  373. return (
  374. <Fragment key={idx}>
  375. <Grid size={2} sx={TraceSx}>
  376. Trace {idx + 1}
  377. </Grid>
  378. <Grid size={3}>
  379. <TypeSelect
  380. trace={idx}
  381. label="Category"
  382. labelId={baseLabelId + "config"}
  383. setTypeConf={setTypeChange}
  384. value={config.types ? config.types[idx] : ""}
  385. />
  386. </Grid>
  387. <Grid size={3}>
  388. <ColSelect
  389. trace={idx}
  390. axis={0}
  391. traceConf={config.traces || []}
  392. label={getAxisNames(config, idx)[0]}
  393. labelId={baseLabelId + "x"}
  394. columns={columns}
  395. setColConf={setColConf}
  396. />{" "}
  397. </Grid>
  398. <Grid size={3}>
  399. <ColSelect
  400. trace={idx}
  401. axis={1}
  402. traceConf={config.traces || []}
  403. label={getAxisNames(config, idx)[1]}
  404. labelId={baseLabelId + "y"}
  405. columns={columns}
  406. setColConf={setColConf}
  407. withNone
  408. />
  409. </Grid>
  410. <Grid size={1}>
  411. {config.traces && config.traces.length > 1 ? (
  412. <Tooltip title="Remove Trace">
  413. <IconButton onClick={onRemoveTrace} data-idx={idx}>
  414. <DeleteOutline color="primary" />
  415. </IconButton>
  416. </Tooltip>
  417. ) : null}
  418. </Grid>
  419. </Fragment>
  420. );
  421. })
  422. : null}
  423. <Grid size={12}>
  424. <Button onClick={onAddTrace} startIcon={<Add color="primary" />}>
  425. Add trace
  426. </Button>
  427. </Grid>
  428. </Grid>
  429. </Paper>
  430. <Chart
  431. active={props.active}
  432. defaultConfig={config ? JSON.stringify(config) : defaultConfig}
  433. updateVarName={props.updateVarName}
  434. data={props.tabularData}
  435. libClassName="taipy-chart"
  436. />
  437. </>
  438. );
  439. };
  440. export default DataNodeChart;