rshow 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -2
- package/dist/main.js +8500 -0
- package/package.json +48 -4
- package/skills/rshow/SKILL.md +144 -0
- package/skills/rshow/reference/cli.md +263 -0
- package/skills/rshow/reference/dashboard.md +122 -0
- package/skills/rshow/reference/mockups.md +113 -0
- package/skills/rshow/reference/page-contract.md +83 -0
- package/skills/rshow/reference/pages.md +174 -0
- package/skills/rshow/reference/poster.md +137 -0
- package/skills/rshow/reference/project-page.md +132 -0
- package/skills/rshow/reference/reports.md +158 -0
- package/skills/rshow/reference/sketch.md +78 -0
- package/skills/rshow/reference/terminal.md +148 -0
- package/skills/rshow/reference/themes.md +139 -0
- package/skills/rshow-implement/SKILL.md +112 -0
- package/skills/rshow-implement/reference/compare.md +156 -0
- package/skills/rshow-implement/reference/handoff.md +125 -0
- package/skills/rshow-implement/reference/map.md +151 -0
- package/skills/rshow-slides/SKILL.md +136 -0
- package/skills/rshow-slides/reference/layouts.md +251 -0
- package/themes/default/dashboard.css +521 -0
- package/themes/default/dashboard.js +1390 -0
- package/themes/default/deck.css +586 -0
- package/themes/default/deck.js +741 -0
- package/themes/default/example/capture.ans +1 -0
- package/themes/default/example/dashboard.html +106 -0
- package/themes/default/example/demo.cast +14 -0
- package/themes/default/example/drawing.excalidraw +377 -0
- package/themes/default/example/index.html +311 -0
- package/themes/default/example/mockup-desktop.html +331 -0
- package/themes/default/example/mockup-platforms.html +742 -0
- package/themes/default/example/mockup.html +416 -0
- package/themes/default/example/page.html +153 -0
- package/themes/default/example/poster-figure.svg +58 -0
- package/themes/default/example/poster.html +181 -0
- package/themes/default/example/project.html +229 -0
- package/themes/default/example/report.html +404 -0
- package/themes/default/example/results.csv +61 -0
- package/themes/default/example/sketch.html +178 -0
- package/themes/default/example/teaser.svg +66 -0
- package/themes/default/example/terminal.html +231 -0
- package/themes/default/example/tui.html +223 -0
- package/themes/default/fonts/IBMPlexMono-Regular-Latin1.woff2 +0 -0
- package/themes/default/fonts/IBMPlexSans-Light-Latin1.woff2 +0 -0
- package/themes/default/fonts/IBMPlexSans-Regular-Latin1.woff2 +0 -0
- package/themes/default/fonts/IBMPlexSans-SemiBold-Latin1.woff2 +0 -0
- package/themes/default/fonts/OFL.txt +93 -0
- package/themes/default/fonts.css +63 -0
- package/themes/default/index.html +573 -0
- package/themes/default/mockup.css +1078 -0
- package/themes/default/mockup.js +1433 -0
- package/themes/default/mode.js +242 -0
- package/themes/default/page.css +320 -0
- package/themes/default/poster.css +480 -0
- package/themes/default/poster.js +223 -0
- package/themes/default/project.css +482 -0
- package/themes/default/project.js +116 -0
- package/themes/default/report.css +776 -0
- package/themes/default/report.js +427 -0
- package/themes/default/sketch.css +166 -0
- package/themes/default/sketch.js +681 -0
- package/themes/default/skins/adwaita.css +669 -0
- package/themes/default/skins/custom.css +596 -0
- package/themes/default/skins/egui.css +639 -0
- package/themes/default/skins/fluent.css +1093 -0
- package/themes/default/skins/fluent.js +458 -0
- package/themes/default/skins/mac.css +647 -0
- package/themes/default/skins/material.css +692 -0
- package/themes/default/skins/sketch.css +436 -0
- package/themes/default/skins/sketch.js +494 -0
- package/themes/default/skins/wire.css +561 -0
- package/themes/default/terminal.css +292 -0
- package/themes/default/terminal.js +433 -0
- package/themes/default/theme.json +149 -0
- package/themes/default/tokens.css +255 -0
- package/themes/default/tui.css +703 -0
- package/themes/default/tui.js +472 -0
- package/worker/index.js +6918 -0
- package/worker/migrations/0001_init.sql +52 -0
- package/worker/migrations/0002_blobs.sql +5 -0
- package/worker/migrations/0003_projects.sql +31 -0
- package/worker/migrations/0004_page_data_blob.sql +19 -0
|
@@ -0,0 +1,1390 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* rshow theme "default": results dashboard runtime. Zero dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Markup:
|
|
5
|
+
*
|
|
6
|
+
* <div class="dashboard" data-src="results.csv" data-facet="workload"
|
|
7
|
+
* data-x="nodes" data-series="system" data-metric="throughput"
|
|
8
|
+
* data-unit="publishes/s" data-filters="system,workload,nodes">
|
|
9
|
+
* <header class="dash-head"><h1>Title</h1>
|
|
10
|
+
* <p class="dash-lede">One line of prose.</p>
|
|
11
|
+
* <p class="dash-meta" data-dashboard-source></p></header>
|
|
12
|
+
* <form class="dash-filters" data-dashboard-filters></form>
|
|
13
|
+
* <p class="dash-status" data-dashboard-status aria-live="polite"></p>
|
|
14
|
+
* <section class="dash-section"><h2>Summary</h2>
|
|
15
|
+
* <div class="dash-box" data-dashboard-summary></div></section>
|
|
16
|
+
* <section class="dash-section"><h2>Chart</h2>
|
|
17
|
+
* <figure class="dash-figure"><div data-dashboard-facets></div>
|
|
18
|
+
* <figcaption data-dashboard-caption></figcaption></figure></section>
|
|
19
|
+
* <section class="dash-section"><h2>Runs</h2>
|
|
20
|
+
* <div class="dash-toolbar" data-dashboard-toolbar></div>
|
|
21
|
+
* <div class="dash-box" data-dashboard-table></div></section>
|
|
22
|
+
* </div>
|
|
23
|
+
*
|
|
24
|
+
* The data comes from a <script type="application/json" id="data"> block if the
|
|
25
|
+
* page has one, otherwise from data-src, which must be a relative path in this
|
|
26
|
+
* folder: the content policy allows connect-src 'self' and nothing else. A .json
|
|
27
|
+
* path is read as an array of rows or as { "rows": [ ... ] }, anything else as
|
|
28
|
+
* CSV with quoted fields.
|
|
29
|
+
*
|
|
30
|
+
* Which column does what is a page decision, so it is an attribute and not a
|
|
31
|
+
* guess: data-facet groups the panels and the summary, data-x is the axis,
|
|
32
|
+
* data-series splits a panel into several lines (1 to 8, the palette size) and
|
|
33
|
+
* data-metric is the number the summary and the chart read. The controls come
|
|
34
|
+
* from data-filters, one select per text column and a from/to pair per number
|
|
35
|
+
* column.
|
|
36
|
+
*
|
|
37
|
+
* State lives in the URL hash, so a link reproduces a view. The chart is drawn
|
|
38
|
+
* with the token values read at draw time, which is why a mode change redraws it
|
|
39
|
+
* instead of restyling it.
|
|
40
|
+
*/
|
|
41
|
+
(function () {
|
|
42
|
+
'use strict';
|
|
43
|
+
|
|
44
|
+
var root = document.querySelector('.dashboard');
|
|
45
|
+
if (!root) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var hosts = {
|
|
50
|
+
source: root.querySelector('[data-dashboard-source]'),
|
|
51
|
+
status: root.querySelector('[data-dashboard-status]'),
|
|
52
|
+
filters: root.querySelector('[data-dashboard-filters]'),
|
|
53
|
+
summary: root.querySelector('[data-dashboard-summary]'),
|
|
54
|
+
facets: root.querySelector('[data-dashboard-facets]'),
|
|
55
|
+
caption: root.querySelector('[data-dashboard-caption]'),
|
|
56
|
+
toolbar: root.querySelector('[data-dashboard-toolbar]'),
|
|
57
|
+
table: root.querySelector('[data-dashboard-table]')
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function attr(name) {
|
|
61
|
+
return (root.getAttribute(name) || '').replace(/^\s+|\s+$/g, '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var config = {
|
|
65
|
+
src: attr('data-src'),
|
|
66
|
+
facet: attr('data-facet'),
|
|
67
|
+
x: attr('data-x'),
|
|
68
|
+
series: attr('data-series'),
|
|
69
|
+
metric: attr('data-metric'),
|
|
70
|
+
unit: attr('data-unit'),
|
|
71
|
+
filters: attr('data-filters')
|
|
72
|
+
.split(',')
|
|
73
|
+
.map(function (name) {
|
|
74
|
+
return name.replace(/^\s+|\s+$/g, '');
|
|
75
|
+
})
|
|
76
|
+
.filter(function (name) {
|
|
77
|
+
return name !== '';
|
|
78
|
+
})
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
var state = {
|
|
82
|
+
source: '',
|
|
83
|
+
columns: [],
|
|
84
|
+
rows: [],
|
|
85
|
+
controls: [],
|
|
86
|
+
metric: '',
|
|
87
|
+
sort: { key: '', dir: 1 },
|
|
88
|
+
view: []
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
var SEP = '\u0001';
|
|
92
|
+
var lastWidth = 0;
|
|
93
|
+
var toolbarBuilt = false;
|
|
94
|
+
|
|
95
|
+
// ------------------------------------------------------------- utilities --
|
|
96
|
+
|
|
97
|
+
function esc(value) {
|
|
98
|
+
return String(value === undefined || value === null ? '' : value)
|
|
99
|
+
.replace(/&/g, '&')
|
|
100
|
+
.replace(/</g, '<')
|
|
101
|
+
.replace(/>/g, '>')
|
|
102
|
+
.replace(/"/g, '"')
|
|
103
|
+
.replace(/'/g, ''');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function el(tag, className) {
|
|
107
|
+
var node = document.createElement(tag);
|
|
108
|
+
if (className) {
|
|
109
|
+
node.className = className;
|
|
110
|
+
}
|
|
111
|
+
return node;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isNumeric(value) {
|
|
115
|
+
if (value === null || value === undefined) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
var text = String(value).replace(/^\s+|\s+$/g, '');
|
|
119
|
+
return text !== '' && isFinite(Number(text));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function round(value) {
|
|
123
|
+
return Math.round(value * 10) / 10;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function fmt(value) {
|
|
127
|
+
if (!isFinite(value)) {
|
|
128
|
+
return '-';
|
|
129
|
+
}
|
|
130
|
+
var text = String(round(value));
|
|
131
|
+
var dot = text.indexOf('.');
|
|
132
|
+
var whole = dot === -1 ? text : text.slice(0, dot);
|
|
133
|
+
var rest = dot === -1 ? '' : text.slice(dot);
|
|
134
|
+
return whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + rest;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function fmtTick(value) {
|
|
138
|
+
if (Math.abs(value) >= 10000) {
|
|
139
|
+
return fmt(value / 1000) + 'k';
|
|
140
|
+
}
|
|
141
|
+
return fmt(value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function label(key) {
|
|
145
|
+
var words = String(key)
|
|
146
|
+
.replace(/[_.\-]+/g, ' ')
|
|
147
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
148
|
+
.toLowerCase()
|
|
149
|
+
.split(' ')
|
|
150
|
+
.filter(function (word) {
|
|
151
|
+
return word !== '';
|
|
152
|
+
});
|
|
153
|
+
if (!words.length) {
|
|
154
|
+
return String(key);
|
|
155
|
+
}
|
|
156
|
+
var unit = /^(s|ms|us|ns|kb|mb|gb|pct|%)$/.test(words[words.length - 1]) && words.length > 1;
|
|
157
|
+
var name = unit ? words.slice(0, -1).join(' ') : words.join(' ');
|
|
158
|
+
var text = name.charAt(0).toUpperCase() + name.slice(1);
|
|
159
|
+
return unit ? text + ' (' + words[words.length - 1] + ')' : text;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function slug(key) {
|
|
163
|
+
return String(key)
|
|
164
|
+
.toLowerCase()
|
|
165
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
166
|
+
.replace(/^-+|-+$/g, '');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function quantile(values, q) {
|
|
170
|
+
if (!values.length) {
|
|
171
|
+
return NaN;
|
|
172
|
+
}
|
|
173
|
+
var sorted = values.slice().sort(function (a, b) {
|
|
174
|
+
return a - b;
|
|
175
|
+
});
|
|
176
|
+
var pos = (sorted.length - 1) * q;
|
|
177
|
+
var low = Math.floor(pos);
|
|
178
|
+
var high = Math.ceil(pos);
|
|
179
|
+
if (low === high) {
|
|
180
|
+
return sorted[low];
|
|
181
|
+
}
|
|
182
|
+
return sorted[low] + (sorted[high] - sorted[low]) * (pos - low);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function niceCeil(value) {
|
|
186
|
+
if (!(value > 0)) {
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
var power = Math.pow(10, Math.floor(Math.log(value) / Math.LN10));
|
|
190
|
+
var step = value / power;
|
|
191
|
+
var top = step <= 1 ? 1 : step <= 2 ? 2 : step <= 2.5 ? 2.5 : step <= 5 ? 5 : 10;
|
|
192
|
+
return top * power;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function distinct(rows, key) {
|
|
196
|
+
var seen = [];
|
|
197
|
+
var index = {};
|
|
198
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
199
|
+
var value = rows[i][key];
|
|
200
|
+
if (value === undefined || value === null || value === '') {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
var id = String(value);
|
|
204
|
+
if (index[id] !== true) {
|
|
205
|
+
index[id] = true;
|
|
206
|
+
seen.push(id);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return seen;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function findColumn(key) {
|
|
213
|
+
for (var i = 0; i < state.columns.length; i += 1) {
|
|
214
|
+
if (state.columns[i].key === key) {
|
|
215
|
+
return state.columns[i];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function controlFor(key) {
|
|
222
|
+
for (var i = 0; i < state.controls.length; i += 1) {
|
|
223
|
+
if (state.controls[i].key === key) {
|
|
224
|
+
return state.controls[i];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function metricTitle() {
|
|
231
|
+
var column = findColumn(state.metric);
|
|
232
|
+
if (!column) {
|
|
233
|
+
return 'the metric';
|
|
234
|
+
}
|
|
235
|
+
if (config.unit && column.label.toLowerCase().indexOf(config.unit.toLowerCase()) === -1) {
|
|
236
|
+
return column.label + ' (' + config.unit + ')';
|
|
237
|
+
}
|
|
238
|
+
return column.label;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function facetKey() {
|
|
242
|
+
if (config.facet && findColumn(config.facet)) {
|
|
243
|
+
return config.facet;
|
|
244
|
+
}
|
|
245
|
+
for (var i = 0; i < state.columns.length; i += 1) {
|
|
246
|
+
if (state.columns[i].type === 'text') {
|
|
247
|
+
return state.columns[i].key;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return '';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function xKey() {
|
|
254
|
+
if (config.x && findColumn(config.x)) {
|
|
255
|
+
return config.x;
|
|
256
|
+
}
|
|
257
|
+
var group = facetKey();
|
|
258
|
+
for (var i = 0; i < state.columns.length; i += 1) {
|
|
259
|
+
var column = state.columns[i];
|
|
260
|
+
if (column.key !== group && column.key !== state.metric) {
|
|
261
|
+
return column.key;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return state.columns.length ? state.columns[0].key : '';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function seriesKey() {
|
|
268
|
+
if (config.series && config.series !== facetKey() && findColumn(config.series)) {
|
|
269
|
+
return config.series;
|
|
270
|
+
}
|
|
271
|
+
return '';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ------------------------------------------------------------ csv, json --
|
|
275
|
+
|
|
276
|
+
function parseCsv(text) {
|
|
277
|
+
var grid = [];
|
|
278
|
+
var row = [];
|
|
279
|
+
var field = '';
|
|
280
|
+
var quoted = false;
|
|
281
|
+
var i = 0;
|
|
282
|
+
while (i < text.length) {
|
|
283
|
+
var ch = text.charAt(i);
|
|
284
|
+
if (quoted) {
|
|
285
|
+
if (ch === '"') {
|
|
286
|
+
if (text.charAt(i + 1) === '"') {
|
|
287
|
+
field += '"';
|
|
288
|
+
i += 2;
|
|
289
|
+
} else {
|
|
290
|
+
quoted = false;
|
|
291
|
+
i += 1;
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
field += ch;
|
|
295
|
+
i += 1;
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (ch === '"') {
|
|
300
|
+
quoted = true;
|
|
301
|
+
i += 1;
|
|
302
|
+
} else if (ch === ',') {
|
|
303
|
+
row.push(field);
|
|
304
|
+
field = '';
|
|
305
|
+
i += 1;
|
|
306
|
+
} else if (ch === '\r') {
|
|
307
|
+
i += 1;
|
|
308
|
+
} else if (ch === '\n') {
|
|
309
|
+
row.push(field);
|
|
310
|
+
grid.push(row);
|
|
311
|
+
row = [];
|
|
312
|
+
field = '';
|
|
313
|
+
i += 1;
|
|
314
|
+
} else {
|
|
315
|
+
field += ch;
|
|
316
|
+
i += 1;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (field !== '' || row.length) {
|
|
320
|
+
row.push(field);
|
|
321
|
+
grid.push(row);
|
|
322
|
+
}
|
|
323
|
+
if (!grid.length) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
var head = grid[0].map(function (name) {
|
|
328
|
+
return name.replace(/^\s+|\s+$/g, '');
|
|
329
|
+
});
|
|
330
|
+
var rows = [];
|
|
331
|
+
for (var r = 1; r < grid.length; r += 1) {
|
|
332
|
+
var cells = grid[r];
|
|
333
|
+
var empty = true;
|
|
334
|
+
var record = {};
|
|
335
|
+
for (var c = 0; c < head.length; c += 1) {
|
|
336
|
+
var value = cells[c] === undefined ? '' : String(cells[c]).replace(/^\s+|\s+$/g, '');
|
|
337
|
+
if (value !== '') {
|
|
338
|
+
empty = false;
|
|
339
|
+
}
|
|
340
|
+
record[head[c]] = value;
|
|
341
|
+
}
|
|
342
|
+
if (!empty) {
|
|
343
|
+
rows.push(record);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return rows;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function columnsFrom(rows) {
|
|
350
|
+
var keys = [];
|
|
351
|
+
var i;
|
|
352
|
+
var j;
|
|
353
|
+
for (i = 0; i < rows.length; i += 1) {
|
|
354
|
+
var names = Object.keys(rows[i]);
|
|
355
|
+
for (j = 0; j < names.length; j += 1) {
|
|
356
|
+
if (keys.indexOf(names[j]) === -1) {
|
|
357
|
+
keys.push(names[j]);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return keys;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function inferType(key, rows) {
|
|
365
|
+
var numeric = true;
|
|
366
|
+
var dates = true;
|
|
367
|
+
var seen = 0;
|
|
368
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
369
|
+
var value = rows[i][key];
|
|
370
|
+
if (value === undefined || value === null || value === '') {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
seen += 1;
|
|
374
|
+
if (numeric && !isNumeric(value)) {
|
|
375
|
+
numeric = false;
|
|
376
|
+
}
|
|
377
|
+
if (dates && !/^\d{4}-\d{2}-\d{2}$/.test(String(value))) {
|
|
378
|
+
dates = false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (seen > 0 && numeric) {
|
|
382
|
+
return 'number';
|
|
383
|
+
}
|
|
384
|
+
if (seen > 0 && dates) {
|
|
385
|
+
return 'date';
|
|
386
|
+
}
|
|
387
|
+
return 'text';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ---------------------------------------------------------------- state --
|
|
391
|
+
|
|
392
|
+
function defaultMetric() {
|
|
393
|
+
var column = findColumn(config.metric);
|
|
394
|
+
if (column && column.type === 'number') {
|
|
395
|
+
return column.key;
|
|
396
|
+
}
|
|
397
|
+
for (var i = 0; i < state.columns.length; i += 1) {
|
|
398
|
+
if (state.columns[i].type === 'number') {
|
|
399
|
+
return state.columns[i].key;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return '';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function buildControls() {
|
|
406
|
+
state.controls = [];
|
|
407
|
+
config.filters.forEach(function (key) {
|
|
408
|
+
var column = findColumn(key);
|
|
409
|
+
if (!column) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (column.type === 'number') {
|
|
413
|
+
var numbers = state.rows
|
|
414
|
+
.map(function (row) {
|
|
415
|
+
return Number(row[key]);
|
|
416
|
+
})
|
|
417
|
+
.filter(function (value) {
|
|
418
|
+
return isFinite(value);
|
|
419
|
+
});
|
|
420
|
+
state.controls.push({
|
|
421
|
+
key: key,
|
|
422
|
+
label: column.label,
|
|
423
|
+
type: 'number',
|
|
424
|
+
from: '',
|
|
425
|
+
to: '',
|
|
426
|
+
min: numbers.length ? Math.min.apply(null, numbers) : 0,
|
|
427
|
+
max: numbers.length ? Math.max.apply(null, numbers) : 0
|
|
428
|
+
});
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
var values = distinct(state.rows, key);
|
|
432
|
+
// A select with a hundred entries is worse than no filter at all.
|
|
433
|
+
if (values.length > 1 && values.length <= 40) {
|
|
434
|
+
state.controls.push({ key: key, label: column.label, type: 'text', value: '', values: values });
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function applyDefaults() {
|
|
440
|
+
state.controls.forEach(function (ctl) {
|
|
441
|
+
if (ctl.type === 'number') {
|
|
442
|
+
ctl.from = '';
|
|
443
|
+
ctl.to = '';
|
|
444
|
+
} else {
|
|
445
|
+
ctl.value = '';
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
state.sort = { key: '', dir: 1 };
|
|
449
|
+
state.metric = defaultMetric();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function readHash() {
|
|
453
|
+
var body = String(window.location.hash || '').replace(/^#/, '');
|
|
454
|
+
if (body === '') {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
function decode(value) {
|
|
458
|
+
try {
|
|
459
|
+
return decodeURIComponent(value.replace(/\+/g, ' '));
|
|
460
|
+
} catch (err) {
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
body.split('&').forEach(function (part) {
|
|
465
|
+
if (part === '') {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
var split = part.indexOf('=');
|
|
469
|
+
if (split === -1) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
var key = decode(part.slice(0, split));
|
|
473
|
+
var value = decode(part.slice(split + 1));
|
|
474
|
+
if (key === 'm') {
|
|
475
|
+
var column = findColumn(value);
|
|
476
|
+
if (column && column.type === 'number') {
|
|
477
|
+
state.metric = value;
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (key === 's') {
|
|
482
|
+
if (findColumn(value)) {
|
|
483
|
+
state.sort.key = value;
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (key === 'd') {
|
|
488
|
+
state.sort.dir = value === 'desc' ? -1 : 1;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (key.indexOf('f.') !== 0) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
var ctl = controlFor(key.slice(2));
|
|
495
|
+
if (!ctl) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (ctl.type === 'number') {
|
|
499
|
+
var bounds = value.split('..');
|
|
500
|
+
ctl.from = bounds[0] ? bounds[0] : '';
|
|
501
|
+
ctl.to = bounds.length > 1 && bounds[1] ? bounds[1] : '';
|
|
502
|
+
} else if (ctl.values.indexOf(value) !== -1) {
|
|
503
|
+
ctl.value = value;
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function writeHash() {
|
|
509
|
+
var parts = [];
|
|
510
|
+
function put(key, value) {
|
|
511
|
+
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
|
|
512
|
+
}
|
|
513
|
+
if (state.metric) {
|
|
514
|
+
put('m', state.metric);
|
|
515
|
+
}
|
|
516
|
+
if (state.sort.key) {
|
|
517
|
+
put('s', state.sort.key);
|
|
518
|
+
put('d', state.sort.dir < 0 ? 'desc' : 'asc');
|
|
519
|
+
}
|
|
520
|
+
state.controls.forEach(function (ctl) {
|
|
521
|
+
if (ctl.type === 'number') {
|
|
522
|
+
if (ctl.from !== '' || ctl.to !== '') {
|
|
523
|
+
put('f.' + ctl.key, ctl.from + '..' + ctl.to);
|
|
524
|
+
}
|
|
525
|
+
} else if (ctl.value !== '') {
|
|
526
|
+
put('f.' + ctl.key, ctl.value);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
var next = parts.length ? '#' + parts.join('&') : '';
|
|
530
|
+
if (window.location.hash === next) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
var url = window.location.pathname + window.location.search + next;
|
|
534
|
+
try {
|
|
535
|
+
window.history.replaceState(null, '', url);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
window.location.replace(url);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function filtered() {
|
|
542
|
+
if (!state.controls.length) {
|
|
543
|
+
return state.rows.slice();
|
|
544
|
+
}
|
|
545
|
+
return state.rows.filter(function (row) {
|
|
546
|
+
for (var i = 0; i < state.controls.length; i += 1) {
|
|
547
|
+
var ctl = state.controls[i];
|
|
548
|
+
var value = row[ctl.key];
|
|
549
|
+
if (ctl.type === 'number') {
|
|
550
|
+
if (ctl.from === '' && ctl.to === '') {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (!isNumeric(value)) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
var number = Number(value);
|
|
557
|
+
if (ctl.from !== '' && isNumeric(ctl.from) && number < Number(ctl.from)) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
if (ctl.to !== '' && isNumeric(ctl.to) && number > Number(ctl.to)) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
} else if (ctl.value !== '' && String(value) !== ctl.value) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return true;
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function sortedView() {
|
|
572
|
+
var rows = state.view.slice();
|
|
573
|
+
var key = state.sort.key;
|
|
574
|
+
var column = findColumn(key);
|
|
575
|
+
if (!key || !column) {
|
|
576
|
+
return rows;
|
|
577
|
+
}
|
|
578
|
+
var dir = state.sort.dir;
|
|
579
|
+
rows.sort(function (a, b) {
|
|
580
|
+
var left = a[key];
|
|
581
|
+
var right = b[key];
|
|
582
|
+
if (column.type === 'number') {
|
|
583
|
+
var an = Number(left);
|
|
584
|
+
var bn = Number(right);
|
|
585
|
+
if (!isFinite(an) && !isFinite(bn)) {
|
|
586
|
+
return 0;
|
|
587
|
+
}
|
|
588
|
+
if (!isFinite(an)) {
|
|
589
|
+
return 1;
|
|
590
|
+
}
|
|
591
|
+
if (!isFinite(bn)) {
|
|
592
|
+
return -1;
|
|
593
|
+
}
|
|
594
|
+
return (an - bn) * dir;
|
|
595
|
+
}
|
|
596
|
+
return String(left === undefined ? '' : left).localeCompare(String(right === undefined ? '' : right), 'en') * dir;
|
|
597
|
+
});
|
|
598
|
+
return rows;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function groupOrder() {
|
|
602
|
+
var key = facetKey();
|
|
603
|
+
if (!key) {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
var present = distinct(state.view, key);
|
|
607
|
+
return distinct(state.rows, key).filter(function (value) {
|
|
608
|
+
return present.indexOf(value) !== -1;
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// --------------------------------------------------------------- render --
|
|
613
|
+
|
|
614
|
+
function renderAll(rebuild) {
|
|
615
|
+
if (rebuild) {
|
|
616
|
+
buildFilters();
|
|
617
|
+
}
|
|
618
|
+
state.view = filtered();
|
|
619
|
+
renderSource();
|
|
620
|
+
renderStatus();
|
|
621
|
+
renderSummary();
|
|
622
|
+
renderCharts();
|
|
623
|
+
renderTable();
|
|
624
|
+
writeHash();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function renderSource() {
|
|
628
|
+
if (!hosts.source) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
var total = state.rows.length;
|
|
632
|
+
hosts.source.innerHTML =
|
|
633
|
+
'Data source: <strong>' +
|
|
634
|
+
esc(state.source) +
|
|
635
|
+
'</strong>. ' +
|
|
636
|
+
fmt(total) +
|
|
637
|
+
(total === 1 ? ' run' : ' runs') +
|
|
638
|
+
', ' +
|
|
639
|
+
fmt(state.columns.length) +
|
|
640
|
+
' columns.';
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function renderStatus(message) {
|
|
644
|
+
if (!hosts.status) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
hosts.status.textContent =
|
|
648
|
+
message === undefined
|
|
649
|
+
? 'Showing ' + fmt(state.view.length) + ' of ' + fmt(state.rows.length) + ' runs.'
|
|
650
|
+
: message;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function renderSummary() {
|
|
654
|
+
var host = hosts.summary;
|
|
655
|
+
if (!host) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
var group = facetKey();
|
|
659
|
+
var column = findColumn(state.metric);
|
|
660
|
+
if (!group || !column) {
|
|
661
|
+
host.textContent = '';
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
var groups = groupOrder();
|
|
665
|
+
if (!groups.length) {
|
|
666
|
+
host.textContent = '';
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
var gathered = {};
|
|
670
|
+
state.view.forEach(function (row) {
|
|
671
|
+
if (!isNumeric(row[state.metric])) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
var id = String(row[group]);
|
|
675
|
+
if (!gathered[id]) {
|
|
676
|
+
gathered[id] = [];
|
|
677
|
+
}
|
|
678
|
+
gathered[id].push(Number(row[state.metric]));
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
var html =
|
|
682
|
+
'<table class="dash-table dash-table--summary"><caption>Runs, median and p95 of ' +
|
|
683
|
+
esc(metricTitle()) +
|
|
684
|
+
' for each ' +
|
|
685
|
+
esc(findColumn(group).label.toLowerCase()) +
|
|
686
|
+
'.</caption><thead><tr><th scope="col">' +
|
|
687
|
+
esc(findColumn(group).label) +
|
|
688
|
+
'</th><th scope="col" class="dash-num">Runs</th><th scope="col" class="dash-num">Median</th>' +
|
|
689
|
+
'<th scope="col" class="dash-num">p95</th></tr></thead><tbody>';
|
|
690
|
+
|
|
691
|
+
groups.forEach(function (value) {
|
|
692
|
+
var values = gathered[value] ? gathered[value] : [];
|
|
693
|
+
html +=
|
|
694
|
+
'<tr><th scope="row">' +
|
|
695
|
+
esc(value) +
|
|
696
|
+
'</th><td class="dash-num">' +
|
|
697
|
+
fmt(values.length) +
|
|
698
|
+
'</td><td class="dash-num">' +
|
|
699
|
+
fmt(quantile(values, 0.5)) +
|
|
700
|
+
'</td><td class="dash-num">' +
|
|
701
|
+
fmt(quantile(values, 0.95)) +
|
|
702
|
+
'</td></tr>';
|
|
703
|
+
});
|
|
704
|
+
html += '</tbody></table>';
|
|
705
|
+
host.innerHTML = html;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function renderTable() {
|
|
709
|
+
var host = hosts.table;
|
|
710
|
+
if (!host) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
var rows = sortedView();
|
|
714
|
+
|
|
715
|
+
if (hosts.toolbar && !toolbarBuilt) {
|
|
716
|
+
hosts.toolbar.textContent = '';
|
|
717
|
+
var note = el('p', 'dash-note');
|
|
718
|
+
note.setAttribute('data-dashboard-order', '');
|
|
719
|
+
var button = el('button', 'dash-btn');
|
|
720
|
+
button.type = 'button';
|
|
721
|
+
button.textContent = 'Download CSV of this view';
|
|
722
|
+
button.addEventListener('click', download);
|
|
723
|
+
hosts.toolbar.appendChild(note);
|
|
724
|
+
hosts.toolbar.appendChild(button);
|
|
725
|
+
toolbarBuilt = true;
|
|
726
|
+
}
|
|
727
|
+
var order = hosts.toolbar ? hosts.toolbar.querySelector('[data-dashboard-order]') : null;
|
|
728
|
+
if (order) {
|
|
729
|
+
var sorted = findColumn(state.sort.key);
|
|
730
|
+
order.textContent =
|
|
731
|
+
fmt(rows.length) +
|
|
732
|
+
' of ' +
|
|
733
|
+
fmt(state.rows.length) +
|
|
734
|
+
' runs, ' +
|
|
735
|
+
(sorted ? 'sorted by ' + sorted.label.toLowerCase() + (state.sort.dir < 0 ? ', high to low' : ', low to high') : 'in data order') +
|
|
736
|
+
'.';
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (!rows.length) {
|
|
740
|
+
host.innerHTML = '<p class="dash-empty">No runs match these filters.</p>';
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
var html = '<table class="dash-table dash-table--runs"><thead><tr>';
|
|
745
|
+
state.columns.forEach(function (column) {
|
|
746
|
+
var active = state.sort.key === column.key;
|
|
747
|
+
html +=
|
|
748
|
+
'<th scope="col"' +
|
|
749
|
+
(column.type === 'number' ? ' class="dash-num"' : '') +
|
|
750
|
+
(active ? ' aria-sort="' + (state.sort.dir < 0 ? 'descending' : 'ascending') + '"' : '') +
|
|
751
|
+
'><button type="button" class="dash-sort" data-sort-key="' +
|
|
752
|
+
esc(column.key) +
|
|
753
|
+
'"' +
|
|
754
|
+
(active ? ' data-dir="' + (state.sort.dir < 0 ? 'desc' : 'asc') + '"' : '') +
|
|
755
|
+
'>' +
|
|
756
|
+
esc(column.label) +
|
|
757
|
+
'</button></th>';
|
|
758
|
+
});
|
|
759
|
+
html += '</tr></thead><tbody>';
|
|
760
|
+
rows.forEach(function (row) {
|
|
761
|
+
html += '<tr>';
|
|
762
|
+
state.columns.forEach(function (column) {
|
|
763
|
+
var value = row[column.key];
|
|
764
|
+
if (column.type === 'number') {
|
|
765
|
+
html += '<td class="dash-num">' + esc(isNumeric(value) ? fmt(Number(value)) : String(value === undefined ? '' : value)) + '</td>';
|
|
766
|
+
} else {
|
|
767
|
+
html += '<td>' + esc(value === undefined ? '' : value) + '</td>';
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
html += '</tr>';
|
|
771
|
+
});
|
|
772
|
+
html += '</tbody></table>';
|
|
773
|
+
host.innerHTML = html;
|
|
774
|
+
|
|
775
|
+
var buttons = host.querySelectorAll('.dash-sort');
|
|
776
|
+
for (var i = 0; i < buttons.length; i += 1) {
|
|
777
|
+
buttons[i].addEventListener('click', onSortClick);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function onSortClick(event) {
|
|
782
|
+
var button = event.currentTarget;
|
|
783
|
+
var key = button.getAttribute('data-sort-key');
|
|
784
|
+
if (state.sort.key === key) {
|
|
785
|
+
state.sort.dir = state.sort.dir < 0 ? 1 : -1;
|
|
786
|
+
} else {
|
|
787
|
+
state.sort.key = key;
|
|
788
|
+
state.sort.dir = 1;
|
|
789
|
+
}
|
|
790
|
+
renderAll(false);
|
|
791
|
+
var again = hosts.table.querySelector('[data-sort-key="' + key.replace(/"/g, '\\"') + '"]');
|
|
792
|
+
if (again) {
|
|
793
|
+
again.focus();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// --------------------------------------------------------------- filters --
|
|
798
|
+
|
|
799
|
+
function setValue(id, value) {
|
|
800
|
+
var node = document.getElementById(id);
|
|
801
|
+
if (node) {
|
|
802
|
+
node.value = value;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function buildFilters() {
|
|
807
|
+
var host = hosts.filters;
|
|
808
|
+
if (!host) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
host.textContent = '';
|
|
812
|
+
if (state.metric) {
|
|
813
|
+
host.appendChild(metricField());
|
|
814
|
+
}
|
|
815
|
+
state.controls.forEach(function (ctl) {
|
|
816
|
+
if (ctl.type === 'number') {
|
|
817
|
+
host.appendChild(numberField(ctl, 'from'));
|
|
818
|
+
host.appendChild(numberField(ctl, 'to'));
|
|
819
|
+
} else {
|
|
820
|
+
host.appendChild(selectField(ctl));
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
if (state.metric || state.controls.length) {
|
|
824
|
+
host.appendChild(actionField());
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function metricField() {
|
|
829
|
+
var field = el('div', 'dash-field');
|
|
830
|
+
var node = el('label');
|
|
831
|
+
node.textContent = 'Metric';
|
|
832
|
+
node.setAttribute('for', 'dash-metric');
|
|
833
|
+
var wrap = el('div', 'dash-select');
|
|
834
|
+
var select = document.createElement('select');
|
|
835
|
+
select.id = 'dash-metric';
|
|
836
|
+
state.columns.forEach(function (column) {
|
|
837
|
+
if (column.type !== 'number') {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
var option = document.createElement('option');
|
|
841
|
+
option.value = column.key;
|
|
842
|
+
option.textContent = column.label;
|
|
843
|
+
if (column.key === state.metric) {
|
|
844
|
+
option.selected = true;
|
|
845
|
+
}
|
|
846
|
+
select.appendChild(option);
|
|
847
|
+
});
|
|
848
|
+
select.addEventListener('change', function () {
|
|
849
|
+
state.metric = select.value;
|
|
850
|
+
renderAll(false);
|
|
851
|
+
});
|
|
852
|
+
wrap.appendChild(select);
|
|
853
|
+
field.appendChild(node);
|
|
854
|
+
field.appendChild(wrap);
|
|
855
|
+
return field;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function selectField(ctl) {
|
|
859
|
+
var id = 'dash-filter-' + slug(ctl.key);
|
|
860
|
+
var field = el('div', 'dash-field');
|
|
861
|
+
var node = el('label');
|
|
862
|
+
node.textContent = ctl.label;
|
|
863
|
+
node.setAttribute('for', id);
|
|
864
|
+
var wrap = el('div', 'dash-select');
|
|
865
|
+
var select = document.createElement('select');
|
|
866
|
+
select.id = id;
|
|
867
|
+
var all = document.createElement('option');
|
|
868
|
+
all.value = '';
|
|
869
|
+
all.textContent = 'All';
|
|
870
|
+
select.appendChild(all);
|
|
871
|
+
ctl.values.forEach(function (value) {
|
|
872
|
+
var option = document.createElement('option');
|
|
873
|
+
option.value = value;
|
|
874
|
+
option.textContent = value;
|
|
875
|
+
if (value === ctl.value) {
|
|
876
|
+
option.selected = true;
|
|
877
|
+
}
|
|
878
|
+
select.appendChild(option);
|
|
879
|
+
});
|
|
880
|
+
select.addEventListener('change', function () {
|
|
881
|
+
ctl.value = select.value;
|
|
882
|
+
renderAll(false);
|
|
883
|
+
});
|
|
884
|
+
wrap.appendChild(select);
|
|
885
|
+
field.appendChild(node);
|
|
886
|
+
field.appendChild(wrap);
|
|
887
|
+
return field;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function numberField(ctl, side) {
|
|
891
|
+
var id = 'dash-filter-' + slug(ctl.key) + '-' + side;
|
|
892
|
+
var field = el('div', 'dash-field');
|
|
893
|
+
var node = el('label');
|
|
894
|
+
node.textContent = ctl.label + ' ' + side;
|
|
895
|
+
node.setAttribute('for', id);
|
|
896
|
+
var input = document.createElement('input');
|
|
897
|
+
input.type = 'number';
|
|
898
|
+
input.step = 'any';
|
|
899
|
+
input.id = id;
|
|
900
|
+
input.min = String(ctl.min);
|
|
901
|
+
input.max = String(ctl.max);
|
|
902
|
+
input.placeholder = String(side === 'from' ? ctl.min : ctl.max);
|
|
903
|
+
input.value = ctl[side];
|
|
904
|
+
input.addEventListener('input', function () {
|
|
905
|
+
ctl[side] = input.value;
|
|
906
|
+
renderAll(false);
|
|
907
|
+
});
|
|
908
|
+
field.appendChild(node);
|
|
909
|
+
field.appendChild(input);
|
|
910
|
+
return field;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function actionField() {
|
|
914
|
+
var field = el('div', 'dash-field dash-actions');
|
|
915
|
+
var button = el('button', 'dash-btn dash-btn--ghost');
|
|
916
|
+
button.type = 'button';
|
|
917
|
+
button.textContent = 'Reset filters';
|
|
918
|
+
button.addEventListener('click', function () {
|
|
919
|
+
state.controls.forEach(function (ctl) {
|
|
920
|
+
if (ctl.type === 'number') {
|
|
921
|
+
ctl.from = '';
|
|
922
|
+
ctl.to = '';
|
|
923
|
+
setValue('dash-filter-' + slug(ctl.key) + '-from', '');
|
|
924
|
+
setValue('dash-filter-' + slug(ctl.key) + '-to', '');
|
|
925
|
+
} else {
|
|
926
|
+
ctl.value = '';
|
|
927
|
+
setValue('dash-filter-' + slug(ctl.key), '');
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
renderAll(false);
|
|
931
|
+
});
|
|
932
|
+
field.appendChild(button);
|
|
933
|
+
return field;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ---------------------------------------------------------------- charts --
|
|
937
|
+
|
|
938
|
+
function tokens() {
|
|
939
|
+
var styles = window.getComputedStyle(document.documentElement);
|
|
940
|
+
function read(name, fallback) {
|
|
941
|
+
var value = styles.getPropertyValue(name).replace(/^\s+|\s+$/g, '');
|
|
942
|
+
return value === '' ? fallback : value;
|
|
943
|
+
}
|
|
944
|
+
var charts = [];
|
|
945
|
+
for (var i = 1; i <= 8; i += 1) {
|
|
946
|
+
charts.push(read('--chart-' + i, '#0f62fe'));
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
fg: read('--fg', '#161616'),
|
|
950
|
+
muted: read('--muted', '#525252'),
|
|
951
|
+
grid: read('--border-subtle', '#e0e0e0'),
|
|
952
|
+
axis: read('--border-strong', '#8d8d8d'),
|
|
953
|
+
accent: read('--accent', '#0f62fe'),
|
|
954
|
+
charts: charts
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function renderCharts() {
|
|
959
|
+
var host = hosts.facets;
|
|
960
|
+
if (!host) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
var width = Math.round(host.clientWidth || 0);
|
|
964
|
+
if (width < 240) {
|
|
965
|
+
width = Math.max(240, Math.round(root.clientWidth - 32));
|
|
966
|
+
}
|
|
967
|
+
lastWidth = width;
|
|
968
|
+
host.textContent = '';
|
|
969
|
+
|
|
970
|
+
var group = facetKey();
|
|
971
|
+
var axis = xKey();
|
|
972
|
+
var column = findColumn(state.metric);
|
|
973
|
+
if (!group || !axis || !column) {
|
|
974
|
+
if (hosts.caption) {
|
|
975
|
+
hosts.caption.textContent = '';
|
|
976
|
+
}
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
var groups = groupOrder();
|
|
980
|
+
if (!state.view.length || !groups.length) {
|
|
981
|
+
host.innerHTML = '<p class="dash-empty">No runs match these filters.</p>';
|
|
982
|
+
if (hosts.caption) {
|
|
983
|
+
hosts.caption.textContent = '';
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
var theme = tokens();
|
|
989
|
+
var split = seriesKey();
|
|
990
|
+
var seriesPresent = split ? distinct(state.view, split) : [];
|
|
991
|
+
var seriesList = split
|
|
992
|
+
? distinct(state.rows, split)
|
|
993
|
+
.filter(function (value) {
|
|
994
|
+
return seriesPresent.indexOf(value) !== -1;
|
|
995
|
+
})
|
|
996
|
+
// Eight marks is the whole palette; a ninth series would repeat a colour.
|
|
997
|
+
.slice(0, 8)
|
|
998
|
+
: [''];
|
|
999
|
+
var many = seriesList.length > 1;
|
|
1000
|
+
var colors = seriesList.map(function (value, index) {
|
|
1001
|
+
return many ? theme.charts[index % theme.charts.length] : theme.accent;
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
var xColumn = findColumn(axis);
|
|
1005
|
+
var xValues = distinct(state.view, axis);
|
|
1006
|
+
if (xColumn && (xColumn.type === 'number' || xColumn.type === 'date')) {
|
|
1007
|
+
xValues.sort(function (a, b) {
|
|
1008
|
+
if (xColumn.type === 'number') {
|
|
1009
|
+
return Number(a) - Number(b);
|
|
1010
|
+
}
|
|
1011
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
var bars = !many && xColumn && xColumn.type !== 'number';
|
|
1015
|
+
|
|
1016
|
+
var buckets = {};
|
|
1017
|
+
state.view.forEach(function (row) {
|
|
1018
|
+
if (!isNumeric(row[state.metric])) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
var id = String(row[group]) + SEP + (split ? String(row[split]) : '') + SEP + String(row[axis]);
|
|
1022
|
+
if (!buckets[id]) {
|
|
1023
|
+
buckets[id] = [];
|
|
1024
|
+
}
|
|
1025
|
+
buckets[id].push(Number(row[state.metric]));
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
var lo = 0;
|
|
1029
|
+
var hi = 0;
|
|
1030
|
+
Object.keys(buckets).forEach(function (id) {
|
|
1031
|
+
buckets[id].forEach(function (value) {
|
|
1032
|
+
if (value > hi) {
|
|
1033
|
+
hi = value;
|
|
1034
|
+
}
|
|
1035
|
+
if (value < lo) {
|
|
1036
|
+
lo = value;
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
if (!(hi > lo)) {
|
|
1041
|
+
hi = lo + 1;
|
|
1042
|
+
}
|
|
1043
|
+
hi = niceCeil(hi);
|
|
1044
|
+
if (lo < 0) {
|
|
1045
|
+
lo = -niceCeil(-lo);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
var cols = width >= 940 ? 3 : width >= 620 ? 2 : 1;
|
|
1049
|
+
if (cols > groups.length) {
|
|
1050
|
+
cols = groups.length;
|
|
1051
|
+
}
|
|
1052
|
+
var gapX = 24;
|
|
1053
|
+
var gapY = 26;
|
|
1054
|
+
var padL = 44;
|
|
1055
|
+
var padR = many ? 64 : 32;
|
|
1056
|
+
var padT = 26;
|
|
1057
|
+
var padB = 24;
|
|
1058
|
+
var panelH = 152;
|
|
1059
|
+
var panelW = Math.floor((width - gapX * (cols - 1)) / cols);
|
|
1060
|
+
var plotW = Math.max(60, panelW - padL - padR);
|
|
1061
|
+
var plotH = panelH - padT - padB;
|
|
1062
|
+
var rowsDown = Math.ceil(groups.length / cols);
|
|
1063
|
+
var height = rowsDown * panelH + (rowsDown - 1) * gapY;
|
|
1064
|
+
var span = hi - lo;
|
|
1065
|
+
|
|
1066
|
+
function xPos(index) {
|
|
1067
|
+
if (xValues.length <= 1) {
|
|
1068
|
+
return plotW / 2;
|
|
1069
|
+
}
|
|
1070
|
+
if (xColumn && xColumn.type === 'number') {
|
|
1071
|
+
var first = Number(xValues[0]);
|
|
1072
|
+
var last = Number(xValues[xValues.length - 1]);
|
|
1073
|
+
if (!(last > first)) {
|
|
1074
|
+
return plotW / 2;
|
|
1075
|
+
}
|
|
1076
|
+
return plotW * (0.06 + (0.88 * (Number(xValues[index]) - first)) / (last - first));
|
|
1077
|
+
}
|
|
1078
|
+
return (plotW * (index + 0.5)) / xValues.length;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function yPos(value) {
|
|
1082
|
+
return plotH - (plotH * (value - lo)) / span;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
var ticks = [lo, lo + span / 2, hi];
|
|
1086
|
+
var parts = [];
|
|
1087
|
+
var names = groups.map(function (value) {
|
|
1088
|
+
return value;
|
|
1089
|
+
});
|
|
1090
|
+
var summary =
|
|
1091
|
+
'Small multiples: ' +
|
|
1092
|
+
metricTitle() +
|
|
1093
|
+
' against ' +
|
|
1094
|
+
findColumn(axis).label.toLowerCase() +
|
|
1095
|
+
', one panel per ' +
|
|
1096
|
+
findColumn(group).label.toLowerCase() +
|
|
1097
|
+
', with the runs listed in the table below.';
|
|
1098
|
+
|
|
1099
|
+
parts.push(
|
|
1100
|
+
'<svg viewBox="0 0 ' + width + ' ' + height + '" role="img" aria-label="' + esc(summary) + '">'
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
groups.forEach(function (value, index) {
|
|
1104
|
+
var gx = (index % cols) * (panelW + gapX);
|
|
1105
|
+
var gy = Math.floor(index / cols) * (panelH + gapY);
|
|
1106
|
+
var ox = gx + padL;
|
|
1107
|
+
var oy = gy + padT;
|
|
1108
|
+
var labels = [];
|
|
1109
|
+
|
|
1110
|
+
parts.push(
|
|
1111
|
+
'<text x="' + ox + '" y="' + (gy + 15) + '" fill="' + theme.fg + '" font-size="13" font-weight="600">' + esc(value) + '</text>'
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
ticks.forEach(function (tick) {
|
|
1115
|
+
var y = oy + yPos(tick);
|
|
1116
|
+
parts.push(
|
|
1117
|
+
'<line x1="' + ox + '" y1="' + round(y) + '" x2="' + (ox + plotW) + '" y2="' + round(y) + '" stroke="' + (tick === lo ? theme.axis : theme.grid) + '" stroke-width="1"></line>'
|
|
1118
|
+
);
|
|
1119
|
+
if (index % cols === 0) {
|
|
1120
|
+
parts.push(
|
|
1121
|
+
'<text x="' + (ox - 8) + '" y="' + round(y + 4) + '" text-anchor="end" fill="' + theme.muted + '" font-size="11">' + esc(fmtTick(tick)) + '</text>'
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
if (Math.floor(index / cols) === rowsDown - 1) {
|
|
1127
|
+
xValues.forEach(function (tick, position) {
|
|
1128
|
+
parts.push(
|
|
1129
|
+
'<text x="' + round(ox + xPos(position)) + '" y="' + (gy + panelH - 10) + '" text-anchor="middle" fill="' + theme.muted + '" font-size="11">' + esc(tick) + '</text>'
|
|
1130
|
+
);
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
seriesList.forEach(function (series, seriesIndex) {
|
|
1135
|
+
var points = [];
|
|
1136
|
+
xValues.forEach(function (tick, position) {
|
|
1137
|
+
var id = value + SEP + (split ? series : '') + SEP + tick;
|
|
1138
|
+
if (!buckets[id]) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
var number = quantile(buckets[id], 0.5);
|
|
1142
|
+
points.push({ x: xPos(position), y: yPos(number), value: number });
|
|
1143
|
+
});
|
|
1144
|
+
if (!points.length) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
var color = colors[seriesIndex];
|
|
1148
|
+
|
|
1149
|
+
if (bars) {
|
|
1150
|
+
var band = plotW / xValues.length;
|
|
1151
|
+
var barW = Math.max(6, Math.min(30, band * 0.55));
|
|
1152
|
+
points.forEach(function (point) {
|
|
1153
|
+
var top = oy + point.y;
|
|
1154
|
+
var base = oy + yPos(Math.max(lo, 0));
|
|
1155
|
+
parts.push(
|
|
1156
|
+
'<rect x="' + round(ox + point.x - barW / 2) + '" y="' + round(top) + '" width="' + round(barW) + '" height="' + round(Math.max(1, base - top)) + '" fill="' + color + '"></rect>'
|
|
1157
|
+
);
|
|
1158
|
+
});
|
|
1159
|
+
} else {
|
|
1160
|
+
var path = points
|
|
1161
|
+
.map(function (point, position) {
|
|
1162
|
+
return (position === 0 ? 'M' : 'L') + round(ox + point.x) + ' ' + round(oy + point.y);
|
|
1163
|
+
})
|
|
1164
|
+
.join(' ');
|
|
1165
|
+
parts.push('<path d="' + path + '" fill="none" stroke="' + color + '" stroke-width="2"></path>');
|
|
1166
|
+
if (points.length <= 24) {
|
|
1167
|
+
points.forEach(function (point) {
|
|
1168
|
+
parts.push(
|
|
1169
|
+
'<circle cx="' + round(ox + point.x) + '" cy="' + round(oy + point.y) + '" r="2.5" fill="' + color + '"></circle>'
|
|
1170
|
+
);
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
var last = points[points.length - 1];
|
|
1176
|
+
if (many && index === 0) {
|
|
1177
|
+
labels.push({ x: ox + last.x + 8, y: oy + last.y + 4, text: series });
|
|
1178
|
+
} else if (!many) {
|
|
1179
|
+
labels.push({ x: ox + last.x + 8, y: oy + last.y + 4, text: fmt(last.value), color: theme.fg });
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
labels.sort(function (a, b) {
|
|
1184
|
+
return a.y - b.y;
|
|
1185
|
+
});
|
|
1186
|
+
for (var pass = 1; pass < labels.length; pass += 1) {
|
|
1187
|
+
if (labels[pass].y - labels[pass - 1].y < 13) {
|
|
1188
|
+
labels[pass].y = labels[pass - 1].y + 13;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
labels.forEach(function (item) {
|
|
1192
|
+
parts.push(
|
|
1193
|
+
'<text x="' + round(item.x) + '" y="' + round(item.y) + '" fill="' + theme.fg + '" font-size="11">' + esc(item.text) + '</text>'
|
|
1194
|
+
);
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
parts.push('</svg>');
|
|
1199
|
+
host.innerHTML = parts.join('');
|
|
1200
|
+
|
|
1201
|
+
if (hosts.caption) {
|
|
1202
|
+
hosts.caption.textContent =
|
|
1203
|
+
metricTitle() +
|
|
1204
|
+
' against ' +
|
|
1205
|
+
findColumn(axis).label.toLowerCase() +
|
|
1206
|
+
', one panel per ' +
|
|
1207
|
+
findColumn(group).label.toLowerCase() +
|
|
1208
|
+
(many
|
|
1209
|
+
? ', one line per ' + findColumn(split).label.toLowerCase() + ', labelled at the end of the first panel.'
|
|
1210
|
+
: ', with the last value labelled.') +
|
|
1211
|
+
' ' +
|
|
1212
|
+
names.length +
|
|
1213
|
+
' panels, one shared vertical scale.';
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// -------------------------------------------------------------- download --
|
|
1218
|
+
|
|
1219
|
+
function csvCell(value) {
|
|
1220
|
+
var text = value === undefined || value === null ? '' : String(value);
|
|
1221
|
+
if (/[",\r\n]/.test(text)) {
|
|
1222
|
+
return '"' + text.replace(/"/g, '""') + '"';
|
|
1223
|
+
}
|
|
1224
|
+
return text;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function download() {
|
|
1228
|
+
var keys = state.columns.map(function (column) {
|
|
1229
|
+
return column.key;
|
|
1230
|
+
});
|
|
1231
|
+
var rows = sortedView();
|
|
1232
|
+
var lines = [keys.map(csvCell).join(',')];
|
|
1233
|
+
rows.forEach(function (row) {
|
|
1234
|
+
lines.push(
|
|
1235
|
+
keys
|
|
1236
|
+
.map(function (key) {
|
|
1237
|
+
return csvCell(row[key]);
|
|
1238
|
+
})
|
|
1239
|
+
.join(',')
|
|
1240
|
+
);
|
|
1241
|
+
});
|
|
1242
|
+
var blob = new Blob([lines.join('\r\n') + '\r\n'], { type: 'text/csv;charset=utf-8' });
|
|
1243
|
+
var url = URL.createObjectURL(blob);
|
|
1244
|
+
var link = document.createElement('a');
|
|
1245
|
+
link.href = url;
|
|
1246
|
+
link.download = 'dashboard-view.csv';
|
|
1247
|
+
document.body.appendChild(link);
|
|
1248
|
+
link.click();
|
|
1249
|
+
document.body.removeChild(link);
|
|
1250
|
+
window.setTimeout(function () {
|
|
1251
|
+
URL.revokeObjectURL(url);
|
|
1252
|
+
}, 2000);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// ----------------------------------------------------------------- load --
|
|
1256
|
+
|
|
1257
|
+
function setRows(rows, source) {
|
|
1258
|
+
state.rows = rows;
|
|
1259
|
+
state.source = source;
|
|
1260
|
+
state.columns = columnsFrom(rows).map(function (key) {
|
|
1261
|
+
return { key: key, label: label(key), type: inferType(key, rows) };
|
|
1262
|
+
});
|
|
1263
|
+
buildControls();
|
|
1264
|
+
applyDefaults();
|
|
1265
|
+
readHash();
|
|
1266
|
+
renderAll(true);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function applyText(text, source) {
|
|
1270
|
+
if (/\.json$/i.test(source) || /^\s*[[{]/.test(text)) {
|
|
1271
|
+
var data;
|
|
1272
|
+
try {
|
|
1273
|
+
data = JSON.parse(text);
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
fail('The JSON in ' + source + ' does not parse: ' + err.message);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
var rows = Array.isArray(data) ? data : data && Array.isArray(data.rows) ? data.rows : null;
|
|
1279
|
+
if (!rows) {
|
|
1280
|
+
fail('The JSON in ' + source + ' needs an array of rows or an object with a rows array.');
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
setRows(rows, source);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
var parsed = parseCsv(text);
|
|
1287
|
+
if (!parsed.length) {
|
|
1288
|
+
fail('No rows found in ' + source + '.');
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
setRows(parsed, source);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function fail(message) {
|
|
1295
|
+
renderStatus(message);
|
|
1296
|
+
if (hosts.facets) {
|
|
1297
|
+
hosts.facets.innerHTML = '<p class="dash-empty">' + esc(message) + '</p>';
|
|
1298
|
+
}
|
|
1299
|
+
if (hosts.table) {
|
|
1300
|
+
hosts.table.innerHTML = '';
|
|
1301
|
+
}
|
|
1302
|
+
if (hosts.summary) {
|
|
1303
|
+
hosts.summary.innerHTML = '';
|
|
1304
|
+
}
|
|
1305
|
+
if (hosts.source) {
|
|
1306
|
+
hosts.source.textContent = config.src ? 'Data source: ' + config.src : 'No data source.';
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function isRelative(url) {
|
|
1311
|
+
return !/^(?:[a-z][a-z0-9+.\-]*:|\/\/|\/)/i.test(url);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function boot() {
|
|
1315
|
+
var inline = document.getElementById('data');
|
|
1316
|
+
if (inline && /json/i.test(inline.getAttribute('type') || '')) {
|
|
1317
|
+
applyText(inline.textContent, 'the inline data block');
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (config.src === '') {
|
|
1321
|
+
fail('No data: add data-src to .dashboard or a script with id="data".');
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (!isRelative(config.src)) {
|
|
1325
|
+
fail('data-src must be a relative path in this folder: ' + config.src);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
window
|
|
1329
|
+
.fetch(config.src, { credentials: 'same-origin' })
|
|
1330
|
+
.then(function (response) {
|
|
1331
|
+
if (!response.ok) {
|
|
1332
|
+
throw new Error('HTTP ' + response.status);
|
|
1333
|
+
}
|
|
1334
|
+
return response.text();
|
|
1335
|
+
})
|
|
1336
|
+
.then(function (text) {
|
|
1337
|
+
applyText(text, config.src);
|
|
1338
|
+
})
|
|
1339
|
+
.catch(function (err) {
|
|
1340
|
+
fail('Could not read ' + config.src + ': ' + err.message);
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// ---------------------------------------------------------------- events --
|
|
1345
|
+
|
|
1346
|
+
function redraw() {
|
|
1347
|
+
if (state.rows.length) {
|
|
1348
|
+
renderCharts();
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
document.addEventListener('rshowmodechange', redraw);
|
|
1353
|
+
|
|
1354
|
+
if (hosts.filters) {
|
|
1355
|
+
hosts.filters.addEventListener('submit', function (event) {
|
|
1356
|
+
event.preventDefault();
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
if (window.matchMedia) {
|
|
1361
|
+
var scheme = window.matchMedia('(prefers-color-scheme: dark)');
|
|
1362
|
+
if (scheme.addEventListener) {
|
|
1363
|
+
scheme.addEventListener('change', redraw);
|
|
1364
|
+
} else if (scheme.addListener) {
|
|
1365
|
+
scheme.addListener(redraw);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
var resizeTimer = 0;
|
|
1370
|
+
window.addEventListener('resize', function () {
|
|
1371
|
+
window.clearTimeout(resizeTimer);
|
|
1372
|
+
resizeTimer = window.setTimeout(function () {
|
|
1373
|
+
var width = hosts.facets ? hosts.facets.clientWidth : 0;
|
|
1374
|
+
if (Math.abs(width - lastWidth) > 16) {
|
|
1375
|
+
redraw();
|
|
1376
|
+
}
|
|
1377
|
+
}, 160);
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
window.addEventListener('hashchange', function () {
|
|
1381
|
+
if (!state.rows.length) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
applyDefaults();
|
|
1385
|
+
readHash();
|
|
1386
|
+
renderAll(true);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
boot();
|
|
1390
|
+
}());
|