wiki-formant 0.47.0 → 0.48.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 CHANGED
@@ -820,6 +820,17 @@ straight between closes and a count's is curved.
820
820
  `mean` for a count, so the week in progress keeps its level instead of reading
821
821
  as a collapse. A single range draws no range buttons and shows every point.
822
822
  `bucketChart` and `chartSteps` are exported for a chart drawn elsewhere.
823
+
824
+ `markers` are events, `{ time, label }`, each drawn as an arrow on the last
825
+ point at or before it, so one mid-week lands on its week and two on one point
826
+ share it (`placeMarkers` does the placing). `step` opens on a step where the
827
+ range can make it, and `onView` hears the range and step on mount and on every
828
+ change, for a page that keeps them in its URL:
829
+
830
+ ```tsx
831
+ <TimeChart series={days} label="Transactions" aggregate="mean" fromZero
832
+ markers={[{ time: haltAt, label: 'Halt' }]} step="month" onView={v => keep(v)} />
833
+ ```
823
834
  `base.css` pins the buttons (`.chart-controls button[aria-pressed]`) to the
824
835
  canvas's top-left corner, rules a line between groups and dims a disabled step;
825
836
  the site's stylesheet gives them and the box a look.
package/dist/chart.d.ts CHANGED
@@ -18,6 +18,16 @@ export type ChartResolution = 'hour' | '4h' | 'day';
18
18
  export type ChartStep = 'day' | 'week' | 'month';
19
19
  /** Points by resolution, from `from` (Unix seconds, 0 for all time), oldest first. */
20
20
  export type ChartLoader = (resolution: ChartResolution, from: number) => Promise<readonly ChartPoint[]>;
21
+ /** Something that happened at a time, in Unix seconds, drawn on the point that holds it. */
22
+ export type ChartMarker = {
23
+ time: number;
24
+ label: string;
25
+ };
26
+ /** The range and step a reader has picked, for a page that keeps them in its URL. */
27
+ export type ChartView = {
28
+ range: ChartRange;
29
+ step: ChartStep | null;
30
+ };
21
31
  /** Every range, shortest first. A chart fed daily points offers the ones from 30D up. */
22
32
  export declare const CHART_RANGES: readonly ChartRange[];
23
33
  /** A step is offered once it makes four points, and the default is the finest that makes 200 or fewer. */
