Skip to content

Commit 8b91673

Browse files
committed
fix: HiDPI canvas rendering for Stats charts and RelationGraph
- All canvas elements now scale by devicePixelRatio for crisp Retina display - Stats BarChart and Histogram use CSS dimensions with DPI-scaled backing store - RelationGraph canvas DPI-aware with all coordinate transforms updated
1 parent 762b0fb commit 8b91673

2 files changed

Lines changed: 59 additions & 28 deletions

File tree

β€Žpackages/dashboard/src/pages/RelationGraph.tsxβ€Ž

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ export default function RelationGraph() {
138138
const canvas = canvasRef.current!;
139139
const rect = canvas.getBoundingClientRect();
140140
return {
141-
cx: (e.clientX - rect.left) * (canvas.width / rect.width),
142-
cy: (e.clientY - rect.top) * (canvas.height / rect.height),
141+
cx: (e.clientX - rect.left) * (canvas.width / rect.width) / dprRef.current,
142+
cy: (e.clientY - rect.top) * (canvas.height / rect.height) / dprRef.current,
143143
};
144144
};
145145

@@ -180,14 +180,17 @@ export default function RelationGraph() {
180180
const graphW = maxX - minX + padding * 2;
181181
const graphH = maxY - minY + padding * 2;
182182

183-
const scale = Math.min(canvas.width / graphW, canvas.height / graphH, 2.5);
183+
const dpr = dprRef.current;
184+
const cw = canvas.width / dpr;
185+
const ch = canvas.height / dpr;
186+
const scale = Math.min(cw / graphW, ch / graphH, 2.5);
184187
const centerX = (minX + maxX) / 2;
185188
const centerY = (minY + maxY) / 2;
186189

187190
transformRef.current = {
188191
scale,
189-
offsetX: canvas.width / 2 - centerX * scale,
190-
offsetY: canvas.height / 2 - centerY * scale,
192+
offsetX: cw / 2 - centerX * scale,
193+
offsetY: ch / 2 - centerY * scale,
191194
};
192195
}, []);
193196

