@reliability-design/web 1.0.2 → 1.0.6

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 (145) hide show
  1. package/dist/components/rel-accordion/README.md +132 -0
  2. package/dist/components/rel-accordion/rel-accordion.ts +61 -29
  3. package/dist/components/rel-alert/README.md +141 -0
  4. package/dist/components/rel-alert/rel-alert.ts +110 -67
  5. package/dist/components/rel-avatar/README.md +119 -0
  6. package/dist/components/rel-avatar/rel-avatar.ts +8 -15
  7. package/dist/components/rel-badge/README.md +86 -0
  8. package/dist/components/rel-badge/rel-badge.ts +28 -21
  9. package/dist/components/rel-breadcrumb/README.md +102 -0
  10. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +24 -18
  11. package/dist/components/rel-button/README.md +130 -0
  12. package/dist/components/rel-button/rel-button.ts +39 -59
  13. package/dist/components/rel-calendar/README.md +109 -0
  14. package/dist/components/rel-calendar/index.ts +1 -0
  15. package/dist/components/rel-calendar/rel-calendar.ts +467 -0
  16. package/dist/components/rel-card/README.md +94 -0
  17. package/dist/components/rel-card/rel-card.ts +21 -11
  18. package/dist/components/rel-cardmedia/README.md +127 -0
  19. package/dist/components/rel-cardmedia/rel-cardmedia.ts +22 -18
  20. package/dist/components/rel-carousel/README.md +166 -0
  21. package/dist/components/rel-carousel/index.ts +1 -0
  22. package/dist/components/rel-carousel/rel-carousel.ts +469 -0
  23. package/dist/components/rel-chart/README.md +286 -0
  24. package/dist/components/rel-chart/index.ts +1 -0
  25. package/dist/components/rel-chart/rel-chart.ts +736 -0
  26. package/dist/components/rel-checkbox/README.md +77 -0
  27. package/dist/components/rel-checkbox/rel-checkbox.ts +36 -45
  28. package/dist/components/rel-chip/README.md +114 -0
  29. package/dist/components/rel-chip/rel-chip.ts +47 -122
  30. package/dist/components/rel-container/README.md +99 -0
  31. package/dist/components/rel-container/rel-container.ts +1 -6
  32. package/dist/components/rel-date-picker/README.md +253 -0
  33. package/dist/components/rel-date-picker/index.ts +1 -0
  34. package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
  35. package/dist/components/rel-divider/README.md +100 -0
  36. package/dist/components/rel-divider/rel-divider.ts +37 -37
  37. package/dist/components/rel-drawer/README.md +200 -0
  38. package/dist/components/rel-drawer/index.ts +1 -0
  39. package/dist/components/rel-drawer/rel-drawer.ts +621 -0
  40. package/dist/components/rel-kbd/README.md +151 -0
  41. package/dist/components/rel-kbd/rel-kbd.ts +118 -0
  42. package/dist/components/rel-layout/README.md +88 -0
  43. package/dist/components/rel-layout/rel-layout.ts +12 -14
  44. package/dist/components/rel-menu/README.md +151 -0
  45. package/dist/components/rel-menu/rel-menu.ts +18 -10
  46. package/dist/components/rel-menubar/README.md +329 -0
  47. package/dist/components/rel-menubar/index.ts +2 -0
  48. package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
  49. package/dist/components/rel-menubar/rel-menubar.ts +576 -0
  50. package/dist/components/rel-nav-menu/README.md +289 -0
  51. package/dist/components/rel-nav-menu/index.ts +3 -0
  52. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +143 -0
  53. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
  54. package/dist/components/rel-nav-menu/rel-nav-menu.ts +331 -0
  55. package/dist/components/rel-navbar/README.md +126 -0
  56. package/dist/components/rel-navbar/rel-navbar.ts +91 -0
  57. package/dist/components/rel-otp/README.md +440 -0
  58. package/dist/components/rel-otp/index.ts +1 -0
  59. package/dist/components/rel-otp/rel-otp.ts +504 -0
  60. package/dist/components/rel-pagination/README.md +410 -0
  61. package/dist/components/rel-pagination/index.ts +1 -0
  62. package/dist/components/rel-pagination/rel-pagination.ts +376 -0
  63. package/dist/components/rel-popover/README.md +568 -0
  64. package/dist/components/rel-popover/index.ts +2 -0
  65. package/dist/components/rel-popover/rel-popover-content.ts +55 -0
  66. package/dist/components/rel-popover/rel-popover.ts +327 -0
  67. package/dist/components/rel-progress/README.md +110 -0
  68. package/dist/components/rel-progress/rel-progress.ts +58 -20
  69. package/dist/components/rel-quote-page/README.md +166 -0
  70. package/dist/components/rel-quote-page/rel-quote-page.ts +816 -0
  71. package/dist/components/rel-radio/README.md +120 -0
  72. package/dist/components/rel-radio/rel-radio.ts +41 -73
  73. package/dist/components/rel-resizable/README.md +140 -0
  74. package/dist/components/rel-resizable/index.ts +3 -0
  75. package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
  76. package/dist/components/rel-resizable/rel-resizable-handle.ts +149 -0
  77. package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
  78. package/dist/components/rel-segmented-button/README.md +141 -0
  79. package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
  80. package/dist/components/rel-select/README.md +107 -0
  81. package/dist/components/rel-select/rel-select.ts +14 -14
  82. package/dist/components/rel-sheet/README.md +224 -0
  83. package/dist/components/rel-sheet/rel-sheet.ts +683 -0
  84. package/dist/components/rel-sidebar/README.md +209 -0
  85. package/dist/components/rel-sidebar/rel-sidebar.ts +1158 -0
  86. package/dist/components/rel-slider/README.md +135 -0
  87. package/dist/components/rel-slider/rel-slider.ts +19 -7
  88. package/dist/components/rel-snackbar/README.md +134 -0
  89. package/dist/components/rel-snackbar/rel-snackbar.ts +73 -2
  90. package/dist/components/rel-stepper/README.md +160 -0
  91. package/dist/components/rel-stepper/rel-stepper.ts +29 -8
  92. package/dist/components/rel-switch/README.md +102 -0
  93. package/dist/components/rel-switch/rel-switch.ts +22 -11
  94. package/dist/components/rel-tab/README.md +163 -0
  95. package/dist/components/rel-tab/rel-tab.ts +46 -22
  96. package/dist/components/rel-table/README.md +178 -0
  97. package/dist/components/rel-table/rel-table-body.ts +16 -0
  98. package/dist/components/rel-table/rel-table-caption.ts +22 -0
  99. package/dist/components/rel-table/rel-table-cell.ts +49 -0
  100. package/dist/components/rel-table/rel-table-footer.ts +18 -0
  101. package/dist/components/rel-table/rel-table-head.ts +33 -0
  102. package/dist/components/rel-table/rel-table-header.ts +16 -0
  103. package/dist/components/rel-table/rel-table-row.ts +32 -0
  104. package/dist/components/rel-table/rel-table.ts +47 -0
  105. package/dist/components/rel-textfield/README.md +90 -0
  106. package/dist/components/rel-textfield/rel-textfield.ts +37 -21
  107. package/dist/components/rel-toast/README.md +148 -0
  108. package/dist/components/rel-toast/rel-toast.ts +32 -14
  109. package/dist/components/rel-tooltip/README.md +103 -0
  110. package/dist/components/rel-tooltip/rel-tooltip.ts +9 -9
  111. package/dist/index.d.mts +846 -7
  112. package/dist/index.d.ts +846 -7
  113. package/dist/index.js +10860 -1803
  114. package/dist/index.js.map +1 -1
  115. package/dist/index.mjs +10820 -1809
  116. package/dist/index.mjs.map +1 -1
  117. package/dist/styles/vars.css +293 -183
  118. package/dist/vars-ITL2K3KO.css +294 -0
  119. package/package.json +2 -2
  120. package/dist/components/rel-accordion/rel-accordion.stories.ts +0 -313
  121. package/dist/components/rel-alert/rel-alert.stories.ts +0 -412
  122. package/dist/components/rel-avatar/rel-avatar.stories.ts +0 -327
  123. package/dist/components/rel-badge/rel-badge.stories.ts +0 -350
  124. package/dist/components/rel-breadcrumb/rel-breadcrumb.stories.ts +0 -306
  125. package/dist/components/rel-button/rel-button.stories.ts +0 -356
  126. package/dist/components/rel-card/rel-card.stories.ts +0 -299
  127. package/dist/components/rel-cardmedia/rel-cardmedia.stories.ts +0 -368
  128. package/dist/components/rel-checkbox/rel-checkbox.stories.ts +0 -414
  129. package/dist/components/rel-chip/rel-chip.stories.ts +0 -552
  130. package/dist/components/rel-container/rel-container.stories.ts +0 -239
  131. package/dist/components/rel-divider/rel-divider.stories.ts +0 -229
  132. package/dist/components/rel-layout/rel-layout.stories.ts +0 -713
  133. package/dist/components/rel-menu/rel-menu.stories.ts +0 -910
  134. package/dist/components/rel-progress/rel-progress.stories.ts +0 -817
  135. package/dist/components/rel-radio/rel-radio.stories.ts +0 -1048
  136. package/dist/components/rel-segmented-button/rel-segmented-button.stories.ts +0 -1079
  137. package/dist/components/rel-select/rel-select.stories.ts +0 -1201
  138. package/dist/components/rel-slider/rel-slider.stories.ts +0 -730
  139. package/dist/components/rel-snackbar/rel-snackbar.stories.ts +0 -808
  140. package/dist/components/rel-stepper/rel-stepper.stories.ts +0 -752
  141. package/dist/components/rel-switch/rel-switch.stories.ts +0 -441
  142. package/dist/components/rel-tab/rel-tab.stories.ts +0 -492
  143. package/dist/components/rel-textfield/rel-textfield.stories.ts +0 -710
  144. package/dist/components/rel-toast/rel-toast.stories.ts +0 -394
  145. package/dist/components/rel-tooltip/rel-tooltip.stories.ts +0 -364
