Skip to content

Commit 8680a91

Browse files
committed
wip: fix chart tags reflow approach
1 parent db508c4 commit 8680a91

3 files changed

Lines changed: 319 additions & 103 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { useRef, useEffect, useState, useCallback } from "react";
2+
import chartData from "@site/src/generated/daily-chart.json";
3+
import styles from "./HomeDailyChart.styles.module.css";
4+
5+
/**
6+
* Compute available width fraction at a given SVG y-coordinate.
7+
*
8+
* The chart line goes from bottom-left (y≈300, x≈0) to top-right (y≈0, x≈1000).
9+
* At a given y level, the available horizontal space for tags (to the left of
10+
* the chart line) is the x-coordinate where the line crosses that y.
11+
*
12+
* Returns a value between 0 (no space) and 1 (full width available).
13+
*/
14+
function getAvailableWidthAtSvgY(svgY: number): number {
15+
const { dayPoints, width: svgW, height: svgH } = chartData;
16+
17+
if (svgY <= 0) return 1;
18+
if (svgY >= svgH) return 0;
19+
20+
for (let i = 0; i < dayPoints.length - 1; i++) {
21+
const [x1, y1] = dayPoints[i];
22+
const [x2, y2] = dayPoints[i + 1];
23+
24+
// Check if svgY falls between y1 and y2 (chart is generally monotonic
25+
// in y but may have small wiggles)
26+
if ((y1 >= svgY && y2 <= svgY) || (y1 <= svgY && y2 >= svgY)) {
27+
const dy = y2 - y1;
28+
const t = Math.abs(dy) > 0.001 ? (svgY - y1) / dy : 0;
29+
return (x1 + t * (x2 - x1)) / svgW;
30+
}
31+
}
32+
33+
return 1;
34+
}
35+
36+
export interface TagItem {
37+
key: string;
38+
node: React.ReactNode;
39+
}
40+
41+
interface RowData {
42+
tags: TagItem[];
43+
maxWidthPct: number;
44+
}
45+
46+
const TAG_HEIGHT_PX = 30;
47+
const SAFETY_FACTOR = 0.88;
48+
49+
/**
50+
* ChartTagCloud renders stat tags in the empty triangular space above the
51+
* chart line. On desktop, it measures each tag's width and distributes them
52+
* into rows whose max-width is derived from the actual chart data (via
53+
* dayPoints), creating a data-aware layout that follows the chart's shape.
54+
* On mobile, it falls back to simple flex-wrap.
55+
*/
56+
export const ChartTagCloud: React.FC<{
57+
tags: TagItem[];
58+
}> = ({ tags }) => {
59+
const measureRef = useRef<HTMLDivElement>(null);
60+
const [layoutRows, setLayoutRows] = useState<RowData[] | null>(null);
61+
const [desktop, setDesktop] = useState(false);
62+
63+
// Detect desktop vs mobile (matches CSS media query at 500px)
64+
useEffect(() => {
65+
const check = () => {
66+
const isDesktop = typeof window === "object" && window.innerWidth > 500;
67+
setDesktop(isDesktop);
68+
if (!isDesktop) {
69+
setLayoutRows(null);
70+
}
71+
};
72+
check();
73+
window.addEventListener("resize", check);
74+
return () => window.removeEventListener("resize", check);
75+
}, []);
76+
77+
// Reset layout when tags change
78+
useEffect(() => {
79+
setLayoutRows(null);
80+
}, [tags]);
81+
82+
// Compute data-aware row layout on desktop
83+
const computeLayout = useCallback(() => {
84+
if (!desktop || !measureRef.current || tags.length === 0) {
85+
return;
86+
}
87+
88+
const container = measureRef.current;
89+
const containerWidth = container.offsetWidth;
90+
if (containerWidth === 0) return;
91+
92+
// Measure each tag's rendered width
93+
const tagWidths: number[] = [];
94+
for (let i = 0; i < container.children.length; i++) {
95+
const child = container.children[i] as HTMLElement;
96+
tagWidths.push(child.offsetWidth + 5); // 5px margin-right
97+
}
98+
99+
// SVG rendered height = containerWidth × (svgHeight / svgWidth)
100+
const svgAspect = chartData.height / chartData.width;
101+
const renderedSvgHeight = containerWidth * svgAspect;
102+
103+
// For a given row index, compute the max available width in pixels
104+
const getRowMaxWidthPx = (row: number): number => {
105+
const rowCenterY = (row + 0.5) * TAG_HEIGHT_PX;
106+
const rowSvgY = (rowCenterY / renderedSvgHeight) * chartData.height;
107+
return getAvailableWidthAtSvgY(rowSvgY) * containerWidth * SAFETY_FACTOR;
108+
};
109+
110+
// Distribute tags into rows
111+
const rows: RowData[] = [];
112+
let currentTags: TagItem[] = [];
113+
let currentRowWidth = 0;
114+
let rowIndex = 0;
115+
116+
for (let i = 0; i < tags.length; i++) {
117+
const tagWidth = tagWidths[i] || 100;
118+
const rowMaxWidth = getRowMaxWidthPx(rowIndex);
119+
120+
if (currentTags.length > 0 && currentRowWidth + tagWidth > rowMaxWidth) {
121+
// Finish current row
122+
rows.push({
123+
tags: currentTags,
124+
maxWidthPct: (getRowMaxWidthPx(rows.length) / containerWidth) * 100,
125+
});
126+
currentTags = [];
127+
currentRowWidth = 0;
128+
rowIndex++;
129+
}
130+
131+
currentTags.push(tags[i]);
132+
currentRowWidth += tagWidth;
133+
}
134+
135+
if (currentTags.length > 0) {
136+
rows.push({
137+
tags: currentTags,
138+
maxWidthPct: (getRowMaxWidthPx(rows.length) / containerWidth) * 100,
139+
});
140+
}
141+
142+
setLayoutRows(rows);
143+
}, [tags, desktop]);
144+
145+
// Run layout computation after render (when measureRef is available)
146+
useEffect(() => {
147+
if (desktop && !layoutRows) {
148+
computeLayout();
149+
}
150+
}, [desktop, layoutRows, computeLayout]);
151+
152+
// Desktop: render with computed row layout
153+
if (desktop && layoutRows) {
154+
return (
155+
<div className={styles.chartBreakdownTags}>
156+
{layoutRows.map((row, i) => (
157+
<div
158+
key={i}
159+
className={styles.chartBreakdownTagRow}
160+
style={{ maxWidth: `${row.maxWidthPct}%` }}
161+
>
162+
{row.tags.map((tag) => (
163+
<div key={tag.key} className={styles.chartBreakdownTag}>
164+
{tag.node}
165+
</div>
166+
))}
167+
</div>
168+
))}
169+
</div>
170+
);
171+
}
172+
173+
// Mobile or measurement pass
174+
const isMeasurementPass = desktop && !layoutRows;
175+
return (
176+
<div
177+
ref={measureRef}
178+
className={styles.chartBreakdownTags}
179+
style={
180+
isMeasurementPass ? { visibility: "hidden", width: "100%" } : undefined
181+
}
182+
>
183+
{tags.map((tag) => (
184+
<div key={tag.key} className={styles.chartBreakdownTag}>
185+
{tag.node}
186+
</div>
187+
))}
188+
</div>
189+
);
190+
};

