@aceshooting/lyra-ui 1.0.0 → 1.1.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 (148) hide show
  1. package/README.md +8 -9
  2. package/custom-elements.json +10463 -4519
  3. package/dist/components/chart/chart.d.ts +46 -1
  4. package/dist/components/chart/chart.js +148 -10
  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 +74 -0
  13. package/dist/components/chart/lite-chart.js +384 -0
  14. package/dist/components/chart/lite-chart.stories.d.ts +12 -0
  15. package/dist/components/chart/lite-chart.stories.js +98 -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 +75 -12
  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 +48 -3
  50. package/dist/components/flag/flag.js +65 -29
  51. package/dist/components/flag/flag.styles.js +1 -1
  52. package/dist/components/flag/language-map.d.ts +11 -0
  53. package/dist/components/flag/language-map.js +12 -1
  54. package/dist/components/gauge/gauge.d.ts +2 -0
  55. package/dist/components/gauge/gauge.js +19 -5
  56. package/dist/components/gauge/gauge.stories.d.ts +2 -0
  57. package/dist/components/gauge/gauge.stories.js +6 -0
  58. package/dist/components/graph/graph-loader.d.ts +28 -0
  59. package/dist/components/graph/graph-loader.js +44 -0
  60. package/dist/components/graph/graph.d.ts +41 -15
  61. package/dist/components/graph/graph.js +146 -71
  62. package/dist/components/graph/graph.stories.d.ts +3 -0
  63. package/dist/components/graph/graph.stories.js +38 -0
  64. package/dist/components/graph/graph.styles.js +6 -1
  65. package/dist/components/heatmap/calendar-grid.d.ts +7 -0
  66. package/dist/components/heatmap/calendar-grid.js +27 -10
  67. package/dist/components/heatmap/heatmap.d.ts +137 -12
  68. package/dist/components/heatmap/heatmap.js +427 -27
  69. package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
  70. package/dist/components/heatmap/heatmap.stories.js +130 -0
  71. package/dist/components/heatmap/heatmap.styles.js +60 -2
  72. package/dist/components/map/map.d.ts +17 -1
  73. package/dist/components/map/map.js +68 -21
  74. package/dist/components/map/map.stories.d.ts +10 -0
  75. package/dist/components/map/map.stories.js +107 -12
  76. package/dist/components/map/map.styles.js +2 -2
  77. package/dist/components/playback/playback.d.ts +6 -2
  78. package/dist/components/playback/playback.js +36 -12
  79. package/dist/components/playback/playback.stories.d.ts +2 -0
  80. package/dist/components/playback/playback.stories.js +6 -0
  81. package/dist/components/playback/playback.styles.js +10 -2
  82. package/dist/components/select/select.d.ts +122 -0
  83. package/dist/components/select/select.js +516 -0
  84. package/dist/components/select/select.stories.d.ts +19 -0
  85. package/dist/components/select/select.stories.js +75 -0
  86. package/dist/components/select/select.styles.d.ts +1 -0
  87. package/dist/components/select/select.styles.js +188 -0
  88. package/dist/components/skeleton/skeleton.js +4 -7
  89. package/dist/components/sparkline/sparkline.d.ts +1 -1
  90. package/dist/components/sparkline/sparkline.js +40 -13
  91. package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
  92. package/dist/components/sparkline/sparkline.stories.js +22 -0
  93. package/dist/components/split/split.d.ts +24 -3
  94. package/dist/components/split/split.js +155 -31
  95. package/dist/components/split/split.stories.d.ts +2 -0
  96. package/dist/components/split/split.stories.js +20 -0
  97. package/dist/components/stat/stat.d.ts +27 -1
  98. package/dist/components/stat/stat.js +56 -3
  99. package/dist/components/stat/stat.stories.d.ts +3 -0
  100. package/dist/components/stat/stat.stories.js +49 -1
  101. package/dist/components/stat/stat.styles.js +44 -0
  102. package/dist/components/table/table.d.ts +64 -2
  103. package/dist/components/table/table.js +213 -17
  104. package/dist/components/table/table.stories.d.ts +6 -0
  105. package/dist/components/table/table.stories.js +45 -0
  106. package/dist/components/table/table.styles.js +51 -4
  107. package/dist/components/time-range/time-range.d.ts +34 -5
  108. package/dist/components/time-range/time-range.js +217 -36
  109. package/dist/components/time-range/time-range.stories.d.ts +2 -0
  110. package/dist/components/time-range/time-range.stories.js +13 -0
  111. package/dist/components/time-range/time-range.styles.js +44 -2
  112. package/dist/components/toast/toast-item.d.ts +14 -2
  113. package/dist/components/toast/toast-item.js +134 -17
  114. package/dist/components/toast/toast-item.styles.js +28 -2
  115. package/dist/components/toast/toast.stories.d.ts +3 -0
  116. package/dist/components/toast/toast.stories.js +41 -1
  117. package/dist/components/toast/toast.styles.js +11 -11
  118. package/dist/components/toast/toaster.d.ts +1 -1
  119. package/dist/components/toast/toaster.js +13 -6
  120. package/dist/components/tree/tree-node.d.ts +13 -7
  121. package/dist/components/tree/tree-node.js +24 -10
  122. package/dist/components/tree/tree.d.ts +64 -15
  123. package/dist/components/tree/tree.js +113 -22
  124. package/dist/components/tree/tree.stories.d.ts +4 -0
  125. package/dist/components/tree/tree.stories.js +31 -1
  126. package/dist/components/tree/update-cascade.d.ts +13 -0
  127. package/dist/components/tree/update-cascade.js +13 -0
  128. package/dist/components/widget/widget.d.ts +15 -1
  129. package/dist/components/widget/widget.js +140 -5
  130. package/dist/components/widget/widget.stories.d.ts +1 -0
  131. package/dist/components/widget/widget.stories.js +10 -0
  132. package/dist/components/widget/widget.styles.js +11 -1
  133. package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
  134. package/dist/components/word-cloud/word-cloud-layout.js +145 -0
  135. package/dist/components/word-cloud/word-cloud.d.ts +65 -0
  136. package/dist/components/word-cloud/word-cloud.js +255 -0
  137. package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
  138. package/dist/components/word-cloud/word-cloud.stories.js +55 -0
  139. package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
  140. package/dist/components/word-cloud/word-cloud.styles.js +54 -0
  141. package/dist/internal/a11y.js +2 -1
  142. package/dist/internal/rtl.d.ts +7 -0
  143. package/dist/internal/rtl.js +9 -0
  144. package/dist/lyra.d.ts +9 -1
  145. package/dist/lyra.js +6 -0
  146. package/llms-full.txt +892 -276
  147. package/llms.txt +12 -9
  148. package/package.json +6 -4
