Metric.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /*
  2. * Copyright 2021-2024 Avaiga Private Limited
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  5. * the License. You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  10. * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  11. * specific language governing permissions and limitations under the License.
  12. */
  13. import React, { CSSProperties, lazy, Suspense, useMemo } from "react";
  14. import { Data, Delta, Layout } from "plotly.js";
  15. import Box from "@mui/material/Box";
  16. import Skeleton from "@mui/material/Skeleton";
  17. import Tooltip from "@mui/material/Tooltip";
  18. import { useTheme } from "@mui/material";
  19. import { useClassNames, useDynamicJsonProperty, useDynamicProperty } from "../../utils/hooks";
  20. import { extractPrefix, extractSuffix, sprintfToD3Converter } from "../../utils/formatConversion";
  21. import { TaipyBaseProps, TaipyHoverProps } from "./utils";
  22. import { darkThemeTemplate } from "../../themes/darkThemeTemplate";
  23. const Plot = lazy(() => import("react-plotly.js"));
  24. interface MetricProps extends TaipyBaseProps, TaipyHoverProps {
  25. value?: number;
  26. defaultValue?: number;
  27. delta?: number;
  28. defaultDelta?: number;
  29. type?: string;
  30. min?: number;
  31. max?: number;
  32. deltaColor?: string;
  33. negativeDeltaColor?: string;
  34. threshold?: number;
  35. defaultThreshold?: number;
  36. format?: string;
  37. deltaFormat?: string;
  38. barColor?: string;
  39. showValue?: boolean;
  40. colorMap?: string;
  41. title?: string;
  42. testId?: string;
  43. layout?: string;
  44. defaultLayout?: string;
  45. style?: string;
  46. defaultStyle?: string;
  47. width?: string | number;
  48. height?: string | number;
  49. template?: string;
  50. template_Dark_?: string;
  51. template_Light_?: string;
  52. }
  53. const emptyLayout = {} as Partial<Layout>;
  54. const defaultStyle = { position: "relative", display: "inline-block" };
  55. const Metric = (props: MetricProps) => {
  56. const { width = "100%", height, showValue = true, deltaColor, negativeDeltaColor } = props;
  57. const value = useDynamicProperty(props.value, props.defaultValue, 0);
  58. const threshold = useDynamicProperty(props.threshold, props.defaultThreshold, undefined);
  59. const delta = useDynamicProperty(props.delta, props.defaultDelta, undefined);
  60. const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
  61. const baseLayout = useDynamicJsonProperty(props.layout, props.defaultLayout || "", emptyLayout);
  62. const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
  63. const theme = useTheme();
  64. const colorMap = useMemo(() => {
  65. try {
  66. const obj = props.colorMap ? JSON.parse(props.colorMap) : null;
  67. if (obj && typeof obj === "object") {
  68. const keys = Object.keys(obj);
  69. return keys
  70. .sort((a, b) => Number(a) - Number(b))
  71. .map((key, index) => {
  72. const nextKey = keys[index + 1] !== undefined ? Number(keys[index + 1]) : props.max || 100;
  73. return { range: [Number(key), nextKey], color: obj[key] };
  74. })
  75. .filter((item) => item.color !== null);
  76. }
  77. } catch (e) {
  78. console.info(`Error parsing color_map value (metric).\n${(e as Error).message || e}`);
  79. }
  80. return undefined;
  81. }, [props.colorMap, props.max]);
  82. const data = useMemo(() => {
  83. const mode = props.type === "none" ? [] : ["gauge"];
  84. showValue && mode.push("number");
  85. delta !== undefined && mode.push("delta");
  86. const deltaIncreasing = deltaColor
  87. ? {
  88. color: deltaColor == "invert" ? "#FF4136" : deltaColor,
  89. }
  90. : undefined;
  91. const deltaDecreasing =
  92. deltaColor == "invert"
  93. ? {
  94. color: "#3D9970",
  95. }
  96. : negativeDeltaColor
  97. ? { color: negativeDeltaColor }
  98. : undefined;
  99. return [
  100. {
  101. domain: { x: [0, 1], y: [0, 1] },
  102. value: value,
  103. type: "indicator",
  104. mode: mode.join("+"),
  105. number: {
  106. prefix: extractPrefix(props.format),
  107. suffix: extractSuffix(props.format),
  108. valueformat: sprintfToD3Converter(props.format),
  109. },
  110. delta: {
  111. reference: typeof value === "number" && typeof delta === "number" ? value - delta : undefined,
  112. prefix: extractPrefix(props.deltaFormat),
  113. suffix: extractSuffix(props.deltaFormat),
  114. valueformat: sprintfToD3Converter(props.deltaFormat),
  115. increasing: deltaIncreasing,
  116. decreasing: deltaDecreasing,
  117. } as Partial<Delta>,
  118. gauge: {
  119. axis: {
  120. range: [props.min || 0, props.max || 100],
  121. },
  122. bar: {
  123. color: props.barColor,
  124. },
  125. steps: colorMap,
  126. shape: props.type === "linear" ? "bullet" : "angular",
  127. threshold: {
  128. line: { color: "red", width: 4 },
  129. thickness: 0.75,
  130. value: threshold,
  131. },
  132. },
  133. },
  134. ] as Data[];
  135. }, [
  136. value,
  137. delta,
  138. props.type,
  139. props.min,
  140. props.max,
  141. deltaColor,
  142. negativeDeltaColor,
  143. threshold,
  144. props.format,
  145. props.deltaFormat,
  146. props.barColor,
  147. showValue,
  148. colorMap,
  149. ]);
  150. const style = useMemo(
  151. () =>
  152. height === undefined
  153. ? ({ ...defaultStyle, width: width } as CSSProperties)
  154. : ({ ...defaultStyle, width: width, height: height } as CSSProperties),
  155. [height, width]
  156. );
  157. const skelStyle = useMemo(() => ({ ...style, minHeight: "7em" }), [style]);
  158. const layout = useMemo(() => {
  159. const layout = { ...baseLayout };
  160. let template = undefined;
  161. try {
  162. const tpl = props.template && JSON.parse(props.template);
  163. const tplTheme =
  164. theme.palette.mode === "dark"
  165. ? props.template_Dark_
  166. ? JSON.parse(props.template_Dark_)
  167. : darkTemplate
  168. : props.template_Light_ && JSON.parse(props.template_Light_);
  169. template = tpl ? (tplTheme ? { ...tpl, ...tplTheme } : tpl) : tplTheme ? tplTheme : undefined;
  170. } catch (e) {
  171. console.info(`Error while parsing Metric.template\n${(e as Error).message || e}`);
  172. }
  173. if (template) {
  174. layout.template = template;
  175. }
  176. if (props.title) {
  177. layout.title = props.title;
  178. }
  179. return layout as Partial<Layout>;
  180. }, [props.title, props.template, props.template_Dark_, props.template_Light_, theme.palette.mode, baseLayout]);
  181. const plotConfig = {displaylogo: false}
  182. return (
  183. <Tooltip title={hover || ""}>
  184. <Box data-testid={props.testId} className={className}>
  185. <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle} />}>
  186. <Plot data={data} layout={layout} style={style} config={plotConfig} useResizeHandler />
  187. </Suspense>
  188. </Box>
  189. </Tooltip>
  190. );
  191. };
  192. export default Metric;
  193. const { colorscale, colorway, font } = darkThemeTemplate.layout;
  194. const darkTemplate = {
  195. layout: {
  196. colorscale,
  197. colorway,
  198. font,
  199. paper_bgcolor: "rgb(31,47,68)",
  200. },
  201. };