wickchart-layouts 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 +49 -0
- package/layouts.mjs +194 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# wickchart-layouts
|
|
2
|
+
|
|
3
|
+
Named workspace persistence for [wickchart](https://github.com/benyblack/wickchart),
|
|
4
|
+
as an opt-in plugin — save and restore whole chart setups by name. Zero
|
|
5
|
+
dependencies, zero core changes: everything rides the core's public
|
|
6
|
+
`getState()` / `setState()` plus the drawing list when wickchart-draw is
|
|
7
|
+
attached.
|
|
8
|
+
|
|
9
|
+
A layout captures **type, theme, log scale, toggles (stats/profile/
|
|
10
|
+
annotations/vol-shading), indicators, view range, positions and alerts** —
|
|
11
|
+
and drawings when a `drawings` handle is passed.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
npm install wickchart wickchart-layouts // the plugin is a separate package
|
|
15
|
+
|
|
16
|
+
import 'wickchart'; // the chart itself
|
|
17
|
+
import { attachDrawings } from 'wickchart-draw';
|
|
18
|
+
import { attachLayouts } from 'wickchart-layouts';
|
|
19
|
+
|
|
20
|
+
const chart = document.querySelector('wick-chart');
|
|
21
|
+
const draw = attachDrawings(chart);
|
|
22
|
+
const layouts = attachLayouts(chart, {
|
|
23
|
+
key: 'my-desk', // storage key (default 'wickchart-layouts')
|
|
24
|
+
drawings: draw, // optional — include drawings in saved layouts
|
|
25
|
+
cap: 20, // max stored layouts (oldest evicted, default 20)
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
layouts.save('swing'); // capture the current setup
|
|
29
|
+
layouts.load('swing'); // apply it back
|
|
30
|
+
layouts.list(); // → [{ name, at, drawingCount }] newest first
|
|
31
|
+
layouts.rename('swing', 'swing-v2');
|
|
32
|
+
layouts.delete('swing');
|
|
33
|
+
layouts.export(); // → JSON string — share it, store it anywhere
|
|
34
|
+
layouts.import(json); // merge layouts from such a string (replaces
|
|
35
|
+
// same-name entries); returns the count
|
|
36
|
+
|
|
37
|
+
chart.addEventListener('wick:layouts', (e) => {
|
|
38
|
+
console.log(e.detail.action, e.detail.name); // save|load|delete|rename|import
|
|
39
|
+
});
|
|
40
|
+
layouts.detach();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `wick:layouts` fires on every mutation — pair a `load` with
|
|
44
|
+
`wickchart-alerts-plus`'s `sync()` if you also persist alerts, since a
|
|
45
|
+
layout load replaces the chart's alert list.
|
|
46
|
+
- `storage` is injectable; storage failures degrade to an in-memory store
|
|
47
|
+
for the session and never throw. Entries are capped (oldest evicted).
|
|
48
|
+
|
|
49
|
+
Peer dependency: wickchart ≥ 1.4.0.
|
package/layouts.mjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wickchart-layouts — named workspace persistence as a wickchart plugin:
|
|
3
|
+
* save/restore whole chart setups — type, theme, scale, toggles,
|
|
4
|
+
* indicators, view range, positions, alerts (all via the core's public
|
|
5
|
+
* getState/setState) plus the drawing list when wickchart-draw is attached.
|
|
6
|
+
*
|
|
7
|
+
* import { attachLayouts } from 'wickchart-layouts';
|
|
8
|
+
* const layouts = attachLayouts(chart, {
|
|
9
|
+
* key: 'my-trading-desk', // storage key (default 'wickchart-layouts')
|
|
10
|
+
* drawings: drawPlugin, // optional wickchart-draw DrawLayer
|
|
11
|
+
* });
|
|
12
|
+
* layouts.save('swing'); // capture the current setup under a name
|
|
13
|
+
* layouts.load('swing'); // apply it back
|
|
14
|
+
* layouts.list(); // → [{ name, at, drawingCount }] newest first
|
|
15
|
+
* layouts.delete('swing');
|
|
16
|
+
* layouts.rename('swing', 'swing-v2');
|
|
17
|
+
* layouts.export(); // → JSON string (share it, store it anywhere)
|
|
18
|
+
* layouts.import(json); // → number of layouts merged in
|
|
19
|
+
* layouts.detach();
|
|
20
|
+
*
|
|
21
|
+
* Every mutation fires `wick:layouts` on the chart with
|
|
22
|
+
* { action, name } — e.g. pair it with wickchart-alerts-plus's sync() to
|
|
23
|
+
* keep persisted alerts in step after a layout load.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const MAX_LAYOUTS = 20;
|
|
27
|
+
const MAX_NAME = 40;
|
|
28
|
+
|
|
29
|
+
const normName = (n) => (typeof n === 'string' ? n.trim().slice(0, MAX_NAME) : '');
|
|
30
|
+
|
|
31
|
+
export function attachLayouts(chart, opts = {}) {
|
|
32
|
+
return new Layouts(chart, opts);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class Layouts {
|
|
36
|
+
constructor(chart, opts = {}) {
|
|
37
|
+
if (!chart || typeof chart.getState !== 'function' || typeof chart.setState !== 'function') {
|
|
38
|
+
throw new TypeError('attachLayouts(chart): the chart element is required');
|
|
39
|
+
}
|
|
40
|
+
this._chart = chart;
|
|
41
|
+
this._drawings = opts.drawings || null; // a wickchart-draw DrawLayer, or null
|
|
42
|
+
this._storage = opts.storage != null ? opts.storage : globalThis.localStorage;
|
|
43
|
+
this._key = typeof opts.key === 'string' && opts.key ? opts.key : 'wickchart-layouts';
|
|
44
|
+
this._cap = Number.isInteger(opts.cap) && opts.cap > 0 ? Math.min(100, opts.cap) : MAX_LAYOUTS;
|
|
45
|
+
this._mem = null; // in-memory fallback when storage is unavailable
|
|
46
|
+
this._seq = 0; // tiebreaker for same-millisecond saves (persisted)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ---------------- public API ---------------- */
|
|
50
|
+
|
|
51
|
+
/** Capture the current setup under `name`. Replaces an existing entry. */
|
|
52
|
+
save(name) {
|
|
53
|
+
const n = normName(name);
|
|
54
|
+
if (!n || !this._chart) return false;
|
|
55
|
+
const entry = { name: n, at: Date.now(), seq: ++this._seq, state: this._chart.getState() };
|
|
56
|
+
if (this._drawings && typeof this._drawings.getDrawings === 'function') {
|
|
57
|
+
entry.drawings = this._drawings.getDrawings();
|
|
58
|
+
}
|
|
59
|
+
const list = this._read().filter((e) => e.name !== n);
|
|
60
|
+
list.push(entry);
|
|
61
|
+
this._write(this._evict(list));
|
|
62
|
+
this._fire('save', n);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Apply a saved setup to the chart. False when the name is unknown. */
|
|
67
|
+
load(name) {
|
|
68
|
+
const n = normName(name);
|
|
69
|
+
const entry = n ? this._read().find((e) => e.name === n) : null;
|
|
70
|
+
if (!entry || !this._chart) return false;
|
|
71
|
+
this._chart.setState(entry.state);
|
|
72
|
+
if (this._drawings && typeof this._drawings.setDrawings === 'function' && entry.drawings) {
|
|
73
|
+
this._drawings.setDrawings(entry.drawings);
|
|
74
|
+
}
|
|
75
|
+
this._fire('load', n);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Saved layouts, newest first: [{ name, at, drawingCount }]. */
|
|
80
|
+
list() {
|
|
81
|
+
return this._read()
|
|
82
|
+
.slice()
|
|
83
|
+
.sort((a, b) => b.at - a.at || (b.seq || 0) - (a.seq || 0))
|
|
84
|
+
.map((e) => ({
|
|
85
|
+
name: e.name,
|
|
86
|
+
at: e.at,
|
|
87
|
+
drawingCount: Array.isArray(e.drawings) ? e.drawings.length : null,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
delete(name) {
|
|
92
|
+
const n = normName(name);
|
|
93
|
+
const list = this._read();
|
|
94
|
+
const next = list.filter((e) => e.name !== n);
|
|
95
|
+
if (next.length === list.length) return false;
|
|
96
|
+
this._write(next);
|
|
97
|
+
this._fire('delete', n);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
rename(from, to) {
|
|
102
|
+
const f = normName(from);
|
|
103
|
+
const t = normName(to);
|
|
104
|
+
if (!f || !t) return false;
|
|
105
|
+
const list = this._read();
|
|
106
|
+
const entry = list.find((e) => e.name === f);
|
|
107
|
+
if (!entry || list.some((e) => e.name === t)) return false;
|
|
108
|
+
entry.name = t;
|
|
109
|
+
this._write(list);
|
|
110
|
+
this._fire('rename', t);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Serialize every saved layout: a portable JSON string. */
|
|
115
|
+
export() {
|
|
116
|
+
return JSON.stringify({ v: 1, layouts: this._read() });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Merge layouts from a string produced by export() (or an array of
|
|
121
|
+
* entries). Same-name entries are replaced. Returns the count merged.
|
|
122
|
+
*/
|
|
123
|
+
import(json) {
|
|
124
|
+
let raw;
|
|
125
|
+
try {
|
|
126
|
+
raw = typeof json === 'string' ? JSON.parse(json) : json;
|
|
127
|
+
} catch (_) {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
const incoming = Array.isArray(raw) ? raw : raw && Array.isArray(raw.layouts) ? raw.layouts : [];
|
|
131
|
+
const merged = this._read();
|
|
132
|
+
let count = 0;
|
|
133
|
+
for (const e of incoming) {
|
|
134
|
+
if (!e || typeof e !== 'object' || !normName(e.name) || !e.state || typeof e.state !== 'object') continue;
|
|
135
|
+
const entry = { name: normName(e.name), at: Number.isFinite(e.at) ? e.at : Date.now(), seq: ++this._seq, state: e.state };
|
|
136
|
+
if (Array.isArray(e.drawings)) entry.drawings = e.drawings;
|
|
137
|
+
const at = merged.findIndex((x) => x.name === entry.name);
|
|
138
|
+
if (at >= 0) merged[at] = entry;
|
|
139
|
+
else merged.push(entry);
|
|
140
|
+
count++;
|
|
141
|
+
}
|
|
142
|
+
if (count) {
|
|
143
|
+
this._write(this._evict(merged));
|
|
144
|
+
this._fire('import', String(count));
|
|
145
|
+
}
|
|
146
|
+
return count;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Nothing to unbind — layouts are pull-based. Clears the chart handle. */
|
|
150
|
+
detach() {
|
|
151
|
+
this._chart = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ---------------- internals ---------------- */
|
|
155
|
+
|
|
156
|
+
_read() {
|
|
157
|
+
if (!this._storage) return this._mem || [];
|
|
158
|
+
try {
|
|
159
|
+
const raw = this._storage.getItem(this._key);
|
|
160
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
161
|
+
return Array.isArray(list)
|
|
162
|
+
? list.filter((e) => e && typeof e === 'object' && typeof e.name === 'string' && e.state)
|
|
163
|
+
: [];
|
|
164
|
+
} catch (_) {
|
|
165
|
+
return this._mem || [];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_write(list) {
|
|
170
|
+
this._mem = list; // always mirrored, so storage loss keeps the session alive
|
|
171
|
+
if (!this._storage) return;
|
|
172
|
+
try {
|
|
173
|
+
this._storage.setItem(this._key, JSON.stringify(list.slice(0, this._cap)));
|
|
174
|
+
} catch (_) {
|
|
175
|
+
/* private mode / quota — best-effort */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Newest kept beyond the cap is a user decision — oldest are dropped. */
|
|
180
|
+
_evict(list) {
|
|
181
|
+
if (list.length <= this._cap) return list;
|
|
182
|
+
return list
|
|
183
|
+
.slice()
|
|
184
|
+
.sort((a, b) => a.at - b.at || (a.seq || 0) - (b.seq || 0))
|
|
185
|
+
.slice(list.length - this._cap);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
_fire(action, name) {
|
|
189
|
+
if (!this._chart || typeof this._chart.dispatchEvent !== 'function') return;
|
|
190
|
+
try {
|
|
191
|
+
this._chart.dispatchEvent(new CustomEvent('wick:layouts', { detail: { action, name } }));
|
|
192
|
+
} catch (_) {}
|
|
193
|
+
}
|
|
194
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wickchart-layouts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Named workspace persistence for wickchart — save/restore whole chart setups (type, indicators, theme, view, drawings, positions, alerts) to localStorage. Opt-in plugin, zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "layouts.mjs",
|
|
7
|
+
"module": "layouts.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./layouts.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"layouts.mjs",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"chart",
|
|
17
|
+
"charting",
|
|
18
|
+
"layouts",
|
|
19
|
+
"workspace",
|
|
20
|
+
"persistence",
|
|
21
|
+
"trading",
|
|
22
|
+
"web-component",
|
|
23
|
+
"wickchart"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"wickchart": ">=1.4.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"wickchart": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|