Skip to content

Commit e6c85db

Browse files
committed
Downloads chart: Add display modes stacked/unstacked
1 parent 83ad717 commit e6c85db

File tree

3 files changed

+109
-8
lines changed

3 files changed

+109
-8
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: 76 additions & 8 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,14 +288,20 @@ void drawChart(
244288

245289
// Chart lines and legends
246290

291+
final lastestDownloads = List.filled(values.length, 0);
247292
final lines = <StringBuffer>[];
248293
for (int versionRange = 0; versionRange < values[0].length; versionRange++) {
249294
final line = StringBuffer();
250295
var c = 'M';
251296
for (int week = 0; week < values.length; week++) {
297+
if (displayMode == DisplayMode.stacked) {
298+
lastestDownloads[week] += values[week][versionRange];
299+
} else {
300+
lastestDownloads[week] = values[week][versionRange];
301+
}
252302
final (x, y) = computeCoordinates(
253303
computeDateForWeekNumber(newestDate, values.length, week),
254-
values[week][versionRange]);
304+
lastestDownloads[week]);
255305
line.write(' $c$x $y');
256306
c = 'L';
257307
}
@@ -265,14 +315,32 @@ void drawChart(
265315
final legendHeight = 8;
266316

267317
for (int i = 0; i < lines.length; i++) {
268-
final path = SVGPathElement();
269-
path.setAttribute('class', '${strokeColorClass(i)} downloads-chart-line ');
270318
// We assign colors in reverse order so that main colors are chosen first for
271319
// the newest versions.
272-
path.setAttribute('d', '${lines[lines.length - 1 - i]}');
320+
final line = lines[lines.length - 1 - i];
321+
322+
final path = SVGPathElement();
323+
path.setAttribute('class', '${strokeColorClass(i)} downloads-chart-line ');
324+
path.setAttribute('d', '$line');
273325
path.setAttribute('clip-path', 'url(#clipRect)');
274326
chart.append(path);
275327

328+
if (displayMode == DisplayMode.stacked) {
329+
final area = SVGPathElement();
330+
area.setAttribute('class', '${fillColorClass(i)} downloads-chart-area ');
331+
final prevLine = i == lines.length - 1
332+
? ' M $xZero $yZero L$xMax $yZero'
333+
: lines[lines.length - 1 - i - 1];
334+
final reversed = prevLine
335+
.toString()
336+
.replaceAll(' M', '')
337+
.split('L')
338+
.reversed
339+
.join('L');
340+
area.setAttribute('d', '$line L$reversed Z');
341+
chart.append(area);
342+
}
343+
276344
final legend = SVGRectElement();
277345
chart.append(legend);
278346
legend.setAttribute('class',

pkg/web_css/lib/src/_pkg.scss

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

292+
.downloads-chart-display-modes {
293+
float: left;
294+
}
295+
292296
.downloads-chart-radio-button {
293297
margin-left: 10px;
294298
}
@@ -426,6 +430,10 @@
426430
stroke-linejoin: round;
427431
}
428432

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

0 commit comments

Comments
 (0)