@@ -5,6 +5,9 @@ interface UsageChartProps {
55 accesses : UsageDataPoint [ ] ;
66}
77
8+ const CHART_HEIGHT = 140 ;
9+ const CHART_PADDING = { top : 10 , right : 10 , bottom : 30 , left : 40 } ;
10+
811export function UsageChart ( { creations, accesses } : UsageChartProps ) {
912 // Fusionner toutes les dates uniques et trier par ordre ASC
1013 const allDates = Array . from ( new Set ( [
@@ -23,7 +26,7 @@ export function UsageChart({ creations, accesses }: UsageChartProps) {
2326 ...accesses . map ( a => a . count ) ,
2427 ) ;
2528
26- // Limiter a 30 barres visibles (les plus recentes )
29+ // Limiter a 30 points visibles (les plus recents )
2730 const visibleDates = allDates . slice ( - 30 ) ;
2831
2932 if ( visibleDates . length === 0 ) {
@@ -34,112 +37,201 @@ export function UsageChart({ creations, accesses }: UsageChartProps) {
3437 ) ;
3538 }
3639
40+ const drawWidth = 100 - CHART_PADDING . left - CHART_PADDING . right ;
41+ const drawHeight = CHART_HEIGHT - CHART_PADDING . top - CHART_PADDING . bottom ;
42+
43+ // Calculer les positions X et Y pour chaque point
44+ 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 ;
47+ }
48+
49+ function getY ( count : number ) : number {
50+ return CHART_PADDING . top + drawHeight - ( count / maxCount ) * drawHeight ;
51+ }
52+
53+ // Construire les paths SVG pour les 2 series
54+ function buildPath ( dataMap : Map < string , number > ) : string {
55+ 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 } ` ;
60+ } ) . join ( ' ' ) ;
61+ }
62+
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` ;
70+ }
71+
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)
78+ 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 ) ;
84+
85+ // Labels X (afficher max ~6 labels pour eviter le chevauchement)
86+ const labelStep = Math . max ( 1 , Math . floor ( visibleDates . length / 6 ) ) ;
87+
3788 return (
3889 < div >
3990 { /* Legende */ }
4091 < div data-testid = "usage-legend" style = { {
4192 display : 'flex' ,
4293 gap : '16px' ,
43- marginBottom : '16px ' ,
94+ marginBottom : '12px ' ,
4495 fontSize : '12px' ,
4596 color : 'var(--text-secondary)' ,
4697 } } >
4798 < div style = { { display : 'flex' , alignItems : 'center' , gap : '6px' } } >
4899 < div style = { {
49- width : '12px ' ,
50- height : '12px ' ,
100+ width : '16px ' ,
101+ height : '3px ' ,
51102 borderRadius : '2px' ,
52- background : 'var(--accent-gradient, var(--accent- primary) )' ,
103+ background : 'var(--accent-primary)' ,
53104 } } />
54105 < span > Creations</ span >
55106 </ div >
56107 < div style = { { display : 'flex' , alignItems : 'center' , gap : '6px' } } >
57108 < div style = { {
58- width : '12px ' ,
59- height : '12px ' ,
109+ width : '16px ' ,
110+ height : '3px ' ,
60111 borderRadius : '2px' ,
61112 backgroundColor : 'var(--info)' ,
62113 } } />
63114 < span > Acces</ span >
64115 </ div >
65116 </ div >
66117
67- { /* Barres */ }
68- < div style = { {
69- display : 'flex' ,
70- alignItems : 'flex-end' ,
71- gap : '4px' ,
72- height : '120px' ,
73- overflow : 'hidden' ,
74- } } >
75- { visibleDates . map ( date => {
118+ { /* Line chart SVG */ }
119+ < svg
120+ data-testid = "usage-line-chart"
121+ viewBox = { `0 0 100 ${ CHART_HEIGHT } ` }
122+ preserveAspectRatio = "none"
123+ style = { { width : '100%' , height : `${ CHART_HEIGHT } px` } }
124+ >
125+ { /* Lignes de grille horizontales */ }
126+ { yTicks . map ( v => {
127+ const y = getY ( v ) ;
128+ return (
129+ < g key = { `tick-${ v } ` } >
130+ < 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"
138+ />
139+ < text
140+ x = { CHART_PADDING . left - 2 }
141+ y = { y + 1.2 }
142+ textAnchor = "end"
143+ fontSize = "3.5"
144+ fill = "var(--text-muted)"
145+ >
146+ { v }
147+ </ text >
148+ </ g >
149+ ) ;
150+ } ) }
151+
152+ { /* Aire sous les courbes (fond transparent) */ }
153+ < path
154+ d = { creationAreaPath }
155+ fill = "var(--accent-primary)"
156+ opacity = "0.1"
157+ />
158+ < path
159+ d = { accessAreaPath }
160+ fill = "var(--info)"
161+ opacity = "0.08"
162+ />
163+
164+ { /* Ligne creations */ }
165+ < path
166+ 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"
173+ />
174+
175+ { /* Ligne acces */ }
176+ < path
177+ data-testid = "line-access"
178+ d = { accessPath }
179+ fill = "none"
180+ stroke = "var(--info)"
181+ strokeWidth = "0.6"
182+ strokeLinecap = "round"
183+ strokeLinejoin = "round"
184+ />
185+
186+ { /* Points sur les courbes */ }
187+ { visibleDates . map ( ( date , i ) => {
76188 const creationCount = creationMap . get ( date ) || 0 ;
77189 const accessCount = accessMap . get ( date ) || 0 ;
78- const creationHeight = ( creationCount / maxCount ) * 100 ;
79- const accessHeight = ( accessCount / maxCount ) * 100 ;
190+ return (
191+ < g key = { date } >
192+ { creationCount > 0 && (
193+ < circle
194+ data-testid = "point-creation"
195+ cx = { getX ( i ) }
196+ cy = { getY ( creationCount ) }
197+ r = "0.8"
198+ fill = "var(--accent-primary)"
199+ >
200+ < title > { `${ date } - Creations: ${ creationCount } ` } </ title >
201+ </ circle >
202+ ) }
203+ { accessCount > 0 && (
204+ < circle
205+ data-testid = "point-access"
206+ cx = { getX ( i ) }
207+ cy = { getY ( accessCount ) }
208+ r = "0.8"
209+ fill = "var(--info)"
210+ >
211+ < title > { `${ date } - Acces: ${ accessCount } ` } </ title >
212+ </ circle >
213+ ) }
214+ </ g >
215+ ) ;
216+ } ) }
80217
218+ { /* Labels X */ }
219+ { visibleDates . map ( ( date , i ) => {
220+ if ( i % labelStep !== 0 && i !== visibleDates . length - 1 ) return null ;
81221 return (
82- < div key = { date } style = { {
83- display : 'flex' ,
84- flexDirection : 'column' ,
85- alignItems : 'center' ,
86- flex : 1 ,
87- minWidth : 0 ,
88- height : '100%' ,
89- justifyContent : 'flex-end' ,
90- } } >
91- < div style = { {
92- display : 'flex' ,
93- gap : '1px' ,
94- alignItems : 'flex-end' ,
95- width : '100%' ,
96- height : '100%' ,
97- } } >
98- { /* Barre creation */ }
99- < div
100- data-testid = "bar-creation"
101- title = { `Creations: ${ creationCount } ` }
102- style = { {
103- flex : 1 ,
104- height : `${ Math . max ( creationHeight , creationCount > 0 ? 4 : 0 ) } %` ,
105- background : 'var(--accent-gradient, var(--accent-primary))' ,
106- borderRadius : '2px 2px 0 0' ,
107- minHeight : creationCount > 0 ? '4px' : '0' ,
108- transition : 'height 0.3s ease' ,
109- } }
110- />
111- { /* Barre acces */ }
112- < div
113- data-testid = "bar-access"
114- title = { `Acces: ${ accessCount } ` }
115- style = { {
116- flex : 1 ,
117- height : `${ Math . max ( accessHeight , accessCount > 0 ? 4 : 0 ) } %` ,
118- backgroundColor : 'var(--info)' ,
119- borderRadius : '2px 2px 0 0' ,
120- minHeight : accessCount > 0 ? '4px' : '0' ,
121- opacity : 0.8 ,
122- transition : 'height 0.3s ease' ,
123- } }
124- />
125- </ div >
126- { /* Label date (abrege) */ }
127- < div style = { {
128- fontSize : '9px' ,
129- color : 'var(--text-muted)' ,
130- marginTop : '4px' ,
131- textAlign : 'center' ,
132- overflow : 'hidden' ,
133- textOverflow : 'ellipsis' ,
134- whiteSpace : 'nowrap' ,
135- maxWidth : '100%' ,
136- } } >
137- { date . length > 7 ? date . slice ( 5 ) : date }
138- </ div >
139- </ div >
222+ < text
223+ key = { `label-${ date } ` }
224+ x = { getX ( i ) }
225+ y = { CHART_HEIGHT - 5 }
226+ textAnchor = "middle"
227+ fontSize = "3"
228+ fill = "var(--text-muted)"
229+ >
230+ { date . length > 7 ? date . slice ( 5 ) : date }
231+ </ text >
140232 ) ;
141233 } ) }
142- </ div >
234+ </ svg >
143235 </ div >
144236 ) ;
145237}
0 commit comments