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