@aceshooting/lyra-ui 1.0.1 → 1.2.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.
Files changed (150) hide show
  1. package/README.md +9 -10
  2. package/custom-elements.json +15118 -8342
  3. package/dist/components/chart/chart.d.ts +57 -1
  4. package/dist/components/chart/chart.js +202 -12
  5. package/dist/components/chart/chart.stories.d.ts +32 -0
  6. package/dist/components/chart/chart.stories.js +141 -0
  7. package/dist/components/chart/chart.styles.js +16 -0
  8. package/dist/components/chart/histogram.d.ts +2 -0
  9. package/dist/components/chart/histogram.js +18 -2
  10. package/dist/components/chart/line-chart.stories.d.ts +17 -0
  11. package/dist/components/chart/line-chart.stories.js +75 -0
  12. package/dist/components/chart/lite-chart.d.ts +83 -0
  13. package/dist/components/chart/lite-chart.js +424 -0
  14. package/dist/components/chart/lite-chart.stories.d.ts +14 -0
  15. package/dist/components/chart/lite-chart.stories.js +114 -0
  16. package/dist/components/chart/lite-chart.styles.d.ts +1 -0
  17. package/dist/components/chart/lite-chart.styles.js +83 -0
  18. package/dist/components/combobox/combobox.d.ts +9 -0
  19. package/dist/components/combobox/combobox.js +76 -13
  20. package/dist/components/combobox/combobox.stories.d.ts +18 -0
  21. package/dist/components/combobox/combobox.stories.js +75 -0
  22. package/dist/components/date-picker/calendar-core.js +13 -2
  23. package/dist/components/date-picker/date-input.d.ts +9 -1
  24. package/dist/components/date-picker/date-input.js +55 -7
  25. package/dist/components/date-picker/date-input.stories.d.ts +5 -0
  26. package/dist/components/date-picker/date-input.stories.js +36 -0
  27. package/dist/components/date-picker/date-picker.d.ts +11 -3
  28. package/dist/components/date-picker/date-picker.js +87 -37
  29. package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
  30. package/dist/components/date-picker/date-picker.stories.js +12 -0
  31. package/dist/components/date-picker/date-picker.styles.js +1 -1
  32. package/dist/components/empty/empty.d.ts +12 -1
  33. package/dist/components/empty/empty.js +20 -3
  34. package/dist/components/empty/empty.stories.d.ts +2 -0
  35. package/dist/components/empty/empty.stories.js +20 -0
  36. package/dist/components/empty/empty.styles.js +11 -0
  37. package/dist/components/export-button/csv.js +6 -2
  38. package/dist/components/export-button/export-button.d.ts +15 -1
  39. package/dist/components/export-button/export-button.js +89 -12
  40. package/dist/components/export-button/export-button.stories.d.ts +1 -0
  41. package/dist/components/export-button/export-button.stories.js +5 -0
  42. package/dist/components/export-button/export-button.styles.js +11 -3
  43. package/dist/components/file-input/accept.d.ts +8 -2
  44. package/dist/components/file-input/accept.js +9 -3
  45. package/dist/components/file-input/file-input.d.ts +5 -2
  46. package/dist/components/file-input/file-input.js +23 -8
  47. package/dist/components/file-input/file-input.stories.d.ts +2 -0
  48. package/dist/components/file-input/file-input.stories.js +9 -0
  49. package/dist/components/flag/flag.d.ts +69 -3
  50. package/dist/components/flag/flag.js +107 -30
  51. package/dist/components/flag/flag.stories.d.ts +1 -0
  52. package/dist/components/flag/flag.stories.js +14 -0
  53. package/dist/components/flag/flag.styles.js +1 -1
  54. package/dist/components/flag/language-map.d.ts +11 -0
  55. package/dist/components/flag/language-map.js +12 -1
  56. package/dist/components/gauge/gauge.d.ts +2 -0
  57. package/dist/components/gauge/gauge.js +19 -5
  58. package/dist/components/gauge/gauge.stories.d.ts +2 -0
  59. package/dist/components/gauge/gauge.stories.js +6 -0
  60. package/dist/components/graph/graph-loader.d.ts +28 -0
  61. package/dist/components/graph/graph-loader.js +44 -0
  62. package/dist/components/graph/graph.d.ts +41 -15
  63. package/dist/components/graph/graph.js +146 -71
  64. package/dist/components/graph/graph.stories.d.ts +3 -0
  65. package/dist/components/graph/graph.stories.js +38 -0
  66. package/dist/components/graph/graph.styles.js +6 -1
  67. package/dist/components/heatmap/calendar-grid.d.ts +7 -0
  68. package/dist/components/heatmap/calendar-grid.js +27 -10
  69. package/dist/components/heatmap/heatmap.d.ts +149 -12
  70. package/dist/components/heatmap/heatmap.js +445 -28
  71. package/dist/components/heatmap/heatmap.stories.d.ts +22 -0
  72. package/dist/components/heatmap/heatmap.stories.js +155 -0
  73. package/dist/components/heatmap/heatmap.styles.js +60 -2
  74. package/dist/components/map/map.d.ts +17 -1
  75. package/dist/components/map/map.js +68 -21
  76. package/dist/components/map/map.stories.d.ts +10 -0
  77. package/dist/components/map/map.stories.js +107 -12
  78. package/dist/components/map/map.styles.js +2 -2
  79. package/dist/components/playback/playback.d.ts +6 -2
  80. package/dist/components/playback/playback.js +36 -12
  81. package/dist/components/playback/playback.stories.d.ts +2 -0
  82. package/dist/components/playback/playback.stories.js +6 -0
  83. package/dist/components/playback/playback.styles.js +10 -2
  84. package/dist/components/select/select.d.ts +125 -0
  85. package/dist/components/select/select.js +480 -0
  86. package/dist/components/select/select.stories.d.ts +21 -0
  87. package/dist/components/select/select.stories.js +91 -0
  88. package/dist/components/select/select.styles.d.ts +1 -0
  89. package/dist/components/select/select.styles.js +212 -0
  90. package/dist/components/skeleton/skeleton.js +4 -7
  91. package/dist/components/sparkline/sparkline.d.ts +1 -1
  92. package/dist/components/sparkline/sparkline.js +40 -13
  93. package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
  94. package/dist/components/sparkline/sparkline.stories.js +22 -0
  95. package/dist/components/split/split.d.ts +24 -3
  96. package/dist/components/split/split.js +155 -31
  97. package/dist/components/split/split.stories.d.ts +2 -0
  98. package/dist/components/split/split.stories.js +20 -0
  99. package/dist/components/stat/stat.d.ts +43 -1
  100. package/dist/components/stat/stat.js +97 -4
  101. package/dist/components/stat/stat.stories.d.ts +4 -0
  102. package/dist/components/stat/stat.stories.js +66 -1
  103. package/dist/components/stat/stat.styles.js +64 -0
  104. package/dist/components/table/table.d.ts +64 -2
  105. package/dist/components/table/table.js +213 -17
  106. package/dist/components/table/table.stories.d.ts +6 -0
  107. package/dist/components/table/table.stories.js +45 -0
  108. package/dist/components/table/table.styles.js +51 -4
  109. package/dist/components/time-range/time-range.d.ts +34 -5
  110. package/dist/components/time-range/time-range.js +217 -36
  111. package/dist/components/time-range/time-range.stories.d.ts +2 -0
  112. package/dist/components/time-range/time-range.stories.js +13 -0
  113. package/dist/components/time-range/time-range.styles.js +44 -2
  114. package/dist/components/toast/toast-item.d.ts +14 -2
  115. package/dist/components/toast/toast-item.js +134 -17
  116. package/dist/components/toast/toast-item.styles.js +28 -2
  117. package/dist/components/toast/toast.stories.d.ts +3 -0
  118. package/dist/components/toast/toast.stories.js +41 -1
  119. package/dist/components/toast/toast.styles.js +11 -11
  120. package/dist/components/toast/toaster.d.ts +1 -1
  121. package/dist/components/toast/toaster.js +13 -6
  122. package/dist/components/tree/tree-node.d.ts +13 -7
  123. package/dist/components/tree/tree-node.js +24 -10
  124. package/dist/components/tree/tree.d.ts +64 -15
  125. package/dist/components/tree/tree.js +113 -22
  126. package/dist/components/tree/tree.stories.d.ts +4 -0
  127. package/dist/components/tree/tree.stories.js +31 -1
  128. package/dist/components/tree/update-cascade.d.ts +13 -0
  129. package/dist/components/tree/update-cascade.js +13 -0
  130. package/dist/components/widget/widget.d.ts +25 -1
  131. package/dist/components/widget/widget.js +164 -6
  132. package/dist/components/widget/widget.stories.d.ts +3 -0
  133. package/dist/components/widget/widget.stories.js +43 -0
  134. package/dist/components/widget/widget.styles.js +20 -3
  135. package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
  136. package/dist/components/word-cloud/word-cloud-layout.js +145 -0
  137. package/dist/components/word-cloud/word-cloud.d.ts +65 -0
  138. package/dist/components/word-cloud/word-cloud.js +255 -0
  139. package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
  140. package/dist/components/word-cloud/word-cloud.stories.js +55 -0
  141. package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
  142. package/dist/components/word-cloud/word-cloud.styles.js +55 -0
  143. package/dist/internal/a11y.js +2 -1
  144. package/dist/internal/rtl.d.ts +7 -0
  145. package/dist/internal/rtl.js +9 -0
  146. package/dist/lyra.d.ts +9 -1
  147. package/dist/lyra.js +6 -0
  148. package/llms-full.txt +962 -276
  149. package/llms.txt +12 -9
  150. package/package.json +6 -4
