chartforge 0.0.1
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/README.md +1339 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
# ⬡ ChartForge
|
|
2
|
+
|
|
3
|
+
> **Production-grade, modular, zero-dependency SVG charting library — built with TypeScript.**
|
|
4
|
+
>
|
|
5
|
+
> Works in vanilla HTML, React, Vue, Angular, Laravel, Node.js (SSR), and any bundler or CDN setup.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [⬡ ChartForge](#-chartforge)
|
|
12
|
+
- [Table of Contents](#table-of-contents)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Framework Guides](#framework-guides)
|
|
16
|
+
- [Vanilla HTML / CDN](#vanilla-html--cdn)
|
|
17
|
+
- [Via `<script type="module">` (modern)](#via-script-typemodule-modern)
|
|
18
|
+
- [Via UMD `<script>` tag (legacy / Laravel CDN)](#via-umd-script-tag-legacy--laravel-cdn)
|
|
19
|
+
- [React](#react)
|
|
20
|
+
- [React Hook](#react-hook)
|
|
21
|
+
- [Vue 3](#vue-3)
|
|
22
|
+
- [Vue Composable](#vue-composable)
|
|
23
|
+
- [Angular](#angular)
|
|
24
|
+
- [Laravel (Blade + Vite)](#laravel-blade--vite)
|
|
25
|
+
- [1. Install via npm (Laravel project root)](#1-install-via-npm-laravel-project-root)
|
|
26
|
+
- [2. Add to `resources/js/app.js`](#2-add-to-resourcesjsappjs)
|
|
27
|
+
- [3. Blade template](#3-blade-template)
|
|
28
|
+
- [4. Or as a self-contained Blade component](#4-or-as-a-self-contained-blade-component)
|
|
29
|
+
- [Node.js / SSR](#nodejs--ssr)
|
|
30
|
+
- [Chart Types](#chart-types)
|
|
31
|
+
- [Configuration Reference](#configuration-reference)
|
|
32
|
+
- [Themes](#themes)
|
|
33
|
+
- [Built-in themes](#built-in-themes)
|
|
34
|
+
- [Custom theme](#custom-theme)
|
|
35
|
+
- [Plugins](#plugins)
|
|
36
|
+
- [Usage pattern](#usage-pattern)
|
|
37
|
+
- [Tooltip Plugin](#tooltip-plugin)
|
|
38
|
+
- [Legend Plugin](#legend-plugin)
|
|
39
|
+
- [Axis Plugin](#axis-plugin)
|
|
40
|
+
- [Grid Plugin](#grid-plugin)
|
|
41
|
+
- [Crosshair Plugin](#crosshair-plugin)
|
|
42
|
+
- [Data Labels Plugin](#data-labels-plugin)
|
|
43
|
+
- [Export Plugin](#export-plugin)
|
|
44
|
+
- [Zoom \& Pan Plugin](#zoom--pan-plugin)
|
|
45
|
+
- [Annotation Plugin](#annotation-plugin)
|
|
46
|
+
- [Adapters (Real-Time Data)](#adapters-real-time-data)
|
|
47
|
+
- [WebSocket Adapter](#websocket-adapter)
|
|
48
|
+
- [Polling Adapter](#polling-adapter)
|
|
49
|
+
- [Custom Adapter](#custom-adapter)
|
|
50
|
+
- [Events \& API](#events--api)
|
|
51
|
+
- [Chart Events](#chart-events)
|
|
52
|
+
- [Instance API](#instance-api)
|
|
53
|
+
- [Static API](#static-api)
|
|
54
|
+
- [Advanced Usage](#advanced-usage)
|
|
55
|
+
- [Middleware](#middleware)
|
|
56
|
+
- [Data Pipeline (Transformers)](#data-pipeline-transformers)
|
|
57
|
+
- [Virtual Rendering](#virtual-rendering)
|
|
58
|
+
- [Extending ChartForge](#extending-chartforge)
|
|
59
|
+
- [Custom Plugin](#custom-plugin)
|
|
60
|
+
- [Custom Renderer](#custom-renderer)
|
|
61
|
+
- [Custom Theme](#custom-theme-1)
|
|
62
|
+
- [Custom Adapter](#custom-adapter-1)
|
|
63
|
+
- [Architecture](#architecture)
|
|
64
|
+
- [Build Reference](#build-reference)
|
|
65
|
+
- [Build outputs](#build-outputs)
|
|
66
|
+
- [License](#license)
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install chartforge
|
|
74
|
+
# or
|
|
75
|
+
yarn add chartforge
|
|
76
|
+
# or
|
|
77
|
+
pnpm add chartforge
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Quick Start
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { ChartForge } from 'chartforge';
|
|
86
|
+
import { TooltipPlugin, AxisPlugin, GridPlugin } from 'chartforge/plugins';
|
|
87
|
+
|
|
88
|
+
const chart = new ChartForge('#chart', {
|
|
89
|
+
type: 'column',
|
|
90
|
+
theme: 'dark',
|
|
91
|
+
data: {
|
|
92
|
+
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
|
|
93
|
+
series: [{ name: 'Revenue', data: [65, 78, 72, 85, 92, 88] }],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
chart.use('tooltip', TooltipPlugin)
|
|
98
|
+
.use('axis', AxisPlugin)
|
|
99
|
+
.use('grid', GridPlugin);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Framework Guides
|
|
105
|
+
|
|
106
|
+
### Vanilla HTML / CDN
|
|
107
|
+
|
|
108
|
+
#### Via `<script type="module">` (modern)
|
|
109
|
+
|
|
110
|
+
```html
|
|
111
|
+
<!DOCTYPE html>
|
|
112
|
+
<html>
|
|
113
|
+
<head>
|
|
114
|
+
<style> #chart { width: 100%; height: 400px; } </style>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
<div id="chart"></div>
|
|
118
|
+
|
|
119
|
+
<script type="module">
|
|
120
|
+
// From CDN (replace with actual CDN URL after publishing)
|
|
121
|
+
import { ChartForge } from 'https://unpkg.com/chartforge/dist/chartforge.js';
|
|
122
|
+
import { TooltipPlugin } from 'https://unpkg.com/chartforge/dist/plugins.js';
|
|
123
|
+
|
|
124
|
+
const chart = new ChartForge('#chart', {
|
|
125
|
+
type: 'line',
|
|
126
|
+
theme: 'dark',
|
|
127
|
+
data: {
|
|
128
|
+
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
|
|
129
|
+
series: [
|
|
130
|
+
{ name: 'Sales', data: [100, 120, 115, 134, 168] },
|
|
131
|
+
{ name: 'Visits', data: [200, 240, 220, 260, 310] },
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
animation: { enabled: true, duration: 800, easing: 'easeOutElastic' },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
chart.use('tooltip', TooltipPlugin);
|
|
138
|
+
</script>
|
|
139
|
+
</body>
|
|
140
|
+
</html>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Via UMD `<script>` tag (legacy / Laravel CDN)
|
|
144
|
+
|
|
145
|
+
```html
|
|
146
|
+
<!-- UMD build — exposes window.ChartForge -->
|
|
147
|
+
<script src="https://unpkg.com/chartforge/dist/chartforge.umd.cjs"></script>
|
|
148
|
+
<script src="https://unpkg.com/chartforge/dist/plugins.umd.cjs"></script>
|
|
149
|
+
|
|
150
|
+
<div id="chart" style="height:400px"></div>
|
|
151
|
+
|
|
152
|
+
<script>
|
|
153
|
+
const { ChartForge } = window.ChartForge;
|
|
154
|
+
const { TooltipPlugin } = window.ChartForgePlugins;
|
|
155
|
+
|
|
156
|
+
const chart = new ChartForge('#chart', {
|
|
157
|
+
type: 'bar',
|
|
158
|
+
data: {
|
|
159
|
+
labels: ['Alpha', 'Beta', 'Gamma'],
|
|
160
|
+
series: [{ data: [42, 75, 38] }],
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
chart.use('tooltip', TooltipPlugin);
|
|
165
|
+
</script>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### React
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// components/Chart.tsx
|
|
174
|
+
import { useEffect, useRef } from 'react';
|
|
175
|
+
import { ChartForge } from 'chartforge';
|
|
176
|
+
import { TooltipPlugin } from 'chartforge/plugins';
|
|
177
|
+
import { LegendPlugin } from 'chartforge/plugins';
|
|
178
|
+
import type { ChartConfig } from 'chartforge';
|
|
179
|
+
|
|
180
|
+
interface ChartProps {
|
|
181
|
+
config: ChartConfig;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function Chart({ config }: ChartProps) {
|
|
185
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
186
|
+
const chartRef = useRef<ChartForge | null>(null);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!ref.current) return;
|
|
190
|
+
|
|
191
|
+
chartRef.current = new ChartForge(ref.current, config);
|
|
192
|
+
chartRef.current
|
|
193
|
+
.use('tooltip', TooltipPlugin)
|
|
194
|
+
.use('legend', LegendPlugin);
|
|
195
|
+
|
|
196
|
+
return () => {
|
|
197
|
+
chartRef.current?.destroy();
|
|
198
|
+
chartRef.current = null;
|
|
199
|
+
};
|
|
200
|
+
}, []); // mount/unmount only
|
|
201
|
+
|
|
202
|
+
// Update data without re-mounting
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
chartRef.current?.updateData(config.data);
|
|
205
|
+
}, [config.data]);
|
|
206
|
+
|
|
207
|
+
return <div ref={ref} style={{ width: '100%', height: 400 }} />;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
// App.tsx
|
|
213
|
+
import { useState } from 'react';
|
|
214
|
+
import { Chart } from './components/Chart';
|
|
215
|
+
|
|
216
|
+
const BASE_CONFIG = {
|
|
217
|
+
type: 'line' as const,
|
|
218
|
+
theme: 'dark',
|
|
219
|
+
data: {
|
|
220
|
+
labels: ['Jan', 'Feb', 'Mar', 'Apr'],
|
|
221
|
+
series: [{ name: 'Revenue', data: [100, 120, 115, 134] }],
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export default function App() {
|
|
226
|
+
const [config, setConfig] = useState(BASE_CONFIG);
|
|
227
|
+
|
|
228
|
+
const randomize = () =>
|
|
229
|
+
setConfig(c => ({
|
|
230
|
+
...c,
|
|
231
|
+
data: {
|
|
232
|
+
...c.data,
|
|
233
|
+
series: [{ name: 'Revenue', data: Array.from({ length: 4 }, () => Math.random() * 150 + 50) }],
|
|
234
|
+
},
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div>
|
|
239
|
+
<button onClick={randomize}>Refresh</button>
|
|
240
|
+
<Chart config={config} />
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### React Hook
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
// hooks/useChart.ts
|
|
250
|
+
import { useEffect, useRef } from 'react';
|
|
251
|
+
import { ChartForge } from 'chartforge';
|
|
252
|
+
import type { ChartConfig, PluginConstructor } from 'chartforge';
|
|
253
|
+
|
|
254
|
+
export function useChart(
|
|
255
|
+
config: ChartConfig,
|
|
256
|
+
plugins: Array<[string, PluginConstructor, unknown?]> = []
|
|
257
|
+
) {
|
|
258
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
259
|
+
const chartRef = useRef<ChartForge | null>(null);
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!containerRef.current) return;
|
|
263
|
+
const chart = new ChartForge(containerRef.current, config);
|
|
264
|
+
plugins.forEach(([name, Plugin, cfg]) => chart.use(name, Plugin, cfg));
|
|
265
|
+
chartRef.current = chart;
|
|
266
|
+
return () => { chart.destroy(); chartRef.current = null; };
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
useEffect(() => { chartRef.current?.updateData(config.data); }, [config.data]);
|
|
270
|
+
useEffect(() => { chartRef.current?.setTheme(config.theme ?? 'light'); }, [config.theme]);
|
|
271
|
+
|
|
272
|
+
return { containerRef, chart: chartRef };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Usage:
|
|
276
|
+
// const { containerRef } = useChart(config, [['tooltip', TooltipPlugin]]);
|
|
277
|
+
// return <div ref={containerRef} style={{ height: 400 }} />;
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### Vue 3
|
|
283
|
+
|
|
284
|
+
```vue
|
|
285
|
+
<!-- components/ChartForge.vue -->
|
|
286
|
+
<template>
|
|
287
|
+
<div ref="containerRef" :style="{ width: '100%', height: height + 'px' }" />
|
|
288
|
+
</template>
|
|
289
|
+
|
|
290
|
+
<script setup lang="ts">
|
|
291
|
+
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
|
292
|
+
import { ChartForge } from 'chartforge';
|
|
293
|
+
import { TooltipPlugin } from 'chartforge/plugins';
|
|
294
|
+
import type { ChartConfig } from 'chartforge';
|
|
295
|
+
|
|
296
|
+
const props = withDefaults(defineProps<{
|
|
297
|
+
config: ChartConfig;
|
|
298
|
+
height?: number;
|
|
299
|
+
}>(), { height: 400 });
|
|
300
|
+
|
|
301
|
+
const containerRef = ref<HTMLDivElement | null>(null);
|
|
302
|
+
let chart: ChartForge | null = null;
|
|
303
|
+
|
|
304
|
+
onMounted(() => {
|
|
305
|
+
if (!containerRef.value) return;
|
|
306
|
+
chart = new ChartForge(containerRef.value, props.config);
|
|
307
|
+
chart.use('tooltip', TooltipPlugin);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
onBeforeUnmount(() => {
|
|
311
|
+
chart?.destroy();
|
|
312
|
+
chart = null;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// React to data changes
|
|
316
|
+
watch(() => props.config.data, (newData) => {
|
|
317
|
+
chart?.updateData(newData);
|
|
318
|
+
}, { deep: true });
|
|
319
|
+
|
|
320
|
+
// React to theme changes
|
|
321
|
+
watch(() => props.config.theme, (theme) => {
|
|
322
|
+
chart?.setTheme(theme ?? 'dark');
|
|
323
|
+
});
|
|
324
|
+
</script>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
```vue
|
|
328
|
+
<!-- App.vue -->
|
|
329
|
+
<template>
|
|
330
|
+
<ChartForgeVue :config="config" :height="350" />
|
|
331
|
+
<button @click="refresh">Refresh</button>
|
|
332
|
+
</template>
|
|
333
|
+
|
|
334
|
+
<script setup lang="ts">
|
|
335
|
+
import { ref } from 'vue';
|
|
336
|
+
import ChartForgeVue from './components/ChartForge.vue';
|
|
337
|
+
|
|
338
|
+
const config = ref({
|
|
339
|
+
type: 'pie' as const,
|
|
340
|
+
theme: 'dark',
|
|
341
|
+
data: {
|
|
342
|
+
labels: ['Desktop', 'Mobile', 'Tablet'],
|
|
343
|
+
series: [{ data: [450, 320, 180] }],
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
function refresh() {
|
|
348
|
+
config.value = {
|
|
349
|
+
...config.value,
|
|
350
|
+
data: {
|
|
351
|
+
...config.value.data,
|
|
352
|
+
series: [{ data: [Math.random() * 500, Math.random() * 400, Math.random() * 200] }],
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
</script>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
#### Vue Composable
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
// composables/useChart.ts
|
|
363
|
+
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
|
364
|
+
import { ChartForge } from 'chartforge';
|
|
365
|
+
import type { ChartConfig } from 'chartforge';
|
|
366
|
+
|
|
367
|
+
export function useChart(config: ChartConfig) {
|
|
368
|
+
const containerRef = ref<HTMLDivElement | null>(null);
|
|
369
|
+
let instance: ChartForge | null = null;
|
|
370
|
+
|
|
371
|
+
onMounted(() => {
|
|
372
|
+
if (!containerRef.value) return;
|
|
373
|
+
instance = new ChartForge(containerRef.value, config);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
onBeforeUnmount(() => { instance?.destroy(); });
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
containerRef,
|
|
380
|
+
updateData: (data: Partial<ChartConfig['data']>) => instance?.updateData(data),
|
|
381
|
+
setTheme: (name: string) => instance?.setTheme(name),
|
|
382
|
+
instance: () => instance,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### Angular
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
// chart.component.ts
|
|
393
|
+
import { Component, Input, ElementRef, OnInit, OnDestroy, OnChanges } from '@angular/core';
|
|
394
|
+
import { ChartForge } from 'chartforge';
|
|
395
|
+
import { TooltipPlugin, LegendPlugin } from 'chartforge/plugins';
|
|
396
|
+
import type { ChartConfig } from 'chartforge';
|
|
397
|
+
|
|
398
|
+
@Component({
|
|
399
|
+
selector: 'app-chart',
|
|
400
|
+
template: `<div [style.height.px]="height" style="width:100%"></div>`,
|
|
401
|
+
})
|
|
402
|
+
export class ChartComponent implements OnInit, OnDestroy, OnChanges {
|
|
403
|
+
@Input() config!: ChartConfig;
|
|
404
|
+
@Input() height = 400;
|
|
405
|
+
|
|
406
|
+
private chart: ChartForge | null = null;
|
|
407
|
+
|
|
408
|
+
constructor(private el: ElementRef<HTMLElement>) {}
|
|
409
|
+
|
|
410
|
+
ngOnInit(): void {
|
|
411
|
+
const container = this.el.nativeElement.querySelector('div')!;
|
|
412
|
+
this.chart = new ChartForge(container, this.config);
|
|
413
|
+
this.chart.use('tooltip', TooltipPlugin).use('legend', LegendPlugin);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
ngOnChanges(): void {
|
|
417
|
+
this.chart?.updateData(this.config.data);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
ngOnDestroy(): void {
|
|
421
|
+
this.chart?.destroy();
|
|
422
|
+
this.chart = null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
### Laravel (Blade + Vite)
|
|
430
|
+
|
|
431
|
+
#### 1. Install via npm (Laravel project root)
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
npm install chartforge
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
#### 2. Add to `resources/js/app.js`
|
|
438
|
+
|
|
439
|
+
```js
|
|
440
|
+
// resources/js/app.js
|
|
441
|
+
import { ChartForge } from 'chartforge';
|
|
442
|
+
import { TooltipPlugin } from 'chartforge/plugins';
|
|
443
|
+
|
|
444
|
+
window.ChartForge = ChartForge;
|
|
445
|
+
window.TooltipPlugin = TooltipPlugin;
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### 3. Blade template
|
|
449
|
+
|
|
450
|
+
```blade
|
|
451
|
+
{{-- resources/views/dashboard.blade.php --}}
|
|
452
|
+
@extends('layouts.app')
|
|
453
|
+
|
|
454
|
+
@section('content')
|
|
455
|
+
<div id="revenue-chart" style="height: 400px; background:#1a1a2e; border-radius:12px;"></div>
|
|
456
|
+
|
|
457
|
+
@push('scripts')
|
|
458
|
+
<script>
|
|
459
|
+
const chart = new window.ChartForge('#revenue-chart', {
|
|
460
|
+
type: 'column',
|
|
461
|
+
theme: 'dark',
|
|
462
|
+
data: {
|
|
463
|
+
labels: {!! json_encode($labels) !!},
|
|
464
|
+
series: [{
|
|
465
|
+
name: 'Revenue',
|
|
466
|
+
data: {!! json_encode($revenues) !!},
|
|
467
|
+
}],
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
chart.use('tooltip', window.TooltipPlugin);
|
|
472
|
+
|
|
473
|
+
// Listen to click events
|
|
474
|
+
chart.on('click', ({ index, value }) => {
|
|
475
|
+
console.log('Clicked bar:', index, 'Value:', value);
|
|
476
|
+
});
|
|
477
|
+
</script>
|
|
478
|
+
@endpush
|
|
479
|
+
@endsection
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
#### 4. Or as a self-contained Blade component
|
|
483
|
+
|
|
484
|
+
```blade
|
|
485
|
+
{{-- resources/views/components/chart.blade.php --}}
|
|
486
|
+
<div
|
|
487
|
+
wire:ignore
|
|
488
|
+
id="{{ $id }}"
|
|
489
|
+
style="height: {{ $height ?? 400 }}px"
|
|
490
|
+
x-data
|
|
491
|
+
x-init="
|
|
492
|
+
const chart = new window.ChartForge('#{{ $id }}', {
|
|
493
|
+
type: '{{ $type }}',
|
|
494
|
+
theme: '{{ $theme ?? 'dark' }}',
|
|
495
|
+
data: {{ Js::from($data) }},
|
|
496
|
+
});
|
|
497
|
+
chart.use('tooltip', window.TooltipPlugin);
|
|
498
|
+
"
|
|
499
|
+
></div>
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
### Node.js / SSR
|
|
505
|
+
|
|
506
|
+
ChartForge requires a DOM. In Node.js environments, use a virtual DOM library:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
npm install jsdom
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
```js
|
|
513
|
+
// generate-chart.mjs
|
|
514
|
+
import { JSDOM } from 'jsdom';
|
|
515
|
+
|
|
516
|
+
// Shim browser globals
|
|
517
|
+
const dom = new JSDOM('<!DOCTYPE html><body></body>');
|
|
518
|
+
global.window = dom.window;
|
|
519
|
+
global.document = dom.window.document;
|
|
520
|
+
global.SVGElement = dom.window.SVGElement;
|
|
521
|
+
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
|
|
522
|
+
global.cancelAnimationFrame = clearTimeout;
|
|
523
|
+
|
|
524
|
+
const { ChartForge } = await import('chartforge');
|
|
525
|
+
|
|
526
|
+
const container = document.createElement('div');
|
|
527
|
+
document.body.appendChild(container);
|
|
528
|
+
|
|
529
|
+
const chart = new ChartForge(container, {
|
|
530
|
+
type: 'line',
|
|
531
|
+
theme: 'dark',
|
|
532
|
+
data: {
|
|
533
|
+
labels: ['Jan', 'Feb', 'Mar'],
|
|
534
|
+
series: [{ name: 'Sales', data: [100, 150, 130] }],
|
|
535
|
+
},
|
|
536
|
+
animation: { enabled: false }, // disable animation for SSR
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await chart.render();
|
|
540
|
+
|
|
541
|
+
// Export SVG string
|
|
542
|
+
const svgStr = container.querySelector('svg').outerHTML;
|
|
543
|
+
|
|
544
|
+
// Save to file
|
|
545
|
+
import { writeFileSync } from 'fs';
|
|
546
|
+
writeFileSync('chart.svg', svgStr);
|
|
547
|
+
|
|
548
|
+
console.log('Chart saved to chart.svg');
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Chart Types
|
|
554
|
+
|
|
555
|
+
| Type | Description | Required `data` shape |
|
|
556
|
+
| --------------- | --------------------------- | ---------------------------------------------- |
|
|
557
|
+
| `column` | Vertical bars | `series[0].data: number[]` |
|
|
558
|
+
| `bar` / `row` | Horizontal bars | `series[0].data: number[]` |
|
|
559
|
+
| `line` | Line chart, multiple series | `series[].data: number[]` |
|
|
560
|
+
| `pie` | Pie chart | `series[0].data: number[]` |
|
|
561
|
+
| `donut` | Donut chart | `series[0].data: number[]` |
|
|
562
|
+
| `scatter` | Scatter/bubble plot | `series[].data: { x, y, r? }[]` |
|
|
563
|
+
| `stackedColumn` | Stacked vertical bars | `series[].data: number[]` |
|
|
564
|
+
| `stackedBar` | Stacked horizontal bars | `series[].data: number[]` |
|
|
565
|
+
| `funnel` | Funnel/conversion chart | `series[0].data: number[]` |
|
|
566
|
+
| `heatmap` | 2D heatmap grid | `series[0].data: number[][]` |
|
|
567
|
+
| `candlestick` | OHLC/candlestick chart | `series[0].data: { open, high, low, close }[]` |
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## Configuration Reference
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
const chart = new ChartForge('#container', {
|
|
575
|
+
// Required
|
|
576
|
+
type: 'column', // Chart type
|
|
577
|
+
data: { labels: [...], series: [...] },
|
|
578
|
+
|
|
579
|
+
// Layout
|
|
580
|
+
width: 'auto', // number | 'auto' (follows container)
|
|
581
|
+
height: 400, // number (pixels)
|
|
582
|
+
responsive: true, // Auto-resize on container resize
|
|
583
|
+
padding: {
|
|
584
|
+
top: 40, right: 40, bottom: 60, left: 60,
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
// Appearance
|
|
588
|
+
theme: 'dark', // 'light' | 'dark' | 'neon' | your custom theme name
|
|
589
|
+
|
|
590
|
+
// Animation
|
|
591
|
+
animation: {
|
|
592
|
+
enabled: true,
|
|
593
|
+
duration: 750, // ms
|
|
594
|
+
easing: 'easeOutQuad',
|
|
595
|
+
// All easings: 'linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
|
|
596
|
+
// 'easeInCubic', 'easeOutCubic', 'easeInOutCubic',
|
|
597
|
+
// 'easeInElastic', 'easeOutElastic', 'easeInBounce', 'easeOutBounce'
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
// Virtual rendering (for very large datasets)
|
|
601
|
+
virtual: {
|
|
602
|
+
enabled: false,
|
|
603
|
+
threshold: 10_000, // auto-enable when data points exceed this
|
|
604
|
+
},
|
|
605
|
+
|
|
606
|
+
// Middleware (runs before every render)
|
|
607
|
+
middleware: [],
|
|
608
|
+
});
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## Themes
|
|
614
|
+
|
|
615
|
+
### Built-in themes
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
import { ChartForge } from 'chartforge';
|
|
619
|
+
|
|
620
|
+
// 'light' | 'dark' | 'neon'
|
|
621
|
+
const chart = new ChartForge('#c', { type: 'line', theme: 'neon', data: { ... } });
|
|
622
|
+
|
|
623
|
+
// Switch at runtime
|
|
624
|
+
chart.setTheme('light');
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Custom theme
|
|
628
|
+
|
|
629
|
+
```ts
|
|
630
|
+
import { ChartForge } from 'chartforge';
|
|
631
|
+
import type { Theme } from 'chartforge';
|
|
632
|
+
|
|
633
|
+
const brandTheme: Theme = {
|
|
634
|
+
background: '#0f1923',
|
|
635
|
+
foreground: '#ffffff',
|
|
636
|
+
grid: '#1e2d3d',
|
|
637
|
+
text: '#c8d8e8',
|
|
638
|
+
textSecondary: '#5a7a9a',
|
|
639
|
+
colors: ['#00d4ff', '#ff6b6b', '#51cf66', '#ffd43b', '#cc5de8'],
|
|
640
|
+
tooltip: {
|
|
641
|
+
background: '#0f1923',
|
|
642
|
+
text: '#c8d8e8',
|
|
643
|
+
border: '#1e2d3d',
|
|
644
|
+
shadow: 'rgba(0, 212, 255, 0.15)',
|
|
645
|
+
},
|
|
646
|
+
legend: {
|
|
647
|
+
text: '#c8d8e8',
|
|
648
|
+
hover: '#ffffff',
|
|
649
|
+
},
|
|
650
|
+
axis: {
|
|
651
|
+
line: '#1e2d3d',
|
|
652
|
+
text: '#5a7a9a',
|
|
653
|
+
grid: '#121e29',
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Register globally (available to all new ChartForge instances)
|
|
658
|
+
ChartForge.registerTheme('brand', brandTheme);
|
|
659
|
+
|
|
660
|
+
const chart = new ChartForge('#c', { type: 'bar', theme: 'brand', data: { ... } });
|
|
661
|
+
|
|
662
|
+
// Or register per-instance
|
|
663
|
+
chart.themeManager.register('brand', brandTheme);
|
|
664
|
+
chart.setTheme('brand');
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## Plugins
|
|
670
|
+
|
|
671
|
+
### Usage pattern
|
|
672
|
+
|
|
673
|
+
```ts
|
|
674
|
+
// Method 1: Fluent chain
|
|
675
|
+
chart
|
|
676
|
+
.use('tooltip', TooltipPlugin, { shadow: true })
|
|
677
|
+
.use('legend', LegendPlugin, { position: 'bottom' })
|
|
678
|
+
.use('axis', AxisPlugin, { y: { label: 'Revenue ($)' } })
|
|
679
|
+
.use('grid', GridPlugin)
|
|
680
|
+
.use('crosshair', CrosshairPlugin);
|
|
681
|
+
|
|
682
|
+
// Method 2: pluginManager
|
|
683
|
+
chart.pluginManager.register('tooltip', TooltipPlugin, { fontSize: 14 });
|
|
684
|
+
|
|
685
|
+
// Get plugin instance later
|
|
686
|
+
const tooltip = chart.getPlugin<TooltipPlugin>('tooltip');
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
### Tooltip Plugin
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
import { TooltipPlugin } from 'chartforge/plugins';
|
|
695
|
+
|
|
696
|
+
chart.use('tooltip', TooltipPlugin, {
|
|
697
|
+
enabled: true,
|
|
698
|
+
backgroundColor: '#1a1a2e',
|
|
699
|
+
textColor: '#e0e0ff',
|
|
700
|
+
borderColor: '#3a3a6e',
|
|
701
|
+
borderRadius: 8,
|
|
702
|
+
padding: 12,
|
|
703
|
+
fontSize: 13,
|
|
704
|
+
shadow: true,
|
|
705
|
+
followCursor: true,
|
|
706
|
+
offset: { x: 14, y: 14 },
|
|
707
|
+
|
|
708
|
+
// Custom formatter (receives the raw hover event data)
|
|
709
|
+
formatter: (data) => {
|
|
710
|
+
if (data.type === 'column') {
|
|
711
|
+
return `<strong>${data.value}</strong> units sold`;
|
|
712
|
+
}
|
|
713
|
+
return String(data.value);
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
### Legend Plugin
|
|
721
|
+
|
|
722
|
+
```ts
|
|
723
|
+
import { LegendPlugin } from 'chartforge/plugins';
|
|
724
|
+
|
|
725
|
+
chart.use('legend', LegendPlugin, {
|
|
726
|
+
enabled: true,
|
|
727
|
+
position: 'bottom', // 'top' | 'bottom' | 'left' | 'right'
|
|
728
|
+
align: 'center', // 'start' | 'center' | 'end'
|
|
729
|
+
layout: 'horizontal', // 'horizontal' | 'vertical'
|
|
730
|
+
fontSize: 12,
|
|
731
|
+
itemSpacing: 12,
|
|
732
|
+
markerSize: 12,
|
|
733
|
+
markerType: 'square', // 'square' | 'circle' | 'line'
|
|
734
|
+
clickable: true, // Toggle series visibility on click
|
|
735
|
+
});
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
### Axis Plugin
|
|
741
|
+
|
|
742
|
+
```ts
|
|
743
|
+
import { AxisPlugin } from 'chartforge/plugins';
|
|
744
|
+
|
|
745
|
+
chart.use('axis', AxisPlugin, {
|
|
746
|
+
x: {
|
|
747
|
+
enabled: true,
|
|
748
|
+
label: 'Month',
|
|
749
|
+
fontSize: 11,
|
|
750
|
+
tickLength: 5,
|
|
751
|
+
},
|
|
752
|
+
y: {
|
|
753
|
+
enabled: true,
|
|
754
|
+
label: 'Revenue ($)',
|
|
755
|
+
fontSize: 11,
|
|
756
|
+
tickLength: 5,
|
|
757
|
+
ticks: 5, // Number of Y-axis tick marks
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
---
|
|
763
|
+
|
|
764
|
+
### Grid Plugin
|
|
765
|
+
|
|
766
|
+
```ts
|
|
767
|
+
import { GridPlugin } from 'chartforge/plugins';
|
|
768
|
+
|
|
769
|
+
chart.use('grid', GridPlugin, {
|
|
770
|
+
enabled: true,
|
|
771
|
+
x: { enabled: true, color: '#2a2a3a', dashArray: '3,3', strokeWidth: 1 },
|
|
772
|
+
y: { enabled: true, color: '#2a2a3a', dashArray: '3,3', strokeWidth: 1, ticks: 5 },
|
|
773
|
+
});
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
### Crosshair Plugin
|
|
779
|
+
|
|
780
|
+
Draws intersecting reference lines following the cursor inside the chart area.
|
|
781
|
+
|
|
782
|
+
```ts
|
|
783
|
+
import { CrosshairPlugin } from 'chartforge/plugins';
|
|
784
|
+
|
|
785
|
+
chart.use('crosshair', CrosshairPlugin, {
|
|
786
|
+
enabled: true,
|
|
787
|
+
x: { enabled: true, color: '#888', dashArray: '4,4', width: 1 },
|
|
788
|
+
y: { enabled: true, color: '#888', dashArray: '4,4', width: 1 },
|
|
789
|
+
});
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
### Data Labels Plugin
|
|
795
|
+
|
|
796
|
+
Show values directly on top of chart elements.
|
|
797
|
+
|
|
798
|
+
```ts
|
|
799
|
+
import { DataLabelsPlugin } from 'chartforge/plugins';
|
|
800
|
+
|
|
801
|
+
chart.use('dataLabels', DataLabelsPlugin, {
|
|
802
|
+
enabled: true,
|
|
803
|
+
fontSize: 11,
|
|
804
|
+
color: '#ffffff',
|
|
805
|
+
anchor: 'top', // 'top' | 'center' | 'bottom'
|
|
806
|
+
offset: 5, // px offset from element
|
|
807
|
+
rotation: -45, // label rotation in degrees
|
|
808
|
+
formatter: (value) => `$${value.toLocaleString()}`,
|
|
809
|
+
});
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
---
|
|
813
|
+
|
|
814
|
+
### Export Plugin
|
|
815
|
+
|
|
816
|
+
Adds SVG / PNG / CSV download buttons above the chart.
|
|
817
|
+
|
|
818
|
+
```ts
|
|
819
|
+
import { ExportPlugin } from 'chartforge/plugins';
|
|
820
|
+
|
|
821
|
+
chart.use('export', ExportPlugin, {
|
|
822
|
+
filename: 'revenue-q1', // download filename (no extension)
|
|
823
|
+
svgButton: true,
|
|
824
|
+
pngButton: true,
|
|
825
|
+
csvButton: true,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// Or trigger exports programmatically
|
|
829
|
+
const exporter = chart.getPlugin<ExportPlugin>('export');
|
|
830
|
+
exporter?.exportSVG();
|
|
831
|
+
await exporter?.exportPNG(3); // 3x scale for retina
|
|
832
|
+
exporter?.exportCSV();
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
### Zoom & Pan Plugin
|
|
838
|
+
|
|
839
|
+
Mouse wheel to zoom, drag to pan, double-click to reset.
|
|
840
|
+
|
|
841
|
+
```ts
|
|
842
|
+
import { ZoomPlugin } from 'chartforge/plugins';
|
|
843
|
+
|
|
844
|
+
chart.use('zoom', ZoomPlugin, {
|
|
845
|
+
enabled: true,
|
|
846
|
+
type: 'xy', // 'x' | 'y' | 'xy'
|
|
847
|
+
minZoom: 0.5,
|
|
848
|
+
maxZoom: 10,
|
|
849
|
+
resetOnDblClick: true,
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// Reset programmatically
|
|
853
|
+
const zoom = chart.getPlugin<ZoomPlugin>('zoom');
|
|
854
|
+
zoom?.reset();
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
---
|
|
858
|
+
|
|
859
|
+
### Annotation Plugin
|
|
860
|
+
|
|
861
|
+
Add horizontal/vertical reference lines, shaded regions, and text labels to any chart.
|
|
862
|
+
|
|
863
|
+
```ts
|
|
864
|
+
import { AnnotationPlugin } from 'chartforge/plugins';
|
|
865
|
+
|
|
866
|
+
chart.use('annotations', AnnotationPlugin, {
|
|
867
|
+
markLines: [
|
|
868
|
+
{ type: 'horizontal', value: 100, label: 'Target', color: '#10b981', dashArray: '5,3' },
|
|
869
|
+
{ type: 'horizontal', value: 50, label: 'Baseline', color: '#ef4444' },
|
|
870
|
+
{ type: 'vertical', value: 2, label: 'Campaign', color: '#f59e0b' },
|
|
871
|
+
],
|
|
872
|
+
markAreas: [
|
|
873
|
+
{ xStart: 1, xEnd: 3, color: '#3b82f6', opacity: 0.1, label: 'Peak period' },
|
|
874
|
+
{ yStart: 80, yEnd: 120, color: '#10b981', opacity: 0.08 },
|
|
875
|
+
],
|
|
876
|
+
texts: [
|
|
877
|
+
{ x: 2, y: 150, text: '🚀 New product launch', color: '#fff', background: '#3b82f6' },
|
|
878
|
+
],
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// Add annotations dynamically
|
|
882
|
+
const ann = chart.getPlugin<AnnotationPlugin>('annotations');
|
|
883
|
+
ann?.addMarkLine({ type: 'horizontal', value: 200, label: 'Record', color: '#ff6b6b' });
|
|
884
|
+
ann?.addText({ x: 4, y: 180, text: 'All-time high', color: '#ff6b6b' });
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
## Adapters (Real-Time Data)
|
|
890
|
+
|
|
891
|
+
ChartForge ships with two built-in real-time adapters.
|
|
892
|
+
|
|
893
|
+
### WebSocket Adapter
|
|
894
|
+
|
|
895
|
+
```ts
|
|
896
|
+
// Connect to a WebSocket feed
|
|
897
|
+
chart.realTime.connect('websocket', {
|
|
898
|
+
url: 'wss://api.example.com/live-data',
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Your server should push: { series: [{ data: [...] }], labels: [...] }
|
|
902
|
+
|
|
903
|
+
// Disconnect when done
|
|
904
|
+
chart.realTime.disconnect('websocket');
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
### Polling Adapter
|
|
908
|
+
|
|
909
|
+
```ts
|
|
910
|
+
chart.realTime.connect('polling', {
|
|
911
|
+
url: '/api/live-metrics',
|
|
912
|
+
interval: 3000, // ms — defaults to 5000
|
|
913
|
+
});
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
### Custom Adapter
|
|
917
|
+
|
|
918
|
+
```ts
|
|
919
|
+
import type { IAdapter, EventHandler, ChartData } from 'chartforge';
|
|
920
|
+
|
|
921
|
+
class SSEAdapter implements IAdapter {
|
|
922
|
+
private _es: EventSource | null = null;
|
|
923
|
+
private _listeners = new Map<string, EventHandler[]>();
|
|
924
|
+
|
|
925
|
+
constructor(private _url: string) {}
|
|
926
|
+
|
|
927
|
+
on(event: string, handler: EventHandler): void {
|
|
928
|
+
if (!this._listeners.has(event)) this._listeners.set(event, []);
|
|
929
|
+
this._listeners.get(event)!.push(handler);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
connect(): void {
|
|
933
|
+
this._es = new EventSource(this._url);
|
|
934
|
+
this._es.addEventListener('message', (e) => {
|
|
935
|
+
const data = JSON.parse(e.data) as ChartData;
|
|
936
|
+
this._listeners.get('data')?.forEach(h => h(data));
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
disconnect(): void {
|
|
941
|
+
this._es?.close();
|
|
942
|
+
this._es = null;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Register globally
|
|
947
|
+
chart.realTime.registerAdapter('sse', SSEAdapter);
|
|
948
|
+
chart.realTime.connect('sse', 'https://api.example.com/stream');
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
---
|
|
952
|
+
|
|
953
|
+
## Events & API
|
|
954
|
+
|
|
955
|
+
### Chart Events
|
|
956
|
+
|
|
957
|
+
```ts
|
|
958
|
+
// Hover over a data element
|
|
959
|
+
chart.on('hover', ({ type, index, value, seriesIndex, point, candle, row, col }) => {
|
|
960
|
+
console.log('Hovered:', type, value);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// Click on a data element
|
|
964
|
+
chart.on('click', ({ type, index, value }) => {
|
|
965
|
+
console.log('Clicked:', type, value);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// Before/after render
|
|
969
|
+
chart.on('beforeRender', (ctx) => { /* ctx: { data, theme, svg, mainGroup } */ });
|
|
970
|
+
chart.on('afterRender', (ctx) => { /* DOM is updated */ });
|
|
971
|
+
|
|
972
|
+
// Unsubscribe
|
|
973
|
+
const unsub = chart.on('click', handler);
|
|
974
|
+
unsub(); // removes the listener
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
### Instance API
|
|
978
|
+
|
|
979
|
+
```ts
|
|
980
|
+
// Update data (re-renders)
|
|
981
|
+
chart.updateData({
|
|
982
|
+
labels: ['A', 'B', 'C'],
|
|
983
|
+
series: [{ name: 'Sales', data: [10, 20, 15] }],
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
// Partial config update
|
|
987
|
+
chart.updateConfig({ type: 'bar', theme: 'neon' });
|
|
988
|
+
|
|
989
|
+
// Switch theme
|
|
990
|
+
chart.setTheme('dark');
|
|
991
|
+
|
|
992
|
+
// Get plugin instance
|
|
993
|
+
const tooltip = chart.getPlugin<TooltipPlugin>('tooltip');
|
|
994
|
+
|
|
995
|
+
// Trigger manual resize
|
|
996
|
+
chart.resize();
|
|
997
|
+
|
|
998
|
+
// Viewport (virtual rendering)
|
|
999
|
+
chart.setViewport(0, 100); // show data points [0, 100)
|
|
1000
|
+
|
|
1001
|
+
// Destroy (cleans up DOM, events, RAF, WebSocket)
|
|
1002
|
+
chart.destroy();
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
### Static API
|
|
1006
|
+
|
|
1007
|
+
```ts
|
|
1008
|
+
// Factory method (same as new ChartForge)
|
|
1009
|
+
const chart = ChartForge.create('#container', { type: 'pie', data: { ... } });
|
|
1010
|
+
|
|
1011
|
+
// Register a theme available to all instances
|
|
1012
|
+
ChartForge.registerTheme('brand', myTheme);
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
## Advanced Usage
|
|
1018
|
+
|
|
1019
|
+
### Middleware
|
|
1020
|
+
|
|
1021
|
+
Middleware runs before every render. Use it for logging, auth, data transformation, etc.
|
|
1022
|
+
|
|
1023
|
+
```ts
|
|
1024
|
+
const chart = new ChartForge('#c', {
|
|
1025
|
+
type: 'line',
|
|
1026
|
+
data: { ... },
|
|
1027
|
+
middleware: [
|
|
1028
|
+
async (ctx, next) => {
|
|
1029
|
+
console.time('render');
|
|
1030
|
+
await next(); // call next to continue the pipeline
|
|
1031
|
+
console.timeEnd('render');
|
|
1032
|
+
},
|
|
1033
|
+
async (ctx, next) => {
|
|
1034
|
+
// Transform data before rendering
|
|
1035
|
+
ctx.data.series = ctx.data.series.map(s => ({
|
|
1036
|
+
...s,
|
|
1037
|
+
data: (s.data as number[]).map(v => v * 1.1), // +10% adjustment
|
|
1038
|
+
}));
|
|
1039
|
+
await next();
|
|
1040
|
+
},
|
|
1041
|
+
],
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
// Add middleware after construction
|
|
1045
|
+
chart.middleware.use(async (ctx, next) => {
|
|
1046
|
+
ctx.theme = { ...ctx.theme, background: '#ff0000' }; // override theme for this render
|
|
1047
|
+
await next();
|
|
1048
|
+
});
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
### Data Pipeline (Transformers)
|
|
1052
|
+
|
|
1053
|
+
Named transformers that process data before it reaches the renderer.
|
|
1054
|
+
|
|
1055
|
+
```ts
|
|
1056
|
+
// Add a normalizer
|
|
1057
|
+
chart.dataPipeline.addTransformer('normalize', (data) => ({
|
|
1058
|
+
...data,
|
|
1059
|
+
series: data.series.map(s => {
|
|
1060
|
+
const max = Math.max(...s.data as number[]);
|
|
1061
|
+
return { ...s, data: (s.data as number[]).map(v => v / max) };
|
|
1062
|
+
}),
|
|
1063
|
+
}));
|
|
1064
|
+
|
|
1065
|
+
// Add a sorter
|
|
1066
|
+
chart.dataPipeline.addTransformer('sort', (data) => ({
|
|
1067
|
+
...data,
|
|
1068
|
+
series: data.series.map(s => ({
|
|
1069
|
+
...s,
|
|
1070
|
+
data: [...s.data as number[]].sort((a, b) => b - a),
|
|
1071
|
+
})),
|
|
1072
|
+
}));
|
|
1073
|
+
|
|
1074
|
+
// Remove a transformer
|
|
1075
|
+
chart.dataPipeline.removeTransformer('sort');
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
### Virtual Rendering
|
|
1079
|
+
|
|
1080
|
+
For datasets with tens of thousands of points, enable virtual rendering to only draw visible points:
|
|
1081
|
+
|
|
1082
|
+
```ts
|
|
1083
|
+
const chart = new ChartForge('#c', {
|
|
1084
|
+
type: 'line',
|
|
1085
|
+
data: { series: [{ data: Array.from({ length: 100_000 }, () => Math.random()) }] },
|
|
1086
|
+
virtual: {
|
|
1087
|
+
enabled: true,
|
|
1088
|
+
threshold: 5_000, // Auto-enable once series total exceeds this
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
// Pan the viewport
|
|
1093
|
+
chart.setViewport(0, 500); // first 500 points
|
|
1094
|
+
chart.setViewport(500, 1000); // next 500 points
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
---
|
|
1098
|
+
|
|
1099
|
+
## Extending ChartForge
|
|
1100
|
+
|
|
1101
|
+
### Custom Plugin
|
|
1102
|
+
|
|
1103
|
+
```ts
|
|
1104
|
+
import { BasePlugin } from 'chartforge/plugins';
|
|
1105
|
+
import type { Theme } from 'chartforge';
|
|
1106
|
+
|
|
1107
|
+
interface WatermarkConfig {
|
|
1108
|
+
text: string;
|
|
1109
|
+
opacity?: number;
|
|
1110
|
+
color?: string;
|
|
1111
|
+
fontSize?: number;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
interface ChartLike {
|
|
1115
|
+
theme: Theme;
|
|
1116
|
+
svg: SVGSVGElement;
|
|
1117
|
+
on: (event: string, h: () => void) => void;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
class WatermarkPlugin extends BasePlugin {
|
|
1121
|
+
private readonly _cfg: Required<WatermarkConfig>;
|
|
1122
|
+
|
|
1123
|
+
constructor(chart: unknown, cfg: WatermarkConfig) {
|
|
1124
|
+
super(chart, cfg);
|
|
1125
|
+
this._cfg = {
|
|
1126
|
+
opacity: 0.1,
|
|
1127
|
+
color: (chart as ChartLike).theme.text,
|
|
1128
|
+
fontSize: 48,
|
|
1129
|
+
...cfg,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
init(): void {
|
|
1134
|
+
const c = this._chart as ChartLike;
|
|
1135
|
+
const svg = c.svg;
|
|
1136
|
+
|
|
1137
|
+
const draw = () => {
|
|
1138
|
+
const existing = svg.querySelector('.cf-watermark');
|
|
1139
|
+
if (existing) svg.removeChild(existing);
|
|
1140
|
+
|
|
1141
|
+
const vb = svg.getAttribute('viewBox')!.split(' ').map(Number);
|
|
1142
|
+
const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
1143
|
+
Object.assign(txt, {});
|
|
1144
|
+
txt.setAttribute('class', 'cf-watermark');
|
|
1145
|
+
txt.setAttribute('x', String(vb[2] / 2));
|
|
1146
|
+
txt.setAttribute('y', String(vb[3] / 2));
|
|
1147
|
+
txt.setAttribute('text-anchor', 'middle');
|
|
1148
|
+
txt.setAttribute('dominant-baseline', 'middle');
|
|
1149
|
+
txt.setAttribute('fill', this._cfg.color);
|
|
1150
|
+
txt.setAttribute('font-size', String(this._cfg.fontSize));
|
|
1151
|
+
txt.setAttribute('opacity', String(this._cfg.opacity));
|
|
1152
|
+
txt.setAttribute('pointer-events', 'none');
|
|
1153
|
+
txt.setAttribute('font-weight', 'bold');
|
|
1154
|
+
txt.setAttribute('transform', `rotate(-30,${vb[2]/2},${vb[3]/2})`);
|
|
1155
|
+
txt.textContent = this._cfg.text;
|
|
1156
|
+
svg.appendChild(txt);
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
draw();
|
|
1160
|
+
c.on('afterRender', draw);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Use it
|
|
1165
|
+
chart.use('watermark', WatermarkPlugin, { text: 'CONFIDENTIAL' });
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
---
|
|
1169
|
+
|
|
1170
|
+
### Custom Renderer
|
|
1171
|
+
|
|
1172
|
+
```ts
|
|
1173
|
+
import { BaseRenderer, RENDERERS } from 'chartforge';
|
|
1174
|
+
import type { ChartLike } from 'chartforge';
|
|
1175
|
+
|
|
1176
|
+
class RadarRenderer extends BaseRenderer {
|
|
1177
|
+
render(): void {
|
|
1178
|
+
const d = this.dims();
|
|
1179
|
+
const cx = d.totalWidth / 2;
|
|
1180
|
+
const cy = d.totalHeight / 2;
|
|
1181
|
+
const r = Math.min(d.width, d.height) / 2 - 20;
|
|
1182
|
+
const series = this.data.series[0].data as number[];
|
|
1183
|
+
const n = series.length;
|
|
1184
|
+
const maxVal = Math.max(...series);
|
|
1185
|
+
const step = (Math.PI * 2) / n;
|
|
1186
|
+
|
|
1187
|
+
const group = this.g('chartforge-radar');
|
|
1188
|
+
this.group.appendChild(group);
|
|
1189
|
+
|
|
1190
|
+
// Draw spokes
|
|
1191
|
+
for (let i = 0; i < n; i++) {
|
|
1192
|
+
const angle = step * i - Math.PI / 2;
|
|
1193
|
+
const x2 = cx + r * Math.cos(angle);
|
|
1194
|
+
const y2 = cy + r * Math.sin(angle);
|
|
1195
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
1196
|
+
line.setAttribute('x1', String(cx)); line.setAttribute('y1', String(cy));
|
|
1197
|
+
line.setAttribute('x2', String(x2)); line.setAttribute('y2', String(y2));
|
|
1198
|
+
line.setAttribute('stroke', this.theme.grid); line.setAttribute('stroke-width', '1');
|
|
1199
|
+
group.appendChild(line);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Draw data polygon
|
|
1203
|
+
const pts = series.map((v, i) => {
|
|
1204
|
+
const angle = step * i - Math.PI / 2;
|
|
1205
|
+
const rv = (v / maxVal) * r;
|
|
1206
|
+
return `${cx + rv * Math.cos(angle)},${cy + rv * Math.sin(angle)}`;
|
|
1207
|
+
}).join(' ');
|
|
1208
|
+
|
|
1209
|
+
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
|
1210
|
+
poly.setAttribute('points', pts);
|
|
1211
|
+
poly.setAttribute('fill', this.color(0) + '55');
|
|
1212
|
+
poly.setAttribute('stroke', this.color(0));
|
|
1213
|
+
poly.setAttribute('stroke-width', '2');
|
|
1214
|
+
group.appendChild(poly);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Register the renderer globally
|
|
1219
|
+
RENDERERS['radar' as never] = RadarRenderer as never;
|
|
1220
|
+
|
|
1221
|
+
// Now use it
|
|
1222
|
+
const chart = new ChartForge('#c', {
|
|
1223
|
+
type: 'radar' as never,
|
|
1224
|
+
data: { series: [{ data: [80, 60, 90, 75, 85, 70] }] },
|
|
1225
|
+
});
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
---
|
|
1229
|
+
|
|
1230
|
+
### Custom Theme
|
|
1231
|
+
|
|
1232
|
+
See the [Themes](#themes) section above for a full custom theme example.
|
|
1233
|
+
|
|
1234
|
+
### Custom Adapter
|
|
1235
|
+
|
|
1236
|
+
See the [Adapters](#adapters-real-time-data) section above for a Server-Sent Events adapter example.
|
|
1237
|
+
|
|
1238
|
+
---
|
|
1239
|
+
|
|
1240
|
+
## Architecture
|
|
1241
|
+
|
|
1242
|
+
```
|
|
1243
|
+
src/
|
|
1244
|
+
├── ChartForge.ts # Main orchestrator class
|
|
1245
|
+
├── types.ts # All shared interfaces — single source of truth
|
|
1246
|
+
├── index.ts # Public barrel — tree-shakeable
|
|
1247
|
+
│
|
|
1248
|
+
├── core/ # Sub-systems (individually importable)
|
|
1249
|
+
│ ├── EventBus.ts # Priority-based pub/sub
|
|
1250
|
+
│ ├── MiddlewarePipeline.ts # Async middleware chain
|
|
1251
|
+
│ ├── DataPipeline.ts # Named data transformers
|
|
1252
|
+
│ ├── AnimationEngine.ts # RAF-based tweening with 11 easings
|
|
1253
|
+
│ ├── ThemeManager.ts # Theme registry + apply
|
|
1254
|
+
│ ├── PluginManager.ts # Plugin lifecycle management
|
|
1255
|
+
│ └── VirtualRenderer.ts # Viewport slicing for large datasets
|
|
1256
|
+
│
|
|
1257
|
+
├── renderers/ # One file per chart type (tree-shakeable)
|
|
1258
|
+
│ ├── BaseRenderer.ts # Abstract base with shared geometry
|
|
1259
|
+
│ ├── ColumnRenderer.ts
|
|
1260
|
+
│ ├── BarRenderer.ts
|
|
1261
|
+
│ ├── LineRenderer.ts
|
|
1262
|
+
│ ├── PieRenderer.ts
|
|
1263
|
+
│ ├── DonutRenderer.ts
|
|
1264
|
+
│ ├── ScatterRenderer.ts
|
|
1265
|
+
│ ├── StackedColumnRenderer.ts
|
|
1266
|
+
│ ├── StackedBarRenderer.ts
|
|
1267
|
+
│ ├── FunnelRenderer.ts
|
|
1268
|
+
│ ├── HeatmapRenderer.ts
|
|
1269
|
+
│ ├── CandlestickRenderer.ts
|
|
1270
|
+
│ └── index.ts # RENDERERS registry
|
|
1271
|
+
│
|
|
1272
|
+
├── plugins/ # One file per plugin (tree-shakeable)
|
|
1273
|
+
│ ├── BasePlugin.ts
|
|
1274
|
+
│ ├── TooltipPlugin.ts # Smart tooltip with per-type formatting
|
|
1275
|
+
│ ├── LegendPlugin.ts # Clickable, snapshotted series toggle
|
|
1276
|
+
│ ├── AxisPlugin.ts # X/Y axes with labels
|
|
1277
|
+
│ ├── GridPlugin.ts # Background grid lines
|
|
1278
|
+
│ ├── CrosshairPlugin.ts # Cursor crosshair lines
|
|
1279
|
+
│ ├── DataLabelsPlugin.ts # Values on elements
|
|
1280
|
+
│ ├── ExportPlugin.ts # SVG/PNG/CSV export
|
|
1281
|
+
│ ├── ZoomPlugin.ts # Wheel zoom + drag pan
|
|
1282
|
+
│ ├── AnnotationPlugin.ts # Mark lines, areas, text
|
|
1283
|
+
│ └── index.ts
|
|
1284
|
+
│
|
|
1285
|
+
├── themes/
|
|
1286
|
+
│ ├── builtins.ts # light | dark | neon
|
|
1287
|
+
│ └── index.ts
|
|
1288
|
+
│
|
|
1289
|
+
├── adapters/ # Real-time data feeds
|
|
1290
|
+
│ ├── WebSocketAdapter.ts
|
|
1291
|
+
│ ├── PollingAdapter.ts
|
|
1292
|
+
│ ├── RealTimeModule.ts
|
|
1293
|
+
│ └── index.ts
|
|
1294
|
+
│
|
|
1295
|
+
└── utils/
|
|
1296
|
+
├── dom.ts # SVG element creation, polar coords
|
|
1297
|
+
├── misc.ts # uid, merge, clamp, debounce, throttle
|
|
1298
|
+
└── index.ts
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
---
|
|
1302
|
+
|
|
1303
|
+
## Build Reference
|
|
1304
|
+
|
|
1305
|
+
```bash
|
|
1306
|
+
# Install dependencies
|
|
1307
|
+
npm install
|
|
1308
|
+
|
|
1309
|
+
# Start dev server with HMR (demo app at localhost:5173)
|
|
1310
|
+
npm run dev
|
|
1311
|
+
|
|
1312
|
+
# Build library — ES + UMD, minified + obfuscated for production
|
|
1313
|
+
NODE_ENV=production npm run build:lib
|
|
1314
|
+
|
|
1315
|
+
# Type-check
|
|
1316
|
+
npm run typecheck
|
|
1317
|
+
|
|
1318
|
+
# Lint
|
|
1319
|
+
npm run lint
|
|
1320
|
+
|
|
1321
|
+
# Preview production build
|
|
1322
|
+
npm run preview
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### Build outputs
|
|
1326
|
+
|
|
1327
|
+
| File | Format | Minified | Use case |
|
|
1328
|
+
| ------------------------- | ------- | --------- | --------------------------------------- |
|
|
1329
|
+
| `dist/chartforge.js` | ESM | prod only | Modern bundlers (Webpack, Vite, Rollup) |
|
|
1330
|
+
| `dist/chartforge.umd.cjs` | UMD/CJS | prod only | `<script>` tag, `require()`, Laravel |
|
|
1331
|
+
| `dist/plugins.js` | ESM | prod only | Tree-shakeable plugin imports |
|
|
1332
|
+
| `dist/themes.js` | ESM | prod only | Tree-shakeable theme imports |
|
|
1333
|
+
| `dist/types/` | `.d.ts` | — | TypeScript consumers |
|
|
1334
|
+
|
|
1335
|
+
---
|
|
1336
|
+
|
|
1337
|
+
## License
|
|
1338
|
+
|
|
1339
|
+
MIT © Anand Pilania
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chartforge",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Production-grade pluggable SVG charting library — zero dependencies",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/chartforge.umd.cjs",
|
|
7
|
+
"module": "./dist/chartforge.js",
|
|
8
|
+
"types": "./dist/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/chartforge.js",
|
|
12
|
+
"require": "./dist/chartforge.umd.cjs",
|
|
13
|
+
"types": "./dist/types/index.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./plugins": {
|
|
16
|
+
"import": "./dist/plugins.js",
|
|
17
|
+
"require": "./dist/plugins.umd.cjs"
|
|
18
|
+
},
|
|
19
|
+
"./themes": {
|
|
20
|
+
"import": "./dist/themes.js",
|
|
21
|
+
"require": "./dist/themes.umd.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"dev": "vite serve demo --config vite.config.ts",
|
|
30
|
+
"build:lib:dev": "",
|
|
31
|
+
"build": "vite build --config vite.config.ts && tsc --emitDeclarationOnly --declarationDir dist/types",
|
|
32
|
+
"build:lib": "vite build --config vite.lib.config.ts",
|
|
33
|
+
"preview": "vite preview demo",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"lint": "eslint src --ext .ts"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"vite": "^5.4.0",
|
|
39
|
+
"vite-plugin-dts": "^4.0.0",
|
|
40
|
+
"@vitejs/plugin-legacy": "^5.0.0",
|
|
41
|
+
"typescript": "^5.5.0",
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"terser": "^5.31.0",
|
|
44
|
+
"eslint": "^9.0.0",
|
|
45
|
+
"globals": "^15.0.0"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"chart",
|
|
49
|
+
"charting",
|
|
50
|
+
"visualization",
|
|
51
|
+
"svg",
|
|
52
|
+
"graphs",
|
|
53
|
+
"data-visualization",
|
|
54
|
+
"pluggable",
|
|
55
|
+
"zero-dependencies",
|
|
56
|
+
"real-time",
|
|
57
|
+
"websocket",
|
|
58
|
+
"animation"
|
|
59
|
+
],
|
|
60
|
+
"author": "Anand Pilania <pilaniaanand@gmail.com>",
|
|
61
|
+
"license": "MIT",
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "https://github.com/anandpilania/chartforge"
|
|
65
|
+
}
|
|
66
|
+
}
|