wickchart 0.4.0 → 1.2.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,191 @@
1
+ /* ==========================================================================
2
+ * react-core — React bindings for <wick-chart> (pure logic, no side effects).
3
+ *
4
+ * import { WickChart, useWickChart } from 'wickchart/react';
5
+ *
6
+ * <WickChart type="candles" indicators="sma:20 volume" volshading
7
+ * data={bars} onRange={fn} onAlert={fn} style={{ height: 420 }} />
8
+ *
9
+ * Props map onto the element 1:1:
10
+ * - string/number/boolean props become attributes ("indicators", "type", …)
11
+ * - `data` assigns the bar array (pass a fresh array to trigger an update)
12
+ * - `overlays` assigns server-side zones & levels via setOverlays()
13
+ * - `onRange` / `onAlert` / … subscribe to the matching `wick:range`,
14
+ * `wick:alert`, … events and unsubscribe on unmount; an `events`
15
+ * object ({ range: fn }) works too
16
+ * - className/style/id/… are passed through to React as usual
17
+ *
18
+ * This module never touches the DOM at import time and does NOT define the
19
+ * element — the package entry (src/react.js) imports <wick-chart> for its
20
+ * side effect, which keeps this file importable in tests and on the server.
21
+ * Requires React >= 16.8 (hooks) — see peerDependencies in package.json.
22
+ * ========================================================================== */
23
+ import {
24
+ useCallback,
25
+ useEffect,
26
+ useLayoutEffect,
27
+ useRef,
28
+ useState,
29
+ createElement,
30
+ forwardRef,
31
+ } from 'react';
32
+
33
+ /** useEffect on the server, useLayoutEffect in the browser (avoids the SSR warning). */
34
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
35
+
36
+ /**
37
+ * Map an event shorthand to the dispatched event name.
38
+ * "range" / "wick:range" → "wick:range" (the 0.x "hab:" prefix is no longer emitted).
39
+ * @param {string} name
40
+ * @returns {string}
41
+ */
42
+ export const toWickEventName = (name) =>
43
+ String(name).startsWith('wick:') ? String(name) : 'wick:' + String(name);
44
+
45
+ /**
46
+ * camelCase prop name → kebab-case attribute name ("volShading" → "vol-shading").
47
+ * @param {string} key
48
+ * @returns {string}
49
+ */
50
+ export const toAttrName = (key) => key.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
51
+
52
+ const DOM_PROPS = new Set(['className', 'class', 'style', 'id', 'title', 'role', 'key', 'ref']);
53
+
54
+ /**
55
+ * Split React props into chart attrs / event handlers / DOM passthrough /
56
+ * the data array / the overlays array.
57
+ * @param {object} props
58
+ * @returns {{ attrs: object, events: Record<string, Function>, dom: object, data: any, overlays: any }}
59
+ */
60
+ export function splitChartProps(props) {
61
+ const attrs = {};
62
+ const events = {};
63
+ const dom = {};
64
+ let data;
65
+ let overlays;
66
+ for (const [key, val] of Object.entries(props || {})) {
67
+ if (key === 'data') {
68
+ data = val;
69
+ } else if (key === 'overlays') {
70
+ overlays = val;
71
+ } else if (key === 'events') {
72
+ for (const [name, fn] of Object.entries(val || {})) events[name] = fn;
73
+ } else if (/^on[A-Z]/.test(key)) {
74
+ events[key.slice(2, 3).toLowerCase() + key.slice(3)] = val;
75
+ } else if (
76
+ DOM_PROPS.has(key) ||
77
+ key.startsWith('aria-') ||
78
+ key.startsWith('data-') ||
79
+ typeof val === 'object'
80
+ ) {
81
+ dom[key] = val;
82
+ } else {
83
+ attrs[key] = val;
84
+ }
85
+ }
86
+ return { attrs, events, dom, data, overlays };
87
+ }
88
+
89
+ /**
90
+ * Apply split props to a chart element. Every write is guarded so re-running
91
+ * with identical values is a no-op (attributes compared as strings, `data`
92
+ * and `overlays` compared by identity — passing a fresh array is what
93
+ * triggers a redraw).
94
+ * @param {HTMLElement} el
95
+ * @param {{ attrs?: object, data?: any, overlays?: any }} split
96
+ */
97
+ export function applyChartProps(el, split) {
98
+ if (!el) return;
99
+ for (const [key, val] of Object.entries(split.attrs || {})) {
100
+ const name = toAttrName(key);
101
+ if (val == null || val === false) {
102
+ if (el.hasAttribute(name)) el.removeAttribute(name);
103
+ } else {
104
+ const str = val === true ? '' : String(val);
105
+ if (el.getAttribute(name) !== str) el.setAttribute(name, str);
106
+ }
107
+ }
108
+ // The element exposes `data` as a getter-only accessor — feed it through
109
+ // setData() (guarded by identity so unchanged arrays never re-ingest).
110
+ if (split.data != null && el.data !== split.data) {
111
+ if (typeof el.setData === 'function') el.setData(split.data);
112
+ else el.data = split.data;
113
+ }
114
+ // Overlays follow the same rule through setOverlays(); identity is tracked
115
+ // on the element because the getter returns copies.
116
+ if (split.overlays != null && el.__wickOverlaysRef !== split.overlays) {
117
+ el.__wickOverlaysRef = split.overlays;
118
+ if (typeof el.setOverlays === 'function') el.setOverlays(split.overlays);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Full control hook: renders nothing — attach the returned ref to your own
124
+ * <wick-chart> element and pass the same options you would give the component.
125
+ *
126
+ * const { ref, chart } = useWickChart({ data: bars, indicators: 'sma:20', onRange });
127
+ * return <wick-chart ref={ref} style={{ height: 420 }} />;
128
+ * // chart.getDataWindow() etc. once mounted
129
+ *
130
+ * @param {object} [options] any <wick-chart> attribute, plus `data`, `events`
131
+ * and `onXxx`-style handlers (see splitChartProps).
132
+ * @returns {{ ref: (node: any) => void, chart: any }} `chart` is the element
133
+ * instance (or null before mount) for the imperative API.
134
+ */
135
+ export function useWickChart(options = {}) {
136
+ const split = splitChartProps(options);
137
+ const splitRef = useRef(split);
138
+ splitRef.current = split;
139
+
140
+ const [chart, setChart] = useState(null);
141
+ const ref = useCallback((node) => { setChart(node); }, []);
142
+
143
+ // Apply attrs/data on every commit — guarded writes make this cheap, and it
144
+ // catches both prop changes and the element mounting after the first render.
145
+ useIsomorphicLayoutEffect(() => {
146
+ if (chart) applyChartProps(chart, splitRef.current);
147
+ });
148
+
149
+ // Subscribe to wick:* events through stable trampolines so subscriptions
150
+ // only churn when the set of event names changes — not on every render.
151
+ const eventsRef = useRef(split.events);
152
+ eventsRef.current = split.events;
153
+ const names = Object.keys(split.events).join(' ');
154
+ useEffect(() => {
155
+ if (!chart || !names) return undefined;
156
+ const offs = names.split(' ').map((key) => {
157
+ const type = toWickEventName(key);
158
+ const trampoline = (ev) => {
159
+ const fn = eventsRef.current[key];
160
+ if (fn) fn(ev);
161
+ };
162
+ chart.addEventListener(type, trampoline);
163
+ return () => chart.removeEventListener(type, trampoline);
164
+ });
165
+ return () => { for (const off of offs) off(); };
166
+ }, [chart, names]);
167
+
168
+ return { ref, chart };
169
+ }
170
+
171
+ /**
172
+ * Drop-in React component for <wick-chart>. Attributes ride through
173
+ * createElement (so they exist at first paint and in SSR output) while the
174
+ * hook keeps them in sync on updates; `data` and event handlers never touch
175
+ * React's prop pipeline. Works the same on React 16.8 → 19.
176
+ */
177
+ export const WickChart = forwardRef(function WickChart(props, fwdRef) {
178
+ const { ref } = useWickChart(props);
179
+ const { attrs, dom } = splitChartProps(props);
180
+ const setEl = useCallback(
181
+ (node) => {
182
+ ref(node);
183
+ if (typeof fwdRef === 'function') fwdRef(node);
184
+ else if (fwdRef) fwdRef.current = node;
185
+ },
186
+ [ref, fwdRef]
187
+ );
188
+ return createElement('wick-chart', { ...dom, ...attrs, ref: setEl });
189
+ });
190
+
191
+ export default WickChart;
package/src/react.js ADDED
@@ -0,0 +1,21 @@
1
+ /* ==========================================================================
2
+ * wickchart/react — React bindings for <wick-chart> (package entry).
3
+ *
4
+ * import { WickChart, useWickChart } from 'wickchart/react';
5
+ *
6
+ * Importing this module also defines the <wick-chart> custom element (same
7
+ * side-effect contract as wickchart/feed). All binding logic lives in
8
+ * ./react-core.js; SSR-safe on the server (the define call is guarded).
9
+ * ========================================================================== */
10
+ import './wick-chart.js';
11
+
12
+ export {
13
+ WickChart,
14
+ useWickChart,
15
+ splitChartProps,
16
+ applyChartProps,
17
+ toWickEventName,
18
+ toAttrName,
19
+ } from './react-core.js';
20
+
21
+ export { default } from './react-core.js';