@@ -200,8 +203,9 @@ export default function RelationGraph() {
200203
const ctx = canvas.getContext('2d');
201204
if (!ctx) return;
202205

203-
const W = canvas.width;
204-
const H = canvas.height;
206+
const dpr = dprRef.current;
207+
const W = canvas.width / dpr;
208+
const H = canvas.height / dpr;
205209
const nodes = nodesRef.current;
206210
const sel = selectedRef.current;
207211
const cam = transformRef.current;
@@ -375,6 +379,24 @@ export default function RelationGraph() {
375379
// Keep selectedRef in sync
376380
useEffect(() => { selectedRef.current = selectedNode; }, [selectedNode]);
377381

382+
// Set up HiDPI canvas
383+
const dprRef = useRef(1);
384+
const canvasInitRef = useRef(false);
385+
useEffect(() => {
386+
const canvas = canvasRef.current;
387+
if (!canvas || canvasInitRef.current) return;
388+
canvasInitRef.current = true;
389+
const dpr = window.devicePixelRatio || 1;
390+
dprRef.current = dpr;
391+
const w = 900, h = 500;
392+
canvas.width = w * dpr;
393+
canvas.height = h * dpr;
394+
canvas.style.width = '100%';
395+
canvas.style.height = 'auto';
396+
const ctx = canvas.getContext('2d');
397+
if (ctx) ctx.scale(dpr, dpr);
398+
}, []);
399+
378400
useEffect(() => {
379401
if (filteredRelations.length > 0) {
380402
alphaRef.current = 1.0; // reheat on data change
@@ -507,8 +529,8 @@ export default function RelationGraph() {
507529
if (!canvas) return;
508530

509531
const rect = canvas.getBoundingClientRect();
510-
const cx = (e.clientX - rect.left) * (canvas.width / rect.width);
511-
const cy = (e.clientY - rect.top) * (canvas.height / rect.height);
532+
const cx = (e.clientX - rect.left) * (canvas.width / rect.width) / dprRef.current;
533+
const cy = (e.clientY - rect.top) * (canvas.height / rect.height) / dprRef.current;
512534

513535
const t = transformRef.current;
514536
const zoomFactor = e.deltaY < 0 ? 1.12 : 1 / 1.12;
@@ -532,8 +554,8 @@ export default function RelationGraph() {
532554
const handler = (e: WheelEvent) => {
533555
e.preventDefault();
534556
const rect = canvas.getBoundingClientRect();
535-
const cx = (e.clientX - rect.left) * (canvas.width / rect.width);
536-
const cy = (e.clientY - rect.top) * (canvas.height / rect.height);
557+
const cx = (e.clientX - rect.left) * (canvas.width / rect.width) / (window.devicePixelRatio || 1);
558+
const cy = (e.clientY - rect.top) * (canvas.height / rect.height) / (window.devicePixelRatio || 1);
537559

538560
const t = transformRef.current;
539561
const zoomFactor = e.deltaY < 0 ? 1.12 : 1 / 1.12;
@@ -564,19 +586,18 @@ export default function RelationGraph() {
564586
};
565587

566588
const getTouchCenter = (touches: React.TouchList) => {
589+
const canvas = canvasRef.current!;
590+
const rect = canvas.getBoundingClientRect();
591+
const dpr = dprRef.current;
567592
if (touches.length < 2) {
568-
const canvas = canvasRef.current!;
569-
const rect = canvas.getBoundingClientRect();
570593
return {
571-
cx: (touches[0]!.clientX - rect.left) * (canvas.width / rect.width),
572-
cy: (touches[0]!.clientY - rect.top) * (canvas.height / rect.height),
594+
cx: (touches[0]!.clientX - rect.left) * (canvas.width / rect.width) / dpr,
595+
cy: (touches[0]!.clientY - rect.top) * (canvas.height / rect.height) / dpr,
573596
};
574597
}
575-
const canvas = canvasRef.current!;
576-
const rect = canvas.getBoundingClientRect();
577598
return {
578-
cx: ((touches[0]!.clientX + touches[1]!.clientX) / 2 - rect.left) * (canvas.width / rect.width),
579-
cy: ((touches[0]!.clientY + touches[1]!.clientY) / 2 - rect.top) * (canvas.height / rect.height),
599+
cx: ((touches[0]!.clientX + touches[1]!.clientX) / 2 - rect.left) * (canvas.width / rect.width) / dpr,
600+
cy: ((touches[0]!.clientY + touches[1]!.clientY) / 2 - rect.top) * (canvas.height / rect.height) / dpr,
580601
};
581602
};
582603

@@ -692,7 +713,7 @@ export default function RelationGraph() {
692713
ref={canvasRef}
693714
width={900}
694715
height={500}
695-
style={{ width: '100%', height: 'auto', background: '#0f0f1a', borderRadius: 8, cursor: dragRef.current.nodeId ? 'grabbing' : dragRef.current.isPanning ? 'grabbing' : 'grab', touchAction: 'none' }}
716+
style={{ width: '100%', height: 'auto', background: '#0f0f1a', borderRadius: 8, cursor: dragRef.current.nodeId ? 'grabbing' : dragRef.current.isPanning ? 'grabbing' : 'grab', touchAction: 'none', imageRendering: 'auto' }}
696717
onMouseDown={onMouseDown}
697718
onMouseMove={onMouseMove}
698719
onMouseUp={onMouseUp}
@@ -705,9 +726,9 @@ export default function RelationGraph() {
705726
{/* Zoom controls - bottom right */}
706727
<div style={{ position: 'absolute', bottom: 40, right: 16, display: 'flex', gap: 4, background: 'rgba(15,15,26,0.8)', borderRadius: 6, padding: 4 }}>
707728
<button style={{ width: 28, height: 28, border: '1px solid rgba(255,255,255,0.15)', borderRadius: 4, background: 'rgba(255,255,255,0.08)', color: '#ccc', fontSize: 16, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
708-
onClick={() => { const t = transformRef.current; const cx = canvasRef.current!.width / 2; const cy = canvasRef.current!.height / 2; const wx = (cx - t.offsetX) / t.scale; const wy = (cy - t.offsetY) / t.scale; const ns = Math.min(t.scale * 1.3, 8); transformRef.current = { scale: ns, offsetX: cx - wx * ns, offsetY: cy - wy * ns }; }}>+</button>
729+
onClick={() => { const t = transformRef.current; const cx = canvasRef.current!.width / 2 / dprRef.current; const cy = canvasRef.current!.height / 2 / dprRef.current; const wx = (cx - t.offsetX) / t.scale; const wy = (cy - t.offsetY) / t.scale; const ns = Math.min(t.scale * 1.3, 8); transformRef.current = { scale: ns, offsetX: cx - wx * ns, offsetY: cy - wy * ns }; }}>+</button>
709730
<button style={{ width: 28, height: 28, border: '1px solid rgba(255,255,255,0.15)', borderRadius: 4, background: 'rgba(255,255,255,0.08)', color: '#ccc', fontSize: 16, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
710-
onClick={() => { const t = transformRef.current; const cx = canvasRef.current!.width / 2; const cy = canvasRef.current!.height / 2; const wx = (cx - t.offsetX) / t.scale; const wy = (cy - t.offsetY) / t.scale; const ns = Math.max(t.scale / 1.3, 0.1); transformRef.current = { scale: ns, offsetX: cx - wx * ns, offsetY: cy - wy * ns }; }}>βˆ’</button>
731+
onClick={() => { const t = transformRef.current; const cx = canvasRef.current!.width / 2 / dprRef.current; const cy = canvasRef.current!.height / 2 / dprRef.current; const wx = (cx - t.offsetX) / t.scale; const wy = (cy - t.offsetY) / t.scale; const ns = Math.max(t.scale / 1.3, 0.1); transformRef.current = { scale: ns, offsetX: cx - wx * ns, offsetY: cy - wy * ns }; }}>βˆ’</button>
711732
<button style={{ width: 28, height: 28, border: '1px solid rgba(255,255,255,0.15)', borderRadius: 4, background: 'rgba(255,255,255,0.08)', color: '#ccc', fontSize: 11, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
712733
onClick={() => { fitToView(); alphaRef.current = Math.max(alphaRef.current, 0.3); }}>Fit</button>
713734
</div>

β€Žpackages/dashboard/src/pages/Stats.tsxβ€Ž

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ function BarChart({ data, colors, height = 220 }: { data: { label: string; value
1010
useEffect(() => {
1111
const canvas = canvasRef.current;
1212
if (!canvas || data.length === 0) return;
13+
const dpr = window.devicePixelRatio || 1;
14+
const rect = canvas.getBoundingClientRect();
15+
canvas.width = rect.width * dpr;
16+
canvas.height = rect.height * dpr;
1317
const ctx = canvas.getContext('2d')!;
14-
const W = canvas.width;
15-
const H = canvas.height;
18+
ctx.scale(dpr, dpr);
19+
const W = rect.width;
20+
const H = rect.height;
1621
const max = Math.max(...data.map(d => d.value), 1);
1722
const barW = Math.min(60, (W - 40) / data.length - 10);
1823
const startX = (W - data.length * (barW + 10) + 10) / 2;
@@ -63,7 +68,7 @@ function BarChart({ data, colors, height = 220 }: { data: { label: string; value
6368
});
6469
}, [data, colors, height]);
6570

66-
return <canvas ref={canvasRef} width={500} height={height} style={{ width: '100%', height: 'auto' }} />;
71+
return <canvas ref={canvasRef} style={{ width: '100%', height: height }} />;
6772
}
6873

6974
// ─── Horizontal Distribution Bar ────────────────────────────────────────────
@@ -113,9 +118,14 @@ function Histogram({ values, label, color }: { values: number[]; label: string;
113118
useEffect(() => {
114119
const canvas = canvasRef.current;
115120
if (!canvas || values.length === 0) return;
121+
const dpr = window.devicePixelRatio || 1;
122+
const rect = canvas.getBoundingClientRect();
123+
canvas.width = rect.width * dpr;
124+
canvas.height = rect.height * dpr;
116125
const ctx = canvas.getContext('2d')!;
117-
const W = canvas.width;
118-
const H = canvas.height;
126+
ctx.scale(dpr, dpr);
127+
const W = rect.width;
128+
const H = rect.height;
119129

120130
// Build 10 buckets [0,0.1), [0.1,0.2), ... [0.9,1.0]
121131
const buckets = new Array(10).fill(0);
@@ -157,7 +167,7 @@ function Histogram({ values, label, color }: { values: number[]; label: string;
157167
ctx.fillText(label, W / 2, H - 0);
158168
}, [values, label, color]);
159169

160-
return <canvas ref={canvasRef} width={300} height={140} style={{ width: '100%', height: 'auto' }} />;
170+
return <canvas ref={canvasRef} style={{ width: '100%', height: 140 }} />;
161171
}
162172

163173
// ─── Main Component ─────────────────────────────────────────────────────────

0 commit comments

Comments
Β (0)