@opendata-ai/openchart-core 8.9.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 = {
@@ -1826,6 +1970,20 @@ function themeConfigToPartial(config) {
1826
1970
  colors.annotationFill = resolveTokenLight(config.colors.annotationFill);
1827
1971
  if (config.colors.annotationText)
1828
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);
1829
1987
  if (config.colors.positive) colors.positive = resolveTokenLight(config.colors.positive);
1830
1988
  if (config.colors.negative) colors.negative = resolveTokenLight(config.colors.negative);
1831
1989
  }
@@ -1900,6 +2058,13 @@ function collectTokenPairs(config) {
1900
2058
  "axis",
1901
2059
  "annotationFill",
1902
2060
  "annotationText",
2061
+ "annotationLabel",
2062
+ "annotationMuted",
2063
+ "annotationLeader",
2064
+ "annotationRule",
2065
+ "annotationRangeFill",
2066
+ "annotationMarkerFill",
2067
+ "annotationPlate",
1903
2068
  "positive",
1904
2069
  "negative"
1905
2070
  ];
@@ -2000,6 +2165,22 @@ function resolveTheme(userTheme, base = DEFAULT_THEME) {
2000
2165
  const dark = isDarkBackground(merged.colors.background);
2001
2166
  if (dark) {
2002
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
+ }
2003
2184
  }
2004
2185
  return {
2005
2186
  ...merged,
@@ -2084,9 +2265,20 @@ function estimateLineCount(text, style, maxWidth, measureText, maxLines) {
2084
2265
  }
2085
2266
  var FOOTNOTE_LINE_HEIGHT = 1.3;
2086
2267
  var FOOTNOTE_BAND_GAP = 4;
2087
- function footnoteBandHeight(count, theme) {
2088
- if (count <= 0) return 0;
2089
- 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);
2090
2282
  }
2091
2283
  function computeChrome(chrome, theme, width, measureText, chromeMode = "full", padding, watermark = true, bottomLegendHeight = 0, crampedTitle = false) {
2092
2284
  if (!chrome || chromeMode === "hidden") {
@@ -2600,6 +2792,10 @@ function buildD3Formatter(formatStr) {
2600
2792
  }
2601
2793
  function formatPercent(value, options) {
2602
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
+ }
2603
2799
  if (fraction) {
2604
2800
  return FORMAT_PERCENT_FRACTION(value);
2605
2801
  }
@@ -2935,6 +3131,19 @@ function formatValue(value) {
2935
3131
  }
2936
3132
 
2937
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
+ }
2938
3147
  var ISO_DATE_RE = /^\d{4}(-\d{2}(-\d{2}(T\d{2}(:\d{2}(:\d{2})?)?)?)?)?$/;
2939
3148
  function inferFieldType(data, field) {
2940
3149
  const sampleSize = Math.min(data.length, 20);
@@ -2979,12 +3188,12 @@ function buildEncoding(channels, options, data) {
2979
3188
  return encoding;
2980
3189
  }
2981
3190
  function buildChartSpec(mark, data, encoding, options) {
3191
+ warnDarkMode(options);
2982
3192
  const spec = { mark, data, encoding };
2983
3193
  if (options?.chrome) spec.chrome = options.chrome;
2984
3194
  if (options?.annotations) spec.annotations = options.annotations;
2985
3195
  if (options?.responsive !== void 0) spec.responsive = options.responsive;
2986
3196
  if (options?.theme) spec.theme = options.theme;
2987
- if (options?.darkMode) spec.darkMode = options.darkMode;
2988
3197
  return spec;
2989
3198
  }
2990
3199
  function lineChart(data, x, y, options) {
@@ -3048,6 +3257,9 @@ function scatterChart(data, x, y, options) {
3048
3257
  return buildChartSpec("point", data, encoding, options);
3049
3258
  }
3050
3259
  function dataTable(data, options) {
3260
+ warnDarkMode(options);
3261
+ if (options?.compact !== void 0)
3262
+ warnBuilderOption("compact", "dataTable compact is deprecated; use density.");
3051
3263
  let columns = options?.columns;
3052
3264
  if (!columns && data.length > 0) {
3053
3265
  columns = Object.keys(data[0]).map((key) => {
@@ -3068,15 +3280,16 @@ function dataTable(data, options) {
3068
3280
  if (options?.rowKey) spec.rowKey = options.rowKey;
3069
3281
  if (options?.chrome) spec.chrome = options.chrome;
3070
3282
  if (options?.theme) spec.theme = options.theme;
3071
- if (options?.darkMode) spec.darkMode = options.darkMode;
3072
3283
  if (options?.search !== void 0) spec.search = options.search;
3073
3284
  if (options?.pagination !== void 0) spec.pagination = options.pagination;
3074
3285
  if (options?.stickyFirstColumn !== void 0) spec.stickyFirstColumn = options.stickyFirstColumn;
3075
- 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";
3076
3288
  if (options?.responsive !== void 0) spec.responsive = options.responsive;
3077
3289
  return spec;
3078
3290
  }
3079
3291
  function tileMap(data, options) {
3292
+ warnDarkMode(options);
3080
3293
  return {
3081
3294
  type: "tilemap",
3082
3295
  data,
@@ -3086,7 +3299,6 @@ function tileMap(data, options) {
3086
3299
  ...options?.chrome && { chrome: options.chrome },
3087
3300
  ...options?.legend && { legend: options.legend },
3088
3301
  ...options?.theme && { theme: options.theme },
3089
- ...options?.darkMode && { darkMode: options.darkMode },
3090
3302
  ...options?.watermark !== void 0 && { watermark: options.watermark },
3091
3303
  ...options?.animation && { animation: options.animation },
3092
3304
  ...options?.valueFormat && { valueFormat: options.valueFormat }
@@ -3107,6 +3319,28 @@ function deepMergeSpec(target, source) {
3107
3319
  return result;
3108
3320
  }
3109
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
+
3110
3344
  // src/motion/sample.ts
3111
3345
  var MOTION_REST_THRESHOLD = 1e-3;
3112
3346
  var MAX_SPRING_DURATION = 1200;
@@ -3243,6 +3477,8 @@ export {
3243
3477
  adaptColorForDarkMode,
3244
3478
  adaptForLightLineStroke,
3245
3479
  adaptTheme,
3480
+ annotationSource,
3481
+ applyEditTransaction,
3246
3482
  areaChart,
3247
3483
  axisTitleOffset,
3248
3484
  barChart,
@@ -3256,6 +3492,7 @@ export {
3256
3492
  computeChrome,
3257
3493
  computeFieldFormatContext,
3258
3494
  computeLabelBounds,
3495
+ computeNoteBandHeight,
3259
3496
  computeXAxisExtentFromLabels,
3260
3497
  contrastRatio,
3261
3498
  cssTokenDefault,
@@ -3309,6 +3546,7 @@ export {
3309
3546
  lineChart,
3310
3547
  maxRotatedLabelWidth,
3311
3548
  meetsAA,
3549
+ mergeThemeConfig,
3312
3550
  motionCSS,
3313
3551
  overlapArea,
3314
3552
  pickLabelColor,