diff --git a/app/test/frontend/static_files_test.dart b/app/test/frontend/static_files_test.dart index e3fdc0b0d8..070f081965 100644 --- a/app/test/frontend/static_files_test.dart +++ b/app/test/frontend/static_files_test.dart @@ -212,7 +212,7 @@ void main() { test('script.dart.js and parts size check', () { final file = cache.getFile('/static/js/script.dart.js'); expect(file, isNotNull); - expect((file!.bytes.length / 1024).round(), closeTo(336, 20)); + expect((file!.bytes.length / 1024).round(), closeTo(345, 20)); final parts = cache.paths .where((path) => diff --git a/pkg/web_app/lib/src/widget/downloads_chart/widget.dart b/pkg/web_app/lib/src/widget/downloads_chart/widget.dart index b5b4c4bf1b..c6f82e1cc4 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/widget.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/widget.dart @@ -24,6 +24,7 @@ const colors = [ String strokeColorClass(int i) => 'downloads-chart-stroke-${colors[i]}'; String fillColorClass(int i) => 'downloads-chart-fill-${colors[i]}'; +String squareColorClass(int i) => 'downloads-chart-square-${colors[i]}'; void create(HTMLElement element, Map options) { final dataPoints = options['points']; @@ -35,12 +36,19 @@ void create(HTMLElement element, Map options) { if (versionsRadio == null) { throw UnsupportedError('data-downloads-chart-versions-radio required'); } + Element createNewSvg() { + return document.createElementNS('http://www.w3.org/2000/svg', 'svg') + ..setAttribute('height', '100%') + ..setAttribute('width', '100%'); + } - final svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('height', '100%'); - svg.setAttribute('width', '100%'); + var svg = createNewSvg(); element.append(svg); + final toolTip = HTMLDivElement() + ..setAttribute('class', 'downloads-chart-tooltip'); + document.body!.appendChild(toolTip); + final data = WeeklyVersionDownloadCounts.fromJson((utf8.decoder .fuse(json.decoder) .convert(base64Decode(dataPoints)) as Map)); @@ -80,15 +88,19 @@ void create(HTMLElement element, Map options) { throw UnsupportedError('Unsupported versions-radio value: "$value"'); } radioButton.onClick.listen((e) { - drawChart(svg, displayList, data.newestDate); + element.removeChild(svg); + svg = createNewSvg(); + element.append(svg); + drawChart(svg, toolTip, displayList, data.newestDate); }); }); - drawChart(svg, majorDisplayLists, data.newestDate); + drawChart(svg, toolTip, majorDisplayLists, data.newestDate); } void drawChart( Element svg, + HTMLDivElement toolTip, ({List ranges, List> weekLists}) displayLists, DateTime newestDate, {bool stacked = false}) { @@ -103,12 +115,14 @@ void drawChart( final leftPadding = 30; final rightPadding = 70; // Make extra room for labels on y-axis final chartWidth = frameWidth - leftPadding - rightPadding; - final chartheight = 420; + final chartHeight = 420; + + final toolTipOffsetFromMouse = 15; DateTime computeDateForWeekNumber( - DateTime newestDate, int totalWeeks, int weekNumber) { + DateTime newestDate, int totalWeeks, int weekIndex) { return newestDate.copyWith( - day: newestDate.day - 7 * (totalWeeks - weekNumber - 1)); + day: newestDate.day - 7 * (totalWeeks - weekIndex - 1)); } /// Computes max value on y-axis such that we get a nice division for the @@ -143,7 +157,7 @@ void drawChart( final x = leftPadding + chartWidth * duration.inMilliseconds / xAxisSpan.inMilliseconds; - final y = topPadding + (chartheight - chartheight * (downloads / maxY)); + final y = topPadding + (chartHeight - chartHeight * (downloads / maxY)); return (x, y); } @@ -222,7 +236,7 @@ void drawChart( clipPath.setAttribute('id', 'clipRect'); final clipRect = SVGRectElement(); clipRect.setAttribute('y', '$yMax'); - clipRect.setAttribute('height', '${chartheight - (lineThickness / 2)}'); + clipRect.setAttribute('height', '${chartHeight - (lineThickness / 2)}'); clipRect.setAttribute('x', '$xZero'); clipRect.setAttribute('width', '$chartWidth'); clipPath.append(clipRect); @@ -291,4 +305,86 @@ void drawChart( legendLabel.getBBox().width + labelPadding; } + + final cursor = SVGLineElement() + ..setAttribute('class', 'downloads-chart-cursor') + ..setAttribute('stroke-dasharray', '15,3') + ..setAttribute('x1', '0') + ..setAttribute('x2', '0') + ..setAttribute('y1', '$yZero') + ..setAttribute('y2', '$yMax'); + chart.append(cursor); + + // Setup mouse handling + + DateTime? lastSelectedDay; + void hideCursor(_) { + cursor.setAttribute('style', 'opacity:0'); + toolTip.setAttribute('style', 'opacity:0;position:absolute;'); + lastSelectedDay = null; + } + + hideCursor(1); + + svg.onMouseMove.listen((e) { + final boundingRect = chart.getBoundingClientRect(); + if (e.x < boundingRect.x + xZero || + e.x > boundingRect.x + xMax || + e.y < boundingRect.y + yMax || + e.y > boundingRect.y + yZero) { + // We are outside the actual chart area + hideCursor(1); + return; + } + + cursor.setAttribute('style', 'opacity:1'); + toolTip.setAttribute( + 'style', + 'top:${e.y + toolTipOffsetFromMouse + document.scrollingElement!.scrollTop}px;' + 'left:${e.x}px;'); + + final pointPercentage = + (e.x - chart.getBoundingClientRect().x - xZero) / chartWidth; + final nearestIndex = ((values.length - 1) * pointPercentage).round(); + + final selectedDay = + computeDateForWeekNumber(newestDate, values.length, nearestIndex); + if (selectedDay == lastSelectedDay) return; + + final coords = computeCoordinates(selectedDay, 0); + cursor.setAttribute('transform', 'translate(${coords.$1}, 0)'); + + final startDay = selectedDay.subtract(Duration(days: 7)); + toolTip.replaceChildren(HTMLDivElement() + ..setAttribute('class', 'downloads-chart-tooltip-date') + ..text = + '${formatAbbrMonthDay(startDay)} - ${formatAbbrMonthDay(selectedDay)}'); + + final downloads = values[nearestIndex]; + for (int i = 0; i < downloads.length; i++) { + final index = ranges.length - 1 - i; + if (downloads[index] > 0) { + // We only show the exact download count in the tooltip if it is non-zero. + final square = HTMLDivElement() + ..setAttribute( + 'class', 'downloads-chart-tooltip-square ${squareColorClass(i)}'); + final rangeText = HTMLSpanElement()..text = '${ranges[index]}: '; + final tooltipRange = HTMLDivElement() + ..setAttribute('class', 'downloads-chart-tooltip-row') + ..append(square) + ..append(rangeText); + final downloadsText = HTMLSpanElement() + ..setAttribute('class', 'downloads-chart-tooltip-downloads') + ..text = '${formatWithThousandSeperators(downloads[index])}'; + final tooltipRow = HTMLDivElement() + ..setAttribute('class', 'downloads-chart-tooltip-row') + ..append(tooltipRange) + ..append(downloadsText); + toolTip.append(tooltipRow); + } + } + lastSelectedDay = selectedDay; + }); + + svg.onMouseLeave.listen(hideCursor); } diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index deb6f70f36..d304010cd7 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -299,6 +299,49 @@ stroke: var(--pub-downloads-chart-frame-color); } + .downloads-chart-tooltip { + border-radius: 5px; + margin: 0px; + padding: 2px 8px; + box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px; + background-color:var(--pub-downloads-chart-tooltip-background); + // opacity: 0.9; + z-index: 100000; + position: absolute; + border: 0.5px solid var(--pub-downloads-chart-frame-color); + } + + .downloads-chart-tooltip-square { + display: flex; + height: 12px; + width: 12px; + border: 1px solid; + margin-right: 5px; + margin-left: 5px; + } + .downloads-chart-tooltip-date { + font-size: small; + color: var(--pub-score_label-text-color);; + } + .downloads-chart-tooltip-downloads { + padding-right: 4px; + padding-left: 10px; + } + + .downloads-chart-tooltip-row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + font-size: small; + color: var(--pub-score_label-text-color); + } + + .downloads-chart-cursor { + stroke: var(--pub-score_label-text-color); + stroke-width: 1; + } + .downloads-chart-x-axis { fill: none; stroke-width: 1; @@ -347,6 +390,36 @@ fill:var(--pub-downloads-chart-color-5); } + .downloads-chart-square-blue { + background-color:var(--pub-downloads-chart-color-bg-0); + color:var(--pub-downloads-chart-color-0); + } + + .downloads-chart-square-red { + background-color:var(--pub-downloads-chart-color-bg-1); + color: var(--pub-downloads-chart-color-1); + } + + .downloads-chart-square-green { + background-color:var(--pub-downloads-chart-color-bg-2); + color: var(--pub-downloads-chart-color-2) + } + + .downloads-chart-square-purple { + background-color:var(--pub-downloads-chart-color-bg-3); + color:var(--pub-downloads-chart-color-3); + } + + .downloads-chart-square-orange { + background-color:var(--pub-downloads-chart-color-bg-4); + color:var(--pub-downloads-chart-color-4); + } + + .downloads-chart-square-turquoise { + background-color:var(--pub-downloads-chart-color-bg-5); + color:var(--pub-downloads-chart-color-5); + } + .downloads-chart-line { fill: none; stroke-width: 2; diff --git a/pkg/web_css/lib/src/_variables.scss b/pkg/web_css/lib/src/_variables.scss index 90763059db..4c84df7dd2 100644 --- a/pkg/web_css/lib/src/_variables.scss +++ b/pkg/web_css/lib/src/_variables.scss @@ -81,11 +81,17 @@ --mdc-typography-font-family: var(--pub-default-text-font_family); --pub-downloads-chart-color-0: var(--pub-markdown-alert-note); + --pub-downloads-chart-color-bg-0: rgb(9, 105, 218, 0.3); --pub-downloads-chart-color-1: var(--pub-markdown-alert-caution); + --pub-downloads-chart-color-bg-1:rgb(207, 34, 46, 0.3); --pub-downloads-chart-color-2: var(--pub-markdown-alert-tip); + --pub-downloads-chart-color-bg-2: rgb(26, 127, 55, 0.3); --pub-downloads-chart-color-3: var(--pub-markdown-alert-important); + --pub-downloads-chart-color-bg-3: rgb(130, 80, 223, 0.3); --pub-downloads-chart-color-4: var(--pub-markdown-alert-warning); + --pub-downloads-chart-color-bg-4:rgb(154, 103, 0, 0.3); --pub-downloads-chart-color-5: #12a4af; + --pub-downloads-chart-color-bg-5: rgb(18, 164, 175, 0.3); } /// Variables that are specific to the light theme. @@ -137,6 +143,7 @@ --pub-tag_sdkbadge-text-color: #1967d2; --pub-downloads-chart-frame-color: #d3d3d3; + --pub-downloads-chart-tooltip-background: rgba(255, 255, 255, 0.9); } .light-theme { @@ -195,6 +202,7 @@ --pub-tag_sdkbadge-text-color: var(--pub-neutral-textColor); --pub-downloads-chart-frame-color: #55585a; + --pub-downloads-chart-tooltip-background: rgba(18, 19, 23, 0.9); // Material Design theme customizations --mdc-theme-surface: var(--pub-neutral-bgColor);