@unpunnyfuns/d3-blockmap 0.1.0 → 0.2.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 +26 -5
- package/dist/{chunk-WIDC3EN2.js → chunk-6BBJWE5O.js} +495 -194
- package/dist/chunk-6BBJWE5O.js.map +1 -0
- package/dist/index.d.ts +60 -8
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +8 -3
- package/dist/react/index.js +49 -38
- package/dist/react/index.js.map +1 -1
- package/package.json +11 -3
- package/dist/chunk-WIDC3EN2.js.map +0 -1
|
@@ -5,53 +5,71 @@ import { tickStep } from "d3-array";
|
|
|
5
5
|
var BITS_PER_BYTE = 8;
|
|
6
6
|
var BYTE_SHIFT = 3;
|
|
7
7
|
var BYTE_MASK = 7;
|
|
8
|
+
var FULL_BYTE = 255;
|
|
8
9
|
var BYTES_PER_PIXEL = 4;
|
|
9
10
|
var OPAQUE_ALPHA = 255;
|
|
10
11
|
var COLOR_TABLE_SIZE = 256;
|
|
12
|
+
var CANVAS_ROWS = 1;
|
|
11
13
|
var PERCENT = 100;
|
|
12
14
|
var PERCENT_FRACTION_DIGITS = 1;
|
|
13
15
|
var TOOLTIP_OFFSET_PX = 8;
|
|
14
|
-
var CANVAS_ROWS = 1;
|
|
15
16
|
var CSS_VAR_MISSING = "--blockmap-missing";
|
|
16
17
|
var CSS_VAR_PARTIAL_LOW = "--blockmap-partial-low";
|
|
17
18
|
var CSS_VAR_PARTIAL_HIGH = "--blockmap-partial-high";
|
|
18
19
|
var CSS_VAR_PARTIAL = "--blockmap-partial";
|
|
19
20
|
var CSS_VAR_COMPLETE = "--blockmap-complete";
|
|
20
21
|
var CSS_VAR_HIGHLIGHT = "--blockmap-highlight";
|
|
21
|
-
var
|
|
22
|
-
var ZOOM_STEP = 1.25;
|
|
23
|
-
var DRAG_THRESHOLD_PX = 3;
|
|
24
|
-
var BLINK_INTERVAL_MS = 500;
|
|
25
|
-
var MIN_VIEW_LENGTH = 1;
|
|
22
|
+
var CSS_VAR_BRUSH = "--blockmap-brush";
|
|
26
23
|
var DEFAULT_THEME = {
|
|
27
24
|
missing: "#2b2b2b",
|
|
28
25
|
partialLow: "#5b3a00",
|
|
29
26
|
partialHigh: "#e0a500",
|
|
30
27
|
partial: "#e0a500",
|
|
31
28
|
complete: "#2ecc71",
|
|
32
|
-
highlight: "#ff4d4f"
|
|
29
|
+
highlight: "#ff4d4f",
|
|
30
|
+
brush: "#ffffff"
|
|
33
31
|
};
|
|
32
|
+
var BRUSH_FILL_OPACITY = 0.3;
|
|
34
33
|
var THEME_ATTRIBUTES = ["class", "style", "data-theme"];
|
|
35
34
|
var DARK_SCHEME_QUERY = "(prefers-color-scheme: dark)";
|
|
36
|
-
var
|
|
37
|
-
var
|
|
38
|
-
var
|
|
35
|
+
var ZOOM_STEP = 1.25;
|
|
36
|
+
var DRAG_THRESHOLD_PX = 3;
|
|
37
|
+
var BLINK_INTERVAL_MS = 500;
|
|
38
|
+
var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
39
|
+
var MIN_VIEW_LENGTH = 1;
|
|
40
|
+
var KEY_PAN_FRACTION = 0.1;
|
|
41
|
+
var KEY_PAN_FRACTION_FAST = 0.5;
|
|
42
|
+
var ARIA_LABEL_DEFAULT_PERCENT_DIGITS = 0;
|
|
43
|
+
var ARIA_ROLEDESCRIPTION = "zoomable block map";
|
|
44
|
+
var ARIA_KEYSHORTCUTS = "Plus - ArrowLeft ArrowRight Home";
|
|
39
45
|
var CSS_VAR_AXIS_COLOR = "--blockmap-axis-color";
|
|
40
46
|
var TICK_SPACING_PX = 80;
|
|
41
47
|
var MIN_TICKS = 2;
|
|
42
48
|
var EDGE_ANCHOR_RATIO = 0.5;
|
|
43
49
|
var DECIMAL_BASE = 10;
|
|
50
|
+
var RANGE_PAIR = 2;
|
|
51
|
+
var MAX_BLOCKS = 4294967295;
|
|
44
52
|
|
|
45
53
|
// src/dom.ts
|
|
46
54
|
var ensureRelative = (container) => {
|
|
47
55
|
const position = getComputedStyle(container).position;
|
|
48
56
|
if (position === "static" || !position) container.style.position = "relative";
|
|
49
57
|
};
|
|
58
|
+
var canvasX = (canvas, event) => event.clientX - canvas.getBoundingClientRect().left;
|
|
50
59
|
|
|
51
60
|
// src/view.ts
|
|
52
61
|
var fullView = (totalBlocks) => ({ start: 0, end: totalBlocks });
|
|
53
62
|
var viewLength = (view) => view.end - view.start;
|
|
54
63
|
var sameView = (a, b) => a.start === b.start && a.end === b.end;
|
|
64
|
+
var createScale = (view, cssWidth) => {
|
|
65
|
+
const length = viewLength(view);
|
|
66
|
+
return {
|
|
67
|
+
domain: [view.start, view.end],
|
|
68
|
+
range: [0, cssWidth],
|
|
69
|
+
x: (block) => cssWidth === 0 ? 0 : (block - view.start) / length * cssWidth,
|
|
70
|
+
block: (x) => cssWidth === 0 ? view.start : view.start + x / cssWidth * length
|
|
71
|
+
};
|
|
72
|
+
};
|
|
55
73
|
var clampView = (view, totalBlocks) => {
|
|
56
74
|
let start = Math.round(Math.min(view.start, view.end));
|
|
57
75
|
let end = Math.round(Math.max(view.start, view.end));
|
|
@@ -60,6 +78,7 @@ var clampView = (view, totalBlocks) => {
|
|
|
60
78
|
start = Math.max(0, Math.min(start, totalBlocks - length));
|
|
61
79
|
return { start, end: start + length };
|
|
62
80
|
};
|
|
81
|
+
var legalView = (view, totalBlocks) => Number.isFinite(view.start) && Number.isFinite(view.end) ? clampView(view, totalBlocks) : void 0;
|
|
63
82
|
var zoomView = (view, anchor, factor, totalBlocks) => {
|
|
64
83
|
const length = viewLength(view);
|
|
65
84
|
const scaled = length * factor;
|
|
@@ -107,15 +126,6 @@ var defaultFormat = (block, step = 1) => {
|
|
|
107
126
|
if (digits > MAX_COMPACT_FRACTION_DIGITS) return groupedFormat.format(block);
|
|
108
127
|
return compactFormatterFor(digits).format(block);
|
|
109
128
|
};
|
|
110
|
-
var createScale = (view, cssWidth) => {
|
|
111
|
-
const length = viewLength(view);
|
|
112
|
-
return {
|
|
113
|
-
domain: [view.start, view.end],
|
|
114
|
-
range: [0, cssWidth],
|
|
115
|
-
x: (block) => cssWidth === 0 ? 0 : (block - view.start) / length * cssWidth,
|
|
116
|
-
block: (x) => cssWidth === 0 ? view.start : view.start + x / cssWidth * length
|
|
117
|
-
};
|
|
118
|
-
};
|
|
119
129
|
var computeTicks = (view, cssWidth, tickCount) => {
|
|
120
130
|
if (cssWidth <= 0) return [];
|
|
121
131
|
const count = tickCount ?? Math.max(MIN_TICKS, Math.floor(cssWidth / TICK_SPACING_PX));
|
|
@@ -155,6 +165,9 @@ var createAxis = (container, options) => {
|
|
|
155
165
|
});
|
|
156
166
|
container.append(root);
|
|
157
167
|
return {
|
|
168
|
+
// Rebuilds every label on each update. There are only a handful of ticks,
|
|
169
|
+
// so a single replaceChildren beats diffing, and it cannot leave stale
|
|
170
|
+
// labels behind after a zoom.
|
|
158
171
|
update: (view, cssWidth) => {
|
|
159
172
|
const spans = computeTicks(view, cssWidth, options.tickCount).map((tick) => {
|
|
160
173
|
const span = document.createElement("span");
|
|
@@ -179,6 +192,208 @@ var createAxis = (container, options) => {
|
|
|
179
192
|
};
|
|
180
193
|
};
|
|
181
194
|
|
|
195
|
+
// src/bins.ts
|
|
196
|
+
var discreteColumn = (block, length, binCount) => Math.floor(block * binCount / length);
|
|
197
|
+
var discreteBlock = (column, length, binCount) => Math.ceil((column + 1) * length / binCount) - 1;
|
|
198
|
+
var clipToView = (rawStart, rawEnd, viewStart, viewEnd) => [Math.max(rawStart, viewStart), Math.min(rawEnd, viewEnd)];
|
|
199
|
+
var createBins = (view, binCount) => {
|
|
200
|
+
const { start: viewStart, end: viewEnd } = view;
|
|
201
|
+
const length = viewEnd - viewStart;
|
|
202
|
+
const counts = new Uint32Array(binCount);
|
|
203
|
+
const binOf = (index) => Math.floor((index - viewStart) * binCount / length);
|
|
204
|
+
const rangeOf = (bin) => [
|
|
205
|
+
viewStart + Math.ceil(bin * length / binCount),
|
|
206
|
+
viewStart + Math.ceil((bin + 1) * length / binCount)
|
|
207
|
+
];
|
|
208
|
+
const sizeOf = (bin) => {
|
|
209
|
+
const [start, end] = rangeOf(bin);
|
|
210
|
+
return end - start;
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
binCount,
|
|
214
|
+
counts,
|
|
215
|
+
binOf,
|
|
216
|
+
rangeOf,
|
|
217
|
+
sizeOf,
|
|
218
|
+
// Full recount from the bitset, used after a resize or a view change.
|
|
219
|
+
rebuild: (countInRange) => {
|
|
220
|
+
counts.fill(0);
|
|
221
|
+
if (binCount === 0) return;
|
|
222
|
+
for (let bin = 0; bin < binCount; bin += 1) {
|
|
223
|
+
const [start, end] = rangeOf(bin);
|
|
224
|
+
counts[bin] = countInRange(start, end);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
/**
|
|
228
|
+
* Adds a wholly-new range's blocks to the bin counts without consulting
|
|
229
|
+
* the bitset: the first and last bins get the part of themselves the
|
|
230
|
+
* range covers, the bins between get their full size.
|
|
231
|
+
*/
|
|
232
|
+
addRange: (rawStart, rawEnd) => {
|
|
233
|
+
const [start, end] = clipToView(rawStart, rawEnd, viewStart, viewEnd);
|
|
234
|
+
if (binCount === 0 || end <= start) return;
|
|
235
|
+
const first = binOf(start);
|
|
236
|
+
const last = binOf(end - 1);
|
|
237
|
+
if (first === last) {
|
|
238
|
+
counts[first] += end - start;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
counts[first] += rangeOf(first)[1] - start;
|
|
242
|
+
for (let bin = first + 1; bin < last; bin += 1) counts[bin] += sizeOf(bin);
|
|
243
|
+
counts[last] += end - rangeOf(last)[0];
|
|
244
|
+
},
|
|
245
|
+
// Which bins a range overlaps, so callers can mark exactly those dirty.
|
|
246
|
+
binsTouching: (rawStart, rawEnd) => {
|
|
247
|
+
const [start, end] = clipToView(rawStart, rawEnd, viewStart, viewEnd);
|
|
248
|
+
if (binCount === 0 || end <= start) return [];
|
|
249
|
+
const first = binOf(start);
|
|
250
|
+
const last = binOf(end - 1);
|
|
251
|
+
const touched = [];
|
|
252
|
+
for (let bin = first; bin <= last; bin += 1) touched.push(bin);
|
|
253
|
+
return touched;
|
|
254
|
+
},
|
|
255
|
+
recountBins: (bins, countInRange) => {
|
|
256
|
+
for (const bin of bins) {
|
|
257
|
+
const [start, end] = rangeOf(bin);
|
|
258
|
+
counts[bin] = countInRange(start, end);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// src/brush.ts
|
|
265
|
+
import { brushX } from "d3-brush";
|
|
266
|
+
import { select } from "d3-selection";
|
|
267
|
+
var BRUSH_ATTRIBUTE = "data-blockmap-brush";
|
|
268
|
+
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
269
|
+
var SELECTION_CLASS = ".selection";
|
|
270
|
+
var GESTURE_LISTENERS = "mousemove.brush mouseup.brush keydown.brush keyup.brush dragstart.drag selectstart.drag";
|
|
271
|
+
var clamp = (value, max) => Math.max(0, Math.min(max, value));
|
|
272
|
+
var toPixels = (selection, view, cssWidth) => {
|
|
273
|
+
if (cssWidth === 0) return null;
|
|
274
|
+
if (selection[1] <= view.start || selection[0] >= view.end) return null;
|
|
275
|
+
const scale = createScale(view, cssWidth);
|
|
276
|
+
return [clamp(scale.x(selection[0]), cssWidth), clamp(scale.x(selection[1]), cssWidth)];
|
|
277
|
+
};
|
|
278
|
+
var toBlocks = (pixels, view, cssWidth, totalBlocks) => {
|
|
279
|
+
const scale = createScale(view, cssWidth);
|
|
280
|
+
const start = clamp(Math.round(scale.block(pixels[0])), totalBlocks);
|
|
281
|
+
let end = clamp(Math.round(scale.block(pixels[1])), totalBlocks);
|
|
282
|
+
if (end <= start) {
|
|
283
|
+
end = Math.min(start + 1, totalBlocks);
|
|
284
|
+
if (end <= start) return [Math.max(0, end - 1), end];
|
|
285
|
+
}
|
|
286
|
+
return [start, end];
|
|
287
|
+
};
|
|
288
|
+
var normalize = (selection, totalBlocks) => {
|
|
289
|
+
if (!selection) return null;
|
|
290
|
+
const start = clamp(Math.round(selection[0]), totalBlocks);
|
|
291
|
+
const end = clamp(Math.round(selection[1]), totalBlocks);
|
|
292
|
+
return end > start ? [start, end] : null;
|
|
293
|
+
};
|
|
294
|
+
var xSpan = (selection) => {
|
|
295
|
+
if (!selection) return null;
|
|
296
|
+
const [x0, x1] = selection;
|
|
297
|
+
if (typeof x0 !== "number" || typeof x1 !== "number") return null;
|
|
298
|
+
return [x0, x1];
|
|
299
|
+
};
|
|
300
|
+
var sameRange = (a, b) => a === b || a !== null && b !== null && a[0] === b[0] && a[1] === b[1];
|
|
301
|
+
var createBrushOverlay = (container, deps) => {
|
|
302
|
+
ensureRelative(container);
|
|
303
|
+
const svg = document.createElementNS(SVG_NAMESPACE, "svg");
|
|
304
|
+
svg.setAttribute(BRUSH_ATTRIBUTE, "");
|
|
305
|
+
Object.assign(svg.style, {
|
|
306
|
+
position: "absolute",
|
|
307
|
+
inset: "0",
|
|
308
|
+
width: "100%",
|
|
309
|
+
height: "100%",
|
|
310
|
+
touchAction: "none",
|
|
311
|
+
overflow: "visible",
|
|
312
|
+
display: "block"
|
|
313
|
+
});
|
|
314
|
+
container.append(svg);
|
|
315
|
+
const g = select(svg).append("g");
|
|
316
|
+
const onMouseDown = (event) => {
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
};
|
|
319
|
+
svg.addEventListener("mousedown", onMouseDown, true);
|
|
320
|
+
const brush = brushX().touchable(() => false).filter(
|
|
321
|
+
(event) => !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.button
|
|
322
|
+
);
|
|
323
|
+
let selection = null;
|
|
324
|
+
let color = DEFAULT_THEME.brush;
|
|
325
|
+
let silent = false;
|
|
326
|
+
let active = false;
|
|
327
|
+
const paint = () => {
|
|
328
|
+
g.select(SELECTION_CLASS).attr("fill", color).attr("stroke", color).attr("fill-opacity", BRUSH_FILL_OPACITY);
|
|
329
|
+
};
|
|
330
|
+
const update = () => {
|
|
331
|
+
const width = deps.getWidth();
|
|
332
|
+
brush.extent([
|
|
333
|
+
[0, 0],
|
|
334
|
+
[width, deps.getHeight()]
|
|
335
|
+
]);
|
|
336
|
+
g.call(brush);
|
|
337
|
+
paint();
|
|
338
|
+
silent = true;
|
|
339
|
+
try {
|
|
340
|
+
g.call(brush.move, selection ? toPixels(selection, deps.getView(), width) : null);
|
|
341
|
+
} finally {
|
|
342
|
+
silent = false;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const blocksFrom = (pixels) => {
|
|
346
|
+
const span = xSpan(pixels);
|
|
347
|
+
if (!span) return null;
|
|
348
|
+
return toBlocks(span, deps.getView(), deps.getWidth(), deps.getTotalBlocks());
|
|
349
|
+
};
|
|
350
|
+
brush.on("start", (event) => {
|
|
351
|
+
if (!event.sourceEvent || silent) return;
|
|
352
|
+
active = true;
|
|
353
|
+
});
|
|
354
|
+
brush.on("brush", (event) => {
|
|
355
|
+
if (!event.sourceEvent || silent) return;
|
|
356
|
+
deps.onBrush(blocksFrom(event.selection));
|
|
357
|
+
});
|
|
358
|
+
brush.on("end", (event) => {
|
|
359
|
+
if (!event.sourceEvent || silent) return;
|
|
360
|
+
active = false;
|
|
361
|
+
const next = normalize(blocksFrom(event.selection), deps.getTotalBlocks());
|
|
362
|
+
const changed = !sameRange(selection, next);
|
|
363
|
+
selection = next;
|
|
364
|
+
update();
|
|
365
|
+
if (changed) deps.onBrushEnd(selection);
|
|
366
|
+
});
|
|
367
|
+
update();
|
|
368
|
+
return {
|
|
369
|
+
update,
|
|
370
|
+
setSelection: (next) => {
|
|
371
|
+
selection = normalize(next, deps.getTotalBlocks());
|
|
372
|
+
update();
|
|
373
|
+
},
|
|
374
|
+
getSelection: () => selection ? [selection[0], selection[1]] : null,
|
|
375
|
+
setColor: (next) => {
|
|
376
|
+
color = next;
|
|
377
|
+
paint();
|
|
378
|
+
},
|
|
379
|
+
// d3-brush only drops its window listeners and re-enables dragging from
|
|
380
|
+
// inside its own gesture end, so destroying mid-drag has to do it by
|
|
381
|
+
// hand or they outlive the map. Every targeted browser has
|
|
382
|
+
// onselectstart, so d3-drag suppresses selection with a listener rather
|
|
383
|
+
// than an inline style and removing the listeners is the whole cleanup.
|
|
384
|
+
// Those listeners are removed by name, which would take another
|
|
385
|
+
// component's brush or drag down with them, so this only runs for a
|
|
386
|
+
// gesture that is actually in flight.
|
|
387
|
+
destroy: () => {
|
|
388
|
+
brush.on("start", null).on("brush", null).on("end", null);
|
|
389
|
+
g.on(".brush", null);
|
|
390
|
+
svg.removeEventListener("mousedown", onMouseDown, true);
|
|
391
|
+
if (active) select(window).on(GESTURE_LISTENERS, null);
|
|
392
|
+
svg.remove();
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
|
|
182
397
|
// src/colors.ts
|
|
183
398
|
import { rgb } from "d3-color";
|
|
184
399
|
import { interpolateRgb, piecewise } from "d3-interpolate";
|
|
@@ -188,7 +403,8 @@ var THEME_VARS = {
|
|
|
188
403
|
partialHigh: CSS_VAR_PARTIAL_HIGH,
|
|
189
404
|
partial: CSS_VAR_PARTIAL,
|
|
190
405
|
complete: CSS_VAR_COMPLETE,
|
|
191
|
-
highlight: CSS_VAR_HIGHLIGHT
|
|
406
|
+
highlight: CSS_VAR_HIGHLIGHT,
|
|
407
|
+
brush: CSS_VAR_BRUSH
|
|
192
408
|
};
|
|
193
409
|
var LAST_INDEX = COLOR_TABLE_SIZE - 1;
|
|
194
410
|
var FIRST_PARTIAL_INDEX = 1;
|
|
@@ -214,7 +430,8 @@ var readTheme = (el) => {
|
|
|
214
430
|
partialHigh: read("partialHigh"),
|
|
215
431
|
partial: read("partial"),
|
|
216
432
|
complete: read("complete"),
|
|
217
|
-
highlight: read("highlight")
|
|
433
|
+
highlight: read("highlight"),
|
|
434
|
+
brush: read("brush")
|
|
218
435
|
};
|
|
219
436
|
};
|
|
220
437
|
var createColorScale = (theme, partial = "coverage") => {
|
|
@@ -262,9 +479,14 @@ var watchTheme = (onChange) => {
|
|
|
262
479
|
|
|
263
480
|
// src/interaction.ts
|
|
264
481
|
var PRIMARY_BUTTON = 0;
|
|
482
|
+
var TOUCH_POINTER = "touch";
|
|
265
483
|
var GRABBING = "grabbing";
|
|
266
484
|
var NO_POINTER = -1;
|
|
267
|
-
var
|
|
485
|
+
var HALF = 2;
|
|
486
|
+
var ZOOM_IN_KEYS = /* @__PURE__ */ new Set(["+", "="]);
|
|
487
|
+
var ZOOM_OUT_KEYS = /* @__PURE__ */ new Set(["-", "_"]);
|
|
488
|
+
var RESET_KEYS = /* @__PURE__ */ new Set(["0", "Home"]);
|
|
489
|
+
var attachInteraction = (surface, canvas, deps) => {
|
|
268
490
|
let drag;
|
|
269
491
|
const columnsToBlocks = (px, view) => px * deps.getDpr() / deps.getBinCount() * viewLength(view);
|
|
270
492
|
const onWheel = (event) => {
|
|
@@ -273,14 +495,18 @@ var attachInteraction = (canvas, deps) => {
|
|
|
273
495
|
if (deps.getBinCount() === 0) return;
|
|
274
496
|
event.preventDefault();
|
|
275
497
|
const view = deps.getView();
|
|
276
|
-
const anchor = view.start + columnsToBlocks(event
|
|
498
|
+
const anchor = view.start + columnsToBlocks(canvasX(canvas, event), view);
|
|
277
499
|
const factor = event.deltaY > 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
|
|
278
500
|
deps.setView(zoomView(view, anchor, factor, deps.getTotalBlocks()));
|
|
279
501
|
};
|
|
280
502
|
const onPointerDown = (event) => {
|
|
281
503
|
if (event.button !== PRIMARY_BUTTON) return;
|
|
504
|
+
if (document.activeElement !== canvas && typeof canvas.focus === "function") {
|
|
505
|
+
canvas.focus({ preventScroll: true });
|
|
506
|
+
}
|
|
507
|
+
if (deps.panRequiresShift() && event.pointerType !== TOUCH_POINTER && !event.shiftKey) return;
|
|
282
508
|
drag = {
|
|
283
|
-
downX: event
|
|
509
|
+
downX: canvasX(canvas, event),
|
|
284
510
|
view: deps.getView(),
|
|
285
511
|
pointerId: event.pointerId ?? NO_POINTER,
|
|
286
512
|
active: false
|
|
@@ -288,14 +514,14 @@ var attachInteraction = (canvas, deps) => {
|
|
|
288
514
|
};
|
|
289
515
|
const onPointerMove = (event) => {
|
|
290
516
|
if (!drag) return;
|
|
291
|
-
const dx = event
|
|
517
|
+
const dx = canvasX(canvas, event) - drag.downX;
|
|
292
518
|
if (!drag.active) {
|
|
293
519
|
if (Math.abs(dx) < DRAG_THRESHOLD_PX) return;
|
|
294
520
|
drag.active = true;
|
|
295
|
-
if (typeof
|
|
296
|
-
|
|
521
|
+
if (typeof surface.setPointerCapture === "function" && drag.pointerId !== NO_POINTER) {
|
|
522
|
+
surface.setPointerCapture(drag.pointerId);
|
|
297
523
|
}
|
|
298
|
-
|
|
524
|
+
surface.style.cursor = GRABBING;
|
|
299
525
|
deps.onDragStart();
|
|
300
526
|
}
|
|
301
527
|
if (deps.getBinCount() === 0) return;
|
|
@@ -304,30 +530,54 @@ var attachInteraction = (canvas, deps) => {
|
|
|
304
530
|
const endDrag = () => {
|
|
305
531
|
if (!drag) return;
|
|
306
532
|
if (drag.active) {
|
|
307
|
-
const hasCapture = drag.pointerId !== NO_POINTER && (
|
|
308
|
-
if (typeof
|
|
309
|
-
|
|
533
|
+
const hasCapture = drag.pointerId !== NO_POINTER && (surface.hasPointerCapture?.(drag.pointerId) ?? true);
|
|
534
|
+
if (typeof surface.releasePointerCapture === "function" && hasCapture) {
|
|
535
|
+
surface.releasePointerCapture(drag.pointerId);
|
|
310
536
|
}
|
|
311
|
-
|
|
537
|
+
surface.style.cursor = "";
|
|
312
538
|
deps.onDragEnd();
|
|
313
539
|
}
|
|
314
540
|
drag = void 0;
|
|
315
541
|
};
|
|
316
542
|
const onDblClick = () => deps.resetView();
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
543
|
+
const onKeyDown = (event) => {
|
|
544
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
545
|
+
if (drag?.active) return;
|
|
546
|
+
const view = deps.getView();
|
|
547
|
+
const length = viewLength(view);
|
|
548
|
+
const centre = view.start + length / HALF;
|
|
549
|
+
const total = deps.getTotalBlocks();
|
|
550
|
+
if (ZOOM_IN_KEYS.has(event.key)) {
|
|
551
|
+
deps.setView(zoomView(view, centre, 1 / ZOOM_STEP, total));
|
|
552
|
+
} else if (ZOOM_OUT_KEYS.has(event.key)) {
|
|
553
|
+
deps.setView(zoomView(view, centre, ZOOM_STEP, total));
|
|
554
|
+
} else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
555
|
+
const fraction = event.shiftKey ? KEY_PAN_FRACTION_FAST : KEY_PAN_FRACTION;
|
|
556
|
+
const delta = length * fraction * (event.key === "ArrowLeft" ? -1 : 1);
|
|
557
|
+
deps.setView(panView(view, delta, total));
|
|
558
|
+
} else if (RESET_KEYS.has(event.key)) {
|
|
559
|
+
deps.resetView();
|
|
560
|
+
} else {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
event.preventDefault();
|
|
564
|
+
};
|
|
565
|
+
surface.addEventListener("wheel", onWheel, { passive: false });
|
|
566
|
+
surface.addEventListener("pointerdown", onPointerDown);
|
|
567
|
+
surface.addEventListener("pointermove", onPointerMove);
|
|
568
|
+
surface.addEventListener("pointerup", endDrag);
|
|
569
|
+
surface.addEventListener("pointercancel", endDrag);
|
|
570
|
+
surface.addEventListener("dblclick", onDblClick);
|
|
571
|
+
canvas.addEventListener("keydown", onKeyDown);
|
|
323
572
|
return () => {
|
|
324
573
|
endDrag();
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
574
|
+
surface.removeEventListener("wheel", onWheel);
|
|
575
|
+
surface.removeEventListener("pointerdown", onPointerDown);
|
|
576
|
+
surface.removeEventListener("pointermove", onPointerMove);
|
|
577
|
+
surface.removeEventListener("pointerup", endDrag);
|
|
578
|
+
surface.removeEventListener("pointercancel", endDrag);
|
|
579
|
+
surface.removeEventListener("dblclick", onDblClick);
|
|
580
|
+
canvas.removeEventListener("keydown", onKeyDown);
|
|
331
581
|
};
|
|
332
582
|
};
|
|
333
583
|
|
|
@@ -347,7 +597,6 @@ var createRenderer = (ctx, source, scheduler = RAF_SCHEDULER, timer = WINDOW_TIM
|
|
|
347
597
|
let image;
|
|
348
598
|
let scale;
|
|
349
599
|
let frame = NO_FRAME;
|
|
350
|
-
let scheduled = false;
|
|
351
600
|
let allDirty = false;
|
|
352
601
|
let highlight;
|
|
353
602
|
let phase = false;
|
|
@@ -367,12 +616,13 @@ var createRenderer = (ctx, source, scheduler = RAF_SCHEDULER, timer = WINDOW_TIM
|
|
|
367
616
|
target.data.set(color, column * BYTES_PER_PIXEL);
|
|
368
617
|
};
|
|
369
618
|
const paintDiscrete = (target, colors) => {
|
|
370
|
-
const {
|
|
619
|
+
const { binCount } = source;
|
|
620
|
+
const view = source.getView();
|
|
371
621
|
const length = viewLength(view);
|
|
372
622
|
for (let block = 0; block < length; block += 1) {
|
|
373
623
|
const color = colorFor(colors, source.has(view.start + block) ? COMPLETE : 0);
|
|
374
|
-
const start =
|
|
375
|
-
const end =
|
|
624
|
+
const start = discreteColumn(block, length, binCount);
|
|
625
|
+
const end = discreteColumn(block + 1, length, binCount);
|
|
376
626
|
for (let column = start; column < end; column += 1) writeColumn(target, column, color);
|
|
377
627
|
}
|
|
378
628
|
};
|
|
@@ -388,7 +638,6 @@ var createRenderer = (ctx, source, scheduler = RAF_SCHEDULER, timer = WINDOW_TIM
|
|
|
388
638
|
}
|
|
389
639
|
};
|
|
390
640
|
const paint = () => {
|
|
391
|
-
scheduled = false;
|
|
392
641
|
frame = NO_FRAME;
|
|
393
642
|
const target = ensureImage();
|
|
394
643
|
if (!target || !scale) {
|
|
@@ -396,15 +645,14 @@ var createRenderer = (ctx, source, scheduler = RAF_SCHEDULER, timer = WINDOW_TIM
|
|
|
396
645
|
allDirty = false;
|
|
397
646
|
return;
|
|
398
647
|
}
|
|
399
|
-
if (viewLength(source.
|
|
648
|
+
if (viewLength(source.getView()) < source.binCount) paintDiscrete(target, scale);
|
|
400
649
|
else paintBins(target, scale);
|
|
401
650
|
ctx.putImageData(target, 0, 0);
|
|
402
651
|
dirty.clear();
|
|
403
652
|
allDirty = false;
|
|
404
653
|
};
|
|
405
654
|
const schedule = () => {
|
|
406
|
-
if (
|
|
407
|
-
scheduled = true;
|
|
655
|
+
if (frame !== NO_FRAME) return;
|
|
408
656
|
frame = scheduler.request(paint);
|
|
409
657
|
};
|
|
410
658
|
const markAllDirty = () => {
|
|
@@ -420,6 +668,9 @@ var createRenderer = (ctx, source, scheduler = RAF_SCHEDULER, timer = WINDOW_TIM
|
|
|
420
668
|
scale = next;
|
|
421
669
|
markAllDirty();
|
|
422
670
|
},
|
|
671
|
+
// Restarts cleanly on every call: the old interval is always cleared, so
|
|
672
|
+
// repeated enables cannot stack up blink timers. Turning the highlight
|
|
673
|
+
// off when it was never on is a no-op, avoiding a pointless repaint.
|
|
423
674
|
setHighlight: (on, color, blink) => {
|
|
424
675
|
if (!on && highlight === void 0) return;
|
|
425
676
|
stopBlink();
|
|
@@ -433,6 +684,7 @@ var createRenderer = (ctx, source, scheduler = RAF_SCHEDULER, timer = WINDOW_TIM
|
|
|
433
684
|
}
|
|
434
685
|
markAllDirty();
|
|
435
686
|
},
|
|
687
|
+
// Theme changed mid-highlight; the blink phase keeps running untouched.
|
|
436
688
|
setHighlightColor: (color) => {
|
|
437
689
|
if (highlight === void 0) return;
|
|
438
690
|
highlight = color;
|
|
@@ -445,8 +697,7 @@ var createRenderer = (ctx, source, scheduler = RAF_SCHEDULER, timer = WINDOW_TIM
|
|
|
445
697
|
markAllDirty,
|
|
446
698
|
destroy: () => {
|
|
447
699
|
stopBlink();
|
|
448
|
-
if (
|
|
449
|
-
scheduled = false;
|
|
700
|
+
if (frame !== NO_FRAME) scheduler.cancel(frame);
|
|
450
701
|
frame = NO_FRAME;
|
|
451
702
|
dirty.clear();
|
|
452
703
|
}
|
|
@@ -464,6 +715,10 @@ var createBitset = (size) => {
|
|
|
464
715
|
let count = 0;
|
|
465
716
|
return {
|
|
466
717
|
has: (index) => (bytes[index >>> BYTE_SHIFT] & 1 << (index & BYTE_MASK)) !== 0,
|
|
718
|
+
/**
|
|
719
|
+
* Returns false when the bit was already set, so callers can skip the
|
|
720
|
+
* dirty-marking work for blocks that did not actually change.
|
|
721
|
+
*/
|
|
467
722
|
set: (index) => {
|
|
468
723
|
const byte = index >>> BYTE_SHIFT;
|
|
469
724
|
const mask = 1 << (index & BYTE_MASK);
|
|
@@ -472,6 +727,11 @@ var createBitset = (size) => {
|
|
|
472
727
|
count += 1;
|
|
473
728
|
return true;
|
|
474
729
|
},
|
|
730
|
+
/**
|
|
731
|
+
* Head/middle/tail: OR a partial mask into the first byte, memset the
|
|
732
|
+
* whole bytes in between, OR a partial mask into the last. `end - 1` is
|
|
733
|
+
* the last included bit, so `toBit` is its offset plus one.
|
|
734
|
+
*/
|
|
475
735
|
fill: (start, end) => {
|
|
476
736
|
if (end <= start) return;
|
|
477
737
|
const firstByte = start >>> BYTE_SHIFT;
|
|
@@ -486,6 +746,7 @@ var createBitset = (size) => {
|
|
|
486
746
|
bytes.fill(FULL_BYTE, firstByte + 1, lastByte);
|
|
487
747
|
bytes[lastByte] |= maskRange(0, toBit);
|
|
488
748
|
},
|
|
749
|
+
// Same head/middle/tail shape as fill, with the masks inverted.
|
|
489
750
|
clearRange: (start, end) => {
|
|
490
751
|
if (end <= start) return;
|
|
491
752
|
const firstByte = start >>> BYTE_SHIFT;
|
|
@@ -500,6 +761,8 @@ var createBitset = (size) => {
|
|
|
500
761
|
bytes.fill(0, firstByte + 1, lastByte);
|
|
501
762
|
bytes[lastByte] &= ~maskRange(0, toBit);
|
|
502
763
|
},
|
|
764
|
+
// Head and tail are masked before counting; the middle bytes are counted
|
|
765
|
+
// whole, one table lookup each.
|
|
503
766
|
countRange: (start, end) => {
|
|
504
767
|
if (end <= start) return 0;
|
|
505
768
|
const firstByte = start >>> BYTE_SHIFT;
|
|
@@ -518,6 +781,11 @@ var createBitset = (size) => {
|
|
|
518
781
|
bytes.fill(0);
|
|
519
782
|
count = 0;
|
|
520
783
|
},
|
|
784
|
+
/**
|
|
785
|
+
* fill and clearRange do not maintain `count`, because recounting a
|
|
786
|
+
* million-bit span per call would defeat the point. Callers that use them
|
|
787
|
+
* compute the delta themselves and write the total back here.
|
|
788
|
+
*/
|
|
521
789
|
setCount: (next) => {
|
|
522
790
|
count = next;
|
|
523
791
|
},
|
|
@@ -527,70 +795,6 @@ var createBitset = (size) => {
|
|
|
527
795
|
};
|
|
528
796
|
};
|
|
529
797
|
|
|
530
|
-
// src/bins.ts
|
|
531
|
-
var clipToView = (rawStart, rawEnd, viewStart, viewEnd) => ({
|
|
532
|
-
start: Math.max(rawStart, viewStart),
|
|
533
|
-
end: Math.min(rawEnd, viewEnd)
|
|
534
|
-
});
|
|
535
|
-
var createBins = (view, binCount) => {
|
|
536
|
-
const { start: viewStart, end: viewEnd } = view;
|
|
537
|
-
const length = viewEnd - viewStart;
|
|
538
|
-
const counts = new Uint32Array(binCount);
|
|
539
|
-
const binOf = (index) => Math.floor((index - viewStart) * binCount / length);
|
|
540
|
-
const rangeOf = (bin) => ({
|
|
541
|
-
start: viewStart + Math.ceil(bin * length / binCount),
|
|
542
|
-
end: viewStart + Math.ceil((bin + 1) * length / binCount)
|
|
543
|
-
});
|
|
544
|
-
const sizeOf = (bin) => {
|
|
545
|
-
const { start, end } = rangeOf(bin);
|
|
546
|
-
return end - start;
|
|
547
|
-
};
|
|
548
|
-
return {
|
|
549
|
-
view,
|
|
550
|
-
binCount,
|
|
551
|
-
counts,
|
|
552
|
-
binOf,
|
|
553
|
-
rangeOf,
|
|
554
|
-
sizeOf,
|
|
555
|
-
rebuild: (countInRange) => {
|
|
556
|
-
counts.fill(0);
|
|
557
|
-
if (binCount === 0) return;
|
|
558
|
-
for (let bin = 0; bin < binCount; bin += 1) {
|
|
559
|
-
const { start, end } = rangeOf(bin);
|
|
560
|
-
counts[bin] = countInRange(start, end);
|
|
561
|
-
}
|
|
562
|
-
},
|
|
563
|
-
addRange: (rawStart, rawEnd) => {
|
|
564
|
-
const { start, end } = clipToView(rawStart, rawEnd, viewStart, viewEnd);
|
|
565
|
-
if (binCount === 0 || end <= start) return;
|
|
566
|
-
const first = binOf(start);
|
|
567
|
-
const last = binOf(end - 1);
|
|
568
|
-
if (first === last) {
|
|
569
|
-
counts[first] += end - start;
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
counts[first] += rangeOf(first).end - start;
|
|
573
|
-
for (let bin = first + 1; bin < last; bin += 1) counts[bin] += sizeOf(bin);
|
|
574
|
-
counts[last] += end - rangeOf(last).start;
|
|
575
|
-
},
|
|
576
|
-
binsTouching: (rawStart, rawEnd) => {
|
|
577
|
-
const { start, end } = clipToView(rawStart, rawEnd, viewStart, viewEnd);
|
|
578
|
-
if (binCount === 0 || end <= start) return [];
|
|
579
|
-
const first = binOf(start);
|
|
580
|
-
const last = binOf(end - 1);
|
|
581
|
-
const touched = [];
|
|
582
|
-
for (let bin = first; bin <= last; bin += 1) touched.push(bin);
|
|
583
|
-
return touched;
|
|
584
|
-
},
|
|
585
|
-
recountBins: (bins, countInRange) => {
|
|
586
|
-
for (const bin of bins) {
|
|
587
|
-
const range = rangeOf(bin);
|
|
588
|
-
counts[bin] = countInRange(range.start, range.end);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
};
|
|
593
|
-
|
|
594
798
|
// src/ranges.ts
|
|
595
799
|
var isFlatPairs = (input) => ArrayBuffer.isView(input) || typeof input[0] === "number";
|
|
596
800
|
var readPairs = (input) => {
|
|
@@ -689,22 +893,23 @@ var createBlockState = (totalBlocks) => {
|
|
|
689
893
|
get binCount() {
|
|
690
894
|
return bins.binCount;
|
|
691
895
|
},
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
},
|
|
896
|
+
// Column count changed: the bin boundaries move, so rebuild from the
|
|
897
|
+
// bitset rather than trying to redistribute the old counts.
|
|
695
898
|
resize: (binCount) => {
|
|
696
899
|
bins = createBins(view, binCount);
|
|
697
900
|
bins.rebuild(bitset.countRange);
|
|
698
901
|
},
|
|
902
|
+
// Takes a view legalView has already checked, so the façade validates once
|
|
903
|
+
// per request rather than again at commit. Returns whether anything moved,
|
|
904
|
+
// so callers can skip a repaint.
|
|
699
905
|
setView: (next) => {
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
if (sameView(clamped, view)) return false;
|
|
703
|
-
view = clamped;
|
|
906
|
+
if (sameView(next, view)) return false;
|
|
907
|
+
view = { ...next };
|
|
704
908
|
bins = createBins(view, bins.binCount);
|
|
705
909
|
bins.rebuild(bitset.countRange);
|
|
706
910
|
return true;
|
|
707
911
|
},
|
|
912
|
+
// A copy, because `view` is live and handed to external callbacks.
|
|
708
913
|
getView: () => ({ ...view }),
|
|
709
914
|
add,
|
|
710
915
|
replace: (indices) => {
|
|
@@ -712,6 +917,11 @@ var createBlockState = (totalBlocks) => {
|
|
|
712
917
|
bins.counts.fill(0);
|
|
713
918
|
add(indices);
|
|
714
919
|
},
|
|
920
|
+
/**
|
|
921
|
+
* Ranges are already merged and disjoint, so each one's blocks are new to
|
|
922
|
+
* the bitset: bins.addRange can add sizes arithmetically and the total is
|
|
923
|
+
* just the summed range lengths, with no counting pass.
|
|
924
|
+
*/
|
|
715
925
|
replaceRanges: (input) => {
|
|
716
926
|
const flat = normalizeRanges(input, totalBlocks);
|
|
717
927
|
bitset.clear();
|
|
@@ -725,12 +935,12 @@ var createBlockState = (totalBlocks) => {
|
|
|
725
935
|
addRanges: (input) => applyRanges(input, true),
|
|
726
936
|
removeRanges: (input) => applyRanges(input, false),
|
|
727
937
|
has: bitset.has,
|
|
938
|
+
// Coverage in [0, 1], which is what the colour scale is indexed by.
|
|
728
939
|
fraction: (bin) => {
|
|
729
940
|
const size = bins.sizeOf(bin);
|
|
730
941
|
return size === 0 ? 0 : bins.counts[bin] / size;
|
|
731
942
|
},
|
|
732
943
|
countOf: (bin) => bins.counts[bin],
|
|
733
|
-
sizeOf: (bin) => bins.sizeOf(bin),
|
|
734
944
|
rangeOf: (bin) => bins.rangeOf(bin),
|
|
735
945
|
progress: () => ({ completed: bitset.count, total: totalBlocks })
|
|
736
946
|
};
|
|
@@ -760,9 +970,9 @@ var formatPercent = (count, size) => {
|
|
|
760
970
|
}
|
|
761
971
|
return `${text}%`;
|
|
762
972
|
};
|
|
763
|
-
var formatBinTooltip = (
|
|
764
|
-
const size =
|
|
765
|
-
const label = size === 1 ? `Block ${countFormat.format(
|
|
973
|
+
var formatBinTooltip = ([start, end], count) => {
|
|
974
|
+
const size = end - start;
|
|
975
|
+
const label = size === 1 ? `Block ${countFormat.format(start)}` : `Blocks ${countFormat.format(start)}${EN_DASH}${countFormat.format(end - 1)}`;
|
|
766
976
|
return `${label}${SEPARATOR}${countFormat.format(count)} / ${countFormat.format(size)} (${formatPercent(count, size)})`;
|
|
767
977
|
};
|
|
768
978
|
var formatBlockTooltip = (index, complete) => `Block ${countFormat.format(index)}${SEPARATOR}${complete ? "complete" : "missing"}`;
|
|
@@ -781,6 +991,8 @@ var createTooltip = (container) => {
|
|
|
781
991
|
ensureRelative(container);
|
|
782
992
|
container.append(el);
|
|
783
993
|
return {
|
|
994
|
+
// x is in container-relative CSS pixels; the offset keeps the label from
|
|
995
|
+
// sitting directly under the cursor.
|
|
784
996
|
show: (x, text) => {
|
|
785
997
|
el.textContent = text;
|
|
786
998
|
el.style.left = `${x + TOOLTIP_OFFSET_PX}px`;
|
|
@@ -799,11 +1011,20 @@ var createTooltip = (container) => {
|
|
|
799
1011
|
var FULL2 = "100%";
|
|
800
1012
|
var CONTEXT_ID = "2d";
|
|
801
1013
|
var NO_FRAME2 = 0;
|
|
1014
|
+
var LOCALE2 = "en-US";
|
|
1015
|
+
var ARIA_LABEL_ATTRIBUTE = "aria-label";
|
|
1016
|
+
var numberFormat = new Intl.NumberFormat(LOCALE2);
|
|
1017
|
+
var defaultLabel = (progress) => {
|
|
1018
|
+
const percent = Math.floor(progress.completed / progress.total * PERCENT).toFixed(
|
|
1019
|
+
ARIA_LABEL_DEFAULT_PERCENT_DIGITS
|
|
1020
|
+
);
|
|
1021
|
+
return `Download progress: ${numberFormat.format(progress.completed)} of ${numberFormat.format(progress.total)} blocks (${percent}%)`;
|
|
1022
|
+
};
|
|
802
1023
|
var dprQuery = (dpr) => `(resolution: ${dpr}dppx)`;
|
|
803
|
-
var discreteBlock = (column, length, binCount, start) => start + Math.ceil((column + 1) * length / binCount) - 1;
|
|
804
1024
|
var createBlockMap = (container, options) => {
|
|
805
1025
|
const state = createBlockState(options.totalBlocks);
|
|
806
|
-
|
|
1026
|
+
const initialView = options.view && legalView(options.view, state.totalBlocks);
|
|
1027
|
+
if (initialView) state.setView(initialView);
|
|
807
1028
|
const canvas = document.createElement("canvas");
|
|
808
1029
|
Object.assign(canvas.style, {
|
|
809
1030
|
display: "block",
|
|
@@ -812,6 +1033,12 @@ var createBlockMap = (container, options) => {
|
|
|
812
1033
|
touchAction: "none"
|
|
813
1034
|
});
|
|
814
1035
|
canvas.height = CANVAS_ROWS;
|
|
1036
|
+
canvas.setAttribute("role", "img");
|
|
1037
|
+
canvas.setAttribute("aria-roledescription", ARIA_ROLEDESCRIPTION);
|
|
1038
|
+
if (options.interactive !== false) {
|
|
1039
|
+
canvas.tabIndex = 0;
|
|
1040
|
+
canvas.setAttribute("aria-keyshortcuts", ARIA_KEYSHORTCUTS);
|
|
1041
|
+
}
|
|
815
1042
|
container.append(canvas);
|
|
816
1043
|
const ctx = canvas.getContext(CONTEXT_ID);
|
|
817
1044
|
if (!ctx) {
|
|
@@ -819,53 +1046,77 @@ var createBlockMap = (container, options) => {
|
|
|
819
1046
|
throw new Error(`Could not acquire a ${CONTEXT_ID} canvas context`);
|
|
820
1047
|
}
|
|
821
1048
|
const renderer = createRenderer(ctx, state);
|
|
822
|
-
const tooltip = createTooltip(container);
|
|
823
|
-
const axisOptions = options.axis === true ? {} : options.axis === false || options.axis === void 0 ? void 0 : options.axis;
|
|
824
|
-
const axis = axisOptions ? createAxis(container, axisOptions) : void 0;
|
|
825
1049
|
let destroyed = false;
|
|
826
1050
|
let cssWidth = 0;
|
|
1051
|
+
let cssHeight = 0;
|
|
827
1052
|
let dpr = devicePixelRatio;
|
|
828
1053
|
let dprMedia;
|
|
829
1054
|
let theme = readTheme(container);
|
|
830
1055
|
let highlightOn = false;
|
|
831
1056
|
let dragging = false;
|
|
832
|
-
|
|
1057
|
+
let downX = Number.NaN;
|
|
1058
|
+
const overlay = options.brush === true ? createBrushOverlay(container, {
|
|
1059
|
+
getView: () => state.getView(),
|
|
1060
|
+
getWidth: () => cssWidth,
|
|
1061
|
+
getHeight: () => cssHeight,
|
|
1062
|
+
getTotalBlocks: () => state.totalBlocks,
|
|
1063
|
+
onBrush: (selection) => options.onBrush?.(selection),
|
|
1064
|
+
onBrushEnd: (selection) => options.onBrushEnd?.(selection)
|
|
1065
|
+
}) : void 0;
|
|
1066
|
+
const tooltip = createTooltip(container);
|
|
1067
|
+
const axisOptions = options.axis === true ? {} : options.axis === false || options.axis === void 0 ? void 0 : options.axis;
|
|
1068
|
+
const axis = axisOptions ? createAxis(container, axisOptions) : void 0;
|
|
1069
|
+
const motionMedia = matchMedia(REDUCED_MOTION_QUERY);
|
|
1070
|
+
const blinkAllowed = () => !motionMedia.matches;
|
|
833
1071
|
const applyHighlight = () => {
|
|
834
1072
|
renderer.setHighlight(highlightOn, highlightColor(theme), blinkAllowed());
|
|
835
1073
|
};
|
|
1074
|
+
const onMotionChange = () => {
|
|
1075
|
+
if (highlightOn) applyHighlight();
|
|
1076
|
+
};
|
|
1077
|
+
motionMedia.addEventListener("change", onMotionChange);
|
|
836
1078
|
const applyTheme = () => {
|
|
837
1079
|
theme = readTheme(container);
|
|
838
1080
|
renderer.setColors(createColorScale(theme, options.partial));
|
|
839
1081
|
if (highlightOn) renderer.setHighlightColor(highlightColor(theme));
|
|
1082
|
+
overlay?.setColor(theme.brush);
|
|
1083
|
+
};
|
|
1084
|
+
const writeLabel = () => {
|
|
1085
|
+
canvas.setAttribute(ARIA_LABEL_ATTRIBUTE, (options.label ?? defaultLabel)(state.progress()));
|
|
1086
|
+
};
|
|
1087
|
+
const updateChrome = () => {
|
|
1088
|
+
axis?.update(state.getView(), cssWidth);
|
|
1089
|
+
overlay?.update();
|
|
840
1090
|
};
|
|
841
1091
|
let pendingView;
|
|
842
1092
|
let viewFrame = NO_FRAME2;
|
|
843
|
-
const getView = () => ({ ...pendingView ?? state.
|
|
1093
|
+
const getView = () => ({ ...pendingView ?? state.getView() });
|
|
844
1094
|
const commitView = () => {
|
|
845
1095
|
viewFrame = NO_FRAME2;
|
|
846
1096
|
const next = pendingView;
|
|
847
1097
|
pendingView = void 0;
|
|
848
1098
|
if (!next || !state.setView(next)) return;
|
|
849
1099
|
renderer.markAllDirty();
|
|
850
|
-
|
|
1100
|
+
updateChrome();
|
|
851
1101
|
options.onViewChange?.(state.getView());
|
|
852
1102
|
};
|
|
853
1103
|
const setView = (view) => {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
if (!hasPendingFrame) viewFrame = requestAnimationFrame(commitView);
|
|
1104
|
+
const legal = legalView(view, state.totalBlocks);
|
|
1105
|
+
if (!legal || sameView(legal, pendingView ?? state.getView())) return;
|
|
1106
|
+
pendingView = legal;
|
|
1107
|
+
if (viewFrame !== NO_FRAME2) return;
|
|
1108
|
+
viewFrame = requestAnimationFrame(commitView);
|
|
860
1109
|
};
|
|
861
1110
|
const resetView = () => setView(fullView(state.totalBlocks));
|
|
862
1111
|
const layout = () => {
|
|
1112
|
+
if (cssWidth === 0) return;
|
|
863
1113
|
const binCount = Math.round(cssWidth * dpr);
|
|
864
|
-
if (binCount
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1114
|
+
if (binCount !== state.binCount) {
|
|
1115
|
+
canvas.width = binCount;
|
|
1116
|
+
state.resize(binCount);
|
|
1117
|
+
renderer.markAllDirty();
|
|
1118
|
+
}
|
|
1119
|
+
updateChrome();
|
|
869
1120
|
};
|
|
870
1121
|
const onDprChange = () => {
|
|
871
1122
|
dpr = devicePixelRatio;
|
|
@@ -881,34 +1132,70 @@ var createBlockMap = (container, options) => {
|
|
|
881
1132
|
const entry = entries[0];
|
|
882
1133
|
if (!entry) return;
|
|
883
1134
|
cssWidth = entry.contentRect.width;
|
|
1135
|
+
cssHeight = entry.contentRect.height;
|
|
884
1136
|
layout();
|
|
885
1137
|
});
|
|
886
1138
|
resizeObserver.observe(container);
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
if (
|
|
891
|
-
const column = Math.min(binCount - 1, Math.max(0, Math.floor(
|
|
1139
|
+
const locate = (x) => {
|
|
1140
|
+
const { binCount } = state;
|
|
1141
|
+
if (binCount === 0) return void 0;
|
|
1142
|
+
if (x < 0 || x >= cssWidth) return void 0;
|
|
1143
|
+
const column = Math.min(binCount - 1, Math.max(0, Math.floor(x * dpr)));
|
|
1144
|
+
const view = state.getView();
|
|
892
1145
|
const length = viewLength(view);
|
|
893
|
-
let text;
|
|
894
1146
|
if (length < binCount) {
|
|
895
|
-
const block = discreteBlock(column, length, binCount
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1147
|
+
const block = view.start + discreteBlock(column, length, binCount);
|
|
1148
|
+
return {
|
|
1149
|
+
block,
|
|
1150
|
+
range: [block, block + 1],
|
|
1151
|
+
completed: state.has(block) ? 1 : 0,
|
|
1152
|
+
total: 1
|
|
1153
|
+
};
|
|
899
1154
|
}
|
|
900
|
-
|
|
1155
|
+
const [start, end] = state.rangeOf(column);
|
|
1156
|
+
return {
|
|
1157
|
+
block: start,
|
|
1158
|
+
range: [start, end],
|
|
1159
|
+
completed: state.countOf(column),
|
|
1160
|
+
total: end - start
|
|
1161
|
+
};
|
|
1162
|
+
};
|
|
1163
|
+
const onPointerMove = (event) => {
|
|
1164
|
+
if (dragging) return;
|
|
1165
|
+
const x = canvasX(canvas, event);
|
|
1166
|
+
const hit = locate(x);
|
|
1167
|
+
if (!hit) {
|
|
1168
|
+
tooltip.hide();
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const text = viewLength(state.getView()) < state.binCount ? formatBlockTooltip(hit.block, hit.completed === 1) : formatBinTooltip(hit.range, hit.completed);
|
|
1172
|
+
tooltip.show(x, text);
|
|
901
1173
|
};
|
|
902
1174
|
const onPointerLeave = () => tooltip.hide();
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1175
|
+
const onPointerDown = (event) => {
|
|
1176
|
+
downX = event.clientX;
|
|
1177
|
+
};
|
|
1178
|
+
const onClick = (event) => {
|
|
1179
|
+
const travelled = Math.abs(event.clientX - downX);
|
|
1180
|
+
downX = Number.NaN;
|
|
1181
|
+
if (travelled >= DRAG_THRESHOLD_PX) return;
|
|
1182
|
+
if (!options.onBlockClick) return;
|
|
1183
|
+
const hit = locate(canvasX(canvas, event));
|
|
1184
|
+
if (!hit) return;
|
|
1185
|
+
options.onBlockClick({ ...hit, originalEvent: event });
|
|
1186
|
+
};
|
|
1187
|
+
container.addEventListener("pointermove", onPointerMove);
|
|
1188
|
+
container.addEventListener("pointerleave", onPointerLeave);
|
|
1189
|
+
container.addEventListener("pointerdown", onPointerDown);
|
|
1190
|
+
container.addEventListener("click", onClick);
|
|
1191
|
+
const detachInteraction = options.interactive === false ? void 0 : attachInteraction(container, canvas, {
|
|
906
1192
|
getView,
|
|
907
1193
|
setView,
|
|
908
1194
|
resetView,
|
|
909
1195
|
getBinCount: () => state.binCount,
|
|
910
1196
|
getDpr: () => dpr,
|
|
911
1197
|
getTotalBlocks: () => state.totalBlocks,
|
|
1198
|
+
panRequiresShift: () => overlay !== void 0,
|
|
912
1199
|
onDragStart: () => {
|
|
913
1200
|
dragging = true;
|
|
914
1201
|
tooltip.hide();
|
|
@@ -922,47 +1209,54 @@ var createBlockMap = (container, options) => {
|
|
|
922
1209
|
applyTheme();
|
|
923
1210
|
if (options.initialRanges) state.replaceRanges(options.initialRanges);
|
|
924
1211
|
if (options.initialBlocks) renderer.markDirty(state.add(options.initialBlocks));
|
|
1212
|
+
writeLabel();
|
|
1213
|
+
const live = (method) => (...args) => {
|
|
1214
|
+
if (!destroyed) method(...args);
|
|
1215
|
+
};
|
|
925
1216
|
return {
|
|
926
|
-
setBlocks: (indices) => {
|
|
927
|
-
if (destroyed) return;
|
|
1217
|
+
setBlocks: live((indices) => {
|
|
928
1218
|
state.replace(indices);
|
|
929
1219
|
renderer.markAllDirty();
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1220
|
+
writeLabel();
|
|
1221
|
+
}),
|
|
1222
|
+
setRanges: live((ranges) => {
|
|
933
1223
|
state.replaceRanges(ranges);
|
|
934
1224
|
renderer.markAllDirty();
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1225
|
+
writeLabel();
|
|
1226
|
+
}),
|
|
1227
|
+
// The incremental paths repaint only the bins the state reports changed.
|
|
1228
|
+
addBlocks: live((indices) => {
|
|
938
1229
|
renderer.markDirty(state.add(indices));
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1230
|
+
writeLabel();
|
|
1231
|
+
}),
|
|
1232
|
+
addRanges: live((ranges) => {
|
|
942
1233
|
renderer.markDirty(state.addRanges(ranges));
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1234
|
+
writeLabel();
|
|
1235
|
+
}),
|
|
1236
|
+
removeRanges: live((ranges) => {
|
|
946
1237
|
renderer.markDirty(state.removeRanges(ranges));
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
},
|
|
952
|
-
resetView: () => {
|
|
953
|
-
if (destroyed) return;
|
|
954
|
-
resetView();
|
|
955
|
-
},
|
|
1238
|
+
writeLabel();
|
|
1239
|
+
}),
|
|
1240
|
+
setView: live(setView),
|
|
1241
|
+
resetView: live(resetView),
|
|
956
1242
|
getView,
|
|
1243
|
+
// Built from the committed view, so the scale always matches what is on
|
|
1244
|
+
// screen rather than a change still waiting for its frame.
|
|
957
1245
|
getScale: () => createScale(state.getView(), cssWidth),
|
|
958
|
-
|
|
959
|
-
|
|
1246
|
+
// Selections are only meaningful while there is an overlay to draw them:
|
|
1247
|
+
// without one setBrush has nothing to store and getBrush nothing to
|
|
1248
|
+
// report. A destroyed map reports none either, rather than whatever the
|
|
1249
|
+
// detached overlay was last holding.
|
|
1250
|
+
setBrush: live((selection) => overlay?.setSelection(selection)),
|
|
1251
|
+
getBrush: () => destroyed ? null : overlay?.getSelection() ?? null,
|
|
1252
|
+
setHighlight: live((on) => {
|
|
960
1253
|
highlightOn = on;
|
|
961
1254
|
applyHighlight();
|
|
962
|
-
},
|
|
1255
|
+
}),
|
|
963
1256
|
getProgress: () => destroyed ? { completed: 0, total: state.totalBlocks } : state.progress(),
|
|
964
|
-
|
|
965
|
-
|
|
1257
|
+
// Idempotent, and unwinds in the reverse order of construction: pending
|
|
1258
|
+
// frame, observers, listeners, owned children, then the canvas itself.
|
|
1259
|
+
destroy: live(() => {
|
|
966
1260
|
destroyed = true;
|
|
967
1261
|
if (viewFrame !== NO_FRAME2) cancelAnimationFrame(viewFrame);
|
|
968
1262
|
viewFrame = NO_FRAME2;
|
|
@@ -970,18 +1264,25 @@ var createBlockMap = (container, options) => {
|
|
|
970
1264
|
resizeObserver.disconnect();
|
|
971
1265
|
unwatchTheme();
|
|
972
1266
|
dprMedia?.removeEventListener("change", onDprChange);
|
|
973
|
-
|
|
974
|
-
|
|
1267
|
+
motionMedia.removeEventListener("change", onMotionChange);
|
|
1268
|
+
container.removeEventListener("pointermove", onPointerMove);
|
|
1269
|
+
container.removeEventListener("pointerleave", onPointerLeave);
|
|
1270
|
+
container.removeEventListener("pointerdown", onPointerDown);
|
|
1271
|
+
container.removeEventListener("click", onClick);
|
|
975
1272
|
detachInteraction?.();
|
|
976
1273
|
renderer.destroy();
|
|
977
1274
|
tooltip.destroy();
|
|
978
1275
|
axis?.destroy();
|
|
1276
|
+
overlay?.destroy();
|
|
979
1277
|
canvas.remove();
|
|
980
|
-
}
|
|
1278
|
+
})
|
|
981
1279
|
};
|
|
982
1280
|
};
|
|
983
1281
|
|
|
984
1282
|
export {
|
|
1283
|
+
fullView,
|
|
1284
|
+
createScale,
|
|
1285
|
+
defaultLabel,
|
|
985
1286
|
createBlockMap
|
|
986
1287
|
};
|
|
987
|
-
//# sourceMappingURL=chunk-
|
|
1288
|
+
//# sourceMappingURL=chunk-6BBJWE5O.js.map
|