Forráskód Böngészése

resolve conflicts

namnguyen 11 hónapja
szülő
commit
a100f8d4bc

+ 114 - 113
frontend/taipy-gui/src/components/Taipy/Chart.tsx

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState, lazy, Suspense } from "react";
+import React, {CSSProperties, useCallback, useEffect, useMemo, useRef, useState, lazy, Suspense} from "react";
 import {
     Config,
     Data,
@@ -26,15 +26,15 @@ import {
 import Skeleton from "@mui/material/Skeleton";
 import Box from "@mui/material/Box";
 import Tooltip from "@mui/material/Tooltip";
-import { useTheme } from "@mui/material";
+import {useTheme} from "@mui/material";
 
-import { getArrayValue, getUpdateVar, TaipyActiveProps, TaipyChangeProps } from "./utils";
+import {getArrayValue, getUpdateVar, TaipyActiveProps, TaipyChangeProps} from "./utils";
 import {
     createRequestChartUpdateAction,
     createSendActionNameAction,
     createSendUpdateAction,
 } from "../../context/taipyReducers";
-import { ColumnDesc } from "./tableUtils";
+import {ColumnDesc} from "./tableUtils";
 import {
     useClassNames,
     useDispatch,
@@ -43,7 +43,7 @@ import {
     useDynamicProperty,
     useModule,
 } from "../../utils/hooks";
-import { darkThemeTemplate } from "../../themes/darkThemeTemplate";
+import {darkThemeTemplate} from "../../themes/darkThemeTemplate";
 
 const Plot = lazy(() => import("react-plotly.js"));
 
@@ -91,7 +91,7 @@ interface ChartConfig {
 
 export type TraceValueType = Record<string, (string | number)[]>;
 
-const defaultStyle = { position: "relative", display: "inline-block" };
+const defaultStyle = {position: "relative", display: "inline-block"};
 
 const indexedData = /^(\d+)\/(.*)/;
 
@@ -105,7 +105,7 @@ const getColNameFromIndexed = (colName: string): string => {
     return colName;
 };
 
-const getValue = <T,>(
+const getValue = <T, >(
     values: TraceValueType | undefined,
     arr: T[],
     idx: number,
@@ -150,21 +150,21 @@ const getDecimatorsPayload = (
 ) => {
     return decimators
         ? {
-              width: plotDiv?.clientWidth,
-              height: plotDiv?.clientHeight,
-              decimators: decimators.map((d, i) =>
-                  d
-                      ? {
-                            decimator: d,
-                            xAxis: getAxis(traces, i, columns, 0),
-                            yAxis: getAxis(traces, i, columns, 1),
-                            zAxis: getAxis(traces, i, columns, 2),
-                            chartMode: modes[i],
-                        }
-                      : undefined
-              ),
-              relayoutData: relayoutData,
-          }
+            width: plotDiv?.clientWidth,
+            height: plotDiv?.clientHeight,
+            decimators: decimators.map((d, i) =>
+                d
+                    ? {
+                        decimator: d,
+                        xAxis: getAxis(traces, i, columns, 0),
+                        yAxis: getAxis(traces, i, columns, 1),
+                        zAxis: getAxis(traces, i, columns, 2),
+                        chartMode: modes[i],
+                    }
+                    : undefined
+            ),
+            relayoutData: relayoutData,
+        }
         : undefined;
 };
 
@@ -177,6 +177,7 @@ const isOnClick = (types: string[]) => (types?.length ? types.every((t) => t ===
 interface WithpointNumbers {
     pointNumbers: number[];
 }
+
 const getPlotIndex = (pt: PlotDatum) =>
     pt.pointIndex === undefined
         ? pt.pointNumber === undefined
@@ -223,7 +224,7 @@ const TaipyPlotlyButtons: ModeBarButtonAny[] = [
             if (!div) {
                 return;
             }
-            const { height } = gd.dataset;
+            const {height} = gd.dataset;
             if (!height) {
                 gd.setAttribute("data-height", getComputedStyle(div).height);
             }
@@ -341,7 +342,7 @@ const Chart = (props: ChartProp) => {
     useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
 
     const layout = useMemo(() => {
-        const layout = { ...baseLayout };
+        const layout = {...baseLayout};
         let template = undefined;
         try {
             const tpl = props.template && JSON.parse(props.template);
@@ -351,7 +352,7 @@ const Chart = (props: ChartProp) => {
                         ? JSON.parse(props.template_Dark_)
                         : darkThemeTemplate
                     : props.template_Light_ && JSON.parse(props.template_Light_);
-            template = tpl ? (tplTheme ? { ...tpl, ...tplTheme } : tpl) : tplTheme ? tplTheme : undefined;
+            template = tpl ? (tplTheme ? {...tpl, ...tplTheme} : tpl) : tplTheme ? tplTheme : undefined;
         } catch (e) {
             console.info(`Error while parsing Chart.template\n${(e as Error).message || e}`);
         }
@@ -401,11 +402,11 @@ const Chart = (props: ChartProp) => {
     const style = useMemo(
         () =>
             height === undefined
-                ? ({ ...defaultStyle, width: width } as CSSProperties)
-                : ({ ...defaultStyle, width: width, height: height } as CSSProperties),
+                ? ({...defaultStyle, width: width} as CSSProperties)
+                : ({...defaultStyle, width: width, height: height} as CSSProperties),
         [width, height]
     );
-    const skelStyle = useMemo(() => ({ ...style, minHeight: "7em" }), [style]);
+    const skelStyle = useMemo(() => ({...style, minHeight: "7em"}), [style]);
 
     const dataPl = useMemo(() => {
         if (props.figure) {
@@ -417,83 +418,83 @@ const Chart = (props: ChartProp) => {
         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;
-              })
+                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;
+            })
             : [];
         return lastDataPl.current;
     }, [props.figure, selected, data, config, dataKey]);
@@ -522,7 +523,7 @@ const Chart = (props: ChartProp) => {
 
     const onRelayout = useCallback(
         (eventData: PlotRelayoutEvent) => {
-            onRangeChange && dispatch(createSendActionNameAction(id, module, { action: onRangeChange, ...eventData }));
+            onRangeChange && dispatch(createSendActionNameAction(id, module, {action: onRangeChange, ...eventData}));
             if (config.decimators && !config.types.includes("scatter3d")) {
                 const backCols = Object.values(config.columns).map((col) => col.dfid);
                 const eventDataKey = Object.entries(eventData)
@@ -577,8 +578,8 @@ const Chart = (props: ChartProp) => {
                 ? props.figure
                     ? index
                     : data[dataKey].tp_index
-                    ? (data[dataKey].tp_index[index] as number)
-                    : index
+                        ? (data[dataKey].tp_index[index] as number)
+                        : index
                 : 0,
         [data, dataKey, props.figure]
     );
@@ -614,9 +615,9 @@ const Chart = (props: ChartProp) => {
     );
 
     return render ? (
-        <Box id={id} data-testid={props.testId} className={className} ref={plotRef}>
-            <Tooltip title={hover || ""}>
-                <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle} />}>
+        <Tooltip title={hover || ""}>
+            <Box id={id} data-testid={props.testId} className={className} ref={plotRef}>
+                <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle}/>}>
                     {Array.isArray(props.figure) && props.figure.length && props.figure[0].data !== undefined ? (
                         <Plot
                             data={props.figure[0].data as Data[]}
@@ -644,8 +645,8 @@ const Chart = (props: ChartProp) => {
                         />
                     )}
                 </Suspense>
-            </Tooltip>
-        </Box>
+            </Box>
+        </Tooltip>
     ) : null;
 };
 

+ 17 - 11
frontend/taipy-gui/src/components/Taipy/Metric.tsx

@@ -12,7 +12,7 @@
  */
 
 import React, {CSSProperties, lazy, Suspense, useMemo} from 'react';
-import {Data} from "plotly.js";
+import {Data, Delta, Layout} from "plotly.js";
 import Box from "@mui/material/Box";
 import Skeleton from "@mui/material/Skeleton";
 import Tooltip from "@mui/material/Tooltip";
@@ -25,6 +25,7 @@ import {darkThemeTemplate} from "../../themes/darkThemeTemplate";
 const Plot = lazy(() => import("react-plotly.js"));
 
 interface MetricProps extends TaipyBaseProps, TaipyHoverProps {
+    title?: string
     type?: string
     min?: number
     max?: number
@@ -50,7 +51,7 @@ interface MetricProps extends TaipyBaseProps, TaipyHoverProps {
     template_Light_?: string;
 }
 
-const emptyLayout = {} as Record<string, Record<string, unknown>>;
+const emptyLayout = {} as Partial<Layout>;
 const defaultStyle = {position: "relative", display: "inline-block"};
 
 const Metric = (props: MetricProps) => {
@@ -103,7 +104,7 @@ const Metric = (props: MetricProps) => {
                     prefix: extractPrefix(props.deltaFormat),
                     suffix: extractSuffix(props.deltaFormat),
                     valueformat: sprintfToD3Converter(props.deltaFormat)
-                },
+                } as Partial<Delta>,
                 gauge: {
                     axis: {
                         range: [
@@ -120,7 +121,7 @@ const Metric = (props: MetricProps) => {
                     }
                 },
             }
-        ];
+        ] as Data[];
     }, [
         props.format,
         props.deltaFormat,
@@ -163,28 +164,33 @@ const Metric = (props: MetricProps) => {
             layout.template = template;
         }
 
-        return layout
+        if (props.title) {
+            layout.title = props.title;
+        }
+
+        return layout as Partial<Layout>;
     }, [
+        props.title,
         props.template,
         props.template_Dark_,
         props.template_Light_,
         theme.palette.mode,
-        baseLayout
+        baseLayout,
     ])
 
     return (
-        <Box data-testid={props.testId} className={className}>
-            <Tooltip title={hover || ""}>
+        <Tooltip title={hover || ""}>
+            <Box data-testid={props.testId} className={className}>
                 <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle}/>}>
                     <Plot
-                        data={data as Data[]}
+                        data={data}
                         layout={layout}
                         style={style}
                         useResizeHandler
                     />
                 </Suspense>
-            </Tooltip>
-        </Box>
+            </Box>
+        </Tooltip>
     );
 }
 

+ 59 - 20
frontend/taipy-gui/src/utils/index.spec.ts

@@ -14,7 +14,7 @@
 import "@testing-library/jest-dom";
 import { FormatConfig } from "../context/taipyReducers";
 
-import { getNumberString } from "./index";
+import { getNumberString, getDateTimeString } from "./index";
 
 let myWarn: jest.Mock;
 
@@ -23,49 +23,88 @@ beforeEach(() => {
     console.warn = myWarn;
 })
 
-const getFormatConfig = (numberFormat?: string): FormatConfig => ({timeZone: "", date: "", dateTime: "", number: numberFormat || "", forceTZ: false})
+const getNumberFormatConfig = (numberFormat?: string): FormatConfig => ({timeZone: "", date: "", dateTime: "", number: numberFormat || "", forceTZ: false})
+const getDateFormatConfig = (dateFormat?: string): FormatConfig => ({timeZone: "", date: dateFormat || "", dateTime: dateFormat || "", number: "", forceTZ: false})
 
 describe("getNumberString", () => {
     it("returns straight", async () => {
-        expect(getNumberString(1, undefined, getFormatConfig())).toBe("1");
+        expect(getNumberString(1, undefined, getNumberFormatConfig())).toBe("1");
     });
     it("returns formatted", async () => {
-        expect(getNumberString(1, "%.1f", getFormatConfig())).toBe("1.0");
+        expect(getNumberString(1, "%.1f", getNumberFormatConfig())).toBe("1.0");
     });
     it("returns formatted float", async () => {
-        expect(getNumberString(1.0, "%.0f", getFormatConfig())).toBe("1");
+        expect(getNumberString(1.0, "%.0f", getNumberFormatConfig())).toBe("1");
     });
     it("returns default formatted", async () => {
-        expect(getNumberString(1, "", getFormatConfig("%.1f"))).toBe("1.0");
+        expect(getNumberString(1, "", getNumberFormatConfig("%.1f"))).toBe("1.0");
     });
     it("returns for non variable format", async () => {
-        expect(getNumberString(1, "toto", getFormatConfig())).toBe("toto");
+        expect(getNumberString(1, "toto", getNumberFormatConfig())).toBe("toto");
     });
     it("returns formatted over default", async () => {
-        expect(getNumberString(1, "%.2f", getFormatConfig("%.1f"))).toBe("1.00");
+        expect(getNumberString(1, "%.2f", getNumberFormatConfig("%.1f"))).toBe("1.00");
     });
     it("returns for string", async () => {
-        expect(getNumberString("null" as unknown as number, "", getFormatConfig("%.1f"))).toBe("null");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] expecting number but found string")
+        expect(getNumberString("null" as unknown as number, "", getNumberFormatConfig("%.1f"))).toBe("null");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] expecting number but found string")
     });
     it("returns for object", async () => {
-        expect(getNumberString({t: 1} as unknown as number, "", getFormatConfig("%.1f"))).toBe("");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] expecting number but found object")
+        expect(getNumberString({t: 1} as unknown as number, "", getNumberFormatConfig("%.1f"))).toBe("");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] expecting number but found object")
     });
     it("returns for bad format", async () => {
-        expect(getNumberString(1, "%.f", getFormatConfig())).toBe("1");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder")
+        expect(getNumberString(1, "%.f", getNumberFormatConfig())).toBe("1");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder")
     });
     it("returns for null", async () => {
-        expect(getNumberString(null as unknown as number, "%2.f", getFormatConfig("%.1f"))).toBe("");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder")
+        expect(getNumberString(null as unknown as number, "%2.f", getNumberFormatConfig("%.1f"))).toBe("");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder")
     });
     it("returns for undefined", async () => {
-        expect(getNumberString(undefined as unknown as number, "%2.f", getFormatConfig("%.1f"))).toBe("");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder")
+        expect(getNumberString(undefined as unknown as number, "%2.f", getNumberFormatConfig("%.1f"))).toBe("");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder")
     });
     it("returns for NaN", async () => {
-        expect(getNumberString(NaN, "%2.f", getFormatConfig("%.1f"))).toBe("NaN");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder")
+        expect(getNumberString(NaN, "%2.f", getNumberFormatConfig("%.1f"))).toBe("NaN");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder")
+    });
+});
+
+describe("getDateTimeString", () => {
+    it("returns straight", async () => {
+        expect(getDateTimeString("2024-10-05", undefined, getDateFormatConfig())).toContain("05 2024");
+    });
+    it("returns formatted", async () => {
+        expect(getDateTimeString("2024-10-05", "dd-MM-yy", getDateFormatConfig())).toBe("05-10-24");
+    });
+    it("returns default formatted", async () => {
+        expect(getDateTimeString("2024-10-05", "", getDateFormatConfig("dd-MM-yy"))).toBe("05-10-24");
+    });
+    it("returns formatted over default", async () => {
+        expect(getDateTimeString("2024-10-05", "dd-MM-yy", getNumberFormatConfig("yy-MM-dd"))).toBe("05-10-24");
+    });
+    it("returns for string", async () => {
+        expect(getDateTimeString("null" as unknown as string, "", getDateFormatConfig("dd-MM-yy"))).toBe("Invalid Date");
+        expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value")
+    });
+    it("returns for object", async () => {
+        expect(getDateTimeString({t: 1} as unknown as string, "", getDateFormatConfig("dd-MM-yy"))).toBe("Invalid Date");
+        expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value")
+    });
+    it("returns for bad format", async () => {
+        expect(getDateTimeString("2024-10-05", "D", getDateFormatConfig())).toContain("05 2024");
+        expect(myWarn).toHaveBeenCalled()
+        expect(myWarn.mock.lastCall).toHaveLength(2)
+        expect(myWarn.mock.lastCall[0]).toBe("Invalid date format:")
+        expect(myWarn.mock.lastCall[1]).toContain("Use `d` instead of `D`")
+    });
+    it("returns for null", async () => {
+        expect(getDateTimeString(null as unknown as string, "dd-MM-yy", getDateFormatConfig())).toBe("null");
+        expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value")
+    });
+    it("returns for undefined", async () => {
+        expect(getDateTimeString(undefined as unknown as string, "dd-MM-yy", getDateFormatConfig())).toBe("null");
+        expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value")
     });
 });

+ 14 - 8
frontend/taipy-gui/src/utils/index.ts

@@ -105,14 +105,20 @@ export const getDateTimeString = (
     tz?: string,
     withTime: boolean = true
 ): string => {
-    if (withTime) {
-        return formatInTimeZone(
-            getDateTime(value) || "",
-            formatConf.forceTZ || !tz ? formatConf.timeZone : tz,
-            datetimeformat || formatConf.dateTime
-        );
+    const dateVal = getDateTime(value);
+    try {
+        if (withTime) {
+            return formatInTimeZone(
+                dateVal || "",
+                formatConf.forceTZ || !tz ? formatConf.timeZone : tz,
+                datetimeformat || formatConf.dateTime
+            );
+        }
+        return format(dateVal || 0, datetimeformat || formatConf.date);
+    } catch (e) {
+        console.warn("Invalid date format:", (e as Error).message || e);
+        return `${dateVal}`;
     }
-    return format(getDateTime(value) || 0, datetimeformat || formatConf.date);
 };
 
 export const getNumberString = (value: number, numberformat: string | undefined, formatConf: FormatConfig): string => {
@@ -121,7 +127,7 @@ export const getNumberString = (value: number, numberformat: string | undefined,
             ? sprintf(numberformat || formatConf.number, value)
             : value.toLocaleString();
     } catch (e) {
-        console.warn("getNumberString: " + (e as Error).message || e);
+        console.warn("Invalid number format:", (e as Error).message || e);
         return (
             (typeof value === "number" && value.toLocaleString()) ||
             (typeof value === "string" && (value as string)) ||

+ 4 - 5
frontend/taipy/src/ScenarioViewer.tsx

@@ -366,7 +366,6 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
             }
         }
         setValid(!!sc);
-        // setSubmissionStatus(0);
         setScenario((oldSc) => (oldSc === sc ? oldSc : sc ? (deepEqual(oldSc, sc) ? oldSc : sc) : invalidScenario));
     }, [props.scenario, props.defaultScenario]);
 
@@ -579,7 +578,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     const addSequenceHandler = useCallback(() => setSequences((seq) => [...seq, ["", [], "", true]]), []);
 
     // Submission status
-    const [submissionStatus, setSubmissionStatus] = useState(0);
+    const [submissionStatus, setSubmissionStatus] = useState(-1);
 
     // on scenario change
     useEffect(() => {
@@ -594,10 +593,10 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     useEffect(() => {
         const ids = props.coreChanged?.scenario;
         if (typeof ids === "string" ? ids === scId : Array.isArray(ids) ? ids.includes(scId) : ids) {
-            props.updateVarName && dispatch(createRequestUpdateAction(id, module, [props.updateVarName], true));
-            if (props.coreChanged?.submission !== undefined) {
+            if (typeof props.coreChanged?.submission === "number") {
                 setSubmissionStatus(props.coreChanged?.submission as number);
             }
+            props.updateVarName && dispatch(createRequestUpdateAction(id, module, [props.updateVarName], true));
         }
     }, [props.coreChanged, props.updateVarName, id, module, dispatch, scId]);
 
@@ -629,7 +628,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                         sx={ChipSx}
                                     />
                                 ) : null}
-                                {submissionStatus ? <StatusChip status={submissionStatus} sx={ChipSx} /> : null}
+                                {submissionStatus > -1 ? <StatusChip status={submissionStatus} sx={ChipSx} /> : null}
                             </Grid>
                             <Grid item>
                                 {showSubmit ? (

+ 2 - 1
frontend/taipy/src/StatusChip.tsx

@@ -3,7 +3,8 @@ import { SxProps, Theme } from "@mui/material";
 import Chip from "@mui/material/Chip";
 
 export enum Status {
-    SUBMITTED = 1,
+    SUBMITTED = 0,
+    UNDEFINED = 1,
     BLOCKED = 2,
     PENDING = 3,
     RUNNING = 4,

+ 3 - 1
taipy/core/data/csv.py

@@ -18,6 +18,7 @@ import pandas as pd
 
 from taipy.config.common.scope import Scope
 
+from .._entity._reload import _Reloader
 from .._version._version_manager_factory import _VersionManagerFactory
 from ..job.job_id import JobId
 from ._file_datanode_mixin import _FileDataNodeMixin
@@ -116,7 +117,8 @@ class CSVDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
             **properties,
         )
 
-        self._write_default_data(default_value)
+        with _Reloader():
+            self._write_default_data(default_value)
 
         self._TAIPY_PROPERTIES.update(
             {

+ 3 - 1
taipy/core/data/excel.py

@@ -18,6 +18,7 @@ from openpyxl import load_workbook
 
 from taipy.config.common.scope import Scope
 
+from .._entity._reload import _Reloader
 from .._version._version_manager_factory import _VersionManagerFactory
 from ..exceptions.exceptions import ExposedTypeLengthMismatch, NonExistingExcelSheet, SheetNameLengthMismatch
 from ..job.job_id import JobId
@@ -118,7 +119,8 @@ class ExcelDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
             **properties,
         )
 
-        self._write_default_data(default_value)
+        with _Reloader():
+            self._write_default_data(default_value)
 
         self._TAIPY_PROPERTIES.update(
             {

+ 3 - 2
taipy/core/data/json.py

@@ -18,7 +18,7 @@ from typing import Any, Dict, List, Optional, Set
 
 from taipy.config.common.scope import Scope
 
-from .._entity._reload import _self_reload
+from .._entity._reload import _Reloader, _self_reload
 from .._version._version_manager_factory import _VersionManagerFactory
 from ._file_datanode_mixin import _FileDataNodeMixin
 from .data_node import DataNode
@@ -112,7 +112,8 @@ class JSONDataNode(DataNode, _FileDataNodeMixin):
         self._decoder = self._properties.get(self._DECODER_KEY, _DefaultJSONDecoder)
         self._encoder = self._properties.get(self._ENCODER_KEY, _DefaultJSONEncoder)
 
-        self._write_default_data(default_value)
+        with _Reloader():
+            self._write_default_data(default_value)
 
         self._TAIPY_PROPERTIES.update(
             {

+ 3 - 1
taipy/core/data/parquet.py

@@ -18,6 +18,7 @@ import pandas as pd
 
 from taipy.config.common.scope import Scope
 
+from .._entity._reload import _Reloader
 from .._version._version_manager_factory import _VersionManagerFactory
 from ..exceptions.exceptions import UnknownCompressionAlgorithm, UnknownParquetEngine
 from ..job.job_id import JobId
@@ -153,7 +154,8 @@ class ParquetDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
             **properties,
         )
 
-        self._write_default_data(default_value)
+        with _Reloader():
+            self._write_default_data(default_value)
 
         if not self._last_edit_date and (isfile(self._path) or isdir(self._path)):
             self._last_edit_date = datetime.now()

+ 3 - 1
taipy/core/data/pickle.py

@@ -15,6 +15,7 @@ from typing import List, Optional, Set
 
 from taipy.config.common.scope import Scope
 
+from .._entity._reload import _Reloader
 from .._version._version_manager_factory import _VersionManagerFactory
 from ._file_datanode_mixin import _FileDataNodeMixin
 from .data_node import DataNode
@@ -98,7 +99,8 @@ class PickleDataNode(DataNode, _FileDataNodeMixin):
             **properties,
         )
 
-        self._write_default_data(default_value)
+        with _Reloader():
+            self._write_default_data(default_value)
 
         self._TAIPY_PROPERTIES.update(
             {

+ 1 - 0
taipy/gui/_renderers/builder.py

@@ -946,6 +946,7 @@ class _Builder:
 
             attributes (list(tuple)): The list of attributes as (property name, property type, default value).
         """
+        attributes.append(("id",)) # Every element should have an id attribute
         for attr in attributes:
             if not isinstance(attr, tuple):
                 attr = (attr,)

+ 2 - 27
taipy/gui/_renderers/factory.py

@@ -78,7 +78,6 @@ class _Factory:
         .set_value_and_default(with_update=False)
         .set_attributes(
             [
-                ("id",),
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
@@ -90,7 +89,6 @@ class _Factory:
         .set_value_and_default(with_update=True, with_default=False, var_type=PropertyType.data)
         .set_attributes(
             [
-                ("id",),
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
@@ -107,7 +105,6 @@ class _Factory:
         .set_value_and_default(with_default=False, var_type=PropertyType.data)
         .set_attributes(
             [
-                ("id",),
                 ("title",),
                 ("width", PropertyType.string_or_number),
                 ("height", PropertyType.string_or_number),
@@ -140,7 +137,6 @@ class _Factory:
         .set_attributes(
             [
                 ("with_time", PropertyType.boolean),
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("editable", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
@@ -160,7 +156,6 @@ class _Factory:
         .set_attributes(
             [
                 ("with_time", PropertyType.boolean),
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("editable", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
@@ -181,7 +176,6 @@ class _Factory:
         ._set_partial()  # partial should be set before page
         .set_attributes(
             [
-                ("id",),
                 ("page",),
                 ("title",),
                 ("on_action", PropertyType.function),
@@ -201,7 +195,6 @@ class _Factory:
         ._set_partial()  # partial should be set before page
         .set_attributes(
             [
-                ("id",),
                 ("page",),
                 ("expanded", PropertyType.dynamic_boolean, True, True, False),
                 ("hover_text", PropertyType.dynamic_string),
@@ -218,7 +211,6 @@ class _Factory:
         ._set_content("content", image=False)
         .set_attributes(
             [
-                ("id",),
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("render", PropertyType.dynamic_boolean, True),
@@ -238,7 +230,6 @@ class _Factory:
         ._set_file_content()
         .set_attributes(
             [
-                ("id",),
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("multiple", PropertyType.boolean, False),
@@ -258,7 +249,6 @@ class _Factory:
         ._set_content("content")
         .set_attributes(
             [
-                ("id",),
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("width",),
@@ -275,7 +265,6 @@ class _Factory:
         .set_value_and_default(with_update=False, native_type=True)
         .set_attributes(
             [
-                ("id",),
                 ("min", PropertyType.number),
                 ("max", PropertyType.number),
                 ("value", PropertyType.dynamic_number),
@@ -296,7 +285,6 @@ class _Factory:
         ._set_propagate()
         .set_attributes(
             [
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
                 ("on_change", PropertyType.function),
@@ -314,7 +302,6 @@ class _Factory:
         .set_value_and_default(with_default=False)
         .set_attributes(
             [
-                ("id",),
                 ("columns[mobile]",),
                 ("gap",),
             ]
@@ -325,7 +312,6 @@ class _Factory:
         .set_value_and_default(default_val="Log-in")
         .set_attributes(
             [
-                ("id",),
                 ("message", PropertyType.dynamic_string),
                 ("on_action", PropertyType.function, "on_login"),
             ]
@@ -338,7 +324,6 @@ class _Factory:
         )
         .set_attributes(
             [
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("label",),
                 ("width",),
@@ -359,7 +344,7 @@ class _Factory:
         .set_value_and_default(var_type=PropertyType.dynamic_number, native_type=True)
         .set_attributes(
             [
-                ("id",),
+                ("title",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("layout", PropertyType.dynamic_dict),
                 ("type", PropertyType.string, "circular"),
@@ -373,6 +358,7 @@ class _Factory:
                 ("format", PropertyType.string),
                 ("delta_format", PropertyType.string),
                 ("color_map", PropertyType.dict),
+                ("hover_text", PropertyType.dynamic_string),
                 ("template", PropertyType.dict),
                 ("template[dark]", PropertyType.dict),
                 ("template[light]", PropertyType.dict),
@@ -382,7 +368,6 @@ class _Factory:
             gui=gui, control_type=control_type, element_name="NavBar", attributes=attrs, default_value=None
         ).set_attributes(
             [
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
                 ("lov", PropertyType.single_lov),
@@ -400,7 +385,6 @@ class _Factory:
         ._set_propagate()
         .set_attributes(
             [
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
                 ("on_change", PropertyType.function),
@@ -416,7 +400,6 @@ class _Factory:
         ._set_partial()  # partial should be set before page
         .set_attributes(
             [
-                ("id",),
                 ("page",),
                 ("anchor", PropertyType.string, "left"),
                 ("on_close", PropertyType.function),
@@ -435,7 +418,6 @@ class _Factory:
         ._set_partial()  # partial should be set before page
         .set_attributes(
             [
-                ("id",),
                 ("page", PropertyType.dynamic_string),
                 ("render", PropertyType.dynamic_boolean, True),
                 ("height", PropertyType.dynamic_string),
@@ -453,7 +435,6 @@ class _Factory:
                 ("filter", PropertyType.boolean),
                 ("height", PropertyType.string_or_number),
                 ("hover_text", PropertyType.dynamic_string),
-                ("id",),
                 ("value_by_id", PropertyType.boolean),
                 ("multiple", PropertyType.boolean),
                 ("width", PropertyType.string_or_number),
@@ -477,7 +458,6 @@ class _Factory:
                 ("active", PropertyType.dynamic_boolean, True),
                 ("height",),
                 ("hover_text", PropertyType.dynamic_string),
-                ("id",),
                 ("value_by_id", PropertyType.boolean),
                 ("max", PropertyType.number, 100),
                 ("min", PropertyType.number, 0),
@@ -502,7 +482,6 @@ class _Factory:
         .set_value_and_default(with_update=False)
         .set_attributes(
             [
-                ("id",),
                 ("without_close", PropertyType.boolean, False),
                 ("hover_text", PropertyType.dynamic_string),
             ]
@@ -523,7 +502,6 @@ class _Factory:
                 ("auto_loading", PropertyType.boolean),
                 ("width", PropertyType.string_or_number, "100%"),
                 ("height", PropertyType.string_or_number, "80vh"),
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("editable", PropertyType.dynamic_boolean, True),
                 ("on_edit", PropertyType.function),
@@ -551,7 +529,6 @@ class _Factory:
         .set_attributes(
             [
                 ("format",),
-                ("id",),
                 ("hover_text", PropertyType.dynamic_string),
                 ("raw", PropertyType.boolean, False),
                 ("mode",),
@@ -565,7 +542,6 @@ class _Factory:
             [
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
-                ("id",),
                 ("label",),
                 ("value_by_id", PropertyType.boolean),
                 ("unselected_value", PropertyType.string, ""),
@@ -591,7 +567,6 @@ class _Factory:
                 ("filter", PropertyType.boolean),
                 ("hover_text", PropertyType.dynamic_string),
                 ("height", PropertyType.string_or_number),
-                ("id",),
                 ("value_by_id", PropertyType.boolean),
                 ("multiple", PropertyType.boolean),
                 ("width", PropertyType.string_or_number),

+ 6 - 0
taipy/gui/viselements.json

@@ -1128,6 +1128,12 @@
                         "type": "str",
                         "doc": "The type of the gauge.<br/>Possible values are:\n<ul>\n<li>\"none\"</li>\n<li>\"circular\"</li>\n<li>\"linear\"</li></ul>."
                     },
+                    {
+                        "name": "title",
+                        "default_value": "None",
+                        "type": "str",
+                        "doc": "The title of the metric."
+                    },
                     {
                         "name": "min",
                         "type": "int|float",

+ 19 - 21
taipy/gui_core/_context.py

@@ -119,10 +119,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         else None
                     )
                     if sequence and hasattr(sequence, "parent_ids") and sequence.parent_ids:  # type: ignore
-                        self.gui._broadcast(
-                            _GuiCoreContext._CORE_CHANGED_NAME,
-                            {"scenario": list(sequence.parent_ids)},  # type: ignore
-                        )
+                        self.broadcast_core_changed({"scenario": list(sequence.parent_ids)})
             except Exception as e:
                 _warn(f"Access to sequence {event.entity_id} failed", e)
         elif event.entity_type == EventEntityType.JOB:
@@ -133,25 +130,26 @@ class _GuiCoreContext(CoreEventConsumerBase):
         elif event.entity_type == EventEntityType.DATA_NODE:
             with self.lock:
                 self.data_nodes_by_owner = None
-            self.gui._broadcast(
-                _GuiCoreContext._CORE_CHANGED_NAME,
-                {"datanode": event.entity_id if event.operation != EventOperation.DELETION else True},
+            self.broadcast_core_changed(
+                {"datanode": event.entity_id if event.operation != EventOperation.DELETION else True}
             )
 
+    def broadcast_core_changed(self, payload: t.Dict[str, t.Any], client_id: t.Optional[str] = None):
+        self.gui._broadcast(_GuiCoreContext._CORE_CHANGED_NAME, payload, client_id)
+
     def scenario_refresh(self, scenario_id: t.Optional[str]):
         with self.lock:
             self.scenario_by_cycle = None
             self.data_nodes_by_owner = None
-        self.gui._broadcast(
-            _GuiCoreContext._CORE_CHANGED_NAME,
-            {"scenario": scenario_id or True},
-        )
+        self.broadcast_core_changed({"scenario": scenario_id or True})
 
     def submission_status_callback(self, submission_id: t.Optional[str] = None, event: t.Optional[Event] = None):
         if not submission_id or not is_readable(t.cast(SubmissionId, submission_id)):
             return
         submission = None
         new_status = None
+        payload: t.Optional[t.Dict[str, t.Any]] = None
+        client_id: t.Optional[str] = None
         try:
             last_status = self.client_submission.get(submission_id)
             if not last_status:
@@ -161,6 +159,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             if not submission or not submission.entity_id:
                 return
 
+            payload = {}
             new_status = t.cast(SubmissionStatus, submission.submission_status)
 
             client_id = submission.properties.get("client_id")
@@ -176,7 +175,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                             if job.is_pending()
                             else None
                         )
-                    self.gui._broadcast(_GuiCoreContext._CORE_CHANGED_NAME, {"tasks": running_tasks}, client_id)
+                    payload.update(tasks=running_tasks)
 
                     if last_status != new_status:
                         # callback
@@ -210,15 +209,14 @@ class _GuiCoreContext(CoreEventConsumerBase):
             _warn(f"Submission ({submission_id}) is not available", e)
 
         finally:
-            entity_id = submission.entity_id if submission else None
-            self.gui._broadcast(
-                _GuiCoreContext._CORE_CHANGED_NAME,
-                {
-                    "jobs": True,
-                    "scenario": entity_id or False,
-                    "submission": new_status.value if new_status else None,
-                },
-            )
+            if payload is not None:
+                payload.update(jobs=True)
+                entity_id = submission.entity_id if submission else None
+                if entity_id:
+                    payload.update(scenario=entity_id)
+                    if new_status:
+                        payload.update(submission=new_status.value)
+                self.broadcast_core_changed(payload, client_id)
 
     def no_change_adapter(self, entity: t.List):
         return entity