@mosaicajs/deck 0.3.0 → 0.4.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/dist/colour.d.ts +16 -3
- package/dist/colour.js +13 -3
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/layer.d.ts +53 -32
- package/dist/layer.js +90 -53
- package/package.json +2 -2
package/dist/colour.d.ts
CHANGED
|
@@ -11,8 +11,17 @@ import type { CategoryValue, Ranks, ScalarColumn } from '@mosaicajs/client';
|
|
|
11
11
|
type Rgba = readonly [number, number, number, number];
|
|
12
12
|
/** An RGB colour, each channel 0–255. */
|
|
13
13
|
export type Rgb = readonly [number, number, number];
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The colour of every mark when no column is encoded, per ground: the theme's secondary ink on a
|
|
16
|
+
* light ground and a pale blue on a dark one. Against its ground's default background (`#f6f6f4`
|
|
17
|
+
* and `#111317`) the grey stands 3.8:1 at its own alpha and 2.8:1 at the alpha a mark is
|
|
18
|
+
* composited at while ten thousand or fewer are drawn, and the blue 6.2:1 and 4.4:1. Past that
|
|
19
|
+
* the marks turn more translucent and are read as a density, where they overlap.
|
|
20
|
+
*
|
|
21
|
+
* The grey is close to {@link UNMAPPED}, which is drawn only while a column colours the marks, so
|
|
22
|
+
* the two are never drawn together.
|
|
23
|
+
*/
|
|
24
|
+
export declare const UNIFORM: Record<'light' | 'dark', Rgba>;
|
|
16
25
|
/**
|
|
17
26
|
* Everything unmappable: the absent sentinel (code 0), a number with no value, a code no key
|
|
18
27
|
* explains, and any value past the palette's end. A low-saturation grey that reads as "no value"
|
|
@@ -116,9 +125,13 @@ export type Colouring = {
|
|
|
116
125
|
* @category Colour
|
|
117
126
|
*/
|
|
118
127
|
export declare const DEFAULT_COLOURING: Colouring;
|
|
119
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* How a column is being coloured. `uniform` is the default and the fallback for anything unmapped:
|
|
130
|
+
* one colour, chosen for the ground the marks are drawn on.
|
|
131
|
+
*/
|
|
120
132
|
export type Encoding = {
|
|
121
133
|
kind: 'uniform';
|
|
134
|
+
ground: 'light' | 'dark';
|
|
122
135
|
} | {
|
|
123
136
|
kind: 'unmapped';
|
|
124
137
|
} | {
|
package/dist/colour.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { hasValue, numericValues, rankedValues } from '@mosaicajs/client/internal';
|
|
2
2
|
const ALPHA = 200;
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The colour of every mark when no column is encoded, per ground: the theme's secondary ink on a
|
|
5
|
+
* light ground and a pale blue on a dark one. Against its ground's default background (`#f6f6f4`
|
|
6
|
+
* and `#111317`) the grey stands 3.8:1 at its own alpha and 2.8:1 at the alpha a mark is
|
|
7
|
+
* composited at while ten thousand or fewer are drawn, and the blue 6.2:1 and 4.4:1. Past that
|
|
8
|
+
* the marks turn more translucent and are read as a density, where they overlap.
|
|
9
|
+
*
|
|
10
|
+
* The grey is close to {@link UNMAPPED}, which is drawn only while a column colours the marks, so
|
|
11
|
+
* the two are never drawn together.
|
|
12
|
+
*/
|
|
13
|
+
export const UNIFORM = { light: [86, 92, 100, ALPHA], dark: [120, 190, 255, ALPHA] };
|
|
5
14
|
/**
|
|
6
15
|
* Everything unmappable: the absent sentinel (code 0), a number with no value, a code no key
|
|
7
16
|
* explains, and any value past the palette's end. A low-saturation grey that reads as "no value"
|
|
@@ -68,6 +77,7 @@ export const DEFAULT_COLOURING = { palette: 'tableau10', ramp: 'viridis', scale:
|
|
|
68
77
|
export function encodingSignature(encoding) {
|
|
69
78
|
switch (encoding.kind) {
|
|
70
79
|
case 'uniform':
|
|
80
|
+
return `uniform|${encoding.ground}`;
|
|
71
81
|
case 'unmapped':
|
|
72
82
|
return encoding.kind;
|
|
73
83
|
case 'category': {
|
|
@@ -158,7 +168,7 @@ export function lighter(c, amount) {
|
|
|
158
168
|
export function writeColours(out, offset, count, column, encoding) {
|
|
159
169
|
const out32 = new Uint32Array(out.buffer, out.byteOffset + offset * 4, count);
|
|
160
170
|
if (encoding.kind === 'uniform' || encoding.kind === 'unmapped') {
|
|
161
|
-
out32.fill(packRgba(encoding.kind === 'uniform' ? UNIFORM : UNMAPPED));
|
|
171
|
+
out32.fill(packRgba(encoding.kind === 'uniform' ? UNIFORM[encoding.ground] : UNMAPPED));
|
|
162
172
|
return;
|
|
163
173
|
}
|
|
164
174
|
if (!column) {
|
package/dist/internal.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `@mosaicajs/deck/internal`: what `@mosaicajs/components` uses of this package beyond the root
|
|
3
3
|
* entry. It is not a public API, and its exports change with the components.
|
|
4
4
|
*/
|
|
5
|
-
export { clusterLayerOf, contourShapes, drawnLevel, encodingOf, type MosaicaLayerInternalProps } from './layer.js';
|
|
5
|
+
export { clusterLayerOf, colourLayerOf, contourShapes, drawnLevel, drawnLevels, encodingOf, levelsKey, type MosaicaLayerInternalProps } from './layer.js';
|
|
6
6
|
export { MarkSlab } from './slab.js';
|
|
7
7
|
export { artifactOfMark } from './pick.js';
|
|
8
8
|
export { UNMAPPED, colourOfFraction, colourOfRank, css, encodingSignature, fractionOf, hexOf, lighter, paletteValues, rampAt, rgbOfHex, valueAtFraction } from './colour.js';
|
package/dist/internal.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `@mosaicajs/deck/internal`: what `@mosaicajs/components` uses of this package beyond the root
|
|
3
3
|
* entry. It is not a public API, and its exports change with the components.
|
|
4
4
|
*/
|
|
5
|
-
export { clusterLayerOf, contourShapes, drawnLevel, encodingOf } from './layer.js';
|
|
5
|
+
export { clusterLayerOf, colourLayerOf, contourShapes, drawnLevel, drawnLevels, encodingOf, levelsKey } from './layer.js';
|
|
6
6
|
export { MarkSlab } from './slab.js';
|
|
7
7
|
export { artifactOfMark } from './pick.js';
|
|
8
8
|
export { UNMAPPED, colourOfFraction, colourOfRank, css, encodingSignature, fractionOf, hexOf, lighter, paletteValues, rampAt, rgbOfHex, valueAtFraction } from './colour.js';
|
package/dist/layer.d.ts
CHANGED
|
@@ -44,16 +44,20 @@ export type MosaicaLayerProps = CompositeLayerProps & {
|
|
|
44
44
|
*/
|
|
45
45
|
status?: PresentedStatus;
|
|
46
46
|
/**
|
|
47
|
-
* The level
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* The level to colour, name and outline each `stacked` or `tiered` artifact layer at, keyed by
|
|
48
|
+
* layer name. A layer with no entry is drawn at its entry in the artifacts projection's
|
|
49
|
+
* `followedLevels`: where its levels declare no zoom range, the finest level that is not crowded
|
|
50
|
+
* in view, which deepens as the map zooms in, and otherwise the coarsest level the camera's zoom
|
|
51
|
+
* serves. A `nested` or `dag` layer takes no level, whatever its entry, and each mark takes the
|
|
52
|
+
* nearest artifact served.
|
|
50
53
|
*/
|
|
51
|
-
|
|
54
|
+
clusterLevels?: ReadonlyMap<string, number>;
|
|
52
55
|
/** Whether to draw the artifacts' names, with the count of the one under the pointer. Defaults to `true`. */
|
|
53
56
|
labels?: boolean;
|
|
54
57
|
/**
|
|
55
58
|
* Whether the map's background is `light` or `dark`. The colours of names, outlines, the
|
|
56
|
-
* picked mark's ring
|
|
59
|
+
* picked mark's ring, the selected region and the points while no column colours them are
|
|
60
|
+
* chosen to suit it. Defaults to `dark`.
|
|
57
61
|
*/
|
|
58
62
|
scheme?: 'light' | 'dark';
|
|
59
63
|
/**
|
|
@@ -190,20 +194,28 @@ export type LayerTimings = {
|
|
|
190
194
|
};
|
|
191
195
|
/** A layer's colouring, as the store's `colourBy` names it: `cluster:<layer>` or a column. */
|
|
192
196
|
export declare function clusterLayerOf(colourBy: string | null | undefined): string | null;
|
|
197
|
+
/** The layer marks are coloured from: the layer coloured by, else the first layer drawn. */
|
|
198
|
+
export declare function colourLayerOf(artifacts: Pick<ArtifactsProjection, 'layers'> | null, colourBy: string | null | undefined): string | undefined;
|
|
193
199
|
/**
|
|
194
|
-
* The level
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
200
|
+
* The level the layer `name` is coloured, named and outlined at, where it is `stacked` or `tiered`:
|
|
201
|
+
* its entry in `chosen` where the layer declares that level, else its entry in the projection's
|
|
202
|
+
* `followedLevels`: the finest level not crowded in view where no level declares a zoom range,
|
|
203
|
+
* else the coarsest served at the camera's zoom. On a layer of any other kind, or one missing from
|
|
204
|
+
* the roster, `undefined`: each point takes its nearest served cluster.
|
|
198
205
|
*/
|
|
199
|
-
export declare function drawnLevel(meta: Meta | null, artifacts: Pick<ArtifactsProjection, '
|
|
206
|
+
export declare function drawnLevel(meta: Meta | null, artifacts: Pick<ArtifactsProjection, 'followedLevels'> | null, name: string | undefined, chosen: ReadonlyMap<string, number> | undefined): number | undefined;
|
|
207
|
+
/** {@link drawnLevel} for each layer drawn, keyed by name. A layer drawn at no level has no entry. */
|
|
208
|
+
export declare function drawnLevels(meta: Meta | null, artifacts: Pick<ArtifactsProjection, 'layers' | 'followedLevels'> | null, chosen: ReadonlyMap<string, number> | undefined): Map<string, number>;
|
|
209
|
+
/** A cache key for a set of {@link drawnLevels}. */
|
|
210
|
+
export declare function levelsKey(levels: ReadonlyMap<string, number>): string;
|
|
200
211
|
/**
|
|
201
212
|
* The current column colour encoding, from the store's legend, the schema and the colour choices.
|
|
202
|
-
* Uniform where nothing is ready to colour by yet, such as a
|
|
203
|
-
* were counted. Unmapped where `/v1/categories` refused the
|
|
204
|
-
* values could not be named. A chosen value colour applies once
|
|
213
|
+
* Uniform, in the one colour for `ground`, where nothing is ready to colour by yet, such as a
|
|
214
|
+
* column chosen before any of its codes were counted. Unmapped where `/v1/categories` refused the
|
|
215
|
+
* column, so the legend can say the values could not be named. A chosen value colour applies once
|
|
216
|
+
* the legend has named its key.
|
|
205
217
|
*/
|
|
206
|
-
export declare function encodingOf(meta: Meta | null, legend: LegendProjection | null, colouring?: Colouring): Encoding;
|
|
218
|
+
export declare function encodingOf(meta: Meta | null, legend: LegendProjection | null, colouring?: Colouring, ground?: 'light' | 'dark'): Encoding;
|
|
207
219
|
/** What a density image is built for: one set of counts, drawn as a wash or a grid on one scale, in one set of colours on one ground. */
|
|
208
220
|
type WashKey = {
|
|
209
221
|
counts: DensityCounts;
|
|
@@ -255,14 +267,19 @@ export type OutlineDatum = {
|
|
|
255
267
|
export type OutlineOptions = {
|
|
256
268
|
opened: bigint | null;
|
|
257
269
|
hovered: bigint | null;
|
|
258
|
-
level
|
|
270
|
+
/** The level each layer is cut at ({@link drawnLevels}). */
|
|
271
|
+
levels: ReadonlyMap<string, number>;
|
|
259
272
|
scheme: 'light' | 'dark';
|
|
260
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* The layer roster, to leave a dependent layer's artifacts out and to draw a `stacked` layer at
|
|
275
|
+
* its level alone; null draws every served layer.
|
|
276
|
+
*/
|
|
261
277
|
meta?: Meta | null;
|
|
262
278
|
};
|
|
263
|
-
/** The level
|
|
279
|
+
/** The level cuts, the layer roster and the opened artifact, which decide what may be hovered. */
|
|
264
280
|
export type ContourOptions = {
|
|
265
|
-
level
|
|
281
|
+
/** The level each layer is cut at ({@link drawnLevels}). */
|
|
282
|
+
levels: ReadonlyMap<string, number>;
|
|
266
283
|
meta?: Meta | null;
|
|
267
284
|
/** The opened artifact, which may be pointed at whether or not a filter matches it. */
|
|
268
285
|
opened?: bigint | null;
|
|
@@ -314,30 +331,32 @@ export type LabelText = {
|
|
|
314
331
|
topic: string | null;
|
|
315
332
|
};
|
|
316
333
|
/**
|
|
317
|
-
* The artifacts named at `
|
|
334
|
+
* The artifacts named at `levels`: every drawn artifact with no drawn child. An artifact with a
|
|
318
335
|
* drawn child is an ancestor of something on the map and is not labelled.
|
|
319
336
|
*
|
|
320
|
-
*
|
|
321
|
-
* every shallower artifact whose children are all deeper than the
|
|
322
|
-
* above the level is still named.
|
|
323
|
-
*
|
|
324
|
-
*
|
|
337
|
+
* On a layer with no entry in `levels` this is the leaves of its served set. On one with an entry
|
|
338
|
+
* it is that level's artifacts and every shallower artifact whose children are all deeper than the
|
|
339
|
+
* level, so a branch that ends above the level is still named. A `stacked` layer in `meta` has no
|
|
340
|
+
* edges between its levels, so on one with an entry it is that level's artifacts alone. Each layer
|
|
341
|
+
* is cut at its own entry only. The level compared is the served `rung`, which the server computes
|
|
342
|
+
* per layer kind; a levelled layer's edges may skip a level, so a client-side depth count would be
|
|
343
|
+
* wrong.
|
|
325
344
|
*
|
|
326
345
|
* Under a filter, an artifact with no member in view that matches it is left out: none of its
|
|
327
346
|
* points are drawn, so its name and its outline would stand over empty ground. A parent whose
|
|
328
347
|
* children all match nothing is on the frontier in their place.
|
|
329
348
|
*/
|
|
330
|
-
export declare function frontier(a: ArtifactsProjection,
|
|
349
|
+
export declare function frontier(a: ArtifactsProjection, levels: ReadonlyMap<string, number>, opened?: bigint | null, meta?: Meta | null): Set<bigint>;
|
|
331
350
|
/**
|
|
332
351
|
* The label candidates for a served set at `zoom`: the top `budget` frontier artifacts by masked
|
|
333
352
|
* count that have text to draw, each with its name, count, topic and pixel box. An artifact with
|
|
334
353
|
* no text and no attached topic gets no label; its key is an identifier, not a name. Name size
|
|
335
354
|
* comes from {@link labelSize} over the candidates' counts.
|
|
336
355
|
*
|
|
337
|
-
* Only the anchor depends on the zoom, so the list is built once per served set,
|
|
356
|
+
* Only the anchor depends on the zoom, so the list is built once per served set, levels and budget,
|
|
338
357
|
* and each zoom bucket scales the anchors into a copy.
|
|
339
358
|
*/
|
|
340
|
-
export declare function labelCandidates(a: ArtifactsProjection, meta: Meta | null,
|
|
359
|
+
export declare function labelCandidates(a: ArtifactsProjection, meta: Meta | null, levels: ReadonlyMap<string, number>, zoom: number, budget: number, opened?: bigint | null): {
|
|
341
360
|
candidates: LabelCandidate[];
|
|
342
361
|
byId: Map<bigint, LabelText>;
|
|
343
362
|
};
|
|
@@ -379,8 +398,8 @@ export type MosaicaLayerInternalProps = MosaicaLayerProps & {
|
|
|
379
398
|
* to {@link resolvePick} to find the item or artifact under the pointer, then set the props that
|
|
380
399
|
* show it, such as `selectedWorldXY` and `hoveredArtifact`. If the artifact's layer declares
|
|
381
400
|
* shapes, call the store's `needShape` with its id: the outline appears once the shape arrives.
|
|
382
|
-
* Otherwise the outline is the artifact's bounding box. Only an artifact drawn at
|
|
383
|
-
* is outlined.
|
|
401
|
+
* Otherwise the outline is the artifact's bounding box. Only an artifact drawn at its layer's level
|
|
402
|
+
* (`clusterLevels`) is outlined.
|
|
384
403
|
*/
|
|
385
404
|
export declare class MosaicaLayer extends CompositeLayer<MosaicaLayerInternalProps> {
|
|
386
405
|
/** @internal */
|
|
@@ -395,7 +414,7 @@ export declare class MosaicaLayer extends CompositeLayer<MosaicaLayerInternalPro
|
|
|
395
414
|
legend: null;
|
|
396
415
|
status: string;
|
|
397
416
|
slab: null;
|
|
398
|
-
|
|
417
|
+
clusterLevels: undefined;
|
|
399
418
|
labels: boolean;
|
|
400
419
|
scheme: string;
|
|
401
420
|
selectedWorldXY: null;
|
|
@@ -484,8 +503,10 @@ export declare class MosaicaLayer extends CompositeLayer<MosaicaLayerInternalPro
|
|
|
484
503
|
* estimated width plus the fade at both ends. Present from the first paint, empty, like the text.
|
|
485
504
|
*/
|
|
486
505
|
private backingLayer;
|
|
487
|
-
/** The level
|
|
488
|
-
private
|
|
506
|
+
/** The level marks are coloured at: that of the layer they are coloured from ({@link colourLayerOf}). */
|
|
507
|
+
private colourLevel;
|
|
508
|
+
/** The level each drawn layer is cut at ({@link drawnLevels}). */
|
|
509
|
+
private levels;
|
|
489
510
|
/**
|
|
490
511
|
* Whether the labels sit on opaque plates with no halo: while density is drawn as hexagons or a
|
|
491
512
|
* grid, or as a smooth wash in a ramp, whose colours fill the ground under the names.
|
package/dist/layer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CompositeLayer } from '@deck.gl/core';
|
|
2
2
|
import { BitmapLayer, ColumnLayer, IconLayer, LineLayer, PolygonLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers';
|
|
3
3
|
import { CLUSTER_PREFIX, NO_ORDINAL, artifactName, WORLD_SIZE, gridToWorld, gridToWorldXY } from '@mosaicajs/client';
|
|
4
|
-
import { NEUTRAL,
|
|
4
|
+
import { NEUTRAL, levelOfLayer } from '@mosaicajs/client/internal';
|
|
5
5
|
import { materialiseStandIn } from './assemble.js';
|
|
6
6
|
import { BACKING_HEIGHT_EM, backingAspect, backingAtlas, backingIcon } from './backing.js';
|
|
7
7
|
import { DEFAULT_COLOURING, buildColourAttribute, encodingSignature, rgbOfHex } from './colour.js';
|
|
@@ -21,18 +21,34 @@ const LABEL_ZOOM_STEP = 4;
|
|
|
21
21
|
export function clusterLayerOf(colourBy) {
|
|
22
22
|
return colourBy && colourBy.startsWith(CLUSTER_PREFIX) ? colourBy.slice(CLUSTER_PREFIX.length) : null;
|
|
23
23
|
}
|
|
24
|
+
/** The layer marks are coloured from: the layer coloured by, else the first layer drawn. */
|
|
25
|
+
export function colourLayerOf(artifacts, colourBy) {
|
|
26
|
+
return clusterLayerOf(colourBy) ?? artifacts?.layers[0];
|
|
27
|
+
}
|
|
24
28
|
/**
|
|
25
|
-
* The level
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
+
* The level the layer `name` is coloured, named and outlined at, where it is `stacked` or `tiered`:
|
|
30
|
+
* its entry in `chosen` where the layer declares that level, else its entry in the projection's
|
|
31
|
+
* `followedLevels`: the finest level not crowded in view where no level declares a zoom range,
|
|
32
|
+
* else the coarsest served at the camera's zoom. On a layer of any other kind, or one missing from
|
|
33
|
+
* the roster, `undefined`: each point takes its nearest served cluster.
|
|
29
34
|
*/
|
|
30
|
-
export function drawnLevel(meta, artifacts,
|
|
31
|
-
if (chosen !== undefined)
|
|
32
|
-
return chosen;
|
|
33
|
-
const name = clusterLayerOf(colourBy) ?? artifacts?.layers[0];
|
|
35
|
+
export function drawnLevel(meta, artifacts, name, chosen) {
|
|
34
36
|
const layer = name === undefined ? undefined : meta?.layers.find((l) => l.name === name);
|
|
35
|
-
return layer ?
|
|
37
|
+
return layer ? levelOfLayer(layer, chosen?.get(layer.name), artifacts?.followedLevels) : undefined;
|
|
38
|
+
}
|
|
39
|
+
/** {@link drawnLevel} for each layer drawn, keyed by name. A layer drawn at no level has no entry. */
|
|
40
|
+
export function drawnLevels(meta, artifacts, chosen) {
|
|
41
|
+
const out = new Map();
|
|
42
|
+
for (const name of artifacts?.layers ?? []) {
|
|
43
|
+
const level = drawnLevel(meta, artifacts, name, chosen);
|
|
44
|
+
if (level !== undefined)
|
|
45
|
+
out.set(name, level);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** A cache key for a set of {@link drawnLevels}. */
|
|
50
|
+
export function levelsKey(levels) {
|
|
51
|
+
return [...levels].map(([name, level]) => `${name}@${level}`).join(',');
|
|
36
52
|
}
|
|
37
53
|
/** The selected region's colour per ground: the interface's ink, since colour on the map is data's. */
|
|
38
54
|
const ACCENT = { light: [27, 29, 33], dark: [236, 238, 241] };
|
|
@@ -85,17 +101,19 @@ const CHROME = [234, 238, 243, 240];
|
|
|
85
101
|
const PLATE = [13, 15, 18, 235];
|
|
86
102
|
/**
|
|
87
103
|
* The current column colour encoding, from the store's legend, the schema and the colour choices.
|
|
88
|
-
* Uniform where nothing is ready to colour by yet, such as a
|
|
89
|
-
* were counted. Unmapped where `/v1/categories` refused the
|
|
90
|
-
* values could not be named. A chosen value colour applies once
|
|
104
|
+
* Uniform, in the one colour for `ground`, where nothing is ready to colour by yet, such as a
|
|
105
|
+
* column chosen before any of its codes were counted. Unmapped where `/v1/categories` refused the
|
|
106
|
+
* column, so the legend can say the values could not be named. A chosen value colour applies once
|
|
107
|
+
* the legend has named its key.
|
|
91
108
|
*/
|
|
92
|
-
export function encodingOf(meta, legend, colouring = DEFAULT_COLOURING) {
|
|
109
|
+
export function encodingOf(meta, legend, colouring = DEFAULT_COLOURING, ground = 'dark') {
|
|
110
|
+
const uniform = { kind: 'uniform', ground };
|
|
93
111
|
const colourBy = legend?.colourBy ?? null;
|
|
94
112
|
if (!colourBy || !meta || !legend || colourBy.startsWith(CLUSTER_PREFIX))
|
|
95
|
-
return
|
|
113
|
+
return uniform;
|
|
96
114
|
const column = meta.declaredScalars.find((c) => c.name === colourBy);
|
|
97
115
|
if (!column)
|
|
98
|
-
return
|
|
116
|
+
return uniform;
|
|
99
117
|
if (legend.categoryErrors[colourBy])
|
|
100
118
|
return { kind: 'unmapped' };
|
|
101
119
|
if (column.category) {
|
|
@@ -103,7 +121,7 @@ export function encodingOf(meta, legend, colouring = DEFAULT_COLOURING) {
|
|
|
103
121
|
// for `/v1/categories`, which only names the legend's entries.
|
|
104
122
|
const rankOfCode = legend.ranks[colourBy];
|
|
105
123
|
if (!rankOfCode || Object.keys(rankOfCode).length === 0)
|
|
106
|
-
return
|
|
124
|
+
return uniform;
|
|
107
125
|
const chosen = new Map();
|
|
108
126
|
const picked = colouring.values[colourBy];
|
|
109
127
|
if (picked) {
|
|
@@ -117,7 +135,7 @@ export function encodingOf(meta, legend, colouring = DEFAULT_COLOURING) {
|
|
|
117
135
|
}
|
|
118
136
|
const domain = legend.domains[colourBy];
|
|
119
137
|
if (!domain)
|
|
120
|
-
return
|
|
138
|
+
return uniform;
|
|
121
139
|
return { kind: 'numeric', column: colourBy, domain, ramp: colouring.ramp, scale: colouring.scale, reverse: colouring.reverse };
|
|
122
140
|
}
|
|
123
141
|
const descriptors = new WeakMap();
|
|
@@ -200,16 +218,24 @@ const OPENED_LINE = 200;
|
|
|
200
218
|
*/
|
|
201
219
|
const BOX_LINE_WIDTH = 0.8;
|
|
202
220
|
/**
|
|
203
|
-
* Whether a served artifact is drawn at `
|
|
204
|
-
* one artifact. Under a filter, one with no matching member in view is
|
|
205
|
-
* which the viewer chose.
|
|
221
|
+
* Whether a served artifact is drawn at its layer's entry in `levels` and has no drawn child:
|
|
222
|
+
* {@link frontier}'s rule for one artifact. Under a filter, one with no matching member in view is
|
|
223
|
+
* not drawn, except `opened`, which the viewer chose.
|
|
206
224
|
*/
|
|
207
|
-
function onFrontier(a, artifact,
|
|
208
|
-
const drawn = (x) =>
|
|
225
|
+
function onFrontier(a, artifact, levels, opened, stacked) {
|
|
226
|
+
const drawn = (x) => {
|
|
227
|
+
const level = levels.get(x.layer);
|
|
228
|
+
const atLevel = level === undefined || (stacked.has(x.layer) ? x.rung === level : x.rung <= level);
|
|
229
|
+
return (x.matched !== false || x.mosaicaId === opened) && atLevel;
|
|
230
|
+
};
|
|
209
231
|
if (!drawn(artifact))
|
|
210
232
|
return false;
|
|
211
233
|
return !(a.lineage.childrenOf.get(artifact.mosaicaId) ?? []).some((c) => drawn(c));
|
|
212
234
|
}
|
|
235
|
+
/** The `stacked` layers of the roster, whose levels are drawn one at a time. */
|
|
236
|
+
function stackedLayers(meta) {
|
|
237
|
+
return new Set(meta?.layers.filter((l) => l.hierarchy.kind === 'stacked').map((l) => l.name) ?? []);
|
|
238
|
+
}
|
|
213
239
|
/** The layers whose artifacts attach their text to another layer's, and draw no shape of their own. */
|
|
214
240
|
function dependentLayers(meta) {
|
|
215
241
|
return new Set(meta?.layers.filter((l) => l.depsOn.length > 0).map((l) => l.name) ?? []);
|
|
@@ -230,7 +256,7 @@ function shapeKindOf(meta, layer) {
|
|
|
230
256
|
*/
|
|
231
257
|
export function contourShapes(a, o) {
|
|
232
258
|
const dependent = dependentLayers(o.meta);
|
|
233
|
-
const front = frontier(a, o.
|
|
259
|
+
const front = frontier(a, o.levels, o.opened ?? null, o.meta);
|
|
234
260
|
const shapes = [];
|
|
235
261
|
for (const artifact of a.served) {
|
|
236
262
|
if (!front.has(artifact.mosaicaId))
|
|
@@ -260,6 +286,7 @@ export function contourShapes(a, o) {
|
|
|
260
286
|
*/
|
|
261
287
|
export function focusOutlines(a, o) {
|
|
262
288
|
const dependent = dependentLayers(o.meta);
|
|
289
|
+
const stacked = stackedLayers(o.meta);
|
|
263
290
|
const data = [];
|
|
264
291
|
const seen = new Set();
|
|
265
292
|
// Opened first, so the artifact under the pointer draws as opened once it is opened.
|
|
@@ -272,7 +299,7 @@ export function focusOutlines(a, o) {
|
|
|
272
299
|
continue;
|
|
273
300
|
if (dependent.has(artifact.layer))
|
|
274
301
|
continue;
|
|
275
|
-
if (!onFrontier(a, artifact, o.
|
|
302
|
+
if (!onFrontier(a, artifact, o.levels, o.opened, stacked))
|
|
276
303
|
continue;
|
|
277
304
|
const outline = outlineOf(artifact, a.shapes?.get(id));
|
|
278
305
|
if (!outline)
|
|
@@ -328,23 +355,26 @@ const COUNT_GAP_EM = 0.35;
|
|
|
328
355
|
/** The topic line beneath the name, in pixels. */
|
|
329
356
|
const TOPIC_SIZE = 12;
|
|
330
357
|
/**
|
|
331
|
-
* The artifacts named at `
|
|
358
|
+
* The artifacts named at `levels`: every drawn artifact with no drawn child. An artifact with a
|
|
332
359
|
* drawn child is an ancestor of something on the map and is not labelled.
|
|
333
360
|
*
|
|
334
|
-
*
|
|
335
|
-
* every shallower artifact whose children are all deeper than the
|
|
336
|
-
* above the level is still named.
|
|
337
|
-
*
|
|
338
|
-
*
|
|
361
|
+
* On a layer with no entry in `levels` this is the leaves of its served set. On one with an entry
|
|
362
|
+
* it is that level's artifacts and every shallower artifact whose children are all deeper than the
|
|
363
|
+
* level, so a branch that ends above the level is still named. A `stacked` layer in `meta` has no
|
|
364
|
+
* edges between its levels, so on one with an entry it is that level's artifacts alone. Each layer
|
|
365
|
+
* is cut at its own entry only. The level compared is the served `rung`, which the server computes
|
|
366
|
+
* per layer kind; a levelled layer's edges may skip a level, so a client-side depth count would be
|
|
367
|
+
* wrong.
|
|
339
368
|
*
|
|
340
369
|
* Under a filter, an artifact with no member in view that matches it is left out: none of its
|
|
341
370
|
* points are drawn, so its name and its outline would stand over empty ground. A parent whose
|
|
342
371
|
* children all match nothing is on the frontier in their place.
|
|
343
372
|
*/
|
|
344
|
-
export function frontier(a,
|
|
373
|
+
export function frontier(a, levels, opened = null, meta = null) {
|
|
374
|
+
const stacked = stackedLayers(meta);
|
|
345
375
|
const out = new Set();
|
|
346
376
|
for (const artifact of a.served)
|
|
347
|
-
if (onFrontier(a, artifact,
|
|
377
|
+
if (onFrontier(a, artifact, levels, opened, stacked))
|
|
348
378
|
out.add(artifact.mosaicaId);
|
|
349
379
|
return out;
|
|
350
380
|
}
|
|
@@ -354,27 +384,27 @@ export function frontier(a, level, opened = null) {
|
|
|
354
384
|
* no text and no attached topic gets no label; its key is an identifier, not a name. Name size
|
|
355
385
|
* comes from {@link labelSize} over the candidates' counts.
|
|
356
386
|
*
|
|
357
|
-
* Only the anchor depends on the zoom, so the list is built once per served set,
|
|
387
|
+
* Only the anchor depends on the zoom, so the list is built once per served set, levels and budget,
|
|
358
388
|
* and each zoom bucket scales the anchors into a copy.
|
|
359
389
|
*/
|
|
360
|
-
export function labelCandidates(a, meta,
|
|
361
|
-
const key = `${a.version}|${
|
|
390
|
+
export function labelCandidates(a, meta, levels, zoom, budget, opened = null) {
|
|
391
|
+
const key = `${a.version}|${levelsKey(levels)}|${budget}|${opened ?? ''}`;
|
|
362
392
|
let held = heldCandidates.get(a.served);
|
|
363
393
|
if (!held || held.key !== key) {
|
|
364
|
-
held = { key, ...namedCandidates(a, meta,
|
|
394
|
+
held = { key, ...namedCandidates(a, meta, levels, budget, opened) };
|
|
365
395
|
heldCandidates.set(a.served, held);
|
|
366
396
|
}
|
|
367
397
|
const scale = 2 ** zoom; // pixels per world unit
|
|
368
398
|
return { candidates: held.candidates.map((c) => ({ ...c, x: c.x * scale, y: c.y * scale })), byId: held.byId };
|
|
369
399
|
}
|
|
370
400
|
/** {@link labelCandidates} with anchors in world units, which the caller scales. */
|
|
371
|
-
function namedCandidates(a, meta,
|
|
401
|
+
function namedCandidates(a, meta, levels, budget, opened) {
|
|
372
402
|
const placed = a.served.filter((x) => x.centroid !== null);
|
|
373
403
|
// A dependent layer's artifacts (a clustering's topic labels) are drawn beneath their target's
|
|
374
404
|
// name and are not candidates of their own.
|
|
375
405
|
const dependent = new Set(meta?.layers.filter((l) => l.depsOn.length > 0).map((l) => l.name) ?? []);
|
|
376
406
|
// The same frontier the hover shapes use, so a name and the shape it labels agree.
|
|
377
|
-
const front = frontier(a,
|
|
407
|
+
const front = frontier(a, levels, opened, meta);
|
|
378
408
|
const named = placed
|
|
379
409
|
.filter((x) => !dependent.has(x.layer) && front.has(x.mosaicaId))
|
|
380
410
|
.filter((x) => artifactName(x, a.attached) !== null)
|
|
@@ -455,8 +485,8 @@ export function outlineOf(a, fetched) {
|
|
|
455
485
|
* to {@link resolvePick} to find the item or artifact under the pointer, then set the props that
|
|
456
486
|
* show it, such as `selectedWorldXY` and `hoveredArtifact`. If the artifact's layer declares
|
|
457
487
|
* shapes, call the store's `needShape` with its id: the outline appears once the shape arrives.
|
|
458
|
-
* Otherwise the outline is the artifact's bounding box. Only an artifact drawn at
|
|
459
|
-
* is outlined.
|
|
488
|
+
* Otherwise the outline is the artifact's bounding box. Only an artifact drawn at its layer's level
|
|
489
|
+
* (`clusterLevels`) is outlined.
|
|
460
490
|
*/
|
|
461
491
|
export class MosaicaLayer extends CompositeLayer {
|
|
462
492
|
/** @internal */
|
|
@@ -471,7 +501,7 @@ export class MosaicaLayer extends CompositeLayer {
|
|
|
471
501
|
legend: null,
|
|
472
502
|
status: 'idle',
|
|
473
503
|
slab: null,
|
|
474
|
-
|
|
504
|
+
clusterLevels: undefined,
|
|
475
505
|
labels: true,
|
|
476
506
|
scheme: 'dark',
|
|
477
507
|
selectedWorldXY: null,
|
|
@@ -655,7 +685,7 @@ export class MosaicaLayer extends CompositeLayer {
|
|
|
655
685
|
const r = this.resolved();
|
|
656
686
|
const slab = this.slab();
|
|
657
687
|
const layers = [];
|
|
658
|
-
// The key covers the palette, level and highlight; `update` compares the table and colour map
|
|
688
|
+
// The key covers the palette, colour level and highlight; `update` compares the table and colour map
|
|
659
689
|
// itself. No texel depends on the served set's version, so it is not in the key.
|
|
660
690
|
const lut = this.lut();
|
|
661
691
|
const lutStarted = performance.now();
|
|
@@ -663,7 +693,7 @@ export class MosaicaLayer extends CompositeLayer {
|
|
|
663
693
|
const highlight = r.artifacts && opened !== null ? r.artifacts.served.find((a) => a.mosaicaId === opened) : undefined;
|
|
664
694
|
const highlightOrdinal = highlight && r.artifacts ? r.artifacts.table.ordinalOf(highlight.layer, highlight.mosaicaId) : NO_ORDINAL;
|
|
665
695
|
if (r.artifacts) {
|
|
666
|
-
lut.update({ artifacts: r.artifacts, level: this.
|
|
696
|
+
lut.update({ artifacts: r.artifacts, level: this.colourLevel(r), highlight: highlightOrdinal }, `${r.artifacts.palette}|${this.colourLevel(r) ?? ''}|${highlightOrdinal}`);
|
|
667
697
|
}
|
|
668
698
|
timings.lutMs = performance.now() - lutStarted;
|
|
669
699
|
// A refusal draws no marks but keeps the slab, whose bands answer the last view that
|
|
@@ -685,9 +715,10 @@ export class MosaicaLayer extends CompositeLayer {
|
|
|
685
715
|
// texture, so switching to it leaves the attribute holding the last column encoding.
|
|
686
716
|
const colourBy = r.legend?.colourBy ?? null;
|
|
687
717
|
const clusterLayer = clusterLayerOf(colourBy);
|
|
688
|
-
|
|
718
|
+
const ground = this.props.scheme ?? 'dark';
|
|
719
|
+
let column = heldColumnEncoding.get(slab) ?? { encoding: { kind: 'uniform', ground }, colourBy: null };
|
|
689
720
|
if (!clusterLayer) {
|
|
690
|
-
column = { encoding: encodingOf(r.meta, r.legend, this.props.colouring ?? DEFAULT_COLOURING), colourBy };
|
|
721
|
+
column = { encoding: encodingOf(r.meta, r.legend, this.props.colouring ?? DEFAULT_COLOURING, ground), colourBy };
|
|
691
722
|
heldColumnEncoding.set(slab, column);
|
|
692
723
|
}
|
|
693
724
|
const encoding = column.encoding;
|
|
@@ -897,9 +928,13 @@ export class MosaicaLayer extends CompositeLayer {
|
|
|
897
928
|
updateTriggers: { getIcon: key, getSize: key, getPixelOffset: key, getColor: scheme }
|
|
898
929
|
});
|
|
899
930
|
}
|
|
900
|
-
/** The level
|
|
901
|
-
|
|
902
|
-
return drawnLevel(r.meta, r.artifacts, r.legend?.colourBy, this.props.
|
|
931
|
+
/** The level marks are coloured at: that of the layer they are coloured from ({@link colourLayerOf}). */
|
|
932
|
+
colourLevel(r) {
|
|
933
|
+
return drawnLevel(r.meta, r.artifacts, colourLayerOf(r.artifacts, r.legend?.colourBy), this.props.clusterLevels);
|
|
934
|
+
}
|
|
935
|
+
/** The level each drawn layer is cut at ({@link drawnLevels}). */
|
|
936
|
+
levels(r) {
|
|
937
|
+
return drawnLevels(r.meta, r.artifacts, this.props.clusterLevels);
|
|
903
938
|
}
|
|
904
939
|
/**
|
|
905
940
|
* Whether the labels sit on opaque plates with no halo: while density is drawn as hexagons or a
|
|
@@ -1061,10 +1096,11 @@ export class MosaicaLayer extends CompositeLayer {
|
|
|
1061
1096
|
const opened = this.props.openedArtifact ?? null;
|
|
1062
1097
|
const hovered = this.props.hoveredArtifact ?? null;
|
|
1063
1098
|
const scheme = this.props.scheme ?? 'dark';
|
|
1064
|
-
const
|
|
1099
|
+
const levels = this.levels(r);
|
|
1100
|
+
const key = a ? `${a.version}|${a.palette}|${opened ?? ''}|${hovered ?? ''}|${scheme}|${levelsKey(levels)}` : '';
|
|
1065
1101
|
let held = a ? heldOutlines.get(a.served) : undefined;
|
|
1066
1102
|
if (a && (!held || held.key !== key || held.shapes !== a.shapes)) {
|
|
1067
|
-
held = { key, shapes: a.shapes, data: focusOutlines(a, { opened, hovered,
|
|
1103
|
+
held = { key, shapes: a.shapes, data: focusOutlines(a, { opened, hovered, levels, scheme, meta: r.meta }) };
|
|
1068
1104
|
heldOutlines.set(a.served, held);
|
|
1069
1105
|
}
|
|
1070
1106
|
const data = held?.data ?? NO_OUTLINES;
|
|
@@ -1105,11 +1141,12 @@ export class MosaicaLayer extends CompositeLayer {
|
|
|
1105
1141
|
const bucket = Math.round(zoom * LABEL_ZOOM_STEP);
|
|
1106
1142
|
const budget = a ? labelBudget(a.served.length) : 0;
|
|
1107
1143
|
const opened = this.props.openedArtifact ?? null;
|
|
1108
|
-
const
|
|
1144
|
+
const levels = this.levels(r);
|
|
1145
|
+
const key = a ? `${a.version}|${a.palette}|${bucket}|${budget}|${levelsKey(levels)}|${opened ?? ''}` : '';
|
|
1109
1146
|
let held = a ? heldLabels.get(a.served) : undefined;
|
|
1110
1147
|
if (a && viewport && (!held || held.key !== key)) {
|
|
1111
1148
|
const scale = 2 ** zoom; // pixels per world unit
|
|
1112
|
-
const { candidates, byId } = labelCandidates(a, r.meta,
|
|
1149
|
+
const { candidates, byId } = labelCandidates(a, r.meta, levels, zoom, budget, opened);
|
|
1113
1150
|
const data = [];
|
|
1114
1151
|
const leaders = [];
|
|
1115
1152
|
let placed = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mosaicajs/deck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A deck.gl layer that draws a Mosaica store, for a host with its own Deck.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mosaica",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@mosaicajs/client": "0.
|
|
48
|
+
"@mosaicajs/client": "0.4.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@deck.gl/aggregation-layers": "^9.3.7",
|