@redvars/peacock 3.3.0 → 3.3.2

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 (159) hide show
  1. package/dist/{IndividualComponent-tDnXrOLV.js → IndividualComponent-Dt5xirYG.js} +2 -2
  2. package/dist/{IndividualComponent-tDnXrOLV.js.map → IndividualComponent-Dt5xirYG.js.map} +1 -1
  3. package/dist/array-D5vjT2Xm.js +14 -0
  4. package/dist/array-D5vjT2Xm.js.map +1 -0
  5. package/dist/assets/components.css +1 -1
  6. package/dist/assets/components.css.map +1 -1
  7. package/dist/assets/styles.css +1 -1
  8. package/dist/assets/styles.css.map +1 -1
  9. package/dist/{button-BGFJfbT2.js → button-ClzS8JLq.js} +3 -4
  10. package/dist/{button-BGFJfbT2.js.map → button-ClzS8JLq.js.map} +1 -1
  11. package/dist/button-group-BMS5WvaF.js +292 -0
  12. package/dist/button-group-BMS5WvaF.js.map +1 -0
  13. package/dist/button-group.js +6 -107
  14. package/dist/button-group.js.map +1 -1
  15. package/dist/button.js +3 -4
  16. package/dist/button.js.map +1 -1
  17. package/dist/card.js +104 -0
  18. package/dist/card.js.map +1 -0
  19. package/dist/chart-bar-DbnXQgvS.js +1121 -0
  20. package/dist/chart-bar-DbnXQgvS.js.map +1 -0
  21. package/dist/chart-bar.js +259 -0
  22. package/dist/chart-bar.js.map +1 -0
  23. package/dist/chart-donut.js +4 -2
  24. package/dist/chart-donut.js.map +1 -1
  25. package/dist/chart-doughnut.js +4 -2
  26. package/dist/chart-doughnut.js.map +1 -1
  27. package/dist/chart-pie.js +4 -2
  28. package/dist/chart-pie.js.map +1 -1
  29. package/dist/chart-stacked-bar.js +401 -0
  30. package/dist/chart-stacked-bar.js.map +1 -0
  31. package/dist/{class-map-DpeNtqCn.js → class-map-59YGWLnx.js} +9 -3
  32. package/dist/class-map-59YGWLnx.js.map +1 -0
  33. package/dist/clock.js +1 -1
  34. package/dist/code-editor.js +7 -7
  35. package/dist/code-editor.js.map +1 -1
  36. package/dist/code-highlighter.js +7 -25
  37. package/dist/code-highlighter.js.map +1 -1
  38. package/dist/custom-elements-jsdocs.json +8824 -5047
  39. package/dist/custom-elements.json +7468 -4147
  40. package/dist/index.js +16 -10
  41. package/dist/index.js.map +1 -1
  42. package/dist/number-counter.js +2 -2
  43. package/dist/{observe-theme-change-BISF-Gl5.js → observe-theme-change-pALI5fmV.js} +2 -2
  44. package/dist/{observe-theme-change-BISF-Gl5.js.map → observe-theme-change-pALI5fmV.js.map} +1 -1
  45. package/dist/peacock-loader.js +42 -1016
  46. package/dist/peacock-loader.js.map +1 -1
  47. package/dist/pie-Dz0IDiPt.js +537 -0
  48. package/dist/pie-Dz0IDiPt.js.map +1 -0
  49. package/dist/{slider-Dk9CFWTG.js → snackbar-74YCdMPL.js} +6205 -3206
  50. package/dist/snackbar-74YCdMPL.js.map +1 -0
  51. package/dist/src/accordion/accordion-item.d.ts +1 -0
  52. package/dist/src/breadcrumb/breadcrumb/breadcrumb.d.ts +2 -0
  53. package/dist/src/breadcrumb/breadcrumb-item/breadcrumb-item.d.ts +1 -0
  54. package/dist/src/button/button-group/button-group.d.ts +4 -0
  55. package/dist/src/card/card.d.ts +27 -0
  56. package/dist/src/card/index.d.ts +1 -0
  57. package/dist/src/chart-bar/chart-bar.d.ts +53 -0
  58. package/dist/src/chart-bar/chart-stacked-bar.d.ts +78 -0
  59. package/dist/src/chart-bar/index.d.ts +2 -0
  60. package/dist/src/code-editor/code-editor.d.ts +4 -3
  61. package/dist/src/code-highlighter/code-highlighter.d.ts +4 -7
  62. package/dist/src/index.d.ts +9 -0
  63. package/dist/src/menu/index.d.ts +3 -0
  64. package/dist/src/menu/menu/MenuSurfaceController.d.ts +18 -0
  65. package/dist/src/menu/menu/menu.d.ts +54 -12
  66. package/dist/src/menu/menu-item/menu-item.d.ts +12 -5
  67. package/dist/src/menu/sub-menu/sub-menu.d.ts +36 -0
  68. package/dist/src/pagination/index.d.ts +1 -0
  69. package/dist/src/pagination/pagination.d.ts +38 -0
  70. package/dist/src/popover/PopoverController.d.ts +4 -1
  71. package/dist/src/snackbar/index.d.ts +1 -0
  72. package/dist/src/snackbar/snackbar.d.ts +40 -0
  73. package/dist/src/table/index.d.ts +1 -0
  74. package/dist/src/table/table.d.ts +110 -0
  75. package/dist/src/tabs/tab-group.d.ts +5 -1
  76. package/dist/src/tabs/tab-panel.d.ts +2 -0
  77. package/dist/src/tabs/tab.d.ts +3 -1
  78. package/dist/src/tabs/tabs.d.ts +2 -0
  79. package/dist/src/tooltip/tooltip.d.ts +1 -3
  80. package/dist/src/tree-view/index.d.ts +2 -0
  81. package/dist/src/tree-view/tree-node.d.ts +69 -0
  82. package/dist/src/tree-view/tree-view.d.ts +40 -0
  83. package/dist/src/tree-view/wc-tree-view.d.ts +6 -0
  84. package/dist/{style-map-CfNHEkQp.js → style-map-DcB52w-l.js} +2 -2
  85. package/dist/{style-map-CfNHEkQp.js.map → style-map-DcB52w-l.js.map} +1 -1
  86. package/dist/test/card.test.d.ts +1 -0
  87. package/dist/test/chart-bar.test.d.ts +1 -0
  88. package/dist/test/icon.test.d.ts +1 -1
  89. package/dist/test/menu.test.d.ts +1 -0
  90. package/dist/test/snackbar.test.d.ts +1 -0
  91. package/dist/test/sub-menu.test.d.ts +1 -0
  92. package/dist/test/tree-view.test.d.ts +1 -0
  93. package/dist/{transform-DRuHEvar.js → transform-DSwFSqzD.js} +13 -558
  94. package/dist/transform-DSwFSqzD.js.map +1 -0
  95. package/dist/tsconfig.tsbuildinfo +1 -1
  96. package/dist/{unsafe-html-CV6Je6HL.js → unsafe-html-C2r3PyzF.js} +2 -2
  97. package/dist/{unsafe-html-CV6Je6HL.js.map → unsafe-html-C2r3PyzF.js.map} +1 -1
  98. package/package.json +1 -1
  99. package/readme.md +40 -40
  100. package/src/accordion/accordion-item.ts +2 -1
  101. package/src/breadcrumb/breadcrumb/breadcrumb.ts +3 -0
  102. package/src/breadcrumb/breadcrumb-item/breadcrumb-item.ts +1 -0
  103. package/src/button/button-group/button-group.ts +6 -0
  104. package/src/card/card.scss +61 -0
  105. package/src/card/card.ts +38 -0
  106. package/src/card/index.ts +1 -0
  107. package/src/chart-bar/chart-bar.scss +58 -0
  108. package/src/chart-bar/chart-bar.ts +306 -0
  109. package/src/chart-bar/chart-stacked-bar.ts +402 -0
  110. package/src/chart-bar/index.ts +2 -0
  111. package/src/code-editor/code-editor.ts +4 -3
  112. package/src/code-highlighter/code-highlighter.ts +4 -22
  113. package/src/divider/divider.scss +2 -2
  114. package/src/empty-state/empty-state.scss +1 -1
  115. package/src/empty-state/empty-state.ts +1 -1
  116. package/src/index.ts +11 -2
  117. package/src/menu/index.ts +3 -0
  118. package/src/menu/menu/MenuSurfaceController.ts +61 -0
  119. package/src/menu/{menu-list/menu-list.scss → menu/menu.scss} +19 -4
  120. package/src/menu/menu/menu.ts +389 -81
  121. package/src/menu/menu-item/menu-item.ts +115 -36
  122. package/src/menu/sub-menu/sub-menu.scss +7 -0
  123. package/src/menu/sub-menu/sub-menu.ts +243 -0
  124. package/src/pagination/index.ts +1 -0
  125. package/src/pagination/pagination.scss +59 -0
  126. package/src/pagination/pagination.ts +135 -0
  127. package/src/peacock-loader.ts +39 -11
  128. package/src/popover/PopoverController.ts +13 -7
  129. package/src/snackbar/demo/index.html +29 -0
  130. package/src/snackbar/index.ts +1 -0
  131. package/src/snackbar/snackbar.scss +73 -0
  132. package/src/snackbar/snackbar.ts +151 -0
  133. package/src/table/index.ts +1 -0
  134. package/src/table/table.scss +174 -0
  135. package/src/table/table.ts +475 -0
  136. package/src/tabs/tab-group.ts +63 -28
  137. package/src/tabs/tab-panel.scss +3 -3
  138. package/src/tabs/tab-panel.ts +3 -0
  139. package/src/tabs/tab.scss +76 -2
  140. package/src/tabs/tab.ts +29 -6
  141. package/src/tabs/tabs.scss +6 -5
  142. package/src/tabs/tabs.ts +19 -5
  143. package/src/text/text.css-component.scss +6 -3
  144. package/src/tooltip/tooltip.scss +16 -13
  145. package/src/tooltip/tooltip.ts +7 -9
  146. package/src/tree-view/demo/index.html +57 -0
  147. package/src/tree-view/index.ts +2 -0
  148. package/src/tree-view/tree-node.scss +101 -0
  149. package/src/tree-view/tree-node.ts +268 -0
  150. package/src/tree-view/tree-view.scss +12 -0
  151. package/src/tree-view/tree-view.ts +182 -0
  152. package/src/tree-view/wc-tree-view.ts +9 -0
  153. package/dist/class-map-DpeNtqCn.js.map +0 -1
  154. package/dist/slider-Dk9CFWTG.js.map +0 -1
  155. package/dist/src/menu/menu-list/menu-list.d.ts +0 -22
  156. package/dist/state-8v48Exzh.js +0 -10
  157. package/dist/state-8v48Exzh.js.map +0 -1
  158. package/dist/transform-DRuHEvar.js.map +0 -1
  159. package/src/menu/menu-list/menu-list.ts +0 -48
