Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified internal/static/dist.zip
Binary file not shown.
8 changes: 4 additions & 4 deletions internal/static/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions internal/static/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
"scripts": {
"dev": "vite",
"preview": "vite preview",
"build": "vite build"
"build": "vite build",
"release": "bash -lc \"rm -rf ./dist && vite build && zip -r dist.zip dist/*\""
},
"devDependencies": {
"vite": "^6.4.1"
},
"dependencies": {
"bootstrap": "^5.3.5",
"bootstrap-icons": "^1.12.1",
"plotly.js-cartesian-dist": "^3.0.1",
"plotly.js-cartesian-dist": "^3.3.0",
"tippy.js": "^6.3.7"
}
}
38 changes: 34 additions & 4 deletions internal/static/src/PlotManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ function debounce(fn, delay) {
}

export default class PlotManager {
#shapesCache;
#lastGcEnabled;

constructor(config) {
this.container = document.getElementById("plots");
this.plots = config.series.map((pd) => new Plot(pd));
this.#shapesCache = new Map();
this.#lastGcEnabled = null;
this.#attach();

window.addEventListener(
Expand All @@ -41,14 +46,39 @@ export default class PlotManager {
}

update(data, gcEnabled, timeRange, force = false) {
// Create GC vertical lines.
// Create GC vertical lines - only if needed.
const shapes = new Map();
if (gcEnabled) {
// Only recreate shapes if GC state changed or events changed
const gcStateChanged = this.#lastGcEnabled !== gcEnabled;

for (const [name, serie] of data.events) {
shapes.set(name, createVerticalLines(serie));
// Check if we need to regenerate shapes for this event
const cached = this.#shapesCache.get(name);
const eventsChanged =
!cached ||
cached.length !== serie.length ||
(serie.length > 0 &&
cached[cached.length - 1]?.x0?.getTime() !==
serie[serie.length - 1]?.getTime());

if (gcStateChanged || eventsChanged || !this.#shapesCache.has(name)) {
const newShapes = createVerticalLines(serie);
this.#shapesCache.set(name, newShapes);
shapes.set(name, newShapes);
} else {
shapes.set(name, this.#shapesCache.get(name));
}
}
} else {
// GC disabled, clear all shapes
if (this.#lastGcEnabled !== false) {
this.#shapesCache.clear();
}
}

this.#lastGcEnabled = gcEnabled;

// X-axis range.
const now = data.times[data.times.length - 1];
const xrange = [now - timeRange * 1000, now];
Expand All @@ -63,11 +93,11 @@ export default class PlotManager {
const gd = document.getElementById(p.name());
// We're being super defensive here to ensure that the div is
// actually there (or Plotly.resize would fail).
if (!gd) return;
if (!p.isVisible()) return;
if (!gd || !p.isVisible()) return;
const { offsetWidth: w, offsetHeight: h } = gd;
if (w === 0 || h === 0) return;

p.updateCachedWidth();
Plotly.Plots.resize(gd);
});
}
Expand Down
11 changes: 7 additions & 4 deletions internal/static/src/RingBuffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ export default class RingBuffer {

slice(lastN) {
const n = Math.min(lastN, this.#size);
const result = [];
for (let i = this.#size - n; i < this.#size; i++) {
result.push(this.#buf[(this.#start + i) % this.#buf.length]);
const result = new Float64Array(n);

const startIdx = this.#size - n;
for (let i = 0; i < n; i++) {
result[i] = this.#buf[(this.#start + startIdx + i) % this.#buf.length];
}
return result;

return Array.from(result);
}

get first() {
Expand Down
26 changes: 23 additions & 3 deletions internal/static/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,31 @@ import "bootstrap/dist/js/bootstrap.min.js";
export let statsMgr;
export let plotMgr;

// RAF-based throttling for plot updates
let rafId = null;
let pendingUpdate = false;
let forceNextUpdate = false;

const scheduleUpdate = () => {
if (rafId !== null) return; // Already scheduled

rafId = requestAnimationFrame(() => {
rafId = null;
if (pendingUpdate && running) {
const data = statsMgr.slice(timerange);
plotMgr.update(data, gcEnabled, timerange, forceNextUpdate);
pendingUpdate = false;
forceNextUpdate = false;
}
});
};

export const drawPlots = (force) => {
if (running) {
const data = statsMgr.slice(timerange);
plotMgr.update(data, gcEnabled, timerange, force);
pendingUpdate = true;
if (force) {
forceNextUpdate = true;
}
scheduleUpdate();
};

export const connect = () => {
Expand Down
31 changes: 22 additions & 9 deletions internal/static/src/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Plot {
#maximized;
#cfg;
#dataTemplate;
#cachedWidth;

constructor(cfg) {
cfg.layout.paper_bgcolor = themeColors[theme.getThemeMode()].paper_bgcolor;
Expand All @@ -33,6 +34,7 @@ class Plot {
this.#updateCount = 0;
this.#dataTemplate = [];
this.#lastData = [{ x: new Date() }];
this.#cachedWidth = null;

if (this.#cfg.type == "heatmap") {
this.#dataTemplate.push({
Expand Down Expand Up @@ -80,8 +82,8 @@ class Plot {
this.#htmlElt = div;

// Measure the final CSS width.
const initialWidth = div.clientWidth;
this.#plotlyLayout.width = initialWidth;
this.#cachedWidth = div.clientWidth;
this.#plotlyLayout.width = this.#cachedWidth;
this.#plotlyLayout.height = defaultPlotHeight;

// Pass a single data with no data to create an empty plot (this removes
Expand Down Expand Up @@ -154,6 +156,7 @@ class Plot {

#extractData(data) {
const serie = data.series.get(this.#cfg.name);

if (this.#cfg.type == "heatmap") {
this.#dataTemplate[0].x = data.times;
this.#dataTemplate[0].z = serie;
Expand All @@ -163,6 +166,7 @@ class Plot {
for (let i = 0; i < this.#dataTemplate.length; i++) {
this.#dataTemplate[i].x = data.times;
this.#dataTemplate[i].y = serie[i];

this.#dataTemplate[i].stackgroup = this.#cfg.subplots[i].stackgroup;
this.#dataTemplate[i].hoveron = this.#cfg.subplots[i].hoveron;
this.#dataTemplate[i].type =
Expand Down Expand Up @@ -198,11 +202,8 @@ class Plot {
this.#plotlyConfig.responsive = false;
}

// **Re‐measure** container width each time
const newWidth = this.#maximized
? plotsDiv.clientWidth
: this.#htmlElt.clientWidth;
this.#plotlyLayout.width = newWidth;
// Use cached width - only recalculated on resize
this.#plotlyLayout.width = this.#cachedWidth;

this.#react();
}
Expand All @@ -215,21 +216,33 @@ class Plot {
this.#plotlyLayout = newLayoutObject(this.#cfg, this.#maximized);
this.#plotlyConfig = newConfigObject(this.#cfg, this.#maximized);

this.#plotlyLayout.width = plotsDiv.clientWidth;
this.#cachedWidth = plotsDiv.clientWidth;
this.#plotlyLayout.width = this.#cachedWidth;
this.#plotlyLayout.height = plotsDiv.parentElement.clientHeight - 50;

this.#react();
}

minimize() {
(this.#maximized = false), this.#maximized;
this.#maximized = false;

this.#plotlyLayout = newLayoutObject(this.#cfg, this.#maximized);
this.#plotlyConfig = newConfigObject(this.#cfg, this.#maximized);

this.#cachedWidth = this.#htmlElt.clientWidth;
this.#plotlyLayout.width = this.#cachedWidth;

this.#react();
}

updateCachedWidth() {
if (this.#maximized) {
this.#cachedWidth = plotsDiv.clientWidth;
} else {
this.#cachedWidth = this.#htmlElt.clientWidth;
}
}

#react() {
Plotly.react(
this.#htmlElt,
Expand Down
Loading