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.
Files changed (77) hide show
  1. package/bin/cli.js +121 -72
  2. package/lib/components/alert.ts +212 -165
  3. package/lib/components/badge.ts +93 -103
  4. package/lib/components/base/BaseComponent.ts +397 -0
  5. package/lib/components/base/FormInput.ts +322 -0
  6. package/lib/components/button.ts +63 -122
  7. package/lib/components/card.ts +109 -155
  8. package/lib/components/charts/areachart.ts +315 -0
  9. package/lib/components/charts/barchart.ts +421 -0
  10. package/lib/components/charts/doughnutchart.ts +263 -0
  11. package/lib/components/charts/lib/BaseChart.ts +402 -0
  12. package/lib/components/charts/lib/chart-types.ts +159 -0
  13. package/lib/components/charts/lib/chart-utils.ts +160 -0
  14. package/lib/components/charts/lib/chart.ts +707 -0
  15. package/lib/components/checkbox.ts +264 -127
  16. package/lib/components/code.ts +75 -108
  17. package/lib/components/container.ts +113 -130
  18. package/lib/components/data.ts +37 -5
  19. package/lib/components/datepicker.ts +195 -147
  20. package/lib/components/dialog.ts +187 -157
  21. package/lib/components/divider.ts +85 -191
  22. package/lib/components/docs-data.json +544 -2027
  23. package/lib/components/dropdown.ts +178 -136
  24. package/lib/components/element.ts +227 -171
  25. package/lib/components/fileupload.ts +285 -228
  26. package/lib/components/guard.ts +92 -0
  27. package/lib/components/heading.ts +46 -69
  28. package/lib/components/helpers.ts +13 -6
  29. package/lib/components/hero.ts +107 -95
  30. package/lib/components/icon.ts +160 -0
  31. package/lib/components/icons.ts +175 -0
  32. package/lib/components/include.ts +153 -5
  33. package/lib/components/input.ts +174 -374
  34. package/lib/components/kpicard.ts +16 -16
  35. package/lib/components/list.ts +378 -240
  36. package/lib/components/loading.ts +142 -211
  37. package/lib/components/menu.ts +103 -97
  38. package/lib/components/modal.ts +138 -144
  39. package/lib/components/nav.ts +169 -90
  40. package/lib/components/paragraph.ts +49 -150
  41. package/lib/components/progress.ts +118 -200
  42. package/lib/components/radio.ts +297 -149
  43. package/lib/components/script.ts +19 -87
  44. package/lib/components/select.ts +184 -186
  45. package/lib/components/sidebar.ts +152 -140
  46. package/lib/components/style.ts +19 -82
  47. package/lib/components/switch.ts +258 -188
  48. package/lib/components/table.ts +1117 -170
  49. package/lib/components/tabs.ts +162 -145
  50. package/lib/components/theme-toggle.ts +108 -169
  51. package/lib/components/tooltip.ts +86 -157
  52. package/lib/components/write.ts +108 -127
  53. package/lib/jux.ts +86 -41
  54. package/machinery/build.js +466 -0
  55. package/machinery/compiler.js +354 -105
  56. package/machinery/server.js +23 -100
  57. package/machinery/watcher.js +153 -130
  58. package/package.json +1 -2
  59. package/presets/base.css +1166 -0
  60. package/presets/notion.css +2 -1975
  61. package/lib/adapters/base-adapter.js +0 -35
  62. package/lib/adapters/index.js +0 -33
  63. package/lib/adapters/mysql-adapter.js +0 -65
  64. package/lib/adapters/postgres-adapter.js +0 -70
  65. package/lib/adapters/sqlite-adapter.js +0 -56
  66. package/lib/components/areachart.ts +0 -1246
  67. package/lib/components/areachartsmooth.ts +0 -1380
  68. package/lib/components/barchart.ts +0 -1250
  69. package/lib/components/chart.ts +0 -127
  70. package/lib/components/doughnutchart.ts +0 -1191
  71. package/lib/components/footer.ts +0 -165
  72. package/lib/components/header.ts +0 -187
  73. package/lib/components/layout.ts +0 -239
  74. package/lib/components/main.ts +0 -137
  75. package/lib/layouts/default.jux +0 -8
  76. package/lib/layouts/figma.jux +0 -0
  77. /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
- }