@redvars/peacock 3.2.9 → 3.3.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 (201) hide show
  1. package/dist/{PeacockComponent-CxJc63xj.js → IndividualComponent-tDnXrOLV.js} +3 -3
  2. package/dist/IndividualComponent-tDnXrOLV.js.map +1 -0
  3. package/dist/assets/tokens.css +2 -0
  4. package/dist/assets/tokens.css.map +1 -0
  5. package/dist/{button-2MeDq0Yy.js → button-BGFJfbT2.js} +23 -36
  6. package/dist/button-BGFJfbT2.js.map +1 -0
  7. package/dist/button-group.js +8 -8
  8. package/dist/button-group.js.map +1 -1
  9. package/dist/button.js +6 -5
  10. package/dist/button.js.map +1 -1
  11. package/dist/chart-donut.js +307 -0
  12. package/dist/chart-donut.js.map +1 -0
  13. package/dist/chart-doughnut.js +307 -0
  14. package/dist/chart-doughnut.js.map +1 -0
  15. package/dist/chart-pie.js +259 -0
  16. package/dist/chart-pie.js.map +1 -0
  17. package/dist/{class-map-BvQRv7eW.js → class-map-DpeNtqCn.js} +2 -2
  18. package/dist/{class-map-BvQRv7eW.js.map → class-map-DpeNtqCn.js.map} +1 -1
  19. package/dist/clock.js +5 -6
  20. package/dist/clock.js.map +1 -1
  21. package/dist/code-editor.js +37 -24
  22. package/dist/code-editor.js.map +1 -1
  23. package/dist/code-highlighter.js +21 -7
  24. package/dist/code-highlighter.js.map +1 -1
  25. package/dist/custom-elements-jsdocs.json +5377 -3122
  26. package/dist/custom-elements.json +5527 -3940
  27. package/dist/{dispatch-event-utils-vbdiOSeC.js → dispatch-event-utils-B4odODQf.js} +2 -15
  28. package/dist/dispatch-event-utils-B4odODQf.js.map +1 -0
  29. package/dist/index.js +13 -9
  30. package/dist/index.js.map +1 -1
  31. package/dist/number-counter.js +12 -10
  32. package/dist/number-counter.js.map +1 -1
  33. package/dist/{observe-theme-change-NneLARW8.js → observe-theme-change-BISF-Gl5.js} +2 -2
  34. package/dist/{observe-theme-change-NneLARW8.js.map → observe-theme-change-BISF-Gl5.js.map} +1 -1
  35. package/dist/peacock-loader.js +124 -62
  36. package/dist/peacock-loader.js.map +1 -1
  37. package/dist/query-QBcUV-L_.js +15 -0
  38. package/dist/query-QBcUV-L_.js.map +1 -0
  39. package/dist/{image-DK6VQW7N.js → slider-Dk9CFWTG.js} +1616 -331
  40. package/dist/slider-Dk9CFWTG.js.map +1 -0
  41. package/dist/src/IndividualComponent.d.ts +1 -0
  42. package/dist/src/accordion/{accordion-item/accordion-item.d.ts → accordion-item.d.ts} +4 -4
  43. package/dist/src/accordion/{accordion/accordion.d.ts → accordion.d.ts} +6 -6
  44. package/dist/src/accordion/{accordion-item/index.d.ts → index.d.ts} +1 -0
  45. package/dist/src/avatar/avatar.d.ts +2 -2
  46. package/dist/src/badge/badge.d.ts +2 -2
  47. package/dist/src/breadcrumb/breadcrumb/breadcrumb.d.ts +7 -8
  48. package/dist/src/breadcrumb/breadcrumb-item/breadcrumb-item.d.ts +3 -3
  49. package/dist/src/button/button/button.d.ts +2 -2
  50. package/dist/src/button/button-group/button-group.d.ts +5 -5
  51. package/dist/src/button/icon-button/icon-button.d.ts +2 -2
  52. package/dist/src/chart-donut/chart-donut.d.ts +53 -0
  53. package/dist/src/chart-donut/index.d.ts +1 -0
  54. package/dist/src/chart-doughnut/chart-doughnut.d.ts +53 -0
  55. package/dist/src/chart-doughnut/index.d.ts +1 -0
  56. package/dist/src/chart-pie/chart-pie.d.ts +50 -0
  57. package/dist/src/chart-pie/index.d.ts +1 -0
  58. package/dist/src/checkbox/checkbox.d.ts +3 -6
  59. package/dist/src/chip/chip/chip.d.ts +4 -4
  60. package/dist/src/chip/tag/tag.d.ts +3 -3
  61. package/dist/src/clock/clock.d.ts +3 -4
  62. package/dist/src/code-editor/code-editor.d.ts +11 -9
  63. package/dist/src/container/container.d.ts +6 -11
  64. package/dist/src/date-picker/date-picker.d.ts +3 -3
  65. package/dist/src/divider/divider.d.ts +2 -2
  66. package/dist/src/elevation/elevation.d.ts +2 -2
  67. package/dist/src/empty-state/empty-state.d.ts +9 -2
  68. package/dist/src/field/field.d.ts +17 -0
  69. package/dist/src/focus-ring/focus-ring.d.ts +1 -1
  70. package/dist/src/icon/icon.d.ts +2 -2
  71. package/dist/src/image/image.d.ts +4 -12
  72. package/dist/src/index.d.ts +5 -1
  73. package/dist/src/input/input.d.ts +2 -2
  74. package/dist/src/link/link.d.ts +4 -5
  75. package/dist/src/menu/menu/menu.d.ts +16 -0
  76. package/dist/src/menu/menu-item/menu-item.d.ts +12 -0
  77. package/dist/src/menu/menu-list/menu-list.d.ts +15 -0
  78. package/dist/src/number-counter/number-counter.d.ts +9 -7
  79. package/dist/src/number-field/number-field.d.ts +1 -1
  80. package/dist/src/popover/index.d.ts +1 -1
  81. package/dist/src/progress/circular-progress/circular-progress.d.ts +3 -3
  82. package/dist/src/progress/linear-progress/linear-progress.d.ts +3 -3
  83. package/dist/src/ripple/ripple.d.ts +60 -4
  84. package/dist/src/skeleton/skeleton.d.ts +6 -5
  85. package/dist/src/slider/index.d.ts +1 -0
  86. package/dist/src/slider/slider.d.ts +52 -0
  87. package/dist/src/spinner/spinner.d.ts +2 -2
  88. package/dist/src/switch/switch.d.ts +2 -2
  89. package/dist/src/tabs/index.d.ts +4 -0
  90. package/dist/src/tabs/tab-group.d.ts +41 -0
  91. package/dist/src/tabs/tab-panel.d.ts +21 -0
  92. package/dist/src/tabs/tab.d.ts +58 -0
  93. package/dist/src/tabs/tabs.d.ts +27 -0
  94. package/dist/src/textarea/textarea.d.ts +3 -3
  95. package/dist/src/time-picker/time-picker.d.ts +3 -3
  96. package/dist/src/{popover/tooltip → tooltip}/tooltip.d.ts +6 -3
  97. package/dist/{state-B09bP3XH.js → state-8v48Exzh.js} +2 -2
  98. package/dist/{state-B09bP3XH.js.map → state-8v48Exzh.js.map} +1 -1
  99. package/dist/{style-map-B8xgVEc9.js → style-map-CfNHEkQp.js} +2 -2
  100. package/dist/{style-map-B8xgVEc9.js.map → style-map-CfNHEkQp.js.map} +1 -1
  101. package/dist/transform-DRuHEvar.js +3312 -0
  102. package/dist/transform-DRuHEvar.js.map +1 -0
  103. package/dist/tsconfig.tsbuildinfo +1 -1
  104. package/dist/{unsafe-html-B-dV3Jps.js → unsafe-html-CV6Je6HL.js} +2 -2
  105. package/dist/{unsafe-html-B-dV3Jps.js.map → unsafe-html-CV6Je6HL.js.map} +1 -1
  106. package/package.json +3 -1
  107. package/readme.md +2 -2
  108. package/scss/tokens.scss +1 -0
  109. package/src/{PeacockComponent.ts → IndividualComponent.ts} +1 -1
  110. package/src/accordion/{accordion-item/accordion-item.scss → accordion-item.scss} +1 -1
  111. package/src/accordion/{accordion-item/accordion-item.ts → accordion-item.ts} +5 -5
  112. package/src/accordion/{accordion/accordion.scss → accordion.scss} +2 -1
  113. package/src/accordion/{accordion/accordion.ts → accordion.ts} +6 -6
  114. package/src/accordion/{accordion-item/index.ts → index.ts} +2 -0
  115. package/src/avatar/avatar.ts +2 -2
  116. package/src/badge/badge.ts +2 -2
  117. package/src/breadcrumb/breadcrumb/breadcrumb.ts +7 -8
  118. package/src/breadcrumb/breadcrumb-item/breadcrumb-item.ts +3 -3
  119. package/src/button/BaseButton.ts +1 -1
  120. package/src/button/button/button.scss +10 -24
  121. package/src/button/button/button.ts +8 -8
  122. package/src/button/button-group/button-group.ts +7 -7
  123. package/src/button/icon-button/icon-button.ts +8 -8
  124. package/src/chart-donut/chart-donut.scss +37 -0
  125. package/src/chart-donut/chart-donut.ts +287 -0
  126. package/src/chart-donut/demo/index.html +51 -0
  127. package/src/chart-donut/index.ts +1 -0
  128. package/src/chart-doughnut/chart-donut.scss +37 -0
  129. package/src/chart-doughnut/chart-doughnut.ts +287 -0
  130. package/src/chart-doughnut/demo/index.html +51 -0
  131. package/src/chart-doughnut/index.ts +1 -0
  132. package/src/chart-pie/chart-pie.scss +27 -0
  133. package/src/chart-pie/chart-pie.ts +256 -0
  134. package/src/chart-pie/demo/index.html +51 -0
  135. package/src/chart-pie/index.ts +1 -0
  136. package/src/checkbox/checkbox.ts +3 -6
  137. package/src/chip/chip/chip.ts +6 -6
  138. package/src/chip/tag/tag.ts +6 -6
  139. package/src/clock/clock.ts +5 -6
  140. package/src/code-editor/code-editor.scss +3 -5
  141. package/src/code-editor/code-editor.ts +30 -15
  142. package/src/code-highlighter/code-highlighter.ts +19 -4
  143. package/src/container/container.ts +6 -11
  144. package/src/date-picker/date-picker.ts +7 -7
  145. package/src/divider/divider.ts +2 -2
  146. package/src/elevation/elevation.ts +2 -2
  147. package/src/empty-state/empty-state.ts +10 -3
  148. package/src/field/field.scss +4 -4
  149. package/src/field/field.ts +19 -2
  150. package/src/focus-ring/focus-ring.scss +2 -1
  151. package/src/focus-ring/focus-ring.ts +1 -1
  152. package/src/icon/icon.ts +2 -2
  153. package/src/icon/p-icon.ts +1 -1
  154. package/src/image/image.scss +55 -48
  155. package/src/image/image.ts +4 -12
  156. package/src/index.ts +6 -2
  157. package/src/input/input.ts +6 -6
  158. package/src/link/link.ts +4 -5
  159. package/src/menu/menu/menu.ts +16 -0
  160. package/src/menu/menu-item/menu-item-colors.scss +2 -2
  161. package/src/menu/menu-item/menu-item.ts +14 -2
  162. package/src/menu/menu-list/menu-list.ts +16 -1
  163. package/src/number-counter/demo/index.html +1 -1
  164. package/src/number-counter/number-counter.ts +11 -9
  165. package/src/number-field/number-field.ts +7 -7
  166. package/src/peacock-loader.ts +71 -44
  167. package/src/popover/index.ts +1 -1
  168. package/src/progress/circular-progress/circular-progress.scss +1 -1
  169. package/src/progress/circular-progress/circular-progress.ts +3 -3
  170. package/src/progress/linear-progress/linear-progress.ts +3 -3
  171. package/src/ripple/ripple.ts +478 -94
  172. package/src/skeleton/skeleton.ts +6 -5
  173. package/src/slider/index.ts +1 -0
  174. package/src/slider/slider.scss +130 -0
  175. package/src/slider/slider.ts +178 -0
  176. package/src/spinner/spinner.ts +2 -2
  177. package/src/switch/switch.ts +4 -4
  178. package/src/tabs/index.ts +4 -0
  179. package/src/tabs/tab-group.scss +10 -0
  180. package/src/tabs/tab-group.ts +137 -0
  181. package/src/tabs/tab-panel.scss +12 -0
  182. package/src/tabs/tab-panel.ts +28 -0
  183. package/src/tabs/tab.scss +157 -0
  184. package/src/tabs/tab.ts +242 -0
  185. package/src/tabs/tabs.scss +18 -0
  186. package/src/tabs/tabs.ts +64 -0
  187. package/src/textarea/textarea.ts +5 -5
  188. package/src/time-picker/time-picker.ts +7 -7
  189. package/src/{popover/tooltip → tooltip}/tooltip.scss +1 -1
  190. package/src/{popover/tooltip → tooltip}/tooltip.ts +10 -6
  191. package/dist/PeacockComponent-CxJc63xj.js.map +0 -1
  192. package/dist/button-2MeDq0Yy.js.map +0 -1
  193. package/dist/dispatch-event-utils-vbdiOSeC.js.map +0 -1
  194. package/dist/image-DK6VQW7N.js.map +0 -1
  195. package/dist/src/PeacockComponent.d.ts +0 -1
  196. package/dist/src/accordion/accordion/index.d.ts +0 -1
  197. package/dist/src/avatar/p-avatar.d.ts +0 -3
  198. package/dist/src/badge/p-badge.d.ts +0 -3
  199. package/src/accordion/accordion/index.ts +0 -1
  200. package/src/avatar/p-avatar.ts +0 -5
  201. package/src/badge/p-badge.ts +0 -5
