juxscript 1.0.18 → 1.0.20

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 (44) hide show
  1. package/lib/components/alert.ts +124 -128
  2. package/lib/components/areachart.ts +169 -287
  3. package/lib/components/areachartsmooth.ts +2 -2
  4. package/lib/components/badge.ts +63 -72
  5. package/lib/components/barchart.ts +120 -48
  6. package/lib/components/button.ts +99 -101
  7. package/lib/components/card.ts +97 -121
  8. package/lib/components/chart-types.ts +159 -0
  9. package/lib/components/chart-utils.ts +160 -0
  10. package/lib/components/chart.ts +628 -48
  11. package/lib/components/checkbox.ts +137 -51
  12. package/lib/components/code.ts +89 -75
  13. package/lib/components/container.ts +1 -1
  14. package/lib/components/datepicker.ts +93 -78
  15. package/lib/components/dialog.ts +163 -130
  16. package/lib/components/divider.ts +111 -193
  17. package/lib/components/docs-data.json +711 -264
  18. package/lib/components/doughnutchart.ts +125 -57
  19. package/lib/components/dropdown.ts +172 -85
  20. package/lib/components/element.ts +66 -61
  21. package/lib/components/fileupload.ts +142 -171
  22. package/lib/components/heading.ts +64 -21
  23. package/lib/components/hero.ts +109 -34
  24. package/lib/components/icon.ts +247 -0
  25. package/lib/components/icons.ts +174 -0
  26. package/lib/components/include.ts +77 -2
  27. package/lib/components/input.ts +174 -125
  28. package/lib/components/list.ts +120 -79
  29. package/lib/components/menu.ts +97 -2
  30. package/lib/components/modal.ts +144 -63
  31. package/lib/components/nav.ts +153 -52
  32. package/lib/components/paragraph.ts +78 -28
  33. package/lib/components/progress.ts +83 -107
  34. package/lib/components/radio.ts +151 -52
  35. package/lib/components/select.ts +110 -102
  36. package/lib/components/sidebar.ts +148 -105
  37. package/lib/components/switch.ts +124 -125
  38. package/lib/components/table.ts +214 -137
  39. package/lib/components/tabs.ts +194 -113
  40. package/lib/components/theme-toggle.ts +38 -7
  41. package/lib/components/tooltip.ts +207 -47
  42. package/lib/jux.ts +24 -5
  43. package/lib/reactivity/state.ts +13 -299
  44. package/package.json +1 -2
@@ -1,50 +1,78 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
4
  /**
4
- * Chart component options
5
+ * Chart component using Chart.js library
6
+ *
7
+ * DEPENDENCY: Requires Chart.js to be loaded
8
+ * Add to your HTML: <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ * Or install: npm install chart.js
5
10
  */
11
+
6
12
  export interface ChartOptions {
7
- type?: 'bar' | 'line' | 'pie' | 'doughnut';
13
+ type?: string;
8
14
  data?: any;
9
15
  options?: any;
16
+ width?: number;
17
+ height?: number;
18
+ title?: string;
19
+ subtitle?: string;
20
+ xAxisLabel?: string;
21
+ yAxisLabel?: string;
22
+ style?: string;
23
+ class?: string;
10
24
  }
11
25
 
12
- /**
13
- * Chart component state
14
- */
15
26
  type ChartState = {
16
27
  type: string;
17
28
  data: any;
18
29
  options: any;
30
+ width: number;
31
+ height: number;
32
+ title: string;
33
+ subtitle: string;
34
+ xAxisLabel: string;
35
+ yAxisLabel: string;
36
+ style: string;
37
+ class: string;
19
38
  };
20
39
 
