Skip to content

Commit 913c00e

Browse files
authored
Downloads chart: Add display modes stacked/unstacked (#8541)
1 parent 6686eae commit 913c00e

File tree

3 files changed

+135
-14
lines changed

3 files changed

+135
-14
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,29 @@ d.Node _downloadsChart(WeeklyVersionDownloadCounts weeklyVersionDownloads) {
195195
initialValue: 'major')
196196
],
197197
);
198+
199+
final displayModes = d.div(
200+
classes: ['downloads-chart-display-modes'],
201+
children: [
202+
radioButtons(
203+
leadingText: 'Display as: ',
204+
name: 'display-modes',
205+
radios: [
206+
(
207+
id: 'display-modes-unstacked',
208+
value: 'unstacked',
209+
label: 'Unstacked'
210+
),
211+
(
212+
id: 'version-modes-stacked',
213+
value: 'stacked',
214+
label: 'Stacked',
215+
),
216+
],
217+
classes: ['downloads-chart-radio-button'],
218+
initialValue: 'unstacked')
219+
],
220+
);
198221
final container = d.div(
199222
classes: ['downloads-chart'],
200223
id: '-downloads-chart',
@@ -203,11 +226,13 @@ d.Node _downloadsChart(WeeklyVersionDownloadCounts weeklyVersionDownloads) {
203226
'data-downloads-chart-points':
204227
base64Encode(jsonUtf8Encoder.convert(weeklyVersionDownloads)),
205228
'data-downloads-chart-versions-radio': 'version-modes',
229+
'data-downloads-chart-display-radio': 'display-modes',
206230
},
207231
);
208232

209233
return d.fragment([
210234
d.h1(text: 'Weekly downloads'),
235+
displayModes,
211236
versionModes,
212237
container,
213238
]);

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

Lines changed: 102 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ String strokeColorClass(int i) => 'downloads-chart-stroke-${colors[i]}';
2626
String fillColorClass(int i) => 'downloads-chart-fill-${colors[i]}';
2727
String squareColorClass(int i) => 'downloads-chart-square-${colors[i]}';
2828

