@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/explorer.js
CHANGED
|
@@ -37,9 +37,9 @@ import { css, html, nothing, svg } from 'lit';
|
|
|
37
37
|
import { property, state } from 'lit/decorators.js';
|
|
38
38
|
import { repeat } from 'lit/directives/repeat.js';
|
|
39
39
|
import { CATEGORY_PALETTES, DEFAULT_DENSITY_CELL_PX, DEFAULT_DENSITY_SCALE, DENSITY_CELL_SIZES, RAMPS, cellDepth, nearestStop } from '@mosaicajs/deck';
|
|
40
|
-
import { CLUSTER_PREFIX, PALETTES, WORLD_SIZE, activeCount, artifactName, colourLayers, emptyDraft } from '@mosaicajs/client';
|
|
41
|
-
import { hasOneLayout,
|
|
42
|
-
import { DENSITY_COLOUR_TITLES, colourOfFraction, css as rgb, hexOf } from '@mosaicajs/deck/internal';
|
|
40
|
+
import { CLUSTER_PREFIX, PALETTES, WORLD_SIZE, activeCount, artifactName, clustersInView, colourLayers, emptyDraft } from '@mosaicajs/client';
|
|
41
|
+
import { follows, hasOneLayout, levelFollowsZoom, levelOfLayer, sizesPoints } from '@mosaicajs/client/internal';
|
|
42
|
+
import { DENSITY_COLOUR_TITLES, clusterLayerOf, colourOfFraction, css as rgb, hexOf } from '@mosaicajs/deck/internal';
|
|
43
43
|
import { MosaicaElement, columnCaption, emit, shortCount } from './base.js';
|
|
44
44
|
import { placeCallout } from './callout.js';
|
|
45
45
|
import { colouringOf, setColouring, sizingOf, watchChoices } from './colouring.js';
|
|
@@ -110,6 +110,8 @@ const px = (n) => n.toLocaleString('en-GB', { maximumFractionDigits: 1 });
|
|
|
110
110
|
const fill = (v, min, max) => `--fill:${(Math.min(1, Math.max(0, (v - min) / (max - min))) * 100).toFixed(1)}%`;
|
|
111
111
|
/** A ramp as a CSS gradient, low to high. */
|
|
112
112
|
const rampGradient = (ramp, reverse) => `linear-gradient(to right, ${Array.from({ length: 8 }, (_, k) => rgb(colourOfFraction(k / 7, ramp, reverse))).join(', ')})`;
|
|
113
|
+
/** The tick on the entry chosen in a list of choices. */
|
|
114
|
+
const tick = (chosen) => (chosen ? html `<span class="tick">${icon('check', 11, 1.6)}</span>` : nothing);
|
|
113
115
|
/** The Density choices in the Layers popover, in order. */
|
|
114
116
|
const DENSITY_MODES = [
|
|
115
117
|
{ mode: 'none', label: 'None' },
|
|
@@ -124,7 +126,16 @@ const CALLOUT_SIZE = { width: 300, height: 220 };
|
|
|
124
126
|
const CALLOUT_REST_MS = 200;
|
|
125
127
|
/** The shortest a callout is made to fit beside its point; what does not fit scrolls inside it. */
|
|
126
128
|
const CALLOUT_MIN_HEIGHT = 180;
|
|
127
|
-
/** The
|
|
129
|
+
/** The controls Tab stops at. */
|
|
130
|
+
const FOCUSABLE = 'button:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
131
|
+
/** Whether `node` is `container` or inside it, through slots and shadow roots. */
|
|
132
|
+
function within(node, container) {
|
|
133
|
+
for (let at = node; at; at = (at instanceof Element ? at.assignedSlot : null) ?? at.parentNode ?? (at instanceof ShadowRoot ? at.host : null))
|
|
134
|
+
if (at === container)
|
|
135
|
+
return true;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
/** The narrow layout's buttons, each opening a sheet, drawn where its panel is. */
|
|
128
139
|
const TABS = [
|
|
129
140
|
{ sheet: 'filters', icon: 'filter', label: 'Fields', panel: 'filters' },
|
|
130
141
|
{ sheet: 'layers', icon: 'layers', label: 'Layers', panel: 'legend' },
|
|
@@ -153,7 +164,20 @@ const TABS = [
|
|
|
153
164
|
* HTML.
|
|
154
165
|
*
|
|
155
166
|
* Below 1000 px the explorer uses the overlay layout whatever `layout` says. At 720 px or less it
|
|
156
|
-
* opens its panels as sheets from a
|
|
167
|
+
* opens its panels as sheets from a bar of buttons under the map, and a picked item's card opens
|
|
168
|
+
* in the Item sheet. A sheet that reaches up over the map's tools makes the map behind it inert
|
|
169
|
+
* until it closes.
|
|
170
|
+
*
|
|
171
|
+
* The status strip speaks for the explorer: its map and its selection card are set `quiet`, so a
|
|
172
|
+
* change is announced once. Where you put something other than a `<mosaica-status>` in the
|
|
173
|
+
* `status` slot, they announce for themselves.
|
|
174
|
+
*
|
|
175
|
+
* An `aria-label` on the explorer names its map, so a page with several explorers can tell them
|
|
176
|
+
* apart.
|
|
177
|
+
*
|
|
178
|
+
* A dataset title, a view's name or a layer's name too long for its line is cut with an ellipsis
|
|
179
|
+
* and keeps all of it in its `title`. Set `--mosaica-long-text: normal` to wrap them. The narrow
|
|
180
|
+
* layout wraps them itself.
|
|
157
181
|
*
|
|
158
182
|
* @summary A complete map application in a single element.
|
|
159
183
|
* @tagname mosaica-explorer
|
|
@@ -163,7 +187,7 @@ const TABS = [
|
|
|
163
187
|
* @slot filters - Replaces the filter panel.
|
|
164
188
|
* @slot selection - Replaces the selection panel, shown while a region is selected.
|
|
165
189
|
* @slot detail - Replaces the item and artifact cards, in the card beside the point.
|
|
166
|
-
* @slot status - Replaces the status strip
|
|
190
|
+
* @slot status - Replaces the status strip: on the map, or under it in the narrow layout.
|
|
167
191
|
* @slot tooltip - Replaces the map's hover tooltip.
|
|
168
192
|
* @slot top-right - Content in the map's top-right corner, above the selection's card.
|
|
169
193
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-viewchange']>} mosaica-viewchange - The map's camera moved.
|
|
@@ -176,11 +200,11 @@ const TABS = [
|
|
|
176
200
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-selectchange']>} mosaica-selectchange - A selection was drawn, changed or cleared, or its counts arrived.
|
|
177
201
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-layerchange']>} mosaica-layerchange - The layers chosen changed.
|
|
178
202
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-colourchange']>} mosaica-colourchange - Colour by changed, in the Layers popover or by a card's paint button.
|
|
179
|
-
* @fires {CustomEvent<MosaicaEventDetails['mosaica-levelchange']>} mosaica-levelchange - The level
|
|
203
|
+
* @fires {CustomEvent<MosaicaEventDetails['mosaica-levelchange']>} mosaica-levelchange - The level one layer is counted, coloured and labelled at changed.
|
|
180
204
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-displaychange']>} mosaica-displaychange - A setting in the Points or Density section changed.
|
|
181
205
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-sizechange']>} mosaica-sizechange - Size by, the size range or the scale changed in the Points section.
|
|
182
206
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-budgetchange']>} mosaica-budgetchange - Most points was let go at a new number.
|
|
183
|
-
* @fires {CustomEvent<MosaicaEventDetails['mosaica-
|
|
207
|
+
* @fires {CustomEvent<MosaicaEventDetails['mosaica-clusterspacingchange']>} mosaica-clusterspacingchange - Cluster density was let go at a new spacing.
|
|
184
208
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-fold']>} mosaica-fold - A field card was folded to one line or opened.
|
|
185
209
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-valuecolour']>} mosaica-valuecolour - Colours were chosen or reset for values of a category, on a field card or in Edit colours.
|
|
186
210
|
* @fires {CustomEvent<MosaicaEventDetails['mosaica-clustercolour']>} mosaica-clustercolour - Colours were chosen or reset for clusters of a layer, on a field card or in Edit colours.
|
|
@@ -217,8 +241,9 @@ const TABS = [
|
|
|
217
241
|
* @csspart points-toggle - The Points switch, with `aria-checked`.
|
|
218
242
|
* @csspart most-points - The Most points slider.
|
|
219
243
|
* @csspart most-points-value - The number of points it stands at, such as "250K".
|
|
220
|
-
* @csspart
|
|
221
|
-
*
|
|
244
|
+
* @csspart cluster-density - The Cluster density slider, while a `nested` or `dag` layer is drawn, or a
|
|
245
|
+
* levelled layer whose level follows the zoom is drawn or coloured by.
|
|
246
|
+
* @csspart cluster-density-value - How many clusters it gives the map at its size, such as "96".
|
|
222
247
|
* @csspart size-by - The Size by button, naming the column the points are sized by or None, with
|
|
223
248
|
* `aria-expanded` while its menu is open.
|
|
224
249
|
* @csspart size-menu - The Size by menu, while it is open.
|
|
@@ -235,7 +260,8 @@ const TABS = [
|
|
|
235
260
|
* `aria-expanded` while its menu is open.
|
|
236
261
|
* @csspart colour-menu - The Colour by menu, while it is open.
|
|
237
262
|
* @csspart colour-option - An entry in the Colour by menu, with `data-value` (empty for None,
|
|
238
|
-
* `cluster:<layer>@<level>` for one level of a levelled layer
|
|
263
|
+
* `cluster:<layer>@<level>` for one level of a levelled layer, `cluster:<layer>@` for Follow zoom
|
|
264
|
+
* on one whose levels declare no zoom range) and `aria-checked`.
|
|
239
265
|
* @csspart palette - The Palette button, while the points are coloured by a layer or a category.
|
|
240
266
|
* @csspart palette-menu - The Palette menu, while it is open.
|
|
241
267
|
* @csspart palette-option - A palette in that menu, with `data-value` and `aria-checked`.
|
|
@@ -259,9 +285,10 @@ const TABS = [
|
|
|
259
285
|
* @csspart detail - The item or artifact card in the map's top-right corner, used when the picked
|
|
260
286
|
* item or artifact has no position on the map, such as after following an item into another
|
|
261
287
|
* view.
|
|
262
|
-
* @csspart sheet - The open sheet, in the narrow layout.
|
|
288
|
+
* @csspart sheet - The open sheet, over the map in the narrow layout.
|
|
263
289
|
* @csspart strip-row - The full-width status strip, in the narrow layout.
|
|
264
|
-
* @csspart tabs - The
|
|
290
|
+
* @csspart tabs - The bar of buttons that open the sheets, in the narrow layout, each with
|
|
291
|
+
* `data-sheet` and `aria-expanded`.
|
|
265
292
|
* @csspart map-<part> - A part of the inner `<mosaica-map>`, such as `map-controls`.
|
|
266
293
|
* @csspart status-<part> - A part of an inner `<mosaica-status>`.
|
|
267
294
|
* @csspart view-picker-<part> - A part of the inner `<mosaica-view-picker>`.
|
|
@@ -323,15 +350,15 @@ let MosaicaExplorer = (() => {
|
|
|
323
350
|
let _budgetMax_decorators;
|
|
324
351
|
let _budgetMax_initializers = [];
|
|
325
352
|
let _budgetMax_extraInitializers = [];
|
|
326
|
-
let
|
|
327
|
-
let
|
|
328
|
-
let
|
|
329
|
-
let
|
|
330
|
-
let
|
|
331
|
-
let
|
|
332
|
-
let
|
|
333
|
-
let
|
|
334
|
-
let
|
|
353
|
+
let _clusterSpacing_decorators;
|
|
354
|
+
let _clusterSpacing_initializers = [];
|
|
355
|
+
let _clusterSpacing_extraInitializers = [];
|
|
356
|
+
let _clusterSpacingMin_decorators;
|
|
357
|
+
let _clusterSpacingMin_initializers = [];
|
|
358
|
+
let _clusterSpacingMin_extraInitializers = [];
|
|
359
|
+
let _clusterSpacingMax_decorators;
|
|
360
|
+
let _clusterSpacingMax_initializers = [];
|
|
361
|
+
let _clusterSpacingMax_extraInitializers = [];
|
|
335
362
|
let _basemap_decorators;
|
|
336
363
|
let _basemap_initializers = [];
|
|
337
364
|
let _basemap_extraInitializers = [];
|
|
@@ -422,9 +449,9 @@ let MosaicaExplorer = (() => {
|
|
|
422
449
|
let _tabFocus_decorators;
|
|
423
450
|
let _tabFocus_initializers = [];
|
|
424
451
|
let _tabFocus_extraInitializers = [];
|
|
425
|
-
let
|
|
426
|
-
let
|
|
427
|
-
let
|
|
452
|
+
let _levels_decorators;
|
|
453
|
+
let _levels_initializers = [];
|
|
454
|
+
let _levels_extraInitializers = [];
|
|
428
455
|
let _pinned_decorators;
|
|
429
456
|
let _pinned_initializers = [];
|
|
430
457
|
let _pinned_extraInitializers = [];
|
|
@@ -457,9 +484,9 @@ let MosaicaExplorer = (() => {
|
|
|
457
484
|
_budget_decorators = [property({ type: Number })];
|
|
458
485
|
_budgetMin_decorators = [property({ type: Number, attribute: 'budget-min' })];
|
|
459
486
|
_budgetMax_decorators = [property({ type: Number, attribute: 'budget-max' })];
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
487
|
+
_clusterSpacing_decorators = [property({ type: Number, attribute: 'cluster-spacing' })];
|
|
488
|
+
_clusterSpacingMin_decorators = [property({ type: Number, attribute: 'cluster-spacing-min' })];
|
|
489
|
+
_clusterSpacingMax_decorators = [property({ type: Number, attribute: 'cluster-spacing-max' })];
|
|
463
490
|
_basemap_decorators = [property({ hasChanged: basemapChanged })];
|
|
464
491
|
_noPoints_decorators = [property({ type: Boolean, attribute: 'no-points' })];
|
|
465
492
|
_radius_decorators = [property({ type: Number })];
|
|
@@ -490,7 +517,7 @@ let MosaicaExplorer = (() => {
|
|
|
490
517
|
_densityColoursOpen_decorators = [state()];
|
|
491
518
|
_sheet_decorators = [state()];
|
|
492
519
|
_tabFocus_decorators = [state()];
|
|
493
|
-
|
|
520
|
+
_levels_decorators = [state()];
|
|
494
521
|
_pinned_decorators = [state()];
|
|
495
522
|
_layersOpen_decorators = [state()];
|
|
496
523
|
_compact_decorators = [state()];
|
|
@@ -510,9 +537,9 @@ let MosaicaExplorer = (() => {
|
|
|
510
537
|
__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);
|
|
511
538
|
__esDecorate(this, null, _budgetMin_decorators, { kind: "accessor", name: "budgetMin", static: false, private: false, access: { has: obj => "budgetMin" in obj, get: obj => obj.budgetMin, set: (obj, value) => { obj.budgetMin = value; } }, metadata: _metadata }, _budgetMin_initializers, _budgetMin_extraInitializers);
|
|
512
539
|
__esDecorate(this, null, _budgetMax_decorators, { kind: "accessor", name: "budgetMax", static: false, private: false, access: { has: obj => "budgetMax" in obj, get: obj => obj.budgetMax, set: (obj, value) => { obj.budgetMax = value; } }, metadata: _metadata }, _budgetMax_initializers, _budgetMax_extraInitializers);
|
|
513
|
-
__esDecorate(this, null,
|
|
514
|
-
__esDecorate(this, null,
|
|
515
|
-
__esDecorate(this, null,
|
|
540
|
+
__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);
|
|
541
|
+
__esDecorate(this, null, _clusterSpacingMin_decorators, { kind: "accessor", name: "clusterSpacingMin", static: false, private: false, access: { has: obj => "clusterSpacingMin" in obj, get: obj => obj.clusterSpacingMin, set: (obj, value) => { obj.clusterSpacingMin = value; } }, metadata: _metadata }, _clusterSpacingMin_initializers, _clusterSpacingMin_extraInitializers);
|
|
542
|
+
__esDecorate(this, null, _clusterSpacingMax_decorators, { kind: "accessor", name: "clusterSpacingMax", static: false, private: false, access: { has: obj => "clusterSpacingMax" in obj, get: obj => obj.clusterSpacingMax, set: (obj, value) => { obj.clusterSpacingMax = value; } }, metadata: _metadata }, _clusterSpacingMax_initializers, _clusterSpacingMax_extraInitializers);
|
|
516
543
|
__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);
|
|
517
544
|
__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);
|
|
518
545
|
__esDecorate(this, null, _radius_decorators, { kind: "accessor", name: "radius", static: false, private: false, access: { has: obj => "radius" in obj, get: obj => obj.radius, set: (obj, value) => { obj.radius = value; } }, metadata: _metadata }, _radius_initializers, _radius_extraInitializers);
|
|
@@ -543,7 +570,7 @@ let MosaicaExplorer = (() => {
|
|
|
543
570
|
__esDecorate(this, null, _densityColoursOpen_decorators, { kind: "accessor", name: "densityColoursOpen", static: false, private: false, access: { has: obj => "densityColoursOpen" in obj, get: obj => obj.densityColoursOpen, set: (obj, value) => { obj.densityColoursOpen = value; } }, metadata: _metadata }, _densityColoursOpen_initializers, _densityColoursOpen_extraInitializers);
|
|
544
571
|
__esDecorate(this, null, _sheet_decorators, { kind: "accessor", name: "sheet", static: false, private: false, access: { has: obj => "sheet" in obj, get: obj => obj.sheet, set: (obj, value) => { obj.sheet = value; } }, metadata: _metadata }, _sheet_initializers, _sheet_extraInitializers);
|
|
545
572
|
__esDecorate(this, null, _tabFocus_decorators, { kind: "accessor", name: "tabFocus", static: false, private: false, access: { has: obj => "tabFocus" in obj, get: obj => obj.tabFocus, set: (obj, value) => { obj.tabFocus = value; } }, metadata: _metadata }, _tabFocus_initializers, _tabFocus_extraInitializers);
|
|
546
|
-
__esDecorate(this, null,
|
|
573
|
+
__esDecorate(this, null, _levels_decorators, { kind: "accessor", name: "levels", static: false, private: false, access: { has: obj => "levels" in obj, get: obj => obj.levels, set: (obj, value) => { obj.levels = value; } }, metadata: _metadata }, _levels_initializers, _levels_extraInitializers);
|
|
547
574
|
__esDecorate(this, null, _pinned_decorators, { kind: "accessor", name: "pinned", static: false, private: false, access: { has: obj => "pinned" in obj, get: obj => obj.pinned, set: (obj, value) => { obj.pinned = value; } }, metadata: _metadata }, _pinned_initializers, _pinned_extraInitializers);
|
|
548
575
|
__esDecorate(this, null, _layersOpen_decorators, { kind: "accessor", name: "layersOpen", static: false, private: false, access: { has: obj => "layersOpen" in obj, get: obj => obj.layersOpen, set: (obj, value) => { obj.layersOpen = value; } }, metadata: _metadata }, _layersOpen_initializers, _layersOpen_extraInitializers);
|
|
549
576
|
__esDecorate(this, null, _compact_decorators, { kind: "accessor", name: "compact", static: false, private: false, access: { has: obj => "compact" in obj, get: obj => obj.compact, set: (obj, value) => { obj.compact = value; } }, metadata: _metadata }, _compact_initializers, _compact_extraInitializers);
|
|
@@ -598,6 +625,13 @@ let MosaicaExplorer = (() => {
|
|
|
598
625
|
height: 100%;
|
|
599
626
|
min-height: 320px;
|
|
600
627
|
}
|
|
628
|
+
/* The narrow layout's map takes the height its bars leave. */
|
|
629
|
+
@container explorer (max-width: 720px) {
|
|
630
|
+
mosaica-map,
|
|
631
|
+
::slotted(mosaica-map) {
|
|
632
|
+
min-height: 0;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
601
635
|
/* What the map draws at its top-left, its tools or a region's tag, sits right of the card; in
|
|
602
636
|
the narrow layout there is no card. */
|
|
603
637
|
@container explorer (width > 720px) {
|
|
@@ -694,11 +728,17 @@ let MosaicaExplorer = (() => {
|
|
|
694
728
|
min-width: 0;
|
|
695
729
|
}
|
|
696
730
|
.title {
|
|
731
|
+
display: block;
|
|
732
|
+
margin: 0;
|
|
697
733
|
font-size: 15px;
|
|
698
734
|
font-weight: 600;
|
|
735
|
+
letter-spacing: 0;
|
|
736
|
+
text-transform: none;
|
|
737
|
+
color: var(--_mosaica-ink);
|
|
699
738
|
overflow: hidden;
|
|
700
739
|
text-overflow: ellipsis;
|
|
701
|
-
white-space:
|
|
740
|
+
white-space: var(--_mosaica-long-text);
|
|
741
|
+
overflow-wrap: anywhere;
|
|
702
742
|
}
|
|
703
743
|
.compact .title {
|
|
704
744
|
font-size: 14px;
|
|
@@ -777,6 +817,10 @@ let MosaicaExplorer = (() => {
|
|
|
777
817
|
linear-gradient(transparent, color-mix(in srgb, var(--_mosaica-ink) 16%, transparent)) bottom / 100% 10px no-repeat scroll,
|
|
778
818
|
var(--_mosaica-surface);
|
|
779
819
|
}
|
|
820
|
+
[part='layers-popover']:focus-visible,
|
|
821
|
+
[part='sheet']:focus-visible {
|
|
822
|
+
outline-offset: -2px;
|
|
823
|
+
}
|
|
780
824
|
[part='layers-popover'] mosaica-layer-picker,
|
|
781
825
|
[part='sheet'] mosaica-layer-picker {
|
|
782
826
|
display: block;
|
|
@@ -857,6 +901,7 @@ let MosaicaExplorer = (() => {
|
|
|
857
901
|
}
|
|
858
902
|
[part='pin'][aria-pressed='true'] {
|
|
859
903
|
background: var(--_mosaica-surface-3);
|
|
904
|
+
box-shadow: inset 0 -2px 0 var(--_mosaica-accent);
|
|
860
905
|
color: var(--_mosaica-ink);
|
|
861
906
|
}
|
|
862
907
|
/* The selection keeps its heading, counts and actions; only its list of marks scrolls, so the
|
|
@@ -903,18 +948,22 @@ let MosaicaExplorer = (() => {
|
|
|
903
948
|
padding: 10px 16px;
|
|
904
949
|
}
|
|
905
950
|
@container explorer (max-width: 720px) {
|
|
951
|
+
/* The map keeps a floor under its height; a frame too short for it and the bars scrolls.
|
|
952
|
+
A long title or name wraps here, in the explorer and in the elements it holds. */
|
|
906
953
|
[part='frame'],
|
|
907
954
|
[part='frame'].docked {
|
|
955
|
+
--mosaica-long-text: normal;
|
|
956
|
+
--_mosaica-long-text: normal;
|
|
908
957
|
grid-template-columns: minmax(0, 1fr);
|
|
909
|
-
grid-template-rows: minmax(
|
|
958
|
+
grid-template-rows: minmax(120px, 1fr) auto auto;
|
|
959
|
+
overflow: hidden auto;
|
|
910
960
|
}
|
|
911
961
|
[part='frame'].titled {
|
|
912
|
-
grid-template-rows: auto minmax(
|
|
962
|
+
grid-template-rows: auto minmax(120px, 1fr) auto auto;
|
|
913
963
|
}
|
|
914
964
|
[part='sidebar'],
|
|
915
965
|
[part='panel'],
|
|
916
966
|
.layers,
|
|
917
|
-
.right,
|
|
918
967
|
.callouts,
|
|
919
968
|
.in-map-strip {
|
|
920
969
|
display: none;
|
|
@@ -938,30 +987,37 @@ let MosaicaExplorer = (() => {
|
|
|
938
987
|
font-size: 11px;
|
|
939
988
|
font-weight: 500;
|
|
940
989
|
}
|
|
941
|
-
[part='tabs'] button[aria-
|
|
990
|
+
[part='tabs'] button[aria-expanded='true'] {
|
|
991
|
+
box-shadow: inset 0 2px 0 var(--_mosaica-accent);
|
|
942
992
|
color: var(--_mosaica-ink);
|
|
943
993
|
font-weight: 600;
|
|
944
994
|
}
|
|
995
|
+
/* The sheet opens over the map alone, so the strip and the bar under it stay in reach. A
|
|
996
|
+
short map gives the sheet all of its height. */
|
|
945
997
|
[part='sheet'] {
|
|
946
998
|
position: absolute;
|
|
947
999
|
left: 0;
|
|
948
1000
|
right: 0;
|
|
949
|
-
bottom:
|
|
950
|
-
max-height:
|
|
951
|
-
|
|
1001
|
+
bottom: 0;
|
|
1002
|
+
max-height: max(80%, min(100%, 240px));
|
|
1003
|
+
display: flex;
|
|
1004
|
+
flex-direction: column;
|
|
1005
|
+
overflow: hidden;
|
|
952
1006
|
background: var(--_mosaica-surface);
|
|
953
1007
|
border-top: 1px solid var(--_mosaica-line);
|
|
954
1008
|
border-radius: 12px 12px 0 0;
|
|
955
1009
|
box-shadow: var(--_mosaica-shadow);
|
|
956
1010
|
z-index: 6;
|
|
957
1011
|
}
|
|
958
|
-
/*
|
|
959
|
-
|
|
960
|
-
|
|
1012
|
+
/* What the sheet holds scrolls between its handle and its footer, so nothing goes under the footer. */
|
|
1013
|
+
.sheet-body {
|
|
1014
|
+
flex: 0 1 auto;
|
|
1015
|
+
min-height: 0;
|
|
1016
|
+
overflow-y: auto;
|
|
1017
|
+
overscroll-behavior: contain;
|
|
961
1018
|
}
|
|
962
1019
|
.sheet-footer {
|
|
963
|
-
|
|
964
|
-
bottom: 0;
|
|
1020
|
+
flex: none;
|
|
965
1021
|
display: flex;
|
|
966
1022
|
gap: 10px;
|
|
967
1023
|
padding: 10px 16px;
|
|
@@ -978,7 +1034,7 @@ let MosaicaExplorer = (() => {
|
|
|
978
1034
|
}
|
|
979
1035
|
[part='sheet']::before {
|
|
980
1036
|
content: '';
|
|
981
|
-
|
|
1037
|
+
flex: none;
|
|
982
1038
|
width: 36px;
|
|
983
1039
|
height: 4px;
|
|
984
1040
|
border-radius: 2px;
|
|
@@ -997,8 +1053,18 @@ let MosaicaExplorer = (() => {
|
|
|
997
1053
|
box-shadow: none;
|
|
998
1054
|
border-radius: 0;
|
|
999
1055
|
border-width: 1px 0 0;
|
|
1000
|
-
height: 44px;
|
|
1001
|
-
|
|
1056
|
+
min-height: 44px;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
@media (forced-colors: active) {
|
|
1060
|
+
[part='layers-toggle'] .on {
|
|
1061
|
+
forced-color-adjust: none;
|
|
1062
|
+
background: Highlight;
|
|
1063
|
+
}
|
|
1064
|
+
[part='tabs'] button[aria-expanded='true'] {
|
|
1065
|
+
forced-color-adjust: none;
|
|
1066
|
+
background: Highlight;
|
|
1067
|
+
color: HighlightText;
|
|
1002
1068
|
}
|
|
1003
1069
|
}
|
|
1004
1070
|
`
|
|
@@ -1070,22 +1136,24 @@ let MosaicaExplorer = (() => {
|
|
|
1070
1136
|
/** The highest value of the Most points slider. */
|
|
1071
1137
|
get budgetMax() { return this.#budgetMax_accessor_storage; }
|
|
1072
1138
|
set budgetMax(value) { this.#budgetMax_accessor_storage = value; }
|
|
1073
|
-
#
|
|
1139
|
+
#clusterSpacing_accessor_storage = (__runInitializers(this, _budgetMax_extraInitializers), __runInitializers(this, _clusterSpacing_initializers, 100));
|
|
1074
1140
|
/**
|
|
1075
|
-
* The
|
|
1076
|
-
*
|
|
1141
|
+
* The side in CSS pixels of the square of the map each cluster is given, as for `<mosaica-map>`'s
|
|
1142
|
+
* `cluster-spacing`: at 100, a map 1,200 by 800 pixels cuts a `nested` or `dag` layer to 96
|
|
1143
|
+
* clusters, and a level that follows the zoom may have 96 in view. `0` removes the limit. Most
|
|
1144
|
+
* clusters in the Layers popover changes it.
|
|
1077
1145
|
*/
|
|
1078
|
-
get
|
|
1079
|
-
set
|
|
1080
|
-
#
|
|
1081
|
-
/** The
|
|
1082
|
-
get
|
|
1083
|
-
set
|
|
1084
|
-
#
|
|
1085
|
-
/** The
|
|
1086
|
-
get
|
|
1087
|
-
set
|
|
1088
|
-
#basemap_accessor_storage = (__runInitializers(this,
|
|
1146
|
+
get clusterSpacing() { return this.#clusterSpacing_accessor_storage; }
|
|
1147
|
+
set clusterSpacing(value) { this.#clusterSpacing_accessor_storage = value; }
|
|
1148
|
+
#clusterSpacingMin_accessor_storage = (__runInitializers(this, _clusterSpacing_extraInitializers), __runInitializers(this, _clusterSpacingMin_initializers, 50));
|
|
1149
|
+
/** The smallest spacing Cluster density offers, at its More end. */
|
|
1150
|
+
get clusterSpacingMin() { return this.#clusterSpacingMin_accessor_storage; }
|
|
1151
|
+
set clusterSpacingMin(value) { this.#clusterSpacingMin_accessor_storage = value; }
|
|
1152
|
+
#clusterSpacingMax_accessor_storage = (__runInitializers(this, _clusterSpacingMin_extraInitializers), __runInitializers(this, _clusterSpacingMax_initializers, 400));
|
|
1153
|
+
/** The largest spacing Cluster density offers, at its Fewer end. */
|
|
1154
|
+
get clusterSpacingMax() { return this.#clusterSpacingMax_accessor_storage; }
|
|
1155
|
+
set clusterSpacingMax(value) { this.#clusterSpacingMax_accessor_storage = value; }
|
|
1156
|
+
#basemap_accessor_storage = (__runInitializers(this, _clusterSpacingMax_extraInitializers), __runInitializers(this, _basemap_initializers, null));
|
|
1089
1157
|
/**
|
|
1090
1158
|
* What is drawn under the points, as for `<mosaica-map>`'s `basemap`. The attribute takes `world`
|
|
1091
1159
|
* and `none`. Unset, a view with a map projection gets the world outline and any other view gets
|
|
@@ -1200,7 +1268,7 @@ let MosaicaExplorer = (() => {
|
|
|
1200
1268
|
get pointsDragged() { return this.#pointsDragged_accessor_storage; }
|
|
1201
1269
|
set pointsDragged(value) { this.#pointsDragged_accessor_storage = value; }
|
|
1202
1270
|
#clustersDragged_accessor_storage = (__runInitializers(this, _pointsDragged_extraInitializers), __runInitializers(this, _clustersDragged_initializers, null));
|
|
1203
|
-
/**
|
|
1271
|
+
/** The cluster spacing Cluster density is being dragged to, before it is let go. @internal */
|
|
1204
1272
|
get clustersDragged() { return this.#clustersDragged_accessor_storage; }
|
|
1205
1273
|
set clustersDragged(value) { this.#clustersDragged_accessor_storage = value; }
|
|
1206
1274
|
#densityColoursOpen_accessor_storage = (__runInitializers(this, _clustersDragged_extraInitializers), __runInitializers(this, _densityColoursOpen_initializers, false));
|
|
@@ -1212,14 +1280,33 @@ let MosaicaExplorer = (() => {
|
|
|
1212
1280
|
get sheet() { return this.#sheet_accessor_storage; }
|
|
1213
1281
|
set sheet(value) { this.#sheet_accessor_storage = value; }
|
|
1214
1282
|
#tabFocus_accessor_storage = (__runInitializers(this, _sheet_extraInitializers), __runInitializers(this, _tabFocus_initializers, null));
|
|
1215
|
-
/** The narrow layout's
|
|
1283
|
+
/** The narrow layout's button focused last, which keeps the bar's one place in the tab order. */
|
|
1216
1284
|
get tabFocus() { return this.#tabFocus_accessor_storage; }
|
|
1217
1285
|
set tabFocus(value) { this.#tabFocus_accessor_storage = value; }
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1286
|
+
/**
|
|
1287
|
+
* What the selection's card shows, as last seen: the record or the refusal the store holds, or
|
|
1288
|
+
* the map's broken pick; null for no card. Picking the same item again brings a new record.
|
|
1289
|
+
*/
|
|
1290
|
+
cardSeen = (__runInitializers(this, _tabFocus_extraInitializers), null);
|
|
1291
|
+
/** Where focus was before the update being drawn: in the open sheet, elsewhere in the explorer, or outside it. */
|
|
1292
|
+
focusBefore = null;
|
|
1293
|
+
/** Whether the host put something other than a status strip in the `status` slot, so no strip speaks. */
|
|
1294
|
+
get stripReplaced() {
|
|
1295
|
+
const given = Array.from(this.children).filter((el) => el.slot === 'status');
|
|
1296
|
+
return given.length > 0 && !given.some((el) => el.localName === 'mosaica-status' || el.querySelector('mosaica-status') !== null);
|
|
1297
|
+
}
|
|
1298
|
+
/** What the host put in the `status` slot changed, and with it whether a strip speaks. */
|
|
1299
|
+
onStatusSlot = () => this.requestUpdate();
|
|
1300
|
+
/** What the status region says: the card that opened last. */
|
|
1301
|
+
announced = '';
|
|
1302
|
+
#levels_accessor_storage = __runInitializers(this, _levels_initializers, new Map());
|
|
1303
|
+
/**
|
|
1304
|
+
* The level each layer is counted, coloured and labelled at, chosen on its card or in Colour by,
|
|
1305
|
+
* keyed by layer name. A layer with no entry takes its default level. @internal
|
|
1306
|
+
*/
|
|
1307
|
+
get levels() { return this.#levels_accessor_storage; }
|
|
1308
|
+
set levels(value) { this.#levels_accessor_storage = value; }
|
|
1309
|
+
#pinned_accessor_storage = (__runInitializers(this, _levels_extraInitializers), __runInitializers(this, _pinned_initializers, []));
|
|
1223
1310
|
/** The cards kept open by Pin, oldest first. @internal */
|
|
1224
1311
|
get pinned() { return this.#pinned_accessor_storage; }
|
|
1225
1312
|
set pinned(value) { this.#pinned_accessor_storage = value; }
|
|
@@ -1294,6 +1381,61 @@ let MosaicaExplorer = (() => {
|
|
|
1294
1381
|
}
|
|
1295
1382
|
super.onStoreChange();
|
|
1296
1383
|
}
|
|
1384
|
+
static get observedAttributes() {
|
|
1385
|
+
return [...super.observedAttributes, 'aria-label'];
|
|
1386
|
+
}
|
|
1387
|
+
/** An `aria-label` on the explorer is passed to its map. */
|
|
1388
|
+
attributeChangedCallback(name, old, value) {
|
|
1389
|
+
super.attributeChangedCallback(name, old, value);
|
|
1390
|
+
if (name === 'aria-label')
|
|
1391
|
+
this.requestUpdate();
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* As the selection's card comes, changes or goes. The wide layouts say which card opened in the
|
|
1395
|
+
* status region. The narrow layout opens the Item sheet for it, and closes that sheet with it; a
|
|
1396
|
+
* card that arrives while the viewer's focus is in another sheet leaves that sheet open. Sheets
|
|
1397
|
+
* are the narrow layout's: growing wider closes the open one, and growing narrow with a card
|
|
1398
|
+
* open opens the Item sheet.
|
|
1399
|
+
*/
|
|
1400
|
+
willUpdate(changed) {
|
|
1401
|
+
super.willUpdate(changed);
|
|
1402
|
+
const sheet = this.renderRoot?.querySelector('[part="sheet"]') ?? null;
|
|
1403
|
+
this.focusBefore = sheet && within(this.shadowRoot?.activeElement ?? null, sheet) ? 'sheet' : this.holdsFocus() ? 'explorer' : null;
|
|
1404
|
+
const card = this.cardSubject();
|
|
1405
|
+
const fresh = card !== this.cardSeen;
|
|
1406
|
+
this.cardSeen = card;
|
|
1407
|
+
if (fresh)
|
|
1408
|
+
this.announced = card && !this.narrow ? `${this.cardName(this.liveKey())}, card opened` : '';
|
|
1409
|
+
if (!this.narrow) {
|
|
1410
|
+
this.sheet = null;
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (card && (fresh || changed.has('narrow'))) {
|
|
1414
|
+
if (this.sheet === null || this.focusBefore === 'explorer')
|
|
1415
|
+
this.sheet = 'detail';
|
|
1416
|
+
}
|
|
1417
|
+
else if (fresh && this.sheet === 'detail')
|
|
1418
|
+
this.sheet = null;
|
|
1419
|
+
}
|
|
1420
|
+
/** Whether focus is in the explorer: in its own tree, or in an element the host put in a slot. */
|
|
1421
|
+
holdsFocus() {
|
|
1422
|
+
const active = this.getRootNode().activeElement;
|
|
1423
|
+
return active !== null && this.contains(active);
|
|
1424
|
+
}
|
|
1425
|
+
/** What the selection's card shows, or null where it has nothing to show or `panels` leaves the card out. */
|
|
1426
|
+
cardSubject() {
|
|
1427
|
+
if (!this.has('detail'))
|
|
1428
|
+
return null;
|
|
1429
|
+
const sel = this.resolvedStore?.get('selection');
|
|
1430
|
+
const broken = this.map?.lastPick?.kind === 'broken' ? this.map.lastPick : null;
|
|
1431
|
+
const artifact = sel?.artifact ?? sel?.artifactRefusal ?? null;
|
|
1432
|
+
return (this.showsArtifact() ? artifact : (sel?.item ?? sel?.itemRefusal ?? artifact)) ?? broken;
|
|
1433
|
+
}
|
|
1434
|
+
/** Whether the selection has a card to show: a click that found nothing shows none, and a broken pick shows its fault. */
|
|
1435
|
+
hasDetail() {
|
|
1436
|
+
const selection = this.resolvedStore?.get('selection');
|
|
1437
|
+
return Boolean(selection?.item || selection?.artifact || selection?.artifactRefusal || selection?.itemRefusal || this.map?.lastPick?.kind === 'broken');
|
|
1438
|
+
}
|
|
1297
1439
|
connectedCallback() {
|
|
1298
1440
|
super.connectedCallback();
|
|
1299
1441
|
if (typeof ResizeObserver === 'undefined')
|
|
@@ -1350,6 +1492,19 @@ let MosaicaExplorer = (() => {
|
|
|
1350
1492
|
if (group && !path.includes(group) && !(editor && path.includes(editor)))
|
|
1351
1493
|
this.closeLayers();
|
|
1352
1494
|
};
|
|
1495
|
+
/**
|
|
1496
|
+
* Focus going from the Layers button, its popover or their menus to somewhere else in the page
|
|
1497
|
+
* closes the popover. The Edit colours dialog it opens is not somewhere else.
|
|
1498
|
+
*/
|
|
1499
|
+
onLayersFocusOut = (e) => {
|
|
1500
|
+
const to = e.relatedTarget;
|
|
1501
|
+
if (!this.layersOpen || !(to instanceof Node))
|
|
1502
|
+
return;
|
|
1503
|
+
const editor = this.renderRoot.querySelector('mosaica-colour-editor');
|
|
1504
|
+
if (within(to, e.currentTarget) || (editor && within(to, editor)))
|
|
1505
|
+
return;
|
|
1506
|
+
this.closeLayers();
|
|
1507
|
+
};
|
|
1353
1508
|
openLayers() {
|
|
1354
1509
|
this.layersOpen = true;
|
|
1355
1510
|
document.addEventListener('pointerdown', this.onOutside, true);
|
|
@@ -1357,12 +1512,12 @@ let MosaicaExplorer = (() => {
|
|
|
1357
1512
|
closeLayers() {
|
|
1358
1513
|
this.layersOpen = false;
|
|
1359
1514
|
this.menu = null;
|
|
1515
|
+
this.densityColoursOpen = false;
|
|
1360
1516
|
document.removeEventListener('pointerdown', this.onOutside, true);
|
|
1361
1517
|
}
|
|
1362
1518
|
render() {
|
|
1363
1519
|
const s = this.resolvedStore;
|
|
1364
1520
|
const region = s?.get('region') ?? null;
|
|
1365
|
-
const selection = s?.get('selection');
|
|
1366
1521
|
const meta = s?.get('meta') ?? null;
|
|
1367
1522
|
const artifacts = s?.get('artifacts');
|
|
1368
1523
|
const layersOn = artifacts?.layers.length ?? 0;
|
|
@@ -1371,9 +1526,8 @@ let MosaicaExplorer = (() => {
|
|
|
1371
1526
|
const narrow = this.narrow;
|
|
1372
1527
|
// Short of room, the docked layout takes the overlay's form.
|
|
1373
1528
|
const floating = this.layout === 'overlay' || compact;
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
const hasDetail = Boolean(selection?.item || selection?.artifact || selection?.artifactRefusal || selection?.itemRefusal || this.map?.lastPick?.kind === 'broken');
|
|
1529
|
+
const levels = this.levels;
|
|
1530
|
+
const hasDetail = this.hasDetail();
|
|
1377
1531
|
// The detail shows whichever changed last.
|
|
1378
1532
|
const showArtifact = this.showsArtifact();
|
|
1379
1533
|
const detail = this.liveCard(showArtifact);
|
|
@@ -1386,43 +1540,50 @@ let MosaicaExplorer = (() => {
|
|
|
1386
1540
|
const shown = meta?.views.find((v) => v.id === s?.get('view').id) ?? (meta?.views.length === 1 ? meta.views[0] : undefined);
|
|
1387
1541
|
// A name that is only the view's id says nothing to a reader.
|
|
1388
1542
|
const viewName = shown && shown.displayName !== shown.id ? shown.displayName : '';
|
|
1389
|
-
|
|
1543
|
+
// The heading is the dataset's title, or without one the view's name.
|
|
1390
1544
|
const names = this.datasetTitle
|
|
1391
|
-
? html `<div class="names"><
|
|
1392
|
-
: html `<div class="names">${pickers}${
|
|
1545
|
+
? html `<div class="names"><h2 part="dataset-title" class="title" title=${this.datasetTitle}>${this.datasetTitle}</h2><div class="sub">${pickers}${pickersShown || !viewName ? nothing : html `<span part="view-name">${viewName}</span>`}</div></div>`
|
|
1546
|
+
: html `<div class="names">${pickers}${pickersShown || !viewName ? nothing : html `<h2 part="view-name" class="title" title=${viewName}>${viewName}</h2>`}</div>`;
|
|
1393
1547
|
const named = pickersShown || viewName !== '' || this.datasetTitle !== '';
|
|
1394
1548
|
const layersPanel = html `<slot name="layers"><mosaica-layer-picker exportparts=${FORWARD['layer-picker']}></mosaica-layer-picker></slot>`;
|
|
1395
|
-
const filters = html `<slot name="filters"><mosaica-filter-panel exportparts=${FORWARD['filter-panel']} pinned=${this.pinnedFilters || nothing} ?compact=${compact || narrow} .
|
|
1396
|
-
|
|
1549
|
+
const filters = html `<slot name="filters"><mosaica-filter-panel exportparts=${FORWARD['filter-panel']} pinned=${this.pinnedFilters || nothing} ?compact=${compact || narrow} .clusterLevels=${levels}></mosaica-filter-panel></slot>`;
|
|
1550
|
+
// The status strip speaks for the map and the selection card, unless the host replaced it with something else.
|
|
1551
|
+
const quiet = !this.stripReplaced;
|
|
1552
|
+
const selectionPanel = this.has('selection') && region ? html `<slot name="selection"><mosaica-selection exportparts=${FORWARD.selection} ?quiet=${quiet}></mosaica-selection></slot>` : nothing;
|
|
1397
1553
|
const selectionCard = selectionPanel === nothing ? nothing : html `<div part="selection-card" class="card">${selectionPanel}</div>`;
|
|
1398
1554
|
const head = html `<div class="head" ?hidden=${!named}>${names}</div>`;
|
|
1399
1555
|
// The card beside the point, where the map holds a position for what is selected; else the
|
|
1400
1556
|
// card stands in the top-right corner.
|
|
1401
1557
|
const anchor = this.liveAnchor(showArtifact);
|
|
1402
1558
|
const detailInColumn = this.has('detail') && hasDetail && anchor === null;
|
|
1403
|
-
|
|
1404
|
-
const
|
|
1559
|
+
// The narrow layout shows the selection and the detail in the Item sheet.
|
|
1560
|
+
const right = html `<div slot="top-right" class="right"><slot name="top-right"></slot>${narrow ? nothing : selectionCard}${detailInColumn && !narrow ? html `<div part="detail" class="card">${detail}</div>` : nothing}</div>`;
|
|
1561
|
+
// The column is named for what it holds.
|
|
1562
|
+
const columnName = this.has('filters') ? 'Fields' : 'View';
|
|
1563
|
+
const docked = html `<aside part="sidebar" aria-label=${columnName}>
|
|
1405
1564
|
${this.has('toolbar') ? head : nothing}
|
|
1406
1565
|
${this.has('filters') ? filters : nothing}
|
|
1407
1566
|
</aside>`;
|
|
1408
1567
|
const panel = this.has('toolbar') || this.has('filters')
|
|
1409
|
-
? html `<div part="panel" class="card" role="region" aria-label
|
|
1568
|
+
? html `<div part="panel" class="card" role="region" aria-label=${columnName}>${this.has('toolbar') ? head : nothing}${this.has('filters') ? filters : nothing}</div>`
|
|
1410
1569
|
: nothing;
|
|
1411
1570
|
const layersButton = this.has('legend')
|
|
1412
|
-
? html `<div class="layers" slot=${compact ? 'bottom-left' : 'top-left'} @keydown=${this.onLayersKey}>
|
|
1571
|
+
? html `<div class="layers" slot=${compact ? 'bottom-left' : 'top-left'} @keydown=${this.onLayersKey} @focusout=${this.onLayersFocusOut}>
|
|
1413
1572
|
<div class="group"><button part="layers-toggle" type="button" aria-label=${`Layers and display, ${layersOn} layers on`} aria-expanded=${this.layersOpen ? 'true' : 'false'}
|
|
1414
|
-
aria-controls
|
|
1573
|
+
aria-controls=${this.layersOpen ? 'layers-popover' : nothing} ?data-on=${layersOn > 0} @click=${() => (this.layersOpen ? this.closeLayers() : this.openLayers())}>${icon('layers', 16, 1.2)}${layersOn > 0 ? html `<span class="on"></span>` : nothing}</button></div>
|
|
1415
1574
|
${this.layersOpen
|
|
1416
|
-
? html `<div part="layers-popover" id="layers-popover" class="card" role="dialog" aria-label="Layers and display" @pointerdown=${this.onPopoverPress}>${this.displaySection()}${layersPanel}</div>${this.choiceMenu()}`
|
|
1575
|
+
? html `<div part="layers-popover" id="layers-popover" class="card" role="dialog" aria-label="Layers and display" tabindex="-1" @pointerdown=${this.onPopoverPress}>${this.displaySection()}${layersPanel}</div>${this.choiceMenu()}`
|
|
1417
1576
|
: nothing}
|
|
1418
1577
|
</div>`
|
|
1419
1578
|
: nothing;
|
|
1420
1579
|
const tabs = TABS.filter((t) => this.has(t.panel));
|
|
1421
|
-
// One
|
|
1580
|
+
// One button is in the page's tab order: the one focused last, else the open sheet's, else the first.
|
|
1422
1581
|
const reachable = tabs.find((t) => t.sheet === this.tabFocus) ?? tabs.find((t) => t.sheet === this.sheet) ?? tabs[0];
|
|
1423
|
-
const tab = ({ sheet, icon: ic, label }) => html `<button type="button"
|
|
1424
|
-
aria-
|
|
1425
|
-
@click=${() => {
|
|
1582
|
+
const tab = ({ sheet, icon: ic, label }) => html `<button type="button" id=${`opens-${sheet}`} data-sheet=${sheet} tabindex=${reachable?.sheet === sheet ? '0' : '-1'}
|
|
1583
|
+
aria-expanded=${this.sheet === sheet ? 'true' : 'false'} aria-controls=${this.sheet === sheet ? 'sheet' : nothing}
|
|
1584
|
+
@click=${(e) => {
|
|
1585
|
+
// A press does not focus a button in every browser, and the sheet takes the focus the explorer holds.
|
|
1586
|
+
e.currentTarget.focus();
|
|
1426
1587
|
this.tabFocus = sheet;
|
|
1427
1588
|
this.sheet = this.sheet === sheet ? null : sheet;
|
|
1428
1589
|
}}>${icon(ic, 18)}${label}</button>`;
|
|
@@ -1442,16 +1603,16 @@ let MosaicaExplorer = (() => {
|
|
|
1442
1603
|
<button class="btn primary" type="button" @click=${() => (this.sheet = null)}>${matchedText}</button>
|
|
1443
1604
|
</div>`;
|
|
1444
1605
|
const sheetBody = this.sheet === 'filters'
|
|
1445
|
-
? html
|
|
1606
|
+
? html `<div class="sheet-body">${filters}</div>${sheetFooter}`
|
|
1446
1607
|
: this.sheet === 'layers'
|
|
1447
|
-
? html
|
|
1608
|
+
? html `<div class="sheet-body">${this.displaySection()}${layersPanel}${this.choiceMenu()}</div>`
|
|
1448
1609
|
: this.sheet === 'detail'
|
|
1449
|
-
? html
|
|
1610
|
+
? html `<div class="sheet-body">${selectionPanel}${detail}</div>`
|
|
1450
1611
|
: nothing;
|
|
1451
1612
|
// The tooltip slot is forwarded only when the host supplied one: a slot assigned another slot
|
|
1452
1613
|
// counts as filled even when that slot is empty, which would hide the map's own tooltip.
|
|
1453
1614
|
return html `<div part="frame" class=${`${floating ? 'floating' : 'docked'}${compact ? ' compact' : ''}${narrow && this.has('toolbar') && named ? ' titled' : ''}`}
|
|
1454
|
-
@mosaica-levelchange=${(e) =>
|
|
1615
|
+
@mosaica-levelchange=${(e) => this.chooseLevel(e.detail.layer, e.detail.level)}
|
|
1455
1616
|
@mosaica-artifactfit=${(e) => this.map?.fitTo(BigInt(e.detail.id))}
|
|
1456
1617
|
@mosaica-viewfollow=${(e) => this.followItem(e.detail)}
|
|
1457
1618
|
@mosaica-close=${(e) => this.closeDetail(e)}>
|
|
@@ -1461,14 +1622,16 @@ let MosaicaExplorer = (() => {
|
|
|
1461
1622
|
${floating && !narrow ? panel : nothing}
|
|
1462
1623
|
<mosaica-map
|
|
1463
1624
|
exportparts=${FORWARD.map}
|
|
1625
|
+
aria-label=${this.getAttribute('aria-label') ?? nothing}
|
|
1626
|
+
?quiet=${quiet}
|
|
1464
1627
|
colour-by=${this.colourBy || nothing}
|
|
1465
1628
|
layers=${this.layers || nothing}
|
|
1466
1629
|
tooltip-fields=${this.tooltipFields}
|
|
1467
1630
|
title-field=${this.titleField || nothing}
|
|
1468
1631
|
budget=${this.budget || nothing}
|
|
1469
|
-
.
|
|
1632
|
+
.clusterSpacing=${this.clusterSpacing}
|
|
1470
1633
|
controls-corner=${compact ? 'bottom-left' : 'top-left'}
|
|
1471
|
-
.
|
|
1634
|
+
.clusterLevels=${levels}
|
|
1472
1635
|
.basemap=${this.basemap}
|
|
1473
1636
|
.noPoints=${this.noPoints}
|
|
1474
1637
|
.radius=${this.radius}
|
|
@@ -1493,22 +1656,24 @@ let MosaicaExplorer = (() => {
|
|
|
1493
1656
|
@mosaica-pick=${() => this.requestUpdate()}
|
|
1494
1657
|
@mosaica-itemopen=${() => this.requestUpdate()}
|
|
1495
1658
|
@mosaica-miss=${() => this.onMiss()}
|
|
1496
|
-
@keydown=${this.onMapKey}
|
|
1497
1659
|
@click=${() => this.requestUpdate()}
|
|
1498
1660
|
>
|
|
1499
1661
|
${narrow ? nothing : layersButton}
|
|
1500
|
-
${
|
|
1501
|
-
${narrow ? nothing : html `<div slot="bottom-right" class="in-map-strip"><slot name="status"><mosaica-status exportparts=${FORWARD.status} ?compact=${compact}></mosaica-status></slot></div>`}
|
|
1662
|
+
${right}
|
|
1663
|
+
${narrow ? nothing : html `<div slot="bottom-right" class="in-map-strip"><slot name="status" @slotchange=${this.onStatusSlot}><mosaica-status exportparts=${FORWARD.status} ?compact=${compact}></mosaica-status></slot></div>`}
|
|
1502
1664
|
${this.querySelector('[slot="tooltip"]') ? html `<slot name="tooltip" slot="tooltip"></slot>` : nothing}
|
|
1503
1665
|
</mosaica-map>
|
|
1504
1666
|
${narrow ? nothing : this.callouts(this.has('detail') && hasDetail ? anchor : null, detail)}
|
|
1667
|
+
${narrow && this.sheet && sheetBody !== nothing
|
|
1668
|
+
? html `<div part="sheet" id="sheet" role="dialog" aria-labelledby=${`opens-${this.sheet}`} tabindex="-1" @keydown=${this.onSheetKey}>${sheetBody}</div>`
|
|
1669
|
+
: nothing}
|
|
1505
1670
|
</div>
|
|
1506
|
-
|
|
1507
|
-
|
|
1671
|
+
<div class="sr-only" role="status">${this.announced}</div>
|
|
1672
|
+
${narrow
|
|
1673
|
+
? html `<div part="strip-row"><slot name="status" @slotchange=${this.onStatusSlot}><mosaica-status exportparts=${FORWARD.status}></mosaica-status></slot></div>
|
|
1674
|
+
<div part="tabs" role="toolbar" aria-label="Panels" @keydown=${this.onTabKey}>${tabs.map(tab)}</div>`
|
|
1508
1675
|
: nothing}
|
|
1509
|
-
<
|
|
1510
|
-
<div part="tabs" role="tablist" aria-label="Explorer panels" @keydown=${this.onTabKey}>${tabs.map(tab)}</div>
|
|
1511
|
-
<mosaica-colour-editor exportparts=${FORWARD['colour-editor']} field=${s?.get('legend').colourBy ?? ''} .level=${level}></mosaica-colour-editor>
|
|
1676
|
+
<mosaica-colour-editor exportparts=${FORWARD['colour-editor']} field=${s?.get('legend').colourBy ?? ''} .clusterLevels=${levels}></mosaica-colour-editor>
|
|
1512
1677
|
</div>`;
|
|
1513
1678
|
}
|
|
1514
1679
|
/** The item or artifact card for what is selected, or the host's in the `detail` slot. */
|
|
@@ -1600,19 +1765,22 @@ let MosaicaExplorer = (() => {
|
|
|
1600
1765
|
if (!m)
|
|
1601
1766
|
return nothing;
|
|
1602
1767
|
const live = this.liveKey();
|
|
1603
|
-
|
|
1768
|
+
// Pin keeps one name, the card's, and says whether it is on by its pressed state; its hint to a pointer says what a press does.
|
|
1769
|
+
const pinButton = (pressed, name, onPress) => html `<button part="pin" slot="actions" type="button" aria-pressed=${pressed ? 'true' : 'false'} aria-label=${`Pin ${name}`} title=${pressed ? 'Unpin' : 'Keep open'} @click=${onPress}>${icon('pin', 13, 1.5)}</button>`;
|
|
1604
1770
|
const cards = this.pinned
|
|
1605
1771
|
.filter((p) => p.view === view)
|
|
1606
1772
|
.map((p) => {
|
|
1607
|
-
const
|
|
1773
|
+
const name = this.cardName(p.key);
|
|
1774
|
+
const unpin = pinButton(true, name, () => (this.pinned = this.pinned.filter((q) => q.key !== p.key)));
|
|
1608
1775
|
const body = p.kind === 'item'
|
|
1609
1776
|
? this.itemCard(p.item, unpin)
|
|
1610
1777
|
: html `<mosaica-artifact-card exportparts=${FORWARD['artifact-card']} .artifact=${p.artifact}>${unpin}</mosaica-artifact-card>`;
|
|
1611
|
-
return { key: p.key, world: p.world, body, pinned: true, name
|
|
1778
|
+
return { key: p.key, world: p.world, body, pinned: true, name };
|
|
1612
1779
|
});
|
|
1613
1780
|
if (anchor && !(live && this.pinned.some((p) => p.key === live && p.view === view))) {
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1781
|
+
const name = this.cardName(live);
|
|
1782
|
+
const body = live ? this.liveCard(this.showsArtifact(), pinButton(false, name, () => this.togglePin())) : detail;
|
|
1783
|
+
cards.push({ key: 'live', world: anchor, body, pinned: false, name });
|
|
1616
1784
|
}
|
|
1617
1785
|
this.cards = cards.map(({ key, world, pinned }) => ({ key, world, pinned }));
|
|
1618
1786
|
if (cards.length === 0)
|
|
@@ -1627,7 +1795,8 @@ let MosaicaExplorer = (() => {
|
|
|
1627
1795
|
/**
|
|
1628
1796
|
* Place each callout beside its point under the camera as it stands, clear of the cards over the
|
|
1629
1797
|
* map and of the callouts placed before it, and join it to its point. A callout whose point is off
|
|
1630
|
-
* the map is hidden with its leader
|
|
1798
|
+
* the map is hidden with its leader, and gives the focus it holds to the map. Reads no layout: the
|
|
1799
|
+
* sizes are those {@link measure} took.
|
|
1631
1800
|
*/
|
|
1632
1801
|
placeCallouts(firm = this.moving !== null) {
|
|
1633
1802
|
const m = this.map;
|
|
@@ -1650,6 +1819,8 @@ let MosaicaExplorer = (() => {
|
|
|
1650
1819
|
const last = this.placements.get(key);
|
|
1651
1820
|
const kept = last && last.world[0] === card.world[0] && last.world[1] === card.world[1] ? { side: last.side, offset: last.offset, firm } : null;
|
|
1652
1821
|
const at = size.width > 0 && size.height > 0 ? placeCallout(point, box, size, [...this.keepClear, ...placed], undefined, kept, CALLOUT_MIN_HEIGHT) : null;
|
|
1822
|
+
if (at === null && !el.hidden && within(this.renderRoot.activeElement, el))
|
|
1823
|
+
m.focus();
|
|
1653
1824
|
el.hidden = at === null;
|
|
1654
1825
|
leader?.setAttribute('visibility', at === null ? 'hidden' : 'visible');
|
|
1655
1826
|
if (!at)
|
|
@@ -1694,6 +1865,7 @@ let MosaicaExplorer = (() => {
|
|
|
1694
1865
|
});
|
|
1695
1866
|
// A card that grew, as under Show all, keeps its side and grows down where that is clear.
|
|
1696
1867
|
this.placeCallouts();
|
|
1868
|
+
this.coverMap();
|
|
1697
1869
|
}
|
|
1698
1870
|
/** The cards over the map that callouts keep clear of, and the map's tools and the Layers button. */
|
|
1699
1871
|
keptClear() {
|
|
@@ -1717,7 +1889,8 @@ let MosaicaExplorer = (() => {
|
|
|
1717
1889
|
this.measure();
|
|
1718
1890
|
return;
|
|
1719
1891
|
}
|
|
1720
|
-
const
|
|
1892
|
+
const sheet = this.renderRoot.querySelector('[part="sheet"]');
|
|
1893
|
+
const now = new Set([...(m ? [m] : []), ...this.keptClear(), ...callouts, ...(sheet ? [sheet] : [])]);
|
|
1721
1894
|
for (const el of this.observed)
|
|
1722
1895
|
if (!now.has(el))
|
|
1723
1896
|
this.sizes.unobserve(el);
|
|
@@ -1754,24 +1927,9 @@ let MosaicaExplorer = (() => {
|
|
|
1754
1927
|
}
|
|
1755
1928
|
});
|
|
1756
1929
|
}
|
|
1757
|
-
/**
|
|
1758
|
-
* Tab from the map itself goes to the card beside the point, the selection's where it shows, so
|
|
1759
|
-
* the card is the next stop after the map; from there Tab goes on in the page's order.
|
|
1760
|
-
*/
|
|
1761
|
-
onMapKey = (e) => {
|
|
1762
|
-
const from = e.composedPath()[0];
|
|
1763
|
-
if (e.key !== 'Tab' || e.shiftKey || !this.map || (from !== this.map && from !== this.map.focusTarget))
|
|
1764
|
-
return;
|
|
1765
|
-
const shown = Array.from(this.renderRoot.querySelectorAll('[part~="callout"]')).filter((c) => !c.hidden);
|
|
1766
|
-
const card = shown.find((c) => c.dataset.callout === 'live') ?? shown[0];
|
|
1767
|
-
if (!card)
|
|
1768
|
-
return;
|
|
1769
|
-
e.preventDefault();
|
|
1770
|
-
card.focus();
|
|
1771
|
-
};
|
|
1772
1930
|
/** Escape on a card: the card not pinned closes, dropping the selection; focus goes back to the map. */
|
|
1773
1931
|
onCalloutKey = (e) => {
|
|
1774
|
-
if (e.key !== 'Escape')
|
|
1932
|
+
if (e.key !== 'Escape' || e.defaultPrevented)
|
|
1775
1933
|
return;
|
|
1776
1934
|
e.stopPropagation();
|
|
1777
1935
|
const key = e.currentTarget.dataset.callout;
|
|
@@ -1810,11 +1968,15 @@ let MosaicaExplorer = (() => {
|
|
|
1810
1968
|
const change = (patch) => this.changeDisplay(patch);
|
|
1811
1969
|
const number = (e) => Number(e.target.value);
|
|
1812
1970
|
const choices = Object.keys(DENSITY_COLOUR_TITLES);
|
|
1971
|
+
// The arrow keys move the choice and leave the list open; a press chooses and closes it.
|
|
1813
1972
|
const colourList = this.densityColoursOpen
|
|
1814
|
-
? html `<div id="density-colour-list" class="ramp-list" role="radiogroup" aria-labelledby="colours-label">
|
|
1973
|
+
? html `<div id="density-colour-list" class="ramp-list" role="radiogroup" aria-labelledby="colours-label" @focusout=${this.onDensityColoursFocusOut} @keydown=${this.onDensityColoursKey}>
|
|
1815
1974
|
${choices.map((c, i) => html `<button type="button" role="radio" data-colours=${c} aria-checked=${c === colours ? 'true' : 'false'} tabindex=${c === colours ? '0' : '-1'}
|
|
1816
|
-
@click=${() =>
|
|
1817
|
-
|
|
1975
|
+
@click=${() => {
|
|
1976
|
+
change({ densityColours: c });
|
|
1977
|
+
this.closeDensityColours();
|
|
1978
|
+
}}
|
|
1979
|
+
@keydown=${(e) => radioKeys(e, choices.length, i, (j) => change({ densityColours: choices[j] }))}>${tick(c === colours)}<span class="bar" style=${`background:${densityGradient(c, scheme)}`}></span>${DENSITY_COLOUR_TITLES[c]}</button>`)}
|
|
1818
1980
|
</div>`
|
|
1819
1981
|
: nothing;
|
|
1820
1982
|
const densityControls = s.density === 'none'
|
|
@@ -1824,22 +1986,23 @@ let MosaicaExplorer = (() => {
|
|
|
1824
1986
|
${this.densityScaleControl(s.densityScale, change)}
|
|
1825
1987
|
${ramped
|
|
1826
1988
|
? html `<span id="colours-label">Colours</span>
|
|
1827
|
-
<button part="density-colours" class="ramp-choice" type="button" aria-
|
|
1989
|
+
<button part="density-colours" class="ramp-choice" type="button" aria-label=${`Colours: ${DENSITY_COLOUR_TITLES[colours]}`} aria-expanded=${this.densityColoursOpen ? 'true' : 'false'}
|
|
1990
|
+
aria-controls=${this.densityColoursOpen ? 'density-colour-list' : nothing} @focusout=${this.onDensityColoursFocusOut}
|
|
1828
1991
|
@click=${() => (this.densityColoursOpen = !this.densityColoursOpen)}><span class="bar" style=${`background:${densityGradient(colours, scheme)}`}></span>${DENSITY_COLOUR_TITLES[colours]}${icon('chev', 12, 1.4)}</button>
|
|
1829
1992
|
${colourList}`
|
|
1830
1993
|
: nothing}
|
|
1831
1994
|
<label for="density-strength">Strength</label>
|
|
1832
1995
|
<div class="with-readout"><input id="density-strength" part="density-strength" class="slider" type="range" min="0.1" max="1" step="0.05" .value=${String(s.densityStrength)}
|
|
1833
|
-
style=${fill(s.densityStrength, 0.1, 1)} @input=${(e) => change({ densityStrength: number(e) })} /><span class="readout">${Math.round(s.densityStrength * 100)}%</span></div>
|
|
1996
|
+
style=${fill(s.densityStrength, 0.1, 1)} aria-valuetext=${`${Math.round(s.densityStrength * 100)}%`} @input=${(e) => change({ densityStrength: number(e) })} /><span class="readout">${Math.round(s.densityStrength * 100)}%</span></div>
|
|
1834
1997
|
</div>`;
|
|
1835
1998
|
return html `<div part="display" class="display">
|
|
1836
|
-
<section class="sec">
|
|
1999
|
+
<section class="sec" aria-labelledby="points-label">
|
|
1837
2000
|
<div class="sec-head">
|
|
1838
|
-
<
|
|
2001
|
+
<h2 class="hd" id="points-label">Points</h2>
|
|
1839
2002
|
<button part="points-toggle" class="switch small" type="button" role="switch" aria-checked=${s.points ? 'true' : 'false'} aria-label="Show points"
|
|
1840
2003
|
@click=${() => change({ points: !s.points })}><span class="knob"></span></button>
|
|
1841
2004
|
</div>
|
|
1842
|
-
${this.mostPoints(!s.points)}${this.
|
|
2005
|
+
${this.mostPoints(!s.points)}${this.clusterDensity()}
|
|
1843
2006
|
<div class="sliders">
|
|
1844
2007
|
${this.sizeControls(radius, !s.points)}
|
|
1845
2008
|
<label for="point-opacity">Opacity</label>
|
|
@@ -1848,10 +2011,10 @@ let MosaicaExplorer = (() => {
|
|
|
1848
2011
|
</div>
|
|
1849
2012
|
</section>
|
|
1850
2013
|
${this.colourSection()}
|
|
1851
|
-
<section class="sec">
|
|
1852
|
-
<
|
|
2014
|
+
<section class="sec" aria-labelledby="density-label">
|
|
2015
|
+
<h2 class="hd" id="density-label">Density</h2>
|
|
1853
2016
|
<div part="density-mode" class="modes" role="radiogroup" aria-labelledby="density-label">
|
|
1854
|
-
${DENSITY_MODES.map((m, i) => html `<button type="button" role="radio" data-mode=${m.mode} aria-checked=${m.mode === s.density ? 'true' : 'false'} tabindex=${i === modeAt ? '0' : '-1'}
|
|
2017
|
+
${DENSITY_MODES.map((m, i) => html `<button type="button" role="radio" data-mode=${m.mode} aria-checked=${m.mode === s.density ? 'true' : 'false'} tabindex=${i === Math.max(0, modeAt) ? '0' : '-1'}
|
|
1855
2018
|
@click=${() => change({ density: m.mode })}
|
|
1856
2019
|
@keydown=${(e) => radioKeys(e, DENSITY_MODES.length, i, (j) => change({ density: DENSITY_MODES[j].mode }))}>${m.label}</button>`)}
|
|
1857
2020
|
</div>
|
|
@@ -1865,67 +2028,89 @@ let MosaicaExplorer = (() => {
|
|
|
1865
2028
|
*/
|
|
1866
2029
|
mostPoints(disabled) {
|
|
1867
2030
|
const s = this.resolvedStore;
|
|
2031
|
+
const min = Math.max(1, this.budgetMin);
|
|
2032
|
+
const max = Math.max(min * 10, this.budgetMax);
|
|
2033
|
+
const span = Math.log(max / min);
|
|
2034
|
+
const at = (n) => Math.min(1, Math.max(0, Math.log(n / min) / span));
|
|
2035
|
+
const shown = this.pointsDragged ?? s?.budget ?? 250_000;
|
|
2036
|
+
// A tick at each power of ten between the ends, where it leaves room for the last end's label.
|
|
2037
|
+
const ticks = [];
|
|
2038
|
+
for (let p = 10 ** Math.ceil(Math.log10(min) + 1e-9); p < max; p *= 10)
|
|
2039
|
+
if (at(p) > 0.08 && at(p) < 0.85)
|
|
2040
|
+
ticks.push({ at: at(p), text: pointsText(p) });
|
|
1868
2041
|
return this.mostSlider({
|
|
1869
2042
|
id: 'most-points',
|
|
1870
2043
|
label: 'Most points',
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2044
|
+
at: at(shown),
|
|
2045
|
+
readout: pointsText(shown),
|
|
2046
|
+
valueText: `${pointsText(shown)} points`,
|
|
2047
|
+
ends: [pointsText(min), pointsText(max)],
|
|
2048
|
+
ticks,
|
|
1874
2049
|
disabled: disabled || !s,
|
|
2050
|
+
of: (t) => Math.min(max, Math.max(min, roundPoints(min * Math.exp(t * span)))),
|
|
1875
2051
|
drag: (n) => (this.pointsDragged = n),
|
|
1876
2052
|
set: (n) => this.setBudget(n)
|
|
1877
2053
|
});
|
|
1878
2054
|
}
|
|
1879
2055
|
/**
|
|
1880
|
-
*
|
|
1881
|
-
*
|
|
2056
|
+
* Cluster density, while a `nested` or `dag` layer is drawn, or a levelled layer following the zoom
|
|
2057
|
+
* is drawn or coloured by: a slider over the cluster spacing on a log scale, from
|
|
2058
|
+
* `cluster-spacing-max` at its Fewer end to `cluster-spacing-min` at its More end, with how many
|
|
2059
|
+
* clusters the spacing gives the map at its size. Letting go sets the store's spacing.
|
|
1882
2060
|
*/
|
|
1883
|
-
|
|
2061
|
+
clusterDensity() {
|
|
1884
2062
|
const s = this.resolvedStore;
|
|
1885
2063
|
const meta = s?.get('meta') ?? null;
|
|
1886
2064
|
if (!s || !meta)
|
|
1887
2065
|
return nothing;
|
|
1888
|
-
const
|
|
1889
|
-
|
|
1890
|
-
|
|
2066
|
+
const coloured = clusterLayerOf(s.get('legend').colourBy);
|
|
2067
|
+
const steered = [...s.get('artifacts').layers, ...(coloured === null ? [] : [coloured])].some((name) => {
|
|
2068
|
+
const layer = meta.layers.find((l) => l.name === name);
|
|
2069
|
+
if (!layer)
|
|
2070
|
+
return false;
|
|
2071
|
+
return layer.hierarchy.kind === 'nested' || layer.hierarchy.kind === 'dag' || follows(layer, this.levels.get(name));
|
|
1891
2072
|
});
|
|
1892
|
-
if (!
|
|
2073
|
+
if (!steered)
|
|
1893
2074
|
return nothing;
|
|
2075
|
+
const dense = Math.max(1, this.clusterSpacingMin);
|
|
2076
|
+
const sparse = Math.max(dense * 2, this.clusterSpacingMax);
|
|
2077
|
+
const span = Math.log(sparse / dense);
|
|
2078
|
+
// With no spacing the map shows every cluster, as at the More end.
|
|
2079
|
+
const shown = this.clustersDragged ?? s.clusterSpacing ?? dense;
|
|
2080
|
+
const map = this.renderRoot.querySelector('mosaica-map');
|
|
2081
|
+
const count = map && map.clientWidth > 0 && map.clientHeight > 0 ? clustersInView(shown, map.clientWidth, map.clientHeight) : null;
|
|
2082
|
+
const square = `one per ${Math.round(shown)} px square`;
|
|
1894
2083
|
return this.mostSlider({
|
|
1895
|
-
id: '
|
|
1896
|
-
label: '
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2084
|
+
id: 'cluster-density',
|
|
2085
|
+
label: 'Cluster density',
|
|
2086
|
+
at: Math.min(1, Math.max(0, Math.log(sparse / shown) / span)),
|
|
2087
|
+
readout: count === null ? `1 per ${Math.round(shown)} px` : pointsText(count),
|
|
2088
|
+
valueText: count === null ? square : `${count} clusters, ${square}`,
|
|
2089
|
+
ends: ['Fewer', 'More'],
|
|
2090
|
+
ticks: [],
|
|
1900
2091
|
disabled: false,
|
|
2092
|
+
of: (t) => Math.min(sparse, Math.max(dense, Math.round(sparse * Math.exp(-t * span)))),
|
|
1901
2093
|
drag: (n) => (this.clustersDragged = n),
|
|
1902
|
-
set: (n) => this.
|
|
2094
|
+
set: (n) => this.setClusterSpacing(n)
|
|
1903
2095
|
});
|
|
1904
2096
|
}
|
|
1905
|
-
/**
|
|
2097
|
+
/**
|
|
2098
|
+
* A slider standing `at` of the way along, with its readout, its ends' labels and its ticks. `of`
|
|
2099
|
+
* gives the value at a share of the way along; dragging passes it to `drag`, and letting go to
|
|
2100
|
+
* `set`.
|
|
2101
|
+
*/
|
|
1906
2102
|
mostSlider(o) {
|
|
1907
|
-
const
|
|
1908
|
-
const max = Math.max(min * 10, o.range[1]);
|
|
1909
|
-
const span = Math.log(max / min);
|
|
1910
|
-
const at = (n) => Math.min(1, Math.max(0, Math.log(n / min) / span));
|
|
1911
|
-
const of = (t) => Math.min(max, Math.max(min, roundPoints(min * Math.exp(t * span))));
|
|
1912
|
-
const t = at(o.shown);
|
|
1913
|
-
// A tick at each power of ten between the ends, where it leaves room for the last end's label.
|
|
1914
|
-
const ticks = [];
|
|
1915
|
-
for (let p = 10 ** Math.ceil(Math.log10(min) + 1e-9); p < max; p *= 10)
|
|
1916
|
-
if (at(p) > 0.08 && at(p) < 0.85)
|
|
1917
|
-
ticks.push(p);
|
|
1918
|
-
const read = (e) => of(Number(e.target.value) / 1000);
|
|
2103
|
+
const read = (e) => o.of(Number(e.target.value) / 1000);
|
|
1919
2104
|
return html `<div class="most">
|
|
1920
|
-
<div class="most-top"><label for=${o.id}>${o.label}</label><span part=${o.id === 'most-points' ? 'most-points-value' : '
|
|
1921
|
-
<input id=${o.id} part=${o.id === 'most-points' ? 'most-points' : '
|
|
1922
|
-
style=${`--fill:${(
|
|
2105
|
+
<div class="most-top"><label for=${o.id}>${o.label}</label><span part=${o.id === 'most-points' ? 'most-points-value' : 'cluster-density-value'} class="most-value">${o.readout}</span></div>
|
|
2106
|
+
<input id=${o.id} part=${o.id === 'most-points' ? 'most-points' : 'cluster-density'} class="slider" type="range" min="0" max="1000" step="1" .value=${String(Math.round(o.at * 1000))} ?disabled=${o.disabled}
|
|
2107
|
+
style=${`--fill:${(o.at * 100).toFixed(1)}%`} aria-valuetext=${o.valueText}
|
|
1923
2108
|
@input=${(e) => o.drag(read(e))}
|
|
1924
2109
|
@change=${(e) => o.set(read(e))} />
|
|
1925
2110
|
<div class="ticks" aria-hidden="true">
|
|
1926
|
-
<span style="left:0">${
|
|
1927
|
-
${ticks.map((p) => html `<span class="mid" style=${`left:${(at
|
|
1928
|
-
<span style="right:0">${
|
|
2111
|
+
<span style="left:0">${o.ends[0]}</span>
|
|
2112
|
+
${o.ticks.map((p) => html `<span class="mid" style=${`left:${(p.at * 100).toFixed(1)}%`}>${p.text}</span>`)}
|
|
2113
|
+
<span style="right:0">${o.ends[1]}</span>
|
|
1929
2114
|
</div>
|
|
1930
2115
|
</div>`;
|
|
1931
2116
|
}
|
|
@@ -1939,15 +2124,15 @@ let MosaicaExplorer = (() => {
|
|
|
1939
2124
|
emit(this, 'mosaica-budgetchange', { budget });
|
|
1940
2125
|
this.requestUpdate();
|
|
1941
2126
|
}
|
|
1942
|
-
/** Set the
|
|
1943
|
-
|
|
2127
|
+
/** Set the store's cluster spacing from Cluster density, and report it. */
|
|
2128
|
+
setClusterSpacing(spacing) {
|
|
1944
2129
|
this.clustersDragged = null;
|
|
1945
2130
|
const s = this.resolvedStore;
|
|
1946
|
-
if (!s ||
|
|
2131
|
+
if (!s || spacing === s.clusterSpacing)
|
|
1947
2132
|
return;
|
|
1948
|
-
s.
|
|
1949
|
-
this.
|
|
1950
|
-
emit(this, 'mosaica-
|
|
2133
|
+
s.setClusterSpacing(spacing);
|
|
2134
|
+
this.clusterSpacing = spacing;
|
|
2135
|
+
emit(this, 'mosaica-clusterspacingchange', { spacing });
|
|
1951
2136
|
}
|
|
1952
2137
|
/**
|
|
1953
2138
|
* The Colour section: Colour by, then the palette for a category, or the ramp, its scale and
|
|
@@ -1965,7 +2150,7 @@ let MosaicaExplorer = (() => {
|
|
|
1965
2150
|
const clusters = colourBy?.startsWith(CLUSTER_PREFIX) ?? false;
|
|
1966
2151
|
const palette = s && clusters ? PALETTES[s.get('artifacts').palette] : CATEGORY_PALETTES[colouring.palette];
|
|
1967
2152
|
const choice = (menu, label, body, stretch = false) => html `<button part=${{ size: 'size-by', colour: 'colour-by', palette: 'palette', ramp: 'ramp' }[menu]} class=${`ramp-choice${stretch ? ' stretch' : ''}`} type="button" aria-haspopup="menu" aria-expanded=${this.menu === menu ? 'true' : 'false'}
|
|
1968
|
-
aria-label=${label} ?disabled=${!meta} @click=${() => (this.menu = this.menu === menu ? null : menu)} @keydown=${this.onMenuButtonKey(menu)}>${body}${icon('chev', 12, 1.4)}</button>`;
|
|
2153
|
+
aria-label=${label} title=${label} ?disabled=${!meta} @click=${() => (this.menu = this.menu === menu ? null : menu)} @keydown=${this.onMenuButtonKey(menu)}>${body}${icon('chev', 12, 1.4)}</button>`;
|
|
1969
2154
|
const byName = current?.title ?? 'None';
|
|
1970
2155
|
const palettes = (column?.category || clusters) && !this.hidePalettes
|
|
1971
2156
|
? html `<span>Palette</span>${choice('palette', `Palette: ${palette.title}`, html `<span class="strip">${palette.colours.slice(0, 10).map((c) => html `<span style=${`background:${hexOf(c)}`}></span>`)}</span><span class="t">${palette.title}</span>`, true)}`
|
|
@@ -1984,8 +2169,8 @@ let MosaicaExplorer = (() => {
|
|
|
1984
2169
|
: nothing;
|
|
1985
2170
|
// A category's values and a layer's clusters have colours of their own to edit.
|
|
1986
2171
|
const editable = Boolean(column?.category) || clusters;
|
|
1987
|
-
return html `<section class="sec">
|
|
1988
|
-
<
|
|
2172
|
+
return html `<section class="sec" aria-labelledby="colour-label">
|
|
2173
|
+
<h2 class="hd" id="colour-label">Colour</h2>
|
|
1989
2174
|
<div class="sliders">
|
|
1990
2175
|
<span>Colour by</span>${choice('colour', `Colour by: ${byName}`, html `<span class="t">${byName}</span>`)}
|
|
1991
2176
|
${palettes}${ramps}
|
|
@@ -1995,7 +2180,8 @@ let MosaicaExplorer = (() => {
|
|
|
1995
2180
|
}
|
|
1996
2181
|
/**
|
|
1997
2182
|
* What Colour by offers: None, every layer that can colour (a levelled layer once per level, by
|
|
1998
|
-
* its levels' titles
|
|
2183
|
+
* its levels' titles, after Follow zoom where no level declares a zoom range), and the rendered
|
|
2184
|
+
* category and number columns.
|
|
1999
2185
|
*/
|
|
2000
2186
|
colourOptions() {
|
|
2001
2187
|
const s = this.resolvedStore;
|
|
@@ -2007,13 +2193,19 @@ let MosaicaExplorer = (() => {
|
|
|
2007
2193
|
const value = `${CLUSTER_PREFIX}${decl.name}`;
|
|
2008
2194
|
if (decl.levels.length <= 1)
|
|
2009
2195
|
return [{ value, title: decl.title || decl.name, kind: 'Clusters', checked: colourBy === value }];
|
|
2010
|
-
const
|
|
2011
|
-
|
|
2196
|
+
const chosen = this.levels.get(decl.name);
|
|
2197
|
+
const following = follows(decl, chosen);
|
|
2198
|
+
const drawn = levelOfLayer(decl, chosen, s?.get('artifacts').followedLevels);
|
|
2199
|
+
const follow = levelFollowsZoom(decl) ? [{ value: `${value}@`, title: `${decl.title || decl.name}, follow zoom`, kind: 'Clusters', checked: colourBy === value && following }] : [];
|
|
2200
|
+
return [...follow, ...decl.levels.map((lv) => ({ value: `${value}@${lv.level}`, title: lv.title || `${decl.title || decl.name}, level ${lv.level}`, kind: 'Clusters', checked: colourBy === value && !following && drawn === lv.level }))];
|
|
2012
2201
|
});
|
|
2013
2202
|
const columns = meta.declaredScalars.filter((c) => c.render).map((c) => ({ value: c.name, title: columnCaption(c.name), kind: c.category ? 'Category' : c.arrowType === 'timestamp_us' ? 'Date' : 'Number', checked: colourBy === c.name }));
|
|
2014
2203
|
return [{ value: '', title: 'None', kind: '', checked: colourBy === null }, ...layers, ...columns];
|
|
2015
2204
|
}
|
|
2016
|
-
/**
|
|
2205
|
+
/**
|
|
2206
|
+
* Colour by `value`: a column, `cluster:<layer>`, `cluster:<layer>@<level>` for one level of a
|
|
2207
|
+
* layer, or `cluster:<layer>@` for the level that follows the zoom.
|
|
2208
|
+
*/
|
|
2017
2209
|
chooseColour(value) {
|
|
2018
2210
|
const s = this.resolvedStore;
|
|
2019
2211
|
if (!s)
|
|
@@ -2022,11 +2214,22 @@ let MosaicaExplorer = (() => {
|
|
|
2022
2214
|
const colourBy = value === '' ? null : at > 0 ? value.slice(0, at) : value;
|
|
2023
2215
|
s.setColourBy(colourBy);
|
|
2024
2216
|
emit(this, 'mosaica-colourchange', { colourBy });
|
|
2025
|
-
if (at > 0) {
|
|
2026
|
-
|
|
2027
|
-
|
|
2217
|
+
if (at > 0 && colourBy !== null) {
|
|
2218
|
+
const layer = colourBy.slice(CLUSTER_PREFIX.length);
|
|
2219
|
+
const level = at === value.length - 1 ? null : Number(value.slice(at + 1));
|
|
2220
|
+
this.chooseLevel(layer, level);
|
|
2221
|
+
emit(this, 'mosaica-levelchange', { layer, level });
|
|
2028
2222
|
}
|
|
2029
2223
|
}
|
|
2224
|
+
/** Count, colour and label `layer` at `level`, or at its default level where `level` is null; other layers keep theirs. */
|
|
2225
|
+
chooseLevel(layer, level) {
|
|
2226
|
+
const levels = new Map(this.levels);
|
|
2227
|
+
if (level === null)
|
|
2228
|
+
levels.delete(layer);
|
|
2229
|
+
else
|
|
2230
|
+
levels.set(layer, level);
|
|
2231
|
+
this.levels = levels;
|
|
2232
|
+
}
|
|
2030
2233
|
/** Colour a layer's clusters from `palette`, and report it. */
|
|
2031
2234
|
chooseClusterPalette(palette) {
|
|
2032
2235
|
const s = this.resolvedStore;
|
|
@@ -2093,7 +2296,7 @@ let MosaicaExplorer = (() => {
|
|
|
2093
2296
|
const at = DENSITY_SCALES.findIndex((x) => x.scale === scale);
|
|
2094
2297
|
return html `<span id="density-scale-label">Scale</span>
|
|
2095
2298
|
<div part="density-scale" class="seg" role="radiogroup" aria-labelledby="density-scale-label">
|
|
2096
|
-
${DENSITY_SCALES.map((x, i) => html `<button type="button" role="radio" data-scale=${x.scale} aria-checked=${x.scale === scale ? 'true' : 'false'} tabindex=${i === at ? '0' : '-1'}
|
|
2299
|
+
${DENSITY_SCALES.map((x, i) => html `<button type="button" role="radio" data-scale=${x.scale} aria-checked=${x.scale === scale ? 'true' : 'false'} tabindex=${i === Math.max(0, at) ? '0' : '-1'}
|
|
2097
2300
|
@click=${() => change({ densityScale: x.scale })}
|
|
2098
2301
|
@keydown=${(e) => radioKeys(e, DENSITY_SCALES.length, i, (j) => change({ densityScale: DENSITY_SCALES[j].scale }))}>${x.label}</button>`)}
|
|
2099
2302
|
</div>`;
|
|
@@ -2122,10 +2325,15 @@ let MosaicaExplorer = (() => {
|
|
|
2122
2325
|
const sizing = this.sizing;
|
|
2123
2326
|
const number = (e) => Number(e.target.value);
|
|
2124
2327
|
const { min, max, step } = SIZE_RANGE;
|
|
2328
|
+
// With no column to offer, the button says why: to a pointer in its `title`, and in the text that describes it.
|
|
2329
|
+
const none = this.resolvedStore?.get('meta') && this.sizeColumns.length === 0;
|
|
2330
|
+
const why = 'No number column to size by';
|
|
2331
|
+
const shown = sizeBy === null ? 'None' : columnCaption(sizeBy);
|
|
2125
2332
|
const choice = html `<span id="size-by-label">Size by</span>
|
|
2126
|
-
<button part="size-by" class="ramp-choice" type="button" aria-haspopup="menu" aria-expanded=${this.menu === 'size' ? 'true' : 'false'} aria-label=${`Size by: ${
|
|
2127
|
-
?disabled=${disabled || (sizeBy === null && this.sizeColumns.length === 0)} title=${
|
|
2128
|
-
@click=${() => (this.menu = this.menu === 'size' ? null : 'size')} @keydown=${this.onMenuButtonKey('size')}><span class="t">${
|
|
2333
|
+
<button part="size-by" class="ramp-choice" type="button" aria-haspopup="menu" aria-expanded=${this.menu === 'size' ? 'true' : 'false'} aria-label=${`Size by: ${shown}`}
|
|
2334
|
+
?disabled=${disabled || (sizeBy === null && this.sizeColumns.length === 0)} title=${none ? why : shown} aria-describedby=${none ? 'size-by-note' : nothing}
|
|
2335
|
+
@click=${() => (this.menu = this.menu === 'size' ? null : 'size')} @keydown=${this.onMenuButtonKey('size')}><span class="t">${shown}</span>${icon('chev', 12, 1.4)}</button>
|
|
2336
|
+
${none ? html `<span id="size-by-note" class="sr-only">${why}</span>` : nothing}`;
|
|
2129
2337
|
if (sizeBy === null) {
|
|
2130
2338
|
if (this.hideSize)
|
|
2131
2339
|
return choice;
|
|
@@ -2138,14 +2346,14 @@ let MosaicaExplorer = (() => {
|
|
|
2138
2346
|
return html `${choice}<span id="size-range-label">Range</span>
|
|
2139
2347
|
<div class="range" role="group" aria-labelledby="size-range-label">
|
|
2140
2348
|
<input part="size-min" class="slider" type="range" min=${min} max=${max} step=${step} aria-label="Smallest size" .value=${String(sizing.min)} ?disabled=${disabled}
|
|
2141
|
-
style=${fill(sizing.min, min, max)} @input=${(e) => this.changeSize({ min: number(e), max: Math.max(number(e), sizing.max) })} />
|
|
2349
|
+
style=${fill(sizing.min, min, max)} aria-valuetext=${`${px(sizing.min)} px`} @input=${(e) => this.changeSize({ min: number(e), max: Math.max(number(e), sizing.max) })} />
|
|
2142
2350
|
<span part="size-range" class="readout">${px(sizing.min)} – ${px(sizing.max)} px</span>
|
|
2143
2351
|
<input part="size-max" class="slider" type="range" min=${min} max=${max} step=${step} aria-label="Largest size" .value=${String(sizing.max)} ?disabled=${disabled}
|
|
2144
|
-
style=${fill(sizing.max, min, max)} @input=${(e) => this.changeSize({ max: number(e), min: Math.min(number(e), sizing.min) })} />
|
|
2352
|
+
style=${fill(sizing.max, min, max)} aria-valuetext=${`${px(sizing.max)} px`} @input=${(e) => this.changeSize({ max: number(e), min: Math.min(number(e), sizing.min) })} />
|
|
2145
2353
|
</div>
|
|
2146
2354
|
<span id="size-scale-label">Scale</span>
|
|
2147
2355
|
<div part="size-scale" class="seg" role="radiogroup" aria-labelledby="size-scale-label">
|
|
2148
|
-
${SIZE_SCALES.map((x, i) => html `<button type="button" role="radio" data-scale=${x.scale} aria-checked=${x.scale === sizing.scale ? 'true' : 'false'} tabindex=${i === at ? '0' : '-1'} ?disabled=${disabled}
|
|
2356
|
+
${SIZE_SCALES.map((x, i) => html `<button type="button" role="radio" data-scale=${x.scale} aria-checked=${x.scale === sizing.scale ? 'true' : 'false'} tabindex=${i === Math.max(0, at) ? '0' : '-1'} ?disabled=${disabled}
|
|
2149
2357
|
@click=${() => this.changeSize({ scale: x.scale })}
|
|
2150
2358
|
@keydown=${(e) => radioKeys(e, SIZE_SCALES.length, i, (j) => this.changeSize({ scale: SIZE_SCALES[j].scale }))}>${x.label}</button>`)}
|
|
2151
2359
|
</div>`;
|
|
@@ -2218,10 +2426,19 @@ let MosaicaExplorer = (() => {
|
|
|
2218
2426
|
const items = Array.from(this.renderRoot.querySelectorAll(`[part~="${menu}-option"]`));
|
|
2219
2427
|
const next = { ArrowDown: (i + 1) % items.length, ArrowUp: (i - 1 + items.length) % items.length, Home: 0, End: items.length - 1 }[e.key];
|
|
2220
2428
|
if (e.key === 'Escape') {
|
|
2429
|
+
// An Escape that has dismissed the map's tooltip does no more.
|
|
2430
|
+
if (e.defaultPrevented)
|
|
2431
|
+
return;
|
|
2221
2432
|
e.stopPropagation();
|
|
2222
2433
|
this.closeMenu(true);
|
|
2223
2434
|
return;
|
|
2224
2435
|
}
|
|
2436
|
+
// Tab closes the menu and goes back to its button, in the popover, and on from there.
|
|
2437
|
+
if (e.key === 'Tab') {
|
|
2438
|
+
e.preventDefault();
|
|
2439
|
+
this.closeMenu(true);
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2225
2442
|
if (next === undefined)
|
|
2226
2443
|
return;
|
|
2227
2444
|
e.preventDefault();
|
|
@@ -2230,8 +2447,8 @@ let MosaicaExplorer = (() => {
|
|
|
2230
2447
|
};
|
|
2231
2448
|
const at = Math.max(0, entries.findIndex((o) => o.value === checked));
|
|
2232
2449
|
const row = (o) => o.line === undefined
|
|
2233
|
-
? html `<span class="lead">${o.swatch ?? nothing}<span>${o.title}</span></span><span class="kind">${o.kind}</span>`
|
|
2234
|
-
: html `${o.swatch ?? nothing}<span class="named"><span class="t">${o.title}</span><span class="line">${o.line}</span></span>`;
|
|
2450
|
+
? html `<span class="lead">${tick(o.value === checked)}${o.swatch ?? nothing}<span>${o.title}</span></span><span class="kind">${o.kind}</span>`
|
|
2451
|
+
: html `${tick(o.value === checked)}${o.swatch ?? nothing}<span class="named"><span class="t">${o.title}</span><span class="line">${o.line}</span></span>`;
|
|
2235
2452
|
return html `<div part=${{ size: 'size-menu', colour: 'colour-menu', palette: 'palette-menu', ramp: 'ramp-menu' }[menu]} class=${`size-menu${menu === 'ramp' ? ' wide' : menu === 'palette' ? ' palettes' : ''}`} popover="manual" role="menu" aria-labelledby="menu-label" @focusout=${this.onMenuFocusOut}>
|
|
2236
2453
|
<div class="hd" id="menu-label">${title}</div>
|
|
2237
2454
|
${entries.map((o, i) => html `<button part=${{ size: 'size-option', colour: 'colour-option', palette: 'palette-option', ramp: 'ramp-option' }[menu]} type="button" role="menuitemradio" data-value=${o.value} aria-checked=${o.value === checked ? 'true' : 'false'}
|
|
@@ -2254,7 +2471,7 @@ let MosaicaExplorer = (() => {
|
|
|
2254
2471
|
this.menu = menu;
|
|
2255
2472
|
};
|
|
2256
2473
|
}
|
|
2257
|
-
/** Focus leaving the open menu
|
|
2474
|
+
/** Focus leaving the open menu closes it. */
|
|
2258
2475
|
onMenuFocusOut = (e) => {
|
|
2259
2476
|
const to = e.relatedTarget;
|
|
2260
2477
|
if (to && e.currentTarget.contains(to))
|
|
@@ -2300,10 +2517,8 @@ let MosaicaExplorer = (() => {
|
|
|
2300
2517
|
this.pointOpacity = patch.pointOpacity;
|
|
2301
2518
|
if (patch.density !== undefined)
|
|
2302
2519
|
this.density = patch.density;
|
|
2303
|
-
if (patch.densityColours !== undefined)
|
|
2520
|
+
if (patch.densityColours !== undefined)
|
|
2304
2521
|
this.densityColours = patch.densityColours ?? '';
|
|
2305
|
-
this.densityColoursOpen = false;
|
|
2306
|
-
}
|
|
2307
2522
|
if (patch.densityStrength !== undefined)
|
|
2308
2523
|
this.densityStrength = patch.densityStrength;
|
|
2309
2524
|
if (patch.densityResolution !== undefined)
|
|
@@ -2312,17 +2527,36 @@ let MosaicaExplorer = (() => {
|
|
|
2312
2527
|
this.densityScale = patch.densityScale;
|
|
2313
2528
|
emit(this, 'mosaica-displaychange', this.display);
|
|
2314
2529
|
}
|
|
2530
|
+
/** Close the list of density colours, with focus on its button. */
|
|
2531
|
+
closeDensityColours() {
|
|
2532
|
+
this.renderRoot.querySelector('[part="density-colours"]')?.focus();
|
|
2533
|
+
this.densityColoursOpen = false;
|
|
2534
|
+
}
|
|
2535
|
+
/** Escape in the list of density colours closes the list alone. */
|
|
2536
|
+
onDensityColoursKey = (e) => {
|
|
2537
|
+
if (e.key !== 'Escape' || e.defaultPrevented)
|
|
2538
|
+
return;
|
|
2539
|
+
e.stopPropagation();
|
|
2540
|
+
this.closeDensityColours();
|
|
2541
|
+
};
|
|
2542
|
+
/** Focus leaving the list of density colours and its button closes the list. */
|
|
2543
|
+
onDensityColoursFocusOut = (e) => {
|
|
2544
|
+
const to = e.relatedTarget;
|
|
2545
|
+
const held = [this.renderRoot.querySelector('#density-colour-list'), this.renderRoot.querySelector('[part="density-colours"]')];
|
|
2546
|
+
if (!held.some((el) => el && to && el.contains(to)))
|
|
2547
|
+
this.densityColoursOpen = false;
|
|
2548
|
+
};
|
|
2315
2549
|
/** Escape closes the Layers popover and returns focus to its button. */
|
|
2316
2550
|
onLayersKey = (e) => {
|
|
2317
|
-
if (e.key !== 'Escape' || !this.layersOpen)
|
|
2551
|
+
if (e.key !== 'Escape' || e.defaultPrevented || !this.layersOpen)
|
|
2318
2552
|
return;
|
|
2319
2553
|
e.stopPropagation();
|
|
2320
2554
|
this.closeLayers();
|
|
2321
2555
|
this.renderRoot.querySelector('[part="layers-toggle"]')?.focus();
|
|
2322
2556
|
};
|
|
2323
|
-
/** The
|
|
2557
|
+
/** The bar's keys: the arrows move between its buttons, wrapping, and Home and End go to the ends. */
|
|
2324
2558
|
onTabKey = (e) => {
|
|
2325
|
-
const buttons = Array.from(this.renderRoot.querySelectorAll('[part="tabs"]
|
|
2559
|
+
const buttons = Array.from(this.renderRoot.querySelectorAll('[part="tabs"] button'));
|
|
2326
2560
|
const at = buttons.indexOf(e.target);
|
|
2327
2561
|
if (at < 0)
|
|
2328
2562
|
return;
|
|
@@ -2337,24 +2571,56 @@ let MosaicaExplorer = (() => {
|
|
|
2337
2571
|
};
|
|
2338
2572
|
/** Escape closes the sheet. */
|
|
2339
2573
|
onSheetKey = (e) => {
|
|
2340
|
-
if (e.key !== 'Escape')
|
|
2574
|
+
if (e.key !== 'Escape' || e.defaultPrevented)
|
|
2341
2575
|
return;
|
|
2342
2576
|
e.stopPropagation();
|
|
2343
2577
|
this.sheet = null;
|
|
2344
2578
|
};
|
|
2345
|
-
/**
|
|
2579
|
+
/**
|
|
2580
|
+
* Focus goes to the first control of the Layers popover as it opens. A sheet that opens takes
|
|
2581
|
+
* the focus the explorer held, and none from the page around it. A sheet that closes gives the
|
|
2582
|
+
* focus it held to its button, or to the map where the layout has grown wide and has no button.
|
|
2583
|
+
*/
|
|
2346
2584
|
updated(changed) {
|
|
2347
2585
|
this.toggleAttribute('data-compact', this.compact);
|
|
2348
2586
|
this.fitLayers();
|
|
2349
2587
|
this.placeMenu(changed.has('menu') && this.menu !== null);
|
|
2350
2588
|
this.followLayout();
|
|
2589
|
+
this.coverMap();
|
|
2590
|
+
if (changed.has('layersOpen') && this.layersOpen)
|
|
2591
|
+
this.renderRoot.querySelector('[part="layers-popover"]')?.querySelector(FOCUSABLE)?.focus();
|
|
2351
2592
|
if (!changed.has('sheet'))
|
|
2352
2593
|
return;
|
|
2353
2594
|
const before = changed.get('sheet');
|
|
2354
|
-
if (this.sheet)
|
|
2355
|
-
this.
|
|
2356
|
-
|
|
2357
|
-
|
|
2595
|
+
if (this.sheet) {
|
|
2596
|
+
if (this.focusBefore)
|
|
2597
|
+
this.renderRoot.querySelector('[part="sheet"]')?.focus();
|
|
2598
|
+
}
|
|
2599
|
+
else if (before && this.focusBefore === 'sheet') {
|
|
2600
|
+
if (this.narrow)
|
|
2601
|
+
this.renderRoot.querySelector(`[part="tabs"] [data-sheet="${before}"]`)?.focus();
|
|
2602
|
+
else
|
|
2603
|
+
this.map?.focus();
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
/**
|
|
2607
|
+
* While an open sheet reaches up over the map's tools, the map behind it is inert: nothing under
|
|
2608
|
+
* the sheet takes focus or a press. A shorter sheet leaves the map above it in use.
|
|
2609
|
+
*/
|
|
2610
|
+
coverMap() {
|
|
2611
|
+
const m = this.map;
|
|
2612
|
+
if (!m)
|
|
2613
|
+
return;
|
|
2614
|
+
const sheet = this.renderRoot.querySelector('[part="sheet"]');
|
|
2615
|
+
// Nothing is measured where no sheet is open, which is every draw of the wide layouts.
|
|
2616
|
+
if (!sheet) {
|
|
2617
|
+
if (m.inert)
|
|
2618
|
+
m.inert = false;
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
const tools = m.shadowRoot?.querySelector('[part="controls"]');
|
|
2622
|
+
const clear = tools ? tools.getBoundingClientRect().bottom : m.getBoundingClientRect().top;
|
|
2623
|
+
m.inert = sheet.getBoundingClientRect().top < clear;
|
|
2358
2624
|
}
|
|
2359
2625
|
/**
|
|
2360
2626
|
* Hold the open Layers popover within the explorer's visible box: the part of it inside the
|