@aceshooting/lyra-ui 1.0.1 → 1.2.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 +9 -10
- package/custom-elements.json +15118 -8342
- package/dist/components/chart/chart.d.ts +57 -1
- package/dist/components/chart/chart.js +202 -12
- package/dist/components/chart/chart.stories.d.ts +32 -0
- package/dist/components/chart/chart.stories.js +141 -0
- package/dist/components/chart/chart.styles.js +16 -0
- package/dist/components/chart/histogram.d.ts +2 -0
- package/dist/components/chart/histogram.js +18 -2
- package/dist/components/chart/line-chart.stories.d.ts +17 -0
- package/dist/components/chart/line-chart.stories.js +75 -0
- package/dist/components/chart/lite-chart.d.ts +83 -0
- package/dist/components/chart/lite-chart.js +424 -0
- package/dist/components/chart/lite-chart.stories.d.ts +14 -0
- package/dist/components/chart/lite-chart.stories.js +114 -0
- package/dist/components/chart/lite-chart.styles.d.ts +1 -0
- package/dist/components/chart/lite-chart.styles.js +83 -0
- package/dist/components/combobox/combobox.d.ts +9 -0
- package/dist/components/combobox/combobox.js +76 -13
- package/dist/components/combobox/combobox.stories.d.ts +18 -0
- package/dist/components/combobox/combobox.stories.js +75 -0
- package/dist/components/date-picker/calendar-core.js +13 -2
- package/dist/components/date-picker/date-input.d.ts +9 -1
- package/dist/components/date-picker/date-input.js +55 -7
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +36 -0
- package/dist/components/date-picker/date-picker.d.ts +11 -3
- package/dist/components/date-picker/date-picker.js +87 -37
- package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
- package/dist/components/date-picker/date-picker.stories.js +12 -0
- package/dist/components/date-picker/date-picker.styles.js +1 -1
- package/dist/components/empty/empty.d.ts +12 -1
- package/dist/components/empty/empty.js +20 -3
- package/dist/components/empty/empty.stories.d.ts +2 -0
- package/dist/components/empty/empty.stories.js +20 -0
- package/dist/components/empty/empty.styles.js +11 -0
- package/dist/components/export-button/csv.js +6 -2
- package/dist/components/export-button/export-button.d.ts +15 -1
- package/dist/components/export-button/export-button.js +89 -12
- package/dist/components/export-button/export-button.stories.d.ts +1 -0
- package/dist/components/export-button/export-button.stories.js +5 -0
- package/dist/components/export-button/export-button.styles.js +11 -3
- package/dist/components/file-input/accept.d.ts +8 -2
- package/dist/components/file-input/accept.js +9 -3
- package/dist/components/file-input/file-input.d.ts +5 -2
- package/dist/components/file-input/file-input.js +23 -8
- package/dist/components/file-input/file-input.stories.d.ts +2 -0
- package/dist/components/file-input/file-input.stories.js +9 -0
- package/dist/components/flag/flag.d.ts +69 -3
- package/dist/components/flag/flag.js +107 -30
- package/dist/components/flag/flag.stories.d.ts +1 -0
- package/dist/components/flag/flag.stories.js +14 -0
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +11 -0
- package/dist/components/flag/language-map.js +12 -1
- package/dist/components/gauge/gauge.d.ts +2 -0
- package/dist/components/gauge/gauge.js +19 -5
- package/dist/components/gauge/gauge.stories.d.ts +2 -0
- package/dist/components/gauge/gauge.stories.js +6 -0
- package/dist/components/graph/graph-loader.d.ts +28 -0
- package/dist/components/graph/graph-loader.js +44 -0
- package/dist/components/graph/graph.d.ts +41 -15
- package/dist/components/graph/graph.js +146 -71
- package/dist/components/graph/graph.stories.d.ts +3 -0
- package/dist/components/graph/graph.stories.js +38 -0
- package/dist/components/graph/graph.styles.js +6 -1
- package/dist/components/heatmap/calendar-grid.d.ts +7 -0
- package/dist/components/heatmap/calendar-grid.js +27 -10
- package/dist/components/heatmap/heatmap.d.ts +149 -12
- package/dist/components/heatmap/heatmap.js +445 -28
- package/dist/components/heatmap/heatmap.stories.d.ts +22 -0
- package/dist/components/heatmap/heatmap.stories.js +155 -0
- package/dist/components/heatmap/heatmap.styles.js +60 -2
- package/dist/components/map/map.d.ts +17 -1
- package/dist/components/map/map.js +68 -21
- package/dist/components/map/map.stories.d.ts +10 -0
- package/dist/components/map/map.stories.js +107 -12
- package/dist/components/map/map.styles.js +2 -2
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +36 -12
- package/dist/components/playback/playback.stories.d.ts +2 -0
- package/dist/components/playback/playback.stories.js +6 -0
- package/dist/components/playback/playback.styles.js +10 -2
- package/dist/components/select/select.d.ts +125 -0
- package/dist/components/select/select.js +480 -0
- package/dist/components/select/select.stories.d.ts +21 -0
- package/dist/components/select/select.stories.js +91 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +212 -0
- package/dist/components/skeleton/skeleton.js +4 -7
- package/dist/components/sparkline/sparkline.d.ts +1 -1
- package/dist/components/sparkline/sparkline.js +40 -13
- package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
- package/dist/components/sparkline/sparkline.stories.js +22 -0
- package/dist/components/split/split.d.ts +24 -3
- package/dist/components/split/split.js +155 -31
- package/dist/components/split/split.stories.d.ts +2 -0
- package/dist/components/split/split.stories.js +20 -0
- package/dist/components/stat/stat.d.ts +43 -1
- package/dist/components/stat/stat.js +97 -4
- package/dist/components/stat/stat.stories.d.ts +4 -0
- package/dist/components/stat/stat.stories.js +66 -1
- package/dist/components/stat/stat.styles.js +64 -0
- package/dist/components/table/table.d.ts +64 -2
- package/dist/components/table/table.js +213 -17
- package/dist/components/table/table.stories.d.ts +6 -0
- package/dist/components/table/table.stories.js +45 -0
- package/dist/components/table/table.styles.js +51 -4
- package/dist/components/time-range/time-range.d.ts +34 -5
- package/dist/components/time-range/time-range.js +217 -36
- package/dist/components/time-range/time-range.stories.d.ts +2 -0
- package/dist/components/time-range/time-range.stories.js +13 -0
- package/dist/components/time-range/time-range.styles.js +44 -2
- package/dist/components/toast/toast-item.d.ts +14 -2
- package/dist/components/toast/toast-item.js +134 -17
- package/dist/components/toast/toast-item.styles.js +28 -2
- package/dist/components/toast/toast.stories.d.ts +3 -0
- package/dist/components/toast/toast.stories.js +41 -1
- package/dist/components/toast/toast.styles.js +11 -11
- package/dist/components/toast/toaster.d.ts +1 -1
- package/dist/components/toast/toaster.js +13 -6
- package/dist/components/tree/tree-node.d.ts +13 -7
- package/dist/components/tree/tree-node.js +24 -10
- package/dist/components/tree/tree.d.ts +64 -15
- package/dist/components/tree/tree.js +113 -22
- package/dist/components/tree/tree.stories.d.ts +4 -0
- package/dist/components/tree/tree.stories.js +31 -1
- package/dist/components/tree/update-cascade.d.ts +13 -0
- package/dist/components/tree/update-cascade.js +13 -0
- package/dist/components/widget/widget.d.ts +25 -1
- package/dist/components/widget/widget.js +164 -6
- package/dist/components/widget/widget.stories.d.ts +3 -0
- package/dist/components/widget/widget.stories.js +43 -0
- package/dist/components/widget/widget.styles.js +20 -3
- package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
- package/dist/components/word-cloud/word-cloud-layout.js +145 -0
- package/dist/components/word-cloud/word-cloud.d.ts +65 -0
- package/dist/components/word-cloud/word-cloud.js +255 -0
- package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
- package/dist/components/word-cloud/word-cloud.stories.js +55 -0
- package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
- package/dist/components/word-cloud/word-cloud.styles.js +55 -0
- package/dist/internal/a11y.js +2 -1
- package/dist/internal/rtl.d.ts +7 -0
- package/dist/internal/rtl.js +9 -0
- package/dist/lyra.d.ts +9 -1
- package/dist/lyra.js +6 -0
- package/llms-full.txt +962 -276
- package/llms.txt +12 -9
- package/package.json +6 -4
|
@@ -5,22 +5,32 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
import { html } from 'lit';
|
|
8
|
-
import { property, query } from 'lit/decorators.js';
|
|
8
|
+
import { property, query, state } from 'lit/decorators.js';
|
|
9
|
+
import { styleMap } from 'lit/directives/style-map.js';
|
|
9
10
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
11
|
import { defineElement } from '../../internal/prefix.js';
|
|
12
|
+
import { srOnly } from '../../internal/a11y.js';
|
|
11
13
|
import { linearAlpha, minMax, sqrtStep } from './heatmap-scale.js';
|
|
12
14
|
import { styles } from './heatmap.styles.js';
|
|
13
|
-
import { buildCalendarGrid, quartileBucket } from './calendar-grid.js';
|
|
15
|
+
import { buildCalendarGrid, parseIsoDate, quartileBucket } from './calendar-grid.js';
|
|
14
16
|
const PAD_LEFT = 60;
|
|
15
17
|
const PAD_TOP = 20;
|
|
16
18
|
const FALLBACK_NO_DATA_FILL = 'rgba(128,128,128,0.25)';
|
|
17
19
|
const RAMP_STEPS = 7;
|
|
18
20
|
const FALLBACK_SCALE_LO = '#cde2fb';
|
|
19
21
|
const FALLBACK_SCALE_HI = '#0969da';
|
|
22
|
+
const FALLBACK_LABEL_FONT = '10px sans-serif';
|
|
20
23
|
const CAL_PAD_LEFT = 28;
|
|
21
24
|
const CAL_LABEL_H = 16;
|
|
22
25
|
const CAL_CELL = 11;
|
|
23
26
|
const CAL_GAP = 2;
|
|
27
|
+
const DEFAULT_BUCKET_COUNT = 5;
|
|
28
|
+
/** Ring stroke width for both the annotation overlay and the keyboard focus ring. */
|
|
29
|
+
const RING_LINE_WIDTH = 2;
|
|
30
|
+
const FALLBACK_FOCUS_RING_COLOR = '#0969da';
|
|
31
|
+
const FALLBACK_ANNOTATION_COLOR = '#cf222e';
|
|
32
|
+
const ARROW_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']);
|
|
33
|
+
const MS_PER_DAY = 86_400_000;
|
|
24
34
|
const HEX_RE = /^([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
25
35
|
const RGB_RE = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i;
|
|
26
36
|
/**
|
|
@@ -56,6 +66,30 @@ function warnInvalidColor(color) {
|
|
|
56
66
|
console.warn(`<lyra-heatmap> could not parse "${color}" (set via --lyra-heatmap-scale-lo/-hi) as a CSS ` +
|
|
57
67
|
'color; falling back to the default ramp endpoint.');
|
|
58
68
|
}
|
|
69
|
+
const warnedInvalidBucketCounts = new Set();
|
|
70
|
+
function warnInvalidBucketCount(value) {
|
|
71
|
+
if (warnedInvalidBucketCounts.has(value))
|
|
72
|
+
return;
|
|
73
|
+
warnedInvalidBucketCounts.add(value);
|
|
74
|
+
console.warn(`<lyra-heatmap> received a non-finite bucket-count (${value}); falling back to ${DEFAULT_BUCKET_COUNT}.`);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Normalizes `bucketCount` to a finite integer >= 2. `Math.max(2, bucketCount)`
|
|
78
|
+
* alone stays `NaN` for a non-numeric `bucket-count` attribute (`Math.max`
|
|
79
|
+
* propagates `NaN`), which silently zeroes out the ramp array and makes every
|
|
80
|
+
* cell keep whatever `fillStyle` the previous draw left. A fractional count
|
|
81
|
+
* is floored rather than passed through as-is, so the ramp array's length
|
|
82
|
+
* (built via `Array.from({ length: buckets }, ...)`, which truncates a
|
|
83
|
+
* fractional `length`) always agrees with the index `quartileBucket` computes
|
|
84
|
+
* from the same `buckets` value.
|
|
85
|
+
*/
|
|
86
|
+
function resolveBucketCount(bucketCount) {
|
|
87
|
+
if (!Number.isFinite(bucketCount)) {
|
|
88
|
+
warnInvalidBucketCount(bucketCount);
|
|
89
|
+
return DEFAULT_BUCKET_COUNT;
|
|
90
|
+
}
|
|
91
|
+
return Math.max(2, Math.floor(bucketCount));
|
|
92
|
+
}
|
|
59
93
|
/**
|
|
60
94
|
* Resolves any syntactically valid CSS `<color>` — hex, `rgb()`, `hsl()`,
|
|
61
95
|
* `oklch()`, a named color, etc. — to an `[r, g, b]` triple.
|
|
@@ -102,16 +136,44 @@ export function mixColor(fromColor, toColor, t) {
|
|
|
102
136
|
return mixRgb(resolveRgb(fromColor, FALLBACK_SCALE_LO), resolveRgb(toColor, FALLBACK_SCALE_HI), t);
|
|
103
137
|
}
|
|
104
138
|
/**
|
|
105
|
-
* `<lyra-heatmap>` — a Canvas
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* via `
|
|
111
|
-
*
|
|
139
|
+
* `<lyra-heatmap>` — a Canvas heatmap with a DPR-aware, resize-aware redraw
|
|
140
|
+
* loop, in one of two `mode`s:
|
|
141
|
+
*
|
|
142
|
+
* - `"matrix"` (default): a `rowLabels` x `colLabels` grid of `values`. `-1`
|
|
143
|
+
* (or any non-finite value) is treated as "no data". `scale="sqrt"`
|
|
144
|
+
* compresses the ramp via `sqrtStep()` so one heavy cell doesn't wash out
|
|
145
|
+
* the rest; the default `"linear"` scale maps linearly instead. `scale`
|
|
146
|
+
* only affects matrix mode — calendar mode always buckets by quartile.
|
|
147
|
+
* - `"calendar"`: a GitHub-style Sunday-Saturday x week grid built from
|
|
148
|
+
* `days`, colored by `quartileBucket()` into `bucketCount` buckets. As in
|
|
149
|
+
* matrix mode, a cell whose `value` is negative or non-finite is treated
|
|
150
|
+
* as "no data" rather than being bucketed.
|
|
151
|
+
*
|
|
152
|
+
* The sequential color ramp's endpoints are read from the
|
|
153
|
+
* `--lyra-heatmap-scale-lo`/`-hi` custom properties (declared in
|
|
154
|
+
* `heatmap.styles.ts`) so hosts can retheme it — canvas can't consume
|
|
155
|
+
* `var()` directly, so they're resolved once per draw via
|
|
156
|
+
* `getComputedStyle`, then normalized to RGB by `resolveRgb()` (any valid
|
|
157
|
+
* CSS color syntax, not just hex — see its doc comment).
|
|
158
|
+
*
|
|
159
|
+
* Every cell is independently addressable: a `pointermove` hit test over the
|
|
160
|
+
* canvas shows `[part="tooltip"]` with that cell's label + value (hidden on
|
|
161
|
+
* `pointerleave`); the canvas is `tabindex="0"` with arrow-key roving focus
|
|
162
|
+
* (a stroked ring redrawn over the focused cell on every draw, plus a
|
|
163
|
+
* visually-hidden `aria-live="polite"` status announcement — avoids a
|
|
164
|
+
* DOM-node-per-cell overlay, which would be hundreds of nodes for a year
|
|
165
|
+
* calendar); and a click, or Enter/Space on the focused cell, fires
|
|
166
|
+
* `lyra-cell-click`. `annotations` additionally strokes a ring around
|
|
167
|
+
* specific cells (e.g. to call out an anomaly), each one optionally
|
|
168
|
+
* surfaced in the legend too via `[part="legend-annotation"]`.
|
|
112
169
|
*
|
|
113
170
|
* @customElement lyra-heatmap
|
|
114
|
-
* @
|
|
171
|
+
* @event lyra-cell-click - Fired on click, or Enter/Space on the
|
|
172
|
+
* focused/hovered cell. `detail: { row, col, value }` in matrix mode,
|
|
173
|
+
* `detail: { date, value }` in calendar mode. `cellText` overrides the
|
|
174
|
+
* built-in English "Row X, Col Y: value" / "Mon DD: value" template used for
|
|
175
|
+
* both the hover tooltip and the keyboard live-region announcement.
|
|
176
|
+
* @csspart base, canvas, tooltip, live-region, legend, legend-lo, legend-hi, legend-annotation
|
|
115
177
|
*/
|
|
116
178
|
export class LyraHeatmap extends LyraElement {
|
|
117
179
|
constructor() {
|
|
@@ -133,13 +195,47 @@ export class LyraHeatmap extends LyraElement {
|
|
|
133
195
|
this.fitToWidth = false;
|
|
134
196
|
this.mode = 'matrix';
|
|
135
197
|
this.days = [];
|
|
136
|
-
this.bucketCount =
|
|
198
|
+
this.bucketCount = DEFAULT_BUCKET_COUNT;
|
|
199
|
+
/** Cells to ring-highlight — `row`/`col` in matrix mode, `date` in calendar mode. See `HeatmapAnnotation`. */
|
|
200
|
+
this.annotations = [];
|
|
201
|
+
/** The current value range, refreshed once per update cycle by `willUpdate()`. See `computeValueRange()`. */
|
|
202
|
+
this.cachedValueRange = null;
|
|
203
|
+
/** The cell currently under the pointer (`null` when not hovering one) — drives `[part="tooltip"]`. */
|
|
204
|
+
this.hoverCell = null;
|
|
205
|
+
/** The roving keyboard-focus cell cursor, moved by arrow keys — drives the
|
|
206
|
+
* canvas-drawn focus ring and the `[part="live-region"]` announcement. */
|
|
207
|
+
this.focusedCell = null;
|
|
208
|
+
/** Text of the visually-hidden `aria-live="polite"` status announcement, refreshed on every focus move. */
|
|
209
|
+
this.liveText = '';
|
|
137
210
|
this.onDprChange = () => {
|
|
138
211
|
this.watchDpr();
|
|
139
212
|
this.draw();
|
|
140
213
|
};
|
|
214
|
+
this.onPointerMove = (e) => {
|
|
215
|
+
this.hoverCell = this.hitTest(e.offsetX, e.offsetY);
|
|
216
|
+
};
|
|
217
|
+
this.onPointerLeave = () => {
|
|
218
|
+
this.hoverCell = null;
|
|
219
|
+
};
|
|
220
|
+
this.onCanvasClick = (e) => {
|
|
221
|
+
// Prefer the click's own position; fall back to whatever's already
|
|
222
|
+
// keyboard-focused (e.g. a synthetic click dispatched via
|
|
223
|
+
// HTMLElement.click(), which carries no real coordinates).
|
|
224
|
+
const pos = this.hitTest(e.offsetX, e.offsetY) ?? this.focusedCell;
|
|
225
|
+
if (!pos)
|
|
226
|
+
return;
|
|
227
|
+
this.focusedCell = pos;
|
|
228
|
+
this.announce(pos);
|
|
229
|
+
this.emitCellClick(pos);
|
|
230
|
+
};
|
|
231
|
+
this.onKeyDown = (e) => {
|
|
232
|
+
if (this.mode === 'calendar')
|
|
233
|
+
this.onCalendarKeyDown(e);
|
|
234
|
+
else
|
|
235
|
+
this.onMatrixKeyDown(e);
|
|
236
|
+
};
|
|
141
237
|
}
|
|
142
|
-
static { this.styles = [LyraElement.styles, styles]; }
|
|
238
|
+
static { this.styles = [LyraElement.styles, styles, srOnly]; }
|
|
143
239
|
connectedCallback() {
|
|
144
240
|
super.connectedCallback();
|
|
145
241
|
this.resizeObserver = new ResizeObserver(() => this.draw());
|
|
@@ -160,10 +256,23 @@ export class LyraHeatmap extends LyraElement {
|
|
|
160
256
|
this.dprQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
161
257
|
this.dprQuery.addEventListener('change', this.onDprChange);
|
|
162
258
|
}
|
|
163
|
-
willUpdate() {
|
|
164
|
-
|
|
259
|
+
willUpdate(changed) {
|
|
260
|
+
if (changed.has('mode') || changed.has('rowLabels') || changed.has('colLabels') || changed.has('days')) {
|
|
261
|
+
// The previous focus/hover cursor may no longer address a real cell
|
|
262
|
+
// once the grid's shape (or mode) changes out from under it —
|
|
263
|
+
// stroking a focus ring at a stale (row, col)/(week, weekday), or
|
|
264
|
+
// leaving the live region announcing a stale value, would be actively
|
|
265
|
+
// misleading, so drop both instead. Reactive-property writes belong in
|
|
266
|
+
// willUpdate() (not updated()), which folds them into this same
|
|
267
|
+
// render rather than scheduling a whole extra update pass.
|
|
268
|
+
this.focusedCell = null;
|
|
269
|
+
this.hoverCell = null;
|
|
270
|
+
this.liveText = '';
|
|
271
|
+
}
|
|
272
|
+
this.cachedValueRange = this.computeValueRange();
|
|
273
|
+
const bounds = this.cachedValueRange;
|
|
165
274
|
const range = bounds ? `${bounds[0]}–${bounds[1]}` : 'no data';
|
|
166
|
-
this.setAttribute('role', '
|
|
275
|
+
this.setAttribute('role', 'group');
|
|
167
276
|
if (this.mode === 'calendar') {
|
|
168
277
|
this.setAttribute('aria-label', `Calendar heatmap of ${this.days.length} days, ${this.valueLabel} range ${range}`);
|
|
169
278
|
}
|
|
@@ -173,8 +282,16 @@ export class LyraHeatmap extends LyraElement {
|
|
|
173
282
|
this.setAttribute('aria-label', `Heatmap of ${rows} × ${cols} cells, ${this.valueLabel} range ${range}`);
|
|
174
283
|
}
|
|
175
284
|
}
|
|
176
|
-
/**
|
|
177
|
-
|
|
285
|
+
/**
|
|
286
|
+
* The real (non-no-data) value range across `values` (or `days` in
|
|
287
|
+
* calendar mode), or `null` if there is none. Only called from
|
|
288
|
+
* `willUpdate()`, which caches the result in `cachedValueRange` for
|
|
289
|
+
* `render()` and `drawMatrix()` to reuse — `willUpdate()`, `render()`, and
|
|
290
|
+
* `updated()` (which triggers `draw()`) all run within the same Lit update
|
|
291
|
+
* cycle against the same `values`/`days`, so a single scan per cycle
|
|
292
|
+
* suffices instead of one per consumer.
|
|
293
|
+
*/
|
|
294
|
+
computeValueRange() {
|
|
178
295
|
const source = this.mode === 'calendar' ? this.days.map((d) => d.value) : this.values.flat();
|
|
179
296
|
return minMax(source.filter((v) => Number.isFinite(v) && v >= 0));
|
|
180
297
|
}
|
|
@@ -192,10 +309,22 @@ export class LyraHeatmap extends LyraElement {
|
|
|
192
309
|
labelColor() {
|
|
193
310
|
return getComputedStyle(this).getPropertyValue('--lyra-color-text-quiet').trim() || '#6b7280';
|
|
194
311
|
}
|
|
312
|
+
/** Reads the customizable canvas axis/label font off the host's computed style. */
|
|
313
|
+
labelFont() {
|
|
314
|
+
return getComputedStyle(this).getPropertyValue('--lyra-heatmap-label-font').trim() || FALLBACK_LABEL_FONT;
|
|
315
|
+
}
|
|
195
316
|
/** Reads the customizable no-data cell fill off the host's computed style. */
|
|
196
317
|
noDataFill() {
|
|
197
318
|
return getComputedStyle(this).getPropertyValue('--lyra-heatmap-no-data-fill').trim() || FALLBACK_NO_DATA_FILL;
|
|
198
319
|
}
|
|
320
|
+
/** Reads the customizable canvas-drawn keyboard-focus-ring stroke color off the host's computed style. */
|
|
321
|
+
focusRingColor() {
|
|
322
|
+
return (getComputedStyle(this).getPropertyValue('--lyra-heatmap-focus-ring-color').trim() || FALLBACK_FOCUS_RING_COLOR);
|
|
323
|
+
}
|
|
324
|
+
/** Reads the customizable canvas-drawn annotation-ring stroke color off the host's computed style. */
|
|
325
|
+
annotationColor() {
|
|
326
|
+
return (getComputedStyle(this).getPropertyValue('--lyra-heatmap-annotation-color').trim() || FALLBACK_ANNOTATION_COLOR);
|
|
327
|
+
}
|
|
199
328
|
draw() {
|
|
200
329
|
if (this.mode === 'calendar')
|
|
201
330
|
this.drawCalendar();
|
|
@@ -225,7 +354,7 @@ export class LyraHeatmap extends LyraElement {
|
|
|
225
354
|
const [scaleLo, scaleHi] = this.scaleEndpoints();
|
|
226
355
|
const loRgb = resolveRgb(scaleLo, FALLBACK_SCALE_LO);
|
|
227
356
|
const hiRgb = resolveRgb(scaleHi, FALLBACK_SCALE_HI);
|
|
228
|
-
const buckets =
|
|
357
|
+
const buckets = resolveBucketCount(this.bucketCount);
|
|
229
358
|
const ramp = Array.from({ length: buckets }, (_, i) => mixRgb(loRgb, hiRgb, i / (buckets - 1)));
|
|
230
359
|
const noDataFill = this.noDataFill();
|
|
231
360
|
for (const cell of cells) {
|
|
@@ -237,8 +366,37 @@ export class LyraHeatmap extends LyraElement {
|
|
|
237
366
|
: ramp[quartileBucket(cell.value, sortedValues, buckets)];
|
|
238
367
|
ctx.fillRect(x, y, CAL_CELL, CAL_CELL);
|
|
239
368
|
}
|
|
369
|
+
// Annotation ring overlay, stroked after the fill pass so it reads
|
|
370
|
+
// clearly over the data-driven cell color — see drawMatrix()'s twin of
|
|
371
|
+
// this block for the matrix-mode equivalent.
|
|
372
|
+
if (this.annotations.length) {
|
|
373
|
+
ctx.lineWidth = RING_LINE_WIDTH;
|
|
374
|
+
ctx.strokeStyle = this.annotationColor();
|
|
375
|
+
for (const ann of this.annotations) {
|
|
376
|
+
if (ann.date == null)
|
|
377
|
+
continue;
|
|
378
|
+
const match = cells.find((c) => c.date === ann.date);
|
|
379
|
+
if (!match)
|
|
380
|
+
continue;
|
|
381
|
+
const x = CAL_PAD_LEFT + match.week * (CAL_CELL + CAL_GAP);
|
|
382
|
+
const y = CAL_LABEL_H + match.weekday * (CAL_CELL + CAL_GAP);
|
|
383
|
+
ctx.strokeRect(x + 0.5, y + 0.5, CAL_CELL - 1, CAL_CELL - 1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Keyboard focus ring, redrawn on top of the fill pass (and any
|
|
387
|
+
// annotation rings) on every draw.
|
|
388
|
+
if (this.focusedCell && 'week' in this.focusedCell) {
|
|
389
|
+
const { week, weekday } = this.focusedCell;
|
|
390
|
+
if (week < weekCount && weekday < 7) {
|
|
391
|
+
ctx.lineWidth = RING_LINE_WIDTH;
|
|
392
|
+
ctx.strokeStyle = this.focusRingColor();
|
|
393
|
+
const x = CAL_PAD_LEFT + week * (CAL_CELL + CAL_GAP);
|
|
394
|
+
const y = CAL_LABEL_H + weekday * (CAL_CELL + CAL_GAP);
|
|
395
|
+
ctx.strokeRect(x + 0.5, y + 0.5, CAL_CELL - 1, CAL_CELL - 1);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
240
398
|
ctx.fillStyle = this.labelColor();
|
|
241
|
-
ctx.font =
|
|
399
|
+
ctx.font = this.labelFont();
|
|
242
400
|
for (const m of monthLabels) {
|
|
243
401
|
ctx.fillText(m.label, CAL_PAD_LEFT + m.week * (CAL_CELL + CAL_GAP), CAL_LABEL_H - 4);
|
|
244
402
|
}
|
|
@@ -248,16 +406,26 @@ export class LyraHeatmap extends LyraElement {
|
|
|
248
406
|
ctx.fillText(label, 2, CAL_LABEL_H + weekday * (CAL_CELL + CAL_GAP) + CAL_CELL - 1);
|
|
249
407
|
});
|
|
250
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Effective per-cell size in matrix mode — `fitToWidth` derives it from
|
|
411
|
+
* the host's measured width, otherwise it's the fixed `cellSize`
|
|
412
|
+
* property. Shared by `drawMatrix()` and the pointer/keyboard hit-testing
|
|
413
|
+
* below (`hitTestMatrix()`, `cellRect()`) so they always agree on exactly
|
|
414
|
+
* the same geometry as what's actually painted.
|
|
415
|
+
*/
|
|
416
|
+
matrixCellSize(cols) {
|
|
417
|
+
if (this.fitToWidth && cols > 0) {
|
|
418
|
+
const hostWidth = this.clientWidth || PAD_LEFT + cols * this.cellSize;
|
|
419
|
+
return Math.max(4, (hostWidth - PAD_LEFT) / cols);
|
|
420
|
+
}
|
|
421
|
+
return this.cellSize;
|
|
422
|
+
}
|
|
251
423
|
drawMatrix() {
|
|
252
424
|
if (!this.canvas)
|
|
253
425
|
return;
|
|
254
426
|
const rows = this.rowLabels.length;
|
|
255
427
|
const cols = this.colLabels.length;
|
|
256
|
-
|
|
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
|
-
}
|
|
428
|
+
const cellSize = this.matrixCellSize(cols);
|
|
261
429
|
const w = PAD_LEFT + cols * cellSize;
|
|
262
430
|
const h = PAD_TOP + rows * cellSize;
|
|
263
431
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -270,7 +438,7 @@ export class LyraHeatmap extends LyraElement {
|
|
|
270
438
|
return;
|
|
271
439
|
ctx.scale(dpr, dpr);
|
|
272
440
|
ctx.clearRect(0, 0, w, h);
|
|
273
|
-
const bounds = this.
|
|
441
|
+
const bounds = this.cachedValueRange;
|
|
274
442
|
const lo = bounds ? bounds[0] : 0;
|
|
275
443
|
const hi = bounds ? bounds[1] : 1;
|
|
276
444
|
const [scaleLo, scaleHi] = this.scaleEndpoints();
|
|
@@ -305,8 +473,40 @@ export class LyraHeatmap extends LyraElement {
|
|
|
305
473
|
ctx.fillRect(x, y, cellSize - 1, cellSize - 1);
|
|
306
474
|
}
|
|
307
475
|
}
|
|
476
|
+
// Annotation ring overlay, stroked after the fill pass so it reads
|
|
477
|
+
// clearly over the data-driven cell color. Uses a dedicated
|
|
478
|
+
// --lyra-heatmap-annotation-color token (defaults to --lyra-color-danger)
|
|
479
|
+
// rather than any of the sequential ramp colors, since it needs to stay
|
|
480
|
+
// visible regardless of which point on that ramp it's drawn over.
|
|
481
|
+
if (this.annotations.length) {
|
|
482
|
+
ctx.lineWidth = RING_LINE_WIDTH;
|
|
483
|
+
ctx.strokeStyle = this.annotationColor();
|
|
484
|
+
for (const ann of this.annotations) {
|
|
485
|
+
if (ann.row == null || ann.col == null)
|
|
486
|
+
continue;
|
|
487
|
+
if (ann.row < 0 || ann.row >= rows || ann.col < 0 || ann.col >= cols)
|
|
488
|
+
continue;
|
|
489
|
+
const x = PAD_LEFT + ann.col * cellSize;
|
|
490
|
+
const y = PAD_TOP + ann.row * cellSize;
|
|
491
|
+
ctx.strokeRect(x + 1, y + 1, cellSize - 3, cellSize - 3);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Keyboard focus ring, redrawn on top of the fill pass (and any
|
|
495
|
+
// annotation rings) on every draw — canvas has no persistent DOM focus
|
|
496
|
+
// affordance of its own for an individual cell, only a `:focus-visible`
|
|
497
|
+
// outline around the whole element (see heatmap.styles.ts).
|
|
498
|
+
if (this.focusedCell && 'row' in this.focusedCell) {
|
|
499
|
+
const { row, col } = this.focusedCell;
|
|
500
|
+
if (row < rows && col < cols) {
|
|
501
|
+
ctx.lineWidth = RING_LINE_WIDTH;
|
|
502
|
+
ctx.strokeStyle = this.focusRingColor();
|
|
503
|
+
const x = PAD_LEFT + col * cellSize;
|
|
504
|
+
const y = PAD_TOP + row * cellSize;
|
|
505
|
+
ctx.strokeRect(x + 1, y + 1, cellSize - 3, cellSize - 3);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
308
508
|
ctx.fillStyle = this.labelColor();
|
|
309
|
-
ctx.font =
|
|
509
|
+
ctx.font = this.labelFont();
|
|
310
510
|
this.rowLabels.forEach((label, r) => {
|
|
311
511
|
ctx.fillText(label, 4, PAD_TOP + r * cellSize + cellSize / 2 + 3);
|
|
312
512
|
});
|
|
@@ -314,16 +514,218 @@ export class LyraHeatmap extends LyraElement {
|
|
|
314
514
|
ctx.fillText(label, PAD_LEFT + c * cellSize + 2, PAD_TOP - 6);
|
|
315
515
|
});
|
|
316
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Maps a pointer position, in canvas-local CSS px (e.g.
|
|
519
|
+
* `PointerEvent.offsetX/offsetY`), to the cell underneath it — or `null`
|
|
520
|
+
* if the pointer is outside the grid. Dispatches on `mode` so callers
|
|
521
|
+
* don't have to.
|
|
522
|
+
*/
|
|
523
|
+
hitTest(x, y) {
|
|
524
|
+
return this.mode === 'calendar' ? this.hitTestCalendar(x, y) : this.hitTestMatrix(x, y);
|
|
525
|
+
}
|
|
526
|
+
hitTestMatrix(x, y) {
|
|
527
|
+
const rows = this.rowLabels.length;
|
|
528
|
+
const cols = this.colLabels.length;
|
|
529
|
+
if (rows === 0 || cols === 0)
|
|
530
|
+
return null;
|
|
531
|
+
const cellSize = this.matrixCellSize(cols);
|
|
532
|
+
const col = Math.floor((x - PAD_LEFT) / cellSize);
|
|
533
|
+
const row = Math.floor((y - PAD_TOP) / cellSize);
|
|
534
|
+
if (row < 0 || row >= rows || col < 0 || col >= cols)
|
|
535
|
+
return null;
|
|
536
|
+
return { row, col };
|
|
537
|
+
}
|
|
538
|
+
hitTestCalendar(x, y) {
|
|
539
|
+
const { weekCount } = buildCalendarGrid(this.days);
|
|
540
|
+
if (weekCount === 0)
|
|
541
|
+
return null;
|
|
542
|
+
const week = Math.floor((x - CAL_PAD_LEFT) / (CAL_CELL + CAL_GAP));
|
|
543
|
+
const weekday = Math.floor((y - CAL_LABEL_H) / (CAL_CELL + CAL_GAP));
|
|
544
|
+
if (week < 0 || week >= weekCount || weekday < 0 || weekday > 6)
|
|
545
|
+
return null;
|
|
546
|
+
return { week, weekday };
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Resolves a (week, weekday) grid position to its real calendar date and
|
|
550
|
+
* value. Computed from grid geometry (`firstWeekStart` + offset), not
|
|
551
|
+
* just looked up in `cells` — so a position with no matching entry in
|
|
552
|
+
* `days` (a gap in a sparse calendar) still resolves to a real ISO date,
|
|
553
|
+
* with a `-1` "no data" sentinel value (the same convention `values`' `-1`
|
|
554
|
+
* sentinel uses in matrix mode), instead of being unresolvable.
|
|
555
|
+
*/
|
|
556
|
+
calendarCellAt(pos) {
|
|
557
|
+
const { cells, firstWeekStart } = buildCalendarGrid(this.days);
|
|
558
|
+
const match = cells.find((c) => c.week === pos.week && c.weekday === pos.weekday);
|
|
559
|
+
if (match)
|
|
560
|
+
return { date: match.date, value: match.value };
|
|
561
|
+
const date = new Date(firstWeekStart.getTime() + (pos.week * 7 + pos.weekday) * MS_PER_DAY);
|
|
562
|
+
return { date: date.toISOString().slice(0, 10), value: -1 };
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Pixel rect (canvas-local CSS px) of a cell — shared by `tooltipStyle()`
|
|
566
|
+
* so the tooltip's position always agrees with the same geometry
|
|
567
|
+
* `drawMatrix()`/`drawCalendar()`/`hitTest*()` use.
|
|
568
|
+
*/
|
|
569
|
+
cellRect(pos) {
|
|
570
|
+
if ('week' in pos) {
|
|
571
|
+
return {
|
|
572
|
+
x: CAL_PAD_LEFT + pos.week * (CAL_CELL + CAL_GAP),
|
|
573
|
+
y: CAL_LABEL_H + pos.weekday * (CAL_CELL + CAL_GAP),
|
|
574
|
+
w: CAL_CELL,
|
|
575
|
+
h: CAL_CELL,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const cellSize = this.matrixCellSize(this.colLabels.length);
|
|
579
|
+
return { x: PAD_LEFT + pos.col * cellSize, y: PAD_TOP + pos.row * cellSize, w: cellSize - 1, h: cellSize - 1 };
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Human-readable "<label>: <value>" text for a cell — shared by the hover
|
|
583
|
+
* tooltip and the keyboard-focus live-region announcement, so both always
|
|
584
|
+
* describe a cell the same way. This is the built-in English fallback used
|
|
585
|
+
* when `cellText` isn't set — see `resolveCellText()`.
|
|
586
|
+
*/
|
|
587
|
+
defaultCellText(pos) {
|
|
588
|
+
return 'week' in pos ? this.calendarCellText(pos) : this.matrixCellText(pos);
|
|
589
|
+
}
|
|
590
|
+
matrixCellText(pos) {
|
|
591
|
+
const rowLabel = this.rowLabels[pos.row] ?? `row ${pos.row + 1}`;
|
|
592
|
+
const colLabel = this.colLabels[pos.col] ?? `col ${pos.col + 1}`;
|
|
593
|
+
const v = this.values[pos.row]?.[pos.col];
|
|
594
|
+
const valueText = v == null || v < 0 || !Number.isFinite(v) ? 'no data' : String(v);
|
|
595
|
+
return `Row ${rowLabel}, Col ${colLabel}: ${valueText}`;
|
|
596
|
+
}
|
|
597
|
+
calendarCellText(pos) {
|
|
598
|
+
const { date, value } = this.calendarCellAt(pos);
|
|
599
|
+
const label = parseIsoDate(date).toLocaleString(undefined, { month: 'short', day: 'numeric', timeZone: 'UTC' });
|
|
600
|
+
const valueText = value < 0 || !Number.isFinite(value) ? 'no data' : String(value);
|
|
601
|
+
return `${label}: ${valueText}`;
|
|
602
|
+
}
|
|
603
|
+
/** The raw numeric value at a cell position, in either mode — shared by `resolveCellText()`
|
|
604
|
+
* so a custom `cellText` formatter receives the same value the built-in template would use. */
|
|
605
|
+
valueAt(pos) {
|
|
606
|
+
if ('week' in pos)
|
|
607
|
+
return this.calendarCellAt(pos).value;
|
|
608
|
+
return this.values[pos.row]?.[pos.col] ?? -1;
|
|
609
|
+
}
|
|
610
|
+
/** Dispatches to the host-provided `cellText` formatter when set, otherwise the built-in template. */
|
|
611
|
+
resolveCellText(pos) {
|
|
612
|
+
return this.cellText ? this.cellText(pos, this.valueAt(pos)) : this.defaultCellText(pos);
|
|
613
|
+
}
|
|
614
|
+
/** Refreshes the visually-hidden live-region text for a newly-focused cell. */
|
|
615
|
+
announce(pos) {
|
|
616
|
+
this.liveText = this.resolveCellText(pos);
|
|
617
|
+
}
|
|
618
|
+
emitCellClick(pos) {
|
|
619
|
+
if ('week' in pos) {
|
|
620
|
+
const { date, value } = this.calendarCellAt(pos);
|
|
621
|
+
this.emit('lyra-cell-click', { date, value });
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
const value = this.values[pos.row]?.[pos.col] ?? -1;
|
|
625
|
+
this.emit('lyra-cell-click', { row: pos.row, col: pos.col, value });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/** Inline `style` for `[part="tooltip"]`, centered above the given cell. */
|
|
629
|
+
tooltipStyle(pos) {
|
|
630
|
+
const rect = this.cellRect(pos);
|
|
631
|
+
return { left: `${rect.x + rect.w / 2}px`, top: `${rect.y}px` };
|
|
632
|
+
}
|
|
633
|
+
onMatrixKeyDown(e) {
|
|
634
|
+
const rows = this.rowLabels.length;
|
|
635
|
+
const cols = this.colLabels.length;
|
|
636
|
+
if (rows === 0 || cols === 0)
|
|
637
|
+
return;
|
|
638
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
639
|
+
e.preventDefault();
|
|
640
|
+
if (this.focusedCell)
|
|
641
|
+
this.emitCellClick(this.focusedCell);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (!ARROW_KEYS.has(e.key))
|
|
645
|
+
return;
|
|
646
|
+
e.preventDefault();
|
|
647
|
+
// The first arrow keypress just moves focus onto the grid (at the
|
|
648
|
+
// top-left cell) rather than also applying that keypress's delta —
|
|
649
|
+
// otherwise ArrowRight from "unfocused" would land on column 1,
|
|
650
|
+
// silently skipping column 0.
|
|
651
|
+
if (!this.focusedCell || !('row' in this.focusedCell)) {
|
|
652
|
+
const next = { row: 0, col: 0 };
|
|
653
|
+
this.focusedCell = next;
|
|
654
|
+
this.announce(next);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const { row, col } = this.focusedCell;
|
|
658
|
+
let next = { row, col };
|
|
659
|
+
if (e.key === 'ArrowUp')
|
|
660
|
+
next = { row: Math.max(0, row - 1), col };
|
|
661
|
+
else if (e.key === 'ArrowDown')
|
|
662
|
+
next = { row: Math.min(rows - 1, row + 1), col };
|
|
663
|
+
else if (e.key === 'ArrowLeft')
|
|
664
|
+
next = { row, col: Math.max(0, col - 1) };
|
|
665
|
+
else if (e.key === 'ArrowRight')
|
|
666
|
+
next = { row, col: Math.min(cols - 1, col + 1) };
|
|
667
|
+
this.focusedCell = next;
|
|
668
|
+
this.announce(next);
|
|
669
|
+
}
|
|
670
|
+
onCalendarKeyDown(e) {
|
|
671
|
+
const { weekCount } = buildCalendarGrid(this.days);
|
|
672
|
+
if (weekCount === 0)
|
|
673
|
+
return;
|
|
674
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
675
|
+
e.preventDefault();
|
|
676
|
+
if (this.focusedCell)
|
|
677
|
+
this.emitCellClick(this.focusedCell);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (!ARROW_KEYS.has(e.key))
|
|
681
|
+
return;
|
|
682
|
+
e.preventDefault();
|
|
683
|
+
if (!this.focusedCell || !('week' in this.focusedCell)) {
|
|
684
|
+
const next = { week: 0, weekday: 0 };
|
|
685
|
+
this.focusedCell = next;
|
|
686
|
+
this.announce(next);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const { week, weekday } = this.focusedCell;
|
|
690
|
+
let next = { week, weekday };
|
|
691
|
+
if (e.key === 'ArrowUp')
|
|
692
|
+
next = { week, weekday: Math.max(0, weekday - 1) };
|
|
693
|
+
else if (e.key === 'ArrowDown')
|
|
694
|
+
next = { week, weekday: Math.min(6, weekday + 1) };
|
|
695
|
+
else if (e.key === 'ArrowLeft')
|
|
696
|
+
next = { week: Math.max(0, week - 1), weekday };
|
|
697
|
+
else if (e.key === 'ArrowRight')
|
|
698
|
+
next = { week: Math.min(weekCount - 1, week + 1), weekday };
|
|
699
|
+
this.focusedCell = next;
|
|
700
|
+
this.announce(next);
|
|
701
|
+
}
|
|
317
702
|
render() {
|
|
318
|
-
const range = this.
|
|
703
|
+
const range = this.cachedValueRange;
|
|
704
|
+
const labeledAnnotations = this.annotations.filter((a) => a.label);
|
|
319
705
|
return html `
|
|
320
706
|
<div part="base">
|
|
321
|
-
<canvas
|
|
707
|
+
<canvas
|
|
708
|
+
part="canvas"
|
|
709
|
+
tabindex="0"
|
|
710
|
+
@pointermove=${this.onPointerMove}
|
|
711
|
+
@pointerleave=${this.onPointerLeave}
|
|
712
|
+
@click=${this.onCanvasClick}
|
|
713
|
+
@keydown=${this.onKeyDown}
|
|
714
|
+
></canvas>
|
|
715
|
+
<div
|
|
716
|
+
part="tooltip"
|
|
717
|
+
?hidden=${!this.hoverCell}
|
|
718
|
+
style=${styleMap(this.hoverCell ? this.tooltipStyle(this.hoverCell) : {})}
|
|
719
|
+
>
|
|
720
|
+
${this.hoverCell ? this.resolveCellText(this.hoverCell) : ''}
|
|
721
|
+
</div>
|
|
722
|
+
<div part="live-region" class="sr-only" role="status" aria-live="polite">${this.liveText}</div>
|
|
322
723
|
<div part="legend">
|
|
323
724
|
<span part="legend-lo">${range ? range[0] : ''}</span>
|
|
324
725
|
<span class="bar"></span>
|
|
325
726
|
<span part="legend-hi">${range ? range[1] : ''}</span>
|
|
326
727
|
<span>${this.valueLabel}</span>
|
|
728
|
+
${labeledAnnotations.map((a) => html `<span part="legend-annotation"><span class="ring-swatch"></span>${a.label}</span>`)}
|
|
327
729
|
</div>
|
|
328
730
|
</div>
|
|
329
731
|
`;
|
|
@@ -359,7 +761,22 @@ __decorate([
|
|
|
359
761
|
__decorate([
|
|
360
762
|
property({ attribute: 'bucket-count', type: Number })
|
|
361
763
|
], LyraHeatmap.prototype, "bucketCount", void 0);
|
|
764
|
+
__decorate([
|
|
765
|
+
property({ attribute: false })
|
|
766
|
+
], LyraHeatmap.prototype, "annotations", void 0);
|
|
767
|
+
__decorate([
|
|
768
|
+
property({ attribute: false })
|
|
769
|
+
], LyraHeatmap.prototype, "cellText", void 0);
|
|
362
770
|
__decorate([
|
|
363
771
|
query('canvas')
|
|
364
772
|
], LyraHeatmap.prototype, "canvas", void 0);
|
|
773
|
+
__decorate([
|
|
774
|
+
state()
|
|
775
|
+
], LyraHeatmap.prototype, "hoverCell", void 0);
|
|
776
|
+
__decorate([
|
|
777
|
+
state()
|
|
778
|
+
], LyraHeatmap.prototype, "focusedCell", void 0);
|
|
779
|
+
__decorate([
|
|
780
|
+
state()
|
|
781
|
+
], LyraHeatmap.prototype, "liveText", void 0);
|
|
365
782
|
defineElement('heatmap', LyraHeatmap);
|
|
@@ -3,3 +3,25 @@ declare const meta: Meta;
|
|
|
3
3
|
export default meta;
|
|
4
4
|
type Story = StoryObj;
|
|
5
5
|
export declare const Default: Story;
|
|
6
|
+
export declare const SqrtScaleFitToWidth: Story;
|
|
7
|
+
export declare const CalendarMode: Story;
|
|
8
|
+
export declare const CustomTheme: Story;
|
|
9
|
+
/**
|
|
10
|
+
* Hover a cell for its tooltip, tab to the grid and use the arrow keys to
|
|
11
|
+
* move the focus ring (a visually-hidden live region announces each move),
|
|
12
|
+
* and click or press Enter/Space on a cell to see its `lyra-cell-click`
|
|
13
|
+
* detail logged below.
|
|
14
|
+
*/
|
|
15
|
+
export declare const HoverFocusClick: Story;
|
|
16
|
+
/**
|
|
17
|
+
* Cells flagged via `annotations` get a stroked ring; ones with a `label`
|
|
18
|
+
* also surface a swatch + text entry in the legend.
|
|
19
|
+
*/
|
|
20
|
+
export declare const Annotations: Story;
|
|
21
|
+
/**
|
|
22
|
+
* `cellText` overrides the built-in English tooltip/live-region template —
|
|
23
|
+
* here with a French translation — for both matrix and calendar modes.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CustomCellText: Story;
|
|
26
|
+
/** `annotations` in calendar mode match by ISO `date` instead of `row`/`col`. */
|
|
27
|
+
export declare const CalendarAnnotations: Story;
|