@aceshooting/lyra-ui 0.1.1 → 0.1.2

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.
Files changed (117) hide show
  1. package/README.md +77 -6
  2. package/custom-elements.json +12015 -0
  3. package/dist/components/chart/bar-chart.d.ts +14 -0
  4. package/dist/components/chart/bar-chart.js +14 -0
  5. package/dist/components/chart/box-plot.d.ts +46 -0
  6. package/dist/components/chart/box-plot.js +145 -0
  7. package/dist/components/chart/box-plot.styles.d.ts +1 -0
  8. package/dist/components/chart/box-plot.styles.js +22 -0
  9. package/dist/components/chart/bubble-chart.d.ts +19 -0
  10. package/dist/components/chart/bubble-chart.js +18 -0
  11. package/dist/components/chart/chart-loader.d.ts +8 -0
  12. package/dist/components/chart/chart-loader.js +26 -0
  13. package/dist/components/chart/chart.d.ts +91 -0
  14. package/dist/components/chart/chart.js +275 -0
  15. package/dist/components/chart/chart.styles.d.ts +1 -0
  16. package/dist/components/chart/chart.styles.js +33 -0
  17. package/dist/components/chart/doughnut-chart.d.ts +15 -0
  18. package/dist/components/chart/doughnut-chart.js +14 -0
  19. package/dist/components/chart/histogram-bin.d.ts +6 -0
  20. package/dist/components/chart/histogram-bin.js +19 -0
  21. package/dist/components/chart/histogram.d.ts +20 -0
  22. package/dist/components/chart/histogram.js +70 -0
  23. package/dist/components/chart/histogram.styles.d.ts +1 -0
  24. package/dist/components/chart/histogram.styles.js +1 -0
  25. package/dist/components/chart/line-chart.d.ts +14 -0
  26. package/dist/components/chart/line-chart.js +14 -0
  27. package/dist/components/chart/pie-chart.d.ts +16 -0
  28. package/dist/components/chart/pie-chart.js +15 -0
  29. package/dist/components/chart/polar-area-chart.d.ts +15 -0
  30. package/dist/components/chart/polar-area-chart.js +14 -0
  31. package/dist/components/chart/radar-chart.d.ts +15 -0
  32. package/dist/components/chart/radar-chart.js +14 -0
  33. package/dist/components/chart/scatter-chart.d.ts +15 -0
  34. package/dist/components/chart/scatter-chart.js +15 -0
  35. package/dist/components/combobox/combobox.d.ts +16 -0
  36. package/dist/components/combobox/combobox.js +113 -6
  37. package/dist/components/combobox/combobox.styles.js +68 -7
  38. package/dist/components/combobox/option.d.ts +2 -2
  39. package/dist/components/combobox/option.js +11 -7
  40. package/dist/components/date-picker/date-input.d.ts +12 -1
  41. package/dist/components/date-picker/date-input.js +68 -6
  42. package/dist/components/date-picker/date-input.styles.js +58 -6
  43. package/dist/components/date-picker/date-picker.js +5 -2
  44. package/dist/components/date-picker/date-picker.styles.js +9 -2
  45. package/dist/components/empty/empty.d.ts +1 -0
  46. package/dist/components/empty/empty.js +14 -2
  47. package/dist/components/export-button/csv.d.ts +10 -0
  48. package/dist/components/export-button/csv.js +28 -0
  49. package/dist/components/export-button/export-button.d.ts +41 -0
  50. package/dist/components/export-button/export-button.js +151 -0
  51. package/dist/components/export-button/export-button.styles.d.ts +1 -0
  52. package/dist/components/export-button/export-button.styles.js +79 -0
  53. package/dist/components/file-input/accept.d.ts +14 -0
  54. package/dist/components/file-input/accept.js +30 -0
  55. package/dist/components/file-input/file-input.d.ts +47 -0
  56. package/dist/components/file-input/file-input.js +182 -0
  57. package/dist/components/file-input/file-input.styles.d.ts +1 -0
  58. package/dist/components/file-input/file-input.styles.js +44 -0
  59. package/dist/components/flag/flag.d.ts +4 -0
  60. package/dist/components/flag/flag.js +24 -1
  61. package/dist/components/flag/flag.styles.js +1 -1
  62. package/dist/components/gauge/gauge.d.ts +29 -0
  63. package/dist/components/gauge/gauge.js +133 -0
  64. package/dist/components/gauge/gauge.styles.d.ts +1 -0
  65. package/dist/components/gauge/gauge.styles.js +60 -0
  66. package/dist/components/graph/graph.d.ts +68 -0
  67. package/dist/components/graph/graph.js +276 -0
  68. package/dist/components/graph/graph.styles.d.ts +1 -0
  69. package/dist/components/graph/graph.styles.js +34 -0
  70. package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
  71. package/dist/components/heatmap/heatmap-scale.js +17 -0
  72. package/dist/components/heatmap/heatmap.d.ts +66 -0
  73. package/dist/components/heatmap/heatmap.js +252 -0
  74. package/dist/components/heatmap/heatmap.styles.d.ts +1 -0
  75. package/dist/components/heatmap/heatmap.styles.js +39 -0
  76. package/dist/components/map/map-loader.d.ts +8 -0
  77. package/dist/components/map/map-loader.js +17 -0
  78. package/dist/components/map/map.d.ts +49 -0
  79. package/dist/components/map/map.js +189 -0
  80. package/dist/components/map/map.styles.d.ts +1 -0
  81. package/dist/components/map/map.styles.js +48 -0
  82. package/dist/components/playback/playback.d.ts +51 -0
  83. package/dist/components/playback/playback.js +146 -0
  84. package/dist/components/playback/playback.styles.d.ts +1 -0
  85. package/dist/components/playback/playback.styles.js +40 -0
  86. package/dist/components/split/split.d.ts +41 -0
  87. package/dist/components/split/split.js +171 -0
  88. package/dist/components/split/split.styles.d.ts +1 -0
  89. package/dist/components/split/split.styles.js +51 -0
  90. package/dist/components/stat/stat.d.ts +9 -0
  91. package/dist/components/stat/stat.js +52 -7
  92. package/dist/components/stat/stat.styles.js +15 -5
  93. package/dist/components/table/table.d.ts +18 -6
  94. package/dist/components/table/table.js +76 -24
  95. package/dist/components/table/table.styles.js +32 -0
  96. package/dist/components/time-range/time-range.d.ts +40 -0
  97. package/dist/components/time-range/time-range.js +219 -0
  98. package/dist/components/time-range/time-range.styles.d.ts +1 -0
  99. package/dist/components/time-range/time-range.styles.js +78 -0
  100. package/dist/components/toast/toast-item.js +10 -2
  101. package/dist/components/toast/toast-item.styles.js +13 -4
  102. package/dist/components/toast/toast.styles.js +0 -1
  103. package/dist/components/tree/tree-node.d.ts +50 -0
  104. package/dist/components/tree/tree-node.js +196 -0
  105. package/dist/components/tree/tree.d.ts +65 -0
  106. package/dist/components/tree/tree.js +227 -0
  107. package/dist/components/tree/tree.styles.d.ts +1 -0
  108. package/dist/components/tree/tree.styles.js +13 -0
  109. package/dist/internal/form-associated.js +49 -1
  110. package/dist/internal/icons.d.ts +11 -0
  111. package/dist/internal/icons.js +64 -0
  112. package/dist/internal/tokens.styles.js +41 -1
  113. package/dist/lyra.d.ts +67 -0
  114. package/dist/lyra.js +55 -0
  115. package/llms-full.txt +1425 -0
  116. package/llms.txt +47 -0
  117. package/package.json +53 -4