21
- /**
22
- * Chart component - placeholder for chart integration
23
- *
24
- * Usage:
25
- * const chart = jux.chart('myChart', {
26
- * type: 'bar',
27
- * data: {
28
- * labels: ['A', 'B', 'C'],
29
- * datasets: [{ data: [10, 20, 30] }]
30
- * }
31
- * });
32
- * chart.render();
33
- */
34
40
  export class Chart {
35
41
  state: ChartState;
36
42
  container: HTMLElement | null = null;
37
43
  _id: string;
38
44
  id: string;
45
+ chartInstance: any = null;
46
+
47
+ // CRITICAL: Store bind/sync instructions for deferred wiring
48
+ private _bindings: Array<{ event: string, handler: Function }> = [];
49
+ private _syncBindings: Array<{
50
+ property: string,
51
+ stateObj: State<any>,
52
+ toState?: Function,
53
+ toComponent?: Function
54
+ }> = [];
55
+
56
+ private _chartJsCheckAttempts = 0;
57
+ private _maxCheckAttempts = 50; // 5 seconds with 100ms intervals
58
+ private static _dataLabelsRegistered = false; // Track if plugin is registered
39
59
 
40
60
  constructor(id: string, options: ChartOptions = {}) {
41
61
  this._id = id;
42
62
  this.id = id;
43
63
 
44
64
  this.state = {
45
- type: options.type ?? 'bar',
46
- data: options.data ?? {},
47
- options: options.options ?? {}
65
+ type: options.type ?? 'line',
66
+ data: options.data ?? { labels: [], datasets: [] },
67
+ options: options.options ?? {},
68
+ width: options.width ?? 400,
69
+ height: options.height ?? 300,
70
+ title: options.title ?? '',
71
+ subtitle: options.subtitle ?? '',
72
+ xAxisLabel: options.xAxisLabel ?? '',
73
+ yAxisLabel: options.yAxisLabel ?? '',
74
+ style: options.style ?? '',
75
+ class: options.class ?? ''
48
76
  };
49
77
  }
50
78
 
@@ -57,71 +85,623 @@ export class Chart {
57
85
  return this;
58
86
  }
59
87
 
88
+ /**
89
+ * Change chart type dynamically (recreates chart)
90
+ */
91
+ changeType(value: string): this {
92
+ this.state.type = value;
93
+
94
+ if (this.chartInstance) {
95
+ // Properly destroy existing chart and clear canvas
96
+ this.chartInstance.destroy();
97
+ this.chartInstance = null;
98
+
99
+ // Clear canvas completely
100
+ const canvas = document.getElementById(`${this._id}-canvas`) as HTMLCanvasElement;
101
+ if (canvas) {
102
+ const ctx = canvas.getContext('2d');
103
+ if (ctx) {
104
+ // Clear the entire canvas
105
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
106
+
107
+ // Reset canvas dimensions to force complete redraw
108
+ const { width, height } = this.state;
109
+ canvas.width = width;
110
+ canvas.height = height;
111
+
112
+ try {
113
+ // Apply pie/doughnut specific colors if needed
114
+ const chartData = this._preparePieChartColors(value, this.state.data);
115
+
116
+ this.chartInstance = new (window as any).Chart(ctx, {
117
+ type: value,
118
+ data: chartData,
119
+ options: this.state.options
120
+ });
121
+ console.log(`✓ Chart "${this._id}" type changed to "${value}"`);
122
+ } catch (error) {
123
+ console.error(`Failed to change chart type to "${value}":`, error);
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ return this;
130
+ }
131
+
132
+ /**
133
+ * Apply unique colors to each slice for pie/doughnut/polar charts
134
+ */
135
+ private _preparePieChartColors(type: string, data: any): any {
136
+ const isPieType = ['pie', 'doughnut', 'polarArea', 'radar'].includes(type);
137
+
138
+ if (!isPieType || !data || !data.datasets) {
139
+ return data;
140
+ }
141
+
142
+ // Clone data to avoid mutating original
143
+ const clonedData = JSON.parse(JSON.stringify(data));
144
+
145
+ // Get theme colors
146
+ const themeColors = this._getThemeColors();
147
+
148
+ clonedData.datasets.forEach((dataset: any) => {
149
+ if (!dataset.backgroundColor || typeof dataset.backgroundColor === 'string') {
150
+ // Generate colors for each data point
151
+ const colors = dataset.data.map((_: any, index: number) => {
152
+ return themeColors[index % themeColors.length];
153
+ });
154
+
155
+ dataset.backgroundColor = colors;
156
+ dataset.borderColor = colors.map((color: string) => color);
157
+ dataset.borderWidth = 2;
158
+ }
159
+ });
160
+
161
+ return clonedData;
162
+ }
163
+
164
+ /**
165
+ * Get current theme colors
166
+ */
167
+ private _getThemeColors(): string[] {
168
+ const themeValue = this.state.options.theme || 'google';
169
+
170
+ // Default color sets by theme
171
+ const themeColorSets: { [key: string]: string[] } = {
172
+ google: ['#4285F4', '#EA4335', '#FBBC04', '#34A853', '#FF6D01', '#46BDC6'],
173
+ seriesa: ['#667eea', '#764ba2', '#f093fb', '#4facfe', '#00f2fe', '#43e97b'],
174
+ hr: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F'],
175
+ figma: ['#0ACF83', '#FF7262', '#1ABCFE', '#A259FF', '#F24E1E', '#FFC700'],
176
+ notion: ['#2EAADC', '#9B59B6', '#E67E22', '#E74C3C', '#F39C12', '#16A085'],
177
+ chalk: ['#96CEB4', '#FFEAA7', '#DFE6E9', '#74B9FF', '#FD79A8', '#A29BFE'],
178
+ mint: ['#26de81', '#20bf6b', '#0fb9b1', '#2bcbba', '#45aaf2', '#4b7bec']
179
+ };
180
+
181
+ return themeColorSets[themeValue] || themeColorSets.google;
182
+ }
183
+
60
184
  data(value: any): this {
61
185
  this.state.data = value;
186
+ this._updateChart();
62
187
  return this;
63
188
  }
64
189
 
65
190
  options(value: any): this {
66
191
  this.state.options = value;
192
+ this._updateChart();
193
+ return this;
194
+ }
195
+
196
+ width(value: number): this {
197
+ this.state.width = value;
198
+ return this;
199
+ }
200
+
201
+ height(value: number): this {
202
+ this.state.height = value;
203
+ return this;
204
+ }
205
+
206
+ style(value: string): this {
207
+ this.state.style = value;
208
+ return this;
209
+ }
210
+
211
+ class(value: string): this {
212
+ this.state.class = value;
213
+ return this;
214
+ }
215
+
216
+ title(value: string): this {
217
+ this.state.title = value;
218
+ return this;
219
+ }
220
+
221
+ subtitle(value: string): this {
222
+ this.state.subtitle = value;
223
+ return this;
224
+ }
225
+
226
+ xAxisLabel(value: string): this {
227
+ this.state.xAxisLabel = value;
228
+ return this;
229
+ }
230
+
231
+ yAxisLabel(value: string): this {
232
+ this.state.yAxisLabel = value;
233
+ return this;
234
+ }
235
+
236
+ /* -------------------------
237
+ * Fluent API - Chart.js Configuration
238
+ * ------------------------- */
239
+
240
+ // Chart.js options configuration methods
241
+ theme(value: string): this {
242
+ // Import theme colors and apply to chart
243
+ import('../themes/charts.js').then(themes => {
244
+ const themeConfig = themes.chartThemes[value as keyof typeof themes.chartThemes];
245
+
246
+ if (!themeConfig) return;
247
+
248
+ // Apply theme colors to datasets
249
+ if (this.chartInstance && this.chartInstance.data.datasets) {
250
+ this.chartInstance.data.datasets.forEach((dataset: any, index: number) => {
251
+ const colorIndex = index % themeConfig.colors.length;
252
+ dataset.borderColor = themeConfig.colors[colorIndex];
253
+ dataset.backgroundColor = themeConfig.colors[colorIndex] + '33'; // 20% opacity
254
+ });
255
+ this.chartInstance.update();
256
+ }
257
+
258
+ // Store theme in state
259
+ this.state.options.theme = value;
260
+ });
261
+
262
+ return this;
263
+ }
264
+
265
+ styleMode(value: string): this {
266
+ // Style mode affects line tension and stepped property
267
+ if (!this.state.options.elements) this.state.options.elements = {};
268
+ if (!this.state.options.elements.line) this.state.options.elements.line = {};
269
+
270
+ // Reset both properties first
271
+ delete this.state.options.elements.line.stepped;
272
+ delete this.state.options.elements.line.tension;
273
+
274
+ switch (value) {
275
+ case 'smooth':
276
+ this.state.options.elements.line.tension = 0.4;
277
+ break;
278
+ case 'stepped':
279
+ this.state.options.elements.line.stepped = true;
280
+ this.state.options.elements.line.tension = 0;
281
+ break;
282
+ case 'linear': // Changed from 'straight' to avoid reserved word issues
283
+ this.state.options.elements.line.tension = 0;
284
+ break;
285
+ }
286
+
287
+ // Apply to dataset if chart is already rendered
288
+ if (this.chartInstance && this.chartInstance.data.datasets) {
289
+ this.chartInstance.data.datasets.forEach((dataset: any) => {
290
+ if (value === 'smooth') {
291
+ dataset.tension = 0.4;
292
+ delete dataset.stepped;
293
+ } else if (value === 'stepped') {
294
+ dataset.stepped = true;
295
+ dataset.tension = 0;
296
+ } else if (value === 'linear') {
297
+ dataset.tension = 0;
298
+ delete dataset.stepped;
299
+ }
300
+ });
301
+ }
302
+
303
+ this._updateChart();
304
+ return this;
305
+ }
306
+
307
+ borderRadius(value: number): this {
308
+ if (!this.state.options.elements) this.state.options.elements = {};
309
+ if (!this.state.options.elements.bar) this.state.options.elements.bar = {};
310
+ this.state.options.elements.bar.borderRadius = value;
311
+ this._updateChart();
312
+ return this;
313
+ }
314
+
315
+ showTicksX(value: boolean): this {
316
+ if (!this.state.options.scales) this.state.options.scales = {};
317
+ if (!this.state.options.scales.x) this.state.options.scales.x = {};
318
+ if (!this.state.options.scales.x.ticks) this.state.options.scales.x.ticks = {};
319
+ this.state.options.scales.x.ticks.display = value;
320
+ this._updateChart();
321
+ return this;
322
+ }
323
+
324
+ showTicksY(value: boolean): this {
325
+ if (!this.state.options.scales) this.state.options.scales = {};
326
+ if (!this.state.options.scales.y) this.state.options.scales.y = {};
327
+ if (!this.state.options.scales.y.ticks) this.state.options.scales.y.ticks = {};
328
+ this.state.options.scales.y.ticks.display = value;
329
+ this._updateChart();
330
+ return this;
331
+ }
332
+
333
+ showDataLabels(value: boolean): this {
334
+ if (!this.state.options.plugins) this.state.options.plugins = {};
335
+
336
+ if (value) {
337
+ // Enable datalabels plugin with proper configuration
338
+ this.state.options.plugins.datalabels = {
339
+ display: true,
340
+ color: '#fff',
341
+ backgroundColor: function (context: any) {
342
+ // Use dataset color or default
343
+ return context.dataset.backgroundColor || 'rgba(0, 0, 0, 0.7)';
344
+ },
345
+ borderRadius: 4,
346
+ padding: 6,
347
+ font: {
348
+ weight: 'bold',
349
+ size: 11
350
+ },
351
+ formatter: (value: any, context: any) => {
352
+ // Format numbers with commas
353
+ if (typeof value === 'number') {
354
+ return value.toLocaleString();
355
+ }
356
+ return value;
357
+ },
358
+ anchor: 'end',
359
+ align: 'end',
360
+ offset: 4
361
+ };
362
+ } else {
363
+ // Disable datalabels
364
+ this.state.options.plugins.datalabels = {
365
+ display: false
366
+ };
367
+ }
368
+
369
+ this._updateChart();
370
+ return this;
371
+ }
372
+
373
+ showLegend(value: boolean): this {
374
+ if (!this.state.options.plugins) this.state.options.plugins = {};
375
+ if (!this.state.options.plugins.legend) this.state.options.plugins.legend = {};
376
+ this.state.options.plugins.legend.display = value;
377
+ this._updateChart();
378
+ return this;
379
+ }
380
+
381
+ showDataTable(value: boolean): this {
382
+ // Store flag for custom rendering (not a Chart.js feature)
383
+ if (!this.state.options.custom) this.state.options.custom = {};
384
+ this.state.options.custom.showDataTable = value;
385
+ return this;
386
+ }
387
+
388
+ legendOrientation(value: string): this {
389
+ if (!this.state.options.plugins) this.state.options.plugins = {};
390
+ if (!this.state.options.plugins.legend) this.state.options.plugins.legend = {};
391
+ this.state.options.plugins.legend.position = value === 'horizontal' ? 'top' : 'right';
392
+ this._updateChart();
393
+ return this;
394
+ }
395
+
396
+ animate(value: boolean): this {
397
+ if (!this.state.options.animation) this.state.options.animation = {};
398
+ this.state.options.animation.duration = value ? 800 : 0;
399
+ this._updateChart();
400
+ return this;
401
+ }
402
+
403
+ animationDuration(value: number): this {
404
+ if (!this.state.options.animation) this.state.options.animation = {};
405
+ this.state.options.animation.duration = value;
406
+ this._updateChart();
407
+ return this;
408
+ }
409
+
410
+ chartOrientation(value: string): this {
411
+ if (!this.state.options.indexAxis) {
412
+ this.state.options.indexAxis = value === 'horizontal' ? 'y' : 'x';
413
+ this._updateChart();
414
+ }
415
+ return this;
416
+ }
417
+
418
+ chartDirection(value: string): this {
419
+ if (!this.state.options.scales) this.state.options.scales = {};
420
+ if (value === 'reverse') {
421
+ if (!this.state.options.scales.x) this.state.options.scales.x = {};
422
+ if (!this.state.options.scales.y) this.state.options.scales.y = {};
423
+ this.state.options.scales.x.reverse = true;
424
+ this.state.options.scales.y.reverse = true;
425
+ } else {
426
+ if (this.state.options.scales.x) this.state.options.scales.x.reverse = false;
427
+ if (this.state.options.scales.y) this.state.options.scales.y.reverse = false;
428
+ }
429
+ this._updateChart();
430
+ return this;
431
+ }
432
+
433
+ bind(event: string, handler: Function): this {
434
+ this._bindings.push({ event, handler });
435
+ return this;
436
+ }
437
+
438
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
439
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
440
+ throw new Error(`Chart.sync: Expected a State object for property "${property}"`);
441
+ }
442
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
67
443
  return this;
68
444
  }
69
445
 
70
446
  /* -------------------------
71
- * Render
447
+ * Helpers
448
+ * ------------------------- */
449
+
450
+ private _waitForChartJs(callback: () => void): void {
451
+ if ((window as any).Chart) {
452
+ // Register ChartDataLabels plugin if available and not already registered
453
+ if (!Chart._dataLabelsRegistered && (window as any).ChartDataLabels) {
454
+ try {
455
+ (window as any).Chart.register((window as any).ChartDataLabels);
456
+ Chart._dataLabelsRegistered = true;
457
+ console.log('✓ ChartDataLabels plugin registered');
458
+ } catch (error) {
459
+ console.warn('Failed to register ChartDataLabels plugin:', error);
460
+ }
461
+ }
462
+
463
+ callback();
464
+ return;
465
+ }
466
+
467
+ if (this._chartJsCheckAttempts >= this._maxCheckAttempts) {
468
+ console.error(
469
+ `Chart.js failed to load after ${this._maxCheckAttempts * 100}ms. ` +
470
+ 'Please ensure Chart.js is properly loaded in your page.'
471
+ );
472
+ this._renderFallbackUI();
473
+ return;
474
+ }
475
+
476
+ this._chartJsCheckAttempts++;
477
+ setTimeout(() => this._waitForChartJs(callback), 100);
478
+ }
479
+
480
+ private _renderFallbackUI(): void {
481
+ const wrapper = document.getElementById(this._id);
482
+ if (!wrapper) return;
483
+
484
+ const { data, type, width, height } = this.state;
485
+ const hasData = data?.labels?.length > 0 || data?.datasets?.length > 0;
486
+
487
+ wrapper.innerHTML = `
488
+ <div style="
489
+ display: flex;
490
+ flex-direction: column;
491
+ align-items: center;
492
+ justify-content: center;
493
+ height: 100%;
494
+ min-height: ${Math.max(height, 200)}px;
495
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
496
+ border-radius: 12px;
497
+ padding: 40px 20px;
498
+ text-align: center;
499
+ color: white;
500
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
501
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
502
+ ">
503
+ <div style="
504
+ background: rgba(255, 255, 255, 0.2);
505
+ backdrop-filter: blur(10px);
506
+ border-radius: 16px;
507
+ padding: 32px;
508
+ max-width: 500px;
509
+ ">
510
+ <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-bottom: 20px;">
511
+ <path d="M3 3v18h18"></path>
512
+ <path d="M18 17V9"></path>
513
+ <path d="M13 17V5"></path>
514
+ <path d="M8 17v-3"></path>
515
+ </svg>
516
+
517
+ <h3 style="
518
+ margin: 0 0 12px 0;
519
+ font-size: 24px;
520
+ font-weight: 700;
521
+ ">Chart.js Required</h3>
522
+
523
+ <p style="
524
+ margin: 0 0 24px 0;
525
+ font-size: 14px;
526
+ opacity: 0.9;
527
+ line-height: 1.6;
528
+ ">
529
+ This ${type} chart needs Chart.js library to render.<br>
530
+ ${hasData ? `Ready to display ${data.labels?.length || 0} data points.` : 'Waiting for data...'}
531
+ </p>
532
+
533
+ <div style="
534
+ background: rgba(0, 0, 0, 0.2);
535
+ border-radius: 8px;
536
+ padding: 16px;
537
+ text-align: left;
538
+ font-family: 'Monaco', 'Courier New', monospace;
539
+ font-size: 13px;
540
+ margin-bottom: 16px;
541
+ ">
542
+ <div style="color: #ffd700; margin-bottom: 8px;">📦 Add to your HTML:</div>
543
+ <code style="
544
+ display: block;
545
+ color: #fff;
546
+ line-height: 1.5;
547
+ word-break: break-all;
548
+ ">&lt;script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"&gt;&lt;/script&gt;</code>
549
+ </div>
550
+
551
+ <div style="
552
+ background: rgba(0, 0, 0, 0.2);
553
+ border-radius: 8px;
554
+ padding: 16px;
555
+ text-align: left;
556
+ font-family: 'Monaco', 'Courier New', monospace;
557
+ font-size: 13px;
558
+ ">
559
+ <div style="color: #ffd700; margin-bottom: 8px;">📦 Or install via npm:</div>
560
+ <code style="
561
+ display: block;
562
+ color: #fff;
563
+ line-height: 1.5;
564
+ ">npm install chart.js</code>
565
+ </div>
566
+
567
+ <button onclick="window.location.reload()" style="
568
+ margin-top: 24px;
569
+ padding: 12px 24px;
570
+ background: rgba(255, 255, 255, 0.9);
571
+ color: #667eea;
572
+ border: none;
573
+ border-radius: 8px;
574
+ font-size: 14px;
575
+ font-weight: 600;
576
+ cursor: pointer;
577
+ transition: all 0.2s;
578
+ " onmouseover="this.style.background='#fff'" onmouseout="this.style.background='rgba(255, 255, 255, 0.9)'">
579
+ 🔄 Reload Page
580
+ </button>
581
+ </div>
582
+ </div>
583
+ `;
584
+ }
585
+
586
+ private _updateChart(): void {
587
+ if (this.chartInstance) {
588
+ this.chartInstance.data = this.state.data;
589
+ this.chartInstance.options = this.state.options;
590
+ this.chartInstance.update();
591
+ }
592
+ }
593
+
594
+ destroy(): void {
595
+ if (this.chartInstance) {
596
+ this.chartInstance.destroy();
597
+ this.chartInstance = null;
598
+ }
599
+ }
600
+
601
+ /* -------------------------
602
+ * Render (5-Step Pattern)
72
603
  * ------------------------- */
73
604
 
74
605
  render(targetId?: string): this {
606
+ // === 1. SETUP: Get or create container ===
75
607
  let container: HTMLElement;
76
-
77
608
  if (targetId) {
78
609
  const target = document.querySelector(targetId);
79
610
  if (!target || !(target instanceof HTMLElement)) {
80
- throw new Error(`Chart: Target element "${targetId}" not found`);
611
+ throw new Error(`Chart: Target "${targetId}" not found`);
81
612
  }
82
613
  container = target;
83
614
  } else {
84
615
  container = getOrCreateContainer(this._id);
85
616
  }
86
-
87
617
  this.container = container;
88
- const { type, data } = this.state;
618
+
619
+ // === 2. PREPARE: Destructure state ===
620
+ const { type, data, options, width, height, style, class: className } = this.state;
621
+
622
+ // === 3. BUILD: Create DOM elements ===
623
+ const wrapper = document.createElement('div');
624
+ wrapper.className = 'jux-chart';
625
+ wrapper.id = this._id;
626
+ wrapper.style.cssText = `width: ${width}px; height: ${height}px; position: relative;`;
627
+ if (className) wrapper.className += ` ${className}`;
628
+ if (style) wrapper.setAttribute('style', wrapper.style.cssText + style);
89
629
 
90
630
  const canvas = document.createElement('canvas');
91
- canvas.id = this._id;
92
- canvas.className = 'jux-chart';
631
+ canvas.id = `${this._id}-canvas`;
632
+ wrapper.appendChild(canvas);
633
+
634
+ // === 4. WIRE: Attach event listeners and sync bindings ===
635
+
636
+ // Wire custom bindings from .bind() calls
637
+ this._bindings.forEach(({ event, handler }) => {
638
+ wrapper.addEventListener(event, handler as EventListener);
639
+ });
93
640
 
94
- // Placeholder rendering (would integrate with Chart.js or similar)
95
- const placeholder = document.createElement('div');
96
- placeholder.className = 'jux-chart-placeholder';
97
- placeholder.textContent = `Chart (${type}) - Integration pending`;
98
- placeholder.style.cssText = 'padding: 2rem; border: 2px dashed var(--border-color); text-align: center;';
641
+ // Initialize chart with retry logic
642
+ this._waitForChartJs(() => {
643
+ const ctx = canvas.getContext('2d');
644
+ if (ctx) {
645
+ try {
646
+ // Apply pie chart colors if needed
647
+ const chartData = this._preparePieChartColors(type, data);
99
648
 
100
- container.appendChild(placeholder);
101
- container.appendChild(canvas);
649
+ this.chartInstance = new (window as any).Chart(ctx, {
650
+ type,
651
+ data: chartData,
652
+ options
653
+ });
654
+ console.log(`✓ Chart "${this._id}" rendered successfully`);
655
+ } catch (error) {
656
+ console.error(`Failed to create chart "${this._id}":`, error);
657
+ this._renderFallbackUI();
658
+ }
659
+ }
660
+ });
661
+
662
+ // Wire sync bindings from .sync() calls
663
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
664
+ if (property === 'data') {
665
+ const transformToComponent = toComponent || ((v: any) => v);
666
+
667
+ stateObj.subscribe((val: any) => {
668
+ const transformed = transformToComponent(val);
669
+ this.state.data = transformed;
670
+
671
+ if (this.chartInstance) {
672
+ this.chartInstance.data = transformed;
673
+ this.chartInstance.update();
674
+ }
675
+ });
676
+ }
677
+ else if (property === 'options') {
678
+ const transformToComponent = toComponent || ((v: any) => v);
102
679
 
680
+ stateObj.subscribe((val: any) => {
681
+ const transformed = transformToComponent(val);
682
+ this.state.options = transformed;
683
+
684
+ if (this.chartInstance) {
685
+ this.chartInstance.options = transformed;
686
+ this.chartInstance.update();
687
+ }
688
+ });
689
+ }
690
+ });
691
+
692
+ // === 5. RENDER: Append to DOM and finalize ===
693
+ container.appendChild(wrapper);
103
694
  return this;
104
695
  }
105
696
 
106
- /**
107
- * Render to another Jux component's container
108
- */
109
697
  renderTo(juxComponent: any): this {
110
- if (!juxComponent || typeof juxComponent !== 'object') {
111
- throw new Error('Chart.renderTo: Invalid component - not an object');
698
+ if (!juxComponent?._id) {
699
+ throw new Error('Chart.renderTo: Invalid component');
112
700
  }
113
-
114
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
115
- throw new Error('Chart.renderTo: Invalid component - missing _id (not a Jux component)');
116
- }
117
-
118
701
  return this.render(`#${juxComponent._id}`);
119
702
  }
120
703
  }
121
704
 
122
- /**
123
- * Factory helper
124
- */
125
705
  export function chart(id: string, options: ChartOptions = {}): Chart {
126
706
  return new Chart(id, options);
127
707
  }