@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
@@ -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
@@ -69,25 +77,73 @@ export declare class LyraChart extends LyraElement {
69
77
  /** True until the lazy-loaded `chart.js` peer dependency has settled (success or failure). */
70
78
  private loading;
71
79
  private zoomed;
80
+ private visible;
81
+ private intersectionObserver?;
82
+ private lastSignature;
72
83
  private canvasEl?;
73
84
  private chart?;
74
85
  private chartJsModule?;
75
86
  private builtType?;
87
+ private suppressZoomComplete;
76
88
  connectedCallback(): void;
77
89
  disconnectedCallback(): void;
78
90
  protected updated(changed: PropertyValues): void;
79
91
  private seriesToDataset;
92
+ /**
93
+ * Resolves the `--lyra-chart-*` theme tokens (declared in
94
+ * `chart.styles.ts`, each layered over an existing semantic token) via
95
+ * `getComputedStyle`. Chart.js renders to canvas, not the DOM, so it can't
96
+ * consume CSS `var()` directly — same constraint documented on
97
+ * `heatmap.ts`'s `labelColor()`/`noDataFill()`/`scaleEndpoints()` — so this
98
+ * is called fresh from `buildConfig()` on every draw rather than cached.
99
+ */
100
+ private themeColors;
101
+ /**
102
+ * A content-affecting-properties fingerprint used by `updated()` to skip a
103
+ * redundant `draw()` when neither visibility nor any of these properties
104
+ * actually changed since the last draw (e.g. an unrelated property/state
105
+ * update, or `requestUpdate()` with nothing changed). Mirrors
106
+ * `lite-chart.ts`'s `computeSignature()`.
107
+ */
108
+ private computeSignature;
80
109
  /**
81
110
  * Builds `options.scales` for the effective chart type: no scale at all for
82
111
  * pie/doughnut (a proportional-area chart has no axis), the single radial
83
112
  * `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.
113
+ * `x`/`y`(/`y2`) block for every other (line/bar/scatter/bubble) type — with
114
+ * `x` itself further split within that block: scatter and bubble datasets
115
+ * carry raw numeric `{x, y(, r)}` points (via `Series.points`) and need a
116
+ * linear `x` scale, while line/bar plot against `labels` and need the
117
+ * default categorical one. `theme` (from `themeColors()`) drives every
118
+ * scale's `ticks.color`/`grid.color`/axis `title.color` so grid lines and
119
+ * labels retheme instead of sitting at Chart.js's own hardcoded defaults.
85
120
  */
86
121
  private buildScales;
122
+ /**
123
+ * `options.onClick` handler wired in `buildConfig()`. The chart-wide
124
+ * `interaction` mode above (`'index'`/`'nearest'`, `intersect: false`) is
125
+ * tuned for hover tooltips — resolving which single point/segment was
126
+ * actually clicked needs its own `getElementsAtEventForMode('nearest', {
127
+ * intersect: true }, true)` lookup instead, so a click landing off any
128
+ * point/segment reports nothing (`elements` empty) rather than firing for
129
+ * whatever's nearest. Covers the per-bar/per-segment click ask for any
130
+ * chart type (bar/line/pie/doughnut/etc.), not just bars.
131
+ */
132
+ private handlePointClick;
87
133
  private buildConfig;
88
134
  private draw;
89
135
  /** Reset any active zoom/pan back to the original view. */
90
136
  resetZoom(): void;
137
+ /**
138
+ * Forces a redraw so `themeColors()` re-reads the `--lyra-chart-*` custom
139
+ * properties from the current computed style. No global theme-broadcast
140
+ * event exists anywhere in lyra-ui (nothing to subscribe to here) — this
141
+ * is the escape hatch for a consumer's own theme-toggle handler to call
142
+ * directly when it flips e.g. a `data-theme` attribute upstream that
143
+ * doesn't otherwise change any `lyra-chart` property, so Lit's reactive
144
+ * update loop has nothing of its own to trigger `draw()` on.
145
+ */
146
+ refreshTheme(): void;
91
147
  render(): TemplateResult;
92
148
  }
93
149
  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,21 @@ 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
+ this.visible = true;
103
+ this.lastSignature = '';
104
+ // `chartjs-plugin-zoom`'s own `resetZoom()` synchronously re-invokes the
105
+ // `onZoomComplete` callback below as part of its reset, which would emit a
106
+ // stale `{zoomed: true}` right before `resetZoom()` emits the real
107
+ // `{zoomed: false}`. Set while this component's own `resetZoom()` is
108
+ // driving the plugin so that re-entrant callback is ignored.
109
+ this.suppressZoomComplete = false;
82
110
  }
83
111
  static { this.styles = [LyraElement.styles, styles]; }
