@incursa/ui-kit 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -71,6 +71,8 @@ The runtime auto-defines the shipped elements on load. If a consumer needs expli
71
71
  Button, button-group, button-toolbar, close-button, alert, and empty-state custom elements.
72
72
  - [`components/collections.js`](components/collections.js)
73
73
  List-group, key-value-grid, and key-value custom elements.
74
+ - [`components/visualizations.js`](components/visualizations.js)
75
+ D3-backed sparkline custom element and small path/data helpers.
74
76
  - [`components/overlays.js`](components/overlays.js)
75
77
  Disclosure, dialog, and drawer custom elements.
76
78
  - [`index.js`](index.js)
@@ -86,6 +88,7 @@ The Web Component layer should mirror the current CSS kit, not reinterpret it.
86
88
  - Form wrappers should keep native controls native.
87
89
  - Navigation components should reflect keyboard and focus state in the DOM.
88
90
  - Feedback and status shells should announce state accessibly, badge/spinner atoms should standardize the common tone and loading defaults, and action/detail plus collection hosts should keep buttons, alerts, list groups, and description-list pairs declarative without inventing a second styling vocabulary.
91
+ - Visualization components should stay compact, accessible, and evidence-supporting. Use D3 for geometry and scales, not DOM mutation.
89
92
  - Overlays should prefer native `<details>` and `<dialog>` behavior when that satisfies the contract.
90
93
  - Tables, data presentation, utility atoms, and the remaining presentation-only surfaces should remain CSS-first until the component contract is explicit and worth the runtime cost.
91
94
 
@@ -12,6 +12,7 @@ The runtime defines the approved v1 host family set:
12
12
  - feedback and status: `inc-state-panel`, `inc-live-region`, `inc-auto-refresh`, `inc-theme-switcher`, `inc-badge`, `inc-spinner`
13
13
  - actions and detail shells: `inc-button`, `inc-button-group`, `inc-button-toolbar`, `inc-close-button`, `inc-alert`, `inc-empty-state`
14
14
  - collections: `inc-list-group`, `inc-key-value-grid`, `inc-key-value`
15
+ - data visualization: `inc-sparkline`
15
16
  - overlays: `inc-disclosure`, `inc-dialog`, `inc-drawer`
16
17
 
17
18
  ## Contract shape
@@ -28,6 +29,7 @@ The runtime defines the approved v1 host family set:
28
29
  - `components/feedback.js`
29
30
  - `components/actions.js`
30
31
  - `components/collections.js`
32
+ - `components/visualizations.js`
31
33
  - `components/overlays.js`
32
34
  - Public registration API is idempotent:
33
35
  - `window.IncWebComponents.defineAll()`
