@mosaicajs/components 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/README.md +2 -0
- package/dist/aggregate.d.ts +4 -10
- package/dist/aggregate.js +7 -13
- package/dist/artifact-card.d.ts +21 -7
- package/dist/artifact-card.js +136 -79
- package/dist/base.d.ts +44 -5
- package/dist/base.js +54 -5
- package/dist/cluster-filter.d.ts +29 -25
- package/dist/cluster-filter.js +123 -106
- package/dist/colour-editor.d.ts +15 -4
- package/dist/colour-editor.js +107 -34
- package/dist/colour-picker.d.ts +12 -5
- package/dist/colour-picker.js +119 -32
- package/dist/display.js +77 -9
- package/dist/events.d.ts +21 -7
- package/dist/explorer.d.ts +116 -37
- package/dist/explorer.js +476 -210
- package/dist/field-card.d.ts +304 -48
- package/dist/field-card.js +1364 -244
- package/dist/filter-panel.d.ts +54 -10
- package/dist/filter-panel.js +242 -42
- package/dist/filter.d.ts +22 -4
- package/dist/filter.js +113 -34
- package/dist/hierarchy.d.ts +68 -45
- package/dist/hierarchy.js +320 -243
- package/dist/item-card.d.ts +47 -16
- package/dist/item-card.js +241 -108
- package/dist/key-picker.d.ts +2 -1
- package/dist/key-picker.js +4 -3
- package/dist/layer-picker.d.ts +4 -2
- package/dist/layer-picker.js +16 -7
- package/dist/lineage.d.ts +132 -0
- package/dist/lineage.js +307 -0
- package/dist/map.d.ts +135 -24
- package/dist/map.js +387 -117
- package/dist/mosaica-components.js +1837 -833
- package/dist/mosaica-components.js.sri +1 -1
- package/dist/mosaica-components.json +2 -2
- package/dist/parts.d.ts +4 -4
- package/dist/parts.js +18 -4
- package/dist/paths.d.ts +34 -12
- package/dist/paths.js +84 -46
- package/dist/selection.d.ts +22 -3
- package/dist/selection.js +125 -59
- package/dist/states.d.ts +72 -0
- package/dist/states.js +135 -26
- package/dist/status.d.ts +63 -5
- package/dist/status.js +215 -36
- package/dist/tokens.d.ts +8 -4
- package/dist/tokens.js +144 -14
- package/dist/view-picker.js +5 -0
- package/package.json +4 -3
package/dist/map.js
CHANGED
|
@@ -35,13 +35,13 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
35
35
|
import { Deck, OrthographicView } from '@deck.gl/core';
|
|
36
36
|
import { css, html, nothing } from 'lit';
|
|
37
37
|
import { property, state } from 'lit/decorators.js';
|
|
38
|
-
import { MAX_DEPTH, WORLD_SIZE, dataToWorldXY } from '@mosaicajs/client';
|
|
38
|
+
import { MAX_DEPTH, WORLD_SIZE, dataToWorldXY, formatMasked } from '@mosaicajs/client';
|
|
39
39
|
import { assertCompositionMatchesServed, chosenColour, hasValue } from '@mosaicajs/client/internal';
|
|
40
40
|
import { DEFAULT_DENSITY_CELL_PX, DEFAULT_DENSITY_SCALE, DensityCounter, MosaicaLayer, cameraFitting, densityCountAt, frameMatches, resolvePick, viewInputOf } from '@mosaicajs/deck';
|
|
41
|
-
import { DENSITY_COLOUR_TITLES, MarkSlab, artifactOfMark, clusterLayerOf, contourShapes, drawnLevel, densityStops, drawnCells, encodingOf, encodingSignature, hoverAt, maxCount } from '@mosaicajs/deck/internal';
|
|
41
|
+
import { DENSITY_COLOUR_TITLES, MarkSlab, artifactOfMark, clusterLayerOf, colourLayerOf, contourShapes, drawnLevel, drawnLevels, densityStops, drawnCells, encodingOf, encodingSignature, hoverAt, levelsKey, maxCount } from '@mosaicajs/deck/internal';
|
|
42
42
|
import { MosaicaElement, emit, idString, shapeDetail, timestampText } from './base.js';
|
|
43
43
|
import { attachContextRoot, defineOnce } from './define.js';
|
|
44
|
-
import { NOTHING_VISIBLE,
|
|
44
|
+
import { ActionFocus, EmptyAnswer, NOTHING_VISIBLE, UNSAID, liveLine, refusedLine, seesNothing, stateLine, stateOf } from './states.js';
|
|
45
45
|
import { MapBasemap, basemapChanged } from './basemap.js';
|
|
46
46
|
import { icon } from './icons.js';
|
|
47
47
|
import { sameFrame } from './view-switch.js';
|
|
@@ -70,13 +70,29 @@ let mapsMade = 0;
|
|
|
70
70
|
* A box or lasso drawn with the toolbar becomes the store's selected region, which narrows the map
|
|
71
71
|
* and every count to the items inside it. Shift-drag draws a box without changing tool. With the
|
|
72
72
|
* map focused, the arrow keys pan and `+` and `-` zoom. Escape cancels a shape being drawn, or
|
|
73
|
-
* clears the region when there is no shape, or drops the picked item when there is neither.
|
|
73
|
+
* clears the region when there is no shape, or drops the picked item when there is neither. With
|
|
74
|
+
* none of the three, the map leaves Escape to the page, so a dialog holding the map still closes.
|
|
74
75
|
*
|
|
75
|
-
*
|
|
76
|
+
* The hover tooltip follows the pointer and stays inside the map. Escape dismisses it wherever the
|
|
77
|
+
* focus is, and it stays away until the pointer moves onto another point. With the focus in the
|
|
78
|
+
* map, or in an element that renders the map inside its own shadow root, such as
|
|
79
|
+
* `<mosaica-explorer>`, the press does nothing else: the map calls `preventDefault` on it, and
|
|
80
|
+
* clears no selection on it. With the focus anywhere else on the page, the map leaves the press as
|
|
81
|
+
* it was, so the page's own Escape still acts.
|
|
82
|
+
*
|
|
83
|
+
* When the map cannot draw the view, it says why over the canvas, and screen readers announce it.
|
|
84
|
+
* Nothing in view is announced as the answer to a change of filters, highlight or selection, and
|
|
85
|
+
* only shown where a pan went. Screen readers also announce the basemap's note, and how many items
|
|
86
|
+
* match inside the selected region, once, when the query that counts it has finished loading. Set
|
|
87
|
+
* `quiet` where another element on the page says the same things, such as a status strip: the map
|
|
88
|
+
* shows the same, and announces only what it alone knows, the basemap's note and a basemap it
|
|
89
|
+
* refuses.
|
|
90
|
+
*
|
|
91
|
+
* With `ground` unset the map follows the page's colour scheme, and draws again when it changes.
|
|
76
92
|
*
|
|
77
93
|
* The canvas is a single tab stop, and it takes any `tabindex` or `aria-label` you set on the map.
|
|
78
|
-
* Without an `aria-label
|
|
79
|
-
*
|
|
94
|
+
* Without an `aria-label` the canvas is named "Map". Either way its description lists its keys, so
|
|
95
|
+
* that someone using a screen reader knows how to move it.
|
|
80
96
|
*
|
|
81
97
|
* @summary An interactive map with no panels around it.
|
|
82
98
|
* @tagname mosaica-map
|
|
@@ -105,23 +121,26 @@ let mapsMade = 0;
|
|
|
105
121
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-layerchange']>} mosaica-layerchange - The
|
|
106
122
|
* `layers` attribute changed.
|
|
107
123
|
* @csspart canvas - The deck.gl canvas's container.
|
|
108
|
-
* @csspart overlay - The
|
|
109
|
-
* server cannot be reached, did not answer or is too busy. If the viewer may see no item in the
|
|
110
|
-
* view at all, it says so instead of "Nothing in view".
|
|
124
|
+
* @csspart overlay - The layer over the canvas that holds the state line.
|
|
111
125
|
* @csspart state - The state line inside the overlay, with `data-state`, and `data-visible="none"`
|
|
112
|
-
* if the viewer may see no item in the view.
|
|
126
|
+
* if the viewer may see no item in the view. It shows a message when the view is refused,
|
|
127
|
+
* expired or empty, or the server cannot be reached, did not answer or is too busy; if the viewer
|
|
128
|
+
* may see no item in the view at all, it says so instead of "Nothing in view". It is in the page
|
|
129
|
+
* in every state, and drawn only while it has a message.
|
|
113
130
|
* @csspart retry - The Retry button in the overlay, after a refusal, no answer or a busy server.
|
|
114
131
|
* @csspart refusal - The words "View refused", or "Server busy", inside the overlay, with
|
|
115
132
|
* `data-code` set to the refusal's code.
|
|
116
133
|
* @csspart reason - The server's reason for a refusal, as it sent it, on a line under "View
|
|
117
134
|
* refused".
|
|
118
135
|
* @csspart controls - The toolbar.
|
|
119
|
-
* @csspart region-tag - The tag
|
|
120
|
-
* match inside it (or outside it, for its complement) and a button that clears it.
|
|
136
|
+
* @csspart region-tag - The tag above the selected region's top-left corner, with how many items
|
|
137
|
+
* match inside it (or outside it, for its complement) and a button that clears it. It stays
|
|
138
|
+
* inside the map when the corner is off it.
|
|
121
139
|
* @csspart tooltip - The hover tooltip.
|
|
122
140
|
* @csspart density-key - The key to density's colours in items per cell, in the bottom-left corner.
|
|
123
141
|
* Shown while density is drawn in a colour ramp or without the points.
|
|
124
|
-
* @csspart note - Why no basemap is drawn under this view, in the bottom-right corner.
|
|
142
|
+
* @csspart note - Why no basemap is drawn under this view, in the bottom-right corner. It is in the
|
|
143
|
+
* page whether or not there is a note, and drawn only while there is one.
|
|
125
144
|
* @csspart attribution - The tile server's credit, in the bottom-right corner while tiles are drawn.
|
|
126
145
|
* @cssprop --mosaica-map-height - The map's height.
|
|
127
146
|
* @cssprop --mosaica-map-bg - The canvas's background, behind the points and any basemap.
|
|
@@ -139,9 +158,9 @@ let MosaicaMap = (() => {
|
|
|
139
158
|
let _budget_decorators;
|
|
140
159
|
let _budget_initializers = [];
|
|
141
160
|
let _budget_extraInitializers = [];
|
|
142
|
-
let
|
|
143
|
-
let
|
|
144
|
-
let
|
|
161
|
+
let _clusterSpacing_decorators;
|
|
162
|
+
let _clusterSpacing_initializers = [];
|
|
163
|
+
let _clusterSpacing_extraInitializers = [];
|
|
145
164
|
let _tooltipFields_decorators;
|
|
146
165
|
let _tooltipFields_initializers = [];
|
|
147
166
|
let _tooltipFields_extraInitializers = [];
|
|
@@ -154,9 +173,9 @@ let MosaicaMap = (() => {
|
|
|
154
173
|
let _palette_decorators;
|
|
155
174
|
let _palette_initializers = [];
|
|
156
175
|
let _palette_extraInitializers = [];
|
|
157
|
-
let
|
|
158
|
-
let
|
|
159
|
-
let
|
|
176
|
+
let _clusterLevels_decorators;
|
|
177
|
+
let _clusterLevels_initializers = [];
|
|
178
|
+
let _clusterLevels_extraInitializers = [];
|
|
160
179
|
let _basemap_decorators;
|
|
161
180
|
let _basemap_initializers = [];
|
|
162
181
|
let _basemap_extraInitializers = [];
|
|
@@ -223,6 +242,9 @@ let MosaicaMap = (() => {
|
|
|
223
242
|
let _noControls_decorators;
|
|
224
243
|
let _noControls_initializers = [];
|
|
225
244
|
let _noControls_extraInitializers = [];
|
|
245
|
+
let _quiet_decorators;
|
|
246
|
+
let _quiet_initializers = [];
|
|
247
|
+
let _quiet_extraInitializers = [];
|
|
226
248
|
let _controlsCorner_decorators;
|
|
227
249
|
let _controlsCorner_initializers = [];
|
|
228
250
|
let _controlsCorner_extraInitializers = [];
|
|
@@ -247,12 +269,12 @@ let MosaicaMap = (() => {
|
|
|
247
269
|
converter: { fromAttribute: (v) => (v ? v.split(/[\s,]+/).filter(Boolean) : []), toAttribute: (v) => v.join(' ') }
|
|
248
270
|
})];
|
|
249
271
|
_budget_decorators = [property({ type: Number })];
|
|
250
|
-
|
|
272
|
+
_clusterSpacing_decorators = [property({ type: Number, attribute: 'cluster-spacing' })];
|
|
251
273
|
_tooltipFields_decorators = [property({ attribute: 'tooltip-fields' })];
|
|
252
274
|
_titleField_decorators = [property({ attribute: 'title-field' })];
|
|
253
275
|
_mode_decorators = [property({ reflect: true })];
|
|
254
276
|
_palette_decorators = [property()];
|
|
255
|
-
|
|
277
|
+
_clusterLevels_decorators = [property({ attribute: false })];
|
|
256
278
|
_basemap_decorators = [property({ hasChanged: basemapChanged })];
|
|
257
279
|
_ground_decorators = [property({ reflect: true })];
|
|
258
280
|
_noPoints_decorators = [property({ type: Boolean, attribute: 'no-points', reflect: true })];
|
|
@@ -275,6 +297,7 @@ let MosaicaMap = (() => {
|
|
|
275
297
|
_sizeMax_decorators = [property({ type: Number, attribute: 'size-max' })];
|
|
276
298
|
_sizeScale_decorators = [property({ attribute: 'size-scale' })];
|
|
277
299
|
_noControls_decorators = [property({ type: Boolean, attribute: 'no-controls' })];
|
|
300
|
+
_quiet_decorators = [property({ type: Boolean, reflect: true })];
|
|
278
301
|
_controlsCorner_decorators = [property({ attribute: 'controls-corner' })];
|
|
279
302
|
_hover_decorators = [state()];
|
|
280
303
|
_hoveredArtifact_decorators = [state()];
|
|
@@ -283,12 +306,12 @@ let MosaicaMap = (() => {
|
|
|
283
306
|
__esDecorate(this, null, _colourBy_decorators, { kind: "accessor", name: "colourBy", static: false, private: false, access: { has: obj => "colourBy" in obj, get: obj => obj.colourBy, set: (obj, value) => { obj.colourBy = value; } }, metadata: _metadata }, _colourBy_initializers, _colourBy_extraInitializers);
|
|
284
307
|
__esDecorate(this, null, _layers_decorators, { kind: "accessor", name: "layers", static: false, private: false, access: { has: obj => "layers" in obj, get: obj => obj.layers, set: (obj, value) => { obj.layers = value; } }, metadata: _metadata }, _layers_initializers, _layers_extraInitializers);
|
|
285
308
|
__esDecorate(this, null, _budget_decorators, { kind: "accessor", name: "budget", static: false, private: false, access: { has: obj => "budget" in obj, get: obj => obj.budget, set: (obj, value) => { obj.budget = value; } }, metadata: _metadata }, _budget_initializers, _budget_extraInitializers);
|
|
286
|
-
__esDecorate(this, null,
|
|
309
|
+
__esDecorate(this, null, _clusterSpacing_decorators, { kind: "accessor", name: "clusterSpacing", static: false, private: false, access: { has: obj => "clusterSpacing" in obj, get: obj => obj.clusterSpacing, set: (obj, value) => { obj.clusterSpacing = value; } }, metadata: _metadata }, _clusterSpacing_initializers, _clusterSpacing_extraInitializers);
|
|
287
310
|
__esDecorate(this, null, _tooltipFields_decorators, { kind: "accessor", name: "tooltipFields", static: false, private: false, access: { has: obj => "tooltipFields" in obj, get: obj => obj.tooltipFields, set: (obj, value) => { obj.tooltipFields = value; } }, metadata: _metadata }, _tooltipFields_initializers, _tooltipFields_extraInitializers);
|
|
288
311
|
__esDecorate(this, null, _titleField_decorators, { kind: "accessor", name: "titleField", static: false, private: false, access: { has: obj => "titleField" in obj, get: obj => obj.titleField, set: (obj, value) => { obj.titleField = value; } }, metadata: _metadata }, _titleField_initializers, _titleField_extraInitializers);
|
|
289
312
|
__esDecorate(this, null, _mode_decorators, { kind: "accessor", name: "mode", static: false, private: false, access: { has: obj => "mode" in obj, get: obj => obj.mode, set: (obj, value) => { obj.mode = value; } }, metadata: _metadata }, _mode_initializers, _mode_extraInitializers);
|
|
290
313
|
__esDecorate(this, null, _palette_decorators, { kind: "accessor", name: "palette", static: false, private: false, access: { has: obj => "palette" in obj, get: obj => obj.palette, set: (obj, value) => { obj.palette = value; } }, metadata: _metadata }, _palette_initializers, _palette_extraInitializers);
|
|
291
|
-
__esDecorate(this, null,
|
|
314
|
+
__esDecorate(this, null, _clusterLevels_decorators, { kind: "accessor", name: "clusterLevels", static: false, private: false, access: { has: obj => "clusterLevels" in obj, get: obj => obj.clusterLevels, set: (obj, value) => { obj.clusterLevels = value; } }, metadata: _metadata }, _clusterLevels_initializers, _clusterLevels_extraInitializers);
|
|
292
315
|
__esDecorate(this, null, _basemap_decorators, { kind: "accessor", name: "basemap", static: false, private: false, access: { has: obj => "basemap" in obj, get: obj => obj.basemap, set: (obj, value) => { obj.basemap = value; } }, metadata: _metadata }, _basemap_initializers, _basemap_extraInitializers);
|
|
293
316
|
__esDecorate(this, null, _ground_decorators, { kind: "accessor", name: "ground", static: false, private: false, access: { has: obj => "ground" in obj, get: obj => obj.ground, set: (obj, value) => { obj.ground = value; } }, metadata: _metadata }, _ground_initializers, _ground_extraInitializers);
|
|
294
317
|
__esDecorate(this, null, _noPoints_decorators, { kind: "accessor", name: "noPoints", static: false, private: false, access: { has: obj => "noPoints" in obj, get: obj => obj.noPoints, set: (obj, value) => { obj.noPoints = value; } }, metadata: _metadata }, _noPoints_initializers, _noPoints_extraInitializers);
|
|
@@ -311,6 +334,7 @@ let MosaicaMap = (() => {
|
|
|
311
334
|
__esDecorate(this, null, _sizeMax_decorators, { kind: "accessor", name: "sizeMax", static: false, private: false, access: { has: obj => "sizeMax" in obj, get: obj => obj.sizeMax, set: (obj, value) => { obj.sizeMax = value; } }, metadata: _metadata }, _sizeMax_initializers, _sizeMax_extraInitializers);
|
|
312
335
|
__esDecorate(this, null, _sizeScale_decorators, { kind: "accessor", name: "sizeScale", static: false, private: false, access: { has: obj => "sizeScale" in obj, get: obj => obj.sizeScale, set: (obj, value) => { obj.sizeScale = value; } }, metadata: _metadata }, _sizeScale_initializers, _sizeScale_extraInitializers);
|
|
313
336
|
__esDecorate(this, null, _noControls_decorators, { kind: "accessor", name: "noControls", static: false, private: false, access: { has: obj => "noControls" in obj, get: obj => obj.noControls, set: (obj, value) => { obj.noControls = value; } }, metadata: _metadata }, _noControls_initializers, _noControls_extraInitializers);
|
|
337
|
+
__esDecorate(this, null, _quiet_decorators, { kind: "accessor", name: "quiet", static: false, private: false, access: { has: obj => "quiet" in obj, get: obj => obj.quiet, set: (obj, value) => { obj.quiet = value; } }, metadata: _metadata }, _quiet_initializers, _quiet_extraInitializers);
|
|
314
338
|
__esDecorate(this, null, _controlsCorner_decorators, { kind: "accessor", name: "controlsCorner", static: false, private: false, access: { has: obj => "controlsCorner" in obj, get: obj => obj.controlsCorner, set: (obj, value) => { obj.controlsCorner = value; } }, metadata: _metadata }, _controlsCorner_initializers, _controlsCorner_extraInitializers);
|
|
315
339
|
__esDecorate(this, null, _hover_decorators, { kind: "accessor", name: "hover", static: false, private: false, access: { has: obj => "hover" in obj, get: obj => obj.hover, set: (obj, value) => { obj.hover = value; } }, metadata: _metadata }, _hover_initializers, _hover_extraInitializers);
|
|
316
340
|
__esDecorate(this, null, _hoveredArtifact_decorators, { kind: "accessor", name: "hoveredArtifact", static: false, private: false, access: { has: obj => "hoveredArtifact" in obj, get: obj => obj.hoveredArtifact, set: (obj, value) => { obj.hoveredArtifact = value; } }, metadata: _metadata }, _hoveredArtifact_initializers, _hoveredArtifact_extraInitializers);
|
|
@@ -331,13 +355,26 @@ let MosaicaMap = (() => {
|
|
|
331
355
|
outline: none;
|
|
332
356
|
overflow: hidden;
|
|
333
357
|
}
|
|
358
|
+
/* Draws nothing: a colour that follows the scheme, whose transition tells the map that the
|
|
359
|
+
ground it draws on has changed. It is inside the map's own tree, where no rule of the
|
|
360
|
+
page's or of an element holding the map reaches it, and its transition outranks the one
|
|
361
|
+
the theme takes away under reduced motion. */
|
|
362
|
+
#ground {
|
|
363
|
+
position: absolute;
|
|
364
|
+
width: 0;
|
|
365
|
+
height: 0;
|
|
366
|
+
pointer-events: none;
|
|
367
|
+
text-decoration-color: light-dark(#000, #fff);
|
|
368
|
+
transition: text-decoration-color 1ms !important;
|
|
369
|
+
}
|
|
334
370
|
[part='canvas'] {
|
|
335
371
|
position: absolute;
|
|
336
372
|
inset: 0;
|
|
337
373
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
374
|
+
/* Inside the canvas's edge, which the map clips at. deck's own canvas takes the focus on a click. */
|
|
375
|
+
[part='canvas']:focus-visible,
|
|
376
|
+
[part='canvas'] canvas:focus-visible {
|
|
377
|
+
outline-offset: -2px;
|
|
341
378
|
}
|
|
342
379
|
:host([mode='box']) [part='canvas'],
|
|
343
380
|
:host([mode='lasso']) [part='canvas'] {
|
|
@@ -399,10 +436,14 @@ let MosaicaMap = (() => {
|
|
|
399
436
|
background: var(--_mosaica-accent);
|
|
400
437
|
color: var(--_mosaica-accent-ink);
|
|
401
438
|
}
|
|
402
|
-
/* The drawn region's count and clear button,
|
|
439
|
+
/* The drawn region's count and clear button, above its top-left corner. It and the tooltip
|
|
440
|
+
are laid out at the map's own corner, so neither is squeezed against an edge, and moved
|
|
441
|
+
to their places once their sizes are known. */
|
|
403
442
|
[part='region-tag'] {
|
|
404
443
|
position: absolute;
|
|
405
444
|
z-index: 3;
|
|
445
|
+
left: 0;
|
|
446
|
+
top: 0;
|
|
406
447
|
display: flex;
|
|
407
448
|
align-items: center;
|
|
408
449
|
gap: 6px;
|
|
@@ -414,7 +455,6 @@ let MosaicaMap = (() => {
|
|
|
414
455
|
font-size: 12px;
|
|
415
456
|
color: var(--_mosaica-ink-2);
|
|
416
457
|
white-space: nowrap;
|
|
417
|
-
transform: translateY(calc(-100% - 4px));
|
|
418
458
|
}
|
|
419
459
|
[part='region-tag'] mosaica-count {
|
|
420
460
|
font-size: inherit;
|
|
@@ -424,6 +464,7 @@ let MosaicaMap = (() => {
|
|
|
424
464
|
font-weight: 600;
|
|
425
465
|
}
|
|
426
466
|
[part='region-tag'] button {
|
|
467
|
+
position: relative;
|
|
427
468
|
width: 20px;
|
|
428
469
|
height: 20px;
|
|
429
470
|
display: grid;
|
|
@@ -431,21 +472,32 @@ let MosaicaMap = (() => {
|
|
|
431
472
|
border-radius: 4px;
|
|
432
473
|
color: var(--_mosaica-ink-2);
|
|
433
474
|
}
|
|
475
|
+
/* The pointer's target is 24 px square around the button, which the tag does not grow for. */
|
|
476
|
+
[part='region-tag'] button::before {
|
|
477
|
+
content: '';
|
|
478
|
+
position: absolute;
|
|
479
|
+
inset: -2px;
|
|
480
|
+
}
|
|
434
481
|
[part='region-tag'] button:hover {
|
|
435
482
|
background: var(--_mosaica-surface-2);
|
|
436
483
|
}
|
|
484
|
+
[part='region-tag'] button:focus-visible {
|
|
485
|
+
outline-offset: -2px;
|
|
486
|
+
}
|
|
437
487
|
[part='tooltip'] {
|
|
438
488
|
position: absolute;
|
|
439
489
|
z-index: 4;
|
|
490
|
+
left: 0;
|
|
491
|
+
top: 0;
|
|
440
492
|
pointer-events: none;
|
|
441
493
|
padding: 8px 10px;
|
|
442
|
-
max-width: 260px;
|
|
494
|
+
max-width: min(260px, 100%);
|
|
443
495
|
background: var(--_mosaica-surface);
|
|
444
496
|
border: 1px solid var(--_mosaica-line);
|
|
445
497
|
border-radius: var(--_mosaica-radius);
|
|
446
498
|
box-shadow: var(--_mosaica-shadow);
|
|
447
499
|
font-size: 12px;
|
|
448
|
-
|
|
500
|
+
overflow-wrap: anywhere;
|
|
449
501
|
}
|
|
450
502
|
[part='tooltip'] .t {
|
|
451
503
|
font-weight: 500;
|
|
@@ -465,7 +517,8 @@ let MosaicaMap = (() => {
|
|
|
465
517
|
justify-content: center;
|
|
466
518
|
pointer-events: none;
|
|
467
519
|
}
|
|
468
|
-
|
|
520
|
+
/* A card while the state has words to show; otherwise it is kept, undrawn, for what it announces. */
|
|
521
|
+
[part='overlay'] [part='state']:not(.sr-only) {
|
|
469
522
|
pointer-events: auto;
|
|
470
523
|
min-height: 40px;
|
|
471
524
|
padding: 6px 8px 6px 14px;
|
|
@@ -477,7 +530,7 @@ let MosaicaMap = (() => {
|
|
|
477
530
|
font-weight: 500;
|
|
478
531
|
color: var(--_mosaica-ink);
|
|
479
532
|
}
|
|
480
|
-
[part='overlay'] [part='state']:not(:has(button)) {
|
|
533
|
+
[part='overlay'] [part='state']:not(.sr-only, :has(button)) {
|
|
481
534
|
padding-right: 14px;
|
|
482
535
|
}
|
|
483
536
|
[part='density-key'] {
|
|
@@ -509,11 +562,13 @@ let MosaicaMap = (() => {
|
|
|
509
562
|
border: 0;
|
|
510
563
|
background: color-mix(in srgb, var(--_mosaica-surface) 80%, transparent);
|
|
511
564
|
}
|
|
565
|
+
/* The ramp's colours are what it shows, so forced colours leave them. */
|
|
512
566
|
[part='density-key'] .ramp {
|
|
513
567
|
display: block;
|
|
514
568
|
width: 120px;
|
|
515
569
|
height: 8px;
|
|
516
570
|
border-radius: 2px;
|
|
571
|
+
forced-color-adjust: none;
|
|
517
572
|
}
|
|
518
573
|
[part='density-key'] .ends {
|
|
519
574
|
display: grid;
|
|
@@ -545,14 +600,24 @@ let MosaicaMap = (() => {
|
|
|
545
600
|
/** How many marks to aim for on screen. `0` keeps the store's budget, which starts at 250000. */
|
|
546
601
|
get budget() { return this.#budget_accessor_storage; }
|
|
547
602
|
set budget(value) { this.#budget_accessor_storage = value; }
|
|
548
|
-
#
|
|
603
|
+
#clusterSpacing_accessor_storage = (__runInitializers(this, _budget_extraInitializers), __runInitializers(this, _clusterSpacing_initializers, 0));
|
|
549
604
|
/**
|
|
550
|
-
* The
|
|
551
|
-
*
|
|
605
|
+
* The side in CSS pixels of the square of the map each cluster is given (`Store.setClusterSpacing`),
|
|
606
|
+
* so the map shows about one cluster per square whatever its size: at 100, a map 1,200 by 800
|
|
607
|
+
* pixels shows 96. `0` keeps the store's setting; setting `0` after another value removes the
|
|
608
|
+
* limit.
|
|
609
|
+
*
|
|
610
|
+
* A `nested` or `dag` layer is cut to that many clusters. A `stacked` or `tiered` layer whose
|
|
611
|
+
* levels declare no zoom range and which `clusterLevels` leaves to follow the zoom is drawn at the
|
|
612
|
+
* finest level with no more than that many clusters in view and no tile in view holding the most a
|
|
613
|
+
* tile is served, so the world view shows a broad level and finer ones appear as the camera zooms
|
|
614
|
+
* in. Without a spacing only a full tile holds a level back, so such a layer is drawn at its
|
|
615
|
+
* finest level at every zoom until its tiles fill, world zoom included, and a `nested` or `dag`
|
|
616
|
+
* layer is drawn at its finest cut.
|
|
552
617
|
*/
|
|
553
|
-
get
|
|
554
|
-
set
|
|
555
|
-
#tooltipFields_accessor_storage = (__runInitializers(this,
|
|
618
|
+
get clusterSpacing() { return this.#clusterSpacing_accessor_storage; }
|
|
619
|
+
set clusterSpacing(value) { this.#clusterSpacing_accessor_storage = value; }
|
|
620
|
+
#tooltipFields_accessor_storage = (__runInitializers(this, _clusterSpacing_extraInitializers), __runInitializers(this, _tooltipFields_initializers, ''));
|
|
556
621
|
/**
|
|
557
622
|
* The columns the hover tooltip shows under the point's title, separated by spaces or commas.
|
|
558
623
|
* Only rendered columns arrive with the points, so another column shows nothing.
|
|
@@ -579,14 +644,17 @@ let MosaicaMap = (() => {
|
|
|
579
644
|
*/
|
|
580
645
|
get palette() { return this.#palette_accessor_storage; }
|
|
581
646
|
set palette(value) { this.#palette_accessor_storage = value; }
|
|
582
|
-
#
|
|
647
|
+
#clusterLevels_accessor_storage = (__runInitializers(this, _palette_extraInitializers), __runInitializers(this, _clusterLevels_initializers, new Map()));
|
|
583
648
|
/**
|
|
584
|
-
* The level
|
|
585
|
-
* outlined
|
|
649
|
+
* The level at which each `stacked` or `tiered` layer's clusters are coloured, labelled and
|
|
650
|
+
* outlined, keyed by layer name. A layer with no entry is drawn at the level that follows the zoom
|
|
651
|
+
* where its levels declare no zoom range (see `cluster-spacing`), and otherwise at the coarsest
|
|
652
|
+
* level the camera's zoom serves. A `nested` or `dag` layer ignores its entry. Set it as a
|
|
653
|
+
* property; it has no attribute.
|
|
586
654
|
*/
|
|
587
|
-
get
|
|
588
|
-
set
|
|
589
|
-
#basemap_accessor_storage = (__runInitializers(this,
|
|
655
|
+
get clusterLevels() { return this.#clusterLevels_accessor_storage; }
|
|
656
|
+
set clusterLevels(value) { this.#clusterLevels_accessor_storage = value; }
|
|
657
|
+
#basemap_accessor_storage = (__runInitializers(this, _clusterLevels_extraInitializers), __runInitializers(this, _basemap_initializers, null));
|
|
590
658
|
/**
|
|
591
659
|
* What is drawn under the points. `world` is the server's outline of land, coastlines and country
|
|
592
660
|
* borders, and needs a view with a map projection. `{tiles, attribution}` is a tile server's URL
|
|
@@ -604,8 +672,9 @@ let MosaicaMap = (() => {
|
|
|
604
672
|
#ground_accessor_storage = (__runInitializers(this, _basemap_extraInitializers), __runInitializers(this, _ground_initializers, ''));
|
|
605
673
|
/**
|
|
606
674
|
* Whether the map's background is `light` or `dark`, for when it differs from the page's, such as
|
|
607
|
-
* a light basemap on a dark page.
|
|
608
|
-
* follow the page. Unset, it follows the page's `color-scheme
|
|
675
|
+
* a light basemap on a dark page. The points' single colour, labels and density colours follow
|
|
676
|
+
* it; the toolbar and panels follow the page. Unset, it follows the page's `color-scheme`, and
|
|
677
|
+
* the system's where the page allows both.
|
|
609
678
|
*/
|
|
610
679
|
get ground() { return this.#ground_accessor_storage; }
|
|
611
680
|
set ground(value) { this.#ground_accessor_storage = value; }
|
|
@@ -751,7 +820,16 @@ let MosaicaMap = (() => {
|
|
|
751
820
|
/** Hides the toolbar. */
|
|
752
821
|
get noControls() { return this.#noControls_accessor_storage; }
|
|
753
822
|
set noControls(value) { this.#noControls_accessor_storage = value; }
|
|
754
|
-
#
|
|
823
|
+
#quiet_accessor_storage = (__runInitializers(this, _noControls_extraInitializers), __runInitializers(this, _quiet_initializers, false));
|
|
824
|
+
/**
|
|
825
|
+
* Leaves to another element what that element also says: the view's state over the canvas and
|
|
826
|
+
* the selected region's count are still shown, and neither is put in a live region. Set it when
|
|
827
|
+
* another element on the page says the same things, such as a status strip. What only the map
|
|
828
|
+
* knows is announced either way: the basemap's note, and a basemap it refuses.
|
|
829
|
+
*/
|
|
830
|
+
get quiet() { return this.#quiet_accessor_storage; }
|
|
831
|
+
set quiet(value) { this.#quiet_accessor_storage = value; }
|
|
832
|
+
#controlsCorner_accessor_storage = (__runInitializers(this, _quiet_extraInitializers), __runInitializers(this, _controlsCorner_initializers, 'top-left'));
|
|
755
833
|
/**
|
|
756
834
|
* The corner the toolbar sits in: `top-left`, `top-right`, `bottom-left` or `bottom-right`.
|
|
757
835
|
* Content in the same corner's slot sits below the toolbar in a top corner, and above it in a
|
|
@@ -794,7 +872,7 @@ let MosaicaMap = (() => {
|
|
|
794
872
|
marks: 0,
|
|
795
873
|
requests: 0,
|
|
796
874
|
densityRequests: 0,
|
|
797
|
-
encoding: 'uniform',
|
|
875
|
+
encoding: 'uniform|dark',
|
|
798
876
|
basemap: [],
|
|
799
877
|
view: { depth: 0, status: 'idle', stale: false, visible: 0, matched: 0, served: 0, provisional: 0 },
|
|
800
878
|
region: null,
|
|
@@ -865,6 +943,7 @@ let MosaicaMap = (() => {
|
|
|
865
943
|
super.connectedCallback();
|
|
866
944
|
this.askPointColumns(this.resolvedStore);
|
|
867
945
|
this.syncBasemap();
|
|
946
|
+
this.watchTooltipKeys();
|
|
868
947
|
if (this.finalizeTimer) {
|
|
869
948
|
clearTimeout(this.finalizeTimer);
|
|
870
949
|
this.finalizeTimer = null;
|
|
@@ -892,6 +971,9 @@ let MosaicaMap = (() => {
|
|
|
892
971
|
clearTimeout(this.describeTimer);
|
|
893
972
|
this.describeTimer = null;
|
|
894
973
|
}
|
|
974
|
+
this.watchTooltipKeys();
|
|
975
|
+
this.tagSized?.disconnect();
|
|
976
|
+
this.tagBox = null;
|
|
895
977
|
this.stopFrameLoop();
|
|
896
978
|
this.finalizeTimer = setTimeout(() => {
|
|
897
979
|
this.finalizeTimer = null;
|
|
@@ -901,6 +983,9 @@ let MosaicaMap = (() => {
|
|
|
901
983
|
}
|
|
902
984
|
updated(changed) {
|
|
903
985
|
this.ensureDeck();
|
|
986
|
+
this.watchTooltipKeys();
|
|
987
|
+
this.placeFloats();
|
|
988
|
+
this.actionFocus.keep(this.focusTarget);
|
|
904
989
|
if (changed.has('measure')) {
|
|
905
990
|
if (this.measure && this.isConnected)
|
|
906
991
|
this.startFrameLoop();
|
|
@@ -915,7 +1000,7 @@ let MosaicaMap = (() => {
|
|
|
915
1000
|
this.drawnBasemap.reset();
|
|
916
1001
|
if (changed.has('basemap') || changed.has('ground'))
|
|
917
1002
|
this.syncBasemap();
|
|
918
|
-
const repaint = ['mode', 'drag', 'dragPolygon', 'basemap', 'ground', 'radius', '
|
|
1003
|
+
const repaint = ['mode', 'drag', 'dragPolygon', 'basemap', 'ground', 'radius', 'clusterLevels', 'hoveredArtifact', 'noPoints', 'pointOpacity', 'density', 'densityColours', 'densityStrength', 'densityScale'];
|
|
919
1004
|
if (repaint.some((k) => changed.has(k)))
|
|
920
1005
|
this.paint();
|
|
921
1006
|
}
|
|
@@ -1017,6 +1102,17 @@ let MosaicaMap = (() => {
|
|
|
1017
1102
|
return 'dark';
|
|
1018
1103
|
return typeof matchMedia !== 'undefined' && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
1019
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Draw for the other ground when the page's or the system's colour scheme changes, which the
|
|
1107
|
+
* colour of the `#ground` element follows: the basemap, the points and names, and the density key.
|
|
1108
|
+
*/
|
|
1109
|
+
onGroundChange = () => {
|
|
1110
|
+
if (this.ground !== '')
|
|
1111
|
+
return;
|
|
1112
|
+
this.syncBasemap();
|
|
1113
|
+
this.paint();
|
|
1114
|
+
this.requestUpdate();
|
|
1115
|
+
};
|
|
1020
1116
|
resetServerData() {
|
|
1021
1117
|
// A hover's title is a record the server answered, and a pick names an item it served.
|
|
1022
1118
|
this.hover = null;
|
|
@@ -1073,6 +1169,11 @@ let MosaicaMap = (() => {
|
|
|
1073
1169
|
}
|
|
1074
1170
|
/** A store adopted since the last update, to which the host's settings are not yet written. */
|
|
1075
1171
|
unpushed = null;
|
|
1172
|
+
/** Keeps the focus on the map when the overlay's Retry goes with its state. */
|
|
1173
|
+
actionFocus = new ActionFocus();
|
|
1174
|
+
emptyAnswer = new EmptyAnswer();
|
|
1175
|
+
/** Whether the view is empty as a query's answer, which is announced, and not where a pan went. */
|
|
1176
|
+
emptyAnswered = false;
|
|
1076
1177
|
/**
|
|
1077
1178
|
* Write the host's settings to the store before the render: every one to a store adopted since
|
|
1078
1179
|
* the last update, else those that changed. `layers` set or changed in this update fires
|
|
@@ -1082,7 +1183,11 @@ let MosaicaMap = (() => {
|
|
|
1082
1183
|
*/
|
|
1083
1184
|
willUpdate(changed) {
|
|
1084
1185
|
super.willUpdate(changed);
|
|
1186
|
+
this.actionFocus.note(this.renderRoot);
|
|
1085
1187
|
const s = this.resolvedStore;
|
|
1188
|
+
const status = s?.get('status') ?? null;
|
|
1189
|
+
this.emptyAnswered = this.emptyAnswer.note(stateOf(status), status?.querying === true);
|
|
1190
|
+
this.hearRegion();
|
|
1086
1191
|
if (!s)
|
|
1087
1192
|
return;
|
|
1088
1193
|
const all = this.unpushed === s;
|
|
@@ -1099,11 +1204,11 @@ let MosaicaMap = (() => {
|
|
|
1099
1204
|
emit(this, 'mosaica-layerchange', { layers: this.layers });
|
|
1100
1205
|
if (touched('budget') && this.budget > 0)
|
|
1101
1206
|
s.setBudget(this.budget);
|
|
1102
|
-
if (touched('
|
|
1103
|
-
if (this.
|
|
1104
|
-
s.
|
|
1105
|
-
else if (!all && (changed.get('
|
|
1106
|
-
s.
|
|
1207
|
+
if (touched('clusterSpacing')) {
|
|
1208
|
+
if (this.clusterSpacing > 0)
|
|
1209
|
+
s.setClusterSpacing(this.clusterSpacing);
|
|
1210
|
+
else if (!all && (changed.get('clusterSpacing') ?? 0) > 0)
|
|
1211
|
+
s.setClusterSpacing(null);
|
|
1107
1212
|
}
|
|
1108
1213
|
if (touched('palette') && this.palette !== '')
|
|
1109
1214
|
s.setPalette(this.palette);
|
|
@@ -1158,7 +1263,7 @@ let MosaicaMap = (() => {
|
|
|
1158
1263
|
};
|
|
1159
1264
|
const legend = s.get('legend');
|
|
1160
1265
|
const clusterLayer = clusterLayerOf(legend.colourBy);
|
|
1161
|
-
p.encoding = clusterLayer ? `cluster|${clusterLayer}` : encodingSignature(encodingOf(s.get('meta'), legend, colouringOf(s)));
|
|
1266
|
+
p.encoding = clusterLayer ? `cluster|${clusterLayer}` : encodingSignature(encodingOf(s.get('meta'), legend, colouringOf(s), this.scheme()));
|
|
1162
1267
|
if (this.measure)
|
|
1163
1268
|
this.measureStore(s);
|
|
1164
1269
|
// Events for what arrived: the selected item's record, the opened artifact, the region's counts.
|
|
@@ -1257,15 +1362,16 @@ let MosaicaMap = (() => {
|
|
|
1257
1362
|
return;
|
|
1258
1363
|
this.probedArtifacts = artifacts;
|
|
1259
1364
|
this.probedComposition = view.composition;
|
|
1260
|
-
this.probe.cluster = this.clusterProbe(
|
|
1365
|
+
this.probe.cluster = this.clusterProbe(s.get('legend').colourBy, artifacts, s.get('marks').bands);
|
|
1261
1366
|
}
|
|
1262
1367
|
/** A sample of carried ordinals, each resolved through the table; see {@link MapProbe.cluster}. */
|
|
1263
|
-
clusterProbe(
|
|
1264
|
-
const
|
|
1368
|
+
clusterProbe(colourBy, a, bands) {
|
|
1369
|
+
const clusterLayer = clusterLayerOf(colourBy);
|
|
1370
|
+
const layer = colourLayerOf(a, colourBy) ?? null;
|
|
1265
1371
|
const rows = clusterLayer ? a.colourServed : a.served;
|
|
1266
1372
|
const rowOrdinals = clusterLayer ? new Set(rows.map((x) => a.table.ordinalOf(x.layer, x.mosaicaId))) : a.servedOrdinals;
|
|
1267
1373
|
const sample = [];
|
|
1268
|
-
const level = this.
|
|
1374
|
+
const level = this.levelOf(layer);
|
|
1269
1375
|
let coloured = 0;
|
|
1270
1376
|
if (layer) {
|
|
1271
1377
|
for (const band of bands) {
|
|
@@ -1305,8 +1411,7 @@ let MosaicaMap = (() => {
|
|
|
1305
1411
|
parent,
|
|
1306
1412
|
views: VIEW,
|
|
1307
1413
|
viewState: this.viewState,
|
|
1308
|
-
|
|
1309
|
-
controller: { keyboard: false },
|
|
1414
|
+
controller: this.controller(),
|
|
1310
1415
|
pickingRadius: MosaicaMap.PICK_RADIUS_PX,
|
|
1311
1416
|
// Presses draw no pick, which would count against the press's time to be a click (`clickPick`).
|
|
1312
1417
|
layerFilter: ({ isPicking, renderPass }) => !isPicking || renderPass.startsWith('picking:hover') || this.picking,
|
|
@@ -1331,7 +1436,9 @@ let MosaicaMap = (() => {
|
|
|
1331
1436
|
return viewState;
|
|
1332
1437
|
},
|
|
1333
1438
|
onClick: (info) => this.onClick(this.clickPick(info)),
|
|
1334
|
-
onHover: (info) => this.onHover(info)
|
|
1439
|
+
onHover: (info) => this.onHover(info),
|
|
1440
|
+
// The store's cluster count follows the canvas's size.
|
|
1441
|
+
onResize: () => this.pushView()
|
|
1335
1442
|
});
|
|
1336
1443
|
this.paint();
|
|
1337
1444
|
}
|
|
@@ -1375,7 +1482,7 @@ let MosaicaMap = (() => {
|
|
|
1375
1482
|
id: 'mosaica',
|
|
1376
1483
|
store: s,
|
|
1377
1484
|
slab: this.slab,
|
|
1378
|
-
|
|
1485
|
+
clusterLevels: this.clusterLevels,
|
|
1379
1486
|
selectedWorldXY: this.selectedWorldXY,
|
|
1380
1487
|
openedArtifact: s.get('selection').artifact?.id ?? null,
|
|
1381
1488
|
hoveredArtifact: this.hoveredArtifact,
|
|
@@ -1420,16 +1527,19 @@ let MosaicaMap = (() => {
|
|
|
1420
1527
|
}
|
|
1421
1528
|
}));
|
|
1422
1529
|
}
|
|
1423
|
-
this.deck.setProps({
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1530
|
+
this.deck.setProps({ layers, viewState: this.viewState, controller: this.controller() });
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* What deck's own input handling does. Its keyboard is off in every mode, since the map takes
|
|
1534
|
+
* the keys itself and a key would otherwise move the camera twice. In box and lasso modes the
|
|
1535
|
+
* drag is the selection's; the wheel still zooms.
|
|
1536
|
+
*/
|
|
1537
|
+
controller() {
|
|
1538
|
+
return this.mode === 'box' || this.mode === 'lasso' ? { keyboard: false, dragPan: false, dragRotate: false } : { keyboard: false };
|
|
1429
1539
|
}
|
|
1430
1540
|
/**
|
|
1431
1541
|
* The shapes a hover or click is resolved against ({@link contourShapes}), held until the served
|
|
1432
|
-
* set, the fetched shapes, the
|
|
1542
|
+
* set, the fetched shapes, the levels or the roster change. Each is the artifact's box until its
|
|
1433
1543
|
* shape is fetched; where boxes overlap, depth and the mark under the pointer decide.
|
|
1434
1544
|
*/
|
|
1435
1545
|
contoursHeld = null;
|
|
@@ -1439,13 +1549,14 @@ let MosaicaMap = (() => {
|
|
|
1439
1549
|
if (!a)
|
|
1440
1550
|
return [];
|
|
1441
1551
|
const meta = s?.get('meta') ?? null;
|
|
1442
|
-
const
|
|
1552
|
+
const levels = this.drawnLevels();
|
|
1553
|
+
const key = levelsKey(levels);
|
|
1443
1554
|
const opened = s?.get('selection').artifact?.id ?? null;
|
|
1444
1555
|
const held = this.contoursHeld;
|
|
1445
|
-
if (held && held.served === a.served && held.fetched === a.shapes && held.
|
|
1556
|
+
if (held && held.served === a.served && held.fetched === a.shapes && held.levels === key && held.meta === meta && held.opened === opened)
|
|
1446
1557
|
return held.shapes;
|
|
1447
|
-
const shapes = contourShapes(a, {
|
|
1448
|
-
this.contoursHeld = { served: a.served, fetched: a.shapes,
|
|
1558
|
+
const shapes = contourShapes(a, { levels, meta, opened });
|
|
1559
|
+
this.contoursHeld = { served: a.served, fetched: a.shapes, levels: key, meta, opened, shapes };
|
|
1449
1560
|
return shapes;
|
|
1450
1561
|
}
|
|
1451
1562
|
/** How far past a hovered shape's edge the pointer goes before the hover lets go, in pixels. */
|
|
@@ -1466,7 +1577,7 @@ let MosaicaMap = (() => {
|
|
|
1466
1577
|
/** What decides what is under a pixel: the camera, the map's size, and the marks and names drawn and their sizes. */
|
|
1467
1578
|
drawnKey() {
|
|
1468
1579
|
const s = this.resolvedStore;
|
|
1469
|
-
return [this.viewState, this.clientWidth, this.clientHeight, s?.get('marks'), s?.get('artifacts'), s?.get('legend'), s?.get('selection').artifact?.id, this.
|
|
1580
|
+
return [this.viewState, this.clientWidth, this.clientHeight, s?.get('marks'), s?.get('artifacts'), s?.get('legend'), s?.get('selection').artifact?.id, levelsKey(this.drawnLevels()), this.noPoints, this.radius];
|
|
1470
1581
|
}
|
|
1471
1582
|
/** Whether the map is running a pick of its own, which the deck's `layerFilter` lets draw. */
|
|
1472
1583
|
picking = false;
|
|
@@ -1497,13 +1608,16 @@ let MosaicaMap = (() => {
|
|
|
1497
1608
|
const at = slot ? this.slab.markAt(Number(slot[1]), info.index) : null;
|
|
1498
1609
|
const artifacts = this.resolvedStore?.get('artifacts') ?? null;
|
|
1499
1610
|
// The mark's own artifact, preferred where shapes interleave.
|
|
1500
|
-
const own = at && artifacts ? artifactOfMark(at.band, at.i, artifacts, this.
|
|
1611
|
+
const own = at && artifacts ? artifactOfMark(at.band, at.i, artifacts, this.levelOf(artifacts.layers[0])) : null;
|
|
1501
1612
|
const world = this.worldAt(info.x, info.y);
|
|
1502
1613
|
this.hoveredArtifact = picked.kind === 'artifact' ? picked.id : world ? this.artifactAt(world, own) : null;
|
|
1503
1614
|
// The viewport carries no shapes; fetch the hovered one. `needShape` asks once per artifact.
|
|
1504
1615
|
if (this.hoveredArtifact !== null)
|
|
1505
1616
|
this.resolvedStore?.needShape(this.hoveredArtifact);
|
|
1506
|
-
|
|
1617
|
+
// A tooltip Escape dismissed stays away while the pointer is on its mark.
|
|
1618
|
+
if (picked.kind !== 'mark' || picked.id !== this.dismissedHover)
|
|
1619
|
+
this.dismissedHover = null;
|
|
1620
|
+
if (picked.kind !== 'mark' || this.pressed || picked.id === this.dismissedHover || picked.id === this.resolvedStore?.get('selection').item?.id) {
|
|
1507
1621
|
if (this.hover)
|
|
1508
1622
|
this.hover = null;
|
|
1509
1623
|
return;
|
|
@@ -1554,6 +1668,94 @@ let MosaicaMap = (() => {
|
|
|
1554
1668
|
});
|
|
1555
1669
|
}, HOVER_DESCRIBE_MS);
|
|
1556
1670
|
}
|
|
1671
|
+
/** The mark whose tooltip Escape dismissed, while the pointer stays on it. */
|
|
1672
|
+
dismissedHover = null;
|
|
1673
|
+
/** Whether the document is listened to for the Escape that dismisses the tooltip. */
|
|
1674
|
+
tooltipKeys = false;
|
|
1675
|
+
/** Listen to the document for Escape while a tooltip shows, wherever the focus is. */
|
|
1676
|
+
watchTooltipKeys() {
|
|
1677
|
+
const on = this.hover !== null && this.isConnected;
|
|
1678
|
+
if (on === this.tooltipKeys)
|
|
1679
|
+
return;
|
|
1680
|
+
this.tooltipKeys = on;
|
|
1681
|
+
if (on)
|
|
1682
|
+
this.ownerDocument.addEventListener('keydown', this.onTooltipKey, true);
|
|
1683
|
+
else
|
|
1684
|
+
this.ownerDocument.removeEventListener('keydown', this.onTooltipKey, true);
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Escape dismisses the tooltip. Pressed in the map, or in an element whose shadow tree holds the
|
|
1688
|
+
* map, such as an explorer, it does nothing else: it is marked as taken, so the same press clears
|
|
1689
|
+
* no selection there. Pressed anywhere else on the page it is left as it was, for the page.
|
|
1690
|
+
*/
|
|
1691
|
+
onTooltipKey = (e) => {
|
|
1692
|
+
if (e.key !== 'Escape' || !this.hover)
|
|
1693
|
+
return;
|
|
1694
|
+
this.dismissedHover = this.hover.id;
|
|
1695
|
+
this.hover = null;
|
|
1696
|
+
const path = e.composedPath();
|
|
1697
|
+
for (let at = this; at;) {
|
|
1698
|
+
if (path.includes(at)) {
|
|
1699
|
+
e.preventDefault();
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
const root = at.getRootNode();
|
|
1703
|
+
at = root instanceof ShadowRoot ? root.host : null;
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
/** The region's tag as last measured: its size, the map's, and how far in the top-left corner's content starts. */
|
|
1707
|
+
tagBox = null;
|
|
1708
|
+
/** Measures the tag again when it or the map changes size, so that placing it as the camera moves reads no layout. */
|
|
1709
|
+
tagSized = typeof ResizeObserver === 'undefined'
|
|
1710
|
+
? null
|
|
1711
|
+
: new ResizeObserver(() => {
|
|
1712
|
+
if (!this.tagBox)
|
|
1713
|
+
return;
|
|
1714
|
+
this.measureTag(this.tagBox.tag);
|
|
1715
|
+
this.placeTag();
|
|
1716
|
+
});
|
|
1717
|
+
measureTag(tag) {
|
|
1718
|
+
// No further left than the top-left corner's content starts, right of whatever the host keeps
|
|
1719
|
+
// over the map's left side.
|
|
1720
|
+
const inset = this.renderRoot.querySelector('.corner.top-left')?.offsetLeft ?? 0;
|
|
1721
|
+
const { width, height } = this.size;
|
|
1722
|
+
this.tagBox = { tag, size: [tag.offsetWidth, tag.offsetHeight], map: [width, height], inset };
|
|
1723
|
+
}
|
|
1724
|
+
/** Put the region's tag above its region's top-left corner, inside the map, from the sizes last measured. */
|
|
1725
|
+
placeTag() {
|
|
1726
|
+
const corner = this.regionCorner();
|
|
1727
|
+
if (!this.tagBox || !corner)
|
|
1728
|
+
return;
|
|
1729
|
+
const { tag, size, map, inset } = this.tagBox;
|
|
1730
|
+
const scale = 2 ** this.viewState.zoom;
|
|
1731
|
+
const [x, y] = [(corner[0] - this.viewState.target[0]) * scale + map[0] / 2, (corner[1] - this.viewState.target[1]) * scale + map[1] / 2];
|
|
1732
|
+
const [left, top] = keptInside([x, y - size[1] - REGION_TAG_GAP_PX], size, [inset, 0], map);
|
|
1733
|
+
tag.style.transform = `translate(${left}px, ${top}px)`;
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Move the tooltip beside the pointer and the region's tag above its region's corner, each kept
|
|
1737
|
+
* inside the map. The tooltip is measured each time it is drawn; the tag when it is first drawn
|
|
1738
|
+
* and when it or the map changes size.
|
|
1739
|
+
*/
|
|
1740
|
+
placeFloats() {
|
|
1741
|
+
const tip = this.renderRoot.querySelector('[part="tooltip"]');
|
|
1742
|
+
if (tip && this.hover) {
|
|
1743
|
+
const { width, height } = this.size;
|
|
1744
|
+
const [left, top] = tooltipCorner([this.hover.x, this.hover.y], [tip.offsetWidth, tip.offsetHeight], [width, height]);
|
|
1745
|
+
tip.style.transform = `translate(${left}px, ${top}px)`;
|
|
1746
|
+
}
|
|
1747
|
+
const tag = this.renderRoot.querySelector('[part="region-tag"]');
|
|
1748
|
+
if (tag !== (this.tagBox?.tag ?? null)) {
|
|
1749
|
+
this.tagSized?.disconnect();
|
|
1750
|
+
this.tagBox = null;
|
|
1751
|
+
if (tag) {
|
|
1752
|
+
this.measureTag(tag);
|
|
1753
|
+
this.tagSized?.observe(tag);
|
|
1754
|
+
this.tagSized?.observe(this);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
this.placeTag();
|
|
1758
|
+
}
|
|
1557
1759
|
/** The element that takes the map's focus: the canvas, a tab stop of its own. @internal */
|
|
1558
1760
|
get focusTarget() {
|
|
1559
1761
|
return this.renderRoot.querySelector('[part="canvas"]');
|
|
@@ -1744,38 +1946,57 @@ let MosaicaMap = (() => {
|
|
|
1744
1946
|
this.requestUpdate();
|
|
1745
1947
|
}
|
|
1746
1948
|
/**
|
|
1747
|
-
* The drawn region's tag
|
|
1748
|
-
*
|
|
1949
|
+
* The drawn region's tag: how many items match inside it (or outside, for its complement) and a
|
|
1950
|
+
* button that clears it. A region drawn as an artifact's outline has none. {@link placeFloats}
|
|
1951
|
+
* puts it above the region's top-left corner.
|
|
1749
1952
|
*/
|
|
1750
1953
|
regionTag() {
|
|
1751
1954
|
const region = this.resolvedStore?.get('region');
|
|
1752
|
-
if (!region)
|
|
1753
|
-
return nothing;
|
|
1754
|
-
let corner = null;
|
|
1755
|
-
if (this.regionWorld)
|
|
1756
|
-
corner = [this.regionWorld[0], this.regionWorld[1]];
|
|
1757
|
-
else if (this.regionPolygon && this.regionPolygon.length > 0) {
|
|
1758
|
-
corner = [Math.min(...this.regionPolygon.map((p) => p[0])), Math.min(...this.regionPolygon.map((p) => p[1]))];
|
|
1759
|
-
}
|
|
1760
|
-
if (!corner)
|
|
1955
|
+
if (!region || !this.regionCorner())
|
|
1761
1956
|
return nothing;
|
|
1762
|
-
|
|
1763
|
-
const { width, height } = this.size;
|
|
1764
|
-
const scale = 2 ** this.viewState.zoom;
|
|
1765
|
-
const [tx, ty] = this.viewState.target;
|
|
1766
|
-
// Kept inside the map where the corner is off its top edge, and no further left than the
|
|
1767
|
-
// top-left corner's content starts, right of whatever the host keeps over the map's left side.
|
|
1768
|
-
const left = (corner[0] - tx) * scale + width / 2;
|
|
1769
|
-
const top = Math.max(30, (corner[1] - ty) * scale + height / 2);
|
|
1770
|
-
return html `<div part="region-tag" style=${`left:max(calc(var(--_mosaica-space) + var(--mosaica-map-inset-left, 0px)), ${left}px);top:${top}px`}>
|
|
1957
|
+
return html `<div part="region-tag">
|
|
1771
1958
|
<mosaica-count .masked=${region.matched}></mosaica-count><span>${region.shape.outside ? 'outside' : 'inside'}</span>
|
|
1772
1959
|
<button type="button" aria-label="Clear selection" title="Clear selection" @click=${() => this.select(null)}>${icon('close', 12, 1.4)}</button>
|
|
1773
1960
|
</div>`;
|
|
1774
1961
|
}
|
|
1775
|
-
/** The
|
|
1776
|
-
|
|
1962
|
+
/** The top-left corner of the drawn region's bounds, in world coordinates; `null` where no shape is drawn. */
|
|
1963
|
+
regionCorner() {
|
|
1964
|
+
if (this.regionWorld)
|
|
1965
|
+
return [this.regionWorld[0], this.regionWorld[1]];
|
|
1966
|
+
if (!this.regionPolygon || this.regionPolygon.length === 0)
|
|
1967
|
+
return null;
|
|
1968
|
+
return [Math.min(...this.regionPolygon.map((p) => p[0])), Math.min(...this.regionPolygon.map((p) => p[1]))];
|
|
1969
|
+
}
|
|
1970
|
+
/** What the map last announced of the selected region, and the shape it said it of. */
|
|
1971
|
+
regionHeard = '';
|
|
1972
|
+
heardShape = null;
|
|
1973
|
+
/**
|
|
1974
|
+
* Decide what the map announces of the selected region: how many items match inside it (or
|
|
1975
|
+
* outside), once the query that counts it has finished loading, since the figure a frame gives
|
|
1976
|
+
* first is an estimate. The words change only with the figure or the shape, so a count is
|
|
1977
|
+
* announced once, whatever else the map says in the meantime.
|
|
1978
|
+
*/
|
|
1979
|
+
hearRegion() {
|
|
1980
|
+
const s = this.resolvedStore;
|
|
1981
|
+
const region = s?.get('region') ?? null;
|
|
1982
|
+
const shape = region?.shape ?? null;
|
|
1983
|
+
if (shape !== this.heardShape) {
|
|
1984
|
+
this.heardShape = shape;
|
|
1985
|
+
this.regionHeard = '';
|
|
1986
|
+
}
|
|
1987
|
+
if (!s || region?.status !== 'shown' || s.get('status').querying)
|
|
1988
|
+
return;
|
|
1989
|
+
this.regionHeard = `${formatMasked(region.matched)} ${region.shape.outside ? 'outside' : 'inside'} the selection`;
|
|
1990
|
+
}
|
|
1991
|
+
/** The level the layer `name` is drawn at: its entry in `clusterLevels`, else the level in view of a levelled layer. */
|
|
1992
|
+
levelOf(name) {
|
|
1777
1993
|
const s = this.resolvedStore;
|
|
1778
|
-
return drawnLevel(s?.get('meta') ?? null, s?.get('artifacts') ?? null,
|
|
1994
|
+
return drawnLevel(s?.get('meta') ?? null, s?.get('artifacts') ?? null, name ?? undefined, this.clusterLevels);
|
|
1995
|
+
}
|
|
1996
|
+
/** {@link levelOf} for each layer drawn. */
|
|
1997
|
+
drawnLevels() {
|
|
1998
|
+
const s = this.resolvedStore;
|
|
1999
|
+
return drawnLevels(s?.get('meta') ?? null, s?.get('artifacts') ?? null, this.clusterLevels);
|
|
1779
2000
|
}
|
|
1780
2001
|
/** Whether the map is drawn on a light or dark background now: `ground` if set, else the page's scheme. */
|
|
1781
2002
|
get drawnGround() {
|
|
@@ -1866,8 +2087,9 @@ let MosaicaMap = (() => {
|
|
|
1866
2087
|
return true;
|
|
1867
2088
|
}
|
|
1868
2089
|
onKey = (e) => {
|
|
1869
|
-
// From the canvas's container, or deck's canvas inside it, which a click focuses.
|
|
1870
|
-
|
|
2090
|
+
// From the canvas's container, or deck's canvas inside it, which a click focuses. A key that
|
|
2091
|
+
// has already dismissed the tooltip does no more.
|
|
2092
|
+
if (e.defaultPrevented || (e.target !== e.currentTarget && !(e.target instanceof HTMLCanvasElement)))
|
|
1871
2093
|
return;
|
|
1872
2094
|
const step = 64 / 2 ** this.viewState.zoom;
|
|
1873
2095
|
const [x, y] = this.viewState.target;
|
|
@@ -1904,6 +2126,10 @@ let MosaicaMap = (() => {
|
|
|
1904
2126
|
else if (this.pickedAt || this.selectedWorldXY) {
|
|
1905
2127
|
this.clearPick();
|
|
1906
2128
|
}
|
|
2129
|
+
else {
|
|
2130
|
+
// Nothing to clear, so the key is left for whatever holds the map, such as a dialog.
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
1907
2133
|
break;
|
|
1908
2134
|
default:
|
|
1909
2135
|
return;
|
|
@@ -1953,22 +2179,25 @@ let MosaicaMap = (() => {
|
|
|
1953
2179
|
const scale = drawnDensityScale(this.densityScale);
|
|
1954
2180
|
const middle = Math.round(densityCountAt(0.5, max, scale));
|
|
1955
2181
|
const label = `Density in ${DENSITY_COLOUR_TITLES[colours]}, ${max > 0 ? `from 0 to ${countText(max)} items per cell on a ${scale} scale` : 'from fewer items to more'}`;
|
|
1956
|
-
|
|
2182
|
+
// What the largest figure counts is a tooltip on the figure and, for a reader of the key's
|
|
2183
|
+
// name, its description.
|
|
2184
|
+
const densest = 'The densest cell in view and around it';
|
|
2185
|
+
return html `<div part="density-key" role="img" aria-label=${label} aria-describedby=${max > 0 ? 'densest' : nothing}>
|
|
1957
2186
|
<span class="caption">${max > 0 ? 'Items per cell' : 'Density'}</span>
|
|
1958
2187
|
<span class="ramp" style=${`background:linear-gradient(to right, ${stops.join(', ')})`}></span>
|
|
1959
2188
|
<span class="ends"
|
|
1960
2189
|
>${max > 0
|
|
1961
|
-
? html `<span>0</span><span>${middle >= 1 && middle < max ? countText(middle) : ''}</span><span title
|
|
2190
|
+
? html `<span>0</span><span>${middle >= 1 && middle < max ? countText(middle) : ''}</span><span title=${densest}>${countText(max)}</span>`
|
|
1962
2191
|
: html `<span>Fewer</span><span></span><span>More items</span>`}</span
|
|
1963
|
-
>
|
|
2192
|
+
>${max > 0 ? html `<span id="densest" class="sr-only">${densest}</span>` : nothing}
|
|
1964
2193
|
</div>`;
|
|
1965
2194
|
}
|
|
1966
2195
|
/** The note saying why no basemap is drawn under this view, and the tile server's credit. */
|
|
1967
2196
|
basemapCorner() {
|
|
1968
2197
|
const { note, attribution } = this.drawnBasemap;
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
return html
|
|
2198
|
+
// The note's element is there before its words are, so they are announced, and is not drawn
|
|
2199
|
+
// while it has none.
|
|
2200
|
+
return html `<span part="note" class=${note ? nothing : 'sr-only'} role="status">${note ?? nothing}</span>${attribution ? html `<span part="attribution">${attribution}</span>` : nothing}`;
|
|
1972
2201
|
}
|
|
1973
2202
|
render() {
|
|
1974
2203
|
const status = this.resolvedStore?.get('status') ?? null;
|
|
@@ -1979,13 +2208,23 @@ let MosaicaMap = (() => {
|
|
|
1979
2208
|
// strip's alone.
|
|
1980
2209
|
const refresh = () => this.resolvedStore?.refresh();
|
|
1981
2210
|
const nothingVisible = seesNothing(this.resolvedStore, state);
|
|
1982
|
-
const
|
|
1983
|
-
?
|
|
2211
|
+
const ofView = nothingVisible
|
|
2212
|
+
? { state, line: { dot: null, words: NOTHING_VISIBLE, action: nothing } }
|
|
1984
2213
|
: state === 'refused' || state === 'unanswered' || state === 'busy' || state === 'expired' || state === 'empty' || (state === 'retrying' && status?.unreachable)
|
|
1985
|
-
?
|
|
1986
|
-
:
|
|
1987
|
-
|
|
1988
|
-
|
|
2214
|
+
? { state, line: stateLine(state, status, { onRefresh: refresh, onRetry: refresh }) }
|
|
2215
|
+
: null;
|
|
2216
|
+
const said = ofView ?? (this.drawnBasemap.refusal ? { state: 'refused', line: refusedLine(this.drawnBasemap.refusal, 'basemap') } : null);
|
|
2217
|
+
// One template whatever the state, so the live element is there before its words are and
|
|
2218
|
+
// each change of them is announced. With no state to show it is not drawn. Nothing in view
|
|
2219
|
+
// is announced as a query's answer; where a pan went, it is shown and not said. `quiet`
|
|
2220
|
+
// leaves the view's states to another element; a basemap's refusal is the map's alone to
|
|
2221
|
+
// say. The selected region's count has a live element of its own, so it is said once,
|
|
2222
|
+
// whatever the state does.
|
|
2223
|
+
const unsaid = ofView !== null && (this.quiet || (state === 'empty' && !nothingVisible && !this.emptyAnswered));
|
|
2224
|
+
const overlay = html `<div part="overlay">
|
|
2225
|
+
<span part="state" class=${said ? nothing : 'sr-only'} data-state=${said?.state ?? state} data-visible=${nothingVisible ? 'none' : nothing}>${liveLine(said?.line ?? NO_LINE, unsaid ? UNSAID : null)}</span>
|
|
2226
|
+
<span class="sr-only" role="status">${this.quiet ? nothing : this.regionHeard}</span>
|
|
2227
|
+
</div>`;
|
|
1989
2228
|
const controls = this.noControls
|
|
1990
2229
|
? nothing
|
|
1991
2230
|
: html `<div part="controls" role="toolbar" aria-label="Map tools">
|
|
@@ -2014,12 +2253,14 @@ let MosaicaMap = (() => {
|
|
|
2014
2253
|
part="canvas"
|
|
2015
2254
|
tabindex=${this.canvasTab}
|
|
2016
2255
|
role="application"
|
|
2017
|
-
aria-label=${this.getAttribute('aria-label') ?? '
|
|
2256
|
+
aria-label=${this.getAttribute('aria-label') ?? 'Map'}
|
|
2257
|
+
aria-describedby="keys"
|
|
2018
2258
|
@keydown=${this.onKey}
|
|
2019
2259
|
@pointerleave=${() => {
|
|
2020
2260
|
// deck reports picks only while the pointer is over the canvas, so clear the hover here.
|
|
2021
2261
|
if (this.hover)
|
|
2022
2262
|
this.hover = null;
|
|
2263
|
+
this.dismissedHover = null;
|
|
2023
2264
|
}}
|
|
2024
2265
|
@pointerdown=${{ handleEvent: this.onPointerDown, capture: true }}
|
|
2025
2266
|
@pointermove=${{ handleEvent: this.onPointerMove, capture: true }}
|
|
@@ -2027,11 +2268,13 @@ let MosaicaMap = (() => {
|
|
|
2027
2268
|
@pointercancel=${{ handleEvent: this.onPointerUp, capture: true }}
|
|
2028
2269
|
@lostpointercapture=${{ handleEvent: this.onPointerUp, capture: true }}
|
|
2029
2270
|
></div>
|
|
2271
|
+
<span id="keys" class="sr-only">Arrow keys pan, plus and minus zoom, Escape clears the selection.</span>
|
|
2272
|
+
<span id="ground" aria-hidden="true" @transitionrun=${this.onGroundChange}></span>
|
|
2030
2273
|
${overlay}
|
|
2031
2274
|
${this.regionTag()}
|
|
2032
2275
|
${corners.filter((c) => c !== this.controlsCorner).map(corner)}
|
|
2033
2276
|
${this.hover
|
|
2034
|
-
? html `<div part="tooltip"
|
|
2277
|
+
? html `<div part="tooltip" role="tooltip">
|
|
2035
2278
|
<slot name="tooltip"><div class="t">${this.hover.title}</div>${this.hover.lines.length > 0 ? html `<div class="s">${this.hover.lines.join(' · ')}</div>` : nothing}</slot>
|
|
2036
2279
|
</div>`
|
|
2037
2280
|
: nothing}`;
|
|
@@ -2043,6 +2286,33 @@ export { MosaicaMap };
|
|
|
2043
2286
|
function countText(n) {
|
|
2044
2287
|
return n.toLocaleString('en-GB', { maximumFractionDigits: 0 });
|
|
2045
2288
|
}
|
|
2289
|
+
/** The overlay's line while the map has no state to show. */
|
|
2290
|
+
const NO_LINE = { dot: null, words: nothing, action: nothing };
|
|
2291
|
+
/** How far the tooltip sits from the pointer along each axis, in pixels. */
|
|
2292
|
+
const TOOLTIP_GAP_PX = 14;
|
|
2293
|
+
/** The gap between the region's tag and its region, in pixels. */
|
|
2294
|
+
const REGION_TAG_GAP_PX = 4;
|
|
2295
|
+
/**
|
|
2296
|
+
* A box's top-left corner, moved as little as keeps the box between `min` and `max` on each axis.
|
|
2297
|
+
* A box larger than that room starts at `min`.
|
|
2298
|
+
*
|
|
2299
|
+
* @internal
|
|
2300
|
+
*/
|
|
2301
|
+
export function keptInside(at, size, min, max) {
|
|
2302
|
+
const axis = (i) => Math.max(min[i], Math.min(at[i], max[i] - size[i]));
|
|
2303
|
+
return [axis(0), axis(1)];
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* The tooltip's top-left corner for a pointer at `pointer` on a map of size `map`: below and right
|
|
2307
|
+
* of the pointer, on the pointer's other side along an axis where it would cross the map's far
|
|
2308
|
+
* edge, and inside the map either way.
|
|
2309
|
+
*
|
|
2310
|
+
* @internal
|
|
2311
|
+
*/
|
|
2312
|
+
export function tooltipCorner(pointer, size, map) {
|
|
2313
|
+
const axis = (i) => (pointer[i] + TOOLTIP_GAP_PX + size[i] > map[i] ? pointer[i] - TOOLTIP_GAP_PX - size[i] : pointer[i] + TOOLTIP_GAP_PX);
|
|
2314
|
+
return keptInside([axis(0), axis(1)], size, [0, 0], map);
|
|
2315
|
+
}
|
|
2046
2316
|
/** The density scale drawn: `linear` where set, else `log`. */
|
|
2047
2317
|
export function drawnDensityScale(scale) {
|
|
2048
2318
|
return scale === 'linear' ? 'linear' : 'log';
|