84
112
  connectedCallback() {
@@ -90,11 +118,21 @@ export class LyraChart extends LyraElement {
90
118
  this.chartJsModule = mod;
91
119
  this.draw();
92
120
  });
121
+ if (typeof IntersectionObserver !== 'undefined') {
122
+ this.intersectionObserver = new IntersectionObserver((entries) => {
123
+ const wasVisible = this.visible;
124
+ this.visible = entries[0]?.isIntersecting ?? true;
125
+ if (this.visible && !wasVisible)
126
+ this.draw();
127
+ });
128
+ this.intersectionObserver.observe(this);
129
+ }
93
130
  }
94
131
  disconnectedCallback() {
95
132
  super.disconnectedCallback();
96
133
  this.chart?.destroy();
97
134
  this.chart = undefined;
135
+ this.intersectionObserver?.disconnect();
98
136
  }
99
137
  updated(changed) {
100
138
  if (this.loading)
@@ -109,9 +147,23 @@ export class LyraChart extends LyraElement {
109
147
  if (changed.has('height')) {
110
148
  this.style.setProperty('--lyra-chart-height', this.height);
111
149
  }
150
+ // While `chart.js` is still loading, `draw()` would no-op anyway (no
151
+ // `chartJsModule`/`canvasEl` yet) — bail before touching `lastSignature`
152
+ // so that phantom "no-op" update doesn't get cached as the baseline and
153
+ // silently swallow the real first draw once loading finishes with no
154
+ // other property having changed in the meantime.
155
+ if (this.loading)
156
+ return;
112
157
  const onlyZoomChanged = changed.size === 1 && changed.has('zoomed');
113
- if (!onlyZoomChanged)
114
- this.draw();
158
+ if (onlyZoomChanged)
159
+ return;
160
+ if (!this.visible)
161
+ return; // becoming visible again triggers its own draw() via the observer above
162
+ const signature = this.computeSignature();
163
+ if (signature === this.lastSignature)
164
+ return;
165
+ this.lastSignature = signature;
166
+ this.draw();
115
167
  }
116
168
  seriesToDataset(s) {
117
169
  const colors = Array.isArray(s.color) ? s.color : s.color ? [s.color] : undefined;
@@ -130,41 +182,134 @@ export class LyraChart extends LyraElement {
130
182
  tooltip: s.noTooltip ? { enabled: false } : undefined,
131
183
  };
132
184
  }
185
+ /**
186
+ * Resolves the `--lyra-chart-*` theme tokens (declared in
187
+ * `chart.styles.ts`, each layered over an existing semantic token) via
188
+ * `getComputedStyle`. Chart.js renders to canvas, not the DOM, so it can't
189
+ * consume CSS `var()` directly — same constraint documented on
190
+ * `heatmap.ts`'s `labelColor()`/`noDataFill()`/`scaleEndpoints()` — so this
191
+ * is called fresh from `buildConfig()` on every draw rather than cached.
192
+ */
193
+ themeColors() {
194
+ const cs = getComputedStyle(this);
195
+ return {
196
+ grid: cs.getPropertyValue('--lyra-chart-grid-color').trim() || FALLBACK_GRID_COLOR,
197
+ tick: cs.getPropertyValue('--lyra-chart-tick-color').trim() || FALLBACK_TICK_COLOR,
198
+ legend: cs.getPropertyValue('--lyra-chart-legend-color').trim() || FALLBACK_LEGEND_COLOR,
199
+ tooltipBg: cs.getPropertyValue('--lyra-chart-tooltip-bg').trim() || FALLBACK_TOOLTIP_BG,
200
+ tooltipText: cs.getPropertyValue('--lyra-chart-tooltip-text').trim() || FALLBACK_TOOLTIP_TEXT,
201
+ };
202
+ }
203
+ /**
204
+ * A content-affecting-properties fingerprint used by `updated()` to skip a
205
+ * redundant `draw()` when neither visibility nor any of these properties
206
+ * actually changed since the last draw (e.g. an unrelated property/state
207
+ * update, or `requestUpdate()` with nothing changed). Mirrors
208
+ * `lite-chart.ts`'s `computeSignature()`.
209
+ */
210
+ computeSignature() {
211
+ return JSON.stringify([
212
+ this.type,
213
+ this.labels,
214
+ this.datasets,
215
+ this.legend,
216
+ this.area,
217
+ this.xLabel,
218
+ this.yLabel,
219
+ this.y2Label,
220
+ this.beginAtZero,
221
+ this.horizontal,
222
+ this.stacked,
223
+ this.config,
224
+ ]);
225
+ }
133
226
  /**
134
227
  * Builds `options.scales` for the effective chart type: no scale at all for
135
228
  * pie/doughnut (a proportional-area chart has no axis), the single radial
136
229
  * `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.
230
+ * `x`/`y`(/`y2`) block for every other (line/bar/scatter/bubble) type — with
231
+ * `x` itself further split within that block: scatter and bubble datasets
232
+ * carry raw numeric `{x, y(, r)}` points (via `Series.points`) and need a
233
+ * linear `x` scale, while line/bar plot against `labels` and need the
234
+ * default categorical one. `theme` (from `themeColors()`) drives every
235
+ * scale's `ticks.color`/`grid.color`/axis `title.color` so grid lines and
236
+ * labels retheme instead of sitting at Chart.js's own hardcoded defaults.
138
237
  */
139
- buildScales() {
238
+ buildScales(theme) {
140
239
  if (this.type === 'pie' || this.type === 'doughnut')
141
240
  return {};
142
241
  if (this.type === 'radar' || this.type === 'polarArea') {
143
242
  return {
144
243
  r: {
145
244
  beginAtZero: this.beginAtZero,
245
+ ticks: { color: theme.tick },
246
+ grid: { color: theme.grid },
247
+ angleLines: { color: theme.grid },
248
+ pointLabels: { color: theme.tick },
146
249
  },
147
250
  };
148
251
  }
149
252
  const hasY2 = this.datasets.some((s) => s.axis === 'y2');
253
+ // `stacked` only applies to bar/line-family charts sharing a categorical
254
+ // axis, per the design spec — scatter/bubble's linear x scale and the
255
+ // radial r scale above are out of scope.
256
+ const stacked = this.stacked && (this.type === 'bar' || this.type === 'line');
150
257
  return {
151
258
  x: {
152
- type: this.type === 'scatter' ? 'linear' : 'category',
153
- title: { display: !!this.xLabel, text: this.xLabel },
259
+ type: this.type === 'scatter' || this.type === 'bubble' ? 'linear' : 'category',
260
+ title: { display: !!this.xLabel, text: this.xLabel, color: theme.tick },
261
+ ticks: { color: theme.tick },
262
+ grid: { color: theme.grid },
263
+ stacked,
264
+ },
265
+ y: {
266
+ beginAtZero: this.beginAtZero,
267
+ title: { display: !!this.yLabel, text: this.yLabel, color: theme.tick },
268
+ ticks: { color: theme.tick },
269
+ grid: { color: theme.grid },
270
+ stacked,
154
271
  },
155
- y: { beginAtZero: this.beginAtZero, title: { display: !!this.yLabel, text: this.yLabel } },
156
272
  ...(hasY2
157
273
  ? {
158
274
  y2: {
159
275
  position: 'right',
160
- grid: { drawOnChartArea: false },
161
- title: { display: !!this.y2Label, text: this.y2Label },
276
+ grid: { drawOnChartArea: false, color: theme.grid },
277
+ title: { display: !!this.y2Label, text: this.y2Label, color: theme.tick },
278
+ ticks: { color: theme.tick },
279
+ stacked,
162
280
  },
163
281
  }
164
282
  : {}),
165
283
  };
166
284
  }
285
+ /**
286
+ * `options.onClick` handler wired in `buildConfig()`. The chart-wide
287
+ * `interaction` mode above (`'index'`/`'nearest'`, `intersect: false`) is
288
+ * tuned for hover tooltips — resolving which single point/segment was
289
+ * actually clicked needs its own `getElementsAtEventForMode('nearest', {
290
+ * intersect: true }, true)` lookup instead, so a click landing off any
291
+ * point/segment reports nothing (`elements` empty) rather than firing for
292
+ * whatever's nearest. Covers the per-bar/per-segment click ask for any
293
+ * chart type (bar/line/pie/doughnut/etc.), not just bars.
294
+ */
295
+ handlePointClick(event, chart) {
296
+ // Chart.js's own `onClick` handler hands us its `ChartEvent` wrapper, but
297
+ // `getElementsAtEventForMode()`'s .d.ts (inaccurately) types its first
298
+ // param as a DOM `Event` — at runtime Chart.js only reads `.x`/`.y` off
299
+ // whatever is passed (see chart.js/src/helpers/helpers.dom.ts
300
+ // `getRelativePosition()`), which `ChartEvent` already has, so the cast
301
+ // here is a type-only correction, not a behavior change.
302
+ const elements = chart.getElementsAtEventForMode(event, 'nearest', { intersect: true }, true);
303
+ const hit = elements[0];
304
+ if (!hit)
305
+ return;
306
+ const { datasetIndex, index } = hit;
307
+ const label = chart.data.labels?.[index];
308
+ const value = chart.data.datasets[datasetIndex]?.data?.[index] ?? null;
309
+ this.emit('lyra-point-click', { datasetIndex, index, label, value });
310
+ }
167
311
  buildConfig() {
312
+ const theme = this.themeColors();
168
313
  const generated = {
169
314
  type: this.type,
170
315
  data: {
@@ -174,9 +319,25 @@ export class LyraChart extends LyraElement {
174
319
  options: {
175
320
  responsive: true,
176
321
  maintainAspectRatio: false,
322
+ // Chart.js's own mechanism for horizontal bars (also flips line/area
323
+ // types onto a horizontal category axis).
324
+ indexAxis: this.horizontal ? 'y' : undefined,
325
+ // Chart.js's own default ~1000ms draw-in animation only ever fires
326
+ // from `new Chart()` (chart/type-change construction) — every
327
+ // in-place update in `draw()` already passes `'none'` to
328
+ // `Chart#update()`. A CSS media query can't reach that
329
+ // canvas-internal animation loop, so `prefersReducedMotion()` is
330
+ // checked here instead and fed into `options.animation`.
331
+ animation: prefersReducedMotion() ? false : undefined,
177
332
  interaction: { intersect: false, mode: this.type === 'scatter' ? 'nearest' : 'index' },
333
+ onClick: (event, _elements, chart) => this.handlePointClick(event, chart),
178
334
  plugins: {
179
- legend: { display: this.legend },
335
+ legend: { display: this.legend, labels: { color: theme.legend } },
336
+ tooltip: {
337
+ backgroundColor: theme.tooltipBg,
338
+ titleColor: theme.tooltipText,
339
+ bodyColor: theme.tooltipText,
340
+ },
180
341
  zoom: this.zoom
181
342
  ? {
182
343
  pan: { enabled: false },
@@ -186,6 +347,8 @@ export class LyraChart extends LyraElement {
186
347
  pinch: { enabled: true },
187
348
  mode: 'x',
188
349
  onZoomComplete: () => {
350
+ if (this.suppressZoomComplete)
351
+ return;
189
352
  this.zoomed = true;
190
353
  this.emit('lyra-zoom', { zoomed: true });
191
354
  },
@@ -194,7 +357,7 @@ export class LyraChart extends LyraElement {
194
357
  }
195
358
  : undefined,
196
359
  },
197
- scales: this.buildScales(),
360
+ scales: this.buildScales(theme),
198
361
  },
199
362
  };
200
363
  if (!this.config)
@@ -223,10 +386,28 @@ export class LyraChart extends LyraElement {
223
386
  }
224
387
  /** Reset any active zoom/pan back to the original view. */
225
388
  resetZoom() {
226
- this.chart?.resetZoom?.();
389
+ this.suppressZoomComplete = true;
390
+ try {
391
+ this.chart?.resetZoom?.();
392
+ }
393
+ finally {
394
+ this.suppressZoomComplete = false;
395
+ }
227
396
  this.zoomed = false;
228
397
  this.emit('lyra-zoom', { zoomed: false });
229
398
  }
399
+ /**
400
+ * Forces a redraw so `themeColors()` re-reads the `--lyra-chart-*` custom
401
+ * properties from the current computed style. No global theme-broadcast
402
+ * event exists anywhere in lyra-ui (nothing to subscribe to here) — this
403
+ * is the escape hatch for a consumer's own theme-toggle handler to call
404
+ * directly when it flips e.g. a `data-theme` attribute upstream that
405
+ * doesn't otherwise change any `lyra-chart` property, so Lit's reactive
406
+ * update loop has nothing of its own to trigger `draw()` on.
407
+ */
408
+ refreshTheme() {
409
+ this.draw();
410
+ }
230
411
  render() {
231
412
  if (this.loading) {
232
413
  return html `
@@ -281,6 +462,12 @@ __decorate([
281
462
  __decorate([
282
463
  property({ type: Boolean, attribute: 'begin-at-zero' })
283
464
  ], LyraChart.prototype, "beginAtZero", void 0);
465
+ __decorate([
466
+ property({ type: Boolean })
467
+ ], LyraChart.prototype, "horizontal", void 0);
468
+ __decorate([
469
+ property({ type: Boolean })
470
+ ], LyraChart.prototype, "stacked", void 0);
284
471
  __decorate([
285
472
  property({ attribute: false })
286
473
  ], LyraChart.prototype, "config", void 0);
@@ -290,6 +477,9 @@ __decorate([
290
477
  __decorate([
291
478
  state()
292
479
  ], LyraChart.prototype, "zoomed", void 0);
480
+ __decorate([
481
+ state()
482
+ ], LyraChart.prototype, "visible", void 0);
293
483
  __decorate([
294
484
  query('canvas')
295
485
  ], LyraChart.prototype, "canvasEl", 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;