@@ -26,6 +26,25 @@ function renderYearMonth(yearMonth: number | string): string {
2626 return `${ s . substring ( 0 , 4 ) } -${ s . substring ( 4 , 6 ) } ` ;
2727}
2828
29+ function getTooltipElement ( chart : {
30+ canvas : HTMLCanvasElement ;
31+ } ) : HTMLDivElement {
32+ const parent = chart . canvas . parentElement ;
33+ if ( ! parent ) {
34+ throw new Error ( "chart-tooltip: canvas has no parent element" ) ;
35+ }
36+
37+ let el = parent . querySelector ( ".chart-tooltip" ) as HTMLDivElement | null ;
38+
39+ if ( ! el ) {
40+ el = document . createElement ( "div" ) ;
41+ el . className = "chart-tooltip" ;
42+ parent . appendChild ( el ) ;
43+ }
44+
45+ return el ;
46+ }
47+
2948class PopularityChart extends HTMLElement {
3049 connectedCallback ( ) {
3150 const script = this . querySelector ( 'script[type="application/json"]' ) ;
@@ -54,10 +73,117 @@ class PopularityChart extends HTMLElement {
5473 canvas . height = 720 ;
5574 this . appendChild ( canvas ) ;
5675
57- this . drawChart ( canvas , data ) ;
76+ const style = getComputedStyle ( document . documentElement ) ;
77+ const textColor = style . getPropertyValue ( "--bs-body-color" ) ;
78+ const gridColor = style . getPropertyValue ( "--bs-border-color" ) ;
79+
80+ if ( data . labels . length < 3 ) {
81+ this . drawBarChart ( canvas , data , textColor , gridColor ) ;
82+ } else {
83+ this . drawLineChart ( canvas , data , textColor , gridColor ) ;
84+ }
5885 }
5986
60- private async drawChart ( canvas : HTMLCanvasElement , data : ChartData ) {
87+ private async drawBarChart (
88+ canvas : HTMLCanvasElement ,
89+ data : ChartData ,
90+ textColor : string ,
91+ gridColor : string ,
92+ ) {
93+ const {
94+ Chart,
95+ BarElement,
96+ BarController,
97+ CategoryScale,
98+ LinearScale,
99+ Tooltip,
100+ } = await import ( "chart.js" ) ;
101+
102+ Chart . register (
103+ BarElement ,
104+ BarController ,
105+ CategoryScale ,
106+ LinearScale ,
107+ Tooltip ,
108+ ) ;
109+
110+ const lastIndex = data . labels . length - 1 ;
111+ const barData = {
112+ labels : data . datasets . map ( ( ds ) => ds . label ) ,
113+ datasets : [
114+ {
115+ label : renderYearMonth ( data . labels [ lastIndex ] ) ,
116+ data : data . datasets . map ( ( ds ) => ds . data [ lastIndex ] ?? 0 ) ,
117+ backgroundColor : colors . slice ( 0 , data . datasets . length ) ,
118+ } ,
119+ ] ,
120+ } ;
121+
122+ new Chart ( canvas , {
123+ type : "bar" ,
124+ data : barData ,
125+ options : {
126+ indexAxis : "y" ,
127+ animation : false ,
128+ maintainAspectRatio : false ,
129+ plugins : {
130+ legend : {
131+ display : false ,
132+ } ,
133+ tooltip : {
134+ enabled : false ,
135+ external : ( { chart, tooltip } ) => {
136+ const el = getTooltipElement ( chart ) ;
137+
138+ if ( tooltip . opacity === 0 ) {
139+ el . style . opacity = "0" ;
140+ return ;
141+ }
142+
143+ const item = tooltip . dataPoints [ 0 ] ;
144+ el . innerHTML = ( item . raw as number ) . toFixed ( 2 ) ;
145+ el . style . opacity = "1" ;
146+ el . style . left = `${ tooltip . caretX } px` ;
147+ el . style . top = `${ tooltip . caretY } px` ;
148+ } ,
149+ } ,
150+ } ,
151+ scales : {
152+ x : {
153+ min : 0 ,
154+ max : 100 ,
155+ border : {
156+ color : gridColor ,
157+ } ,
158+ grid : {
159+ color : gridColor ,
160+ } ,
161+ ticks : {
162+ color : textColor ,
163+ } ,
164+ } ,
165+ y : {
166+ border : {
167+ color : gridColor ,
168+ } ,
169+ grid : {
170+ display : false ,
171+ } ,
172+ ticks : {
173+ color : textColor ,
174+ } ,
175+ } ,
176+ } ,
177+ } ,
178+ } ) ;
179+ }
180+
181+ private async drawLineChart (
182+ canvas : HTMLCanvasElement ,
183+ data : ChartData ,
184+ textColor : string ,
185+ gridColor : string ,
186+ ) {
61187 const {
62188 Chart,
63189 LineElement,
@@ -79,26 +205,47 @@ class PopularityChart extends HTMLElement {
79205 Tooltip ,
80206 ) ;
81207
82- const style = getComputedStyle ( document . documentElement ) ;
83- const textColor = style . getPropertyValue ( "--bs-body-color" ) ;
84- const gridColor = style . getPropertyValue ( "--bs-border-color" ) ;
85-
86208 new Chart ( canvas , {
87209 type : "line" ,
88210 data,
89211 options : {
212+ animation : false ,
90213 maintainAspectRatio : false ,
91214 interaction : {
92215 mode : "index" ,
93216 intersect : false ,
94217 } ,
95218 plugins : {
96219 tooltip : {
97- displayColors : false ,
220+ enabled : false ,
98221 itemSort : ( a , b ) =>
99222 ( b . raw as number ) - ( a . raw as number ) ,
100- callbacks : {
101- title : ( items ) => renderYearMonth ( items [ 0 ] . label ) ,
223+ external : ( { chart, tooltip } ) => {
224+ const el = getTooltipElement ( chart ) ;
225+
226+ if ( tooltip . opacity === 0 ) {
227+ el . style . opacity = "0" ;
228+ return ;
229+ }
230+
231+ const rows = tooltip . dataPoints
232+ . map ( ( item ) => {
233+ const color =
234+ colors [
235+ item . datasetIndex % colors . length
236+ ] ;
237+ return `<tr>
238+ <td style="color:${ color } ">●</td>
239+ <td>${ item . dataset . label } </td>
240+ <td>${ ( item . raw as number ) . toFixed ( 2 ) } </td>
241+ </tr>` ;
242+ } )
243+ . join ( "" ) ;
244+
245+ el . innerHTML = `<div class="chart-tooltip-title">${ renderYearMonth ( tooltip . title [ 0 ] ) } </div><table>${ rows } </table>` ;
246+ el . style . opacity = "1" ;
247+ el . style . left = `${ tooltip . caretX } px` ;
248+ el . style . top = `${ tooltip . caretY } px` ;
102249 } ,
103250 } ,
104251 legend : {
@@ -110,6 +257,9 @@ class PopularityChart extends HTMLElement {
110257 normalized : true ,
111258 scales : {
112259 x : {
260+ border : {
261+ color : gridColor ,
262+ } ,
113263 ticks : {
114264 callback ( val ) {
115265 return renderYearMonth (
@@ -127,6 +277,9 @@ class PopularityChart extends HTMLElement {
127277 y : {
128278 type : "linear" ,
129279 min : 0 ,
280+ border : {
281+ color : gridColor ,
282+ } ,
130283 grid : {
131284 color : gridColor ,
132285 } ,
0 commit comments