@vernikr/size-report 2.6.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 +111 -45
- package/package.json +2 -2
- package/src/css.js +2 -2
- package/src/data.js +21 -5
- package/src/locales.js +2 -2
- package/src/page/app.css +7 -2
- package/src/page/app.js +38 -47
- package/src/page/build.js +5 -3
- package/src/page/panel.js +24 -10
- package/src/page/state.js +43 -73
- package/src/page/table.js +221 -274
- package/src/table.css +89 -57
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,256 +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;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/* A text's breadth in `ch`, the unit a column is measured in: a digit is exactly one `ch` in this table's font
|
|
123
|
-
* (measured in Chrome: 8.67px against 8.67px), and a thin space is counted as a third of a digit — nothing else is
|
|
124
|
-
* discounted. Without that one fraction every number column would come out a fifth wider than the number in it (the
|
|
125
|
-
* grouping makes a text 21 % narrower than its characters, measured), which would give back more than the padding and
|
|
126
|
-
* the clipping of this step save. What is left over-measures — a letter is 0.84 of a `ch`, a slash 0.48 — and that is
|
|
127
|
-
* the safe side: a cell clips nothing, so a column a character short would show a number running over its neighbour. */
|
|
128
|
-
function appBreadth(text) {
|
|
129
|
-
let wide = 0;
|
|
130
|
-
for (let i = 0; i < text.length; i++) wide += text[i] === '\u2009' ? 0.35 : 1;
|
|
131
|
-
return wide;
|
|
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);
|
|
132
125
|
}
|
|
133
126
|
|
|
134
|
-
/*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
function
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
});
|
|
156
|
-
return { total: total, files: wide };
|
|
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;
|
|
157
148
|
}
|
|
158
149
|
|
|
159
|
-
/* A
|
|
160
|
-
*
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return per.map((n) => Math.max(n, share));
|
|
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;
|
|
167
157
|
}
|
|
168
158
|
|
|
169
|
-
/*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const col = appEl('col', 'm' + mi);
|
|
178
|
-
col.style.setProperty('--ch', n + 'ch');
|
|
179
|
-
group.appendChild(col);
|
|
180
|
-
return col;
|
|
181
|
-
};
|
|
182
|
-
group.appendChild(appEl('col', 'c-commit'));
|
|
183
|
-
appGroupWide(wide.total, appUi.total).forEach(add);
|
|
184
|
-
files.forEach((i) => appGroupWide(wide.files[i], appData.files[i].label)
|
|
185
|
-
.forEach((n, mi) => { cache.cols[i].push(add(n, mi)); }));
|
|
186
|
-
return group;
|
|
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;
|
|
187
167
|
}
|
|
188
168
|
|
|
189
|
-
/*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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;
|
|
200
193
|
}
|
|
201
194
|
|
|
202
|
-
/*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const td = appCell(cell, mi);
|
|
214
|
-
cache.sums[mi][r] = cell.value;
|
|
215
|
-
cache.cells[mi][r] = td;
|
|
216
|
-
tr.appendChild(td);
|
|
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); }
|
|
217
206
|
});
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
};
|
|
220
229
|
}
|
|
221
230
|
|
|
222
|
-
/* The
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
cache.
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/* The body: the commit rows from the newest down, plus the "now" row with the absolute sizes at HEAD. The deltas
|
|
241
|
-
* under it add up to it, which is why it stands first. */
|
|
242
|
-
export function appBody(files, metrics, cache) {
|
|
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;
|
|
253
|
+
for (let r = span.r0; r <= span.r1; r++) {
|
|
254
|
+
if (!cache.rows.has(r)) appPlace(cache, r, span);
|
|
255
|
+
}
|
|
247
256
|
}
|
|
248
257
|
|
|
249
|
-
/* The
|
|
250
|
-
*
|
|
251
|
-
*
|
|
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
|
-
grid.insertBefore(appCols(files, appWidest(files, metrics), cache), grid.firstChild);
|
|
268
|
-
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));
|
|
269
275
|
return cache;
|
|
270
276
|
}
|
|
271
|
-
|
|
272
|
-
/* A file's column shown or hidden: its cells and its headings together, one class per node and no new node. A hidden
|
|
273
|
-
* column keeps its place in the markup — the order of the columns is the files' business, not the choice's. */
|
|
274
|
-
export function appColumn(cache, i, on) {
|
|
275
|
-
cache.cols[i].forEach((node) => node.classList.toggle('off', !on));
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/* A file's share of the totals: switched off it takes exactly its own numbers out of the running sums, switched on
|
|
279
|
-
* 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
|
|
280
|
-
* exact in either direction. */
|
|
281
|
-
export function appContribute(cache, i, on) {
|
|
282
|
-
const sign = on ? 1 : -1;
|
|
283
|
-
appData.rows.forEach((row, r) => {
|
|
284
|
-
const values = row.values[i];
|
|
285
|
-
if (values === null) return;
|
|
286
|
-
cache.keys.forEach((key, mi) => { cache.sums[mi][r] += sign * values[key]; });
|
|
287
|
-
});
|
|
288
|
-
const values = appData.now[i];
|
|
289
|
-
if (values === null) return;
|
|
290
|
-
const last = appData.rows.length;
|
|
291
|
-
cache.keys.forEach((key, mi) => { cache.sums[mi][last] += sign * values[key]; });
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/* The totals written where they stand: a data row shows the change against the row below it, the "now" row the
|
|
295
|
-
* absolute size — the same rules the first drawing used (`cellParts`, `valueParts`), so the reader sees one kind of
|
|
296
|
-
* number and not two. */
|
|
297
|
-
export function appTotals(cache) {
|
|
298
|
-
const last = appData.rows.length;
|
|
299
|
-
cache.cells.forEach((cells, mi) => {
|
|
300
|
-
cells.forEach((td, r) => {
|
|
301
|
-
if (r === last) { appFill(td, valueParts(cache.sums[mi][r])); return; }
|
|
302
|
-
appFill(td, cellParts(cache.sums[mi][r], deltaOf(cache.sums[mi][r], r === 0 ? null : cache.sums[mi][r - 1]), '−'));
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/* The running sums of the whole view again: a link, a record from the memory and the first drawing change the choice
|
|
308
|
-
* as a whole rather than a column, so the sums start from the sums with every column on and take the switched-off
|
|
309
|
-
* ones out — one copy of the numbers instead of a recount. */
|
|
310
|
-
export function appTotalsReset(cache) {
|
|
311
|
-
cache.sums = cache.all.map((row) => row.slice());
|
|
312
|
-
appData.files.forEach((_f, i) => { if (!appView.files[i]) appContribute(cache, i, false); });
|
|
313
|
-
appTotals(cache);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/* The metrics of the choice: a class on the table hides every cell of a metric at once, and the group headings follow
|
|
317
|
-
* how many are left — two things that change, instead of a pass over the metric's cells (measured: 2.20 ms and 265
|
|
318
|
-
* operations against 433 ms and 56 496 for walking them). `.m-none` is what the styling reads to drop the group's
|
|
319
|
-
* left border when there is nothing left to separate. */
|
|
320
|
-
export function appMetrics(cache) {
|
|
321
|
-
let on = 0;
|
|
322
|
-
cache.keys.forEach((key, mi) => {
|
|
323
|
-
const visible = appView.metrics[key] === true;
|
|
324
|
-
if (visible) on++;
|
|
325
|
-
cache.grid.classList.toggle('m-off-' + mi, !visible);
|
|
326
|
-
});
|
|
327
|
-
cache.grid.classList.toggle('m-none', on === 0);
|
|
328
|
-
cache.spans.forEach((th) => { th.colSpan = on === 0 ? 1 : on; });
|
|
329
|
-
}
|