This commit is contained in:
sebseb7
2025-12-26 01:05:43 +01:00
parent d586d12e68
commit 3d43a42b12
2 changed files with 104 additions and 32 deletions

View File

@@ -113,9 +113,12 @@ export default class Chart extends Component {
this.state = {
data: [],
loading: true,
hiddenSeries: {} // { seriesId: true/false }
hiddenSeries: {}, // { seriesId: true/false }
lastValues: {}, // { channelId: lastValue } - for detecting changes
flashStates: {} // { channelId: 'up' | 'down' | null } - for flash animation
};
this.interval = null;
this.flashTimeouts = {}; // Store timeouts to clear flash states
}
componentDidMount() {
@@ -156,6 +159,8 @@ export default class Chart extends Component {
if (this.interval) {
clearInterval(this.interval);
}
// Clear any pending flash timeouts
Object.values(this.flashTimeouts).forEach(timeout => clearTimeout(timeout));
}
getEffectiveChannels(props) {
@@ -189,6 +194,16 @@ export default class Chart extends Component {
fetch(`/api/readings?selection=${encodeURIComponent(selectionStr)}&since=${since}&until=${until}`)
.then(res => res.json())
.then(dataObj => {
// Safety check: ensure dataObj is a valid object
if (!dataObj || typeof dataObj !== 'object') {
console.error('Invalid data received from API:', dataObj);
this.setState({ data: [], loading: false });
return;
}
// Recalculate effective channels inside callback (closure fix)
const channelList = this.getEffectiveChannels(this.props);
// 1. Parse raw rows into intervals per channel
const intervals = [];
const timestampsSet = new Set();
@@ -196,7 +211,10 @@ export default class Chart extends Component {
// dataObj format: { "device:channel": [ [timestamp, value, until], ... ] }
Object.entries(dataObj).forEach(([id, points]) => {
// Check if this ID is in our effective/requested list
if (!effectiveChannels.includes(id)) return;
if (!channelList || !channelList.includes(id)) return;
// Skip if points is not a valid array
if (!Array.isArray(points)) return;
// Ensure sorted by time
points.sort((a, b) => new Date(a[0]) - new Date(b[0]));
@@ -274,6 +292,12 @@ export default class Chart extends Component {
row[inv.id] = inv.val;
}
});
// Ensure all channel values are numbers or null (MUI-X requirement)
channelList.forEach(ch => {
if (row[ch] !== null && (typeof row[ch] !== 'number' || !Number.isFinite(row[ch]))) {
row[ch] = null;
}
});
return row;
});
@@ -289,7 +313,45 @@ export default class Chart extends Component {
});
}
this.setState({ data: processedData, loading: false });
// 5. Detect value changes for flash animation
const effectiveChannels = this.getEffectiveChannels(this.props);
const newLastValues = {};
const newFlashStates = { ...this.state.flashStates };
// Get latest value for each channel (last row)
if (processedData.length > 0) {
const lastRow = processedData[processedData.length - 1];
effectiveChannels.forEach(channelId => {
const newVal = lastRow[channelId];
if (newVal !== null && newVal !== undefined) {
newLastValues[channelId] = newVal;
const oldVal = this.state.lastValues[channelId];
// Only flash if we had a previous value and it changed
if (oldVal !== undefined && oldVal !== newVal) {
const direction = newVal > oldVal ? 'up' : 'down';
newFlashStates[channelId] = direction;
// Clear flash after 1 second
if (this.flashTimeouts[channelId]) {
clearTimeout(this.flashTimeouts[channelId]);
}
this.flashTimeouts[channelId] = setTimeout(() => {
this.setState(prev => ({
flashStates: { ...prev.flashStates, [channelId]: null }
}));
}, 1000);
}
}
});
}
this.setState({
data: processedData,
loading: false,
lastValues: newLastValues,
flashStates: newFlashStates
});
})
.catch(err => {
console.error(err);
@@ -350,7 +412,7 @@ export default class Chart extends Component {
};
render() {
const { loading, data, hiddenSeries } = this.state;
const { loading, data, hiddenSeries, flashStates } = this.state;
const { channelConfig, windowEnd, range } = this.props;
const effectiveChannels = this.getEffectiveChannels(this.props);
@@ -456,7 +518,10 @@ export default class Chart extends Component {
<Paper sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
{/* Custom Interactive Legend */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1.5, justifyContent: 'center', mb: 1, py: 0.5 }}>
{legendItems.map(item => (
{legendItems.map(item => {
const flash = flashStates[item.id];
const flashColor = flash === 'up' ? 'rgba(76, 175, 80, 0.4)' : flash === 'down' ? 'rgba(244, 67, 54, 0.4)' : 'transparent';
return (
<Box
key={item.id}
onClick={() => this.toggleSeries(item.id)}
@@ -467,8 +532,11 @@ export default class Chart extends Component {
cursor: 'pointer',
opacity: item.hidden ? 0.4 : 1,
textDecoration: item.hidden ? 'line-through' : 'none',
transition: 'opacity 0.2s',
transition: 'opacity 0.2s, background-color 0.3s',
userSelect: 'none',
backgroundColor: flashColor,
borderRadius: 1,
px: 0.5,
'&:hover': { opacity: item.hidden ? 0.6 : 0.8 },
}}
>
@@ -486,7 +554,8 @@ export default class Chart extends Component {
{item.label}
</Typography>
</Box>
))}
);
})}
</Box>
<Box sx={{ flexGrow: 1, width: '100%', height: '100%' }}>
<LineChart

View File

@@ -618,6 +618,9 @@ module.exports = {
historyApiFallback: true,
hot: true,
allowedHosts: 'all',
client: {
webSocketURL: 'auto://0.0.0.0:0/ws',
},
setupMiddlewares: (middlewares, devServer) => {
if (!devServer) {
throw new Error('webpack-dev-server is not defined');