@aceshooting/lyra-ui 0.1.3 → 0.1.4
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 -8
- package/custom-elements.json +4222 -3430
- package/dist/components/chart/bar-chart.js +17 -4
- package/dist/components/chart/box-plot.d.ts +8 -1
- package/dist/components/chart/box-plot.js +40 -4
- package/dist/components/chart/box-plot.styles.js +4 -0
- package/dist/components/chart/bubble-chart.d.ts +1 -2
- package/dist/components/chart/bubble-chart.js +17 -4
- package/dist/components/chart/chart-loader.d.ts +21 -4
- package/dist/components/chart/chart-loader.js +39 -12
- package/dist/components/chart/chart.d.ts +9 -3
- package/dist/components/chart/chart.js +42 -21
- package/dist/components/chart/doughnut-chart.d.ts +1 -2
- package/dist/components/chart/doughnut-chart.js +17 -4
- package/dist/components/chart/histogram-bin.js +12 -2
- package/dist/components/chart/histogram.d.ts +2 -2
- package/dist/components/chart/histogram.js +16 -2
- package/dist/components/chart/line-chart.js +17 -4
- package/dist/components/chart/pie-chart.d.ts +1 -2
- package/dist/components/chart/pie-chart.js +17 -4
- package/dist/components/chart/polar-area-chart.d.ts +1 -2
- package/dist/components/chart/polar-area-chart.js +17 -4
- package/dist/components/chart/radar-chart.d.ts +1 -2
- package/dist/components/chart/radar-chart.js +17 -4
- package/dist/components/chart/scatter-chart.js +17 -4
- package/dist/components/combobox/combobox.js +3 -4
- package/dist/components/combobox/combobox.stories.d.ts +6 -0
- package/dist/components/combobox/combobox.stories.js +27 -0
- package/dist/components/combobox/combobox.styles.js +2 -2
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +12 -0
- package/dist/components/date-picker/date-input.styles.js +2 -2
- package/dist/components/date-picker/date-picker.stories.d.ts +6 -0
- package/dist/components/date-picker/date-picker.stories.js +13 -0
- package/dist/components/empty/empty.d.ts +1 -1
- package/dist/components/empty/empty.js +1 -1
- package/dist/components/export-button/export-button.d.ts +2 -2
- package/dist/components/export-button/export-button.js +2 -2
- package/dist/components/file-input/accept.d.ts +1 -2
- package/dist/components/file-input/accept.js +1 -2
- package/dist/components/file-input/file-input.d.ts +2 -2
- package/dist/components/file-input/file-input.js +2 -2
- package/dist/components/flag/flag.stories.d.ts +5 -0
- package/dist/components/flag/flag.stories.js +17 -0
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/gauge/gauge.d.ts +1 -1
- package/dist/components/gauge/gauge.js +1 -1
- package/dist/components/graph/graph.d.ts +33 -4
- package/dist/components/graph/graph.js +146 -30
- package/dist/components/heatmap/calendar-grid.d.ts +31 -0
- package/dist/components/heatmap/calendar-grid.js +52 -0
- package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
- package/dist/components/heatmap/heatmap-scale.js +21 -0
- package/dist/components/heatmap/heatmap.d.ts +19 -0
- package/dist/components/heatmap/heatmap.js +133 -20
- package/dist/components/heatmap/heatmap.styles.js +4 -0
- package/dist/components/map/map.d.ts +15 -0
- package/dist/components/map/map.js +89 -7
- package/dist/components/playback/playback.d.ts +2 -2
- package/dist/components/playback/playback.js +2 -2
- package/dist/components/skeleton/skeleton.d.ts +2 -2
- package/dist/components/skeleton/skeleton.js +2 -2
- package/dist/components/sparkline/sparkline.stories.d.ts +7 -0
- package/dist/components/sparkline/sparkline.stories.js +17 -0
- package/dist/components/stat/stat.d.ts +1 -1
- package/dist/components/stat/stat.js +6 -7
- package/dist/components/table/table.d.ts +1 -2
- package/dist/components/table/table.js +1 -2
- package/dist/components/table/table.styles.js +2 -3
- package/dist/components/time-range/time-range.js +4 -4
- package/dist/components/toast/toast.stories.d.ts +5 -0
- package/dist/components/toast/toast.stories.js +32 -0
- package/dist/components/tree/tree-node.d.ts +2 -2
- package/dist/components/tree/tree-node.js +2 -2
- package/dist/internal/form-associated.js +1 -2
- package/dist/internal/icons.js +1 -1
- package/dist/internal/tokens.styles.js +3 -3
- package/llms-full.txt +59 -83
- package/llms.txt +1 -2
- package/package.json +4 -1
|
@@ -8,14 +8,19 @@ import { html } from 'lit';
|
|
|
8
8
|
import { property, query } from 'lit/decorators.js';
|
|
9
9
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
10
|
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
-
import { linearAlpha, sqrtStep } from './heatmap-scale.js';
|
|
11
|
+
import { linearAlpha, minMax, sqrtStep } from './heatmap-scale.js';
|
|
12
12
|
import { styles } from './heatmap.styles.js';
|
|
13
|
+
import { buildCalendarGrid, quartileBucket } from './calendar-grid.js';
|
|
13
14
|
const PAD_LEFT = 60;
|
|
14
15
|
const PAD_TOP = 20;
|
|
15
|
-
const
|
|
16
|
+
const FALLBACK_NO_DATA_FILL = 'rgba(128,128,128,0.25)';
|
|
16
17
|
const RAMP_STEPS = 7;
|
|
17
18
|
const FALLBACK_SCALE_LO = '#cde2fb';
|
|
18
19
|
const FALLBACK_SCALE_HI = '#0969da';
|
|
20
|
+
const CAL_PAD_LEFT = 28;
|
|
21
|
+
const CAL_LABEL_H = 16;
|
|
22
|
+
const CAL_CELL = 11;
|
|
23
|
+
const CAL_GAP = 2;
|
|
19
24
|
const HEX_RE = /^([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
20
25
|
const RGB_RE = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i;
|
|
21
26
|
/**
|
|
@@ -117,6 +122,18 @@ export class LyraHeatmap extends LyraElement {
|
|
|
117
122
|
this.cellSize = 22;
|
|
118
123
|
this.valueLabel = 'value';
|
|
119
124
|
this.scale = 'linear';
|
|
125
|
+
/**
|
|
126
|
+
* When set, `cellSize` is derived from the host's measured `clientWidth`
|
|
127
|
+
* on every draw (including ResizeObserver-triggered redraws) instead of
|
|
128
|
+
* the fixed `cell-size` attribute, so the grid actually fills the
|
|
129
|
+
* available width. Without this, canvas dimensions are computed purely
|
|
130
|
+
* from `PAD_LEFT + cols * cellSize`, so a resize-triggered redraw is a
|
|
131
|
+
* geometric no-op.
|
|
132
|
+
*/
|
|
133
|
+
this.fitToWidth = false;
|
|
134
|
+
this.mode = 'matrix';
|
|
135
|
+
this.days = [];
|
|
136
|
+
this.bucketCount = 5;
|
|
120
137
|
this.onDprChange = () => {
|
|
121
138
|
this.watchDpr();
|
|
122
139
|
this.draw();
|
|
@@ -135,16 +152,31 @@ export class LyraHeatmap extends LyraElement {
|
|
|
135
152
|
this.dprQuery?.removeEventListener('change', this.onDprChange);
|
|
136
153
|
}
|
|
137
154
|
watchDpr() {
|
|
155
|
+
// A MediaQueryList's `matches` is fixed at creation time, so crossing the
|
|
156
|
+
// DPR threshold it was built for means building a fresh one for the new
|
|
157
|
+
// ratio — remove the previous instance's listener first, or it leaks
|
|
158
|
+
// (disconnectedCallback only ever cleans up whichever is current).
|
|
159
|
+
this.dprQuery?.removeEventListener('change', this.onDprChange);
|
|
138
160
|
this.dprQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
139
161
|
this.dprQuery.addEventListener('change', this.onDprChange);
|
|
140
162
|
}
|
|
141
163
|
willUpdate() {
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const flat = this.values.flat().filter((v) => v >= 0);
|
|
145
|
-
const range = flat.length ? `${Math.min(...flat)}–${Math.max(...flat)}` : 'no data';
|
|
164
|
+
const bounds = this.valueRange();
|
|
165
|
+
const range = bounds ? `${bounds[0]}–${bounds[1]}` : 'no data';
|
|
146
166
|
this.setAttribute('role', 'img');
|
|
147
|
-
this.
|
|
167
|
+
if (this.mode === 'calendar') {
|
|
168
|
+
this.setAttribute('aria-label', `Calendar heatmap of ${this.days.length} days, ${this.valueLabel} range ${range}`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const rows = this.rowLabels.length;
|
|
172
|
+
const cols = this.colLabels.length;
|
|
173
|
+
this.setAttribute('aria-label', `Heatmap of ${rows} × ${cols} cells, ${this.valueLabel} range ${range}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** The real (non-no-data) value range across `values` (or `days` in calendar mode), or `null` if there is none. Shared by `willUpdate()`, `draw()`, and the legend. */
|
|
177
|
+
valueRange() {
|
|
178
|
+
const source = this.mode === 'calendar' ? this.days.map((d) => d.value) : this.values.flat();
|
|
179
|
+
return minMax(source.filter((v) => Number.isFinite(v) && v >= 0));
|
|
148
180
|
}
|
|
149
181
|
updated() {
|
|
150
182
|
this.draw();
|
|
@@ -160,13 +192,74 @@ export class LyraHeatmap extends LyraElement {
|
|
|
160
192
|
labelColor() {
|
|
161
193
|
return getComputedStyle(this).getPropertyValue('--lyra-color-text-quiet').trim() || '#6b7280';
|
|
162
194
|
}
|
|
195
|
+
/** Reads the customizable no-data cell fill off the host's computed style. */
|
|
196
|
+
noDataFill() {
|
|
197
|
+
return getComputedStyle(this).getPropertyValue('--lyra-heatmap-no-data-fill').trim() || FALLBACK_NO_DATA_FILL;
|
|
198
|
+
}
|
|
163
199
|
draw() {
|
|
200
|
+
if (this.mode === 'calendar')
|
|
201
|
+
this.drawCalendar();
|
|
202
|
+
else
|
|
203
|
+
this.drawMatrix();
|
|
204
|
+
}
|
|
205
|
+
drawCalendar() {
|
|
206
|
+
if (!this.canvas)
|
|
207
|
+
return;
|
|
208
|
+
const { cells, weekCount, monthLabels } = buildCalendarGrid(this.days);
|
|
209
|
+
const w = CAL_PAD_LEFT + Math.max(1, weekCount) * (CAL_CELL + CAL_GAP);
|
|
210
|
+
const h = CAL_LABEL_H + 7 * (CAL_CELL + CAL_GAP);
|
|
211
|
+
const dpr = window.devicePixelRatio || 1;
|
|
212
|
+
this.canvas.width = w * dpr;
|
|
213
|
+
this.canvas.height = h * dpr;
|
|
214
|
+
this.canvas.style.width = `${w}px`;
|
|
215
|
+
this.canvas.style.height = `${h}px`;
|
|
216
|
+
const ctx = this.canvas.getContext('2d');
|
|
217
|
+
if (!ctx)
|
|
218
|
+
return;
|
|
219
|
+
ctx.scale(dpr, dpr);
|
|
220
|
+
ctx.clearRect(0, 0, w, h);
|
|
221
|
+
const sortedValues = cells
|
|
222
|
+
.map((c) => c.value)
|
|
223
|
+
.filter((v) => Number.isFinite(v) && v >= 0)
|
|
224
|
+
.sort((a, b) => a - b);
|
|
225
|
+
const [scaleLo, scaleHi] = this.scaleEndpoints();
|
|
226
|
+
const loRgb = resolveRgb(scaleLo, FALLBACK_SCALE_LO);
|
|
227
|
+
const hiRgb = resolveRgb(scaleHi, FALLBACK_SCALE_HI);
|
|
228
|
+
const buckets = Math.max(2, this.bucketCount);
|
|
229
|
+
const ramp = Array.from({ length: buckets }, (_, i) => mixRgb(loRgb, hiRgb, i / (buckets - 1)));
|
|
230
|
+
const noDataFill = this.noDataFill();
|
|
231
|
+
for (const cell of cells) {
|
|
232
|
+
const x = CAL_PAD_LEFT + cell.week * (CAL_CELL + CAL_GAP);
|
|
233
|
+
const y = CAL_LABEL_H + cell.weekday * (CAL_CELL + CAL_GAP);
|
|
234
|
+
ctx.fillStyle =
|
|
235
|
+
cell.value < 0 || !Number.isFinite(cell.value)
|
|
236
|
+
? noDataFill
|
|
237
|
+
: ramp[quartileBucket(cell.value, sortedValues, buckets)];
|
|
238
|
+
ctx.fillRect(x, y, CAL_CELL, CAL_CELL);
|
|
239
|
+
}
|
|
240
|
+
ctx.fillStyle = this.labelColor();
|
|
241
|
+
ctx.font = '10px sans-serif';
|
|
242
|
+
for (const m of monthLabels) {
|
|
243
|
+
ctx.fillText(m.label, CAL_PAD_LEFT + m.week * (CAL_CELL + CAL_GAP), CAL_LABEL_H - 4);
|
|
244
|
+
}
|
|
245
|
+
const WEEKDAY_LABELS = ['', 'Mon', '', 'Wed', '', 'Fri', ''];
|
|
246
|
+
WEEKDAY_LABELS.forEach((label, weekday) => {
|
|
247
|
+
if (label)
|
|
248
|
+
ctx.fillText(label, 2, CAL_LABEL_H + weekday * (CAL_CELL + CAL_GAP) + CAL_CELL - 1);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
drawMatrix() {
|
|
164
252
|
if (!this.canvas)
|
|
165
253
|
return;
|
|
166
254
|
const rows = this.rowLabels.length;
|
|
167
255
|
const cols = this.colLabels.length;
|
|
168
|
-
|
|
169
|
-
|
|
256
|
+
let cellSize = this.cellSize;
|
|
257
|
+
if (this.fitToWidth && cols > 0) {
|
|
258
|
+
const hostWidth = this.clientWidth || PAD_LEFT + cols * this.cellSize;
|
|
259
|
+
cellSize = Math.max(4, (hostWidth - PAD_LEFT) / cols);
|
|
260
|
+
}
|
|
261
|
+
const w = PAD_LEFT + cols * cellSize;
|
|
262
|
+
const h = PAD_TOP + rows * cellSize;
|
|
170
263
|
const dpr = window.devicePixelRatio || 1;
|
|
171
264
|
this.canvas.width = w * dpr;
|
|
172
265
|
this.canvas.height = h * dpr;
|
|
@@ -177,24 +270,29 @@ export class LyraHeatmap extends LyraElement {
|
|
|
177
270
|
return;
|
|
178
271
|
ctx.scale(dpr, dpr);
|
|
179
272
|
ctx.clearRect(0, 0, w, h);
|
|
180
|
-
const
|
|
181
|
-
const lo =
|
|
182
|
-
const hi =
|
|
273
|
+
const bounds = this.valueRange();
|
|
274
|
+
const lo = bounds ? bounds[0] : 0;
|
|
275
|
+
const hi = bounds ? bounds[1] : 1;
|
|
183
276
|
const [scaleLo, scaleHi] = this.scaleEndpoints();
|
|
184
277
|
const loRgb = resolveRgb(scaleLo, FALLBACK_SCALE_LO);
|
|
185
278
|
const hiRgb = resolveRgb(scaleHi, FALLBACK_SCALE_HI);
|
|
186
279
|
const ramp = Array.from({ length: RAMP_STEPS }, (_, i) => mixRgb(loRgb, hiRgb, i / (RAMP_STEPS - 1)));
|
|
280
|
+
const noDataFill = this.noDataFill();
|
|
187
281
|
for (let r = 0; r < rows; r++) {
|
|
188
282
|
for (let c = 0; c < cols; c++) {
|
|
189
283
|
const v = this.values[r]?.[c] ?? -1;
|
|
190
|
-
const x = PAD_LEFT + c *
|
|
191
|
-
const y = PAD_TOP + r *
|
|
192
|
-
if (v < 0) {
|
|
193
|
-
|
|
284
|
+
const x = PAD_LEFT + c * cellSize;
|
|
285
|
+
const y = PAD_TOP + r * cellSize;
|
|
286
|
+
if (v < 0 || !Number.isFinite(v)) {
|
|
287
|
+
// `v < 0` alone is false for NaN, which would otherwise fall through to
|
|
288
|
+
// the ramp branches below, compute an unparsable rgb(NaN, NaN, NaN)
|
|
289
|
+
// fillStyle, and silently paint with whatever color the previous cell
|
|
290
|
+
// left in `ctx.fillStyle` (canvas ignores unparsable assignments).
|
|
291
|
+
ctx.fillStyle = noDataFill;
|
|
194
292
|
}
|
|
195
293
|
else if (this.scale === 'sqrt') {
|
|
196
294
|
const step = sqrtStep(v, hi, RAMP_STEPS);
|
|
197
|
-
ctx.fillStyle = step < 0 ?
|
|
295
|
+
ctx.fillStyle = step < 0 ? noDataFill : ramp[step];
|
|
198
296
|
}
|
|
199
297
|
else {
|
|
200
298
|
// linearAlpha() returns a 0.1-1.0 ramp position; reused here as a
|
|
@@ -204,24 +302,27 @@ export class LyraHeatmap extends LyraElement {
|
|
|
204
302
|
const t = linearAlpha(v, lo, hi);
|
|
205
303
|
ctx.fillStyle = mixRgb(loRgb, hiRgb, t);
|
|
206
304
|
}
|
|
207
|
-
ctx.fillRect(x, y,
|
|
305
|
+
ctx.fillRect(x, y, cellSize - 1, cellSize - 1);
|
|
208
306
|
}
|
|
209
307
|
}
|
|
210
308
|
ctx.fillStyle = this.labelColor();
|
|
211
309
|
ctx.font = '10px sans-serif';
|
|
212
310
|
this.rowLabels.forEach((label, r) => {
|
|
213
|
-
ctx.fillText(label, 4, PAD_TOP + r *
|
|
311
|
+
ctx.fillText(label, 4, PAD_TOP + r * cellSize + cellSize / 2 + 3);
|
|
214
312
|
});
|
|
215
313
|
this.colLabels.forEach((label, c) => {
|
|
216
|
-
ctx.fillText(label, PAD_LEFT + c *
|
|
314
|
+
ctx.fillText(label, PAD_LEFT + c * cellSize + 2, PAD_TOP - 6);
|
|
217
315
|
});
|
|
218
316
|
}
|
|
219
317
|
render() {
|
|
318
|
+
const range = this.valueRange();
|
|
220
319
|
return html `
|
|
221
320
|
<div part="base">
|
|
222
321
|
<canvas part="canvas"></canvas>
|
|
223
322
|
<div part="legend">
|
|
323
|
+
<span part="legend-lo">${range ? range[0] : ''}</span>
|
|
224
324
|
<span class="bar"></span>
|
|
325
|
+
<span part="legend-hi">${range ? range[1] : ''}</span>
|
|
225
326
|
<span>${this.valueLabel}</span>
|
|
226
327
|
</div>
|
|
227
328
|
</div>
|
|
@@ -246,6 +347,18 @@ __decorate([
|
|
|
246
347
|
__decorate([
|
|
247
348
|
property()
|
|
248
349
|
], LyraHeatmap.prototype, "scale", void 0);
|
|
350
|
+
__decorate([
|
|
351
|
+
property({ type: Boolean, attribute: 'fit-to-width' })
|
|
352
|
+
], LyraHeatmap.prototype, "fitToWidth", void 0);
|
|
353
|
+
__decorate([
|
|
354
|
+
property()
|
|
355
|
+
], LyraHeatmap.prototype, "mode", void 0);
|
|
356
|
+
__decorate([
|
|
357
|
+
property({ attribute: false })
|
|
358
|
+
], LyraHeatmap.prototype, "days", void 0);
|
|
359
|
+
__decorate([
|
|
360
|
+
property({ attribute: 'bucket-count', type: Number })
|
|
361
|
+
], LyraHeatmap.prototype, "bucketCount", void 0);
|
|
249
362
|
__decorate([
|
|
250
363
|
query('canvas')
|
|
251
364
|
], LyraHeatmap.prototype, "canvas", void 0);
|
|
@@ -9,6 +9,10 @@ export const styles = css `
|
|
|
9
9
|
the ramp without raw hex leaking into the public API. */
|
|
10
10
|
--lyra-heatmap-scale-lo: #cde2fb;
|
|
11
11
|
--lyra-heatmap-scale-hi: #0969da;
|
|
12
|
+
/* No-data cell fill (the -1 sentinel / NaN case) — same resolve-via-
|
|
13
|
+
getComputedStyle pattern as the ramp endpoints above, so hosts can
|
|
14
|
+
retheme it instead of it being a hardcoded literal in heatmap.ts. */
|
|
15
|
+
--lyra-heatmap-no-data-fill: rgba(128, 128, 128, 0.25);
|
|
12
16
|
}
|
|
13
17
|
[part='base'] {
|
|
14
18
|
display: flex;
|
|
@@ -12,6 +12,13 @@ export interface ChoroplethLayer {
|
|
|
12
12
|
field: string;
|
|
13
13
|
stops: [number, string][];
|
|
14
14
|
}
|
|
15
|
+
export interface MapMarker {
|
|
16
|
+
id?: string;
|
|
17
|
+
lngLat: [number, number];
|
|
18
|
+
color?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
html?: string;
|
|
21
|
+
}
|
|
15
22
|
/**
|
|
16
23
|
* `<lyra-map>` — a maplibre-gl wrapper with a declarative legend and
|
|
17
24
|
* choropleth GeoJSON layer, plus a raw `map` escape hatch. Requires the
|
|
@@ -29,17 +36,25 @@ export declare class LyraMap extends LyraElement {
|
|
|
29
36
|
mapStyle: StyleSpecification | string;
|
|
30
37
|
legend: LegendEntry[];
|
|
31
38
|
choropleth?: ChoroplethLayer;
|
|
39
|
+
markers: MapMarker[];
|
|
32
40
|
/** True until the lazy-loaded `maplibre-gl` peer dependency has settled (success or failure). */
|
|
33
41
|
private loading;
|
|
34
42
|
private containerEl?;
|
|
35
43
|
private _map?;
|
|
36
44
|
private _styleLoaded;
|
|
45
|
+
private _appliedChoroplethSourceId?;
|
|
46
|
+
private _appliedFillLayerId?;
|
|
47
|
+
private _maplibreModule;
|
|
48
|
+
private _markerInstances;
|
|
37
49
|
/** The raw `maplibregl.Map` instance — escape hatch for anything this wrapper doesn't expose. */
|
|
38
50
|
get map(): MaplibreMap | undefined;
|
|
39
51
|
connectedCallback(): void;
|
|
40
52
|
disconnectedCallback(): void;
|
|
41
53
|
protected updated(changed: PropertyValues): void;
|
|
42
54
|
private applyChoropleth;
|
|
55
|
+
/** Removes whatever choropleth layer/source is currently applied, if any. */
|
|
56
|
+
private removeChoropleth;
|
|
57
|
+
private applyMarkers;
|
|
43
58
|
render(): TemplateResult;
|
|
44
59
|
}
|
|
45
60
|
declare global {
|
|
@@ -41,6 +41,7 @@ export class LyraMap extends LyraElement {
|
|
|
41
41
|
this.zoom = 2;
|
|
42
42
|
this.mapStyle = DEFAULT_STYLE;
|
|
43
43
|
this.legend = [];
|
|
44
|
+
this.markers = [];
|
|
44
45
|
/** True until the lazy-loaded `maplibre-gl` peer dependency has settled (success or failure). */
|
|
45
46
|
this.loading = true;
|
|
46
47
|
// Tracks whether the style has fired its initial 'load' (i.e. addSource/
|
|
@@ -50,6 +51,7 @@ export class LyraMap extends LyraElement {
|
|
|
50
51
|
// own GeoJSON source is added — which would wrongly block subsequent
|
|
51
52
|
// choropleth updates from ever re-running.
|
|
52
53
|
this._styleLoaded = false;
|
|
54
|
+
this._markerInstances = new Map();
|
|
53
55
|
}
|
|
54
56
|
static { this.styles = [LyraElement.styles, styles]; }
|
|
55
57
|
/** The raw `maplibregl.Map` instance — escape hatch for anything this wrapper doesn't expose. */
|
|
@@ -62,6 +64,7 @@ export class LyraMap extends LyraElement {
|
|
|
62
64
|
this.loading = false;
|
|
63
65
|
if (!mod)
|
|
64
66
|
return;
|
|
67
|
+
this._maplibreModule = mod;
|
|
65
68
|
// `[part="container"]` only exists once `loading` flips to `false` and
|
|
66
69
|
// Lit re-renders — wait for that render to land before querying it.
|
|
67
70
|
await this.updateComplete;
|
|
@@ -80,14 +83,11 @@ export class LyraMap extends LyraElement {
|
|
|
80
83
|
this._map.on('load', () => {
|
|
81
84
|
this._styleLoaded = true;
|
|
82
85
|
this.applyChoropleth();
|
|
86
|
+
this.applyMarkers();
|
|
83
87
|
this.emit('lyra-map-load');
|
|
84
88
|
});
|
|
85
89
|
this._map.on('click', (e) => {
|
|
86
|
-
|
|
87
|
-
// set: applyChoropleth() only adds it once the style has finished loading, so
|
|
88
|
-
// a click landing in that window (choropleth set before the map's own 'load')
|
|
89
|
-
// would otherwise query a layer id maplibre-gl doesn't know about yet.
|
|
90
|
-
const fillLayerId = this.choropleth ? `${this.choropleth.sourceId}-fill` : undefined;
|
|
90
|
+
const fillLayerId = this._appliedFillLayerId;
|
|
91
91
|
const features = fillLayerId && this._map.getLayer(fillLayerId)
|
|
92
92
|
? this._map.queryRenderedFeatures(e.point, { layers: [fillLayerId] })
|
|
93
93
|
: [];
|
|
@@ -103,24 +103,53 @@ export class LyraMap extends LyraElement {
|
|
|
103
103
|
this._map?.remove();
|
|
104
104
|
this._map = undefined;
|
|
105
105
|
this._styleLoaded = false;
|
|
106
|
+
this._appliedChoroplethSourceId = undefined;
|
|
107
|
+
this._appliedFillLayerId = undefined;
|
|
108
|
+
for (const marker of this._markerInstances.values())
|
|
109
|
+
marker.remove();
|
|
110
|
+
this._markerInstances.clear();
|
|
106
111
|
}
|
|
107
112
|
updated(changed) {
|
|
108
113
|
if (this.loading)
|
|
109
114
|
this.setAttribute('aria-busy', 'true');
|
|
110
115
|
else
|
|
111
116
|
this.removeAttribute('aria-busy');
|
|
112
|
-
if (changed.has('
|
|
117
|
+
if (changed.has('mapStyle') && this._map) {
|
|
118
|
+
// A style change wipes every layer/source maplibre-gl knows about, so
|
|
119
|
+
// the previously-applied choropleth bookkeeping is stale the instant
|
|
120
|
+
// setStyle() is called — clear it and re-apply once the new style's
|
|
121
|
+
// own 'style.load' fires (mirrors the constructor's 'load' handshake).
|
|
122
|
+
this._styleLoaded = false;
|
|
123
|
+
this._appliedChoroplethSourceId = undefined;
|
|
124
|
+
this._appliedFillLayerId = undefined;
|
|
125
|
+
this._map.setStyle(this.mapStyle);
|
|
126
|
+
this._map.once('style.load', () => {
|
|
127
|
+
this._styleLoaded = true;
|
|
128
|
+
this.applyChoropleth();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else if (changed.has('choropleth') && this._styleLoaded) {
|
|
113
132
|
this.applyChoropleth();
|
|
133
|
+
}
|
|
114
134
|
if (changed.has('center') && this._map)
|
|
115
135
|
this._map.setCenter(this.center);
|
|
116
136
|
if (changed.has('zoom') && this._map)
|
|
117
137
|
this._map.setZoom(this.zoom);
|
|
138
|
+
if (changed.has('markers') && this._map)
|
|
139
|
+
this.applyMarkers();
|
|
118
140
|
}
|
|
119
141
|
applyChoropleth() {
|
|
120
|
-
if (!this.
|
|
142
|
+
if (!this._map)
|
|
121
143
|
return;
|
|
144
|
+
if (!this.choropleth) {
|
|
145
|
+
this.removeChoropleth();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
122
148
|
const { sourceId, geojson, field, stops } = this.choropleth;
|
|
123
149
|
const fillLayerId = `${sourceId}-fill`;
|
|
150
|
+
if (this._appliedChoroplethSourceId && this._appliedChoroplethSourceId !== sourceId) {
|
|
151
|
+
this.removeChoropleth();
|
|
152
|
+
}
|
|
124
153
|
const colorExpr = ['interpolate', ['linear'], ['get', field]];
|
|
125
154
|
for (const [value, color] of stops)
|
|
126
155
|
colorExpr.push(value, color);
|
|
@@ -141,6 +170,56 @@ export class LyraMap extends LyraElement {
|
|
|
141
170
|
paint: { 'fill-color': colorExpr, 'fill-opacity': 0.75 },
|
|
142
171
|
});
|
|
143
172
|
}
|
|
173
|
+
this._appliedChoroplethSourceId = sourceId;
|
|
174
|
+
this._appliedFillLayerId = fillLayerId;
|
|
175
|
+
}
|
|
176
|
+
/** Removes whatever choropleth layer/source is currently applied, if any. */
|
|
177
|
+
removeChoropleth() {
|
|
178
|
+
if (!this._map || !this._appliedChoroplethSourceId)
|
|
179
|
+
return;
|
|
180
|
+
if (this._appliedFillLayerId && this._map.getLayer(this._appliedFillLayerId)) {
|
|
181
|
+
this._map.removeLayer(this._appliedFillLayerId);
|
|
182
|
+
}
|
|
183
|
+
if (this._map.getSource(this._appliedChoroplethSourceId)) {
|
|
184
|
+
this._map.removeSource(this._appliedChoroplethSourceId);
|
|
185
|
+
}
|
|
186
|
+
this._appliedChoroplethSourceId = undefined;
|
|
187
|
+
this._appliedFillLayerId = undefined;
|
|
188
|
+
}
|
|
189
|
+
async applyMarkers() {
|
|
190
|
+
if (!this._map)
|
|
191
|
+
return;
|
|
192
|
+
const mod = await loadMaplibre();
|
|
193
|
+
if (!mod)
|
|
194
|
+
return;
|
|
195
|
+
const visible = new Set();
|
|
196
|
+
for (const m of this.markers) {
|
|
197
|
+
const key = m.id ?? `${m.lngLat[0]},${m.lngLat[1]}`;
|
|
198
|
+
visible.add(key);
|
|
199
|
+
const existing = this._markerInstances.get(key);
|
|
200
|
+
if (!existing) {
|
|
201
|
+
const marker = new mod.Marker(m.color ? { color: m.color } : undefined).setLngLat(m.lngLat);
|
|
202
|
+
if (m.html || m.label) {
|
|
203
|
+
const popup = new mod.Popup({ offset: 12 });
|
|
204
|
+
if (m.html)
|
|
205
|
+
popup.setHTML(m.html);
|
|
206
|
+
else if (m.label)
|
|
207
|
+
popup.setText(m.label);
|
|
208
|
+
marker.setPopup(popup);
|
|
209
|
+
}
|
|
210
|
+
marker.addTo(this._map);
|
|
211
|
+
this._markerInstances.set(key, marker);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
existing.setLngLat(m.lngLat);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const [key, marker] of this._markerInstances) {
|
|
218
|
+
if (!visible.has(key)) {
|
|
219
|
+
marker.remove();
|
|
220
|
+
this._markerInstances.delete(key);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
144
223
|
}
|
|
145
224
|
render() {
|
|
146
225
|
// Lit's own whitespace/comment marker nodes around the `${...}` binding mean
|
|
@@ -180,6 +259,9 @@ __decorate([
|
|
|
180
259
|
__decorate([
|
|
181
260
|
property({ attribute: false })
|
|
182
261
|
], LyraMap.prototype, "choropleth", void 0);
|
|
262
|
+
__decorate([
|
|
263
|
+
property({ attribute: false })
|
|
264
|
+
], LyraMap.prototype, "markers", void 0);
|
|
183
265
|
__decorate([
|
|
184
266
|
state()
|
|
185
267
|
], LyraMap.prototype, "loading", void 0);
|
|
@@ -2,8 +2,8 @@ import { type TemplateResult, type PropertyValues } from 'lit';
|
|
|
2
2
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
3
|
/**
|
|
4
4
|
* `<lyra-playback>` — steps an index through `[0, length)` on a fixed
|
|
5
|
-
* interval (play/pause),
|
|
6
|
-
*
|
|
5
|
+
* interval (play/pause), the common building block behind ad-hoc
|
|
6
|
+
* play-timers in time-series dashboards.
|
|
7
7
|
*
|
|
8
8
|
* @customElement lyra-playback
|
|
9
9
|
* @event lyra-play
|
|
@@ -12,8 +12,8 @@ import { playIcon, pauseIcon } from '../../internal/icons.js';
|
|
|
12
12
|
import { styles } from './playback.styles.js';
|
|
13
13
|
/**
|
|
14
14
|
* `<lyra-playback>` — steps an index through `[0, length)` on a fixed
|
|
15
|
-
* interval (play/pause),
|
|
16
|
-
*
|
|
15
|
+
* interval (play/pause), the common building block behind ad-hoc
|
|
16
|
+
* play-timers in time-series dashboards.
|
|
17
17
|
*
|
|
18
18
|
* @customElement lyra-playback
|
|
19
19
|
* @event lyra-play
|
|
@@ -3,8 +3,8 @@ import { LyraElement } from '../../internal/lyra-element.js';
|
|
|
3
3
|
export type SkeletonVariant = 'text' | 'circle' | 'rect';
|
|
4
4
|
export type SkeletonEffect = 'pulse' | 'sheen';
|
|
5
5
|
/**
|
|
6
|
-
* `<lyra-skeleton>` — a loading placeholder. First-party invention
|
|
7
|
-
*
|
|
6
|
+
* `<lyra-skeleton>` — a loading placeholder. First-party invention, standing
|
|
7
|
+
* in for the bespoke `animate-pulse` div most dashboards hand-roll.
|
|
8
8
|
*
|
|
9
9
|
* @customElement lyra-skeleton
|
|
10
10
|
* @csspart base - The placeholder shape.
|
|
@@ -11,8 +11,8 @@ import { defineElement } from '../../internal/prefix.js';
|
|
|
11
11
|
import { srOnly } from '../../internal/a11y.js';
|
|
12
12
|
import { styles } from './skeleton.styles.js';
|
|
13
13
|
/**
|
|
14
|
-
* `<lyra-skeleton>` — a loading placeholder. First-party invention
|
|
15
|
-
*
|
|
14
|
+
* `<lyra-skeleton>` — a loading placeholder. First-party invention, standing
|
|
15
|
+
* in for the bespoke `animate-pulse` div most dashboards hand-roll.
|
|
16
16
|
*
|
|
17
17
|
* @customElement lyra-skeleton
|
|
18
18
|
* @csspart base - The placeholder shape.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
const DATA = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
|
|
3
|
+
const meta = {
|
|
4
|
+
title: 'Sparkline',
|
|
5
|
+
component: 'lyra-sparkline',
|
|
6
|
+
tags: ['autodocs'],
|
|
7
|
+
};
|
|
8
|
+
export default meta;
|
|
9
|
+
export const Line = {
|
|
10
|
+
render: () => html `<lyra-sparkline type="line" .values=${DATA}></lyra-sparkline>`,
|
|
11
|
+
};
|
|
12
|
+
export const Area = {
|
|
13
|
+
render: () => html `<lyra-sparkline type="area" .values=${DATA}></lyra-sparkline>`,
|
|
14
|
+
};
|
|
15
|
+
export const Bar = {
|
|
16
|
+
render: () => html `<lyra-sparkline type="bar" .values=${DATA}></lyra-sparkline>`,
|
|
17
|
+
};
|
|
@@ -4,7 +4,7 @@ export type StatVariant = 'neutral' | 'success' | 'warning' | 'danger';
|
|
|
4
4
|
export type StatGoodDirection = 'up' | 'down';
|
|
5
5
|
/**
|
|
6
6
|
* `<lyra-stat>` — a KPI/stat card. First-party invention consolidating the
|
|
7
|
-
* "metric row" / "KPI card" pattern
|
|
7
|
+
* "metric row" / "KPI card" pattern common to dashboard UIs.
|
|
8
8
|
*
|
|
9
9
|
* @customElement lyra-stat
|
|
10
10
|
* @slot - Leading icon.
|
|
@@ -12,7 +12,7 @@ import { chevronIcon } from '../../internal/icons.js';
|
|
|
12
12
|
import { styles } from './stat.styles.js';
|
|
13
13
|
/**
|
|
14
14
|
* `<lyra-stat>` — a KPI/stat card. First-party invention consolidating the
|
|
15
|
-
* "metric row" / "KPI card" pattern
|
|
15
|
+
* "metric row" / "KPI card" pattern common to dashboard UIs.
|
|
16
16
|
*
|
|
17
17
|
* @customElement lyra-stat
|
|
18
18
|
* @slot - Leading icon.
|
|
@@ -33,8 +33,7 @@ export class LyraStat extends LyraElement {
|
|
|
33
33
|
this.goodDirection = 'up';
|
|
34
34
|
// Same fix `lyra-empty` already established: `[part]:empty` never matches
|
|
35
35
|
// because the part always contains a literal `<slot>` child. Track real
|
|
36
|
-
// slot assignment in JS instead
|
|
37
|
-
// §lyra-stat, High).
|
|
36
|
+
// slot assignment in JS instead.
|
|
38
37
|
this.hasIcon = false;
|
|
39
38
|
this.hasCaptionSlot = false;
|
|
40
39
|
this.onIconSlotChange = (e) => {
|
|
@@ -55,10 +54,10 @@ export class LyraStat extends LyraElement {
|
|
|
55
54
|
const hasTrend = !isNaN(this.trend);
|
|
56
55
|
const rawDirection = this.trend > 0 ? 'up' : this.trend < 0 ? 'down' : 'flat';
|
|
57
56
|
const isGood = rawDirection === 'flat' ? null : rawDirection === this.goodDirection;
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
57
|
+
// The trend pill previously rendered literal ▲/▼ glyphs — font-dependent
|
|
58
|
+
// and inconsistent with the rest of the icon set — swapped for the
|
|
59
|
+
// shared chevronIcon(), rotated per direction via CSS on the wrapping
|
|
60
|
+
// [part='trend'].
|
|
62
61
|
const arrow = rawDirection === 'flat' ? '–' : chevronIcon();
|
|
63
62
|
const hasCaption = this.hasCaptionSlot || this.caption.length > 0;
|
|
64
63
|
return html `
|
|
@@ -14,8 +14,7 @@ export interface TableColumn<T> {
|
|
|
14
14
|
* Header/row activation is delegated: one `click` and one `keydown`
|
|
15
15
|
* listener on `<table>` resolve the target via `closest('[data-col-key]'
|
|
16
16
|
* | '[data-row-key]')` and a key→object lookup map, instead of allocating
|
|
17
|
-
* fresh per-column/per-row closures on every render
|
|
18
|
-
* "dashboard-atoms" §lyra-table, Medium).
|
|
17
|
+
* fresh per-column/per-row closures on every render.
|
|
19
18
|
*
|
|
20
19
|
* @customElement lyra-table
|
|
21
20
|
* @event lyra-sort - A sortable header was activated. `detail: { key }`.
|
|
@@ -18,8 +18,7 @@ import '../empty/empty.js';
|
|
|
18
18
|
* Header/row activation is delegated: one `click` and one `keydown`
|
|
19
19
|
* listener on `<table>` resolve the target via `closest('[data-col-key]'
|
|
20
20
|
* | '[data-row-key]')` and a key→object lookup map, instead of allocating
|
|
21
|
-
* fresh per-column/per-row closures on every render
|
|
22
|
-
* "dashboard-atoms" §lyra-table, Medium).
|
|
21
|
+
* fresh per-column/per-row closures on every render.
|
|
23
22
|
*
|
|
24
23
|
* @customElement lyra-table
|
|
25
24
|
* @event lyra-sort - A sortable header was activated. `detail: { key }`.
|
|
@@ -51,9 +51,8 @@ export const styles = css `
|
|
|
51
51
|
}
|
|
52
52
|
/* Rotate the wrapping part element, not the svg — internal/icons.ts's
|
|
53
53
|
documented contract ("callers ... rotate the wrapping part element via
|
|
54
|
-
CSS transform: rotate(...), not the svg").
|
|
55
|
-
|
|
56
|
-
<svg> directly. */
|
|
54
|
+
CSS transform: rotate(...), not the svg"). This previously rotated the
|
|
55
|
+
inner <svg> directly. */
|
|
57
56
|
[part='sort-icon'][data-dir='asc'] {
|
|
58
57
|
transform: rotate(-90deg);
|
|
59
58
|
}
|
|
@@ -147,10 +147,10 @@ export class LyraTimeRange extends LyraElement {
|
|
|
147
147
|
this.end = Math.min(hi, Math.max(lo, this.end));
|
|
148
148
|
}
|
|
149
149
|
// Keep start <= end regardless of which side changed (a controlled
|
|
150
|
-
// caller may set only `end`, e.g. two-way-binding an external store
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
150
|
+
// caller may set only `end`, e.g. two-way-binding an external store).
|
|
151
|
+
// The handle that just moved wins; the other side is pulled to meet it,
|
|
152
|
+
// mirroring the direction `clamp()` already uses during interactive
|
|
153
|
+
// dragging.
|
|
154
154
|
if (changed.has('start') || changed.has('min') || changed.has('max')) {
|
|
155
155
|
this.start = Math.min(this.start, this.end);
|
|
156
156
|
}
|