graphein 0.9.1 → 0.10.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/dist/index.js CHANGED
@@ -741,11 +741,33 @@ var CATEGORICAL = {
741
741
  "#f97316",
742
742
  "#6366f1",
743
743
  "#84cc16"
744
+ ],
745
+ colorblind: ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7", "#999999"],
746
+ bright: [
747
+ "#2563eb",
748
+ "#06b6d4",
749
+ "#22c55e",
750
+ "#eab308",
751
+ "#f97316",
752
+ "#ef4444",
753
+ "#a855f7",
754
+ "#ec4899"
755
+ ],
756
+ muted: [
757
+ "#4f46e5",
758
+ "#0f766e",
759
+ "#65a30d",
760
+ "#ca8a04",
761
+ "#c2410c",
762
+ "#be123c",
763
+ "#7c3aed",
764
+ "#475569"
744
765
  ]
745
766
  };
746
767
  function categorical(name = "graphein") {
747
768
  return [...CATEGORICAL[name] ?? CATEGORICAL.graphein];
748
769
  }
770
+ var categoricalSchemes = Object.keys(CATEGORICAL);
749
771
  var must = (hex) => {
750
772
  const c = parseColor(hex);
751
773
  if (!c) throw new Error(`Graphein: invalid palette color ${hex}`);
@@ -1841,9 +1863,9 @@ function summarizeChart(spec) {
1841
1863
  return { label: description.trim(), rowCount };
1842
1864
  }
1843
1865
  const type = chartTypeLabel(spec.type);
1844
- const title = chartTitleText(spec) ?? (spec.type === "kpi" ? spec.label : void 0);
1866
+ const title2 = chartTitleText(spec) ?? (spec.type === "kpi" ? spec.label : void 0);
1845
1867
  let label = type;
1846
- if (title) label += `: ${title}`;
1868
+ if (title2) label += `: ${title2}`;
1847
1869
  if (rowCount > 0 && spec.type !== "kpi") {
1848
1870
  label += `. ${rowCount} data point${rowCount === 1 ? "" : "s"}.`;
1849
1871
  }
@@ -3071,1484 +3093,998 @@ function linearRegression(xs, ys) {
3071
3093
  };
3072
3094
  }
3073
3095
 
3074
- // src/render/env.ts
3075
- var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
3076
- function getDevicePixelRatio() {
3077
- const dpr = globalThis.devicePixelRatio;
3078
- return typeof dpr === "number" && dpr > 0 ? dpr : 1;
3079
- }
3096
+ // src/spec/types.ts
3097
+ var SLICER_TYPES = [
3098
+ "dropdown",
3099
+ "search",
3100
+ "list",
3101
+ "range",
3102
+ "dateRange"
3103
+ ];
3104
+ var CHART_TYPES = [
3105
+ "line",
3106
+ "area",
3107
+ "bar",
3108
+ "scatter",
3109
+ "combo",
3110
+ "histogram",
3111
+ "pie",
3112
+ "heatmap",
3113
+ "funnel",
3114
+ "kpi",
3115
+ "treemap",
3116
+ "gauge",
3117
+ "bullet",
3118
+ "calendarHeatmap",
3119
+ "waterfall",
3120
+ "slope",
3121
+ "dumbbell",
3122
+ "table",
3123
+ "matrix",
3124
+ "box",
3125
+ "sankey",
3126
+ "choropleth",
3127
+ "dropdown",
3128
+ "search",
3129
+ "list",
3130
+ "range",
3131
+ "dateRange"
3132
+ ];
3080
3133
 
