Skip to content

Commit 5c0910b

Browse files
auto scroll to bar plot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 330aadb commit 5c0910b

1 file changed

Lines changed: 22 additions & 32 deletions

File tree

client/src/components/Barplot.jsx

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import * as d3 from "d3";
33

44
const MARGIN = { top: 16, right: 48, bottom: 8, left: 160 };
55
const MIN_WIDTH = 220;
6-
const MIN_HEIGHT = 240;
7-
const MAX_BANDWIDTH = 20;
8-
const BAND_PADDING = 0.2;
6+
const BAR_HEIGHT = 18;
7+
const BAR_GAP = 4;
8+
const ROW_HEIGHT = BAR_HEIGHT + BAR_GAP;
9+
const PADDING_INNER = BAR_GAP / ROW_HEIGHT;
10+
const TICK_FONT_SIZE = 15;
11+
const LABEL_FONT_SIZE = 14;
912

1013
/**
1114
* React wrapper around the original D3 barplot from client/js/main.js (barplot_new).
@@ -19,7 +22,7 @@ export default function Barplot({ data, onTickClick, disabled }) {
1922
const svgRef = useRef(null);
2023
const onTickClickRef = useRef(onTickClick);
2124
const disabledRef = useRef(disabled);
22-
const [size, setSize] = useState({ width: 0, height: 0 });
25+
const [width, setWidth] = useState(0);
2326

2427
useEffect(() => {
2528
onTickClickRef.current = onTickClick;
@@ -33,10 +36,7 @@ export default function Barplot({ data, onTickClick, disabled }) {
3336
if (!el) return;
3437
const update = () => {
3538
const rect = el.getBoundingClientRect();
36-
setSize({
37-
width: Math.max(MIN_WIDTH, Math.floor(rect.width)),
38-
height: Math.max(MIN_HEIGHT, Math.floor(rect.height)),
39-
});
39+
setWidth(Math.max(MIN_WIDTH, Math.floor(rect.width)));
4040
};
4141
update();
4242
const ro = new ResizeObserver(update);
@@ -51,49 +51,40 @@ export default function Barplot({ data, onTickClick, disabled }) {
5151
d3.select(svgEl).selectAll("*").remove();
5252

5353
if (!data || data.length === 0) return;
54-
if (size.width === 0 || size.height === 0) return;
54+
if (width === 0) return;
5555

5656
const plotData = data.slice(0, 50);
57+
const n = plotData.length;
5758

58-
const { width: outerW, height: outerH } = size;
59-
const width = outerW - MARGIN.left - MARGIN.right;
60-
const height = outerH - MARGIN.top - MARGIN.bottom;
59+
const outerW = width;
60+
const contentHeight = n * ROW_HEIGHT - BAR_GAP;
61+
const outerH = MARGIN.top + contentHeight + MARGIN.bottom;
62+
const innerWidth = outerW - MARGIN.left - MARGIN.right;
6163

6264
d3.select(svgEl)
6365
.attr("viewBox", `0 0 ${outerW} ${outerH}`)
64-
.attr("preserveAspectRatio", "none")
6566
.attr("width", outerW)
6667
.attr("height", outerH);
6768

68-
// Fill the full height. If bandwidth would exceed MAX_BANDWIDTH, increase
69-
// padding instead of shrinking the range — bars stay capped, gaps absorb
70-
// the extra space.
71-
const n = plotData.length;
72-
const naturalBandwidth = (height / (n + BAND_PADDING)) * (1 - BAND_PADDING);
73-
const bandPadding =
74-
naturalBandwidth > MAX_BANDWIDTH
75-
? (height - MAX_BANDWIDTH * n) / (height + MAX_BANDWIDTH)
76-
: BAND_PADDING;
77-
7869
const svg = d3
7970
.select(svgEl)
8071
.append("g")
8172
.attr("transform", `translate(${MARGIN.left},${MARGIN.top})`)
8273
.attr("fill", "white");
8374

8475
const xMax = d3.max(plotData, (d) => d.prob) || 1;
85-
const xScale = d3.scaleLinear().domain([0, xMax]).range([0, width]);
76+
const xScale = d3.scaleLinear().domain([0, xMax]).range([0, innerWidth]);
8677
const yScale = d3
8778
.scaleBand()
8879
.domain(plotData.map((d) => d.text))
89-
.range([0, height])
90-
.padding(bandPadding);
80+
.range([0, contentHeight])
81+
.paddingInner(PADDING_INNER)
82+
.paddingOuter(0);
9183

9284
const yAxis = svg.append("g").call(d3.axisLeft(yScale));
93-
const tickFontSize = Math.min(15, Math.max(5, yScale.bandwidth() * 0.9));
9485
yAxis
9586
.selectAll("text")
96-
.style("font-size", `${tickFontSize}px`)
87+
.style("font-size", `${TICK_FONT_SIZE}px`)
9788
.style("font-weight", "500");
9889

9990
yAxis
@@ -132,7 +123,6 @@ export default function Barplot({ data, onTickClick, disabled }) {
132123
.attr("height", yScale.bandwidth())
133124
.attr("width", (d) => xScale(d.prob));
134125

135-
const labelFontSize = Math.min(14, Math.max(5, yScale.bandwidth() * 0.9));
136126
svg
137127
.selectAll(".label")
138128
.data(plotData)
@@ -143,14 +133,14 @@ export default function Barplot({ data, onTickClick, disabled }) {
143133
.attr("y", (d) => yScale(d.text) + yScale.bandwidth() / 2)
144134
.attr("dy", "0.35em")
145135
.attr("dx", "8px")
146-
.style("font-size", `${labelFontSize}px`)
136+
.style("font-size", `${LABEL_FONT_SIZE}px`)
147137
.text((d) => (Number(d.prob) === 0 ? "<0.01" : d.prob));
148-
}, [data, size]);
138+
}, [data, width]);
149139

150140
const hasData = data && data.length > 0;
151141

152142
return (
153-
<div ref={containerRef} className="w-full h-full relative">
143+
<div ref={containerRef} className="w-full h-full relative overflow-y-auto">
154144
{hasData ? (
155145
<svg ref={svgRef} className="barplot-svg block" />
156146
) : (

0 commit comments

Comments
 (0)