Skip to content

Commit 07d2c88

Browse files
committed
Downloads chart: Add option to display as percentage
1 parent e21442b commit 07d2c88

File tree

2 files changed

+69
-23
lines changed

2 files changed

+69
-23
lines changed

app/lib/frontend/templates/views/pkg/score_tab.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ d.Node _downloadsChart(WeeklyVersionDownloadCounts weeklyVersionDownloads) {
219219
value: 'stacked',
220220
label: 'Stacked',
221221
),
222+
(
223+
id: 'version-modes-percentage',
224+
value: 'percentage',
225+
label: 'Percentage',
226+
),
222227
],
223228
classes: ['downloads-chart-radio-button'],
224229
initialValue: 'unstacked')

pkg/web_app/lib/src/widget/downloads_chart/widget.dart

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:_pub_shared/format/date_format.dart';
1010
import 'package:_pub_shared/format/number_format.dart';
1111
import 'package:web/web.dart';
1212
import 'package:web_app/src/web_util.dart';
13+
import 'package:web_app/src/widget/widget.dart';
1314

1415
import 'computations.dart';
1516

@@ -29,6 +30,7 @@ String squareColorClass(int i) => 'downloads-chart-square-${colors[i]}';
2930
enum DisplayMode {
3031
stacked,
3132
unstacked,
33+
percentage,
3234
}
3335

3436
void create(HTMLElement element, Map<String, String> options) {
@@ -64,6 +66,8 @@ void create(HTMLElement element, Map<String, String> options) {
6466
.fuse(json.decoder)
6567
.convert(base64Decode(dataPoints)) as Map<String, dynamic>));
6668
final weeksToDisplay = math.min(40, data.totalWeeklyDownloads.length);
69+
final totals =
70+
data.totalWeeklyDownloads.sublist(0, weeksToDisplay).reversed.toList();
6771

6872
final majorDisplayLists = prepareWeekLists(
6973
data.totalWeeklyDownloads,
@@ -106,14 +110,21 @@ void create(HTMLElement element, Map<String, String> options) {
106110
svg = createNewSvg();
107111
element.append(svg);
108112
currentDisplayList = displayList;
109-
drawChart(svg, toolTip, displayList, data.newestDate,
110-
displayMode: currentDisplayMode);
113+
drawChart(
114+
svg,
115+
toolTip,
116+
displayList,
117+
data.newestDate,
118+
totals,
119+
displayMode: currentDisplayMode,
120+
);
111121
});
112122
});
113123

114124
final displayModesMap = <String, DisplayMode>{
115125
'stacked': DisplayMode.stacked,
116-
'unstacked': DisplayMode.unstacked
126+
'unstacked': DisplayMode.unstacked,
127+
'percentage': DisplayMode.percentage,
117128
};
118129

119130
final displayModes = document.getElementsByName(displayRadio).toList();
@@ -131,19 +142,32 @@ void create(HTMLElement element, Map<String, String> options) {
131142
svg = createNewSvg();
132143
element.append(svg);
133144
currentDisplayMode = displayMode;
134-
drawChart(svg, toolTip, currentDisplayList, data.newestDate,
135-
displayMode: displayMode);
145+
drawChart(
146+
svg,
147+
toolTip,
148+
currentDisplayList,
149+
data.newestDate,
150+
totals,
151+
displayMode: displayMode,
152+
);
136153
});
137154
});
138155

139-
drawChart(svg, toolTip, majorDisplayLists, data.newestDate);
156+
drawChart(
157+
svg,
158+
toolTip,
159+
majorDisplayLists,
160+
data.newestDate,
161+
totals,
162+
);
140163
}
141164