@@ -40,6 +40,22 @@ __decorate([
40
40
  __decorate([
41
41
  property()
42
42
  ], LyraHistogram.prototype, "label", void 0);
43
+ // Both the `labels` and `datasets` accessors below derive from the same
44
+ // `binValues(values, bins)` pass, and `LyraChart` reads `datasets` more than
45
+ // once per `draw()`/`render()` — memoize the last bucketing result per
46
+ // instance (keyed by reference equality on `values`/`bins`, matching Lit's
47
+ // own change detection for the `values` array) so an unrelated property
48
+ // change doesn't re-run the O(n) bucketing loop from scratch on every access.
49
+ const bucketCache = new WeakMap();
50
+ export function binnedBuckets(el) {
51
+ const cached = bucketCache.get(el);
52
+ if (cached && cached.values === el.values && cached.bins === el.bins) {
53
+ return cached.buckets;
54
+ }
55
+ const buckets = binValues(el.values, el.bins);
56
+ bucketCache.set(el, { values: el.values, bins: el.bins, buckets });
57
+ return buckets;
58
+ }
43
59
  // `labels`/`datasets` are computed from `values`/`bins` rather than settable
44
60
  // props. `LyraChart` declares both as plain (decorator-managed) class
45
61
  // fields, and TypeScript forbids a subclass from re-declaring a base field
@@ -51,7 +67,7 @@ Object.defineProperty(LyraHistogram.prototype, 'labels', {
51
67
  configurable: true,
52
68
  enumerable: true,
53
69
  get() {
54
- return binValues(this.values, this.bins).map((b) => b.label);
70
+ return binnedBuckets(this).map((b) => b.label);
55
71
  },
56
72
  set(_v) {
57
73
  /* derived from `values`/`bins`; direct writes are ignored */
@@ -61,7 +77,7 @@ Object.defineProperty(LyraHistogram.prototype, 'datasets', {
61
77
  configurable: true,
62
78
  enumerable: true,
63
79
  get() {
64
- return [{ label: this.label, data: binValues(this.values, this.bins).map((b) => b.count) }];
80
+ return [{ label: this.label, data: binnedBuckets(this).map((b) => b.count) }];
65
81
  },
66
82
  set(_v) {
67
83
  /* derived from `values`/`bins`; direct writes are ignored */
@@ -3,3 +3,20 @@ declare const meta: Meta;
3
3
  export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Default: Story;
6
+ /**
7
+ * `area` fills every series lacking its own per-series `fill` (unlike
8
+ * `Default`'s "Sessions" series, which opts into a fill via its own
9
+ * `fill: true` instead of the chart-wide default).
10
+ */
11
+ export declare const AreaFill: Story;
12
+ /** `zoom` enables wheel/drag/pinch zoom; a reset-zoom button appears once zoomed. */
13
+ export declare const WithZoom: Story;
14
+ /** A second series on `axis: "y2"` plots against its own right-side scale, labelled by `y2-label`. */
15
+ export declare const DualAxis: Story;
16
+ /**
17
+ * `beginAtZero` lets the y axis start near the data's own minimum instead of
18
+ * 0 — set via the `.beginAtZero` property, since the `begin-at-zero`
19
+ * *attribute*'s presence (not its string value) is what Lit's boolean
20
+ * converter reads, so `begin-at-zero="false"` would not turn it off.
21
+ */
22
+ export declare const WithoutBeginAtZero: Story;
@@ -22,3 +22,78 @@ export const Default = {
22
22
  `;
23
23
  },
24
24
  };
25
+ /**
26
+ * `area` fills every series lacking its own per-series `fill` (unlike
27
+ * `Default`'s "Sessions" series, which opts into a fill via its own
28
+ * `fill: true` instead of the chart-wide default).
29
+ */
30
+ export const AreaFill = {
31
+ render: () => {
32
+ const series = [{ label: 'Sessions', data: [4, 7, 6, 9, 12] }];
33
+ return html `
34
+ <lyra-line-chart
35
+ area
36
+ height="16rem"
37
+ style="width: 22rem"
38
+ .labels=${['Jan', 'Feb', 'Mar', 'Apr', 'May']}
39
+ .datasets=${series}
40
+ ></lyra-line-chart>
41
+ `;
42
+ },
43
+ };
44
+ /** `zoom` enables wheel/drag/pinch zoom; a reset-zoom button appears once zoomed. */
45
+ export const WithZoom = {
46
+ render: () => {
47
+ const series = [{ label: 'Sessions', data: [4, 7, 6, 9, 12, 8, 15, 10, 6, 9] }];
48
+ return html `
49
+ <lyra-line-chart
50
+ zoom
51
+ height="16rem"
52
+ style="width: 22rem"
53
+ .labels=${['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O']}
54
+ .datasets=${series}
55
+ @lyra-zoom=${(e) => console.log('lyra-zoom', e.detail)}
56
+ ></lyra-line-chart>
57
+ `;
58
+ },
59
+ };
60
+ /** A second series on `axis: "y2"` plots against its own right-side scale, labelled by `y2-label`. */
61
+ export const DualAxis = {
62
+ render: () => {
63
+ const series = [
64
+ { label: 'Sessions', data: [40, 70, 60, 90, 120] },
65
+ { label: 'Conversion rate', data: [0.02, 0.03, 0.025, 0.04, 0.05], axis: 'y2' },
66
+ ];
67
+ return html `
68
+ <lyra-line-chart
69
+ legend
70
+ height="16rem"
71
+ style="width: 22rem"
72
+ y-label="Sessions"
73
+ y2-label="Conversion rate"
74
+ .labels=${['Jan', 'Feb', 'Mar', 'Apr', 'May']}
75
+ .datasets=${series}
76
+ ></lyra-line-chart>
77
+ `;
78
+ },
79
+ };
80
+ /**
81
+ * `beginAtZero` lets the y axis start near the data's own minimum instead of
82
+ * 0 — set via the `.beginAtZero` property, since the `begin-at-zero`
83
+ * *attribute*'s presence (not its string value) is what Lit's boolean
84
+ * converter reads, so `begin-at-zero="false"` would not turn it off.
85
+ */
86
+ export const WithoutBeginAtZero = {
87
+ render: () => {
88
+ const series = [{ label: 'Temperature (°C)', data: [18, 19, 21, 20, 22] }];
89
+ return html `
90
+ <lyra-line-chart
91
+ height="16rem"
92
+ style="width: 22rem"
93
+ .beginAtZero=${false}
94
+ .labels=${['Jan', 'Feb', 'Mar', 'Apr', 'May']}
95
+ .datasets=${series}
96
+ ></lyra-line-chart>
97
+ `;
98
+ },
99
+ };
@@ -0,0 +1,83 @@
1
+ import { type TemplateResult, type PropertyValues } from 'lit';
2
+ import { LyraElement } from '../../internal/lyra-element.js';
3
+ export interface LiteSeries {
4
+ label: string;
5
+ data: (number | null)[];
6
+ /** Defaults to a color from the built-in categorical palette, keyed by dataset index. */
7
+ color?: string;
8
+ }
9
+ export type LyraLiteChartType = 'bar' | 'line';
10
+ /**
11
+ * `<lyra-lite-chart>` — a dependency-free bar/line chart, plain SVG/DOM
12
+ * rendering with zero peer dependencies (unlike `lyra-chart`, which wraps
13
+ * `chart.js`). For a project whose architecture forbids a charting
14
+ * dependency outright, this covers the common bar/line case: grouped or
15
+ * stacked bars, multi-series lines, per-point click, and hover tooltips
16
+ * (native SVG `<title>`, no positioning JS needed) — not a full `lyra-chart`
17
+ * replacement (no zoom/pan, no pie/doughnut/radar/scatter/bubble types, no
18
+ * horizontal/dual-y-axis, no raw-config passthrough).
19
+ *
20
+ * Because this renders real DOM (not canvas), it reuses `lyra-chart`'s
21
+ * `--lyra-chart-*` theme tokens directly via CSS `var()` — no
22
+ * `getComputedStyle()`-based re-theming step is needed the way `chart.ts`
23
+ * needs one for its canvas.
24
+ *
25
+ * @customElement lyra-lite-chart
26
+ * @event lyra-point-click - Fired when a bar/point is activated (click, or
27
+ * Enter/Space while focused). `detail: { datasetIndex: number, index:
28
+ * number, label: string | undefined, value: number | null }` — same shape
29
+ * as `lyra-chart`'s `lyra-point-click`.
30
+ * @csspart base - The host's flex layout wrapper.
31
+ * @csspart grid-line - Each horizontal gridline.
32
+ * @csspart axis-label - Each axis tick label.
33
+ * @csspart axis-title - The x/y axis title text, when set.
34
+ * @csspart bar - Each bar rect (type="bar").
35
+ * @csspart line - Each series' stroked line path (type="line").
36
+ * @csspart point - Each series' per-point hit target (type="line").
37
+ * @csspart legend - The legend row, when `legend` is set.
38
+ * @csspart legend-item - Each legend entry.
39
+ * @csspart legend-swatch - Each legend entry's color swatch.
40
+ */
41
+ export declare class LyraLiteChart extends LyraElement {
42
+ static styles: import("lit").CSSResultGroup[];
43
+ type: LyraLiteChartType;
44
+ labels: string[];
45
+ datasets: LiteSeries[];
46
+ legend: boolean;
47
+ height: string;
48
+ xLabel: string;
49
+ yLabel: string;
50
+ beginAtZero: boolean;
51
+ /** Stacks each category's bars into one segmented bar. Ignored for `type="line"`. */
52
+ stacked: boolean;
53
+ /** Formats a y-axis tick value for display (e.g. `(v) => \`$${v.toFixed(2)}\``). Falls back to the
54
+ * built-in nice-number formatter when unset. */
55
+ tickFormat?: (value: number) => string;
56
+ private plotWidth;
57
+ private plotHeight;
58
+ private visible;
59
+ private svgEl?;
60
+ private resizeObserver?;
61
+ private intersectionObserver?;
62
+ private lastSignature;
63
+ private lastResult?;
64
+ connectedCallback(): void;
65
+ disconnectedCallback(): void;
66
+ protected firstUpdated(): void;
67
+ protected updated(changed: PropertyValues): void;
68
+ private colorFor;
69
+ private domain;
70
+ private computeSignature;
71
+ private emitPoint;
72
+ private onPointKeyDown;
73
+ private renderGrid;
74
+ private renderBars;
75
+ private renderLines;
76
+ render(): TemplateResult;
77
+ private renderChart;
78
+ }
79
+ declare global {
80
+ interface HTMLElementTagNameMap {
81
+ 'lyra-lite-chart': LyraLiteChart;
82
+ }
83
+ }
@@ -0,0 +1,424 @@
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, nothing } from 'lit';
8
+ import { property, state, query } from 'lit/decorators.js';
9
+ import { LyraElement } from '../../internal/lyra-element.js';
10
+ import { defineElement } from '../../internal/prefix.js';
11
+ import { styles } from './lite-chart.styles.js';
12
+ // Colorblind-considered categorical palette, used round-robin when a Series
13
+ // doesn't set its own `color` — same role as Chart.js's default dataset
14
+ // colors for lyra-chart, just a fixed JS array instead of a peer dep's.
15
+ const DEFAULT_PALETTE = [
16
+ '#4c6ef5',
17
+ '#f76707',
18
+ '#2f9e44',
19
+ '#e64980',
20
+ '#ae3ec9',
21
+ '#1098ad',
22
+ '#f59f00',
23
+ '#495057',
24
+ ];
25
+ const PAD_LEFT = 36;
26
+ const PAD_RIGHT = 8;
27
+ const PAD_TOP = 8;
28
+ const PAD_BOTTOM = 20;
29
+ const AXIS_TITLE_SPACE = 14;
30
+ const TICK_COUNT = 4;
31
+ const BAR_GROUP_GAP = 0.2; // fraction of a category slot left as gap between categories
32
+ const BAR_GAP = 0.08; // fraction of a category slot left as gap between grouped bars
33
+ /**
34
+ * Picks a "nice" (1/2/5 × 10^n) step size for an axis spanning `span` over
35
+ * roughly `count` ticks — the standard Heckbert nice-numbers approach, so
36
+ * axis labels read as 0/25/50/75/100 rather than 0/23.4/46.8/70.2/93.6.
37
+ */
38
+ function niceStep(span, count) {
39
+ if (span <= 0)
40
+ return 1;
41
+ const rough = span / Math.max(1, count);
42
+ const magnitude = 10 ** Math.floor(Math.log10(rough));
43
+ const residual = rough / magnitude;
44
+ const niceResidual = residual < 1.5 ? 1 : residual < 3 ? 2 : residual < 7 ? 5 : 10;
45
+ return niceResidual * magnitude;
46
+ }
47
+ /** Nice-rounded [lo, hi, ticks[]] for an axis covering `dataLo..dataHi`. */
48
+ function niceDomain(dataLo, dataHi, beginAtZero, count) {
49
+ let lo = beginAtZero ? Math.min(0, dataLo) : dataLo;
50
+ let hi = beginAtZero ? Math.max(0, dataHi) : dataHi;
51
+ if (lo === hi) {
52
+ lo -= 1;
53
+ hi += 1;
54
+ }
55
+ const step = niceStep(hi - lo, count);
56
+ lo = Math.floor(lo / step) * step;
57
+ hi = Math.ceil(hi / step) * step;
58
+ const ticks = [];
59
+ for (let v = lo; v <= hi + step / 2; v += step)
60
+ ticks.push(Math.round(v / step) * step);
61
+ return { lo, hi, ticks };
62
+ }
63
+ /**
64
+ * `<lyra-lite-chart>` — a dependency-free bar/line chart, plain SVG/DOM
65
+ * rendering with zero peer dependencies (unlike `lyra-chart`, which wraps
66
+ * `chart.js`). For a project whose architecture forbids a charting
67
+ * dependency outright, this covers the common bar/line case: grouped or
68
+ * stacked bars, multi-series lines, per-point click, and hover tooltips
69
+ * (native SVG `<title>`, no positioning JS needed) — not a full `lyra-chart`
70
+ * replacement (no zoom/pan, no pie/doughnut/radar/scatter/bubble types, no
71
+ * horizontal/dual-y-axis, no raw-config passthrough).
72
+ *
73
+ * Because this renders real DOM (not canvas), it reuses `lyra-chart`'s
74
+ * `--lyra-chart-*` theme tokens directly via CSS `var()` — no
75
+ * `getComputedStyle()`-based re-theming step is needed the way `chart.ts`
76
+ * needs one for its canvas.
77
+ *
78
+ * @customElement lyra-lite-chart
79
+ * @event lyra-point-click - Fired when a bar/point is activated (click, or
80
+ * Enter/Space while focused). `detail: { datasetIndex: number, index:
81
+ * number, label: string | undefined, value: number | null }` — same shape
82
+ * as `lyra-chart`'s `lyra-point-click`.
83
+ * @csspart base - The host's flex layout wrapper.
84
+ * @csspart grid-line - Each horizontal gridline.
85
+ * @csspart axis-label - Each axis tick label.
86
+ * @csspart axis-title - The x/y axis title text, when set.
87
+ * @csspart bar - Each bar rect (type="bar").
88
+ * @csspart line - Each series' stroked line path (type="line").
89
+ * @csspart point - Each series' per-point hit target (type="line").
90
+ * @csspart legend - The legend row, when `legend` is set.
91
+ * @csspart legend-item - Each legend entry.
92
+ * @csspart legend-swatch - Each legend entry's color swatch.
93
+ */
94
+ export class LyraLiteChart extends LyraElement {
95
+ constructor() {
96
+ super(...arguments);
97
+ this.type = 'bar';
98
+ this.labels = [];
99
+ this.datasets = [];
100
+ this.legend = false;
101
+ this.height = '280px';
102
+ this.xLabel = '';
103
+ this.yLabel = '';
104
+ this.beginAtZero = true;
105
+ /** Stacks each category's bars into one segmented bar. Ignored for `type="line"`. */
106
+ this.stacked = false;
107
+ this.plotWidth = 0;
108
+ this.plotHeight = 0;
109
+ this.visible = true;
110
+ this.lastSignature = '';
111
+ }
112
+ static { this.styles = [LyraElement.styles, styles]; }
113
+ connectedCallback() {
114
+ super.connectedCallback();
115
+ this.resizeObserver = new ResizeObserver((entries) => {
116
+ const box = entries[0]?.contentBoxSize?.[0];
117
+ if (box) {
118
+ this.plotWidth = box.inlineSize;
119
+ this.plotHeight = box.blockSize;
120
+ }
121
+ else {
122
+ const rect = this.svgEl?.getBoundingClientRect();
123
+ if (rect) {
124
+ this.plotWidth = rect.width;
125
+ this.plotHeight = rect.height;
126
+ }
127
+ }
128
+ });
129
+ if (typeof IntersectionObserver !== 'undefined') {
130
+ this.intersectionObserver = new IntersectionObserver((entries) => {
131
+ this.visible = entries[0]?.isIntersecting ?? true;
132
+ });
133
+ this.intersectionObserver.observe(this);
134
+ }
135
+ }
136
+ disconnectedCallback() {
137
+ super.disconnectedCallback();
138
+ this.resizeObserver?.disconnect();
139
+ this.intersectionObserver?.disconnect();
140
+ }
141
+ firstUpdated() {
142
+ if (this.svgEl)
143
+ this.resizeObserver?.observe(this.svgEl);
144
+ }
145
+ updated(changed) {
146
+ if (changed.has('height')) {
147
+ this.style.setProperty('--lyra-chart-height', this.height);
148
+ }
149
+ }
150
+ colorFor(index, series) {
151
+ return series.color ?? DEFAULT_PALETTE[index % DEFAULT_PALETTE.length];
152
+ }
153
+ domain() {
154
+ let lo = Infinity;
155
+ let hi = -Infinity;
156
+ if (this.type === 'bar' && this.stacked) {
157
+ // Stacked bars: each category's extent is the sum of its (signed)
158
+ // positive/negative segments, not the max single value.
159
+ for (let i = 0; i < this.labels.length; i++) {
160
+ let pos = 0;
161
+ let neg = 0;
162
+ for (const s of this.datasets) {
163
+ const v = s.data[i];
164
+ if (v == null)
165
+ continue;
166
+ if (v >= 0)
167
+ pos += v;
168
+ else
169
+ neg += v;
170
+ }
171
+ lo = Math.min(lo, neg);
172
+ hi = Math.max(hi, pos);
173
+ }
174
+ }
175
+ else {
176
+ for (const s of this.datasets) {
177
+ for (const v of s.data) {
178
+ if (v == null)
179
+ continue;
180
+ lo = Math.min(lo, v);
181
+ hi = Math.max(hi, v);
182
+ }
183
+ }
184
+ }
185
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
186
+ lo = 0;
187
+ hi = 1;
188
+ }
189
+ return niceDomain(lo, hi, this.beginAtZero, TICK_COUNT);
190
+ }
191
+ computeSignature() {
192
+ return JSON.stringify([
193
+ this.type,
194
+ this.labels,
195
+ this.datasets,
196
+ this.legend,
197
+ this.xLabel,
198
+ this.yLabel,
199
+ this.beginAtZero,
200
+ this.stacked,
201
+ this.plotWidth,
202
+ this.plotHeight,
203
+ ]);
204
+ }
205
+ emitPoint(datasetIndex, index) {
206
+ const label = this.labels[index];
207
+ const value = this.datasets[datasetIndex]?.data[index] ?? null;
208
+ this.emit('lyra-point-click', { datasetIndex, index, label, value });
209
+ }
210
+ onPointKeyDown(e, datasetIndex, index) {
211
+ if (e.key !== 'Enter' && e.key !== ' ')
212
+ return;
213
+ e.preventDefault();
214
+ this.emitPoint(datasetIndex, index);
215
+ }
216
+ renderGrid(plotX, plotY, plotW, plotH, ticks, lo, hi) {
217
+ const span = hi - lo || 1;
218
+ return ticks.map((t) => {
219
+ const y = plotY + plotH - ((t - lo) / span) * plotH;
220
+ return svg `
221
+ <line part="grid-line" x1=${plotX} y1=${y} x2=${plotX + plotW} y2=${y}></line>
222
+ <text part="axis-label" x=${plotX - 6} y=${y} text-anchor="end" dominant-baseline="middle">${this.tickFormat ? this.tickFormat(t) : formatTick(t)}</text>
223
+ `;
224
+ });
225
+ }
226
+ renderBars(plotX, plotY, plotW, plotH, lo, hi) {
227
+ const span = hi - lo || 1;
228
+ const n = this.labels.length;
229
+ const slot = n > 0 ? plotW / n : 0;
230
+ const groupCount = this.stacked ? 1 : Math.max(1, this.datasets.length);
231
+ const groupW = slot * (1 - BAR_GROUP_GAP);
232
+ const barW = (groupW - BAR_GAP * slot * (groupCount - 1)) / groupCount;
233
+ const bars = [];
234
+ for (let i = 0; i < n; i++) {
235
+ const slotStart = plotX + i * slot + (slot - groupW) / 2;
236
+ let stackPos = 0; // running positive-side offset (in value units) for stacked bars
237
+ let stackNeg = 0; // running negative-side offset
238
+ this.datasets.forEach((s, di) => {
239
+ const v = s.data[i];
240
+ if (v == null)
241
+ return;
242
+ const color = this.colorFor(di, s);
243
+ let barX;
244
+ let barValLo;
245
+ let barValHi;
246
+ if (this.stacked) {
247
+ barX = slotStart;
248
+ if (v >= 0) {
249
+ barValLo = stackPos;
250
+ barValHi = stackPos + v;
251
+ stackPos += v;
252
+ }
253
+ else {
254
+ barValLo = stackNeg + v;
255
+ barValHi = stackNeg;
256
+ stackNeg += v;
257
+ }
258
+ }
259
+ else {
260
+ barX = slotStart + di * (barW + BAR_GAP * slot);
261
+ barValLo = Math.min(0, v);
262
+ barValHi = Math.max(0, v);
263
+ }
264
+ const y1 = plotY + plotH - ((barValHi - lo) / span) * plotH;
265
+ const y2 = plotY + plotH - ((barValLo - lo) / span) * plotH;
266
+ const label = this.labels[i];
267
+ bars.push(svg `
268
+ <rect
269
+ part="bar"
270
+ x=${barX}
271
+ y=${y1}
272
+ width=${Math.max(0, barW)}
273
+ height=${Math.max(0, y2 - y1)}
274
+ fill=${color}
275
+ tabindex="0"
276
+ role="button"
277
+ aria-label=${`${s.label}, ${label}: ${v}`}
278
+ @click=${() => this.emitPoint(di, i)}
279
+ @keydown=${(e) => this.onPointKeyDown(e, di, i)}
280
+ ><title>${s.label} — ${label}: ${v}</title></rect>
281
+ `);
282
+ });
283
+ }
284
+ return bars;
285
+ }
286
+ renderLines(plotX, plotY, plotW, plotH, lo, hi) {
287
+ const span = hi - lo || 1;
288
+ const n = this.labels.length;
289
+ const xFor = (i) => plotX + (n > 1 ? (i / (n - 1)) * plotW : plotW / 2);
290
+ const yFor = (v) => plotY + plotH - ((v - lo) / span) * plotH;
291
+ return this.datasets.map((s, di) => {
292
+ const color = this.colorFor(di, s);
293
+ const pts = s.data
294
+ .map((v, i) => (v == null ? null : [xFor(i), yFor(v)]))
295
+ .filter((p) => p !== null);
296
+ const d = pts.map(([x, y], i) => `${i ? 'L' : 'M'}${x},${y}`).join(' ');
297
+ const dots = s.data.map((v, i) => {
298
+ if (v == null)
299
+ return nothing;
300
+ const label = this.labels[i];
301
+ return svg `
302
+ <circle
303
+ part="point"
304
+ cx=${xFor(i)}
305
+ cy=${yFor(v)}
306
+ r="4"
307
+ fill=${color}
308
+ tabindex="0"
309
+ role="button"
310
+ aria-label=${`${s.label}, ${label}: ${v}`}
311
+ @click=${() => this.emitPoint(di, i)}
312
+ @keydown=${(e) => this.onPointKeyDown(e, di, i)}
313
+ ><title>${s.label} — ${label}: ${v}</title></circle>
314
+ `;
315
+ });
316
+ return svg `<path part="line" d=${d} stroke=${color}></path>${dots}`;
317
+ });
318
+ }
319
+ render() {
320
+ if (!this.visible && this.lastResult)
321
+ return this.lastResult;
322
+ const signature = this.computeSignature();
323
+ if (signature === this.lastSignature && this.lastResult)
324
+ return this.lastResult;
325
+ this.lastSignature = signature;
326
+ const result = this.renderChart();
327
+ this.lastResult = result;
328
+ return result;
329
+ }
330
+ renderChart() {
331
+ const w = this.plotWidth || 400;
332
+ const h = this.plotHeight || 200;
333
+ const padLeft = PAD_LEFT + (this.yLabel ? AXIS_TITLE_SPACE : 0);
334
+ const padBottom = PAD_BOTTOM + (this.xLabel ? AXIS_TITLE_SPACE : 0);
335
+ const plotX = padLeft;
336
+ const plotY = PAD_TOP;
337
+ const plotW = Math.max(0, w - padLeft - PAD_RIGHT);
338
+ const plotH = Math.max(0, h - plotY - padBottom);
339
+ const { lo, hi, ticks } = this.domain();
340
+ const grid = this.renderGrid(plotX, plotY, plotW, plotH, ticks, lo, hi);
341
+ const marks = this.type === 'bar'
342
+ ? this.renderBars(plotX, plotY, plotW, plotH, lo, hi)
343
+ : this.renderLines(plotX, plotY, plotW, plotH, lo, hi);
344
+ const categoryLabels = this.labels.map((label, i) => {
345
+ const n = this.labels.length;
346
+ const x = plotX + (n > 1 ? (i / (n - 1)) * plotW : plotW / 2);
347
+ return svg `<text part="axis-label" x=${x} y=${plotY + plotH + 14} text-anchor="middle">${label}</text>`;
348
+ });
349
+ const chartLabel = this.datasets.map((d) => d.label).join(', ') || 'Chart';
350
+ return html `
351
+ <div part="base">
352
+ <svg viewBox="0 0 ${w} ${h}" role="group" aria-label=${chartLabel}>
353
+ ${grid}
354
+ ${categoryLabels}
355
+ ${marks}
356
+ ${this.yLabel
357
+ ? svg `<text part="axis-title" x=${12} y=${plotY + plotH / 2} text-anchor="middle" transform="rotate(-90, 12, ${plotY + plotH / 2})">${this.yLabel}</text>`
358
+ : nothing}
359
+ ${this.xLabel
360
+ ? svg `<text part="axis-title" x=${plotX + plotW / 2} y=${plotY + plotH + padBottom - 2} text-anchor="middle">${this.xLabel}</text>`
361
+ : nothing}
362
+ </svg>
363
+ ${this.legend
364
+ ? html `<div part="legend">
365
+ ${this.datasets.map((s, i) => html `
366
+ <span part="legend-item">
367
+ <span part="legend-swatch" style="background:${this.colorFor(i, s)}"></span>
368
+ ${s.label}
369
+ </span>
370
+ `)}
371
+ </div>`
372
+ : nothing}
373
+ </div>
374
+ `;
375
+ }
376
+ }
377
+ __decorate([
378
+ property()
379
+ ], LyraLiteChart.prototype, "type", void 0);
380
+ __decorate([
381
+ property({ attribute: false })
382
+ ], LyraLiteChart.prototype, "labels", void 0);
383
+ __decorate([
384
+ property({ attribute: false })
385
+ ], LyraLiteChart.prototype, "datasets", void 0);
386
+ __decorate([
387
+ property({ type: Boolean })
388
+ ], LyraLiteChart.prototype, "legend", void 0);
389
+ __decorate([
390
+ property()
391
+ ], LyraLiteChart.prototype, "height", void 0);
392
+ __decorate([
393
+ property({ attribute: 'x-label' })
394
+ ], LyraLiteChart.prototype, "xLabel", void 0);
395
+ __decorate([
396
+ property({ attribute: 'y-label' })
397
+ ], LyraLiteChart.prototype, "yLabel", void 0);
398
+ __decorate([
399
+ property({ type: Boolean, attribute: 'begin-at-zero' })
400
+ ], LyraLiteChart.prototype, "beginAtZero", void 0);
401
+ __decorate([
402
+ property({ type: Boolean })
403
+ ], LyraLiteChart.prototype, "stacked", void 0);
404
+ __decorate([
405
+ property({ attribute: false })
406
+ ], LyraLiteChart.prototype, "tickFormat", void 0);
407
+ __decorate([
408
+ state()
409
+ ], LyraLiteChart.prototype, "plotWidth", void 0);
410
+ __decorate([
411
+ state()
412
+ ], LyraLiteChart.prototype, "plotHeight", void 0);
413
+ __decorate([
414
+ state()
415
+ ], LyraLiteChart.prototype, "visible", void 0);
416
+ __decorate([
417
+ query('svg')
418
+ ], LyraLiteChart.prototype, "svgEl", void 0);
419
+ function formatTick(v) {
420
+ // Avoid float noise (e.g. 0.30000000000000004) from the niceStep() math
421
+ // above without hardcoding a fixed decimal count that'd butcher large ints.
422
+ return Number(v.toFixed(6)).toString();
423
+ }
424
+ defineElement('lite-chart', LyraLiteChart);
@@ -0,0 +1,14 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ /** Zero-dependency alternative to `lyra-chart` — plain SVG/DOM, no `chart.js` peer dep. */
6
+ export declare const Default: Story;
7
+ export declare const GroupedBars: Story;
8
+ /** `stacked` sums each category's bars into one segmented bar instead of grouping them side by side. */
9
+ export declare const StackedBars: Story;
10
+ export declare const Line: Story;
11
+ /** Clicking (or Enter/Space on a focused) bar/point fires `lyra-point-click`, same detail shape as `lyra-chart`'s. */
12
+ export declare const ClickToFilter: Story;
13
+ /** `tickFormat` customizes y-axis tick labels (e.g. currency) instead of the built-in nice-number formatter. */
14
+ export declare const CurrencyTickFormat: Story;