@@ -42,6 +42,10 @@ export type LyraChartType = 'line' | 'bar' | 'scatter' | 'pie' | 'doughnut' | 'r
42
42
  *
43
43
  * @customElement lyra-chart
44
44
  * @event lyra-zoom - `detail: { zoomed }`.
45
+ * @event lyra-point-click - Fired when a click lands on (or nearest,
46
+ * intersect-only — see `handlePointClick()`) a data point/segment.
47
+ * `detail: { datasetIndex: number, index: number, label: string |
48
+ * undefined, value: unknown }`.
45
49
  * @csspart base, canvas, reset-zoom-button
46
50
  */
47
51
  export declare class LyraChart extends LyraElement {
@@ -57,6 +61,10 @@ export declare class LyraChart extends LyraElement {
57
61
  yLabel: string;
58
62
  y2Label: string;
59
63
  beginAtZero: boolean;
64
+ /** Sets `options.indexAxis = 'y'`, Chart.js's own mechanism for horizontal bars (also applies to line/area types). */
65
+ horizontal: boolean;
66
+ /** Stacks the `x`/`y`(/`y2`) scale entries `buildScales()` returns; only meaningful for `bar` and `line` types. */
67
+ stacked: boolean;
60
68
  /**
61
69
  * Raw Chart.js configuration passthrough — mirrors `wa-chart`'s `config`
62
70
  * property. Recursively deep-merged over the `Series`-derived config in
@@ -73,21 +81,58 @@ export declare class LyraChart extends LyraElement {
73
81
  private chart?;
74
82
  private chartJsModule?;
75
83
  private builtType?;
84
+ private suppressZoomComplete;
76
85
  connectedCallback(): void;
77
86
  disconnectedCallback(): void;
78
87
  protected updated(changed: PropertyValues): void;
79
88
  private seriesToDataset;
89
+ /**
90
+ * Resolves the `--lyra-chart-*` theme tokens (declared in
91
+ * `chart.styles.ts`, each layered over an existing semantic token) via
92
+ * `getComputedStyle`. Chart.js renders to canvas, not the DOM, so it can't
93
+ * consume CSS `var()` directly — same constraint documented on
94
+ * `heatmap.ts`'s `labelColor()`/`noDataFill()`/`scaleEndpoints()` — so this
95
+ * is called fresh from `buildConfig()` on every draw rather than cached.
96
+ */
97
+ private themeColors;
80
98
  /**
81
99
  * Builds `options.scales` for the effective chart type: no scale at all for
82
100
  * pie/doughnut (a proportional-area chart has no axis), the single radial
83
101
  * `r` scale Chart.js v4 uses for radar/polarArea, and the cartesian
84
- * `x`/`y`(/`y2`) block for every other (line/bar/scatter/bubble) type.
102
+ * `x`/`y`(/`y2`) block for every other (line/bar/scatter/bubble) type — with
103
+ * `x` itself further split within that block: scatter and bubble datasets
104
+ * carry raw numeric `{x, y(, r)}` points (via `Series.points`) and need a
105
+ * linear `x` scale, while line/bar plot against `labels` and need the
106
+ * default categorical one. `theme` (from `themeColors()`) drives every
107
+ * scale's `ticks.color`/`grid.color`/axis `title.color` so grid lines and
108
+ * labels retheme instead of sitting at Chart.js's own hardcoded defaults.
85
109
  */
86
110
  private buildScales;
111
+ /**
112
+ * `options.onClick` handler wired in `buildConfig()`. The chart-wide
113
+ * `interaction` mode above (`'index'`/`'nearest'`, `intersect: false`) is
114
+ * tuned for hover tooltips — resolving which single point/segment was
115
+ * actually clicked needs its own `getElementsAtEventForMode('nearest', {
116
+ * intersect: true }, true)` lookup instead, so a click landing off any
117
+ * point/segment reports nothing (`elements` empty) rather than firing for
118
+ * whatever's nearest. Covers the per-bar/per-segment click ask for any
119
+ * chart type (bar/line/pie/doughnut/etc.), not just bars.
120
+ */
121
+ private handlePointClick;
87
122
  private buildConfig;
88
123
  private draw;
89
124
  /** Reset any active zoom/pan back to the original view. */
90
125
  resetZoom(): void;
126
+ /**
127
+ * Forces a redraw so `themeColors()` re-reads the `--lyra-chart-*` custom
128
+ * properties from the current computed style. No global theme-broadcast
129
+ * event exists anywhere in lyra-ui (nothing to subscribe to here) — this
130
+ * is the escape hatch for a consumer's own theme-toggle handler to call
131
+ * directly when it flips e.g. a `data-theme` attribute upstream that
132
+ * doesn't otherwise change any `lyra-chart` property, so Lit's reactive
133
+ * update loop has nothing of its own to trigger `draw()` on.
134
+ */
135
+ refreshTheme(): void;
91
136
  render(): TemplateResult;
92
137
  }
93
138
  declare global {
@@ -27,6 +27,18 @@ function isPlainObject(value) {
27
27
  // response) reach up through the merge and mutate `Object.prototype` —
28
28
  // skipped unconditionally regardless of `base`'s own shape.
29
29
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
30
+ function prefersReducedMotion() {
31
+ return typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
32
+ }
33
+ // Defensive JS-side fallbacks for themeColors() below, mirroring the
34
+ // light-mode default of each `--lyra-chart-*` token's own fallback chain
35
+ // (see chart.styles.ts) — only reached if getComputedStyle somehow can't
36
+ // resolve the custom property at all (e.g. host detached from the document).
37
+ const FALLBACK_GRID_COLOR = '#8a8a90';
38
+ const FALLBACK_TICK_COLOR = '#6b7280';
39
+ const FALLBACK_LEGEND_COLOR = '#1a1a1a';
40
+ const FALLBACK_TOOLTIP_BG = '#fff';
41
+ const FALLBACK_TOOLTIP_TEXT = '#1a1a1a';
30
42
  function deepMerge(base, override) {
31
43
  if (!isPlainObject(base) || !isPlainObject(override)) {
32
44
  return (override === undefined ? base : override);
@@ -60,6 +72,10 @@ function deepMerge(base, override) {
60
72
  *
61
73
  * @customElement lyra-chart
62
74
  * @event lyra-zoom - `detail: { zoomed }`.
75
+ * @event lyra-point-click - Fired when a click lands on (or nearest,
76
+ * intersect-only — see `handlePointClick()`) a data point/segment.
77
+ * `detail: { datasetIndex: number, index: number, label: string |
78
+ * undefined, value: unknown }`.
63
79
  * @csspart base, canvas, reset-zoom-button
64
80
  */
65
81
  export class LyraChart extends LyraElement {
@@ -76,9 +92,19 @@ export class LyraChart extends LyraElement {
76
92
  this.yLabel = '';
77
93
  this.y2Label = '';
78
94
  this.beginAtZero = true;
95
+ /** Sets `options.indexAxis = 'y'`, Chart.js's own mechanism for horizontal bars (also applies to line/area types). */
96
+ this.horizontal = false;
97
+ /** Stacks the `x`/`y`(/`y2`) scale entries `buildScales()` returns; only meaningful for `bar` and `line` types. */
98
+ this.stacked = false;
79
99
  /** True until the lazy-loaded `chart.js` peer dependency has settled (success or failure). */
80
100
  this.loading = true;
81
101
  this.zoomed = false;
102
+ // `chartjs-plugin-zoom`'s own `resetZoom()` synchronously re-invokes the
103
+ // `onZoomComplete` callback below as part of its reset, which would emit a
104
+ // stale `{zoomed: true}` right before `resetZoom()` emits the real
105
+ // `{zoomed: false}`. Set while this component's own `resetZoom()` is
106
+ // driving the plugin so that re-entrant callback is ignored.
107
+ this.suppressZoomComplete = false;
82
108
  }
83
109
  static { this.styles = [LyraElement.styles, styles]; }
84
110
  connectedCallback() {
@@ -130,41 +156,111 @@ export class LyraChart extends LyraElement {
130
156
  tooltip: s.noTooltip ? { enabled: false } : undefined,
131
157
  };
132
158
  }
159
+ /**
160
+ * Resolves the `--lyra-chart-*` theme tokens (declared in
161
+ * `chart.styles.ts`, each layered over an existing semantic token) via
162
+ * `getComputedStyle`. Chart.js renders to canvas, not the DOM, so it can't
163
+ * consume CSS `var()` directly — same constraint documented on
164
+ * `heatmap.ts`'s `labelColor()`/`noDataFill()`/`scaleEndpoints()` — so this
165
+ * is called fresh from `buildConfig()` on every draw rather than cached.
166
+ */
167
+ themeColors() {
168
+ const cs = getComputedStyle(this);
169
+ return {
170
+ grid: cs.getPropertyValue('--lyra-chart-grid-color').trim() || FALLBACK_GRID_COLOR,
171
+ tick: cs.getPropertyValue('--lyra-chart-tick-color').trim() || FALLBACK_TICK_COLOR,
172
+ legend: cs.getPropertyValue('--lyra-chart-legend-color').trim() || FALLBACK_LEGEND_COLOR,
173
+ tooltipBg: cs.getPropertyValue('--lyra-chart-tooltip-bg').trim() || FALLBACK_TOOLTIP_BG,
174
+ tooltipText: cs.getPropertyValue('--lyra-chart-tooltip-text').trim() || FALLBACK_TOOLTIP_TEXT,
175
+ };
176
+ }
133
177
  /**
134
178
  * Builds `options.scales` for the effective chart type: no scale at all for
135
179
  * pie/doughnut (a proportional-area chart has no axis), the single radial
136
180
  * `r` scale Chart.js v4 uses for radar/polarArea, and the cartesian
137
- * `x`/`y`(/`y2`) block for every other (line/bar/scatter/bubble) type.
181
+ * `x`/`y`(/`y2`) block for every other (line/bar/scatter/bubble) type — with
182
+ * `x` itself further split within that block: scatter and bubble datasets
183
+ * carry raw numeric `{x, y(, r)}` points (via `Series.points`) and need a
184
+ * linear `x` scale, while line/bar plot against `labels` and need the
185
+ * default categorical one. `theme` (from `themeColors()`) drives every
186
+ * scale's `ticks.color`/`grid.color`/axis `title.color` so grid lines and
187
+ * labels retheme instead of sitting at Chart.js's own hardcoded defaults.
138
188
  */
139
- buildScales() {
189
+ buildScales(theme) {
140
190
  if (this.type === 'pie' || this.type === 'doughnut')
141
191
  return {};
142
192
  if (this.type === 'radar' || this.type === 'polarArea') {
143
193
  return {
144
194
  r: {
145
195
  beginAtZero: this.beginAtZero,
196
+ ticks: { color: theme.tick },
197
+ grid: { color: theme.grid },
198
+ angleLines: { color: theme.grid },
199
+ pointLabels: { color: theme.tick },
146
200
  },
147
201
  };
148
202
  }
149
203
  const hasY2 = this.datasets.some((s) => s.axis === 'y2');
204
+ // `stacked` only applies to bar/line-family charts sharing a categorical
205
+ // axis, per the design spec — scatter/bubble's linear x scale and the
206
+ // radial r scale above are out of scope.
207
+ const stacked = this.stacked && (this.type === 'bar' || this.type === 'line');
150
208
  return {
151
209
  x: {
152
- type: this.type === 'scatter' ? 'linear' : 'category',
153
- title: { display: !!this.xLabel, text: this.xLabel },
210
+ type: this.type === 'scatter' || this.type === 'bubble' ? 'linear' : 'category',
211
+ title: { display: !!this.xLabel, text: this.xLabel, color: theme.tick },
212
+ ticks: { color: theme.tick },
213
+ grid: { color: theme.grid },
214
+ stacked,
215
+ },
216
+ y: {
217
+ beginAtZero: this.beginAtZero,
218
+ title: { display: !!this.yLabel, text: this.yLabel, color: theme.tick },
219
+ ticks: { color: theme.tick },
220
+ grid: { color: theme.grid },
221
+ stacked,
154
222
  },
155
- y: { beginAtZero: this.beginAtZero, title: { display: !!this.yLabel, text: this.yLabel } },
156
223
  ...(hasY2
157
224
  ? {
158
225
  y2: {
159
226
  position: 'right',
160
- grid: { drawOnChartArea: false },
161
- title: { display: !!this.y2Label, text: this.y2Label },
227
+ grid: { drawOnChartArea: false, color: theme.grid },
228
+ title: { display: !!this.y2Label, text: this.y2Label, color: theme.tick },
229
+ ticks: { color: theme.tick },
230
+ stacked,
162
231
  },
163
232
  }
164
233
  : {}),
165
234
  };
166
235
  }
236
+ /**
237
+ * `options.onClick` handler wired in `buildConfig()`. The chart-wide
238
+ * `interaction` mode above (`'index'`/`'nearest'`, `intersect: false`) is
239
+ * tuned for hover tooltips — resolving which single point/segment was
240
+ * actually clicked needs its own `getElementsAtEventForMode('nearest', {
241
+ * intersect: true }, true)` lookup instead, so a click landing off any
242
+ * point/segment reports nothing (`elements` empty) rather than firing for
243
+ * whatever's nearest. Covers the per-bar/per-segment click ask for any
244
+ * chart type (bar/line/pie/doughnut/etc.), not just bars.
245
+ */
246
+ handlePointClick(event, chart) {
247
+ // Chart.js's own `onClick` handler hands us its `ChartEvent` wrapper, but
248
+ // `getElementsAtEventForMode()`'s .d.ts (inaccurately) types its first
249
+ // param as a DOM `Event` — at runtime Chart.js only reads `.x`/`.y` off
250
+ // whatever is passed (see chart.js/src/helpers/helpers.dom.ts
251
+ // `getRelativePosition()`), which `ChartEvent` already has, so the cast
252
+ // here is a type-only correction, not a behavior change.
253
+ const elements = chart.getElementsAtEventForMode(event, 'nearest', { intersect: true }, true);
254
+ const hit = elements[0];
255
+ if (!hit)
256
+ return;
257
+ const { datasetIndex, index } = hit;
258
+ const label = chart.data.labels?.[index];
259
+ const value = chart.data.datasets[datasetIndex]?.data?.[index] ?? null;
260
+ this.emit('lyra-point-click', { datasetIndex, index, label, value });
261
+ }
167
262
  buildConfig() {
263
+ const theme = this.themeColors();
168
264
  const generated = {
169
265
  type: this.type,
170
266
  data: {
@@ -174,9 +270,25 @@ export class LyraChart extends LyraElement {
174
270
  options: {
175
271
  responsive: true,
176
272
  maintainAspectRatio: false,
273
+ // Chart.js's own mechanism for horizontal bars (also flips line/area
274
+ // types onto a horizontal category axis).
275
+ indexAxis: this.horizontal ? 'y' : undefined,
276
+ // Chart.js's own default ~1000ms draw-in animation only ever fires
277
+ // from `new Chart()` (chart/type-change construction) — every
278
+ // in-place update in `draw()` already passes `'none'` to
279
+ // `Chart#update()`. A CSS media query can't reach that
280
+ // canvas-internal animation loop, so `prefersReducedMotion()` is
281
+ // checked here instead and fed into `options.animation`.
282
+ animation: prefersReducedMotion() ? false : undefined,
177
283
  interaction: { intersect: false, mode: this.type === 'scatter' ? 'nearest' : 'index' },
284
+ onClick: (event, _elements, chart) => this.handlePointClick(event, chart),
178
285
  plugins: {
179
- legend: { display: this.legend },
286
+ legend: { display: this.legend, labels: { color: theme.legend } },
287
+ tooltip: {
288
+ backgroundColor: theme.tooltipBg,
289
+ titleColor: theme.tooltipText,
290
+ bodyColor: theme.tooltipText,
291
+ },
180
292
  zoom: this.zoom
181
293
  ? {
182
294
  pan: { enabled: false },
@@ -186,6 +298,8 @@ export class LyraChart extends LyraElement {
186
298
  pinch: { enabled: true },
187
299
  mode: 'x',
188
300
  onZoomComplete: () => {
301
+ if (this.suppressZoomComplete)
302
+ return;
189
303
  this.zoomed = true;
190
304
  this.emit('lyra-zoom', { zoomed: true });
191
305
  },
@@ -194,7 +308,7 @@ export class LyraChart extends LyraElement {
194
308
  }
195
309
  : undefined,
196
310
  },
197
- scales: this.buildScales(),
311
+ scales: this.buildScales(theme),
198
312
  },
199
313
  };
200
314
  if (!this.config)
@@ -223,10 +337,28 @@ export class LyraChart extends LyraElement {
223
337
  }
224
338
  /** Reset any active zoom/pan back to the original view. */
225
339
  resetZoom() {
226
- this.chart?.resetZoom?.();
340
+ this.suppressZoomComplete = true;
341
+ try {
342
+ this.chart?.resetZoom?.();
343
+ }
344
+ finally {
345
+ this.suppressZoomComplete = false;
346
+ }
227
347
  this.zoomed = false;
228
348
  this.emit('lyra-zoom', { zoomed: false });
229
349
  }
350
+ /**
351
+ * Forces a redraw so `themeColors()` re-reads the `--lyra-chart-*` custom
352
+ * properties from the current computed style. No global theme-broadcast
353
+ * event exists anywhere in lyra-ui (nothing to subscribe to here) — this
354
+ * is the escape hatch for a consumer's own theme-toggle handler to call
355
+ * directly when it flips e.g. a `data-theme` attribute upstream that
356
+ * doesn't otherwise change any `lyra-chart` property, so Lit's reactive
357
+ * update loop has nothing of its own to trigger `draw()` on.
358
+ */
359
+ refreshTheme() {
360
+ this.draw();
361
+ }
230
362
  render() {
231
363
  if (this.loading) {
232
364
  return html `
@@ -281,6 +413,12 @@ __decorate([
281
413
  __decorate([
282
414
  property({ type: Boolean, attribute: 'begin-at-zero' })
283
415
  ], LyraChart.prototype, "beginAtZero", void 0);
416
+ __decorate([
417
+ property({ type: Boolean })
418
+ ], LyraChart.prototype, "horizontal", void 0);
419
+ __decorate([
420
+ property({ type: Boolean })
421
+ ], LyraChart.prototype, "stacked", void 0);
284
422
  __decorate([
285
423
  property({ attribute: false })
286
424
  ], LyraChart.prototype, "config", void 0);
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
6
+ /**
7
+ * The `config` property is the raw Chart.js passthrough escape hatch — it is
8
+ * deep-merged over the `Series`-generated config, so a nested key like
9
+ * `options.plugins.title` can be set without discarding the rest of the
10
+ * generated config the `type`/`labels`/`datasets` attributes produce.
11
+ */
12
+ export declare const ConfigPassthrough: Story;
13
+ /** `horizontal` sets Chart.js's `indexAxis: 'y'`, flipping bars onto a horizontal axis. */
14
+ export declare const Horizontal: Story;
15
+ /** `stacked` stacks every dataset sharing an axis on top of each other instead of side by side. */
16
+ export declare const Stacked: Story;
17
+ /**
18
+ * The `--lyra-chart-grid-color`/`-tick-color`/`-legend-color`/`-tooltip-bg`/
19
+ * `-tooltip-text` custom properties retheme Chart.js's canvas-drawn chrome
20
+ * (grid lines, axis ticks/titles, legend labels, tooltip). Chart.js can't
21
+ * consume `var()` directly, so these are resolved via `getComputedStyle`
22
+ * once per draw — see `themeColors()` in `chart.ts`.
23
+ */
24
+ export declare const ThemedTokens: Story;
25
+ /**
26
+ * `lyra-point-click` fires whenever a click lands on (or nearest,
27
+ * intersect-only) a data point/segment — covers any `type`, not just bars.
28
+ * `refreshTheme()` forces a redraw so an out-of-band theme change (e.g. a
29
+ * host-level dark-mode toggle that doesn't touch any `lyra-chart` property)
30
+ * is picked up immediately, rather than waiting for the next reactive update.
31
+ */
32
+ export declare const PointClickAndRefreshTheme: Story;
@@ -0,0 +1,141 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'Charts/Chart',
4
+ component: 'lyra-chart',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Default = {
9
+ render: () => {
10
+ const series = [{ label: 'Revenue', data: [12, 19, 14, 22] }];
11
+ return html `
12
+ <lyra-chart
13
+ type="bar"
14
+ height="16rem"
15
+ style="width: 22rem"
16
+ legend
17
+ .labels=${['Q1', 'Q2', 'Q3', 'Q4']}
18
+ .datasets=${series}
19
+ ></lyra-chart>
20
+ `;
21
+ },
22
+ };
23
+ /**
24
+ * The `config` property is the raw Chart.js passthrough escape hatch — it is
25
+ * deep-merged over the `Series`-generated config, so a nested key like
26
+ * `options.plugins.title` can be set without discarding the rest of the
27
+ * generated config the `type`/`labels`/`datasets` attributes produce.
28
+ */
29
+ export const ConfigPassthrough = {
30
+ render: () => {
31
+ const series = [{ label: 'Revenue', data: [12, 19, 14, 22] }];
32
+ return html `
33
+ <lyra-chart
34
+ type="bar"
35
+ height="16rem"
36
+ style="width: 22rem"
37
+ .labels=${['Q1', 'Q2', 'Q3', 'Q4']}
38
+ .datasets=${series}
39
+ .config=${{
40
+ options: { plugins: { title: { display: true, text: 'Quarterly revenue' } } },
41
+ }}
42
+ ></lyra-chart>
43
+ `;
44
+ },
45
+ };
46
+ /** `horizontal` sets Chart.js's `indexAxis: 'y'`, flipping bars onto a horizontal axis. */
47
+ export const Horizontal = {
48
+ render: () => {
49
+ const series = [{ label: 'Revenue', data: [12, 19, 14, 22] }];
50
+ return html `
51
+ <lyra-chart
52
+ type="bar"
53
+ horizontal
54
+ height="16rem"
55
+ style="width: 22rem"
56
+ .labels=${['Q1', 'Q2', 'Q3', 'Q4']}
57
+ .datasets=${series}
58
+ ></lyra-chart>
59
+ `;
60
+ },
61
+ };
62
+ /** `stacked` stacks every dataset sharing an axis on top of each other instead of side by side. */
63
+ export const Stacked = {
64
+ render: () => {
65
+ const series = [
66
+ { label: 'Product A', data: [12, 19, 14, 22] },
67
+ { label: 'Product B', data: [8, 11, 9, 15] },
68
+ ];
69
+ return html `
70
+ <lyra-chart
71
+ type="bar"
72
+ stacked
73
+ legend
74
+ height="16rem"
75
+ style="width: 22rem"
76
+ .labels=${['Q1', 'Q2', 'Q3', 'Q4']}
77
+ .datasets=${series}
78
+ ></lyra-chart>
79
+ `;
80
+ },
81
+ };
82
+ /**
83
+ * The `--lyra-chart-grid-color`/`-tick-color`/`-legend-color`/`-tooltip-bg`/
84
+ * `-tooltip-text` custom properties retheme Chart.js's canvas-drawn chrome
85
+ * (grid lines, axis ticks/titles, legend labels, tooltip). Chart.js can't
86
+ * consume `var()` directly, so these are resolved via `getComputedStyle`
87
+ * once per draw — see `themeColors()` in `chart.ts`.
88
+ */
89
+ export const ThemedTokens = {
90
+ render: () => {
91
+ const series = [{ label: 'Revenue', data: [12, 19, 14, 22] }];
92
+ return html `
93
+ <lyra-chart
94
+ type="bar"
95
+ legend
96
+ height="16rem"
97
+ style="
98
+ width: 22rem;
99
+ --lyra-chart-grid-color: #e5484d;
100
+ --lyra-chart-tick-color: #e5484d;
101
+ --lyra-chart-legend-color: #e5484d;
102
+ --lyra-chart-tooltip-bg: #1a1a1a;
103
+ --lyra-chart-tooltip-text: #fff;
104
+ "
105
+ .labels=${['Q1', 'Q2', 'Q3', 'Q4']}
106
+ .datasets=${series}
107
+ ></lyra-chart>
108
+ `;
109
+ },
110
+ };
111
+ /**
112
+ * `lyra-point-click` fires whenever a click lands on (or nearest,
113
+ * intersect-only) a data point/segment — covers any `type`, not just bars.
114
+ * `refreshTheme()` forces a redraw so an out-of-band theme change (e.g. a
115
+ * host-level dark-mode toggle that doesn't touch any `lyra-chart` property)
116
+ * is picked up immediately, rather than waiting for the next reactive update.
117
+ */
118
+ export const PointClickAndRefreshTheme = {
119
+ render: () => {
120
+ const series = [{ label: 'Revenue', data: [12, 19, 14, 22] }];
121
+ return html `
122
+ <lyra-chart
123
+ type="bar"
124
+ height="16rem"
125
+ style="width: 22rem"
126
+ .labels=${['Q1', 'Q2', 'Q3', 'Q4']}
127
+ .datasets=${series}
128
+ @lyra-point-click=${(e) => console.log('lyra-point-click', e.detail)}
129
+ ></lyra-chart>
130
+ <button
131
+ type="button"
132
+ @click=${(e) => {
133
+ const chart = e.target.previousElementSibling;
134
+ chart.refreshTheme();
135
+ }}
136
+ >
137
+ refreshTheme()
138
+ </button>
139
+ `;
140
+ },
141
+ };
@@ -4,6 +4,18 @@ export const styles = css `
4
4
  display: block;
5
5
  position: relative;
6
6
  block-size: var(--lyra-chart-height, 280px);
7
+ /* Chart.js renders to canvas, not the DOM, so it can't consume CSS
8
+ var() directly — chart.ts's themeColors() resolves these once per
9
+ draw() via getComputedStyle, same pattern as heatmap.ts's scale-lo/-hi.
10
+ Each is its own token (rather than reusing the semantic ones directly)
11
+ so a host can retheme just the chart's grid/ticks/legend/tooltip
12
+ without affecting unrelated text/border/surface colors elsewhere in
13
+ the component, while still defaulting to those semantic tokens. */
14
+ --lyra-chart-grid-color: var(--lyra-color-border);
15
+ --lyra-chart-tick-color: var(--lyra-color-text-quiet);
16
+ --lyra-chart-legend-color: var(--lyra-color-text);
17
+ --lyra-chart-tooltip-bg: var(--lyra-color-surface);
18
+ --lyra-chart-tooltip-text: var(--lyra-color-text);
7
19
  }
8
20
  [part='base'] {
9
21
  position: relative;
@@ -30,4 +42,8 @@ export const styles = css `
30
42
  color: var(--lyra-color-text);
31
43
  cursor: pointer;
32
44
  }
45
+ [part='reset-zoom-button']:focus-visible {
46
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
47
+ outline-offset: var(--lyra-focus-ring-offset);
48
+ }
33
49
  `;
@@ -1,4 +1,5 @@
1
1
  import { LyraChart } from './chart.js';
2
+ import { type HistogramBucket } from './histogram-bin.js';
2
3
  /**
3
4
  * `<lyra-histogram>` — bins `values` into `bins` equal-width buckets and
4
5
  * renders them as a bar chart. Chart.js has no built-in histogram
@@ -13,6 +14,7 @@ export declare class LyraHistogram extends LyraChart {
13
14
  values: number[];
14
15
  label: string;
15
16
  }
17
+ export declare function binnedBuckets(el: LyraHistogram): HistogramBucket[];
16
18
  declare global {
17
19
  interface HTMLElementTagNameMap {
18
20
  'lyra-histogram': LyraHistogram;
@@ -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;