working epics ioc and tested

This commit is contained in:
Martino Ferrari
2026-05-04 21:13:36 +02:00
parent 90669c5fd6
commit 0a5a85e4c4
25 changed files with 1550 additions and 136 deletions
+3 -1
View File
@@ -1,6 +1,7 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { getSignalStore, getMetaStore } from '../lib/stores';
import { formatValue } from '../lib/format';
import type { Widget, SignalValue, SignalMeta } from '../lib/types';
const DEFAULT_VALUE: SignalValue = { value: null, quality: 'unknown', ts: null };
@@ -34,6 +35,7 @@ export default function BarH({ widget, onContextMenu }: Props) {
const minVal = parseFloat(widget.options['min'] ?? String(meta?.displayLow ?? 0));
const maxVal = parseFloat(widget.options['max'] ?? String(meta?.displayHigh ?? 100));
const quality = sv.quality;
const fmt = widget.options['format'] ?? '';
const rawV = sv.value;
const rawValue: number | null = rawV === null || rawV === undefined ? null
@@ -41,7 +43,7 @@ export default function BarH({ widget, onContextMenu }: Props) {
function displayValue(): string {
if (rawValue === null || isNaN(rawValue)) return '---';
return Number.isFinite(rawValue) ? rawValue.toPrecision(4).replace(/\.?0+$/, '') : String(rawValue);
return formatValue(rawValue, fmt);
}
function fillPercent(): number {
+3 -1
View File
@@ -1,6 +1,7 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { getSignalStore, getMetaStore } from '../lib/stores';
import { formatValue } from '../lib/format';
import type { Widget, SignalValue, SignalMeta } from '../lib/types';
const DEFAULT_VALUE: SignalValue = { value: null, quality: 'unknown', ts: null };
@@ -34,6 +35,7 @@ export default function BarV({ widget, onContextMenu }: Props) {
const minVal = parseFloat(widget.options['min'] ?? String(meta?.displayLow ?? 0));
const maxVal = parseFloat(widget.options['max'] ?? String(meta?.displayHigh ?? 100));
const quality = sv.quality;
const fmt = widget.options['format'] ?? '';
const rawV = sv.value;
const rawValue: number | null = rawV === null || rawV === undefined ? null
@@ -41,7 +43,7 @@ export default function BarV({ widget, onContextMenu }: Props) {
function displayValue(): string {
if (rawValue === null || isNaN(rawValue)) return '---';
return Number.isFinite(rawValue) ? rawValue.toPrecision(4).replace(/\.?0+$/, '') : String(rawValue);
return formatValue(rawValue, fmt);
}
function fillPercent(): number {
+3 -1
View File
@@ -1,6 +1,7 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { getSignalStore, getMetaStore } from '../lib/stores';
import { formatValue } from '../lib/format';
import type { Widget, SignalValue, SignalMeta } from '../lib/types';
const DEFAULT_VALUE: SignalValue = { value: null, quality: 'unknown', ts: null };
@@ -60,13 +61,14 @@ export default function Gauge({ widget, onContextMenu }: Props) {
const thresholdHigh = widget.options['thresholdHigh'] ? parseFloat(widget.options['thresholdHigh']) : null;
const quality = sv.quality;
const fmt = widget.options['format'] ?? '';
const rawV = sv.value;
const rawValue: number | null = rawV === null || rawV === undefined ? null
: typeof rawV === 'number' ? rawV : parseFloat(String(rawV));
function displayValue(): string {
if (rawValue === null || isNaN(rawValue)) return '---';
return Number.isFinite(rawValue) ? rawValue.toPrecision(4).replace(/\.?0+$/, '') : String(rawValue);
return formatValue(rawValue, fmt);
}
function fillColor(): string {
+46 -29
View File
@@ -4,6 +4,7 @@ import uPlot from 'uplot';
import * as echarts from 'echarts';
import { getSignalStore } from '../lib/stores';
import { wsClient } from '../lib/ws';
import { formatValue } from '../lib/format';
import type { Widget, SignalRef } from '../lib/types';
interface Props {
@@ -82,6 +83,7 @@ export default function PlotWidget({ widget, onContextMenu, timeRange }: Props)
const buffers: SeriesBuffer[] = signals.map(() => ({ timestamps: [], values: [] }));
const histValues: number[][] = signals.map(() => []);
const unsubs: (() => void)[] = [];
const fmt = widget.options['format'] ?? '';
// ── uPlot (timeseries) ──────────────────────────────────────────────────
let uplot: uPlot | null = null;
@@ -110,6 +112,37 @@ export default function PlotWidget({ widget, onContextMenu, timeRange }: Props)
] as uPlot.AlignedData;
}
function makeUplotOpts(): uPlot.Options {
const yAxis: uPlot.Axis = {
stroke: '#64748b',
grid: { stroke: '#2d3748' },
ticks: { stroke: '#475569' },
};
if (fmt) {
yAxis.values = (_u: uPlot, vals: (number | null)[]) =>
vals.map(v => (v === null || v === undefined) ? '' : formatValue(v, fmt));
}
const seriesConf: uPlot.Series[] = [
{},
...signals.map((s, i) => ({
label: s.name,
stroke: s.color ?? COLORS[i % COLORS.length],
width: 1.5,
})),
];
return {
width: w,
height: h,
series: seriesConf,
legend: { show: showLegend },
axes: [
{ stroke: '#64748b', grid: { stroke: '#2d3748' }, ticks: { stroke: '#475569' } },
yAxis,
],
scales: { x: { time: true }, y: scaleY },
};
}
// ── ECharts ────────────────────────────────────────────────────────────
let echart: echarts.EChartsType | null = null;
@@ -219,37 +252,15 @@ export default function PlotWidget({ widget, onContextMenu, timeRange }: Props)
if (yMin !== undefined && yMin !== 'auto') scaleY.min = parseFloat(yMin);
if (yMax !== undefined && yMax !== 'auto') scaleY.max = parseFloat(yMax);
if (plotType === 'timeseries') {
const seriesConf: uPlot.Series[] = [
{},
...signals.map((s, i) => ({
label: s.name,
stroke: s.color ?? COLORS[i % COLORS.length],
width: 1.5,
})),
];
uplot = new uPlot(
{
width: w,
height: h,
series: seriesConf,
legend: { show: showLegend },
axes: [
{ stroke: '#64748b', grid: { stroke: '#2d3748' }, ticks: { stroke: '#475569' } },
{ stroke: '#64748b', grid: { stroke: '#2d3748' }, ticks: { stroke: '#475569' } },
],
scales: { x: { time: true }, y: scaleY },
},
uplotData(),
el,
);
} else {
if (plotType !== 'timeseries') {
echart = echarts.init(el);
echart.setOption(echartsOption());
}
// ── Historical mode (timeseries only) ──────────────────────────────────
// uPlot is NOT created until data arrives — initialising with empty arrays
// causes the x-axis to start at epoch 0, compressing all real data to the
// right edge of the chart.
if (timeRange && plotType === 'timeseries') {
setHistStatus('loading');
let cancelled = false;
@@ -272,18 +283,24 @@ export default function PlotWidget({ widget, onContextMenu, timeRange }: Props)
if (cancelled) return;
const totalPoints = buffers.reduce((s, b) => s + b.timestamps.length, 0);
setHistStatus(totalPoints === 0 ? 'empty' : 'idle');
if (uplot) uplot.setData(uplotData());
// Create uPlot now — with real data — so the x-axis is correct from the start.
uplot = new uPlot(makeUplotOpts(), uplotData(), el);
}).catch(() => {
if (!cancelled) setHistStatus('error');
});
return () => {
cancelled = true;
if (uplot) uplot.destroy();
if (echart) echart.dispose();
uplot?.destroy();
echart?.dispose();
};
}
// ── Live mode: init uPlot immediately ──────────────────────────────────
if (plotType === 'timeseries') {
uplot = new uPlot(makeUplotOpts(), uplotData(), el);
}
// ── Live streaming mode ────────────────────────────────────────────────
setHistStatus('idle');
+3 -3
View File
@@ -1,6 +1,7 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { getSignalStore, getMetaStore } from '../lib/stores';
import { formatValue } from '../lib/format';
import type { Widget, SignalValue, SignalMeta } from '../lib/types';
const DEFAULT_VALUE: SignalValue = { value: null, quality: 'unknown', ts: null };
@@ -32,13 +33,12 @@ export default function TextView({ widget, onContextMenu }: Props) {
const rawUnit = widget.options['unit'];
const unit = rawUnit === 'none' ? '' : (rawUnit || meta?.unit || '');
const quality = sv.quality;
const fmt = widget.options['format'] ?? '';
function displayValue(): string {
if (!sv || sv.value === null || sv.value === undefined) return '---';
const v = sv.value;
if (typeof v === 'number') {
return Number.isFinite(v) ? v.toPrecision(6).replace(/\.?0+$/, '') : String(v);
}
if (typeof v === 'number') return formatValue(v, fmt);
return String(v);
}