@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 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
- /** The colour of every mark when no column is encoded. */
15
- export declare const UNIFORM: Rgba;
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
- /** How a column is being coloured. `uniform` is the default and the fallback for anything unmapped. */
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
- /** The colour of every mark when no column is encoded. */
4
- export const UNIFORM = [120, 190, 255, ALPHA];
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) {
@@ -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 of a `stacked` or `tiered` artifact layer to colour, name and outline artifacts at.
48
- * Defaults to the coarsest level served. On a `nested` or `dag` layer it has no default, and each
49
- * mark takes the nearest artifact served.
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
- clusterLevel?: number;
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 and the selected region are chosen to suit it. Defaults to `dark`.
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 clusters are coloured, named and outlined at: `chosen` where given, else the top
195
- * (coarsest) level served at the camera's zoom of the layer coloured by, or of the first layer
196
- * drawn, where that layer is `stacked` or `tiered`. On any other layer, `undefined`: each point
197
- * takes its nearest served cluster.
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, 'layers' | 'served'> | null, colourBy: string | null | undefined, chosen: number | undefined): number | undefined;
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 column chosen before any of its codes
203
- * were counted. Unmapped where `/v1/categories` refused the column, so the legend can say the
204
- * values could not be named. A chosen value colour applies once the legend has named its key.
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: number | undefined;
270
+ /** The level each layer is cut at ({@link drawnLevels}). */
271
+ levels: ReadonlyMap<string, number>;
259
272
  scheme: 'light' | 'dark';
260
- /** The layer roster, to leave a dependent layer's artifacts out; null draws every served layer. */
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 cut, the layer roster and the opened artifact, which decide what may be hovered. */
279
+ /** The level cuts, the layer roster and the opened artifact, which decide what may be hovered. */
264
280
  export type ContourOptions = {
265
- level: number | undefined;
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 `level`: every drawn artifact with no drawn child. An artifact with a
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
- * With no level this is the leaves of the served set. With one it is that level's artifacts and
321
- * every shallower artifact whose children are all deeper than the level, so a branch that ends
322
- * above the level is still named. The level compared is the served `rung`, which the server
323
- * computes per layer kind; a levelled layer's edges may skip a level, so a client-side depth count
324
- * would be wrong.
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, level: number | undefined, opened?: bigint | null): Set<bigint>;
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, level and budget,
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, level: number | undefined, zoom: number, budget: number, opened?: bigint | 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 `clusterLevel`
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
- clusterLevel: undefined;
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 clusters are drawn at ({@link drawnLevel}). */
488
- private levelOf;
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, topLevelServed } from '@mosaicajs/client/internal';
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 clusters are coloured, named and outlined at: `chosen` where given, else the top
26
- * (coarsest) level served at the camera's zoom of the layer coloured by, or of the first layer
27
- * drawn, where that layer is `stacked` or `tiered`. On any other layer, `undefined`: each point
28
- * takes its nearest served cluster.
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, colourBy, chosen) {
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 ? topLevelServed(layer, artifacts?.served ?? []) : undefined;
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 column chosen before any of its codes
89
- * were counted. Unmapped where `/v1/categories` refused the column, so the legend can say the
90
- * values could not be named. A chosen value colour applies once the legend has named its key.
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 { kind: 'uniform' };
113
+ return uniform;
96
114
  const column = meta.declaredScalars.find((c) => c.name === colourBy);
97
115
  if (!column)
98
- return { kind: 'uniform' };
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 { kind: 'uniform' };
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 { kind: 'uniform' };
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 `level` and has no drawn child: {@link frontier}'s rule for
204
- * one artifact. Under a filter, one with no matching member in view is not drawn, except `opened`,
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, level, opened = null) {
208
- const drawn = (x) => (x.matched !== false || x.mosaicaId === opened) && (level === undefined || x.rung <= level);
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.level, o.opened ?? null);
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.level, o.opened))
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 `level`: every drawn artifact with no drawn child. An artifact with a
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
- * With no level this is the leaves of the served set. With one it is that level's artifacts and
335
- * every shallower artifact whose children are all deeper than the level, so a branch that ends
336
- * above the level is still named. The level compared is the served `rung`, which the server
337
- * computes per layer kind; a levelled layer's edges may skip a level, so a client-side depth count
338
- * would be wrong.
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, level, opened = null) {
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, level, opened))
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, level and budget,
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, level, zoom, budget, opened = null) {
361
- const key = `${a.version}|${level ?? ''}|${budget}|${opened ?? ''}`;
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, level, budget, opened) };
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, level, budget, opened) {
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, level, opened);
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 `clusterLevel`
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
- clusterLevel: undefined,
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.levelOf(r), highlight: highlightOrdinal }, `${r.artifacts.palette}|${this.levelOf(r) ?? ''}|${highlightOrdinal}`);
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
- let column = heldColumnEncoding.get(slab) ?? { encoding: { kind: 'uniform' }, colourBy: null };
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 clusters are drawn at ({@link drawnLevel}). */
901
- levelOf(r) {
902
- return drawnLevel(r.meta, r.artifacts, r.legend?.colourBy, this.props.clusterLevel);
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 key = a ? `${a.version}|${a.palette}|${opened ?? ''}|${hovered ?? ''}|${scheme}|${this.levelOf(r) ?? ''}` : '';
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, level: this.levelOf(r), scheme, meta: r.meta }) };
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 key = a ? `${a.version}|${a.palette}|${bucket}|${budget}|${this.levelOf(r) ?? ''}|${opened ?? ''}` : '';
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, this.levelOf(r), zoom, budget, opened);
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.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.3.0"
48
+ "@mosaicajs/client": "0.4.0"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@deck.gl/aggregation-layers": "^9.3.7",