瀏覽代碼

Chart: Support indexed data (#2390)

* Chart: Support indexed data
resolves #2338

* codespell ...

* fix test

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 4 月之前
父節點
當前提交
eb00833b35

+ 4 - 4
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();

+ 229 - 144
frontend/taipy-gui/src/components/Taipy/Chart.tsx

@@ -60,6 +60,7 @@ interface ChartProp extends TaipyActiveProps, TaipyChangeProps {
     defaultConfig: string;
     config?: string;
     data?: Record<string, TraceValueType>;
+    //data${number}?: Record<string, TraceValueType>;
     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<Record<string, unknown>>;
     onClick?: string;
+    dataVarNames?: string;
 }
 
 interface ChartConfig {
-    columns: Record<string, ColumnDesc>;
+    columns: Array<Record<string, ColumnDesc>>;
     labels: string[];
     modes: string[];
     types: string[];
@@ -217,7 +219,7 @@ export const getPlotIndex = (pt: PlotDatum) =>
         : pt.pointIndex;
 
 const defaultConfig = {
-    columns: {} as Record<string, ColumnDesc>,
+    columns: [] as Array<Record<string, ColumnDesc>>,
     labels: [],
     modes: [],
     types: [],
@@ -285,6 +287,15 @@ const getDataKey = (columns?: Record<string, ColumnDesc>, decimators?: string[])
     return [backCols, backCols.join("-") + (decimators ? `--${decimators.join("")}` : "")];
 };
 
+const isDataRefresh = (data?: Record<string, TraceValueType>) => data?.__taipy_refresh !== undefined;
+const getDataVarName = (updateVarName: string | undefined, dataVarNames: string[], idx: number) =>
+    idx === 0 ? updateVarName : dataVarNames[idx - 1];
+const getData = (
+    data: Record<string, TraceValueType>,
+    additionalDatas: Array<Record<string, TraceValueType>>,
+    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<number[][]>([]);
     const plotRef = useRef<HTMLDivElement>(null);
-    const [dataKey, setDataKey] = useState("__default__");
+    const [dataKeys, setDataKeys] = useState<string[]>([]);
     const lastDataPl = useRef<Data[]>([]);
     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<Array<Record<string, TraceValueType>>>([]);
+    const additionalDatas = useMemo(() => {
+        const newAdditionalDatas = dataVarNames.map(
+            (_, idx) => (props as unknown as Record<string, Record<string, TraceValueType>>)[`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 dataList = dataKeys.map((_, idx) => getData(data, additionalDatas, idx));
+        if (!dataList.length || dataList.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<string, unknown>;
-                  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<string, unknown>)[prop];
-                          if (typeof val === "string") {
-                              const arr = getValueFromCol(datum, val as string);
-                              if (arr.length) {
-                                  (ret.marker as Record<string, unknown>)[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 < dataList.length ? dataList[idx] : dataList[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<string, unknown>;
+            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<string, unknown>)[prop];
+                    if (typeof val === "string") {
+                        const arr = getValueFromCol(datum, val as string);
+                        if (arr.length) {
+                            (ret.marker as Record<string, unknown>)[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<Config> = {};
@@ -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) {

+ 18 - 2
taipy/gui/_renderers/builder.py

@@ -610,7 +610,7 @@ class _Builder:
         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 @@ class _Builder:
         # 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)
 

+ 9 - 2
taipy/gui/gui.py

@@ -1903,11 +1903,18 @@ class Gui:
                 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)

+ 45 - 20
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],

+ 1 - 1
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.<br/>See the section on the <a href=\"#the-data-property\"><i>data</i> property</a> below for more details."
                     },
                     {

+ 18 - 0
tests/gui/builder/control/test_chart.py

@@ -258,3 +258,21 @@ def test_chart_indexed_properties_with_arrays_builder(gui: Gui, helpers):
         "&quot;lines&quot;: [null, &#x7B;&quot;dash&quot;: &quot;dashdot&quot;&#x7D;, &#x7B;&quot;dash&quot;: &quot;dash&quot;&#x7D;, null, &#x7B;&quot;dash&quot;: &quot;dashdot&quot;&#x7D;, &#x7B;&quot;dash&quot;: &quot;dash&quot;&#x7D;]",  # 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 = [
+        "<Chart",
+        'updateVarName="_TpD_tpec_TpExPr_csvdata_TPMDL_0"',
+        'dataVarNames="_TpD_tpec_TpExPr_csvdata_TPMDL_0"',
+        "data={_TpD_tpec_TpExPr_csvdata_TPMDL_0}",
+        "data1={_TpD_tpec_TpExPr_csvdata_TPMDL_0}",
+    ]
+    gui._set_frame(inspect.currentframe())
+    helpers.test_control_builder(gui, page, expected_list)

+ 12 - 0
tests/gui/control/test_chart.py

@@ -218,3 +218,15 @@ def test_chart_indexed_properties_with_arrays(gui: Gui, helpers):
         "&quot;lines&quot;: [null, &#x7B;&quot;dash&quot;: &quot;dashdot&quot;&#x7D;, &#x7B;&quot;dash&quot;: &quot;dash&quot;&#x7D;, null, &#x7B;&quot;dash&quot;: &quot;dashdot&quot;&#x7D;, &#x7B;&quot;dash&quot;: &quot;dash&quot;&#x7D;]",  # noqa: E501
     ]
     helpers.test_control_md(gui, md, expected_list)
+
+def test_chart_multi_data(gui: Gui, helpers, csvdata):
+    md_string = "<|{csvdata}|chart|x=Day|y=Daily hospital occupancy|data[1]={csvdata}|>"
+    expected_list = [
+        "<Chart",
+        'updateVarName="_TpD_tpec_TpExPr_csvdata_TPMDL_0"',
+        'dataVarNames="_TpD_tpec_TpExPr_csvdata_TPMDL_0"',
+        "data={_TpD_tpec_TpExPr_csvdata_TPMDL_0}",
+        "data1={_TpD_tpec_TpExPr_csvdata_TPMDL_0}",
+    ]
+    gui._set_frame(inspect.currentframe())
+    helpers.test_control_md(gui, md_string, expected_list)

+ 1 - 1
tests/gui/gui_specific/test_gui.py

@@ -73,7 +73,7 @@ def test__chart_conf(gui: Gui):
 
         d = json.loads(res)
         assert isinstance(d, dict)
-        assert d["columns"]["col1"]["type"] == "int"
+        assert d["columns"][0]["col1"]["type"] == "int"
 
         res = gui._chart_conf(False, None, "", "")
         assert repr(res) == "Taipy: Do not update"