@@ -32,6 +42,12 @@ export declare function chartSteps(span: number): {
32
42
  * not read as a collapse. Volume is summed.
33
43
  */
34
44
  export declare function bucketChart(points: readonly ChartPoint[], step: ChartStep, aggregate: 'last' | 'mean'): ChartPoint[];
45
+ /**
46
+ * Markers on the points drawn: each on the last point at or before it, so one
47
+ * mid-week sits on its week. Markers on one point share it, labels joined; one
48
+ * before the first point or after the last point's step is left off.
49
+ */
50
+ export declare function placeMarkers(markers: readonly ChartMarker[], shown: readonly ChartPoint[], step: ChartStep | null): ChartMarker[];
35
51
  /** Compact from a thousand up, four significant figures below. */
36
52
  export declare const formatChartValue: (n: number) => string;
37
53
  export interface TimeChartProps {
@@ -58,6 +74,12 @@ export interface TimeChartProps {
58
74
  * window links, which load another digest rather than slice this one.
59
75
  */
60
76
  controls?: ReactNode;
77
+ /** Events drawn on the points that hold them. */
78
+ markers?: readonly ChartMarker[];
79
+ /** The step shown first, where the range can make it. */
80
+ step?: ChartStep;
81
+ /** Called with the range and step on mount and on every change. */
82
+ onView?: (view: ChartView) => void;
61
83
  className?: string;
62
84
  }
63
85
  /**
@@ -65,4 +87,4 @@ export interface TimeChartProps {
65
87
  * `.chart-controls button[aria-pressed]`, and a site's stylesheet gives them
66
88
  * and the box their look; the line and fill are the canvas box's `color`.
67
89
  */
68
- export declare function TimeChart({ series, label, ranges, range: initial, aggregate, fromZero, format, candles: initialCandles, height, controls, className, }: TimeChartProps): import("react").JSX.Element;
90
+ export declare function TimeChart({ series, label, ranges, range: initial, aggregate, fromZero, format, candles: initialCandles, height, controls, markers, step: initialStep, onView, className, }: TimeChartProps): import("react").JSX.Element;
package/dist/chart.js CHANGED
@@ -8,7 +8,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  // loaded on mount, so the page paints without it. Every colour is the canvas
9
9
  // box's own `color`, its axis `--chart-axis`, and a candle `--chart-up` or
10
10
  // `--chart-down`, each if set. A count's line is curved; a price's is straight,
11
- // since a curve between two closes draws prices nobody paid.
11
+ // since a curve between two closes draws prices nobody paid. Events are markers
12
+ // on the point they fall in, so a spike carries the reason for it.
12
13
  import { useEffect, useMemo, useRef, useState } from 'react';
13
14
  const DAY = 86_400;
14
15
  const RANGE = {
@@ -74,6 +75,29 @@ export function bucketChart(points, step, aggregate) {
74
75
  }
75
76
  return out;
76
77
  }
78
+ /**
79
+ * Markers on the points drawn: each on the last point at or before it, so one
80
+ * mid-week sits on its week. Markers on one point share it, labels joined; one
81
+ * before the first point or after the last point's step is left off.
82
+ */
83
+ export function placeMarkers(markers, shown, step) {
84
+ const first = shown[0];
85
+ const last = shown.at(-1);
86
+ if (!first || !last)
87
+ return [];
88
+ const end = last.time + (step ? STEP[step].span : DAY);
89
+ const placed = new Map();
90
+ for (const m of [...markers].sort((a, b) => a.time - b.time)) {
91
+ if (m.time < first.time || m.time >= end)
92
+ continue;
93
+ let i = shown.length - 1;
94
+ while (i > 0 && shown[i].time > m.time)
95
+ i--;
96
+ const time = shown[i].time;
97
+ placed.set(time, [...(placed.get(time) ?? []), m.label]);
98
+ }
99
+ return [...placed].map(([time, labels]) => ({ time, label: labels.join(' · ') }));
100
+ }
77
101
  const isCandle = (p) => p.open !== undefined && p.high !== undefined && p.low !== undefined;
78
102
  const compact = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 });
79
103
  const precise = new Intl.NumberFormat('en', { maximumSignificantDigits: 4 });
@@ -102,14 +126,16 @@ const fromZeroScale = (base) => {
102
126
  * `.chart-controls button[aria-pressed]`, and a site's stylesheet gives them
103
127
  * and the box their look; the line and fill are the canvas box's `color`.
104
128
  */
105
- 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, controls, className, }) {
129
+ 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, controls, markers, step: initialStep, onView, className, }) {
106
130
  const box = useRef(null);
107
131
  const chart = useRef(null);
108
132
  const formatRef = useRef(format);
109
133
  formatRef.current = format;
134
+ const onViewRef = useRef(onView);
135
+ onViewRef.current = onView;
110
136
  const [ready, setReady] = useState(false);
111
137
  const [range, setRange] = useState(initial);
112
- const [step, setStep] = useState(null);
138
+ const [step, setStep] = useState(initialStep ?? null);
113
139
  const [candles, setCandles] = useState(initialCandles);
114
140
  const [loaded, setLoaded] = useState(null);
115
141
  const resolution = RANGE[range].resolution;
@@ -118,7 +144,7 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
118
144
  if (!el)
119
145
  return;
120
146
  let disposed = false;
121
- import('lightweight-charts').then(({ createChart, AreaSeries, CandlestickSeries, HistogramSeries, ColorType, CrosshairMode, LineStyle, LineType }) => {
147
+ import('lightweight-charts').then(({ createChart, createSeriesMarkers, AreaSeries, CandlestickSeries, HistogramSeries, ColorType, CrosshairMode, LineStyle, LineType }) => {
122
148
  if (disposed)
123
149
  return;
124
150
  const style = getComputedStyle(el);
@@ -177,7 +203,11 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
177
203
  api.panes()[1]?.setStretchFactor(1);
178
204
  return volume;
179
205
  };
180
- chart.current = { api, area, candle, volume: null, addVolume, up, down };
206
+ chart.current = {
207
+ api, area, candle, volume: null, addVolume, up, down, ink,
208
+ areaMarkers: createSeriesMarkers(area, []),
209
+ candleMarkers: createSeriesMarkers(candle, []),
210
+ };
181
211
  setReady(true);
182
212
  });
183
213
  return () => {
@@ -259,8 +289,21 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
259
289
  const rising = p.value >= (p.open ?? shown[i - 1]?.value ?? p.value);
260
290
  return { time: time(p), value: p.volume ?? 0, color: (rising ? c.up : c.down).replace(/[\d.]+\)$/, '0.5)') };
261
291
  }));