@@ -0,0 +1,276 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { html, svg } from 'lit';
8
+ import { property, state } from 'lit/decorators.js';
9
+ import { LyraElement } from '../../internal/lyra-element.js';
10
+ import { defineElement } from '../../internal/prefix.js';
11
+ import { styles } from './graph.styles.js';
12
+ import '../skeleton/skeleton.js';
13
+ let d3Modules;
14
+ /**
15
+ * Lazily loads the optional peer dependencies `d3-force`/`d3-drag`/`d3-zoom`/
16
+ * `d3-selection` once per page. Resolves to `null` (with a one-time warning)
17
+ * if they aren't installed — mirrors `<lyra-flag>`'s peer-dependency pattern.
18
+ */
19
+ function loadD3() {
20
+ if (!d3Modules) {
21
+ d3Modules = Promise.all([
22
+ import('d3-force'),
23
+ import('d3-drag'),
24
+ import('d3-zoom'),
25
+ import('d3-selection'),
26
+ ])
27
+ .then(([force, dragMod, zoomMod, selectionMod]) => ({
28
+ forceSimulation: force.forceSimulation,
29
+ forceLink: force.forceLink,
30
+ forceManyBody: force.forceManyBody,
31
+ forceCenter: force.forceCenter,
32
+ forceCollide: force.forceCollide,
33
+ drag: dragMod.drag,
34
+ zoom: zoomMod.zoom,
35
+ select: selectionMod.select,
36
+ }))
37
+ .catch(() => {
38
+ console.warn('<lyra-graph> needs the optional peer dependencies `d3-force`, `d3-drag`, ' +
39
+ '`d3-zoom`, and `d3-selection` — install them with `pnpm add d3-force d3-drag d3-zoom d3-selection`.');
40
+ return null;
41
+ });
42
+ }
43
+ return d3Modules;
44
+ }
45
+ const MIN_RADIUS = 6;
46
+ const MAX_RADIUS = 24;
47
+ /**
48
+ * `<lyra-graph>` — a force-directed node-link diagram with pan/zoom/drag.
49
+ * Requires the optional peer deps `d3-force`/`d3-drag`/`d3-zoom`/`d3-selection`
50
+ * (lazy-loaded; a consumer who never uses this component pays zero d3 cost).
51
+ *
52
+ * @customElement lyra-graph
53
+ * @event lyra-node-click - `detail: { id }`.
54
+ * @event lyra-link-click - `detail: { source, target }`.
55
+ * @csspart base, svg, node, link, label
56
+ */
57
+ export class LyraGraph extends LyraElement {
58
+ constructor() {
59
+ super(...arguments);
60
+ this.nodes = [];
61
+ this.links = [];
62
+ this.width = 800;
63
+ this.height = 600;
64
+ this.chargeStrength = -300;
65
+ this.linkDistance = 100;
66
+ /** True until the lazy-loaded d3 peer dependencies have settled (success or failure). */
67
+ this.loading = true;
68
+ this.simNodes = [];
69
+ this.simLinks = [];
70
+ /** Current pan/zoom transform (SVG `transform` attribute syntax), driven by d3-zoom. */
71
+ this.transform = '';
72
+ /** Node `<circle>`s already wired up with d3-drag; cleared on every simulation rebuild
73
+ * so DOM elements Lit reuses across a rebuild get rebound to their fresh datum. */
74
+ this.boundNodeEls = new WeakSet();
75
+ }
76
+ static { this.styles = [LyraElement.styles, styles]; }
77
+ connectedCallback() {
78
+ super.connectedCallback();
79
+ void loadD3().then((mods) => {
80
+ this.loading = false;
81
+ // The element may have been removed from the DOM while the dynamic
82
+ // d3 imports were in flight — don't spin up a simulation for a
83
+ // detached instance (disconnectedCallback's cleanup already ran).
84
+ if (!mods || !this.isConnected)
85
+ return;
86
+ this.d3 = mods;
87
+ this.rebuildSimulation();
88
+ });
89
+ }
90
+ disconnectedCallback() {
91
+ super.disconnectedCallback();
92
+ this.simulation?.stop();
93
+ }
94
+ nodeRadius(n) {
95
+ return n.radius ?? (MIN_RADIUS + MAX_RADIUS) / 2;
96
+ }
97
+ updated(changed) {
98
+ if (this.loading)
99
+ this.setAttribute('aria-busy', 'true');
100
+ else
101
+ this.removeAttribute('aria-busy');
102
+ if (!this.d3)
103
+ return;
104
+ if (changed.has('nodes') || changed.has('links')) {
105
+ this.rebuildSimulation();
106
+ }
107
+ else if (changed.has('width') || changed.has('height')) {
108
+ this.simulation?.force('center', this.d3.forceCenter(this.width / 2, this.height / 2));
109
+ this.simulation?.alpha(0.1).restart();
110
+ }
111
+ this.applyInteractions();
112
+ }
113
+ /**
114
+ * Imperatively wires up d3-zoom (pan/zoom on the `<svg>`) and d3-drag
115
+ * (per-node drag) against the just-rendered DOM. Runs after every render:
116
+ * binding the `<svg>` is a one-time no-op guard (`zoomedEl`), and node
117
+ * `<circle>`s are skipped once bound (`boundNodeEls`) — a WeakSet reset on
118
+ * every `rebuildSimulation()` so DOM nodes Lit reuses across a rebuild get
119
+ * rebound against their new datum instead of a stale one.
120
+ */
121
+ applyInteractions() {
122
+ if (!this.d3)
123
+ return;
124
+ const svgEl = this.renderRoot.querySelector('svg');
125
+ if (svgEl && svgEl !== this.zoomedEl) {
126
+ this.zoomedEl = svgEl;
127
+ const zoomBehavior = this.d3.zoom().on('zoom', (event) => {
128
+ this.transform = event.transform.toString();
129
+ });
130
+ this.d3.select(svgEl).call(zoomBehavior);
131
+ }
132
+ const nodeEls = this.renderRoot.querySelectorAll('[part="node"]');
133
+ nodeEls.forEach((el, i) => {
134
+ if (this.boundNodeEls.has(el))
135
+ return;
136
+ const n = this.simNodes[i];
137
+ if (!n)
138
+ return;
139
+ this.boundNodeEls.add(el);
140
+ this.d3.select(el).call(this.d3.drag()
141
+ .on('start', (event) => {
142
+ // Keep a node drag from also triggering the svg's own pan gesture.
143
+ event.sourceEvent?.stopPropagation();
144
+ if (!event.active)
145
+ this.simulation?.alphaTarget(0.3).restart();
146
+ n.fx = n.x;
147
+ n.fy = n.y;
148
+ })
149
+ .on('drag', (event) => {
150
+ n.fx = event.x;
151
+ n.fy = event.y;
152
+ })
153
+ .on('end', (event) => {
154
+ if (!event.active)
155
+ this.simulation?.alphaTarget(0);
156
+ n.fx = null;
157
+ n.fy = null;
158
+ }));
159
+ });
160
+ }
161
+ rebuildSimulation() {
162
+ if (!this.d3)
163
+ return;
164
+ this.simulation?.stop();
165
+ this.boundNodeEls = new WeakSet();
166
+ const nodes = this.nodes.map((n) => ({ ...n }));
167
+ const byId = new Map(nodes.map((n) => [n.id, n]));
168
+ const links = this.links
169
+ .filter((l) => byId.has(l.source) && byId.has(l.target))
170
+ .map((l) => ({ ...l, source: byId.get(l.source), target: byId.get(l.target) }));
171
+ this.simulation = this.d3
172
+ .forceSimulation(nodes)
173
+ .force('link', this.d3.forceLink(links).distance(this.linkDistance))
174
+ .force('charge', this.d3.forceManyBody().strength(this.chargeStrength))
175
+ .force('center', this.d3.forceCenter(this.width / 2, this.height / 2))
176
+ .force('collide', this.d3.forceCollide().radius((n) => this.nodeRadius(n) + 10))
177
+ .on('tick', () => {
178
+ this.simNodes = [...nodes];
179
+ this.simLinks = [...links];
180
+ });
181
+ }
182
+ onNodeClick(node) {
183
+ this.emit('lyra-node-click', { id: node.id });
184
+ }
185
+ onLinkClick(link) {
186
+ const source = typeof link.source === 'object' ? link.source.id : String(link.source);
187
+ const target = typeof link.target === 'object' ? link.target.id : String(link.target);
188
+ this.emit('lyra-link-click', { source, target });
189
+ }
190
+ render() {
191
+ if (this.loading) {
192
+ return html `
193
+ <div part="base">
194
+ <lyra-skeleton
195
+ variant="rect"
196
+ style=${`--lyra-skeleton-w:${this.width}px;--lyra-skeleton-h:${this.height}px`}
197
+ ></lyra-skeleton>
198
+ </div>
199
+ `;
200
+ }
201
+ return html `
202
+ <div part="base">
203
+ <svg part="svg" viewBox="0 0 ${this.width} ${this.height}">
204
+ <g transform=${this.transform}>
205
+ ${this.simLinks.map((l) => {
206
+ const source = l.source;
207
+ const target = l.target;
208
+ return svg `<line
209
+ part="link"
210
+ stroke-width=${l.width ?? 1.5}
211
+ x1=${source.x ?? 0}
212
+ y1=${source.y ?? 0}
213
+ x2=${target.x ?? 0}
214
+ y2=${target.y ?? 0}
215
+ @click=${() => this.onLinkClick(l)}
216
+ ></line>`;
217
+ })}
218
+ ${this.simNodes.map((n) => svg `<g>
219
+ <circle
220
+ part="node"
221
+ role="button"
222
+ tabindex="0"
223
+ aria-label=${n.label ?? n.id}
224
+ r=${this.nodeRadius(n)}
225
+ cx=${n.x ?? 0}
226
+ cy=${n.y ?? 0}
227
+ style=${n.color ? `--lyra-node-fill:${n.color}` : ''}
228
+ @click=${() => this.onNodeClick(n)}
229
+ @keydown=${(e) => {
230
+ if (e.key === 'Enter' || e.key === ' ') {
231
+ e.preventDefault();
232
+ this.onNodeClick(n);
233
+ }
234
+ }}
235
+ ></circle>
236
+ ${n.label
237
+ ? svg `<text part="label" x=${(n.x ?? 0) + this.nodeRadius(n) + 2} y=${n.y ?? 0}>${n.label}</text>`
238
+ : ''}
239
+ </g>`)}
240
+ </g>
241
+ </svg>
242
+ </div>
243
+ `;
244
+ }
245
+ }
246
+ __decorate([
247
+ property({ attribute: false })
248
+ ], LyraGraph.prototype, "nodes", void 0);
249
+ __decorate([
250
+ property({ attribute: false })
251
+ ], LyraGraph.prototype, "links", void 0);
252
+ __decorate([
253
+ property({ type: Number })
254
+ ], LyraGraph.prototype, "width", void 0);
255
+ __decorate([
256
+ property({ type: Number })
257
+ ], LyraGraph.prototype, "height", void 0);
258
+ __decorate([
259
+ property({ type: Number, attribute: 'charge-strength' })
260
+ ], LyraGraph.prototype, "chargeStrength", void 0);
261
+ __decorate([
262
+ property({ type: Number, attribute: 'link-distance' })
263
+ ], LyraGraph.prototype, "linkDistance", void 0);
264
+ __decorate([
265
+ state()
266
+ ], LyraGraph.prototype, "loading", void 0);
267
+ __decorate([
268
+ state()
269
+ ], LyraGraph.prototype, "simNodes", void 0);
270
+ __decorate([
271
+ state()
272
+ ], LyraGraph.prototype, "simLinks", void 0);
273
+ __decorate([
274
+ state()
275
+ ], LyraGraph.prototype, "transform", void 0);
276
+ defineElement('graph', LyraGraph);
@@ -0,0 +1 @@
1
+ export declare const styles: import("lit").CSSResult;
@@ -0,0 +1,34 @@
1
+ import { css } from 'lit';
2
+ export const styles = css `
3
+ :host {
4
+ display: block;
5
+ }
6
+ svg {
7
+ display: block;
8
+ inline-size: 100%;
9
+ block-size: 100%;
10
+ }
11
+ [part='link'] {
12
+ stroke: var(--lyra-color-border);
13
+ fill: none;
14
+ }
15
+ [part='node'] {
16
+ /* --lyra-node-fill is set inline per-node (see graph.ts) from GraphNode.color;
17
+ falls back to the brand token when a node doesn't supply one. An inline
18
+ style declaration always wins the cascade over this selector, so setting
19
+ fill directly here (rather than via the presentation attribute) is what
20
+ lets a per-node color actually take effect. */
21
+ fill: var(--lyra-node-fill, var(--lyra-color-brand));
22
+ cursor: pointer;
23
+ }
24
+ [part='node']:focus-visible {
25
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
26
+ outline-offset: var(--lyra-focus-ring-offset);
27
+ }
28
+ [part='label'] {
29
+ font-size: 10px;
30
+ fill: var(--lyra-color-text);
31
+ font-family: var(--lyra-font);
32
+ pointer-events: none;
33
+ }
34
+ `;
@@ -0,0 +1,8 @@
1
+ /** Maps `value` in `[lo, hi]` to a 0.1–1.0 alpha, matching the cd-heatmap ramp. */
2
+ export declare function linearAlpha(value: number, lo: number, hi: number): number;
3
+ /**
4
+ * Square-root-scaled bucket index in `[0, steps-1]`, or `-1` for no-data.
5
+ * Compresses large counts so a single heavy cell doesn't wash out the rest
6
+ * of a sequential color ramp.
7
+ */
8
+ export declare function sqrtStep(count: number, max: number, steps: number): number;
@@ -0,0 +1,17 @@
1
+ /** Maps `value` in `[lo, hi]` to a 0.1–1.0 alpha, matching the cd-heatmap ramp. */
2
+ export function linearAlpha(value, lo, hi) {
3
+ const span = hi - lo || 1;
4
+ const t = (value - lo) / span;
5
+ return 0.1 + 0.9 * Math.min(1, Math.max(0, t));
6
+ }
7
+ /**
8
+ * Square-root-scaled bucket index in `[0, steps-1]`, or `-1` for no-data.
9
+ * Compresses large counts so a single heavy cell doesn't wash out the rest
10
+ * of a sequential color ramp.
11
+ */
12
+ export function sqrtStep(count, max, steps) {
13
+ if (count <= 0 || max <= 0)
14
+ return -1;
15
+ const ratio = Math.sqrt(count) / Math.sqrt(max);
16
+ return Math.min(steps - 1, Math.floor(ratio * steps));
17
+ }
@@ -0,0 +1,66 @@
1
+ import { type TemplateResult } from 'lit';
2
+ import { LyraElement } from '../../internal/lyra-element.js';
3
+ /**
4
+ * Parses a strict `#rgb` or `#rrggbb` hex string into an `[r, g, b]` triple,
5
+ * or `null` if `hex` isn't one (rather than silently coercing an unparsable
6
+ * string to `0` via `Number.parseInt(..., 16)` returning `NaN`).
7
+ */
8
+ export declare function hexToRgb(hex: string): [number, number, number] | null;
9
+ /**
10
+ * Resolves any syntactically valid CSS `<color>` — hex, `rgb()`, `hsl()`,
11
+ * `oklch()`, a named color, etc. — to an `[r, g, b]` triple.
12
+ *
13
+ * Hand-rolling a parser for every CSS color syntax is unnecessary and
14
+ * error-prone (a naive hex-only parser silently turns an unrecognized format
15
+ * into `NaN` -> `0`, i.e. solid black). The canvas 2D context already
16
+ * implements the full CSS color grammar via its `fillStyle` setter, so this
17
+ * normalizes through that instead. Assigning an unparsable string to
18
+ * `fillStyle` is a spec'd no-op (the previous value is kept, it never
19
+ * throws), so a sentinel round-trip is used to detect that case and fall
20
+ * back to `fallbackHex` (with a one-time `console.warn`) instead of
21
+ * silently drawing the wrong color.
22
+ */
23
+ export declare function resolveRgb(color: string, fallbackHex: string): [number, number, number];
24
+ /** Linearly interpolates between two CSS colors (any valid syntax) at `t` in `[0, 1]`. */
25
+ export declare function mixColor(fromColor: string, toColor: string, t: number): string;
26
+ /**
27
+ * `<lyra-heatmap>` — a Canvas matrix heatmap with a DPR-aware, resize-aware
28
+ * redraw loop. `-1` cells are treated as "no data". The sequential color
29
+ * ramp's endpoints are read from the `--lyra-heatmap-scale-lo`/`-hi` custom
30
+ * properties (declared in `heatmap.styles.ts`) so hosts can retheme it —
31
+ * canvas can't consume `var()` directly, so they're resolved once per draw
32
+ * via `getComputedStyle`, then normalized to RGB by `resolveRgb()` (any
33
+ * valid CSS color syntax, not just hex — see its doc comment).
34
+ *
35
+ * @customElement lyra-heatmap
36
+ * @csspart base, canvas, legend
37
+ */
38
+ export declare class LyraHeatmap extends LyraElement {
39
+ static styles: import("lit").CSSResultGroup[];
40
+ rowLabels: string[];
41
+ colLabels: string[];
42
+ values: number[][];
43
+ cellSize: number;
44
+ valueLabel: string;
45
+ scale: 'linear' | 'sqrt';
46
+ private canvas?;
47
+ private resizeObserver?;
48
+ private dprQuery?;
49
+ connectedCallback(): void;
50
+ disconnectedCallback(): void;
51
+ private watchDpr;
52
+ private onDprChange;
53
+ protected willUpdate(): void;
54
+ protected updated(): void;
55
+ /** Reads the customizable ramp endpoints off the host's computed style. */
56
+ private scaleEndpoints;
57
+ /** Resolves the `--lyra-color-text-quiet` chrome token for axis labels. */
58
+ private labelColor;
59
+ private draw;
60
+ render(): TemplateResult;
61
+ }
62
+ declare global {
63
+ interface HTMLElementTagNameMap {
64
+ 'lyra-heatmap': LyraHeatmap;
65
+ }
66
+ }
@@ -0,0 +1,252 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { html } from 'lit';
8
+ import { property, query } from 'lit/decorators.js';
9
+ import { LyraElement } from '../../internal/lyra-element.js';
10
+ import { defineElement } from '../../internal/prefix.js';
11
+ import { linearAlpha, sqrtStep } from './heatmap-scale.js';
12
+ import { styles } from './heatmap.styles.js';
13
+ const PAD_LEFT = 60;
14
+ const PAD_TOP = 20;
15
+ const NO_DATA_FILL = 'rgba(128,128,128,0.25)';
16
+ const RAMP_STEPS = 7;
17
+ const FALLBACK_SCALE_LO = '#cde2fb';
18
+ const FALLBACK_SCALE_HI = '#0969da';
19
+ const HEX_RE = /^([0-9a-f]{3}|[0-9a-f]{6})$/i;
20
+ const RGB_RE = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i;
21
+ /**
22
+ * Parses a strict `#rgb` or `#rrggbb` hex string into an `[r, g, b]` triple,
23
+ * or `null` if `hex` isn't one (rather than silently coercing an unparsable
24
+ * string to `0` via `Number.parseInt(..., 16)` returning `NaN`).
25
+ */
26
+ export function hexToRgb(hex) {
27
+ const clean = hex.trim().replace('#', '');
28
+ if (!HEX_RE.test(clean))
29
+ return null;
30
+ const full = clean.length === 3 ? clean.split('').map((c) => c + c).join('') : clean;
31
+ const num = Number.parseInt(full, 16);
32
+ return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
33
+ }
34
+ function parseRgbString(value) {
35
+ const match = RGB_RE.exec(value);
36
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
37
+ }
38
+ let scratchCtx;
39
+ /** A detached 1x1 canvas used solely to normalize CSS color strings. */
40
+ function getScratchCtx() {
41
+ if (scratchCtx === undefined) {
42
+ scratchCtx = document.createElement('canvas').getContext('2d');
43
+ }
44
+ return scratchCtx;
45
+ }
46
+ const warnedInvalidColors = new Set();
47
+ function warnInvalidColor(color) {
48
+ if (warnedInvalidColors.has(color))
49
+ return;
50
+ warnedInvalidColors.add(color);
51
+ console.warn(`<lyra-heatmap> could not parse "${color}" (set via --lyra-heatmap-scale-lo/-hi) as a CSS ` +
52
+ 'color; falling back to the default ramp endpoint.');
53
+ }
54
+ /**
55
+ * Resolves any syntactically valid CSS `<color>` — hex, `rgb()`, `hsl()`,
56
+ * `oklch()`, a named color, etc. — to an `[r, g, b]` triple.
57
+ *
58
+ * Hand-rolling a parser for every CSS color syntax is unnecessary and
59
+ * error-prone (a naive hex-only parser silently turns an unrecognized format
60
+ * into `NaN` -> `0`, i.e. solid black). The canvas 2D context already
61
+ * implements the full CSS color grammar via its `fillStyle` setter, so this
62
+ * normalizes through that instead. Assigning an unparsable string to
63
+ * `fillStyle` is a spec'd no-op (the previous value is kept, it never
64
+ * throws), so a sentinel round-trip is used to detect that case and fall
65
+ * back to `fallbackHex` (with a one-time `console.warn`) instead of
66
+ * silently drawing the wrong color.
67
+ */
68
+ export function resolveRgb(color, fallbackHex) {
69
+ const fallback = hexToRgb(fallbackHex) ?? [0, 0, 0];
70
+ const direct = hexToRgb(color);
71
+ if (direct)
72
+ return direct;
73
+ const ctx = getScratchCtx();
74
+ if (!ctx)
75
+ return fallback;
76
+ const sentinel = 'rgb(1, 2, 3)';
77
+ ctx.fillStyle = sentinel;
78
+ const sentinelNormalized = ctx.fillStyle;
79
+ ctx.fillStyle = color;
80
+ if (ctx.fillStyle === sentinelNormalized && color.trim() !== sentinel) {
81
+ warnInvalidColor(color);
82
+ return fallback;
83
+ }
84
+ const normalized = ctx.fillStyle;
85
+ return hexToRgb(normalized) ?? parseRgbString(normalized) ?? fallback;
86
+ }
87
+ /** Linearly interpolates between two already-resolved `[r, g, b]` triples at `t` in `[0, 1]`. */
88
+ function mixRgb(from, to, t) {
89
+ const clamped = Math.min(1, Math.max(0, t));
90
+ const r = Math.round(from[0] + (to[0] - from[0]) * clamped);
91
+ const g = Math.round(from[1] + (to[1] - from[1]) * clamped);
92
+ const b = Math.round(from[2] + (to[2] - from[2]) * clamped);
93
+ return `rgb(${r}, ${g}, ${b})`;
94
+ }
95
+ /** Linearly interpolates between two CSS colors (any valid syntax) at `t` in `[0, 1]`. */
96
+ export function mixColor(fromColor, toColor, t) {
97
+ return mixRgb(resolveRgb(fromColor, FALLBACK_SCALE_LO), resolveRgb(toColor, FALLBACK_SCALE_HI), t);
98
+ }
99
+ /**
100
+ * `<lyra-heatmap>` — a Canvas matrix heatmap with a DPR-aware, resize-aware
101
+ * redraw loop. `-1` cells are treated as "no data". The sequential color
102
+ * ramp's endpoints are read from the `--lyra-heatmap-scale-lo`/`-hi` custom
103
+ * properties (declared in `heatmap.styles.ts`) so hosts can retheme it —
104
+ * canvas can't consume `var()` directly, so they're resolved once per draw
105
+ * via `getComputedStyle`, then normalized to RGB by `resolveRgb()` (any
106
+ * valid CSS color syntax, not just hex — see its doc comment).
107
+ *
108
+ * @customElement lyra-heatmap
109
+ * @csspart base, canvas, legend
110
+ */
111
+ export class LyraHeatmap extends LyraElement {
112
+ constructor() {
113
+ super(...arguments);
114
+ this.rowLabels = [];
115
+ this.colLabels = [];
116
+ this.values = [];
117
+ this.cellSize = 22;
118
+ this.valueLabel = 'value';
119
+ this.scale = 'linear';
120
+ this.onDprChange = () => {
121
+ this.watchDpr();
122
+ this.draw();
123
+ };
124
+ }
125
+ static { this.styles = [LyraElement.styles, styles]; }
126
+ connectedCallback() {
127
+ super.connectedCallback();
128
+ this.resizeObserver = new ResizeObserver(() => this.draw());
129
+ this.resizeObserver.observe(this);
130
+ this.watchDpr();
131
+ }
132
+ disconnectedCallback() {
133
+ super.disconnectedCallback();
134
+ this.resizeObserver?.disconnect();
135
+ this.dprQuery?.removeEventListener('change', this.onDprChange);
136
+ }
137
+ watchDpr() {
138
+ this.dprQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
139
+ this.dprQuery.addEventListener('change', this.onDprChange);
140
+ }
141
+ willUpdate() {
142
+ const rows = this.rowLabels.length;
143
+ const cols = this.colLabels.length;
144
+ const flat = this.values.flat().filter((v) => v >= 0);
145
+ const range = flat.length ? `${Math.min(...flat)}–${Math.max(...flat)}` : 'no data';
146
+ this.setAttribute('role', 'img');
147
+ this.setAttribute('aria-label', `Heatmap of ${rows} × ${cols} cells, ${this.valueLabel} range ${range}`);
148
+ }
149
+ updated() {
150
+ this.draw();
151
+ }
152
+ /** Reads the customizable ramp endpoints off the host's computed style. */
153
+ scaleEndpoints() {
154
+ const cs = getComputedStyle(this);
155
+ const lo = cs.getPropertyValue('--lyra-heatmap-scale-lo').trim() || FALLBACK_SCALE_LO;
156
+ const hi = cs.getPropertyValue('--lyra-heatmap-scale-hi').trim() || FALLBACK_SCALE_HI;
157
+ return [lo, hi];
158
+ }
159
+ /** Resolves the `--lyra-color-text-quiet` chrome token for axis labels. */
160
+ labelColor() {
161
+ return getComputedStyle(this).getPropertyValue('--lyra-color-text-quiet').trim() || '#6b7280';
162
+ }
163
+ draw() {
164
+ if (!this.canvas)
165
+ return;
166
+ const rows = this.rowLabels.length;
167
+ const cols = this.colLabels.length;
168
+ const w = PAD_LEFT + cols * this.cellSize;
169
+ const h = PAD_TOP + rows * this.cellSize;
170
+ const dpr = window.devicePixelRatio || 1;
171
+ this.canvas.width = w * dpr;
172
+ this.canvas.height = h * dpr;
173
+ this.canvas.style.width = `${w}px`;
174
+ this.canvas.style.height = `${h}px`;
175
+ const ctx = this.canvas.getContext('2d');
176
+ if (!ctx)
177
+ return;
178
+ ctx.scale(dpr, dpr);
179
+ ctx.clearRect(0, 0, w, h);
180
+ const flat = this.values.flat().filter((v) => v >= 0);
181
+ const lo = flat.length ? Math.min(...flat) : 0;
182
+ const hi = flat.length ? Math.max(...flat) : 1;
183
+ const [scaleLo, scaleHi] = this.scaleEndpoints();
184
+ const loRgb = resolveRgb(scaleLo, FALLBACK_SCALE_LO);
185
+ const hiRgb = resolveRgb(scaleHi, FALLBACK_SCALE_HI);
186
+ const ramp = Array.from({ length: RAMP_STEPS }, (_, i) => mixRgb(loRgb, hiRgb, i / (RAMP_STEPS - 1)));
187
+ for (let r = 0; r < rows; r++) {
188
+ for (let c = 0; c < cols; c++) {
189
+ const v = this.values[r]?.[c] ?? -1;
190
+ const x = PAD_LEFT + c * this.cellSize;
191
+ const y = PAD_TOP + r * this.cellSize;
192
+ if (v < 0) {
193
+ ctx.fillStyle = NO_DATA_FILL;
194
+ }
195
+ else if (this.scale === 'sqrt') {
196
+ const step = sqrtStep(v, hi, RAMP_STEPS);
197
+ ctx.fillStyle = step < 0 ? NO_DATA_FILL : ramp[step];
198
+ }
199
+ else {
200
+ // linearAlpha() returns a 0.1-1.0 ramp position; reused here as a
201
+ // mix ratio between the two ramp-endpoint colors (rather than as a
202
+ // literal canvas alpha channel) so the 'linear' scale also respects
203
+ // --lyra-heatmap-scale-lo/-hi.
204
+ const t = linearAlpha(v, lo, hi);
205
+ ctx.fillStyle = mixRgb(loRgb, hiRgb, t);
206
+ }
207
+ ctx.fillRect(x, y, this.cellSize - 1, this.cellSize - 1);
208
+ }
209
+ }
210
+ ctx.fillStyle = this.labelColor();
211
+ ctx.font = '10px sans-serif';
212
+ this.rowLabels.forEach((label, r) => {
213
+ ctx.fillText(label, 4, PAD_TOP + r * this.cellSize + this.cellSize / 2 + 3);
214
+ });
215
+ this.colLabels.forEach((label, c) => {
216
+ ctx.fillText(label, PAD_LEFT + c * this.cellSize + 2, PAD_TOP - 6);
217
+ });
218
+ }
219
+ render() {
220
+ return html `
221
+ <div part="base">
222
+ <canvas part="canvas"></canvas>
223
+ <div part="legend">
224
+ <span class="bar"></span>
225
+ <span>${this.valueLabel}</span>
226
+ </div>
227
+ </div>
228
+ `;
229
+ }
230
+ }
231
+ __decorate([
232
+ property({ attribute: false })
233
+ ], LyraHeatmap.prototype, "rowLabels", void 0);
234
+ __decorate([
235
+ property({ attribute: false })
236
+ ], LyraHeatmap.prototype, "colLabels", void 0);
237
+ __decorate([
238
+ property({ attribute: false })
239
+ ], LyraHeatmap.prototype, "values", void 0);
240
+ __decorate([
241
+ property({ type: Number, attribute: 'cell-size' })
242
+ ], LyraHeatmap.prototype, "cellSize", void 0);
243
+ __decorate([
244
+ property({ attribute: 'value-label' })
245
+ ], LyraHeatmap.prototype, "valueLabel", void 0);
246
+ __decorate([
247
+ property()
248
+ ], LyraHeatmap.prototype, "scale", void 0);
249
+ __decorate([
250
+ query('canvas')
251
+ ], LyraHeatmap.prototype, "canvas", void 0);
252
+ defineElement('heatmap', LyraHeatmap);
@@ -0,0 +1 @@
1
+ export declare const styles: import("lit").CSSResult;