Repository: Avaiga/taipy

Issue:
Add support for data slice update in charts
### Description

Multi-trace charts rely on a single *data* property, which is a limitation.

### Solution Proposed

Make *data* an indexed property.

### Acceptance Criteria

- [ ] If applicable, a new demo code is provided to show the new feature in action.
- [ ] Integration tests exhibiting how the functionality works are added.
- [ ] Any new code is covered by a unit tested.
- [ ] Check code coverage is at least 90%.
- [ ] Related issue(s) in taipy-doc are created for documentation and Release Notes are updated.


### Code of Conduct

- [X] I have checked the [existing issues](https://github.com/Avaiga/taipy/issues?q=is%3Aissue+).
- [ ] I am willing to work on this issue (optional)

Discussion:
@github-actions: This issue has been labelled as "🥶Waiting for contributor" because it has been inactive for more than 14 days. If you would like to continue working on this issue, please add another comment or create a PR that links to this issue. If a PR has already been created which refers to this issue, then you should explicitly mention this issue in the relevant PR. Otherwise, you will be unassigned in 14 days. For more information please refer to the contributing guidelines.@jrobinAV: To add my two cents to the topic, when communicating about this indexed data topic, instead of `data slice,` I would suggest using something else like `data layer` or `data stratum` or any better naming since data slicing already means something in data processing.



Solution patch:
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..86bebfc2e 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<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) {
diff --git a/frontend/taipy-gui/src/components/pages/TaipyRendered.tsx b/frontend/taipy-gui/src/components/pages/TaipyRendered.tsx
index e2d470e68..e22b755f8 100644
--- a/frontend/taipy-gui/src/components/pages/TaipyRendered.tsx
+++ b/frontend/taipy-gui/src/components/pages/TaipyRendered.tsx
@@ -42,6 +42,7 @@ interface AxiosRenderer {
     style: string;
     head: HeadProps[];
     context: string;
+    scriptPaths: string[];
 }
 
 // set global style the traditional way
@@ -61,6 +62,18 @@ const setStyle = (id: string, styleString: string): void => {
     }
 };
 
+// set script tag for the page
+const setScript = (id: string, scriptPaths: string[]): void => {
+    document.querySelectorAll(`script[id^="${id}_"]`).forEach(script => script.remove());
+    scriptPaths.forEach((path, index) => {
+        const script = document.createElement("script");
+        script.id = `${id}_${index}`;
+        script.src = path;
+        script.defer = true;
+        document.head.append(script);
+    });
+};
+
 interface PageState {
     jsx?: string;
     module?: string;
@@ -97,9 +110,10 @@ const TaipyRendered = (props: TaipyRenderedProps) => {
                     if (!fromBlock) {
                         setStyle(
                             path == "/TaiPy_root_page" ? "Taipy_root_style" : "Taipy_style",
-                            result.data.style || ""
+                            result.data.style || "",
                         );
                         Array.isArray(result.data.head) && setHead(result.data.head);
+                        Array.isArray(result.data.scriptPaths) && setScript("Taipy_script", result.data.scriptPaths);
                     }
                 })
                 .catch((error) => {
diff --git a/taipy/gui/_page.py b/taipy/gui/_page.py
index 6ccf8e950..a1da83da1 100644
--- a/taipy/gui/_page.py
+++ b/taipy/gui/_page.py
@@ -16,6 +16,7 @@ import logging
 import re
 import typing as t
 import warnings
+from pathlib import Path
 
 from ._warnings import TaipyGuiAlwaysWarning
 
@@ -34,6 +35,7 @@ class _Page(object):
         self._style: t.Optional[t.Union[str, t.Dict[str, t.Any]]] = None
         self._route: t.Optional[str] = None
         self._head: t.Optional[list] = None
+        self._script_paths: t.Optional[t.Union[str, Path, t.List[t.Union[str, Path]]]] = None
 
     def render(self, gui: Gui, silent: t.Optional[bool] = False):
         if self._renderer is None:
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 @@ 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)
 
diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py
index e2d57fc0f..9d11e1ee0 100644
--- a/taipy/gui/gui.py
+++ b/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)
@@ -2087,6 +2094,7 @@ class Gui:
         new_page._route = name
         new_page._renderer = page
         new_page._style = style or page._get_style()