@@ -0,0 +1,306 @@
1
+ import { html, LitElement, PropertyValues } from 'lit';
2
+ import { property, query } from 'lit/decorators.js';
3
+ import { styleMap } from 'lit/directives/style-map.js';
4
+ import IndividualComponent from 'src/IndividualComponent.js';
5
+ import * as d3 from 'd3';
6
+ import styles from './chart-bar.scss';
7
+
8
+ export type ChartBarItem = {
9
+ name: string;
10
+ value: number;
11
+ label?: string;
12
+ color?: string;
13
+ };
14
+
15
+ const chartColors: string[] = [];
16
+ ['purple', 'blue', 'red', 'green', 'yellow', 'orange'].forEach(colorName => {
17
+ chartColors.push(`var(--color-${colorName})`);
18
+ });
19
+
20
+ const DEFAULT_WIDTH = 480;
21
+ const DEFAULT_HEIGHT = 320;
22
+ const BAR_RADIUS = 10;
23
+ const DURATION = 450;
24
+
25
+ function debounce<T extends (...args: any[]) => void>(fn: T, wait: number): T {
26
+ let timer: ReturnType<typeof setTimeout>;
27
+ return ((...args: any[]) => {
28
+ clearTimeout(timer);
29
+ timer = setTimeout(() => fn(...args), wait);
30
+ }) as T;
31
+ }
32
+
33
+ /**
34
+ * @label Chart Bar
35
+ * @tag wc-chart-bar
36
+ * @rawTag chart-bar
37
+ * @summary A vertical bar chart that follows Material Design 3 color and spacing tokens.
38
+ * @tags charts
39
+ *
40
+ * @example
41
+ * ```html
42
+ * <wc-chart-bar width="520" height="320"></wc-chart-bar>
43
+ * <script>
44
+ * document.querySelector('wc-chart-bar').data = [
45
+ * { name: 'apples', label: 'Apples', value: 20 },
46
+ * { name: 'bananas', label: 'Bananas', value: 35 },
47
+ * { name: 'cherries', label: 'Cherries', value: 15 },
48
+ * ];
49
+ * </script>
50
+ * ```
51
+ */
52
+ @IndividualComponent
53
+ export class ChartBar extends LitElement {
54
+ static styles = [styles];
55
+
56
+ @query('svg')
57
+ private svgElement?: SVGElement;
58
+
59
+ /** Width of the chart in pixels. */
60
+ @property({ type: Number, reflect: true }) width: number = 0;
61
+
62
+ /** Height of the chart in pixels. */
63
+ @property({ type: Number, reflect: true }) height: number = DEFAULT_HEIGHT;
64
+
65
+ /** Margin around the chart drawing area. */
66
+ @property({ type: Number }) margin: number = 24;
67
+
68
+ /** Chart data array. Each item should have name, value, and optional label and color. */
69
+ @property({ type: Array }) data: ChartBarItem[] = [];
70
+
71
+ /** Whether to render value labels above bars. */
72
+ @property({ type: Boolean, attribute: 'show-values' }) showValues: boolean = true;
73
+
74
+ private _initialized = false;
75
+
76
+ private _debouncedRenderChart = debounce(() => {
77
+ this._renderChart(true);
78
+ }, 200);
79
+
80
+ firstUpdated() {
81
+ this._renderChart(false);
82
+ }
83
+
84
+ updated(changedProperties: PropertyValues) {
85
+ if (!this._initialized) {
86
+ this._initialized = true;
87
+ return;
88
+ }
89
+ const watchedProps = ['width', 'height', 'margin', 'data', 'showValues'];
90
+ const hasChanged = watchedProps.some(prop => changedProperties.has(prop));
91
+ if (hasChanged) {
92
+ this._debouncedRenderChart();
93
+ }
94
+ }
95
+
96
+ private _getPaletteScale() {
97
+ return d3
98
+ .scaleOrdinal<string, string>()
99
+ .domain(this.data.map(d => d.name))
100
+ .range(chartColors);
101
+ }
102
+
103
+ private _resolveColor(
104
+ name: string,
105
+ override: string | undefined,
106
+ scale: d3.ScaleOrdinal<string, string>,
107
+ ) {
108
+ return override || scale(name);
109
+ }
110
+
111
+ private _renderChart(animate: boolean) {
112
+ if (!this.svgElement) return;
113
+
114
+ const width = this.width > 0 ? this.width : DEFAULT_WIDTH;
115
+ const height = this.height > 0 ? this.height : DEFAULT_HEIGHT;
116
+ const margin = Math.max(this.margin, 12);
117
+ const data = this.data ?? [];
118
+
119
+ const svg = d3.select(this.svgElement);
120
+ svg.attr('width', width).attr('height', height);
121
+
122
+ const innerWidth = Math.max(width - margin * 2, 0);
123
+ const innerHeight = Math.max(height - margin * 2, 0);
124
+ const colorScale = this._getPaletteScale();
125
+
126
+ const container = svg.select<SVGGElement>('.chart-container');
127
+ container.attr('transform', `translate(${margin},${margin})`);
128
+
129
+ if (!data.length || innerWidth === 0 || innerHeight === 0) {
130
+ container.select('.bars').selectAll('*').remove();
131
+ container.select('.x-axis').selectAll('*').remove();
132
+ container.select('.y-grid').selectAll('*').remove();
133
+ container.select('.value-labels').selectAll('*').remove();
134
+ return;
135
+ }
136
+
137
+ const xScale = d3
138
+ .scaleBand<string>()
139
+ .domain(data.map(d => d.name))
140
+ .range([0, innerWidth])
141
+ .padding(0.28);
142
+
143
+ const maxValue = d3.max(data, d => d.value) ?? 0;
144
+ const yScale = d3
145
+ .scaleLinear()
146
+ .domain([0, maxValue || 1])
147
+ .nice()
148
+ .range([innerHeight, 0]);
149
+
150
+ const yGrid = container.select<SVGGElement>('.y-grid');
151
+ yGrid
152
+ .call(
153
+ d3
154
+ .axisLeft(yScale)
155
+ .ticks(5)
156
+ .tickSize(-innerWidth)
157
+ .tickFormat(() => ''),
158
+ )
159
+ .selectAll('.tick text')
160
+ .remove();
161
+ yGrid.select('.domain').remove();
162
+ yGrid.selectAll('.tick line').attr('class', 'gridline');
163
+
164
+ const xAxis = container.select<SVGGElement>('.x-axis');
165
+ xAxis
166
+ .attr('transform', `translate(0,${innerHeight})`)
167
+ .call(
168
+ d3
169
+ .axisBottom(xScale)
170
+ .tickSizeOuter(0)
171
+ .tickFormat(name => {
172
+ const entry = data.find(d => d.name === name);
173
+ return entry?.label ?? name;
174
+ }),
175
+ );
176
+ xAxis.select('.domain').attr('stroke', 'var(--color-outline-variant)');
177
+ xAxis.selectAll('.tick line').remove();
178
+ xAxis
179
+ .selectAll('.tick text')
180
+ .attr('class', 'axis-label')
181
+ .attr('dy', '1.1em');
182
+
183
+ const bars = container
184
+ .select('.bars')
185
+ .selectAll<SVGRectElement, ChartBarItem>('rect')
186
+ .data(data, d => d.name)
187
+ .join(
188
+ enter =>
189
+ enter
190
+ .append('rect')
191
+ .attr('class', 'bar')
192
+ .attr('x', d => xScale(d.name) ?? 0)
193
+ .attr('width', xScale.bandwidth())
194
+ .attr('y', innerHeight)
195
+ .attr('height', 0)
196
+ .attr('rx', BAR_RADIUS)
197
+ .attr('ry', BAR_RADIUS)
198
+ .style('fill', d =>
199
+ this._resolveColor(d.name, d.color, colorScale),
200
+ ),
201
+ update => update,
202
+ exit =>
203
+ exit
204
+ .transition()
205
+ .duration(DURATION)
206
+ .attr('height', 0)
207
+ .attr('y', innerHeight)
208
+ .remove(),
209
+ );
210
+
211
+ bars
212
+ .attr('x', d => xScale(d.name) ?? 0)
213
+ .attr('width', xScale.bandwidth())
214
+ .attr('rx', BAR_RADIUS)
215
+ .attr('ry', BAR_RADIUS)
216
+ .style('fill', d => this._resolveColor(d.name, d.color, colorScale));
217
+
218
+ if (animate) {
219
+ bars
220
+ .transition()
221
+ .duration(DURATION)
222
+ .attr('y', d => yScale(d.value))
223
+ .attr('height', d => innerHeight - yScale(d.value));
224
+ } else {
225
+ bars
226
+ .attr('y', d => yScale(d.value))
227
+ .attr('height', d => innerHeight - yScale(d.value));
228
+ }
229
+
230
+ const valueLabels = container
231
+ .select('.value-labels')
232
+ .selectAll<SVGTextElement, ChartBarItem>('text')
233
+ .data(this.showValues ? data : [], d => d.name)
234
+ .join(
235
+ enter =>
236
+ enter
237
+ .append('text')
238
+ .attr('class', 'value-label')
239
+ .attr('text-anchor', 'middle')
240
+ .attr('x', d => (xScale(d.name) ?? 0) + xScale.bandwidth() / 2)
241
+ .attr('y', innerHeight - 6)
242
+ .text(d => d.value.toLocaleString()),
243
+ update => update,
244
+ exit => exit.remove(),
245
+ );
246
+
247
+ const resolveLabelY = (value: number) => {
248
+ const offset = yScale(value) - 8;
249
+ return Math.min(offset, innerHeight - 8);
250
+ };
251
+
252
+ if (animate) {
253
+ valueLabels
254
+ .transition()
255
+ .duration(DURATION)
256
+ .attr('x', d => (xScale(d.name) ?? 0) + xScale.bandwidth() / 2)
257
+ .attr('y', d => resolveLabelY(d.value))
258
+ .text(d => d.value.toLocaleString());
259
+ } else {
260
+ valueLabels
261
+ .attr('x', d => (xScale(d.name) ?? 0) + xScale.bandwidth() / 2)
262
+ .attr('y', d => resolveLabelY(d.value))
263
+ .text(d => d.value.toLocaleString());
264
+ }
265
+ }
266
+
267
+ render() {
268
+ const paletteScale = this._getPaletteScale();
269
+ const legendItems = this.data.map(item => ({
270
+ name: item.label ?? item.name,
271
+ color: this._resolveColor(item.name, item.color, paletteScale),
272
+ }));
273
+
274
+ return html`
275
+ <div class="chart-frame">
276
+ <svg role="img" aria-label="Bar chart">
277
+ <g class="chart-container">
278
+ <g class="y-grid"></g>
279
+ <g class="bars"></g>
280
+ <g class="x-axis"></g>
281
+ <g class="value-labels"></g>
282
+ </g>
283
+ </svg>
284
+ ${legendItems.length
285
+ ? html`<div class="legend" role="list">
286
+ ${legendItems.map(
287
+ item => html`<span class="legend-item" role="listitem">
288
+ <span
289
+ class="swatch"
290
+ style=${styleMap({ background: item.color })}
291
+ ></span>
292
+ <span>${item.name}</span>
293
+ </span>`,
294
+ )}
295
+ </div>`
296
+ : null}
297
+ </div>
298
+ `;
299
+ }
300
+ }
301
+
302
+ declare global {
303
+ interface HTMLElementTagNameMap {
304
+ 'wc-chart-bar': ChartBar;
305
+ }
306
+ }
@@ -0,0 +1,402 @@
1
+ import { html, LitElement, PropertyValues } from 'lit';
2
+ import { property, query } from 'lit/decorators.js';
3
+ import { styleMap } from 'lit/directives/style-map.js';
4
+ import IndividualComponent from 'src/IndividualComponent.js';
5
+ import * as d3 from 'd3';
6
+ import styles from './chart-bar.scss';
7
+
8
+ export type ChartStackedSegment = {
9
+ name: string;
10
+ value: number;
11
+ label?: string;
12
+ color?: string;
13
+ };
14
+
15
+ export type ChartStackedBarItem = {
16
+ name: string;
17
+ label?: string;
18
+ segments: ChartStackedSegment[];
19
+ };
20
+
21
+ const chartColors: string[] = [];
22
+ ['purple', 'blue', 'red', 'green', 'yellow', 'orange'].forEach(colorName => {
23
+ chartColors.push(`var(--color-${colorName})`);
24
+ });
25
+
26
+ const DEFAULT_WIDTH = 520;
27
+ const DEFAULT_HEIGHT = 360;
28
+ const BAR_RADIUS = 8;
29
+ const DURATION = 450;
30
+
31
+ function debounce<T extends (...args: any[]) => void>(fn: T, wait: number): T {
32
+ let timer: ReturnType<typeof setTimeout>;
33
+ return ((...args: any[]) => {
34
+ clearTimeout(timer);
35
+ timer = setTimeout(() => fn(...args), wait);
36
+ }) as T;
37
+ }
38
+
39
+ /**
40
+ * @label Chart Stacked Bar
41
+ * @tag wc-chart-stacked-bar
42
+ * @rawTag chart-stacked-bar
43
+ * @summary A stacked bar chart that groups series by category using Material Design 3 tokens.
44
+ * @tags charts
45
+ *
46
+ * @example
47
+ * ```html
48
+ * <wc-chart-stacked-bar width="560" height="360"></wc-chart-stacked-bar>
49
+ * <script>
50
+ * document.querySelector('wc-chart-stacked-bar').data = [
51
+ * {
52
+ * name: 'q1',
53
+ * label: 'Q1',
54
+ * segments: [
55
+ * { name: 'mobile', label: 'Mobile', value: 40 },
56
+ * { name: 'web', label: 'Web', value: 25 },
57
+ * { name: 'store', label: 'Store', value: 15 },
58
+ * ],
59
+ * },
60
+ * {
61
+ * name: 'q2',
62
+ * label: 'Q2',
63
+ * segments: [
64
+ * { name: 'mobile', label: 'Mobile', value: 32 },
65
+ * { name: 'web', label: 'Web', value: 30 },
66
+ * { name: 'store', label: 'Store', value: 18 },
67
+ * ],
68
+ * },
69
+ * ];
70
+ * </script>
71
+ * ```
72
+ */
73
+ @IndividualComponent
74
+ export class ChartStackedBar extends LitElement {
75
+ static styles = [styles];
76
+
77
+ @query('svg')
78
+ private svgElement?: SVGElement;
79
+
80
+ /** Width of the chart in pixels. */
81
+ @property({ type: Number, reflect: true }) width: number = 0;
82
+
83
+ /** Height of the chart in pixels. */
84
+ @property({ type: Number, reflect: true }) height: number = DEFAULT_HEIGHT;
85
+
86
+ /** Margin around the chart drawing area. */
87
+ @property({ type: Number }) margin: number = 28;
88
+
89
+ /** Chart data array. Each item holds the stacked segments for a category. */
90
+ @property({ type: Array }) data: ChartStackedBarItem[] = [];
91
+
92
+ /** Whether to render total value labels above each stack. */
93
+ @property({ type: Boolean, attribute: 'show-values' }) showValues: boolean = true;
94
+
95
+ /** Whether to render the legend. */
96
+ @property({ type: Boolean, attribute: 'show-legend' }) showLegend: boolean = true;
97
+
98
+ private _initialized = false;
99
+
100
+ private _debouncedRenderChart = debounce(() => {
101
+ this._renderChart(true);
102
+ }, 200);
103
+
104
+ firstUpdated() {
105
+ this._renderChart(false);
106
+ }
107
+
108
+ updated(changedProperties: PropertyValues) {
109
+ if (!this._initialized) {
110
+ this._initialized = true;
111
+ return;
112
+ }
113
+ const watchedProps = [
114
+ 'width',
115
+ 'height',
116
+ 'margin',
117
+ 'data',
118
+ 'showValues',
119
+ 'showLegend',
120
+ ];
121
+ const hasChanged = watchedProps.some(prop => changedProperties.has(prop));
122
+ if (hasChanged) {
123
+ this._debouncedRenderChart();
124
+ }
125
+ }
126
+
127
+ private _getSegmentKeys() {
128
+ const keys = new Set<string>();
129
+ this.data?.forEach(item => {
130
+ item.segments?.forEach(segment => keys.add(segment.name));
131
+ });
132
+ return Array.from(keys);
133
+ }
134
+
135
+ private _getColorScale(keys: string[]) {
136
+ return d3
137
+ .scaleOrdinal<string, string>()
138
+ .domain(keys)
139
+ .range(chartColors);
140
+ }
141
+
142
+ private _getColorMap(
143
+ keys: string[],
144
+ scale: d3.ScaleOrdinal<string, string>,
145
+ ) {
146
+ const map = new Map<string, string>();
147
+ keys.forEach(key => {
148
+ const override = this.data
149
+ .map(item => item.segments.find(seg => seg.name === key)?.color)
150
+ .find(color => !!color);
151
+ map.set(key, override || scale(key));
152
+ });
153
+ return map;
154
+ }
155
+
156
+ private _getSegmentLabel(key: string) {
157
+ const segment = this.data
158
+ .map(item => item.segments.find(seg => seg.name === key))
159
+ .find(Boolean);
160
+ return segment?.label ?? key;
161
+ }
162
+
163
+ private _getTotals() {
164
+ return this.data.map(item =>
165
+ item.segments.reduce((sum, seg) => sum + seg.value, 0),
166
+ );
167
+ }
168
+
169
+ private _renderChart(animate: boolean) {
170
+ if (!this.svgElement) return;
171
+
172
+ const width = this.width > 0 ? this.width : DEFAULT_WIDTH;
173
+ const height = this.height > 0 ? this.height : DEFAULT_HEIGHT;
174
+ const margin = Math.max(this.margin, 16);
175
+ const data = this.data ?? [];
176
+
177
+ const svg = d3.select(this.svgElement);
178
+ svg.attr('width', width).attr('height', height);
179
+
180
+ const innerWidth = Math.max(width - margin * 2, 0);
181
+ const innerHeight = Math.max(height - margin * 2, 0);
182
+
183
+ const container = svg.select<SVGGElement>('.chart-container');
184
+ container.attr('transform', `translate(${margin},${margin})`);
185
+
186
+ if (!data.length || innerWidth === 0 || innerHeight === 0) {
187
+ container.select('.bars').selectAll('*').remove();
188
+ container.select('.x-axis').selectAll('*').remove();
189
+ container.select('.y-grid').selectAll('*').remove();
190
+ container.select('.value-labels').selectAll('*').remove();
191
+ return;
192
+ }
193
+
194
+ const keys = this._getSegmentKeys();
195
+ const colorScale = this._getColorScale(keys);
196
+ const colorMap = this._getColorMap(keys, colorScale);
197
+
198
+ const totals = this._getTotals();
199
+ const xScale = d3
200
+ .scaleBand<string>()
201
+ .domain(data.map(d => d.name))
202
+ .range([0, innerWidth])
203
+ .padding(0.3);
204
+ const maxValue = d3.max(totals) ?? 0;
205
+ const yScale = d3
206
+ .scaleLinear()
207
+ .domain([0, maxValue || 1])
208
+ .nice()
209
+ .range([innerHeight, 0]);
210
+
211
+ const stackedSeries = d3
212
+ .stack<ChartStackedBarItem, string>()
213
+ .keys(keys)
214
+ .value(
215
+ (d, key) => d.segments.find(segment => segment.name === key)?.value ?? 0,
216
+ )(data);
217
+
218
+ const yGrid = container.select<SVGGElement>('.y-grid');
219
+ yGrid
220
+ .call(
221
+ d3
222
+ .axisLeft(yScale)
223
+ .ticks(5)
224
+ .tickSize(-innerWidth)
225
+ .tickFormat(() => ''),
226
+ )
227
+ .selectAll('.tick text')
228
+ .remove();
229
+ yGrid.select('.domain').remove();
230
+ yGrid.selectAll('.tick line').attr('class', 'gridline');
231
+
232
+ const xAxis = container.select<SVGGElement>('.x-axis');
233
+ xAxis
234
+ .attr('transform', `translate(0,${innerHeight})`)
235
+ .call(
236
+ d3
237
+ .axisBottom(xScale)
238
+ .tickSizeOuter(0)
239
+ .tickFormat(name => {
240
+ const entry = data.find(d => d.name === name);
241
+ return entry?.label ?? name;
242
+ }),
243
+ );
244
+ xAxis.select('.domain').attr('stroke', 'var(--color-outline-variant)');
245
+ xAxis.selectAll('.tick line').remove();
246
+ xAxis
247
+ .selectAll('.tick text')
248
+ .attr('class', 'axis-label')
249
+ .attr('dy', '1.1em');
250
+
251
+ const barGroups = container
252
+ .select('.bars')
253
+ .selectAll<SVGGElement, d3.Series<ChartStackedBarItem, string>>(
254
+ '.bar-group',
255
+ )
256
+ .data(stackedSeries, series => series.key)
257
+ .join(
258
+ enter =>
259
+ enter
260
+ .append('g')
261
+ .attr('class', 'bar-group')
262
+ .style('fill', series => colorMap.get(series.key) ?? ''),
263
+ update => update,
264
+ exit => exit.remove(),
265
+ )
266
+ .style('fill', series => colorMap.get(series.key) ?? colorScale(series.key));
267
+
268
+ const segmentJoin = barGroups
269
+ .selectAll<SVGRectElement, d3.SeriesPoint<ChartStackedBarItem>>('rect')
270
+ .data(series => series, d => d.data.name)
271
+ .join(
272
+ enter =>
273
+ enter
274
+ .append('rect')
275
+ .attr('class', 'stacked-segment')
276
+ .attr('x', d => xScale(d.data.name) ?? 0)
277
+ .attr('width', xScale.bandwidth())
278
+ .attr('y', innerHeight)
279
+ .attr('height', 0)
280
+ .attr('rx', BAR_RADIUS)
281
+ .attr('ry', BAR_RADIUS),
282
+ update => update,
283
+ exit =>
284
+ exit
285
+ .transition()
286
+ .duration(DURATION)
287
+ .attr('y', innerHeight)
288
+ .attr('height', 0)
289
+ .remove(),
290
+ );
291
+
292
+ segmentJoin
293
+ .attr('x', d => xScale(d.data.name) ?? 0)
294
+ .attr('width', xScale.bandwidth())
295
+ .attr('rx', BAR_RADIUS)
296
+ .attr('ry', BAR_RADIUS);
297
+
298
+ if (animate) {
299
+ segmentJoin
300
+ .transition()
301
+ .duration(DURATION)
302
+ .attr('y', d => yScale(d[1]))
303
+ .attr('height', d => yScale(d[0]) - yScale(d[1]));
304
+ } else {
305
+ segmentJoin
306
+ .attr('y', d => yScale(d[1]))
307
+ .attr('height', d => yScale(d[0]) - yScale(d[1]));
308
+ }
309
+
310
+ const totalLabels = container
311
+ .select('.value-labels')
312
+ .selectAll<SVGTextElement, ChartStackedBarItem>('text')
313
+ .data(this.showValues ? data : [], d => d.name)
314
+ .join(
315
+ enter =>
316
+ enter
317
+ .append('text')
318
+ .attr('class', 'value-label')
319
+ .attr('text-anchor', 'middle')
320
+ .attr('x', d => (xScale(d.name) ?? 0) + xScale.bandwidth() / 2)
321
+ .attr('y', innerHeight - 6)
322
+ .text(d =>
323
+ d.segments
324
+ .reduce((sum, seg) => sum + seg.value, 0)
325
+ .toLocaleString(),
326
+ ),
327
+ update => update,
328
+ exit => exit.remove(),
329
+ );
330
+
331
+ const labelPosition = (item: ChartStackedBarItem) => {
332
+ const total = item.segments.reduce((sum, seg) => sum + seg.value, 0);
333
+ const offset = yScale(total) - 8;
334
+ return Math.min(offset, innerHeight - 8);
335
+ };
336
+
337
+ if (animate) {
338
+ totalLabels
339
+ .transition()
340
+ .duration(DURATION)
341
+ .attr('x', d => (xScale(d.name) ?? 0) + xScale.bandwidth() / 2)
342
+ .attr('y', d => labelPosition(d))
343
+ .text(d =>
344
+ d.segments
345
+ .reduce((sum, seg) => sum + seg.value, 0)
346
+ .toLocaleString(),
347
+ );
348
+ } else {
349
+ totalLabels
350
+ .attr('x', d => (xScale(d.name) ?? 0) + xScale.bandwidth() / 2)
351
+ .attr('y', d => labelPosition(d))
352
+ .text(d =>
353
+ d.segments
354
+ .reduce((sum, seg) => sum + seg.value, 0)
355
+ .toLocaleString(),
356
+ );
357
+ }
358
+ }
359
+
360
+ render() {
361
+ const keys = this._getSegmentKeys();
362
+ const colorScale = this._getColorScale(keys);
363
+ const colorMap = this._getColorMap(keys, colorScale);
364
+
365
+ const legendItems = keys.map(key => ({
366
+ name: this._getSegmentLabel(key),
367
+ color: colorMap.get(key) ?? colorScale(key),
368
+ }));
369
+
370
+ return html`
371
+ <div class="chart-frame">
372
+ <svg role="img" aria-label="Stacked bar chart">
373
+ <g class="chart-container">
374
+ <g class="y-grid"></g>
375
+ <g class="bars"></g>
376
+ <g class="x-axis"></g>
377
+ <g class="value-labels"></g>
378
+ </g>
379
+ </svg>
380
+ ${this.showLegend && legendItems.length
381
+ ? html`<div class="legend" role="list">
382
+ ${legendItems.map(
383
+ item => html`<span class="legend-item" role="listitem">
384
+ <span
385
+ class="swatch"
386
+ style=${styleMap({ background: item.color })}
387
+ ></span>
388
+ <span>${item.name}</span>
389
+ </span>`,
390
+ )}
391
+ </div>`
392
+ : null}
393
+ </div>
394
+ `;
395
+ }
396
+ }
397
+
398
+ declare global {
399
+ interface HTMLElementTagNameMap {
400
+ 'wc-chart-stacked-bar': ChartStackedBar;
401
+ }
402
+ }
@@ -0,0 +1,2 @@
1
+ export { ChartBar } from './chart-bar.js';
2
+ export { ChartStackedBar } from './chart-stacked-bar.js';