292
+ const drawn = placeMarkers(markers ?? [], shown, view?.step ?? null).map(m => ({
293
+ time: time(m),
294
+ position: 'aboveBar',
295
+ shape: 'arrowDown',
296
+ color: c.ink,
297
+ text: m.label,
298
+ }));
299
+ c.areaMarkers.setMarkers(asCandles ? [] : drawn);
300
+ c.candleMarkers.setMarkers(asCandles ? drawn : []);
262
301
  c.api.timeScale().fitContent();
263
- }, [ready, source, view, candles]);
302
+ }, [ready, source, view, candles, markers]);
303
+ const shownStep = view?.step ?? null;
304
+ useEffect(() => {
305
+ onViewRef.current?.({ range, step: shownStep });
306
+ }, [range, shownStep]);
264
307
  // The steps always show where a range is drawn by day, so the buttons do not jump about; one
265
308
  // the range cannot make (any, under 24H and 7D) is disabled.
266
309
  const daily = ranges.some(r => RANGE[r].resolution === 'day');
package/dist/metadata.js CHANGED
@@ -19,7 +19,6 @@
19
19
  // with no image while their og:image named a card, one listed citations, one
20
20
  // collection page had no ItemList at all.
21
21
  import { decodeEntities } from './markdown.js';
22
- import { stripHtml } from './text.js';
23
22
  export function pageMetadata(o) {
24
23
  const { title, description, url, type = 'website', image, imageSize = { width: 1200, height: 630 }, siteName, locale, handle, markdownTwin, article } = o;
25
24
  const images = image ? [{ url: image, ...imageSize, alt: o.imageAlt ?? title }] : undefined;
@@ -118,14 +117,20 @@ export function citationsFromReferences(items, max = 50) {
118
117
  * characters is a heading with no section, and is skipped.
119
118
  */
120
119
  export function questionPairs(html) {
120
+ // Plain text, not `stripHtml`, which keeps an href in parentheses for a model
121
+ // following citations: a heading's permalink anchor would end it in "(#id)"
122
+ // rather than "?". The anchor itself goes, glyph and all.
123
+ const plain = (s) => decodeEntities(s.replace(/<a\b[^>]*href="#[^"]*"[^>]*>[\s\S]*?<\/a>/gi, '').replace(/<[^>]+>/g, ' '))
124
+ .replace(/\s+/g, ' ')
125
+ .trim();
121
126
  const pairs = [];
122
127
  const sections = html.split(/<h[23][^>]*>/i);
123
128
  for (const section of sections.slice(1)) {
124
129
  const [heading = '', ...rest] = section.split(/<\/h[23]>/i);
125
- const q = stripHtml(heading).replace(/\s+/g, ' ');
130
+ const q = plain(heading).replace(/\s+([?.,;:!])/g, '$1');
126
131
  if (!q.endsWith('?'))
127
132
  continue;
128
- const a = stripHtml(rest.join('')).replace(/\s+/g, ' ');
133
+ const a = plain(rest.join('')).replace(/\s+([?.,;:!])/g, '$1');
129
134
  if (a.length >= 40)
130
135
  pairs.push({ q, a: a.slice(0, 1000) });
131
136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-formant",
3
- "version": "0.47.0",
3
+ "version": "0.48.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",