@aceshooting/lyra-ui 0.1.1 → 0.1.3
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 +77 -6
- package/custom-elements.json +12015 -0
- package/dist/components/chart/bar-chart.d.ts +14 -0
- package/dist/components/chart/bar-chart.js +14 -0
- package/dist/components/chart/box-plot.d.ts +46 -0
- package/dist/components/chart/box-plot.js +145 -0
- package/dist/components/chart/box-plot.styles.d.ts +1 -0
- package/dist/components/chart/box-plot.styles.js +22 -0
- package/dist/components/chart/bubble-chart.d.ts +19 -0
- package/dist/components/chart/bubble-chart.js +18 -0
- package/dist/components/chart/chart-loader.d.ts +8 -0
- package/dist/components/chart/chart-loader.js +26 -0
- package/dist/components/chart/chart.d.ts +91 -0
- package/dist/components/chart/chart.js +275 -0
- package/dist/components/chart/chart.styles.d.ts +1 -0
- package/dist/components/chart/chart.styles.js +33 -0
- package/dist/components/chart/doughnut-chart.d.ts +15 -0
- package/dist/components/chart/doughnut-chart.js +14 -0
- package/dist/components/chart/histogram-bin.d.ts +6 -0
- package/dist/components/chart/histogram-bin.js +19 -0
- package/dist/components/chart/histogram.d.ts +20 -0
- package/dist/components/chart/histogram.js +70 -0
- package/dist/components/chart/histogram.styles.d.ts +1 -0
- package/dist/components/chart/histogram.styles.js +1 -0
- package/dist/components/chart/line-chart.d.ts +14 -0
- package/dist/components/chart/line-chart.js +14 -0
- package/dist/components/chart/pie-chart.d.ts +16 -0
- package/dist/components/chart/pie-chart.js +15 -0
- package/dist/components/chart/polar-area-chart.d.ts +15 -0
- package/dist/components/chart/polar-area-chart.js +14 -0
- package/dist/components/chart/radar-chart.d.ts +15 -0
- package/dist/components/chart/radar-chart.js +14 -0
- package/dist/components/chart/scatter-chart.d.ts +15 -0
- package/dist/components/chart/scatter-chart.js +15 -0
- package/dist/components/combobox/combobox.d.ts +16 -0
- package/dist/components/combobox/combobox.js +113 -6
- package/dist/components/combobox/combobox.styles.js +68 -7
- package/dist/components/combobox/option.d.ts +2 -2
- package/dist/components/combobox/option.js +11 -7
- package/dist/components/date-picker/date-input.d.ts +12 -1
- package/dist/components/date-picker/date-input.js +68 -6
- package/dist/components/date-picker/date-input.styles.js +58 -6
- package/dist/components/date-picker/date-picker.js +5 -2
- package/dist/components/date-picker/date-picker.styles.js +9 -2
- package/dist/components/empty/empty.d.ts +1 -0
- package/dist/components/empty/empty.js +14 -2
- package/dist/components/export-button/csv.d.ts +10 -0
- package/dist/components/export-button/csv.js +28 -0
- package/dist/components/export-button/export-button.d.ts +41 -0
- package/dist/components/export-button/export-button.js +151 -0
- package/dist/components/export-button/export-button.styles.d.ts +1 -0
- package/dist/components/export-button/export-button.styles.js +79 -0
- package/dist/components/file-input/accept.d.ts +14 -0
- package/dist/components/file-input/accept.js +30 -0
- package/dist/components/file-input/file-input.d.ts +47 -0
- package/dist/components/file-input/file-input.js +182 -0
- package/dist/components/file-input/file-input.styles.d.ts +1 -0
- package/dist/components/file-input/file-input.styles.js +44 -0
- package/dist/components/flag/flag.d.ts +4 -0
- package/dist/components/flag/flag.js +24 -1
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/gauge/gauge.d.ts +29 -0
- package/dist/components/gauge/gauge.js +133 -0
- package/dist/components/gauge/gauge.styles.d.ts +1 -0
- package/dist/components/gauge/gauge.styles.js +60 -0
- package/dist/components/graph/graph.d.ts +68 -0
- package/dist/components/graph/graph.js +276 -0
- package/dist/components/graph/graph.styles.d.ts +1 -0
- package/dist/components/graph/graph.styles.js +34 -0
- package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
- package/dist/components/heatmap/heatmap-scale.js +17 -0
- package/dist/components/heatmap/heatmap.d.ts +66 -0
- package/dist/components/heatmap/heatmap.js +252 -0
- package/dist/components/heatmap/heatmap.styles.d.ts +1 -0
- package/dist/components/heatmap/heatmap.styles.js +39 -0
- package/dist/components/map/map-loader.d.ts +8 -0
- package/dist/components/map/map-loader.js +17 -0
- package/dist/components/map/map.d.ts +49 -0
- package/dist/components/map/map.js +189 -0
- package/dist/components/map/map.styles.d.ts +1 -0
- package/dist/components/map/map.styles.js +48 -0
- package/dist/components/playback/playback.d.ts +51 -0
- package/dist/components/playback/playback.js +146 -0
- package/dist/components/playback/playback.styles.d.ts +1 -0
- package/dist/components/playback/playback.styles.js +40 -0
- package/dist/components/split/split.d.ts +41 -0
- package/dist/components/split/split.js +171 -0
- package/dist/components/split/split.styles.d.ts +1 -0
- package/dist/components/split/split.styles.js +51 -0
- package/dist/components/stat/stat.d.ts +9 -0
- package/dist/components/stat/stat.js +52 -7
- package/dist/components/stat/stat.styles.js +15 -5
- package/dist/components/table/table.d.ts +18 -6
- package/dist/components/table/table.js +76 -24
- package/dist/components/table/table.styles.js +32 -0
- package/dist/components/time-range/time-range.d.ts +40 -0
- package/dist/components/time-range/time-range.js +219 -0
- package/dist/components/time-range/time-range.styles.d.ts +1 -0
- package/dist/components/time-range/time-range.styles.js +78 -0
- package/dist/components/toast/toast-item.js +10 -2
- package/dist/components/toast/toast-item.styles.js +13 -4
- package/dist/components/toast/toast.styles.js +0 -1
- package/dist/components/tree/tree-node.d.ts +50 -0
- package/dist/components/tree/tree-node.js +196 -0
- package/dist/components/tree/tree.d.ts +65 -0
- package/dist/components/tree/tree.js +227 -0
- package/dist/components/tree/tree.styles.d.ts +1 -0
- package/dist/components/tree/tree.styles.js +13 -0
- package/dist/internal/form-associated.js +49 -1
- package/dist/internal/icons.d.ts +11 -0
- package/dist/internal/icons.js +64 -0
- package/dist/internal/tokens.styles.js +41 -1
- package/dist/lyra.d.ts +67 -0
- package/dist/lyra.js +55 -0
- package/llms-full.txt +1425 -0
- package/llms.txt +47 -0
- 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;
|