diff --git a/frontend/taipy-gui/src/components/Taipy/Chart.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Chart.spec.tsx index 0b333f655..2705cfa79 100644 --- a/frontend/taipy-gui/src/components/Taipy/Chart.spec.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Chart.spec.tsx @@ -60,7 +60,7 @@ const chartValue = { }, }; const chartConfig = JSON.stringify({ - columns: { Day_str: { dfid: "Day" }, "Daily hospital occupancy": { dfid: "Daily hospital occupancy" } }, + columns: [{ Day_str: { dfid: "Day" }, "Daily hospital occupancy": { dfid: "Daily hospital occupancy" } }], traces: [["Day_str", "Daily hospital occupancy"]], xaxis: ["x"], yaxis: ["y"], @@ -86,7 +86,7 @@ const mapValue = { }, }; const mapConfig = JSON.stringify({ - columns: { Lat: { dfid: "Lat" }, Lon: { dfid: "Lon" } }, + columns: [{ Lat: { dfid: "Lat" }, Lon: { dfid: "Lon" } }], traces: [["Lat", "Lon"]], xaxis: ["x"], yaxis: ["y"], @@ -173,7 +173,7 @@ describe("Chart Component", () => { payload: { id: "chart", names: ["varName"], refresh: false }, type: "REQUEST_UPDATE", }); - expect(dispatch).toHaveBeenCalledWith({ + await waitFor(() => expect(dispatch).toHaveBeenCalledWith({ name: "data_var", payload: { alldata: true, @@ -183,7 +183,7 @@ describe("Chart Component", () => { id: "chart", }, type: "REQUEST_DATA_UPDATE", - }); + })); }); it("dispatch a well formed message on selection", async () => { const dispatch = jest.fn(); diff --git a/frontend/taipy-gui/src/components/Taipy/Chart.tsx b/frontend/taipy-gui/src/components/Taipy/Chart.tsx index bdcc976fd..3c01c9de4 100644 --- a/frontend/taipy-gui/src/components/Taipy/Chart.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Chart.tsx @@ -60,6 +60,7 @@ interface ChartProp extends TaipyActiveProps, TaipyChangeProps { defaultConfig: string; config?: string; data?: Record; + //data${number}?: Record; defaultLayout?: string; layout?: string; plotConfig?: string; @@ -69,13 +70,14 @@ interface ChartProp extends TaipyActiveProps, TaipyChangeProps { template?: string; template_Dark_?: string; template_Light_?: string; - //[key: `selected_${number}`]: number[]; + //[key: `selected${number}`]: number[]; figure?: Array>; onClick?: string; + dataVarNames?: string; } interface ChartConfig { - columns: Record; + columns: Array>; labels: string[]; modes: string[]; types: string[]; @@ -217,7 +219,7 @@ export const getPlotIndex = (pt: PlotDatum) => : pt.pointIndex; const defaultConfig = { - columns: {} as Record, + columns: [] as Array>, labels: [], modes: [], types: [], @@ -285,6 +287,15 @@ const getDataKey = (columns?: Record, decimators?: string[]) return [backCols, backCols.join("-") + (decimators ? `--${decimators.join("")}` : "")]; }; +const isDataRefresh = (data?: Record) => data?.__taipy_refresh !== undefined; +const getDataVarName = (updateVarName: string | undefined, dataVarNames: string[], idx: number) => + idx === 0 ? updateVarName : dataVarNames[idx - 1]; +const getData = ( + data: Record, + additionalDatas: Array>, + idx: number +) => (idx === 0 ? data : (idx <= additionalDatas.length ? additionalDatas[idx - 1]: undefined)); + const Chart = (props: ChartProp) => { const { title = "", @@ -301,18 +312,36 @@ const Chart = (props: ChartProp) => { const dispatch = useDispatch(); const [selected, setSelected] = useState([]); const plotRef = useRef(null); - const [dataKey, setDataKey] = useState("__default__"); + const [dataKeys, setDataKeys] = useState([]); const lastDataPl = useRef([]); const theme = useTheme(); const module = useModule(); - const refresh = useMemo(() => (data?.__taipy_refresh !== undefined ? nanoid() : false), [data]); const className = useClassNames(props.libClassName, props.dynamicClassName, props.className); const active = useDynamicProperty(props.active, props.defaultActive, true); const render = useDynamicProperty(props.render, props.defaultRender, true); const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined); const baseLayout = useDynamicJsonProperty(props.layout, props.defaultLayout || "", emptyLayout); + const dataVarNames = useMemo(() => (props.dataVarNames ? props.dataVarNames.split(";") : []), [props.dataVarNames]); + const oldAdditionalDatas = useRef>>([]); + const additionalDatas = useMemo(() => { + const newAdditionalDatas = dataVarNames.map( + (_, idx) => (props as unknown as Record>)[`data${idx + 1}`] + ); + if (newAdditionalDatas.length !== oldAdditionalDatas.current.length) { + oldAdditionalDatas.current = newAdditionalDatas; + } else if (!newAdditionalDatas.every((d, idx) => d === oldAdditionalDatas.current[idx])) { + oldAdditionalDatas.current = newAdditionalDatas; + } + return oldAdditionalDatas.current; + }, [dataVarNames, props]); + + const refresh = useMemo( + () => (isDataRefresh(data) || additionalDatas.some((d) => isDataRefresh(d)) ? nanoid() : false), + [data, additionalDatas] + ); + // get props.selected[i] values useEffect(() => { if (props.figure) { @@ -353,30 +382,53 @@ const Chart = (props: ChartProp) => { const config = useDynamicJsonProperty(props.config, props.defaultConfig, defaultConfig); useEffect(() => { - if (updateVarName) { - const [backCols, dtKey] = getDataKey(config.columns, config.decimators); - setDataKey(dtKey); - if (refresh || !data[dtKey]) { - dispatch( - createRequestChartUpdateAction( - updateVarName, - id, - module, - backCols, - dtKey, - getDecimatorsPayload( - config.decimators, - plotRef.current, - config.modes, - config.columns, - config.traces - ) - ) - ); - } - } + setDataKeys((oldDtKeys) => { + let changed = false; + const newDtKeys = (config.columns || []).map((columns, idx) => { + const varName = getDataVarName(updateVarName, dataVarNames, idx); + if (varName) { + const [backCols, dtKey] = getDataKey(columns, config.decimators); + changed = changed || idx > oldDtKeys.length || oldDtKeys[idx] !== dtKey; + const lData = getData(data, additionalDatas, idx); + if (lData === undefined || isDataRefresh(lData) || !lData[dtKey]) { + Promise.resolve().then(() => + dispatch( + createRequestChartUpdateAction( + varName, + id, + module, + backCols, + dtKey, + getDecimatorsPayload( + config.decimators, + plotRef.current, + config.modes, + columns, + config.traces + ) + ) + ) + ); + } + return dtKey; + } + return ""; + }); + return changed ? newDtKeys : oldDtKeys; + }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [refresh, dispatch, config.columns, config.traces, config.modes, config.decimators, updateVarName, id, module]); + }, [ + refresh, + dispatch, + config.columns, + config.traces, + config.modes, + config.decimators, + updateVarName, + dataVarNames, + id, + module, + ]); useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars); @@ -411,14 +463,14 @@ const Chart = (props: ChartProp) => { xaxis: { title: config.traces.length && config.traces[0].length && config.traces[0][0] - ? getColNameFromIndexed(config.columns[config.traces[0][0]]?.dfid) + ? getColNameFromIndexed(config.columns[0][config.traces[0][0]]?.dfid) : undefined, ...layout.xaxis, }, yaxis: { title: - config.traces.length == 1 && config.traces[0].length > 1 && config.columns[config.traces[0][1]] - ? getColNameFromIndexed(config.columns[config.traces[0][1]]?.dfid) + config.traces.length == 1 && config.traces[0].length > 1 && config.columns[0][config.traces[0][1]] + ? getColNameFromIndexed(config.columns[0][config.traces[0][1]]?.dfid) : undefined, ...layout.yaxis, }, @@ -447,98 +499,112 @@ const Chart = (props: ChartProp) => { const dataPl = useMemo(() => { if (props.figure) { - return lastDataPl.current; - } - if (data.__taipy_refresh !== undefined) { return lastDataPl.current || []; } - const dtKey = getDataKey(config.columns, config.decimators)[1]; - if (!dataKey.startsWith(dtKey)) { + const datas = dataKeys.map((_, idx) => getData(data, additionalDatas, idx)); + if (!datas.length || datas.every((d) => !d || isDataRefresh(d) || !Object.keys(d).length)) { return lastDataPl.current || []; } - const datum = data[dataKey]; - lastDataPl.current = datum - ? config.traces.map((trace, idx) => { - const ret = { - ...getArrayValue(config.options, idx, {}), - type: config.types[idx], - mode: config.modes[idx], - name: - getArrayValue(config.names, idx) || - (config.columns[trace[1]] ? getColNameFromIndexed(config.columns[trace[1]].dfid) : undefined), - } as Record; - ret.marker = { ...getArrayValue(config.markers, idx, ret.marker || {}) }; - if (Object.keys(ret.marker as object).length) { - MARKER_TO_COL.forEach((prop) => { - const val = (ret.marker as Record)[prop]; - if (typeof val === "string") { - const arr = getValueFromCol(datum, val as string); - if (arr.length) { - (ret.marker as Record)[prop] = arr; - } - } - }); - } else { - delete ret.marker; - } - const xs = getValue(datum, trace, 0) || []; - const ys = getValue(datum, trace, 1) || []; - const addIndex = getArrayValue(config.addIndex, idx, true) && !ys.length; - const baseX = addIndex ? Array.from(Array(xs.length).keys()) : xs; - const baseY = addIndex ? xs : ys; - const axisNames = config.axisNames.length > idx ? config.axisNames[idx] : ([] as string[]); - if (baseX.length) { - if (axisNames.length > 0) { - ret[axisNames[0]] = baseX; - } else { - ret.x = baseX; - } - } - if (baseY.length) { - if (axisNames.length > 1) { - ret[axisNames[1]] = baseY; - } else { - ret.y = baseY; - } - } - const baseZ = getValue(datum, trace, 2, true); - if (baseZ) { - if (axisNames.length > 2) { - ret[axisNames[2]] = baseZ; - } else { - ret.z = baseZ; - } - } - // Hack for treemap charts: create a fallback 'parents' column if needed - // This works ONLY because 'parents' is the third named axis - // (see __CHART_AXIS in gui/utils/chart_config_builder.py) - else if (config.types[idx] === "treemap" && Array.isArray(ret.labels)) { - ret.parents = Array(ret.labels.length).fill(""); - } - // Other axis - for (let i = 3; i < axisNames.length; i++) { - ret[axisNames[i]] = getValue(datum, trace, i, true); - } - ret.text = getValue(datum, config.texts, idx, true); - ret.xaxis = config.xaxis[idx]; - ret.yaxis = config.yaxis[idx]; - ret.hovertext = getValue(datum, config.labels, idx, true); - const selPoints = getArrayValue(selected, idx, []); - if (selPoints?.length) { - ret.selectedpoints = selPoints; - } - ret.orientation = getArrayValue(config.orientations, idx); - ret.line = getArrayValue(config.lines, idx); - ret.textposition = getArrayValue(config.textAnchors, idx); - const selectedMarker = getArrayValue(config.selectedMarkers, idx); - if (selectedMarker) { - ret.selected = { marker: selectedMarker }; - } - return ret as Data; - }) - : lastDataPl.current || []; + let changed = false; + const newDataPl = config.traces.map((trace, idx) => { + const currentData = idx < lastDataPl.current.length ? lastDataPl.current[idx] : {}; + const dataKey = idx < dataKeys.length ? dataKeys[idx] : dataKeys[0]; + const lData = idx < datas.length ? datas[idx] : datas[0]; + if (!lData || isDataRefresh(lData) || !Object.keys(lData).length) { + return currentData; + } + const dtKey = getDataKey( + idx < config.columns?.length ? config.columns[idx] : undefined, + config.decimators + )[1]; + if (!dataKey.startsWith(dtKey) || !dtKey.length) { + return currentData; + } + changed = true; + const datum = lData[dataKey]; + const columns = config.columns[idx]; + const ret = { + ...getArrayValue(config.options, idx, {}), + type: config.types[idx], + mode: config.modes[idx], + name: + getArrayValue(config.names, idx) || + (columns[trace[1]] ? getColNameFromIndexed(columns[trace[1]].dfid) : undefined), + } as Record; + ret.marker = { ...getArrayValue(config.markers, idx, ret.marker || {}) }; + if (Object.keys(ret.marker as object).length) { + MARKER_TO_COL.forEach((prop) => { + const val = (ret.marker as Record)[prop]; + if (typeof val === "string") { + const arr = getValueFromCol(datum, val as string); + if (arr.length) { + (ret.marker as Record)[prop] = arr; + } + } + }); + } else { + delete ret.marker; + } + const xs = getValue(datum, trace, 0) || []; + const ys = getValue(datum, trace, 1) || []; + const addIndex = getArrayValue(config.addIndex, idx, true) && !ys.length; + const baseX = addIndex ? Array.from(Array(xs.length).keys()) : xs; + const baseY = addIndex ? xs : ys; + const axisNames = config.axisNames.length > idx ? config.axisNames[idx] : ([] as string[]); + if (baseX.length) { + if (axisNames.length > 0) { + ret[axisNames[0]] = baseX; + } else { + ret.x = baseX; + } + } + if (baseY.length) { + if (axisNames.length > 1) { + ret[axisNames[1]] = baseY; + } else { + ret.y = baseY; + } + } + const baseZ = getValue(datum, trace, 2, true); + if (baseZ) { + if (axisNames.length > 2) { + ret[axisNames[2]] = baseZ; + } else { + ret.z = baseZ; + } + } + // Hack for treemap charts: create a fallback 'parents' column if needed + // This works ONLY because 'parents' is the third named axis + // (see __CHART_AXIS in gui/utils/chart_config_builder.py) + else if (config.types[idx] === "treemap" && Array.isArray(ret.labels)) { + ret.parents = Array(ret.labels.length).fill(""); + } + // Other axis + for (let i = 3; i < axisNames.length; i++) { + ret[axisNames[i]] = getValue(datum, trace, i, true); + } + ret.text = getValue(datum, config.texts, idx, true); + ret.xaxis = config.xaxis[idx]; + ret.yaxis = config.yaxis[idx]; + ret.hovertext = getValue(datum, config.labels, idx, true); + const selPoints = getArrayValue(selected, idx, []); + if (selPoints?.length) { + ret.selectedpoints = selPoints; + } + ret.orientation = getArrayValue(config.orientations, idx); + ret.line = getArrayValue(config.lines, idx); + ret.textposition = getArrayValue(config.textAnchors, idx); + const selectedMarker = getArrayValue(config.selectedMarkers, idx); + if (selectedMarker) { + ret.selected = { marker: selectedMarker }; + } + return ret as Data; + }); + if (changed) { + lastDataPl.current = newDataPl; + } return lastDataPl.current; - }, [props.figure, selected, data, config, dataKey]); + }, [props.figure, selected, data, additionalDatas, config, dataKeys]); const plotConfig = useMemo(() => { let plConf: Partial = {}; @@ -567,28 +633,41 @@ const Chart = (props: ChartProp) => { (eventData: PlotRelayoutEvent) => { onRangeChange && dispatch(createSendActionNameAction(id, module, { action: onRangeChange, ...eventData })); if (config.decimators && !config.types.includes("scatter3d")) { - const [backCols, dtKeyBase] = getDataKey(config.columns, config.decimators); + const [backCols, dtKeyBase] = getDataKey( + config.columns?.length ? config.columns[0] : undefined, + config.decimators + ); const dtKey = `${dtKeyBase}--${Object.entries(eventData) .map(([k, v]) => `${k}=${v}`) .join("-")}`; - setDataKey(dtKey); - dispatch( - createRequestChartUpdateAction( - updateVarName, - id, - module, - backCols, - dtKey, - getDecimatorsPayload( - config.decimators, - plotRef.current, - config.modes, - config.columns, - config.traces, - eventData - ) - ) - ); + setDataKeys((oldDataKeys) => { + if (oldDataKeys.length === 0) { + return [dtKey]; + } + if (oldDataKeys[0] !== dtKey) { + Promise.resolve().then(() => + dispatch( + createRequestChartUpdateAction( + updateVarName, + id, + module, + backCols, + dtKey, + getDecimatorsPayload( + config.decimators, + plotRef.current, + config.modes, + config.columns?.length ? config.columns[0] : {}, + config.traces, + eventData + ) + ) + ) + ); + return [dtKey, ...oldDataKeys.slice(1)]; + } + return oldDataKeys; + }); } }, [ @@ -646,15 +725,21 @@ const Chart = (props: ChartProp) => { ); const getRealIndex = useCallback( - (index?: number) => - typeof index === "number" + (dataIdx: number, index?: number) => { + const lData = getData(data, additionalDatas, dataIdx); + if (!lData) { + return index || 0; + } + const dtKey = dataKeys[dataIdx]; + return typeof index === "number" ? props.figure ? index - : data[dataKey].tp_index - ? (data[dataKey].tp_index[index] as number) + : lData[dtKey].tp_index + ? (lData[dtKey].tp_index[index] as number) : index - : 0, - [data, dataKey, props.figure] + : 0; + }, + [data, additionalDatas, dataKeys, props.figure] ); const onSelect = useCallback( @@ -662,7 +747,7 @@ const Chart = (props: ChartProp) => { if (updateVars) { const traces = (evt?.points || []).reduce((tr, pt) => { tr[pt.curveNumber] = tr[pt.curveNumber] || []; - tr[pt.curveNumber].push(getRealIndex(getPlotIndex(pt))); + tr[pt.curveNumber].push(getRealIndex(pt.curveNumber, getPlotIndex(pt))); return tr; }, [] as number[][]); if (config.traces.length === 0) { diff --git a/taipy/gui/_renderers/builder.py b/taipy/gui/_renderers/builder.py index 02eea87fa..ba9d5ee95 100644 --- a/taipy/gui/_renderers/builder.py +++ b/taipy/gui/_renderers/builder.py @@ -610,7 +610,7 @@ def _get_chart_config(self, default_type: str, default_mode: str): self.__attributes["_default_mode"] = default_mode rebuild_fn_hash = self.__build_rebuild_fn( self.__gui._get_call_method_name("_chart_conf"), - _CHART_NAMES + ("_default_type", "_default_mode", "data"), + _CHART_NAMES + ("_default_type", "_default_mode"), ) if rebuild_fn_hash: self.__set_react_attribute("config", rebuild_fn_hash) @@ -618,7 +618,23 @@ def _get_chart_config(self, default_type: str, default_mode: str): # read column definitions data = self.__attributes.get("data") data_hash = self.__hashes.get("data", "") - col_types = self.__gui._get_accessor().get_col_types(data_hash, _TaipyData(data, data_hash)) + col_types = [self.__gui._get_accessor().get_col_types(data_hash, _TaipyData(data, data_hash))] + + if data_hash: + data_updates: t.List[str] = [] + data_idx = 1 + name_idx = f"data[{data_idx}]" + while add_data_hash := self.__hashes.get(name_idx): + typed_hash = self.__get_typed_hash_name(add_data_hash, _TaipyData) + data_updates.append(typed_hash) + self.__set_react_attribute(f"data{data_idx}",_get_client_var_name(typed_hash)) + add_data = self.__attributes.get(name_idx) + data_idx += 1 + name_idx = f"data[{data_idx}]" + col_types.append( + self.__gui._get_accessor().get_col_types(add_data_hash, _TaipyData(add_data, add_data_hash)) + ) + self.set_attribute("dataVarNames", ";".join(data_updates)) config = _build_chart_config(self.__gui, self.__attributes, col_types) diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py index e2d57fc0f..eff45ecb0 100644 --- a/taipy/gui/gui.py +++ b/taipy/gui/gui.py @@ -1903,11 +1903,18 @@ def _chart_conf( rebuild = rebuild_val if rebuild_val is not None else rebuild if rebuild: attributes, hashes = self.__get_attributes(attr_json, hash_json, kwargs) - data_hash = hashes.get("data", "") + idx = 0 + data_hashes = [] + while data_hash := hashes.get("data" if idx == 0 else f"data[{idx}]", ""): + data_hashes.append(data_hash) + idx += 1 config = _build_chart_config( self, attributes, - self._get_accessor().get_col_types(data_hash, _TaipyData(kwargs.get(data_hash), data_hash)), + [ + self._get_accessor().get_col_types(data_hash, _TaipyData(kwargs.get(data_hash), data_hash)) + for data_hash in data_hashes + ], ) return json.dumps(config, cls=_TaipyJsonEncoder) diff --git a/taipy/gui/utils/chart_config_builder.py b/taipy/gui/utils/chart_config_builder.py index 6206cb93f..4ce9c68e6 100644 --- a/taipy/gui/utils/chart_config_builder.py +++ b/taipy/gui/utils/chart_config_builder.py @@ -112,7 +112,7 @@ def __get_col_from_indexed(col_name: str, idx: int) -> t.Optional[str]: return col_name -def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t.Dict[str, str]): # noqa: C901 +def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types_list: t.List[t.Dict[str, str]]): # noqa: C901 if "data" not in attributes and "figure" in attributes: return {"traces": []} default_type = attributes.get("_default_type", "scatter") @@ -167,32 +167,47 @@ def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t # axis names axis.append(__CHART_AXIS.get(trace[_Chart_iprops.type.value] or "", __CHART_DEFAULT_AXIS)) + idx = 1 + while f"data[{idx}]" in attributes: + if idx >= len(traces): + traces.append(list(traces[0])) + axis.append(__CHART_AXIS.get(traces[0][_Chart_iprops.type.value] or "", __CHART_DEFAULT_AXIS)) + idx += 1 + # list of data columns name indexes with label text dt_idx = tuple(e.value for e in (axis[0] + (_Chart_iprops.label, _Chart_iprops.text))) # configure columns - columns: t.Set[str] = set() - for j, trace in enumerate(traces): + columns: t.List[t.Set[str]] = [set()] * len(traces) + for idx, trace in enumerate(traces): dt_idx = tuple( - e.value for e in (axis[j] if j < len(axis) else axis[0]) + (_Chart_iprops.label, _Chart_iprops.text) + e.value for e in (axis[idx] if idx < len(axis) else axis[0]) + (_Chart_iprops.label, _Chart_iprops.text) ) - columns.update([trace[i] or "" for i in dt_idx if trace[i]]) + columns[idx].update([trace[i] or "" for i in dt_idx if trace[i]]) # add optional column if any markers = [ t[_Chart_iprops.marker.value] or ({"color": t[_Chart_iprops.color.value]} if t[_Chart_iprops.color.value] else None) for t in traces ] - opt_cols = set() - for m in markers: + opt_cols: t.List[t.Set[str]] = [set()] * len(traces) + for idx, m in enumerate(markers): if isinstance(m, (dict, _MapDict)): for prop1 in __CHART_MARKER_TO_COLS: val = m.get(prop1) - if isinstance(val, str) and val not in columns: - opt_cols.add(val) + if isinstance(val, str) and val not in columns[idx]: + opt_cols[idx].add(val) # Validate the column names - col_dict = _get_columns_dict(attributes.get("data"), list(columns), col_types, opt_columns=opt_cols) + col_dicts = [] + for idx, col_types in enumerate(col_types_list): + if add_col_dict := _get_columns_dict( + attributes.get("data" if idx == 0 else f"data[{idx}]"), + list(columns[idx] if idx < len(columns) else columns[0]), + col_types, + opt_columns=opt_cols[idx] if idx < len(opt_cols) else opt_cols[0], + ): + col_dicts.append(add_col_dict) # Manage Decimator decimators: t.List[t.Optional[str]] = [] @@ -208,7 +223,14 @@ def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t # set default columns if not defined icols = [ - [c2 for c2 in [__get_col_from_indexed(c1, i) for c1 in t.cast(dict, col_dict).keys()] if c2] + [ + c2 + for c2 in [ + __get_col_from_indexed(c1, i) + for c1 in t.cast(dict, col_dicts[i] if i < len(col_dicts) else col_dicts[0]).keys() + ] + if c2 + ] for i in range(len(traces)) ] @@ -222,21 +244,24 @@ def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t for j, v in enumerate(tr) ] - if col_dict is not None: - reverse_cols = {str(cd.get("dfid")): c for c, cd in col_dict.items()} + if col_dicts: + reverse_cols = [{str(cd.get("dfid")): c for c, cd in col_dict.items()} for col_dict in col_dicts] + for idx in range(len(traces)): + if idx < len(reverse_cols): + reverse_cols.append(reverse_cols[0]) # List used axis used_axis = [[e for e in (axis[j] if j < len(axis) else axis[0]) if tr[e.value]] for j, tr in enumerate(traces)] ret_dict = { - "columns": col_dict, + "columns": col_dicts, "labels": [ - reverse_cols.get(tr[_Chart_iprops.label.value] or "", (tr[_Chart_iprops.label.value] or "")) - for tr in traces + reverse_cols[idx].get(tr[_Chart_iprops.label.value] or "", (tr[_Chart_iprops.label.value] or "")) + for idx, tr in enumerate(traces) ], "texts": [ - reverse_cols.get(tr[_Chart_iprops.text.value] or "", (tr[_Chart_iprops.text.value] or None)) - for tr in traces + reverse_cols[idx].get(tr[_Chart_iprops.text.value] or "", (tr[_Chart_iprops.text.value] or None)) + for idx, tr in enumerate(traces) ], "modes": [tr[_Chart_iprops.mode.value] for tr in traces], "types": [tr[_Chart_iprops.type.value] for tr in traces], @@ -253,8 +278,8 @@ def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t for tr in traces ], "traces": [ - [reverse_cols.get(c or "", c) for c in [tr[e.value] for e in used_axis[j]]] - for j, tr in enumerate(traces) + [reverse_cols[idx].get(c or "", c) for c in [tr[e.value] for e in used_axis[idx]]] + for idx, tr in enumerate(traces) ], "orientations": [tr[_Chart_iprops.orientation.value] for tr in traces], "names": [tr[_Chart_iprops._name.value] for tr in traces], diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index bb14fded3..6636a3caf 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -492,7 +492,7 @@ "name": "data", "default_property": true, "required": true, - "type": "dynamic(Any)", + "type": "indexed(dynamic(Any))", "doc": "The data object bound to this chart control.
See the section on the data property below for more details." }, { diff --git a/tests/gui/builder/control/test_chart.py b/tests/gui/builder/control/test_chart.py index fc3932b1d..7f3ba8369 100644 --- a/tests/gui/builder/control/test_chart.py +++ b/tests/gui/builder/control/test_chart.py @@ -258,3 +258,21 @@ def test_chart_indexed_properties_with_arrays_builder(gui: Gui, helpers): ""lines": [null, {"dash": "dashdot"}, {"dash": "dash"}, null, {"dash": "dashdot"}, {"dash": "dash"}]", # noqa: E501 ] helpers.test_control_builder(gui, page, expected_list) + +def test_chart_multi_data(gui: Gui, helpers, csvdata): + with tgb.Page(frame=None) as page: + tgb.chart( # type: ignore[attr-defined] + data="{csvdata}", + x="Day", + y="Daily hospital occupancy", + data__1="{csvdata}", + ) + expected_list = [ + "