@sigrun/shared 0.1.0 → 0.1.1
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/dist/blocks/format.d.ts +28 -0
- package/dist/blocks/format.js +199 -0
- package/dist/blocks/icons.d.ts +12 -0
- package/dist/blocks/icons.js +64 -0
- package/dist/blocks/index.d.ts +24 -0
- package/dist/blocks/index.js +20 -0
- package/dist/blocks/lttb.d.ts +16 -0
- package/dist/blocks/lttb.js +67 -0
- package/dist/blocks/ops.d.ts +44 -0
- package/dist/blocks/ops.js +294 -0
- package/dist/blocks/text.d.ts +43 -0
- package/dist/blocks/text.js +790 -0
- package/dist/blocks/transport.d.ts +77 -0
- package/dist/blocks/transport.js +11 -0
- package/dist/blocks/types.d.ts +541 -0
- package/dist/blocks/types.js +17 -0
- package/dist/jobs.d.ts +9 -0
- package/dist/live.d.ts +71 -0
- package/dist/live.js +82 -0
- package/dist/session-meta.d.ts +19 -3
- package/dist/settings.d.ts +7 -0
- package/dist/turns.d.ts +17 -0
- package/dist/turns.js +35 -4
- package/dist/views.d.ts +45 -3
- package/package.json +10 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `formatValue` (UI-blocks spec §5.3): the one value formatter behind
|
|
3
|
+
* `format` props — `stat`, `meter`, table columns, chart axes and card
|
|
4
|
+
* time slots. Pure `Intl` only; no imports outside `shared/src/`.
|
|
5
|
+
*
|
|
6
|
+
* Determinism (spec §14.3): the same input and locale give the same
|
|
7
|
+
* bytes. Everything here is derived from the arguments — no clock
|
|
8
|
+
* reads (the `relative` format takes the timestamp to describe, and
|
|
9
|
+
* `now` comes from `Date.now()` at the CALL SITE inside `toText`
|
|
10
|
+
* rendering... which is equally forbidden to wander). `relative` is
|
|
11
|
+
* therefore stable within a call and pinned by tests that pass a
|
|
12
|
+
* timestamp chosen so the unit boundary cannot flip mid-run.
|
|
13
|
+
*/
|
|
14
|
+
import type { JsonValue } from "../json.js";
|
|
15
|
+
/**
|
|
16
|
+
* Render `value` in `format` for `locale` (spec §5.3's format list).
|
|
17
|
+
*
|
|
18
|
+
* - `undefined` format: numbers group digits; strings pass through;
|
|
19
|
+
* `null` renders empty; booleans spell out; objects and arrays render
|
|
20
|
+
* as JSON with SORTED keys (deterministic bytes regardless of the
|
|
21
|
+
* key order the data arrived in).
|
|
22
|
+
* - `datetime`/`date`/`time` take epoch milliseconds or an ISO string,
|
|
23
|
+
* rendered in `timeZone` when given (defaults to the locale's).
|
|
24
|
+
* - `relative` describes a timestamp relative to `Date.now()`; the unit
|
|
25
|
+
* is chosen by the span, per `Intl.RelativeTimeFormat`.
|
|
26
|
+
* - An unknown format falls back to plain number formatting.
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatValue(value: JsonValue, format: string | undefined, locale: string, timeZone?: string): string;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `formatValue` (UI-blocks spec §5.3): the one value formatter behind
|
|
3
|
+
* `format` props — `stat`, `meter`, table columns, chart axes and card
|
|
4
|
+
* time slots. Pure `Intl` only; no imports outside `shared/src/`.
|
|
5
|
+
*
|
|
6
|
+
* Determinism (spec §14.3): the same input and locale give the same
|
|
7
|
+
* bytes. Everything here is derived from the arguments — no clock
|
|
8
|
+
* reads (the `relative` format takes the timestamp to describe, and
|
|
9
|
+
* `now` comes from `Date.now()` at the CALL SITE inside `toText`
|
|
10
|
+
* rendering... which is equally forbidden to wander). `relative` is
|
|
11
|
+
* therefore stable within a call and pinned by tests that pass a
|
|
12
|
+
* timestamp chosen so the unit boundary cannot flip mid-run.
|
|
13
|
+
*/
|
|
14
|
+
/** The byte-scale units `bytes` walks up through (binary steps of 1024). */
|
|
15
|
+
const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
16
|
+
/** The duration steps `duration` walks down through, with their ms sizes. */
|
|
17
|
+
const DURATION_STEPS = [
|
|
18
|
+
{ unit: "d", ms: 86_400_000 },
|
|
19
|
+
{ unit: "h", ms: 3_600_000 },
|
|
20
|
+
{ unit: "m", ms: 60_000 },
|
|
21
|
+
{ unit: "s", ms: 1_000 },
|
|
22
|
+
{ unit: "ms", ms: 1 },
|
|
23
|
+
];
|
|
24
|
+
/** A locale-aware number formatter with at most `max` fraction digits. */
|
|
25
|
+
function numberFormatter(locale, maximumFractionDigits) {
|
|
26
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits });
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `bytes`: 1024-based scale with a `maximumFractionDigits: 1` number,
|
|
30
|
+
* e.g. `1536 → "1.5 KB"`, `512 → "512 B"`.
|
|
31
|
+
*/
|
|
32
|
+
function formatBytes(value, locale) {
|
|
33
|
+
let unit = 0;
|
|
34
|
+
let scaled = value;
|
|
35
|
+
while (Math.abs(scaled) >= 1024 && unit < BYTE_UNITS.length - 1) {
|
|
36
|
+
scaled /= 1024;
|
|
37
|
+
unit++;
|
|
38
|
+
}
|
|
39
|
+
return `${numberFormatter(locale, 1).format(scaled)} ${BYTE_UNITS[unit]}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `duration`: milliseconds in, the two most significant non-zero units
|
|
43
|
+
* out, e.g. `11520000 → "3h 12m"`, `1500 → "1s 500ms"`, `0 → "0s"`.
|
|
44
|
+
* Sub-second remainders are truncated (not rounded) so the last unit
|
|
45
|
+
* named is the one actually covered.
|
|
46
|
+
*/
|
|
47
|
+
function formatDuration(ms, locale) {
|
|
48
|
+
let rest = Math.trunc(ms);
|
|
49
|
+
const parts = [];
|
|
50
|
+
for (const step of DURATION_STEPS) {
|
|
51
|
+
if (parts.length === 2)
|
|
52
|
+
break;
|
|
53
|
+
if (rest >= step.ms || (parts.length > 0 && rest > 0)) {
|
|
54
|
+
const size = Math.floor(rest / step.ms);
|
|
55
|
+
parts.push(`${numberFormatter(locale, 0).format(size)}${step.unit}`);
|
|
56
|
+
rest -= size * step.ms;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (parts.length === 0)
|
|
60
|
+
return `0s`;
|
|
61
|
+
return parts.join(" ");
|
|
62
|
+
}
|
|
63
|
+
/** Coerce a datetime-ish JSON value to epoch milliseconds, or `undefined`. */
|
|
64
|
+
function epochMillis(value) {
|
|
65
|
+
if (typeof value === "number")
|
|
66
|
+
return value;
|
|
67
|
+
if (typeof value === "string") {
|
|
68
|
+
const parsed = Date.parse(value);
|
|
69
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render `value` in `format` for `locale` (spec §5.3's format list).
|
|
75
|
+
*
|
|
76
|
+
* - `undefined` format: numbers group digits; strings pass through;
|
|
77
|
+
* `null` renders empty; booleans spell out; objects and arrays render
|
|
78
|
+
* as JSON with SORTED keys (deterministic bytes regardless of the
|
|
79
|
+
* key order the data arrived in).
|
|
80
|
+
* - `datetime`/`date`/`time` take epoch milliseconds or an ISO string,
|
|
81
|
+
* rendered in `timeZone` when given (defaults to the locale's).
|
|
82
|
+
* - `relative` describes a timestamp relative to `Date.now()`; the unit
|
|
83
|
+
* is chosen by the span, per `Intl.RelativeTimeFormat`.
|
|
84
|
+
* - An unknown format falls back to plain number formatting.
|
|
85
|
+
*/
|
|
86
|
+
export function formatValue(value, format, locale, timeZone) {
|
|
87
|
+
switch (format) {
|
|
88
|
+
case "number":
|
|
89
|
+
case undefined:
|
|
90
|
+
return plain(value, locale);
|
|
91
|
+
case "compact":
|
|
92
|
+
return typeof value === "number"
|
|
93
|
+
? new Intl.NumberFormat(locale, {
|
|
94
|
+
notation: "compact",
|
|
95
|
+
maximumFractionDigits: 1,
|
|
96
|
+
}).format(value)
|
|
97
|
+
: plain(value, locale);
|
|
98
|
+
case "percent":
|
|
99
|
+
return typeof value === "number"
|
|
100
|
+
? new Intl.NumberFormat(locale, {
|
|
101
|
+
style: "percent",
|
|
102
|
+
maximumFractionDigits: 1,
|
|
103
|
+
}).format(value)
|
|
104
|
+
: plain(value, locale);
|
|
105
|
+
case "bytes":
|
|
106
|
+
return typeof value === "number" ? formatBytes(value, locale) : plain(value, locale);
|
|
107
|
+
case "duration":
|
|
108
|
+
return typeof value === "number" ? formatDuration(value, locale) : plain(value, locale);
|
|
109
|
+
case "datetime":
|
|
110
|
+
case "date":
|
|
111
|
+
case "time": {
|
|
112
|
+
const at = epochMillis(value);
|
|
113
|
+
if (at === undefined)
|
|
114
|
+
return plain(value, locale);
|
|
115
|
+
const options = format === "datetime"
|
|
116
|
+
? { dateStyle: "medium", timeStyle: "short" }
|
|
117
|
+
: format === "date"
|
|
118
|
+
? { dateStyle: "medium" }
|
|
119
|
+
: { timeStyle: "medium" };
|
|
120
|
+
return new Intl.DateTimeFormat(locale, timeZone ? { ...options, timeZone } : options).format(at);
|
|
121
|
+
}
|
|
122
|
+
case "relative": {
|
|
123
|
+
const at = epochMillis(value);
|
|
124
|
+
if (at === undefined)
|
|
125
|
+
return plain(value, locale);
|
|
126
|
+
return relativeFrom(at, Date.now(), locale);
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
if (format.startsWith("currency:")) {
|
|
130
|
+
const currency = format.slice("currency:".length);
|
|
131
|
+
if (typeof value !== "number")
|
|
132
|
+
return plain(value, locale);
|
|
133
|
+
try {
|
|
134
|
+
return new Intl.NumberFormat(locale, {
|
|
135
|
+
style: "currency",
|
|
136
|
+
currency,
|
|
137
|
+
}).format(value);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// An invalid ISO 4217 code (validation's job to catch
|
|
141
|
+
// earlier) renders as a plain number, never a throw.
|
|
142
|
+
return numberFormatter(locale, 2).format(value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return plain(value, locale);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** The no-format rendering: locale-numbered primitives, sorted-key JSON. */
|
|
149
|
+
function plain(value, locale) {
|
|
150
|
+
if (value === null)
|
|
151
|
+
return "";
|
|
152
|
+
switch (typeof value) {
|
|
153
|
+
case "string":
|
|
154
|
+
return value;
|
|
155
|
+
case "number":
|
|
156
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 3 }).format(value);
|
|
157
|
+
case "boolean":
|
|
158
|
+
return value ? "true" : "false";
|
|
159
|
+
default:
|
|
160
|
+
return JSON.stringify(sortKeys(value));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Recursively copy `value` with every object's keys in sorted order. */
|
|
164
|
+
function sortKeys(value) {
|
|
165
|
+
if (Array.isArray(value))
|
|
166
|
+
return value.map(sortKeys);
|
|
167
|
+
if (value !== null && typeof value === "object") {
|
|
168
|
+
const out = {};
|
|
169
|
+
for (const key of Object.keys(value).sort()) {
|
|
170
|
+
out[key] = sortKeys(value[key]);
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* `"<span> ago"` for a timestamp: picks the unit whose threshold the
|
|
178
|
+
* span has crossed (`Intl.RelativeTimeFormat` conventions: seconds
|
|
179
|
+
* under a minute, minutes under an hour, hours under a day, days under
|
|
180
|
+
* a week, then weeks, months, years).
|
|
181
|
+
*/
|
|
182
|
+
function relativeFrom(at, now, locale) {
|
|
183
|
+
const spanSeconds = Math.round((at - now) / 1000);
|
|
184
|
+
const abs = Math.abs(spanSeconds);
|
|
185
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
186
|
+
if (abs < 60)
|
|
187
|
+
return rtf.format(spanSeconds, "second");
|
|
188
|
+
if (abs < 3600)
|
|
189
|
+
return rtf.format(Math.round(spanSeconds / 60), "minute");
|
|
190
|
+
if (abs < 86_400)
|
|
191
|
+
return rtf.format(Math.round(spanSeconds / 3600), "hour");
|
|
192
|
+
if (abs < 7 * 86_400)
|
|
193
|
+
return rtf.format(Math.round(spanSeconds / 86_400), "day");
|
|
194
|
+
if (abs < 30 * 86_400)
|
|
195
|
+
return rtf.format(Math.round(spanSeconds / (7 * 86_400)), "week");
|
|
196
|
+
if (abs < 365 * 86_400)
|
|
197
|
+
return rtf.format(Math.round(spanSeconds / (30 * 86_400)), "month");
|
|
198
|
+
return rtf.format(Math.round(spanSeconds / (365 * 86_400)), "year");
|
|
199
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The named icons (UI-blocks spec §10.2): a CLOSED set of 49 names.
|
|
3
|
+
*
|
|
4
|
+
* The webui draws them from a bundled subset of Lucide; the TUI maps
|
|
5
|
+
* each name to a Unicode glyph with an ASCII fallback. An unknown icon
|
|
6
|
+
* name fails validation (U0 Task 2) — `IconName` is the literal union,
|
|
7
|
+
* so a typo is a type error wherever the contract is used literally.
|
|
8
|
+
*/
|
|
9
|
+
/** The v1 icon set, exactly as spec §10.2 pins it. */
|
|
10
|
+
export declare const ICON_NAMES: readonly ["add", "edit", "delete", "refresh", "search", "filter", "share", "copy", "link", "external", "more", "check", "close", "drag", "file", "folder", "calendar", "clock", "user", "users", "tag", "list", "board", "chart", "image", "book", "cart", "home", "bell", "lock", "key", "info", "warn", "error", "success", "pending", "running", "sun", "moon", "cloud", "cloud-sun", "cloud-rain", "cloud-snow", "cloud-lightning", "fog", "wind", "droplets", "thermometer", "umbrella"];
|
|
11
|
+
/** One name from the closed set. */
|
|
12
|
+
export type IconName = (typeof ICON_NAMES)[number];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The named icons (UI-blocks spec §10.2): a CLOSED set of 49 names.
|
|
3
|
+
*
|
|
4
|
+
* The webui draws them from a bundled subset of Lucide; the TUI maps
|
|
5
|
+
* each name to a Unicode glyph with an ASCII fallback. An unknown icon
|
|
6
|
+
* name fails validation (U0 Task 2) — `IconName` is the literal union,
|
|
7
|
+
* so a typo is a type error wherever the contract is used literally.
|
|
8
|
+
*/
|
|
9
|
+
/** The v1 icon set, exactly as spec §10.2 pins it. */
|
|
10
|
+
export const ICON_NAMES = [
|
|
11
|
+
// navigation and actions
|
|
12
|
+
"add",
|
|
13
|
+
"edit",
|
|
14
|
+
"delete",
|
|
15
|
+
"refresh",
|
|
16
|
+
"search",
|
|
17
|
+
"filter",
|
|
18
|
+
"share",
|
|
19
|
+
"copy",
|
|
20
|
+
"link",
|
|
21
|
+
"external",
|
|
22
|
+
"more",
|
|
23
|
+
"check",
|
|
24
|
+
"close",
|
|
25
|
+
"drag",
|
|
26
|
+
// objects
|
|
27
|
+
"file",
|
|
28
|
+
"folder",
|
|
29
|
+
"calendar",
|
|
30
|
+
"clock",
|
|
31
|
+
"user",
|
|
32
|
+
"users",
|
|
33
|
+
"tag",
|
|
34
|
+
"list",
|
|
35
|
+
"board",
|
|
36
|
+
"chart",
|
|
37
|
+
"image",
|
|
38
|
+
"book",
|
|
39
|
+
"cart",
|
|
40
|
+
"home",
|
|
41
|
+
"bell",
|
|
42
|
+
"lock",
|
|
43
|
+
"key",
|
|
44
|
+
// status
|
|
45
|
+
"info",
|
|
46
|
+
"warn",
|
|
47
|
+
"error",
|
|
48
|
+
"success",
|
|
49
|
+
"pending",
|
|
50
|
+
"running",
|
|
51
|
+
// weather
|
|
52
|
+
"sun",
|
|
53
|
+
"moon",
|
|
54
|
+
"cloud",
|
|
55
|
+
"cloud-sun",
|
|
56
|
+
"cloud-rain",
|
|
57
|
+
"cloud-snow",
|
|
58
|
+
"cloud-lightning",
|
|
59
|
+
"fog",
|
|
60
|
+
"wind",
|
|
61
|
+
"droplets",
|
|
62
|
+
"thermometer",
|
|
63
|
+
"umbrella",
|
|
64
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sigrun/shared/blocks` — the block-set foundation (UI-blocks U0).
|
|
3
|
+
*
|
|
4
|
+
* One entry point for the wire types (§4/§5/§6.1), the data-op applier
|
|
5
|
+
* (§4.3), the icon names (§10.2), display downsampling (§5.4), and the
|
|
6
|
+
* channel-facing surface transport. Consumed by the server, the webui
|
|
7
|
+
* and the TUI alike; re-exported for extensions through
|
|
8
|
+
* `@sigrun/sigrun/blocks` (U0 Task 4).
|
|
9
|
+
*
|
|
10
|
+
* The runtime half is deliberately tiny and dependency-free: the purity
|
|
11
|
+
* guard (`test/views-purity.test.ts`) holds every specifier inside
|
|
12
|
+
* `shared/src/`.
|
|
13
|
+
*/
|
|
14
|
+
export { BLOCK_SET_VERSION } from "./types.js";
|
|
15
|
+
export { DataOpError, applyOps, resolvePointer } from "./ops.js";
|
|
16
|
+
export { ICON_NAMES } from "./icons.js";
|
|
17
|
+
export { lttb } from "./lttb.js";
|
|
18
|
+
export { formatValue } from "./format.js";
|
|
19
|
+
export { toText } from "./text.js";
|
|
20
|
+
export type { Action, ActionButton, Bindable, Binding, Block, BlockBase, BulkAction, CalloutBlock, Card, CardBadge, CardMetric, CardMeta, CardStatus, ChartAxisX, ChartAxisY, ChartBlock, ChartSeries, ChartThreshold, ChoiceField, ChoiceOption, CollectionBlock, CollectionColumn, CollectionGroup, CollectionSort, CollectionTime, ColumnChild, ColumnsBlock, Confirm, CoreTarget, DataOp, DataOps, DataUpdate, DateField, DatePreset, DaterangeField, DatetimeField, DetailsBlock, DiffBlock, Field, FieldBase, FieldsBlock, FieldsRow, FileField, Format, FormBlock, GridBlock, GridChild, ImageBlock, ListField, LogBlock, LogLine, LongtextField, MarkdownBlock, MeterBlock, MeterThreshold, NumberField, PageBlock, PageEmpty, Placement, SectionBlock, SecretField, Size, StackBlock, StatBlock, TextField, ToggleField, Tone, Tree, TreeBlock, TreeNode, WidthCondition, } from "./types.js";
|
|
21
|
+
export type { IconName } from "./icons.js";
|
|
22
|
+
export type { TextOptions } from "./text.js";
|
|
23
|
+
export type { SurfaceAccess, SurfaceActErr, SurfaceActOk, SurfaceActResult, SurfaceChangedEvent, SurfaceFetch, SurfaceLoad, SurfaceTransport, SurfaceUploadBody, } from "./transport.js";
|
|
24
|
+
export type { JsonObject, JsonValue } from "../json.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sigrun/shared/blocks` — the block-set foundation (UI-blocks U0).
|
|
3
|
+
*
|
|
4
|
+
* One entry point for the wire types (§4/§5/§6.1), the data-op applier
|
|
5
|
+
* (§4.3), the icon names (§10.2), display downsampling (§5.4), and the
|
|
6
|
+
* channel-facing surface transport. Consumed by the server, the webui
|
|
7
|
+
* and the TUI alike; re-exported for extensions through
|
|
8
|
+
* `@sigrun/sigrun/blocks` (U0 Task 4).
|
|
9
|
+
*
|
|
10
|
+
* The runtime half is deliberately tiny and dependency-free: the purity
|
|
11
|
+
* guard (`test/views-purity.test.ts`) holds every specifier inside
|
|
12
|
+
* `shared/src/`.
|
|
13
|
+
*/
|
|
14
|
+
// ─── Runtime (pure, dependency-free) ──────────────────────────────────────
|
|
15
|
+
export { BLOCK_SET_VERSION } from "./types.js";
|
|
16
|
+
export { DataOpError, applyOps, resolvePointer } from "./ops.js";
|
|
17
|
+
export { ICON_NAMES } from "./icons.js";
|
|
18
|
+
export { lttb } from "./lttb.js";
|
|
19
|
+
export { formatValue } from "./format.js";
|
|
20
|
+
export { toText } from "./text.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Largest-triangle-three-buckets downsampling (UI-blocks spec §5.4):
|
|
3
|
+
* the display-side downsample channels apply to line and area series
|
|
4
|
+
* (at most 2,000 points per series on the wire; a channel shows fewer).
|
|
5
|
+
*
|
|
6
|
+
* Pure: returns a fresh array of fresh tuples; never mutates `points`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Downsample `points` to at most `threshold` points, always keeping the
|
|
10
|
+
* first and the last point. The interior is bucketed into `threshold -
|
|
11
|
+
* 2` buckets; each bucket keeps the point forming the largest triangle
|
|
12
|
+
* with the previously selected point and the next bucket's average —
|
|
13
|
+
* which preserves visual shape (peaks and troughs) rather than
|
|
14
|
+
* sampling on a fixed interval.
|
|
15
|
+
*/
|
|
16
|
+
export declare function lttb(points: readonly (readonly [number, number])[], threshold: number): [number, number][];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Largest-triangle-three-buckets downsampling (UI-blocks spec §5.4):
|
|
3
|
+
* the display-side downsample channels apply to line and area series
|
|
4
|
+
* (at most 2,000 points per series on the wire; a channel shows fewer).
|
|
5
|
+
*
|
|
6
|
+
* Pure: returns a fresh array of fresh tuples; never mutates `points`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Downsample `points` to at most `threshold` points, always keeping the
|
|
10
|
+
* first and the last point. The interior is bucketed into `threshold -
|
|
11
|
+
* 2` buckets; each bucket keeps the point forming the largest triangle
|
|
12
|
+
* with the previously selected point and the next bucket's average —
|
|
13
|
+
* which preserves visual shape (peaks and troughs) rather than
|
|
14
|
+
* sampling on a fixed interval.
|
|
15
|
+
*/
|
|
16
|
+
export function lttb(points, threshold) {
|
|
17
|
+
const count = points.length;
|
|
18
|
+
if (count === 0 || threshold <= 0)
|
|
19
|
+
return [];
|
|
20
|
+
if (threshold === 1)
|
|
21
|
+
return [[points[0][0], points[0][1]]];
|
|
22
|
+
if (threshold === 2) {
|
|
23
|
+
return [
|
|
24
|
+
[points[0][0], points[0][1]],
|
|
25
|
+
[count === 1 ? points[0][0] : points[count - 1][0],
|
|
26
|
+
count === 1 ? points[0][1] : points[count - 1][1]],
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
if (threshold >= count) {
|
|
30
|
+
return points.map((point) => [point[0], point[1]]);
|
|
31
|
+
}
|
|
32
|
+
const sampled = [[points[0][0], points[0][1]]];
|
|
33
|
+
const bucketSize = (count - 2) / (threshold - 2);
|
|
34
|
+
for (let bucket = 0; bucket < threshold - 2; bucket++) {
|
|
35
|
+
const start = Math.floor(1 + bucket * bucketSize);
|
|
36
|
+
const end = Math.min(Math.floor(1 + (bucket + 1) * bucketSize), count - 1);
|
|
37
|
+
// The average of the NEXT bucket's points — the third corner of
|
|
38
|
+
// every candidate triangle. The last bucket averages the final
|
|
39
|
+
// point's window [count-1, count).
|
|
40
|
+
const nextStart = end;
|
|
41
|
+
const nextEnd = Math.min(Math.floor(1 + (bucket + 2) * bucketSize), count);
|
|
42
|
+
let avgX = 0;
|
|
43
|
+
let avgY = 0;
|
|
44
|
+
for (let i = nextStart; i < nextEnd; i++) {
|
|
45
|
+
avgX += points[i][0];
|
|
46
|
+
avgY += points[i][1];
|
|
47
|
+
}
|
|
48
|
+
avgX /= nextEnd - nextStart;
|
|
49
|
+
avgY /= nextEnd - nextStart;
|
|
50
|
+
const previous = sampled[sampled.length - 1];
|
|
51
|
+
let bestArea = -1;
|
|
52
|
+
let bestIndex = start;
|
|
53
|
+
for (let i = start; i < end; i++) {
|
|
54
|
+
// Twice the signed area of the triangle (previous, candidate,
|
|
55
|
+
// average) — the factor is constant, so abs is enough to rank.
|
|
56
|
+
const area = Math.abs((previous[0] - avgX) * (points[i][1] - previous[1]) -
|
|
57
|
+
(previous[0] - points[i][0]) * (avgY - previous[1]));
|
|
58
|
+
if (area > bestArea) {
|
|
59
|
+
bestArea = area;
|
|
60
|
+
bestIndex = i;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
sampled.push([points[bestIndex][0], points[bestIndex][1]]);
|
|
64
|
+
}
|
|
65
|
+
sampled.push([points[count - 1][0], points[count - 1][1]]);
|
|
66
|
+
return sampled;
|
|
67
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `applyOps` and `resolvePointer` (UI-blocks spec §4.2/§4.3): the single
|
|
3
|
+
* applier every surface's data updates go through, and the pointer
|
|
4
|
+
* reader bindings resolve with.
|
|
5
|
+
*
|
|
6
|
+
* Purity contract:
|
|
7
|
+
*
|
|
8
|
+
* - `applyOps` NEVER mutates its input — it clones along the touched
|
|
9
|
+
* path only (copy-on-write), so a batch of ops over a large document
|
|
10
|
+
* shares every untouched branch by reference with the input. The
|
|
11
|
+
* property suite pins this (frozen inputs, snapshot equality).
|
|
12
|
+
* - Pointers are RFC 6901 with `~0` (`~`) and `~1` (`/`) escaping, plus
|
|
13
|
+
* the one extension (spec §4.2): a pointer starting `./` resolves
|
|
14
|
+
* against the passed `item` (a collection's item template), which
|
|
15
|
+
* only `resolvePointer` takes — the DATA side never sees `./`.
|
|
16
|
+
* - A failing op throws `DataOpError` carrying the op's INDEX, so the
|
|
17
|
+
* caller (U3/U5) can treat it as the full-refetch signal (spec §16).
|
|
18
|
+
*/
|
|
19
|
+
import type { JsonObject, JsonValue } from "../json.js";
|
|
20
|
+
import type { DataOp } from "./types.js";
|
|
21
|
+
/** An op that could not be applied; `opIndex` names which one failed. */
|
|
22
|
+
export declare class DataOpError extends Error {
|
|
23
|
+
readonly opIndex: number;
|
|
24
|
+
constructor(opIndex: number, message: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Apply a batch of `DataOp`s (spec §4.3) to `data`, returning a NEW
|
|
28
|
+
* object. Pure: untouched branches are shared by reference with the
|
|
29
|
+
* input; the input is never mutated. Ops apply sequentially — an
|
|
30
|
+
* earlier op's write is visible to the next.
|
|
31
|
+
*
|
|
32
|
+
* Throws `DataOpError` (naming the op's index) on any op whose path
|
|
33
|
+
* does not exist, or whose target is not an array where one is required.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyOps(data: JsonObject, ops: readonly DataOp[]): JsonObject;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve an RFC 6901 `pointer` against `data`; a pointer starting
|
|
38
|
+
* `./` resolves against `item` instead (spec §4.2's one extension,
|
|
39
|
+
* used inside collection item templates). A missing path — including a
|
|
40
|
+
* path THROUGH a JSON null, and a malformed pointer (no `/` or `./`
|
|
41
|
+
* prefix) — is `undefined`, never an error: "a binding to a missing
|
|
42
|
+
* path renders as empty, never as an error".
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolvePointer(data: JsonValue, pointer: string, item?: JsonValue): JsonValue | undefined;
|