|
@@ -11,62 +11,57 @@
|
|
* specific language governing permissions and limitations under the License.
|
|
* specific language governing permissions and limitations under the License.
|
|
*/
|
|
*/
|
|
|
|
|
|
-import React, {CSSProperties, lazy, Suspense, useMemo} from 'react';
|
|
|
|
-import {Data, Delta, Layout} from "plotly.js";
|
|
|
|
|
|
+import React, { CSSProperties, lazy, Suspense, useMemo } from "react";
|
|
|
|
+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";
|
|
-import {useTheme} from "@mui/material";
|
|
|
|
-import {useClassNames, useDynamicJsonProperty, useDynamicProperty} from "../../utils/hooks";
|
|
|
|
-import {extractPrefix, extractSuffix, sprintfToD3Converter} from "../../utils/formatConversion";
|
|
|
|
-import {TaipyBaseProps, TaipyHoverProps} from "./utils";
|
|
|
|
-import {darkThemeTemplate} from "../../themes/darkThemeTemplate";
|
|
|
|
|
|
+import { useTheme } from "@mui/material";
|
|
|
|
+import { useClassNames, useDynamicJsonProperty, useDynamicProperty } from "../../utils/hooks";
|
|
|
|
+import { extractPrefix, extractSuffix, sprintfToD3Converter } from "../../utils/formatConversion";
|
|
|
|
+import { TaipyBaseProps, TaipyHoverProps } from "./utils";
|
|
|
|
+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
|
|
|
|
- min?: number
|
|
|
|
- max?: number
|
|
|
|
- value?: number
|
|
|
|
- defaultValue?: number
|
|
|
|
- delta?: number
|
|
|
|
- defaultDelta?: number
|
|
|
|
- deltaColor?: string
|
|
|
|
- negativeDeltaColor?: string
|
|
|
|
- threshold?: number
|
|
|
|
- defaultThreshold?: number
|
|
|
|
- testId?: string
|
|
|
|
- defaultLayout?: string;
|
|
|
|
|
|
+ value?: number;
|
|
|
|
+ defaultValue?: number;
|
|
|
|
+ delta?: number;
|
|
|
|
+ defaultDelta?: number;
|
|
|
|
+ type?: string;
|
|
|
|
+ min?: number;
|
|
|
|
+ max?: number;
|
|
|
|
+ deltaColor?: string;
|
|
|
|
+ negativeDeltaColor?: string;
|
|
|
|
+ threshold?: number;
|
|
|
|
+ defaultThreshold?: number;
|
|
|
|
+ format?: string;
|
|
|
|
+ deltaFormat?: string;
|
|
|
|
+ barColor?: string;
|
|
|
|
+ showValue?: boolean;
|
|
|
|
+ colorMap?: string;
|
|
|
|
+ title?: string;
|
|
|
|
+ testId?: string;
|
|
layout?: string;
|
|
layout?: string;
|
|
- defaultStyle?: string;
|
|
|
|
|
|
+ defaultLayout?: string;
|
|
style?: string;
|
|
style?: string;
|
|
|
|
+ defaultStyle?: string;
|
|
width?: string | number;
|
|
width?: string | number;
|
|
height?: string | number;
|
|
height?: string | number;
|
|
- showValue?: boolean;
|
|
|
|
- format?: string;
|
|
|
|
- deltaFormat?: string;
|
|
|
|
- colorMap?: string;
|
|
|
|
template?: string;
|
|
template?: string;
|
|
template_Dark_?: string;
|
|
template_Dark_?: string;
|
|
template_Light_?: string;
|
|
template_Light_?: string;
|
|
}
|
|
}
|
|
|
|
|
|
const emptyLayout = {} as Partial<Layout>;
|
|
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) => {
|
|
- const {
|
|
|
|
- width = "100%",
|
|
|
|
- height,
|
|
|
|
- showValue = true,
|
|
|
|
- deltaColor,
|
|
|
|
- negativeDeltaColor
|
|
|
|
- } = props;
|
|
|
|
- const value = useDynamicProperty(props.value, props.defaultValue, 0)
|
|
|
|
- const threshold = useDynamicProperty(props.threshold, props.defaultThreshold, undefined)
|
|
|
|
- const delta = useDynamicProperty(props.delta, props.defaultDelta, undefined)
|
|
|
|
|
|
+ const { width = "100%", height, showValue = true, deltaColor, negativeDeltaColor } = props;
|
|
|
|
+ const value = useDynamicProperty(props.value, props.defaultValue, 0);
|
|
|
|
+ const threshold = useDynamicProperty(props.threshold, props.defaultThreshold, undefined);
|
|
|
|
+ const delta = useDynamicProperty(props.delta, props.defaultDelta, undefined);
|
|
const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
|
|
const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
|
|
const baseLayout = useDynamicJsonProperty(props.layout, props.defaultLayout || "", emptyLayout);
|
|
const baseLayout = useDynamicJsonProperty(props.layout, props.defaultLayout || "", emptyLayout);
|
|
const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
|
|
const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
|
|
@@ -75,31 +70,42 @@ const Metric = (props: MetricProps) => {
|
|
const colorMap = useMemo(() => {
|
|
const colorMap = useMemo(() => {
|
|
try {
|
|
try {
|
|
const obj = props.colorMap ? JSON.parse(props.colorMap) : null;
|
|
const obj = props.colorMap ? JSON.parse(props.colorMap) : null;
|
|
- if (obj && typeof obj === 'object') {
|
|
|
|
|
|
+ if (obj && typeof obj === "object") {
|
|
const keys = Object.keys(obj);
|
|
const keys = Object.keys(obj);
|
|
- return keys.sort((a, b) => Number(a) - Number(b)).map((key, index) => {
|
|
|
|
- const nextKey = keys[index + 1] !== undefined ? Number(keys[index + 1]) : props.max || 100;
|
|
|
|
- return {range: [Number(key), nextKey], color: obj[key]};
|
|
|
|
- }).filter(item => item.color !== null)
|
|
|
|
|
|
+ return keys
|
|
|
|
+ .sort((a, b) => Number(a) - Number(b))
|
|
|
|
+ .map((key, index) => {
|
|
|
|
+ const nextKey = keys[index + 1] !== undefined ? Number(keys[index + 1]) : props.max || 100;
|
|
|
|
+ return { range: [Number(key), nextKey], color: obj[key] };
|
|
|
|
+ })
|
|
|
|
+ .filter((item) => item.color !== null);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
} catch (e) {
|
|
console.info(`Error parsing color_map value (metric).\n${(e as Error).message || e}`);
|
|
console.info(`Error parsing color_map value (metric).\n${(e as Error).message || e}`);
|
|
}
|
|
}
|
|
return undefined;
|
|
return undefined;
|
|
- }, [props.colorMap, props.max])
|
|
|
|
|
|
+ }, [props.colorMap, props.max]);
|
|
|
|
|
|
const data = useMemo(() => {
|
|
const data = useMemo(() => {
|
|
- const mode = (props.type === "none") ? [] : ["gauge"];
|
|
|
|
|
|
+ const mode = props.type === "none" ? [] : ["gauge"];
|
|
showValue && mode.push("number");
|
|
showValue && mode.push("number");
|
|
- (delta !== undefined) && mode.push("delta");
|
|
|
|
- const deltaIncreasing = deltaColor ? {
|
|
|
|
- color: deltaColor == "invert" ? "#FF4136" : deltaColor } : undefined
|
|
|
|
- const deltaDecreasing = deltaColor == "invert" ? {
|
|
|
|
- color: "#3D9970"
|
|
|
|
- } : negativeDeltaColor ? { color: negativeDeltaColor } : undefined;
|
|
|
|
|
|
+ delta !== undefined && mode.push("delta");
|
|
|
|
+ const deltaIncreasing = deltaColor
|
|
|
|
+ ? {
|
|
|
|
+ color: deltaColor == "invert" ? "#FF4136" : deltaColor,
|
|
|
|
+ }
|
|
|
|
+ : undefined;
|
|
|
|
+ const deltaDecreasing =
|
|
|
|
+ deltaColor == "invert"
|
|
|
|
+ ? {
|
|
|
|
+ color: "#3D9970",
|
|
|
|
+ }
|
|
|
|
+ : negativeDeltaColor
|
|
|
|
+ ? { color: negativeDeltaColor }
|
|
|
|
+ : undefined;
|
|
return [
|
|
return [
|
|
{
|
|
{
|
|
- domain: {x: [0, 1], y: [0, 1]},
|
|
|
|
|
|
+ domain: { x: [0, 1], y: [0, 1] },
|
|
value: value,
|
|
value: value,
|
|
type: "indicator",
|
|
type: "indicator",
|
|
mode: mode.join("+"),
|
|
mode: mode.join("+"),
|
|
@@ -109,58 +115,58 @@ const Metric = (props: MetricProps) => {
|
|
valueformat: sprintfToD3Converter(props.format),
|
|
valueformat: sprintfToD3Converter(props.format),
|
|
},
|
|
},
|
|
delta: {
|
|
delta: {
|
|
- reference: typeof value === 'number' && typeof delta === 'number' ? value - delta : undefined,
|
|
|
|
|
|
+ reference: typeof value === "number" && typeof delta === "number" ? value - delta : undefined,
|
|
prefix: extractPrefix(props.deltaFormat),
|
|
prefix: extractPrefix(props.deltaFormat),
|
|
suffix: extractSuffix(props.deltaFormat),
|
|
suffix: extractSuffix(props.deltaFormat),
|
|
valueformat: sprintfToD3Converter(props.deltaFormat),
|
|
valueformat: sprintfToD3Converter(props.deltaFormat),
|
|
increasing: deltaIncreasing,
|
|
increasing: deltaIncreasing,
|
|
- decreasing: deltaDecreasing
|
|
|
|
-
|
|
|
|
|
|
+ decreasing: deltaDecreasing,
|
|
} as Partial<Delta>,
|
|
} as Partial<Delta>,
|
|
gauge: {
|
|
gauge: {
|
|
axis: {
|
|
axis: {
|
|
- range: [
|
|
|
|
- props.min || 0,
|
|
|
|
- props.max || 100
|
|
|
|
- ]
|
|
|
|
|
|
+ range: [props.min || 0, props.max || 100],
|
|
|
|
+ },
|
|
|
|
+ bar: {
|
|
|
|
+ color: props.barColor,
|
|
},
|
|
},
|
|
steps: colorMap,
|
|
steps: colorMap,
|
|
shape: props.type === "linear" ? "bullet" : "angular",
|
|
shape: props.type === "linear" ? "bullet" : "angular",
|
|
threshold: {
|
|
threshold: {
|
|
- line: {color: "red", width: 4},
|
|
|
|
|
|
+ line: { color: "red", width: 4 },
|
|
thickness: 0.75,
|
|
thickness: 0.75,
|
|
- value: threshold
|
|
|
|
- }
|
|
|
|
|
|
+ value: threshold,
|
|
|
|
+ },
|
|
},
|
|
},
|
|
- }
|
|
|
|
|
|
+ },
|
|
] as Data[];
|
|
] as Data[];
|
|
}, [
|
|
}, [
|
|
- props.format,
|
|
|
|
- props.deltaFormat,
|
|
|
|
|
|
+ value,
|
|
|
|
+ delta,
|
|
|
|
+ props.type,
|
|
props.min,
|
|
props.min,
|
|
props.max,
|
|
props.max,
|
|
- props.type,
|
|
|
|
- value,
|
|
|
|
- showValue,
|
|
|
|
deltaColor,
|
|
deltaColor,
|
|
negativeDeltaColor,
|
|
negativeDeltaColor,
|
|
- delta,
|
|
|
|
threshold,
|
|
threshold,
|
|
- colorMap
|
|
|
|
|
|
+ props.format,
|
|
|
|
+ props.deltaFormat,
|
|
|
|
+ props.barColor,
|
|
|
|
+ showValue,
|
|
|
|
+ colorMap,
|
|
]);
|
|
]);
|
|
|
|
|
|
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),
|
|
[height, width]
|
|
[height, width]
|
|
);
|
|
);
|
|
|
|
|
|
- const skelStyle = useMemo(() => ({...style, minHeight: "7em"}), [style]);
|
|
|
|
|
|
+ const skelStyle = useMemo(() => ({ ...style, minHeight: "7em" }), [style]);
|
|
|
|
|
|
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);
|
|
@@ -170,7 +176,7 @@ const Metric = (props: MetricProps) => {
|
|
? JSON.parse(props.template_Dark_)
|
|
? JSON.parse(props.template_Dark_)
|
|
: darkTemplate
|
|
: darkTemplate
|
|
: 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 Metric.template\n${(e as Error).message || e}`);
|
|
console.info(`Error while parsing Metric.template\n${(e as Error).message || e}`);
|
|
}
|
|
}
|
|
@@ -183,34 +189,23 @@ const Metric = (props: MetricProps) => {
|
|
}
|
|
}
|
|
|
|
|
|
return layout as Partial<Layout>;
|
|
return layout as Partial<Layout>;
|
|
- }, [
|
|
|
|
- props.title,
|
|
|
|
- props.template,
|
|
|
|
- props.template_Dark_,
|
|
|
|
- props.template_Light_,
|
|
|
|
- theme.palette.mode,
|
|
|
|
- baseLayout,
|
|
|
|
- ])
|
|
|
|
|
|
+ }, [props.title, props.template, props.template_Dark_, props.template_Light_, theme.palette.mode, baseLayout]);
|
|
|
|
|
|
|
|
+ const plotConfig = {displaylogo: false}
|
|
return (
|
|
return (
|
|
<Tooltip title={hover || ""}>
|
|
<Tooltip title={hover || ""}>
|
|
<Box data-testid={props.testId} className={className}>
|
|
<Box data-testid={props.testId} className={className}>
|
|
- <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle}/>}>
|
|
|
|
- <Plot
|
|
|
|
- data={data}
|
|
|
|
- layout={layout}
|
|
|
|
- style={style}
|
|
|
|
- useResizeHandler
|
|
|
|
- />
|
|
|
|
|
|
+ <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle} />}>
|
|
|
|
+ <Plot data={data} layout={layout} style={style} config={plotConfig} useResizeHandler />
|
|
</Suspense>
|
|
</Suspense>
|
|
</Box>
|
|
</Box>
|
|
</Tooltip>
|
|
</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,
|
|
@@ -218,4 +213,4 @@ const darkTemplate = {
|
|
font,
|
|
font,
|
|
paper_bgcolor: "rgb(31,47,68)",
|
|
paper_bgcolor: "rgb(31,47,68)",
|
|
},
|
|
},
|
|
-}
|
|
|
|
|
|
+};
|