import React, { Component } from 'react'; import { Paper, Typography, Box, CircularProgress } from '@mui/material'; import { LineChart } from '@mui/x-charts/LineChart'; export default class Chart extends Component { constructor(props) { super(props); this.state = { data: [], loading: true }; this.intervalId = null; } componentDidMount() { this.fetchData(); this.intervalId = setInterval(this.fetchData, 60000); } componentDidUpdate(prevProps) { // Compare props to see if we need to refetch const prevEffective = this.getEffectiveChannels(prevProps); const currEffective = this.getEffectiveChannels(this.props); if (prevEffective.join(',') !== currEffective.join(',')) { this.fetchData(); } } componentWillUnmount() { if (this.intervalId) { clearInterval(this.intervalId); } } getEffectiveChannels(props) { return props.channelConfig ? props.channelConfig.map(c => c.id) : props.selectedChannels; } fetchData = () => { const effectiveChannels = this.getEffectiveChannels(this.props); // Only fetch if selection exists if (effectiveChannels.length === 0) { this.setState({ data: [], loading: false }); return; } const selectionStr = effectiveChannels.join(','); const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); fetch(`/api/readings?selection=${encodeURIComponent(selectionStr)}&since=${since}`) .then(res => res.json()) .then(rows => { const timeMap = new Map(); rows.forEach(row => { const id = `${row.device}:${row.channel}`; if (!effectiveChannels.includes(id)) return; const time = new Date(row.timestamp).getTime(); if (!timeMap.has(time)) { timeMap.set(time, { time: new Date(row.timestamp) }); } const entry = timeMap.get(time); let val = row.value; if (row.data_type === 'json' && !val) { val = null; } entry[id] = val; }); const sortedData = Array.from(timeMap.values()).sort((a, b) => a.time - b.time); this.setState({ data: sortedData, loading: false }); }) .catch(err => { console.error("Failed to fetch data", err); this.setState({ loading: false }); }); }; computeAxisLimits(axisKey, effectiveChannels, series) { // Collect all data points for this axis let axisMin = Infinity; let axisMax = -Infinity; const axisSeries = series.filter(s => s.yAxisKey === axisKey).map(s => s.dataKey); if (axisSeries.length === 0) return {}; // No data for this axis // Check if config exists for this axis const { axisConfig } = this.props; let cfgMin = NaN; let cfgMax = NaN; if (axisConfig && axisConfig[axisKey]) { cfgMin = parseFloat(axisConfig[axisKey].min); cfgMax = parseFloat(axisConfig[axisKey].max); } // Optimization: If no config set, just return empty and let chart autoscale fully. if (isNaN(cfgMin) && isNaN(cfgMax)) return {}; // Calculate data bounds let hasData = false; this.state.data.forEach(row => { axisSeries.forEach(key => { const val = row[key]; if (val !== null && val !== undefined) { hasData = true; if (val < axisMin) axisMin = val; if (val > axisMax) axisMax = val; } }); }); if (!hasData) return {}; // No valid data points // Apply config soft limits if (!isNaN(cfgMin)) axisMin = Math.min(axisMin, cfgMin); if (!isNaN(cfgMax)) axisMax = Math.max(axisMax, cfgMax); return { min: axisMin, max: axisMax }; } render() { const { loading, data } = this.state; const { channelConfig } = this.props; const effectiveChannels = this.getEffectiveChannels(this.props); if (loading) return ; if (effectiveChannels.length === 0) return No channels selected.; const series = effectiveChannels.map(id => { // Find alias and axis if config exists let label = id; let yAxisKey = 'left'; if (channelConfig) { const item = channelConfig.find(c => c.id === id); if (item) { if (item.alias) label = item.alias; if (item.yAxis) yAxisKey = item.yAxis; } } return { dataKey: id, label: label, connectNulls: true, showMark: false, yAxisKey: yAxisKey, }; }); const hasRightAxis = series.some(s => s.yAxisKey === 'right'); const leftLimits = this.computeAxisLimits('left', effectiveChannels, series); const rightLimits = this.computeAxisLimits('right', effectiveChannels, series); const yAxes = [ { id: 'left', scaleType: 'linear', ...leftLimits } ]; if (hasRightAxis) { yAxes.push({ id: 'right', scaleType: 'linear', ...rightLimits }); } return ( Last 24 Hours date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }]} yAxis={yAxes} rightAxis={hasRightAxis ? 'right' : null} slotProps={{ legend: { direction: 'row', position: { vertical: 'top', horizontal: 'middle' }, padding: 0, }, }} /> ); } }