cordova-plugin-ra-chart 1.0.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,254 @@
1
+ import {
2
+ AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef,
3
+ EventEmitter, Inject, Input, NgZone, OnChanges, OnDestroy, Output, SimpleChanges, ViewChild
4
+ } from '@angular/core';
5
+ import { CommonModule } from '@angular/common';
6
+
7
+ import { RA_CHART_SCRIPT_URL, ensureRaChart } from './ra-chart.loader';
8
+ import type {
9
+ RaAnimation, RaBand, RaChartInstance, RaChartOptions, RaChartType,
10
+ RaFieldMap, RaSelectEvent, RaSeries, RaToggleEvent, RaZoomEvent
11
+ } from './ra-chart.types';
12
+
13
+ /**
14
+ * `<ra-chart>` — Ionic/Angular wrapper around the raChart engine.
15
+ *
16
+ * <ra-chart type="column" category="month"
17
+ * [series]="series" [data]="rows" [zoom]="true"
18
+ * (select)="onSelect($event)"></ra-chart>
19
+ *
20
+ * The engine runs entirely outside Angular's zone, so touch scrubbing and
21
+ * entrance animations never trigger change detection. Events are re-entered
22
+ * into the zone before they reach your handlers.
23
+ */
24
+ @Component({
25
+ selector: 'ra-chart',
26
+ standalone: true,
27
+ imports: [CommonModule],
28
+ templateUrl: './ra-chart.component.html',
29
+ changeDetection: ChangeDetectionStrategy.OnPush,
30
+ styles: [`
31
+ :host { display: block; position: relative; }
32
+ :host .ra-chart--busy { display: none; }
33
+ `]
34
+ })
35
+ export class RaChartComponent implements AfterViewInit, OnChanges, OnDestroy {
36
+ @ViewChild('host', { static: true }) hostRef!: ElementRef<HTMLDivElement>;
37
+
38
+ // ---- data ----
39
+ @Input() type: RaChartType = 'column';
40
+ @Input() data: any[] = [];
41
+ @Input() category = 'category';
42
+ @Input() series: RaSeries[] = [];
43
+ @Input() colors: string[] | null = null;
44
+
45
+ // ---- shape ----
46
+ @Input() stacked = false;
47
+ @Input() fullStacked = false;
48
+ @Input() smooth = false;
49
+ @Input() stepped = false;
50
+ @Input() semi = false;
51
+ @Input() depth = 0;
52
+
53
+ // ---- field mapping ----
54
+ @Input() fields: RaFieldMap | null = null;
55
+ @Input() xField = 'x';
56
+ @Input() sizeField = 'size';
57
+
58
+ // ---- scale ----
59
+ @Input() min = 0;
60
+ @Input() max: number | null = null;
61
+ @Input() bands: RaBand[] | null = null;
62
+ @Input() upColor?: string;
63
+ @Input() downColor?: string;
64
+
65
+ // ---- interaction ----
66
+ @Input() zoom = false;
67
+ @Input() pinchZoom = true;
68
+ @Input() legend = true;
69
+ @Input() haptics = true;
70
+ @Input() sheetTooltip: boolean | 'auto' = 'auto';
71
+ @Input() tooltipHold = 2600;
72
+
73
+ // ---- presentation ----
74
+ @Input() height = 260;
75
+ @Input() animation: RaAnimation = { duration: 800, delay: 70 };
76
+ @Input() loading = false;
77
+ @Input() emptyText = 'No data to display';
78
+ @Input() loadingText = 'Loading…';
79
+
80
+ // ---- events ----
81
+ @Output() select = new EventEmitter<RaSelectEvent>();
82
+ @Output() zoomChange = new EventEmitter<RaZoomEvent>();
83
+ @Output() toggle = new EventEmitter<RaToggleEvent>();
84
+ @Output() ready = new EventEmitter<RaChartInstance>();
85
+
86
+ /** Set when the engine script cannot be loaded, so the template can say so. */
87
+ error: string | null = null;
88
+
89
+ private chart: RaChartInstance | null = null;
90
+ private booted = false;
91
+ private listeners: Array<[string, EventListener]> = [];
92
+
93
+ constructor(
94
+ private zone: NgZone,
95
+ private cdr: ChangeDetectorRef,
96
+ @Inject(RA_CHART_SCRIPT_URL) private scriptUrl: string
97
+ ) { }
98
+
99
+ /** True when the current type has nothing to draw. */
100
+ get isEmpty(): boolean {
101
+ if (this.loading || this.error) return false;
102
+ if (!this.data || this.data.length === 0) return true;
103
+ // sankey carries its own from/to/value rows; every other type needs a series
104
+ if (this.type === 'sankey') return false;
105
+ return this.series.length === 0;
106
+ }
107
+
108
+ ngAfterViewInit(): void {
109
+ this.boot();
110
+ }
111
+
112
+ ngOnChanges(changes: SimpleChanges): void {
113
+ if (!this.chart) return;
114
+
115
+ // data-only change on a stable shape → let the engine morph the values
116
+ const keys = Object.keys(changes);
117
+ if (keys.length === 1 && keys[0] === 'data' && !changes['data'].firstChange) {
118
+ if (this.isEmpty) { this.teardownChart(); this.cdr.markForCheck(); return; }
119
+ this.zone.runOutsideAngular(() => this.chart!.setData(this.data || []));
120
+ return;
121
+ }
122
+ if (keys.length === 1 && keys[0] === 'loading') { this.cdr.markForCheck(); return; }
123
+
124
+ if (this.isEmpty) { this.teardownChart(); this.cdr.markForCheck(); return; }
125
+ this.zone.runOutsideAngular(() => this.chart!.update(this.buildOptions()));
126
+ }
127
+
128
+ ngOnDestroy(): void {
129
+ this.teardownChart();
130
+ }
131
+
132
+ // ---------------------------------------------------------------
133
+ // imperative API — grab the component with @ViewChild and call these
134
+ // ---------------------------------------------------------------
135
+
136
+ /** The live engine instance, or null before first render. */
137
+ get instance(): RaChartInstance | null {
138
+ return this.chart;
139
+ }
140
+
141
+ replay(): void {
142
+ this.zone.runOutsideAngular(() => this.chart?.replay());
143
+ }
144
+
145
+ resize(): void {
146
+ this.zone.runOutsideAngular(() => this.chart?.resize());
147
+ }
148
+
149
+ zoomTo(from: number, to: number): void {
150
+ this.zone.runOutsideAngular(() => this.chart?.zoomTo(from, to));
151
+ }
152
+
153
+ resetZoom(): void {
154
+ this.zone.runOutsideAngular(() => this.chart?.resetZoom());
155
+ }
156
+
157
+ toggleSeries(index: number, hidden?: boolean): void {
158
+ this.zone.runOutsideAngular(() => this.chart?.toggleSeries(index, hidden));
159
+ }
160
+
161
+ /** SVG markup of the current chart — handy for share sheets and exports. */
162
+ toSVG(): string {
163
+ return this.chart ? this.chart.toSVG() : '';
164
+ }
165
+
166
+ // ---------------------------------------------------------------
167
+ // internals
168
+ // ---------------------------------------------------------------
169
+
170
+ private buildOptions(): RaChartOptions {
171
+ const o: RaChartOptions = {
172
+ type: this.type,
173
+ data: this.data || [],
174
+ category: this.category,
175
+ series: this.series || [],
176
+ colors: this.colors,
177
+ stacked: this.stacked,
178
+ fullStacked: this.fullStacked,
179
+ smooth: this.smooth,
180
+ stepped: this.stepped,
181
+ semi: this.semi,
182
+ depth: this.depth,
183
+ fields: this.fields,
184
+ xField: this.xField,
185
+ sizeField: this.sizeField,
186
+ min: this.min,
187
+ max: this.max,
188
+ bands: this.bands,
189
+ zoom: this.zoom,
190
+ pinchZoom: this.pinchZoom,
191
+ legend: this.legend,
192
+ height: this.height,
193
+ haptics: this.haptics,
194
+ sheetTooltip: this.sheetTooltip,
195
+ tooltipHold: this.tooltipHold,
196
+ animation: this.animation
197
+ };
198
+ if (this.upColor) o.upColor = this.upColor;
199
+ if (this.downColor) o.downColor = this.downColor;
200
+ return o;
201
+ }
202
+
203
+ private boot(): void {
204
+ if (this.booted || this.isEmpty) return;
205
+ this.booted = true;
206
+
207
+ ensureRaChart(this.scriptUrl).then(Ctor => {
208
+ if (!this.hostRef) return;
209
+ this.zone.runOutsideAngular(() => {
210
+ this.bindEvents();
211
+ this.chart = new Ctor(this.hostRef.nativeElement, this.buildOptions());
212
+ });
213
+ this.zone.run(() => {
214
+ this.error = null;
215
+ this.ready.emit(this.chart!);
216
+ this.cdr.markForCheck();
217
+ });
218
+ }).catch((err: Error) => {
219
+ this.zone.run(() => {
220
+ this.error = 'Chart engine failed to load';
221
+ this.booted = false;
222
+ this.cdr.markForCheck();
223
+ });
224
+ console.error('[ra-chart]', err.message, '— expected at:', this.scriptUrl);
225
+ });
226
+ }
227
+
228
+ private bindEvents(): void {
229
+ const host = this.hostRef.nativeElement;
230
+ const bridge = (name: string, out: EventEmitter<any>) => {
231
+ const fn = ((e: CustomEvent) => {
232
+ // only surface to Angular if somebody is actually listening
233
+ if (out.observers.length === 0) return;
234
+ this.zone.run(() => out.emit(e.detail));
235
+ }) as EventListener;
236
+ host.addEventListener(name, fn);
237
+ this.listeners.push([name, fn]);
238
+ };
239
+ bridge('raChartSelect', this.select);
240
+ bridge('raChartZoom', this.zoomChange);
241
+ bridge('raChartToggle', this.toggle);
242
+ }
243
+
244
+ private teardownChart(): void {
245
+ const host = this.hostRef?.nativeElement;
246
+ if (host) {
247
+ this.listeners.forEach(([name, fn]) => host.removeEventListener(name, fn));
248
+ }
249
+ this.listeners = [];
250
+ this.chart?.destroy();
251
+ this.chart = null;
252
+ this.booted = false;
253
+ }
254
+ }
@@ -0,0 +1,63 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import type { RaChartConstructor } from './ra-chart.types';
3
+
4
+ /**
5
+ * Where `ra-chart.js` lives when it is NOT bundled via angular.json "scripts".
6
+ * Override in your app config to change the path:
7
+ *
8
+ * providers: [{ provide: RA_CHART_SCRIPT_URL, useValue: 'assets/vendor/ra-chart.js' }]
9
+ */
10
+ export const RA_CHART_SCRIPT_URL = new InjectionToken<string>('RA_CHART_SCRIPT_URL', {
11
+ providedIn: 'root',
12
+ factory: () => 'assets/ra-chart/ra-chart.js'
13
+ });
14
+
15
+ let pending: Promise<RaChartConstructor> | null = null;
16
+
17
+ /** The engine attaches itself to `window`; grab it if it is already there. */
18
+ function fromWindow(): RaChartConstructor | null {
19
+ const w = window as any;
20
+ return w && w.RaChart ? (w.RaChart as RaChartConstructor) : null;
21
+ }
22
+
23
+ /**
24
+ * Resolve the engine, lazy-injecting the script the first time if needed.
25
+ * Repeat calls share one in-flight promise, so many charts on one page
26
+ * trigger exactly one network request.
27
+ */
28
+ export function ensureRaChart(scriptUrl: string): Promise<RaChartConstructor> {
29
+ const existing = fromWindow();
30
+ if (existing) return Promise.resolve(existing);
31
+ if (pending) return pending;
32
+
33
+ pending = new Promise<RaChartConstructor>((resolve, reject) => {
34
+ // another loader may have injected the same tag already
35
+ const selector = `script[data-ra-chart="1"]`;
36
+ let tag = document.querySelector<HTMLScriptElement>(selector);
37
+
38
+ const done = () => {
39
+ const ctor = fromWindow();
40
+ if (ctor) resolve(ctor);
41
+ else reject(new Error('ra-chart.js loaded but window.RaChart is missing'));
42
+ };
43
+
44
+ if (tag) {
45
+ tag.addEventListener('load', done);
46
+ tag.addEventListener('error', () => reject(new Error('Failed to load ' + scriptUrl)));
47
+ return;
48
+ }
49
+
50
+ tag = document.createElement('script');
51
+ tag.src = scriptUrl;
52
+ tag.async = true;
53
+ tag.dataset['raChart'] = '1';
54
+ tag.onload = done;
55
+ tag.onerror = () => {
56
+ pending = null;
57
+ reject(new Error('Failed to load ' + scriptUrl));
58
+ };
59
+ document.head.appendChild(tag);
60
+ });
61
+
62
+ return pending;
63
+ }
@@ -0,0 +1,23 @@
1
+ import { NgModule, ModuleWithProviders } from '@angular/core';
2
+ import { RaChartComponent } from './ra-chart.component';
3
+ import { RA_CHART_SCRIPT_URL } from './ra-chart.loader';
4
+
5
+ /**
6
+ * Compatibility module for apps that are not on standalone components yet.
7
+ * Standalone apps can import `RaChartComponent` directly instead.
8
+ */
9
+ @NgModule({
10
+ imports: [RaChartComponent],
11
+ exports: [RaChartComponent]
12
+ })
13
+ export class RaChartModule {
14
+ /** Point the lazy loader at a custom script path. */
15
+ static forRoot(config: { scriptUrl?: string } = {}): ModuleWithProviders<RaChartModule> {
16
+ return {
17
+ ngModule: RaChartModule,
18
+ providers: config.scriptUrl
19
+ ? [{ provide: RA_CHART_SCRIPT_URL, useValue: config.scriptUrl }]
20
+ : []
21
+ };
22
+ }
23
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * raChart Cordova — types re-exported for Ionic/Angular consumers.
3
+ *
4
+ * These mirror `types/index.d.ts`; they live here as plain source so the
5
+ * Ionic app can import them without a package build step.
6
+ */
7
+
8
+ export type RaChartType =
9
+ | 'column' | 'bar' | 'line' | 'area' | 'lollipop'
10
+ | 'pie' | 'donut' | 'radar'
11
+ | 'treemap' | 'sankey' | 'wordcloud' | 'venn'
12
+ | 'candlestick' | 'timeline' | 'gantt'
13
+ | 'scatter' | 'bubble'
14
+ | 'funnel' | 'pyramid'
15
+ | 'gauge' | 'heatmap';
16
+
17
+ export interface RaSeries {
18
+ name: string;
19
+ field: string;
20
+ }
21
+
22
+ export interface RaFieldMap {
23
+ open?: string;
24
+ high?: string;
25
+ low?: string;
26
+ close?: string;
27
+ start?: string;
28
+ end?: string;
29
+ progress?: string;
30
+ from?: string;
31
+ to?: string;
32
+ value?: string;
33
+ }
34
+
35
+ export interface RaBand {
36
+ from?: number;
37
+ to?: number;
38
+ color?: string;
39
+ }
40
+
41
+ export interface RaAnimation {
42
+ duration?: number;
43
+ delay?: number;
44
+ }
45
+
46
+ export interface RaChartOptions {
47
+ type?: RaChartType;
48
+ data?: any[];
49
+ category?: string;
50
+ series?: RaSeries[];
51
+ colors?: string[] | null;
52
+ stacked?: boolean;
53
+ fullStacked?: boolean;
54
+ smooth?: boolean;
55
+ stepped?: boolean;
56
+ semi?: boolean;
57
+ depth?: number;
58
+ fields?: RaFieldMap | null;
59
+ xField?: string;
60
+ sizeField?: string;
61
+ min?: number;
62
+ max?: number | null;
63
+ bands?: RaBand[] | null;
64
+ upColor?: string;
65
+ downColor?: string;
66
+ zoom?: boolean;
67
+ pinchZoom?: boolean;
68
+ legend?: boolean;
69
+ height?: number;
70
+ haptics?: boolean;
71
+ sheetTooltip?: boolean | 'auto';
72
+ tooltipHold?: number;
73
+ animation?: RaAnimation;
74
+ }
75
+
76
+ export interface RaSelectEvent {
77
+ seriesIndex?: number;
78
+ categoryIndex?: number;
79
+ index?: number;
80
+ series?: string;
81
+ category?: string;
82
+ value?: number;
83
+ percent?: number;
84
+ x?: number;
85
+ y?: number;
86
+ size?: number;
87
+ ohlc?: { o: number; h: number; l: number; c: number };
88
+ start?: number;
89
+ end?: number;
90
+ progress?: number | null;
91
+ kind?: 'link' | 'node';
92
+ from?: string;
93
+ to?: string;
94
+ name?: string;
95
+ row?: any;
96
+ col?: number;
97
+ [key: string]: any;
98
+ }
99
+
100
+ export interface RaZoomEvent {
101
+ from: number;
102
+ to: number;
103
+ startIndex: number;
104
+ endIndex: number;
105
+ }
106
+
107
+ export interface RaToggleEvent {
108
+ index: number;
109
+ name: string;
110
+ hidden: boolean;
111
+ }
112
+
113
+ /** Runtime handle returned by the engine. */
114
+ export interface RaChartInstance {
115
+ setData(data: any[]): RaChartInstance;
116
+ setType(type: RaChartType): RaChartInstance;
117
+ update(partial: RaChartOptions): RaChartInstance;
118
+ replay(): RaChartInstance;
119
+ resize(): RaChartInstance;
120
+ zoomTo(from: number, to: number): RaChartInstance;
121
+ resetZoom(): RaChartInstance;
122
+ getZoom(): RaZoomEvent;
123
+ toggleSeries(index: number, hidden?: boolean): RaChartInstance;
124
+ toSVG(): string;
125
+ destroy(): void;
126
+ }
127
+
128
+ export interface RaChartConstructor {
129
+ new(host: HTMLElement | string, options?: RaChartOptions): RaChartInstance;
130
+ version: string;
131
+ defaults: RaChartOptions;
132
+ palette: string[];
133
+ paletteDark: string[];
134
+ types: RaChartType[];
135
+ create(host: HTMLElement | string, options?: RaChartOptions): RaChartInstance;
136
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "cordova-plugin-ra-chart",
3
+ "version": "1.0.0",
4
+ "description": "Dependency-free SVG chart engine for Cordova and Ionic — 21 chart types with touch gestures, pinch-to-zoom, haptics and dark-mode theming. No native code, no build step.",
5
+ "main": "www/ra-chart.js",
6
+ "types": "types/index.d.ts",
7
+ "license": "MIT",
8
+ "author": "RemoteApps",
9
+ "cordova": {
10
+ "id": "cordova-plugin-ra-chart",
11
+ "platforms": [
12
+ "android",
13
+ "ios",
14
+ "browser",
15
+ "electron"
16
+ ]
17
+ },
18
+ "keywords": [
19
+ "ecosystem:cordova",
20
+ "cordova-android",
21
+ "cordova-ios",
22
+ "cordova-browser",
23
+ "ionic",
24
+ "angular",
25
+ "chart",
26
+ "charts",
27
+ "svg",
28
+ "graph",
29
+ "dashboard",
30
+ "mobile",
31
+ "offline",
32
+ "no-dependencies"
33
+ ],
34
+ "files": [
35
+ "www/",
36
+ "types/",
37
+ "ionic/",
38
+ "plugin.xml",
39
+ "README.md"
40
+ ],
41
+ "engines": {
42
+ "cordovaDependencies": {
43
+ "1.0.0": {
44
+ "cordova": ">=9.0.0"
45
+ }
46
+ }
47
+ },
48
+ "dependencies": {},
49
+ "peerDependencies": {},
50
+ "sideEffects": [
51
+ "www/ra-chart.js",
52
+ "www/ra-chart.css"
53
+ ]
54
+ }
package/plugin.xml ADDED
@@ -0,0 +1,45 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ raChart Cordova plugin descriptor.
4
+
5
+ Install: cordova plugin add ./ra-chart-cordova
6
+ After install, `window.RaChart` is available on deviceready.
7
+
8
+ This is a pure web-asset plugin: it ships JS + CSS only, so it works on
9
+ every Cordova platform (and in the browser) with no native compilation.
10
+ -->
11
+ <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
12
+ id="cordova-plugin-ra-chart"
13
+ version="1.0.0">
14
+
15
+ <name>raChart</name>
16
+ <description>
17
+ Dependency-free SVG chart engine for Cordova and Ionic. 21 chart types with
18
+ touch gestures, pinch-to-zoom, haptics and dark-mode theming. No native code.
19
+ </description>
20
+ <license>MIT</license>
21
+ <keywords>cordova,ionic,chart,charts,svg,graph,dashboard,mobile,offline</keywords>
22
+ <author>RemoteApps</author>
23
+
24
+ <engines>
25
+ <engine name="cordova" version=">=9.0.0" />
26
+ </engines>
27
+
28
+ <!-- window.RaChart -->
29
+ <js-module src="www/ra-chart.js" name="RaChart">
30
+ <clobbers target="RaChart" />
31
+ </js-module>
32
+
33
+ <!--
34
+ Cordova does not inject stylesheets, so link the CSS yourself in index.html:
35
+ <link rel="stylesheet" href="plugins/cordova-plugin-ra-chart/www/ra-chart.css">
36
+ Ionic apps normally add it to angular.json "styles" instead.
37
+ -->
38
+ <asset src="www/ra-chart.css" target="css/ra-chart.css" />
39
+
40
+ <platform name="android" />
41
+ <platform name="ios" />
42
+ <platform name="browser" />
43
+ <platform name="electron" />
44
+
45
+ </plugin>