juxscript 1.0.19 → 1.0.21
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/bin/cli.js +121 -72
- package/lib/components/alert.ts +212 -165
- package/lib/components/badge.ts +93 -103
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +63 -122
- package/lib/components/card.ts +109 -155
- package/lib/components/charts/areachart.ts +315 -0
- package/lib/components/charts/barchart.ts +421 -0
- package/lib/components/charts/doughnutchart.ts +263 -0
- package/lib/components/charts/lib/BaseChart.ts +402 -0
- package/lib/components/charts/lib/chart-types.ts +159 -0
- package/lib/components/charts/lib/chart-utils.ts +160 -0
- package/lib/components/charts/lib/chart.ts +707 -0
- package/lib/components/checkbox.ts +264 -127
- package/lib/components/code.ts +75 -108
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +195 -147
- package/lib/components/dialog.ts +187 -157
- package/lib/components/divider.ts +85 -191
- package/lib/components/docs-data.json +544 -2027
- package/lib/components/dropdown.ts +178 -136
- package/lib/components/element.ts +227 -171
- package/lib/components/fileupload.ts +285 -228
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +46 -69
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +107 -95
- package/lib/components/icon.ts +160 -0
- package/lib/components/icons.ts +175 -0
- package/lib/components/include.ts +153 -5
- package/lib/components/input.ts +174 -374
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +378 -240
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +103 -97
- package/lib/components/modal.ts +138 -144
- package/lib/components/nav.ts +169 -90
- package/lib/components/paragraph.ts +49 -150
- package/lib/components/progress.ts +118 -200
- package/lib/components/radio.ts +297 -149
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +184 -186
- package/lib/components/sidebar.ts +152 -140
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +258 -188
- package/lib/components/table.ts +1117 -170
- package/lib/components/tabs.ts +162 -145
- package/lib/components/theme-toggle.ts +108 -169
- package/lib/components/tooltip.ts +86 -157
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +86 -41
- package/machinery/build.js +466 -0
- package/machinery/compiler.js +354 -105
- package/machinery/server.js +23 -100
- package/machinery/watcher.js +153 -130
- package/package.json +1 -2
- package/presets/base.css +1166 -0
- package/presets/notion.css +2 -1975
- package/lib/adapters/base-adapter.js +0 -35
- package/lib/adapters/index.js +0 -33
- package/lib/adapters/mysql-adapter.js +0 -65
- package/lib/adapters/postgres-adapter.js +0 -70
- package/lib/adapters/sqlite-adapter.js +0 -56
- package/lib/components/areachart.ts +0 -1246
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1250
- package/lib/components/chart.ts +0 -127
- package/lib/components/doughnutchart.ts +0 -1191
- package/lib/components/footer.ts +0 -165
- package/lib/components/header.ts +0 -187
- package/lib/components/layout.ts +0 -239
- package/lib/components/main.ts +0 -137
- package/lib/layouts/default.jux +0 -8
- package/lib/layouts/figma.jux +0 -0
- /package/lib/{themes → components/charts/lib}/charts.js +0 -0
|
@@ -1,1191 +0,0 @@
|
|
|
1
|
-
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
-
import { State } from '../reactivity/state.js';
|
|
3
|
-
import {
|
|
4
|
-
googleTheme,
|
|
5
|
-
seriesaTheme,
|
|
6
|
-
hrTheme,
|
|
7
|
-
figmaTheme,
|
|
8
|
-
notionTheme,
|
|
9
|
-
chalkTheme,
|
|
10
|
-
mintTheme
|
|
11
|
-
} from '../themes/charts.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Bar chart data point
|
|
15
|
-
*/
|
|
16
|
-
export interface DoughnutChartDataPoint {
|
|
17
|
-
label: string;
|
|
18
|
-
value: number;
|
|
19
|
-
color?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Bar chart options
|
|
24
|
-
*/
|
|
25
|
-
export interface DoughnutChartOptions {
|
|
26
|
-
data?: DoughnutChartDataPoint[];
|
|
27
|
-
title?: string;
|
|
28
|
-
subtitle?: string;
|
|
29
|
-
xAxisLabel?: string;
|
|
30
|
-
yAxisLabel?: string;
|
|
31
|
-
showTicksX?: boolean;
|
|
32
|
-
showTicksY?: boolean;
|
|
33
|
-
showScaleX?: boolean;
|
|
34
|
-
showScaleY?: boolean;
|
|
35
|
-
scaleXUnit?: string;
|
|
36
|
-
scaleYUnit?: string;
|
|
37
|
-
showLegend?: boolean;
|
|
38
|
-
legendOrientation?: 'horizontal' | 'vertical';
|
|
39
|
-
showDataTable?: boolean;
|
|
40
|
-
showDataLabels?: boolean;
|
|
41
|
-
animate?: boolean;
|
|
42
|
-
animationDuration?: number;
|
|
43
|
-
chartOrientation?: 'vertical' | 'horizontal'; // NEW
|
|
44
|
-
chartDirection?: 'normal' | 'reverse'; // NEW: normal = bottom-to-top or left-to-right, reverse = opposite
|
|
45
|
-
width?: number;
|
|
46
|
-
height?: number;
|
|
47
|
-
colors?: string[];
|
|
48
|
-
class?: string;
|
|
49
|
-
style?: string;
|
|
50
|
-
theme?: 'google' | 'seriesa' | 'hr' | 'figma' | 'notion' | 'chalk' | 'mint';
|
|
51
|
-
styleMode?: 'default' | 'gradient' | 'outline' | 'dashed' | 'glow' | 'glass';
|
|
52
|
-
borderRadius?: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Bar chart state
|
|
57
|
-
*/
|
|
58
|
-
type DoughnutChartState = {
|
|
59
|
-
data: DoughnutChartDataPoint[];
|
|
60
|
-
title: string;
|
|
61
|
-
subtitle: string;
|
|
62
|
-
xAxisLabel: string;
|
|
63
|
-
yAxisLabel: string;
|
|
64
|
-
showTicksX: boolean;
|
|
65
|
-
showTicksY: boolean;
|
|
66
|
-
showScaleX: boolean;
|
|
67
|
-
showScaleY: boolean;
|
|
68
|
-
scaleXUnit: string;
|
|
69
|
-
scaleYUnit: string;
|
|
70
|
-
showLegend: boolean;
|
|
71
|
-
legendOrientation: 'horizontal' | 'vertical';
|
|
72
|
-
showDataTable: boolean;
|
|
73
|
-
showDataLabels: boolean;
|
|
74
|
-
animate: boolean;
|
|
75
|
-
animationDuration: number;
|
|
76
|
-
chartOrientation: 'vertical' | 'horizontal'; // NEW
|
|
77
|
-
chartDirection: 'normal' | 'reverse'; // NEW
|
|
78
|
-
width: number;
|
|
79
|
-
height: number;
|
|
80
|
-
colors: string[];
|
|
81
|
-
class: string;
|
|
82
|
-
style: string;
|
|
83
|
-
theme?: 'google' | 'seriesa' | 'hr' | 'figma' | 'notion' | 'chalk' | 'mint';
|
|
84
|
-
styleMode: 'default' | 'gradient' | 'outline' | 'dashed' | 'glow' | 'glass';
|
|
85
|
-
borderRadius: number;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Bar chart component - Simple SVG-based bar chart
|
|
90
|
-
*
|
|
91
|
-
* Usage:
|
|
92
|
-
* jux.doughnutchart('sales-chart')
|
|
93
|
-
* .data([
|
|
94
|
-
* { label: 'Jan', value: 100 },
|
|
95
|
-
* { label: 'Feb', value: 150 },
|
|
96
|
-
* { label: 'Mar', value: 200 }
|
|
97
|
-
* ])
|
|
98
|
-
* .title('Monthly Sales')
|
|
99
|
-
* .showLegend(true)
|
|
100
|
-
* .render('#app');
|
|
101
|
-
*/
|
|
102
|
-
export class DoughnutChart {
|
|
103
|
-
state: DoughnutChartState;
|
|
104
|
-
container: HTMLElement | null = null;
|
|
105
|
-
_id: string;
|
|
106
|
-
id: string;
|
|
107
|
-
|
|
108
|
-
// State bindings
|
|
109
|
-
private _boundTheme?: State<string>;
|
|
110
|
-
private _boundStyleMode?: State<string>;
|
|
111
|
-
private _boundBorderRadius?: State<number>;
|
|
112
|
-
|
|
113
|
-
constructor(id: string, options: DoughnutChartOptions = {}) {
|
|
114
|
-
this._id = id;
|
|
115
|
-
this.id = id;
|
|
116
|
-
|
|
117
|
-
const defaultColors = [
|
|
118
|
-
'#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
|
|
119
|
-
'#ec4899', '#06b6d4', '#f97316', '#84cc16', '#6366f1'
|
|
120
|
-
];
|
|
121
|
-
|
|
122
|
-
this.state = {
|
|
123
|
-
data: options.data ?? [],
|
|
124
|
-
title: options.title ?? '',
|
|
125
|
-
subtitle: options.subtitle ?? '',
|
|
126
|
-
xAxisLabel: options.xAxisLabel ?? '',
|
|
127
|
-
yAxisLabel: options.yAxisLabel ?? '',
|
|
128
|
-
showTicksX: options.showTicksX ?? true,
|
|
129
|
-
showTicksY: options.showTicksY ?? true,
|
|
130
|
-
showScaleX: options.showScaleX ?? true,
|
|
131
|
-
showScaleY: options.showScaleY ?? true,
|
|
132
|
-
scaleXUnit: options.scaleXUnit ?? '',
|
|
133
|
-
scaleYUnit: options.scaleYUnit ?? '',
|
|
134
|
-
showLegend: options.showLegend ?? false,
|
|
135
|
-
legendOrientation: options.legendOrientation ?? 'horizontal',
|
|
136
|
-
showDataTable: options.showDataTable ?? false,
|
|
137
|
-
showDataLabels: options.showDataLabels ?? true,
|
|
138
|
-
animate: options.animate ?? true,
|
|
139
|
-
animationDuration: options.animationDuration ?? 800,
|
|
140
|
-
chartOrientation: options.chartOrientation ?? 'vertical', // NEW
|
|
141
|
-
chartDirection: options.chartDirection ?? 'normal', // NEW
|
|
142
|
-
width: options.width ?? 600,
|
|
143
|
-
height: options.height ?? 400,
|
|
144
|
-
colors: options.colors ?? defaultColors,
|
|
145
|
-
class: options.class ?? '',
|
|
146
|
-
style: options.style ?? '',
|
|
147
|
-
theme: options.theme,
|
|
148
|
-
styleMode: options.styleMode ?? 'default',
|
|
149
|
-
borderRadius: options.borderRadius ?? 4
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/* -------------------------
|
|
154
|
-
* State Binding Methods
|
|
155
|
-
* ------------------------- */
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Bind theme to reactive state
|
|
159
|
-
*/
|
|
160
|
-
bindTheme(stateObj: State<string>): this {
|
|
161
|
-
this._boundTheme = stateObj;
|
|
162
|
-
|
|
163
|
-
stateObj.subscribe((val) => {
|
|
164
|
-
this.theme(val as any);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
return this;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Bind styleMode to reactive state
|
|
172
|
-
*/
|
|
173
|
-
bindStyleMode(stateObj: State<string>): this {
|
|
174
|
-
this._boundStyleMode = stateObj;
|
|
175
|
-
|
|
176
|
-
stateObj.subscribe((val) => {
|
|
177
|
-
this.styleMode(val as any);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
return this;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Bind borderRadius to reactive state
|
|
185
|
-
*/
|
|
186
|
-
bindBorderRadius(stateObj: State<number>): this {
|
|
187
|
-
this._boundBorderRadius = stateObj;
|
|
188
|
-
|
|
189
|
-
stateObj.subscribe((val) => {
|
|
190
|
-
this.borderRadius(val);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
return this;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/* -------------------------
|
|
197
|
-
* Fluent API
|
|
198
|
-
* ------------------------- */
|
|
199
|
-
|
|
200
|
-
data(value: DoughnutChartDataPoint[]): this {
|
|
201
|
-
this.state.data = value;
|
|
202
|
-
this._updateChart();
|
|
203
|
-
return this;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
title(value: string): this {
|
|
207
|
-
this.state.title = value;
|
|
208
|
-
this._updateChart();
|
|
209
|
-
return this;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
subtitle(value: string): this {
|
|
213
|
-
this.state.subtitle = value;
|
|
214
|
-
this._updateChart();
|
|
215
|
-
return this;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
xAxisLabel(value: string): this {
|
|
219
|
-
this.state.xAxisLabel = value;
|
|
220
|
-
this._updateChart();
|
|
221
|
-
return this;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
yAxisLabel(value: string): this {
|
|
225
|
-
this.state.yAxisLabel = value;
|
|
226
|
-
this._updateChart();
|
|
227
|
-
return this;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
showTicksX(value: boolean): this {
|
|
231
|
-
this.state.showTicksX = value;
|
|
232
|
-
this._updateChart();
|
|
233
|
-
return this;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
showTicksY(value: boolean): this {
|
|
237
|
-
this.state.showTicksY = value;
|
|
238
|
-
this._updateChart();
|
|
239
|
-
return this;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
showScaleX(value: boolean): this {
|
|
243
|
-
this.state.showScaleX = value;
|
|
244
|
-
this._updateChart();
|
|
245
|
-
return this;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
showScaleY(value: boolean): this {
|
|
249
|
-
this.state.showScaleY = value;
|
|
250
|
-
this._updateChart();
|
|
251
|
-
return this;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
scaleXUnit(value: string): this {
|
|
255
|
-
this.state.scaleXUnit = value;
|
|
256
|
-
this._updateChart();
|
|
257
|
-
return this;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
scaleYUnit(value: string): this {
|
|
261
|
-
this.state.scaleYUnit = value;
|
|
262
|
-
this._updateChart();
|
|
263
|
-
return this;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
showLegend(value: boolean): this {
|
|
267
|
-
this.state.showLegend = value;
|
|
268
|
-
this._updateChart();
|
|
269
|
-
return this;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
legendOrientation(value: 'horizontal' | 'vertical'): this {
|
|
273
|
-
this.state.legendOrientation = value;
|
|
274
|
-
this._updateChart();
|
|
275
|
-
return this;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
showDataTable(value: boolean): this {
|
|
279
|
-
this.state.showDataTable = value;
|
|
280
|
-
this._updateChart();
|
|
281
|
-
return this;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Show/hide value labels on bars
|
|
286
|
-
*/
|
|
287
|
-
showDataLabels(value: boolean): this {
|
|
288
|
-
this.state.showDataLabels = value;
|
|
289
|
-
this._updateChart();
|
|
290
|
-
return this;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Enable/disable bar grow animation
|
|
295
|
-
*/
|
|
296
|
-
animate(value: boolean): this {
|
|
297
|
-
this.state.animate = value;
|
|
298
|
-
this._updateChart();
|
|
299
|
-
return this;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Set animation duration in milliseconds
|
|
304
|
-
*/
|
|
305
|
-
animationDuration(value: number): this {
|
|
306
|
-
this.state.animationDuration = value;
|
|
307
|
-
this._updateChart();
|
|
308
|
-
return this;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Set chart orientation (vertical bars or horizontal bars)
|
|
313
|
-
*/
|
|
314
|
-
chartOrientation(value: 'vertical' | 'horizontal'): this {
|
|
315
|
-
this.state.chartOrientation = value;
|
|
316
|
-
this._updateChart();
|
|
317
|
-
return this;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Set chart direction (normal or reverse)
|
|
322
|
-
* For vertical: normal = bottom-to-top, reverse = top-to-bottom
|
|
323
|
-
* For horizontal: normal = left-to-right, reverse = right-to-left
|
|
324
|
-
*/
|
|
325
|
-
chartDirection(value: 'normal' | 'reverse'): this {
|
|
326
|
-
this.state.chartDirection = value;
|
|
327
|
-
this._updateChart();
|
|
328
|
-
return this;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
width(value: number): this {
|
|
332
|
-
this.state.width = value;
|
|
333
|
-
this._updateChart();
|
|
334
|
-
return this;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
height(value: number): this {
|
|
338
|
-
this.state.height = value;
|
|
339
|
-
this._updateChart();
|
|
340
|
-
return this;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
colors(value: string[]): this {
|
|
344
|
-
this.state.colors = value;
|
|
345
|
-
this._updateChart();
|
|
346
|
-
return this;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
class(value: string): this {
|
|
350
|
-
this.state.class = value;
|
|
351
|
-
return this;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
style(value: string): this {
|
|
355
|
-
this.state.style = value;
|
|
356
|
-
return this;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Set chart theme
|
|
361
|
-
*/
|
|
362
|
-
theme(value: 'google' | 'seriesa' | 'hr' | 'figma' | 'notion' | 'chalk' | 'mint'): this {
|
|
363
|
-
this.state.theme = value;
|
|
364
|
-
this._applyTheme(value);
|
|
365
|
-
this._updateChart();
|
|
366
|
-
return this;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Set bar style mode
|
|
371
|
-
*/
|
|
372
|
-
styleMode(value: 'default' | 'gradient' | 'outline' | 'dashed' | 'glow' | 'glass'): this {
|
|
373
|
-
this.state.styleMode = value;
|
|
374
|
-
this._updateChart();
|
|
375
|
-
return this;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Set border radius for bars (0 = sharp corners, higher = rounder)
|
|
380
|
-
*/
|
|
381
|
-
borderRadius(value: number): this {
|
|
382
|
-
this.state.borderRadius = value;
|
|
383
|
-
this._updateChart();
|
|
384
|
-
return this;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/* -------------------------
|
|
388
|
-
* Update chart
|
|
389
|
-
* ------------------------- */
|
|
390
|
-
|
|
391
|
-
private _updateChart(): void {
|
|
392
|
-
if (!this.container) return;
|
|
393
|
-
|
|
394
|
-
// Find the wrapper div
|
|
395
|
-
const wrapper = this.container.querySelector(`#${this._id}`) as HTMLElement;
|
|
396
|
-
if (!wrapper) return;
|
|
397
|
-
|
|
398
|
-
// Clear and rebuild
|
|
399
|
-
wrapper.innerHTML = '';
|
|
400
|
-
this._buildChart(wrapper);
|
|
401
|
-
|
|
402
|
-
// Reapply theme after rebuild
|
|
403
|
-
if (this.state.theme) {
|
|
404
|
-
this._applyThemeToWrapper(wrapper);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
private _buildChart(wrapper: HTMLElement): void {
|
|
409
|
-
const { data, title, subtitle, width, height, showLegend, showDataTable } = this.state;
|
|
410
|
-
|
|
411
|
-
// Title
|
|
412
|
-
if (title) {
|
|
413
|
-
const titleEl = document.createElement('h3');
|
|
414
|
-
titleEl.className = 'jux-doughnutchart-title';
|
|
415
|
-
titleEl.textContent = title;
|
|
416
|
-
wrapper.appendChild(titleEl);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Subtitle
|
|
420
|
-
if (subtitle) {
|
|
421
|
-
const subtitleEl = document.createElement('p');
|
|
422
|
-
subtitleEl.className = 'jux-doughnutchart-subtitle';
|
|
423
|
-
subtitleEl.textContent = subtitle;
|
|
424
|
-
wrapper.appendChild(subtitleEl);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// SVG Chart
|
|
428
|
-
const svg = this._createSVG();
|
|
429
|
-
wrapper.appendChild(svg);
|
|
430
|
-
|
|
431
|
-
// Legend
|
|
432
|
-
if (showLegend) {
|
|
433
|
-
const legend = this._createLegend();
|
|
434
|
-
wrapper.appendChild(legend);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Data Table
|
|
438
|
-
if (showDataTable) {
|
|
439
|
-
const table = this._createDataTable();
|
|
440
|
-
wrapper.appendChild(table);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
private _createSVG(): SVGSVGElement {
|
|
445
|
-
const {
|
|
446
|
-
data, width, height, colors,
|
|
447
|
-
styleMode, showDataLabels, animate, animationDuration
|
|
448
|
-
} = this.state;
|
|
449
|
-
|
|
450
|
-
if (!data.length) {
|
|
451
|
-
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
452
|
-
svg.setAttribute('width', width.toString());
|
|
453
|
-
svg.setAttribute('height', height.toString());
|
|
454
|
-
svg.setAttribute('class', 'jux-doughnutchart-svg');
|
|
455
|
-
return svg;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
459
|
-
svg.setAttribute('width', width.toString());
|
|
460
|
-
svg.setAttribute('height', height.toString());
|
|
461
|
-
svg.setAttribute('class', 'jux-doughnutchart-svg');
|
|
462
|
-
|
|
463
|
-
// Add animation styles to SVG
|
|
464
|
-
if (animate) {
|
|
465
|
-
const animationId = `slice-scale-${this._id}`;
|
|
466
|
-
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
|
467
|
-
|
|
468
|
-
style.textContent = `
|
|
469
|
-
@keyframes ${animationId} {
|
|
470
|
-
from {
|
|
471
|
-
transform: scale(0);
|
|
472
|
-
opacity: 0;
|
|
473
|
-
}
|
|
474
|
-
to {
|
|
475
|
-
transform: scale(1);
|
|
476
|
-
opacity: 1;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
.jux-slice-animated {
|
|
480
|
-
transform-origin: ${width / 2}px ${height / 2}px;
|
|
481
|
-
animation: ${animationId} ${animationDuration}ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
|
482
|
-
opacity: 0;
|
|
483
|
-
}
|
|
484
|
-
.jux-label-animated {
|
|
485
|
-
opacity: 0;
|
|
486
|
-
animation: fadeIn 400ms ease-out forwards;
|
|
487
|
-
}
|
|
488
|
-
@keyframes fadeIn {
|
|
489
|
-
from { opacity: 0; transform: scale(0.8); }
|
|
490
|
-
to { opacity: 1; transform: scale(1); }
|
|
491
|
-
}
|
|
492
|
-
`;
|
|
493
|
-
svg.appendChild(style);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Calculate dimensions - adjust margin based on whether labels are shown
|
|
497
|
-
const centerX = width / 2;
|
|
498
|
-
const centerY = height / 2;
|
|
499
|
-
const margin = showDataLabels ? 100 : 40; // Less margin when no labels
|
|
500
|
-
const radius = Math.min(width, height) / 2 - margin;
|
|
501
|
-
const innerRadius = radius * 0.6; // Inner radius for doughnut hole
|
|
502
|
-
|
|
503
|
-
// Calculate total value
|
|
504
|
-
const total = data.reduce((sum, point) => sum + point.value, 0);
|
|
505
|
-
|
|
506
|
-
// Render slices
|
|
507
|
-
this._renderDoughnutSlices(svg, data, colors, centerX, centerY, radius, innerRadius, total);
|
|
508
|
-
|
|
509
|
-
return svg;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
private _renderDoughnutSlices(
|
|
513
|
-
svg: SVGSVGElement,
|
|
514
|
-
data: DoughnutChartDataPoint[],
|
|
515
|
-
colors: string[],
|
|
516
|
-
centerX: number,
|
|
517
|
-
centerY: number,
|
|
518
|
-
radius: number,
|
|
519
|
-
innerRadius: number,
|
|
520
|
-
total: number
|
|
521
|
-
): void {
|
|
522
|
-
const { styleMode, showDataLabels, animate } = this.state;
|
|
523
|
-
|
|
524
|
-
let currentAngle = -90; // Start at top (12 o'clock)
|
|
525
|
-
|
|
526
|
-
data.forEach((point, index) => {
|
|
527
|
-
const color = point.color || colors[index % colors.length];
|
|
528
|
-
const percentage = (point.value / total) * 100;
|
|
529
|
-
const sliceAngle = (point.value / total) * 360;
|
|
530
|
-
|
|
531
|
-
// Create slice path
|
|
532
|
-
const path = this._createDoughnutSlice(
|
|
533
|
-
centerX,
|
|
534
|
-
centerY,
|
|
535
|
-
radius,
|
|
536
|
-
innerRadius,
|
|
537
|
-
currentAngle,
|
|
538
|
-
currentAngle + sliceAngle
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
this._renderSlice(
|
|
542
|
-
svg,
|
|
543
|
-
path,
|
|
544
|
-
color,
|
|
545
|
-
index,
|
|
546
|
-
point,
|
|
547
|
-
centerX,
|
|
548
|
-
centerY,
|
|
549
|
-
radius,
|
|
550
|
-
innerRadius,
|
|
551
|
-
currentAngle,
|
|
552
|
-
sliceAngle,
|
|
553
|
-
percentage
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
currentAngle += sliceAngle;
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
// Add center circle for clean doughnut hole
|
|
560
|
-
const centerCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
561
|
-
centerCircle.setAttribute('cx', centerX.toString());
|
|
562
|
-
centerCircle.setAttribute('cy', centerY.toString());
|
|
563
|
-
centerCircle.setAttribute('r', innerRadius.toString());
|
|
564
|
-
centerCircle.setAttribute('fill', 'white');
|
|
565
|
-
svg.appendChild(centerCircle);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
private _createDoughnutSlice(
|
|
569
|
-
centerX: number,
|
|
570
|
-
centerY: number,
|
|
571
|
-
outerRadius: number,
|
|
572
|
-
innerRadius: number,
|
|
573
|
-
startAngle: number,
|
|
574
|
-
endAngle: number
|
|
575
|
-
): string {
|
|
576
|
-
// Convert angles to radians
|
|
577
|
-
const startRad = (startAngle * Math.PI) / 180;
|
|
578
|
-
const endRad = (endAngle * Math.PI) / 180;
|
|
579
|
-
|
|
580
|
-
// Calculate outer arc points
|
|
581
|
-
const x1 = centerX + outerRadius * Math.cos(startRad);
|
|
582
|
-
const y1 = centerY + outerRadius * Math.sin(startRad);
|
|
583
|
-
const x2 = centerX + outerRadius * Math.cos(endRad);
|
|
584
|
-
const y2 = centerY + outerRadius * Math.sin(endRad);
|
|
585
|
-
|
|
586
|
-
// Calculate inner arc points
|
|
587
|
-
const x3 = centerX + innerRadius * Math.cos(endRad);
|
|
588
|
-
const y3 = centerY + innerRadius * Math.sin(endRad);
|
|
589
|
-
const x4 = centerX + innerRadius * Math.cos(startRad);
|
|
590
|
-
const y4 = centerY + innerRadius * Math.sin(startRad);
|
|
591
|
-
|
|
592
|
-
// Determine if we need a large arc (> 180 degrees)
|
|
593
|
-
const largeArcFlag = endAngle - startAngle > 180 ? 1 : 0;
|
|
594
|
-
|
|
595
|
-
// Build the path
|
|
596
|
-
const path = [
|
|
597
|
-
`M ${x1} ${y1}`, // Move to start of outer arc
|
|
598
|
-
`A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${x2} ${y2}`, // Outer arc
|
|
599
|
-
`L ${x3} ${y3}`, // Line to start of inner arc
|
|
600
|
-
`A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${x4} ${y4}`, // Inner arc (reverse)
|
|
601
|
-
'Z' // Close path
|
|
602
|
-
].join(' ');
|
|
603
|
-
|
|
604
|
-
return path;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
private _renderSlice(
|
|
608
|
-
svg: SVGSVGElement,
|
|
609
|
-
pathData: string,
|
|
610
|
-
color: string,
|
|
611
|
-
index: number,
|
|
612
|
-
point: DoughnutChartDataPoint,
|
|
613
|
-
centerX: number,
|
|
614
|
-
centerY: number,
|
|
615
|
-
radius: number,
|
|
616
|
-
innerRadius: number,
|
|
617
|
-
startAngle: number,
|
|
618
|
-
sliceAngle: number,
|
|
619
|
-
percentage: number
|
|
620
|
-
): void {
|
|
621
|
-
const { styleMode, showDataLabels, animate, animationDuration } = this.state;
|
|
622
|
-
|
|
623
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
624
|
-
path.setAttribute('d', pathData);
|
|
625
|
-
|
|
626
|
-
if (animate) {
|
|
627
|
-
path.classList.add('jux-slice-animated');
|
|
628
|
-
path.style.animationDelay = `${index * 150}ms`;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// Apply style modes
|
|
632
|
-
if (styleMode === 'gradient') {
|
|
633
|
-
const gradientId = `slice-gradient-${this._id}-${index}`;
|
|
634
|
-
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'radialGradient');
|
|
635
|
-
gradient.setAttribute('id', gradientId);
|
|
636
|
-
|
|
637
|
-
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
|
638
|
-
stop1.setAttribute('offset', '0%');
|
|
639
|
-
stop1.setAttribute('stop-color', this._lightenColor(color, 30));
|
|
640
|
-
|
|
641
|
-
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
|
642
|
-
stop2.setAttribute('offset', '100%');
|
|
643
|
-
stop2.setAttribute('stop-color', color);
|
|
644
|
-
|
|
645
|
-
gradient.appendChild(stop1);
|
|
646
|
-
gradient.appendChild(stop2);
|
|
647
|
-
svg.appendChild(gradient);
|
|
648
|
-
|
|
649
|
-
path.setAttribute('fill', `url(#${gradientId})`);
|
|
650
|
-
} else if (styleMode === 'outline') {
|
|
651
|
-
path.setAttribute('fill', 'transparent');
|
|
652
|
-
path.setAttribute('stroke', color);
|
|
653
|
-
path.setAttribute('stroke-width', '3');
|
|
654
|
-
} else if (styleMode === 'dashed') {
|
|
655
|
-
path.setAttribute('fill', this._lightenColor(color, 60));
|
|
656
|
-
path.setAttribute('stroke', color);
|
|
657
|
-
path.setAttribute('stroke-width', '2');
|
|
658
|
-
path.setAttribute('stroke-dasharray', '8,4');
|
|
659
|
-
} else if (styleMode === 'glow') {
|
|
660
|
-
path.setAttribute('fill', color);
|
|
661
|
-
|
|
662
|
-
const filterId = `glow-slice-${this._id}-${index}`;
|
|
663
|
-
const defs = svg.querySelector('defs') || document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
664
|
-
if (!svg.querySelector('defs')) {
|
|
665
|
-
svg.insertBefore(defs, svg.firstChild);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const filter = document.createElementNS('http://www.w3.org/2000/svg', 'filter');
|
|
669
|
-
filter.setAttribute('id', filterId);
|
|
670
|
-
filter.setAttribute('x', '-50%');
|
|
671
|
-
filter.setAttribute('y', '-50%');
|
|
672
|
-
filter.setAttribute('width', '200%');
|
|
673
|
-
filter.setAttribute('height', '200%');
|
|
674
|
-
|
|
675
|
-
const feGaussianBlur = document.createElementNS('http://www.w3.org/2000/svg', 'feGaussianBlur');
|
|
676
|
-
feGaussianBlur.setAttribute('in', 'SourceGraphic');
|
|
677
|
-
feGaussianBlur.setAttribute('stdDeviation', '4');
|
|
678
|
-
filter.appendChild(feGaussianBlur);
|
|
679
|
-
defs.appendChild(filter);
|
|
680
|
-
|
|
681
|
-
path.setAttribute('filter', `url(#${filterId})`);
|
|
682
|
-
} else if (styleMode === 'glass') {
|
|
683
|
-
path.setAttribute('fill', color);
|
|
684
|
-
path.setAttribute('fill-opacity', '0.7');
|
|
685
|
-
path.setAttribute('stroke', color);
|
|
686
|
-
path.setAttribute('stroke-width', '2');
|
|
687
|
-
path.setAttribute('stroke-opacity', '0.9');
|
|
688
|
-
} else {
|
|
689
|
-
path.setAttribute('fill', color);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Add stroke for separation
|
|
693
|
-
if (styleMode !== 'outline') {
|
|
694
|
-
path.setAttribute('stroke', 'white');
|
|
695
|
-
path.setAttribute('stroke-width', '2');
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
svg.appendChild(path);
|
|
699
|
-
|
|
700
|
-
// Add external labels with percentage bubbles
|
|
701
|
-
if (showDataLabels) {
|
|
702
|
-
const middleAngle = startAngle + (sliceAngle / 2);
|
|
703
|
-
const middleRad = (middleAngle * Math.PI) / 180;
|
|
704
|
-
|
|
705
|
-
// Line start (from outer edge)
|
|
706
|
-
const lineStartRadius = radius + 5;
|
|
707
|
-
const lineStartX = centerX + lineStartRadius * Math.cos(middleRad);
|
|
708
|
-
const lineStartY = centerY + lineStartRadius * Math.sin(middleRad);
|
|
709
|
-
|
|
710
|
-
// Bubble position (along the line)
|
|
711
|
-
const bubbleRadius = radius + 45;
|
|
712
|
-
const bubbleX = centerX + bubbleRadius * Math.cos(middleRad);
|
|
713
|
-
const bubbleY = centerY + bubbleRadius * Math.sin(middleRad);
|
|
714
|
-
|
|
715
|
-
// Label position (beyond the bubble)
|
|
716
|
-
const labelDistance = radius + 80;
|
|
717
|
-
const externalLabelX = centerX + labelDistance * Math.cos(middleRad);
|
|
718
|
-
const externalLabelY = centerY + labelDistance * Math.sin(middleRad);
|
|
719
|
-
|
|
720
|
-
const lineGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
721
|
-
|
|
722
|
-
if (animate) {
|
|
723
|
-
lineGroup.classList.add('jux-label-animated');
|
|
724
|
-
lineGroup.style.animationDelay = `${index * 150 + animationDuration + 100}ms`;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Line from slice to bubble
|
|
728
|
-
const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
729
|
-
line1.setAttribute('x1', lineStartX.toString());
|
|
730
|
-
line1.setAttribute('y1', lineStartY.toString());
|
|
731
|
-
line1.setAttribute('x2', bubbleX.toString());
|
|
732
|
-
line1.setAttribute('y2', bubbleY.toString());
|
|
733
|
-
line1.setAttribute('stroke', color);
|
|
734
|
-
line1.setAttribute('stroke-width', '2');
|
|
735
|
-
line1.setAttribute('opacity', '0.8');
|
|
736
|
-
lineGroup.appendChild(line1);
|
|
737
|
-
|
|
738
|
-
// Line from bubble to label
|
|
739
|
-
const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
740
|
-
line2.setAttribute('x1', bubbleX.toString());
|
|
741
|
-
line2.setAttribute('y1', bubbleY.toString());
|
|
742
|
-
line2.setAttribute('x2', externalLabelX.toString());
|
|
743
|
-
line2.setAttribute('y2', externalLabelY.toString());
|
|
744
|
-
line2.setAttribute('stroke', color);
|
|
745
|
-
line2.setAttribute('stroke-width', '2');
|
|
746
|
-
line2.setAttribute('opacity', '0.8');
|
|
747
|
-
lineGroup.appendChild(line2);
|
|
748
|
-
|
|
749
|
-
// Percentage bubble
|
|
750
|
-
const bubbleGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
751
|
-
|
|
752
|
-
// Shadow circle for depth
|
|
753
|
-
const shadowCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
754
|
-
shadowCircle.setAttribute('cx', bubbleX.toString());
|
|
755
|
-
shadowCircle.setAttribute('cy', bubbleY.toString());
|
|
756
|
-
shadowCircle.setAttribute('r', '32');
|
|
757
|
-
shadowCircle.setAttribute('fill', 'rgba(0, 0, 0, 0.15)');
|
|
758
|
-
shadowCircle.setAttribute('filter', 'blur(4px)');
|
|
759
|
-
bubbleGroup.appendChild(shadowCircle);
|
|
760
|
-
|
|
761
|
-
// White background circle
|
|
762
|
-
const whiteBg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
763
|
-
whiteBg.setAttribute('cx', bubbleX.toString());
|
|
764
|
-
whiteBg.setAttribute('cy', bubbleY.toString());
|
|
765
|
-
whiteBg.setAttribute('r', '30');
|
|
766
|
-
whiteBg.setAttribute('fill', 'white');
|
|
767
|
-
whiteBg.setAttribute('stroke', color);
|
|
768
|
-
whiteBg.setAttribute('stroke-width', '3');
|
|
769
|
-
bubbleGroup.appendChild(whiteBg);
|
|
770
|
-
|
|
771
|
-
// Percentage label
|
|
772
|
-
const percentLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
773
|
-
percentLabel.setAttribute('x', bubbleX.toString());
|
|
774
|
-
percentLabel.setAttribute('y', (bubbleY - 3).toString());
|
|
775
|
-
percentLabel.setAttribute('text-anchor', 'middle');
|
|
776
|
-
percentLabel.setAttribute('dominant-baseline', 'middle');
|
|
777
|
-
percentLabel.setAttribute('fill', color);
|
|
778
|
-
percentLabel.setAttribute('font-size', '14');
|
|
779
|
-
percentLabel.setAttribute('font-weight', '800');
|
|
780
|
-
percentLabel.setAttribute('font-family', 'inherit');
|
|
781
|
-
percentLabel.textContent = `${percentage.toFixed(1)}%`;
|
|
782
|
-
bubbleGroup.appendChild(percentLabel);
|
|
783
|
-
|
|
784
|
-
// Value label below percentage
|
|
785
|
-
const valueLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
786
|
-
valueLabel.setAttribute('x', bubbleX.toString());
|
|
787
|
-
valueLabel.setAttribute('y', (bubbleY + 11).toString());
|
|
788
|
-
valueLabel.setAttribute('text-anchor', 'middle');
|
|
789
|
-
valueLabel.setAttribute('dominant-baseline', 'middle');
|
|
790
|
-
valueLabel.setAttribute('fill', '#6b7280');
|
|
791
|
-
valueLabel.setAttribute('font-size', '10');
|
|
792
|
-
valueLabel.setAttribute('font-weight', '600');
|
|
793
|
-
valueLabel.setAttribute('font-family', 'inherit');
|
|
794
|
-
valueLabel.textContent = point.value.toString();
|
|
795
|
-
bubbleGroup.appendChild(valueLabel);
|
|
796
|
-
|
|
797
|
-
lineGroup.appendChild(bubbleGroup);
|
|
798
|
-
|
|
799
|
-
// Category label at the end
|
|
800
|
-
const externalLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
801
|
-
externalLabel.setAttribute('x', externalLabelX.toString());
|
|
802
|
-
externalLabel.setAttribute('y', externalLabelY.toString());
|
|
803
|
-
|
|
804
|
-
// Anchor based on which side of the circle
|
|
805
|
-
const anchor = middleAngle > -90 && middleAngle < 90 ? 'start' : 'end';
|
|
806
|
-
externalLabel.setAttribute('text-anchor', anchor);
|
|
807
|
-
externalLabel.setAttribute('dominant-baseline', 'middle');
|
|
808
|
-
externalLabel.setAttribute('fill', '#374151');
|
|
809
|
-
externalLabel.setAttribute('font-size', '13');
|
|
810
|
-
externalLabel.setAttribute('font-weight', '600');
|
|
811
|
-
externalLabel.setAttribute('font-family', 'inherit');
|
|
812
|
-
externalLabel.textContent = point.label;
|
|
813
|
-
lineGroup.appendChild(externalLabel);
|
|
814
|
-
|
|
815
|
-
svg.appendChild(lineGroup);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Add hover effect
|
|
819
|
-
path.style.cursor = 'pointer';
|
|
820
|
-
path.style.transition = 'transform 0.2s, opacity 0.2s';
|
|
821
|
-
|
|
822
|
-
path.addEventListener('mouseenter', () => {
|
|
823
|
-
path.style.transform = 'scale(1.05)';
|
|
824
|
-
path.style.opacity = '0.9';
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
path.addEventListener('mouseleave', () => {
|
|
828
|
-
path.style.transform = 'scale(1)';
|
|
829
|
-
path.style.opacity = '1';
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// Remove the old bar rendering methods
|
|
834
|
-
private _renderVerticalBars(): void {
|
|
835
|
-
// Not used for doughnut chart
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
private _renderHorizontalBars(): void {
|
|
839
|
-
// Not used for doughnut chart
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
private _renderBar(): void {
|
|
843
|
-
// Not used for doughnut chart
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
/* -------------------------
|
|
847
|
-
* Legend and Data Table
|
|
848
|
-
* ------------------------- */
|
|
849
|
-
|
|
850
|
-
private _createLegend(): HTMLElement {
|
|
851
|
-
const { data, colors, legendOrientation } = this.state;
|
|
852
|
-
|
|
853
|
-
const legend = document.createElement('div');
|
|
854
|
-
legend.className = 'jux-doughnutchart-legend';
|
|
855
|
-
|
|
856
|
-
data.forEach((point, index) => {
|
|
857
|
-
const color = point.color || colors[index % colors.length];
|
|
858
|
-
|
|
859
|
-
const item = document.createElement('div');
|
|
860
|
-
item.className = 'jux-doughnutchart-legend-item';
|
|
861
|
-
|
|
862
|
-
const swatch = document.createElement('div');
|
|
863
|
-
swatch.className = 'jux-doughnutchart-legend-swatch';
|
|
864
|
-
swatch.style.background = color;
|
|
865
|
-
|
|
866
|
-
const label = document.createElement('span');
|
|
867
|
-
label.className = 'jux-doughnutchart-legend-label';
|
|
868
|
-
label.textContent = point.label;
|
|
869
|
-
|
|
870
|
-
item.appendChild(swatch);
|
|
871
|
-
item.appendChild(label);
|
|
872
|
-
legend.appendChild(item);
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
return legend;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
private _createDataTable(): HTMLElement {
|
|
879
|
-
const { data, xAxisLabel, yAxisLabel, colors } = this.state;
|
|
880
|
-
|
|
881
|
-
// Calculate total for percentages
|
|
882
|
-
const total = data.reduce((sum, point) => sum + point.value, 0);
|
|
883
|
-
|
|
884
|
-
const table = document.createElement('table');
|
|
885
|
-
table.className = 'jux-doughnutchart-table';
|
|
886
|
-
|
|
887
|
-
const thead = document.createElement('thead');
|
|
888
|
-
const headerRow = document.createElement('tr');
|
|
889
|
-
|
|
890
|
-
const columnHeaders = [
|
|
891
|
-
xAxisLabel || 'Month',
|
|
892
|
-
yAxisLabel || 'Revenue ($)',
|
|
893
|
-
'Percentage'
|
|
894
|
-
];
|
|
895
|
-
|
|
896
|
-
columnHeaders.forEach(text => {
|
|
897
|
-
const th = document.createElement('th');
|
|
898
|
-
th.textContent = text;
|
|
899
|
-
headerRow.appendChild(th);
|
|
900
|
-
});
|
|
901
|
-
thead.appendChild(headerRow);
|
|
902
|
-
table.appendChild(thead);
|
|
903
|
-
|
|
904
|
-
const tbody = document.createElement('tbody');
|
|
905
|
-
data.forEach((point, index) => {
|
|
906
|
-
const row = document.createElement('tr');
|
|
907
|
-
const color = point.color || colors[index % colors.length];
|
|
908
|
-
const percentage = (point.value / total) * 100;
|
|
909
|
-
|
|
910
|
-
// Label cell with color indicator
|
|
911
|
-
const labelCell = document.createElement('td');
|
|
912
|
-
labelCell.style.cssText = `
|
|
913
|
-
display: flex;
|
|
914
|
-
align-items: center;
|
|
915
|
-
gap: 8px;
|
|
916
|
-
`;
|
|
917
|
-
|
|
918
|
-
const colorSwatch = document.createElement('div');
|
|
919
|
-
colorSwatch.style.cssText = `
|
|
920
|
-
width: 16px;
|
|
921
|
-
height: 16px;
|
|
922
|
-
border-radius: 3px;
|
|
923
|
-
background: ${color};
|
|
924
|
-
flex-shrink: 0;
|
|
925
|
-
`;
|
|
926
|
-
labelCell.appendChild(colorSwatch);
|
|
927
|
-
|
|
928
|
-
const labelText = document.createElement('span');
|
|
929
|
-
labelText.textContent = point.label;
|
|
930
|
-
labelCell.appendChild(labelText);
|
|
931
|
-
|
|
932
|
-
// Value cell
|
|
933
|
-
const valueCell = document.createElement('td');
|
|
934
|
-
valueCell.textContent = point.value.toString();
|
|
935
|
-
|
|
936
|
-
// Percentage cell with colored border
|
|
937
|
-
const percentCell = document.createElement('td');
|
|
938
|
-
const percentBadge = document.createElement('span');
|
|
939
|
-
percentBadge.textContent = `${percentage.toFixed(1)}%`;
|
|
940
|
-
percentBadge.style.cssText = `
|
|
941
|
-
display: inline-block;
|
|
942
|
-
padding: 4px 12px;
|
|
943
|
-
border-radius: 12px;
|
|
944
|
-
border: 2px solid ${color};
|
|
945
|
-
color: ${color};
|
|
946
|
-
font-weight: 700;
|
|
947
|
-
font-size: 13px;
|
|
948
|
-
background: ${this._lightenColor(color, 90)};
|
|
949
|
-
`;
|
|
950
|
-
percentCell.appendChild(percentBadge);
|
|
951
|
-
|
|
952
|
-
row.appendChild(labelCell);
|
|
953
|
-
row.appendChild(valueCell);
|
|
954
|
-
row.appendChild(percentCell);
|
|
955
|
-
tbody.appendChild(row);
|
|
956
|
-
});
|
|
957
|
-
table.appendChild(tbody);
|
|
958
|
-
|
|
959
|
-
return table;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
private _lightenColor(color: string, percent: number): string {
|
|
963
|
-
const num = parseInt(color.replace('#', ''), 16);
|
|
964
|
-
const r = Math.min(255, Math.floor((num >> 16) + ((255 - (num >> 16)) * percent / 100)));
|
|
965
|
-
const g = Math.min(255, Math.floor(((num >> 8) & 0x00FF) + ((255 - ((num >> 8) & 0x00FF)) * percent / 100)));
|
|
966
|
-
const b = Math.min(255, Math.floor((num & 0x0000FF) + ((255 - (num & 0x0000FF)) * percent / 100)));
|
|
967
|
-
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
private _applyTheme(themeName: string): void {
|
|
971
|
-
const themes: Record<string, any> = {
|
|
972
|
-
google: googleTheme,
|
|
973
|
-
seriesa: seriesaTheme,
|
|
974
|
-
hr: hrTheme,
|
|
975
|
-
figma: figmaTheme,
|
|
976
|
-
notion: notionTheme,
|
|
977
|
-
chalk: chalkTheme,
|
|
978
|
-
mint: mintTheme
|
|
979
|
-
};
|
|
980
|
-
|
|
981
|
-
const theme = themes[themeName];
|
|
982
|
-
if (!theme) return;
|
|
983
|
-
|
|
984
|
-
// Apply colors
|
|
985
|
-
this.state.colors = theme.colors;
|
|
986
|
-
|
|
987
|
-
// Inject base styles (once)
|
|
988
|
-
const baseStyleId = 'jux-doughnutchart-base-styles';
|
|
989
|
-
if (!document.getElementById(baseStyleId)) {
|
|
990
|
-
const style = document.createElement('style');
|
|
991
|
-
style.id = baseStyleId;
|
|
992
|
-
style.textContent = this._getBaseStyles();
|
|
993
|
-
document.head.appendChild(style);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// Inject font (once per theme)
|
|
997
|
-
if (theme.font && !document.querySelector(`link[href="${theme.font}"]`)) {
|
|
998
|
-
const link = document.createElement('link');
|
|
999
|
-
link.rel = 'stylesheet';
|
|
1000
|
-
link.href = theme.font;
|
|
1001
|
-
document.head.appendChild(link);
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// Apply theme-specific styles
|
|
1005
|
-
const styleId = `jux-doughnutchart-theme-${themeName}`;
|
|
1006
|
-
let styleElement = document.getElementById(styleId) as HTMLStyleElement;
|
|
1007
|
-
|
|
1008
|
-
if (!styleElement) {
|
|
1009
|
-
styleElement = document.createElement('style');
|
|
1010
|
-
styleElement.id = styleId;
|
|
1011
|
-
document.head.appendChild(styleElement);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Generate CSS with theme variables
|
|
1015
|
-
const variablesCSS = Object.entries(theme.variables)
|
|
1016
|
-
.map(([key, value]) => ` ${key}: ${value};`)
|
|
1017
|
-
.join('\n');
|
|
1018
|
-
|
|
1019
|
-
styleElement.textContent = `
|
|
1020
|
-
.jux-doughnutchart.theme-${themeName} {
|
|
1021
|
-
${variablesCSS}
|
|
1022
|
-
}
|
|
1023
|
-
`;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
private _applyThemeToWrapper(wrapper: HTMLElement): void {
|
|
1027
|
-
if (!this.state.theme) return;
|
|
1028
|
-
|
|
1029
|
-
// Remove old theme classes
|
|
1030
|
-
wrapper.classList.remove('theme-google', 'theme-seriesa', 'theme-hr', 'theme-figma', 'theme-notion', 'theme-chalk', 'theme-mint');
|
|
1031
|
-
|
|
1032
|
-
// Add new theme class
|
|
1033
|
-
wrapper.classList.add(`theme-${this.state.theme}`);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
private _getBaseStyles(): string {
|
|
1037
|
-
return `
|
|
1038
|
-
.jux-doughnutchart {
|
|
1039
|
-
font-family: var(--chart-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
|
1040
|
-
display: inline-block;
|
|
1041
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
1042
|
-
border-radius: 12px;
|
|
1043
|
-
background: white;
|
|
1044
|
-
padding: 24px;
|
|
1045
|
-
}
|
|
1046
|
-
.jux-doughnutchart.flat{
|
|
1047
|
-
box-shadow: none;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
.jux-doughnutchart-title {
|
|
1051
|
-
margin: 0 0 0.5rem 0;
|
|
1052
|
-
font-size: 1.25rem;
|
|
1053
|
-
font-weight: 600;
|
|
1054
|
-
font-family: inherit;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
.jux-doughnutchart-subtitle {
|
|
1058
|
-
margin: 0 0 1rem 0;
|
|
1059
|
-
font-size: 0.875rem;
|
|
1060
|
-
color: #6b7280;
|
|
1061
|
-
font-family: inherit;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
.jux-doughnutchart-legend {
|
|
1065
|
-
display: flex;
|
|
1066
|
-
flex-wrap: wrap;
|
|
1067
|
-
gap: 1rem;
|
|
1068
|
-
margin-top: 1rem;
|
|
1069
|
-
justify-content: center;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
.jux-doughnutchart-legend-item {
|
|
1073
|
-
display: flex;
|
|
1074
|
-
align-items: center;
|
|
1075
|
-
gap: 0.5rem;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
.jux-doughnutchart-legend-swatch {
|
|
1079
|
-
width: 12px;
|
|
1080
|
-
height: 12px;
|
|
1081
|
-
border-radius: 2px;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
.jux-doughnutchart-legend-label {
|
|
1085
|
-
font-size: 0.875rem;
|
|
1086
|
-
color: #374151;
|
|
1087
|
-
font-family: inherit;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
.jux-doughnutchart-table {
|
|
1091
|
-
width: 100%;
|
|
1092
|
-
margin-top: 1.5rem;
|
|
1093
|
-
border-collapse: collapse;
|
|
1094
|
-
font-size: 0.875rem;
|
|
1095
|
-
font-family: inherit;
|
|
1096
|
-
border: 1px solid #e5e7eb;
|
|
1097
|
-
border-radius: 8px;
|
|
1098
|
-
overflow: hidden;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
.jux-doughnutchart-table thead {
|
|
1102
|
-
background: #f9fafb;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
.jux-doughnutchart-table thead th {
|
|
1106
|
-
text-align: center;
|
|
1107
|
-
padding: 12px 16px;
|
|
1108
|
-
border-bottom: 2px solid #e5e7eb;
|
|
1109
|
-
font-weight: 600;
|
|
1110
|
-
color: #374151;
|
|
1111
|
-
font-size: 13px;
|
|
1112
|
-
text-transform: uppercase;
|
|
1113
|
-
letter-spacing: 0.5px;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
.jux-doughnutchart-table tbody td {
|
|
1117
|
-
padding: 12px 16px;
|
|
1118
|
-
border-bottom: 1px solid #f3f4f6;
|
|
1119
|
-
text-align: center;
|
|
1120
|
-
vertical-align: middle;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
.jux-doughnutchart-table tbody tr:last-child td {
|
|
1124
|
-
border-bottom: none;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
.jux-doughnutchart-table tbody tr:hover {
|
|
1128
|
-
background: #f9fafb;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
.jux-doughnutchart-svg {
|
|
1132
|
-
font-family: inherit;
|
|
1133
|
-
}
|
|
1134
|
-
`;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
render(targetId?: string | HTMLElement): this {
|
|
1138
|
-
// Apply theme first if set
|
|
1139
|
-
if (this.state.theme) {
|
|
1140
|
-
this._applyTheme(this.state.theme);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
let container: HTMLElement;
|
|
1144
|
-
|
|
1145
|
-
if (targetId) {
|
|
1146
|
-
if (targetId instanceof HTMLElement) {
|
|
1147
|
-
container = targetId;
|
|
1148
|
-
} else {
|
|
1149
|
-
const target = document.querySelector(targetId);
|
|
1150
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
1151
|
-
throw new Error(`DoughnutChart: Target element "${targetId}" not found`);
|
|
1152
|
-
}
|
|
1153
|
-
container = target;
|
|
1154
|
-
}
|
|
1155
|
-
} else {
|
|
1156
|
-
container = getOrCreateContainer(this._id);
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
this.container = container;
|
|
1160
|
-
const { class: className, style } = this.state;
|
|
1161
|
-
|
|
1162
|
-
const wrapper = document.createElement('div');
|
|
1163
|
-
wrapper.id = this._id;
|
|
1164
|
-
wrapper.className = 'jux-doughnutchart';
|
|
1165
|
-
|
|
1166
|
-
// Add theme class
|
|
1167
|
-
if (this.state.theme) {
|
|
1168
|
-
wrapper.classList.add(`theme-${this.state.theme}`);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
// Add custom class
|
|
1172
|
-
if (className) {
|
|
1173
|
-
wrapper.classList.add(...className.split(' '));
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
if (style) {
|
|
1177
|
-
wrapper.setAttribute('style', style);
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
container.appendChild(wrapper);
|
|
1181
|
-
|
|
1182
|
-
// Build chart content
|
|
1183
|
-
this._buildChart(wrapper);
|
|
1184
|
-
|
|
1185
|
-
return this;
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
export function doughnutchart(id: string, options: DoughnutChartOptions = {}): DoughnutChart {
|
|
1190
|
-
return new DoughnutChart(id, options);
|
|
1191
|
-
}
|