github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/apps/sys.monitor/site.main.src/src/charts/TimeSeriesChart.js (about) 1 /* 2 * Copyright (c) 2022-present unTill Pro, Ltd. 3 */ 4 5 import { Box } from '@mui/material'; 6 import * as React from 'react'; 7 import { useState, useEffect } from 'react'; 8 import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Tooltip } from 'recharts'; 9 import { Error, useDataProvider, useTranslate } from 'react-admin'; 10 import MonCard from '../elements/MonCard'; 11 import Switch from '@mui/material/Switch'; 12 import Typography from '@mui/material/Typography'; 13 import { useSelector, useDispatch } from 'react-redux' 14 import { toggleItem, ALL } from '../features/filters/filtersSlice' 15 import { FormatValue } from '../utils/Units'; 16 import { ResMetrics } from '../data/Resources'; 17 import { Bars } from 'svg-loaders-react' 18 19 // https://www.heavy.ai/blog/12-color-palettes-for-telling-better-stories-with-your-data 20 export const PaletteSpringPastels = ["#fd7f6f", "#7eb0d5", "#b2e061", "#bd7ebe", "#ffb55a", "#ffee65", "#beb9db", "#fdcce5", "#8bd3c7"] 21 export const PaletteDutchField = ["#e60049", "#0bb4ff", "#50e991", "#e6d800", "#9b19f5", "#ffa300", "#dc0ab4", "#b3d4ff", "#00bfa0"] 22 export const PaletteStatusCodes = ["#7eb0d5", "#ffb55a", "#fd7f6f", "#e60049"] 23 export const LayoutLegendWidth = "300px" 24 25 26 /* 27 props: 28 - caption 29 - showAll 30 - data: [ 31 { 32 name: 'Serie 1' 33 data: [ 34 {x: '10:11:12', value: 34}, 35 ... 36 ] 37 }, ... 38 ] 39 - aggs: ['avg'] 40 - path (to serialize filters) 41 - units: 42 - percent 43 - bps (bytes per second) 44 45 */ 46 47 function calcAgg(agg, entry, data, units, loading) { 48 var v = 0 49 if (loading) { 50 return 0 51 } 52 if (agg === "avg") { 53 data.forEach(e => { 54 v += e[entry.id] 55 }) 56 v = v/data.length 57 } 58 if (agg === "sum") { 59 data.forEach(e => { 60 v += e[entry.id] 61 }) 62 } 63 64 return FormatValue(units, v) 65 } 66 67 const TimeSeriesChart = (props) => { 68 69 const meta = props.meta 70 const dataProvider = useDataProvider() 71 72 73 const filter = useSelector((state) => state.filters.items[props.path] || [ALL]) 74 const appInterval = useSelector((state) => state.app.interval) 75 76 const [loading, setLoading] = useState(true); 77 const [error, setError] = useState(); 78 const [metrics, setMetrics] = useState(); 79 const [interval, setInterval] = useState(appInterval); 80 81 const reload = (intv) => { 82 const qmeta = meta.query 83 qmeta.interval = intv 84 dataProvider.getMany(ResMetrics, {meta: qmeta}) 85 .then(({ data }) => { 86 setMetrics(data); 87 setLoading(false); 88 }) 89 .catch(error => { 90 setError(error); 91 setLoading(false); 92 }) 93 } 94 95 useEffect(() => { 96 reload(interval) 97 }, []); 98 99 if (appInterval != interval) { // Interval has changed 100 setInterval(appInterval) 101 setLoading(true) 102 setError(false) 103 reload(appInterval); 104 } 105 106 const dispatch = useDispatch() 107 const translate = useTranslate(); 108 109 if (error) { 110 return ( 111 <MonCard caption={props.caption}> 112 <Error /> 113 </MonCard> 114 ) 115 } 116 117 const data = loading?null:meta.transform(metrics) 118 const colors = props.palette || PaletteSpringPastels 119 120 const hdrStyle = (props.aggs && props.aggs.length>0) ? { 121 borderRight: '1px solid #ccc', 122 paddingRight: '.7em', 123 }:{} 124 const cellStyle = (props.aggs && props.aggs.length>0) ? { 125 paddingRight: '.7em', 126 }:{} 127 const hdrStyle2 = (props.aggs && props.aggs.length>0) ? { 128 paddingLeft: '.5em', 129 }:{} 130 131 const filters = meta.dataKeys.flatMap(v => v.id) 132 var moment = require('moment'); 133 134 return ( 135 <MonCard caption={props.caption} noframe={props.noframe}> 136 <Box display="flex"> 137 {!props.nolegend?( 138 <Box sx={{flexBasis: LayoutLegendWidth, flexShrink: 0, flexGrow: 0}}> 139 <table cellPadding={1}><tbody> 140 {props.showAll?( 141 <tr key={"tr0"}> 142 <td><Switch size="small" disabled={loading} checked={filter.includes(ALL)} onChange={() => {dispatch(toggleItem({target: props.path, item: ALL, values: filters}))}} /></td> 143 <td style={hdrStyle}><Typography color="info" whiteSpace={'nowrap'}>{translate('common.showAll')}</Typography></td> 144 {props.aggs.map((agg, index) => ( 145 <td key={`tr0td${index}`} style={hdrStyle2}><Typography whiteSpace={'nowrap'} color="info">{agg}</Typography></td> 146 ))} 147 </tr>):""} 148 {meta.dataKeys.map((entry, index) => { 149 const serieId = entry.id 150 return ( 151 <tr key={`tr${index}`}> 152 <td><Switch size="small" checked={filter.includes(ALL) || filter.includes(serieId)} onChange={() => { 153 dispatch(toggleItem({target: props.path, item: serieId, values: filters})) 154 }}/></td> 155 <td style={cellStyle}><Typography whiteSpace={'nowrap'} color={colors[index]}>{entry.name}</Typography></td> 156 { 157 props.aggs.map((agg, i2) => ( 158 <td key={`tr${index}td${i2}`} style={hdrStyle2}><Typography whiteSpace={'nowrap'} color={colors[index]}>{calcAgg(agg, entry, data, props.units, loading)}</Typography></td> 159 )) 160 } 161 </tr> 162 ) 163 })} 164 </tbody></table> 165 </Box>):""} 166 {!props.nochart?( 167 <Box sx={{flex: 1}}> 168 169 {loading?( 170 <Box width="100%" height={props.height} sx={{ border: '1px solid #ccc' }} display='flex' alignItems={'center'} justifyContent={'center'}> 171 <Bars stroke={PaletteSpringPastels[1]} fill={PaletteSpringPastels[1]} width="60"/> 172 </Box> 173 ):( 174 <ResponsiveContainer width="95%" aspect={props.aspect} height={props.height}> 175 <LineChart data={data} style={{alignSelf: "center"}} width={500} height={200} margin={{ top: 5, right: 20, bottom: 5, left: 10 }}> 176 {meta.dataKeys.map((entry, index) => { 177 if (filter.includes(entry.id) || filter.includes(ALL)) { 178 return( 179 <Line key={`line${index}`} dot={false} isAnimationActive={false} name={entry.name} type="monotone" dataKey={entry.id} stroke={colors[index]} activeDot={{ r: 8 }} /> 180 ) 181 } 182 return "" 183 })} 184 <CartesianGrid stroke="#ccc" strokeDasharray="3 3"/> 185 <Tooltip 186 labelFormatter={(lbl) => {return moment(lbl).format('HH:mm:ss')}} 187 formatter={(v) => {return FormatValue(props.units, v)}} /> 188 <XAxis dataKey="x" type='number' scale='time' domain = {['dataMin', 'dataMax']} tickFormatter = {(unixTime) => moment(unixTime).format('HH:mm:ss')} /> 189 <YAxis tickFormatter={(v) => {return FormatValue(props.units, v)}} /> 190 </LineChart> 191 </ResponsiveContainer> 192 )} 193 </Box> 194 ):""} 195 </Box> 196 {props.footer} 197 </MonCard> 198 ) 199 }; 200 201 export default TimeSeriesChart 202 // <XAxis dataKey="x" tickFormatter={timeStr => moment(timeStr).format('HH:mm')} allowDuplicatedCategory={false} interval={20} /> 203