@aceshooting/lyra-ui 2.0.0 → 2.1.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 +87 -14
- package/custom-elements.json +11555 -5539
- package/dist/components/app-rail/app-rail-item.d.ts +30 -0
- package/dist/components/app-rail/app-rail-item.js +68 -0
- package/dist/components/app-rail/app-rail-item.styles.d.ts +1 -0
- package/dist/components/app-rail/app-rail-item.styles.js +65 -0
- package/dist/components/app-rail/app-rail.d.ts +21 -24
- package/dist/components/app-rail/app-rail.js +68 -132
- package/dist/components/app-rail/app-rail.styles.js +5 -2
- package/dist/components/attachment-chip/attachment-chip.d.ts +38 -11
- package/dist/components/attachment-chip/attachment-chip.js +73 -29
- package/dist/components/attachment-trigger/attachment-trigger.d.ts +13 -0
- package/dist/components/attachment-trigger/attachment-trigger.js +13 -1
- package/dist/components/attachment-trigger/attachment-trigger.styles.js +28 -6
- package/dist/components/chart/bar-chart.js +2 -18
- package/dist/components/chart/box-plot.d.ts +25 -1
- package/dist/components/chart/box-plot.js +136 -6
- package/dist/components/chart/box-plot.styles.js +13 -0
- package/dist/components/chart/bubble-chart.js +2 -18
- package/dist/components/chart/chart.d.ts +54 -10
- package/dist/components/chart/chart.js +182 -39
- package/dist/components/chart/doughnut-chart.js +2 -18
- package/dist/components/chart/histogram-bin.d.ts +5 -2
- package/dist/components/chart/histogram-bin.js +12 -4
- package/dist/components/chart/histogram.js +8 -16
- package/dist/components/chart/line-chart.js +2 -18
- package/dist/components/chart/lite-chart.d.ts +80 -3
- package/dist/components/chart/lite-chart.js +250 -53
- package/dist/components/chart/pie-chart.js +2 -18
- package/dist/components/chart/polar-area-chart.js +2 -18
- package/dist/components/chart/radar-chart.js +2 -18
- package/dist/components/chart/scatter-chart.js +2 -18
- package/dist/components/chat-composer/chat-composer.d.ts +42 -1
- package/dist/components/chat-composer/chat-composer.js +84 -3
- package/dist/components/chat-composer/chat-composer.styles.js +11 -1
- package/dist/components/chat-message/chat-message.d.ts +32 -4
- package/dist/components/chat-message/chat-message.js +57 -17
- package/dist/components/checkbox/checkbox.d.ts +66 -8
- package/dist/components/checkbox/checkbox.js +173 -50
- package/dist/components/checkbox/checkbox.styles.js +9 -2
- package/dist/components/chip/chip-group.js +8 -2
- package/dist/components/chip/chip.js +9 -2
- package/dist/components/citation-badge/citation-badge.js +21 -3
- package/dist/components/code-block/code-block.d.ts +38 -0
- package/dist/components/code-block/code-block.js +72 -7
- package/dist/components/code-block/code-loader.d.ts +44 -1
- package/dist/components/code-block/code-loader.js +63 -0
- package/dist/components/combobox/combobox.d.ts +46 -5
- package/dist/components/combobox/combobox.js +175 -35
- package/dist/components/combobox/combobox.styles.js +3 -3
- package/dist/components/context-meter/context-meter.js +17 -8
- package/dist/components/context-meter/context-meter.styles.js +0 -6
- package/dist/components/conversation-item/conversation-item.d.ts +13 -16
- package/dist/components/conversation-item/conversation-item.js +29 -20
- package/dist/components/date-picker/calendar-core.d.ts +24 -0
- package/dist/components/date-picker/calendar-core.js +51 -4
- package/dist/components/date-picker/date-input.d.ts +77 -10
- package/dist/components/date-picker/date-input.js +278 -73
- package/dist/components/date-picker/date-picker.d.ts +28 -4
- package/dist/components/date-picker/date-picker.js +95 -37
- package/dist/components/dialog/confirm.js +3 -6
- package/dist/components/dialog/dialog.d.ts +75 -31
- package/dist/components/dialog/dialog.js +147 -131
- package/dist/components/dialog/dialog.styles.js +46 -2
- package/dist/components/dock-panel/dock-panel.d.ts +8 -0
- package/dist/components/dock-panel/dock-panel.js +15 -1
- package/dist/components/dock-panel/dock-panel.styles.js +15 -1
- package/dist/components/document-preview/document-preview.d.ts +15 -7
- package/dist/components/document-preview/document-preview.js +29 -13
- package/dist/components/empty/empty.d.ts +1 -2
- package/dist/components/empty/empty.js +18 -12
- package/dist/components/empty/empty.styles.js +0 -3
- package/dist/components/export-button/export-button.d.ts +12 -0
- package/dist/components/export-button/export-button.js +33 -5
- package/dist/components/export-button/export-button.styles.js +5 -1
- package/dist/components/file-input/accept.js +2 -0
- package/dist/components/file-input/file-input.d.ts +10 -1
- package/dist/components/file-input/file-input.js +36 -5
- package/dist/components/flag/flag.d.ts +1 -1
- package/dist/components/flag/flag.js +15 -1
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +4 -1
- package/dist/components/flag/language-map.js +6 -3
- package/dist/components/gauge/gauge.d.ts +10 -1
- package/dist/components/gauge/gauge.js +28 -9
- package/dist/components/generation-status/generation-status.d.ts +13 -5
- package/dist/components/generation-status/generation-status.js +59 -5
- package/dist/components/graph/graph.d.ts +37 -3
- package/dist/components/graph/graph.js +132 -20
- package/dist/components/heatmap/calendar-grid.d.ts +15 -6
- package/dist/components/heatmap/calendar-grid.js +17 -6
- package/dist/components/heatmap/heatmap-scale.d.ts +5 -1
- package/dist/components/heatmap/heatmap-scale.js +8 -2
- package/dist/components/heatmap/heatmap.d.ts +166 -25
- package/dist/components/heatmap/heatmap.js +372 -89
- package/dist/components/json-viewer/json-viewer.d.ts +22 -5
- package/dist/components/json-viewer/json-viewer.js +115 -37
- package/dist/components/json-viewer/json-viewer.styles.js +2 -1
- package/dist/components/kbd/kbd.js +24 -7
- package/dist/components/live-region/live-region.d.ts +10 -1
- package/dist/components/live-region/live-region.js +42 -6
- package/dist/components/map/map.d.ts +24 -1
- package/dist/components/map/map.js +89 -24
- package/dist/components/markdown/markdown-loader.d.ts +24 -0
- package/dist/components/markdown/markdown-loader.js +35 -1
- package/dist/components/markdown/markdown.d.ts +46 -8
- package/dist/components/markdown/markdown.js +136 -31
- package/dist/components/markdown/markdown.styles.js +7 -1
- package/dist/components/media-card/media-card.d.ts +25 -17
- package/dist/components/media-card/media-card.js +43 -65
- package/dist/components/mention-popover/mention-popover.d.ts +4 -1
- package/dist/components/mention-popover/mention-popover.js +23 -5
- package/dist/components/menu/menu-item.d.ts +32 -3
- package/dist/components/menu/menu-item.js +94 -7
- package/dist/components/menu/menu-item.styles.js +8 -0
- package/dist/components/menu/menu.d.ts +25 -1
- package/dist/components/menu/menu.js +99 -3
- package/dist/components/menu/menu.styles.js +3 -2
- package/dist/components/model-select/model-select.d.ts +48 -3
- package/dist/components/model-select/model-select.js +120 -22
- package/dist/components/model-select/model-select.styles.js +17 -3
- package/dist/components/model-settings-panel/model-settings-panel.d.ts +25 -1
- package/dist/components/model-settings-panel/model-settings-panel.js +65 -1
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +72 -29
- package/dist/components/playback/playback.styles.js +6 -1
- package/dist/components/responsive-panel/responsive-panel.d.ts +29 -16
- package/dist/components/responsive-panel/responsive-panel.js +130 -132
- package/dist/components/responsive-panel/responsive-panel.styles.js +1 -1
- package/dist/components/result-card/result-card.d.ts +8 -2
- package/dist/components/result-card/result-card.js +9 -3
- package/dist/components/result-card/result-card.styles.js +0 -1
- package/dist/components/select/select.d.ts +59 -20
- package/dist/components/select/select.js +125 -43
- package/dist/components/skeleton/skeleton.d.ts +3 -0
- package/dist/components/skeleton/skeleton.js +7 -1
- package/dist/components/skeleton/skeleton.styles.js +5 -4
- package/dist/components/slider/slider.d.ts +57 -5
- package/dist/components/slider/slider.js +195 -80
- package/dist/components/source-card/source-card.d.ts +17 -5
- package/dist/components/source-card/source-card.js +55 -8
- package/dist/components/source-card/source-card.styles.js +1 -1
- package/dist/components/source-list/source-list.js +9 -1
- package/dist/components/sparkline/sparkline.js +34 -10
- package/dist/components/split/split.d.ts +141 -14
- package/dist/components/split/split.js +357 -52
- package/dist/components/split/split.styles.js +18 -1
- package/dist/components/stat/stat.d.ts +10 -4
- package/dist/components/stat/stat.js +46 -11
- package/dist/components/stream-status/stream-status.d.ts +27 -11
- package/dist/components/stream-status/stream-status.js +95 -13
- package/dist/components/streaming-text/streaming-text.d.ts +9 -3
- package/dist/components/streaming-text/streaming-text.js +25 -10
- package/dist/components/streaming-text/streaming-text.styles.js +3 -2
- package/dist/components/switch/switch.d.ts +52 -7
- package/dist/components/switch/switch.js +111 -40
- package/dist/components/switch/switch.styles.js +1 -1
- package/dist/components/table/table.d.ts +18 -3
- package/dist/components/table/table.js +88 -20
- package/dist/components/tabs/tabs.d.ts +23 -0
- package/dist/components/tabs/tabs.js +76 -12
- package/dist/components/tabs/tabs.styles.js +11 -0
- package/dist/components/thinking-panel/thinking-panel.d.ts +3 -1
- package/dist/components/thinking-panel/thinking-panel.js +34 -9
- package/dist/components/thinking-panel/thinking-panel.styles.js +6 -0
- package/dist/components/time-range/time-range.d.ts +59 -2
- package/dist/components/time-range/time-range.js +163 -67
- package/dist/components/toast/toast-item.d.ts +12 -3
- package/dist/components/toast/toast-item.js +90 -53
- package/dist/components/toast/toast-item.styles.js +59 -28
- package/dist/components/toast/toast.js +17 -5
- package/dist/components/toast/toaster.js +10 -1
- package/dist/components/tool-approval-dialog/tool-approval-dialog.d.ts +11 -10
- package/dist/components/tool-approval-dialog/tool-approval-dialog.js +50 -124
- package/dist/components/tool-approval-dialog/tool-approval-dialog.styles.js +1 -1
- package/dist/components/tool-call-chip/tool-call-chip.d.ts +18 -2
- package/dist/components/tool-call-chip/tool-call-chip.js +86 -25
- package/dist/components/tool-param-form/tool-param-form.d.ts +89 -28
- package/dist/components/tool-param-form/tool-param-form.js +350 -106
- package/dist/components/tool-result-dialog/tool-result-dialog.d.ts +27 -7
- package/dist/components/tool-result-dialog/tool-result-dialog.js +80 -115
- package/dist/components/tool-result-dialog/tool-result-dialog.styles.js +1 -1
- package/dist/components/tool-result-view/registry.d.ts +0 -8
- package/dist/components/tool-result-view/registry.js +7 -2
- package/dist/components/tool-result-view/tool-result-view.d.ts +6 -3
- package/dist/components/tool-result-view/tool-result-view.js +18 -4
- package/dist/components/tool-select-dialog/tool-select-dialog.d.ts +11 -11
- package/dist/components/tool-select-dialog/tool-select-dialog.js +44 -133
- package/dist/components/tool-select-dialog/tool-select-dialog.styles.js +13 -4
- package/dist/components/tree/tree-node.d.ts +4 -1
- package/dist/components/tree/tree-node.js +4 -1
- package/dist/components/tree/tree.d.ts +17 -8
- package/dist/components/tree/tree.js +21 -20
- package/dist/components/typing-indicator/typing-indicator.d.ts +12 -2
- package/dist/components/typing-indicator/typing-indicator.js +16 -4
- package/dist/components/typing-indicator/typing-indicator.styles.js +2 -2
- package/dist/components/virtual-list/virtual-list.d.ts +56 -19
- package/dist/components/virtual-list/virtual-list.js +184 -38
- package/dist/components/virtual-list/virtual-list.styles.js +12 -0
- package/dist/components/widget/widget.d.ts +5 -4
- package/dist/components/widget/widget.js +54 -120
- package/dist/components/widget/widget.styles.js +8 -2
- package/dist/components/word-cloud/word-cloud-layout.d.ts +7 -0
- package/dist/components/word-cloud/word-cloud-layout.js +20 -6
- package/dist/components/word-cloud/word-cloud.d.ts +22 -2
- package/dist/components/word-cloud/word-cloud.js +45 -6
- package/dist/components/word-cloud/word-cloud.styles.js +13 -1
- package/dist/internal/anchored-validity.d.ts +1 -0
- package/dist/internal/anchored-validity.js +86 -0
- package/dist/internal/form-associated.d.ts +7 -0
- package/dist/internal/form-associated.js +43 -4
- package/dist/internal/numbers.d.ts +12 -0
- package/dist/internal/numbers.js +26 -0
- package/dist/internal/overlay-manager.d.ts +51 -0
- package/dist/internal/overlay-manager.js +523 -0
- package/dist/internal/positioner.js +27 -3
- package/dist/internal/safe-url.d.ts +12 -0
- package/dist/internal/safe-url.js +41 -0
- package/dist/internal/tokens.styles.js +17 -11
- package/dist/lyra.d.ts +5 -2
- package/dist/lyra.js +3 -1
- package/llms-full.txt +970 -426
- package/llms.txt +5 -4
- package/package.json +19 -2
|
@@ -22,9 +22,22 @@ const FALLBACK_SCALE_HI = '#0969da';
|
|
|
22
22
|
const FALLBACK_LABEL_FONT = '10px sans-serif';
|
|
23
23
|
const CAL_PAD_LEFT = 28;
|
|
24
24
|
const CAL_LABEL_H = 16;
|
|
25
|
+
/** Calendar mode's original fixed cell size — now also the effective fallback
|
|
26
|
+
* the `cellSize` accessor uses in calendar mode when left unset (see its
|
|
27
|
+
* doc comment), so a consumer who never touches `cellSize` sees no change. */
|
|
25
28
|
const CAL_CELL = 11;
|
|
26
29
|
const CAL_GAP = 2;
|
|
30
|
+
/** Matrix mode's original fixed cell size — the effective fallback the
|
|
31
|
+
* `cellSize` accessor uses in matrix mode when left unset. */
|
|
32
|
+
const DEFAULT_MATRIX_CELL_SIZE = 22;
|
|
27
33
|
const DEFAULT_BUCKET_COUNT = 5;
|
|
34
|
+
/**
|
|
35
|
+
* A linear RGB ramp cannot contain more than 256 visually distinct integer
|
|
36
|
+
* steps on any channel. Keeping the bucket count within that bound avoids an
|
|
37
|
+
* untrusted attribute/property value turning the ramp into an unbounded
|
|
38
|
+
* allocation without discarding any useful color resolution.
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_BUCKET_COUNT = 256;
|
|
28
41
|
/** Ring stroke width for both the annotation overlay and the keyboard focus ring. */
|
|
29
42
|
const RING_LINE_WIDTH = 2;
|
|
30
43
|
const FALLBACK_FOCUS_RING_COLOR = '#0969da';
|
|
@@ -66,30 +79,26 @@ function warnInvalidColor(color) {
|
|
|
66
79
|
console.warn(`<lyra-heatmap> could not parse "${color}" (set via --lyra-heatmap-scale-lo/-hi) as a CSS ` +
|
|
67
80
|
'color; falling back to the default ramp endpoint.');
|
|
68
81
|
}
|
|
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
82
|
/**
|
|
77
|
-
* Normalizes
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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.
|
|
83
|
+
* Normalizes a bucket count to the safe, renderable range. Non-finite values
|
|
84
|
+
* restore the public default; finite values are floored and clamped to
|
|
85
|
+
* `[2, MAX_BUCKET_COUNT]`. Flooring keeps the ramp array's length in exact
|
|
86
|
+
* agreement with the count used by `quartileBucket()`.
|
|
85
87
|
*/
|
|
86
|
-
function
|
|
88
|
+
export function normalizeBucketCount(bucketCount) {
|
|
87
89
|
if (!Number.isFinite(bucketCount)) {
|
|
88
|
-
warnInvalidBucketCount(bucketCount);
|
|
89
90
|
return DEFAULT_BUCKET_COUNT;
|
|
90
91
|
}
|
|
91
|
-
return Math.max(2, Math.floor(bucketCount));
|
|
92
|
+
return Math.min(MAX_BUCKET_COUNT, Math.max(2, Math.floor(bucketCount)));
|
|
92
93
|
}
|
|
94
|
+
const bucketCountConverter = {
|
|
95
|
+
fromAttribute(value) {
|
|
96
|
+
return value === null ? DEFAULT_BUCKET_COUNT : normalizeBucketCount(Number(value));
|
|
97
|
+
},
|
|
98
|
+
toAttribute(value) {
|
|
99
|
+
return String(normalizeBucketCount(value));
|
|
100
|
+
},
|
|
101
|
+
};
|
|
93
102
|
/**
|
|
94
103
|
* Resolves any syntactically valid CSS `<color>` — hex, `rgb()`, `hsl()`,
|
|
95
104
|
* `oklch()`, a named color, etc. — to an `[r, g, b]` triple.
|
|
@@ -142,12 +151,16 @@ export function mixColor(fromColor, toColor, t) {
|
|
|
142
151
|
* - `"matrix"` (default): a `rowLabels` x `colLabels` grid of `values`. `-1`
|
|
143
152
|
* (or any non-finite value) is treated as "no data". `scale="sqrt"`
|
|
144
153
|
* compresses the ramp via `sqrtStep()` so one heavy cell doesn't wash out
|
|
145
|
-
* the rest; the default `"linear"` scale maps linearly instead.
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* `
|
|
149
|
-
*
|
|
150
|
-
*
|
|
154
|
+
* the rest; the default `"linear"` scale maps linearly instead.
|
|
155
|
+
* - `"calendar"`: a GitHub-style Sunday-Saturday (or `firstDayOfWeek`-anchored
|
|
156
|
+
* — see below) x week grid built from `days`. `scale` governs its
|
|
157
|
+
* bucketing too: the default `"linear"` buckets by `quartileBucket()`
|
|
158
|
+
* (today's original behavior, unchanged); `"sqrt"` instead compresses via
|
|
159
|
+
* the same `sqrtStep()` magnitude compression matrix mode uses, so one
|
|
160
|
+
* heavy day doesn't wash out the rest. As in matrix mode, a cell whose
|
|
161
|
+
* `value` is negative or non-finite is treated as "no data" rather than
|
|
162
|
+
* being bucketed — as is a grid position with no matching entry in `days`
|
|
163
|
+
* at all (a gap in a sparse calendar).
|
|
151
164
|
*
|
|
152
165
|
* The sequential color ramp's endpoints are read from the
|
|
153
166
|
* `--lyra-heatmap-scale-lo`/`-hi` custom properties (declared in
|
|
@@ -171,7 +184,21 @@ export function mixColor(fromColor, toColor, t) {
|
|
|
171
184
|
* week column — drawing, hit-testing, the focus ring, and month-label
|
|
172
185
|
* positioning all consult it consistently, so a consumer can pixel-align a
|
|
173
186
|
* calendar's week columns with a sibling chart's coordinate system. Unset
|
|
174
|
-
* (the default) keeps the original evenly-spaced formula.
|
|
187
|
+
* (the default) keeps the original evenly-spaced formula. `rowY` is its
|
|
188
|
+
* calendar-mode vertical analogue — overrides the y-origin computed for each
|
|
189
|
+
* weekday row, consulted consistently by drawing, hit-testing, and the focus
|
|
190
|
+
* ring via the private `rowYFor()` helper (mirroring `columnXFor()` exactly).
|
|
191
|
+
*
|
|
192
|
+
* `firstDayOfWeek` (calendar mode only, default `0`/Sunday, no-op in matrix
|
|
193
|
+
* mode) anchors the calendar grid at a different weekday — `0`-`6`, same
|
|
194
|
+
* numbering as `CalendarCellPos.weekday` (`0` Sunday .. `6` Saturday) —
|
|
195
|
+
* threaded into `buildCalendarGrid()`.
|
|
196
|
+
*
|
|
197
|
+
* `cellSize`/`fitToWidth` (previously matrix-mode only) also drive calendar
|
|
198
|
+
* mode's per-cell size: unset, calendar mode keeps today's original 11px
|
|
199
|
+
* cell size unchanged; explicitly set, the same fixed size (or, with
|
|
200
|
+
* `fitToWidth`, the same host-width-derived size matrix mode already
|
|
201
|
+
* supports) governs calendar mode's grid too.
|
|
175
202
|
*
|
|
176
203
|
* @customElement lyra-heatmap
|
|
177
204
|
* @event lyra-cell-click - Fired on click, or Enter/Space on the
|
|
@@ -179,7 +206,14 @@ export function mixColor(fromColor, toColor, t) {
|
|
|
179
206
|
* `detail: { date, value }` in calendar mode. `cellText` overrides the
|
|
180
207
|
* built-in English "Row X, Col Y: value" / "Mon DD: value" template used for
|
|
181
208
|
* both the hover tooltip and the keyboard live-region announcement.
|
|
182
|
-
* @csspart base
|
|
209
|
+
* @csspart base - The heatmap wrapper.
|
|
210
|
+
* @csspart canvas - The heatmap canvas.
|
|
211
|
+
* @csspart tooltip - The hover tooltip.
|
|
212
|
+
* @csspart live-region - The visually hidden keyboard announcement region.
|
|
213
|
+
* @csspart legend - The color legend.
|
|
214
|
+
* @csspart legend-lo - The low legend endpoint.
|
|
215
|
+
* @csspart legend-hi - The high legend endpoint.
|
|
216
|
+
* @csspart legend-annotation - An annotation label.
|
|
183
217
|
*/
|
|
184
218
|
export class LyraHeatmap extends LyraElement {
|
|
185
219
|
constructor() {
|
|
@@ -187,25 +221,57 @@ export class LyraHeatmap extends LyraElement {
|
|
|
187
221
|
this.rowLabels = [];
|
|
188
222
|
this.colLabels = [];
|
|
189
223
|
this.values = [];
|
|
190
|
-
this.cellSize = 22;
|
|
191
224
|
this.valueLabel = 'value';
|
|
225
|
+
/**
|
|
226
|
+
* `"linear"` (default) maps values linearly to the color ramp in matrix
|
|
227
|
+
* mode, and buckets calendar-mode values via `quartileBucket()` — both
|
|
228
|
+
* unchanged from before this property governed calendar mode too.
|
|
229
|
+
* `"sqrt"` compresses via `sqrtStep()`'s square-root magnitude compression
|
|
230
|
+
* instead, in *both* modes, so one heavy cell/day doesn't wash out the
|
|
231
|
+
* rest of a skewed dataset.
|
|
232
|
+
*/
|
|
192
233
|
this.scale = 'linear';
|
|
193
234
|
/**
|
|
194
235
|
* When set, `cellSize` is derived from the host's measured `clientWidth`
|
|
195
236
|
* on every draw (including ResizeObserver-triggered redraws) instead of
|
|
196
237
|
* the fixed `cell-size` attribute, so the grid actually fills the
|
|
197
238
|
* available width. Without this, canvas dimensions are computed purely
|
|
198
|
-
* from `PAD_LEFT + cols * cellSize
|
|
199
|
-
*
|
|
239
|
+
* from `PAD_LEFT + cols * cellSize` (matrix mode) or
|
|
240
|
+
* `CAL_PAD_LEFT + weekCount * cellSize` (calendar mode), so a
|
|
241
|
+
* resize-triggered redraw is a geometric no-op. Originally matrix-mode
|
|
242
|
+
* only; now applies to calendar mode too.
|
|
200
243
|
*/
|
|
201
244
|
this.fitToWidth = false;
|
|
202
245
|
this.mode = 'matrix';
|
|
203
246
|
this.days = [];
|
|
204
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Calendar mode only (no-op in matrix mode): anchors the calendar grid at
|
|
249
|
+
* a different weekday instead of always Sunday — `0`-`6`, same numbering
|
|
250
|
+
* as `CalendarCellPos.weekday` (`0` Sunday .. `6` Saturday, matching
|
|
251
|
+
* `CalendarCell.weekday`'s existing convention). Threaded into
|
|
252
|
+
* `buildCalendarGrid()`. Defaults to `0` (Sunday), unchanged from before
|
|
253
|
+
* this property existed.
|
|
254
|
+
*/
|
|
255
|
+
this.firstDayOfWeek = 0;
|
|
256
|
+
this._bucketCount = DEFAULT_BUCKET_COUNT;
|
|
205
257
|
/** Cells to ring-highlight — `row`/`col` in matrix mode, `date` in calendar mode. See `HeatmapAnnotation`. */
|
|
206
258
|
this.annotations = [];
|
|
259
|
+
this.themeRefreshQueued = false;
|
|
207
260
|
/** The current value range, refreshed once per update cycle by `willUpdate()`. See `computeValueRange()`. */
|
|
208
261
|
this.cachedValueRange = null;
|
|
262
|
+
/**
|
|
263
|
+
* The calendar-mode grid layout, refreshed by `willUpdate()` only when
|
|
264
|
+
* `days` actually changes (see `computeValueRange()`'s twin comment above)
|
|
265
|
+
* — `buildCalendarGrid()` parses every date, filters invalid ones, and does
|
|
266
|
+
* a full `.slice().sort()` for month labels, so recomputing it from
|
|
267
|
+
* `drawCalendar()`, `hitTestCalendar()`, `calendarCellAt()`, and
|
|
268
|
+
* `onCalendarKeyDown()` independently would redo that work 2-4x for a
|
|
269
|
+
* single hover/click/keydown.
|
|
270
|
+
*/
|
|
271
|
+
this.cachedCalendarGrid = buildCalendarGrid(this.days);
|
|
272
|
+
this.cachedCalendarSortedValues = [];
|
|
273
|
+
this.cachedCalendarCellsByPos = new Map();
|
|
274
|
+
this.cachedRamp = null;
|
|
209
275
|
/** The cell currently under the pointer (`null` when not hovering one) — drives `[part="tooltip"]`. */
|
|
210
276
|
this.hoverCell = null;
|
|
211
277
|
/** The roving keyboard-focus cell cursor, moved by arrow keys — drives the
|
|
@@ -230,6 +296,28 @@ export class LyraHeatmap extends LyraElement {
|
|
|
230
296
|
this.watchDpr();
|
|
231
297
|
this.draw();
|
|
232
298
|
};
|
|
299
|
+
this.onColorSchemeChange = () => {
|
|
300
|
+
this.refreshTheme();
|
|
301
|
+
};
|
|
302
|
+
this.queueThemeRefresh = () => {
|
|
303
|
+
if (this.themeRefreshQueued)
|
|
304
|
+
return;
|
|
305
|
+
this.themeRefreshQueued = true;
|
|
306
|
+
queueMicrotask(() => {
|
|
307
|
+
this.themeRefreshQueued = false;
|
|
308
|
+
if (this.isConnected)
|
|
309
|
+
this.refreshTheme();
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
this.scheduleDraw = () => {
|
|
313
|
+
if (this.drawRafId !== undefined)
|
|
314
|
+
return;
|
|
315
|
+
this.drawRafId = requestAnimationFrame(() => {
|
|
316
|
+
this.drawRafId = undefined;
|
|
317
|
+
if (this.isConnected)
|
|
318
|
+
this.draw();
|
|
319
|
+
});
|
|
320
|
+
};
|
|
233
321
|
this.onPointerMove = (e) => {
|
|
234
322
|
const next = this.hitTest(e.offsetX, e.offsetY);
|
|
235
323
|
if (!this.samePos(this.hoverCell, next))
|
|
@@ -258,16 +346,49 @@ export class LyraHeatmap extends LyraElement {
|
|
|
258
346
|
};
|
|
259
347
|
}
|
|
260
348
|
static { this.styles = [LyraElement.styles, styles, srOnly]; }
|
|
349
|
+
/**
|
|
350
|
+
* Effective per-cell size (CSS px). Originally matrix-mode only; now also
|
|
351
|
+
* governs calendar mode's cell size (replacing the previously hardcoded
|
|
352
|
+
* 11px constant there) once explicitly set. Left unset, each mode keeps
|
|
353
|
+
* its own original default — `DEFAULT_MATRIX_CELL_SIZE` (22) in matrix
|
|
354
|
+
* mode, `CAL_CELL` (11) in calendar mode — so an existing consumer who
|
|
355
|
+
* never touches `cellSize` sees no change in either mode. Set explicitly
|
|
356
|
+
* (attribute or property), the same value governs both modes alike.
|
|
357
|
+
*/
|
|
358
|
+
get cellSize() {
|
|
359
|
+
return this._cellSize ?? (this.mode === 'calendar' ? CAL_CELL : DEFAULT_MATRIX_CELL_SIZE);
|
|
360
|
+
}
|
|
361
|
+
set cellSize(value) {
|
|
362
|
+
const oldValue = this._cellSize;
|
|
363
|
+
this._cellSize = value;
|
|
364
|
+
this.requestUpdate('cellSize', oldValue);
|
|
365
|
+
}
|
|
366
|
+
get bucketCount() {
|
|
367
|
+
return this._bucketCount;
|
|
368
|
+
}
|
|
369
|
+
set bucketCount(value) {
|
|
370
|
+
const oldValue = this._bucketCount;
|
|
371
|
+
this._bucketCount = normalizeBucketCount(value);
|
|
372
|
+
this.requestUpdate('bucketCount', oldValue);
|
|
373
|
+
}
|
|
261
374
|
connectedCallback() {
|
|
262
375
|
super.connectedCallback();
|
|
263
|
-
this.resizeObserver = new ResizeObserver(
|
|
376
|
+
this.resizeObserver = new ResizeObserver(this.scheduleDraw);
|
|
264
377
|
this.resizeObserver.observe(this);
|
|
265
378
|
this.watchDpr();
|
|
379
|
+
this.watchTheme();
|
|
266
380
|
}
|
|
267
381
|
disconnectedCallback() {
|
|
268
382
|
super.disconnectedCallback();
|
|
269
383
|
this.resizeObserver?.disconnect();
|
|
270
384
|
this.dprQuery?.removeEventListener('change', this.onDprChange);
|
|
385
|
+
this.colorSchemeQuery?.removeEventListener('change', this.onColorSchemeChange);
|
|
386
|
+
this.themeObserver?.disconnect();
|
|
387
|
+
this.themeObserver = undefined;
|
|
388
|
+
if (this.drawRafId !== undefined) {
|
|
389
|
+
cancelAnimationFrame(this.drawRafId);
|
|
390
|
+
this.drawRafId = undefined;
|
|
391
|
+
}
|
|
271
392
|
}
|
|
272
393
|
watchDpr() {
|
|
273
394
|
// A MediaQueryList's `matches` is fixed at creation time, so crossing the
|
|
@@ -278,24 +399,69 @@ export class LyraHeatmap extends LyraElement {
|
|
|
278
399
|
this.dprQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
279
400
|
this.dprQuery.addEventListener('change', this.onDprChange);
|
|
280
401
|
}
|
|
402
|
+
watchTheme() {
|
|
403
|
+
const view = this.ownerDocument.defaultView;
|
|
404
|
+
if (!view)
|
|
405
|
+
return;
|
|
406
|
+
this.colorSchemeQuery = view.matchMedia?.('(prefers-color-scheme: dark)');
|
|
407
|
+
this.colorSchemeQuery?.addEventListener('change', this.onColorSchemeChange);
|
|
408
|
+
if (typeof MutationObserver === 'undefined')
|
|
409
|
+
return;
|
|
410
|
+
const targets = [this];
|
|
411
|
+
let parent = this.parentElement;
|
|
412
|
+
while (parent) {
|
|
413
|
+
targets.push(parent);
|
|
414
|
+
parent = parent.parentElement;
|
|
415
|
+
}
|
|
416
|
+
this.themeObserver = new MutationObserver(this.queueThemeRefresh);
|
|
417
|
+
for (const target of targets) {
|
|
418
|
+
this.themeObserver.observe(target, {
|
|
419
|
+
attributes: true,
|
|
420
|
+
attributeFilter: ['class', 'style', 'data-theme', 'data-color-scheme'],
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
281
424
|
willUpdate(changed) {
|
|
282
|
-
if (changed.has('mode') ||
|
|
425
|
+
if (changed.has('mode') ||
|
|
426
|
+
changed.has('rowLabels') ||
|
|
427
|
+
changed.has('colLabels') ||
|
|
428
|
+
changed.has('days') ||
|
|
429
|
+
changed.has('firstDayOfWeek')) {
|
|
283
430
|
// The previous focus/hover cursor may no longer address a real cell
|
|
284
431
|
// once the grid's shape (or mode) changes out from under it —
|
|
285
432
|
// stroking a focus ring at a stale (row, col)/(week, weekday), or
|
|
286
433
|
// leaving the live region announcing a stale value, would be actively
|
|
287
434
|
// misleading, so drop both instead. Reactive-property writes belong in
|
|
288
435
|
// willUpdate() (not updated()), which folds them into this same
|
|
289
|
-
// render rather than scheduling a whole extra update pass.
|
|
436
|
+
// render rather than scheduling a whole extra update pass. A
|
|
437
|
+
// `firstDayOfWeek` change reshuffles every cell's week/weekday the same
|
|
438
|
+
// way a `days` change does, so it resets the cursor too.
|
|
290
439
|
this.focusedCell = null;
|
|
291
440
|
this.hoverCell = null;
|
|
292
441
|
this.liveText = '';
|
|
293
442
|
}
|
|
443
|
+
// Calendar-mode grid layout depends only on `days`/`firstDayOfWeek` (not
|
|
444
|
+
// `mode`) — rebuilt here, once per cycle, instead of independently by
|
|
445
|
+
// every call site (see `cachedCalendarGrid`'s doc comment).
|
|
446
|
+
if (changed.has('days') || changed.has('firstDayOfWeek') || !this.hasUpdated) {
|
|
447
|
+
this.cachedCalendarGrid = buildCalendarGrid(this.days, this.firstDayOfWeek);
|
|
448
|
+
this.cachedCalendarSortedValues = this.cachedCalendarGrid.cells
|
|
449
|
+
.map((cell) => cell.value)
|
|
450
|
+
.filter((value) => Number.isFinite(value) && value >= 0)
|
|
451
|
+
.sort((a, b) => a - b);
|
|
452
|
+
this.cachedCalendarCellsByPos = new Map(this.cachedCalendarGrid.cells.map((cell) => [`${cell.week}:${cell.weekday}`, cell]));
|
|
453
|
+
}
|
|
294
454
|
if (!this.hasUpdated) {
|
|
295
455
|
this.authorSuppliedRole = this.hasAttribute('role');
|
|
296
456
|
this.authorSuppliedAriaLabel = this.hasAttribute('aria-label');
|
|
297
457
|
}
|
|
298
|
-
|
|
458
|
+
// A hover/keyboard-focus @state() change (hoverCell/focusedCell/liveText)
|
|
459
|
+
// triggers this same willUpdate() but never touches values/days/mode, so
|
|
460
|
+
// gating the O(rows*cols) rescan the same way the focus/hover reset above
|
|
461
|
+
// is gated avoids redoing it on every pointer move.
|
|
462
|
+
if (changed.has('values') || changed.has('days') || changed.has('mode') || !this.hasUpdated) {
|
|
463
|
+
this.cachedValueRange = this.computeValueRange();
|
|
464
|
+
}
|
|
299
465
|
const bounds = this.cachedValueRange;
|
|
300
466
|
const range = bounds ? `${bounds[0]}–${bounds[1]}` : 'no data';
|
|
301
467
|
// Only default role/aria-label when the author hasn't already supplied
|
|
@@ -331,7 +497,29 @@ export class LyraHeatmap extends LyraElement {
|
|
|
331
497
|
const source = this.mode === 'calendar' ? this.days.map((d) => d.value) : this.values.flat();
|
|
332
498
|
return minMax(source.filter((v) => Number.isFinite(v) && v >= 0));
|
|
333
499
|
}
|
|
334
|
-
updated() {
|
|
500
|
+
updated(changed) {
|
|
501
|
+
if ([
|
|
502
|
+
'values',
|
|
503
|
+
'rowLabels',
|
|
504
|
+
'colLabels',
|
|
505
|
+
'cellSize',
|
|
506
|
+
'valueLabel',
|
|
507
|
+
'scale',
|
|
508
|
+
'fitToWidth',
|
|
509
|
+
'mode',
|
|
510
|
+
'days',
|
|
511
|
+
'bucketCount',
|
|
512
|
+
'annotations',
|
|
513
|
+
'columnX',
|
|
514
|
+
'rowY',
|
|
515
|
+
'firstDayOfWeek',
|
|
516
|
+
'focusedCell',
|
|
517
|
+
].some((name) => changed.has(name))) {
|
|
518
|
+
this.draw();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/** Redraws canvas content after an upstream token or theme change. */
|
|
522
|
+
refreshTheme() {
|
|
335
523
|
this.draw();
|
|
336
524
|
}
|
|
337
525
|
/** Reads the customizable ramp endpoints off the host's computed style. */
|
|
@@ -367,17 +555,63 @@ export class LyraHeatmap extends LyraElement {
|
|
|
367
555
|
else
|
|
368
556
|
this.drawMatrix();
|
|
369
557
|
}
|
|
558
|
+
colorRamp(bucketCount) {
|
|
559
|
+
const [scaleLo, scaleHi] = this.scaleEndpoints();
|
|
560
|
+
const normalizedBucketCount = normalizeBucketCount(bucketCount);
|
|
561
|
+
const key = `${scaleLo}\u0000${scaleHi}\u0000${normalizedBucketCount}`;
|
|
562
|
+
if (this.cachedRamp?.key === key)
|
|
563
|
+
return this.cachedRamp;
|
|
564
|
+
const loRgb = resolveRgb(scaleLo, FALLBACK_SCALE_LO);
|
|
565
|
+
const hiRgb = resolveRgb(scaleHi, FALLBACK_SCALE_HI);
|
|
566
|
+
const colors = Array.from({ length: normalizedBucketCount }, (_, i) => mixRgb(loRgb, hiRgb, i / (normalizedBucketCount - 1)));
|
|
567
|
+
this.cachedRamp = { key, colors, loRgb, hiRgb };
|
|
568
|
+
return this.cachedRamp;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Effective per-cell size in calendar mode — mirrors `matrixCellSize()`
|
|
572
|
+
* exactly: `fitToWidth` derives it from the host's measured width,
|
|
573
|
+
* otherwise it's the (possibly explicitly-set) `cellSize` property, which
|
|
574
|
+
* itself falls back to today's original 11px calendar default when left
|
|
575
|
+
* unset (see `cellSize`'s accessor doc comment). Shared by `columnXFor()`,
|
|
576
|
+
* `rowYFor()`, `drawCalendar()`, and the hit-testing below (`weekAtX()`,
|
|
577
|
+
* `weekdayAtY()`, `cellRect()`) so they always agree on exactly the same
|
|
578
|
+
* geometry as what's actually painted.
|
|
579
|
+
*/
|
|
580
|
+
calendarCellSize() {
|
|
581
|
+
const { weekCount } = this.cachedCalendarGrid;
|
|
582
|
+
if (this.fitToWidth && weekCount > 0) {
|
|
583
|
+
// Unlike matrix mode's contiguous cells, calendar columns are spaced
|
|
584
|
+
// `cellSize + CAL_GAP` apart (see `columnXFor()`), so the gap has to be
|
|
585
|
+
// subtracted back out here — otherwise the painted width would overshoot
|
|
586
|
+
// `hostWidth` by `weekCount * CAL_GAP` (every column's gap, once each).
|
|
587
|
+
const hostWidth = this.clientWidth || CAL_PAD_LEFT + weekCount * (this.cellSize + CAL_GAP);
|
|
588
|
+
return Math.max(4, (hostWidth - CAL_PAD_LEFT) / weekCount - CAL_GAP);
|
|
589
|
+
}
|
|
590
|
+
return this.cellSize;
|
|
591
|
+
}
|
|
370
592
|
/**
|
|
371
593
|
* Calendar-mode week-column x-origin (canvas-local CSS px) — `columnX(week)`
|
|
372
|
-
* when set, otherwise the original evenly-spaced formula
|
|
373
|
-
*
|
|
594
|
+
* when set, otherwise the original evenly-spaced formula (now derived from
|
|
595
|
+
* `calendarCellSize()` rather than the fixed `CAL_CELL` constant). Shared
|
|
596
|
+
* by every calendar-mode drawing and hit-testing call site (`drawCalendar()`,
|
|
374
597
|
* `hitTestCalendar()`/`weekAtX()`, `cellRect()`) so painted geometry and
|
|
375
598
|
* interactive hit-testing never disagree — mirrors the existing
|
|
376
599
|
* `matrixCellSize()` invariant for matrix mode's
|
|
377
600
|
* `drawMatrix()`/`hitTestMatrix()`/`cellRect()`.
|
|
378
601
|
*/
|
|
379
602
|
columnXFor(week) {
|
|
380
|
-
return this.columnX ? this.columnX(week) : CAL_PAD_LEFT + week * (
|
|
603
|
+
return this.columnX ? this.columnX(week) : CAL_PAD_LEFT + week * (this.calendarCellSize() + CAL_GAP);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Calendar-mode weekday-row y-origin (canvas-local CSS px) — `rowY(weekday)`
|
|
607
|
+
* when set, otherwise the original evenly-spaced formula (now derived from
|
|
608
|
+
* `calendarCellSize()` rather than the fixed `CAL_CELL` constant). The
|
|
609
|
+
* vertical analogue of `columnXFor()`, mirroring its exact
|
|
610
|
+
* dispatch-with-computed-fallback shape and shared by the same
|
|
611
|
+
* drawing/hit-testing/focus-ring call sites.
|
|
612
|
+
*/
|
|
613
|
+
rowYFor(weekday) {
|
|
614
|
+
return this.rowY ? this.rowY(weekday) : CAL_LABEL_H + weekday * (this.calendarCellSize() + CAL_GAP);
|
|
381
615
|
}
|
|
382
616
|
/**
|
|
383
617
|
* Inverse of `columnXFor()`: resolves an x position (canvas-local CSS px)
|
|
@@ -391,28 +625,51 @@ export class LyraHeatmap extends LyraElement {
|
|
|
391
625
|
*/
|
|
392
626
|
weekAtX(x, weekCount) {
|
|
393
627
|
if (!this.columnX) {
|
|
394
|
-
const
|
|
628
|
+
const cellSize = this.calendarCellSize();
|
|
629
|
+
const week = Math.floor((x - CAL_PAD_LEFT) / (cellSize + CAL_GAP));
|
|
395
630
|
return week >= 0 && week < weekCount ? week : null;
|
|
396
631
|
}
|
|
397
632
|
for (let week = 0; week < weekCount; week++) {
|
|
398
633
|
const start = this.columnXFor(week);
|
|
399
|
-
const end = week + 1 < weekCount ? this.columnXFor(week + 1) : start + (
|
|
634
|
+
const end = week + 1 < weekCount ? this.columnXFor(week + 1) : start + (this.calendarCellSize() + CAL_GAP);
|
|
400
635
|
if (x >= start && x < end)
|
|
401
636
|
return week;
|
|
402
637
|
}
|
|
403
638
|
return null;
|
|
404
639
|
}
|
|
405
640
|
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* (
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
641
|
+
* Inverse of `rowYFor()`: resolves a y position (canvas-local CSS px) to
|
|
642
|
+
* the weekday row it falls in (0-6), or `null` if it's outside every row.
|
|
643
|
+
* Mirrors `weekAtX()`'s closed-form-vs-scan split for the same reason: the
|
|
644
|
+
* default spacing has a closed-form inverse; an arbitrary `rowY` override
|
|
645
|
+
* doesn't.
|
|
646
|
+
*/
|
|
647
|
+
weekdayAtY(y) {
|
|
648
|
+
if (!this.rowY) {
|
|
649
|
+
const cellSize = this.calendarCellSize();
|
|
650
|
+
const weekday = Math.floor((y - CAL_LABEL_H) / (cellSize + CAL_GAP));
|
|
651
|
+
return weekday >= 0 && weekday < 7 ? weekday : null;
|
|
652
|
+
}
|
|
653
|
+
for (let weekday = 0; weekday < 7; weekday++) {
|
|
654
|
+
const start = this.rowYFor(weekday);
|
|
655
|
+
const end = weekday + 1 < 7 ? this.rowYFor(weekday + 1) : start + (this.calendarCellSize() + CAL_GAP);
|
|
656
|
+
if (y >= start && y < end)
|
|
657
|
+
return weekday;
|
|
658
|
+
}
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Locale-derived weekday-axis labels for the 7 rows drawn down the left of
|
|
663
|
+
* the calendar grid. Only indices 1, 3, and 5 (relative to `firstWeekStart`)
|
|
664
|
+
* are filled in — matching today's sparse every-other-day label density —
|
|
665
|
+
* the rest stay blank. `firstWeekStart` is always the UTC
|
|
666
|
+
* `firstDayOfWeek`-weekday by construction (see `buildCalendarGrid()`), so
|
|
667
|
+
* weekday index 0..6 is a fixed positional mapping relative to it (with the
|
|
668
|
+
* default `firstDayOfWeek` of 0, that's the original Sun..Sat mapping,
|
|
669
|
+
* unchanged); only the label *text* for indices 1/3/5 is derived here, via
|
|
670
|
+
* `Intl.DateTimeFormat` on a real UTC date that actually falls on that
|
|
671
|
+
* weekday, so it follows the runtime locale instead of a hardcoded English
|
|
672
|
+
* array (see `calendarCellText()`'s tooltip text for the same pattern).
|
|
416
673
|
*/
|
|
417
674
|
weekdayLabels(firstWeekStart) {
|
|
418
675
|
const formatter = new Intl.DateTimeFormat(undefined, { weekday: 'short', timeZone: 'UTC' });
|
|
@@ -425,9 +682,14 @@ export class LyraHeatmap extends LyraElement {
|
|
|
425
682
|
drawCalendar() {
|
|
426
683
|
if (!this.canvas)
|
|
427
684
|
return;
|
|
428
|
-
const { cells, weekCount, firstWeekStart, monthLabels } =
|
|
685
|
+
const { cells, weekCount, firstWeekStart, monthLabels } = this.cachedCalendarGrid;
|
|
686
|
+
const cellSize = this.calendarCellSize();
|
|
429
687
|
const w = this.columnXFor(Math.max(1, weekCount));
|
|
430
|
-
|
|
688
|
+
// Derived from `rowYFor(7)` (one past the last of the 7 weekday rows), the
|
|
689
|
+
// vertical mirror of `w`'s `columnXFor(weekCount)` — so a custom `rowY`
|
|
690
|
+
// that spaces rows out further than the default formula still gets a
|
|
691
|
+
// canvas tall enough to paint every row, instead of clipping them.
|
|
692
|
+
const h = this.rowYFor(7);
|
|
431
693
|
const dpr = window.devicePixelRatio || 1;
|
|
432
694
|
this.canvas.width = w * dpr;
|
|
433
695
|
this.canvas.height = h * dpr;
|
|
@@ -438,24 +700,39 @@ export class LyraHeatmap extends LyraElement {
|
|
|
438
700
|
return;
|
|
439
701
|
ctx.scale(dpr, dpr);
|
|
440
702
|
ctx.clearRect(0, 0, w, h);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const [scaleLo, scaleHi] = this.scaleEndpoints();
|
|
446
|
-
const loRgb = resolveRgb(scaleLo, FALLBACK_SCALE_LO);
|
|
447
|
-
const hiRgb = resolveRgb(scaleHi, FALLBACK_SCALE_HI);
|
|
448
|
-
const buckets = resolveBucketCount(this.bucketCount);
|
|
449
|
-
const ramp = Array.from({ length: buckets }, (_, i) => mixRgb(loRgb, hiRgb, i / (buckets - 1)));
|
|
703
|
+
// Normalize at the allocation boundary as a final guard even though the
|
|
704
|
+
// public accessor and attribute converter already normalize their inputs.
|
|
705
|
+
const buckets = normalizeBucketCount(this.bucketCount);
|
|
706
|
+
const ramp = this.colorRamp(buckets).colors;
|
|
450
707
|
const noDataFill = this.noDataFill();
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
708
|
+
// Only consulted when `scale === 'sqrt'` — mirrors `drawMatrix()`'s own
|
|
709
|
+
// `hi` bound, reusing the already-cached value range instead of
|
|
710
|
+
// rescanning `sortedValues` for its max.
|
|
711
|
+
const bounds = this.cachedValueRange;
|
|
712
|
+
const hi = bounds ? bounds[1] : 1;
|
|
713
|
+
// Indexed once per draw so every (week, weekday) grid position can be
|
|
714
|
+
// painted below, not just positions with a matching entry in `cells` — a
|
|
715
|
+
// day missing from `days` entirely (a gap in a sparse calendar) still
|
|
716
|
+
// gets a real grid cell (see `calendarCellAt()`'s doc comment for the
|
|
717
|
+
// same `-1` no-data sentinel used here) instead of being left
|
|
718
|
+
// transparent.
|
|
719
|
+
for (let week = 0; week < weekCount; week++) {
|
|
720
|
+
for (let weekday = 0; weekday < 7; weekday++) {
|
|
721
|
+
const value = this.cachedCalendarCellsByPos.get(`${week}:${weekday}`)?.value ?? -1;
|
|
722
|
+
const x = this.columnXFor(week);
|
|
723
|
+
const y = this.rowYFor(weekday);
|
|
724
|
+
if (value < 0 || !Number.isFinite(value)) {
|
|
725
|
+
ctx.fillStyle = noDataFill;
|
|
726
|
+
}
|
|
727
|
+
else if (this.scale === 'sqrt') {
|
|
728
|
+
const step = sqrtStep(value, hi, buckets);
|
|
729
|
+
ctx.fillStyle = step < 0 ? noDataFill : ramp[step];
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
ctx.fillStyle = ramp[quartileBucket(value, this.cachedCalendarSortedValues, buckets)];
|
|
733
|
+
}
|
|
734
|
+
ctx.fillRect(x, y, cellSize, cellSize);
|
|
735
|
+
}
|
|
459
736
|
}
|
|
460
737
|
// Annotation ring overlay, stroked after the fill pass so it reads
|
|
461
738
|
// clearly over the data-driven cell color — see drawMatrix()'s twin of
|
|
@@ -470,8 +747,8 @@ export class LyraHeatmap extends LyraElement {
|
|
|
470
747
|
if (!match)
|
|
471
748
|
continue;
|
|
472
749
|
const x = this.columnXFor(match.week);
|
|
473
|
-
const y =
|
|
474
|
-
ctx.strokeRect(x + 0.5, y + 0.5,
|
|
750
|
+
const y = this.rowYFor(match.weekday);
|
|
751
|
+
ctx.strokeRect(x + 0.5, y + 0.5, cellSize - 1, cellSize - 1);
|
|
475
752
|
}
|
|
476
753
|
}
|
|
477
754
|
// Keyboard focus ring, redrawn on top of the fill pass (and any
|
|
@@ -482,8 +759,8 @@ export class LyraHeatmap extends LyraElement {
|
|
|
482
759
|
ctx.lineWidth = RING_LINE_WIDTH;
|
|
483
760
|
ctx.strokeStyle = this.focusRingColor();
|
|
484
761
|
const x = this.columnXFor(week);
|
|
485
|
-
const y =
|
|
486
|
-
ctx.strokeRect(x + 0.5, y + 0.5,
|
|
762
|
+
const y = this.rowYFor(weekday);
|
|
763
|
+
ctx.strokeRect(x + 0.5, y + 0.5, cellSize - 1, cellSize - 1);
|
|
487
764
|
}
|
|
488
765
|
}
|
|
489
766
|
ctx.fillStyle = this.labelColor();
|
|
@@ -494,7 +771,7 @@ export class LyraHeatmap extends LyraElement {
|
|
|
494
771
|
const WEEKDAY_LABELS = this.weekdayLabels(firstWeekStart);
|
|
495
772
|
WEEKDAY_LABELS.forEach((label, weekday) => {
|
|
496
773
|
if (label)
|
|
497
|
-
ctx.fillText(label, 2,
|
|
774
|
+
ctx.fillText(label, 2, this.rowYFor(weekday) + cellSize - 1);
|
|
498
775
|
});
|
|
499
776
|
}
|
|
500
777
|
/**
|
|
@@ -532,10 +809,9 @@ export class LyraHeatmap extends LyraElement {
|
|
|
532
809
|
const bounds = this.cachedValueRange;
|
|
533
810
|
const lo = bounds ? bounds[0] : 0;
|
|
534
811
|
const hi = bounds ? bounds[1] : 1;
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const hiRgb =
|
|
538
|
-
const ramp = Array.from({ length: RAMP_STEPS }, (_, i) => mixRgb(loRgb, hiRgb, i / (RAMP_STEPS - 1)));
|
|
812
|
+
const rampData = this.colorRamp(RAMP_STEPS);
|
|
813
|
+
const ramp = rampData.colors;
|
|
814
|
+
const { loRgb, hiRgb } = rampData;
|
|
539
815
|
const noDataFill = this.noDataFill();
|
|
540
816
|
for (let r = 0; r < rows; r++) {
|
|
541
817
|
for (let c = 0; c < cols; c++) {
|
|
@@ -627,12 +903,12 @@ export class LyraHeatmap extends LyraElement {
|
|
|
627
903
|
return { row, col };
|
|
628
904
|
}
|
|
629
905
|
hitTestCalendar(x, y) {
|
|
630
|
-
const { weekCount } =
|
|
906
|
+
const { weekCount } = this.cachedCalendarGrid;
|
|
631
907
|
if (weekCount === 0)
|
|
632
908
|
return null;
|
|
633
909
|
const week = this.weekAtX(x, weekCount);
|
|
634
|
-
const weekday =
|
|
635
|
-
if (week === null || weekday
|
|
910
|
+
const weekday = this.weekdayAtY(y);
|
|
911
|
+
if (week === null || weekday === null)
|
|
636
912
|
return null;
|
|
637
913
|
return { week, weekday };
|
|
638
914
|
}
|
|
@@ -645,7 +921,7 @@ export class LyraHeatmap extends LyraElement {
|
|
|
645
921
|
* sentinel uses in matrix mode), instead of being unresolvable.
|
|
646
922
|
*/
|
|
647
923
|
calendarCellAt(pos) {
|
|
648
|
-
const { cells, firstWeekStart } =
|
|
924
|
+
const { cells, firstWeekStart } = this.cachedCalendarGrid;
|
|
649
925
|
const match = cells.find((c) => c.week === pos.week && c.weekday === pos.weekday);
|
|
650
926
|
if (match)
|
|
651
927
|
return { date: match.date, value: match.value };
|
|
@@ -659,11 +935,12 @@ export class LyraHeatmap extends LyraElement {
|
|
|
659
935
|
*/
|
|
660
936
|
cellRect(pos) {
|
|
661
937
|
if ('week' in pos) {
|
|
938
|
+
const cellSize = this.calendarCellSize();
|
|
662
939
|
return {
|
|
663
940
|
x: this.columnXFor(pos.week),
|
|
664
|
-
y:
|
|
665
|
-
w:
|
|
666
|
-
h:
|
|
941
|
+
y: this.rowYFor(pos.weekday),
|
|
942
|
+
w: cellSize,
|
|
943
|
+
h: cellSize,
|
|
667
944
|
};
|
|
668
945
|
}
|
|
669
946
|
const cellSize = this.matrixCellSize(this.colLabels.length);
|
|
@@ -777,7 +1054,7 @@ export class LyraHeatmap extends LyraElement {
|
|
|
777
1054
|
this.announce(next);
|
|
778
1055
|
}
|
|
779
1056
|
onCalendarKeyDown(e) {
|
|
780
|
-
const { weekCount } =
|
|
1057
|
+
const { weekCount } = this.cachedCalendarGrid;
|
|
781
1058
|
if (weekCount === 0)
|
|
782
1059
|
return;
|
|
783
1060
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
@@ -851,7 +1128,7 @@ __decorate([
|
|
|
851
1128
|
], LyraHeatmap.prototype, "values", void 0);
|
|
852
1129
|
__decorate([
|
|
853
1130
|
property({ type: Number, attribute: 'cell-size' })
|
|
854
|
-
], LyraHeatmap.prototype, "cellSize",
|
|
1131
|
+
], LyraHeatmap.prototype, "cellSize", null);
|
|
855
1132
|
__decorate([
|
|
856
1133
|
property({ attribute: 'value-label' })
|
|
857
1134
|
], LyraHeatmap.prototype, "valueLabel", void 0);
|
|
@@ -868,8 +1145,11 @@ __decorate([
|
|
|
868
1145
|
property({ attribute: false })
|
|
869
1146
|
], LyraHeatmap.prototype, "days", void 0);
|
|
870
1147
|
__decorate([
|
|
871
|
-
property({ attribute: '
|
|
872
|
-
], LyraHeatmap.prototype, "
|
|
1148
|
+
property({ type: Number, attribute: 'first-day-of-week' })
|
|
1149
|
+
], LyraHeatmap.prototype, "firstDayOfWeek", void 0);
|
|
1150
|
+
__decorate([
|
|
1151
|
+
property({ attribute: 'bucket-count', converter: bucketCountConverter })
|
|
1152
|
+
], LyraHeatmap.prototype, "bucketCount", null);
|
|
873
1153
|
__decorate([
|
|
874
1154
|
property({ attribute: false })
|
|
875
1155
|
], LyraHeatmap.prototype, "annotations", void 0);
|
|
@@ -879,6 +1159,9 @@ __decorate([
|
|
|
879
1159
|
__decorate([
|
|
880
1160
|
property({ attribute: false })
|
|
881
1161
|
], LyraHeatmap.prototype, "columnX", void 0);
|
|
1162
|
+
__decorate([
|
|
1163
|
+
property({ attribute: false })
|
|
1164
|
+
], LyraHeatmap.prototype, "rowY", void 0);
|
|
882
1165
|
__decorate([
|
|
883
1166
|
query('canvas')
|
|
884
1167
|
], LyraHeatmap.prototype, "canvas", void 0);
|