3081
- // src/render/sizing.ts
3082
- function computeBackingSize(width, height, dpr) {
3083
- const d = dpr > 0 ? dpr : 1;
3084
- const cssWidth = Math.max(0, width);
3085
- const cssHeight = Math.max(0, height);
3086
- return {
3087
- cssWidth,
3088
- cssHeight,
3089
- pixelWidth: Math.max(1, Math.round(cssWidth * d)),
3090
- pixelHeight: Math.max(1, Math.round(cssHeight * d)),
3091
- dpr: d
3134
+ // src/spec/transform/types.ts
3135
+ var TRANSFORM_KINDS = ["aggregate", "bin", "filter", "fold", "timeUnit", "calculate"];
3136
+ var TIME_UNITS = [
3137
+ "year",
3138
+ "quarter",
3139
+ "month",
3140
+ "week",
3141
+ "day",
3142
+ "hour",
3143
+ "minute",
3144
+ "second"
3145
+ ];
3146
+
3147
+ // src/spec/transform/aggregate.ts
3148
+ function applyAggregate(transform2, data) {
3149
+ const groupby = transform2.groupby ?? [];
3150
+ const reads = groupby.map((f) => accessor(f));
3151
+ const emit = (keyValues, rows) => {
3152
+ const out2 = {};
3153
+ groupby.forEach((field, i) => {
3154
+ out2[field] = keyValues[i];
3155
+ });
3156
+ for (const agg of transform2.aggregate) {
3157
+ const values = agg.field === void 0 ? rows : rows.map((r) => r[agg.field]);
3158
+ out2[agg.as] = aggregateValues(values, agg.op);
3159
+ }
3160
+ return out2;
3092
3161
  };
3162
+ if (groupby.length === 0) {
3163
+ return [emit([], data)];
3164
+ }
3165
+ const groups = /* @__PURE__ */ new Map();
3166
+ for (const row of data) {
3167
+ const keyValues = reads.map((read) => read(row));
3168
+ const key = keyValues.map((v) => toKey(v)).join("\0");
3169
+ let group = groups.get(key);
3170
+ if (group === void 0) {
3171
+ group = { keys: keyValues, rows: [] };
3172
+ groups.set(key, group);
3173
+ }
3174
+ group.rows.push(row);
3175
+ }
3176
+ const out = [];
3177
+ for (const group of groups.values()) {
3178
+ out.push(emit(group.keys, group.rows));
3179
+ }
3180
+ return out;
3093
3181
  }
3094
3182
 
3095
- // src/render/dom.ts
3096
- function setStyle(el, style) {
3097
- Object.assign(el.style, style);
3098
- }
3099
- function createDiv(className, style) {
3100
- const el = document.createElement("div");
3101
- if (className) el.className = className;
3102
- if (style) setStyle(el, style);
3103
- return el;
3183
+ // src/spec/transform/bin.ts
3184
+ function roundNice(value, step) {
3185
+ if (!Number.isFinite(step) || step === 0) return value;
3186
+ const places = Math.max(0, -Math.floor(Math.log10(Math.abs(step))) + 1);
3187
+ const rounded = Number(value.toFixed(Math.min(places, 15)));
3188
+ return Object.is(rounded, -0) ? 0 : rounded;
3104
3189
  }
3105
- var fillParentStyle = {
3106
- position: "absolute",
3107
- top: "0",
3108
- left: "0"
3109
- };
3110
-
3111
- // src/render/canvasLayer.ts
3112
- var CanvasLayer = class {
3113
- constructor(canvas) {
3114
- __publicField(this, "canvas");
3115
- __publicField(this, "ctx");
3116
- /** Logical (CSS px) width. */
3117
- __publicField(this, "width", 0);
3118
- /** Logical (CSS px) height. */
3119
- __publicField(this, "height", 0);
3120
- __publicField(this, "dpr", 1);
3121
- this.canvas = canvas ?? document.createElement("canvas");
3122
- const ctx = this.canvas.getContext("2d");
3123
- if (!ctx) {
3124
- throw new Error("Graphein: 2D canvas context is unavailable in this environment.");
3190
+ function computeBins(values, opts = {}) {
3191
+ let min = Infinity;
3192
+ let max = -Infinity;
3193
+ if (opts.extent) {
3194
+ [min, max] = opts.extent;
3195
+ } else {
3196
+ for (const v of values) {
3197
+ if (!Number.isFinite(v)) continue;
3198
+ if (v < min) min = v;
3199
+ if (v > max) max = v;
3125
3200
  }
3126
- this.ctx = ctx;
3127
3201
  }
3128
- /** Resize the layer to a CSS size; updates the backing store + DPR transform. */
3129
- resize(width, height, dpr = getDevicePixelRatio()) {
3130
- const b = computeBackingSize(width, height, dpr);
3131
- this.width = b.cssWidth;
3132
- this.height = b.cssHeight;
3133
- this.dpr = b.dpr;
3134
- if (this.canvas.width !== b.pixelWidth) this.canvas.width = b.pixelWidth;
3135
- if (this.canvas.height !== b.pixelHeight) this.canvas.height = b.pixelHeight;
3136
- this.canvas.style.width = `${b.cssWidth}px`;
3137
- this.canvas.style.height = `${b.cssHeight}px`;
3138
- this.ctx.setTransform(b.dpr, 0, 0, b.dpr, 0, 0);
3202
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
3203
+ const explicit = opts.step !== void 0 && opts.step > 0;
3204
+ if (min === max) {
3205
+ const step2 = explicit ? opts.step : 1;
3206
+ return { start: min, step: step2, count: 1 };
3139
3207
  }
3140
- /** Clear the entire backing store regardless of the current transform. */
3141
- clear() {
3142
- const { ctx, canvas } = this;
3143
- ctx.save();
3144
- ctx.setTransform(1, 0, 0, 1, 0, 0);
3145
- ctx.clearRect(0, 0, canvas.width, canvas.height);
3146
- ctx.restore();
3208
+ const nice = opts.nice !== false;
3209
+ let step = explicit ? opts.step : tickStep(min, max, opts.maxbins ?? 10);
3210
+ if (!Number.isFinite(step) || step <= 0) step = max - min || 1;
3211
+ const start = nice ? Math.floor(min / step) * step : min;
3212
+ const end = nice ? Math.ceil(max / step) * step : max;
3213
+ const count = Math.max(1, Math.ceil((end - start) / step - 1e-9));
3214
+ return { start: roundNice(start, step), step, count };
3215
+ }
3216
+ function applyBin(transform2, data) {
3217
+ const read = accessor(transform2.bin);
3218
+ const finite4 = [];
3219
+ for (const row of data) {
3220
+ const n = toNumber(read(row));
3221
+ if (Number.isFinite(n)) finite4.push(n);
3222
+ }
3223
+ const layout2 = computeBins(finite4, {
3224
+ maxbins: transform2.maxbins,
3225
+ step: transform2.step,
3226
+ extent: transform2.extent,
3227
+ nice: transform2.nice
3228
+ });
3229
+ const startField = Array.isArray(transform2.as) ? transform2.as[0] : transform2.as;
3230
+ const endField = Array.isArray(transform2.as) ? transform2.as[1] : void 0;
3231
+ return data.map((row) => {
3232
+ const out = { ...row };
3233
+ const v = toNumber(read(row));
3234
+ const outside = transform2.extent != null && (v < transform2.extent[0] || v > transform2.extent[1]);
3235
+ if (layout2 == null || !Number.isFinite(v) || outside) {
3236
+ out[startField] = null;
3237
+ if (endField) out[endField] = null;
3238
+ return out;
3239
+ }
3240
+ let index = Math.floor((v - layout2.start) / layout2.step + 1e-9);
3241
+ if (index < 0) index = 0;
3242
+ if (index > layout2.count - 1) index = layout2.count - 1;
3243
+ out[startField] = roundNice(layout2.start + index * layout2.step, layout2.step);
3244
+ if (endField) out[endField] = roundNice(layout2.start + (index + 1) * layout2.step, layout2.step);
3245
+ return out;
3246
+ });
3247
+ }
3248
+
3249
+ // src/spec/transform/expr/error.ts
3250
+ var ExpressionError = class extends Error {
3251
+ constructor(message, pos) {
3252
+ super(message);
3253
+ /** Source offset where the problem was detected, when known. */
3254
+ __publicField(this, "pos");
3255
+ this.name = "ExpressionError";
3256
+ this.pos = pos;
3147
3257
  }
3148
3258
  };
3149
3259
 
3150
- // src/render/surface.ts
3151
- var Surface = class {
3152
- constructor(container) {
3153
- __publicField(this, "container");
3154
- __publicField(this, "root");
3155
- __publicField(this, "marks");
3156
- __publicField(this, "interaction");
3157
- __publicField(this, "overlay");
3158
- /** Visually-hidden container for the screen-reader data-table fallback. */
3159
- __publicField(this, "a11y");
3160
- __publicField(this, "width", 0);
3161
- __publicField(this, "height", 0);
3162
- __publicField(this, "dpr", 1);
3163
- /**
3164
- * When true, overlay text (axis labels, titles, legends, annotation labels) is
3165
- * painted onto the `marks` canvas instead of the HTML overlay. Set by headless
3166
- * renderers (`@graphein/node`) where there is no DOM to host crisp text.
3167
- * Defaults to false (browser).
3168
- */
3169
- __publicField(this, "headless", false);
3170
- this.container = container;
3171
- this.root = createDiv("graphein-root", {
3172
- position: "relative",
3173
- width: "100%",
3174
- height: "100%",
3175
- overflow: "hidden"
3176
- });
3177
- const marksCanvas = document.createElement("canvas");
3178
- marksCanvas.className = "graphein-layer-marks";
3179
- setStyle(marksCanvas, { ...fillParentStyle });
3180
- const interactionCanvas = document.createElement("canvas");
3181
- interactionCanvas.className = "graphein-layer-interaction";
3182
- setStyle(interactionCanvas, { ...fillParentStyle, pointerEvents: "none" });
3183
- this.overlay = createDiv("graphein-layer-overlay", {
3184
- ...fillParentStyle,
3185
- width: "100%",
3186
- height: "100%",
3187
- pointerEvents: "none"
3188
- });
3189
- this.a11y = createDiv("graphein-a11y", {
3190
- position: "absolute",
3191
- width: "1px",
3192
- height: "1px",
3193
- overflow: "hidden",
3194
- clip: "rect(0 0 0 0)",
3195
- clipPath: "inset(50%)",
3196
- whiteSpace: "nowrap",
3197
- border: "0",
3198
- margin: "-1px",
3199
- padding: "0"
3200
- });
3201
- this.root.appendChild(marksCanvas);
3202
- this.root.appendChild(interactionCanvas);
3203
- this.root.appendChild(this.overlay);
3204
- this.root.appendChild(this.a11y);
3205
- container.appendChild(this.root);
3206
- this.marks = new CanvasLayer(marksCanvas);
3207
- this.interaction = new CanvasLayer(interactionCanvas);
3260
+ // src/spec/transform/expr/tokenize.ts
3261
+ var MULTI_OPS = ["===", "!==", "==", "!=", "<=", ">=", "&&", "||"];
3262
+ var SINGLE_OPS = new Set("+-*/%!<>?:.,()[]".split(""));
3263
+ function isDigit(ch) {
3264
+ return ch >= "0" && ch <= "9";
3265
+ }
3266
+ function isIdentStart(ch) {
3267
+ return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_" || ch === "$";
3268
+ }
3269
+ function isIdentPart(ch) {
3270
+ return isIdentStart(ch) || isDigit(ch);
3271
+ }
3272
+ function tokenize(src) {
3273
+ const tokens = [];
3274
+ let i = 0;
3275
+ const n = src.length;
3276
+ while (i < n) {
3277
+ const ch = src[i];
3278
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
3279
+ i++;
3280
+ continue;
3281
+ }
3282
+ if (ch === '"' || ch === "'") {
3283
+ const start = i;
3284
+ const quote = ch;
3285
+ i++;
3286
+ let out = "";
3287
+ while (i < n && src[i] !== quote) {
3288
+ if (src[i] === "\\") {
3289
+ const next = src[i + 1];
3290
+ if (next === void 0) throw new ExpressionError("Unterminated string literal.", start);
3291
+ out += next === "n" ? "\n" : next === "t" ? " " : next === "r" ? "\r" : next;
3292
+ i += 2;
3293
+ } else {
3294
+ out += src[i];
3295
+ i++;
3296
+ }
3297
+ }
3298
+ if (i >= n) throw new ExpressionError("Unterminated string literal.", start);
3299
+ i++;
3300
+ tokens.push({ type: "str", value: out, pos: start });
3301
+ continue;
3302
+ }
3303
+ if (isDigit(ch) || ch === "." && isDigit(src[i + 1] ?? "")) {
3304
+ const start = i;
3305
+ while (i < n && isDigit(src[i])) i++;
3306
+ if (src[i] === ".") {
3307
+ i++;
3308
+ while (i < n && isDigit(src[i])) i++;
3309
+ }
3310
+ if (src[i] === "e" || src[i] === "E") {
3311
+ i++;
3312
+ if (src[i] === "+" || src[i] === "-") i++;
3313
+ if (!isDigit(src[i] ?? "")) throw new ExpressionError("Malformed number literal.", start);
3314
+ while (i < n && isDigit(src[i])) i++;
3315
+ }
3316
+ tokens.push({ type: "num", value: Number(src.slice(start, i)), pos: start });
3317
+ continue;
3318
+ }
3319
+ if (isIdentStart(ch)) {
3320
+ const start = i;
3321
+ i++;
3322
+ while (i < n && isIdentPart(src[i])) i++;
3323
+ tokens.push({ type: "ident", value: src.slice(start, i), pos: start });
3324
+ continue;
3325
+ }
3326
+ const three = src.slice(i, i + 3);
3327
+ const two = src.slice(i, i + 2);
3328
+ const multi = MULTI_OPS.find((op) => op === three) ?? MULTI_OPS.find((op) => op === two);
3329
+ if (multi) {
3330
+ tokens.push({ type: "op", value: multi, pos: i });
3331
+ i += multi.length;
3332
+ continue;
3333
+ }
3334
+ if (SINGLE_OPS.has(ch)) {
3335
+ tokens.push({ type: "op", value: ch, pos: i });
3336
+ i++;
3337
+ continue;
3338
+ }
3339
+ throw new ExpressionError(`Unexpected character ${JSON.stringify(ch)}.`, i);
3208
3340
  }
3209
- resize(width, height, dpr = getDevicePixelRatio()) {
3210
- this.width = width;
3211
- this.height = height;
3212
- this.dpr = dpr;
3213
- this.marks.resize(width, height, dpr);
3214
- this.interaction.resize(width, height, dpr);
3215
- setStyle(this.overlay, { width: `${width}px`, height: `${height}px` });
3341
+ tokens.push({ type: "eof", value: "", pos: n });
3342
+ return tokens;
3343
+ }
3344
+
3345
+ // src/spec/transform/expr/parse.ts
3346
+ var EQUALITY = /* @__PURE__ */ new Set(["==", "!=", "===", "!=="]);
3347
+ var RELATIONAL = /* @__PURE__ */ new Set(["<", "<=", ">", ">="]);
3348
+ var ADDITIVE = /* @__PURE__ */ new Set(["+", "-"]);
3349
+ var MULTIPLICATIVE = /* @__PURE__ */ new Set(["*", "/", "%"]);
3350
+ var Parser = class {
3351
+ constructor(tokens) {
3352
+ __publicField(this, "tokens");
3353
+ __publicField(this, "pos", 0);
3354
+ this.tokens = tokens;
3216
3355
  }
3217
- /** Clear both canvas layers and empty the HTML overlay. */
3218
- clear() {
3219
- this.marks.clear();
3220
- this.interaction.clear();
3221
- this.overlay.replaceChildren();
3356
+ peek() {
3357
+ return this.tokens[this.pos];
3222
3358
  }
3223
- /** Remove all DOM created by this Surface. */
3224
- destroy() {
3225
- this.clear();
3226
- this.root.remove();
3359
+ next() {
3360
+ return this.tokens[this.pos++];
3227
3361
  }
3228
- };
3229
-
3230
- // src/render/overlayText.ts
3231
- function paintCanvasText(ctx, c) {
3232
- const align = c.align ?? "left";
3233
- const baseline = c.baseline ?? "top";
3234
- ctx.save();
3235
- if (c.opacity != null) ctx.globalAlpha *= c.opacity;
3236
- if (c.rotate) {
3237
- ctx.translate(c.x, c.y);
3238
- ctx.rotate(c.rotate);
3239
- ctx.translate(-c.x, -c.y);
3362
+ isOp(value) {
3363
+ const t = this.peek();
3364
+ return t.type === "op" && t.value === value;
3240
3365
  }
3241
- ctx.font = c.font;
3242
- ctx.textAlign = align;
3243
- ctx.textBaseline = baseline;
3244
- if (c.pill) {
3245
- const padX = c.pill.padX ?? 7;
3246
- const padY = c.pill.padY ?? 2;
3247
- const tw = ctx.measureText(c.text).width;
3248
- const th = c.size;
3249
- let bx = c.x;
3250
- if (align === "center") bx = c.x - tw / 2;
3251
- else if (align === "right") bx = c.x - tw;
3252
- let by = c.y;
3253
- if (baseline === "middle") by = c.y - th / 2;
3254
- else if (baseline === "alphabetic") by = c.y - th;
3255
- ctx.beginPath();
3256
- roundedRect(ctx, bx - padX, by - padY, tw + padX * 2, th + padY * 2, c.pill.radius ?? 999);
3257
- ctx.fillStyle = c.pill.background;
3258
- ctx.fill();
3259
- if (c.pill.border) {
3260
- ctx.lineWidth = 1;
3261
- ctx.strokeStyle = c.pill.border;
3262
- ctx.stroke();
3366
+ eat(value) {
3367
+ if (!this.isOp(value)) {
3368
+ const t = this.peek();
3369
+ throw new ExpressionError(`Expected "${value}" but found ${describe(t)}.`, t.pos);
3263
3370
  }
3371
+ this.pos++;
3264
3372
  }
3265
- ctx.fillStyle = c.color;
3266
- ctx.fillText(c.text, c.x, c.y);
3267
- ctx.restore();
3268
- }
3269
- function overlayTextToCanvasCmd(o, font) {
3270
- const t = o.transform ?? "";
3271
- const rotate = t.includes("rotate(-90deg)") ? -Math.PI / 2 : t.includes("rotate(90deg)") ? Math.PI / 2 : (() => {
3272
- const m = t.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
3273
- return m ? parseFloat(m[1]) * Math.PI / 180 : void 0;
3274
- })();
3275
- let align = "left";
3276
- let x = o.left;
3277
- const y = o.top;
3278
- if (o.width != null) {
3279
- align = o.align ?? "left";
3280
- x = align === "right" ? o.left + o.width : align === "center" ? o.left + o.width / 2 : o.left;
3281
- } else if (t.includes("translateX(-100%)")) {
3282
- align = "right";
3283
- } else if (t.includes("translateX(-50%)") || t.includes("translate(-50%")) {
3284
- align = "center";
3373
+ parse() {
3374
+ const node = this.parseTernary();
3375
+ const t = this.peek();
3376
+ if (t.type !== "eof") throw new ExpressionError(`Unexpected ${describe(t)}.`, t.pos);
3377
+ return node;
3285
3378
  }
3286
- const baseline = t.includes("translateY(-50%)") || t.includes(",-50%") || t.includes(", -50%") ? "middle" : "top";
3287
- return {
3288
- x,
3289
- y,
3290
- text: o.text,
3291
- font,
3292
- color: o.color,
3293
- size: o.size,
3294
- align,
3295
- baseline,
3296
- rotate,
3297
- opacity: o.opacity,
3298
- pill: o.pill
3299
- };
3300
- }
3301
- function legendSwatchWidth(symbol, swatch = 11) {
3302
- return symbol === "line" ? swatch + 3 : swatch;
3303
- }
3304
- function paintLegendSwatch(ctx, x, midY, symbol, color, swatch = 11) {
3305
- ctx.save();
3306
- ctx.fillStyle = color;
3307
- ctx.beginPath();
3308
- if (symbol === "circle") {
3309
- ctx.arc(x + swatch / 2, midY, swatch / 2, 0, Math.PI * 2);
3310
- } else if (symbol === "line") {
3311
- roundedRect(ctx, x, midY - 1.5, swatch + 3, 3, 1.5);
3312
- } else {
3313
- roundedRect(ctx, x, midY - swatch / 2, swatch, swatch, 2);
3379
+ parseTernary() {
3380
+ const test = this.parseBinary(0);
3381
+ if (this.isOp("?")) {
3382
+ this.eat("?");
3383
+ const then = this.parseTernary();
3384
+ this.eat(":");
3385
+ const alt = this.parseTernary();
3386
+ return { type: "ternary", test, then, alt };
3387
+ }
3388
+ return test;
3314
3389
  }
3315
- ctx.fill();
3316
- ctx.restore();
3317
- }
3318
-
3319
- // src/render/text.ts
3320
- var measureCtx;
3321
- var injectedCtx = null;
3322
- function setMeasureContext(ctx) {
3323
- injectedCtx = ctx;
3324
- }
3325
- function getMeasureCtx() {
3326
- if (injectedCtx) return injectedCtx;
3327
- if (measureCtx !== void 0) return measureCtx;
3328
- try {
3329
- measureCtx = document.createElement("canvas").getContext("2d");
3330
- } catch {
3331
- measureCtx = null;
3390
+ /** Precedence-climbing for the binary/logical operator levels. */
3391
+ parseBinary(minLevel) {
3392
+ let left = this.parseUnary();
3393
+ for (; ; ) {
3394
+ const t = this.peek();
3395
+ if (t.type !== "op") break;
3396
+ const level = binaryLevel(t.value);
3397
+ if (level < minLevel || level < 0) break;
3398
+ this.next();
3399
+ const right = this.parseBinary(level + 1);
3400
+ const op = t.value;
3401
+ left = op === "&&" || op === "||" ? { type: "logical", op, left, right } : { type: "binary", op, left, right };
3402
+ }
3403
+ return left;
3332
3404
  }
3333
- return measureCtx;
3334
- }
3335
- function measureText(text, font) {
3336
- const ctx = getMeasureCtx();
3337
- if (!ctx) {
3338
- const sizeMatch = /(\d+(?:\.\d+)?)px/.exec(font);
3339
- const size = sizeMatch ? parseFloat(sizeMatch[1]) : 12;
3340
- return { width: text.length * size * 0.55 };
3405
+ parseUnary() {
3406
+ const t = this.peek();
3407
+ if (t.type === "op" && (t.value === "!" || t.value === "-" || t.value === "+")) {
3408
+ this.next();
3409
+ return { type: "unary", op: t.value, arg: this.parseUnary() };
3410
+ }
3411
+ return this.parsePostfix();
3341
3412
  }
3342
- ctx.font = font;
3343
- return { width: ctx.measureText(text).width };
3344
- }
3345
- function fontString(size, family, weight = 400, style = "normal") {
3346
- return `${style} ${weight} ${size}px ${family}`;
3347
- }
3348
- function ellipsize(text, maxWidth, font) {
3349
- if (maxWidth <= 0 || text.length === 0) return "";
3350
- if (measureText(text, font).width <= maxWidth) return text;
3351
- const ellipsis = "\u2026";
3352
- const ellipsisW = measureText(ellipsis, font).width;
3353
- if (ellipsisW > maxWidth) return "";
3354
- let lo = 0;
3355
- let hi = text.length;
3356
- while (lo < hi) {
3357
- const mid = Math.ceil((lo + hi) / 2);
3358
- if (measureText(text.slice(0, mid) + ellipsis, font).width <= maxWidth) lo = mid;
3359
- else hi = mid - 1;
3413
+ parsePostfix() {
3414
+ let node = this.parsePrimary();
3415
+ for (; ; ) {
3416
+ if (this.isOp(".")) {
3417
+ this.eat(".");
3418
+ const t = this.next();
3419
+ if (t.type !== "ident") throw new ExpressionError(`Expected a property name after ".".`, t.pos);
3420
+ node = { type: "member", obj: node, prop: { type: "str", value: t.value }, computed: false };
3421
+ } else if (this.isOp("[")) {
3422
+ this.eat("[");
3423
+ const prop = this.parseTernary();
3424
+ this.eat("]");
3425
+ node = { type: "member", obj: node, prop, computed: true };
3426
+ } else {
3427
+ break;
3428
+ }
3429
+ }
3430
+ return node;
3360
3431
  }
3361
- return text.slice(0, lo) + ellipsis;
3362
- }
3363
-
3364
- // src/theme/tokens.ts
3365
- var PALETTE = [
3366
- "#3b82f6",
3367
- // blue
3368
- "#14b8a6",
3369
- // teal
3370
- "#f59e0b",
3371
- // amber
3372
- "#ef4444",
3373
- // red
3374
- "#8b5cf6",
3375
- // violet
3376
- "#ec4899",
3377
- // pink
3378
- "#10b981",
3379
- // emerald
3380
- "#f97316",
3381
- // orange
3382
- "#6366f1",
3383
- // indigo
3384
- "#84cc16"
3385
- // lime
3386
- ];
3387
- var FONT_FAMILY = 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
3388
- var SHARED = {
3389
- font: {
3390
- family: FONT_FAMILY,
3391
- size: { tiny: 10, small: 11, base: 12, large: 14, title: 16 },
3392
- weight: { normal: 400, medium: 500, bold: 600 }
3393
- },
3394
- spacing: { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 },
3395
- radius: { sm: 4, md: 6, lg: 8 },
3396
- stroke: { thin: 1, base: 1.5, thick: 2 }
3397
- };
3398
- var lightTheme = {
3399
- name: "light",
3400
- dark: false,
3401
- color: {
3402
- background: "#ffffff",
3403
- surface: "#f8fafc",
3404
- text: "#0f172a",
3405
- textMuted: "#64748b",
3406
- axis: "#cbd5e1",
3407
- grid: "#eef2f6",
3408
- border: "#e2e8f0",
3409
- accent: "#0d9488",
3410
- palette: PALETTE,
3411
- positive: "#16a34a",
3412
- negative: "#dc2626"
3413
- },
3414
- ...SHARED
3415
- };
3416
- var darkTheme = {
3417
- name: "dark",
3418
- dark: true,
3419
- color: {
3420
- background: "#0b1220",
3421
- surface: "#111a2e",
3422
- text: "#f1f5f9",
3423
- textMuted: "#94a3b8",
3424
- axis: "#334155",
3425
- grid: "#1c2740",
3426
- border: "#1e293b",
3427
- accent: "#2dd4bf",
3428
- palette: PALETTE,
3429
- positive: "#22c55e",
3430
- negative: "#f87171"
3431
- },
3432
- ...SHARED
3433
- };
3434
- var themes = {
3435
- light: lightTheme,
3436
- dark: darkTheme
3437
- };
3438
- function isPlainObject(v) {
3439
- return typeof v === "object" && v !== null && !Array.isArray(v);
3440
- }
3441
- function deepMerge(base, override) {
3442
- if (!isPlainObject(override)) return base;
3443
- if (!isPlainObject(base)) return override;
3444
- const out = { ...base };
3445
- for (const [key, value] of Object.entries(override)) {
3446
- const baseValue = out[key];
3447
- if (isPlainObject(baseValue) && isPlainObject(value)) {
3448
- out[key] = deepMerge(baseValue, value);
3449
- } else {
3450
- out[key] = value;
3432
+ parsePrimary() {
3433
+ const t = this.next();
3434
+ if (t.type === "num") return { type: "num", value: t.value };
3435
+ if (t.type === "str") return { type: "str", value: t.value };
3436
+ if (t.type === "ident") {
3437
+ const name = t.value;
3438
+ if (name === "true") return { type: "lit", value: true };
3439
+ if (name === "false") return { type: "lit", value: false };
3440
+ if (name === "null") return { type: "lit", value: null };
3441
+ if (this.isOp("(")) {
3442
+ this.eat("(");
3443
+ const args = [];
3444
+ if (!this.isOp(")")) {
3445
+ args.push(this.parseTernary());
3446
+ while (this.isOp(",")) {
3447
+ this.eat(",");
3448
+ args.push(this.parseTernary());
3449
+ }
3450
+ }
3451
+ this.eat(")");
3452
+ return { type: "call", callee: name, args };
3453
+ }
3454
+ if (name === "datum") return { type: "datum" };
3455
+ return { type: "field", name };
3451
3456
  }
3457
+ if (t.type === "op" && t.value === "(") {
3458
+ const node = this.parseTernary();
3459
+ this.eat(")");
3460
+ return node;
3461
+ }
3462
+ throw new ExpressionError(`Unexpected ${describe(t)}.`, t.pos);
3452
3463
  }
3453
- return out;
3464
+ };
3465
+ function binaryLevel(op) {
3466
+ if (op === "||") return 1;
3467
+ if (op === "&&") return 2;
3468
+ if (EQUALITY.has(op)) return 3;
3469
+ if (RELATIONAL.has(op)) return 4;
3470
+ if (ADDITIVE.has(op)) return 5;
3471
+ if (MULTIPLICATIVE.has(op)) return 6;
3472
+ return -1;
3454
3473
  }
3455
- function resolveTheme(theme) {
3456
- if (theme == null) return lightTheme;
3457
- if (typeof theme === "string") return themes[theme] ?? lightTheme;
3458
- const baseName = theme.base ?? (theme.dark ? "dark" : "light");
3459
- const base = themes[baseName] ?? lightTheme;
3460
- const { base: _ignored, ...override } = theme;
3461
- return deepMerge(base, override);
3474
+ function describe(t) {
3475
+ if (t.type === "eof") return "end of input";
3476
+ if (t.type === "str") return `string ${JSON.stringify(t.value)}`;
3477
+ if (t.type === "num") return `number ${t.value}`;
3478
+ return `"${t.value}"`;
3479
+ }
3480
+ function parse(src) {
3481
+ if (typeof src !== "string" || src.trim() === "") {
3482
+ throw new ExpressionError("Expression must be a non-empty string.", 0);
3483
+ }
3484
+ return new Parser(tokenize(src)).parse();
3462
3485
  }
3463
3486
 
3464
- // src/theme/sketch.ts
3465
- var SKETCH_FONT_NAME = "Patrick Hand";
3466
- var SKETCH_FONT_FAMILY = `'Patrick Hand', 'Segoe Print', 'Bradley Hand', 'Comic Sans MS', ui-rounded, cursive`;
3467
- var injected = false;
3468
- function injectViaStyle(dataUrl) {
3469
- const style = document.createElement("style");
3470
- style.setAttribute("data-graphein-sketch-font", "");
3471
- style.textContent = `@font-face{font-family:'${SKETCH_FONT_NAME}';font-style:normal;font-weight:400;font-display:swap;src:url("${dataUrl}") format("woff2");}`;
3472
- document.head.appendChild(style);
3487
+ // src/spec/transform/expr/evaluate.ts
3488
+ var BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3489
+ function truthy(v) {
3490
+ if (typeof v === "number") return v !== 0 && !Number.isNaN(v);
3491
+ return Boolean(v);
3473
3492
  }
3474
- async function ensureSketchFont() {
3475
- if (injected) return false;
3476
- if (typeof document === "undefined") return false;
3477
- injected = true;
3478
- try {
3479
- const { PATRICK_HAND_DATA_URL } = await import('./patrickHand-ITDQJJE2.js');
3480
- const docFonts = document.fonts;
3481
- if (typeof FontFace !== "undefined" && docFonts?.add) {
3482
- const face = new FontFace(
3483
- SKETCH_FONT_NAME,
3484
- `url("${PATRICK_HAND_DATA_URL}") format("woff2")`,
3485
- { style: "normal", weight: "400" }
3486
- );
3487
- await face.load();
3488
- docFonts.add(face);
3489
- return true;
3490
- }
3491
- injectViaStyle(PATRICK_HAND_DATA_URL);
3492
- return true;
3493
- } catch {
3494
- injected = false;
3495
- return false;
3496
- }
3493
+ function toStr(v) {
3494
+ if (v == null) return "";
3495
+ if (v instanceof Date) return v.toISOString();
3496
+ return String(v);
3497
3497
  }
3498
- function withSketchFont(tokens) {
3499
- return {
3500
- ...tokens,
3501
- font: {
3502
- ...tokens.font,
3503
- family: `${SKETCH_FONT_FAMILY}`
3504
- }
3505
- };
3498
+ function looseEquals(a, b) {
3499
+ if (a == null || b == null) return a == null && b == null;
3500
+ const na = toNumber(a);
3501
+ const nb = toNumber(b);
3502
+ if (Number.isFinite(na) && Number.isFinite(nb)) return na === nb;
3503
+ return toStr(a) === toStr(b);
3506
3504
  }
3507
-
3508
- // src/spec/types.ts
3509
- var SLICER_TYPES = [
3510
- "dropdown",
3511
- "search",
3512
- "list",
3513
- "range",
3514
- "dateRange"
3515
- ];
3516
- var CHART_TYPES = [
3517
- "line",
3518
- "area",
3519
- "bar",
3520
- "scatter",
3521
- "combo",
3522
- "histogram",
3523
- "pie",
3524
- "heatmap",
3525
- "funnel",
3526
- "kpi",
3527
- "treemap",
3528
- "gauge",
3529
- "bullet",
3530
- "calendarHeatmap",
3531
- "waterfall",
3532
- "slope",
3533
- "dumbbell",
3534
- "table",
3535
- "matrix",
3536
- "box",
3537
- "sankey",
3538
- "choropleth",
3539
- "dropdown",
3540
- "search",
3541
- "list",
3542
- "range",
3543
- "dateRange"
3544
- ];
3545
-
3546
- // src/spec/transform/types.ts
3547
- var TRANSFORM_KINDS = ["aggregate", "bin", "filter", "fold", "timeUnit", "calculate"];
3548
- var TIME_UNITS = [
3549
- "year",
3550
- "quarter",
3551
- "month",
3552
- "week",
3553
- "day",
3554
- "hour",
3555
- "minute",
3556
- "second"
3557
- ];
3558
-
3559
- // src/spec/transform/aggregate.ts
3560
- function applyAggregate(transform2, data) {
3561
- const groupby = transform2.groupby ?? [];
3562
- const reads = groupby.map((f) => accessor(f));
3563
- const emit = (keyValues, rows) => {
3564
- const out2 = {};
3565
- groupby.forEach((field, i) => {
3566
- out2[field] = keyValues[i];
3567
- });
3568
- for (const agg of transform2.aggregate) {
3569
- const values = agg.field === void 0 ? rows : rows.map((r) => r[agg.field]);
3570
- out2[agg.as] = aggregateValues(values, agg.op);
3505
+ function compare(a, b) {
3506
+ const na = toNumber(a);
3507
+ const nb = toNumber(b);
3508
+ if (Number.isFinite(na) && Number.isFinite(nb)) return na < nb ? -1 : na > nb ? 1 : 0;
3509
+ const sa = toStr(a);
3510
+ const sb = toStr(b);
3511
+ return sa < sb ? -1 : sa > sb ? 1 : 0;
3512
+ }
3513
+ var nums = (args) => args.map(toNumber);
3514
+ var FUNCTIONS = {
3515
+ abs: (a) => Math.abs(toNumber(a[0])),
3516
+ round: (a) => {
3517
+ const d = a.length > 1 ? Math.trunc(toNumber(a[1])) : 0;
3518
+ const f = 10 ** d;
3519
+ return Math.round(toNumber(a[0]) * f) / f;
3520
+ },
3521
+ floor: (a) => Math.floor(toNumber(a[0])),
3522
+ ceil: (a) => Math.ceil(toNumber(a[0])),
3523
+ trunc: (a) => Math.trunc(toNumber(a[0])),
3524
+ sign: (a) => Math.sign(toNumber(a[0])),
3525
+ sqrt: (a) => Math.sqrt(toNumber(a[0])),
3526
+ exp: (a) => Math.exp(toNumber(a[0])),
3527
+ log: (a) => Math.log(toNumber(a[0])),
3528
+ log10: (a) => Math.log10(toNumber(a[0])),
3529
+ log2: (a) => Math.log2(toNumber(a[0])),
3530
+ pow: (a) => toNumber(a[0]) ** toNumber(a[1]),
3531
+ min: (a) => Math.min(...nums(a)),
3532
+ max: (a) => Math.max(...nums(a)),
3533
+ number: (a) => toNumber(a[0]),
3534
+ isFinite: (a) => Number.isFinite(toNumber(a[0])),
3535
+ isNaN: (a) => Number.isNaN(toNumber(a[0])),
3536
+ str: (a) => toStr(a[0]),
3537
+ lower: (a) => toStr(a[0]).toLowerCase(),
3538
+ upper: (a) => toStr(a[0]).toUpperCase(),
3539
+ trim: (a) => toStr(a[0]).trim(),
3540
+ length: (a) => Array.isArray(a[0]) ? a[0].length : toStr(a[0]).length,
3541
+ substring: (a) => {
3542
+ const s = toStr(a[0]);
3543
+ const start = Math.trunc(toNumber(a[1]));
3544
+ return a.length > 2 ? s.substring(start, Math.trunc(toNumber(a[2]))) : s.substring(start);
3545
+ },
3546
+ replace: (a) => toStr(a[0]).split(toStr(a[1])).join(toStr(a[2])),
3547
+ contains: (a) => toStr(a[0]).includes(toStr(a[1])),
3548
+ startsWith: (a) => toStr(a[0]).startsWith(toStr(a[1])),
3549
+ endsWith: (a) => toStr(a[0]).endsWith(toStr(a[1])),
3550
+ concat: (a) => a.map(toStr).join(""),
3551
+ coalesce: (a) => a.find((v) => v != null),
3552
+ if: (a) => truthy(a[0]) ? a[1] : a[2],
3553
+ year: (a) => toDate(a[0])?.getFullYear() ?? NaN,
3554
+ month: (a) => {
3555
+ const d = toDate(a[0]);
3556
+ return d ? d.getMonth() + 1 : NaN;
3557
+ },
3558
+ day: (a) => toDate(a[0])?.getDate() ?? NaN,
3559
+ hours: (a) => toDate(a[0])?.getHours() ?? NaN,
3560
+ minutes: (a) => toDate(a[0])?.getMinutes() ?? NaN
3561
+ };
3562
+ var FUNCTION_NAMES = Object.keys(FUNCTIONS);
3563
+ function evalNode(node, row) {
3564
+ switch (node.type) {
3565
+ case "num":
3566
+ case "str":
3567
+ case "lit":
3568
+ return node.value;
3569
+ case "datum":
3570
+ return row;
3571
+ case "field":
3572
+ return row[node.name];
3573
+ case "unary": {
3574
+ const v = evalNode(node.arg, row);
3575
+ if (node.op === "!") return !truthy(v);
3576
+ if (node.op === "-") return -toNumber(v);
3577
+ return toNumber(v);
3571
3578
  }
3572
- return out2;
3573
- };
3574
- if (groupby.length === 0) {
3575
- return [emit([], data)];
3576
- }
3577
- const groups = /* @__PURE__ */ new Map();
3578
- for (const row of data) {
3579
- const keyValues = reads.map((read) => read(row));
3580
- const key = keyValues.map((v) => toKey(v)).join("\0");
3581
- let group = groups.get(key);
3582
- if (group === void 0) {
3583
- group = { keys: keyValues, rows: [] };
3584
- groups.set(key, group);
3579
+ case "logical": {
3580
+ const left = evalNode(node.left, row);
3581
+ if (node.op === "&&") return truthy(left) ? evalNode(node.right, row) : left;
3582
+ return truthy(left) ? left : evalNode(node.right, row);
3583
+ }
3584
+ case "ternary":
3585
+ return truthy(evalNode(node.test, row)) ? evalNode(node.then, row) : evalNode(node.alt, row);
3586
+ case "binary":
3587
+ return evalBinary(node.op, evalNode(node.left, row), evalNode(node.right, row));
3588
+ case "member": {
3589
+ const obj = evalNode(node.obj, row);
3590
+ if (obj == null || typeof obj !== "object") return void 0;
3591
+ const key = node.computed ? toStr(evalNode(node.prop, row)) : node.prop.value;
3592
+ if (BLOCKED_KEYS.has(key)) return void 0;
3593
+ return obj[key];
3594
+ }
3595
+ case "call": {
3596
+ const fn = Object.prototype.hasOwnProperty.call(FUNCTIONS, node.callee) ? FUNCTIONS[node.callee] : void 0;
3597
+ if (!fn) throw new ExpressionError(`Unknown function "${node.callee}".`);
3598
+ return fn(node.args.map((a) => evalNode(a, row)));
3585
3599
  }
3586
- group.rows.push(row);
3587
3600
  }
3588
- const out = [];
3589
- for (const group of groups.values()) {
3590
- out.push(emit(group.keys, group.rows));
3601
+ }
3602
+ function evalBinary(op, a, b) {
3603
+ switch (op) {
3604
+ case "+":
3605
+ return typeof a === "string" || typeof b === "string" ? toStr(a) + toStr(b) : toNumber(a) + toNumber(b);
3606
+ case "-":
3607
+ return toNumber(a) - toNumber(b);
3608
+ case "*":
3609
+ return toNumber(a) * toNumber(b);
3610
+ case "/":
3611
+ return toNumber(a) / toNumber(b);
3612
+ case "%":
3613
+ return toNumber(a) % toNumber(b);
3614
+ case "<":
3615
+ return compare(a, b) < 0;
3616
+ case "<=":
3617
+ return compare(a, b) <= 0;
3618
+ case ">":
3619
+ return compare(a, b) > 0;
3620
+ case ">=":
3621
+ return compare(a, b) >= 0;
3622
+ case "==":
3623
+ return looseEquals(a, b);
3624
+ case "!=":
3625
+ return !looseEquals(a, b);
3626
+ case "===":
3627
+ return a === b;
3628
+ case "!==":
3629
+ return a !== b;
3630
+ default:
3631
+ throw new ExpressionError(`Unknown operator "${op}".`);
3591
3632
  }
3592
- return out;
3633
+ }
3634
+ function evaluate(node, row) {
3635
+ return evalNode(node, row);
3593
3636
  }
3594
3637
 
3595
- // src/spec/transform/bin.ts
3596
- function roundNice(value, step) {
3597
- if (!Number.isFinite(step) || step === 0) return value;
3598
- const places = Math.max(0, -Math.floor(Math.log10(Math.abs(step))) + 1);
3599
- const rounded = Number(value.toFixed(Math.min(places, 15)));
3600
- return Object.is(rounded, -0) ? 0 : rounded;
3638
+ // src/spec/transform/expr/index.ts
3639
+ function compileExpression(src) {
3640
+ const ast = parse(src);
3641
+ return (row) => evaluate(ast, row);
3601
3642
  }
3602
- function computeBins(values, opts = {}) {
3603
- let min = Infinity;
3604
- let max = -Infinity;
3605
- if (opts.extent) {
3606
- [min, max] = opts.extent;
3607
- } else {
3608
- for (const v of values) {
3609
- if (!Number.isFinite(v)) continue;
3610
- if (v < min) min = v;
3611
- if (v > max) max = v;
3612
- }
3613
- }
3614
- if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
3615
- const explicit = opts.step !== void 0 && opts.step > 0;
3616
- if (min === max) {
3617
- const step2 = explicit ? opts.step : 1;
3618
- return { start: min, step: step2, count: 1 };
3643
+ function checkExpression(src) {
3644
+ try {
3645
+ parse(src);
3646
+ return null;
3647
+ } catch (e) {
3648
+ if (e instanceof ExpressionError) return { message: e.message, pos: e.pos };
3649
+ throw e;
3619
3650
  }
3620
- const nice = opts.nice !== false;
3621
- let step = explicit ? opts.step : tickStep(min, max, opts.maxbins ?? 10);
3622
- if (!Number.isFinite(step) || step <= 0) step = max - min || 1;
3623
- const start = nice ? Math.floor(min / step) * step : min;
3624
- const end = nice ? Math.ceil(max / step) * step : max;
3625
- const count = Math.max(1, Math.ceil((end - start) / step - 1e-9));
3626
- return { start: roundNice(start, step), step, count };
3627
3651
  }
3628
- function applyBin(transform2, data) {
3629
- const read = accessor(transform2.bin);
3630
- const finite4 = [];
3631
- for (const row of data) {
3632
- const n = toNumber(read(row));
3633
- if (Number.isFinite(n)) finite4.push(n);
3634
- }
3635
- const layout2 = computeBins(finite4, {
3636
- maxbins: transform2.maxbins,
3637
- step: transform2.step,
3638
- extent: transform2.extent,
3639
- nice: transform2.nice
3640
- });
3641
- const startField = Array.isArray(transform2.as) ? transform2.as[0] : transform2.as;
3642
- const endField = Array.isArray(transform2.as) ? transform2.as[1] : void 0;
3643
- return data.map((row) => {
3644
- const out = { ...row };
3645
- const v = toNumber(read(row));
3646
- const outside = transform2.extent != null && (v < transform2.extent[0] || v > transform2.extent[1]);
3647
- if (layout2 == null || !Number.isFinite(v) || outside) {
3648
- out[startField] = null;
3649
- if (endField) out[endField] = null;
3650
- return out;
3651
- }
3652
- let index = Math.floor((v - layout2.start) / layout2.step + 1e-9);
3653
- if (index < 0) index = 0;
3654
- if (index > layout2.count - 1) index = layout2.count - 1;
3655
- out[startField] = roundNice(layout2.start + index * layout2.step, layout2.step);
3656
- if (endField) out[endField] = roundNice(layout2.start + (index + 1) * layout2.step, layout2.step);
3657
- return out;
3658
- });
3659
- }
3660
-
3661
- // src/spec/transform/expr/error.ts
3662
- var ExpressionError = class extends Error {
3663
- constructor(message, pos) {
3664
- super(message);
3665
- /** Source offset where the problem was detected, when known. */
3666
- __publicField(this, "pos");
3667
- this.name = "ExpressionError";
3668
- this.pos = pos;
3669
- }
3670
- };
3671
3652
 
3672
- // src/spec/transform/expr/tokenize.ts
3673
- var MULTI_OPS = ["===", "!==", "==", "!=", "<=", ">=", "&&", "||"];
3674
- var SINGLE_OPS = new Set("+-*/%!<>?:.,()[]".split(""));
3675
- function isDigit(ch) {
3676
- return ch >= "0" && ch <= "9";
3677
- }
3678
- function isIdentStart(ch) {
3679
- return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_" || ch === "$";
3680
- }
3681
- function isIdentPart(ch) {
3682
- return isIdentStart(ch) || isDigit(ch);
3683
- }
3684
- function tokenize(src) {
3685
- const tokens = [];
3686
- let i = 0;
3687
- const n = src.length;
3688
- while (i < n) {
3689
- const ch = src[i];
3690
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
3691
- i++;
3692
- continue;
3693
- }
3694
- if (ch === '"' || ch === "'") {
3695
- const start = i;
3696
- const quote = ch;
3697
- i++;
3698
- let out = "";
3699
- while (i < n && src[i] !== quote) {
3700
- if (src[i] === "\\") {
3701
- const next = src[i + 1];
3702
- if (next === void 0) throw new ExpressionError("Unterminated string literal.", start);
3703
- out += next === "n" ? "\n" : next === "t" ? " " : next === "r" ? "\r" : next;
3704
- i += 2;
3705
- } else {
3706
- out += src[i];
3707
- i++;
3708
- }
3709
- }
3710
- if (i >= n) throw new ExpressionError("Unterminated string literal.", start);
3711
- i++;
3712
- tokens.push({ type: "str", value: out, pos: start });
3713
- continue;
3714
- }
3715
- if (isDigit(ch) || ch === "." && isDigit(src[i + 1] ?? "")) {
3716
- const start = i;
3717
- while (i < n && isDigit(src[i])) i++;
3718
- if (src[i] === ".") {
3719
- i++;
3720
- while (i < n && isDigit(src[i])) i++;
3721
- }
3722
- if (src[i] === "e" || src[i] === "E") {
3723
- i++;
3724
- if (src[i] === "+" || src[i] === "-") i++;
3725
- if (!isDigit(src[i] ?? "")) throw new ExpressionError("Malformed number literal.", start);
3726
- while (i < n && isDigit(src[i])) i++;
3727
- }
3728
- tokens.push({ type: "num", value: Number(src.slice(start, i)), pos: start });
3729
- continue;
3730
- }
3731
- if (isIdentStart(ch)) {
3732
- const start = i;
3733
- i++;
3734
- while (i < n && isIdentPart(src[i])) i++;
3735
- tokens.push({ type: "ident", value: src.slice(start, i), pos: start });
3736
- continue;
3737
- }
3738
- const three = src.slice(i, i + 3);
3739
- const two = src.slice(i, i + 2);
3740
- const multi = MULTI_OPS.find((op) => op === three) ?? MULTI_OPS.find((op) => op === two);
3741
- if (multi) {
3742
- tokens.push({ type: "op", value: multi, pos: i });
3743
- i += multi.length;
3744
- continue;
3745
- }
3746
- if (SINGLE_OPS.has(ch)) {
3747
- tokens.push({ type: "op", value: ch, pos: i });
3748
- i++;
3749
- continue;
3750
- }
3751
- throw new ExpressionError(`Unexpected character ${JSON.stringify(ch)}.`, i);
3752
- }
3753
- tokens.push({ type: "eof", value: "", pos: n });
3754
- return tokens;
3653
+ // src/spec/transform/calculate.ts
3654
+ function applyCalculate(transform2, data) {
3655
+ const evalRow = compileExpression(transform2.calculate);
3656
+ const as = transform2.as;
3657
+ return data.map((row) => ({ ...row, [as]: evalRow(row) }));
3755
3658
  }
3756
3659
 
3757
- // src/spec/transform/expr/parse.ts
3758
- var EQUALITY = /* @__PURE__ */ new Set(["==", "!=", "===", "!=="]);
3759
- var RELATIONAL = /* @__PURE__ */ new Set(["<", "<=", ">", ">="]);
3760
- var ADDITIVE = /* @__PURE__ */ new Set(["+", "-"]);
3761
- var MULTIPLICATIVE = /* @__PURE__ */ new Set(["*", "/", "%"]);
3762
- var Parser = class {
3763
- constructor(tokens) {
3764
- __publicField(this, "tokens");
3765
- __publicField(this, "pos", 0);
3766
- this.tokens = tokens;
3660
+ // src/spec/transform/filter.ts
3661
+ function toComparable(value) {
3662
+ if (typeof value === "number") return value;
3663
+ if (value instanceof Date) return value.getTime();
3664
+ const n = toNumber(value);
3665
+ if (!Number.isNaN(n)) return n;
3666
+ const d = toDate(value);
3667
+ return d ? d.getTime() : NaN;
3668
+ }
3669
+ function compilePredicate(pred) {
3670
+ if ("and" in pred) {
3671
+ const parts = pred.and.map(compilePredicate);
3672
+ return (row) => parts.every((p) => p(row));
3767
3673
  }
3768
- peek() {
3769
- return this.tokens[this.pos];
3674
+ if ("or" in pred) {
3675
+ const parts = pred.or.map(compilePredicate);
3676
+ return (row) => parts.some((p) => p(row));
3770
3677
  }
3771
- next() {
3772
- return this.tokens[this.pos++];
3678
+ if ("not" in pred) {
3679
+ const inner = compilePredicate(pred.not);
3680
+ return (row) => !inner(row);
3773
3681
  }
3774
- isOp(value) {
3775
- const t = this.peek();
3776
- return t.type === "op" && t.value === value;
3682
+ const read = accessor(pred.field);
3683
+ if ("equals" in pred) {
3684
+ const want = toKey(pred.equals);
3685
+ return (row) => toKey(read(row)) === want;
3777
3686
  }
3778
- eat(value) {
3779
- if (!this.isOp(value)) {
3780
- const t = this.peek();
3781
- throw new ExpressionError(`Expected "${value}" but found ${describe(t)}.`, t.pos);
3782
- }
3783
- this.pos++;
3687
+ if ("ne" in pred) {
3688
+ const want = toKey(pred.ne);
3689
+ return (row) => toKey(read(row)) !== want;
3784
3690
  }
3785
- parse() {
3786
- const node = this.parseTernary();
3787
- const t = this.peek();
3788
- if (t.type !== "eof") throw new ExpressionError(`Unexpected ${describe(t)}.`, t.pos);
3789
- return node;
3691
+ if ("oneOf" in pred) {
3692
+ const want = new Set(pred.oneOf.map((x) => toKey(x)));
3693
+ return (row) => want.has(toKey(read(row)));
3790
3694
  }
3791
- parseTernary() {
3792
- const test = this.parseBinary(0);
3793
- if (this.isOp("?")) {
3794
- this.eat("?");
3795
- const then = this.parseTernary();
3796
- this.eat(":");
3797
- const alt = this.parseTernary();
3798
- return { type: "ternary", test, then, alt };
3799
- }
3800
- return test;
3695
+ if ("contains" in pred) {
3696
+ const q = String(pred.contains).toLowerCase();
3697
+ return (row) => {
3698
+ const raw = read(row);
3699
+ return raw != null && String(raw).toLowerCase().includes(q);
3700
+ };
3801
3701
  }
3802
- /** Precedence-climbing for the binary/logical operator levels. */
3803
- parseBinary(minLevel) {
3804
- let left = this.parseUnary();
3805
- for (; ; ) {
3806
- const t = this.peek();
3807
- if (t.type !== "op") break;
3808
- const level = binaryLevel(t.value);
3809
- if (level < minLevel || level < 0) break;
3810
- this.next();
3811
- const right = this.parseBinary(level + 1);
3812
- const op = t.value;
3813
- left = op === "&&" || op === "||" ? { type: "logical", op, left, right } : { type: "binary", op, left, right };
3814
- }
3815
- return left;
3702
+ if ("valid" in pred) {
3703
+ const wantValid = pred.valid !== false;
3704
+ return (row) => {
3705
+ const raw = read(row);
3706
+ const ok = raw != null && !(typeof raw === "number" && Number.isNaN(raw));
3707
+ return ok === wantValid;
3708
+ };
3816
3709
  }
3817
- parseUnary() {
3818
- const t = this.peek();
3819
- if (t.type === "op" && (t.value === "!" || t.value === "-" || t.value === "+")) {
3820
- this.next();
3821
- return { type: "unary", op: t.value, arg: this.parseUnary() };
3822
- }
3823
- return this.parsePostfix();
3710
+ if ("range" in pred) {
3711
+ const a = toComparable(pred.range[0]);
3712
+ const b = toComparable(pred.range[1]);
3713
+ const min = Math.min(a, b);
3714
+ const max = Math.max(a, b);
3715
+ return (row) => {
3716
+ const c = toComparable(read(row));
3717
+ return !Number.isNaN(c) && c >= min && c <= max;
3718
+ };
3824
3719
  }
3825
- parsePostfix() {
3826
- let node = this.parsePrimary();
3827
- for (; ; ) {
3828
- if (this.isOp(".")) {
3829
- this.eat(".");
3830
- const t = this.next();
3831
- if (t.type !== "ident") throw new ExpressionError(`Expected a property name after ".".`, t.pos);
3832
- node = { type: "member", obj: node, prop: { type: "str", value: t.value }, computed: false };
3833
- } else if (this.isOp("[")) {
3834
- this.eat("[");
3835
- const prop = this.parseTernary();
3836
- this.eat("]");
3837
- node = { type: "member", obj: node, prop, computed: true };
3838
- } else {
3839
- break;
3840
- }
3841
- }
3842
- return node;
3720
+ if ("gt" in pred) {
3721
+ const t = toComparable(pred.gt);
3722
+ return (row) => {
3723
+ const c = toComparable(read(row));
3724
+ return !Number.isNaN(c) && c > t;
3725
+ };
3843
3726
  }
3844
- parsePrimary() {
3845
- const t = this.next();
3846
- if (t.type === "num") return { type: "num", value: t.value };
3847
- if (t.type === "str") return { type: "str", value: t.value };
3848
- if (t.type === "ident") {
3849
- const name = t.value;
3850
- if (name === "true") return { type: "lit", value: true };
3851
- if (name === "false") return { type: "lit", value: false };
3852
- if (name === "null") return { type: "lit", value: null };
3853
- if (this.isOp("(")) {
3854
- this.eat("(");
3855
- const args = [];
3856
- if (!this.isOp(")")) {
3857
- args.push(this.parseTernary());
3858
- while (this.isOp(",")) {
3859
- this.eat(",");
3860
- args.push(this.parseTernary());
3861
- }
3862
- }
3863
- this.eat(")");
3864
- return { type: "call", callee: name, args };
3865
- }
3866
- if (name === "datum") return { type: "datum" };
3867
- return { type: "field", name };
3868
- }
3869
- if (t.type === "op" && t.value === "(") {
3870
- const node = this.parseTernary();
3871
- this.eat(")");
3872
- return node;
3873
- }
3874
- throw new ExpressionError(`Unexpected ${describe(t)}.`, t.pos);
3727
+ if ("gte" in pred) {
3728
+ const t = toComparable(pred.gte);
3729
+ return (row) => {
3730
+ const c = toComparable(read(row));
3731
+ return !Number.isNaN(c) && c >= t;
3732
+ };
3875
3733
  }
3876
- };
3877
- function binaryLevel(op) {
3878
- if (op === "||") return 1;
3879
- if (op === "&&") return 2;
3880
- if (EQUALITY.has(op)) return 3;
3881
- if (RELATIONAL.has(op)) return 4;
3882
- if (ADDITIVE.has(op)) return 5;
3883
- if (MULTIPLICATIVE.has(op)) return 6;
3884
- return -1;
3734
+ if ("lt" in pred) {
3735
+ const t = toComparable(pred.lt);
3736
+ return (row) => {
3737
+ const c = toComparable(read(row));
3738
+ return !Number.isNaN(c) && c < t;
3739
+ };
3740
+ }
3741
+ if ("lte" in pred) {
3742
+ const t = toComparable(pred.lte);
3743
+ return (row) => {
3744
+ const c = toComparable(read(row));
3745
+ return !Number.isNaN(c) && c <= t;
3746
+ };
3747
+ }
3748
+ return () => false;
3885
3749
  }
3886
- function describe(t) {
3887
- if (t.type === "eof") return "end of input";
3888
- if (t.type === "str") return `string ${JSON.stringify(t.value)}`;
3889
- if (t.type === "num") return `number ${t.value}`;
3890
- return `"${t.value}"`;
3750
+ function applyFilter(transform2, data) {
3751
+ const test = compilePredicate(transform2.filter);
3752
+ return data.filter(test);
3891
3753
  }
3892
- function parse(src) {
3893
- if (typeof src !== "string" || src.trim() === "") {
3894
- throw new ExpressionError("Expression must be a non-empty string.", 0);
3754
+
3755
+ // src/spec/transform/fold.ts
3756
+ function applyFold(transform2, data) {
3757
+ const [keyName, valueName] = transform2.as ?? ["key", "value"];
3758
+ const out = [];
3759
+ for (const row of data) {
3760
+ for (const column of transform2.fold) {
3761
+ out.push({ ...row, [keyName]: column, [valueName]: row[column] });
3762
+ }
3895
3763
  }
3896
- return new Parser(tokenize(src)).parse();
3764
+ return out;
3897
3765
  }
3898
3766
 
3899
- // src/spec/transform/expr/evaluate.ts
3900
- var BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3901
- function truthy(v) {
3902
- if (typeof v === "number") return v !== 0 && !Number.isNaN(v);
3903
- return Boolean(v);
3904
- }
3905
- function toStr(v) {
3906
- if (v == null) return "";
3907
- if (v instanceof Date) return v.toISOString();
3908
- return String(v);
3767
+ // src/spec/transform/timeUnit.ts
3768
+ function truncateTo(date, unit) {
3769
+ const year = date.getFullYear();
3770
+ const month = date.getMonth();
3771
+ const day = date.getDate();
3772
+ const hour = date.getHours();
3773
+ const minute = date.getMinutes();
3774
+ const second = date.getSeconds();
3775
+ switch (unit) {
3776
+ case "year":
3777
+ return new Date(year, 0, 1);
3778
+ case "quarter":
3779
+ return new Date(year, Math.floor(month / 3) * 3, 1);
3780
+ case "month":
3781
+ return new Date(year, month, 1);
3782
+ case "week": {
3783
+ const start = new Date(year, month, day - date.getDay());
3784
+ start.setHours(0, 0, 0, 0);
3785
+ return start;
3786
+ }
3787
+ case "day":
3788
+ return new Date(year, month, day);
3789
+ case "hour":
3790
+ return new Date(year, month, day, hour);
3791
+ case "minute":
3792
+ return new Date(year, month, day, hour, minute);
3793
+ case "second":
3794
+ return new Date(year, month, day, hour, minute, second);
3795
+ default:
3796
+ return new Date(date.getTime());
3797
+ }
3909
3798
  }
3910
- function looseEquals(a, b) {
3911
- if (a == null || b == null) return a == null && b == null;
3912
- const na = toNumber(a);
3913
- const nb = toNumber(b);
3914
- if (Number.isFinite(na) && Number.isFinite(nb)) return na === nb;
3915
- return toStr(a) === toStr(b);
3799
+ function applyTimeUnit(transform2, data) {
3800
+ const read = accessor(transform2.field);
3801
+ return data.map((row) => {
3802
+ const date = toDate(read(row));
3803
+ return { ...row, [transform2.as]: date ? truncateTo(date, transform2.timeUnit) : null };
3804
+ });
3916
3805
  }
3917
- function compare(a, b) {
3918
- const na = toNumber(a);
3919
- const nb = toNumber(b);
3920
- if (Number.isFinite(na) && Number.isFinite(nb)) return na < nb ? -1 : na > nb ? 1 : 0;
3921
- const sa = toStr(a);
3922
- const sb = toStr(b);
3923
- return sa < sb ? -1 : sa > sb ? 1 : 0;
3806
+
3807
+ // src/spec/transform/apply.ts
3808
+ function applyTransform(transform2, data) {
3809
+ if ("filter" in transform2) return applyFilter(transform2, data);
3810
+ if ("aggregate" in transform2) return applyAggregate(transform2, data);
3811
+ if ("bin" in transform2) return applyBin(transform2, data);
3812
+ if ("fold" in transform2) return applyFold(transform2, data);
3813
+ if ("timeUnit" in transform2) return applyTimeUnit(transform2, data);
3814
+ if ("calculate" in transform2) return applyCalculate(transform2, data);
3815
+ return data;
3924
3816
  }
3925
- var nums = (args) => args.map(toNumber);
3926
- var FUNCTIONS = {
3927
- abs: (a) => Math.abs(toNumber(a[0])),
3928
- round: (a) => {
3929
- const d = a.length > 1 ? Math.trunc(toNumber(a[1])) : 0;
3930
- const f = 10 ** d;
3931
- return Math.round(toNumber(a[0]) * f) / f;
3932
- },
3933
- floor: (a) => Math.floor(toNumber(a[0])),
3934
- ceil: (a) => Math.ceil(toNumber(a[0])),
3935
- trunc: (a) => Math.trunc(toNumber(a[0])),
3936
- sign: (a) => Math.sign(toNumber(a[0])),
3937
- sqrt: (a) => Math.sqrt(toNumber(a[0])),
3938
- exp: (a) => Math.exp(toNumber(a[0])),
3939
- log: (a) => Math.log(toNumber(a[0])),
3940
- log10: (a) => Math.log10(toNumber(a[0])),
3941
- log2: (a) => Math.log2(toNumber(a[0])),
3942
- pow: (a) => toNumber(a[0]) ** toNumber(a[1]),
3943
- min: (a) => Math.min(...nums(a)),
3944
- max: (a) => Math.max(...nums(a)),
3945
- number: (a) => toNumber(a[0]),
3946
- isFinite: (a) => Number.isFinite(toNumber(a[0])),
3947
- isNaN: (a) => Number.isNaN(toNumber(a[0])),
3948
- str: (a) => toStr(a[0]),
3949
- lower: (a) => toStr(a[0]).toLowerCase(),
3950
- upper: (a) => toStr(a[0]).toUpperCase(),
3951
- trim: (a) => toStr(a[0]).trim(),
3952
- length: (a) => Array.isArray(a[0]) ? a[0].length : toStr(a[0]).length,
3953
- substring: (a) => {
3954
- const s = toStr(a[0]);
3955
- const start = Math.trunc(toNumber(a[1]));
3956
- return a.length > 2 ? s.substring(start, Math.trunc(toNumber(a[2]))) : s.substring(start);
3957
- },
3958
- replace: (a) => toStr(a[0]).split(toStr(a[1])).join(toStr(a[2])),
3959
- contains: (a) => toStr(a[0]).includes(toStr(a[1])),
3960
- startsWith: (a) => toStr(a[0]).startsWith(toStr(a[1])),
3961
- endsWith: (a) => toStr(a[0]).endsWith(toStr(a[1])),
3962
- concat: (a) => a.map(toStr).join(""),
3963
- coalesce: (a) => a.find((v) => v != null),
3964
- if: (a) => truthy(a[0]) ? a[1] : a[2],
3965
- year: (a) => toDate(a[0])?.getFullYear() ?? NaN,
3966
- month: (a) => {
3967
- const d = toDate(a[0]);
3968
- return d ? d.getMonth() + 1 : NaN;
3969
- },
3970
- day: (a) => toDate(a[0])?.getDate() ?? NaN,
3971
- hours: (a) => toDate(a[0])?.getHours() ?? NaN,
3972
- minutes: (a) => toDate(a[0])?.getMinutes() ?? NaN
3973
- };
3974
- var FUNCTION_NAMES = Object.keys(FUNCTIONS);
3975
- function evalNode(node, row) {
3976
- switch (node.type) {
3977
- case "num":
3978
- case "str":
3979
- case "lit":
3980
- return node.value;
3981
- case "datum":
3982
- return row;
3983
- case "field":
3984
- return row[node.name];
3985
- case "unary": {
3986
- const v = evalNode(node.arg, row);
3987
- if (node.op === "!") return !truthy(v);
3988
- if (node.op === "-") return -toNumber(v);
3989
- return toNumber(v);
3990
- }
3991
- case "logical": {
3992
- const left = evalNode(node.left, row);
3993
- if (node.op === "&&") return truthy(left) ? evalNode(node.right, row) : left;
3994
- return truthy(left) ? left : evalNode(node.right, row);
3995
- }
3996
- case "ternary":
3997
- return truthy(evalNode(node.test, row)) ? evalNode(node.then, row) : evalNode(node.alt, row);
3998
- case "binary":
3999
- return evalBinary(node.op, evalNode(node.left, row), evalNode(node.right, row));
4000
- case "member": {
4001
- const obj = evalNode(node.obj, row);
4002
- if (obj == null || typeof obj !== "object") return void 0;
4003
- const key = node.computed ? toStr(evalNode(node.prop, row)) : node.prop.value;
4004
- if (BLOCKED_KEYS.has(key)) return void 0;
4005
- return obj[key];
4006
- }
4007
- case "call": {
4008
- const fn = Object.prototype.hasOwnProperty.call(FUNCTIONS, node.callee) ? FUNCTIONS[node.callee] : void 0;
4009
- if (!fn) throw new ExpressionError(`Unknown function "${node.callee}".`);
4010
- return fn(node.args.map((a) => evalNode(a, row)));
4011
- }
3817
+ function applyTransforms(transforms, data) {
3818
+ if (!transforms || transforms.length === 0) return data;
3819
+ let out = data;
3820
+ for (const transform2 of transforms) {
3821
+ out = applyTransform(transform2, out);
4012
3822
  }
4013
- }
4014
- function evalBinary(op, a, b) {
4015
- switch (op) {
4016
- case "+":
4017
- return typeof a === "string" || typeof b === "string" ? toStr(a) + toStr(b) : toNumber(a) + toNumber(b);
4018
- case "-":
4019
- return toNumber(a) - toNumber(b);
4020
- case "*":
4021
- return toNumber(a) * toNumber(b);
4022
- case "/":
4023
- return toNumber(a) / toNumber(b);
4024
- case "%":
4025
- return toNumber(a) % toNumber(b);
4026
- case "<":
4027
- return compare(a, b) < 0;
4028
- case "<=":
4029
- return compare(a, b) <= 0;
4030
- case ">":
4031
- return compare(a, b) > 0;
4032
- case ">=":
4033
- return compare(a, b) >= 0;
4034
- case "==":
4035
- return looseEquals(a, b);
4036
- case "!=":
4037
- return !looseEquals(a, b);
4038
- case "===":
4039
- return a === b;
4040
- case "!==":
4041
- return a !== b;
4042
- default:
4043
- throw new ExpressionError(`Unknown operator "${op}".`);
4044
- }
4045
- }
4046
- function evaluate(node, row) {
4047
- return evalNode(node, row);
3823
+ return out;
4048
3824
  }
4049
3825
 
4050
- // src/spec/transform/expr/index.ts
4051
- function compileExpression(src) {
4052
- const ast = parse(src);
4053
- return (row) => evaluate(ast, row);
3826
+ // src/spec/lint.ts
3827
+ var MAX_PIE_SLICES = 7;
3828
+ var MAX_SERIES = 12;
3829
+ var MAX_DISCRETE_AXIS = 50;
3830
+ var POSITIONAL_AXES = ["x", "y"];
3831
+ function isObject(v) {
3832
+ return typeof v === "object" && v !== null && !Array.isArray(v);
4054
3833
  }
4055
- function checkExpression(src) {
3834
+ function fieldDef(encoding, channel) {
3835
+ const def = encoding?.[channel];
3836
+ if (isObject(def) && typeof def.field === "string" && def.field !== "") return def;
3837
+ return void 0;
3838
+ }
3839
+ function effectiveData(spec) {
3840
+ const data = Array.isArray(spec.data) ? spec.data : [];
3841
+ if (!("transform" in spec) || !Array.isArray(spec.transform) || spec.transform.length === 0) return data;
4056
3842
  try {
4057
- parse(src);
4058
- return null;
4059
- } catch (e) {
4060
- if (e instanceof ExpressionError) return { message: e.message, pos: e.pos };
4061
- throw e;
3843
+ return applyTransforms(spec.transform, data);
3844
+ } catch {
3845
+ return data;
4062
3846
  }
4063
3847
  }
4064
-
4065
- // src/spec/transform/calculate.ts
4066
- function applyCalculate(transform2, data) {
4067
- const evalRow = compileExpression(transform2.calculate);
4068
- const as = transform2.as;
4069
- return data.map((row) => ({ ...row, [as]: evalRow(row) }));
4070
- }
4071
-
4072
- // src/spec/transform/filter.ts
4073
- function toComparable(value) {
4074
- if (typeof value === "number") return value;
4075
- if (value instanceof Date) return value.getTime();
4076
- const n = toNumber(value);
4077
- if (!Number.isNaN(n)) return n;
4078
- const d = toDate(value);
4079
- return d ? d.getTime() : NaN;
4080
- }
4081
- function compilePredicate(pred) {
4082
- if ("and" in pred) {
4083
- const parts = pred.and.map(compilePredicate);
4084
- return (row) => parts.every((p) => p(row));
4085
- }
4086
- if ("or" in pred) {
4087
- const parts = pred.or.map(compilePredicate);
4088
- return (row) => parts.some((p) => p(row));
4089
- }
4090
- if ("not" in pred) {
4091
- const inner = compilePredicate(pred.not);
4092
- return (row) => !inner(row);
4093
- }
4094
- const read = accessor(pred.field);
4095
- if ("equals" in pred) {
4096
- const want = toKey(pred.equals);
4097
- return (row) => toKey(read(row)) === want;
4098
- }
4099
- if ("ne" in pred) {
4100
- const want = toKey(pred.ne);
4101
- return (row) => toKey(read(row)) !== want;
4102
- }
4103
- if ("oneOf" in pred) {
4104
- const want = new Set(pred.oneOf.map((x) => toKey(x)));
4105
- return (row) => want.has(toKey(read(row)));
3848
+ function lintSpec(spec) {
3849
+ const findings = [];
3850
+ const add = (rule, path, message, severity = "warning") => findings.push({ rule, path, message, severity });
3851
+ const encoding = isObject(spec) ? spec.encoding : void 0;
3852
+ const data = effectiveData(spec);
3853
+ if (data.length === 0) return findings;
3854
+ if (encoding) {
3855
+ for (const channel of ["x", "y", "color", "series"]) {
3856
+ const def = fieldDef(encoding, channel);
3857
+ if (!def) continue;
3858
+ const declared = def.type;
3859
+ if ((declared === "nominal" || declared === "ordinal") && inferType(data, def.field) === "temporal") {
3860
+ findings.push({
3861
+ rule: "temporal-typed-as-categorical",
3862
+ path: `encoding.${channel}.type`,
3863
+ message: `Field "${def.field}" looks temporal but is typed "${declared}". Set type:"temporal" so it scales as time.`,
3864
+ severity: "warning",
3865
+ fix: [{ op: "replace", path: `/encoding/${channel}/type`, value: "temporal" }]
3866
+ });
3867
+ }
3868
+ }
4106
3869
  }
4107
- if ("contains" in pred) {
4108
- const q = String(pred.contains).toLowerCase();
4109
- return (row) => {
4110
- const raw = read(row);
4111
- return raw != null && String(raw).toLowerCase().includes(q);
4112
- };
3870
+ if (spec.type === "pie") {
3871
+ const def = fieldDef(encoding, "color") ?? fieldDef(encoding, "theta");
3872
+ const slices = def ? uniqueValues(data, def.field).length : data.length;
3873
+ if (slices > MAX_PIE_SLICES) {
3874
+ add(
3875
+ "pie-too-many-slices",
3876
+ "encoding.color",
3877
+ `Pie has ${slices} slices (> ${MAX_PIE_SLICES}); proportions get hard to compare. Use a bar chart, or group small slices into "Other".`
3878
+ );
3879
+ }
4113
3880
  }
4114
- if ("valid" in pred) {
4115
- const wantValid = pred.valid !== false;
4116
- return (row) => {
4117
- const raw = read(row);
4118
- const ok = raw != null && !(typeof raw === "number" && Number.isNaN(raw));
4119
- return ok === wantValid;
4120
- };
3881
+ for (const channel of ["series", "color"]) {
3882
+ const def = fieldDef(encoding, channel);
3883
+ if (!def) continue;
3884
+ if (inferType(data, def.field) === "quantitative" && channel === "color") continue;
3885
+ const count = uniqueValues(data, def.field).length;
3886
+ if (count > MAX_SERIES) {
3887
+ add(
3888
+ "too-many-series",
3889
+ `encoding.${channel}`,
3890
+ `"${def.field}" has ${count} distinct values (> ${MAX_SERIES}); the legend/colors become hard to tell apart. Filter, group, or facet instead.`
3891
+ );
3892
+ break;
3893
+ }
4121
3894
  }
4122
- if ("range" in pred) {
4123
- const a = toComparable(pred.range[0]);
4124
- const b = toComparable(pred.range[1]);
4125
- const min = Math.min(a, b);
4126
- const max = Math.max(a, b);
4127
- return (row) => {
4128
- const c = toComparable(read(row));
4129
- return !Number.isNaN(c) && c >= min && c <= max;
4130
- };
3895
+ if (spec.type === "bar") {
3896
+ const y = fieldDef(encoding, "y");
3897
+ const scale = y?.scale;
3898
+ if (scale?.zero === false) {
3899
+ add(
3900
+ "bar-nonzero-baseline",
3901
+ "encoding.y.scale.zero",
3902
+ "Bar lengths encode magnitude, so a non-zero baseline exaggerates differences. Remove scale.zero:false (bars should start at 0)."
3903
+ );
3904
+ } else if (Array.isArray(scale?.domain) && typeof scale.domain[0] === "number" && scale.domain[0] > 0) {
3905
+ add(
3906
+ "bar-nonzero-baseline",
3907
+ "encoding.y.scale.domain",
3908
+ `Bar y-domain starts at ${scale.domain[0]} (> 0); a truncated baseline exaggerates differences. Start the domain at 0.`
3909
+ );
3910
+ }
4131
3911
  }
4132
- if ("gt" in pred) {
4133
- const t = toComparable(pred.gt);
4134
- return (row) => {
4135
- const c = toComparable(read(row));
4136
- return !Number.isNaN(c) && c > t;
4137
- };
3912
+ if (encoding) {
3913
+ for (const channel of ["x", "y"]) {
3914
+ const def = fieldDef(encoding, channel);
3915
+ if (def?.scale?.type !== "log") continue;
3916
+ const read = accessor(def.field);
3917
+ const hasNonPositive = data.some((d) => {
3918
+ const n = toNumber(read(d));
3919
+ return Number.isFinite(n) && n <= 0;
3920
+ });
3921
+ if (hasNonPositive) {
3922
+ add(
3923
+ "log-nonpositive-data",
3924
+ `encoding.${channel}.scale`,
3925
+ `"${def.field}" has values \u2264 0, which a log scale cannot plot. Use a linear scale or filter non-positive values.`
3926
+ );
3927
+ }
3928
+ }
4138
3929
  }
4139
- if ("gte" in pred) {
4140
- const t = toComparable(pred.gte);
4141
- return (row) => {
4142
- const c = toComparable(read(row));
4143
- return !Number.isNaN(c) && c >= t;
4144
- };
3930
+ if (spec.type === "bar" || spec.type === "line" || spec.type === "area") {
3931
+ for (const channel of POSITIONAL_AXES) {
3932
+ const def = fieldDef(encoding, channel);
3933
+ if (!def) continue;
3934
+ const t = def.type ?? inferType(data, def.field);
3935
+ const discrete = t === "nominal" || t === "ordinal" || def.scale?.type === "band" || def.scale?.type === "point";
3936
+ if (!discrete) continue;
3937
+ const count = uniqueValues(data, def.field).length;
3938
+ if (count > MAX_DISCRETE_AXIS) {
3939
+ add(
3940
+ "high-cardinality-axis",
3941
+ `encoding.${channel}`,
3942
+ `"${def.field}" has ${count} categories on the ${channel}-axis (> ${MAX_DISCRETE_AXIS}); labels will be unreadable. Aggregate, filter to a top-N, or use a different chart.`
3943
+ );
3944
+ }
3945
+ }
4145
3946
  }
4146
- if ("lt" in pred) {
4147
- const t = toComparable(pred.lt);
4148
- return (row) => {
4149
- const c = toComparable(read(row));
4150
- return !Number.isNaN(c) && c < t;
4151
- };
3947
+ if (spec.type === "histogram") {
3948
+ const def = fieldDef(encoding, "x");
3949
+ if (def) {
3950
+ const t = def.type ?? inferType(data, def.field);
3951
+ if (t !== "quantitative") {
3952
+ add(
3953
+ "histogram-nonnumeric-field",
3954
+ "encoding.x",
3955
+ `Histogram field "${def.field}" is ${t}, not quantitative \u2014 binning needs numbers. Use a numeric field, or a bar chart for categories.`
3956
+ );
3957
+ }
3958
+ }
4152
3959
  }
4153
- if ("lte" in pred) {
4154
- const t = toComparable(pred.lte);
4155
- return (row) => {
4156
- const c = toComparable(read(row));
4157
- return !Number.isNaN(c) && c <= t;
4158
- };
3960
+ if (spec.type === "combo" && Array.isArray(spec.layers)) {
3961
+ const layers = spec.layers;
3962
+ const hasRight = layers.some((l) => isObject(l) && l.axis === "right");
3963
+ const hasLeft = layers.some((l) => isObject(l) && l.axis !== "right");
3964
+ if (hasRight && hasLeft) {
3965
+ add(
3966
+ "combo-dual-axis",
3967
+ "layers",
3968
+ "Dual-axis combo: a secondary y-axis can suggest correlations that are not real. Use it only for genuinely different units/scales, and label both axes clearly.",
3969
+ "info"
3970
+ );
3971
+ }
4159
3972
  }
4160
- return () => false;
4161
- }
4162
- function applyFilter(transform2, data) {
4163
- const test = compilePredicate(transform2.filter);
4164
- return data.filter(test);
3973
+ return findings;
4165
3974
  }
4166
3975
 
4167
- // src/spec/transform/fold.ts
4168
- function applyFold(transform2, data) {
4169
- const [keyName, valueName] = transform2.as ?? ["key", "value"];
4170
- const out = [];
4171
- for (const row of data) {
4172
- for (const column of transform2.fold) {
4173
- out.push({ ...row, [keyName]: column, [valueName]: row[column] });
3976
+ // src/spec/suggest.ts
3977
+ function levenshtein(a, b) {
3978
+ if (a === b) return 0;
3979
+ const m = a.length;
3980
+ const n = b.length;
3981
+ if (m === 0) return n;
3982
+ if (n === 0) return m;
3983
+ let prev = new Array(n + 1);
3984
+ let curr = new Array(n + 1);
3985
+ for (let j = 0; j <= n; j++) prev[j] = j;
3986
+ for (let i = 1; i <= m; i++) {
3987
+ curr[0] = i;
3988
+ const ca = a.charCodeAt(i - 1);
3989
+ for (let j = 1; j <= n; j++) {
3990
+ const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
3991
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
4174
3992
  }
3993
+ [prev, curr] = [curr, prev];
4175
3994
  }
4176
- return out;
3995
+ return prev[n];
4177
3996
  }
4178
-
4179
- // src/spec/transform/timeUnit.ts
4180
- function truncateTo(date, unit) {
4181
- const year = date.getFullYear();
4182
- const month = date.getMonth();
4183
- const day = date.getDate();
4184
- const hour = date.getHours();
4185
- const minute = date.getMinutes();
4186
- const second = date.getSeconds();
4187
- switch (unit) {
4188
- case "year":
4189
- return new Date(year, 0, 1);
4190
- case "quarter":
4191
- return new Date(year, Math.floor(month / 3) * 3, 1);
4192
- case "month":
4193
- return new Date(year, month, 1);
4194
- case "week": {
4195
- const start = new Date(year, month, day - date.getDay());
4196
- start.setHours(0, 0, 0, 0);
4197
- return start;
4198
- }
4199
- case "day":
4200
- return new Date(year, month, day);
4201
- case "hour":
4202
- return new Date(year, month, day, hour);
4203
- case "minute":
4204
- return new Date(year, month, day, hour, minute);
4205
- case "second":
4206
- return new Date(year, month, day, hour, minute, second);
4207
- default:
4208
- return new Date(date.getTime());
4209
- }
3997
+ function tolerance(input) {
3998
+ return Math.max(2, Math.ceil(input.length * 0.5));
4210
3999
  }
4211
- function applyTimeUnit(transform2, data) {
4212
- const read = accessor(transform2.field);
4213
- return data.map((row) => {
4214
- const date = toDate(read(row));
4215
- return { ...row, [transform2.as]: date ? truncateTo(date, transform2.timeUnit) : null };
4216
- });
4000
+ function rank(input, candidates) {
4001
+ const lower = input.toLowerCase();
4002
+ return candidates.map((value) => {
4003
+ const lc = value.toLowerCase();
4004
+ const distance = lc === lower ? 0 : levenshtein(lower, lc);
4005
+ return { value, distance };
4006
+ }).sort((a, b) => a.distance - b.distance || a.value.localeCompare(b.value));
4217
4007
  }
4218
-
4219
- // src/spec/transform/apply.ts
4220
- function applyTransform(transform2, data) {
4221
- if ("filter" in transform2) return applyFilter(transform2, data);
4222
- if ("aggregate" in transform2) return applyAggregate(transform2, data);
4223
- if ("bin" in transform2) return applyBin(transform2, data);
4224
- if ("fold" in transform2) return applyFold(transform2, data);
4225
- if ("timeUnit" in transform2) return applyTimeUnit(transform2, data);
4226
- if ("calculate" in transform2) return applyCalculate(transform2, data);
4227
- return data;
4008
+ function suggestions(input, candidates, limit = 3, maxDistance = tolerance(input)) {
4009
+ if (!input) return [];
4010
+ return rank(input, candidates).filter((r) => r.distance <= maxDistance).slice(0, limit).map((r) => r.value);
4228
4011
  }
4229
- function applyTransforms(transforms, data) {
4230
- if (!transforms || transforms.length === 0) return data;
4231
- let out = data;
4232
- for (const transform2 of transforms) {
4233
- out = applyTransform(transform2, out);
4234
- }
4235
- return out;
4012
+ function closest(input, candidates) {
4013
+ if (!input) return void 0;
4014
+ const ranked = rank(input, candidates);
4015
+ if (ranked.length === 0) return void 0;
4016
+ const best = ranked[0];
4017
+ const bound = Math.max(2, Math.floor(input.length * 0.34));
4018
+ if (best.distance > bound) return void 0;
4019
+ const runnerUp = ranked[1];
4020
+ if (runnerUp && runnerUp.distance === best.distance) return void 0;
4021
+ return best.value;
4236
4022
  }
4237
4023
 
4238
- // src/spec/infer.ts
4239
- function looksTemporal2(value) {
4240
- if (value.length < 4) return false;
4241
- if (/^\d{4}(-\d{2}(-\d{2})?)?([T\s]\d{2}:\d{2})?/.test(value)) {
4242
- const t = Date.parse(value);
4243
- return !Number.isNaN(t);
4024
+ // src/spec/jsonpatch.ts
4025
+ function clone(value) {
4026
+ if (value === null || typeof value !== "object") return value;
4027
+ if (value instanceof Date) return new Date(value.getTime());
4028
+ if (Array.isArray(value)) return value.map((v) => clone(v));
4029
+ const out = {};
4030
+ for (const key of Object.keys(value)) {
4031
+ out[key] = clone(value[key]);
4244
4032
  }
4245
- return false;
4033
+ return out;
4246
4034
  }
4247
- function inferValueType(value) {
4248
- if (value == null) return void 0;
4249
- if (typeof value === "number") return Number.isFinite(value) ? "quantitative" : void 0;
4250
- if (value instanceof Date) return "temporal";
4251
- if (typeof value === "boolean") return "nominal";
4252
- if (typeof value === "string") {
4253
- if (value.trim() === "") return void 0;
4254
- if (looksTemporal2(value)) return "temporal";
4255
- if (/^-?\d*\.?\d+(e[-+]?\d+)?$/i.test(value.trim())) return "quantitative";
4256
- return "nominal";
4257
- }
4258
- return "nominal";
4035
+ function decode(token) {
4036
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
4259
4037
  }
4260
- function inferFieldType(data, field, sample = 50) {
4261
- let seen = 0;
4262
- const counts = {
4263
- quantitative: 0,
4264
- temporal: 0,
4265
- ordinal: 0,
4266
- nominal: 0
4267
- };
4268
- for (let i = 0; i < data.length && seen < sample; i++) {
4269
- const t = inferValueType(data[i]?.[field]);
4270
- if (!t) continue;
4271
- counts[t]++;
4272
- seen++;
4273
- }
4274
- if (seen === 0) return "nominal";
4275
- if (counts.temporal >= seen * 0.6) return "temporal";
4276
- if (counts.quantitative >= seen * 0.6) return "quantitative";
4277
- return "nominal";
4038
+ function parsePointer(pointer) {
4039
+ if (pointer === "") return [];
4040
+ if (pointer[0] !== "/") throw new Error(`Invalid JSON Pointer: ${pointer}`);
4041
+ return pointer.slice(1).split("/").map(decode);
4278
4042
  }
4279
- function inferFieldTypes(data) {
4280
- const fields = /* @__PURE__ */ new Set();
4281
- for (let i = 0; i < Math.min(data.length, 20); i++) {
4282
- const row = data[i];
4283
- if (row) for (const k of Object.keys(row)) fields.add(k);
4043
+ function toPointer(path) {
4044
+ if (path === "") return "";
4045
+ const parts = [];
4046
+ for (const seg of path.replace(/\[(\d+)\]/g, ".$1").split(".")) {
4047
+ if (seg === "") continue;
4048
+ parts.push(seg.replace(/~/g, "~0").replace(/\//g, "~1"));
4284
4049
  }
4285
- const out = {};
4286
- for (const f of fields) out[f] = inferFieldType(data, f);
4287
- return out;
4288
- }
4289
-
4290
- // src/spec/lint.ts
4291
- var MAX_PIE_SLICES = 7;
4292
- var MAX_SERIES = 12;
4293
- var MAX_DISCRETE_AXIS = 50;
4294
- var POSITIONAL_AXES = ["x", "y"];
4295
- function isObject(v) {
4296
- return typeof v === "object" && v !== null && !Array.isArray(v);
4050
+ return "/" + parts.join("/");
4297
4051
  }
4298
- function fieldDef(encoding, channel) {
4299
- const def = encoding?.[channel];
4300
- if (isObject(def) && typeof def.field === "string" && def.field !== "") return def;
4301
- return void 0;
4052
+ function isObject2(v) {
4053
+ return v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
4302
4054
  }
4303
- function effectiveData(spec) {
4304
- const data = Array.isArray(spec.data) ? spec.data : [];
4305
- if (!("transform" in spec) || !Array.isArray(spec.transform) || spec.transform.length === 0) return data;
4306
- try {
4307
- return applyTransforms(spec.transform, data);
4308
- } catch {
4309
- return data;
4055
+ function applyOne(doc, op) {
4056
+ const segments = parsePointer(op.path);
4057
+ if (segments.length === 0) {
4058
+ return op.op === "remove" ? doc : op.value;
4310
4059
  }
4311
- }
4312
- function lintSpec(spec) {
4313
- const findings = [];
4314
- const add = (rule, path, message, severity = "warning") => findings.push({ rule, path, message, severity });
4315
- const encoding = isObject(spec) ? spec.encoding : void 0;
4316
- const data = effectiveData(spec);
4317
- if (data.length === 0) return findings;
4318
- if (encoding) {
4319
- for (const channel of ["x", "y", "color", "series"]) {
4320
- const def = fieldDef(encoding, channel);
4321
- if (!def) continue;
4322
- const declared = def.type;
4323
- if ((declared === "nominal" || declared === "ordinal") && inferType(data, def.field) === "temporal") {
4324
- findings.push({
4325
- rule: "temporal-typed-as-categorical",
4326
- path: `encoding.${channel}.type`,
4327
- message: `Field "${def.field}" looks temporal but is typed "${declared}". Set type:"temporal" so it scales as time.`,
4328
- severity: "warning",
4329
- fix: [{ op: "replace", path: `/encoding/${channel}/type`, value: "temporal" }]
4330
- });
4331
- }
4060
+ let parent = doc;
4061
+ for (let i = 0; i < segments.length - 1; i++) {
4062
+ const seg = segments[i];
4063
+ if (Array.isArray(parent)) {
4064
+ parent = parent[Number(seg)];
4065
+ } else if (isObject2(parent)) {
4066
+ parent = parent[seg];
4067
+ } else {
4068
+ return doc;
4332
4069
  }
4070
+ if (parent === void 0 || parent === null) return doc;
4333
4071
  }
4334
- if (spec.type === "pie") {
4335
- const def = fieldDef(encoding, "color") ?? fieldDef(encoding, "theta");
4336
- const slices = def ? uniqueValues(data, def.field).length : data.length;
4337
- if (slices > MAX_PIE_SLICES) {
4338
- add(
4339
- "pie-too-many-slices",
4340
- "encoding.color",
4341
- `Pie has ${slices} slices (> ${MAX_PIE_SLICES}); proportions get hard to compare. Use a bar chart, or group small slices into "Other".`
4342
- );
4343
- }
4344
- }
4345
- for (const channel of ["series", "color"]) {
4346
- const def = fieldDef(encoding, channel);
4347
- if (!def) continue;
4348
- if (inferType(data, def.field) === "quantitative" && channel === "color") continue;
4349
- const count = uniqueValues(data, def.field).length;
4350
- if (count > MAX_SERIES) {
4351
- add(
4352
- "too-many-series",
4353
- `encoding.${channel}`,
4354
- `"${def.field}" has ${count} distinct values (> ${MAX_SERIES}); the legend/colors become hard to tell apart. Filter, group, or facet instead.`
4355
- );
4356
- break;
4357
- }
4358
- }
4359
- if (spec.type === "bar") {
4360
- const y = fieldDef(encoding, "y");
4361
- const scale = y?.scale;
4362
- if (scale?.zero === false) {
4363
- add(
4364
- "bar-nonzero-baseline",
4365
- "encoding.y.scale.zero",
4366
- "Bar lengths encode magnitude, so a non-zero baseline exaggerates differences. Remove scale.zero:false (bars should start at 0)."
4367
- );
4368
- } else if (Array.isArray(scale?.domain) && typeof scale.domain[0] === "number" && scale.domain[0] > 0) {
4369
- add(
4370
- "bar-nonzero-baseline",
4371
- "encoding.y.scale.domain",
4372
- `Bar y-domain starts at ${scale.domain[0]} (> 0); a truncated baseline exaggerates differences. Start the domain at 0.`
4373
- );
4374
- }
4375
- }
4376
- if (encoding) {
4377
- for (const channel of ["x", "y"]) {
4378
- const def = fieldDef(encoding, channel);
4379
- if (def?.scale?.type !== "log") continue;
4380
- const read = accessor(def.field);
4381
- const hasNonPositive = data.some((d) => {
4382
- const n = toNumber(read(d));
4383
- return Number.isFinite(n) && n <= 0;
4384
- });
4385
- if (hasNonPositive) {
4386
- add(
4387
- "log-nonpositive-data",
4388
- `encoding.${channel}.scale`,
4389
- `"${def.field}" has values \u2264 0, which a log scale cannot plot. Use a linear scale or filter non-positive values.`
4390
- );
4391
- }
4392
- }
4393
- }
4394
- if (spec.type === "bar" || spec.type === "line" || spec.type === "area") {
4395
- for (const channel of POSITIONAL_AXES) {
4396
- const def = fieldDef(encoding, channel);
4397
- if (!def) continue;
4398
- const t = def.type ?? inferType(data, def.field);
4399
- const discrete = t === "nominal" || t === "ordinal" || def.scale?.type === "band" || def.scale?.type === "point";
4400
- if (!discrete) continue;
4401
- const count = uniqueValues(data, def.field).length;
4402
- if (count > MAX_DISCRETE_AXIS) {
4403
- add(
4404
- "high-cardinality-axis",
4405
- `encoding.${channel}`,
4406
- `"${def.field}" has ${count} categories on the ${channel}-axis (> ${MAX_DISCRETE_AXIS}); labels will be unreadable. Aggregate, filter to a top-N, or use a different chart.`
4407
- );
4408
- }
4409
- }
4410
- }
4411
- if (spec.type === "histogram") {
4412
- const def = fieldDef(encoding, "x");
4413
- if (def) {
4414
- const t = def.type ?? inferType(data, def.field);
4415
- if (t !== "quantitative") {
4416
- add(
4417
- "histogram-nonnumeric-field",
4418
- "encoding.x",
4419
- `Histogram field "${def.field}" is ${t}, not quantitative \u2014 binning needs numbers. Use a numeric field, or a bar chart for categories.`
4420
- );
4421
- }
4422
- }
4423
- }
4424
- if (spec.type === "combo" && Array.isArray(spec.layers)) {
4425
- const layers = spec.layers;
4426
- const hasRight = layers.some((l) => isObject(l) && l.axis === "right");
4427
- const hasLeft = layers.some((l) => isObject(l) && l.axis !== "right");
4428
- if (hasRight && hasLeft) {
4429
- add(
4430
- "combo-dual-axis",
4431
- "layers",
4432
- "Dual-axis combo: a secondary y-axis can suggest correlations that are not real. Use it only for genuinely different units/scales, and label both axes clearly.",
4433
- "info"
4434
- );
4435
- }
4436
- }
4437
- return findings;
4438
- }
4439
-
4440
- // src/spec/suggest.ts
4441
- function levenshtein(a, b) {
4442
- if (a === b) return 0;
4443
- const m = a.length;
4444
- const n = b.length;
4445
- if (m === 0) return n;
4446
- if (n === 0) return m;
4447
- let prev = new Array(n + 1);
4448
- let curr = new Array(n + 1);
4449
- for (let j = 0; j <= n; j++) prev[j] = j;
4450
- for (let i = 1; i <= m; i++) {
4451
- curr[0] = i;
4452
- const ca = a.charCodeAt(i - 1);
4453
- for (let j = 1; j <= n; j++) {
4454
- const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
4455
- curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
4456
- }
4457
- [prev, curr] = [curr, prev];
4458
- }
4459
- return prev[n];
4460
- }
4461
- function tolerance(input) {
4462
- return Math.max(2, Math.ceil(input.length * 0.5));
4463
- }
4464
- function rank(input, candidates) {
4465
- const lower = input.toLowerCase();
4466
- return candidates.map((value) => {
4467
- const lc = value.toLowerCase();
4468
- const distance = lc === lower ? 0 : levenshtein(lower, lc);
4469
- return { value, distance };
4470
- }).sort((a, b) => a.distance - b.distance || a.value.localeCompare(b.value));
4471
- }
4472
- function suggestions(input, candidates, limit = 3, maxDistance = tolerance(input)) {
4473
- if (!input) return [];
4474
- return rank(input, candidates).filter((r) => r.distance <= maxDistance).slice(0, limit).map((r) => r.value);
4475
- }
4476
- function closest(input, candidates) {
4477
- if (!input) return void 0;
4478
- const ranked = rank(input, candidates);
4479
- if (ranked.length === 0) return void 0;
4480
- const best = ranked[0];
4481
- const bound = Math.max(2, Math.floor(input.length * 0.34));
4482
- if (best.distance > bound) return void 0;
4483
- const runnerUp = ranked[1];
4484
- if (runnerUp && runnerUp.distance === best.distance) return void 0;
4485
- return best.value;
4486
- }
4487
-
4488
- // src/spec/jsonpatch.ts
4489
- function clone(value) {
4490
- if (value === null || typeof value !== "object") return value;
4491
- if (value instanceof Date) return new Date(value.getTime());
4492
- if (Array.isArray(value)) return value.map((v) => clone(v));
4493
- const out = {};
4494
- for (const key of Object.keys(value)) {
4495
- out[key] = clone(value[key]);
4496
- }
4497
- return out;
4498
- }
4499
- function decode(token) {
4500
- return token.replace(/~1/g, "/").replace(/~0/g, "~");
4501
- }
4502
- function parsePointer(pointer) {
4503
- if (pointer === "") return [];
4504
- if (pointer[0] !== "/") throw new Error(`Invalid JSON Pointer: ${pointer}`);
4505
- return pointer.slice(1).split("/").map(decode);
4506
- }
4507
- function toPointer(path) {
4508
- if (path === "") return "";
4509
- const parts = [];
4510
- for (const seg of path.replace(/\[(\d+)\]/g, ".$1").split(".")) {
4511
- if (seg === "") continue;
4512
- parts.push(seg.replace(/~/g, "~0").replace(/\//g, "~1"));
4513
- }
4514
- return "/" + parts.join("/");
4515
- }
4516
- function isObject2(v) {
4517
- return v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
4518
- }
4519
- function applyOne(doc, op) {
4520
- const segments = parsePointer(op.path);
4521
- if (segments.length === 0) {
4522
- return op.op === "remove" ? doc : op.value;
4523
- }
4524
- let parent = doc;
4525
- for (let i = 0; i < segments.length - 1; i++) {
4526
- const seg = segments[i];
4527
- if (Array.isArray(parent)) {
4528
- parent = parent[Number(seg)];
4529
- } else if (isObject2(parent)) {
4530
- parent = parent[seg];
4531
- } else {
4532
- return doc;
4533
- }
4534
- if (parent === void 0 || parent === null) return doc;
4535
- }
4536
- const key = segments[segments.length - 1];
4537
- if (Array.isArray(parent)) {
4538
- const idx = key === "-" ? parent.length : Number(key);
4539
- if (Number.isNaN(idx)) return doc;
4540
- if (op.op === "remove") {
4541
- if (idx >= 0 && idx < parent.length) parent.splice(idx, 1);
4542
- } else if (op.op === "add") {
4543
- parent.splice(Math.min(idx, parent.length), 0, op.value);
4544
- } else {
4545
- if (idx >= 0 && idx < parent.length) parent[idx] = op.value;
4546
- }
4547
- } else if (isObject2(parent)) {
4548
- if (op.op === "remove") {
4549
- delete parent[key];
4550
- } else {
4551
- parent[key] = op.value;
4072
+ const key = segments[segments.length - 1];
4073
+ if (Array.isArray(parent)) {
4074
+ const idx = key === "-" ? parent.length : Number(key);
4075
+ if (Number.isNaN(idx)) return doc;
4076
+ if (op.op === "remove") {
4077
+ if (idx >= 0 && idx < parent.length) parent.splice(idx, 1);
4078
+ } else if (op.op === "add") {
4079
+ parent.splice(Math.min(idx, parent.length), 0, op.value);
4080
+ } else {
4081
+ if (idx >= 0 && idx < parent.length) parent[idx] = op.value;
4082
+ }
4083
+ } else if (isObject2(parent)) {
4084
+ if (op.op === "remove") {
4085
+ delete parent[key];
4086
+ } else {
4087
+ parent[key] = op.value;
4552
4088
  }
4553
4089
  }
4554
4090
  return doc;
@@ -5631,133 +5167,810 @@ function validateDashboard(spec) {
5631
5167
  for (const w of res.warnings) warn(prefix(w.path), w.message);
5632
5168
  });
5633
5169
  }
5634
- validateInteraction(spec, err, warn);
5635
- const interactions = spec.interactions;
5636
- if (interactions !== void 0 && interactions !== "auto" && interactions !== "none") {
5637
- if (!Array.isArray(interactions)) {
5638
- err("interactions", '"interactions" must be "auto", "none", or an array of links.');
5639
- } else {
5640
- interactions.forEach((link, i) => {
5641
- if (!isObject3(link)) {
5642
- err(`interactions[${i}]`, "Each link must be an object { source, target }.");
5643
- return;
5644
- }
5645
- if (typeof link.source !== "string" || !ids.has(link.source)) {
5646
- err(`interactions[${i}].source`, 'Link "source" must reference a view id.');
5647
- }
5648
- if (link.target === void 0) {
5649
- err(`interactions[${i}].target`, 'Link needs a "target" (a view id, an array, or "*").');
5650
- } else if (link.target !== "*") {
5651
- const targets = Array.isArray(link.target) ? link.target : [link.target];
5652
- targets.forEach((t, j) => {
5653
- if (typeof t !== "string" || !ids.has(t)) {
5654
- const suffix = Array.isArray(link.target) ? `[${j}]` : "";
5655
- err(`interactions[${i}].target${suffix}`, 'Link "target" must reference a view id.');
5656
- }
5657
- });
5658
- }
5659
- if (link.as !== void 0 && !["highlight", "filter", "none"].includes(link.as)) {
5660
- err(`interactions[${i}].as`, 'Expected "highlight", "filter", or "none".');
5661
- }
5662
- });
5170
+ validateInteraction(spec, err, warn);
5171
+ const interactions = spec.interactions;
5172
+ if (interactions !== void 0 && interactions !== "auto" && interactions !== "none") {
5173
+ if (!Array.isArray(interactions)) {
5174
+ err("interactions", '"interactions" must be "auto", "none", or an array of links.');
5175
+ } else {
5176
+ interactions.forEach((link, i) => {
5177
+ if (!isObject3(link)) {
5178
+ err(`interactions[${i}]`, "Each link must be an object { source, target }.");
5179
+ return;
5180
+ }
5181
+ if (typeof link.source !== "string" || !ids.has(link.source)) {
5182
+ err(`interactions[${i}].source`, 'Link "source" must reference a view id.');
5183
+ }
5184
+ if (link.target === void 0) {
5185
+ err(`interactions[${i}].target`, 'Link needs a "target" (a view id, an array, or "*").');
5186
+ } else if (link.target !== "*") {
5187
+ const targets = Array.isArray(link.target) ? link.target : [link.target];
5188
+ targets.forEach((t, j) => {
5189
+ if (typeof t !== "string" || !ids.has(t)) {
5190
+ const suffix = Array.isArray(link.target) ? `[${j}]` : "";
5191
+ err(`interactions[${i}].target${suffix}`, 'Link "target" must reference a view id.');
5192
+ }
5193
+ });
5194
+ }
5195
+ if (link.as !== void 0 && !["highlight", "filter", "none"].includes(link.as)) {
5196
+ err(`interactions[${i}].as`, 'Expected "highlight", "filter", or "none".');
5197
+ }
5198
+ });
5199
+ }
5200
+ }
5201
+ if (spec.subtitle !== void 0 && typeof spec.subtitle !== "string") {
5202
+ err("subtitle", '"subtitle" must be a string.');
5203
+ }
5204
+ const layout2 = spec.layout;
5205
+ if (layout2 !== void 0) {
5206
+ if (!isObject3(layout2)) {
5207
+ err("layout", '"layout" must be an object { cols?, rowHeight?, gap?, breakpoints?, navigators?, sections? }.');
5208
+ } else {
5209
+ for (const k of ["cols", "rowHeight", "gap", "maxWidth", "padding"]) {
5210
+ const n = layout2[k];
5211
+ if (n !== void 0 && (typeof n !== "number" || !Number.isFinite(n) || n < 0)) {
5212
+ err(`layout.${k}`, `"${k}" must be a non-negative number.`);
5213
+ }
5214
+ }
5215
+ if (layout2.navigators !== void 0 && layout2.navigators !== "top" && layout2.navigators !== "inline") {
5216
+ err("layout.navigators", 'Expected "top" or "inline".');
5217
+ }
5218
+ if (layout2.preset !== void 0 && !["auto", "kpi-first", "sidebar"].includes(layout2.preset)) {
5219
+ err("layout.preset", 'Expected "auto", "kpi-first", or "sidebar".');
5220
+ }
5221
+ if (layout2.density !== void 0 && !["compact", "standard", "comfortable"].includes(layout2.density)) {
5222
+ err("layout.density", 'Expected "compact", "standard", or "comfortable".');
5223
+ }
5224
+ if (layout2.breakpoints !== void 0) {
5225
+ if (!Array.isArray(layout2.breakpoints)) {
5226
+ err("layout.breakpoints", '"breakpoints" must be an array of { maxWidth, cols }.');
5227
+ } else {
5228
+ layout2.breakpoints.forEach((bp, i) => {
5229
+ if (!isObject3(bp)) {
5230
+ err(`layout.breakpoints[${i}]`, "Each breakpoint must be an object { maxWidth, cols }.");
5231
+ return;
5232
+ }
5233
+ for (const k of ["maxWidth", "cols"]) {
5234
+ const n = bp[k];
5235
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 1) {
5236
+ err(`layout.breakpoints[${i}].${k}`, `"${k}" must be a positive number.`);
5237
+ }
5238
+ }
5239
+ });
5240
+ }
5241
+ }
5242
+ if (layout2.sections !== void 0) {
5243
+ if (!Array.isArray(layout2.sections)) {
5244
+ err("layout.sections", '"sections" must be an array of dashboard sections.');
5245
+ } else {
5246
+ const sectionViewIds = /* @__PURE__ */ new Set();
5247
+ layout2.sections.forEach((section, i) => {
5248
+ if (!isObject3(section)) {
5249
+ err(`layout.sections[${i}]`, "Each section must be an object { views }.");
5250
+ return;
5251
+ }
5252
+ for (const k of ["id", "title", "subtitle", "background"]) {
5253
+ if (section[k] !== void 0 && typeof section[k] !== "string") {
5254
+ err(`layout.sections[${i}].${k}`, `"${k}" must be a string.`);
5255
+ }
5256
+ }
5257
+ for (const k of ["cols", "rowHeight"]) {
5258
+ const n = section[k];
5259
+ if (n !== void 0 && (typeof n !== "number" || !Number.isFinite(n) || n < 1)) {
5260
+ err(`layout.sections[${i}].${k}`, `"${k}" must be a positive number.`);
5261
+ }
5262
+ }
5263
+ if (section.collapsed !== void 0 && typeof section.collapsed !== "boolean") {
5264
+ err(`layout.sections[${i}].collapsed`, '"collapsed" must be a boolean.');
5265
+ }
5266
+ if (!Array.isArray(section.views)) {
5267
+ err(`layout.sections[${i}].views`, '"views" must be an array of view ids.');
5268
+ } else {
5269
+ section.views.forEach((id, j) => {
5270
+ const path = `layout.sections[${i}].views[${j}]`;
5271
+ if (typeof id !== "string" || id === "") {
5272
+ err(path, "Section view references must be non-empty strings.");
5273
+ } else if (!ids.has(id)) {
5274
+ err(path, `Section view "${id}" must reference an existing view id.`);
5275
+ } else if (sectionViewIds.has(id)) {
5276
+ err(path, `View "${id}" is already assigned to another section.`);
5277
+ } else {
5278
+ sectionViewIds.add(id);
5279
+ }
5280
+ });
5281
+ }
5282
+ });
5283
+ }
5284
+ }
5285
+ }
5286
+ }
5287
+ return { valid: errors.length === 0, errors, warnings };
5288
+ }
5289
+ function assertValidSpec(spec) {
5290
+ const { valid, errors } = validateSpec(spec);
5291
+ if (!valid) {
5292
+ const detail = errors.map((e) => ` - ${e.path || "(root)"}: ${e.message}`).join("\n");
5293
+ throw new Error(`Invalid Graphein chart spec:
5294
+ ${detail}`);
5295
+ }
5296
+ return spec;
5297
+ }
5298
+
5299
+ // src/analyze/recommend.ts
5300
+ function profileColumns(data) {
5301
+ const fields = /* @__PURE__ */ new Set();
5302
+ for (const row of data) {
5303
+ for (const field of Object.keys(row)) fields.add(field);
5304
+ }
5305
+ return [...fields].map((field) => ({
5306
+ field,
5307
+ type: inferType(data, field),
5308
+ cardinality: uniqueValues(data, field).length
5309
+ }));
5310
+ }
5311
+ function title(text) {
5312
+ return text.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (m) => m.toUpperCase());
5313
+ }
5314
+ function fieldDef2(profile) {
5315
+ return { field: profile.field, type: profile.type };
5316
+ }
5317
+ function boost(score, family, intent) {
5318
+ if (!intent) return score;
5319
+ const matches = family === intent || intent === "composition" && family === "comparison" || intent === "comparison" && family === "density";
5320
+ return Math.min(1, score + (matches ? 0.12 : 0));
5321
+ }
5322
+ function pushValid(candidates, candidate, intent) {
5323
+ const score = Number(boost(candidate.score, candidate.family, intent).toFixed(3));
5324
+ const spec = candidate.spec;
5325
+ if (!validateSpec(spec).valid) return;
5326
+ candidates.push({ ...candidate, score });
5327
+ }
5328
+ function firstLowCardNominal(profiles, exclude = /* @__PURE__ */ new Set()) {
5329
+ return profiles.find((p) => p.type === "nominal" && p.cardinality <= 12 && !exclude.has(p.field));
5330
+ }
5331
+ function recommendChart(data, opts = {}) {
5332
+ if (!Array.isArray(data) || data.length === 0) return [];
5333
+ const profiles = profileColumns(data);
5334
+ const quantitative = profiles.filter((p) => p.type === "quantitative");
5335
+ const ordered = profiles.filter((p) => p.type === "temporal" || p.type === "ordinal");
5336
+ const nominal = profiles.filter((p) => p.type === "nominal");
5337
+ const lowNominal = nominal.filter((p) => p.cardinality <= 12);
5338
+ const manyNominal = nominal.filter((p) => p.cardinality > 12);
5339
+ const candidates = [];
5340
+ const measure = quantitative[0];
5341
+ if (ordered.length > 0 && measure) {
5342
+ const x = ordered[0];
5343
+ const encoding = { x: fieldDef2(x), y: fieldDef2(measure) };
5344
+ const series = firstLowCardNominal(profiles, /* @__PURE__ */ new Set([x.field, measure.field]));
5345
+ if (series) encoding.series = fieldDef2(series);
5346
+ pushValid(
5347
+ candidates,
5348
+ {
5349
+ spec: {
5350
+ type: "line",
5351
+ data,
5352
+ encoding,
5353
+ title: `${title(measure.field)} over ${title(x.field)}`
5354
+ },
5355
+ score: 0.92,
5356
+ family: "trend",
5357
+ rationale: `${title(x.field)} is ${x.type}, so a line chart shows how ${title(measure.field)} changes over ordered rows${series ? `, split by low-cardinality ${title(series.field)}` : ""}.`
5358
+ },
5359
+ opts.intent
5360
+ );
5361
+ }
5362
+ if (measure) {
5363
+ const category = lowNominal[0];
5364
+ if (category) {
5365
+ pushValid(
5366
+ candidates,
5367
+ {
5368
+ spec: {
5369
+ type: "bar",
5370
+ data,
5371
+ encoding: { x: fieldDef2(category), y: fieldDef2(measure) },
5372
+ title: `${title(measure.field)} by ${title(category.field)}`
5373
+ },
5374
+ score: 0.84,
5375
+ family: "comparison",
5376
+ rationale: `${title(category.field)} has ${category.cardinality} categories, making bars a good comparison of ${title(measure.field)}.`
5377
+ },
5378
+ opts.intent
5379
+ );
5380
+ if (category.cardinality <= 7) {
5381
+ pushValid(
5382
+ candidates,
5383
+ {
5384
+ spec: {
5385
+ type: "pie",
5386
+ data,
5387
+ encoding: { theta: fieldDef2(measure), color: fieldDef2(category) },
5388
+ title: `${title(measure.field)} share by ${title(category.field)}`
5389
+ },
5390
+ score: 0.58,
5391
+ family: "composition",
5392
+ rationale: `${title(category.field)} has ${category.cardinality} slices, so a pie can show part-to-whole share for ${title(measure.field)}.`
5393
+ },
5394
+ opts.intent
5395
+ );
5396
+ }
5397
+ } else if (manyNominal.length > 0) {
5398
+ const wideCategory = manyNominal[0];
5399
+ pushValid(
5400
+ candidates,
5401
+ {
5402
+ spec: {
5403
+ type: "bar",
5404
+ data,
5405
+ encoding: { x: fieldDef2(wideCategory), y: fieldDef2(measure) },
5406
+ title: `${title(measure.field)} by ${title(wideCategory.field)}`
5407
+ },
5408
+ score: 0.62,
5409
+ family: "comparison",
5410
+ rationale: `${title(wideCategory.field)} has ${wideCategory.cardinality} categories; use this bar chart with top-N filtering or grouping to keep labels readable.`
5411
+ },
5412
+ opts.intent
5413
+ );
5414
+ }
5415
+ }
5416
+ if (quantitative.length >= 2) {
5417
+ const [x, y] = quantitative;
5418
+ const encoding = { x: fieldDef2(x), y: fieldDef2(y) };
5419
+ const size = quantitative[2];
5420
+ const color = firstLowCardNominal(profiles, /* @__PURE__ */ new Set([x.field, y.field]));
5421
+ if (size) encoding.size = fieldDef2(size);
5422
+ if (color) encoding.color = fieldDef2(color);
5423
+ pushValid(
5424
+ candidates,
5425
+ {
5426
+ spec: {
5427
+ type: "scatter",
5428
+ data,
5429
+ encoding,
5430
+ title: `${title(y.field)} vs ${title(x.field)}`
5431
+ },
5432
+ score: 0.86,
5433
+ family: "relationship",
5434
+ rationale: `${title(x.field)} and ${title(y.field)} are quantitative, so scatter reveals their relationship${size ? ` with ${title(size.field)} as bubble size` : ""}${color ? ` and ${title(color.field)} as color` : ""}.`
5435
+ },
5436
+ opts.intent
5437
+ );
5438
+ }
5439
+ if (quantitative.length === 1) {
5440
+ const q = quantitative[0];
5441
+ pushValid(
5442
+ candidates,
5443
+ {
5444
+ spec: {
5445
+ type: "histogram",
5446
+ data,
5447
+ encoding: { x: fieldDef2(q) },
5448
+ title: `${title(q.field)} distribution`
5449
+ },
5450
+ score: 0.78,
5451
+ family: "distribution",
5452
+ rationale: `${title(q.field)} is the only quantitative field, so a histogram shows its distribution.`
5453
+ },
5454
+ opts.intent
5455
+ );
5456
+ pushValid(
5457
+ candidates,
5458
+ {
5459
+ spec: {
5460
+ type: "kpi",
5461
+ data,
5462
+ value: { field: q.field, aggregate: "sum" },
5463
+ label: `Total ${title(q.field)}`,
5464
+ title: `Total ${title(q.field)}`
5465
+ },
5466
+ score: 0.62,
5467
+ family: "value",
5468
+ rationale: `${title(q.field)} is the only quantitative field, so a KPI summarizes it as a total.`
5469
+ },
5470
+ opts.intent
5471
+ );
5472
+ }
5473
+ if (lowNominal.length >= 2 && measure) {
5474
+ const [x, y] = lowNominal;
5475
+ pushValid(
5476
+ candidates,
5477
+ {
5478
+ spec: {
5479
+ type: "heatmap",
5480
+ data,
5481
+ encoding: { x: fieldDef2(x), y: fieldDef2(y), color: fieldDef2(measure) },
5482
+ title: `${title(measure.field)} by ${title(x.field)} and ${title(y.field)}`
5483
+ },
5484
+ score: 0.76,
5485
+ family: "density",
5486
+ rationale: `${title(x.field)} and ${title(y.field)} are low-cardinality categories, so a heatmap shows ${title(measure.field)} across their combinations.`
5487
+ },
5488
+ opts.intent
5489
+ );
5490
+ pushValid(
5491
+ candidates,
5492
+ {
5493
+ spec: {
5494
+ type: "matrix",
5495
+ data,
5496
+ rows: [y.field],
5497
+ columns: [x.field],
5498
+ values: [{ field: measure.field, op: "sum" }],
5499
+ title: `${title(measure.field)} matrix`
5500
+ },
5501
+ score: 0.68,
5502
+ family: "density",
5503
+ rationale: `A matrix summarizes ${title(measure.field)} by ${title(y.field)} rows and ${title(x.field)} columns.`
5504
+ },
5505
+ opts.intent
5506
+ );
5507
+ }
5508
+ return candidates.sort((a, b) => b.score - a.score).slice(0, opts.maxResults ?? 5).map(({ spec, score, rationale }) => ({ spec, score, rationale }));
5509
+ }
5510
+
5511
+ // src/render/env.ts
5512
+ var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
5513
+ function getDevicePixelRatio() {
5514
+ const dpr = globalThis.devicePixelRatio;
5515
+ return typeof dpr === "number" && dpr > 0 ? dpr : 1;
5516
+ }
5517
+
5518
+ // src/render/sizing.ts
5519
+ function computeBackingSize(width, height, dpr) {
5520
+ const d = dpr > 0 ? dpr : 1;
5521
+ const cssWidth = Math.max(0, width);
5522
+ const cssHeight = Math.max(0, height);
5523
+ return {
5524
+ cssWidth,
5525
+ cssHeight,
5526
+ pixelWidth: Math.max(1, Math.round(cssWidth * d)),
5527
+ pixelHeight: Math.max(1, Math.round(cssHeight * d)),
5528
+ dpr: d
5529
+ };
5530
+ }
5531
+
5532
+ // src/render/dom.ts
5533
+ function setStyle(el, style) {
5534
+ Object.assign(el.style, style);
5535
+ }
5536
+ function createDiv(className, style) {
5537
+ const el = document.createElement("div");
5538
+ if (className) el.className = className;
5539
+ if (style) setStyle(el, style);
5540
+ return el;
5541
+ }
5542
+ var fillParentStyle = {
5543
+ position: "absolute",
5544
+ top: "0",
5545
+ left: "0"
5546
+ };
5547
+
5548
+ // src/render/canvasLayer.ts
5549
+ var CanvasLayer = class {
5550
+ constructor(canvas) {
5551
+ __publicField(this, "canvas");
5552
+ __publicField(this, "ctx");
5553
+ /** Logical (CSS px) width. */
5554
+ __publicField(this, "width", 0);
5555
+ /** Logical (CSS px) height. */
5556
+ __publicField(this, "height", 0);
5557
+ __publicField(this, "dpr", 1);
5558
+ this.canvas = canvas ?? document.createElement("canvas");
5559
+ const ctx = this.canvas.getContext("2d");
5560
+ if (!ctx) {
5561
+ throw new Error("Graphein: 2D canvas context is unavailable in this environment.");
5562
+ }
5563
+ this.ctx = ctx;
5564
+ }
5565
+ /** Resize the layer to a CSS size; updates the backing store + DPR transform. */
5566
+ resize(width, height, dpr = getDevicePixelRatio()) {
5567
+ const b = computeBackingSize(width, height, dpr);
5568
+ this.width = b.cssWidth;
5569
+ this.height = b.cssHeight;
5570
+ this.dpr = b.dpr;
5571
+ if (this.canvas.width !== b.pixelWidth) this.canvas.width = b.pixelWidth;
5572
+ if (this.canvas.height !== b.pixelHeight) this.canvas.height = b.pixelHeight;
5573
+ this.canvas.style.width = `${b.cssWidth}px`;
5574
+ this.canvas.style.height = `${b.cssHeight}px`;
5575
+ this.ctx.setTransform(b.dpr, 0, 0, b.dpr, 0, 0);
5576
+ }
5577
+ /** Clear the entire backing store regardless of the current transform. */
5578
+ clear() {
5579
+ const { ctx, canvas } = this;
5580
+ ctx.save();
5581
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
5582
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
5583
+ ctx.restore();
5584
+ }
5585
+ };
5586
+
5587
+ // src/render/surface.ts
5588
+ var Surface = class {
5589
+ constructor(container) {
5590
+ __publicField(this, "container");
5591
+ __publicField(this, "root");
5592
+ __publicField(this, "marks");
5593
+ __publicField(this, "interaction");
5594
+ __publicField(this, "overlay");
5595
+ /** Visually-hidden container for the screen-reader data-table fallback. */
5596
+ __publicField(this, "a11y");
5597
+ __publicField(this, "width", 0);
5598
+ __publicField(this, "height", 0);
5599
+ __publicField(this, "dpr", 1);
5600
+ /**
5601
+ * When true, overlay text (axis labels, titles, legends, annotation labels) is
5602
+ * painted onto the `marks` canvas instead of the HTML overlay. Set by headless
5603
+ * renderers (`@graphein/node`) where there is no DOM to host crisp text.
5604
+ * Defaults to false (browser).
5605
+ */
5606
+ __publicField(this, "headless", false);
5607
+ this.container = container;
5608
+ this.root = createDiv("graphein-root", {
5609
+ position: "relative",
5610
+ width: "100%",
5611
+ height: "100%",
5612
+ overflow: "hidden"
5613
+ });
5614
+ const marksCanvas = document.createElement("canvas");
5615
+ marksCanvas.className = "graphein-layer-marks";
5616
+ setStyle(marksCanvas, { ...fillParentStyle });
5617
+ const interactionCanvas = document.createElement("canvas");
5618
+ interactionCanvas.className = "graphein-layer-interaction";
5619
+ setStyle(interactionCanvas, { ...fillParentStyle, pointerEvents: "none" });
5620
+ this.overlay = createDiv("graphein-layer-overlay", {
5621
+ ...fillParentStyle,
5622
+ width: "100%",
5623
+ height: "100%",
5624
+ pointerEvents: "none"
5625
+ });
5626
+ this.a11y = createDiv("graphein-a11y", {
5627
+ position: "absolute",
5628
+ width: "1px",
5629
+ height: "1px",
5630
+ overflow: "hidden",
5631
+ clip: "rect(0 0 0 0)",
5632
+ clipPath: "inset(50%)",
5633
+ whiteSpace: "nowrap",
5634
+ border: "0",
5635
+ margin: "-1px",
5636
+ padding: "0"
5637
+ });
5638
+ this.root.appendChild(marksCanvas);
5639
+ this.root.appendChild(interactionCanvas);
5640
+ this.root.appendChild(this.overlay);
5641
+ this.root.appendChild(this.a11y);
5642
+ container.appendChild(this.root);
5643
+ this.marks = new CanvasLayer(marksCanvas);
5644
+ this.interaction = new CanvasLayer(interactionCanvas);
5645
+ }
5646
+ resize(width, height, dpr = getDevicePixelRatio()) {
5647
+ this.width = width;
5648
+ this.height = height;
5649
+ this.dpr = dpr;
5650
+ this.marks.resize(width, height, dpr);
5651
+ this.interaction.resize(width, height, dpr);
5652
+ setStyle(this.overlay, { width: `${width}px`, height: `${height}px` });
5653
+ }
5654
+ /** Clear both canvas layers and empty the HTML overlay. */
5655
+ clear() {
5656
+ this.marks.clear();
5657
+ this.interaction.clear();
5658
+ this.overlay.replaceChildren();
5659
+ }
5660
+ /** Remove all DOM created by this Surface. */
5661
+ destroy() {
5662
+ this.clear();
5663
+ this.root.remove();
5664
+ }
5665
+ };
5666
+
5667
+ // src/render/overlayText.ts
5668
+ function paintCanvasText(ctx, c) {
5669
+ const align = c.align ?? "left";
5670
+ const baseline = c.baseline ?? "top";
5671
+ ctx.save();
5672
+ if (c.opacity != null) ctx.globalAlpha *= c.opacity;
5673
+ if (c.rotate) {
5674
+ ctx.translate(c.x, c.y);
5675
+ ctx.rotate(c.rotate);
5676
+ ctx.translate(-c.x, -c.y);
5677
+ }
5678
+ ctx.font = c.font;
5679
+ ctx.textAlign = align;
5680
+ ctx.textBaseline = baseline;
5681
+ if (c.pill) {
5682
+ const padX = c.pill.padX ?? 7;
5683
+ const padY = c.pill.padY ?? 2;
5684
+ const tw = ctx.measureText(c.text).width;
5685
+ const th = c.size;
5686
+ let bx = c.x;
5687
+ if (align === "center") bx = c.x - tw / 2;
5688
+ else if (align === "right") bx = c.x - tw;
5689
+ let by = c.y;
5690
+ if (baseline === "middle") by = c.y - th / 2;
5691
+ else if (baseline === "alphabetic") by = c.y - th;
5692
+ ctx.beginPath();
5693
+ roundedRect(ctx, bx - padX, by - padY, tw + padX * 2, th + padY * 2, c.pill.radius ?? 999);
5694
+ ctx.fillStyle = c.pill.background;
5695
+ ctx.fill();
5696
+ if (c.pill.border) {
5697
+ ctx.lineWidth = 1;
5698
+ ctx.strokeStyle = c.pill.border;
5699
+ ctx.stroke();
5700
+ }
5701
+ }
5702
+ ctx.fillStyle = c.color;
5703
+ ctx.fillText(c.text, c.x, c.y);
5704
+ ctx.restore();
5705
+ }
5706
+ function overlayTextToCanvasCmd(o, font) {
5707
+ const t = o.transform ?? "";
5708
+ const rotate = t.includes("rotate(-90deg)") ? -Math.PI / 2 : t.includes("rotate(90deg)") ? Math.PI / 2 : (() => {
5709
+ const m = t.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
5710
+ return m ? parseFloat(m[1]) * Math.PI / 180 : void 0;
5711
+ })();
5712
+ let align = "left";
5713
+ let x = o.left;
5714
+ const y = o.top;
5715
+ if (o.width != null) {
5716
+ align = o.align ?? "left";
5717
+ x = align === "right" ? o.left + o.width : align === "center" ? o.left + o.width / 2 : o.left;
5718
+ } else if (t.includes("translateX(-100%)")) {
5719
+ align = "right";
5720
+ } else if (t.includes("translateX(-50%)") || t.includes("translate(-50%")) {
5721
+ align = "center";
5722
+ }
5723
+ const baseline = t.includes("translateY(-50%)") || t.includes(",-50%") || t.includes(", -50%") ? "middle" : "top";
5724
+ return {
5725
+ x,
5726
+ y,
5727
+ text: o.text,
5728
+ font,
5729
+ color: o.color,
5730
+ size: o.size,
5731
+ align,
5732
+ baseline,
5733
+ rotate,
5734
+ opacity: o.opacity,
5735
+ pill: o.pill
5736
+ };
5737
+ }
5738
+ function legendSwatchWidth(symbol, swatch = 11) {
5739
+ return symbol === "line" ? swatch + 3 : swatch;
5740
+ }
5741
+ function paintLegendSwatch(ctx, x, midY, symbol, color, swatch = 11) {
5742
+ ctx.save();
5743
+ ctx.fillStyle = color;
5744
+ ctx.beginPath();
5745
+ if (symbol === "circle") {
5746
+ ctx.arc(x + swatch / 2, midY, swatch / 2, 0, Math.PI * 2);
5747
+ } else if (symbol === "line") {
5748
+ roundedRect(ctx, x, midY - 1.5, swatch + 3, 3, 1.5);
5749
+ } else {
5750
+ roundedRect(ctx, x, midY - swatch / 2, swatch, swatch, 2);
5751
+ }
5752
+ ctx.fill();
5753
+ ctx.restore();
5754
+ }
5755
+
5756
+ // src/render/text.ts
5757
+ var measureCtx;
5758
+ var injectedCtx = null;
5759
+ function setMeasureContext(ctx) {
5760
+ injectedCtx = ctx;
5761
+ }
5762
+ function getMeasureCtx() {
5763
+ if (injectedCtx) return injectedCtx;
5764
+ if (measureCtx !== void 0) return measureCtx;
5765
+ try {
5766
+ measureCtx = document.createElement("canvas").getContext("2d");
5767
+ } catch {
5768
+ measureCtx = null;
5769
+ }
5770
+ return measureCtx;
5771
+ }
5772
+ function measureText(text, font) {
5773
+ const ctx = getMeasureCtx();
5774
+ if (!ctx) {
5775
+ const sizeMatch = /(\d+(?:\.\d+)?)px/.exec(font);
5776
+ const size = sizeMatch ? parseFloat(sizeMatch[1]) : 12;
5777
+ return { width: text.length * size * 0.55 };
5778
+ }
5779
+ ctx.font = font;
5780
+ return { width: ctx.measureText(text).width };
5781
+ }
5782
+ function fontString(size, family, weight = 400, style = "normal") {
5783
+ return `${style} ${weight} ${size}px ${family}`;
5784
+ }
5785
+ function ellipsize(text, maxWidth, font) {
5786
+ if (maxWidth <= 0 || text.length === 0) return "";
5787
+ if (measureText(text, font).width <= maxWidth) return text;
5788
+ const ellipsis = "\u2026";
5789
+ const ellipsisW = measureText(ellipsis, font).width;
5790
+ if (ellipsisW > maxWidth) return "";
5791
+ let lo = 0;
5792
+ let hi = text.length;
5793
+ while (lo < hi) {
5794
+ const mid = Math.ceil((lo + hi) / 2);
5795
+ if (measureText(text.slice(0, mid) + ellipsis, font).width <= maxWidth) lo = mid;
5796
+ else hi = mid - 1;
5797
+ }
5798
+ return text.slice(0, lo) + ellipsis;
5799
+ }
5800
+
5801
+ // src/theme/tokens.ts
5802
+ var PALETTE = categorical("graphein");
5803
+ var FONT_FAMILY = 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
5804
+ var SHARED = {
5805
+ font: {
5806
+ family: FONT_FAMILY,
5807
+ size: { tiny: 10, small: 11, base: 12, large: 14, title: 16 },
5808
+ weight: { normal: 400, medium: 500, bold: 600 }
5809
+ },
5810
+ spacing: { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 },
5811
+ radius: { sm: 4, md: 6, lg: 8 },
5812
+ stroke: { thin: 1, base: 1.5, thick: 2 }
5813
+ };
5814
+ var lightTheme = {
5815
+ name: "light",
5816
+ dark: false,
5817
+ color: {
5818
+ background: "#ffffff",
5819
+ surface: "#f8fafc",
5820
+ text: "#0f172a",
5821
+ textMuted: "#64748b",
5822
+ axis: "#cbd5e1",
5823
+ grid: "#eef2f6",
5824
+ border: "#e2e8f0",
5825
+ accent: "#0d9488",
5826
+ palette: PALETTE,
5827
+ positive: "#16a34a",
5828
+ negative: "#dc2626"
5829
+ },
5830
+ ...SHARED
5831
+ };
5832
+ var darkTheme = {
5833
+ name: "dark",
5834
+ dark: true,
5835
+ color: {
5836
+ background: "#0b1220",
5837
+ surface: "#111a2e",
5838
+ text: "#f1f5f9",
5839
+ textMuted: "#94a3b8",
5840
+ axis: "#334155",
5841
+ grid: "#1c2740",
5842
+ border: "#1e293b",
5843
+ accent: "#2dd4bf",
5844
+ palette: PALETTE,
5845
+ positive: "#22c55e",
5846
+ negative: "#f87171"
5847
+ },
5848
+ ...SHARED
5849
+ };
5850
+ var themes = {
5851
+ light: lightTheme,
5852
+ dark: darkTheme
5853
+ };
5854
+ function isPlainObject(v) {
5855
+ return typeof v === "object" && v !== null && !Array.isArray(v);
5856
+ }
5857
+ function deepMerge(base, override) {
5858
+ if (!isPlainObject(override)) return base;
5859
+ if (!isPlainObject(base)) return override;
5860
+ const out = { ...base };
5861
+ for (const [key, value] of Object.entries(override)) {
5862
+ const baseValue = out[key];
5863
+ if (isPlainObject(baseValue) && isPlainObject(value)) {
5864
+ out[key] = deepMerge(baseValue, value);
5865
+ } else {
5866
+ out[key] = value;
5867
+ }
5868
+ }
5869
+ return out;
5870
+ }
5871
+ function resolveTheme(theme) {
5872
+ if (theme == null) return lightTheme;
5873
+ if (typeof theme === "string") return themes[theme] ?? lightTheme;
5874
+ const baseName = theme.base ?? (theme.dark ? "dark" : "light");
5875
+ const base = themes[baseName] ?? lightTheme;
5876
+ const { base: _ignored, ...override } = theme;
5877
+ return deepMerge(base, override);
5878
+ }
5879
+
5880
+ // src/theme/sketch.ts
5881
+ var SKETCH_FONT_NAME = "Patrick Hand";
5882
+ var SKETCH_FONT_FAMILY = `'Patrick Hand', 'Segoe Print', 'Bradley Hand', 'Comic Sans MS', ui-rounded, cursive`;
5883
+ var injected = false;
5884
+ function injectViaStyle(dataUrl) {
5885
+ const style = document.createElement("style");
5886
+ style.setAttribute("data-graphein-sketch-font", "");
5887
+ style.textContent = `@font-face{font-family:'${SKETCH_FONT_NAME}';font-style:normal;font-weight:400;font-display:swap;src:url("${dataUrl}") format("woff2");}`;
5888
+ document.head.appendChild(style);
5889
+ }
5890
+ async function ensureSketchFont() {
5891
+ if (injected) return false;
5892
+ if (typeof document === "undefined") return false;
5893
+ injected = true;
5894
+ try {
5895
+ const { PATRICK_HAND_DATA_URL } = await import('./patrickHand-ITDQJJE2.js');
5896
+ const docFonts = document.fonts;
5897
+ if (typeof FontFace !== "undefined" && docFonts?.add) {
5898
+ const face = new FontFace(
5899
+ SKETCH_FONT_NAME,
5900
+ `url("${PATRICK_HAND_DATA_URL}") format("woff2")`,
5901
+ { style: "normal", weight: "400" }
5902
+ );
5903
+ await face.load();
5904
+ docFonts.add(face);
5905
+ return true;
5906
+ }
5907
+ injectViaStyle(PATRICK_HAND_DATA_URL);
5908
+ return true;
5909
+ } catch {
5910
+ injected = false;
5911
+ return false;
5912
+ }
5913
+ }
5914
+ function withSketchFont(tokens) {
5915
+ return {
5916
+ ...tokens,
5917
+ font: {
5918
+ ...tokens.font,
5919
+ family: `${SKETCH_FONT_FAMILY}`
5663
5920
  }
5921
+ };
5922
+ }
5923
+
5924
+ // src/spec/infer.ts
5925
+ function looksTemporal2(value) {
5926
+ if (value.length < 4) return false;
5927
+ if (/^\d{4}(-\d{2}(-\d{2})?)?([T\s]\d{2}:\d{2})?/.test(value)) {
5928
+ const t = Date.parse(value);
5929
+ return !Number.isNaN(t);
5664
5930
  }
5665
- if (spec.subtitle !== void 0 && typeof spec.subtitle !== "string") {
5666
- err("subtitle", '"subtitle" must be a string.');
5931
+ return false;
5932
+ }
5933
+ function inferValueType(value) {
5934
+ if (value == null) return void 0;
5935
+ if (typeof value === "number") return Number.isFinite(value) ? "quantitative" : void 0;
5936
+ if (value instanceof Date) return "temporal";
5937
+ if (typeof value === "boolean") return "nominal";
5938
+ if (typeof value === "string") {
5939
+ if (value.trim() === "") return void 0;
5940
+ if (looksTemporal2(value)) return "temporal";
5941
+ if (/^-?\d*\.?\d+(e[-+]?\d+)?$/i.test(value.trim())) return "quantitative";
5942
+ return "nominal";
5667
5943
  }
5668
- const layout2 = spec.layout;
5669
- if (layout2 !== void 0) {
5670
- if (!isObject3(layout2)) {
5671
- err("layout", '"layout" must be an object { cols?, rowHeight?, gap?, breakpoints?, navigators?, sections? }.');
5672
- } else {
5673
- for (const k of ["cols", "rowHeight", "gap", "maxWidth", "padding"]) {
5674
- const n = layout2[k];
5675
- if (n !== void 0 && (typeof n !== "number" || !Number.isFinite(n) || n < 0)) {
5676
- err(`layout.${k}`, `"${k}" must be a non-negative number.`);
5677
- }
5678
- }
5679
- if (layout2.navigators !== void 0 && layout2.navigators !== "top" && layout2.navigators !== "inline") {
5680
- err("layout.navigators", 'Expected "top" or "inline".');
5681
- }
5682
- if (layout2.preset !== void 0 && !["auto", "kpi-first", "sidebar"].includes(layout2.preset)) {
5683
- err("layout.preset", 'Expected "auto", "kpi-first", or "sidebar".');
5684
- }
5685
- if (layout2.density !== void 0 && !["compact", "standard", "comfortable"].includes(layout2.density)) {
5686
- err("layout.density", 'Expected "compact", "standard", or "comfortable".');
5687
- }
5688
- if (layout2.breakpoints !== void 0) {
5689
- if (!Array.isArray(layout2.breakpoints)) {
5690
- err("layout.breakpoints", '"breakpoints" must be an array of { maxWidth, cols }.');
5691
- } else {
5692
- layout2.breakpoints.forEach((bp, i) => {
5693
- if (!isObject3(bp)) {
5694
- err(`layout.breakpoints[${i}]`, "Each breakpoint must be an object { maxWidth, cols }.");
5695
- return;
5696
- }
5697
- for (const k of ["maxWidth", "cols"]) {
5698
- const n = bp[k];
5699
- if (typeof n !== "number" || !Number.isFinite(n) || n < 1) {
5700
- err(`layout.breakpoints[${i}].${k}`, `"${k}" must be a positive number.`);
5701
- }
5702
- }
5703
- });
5704
- }
5705
- }
5706
- if (layout2.sections !== void 0) {
5707
- if (!Array.isArray(layout2.sections)) {
5708
- err("layout.sections", '"sections" must be an array of dashboard sections.');
5709
- } else {
5710
- const sectionViewIds = /* @__PURE__ */ new Set();
5711
- layout2.sections.forEach((section, i) => {
5712
- if (!isObject3(section)) {
5713
- err(`layout.sections[${i}]`, "Each section must be an object { views }.");
5714
- return;
5715
- }
5716
- for (const k of ["id", "title", "subtitle", "background"]) {
5717
- if (section[k] !== void 0 && typeof section[k] !== "string") {
5718
- err(`layout.sections[${i}].${k}`, `"${k}" must be a string.`);
5719
- }
5720
- }
5721
- for (const k of ["cols", "rowHeight"]) {
5722
- const n = section[k];
5723
- if (n !== void 0 && (typeof n !== "number" || !Number.isFinite(n) || n < 1)) {
5724
- err(`layout.sections[${i}].${k}`, `"${k}" must be a positive number.`);
5725
- }
5726
- }
5727
- if (section.collapsed !== void 0 && typeof section.collapsed !== "boolean") {
5728
- err(`layout.sections[${i}].collapsed`, '"collapsed" must be a boolean.');
5729
- }
5730
- if (!Array.isArray(section.views)) {
5731
- err(`layout.sections[${i}].views`, '"views" must be an array of view ids.');
5732
- } else {
5733
- section.views.forEach((id, j) => {
5734
- const path = `layout.sections[${i}].views[${j}]`;
5735
- if (typeof id !== "string" || id === "") {
5736
- err(path, "Section view references must be non-empty strings.");
5737
- } else if (!ids.has(id)) {
5738
- err(path, `Section view "${id}" must reference an existing view id.`);
5739
- } else if (sectionViewIds.has(id)) {
5740
- err(path, `View "${id}" is already assigned to another section.`);
5741
- } else {
5742
- sectionViewIds.add(id);
5743
- }
5744
- });
5745
- }
5746
- });
5747
- }
5748
- }
5749
- }
5944
+ return "nominal";
5945
+ }
5946
+ function inferFieldType(data, field, sample = 50) {
5947
+ let seen = 0;
5948
+ const counts = {
5949
+ quantitative: 0,
5950
+ temporal: 0,
5951
+ ordinal: 0,
5952
+ nominal: 0
5953
+ };
5954
+ for (let i = 0; i < data.length && seen < sample; i++) {
5955
+ const t = inferValueType(data[i]?.[field]);
5956
+ if (!t) continue;
5957
+ counts[t]++;
5958
+ seen++;
5750
5959
  }
5751
- return { valid: errors.length === 0, errors, warnings };
5960
+ if (seen === 0) return "nominal";
5961
+ if (counts.temporal >= seen * 0.6) return "temporal";
5962
+ if (counts.quantitative >= seen * 0.6) return "quantitative";
5963
+ return "nominal";
5752
5964
  }
5753
- function assertValidSpec(spec) {
5754
- const { valid, errors } = validateSpec(spec);
5755
- if (!valid) {
5756
- const detail = errors.map((e) => ` - ${e.path || "(root)"}: ${e.message}`).join("\n");
5757
- throw new Error(`Invalid Graphein chart spec:
5758
- ${detail}`);
5965
+ function inferFieldTypes(data) {
5966
+ const fields = /* @__PURE__ */ new Set();
5967
+ for (let i = 0; i < Math.min(data.length, 20); i++) {
5968
+ const row = data[i];
5969
+ if (row) for (const k of Object.keys(row)) fields.add(k);
5759
5970
  }
5760
- return spec;
5971
+ const out = {};
5972
+ for (const f of fields) out[f] = inferFieldType(data, f);
5973
+ return out;
5761
5974
  }
5762
5975
 
5763
5976
  // src/spec/repair.ts
@@ -5784,10 +5997,10 @@ function repairSpec(spec) {
5784
5997
 
5785
5998
  // src/spec/sketch.ts
5786
5999
  function deriveSeed(spec) {
5787
- const title = typeof spec.title === "string" ? spec.title : spec.title?.text ?? spec.title?.subtitle ?? "";
6000
+ const title2 = typeof spec.title === "string" ? spec.title : spec.title?.text ?? spec.title?.subtitle ?? "";
5788
6001
  const dataLen = Array.isArray(spec.data) ? spec.data.length : 0;
5789
6002
  const firstRow = Array.isArray(spec.data) && spec.data[0] ? Object.keys(spec.data[0]).join(",") : "";
5790
- return hashString(`${spec.type}|${title}|${dataLen}|${firstRow}`) || 1;
6003
+ return hashString(`${spec.type}|${title2}|${dataLen}|${firstRow}`) || 1;
5791
6004
  }
5792
6005
  function resolveSketch(spec) {
5793
6006
  const sketch = spec.sketch;
@@ -6242,25 +6455,25 @@ function drawAxisTitles(surface, model) {
6242
6455
  function drawTitle(surface, frame, spec, tokens) {
6243
6456
  if (!frame.titleRect) return;
6244
6457
  const f = tokens.font;
6245
- const title = typeof spec.title === "string" ? { text: spec.title } : spec.title ?? {};
6458
+ const title2 = typeof spec.title === "string" ? { text: spec.title } : spec.title ?? {};
6246
6459
  const align = typeof spec.title === "object" && spec.title?.align || "left";
6247
6460
  const titleFont = fontString(f.size.title, f.family, f.weight.bold);
6248
6461
  addText(surface, titleFont, {
6249
6462
  left: frame.titleRect.x,
6250
6463
  top: frame.titleRect.y,
6251
6464
  width: frame.titleRect.width,
6252
- text: title.text ?? "",
6465
+ text: title2.text ?? "",
6253
6466
  color: tokens.color.text,
6254
6467
  size: f.size.title,
6255
6468
  weight: f.weight.bold,
6256
6469
  align
6257
6470
  });
6258
- if (frame.subtitleRect && title.subtitle) {
6471
+ if (frame.subtitleRect && title2.subtitle) {
6259
6472
  addText(surface, fontString(f.size.small, f.family, f.weight.normal), {
6260
6473
  left: frame.subtitleRect.x,
6261
6474
  top: frame.subtitleRect.y,
6262
6475
  width: frame.subtitleRect.width,
6263
- text: title.subtitle,
6476
+ text: title2.subtitle,
6264
6477
  color: tokens.color.textMuted,
6265
6478
  size: f.size.small,
6266
6479
  align
@@ -6418,6 +6631,10 @@ function formatNumericTick(value, userFormat) {
6418
6631
  return formatNumber2(value, ",");
6419
6632
  }
6420
6633
  var DEFAULT_PADDING = { top: 12, right: 16, bottom: 12, left: 12 };
6634
+ var LEGEND_SWATCH2 = 11;
6635
+ function legendInteractive(spec) {
6636
+ return typeof spec.legend === "object" && spec.legend.interactive === true;
6637
+ }
6421
6638
  function buildCartesianModel(spec, tokens, opts) {
6422
6639
  const data = spec.data ?? [];
6423
6640
  const enc = spec.encoding;
@@ -6428,7 +6645,7 @@ function buildCartesianModel(spec, tokens, opts) {
6428
6645
  const seriesField = resolveSeriesField(spec);
6429
6646
  const rawSeries = groupBySeries(data, seriesField);
6430
6647
  const seriesKeys = rawSeries.map((s) => s.key);
6431
- const palette = tokens.color.palette;
6648
+ const palette = Array.isArray(spec.palette) ? spec.palette : typeof spec.palette === "string" ? categorical(spec.palette) : tokens.color.palette;
6432
6649
  const colorScale = ordinalColorScale({ domain: seriesKeys, palette });
6433
6650
  const colorCache = /* @__PURE__ */ new Map();
6434
6651
  const sharedColor = opts.shared?.colorOf;
@@ -6592,6 +6809,21 @@ function buildCartesianModel(spec, tokens, opts) {
6592
6809
  xAxis: { show: xShow, labels: xLabels, title: resolveAxisTitle(enc.x, xAxisCfg), edgeAnchored: !categories, labelAngle: xAxisCfg.labelAngle },
6593
6810
  yAxis: { show: yShow, labels: yLabels, title: resolveAxisTitle(enc.y, yAxisCfg) }
6594
6811
  });
6812
+ const legendHits = legendInteractive(spec) && seriesField && frame.legendItems ? frame.legendItems.map((item, i) => {
6813
+ const rowH = Math.max(LEGEND_SWATCH2, Math.round(tokens.font.size.small * 1.35));
6814
+ const midY = item.y + LEGEND_SWATCH2 / 2;
6815
+ return {
6816
+ rect: {
6817
+ x: item.x,
6818
+ y: midY - rowH / 2,
6819
+ width: item.width,
6820
+ height: rowH
6821
+ },
6822
+ key: series[i]?.key ?? item.label,
6823
+ value: series[i]?.value ?? item.label,
6824
+ label: item.label
6825
+ };
6826
+ }) : void 0;
6595
6827
  const plot = frame.plot;
6596
6828
  const yRange = [plot.y + plot.height, plot.y];
6597
6829
  const yScale = yIsLog ? logScale({ domain: yDomain, range: yRange, base: yBase, clamp: yClamp }) : linearScale({ domain: yDomain, range: yRange, clamp: yClamp });
@@ -6628,6 +6860,7 @@ function buildCartesianModel(spec, tokens, opts) {
6628
6860
  xTicks,
6629
6861
  yTicks,
6630
6862
  colorOf,
6863
+ legendHits,
6631
6864
  sketch: resolveSketch(spec)
6632
6865
  };
6633
6866
  }
@@ -7137,6 +7370,8 @@ function drawBar(surface, model) {
7137
7370
  }
7138
7371
 
7139
7372
  // src/charts/scatter.ts
7373
+ var QUANTIZE_MIN_POINTS = 4e3;
7374
+ var QUANTIZE_AREA_DIVISOR = 4;
7140
7375
  function isMissing(value) {
7141
7376
  return value == null || value === "";
7142
7377
  }
@@ -7146,6 +7381,35 @@ function isInvalidPositionValue(value) {
7146
7381
  function fixedRadius() {
7147
7382
  return 3.75;
7148
7383
  }
7384
+ function shouldQuantize(count, plot, hasSize) {
7385
+ return !hasSize && count > QUANTIZE_MIN_POINTS && count > plot.width * plot.height / QUANTIZE_AREA_DIVISOR;
7386
+ }
7387
+ function quantizePoints(points) {
7388
+ if (points.length === 0) return [];
7389
+ let minX = Infinity;
7390
+ let maxX = -Infinity;
7391
+ let minY = Infinity;
7392
+ let maxY = -Infinity;
7393
+ for (const p of points) {
7394
+ if (p.x < minX) minX = p.x;
7395
+ if (p.x > maxX) maxX = p.x;
7396
+ if (p.y < minY) minY = p.y;
7397
+ if (p.y > maxY) maxY = p.y;
7398
+ }
7399
+ const seen = /* @__PURE__ */ new Set();
7400
+ const keep = [];
7401
+ for (const p of points) {
7402
+ const extentPoint = p.x === minX || p.x === maxX || p.y === minY || p.y === maxY;
7403
+ const ix = Math.round(p.x);
7404
+ const iy = Math.round(p.y);
7405
+ const key = `${p.seriesKey}|${ix}|${iy}`;
7406
+ if (extentPoint || !seen.has(key)) {
7407
+ keep.push(p);
7408
+ seen.add(key);
7409
+ }
7410
+ }
7411
+ return keep;
7412
+ }
7149
7413
  function radiusScale(rows, field, plotSize) {
7150
7414
  const rMin = 3;
7151
7415
  const rMax = clamp(plotSize * 0.05, 10, 26);
@@ -7188,18 +7452,22 @@ function drawScatter(surface, model) {
7188
7452
  y: py,
7189
7453
  r: readSize ? radius(readSize(row)) : radius(),
7190
7454
  color: series.color,
7191
- alpha: rowAlpha(model.emphasis, row)
7455
+ alpha: rowAlpha(model.emphasis, row),
7456
+ seriesKey: series.key
7192
7457
  });
7193
7458
  }
7194
7459
  }
7195
- if (sizeField) points.sort((a, b) => b.r - a.r);
7460
+ const originalCount = points.length;
7461
+ const drawPoints = shouldQuantize(originalCount, plot, !!sizeField) ? quantizePoints(points) : points;
7462
+ model.markStats = { drawn: drawPoints.length, original: originalCount };
7463
+ if (sizeField) drawPoints.sort((a, b) => b.r - a.r);
7196
7464
  ctx.save();
7197
7465
  ctx.beginPath();
7198
7466
  ctx.rect(plot.x, plot.y, plot.width, plot.height);
7199
7467
  ctx.clip();
7200
7468
  if (model.sketch) {
7201
7469
  const pen = new RoughPen(ctx, model.sketch);
7202
- for (const point of points) {
7470
+ for (const point of drawPoints) {
7203
7471
  const prev = ctx.globalAlpha;
7204
7472
  if (point.alpha !== 1) ctx.globalAlpha = point.alpha;
7205
7473
  pen.circle(point.x, point.y, point.r, {
@@ -7215,7 +7483,7 @@ function drawScatter(surface, model) {
7215
7483
  }
7216
7484
  ctx.lineWidth = 1;
7217
7485
  ctx.strokeStyle = tokens.color.background;
7218
- for (const point of points) {
7486
+ for (const point of drawPoints) {
7219
7487
  ctx.beginPath();
7220
7488
  ctx.arc(point.x, point.y, point.r, 0, Math.PI * 2);
7221
7489
  ctx.fillStyle = point.color;
@@ -7473,10 +7741,10 @@ function buildBoxInteraction(model) {
7473
7741
 
7474
7742
  // src/charts/chrome.ts
7475
7743
  var CHROME_PAD = { top: 14, right: 16, bottom: 14, left: 16 };
7476
- function resolveTitle2(title) {
7477
- if (typeof title === "string") return { text: title, align: "left" };
7478
- if (title && typeof title === "object") {
7479
- const t = title;
7744
+ function resolveTitle2(title2) {
7745
+ if (typeof title2 === "string") return { text: title2, align: "left" };
7746
+ if (title2 && typeof title2 === "object") {
7747
+ const t = title2;
7480
7748
  return { text: t.text, subtitle: t.subtitle, align: t.align ?? "left" };
7481
7749
  }
7482
7750
  return { align: "left" };
@@ -7531,9 +7799,9 @@ function addOverlayText(surface, tokens, o) {
7531
7799
  surface.overlay.appendChild(el);
7532
7800
  return el;
7533
7801
  }
7534
- function drawTitleBlock(surface, tokens, size, title) {
7802
+ function drawTitleBlock(surface, tokens, size, title2) {
7535
7803
  const f = tokens.font;
7536
- const t = resolveTitle2(title);
7804
+ const t = resolveTitle2(title2);
7537
7805
  let top = CHROME_PAD.top;
7538
7806
  const innerWidth = size.width - CHROME_PAD.left - CHROME_PAD.right;
7539
7807
  if (t.text) {
@@ -9912,11 +10180,11 @@ function buildHeader(opts) {
9912
10180
  thead.appendChild(tr);
9913
10181
  return thead;
9914
10182
  }
9915
- function makeHeaderCell(opts, title, colIndex, align, rowIndex = 0) {
10183
+ function makeHeaderCell(opts, title2, colIndex, align, rowIndex = 0) {
9916
10184
  const th = document.createElement("th");
9917
10185
  const metrics = densityMetrics(opts.density);
9918
10186
  th.scope = "col";
9919
- th.textContent = title;
10187
+ th.textContent = title2;
9920
10188
  const isSorted = colIndex != null && opts.sortState?.col === colIndex;
9921
10189
  if (isSorted) th.setAttribute("aria-sort", opts.sortState?.dir === "desc" ? "descending" : "ascending");
9922
10190
  setCellBaseStyles(th, opts.tokens, align ?? "left", opts.stickyHeader !== false, metrics.padding);
@@ -9948,7 +10216,7 @@ function makeHeaderCell(opts, title, colIndex, align, rowIndex = 0) {
9948
10216
  font: "inherit"
9949
10217
  });
9950
10218
  const label = document.createElement("span");
9951
- label.textContent = title;
10219
+ label.textContent = title2;
9952
10220
  label.style.overflow = "hidden";
9953
10221
  label.style.textOverflow = "ellipsis";
9954
10222
  const glyph = document.createElement("span");
@@ -10777,10 +11045,10 @@ function prepareMatrix(spec) {
10777
11045
  },
10778
11046
  ...leafValueColumns.map(({ leaf, valueIndex }) => {
10779
11047
  const valueDef = spec.values[valueIndex];
10780
- const title = spec.values.length > 1 ? valueDef.label ?? valueDef.field : leaf.key;
11048
+ const title2 = spec.values.length > 1 ? valueDef.label ?? valueDef.field : leaf.key;
10781
11049
  return {
10782
11050
  key: `${leaf.path.join(PATH_SEPARATOR2)}:${valueIndex}`,
10783
- title,
11051
+ title: title2,
10784
11052
  align: "right",
10785
11053
  type: "quantitative",
10786
11054
  format: valueDef.showAs && valueDef.showAs !== "value" ? ".1%" : valueDef.format,
@@ -10978,8 +11246,8 @@ function mountSlicerShell(surface, tokens, size, opts = {}) {
10978
11246
  color: c.text,
10979
11247
  font: fontString(tokens.font.size.base, tokens.font.family, tokens.font.weight.normal)
10980
11248
  });
10981
- const title = resolveTitle2(opts.title);
10982
- const labelText2 = title.text ?? opts.label;
11249
+ const title2 = resolveTitle2(opts.title);
11250
+ const labelText2 = title2.text ?? opts.label;
10983
11251
  const header = document.createElement("div");
10984
11252
  Object.assign(header.style, {
10985
11253
  display: "flex",
@@ -10993,16 +11261,16 @@ function mountSlicerShell(surface, tokens, size, opts = {}) {
10993
11261
  labelEl.textContent = labelText2 ?? "";
10994
11262
  Object.assign(labelEl.style, {
10995
11263
  font: fontString(
10996
- title.text ? tokens.font.size.large : tokens.font.size.small,
11264
+ title2.text ? tokens.font.size.large : tokens.font.size.small,
10997
11265
  tokens.font.family,
10998
- title.text ? tokens.font.weight.bold : tokens.font.weight.medium
11266
+ title2.text ? tokens.font.weight.bold : tokens.font.weight.medium
10999
11267
  ),
11000
- color: title.text ? c.text : c.textMuted,
11268
+ color: title2.text ? c.text : c.textMuted,
11001
11269
  whiteSpace: "nowrap",
11002
11270
  overflow: "hidden",
11003
11271
  textOverflow: "ellipsis"
11004
11272
  });
11005
- if (title.subtitle) labelEl.title = title.subtitle;
11273
+ if (title2.subtitle) labelEl.title = title2.subtitle;
11006
11274
  const clearSlot = document.createElement("div");
11007
11275
  clearSlot.style.flex = "0 0 auto";
11008
11276
  header.appendChild(labelEl);
@@ -11104,8 +11372,8 @@ function makePopover(tokens) {
11104
11372
  const c = tokens.color;
11105
11373
  const pop = document.createElement("div");
11106
11374
  Object.assign(pop.style, {
11107
- position: "absolute",
11108
- zIndex: "20",
11375
+ position: "fixed",
11376
+ zIndex: "2147483000",
11109
11377
  background: c.surface,
11110
11378
  border: `1px solid ${c.border}`,
11111
11379
  borderRadius: `${tokens.radius.md}px`,
@@ -11322,14 +11590,26 @@ function makeDualSlider(tokens, opts) {
11322
11590
  }
11323
11591
  };
11324
11592
  }
11325
- function positionPopover(pop, anchor, host) {
11593
+ function positionPopover(pop, anchor) {
11326
11594
  const a = anchor.getBoundingClientRect();
11327
- const h = host.getBoundingClientRect();
11328
- pop.style.left = `${a.left - h.left}px`;
11329
- pop.style.top = `${a.bottom - h.top + 4}px`;
11595
+ const vw = window.innerWidth || document.documentElement.clientWidth || 0;
11596
+ const vh = window.innerHeight || document.documentElement.clientHeight || 0;
11597
+ const margin = 8;
11598
+ const gap = 4;
11330
11599
  pop.style.width = `${a.width}px`;
11331
- const maxH = Math.max(80, h.bottom - a.bottom - 8);
11332
- pop.style.maxHeight = `${maxH}px`;
11600
+ const left = Math.min(Math.max(margin, a.left), Math.max(margin, vw - a.width - margin));
11601
+ pop.style.left = `${left}px`;
11602
+ const spaceBelow = vh - a.bottom - margin;
11603
+ const spaceAbove = a.top - margin;
11604
+ if (spaceBelow >= 160 || spaceBelow >= spaceAbove) {
11605
+ pop.style.top = `${a.bottom + gap}px`;
11606
+ pop.style.bottom = "";
11607
+ pop.style.maxHeight = `${Math.max(80, spaceBelow - gap)}px`;
11608
+ } else {
11609
+ pop.style.top = "";
11610
+ pop.style.bottom = `${vh - a.top + gap}px`;
11611
+ pop.style.maxHeight = `${Math.max(80, spaceAbove - gap)}px`;
11612
+ }
11333
11613
  }
11334
11614
  function clamp7(v, lo, hi) {
11335
11615
  return v < lo ? lo : v > hi ? hi : v;
@@ -11390,12 +11670,14 @@ function emptyNotice(host, message, color) {
11390
11670
  }
11391
11671
 
11392
11672
  // src/charts/slicers/dropdown.ts
11673
+ var openMenus = /* @__PURE__ */ new WeakMap();
11393
11674
  function drawDropdown(surface, spec, tokens, size, context) {
11394
11675
  const s = spec;
11395
11676
  if (surface.headless) {
11396
11677
  drawDropdownCanvas(surface, s, tokens, size, context);
11397
11678
  return;
11398
11679
  }
11680
+ openMenus.get(surface)?.();
11399
11681
  const shell = mountSlicerShell(surface, tokens, size, {
11400
11682
  title: s.title,
11401
11683
  label: slicerLabel(s)
@@ -11429,16 +11711,34 @@ function drawDropdown(surface, spec, tokens, size, context) {
11429
11711
  emit();
11430
11712
  };
11431
11713
  let pop = null;
11714
+ const reposition = () => {
11715
+ if (!pop) return;
11716
+ if (!trigger.isConnected) {
11717
+ closeMenu();
11718
+ return;
11719
+ }
11720
+ positionPopover(pop, trigger);
11721
+ };
11432
11722
  const closeMenu = () => {
11433
11723
  if (pop) {
11434
11724
  pop.remove();
11435
11725
  pop = null;
11436
11726
  document.removeEventListener("pointerdown", onOutside, true);
11727
+ document.removeEventListener("keydown", onKey, true);
11728
+ window.removeEventListener("scroll", reposition, true);
11729
+ window.removeEventListener("resize", reposition);
11730
+ openMenus.delete(surface);
11437
11731
  }
11438
11732
  };
11439
11733
  const onOutside = (e) => {
11440
11734
  if (pop && !pop.contains(e.target) && !trigger.contains(e.target)) closeMenu();
11441
11735
  };
11736
+ const onKey = (e) => {
11737
+ if (e.key === "Escape" && pop) {
11738
+ closeMenu();
11739
+ trigger.focus();
11740
+ }
11741
+ };
11442
11742
  const openMenu = () => {
11443
11743
  if (pop) {
11444
11744
  closeMenu();
@@ -11469,9 +11769,13 @@ function drawDropdown(surface, spec, tokens, size, context) {
11469
11769
  });
11470
11770
  pop.appendChild(row);
11471
11771
  }
11472
- shell.host.appendChild(pop);
11473
- positionPopover(pop, trigger, shell.host);
11772
+ document.body.appendChild(pop);
11773
+ positionPopover(pop, trigger);
11774
+ openMenus.set(surface, closeMenu);
11474
11775
  document.addEventListener("pointerdown", onOutside, true);
11776
+ document.addEventListener("keydown", onKey, true);
11777
+ window.addEventListener("scroll", reposition, true);
11778
+ window.addEventListener("resize", reposition);
11475
11779
  };
11476
11780
  trigger.addEventListener("click", openMenu);
11477
11781
  refreshChrome();
@@ -12131,10 +12435,12 @@ function tooltipEnabled(spec) {
12131
12435
  return true;
12132
12436
  }
12133
12437
  function buildCartesianInteraction(model) {
12134
- if (!tooltipEnabled(model.spec)) return null;
12135
- if (model.series.length === 0) return null;
12136
- if (model.spec.type === "scatter") return scatterInteraction(model);
12137
- return indexInteraction(model);
12438
+ if (!tooltipEnabled(model.spec) || model.series.length === 0) {
12439
+ return model.legendHits?.length ? { region: model.plot, hitTest: () => null, legendHits: model.legendHits } : null;
12440
+ }
12441
+ const interaction = model.spec.type === "scatter" ? scatterInteraction(model) : indexInteraction(model);
12442
+ if (interaction) interaction.legendHits = model.legendHits;
12443
+ return interaction;
12138
12444
  }
12139
12445
  function inside(r, px, py, pad3 = 0) {
12140
12446
  return px >= r.x - pad3 && px <= r.x + r.width + pad3 && py >= r.y - pad3 && py <= r.y + r.height + pad3;
@@ -12285,17 +12591,35 @@ function scatterInteraction(model) {
12285
12591
  }
12286
12592
  }
12287
12593
  if (points.length === 0) return null;
12594
+ const cell = HIT_RADIUS;
12595
+ const grid = /* @__PURE__ */ new Map();
12596
+ for (let i = 0; i < points.length; i++) {
12597
+ const col = Math.floor(points[i].px / cell);
12598
+ const row = Math.floor(points[i].py / cell);
12599
+ const key = `${col},${row}`;
12600
+ const bucket = grid.get(key);
12601
+ if (bucket) bucket.push(i);
12602
+ else grid.set(key, [i]);
12603
+ }
12288
12604
  const nearest = (px, py) => {
12289
12605
  if (!inside(plot, px, py, HIT_RADIUS)) return -1;
12290
12606
  let best = -1;
12291
12607
  let bestSq = HIT_RADIUS * HIT_RADIUS;
12292
- for (let i = 0; i < points.length; i++) {
12293
- const dx = points[i].px - px;
12294
- const dy = points[i].py - py;
12295
- const sq = dx * dx + dy * dy;
12296
- if (sq <= bestSq) {
12297
- bestSq = sq;
12298
- best = i;
12608
+ const col = Math.floor(px / cell);
12609
+ const row = Math.floor(py / cell);
12610
+ for (let dc = -1; dc <= 1; dc++) {
12611
+ for (let dr = -1; dr <= 1; dr++) {
12612
+ const bucket = grid.get(`${col + dc},${row + dr}`);
12613
+ if (!bucket) continue;
12614
+ for (const i of bucket) {
12615
+ const dx = points[i].px - px;
12616
+ const dy = points[i].py - py;
12617
+ const sq = dx * dx + dy * dy;
12618
+ if (sq < bestSq || sq === bestSq && i > best) {
12619
+ bestSq = sq;
12620
+ best = i;
12621
+ }
12622
+ }
12299
12623
  }
12300
12624
  }
12301
12625
  return best;
@@ -13269,8 +13593,8 @@ function resolveValue2(ref, data) {
13269
13593
  function valueField2(ref) {
13270
13594
  return typeof ref === "number" ? null : ref.field;
13271
13595
  }
13272
- function titleText(title) {
13273
- return resolveTitle2(title).text;
13596
+ function titleText(title2) {
13597
+ return resolveTitle2(title2).text;
13274
13598
  }
13275
13599
  function withAlpha3(color, alpha) {
13276
13600
  const rgba2 = parseColor(color);
@@ -13454,9 +13778,9 @@ function drawGauge(surface, spec, tokens, size, context) {
13454
13778
  function clamp9(value, min, max) {
13455
13779
  return Math.max(min, Math.min(max, value));
13456
13780
  }
13457
- function titleText2(title) {
13458
- if (typeof title === "string") return title;
13459
- if (title && typeof title === "object") return title.text;
13781
+ function titleText2(title2) {
13782
+ if (typeof title2 === "string") return title2;
13783
+ if (title2 && typeof title2 === "object") return title2.text;
13460
13784
  return void 0;
13461
13785
  }
13462
13786
  function valueField3(ref) {
@@ -15285,6 +15609,13 @@ var Tooltip = class {
15285
15609
  };
15286
15610
 
15287
15611
  // src/interaction/controller.ts
15612
+ function resolveLegendHit(model, px, py) {
15613
+ for (const hit of model.legendHits ?? []) {
15614
+ const r = hit.rect;
15615
+ if (px >= r.x && px <= r.x + r.width && py >= r.y && py <= r.y + r.height) return hit;
15616
+ }
15617
+ return null;
15618
+ }
15288
15619
  var InteractionController = class {
15289
15620
  constructor(surface, tokens) {
15290
15621
  __publicField(this, "surface", surface);
@@ -15313,9 +15644,17 @@ var InteractionController = class {
15313
15644
  __publicField(this, "onClick", (e) => {
15314
15645
  const cfg = this.selectCfg;
15315
15646
  const model = this.model;
15316
- if (!cfg || !model || !model.pick) return;
15647
+ if (!cfg || !model) return;
15317
15648
  const rect = this.surface.root.getBoundingClientRect();
15318
- cfg.onPick(model.pick(e.clientX - rect.left, e.clientY - rect.top));
15649
+ const px = e.clientX - rect.left;
15650
+ const py = e.clientY - rect.top;
15651
+ const onLegendToggle = cfg.onLegendToggle;
15652
+ const legendHit = onLegendToggle ? resolveLegendHit(model, px, py) : null;
15653
+ if (legendHit && onLegendToggle) {
15654
+ onLegendToggle(legendHit, e);
15655
+ return;
15656
+ }
15657
+ if (model.pick && cfg.onPick) cfg.onPick(model.pick(px, py));
15319
15658
  });
15320
15659
  __publicField(this, "flush", () => {
15321
15660
  this.raf = 0;
@@ -15343,7 +15682,7 @@ var InteractionController = class {
15343
15682
  this.tokens = tokens;
15344
15683
  this.tooltip.setTokens(tokens);
15345
15684
  this.activeKey = null;
15346
- this.pickable = Boolean(model?.pick);
15685
+ this.pickable = Boolean(model?.pick || model?.legendHits?.length);
15347
15686
  if (!model) this.tooltip.hide();
15348
15687
  }
15349
15688
  /** Wire click/tap selection for this chart (or `null` to disable it). */
@@ -15611,6 +15950,14 @@ function buildRenderReport(input) {
15611
15950
  const colorCount = model ? new Set(seriesColors).size || 1 : seriesCount;
15612
15951
  const markCount = model ? model.series.reduce((n, s) => n + s.rows.length, 0) : data.length;
15613
15952
  const plot = model?.plot;
15953
+ if (type === "scatter" && model?.markStats && model.markStats.drawn < model.markStats.original) {
15954
+ add({
15955
+ code: "scatter-quantized",
15956
+ severity: "info",
15957
+ message: `Dense scatter rendering was quantized from ${model.markStats.original} to ${model.markStats.drawn} drawn points to keep interaction smooth.`,
15958
+ details: { drawn: model.markStats.drawn, original: model.markStats.original }
15959
+ });
15960
+ }
15614
15961
  const hasLiteralValue = VALUE_CHART_TYPES.has(type) && typeof spec.value === "number";
15615
15962
  if (data.length === 0 && !hasLiteralValue) {
15616
15963
  add({
@@ -15634,9 +15981,9 @@ function buildRenderReport(input) {
15634
15981
  }
15635
15982
  if (model) {
15636
15983
  checkDegenerateAxis(model, add);
15637
- checkClippedMarks(model, add);
15638
- checkAxisLabelOverlap(model, tokens, add);
15639
- checkLegendOverflow(model, add);
15984
+ checkClippedMarks(model, spec, add);
15985
+ checkAxisLabelOverlap(model, spec, tokens, add);
15986
+ checkLegendOverflow(model, spec, add);
15640
15987
  }
15641
15988
  if (bg) {
15642
15989
  const specColor = spec.color;
@@ -15675,7 +16022,8 @@ function buildRenderReport(input) {
15675
16022
  code: "too-many-colors",
15676
16023
  severity: "info",
15677
16024
  message: `${colorCount} series share one color scale \u2014 beyond ~${MAX_DISTINGUISHABLE_SERIES} the colors repeat or blur together. Consider faceting, filtering, or grouping the long tail.`,
15678
- details: { colorCount, max: MAX_DISTINGUISHABLE_SERIES }
16025
+ details: { colorCount, max: MAX_DISTINGUISHABLE_SERIES },
16026
+ ...tooManyColorsRepair(spec)
15679
16027
  });
15680
16028
  }
15681
16029
  diagnostics.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
@@ -15683,6 +16031,11 @@ function buildRenderReport(input) {
15683
16031
  const summary = summarize({ ...spec, data }) || void 0;
15684
16032
  return { type, size, plot, markCount, seriesCount, colorCount, ok, diagnostics, summary };
15685
16033
  }
16034
+ function repairReport(spec, report) {
16035
+ const applied = report.diagnostics.flatMap((d) => d.fix ?? []);
16036
+ if (applied.length === 0) return { spec, applied: [] };
16037
+ return { spec: applyPatch(spec, applied), applied };
16038
+ }
15686
16039
  function seriesCountOf(spec, data) {
15687
16040
  const enc = spec.encoding;
15688
16041
  const field = enc?.series?.field ?? enc?.color?.field;
@@ -15714,7 +16067,7 @@ function checkDegenerateAxis(model, add) {
15714
16067
  });
15715
16068
  }
15716
16069
  }
15717
- function checkClippedMarks(model, add) {
16070
+ function checkClippedMarks(model, spec, add) {
15718
16071
  const field = model.y.field;
15719
16072
  if (!field) return;
15720
16073
  const read = accessor(field);
@@ -15740,11 +16093,14 @@ function checkClippedMarks(model, add) {
15740
16093
  severity: "warning",
15741
16094
  axis: "y",
15742
16095
  message: `Some data falls outside the y-axis range [${round(Math.min(lo, hi))}, ${round(Math.max(lo, hi))}] and is clipped at the plot edge \u2014 widen the y domain or drop the manual scale.`,
15743
- details: { dataMin: round(lo), dataMax: round(hi) }
16096
+ details: { dataMin: round(lo), dataMax: round(hi) },
16097
+ ...manualYDomainFix(spec) ?? {
16098
+ hint: "Drop the manual y scale domain or set a y scale domain/zero option that includes every data value."
16099
+ }
15744
16100
  });
15745
16101
  }
15746
16102
  }
15747
- function checkAxisLabelOverlap(model, tokens, add) {
16103
+ function checkAxisLabelOverlap(model, spec, tokens, add) {
15748
16104
  const ticks2 = model.xTicks;
15749
16105
  if (ticks2.length < 2) return;
15750
16106
  if (model.frame.xLabelAngle > 0) return;
@@ -15764,11 +16120,14 @@ function checkAxisLabelOverlap(model, tokens, add) {
15764
16120
  severity: "warning",
15765
16121
  axis: "x",
15766
16122
  message: `${collisions} adjacent x-axis label${collisions === 1 ? "" : "s"} overlap \u2014 there isn't room to show all ${ticks2.length} categories. Reduce categories, widen the chart, or aggregate the long tail.`,
15767
- details: { collisions, ticks: ticks2.length }
16123
+ details: { collisions, ticks: ticks2.length },
16124
+ ...xLabelAngleFix(spec) ?? {
16125
+ hint: "Rotate x-axis labels, facet the chart, widen it, or aggregate the long tail."
16126
+ }
15768
16127
  });
15769
16128
  }
15770
16129
  }
15771
- function checkLegendOverflow(model, add) {
16130
+ function checkLegendOverflow(model, spec, add) {
15772
16131
  const shown = model.frame.legendItems?.length;
15773
16132
  if (shown === void 0) return;
15774
16133
  const total = model.series.length;
@@ -15777,10 +16136,52 @@ function checkLegendOverflow(model, add) {
15777
16136
  code: "legend-overflow",
15778
16137
  severity: "warning",
15779
16138
  message: `The legend shows only ${shown} of ${total} series \u2014 the rest were clipped to fit. Move the legend to the top/bottom or give the chart more height.`,
15780
- details: { shown, total }
16139
+ details: { shown, total },
16140
+ ...legendPositionFix(spec) ?? {
16141
+ hint: "Move the legend to top/bottom, increase chart height, filter series, or facet the chart."
16142
+ }
15781
16143
  });
15782
16144
  }
15783
16145
  }
16146
+ function isRecord(v) {
16147
+ return v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
16148
+ }
16149
+ function xLabelAngleFix(spec) {
16150
+ const axes = spec.axes;
16151
+ if (!isRecord(axes)) return void 0;
16152
+ const x = axes.x;
16153
+ if (x === void 0) return { fix: [{ op: "add", path: "/axes/x", value: { labelAngle: 45 } }] };
16154
+ if (!isRecord(x)) return void 0;
16155
+ const op = Object.prototype.hasOwnProperty.call(x, "labelAngle") ? "replace" : "add";
16156
+ return { fix: [{ op, path: "/axes/x/labelAngle", value: 45 }] };
16157
+ }
16158
+ function legendPositionFix(spec) {
16159
+ const legend = spec.legend;
16160
+ if (legend === false) return void 0;
16161
+ if (legend === void 0 || legend === true) {
16162
+ return { fix: [{ op: legend === void 0 ? "add" : "replace", path: "/legend", value: { position: "bottom" } }] };
16163
+ }
16164
+ if (!isRecord(legend)) return void 0;
16165
+ const current = legend.position;
16166
+ if (current === "bottom" || current === "top") return void 0;
16167
+ const op = Object.prototype.hasOwnProperty.call(legend, "position") ? "replace" : "add";
16168
+ return { fix: [{ op, path: "/legend/position", value: "bottom" }] };
16169
+ }
16170
+ function tooManyColorsRepair(spec) {
16171
+ const palette = spec.palette;
16172
+ const hint = "Switch to the colorblind palette, facet the chart, filter series, or group the long tail.";
16173
+ if (Array.isArray(palette) || palette === "colorblind") return { hint };
16174
+ return { fix: [{ op: palette === void 0 ? "add" : "replace", path: "/palette", value: "colorblind" }], hint };
16175
+ }
16176
+ function manualYDomainFix(spec) {
16177
+ const enc = spec.encoding;
16178
+ if (!isRecord(enc)) return void 0;
16179
+ const y = enc.y;
16180
+ if (!isRecord(y)) return void 0;
16181
+ const scale = y.scale;
16182
+ if (!isRecord(scale) || !Object.prototype.hasOwnProperty.call(scale, "domain")) return void 0;
16183
+ return { fix: [{ op: "remove", path: "/encoding/y/scale/domain" }] };
16184
+ }
15784
16185
 
15785
16186
  // src/runtime/facet.ts
15786
16187
  var FACETABLE_TYPES2 = /* @__PURE__ */ new Set(["line", "area", "bar", "scatter"]);
@@ -15816,9 +16217,9 @@ function autoColumns(n) {
15816
16217
  function legendSymbol(type) {
15817
16218
  return type === "scatter" ? "circle" : type === "line" ? "line" : "square";
15818
16219
  }
15819
- function titleInput3(title) {
15820
- if (!title) return void 0;
15821
- return typeof title === "string" ? { text: title } : title;
16220
+ function titleInput3(title2) {
16221
+ if (!title2) return void 0;
16222
+ return typeof title2 === "string" ? { text: title2 } : title2;
15822
16223
  }
15823
16224
  function panelSpec(spec, data, value) {
15824
16225
  const base = spec;
@@ -15989,6 +16390,60 @@ var CANVAS_MARK_TYPES = /* @__PURE__ */ new Set([
15989
16390
  "choropleth"
15990
16391
  ]);
15991
16392
  var EMPTY_DATA = [];
16393
+ function legendConfigFor(model) {
16394
+ const legend = model.spec.legend;
16395
+ if (!(typeof legend === "object" && legend.interactive === true)) return null;
16396
+ const field = model.seriesField;
16397
+ if (!field || !model.legendHits?.length) return null;
16398
+ return { param: legend.param ?? field, field };
16399
+ }
16400
+ function visibleLegendKeys(value, field) {
16401
+ if (!value || value.kind !== "set" || value.field !== field) return null;
16402
+ return new Set(value.values.map((v) => toKey(v)));
16403
+ }
16404
+ function applyLegendVisibility(model, store) {
16405
+ const cfg = legendConfigFor(model);
16406
+ if (!cfg) return null;
16407
+ const visible = visibleLegendKeys(store.get(cfg.param), cfg.field);
16408
+ if (!visible) return cfg;
16409
+ model.series = model.series.filter((s) => visible.has(toKey(s.value)));
16410
+ return cfg;
16411
+ }
16412
+ function publishLegendToggle(store, cfg, allHits, hit, isolate) {
16413
+ const all = allHits.map((h) => h.value);
16414
+ const allKeys = all.map((v) => toKey(v));
16415
+ const hitKey = toKey(hit.value);
16416
+ const current = visibleLegendKeys(store.get(cfg.param), cfg.field);
16417
+ const visible = current ? new Set(current) : new Set(allKeys);
16418
+ if (isolate) {
16419
+ if (visible.size === 1 && visible.has(hitKey)) {
16420
+ store.set(cfg.param, null);
16421
+ return;
16422
+ }
16423
+ visible.clear();
16424
+ visible.add(hitKey);
16425
+ } else if (visible.has(hitKey)) {
16426
+ visible.delete(hitKey);
16427
+ } else {
16428
+ visible.add(hitKey);
16429
+ }
16430
+ if (visible.size === 0 || visible.size === allKeys.length) {
16431
+ store.set(cfg.param, null);
16432
+ return;
16433
+ }
16434
+ store.set(cfg.param, {
16435
+ kind: "set",
16436
+ field: cfg.field,
16437
+ values: all.filter((v) => visible.has(toKey(v)))
16438
+ });
16439
+ }
16440
+ function specLegendParam(spec) {
16441
+ const legend = spec.legend;
16442
+ if (!(typeof legend === "object" && legend.interactive === true)) return null;
16443
+ if (legend.param) return legend.param;
16444
+ const enc = spec.encoding;
16445
+ return enc?.series?.field ?? enc?.color?.field ?? null;
16446
+ }
15992
16447
  function resolveContainer(target) {
15993
16448
  if (typeof target === "string") {
15994
16449
  const el = document.querySelector(target);
@@ -16084,6 +16539,7 @@ function render(target, spec, options) {
16084
16539
  let interactionModel = null;
16085
16540
  let reportModel;
16086
16541
  let facetRpt;
16542
+ let legendSelect = null;
16087
16543
  const type = currentSpec.type;
16088
16544
  const facetLayout = isFaceted(effectiveSpec) ? buildFacetModels(effectiveSpec, tokens, size) : null;
16089
16545
  if (facetLayout) {
@@ -16092,6 +16548,8 @@ function render(target, spec, options) {
16092
16548
  } else if (CARTESIAN_TYPES.has(type)) {
16093
16549
  const renderer = cartesianRenderers[type];
16094
16550
  const model = buildCartesianModel(effectiveSpec, tokens, size);
16551
+ const legendCfg = applyLegendVisibility(model, store);
16552
+ if (legendCfg && model.legendHits?.length) legendSelect = { cfg: legendCfg, hits: model.legendHits };
16095
16553
  model.emphasis = emphasis;
16096
16554
  reportModel = model;
16097
16555
  if (!anim.enabled) {
@@ -16140,9 +16598,14 @@ function render(target, spec, options) {
16140
16598
  }
16141
16599
  if (!interaction) interaction = new InteractionController(surface, tokens);
16142
16600
  interaction.setModel(interactionModel, tokens);
16143
- if (ownParam && interactionModel?.pick) {
16144
- const cfg = { store, param: ownParam.name, def: ownParam.select };
16145
- interaction.setSelect({ onPick: (value) => applyPick(cfg, value) });
16601
+ if (ownParam && interactionModel?.pick || legendSelect && interactionModel?.legendHits?.length) {
16602
+ const cfg = ownParam ? { store, param: ownParam.name, def: ownParam.select } : null;
16603
+ interaction.setSelect({
16604
+ onPick: cfg && interactionModel?.pick ? (value) => applyPick(cfg, value) : void 0,
16605
+ onLegendToggle: legendSelect ? (hit, event) => {
16606
+ publishLegendToggle(store, legendSelect.cfg, legendSelect.hits, hit, event.shiftKey || event.altKey);
16607
+ } : void 0
16608
+ });
16146
16609
  } else {
16147
16610
  interaction.setSelect(null);
16148
16611
  }
@@ -16199,7 +16662,7 @@ function render(target, spec, options) {
16199
16662
  const unsubscribe = store.subscribe((name, value) => {
16200
16663
  for (const listener of [...hostListeners]) listener(name, value);
16201
16664
  if (destroyed) return;
16202
- if (dependentParams(currentSpec.highlight, currentSpec.filter).has(name)) {
16665
+ if (dependentParams(currentSpec.highlight, currentSpec.filter).has(name) || specLegendParam(currentSpec) === name) {
16203
16666
  draw(false);
16204
16667
  }
16205
16668
  });
@@ -16380,6 +16843,14 @@ function isSlicerType(type) {
16380
16843
  function fieldOf(channel) {
16381
16844
  return channel && typeof channel.field === "string" ? channel.field : void 0;
16382
16845
  }
16846
+ function legendSource(spec) {
16847
+ const legend = spec.legend;
16848
+ if (!(typeof legend === "object" && legend.interactive === true)) return null;
16849
+ const enc = spec.encoding;
16850
+ const field = fieldOf(enc?.series) ?? fieldOf(enc?.color);
16851
+ if (!field) return null;
16852
+ return { param: legend.param ?? field, field };
16853
+ }
16383
16854
  function specFields(spec) {
16384
16855
  const out = /* @__PURE__ */ new Set();
16385
16856
  if (isSlicerType(spec.type)) {
@@ -16468,9 +16939,14 @@ function wireViews(views, interactions) {
16468
16939
  });
16469
16940
  } else if (PICKABLE.has(type)) {
16470
16941
  const fields = keyFields(v.spec);
16471
- if (fields.length === 0) return;
16472
- const param = autoParamName(v);
16473
- sources.push({ view: i, param, fields, as: "highlight", slicer: false });
16942
+ if (fields.length > 0) {
16943
+ const param = autoParamName(v);
16944
+ sources.push({ view: i, param, fields, as: "highlight", slicer: false });
16945
+ }
16946
+ const legend = legendSource(v.spec);
16947
+ if (legend) {
16948
+ sources.push({ view: i, param: legend.param, fields: [legend.field], as: "filter", slicer: true });
16949
+ }
16474
16950
  }
16475
16951
  });
16476
16952
  const explicit = Array.isArray(interactions);
@@ -16536,6 +17012,14 @@ var DEFAULT_BREAKPOINTS = [
16536
17012
  { maxWidth: 960, cols: 6 }
16537
17013
  ];
16538
17014
  var STRIP_SLICERS = /* @__PURE__ */ new Set(["dropdown", "search", "dateRange", "range"]);
17015
+ function stripCellHeight(view, rowHeight) {
17016
+ const titled = !!(view.title || view.subtitle);
17017
+ const titlePad = titled ? 30 : 0;
17018
+ const base = Math.max(96, rowHeight + 8);
17019
+ if (view.spec.type === "dateRange") return Math.max(base, 150 + titlePad);
17020
+ if (view.spec.type === "range") return Math.max(base, 132 + titlePad);
17021
+ return Math.max(base, 96 + titlePad);
17022
+ }
16539
17023
  var clamp10 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
16540
17024
  function defaultSpan(spec, cols) {
16541
17025
  const t = spec.type;
@@ -16799,14 +17283,14 @@ function renderDashboard(target, spec, options) {
16799
17283
  gap: "2px"
16800
17284
  });
16801
17285
  if (view.title) {
16802
- const title = document.createElement("div");
16803
- title.textContent = view.title;
16804
- Object.assign(title.style, {
17286
+ const title2 = document.createElement("div");
17287
+ title2.textContent = view.title;
17288
+ Object.assign(title2.style, {
16805
17289
  font: `${tokens.font.weight.bold} ${tokens.font.size.base}px ${tokens.font.family}`,
16806
17290
  color: tokens.color.text,
16807
17291
  letterSpacing: "-0.01em"
16808
17292
  });
16809
- header.appendChild(title);
17293
+ header.appendChild(title2);
16810
17294
  }
16811
17295
  if (view.subtitle) {
16812
17296
  const subtitle = document.createElement("div");
@@ -16838,6 +17322,7 @@ function renderDashboard(target, spec, options) {
16838
17322
  Object.assign(strip.style, {
16839
17323
  display: "flex",
16840
17324
  flexWrap: "wrap",
17325
+ alignItems: "flex-start",
16841
17326
  gap: `${layout2.gap}px`
16842
17327
  });
16843
17328
  for (const view of stripViews) {
@@ -16845,7 +17330,7 @@ function renderDashboard(target, spec, options) {
16845
17330
  Object.assign(cell.style, {
16846
17331
  flex: "1 1 220px",
16847
17332
  minWidth: "200px",
16848
- height: `${Math.max(96, layout2.rowHeight + 8)}px`
17333
+ height: `${stripCellHeight(view, layout2.rowHeight)}px`
16849
17334
  });
16850
17335
  strip.appendChild(cell);
16851
17336
  deferredMounts.push({ el: host, view, mounted: false });
@@ -16885,10 +17370,10 @@ function renderDashboard(target, spec, options) {
16885
17370
  const titles = document.createElement("span");
16886
17371
  Object.assign(titles.style, { display: "flex", flexDirection: "column", gap: "2px" });
16887
17372
  if (plan.section?.title) {
16888
- const title = document.createElement("span");
16889
- title.textContent = plan.section.title;
16890
- Object.assign(title.style, { fontWeight: String(tokens.font.weight.bold), fontSize: `${tokens.font.size.large}px` });
16891
- titles.appendChild(title);
17373
+ const title2 = document.createElement("span");
17374
+ title2.textContent = plan.section.title;
17375
+ Object.assign(title2.style, { fontWeight: String(tokens.font.weight.bold), fontSize: `${tokens.font.size.large}px` });
17376
+ titles.appendChild(title2);
16892
17377
  }
16893
17378
  if (plan.section?.subtitle) {
16894
17379
  const subtitle = document.createElement("span");
@@ -17231,6 +17716,6 @@ function renderToContext(target, spec) {
17231
17716
  // src/index.ts
17232
17717
  var VERSION = "0.1.0";
17233
17718
 
17234
- export { CARTESIAN_TYPES, CHART_TYPES, CanvasLayer, DEFAULT_ENTRANCE_DURATION, DEFAULT_ROUGH_STYLE, DEFAULT_UPDATE_DURATION, DIM_ALPHA, ExpressionError, FACETABLE_TYPES2 as FACETABLE_TYPES, FUNCTION_NAMES, InteractionController, RoughPen, SKETCH_FONT_FAMILY, SKETCH_FONT_NAME, SLICER_TYPES, SR_ONLY_STYLE, Surface, TICK_SIZE, TIME_UNITS, TRANSFORM_KINDS, Tooltip, VERSION, aggregateValues, analyzeChart, animate, applyA11y, applyAggregate, applyBin, applyCalculate, applyFilter, applyFold, applyPatch, applyPick, applyTimeUnit, applyTransform, applyTransforms, arc, area, assertValidSpec, autoInsightAnnotations, backOut, bandScale, bounceOut, buildCartesianInteraction, buildCartesianModel, buildComboModel, buildDataTableFallback, buildFacetModels, buildHistogramModel, buildRenderReport, cartesianInteractionBuilders, cartesianRenderers, categorical, chartTitleText, chartTypeLabel, checkExpression, clamp012 as clamp01, clamp255, compileExpression, compilePredicate, computeBackingSize, computeBins, computeFrame, computeSeriesInsights, contrastRatio, createDiv, createRoughPen, createSelectionStore, cubicIn, cubicInOut, cubicOut, curveCatmullRom, curveLinear, curveMonotoneX, curveStep, curveStepAfter, curveStepBefore, customRenderers, darkTheme, decimate, defaultSpan, dependentParams, diverging, divergingColorScale, divergingSchemes, drawAnnotationLabels, drawAnnotations, drawAxesUnderlay, drawFacet, drawLegend, drawOverlay, drawTitle, drawTrendlineLabels, drawTrendlines, easings, elasticOut, ellipsize, ensureSketchFont, expoInOut, expoOut, facetReport, fillParentStyle, filterRows, fontString, formatDate, formatNumber2 as formatNumber, formatValue, getDevicePixelRatio, groupBy, hashString, inferFieldType, inferFieldTypes, inferValueType, inheritInto, interpolateArray, interpolateNumber, interpolateNumberArray, interpolateObject, interpolateOklab, interpolateRgb, isBrowser, isEmptyValue, isFaceted, isParamClause, isSlicerType, keyFields, legendSwatchWidth, lightTheme, line, linear, linearRegression, linearScale, linearToSrgb, lintSpec, literalToValue, logScale, lttbRun, makeMatcher, matchesValue, measureText, monotoneXTangents, mulberry32, niceDomain, oklabToOklch, oklabToRgb, oklchToOklab, ordinalColorScale, overlayTextToCanvasCmd, paintCanvasText, paintLegendSwatch, parseColor, parseNumberFormat, pivot, placeViews, pointScale, polygonHachureLines, prefersReducedMotion, prettyDate, quadIn, quadInOut, quadOut, rampFromStops, readableTextColor, relativeLuminance, render, renderDashboard, renderToContext, repairSpec, resolveCurve, resolveDashboardLayout, resolveDashboardSections, resolveEmphasis, resolveEntrance, resolveFilterValues, resolveInsightOptions, resolveSketch, resolveTheme, resolveUpdate, rgbToOklab, rgbaToCss, rotatePoint, roundedRect, sampleArc, sequential, sequentialColorScale, sequentialSchemes, setMeasureContext, setStyle, sinInOut, slicerParamName, smartDate, specFields, srgbToLinear, summarize, summarizeChart, themes, tickIncrement, tickStep, ticks, timeScale, timeTickFormat, timeTicks, toHex, toPointer, tooltipEnabled, truncateTo, validateDashboard, validateSpec, wireViews, withAlpha, withSketchFont };
17719
+ export { CARTESIAN_TYPES, CHART_TYPES, CanvasLayer, DEFAULT_ENTRANCE_DURATION, DEFAULT_ROUGH_STYLE, DEFAULT_UPDATE_DURATION, DIM_ALPHA, ExpressionError, FACETABLE_TYPES2 as FACETABLE_TYPES, FUNCTION_NAMES, InteractionController, RoughPen, SKETCH_FONT_FAMILY, SKETCH_FONT_NAME, SLICER_TYPES, SR_ONLY_STYLE, Surface, TICK_SIZE, TIME_UNITS, TRANSFORM_KINDS, Tooltip, VERSION, aggregateValues, analyzeChart, animate, applyA11y, applyAggregate, applyBin, applyCalculate, applyFilter, applyFold, applyPatch, applyPick, applyTimeUnit, applyTransform, applyTransforms, arc, area, assertValidSpec, autoInsightAnnotations, backOut, bandScale, bounceOut, buildCartesianInteraction, buildCartesianModel, buildComboModel, buildDataTableFallback, buildFacetModels, buildHistogramModel, buildRenderReport, cartesianInteractionBuilders, cartesianRenderers, categorical, categoricalSchemes, chartTitleText, chartTypeLabel, checkExpression, clamp012 as clamp01, clamp255, compileExpression, compilePredicate, computeBackingSize, computeBins, computeFrame, computeSeriesInsights, contrastRatio, createDiv, createRoughPen, createSelectionStore, cubicIn, cubicInOut, cubicOut, curveCatmullRom, curveLinear, curveMonotoneX, curveStep, curveStepAfter, curveStepBefore, customRenderers, darkTheme, decimate, defaultSpan, dependentParams, diverging, divergingColorScale, divergingSchemes, drawAnnotationLabels, drawAnnotations, drawAxesUnderlay, drawFacet, drawLegend, drawOverlay, drawTitle, drawTrendlineLabels, drawTrendlines, easings, elasticOut, ellipsize, ensureSketchFont, expoInOut, expoOut, facetReport, fillParentStyle, filterRows, fontString, formatDate, formatNumber2 as formatNumber, formatValue, getDevicePixelRatio, groupBy, hashString, inferFieldType, inferFieldTypes, inferValueType, inheritInto, interpolateArray, interpolateNumber, interpolateNumberArray, interpolateObject, interpolateOklab, interpolateRgb, isBrowser, isEmptyValue, isFaceted, isParamClause, isSlicerType, keyFields, legendSwatchWidth, lightTheme, line, linear, linearRegression, linearScale, linearToSrgb, lintSpec, literalToValue, logScale, lttbRun, makeMatcher, matchesValue, measureText, monotoneXTangents, mulberry32, niceDomain, oklabToOklch, oklabToRgb, oklchToOklab, ordinalColorScale, overlayTextToCanvasCmd, paintCanvasText, paintLegendSwatch, parseColor, parseNumberFormat, pivot, placeViews, pointScale, polygonHachureLines, prefersReducedMotion, prettyDate, profileColumns, quadIn, quadInOut, quadOut, rampFromStops, readableTextColor, recommendChart, relativeLuminance, render, renderDashboard, renderToContext, repairReport, repairSpec, resolveCurve, resolveDashboardLayout, resolveDashboardSections, resolveEmphasis, resolveEntrance, resolveFilterValues, resolveInsightOptions, resolveSketch, resolveTheme, resolveUpdate, rgbToOklab, rgbaToCss, rotatePoint, roundedRect, sampleArc, sequential, sequentialColorScale, sequentialSchemes, setMeasureContext, setStyle, sinInOut, slicerParamName, smartDate, specFields, srgbToLinear, summarize, summarizeChart, themes, tickIncrement, tickStep, ticks, timeScale, timeTickFormat, timeTicks, toHex, toPointer, tooltipEnabled, truncateTo, validateDashboard, validateSpec, wireViews, withAlpha, withSketchFont };
17235
17720
  //# sourceMappingURL=index.js.map
17236
17721
  //# sourceMappingURL=index.js.map