@opendata-ai/openchart-core 8.8.0 → 8.9.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/index.js CHANGED
@@ -1,3 +1,121 @@
1
+ // src/helpers/edit-transactions.ts
2
+ function isContainer(value) {
3
+ return Array.isArray(value) || value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
4
+ }
5
+ function clone(value) {
6
+ if (Array.isArray(value)) return value.map(clone);
7
+ if (!isContainer(value)) return value;
8
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, clone(child)]));
9
+ }
10
+ function tokens(path) {
11
+ if (!path.startsWith("/")) throw new Error("Expected a non-root JSON Pointer beginning with /.");
12
+ return path.slice(1).split("/").map((token) => {
13
+ if (/~(?:[^01]|$)/.test(token)) throw new Error("Invalid JSON Pointer escape.");
14
+ const decoded = token.replace(/~1/g, "/").replace(/~0/g, "~");
15
+ if (["__proto__", "prototype", "constructor"].includes(decoded)) {
16
+ throw new Error("Unsafe property in edit path.");
17
+ }
18
+ return decoded;
19
+ });
20
+ }
21
+ function pointer(parts) {
22
+ return `/${parts.map((part) => part.replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`;
23
+ }
24
+ function keyAt(parent, token, insert = false) {
25
+ if (!Array.isArray(parent)) return token;
26
+ if (insert && token === "-") return parent.length;
27
+ if (!/^(0|[1-9]\d*)$/.test(token)) throw new Error("Expected an array index.");
28
+ const index = Number(token);
29
+ if (!Number.isSafeInteger(index) || index > parent.length || !insert && index === parent.length) {
30
+ throw new Error("Array index is out of bounds.");
31
+ }
32
+ return index;
33
+ }
34
+ function read(parent, key) {
35
+ return parent[key];
36
+ }
37
+ function checkExpectation(root, parts, expected) {
38
+ if (!expected) return;
39
+ let end = -1;
40
+ for (let i = 0; i + 1 < parts.length; i++) {
41
+ if (parts[i] === "annotations" && /^(0|[1-9]\d*)$/.test(parts[i + 1])) end = i + 2;
42
+ }
43
+ if (end === -1) throw new Error("Annotation precondition requires an annotation path.");
44
+ let target = root;
45
+ for (const part of parts.slice(0, end)) {
46
+ if (!isContainer(target)) throw new Error("Annotation target no longer exists.");
47
+ const key = keyAt(target, part);
48
+ if (!Object.hasOwn(target, key)) throw new Error("Annotation target no longer exists.");
49
+ target = read(target, key);
50
+ }
51
+ if (!isContainer(target) || Array.isArray(target))
52
+ throw new Error("Annotation target is invalid.");
53
+ for (const field of ["id", "type", "text"]) {
54
+ if (Object.hasOwn(expected, field) && target[field] !== expected[field]) {
55
+ throw new Error(`Annotation ${field} changed; discard the stale edit.`);
56
+ }
57
+ }
58
+ }
59
+ function applyEditTransaction(spec, tx) {
60
+ const result = clone(spec);
61
+ const inverse = [];
62
+ for (const [opIndex, op] of tx.ops.entries()) {
63
+ try {
64
+ if (!["set", "add", "remove"].includes(op.op)) throw new Error("Unknown edit operation.");
65
+ if (op.op !== "remove" && !Object.hasOwn(op, "value"))
66
+ throw new Error("Edit value is missing.");
67
+ const parts = tokens(op.path);
68
+ checkExpectation(result, parts, op.expect);
69
+ let parent = result;
70
+ const created = [];
71
+ for (let i = 0; i < parts.length - 1; i++) {
72
+ if (!isContainer(parent)) throw new Error("Cannot traverse a primitive or non-JSON value.");
73
+ const key2 = keyAt(parent, parts[i]);
74
+ if (!Object.hasOwn(parent, key2)) {
75
+ if (op.op !== "add" || Array.isArray(parent))
76
+ throw new Error("Intermediate container is missing.");
77
+ parent[key2] = /^(0|[1-9]\d*|-)$/.test(parts[i + 1]) ? [] : {};
78
+ created.push(pointer(parts.slice(0, i + 1)));
79
+ }
80
+ parent = read(parent, key2);
81
+ }
82
+ if (!isContainer(parent)) throw new Error("Edit parent is not a JSON container.");
83
+ const key = keyAt(parent, parts.at(-1), op.op === "add");
84
+ const exists = Object.hasOwn(parent, key);
85
+ const old = read(parent, key);
86
+ const path = pointer([...parts.slice(0, -1), String(key)]);
87
+ let undo;
88
+ if (op.op === "remove") {
89
+ if (!exists) throw new Error("Edit target no longer exists.");
90
+ if (Array.isArray(parent)) parent.splice(Number(key), 1);
91
+ else delete parent[key];
92
+ undo = { op: "add", path, value: old };
93
+ } else if (op.op === "add" && Array.isArray(parent)) {
94
+ parent.splice(Number(key), 0, clone(op.value));
95
+ undo = { op: "remove", path };
96
+ } else {
97
+ parent[key] = clone(op.value);
98
+ undo = exists ? { op: "set", path, value: old } : { op: "remove", path };
99
+ }
100
+ inverse.unshift(undo, ...created.reverse().map((path2) => ({ op: "remove", path: path2 })));
101
+ } catch (error) {
102
+ return {
103
+ ok: false,
104
+ error: {
105
+ opIndex,
106
+ path: op.path,
107
+ reason: error instanceof Error ? error.message : String(error)
108
+ }
109
+ };
110
+ }
111
+ }
112
+ return {
113
+ ok: true,
114
+ spec: result,
115
+ inverse: { ...tx.revision !== void 0 ? { revision: tx.revision } : {}, ops: inverse }
116
+ };
117
+ }
118
+
1
119
  // src/types/encoding.ts