@@ -0,0 +1,287 @@
1
+ import { html, LitElement, PropertyValues } from 'lit';
2
+ import { property, query } from 'lit/decorators.js';
3
+ import IndividualComponent from 'src/IndividualComponent.js';
4
+ import * as d3 from 'd3';
5
+ import styles from './chart-donut.scss';
6
+
7
+ export type ChartDonutColor = {
8
+ color: string;
9
+ };
10
+
11
+ export type ChartDonutItem = {
12
+ name: string;
13
+ value: number;
14
+ label?: string;
15
+ color?: string;
16
+ };
17
+
18
+ const chartColors: ChartDonutColor[] = [];
19
+ ['purple', 'blue', 'red', 'green', 'yellow', 'orange'].forEach(colorName => {
20
+ chartColors.push({
21
+ color: `var(--color-${colorName})`,
22
+ });
23
+ });
24
+
25
+ /** SVGPathElement augmented with the last rendered arc datum for smooth tween interpolation. */
26
+ interface ArcPathElement extends SVGPathElement {
27
+ _prevDatum?: d3.PieArcDatum<ChartDonutItem>;
28
+ }
29
+
30
+ function debounce<T extends (...args: any[]) => void>(fn: T, wait: number): T {
31
+ let timer: ReturnType<typeof setTimeout>;
32
+ return ((...args: any[]) => {
33
+ clearTimeout(timer);
34
+ timer = setTimeout(() => fn(...args), wait);
35
+ }) as T;
36
+ }
37
+
38
+ /**
39
+ * @label Chart Donut
40
+ * @tag wc-chart-donut
41
+ * @rawTag chart-donut
42
+ * @summary A donut chart is a circular chart with a blank center. The area in the center can be used to display information.
43
+ * @tags charts
44
+ *
45
+ * @example
46
+ * ```html
47
+ * <wc-chart-donut width="400" label="Total"></wc-chart-donut>
48
+ * <script>
49
+ * document.querySelector('wc-chart-donut').data = [
50
+ * { name: 'A', value: 30, label: 'Category A' },
51
+ * { name: 'B', value: 50, label: 'Category B' },
52
+ * { name: 'C', value: 20, label: 'Category C' },
53
+ * ];
54
+ * </script>
55
+ * ```
56
+ */
57
+ @IndividualComponent
58
+ export class ChartDonut extends LitElement {
59
+ static styles = [styles];
60
+
61
+ @query('svg')
62
+ private svgElement?: SVGElement;
63
+
64
+ /** Width (and height) of the chart in pixels. */
65
+ @property({ type: Number, reflect: true }) width: number = 0;
66
+
67
+ /** Margin around the chart. */
68
+ @property({ type: Number, reflect: true }) margin: number = 10;
69
+
70
+ /** Whether to show labels outside the chart. */
71
+ @property({ type: Boolean, reflect: true, attribute: 'show-labels' })
72
+ showLabels: boolean = true;
73
+
74
+ /** Chart data array. Each item should have name, value, and optional label and color. */
75
+ @property({ type: Array }) data: ChartDonutItem[] = [];
76
+
77
+ /** Label displayed in the center of the donut. */
78
+ @property({ type: String }) label?: string;
79
+
80
+ private _initialized = false;
81
+
82
+ private _debouncedRenderChart = debounce(() => {
83
+ this._renderChart(true);
84
+ }, 300);
85
+
86
+ firstUpdated() {
87
+ this._renderChart(false);
88
+ }
89
+
90
+ updated(changedProperties: PropertyValues) {
91
+ if (!this._initialized) {
92
+ this._initialized = true;
93
+ return;
94
+ }
95
+ const watchedProps = ['width', 'margin', 'showLabels', 'data'];
96
+ const hasChanged = watchedProps.some(prop => changedProperties.has(prop));
97
+ if (hasChanged) {
98
+ this._debouncedRenderChart();
99
+ }
100
+ }
101
+
102
+ private _getRadius(): number {
103
+ return this.width / 2 - this.margin - 100;
104
+ }
105
+
106
+ private _getTotal(): number {
107
+ return this.data.reduce((total, d) => total + d.value, 0);
108
+ }
109
+
110
+ private _getPieData() {
111
+ const pie = d3
112
+ .pie<ChartDonutItem>()
113
+ .sort(null)
114
+ .value(d => d.value);
115
+ return pie(this.data);
116
+ }
117
+
118
+ private _getColorScale() {
119
+ return d3
120
+ .scaleOrdinal<string, ChartDonutColor>()
121
+ .domain(this.data.map(d => d.name))
122
+ .range(chartColors);
123
+ }
124
+
125
+ private _renderChart(animate: boolean) {
126
+ if (!this.svgElement) return;
127
+
128
+ const DURATION = 500;
129
+ const radius = this._getRadius();
130
+ const pieData = this._getPieData();
131
+ const colorScale = this._getColorScale();
132
+ const total = this._getTotal();
133
+
134
+ const svg = d3.select(this.svgElement);
135
+
136
+ const doughnutArc = d3
137
+ .arc<d3.PieArcDatum<ChartDonutItem>>()
138
+ .innerRadius(radius * 0.72)
139
+ .outerRadius(radius);
140
+
141
+ const labelsArc = d3
142
+ .arc<d3.PieArcDatum<ChartDonutItem>>()
143
+ .innerRadius(radius + 10)
144
+ .outerRadius(radius + 10);
145
+
146
+ // Update SVG dimensions and center transform
147
+ svg.attr('width', this.width).attr('height', this.width);
148
+ svg
149
+ .select('.chart-container')
150
+ .attr('transform', `translate(${this.width / 2},${this.width / 2})`);
151
+
152
+ // Arc paths — keyed by name so D3 matches elements across updates
153
+ const $paths = svg
154
+ .select('.arc-container')
155
+ .selectAll<SVGPathElement, d3.PieArcDatum<ChartDonutItem>>('.arc')
156
+ .data(pieData, d => d.data.name)
157
+ .join('path')
158
+ .attr('class', 'arc')
159
+ .style('fill', d => d.data.color || colorScale(d.data.name).color);
160
+
161
+ if (animate) {
162
+ $paths
163
+ .transition()
164
+ .duration(DURATION)
165
+ .ease(d3.easeCubicInOut)
166
+ .attrTween('d', function (this: SVGPathElement, d) {
167
+ const self = this as ArcPathElement;
168
+ // Interpolate from the last rendered angles to the new ones.
169
+ // New (entering) arcs start collapsed at their startAngle.
170
+ const prev: { startAngle: number; endAngle: number } =
171
+ self._prevDatum ?? {
172
+ startAngle: d.startAngle,
173
+ endAngle: d.startAngle,
174
+ };
175
+ self._prevDatum = d;
176
+ const iStart = d3.interpolateNumber(prev.startAngle, d.startAngle);
177
+ const iEnd = d3.interpolateNumber(prev.endAngle, d.endAngle);
178
+ return (t: number) =>
179
+ doughnutArc({ ...d, startAngle: iStart(t), endAngle: iEnd(t) }) ??
180
+ '';
181
+ });
182
+ } else {
183
+ // Initial render: draw immediately and seed previous-datum for later tweens
184
+ $paths
185
+ .each(function (this: SVGPathElement, d) {
186
+ (this as ArcPathElement)._prevDatum = d;
187
+ })
188
+ .attr('d', d => doughnutArc(d) ?? '');
189
+ }
190
+
191
+ // Animate the central total counter
192
+ const $title = svg.select('.title');
193
+ if (animate) {
194
+ $title
195
+ .transition()
196
+ .duration(DURATION)
197
+ .ease(d3.easeCubicInOut)
198
+ .tween('text', function (this: d3.BaseType) {
199
+ const sel = d3.select(this as SVGTextElement);
200
+ const start = parseFloat(sel.text()) || 0;
201
+ const interp = d3.interpolateNumber(start, total);
202
+ return function (t: number) {
203
+ sel.text(Math.round(interp(t)));
204
+ };
205
+ });
206
+ } else {
207
+ $title.text(total);
208
+ }
209
+
210
+ // Label polylines and text
211
+ const $chartContainer = svg.select('.chart-container');
212
+
213
+ if (this.showLabels) {
214
+ const pointsFn = (d: d3.PieArcDatum<ChartDonutItem>) => {
215
+ const posA = doughnutArc.centroid(d);
216
+ const posB = labelsArc.centroid(d);
217
+ const posC = posB.slice() as [number, number];
218
+ const midAngle = d.startAngle + (d.endAngle - d.startAngle) / 2;
219
+ posC[0] = radius * (midAngle < Math.PI ? 1 : -1);
220
+ return [posA, posB, posC].map(p => p.join(',')).join(' ');
221
+ };
222
+
223
+ const transformFn = (d: d3.PieArcDatum<ChartDonutItem>) => {
224
+ const pos = labelsArc.centroid(d);
225
+ const midAngle = d.startAngle + (d.endAngle - d.startAngle) / 2;
226
+ pos[0] = radius * (midAngle < Math.PI ? 1 : -1);
227
+ return `translate(${pos})`;
228
+ };
229
+
230
+ const anchorFn = (d: d3.PieArcDatum<ChartDonutItem>) => {
231
+ const midAngle = d.startAngle + (d.endAngle - d.startAngle) / 2;
232
+ return midAngle < Math.PI ? 'start' : 'end';
233
+ };
234
+
235
+ const $polylines = $chartContainer
236
+ .selectAll<SVGPolylineElement, d3.PieArcDatum<ChartDonutItem>>(
237
+ '.item-polyline',
238
+ )
239
+ .data(pieData, d => d.data.name)
240
+ .join('polyline')
241
+ .attr('class', 'item-polyline');
242
+
243
+ const $labels = $chartContainer
244
+ .selectAll<SVGTextElement, d3.PieArcDatum<ChartDonutItem>>('.item-label')
245
+ .data(pieData, d => d.data.name)
246
+ .join('text')
247
+ .attr('class', 'item-label')
248
+ .text(d => d.data.label ?? '');
249
+
250
+ if (animate) {
251
+ $polylines
252
+ .transition()
253
+ .duration(DURATION)
254
+ .ease(d3.easeCubicInOut)
255
+ .attr('points', pointsFn);
256
+ $labels
257
+ .transition()
258
+ .duration(DURATION)
259
+ .ease(d3.easeCubicInOut)
260
+ .attr('transform', transformFn)
261
+ .style('text-anchor', anchorFn);
262
+ } else {
263
+ $polylines.attr('points', pointsFn);
264
+ $labels.attr('transform', transformFn).style('text-anchor', anchorFn);
265
+ }
266
+ } else {
267
+ $chartContainer.selectAll('.item-polyline').remove();
268
+ $chartContainer.selectAll('.item-label').remove();
269
+ }
270
+ }
271
+
272
+ render() {
273
+ return html`
274
+ <div class="chart">
275
+ <svg>
276
+ <g class="chart-container">
277
+ <g class="arc-container"></g>
278
+ <text class="title" text-anchor="middle"></text>
279
+ <text class="label" text-anchor="middle" y="16">
280
+ ${this.label}
281
+ </text>
282
+ </g>
283
+ </svg>
284
+ </div>
285
+ `;
286
+ }
287
+ }
@@ -0,0 +1,51 @@
1
+ <!doctype html>
2
+ <html lang='en-GB'>
3
+ <head>
4
+ <meta charset='utf-8'>
5
+ <meta name='viewport' content='width=device-width, initial-scale=1.0, viewport-fit=cover' />
6
+ <link rel='stylesheet' href='/dist/assets/styles.css' />
7
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@100..900&family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
8
+
9
+ <style>
10
+ body {
11
+ background: #fafafa;
12
+ }
13
+ </style>
14
+ </head>
15
+ <body>
16
+
17
+ <wc-button id="randomize">Randomize</wc-button>
18
+ <br />
19
+
20
+ <wc-chart-doughnut width="400" margin="20"></wc-chart-doughnut>
21
+
22
+ <script>
23
+ const chart = document.querySelector('wc-chart-doughnut');
24
+ chart.label = "Browsers";
25
+ chart.data = [
26
+ { label: 'Firefox', value: 10, color: '--color-red', hoverColor: '--color-red-40' },
27
+ { label: 'Chrome', value: 20, color: '--color-green', hoverColor: '--color-green-50' },
28
+ { label: 'Microsoft Edge', value: 30 },
29
+ { label: 'Internet Explorer', value: 40 },
30
+ { label: 'MC Browser', value: 50 },
31
+ ];
32
+
33
+ function randomNumber(min, max) {
34
+ return parseInt(Math.random() * (max - min) + min);
35
+ }
36
+
37
+ let count = 1;
38
+ document.querySelector('#randomize').addEventListener('click', () => {
39
+ chart.data = chart.data.map(d => ({ ...d, value: parseInt(Math.random() * 100) }));
40
+ chart.width = randomNumber(400, 600);
41
+ });
42
+ </script>
43
+
44
+
45
+ <script type='module'>
46
+ import { Button, ChartDonut } from '/dist/index.js';
47
+ window.customElements.define('wc-chart-doughnut', ChartDonut);
48
+ window.customElements.define('wc-button', Button);
49
+ </script>
50
+ </body>
51
+ </html>
@@ -0,0 +1 @@
1
+ export { ChartDonut } from './chart-donut.js';
@@ -0,0 +1,37 @@
1
+ @use '../../scss/mixin';
2
+
3
+ @include mixin.base-styles;
4
+
5
+ :host {
6
+ display: inline-block;
7
+ }
8
+
9
+ .title {
10
+ @include mixin.get-typography('title-large-emphasized');
11
+ fill: var(--color-on-surface);
12
+ }
13
+
14
+ .label {
15
+ @include mixin.get-typography('title-medium');
16
+ fill: var(--color-on-surface);
17
+ }
18
+
19
+ .arc {
20
+ cursor: pointer;
21
+ transition: filter 150ms ease;
22
+
23
+ &:hover {
24
+ filter: brightness(1.2);
25
+ }
26
+ }
27
+
28
+ .item-polyline {
29
+ fill: none;
30
+ stroke-width: 1;
31
+ stroke: var(--color-on-surface);
32
+ }
33
+
34
+ .item-label {
35
+ fill: var(--color-on-surface);
36
+ @include mixin.get-typography('label-medium');
37
+ }
@@ -0,0 +1,287 @@
1
+ import { html, LitElement, PropertyValues } from 'lit';
2
+ import { property, query } from 'lit/decorators.js';
3
+ import IndividualComponent from 'src/IndividualComponent.js';
4
+ import * as d3 from 'd3';
5
+ import styles from './chart-donut.scss';
6
+
7
+ export type ChartDoughnutColor = {
8
+ color: string;
9
+ };
10
+
11
+ export type ChartDoughnutItem = {
12
+ name: string;
13
+ value: number;
14
+ label?: string;
15
+ color?: string;
16
+ };
17
+
18
+ const chartColors: ChartDoughnutColor[] = [];
19
+ ['purple', 'blue', 'red', 'green', 'yellow', 'orange'].forEach(colorName => {
20
+ chartColors.push({
21
+ color: `var(--color-${colorName})`,
22
+ });
23
+ });
24
+
25
+ /** SVGPathElement augmented with the last rendered arc datum for smooth tween interpolation. */
26
+ interface ArcPathElement extends SVGPathElement {
27
+ _prevDatum?: d3.PieArcDatum<ChartDoughnutItem>;
28
+ }
29
+
30
+ function debounce<T extends (...args: any[]) => void>(fn: T, wait: number): T {
31
+ let timer: ReturnType<typeof setTimeout>;
32
+ return ((...args: any[]) => {
33
+ clearTimeout(timer);
34
+ timer = setTimeout(() => fn(...args), wait);
35
+ }) as T;
36
+ }
37
+
38
+ /**
39
+ * @label Chart Doughnut
40
+ * @tag wc-chart-doughnut
41
+ * @rawTag chart-doughnut
42
+ * @summary A doughnut chart is a circular chart with a blank center. The area in the center can be used to display information.
43
+ * @tags charts
44
+ *
45
+ * @example
46
+ * ```html
47
+ * <wc-chart-doughnut width="400" label="Total"></wc-chart-doughnut>
48
+ * <script>
49
+ * document.querySelector('wc-chart-doughnut').data = [
50
+ * { name: 'A', value: 30, label: 'Category A' },
51
+ * { name: 'B', value: 50, label: 'Category B' },
52
+ * { name: 'C', value: 20, label: 'Category C' },
53
+ * ];
54
+ * </script>
55
+ * ```
56
+ */
57
+ @IndividualComponent
58
+ export class ChartDoughnut extends LitElement {
59
+ static styles = [styles];
60
+
61
+ @query('svg')
62
+ private svgElement?: SVGElement;
63
+
64
+ /** Width (and height) of the chart in pixels. */
65
+ @property({ type: Number, reflect: true }) width: number = 0;
66
+
67
+ /** Margin around the chart. */
68
+ @property({ type: Number, reflect: true }) margin: number = 10;
69
+
70
+ /** Whether to show labels outside the chart. */
71
+ @property({ type: Boolean, reflect: true, attribute: 'show-labels' })
72
+ showLabels: boolean = true;
73
+
74
+ /** Chart data array. Each item should have name, value, and optional label and color. */
75
+ @property({ type: Array }) data: ChartDoughnutItem[] = [];
76
+
77
+ /** Label displayed in the center of the doughnut. */
78
+ @property({ type: String }) label?: string;
79
+
80
+ private _initialized = false;
81
+
82
+ private _debouncedRenderChart = debounce(() => {
83
+ this._renderChart(true);
84
+ }, 300);
85
+
86
+ firstUpdated() {
87
+ this._renderChart(false);
88
+ }
89
+
90
+ updated(changedProperties: PropertyValues) {
91
+ if (!this._initialized) {
92
+ this._initialized = true;
93
+ return;
94
+ }
95
+ const watchedProps = ['width', 'margin', 'showLabels', 'data'];
96
+ const hasChanged = watchedProps.some(prop => changedProperties.has(prop));
97
+ if (hasChanged) {
98
+ this._debouncedRenderChart();
99
+ }
100
+ }
101
+
102
+ private _getRadius(): number {
103
+ return this.width / 2 - this.margin - 100;
104
+ }
105
+
106
+ private _getTotal(): number {
107
+ return this.data.reduce((total, d) => total + d.value, 0);
108
+ }
109
+
110
+ private _getPieData() {
111
+ const pie = d3
112
+ .pie<ChartDoughnutItem>()
113
+ .sort(null)
114
+ .value(d => d.value);
115
+ return pie(this.data);
116
+ }
117
+
118
+ private _getColorScale() {
119
+ return d3
120
+ .scaleOrdinal<string, ChartDoughnutColor>()
121
+ .domain(this.data.map(d => d.name))
122
+ .range(chartColors);
123
+ }
124
+
125
+ private _renderChart(animate: boolean) {
126
+ if (!this.svgElement) return;
127
+
128
+ const DURATION = 500;
129
+ const radius = this._getRadius();
130
+ const pieData = this._getPieData();
131
+ const colorScale = this._getColorScale();
132
+ const total = this._getTotal();
133
+
134
+ const svg = d3.select(this.svgElement);
135
+
136
+ const doughnutArc = d3
137
+ .arc<d3.PieArcDatum<ChartDoughnutItem>>()
138
+ .innerRadius(radius * 0.72)
139
+ .outerRadius(radius);
140
+
141
+ const labelsArc = d3
142
+ .arc<d3.PieArcDatum<ChartDoughnutItem>>()
143
+ .innerRadius(radius + 10)
144
+ .outerRadius(radius + 10);
145
+
146
+ // Update SVG dimensions and center transform
147
+ svg.attr('width', this.width).attr('height', this.width);
148
+ svg
149
+ .select('.chart-container')
150
+ .attr('transform', `translate(${this.width / 2},${this.width / 2})`);
151
+
152
+ // Arc paths — keyed by name so D3 matches elements across updates
153
+ const $paths = svg
154
+ .select('.arc-container')
155
+ .selectAll<SVGPathElement, d3.PieArcDatum<ChartDoughnutItem>>('.arc')
156
+ .data(pieData, d => d.data.name)
157
+ .join('path')
158
+ .attr('class', 'arc')
159
+ .style('fill', d => d.data.color || colorScale(d.data.name).color);
160
+
161
+ if (animate) {
162
+ $paths
163
+ .transition()
164
+ .duration(DURATION)
165
+ .ease(d3.easeCubicInOut)
166
+ .attrTween('d', function (this: SVGPathElement, d) {
167
+ const self = this as ArcPathElement;
168
+ // Interpolate from the last rendered angles to the new ones.
169
+ // New (entering) arcs start collapsed at their startAngle.
170
+ const prev: { startAngle: number; endAngle: number } =
171
+ self._prevDatum ?? {
172
+ startAngle: d.startAngle,
173
+ endAngle: d.startAngle,
174
+ };
175
+ self._prevDatum = d;
176
+ const iStart = d3.interpolateNumber(prev.startAngle, d.startAngle);
177
+ const iEnd = d3.interpolateNumber(prev.endAngle, d.endAngle);
178
+ return (t: number) =>
179
+ doughnutArc({ ...d, startAngle: iStart(t), endAngle: iEnd(t) }) ??
180
+ '';
181
+ });
182
+ } else {
183
+ // Initial render: draw immediately and seed previous-datum for later tweens
184
+ $paths
185
+ .each(function (this: SVGPathElement, d) {
186
+ (this as ArcPathElement)._prevDatum = d;
187
+ })
188
+ .attr('d', d => doughnutArc(d) ?? '');
189
+ }
190
+
191
+ // Animate the central total counter
192
+ const $title = svg.select('.title');
193
+ if (animate) {
194
+ $title
195
+ .transition()
196
+ .duration(DURATION)
197
+ .ease(d3.easeCubicInOut)
198
+ .tween('text', function (this: d3.BaseType) {
199
+ const sel = d3.select(this as SVGTextElement);
200
+ const start = parseFloat(sel.text()) || 0;
201
+ const interp = d3.interpolateNumber(start, total);
202
+ return function (t: number) {
203
+ sel.text(Math.round(interp(t)));
204
+ };
205
+ });
206
+ } else {
207
+ $title.text(total);
208
+ }
209
+
210
+ // Label polylines and text
211
+ const $chartContainer = svg.select('.chart-container');
212
+
213
+ if (this.showLabels) {
214
+ const pointsFn = (d: d3.PieArcDatum<ChartDoughnutItem>) => {
215
+ const posA = doughnutArc.centroid(d);
216
+ const posB = labelsArc.centroid(d);
217
+ const posC = posB.slice() as [number, number];
218
+ const midAngle = d.startAngle + (d.endAngle - d.startAngle) / 2;
219
+ posC[0] = radius * (midAngle < Math.PI ? 1 : -1);
220
+ return [posA, posB, posC].map(p => p.join(',')).join(' ');
221
+ };
222
+
223
+ const transformFn = (d: d3.PieArcDatum<ChartDoughnutItem>) => {
224
+ const pos = labelsArc.centroid(d);
225
+ const midAngle = d.startAngle + (d.endAngle - d.startAngle) / 2;
226
+ pos[0] = radius * (midAngle < Math.PI ? 1 : -1);
227
+ return `translate(${pos})`;
228
+ };
229
+
230
+ const anchorFn = (d: d3.PieArcDatum<ChartDoughnutItem>) => {
231
+ const midAngle = d.startAngle + (d.endAngle - d.startAngle) / 2;
232
+ return midAngle < Math.PI ? 'start' : 'end';
233
+ };
234
+
235
+ const $polylines = $chartContainer
236
+ .selectAll<SVGPolylineElement, d3.PieArcDatum<ChartDoughnutItem>>(
237
+ '.item-polyline',
238
+ )
239
+ .data(pieData, d => d.data.name)
240
+ .join('polyline')
241
+ .attr('class', 'item-polyline');
242
+
243
+ const $labels = $chartContainer
244
+ .selectAll<SVGTextElement, d3.PieArcDatum<ChartDoughnutItem>>('.item-label')
245
+ .data(pieData, d => d.data.name)
246
+ .join('text')
247
+ .attr('class', 'item-label')
248
+ .text(d => d.data.label ?? '');
249
+
250
+ if (animate) {
251
+ $polylines
252
+ .transition()
253
+ .duration(DURATION)
254
+ .ease(d3.easeCubicInOut)
255
+ .attr('points', pointsFn);
256
+ $labels
257
+ .transition()
258
+ .duration(DURATION)
259
+ .ease(d3.easeCubicInOut)
260
+ .attr('transform', transformFn)
261
+ .style('text-anchor', anchorFn);
262
+ } else {
263
+ $polylines.attr('points', pointsFn);
264
+ $labels.attr('transform', transformFn).style('text-anchor', anchorFn);
265
+ }
266
+ } else {
267
+ $chartContainer.selectAll('.item-polyline').remove();
268
+ $chartContainer.selectAll('.item-label').remove();
269
+ }
270
+ }
271
+
272
+ render() {
273
+ return html`
274
+ <div class="chart">
275
+ <svg>
276
+ <g class="chart-container">
277
+ <g class="arc-container"></g>
278
+ <text class="title" text-anchor="middle"></text>
279
+ <text class="label" text-anchor="middle" y="16">
280
+ ${this.label}
281
+ </text>
282
+ </g>
283
+ </svg>
284
+ </div>
285
+ `;
286
+ }
287
+ }