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