142165
void drawChart(
143166
Element svg,
144167
HTMLDivElement toolTip,
145168
({List<String> ranges, List<List<int>> weekLists}) displayLists,
146169
DateTime newestDate,
170+
List<int> totals,
147171
{DisplayMode displayMode = DisplayMode.unstacked}) {
148172
final ranges = displayLists.ranges;
149173
final values = displayLists.weekLists;
@@ -169,10 +193,13 @@ void drawChart(
169193
/// Computes max value on y-axis such that we get a nice division for the
170194
/// interval length between the numbers shown by the ticks on the y axis.
171195
(int maxY, int interval) computeMaxYAndInterval(List<List<int>> values) {
172-
final maxDownloads = displayMode == DisplayMode.unstacked
173-
? values.fold<int>(1, (a, b) => math.max<int>(a, b.reduce(math.max)))
174-
: values.fold<int>(
175-
1, (a, b) => math.max<int>(a, b.reduce((x, y) => x + y)));
196+
final maxDownloads = switch (displayMode) {
197+
DisplayMode.unstacked =>
198+
values.fold<int>(1, (a, b) => math.max<int>(a, b.reduce(math.max))),
199+
DisplayMode.stacked => values.fold<int>(
200+
1, (a, b) => math.max<int>(a, b.reduce((x, y) => x + y))),
201+
_ => 100 // percentage
202+
};
176203

177204
final digits = maxDownloads.toString().length;
178205
final buffer = StringBuffer()..write('1');
@@ -195,7 +222,7 @@ void drawChart(
195222
final firstDate = computeDateForWeekNumber(newestDate, values.length, 0);
196223
final xAxisSpan = newestDate.difference(firstDate);
197224

198-
(double, double) computeCoordinates(DateTime date, int downloads) {
225+
(double, double) computeCoordinates(DateTime date, num downloads) {
199226
final duration = date.difference(firstDate);
200227
// We don't risk division by 0 here, since `xAxisSpan` is a non-zero duration.
201228
final x = leftPadding +
@@ -257,8 +284,10 @@ void drawChart(
257284
final tickLabel = SVGTextElement();
258285
tickLabel.setAttribute(
259286
'class', 'downloads-chart-tick-label downloads-chart-tick-label-y');
260-
tickLabel.text =
261-
'${compactFormat(i * interval).value}${compactFormat(i * interval).suffix}';
287+
final suffix = displayMode == DisplayMode.percentage
288+
? '%'
289+
: compactFormat(i * interval).suffix;
290+
tickLabel.text = '${compactFormat(i * interval).value}$suffix';
262291
tickLabel.setAttribute('x', '${xMax + marginPadding}');
263292
tickLabel.setAttribute('y', '$y');
264293
chart.append(tickLabel);
@@ -288,19 +317,24 @@ void drawChart(
288317

289318
// Chart lines and legends
290319

291-
final lastestDownloads = List.filled(values.length, 0);
320+
final latestDownloads = List<num>.filled(values.length, 0);
292321
final lines = <List<(double, double)>>[];
293322
for (int versionRange = 0; versionRange < values[0].length; versionRange++) {
294323
final List<(double, double)> lineCoordinates = <(double, double)>[];
295324
for (int week = 0; week < values.length; week++) {
296-
if (displayMode == DisplayMode.stacked) {
297-
lastestDownloads[week] += values[week][versionRange];
325+
final value = displayMode == DisplayMode.percentage
326+
? values[week][versionRange] * 100 / totals[week]
327+
: values[week][versionRange];
328+
329+
if (displayMode == DisplayMode.unstacked) {
330+
latestDownloads[week] = value;
298331
} else {
299-
lastestDownloads[week] = values[week][versionRange];
332+
latestDownloads[week] += value;
300333
}
334+
301335
final (x, y) = computeCoordinates(
302336
computeDateForWeekNumber(newestDate, values.length, week),
303-
lastestDownloads[week]);
337+
latestDownloads[week]);
304338
lineCoordinates.add((x, y));
305339
}
306340
lines.add(lineCoordinates);
@@ -349,7 +383,8 @@ void drawChart(
349383
path.setAttribute('clip-path', 'url(#clipRect)');
350384
chart.append(path);
351385

352-
if (displayMode == DisplayMode.stacked) {
386+
if (displayMode == DisplayMode.stacked ||
387+
displayMode == DisplayMode.percentage) {
353388
final prevLine = i == lines.length - 1
354389
? [(xZero, yZero), (xMax, yZero)]
355390
: lines[lines.length - 1 - i - 1];
@@ -450,20 +485,26 @@ void drawChart(
450485

451486
final downloads = values[nearestIndex];
452487
for (int i = 0; i < downloads.length; i++) {
453-
final index = ranges.length - 1 - i;
454-
if (downloads[index] > 0) {
488+
final rangeIndex = ranges.length - 1 - i;
489+
if (downloads[rangeIndex] > 0) {
455490
// We only show the exact download count in the tooltip if it is non-zero.
456491
final square = HTMLDivElement()
457492
..setAttribute(
458493
'class', 'downloads-chart-tooltip-square ${squareColorClass(i)}');
459-
final rangeText = HTMLSpanElement()..text = '${ranges[index]}: ';
494+
final rangeText = HTMLSpanElement()..text = '${ranges[rangeIndex]}: ';
460495
final tooltipRange = HTMLDivElement()
461496
..setAttribute('class', 'downloads-chart-tooltip-row')
462497
..append(square)
463498
..append(rangeText);
499+
500+
final suffix = (displayMode == DisplayMode.percentage)
501+
? '(${(downloads[rangeIndex] * 100 / totals[nearestIndex]).toStringAsPrecision(2)}%)'
502+
: '';
503+
final text =
504+
'${formatWithThousandSeperators(downloads[rangeIndex])}$suffix';
464505
final downloadsText = HTMLSpanElement()
465506
..setAttribute('class', 'downloads-chart-tooltip-downloads')
466-
..text = '${formatWithThousandSeperators(downloads[index])}';
507+
..text = text;
467508
final tooltipRow = HTMLDivElement()
468509
..setAttribute('class', 'downloads-chart-tooltip-row')
469510
..append(tooltipRange)

0 commit comments

Comments
 (0)