This commit is contained in:
sebseb7
2025-12-25 02:25:25 +01:00
parent db4f27302b
commit a1793d0998
3 changed files with 70 additions and 13 deletions

View File

@@ -99,7 +99,9 @@ export default class Chart extends Component {
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i++) {
const [tsStr, rawVal, untilStr] = points[i]; const [tsStr, rawVal, untilStr] = points[i];
const val = Number(rawVal); const numVal = Number(rawVal);
// MUI-X charts only accepts numbers and null - NaN causes errors
const val = Number.isNaN(numVal) ? null : numVal;
let start = new Date(tsStr).getTime(); let start = new Date(tsStr).getTime();
let explicitEnd = untilStr ? new Date(untilStr).getTime() : null; let explicitEnd = untilStr ? new Date(untilStr).getTime() : null;
@@ -230,12 +232,14 @@ export default class Chart extends Component {
let label = id; let label = id;
let yAxisKey = 'left'; let yAxisKey = 'left';
let color = undefined; let color = undefined;
let fillColor = undefined;
if (channelConfig) { if (channelConfig) {
const item = channelConfig.find(c => c.id === id); const item = channelConfig.find(c => c.id === id);
if (item) { if (item) {
if (item.alias) label = item.alias; if (item.alias) label = item.alias;
if (item.yAxis) yAxisKey = item.yAxis; if (item.yAxis) yAxisKey = item.yAxis;
if (item.color) color = item.color; if (item.color) color = item.color;
if (item.fillColor) fillColor = item.fillColor;
} }
} }
@@ -247,6 +251,16 @@ export default class Chart extends Component {
yAxisKey: yAxisKey, yAxisKey: yAxisKey,
}; };
if (color) sObj.color = color; if (color) sObj.color = color;
// Enable area fill if fillColor is set (with 50% transparency)
if (fillColor) {
sObj.area = true;
// Convert hex to rgba with 50% opacity
const hex = fillColor.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
sObj.areaColor = `rgba(${r}, ${g}, ${b}, 0.5)`;
}
return sObj; return sObj;
}); });
@@ -289,6 +303,15 @@ export default class Chart extends Component {
position: { vertical: 'top', horizontal: 'middle' }, position: { vertical: 'top', horizontal: 'middle' },
padding: 0, padding: 0,
}, },
lineHighlight: { strokeWidth: 3 },
}}
sx={{
'& .MuiLineElement-root': {
strokeWidth: 3,
},
'& .MuiAreaElement-root': {
fillOpacity: 0.5,
},
}} }}
/> />
</Box> </Box>

View File

