wiki-formant 0.36.0 → 0.37.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 +8 -1
- package/dist/chart.d.ts +15 -5
- package/dist/chart.js +87 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -753,9 +753,16 @@ monthly.
|
|
|
753
753
|
|
|
754
754
|
// A loader: asked for 'hour' (24H), '4h' (7D) or 'day' from 0 (every longer
|
|
755
755
|
// range), once each.
|
|
756
|
-
<TimeChart series={load} label="Price" format={formatPrice} range="30d" />
|
|
756
|
+
<TimeChart series={load} label="Price" format={formatPrice} range="30d" candles />
|
|
757
757
|
```
|
|
758
758
|
|
|
759
|
+
A price point may carry `open`, `high` and `low` (its `value` is the close)
|
|
760
|
+
and `volume`. Points that carry the candle get a Line/Candles toggle, opening on
|
|
761
|
+
candles with `candles`; points that carry volume get a volume pane under the
|
|
762
|
+
price, a fifth of the height. Candles take `--chart-up` and `--chart-down` from
|
|
763
|
+
the canvas box, else its `color`. A week of candles opens on its first, closes
|
|
764
|
+
on its last, spans the highest high and lowest low, and sums its volume.
|
|
765
|
+
|
|
759
766
|
`aggregate` is how a week or month is made from its days: `last` for a price,
|
|
760
767
|
`mean` for a count, so the week in progress keeps its level instead of reading
|
|
761
768
|
as a collapse. A single range draws no range buttons and shows every point.
|
package/dist/chart.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* One value at a time in Unix seconds, UTC. A price may carry the rest of its
|
|
3
|
+
* candle, `value` being the close, and the volume traded in it.
|
|
4
|
+
*/
|
|
2
5
|
export type ChartPoint = {
|
|
3
6
|
time: number;
|
|
4
7
|
value: number;
|
|
8
|
+
open?: number;
|
|
9
|
+
high?: number;
|
|
10
|
+
low?: number;
|
|
11
|
+
volume?: number;
|
|
5
12
|
};
|
|
6
13
|
export type ChartRange = '24h' | '7d' | '30d' | '90d' | '1y' | 'all';
|
|
7
14
|
/** What a loader is asked for: hours for 24H, four hours for 7D, days for every longer range. */
|
|
@@ -19,8 +26,9 @@ export declare function chartSteps(span: number): {
|
|
|
19
26
|
};
|
|
20
27
|
/**
|
|
21
28
|
* Points, oldest first, one per step. A price keeps the last value in each
|
|
22
|
-
* step
|
|
23
|
-
*
|
|
29
|
+
* step, as a candle does its close: the first open, the highest high, the
|
|
30
|
+
* lowest low. A count takes the mean of its days, so the week in progress does
|
|
31
|
+
* not read as a collapse. Volume is summed.
|
|
24
32
|
*/
|
|
25
33
|
export declare function bucketChart(points: readonly ChartPoint[], step: ChartStep, aggregate: 'last' | 'mean'): ChartPoint[];
|
|
26
34
|
/** Compact from a thousand up, four significant figures below. */
|
|
@@ -40,13 +48,15 @@ export interface TimeChartProps {
|
|
|
40
48
|
fromZero?: boolean;
|
|
41
49
|
/** The value axis and crosshair label. Read on every draw, so it need not be stable. */
|
|
42
50
|
format?: (n: number) => string;
|
|
51
|
+
/** Open on candles, where the points carry them. A Line/Candles toggle shows either way. */
|
|
52
|
+
candles?: boolean;
|
|
43
53
|
/** In pixels. Otherwise the stylesheet's `--chart-height`, else 15rem. */
|
|
44
54
|
height?: number;
|
|
45
55
|
className?: string;
|
|
46
56
|
}
|
|
47
57
|
/**
|
|
48
|
-
* A time series with its range and
|
|
58
|
+
* A time series with its range, step and style buttons. The buttons are
|
|
49
59
|
* `.chart-controls button[aria-pressed]`, and a site's stylesheet gives them
|
|
50
60
|
* and the box their look; the line and fill are the canvas box's `color`.
|
|
51
61
|
*/
|
|
52
|
-
export declare function TimeChart({ series, label, ranges, range: initial, aggregate, fromZero, format, height, className, }: TimeChartProps): import("react").JSX.Element;
|
|
62
|
+
export declare function TimeChart({ series, label, ranges, range: initial, aggregate, fromZero, format, candles: initialCandles, height, className, }: TimeChartProps): import("react").JSX.Element;
|
package/dist/chart.js
CHANGED
|
@@ -3,8 +3,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// chart.tsx — one time series as an area chart: the token, activity and visitor
|
|
4
4
|
// charts on every site. Ranges run from 24 hours to all time, and a long range
|
|
5
5
|
// steps by day, week or month, so three years do not draw as a thousand points.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// A price that carries its open, high and low can be drawn as candles, and one
|
|
7
|
+
// that carries volume gets a volume pane under it. `lightweight-charts` is
|
|
8
|
+
// loaded on mount, so the page paints without it. Every colour is the canvas
|
|
9
|
+
// box's own `color`, its axis `--chart-axis`, and a candle `--chart-up` or
|
|
10
|
+
// `--chart-down`, each if set.
|
|
8
11
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
9
12
|
const DAY = 86_400;
|
|
10
13
|
const RANGE = {
|
|
@@ -41,23 +44,35 @@ function bucketStart(time, step) {
|
|
|
41
44
|
}
|
|
42
45
|
/**
|
|
43
46
|
* Points, oldest first, one per step. A price keeps the last value in each
|
|
44
|
-
* step
|
|
45
|
-
*
|
|
47
|
+
* step, as a candle does its close: the first open, the highest high, the
|
|
48
|
+
* lowest low. A count takes the mean of its days, so the week in progress does
|
|
49
|
+
* not read as a collapse. Volume is summed.
|
|
46
50
|
*/
|
|
47
51
|
export function bucketChart(points, step, aggregate) {
|
|
48
52
|
const out = [];
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
let n = 0;
|
|
54
|
+
for (const p of points) {
|
|
55
|
+
const start = bucketStart(p.time, step);
|
|
51
56
|
const last = out.at(-1);
|
|
52
|
-
if (last?.time !== start)
|
|
53
|
-
out.push({ time: start
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
if (last?.time !== start) {
|
|
58
|
+
out.push({ ...p, time: start });
|
|
59
|
+
n = 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (aggregate === 'last')
|
|
63
|
+
last.value = p.value;
|
|
56
64
|
else
|
|
57
|
-
last.value += (value - last.value) / ++
|
|
65
|
+
last.value += (p.value - last.value) / ++n;
|
|
66
|
+
if (last.high !== undefined && p.high !== undefined)
|
|
67
|
+
last.high = Math.max(last.high, p.high);
|
|
68
|
+
if (last.low !== undefined && p.low !== undefined)
|
|
69
|
+
last.low = Math.min(last.low, p.low);
|
|
70
|
+
if (p.volume !== undefined)
|
|
71
|
+
last.volume = (last.volume ?? 0) + p.volume;
|
|
58
72
|
}
|
|
59
|
-
return out
|
|
73
|
+
return out;
|
|
60
74
|
}
|
|
75
|
+
const isCandle = (p) => p.open !== undefined && p.high !== undefined && p.low !== undefined;
|
|
61
76
|
const compact = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 });
|
|
62
77
|
const precise = new Intl.NumberFormat('en', { maximumSignificantDigits: 4 });
|
|
63
78
|
/** Compact from a thousand up, four significant figures below. */
|
|
@@ -81,11 +96,11 @@ const fromZeroScale = (base) => {
|
|
|
81
96
|
return info;
|
|
82
97
|
};
|
|
83
98
|
/**
|
|
84
|
-
* A time series with its range and
|
|
99
|
+
* A time series with its range, step and style buttons. The buttons are
|
|
85
100
|
* `.chart-controls button[aria-pressed]`, and a site's stylesheet gives them
|
|
86
101
|
* and the box their look; the line and fill are the canvas box's `color`.
|
|
87
102
|
*/
|
|
88
|
-
export function TimeChart({ series, label, ranges = typeof series === 'function' ? CHART_RANGES : DAILY_RANGES, range: initial = ranges.includes('30d') ? '30d' : ranges[0], aggregate = 'last', fromZero = false, format = formatChartValue, height, className, }) {
|
|
103
|
+
export function TimeChart({ series, label, ranges = typeof series === 'function' ? CHART_RANGES : DAILY_RANGES, range: initial = ranges.includes('30d') ? '30d' : ranges[0], aggregate = 'last', fromZero = false, format = formatChartValue, candles: initialCandles = false, height, className, }) {
|
|
89
104
|
const box = useRef(null);
|
|
90
105
|
const chart = useRef(null);
|
|
91
106
|
const formatRef = useRef(format);
|
|
@@ -93,6 +108,7 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
93
108
|
const [ready, setReady] = useState(false);
|
|
94
109
|
const [range, setRange] = useState(initial);
|
|
95
110
|
const [step, setStep] = useState(null);
|
|
111
|
+
const [candles, setCandles] = useState(initialCandles);
|
|
96
112
|
const [loaded, setLoaded] = useState(null);
|
|
97
113
|
const resolution = RANGE[range].resolution;
|
|
98
114
|
useEffect(() => {
|
|
@@ -100,12 +116,15 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
100
116
|
if (!el)
|
|
101
117
|
return;
|
|
102
118
|
let disposed = false;
|
|
103
|
-
import('lightweight-charts').then(({ createChart, AreaSeries, ColorType, CrosshairMode, LineStyle, LineType }) => {
|
|
119
|
+
import('lightweight-charts').then(({ createChart, AreaSeries, CandlestickSeries, HistogramSeries, ColorType, CrosshairMode, LineStyle, LineType }) => {
|
|
104
120
|
if (disposed)
|
|
105
121
|
return;
|
|
106
122
|
const style = getComputedStyle(el);
|
|
107
123
|
const axis = style.getPropertyValue('--chart-axis').trim();
|
|
108
124
|
const ink = rgba(style.color);
|
|
125
|
+
const up = rgba(style.getPropertyValue('--chart-up').trim() || style.color);
|
|
126
|
+
const down = rgba(style.getPropertyValue('--chart-down').trim() || style.color, style.getPropertyValue('--chart-down').trim() ? 1 : 0.45);
|
|
127
|
+
const priceFormat = { type: 'custom', formatter: (n) => formatRef.current(n), minMove: 1e-12 };
|
|
109
128
|
const guide = { color: rgba(style.color, 0.4), width: 1, style: LineStyle.Dotted, labelBackgroundColor: ink };
|
|
110
129
|
const api = createChart(el, {
|
|
111
130
|
autoSize: true,
|
|
@@ -130,13 +149,30 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
130
149
|
lineType: LineType.Curved,
|
|
131
150
|
crosshairMarkerBackgroundColor: ink,
|
|
132
151
|
crosshairMarkerBorderColor: ink,
|
|
133
|
-
priceFormat
|
|
152
|
+
priceFormat,
|
|
134
153
|
...(fromZero && { autoscaleInfoProvider: fromZeroScale }),
|
|
135
154
|
});
|
|
155
|
+
const candle = api.addSeries(CandlestickSeries, {
|
|
156
|
+
visible: false,
|
|
157
|
+
upColor: up,
|
|
158
|
+
downColor: down,
|
|
159
|
+
borderUpColor: up,
|
|
160
|
+
borderDownColor: down,
|
|
161
|
+
wickUpColor: up,
|
|
162
|
+
wickDownColor: down,
|
|
163
|
+
priceFormat,
|
|
164
|
+
});
|
|
136
165
|
// The chart cannot be scrolled or zoomed, so every size it takes shows every point,
|
|
137
166
|
// the first included: data drawn before autoSize has measured the box fits nothing.
|
|
138
167
|
api.timeScale().subscribeSizeChange(() => api.timeScale().fitContent());
|
|
139
|
-
|
|
168
|
+
// Volume takes a pane of its own under the price, a fifth of the height.
|
|
169
|
+
const addVolume = () => {
|
|
170
|
+
const volume = api.addSeries(HistogramSeries, { priceFormat: { type: 'volume' }, priceLineVisible: false, lastValueVisible: false }, 1);
|
|
171
|
+
api.panes()[0]?.setStretchFactor(4);
|
|
172
|
+
api.panes()[1]?.setStretchFactor(1);
|
|
173
|
+
return volume;
|
|
174
|
+
};
|
|
175
|
+
chart.current = { api, area, candle, volume: null, addVolume, up, down };
|
|
140
176
|
setReady(true);
|
|
141
177
|
});
|
|
142
178
|
return () => {
|
|
@@ -175,20 +211,46 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
175
211
|
const end = points.at(-1).time;
|
|
176
212
|
const span = RANGE[range].span;
|
|
177
213
|
const windowed = span === Infinity || ranges.length < 2 ? points : points.filter(p => p.time > Date.now() / 1000 - span);
|
|
214
|
+
const drawn = (shown, steps, step) => ({
|
|
215
|
+
shown,
|
|
216
|
+
steps,
|
|
217
|
+
step,
|
|
218
|
+
ohlc: aggregate === 'last' && shown.length > 0 && shown.every(isCandle),
|
|
219
|
+
volume: shown.some(p => p.volume !== undefined),
|
|
220
|
+
});
|
|
178
221
|
if (resolution !== 'day' || !windowed.length)
|
|
179
|
-
return
|
|
222
|
+
return drawn(windowed, [], null);
|
|
180
223
|
const { steps, fallback } = chartSteps(end - windowed[0].time);
|
|
181
224
|
const chosen = step && steps.includes(step) ? step : fallback;
|
|
182
|
-
return
|
|
225
|
+
return drawn(chosen === 'day' ? windowed : bucketChart(windowed, chosen, aggregate), steps, chosen);
|
|
183
226
|
}, [points, range, ranges.length, resolution, step, aggregate]);
|
|
184
227
|
// While a resolution loads, the last one stays under the skeleton; an empty or failed one clears it.
|
|
185
228
|
useEffect(() => {
|
|
186
|
-
|
|
229
|
+
const c = chart.current;
|
|
230
|
+
if (!ready || !c || !source)
|
|
187
231
|
return;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
232
|
+
const shown = view?.shown ?? [];
|
|
233
|
+
const asCandles = candles && !!view?.ohlc;
|
|
234
|
+
const time = (p) => p.time;
|
|
235
|
+
c.api.applyOptions({ timeScale: { timeVisible: source.resolution !== 'day' } });
|
|
236
|
+
c.area.applyOptions({ visible: !asCandles });
|
|
237
|
+
c.candle.applyOptions({ visible: asCandles });
|
|
238
|
+
c.area.setData(asCandles ? [] : shown.map(p => ({ time: time(p), value: p.value })));
|
|
239
|
+
c.candle.setData(asCandles ? shown.map(p => ({ time: time(p), open: p.open, high: p.high, low: p.low, close: p.value })) : []);
|
|
240
|
+
if (view?.volume && !c.volume)
|
|
241
|
+
c.volume = c.addVolume();
|
|
242
|
+
else if (!view?.volume && c.volume) {
|
|
243
|
+
c.api.removeSeries(c.volume);
|
|
244
|
+
c.volume = null;
|
|
245
|
+
if (c.api.panes().length > 1)
|
|
246
|
+
c.api.removePane(1);
|
|
247
|
+
}
|
|
248
|
+
c.volume?.setData(shown.map((p, i) => {
|
|
249
|
+
const rising = p.value >= (p.open ?? shown[i - 1]?.value ?? p.value);
|
|
250
|
+
return { time: time(p), value: p.volume ?? 0, color: (rising ? c.up : c.down).replace(/[\d.]+\)$/, '0.5)') };
|
|
251
|
+
}));
|
|
252
|
+
c.api.timeScale().fitContent();
|
|
253
|
+
}, [ready, source, view, candles]);
|
|
192
254
|
const status = source && 'error' in source ? source.error : points && !view?.shown.length ? 'No data in this range' : null;
|
|
193
|
-
return (_jsxs("figure", { className: className ? `chart ${className}` : 'chart', children: [_jsx("div", { ref: box, className: "chart-canvas", role: "img", "aria-label": label, style: height ? { height } : undefined }), (!ready || (!points && !status)) && _jsx("div", { className: "chart-status skeleton", "aria-hidden": true }), status && _jsx("p", { className: "chart-status", children: status }), (ranges.length > 1 || (view && view.steps.length > 1)) && (_jsxs("div", { className: "chart-controls", children: [ranges.length > 1 && (_jsx("div", { role: "group", "aria-label": "Range", children: ranges.map(r => (_jsx("button", { type: "button", "aria-pressed": r === range, onClick: () => { setRange(r); setStep(null); }, children: RANGE[r].label }, r))) })), view && view.steps.length > 1 && (_jsx("div", { role: "group", "aria-label": "Step", children: view.steps.map(s => (_jsx("button", { type: "button", "aria-pressed": s === view.step, onClick: () => setStep(s), title: `One point per ${s}`, children: STEP[s].label }, s))) }))] }))] }));
|
|
255
|
+
return (_jsxs("figure", { className: className ? `chart ${className}` : 'chart', children: [_jsx("div", { ref: box, className: "chart-canvas", role: "img", "aria-label": label, style: height ? { height } : undefined }), (!ready || (!points && !status)) && _jsx("div", { className: "chart-status skeleton", "aria-hidden": true }), status && _jsx("p", { className: "chart-status", children: status }), (ranges.length > 1 || (view && (view.steps.length > 1 || view.ohlc))) && (_jsxs("div", { className: "chart-controls", children: [ranges.length > 1 && (_jsx("div", { role: "group", "aria-label": "Range", children: ranges.map(r => (_jsx("button", { type: "button", "aria-pressed": r === range, onClick: () => { setRange(r); setStep(null); }, children: RANGE[r].label }, r))) })), view && view.steps.length > 1 && (_jsx("div", { role: "group", "aria-label": "Step", children: view.steps.map(s => (_jsx("button", { type: "button", "aria-pressed": s === view.step, onClick: () => setStep(s), title: `One point per ${s}`, children: STEP[s].label }, s))) })), view?.ohlc && (_jsxs("div", { role: "group", "aria-label": "Style", children: [_jsx("button", { type: "button", "aria-pressed": !candles, onClick: () => setCandles(false), children: "Line" }), _jsx("button", { type: "button", "aria-pressed": candles, onClick: () => setCandles(true), children: "Candles" })] }))] }))] }));
|
|
194
256
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|