@@ -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
1112export 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