+        new_page._script_paths = page._get_script_paths()
         # Append page to _config
         self._config.pages.append(new_page)
         self._config.routes.append(name)
@@ -2571,7 +2579,7 @@ class Gui:
             with self._set_locals_context(context):
                 self._call_on_page_load(nav_page)
             return self._server._render(
-                page._rendered_jsx, page._style if page._style is not None else "", page._head, context
+                page._rendered_jsx, page._script_paths if page._script_paths is not None else [], page._style if page._style is not None else "", page._head, context # noqa: E501
             )
         else:
             return ("No page template", 404)
diff --git a/taipy/gui/page.py b/taipy/gui/page.py
index 0be7f20e8..e87984dc8 100644
--- a/taipy/gui/page.py
+++ b/taipy/gui/page.py
@@ -13,8 +13,11 @@ from __future__ import annotations
 
 import inspect
 import typing as t
+from pathlib import Path
 from types import FrameType
+from urllib.parse import urlparse
 
+from ._warnings import _warn
 from .utils import _filter_locals, _get_module_name_from_frame
 
 if t.TYPE_CHECKING:
@@ -75,6 +78,7 @@ class Page:
         self._notebook_gui: t.Optional["Gui"] = None
         self._notebook_page: t.Optional["_Page"] = None
         self.set_style(kwargs.get("style", None))
+        self._script_paths(kwargs.get("script_paths", None))
 
     def create_page(self) -> t.Optional[Page]:
         """Create the page content for page modules.
@@ -180,3 +184,34 @@ class Page:
 
     def _get_style(self):
         return self.__style
+
+    def _script_paths(self, script_paths: t.Optional[t.Union[str, Path, t.List[t.Union[str, Path]]]]) -> Page:
+        """
+        Load a script or a list of scripts to be used in the page.
+
+        Arguments:
+            script_paths (str, Path, list): The path to the script file or a list of paths to the script files.
+
+        Returns:
+            This `Page` instance.
+        """
+        if script_paths:
+            if isinstance(script_paths, (str, Path)):
+                script_paths = [script_paths]
+            for script_path in script_paths:
+                if isinstance(script_path, str):
+                    parsed_url = urlparse(script_path)
+                    if parsed_url.netloc:
+                        continue
+                    script_path = Path(script_path)
+
+                if isinstance(script_path,
+                              Path) and script_path.exists() and script_path.is_file() and script_path.suffix == ".js":
+                    continue
+                else:
+                    _warn(f"Script path '{script_path}' does not exist, is not a file, or is not a JavaScript file.")
+        self.__script_paths = script_paths if script_paths else None
+        return self
+
+    def _get_script_paths(self):
+        return self.__script_paths
diff --git a/taipy/gui/server.py b/taipy/gui/server.py
index 543f83acc..df13e3404 100644
--- a/taipy/gui/server.py
+++ b/taipy/gui/server.py
@@ -240,7 +240,7 @@ class _Server:
         return taipy_bp
 
     # Update to render as JSX
-    def _render(self, html_fragment, style, head, context):
+    def _render(self, html_fragment, script_paths, style, head, context):
         template_str = _Server.__RE_OPENING_CURLY.sub(_Server.__OPENING_CURLY, html_fragment)
         template_str = _Server.__RE_CLOSING_CURLY.sub(_Server.__CLOSING_CURLY, template_str)
         template_str = template_str.replace('"{!', "{")
@@ -252,6 +252,7 @@ class _Server:
                 "style": (style + os.linesep) if style else "",
                 "head": head or [],
                 "context": context or self._gui._get_default_module_name(),
+                "scriptPaths": script_paths
             }
         )
 
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.<br/>See the section on the <a href=\"#the-data-property\"><i>data</i> property</a> below for more details."
                     },
                     {


Test patch:
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):
         "&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)
diff --git a/tests/gui/control/test_chart.py b/tests/gui/control/test_chart.py
index c289b4b68..5694d52c0 100644
--- a/tests/gui/control/test_chart.py
+++ b/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)
diff --git a/tests/gui/gui_specific/test_gui.py b/tests/gui/gui_specific/test_gui.py
index f7f3b8c2a..cf0fb2563 100644
--- a/tests/gui/gui_specific/test_gui.py
+++ b/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"