Skip to content

Commit 1468121

Browse files
authored
Merge pull request #10247 from The-OpenROAD-Project-staging/web-charts-hit-test
web: add column hit-test for charts widget
2 parents 62d109c + ed1db83 commit 1468121

2 files changed

Lines changed: 86 additions & 13 deletions

File tree

src/web/src/charts-widget.js

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ const kPositiveBorder = '#006400'; // darkgreen
2121
// Line chart series colors (Tableau 10)
2222
const kLineColors = ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2',
2323
'#59a14f', '#edc948', '#b07aa1', '#ff9da7'];
24+
const kNegativeHighlight = 'rgba(240,128,128,0.12)';
25+
const kPositiveHighlight = 'rgba(144,238,144,0.12)';
26+
const kNegativeHover = '#ff9999';
27+
const kPositiveHover = '#b0ffb0';
28+
29+
// Pure hit-test — returns the bar whose column contains (mx, my), or null.
30+
// Uses the full column height (chartArea top to bottom) so that buckets with
31+
// few paths are easy to click.
32+
export function hitTestColumn(bars, chartArea, mx, my) {
33+
if (!bars || !chartArea) return null;
34+
if (my < chartArea.top || my > chartArea.bottom) return null;
35+
for (const bar of bars) {
36+
if (mx >= bar.x && mx <= bar.x + bar.width) {
37+
return bar;
38+
}
39+
}
40+
return null;
41+
}
2442

2543
// Pure layout computation — extracted for testability.
2644
export function computeHistogramLayout(histogramData, canvasWidth, canvasHeight) {
@@ -436,16 +454,24 @@ export class ChartsWidget {
436454
}
437455

438456
_drawBars(ctx) {
457+
const ca = this._chartArea;
439458
for (const bar of this._bars) {
440-
if (bar.height <= 0) continue;
441-
442459
const isHovered = (this._hoveredBar === bar);
443460

461+
// Draw a subtle column highlight on hover so the full clickable
462+
// area is visible, even for buckets with very short bars.
463+
if (isHovered && ca) {
464+
ctx.fillStyle = bar.negative
465+
? kNegativeHighlight : kPositiveHighlight;
466+
ctx.fillRect(bar.x, ca.top, bar.width, ca.bottom - ca.top);
467+
}
468+
469+
if (bar.height <= 0) continue;
470+
444471
// Fill
445472
ctx.fillStyle = bar.negative ? kNegativeFill : kPositiveFill;
446473
if (isHovered) {
447-
// Lighten on hover
448-
ctx.fillStyle = bar.negative ? '#ff9999' : '#b0ffb0';
474+
ctx.fillStyle = bar.negative ? kNegativeHover : kPositiveHover;
449475
}
450476
ctx.fillRect(bar.x, bar.y, bar.width, bar.height);
451477

@@ -466,17 +492,10 @@ export class ChartsWidget {
466492
}
467493

468494
_hitTestBar(e) {
469-
if (this._activeDebugChart >= 0 || !this._bars) return null;
470495
const rect = this._canvas.getBoundingClientRect();
471496
const mx = e.clientX - rect.left;
472497
const my = e.clientY - rect.top;
473-
for (const bar of this._bars) {
474-
if (mx >= bar.x && mx <= bar.x + bar.width &&
475-
my >= bar.y && my <= bar.y + bar.height) {
476-
return bar;
477-
}
478-
}
479-
return null;
498+
return hitTestColumn(this._bars, this._chartArea, mx, my);
480499
}
481500

482501
_handleHover(e) {

src/web/test/js/test-charts-widget.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { describe, it } from 'node:test';
66
import assert from 'node:assert/strict';
77
import {
88
ChartsWidget,
9-
computeHistogramLayout,
9+
computeHistogramLayout, hitTestColumn,
1010
kLeftMargin, kRightMargin, kTopMargin, kBottomMargin,
1111
} from '../../src/charts-widget.js';
1212

@@ -199,6 +199,60 @@ describe('computeHistogramLayout', () => {
199199
});
200200
});
201201

202+
describe('hitTestColumn', () => {
203+
// Build a two-bar layout: one tall bar, one very short bar.
204+
const layout = computeHistogramLayout({
205+
bins: [
206+
{ lower: -0.1, upper: 0.0, count: 100, negative: true },
207+
{ lower: 0.0, upper: 0.1, count: 2, negative: false },
208+
],
209+
time_unit: 'ns',
210+
}, 500, 400);
211+
const { bars, chartArea } = layout;
212+
// Sanity: the second bar is much shorter than the first.
213+
assert.ok(bars[1].height < bars[0].height / 5);
214+
215+
it('hits a bar by clicking within its rendered area', () => {
216+
const cx = bars[0].x + bars[0].width / 2;
217+
const cy = bars[0].y + bars[0].height / 2;
218+
assert.equal(hitTestColumn(bars, chartArea, cx, cy), bars[0]);
219+
});
220+
221+
it('hits a short bar by clicking above it in the same column', () => {
222+
// Click in the middle of the column, well above the tiny bar.
223+
const cx = bars[1].x + bars[1].width / 2;
224+
const cy = chartArea.top + 5; // near the top of the chart
225+
assert.equal(hitTestColumn(bars, chartArea, cx, cy), bars[1]);
226+
});
227+
228+
it('returns null when clicking outside the chart area vertically', () => {
229+
const cx = bars[0].x + bars[0].width / 2;
230+
assert.equal(hitTestColumn(bars, chartArea, cx, chartArea.top - 1), null);
231+
assert.equal(hitTestColumn(bars, chartArea, cx, chartArea.bottom + 1), null);
232+
});
233+
234+
it('returns null when clicking between bars (gap region)', () => {
235+
// The gap is between bar 0's right edge and bar 1's left edge.
236+
const cx = bars[0].x + bars[0].width + 1; // in the gap
237+
const cy = (chartArea.top + chartArea.bottom) / 2;
238+
// Only null if the point is truly outside both bars' x ranges.
239+
const hit = hitTestColumn(bars, chartArea, cx, cy);
240+
if (cx < bars[1].x) {
241+
assert.equal(hit, null);
242+
}
243+
});
244+
245+
it('returns null for null bars or chartArea', () => {
246+
assert.equal(hitTestColumn(null, chartArea, 100, 200), null);
247+
assert.equal(hitTestColumn(bars, null, 100, 200), null);
248+
});
249+
250+
it('returns null when clicking left of all bars', () => {
251+
const cy = (chartArea.top + chartArea.bottom) / 2;
252+
assert.equal(hitTestColumn(bars, chartArea, bars[0].x - 5, cy), null);
253+
});
254+
});
255+
202256
describe('ChartsWidget debug charts', () => {
203257
it('ignores histogram hover and click handlers while a debug chart is active', async () => {
204258
const { app, widget } = createWidget();

0 commit comments

Comments
 (0)