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.
package/README.md ADDED
@@ -0,0 +1,543 @@
1
+ # raChart Cordova
2
+
3
+ A dependency-free SVG chart engine for **Cordova** and **Ionic**. 21 chart types, built for touch: tap to inspect, drag to scrub, pinch to zoom, with haptics, dark mode and offline rendering.
4
+
5
+ No jQuery. No chart library. No native code. No build step. One 60 KB JS file and one CSS file.
6
+
7
+ ```
8
+ ra-chart-cordova/
9
+ ├── plugin.xml Cordova plugin descriptor
10
+ ├── package.json
11
+ ├── www/
12
+ │ ├── ra-chart.js the engine (window.RaChart)
13
+ │ └── ra-chart.css styles + theme variables
14
+ ├── types/index.d.ts TypeScript definitions
15
+ ├── ionic/ Angular wrapper
16
+ │ ├── ra-chart.component.ts/.html <ra-chart>
17
+ │ ├── ra-chart.module.ts NgModule for non-standalone apps
18
+ │ ├── ra-chart.loader.ts lazy script loader
19
+ │ ├── ra-chart.types.ts
20
+ │ └── index.ts
21
+ └── demo/
22
+ ├── index.html plain mobile demo (open in a browser)
23
+ └── dashboard.page.ts/.html Ionic page example
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Table of contents
29
+
30
+ 1. [Install](#install)
31
+ 2. [Quick start](#quick-start)
32
+ 3. [Ionic / Angular usage](#ionic--angular-usage)
33
+ 4. [Chart types](#chart-types)
34
+ 5. [Data shapes](#data-shapes)
35
+ 6. [Options](#options)
36
+ 7. [Methods](#methods)
37
+ 8. [Events](#events)
38
+ 9. [Mobile behaviour](#mobile-behaviour)
39
+ 10. [Theming and dark mode](#theming-and-dark-mode)
40
+ 11. [Recipes](#recipes)
41
+ 12. [Troubleshooting](#troubleshooting)
42
+
43
+ ---
44
+
45
+ ## Install
46
+
47
+ ### Option A — Cordova plugin
48
+
49
+ ```bash
50
+ cordova plugin add ./ra-chart-cordova
51
+ ```
52
+
53
+ `window.RaChart` is available once `deviceready` fires. Cordova does not inject CSS, so link it yourself in `index.html`:
54
+
55
+ ```html
56
+ <link rel="stylesheet" href="plugins/cordova-plugin-ra-chart/www/ra-chart.css">
57
+ ```
58
+
59
+ ### Option B — Ionic app (recommended)
60
+
61
+ Copy the two runtime files into your assets and the wrapper into your source tree:
62
+
63
+ ```
64
+ src/assets/ra-chart/ra-chart.js ← from www/
65
+ src/assets/ra-chart/ra-chart.css ← from www/
66
+ src/app/shared/ra-chart/ ← the whole ionic/ folder
67
+ ```
68
+
69
+ Add the stylesheet to `angular.json`:
70
+
71
+ ```json
72
+ "styles": [
73
+ "src/theme/variables.scss",
74
+ "src/global.scss",
75
+ "src/assets/ra-chart/ra-chart.css"
76
+ ]
77
+ ```
78
+
79
+ The component lazy-loads `ra-chart.js` from `assets/ra-chart/ra-chart.js` on first use. To bundle it eagerly instead, add it to `angular.json` → `"scripts"`; the loader will detect `window.RaChart` and skip the fetch.
80
+
81
+ To load it from somewhere else:
82
+
83
+ ```ts
84
+ // main.ts
85
+ import { RA_CHART_SCRIPT_URL } from './app/shared/ra-chart';
86
+
87
+ bootstrapApplication(AppComponent, {
88
+ providers: [
89
+ { provide: RA_CHART_SCRIPT_URL, useValue: 'assets/vendor/ra-chart.js' }
90
+ ]
91
+ });
92
+ ```
93
+
94
+ ### Option C — plain browser
95
+
96
+ ```html
97
+ <link rel="stylesheet" href="ra-chart.css">
98
+ <script src="ra-chart.js"></script>
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Quick start
104
+
105
+ ```html
106
+ <div id="sales"></div>
107
+ ```
108
+
109
+ ```js
110
+ var chart = new RaChart('#sales', {
111
+ type: 'column',
112
+ category: 'month',
113
+ series: [
114
+ { name: 'Revenue', field: 'revenue' },
115
+ { name: 'Target', field: 'target' }
116
+ ],
117
+ data: [
118
+ { month: 'Jan', revenue: 42, target: 38 },
119
+ { month: 'Feb', revenue: 55, target: 45 }
120
+ ],
121
+ height: 240
122
+ });
123
+
124
+ chart.setData(newRows); // animates to the new values
125
+ chart.destroy(); // always clean up
126
+ ```
127
+
128
+ In Cordova, wait for the platform first:
129
+
130
+ ```js
131
+ document.addEventListener('deviceready', function () {
132
+ new RaChart('#sales', { /* ... */ });
133
+ });
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Ionic / Angular usage
139
+
140
+ ### Standalone component (Angular 16+)
141
+
142
+ ```ts
143
+ import { Component } from '@angular/core';
144
+ import { RaChartComponent } from '../shared/ra-chart';
145
+ import type { RaSeries, RaSelectEvent } from '../shared/ra-chart';
146
+
147
+ @Component({
148
+ selector: 'app-sales',
149
+ standalone: true,
150
+ imports: [RaChartComponent],
151
+ templateUrl: './sales.page.html'
152
+ })
153
+ export class SalesPage {
154
+ series: RaSeries[] = [{ name: 'Revenue', field: 'revenue' }];
155
+ rows = [{ month: 'Jan', revenue: 42 }, { month: 'Feb', revenue: 55 }];
156
+
157
+ onSelect(e: RaSelectEvent) {
158
+ console.log(e.category, e.value);
159
+ }
160
+ }
161
+ ```
162
+
163
+ ```html
164
+ <ra-chart type="column"
165
+ category="month"
166
+ [series]="series"
167
+ [data]="rows"
168
+ [zoom]="true"
169
+ [height]="240"
170
+ (select)="onSelect($event)">
171
+ </ra-chart>
172
+ ```
173
+
174
+ ### NgModule apps
175
+
176
+ ```ts
177
+ import { RaChartModule } from './shared/ra-chart';
178
+
179
+ @NgModule({
180
+ imports: [RaChartModule] // or RaChartModule.forRoot({ scriptUrl: '...' })
181
+ })
182
+ export class SalesPageModule {}
183
+ ```
184
+
185
+ ### Calling methods
186
+
187
+ ```ts
188
+ @ViewChild('chart') chart?: RaChartComponent;
189
+ ```
190
+
191
+ ```html
192
+ <ra-chart #chart ...></ra-chart>
193
+ ```
194
+
195
+ ```ts
196
+ this.chart?.replay();
197
+ this.chart?.zoomTo(0.5, 1);
198
+ this.chart?.resetZoom();
199
+ const svg = this.chart?.toSVG();
200
+ this.chart?.instance?.setData(rows); // raw engine handle
201
+ ```
202
+
203
+ ### Change detection
204
+
205
+ The component is `OnPush` and the engine runs **outside** Angular's zone, so touch scrubbing and animations never trigger change detection. Replace arrays instead of mutating them so `ngOnChanges` fires:
206
+
207
+ ```ts
208
+ this.rows = [...this.rows, newRow]; // ✅ detected
209
+ this.rows.push(newRow); // ❌ ignored
210
+ ```
211
+
212
+ A change to `data` alone animates the values in place. Changing any other input rebuilds the chart.
213
+
214
+ ---
215
+
216
+ ## Chart types
217
+
218
+ | `type` | Shape | Notes |
219
+ |---|---|---|
220
+ | `column` | vertical bars | `stacked`, `fullStacked`, `depth` |
221
+ | `bar` | horizontal bars | same options |
222
+ | `line` | line | `smooth`, `stepped` |
223
+ | `area` | filled line | `smooth`, `stepped` |
224
+ | `lollipop` | stem + dot | good for sparse mobile series |
225
+ | `pie` | pie | `semi`, `depth` |
226
+ | `donut` | donut with live centre total | `semi`, `depth` |
227
+ | `radar` | multi-axis polygon | |
228
+ | `treemap` | squarified hierarchy | |
229
+ | `sankey` | flow diagram | uses `from`/`to`/`value` rows |
230
+ | `wordcloud` | spiral-packed words | |
231
+ | `venn` | up to 3 sets | |
232
+ | `candlestick` | OHLC | `upColor`, `downColor`, `zoom` |
233
+ | `timeline` | events on an axis | accepts dates |
234
+ | `gantt` | task bars with progress | accepts dates |
235
+ | `scatter` | numeric X/Y | `xField` |
236
+ | `bubble` | X/Y + size | `xField`, `sizeField` |
237
+ | `funnel` | conversion stages | |
238
+ | `pyramid` | proportional triangle | |
239
+ | `gauge` | needle + bands | `max`, `bands` |
240
+ | `heatmap` | matrix on a sequential ramp | |
241
+
242
+ `RaChart.types` returns this list at runtime.
243
+
244
+ ---
245
+
246
+ ## Data shapes
247
+
248
+ Everything takes an **array of plain objects**. `category` names the label field; `series` maps display names to value fields.
249
+
250
+ **Most types** — `column`, `bar`, `line`, `area`, `lollipop`, `radar`, `heatmap`:
251
+
252
+ ```js
253
+ category: 'month',
254
+ series: [{ name: 'Revenue', field: 'revenue' }, { name: 'Cost', field: 'cost' }],
255
+ data: [{ month: 'Jan', revenue: 42, cost: 30 }]
256
+ ```
257
+
258
+ **Single-value types** — `pie`, `donut`, `treemap`, `wordcloud`, `venn`, `funnel`, `pyramid`, `gauge` (first series only):
259
+
260
+ ```js
261
+ category: 'source',
262
+ series: [{ name: 'Share', field: 'value' }],
263
+ data: [{ source: 'Direct', value: 42 }]
264
+ ```
265
+
266
+ **Candlestick:**
267
+
268
+ ```js
269
+ category: 'day',
270
+ data: [{ day: '1', open: 102, high: 108, low: 100, close: 106 }]
271
+ // rename columns: fields: { open: 'o', high: 'h', low: 'l', close: 'c' }
272
+ ```
273
+
274
+ **Gantt** — dates or numbers, `progress` is optional (0–100):
275
+
276
+ ```js
277
+ category: 'task',
278
+ data: [{ task: 'Design', start: '2026-01-20', end: '2026-03-01', progress: 100 }]
279
+ // fields: { start: 'from', end: 'until', progress: 'pct' }
280
+ ```
281
+
282
+ **Timeline:**
283
+
284
+ ```js
285
+ category: 'milestone',
286
+ series: [{ name: 'Date', field: 'date' }],
287
+ data: [{ milestone: 'Launch', date: '2026-07-01' }]
288
+ ```
289
+
290
+ **Sankey** — no `category`/`series`, just links:
291
+
292
+ ```js
293
+ data: [{ from: 'Solar', to: 'Grid', value: 30 }]
294
+ // fields: { from: 'src', to: 'dst', value: 'amount' }
295
+ ```
296
+
297
+ **Scatter / bubble:**
298
+
299
+ ```js
300
+ xField: 'price', sizeField: 'reach',
301
+ series: [{ name: 'Brand A', field: 'rating' }],
302
+ data: [{ price: 12, rating: 3.8, reach: 20 }]
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Options
308
+
309
+ ### Data
310
+
311
+ | Option | Default | Description |
312
+ |---|---|---|
313
+ | `type` | `'column'` | Any type from the table above |
314
+ | `data` | `[]` | Array of plain rows |
315
+ | `category` | `'category'` | Row field used for labels |
316
+ | `series` | `[]` | `[{ name, field }]` |
317
+ | `colors` | `null` | Hex array; `null` picks the theme-aware palette |
318
+ | `fields` | `null` | Rename columns per type (see above) |
319
+ | `xField` / `sizeField` | `'x'` / `'size'` | Scatter and bubble |
320
+
321
+ ### Shape
322
+
323
+ | Option | Default | Description |
324
+ |---|---|---|
325
+ | `stacked` | `false` | Stack column/bar series |
326
+ | `fullStacked` | `false` | 100% stacked; tooltips switch to % |
327
+ | `smooth` | `false` | Curved line/area |
328
+ | `stepped` | `false` | Stepped line/area |
329
+ | `semi` | `false` | Half-circle pie/donut |
330
+ | `depth` | `0` | 3D extrusion px (column, bar, pie, donut) |
331
+
332
+ ### Scale
333
+
334
+ | Option | Default | Description |
335
+ |---|---|---|
336
+ | `min` | `0` | Value-axis minimum |
337
+ | `max` | `null` | Gauge maximum |
338
+ | `bands` | `null` | Gauge zones `[{ from, to, color }]` |
339
+ | `upColor` / `downColor` | green / red | Candlestick bull/bear |
340
+
341
+ ### Interaction
342
+
343
+ | Option | Default | Description |
344
+ |---|---|---|
345
+ | `zoom` | `false` | Range slider (XY types + candlestick) |
346
+ | `pinchZoom` | `true` | Pinch and pan when `zoom` is on |
347
+ | `legend` | `true` | Tappable; auto-hidden for single-series XY |
348
+ | `haptics` | `true` | Vibrate on touch-select |
349
+ | `sheetTooltip` | `'auto'` | Dock tooltip to bottom; auto = touch device under 480px |
350
+ | `tooltipHold` | `2600` | ms the tooltip stays after finger lift; `0` = until next tap |
351
+
352
+ ### Presentation
353
+
354
+ | Option | Default | Description |
355
+ |---|---|---|
356
+ | `height` | `260` | Plot height px; legend and zoom bar sit outside it |
357
+ | `animation` | `{duration: 800, delay: 70}` | Entrance timing; data changes morph over 550 ms |
358
+
359
+ ---
360
+
361
+ ## Methods
362
+
363
+ ```js
364
+ chart.setData(rows) // morph to new values (rebuilds if the shape changed)
365
+ chart.setType('donut') // switch type; resets zoom and hidden series
366
+ chart.update({ depth: 12 }) // merge options and rebuild
367
+ chart.replay() // replay the entrance animation
368
+ chart.resize() // re-measure (automatic on rotate/resize)
369
+
370
+ chart.zoomTo(0.25, 0.75) // fraction window
371
+ chart.zoomTo(5, 10) // integers = data index range
372
+ chart.resetZoom()
373
+ chart.getZoom() // { from, to, startIndex, endIndex }
374
+
375
+ chart.toggleSeries(1) // hide/show series or slice by index
376
+ chart.toggleSeries(1, true) // explicitly hide
377
+
378
+ chart.toSVG() // SVG string, background included — for share/export
379
+ chart.destroy() // remove listeners and observers
380
+ ```
381
+
382
+ Statics: `RaChart.version`, `RaChart.defaults`, `RaChart.palette`, `RaChart.paletteDark`, `RaChart.types`, `RaChart.create(el, opts)`.
383
+
384
+ ---
385
+
386
+ ## Events
387
+
388
+ The engine dispatches **DOM CustomEvents** on the host element. The Angular component re-emits them as `@Output()`s.
389
+
390
+ | DOM event | Angular output | Detail |
391
+ |---|---|---|
392
+ | `raChartReady` | `(ready)` | The chart instance |
393
+ | `raChartSelect` | `(select)` | The tapped mark |
394
+ | `raChartZoom` | `(zoomChange)` | `{ from, to, startIndex, endIndex }` |
395
+ | `raChartToggle` | `(toggle)` | `{ index, name, hidden }` |
396
+
397
+ ```js
398
+ document.getElementById('sales')
399
+ .addEventListener('raChartSelect', function (e) {
400
+ console.log(e.detail.category, e.detail.value);
401
+ });
402
+ ```
403
+
404
+ `raChartSelect` detail varies by type — `category`/`series`/`value` for most, plus `ohlc` for candlestick, `start`/`end`/`progress` for gantt, `x`/`y`/`size` for scatter and bubble, `kind`/`from`/`to` for sankey. `row` carries the original data object.
405
+
406
+ ---
407
+
408
+ ## Mobile behaviour
409
+
410
+ **Touch.** Tap any mark to open its tooltip; drag without lifting to scrub across marks, each one snapping the tooltip along. The tooltip stays `tooltipHold` ms after you lift so it is readable, then fades. Tapping empty space dismisses it.
411
+
412
+ **Pinch and pan.** With `zoom: true`, two fingers on the plot zoom around the pinch midpoint and one finger pans once you are zoomed in. Panning only takes over the gesture after clearly horizontal movement, so vertical page scrolling still works. The range slider stays in sync.
413
+
414
+ **Hit targets.** On coarse pointers, dots and points get invisible finger-sized hit rings, thin candlesticks get full-band hit columns, zoom handles grow to 26 px, and legend rows carry 44 px of padded height.
415
+
416
+ **Haptics.** Selecting a mark or toggling a legend entry fires a light impact through Capacitor Haptics when available, falling back to `navigator.vibrate`, and silently doing nothing otherwise. Disable with `haptics: false`.
417
+
418
+ **Bottom-sheet tooltips.** Under 480 px on touch devices the tooltip docks to the bottom edge instead of floating, so your thumb never covers the value.
419
+
420
+ **Rotation.** A `ResizeObserver` on the host redraws after orientation changes, debounced 160 ms, without re-running entrance animations.
421
+
422
+ **Backgrounding.** Browsers freeze `requestAnimationFrame` while a page is hidden, which would otherwise leave a chart stuck mid-animation when the user switches apps. Charts built while hidden skip straight to their final frame, and a tween interrupted by backgrounding settles on its last frame instead of resuming half-drawn.
423
+
424
+ **Reduced motion.** With `prefers-reduced-motion: reduce`, animations resolve to their final frame immediately.
425
+
426
+ **Offline.** Everything renders locally — no network, no fonts, no tiles.
427
+
428
+ ---
429
+
430
+ ## Theming and dark mode
431
+
432
+ Every colour is a CSS custom property, and the engine reads the same variables when it paints SVG. Restyle from your Ionic theme file without touching JS:
433
+
434
+ ```css
435
+ :root {
436
+ --ra-chart-surface: #ffffff;
437
+ --ra-chart-text: #1f2937;
438
+ --ra-chart-label: #898781;
439
+ --ra-chart-grid: #e1e0d9;
440
+ --ra-chart-zoom-handle-border: #534ab7;
441
+ }
442
+ ```
443
+
444
+ Dark mode activates automatically from `prefers-color-scheme`, Ionic 8's `.ion-palette-dark`, or a legacy `body.dark`. The series palette swaps to steps validated against a dark surface — it is a re-stepped palette, not an inverted one.
445
+
446
+ Force a mode per chart with a class:
447
+
448
+ ```html
449
+ <div class="ra-chart ra-chart--light"></div>
450
+ <div class="ra-chart ra-chart--dark"></div>
451
+ ```
452
+
453
+ The default 8-colour palette is ordered so adjacent series stay distinguishable for colourblind viewers. If you pass your own `colors`, keep that in mind. Charts also carry non-colour cues — legends, direct labels, tooltips.
454
+
455
+ ---
456
+
457
+ ## Recipes
458
+
459
+ **Live data**
460
+
461
+ ```ts
462
+ this.sub = interval(5000).subscribe(() => {
463
+ this.rows = [...fetchLatest()]; // new array reference
464
+ });
465
+ ```
466
+
467
+ **Switch type from an ion-segment**
468
+
469
+ ```html
470
+ <ion-segment [value]="type" (ionChange)="type = $any($event.detail.value)">
471
+ <ion-segment-button value="column"><ion-label>Bars</ion-label></ion-segment-button>
472
+ <ion-segment-button value="donut"><ion-label>Donut</ion-label></ion-segment-button>
473
+ </ion-segment>
474
+
475
+ <ra-chart [type]="type" ...></ra-chart>
476
+ ```
477
+
478
+ **Share a chart as an image**
479
+
480
+ ```ts
481
+ import { Share } from '@capacitor/share';
482
+ import { Filesystem, Directory } from '@capacitor/filesystem';
483
+
484
+ async share() {
485
+ const svg = this.chart!.toSVG();
486
+ const file = await Filesystem.writeFile({
487
+ path: 'chart.svg',
488
+ data: btoa(unescape(encodeURIComponent(svg))),
489
+ directory: Directory.Cache
490
+ });
491
+ await Share.share({ title: 'Chart', url: file.uri });
492
+ }
493
+ ```
494
+
495
+ **Brand colours**
496
+
497
+ ```ts
498
+ colors = ['#534ab7', '#2196f3', '#f0334b', '#eda100'];
499
+ ```
500
+
501
+ ```html
502
+ <ra-chart [colors]="colors" ...></ra-chart>
503
+ ```
504
+
505
+ **Long series on a small screen** — show a window and let the user pinch:
506
+
507
+ ```ts
508
+ ready(chart: RaChartInstance) {
509
+ chart.zoomTo(Math.max(0, this.rows.length - 12), this.rows.length);
510
+ }
511
+ ```
512
+
513
+ ```html
514
+ <ra-chart [zoom]="true" (ready)="ready($event)" ...></ra-chart>
515
+ ```
516
+
517
+ ---
518
+
519
+ ## Troubleshooting
520
+
521
+ **"Chart engine failed to load"** — `ra-chart.js` is not at the loader's path. Confirm it sits in `src/assets/ra-chart/`, that `angular.json` copies `src/assets`, and override `RA_CHART_SCRIPT_URL` if you keep it elsewhere.
522
+
523
+ **Chart is blank but no error** — the host had zero width at build time (a hidden tab, a collapsed accordion). Call `chart.resize()` or the component's `resize()` when the container becomes visible.
524
+
525
+ **Nothing happens when data changes** — you mutated the array. Assign a new reference.
526
+
527
+ **Page will not scroll over the chart** — only happens with `zoom: true`; vertical scrolling is preserved and horizontal drags pan. Set `pinchZoom: false` to disable gesture capture entirely.
528
+
529
+ **Labels are cut off** — increase `height`, or shorten category labels. Horizontal `bar` and `gantt` cap the label gutter at about a third of the width.
530
+
531
+ **Tooltip is clipped inside `ion-card`** — the tooltip is absolutely positioned inside the chart element; give the card content enough padding, or set `sheetTooltip: true` to dock it.
532
+
533
+ **No haptics on device** — install `@capacitor/haptics`, or rely on the `navigator.vibrate` fallback (Android only; iOS Safari does not support it).
534
+
535
+ ---
536
+
537
+ ## Browser and platform support
538
+
539
+ Android WebView 60+ (Android 8+), iOS 12+, and all modern desktop browsers. `ResizeObserver` falls back to window resize/orientation listeners; `CustomEvent` falls back to `createEvent` for old WebViews.
540
+
541
+ ## Licence
542
+
543
+ MIT — RemoteApps.
package/ionic/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * raChart Cordova — Ionic/Angular entry point.
3
+ *
4
+ * Standalone (Angular 16+):
5
+ * import { RaChartComponent } from 'src/app/shared/ra-chart';
6
+ * @Component({ imports: [RaChartComponent], ... })
7
+ *
8
+ * NgModule apps:
9
+ * import { RaChartModule } from 'src/app/shared/ra-chart';
10
+ * @NgModule({ imports: [RaChartModule], ... })
11
+ */
12
+
13
+ export * from './ra-chart.types';
14
+ export * from './ra-chart.loader';
15
+ export * from './ra-chart.component';
16
+ export * from './ra-chart.module';
@@ -0,0 +1,15 @@
1
+ <!-- The engine renders into this element and adds its own .ra-chart class. -->
2
+ <div #host class="ra-chart" [class.ra-chart--busy]="loading || isEmpty"></div>
3
+
4
+ <div class="ra-chart__loading" *ngIf="loading" [style.height.px]="height">
5
+ <span class="ra-chart__spinner"></span>
6
+ <span>{{ loadingText }}</span>
7
+ </div>
8
+
9
+ <div class="ra-chart__empty" *ngIf="!loading && isEmpty" [style.height.px]="height">
10
+ <span>{{ emptyText }}</span>
11
+ </div>
12
+
13
+ <div class="ra-chart__empty" *ngIf="error" [style.height.px]="height">
14
+ <span>{{ error }}</span>
15
+ </div>