Преглед изворни кода

Merge pull request #1424 from Avaiga/metric-label

Add title for Metric.tsx
Nam Nguyen пре 11 месеци
родитељ
комит
f49b1481b2

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

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

+ 19 - 13
frontend/taipy-gui/src/components/Taipy/Metric.tsx

@@ -12,7 +12,7 @@
  */
  */
 
 
 import React, {CSSProperties, lazy, Suspense, useMemo} from 'react';
 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 Box from "@mui/material/Box";
 import Skeleton from "@mui/material/Skeleton";
 import Skeleton from "@mui/material/Skeleton";
 import Tooltip from "@mui/material/Tooltip";
 import Tooltip from "@mui/material/Tooltip";
@@ -20,11 +20,12 @@ import {useTheme} from "@mui/material";
 import {useClassNames, useDynamicJsonProperty, useDynamicProperty} from "../../utils/hooks";
 import {useClassNames, useDynamicJsonProperty, useDynamicProperty} from "../../utils/hooks";
 import {extractPrefix, extractSuffix, sprintfToD3Converter} from "../../utils/formatConversion";
 import {extractPrefix, extractSuffix, sprintfToD3Converter} from "../../utils/formatConversion";
 import {TaipyBaseProps, TaipyHoverProps} from "./utils";
 import {TaipyBaseProps, TaipyHoverProps} from "./utils";
-import { darkThemeTemplate } from "../../themes/darkThemeTemplate";
+import {darkThemeTemplate} from "../../themes/darkThemeTemplate";
 
 
 const Plot = lazy(() => import("react-plotly.js"));
 const Plot = lazy(() => import("react-plotly.js"));
 
 
 interface MetricProps extends TaipyBaseProps, TaipyHoverProps {
 interface MetricProps extends TaipyBaseProps, TaipyHoverProps {
+    title?: string
     type?: string
     type?: string
     min?: number
     min?: number
     max?: number
     max?: number
@@ -49,7 +50,7 @@ interface MetricProps extends TaipyBaseProps, TaipyHoverProps {
     template_Light_?: string;
     template_Light_?: string;
 }
 }
 
 
-const emptyLayout = {} as Record<string, Record<string, unknown>>;
+const emptyLayout = {} as Partial<Layout>;
 const defaultStyle = {position: "relative", display: "inline-block"};
 const defaultStyle = {position: "relative", display: "inline-block"};
 
 
 const Metric = (props: MetricProps) => {
 const Metric = (props: MetricProps) => {
@@ -86,7 +87,7 @@ const Metric = (props: MetricProps) => {
                     prefix: extractPrefix(props.deltaFormat),
                     prefix: extractPrefix(props.deltaFormat),
                     suffix: extractSuffix(props.deltaFormat),
                     suffix: extractSuffix(props.deltaFormat),
                     valueformat: sprintfToD3Converter(props.deltaFormat)
                     valueformat: sprintfToD3Converter(props.deltaFormat)
-                },
+                } as Partial<Delta>,
                 gauge: {
                 gauge: {
                     axis: {
                     axis: {
                         range: [
                         range: [
@@ -102,7 +103,7 @@ const Metric = (props: MetricProps) => {
                     }
                     }
                 },
                 },
             }
             }
-        ];
+        ] as Data[];
     }, [
     }, [
         props.format,
         props.format,
         props.deltaFormat,
         props.deltaFormat,
@@ -144,34 +145,39 @@ const Metric = (props: MetricProps) => {
             layout.template = template;
             layout.template = template;
         }
         }
 
 
-        return layout
+        if (props.title) {
+            layout.title = props.title;
+        }
+
+        return layout as Partial<Layout>;
     }, [
     }, [
+        props.title,
         props.template,
         props.template,
         props.template_Dark_,
         props.template_Dark_,
         props.template_Light_,
         props.template_Light_,
         theme.palette.mode,
         theme.palette.mode,
-        baseLayout
+        baseLayout,
     ])
     ])
 
 
     return (
     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}/>}>
                 <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle}/>}>
                     <Plot
                     <Plot
-                        data={data as Data[]}
+                        data={data}
                         layout={layout}
                         layout={layout}
                         style={style}
                         style={style}
                         useResizeHandler
                         useResizeHandler
                     />
                     />
                 </Suspense>
                 </Suspense>
-            </Tooltip>
-        </Box>
+            </Box>
+        </Tooltip>
     );
     );
 }
 }
 
 
 export default Metric;
 export default Metric;
 
 
-const { colorscale, colorway, font} = darkThemeTemplate.layout;
+const {colorscale, colorway, font} = darkThemeTemplate.layout;
 const darkTemplate = {
 const darkTemplate = {
     layout: {
     layout: {
         colorscale,
         colorscale,

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

@@ -360,6 +360,7 @@ class _Factory:
         .set_attributes(
         .set_attributes(
             [
             [
                 ("id",),
                 ("id",),
+                ("title",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("layout", PropertyType.dynamic_dict),
                 ("layout", PropertyType.dynamic_dict),
                 ("type", PropertyType.string, "circular"),
                 ("type", PropertyType.string, "circular"),
@@ -372,6 +373,7 @@ class _Factory:
                 ("show_value", PropertyType.boolean, True),
                 ("show_value", PropertyType.boolean, True),
                 ("format", PropertyType.string),
                 ("format", PropertyType.string),
                 ("delta_format", PropertyType.string),
                 ("delta_format", PropertyType.string),
+                ("hover_text", PropertyType.dynamic_string),
                 ("template", PropertyType.dict),
                 ("template", PropertyType.dict),
                 ("template[dark]", PropertyType.dict),
                 ("template[dark]", PropertyType.dict),
                 ("template[light]", PropertyType.dict),
                 ("template[light]", PropertyType.dict),

+ 6 - 0
taipy/gui/viselements.json

@@ -1128,6 +1128,12 @@
                         "type": "str",
                         "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>."
                         "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",
                         "name": "min",
                         "type": "int|float",
                         "type": "int|float",