From c8e81b9f9b3b3d51e6167f66d5f8b4929e8caab5 Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Wed, 15 Jan 2025 11:51:52 +0000 Subject: [PATCH 1/6] Experimental: Add downloads version chart - noninteractive --- .../templates/views/pkg/score_tab.dart | 10 +- pkg/_pub_shared/lib/format/date_format.dart | 38 +++ .../src/widget/downloads_chart/widget.dart | 230 +++++++++++++++++- pkg/web_css/lib/src/_pkg.scss | 98 ++++++++ pkg/web_css/lib/src/_variables.scss | 8 + 5 files changed, 379 insertions(+), 5 deletions(-) create mode 100644 pkg/_pub_shared/lib/format/date_format.dart diff --git a/app/lib/frontend/templates/views/pkg/score_tab.dart b/app/lib/frontend/templates/views/pkg/score_tab.dart index ede3e927d8..79d63583e6 100644 --- a/app/lib/frontend/templates/views/pkg/score_tab.dart +++ b/app/lib/frontend/templates/views/pkg/score_tab.dart @@ -12,6 +12,7 @@ import 'package:pub_dev/shared/utils.dart'; import '../../../../scorecard/models.dart' hide ReportStatus; import '../../../../shared/urls.dart' as urls; import '../../../dom/dom.dart' as d; + import '../../../request_context.dart'; import '../../../static_files.dart'; @@ -177,7 +178,7 @@ d.Node _section(ReportSection section) { } d.Node _downloadsChart(WeeklyVersionDownloadCounts weeklyVersionDownloads) { - return d.div( + final container = d.div( classes: ['downloads-chart'], id: '-downloads-chart', attributes: { @@ -186,6 +187,13 @@ d.Node _downloadsChart(WeeklyVersionDownloadCounts weeklyVersionDownloads) { base64Encode(jsonUtf8Encoder.convert(weeklyVersionDownloads)) }, ); + + return d.fragment([ + d.h1( + classes: ['hash-header'], + text: 'Weekly Downloads over the last 40 weeks'), + container, + ]); } final _statusIconUrls = { diff --git a/pkg/_pub_shared/lib/format/date_format.dart b/pkg/_pub_shared/lib/format/date_format.dart new file mode 100644 index 0000000000..e33cf1c93f --- /dev/null +++ b/pkg/_pub_shared/lib/format/date_format.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Formats a DateTime into abbriviated month and day + +String formatAbbrMonthDay(DateTime date) { + final String month; + switch (date.month) { + case 1: + month = 'Jan'; + case 2: + month = 'Feb'; + case 3: + month = 'Mar'; + case 4: + month = 'Apr'; + case 5: + month = 'May'; + case 6: + month = 'Jun'; + case 7: + month = 'Jul'; + case 8: + month = 'Aug'; + case 9: + month = 'Sep'; + case 10: + month = 'Oct'; + case 11: + month = 'Nov'; + case 12: + month = 'Dec'; + default: + month = ''; + } + return '$month ${date.day}'; +} 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 009bf59458..b69392ef7f 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/widget.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/widget.dart @@ -6,10 +6,30 @@ import 'dart:convert'; import 'dart:math' as math; import 'package:_pub_shared/data/download_counts_data.dart'; +import 'package:_pub_shared/format/date_format.dart'; +import 'package:_pub_shared/format/number_format.dart'; import 'package:web/web.dart'; import 'computations.dart'; +const lineColorClasses = [ + 'downloads-chart-line-color-blue', + 'downloads-chart-line-color-red', + 'downloads-chart-line-color-green', + 'downloads-chart-line-color-purple', + 'downloads-chart-line-color-orange', + 'downloads-chart-line-color-turquoise', +]; + +const legendColorClasses = [ + 'downloads-chart-legend-color-blue', + 'downloads-chart-legend-color-red', + 'downloads-chart-legend-color-green', + 'downloads-chart-legend-color-purple', + 'downloads-chart-legend-color-orange', + 'downloads-chart-legend-color-turquoise', +]; + void create(HTMLElement element, Map options) { final dataPoints = options['points']; if (dataPoints == null) { @@ -17,23 +37,225 @@ void create(HTMLElement element, Map options) { } final svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('height', '100%'); + svg.setAttribute('width', '100%'); + element.append(svg); final data = WeeklyVersionDownloadCounts.fromJson((utf8.decoder .fuse(json.decoder) .convert(base64Decode(dataPoints)) as Map)); - final weeksToDisplay = math.min(28, data.totalWeeklyDownloads.length); + final weeksToDisplay = math.min(40, data.totalWeeklyDownloads.length); final majorDisplayLists = prepareWeekLists( data.totalWeeklyDownloads, data.majorRangeWeeklyDownloads, weeksToDisplay, ); - final majorRanges = data.majorRangeWeeklyDownloads.map((e) => e.versionRange); + final majorRanges = + data.majorRangeWeeklyDownloads.map((e) => e.versionRange).toList(); drawChart(svg, majorRanges, majorDisplayLists, data.newestDate); } -void drawChart(Element svg, Iterable ranges, Iterable> values, +void drawChart(Element svg, List ranges, List> values, DateTime newestDate, - {bool stacked = true}) {} + {bool stacked = false}) { + final width = 775; // TODO(zarah): make this width dynamic + final topPadding = 30; + final leftPadding = 30; + final rightPadding = 70; // make extra room for labels on y-axis + final drawingWidth = width - leftPadding - rightPadding; + final chartheight = 420; + + DateTime computeDateForWeekNumber( + DateTime newestDate, int totalWeeks, int weekNumber) { + return newestDate.copyWith( + day: newestDate.day - 7 * (totalWeeks - weekNumber - 1)); + } + + // Computes max value on y-axis such that we get a nice division for the + // interval length between the numbers shown by the tics on the y axis. + (int maxY, int interval) computeMaxYAndInterval(List> values) { + final maxDownloads = + values.fold(1, (a, b) => math.max(a, b.reduce(math.max))); + final digits = maxDownloads.toString().length; + final buffer = StringBuffer()..write('1'); + if (digits > 2) { + buffer.writeAll(List.filled(digits - 2, '0')); + } + final firstDiv = int.parse(buffer.toString()); + final candidates = [firstDiv, 2 * firstDiv, 5 * firstDiv, 10 * firstDiv]; + + for (final d in candidates) { + if (maxDownloads / d <= 10) { + return ((maxDownloads / d).ceil() * d, d); + } + } + // This should not happen! + return (maxDownloads, firstDiv); + } + + final (maxY, interval) = computeMaxYAndInterval(values); + final firstDate = computeDateForWeekNumber(newestDate, values.length, 0); + + (double, double) computeCoordinates(DateTime date, int downloads) { + final xAxisSpan = newestDate.difference(firstDate); + final duration = date.difference(firstDate); + final x = leftPadding + + drawingWidth * duration.inMilliseconds / xAxisSpan.inMilliseconds; + final y = topPadding + (chartheight - chartheight * (downloads / maxY)); + return (x, y); + } + + final chart = SVGGElement(); + svg.append(chart); + + // Axis and tics + + final (xZero, yZero) = computeCoordinates(firstDate, 0); + final (xMax, yMax) = computeCoordinates(newestDate, maxY); + final lineThickness = 1; + final padding = 8; + final ticLength = 10; + final ticLabelYCoor = yZero + ticLength + 2 * padding; + + final xaxis = SVGPathElement(); + xaxis.setAttribute('class', 'downloads-chart-x-axis'); + // We add half of the line thickness at both ends of the x-axis so that it + // covers the vertical tics at the end. + xaxis.setAttribute('d', + 'M${xZero - (lineThickness / 2)} $yZero L${xMax + (lineThickness / 2)} $yZero'); + chart.append(xaxis); + + var firstTicLabel = SVGTextElement(); + for (int week = 0; week < values.length; week += 4) { + final date = computeDateForWeekNumber(newestDate, values.length, week); + final (x, y) = computeCoordinates(date, 0); + + final tic = SVGPathElement(); + tic.setAttribute('class', 'downloads-chart-x-axis'); + tic.setAttribute('d', 'M$x $y l0 $ticLength'); + chart.append(tic); + + final ticLabel = SVGTextElement(); + chart.append(ticLabel); + ticLabel.setAttribute( + 'class', 'downloads-chart-tic-label downloads-chart-tic-label-x'); + ticLabel.text = formatAbbrMonthDay(date); + ticLabel.setAttribute('y', '$ticLabelYCoor'); + ticLabel.setAttribute('x', '$x'); + + if (week == 0) { + firstTicLabel = ticLabel; + } + } + + for (int i = 0; i <= maxY / interval; i++) { + final (x, y) = computeCoordinates(firstDate, i * interval); + + final ticLabel = SVGTextElement(); + ticLabel.setAttribute( + 'class', 'downloads-chart-tic-label downloads-chart-tic-label-y'); + ticLabel.text = + '${compactFormat(i * interval).value}${compactFormat(i * interval).suffix}'; + ticLabel.setAttribute('x', '${xMax + padding}'); + ticLabel.setAttribute('y', '$y'); + chart.append(ticLabel); + + if (i == 0) { + // No long tic in the bottom, we have the x-axis here. + continue; + } + + final longTic = SVGPathElement(); + longTic.setAttribute('class', 'downloads-chart-frame'); + longTic.setAttribute('d', + 'M${xZero - (lineThickness / 2)} $y L${xMax - (lineThickness / 2)} $y'); + chart.append(longTic); + } + + // We use the clipPath to cut the ends of the chart lines so that we don't + // draw outside the frame of the chart. + final clipPath = SVGClipPathElement(); + clipPath.setAttribute('id', 'clipRect'); + final clipRect = SVGRectElement(); + clipRect.setAttribute('y', '$yMax'); + clipRect.setAttribute('height', '${chartheight - (lineThickness / 2)}'); + clipRect.setAttribute('x', '${xZero - (lineThickness / 2)}'); + clipRect.setAttribute('width', '${drawingWidth + lineThickness}'); + clipPath.append(clipRect); + chart.append(clipPath); + + // Chart lines and legends + + final lines = []; + for (int versionRange = 0; versionRange < values[0].length; versionRange++) { + final line = StringBuffer(); + var c = 'M'; + for (int week = 0; week < values.length; week++) { + final (x, y) = computeCoordinates( + computeDateForWeekNumber(newestDate, values.length, week), + values[week][versionRange]); + line.write(' $c$x $y'); + c = 'L'; + } + lines.add(line); + } + + double legendXCoor = xZero - firstTicLabel.getBBox().width / 2; + double legendYCoor = + ticLabelYCoor + firstTicLabel.getBBox().height + 2 * padding; + final legendWidth = 20; + final legendHeight = 8; + + for (int j = 0; j < lines.length; j++) { + final path = SVGPathElement(); + path.setAttribute('class', '${lineColorClasses[j]} downloads-chart-line '); + // We assign colors in revers order so that main colors are chosen first for + // the newest versions. + path.setAttribute('d', '${lines[lines.length - 1 - j]}'); + path.setAttribute('clip-path', 'url(#clipRect)'); + chart.append(path); + + final legend = SVGRectElement(); + chart.append(legend); + legend.setAttribute( + 'class', 'downloads-chart-legend ${legendColorClasses[j]}'); + legend.setAttribute('height', '$legendHeight'); + legend.setAttribute('width', '$legendWidth'); + + final legendLabel = SVGTextElement(); + chart.append(legendLabel); + legendLabel.setAttribute( + 'class', 'downloads-chart-tic-label downloads-chart-tic-label-y'); + legendLabel.text = ranges[j]; + + if (legendXCoor + padding + legendWidth + legendLabel.getBBox().width > + xMax) { + // There is no room for the legend and label. + // Make a new line and update legendXCoor and legendYCoor accordingly. + + legendXCoor = xZero - firstTicLabel.getBBox().width / 2; + legendYCoor += 2 * padding + legendHeight; + } + + legend.setAttribute('x', '$legendXCoor'); + legend.setAttribute('y', '$legendYCoor'); + legendLabel.setAttribute('y', '${legendYCoor + legendHeight / 2}'); + legendLabel.setAttribute('x', '${legendXCoor + padding + legendWidth}'); + + // Update x coordinate for next legend + legendXCoor += + legendWidth + padding + legendLabel.getBBox().width + 2 * padding; + } + + final height = legendYCoor + 3 * padding; + final frame = SVGRectElement(); + chart.append(frame); + frame.setAttribute('height', '$height'); + frame.setAttribute('width', '$width'); + frame.setAttribute('rx', '15'); + frame.setAttribute('ry', '15'); + frame.setAttribute('class', 'downloads-chart-frame'); +} diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index d9e2d8e387..1281311906 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -280,6 +280,104 @@ } } + .downloads-chart { + display: flex; + height: 700px; + width: 775px; + flex-direction: column; + margin-top: 16px; + padding-top: 16px; + } + + .downloads-chart-frame { + fill:transparent; + stroke-width: 1; + stroke: lightgrey; + } + + .downloads-chart-x-axis { + fill: none; + stroke-width: 1; + stroke: var(--pub-score_label-text-color); + } + + .downloads-chart-tic-label { + fill: var(--pub-score_label-text-color); + font-size: small; + } + + .downloads-chart-tic-label-x { + text-anchor: middle; + } + + .downloads-chart-tic-label-y { + dominant-baseline: middle; + } + + .downloads-chart-legend { + fill-opacity: 0.3; + stroke-width: 1; + } + + .downloads-chart-legend-color-blue { + fill:var(--pub-downloads-chart-color-0); + stroke:var(--pub-downloads-chart-color-0); + } + + .downloads-chart-legend-color-red { + fill:var(--pub-downloads-chart-color-1); + stroke:var(--pub-downloads-chart-color-1); + } + + .downloads-chart-legend-color-green { + fill:var(--pub-downloads-chart-color-2); + stroke:var(--pub-downloads-chart-color-2); + } + + .downloads-chart-legend-color-purple { + fill:var(--pub-downloads-chart-color-3); + stroke:var(--pub-downloads-chart-color-3); + } + + .downloads-chart-legend-color-orange { + fill:var(--pub-downloads-chart-color-4); + stroke:var(--pub-downloads-chart-color-4); + } + + .downloads-chart-legend-color-turquoise { + fill:var(--pub-downloads-chart-color-5); + stroke:var(--pub-downloads-chart-color-5); + } + + .downloads-chart-line { + fill: none; + stroke-width: 2; + } + + .downloads-chart-line-color-blue { + stroke: var(--pub-downloads-chart-color-0); + } + + .downloads-chart-line-color-red { + stroke: var(--pub-downloads-chart-color-1); + } + + .downloads-chart-line-color-green { + stroke: var(--pub-downloads-chart-color-2); + } + + .downloads-chart-line-color-purple { + stroke: var(--pub-downloads-chart-color-3); + } + + .downloads-chart-line-color-orange { + stroke: var(--pub-downloads-chart-color-4); + } + + .downloads-chart-line-color-turquoise { + stroke: var(--pub-downloads-chart-color-5); + } + .pkg-page-title-copy { position: relative; display: inline-block; diff --git a/pkg/web_css/lib/src/_variables.scss b/pkg/web_css/lib/src/_variables.scss index fff541cac2..95e7779827 100644 --- a/pkg/web_css/lib/src/_variables.scss +++ b/pkg/web_css/lib/src/_variables.scss @@ -79,6 +79,14 @@ --mdc-theme-primary: #0175c2; --mdc-theme-secondary: #0066d9; --mdc-typography-font-family: var(--pub-default-text-font_family); + + + --pub-downloads-chart-color-0: var(--pub-markdown-alert-note); + --pub-downloads-chart-color-1: var(--pub-markdown-alert-caution); + --pub-downloads-chart-color-2: var(--pub-markdown-alert-tip); + --pub-downloads-chart-color-3: var(--pub-markdown-alert-important); + --pub-downloads-chart-color-4: var(--pub-markdown-alert-warning); + --pub-downloads-chart-color-5: #12a4af; } /// Variables that are specific to the light theme. From 2c02a133d157aa5549be59312477a12a9be1f126 Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Fri, 17 Jan 2025 11:30:26 +0000 Subject: [PATCH 2/6] newline --- app/lib/frontend/templates/views/pkg/score_tab.dart | 1 - pkg/web_css/lib/src/_variables.scss | 1 - 2 files changed, 2 deletions(-) diff --git a/app/lib/frontend/templates/views/pkg/score_tab.dart b/app/lib/frontend/templates/views/pkg/score_tab.dart index 79d63583e6..7e3f2e09d8 100644 --- a/app/lib/frontend/templates/views/pkg/score_tab.dart +++ b/app/lib/frontend/templates/views/pkg/score_tab.dart @@ -12,7 +12,6 @@ import 'package:pub_dev/shared/utils.dart'; import '../../../../scorecard/models.dart' hide ReportStatus; import '../../../../shared/urls.dart' as urls; import '../../../dom/dom.dart' as d; - import '../../../request_context.dart'; import '../../../static_files.dart'; diff --git a/pkg/web_css/lib/src/_variables.scss b/pkg/web_css/lib/src/_variables.scss index 95e7779827..47e01f5d38 100644 --- a/pkg/web_css/lib/src/_variables.scss +++ b/pkg/web_css/lib/src/_variables.scss @@ -80,7 +80,6 @@ --mdc-theme-secondary: #0066d9; --mdc-typography-font-family: var(--pub-default-text-font_family); - --pub-downloads-chart-color-0: var(--pub-markdown-alert-note); --pub-downloads-chart-color-1: var(--pub-markdown-alert-caution); --pub-downloads-chart-color-2: var(--pub-markdown-alert-tip); From 8a25b509de4a162e7a27ee7191f602b5f22df87b Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Mon, 20 Jan 2025 10:37:15 +0000 Subject: [PATCH 3/6] cleanups --- .../templates/views/pkg/score_tab.dart | 4 +- pkg/_pub_shared/lib/format/date_format.dart | 44 ++---- .../src/widget/downloads_chart/widget.dart | 148 +++++++++--------- pkg/web_css/lib/src/_pkg.scss | 39 ++--- pkg/web_css/test/expression_test.dart | 2 + 5 files changed, 111 insertions(+), 126 deletions(-) diff --git a/app/lib/frontend/templates/views/pkg/score_tab.dart b/app/lib/frontend/templates/views/pkg/score_tab.dart index 7e3f2e09d8..7bfda58766 100644 --- a/app/lib/frontend/templates/views/pkg/score_tab.dart +++ b/app/lib/frontend/templates/views/pkg/score_tab.dart @@ -188,9 +188,7 @@ d.Node _downloadsChart(WeeklyVersionDownloadCounts weeklyVersionDownloads) { ); return d.fragment([ - d.h1( - classes: ['hash-header'], - text: 'Weekly Downloads over the last 40 weeks'), + d.h1(text: 'Weekly Downloads over the last 40 weeks'), container, ]); } diff --git a/pkg/_pub_shared/lib/format/date_format.dart b/pkg/_pub_shared/lib/format/date_format.dart index e33cf1c93f..1da402a90f 100644 --- a/pkg/_pub_shared/lib/format/date_format.dart +++ b/pkg/_pub_shared/lib/format/date_format.dart @@ -5,34 +5,20 @@ // Formats a DateTime into abbriviated month and day String formatAbbrMonthDay(DateTime date) { - final String month; - switch (date.month) { - case 1: - month = 'Jan'; - case 2: - month = 'Feb'; - case 3: - month = 'Mar'; - case 4: - month = 'Apr'; - case 5: - month = 'May'; - case 6: - month = 'Jun'; - case 7: - month = 'Jul'; - case 8: - month = 'Aug'; - case 9: - month = 'Sep'; - case 10: - month = 'Oct'; - case 11: - month = 'Nov'; - case 12: - month = 'Dec'; - default: - month = ''; - } + final month = switch (date.month) { + 1 => 'Jan', + 2 => 'Feb', + 3 => 'Mar', + 4 => 'Apr', + 5 => 'May', + 6 => 'Jun', + 7 => 'Jul', + 8 => 'Aug', + 9 => 'Sep', + 10 => 'Oct', + 11 => 'Nov', + _ => 'Dec' + }; + return '$month ${date.day}'; } 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 b69392ef7f..cc96f1309b 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/widget.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/widget.dart @@ -12,23 +12,17 @@ import 'package:web/web.dart'; import 'computations.dart'; -const lineColorClasses = [ - 'downloads-chart-line-color-blue', - 'downloads-chart-line-color-red', - 'downloads-chart-line-color-green', - 'downloads-chart-line-color-purple', - 'downloads-chart-line-color-orange', - 'downloads-chart-line-color-turquoise', +const colors = [ + 'blue', + 'red', + 'green', + 'purple', + 'orange', + 'turquoise', ]; -const legendColorClasses = [ - 'downloads-chart-legend-color-blue', - 'downloads-chart-legend-color-red', - 'downloads-chart-legend-color-green', - 'downloads-chart-legend-color-purple', - 'downloads-chart-legend-color-orange', - 'downloads-chart-legend-color-turquoise', -]; +String strokeColorClass(int i) => 'downloads-chart-stroke-${colors[i]}'; +String fillColorClass(int i) => 'downloads-chart-fill-${colors[i]}'; void create(HTMLElement element, Map options) { final dataPoints = options['points']; @@ -61,11 +55,13 @@ void create(HTMLElement element, Map options) { void drawChart(Element svg, List ranges, List> values, DateTime newestDate, {bool stacked = false}) { - final width = 775; // TODO(zarah): make this width dynamic + if (values.isEmpty) return; + final frameWidth = + 775; // TODO(zarah): Investigate if this width can be dynamic final topPadding = 30; final leftPadding = 30; final rightPadding = 70; // make extra room for labels on y-axis - final drawingWidth = width - leftPadding - rightPadding; + final chartWidth = frameWidth - leftPadding - rightPadding; final chartheight = 420; DateTime computeDateForWeekNumber( @@ -75,7 +71,7 @@ void drawChart(Element svg, List ranges, List> values, } // Computes max value on y-axis such that we get a nice division for the - // interval length between the numbers shown by the tics on the y axis. + // interval length between the numbers shown by the ticks on the y axis. (int maxY, int interval) computeMaxYAndInterval(List> values) { final maxDownloads = values.fold(1, (a, b) => math.max(a, b.reduce(math.max))); @@ -98,12 +94,14 @@ void drawChart(Element svg, List ranges, List> values, final (maxY, interval) = computeMaxYAndInterval(values); final firstDate = computeDateForWeekNumber(newestDate, values.length, 0); + final xAxisSpan = newestDate.difference(firstDate); (double, double) computeCoordinates(DateTime date, int downloads) { - final xAxisSpan = newestDate.difference(firstDate); final duration = date.difference(firstDate); + // We don't risk division by 0 here, since `xAxisSpan` is a non-zero duration. final x = leftPadding + - drawingWidth * duration.inMilliseconds / xAxisSpan.inMilliseconds; + chartWidth * duration.inMilliseconds / xAxisSpan.inMilliseconds; + final y = topPadding + (chartheight - chartheight * (downloads / maxY)); return (x, y); } @@ -111,68 +109,70 @@ void drawChart(Element svg, List ranges, List> values, final chart = SVGGElement(); svg.append(chart); - // Axis and tics + // Axis and ticks final (xZero, yZero) = computeCoordinates(firstDate, 0); final (xMax, yMax) = computeCoordinates(newestDate, maxY); final lineThickness = 1; final padding = 8; - final ticLength = 10; - final ticLabelYCoor = yZero + ticLength + 2 * padding; + final labelPadding = 16; + final tickLength = 10; + final tickLabelYCoordinate = yZero + tickLength + labelPadding; final xaxis = SVGPathElement(); xaxis.setAttribute('class', 'downloads-chart-x-axis'); // We add half of the line thickness at both ends of the x-axis so that it - // covers the vertical tics at the end. + // covers the vertical ticks at the end. xaxis.setAttribute('d', 'M${xZero - (lineThickness / 2)} $yZero L${xMax + (lineThickness / 2)} $yZero'); chart.append(xaxis); - var firstTicLabel = SVGTextElement(); + late SVGTextElement firstTickLabel; + // place a tick every 4 weeks for (int week = 0; week < values.length; week += 4) { final date = computeDateForWeekNumber(newestDate, values.length, week); final (x, y) = computeCoordinates(date, 0); - final tic = SVGPathElement(); - tic.setAttribute('class', 'downloads-chart-x-axis'); - tic.setAttribute('d', 'M$x $y l0 $ticLength'); - chart.append(tic); + final tick = SVGPathElement(); + tick.setAttribute('class', 'downloads-chart-x-axis'); + tick.setAttribute('d', 'M$x $y l0 $tickLength'); + chart.append(tick); - final ticLabel = SVGTextElement(); - chart.append(ticLabel); - ticLabel.setAttribute( - 'class', 'downloads-chart-tic-label downloads-chart-tic-label-x'); - ticLabel.text = formatAbbrMonthDay(date); - ticLabel.setAttribute('y', '$ticLabelYCoor'); - ticLabel.setAttribute('x', '$x'); + final tickLabel = SVGTextElement(); + chart.append(tickLabel); + tickLabel.setAttribute( + 'class', 'downloads-chart-tick-label downloads-chart-tick-label-x'); + tickLabel.text = formatAbbrMonthDay(date); + tickLabel.setAttribute('y', '$tickLabelYCoordinate'); + tickLabel.setAttribute('x', '$x'); if (week == 0) { - firstTicLabel = ticLabel; + firstTickLabel = tickLabel; } } for (int i = 0; i <= maxY / interval; i++) { final (x, y) = computeCoordinates(firstDate, i * interval); - final ticLabel = SVGTextElement(); - ticLabel.setAttribute( - 'class', 'downloads-chart-tic-label downloads-chart-tic-label-y'); - ticLabel.text = + final tickLabel = SVGTextElement(); + tickLabel.setAttribute( + 'class', 'downloads-chart-tick-label downloads-chart-tick-label-y'); + tickLabel.text = '${compactFormat(i * interval).value}${compactFormat(i * interval).suffix}'; - ticLabel.setAttribute('x', '${xMax + padding}'); - ticLabel.setAttribute('y', '$y'); - chart.append(ticLabel); + tickLabel.setAttribute('x', '${xMax + padding}'); + tickLabel.setAttribute('y', '$y'); + chart.append(tickLabel); if (i == 0) { - // No long tic in the bottom, we have the x-axis here. + // No long tick in the bottom, we have the x-axis here. continue; } - final longTic = SVGPathElement(); - longTic.setAttribute('class', 'downloads-chart-frame'); - longTic.setAttribute('d', + final longTick = SVGPathElement(); + longTick.setAttribute('class', 'downloads-chart-frame'); + longTick.setAttribute('d', 'M${xZero - (lineThickness / 2)} $y L${xMax - (lineThickness / 2)} $y'); - chart.append(longTic); + chart.append(longTick); } // We use the clipPath to cut the ends of the chart lines so that we don't @@ -182,8 +182,8 @@ void drawChart(Element svg, List ranges, List> values, final clipRect = SVGRectElement(); clipRect.setAttribute('y', '$yMax'); clipRect.setAttribute('height', '${chartheight - (lineThickness / 2)}'); - clipRect.setAttribute('x', '${xZero - (lineThickness / 2)}'); - clipRect.setAttribute('width', '${drawingWidth + lineThickness}'); + clipRect.setAttribute('x', '$xZero'); + clipRect.setAttribute('width', '$chartWidth'); clipPath.append(clipRect); chart.append(clipPath); @@ -203,59 +203,63 @@ void drawChart(Element svg, List ranges, List> values, lines.add(line); } - double legendXCoor = xZero - firstTicLabel.getBBox().width / 2; + double legendXCoor = xZero; double legendYCoor = - ticLabelYCoor + firstTicLabel.getBBox().height + 2 * padding; + tickLabelYCoordinate + firstTickLabel.getBBox().height + labelPadding; final legendWidth = 20; final legendHeight = 8; - for (int j = 0; j < lines.length; j++) { + for (int i = 0; i < lines.length; i++) { final path = SVGPathElement(); - path.setAttribute('class', '${lineColorClasses[j]} downloads-chart-line '); - // We assign colors in revers order so that main colors are chosen first for + path.setAttribute('class', '${strokeColorClass(i)} downloads-chart-line '); + // We assign colors in reverse order so that main colors are chosen first for // the newest versions. - path.setAttribute('d', '${lines[lines.length - 1 - j]}'); + path.setAttribute('d', '${lines[lines.length - 1 - i]}'); path.setAttribute('clip-path', 'url(#clipRect)'); chart.append(path); final legend = SVGRectElement(); chart.append(legend); - legend.setAttribute( - 'class', 'downloads-chart-legend ${legendColorClasses[j]}'); + legend.setAttribute('class', + 'downloads-chart-legend ${fillColorClass(i)} ${strokeColorClass(i)}'); legend.setAttribute('height', '$legendHeight'); legend.setAttribute('width', '$legendWidth'); final legendLabel = SVGTextElement(); chart.append(legendLabel); - legendLabel.setAttribute( - 'class', 'downloads-chart-tic-label downloads-chart-tic-label-y'); - legendLabel.text = ranges[j]; + legendLabel.setAttribute('class', 'downloads-chart-tick-label'); + if (i == 5) { + // We have an 'other' line + legendLabel.text = 'Other'; + } else { + legendLabel.text = ranges[ranges.length - 1 - i]; + } if (legendXCoor + padding + legendWidth + legendLabel.getBBox().width > xMax) { // There is no room for the legend and label. // Make a new line and update legendXCoor and legendYCoor accordingly. - legendXCoor = xZero - firstTicLabel.getBBox().width / 2; + legendXCoor = xZero; legendYCoor += 2 * padding + legendHeight; } legend.setAttribute('x', '$legendXCoor'); legend.setAttribute('y', '$legendYCoor'); - legendLabel.setAttribute('y', '${legendYCoor + legendHeight / 2}'); + legendLabel.setAttribute('y', '${legendYCoor + legendHeight}'); legendLabel.setAttribute('x', '${legendXCoor + padding + legendWidth}'); // Update x coordinate for next legend legendXCoor += - legendWidth + padding + legendLabel.getBBox().width + 2 * padding; + legendWidth + padding + legendLabel.getBBox().width + labelPadding; } - final height = legendYCoor + 3 * padding; - final frame = SVGRectElement(); + final frameHeight = legendYCoor + padding + labelPadding; + final frame = SVGRectElement() + ..setAttribute('class', 'downloads-chart-frame') + ..setAttribute('height', '$frameHeight') + ..setAttribute('width', '$frameWidth') + ..setAttribute('rx', '15') + ..setAttribute('ry', '15'); chart.append(frame); - frame.setAttribute('height', '$height'); - frame.setAttribute('width', '$width'); - frame.setAttribute('rx', '15'); - frame.setAttribute('ry', '15'); - frame.setAttribute('class', 'downloads-chart-frame'); } diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index 1281311906..0d053f8dae 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -301,16 +301,16 @@ stroke: var(--pub-score_label-text-color); } - .downloads-chart-tic-label { + .downloads-chart-tick-label { fill: var(--pub-score_label-text-color); font-size: small; } - .downloads-chart-tic-label-x { + .downloads-chart-tick-label-x { text-anchor: middle; } - .downloads-chart-tic-label-y { + .downloads-chart-tick-label-y { dominant-baseline: middle; } @@ -319,62 +319,57 @@ stroke-width: 1; } - .downloads-chart-legend-color-blue { + .downloads-chart-fill-blue { fill:var(--pub-downloads-chart-color-0); - stroke:var(--pub-downloads-chart-color-0); } - .downloads-chart-legend-color-red { + .downloads-chart-fill-red { fill:var(--pub-downloads-chart-color-1); - stroke:var(--pub-downloads-chart-color-1); } - .downloads-chart-legend-color-green { + .downloads-chart-fill-green { fill:var(--pub-downloads-chart-color-2); - stroke:var(--pub-downloads-chart-color-2); } - .downloads-chart-legend-color-purple { + .downloads-chart-fill-purple { fill:var(--pub-downloads-chart-color-3); - stroke:var(--pub-downloads-chart-color-3); } - .downloads-chart-legend-color-orange { + .downloads-chart-fill-orange { fill:var(--pub-downloads-chart-color-4); - stroke:var(--pub-downloads-chart-color-4); } - .downloads-chart-legend-color-turquoise { + .downloads-chart-fill-turquoise { fill:var(--pub-downloads-chart-color-5); - stroke:var(--pub-downloads-chart-color-5); } .downloads-chart-line { fill: none; - stroke-width: 2; + stroke-width: 3; + stroke-linejoin: round; } - .downloads-chart-line-color-blue { + .downloads-chart-stroke-blue { stroke: var(--pub-downloads-chart-color-0); } - .downloads-chart-line-color-red { + .downloads-chart-stroke-red { stroke: var(--pub-downloads-chart-color-1); } - .downloads-chart-line-color-green { + .downloads-chart-stroke-green { stroke: var(--pub-downloads-chart-color-2); } - .downloads-chart-line-color-purple { + .downloads-chart-stroke-purple { stroke: var(--pub-downloads-chart-color-3); } - .downloads-chart-line-color-orange { + .downloads-chart-stroke-orange { stroke: var(--pub-downloads-chart-color-4); } - .downloads-chart-line-color-turquoise { + .downloads-chart-stroke-turquoise { stroke: var(--pub-downloads-chart-color-5); } diff --git a/pkg/web_css/test/expression_test.dart b/pkg/web_css/test/expression_test.dart index 9d529d7a85..65cfce6122 100644 --- a/pkg/web_css/test/expression_test.dart +++ b/pkg/web_css/test/expression_test.dart @@ -49,6 +49,8 @@ void main() { (e) => e.startsWith('detail-tab-') && e.endsWith('-content')); expressions.removeWhere((e) => e.startsWith('package-badge-')); expressions.removeWhere((e) => e.startsWith('pub-toc-node-')); + // downloads chart color classes + expressions.removeWhere((e) => e.startsWith('downloads-chart')); // shared CSS file (with dartdoc) expressions.removeAll([ 'cookie-notice-container', From 1b87b7cf7ae26cce8d7a53551e6a3503bae407d6 Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Tue, 21 Jan 2025 09:27:04 +0000 Subject: [PATCH 4/6] renaming --- .../src/widget/downloads_chart/widget.dart | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) 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 cc96f1309b..f4750140be 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/widget.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/widget.dart @@ -114,7 +114,7 @@ void drawChart(Element svg, List ranges, List> values, final (xZero, yZero) = computeCoordinates(firstDate, 0); final (xMax, yMax) = computeCoordinates(newestDate, maxY); final lineThickness = 1; - final padding = 8; + final marginPadding = 8; final labelPadding = 16; final tickLength = 10; final tickLabelYCoordinate = yZero + tickLength + labelPadding; @@ -123,8 +123,9 @@ void drawChart(Element svg, List ranges, List> values, xaxis.setAttribute('class', 'downloads-chart-x-axis'); // We add half of the line thickness at both ends of the x-axis so that it // covers the vertical ticks at the end. - xaxis.setAttribute('d', - 'M${xZero - (lineThickness / 2)} $yZero L${xMax + (lineThickness / 2)} $yZero'); + final xAxisStart = xZero - (lineThickness / 2); + final xAxisEnd = xMax + (lineThickness / 2); + xaxis.setAttribute('d', 'M$xAxisStart $yZero L$xAxisEnd $yZero'); chart.append(xaxis); late SVGTextElement firstTickLabel; @@ -159,7 +160,7 @@ void drawChart(Element svg, List ranges, List> values, 'class', 'downloads-chart-tick-label downloads-chart-tick-label-y'); tickLabel.text = '${compactFormat(i * interval).value}${compactFormat(i * interval).suffix}'; - tickLabel.setAttribute('x', '${xMax + padding}'); + tickLabel.setAttribute('x', '${xMax + marginPadding}'); tickLabel.setAttribute('y', '$y'); chart.append(tickLabel); @@ -170,8 +171,7 @@ void drawChart(Element svg, List ranges, List> values, final longTick = SVGPathElement(); longTick.setAttribute('class', 'downloads-chart-frame'); - longTick.setAttribute('d', - 'M${xZero - (lineThickness / 2)} $y L${xMax - (lineThickness / 2)} $y'); + longTick.setAttribute('d', 'M$xAxisStart} $y L$xAxisEnd $y'); chart.append(longTick); } @@ -235,26 +235,32 @@ void drawChart(Element svg, List ranges, List> values, legendLabel.text = ranges[ranges.length - 1 - i]; } - if (legendXCoor + padding + legendWidth + legendLabel.getBBox().width > + if (legendXCoor + + marginPadding + + legendWidth + + legendLabel.getBBox().width > xMax) { // There is no room for the legend and label. // Make a new line and update legendXCoor and legendYCoor accordingly. legendXCoor = xZero; - legendYCoor += 2 * padding + legendHeight; + legendYCoor += 2 * marginPadding + legendHeight; } legend.setAttribute('x', '$legendXCoor'); legend.setAttribute('y', '$legendYCoor'); legendLabel.setAttribute('y', '${legendYCoor + legendHeight}'); - legendLabel.setAttribute('x', '${legendXCoor + padding + legendWidth}'); + legendLabel.setAttribute( + 'x', '${legendXCoor + marginPadding + legendWidth}'); // Update x coordinate for next legend - legendXCoor += - legendWidth + padding + legendLabel.getBBox().width + labelPadding; + legendXCoor += legendWidth + + marginPadding + + legendLabel.getBBox().width + + labelPadding; } - final frameHeight = legendYCoor + padding + labelPadding; + final frameHeight = legendYCoor + marginPadding + labelPadding; final frame = SVGRectElement() ..setAttribute('class', 'downloads-chart-frame') ..setAttribute('height', '$frameHeight') From 5bf52e5385a73e1e4256fb2adc821b2344add58c Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Tue, 21 Jan 2025 10:39:17 +0000 Subject: [PATCH 5/6] comments --- pkg/_pub_shared/lib/format/date_format.dart | 2 +- .../widget/downloads_chart/computations.dart | 11 +++- .../src/widget/downloads_chart/widget.dart | 63 +++++++++---------- .../downloads_chart/downloads_chart_test.dart | 6 +- pkg/web_css/lib/src/_pkg.scss | 6 +- pkg/web_css/lib/src/_variables.scss | 4 ++ 6 files changed, 51 insertions(+), 41 deletions(-) diff --git a/pkg/_pub_shared/lib/format/date_format.dart b/pkg/_pub_shared/lib/format/date_format.dart index 1da402a90f..fbea96b59c 100644 --- a/pkg/_pub_shared/lib/format/date_format.dart +++ b/pkg/_pub_shared/lib/format/date_format.dart @@ -2,7 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -// Formats a DateTime into abbriviated month and day +// Formats a DateTime into abbreviated month and day String formatAbbrMonthDay(DateTime date) { final month = switch (date.month) { diff --git a/pkg/web_app/lib/src/widget/downloads_chart/computations.dart b/pkg/web_app/lib/src/widget/downloads_chart/computations.dart index 1d59151509..025dedf726 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/computations.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/computations.dart @@ -13,16 +13,22 @@ Iterable prepareRanges(List rangeDownloads) { /// /// The 'i'th entry in the iterable is a list of the download values /// (y coordinates) for the 'i'th week (x coordinate). -List> prepareWeekLists( +({List ranges, List> weekLists}) prepareWeekLists( List totals, List rangeDownloads, int displayLength, ) { final result = >[]; + final ranges = []; final showOther = totals[0] > rangeDownloads.fold(0, (sum, d) => sum + d.counts[0]); + if (showOther) { + ranges.add('Other'); + } + rangeDownloads.forEach((d) => ranges.add(d.versionRange)); + for (int week = 0; week < displayLength; week++) { final weekList = []; if (showOther) { @@ -32,5 +38,6 @@ List> prepareWeekLists( rangeDownloads.forEach((d) => weekList.add(d.counts[week])); result.add(weekList); } - return result.reversed.toList(); + + return (ranges: ranges, weekLists: result.reversed.toList()); } 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 f4750140be..fb9d23a3b5 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/widget.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/widget.dart @@ -25,10 +25,11 @@ String strokeColorClass(int i) => 'downloads-chart-stroke-${colors[i]}'; String fillColorClass(int i) => 'downloads-chart-fill-${colors[i]}'; void create(HTMLElement element, Map options) { - final dataPoints = options['points']; + var dataPoints = options['points']; if (dataPoints == null) { throw UnsupportedError('data-downloads-chart-points required'); } + dataPoints = testString; final svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('height', '100%'); @@ -46,21 +47,25 @@ void create(HTMLElement element, Map options) { data.majorRangeWeeklyDownloads, weeksToDisplay, ); - final majorRanges = - data.majorRangeWeeklyDownloads.map((e) => e.versionRange).toList(); - drawChart(svg, majorRanges, majorDisplayLists, data.newestDate); + drawChart(svg, majorDisplayLists, data.newestDate); } -void drawChart(Element svg, List ranges, List> values, +void drawChart( + Element svg, + ({List ranges, List> weekLists}) displayLists, DateTime newestDate, {bool stacked = false}) { + final ranges = displayLists.ranges; + final values = displayLists.weekLists; + if (values.isEmpty) return; + final frameWidth = 775; // TODO(zarah): Investigate if this width can be dynamic final topPadding = 30; final leftPadding = 30; - final rightPadding = 70; // make extra room for labels on y-axis + final rightPadding = 70; // Make extra room for labels on y-axis final chartWidth = frameWidth - leftPadding - rightPadding; final chartheight = 420; @@ -70,8 +75,8 @@ void drawChart(Element svg, List ranges, List> values, day: newestDate.day - 7 * (totalWeeks - weekNumber - 1)); } - // Computes max value on y-axis such that we get a nice division for the - // interval length between the numbers shown by the ticks on the y axis. + /// Computes max value on y-axis such that we get a nice division for the + /// interval length between the numbers shown by the ticks on the y axis. (int maxY, int interval) computeMaxYAndInterval(List> values) { final maxDownloads = values.fold(1, (a, b) => math.max(a, b.reduce(math.max))); @@ -88,7 +93,7 @@ void drawChart(Element svg, List ranges, List> values, return ((maxDownloads / d).ceil() * d, d); } } - // This should not happen! + // This should not happen! But we don't want to break if it does. return (maxDownloads, firstDiv); } @@ -129,7 +134,7 @@ void drawChart(Element svg, List ranges, List> values, chart.append(xaxis); late SVGTextElement firstTickLabel; - // place a tick every 4 weeks + // Place a tick every 4 weeks for (int week = 0; week < values.length; week += 4) { final date = computeDateForWeekNumber(newestDate, values.length, week); final (x, y) = computeCoordinates(date, 0); @@ -171,7 +176,7 @@ void drawChart(Element svg, List ranges, List> values, final longTick = SVGPathElement(); longTick.setAttribute('class', 'downloads-chart-frame'); - longTick.setAttribute('d', 'M$xAxisStart} $y L$xAxisEnd $y'); + longTick.setAttribute('d', 'M$xAxisStart $y L$xAxisEnd $y'); chart.append(longTick); } @@ -203,8 +208,8 @@ void drawChart(Element svg, List ranges, List> values, lines.add(line); } - double legendXCoor = xZero; - double legendYCoor = + double legendX = xZero; + double legendY = tickLabelYCoordinate + firstTickLabel.getBBox().height + labelPadding; final legendWidth = 20; final legendHeight = 8; @@ -228,39 +233,30 @@ void drawChart(Element svg, List ranges, List> values, final legendLabel = SVGTextElement(); chart.append(legendLabel); legendLabel.setAttribute('class', 'downloads-chart-tick-label'); - if (i == 5) { - // We have an 'other' line - legendLabel.text = 'Other'; - } else { - legendLabel.text = ranges[ranges.length - 1 - i]; - } + legendLabel.text = ranges[ranges.length - 1 - i]; - if (legendXCoor + - marginPadding + - legendWidth + - legendLabel.getBBox().width > + if (legendX + marginPadding + legendWidth + legendLabel.getBBox().width > xMax) { // There is no room for the legend and label. // Make a new line and update legendXCoor and legendYCoor accordingly. - legendXCoor = xZero; - legendYCoor += 2 * marginPadding + legendHeight; + legendX = xZero; + legendY += 2 * marginPadding + legendHeight; } - legend.setAttribute('x', '$legendXCoor'); - legend.setAttribute('y', '$legendYCoor'); - legendLabel.setAttribute('y', '${legendYCoor + legendHeight}'); - legendLabel.setAttribute( - 'x', '${legendXCoor + marginPadding + legendWidth}'); + legend.setAttribute('x', '$legendX'); + legend.setAttribute('y', '$legendY'); + legendLabel.setAttribute('y', '${legendY + legendHeight}'); + legendLabel.setAttribute('x', '${legendX + marginPadding + legendWidth}'); // Update x coordinate for next legend - legendXCoor += legendWidth + + legendX += legendWidth + marginPadding + legendLabel.getBBox().width + labelPadding; } - final frameHeight = legendYCoor + marginPadding + labelPadding; + final frameHeight = legendY + marginPadding + labelPadding; final frame = SVGRectElement() ..setAttribute('class', 'downloads-chart-frame') ..setAttribute('height', '$frameHeight') @@ -269,3 +265,6 @@ void drawChart(Element svg, List ranges, List> values, ..setAttribute('ry', '15'); chart.append(frame); } + +final testString = + 'eyJ0b3RhbFdlZWtseURvd25sb2FkcyI6WzY5MTE5Niw2MDM1NjgsMzUxNDM1LDM1MTYyNCw2MzY0MjUsNzAwNjY4LDY3NzYwOSw2NTcyOTQsNjcwNzU4LDY1NjM0Nyw2MzY5MDQsNjI2NTI3LDY0NjUzMCw2MzgzODMsNjMwNTc3LDU3MjkzOCw2MjUwODUsNTYzMzI0LDU4MDQ1OCw1NjY0MTMsNTc3MzA5LDU5NjY5Miw1NTU3MDcsNTcwNjA3LDU2NzExNSw1MzQ5NjksNTA2OTY4LDUxMDc2NCw0NTkxMjIsNTI5NDA1LDQ5MjEzNCw0OTQwNjksNDg0MjI5LDQ3NTk2OCw0ODc0NTksNTAxNzgwLDQ2MjU4Myw0MzM2NjgsNTE3MDAwLDU1NjgzOCw1MDM1NjMsNTIxMjgwLDQ4MjMzNyw1MzA4MzIsNTM5MDAyLDUyNjYwOSw1MTQzNzEsNDgwNTYxLDQzNzY1OCw0MzA0MDMsNDU0MzYxLDM1Nzk0OV0sIm1ham9yUmFuZ2VXZWVrbHlEb3dubG9hZHMiOlt7ImNvdW50cyI6WzE2MjEyMywxNDIxMTQsOTQyNTcsODk3NzQsMTM4NDgwLDE5NzExOCwyMTE5NTksMTg3MjUxLDE3NDA1MywxNzk4MzEsMTY1NDk0LDE2MTQ0MywxNTcxMTgsMTU3MTYzLDE3NjI0NiwxNTk0OTksMTcyODg0LDE1NTczMywxNjk3ODcsMTcyNzM3LDE3NDE1OSwxNzk5NDYsMTYxOTgwLDE3NDM5NCwxOTc4NTgsMTkxMTcyLDE4NTYwOSwxODQyNjcsMTcyMzQzLDIxOTM2MCwxOTYwMzYsMTkwNDAyLDE5NTc3MiwxOTMwNDEsMjA0NzIzLDIxNzY3OCwyMDYzMTEsMjAwMTEzLDIyODM5MSwyNTc5NDQsMjQzNjU1LDI3MzI4NSwyNTEzMzUsMjc4ODU1LDI4MDkwMywyNzAzNTcsMjc4NzY2LDI1OTI3NiwyNTE1NDUsMjQ1NzczLDI2MzE4MywxOTAwNzZdLCJ2ZXJzaW9uUmFuZ2UiOiI+PTQuMC4wLTAgPDUuMC4wIn0seyJjb3VudHMiOlsxNTYwNywxNTkxNyw5MDgzLDEwMTc1LDE3MjM2LDE2Nzg1LDE4OTQ1LDE5ODgxLDE5ODc5LDIxNTUxLDE5MTkyLDI4NjA5LDI1NzU5LDI2NTkwLDI3NjMzLDI4MjU1LDI5ODIwLDI4NDIyLDMwMDUzLDI5NDk0LDM0MTY3LDM1OTE1LDM1MzQ4LDM4NjE1LDQwMzI1LDM5MzczLDQxMDY5LDQzMzI0LDQyNDgwLDQ3NDA3LDUzNjczLDYxMTk1LDY0MDA4LDYzOTczLDY2NTYwLDczMTAwLDY3NDgyLDY3Nzk1LDkyMDc3LDEwMDU0NSwxMDY2MTMsMTIwNTI1LDEzMzE0OCwxNTcxMDUsMTYzMzIyLDE2NDI4NSwxNDg4OTEsMTQxNDQ5LDEwNzI3MCw5OTk5OSw5NzY2Niw3MDAwOF0sInZlcnNpb25SYW5nZSI6Ij49NS4wLjAtMCA8Ni4wLjAifSx7ImNvdW50cyI6WzM5OTg2LDQzNzE4LDE3Mzk0LDE1MTQxLDM4MTc2LDQxNTUyLDM3MTY1LDQ0MTU1LDUxMjc5LDU1OTg5LDUzMzcyLDUwNTY4LDUwMTA5LDUzNDUxLDQ4NzA3LDU0MjQ3LDYzNDg1LDU1NTk5LDU0NDA5LDU2MzMwLDU1NDU5LDU4NTAxLDUyMjExLDYwOTczLDUzNDA3LDU4ODkwLDUzMDA4LDYyNzUzLDQ2NDcxLDU3NjAzLDUwODM3LDU4NzE3LDU1MTA5LDUzMTYwLDUyMDkyLDUzNTQ2LDUyMzk2LDQ3MzU4LDYyNjQ5LDY0ODE2LDU4NjMwLDUxOTAzLDIxNTgwLDkyNTksMCwwLDAsMCwwLDAsMCwwXSwidmVyc2lvblJhbmdlIjoiPj02LjAuMC0wIDw3LjAuMCJ9LHsiY291bnRzIjpbODk2NywxMDMzNywzODY5LDQ5MjgsOTEzNywxMDIyNywxMjM4MCwxMjkxOSwxMjcwNywxMzU1OCwxNTEyNiwxNDY1MSwxNzM3MCwxNzM5NiwxNzI4OSwxODQwNSwyMzg3MSwxODc0NywxOTM4NywxNzI1NiwxOTI1OSwxOTMxNCwyMzUzMywyNDQ5NCwyMjEzOSwyNTUxMCwyNDQ3OSwyMjUwNSwyNzM1NiwyNjQyMiwyNjgzNywyODgxMywyNjk2MCwyODk0MCwzMjY2NCwzMTMwOCwzMjY0OCw0MTYxNSw1NzE2MSw0NzY1MiwyNjE0NiwwLDAsMCwwLDAsMCwwLDAsMCwwLDBdLCJ2ZXJzaW9uUmFuZ2UiOiI+PTcuMC4wLTAgPDguMC4wIn0seyJjb3VudHMiOls0MzkwNTEsMzY4MDgyLDIxMjU1MCwyMTg3NjcsNDEyNDE5LDQwNzgzNCwzNjk0NzEsMzcxODU5LDM5MDUyMCwzNjIxNDMsMzU2MDUzLDM0MDk2MiwzNjkwNjEsMzUxNjMzLDMyNzAwMywyNzg5MjksMzAzMDIwLDI3NTc1NywyNzMzMTUsMjU2OTA0LDI1MTkyNCwyNTc2NDUsMjQzODQyLDIyNzYxNywyMTE5MjcsMTc2NDk5LDE1OTc5NiwxNTI3MjksMTMxNjA4LDEzMzk5NSwxMjE4ODEsMTEyMzg2LDEwMDA5Niw5MDg2MSw4MTQyMyw3NTYzNyw1MjA5NCwzMDg1NiwxNTI1MiwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwXSwidmVyc2lvblJhbmdlIjoiPj04LjAuMC0wIDw5LjAuMCJ9XSwibWlub3JSYW5nZVdlZWtseURvd25sb2FkcyI6W3siY291bnRzIjpbMTU2MDcsMTU5MTcsOTA4MywxMDE3NSwxNzIzNiwxNjc4NSwxODk0NSwxOTg4MSwxOTg3OSwyMTU1MSwxOTE5MiwyODYwOSwyNTc1OSwyNjU5MCwyNzYzMywyODI1NSwyOTgyMCwyODQyMiwzMDA1MywyOTQ5NCwzNDE2NywzNTkxNSwzNTM0OCwzODYxNSw0MDMyNSwzOTM3Myw0MTA2OSw0MzMyNCw0MjQ4MCw0NzQwNyw1MzY3Myw2MTE5NSw2NDAwOCw2Mzk3Myw2NjU2MCw3MzEwMCw2NzQ4Miw2Nzc5NSw5MjA3NywxMDA1NDUsMTA2NjEzLDEyMDUyNSwxMzMxNDgsMTU3MTA1LDE2MzMyMiwxNjQyODUsMTQ4ODkxLDE0MTQ0OSwxMDcyNzAsOTk5OTksOTc2NjYsNzAwMDhdLCJ2ZXJzaW9uUmFuZ2UiOiI+PTUuMC4wLTAgPDUuMS4wIn0seyJjb3VudHMiOlszOTk4Niw0MzcxOCwxNzM5NCwxNTE0MSwzODE3Niw0MTU1MiwzNzE2NSw0NDE1NSw1MTI3OSw1NTk4OSw1MzM3Miw1MDU2OCw1MDEwOSw1MzQ1MSw0ODcwNyw1NDI0Nyw2MzQ4NSw1NTU5OSw1NDQwOSw1NjMzMCw1NTQ1OSw1ODUwMSw1MjIxMSw2MDk3Myw1MzQwNyw1ODg5MCw1MzAwOCw2Mjc1Myw0NjQ3MSw1NzYwMyw1MDgzNyw1ODcxNyw1NTEwOSw1MzE2MCw1MjA5Miw1MzU0Niw1MjM5Niw0NzM1OCw2MjY0OSw2NDgxNiw1ODYzMCw1MTkwMywyMTU4MCw5MjU5LDAsMCwwLDAsMCwwLDAsMF0sInZlcnNpb25SYW5nZSI6Ij49Ni4wLjAtMCA8Ni4xLjAifSx7ImNvdW50cyI6Wzg5NjcsMTAzMzcsMzg2OSw0OTI4LDkxMzcsMTAyMjcsMTIzODAsMTI5MTksMTI3MDcsMTM1NTgsMTUxMjYsMTQ2NTEsMTczNzAsMTczOTYsMTcyODksMTg0MDUsMjM4NzEsMTg3NDcsMTkzODcsMTcyNTYsMTkyNTksMTkzMTQsMjM1MzMsMjQ0OTQsMjIxMzksMjU1MTAsMjQ0NzksMjI1MDUsMjczNTYsMjY0MjIsMjY4MzcsMjg4MTMsMjY5NjAsMjg5NDAsMzI2NjQsMzEzMDgsMzI2NDgsNDE2MTUsNTcxNjEsNDc2NTIsMjYxNDYsMCwwLDAsMCwwLDAsMCwwLDAsMCwwXSwidmVyc2lvblJhbmdlIjoiPj03LjAuMC0wIDw3LjEuMCJ9LHsiY291bnRzIjpbMTA1NjI5LDk4Nzc2LDYwMzYxLDY0MjI3LDEyNTIyOCwxMzc5MDgsMTQ0NDQ3LDE1NTQzNCwxNzMzMDMsMTU3NTAxLDE2NTI2MywxNzUzMjMsMjA5NjM5LDI5ODg5MCwzMjcwMDMsMjc4OTI5LDMwMzAyMCwyNzU3NTcsMjczMzE1LDI1NjkwNCwyNTE5MjQsMjU3NjQ1LDI0Mzg0MiwyMjc2MTcsMjExOTI3LDE3NjQ5OSwxNTk3OTYsMTUyNzI5LDEzMTYwOCwxMzM5OTUsMTIxODgxLDExMjM4NiwxMDAwOTYsOTA4NjEsODE0MjMsNzU2MzcsNTIwOTQsMzA4NTYsMTUyNTIsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMF0sInZlcnNpb25SYW5nZSI6Ij49OC4wLjAtMCA8OC4xLjAifSx7ImNvdW50cyI6WzMzMzQyMiwyNjkzMDYsMTUyMTg5LDE1NDU0MCwyODcxOTEsMjY5OTI2LDIyNTAyNCwyMTY0MjUsMjE3MjE3LDIwNDY0MiwxOTA3OTAsMTY1NjM5LDE1OTQyMiw1Mjc0MywwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDBdLCJ2ZXJzaW9uUmFuZ2UiOiI+PTguMS4wLTAgPDguMi4wIn1dLCJwYXRjaFJhbmdlV2Vla2x5RG93bmxvYWRzIjpbeyJjb3VudHMiOls4MzkwLDgzOTEsNDkyMyw1NjkwLDEwNzQyLDEzNjgxLDEzMjE1LDE1NzY5LDE2OTkxLDE3NzM3LDE4NjEyLDIzNDk3LDQwNDE1LDExODI3NCw4OTkzNywwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwXSwidmVyc2lvblJhbmdlIjoiPj04LjAuMy0wIDw4LjAuNCJ9LHsiY291bnRzIjpbMjc5OTcsMzA1NzYsMTc2NzEsMjA2MzYsMzY2OTYsNDQ2MjQsNDYxMjksNDk4MDEsNTUyNTYsNjE3NzIsMTM0NjY3LDE2NTYzOSwxNTk0MjIsNTI3NDMsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwXSwidmVyc2lvblJhbmdlIjoiPj04LjEuMC0wIDw4LjEuMSJ9LHsiY291bnRzIjpbNTUwNzksNTY1MTAsMzE1MjcsMzYzOTEsNzgzNjAsMTY3NDgwLDE3ODg5NSwxNjY2MjQsMTYxOTYxLDE0Mjg3MCw1NjEyMywwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDBdLCJ2ZXJzaW9uUmFuZ2UiOiI+PTguMS4xLTAgPDguMS4yIn0seyJjb3VudHMiOlsxMDUxMjYsMTgyMjIwLDEwMjk5MSw5NzUxMywxNzIxMzUsNTc4MjIsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMF0sInZlcnNpb25SYW5nZSI6Ij49OC4xLjItMCA8OC4xLjMifSx7ImNvdW50cyI6WzE0NTIyMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMF0sInZlcnNpb25SYW5nZSI6Ij49OC4xLjMtMCA8OC4xLjQifV0sIm5ld2VzdERhdGUiOiIyMDI1LTAxLTE5VDAwOjAwOjAwLjAwMCJ9'; diff --git a/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart b/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart index c94aff92ca..6ffd9a6466 100644 --- a/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart +++ b/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart @@ -52,9 +52,9 @@ void main() { (counts: l3, versionRange: '>=6.0.0-0 <7.0.0') ]; - final w1 = prepareWeekLists(totals, majorRangeDownloads, 52).toList(); - final w2 = prepareWeekLists(totals, minorRangeDownloads, 52).toList(); - final w3 = prepareWeekLists(totals, patchRangeDownloads, 52).toList(); + final w1 = prepareWeekLists(totals, majorRangeDownloads, 52).weekLists; + final w2 = prepareWeekLists(totals, minorRangeDownloads, 52).weekLists; + final w3 = prepareWeekLists(totals, patchRangeDownloads, 52).weekLists; for (int i = 42; i < 52; i++) { expect(w1[i], [10, 10, 10, 10, 70]); diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index 0d053f8dae..3e43828e07 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -290,9 +290,9 @@ } .downloads-chart-frame { - fill:transparent; + fill:none; stroke-width: 1; - stroke: lightgrey; + stroke: var(--pub-downloads-chart-frame-color); } .downloads-chart-x-axis { @@ -345,7 +345,7 @@ .downloads-chart-line { fill: none; - stroke-width: 3; + stroke-width: 2; stroke-linejoin: round; } diff --git a/pkg/web_css/lib/src/_variables.scss b/pkg/web_css/lib/src/_variables.scss index 47e01f5d38..8193b81d24 100644 --- a/pkg/web_css/lib/src/_variables.scss +++ b/pkg/web_css/lib/src/_variables.scss @@ -134,6 +134,8 @@ --pub-tag_simplebadge-text-color: #444444; --pub-tag_sdkbadge-separator-color: rgba(25, 103, 210, 0.5); // #1967d2 + 0.5 opacity; --pub-tag_sdkbadge-text-color: #1967d2; + + --pub-downloads-chart-frame-color: #d3d3d3; } .light-theme { @@ -190,6 +192,8 @@ --pub-tag_sdkbadge-separator-color: var(--pub-neutral-textColor); --pub-tag_sdkbadge-text-color: var(--pub-neutral-textColor); + --pub-downloads-chart-frame-color: #55585a; + // Material Design theme customizations --mdc-theme-surface: var(--pub-neutral-bgColor); --mdc-theme-on-primary: var(--pub-neutral-textColor); From a44f8dc9a919eca58baa6cb08929a45fddf16d7e Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Tue, 21 Jan 2025 11:14:52 +0000 Subject: [PATCH 6/6] remove test string --- pkg/web_app/lib/src/widget/downloads_chart/widget.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 fb9d23a3b5..ea7223bf29 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/widget.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/widget.dart @@ -25,11 +25,10 @@ String strokeColorClass(int i) => 'downloads-chart-stroke-${colors[i]}'; String fillColorClass(int i) => 'downloads-chart-fill-${colors[i]}'; void create(HTMLElement element, Map options) { - var dataPoints = options['points']; + final dataPoints = options['points']; if (dataPoints == null) { throw UnsupportedError('data-downloads-chart-points required'); } - dataPoints = testString; final svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('height', '100%'); @@ -265,6 +264,3 @@ void drawChart( ..setAttribute('ry', '15'); chart.append(frame); } - -final testString = - 'eyJ0b3RhbFdlZWtseURvd25sb2FkcyI6WzY5MTE5Niw2MDM1NjgsMzUxNDM1LDM1MTYyNCw2MzY0MjUsNzAwNjY4LDY3NzYwOSw2NTcyOTQsNjcwNzU4LDY1NjM0Nyw2MzY5MDQsNjI2NTI3LDY0NjUzMCw2MzgzODMsNjMwNTc3LDU3MjkzOCw2MjUwODUsNTYzMzI0LDU4MDQ1OCw1NjY0MTMsNTc3MzA5LDU5NjY5Miw1NTU3MDcsNTcwNjA3LDU2NzExNSw1MzQ5NjksNTA2OTY4LDUxMDc2NCw0NTkxMjIsNTI5NDA1LDQ5MjEzNCw0OTQwNjksNDg0MjI5LDQ3NTk2OCw0ODc0NTksNTAxNzgwLDQ2MjU4Myw0MzM2NjgsNTE3MDAwLDU1NjgzOCw1MDM1NjMsNTIxMjgwLDQ4MjMzNyw1MzA4MzIsNTM5MDAyLDUyNjYwOSw1MTQzNzEsNDgwNTYxLDQzNzY1OCw0MzA0MDMsNDU0MzYxLDM1Nzk0OV0sIm1ham9yUmFuZ2VXZWVrbHlEb3dubG9hZHMiOlt7ImNvdW50cyI6WzE2MjEyMywxNDIxMTQsOTQyNTcsODk3NzQsMTM4NDgwLDE5NzExOCwyMTE5NTksMTg3MjUxLDE3NDA1MywxNzk4MzEsMTY1NDk0LDE2MTQ0MywxNTcxMTgsMTU3MTYzLDE3NjI0NiwxNTk0OTksMTcyODg0LDE1NTczMywxNjk3ODcsMTcyNzM3LDE3NDE1OSwxNzk5NDYsMTYxOTgwLDE3NDM5NCwxOTc4NTgsMTkxMTcyLDE4NTYwOSwxODQyNjcsMTcyMzQzLDIxOTM2MCwxOTYwMzYsMTkwNDAyLDE5NTc3MiwxOTMwNDEsMjA0NzIzLDIxNzY3OCwyMDYzMTEsMjAwMTEzLDIyODM5MSwyNTc5NDQsMjQzNjU1LDI3MzI4NSwyNTEzMzUsMjc4ODU1LDI4MDkwMywyNzAzNTcsMjc4NzY2LDI1OTI3NiwyNTE1NDUsMjQ1NzczLDI2MzE4MywxOTAwNzZdLCJ2ZXJzaW9uUmFuZ2UiOiI+PTQuMC4wLTAgPDUuMC4wIn0seyJjb3VudHMiOlsxNTYwNywxNTkxNyw5MDgzLDEwMTc1LDE3MjM2LDE2Nzg1LDE4OTQ1LDE5ODgxLDE5ODc5LDIxNTUxLDE5MTkyLDI4NjA5LDI1NzU5LDI2NTkwLDI3NjMzLDI4MjU1LDI5ODIwLDI4NDIyLDMwMDUzLDI5NDk0LDM0MTY3LDM1OTE1LDM1MzQ4LDM4NjE1LDQwMzI1LDM5MzczLDQxMDY5LDQzMzI0LDQyNDgwLDQ3NDA3LDUzNjczLDYxMTk1LDY0MDA4LDYzOTczLDY2NTYwLDczMTAwLDY3NDgyLDY3Nzk1LDkyMDc3LDEwMDU0NSwxMDY2MTMsMTIwNTI1LDEzMzE0OCwxNTcxMDUsMTYzMzIyLDE2NDI4NSwxNDg4OTEsMTQxNDQ5LDEwNzI3MCw5OTk5OSw5NzY2Niw3MDAwOF0sInZlcnNpb25SYW5nZSI6Ij49NS4wLjAtMCA8Ni4wLjAifSx7ImNvdW50cyI6WzM5OTg2LDQzNzE4LDE3Mzk0LDE1MTQxLDM4MTc2LDQxNTUyLDM3MTY1LDQ0MTU1LDUxMjc5LDU1OTg5LDUzMzcyLDUwNTY4LDUwMTA5LDUzNDUxLDQ4NzA3LDU0MjQ3LDYzNDg1LDU1NTk5LDU0NDA5LDU2MzMwLDU1NDU5LDU4NTAxLDUyMjExLDYwOTczLDUzNDA3LDU4ODkwLDUzMDA4LDYyNzUzLDQ2NDcxLDU3NjAzLDUwODM3LDU4NzE3LDU1MTA5LDUzMTYwLDUyMDkyLDUzNTQ2LDUyMzk2LDQ3MzU4LDYyNjQ5LDY0ODE2LDU4NjMwLDUxOTAzLDIxNTgwLDkyNTksMCwwLDAsMCwwLDAsMCwwXSwidmVyc2lvblJhbmdlIjoiPj02LjAuMC0wIDw3LjAuMCJ9LHsiY291bnRzIjpbODk2NywxMDMzNywzODY5LDQ5MjgsOTEzNywxMDIyNywxMjM4MCwxMjkxOSwxMjcwNywxMzU1OCwxNTEyNiwxNDY1MSwxNzM3MCwxNzM5NiwxNzI4OSwxODQwNSwyMzg3MSwxODc0NywxOTM4NywxNzI1NiwxOTI1OSwxOTMxNCwyMzUzMywyNDQ5NCwyMjEzOSwyNTUxMCwyNDQ3OSwyMjUwNSwyNzM1NiwyNjQyMiwyNjgzNywyODgxMywyNjk2MCwyODk0MCwzMjY2NCwzMTMwOCwzMjY0OCw0MTYxNSw1NzE2MSw0NzY1MiwyNjE0NiwwLDAsMCwwLDAsMCwwLDAsMCwwLDBdLCJ2ZXJzaW9uUmFuZ2UiOiI+PTcuMC4wLTAgPDguMC4wIn0seyJjb3VudHMiOls0MzkwNTEsMzY4MDgyLDIxMjU1MCwyMTg3NjcsNDEyNDE5LDQwNzgzNCwzNjk0NzEsMzcxODU5LDM5MDUyMCwzNjIxNDMsMzU2MDUzLDM0MDk2MiwzNjkwNjEsMzUxNjMzLDMyNzAwMywyNzg5MjksMzAzMDIwLDI3NTc1NywyNzMzMTUsMjU2OTA0LDI1MTkyNCwyNTc2NDUsMjQzODQyLDIyNzYxNywyMTE5MjcsMTc2NDk5LDE1OTc5NiwxNTI3MjksMTMxNjA4LDEzMzk5NSwxMjE4ODEsMTEyMzg2LDEwMDA5Niw5MDg2MSw4MTQyMyw3NTYzNyw1MjA5NCwzMDg1NiwxNTI1MiwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwXSwidmVyc2lvblJhbmdlIjoiPj04LjAuMC0wIDw5LjAuMCJ9XSwibWlub3JSYW5nZVdlZWtseURvd25sb2FkcyI6W3siY291bnRzIjpbMTU2MDcsMTU5MTcsOTA4MywxMDE3NSwxNzIzNiwxNjc4NSwxODk0NSwxOTg4MSwxOTg3OSwyMTU1MSwxOTE5MiwyODYwOSwyNTc1OSwyNjU5MCwyNzYzMywyODI1NSwyOTgyMCwyODQyMiwzMDA1MywyOTQ5NCwzNDE2NywzNTkxNSwzNTM0OCwzODYxNSw0MDMyNSwzOTM3Myw0MTA2OSw0MzMyNCw0MjQ4MCw0NzQwNyw1MzY3Myw2MTE5NSw2NDAwOCw2Mzk3Myw2NjU2MCw3MzEwMCw2NzQ4Miw2Nzc5NSw5MjA3NywxMDA1NDUsMTA2NjEzLDEyMDUyNSwxMzMxNDgsMTU3MTA1LDE2MzMyMiwxNjQyODUsMTQ4ODkxLDE0MTQ0OSwxMDcyNzAsOTk5OTksOTc2NjYsNzAwMDhdLCJ2ZXJzaW9uUmFuZ2UiOiI+PTUuMC4wLTAgPDUuMS4wIn0seyJjb3VudHMiOlszOTk4Niw0MzcxOCwxNzM5NCwxNTE0MSwzODE3Niw0MTU1MiwzNzE2NSw0NDE1NSw1MTI3OSw1NTk4OSw1MzM3Miw1MDU2OCw1MDEwOSw1MzQ1MSw0ODcwNyw1NDI0Nyw2MzQ4NSw1NTU5OSw1NDQwOSw1NjMzMCw1NTQ1OSw1ODUwMSw1MjIxMSw2MDk3Myw1MzQwNyw1ODg5MCw1MzAwOCw2Mjc1Myw0NjQ3MSw1NzYwMyw1MDgzNyw1ODcxNyw1NTEwOSw1MzE2MCw1MjA5Miw1MzU0Niw1MjM5Niw0NzM1OCw2MjY0OSw2NDgxNiw1ODYzMCw1MTkwMywyMTU4MCw5MjU5LDAsMCwwLDAsMCwwLDAsMF0sInZlcnNpb25SYW5nZSI6Ij49Ni4wLjAtMCA8Ni4xLjAifSx7ImNvdW50cyI6Wzg5NjcsMTAzMzcsMzg2OSw0OTI4LDkxMzcsMTAyMjcsMTIzODAsMTI5MTksMTI3MDcsMTM1NTgsMTUxMjYsMTQ2NTEsMTczNzAsMTczOTYsMTcyODksMTg0MDUsMjM4NzEsMTg3NDcsMTkzODcsMTcyNTYsMTkyNTksMTkzMTQsMjM1MzMsMjQ0OTQsMjIxMzksMjU1MTAsMjQ0NzksMjI1MDUsMjczNTYsMjY0MjIsMjY4MzcsMjg4MTMsMjY5NjAsMjg5NDAsMzI2NjQsMzEzMDgsMzI2NDgsNDE2MTUsNTcxNjEsNDc2NTIsMjYxNDYsMCwwLDAsMCwwLDAsMCwwLDAsMCwwXSwidmVyc2lvblJhbmdlIjoiPj03LjAuMC0wIDw3LjEuMCJ9LHsiY291bnRzIjpbMTA1NjI5LDk4Nzc2LDYwMzYxLDY0MjI3LDEyNTIyOCwxMzc5MDgsMTQ0NDQ3LDE1NTQzNCwxNzMzMDMsMTU3NTAxLDE2NTI2MywxNzUzMjMsMjA5NjM5LDI5ODg5MCwzMjcwMDMsMjc4OTI5LDMwMzAyMCwyNzU3NTcsMjczMzE1LDI1NjkwNCwyNTE5MjQsMjU3NjQ1LDI0Mzg0MiwyMjc2MTcsMjExOTI3LDE3NjQ5OSwxNTk3OTYsMTUyNzI5LDEzMTYwOCwxMzM5OTUsMTIxODgxLDExMjM4NiwxMDAwOTYsOTA4NjEsODE0MjMsNzU2MzcsNTIwOTQsMzA4NTYsMTUyNTIsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMF0sInZlcnNpb25SYW5nZSI6Ij49OC4wLjAtMCA8OC4xLjAifSx7ImNvdW50cyI6WzMzMzQyMiwyNjkzMDYsMTUyMTg5LDE1NDU0MCwyODcxOTEsMjY5OTI2LDIyNTAyNCwyMTY0MjUsMjE3MjE3LDIwNDY0MiwxOTA3OTAsMTY1NjM5LDE1OTQyMiw1Mjc0MywwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDBdLCJ2ZXJzaW9uUmFuZ2UiOiI+PTguMS4wLTAgPDguMi4wIn1dLCJwYXRjaFJhbmdlV2Vla2x5RG93bmxvYWRzIjpbeyJjb3VudHMiOls4MzkwLDgzOTEsNDkyMyw1NjkwLDEwNzQyLDEzNjgxLDEzMjE1LDE1NzY5LDE2OTkxLDE3NzM3LDE4NjEyLDIzNDk3LDQwNDE1LDExODI3NCw4OTkzNywwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwXSwidmVyc2lvblJhbmdlIjoiPj04LjAuMy0wIDw4LjAuNCJ9LHsiY291bnRzIjpbMjc5OTcsMzA1NzYsMTc2NzEsMjA2MzYsMzY2OTYsNDQ2MjQsNDYxMjksNDk4MDEsNTUyNTYsNjE3NzIsMTM0NjY3LDE2NTYzOSwxNTk0MjIsNTI3NDMsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwXSwidmVyc2lvblJhbmdlIjoiPj04LjEuMC0wIDw4LjEuMSJ9LHsiY291bnRzIjpbNTUwNzksNTY1MTAsMzE1MjcsMzYzOTEsNzgzNjAsMTY3NDgwLDE3ODg5NSwxNjY2MjQsMTYxOTYxLDE0Mjg3MCw1NjEyMywwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDBdLCJ2ZXJzaW9uUmFuZ2UiOiI+PTguMS4xLTAgPDguMS4yIn0seyJjb3VudHMiOlsxMDUxMjYsMTgyMjIwLDEwMjk5MSw5NzUxMywxNzIxMzUsNTc4MjIsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMF0sInZlcnNpb25SYW5nZSI6Ij49OC4xLjItMCA8OC4xLjMifSx7ImNvdW50cyI6WzE0NTIyMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMF0sInZlcnNpb25SYW5nZSI6Ij49OC4xLjMtMCA8OC4xLjQifV0sIm5ld2VzdERhdGUiOiIyMDI1LTAxLTE5VDAwOjAwOjAwLjAwMCJ9';