@@ -24,6 +24,7 @@ const colors = [
2424
2525String strokeColorClass (int i) => 'downloads-chart-stroke-${colors [i ]}' ;
2626String fillColorClass (int i) => 'downloads-chart-fill-${colors [i ]}' ;
27+ String squareColorClass (int i) => 'downloads-chart-square-${colors [i ]}' ;
2728
2829void create (HTMLElement element, Map <String , String > options) {
2930 final dataPoints = options['points' ];
@@ -35,12 +36,19 @@ void create(HTMLElement element, Map<String, String> options) {
3536 if (versionsRadio == null ) {
3637 throw UnsupportedError ('data-downloads-chart-versions-radio required' );
3738 }
39+ Element createNewSvg () {
40+ return document.createElementNS ('http://www.w3.org/2000/svg' , 'svg' )
41+ ..setAttribute ('height' , '100%' )
42+ ..setAttribute ('width' , '100%' );
43+ }
3844
39- final svg = document.createElementNS ('http://www.w3.org/2000/svg' , 'svg' );
40- svg.setAttribute ('height' , '100%' );
41- svg.setAttribute ('width' , '100%' );
45+ var svg = createNewSvg ();
4246 element.append (svg);
4347
48+ final toolTip = HTMLDivElement ()
49+ ..setAttribute ('class' , 'downloads-chart-tooltip' );
50+ document.body! .appendChild (toolTip);
51+
4452 final data = WeeklyVersionDownloadCounts .fromJson ((utf8.decoder
4553 .fuse (json.decoder)
4654 .convert (base64Decode (dataPoints)) as Map <String , dynamic >));
@@ -80,15 +88,19 @@ void create(HTMLElement element, Map<String, String> options) {
8088 throw UnsupportedError ('Unsupported versions-radio value: "$value "' );
8189 }
8290 radioButton.onClick.listen ((e) {
83- drawChart (svg, displayList, data.newestDate);
91+ element.removeChild (svg);
92+ svg = createNewSvg ();
93+ element.append (svg);
94+ drawChart (svg, toolTip, displayList, data.newestDate);
8495 });
8596 });
8697
87- drawChart (svg, majorDisplayLists, data.newestDate);
98+ drawChart (svg, toolTip, majorDisplayLists, data.newestDate);
8899}
89100
90101void drawChart (
91102 Element svg,
103+ HTMLDivElement toolTip,
92104 ({List <String > ranges, List <List <int >> weekLists}) displayLists,
93105 DateTime newestDate,
94106 {bool stacked = false }) {
@@ -103,12 +115,14 @@ void drawChart(
103115 final leftPadding = 30 ;
104116 final rightPadding = 70 ; // Make extra room for labels on y-axis
105117 final chartWidth = frameWidth - leftPadding - rightPadding;
106- final chartheight = 420 ;
118+ final chartHeight = 420 ;
119+
120+ final toolTipOffsetFromMouse = 15 ;
107121
108122 DateTime computeDateForWeekNumber (
109- DateTime newestDate, int totalWeeks, int weekNumber ) {
123+ DateTime newestDate, int totalWeeks, int weekIndex ) {
110124 return newestDate.copyWith (
111- day: newestDate.day - 7 * (totalWeeks - weekNumber - 1 ));
125+ day: newestDate.day - 7 * (totalWeeks - weekIndex - 1 ));
112126 }
113127
114128 /// Computes max value on y-axis such that we get a nice division for the
@@ -143,7 +157,7 @@ void drawChart(
143157 final x = leftPadding +
144158 chartWidth * duration.inMilliseconds / xAxisSpan.inMilliseconds;
145159
146- final y = topPadding + (chartheight - chartheight * (downloads / maxY));
160+ final y = topPadding + (chartHeight - chartHeight * (downloads / maxY));
147161 return (x, y);
148162 }
149163
@@ -222,7 +236,7 @@ void drawChart(
222236 clipPath.setAttribute ('id' , 'clipRect' );
223237 final clipRect = SVGRectElement ();
224238 clipRect.setAttribute ('y' , '$yMax ' );
225- clipRect.setAttribute ('height' , '${chartheight - (lineThickness / 2 )}' );
239+ clipRect.setAttribute ('height' , '${chartHeight - (lineThickness / 2 )}' );
226240 clipRect.setAttribute ('x' , '$xZero ' );
227241 clipRect.setAttribute ('width' , '$chartWidth ' );
228242 clipPath.append (clipRect);
@@ -291,4 +305,86 @@ void drawChart(
291305 legendLabel.getBBox ().width +
292306 labelPadding;
293307 }
308+
309+ final cursor = SVGLineElement ()
310+ ..setAttribute ('class' , 'downloads-chart-cursor' )
311+ ..setAttribute ('stroke-dasharray' , '15,3' )
312+ ..setAttribute ('x1' , '0' )
313+ ..setAttribute ('x2' , '0' )
314+ ..setAttribute ('y1' , '$yZero ' )
315+ ..setAttribute ('y2' , '$yMax ' );
316+ chart.append (cursor);
317+
318+ // Setup mouse handling
319+
320+ DateTime ? lastSelectedDay;
321+ void hideCursor (_) {
322+ cursor.setAttribute ('style' , 'opacity:0' );
323+ toolTip.setAttribute ('style' , 'opacity:0;position:absolute;' );
324+ lastSelectedDay = null ;
325+ }
326+
327+ hideCursor (1 );
328+
329+ svg.onMouseMove.listen ((e) {
330+ final boundingRect = chart.getBoundingClientRect ();
331+ if (e.x < boundingRect.x + xZero ||
332+ e.x > boundingRect.x + xMax ||
333+ e.y < boundingRect.y + yMax ||
334+ e.y > boundingRect.y + yZero) {
335+ // We are outside the actual chart area
336+ hideCursor (1 );
337+ return ;
338+ }
339+
340+ cursor.setAttribute ('style' , 'opacity:1' );
341+ toolTip.setAttribute (
342+ 'style' ,
343+ 'top:${e .y + toolTipOffsetFromMouse + document .scrollingElement !.scrollTop }px;'
344+ 'left:${e .x }px;' );
345+
346+ final pointPercentage =
347+ (e.x - chart.getBoundingClientRect ().x - xZero) / chartWidth;
348+ final nearestIndex = ((values.length - 1 ) * pointPercentage).round ();
349+
350+ final selectedDay =
351+ computeDateForWeekNumber (newestDate, values.length, nearestIndex);
352+ if (selectedDay == lastSelectedDay) return ;
353+
354+ final coords = computeCoordinates (selectedDay, 0 );
355+ cursor.setAttribute ('transform' , 'translate(${coords .$1 }, 0)' );
356+
357+ final startDay = selectedDay.subtract (Duration (days: 7 ));
358+ toolTip.replaceChildren (HTMLDivElement ()
359+ ..setAttribute ('class' , 'downloads-chart-tooltip-date' )
360+ ..text =
361+ '${formatAbbrMonthDay (startDay )} - ${formatAbbrMonthDay (selectedDay )}' );
362+
363+ final downloads = values[nearestIndex];
364+ for (int i = 0 ; i < downloads.length; i++ ) {
365+ final index = ranges.length - 1 - i;
366+ if (downloads[index] > 0 ) {
367+ // We only show the exact download count in the tooltip if it is non-zero.
368+ final square = HTMLDivElement ()
369+ ..setAttribute (
370+ 'class' , 'downloads-chart-tooltip-square ${squareColorClass (i )}' );
371+ final rangeText = HTMLSpanElement ()..text = '${ranges [index ]}: ' ;
372+ final tooltipRange = HTMLDivElement ()
373+ ..setAttribute ('class' , 'downloads-chart-tooltip-row' )
374+ ..append (square)
375+ ..append (rangeText);
376+ final downloadsText = HTMLSpanElement ()
377+ ..setAttribute ('class' , 'downloads-chart-tooltip-downloads' )
378+ ..text = '${formatWithThousandSeperators (downloads [index ])}' ;
379+ final tooltipRow = HTMLDivElement ()
380+ ..setAttribute ('class' , 'downloads-chart-tooltip-row' )
381+ ..append (tooltipRange)
382+ ..append (downloadsText);
383+ toolTip.append (tooltipRow);
384+ }
385+ }
386+ lastSelectedDay = selectedDay;
387+ });
388+
389+ svg.onMouseLeave.listen (hideCursor);
294390}
0 commit comments