chartforge 0.0.2 → 0.1.0

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.
@@ -0,0 +1,1108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const builtins = require("./builtins-5HYzyd_B.cjs");
4
+ const misc = require("./misc-tOf-LXeZ.cjs");
5
+ class EventBus {
6
+ constructor() {
7
+ this._events = /* @__PURE__ */ new Map();
8
+ }
9
+ on(event, handler, priority = 0) {
10
+ if (!this._events.has(event)) this._events.set(event, []);
11
+ const list = this._events.get(event);
12
+ list.push({ handler, priority });
13
+ list.sort((a, b) => b.priority - a.priority);
14
+ return () => this.off(event, handler);
15
+ }
16
+ off(event, handler) {
17
+ const list = this._events.get(event);
18
+ if (!list) return;
19
+ const idx = list.findIndex((h) => h.handler === handler);
20
+ if (idx !== -1) list.splice(idx, 1);
21
+ }
22
+ emit(event, data) {
23
+ this._events.get(event)?.forEach(({ handler }) => handler(data));
24
+ }
25
+ async emitAsync(event, data) {
26
+ const list = this._events.get(event);
27
+ if (!list) return;
28
+ for (const { handler } of list) await handler(data);
29
+ }
30
+ clear(event) {
31
+ event ? this._events.delete(event) : this._events.clear();
32
+ }
33
+ }
34
+ class MiddlewarePipeline {
35
+ constructor() {
36
+ this._fns = [];
37
+ }
38
+ use(fn) {
39
+ this._fns.push(fn);
40
+ return this;
41
+ }
42
+ async execute(ctx) {
43
+ let i = 0;
44
+ const next = async () => {
45
+ if (i >= this._fns.length) return;
46
+ await this._fns[i++](ctx, next);
47
+ };
48
+ await next();
49
+ return ctx;
50
+ }
51
+ }
52
+ class DataPipeline {
53
+ constructor() {
54
+ this._transformers = [];
55
+ }
56
+ addTransformer(name, fn) {
57
+ this._transformers.push({ name, fn });
58
+ return this;
59
+ }
60
+ removeTransformer(name) {
61
+ const idx = this._transformers.findIndex((t) => t.name === name);
62
+ if (idx !== -1) this._transformers.splice(idx, 1);
63
+ }
64
+ async transform(data, config) {
65
+ let result = data;
66
+ for (const t of this._transformers) {
67
+ result = await t.fn(result, config);
68
+ }
69
+ return result;
70
+ }
71
+ }
72
+ const EASINGS = {
73
+ linear: (t) => t,
74
+ easeInQuad: (t) => t * t,
75
+ easeOutQuad: (t) => t * (2 - t),
76
+ easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
77
+ easeInCubic: (t) => t * t * t,
78
+ easeOutCubic: (t) => --t * t * t + 1,
79
+ easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
80
+ easeInElastic: (t) => {
81
+ const c4 = 2 * Math.PI / 3;
82
+ return t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
83
+ },
84
+ easeOutElastic: (t) => {
85
+ const c4 = 2 * Math.PI / 3;
86
+ return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
87
+ },
88
+ easeInBounce: (t) => {
89
+ const n1 = 7.5625, d1 = 2.75;
90
+ const eob = (t2) => {
91
+ if (t2 < 1 / d1) return n1 * t2 * t2;
92
+ if (t2 < 2 / d1) return n1 * (t2 -= 1.5 / d1) * t2 + 0.75;
93
+ if (t2 < 2.5 / d1) return n1 * (t2 -= 2.25 / d1) * t2 + 0.9375;
94
+ return n1 * (t2 -= 2.625 / d1) * t2 + 0.984375;
95
+ };
96
+ return 1 - eob(1 - t);
97
+ },
98
+ easeOutBounce: (t) => {
99
+ const n1 = 7.5625, d1 = 2.75;
100
+ if (t < 1 / d1) return n1 * t * t;
101
+ if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
102
+ if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
103
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
104
+ }
105
+ };
106
+ class AnimationEngine {
107
+ constructor() {
108
+ this._running = /* @__PURE__ */ new Map();
109
+ }
110
+ animate(id, from, to, duration, easing = "easeOutQuad", onUpdate, onComplete) {
111
+ this.stop(id);
112
+ const easingFn = EASINGS[easing] ?? EASINGS.linear;
113
+ const start = performance.now();
114
+ const tick = (now) => {
115
+ const elapsed = now - start;
116
+ const progress = Math.min(elapsed / duration, 1);
117
+ const eased = easingFn(progress);
118
+ onUpdate(from + (to - from) * eased, progress);
119
+ if (progress < 1) {
120
+ const rafId2 = requestAnimationFrame(tick);
121
+ this._running.set(id, { rafId: rafId2 });
122
+ } else {
123
+ this._running.delete(id);
124
+ onComplete?.();
125
+ }
126
+ };
127
+ const rafId = requestAnimationFrame(tick);
128
+ this._running.set(id, { rafId });
129
+ }
130
+ stop(id) {
131
+ const state = this._running.get(id);
132
+ if (state) {
133
+ cancelAnimationFrame(state.rafId);
134
+ this._running.delete(id);
135
+ }
136
+ }
137
+ stopAll() {
138
+ this._running.forEach((s) => cancelAnimationFrame(s.rafId));
139
+ this._running.clear();
140
+ }
141
+ }
142
+ class ThemeManager {
143
+ constructor() {
144
+ this._themes = /* @__PURE__ */ new Map();
145
+ this._current = null;
146
+ }
147
+ register(name, theme) {
148
+ this._themes.set(name, theme);
149
+ }
150
+ get(name) {
151
+ return this._themes.get(name);
152
+ }
153
+ apply(name) {
154
+ const theme = this._themes.get(name);
155
+ if (!theme) {
156
+ console.warn(`[ChartForge] Theme "${name}" not found`);
157
+ return null;
158
+ }
159
+ this._current = name;
160
+ return theme;
161
+ }
162
+ getCurrent() {
163
+ return this._current ? this._themes.get(this._current) ?? null : null;
164
+ }
165
+ get currentName() {
166
+ return this._current;
167
+ }
168
+ }
169
+ class PluginManager {
170
+ constructor(_chart) {
171
+ this._chart = _chart;
172
+ this._plugins = /* @__PURE__ */ new Map();
173
+ this._inited = /* @__PURE__ */ new Set();
174
+ }
175
+ register(name, Plugin, config = {}) {
176
+ if (this._plugins.has(name)) {
177
+ console.warn(`[ChartForge] Plugin "${name}" already registered`);
178
+ return;
179
+ }
180
+ const instance = new Plugin(this._chart, config);
181
+ this._plugins.set(name, { instance, config });
182
+ if (this._chart.initialized) {
183
+ this._initOne(name, instance);
184
+ }
185
+ }
186
+ get(name) {
187
+ return this._plugins.get(name)?.instance ?? null;
188
+ }
189
+ has(name) {
190
+ return this._plugins.has(name);
191
+ }
192
+ remove(name) {
193
+ const entry = this._plugins.get(name);
194
+ if (!entry) return;
195
+ entry.instance.destroy?.();
196
+ this._plugins.delete(name);
197
+ this._inited.delete(name);
198
+ }
199
+ initAll() {
200
+ this._plugins.forEach((entry, name) => {
201
+ if (!this._inited.has(name)) this._initOne(name, entry.instance);
202
+ });
203
+ }
204
+ destroyAll() {
205
+ this._plugins.forEach((e) => e.instance.destroy?.());
206
+ this._plugins.clear();
207
+ this._inited.clear();
208
+ }
209
+ _initOne(name, plugin) {
210
+ plugin.init?.();
211
+ this._inited.add(name);
212
+ }
213
+ }
214
+ class VirtualRenderer {
215
+ constructor(_chart) {
216
+ this._chart = _chart;
217
+ this._viewport = { start: 0, end: 100 };
218
+ this._threshold = _chart.config.virtual?.threshold ?? 1e4;
219
+ }
220
+ updateViewport(start, end) {
221
+ this._viewport = { start, end };
222
+ }
223
+ shouldVirtualize() {
224
+ const total = this._chart.config.data.series.reduce(
225
+ (sum, s) => sum + (Array.isArray(s.data) ? s.data.length : 0),
226
+ 0
227
+ );
228
+ return total > this._threshold;
229
+ }
230
+ getVisibleData() {
231
+ const { start, end } = this._viewport;
232
+ const src = this._chart.config.data;
233
+ return {
234
+ series: src.series.map((s) => ({
235
+ ...s,
236
+ data: s.data.slice(start, end)
237
+ })),
238
+ ...src.labels && { labels: src.labels.slice(start, end) }
239
+ };
240
+ }
241
+ }
242
+ class RealTimeModule {
243
+ constructor(_chart) {
244
+ this._chart = _chart;
245
+ this._registry = /* @__PURE__ */ new Map();
246
+ this._connections = /* @__PURE__ */ new Map();
247
+ }
248
+ registerAdapter(name, Adapter) {
249
+ this._registry.set(name, Adapter);
250
+ }
251
+ connect(type, config) {
252
+ const Adapter = this._registry.get(type);
253
+ if (!Adapter) {
254
+ console.error(`[ChartForge] Real-time adapter "${type}" not registered`);
255
+ return;
256
+ }
257
+ const adapter = new Adapter(config);
258
+ adapter.on("data", (data) => this._chart.updateData(data));
259
+ void adapter.connect();
260
+ this._connections.set(type, adapter);
261
+ }
262
+ disconnect(type) {
263
+ const adapter = this._connections.get(type);
264
+ if (adapter) {
265
+ adapter.disconnect();
266
+ this._connections.delete(type);
267
+ }
268
+ }
269
+ disconnectAll() {
270
+ this._connections.forEach((a) => a.disconnect());
271
+ this._connections.clear();
272
+ }
273
+ }
274
+ class WebSocketAdapter {
275
+ constructor(_config) {
276
+ this._config = _config;
277
+ this._ws = null;
278
+ this._listeners = /* @__PURE__ */ new Map();
279
+ }
280
+ on(event, handler) {
281
+ if (!this._listeners.has(event)) this._listeners.set(event, []);
282
+ this._listeners.get(event).push(handler);
283
+ }
284
+ _emit(event, data) {
285
+ this._listeners.get(event)?.forEach((h) => h(data));
286
+ }
287
+ connect() {
288
+ this._ws = new WebSocket(this._config.url);
289
+ this._ws.addEventListener("message", (e) => {
290
+ try {
291
+ const data = JSON.parse(e.data);
292
+ this._emit("data", data);
293
+ } catch {
294
+ console.warn("[ChartForge] WebSocketAdapter: invalid JSON", e.data);
295
+ }
296
+ });
297
+ this._ws.addEventListener("error", (e) => this._emit("error", e));
298
+ }
299
+ disconnect() {
300
+ this._ws?.close();
301
+ this._ws = null;
302
+ }
303
+ send(data) {
304
+ if (this._ws?.readyState === WebSocket.OPEN) {
305
+ this._ws.send(JSON.stringify(data));
306
+ }
307
+ }
308
+ }
309
+ class PollingAdapter {
310
+ constructor(_config) {
311
+ this._config = _config;
312
+ this._timer = null;
313
+ this._listeners = /* @__PURE__ */ new Map();
314
+ }
315
+ on(event, handler) {
316
+ if (!this._listeners.has(event)) this._listeners.set(event, []);
317
+ this._listeners.get(event).push(handler);
318
+ }
319
+ _emit(event, data) {
320
+ this._listeners.get(event)?.forEach((h) => h(data));
321
+ }
322
+ async _poll() {
323
+ try {
324
+ const res = await fetch(this._config.url);
325
+ const data = await res.json();
326
+ this._emit("data", data);
327
+ } catch (err) {
328
+ this._emit("error", err);
329
+ }
330
+ }
331
+ connect() {
332
+ void this._poll();
333
+ this._timer = setInterval(() => void this._poll(), this._config.interval ?? 5e3);
334
+ }
335
+ disconnect() {
336
+ if (this._timer) {
337
+ clearInterval(this._timer);
338
+ this._timer = null;
339
+ }
340
+ }
341
+ }
342
+ class BaseRenderer {
343
+ constructor(chart, data) {
344
+ this.chart = chart;
345
+ this.data = data;
346
+ this.theme = chart.theme;
347
+ this.config = chart.config;
348
+ this.group = chart.mainGroup;
349
+ this.padding = {
350
+ top: chart.config.padding?.top ?? 40,
351
+ right: chart.config.padding?.right ?? 40,
352
+ bottom: chart.config.padding?.bottom ?? 60,
353
+ left: chart.config.padding?.left ?? 60
354
+ };
355
+ }
356
+ dims() {
357
+ const vb = this.chart.svg.getAttribute("viewBox").split(" ").map(Number);
358
+ const tw = vb[2], th = vb[3];
359
+ return {
360
+ width: tw - this.padding.left - this.padding.right,
361
+ height: th - this.padding.top - this.padding.bottom,
362
+ totalWidth: tw,
363
+ totalHeight: th
364
+ };
365
+ }
366
+ color(i) {
367
+ return this.theme.colors[i % this.theme.colors.length];
368
+ }
369
+ g(className) {
370
+ return misc.createSVGElement("g", { className });
371
+ }
372
+ }
373
+ class PieRenderer extends BaseRenderer {
374
+ render() {
375
+ const d = this.dims();
376
+ const cx = d.totalWidth / 2;
377
+ const cy = d.totalHeight / 2;
378
+ const r = Math.min(d.width, d.height) / 2 - 20;
379
+ const group = this.g("chartforge-pie");
380
+ this.group.appendChild(group);
381
+ this._drawSlices(group, cx, cy, r);
382
+ if (this.config.animation?.enabled) this._animate(group);
383
+ }
384
+ _drawSlices(group, cx, cy, r, innerR = 0) {
385
+ const values = this.data.series[0].data;
386
+ const total = values.reduce((s, v) => s + v, 0);
387
+ let angle = 0;
388
+ values.forEach((value, i) => {
389
+ const sweep = value / total * 360;
390
+ const endAngle = angle + sweep;
391
+ const path = this._slice(cx, cy, r, innerR, angle, endAngle, i);
392
+ group.appendChild(path);
393
+ if (innerR === 0) {
394
+ const mid = angle + sweep / 2;
395
+ const lr = r * 0.7;
396
+ const lpos = misc.polarToCartesian(cx, cy, lr, mid);
397
+ const text = misc.createSVGElement("text", {
398
+ x: lpos.x,
399
+ y: lpos.y,
400
+ "text-anchor": "middle",
401
+ "dominant-baseline": "middle",
402
+ fill: "#fff",
403
+ "font-size": "12",
404
+ "font-weight": "bold"
405
+ });
406
+ text.textContent = `${(value / total * 100).toFixed(1)}%`;
407
+ group.appendChild(text);
408
+ }
409
+ angle = endAngle;
410
+ });
411
+ }
412
+ _slice(cx, cy, outerR, innerR, startAngle, endAngle, i) {
413
+ const oStart = misc.polarToCartesian(cx, cy, outerR, endAngle);
414
+ const oEnd = misc.polarToCartesian(cx, cy, outerR, startAngle);
415
+ const large = endAngle - startAngle <= 180 ? "0" : "1";
416
+ const value = this.data.series[0].data[i];
417
+ let d;
418
+ if (innerR > 0) {
419
+ const iStart = misc.polarToCartesian(cx, cy, innerR, endAngle);
420
+ const iEnd = misc.polarToCartesian(cx, cy, innerR, startAngle);
421
+ d = [
422
+ `M ${oStart.x} ${oStart.y}`,
423
+ `A ${outerR} ${outerR} 0 ${large} 0 ${oEnd.x} ${oEnd.y}`,
424
+ `L ${iEnd.x} ${iEnd.y}`,
425
+ `A ${innerR} ${innerR} 0 ${large} 1 ${iStart.x} ${iStart.y}`,
426
+ "Z"
427
+ ].join(" ");
428
+ } else {
429
+ d = [
430
+ `M ${cx} ${cy}`,
431
+ `L ${oStart.x} ${oStart.y}`,
432
+ `A ${outerR} ${outerR} 0 ${large} 0 ${oEnd.x} ${oEnd.y}`,
433
+ "Z"
434
+ ].join(" ");
435
+ }
436
+ const path = misc.createSVGElement("path", {
437
+ d,
438
+ fill: this.color(i),
439
+ stroke: this.theme.background,
440
+ "stroke-width": "2"
441
+ });
442
+ path.addEventListener("mouseenter", () => {
443
+ path.setAttribute("opacity", "0.8");
444
+ this.chart.emit("hover", { type: "pie", index: i, value });
445
+ });
446
+ path.addEventListener("mouseleave", () => path.setAttribute("opacity", "1"));
447
+ path.addEventListener("click", () => this.chart.emit("click", { type: "pie", index: i, value }));
448
+ return path;
449
+ }
450
+ _animate(group) {
451
+ group.querySelectorAll("path").forEach((path, i) => {
452
+ path.style.transformOrigin = "center";
453
+ path.style.transform = "scale(0)";
454
+ this.chart.animationEngine.animate(
455
+ `pie-${i}`,
456
+ 0,
457
+ 1,
458
+ this.config.animation?.duration ?? 750,
459
+ "easeOutElastic",
460
+ (v) => {
461
+ path.style.transform = `scale(${v})`;
462
+ }
463
+ );
464
+ });
465
+ }
466
+ }
467
+ class DonutRenderer extends PieRenderer {
468
+ render() {
469
+ const d = this.dims();
470
+ const cx = d.totalWidth / 2;
471
+ const cy = d.totalHeight / 2;
472
+ const outerR = Math.min(d.width, d.height) / 2 - 20;
473
+ const innerR = outerR * 0.6;
474
+ const group = this.g("chartforge-donut");
475
+ this.group.appendChild(group);
476
+ this._drawSlices(group, cx, cy, outerR, innerR);
477
+ const label = misc.createSVGElement("text", {
478
+ x: cx,
479
+ y: cy,
480
+ "text-anchor": "middle",
481
+ "dominant-baseline": "middle",
482
+ fill: this.theme.text,
483
+ "font-size": "24",
484
+ "font-weight": "bold"
485
+ });
486
+ label.textContent = "Total";
487
+ group.appendChild(label);
488
+ if (this.config.animation?.enabled) this._animate(group);
489
+ }
490
+ }
491
+ class ColumnRenderer extends BaseRenderer {
492
+ render() {
493
+ const d = this.dims();
494
+ const values = this.data.series[0].data;
495
+ const maxVal = Math.max(...values);
496
+ const cw = d.width / values.length * 0.8;
497
+ const gap = d.width / values.length * 0.2;
498
+ const group = this.g("chartforge-columns");
499
+ this.group.appendChild(group);
500
+ values.forEach((value, i) => {
501
+ const targetH = value / maxVal * d.height;
502
+ const x = this.padding.left + i * (cw + gap);
503
+ const baseY = this.padding.top + d.height;
504
+ const rect = misc.createSVGElement("rect", {
505
+ x,
506
+ y: baseY,
507
+ width: cw,
508
+ height: 0,
509
+ fill: this.color(i)
510
+ });
511
+ group.appendChild(rect);
512
+ if (this.config.animation?.enabled) {
513
+ this.chart.animationEngine.animate(
514
+ `col-${i}`,
515
+ 0,
516
+ targetH,
517
+ this.config.animation.duration ?? 750,
518
+ this.config.animation.easing ?? "easeOutQuad",
519
+ (h) => {
520
+ rect.setAttribute("height", String(h));
521
+ rect.setAttribute("y", String(baseY - h));
522
+ }
523
+ );
524
+ } else {
525
+ rect.setAttribute("height", String(targetH));
526
+ rect.setAttribute("y", String(baseY - targetH));
527
+ }
528
+ rect.addEventListener("mouseenter", () => {
529
+ rect.setAttribute("opacity", "0.8");
530
+ this.chart.emit("hover", { type: "column", index: i, value });
531
+ });
532
+ rect.addEventListener("mouseleave", () => rect.setAttribute("opacity", "1"));
533
+ rect.addEventListener("click", () => this.chart.emit("click", { type: "column", index: i, value }));
534
+ });
535
+ }
536
+ }
537
+ class BarRenderer extends BaseRenderer {
538
+ render() {
539
+ const d = this.dims();
540
+ const values = this.data.series[0].data;
541
+ const maxVal = Math.max(...values);
542
+ const bh = d.height / values.length * 0.8;
543
+ const gap = d.height / values.length * 0.2;
544
+ const group = this.g("chartforge-bars");
545
+ this.group.appendChild(group);
546
+ values.forEach((value, i) => {
547
+ const targetW = value / maxVal * d.width;
548
+ const y = this.padding.top + i * (bh + gap);
549
+ const rect = misc.createSVGElement("rect", {
550
+ x: this.padding.left,
551
+ y,
552
+ width: 0,
553
+ height: bh,
554
+ fill: this.color(i)
555
+ });
556
+ group.appendChild(rect);
557
+ if (this.config.animation?.enabled) {
558
+ this.chart.animationEngine.animate(
559
+ `bar-${i}`,
560
+ 0,
561
+ targetW,
562
+ this.config.animation.duration ?? 750,
563
+ this.config.animation.easing ?? "easeOutQuad",
564
+ (w) => rect.setAttribute("width", String(w))
565
+ );
566
+ } else {
567
+ rect.setAttribute("width", String(targetW));
568
+ }
569
+ rect.addEventListener("mouseenter", () => {
570
+ rect.setAttribute("opacity", "0.8");
571
+ this.chart.emit("hover", { type: "bar", index: i, value });
572
+ });
573
+ rect.addEventListener("mouseleave", () => rect.setAttribute("opacity", "1"));
574
+ rect.addEventListener("click", () => this.chart.emit("click", { type: "bar", index: i, value }));
575
+ const label = misc.createSVGElement("text", {
576
+ x: this.padding.left + 6,
577
+ y: y + bh / 2,
578
+ fill: "#fff",
579
+ "font-size": "12",
580
+ "dominant-baseline": "middle"
581
+ });
582
+ label.textContent = String(this.data.labels?.[i] ?? `Item ${i + 1}`);
583
+ group.appendChild(label);
584
+ });
585
+ }
586
+ }
587
+ class LineRenderer extends BaseRenderer {
588
+ render() {
589
+ const d = this.dims();
590
+ const group = this.g("chartforge-lines");
591
+ this.group.appendChild(group);
592
+ const allVals = this.data.series.flatMap((s) => s.data);
593
+ const maxVal = Math.max(...allVals);
594
+ const minVal = Math.min(...allVals);
595
+ const range = maxVal - minVal || 1;
596
+ this.data.series.forEach((series, si) => {
597
+ const values = series.data;
598
+ const pts = values.map((v, i) => ({
599
+ x: this.padding.left + i / Math.max(values.length - 1, 1) * d.width,
600
+ y: this.padding.top + d.height - (v - minVal) / range * d.height,
601
+ value: v
602
+ }));
603
+ const pathD = pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
604
+ const path = misc.createSVGElement("path", {
605
+ d: pathD,
606
+ fill: "none",
607
+ stroke: this.color(si),
608
+ "stroke-width": "2"
609
+ });
610
+ group.appendChild(path);
611
+ pts.forEach((pt, i) => {
612
+ const circle = misc.createSVGElement("circle", {
613
+ cx: pt.x,
614
+ cy: pt.y,
615
+ r: 4,
616
+ fill: this.color(si)
617
+ });
618
+ circle.addEventListener("mouseenter", () => {
619
+ circle.setAttribute("r", "6");
620
+ this.chart.emit("hover", { type: "line", seriesIndex: si, index: i, value: pt.value });
621
+ });
622
+ circle.addEventListener("mouseleave", () => circle.setAttribute("r", "4"));
623
+ circle.addEventListener("click", () => this.chart.emit("click", { type: "line", seriesIndex: si, index: i, value: pt.value }));
624
+ group.appendChild(circle);
625
+ });
626
+ if (this.config.animation?.enabled) {
627
+ const len = path.getTotalLength();
628
+ path.style.strokeDasharray = String(len);
629
+ path.style.strokeDashoffset = String(len);
630
+ this.chart.animationEngine.animate(
631
+ `line-${si}`,
632
+ len,
633
+ 0,
634
+ this.config.animation.duration ?? 750,
635
+ this.config.animation.easing ?? "easeOutQuad",
636
+ (off) => {
637
+ path.style.strokeDashoffset = String(off);
638
+ }
639
+ );
640
+ }
641
+ });
642
+ }
643
+ }
644
+ class ScatterRenderer extends BaseRenderer {
645
+ render() {
646
+ const d = this.dims();
647
+ const group = this.g("chartforge-scatter");
648
+ this.group.appendChild(group);
649
+ this.data.series.forEach((series, si) => {
650
+ const pts = series.data;
651
+ const maxX = Math.max(...pts.map((p) => p.x));
652
+ const maxY = Math.max(...pts.map((p) => p.y));
653
+ pts.forEach((pt, i) => {
654
+ const cx = this.padding.left + pt.x / maxX * d.width;
655
+ const cy = this.padding.top + d.height - pt.y / maxY * d.height;
656
+ const r0 = pt.r ?? 5;
657
+ const circle = misc.createSVGElement("circle", {
658
+ cx,
659
+ cy,
660
+ r: this.config.animation?.enabled ? 0 : r0,
661
+ fill: this.color(si),
662
+ opacity: "0.7"
663
+ });
664
+ group.appendChild(circle);
665
+ if (this.config.animation?.enabled) {
666
+ this.chart.animationEngine.animate(
667
+ `scatter-${si}-${i}`,
668
+ 0,
669
+ r0,
670
+ this.config.animation.duration ?? 750,
671
+ "easeOutElastic",
672
+ (r) => circle.setAttribute("r", String(r))
673
+ );
674
+ }
675
+ circle.addEventListener("mouseenter", () => {
676
+ circle.setAttribute("r", String(r0 * 1.5));
677
+ circle.setAttribute("opacity", "1");
678
+ this.chart.emit("hover", { type: "scatter", seriesIndex: si, index: i, point: pt });
679
+ });
680
+ circle.addEventListener("mouseleave", () => {
681
+ circle.setAttribute("r", String(r0));
682
+ circle.setAttribute("opacity", "0.7");
683
+ });
684
+ circle.addEventListener("click", () => this.chart.emit("click", { type: "scatter", seriesIndex: si, index: i, point: pt }));
685
+ });
686
+ });
687
+ }
688
+ }
689
+ class StackedColumnRenderer extends BaseRenderer {
690
+ render() {
691
+ const d = this.dims();
692
+ const labels = this.data.labels ?? [];
693
+ const nCats = labels.length;
694
+ const cw = d.width / nCats * 0.8;
695
+ const gap = d.width / nCats * 0.2;
696
+ const totals = Array.from(
697
+ { length: nCats },
698
+ (_, i) => this.data.series.reduce((s, ser) => s + (ser.data[i] ?? 0), 0)
699
+ );
700
+ const maxTotal = Math.max(...totals);
701
+ const group = this.g("chartforge-stacked-columns");
702
+ this.group.appendChild(group);
703
+ for (let ci = 0; ci < nCats; ci++) {
704
+ let yOff = 0;
705
+ const x = this.padding.left + ci * (cw + gap);
706
+ const baseY = this.padding.top + d.height;
707
+ this.data.series.forEach((series, si) => {
708
+ const value = series.data[ci] ?? 0;
709
+ const targetH = value / maxTotal * d.height;
710
+ const rect = misc.createSVGElement("rect", {
711
+ x,
712
+ y: baseY - yOff,
713
+ width: cw,
714
+ height: 0,
715
+ fill: this.color(si)
716
+ });
717
+ group.appendChild(rect);
718
+ const capturedYOff = yOff;
719
+ if (this.config.animation?.enabled) {
720
+ this.chart.animationEngine.animate(
721
+ `scol-${ci}-${si}`,
722
+ 0,
723
+ targetH,
724
+ this.config.animation.duration ?? 750,
725
+ this.config.animation.easing ?? "easeOutQuad",
726
+ (h) => {
727
+ rect.setAttribute("height", String(h));
728
+ rect.setAttribute("y", String(baseY - capturedYOff - h));
729
+ }
730
+ );
731
+ } else {
732
+ rect.setAttribute("height", String(targetH));
733
+ rect.setAttribute("y", String(baseY - capturedYOff - targetH));
734
+ }
735
+ rect.addEventListener("mouseenter", () => {
736
+ rect.setAttribute("opacity", "0.8");
737
+ this.chart.emit("hover", { type: "stackedColumn", catIndex: ci, seriesIndex: si, value });
738
+ });
739
+ rect.addEventListener("mouseleave", () => rect.setAttribute("opacity", "1"));
740
+ yOff += targetH;
741
+ });
742
+ }
743
+ }
744
+ }
745
+ class StackedBarRenderer extends BaseRenderer {
746
+ render() {
747
+ const d = this.dims();
748
+ const nCats = (this.data.labels ?? []).length;
749
+ const bh = d.height / nCats * 0.8;
750
+ const gap = d.height / nCats * 0.2;
751
+ const totals = Array.from(
752
+ { length: nCats },
753
+ (_, i) => this.data.series.reduce((s, ser) => s + (ser.data[i] ?? 0), 0)
754
+ );
755
+ const maxTotal = Math.max(...totals);
756
+ const group = this.g("chartforge-stacked-bars");
757
+ this.group.appendChild(group);
758
+ for (let ci = 0; ci < nCats; ci++) {
759
+ let xOff = 0;
760
+ const y = this.padding.top + ci * (bh + gap);
761
+ this.data.series.forEach((series, si) => {
762
+ const value = series.data[ci] ?? 0;
763
+ const targetW = value / maxTotal * d.width;
764
+ const rect = misc.createSVGElement("rect", {
765
+ x: this.padding.left + xOff,
766
+ y,
767
+ width: 0,
768
+ height: bh,
769
+ fill: this.color(si)
770
+ });
771
+ group.appendChild(rect);
772
+ const capturedXOff = xOff;
773
+ if (this.config.animation?.enabled) {
774
+ this.chart.animationEngine.animate(
775
+ `sbar-${ci}-${si}`,
776
+ 0,
777
+ targetW,
778
+ this.config.animation.duration ?? 750,
779
+ this.config.animation.easing ?? "easeOutQuad",
780
+ (w) => {
781
+ rect.setAttribute("width", String(w));
782
+ rect.setAttribute("x", String(this.padding.left + capturedXOff));
783
+ }
784
+ );
785
+ } else {
786
+ rect.setAttribute("width", String(targetW));
787
+ }
788
+ rect.addEventListener("mouseenter", () => {
789
+ rect.setAttribute("opacity", "0.8");
790
+ this.chart.emit("hover", { type: "stackedBar", catIndex: ci, seriesIndex: si, value });
791
+ });
792
+ rect.addEventListener("mouseleave", () => rect.setAttribute("opacity", "1"));
793
+ xOff += targetW;
794
+ });
795
+ }
796
+ }
797
+ }
798
+ class FunnelRenderer extends BaseRenderer {
799
+ render() {
800
+ const d = this.dims();
801
+ const values = this.data.series[0].data;
802
+ const maxVal = Math.max(...values);
803
+ const segH = d.height / values.length;
804
+ const group = this.g("chartforge-funnel");
805
+ this.group.appendChild(group);
806
+ values.forEach((value, i) => {
807
+ const nextVal = values[i + 1] ?? 0;
808
+ const topW = value / maxVal * d.width;
809
+ const botW = nextVal / maxVal * d.width;
810
+ const y = this.padding.top + i * segH;
811
+ const topL = this.padding.left + (d.width - topW) / 2;
812
+ const botL = this.padding.left + (d.width - botW) / 2;
813
+ const poly = misc.createSVGElement("polygon", {
814
+ points: `${topL},${y} ${topL + topW},${y} ${botL + botW},${y + segH} ${botL},${y + segH}`,
815
+ fill: this.color(i),
816
+ stroke: this.theme.background,
817
+ "stroke-width": "2"
818
+ });
819
+ group.appendChild(poly);
820
+ const label = misc.createSVGElement("text", {
821
+ x: d.totalWidth / 2,
822
+ y: y + segH / 2,
823
+ fill: "#fff",
824
+ "font-size": "14",
825
+ "text-anchor": "middle",
826
+ "dominant-baseline": "middle",
827
+ "font-weight": "bold"
828
+ });
829
+ label.textContent = `${this.data.labels?.[i] ?? `Stage ${i + 1}`}: ${value}`;
830
+ group.appendChild(label);
831
+ poly.addEventListener("mouseenter", () => {
832
+ poly.setAttribute("opacity", "0.8");
833
+ this.chart.emit("hover", { type: "funnel", index: i, value });
834
+ });
835
+ poly.addEventListener("mouseleave", () => poly.setAttribute("opacity", "1"));
836
+ });
837
+ }
838
+ }
839
+ class HeatmapRenderer extends BaseRenderer {
840
+ render() {
841
+ const d = this.dims();
842
+ const grid = this.data.series[0].data;
843
+ const rows = grid.length;
844
+ const cols = grid[0]?.length ?? 0;
845
+ const cw = d.width / cols;
846
+ const ch = d.height / rows;
847
+ const flat = grid.flat();
848
+ const min = Math.min(...flat);
849
+ const max = Math.max(...flat);
850
+ const rng = max - min || 1;
851
+ const group = this.g("chartforge-heatmap");
852
+ this.group.appendChild(group);
853
+ grid.forEach((row, ri) => {
854
+ row.forEach((value, ci) => {
855
+ const intensity = (value - min) / rng;
856
+ const r = Math.round(intensity * 255);
857
+ const b = Math.round((1 - intensity) * 255);
858
+ const fill = `rgb(${r},100,${b})`;
859
+ const rect = misc.createSVGElement("rect", {
860
+ x: this.padding.left + ci * cw,
861
+ y: this.padding.top + ri * ch,
862
+ width: cw,
863
+ height: ch,
864
+ fill,
865
+ stroke: this.theme.background,
866
+ "stroke-width": "1"
867
+ });
868
+ group.appendChild(rect);
869
+ rect.addEventListener("mouseenter", () => {
870
+ rect.setAttribute("stroke-width", "2");
871
+ this.chart.emit("hover", { type: "heatmap", row: ri, col: ci, value });
872
+ });
873
+ rect.addEventListener("mouseleave", () => rect.setAttribute("stroke-width", "1"));
874
+ });
875
+ });
876
+ }
877
+ }
878
+ class CandlestickRenderer extends BaseRenderer {
879
+ render() {
880
+ const d = this.dims();
881
+ const candles = this.data.series[0].data;
882
+ const allVals = candles.flatMap((c) => [c.open, c.high, c.low, c.close]);
883
+ const minVal = Math.min(...allVals);
884
+ const maxVal = Math.max(...allVals);
885
+ const range = maxVal - minVal || 1;
886
+ const cw = d.width / candles.length * 0.7;
887
+ const gap = d.width / candles.length * 0.3;
888
+ const group = this.g("chartforge-candlestick");
889
+ this.group.appendChild(group);
890
+ const toY = (v) => this.padding.top + d.height - (v - minVal) / range * d.height;
891
+ candles.forEach((candle, i) => {
892
+ const cx = this.padding.left + i * (cw + gap) + cw / 2;
893
+ const openY = toY(candle.open);
894
+ const closeY = toY(candle.close);
895
+ const highY = toY(candle.high);
896
+ const lowY = toY(candle.low);
897
+ const positive = candle.close >= candle.open;
898
+ const color = positive ? "#10b981" : "#ef4444";
899
+ const wick = misc.createSVGElement("line", {
900
+ x1: cx,
901
+ y1: highY,
902
+ x2: cx,
903
+ y2: lowY,
904
+ stroke: color,
905
+ "stroke-width": "1"
906
+ });
907
+ group.appendChild(wick);
908
+ const bodyY = Math.min(openY, closeY);
909
+ const bodyH = Math.max(Math.abs(closeY - openY), 1);
910
+ const body = misc.createSVGElement("rect", {
911
+ x: cx - cw / 2,
912
+ y: bodyY,
913
+ width: cw,
914
+ height: bodyH,
915
+ fill: positive ? color : "#ffffff",
916
+ stroke: color,
917
+ "stroke-width": "2"
918
+ });
919
+ group.appendChild(body);
920
+ body.addEventListener("mouseenter", () => {
921
+ body.setAttribute("opacity", "0.8");
922
+ this.chart.emit("hover", { type: "candlestick", index: i, candle });
923
+ });
924
+ body.addEventListener("mouseleave", () => body.setAttribute("opacity", "1"));
925
+ body.addEventListener("click", () => this.chart.emit("click", { type: "candlestick", index: i, candle }));
926
+ });
927
+ }
928
+ }
929
+ const RENDERERS = {
930
+ pie: PieRenderer,
931
+ donut: DonutRenderer,
932
+ column: ColumnRenderer,
933
+ bar: BarRenderer,
934
+ row: BarRenderer,
935
+ line: LineRenderer,
936
+ scatter: ScatterRenderer,
937
+ stackedColumn: StackedColumnRenderer,
938
+ stackedBar: StackedBarRenderer,
939
+ funnel: FunnelRenderer,
940
+ heatmap: HeatmapRenderer,
941
+ candlestick: CandlestickRenderer
942
+ };
943
+ const DEFAULT_CONFIG = {
944
+ width: "auto",
945
+ height: 400,
946
+ responsive: true,
947
+ theme: "light",
948
+ animation: { enabled: true, duration: 750, easing: "easeOutQuad" },
949
+ plugins: {},
950
+ middleware: [],
951
+ virtual: { enabled: false, threshold: 1e4 },
952
+ padding: { top: 40, right: 40, bottom: 60, left: 60 }
953
+ };
954
+ class ChartForge {
955
+ constructor(container, config) {
956
+ this.initialized = false;
957
+ this._resizeObserver = null;
958
+ this._rendering = false;
959
+ this.id = misc.uid();
960
+ this.container = typeof container === "string" ? document.querySelector(container) : container;
961
+ if (!this.container) throw new Error("[ChartForge] Container not found");
962
+ this.config = misc.merge({ ...DEFAULT_CONFIG }, config);
963
+ this.eventBus = new EventBus();
964
+ this.middleware = new MiddlewarePipeline();
965
+ this.dataPipeline = new DataPipeline();
966
+ this.animationEngine = new AnimationEngine();
967
+ this.themeManager = new ThemeManager();
968
+ this.pluginManager = new PluginManager(this);
969
+ this.virtualRenderer = new VirtualRenderer(this);
970
+ this.realTime = new RealTimeModule(this);
971
+ for (const [name, t] of Object.entries(builtins.BUILT_IN_THEMES)) {
972
+ this.themeManager.register(name, t);
973
+ }
974
+ this.realTime.registerAdapter("websocket", WebSocketAdapter);
975
+ this.realTime.registerAdapter("polling", PollingAdapter);
976
+ this._init();
977
+ }
978
+ _init() {
979
+ const theme = this.themeManager.apply(this.config.theme ?? "light");
980
+ this.theme = theme ?? builtins.BUILT_IN_THEMES["light"];
981
+ this._createSVG();
982
+ this._setupMiddleware();
983
+ if (this.config.responsive) {
984
+ this._resizeObserver = new ResizeObserver(misc.debounce(() => this.resize(), 150));
985
+ this._resizeObserver.observe(this.container);
986
+ }
987
+ this.initialized = true;
988
+ this.pluginManager.initAll();
989
+ void this.render();
990
+ }
991
+ _createSVG() {
992
+ const w = this.config.width === "auto" ? this.container.offsetWidth || 600 : this.config.width ?? 600;
993
+ const h = this.config.height ?? 400;
994
+ this.svg = misc.createSVGElement("svg", {
995
+ width: "100%",
996
+ height: "100%",
997
+ viewBox: `0 0 ${w} ${h}`,
998
+ className: "chartforge-svg",
999
+ role: "img"
1000
+ });
1001
+ this.svg.appendChild(misc.createSVGElement("defs"));
1002
+ this.mainGroup = misc.createSVGElement("g", { className: "chartforge-main" });
1003
+ this.svg.appendChild(this.mainGroup);
1004
+ this.svg.style.cssText = `
1005
+ display:block;
1006
+ font-family:'system-ui', sans-serif;
1007
+ background:${this.theme.background};
1008
+ border-radius:inherit;
1009
+ `;
1010
+ this.container.appendChild(this.svg);
1011
+ }
1012
+ _setupMiddleware() {
1013
+ this.middleware.use(async (ctx, next) => {
1014
+ this.eventBus.emit("beforeRender", ctx);
1015
+ await next();
1016
+ });
1017
+ for (const fn of this.config.middleware ?? []) {
1018
+ this.middleware.use(fn);
1019
+ }
1020
+ }
1021
+ async render() {
1022
+ if (this._rendering) return;
1023
+ this._rendering = true;
1024
+ try {
1025
+ const ctx = {
1026
+ data: this.config.data,
1027
+ theme: this.theme,
1028
+ svg: this.svg,
1029
+ mainGroup: this.mainGroup
1030
+ };
1031
+ await this.middleware.execute(ctx);
1032
+ const data = await this.dataPipeline.transform(this.config.data, this.config);
1033
+ const useVirtual = this.config.virtual?.enabled && this.virtualRenderer.shouldVirtualize();
1034
+ this._renderData(useVirtual ? this.virtualRenderer.getVisibleData() : data);
1035
+ this.eventBus.emit("afterRender", ctx);
1036
+ } finally {
1037
+ this._rendering = false;
1038
+ }
1039
+ }
1040
+ _renderData(data) {
1041
+ misc.removeChildren(this.mainGroup);
1042
+ const RendererClass = RENDERERS[this.config.type];
1043
+ if (!RendererClass) {
1044
+ console.error(`[ChartForge] Unknown chart type: "${this.config.type}"`);
1045
+ return;
1046
+ }
1047
+ new RendererClass(this, data).render();
1048
+ }
1049
+ updateData(data) {
1050
+ this.config.data = misc.merge({ ...this.config.data }, data);
1051
+ void this.render();
1052
+ }
1053
+ updateConfig(config) {
1054
+ this.config = misc.merge({ ...this.config }, config);
1055
+ void this.render();
1056
+ }
1057
+ setTheme(name) {
1058
+ const t = this.themeManager.apply(name);
1059
+ if (!t) return;
1060
+ this.theme = t;
1061
+ this.svg.style.background = t.background;
1062
+ void this.render();
1063
+ }
1064
+ use(name, Plugin, config) {
1065
+ this.pluginManager.register(name, Plugin, config);
1066
+ return this;
1067
+ }
1068
+ getPlugin(name) {
1069
+ return this.pluginManager.get(name);
1070
+ }
1071
+ setViewport(start, end) {
1072
+ this.virtualRenderer.updateViewport(start, end);
1073
+ void this.render();
1074
+ }
1075
+ resize() {
1076
+ const w = this.container.offsetWidth || 600;
1077
+ const h = this.config.height ?? 400;
1078
+ this.svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
1079
+ void this.render();
1080
+ }
1081
+ on(event, handler, priority) {
1082
+ return this.eventBus.on(event, handler, priority);
1083
+ }
1084
+ off(event, handler) {
1085
+ this.eventBus.off(event, handler);
1086
+ }
1087
+ emit(event, data) {
1088
+ this.eventBus.emit(event, data);
1089
+ }
1090
+ destroy() {
1091
+ this.animationEngine.stopAll();
1092
+ this.realTime.disconnectAll();
1093
+ this.pluginManager.destroyAll();
1094
+ this._resizeObserver?.disconnect();
1095
+ this.eventBus.clear();
1096
+ this.svg?.parentNode?.removeChild(this.svg);
1097
+ }
1098
+ static create(container, config) {
1099
+ return new ChartForge(container, config);
1100
+ }
1101
+ static registerTheme(name, theme) {
1102
+ ChartForge._globalThemes.set(name, theme);
1103
+ }
1104
+ static {
1105
+ this._globalThemes = /* @__PURE__ */ new Map();
1106
+ }
1107
+ }
1108
+ exports.ChartForge = ChartForge;