@@ -44,6 +44,7 @@ class ViewManager extends Component {
views: [], views: [],
open: false, open: false,
colorPickerOpen: false, colorPickerOpen: false,
colorPickerMode: 'line',
editingId: null, editingId: null,
viewName: '', viewName: '',
availableDevices: [], availableDevices: [],
@@ -183,7 +184,7 @@ class ViewManager extends Component {
const { user } = this.props; const { user } = this.props;
try { try {
await fetch('/api/views/reorder', { const res = await fetch('/api/views/reorder', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -191,6 +192,10 @@ class ViewManager extends Component {
}, },
body: JSON.stringify({ order }) body: JSON.stringify({ order })
}); });
if (!res.ok) {
const err = await res.json();
console.error("Failed to save order:", err);
}
} catch (err) { } catch (err) {
console.error("Failed to save order", err); console.error("Failed to save order", err);
} }
@@ -276,19 +281,33 @@ class ViewManager extends Component {
})); }));
}; };
openColorPicker = (idx) => { openColorPicker = (idx, mode = 'line') => {
this.setState({ colorPickerOpen: true, pickerTargetIndex: idx }); this.setState({ colorPickerOpen: true, pickerTargetIndex: idx, colorPickerMode: mode });
}; };
selectColor = (color) => { selectColor = (color) => {
const { pickerTargetIndex, viewConfig } = this.state; const { pickerTargetIndex, viewConfig, colorPickerMode } = this.state;
if (pickerTargetIndex !== null) { if (pickerTargetIndex !== null) {
const newConfig = [...viewConfig]; const newConfig = viewConfig.map((ch, i) => {
newConfig[pickerTargetIndex].color = color; if (i === pickerTargetIndex) {
if (colorPickerMode === 'fill') {
return { ...ch, fillColor: color };
} else {
return { ...ch, color: color };
}
}
return ch;
});
this.setState({ viewConfig: newConfig, colorPickerOpen: false, pickerTargetIndex: null }); this.setState({ viewConfig: newConfig, colorPickerOpen: false, pickerTargetIndex: null });
} }
}; };
clearFillColor = (idx) => {
const newConfig = [...this.state.viewConfig];
delete newConfig[idx].fillColor;
this.setState({ viewConfig: newConfig });
};
handleRangeChange = (e, newVal) => { handleRangeChange = (e, newVal) => {
if (newVal) this.setState({ rangeLabel: newVal }); if (newVal) this.setState({ rangeLabel: newVal });
}; };
@@ -371,7 +390,8 @@ class ViewManager extends Component {
id: `${c.device}:${c.channel}`, id: `${c.device}:${c.channel}`,
alias: c.alias, alias: c.alias,
yAxis: c.yAxis || 'left', yAxis: c.yAxis || 'left',
color: c.color color: c.color,
fillColor: c.fillColor
}))} }))}
axisConfig={axes} axisConfig={axes}
windowEnd={windowEnd} windowEnd={windowEnd}
@@ -413,9 +433,17 @@ class ViewManager extends Component {
<List dense> <List dense>
{viewConfig.map((ch, idx) => ( {viewConfig.map((ch, idx) => (
<ListItem key={idx} sx={{ pl: 0 }}> <ListItem key={idx} sx={{ pl: 0 }}>
<IconButton size="small" onClick={() => this.openColorPicker(idx)}> <IconButton size="small" onClick={() => this.openColorPicker(idx, 'line')} title="Line color">
<Box sx={{ width: 20, height: 20, bgcolor: ch.color || '#fff', borderRadius: '50%' }} /> <Box sx={{ width: 20, height: 20, bgcolor: ch.color || '#fff', borderRadius: '50%', border: '2px solid #fff' }} />
</IconButton> </IconButton>
<IconButton size="small" onClick={() => this.openColorPicker(idx, 'fill')} title="Fill color (area)">
<Box sx={{ width: 20, height: 20, bgcolor: ch.fillColor || 'transparent', borderRadius: '50%', border: ch.fillColor ? '2px solid #fff' : '2px dashed #666' }} />
</IconButton>
{ch.fillColor && (
<IconButton size="small" onClick={() => this.clearFillColor(idx)} title="Remove fill" sx={{ ml: -0.5 }}>
<DeleteIcon sx={{ fontSize: 14 }} />
</IconButton>
)}
<ListItemText <ListItemText
primary={ch.alias} primary={ch.alias}
secondary={`${ch.device}:${ch.channel} (${ch.yAxis})`} secondary={`${ch.device}:${ch.channel} (${ch.yAxis})`}

View File

@@ -131,7 +131,7 @@ module.exports = {
// Publicly list views // Publicly list views
app.get('/api/views', (req, res) => { app.get('/api/views', (req, res) => {
try { try {
const stmt = db.prepare('SELECT * FROM views ORDER BY name'); const stmt = db.prepare('SELECT * FROM views ORDER BY position ASC, id ASC');
const rows = stmt.all(); const rows = stmt.all();
const views = rows.map(row => { const views = rows.map(row => {
try { try {
@@ -148,7 +148,7 @@ module.exports = {
app.get('/api/views/:id', (req, res) => { app.get('/api/views/:id', (req, res) => {
try { try {
const stmt = db.prepare('SELECT * FROM views ORDER BY position ASC, id ASC'); const stmt = db.prepare('SELECT * FROM views WHERE id = ?');
const view = stmt.get(req.params.id); const view = stmt.get(req.params.id);
if (view) { if (view) {
view.config = JSON.parse(view.config); view.config = JSON.parse(view.config);
@@ -195,17 +195,23 @@ module.exports = {
// Reorder Views // Reorder Views
app.post('/api/views/reorder', requireAdmin, (req, res) => { app.post('/api/views/reorder', requireAdmin, (req, res) => {
const { order } = req.body; const { order } = req.body;
console.log('[API] Reorder request:', order);
if (!Array.isArray(order)) return res.status(400).json({ error: 'Invalid format' }); if (!Array.isArray(order)) return res.status(400).json({ error: 'Invalid format' });
const updateStmt = db.prepare('UPDATE views SET position = ? WHERE id = ?'); const updateStmt = db.prepare('UPDATE views SET position = ? WHERE id = ?');
const updateMany = db.transaction((items) => { const updateMany = db.transaction((items) => {
for (const item of items) updateStmt.run(item.position, item.id); for (const item of items) {
console.log('[API] Updating view', item.id, 'to position', item.position);
updateStmt.run(item.position, item.id);
}
}); });
try { try {
updateMany(order); updateMany(order);
console.log('[API] Reorder successful');
res.json({ success: true }); res.json({ success: true });
} catch (err) { } catch (err) {
console.error('[API] Reorder error:', err);
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} }
}); });