Skip to content

Commit 626fd0d

Browse files
committed
web: display debug charts (GPL line charts) in charts widget
Fetch debug_charts data from the server on each debug_paused push and display them as canvas-based line charts in the ChartsWidget. Each chart gets its own tab alongside a "Histogram" tab that switches back to the existing slack histogram view. Also fix mouseleave, resize, and public render() to route through _syncView() so the active chart type is respected. Signed-off-by: Matt Liberty <mliberty@precisioninno.com>
1 parent f9c1062 commit 626fd0d

8 files changed

Lines changed: 489 additions & 20 deletions

File tree

src/web/src/charts-widget.js

Lines changed: 248 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const kNegativeBorder = '#8b0000'; // darkred
1818
const kPositiveFill = '#90ee90'; // lightgreen
1919
const kPositiveBorder = '#006400'; // darkgreen
2020

21+
// Line chart series colors (Tableau 10)
22+
const kLineColors = ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2',
23+
'#59a14f', '#edc948', '#b07aa1', '#ff9da7'];
24+
2125
// Pure layout computation — extracted for testability.
2226
export function computeHistogramLayout(histogramData, canvasWidth, canvasHeight) {
2327
const bins = histogramData?.bins;
@@ -107,6 +111,10 @@ export class ChartsWidget {
107111
this._chartArea = null;
108112
this._hoveredBar = null;
109113

114+
// Debug charts (line charts from GPL, etc.)
115+
this._debugCharts = [];
116+
this._activeDebugChart = -1; // -1 = show histogram
117+
110118
this._build();
111119
}
112120

@@ -116,6 +124,12 @@ export class ChartsWidget {
116124
const el = document.createElement('div');
117125
el.className = 'charts-widget';
118126

127+
// Debug chart tabs (hidden until debug charts arrive)
128+
this._debugTabBar = document.createElement('div');
129+
this._debugTabBar.className = 'timing-tab-bar';
130+
this._debugTabBar.style.display = 'none';
131+
el.appendChild(this._debugTabBar);
132+
119133
// Toolbar
120134
const toolbar = document.createElement('div');
121135
toolbar.className = 'charts-toolbar';
@@ -184,6 +198,10 @@ export class ChartsWidget {
184198
this._tooltip.style.display = 'none';
185199
el.appendChild(this._tooltip);
186200

201+
// Group histogram-specific elements so we can hide them
202+
// when a debug line chart tab is active.
203+
this._histogramControls = [toolbar, tabBar, filterRow];
204+
187205
this.element = el;
188206

189207
this._ctx = this._canvas.getContext('2d');
@@ -216,15 +234,13 @@ export class ChartsWidget {
216234
this._canvas.addEventListener('mouseleave', () => {
217235
this._hoveredBar = null;
218236
this._tooltip.style.display = 'none';
219-
this._render();
237+
this._syncView();
220238
});
221239
this._canvas.addEventListener('click', (e) => this._handleClick(e));
222240

223241
// Re-render on resize
224242
const ro = new ResizeObserver(() => {
225-
this._sizeCanvas();
226-
this._computeLayout();
227-
this._render();
243+
this._syncView();
228244
});
229245
ro.observe(this._canvas);
230246
}
@@ -288,9 +304,7 @@ export class ChartsWidget {
288304
}
289305
const data = await this._app.websocketManager.request(req);
290306
this._histogramData = data;
291-
this._sizeCanvas();
292-
this._computeLayout();
293-
this._render();
307+
this._syncView();
294308

295309
const total = data.total_endpoints || 0;
296310
const unconstrained = data.unconstrained_count || 0;
@@ -313,7 +327,7 @@ export class ChartsWidget {
313327
this._chartArea = result.chartArea;
314328
}
315329

316-
render() { this._render(); }
330+
render() { this._syncView(); }
317331

318332
_render() {
319333
const rect = this._canvas.getBoundingClientRect();
@@ -452,7 +466,7 @@ export class ChartsWidget {
452466
}
453467

454468
_hitTestBar(e) {
455-
if (!this._bars) return null;
469+
if (this._activeDebugChart >= 0 || !this._bars) return null;
456470
const rect = this._canvas.getBoundingClientRect();
457471
const mx = e.clientX - rect.left;
458472
const my = e.clientY - rect.top;
@@ -466,6 +480,11 @@ export class ChartsWidget {
466480
}
467481

468482
_handleHover(e) {
483+
if (this._activeDebugChart >= 0) {
484+
this._hoveredBar = null;
485+
this._tooltip.style.display = 'none';
486+
return;
487+
}
469488
const bar = this._hitTestBar(e);
470489
if (bar !== this._hoveredBar) {
471490
this._hoveredBar = bar;
@@ -492,6 +511,7 @@ export class ChartsWidget {
492511
}
493512

494513
async _handleClick(e) {
514+
if (this._activeDebugChart >= 0) return;
495515
const bar = this._hitTestBar(e);
496516
if (!bar || bar.count === 0) return;
497517

@@ -515,4 +535,223 @@ export class ChartsWidget {
515535
console.error('Charts bar click error:', err);
516536
}
517537
}
538+
539+
// ---- Debug line charts (GPL HPWL, density, etc.) ----
540+
541+
setDebugCharts(charts) {
542+
this._debugCharts = charts;
543+
this._rebuildDebugTabs();
544+
// Auto-select the first debug chart if none is active;
545+
// reset to histogram if charts are now empty.
546+
if (charts.length === 0) {
547+
this._activeDebugChart = -1;
548+
} else if (this._activeDebugChart < 0) {
549+
this._activeDebugChart = 0;
550+
} else if (this._activeDebugChart >= charts.length) {
551+
this._activeDebugChart = 0;
552+
}
553+
this._syncView();
554+
}
555+
556+
_rebuildDebugTabs() {
557+
const bar = this._debugTabBar;
558+
bar.innerHTML = '';
559+
if (this._debugCharts.length === 0) {
560+
bar.style.display = 'none';
561+
return;
562+
}
563+
bar.style.display = '';
564+
565+
// "Histogram" tab to switch back to the slack histogram.
566+
const histTab = document.createElement('div');
567+
histTab.className = 'timing-tab' +
568+
(this._activeDebugChart < 0 ? ' active' : '');
569+
histTab.textContent = 'Histogram';
570+
histTab.addEventListener('click', () => {
571+
this._activeDebugChart = -1;
572+
this._rebuildDebugTabs();
573+
this._syncView();
574+
});
575+
bar.appendChild(histTab);
576+
577+
// One tab per debug chart.
578+
this._debugCharts.forEach((chart, i) => {
579+
const tab = document.createElement('div');
580+
tab.className = 'timing-tab' +
581+
(this._activeDebugChart === i ? ' active' : '');
582+
tab.textContent = chart.name;
583+
tab.addEventListener('click', () => {
584+
this._activeDebugChart = i;
585+
this._rebuildDebugTabs();
586+
this._syncView();
587+
});
588+
bar.appendChild(tab);
589+
});
590+
}
591+
592+
_syncView() {
593+
const showHist = this._activeDebugChart < 0;
594+
if (!showHist) {
595+
this._hoveredBar = null;
596+
this._tooltip.style.display = 'none';
597+
}
598+
for (const el of this._histogramControls) {
599+
el.style.display = showHist ? '' : 'none';
600+
}
601+
this._sizeCanvas();
602+
if (showHist) {
603+
this._computeLayout();
604+
this._render();
605+
} else {
606+
this._renderLineChart(this._debugCharts[this._activeDebugChart]);
607+
}
608+
}
609+
610+
_renderLineChart(chart) {
611+
const rect = this._canvas.getBoundingClientRect();
612+
const w = rect.width;
613+
const h = rect.height;
614+
const ctx = this._ctx;
615+
const tc = getThemeColors();
616+
617+
ctx.clearRect(0, 0, w, h);
618+
ctx.fillStyle = tc.canvasBg;
619+
ctx.fillRect(0, 0, w, h);
620+
621+
const pts = chart.points;
622+
if (!pts || pts.length === 0) {
623+
ctx.fillStyle = tc.canvasText;
624+
ctx.font = '14px monospace';
625+
ctx.textAlign = 'center';
626+
ctx.textBaseline = 'middle';
627+
ctx.fillText('No data points yet', w / 2, h / 2);
628+
return;
629+
}
630+
631+
const numSeries = chart.y_labels ? chart.y_labels.length : 1;
632+
const cLeft = kLeftMargin;
633+
const cRight = w - kRightMargin;
634+
const cTop = kTopMargin;
635+
const cBottom = h - kBottomMargin;
636+
const cW = cRight - cLeft;
637+
const cH = cBottom - cTop;
638+
if (cW <= 0 || cH <= 0) return;
639+
640+
// Compute data ranges.
641+
let xMin = pts[0].x, xMax = pts[0].x;
642+
let yMin = Infinity, yMax = -Infinity;
643+
for (const p of pts) {
644+
if (p.x < xMin) xMin = p.x;
645+
if (p.x > xMax) xMax = p.x;
646+
for (const v of p.ys) {
647+
if (v < yMin) yMin = v;
648+
if (v > yMax) yMax = v;
649+
}
650+
}
651+
if (xMin === xMax) xMax = xMin + 1;
652+
if (!isFinite(yMin) || !isFinite(yMax)) { yMin = 0; yMax = 1; }
653+
// Add 5% padding to Y range.
654+
const yPad = (yMax - yMin) * 0.05 || 1;
655+
yMin -= yPad;
656+
yMax += yPad;
657+
658+
const toX = (v) => cLeft + ((v - xMin) / (xMax - xMin)) * cW;
659+
const toY = (v) => cBottom - ((v - yMin) / (yMax - yMin)) * cH;
660+
661+
// Axes.
662+
ctx.strokeStyle = tc.canvasAxis;
663+
ctx.lineWidth = 1;
664+
ctx.beginPath();
665+
ctx.moveTo(cLeft, cTop);
666+
ctx.lineTo(cLeft, cBottom);
667+
ctx.lineTo(cRight, cBottom);
668+
ctx.stroke();
669+
670+
// Y axis ticks.
671+
ctx.fillStyle = tc.canvasLabel;
672+
ctx.font = '10px monospace';
673+
ctx.textAlign = 'right';
674+
ctx.textBaseline = 'middle';
675+
const nYTicks = 5;
676+
for (let i = 0; i <= nYTicks; i++) {
677+
const v = yMin + (yMax - yMin) * i / nYTicks;
678+
const y = toY(v);
679+
ctx.fillText(this._formatNum(v), cLeft - 4, y);
680+
if (i > 0 && i < nYTicks) {
681+
ctx.strokeStyle = tc.canvasGrid;
682+
ctx.beginPath();
683+
ctx.moveTo(cLeft, y);
684+
ctx.lineTo(cRight, y);
685+
ctx.stroke();
686+
}
687+
}
688+
689+
// X axis ticks.
690+
ctx.textAlign = 'center';
691+
ctx.textBaseline = 'top';
692+
if (pts.length === 1) {
693+
ctx.fillStyle = tc.canvasLabel;
694+
ctx.fillText(this._formatNum(pts[0].x), toX(pts[0].x), cBottom + 4);
695+
} else {
696+
const nXTicks = Math.min(pts.length - 1, 6);
697+
for (let i = 0; i <= nXTicks; i++) {
698+
const v = xMin + (xMax - xMin) * i / nXTicks;
699+
const x = toX(v);
700+
ctx.fillStyle = tc.canvasLabel;
701+
ctx.fillText(this._formatNum(v), x, cBottom + 4);
702+
}
703+
}
704+
705+
// Axis labels.
706+
ctx.fillStyle = tc.canvasTitle;
707+
ctx.font = '11px monospace';
708+
ctx.textAlign = 'center';
709+
ctx.fillText(chart.x_label || '', (cLeft + cRight) / 2, cBottom + 22);
710+
711+
// Title.
712+
ctx.fillStyle = tc.fgPrimary;
713+
ctx.font = '13px monospace';
714+
ctx.textBaseline = 'top';
715+
ctx.fillText(chart.name, w / 2, 4);
716+
717+
// Draw each Y series as a line.
718+
for (let s = 0; s < numSeries; s++) {
719+
ctx.strokeStyle = kLineColors[s % kLineColors.length];
720+
ctx.lineWidth = 1.5;
721+
ctx.beginPath();
722+
let first = true;
723+
for (const p of pts) {
724+
const px = toX(p.x);
725+
const py = toY(p.ys[s]);
726+
if (first) { ctx.moveTo(px, py); first = false; }
727+
else { ctx.lineTo(px, py); }
728+
}
729+
ctx.stroke();
730+
}
731+
732+
// Legend (if multiple series).
733+
if (numSeries > 1 && chart.y_labels) {
734+
ctx.font = '10px monospace';
735+
ctx.textAlign = 'left';
736+
ctx.textBaseline = 'top';
737+
let lx = cLeft + 8;
738+
const ly = cTop + 4;
739+
for (let s = 0; s < numSeries; s++) {
740+
const color = kLineColors[s % kLineColors.length];
741+
ctx.fillStyle = color;
742+
ctx.fillRect(lx, ly + s * 14, 12, 10);
743+
ctx.fillStyle = tc.canvasLabel;
744+
ctx.fillText(chart.y_labels[s], lx + 16, ly + s * 14);
745+
}
746+
}
747+
}
748+
749+
_formatNum(v) {
750+
const abs = Math.abs(v);
751+
if (abs === 0) return '0';
752+
if (abs >= 1e6) return (v / 1e6).toFixed(1) + 'M';
753+
if (abs >= 1e3) return (v / 1e3).toFixed(1) + 'k';
754+
if (abs >= 1) return v.toFixed(1);
755+
return v.toPrecision(3);
756+
}
518757
}

src/web/src/main.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,12 @@ app.websocketManager.onPush = (msg) => {
693693
// Use the debounced version so that a debug_refresh arriving
694694
// in the same event-loop turn is coalesced (avoids 2x tiles).
695695
scheduleRedrawAllLayers();
696+
// Fetch debug charts (e.g. GPL HPWL vs iteration).
697+
if (app.chartsWidget) {
698+
app.websocketManager.request({ type: 'debug_charts' })
699+
.then(data => app.chartsWidget.setDebugCharts(data.charts || []))
700+
.catch(() => {});
701+
}
696702
} else if (msg.type === 'debug_resumed') {
697703
const btn = document.getElementById('debug-continue-btn');
698704
if (btn) btn.style.display = 'none';

src/web/src/tile_generator.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2088,7 +2088,7 @@ void TileGenerator::drawRendererOverlay(std::vector<unsigned char>& image,
20882088
static int penWidthPx(const PenState& pen, double scale)
20892089
{
20902090
if (pen.cosmetic) {
2091-
return 1;
2091+
return std::max(1, pen.width);
20922092
}
20932093
return std::max(1, static_cast<int>(pen.width * scale));
20942094
}

0 commit comments

Comments
 (0)