Metric.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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} 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. type?: string
  26. min?: number
  27. max?: number
  28. value?: number
  29. defaultValue?: number
  30. delta?: number
  31. defaultDelta?: number
  32. threshold?: number
  33. defaultThreshold?: number
  34. testId?: string
  35. defaultLayout?: string;
  36. layout?: string;
  37. defaultStyle?: string;
  38. style?: string;
  39. width?: string | number;
  40. height?: string | number;
  41. showValue?: boolean;
  42. format?: string;
  43. deltaFormat?: string;
  44. colorMap?: string;
  45. template?: string;
  46. template_Dark_?: string;
  47. template_Light_?: string;
  48. }
  49. const emptyLayout = {} as Record<string, Record<string, unknown>>;
  50. const defaultStyle = {position: "relative", display: "inline-block"};
  51. const Metric = (props: MetricProps) => {
  52. const {
  53. width = "100%",
  54. height,
  55. showValue = true
  56. } = 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.sort((a, b) => Number(a) - Number(b)).map((key, index) => {
  70. const nextKey = keys[index + 1] !== undefined ? Number(keys[index + 1]) : props.max || 100;
  71. return {range: [Number(key), nextKey], color: obj[key]};
  72. }).filter(item => item.color !== null)
  73. }
  74. } catch (e) {
  75. console.info(`Error parsing color_map value (metric).\n${(e as Error).message || e}`);
  76. }
  77. return undefined;
  78. }, [props.colorMap, props.max])
  79. const data = useMemo(() => {
  80. const mode = (props.type === "none") ? [] : ["gauge"];
  81. showValue && mode.push("number");
  82. (delta !== undefined) && mode.push("delta");
  83. return [
  84. {
  85. domain: {x: [0, 1], y: [0, 1]},
  86. value: value,
  87. type: "indicator",
  88. mode: mode.join("+"),
  89. number: {
  90. prefix: extractPrefix(props.format),
  91. suffix: extractSuffix(props.format),
  92. valueformat: sprintfToD3Converter(props.format),
  93. },
  94. delta: {
  95. reference: typeof value === 'number' && typeof delta === 'number' ? value - delta : undefined,
  96. prefix: extractPrefix(props.deltaFormat),
  97. suffix: extractSuffix(props.deltaFormat),
  98. valueformat: sprintfToD3Converter(props.deltaFormat)
  99. },
  100. gauge: {
  101. axis: {
  102. range: [
  103. props.min || 0,
  104. props.max || 100
  105. ]
  106. },
  107. steps: colorMap,
  108. shape: props.type === "linear" ? "bullet" : "angular",
  109. threshold: {
  110. line: {color: "red", width: 4},
  111. thickness: 0.75,
  112. value: threshold
  113. }
  114. },
  115. }
  116. ];
  117. }, [
  118. props.format,
  119. props.deltaFormat,
  120. props.min,
  121. props.max,
  122. props.type,
  123. value,
  124. showValue,
  125. delta,
  126. threshold,
  127. colorMap
  128. ]);
  129. const style = useMemo(
  130. () =>
  131. height === undefined
  132. ? ({...defaultStyle, width: width} as CSSProperties)
  133. : ({...defaultStyle, width: width, height: height} as CSSProperties),
  134. [height, width]
  135. );
  136. const skelStyle = useMemo(() => ({...style, minHeight: "7em"}), [style]);
  137. const layout = useMemo(() => {
  138. const layout = {...baseLayout};
  139. let template = undefined;
  140. try {
  141. const tpl = props.template && JSON.parse(props.template);
  142. const tplTheme =
  143. theme.palette.mode === "dark"
  144. ? props.template_Dark_
  145. ? JSON.parse(props.template_Dark_)
  146. : darkTemplate
  147. : props.template_Light_ && JSON.parse(props.template_Light_);
  148. template = tpl ? (tplTheme ? {...tpl, ...tplTheme} : tpl) : tplTheme ? tplTheme : undefined;
  149. } catch (e) {
  150. console.info(`Error while parsing Metric.template\n${(e as Error).message || e}`);
  151. }
  152. if (template) {
  153. layout.template = template;
  154. }
  155. return layout
  156. }, [
  157. props.template,
  158. props.template_Dark_,
  159. props.template_Light_,
  160. theme.palette.mode,
  161. baseLayout
  162. ])
  163. return (
  164. <Box data-testid={props.testId} className={className}>
  165. <Tooltip title={hover || ""}>
  166. <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle}/>}>
  167. <Plot
  168. data={data as Data[]}
  169. layout={layout}
  170. style={style}
  171. useResizeHandler
  172. />
  173. </Suspense>
  174. </Tooltip>
  175. </Box>
  176. );
  177. }
  178. export default Metric;
  179. const {colorscale, colorway, font} = darkThemeTemplate.layout;
  180. const darkTemplate = {
  181. layout: {
  182. colorscale,
  183. colorway,
  184. font,
  185. paper_bgcolor: "rgb(31,47,68)",
  186. },
  187. }