wickchart-compare 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.
- package/README.md +48 -0
- package/compare.mjs +200 -0
- package/core.mjs +141 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# wickchart-compare
|
|
2
|
+
|
|
3
|
+
Normalized multi-asset overlays for [wickchart](https://github.com/benyblack/wickchart),
|
|
4
|
+
as an opt-in plugin layer — the core stays compare-free. Zero dependencies,
|
|
5
|
+
zero core changes: the lines draw through the public layer API against their
|
|
6
|
+
own invisible scale, so the price axis is never distorted.
|
|
7
|
+
|
|
8
|
+
Two kinds of entries:
|
|
9
|
+
|
|
10
|
+
- **Percent lines** — a close series rebased to 0% (TradingView-style
|
|
11
|
+
compare overlay), drawn as a colored line over the main pane.
|
|
12
|
+
- **Derived lines** — `ratio` (a/b) or `diff` (a−b) of two series,
|
|
13
|
+
the `formula="BTC/ETH"` use case, rebased the same way; the legend chip
|
|
14
|
+
shows the raw ratio/diff value.
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
npm install wickchart wickchart-compare // the plugin is a separate package
|
|
18
|
+
|
|
19
|
+
import 'wickchart'; // the chart itself
|
|
20
|
+
import { attachCompare } from 'wickchart-compare';
|
|
21
|
+
|
|
22
|
+
const chart = document.querySelector('wick-chart');
|
|
23
|
+
const cmp = attachCompare(chart);
|
|
24
|
+
|
|
25
|
+
cmp.setSeries([
|
|
26
|
+
{ label: 'ETH', data: ethBars }, // OHLC or {time, value}
|
|
27
|
+
{ label: 'SOL', data: solBars, color: '#22d3ee', width: 2 },
|
|
28
|
+
{ label: 'BTC/ETH', op: 'ratio', a: btcBars, b: ethBars }, // derived
|
|
29
|
+
{ label: 'BTC−ETH', op: 'diff', a: btcBars, b: ethBars },
|
|
30
|
+
]);
|
|
31
|
+
cmp.setRebase('visible'); // 0% at the window's left edge, re-normalized as
|
|
32
|
+
// you pan (TV-style); 'first' = dataset start;
|
|
33
|
+
// or an epoch-ms anchor
|
|
34
|
+
cmp.clear(); // remove all lines
|
|
35
|
+
cmp.detach();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **Time alignment**: every series is sampled onto the main chart's bar
|
|
39
|
+
times (last known value at-or-before each bar), so timeframes can mix and
|
|
40
|
+
gaps break the line instead of bridging.
|
|
41
|
+
- **Legend**: a chip row under the core legend shows each series with its
|
|
42
|
+
live value (`ETH +3.24%`, `BTC/ETH 1543.2`), recomputed every frame.
|
|
43
|
+
- **Scale**: rebased values share one invisible scale inset 8% from the
|
|
44
|
+
pane's top/bottom edges; the main chart's price scale is untouched.
|
|
45
|
+
- Series are validated and capped at 6; invalid entries are dropped, never
|
|
46
|
+
thrown.
|
|
47
|
+
|
|
48
|
+
Peer dependency: wickchart ≥ 1.4.0 (the plugin layer API).
|
package/compare.mjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wickchart-compare — normalized multi-asset overlays as a wickchart plugin
|
|
3
|
+
* layer: percent-rebased close series (compare="ETH"-style lines) and
|
|
4
|
+
* derived ratio / diff lines (formula="BTC/ETH"-style), drawn over the main
|
|
5
|
+
* pane against their own invisible scale so the price axis is untouched.
|
|
6
|
+
* Everything builds on the public layer API (addLayer) — zero core changes.
|
|
7
|
+
*
|
|
8
|
+
* import { attachCompare } from 'wickchart-compare';
|
|
9
|
+
* const cmp = attachCompare(chart);
|
|
10
|
+
* cmp.setSeries([
|
|
11
|
+
* { label: 'ETH', data: ethBars }, // % line (closes)
|
|
12
|
+
* { label: 'BTC/ETH', op: 'ratio', a: btcBars, b: ethBars },
|
|
13
|
+
* { label: 'BTC−ETH', op: 'diff', a: btcBars, b: ethBars },
|
|
14
|
+
* ]);
|
|
15
|
+
* cmp.setRebase('visible'); // 0% at the window's left edge (TV-style);
|
|
16
|
+
* // 'first' (dataset start) or an epoch ms
|
|
17
|
+
* cmp.clear(); cmp.detach();
|
|
18
|
+
*
|
|
19
|
+
* A legend row under the core legend shows every series with its live value
|
|
20
|
+
* (+3.2% for percent lines, the raw ratio/diff for derived ones). Series are
|
|
21
|
+
* time-aligned onto the main chart's bars, so any timeframes can mix.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { normalizeSeries, computeLine, timeWindow, DEFAULT_COLORS } from './core.mjs';
|
|
25
|
+
|
|
26
|
+
const FONT = '600 10px ui-sans-serif, system-ui, sans-serif';
|
|
27
|
+
const PAD_FRAC = 0.08; // keep rebased lines off the pane's top/bottom edges
|
|
28
|
+
|
|
29
|
+
const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
30
|
+
|
|
31
|
+
/** Compact value formatting for legend chips (ratio/diff raw values). */
|
|
32
|
+
function fmtV(v) {
|
|
33
|
+
const a = Math.abs(v);
|
|
34
|
+
return a >= 1000 ? v.toFixed(0) : a >= 100 ? v.toFixed(1) : a >= 1 ? v.toFixed(3) : v.toFixed(4);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normRebase = (r) => (r === 'visible' || r === 'first' || isNum(r) ? r : 'first');
|
|
38
|
+
|
|
39
|
+
export function attachCompare(chart, opts = {}) {
|
|
40
|
+
return new CompareLayer(chart, opts);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class CompareLayer {
|
|
44
|
+
constructor(chart, opts = {}) {
|
|
45
|
+
if (!chart || typeof chart.addLayer !== 'function') {
|
|
46
|
+
throw new TypeError('attachCompare(chart): the chart element is required');
|
|
47
|
+
}
|
|
48
|
+
this._chart = chart;
|
|
49
|
+
this._defs = normalizeSeries(opts.series);
|
|
50
|
+
this._rebase = normRebase(opts.rebase);
|
|
51
|
+
this._layer = { id: 'wick-compare', draw: (api) => this._render(api) };
|
|
52
|
+
chart.addLayer(this._layer);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ---------------- public API ---------------- */
|
|
56
|
+
|
|
57
|
+
/** Replace the compare entries (validated; invalid entries are dropped). */
|
|
58
|
+
setSeries(list) {
|
|
59
|
+
this._defs = normalizeSeries(list);
|
|
60
|
+
this._redraw();
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Rebase mode: 'visible' | 'first' | epoch-ms number. */
|
|
65
|
+
setRebase(r) {
|
|
66
|
+
this._rebase = normRebase(r);
|
|
67
|
+
this._redraw();
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get rebase() {
|
|
72
|
+
return this._rebase;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get count() {
|
|
76
|
+
return this._defs.length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clear() {
|
|
80
|
+
this._defs = [];
|
|
81
|
+
this._redraw();
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
detach() {
|
|
86
|
+
try {
|
|
87
|
+
this._chart.removeLayer('wick-compare');
|
|
88
|
+
} catch (_) {}
|
|
89
|
+
this._chart = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ---------------- render ---------------- */
|
|
93
|
+
|
|
94
|
+
_redraw() {
|
|
95
|
+
if (this._chart && typeof this._chart.requestDraw === 'function') this._chart.requestDraw();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_colorOf(def, pal, i) {
|
|
99
|
+
if (def.color === 'up' || def.color === 'down' || def.color === 'accent') return pal[def.color];
|
|
100
|
+
if (def.color) return def.color;
|
|
101
|
+
return DEFAULT_COLORS[i % DEFAULT_COLORS.length];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_render(api) {
|
|
105
|
+
const { ctx, layout, palette: pal, data } = api;
|
|
106
|
+
if (!data.length || !this._defs.length) return;
|
|
107
|
+
const main = layout.main;
|
|
108
|
+
const t0 = api.xToTime(0);
|
|
109
|
+
const t1 = api.xToTime(layout.plotRight);
|
|
110
|
+
if (!isNum(t0) || !isNum(t1) || t1 <= t0) return;
|
|
111
|
+
const win = timeWindow(data, t0, t1);
|
|
112
|
+
if (!win) return;
|
|
113
|
+
|
|
114
|
+
// one rebased line per entry, aligned onto the main chart's bar times
|
|
115
|
+
const times = [];
|
|
116
|
+
for (let i = win[0]; i <= win[1]; i++) times.push(data[i].time);
|
|
117
|
+
const lines = this._defs.map((def) => computeLine(def, times, this._rebase));
|
|
118
|
+
|
|
119
|
+
// shared invisible scale: the union of all rebased values in view
|
|
120
|
+
let min = Infinity;
|
|
121
|
+
let max = -Infinity;
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
for (const p of line) {
|
|
124
|
+
if (p && isNum(p.pct)) {
|
|
125
|
+
if (p.pct < min) min = p.pct;
|
|
126
|
+
if (p.pct > max) max = p.pct;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!isNum(min) || !isNum(max)) return;
|
|
131
|
+
const pad = main.h * PAD_FRAC;
|
|
132
|
+
const span = max - min || 1;
|
|
133
|
+
const yOf = (pct) => main.y0 + pad + ((max - pct) / span) * (main.h - 2 * pad);
|
|
134
|
+
|
|
135
|
+
ctx.save();
|
|
136
|
+
ctx.beginPath();
|
|
137
|
+
ctx.rect(0, main.y0, layout.plotRight + 1, main.h);
|
|
138
|
+
ctx.clip();
|
|
139
|
+
ctx.globalAlpha = 0.9;
|
|
140
|
+
|
|
141
|
+
lines.forEach((line, si) => {
|
|
142
|
+
const color = this._colorOf(this._defs[si], pal, si);
|
|
143
|
+
ctx.strokeStyle = color;
|
|
144
|
+
ctx.lineWidth = this._defs[si].width || 1.5;
|
|
145
|
+
ctx.beginPath();
|
|
146
|
+
let started = false;
|
|
147
|
+
for (const p of line) {
|
|
148
|
+
if (!p || !isNum(p.pct)) {
|
|
149
|
+
started = false; // gap in the series — break the stroke
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const x = api.timeToX(p.t);
|
|
153
|
+
const y = yOf(p.pct);
|
|
154
|
+
if (!isNum(x) || !isNum(y)) continue;
|
|
155
|
+
if (!started) {
|
|
156
|
+
ctx.moveTo(x, y);
|
|
157
|
+
started = true;
|
|
158
|
+
} else {
|
|
159
|
+
ctx.lineTo(x, y);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
ctx.stroke();
|
|
163
|
+
});
|
|
164
|
+
ctx.restore();
|
|
165
|
+
ctx.globalAlpha = 1;
|
|
166
|
+
|
|
167
|
+
this._legend(ctx, api, lines, pal);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** One chip per series: color square + label + live value. */
|
|
171
|
+
_legend(ctx, api, lines, pal) {
|
|
172
|
+
ctx.save();
|
|
173
|
+
ctx.font = FONT;
|
|
174
|
+
ctx.textAlign = 'left';
|
|
175
|
+
ctx.textBaseline = 'middle';
|
|
176
|
+
const xMax = api.layout.plotRight - 40;
|
|
177
|
+
let x = 10;
|
|
178
|
+
const y = api.layout.main.y0 + 28;
|
|
179
|
+
for (let si = 0; si < this._defs.length; si++) {
|
|
180
|
+
const def = this._defs[si];
|
|
181
|
+
const last = [...lines[si]].reverse().find((p) => p && isNum(p.pct));
|
|
182
|
+
if (!last) continue;
|
|
183
|
+
if (x > xMax) break; // out of room — later chips are skipped this frame
|
|
184
|
+
const color = this._colorOf(def, pal, si);
|
|
185
|
+
const value = def.op === 'percent'
|
|
186
|
+
? `${last.pct >= 0 ? '+' : ''}${last.pct.toFixed(2)}%`
|
|
187
|
+
: fmtV(last.raw);
|
|
188
|
+
const text = `${def.label} ${value}`;
|
|
189
|
+
ctx.fillStyle = color;
|
|
190
|
+
ctx.fillRect(x, y - 2, 6, 6);
|
|
191
|
+
ctx.strokeStyle = pal.bg;
|
|
192
|
+
ctx.lineWidth = 3;
|
|
193
|
+
ctx.strokeText(text, x + 10, y);
|
|
194
|
+
ctx.fillStyle = color;
|
|
195
|
+
ctx.fillText(text, x + 10, y);
|
|
196
|
+
x += 16 + ctx.measureText(text).width;
|
|
197
|
+
}
|
|
198
|
+
ctx.restore();
|
|
199
|
+
}
|
|
200
|
+
}
|
package/core.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wickchart-compare — pure compare model: normalization, time alignment and
|
|
3
|
+
* rebasing math. No DOM, no canvas; everything is unit-testable plain data
|
|
4
|
+
* in / data out.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const MAX_SERIES = 6;
|
|
8
|
+
|
|
9
|
+
/** Fill/stroke tints cycled per series — read on dark and light themes. */
|
|
10
|
+
export const DEFAULT_COLORS = ['#f0b90b', '#a78bfa', '#16c784', '#22d3ee', '#ea3943', '#4c8dff'];
|
|
11
|
+
|
|
12
|
+
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
13
|
+
const OPS = new Set(['ratio', 'diff']);
|
|
14
|
+
const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
15
|
+
|
|
16
|
+
const toMs = (t) => (t < 1e12 ? t * 1000 : t);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Close/value pairs [[ms, v], …] sorted by time; null when nothing valid.
|
|
20
|
+
* Accepts OHLC bars ({ time, close }) and plain series ({ time, value }).
|
|
21
|
+
*/
|
|
22
|
+
function closes(bars) {
|
|
23
|
+
if (!Array.isArray(bars) || !bars.length) return null;
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const b of bars) {
|
|
26
|
+
if (!b || typeof b !== 'object') continue;
|
|
27
|
+
const t = Number(b.time);
|
|
28
|
+
const v = Number(b.close != null ? b.close : b.value);
|
|
29
|
+
if (!isNum(t) || !isNum(v)) continue;
|
|
30
|
+
out.push([toMs(t), v]);
|
|
31
|
+
}
|
|
32
|
+
if (!out.length) return null;
|
|
33
|
+
out.sort((x, y) => x[0] - y[0]);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Last sample at or before t (binary search); null when t precedes the first. */
|
|
38
|
+
export function sampleAt(samples, t) {
|
|
39
|
+
let lo = 0;
|
|
40
|
+
let hi = samples.length - 1;
|
|
41
|
+
if (t < samples[0][0]) return null;
|
|
42
|
+
while (lo < hi) {
|
|
43
|
+
const mid = (lo + hi + 1) >> 1;
|
|
44
|
+
if (samples[mid][0] <= t) lo = mid;
|
|
45
|
+
else hi = mid - 1;
|
|
46
|
+
}
|
|
47
|
+
return samples[lo][1];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Align b onto a's timestamps: ratio (a/b) or diff (a−b) per shared time. */
|
|
51
|
+
function mergeOp(a, b, op) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const [t, va] of a) {
|
|
54
|
+
const vb = sampleAt(b, t);
|
|
55
|
+
if (vb == null || vb === 0) continue; // no denominator before b starts
|
|
56
|
+
out.push([t, op === 'ratio' ? va / vb : va - vb]);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate + clamp a list of raw compare entries. Invalid entries are
|
|
63
|
+
* dropped (never throw) — same contract as normalizeDrawings /
|
|
64
|
+
* normalizeSessions in the other plugins.
|
|
65
|
+
*
|
|
66
|
+
* Entry shape:
|
|
67
|
+
* { label, data, color?, width? } → percent line (close series)
|
|
68
|
+
* { label, a, b, op: 'ratio'|'diff', … } → derived line
|
|
69
|
+
* color: hex or 'up'/'down'/'accent' (resolved against the palette at draw).
|
|
70
|
+
* An invalid `op` falls through to the percent path.
|
|
71
|
+
*/
|
|
72
|
+
export function normalizeSeries(list) {
|
|
73
|
+
if (!Array.isArray(list)) return [];
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const raw of list) {
|
|
76
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
77
|
+
if (out.length >= MAX_SERIES) break;
|
|
78
|
+
const label = typeof raw.label === 'string' ? raw.label.trim().slice(0, 24) : '';
|
|
79
|
+
if (!label) continue;
|
|
80
|
+
const op = OPS.has(raw.op) ? raw.op : null;
|
|
81
|
+
let samples = null;
|
|
82
|
+
if (op) samples = mergeOp(closes(raw.a), closes(raw.b), op);
|
|
83
|
+
else samples = closes(raw.data);
|
|
84
|
+
if (!samples || !samples.length) continue;
|
|
85
|
+
const color = typeof raw.color === 'string' && (['up', 'down', 'accent'].includes(raw.color) || HEX.test(raw.color))
|
|
86
|
+
? raw.color
|
|
87
|
+
: null;
|
|
88
|
+
const width = isNum(raw.width) ? Math.min(4, Math.max(1, raw.width)) : null;
|
|
89
|
+
out.push({ label, op: op || 'percent', samples, color, width });
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** [firstIndex, lastIndex] of data (sorted by time) inside [t0, t1]. */
|
|
95
|
+
export function timeWindow(data, t0, t1) {
|
|
96
|
+
const n = data.length;
|
|
97
|
+
if (!n || t1 < data[0].time || t0 > data[n - 1].time) return null;
|
|
98
|
+
let lo = 0;
|
|
99
|
+
let hi = n - 1;
|
|
100
|
+
while (lo < hi) {
|
|
101
|
+
const mid = (lo + hi) >> 1;
|
|
102
|
+
if (data[mid].time < t0) lo = mid + 1;
|
|
103
|
+
else hi = mid;
|
|
104
|
+
}
|
|
105
|
+
const i0 = lo;
|
|
106
|
+
if (data[i0].time > t1) return null; // window falls inside a data gap
|
|
107
|
+
hi = n - 1;
|
|
108
|
+
while (lo < hi) {
|
|
109
|
+
const mid = (lo + hi + 1) >> 1;
|
|
110
|
+
if (data[mid].time <= t1) lo = mid;
|
|
111
|
+
else hi = mid - 1;
|
|
112
|
+
}
|
|
113
|
+
return [i0, lo];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Rebased points for one entry over the main chart's bar times.
|
|
118
|
+
* rebase: 'visible' → 0% at the first sampled time in the window;
|
|
119
|
+
* 'first' → 0% at the entry's own first sample;
|
|
120
|
+
* number → 0% sampled at that epoch (falls back to first sample).
|
|
121
|
+
* Returns [{ t, pct, raw } | null] — null where the series has no sample yet.
|
|
122
|
+
*/
|
|
123
|
+
export function computeLine(entry, times, rebase) {
|
|
124
|
+
const { samples } = entry;
|
|
125
|
+
const raws = [];
|
|
126
|
+
for (const t of times) {
|
|
127
|
+
const v = sampleAt(samples, t);
|
|
128
|
+
raws.push(v == null ? null : { t, raw: v });
|
|
129
|
+
}
|
|
130
|
+
let anchor;
|
|
131
|
+
if (typeof rebase === 'number' && isNum(rebase)) {
|
|
132
|
+
const at = sampleAt(samples, rebase);
|
|
133
|
+
anchor = at == null ? samples[0][1] : at;
|
|
134
|
+
} else if (rebase === 'visible') {
|
|
135
|
+
const first = raws.find((r) => r);
|
|
136
|
+
anchor = first ? first.raw : samples[0][1];
|
|
137
|
+
} else {
|
|
138
|
+
anchor = samples[0][1];
|
|
139
|
+
}
|
|
140
|
+
return raws.map((r) => (r && anchor !== 0 ? { t: r.t, pct: (r.raw / anchor - 1) * 100, raw: r.raw } : null));
|
|
141
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wickchart-compare",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Normalized multi-asset compare overlays for wickchart — percent-rebased series and derived ratio/diff lines (BTC/ETH-style) with a live legend. Opt-in plugin, zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "compare.mjs",
|
|
7
|
+
"module": "compare.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./compare.mjs",
|
|
10
|
+
"./core": "./core.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"compare.mjs",
|
|
14
|
+
"core.mjs",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"chart",
|
|
19
|
+
"charting",
|
|
20
|
+
"compare",
|
|
21
|
+
"overlay",
|
|
22
|
+
"spread",
|
|
23
|
+
"ratio",
|
|
24
|
+
"trading",
|
|
25
|
+
"web-component",
|
|
26
|
+
"wickchart",
|
|
27
|
+
"canvas",
|
|
28
|
+
"crypto"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"wickchart": ">=1.4.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"wickchart": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|