site/src/components/HomeDailyChart/HomeDailyChart.styles.module.css

Lines changed: 17 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,24 @@
9595
max-width: 100%;
9696
}
9797

98-
.chartBreakdownTags {
99-
position: absolute;
98+
.chartAreaWrapper {
99+
position: relative;
100100
}
101101

102-
.chartBreakdownTagsTopRow {
103-
max-width: 100%;
102+
.chartBreakdownTags {
103+
position: absolute;
104+
top: 0;
105+
left: 0;
106+
width: 100%;
107+
z-index: 1;
108+
pointer-events: none;
104109
}
105110

106-
.chartBreakdownTagsSubsequentRows {
107-
max-width: 70%;
111+
.chartBreakdownTagRow {
112+
display: flex;
113+
flex-wrap: wrap;
114+
line-height: 1;
115+
margin-bottom: 2px;
108116
}
109117

110118
.chartBreakdownTag {
@@ -230,7 +238,6 @@
230238

231239
.homeChartDesktop {
232240
display: block;
233-
padding-top: 20px;
234241
}
235242

236243
.homeChartMobile {
@@ -250,39 +257,6 @@
250257
}
251258
}
252259

253-
@media screen and (max-width: 920px) {
254-
.chartTagMax920 {
255-
display: none;
256-
}
257-
}
258-
259-
@media screen and (max-width: 999px) {
260-
.chartBreakdownTags {
261-
height: 100px;
262-
overflow: visible;
263-
}
264-
265-
.chartBreakdownTags > div {
266-
max-width: none;
267-
}
268-
269-
.homeChartDesktop {
270-
padding-top: 80px;
271-
}
272-
}
273-
274-
@media screen and (max-width: 800px) {
275-
.homeChartDesktop {
276-
padding-top: 100px;
277-
}
278-
}
279-
280-
@media screen and (max-width: 600px) {
281-
.homeChartDesktop {
282-
padding-top: 150px;
283-
}
284-
}
285-
286260
@media screen and (max-width: 500px) {
287261
.chartTitle span {
288262
border-left: 0px;
@@ -302,12 +276,11 @@
302276
.chartBreakdownTags {
303277
position: relative;
304278
top: -5px;
305-
height: 160px;
306-
overflow: visible;
279+
pointer-events: auto;
307280
}
308281

309-
.chartBreakdownTags > div {
310-
max-width: none;
282+
.chartAreaWrapper {
283+
position: static;
311284
}
312285

313286
.chartBreakdownTag {

0 commit comments

Comments
 (0)