juxscript 1.0.57 → 1.0.60

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.
@@ -1,421 +0,0 @@
1
- import { BaseChart, BaseChartState, ChartDataPoint } from './lib/BaseChart.js';
2
-
3
- export interface BarChartOptions {
4
- data?: ChartDataPoint[];
5
- title?: string;
6
- subtitle?: string;
7
- xAxisLabel?: string;
8
- yAxisLabel?: string;
9
- showTicksX?: boolean;
10
- showTicksY?: boolean;
11
- showScaleX?: boolean;
12
- showScaleY?: boolean;
13
- scaleXUnit?: string;
14
- scaleYUnit?: string;
15
- showLegend?: boolean;
16
- legendOrientation?: 'horizontal' | 'vertical';
17
- showDataTable?: boolean;
18
- showDataLabels?: boolean;
19
- animate?: boolean;
20
- animationDuration?: number;
21
- chartOrientation?: 'vertical' | 'horizontal';
22
- chartDirection?: 'normal' | 'reverse';
23
- width?: number;
24
- height?: number;
25
- colors?: string[];
26
- class?: string;
27
- style?: string;
28
- theme?: 'google' | 'seriesa' | 'hr' | 'figma' | 'notion' | 'chalk' | 'mint';
29
- styleMode?: 'default' | 'gradient' | 'outline' | 'dashed' | 'glow' | 'glass';
30
- borderRadius?: number;
31
- }
32
-
33
- interface BarChartState extends BaseChartState {
34
- data: ChartDataPoint[];
35
- chartOrientation: 'vertical' | 'horizontal';
36
- chartDirection: 'normal' | 'reverse';
37
- }
38
-
39
- export class BarChart extends BaseChart<BarChartState> {
40
- constructor(id: string, options: BarChartOptions = {}) {
41
- const defaultColors = [
42
- '#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
43
- '#ec4899', '#06b6d4', '#f97316', '#84cc16', '#6366f1'
44
- ];
45
-
46
- super(id, {
47
- data: options.data ?? [],
48
- title: options.title ?? '',
49
- subtitle: options.subtitle ?? '',
50
- xAxisLabel: options.xAxisLabel ?? '',
51
- yAxisLabel: options.yAxisLabel ?? '',
52
- showTicksX: options.showTicksX ?? true,
53
- showTicksY: options.showTicksY ?? true,
54
- showScaleX: options.showScaleX ?? true,
55
- showScaleY: options.showScaleY ?? true,
56
- scaleXUnit: options.scaleXUnit ?? '',
57
- scaleYUnit: options.scaleYUnit ?? '',
58
- showLegend: options.showLegend ?? false,
59
- legendOrientation: options.legendOrientation ?? 'horizontal',
60
- showDataTable: options.showDataTable ?? false,
61
- showDataLabels: options.showDataLabels ?? true,
62
- animate: options.animate ?? true,
63
- animationDuration: options.animationDuration ?? 800,
64
- chartOrientation: options.chartOrientation ?? 'vertical',
65
- chartDirection: options.chartDirection ?? 'normal',
66
- width: options.width ?? 600,
67
- height: options.height ?? 400,
68
- colors: options.colors ?? defaultColors,
69
- class: options.class ?? '',
70
- style: options.style ?? '',
71
- theme: options.theme,
72
- styleMode: options.styleMode ?? 'default',
73
- borderRadius: options.borderRadius ?? 4
74
- });
75
- }
76
-
77
- /* ═════════════════════════════════════════════════════════════════
78
- * CHART-SPECIFIC FLUENT API
79
- * ═════════════════════════════════════════════════════════════════ */
80
-
81
- chartOrientation(value: 'vertical' | 'horizontal'): this {
82
- this.state.chartOrientation = value;
83
- if (this.container) this._updateChart();
84
- return this;
85
- }
86
-
87
- chartDirection(value: 'normal' | 'reverse'): this {
88
- this.state.chartDirection = value;
89
- if (this.container) this._updateChart();
90
- return this;
91
- }
92
-
93
- /* ═════════════════════════════════════════════════════════════════
94
- * ABSTRACT METHOD IMPLEMENTATIONS
95
- * ═════════════════════════════════════════════════════════════════ */
96
-
97
- protected _getChartClassName(): string {
98
- return 'jux-barchart';
99
- }
100
-
101
- protected _createSVG(): SVGSVGElement {
102
- const { data, width, height, animate } = this.state;
103
-
104
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
105
- svg.setAttribute('width', width.toString());
106
- svg.setAttribute('height', height.toString());
107
- svg.setAttribute('class', 'jux-barchart-svg');
108
-
109
- if (!data.length) return svg;
110
-
111
- if (animate) this._addAnimationStyles(svg);
112
-
113
- const padding = { top: 40, right: 40, bottom: 60, left: 60 };
114
- const chartWidth = width - padding.left - padding.right;
115
- const chartHeight = height - padding.top - padding.bottom;
116
- const maxValue = Math.max(...data.map(d => d.value));
117
-
118
- if (this.state.chartOrientation === 'vertical') {
119
- this._renderVerticalBars(svg, padding, chartWidth, chartHeight, maxValue);
120
- } else {
121
- this._renderHorizontalBars(svg, padding, chartWidth, chartHeight, maxValue);
122
- }
123
-
124
- return svg;
125
- }
126
-
127
- protected _getBaseStyles(): string {
128
- return `
129
- .jux-barchart {
130
- font-family: var(--chart-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
131
- display: inline-block;
132
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
133
- border-radius: 12px;
134
- background: white;
135
- padding: 24px;
136
- }
137
- .jux-barchart-title {
138
- margin: 0 0 0.5rem 0;
139
- font-size: 1.25rem;
140
- font-weight: 600;
141
- }
142
- .jux-barchart-subtitle {
143
- margin: 0 0 1rem 0;
144
- font-size: 0.875rem;
145
- color: #6b7280;
146
- }
147
- .jux-barchart-legend {
148
- display: flex;
149
- flex-wrap: wrap;
150
- gap: 1rem;
151
- margin-top: 1rem;
152
- justify-content: center;
153
- }
154
- .jux-barchart-legend-item {
155
- display: flex;
156
- align-items: center;
157
- gap: 0.5rem;
158
- }
159
- .jux-barchart-legend-swatch {
160
- width: 12px;
161
- height: 12px;
162
- border-radius: 2px;
163
- }
164
- .jux-barchart-legend-label {
165
- font-size: 0.875rem;
166
- color: #374151;
167
- }
168
- .jux-barchart-table {
169
- width: 100%;
170
- margin-top: 1rem;
171
- border-collapse: collapse;
172
- font-size: 0.875rem;
173
- }
174
- .jux-barchart-table thead th {
175
- text-align: center;
176
- padding: 0.5rem;
177
- border-bottom: 2px solid #e5e7eb;
178
- font-weight: 600;
179
- }
180
- .jux-barchart-table tbody td {
181
- padding: 0.5rem;
182
- border-bottom: 1px solid #f3f4f6;
183
- text-align: center;
184
- }
185
- .jux-barchart-svg {
186
- font-family: inherit;
187
- }
188
- `;
189
- }
190
-
191
- /* ═════════════════════════════════════════════════════════════════
192
- * PRIVATE RENDERING METHODS
193
- * ═════════════════════════════════════════════════════════════════ */
194
-
195
- private _addAnimationStyles(svg: SVGSVGElement): void {
196
- const { animationDuration, chartOrientation, chartDirection } = this.state;
197
- const animationId = `bar-grow-${this._id}`;
198
- const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
199
-
200
- let transformOrigin = chartOrientation === 'horizontal'
201
- ? (chartDirection === 'normal' ? 'left' : 'right')
202
- : (chartDirection === 'reverse' ? 'top' : 'bottom');
203
-
204
- let scaleAxis = chartOrientation === 'horizontal' ? 'scaleX' : 'scaleY';
205
-
206
- style.textContent = `
207
- @keyframes ${animationId} {
208
- from { transform: ${scaleAxis}(0); opacity: 0; }
209
- to { transform: ${scaleAxis}(1); opacity: 1; }
210
- }
211
- .jux-bar-animated {
212
- transform-origin: ${transformOrigin};
213
- animation: ${animationId} ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
214
- }
215
- .jux-label-animated {
216
- opacity: 0;
217
- animation: fadeIn ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
218
- }
219
- @keyframes fadeIn {
220
- from { opacity: 0; }
221
- to { opacity: 1; }
222
- }
223
- `;
224
- svg.appendChild(style);
225
- }
226
-
227
- private _renderVerticalBars(svg: SVGSVGElement, padding: any, chartWidth: number, chartHeight: number, maxValue: number): void {
228
- const { data, width, height, colors, xAxisLabel, yAxisLabel, showTicksX, showTicksY, showScaleX, showScaleY, scaleYUnit, chartDirection } = this.state;
229
- const isReverse = chartDirection === 'reverse';
230
- const yScale = chartHeight / maxValue;
231
- const barWidth = chartWidth / data.length;
232
- const barGap = barWidth * 0.2;
233
- const actualBarWidth = barWidth - barGap;
234
-
235
- // Y-axis
236
- if (showScaleY) {
237
- this._renderAxis(svg, padding.left, padding.top, padding.left, height - padding.bottom);
238
- if (yAxisLabel && showTicksY) {
239
- this._renderAxisLabel(svg, yAxisLabel, 20, padding.top + chartHeight / 2, -90);
240
- }
241
- if (showTicksY) {
242
- this._renderYTicks(svg, maxValue, yScale, padding, width, height, scaleYUnit, isReverse, chartHeight);
243
- }
244
- }
245
-
246
- // X-axis
247
- if (showScaleX) {
248
- const axisY = isReverse ? padding.top : height - padding.bottom;
249
- this._renderAxis(svg, padding.left, axisY, width - padding.right, axisY);
250
- if (xAxisLabel && showTicksX) {
251
- this._renderAxisLabel(svg, xAxisLabel, padding.left + chartWidth / 2, height - 15, 0);
252
- }
253
- }
254
-
255
- // Bars
256
- data.forEach((point, index) => {
257
- const x = padding.left + (index * barWidth) + (barGap / 2);
258
- const barHeight = point.value * yScale;
259
- const y = isReverse ? padding.top : height - padding.bottom - barHeight;
260
- const color = point.color || colors[index % colors.length];
261
-
262
- this._renderBar(svg, x, y, actualBarWidth, barHeight, color, index, point, false);
263
-
264
- // Category label
265
- if (showTicksX && showScaleX) {
266
- this._renderCategoryLabel(svg, point.label, x + actualBarWidth / 2, height - padding.bottom + 20, index);
267
- }
268
- });
269
- }
270
-
271
- private _renderHorizontalBars(svg: SVGSVGElement, padding: any, chartWidth: number, chartHeight: number, maxValue: number): void {
272
- const { data, width, height, colors, showScaleY, showTicksY, chartDirection } = this.state;
273
- const isReverse = chartDirection === 'reverse';
274
- const xScale = chartWidth / maxValue;
275
- const barHeight = chartHeight / data.length;
276
- const barGap = barHeight * 0.2;
277
- const actualBarHeight = barHeight - barGap;
278
-
279
- // Bars
280
- data.forEach((point, index) => {
281
- const y = padding.top + (index * barHeight) + (barGap / 2);
282
- const barWidth = point.value * xScale;
283
- const x = isReverse ? width - padding.right - barWidth : padding.left;
284
- const color = point.color || colors[index % colors.length];
285
-
286
- this._renderBar(svg, x, y, barWidth, actualBarHeight, color, index, point, true);
287
-
288
- // Category label
289
- if (showTicksY && showScaleY) {
290
- this._renderCategoryLabel(svg, point.label, padding.left - 10, y + actualBarHeight / 2 + 4, index);
291
- }
292
- });
293
- }
294
-
295
- private _renderAxis(svg: SVGSVGElement, x1: number, y1: number, x2: number, y2: number): void {
296
- const axis = document.createElementNS('http://www.w3.org/2000/svg', 'line');
297
- axis.setAttribute('x1', x1.toString());
298
- axis.setAttribute('y1', y1.toString());
299
- axis.setAttribute('x2', x2.toString());
300
- axis.setAttribute('y2', y2.toString());
301
- axis.setAttribute('stroke', '#9ca3af');
302
- axis.setAttribute('stroke-width', '2');
303
- svg.appendChild(axis);
304
- }
305
-
306
- private _renderAxisLabel(svg: SVGSVGElement, text: string, x: number, y: number, rotate: number): void {
307
- const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
308
- label.setAttribute('x', x.toString());
309
- label.setAttribute('y', y.toString());
310
- label.setAttribute('text-anchor', 'middle');
311
- if (rotate !== 0) label.setAttribute('transform', `rotate(${rotate}, ${x}, ${y})`);
312
- label.setAttribute('fill', '#6b7280');
313
- label.setAttribute('font-size', '12');
314
- label.setAttribute('font-weight', '500');
315
- label.textContent = text;
316
- svg.appendChild(label);
317
- }
318
-
319
- private _renderYTicks(svg: SVGSVGElement, maxValue: number, yScale: number, padding: any, width: number, height: number, unit: string, isReverse: boolean, chartHeight: number): void {
320
- const numTicks = 5;
321
- for (let i = 0; i <= numTicks; i++) {
322
- const value = (maxValue / numTicks) * i;
323
- const y = isReverse
324
- ? padding.top + (value * yScale)
325
- : height - padding.bottom - (value * yScale);
326
-
327
- // Grid line
328
- const gridLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
329
- gridLine.setAttribute('x1', padding.left.toString());
330
- gridLine.setAttribute('y1', y.toString());
331
- gridLine.setAttribute('x2', (width - padding.right).toString());
332
- gridLine.setAttribute('y2', y.toString());
333
- gridLine.setAttribute('stroke', '#e5e7eb');
334
- gridLine.setAttribute('stroke-width', '1');
335
- gridLine.setAttribute('stroke-dasharray', '4,4');
336
- svg.appendChild(gridLine);
337
-
338
- // Tick label
339
- const tickLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
340
- tickLabel.setAttribute('x', (padding.left - 10).toString());
341
- tickLabel.setAttribute('y', (y + 4).toString());
342
- tickLabel.setAttribute('text-anchor', 'end');
343
- tickLabel.setAttribute('fill', '#6b7280');
344
- tickLabel.setAttribute('font-size', '11');
345
- tickLabel.textContent = Math.round(value).toString() + (unit || '');
346
- svg.appendChild(tickLabel);
347
- }
348
- }
349
-
350
- private _renderCategoryLabel(svg: SVGSVGElement, text: string, x: number, y: number, index: number): void {
351
- const { animate } = this.state;
352
- const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
353
- label.setAttribute('x', x.toString());
354
- label.setAttribute('y', y.toString());
355
- label.setAttribute('text-anchor', 'middle');
356
- label.setAttribute('fill', '#6b7280');
357
- label.setAttribute('font-size', '11');
358
- label.textContent = text;
359
-
360
- if (animate) {
361
- label.classList.add('jux-label-animated');
362
- label.style.animationDelay = `${index * 100 + 200}ms`;
363
- }
364
-
365
- svg.appendChild(label);
366
- }
367
-
368
- private _renderBar(svg: SVGSVGElement, x: number, y: number, width: number, height: number, color: string, index: number, point: ChartDataPoint, isHorizontal: boolean = false): void {
369
- const { borderRadius, showDataLabels, animate, animationDuration } = this.state;
370
-
371
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
372
- rect.setAttribute('x', x.toString());
373
- rect.setAttribute('y', y.toString());
374
- rect.setAttribute('width', width.toString());
375
- rect.setAttribute('height', height.toString());
376
- rect.setAttribute('rx', borderRadius.toString());
377
- rect.setAttribute('ry', borderRadius.toString());
378
- rect.setAttribute('fill', color);
379
-
380
- if (animate) {
381
- rect.classList.add('jux-bar-animated');
382
- rect.style.animationDelay = `${index * 100}ms`;
383
- }
384
-
385
- svg.appendChild(rect);
386
-
387
- // Value labels
388
- if (showDataLabels) {
389
- const valueLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
390
-
391
- if (isHorizontal) {
392
- valueLabel.setAttribute('x', (x + width / 2).toString());
393
- valueLabel.setAttribute('y', (y + height / 2 + 4).toString());
394
- valueLabel.setAttribute('fill', '#ffffff');
395
- } else {
396
- valueLabel.setAttribute('x', (x + width / 2).toString());
397
- valueLabel.setAttribute('y', (y - 5).toString());
398
- valueLabel.setAttribute('fill', '#374151');
399
- }
400
-
401
- valueLabel.setAttribute('text-anchor', 'middle');
402
- valueLabel.setAttribute('font-weight', '600');
403
- valueLabel.setAttribute('font-size', '11');
404
- valueLabel.textContent = point.value.toString();
405
-
406
- if (animate) {
407
- valueLabel.classList.add('jux-label-animated');
408
- valueLabel.style.animationDelay = `${index * 100 + animationDuration - 200}ms`;
409
- }
410
-
411
- svg.appendChild(valueLabel);
412
- }
413
- }
414
- }
415
-
416
- export function barchart(id: string, options: BarChartOptions = {}): BarChart {
417
- return new BarChart(id, options);
418
- }
419
-
420
- // Re-export the data point type for convenience
421
- export type BarChartDataPoint = ChartDataPoint;
@@ -1,263 +0,0 @@
1
- import { BaseChart, BaseChartState, ChartDataPoint } from './lib/BaseChart.js';
2
-
3
- export interface DoughnutChartOptions {
4
- data?: ChartDataPoint[];
5
- title?: string;
6
- subtitle?: string;
7
- showLegend?: boolean;
8
- legendOrientation?: 'horizontal' | 'vertical';
9
- showDataTable?: boolean;
10
- showDataLabels?: boolean;
11
- animate?: boolean;
12
- animationDuration?: number;
13
- width?: number;
14
- height?: number;
15
- colors?: string[];
16
- class?: string;
17
- style?: string;
18
- theme?: 'google' | 'seriesa' | 'hr' | 'figma' | 'notion' | 'chalk' | 'mint';
19
- styleMode?: 'default' | 'gradient' | 'outline' | 'dashed' | 'glow' | 'glass';
20
- borderRadius?: number;
21
- }
22
-
23
- interface DoughnutChartState extends BaseChartState {
24
- data: ChartDataPoint[];
25
- }
26
-
27
- export class DoughnutChart extends BaseChart<DoughnutChartState> {
28
- constructor(id: string, options: DoughnutChartOptions = {}) {
29
- const defaultColors = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899'];
30
-
31
- super(id, {
32
- data: options.data ?? [],
33
- title: options.title ?? '',
34
- subtitle: options.subtitle ?? '',
35
- xAxisLabel: '',
36
- yAxisLabel: '',
37
- showTicksX: false,
38
- showTicksY: false,
39
- showScaleX: false,
40
- showScaleY: false,
41
- scaleXUnit: '',
42
- scaleYUnit: '',
43
- showLegend: options.showLegend ?? false,
44
- legendOrientation: options.legendOrientation ?? 'horizontal',
45
- showDataTable: options.showDataTable ?? false,
46
- showDataLabels: options.showDataLabels ?? true,
47
- animate: options.animate ?? true,
48
- animationDuration: options.animationDuration ?? 800,
49
- width: options.width ?? 600,
50
- height: options.height ?? 400,
51
- colors: options.colors ?? defaultColors,
52
- class: options.class ?? '',
53
- style: options.style ?? '',
54
- theme: options.theme,
55
- styleMode: options.styleMode ?? 'default',
56
- borderRadius: options.borderRadius ?? 4
57
- });
58
- }
59
-
60
- protected _getChartClassName(): string {
61
- return 'jux-doughnutchart';
62
- }
63
-
64
- protected _createSVG(): SVGSVGElement {
65
- const { data, width, height, colors, showDataLabels, animate } = this.state;
66
-
67
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
68
- svg.setAttribute('width', width.toString());
69
- svg.setAttribute('height', height.toString());
70
- svg.setAttribute('class', 'jux-doughnutchart-svg');
71
-
72
- if (!data.length) return svg;
73
-
74
- if (animate) this._addAnimationStyles(svg);
75
-
76
- const centerX = width / 2;
77
- const centerY = height / 2;
78
- const margin = showDataLabels ? 100 : 40;
79
- const radius = Math.min(width, height) / 2 - margin;
80
- const innerRadius = radius * 0.6;
81
- const total = data.reduce((sum, point) => sum + point.value, 0);
82
-
83
- this._renderDoughnutSlices(svg, centerX, centerY, radius, innerRadius, total);
84
-
85
- return svg;
86
- }
87
-
88
- protected _getBaseStyles(): string {
89
- return `
90
- .jux-doughnutchart {
91
- font-family: var(--chart-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
92
- display: inline-block;
93
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
94
- border-radius: 12px;
95
- background: white;
96
- padding: 24px;
97
- }
98
- .jux-doughnutchart-title {
99
- margin: 0 0 0.5rem 0;
100
- font-size: 1.25rem;
101
- font-weight: 600;
102
- }
103
- .jux-doughnutchart-subtitle {
104
- margin: 0 0 1rem 0;
105
- font-size: 0.875rem;
106
- color: #6b7280;
107
- }
108
- .jux-doughnutchart-legend {
109
- display: flex;
110
- flex-wrap: wrap;
111
- gap: 1rem;
112
- margin-top: 1rem;
113
- justify-content: center;
114
- }
115
- .jux-doughnutchart-legend-item {
116
- display: flex;
117
- align-items: center;
118
- gap: 0.5rem;
119
- }
120
- .jux-doughnutchart-legend-swatch {
121
- width: 12px;
122
- height: 12px;
123
- border-radius: 2px;
124
- }
125
- .jux-doughnutchart-legend-label {
126
- font-size: 0.875rem;
127
- color: #374151;
128
- }
129
- .jux-doughnutchart-table {
130
- width: 100%;
131
- margin-top: 1.5rem;
132
- border-collapse: collapse;
133
- font-size: 0.875rem;
134
- border: 1px solid #e5e7eb;
135
- border-radius: 8px;
136
- overflow: hidden;
137
- }
138
- .jux-doughnutchart-table thead {
139
- background: #f9fafb;
140
- }
141
- .jux-doughnutchart-table thead th {
142
- text-align: center;
143
- padding: 12px 16px;
144
- border-bottom: 2px solid #e5e7eb;
145
- font-weight: 600;
146
- }
147
- .jux-doughnutchart-table tbody td {
148
- padding: 12px 16px;
149
- border-bottom: 1px solid #f3f4f6;
150
- text-align: center;
151
- }
152
- .jux-doughnutchart-svg {
153
- font-family: inherit;
154
- }
155
- `;
156
- }
157
-
158
- private _addAnimationStyles(svg: SVGSVGElement): void {
159
- const { animationDuration } = this.state;
160
- const animationId = `slice-scale-${this._id}`;
161
- const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
162
-
163
- style.textContent = `
164
- @keyframes ${animationId} {
165
- from { transform: scale(0); opacity: 0; }
166
- to { transform: scale(1); opacity: 1; }
167
- }
168
- .jux-slice-animated {
169
- transform-origin: ${this.state.width / 2}px ${this.state.height / 2}px;
170
- animation: ${animationId} ${animationDuration}ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
171
- opacity: 0;
172
- }
173
- `;
174
- svg.appendChild(style);
175
- }
176
-
177
- private _renderDoughnutSlices(svg: SVGSVGElement, centerX: number, centerY: number, radius: number, innerRadius: number, total: number): void {
178
- const { data, colors, animate, showDataLabels } = this.state;
179
- let currentAngle = -90;
180
-
181
- data.forEach((point, index) => {
182
- const color = point.color || colors[index % colors.length];
183
- const sliceAngle = (point.value / total) * 360;
184
- const path = this._createDoughnutSlice(centerX, centerY, radius, innerRadius, currentAngle, currentAngle + sliceAngle);
185
-
186
- const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
187
- pathEl.setAttribute('d', path);
188
- pathEl.setAttribute('fill', color);
189
- pathEl.setAttribute('stroke', 'white');
190
- pathEl.setAttribute('stroke-width', '2');
191
-
192
- if (animate) {
193
- pathEl.classList.add('jux-slice-animated');
194
- pathEl.style.animationDelay = `${index * 150}ms`;
195
- }
196
-
197
- svg.appendChild(pathEl);
198
-
199
- // ✅ Add data labels if enabled
200
- if (showDataLabels) {
201
- const midAngle = currentAngle + sliceAngle / 2;
202
- const labelRadius = (radius + innerRadius) / 2;
203
- const labelX = centerX + labelRadius * Math.cos((midAngle * Math.PI) / 180);
204
- const labelY = centerY + labelRadius * Math.sin((midAngle * Math.PI) / 180);
205
-
206
- const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
207
- label.setAttribute('x', labelX.toString());
208
- label.setAttribute('y', labelY.toString());
209
- label.setAttribute('text-anchor', 'middle');
210
- label.setAttribute('dominant-baseline', 'middle');
211
- label.setAttribute('fill', '#ffffff');
212
- label.setAttribute('font-weight', '600');
213
- label.setAttribute('font-size', '14');
214
- label.textContent = point.value.toString();
215
-
216
- if (animate) {
217
- label.style.opacity = '0';
218
- label.style.animation = `fadeIn 400ms ease-out ${index * 150 + 400}ms forwards`;
219
- }
220
-
221
- svg.appendChild(label);
222
- }
223
-
224
- currentAngle += sliceAngle;
225
- });
226
-
227
- // Center circle
228
- const centerCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
229
- centerCircle.setAttribute('cx', centerX.toString());
230
- centerCircle.setAttribute('cy', centerY.toString());
231
- centerCircle.setAttribute('r', innerRadius.toString());
232
- centerCircle.setAttribute('fill', 'white');
233
- svg.appendChild(centerCircle);
234
- }
235
-
236
- private _createDoughnutSlice(centerX: number, centerY: number, outerRadius: number, innerRadius: number, startAngle: number, endAngle: number): string {
237
- const startRad = (startAngle * Math.PI) / 180;
238
- const endRad = (endAngle * Math.PI) / 180;
239
-
240
- const x1 = centerX + outerRadius * Math.cos(startRad);
241
- const y1 = centerY + outerRadius * Math.sin(startRad);
242
- const x2 = centerX + outerRadius * Math.cos(endRad);
243
- const y2 = centerY + outerRadius * Math.sin(endRad);
244
- const x3 = centerX + innerRadius * Math.cos(endRad);
245
- const y3 = centerY + innerRadius * Math.sin(endRad);
246
- const x4 = centerX + innerRadius * Math.cos(startRad);
247
- const y4 = centerY + innerRadius * Math.sin(startRad);
248
-
249
- const largeArcFlag = endAngle - startAngle > 180 ? 1 : 0;
250
-
251
- return [
252
- `M ${x1} ${y1}`,
253
- `A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${x2} ${y2}`,
254
- `L ${x3} ${y3}`,
255
- `A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${x4} ${y4}`,
256
- 'Z'
257
- ].join(' ');
258
- }
259
- }
260
-
261
- export function doughnutchart(id: string, options: DoughnutChartOptions = {}): DoughnutChart {
262
- return new DoughnutChart(id, options);
263
- }