wiki-formant 0.57.1 → 0.58.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
@@ -847,7 +847,8 @@ own subpath, because it imports `@simplewebauthn/browser`.
847
847
  `Stats` (`wiki-formant/stats`) renders a `digest` as one page: the
848
848
  headline figures, each with its change from the window before
849
849
  (`.stats-delta[data-better]`, a share or for the bounce rate points, for the
850
- site's stylesheet to colour), one of them per day, every non-empty
850
+ site's stylesheet to colour), one of them by day, week or month (a count's
851
+ total, a rate's mean), every non-empty
851
852
  ranked list, and `?days=` links between windows, all time among them, pinned in
852
853
  the chart's frame as its own buttons are (`TimeChart`'s `controls`). Each
853
854
  figure is a button that charts it: visitors, agents, page views, bounce rate,
@@ -897,7 +898,7 @@ that makes 200 or fewer, so a year opens weekly and five years monthly.
897
898
 
898
899
  ```tsx
899
900
  // Daily points: the chart slices and steps them itself; 30D up.
900
- <TimeChart series={days} label="Transactions" aggregate="mean" fromZero />
901
+ <TimeChart series={days} label="Transactions" aggregate="sum" fromZero />
901
902
 
902
903
  // A loader: asked for 'hour' (24H), '4h' (7D) or 'day' from 0 (every longer
903
904
  // range), once each.
@@ -915,8 +916,10 @@ the slot empty, except the last, which is the price now. A price's line is
915
916
  straight between closes and a count's is curved.
916
917
 
917
918
  `aggregate` is how a week or month is made from its days: `last` for a price,
918
- `mean` for a count, so the week in progress keeps its level instead of reading
919
- as a collapse. `fromZero` starts a count's axis at zero and stops it there, with
919
+ `sum` for a count, so a week reads as the week's total, and `mean` for a rate,
920
+ which no total makes sense of. A stepped range starts on a whole week or month,
921
+ so its first point is not a few days of one; the week in progress is its total
922
+ so far. `fromZero` starts a count's axis at zero and stops it there, with
920
923
  no negative ticks under it. A single range draws no range buttons and shows every point.
921
924
  `chartSteps` is exported for a chart drawn elsewhere.
922
925
 
@@ -932,7 +935,7 @@ range can make it, and `onView` hears the range and step on mount and on every
932
935
  change, for a page that keeps them in its URL:
933
936
 
934
937
  ```tsx
935
- <TimeChart series={days} label="Transactions" aggregate="mean" fromZero
938
+ <TimeChart series={days} label="Transactions" aggregate="sum" fromZero
936
939
  markers={[{ time: haltAt, label: 'Halt' }]} step="month" onView={v => keep(v)} />
