Skip to content

Commit 3fc2e11

Browse files
pfillion42claude
andcommitted
fix: improve UsageChart readability with proper SVG coordinates
Rewrite line chart with pixel-based viewBox (600x200), readable font sizes, explicit X/Y axes, and proper date label formatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c2c71e0 commit 3fc2e11

1 file changed

Lines changed: 81 additions & 107 deletions

File tree

client/src/components/UsageChart.tsx

Lines changed: 81 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,25 @@ interface UsageChartProps {
55
accesses: UsageDataPoint[];
66
}
77

8-
const CHART_HEIGHT = 140;
9-
const CHART_PADDING = { top: 10, right: 10, bottom: 30, left: 40 };
8+
const SVG_WIDTH = 600;
9+
const SVG_HEIGHT = 200;
10+
const PADDING = { top: 16, right: 16, bottom: 36, left: 44 };
1011

1112
export function UsageChart({ creations, accesses }: UsageChartProps) {
12-
// Fusionner toutes les dates uniques et trier par ordre ASC
1313
const allDates = Array.from(new Set([
1414
...creations.map(c => c.date),
1515
...accesses.map(a => a.date),
1616
])).sort();
1717

18-
// Construire les maps pour acces rapide
1918
const creationMap = new Map(creations.map(c => [c.date, c.count]));
2019
const accessMap = new Map(accesses.map(a => [a.date, a.count]));
2120

22-
// Trouver le max pour calculer les hauteurs
2321
const maxCount = Math.max(
2422
1,
2523
...creations.map(c => c.count),
2624
...accesses.map(a => a.count),
2725
);
2826

29-
// Limiter a 30 points visibles (les plus recents)
3027
const visibleDates = allDates.slice(-30);
3128

3229
if (visibleDates.length === 0) {
@@ -37,110 +34,93 @@ export function UsageChart({ creations, accesses }: UsageChartProps) {
3734
);
3835
}
3936

40-
const drawWidth = 100 - CHART_PADDING.left - CHART_PADDING.right;
41-
const drawHeight = CHART_HEIGHT - CHART_PADDING.top - CHART_PADDING.bottom;
37+
const plotW = SVG_WIDTH - PADDING.left - PADDING.right;
38+
const plotH = SVG_HEIGHT - PADDING.top - PADDING.bottom;
4239

43-
// Calculer les positions X et Y pour chaque point
4440
function getX(i: number): number {
45-
if (visibleDates.length === 1) return CHART_PADDING.left + drawWidth / 2;
46-
return CHART_PADDING.left + (i / (visibleDates.length - 1)) * drawWidth;
41+
if (visibleDates.length === 1) return PADDING.left + plotW / 2;
42+
return PADDING.left + (i / (visibleDates.length - 1)) * plotW;
4743
}
4844

4945
function getY(count: number): number {
50-
return CHART_PADDING.top + drawHeight - (count / maxCount) * drawHeight;
46+
return PADDING.top + plotH - (count / maxCount) * plotH;
5147
}
5248

53-
// Construire les paths SVG pour les 2 series
54-
function buildPath(dataMap: Map<string, number>): string {
49+
function buildLine(dataMap: Map<string, number>): string {
5550
return visibleDates.map((date, i) => {
56-
const count = dataMap.get(date) || 0;
57-
const x = getX(i);
58-
const y = getY(count);
59-
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
51+
const c = dataMap.get(date) || 0;
52+
return `${i === 0 ? 'M' : 'L'}${getX(i)},${getY(c)}`;
6053
}).join(' ');
6154
}
6255

63-
// Construire le path de l'aire sous la courbe
64-
function buildAreaPath(dataMap: Map<string, number>): string {
65-
const linePath = buildPath(dataMap);
66-
const lastX = getX(visibleDates.length - 1);
67-
const firstX = getX(0);
68-
const baseY = CHART_PADDING.top + drawHeight;
69-
return `${linePath} L ${lastX} ${baseY} L ${firstX} ${baseY} Z`;
56+
function buildArea(dataMap: Map<string, number>): string {
57+
const line = buildLine(dataMap);
58+
const baseY = PADDING.top + plotH;
59+
return `${line} L${getX(visibleDates.length - 1)},${baseY} L${getX(0)},${baseY} Z`;
7060
}
7161

72-
const creationPath = buildPath(creationMap);
73-
const accessPath = buildPath(accessMap);
74-
const creationAreaPath = buildAreaPath(creationMap);
75-
const accessAreaPath = buildAreaPath(accessMap);
76-
77-
// Graduations Y (3-4 niveaux)
62+
// Graduations Y
63+
const yStep = Math.ceil(maxCount / 4) || 1;
7864
const yTicks: number[] = [];
79-
const step = Math.ceil(maxCount / 3);
80-
for (let v = 0; v <= maxCount; v += step) {
81-
yTicks.push(v);
82-
}
83-
if (!yTicks.includes(maxCount)) yTicks.push(maxCount);
65+
for (let v = 0; v <= maxCount; v += yStep) yTicks.push(v);
66+
if (yTicks[yTicks.length - 1] < maxCount) yTicks.push(maxCount);
8467

85-
// Labels X (afficher max ~6 labels pour eviter le chevauchement)
86-
const labelStep = Math.max(1, Math.floor(visibleDates.length / 6));
68+
// Labels X : afficher max ~8 labels
69+
const xLabelStep = Math.max(1, Math.ceil(visibleDates.length / 8));
70+
71+
function formatLabel(date: string): string {
72+
// YYYY-MM-DD -> MM-DD, YYYY-WXX -> WXX, YYYY-MM -> YYYY-MM
73+
if (date.includes('-W')) return date.slice(5); // W06
74+
if (date.length === 10) return date.slice(5); // 02-14
75+
return date; // 2026-02
76+
}
8777

8878
return (
8979
<div>
9080
{/* Legende */}
9181
<div data-testid="usage-legend" style={{
9282
display: 'flex',
93-
gap: '16px',
83+
gap: '20px',
9484
marginBottom: '12px',
9585
fontSize: '12px',
9686
color: 'var(--text-secondary)',
9787
}}>
9888
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
9989
<div style={{
100-
width: '16px',
101-
height: '3px',
102-
borderRadius: '2px',
103-
background: 'var(--accent-primary)',
90+
width: '20px', height: '3px', borderRadius: '2px',
91+
backgroundColor: 'var(--accent-primary)',
10492
}} />
10593
<span>Creations</span>
10694
</div>
10795
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
10896
<div style={{
109-
width: '16px',
110-
height: '3px',
111-
borderRadius: '2px',
97+
width: '20px', height: '3px', borderRadius: '2px',
11298
backgroundColor: 'var(--info)',
11399
}} />
114100
<span>Acces</span>
115101
</div>
116102
</div>
117103

118-
{/* Line chart SVG */}
104+
{/* SVG line chart */}
119105
<svg
120106
data-testid="usage-line-chart"
121-
viewBox={`0 0 100 ${CHART_HEIGHT}`}
122-
preserveAspectRatio="none"
123-
style={{ width: '100%', height: `${CHART_HEIGHT}px` }}
107+
viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`}
108+
style={{ width: '100%', height: 'auto' }}
124109
>
125-
{/* Lignes de grille horizontales */}
110+
{/* Grille horizontale + labels Y (quantite) */}
126111
{yTicks.map(v => {
127112
const y = getY(v);
128113
return (
129-
<g key={`tick-${v}`}>
114+
<g key={`y-${v}`}>
130115
<line
131-
x1={CHART_PADDING.left}
132-
y1={y}
133-
x2={CHART_PADDING.left + drawWidth}
134-
y2={y}
135-
stroke="var(--border-default)"
136-
strokeWidth="0.2"
137-
strokeDasharray="1,1"
116+
x1={PADDING.left} y1={y}
117+
x2={SVG_WIDTH - PADDING.right} y2={y}
118+
stroke="var(--border-default)" strokeWidth="1"
119+
strokeDasharray="4,4" opacity="0.5"
138120
/>
139121
<text
140-
x={CHART_PADDING.left - 2}
141-
y={y + 1.2}
142-
textAnchor="end"
143-
fontSize="3.5"
122+
x={PADDING.left - 8} y={y + 4}
123+
textAnchor="end" fontSize="11"
144124
fill="var(--text-muted)"
145125
>
146126
{v}
@@ -149,85 +129,79 @@ export function UsageChart({ creations, accesses }: UsageChartProps) {
149129
);
150130
})}
151131

152-
{/* Aire sous les courbes (fond transparent) */}
153-
<path
154-
d={creationAreaPath}
155-
fill="var(--accent-primary)"
156-
opacity="0.1"
132+
{/* Axe X (bas) */}
133+
<line
134+
x1={PADDING.left} y1={PADDING.top + plotH}
135+
x2={SVG_WIDTH - PADDING.right} y2={PADDING.top + plotH}
136+
stroke="var(--border-default)" strokeWidth="1"
157137
/>
158-
<path
159-
d={accessAreaPath}
160-
fill="var(--info)"
161-
opacity="0.08"
138+
139+
{/* Axe Y (gauche) */}
140+
<line
141+
x1={PADDING.left} y1={PADDING.top}
142+
x2={PADDING.left} y2={PADDING.top + plotH}
143+
stroke="var(--border-default)" strokeWidth="1"
162144
/>
163145

146+
{/* Aire sous les courbes */}
147+
<path d={buildArea(creationMap)} fill="var(--accent-primary)" opacity="0.12" />
148+
<path d={buildArea(accessMap)} fill="var(--info)" opacity="0.10" />
149+
164150
{/* Ligne creations */}
165151
<path
166152
data-testid="line-creation"
167-
d={creationPath}
168-
fill="none"
169-
stroke="var(--accent-primary)"
170-
strokeWidth="0.6"
171-
strokeLinecap="round"
172-
strokeLinejoin="round"
153+
d={buildLine(creationMap)}
154+
fill="none" stroke="var(--accent-primary)"
155+
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
173156
/>
174157

175158
{/* Ligne acces */}
176159
<path
177160
data-testid="line-access"
178-
d={accessPath}
179-
fill="none"
180-
stroke="var(--info)"
181-
strokeWidth="0.6"
182-
strokeLinecap="round"
183-
strokeLinejoin="round"
161+
d={buildLine(accessMap)}
162+
fill="none" stroke="var(--info)"
163+
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
184164
/>
185165

186-
{/* Points sur les courbes */}
166+
{/* Points */}
187167
{visibleDates.map((date, i) => {
188-
const creationCount = creationMap.get(date) || 0;
189-
const accessCount = accessMap.get(date) || 0;
168+
const cc = creationMap.get(date) || 0;
169+
const ac = accessMap.get(date) || 0;
190170
return (
191171
<g key={date}>
192-
{creationCount > 0 && (
172+
{cc > 0 && (
193173
<circle
194174
data-testid="point-creation"
195-
cx={getX(i)}
196-
cy={getY(creationCount)}
197-
r="0.8"
198-
fill="var(--accent-primary)"
175+
cx={getX(i)} cy={getY(cc)} r="3.5"
176+
fill="var(--accent-primary)" stroke="var(--bg-surface)" strokeWidth="1.5"
199177
>
200-
<title>{`${date} - Creations: ${creationCount}`}</title>
178+
<title>{`${date} Creations: ${cc}`}</title>
201179
</circle>
202180
)}
203-
{accessCount > 0 && (
181+
{ac > 0 && (
204182
<circle
205183
data-testid="point-access"
206-
cx={getX(i)}
207-
cy={getY(accessCount)}
208-
r="0.8"
209-
fill="var(--info)"
184+
cx={getX(i)} cy={getY(ac)} r="3.5"
185+
fill="var(--info)" stroke="var(--bg-surface)" strokeWidth="1.5"
210186
>
211-
<title>{`${date} - Acces: ${accessCount}`}</title>
187+
<title>{`${date} Acces: ${ac}`}</title>
212188
</circle>
213189
)}
214190
</g>
215191
);
216192
})}
217193

218-
{/* Labels X */}
194+
{/* Labels X (dates) */}
219195
{visibleDates.map((date, i) => {
220-
if (i % labelStep !== 0 && i !== visibleDates.length - 1) return null;
196+
if (i % xLabelStep !== 0 && i !== visibleDates.length - 1) return null;
221197
return (
222198
<text
223-
key={`label-${date}`}
224-
x={getX(i)}
225-
y={CHART_HEIGHT - 5}
226-
textAnchor="middle"
227-
fontSize="3"
199+
key={`x-${date}`}
200+
x={getX(i)} y={PADDING.top + plotH + 20}
201+
textAnchor="middle" fontSize="11"
228202
fill="var(--text-muted)"
229203
>
230-
{date.length > 7 ? date.slice(5) : date}
204+
{formatLabel(date)}
231205
</text>
232206
);
233207
})}

0 commit comments

Comments
 (0)