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.
Files changed (3) hide show
  1. package/README.md +49 -0
  2. package/layouts.mjs +194 -0
  3. 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
+ }