937
940
  ```
938
941
  `base.css` pins the buttons (`.chart-controls button[aria-pressed]`) to the
package/dist/chart.d.ts CHANGED
@@ -28,6 +28,8 @@ export type ChartView = {
28
28
  range: ChartRange;
29
29
  step: ChartStep | null;
30
30
  };
31
+ /** How a week or month is made from its days: a price's last, a count's total, a rate's mean. */
32
+ export type ChartAggregate = 'last' | 'sum' | 'mean';
31
33
  /** Every range, shortest first. A chart fed daily points offers the ones from 30D up. */
32
34
  export declare const CHART_RANGES: readonly ChartRange[];
33
35
  /** A step is offered once it makes four points, and the default is the finest that makes 200 or fewer. */
@@ -51,8 +53,8 @@ export interface TimeChartProps {
51
53
  ranges?: readonly ChartRange[];
52
54
  /** The range shown first. */
53
55
  range?: ChartRange;
54
- /** A week or month of prices keeps its last; of counts, the mean of its days. */
55
- aggregate?: 'last' | 'mean';
56
+ /** A week or month of prices keeps its last; of counts, the total of its days; of rates, their mean. */
57
+ aggregate?: ChartAggregate;
56
58
  /** The value axis starts at zero, so a quiet week does not read as a collapse. */
57
59
  fromZero?: boolean;
58
60
  /**
package/dist/chart.js CHANGED
@@ -48,8 +48,8 @@ function bucketStart(time, step) {
48
48
  /**
49
49
  * Points, oldest first, one per step. A price keeps the last value in each
50
50
  * step, as a candle does its close: the first open, the highest high, the
51
- * lowest low. A count takes the mean of its days, so the week in progress does
52
- * not read as a collapse. Volume is summed.
51
+ * lowest low. A count is the total of its days; a rate, their mean. Volume is
52
+ * summed.
53
53
  *
54
54
  * @internal
55
55
  */
@@ -66,6 +66,8 @@ export function bucketChart(points, step, aggregate) {
66
66
  }
67
67
  if (aggregate === 'last')
68
68
  last.value = p.value;
69
+ else if (aggregate === 'sum')
70
+ last.value += p.value;
69
71
  else
70
72
  last.value += (p.value - last.value) / ++n;
71
73
  if (last.high !== undefined && p.high !== undefined)
@@ -256,7 +258,7 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
256
258
  if (!ready || !c)
257
259
  return;
258
260
  c.api.priceScale('right').applyOptions({ scaleMargins: { top: MARGIN_TOP, bottom: fromZero ? 0 : 0.1 } });
259
- c.area.applyOptions({ lineType: aggregate === 'mean' ? c.curved : c.straight, autoscaleInfoProvider: fromZero ? fromZeroScale : ownScale });
261
+ c.area.applyOptions({ lineType: aggregate === 'last' ? c.straight : c.curved, autoscaleInfoProvider: fromZero ? fromZeroScale : ownScale });
260
262
  }, [ready, fromZero, aggregate]);
261
263
  // One request per resolution for the life of the loader: every range from 30D up reads the same days.
262
264
  const cache = useRef(new Map());
@@ -298,7 +300,11 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
298
300
  return drawn(windowed, [], null);
299
301
  const { steps, fallback } = chartSteps(end - windowed[0].time);
300
302
  const chosen = step && steps.includes(step) ? step : fallback;
301
- return drawn(chosen === 'day' ? windowed : bucketChart(windowed, chosen, aggregate), steps, chosen);
303
+ if (chosen === 'day')
304
+ return drawn(windowed, steps, chosen);
305
+ // A stepped range starts on a whole week or month, so its first total is not a few days of one.
306
+ const from = bucketStart(windowed[0].time, chosen);
307
+ return drawn(bucketChart(points.filter(p => p.time >= from), chosen, aggregate), steps, chosen);
302
308
  }, [points, range, ranges.length, resolution, step, aggregate]);
303
309
  // While a resolution loads, the last one stays under the skeleton; an empty or failed one clears it.
304
310
  useEffect(() => {
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  // stats-figures.tsx — the stats page's headline figures and the chart under
4
- // them. Each figure is a button that charts that figure per day, so the chart
4
+ // them. Each figure is a button that charts that figure by day, week or month, so the chart
5
5
  // is not only visitors. Its own module because it holds state; `Stats` stays a
6
6
  // server component and hands it the digest, which is plain JSON.
7
7
  import { useMemo, useState } from 'react';
@@ -34,6 +34,7 @@ const figuresOf = ({ previous: p, ...d }) => [
34
34
  delta: change(d.bounce_rate, p.bounce_rate, { points: true, lowerIsBetter: true }),
35
35
  day: r => (r.visits ? (100 * r.bounces) / r.visits : null),
36
36
  format: percent,
37
+ aggregate: 'mean',
37
38
  },
38
39
  {
39
40
  label: 'Visit length',
@@ -41,6 +42,7 @@ const figuresOf = ({ previous: p, ...d }) => [
41
42
  delta: change(d.visit_duration, p.visit_duration),
42
43
  day: r => (r.visits ? r.seconds / r.visits : null),
43
44
  format: duration,
45
+ aggregate: 'mean',
44
46
  },
45
47
  ...(d.follow_clicks
46
48
  ? [
@@ -74,5 +76,5 @@ export function StatsFigures({ digest, first, windows }) {
74
76
  return value == null ? null : { time, value };
75
77
  }).filter(p => p !== null);
76
78
  }, [byDay, first, figure]);
77
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "stats-figures", role: "group", "aria-label": "Chart", children: figures.map((f, i) => (_jsxs("button", { type: "button", "aria-pressed": i === chosen, onClick: () => choose(i), children: [_jsx("span", { children: f.label }), _jsxs("strong", { children: [f.value, f.delta && (_jsx("small", { className: "stats-delta", "data-better": f.delta.better, children: f.delta.text }))] })] }, f.label))) }), _jsx(TimeChart, { series: series, label: `${figure.label} per day`, ranges: ['all'], aggregate: "mean", fromZero: true, format: figure.format, controls: windows, className: "stats-chart" })] }));
79
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "stats-figures", role: "group", "aria-label": "Chart", children: figures.map((f, i) => (_jsxs("button", { type: "button", "aria-pressed": i === chosen, onClick: () => choose(i), children: [_jsx("span", { children: f.label }), _jsxs("strong", { children: [f.value, f.delta && (_jsx("small", { className: "stats-delta", "data-better": f.delta.better, children: f.delta.text }))] })] }, f.label))) }), _jsx(TimeChart, { series: series, label: figure.label, ranges: ['all'], aggregate: figure.aggregate ?? 'sum', fromZero: true, format: figure.format, controls: windows, className: "stats-chart" })] }));
78
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-formant",
3
- "version": "0.57.1",
3
+ "version": "0.58.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",