29+
enum DisplayMode {
30+
stacked,
31+
unstacked,
32+
}
33+
2934
void create(HTMLElement element, Map<String, String> options) {
3035
final dataPoints = options['points'];
3136
if (dataPoints == null) {
@@ -36,6 +41,12 @@ void create(HTMLElement element, Map<String, String> options) {
3641
if (versionsRadio == null) {
3742
throw UnsupportedError('data-downloads-chart-versions-radio required');
3843
}
44+
45+
final displayRadio = options['display-radio'];
46+
if (displayRadio == null) {
47+
throw UnsupportedError('data-downloads-chart-display-radio required');
48+
}
49+
3950
Element createNewSvg() {
4051
return document.createElementNS('http://www.w3.org/2000/svg', 'svg')
4152
..setAttribute('height', '100%')
@@ -72,6 +83,9 @@ void create(HTMLElement element, Map<String, String> options) {
7283
weeksToDisplay,
7384
);
7485

86+
var currentDisplayList = majorDisplayLists;
87+
var currentDisplayMode = DisplayMode.unstacked;
88+
7589
final versionModesLists = {
7690
'major': majorDisplayLists,
7791
'minor': minorDisplayLists,
@@ -91,7 +105,34 @@ void create(HTMLElement element, Map<String, String> options) {
91105
element.removeChild(svg);
92106
svg = createNewSvg();
93107
element.append(svg);
94-
drawChart(svg, toolTip, displayList, data.newestDate);
108+
currentDisplayList = displayList;
109+
drawChart(svg, toolTip, displayList, data.newestDate,
110+
displayMode: currentDisplayMode);
111+
});
112+
});
113+
114+
final displayModesMap = <String, DisplayMode>{
115+
'stacked': DisplayMode.stacked,
116+
'unstacked': DisplayMode.unstacked
117+
};
118+
119+
final displayModes = document.getElementsByName(displayRadio).toList();
120+
displayModes.forEach((i) {
121+
final radioButton = i as HTMLInputElement;
122+
final value = radioButton.value;
123+
final displayMode = displayModesMap[value];
124+
125+
if (displayMode == null) {
126+
throw UnsupportedError('Unsupported display-radio value: "$value"');
127+
}
128+
129+
radioButton.onClick.listen((e) {
130+
element.removeChild(svg);
131+
svg = createNewSvg();
132+
element.append(svg);
133+
currentDisplayMode = displayMode;
134+
drawChart(svg, toolTip, currentDisplayList, data.newestDate,
135+
displayMode: displayMode);
95136
});
96137
});
97138

@@ -103,7 +144,7 @@ void drawChart(
103144
HTMLDivElement toolTip,
104145
({List<String> ranges, List<List<int>> weekLists}) displayLists,
105146
DateTime newestDate,
106-
{bool stacked = false}) {
147+
{DisplayMode displayMode = DisplayMode.unstacked}) {
107148
final ranges = displayLists.ranges;
108149
final values = displayLists.weekLists;
109150

@@ -128,8 +169,11 @@ void drawChart(
128169
/// Computes max value on y-axis such that we get a nice division for the
129170
/// interval length between the numbers shown by the ticks on the y axis.
130171
(int maxY, int interval) computeMaxYAndInterval(List<List<int>> values) {
131-
final maxDownloads =
132-
values.fold<int>(1, (a, b) => math.max<int>(a, b.reduce(math.max)));
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)));
176+
133177
final digits = maxDownloads.toString().length;
134178
final buffer = StringBuffer()..write('1');
135179
if (digits > 2) {
@@ -244,18 +288,49 @@ void drawChart(
244288

245289
// Chart lines and legends
246290

247-
final lines = <StringBuffer>[];
291+
final lastestDownloads = List.filled(values.length, 0);
292+
final lines = <List<(double, double)>>[];
248293
for (int versionRange = 0; versionRange < values[0].length; versionRange++) {
249-
final line = StringBuffer();
250-
var c = 'M';
294+
final List<(double, double)> lineCoordinates = <(double, double)>[];
251295
for (int week = 0; week < values.length; week++) {
296+
if (displayMode == DisplayMode.stacked) {
297+
lastestDownloads[week] += values[week][versionRange];
298+
} else {
299+
lastestDownloads[week] = values[week][versionRange];
300+
}
252301
final (x, y) = computeCoordinates(
253302
computeDateForWeekNumber(newestDate, values.length, week),
254-
values[week][versionRange]);
255-
line.write(' $c$x $y');
256-
c = 'L';
303+
lastestDownloads[week]);
304+
lineCoordinates.add((x, y));
257305
}
258-
lines.add(line);
306+
lines.add(lineCoordinates);
307+
}
308+
309+
StringBuffer computeLinePath(List<(double, double)> coordinates) {
310+
final path = StringBuffer();
311+
var command = 'M';
312+
coordinates.forEach((c) {
313+
path.write(' $command${c.$1} ${c.$2}');
314+
command = 'L';
315+
});
316+
return path;
317+
}
318+
319+
StringBuffer computeAreaPath(List<(double, double)> topCoordinates,
320+
List<(double, double)> bottomCoordinates) {
321+
final path = StringBuffer();
322+
var command = 'M';
323+
topCoordinates.forEach((c) {
324+
path.write(' $command${c.$1} ${c.$2}');
325+
command = 'L';
326+
});
327+
328+
bottomCoordinates.reversed.forEach((c) {
329+
path.write(' $command${c.$1} ${c.$2}');
330+
command = 'L';
331+
});
332+
path.write('Z');
333+
return path;
259334
}
260335

261336
double legendX = xZero;
@@ -265,14 +340,27 @@ void drawChart(
265340
final legendHeight = 8;
266341

267342
for (int i = 0; i < lines.length; i++) {
343+
// We add the lines in reverse order so that the newest versions get the
344+
// main colors.
345+
final line = computeLinePath(lines[lines.length - 1 - i]);
268346
final path = SVGPathElement();
269347
path.setAttribute('class', '${strokeColorClass(i)} downloads-chart-line ');
270-
// We assign colors in reverse order so that main colors are chosen first for
271-
// the newest versions.
272-
path.setAttribute('d', '${lines[lines.length - 1 - i]}');
348+
path.setAttribute('d', '$line');
273349
path.setAttribute('clip-path', 'url(#clipRect)');
274350
chart.append(path);
275351

352+
if (displayMode == DisplayMode.stacked) {
353+
final prevLine = i == lines.length - 1
354+
? [(xZero, yZero), (xMax, yZero)]
355+
: lines[lines.length - 1 - i - 1];
356+
final areaPath = computeAreaPath(lines[lines.length - 1 - i], prevLine);
357+
final area = SVGPathElement();
358+
area.setAttribute('class', '${fillColorClass(i)} downloads-chart-area ');
359+
area.setAttribute('d', '$areaPath');
360+
area.setAttribute('clip-path', 'url(#clipRect)');
361+
chart.append(area);
362+
}
363+
276364
final legend = SVGRectElement();
277365
chart.append(legend);
278366
legend.setAttribute('class',

pkg/web_css/lib/src/_pkg.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@
288288
float: right;
289289
}
290290

291+
.downloads-chart-display-modes {
292+
float: left;
293+
}
294+
291295
.downloads-chart-radio-button {
292296
margin-left: 10px;
293297
}
@@ -425,6 +429,10 @@
425429
stroke-linejoin: round;
426430
}
427431

432+
.downloads-chart-area {
433+
opacity: 0.3;
434+
}
435+
428436
.downloads-chart-stroke-blue {
429437
stroke: var(--pub-downloads-chart-color-0);
430438
}

0 commit comments

Comments
 (0)