@redvars/peacock 3.2.10 → 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.
- package/dist/{PeacockComponent-CxJc63xj.js → IndividualComponent-tDnXrOLV.js} +3 -3
- package/dist/IndividualComponent-tDnXrOLV.js.map +1 -0
- package/dist/{button-DaL4va7Q.js → button-BGFJfbT2.js} +22 -35
- package/dist/button-BGFJfbT2.js.map +1 -0
- package/dist/button-group.js +8 -8
- package/dist/button-group.js.map +1 -1
- package/dist/button.js +6 -5
- package/dist/button.js.map +1 -1
- package/dist/chart-donut.js +307 -0
- package/dist/chart-donut.js.map +1 -0
- package/dist/chart-doughnut.js +307 -0
- package/dist/chart-doughnut.js.map +1 -0
- package/dist/chart-pie.js +259 -0
- package/dist/chart-pie.js.map +1 -0
- package/dist/{class-map-BvQRv7eW.js → class-map-DpeNtqCn.js} +2 -2
- package/dist/{class-map-BvQRv7eW.js.map → class-map-DpeNtqCn.js.map} +1 -1
- package/dist/clock.js +5 -6
- package/dist/clock.js.map +1 -1
- package/dist/code-editor.js +37 -24
- package/dist/code-editor.js.map +1 -1
- package/dist/code-highlighter.js +21 -7
- package/dist/code-highlighter.js.map +1 -1
- package/dist/custom-elements-jsdocs.json +5377 -3122
- package/dist/custom-elements.json +5527 -3940
- package/dist/{dispatch-event-utils-vbdiOSeC.js → dispatch-event-utils-B4odODQf.js} +2 -15
- package/dist/dispatch-event-utils-B4odODQf.js.map +1 -0
- package/dist/index.js +13 -9
- package/dist/index.js.map +1 -1
- package/dist/number-counter.js +12 -10
- package/dist/number-counter.js.map +1 -1
- package/dist/{observe-theme-change-NneLARW8.js → observe-theme-change-BISF-Gl5.js} +2 -2
- package/dist/{observe-theme-change-NneLARW8.js.map → observe-theme-change-BISF-Gl5.js.map} +1 -1
- package/dist/peacock-loader.js +124 -62
- package/dist/peacock-loader.js.map +1 -1
- package/dist/query-QBcUV-L_.js +15 -0
- package/dist/query-QBcUV-L_.js.map +1 -0
- package/dist/{image-v3BujlY5.js → slider-Dk9CFWTG.js} +1606 -327
- package/dist/slider-Dk9CFWTG.js.map +1 -0
- package/dist/src/IndividualComponent.d.ts +1 -0
- package/dist/src/accordion/{accordion-item/accordion-item.d.ts → accordion-item.d.ts} +4 -4
- package/dist/src/accordion/{accordion/accordion.d.ts → accordion.d.ts} +6 -6
- package/dist/src/accordion/{accordion-item/index.d.ts → index.d.ts} +1 -0
- package/dist/src/avatar/avatar.d.ts +2 -2
- package/dist/src/badge/badge.d.ts +2 -2
- package/dist/src/breadcrumb/breadcrumb/breadcrumb.d.ts +7 -8
- package/dist/src/breadcrumb/breadcrumb-item/breadcrumb-item.d.ts +3 -3
- package/dist/src/button/button/button.d.ts +2 -2
- package/dist/src/button/button-group/button-group.d.ts +5 -5
- package/dist/src/button/icon-button/icon-button.d.ts +2 -2
- package/dist/src/chart-donut/chart-donut.d.ts +53 -0
- package/dist/src/chart-donut/index.d.ts +1 -0
- package/dist/src/chart-doughnut/chart-doughnut.d.ts +53 -0
- package/dist/src/chart-doughnut/index.d.ts +1 -0
- package/dist/src/chart-pie/chart-pie.d.ts +50 -0
- package/dist/src/chart-pie/index.d.ts +1 -0
- package/dist/src/checkbox/checkbox.d.ts +3 -6
- package/dist/src/chip/chip/chip.d.ts +4 -4
- package/dist/src/chip/tag/tag.d.ts +3 -3
- package/dist/src/clock/clock.d.ts +3 -4
- package/dist/src/code-editor/code-editor.d.ts +11 -9
- package/dist/src/container/container.d.ts +6 -11
- package/dist/src/date-picker/date-picker.d.ts +3 -3
- package/dist/src/divider/divider.d.ts +2 -2
- package/dist/src/elevation/elevation.d.ts +2 -2
- package/dist/src/empty-state/empty-state.d.ts +9 -2
- package/dist/src/field/field.d.ts +17 -0
- package/dist/src/focus-ring/focus-ring.d.ts +1 -1
- package/dist/src/icon/icon.d.ts +2 -2
- package/dist/src/image/image.d.ts +4 -12
- package/dist/src/index.d.ts +5 -1
- package/dist/src/input/input.d.ts +2 -2
- package/dist/src/link/link.d.ts +4 -5
- package/dist/src/menu/menu/menu.d.ts +16 -0
- package/dist/src/menu/menu-item/menu-item.d.ts +12 -0
- package/dist/src/menu/menu-list/menu-list.d.ts +15 -0
- package/dist/src/number-counter/number-counter.d.ts +9 -7
- package/dist/src/number-field/number-field.d.ts +1 -1
- package/dist/src/popover/index.d.ts +1 -1
- package/dist/src/progress/circular-progress/circular-progress.d.ts +3 -3
- package/dist/src/progress/linear-progress/linear-progress.d.ts +3 -3
- package/dist/src/ripple/ripple.d.ts +60 -4
- package/dist/src/skeleton/skeleton.d.ts +6 -5
- package/dist/src/slider/index.d.ts +1 -0
- package/dist/src/slider/slider.d.ts +52 -0
- package/dist/src/spinner/spinner.d.ts +2 -2
- package/dist/src/switch/switch.d.ts +2 -2
- package/dist/src/tabs/index.d.ts +4 -0
- package/dist/src/tabs/tab-group.d.ts +41 -0
- package/dist/src/tabs/tab-panel.d.ts +21 -0
- package/dist/src/tabs/tab.d.ts +58 -0
- package/dist/src/tabs/tabs.d.ts +27 -0
- package/dist/src/textarea/textarea.d.ts +3 -3
- package/dist/src/time-picker/time-picker.d.ts +3 -3
- package/dist/src/{popover/tooltip → tooltip}/tooltip.d.ts +6 -3
- package/dist/{state-B09bP3XH.js → state-8v48Exzh.js} +2 -2
- package/dist/{state-B09bP3XH.js.map → state-8v48Exzh.js.map} +1 -1
- package/dist/{style-map-B8xgVEc9.js → style-map-CfNHEkQp.js} +2 -2
- package/dist/{style-map-B8xgVEc9.js.map → style-map-CfNHEkQp.js.map} +1 -1
- package/dist/transform-DRuHEvar.js +3312 -0
- package/dist/transform-DRuHEvar.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/{unsafe-html-B-dV3Jps.js → unsafe-html-CV6Je6HL.js} +2 -2
- package/dist/{unsafe-html-B-dV3Jps.js.map → unsafe-html-CV6Je6HL.js.map} +1 -1
- package/package.json +3 -1
- package/readme.md +2 -2
- package/src/{PeacockComponent.ts → IndividualComponent.ts} +1 -1
- package/src/accordion/{accordion-item/accordion-item.scss → accordion-item.scss} +1 -1
- package/src/accordion/{accordion-item/accordion-item.ts → accordion-item.ts} +5 -5
- package/src/accordion/{accordion/accordion.scss → accordion.scss} +2 -1
- package/src/accordion/{accordion/accordion.ts → accordion.ts} +6 -6
- package/src/accordion/{accordion-item/index.ts → index.ts} +2 -0
- package/src/avatar/avatar.ts +2 -2
- package/src/badge/badge.ts +2 -2
- package/src/breadcrumb/breadcrumb/breadcrumb.ts +7 -8
- package/src/breadcrumb/breadcrumb-item/breadcrumb-item.ts +3 -3
- package/src/button/BaseButton.ts +1 -1
- package/src/button/button/button.scss +9 -23
- package/src/button/button/button.ts +8 -8
- package/src/button/button-group/button-group.ts +7 -7
- package/src/button/icon-button/icon-button.ts +8 -8
- package/src/chart-donut/chart-donut.scss +37 -0
- package/src/chart-donut/chart-donut.ts +287 -0
- package/src/chart-donut/demo/index.html +51 -0
- package/src/chart-donut/index.ts +1 -0
- package/src/chart-doughnut/chart-donut.scss +37 -0
- package/src/chart-doughnut/chart-doughnut.ts +287 -0
- package/src/chart-doughnut/demo/index.html +51 -0
- package/src/chart-doughnut/index.ts +1 -0
- package/src/chart-pie/chart-pie.scss +27 -0
- package/src/chart-pie/chart-pie.ts +256 -0
- package/src/chart-pie/demo/index.html +51 -0
- package/src/chart-pie/index.ts +1 -0
- package/src/checkbox/checkbox.ts +3 -6
- package/src/chip/chip/chip.ts +6 -6
- package/src/chip/tag/tag.ts +6 -6
- package/src/clock/clock.ts +5 -6
- package/src/code-editor/code-editor.scss +3 -5
- package/src/code-editor/code-editor.ts +30 -15
- package/src/code-highlighter/code-highlighter.ts +19 -4
- package/src/container/container.ts +6 -11
- package/src/date-picker/date-picker.ts +7 -7
- package/src/divider/divider.ts +2 -2
- package/src/elevation/elevation.ts +2 -2
- package/src/empty-state/empty-state.ts +10 -3
- package/src/field/field.scss +4 -4
- package/src/field/field.ts +19 -2
- package/src/focus-ring/focus-ring.scss +2 -1
- package/src/focus-ring/focus-ring.ts +1 -1
- package/src/icon/icon.ts +2 -2
- package/src/icon/p-icon.ts +1 -1
- package/src/image/image.ts +4 -12
- package/src/index.ts +6 -2
- package/src/input/input.ts +6 -6
- package/src/link/link.ts +4 -5
- package/src/menu/menu/menu.ts +16 -0
- package/src/menu/menu-item/menu-item-colors.scss +2 -2
- package/src/menu/menu-item/menu-item.ts +14 -2
- package/src/menu/menu-list/menu-list.ts +16 -1
- package/src/number-counter/demo/index.html +1 -1
- package/src/number-counter/number-counter.ts +11 -9
- package/src/number-field/number-field.ts +7 -7
- package/src/peacock-loader.ts +71 -44
- package/src/popover/index.ts +1 -1
- package/src/progress/circular-progress/circular-progress.scss +1 -1
- package/src/progress/circular-progress/circular-progress.ts +3 -3
- package/src/progress/linear-progress/linear-progress.ts +3 -3
- package/src/ripple/ripple.ts +478 -94
- package/src/skeleton/skeleton.ts +6 -5
- package/src/slider/index.ts +1 -0
- package/src/slider/slider.scss +130 -0
- package/src/slider/slider.ts +178 -0
- package/src/spinner/spinner.ts +2 -2
- package/src/switch/switch.ts +4 -4
- package/src/tabs/index.ts +4 -0
- package/src/tabs/tab-group.scss +10 -0
- package/src/tabs/tab-group.ts +137 -0
- package/src/tabs/tab-panel.scss +12 -0
- package/src/tabs/tab-panel.ts +28 -0
- package/src/tabs/tab.scss +157 -0
- package/src/tabs/tab.ts +242 -0
- package/src/tabs/tabs.scss +18 -0
- package/src/tabs/tabs.ts +64 -0
- package/src/textarea/textarea.ts +5 -5
- package/src/time-picker/time-picker.ts +7 -7
- package/src/{popover/tooltip → tooltip}/tooltip.scss +1 -1
- package/src/{popover/tooltip → tooltip}/tooltip.ts +10 -6
- package/dist/PeacockComponent-CxJc63xj.js.map +0 -1
- package/dist/button-DaL4va7Q.js.map +0 -1
- package/dist/dispatch-event-utils-vbdiOSeC.js.map +0 -1
- package/dist/image-v3BujlY5.js.map +0 -1
- package/dist/src/PeacockComponent.d.ts +0 -1
- package/dist/src/accordion/accordion/index.d.ts +0 -1
- package/dist/src/avatar/p-avatar.d.ts +0 -3
- package/dist/src/badge/p-badge.d.ts +0 -3
- package/src/accordion/accordion/index.ts +0 -1
- package/src/avatar/p-avatar.ts +0 -5
- 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
|
+
}
|