@aceshooting/lyra-ui 0.1.3 → 0.1.5

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 (82) hide show
  1. package/README.md +2 -8
  2. package/custom-elements.json +4046 -3214
  3. package/dist/components/chart/bar-chart.js +17 -4
  4. package/dist/components/chart/box-plot.d.ts +8 -1
  5. package/dist/components/chart/box-plot.js +40 -4
  6. package/dist/components/chart/box-plot.styles.js +4 -0
  7. package/dist/components/chart/bubble-chart.d.ts +1 -2
  8. package/dist/components/chart/bubble-chart.js +17 -4
  9. package/dist/components/chart/chart-loader.d.ts +21 -4
  10. package/dist/components/chart/chart-loader.js +39 -12
  11. package/dist/components/chart/chart.d.ts +9 -3
  12. package/dist/components/chart/chart.js +42 -21
  13. package/dist/components/chart/doughnut-chart.d.ts +1 -2
  14. package/dist/components/chart/doughnut-chart.js +17 -4
  15. package/dist/components/chart/histogram-bin.js +12 -2
  16. package/dist/components/chart/histogram.d.ts +2 -2
  17. package/dist/components/chart/histogram.js +16 -2
  18. package/dist/components/chart/line-chart.js +17 -4
  19. package/dist/components/chart/pie-chart.d.ts +1 -2
  20. package/dist/components/chart/pie-chart.js +17 -4
  21. package/dist/components/chart/polar-area-chart.d.ts +1 -2
  22. package/dist/components/chart/polar-area-chart.js +17 -4
  23. package/dist/components/chart/radar-chart.d.ts +1 -2
  24. package/dist/components/chart/radar-chart.js +17 -4
  25. package/dist/components/chart/scatter-chart.js +17 -4
  26. package/dist/components/combobox/combobox.js +3 -4
  27. package/dist/components/combobox/combobox.stories.d.ts +6 -0
  28. package/dist/components/combobox/combobox.stories.js +27 -0
  29. package/dist/components/combobox/combobox.styles.js +2 -2
  30. package/dist/components/date-picker/date-input.stories.d.ts +5 -0
  31. package/dist/components/date-picker/date-input.stories.js +12 -0
  32. package/dist/components/date-picker/date-input.styles.js +2 -2
  33. package/dist/components/date-picker/date-picker.stories.d.ts +6 -0
  34. package/dist/components/date-picker/date-picker.stories.js +13 -0
  35. package/dist/components/empty/empty.d.ts +1 -1
  36. package/dist/components/empty/empty.js +1 -1
  37. package/dist/components/export-button/export-button.d.ts +2 -2
  38. package/dist/components/export-button/export-button.js +2 -2
  39. package/dist/components/file-input/accept.d.ts +1 -2
  40. package/dist/components/file-input/accept.js +1 -2
  41. package/dist/components/file-input/file-input.d.ts +2 -2
  42. package/dist/components/file-input/file-input.js +2 -2
  43. package/dist/components/flag/flag.stories.d.ts +5 -0
  44. package/dist/components/flag/flag.stories.js +17 -0
  45. package/dist/components/flag/flag.styles.js +1 -1
  46. package/dist/components/gauge/gauge.d.ts +1 -1
  47. package/dist/components/gauge/gauge.js +1 -1
  48. package/dist/components/graph/graph.d.ts +33 -4
  49. package/dist/components/graph/graph.js +146 -30
  50. package/dist/components/heatmap/calendar-grid.d.ts +31 -0
  51. package/dist/components/heatmap/calendar-grid.js +52 -0
  52. package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
  53. package/dist/components/heatmap/heatmap-scale.js +21 -0
  54. package/dist/components/heatmap/heatmap.d.ts +19 -0
  55. package/dist/components/heatmap/heatmap.js +133 -20
  56. package/dist/components/heatmap/heatmap.styles.js +4 -0
  57. package/dist/components/map/map.d.ts +15 -0
  58. package/dist/components/map/map.js +89 -7
  59. package/dist/components/playback/playback.d.ts +2 -2
  60. package/dist/components/playback/playback.js +8 -2
  61. package/dist/components/skeleton/skeleton.d.ts +2 -2
  62. package/dist/components/skeleton/skeleton.js +2 -2
  63. package/dist/components/sparkline/sparkline.stories.d.ts +7 -0
  64. package/dist/components/sparkline/sparkline.stories.js +17 -0
  65. package/dist/components/stat/stat.d.ts +1 -1
  66. package/dist/components/stat/stat.js +6 -7
  67. package/dist/components/stat/stat.stories.d.ts +5 -0
  68. package/dist/components/stat/stat.stories.js +16 -0
  69. package/dist/components/table/table.d.ts +1 -2
  70. package/dist/components/table/table.js +1 -2
  71. package/dist/components/table/table.styles.js +2 -3
  72. package/dist/components/time-range/time-range.js +4 -4
  73. package/dist/components/toast/toast.stories.d.ts +5 -0
  74. package/dist/components/toast/toast.stories.js +32 -0
  75. package/dist/components/tree/tree-node.d.ts +2 -2
  76. package/dist/components/tree/tree-node.js +2 -2
  77. package/dist/internal/form-associated.js +1 -2
  78. package/dist/internal/icons.js +1 -1
  79. package/dist/internal/tokens.styles.js +3 -3
  80. package/llms-full.txt +59 -83
  81. package/llms.txt +1 -2
  82. 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') || 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();
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
- this.applyInteractions();
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. 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
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.zoom().on('zoom', (event) => {
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
- const nodeEls = this.renderRoot.querySelectorAll('[part="node"]');
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.d3.forceLink(links).distance(this.linkDistance))
174
- .force('charge', this.d3.forceManyBody().strength(this.chargeStrength))
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
- this.simNodes = [...nodes];
179
- this.simLinks = [...links];
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) => svg `<g>
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=${n.color ? `--lyra-node-fill:${n.color}` : ''}
336
+ style=${styleMap(fill ? { '--lyra-node-fill': fill } : {})}
228
337
  @click=${() => this.onNodeClick(n)}
229
338
  @keydown=${(e) => {
230
- if (e.key === 'Enter' || e.key === ' ') {
231
- e.preventDefault();
232
- this.onNodeClick(n);
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
- ? svg `<text part="label" x=${(n.x ?? 0) + this.nodeRadius(n) + 2} y=${n.y ?? 0}>${n.label}</text>`
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 {