@vernikr/size-report 2.7.0 → 2.8.0
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/README.md +65 -44
- package/package.json +2 -2
- package/src/css.js +2 -2
- package/src/locales.js +0 -6
- package/src/page/app.css +7 -29
- package/src/page/app.js +29 -57
- package/src/page/build.js +6 -9
- package/src/page/table.js +221 -294
- package/src/table.css +89 -57
- package/src/page/work.js +0 -55
package/src/page/table.js
CHANGED
|
@@ -1,51 +1,88 @@
|
|
|
1
|
-
import { cellParts, commitParts,
|
|
1
|
+
import { cellParts, commitParts, nowModel, rowModel, valueParts } from '../derived.js';
|
|
2
2
|
import { appEl } from './dom.js';
|
|
3
3
|
import { appData, appUi, appView } from './state.js';
|
|
4
4
|
|
|
5
|
-
/* The table
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Hence nothing below builds a node after the first drawing — a click writes a class and a number.
|
|
5
|
+
/* The table: a grid of plain elements (`<div>`), of which only the part the reader looks at is built. The file's
|
|
6
|
+
* length is the report's, but the price of a report is paid by whoever opens it, so the table is no longer a
|
|
7
|
+
* `<table>` with every cell of every column in it.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* **Why not a `<table>`.** Measured on this repository's own report (238 500 cells = 370 758 nodes, a table 71 712 ×
|
|
10
|
+
* 5 982 px): about 1.4 GB of a browser's memory, of which roughly half the nodes and half the painted area, and a
|
|
11
|
+
* browser's relayout of it costs close to a second on any switch (`probes/step-12-columns.mjs`). `content-visibility:
|
|
12
|
+
* auto`, the cheap way out, is ignored on a table row by Chrome 153 (`probes/step-10-tables.mjs`), so the answer is
|
|
13
|
+
* to build less rather than to promise the browser will skip it. A grid of `position: absolute` rows has no layout to
|
|
14
|
+
* be redone: a row is placed by its `top`, a column by the `left` of the group of cells that starts it, and the
|
|
15
|
+
* browser never measures a cell to decide a width — every column is `--col` wide (70px), which is what the numbers
|
|
16
|
+
* need and no more (the counted widths this step replaced were 47–70px).
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* **Why not a library.** A virtualizer for two axes is not a solved problem for a page like this one — measured from
|
|
19
|
+
* the tarballs, `@tanstack/virtual-core` is ~6.7 kB gzip and headless (the rows and columns are two virtualizers and
|
|
20
|
+
* every node is yours to write), `virtua` (~6.1 kB) calls its grid `experimental_VGrid` and has no sticky pieces,
|
|
21
|
+
* `Clusterize.js` virtualizes rows from a string of all of them and knows nothing of columns. All three would have to
|
|
22
|
+
* be vendored into the artifact — the page is one file, opens from disk and resolves no import — and the report
|
|
23
|
+
* measures its own bytes, so the library would be measured by the very tool it is inside. What is left to write
|
|
24
|
+
* after any of them is what this file is: the window, the cells, the header and the pinned column.
|
|
25
|
+
*
|
|
26
|
+
* **What is built.** The window is the rows and the columns the shell shows, plus `APP_OVER` beyond each edge: what
|
|
27
|
+
* the reader is about to reach is already there, so the edge of the window is never seen empty. Scrolling costs no
|
|
28
|
+
* JavaScript, the rows sit in the scrolled content at their own `top`; the script works only when the window has
|
|
29
|
+
* really moved, and then only on what left it and what entered it (measured on the prototype: 0.9 ms a step down the
|
|
30
|
+
* table, against 6.5 ms for building the whole window again). The header sticks to the shell's top and the commit
|
|
31
|
+
* column to its left (both `position: sticky`), so the row and the column a number belongs to are always in sight.
|
|
32
|
+
*
|
|
33
|
+
* The choice is applied by building the window again: the columns of a switched-off file are simply not among the
|
|
34
|
+
* columns that are built, so there is nothing to hide and nothing to recount — the totals are the sum over the files
|
|
35
|
+
* that are on, counted by `rowModel` for the rows the window holds (`src/derived.js`, one place for that
|
|
36
|
+
* arithmetic).
|
|
17
37
|
*/
|
|
18
38
|
|
|
19
|
-
/* The
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
/* The geometry in pixels: written here and read by the styling (`src/table.css`), which is one copy too many — hence
|
|
40
|
+
* `test/page-grid.test.js` holds the two together, and the report says the same numbers in its journal. */
|
|
41
|
+
export const APP_COL = 70;
|
|
42
|
+
export const APP_ROW = 25;
|
|
43
|
+
export const APP_HEAD = 44;
|
|
44
|
+
|
|
45
|
+
/* How much more than the visible window is built, in rows and in columns. A window that ends exactly at the edge of
|
|
46
|
+
* the shell shows an empty band while the browser scrolls a notch; four rows and four columns of slack are cheaper
|
|
47
|
+
* than that band, and they are what makes a scroll with the wheel or the trackpad look like a scroll. */
|
|
48
|
+
export const APP_OVER = 4;
|
|
25
49
|
|
|
26
|
-
/*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
/* The window of a shell that has no size: jsdom lays nothing out, and a shell with no metrics is hidden. A page is
|
|
51
|
+
* not written for that case, but its checks are: without these figures a check would read an empty table and say
|
|
52
|
+
* nothing rather than say it about the right thing. */
|
|
53
|
+
export const APP_MIN_ROWS = 24;
|
|
54
|
+
export const APP_MIN_COLS = 10;
|
|
55
|
+
|
|
56
|
+
/* The order of the columns: the files the last commit touched come first — the report is rebuilt after every
|
|
57
|
+
* commit, and a reader's first question is what that edit brought. Inside each part the order is the settings', and
|
|
58
|
+
* it depends on the files rather than on the choice: that is what lets a column be switched off without moving the
|
|
59
|
+
* others. */
|
|
60
|
+
export function appOrder() {
|
|
61
|
+
const files = [];
|
|
62
|
+
appData.files.forEach((_f, i) => files.push(i));
|
|
63
|
+
files.sort((a, b) => (appData.last[a] === true ? 0 : 1) - (appData.last[b] === true ? 0 : 1));
|
|
64
|
+
return files;
|
|
33
65
|
}
|
|
34
66
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
67
|
+
/* The files whose columns are built, in the order of the columns: what is switched off is not among them, which is
|
|
68
|
+
* the whole of what a switch changes about the grid. */
|
|
69
|
+
function appList() {
|
|
70
|
+
const out = [];
|
|
71
|
+
appOrder().forEach((i) => { if (appView.files[i] === true) out.push(i); });
|
|
72
|
+
return out;
|
|
39
73
|
}
|
|
40
74
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
/* A number with its sign: the rules of a cell's content live in `cellParts` and `valueParts`, only the node is here —
|
|
76
|
+
* and its class, which carries the alignment (`num`), the group's left edge (`g`), the gap (`miss`) and the colour of
|
|
77
|
+
* the change (`up`/`down`). The colour stands on the cell itself rather than on a child of it: a report of this
|
|
78
|
+
* repository's size has a quarter of a million of these numbers, and one node instead of two is half of the window. */
|
|
79
|
+
function appNum(parts, cls) {
|
|
80
|
+
return appEl('span', cls + (parts.miss ? ' miss' : '') + (parts.dir === null ? '' : ' ' + parts.dir), parts.text);
|
|
45
81
|
}
|
|
46
82
|
|
|
47
|
-
/* A commit's caption: the date, the subject, the journal section's mark. The column
|
|
48
|
-
*
|
|
83
|
+
/* A commit's caption: the date, the subject, the journal section's mark. The column has a fixed width, so the
|
|
84
|
+
* caption is clipped with an ellipsis rather than wrapped (`.clip` in the styling), while the whole subject stands
|
|
85
|
+
* in the tooltip. */
|
|
49
86
|
export function appCommit(row) {
|
|
50
87
|
const parts = commitParts(row, appData.report.showSha, row.href);
|
|
51
88
|
const name = parts.href ? appEl('a', 'subj', parts.subject) : appEl('span', 'subj', parts.subject);
|
|
@@ -58,14 +95,12 @@ export function appCommit(row) {
|
|
|
58
95
|
clip.appendChild(appEl('span', 'when', parts.when));
|
|
59
96
|
clip.appendChild(name);
|
|
60
97
|
clip.appendChild(mark);
|
|
61
|
-
|
|
62
|
-
th.appendChild(clip);
|
|
63
|
-
return th;
|
|
98
|
+
return clip;
|
|
64
99
|
}
|
|
65
100
|
|
|
66
|
-
/* The empty states: when there will be no numbers at all, the page says so in words rather than showing a grid
|
|
67
|
-
* columns. Every file can be switched off — then the total volume remains, and the note explains why there
|
|
68
|
-
* columns. The
|
|
101
|
+
/* The empty states: when there will be no numbers at all, the page says so in words rather than showing a grid
|
|
102
|
+
* without columns. Every file can be switched off — then the total volume remains, and the note explains why there
|
|
103
|
+
* are no columns. The shell stays where it is either way: this is about what is shown, not about what exists. */
|
|
69
104
|
export function appState(metricsCount, filesCount) {
|
|
70
105
|
const state = document.getElementById('state');
|
|
71
106
|
const text = metricsCount === 0 ? appUi.empty : (filesCount === 0 ? appUi.noFiles : '');
|
|
@@ -74,276 +109,168 @@ export function appState(metricsCount, filesCount) {
|
|
|
74
109
|
document.getElementById('shell').hidden = metricsCount === 0;
|
|
75
110
|
}
|
|
76
111
|
|
|
77
|
-
/*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
* headings of each file's column are collected in the cache: they are the nodes a click has to touch. */
|
|
91
|
-
export function appHead(files, metrics, cache) {
|
|
92
|
-
const head = appEl('tr');
|
|
93
|
-
const commit = appEl('th', 'c-commit', appUi.commit);
|
|
94
|
-
commit.rowSpan = 2;
|
|
95
|
-
head.appendChild(commit);
|
|
96
|
-
const subs = appEl('tr');
|
|
97
|
-
const group = (label, i) => {
|
|
98
|
-
const th = appEl('th', 'gh', label);
|
|
99
|
-
th.colSpan = metrics.length;
|
|
100
|
-
head.appendChild(th);
|
|
101
|
-
cache.spans.push(th);
|
|
102
|
-
/* The group's own heading belongs to the column: hiding a file has to take its caption with it, or the header
|
|
103
|
-
* would keep a name over numbers that are gone. */
|
|
104
|
-
if (i !== null) cache.cols[i].push(th);
|
|
105
|
-
metrics.forEach((_key, mi) => {
|
|
106
|
-
const cell = appEl('th', 'g m' + mi, appData.metrics[mi].label);
|
|
107
|
-
subs.appendChild(cell);
|
|
108
|
-
if (i !== null) cache.cols[i].push(cell);
|
|
109
|
-
});
|
|
110
|
-
};
|
|
111
|
-
group(appUi.total, null);
|
|
112
|
-
files.forEach((i) => {
|
|
113
|
-
cache.cols[i] = [];
|
|
114
|
-
group(appData.files[i].label, i);
|
|
115
|
-
});
|
|
116
|
-
const thead = appEl('thead');
|
|
117
|
-
thead.appendChild(head);
|
|
118
|
-
thead.appendChild(subs);
|
|
119
|
-
return thead;
|
|
112
|
+
/* One cell of the window: the column decides what it holds. The group is the column's ordinal divided by the number of
|
|
113
|
+
* metrics — `0` is the total over the files, `g ≥ 1` is the g-th column of the order — and it is the same arithmetic
|
|
114
|
+
* the header is placed by, so a column and its caption cannot drift apart. The model lists the *enabled* files in the
|
|
115
|
+
* order of the data (that is what its mask means), while the columns stand in the order of the settings, which is why
|
|
116
|
+
* the ordinal of the column is turned into the ordinal of the model by `slot` (counted once per window). */
|
|
117
|
+
function appCell(model, c, now, cache) {
|
|
118
|
+
const mi = c % cache.keys.length;
|
|
119
|
+
const group = (c - mi) / cache.keys.length;
|
|
120
|
+
const cls = 'num' + (mi === 0 ? ' g' : '');
|
|
121
|
+
const at = group === 0 ? null : cache.slot[group - 1];
|
|
122
|
+
const cell = at === null ? model.total[mi] : (model.files[at] || [])[mi];
|
|
123
|
+
if (cell === undefined) return appEl('span', cls);
|
|
124
|
+
return appNum(now ? valueParts(cell) : cellParts(cell.value, cell.delta, '−'), cls);
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
/* A
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
/* A row of the window: the caption of the commit and the numbers of the columns the window holds. The top row is the
|
|
128
|
+
* state at HEAD (`nowModel`) rather than the last commit's cells: an absolute number stands in the table once, and
|
|
129
|
+
* the deltas below it add up to it. */
|
|
130
|
+
function appRow(cache, r, span) {
|
|
131
|
+
const last = appData.rows.length;
|
|
132
|
+
const now = r === 0;
|
|
133
|
+
const i = last - r;
|
|
134
|
+
const model = now
|
|
135
|
+
? nowModel(appData.now, cache.keys, appView.files)
|
|
136
|
+
: rowModel(appData.rows[i].values, i === 0 ? null : appData.rows[i - 1].values, cache.keys, appView.files);
|
|
137
|
+
const row = appEl('div', 'row' + (now ? ' now' : ''));
|
|
138
|
+
row.style.top = (APP_HEAD + r * APP_ROW) + 'px';
|
|
139
|
+
const commit = appEl('div', 'c-commit');
|
|
140
|
+
if (now) commit.textContent = appUi.now;
|
|
141
|
+
else commit.appendChild(appCommit(appData.rows[i]));
|
|
142
|
+
row.appendChild(commit);
|
|
143
|
+
const cells = appEl('div', 'cells');
|
|
144
|
+
cells.style.left = (span.c0 * APP_COL) + 'px';
|
|
145
|
+
for (let c = span.c0; c <= span.c1; c++) cells.appendChild(appCell(model, c, now, cache));
|
|
146
|
+
row.appendChild(cells);
|
|
147
|
+
return row;
|
|
132
148
|
}
|
|
133
149
|
|
|
134
|
-
/*
|
|
135
|
-
*
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const delta = (cell) => appBreadth(cellParts(cell.value, cell.delta, '−').text);
|
|
142
|
-
const value = (v) => appBreadth(valueParts(v).text);
|
|
143
|
-
appData.rows.forEach((row, r) => {
|
|
144
|
-
const model = rowModel(row.values, r === 0 ? null : appData.rows[r - 1].values, keys);
|
|
145
|
-
model.total.forEach((cell, mi) => put(total, mi, delta(cell)));
|
|
146
|
-
files.forEach((i) => model.files[i].forEach((cell, mi) => put(wide[i], mi, delta(cell))));
|
|
147
|
-
});
|
|
148
|
-
const now = nowModel(appData.now, keys);
|
|
149
|
-
now.total.forEach((v, mi) => put(total, mi, value(v)));
|
|
150
|
-
files.forEach((i) => now.files[i].forEach((v, mi) => put(wide[i], mi, value(v))));
|
|
151
|
-
keys.forEach((_key, mi) => {
|
|
152
|
-
const caption = appBreadth(appData.metrics[mi].label);
|
|
153
|
-
put(total, mi, caption);
|
|
154
|
-
wide.forEach((per) => put(per, mi, caption));
|
|
155
|
-
});
|
|
156
|
-
return { total: total, files: wide };
|
|
150
|
+
/* A built row into the window: the map is what says which rows are in the markup, so a row goes into it where it is
|
|
151
|
+
* made — otherwise a window that moved would build its rows beside the ones that are still there. */
|
|
152
|
+
function appPlace(cache, r, span) {
|
|
153
|
+
const row = appRow(cache, r, span);
|
|
154
|
+
cache.rows.set(r, row);
|
|
155
|
+
cache.grid.appendChild(row);
|
|
156
|
+
return row;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
/* A
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return
|
|
159
|
+
/* A caption of the header: a file's name or a metric's. The room is a fixed number of columns, so a name that does
|
|
160
|
+
* not fit is cut with an ellipsis (the styling) rather than wrapped — the header is one line high, and the whole name
|
|
161
|
+
* is reachable in the tooltip. */
|
|
162
|
+
function appCaption(text, cls, span) {
|
|
163
|
+
const el = appEl('span', cls, text);
|
|
164
|
+
if (span > 1) el.style.gridColumn = 'span ' + span;
|
|
165
|
+
el.title = text;
|
|
166
|
+
return el;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
/* The
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.
|
|
186
|
-
|
|
169
|
+
/* The header: the total and the enabled files over their columns, and one caption per metric column under them. It
|
|
170
|
+
* holds the window's columns alone and is built again only when the window moves sideways — scrolling down leaves it
|
|
171
|
+
* untouched, and the two rows of it are placed by `grid-column`, so a group of metrics is a group in the grid. */
|
|
172
|
+
function appHead(cache, span) {
|
|
173
|
+
const count = cache.keys.length;
|
|
174
|
+
const g0 = Math.floor(span.c0 / count);
|
|
175
|
+
const g1 = Math.floor(span.c1 / count);
|
|
176
|
+
const left = (g0 * count * APP_COL) + 'px';
|
|
177
|
+
const head = appEl('div', 'head');
|
|
178
|
+
head.appendChild(appEl('div', 'c-commit', appUi.commit));
|
|
179
|
+
const groups = appEl('div', 'hgroups');
|
|
180
|
+
groups.style.left = left;
|
|
181
|
+
const metrics = appEl('div', 'hmetrics');
|
|
182
|
+
metrics.style.left = left;
|
|
183
|
+
for (let g = g0; g <= g1; g++) {
|
|
184
|
+
const label = g === 0 ? appUi.total : appData.files[cache.list[g - 1]].label;
|
|
185
|
+
groups.appendChild(appCaption(label, 'gh', count));
|
|
186
|
+
for (let mi = 0; mi < count; mi++) {
|
|
187
|
+
metrics.appendChild(appCaption(cache.labels[mi], mi === 0 ? 'g' : '', 1));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
head.appendChild(groups);
|
|
191
|
+
head.appendChild(metrics);
|
|
192
|
+
return head;
|
|
187
193
|
}
|
|
188
194
|
|
|
189
|
-
/*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
195
|
+
/* The window the shell shows: the rows and the columns, in the ordinals of the whole grid, and the size of that
|
|
196
|
+
* grid. The rows are counted from the top of the content, under the header: the header is stuck to the top of the
|
|
197
|
+
* shell and covers the first rows of the content, hence the offset at both ends of the window. */
|
|
198
|
+
export function appSpan(cache) {
|
|
199
|
+
const shell = cache.shell;
|
|
200
|
+
/* The metrics and the files of the window are read from the view here rather than kept: a switch changes them, and
|
|
201
|
+
* what is built has to be the choice as it is now — the keys, their captions and the ordinals of both below. */
|
|
202
|
+
cache.keys = [];
|
|
203
|
+
cache.labels = [];
|
|
204
|
+
appData.metrics.forEach((m) => {
|
|
205
|
+
if (appView.metrics[m.key] === true) { cache.keys.push(m.key); cache.labels.push(m.label); }
|
|
199
206
|
});
|
|
207
|
+
const count = cache.keys.length;
|
|
208
|
+
cache.list = appList();
|
|
209
|
+
/* The ordinals of the model: `rowModel` lists the enabled files in the order of the data, the columns stand in the
|
|
210
|
+
* order of the settings, and `rank` is the bridge between the two — counted once per window, asked per cell. */
|
|
211
|
+
let rank = 0;
|
|
212
|
+
cache.rank = [];
|
|
213
|
+
appData.files.forEach((_f, i) => { cache.rank[i] = appView.files[i] === true ? rank++ : -1; });
|
|
214
|
+
cache.slot = cache.list.map((i) => cache.rank[i]);
|
|
215
|
+
const cols = count * (cache.list.length + 1);
|
|
216
|
+
const rows = appData.rows.length + 1;
|
|
217
|
+
cache.grid.style.width = (cols * APP_COL) + 'px';
|
|
218
|
+
cache.grid.style.height = (APP_HEAD + rows * APP_ROW) + 'px';
|
|
219
|
+
if (count === 0) return { r0: 0, r1: -1, c0: 0, c1: -1 };
|
|
220
|
+
const high = shell.clientHeight || APP_MIN_ROWS * APP_ROW;
|
|
221
|
+
const wide = shell.clientWidth || APP_MIN_COLS * APP_COL;
|
|
222
|
+
const top = shell.scrollTop + APP_HEAD;
|
|
223
|
+
return {
|
|
224
|
+
r0: Math.max(0, Math.floor(top / APP_ROW) - APP_OVER),
|
|
225
|
+
r1: Math.min(rows - 1, Math.floor((top + high) / APP_ROW) + APP_OVER),
|
|
226
|
+
c0: Math.max(0, Math.floor(shell.scrollLeft / APP_COL) - APP_OVER),
|
|
227
|
+
c1: Math.min(cols - 1, Math.floor((shell.scrollLeft + wide) / APP_COL) + APP_OVER)
|
|
228
|
+
};
|
|
200
229
|
}
|
|
201
230
|
|
|
202
|
-
/* A
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
export function
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
cache.
|
|
215
|
-
cache.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
* delta below it adds up to. Its numbers are the state at HEAD rather than the last commit's cells, which is why the
|
|
224
|
-
* running sums take them from here instead of assuming they are the row above. */
|
|
225
|
-
export function appNow(files, metrics, cache) {
|
|
226
|
-
const tr = appEl('tr', 'now');
|
|
227
|
-
tr.appendChild(appEl('th', 'c-commit', appUi.now));
|
|
228
|
-
const model = nowModel(appData.now, metrics);
|
|
229
|
-
const last = appData.rows.length;
|
|
230
|
-
model.total.forEach((v, mi) => {
|
|
231
|
-
const td = appValueCell(v, mi);
|
|
232
|
-
cache.sums[mi][last] = v;
|
|
233
|
-
cache.cells[mi][last] = td;
|
|
234
|
-
tr.appendChild(td);
|
|
231
|
+
/* The window drawn. A scroll costs what left the window and what entered it, and nothing else: the row that is
|
|
232
|
+
* already built is not touched. Sideways — the window of columns is another window — the header and the rows are
|
|
233
|
+
* built again, because a column that is not there cannot be shown; the reader's own choice (`redraw`) is the same
|
|
234
|
+
* kind of change, and comes here by the same road. */
|
|
235
|
+
export function appWindow(cache, redraw) {
|
|
236
|
+
const span = appSpan(cache);
|
|
237
|
+
const was = cache.win;
|
|
238
|
+
cache.win = span;
|
|
239
|
+
if (redraw === true || was === null || was.c0 !== span.c0 || was.c1 !== span.c1) {
|
|
240
|
+
const head = cache.grid.querySelector('.head');
|
|
241
|
+
if (head !== null) head.remove();
|
|
242
|
+
cache.grid.insertBefore(appHead(cache, span), cache.grid.firstChild);
|
|
243
|
+
cache.rows.clear();
|
|
244
|
+
[...cache.grid.querySelectorAll('.row')].forEach((row) => row.remove());
|
|
245
|
+
for (let r = span.r0; r <= span.r1; r++) appPlace(cache, r, span);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
[...cache.rows.keys()].forEach((r) => {
|
|
249
|
+
if (r >= span.r0 && r <= span.r1) return;
|
|
250
|
+
cache.rows.get(r).remove();
|
|
251
|
+
cache.rows.delete(r);
|
|
235
252
|
});
|
|
236
|
-
|
|
237
|
-
|
|
253
|
+
for (let r = span.r0; r <= span.r1; r++) {
|
|
254
|
+
if (!cache.rows.has(r)) appPlace(cache, r, span);
|
|
255
|
+
}
|
|
238
256
|
}
|
|
239
257
|
|
|
240
|
-
/* The
|
|
241
|
-
*
|
|
242
|
-
|
|
243
|
-
const body = appEl('tbody');
|
|
244
|
-
for (let r = appData.rows.length - 1; r >= 0; r--) body.appendChild(appRow(r, files, metrics, cache));
|
|
245
|
-
body.insertBefore(appNow(files, metrics, cache), body.firstChild);
|
|
246
|
-
return body;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/* The whole table, built once. What the cache holds is what the reader's choice works with: the nodes of each file's
|
|
250
|
-
* column — its cells, its headings and its `<col>` (`cols`), the group headings whose colSpan follows the enabled
|
|
251
|
-
* metrics (`spans`), the cells of the totals (`cells`), the running sums (`sums`) and those same sums with every
|
|
252
|
-
* column on (`all`) — the sums a view painted again starts from. */
|
|
258
|
+
/* The grid: the shell whose scroll it follows, the metrics it counts in and the window at the place the reader is.
|
|
259
|
+
* The first drawing happens where the view is known (`appPaint` of the assembling chapter): what is built here is the
|
|
260
|
+
* place the table stands in, and no cell of it. */
|
|
253
261
|
export function appTable(grid) {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
* widths from, and it has to be in the markup before the browser settles on a layout — the drawing is one pass, not
|
|
268
|
-
* two. */
|
|
269
|
-
grid.insertBefore(appCols(files, appWidest(files, metrics), cache), grid.firstChild);
|
|
270
|
-
cache.all = cache.sums.map((row) => row.slice());
|
|
262
|
+
const cache = {
|
|
263
|
+
grid: grid,
|
|
264
|
+
shell: document.getElementById('shell'),
|
|
265
|
+
keys: [],
|
|
266
|
+
labels: [],
|
|
267
|
+
list: [],
|
|
268
|
+
rank: [],
|
|
269
|
+
slot: [],
|
|
270
|
+
rows: new Map(),
|
|
271
|
+
win: null
|
|
272
|
+
};
|
|
273
|
+
cache.shell.addEventListener('scroll', () => appWindow(cache));
|
|
274
|
+
window.addEventListener('resize', () => appWindow(cache, true));
|
|
271
275
|
return cache;
|
|
272
276
|
}
|
|
273
|
-
|
|
274
|
-
/* How many nodes a file's column holds — its cells, its captions and its `<col>`. It is the unit the page's bar
|
|
275
|
-
* counts in (`appDraw`), so it is answered here, where the column's nodes are: the choice's state costs arithmetic,
|
|
276
|
-
* and the nodes are the table's business. */
|
|
277
|
-
export function appColumnSize(cache, i) {
|
|
278
|
-
return cache.cols[i].length;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/* Whether a file's column has to be drawn at all: `drawn` is the state its nodes carry (the table is built with every
|
|
282
|
-
* column shown), so a column that is on and was never drawn is already right. The first drawing of a report has
|
|
283
|
-
* nothing switched off and therefore costs nothing, and a record from the memory or a link queues exactly the columns
|
|
284
|
-
* that differ from it — on a table of a few hundred thousand cells that is the difference between a page that opens
|
|
285
|
-
* and a page that works for a minute after opening. */
|
|
286
|
-
export function appColumnStale(cache, i) {
|
|
287
|
-
return cache.drawn[i] !== (appView.files[i] === true);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/* A file's column shown or hidden: its cells and its headings together, one class per node and no new node. A hidden
|
|
291
|
-
* column keeps its place in the markup — the order of the columns is the files' business, not the choice's. What the
|
|
292
|
-
* nodes carry is remembered (`drawn`), or a repeated switch would walk a column nobody has to see again. */
|
|
293
|
-
export function appColumn(cache, i, on) {
|
|
294
|
-
cache.cols[i].forEach((node) => node.classList.toggle('off', !on));
|
|
295
|
-
cache.drawn[i] = on;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/* A file's share of the totals: switched off it takes exactly its own numbers out of the running sums, switched on
|
|
299
|
-
* it puts them back. Two rows per file are the whole of it — the numbers are the data's, and a sum of integers is
|
|
300
|
-
* exact in either direction. */
|
|
301
|
-
export function appContribute(cache, i, on) {
|
|
302
|
-
const sign = on ? 1 : -1;
|
|
303
|
-
appData.rows.forEach((row, r) => {
|
|
304
|
-
const values = row.values[i];
|
|
305
|
-
if (values === null) return;
|
|
306
|
-
cache.keys.forEach((key, mi) => { cache.sums[mi][r] += sign * values[key]; });
|
|
307
|
-
});
|
|
308
|
-
const values = appData.now[i];
|
|
309
|
-
if (values === null) return;
|
|
310
|
-
const last = appData.rows.length;
|
|
311
|
-
cache.keys.forEach((key, mi) => { cache.sums[mi][last] += sign * values[key]; });
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/* The totals written where they stand: a data row shows the change against the row below it, the "now" row the
|
|
315
|
-
* absolute size — the same rules the first drawing used (`cellParts`, `valueParts`), so the reader sees one kind of
|
|
316
|
-
* number and not two. */
|
|
317
|
-
export function appTotals(cache) {
|
|
318
|
-
const last = appData.rows.length;
|
|
319
|
-
cache.cells.forEach((cells, mi) => {
|
|
320
|
-
cells.forEach((td, r) => {
|
|
321
|
-
if (r === last) { appFill(td, valueParts(cache.sums[mi][r])); return; }
|
|
322
|
-
appFill(td, cellParts(cache.sums[mi][r], deltaOf(cache.sums[mi][r], r === 0 ? null : cache.sums[mi][r - 1]), '−'));
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/* The running sums of the whole view again: a link, a record from the memory and the first drawing change the choice
|
|
328
|
-
* as a whole rather than a column, so the sums start from the sums with every column on and take the switched-off
|
|
329
|
-
* ones out — one copy of the numbers instead of a recount. */
|
|
330
|
-
export function appTotalsReset(cache) {
|
|
331
|
-
cache.sums = cache.all.map((row) => row.slice());
|
|
332
|
-
appData.files.forEach((_f, i) => { if (!appView.files[i]) appContribute(cache, i, false); });
|
|
333
|
-
appTotals(cache);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/* The metrics of the choice: a class on the table hides every cell of a metric at once, and the group headings follow
|
|
337
|
-
* how many are left — two things that change, instead of a pass over the metric's cells (measured: 2.20 ms and 265
|
|
338
|
-
* operations against 433 ms and 56 496 for walking them). `.m-none` is what the styling reads to drop the group's
|
|
339
|
-
* left border when there is nothing left to separate. */
|
|
340
|
-
export function appMetrics(cache) {
|
|
341
|
-
let on = 0;
|
|
342
|
-
cache.keys.forEach((key, mi) => {
|
|
343
|
-
const visible = appView.metrics[key] === true;
|
|
344
|
-
if (visible) on++;
|
|
345
|
-
cache.grid.classList.toggle('m-off-' + mi, !visible);
|
|
346
|
-
});
|
|
347
|
-
cache.grid.classList.toggle('m-none', on === 0);
|
|
348
|
-
cache.spans.forEach((th) => { th.colSpan = on === 0 ? 1 : on; });
|
|
349
|
-
}
|