@aceshooting/lyra-ui 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -8
- package/custom-elements.json +3745 -2953
- package/dist/components/chart/bar-chart.js +17 -4
- package/dist/components/chart/box-plot.d.ts +8 -1
- package/dist/components/chart/box-plot.js +40 -4
- package/dist/components/chart/box-plot.styles.js +4 -0
- package/dist/components/chart/bubble-chart.d.ts +1 -2
- package/dist/components/chart/bubble-chart.js +17 -4
- package/dist/components/chart/chart-loader.d.ts +21 -4
- package/dist/components/chart/chart-loader.js +39 -12
- package/dist/components/chart/chart.d.ts +9 -3
- package/dist/components/chart/chart.js +42 -21
- package/dist/components/chart/doughnut-chart.d.ts +1 -2
- package/dist/components/chart/doughnut-chart.js +17 -4
- package/dist/components/chart/histogram-bin.js +12 -2
- package/dist/components/chart/histogram.d.ts +2 -2
- package/dist/components/chart/histogram.js +16 -2
- package/dist/components/chart/line-chart.js +17 -4
- package/dist/components/chart/pie-chart.d.ts +1 -2
- package/dist/components/chart/pie-chart.js +17 -4
- package/dist/components/chart/polar-area-chart.d.ts +1 -2
- package/dist/components/chart/polar-area-chart.js +17 -4
- package/dist/components/chart/radar-chart.d.ts +1 -2
- package/dist/components/chart/radar-chart.js +17 -4
- package/dist/components/chart/scatter-chart.js +17 -4
- package/dist/components/combobox/combobox.js +3 -4
- package/dist/components/combobox/combobox.stories.d.ts +6 -0
- package/dist/components/combobox/combobox.stories.js +27 -0
- package/dist/components/combobox/combobox.styles.js +2 -2
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +12 -0
- package/dist/components/date-picker/date-input.styles.js +2 -2
- package/dist/components/date-picker/date-picker.stories.d.ts +6 -0
- package/dist/components/date-picker/date-picker.stories.js +13 -0
- package/dist/components/empty/empty.d.ts +1 -1
- package/dist/components/empty/empty.js +1 -1
- package/dist/components/export-button/export-button.d.ts +2 -2
- package/dist/components/export-button/export-button.js +2 -2
- package/dist/components/file-input/accept.d.ts +1 -2
- package/dist/components/file-input/accept.js +1 -2
- package/dist/components/file-input/file-input.d.ts +2 -2
- package/dist/components/file-input/file-input.js +2 -2
- package/dist/components/flag/flag.stories.d.ts +5 -0
- package/dist/components/flag/flag.stories.js +17 -0
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/gauge/gauge.d.ts +1 -1
- package/dist/components/gauge/gauge.js +1 -1
- package/dist/components/graph/graph.d.ts +33 -4
- package/dist/components/graph/graph.js +146 -30
- package/dist/components/heatmap/calendar-grid.d.ts +31 -0
- package/dist/components/heatmap/calendar-grid.js +52 -0
- package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
- package/dist/components/heatmap/heatmap-scale.js +21 -0
- package/dist/components/heatmap/heatmap.d.ts +19 -0
- package/dist/components/heatmap/heatmap.js +133 -20
- package/dist/components/heatmap/heatmap.styles.js +4 -0
- package/dist/components/map/map.d.ts +15 -0
- package/dist/components/map/map.js +89 -7
- package/dist/components/playback/playback.d.ts +2 -2
- package/dist/components/playback/playback.js +2 -2
- package/dist/components/skeleton/skeleton.d.ts +2 -2
- package/dist/components/skeleton/skeleton.js +2 -2
- package/dist/components/sparkline/sparkline.stories.d.ts +7 -0
- package/dist/components/sparkline/sparkline.stories.js +17 -0
- package/dist/components/stat/stat.d.ts +1 -1
- package/dist/components/stat/stat.js +6 -7
- package/dist/components/table/table.d.ts +1 -2
- package/dist/components/table/table.js +1 -2
- package/dist/components/table/table.styles.js +2 -3
- package/dist/components/time-range/time-range.js +4 -4
- package/dist/components/toast/toast.stories.d.ts +5 -0
- package/dist/components/toast/toast.stories.js +32 -0
- package/dist/components/tree/tree-node.d.ts +2 -2
- package/dist/components/tree/tree-node.js +2 -2
- package/dist/internal/form-associated.js +1 -2
- package/dist/internal/icons.js +1 -1
- package/dist/internal/tokens.styles.js +3 -3
- package/llms-full.txt +59 -83
- package/llms.txt +1 -2
- package/package.json +4 -1
|
@@ -6,6 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
};
|
|
7
7
|
import { html, svg } from 'lit';
|
|
8
8
|
import { property, state } from 'lit/decorators.js';
|
|
9
|
+
import { styleMap } from 'lit/directives/style-map.js';
|
|
9
10
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
11
|
import { defineElement } from '../../internal/prefix.js';
|
|
11
12
|
import { styles } from './graph.styles.js';
|
|
@@ -44,6 +45,20 @@ function loadD3() {
|
|
|
44
45
|
}
|
|
45
46
|
const MIN_RADIUS = 6;
|
|
46
47
|
const MAX_RADIUS = 24;
|
|
48
|
+
/**
|
|
49
|
+
* Rejects a `GraphNode.color` that could break out of the single
|
|
50
|
+
* `--lyra-node-fill` custom-property declaration it's assigned to — `;`,
|
|
51
|
+
* `{`, and `}` are all a value needs to terminate that declaration and start
|
|
52
|
+
* another. This matters even though the node fill is set via Lit's
|
|
53
|
+
* `styleMap` directive (not raw string interpolation): `styleMap`'s first
|
|
54
|
+
* commit for a given attribute part serializes the whole `style` value as a
|
|
55
|
+
* single string (only later updates go through the safe
|
|
56
|
+
* `CSSStyleDeclaration.setProperty()` path), so an unsanitized value could
|
|
57
|
+
* still inject on that first render.
|
|
58
|
+
*/
|
|
59
|
+
function sanitizeNodeColor(color) {
|
|
60
|
+
return color != null && !/[;{}]/.test(color) ? color : undefined;
|
|
61
|
+
}
|
|
47
62
|
/**
|
|
48
63
|
* `<lyra-graph>` — a force-directed node-link diagram with pan/zoom/drag.
|
|
49
64
|
* Requires the optional peer deps `d3-force`/`d3-drag`/`d3-zoom`/`d3-selection`
|
|
@@ -63,6 +78,8 @@ export class LyraGraph extends LyraElement {
|
|
|
63
78
|
this.height = 600;
|
|
64
79
|
this.chargeStrength = -300;
|
|
65
80
|
this.linkDistance = 100;
|
|
81
|
+
this.minZoom = 0.1;
|
|
82
|
+
this.maxZoom = 8;
|
|
66
83
|
/** True until the lazy-loaded d3 peer dependencies have settled (success or failure). */
|
|
67
84
|
this.loading = true;
|
|
68
85
|
this.simNodes = [];
|
|
@@ -72,6 +89,13 @@ export class LyraGraph extends LyraElement {
|
|
|
72
89
|
/** Node `<circle>`s already wired up with d3-drag; cleared on every simulation rebuild
|
|
73
90
|
* so DOM elements Lit reuses across a rebuild get rebound to their fresh datum. */
|
|
74
91
|
this.boundNodeEls = new WeakSet();
|
|
92
|
+
/** Node/link/label DOM elements, index-aligned with simNodes/simLinks, cached
|
|
93
|
+
* once per structural rebuild and written to directly by onTick() — this is
|
|
94
|
+
* what lets ticks update positions without going through Lit's reactive
|
|
95
|
+
* simNodes/simLinks properties (see rebuildSimulation()'s doc comment). */
|
|
96
|
+
this.nodeEls = [];
|
|
97
|
+
this.nodeLabelEls = [];
|
|
98
|
+
this.linkEls = [];
|
|
75
99
|
}
|
|
76
100
|
static { this.styles = [LyraElement.styles, styles]; }
|
|
77
101
|
connectedCallback() {
|
|
@@ -94,6 +118,17 @@ export class LyraGraph extends LyraElement {
|
|
|
94
118
|
nodeRadius(n) {
|
|
95
119
|
return n.radius ?? (MIN_RADIUS + MAX_RADIUS) / 2;
|
|
96
120
|
}
|
|
121
|
+
willUpdate(changed) {
|
|
122
|
+
// rebuildSimulation() (re)assigns the simNodes/simLinks reactive
|
|
123
|
+
// properties — doing that from willUpdate() folds them into the render
|
|
124
|
+
// this same update is already about to perform. Doing it from updated()
|
|
125
|
+
// instead would set a reactive property *after* the update completed,
|
|
126
|
+
// which Lit schedules as a whole extra update pass (a dev-mode warning,
|
|
127
|
+
// and pointless work).
|
|
128
|
+
if (this.d3 && (changed.has('nodes') || changed.has('links'))) {
|
|
129
|
+
this.rebuildSimulation();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
97
132
|
updated(changed) {
|
|
98
133
|
if (this.loading)
|
|
99
134
|
this.setAttribute('aria-busy', 'true');
|
|
@@ -101,35 +136,61 @@ export class LyraGraph extends LyraElement {
|
|
|
101
136
|
this.removeAttribute('aria-busy');
|
|
102
137
|
if (!this.d3)
|
|
103
138
|
return;
|
|
104
|
-
if (changed.has('nodes')
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
139
|
+
if (!changed.has('nodes') && !changed.has('links')) {
|
|
140
|
+
if (changed.has('width') || changed.has('height')) {
|
|
141
|
+
this.simulation?.force('center', this.d3.forceCenter(this.width / 2, this.height / 2));
|
|
142
|
+
this.simulation?.alpha(0.1).restart();
|
|
143
|
+
}
|
|
144
|
+
else if (changed.has('chargeStrength') || changed.has('linkDistance')) {
|
|
145
|
+
// Without this branch, chargeStrength/linkDistance only took effect
|
|
146
|
+
// the next time nodes/links also changed (rebuildSimulation() reads
|
|
147
|
+
// them fresh) — retune the already-created force objects in place
|
|
148
|
+
// instead of rebuilding the whole simulation.
|
|
149
|
+
if (changed.has('chargeStrength'))
|
|
150
|
+
this.chargeForce?.strength(this.chargeStrength);
|
|
151
|
+
if (changed.has('linkDistance'))
|
|
152
|
+
this.linkForce?.distance(this.linkDistance);
|
|
153
|
+
this.simulation?.alpha(0.3).restart();
|
|
154
|
+
}
|
|
110
155
|
}
|
|
111
|
-
|
|
156
|
+
// simNodes/simLinks only ever change once per structural rebuild (see
|
|
157
|
+
// rebuildSimulation()'s doc comment) — never on a tick — so gating the
|
|
158
|
+
// node/link querySelectorAll scan on that is equivalent to "structurally
|
|
159
|
+
// changed" without needing a separate flag.
|
|
160
|
+
this.applyInteractions(changed.has('simNodes') || changed.has('simLinks'));
|
|
112
161
|
}
|
|
113
162
|
/**
|
|
114
163
|
* Imperatively wires up d3-zoom (pan/zoom on the `<svg>`) and d3-drag
|
|
115
|
-
* (per-node drag) against the just-rendered DOM.
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
164
|
+
* (per-node drag) against the just-rendered DOM. The zoom bind is a
|
|
165
|
+
* one-time no-op guard (`zoomedEl`); the node-drag bind + node/link/label
|
|
166
|
+
* element caching for `onTick()` only run when `structural` is true (a
|
|
167
|
+
* fresh structural render just happened), not on every call — otherwise
|
|
168
|
+
* this would re-scan the DOM via `querySelectorAll` on every Lit update,
|
|
169
|
+
* which used to include every single simulation tick. `<circle>`s already
|
|
170
|
+
* bound are skipped (`boundNodeEls`) — a WeakSet reset on every
|
|
171
|
+
* `rebuildSimulation()` so DOM nodes Lit reuses across a rebuild get
|
|
119
172
|
* rebound against their new datum instead of a stale one.
|
|
120
173
|
*/
|
|
121
|
-
applyInteractions() {
|
|
174
|
+
applyInteractions(structural) {
|
|
122
175
|
if (!this.d3)
|
|
123
176
|
return;
|
|
124
177
|
const svgEl = this.renderRoot.querySelector('svg');
|
|
125
178
|
if (svgEl && svgEl !== this.zoomedEl) {
|
|
126
179
|
this.zoomedEl = svgEl;
|
|
127
|
-
const zoomBehavior = this.d3
|
|
180
|
+
const zoomBehavior = this.d3
|
|
181
|
+
.zoom()
|
|
182
|
+
.scaleExtent([this.minZoom, this.maxZoom])
|
|
183
|
+
.on('zoom', (event) => {
|
|
128
184
|
this.transform = event.transform.toString();
|
|
129
185
|
});
|
|
130
186
|
this.d3.select(svgEl).call(zoomBehavior);
|
|
131
187
|
}
|
|
132
|
-
|
|
188
|
+
if (!structural)
|
|
189
|
+
return;
|
|
190
|
+
const nodeEls = Array.from(this.renderRoot.querySelectorAll('[part="node"]'));
|
|
191
|
+
this.nodeEls = nodeEls;
|
|
192
|
+
this.nodeLabelEls = nodeEls.map((el) => el.parentElement?.querySelector('[part="label"]') ?? null);
|
|
193
|
+
this.linkEls = Array.from(this.renderRoot.querySelectorAll('[part="link"]'));
|
|
133
194
|
nodeEls.forEach((el, i) => {
|
|
134
195
|
if (this.boundNodeEls.has(el))
|
|
135
196
|
return;
|
|
@@ -158,6 +219,43 @@ export class LyraGraph extends LyraElement {
|
|
|
158
219
|
}));
|
|
159
220
|
});
|
|
160
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Runs on every d3-force simulation tick (up to ~300 while a graph settles
|
|
224
|
+
* on load, continuously while a node is being dragged via
|
|
225
|
+
* `alphaTarget(0.3)`). Writes positions straight to the already-rendered
|
|
226
|
+
* DOM via direct d3-selection `.attr()` calls instead of reassigning the
|
|
227
|
+
* reactive `simNodes`/`simLinks` properties, which would force a full Lit
|
|
228
|
+
* re-render (and, before the `structural` gate above, an unconditional
|
|
229
|
+
* `querySelectorAll` scan) on every single frame.
|
|
230
|
+
*/
|
|
231
|
+
onTick() {
|
|
232
|
+
if (!this.d3)
|
|
233
|
+
return;
|
|
234
|
+
const d3 = this.d3;
|
|
235
|
+
this.simNodes.forEach((n, i) => {
|
|
236
|
+
const circle = this.nodeEls[i];
|
|
237
|
+
if (circle)
|
|
238
|
+
d3.select(circle).attr('cx', n.x ?? 0).attr('cy', n.y ?? 0);
|
|
239
|
+
const label = this.nodeLabelEls[i];
|
|
240
|
+
if (label) {
|
|
241
|
+
d3.select(label)
|
|
242
|
+
.attr('x', (n.x ?? 0) + this.nodeRadius(n) + 2)
|
|
243
|
+
.attr('y', n.y ?? 0);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
this.simLinks.forEach((l, i) => {
|
|
247
|
+
const line = this.linkEls[i];
|
|
248
|
+
if (!line)
|
|
249
|
+
return;
|
|
250
|
+
const source = l.source;
|
|
251
|
+
const target = l.target;
|
|
252
|
+
d3.select(line)
|
|
253
|
+
.attr('x1', source.x ?? 0)
|
|
254
|
+
.attr('y1', source.y ?? 0)
|
|
255
|
+
.attr('x2', target.x ?? 0)
|
|
256
|
+
.attr('y2', target.y ?? 0);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
161
259
|
rebuildSimulation() {
|
|
162
260
|
if (!this.d3)
|
|
163
261
|
return;
|
|
@@ -168,16 +266,25 @@ export class LyraGraph extends LyraElement {
|
|
|
168
266
|
const links = this.links
|
|
169
267
|
.filter((l) => byId.has(l.source) && byId.has(l.target))
|
|
170
268
|
.map((l) => ({ ...l, source: byId.get(l.source), target: byId.get(l.target) }));
|
|
269
|
+
this.linkForce = this.d3.forceLink(links).distance(this.linkDistance);
|
|
270
|
+
this.chargeForce = this.d3.forceManyBody().strength(this.chargeStrength);
|
|
171
271
|
this.simulation = this.d3
|
|
172
272
|
.forceSimulation(nodes)
|
|
173
|
-
.force('link', this.
|
|
174
|
-
.force('charge', this.
|
|
273
|
+
.force('link', this.linkForce)
|
|
274
|
+
.force('charge', this.chargeForce)
|
|
175
275
|
.force('center', this.d3.forceCenter(this.width / 2, this.height / 2))
|
|
176
276
|
.force('collide', this.d3.forceCollide().radius((n) => this.nodeRadius(n) + 10))
|
|
177
|
-
.on('tick', () =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
277
|
+
.on('tick', () => this.onTick());
|
|
278
|
+
// Assign these SAME array/object references (not copies) exactly once
|
|
279
|
+
// for this structural rebuild: forceSimulation(nodes) above already
|
|
280
|
+
// initialized their .x/.y synchronously, so this one Lit re-render
|
|
281
|
+
// creates the initial DOM with correct starting positions. Every
|
|
282
|
+
// subsequent tick mutates these same node/link objects in place and
|
|
283
|
+
// calls onTick() to write positions straight to the DOM — reassigning
|
|
284
|
+
// simNodes/simLinks on every tick (as before) would force a full Lit
|
|
285
|
+
// re-render up to ~300 times on load and continuously while dragging.
|
|
286
|
+
this.simNodes = nodes;
|
|
287
|
+
this.simLinks = links;
|
|
181
288
|
}
|
|
182
289
|
onNodeClick(node) {
|
|
183
290
|
this.emit('lyra-node-click', { id: node.id });
|
|
@@ -215,7 +322,9 @@ export class LyraGraph extends LyraElement {
|
|
|
215
322
|
@click=${() => this.onLinkClick(l)}
|
|
216
323
|
></line>`;
|
|
217
324
|
})}
|
|
218
|
-
${this.simNodes.map((n) =>
|
|
325
|
+
${this.simNodes.map((n) => {
|
|
326
|
+
const fill = sanitizeNodeColor(n.color);
|
|
327
|
+
return svg `<g>
|
|
219
328
|
<circle
|
|
220
329
|
part="node"
|
|
221
330
|
role="button"
|
|
@@ -224,19 +333,20 @@ export class LyraGraph extends LyraElement {
|
|
|
224
333
|
r=${this.nodeRadius(n)}
|
|
225
334
|
cx=${n.x ?? 0}
|
|
226
335
|
cy=${n.y ?? 0}
|
|
227
|
-
style=${
|
|
336
|
+
style=${styleMap(fill ? { '--lyra-node-fill': fill } : {})}
|
|
228
337
|
@click=${() => this.onNodeClick(n)}
|
|
229
338
|
@keydown=${(e) => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
339
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
340
|
+
e.preventDefault();
|
|
341
|
+
this.onNodeClick(n);
|
|
342
|
+
}
|
|
343
|
+
}}
|
|
235
344
|
></circle>
|
|
236
345
|
${n.label
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
</g
|
|
346
|
+
? svg `<text part="label" x=${(n.x ?? 0) + this.nodeRadius(n) + 2} y=${n.y ?? 0}>${n.label}</text>`
|
|
347
|
+
: ''}
|
|
348
|
+
</g>`;
|
|
349
|
+
})}
|
|
240
350
|
</g>
|
|
241
351
|
</svg>
|
|
242
352
|
</div>
|
|
@@ -261,6 +371,12 @@ __decorate([
|
|
|
261
371
|
__decorate([
|
|
262
372
|
property({ type: Number, attribute: 'link-distance' })
|
|
263
373
|
], LyraGraph.prototype, "linkDistance", void 0);
|
|
374
|
+
__decorate([
|
|
375
|
+
property({ type: Number, attribute: 'min-zoom' })
|
|
376
|
+
], LyraGraph.prototype, "minZoom", void 0);
|
|
377
|
+
__decorate([
|
|
378
|
+
property({ type: Number, attribute: 'max-zoom' })
|
|
379
|
+
], LyraGraph.prototype, "maxZoom", void 0);
|
|
264
380
|
__decorate([
|
|
265
381
|
state()
|
|
266
382
|
], LyraGraph.prototype, "loading", void 0);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface CalendarDay {
|
|
2
|
+
/** ISO `yyyy-mm-dd` date string. */
|
|
3
|
+
date: string;
|
|
4
|
+
value: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CalendarCell {
|
|
7
|
+
date: string;
|
|
8
|
+
value: number;
|
|
9
|
+
/** 0-based week column, counting from the grid's first (earliest-containing) Sunday. */
|
|
10
|
+
week: number;
|
|
11
|
+
/** 0 (Sunday) .. 6 (Saturday). */
|
|
12
|
+
weekday: number;
|
|
13
|
+
}
|
|
14
|
+
/** Parses a `yyyy-mm-dd` string as UTC midnight, avoiding local-timezone day-boundary drift. */
|
|
15
|
+
export declare function parseIsoDate(iso: string): Date;
|
|
16
|
+
/**
|
|
17
|
+
* Lays `days` out onto a GitHub-style Sunday-Saturday x week grid. `days`
|
|
18
|
+
* need not be sorted or contiguous; the grid spans from the earliest to the
|
|
19
|
+
* latest date present, anchored at the Sunday on/before the earliest date.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildCalendarGrid(days: CalendarDay[]): {
|
|
22
|
+
cells: CalendarCell[];
|
|
23
|
+
weekCount: number;
|
|
24
|
+
firstWeekStart: Date;
|
|
25
|
+
monthLabels: {
|
|
26
|
+
week: number;
|
|
27
|
+
label: string;
|
|
28
|
+
}[];
|
|
29
|
+
};
|
|
30
|
+
/** Splits `value`'s rank within `sorted` (ascending, pre-sorted by the caller) into `bucketCount` quartile-style buckets. */
|
|
31
|
+
export declare function quartileBucket(value: number, sorted: number[], bucketCount: number): number;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const MS_PER_DAY = 86_400_000;
|
|
2
|
+
/** Parses a `yyyy-mm-dd` string as UTC midnight, avoiding local-timezone day-boundary drift. */
|
|
3
|
+
export function parseIsoDate(iso) {
|
|
4
|
+
const [y, m, d] = iso.split('-').map(Number);
|
|
5
|
+
return new Date(Date.UTC(y, m - 1, d));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Lays `days` out onto a GitHub-style Sunday-Saturday x week grid. `days`
|
|
9
|
+
* need not be sorted or contiguous; the grid spans from the earliest to the
|
|
10
|
+
* latest date present, anchored at the Sunday on/before the earliest date.
|
|
11
|
+
*/
|
|
12
|
+
export function buildCalendarGrid(days) {
|
|
13
|
+
if (days.length === 0) {
|
|
14
|
+
return { cells: [], weekCount: 0, firstWeekStart: new Date(0), monthLabels: [] };
|
|
15
|
+
}
|
|
16
|
+
const parsed = days.map((d) => ({ ...d, dateObj: parseIsoDate(d.date) }));
|
|
17
|
+
const minTime = Math.min(...parsed.map((d) => d.dateObj.getTime()));
|
|
18
|
+
const min = new Date(minTime);
|
|
19
|
+
const firstWeekStart = new Date(Date.UTC(min.getUTCFullYear(), min.getUTCMonth(), min.getUTCDate() - min.getUTCDay()));
|
|
20
|
+
const cells = parsed.map((d) => {
|
|
21
|
+
const weekday = d.dateObj.getUTCDay();
|
|
22
|
+
const daysSinceStart = Math.round((d.dateObj.getTime() - firstWeekStart.getTime()) / MS_PER_DAY);
|
|
23
|
+
const week = Math.floor(daysSinceStart / 7);
|
|
24
|
+
return { date: d.date, value: d.value, week, weekday };
|
|
25
|
+
});
|
|
26
|
+
const weekCount = Math.max(...cells.map((c) => c.week)) + 1;
|
|
27
|
+
const monthLabels = [];
|
|
28
|
+
let lastMonth = -1;
|
|
29
|
+
for (const cell of cells.slice().sort((a, b) => a.week - b.week)) {
|
|
30
|
+
const cellDate = parseIsoDate(cell.date);
|
|
31
|
+
const month = cellDate.getUTCMonth();
|
|
32
|
+
if (cell.weekday === 0 && month !== lastMonth) {
|
|
33
|
+
lastMonth = month;
|
|
34
|
+
monthLabels.push({
|
|
35
|
+
week: cell.week,
|
|
36
|
+
label: cellDate.toLocaleString('en', { month: 'short', timeZone: 'UTC' }),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { cells, weekCount, firstWeekStart, monthLabels };
|
|
41
|
+
}
|
|
42
|
+
/** Splits `value`'s rank within `sorted` (ascending, pre-sorted by the caller) into `bucketCount` quartile-style buckets. */
|
|
43
|
+
export function quartileBucket(value, sorted, bucketCount) {
|
|
44
|
+
if (sorted.length === 0)
|
|
45
|
+
return 0;
|
|
46
|
+
let count = 0;
|
|
47
|
+
for (const v of sorted)
|
|
48
|
+
if (v <= value)
|
|
49
|
+
count++;
|
|
50
|
+
const rank = count / sorted.length;
|
|
51
|
+
return Math.min(bucketCount - 1, Math.floor(rank * bucketCount));
|
|
52
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/** Maps `value` in `[lo, hi]` to a 0.1–1.0 alpha, matching the cd-heatmap ramp. */
|
|
2
2
|
export declare function linearAlpha(value: number, lo: number, hi: number): number;
|
|
3
|
+
/**
|
|
4
|
+
* Computes `[min, max]` of `values` via a linear scan, or `null` for an
|
|
5
|
+
* empty array. Deliberately not `Math.min(...values)`/`Math.max(...values)`
|
|
6
|
+
* — spreading a large array as call arguments throws `RangeError: Maximum
|
|
7
|
+
* call stack size exceeded` once the engine's argument-list limit is
|
|
8
|
+
* exceeded (verified at ~150k+ elements).
|
|
9
|
+
*/
|
|
10
|
+
export declare function minMax(values: number[]): [number, number] | null;
|
|
3
11
|
/**
|
|
4
12
|
* Square-root-scaled bucket index in `[0, steps-1]`, or `-1` for no-data.
|
|
5
13
|
* Compresses large counts so a single heavy cell doesn't wash out the rest
|
|
@@ -4,6 +4,27 @@ export function linearAlpha(value, lo, hi) {
|
|
|
4
4
|
const t = (value - lo) / span;
|
|
5
5
|
return 0.1 + 0.9 * Math.min(1, Math.max(0, t));
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Computes `[min, max]` of `values` via a linear scan, or `null` for an
|
|
9
|
+
* empty array. Deliberately not `Math.min(...values)`/`Math.max(...values)`
|
|
10
|
+
* — spreading a large array as call arguments throws `RangeError: Maximum
|
|
11
|
+
* call stack size exceeded` once the engine's argument-list limit is
|
|
12
|
+
* exceeded (verified at ~150k+ elements).
|
|
13
|
+
*/
|
|
14
|
+
export function minMax(values) {
|
|
15
|
+
if (values.length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
let lo = values[0];
|
|
18
|
+
let hi = values[0];
|
|
19
|
+
for (let i = 1; i < values.length; i++) {
|
|
20
|
+
const v = values[i];
|
|
21
|
+
if (v < lo)
|
|
22
|
+
lo = v;
|
|
23
|
+
if (v > hi)
|
|
24
|
+
hi = v;
|
|
25
|
+
}
|
|
26
|
+
return [lo, hi];
|
|
27
|
+
}
|
|
7
28
|
/**
|
|
8
29
|
* Square-root-scaled bucket index in `[0, steps-1]`, or `-1` for no-data.
|
|
9
30
|
* Compresses large counts so a single heavy cell doesn't wash out the rest
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type TemplateResult } from 'lit';
|
|
2
2
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
import { type CalendarDay } from './calendar-grid.js';
|
|
3
4
|
/**
|
|
4
5
|
* Parses a strict `#rgb` or `#rrggbb` hex string into an `[r, g, b]` triple,
|
|
5
6
|
* or `null` if `hex` isn't one (rather than silently coercing an unparsable
|
|
@@ -43,6 +44,18 @@ export declare class LyraHeatmap extends LyraElement {
|
|
|
43
44
|
cellSize: number;
|
|
44
45
|
valueLabel: string;
|
|
45
46
|
scale: 'linear' | 'sqrt';
|
|
47
|
+
/**
|
|
48
|
+
* When set, `cellSize` is derived from the host's measured `clientWidth`
|
|
49
|
+
* on every draw (including ResizeObserver-triggered redraws) instead of
|
|
50
|
+
* the fixed `cell-size` attribute, so the grid actually fills the
|
|
51
|
+
* available width. Without this, canvas dimensions are computed purely
|
|
52
|
+
* from `PAD_LEFT + cols * cellSize`, so a resize-triggered redraw is a
|
|
53
|
+
* geometric no-op.
|
|
54
|
+
*/
|
|
55
|
+
fitToWidth: boolean;
|
|
56
|
+
mode: 'matrix' | 'calendar';
|
|
57
|
+
days: CalendarDay[];
|
|
58
|
+
bucketCount: number;
|
|
46
59
|
private canvas?;
|
|
47
60
|
private resizeObserver?;
|
|
48
61
|
private dprQuery?;
|
|
@@ -51,12 +64,18 @@ export declare class LyraHeatmap extends LyraElement {
|
|
|
51
64
|
private watchDpr;
|
|
52
65
|
private onDprChange;
|
|
53
66
|
protected willUpdate(): void;
|
|
67
|
+
/** The real (non-no-data) value range across `values` (or `days` in calendar mode), or `null` if there is none. Shared by `willUpdate()`, `draw()`, and the legend. */
|
|
68
|
+
private valueRange;
|
|
54
69
|
protected updated(): void;
|
|
55
70
|
/** Reads the customizable ramp endpoints off the host's computed style. */
|
|
56
71
|
private scaleEndpoints;
|
|
57
72
|
/** Resolves the `--lyra-color-text-quiet` chrome token for axis labels. */
|
|
58
73
|
private labelColor;
|
|
74
|
+
/** Reads the customizable no-data cell fill off the host's computed style. */
|
|
75
|
+
private noDataFill;
|
|
59
76
|
private draw;
|
|
77
|
+
private drawCalendar;
|
|
78
|
+
private drawMatrix;
|
|
60
79
|
render(): TemplateResult;
|
|
61
80
|
}
|
|
62
81
|
declare global {
|