From e6c85db70c33525e97c47bea5d9facc8840c96a0 Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Fri, 7 Feb 2025 10:49:07 +0000 Subject: [PATCH 1/3] Downloads chart: Add display modes stacked/unstacked --- .../templates/views/pkg/score_tab.dart | 25 ++++++ .../src/widget/downloads_chart/widget.dart | 84 +++++++++++++++++-- pkg/web_css/lib/src/_pkg.scss | 8 ++ 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/app/lib/frontend/templates/views/pkg/score_tab.dart b/app/lib/frontend/templates/views/pkg/score_tab.dart index d7bbd160bb..8557f1053d 100644 --- a/app/lib/frontend/templates/views/pkg/score_tab.dart +++ b/app/lib/frontend/templates/views/pkg/score_tab.dart @@ -195,6 +195,29 @@ d.Node _downloadsChart(WeeklyVersionDownloadCounts weeklyVersionDownloads) { initialValue: 'major') ], ); + + final displayModes = d.div( + classes: ['downloads-chart-display-modes'], + children: [ + radioButtons( + leadingText: 'Display as: ', + name: 'display-modes', + radios: [ + ( + id: 'display-modes-unstacked', + value: 'unstacked', + label: 'Unstacked' + ), + ( + id: 'version-modes-stacked', + value: 'stacked', + label: 'Stacked', + ), + ], + classes: ['downloads-chart-radio-button'], + initialValue: 'unstacked') + ], + ); final container = d.div( classes: ['downloads-chart'], id: '-downloads-chart', @@ -203,11 +226,13 @@ d.Node _downloadsChart(WeeklyVersionDownloadCounts weeklyVersionDownloads) { 'data-downloads-chart-points': base64Encode(jsonUtf8Encoder.convert(weeklyVersionDownloads)), 'data-downloads-chart-versions-radio': 'version-modes', + 'data-downloads-chart-display-radio': 'display-modes', }, ); return d.fragment([ d.h1(text: 'Weekly downloads'), + displayModes, versionModes, container, ]); 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 c6f82e1cc4..93cb50a89e 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/widget.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/widget.dart @@ -26,6 +26,11 @@ 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]}'; +enum DisplayMode { + stacked, + unstacked, +} + void create(HTMLElement element, Map options) { final dataPoints = options['points']; if (dataPoints == null) { @@ -36,6 +41,12 @@ void create(HTMLElement element, Map options) { if (versionsRadio == null) { throw UnsupportedError('data-downloads-chart-versions-radio required'); } + + final displayRadio = options['display-radio']; + if (displayRadio == null) { + throw UnsupportedError('data-downloads-chart-display-radio required'); + } + Element createNewSvg() { return document.createElementNS('http://www.w3.org/2000/svg', 'svg') ..setAttribute('height', '100%') @@ -72,6 +83,9 @@ void create(HTMLElement element, Map options) { weeksToDisplay, ); + var currentDisplayList = majorDisplayLists; + var currentDisplayMode = DisplayMode.unstacked; + final versionModesLists = { 'major': majorDisplayLists, 'minor': minorDisplayLists, @@ -91,7 +105,34 @@ void create(HTMLElement element, Map options) { element.removeChild(svg); svg = createNewSvg(); element.append(svg); - drawChart(svg, toolTip, displayList, data.newestDate); + currentDisplayList = displayList; + drawChart(svg, toolTip, displayList, data.newestDate, + displayMode: currentDisplayMode); + }); + }); + + final displayModesMap = { + 'stacked': DisplayMode.stacked, + 'unstacked': DisplayMode.unstacked + }; + + final displayModes = document.getElementsByName(displayRadio).toList(); + displayModes.forEach((i) { + final radioButton = i as HTMLInputElement; + final value = radioButton.value; + final displayMode = displayModesMap[value]; + + if (displayMode == null) { + throw UnsupportedError('Unsupported display-radio value: "$value"'); + } + + radioButton.onClick.listen((e) { + element.removeChild(svg); + svg = createNewSvg(); + element.append(svg); + currentDisplayMode = displayMode; + drawChart(svg, toolTip, currentDisplayList, data.newestDate, + displayMode: displayMode); }); }); @@ -103,7 +144,7 @@ void drawChart( HTMLDivElement toolTip, ({List ranges, List> weekLists}) displayLists, DateTime newestDate, - {bool stacked = false}) { + {DisplayMode displayMode = DisplayMode.unstacked}) { final ranges = displayLists.ranges; final values = displayLists.weekLists; @@ -128,8 +169,11 @@ void drawChart( /// 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))); + final maxDownloads = displayMode == DisplayMode.unstacked + ? values.fold(1, (a, b) => math.max(a, b.reduce(math.max))) + : values.fold( + 1, (a, b) => math.max(a, b.reduce((x, y) => x + y))); + final digits = maxDownloads.toString().length; final buffer = StringBuffer()..write('1'); if (digits > 2) { @@ -244,14 +288,20 @@ void drawChart( // Chart lines and legends + final lastestDownloads = List.filled(values.length, 0); 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++) { + if (displayMode == DisplayMode.stacked) { + lastestDownloads[week] += values[week][versionRange]; + } else { + lastestDownloads[week] = values[week][versionRange]; + } final (x, y) = computeCoordinates( computeDateForWeekNumber(newestDate, values.length, week), - values[week][versionRange]); + lastestDownloads[week]); line.write(' $c$x $y'); c = 'L'; } @@ -265,14 +315,32 @@ void drawChart( final legendHeight = 8; for (int i = 0; i < lines.length; i++) { - final path = SVGPathElement(); - 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 - i]}'); + final line = lines[lines.length - 1 - i]; + + final path = SVGPathElement(); + path.setAttribute('class', '${strokeColorClass(i)} downloads-chart-line '); + path.setAttribute('d', '$line'); path.setAttribute('clip-path', 'url(#clipRect)'); chart.append(path); + if (displayMode == DisplayMode.stacked) { + final area = SVGPathElement(); + area.setAttribute('class', '${fillColorClass(i)} downloads-chart-area '); + final prevLine = i == lines.length - 1 + ? ' M $xZero $yZero L$xMax $yZero' + : lines[lines.length - 1 - i - 1]; + final reversed = prevLine + .toString() + .replaceAll(' M', '') + .split('L') + .reversed + .join('L'); + area.setAttribute('d', '$line L$reversed Z'); + chart.append(area); + } + final legend = SVGRectElement(); chart.append(legend); legend.setAttribute('class', diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index d304010cd7..d8d03192b0 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -289,6 +289,10 @@ float: right; } + .downloads-chart-display-modes { + float: left; + } + .downloads-chart-radio-button { margin-left: 10px; } @@ -426,6 +430,10 @@ stroke-linejoin: round; } + .downloads-chart-area { + opacity: 0.3; + } + .downloads-chart-stroke-blue { stroke: var(--pub-downloads-chart-color-0); } From dedfe4a01548edcb5921bb7ed3b79f9bdef3dd16 Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Fri, 7 Feb 2025 12:57:40 +0000 Subject: [PATCH 2/3] store lines as coordinates --- .../src/widget/downloads_chart/widget.dart | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 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 93cb50a89e..07a65249f2 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/widget.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/widget.dart @@ -289,10 +289,9 @@ void drawChart( // Chart lines and legends final lastestDownloads = List.filled(values.length, 0); - final lines = []; + final lines = >[]; for (int versionRange = 0; versionRange < values[0].length; versionRange++) { - final line = StringBuffer(); - var c = 'M'; + final List<(double, double)> lineCoordinates = <(double, double)>[]; for (int week = 0; week < values.length; week++) { if (displayMode == DisplayMode.stacked) { lastestDownloads[week] += values[week][versionRange]; @@ -302,10 +301,36 @@ void drawChart( final (x, y) = computeCoordinates( computeDateForWeekNumber(newestDate, values.length, week), lastestDownloads[week]); - line.write(' $c$x $y'); - c = 'L'; + lineCoordinates.add((x, y)); } - lines.add(line); + lines.add(lineCoordinates); + } + + StringBuffer computeLine(List<(double, double)> coordinates) { + final path = StringBuffer(); + var command = 'M'; + coordinates.forEach((c) { + path.write(' $command${c.$1} ${c.$2}'); + command = 'L'; + }); + return path; + } + + StringBuffer computeArea(List<(double, double)> topCoordinates, + List<(double, double)> bottomCoordinates) { + final path = StringBuffer(); + var command = 'M'; + topCoordinates.forEach((c) { + path.write(' $command${c.$1} ${c.$2}'); + command = 'L'; + }); + + bottomCoordinates.reversed.forEach((c) { + path.write(' $command${c.$1} ${c.$2}'); + command = 'L'; + }); + path.write('Z'); + return path; } double legendX = xZero; @@ -315,10 +340,9 @@ void drawChart( final legendHeight = 8; for (int i = 0; i < lines.length; i++) { - // We assign colors in reverse order so that main colors are chosen first for - // the newest versions. - final line = lines[lines.length - 1 - i]; - + // We add the lines in reverse order so that the newest versions get the + // main color. + final line = computeLine(lines[lines.length - 1 - i]); final path = SVGPathElement(); path.setAttribute('class', '${strokeColorClass(i)} downloads-chart-line '); path.setAttribute('d', '$line'); @@ -326,18 +350,14 @@ void drawChart( chart.append(path); if (displayMode == DisplayMode.stacked) { - final area = SVGPathElement(); - area.setAttribute('class', '${fillColorClass(i)} downloads-chart-area '); final prevLine = i == lines.length - 1 - ? ' M $xZero $yZero L$xMax $yZero' + ? [(xZero, yZero), (xMax, yZero)] : lines[lines.length - 1 - i - 1]; - final reversed = prevLine - .toString() - .replaceAll(' M', '') - .split('L') - .reversed - .join('L'); - area.setAttribute('d', '$line L$reversed Z'); + final areaPath = computeArea(lines[lines.length - 1 - i], prevLine); + final area = SVGPathElement(); + area.setAttribute('class', '${fillColorClass(i)} downloads-chart-area '); + area.setAttribute('d', '$areaPath'); + area.setAttribute('clip-path', 'url(#clipRect)'); chart.append(area); } From 34688215bd591e0d0a50ea04c8f065a104c4fd35 Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Fri, 7 Feb 2025 13:01:01 +0000 Subject: [PATCH 3/3] renaming --- pkg/web_app/lib/src/widget/downloads_chart/widget.dart | 10 +++++----- 1 file changed, 5 insertions(+), 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 07a65249f2..5481ddac36 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/widget.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/widget.dart @@ -306,7 +306,7 @@ void drawChart( lines.add(lineCoordinates); } - StringBuffer computeLine(List<(double, double)> coordinates) { + StringBuffer computeLinePath(List<(double, double)> coordinates) { final path = StringBuffer(); var command = 'M'; coordinates.forEach((c) { @@ -316,7 +316,7 @@ void drawChart( return path; } - StringBuffer computeArea(List<(double, double)> topCoordinates, + StringBuffer computeAreaPath(List<(double, double)> topCoordinates, List<(double, double)> bottomCoordinates) { final path = StringBuffer(); var command = 'M'; @@ -341,8 +341,8 @@ void drawChart( for (int i = 0; i < lines.length; i++) { // We add the lines in reverse order so that the newest versions get the - // main color. - final line = computeLine(lines[lines.length - 1 - i]); + // main colors. + final line = computeLinePath(lines[lines.length - 1 - i]); final path = SVGPathElement(); path.setAttribute('class', '${strokeColorClass(i)} downloads-chart-line '); path.setAttribute('d', '$line'); @@ -353,7 +353,7 @@ void drawChart( final prevLine = i == lines.length - 1 ? [(xZero, yZero), (xMax, yZero)] : lines[lines.length - 1 - i - 1]; - final areaPath = computeArea(lines[lines.length - 1 - i], prevLine); + final areaPath = computeAreaPath(lines[lines.length - 1 - i], prevLine); final area = SVGPathElement(); area.setAttribute('class', '${fillColorClass(i)} downloads-chart-area '); area.setAttribute('d', '$areaPath');