@@ -0,0 +1,736 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { customElement, property, state, query } from "lit/decorators.js";
3
+
4
+ interface SeriesConfig {
5
+ key: string;
6
+ label?: string;
7
+ colorToken?: string;
8
+ }
9
+
10
+ interface TooltipData {
11
+ x: string;
12
+ series: { label: string; value: number; color: string }[];
13
+ xPos: number;
14
+ yPos: number;
15
+ }
16
+
17
+ @customElement("rel-chart")
18
+ export class RelChart extends LitElement {
19
+ @property({ type: String, reflect: true }) type: "bar" | "line" = "bar";
20
+ @property({ type: Number }) height = 320;
21
+ @property({ type: String, attribute: "x-key" }) xKey = "";
22
+ @property({ type: String, attribute: "series-json" }) seriesJson = "";
23
+ @property({ type: String, attribute: "data-json" }) dataJson = "";
24
+ @property({ type: Boolean, attribute: "show-grid" }) showGrid = true;
25
+ @property({ type: Boolean, attribute: "show-axis" }) showAxis = true;
26
+ @property({ type: Boolean, attribute: "show-legend" }) showLegend = true;
27
+ @property({ type: Boolean }) stacked = false;
28
+ @property({ type: String, attribute: "format-x" }) formatX: "date" | "string" | "number" = "string";
29
+ @property({ type: String }) locale = "en-US";
30
+ @property({ type: Boolean, reflect: true }) disabled = false;
31
+
32
+ // Programmatic properties - using setters to trigger updates
33
+ private _data: Array<Record<string, unknown>> = [];
34
+ private _series: SeriesConfig[] = [];
35
+
36
+ public get data() {
37
+ return this._data;
38
+ }
39
+
40
+ public set data(value: Array<Record<string, unknown>>) {
41
+ const oldValue = this._data;
42
+ this._data = value;
43
+ this.requestUpdate('data', oldValue);
44
+ }
45
+
46
+ public get series() {
47
+ return this._series;
48
+ }
49
+
50
+ public set series(value: SeriesConfig[]) {
51
+ const oldValue = this._series;
52
+ this._series = value;
53
+ this.requestUpdate('series', oldValue);
54
+ }
55
+
56
+ @state() private chartWidth = 600; // Default width before measurement
57
+ @state() private tooltip: TooltipData | null = null;
58
+
59
+ @query(".chart-container") private container?: HTMLElement;
60
+
61
+ private resizeObserver?: ResizeObserver;
62
+ private prefersReducedMotion = false;
63
+ private resizeTimeout?: number;
64
+
65
+ // Default color palette using RDS tokens
66
+ private readonly defaultColors = [
67
+ "var(--rel-colors-blue-primary)",
68
+ "var(--rel-colors-green-primary)",
69
+ "var(--rel-colors-orange-warning)",
70
+ "var(--rel-colors-red-primary)",
71
+ "var(--rel-colors-gold)",
72
+ "var(--rel-colors-platinum)",
73
+ ];
74
+
75
+ static styles = css`
76
+ :host {
77
+ display: block;
78
+ font-family: var(--rel-fontFamilies-primary);
79
+ position: relative;
80
+ }
81
+
82
+ :host([disabled]) {
83
+ opacity: 0.6;
84
+ pointer-events: none;
85
+ }
86
+
87
+ .chart-container {
88
+ width: 100%;
89
+ position: relative;
90
+ background: var(--rel-colors-white, #fff);
91
+ border: 1px solid var(--rel-border-default, #ddd);
92
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
93
+ padding: var(--rel-spacing-space-md, 16px);
94
+ }
95
+
96
+ svg {
97
+ display: block;
98
+ width: 100%;
99
+ overflow: visible;
100
+ }
101
+
102
+ .grid-line {
103
+ stroke: var(--rel-colors-gray-border, #ddd);
104
+ stroke-width: 1;
105
+ stroke-dasharray: 2 2;
106
+ }
107
+
108
+ .axis-line {
109
+ stroke: var(--rel-colors-gray-divider, #bdbdbd);
110
+ stroke-width: 1;
111
+ }
112
+
113
+ .axis-label {
114
+ fill: var(--rel-text-primary, #333);
115
+ font-size: var(--rel-fontSizes-xs, 10px);
116
+ font-family: var(--rel-fontFamilies-primary, sans-serif);
117
+ }
118
+
119
+ .bar {
120
+ cursor: pointer;
121
+ transition: opacity 0.15s ease;
122
+ }
123
+
124
+ .bar:hover {
125
+ opacity: 0.8;
126
+ }
127
+
128
+ .bar:focus-visible {
129
+ outline: 2px solid var(--rel-border-info, #0091ea);
130
+ outline-offset: 2px;
131
+ }
132
+
133
+ .line-path {
134
+ fill: none;
135
+ stroke-width: 2;
136
+ transition: stroke-width 0.15s ease;
137
+ }
138
+
139
+ .line-path:hover {
140
+ stroke-width: 3;
141
+ }
142
+
143
+ .point {
144
+ cursor: pointer;
145
+ transition: r 0.15s ease;
146
+ }
147
+
148
+ .point:hover {
149
+ r: 6;
150
+ }
151
+
152
+ .point:focus-visible {
153
+ outline: 2px solid var(--rel-border-info, #0091ea);
154
+ outline-offset: 2px;
155
+ }
156
+
157
+ .legend {
158
+ display: flex;
159
+ gap: var(--rel-spacing-space-md);
160
+ margin-top: var(--rel-spacing-space-md);
161
+ flex-wrap: wrap;
162
+ justify-content: center;
163
+ }
164
+
165
+ .legend-item {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: var(--rel-spacing-space-xs, 8px);
169
+ font-size: var(--rel-fontSizes-sm, 12px);
170
+ color: var(--rel-text-primary, #333);
171
+ }
172
+
173
+ .legend-color {
174
+ width: 12px;
175
+ height: 12px;
176
+ border-radius: var(--rel-borders-radius-border-radius-100);
177
+ }
178
+
179
+ .tooltip {
180
+ position: absolute;
181
+ background: var(--rel-colors-white, #fff);
182
+ border: 1px solid var(--rel-border-default, #ddd);
183
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
184
+ padding: var(--rel-spacing-space-sm, 12px);
185
+ box-shadow: var(--rel-shadows-shadow-200);
186
+ pointer-events: none;
187
+ z-index: 100;
188
+ font-size: var(--rel-fontSizes-sm, 12px);
189
+ min-width: 120px;
190
+ }
191
+
192
+ .tooltip-title {
193
+ font-weight: var(--rel-fontWeights-semibold, 600);
194
+ color: var(--rel-text-heading, #2b2b2b);
195
+ margin-bottom: var(--rel-spacing-space-xs, 8px);
196
+ }
197
+
198
+ .tooltip-row {
199
+ display: flex;
200
+ justify-content: space-between;
201
+ align-items: center;
202
+ gap: var(--rel-spacing-space-md);
203
+ margin-bottom: var(--rel-spacing-space-3xs);
204
+ }
205
+
206
+ .tooltip-row:last-child {
207
+ margin-bottom: 0;
208
+ }
209
+
210
+ .tooltip-label {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: var(--rel-spacing-space-xs, 8px);
214
+ color: var(--rel-text-primary, #333);
215
+ }
216
+
217
+ .tooltip-dot {
218
+ width: 8px;
219
+ height: 8px;
220
+ border-radius: 50%;
221
+ }
222
+
223
+ .tooltip-value {
224
+ font-weight: var(--rel-fontWeights-semibold, 600);
225
+ color: var(--rel-text-heading, #2b2b2b);
226
+ }
227
+
228
+ @media (prefers-reduced-motion: reduce) {
229
+ .bar,
230
+ .line-path,
231
+ .point {
232
+ transition: none;
233
+ }
234
+ }
235
+ `;
236
+
237
+ connectedCallback() {
238
+ super.connectedCallback();
239
+ this.prefersReducedMotion = window.matchMedia(
240
+ "(prefers-reduced-motion: reduce)"
241
+ ).matches;
242
+
243
+ this.resizeObserver = new ResizeObserver(() => {
244
+ if (this.resizeTimeout) {
245
+ clearTimeout(this.resizeTimeout);
246
+ }
247
+ this.resizeTimeout = window.setTimeout(() => {
248
+ this.measureWidth();
249
+ }, 50);
250
+ });
251
+ }
252
+
253
+ disconnectedCallback() {
254
+ super.disconnectedCallback();
255
+ if (this.resizeObserver) {
256
+ this.resizeObserver.disconnect();
257
+ }
258
+ if (this.resizeTimeout) {
259
+ clearTimeout(this.resizeTimeout);
260
+ }
261
+ }
262
+
263
+ firstUpdated() {
264
+ if (this.container) {
265
+ this.resizeObserver?.observe(this.container);
266
+ this.measureWidth();
267
+ }
268
+ }
269
+
270
+ updated(changedProperties: Map<string, any>) {
271
+ // Re-measure when data or series changes, but only if we have data to render
272
+ if ((changedProperties.has("data") || changedProperties.has("series")) && this._data.length > 0) {
273
+ // Use requestAnimationFrame to ensure DOM is ready
274
+ requestAnimationFrame(() => {
275
+ this.measureWidth();
276
+ });
277
+ }
278
+ }
279
+
280
+ private measureWidth() {
281
+ if (this.container) {
282
+ const containerWidth = this.container.offsetWidth;
283
+
284
+ if (containerWidth === 0) return;
285
+
286
+ const measuredWidth = containerWidth - 32; // Account for padding
287
+ const newWidth = Math.max(measuredWidth, 100);
288
+
289
+ if (this.chartWidth !== newWidth) {
290
+ this.chartWidth = newWidth;
291
+ }
292
+ }
293
+ }
294
+
295
+ private getChartData(): Array<Record<string, unknown>> {
296
+ if (this.data && this.data.length > 0) {
297
+ return this.data;
298
+ }
299
+
300
+ if (this.dataJson) {
301
+ try {
302
+ return JSON.parse(this.dataJson);
303
+ } catch (e) {
304
+ console.error("Failed to parse data-json:", e);
305
+ return [];
306
+ }
307
+ }
308
+
309
+ return [];
310
+ }
311
+
312
+ private getSeriesConfig(): SeriesConfig[] {
313
+ if (this.series && this.series.length > 0) {
314
+ return this.series;
315
+ }
316
+
317
+ if (this.seriesJson) {
318
+ try {
319
+ return JSON.parse(this.seriesJson);
320
+ } catch (e) {
321
+ console.error("Failed to parse series-json:", e);
322
+ return [];
323
+ }
324
+ }
325
+
326
+ return [];
327
+ }
328
+
329
+ private getSeriesColor(index: number, config?: SeriesConfig): string {
330
+ if (config?.colorToken) {
331
+ return config.colorToken.startsWith("var(")
332
+ ? config.colorToken
333
+ : `var(${config.colorToken})`;
334
+ }
335
+ return this.defaultColors[index % this.defaultColors.length];
336
+ }
337
+
338
+ private formatXValue(value: unknown): string {
339
+ if (this.formatX === "date" && value instanceof Date) {
340
+ return new Intl.DateTimeFormat(this.locale, {
341
+ month: "short",
342
+ day: "numeric",
343
+ }).format(value);
344
+ }
345
+
346
+ if (this.formatX === "date" && typeof value === "string") {
347
+ try {
348
+ const date = new Date(value);
349
+ return new Intl.DateTimeFormat(this.locale, {
350
+ month: "short",
351
+ day: "numeric",
352
+ }).format(date);
353
+ } catch {
354
+ return String(value);
355
+ }
356
+ }
357
+
358
+ return String(value);
359
+ }
360
+
361
+ private calculateScales(data: Array<Record<string, unknown>>, seriesConfigs: SeriesConfig[]) {
362
+ const padding = { top: 20, right: 20, bottom: 40, left: 50 };
363
+ const chartHeight = this.height - padding.top - padding.bottom;
364
+ const chartWidth = this.chartWidth - padding.left - padding.right;
365
+
366
+ // Find max value across all series
367
+ let maxValue = 0;
368
+ data.forEach((item) => {
369
+ seriesConfigs.forEach((config) => {
370
+ const value = Number(item[config.key]) || 0;
371
+ maxValue = Math.max(maxValue, value);
372
+ });
373
+ });
374
+
375
+ // Add 10% padding to max
376
+ maxValue = maxValue * 1.1;
377
+
378
+ return {
379
+ padding,
380
+ chartHeight,
381
+ chartWidth,
382
+ maxValue,
383
+ xScale: (index: number) => padding.left + (index * chartWidth) / data.length,
384
+ yScale: (value: number) => padding.top + chartHeight - (value / maxValue) * chartHeight,
385
+ barWidth: chartWidth / data.length * 0.6 / seriesConfigs.length,
386
+ };
387
+ }
388
+
389
+ private handleBarHover(
390
+ event: MouseEvent,
391
+ datum: Record<string, unknown>,
392
+ index: number
393
+ ) {
394
+ if (this.disabled) return;
395
+
396
+ const data = this.getChartData();
397
+ const seriesConfigs = this.getSeriesConfig();
398
+ const target = event.currentTarget as SVGElement;
399
+ const rect = target.getBoundingClientRect();
400
+
401
+ const tooltipData: TooltipData = {
402
+ x: this.formatXValue(datum[this.xKey]),
403
+ series: seriesConfigs.map((config, i) => ({
404
+ label: config.label || config.key,
405
+ value: Number(datum[config.key]) || 0,
406
+ color: this.getSeriesColor(i, config),
407
+ })),
408
+ xPos: rect.left + rect.width / 2,
409
+ yPos: rect.top,
410
+ };
411
+
412
+ this.tooltip = tooltipData;
413
+
414
+ this.dispatchEvent(
415
+ new CustomEvent("rel-hover", {
416
+ detail: { index, datum, seriesKey: undefined },
417
+ bubbles: true,
418
+ composed: true,
419
+ })
420
+ );
421
+ }
422
+
423
+ private handlePointHover(
424
+ event: MouseEvent,
425
+ datum: Record<string, unknown>,
426
+ index: number,
427
+ seriesKey: string
428
+ ) {
429
+ if (this.disabled) return;
430
+
431
+ const seriesConfigs = this.getSeriesConfig();
432
+ const seriesIndex = seriesConfigs.findIndex((s) => s.key === seriesKey);
433
+ const config = seriesConfigs[seriesIndex];
434
+ const target = event.currentTarget as SVGElement;
435
+ const rect = target.getBoundingClientRect();
436
+
437
+ const tooltipData: TooltipData = {
438
+ x: this.formatXValue(datum[this.xKey]),
439
+ series: [{
440
+ label: config.label || config.key,
441
+ value: Number(datum[config.key]) || 0,
442
+ color: this.getSeriesColor(seriesIndex, config),
443
+ }],
444
+ xPos: rect.left + rect.width / 2,
445
+ yPos: rect.top,
446
+ };
447
+
448
+ this.tooltip = tooltipData;
449
+
450
+ this.dispatchEvent(
451
+ new CustomEvent("rel-hover", {
452
+ detail: { index, datum, seriesKey },
453
+ bubbles: true,
454
+ composed: true,
455
+ })
456
+ );
457
+ }
458
+
459
+ private handleMouseLeave() {
460
+ this.tooltip = null;
461
+ }
462
+
463
+ private renderGrid(scales: ReturnType<typeof this.calculateScales>) {
464
+ if (!this.showGrid) return nothing;
465
+
466
+ const gridLines = [];
467
+ const numLines = 5;
468
+
469
+ for (let i = 0; i <= numLines; i++) {
470
+ const y = scales.padding.top + (i * scales.chartHeight) / numLines;
471
+ gridLines.push(html`
472
+ <line
473
+ class="grid-line"
474
+ x1="${scales.padding.left}"
475
+ y1="${y}"
476
+ x2="${scales.padding.left + scales.chartWidth}"
477
+ y2="${y}"
478
+ part="grid"
479
+ />
480
+ `);
481
+ }
482
+
483
+ return gridLines;
484
+ }
485
+
486
+ private renderAxis(
487
+ data: Array<Record<string, unknown>>,
488
+ scales: ReturnType<typeof this.calculateScales>
489
+ ) {
490
+ if (!this.showAxis) return nothing;
491
+
492
+ const yAxisLabels = [];
493
+ const numLabels = 5;
494
+
495
+ for (let i = 0; i <= numLabels; i++) {
496
+ const value = (scales.maxValue * (numLabels - i)) / numLabels;
497
+ const y = scales.padding.top + (i * scales.chartHeight) / numLabels;
498
+
499
+ yAxisLabels.push(html`
500
+ <text
501
+ class="axis-label"
502
+ x="${scales.padding.left - 10}"
503
+ y="${y + 4}"
504
+ text-anchor="end"
505
+ part="axis"
506
+ >
507
+ ${Math.round(value)}
508
+ </text>
509
+ `);
510
+ }
511
+
512
+ const xAxisLabels = data.map((item, index) => {
513
+ const x = scales.xScale(index) + scales.chartWidth / data.length / 2;
514
+ return html`
515
+ <text
516
+ class="axis-label"
517
+ x="${x}"
518
+ y="${scales.padding.top + scales.chartHeight + 20}"
519
+ text-anchor="middle"
520
+ part="axis"
521
+ >
522
+ ${this.formatXValue(item[this.xKey])}
523
+ </text>
524
+ `;
525
+ });
526
+
527
+ return html`
528
+ <line
529
+ class="axis-line"
530
+ x1="${scales.padding.left}"
531
+ y1="${scales.padding.top + scales.chartHeight}"
532
+ x2="${scales.padding.left + scales.chartWidth}"
533
+ y2="${scales.padding.top + scales.chartHeight}"
534
+ part="axis"
535
+ />
536
+ <line
537
+ class="axis-line"
538
+ x1="${scales.padding.left}"
539
+ y1="${scales.padding.top}"
540
+ x2="${scales.padding.left}"
541
+ y2="${scales.padding.top + scales.chartHeight}"
542
+ part="axis"
543
+ />
544
+ ${yAxisLabels} ${xAxisLabels}
545
+ `;
546
+ }
547
+
548
+ private renderBarChart(
549
+ data: Array<Record<string, unknown>>,
550
+ seriesConfigs: SeriesConfig[]
551
+ ) {
552
+ const scales = this.calculateScales(data, seriesConfigs);
553
+
554
+ const bars = data.flatMap((item, dataIndex) => {
555
+ return seriesConfigs.map((config, seriesIndex) => {
556
+ const value = Number(item[config.key]) || 0;
557
+ const x =
558
+ scales.xScale(dataIndex) +
559
+ (scales.chartWidth / data.length - scales.barWidth * seriesConfigs.length) / 2 +
560
+ seriesIndex * scales.barWidth;
561
+ const y = scales.yScale(value);
562
+ const height = scales.padding.top + scales.chartHeight - y;
563
+ const color = this.getSeriesColor(seriesIndex, config);
564
+
565
+ return html`
566
+ <rect
567
+ class="bar"
568
+ part="bar"
569
+ x="${x}"
570
+ y="${y}"
571
+ width="${scales.barWidth}"
572
+ height="${height}"
573
+ fill="${color}"
574
+ role="button"
575
+ tabindex="0"
576
+ aria-label="${config.label || config.key}: ${value}"
577
+ @mouseenter="${(e: MouseEvent) => this.handleBarHover(e, item, dataIndex)}"
578
+ @mouseleave="${this.handleMouseLeave}"
579
+ @focus="${(e: FocusEvent) => this.handleBarHover(e as any, item, dataIndex)}"
580
+ @blur="${this.handleMouseLeave}"
581
+ />
582
+ `;
583
+ });
584
+ });
585
+
586
+ return html`
587
+ <svg
588
+ height="${this.height}"
589
+ part="svg"
590
+ role="img"
591
+ aria-label="Bar chart"
592
+ >
593
+ ${this.renderGrid(scales)} ${this.renderAxis(data, scales)} ${bars}
594
+ </svg>
595
+ `;
596
+ }
597
+
598
+ private renderLineChart(
599
+ data: Array<Record<string, unknown>>,
600
+ seriesConfigs: SeriesConfig[]
601
+ ) {
602
+ const scales = this.calculateScales(data, seriesConfigs);
603
+
604
+ const lines = seriesConfigs.map((config, seriesIndex) => {
605
+ const points = data.map((item, index) => {
606
+ const value = Number(item[config.key]) || 0;
607
+ const x = scales.xScale(index) + scales.chartWidth / data.length / 2;
608
+ const y = scales.yScale(value);
609
+ return { x, y, value, item, index };
610
+ });
611
+
612
+ const pathData = points
613
+ .map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`)
614
+ .join(" ");
615
+
616
+ return html`
617
+ <path
618
+ class="line-path"
619
+ part="line"
620
+ d="${pathData}"
621
+ stroke="${this.getSeriesColor(seriesIndex, config)}"
622
+ />
623
+ ${points.map(
624
+ (p) => html`
625
+ <circle
626
+ class="point"
627
+ part="point"
628
+ cx="${p.x}"
629
+ cy="${p.y}"
630
+ r="4"
631
+ fill="${this.getSeriesColor(seriesIndex, config)}"
632
+ role="button"
633
+ tabindex="0"
634
+ aria-label="${config.label || config.key}: ${p.value}"
635
+ @mouseenter="${(e: MouseEvent) =>
636
+ this.handlePointHover(e, p.item, p.index, config.key)}"
637
+ @mouseleave="${this.handleMouseLeave}"
638
+ @focus="${(e: FocusEvent) =>
639
+ this.handlePointHover(e as any, p.item, p.index, config.key)}"
640
+ @blur="${this.handleMouseLeave}"
641
+ />
642
+ `
643
+ )}
644
+ `;
645
+ });
646
+
647
+ return html`
648
+ <svg
649
+ height="${this.height}"
650
+ part="svg"
651
+ role="img"
652
+ aria-label="Line chart"
653
+ >
654
+ ${this.renderGrid(scales)} ${this.renderAxis(data, scales)} ${lines}
655
+ </svg>
656
+ `;
657
+ }
658
+
659
+ private renderLegend(seriesConfigs: SeriesConfig[]) {
660
+ if (!this.showLegend || seriesConfigs.length === 0) return nothing;
661
+
662
+ return html`
663
+ <div class="legend" part="legend">
664
+ ${seriesConfigs.map(
665
+ (config, index) => html`
666
+ <div class="legend-item">
667
+ <div
668
+ class="legend-color"
669
+ style="background: ${this.getSeriesColor(index, config)}"
670
+ ></div>
671
+ <span>${config.label || config.key}</span>
672
+ </div>
673
+ `
674
+ )}
675
+ </div>
676
+ `;
677
+ }
678
+
679
+ private renderTooltip() {
680
+ if (!this.tooltip) return nothing;
681
+
682
+ const containerRect = this.container?.getBoundingClientRect();
683
+ if (!containerRect) return nothing;
684
+
685
+ const tooltipX = this.tooltip.xPos - containerRect.left;
686
+ const tooltipY = this.tooltip.yPos - containerRect.top - 10;
687
+
688
+ return html`
689
+ <div
690
+ class="tooltip"
691
+ part="tooltip"
692
+ style="left: ${tooltipX}px; top: ${tooltipY}px; transform: translate(-50%, -100%)"
693
+ >
694
+ <div class="tooltip-title">${this.tooltip.x}</div>
695
+ ${this.tooltip.series.map(
696
+ (s) => html`
697
+ <div class="tooltip-row">
698
+ <div class="tooltip-label">
699
+ <div
700
+ class="tooltip-dot"
701
+ style="background: ${s.color}"
702
+ ></div>
703
+ <span>${s.label}</span>
704
+ </div>
705
+ <span class="tooltip-value">${s.value}</span>
706
+ </div>
707
+ `
708
+ )}
709
+ </div>
710
+ `;
711
+ }
712
+
713
+ render() {
714
+ const data = this.getChartData();
715
+ const seriesConfigs = this.getSeriesConfig();
716
+
717
+ if (!data.length || !seriesConfigs.length || !this.xKey) {
718
+ return html`
719
+ <div class="chart-container" part="container">
720
+ <p style="text-align: center; color: var(--rel-text-placeholder, #a9a9a9); padding: 40px;">
721
+ No data available
722
+ </p>
723
+ </div>
724
+ `;
725
+ }
726
+
727
+ return html`
728
+ <div class="chart-container" part="container">
729
+ ${this.type === "bar"
730
+ ? this.renderBarChart(data, seriesConfigs)
731
+ : this.renderLineChart(data, seriesConfigs)}
732
+ ${this.renderLegend(seriesConfigs)} ${this.renderTooltip()}
733
+ </div>
734
+ `;
735
+ }
736
+ }