2
120
  function required(...types) {
3
121
  return { required: true, allowedTypes: types };
@@ -521,6 +639,13 @@ var CSS_TOKENS = [
521
639
  { name: "--oc-source-size", light: "11px" },
522
640
  { name: "--oc-source-weight", light: "400" },
523
641
  { name: "--oc-body-size", light: "13px" },
642
+ { name: "--oc-annotation-label", light: "#333333", dark: "#d1d5db" },
643
+ { name: "--oc-annotation-muted", light: "#6b7280", dark: "#9ca3af" },
644
+ { name: "--oc-annotation-leader", light: "#8f8f8f", dark: "#9aa0a6" },
645
+ { name: "--oc-annotation-rule", light: "#666666", dark: "#666666" },
646
+ { name: "--oc-annotation-range-fill", light: "#000000", dark: "#000000" },
647
+ { name: "--oc-annotation-marker-fill", light: "#ffffff", dark: "#0a0a0a" },
648
+ { name: "--oc-annotation-plate", light: "#ffffff", dark: "#0a0a0a" },
524
649
  { name: "--oc-eyebrow-size", light: "11px" },
525
650
  { name: "--oc-eyebrow-weight", light: "500" },
526
651
  { name: "--oc-eyebrow-tracking", light: "0.08em" },
@@ -1439,6 +1564,13 @@ var DEFAULT_THEME = {
1439
1564
  axis: "#71717a",
1440
1565
  annotationFill: "rgba(0,0,0,0.04)",
1441
1566
  annotationText: "#71717a",
1567
+ annotationLabel: "#333333",
1568
+ annotationMuted: "#6b7280",
1569
+ annotationLeader: "#8f8f8f",
1570
+ annotationRule: "#666666",
1571
+ annotationRangeFill: "#000000",
1572
+ annotationMarkerFill: "#ffffff",
1573
+ annotationPlate: "#ffffff",
1442
1574
  positive: "#15803d",
1443
1575
  negative: "#dc2626"
1444
1576
  },
@@ -1605,6 +1737,13 @@ function adaptTheme(theme) {
1605
1737
  axis: darkAxis,
1606
1738
  annotationFill: pairs?.["colors.annotationFill"] ? pairs["colors.annotationFill"].dark : overridden(theme.colors.annotationFill, light.annotationFill, "rgba(255,255,255,0.06)"),
1607
1739
  annotationText: pairs?.["colors.annotationText"] ? pairs["colors.annotationText"].dark : overridden(theme.colors.annotationText, light.annotationText, darkMuted),
1740
+ annotationLabel: pairs?.["colors.annotationLabel"]?.dark ?? overridden(theme.colors.annotationLabel, light.annotationLabel, "#d1d5db"),
1741
+ annotationMuted: pairs?.["colors.annotationMuted"]?.dark ?? overridden(theme.colors.annotationMuted, light.annotationMuted, "#9ca3af"),
1742
+ annotationLeader: pairs?.["colors.annotationLeader"]?.dark ?? overridden(theme.colors.annotationLeader, light.annotationLeader, "#9aa0a6"),
1743
+ annotationRule: pairs?.["colors.annotationRule"]?.dark ?? overridden(theme.colors.annotationRule, light.annotationRule, "#666666"),
1744
+ annotationRangeFill: pairs?.["colors.annotationRangeFill"]?.dark ?? overridden(theme.colors.annotationRangeFill, light.annotationRangeFill, "#000000"),
1745
+ annotationMarkerFill: pairs?.["colors.annotationMarkerFill"]?.dark ?? overridden(theme.colors.annotationMarkerFill, light.annotationMarkerFill, "#0a0a0a"),
1746
+ annotationPlate: pairs?.["colors.annotationPlate"]?.dark ?? overridden(theme.colors.annotationPlate, light.annotationPlate, "#0a0a0a"),
1608
1747
  categorical,
1609
1748
  categoricalFill,
1610
1749
  hairline: darkHairline,
@@ -1656,6 +1795,11 @@ function adaptForLightLineStroke(color) {
1656
1795
  return c.formatHex();
1657
1796
  }
1658
1797
 
1798
+ // src/theme/merge.ts
1799
+ function mergeThemeConfig(base, override) {
1800
+ return override ? { ...base, ...override } : base;
1801
+ }
1802
+
1659
1803
  // src/theme/presets.ts
1660
1804
  var editorial = {};
1661
1805
  var essay = {
@@ -1771,6 +1915,23 @@ var terminal = {
1771
1915
  borderRadius: 4,
1772
1916
  seriesStrategy: "accent-neutral"
1773
1917
  };
1918
+ var clarity = {
1919
+ colors: {
1920
+ background: { light: "#ffffff", dark: "#09090b" },
1921
+ axis: { light: "#52525b", dark: "#a1a1aa" }
1922
+ },
1923
+ fonts: { sizes: { title: 26, subtitle: 14, body: 13, small: 12, axisTick: 12 } },
1924
+ chrome: {
1925
+ eyebrow: { color: { light: "#0f766e", dark: "#5eead4" } },
1926
+ source: { fontSize: 12 },
1927
+ footer: { fontSize: 12 }
1928
+ },
1929
+ spacing: { chromeToChart: 16, chartToFooter: 12 }
1930
+ };
1931
+ var legacyEditorial = {
1932
+ fonts: { sizes: { title: 26, subtitle: 14, body: 13, small: 11, axisTick: 11 } },
1933
+ spacing: { padding: 20, chromeGap: 6, chromeToChart: 8, chartToFooter: 8 }
1934
+ };
1774
1935
 
1775
1936
  // src/theme/resolve.ts
1776
1937
  function deepMerge(target, source) {
@@ -1809,6 +1970,20 @@ function themeConfigToPartial(config) {
1809
1970
  colors.annotationFill = resolveTokenLight(config.colors.annotationFill);
1810
1971
  if (config.colors.annotationText)
1811
1972
  colors.annotationText = resolveTokenLight(config.colors.annotationText);
1973
+ if (config.colors.annotationLabel)
1974
+ colors.annotationLabel = resolveTokenLight(config.colors.annotationLabel);
1975
+ if (config.colors.annotationMuted)
1976
+ colors.annotationMuted = resolveTokenLight(config.colors.annotationMuted);
1977
+ if (config.colors.annotationLeader)
1978
+ colors.annotationLeader = resolveTokenLight(config.colors.annotationLeader);
1979
+ if (config.colors.annotationRule)
1980
+ colors.annotationRule = resolveTokenLight(config.colors.annotationRule);
1981
+ if (config.colors.annotationRangeFill)
1982
+ colors.annotationRangeFill = resolveTokenLight(config.colors.annotationRangeFill);
1983
+ if (config.colors.annotationMarkerFill)
1984
+ colors.annotationMarkerFill = resolveTokenLight(config.colors.annotationMarkerFill);
1985
+ if (config.colors.annotationPlate)
1986
+ colors.annotationPlate = resolveTokenLight(config.colors.annotationPlate);
1812
1987
  if (config.colors.positive) colors.positive = resolveTokenLight(config.colors.positive);
1813
1988
  if (config.colors.negative) colors.negative = resolveTokenLight(config.colors.negative);
1814
1989
  }
@@ -1883,6 +2058,13 @@ function collectTokenPairs(config) {
1883
2058
  "axis",
1884
2059
  "annotationFill",
1885
2060
  "annotationText",
2061
+ "annotationLabel",
2062
+ "annotationMuted",
2063
+ "annotationLeader",
2064
+ "annotationRule",
2065
+ "annotationRangeFill",
2066
+ "annotationMarkerFill",
2067
+ "annotationPlate",
1886
2068
  "positive",
1887
2069
  "negative"
1888
2070
  ];
@@ -1983,6 +2165,22 @@ function resolveTheme(userTheme, base = DEFAULT_THEME) {
1983
2165
  const dark = isDarkBackground(merged.colors.background);
1984
2166
  if (dark) {
1985
2167
  merged = adaptChromeForDarkBg(merged, merged.colors.text);
2168
+ const authoredColors = userTheme?.colors && !Array.isArray(userTheme.colors) ? userTheme.colors : {};
2169
+ for (const key of [
2170
+ "annotationLabel",
2171
+ "annotationMuted",
2172
+ "annotationLeader",
2173
+ "annotationRule",
2174
+ "annotationRangeFill",
2175
+ "annotationMarkerFill",
2176
+ "annotationPlate"
2177
+ ]) {
2178
+ if (authoredColors[key] === void 0)
2179
+ merged.colors[key] = cssTokenDefault(
2180
+ `--oc-${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`,
2181
+ "dark"
2182
+ );
2183
+ }
1986
2184
  }
1987
2185
  return {
1988
2186
  ...merged,
@@ -2067,9 +2265,20 @@ function estimateLineCount(text, style, maxWidth, measureText, maxLines) {
2067
2265
  }
2068
2266
  var FOOTNOTE_LINE_HEIGHT = 1.3;
2069
2267
  var FOOTNOTE_BAND_GAP = 4;
2070
- function footnoteBandHeight(count, theme) {
2071
- if (count <= 0) return 0;
2072
- return count * theme.fonts.sizes.small * FOOTNOTE_LINE_HEIGHT + FOOTNOTE_BAND_GAP;
2268
+ function computeNoteBandHeight(input, theme) {
2269
+ if (typeof input !== "number" && input.noteBandHeight !== void 0) return input.noteBandHeight;
2270
+ const count = typeof input === "number" ? input : (input.footnotes?.reduce((sum, note) => sum + (note.lines?.length ?? 1), 0) ?? 0) + (input.seriesSummary?.reduce((sum, entry) => sum + entry.lines.length, 0) ?? 0);
2271
+ return count <= 0 ? 0 : count * theme.fonts.sizes.small * FOOTNOTE_LINE_HEIGHT + FOOTNOTE_BAND_GAP;
2272
+ }
2273
+ var warnedFootnoteBandHeight = false;
2274
+ function footnoteBandHeight(input, theme) {
2275
+ if (!warnedFootnoteBandHeight) {
2276
+ warnedFootnoteBandHeight = true;
2277
+ console.warn(
2278
+ "[openchart] footnoteBandHeight is deprecated; read layout.chrome.noteBandHeight."
2279
+ );
2280
+ }
2281
+ return computeNoteBandHeight(input, theme);
2073
2282
  }
2074
2283
  function computeChrome(chrome, theme, width, measureText, chromeMode = "full", padding, watermark = true, bottomLegendHeight = 0, crampedTitle = false) {
2075
2284
  if (!chrome || chromeMode === "hidden") {
@@ -2583,6 +2792,10 @@ function buildD3Formatter(formatStr) {
2583
2792
  }
2584
2793
  function formatPercent(value, options) {
2585
2794
  const fraction = options?.fraction ?? true;
2795
+ const percent = fraction ? value * 100 : value;
2796
+ if (Number.isFinite(percent) && percent !== 0 && Math.abs(percent) < 0.1) {
2797
+ return percent < 0 ? `>${FORMAT_PERCENT_RAW(-0.1)}%` : `<${FORMAT_PERCENT_RAW(0.1)}%`;
2798
+ }
2586
2799
  if (fraction) {
2587
2800
  return FORMAT_PERCENT_FRACTION(value);
2588
2801
  }
@@ -2918,6 +3131,19 @@ function formatValue(value) {
2918
3131
  }
2919
3132
 
2920
3133
  // src/helpers/spec-builders.ts
3134
+ var warnedBuilderOptions = /* @__PURE__ */ new Set();
3135
+ function warnBuilderOption(key, message) {
3136
+ if (warnedBuilderOptions.has(key)) return;
3137
+ warnedBuilderOptions.add(key);
3138
+ console.warn(`[openchart] ${message}`);
3139
+ }
3140
+ function warnDarkMode(options) {
3141
+ if (options?.darkMode !== void 0)
3142
+ warnBuilderOption(
3143
+ "darkMode",
3144
+ "Builder darkMode is deprecated and has no effect; use the mount option."
3145
+ );
3146
+ }
2921
3147
  var ISO_DATE_RE = /^\d{4}(-\d{2}(-\d{2}(T\d{2}(:\d{2}(:\d{2})?)?)?)?)?$/;
2922
3148
  function inferFieldType(data, field) {
2923
3149
  const sampleSize = Math.min(data.length, 20);
@@ -2962,12 +3188,12 @@ function buildEncoding(channels, options, data) {
2962
3188
  return encoding;
2963
3189
  }
2964
3190
  function buildChartSpec(mark, data, encoding, options) {
3191
+ warnDarkMode(options);
2965
3192
  const spec = { mark, data, encoding };
2966
3193
  if (options?.chrome) spec.chrome = options.chrome;
2967
3194
  if (options?.annotations) spec.annotations = options.annotations;
2968
3195
  if (options?.responsive !== void 0) spec.responsive = options.responsive;
2969
3196
  if (options?.theme) spec.theme = options.theme;
2970
- if (options?.darkMode) spec.darkMode = options.darkMode;
2971
3197
  return spec;
2972
3198
  }
2973
3199
  function lineChart(data, x, y, options) {
@@ -3031,6 +3257,9 @@ function scatterChart(data, x, y, options) {
3031
3257
  return buildChartSpec("point", data, encoding, options);
3032
3258
  }
3033
3259
  function dataTable(data, options) {
3260
+ warnDarkMode(options);
3261
+ if (options?.compact !== void 0)
3262
+ warnBuilderOption("compact", "dataTable compact is deprecated; use density.");
3034
3263
  let columns = options?.columns;
3035
3264
  if (!columns && data.length > 0) {
3036
3265
  columns = Object.keys(data[0]).map((key) => {
@@ -3051,15 +3280,16 @@ function dataTable(data, options) {
3051
3280
  if (options?.rowKey) spec.rowKey = options.rowKey;
3052
3281
  if (options?.chrome) spec.chrome = options.chrome;
3053
3282
  if (options?.theme) spec.theme = options.theme;
3054
- if (options?.darkMode) spec.darkMode = options.darkMode;
3055
3283
  if (options?.search !== void 0) spec.search = options.search;
3056
3284
  if (options?.pagination !== void 0) spec.pagination = options.pagination;
3057
3285
  if (options?.stickyFirstColumn !== void 0) spec.stickyFirstColumn = options.stickyFirstColumn;
3058
- if (options?.compact !== void 0) spec.compact = options.compact;
3286
+ if (options?.density !== void 0) spec.density = options.density;
3287
+ else if (options?.compact !== void 0) spec.density = options.compact ? "condensed" : "regular";
3059
3288
  if (options?.responsive !== void 0) spec.responsive = options.responsive;
3060
3289
  return spec;
3061
3290
  }
3062
3291
  function tileMap(data, options) {
3292
+ warnDarkMode(options);
3063
3293
  return {
3064
3294
  type: "tilemap",
3065
3295
  data,
@@ -3069,7 +3299,6 @@ function tileMap(data, options) {
3069
3299
  ...options?.chrome && { chrome: options.chrome },
3070
3300
  ...options?.legend && { legend: options.legend },
3071
3301
  ...options?.theme && { theme: options.theme },
3072
- ...options?.darkMode && { darkMode: options.darkMode },
3073
3302
  ...options?.watermark !== void 0 && { watermark: options.watermark },
3074
3303
  ...options?.animation && { animation: options.animation },
3075
3304
  ...options?.valueFormat && { valueFormat: options.valueFormat }
@@ -3089,6 +3318,104 @@ function deepMergeSpec(target, source) {
3089
3318
  }
3090
3319
  return result;
3091
3320
  }
3321
+
3322
+ // src/helpers/annotation-source.ts
3323
+ var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3324
+ function annotationSource(spec) {
3325
+ const root = object(spec);
3326
+ if (Array.isArray(root?.annotations))
3327
+ return { annotations: root.annotations, path: "/annotations" };
3328
+ function firstLeaf(node, path) {
3329
+ if (Array.isArray(node?.layer)) {
3330
+ for (const [index, child] of node.layer.entries()) {
3331
+ const result = firstLeaf(object(child), `${path}/layer/${index}`);
3332
+ if (result) return result;
3333
+ }
3334
+ } else if (node && "mark" in node) {
3335
+ return {
3336
+ annotations: Array.isArray(node.annotations) ? node.annotations : [],
3337
+ path: `${path}/annotations`
3338
+ };
3339
+ }
3340
+ }
3341
+ return firstLeaf(root, "") ?? { annotations: [], path: "/annotations" };
3342
+ }
3343
+
3344
+ // src/motion/sample.ts
3345
+ var MOTION_REST_THRESHOLD = 1e-3;
3346
+ var MAX_SPRING_DURATION = 1200;
3347
+ var FLUID_SPRING = {
3348
+ stiffness: 320,
3349
+ damping: 36,
3350
+ mass: 1
3351
+ };
3352
+ function springState(timeMs, config) {
3353
+ const stiffness = config.stiffness ?? FLUID_SPRING.stiffness;
3354
+ const mass = config.mass ?? FLUID_SPRING.mass;
3355
+ const rootStiffness = Math.sqrt(stiffness);
3356
+ const rootMass = Math.sqrt(mass);
3357
+ const omega = rootStiffness / rootMass;
3358
+ const damping = config.damping ?? FLUID_SPRING.damping;
3359
+ const zeta = Math.max(1, damping / rootStiffness / rootMass / 2);
3360
+ const t = Math.max(0, timeMs) / 1e3;
3361
+ let remaining;
3362
+ let velocity;
3363
+ if (zeta - 1 < 1e-4) {
3364
+ const scaledTime = omega * t;
3365
+ const decay = Math.exp(-scaledTime);
3366
+ remaining = decay === 0 ? 0 : (1 + scaledTime) * decay;
3367
+ velocity = decay === 0 ? 0 : omega * (scaledTime * decay);
3368
+ } else {
3369
+ const inverseZeta = 1 / zeta;
3370
+ const q = damping / 2 * (1 + Math.sqrt(1 - inverseZeta * inverseZeta));
3371
+ const slow = -stiffness / q;
3372
+ const fast = -q / mass;
3373
+ const ratio = slow / fast;
3374
+ const slowDecay = Math.exp(slow * t);
3375
+ const fastDecay = Math.exp(fast * t);
3376
+ if (slowDecay === 0) {
3377
+ remaining = 0;
3378
+ velocity = 0;
3379
+ } else {
3380
+ remaining = (slowDecay - ratio * fastDecay) / (1 - ratio);
3381
+ velocity = -slow * ((slowDecay - fastDecay) / (1 - ratio));
3382
+ }
3383
+ }
3384
+ return {
3385
+ position: Math.min(1, Math.max(0, 1 - remaining)),
3386
+ velocity,
3387
+ settled: remaining <= MOTION_REST_THRESHOLD && Math.abs(velocity) <= MOTION_REST_THRESHOLD
3388
+ };
3389
+ }
3390
+ function springDuration(config) {
3391
+ for (const name of ["stiffness", "damping", "mass"]) {
3392
+ const value = config[name];
3393
+ if (value === void 0) continue;
3394
+ if (!Number.isFinite(value) || value <= 0)
3395
+ throw new Error(`spring.${name} must be finite and positive`);
3396
+ }
3397
+ for (let ms = 1; ms < MAX_SPRING_DURATION; ms++) {
3398
+ if (springState(ms, config).settled) return ms;
3399
+ }
3400
+ return MAX_SPRING_DURATION;
3401
+ }
3402
+ function sampleMotion(phase, elapsedMs) {
3403
+ if (phase.duration <= 0 || elapsedMs >= phase.duration) {
3404
+ return { position: 1, velocity: 0, settled: true };
3405
+ }
3406
+ if (elapsedMs <= 0) return { position: 0, velocity: 0, settled: false };
3407
+ if (phase.spring) return springState(elapsedMs, phase.spring);
3408
+ const t = elapsedMs / phase.duration;
3409
+ const power = phase.ease === "snappy" ? 5 : 3;
3410
+ return {
3411
+ position: 1 - (1 - t) ** power,
3412
+ velocity: power * (1 - t) ** (power - 1) * 1e3 / phase.duration,
3413
+ settled: false
3414
+ };
3415
+ }
3416
+ function motionCSS(phase) {
3417
+ return `linear(${Array.from({ length: 101 }, (_, i) => sampleMotion(phase, i * phase.duration / 100).position.toFixed(6)).join(",")})`;
3418
+ }
3092
3419
  export {
3093
3420
  ACHROMATIC_RAMP,
3094
3421
  AXIS_TITLE_GAP,
@@ -3112,6 +3439,7 @@ export {
3112
3439
  DEFAULT_THEME,
3113
3440
  DIVERGING_PALETTES,
3114
3441
  EXTENDED_OFFSET_STRATEGIES,
3442
+ FLUID_SPRING,
3115
3443
  FOOTNOTE_LINE_HEIGHT,
3116
3444
  GRAPH_ENCODING_RULES,
3117
3445
  HEIGHT_CRAMPED_MAX,
@@ -3130,7 +3458,9 @@ export {
3130
3458
  MAX_LEFT_LABEL_FRACTION_DEFAULT,
3131
3459
  MAX_LEFT_LABEL_FRACTION_MEDIUM,
3132
3460
  MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX,
3461
+ MAX_SPRING_DURATION,
3133
3462
  MAX_SR_TABLE_ROWS,
3463
+ MOTION_REST_THRESHOLD,
3134
3464
  NARROW_VIEWPORT_MAX,
3135
3465
  OFFSET_STRATEGIES,
3136
3466
  SEQUENTIAL_PALETTES,
@@ -3147,6 +3477,8 @@ export {
3147
3477
  adaptColorForDarkMode,
3148
3478
  adaptForLightLineStroke,
3149
3479
  adaptTheme,
3480
+ annotationSource,
3481
+ applyEditTransaction,
3150
3482
  areaChart,
3151
3483
  axisTitleOffset,
3152
3484
  barChart,
@@ -3155,10 +3487,12 @@ export {
3155
3487
  buildD3Formatter,
3156
3488
  buildTemporalFormatter,
3157
3489
  checkPaletteDistinguishability,
3490
+ clarity,
3158
3491
  columnChart,
3159
3492
  computeChrome,
3160
3493
  computeFieldFormatContext,
3161
3494
  computeLabelBounds,
3495
+ computeNoteBandHeight,
3162
3496
  computeXAxisExtentFromLabels,
3163
3497
  contrastRatio,
3164
3498
  cssTokenDefault,
@@ -3208,9 +3542,12 @@ export {
3208
3542
  isTileMapSpec,
3209
3543
  isValueDef,
3210
3544
  isYearContext,
3545
+ legacyEditorial,
3211
3546
  lineChart,
3212
3547
  maxRotatedLabelWidth,
3213
3548
  meetsAA,
3549
+ mergeThemeConfig,
3550
+ motionCSS,
3214
3551
  overlapArea,
3215
3552
  pickLabelColor,
3216
3553
  pieChart,
@@ -3222,8 +3559,10 @@ export {
3222
3559
  resolveNumberFormatter,
3223
3560
  resolveSchemeName,
3224
3561
  resolveTheme,
3562
+ sampleMotion,
3225
3563
  scatterChart,
3226
3564
  simulateColorBlindness,
3565
+ springDuration,
3227
3566
  terminal,
3228
3567
  textAscent,
3229
3568
  tileMap,