juxscript 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +121 -72
- package/lib/components/alert.ts +212 -165
- package/lib/components/badge.ts +93 -103
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +63 -122
- package/lib/components/card.ts +109 -155
- package/lib/components/charts/areachart.ts +315 -0
- package/lib/components/charts/barchart.ts +421 -0
- package/lib/components/charts/doughnutchart.ts +263 -0
- package/lib/components/charts/lib/BaseChart.ts +402 -0
- package/lib/components/charts/lib/chart-types.ts +159 -0
- package/lib/components/charts/lib/chart-utils.ts +160 -0
- package/lib/components/charts/lib/chart.ts +707 -0
- package/lib/components/checkbox.ts +264 -127
- package/lib/components/code.ts +75 -108
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +195 -147
- package/lib/components/dialog.ts +187 -157
- package/lib/components/divider.ts +85 -191
- package/lib/components/docs-data.json +544 -2027
- package/lib/components/dropdown.ts +178 -136
- package/lib/components/element.ts +227 -171
- package/lib/components/fileupload.ts +285 -228
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +46 -69
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +107 -95
- package/lib/components/icon.ts +160 -0
- package/lib/components/icons.ts +175 -0
- package/lib/components/include.ts +153 -5
- package/lib/components/input.ts +174 -374
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +378 -240
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +103 -97
- package/lib/components/modal.ts +138 -144
- package/lib/components/nav.ts +169 -90
- package/lib/components/paragraph.ts +49 -150
- package/lib/components/progress.ts +118 -200
- package/lib/components/radio.ts +297 -149
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +184 -186
- package/lib/components/sidebar.ts +152 -140
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +258 -188
- package/lib/components/table.ts +1117 -170
- package/lib/components/tabs.ts +162 -145
- package/lib/components/theme-toggle.ts +108 -169
- package/lib/components/tooltip.ts +86 -157
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +86 -41
- package/machinery/build.js +466 -0
- package/machinery/compiler.js +354 -105
- package/machinery/server.js +23 -100
- package/machinery/watcher.js +153 -130
- package/package.json +1 -2
- package/presets/base.css +1166 -0
- package/presets/notion.css +2 -1975
- package/lib/adapters/base-adapter.js +0 -35
- package/lib/adapters/index.js +0 -33
- package/lib/adapters/mysql-adapter.js +0 -65
- package/lib/adapters/postgres-adapter.js +0 -70
- package/lib/adapters/sqlite-adapter.js +0 -56
- package/lib/components/areachart.ts +0 -1246
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1250
- package/lib/components/chart.ts +0 -127
- package/lib/components/doughnutchart.ts +0 -1191
- package/lib/components/footer.ts +0 -165
- package/lib/components/header.ts +0 -187
- package/lib/components/layout.ts +0 -239
- package/lib/components/main.ts +0 -137
- package/lib/layouts/default.jux +0 -8
- package/lib/layouts/figma.jux +0 -0
- /package/lib/{themes → components/charts/lib}/charts.js +0 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import { getOrCreateContainer } from '../../helpers.js';
|
|
2
|
+
import { State } from '../../../reactivity/state.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
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
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface ChartOptions {
|
|
13
|
+
type?: string;
|
|
14
|
+
data?: any;
|
|
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;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ChartState = {
|
|
27
|
+
type: string;
|
|
28
|
+
data: any;
|
|
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;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export class Chart {
|
|
41
|
+
state: ChartState;
|
|
42
|
+
container: HTMLElement | null = null;
|
|
43
|
+
_id: string;
|
|
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
|
|
59
|
+
|
|
60
|
+
constructor(id: string, options: ChartOptions = {}) {
|
|
61
|
+
this._id = id;
|
|
62
|
+
this.id = id;
|
|
63
|
+
|
|
64
|
+
this.state = {
|
|
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 ?? ''
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* -------------------------
|
|
80
|
+
* Fluent API
|
|
81
|
+
* ------------------------- */
|
|
82
|
+
|
|
83
|
+
type(value: string): this {
|
|
84
|
+
this.state.type = value;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
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
|
+
|
|
184
|
+
data(value: any): this {
|
|
185
|
+
this.state.data = value;
|
|
186
|
+
this._updateChart();
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
options(value: any): this {
|
|
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('./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 });
|
|
443
|
+
return this;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/* -------------------------
|
|
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
|
+
"><script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script></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)
|
|
603
|
+
* ------------------------- */
|
|
604
|
+
|
|
605
|
+
render(targetId?: string): this {
|
|
606
|
+
// === 1. SETUP: Get or create container ===
|
|
607
|
+
let container: HTMLElement;
|
|
608
|
+
if (targetId) {
|
|
609
|
+
const target = document.querySelector(targetId);
|
|
610
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
611
|
+
throw new Error(`Chart: Target "${targetId}" not found`);
|
|
612
|
+
}
|
|
613
|
+
container = target;
|
|
614
|
+
} else {
|
|
615
|
+
container = getOrCreateContainer(this._id);
|
|
616
|
+
}
|
|
617
|
+
this.container = container;
|
|
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);
|
|
629
|
+
|
|
630
|
+
const canvas = document.createElement('canvas');
|
|
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
|
+
});
|
|
640
|
+
|
|
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);
|
|
648
|
+
|
|
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);
|
|
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);
|
|
694
|
+
return this;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
renderTo(juxComponent: any): this {
|
|
698
|
+
if (!juxComponent?._id) {
|
|
699
|
+
throw new Error('Chart.renderTo: Invalid component');
|
|
700
|
+
}
|
|
701
|
+
return this.render(`#${juxComponent._id}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export function chart(id: string, options: ChartOptions = {}): Chart {
|
|
706
|
+
return new Chart(id, options);
|
|
707
|
+
}
|