fix: add gaps in plots where no data exists (#1065)

Scans the window from the most recent timestamp to however many seconds
back are being stored. Then adds blank spaces (NaN values) in between
data items that are too far apart. Lastly, if the earliest item is
blank, there are probably fewer than the expected number of items. In
that case, it adjusts the first timestamp to keep plot width mostly
consistent.
This commit is contained in:
Sylvie Crowe 2024-10-27 12:00:15 -07:00 committed by GitHub
parent bbd530c052
commit cbb825982b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -101,7 +101,8 @@ class SysinfoViewModel {
viewText: jotai.Atom<string>; viewText: jotai.Atom<string>;
viewName: jotai.Atom<string>; viewName: jotai.Atom<string>;
dataAtom: jotai.PrimitiveAtom<Array<DataItem>>; dataAtom: jotai.PrimitiveAtom<Array<DataItem>>;
addDataAtom: jotai.WritableAtom<unknown, [DataItem[]], void>; addInitialDataAtom: jotai.WritableAtom<unknown, [DataItem[]], void>;
addContinuousDataAtom: jotai.WritableAtom<unknown, [DataItem], void>;
incrementCount: jotai.WritableAtom<unknown, [], Promise<void>>; incrementCount: jotai.WritableAtom<unknown, [], Promise<void>>;
loadingAtom: jotai.PrimitiveAtom<boolean>; loadingAtom: jotai.PrimitiveAtom<boolean>;
numPoints: jotai.Atom<number>; numPoints: jotai.Atom<number>;
@ -117,18 +118,57 @@ class SysinfoViewModel {
this.viewType = viewType; this.viewType = viewType;
this.blockId = blockId; this.blockId = blockId;
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`); this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
this.addDataAtom = jotai.atom(null, (get, set, points) => { this.addInitialDataAtom = jotai.atom(null, (get, set, points) => {
const targetLen = get(this.numPoints) + 1;
try {
const newDataRaw = [...points];
if (newDataRaw.length == 0) {
return;
}
const latestItemTs = newDataRaw[newDataRaw.length - 1]?.ts ?? 0;
const cutoffTs = latestItemTs - 1000 * targetLen;
const blankItemTemplate = { ...newDataRaw[newDataRaw.length - 1] };
for (const key in blankItemTemplate) {
blankItemTemplate[key] = NaN;
}
const newDataFiltered = newDataRaw.filter((dataItem) => dataItem.ts >= cutoffTs);
if (newDataFiltered.length == 0) {
return;
}
const newDataWithGaps: Array<DataItem> = [];
if (newDataFiltered[0].ts > cutoffTs) {
const blankItemStart = { ...blankItemTemplate, ts: cutoffTs };
const blankItemEnd = { ...blankItemTemplate, ts: newDataFiltered[0].ts - 1 };
newDataWithGaps.push(blankItemStart);
newDataWithGaps.push(blankItemEnd);
}
newDataWithGaps.push(newDataFiltered[0]);
for (let i = 1; i < newDataFiltered.length; i++) {
const prevIdxItem = newDataFiltered[i - 1];
const curIdxItem = newDataFiltered[i];
const timeDiff = curIdxItem.ts - prevIdxItem.ts;
if (timeDiff > 2000) {
const blankItemStart = { ...blankItemTemplate, ts: prevIdxItem.ts + 1, blank: 1 };
const blankItemEnd = { ...blankItemTemplate, ts: curIdxItem.ts - 1, blank: 1 };
newDataWithGaps.push(blankItemStart);
newDataWithGaps.push(blankItemEnd);
}
newDataWithGaps.push(curIdxItem);
}
set(this.dataAtom, newDataWithGaps);
} catch (e) {
console.log("Error adding data to sysinfo", e);
}
});
this.addContinuousDataAtom = jotai.atom(null, (get, set, newPoint) => {
const targetLen = get(this.numPoints) + 1; const targetLen = get(this.numPoints) + 1;
let data = get(this.dataAtom); let data = get(this.dataAtom);
try { try {
if (data.length > targetLen) { const latestItemTs = newPoint?.ts ?? 0;
data = data.slice(data.length - targetLen); const cutoffTs = latestItemTs - 1000 * targetLen;
} data.push(newPoint);
if (data.length < targetLen) { const newData = data.filter((dataItem) => dataItem.ts >= cutoffTs);
const defaultData = this.getDefaultData();
data = [...defaultData.slice(defaultData.length - targetLen + data.length), ...data];
}
const newData = [...data.slice(points.length), ...points];
set(this.dataAtom, newData); set(this.dataAtom, newData);
} catch (e) { } catch (e) {
console.log("Error adding data to sysinfo", e); console.log("Error adding data to sysinfo", e);
@ -188,7 +228,7 @@ class SysinfoViewModel {
} }
return connValue; return connValue;
}); });
this.dataAtom = jotai.atom(this.getDefaultData()); this.dataAtom = jotai.atom([]);
this.loadInitialData(); this.loadInitialData();
this.connStatus = jotai.atom((get) => { this.connStatus = jotai.atom((get) => {
const blockData = get(this.blockAtom); const blockData = get(this.blockAtom);
@ -214,8 +254,8 @@ class SysinfoViewModel {
const newData = this.getDefaultData(); const newData = this.getDefaultData();
const initialDataItems: DataItem[] = initialData.map(convertWaveEventToDataItem); const initialDataItems: DataItem[] = initialData.map(convertWaveEventToDataItem);
// splice the initial data into the default data (replacing the newest points) // splice the initial data into the default data (replacing the newest points)
newData.splice(newData.length - initialDataItems.length, initialDataItems.length, ...initialDataItems); //newData.splice(newData.length - initialDataItems.length, initialDataItems.length, ...initialDataItems);
globalStore.set(this.addDataAtom, newData); globalStore.set(this.addInitialDataAtom, initialDataItems);
} catch (e) { } catch (e) {
console.log("Error loading initial data for sysinfo", e); console.log("Error loading initial data for sysinfo", e);
} finally { } finally {
@ -301,7 +341,7 @@ function SysinfoView({ model, blockId }: SysinfoViewProps) {
const connName = jotai.useAtomValue(model.connection); const connName = jotai.useAtomValue(model.connection);
const lastConnName = React.useRef(connName); const lastConnName = React.useRef(connName);
const connStatus = jotai.useAtomValue(model.connStatus); const connStatus = jotai.useAtomValue(model.connStatus);
const addPlotData = jotai.useSetAtom(model.addDataAtom); const addContinuousData = jotai.useSetAtom(model.addContinuousDataAtom);
const loading = jotai.useAtomValue(model.loadingAtom); const loading = jotai.useAtomValue(model.loadingAtom);
React.useEffect(() => { React.useEffect(() => {
@ -323,7 +363,13 @@ function SysinfoView({ model, blockId }: SysinfoViewProps) {
return; return;
} }
const dataItem = convertWaveEventToDataItem(event); const dataItem = convertWaveEventToDataItem(event);
addPlotData([dataItem]); const prevData = globalStore.get(model.dataAtom);
const prevLastTs = prevData[prevData.length - 1]?.ts ?? 0;
if (dataItem.ts - prevLastTs > 2000) {
model.loadInitialData();
} else {
addContinuousData(dataItem);
}
}, },
}); });
console.log("subscribe to sysinfo", connName); console.log("subscribe to sysinfo", connName);
@ -348,6 +394,7 @@ type SingleLinePlotProps = {
defaultColor: string; defaultColor: string;
title?: boolean; title?: boolean;
sparkline?: boolean; sparkline?: boolean;
targetLen: number;
}; };
function SingleLinePlot({ function SingleLinePlot({
@ -358,6 +405,7 @@ function SingleLinePlot({
defaultColor, defaultColor,
title = false, title = false,
sparkline = false, sparkline = false,
targetLen,
}: SingleLinePlotProps) { }: SingleLinePlotProps) {
const containerRef = React.useRef<HTMLInputElement>(); const containerRef = React.useRef<HTMLInputElement>();
const domRect = useDimensionsWithExistingRef(containerRef, 300); const domRect = useDimensionsWithExistingRef(containerRef, 300);
@ -440,12 +488,15 @@ function SingleLinePlot({
); );
let maxY = resolveDomainBound(yvalMeta?.maxy, plotData[plotData.length - 1]) ?? 100; let maxY = resolveDomainBound(yvalMeta?.maxy, plotData[plotData.length - 1]) ?? 100;
let minY = resolveDomainBound(yvalMeta?.miny, plotData[plotData.length - 1]) ?? 0; let minY = resolveDomainBound(yvalMeta?.miny, plotData[plotData.length - 1]) ?? 0;
let maxX = plotData[plotData.length - 1].ts;
let minX = maxX - targetLen * 1000;
const plot = Plot.plot({ const plot = Plot.plot({
axis: !sparkline, axis: !sparkline,
x: { x: {
grid: true, grid: true,
label: "time", label: "time",
tickFormat: (d) => `${dayjs.unix(d / 1000).format("HH:mm:ss")}`, tickFormat: (d) => `${dayjs.unix(d / 1000).format("HH:mm:ss")}`,
domain: [minX, maxX],
}, },
y: { label: labelY, domain: [minY, maxY] }, y: { label: labelY, domain: [minY, maxY] },
width: plotWidth, width: plotWidth,
@ -469,6 +520,7 @@ const SysinfoViewInner = React.memo(({ model }: SysinfoViewProps) => {
const yvals = jotai.useAtomValue(model.metrics); const yvals = jotai.useAtomValue(model.metrics);
const plotMeta = jotai.useAtomValue(model.plotMetaAtom); const plotMeta = jotai.useAtomValue(model.plotMetaAtom);
const osRef = React.useRef<OverlayScrollbarsComponentRef>(); const osRef = React.useRef<OverlayScrollbarsComponentRef>();
const targetLen = jotai.useAtomValue(model.numPoints) + 1;
let title = false; let title = false;
let cols2 = false; let cols2 = false;
if (yvals.length > 1) { if (yvals.length > 1) {
@ -491,6 +543,7 @@ const SysinfoViewInner = React.memo(({ model }: SysinfoViewProps) => {
blockId={model.blockId} blockId={model.blockId}
defaultColor={"var(--accent-color)"} defaultColor={"var(--accent-color)"}
title={title} title={title}
targetLen={targetLen}
/> />
); );
})} })}