@@ -0,0 +1,629 @@
1
+ import { extent } from "d3-array";
2
+ import { scaleLinear, scaleTime } from "d3-scale";
3
+ import {
4
+ area as d3Area,
5
+ curveLinear,
6
+ curveMonotoneX,
7
+ curveStepAfter,
8
+ line as d3Line,
9
+ } from "d3-shape";
10
+
11
+ import { createUniqueId } from "../shared.js";
12
+
13
+ const SVG_NS = "http://www.w3.org/2000/svg";
14
+ const DEFAULT_WIDTH = 120;
15
+ const DEFAULT_HEIGHT = 32;
16
+ const DEFAULT_PADDING = 3;
17
+ const VALID_VARIANTS = new Set(["line", "area", "bar"]);
18
+ const VALID_TONES = new Set(["default", "positive", "negative", "muted", "accent"]);
19
+ const VALID_CURVES = new Set(["linear", "monotone", "step"]);
20
+ const CURVES = {
21
+ linear: curveLinear,
22
+ monotone: curveMonotoneX,
23
+ step: curveStepAfter,
24
+ };
25
+
26
+ function toFiniteNumber(value) {
27
+ if (value === null || value === undefined || value === "") {
28
+ return null;
29
+ }
30
+
31
+ const parsed = Number(value);
32
+ return Number.isFinite(parsed) ? parsed : null;
33
+ }
34
+
35
+ function normalizeToken(value, allowedValues, fallback) {
36
+ const normalized = String(value ?? "").trim().toLowerCase();
37
+ return allowedValues.has(normalized) ? normalized : fallback;
38
+ }
39
+
40
+ function parseXValue(value, index) {
41
+ if (value instanceof Date && Number.isFinite(value.getTime())) {
42
+ return value;
43
+ }
44
+
45
+ if (typeof value === "number" && Number.isFinite(value)) {
46
+ return value;
47
+ }
48
+
49
+ if (typeof value === "string" && value.trim()) {
50
+ const numeric = Number(value);
51
+ if (Number.isFinite(numeric)) {
52
+ return numeric;
53
+ }
54
+
55
+ const date = new Date(value);
56
+ if (Number.isFinite(date.getTime())) {
57
+ return date;
58
+ }
59
+ }
60
+
61
+ return index;
62
+ }
63
+
64
+ function normalizeInputPoint(item, index) {
65
+ if (typeof item === "number") {
66
+ return { x: index, y: Number.isFinite(item) ? item : null };
67
+ }
68
+
69
+ if (item instanceof Date) {
70
+ return { x: index, y: null };
71
+ }
72
+
73
+ if (item && typeof item === "object") {
74
+ return {
75
+ x: item.x ?? index,
76
+ y: toFiniteNumber(item.y),
77
+ };
78
+ }
79
+
80
+ const numeric = toFiniteNumber(item);
81
+ return { x: index, y: numeric };
82
+ }
83
+
84
+ function parseSparklinePoints(input) {
85
+ if (Array.isArray(input)) {
86
+ return input.map(normalizeInputPoint);
87
+ }
88
+
89
+ if (input === null || input === undefined) {
90
+ return [];
91
+ }
92
+
93
+ if (typeof input === "string") {
94
+ const text = input.trim();
95
+ if (!text) {
96
+ return [];
97
+ }
98
+
99
+ if (text.startsWith("[")) {
100
+ try {
101
+ const parsed = JSON.parse(text);
102
+ return parseSparklinePoints(parsed);
103
+ } catch {
104
+ return [];
105
+ }
106
+ }
107
+
108
+ return text
109
+ .split(/[\s,;]+/u)
110
+ .filter(Boolean)
111
+ .map((part, index) => ({ x: index, y: toFiniteNumber(part) }));
112
+ }
113
+
114
+ return [];
115
+ }
116
+
117
+ function normalizeSparklineExtent(points, options = {}) {
118
+ const normalizedPoints = parseSparklinePoints(points);
119
+ const referenceValue = toFiniteNumber(options.referenceValue);
120
+ const prepared = normalizedPoints.map((point, index) => ({
121
+ x: point.x,
122
+ xValue: parseXValue(point.x, index),
123
+ y: toFiniteNumber(point.y),
124
+ index,
125
+ }));
126
+ const validPoints = prepared.filter((point) => point.y !== null);
127
+ const yValues = validPoints.map((point) => point.y);
128
+
129
+ if (referenceValue !== null) {
130
+ yValues.push(referenceValue);
131
+ }
132
+
133
+ if (!validPoints.length) {
134
+ return {
135
+ empty: true,
136
+ points: prepared,
137
+ validPoints,
138
+ xDomain: [0, 1],
139
+ yDomain: [0, 1],
140
+ xMode: "index",
141
+ referenceValue,
142
+ };
143
+ }
144
+
145
+ let yDomain = extent(yValues);
146
+ if (!Number.isFinite(yDomain[0]) || !Number.isFinite(yDomain[1])) {
147
+ yDomain = [0, 1];
148
+ }
149
+
150
+ if (Object.is(yDomain[0], yDomain[1])) {
151
+ const value = yDomain[0];
152
+ const padding = value === 0 ? 1 : Math.max(Math.abs(value) * 0.08, 1);
153
+ yDomain = [value - padding, value + padding];
154
+ }
155
+
156
+ const allDates = prepared.every((point) => point.xValue instanceof Date);
157
+ const allNumbers = prepared.every((point) => typeof point.xValue === "number" && Number.isFinite(point.xValue));
158
+ const xMode = allDates ? "time" : (allNumbers ? "number" : "index");
159
+ const xValues = prepared.map((point) => xMode === "index" ? point.index : point.xValue);
160
+ let xDomain = extent(xValues);
161
+
162
+ if (xMode === "time") {
163
+ const left = xDomain[0] instanceof Date ? xDomain[0].getTime() : NaN;
164
+ const right = xDomain[1] instanceof Date ? xDomain[1].getTime() : NaN;
165
+ if (!Number.isFinite(left) || !Number.isFinite(right)) {
166
+ xDomain = [new Date(0), new Date(1)];
167
+ } else if (left === right) {
168
+ xDomain = [new Date(left - 1), new Date(right + 1)];
169
+ }
170
+ } else {
171
+ if (!Number.isFinite(xDomain[0]) || !Number.isFinite(xDomain[1])) {
172
+ xDomain = [0, Math.max(prepared.length - 1, 1)];
173
+ } else if (Object.is(xDomain[0], xDomain[1])) {
174
+ xDomain = [xDomain[0] - 1, xDomain[1] + 1];
175
+ }
176
+ }
177
+
178
+ return {
179
+ empty: false,
180
+ points: prepared,
181
+ validPoints,
182
+ xDomain,
183
+ yDomain,
184
+ xMode,
185
+ referenceValue,
186
+ };
187
+ }
188
+
189
+ function clamp(value, min, max) {
190
+ return Math.max(min, Math.min(max, value));
191
+ }
192
+
193
+ function round(value) {
194
+ return Number.isFinite(value) ? Number(value.toFixed(3)) : 0;
195
+ }
196
+
197
+ function createScales(extentInfo, width, height, padding) {
198
+ const xRange = [padding, Math.max(width - padding, padding)];
199
+ const yRange = [Math.max(height - padding, padding), padding];
200
+ const xScale = extentInfo.xMode === "time"
201
+ ? scaleTime(extentInfo.xDomain, xRange)
202
+ : scaleLinear(extentInfo.xDomain, xRange);
203
+ const yScale = scaleLinear(extentInfo.yDomain, yRange);
204
+
205
+ return { xScale, yScale };
206
+ }
207
+
208
+ function getX(point, extentInfo) {
209
+ return extentInfo.xMode === "index" ? point.index : point.xValue;
210
+ }
211
+
212
+ function singlePointPath(point, extentInfo, xScale, yScale, width) {
213
+ const center = round(xScale(getX(point, extentInfo)));
214
+ const y = round(yScale(point.y));
215
+ const half = Math.min(4, Math.max(1, width / 20));
216
+ const left = round(clamp(center - half, 0, width));
217
+ const right = round(clamp(center + half, 0, width));
218
+
219
+ return `M${left},${y}L${right},${y}`;
220
+ }
221
+
222
+ function buildSparklinePath(points, options = {}) {
223
+ const width = Math.max(toFiniteNumber(options.width) ?? DEFAULT_WIDTH, 1);
224
+ const height = Math.max(toFiniteNumber(options.height) ?? DEFAULT_HEIGHT, 1);
225
+ const padding = Math.max(toFiniteNumber(options.padding) ?? DEFAULT_PADDING, 0);
226
+ const variant = normalizeToken(options.variant, VALID_VARIANTS, "line");
227
+ const curve = normalizeToken(options.curve, VALID_CURVES, "linear");
228
+ const extentInfo = normalizeSparklineExtent(points, options);
229
+
230
+ if (extentInfo.empty) {
231
+ return {
232
+ empty: true,
233
+ width,
234
+ height,
235
+ padding,
236
+ variant,
237
+ curve,
238
+ linePath: "",
239
+ areaPath: "",
240
+ bars: [],
241
+ marker: null,
242
+ minPoint: null,
243
+ maxPoint: null,
244
+ referenceY: null,
245
+ extent: extentInfo,
246
+ };
247
+ }
248
+
249
+ const { xScale, yScale } = createScales(extentInfo, width, height, padding);
250
+ const curveFactory = CURVES[curve] || curveLinear;
251
+ const lineGenerator = d3Line()
252
+ .defined((point) => point.y !== null)
253
+ .x((point) => round(xScale(getX(point, extentInfo))))
254
+ .y((point) => round(yScale(point.y)))
255
+ .curve(curveFactory);
256
+ const validPoints = extentInfo.validPoints;
257
+ const linePath = validPoints.length === 1
258
+ ? singlePointPath(validPoints[0], extentInfo, xScale, yScale, width)
259
+ : (lineGenerator(extentInfo.points) || "");
260
+ const baseline = clamp(0, extentInfo.yDomain[0], extentInfo.yDomain[1]);
261
+ const areaGenerator = d3Area()
262
+ .defined((point) => point.y !== null)
263
+ .x((point) => round(xScale(getX(point, extentInfo))))
264
+ .y0(round(yScale(baseline)))
265
+ .y1((point) => round(yScale(point.y)))
266
+ .curve(curveFactory);
267
+ const areaPath = validPoints.length > 1 ? (areaGenerator(extentInfo.points) || "") : "";
268
+ const barWidth = Math.max(1, Math.min(8, (width - (padding * 2)) / Math.max(extentInfo.points.length, 1) * 0.58));
269
+ const baselineY = round(yScale(baseline));
270
+ const bars = validPoints.map((point) => {
271
+ const x = round(xScale(getX(point, extentInfo)) - (barWidth / 2));
272
+ const y = round(yScale(point.y));
273
+ return {
274
+ x,
275
+ y: Math.min(y, baselineY),
276
+ width: round(barWidth),
277
+ height: Math.max(1, round(Math.abs(baselineY - y))),
278
+ };
279
+ });
280
+ const lastPoint = validPoints[validPoints.length - 1] || null;
281
+ const marker = lastPoint ? {
282
+ x: round(xScale(getX(lastPoint, extentInfo))),
283
+ y: round(yScale(lastPoint.y)),
284
+ value: lastPoint.y,
285
+ } : null;
286
+ const minPoint = validPoints.reduce((candidate, point) => point.y < candidate.y ? point : candidate, validPoints[0]);
287
+ const maxPoint = validPoints.reduce((candidate, point) => point.y > candidate.y ? point : candidate, validPoints[0]);
288
+ const mapMarker = (point) => point ? {
289
+ x: round(xScale(getX(point, extentInfo))),
290
+ y: round(yScale(point.y)),
291
+ value: point.y,
292
+ } : null;
293
+
294
+ return {
295
+ empty: false,
296
+ width,
297
+ height,
298
+ padding,
299
+ variant,
300
+ curve,
301
+ linePath,
302
+ areaPath,
303
+ bars,
304
+ marker,
305
+ minPoint: mapMarker(minPoint),
306
+ maxPoint: mapMarker(maxPoint),
307
+ referenceY: extentInfo.referenceValue === null ? null : round(yScale(extentInfo.referenceValue)),
308
+ extent: extentInfo,
309
+ };
310
+ }
311
+
312
+ function svgElement(name) {
313
+ return document.createElementNS(SVG_NS, name);
314
+ }
315
+
316
+ function hasInvalidPathData(value) {
317
+ return /(?:NaN|Infinity|-Infinity)/u.test(String(value ?? ""));
318
+ }
319
+
320
+ class IncSparklineElement extends HTMLElement {
321
+ static observedAttributes = [
322
+ "aria-label",
323
+ "curve",
324
+ "empty-label",
325
+ "height",
326
+ "points",
327
+ "reference-value",
328
+ "show-last-marker",
329
+ "show-min-max",
330
+ "tone",
331
+ "values",
332
+ "variant",
333
+ "width",
334
+ ];
335
+
336
+ #hasPropertyPoints = false;
337
+ #propertyPoints = [];
338
+ #titleId = createUniqueId("inc-sparkline-title");
339
+ #descId = createUniqueId("inc-sparkline-desc");
340
+
341
+ connectedCallback() {
342
+ this.#render();
343
+ }
344
+
345
+ attributeChangedCallback() {
346
+ if (this.isConnected) {
347
+ this.#render();
348
+ }
349
+ }
350
+
351
+ get points() {
352
+ if (this.#hasPropertyPoints) {
353
+ return parseSparklinePoints(this.#propertyPoints);
354
+ }
355
+
356
+ if (this.hasAttribute("points")) {
357
+ return parseSparklinePoints(this.getAttribute("points"));
358
+ }
359
+
360
+ return parseSparklinePoints(this.getAttribute("values"));
361
+ }
362
+
363
+ set points(value) {
364
+ if (value === null || value === undefined) {
365
+ this.#hasPropertyPoints = false;
366
+ this.#propertyPoints = [];
367
+ } else {
368
+ this.#hasPropertyPoints = true;
369
+ this.#propertyPoints = value;
370
+ }
371
+
372
+ if (this.isConnected) {
373
+ this.#render();
374
+ }
375
+ }
376
+
377
+ get values() {
378
+ return this.getAttribute("values") || "";
379
+ }
380
+
381
+ set values(value) {
382
+ if (value === null || value === undefined || value === "") {
383
+ this.removeAttribute("values");
384
+ return;
385
+ }
386
+
387
+ this.setAttribute("values", String(value));
388
+ }
389
+
390
+ get width() {
391
+ return Math.max(toFiniteNumber(this.getAttribute("width")) ?? DEFAULT_WIDTH, 1);
392
+ }
393
+
394
+ set width(value) {
395
+ if (value === null || value === undefined || value === "") {
396
+ this.removeAttribute("width");
397
+ return;
398
+ }
399
+
400
+ this.setAttribute("width", String(value));
401
+ }
402
+
403
+ get height() {
404
+ return Math.max(toFiniteNumber(this.getAttribute("height")) ?? DEFAULT_HEIGHT, 1);
405
+ }
406
+
407
+ set height(value) {
408
+ if (value === null || value === undefined || value === "") {
409
+ this.removeAttribute("height");
410
+ return;
411
+ }
412
+
413
+ this.setAttribute("height", String(value));
414
+ }
415
+
416
+ #render() {
417
+ const width = this.width;
418
+ const height = this.height;
419
+ const variant = normalizeToken(this.getAttribute("variant"), VALID_VARIANTS, "line");
420
+ const tone = normalizeToken(this.getAttribute("tone"), VALID_TONES, "default");
421
+ const curve = normalizeToken(this.getAttribute("curve"), VALID_CURVES, "linear");
422
+ const referenceValue = toFiniteNumber(this.getAttribute("reference-value"));
423
+ const label = this.getAttribute("aria-label") || "Sparkline trend";
424
+ const emptyLabel = this.getAttribute("empty-label") ?? "No data";
425
+ const model = buildSparklinePath(this.points, {
426
+ curve,
427
+ height,
428
+ referenceValue,
429
+ variant,
430
+ width,
431
+ });
432
+
433
+ this.classList.add("inc-sparkline");
434
+ [...this.classList]
435
+ .filter((token) => token.startsWith("inc-sparkline--"))
436
+ .forEach((token) => this.classList.remove(token));
437
+ this.classList.add(`inc-sparkline--${variant}`, `inc-sparkline--tone-${tone}`);
438
+ this.style.setProperty("--inc-sparkline-width", `${width}px`);
439
+ this.style.setProperty("--inc-sparkline-height", `${height}px`);
440
+
441
+ const svg = svgElement("svg");
442
+ svg.classList.add("inc-sparkline__svg");
443
+ svg.setAttribute("part", "svg");
444
+ svg.setAttribute("width", String(width));
445
+ svg.setAttribute("height", String(height));
446
+ svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
447
+ svg.setAttribute("role", "img");
448
+ svg.setAttribute("aria-labelledby", `${this.#titleId} ${this.#descId}`);
449
+ svg.setAttribute("focusable", "false");
450
+
451
+ const title = svgElement("title");
452
+ title.id = this.#titleId;
453
+ title.textContent = label;
454
+
455
+ const desc = svgElement("desc");
456
+ desc.id = this.#descId;
457
+ desc.textContent = model.empty
458
+ ? (emptyLabel || "No sparkline data available.")
459
+ : this.#buildDescription(model);
460
+
461
+ svg.append(title, desc);
462
+
463
+ if (model.empty || hasInvalidPathData(model.linePath) || hasInvalidPathData(model.areaPath)) {
464
+ this.#renderEmpty(svg, width, height, emptyLabel);
465
+ this.replaceChildren(svg);
466
+ return;
467
+ }
468
+
469
+ if (model.referenceY !== null) {
470
+ const reference = svgElement("line");
471
+ reference.classList.add("inc-sparkline__reference");
472
+ reference.setAttribute("part", "reference");
473
+ reference.setAttribute("x1", String(model.padding));
474
+ reference.setAttribute("x2", String(width - model.padding));
475
+ reference.setAttribute("y1", String(model.referenceY));
476
+ reference.setAttribute("y2", String(model.referenceY));
477
+ reference.setAttribute("vector-effect", "non-scaling-stroke");
478
+ svg.append(reference);
479
+ }
480
+
481
+ if (variant === "bar") {
482
+ model.bars.forEach((bar) => {
483
+ const rect = svgElement("rect");
484
+ rect.classList.add("inc-sparkline__bar");
485
+ rect.setAttribute("part", "bar");
486
+ rect.setAttribute("x", String(bar.x));
487
+ rect.setAttribute("y", String(bar.y));
488
+ rect.setAttribute("width", String(bar.width));
489
+ rect.setAttribute("height", String(bar.height));
490
+ svg.append(rect);
491
+ });
492
+ } else {
493
+ if (variant === "area" && model.areaPath) {
494
+ const area = svgElement("path");
495
+ area.classList.add("inc-sparkline__area");
496
+ area.setAttribute("part", "area");
497
+ area.setAttribute("d", model.areaPath);
498
+ area.setAttribute("vector-effect", "non-scaling-stroke");
499
+ svg.append(area);
500
+ }
501
+
502
+ const line = svgElement("path");
503
+ line.classList.add("inc-sparkline__line");
504
+ line.setAttribute("part", "line");
505
+ line.setAttribute("d", model.linePath);
506
+ line.setAttribute("vector-effect", "non-scaling-stroke");
507
+ svg.append(line);
508
+ }
509
+
510
+ if (this.hasAttribute("show-min-max")) {
511
+ this.#appendMarker(svg, model.minPoint, "min");
512
+ if (model.maxPoint?.x !== model.minPoint?.x || model.maxPoint?.y !== model.minPoint?.y) {
513
+ this.#appendMarker(svg, model.maxPoint, "max");
514
+ }
515
+ }
516
+
517
+ if (this.hasAttribute("show-last-marker")) {
518
+ this.#appendMarker(svg, model.marker, "last");
519
+ }
520
+
521
+ this.replaceChildren(svg);
522
+ }
523
+
524
+ #renderEmpty(svg, width, height, emptyLabel) {
525
+ const line = svgElement("line");
526
+ line.classList.add("inc-sparkline__empty-line");
527
+ line.setAttribute("part", "line");
528
+ line.setAttribute("x1", "3");
529
+ line.setAttribute("x2", String(Math.max(width - 3, 3)));
530
+ line.setAttribute("y1", String(round(height / 2)));
531
+ line.setAttribute("y2", String(round(height / 2)));
532
+ line.setAttribute("vector-effect", "non-scaling-stroke");
533
+ svg.append(line);
534
+
535
+ if (emptyLabel) {
536
+ const text = svgElement("text");
537
+ text.classList.add("inc-sparkline__empty");
538
+ text.setAttribute("part", "empty");
539
+ text.setAttribute("x", String(round(width / 2)));
540
+ text.setAttribute("y", String(round((height / 2) + 3)));
541
+ text.setAttribute("text-anchor", "middle");
542
+ text.textContent = emptyLabel;
543
+ svg.append(text);
544
+ }
545
+ }
546
+
547
+ #appendMarker(svg, point, modifier) {
548
+ if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
549
+ return;
550
+ }
551
+
552
+ const marker = svgElement("circle");
553
+ marker.classList.add("inc-sparkline__marker", `inc-sparkline__marker--${modifier}`);
554
+ marker.setAttribute("part", "marker");
555
+ marker.setAttribute("cx", String(point.x));
556
+ marker.setAttribute("cy", String(point.y));
557
+ marker.setAttribute("r", modifier === "last" ? "2.4" : "1.8");
558
+ marker.setAttribute("vector-effect", "non-scaling-stroke");
559
+ svg.append(marker);
560
+ }
561
+
562
+ #buildDescription(model) {
563
+ const values = model.extent.validPoints.map((point) => point.y);
564
+ const min = Math.min(...values);
565
+ const max = Math.max(...values);
566
+ const latest = model.marker?.value;
567
+ const parts = [`${values.length} data ${values.length === 1 ? "point" : "points"}.`];
568
+
569
+ if (Number.isFinite(latest)) {
570
+ parts.push(`Latest value ${latest}.`);
571
+ }
572
+
573
+ if (Number.isFinite(min) && Number.isFinite(max)) {
574
+ parts.push(`Range ${min} to ${max}.`);
575
+ }
576
+
577
+ if (model.extent.referenceValue !== null) {
578
+ parts.push(`Reference value ${model.extent.referenceValue}.`);
579
+ }
580
+
581
+ return parts.join(" ");
582
+ }
583
+ }
584
+
585
+ const visualizationDefinitions = [
586
+ ["inc-sparkline", IncSparklineElement],
587
+ ];
588
+
589
+ const visualizationComponents = {
590
+ IncSparklineElement,
591
+ };
592
+
593
+ function defineVisualizationComponents(registry = globalThis.customElements) {
594
+ if (!registry || typeof registry.define !== "function" || typeof registry.get !== "function") {
595
+ return [];
596
+ }
597
+
598
+ const defined = [];
599
+ for (const [tagName, ctor] of visualizationDefinitions) {
600
+ if (!registry.get(tagName)) {
601
+ registry.define(tagName, ctor);
602
+ defined.push(tagName);
603
+ }
604
+ }
605
+
606
+ return defined;
607
+ }
608
+
609
+ if (typeof globalThis !== "undefined") {
610
+ const namespace = globalThis.IncWebComponents || (globalThis.IncWebComponents = {});
611
+ namespace.visualizations = Object.assign({}, namespace.visualizations, {
612
+ buildSparklinePath,
613
+ defineVisualizationComponents,
614
+ normalizeSparklineExtent,
615
+ parseSparklinePoints,
616
+ visualizationDefinitions,
617
+ components: visualizationComponents,
618
+ });
619
+ }
620
+
621
+ export {
622
+ IncSparklineElement,
623
+ buildSparklinePath,
624
+ defineVisualizationComponents,
625
+ normalizeSparklineExtent,
626
+ parseSparklinePoints,
627
+ visualizationComponents,
628
+ visualizationDefinitions,
629
+ };