graphein 0.9.0 → 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,1145 +3093,726 @@ 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
-
3349
- // src/theme/tokens.ts
3350
- var PALETTE = [
3351
- "#3b82f6",
3352
- // blue
3353
- "#14b8a6",
3354
- // teal
3355
- "#f59e0b",
3356
- // amber
3357
- "#ef4444",
3358
- // red
3359
- "#8b5cf6",
3360
- // violet
3361
- "#ec4899",
3362
- // pink
3363
- "#10b981",
3364
- // emerald
3365
- "#f97316",
3366
- // orange
3367
- "#6366f1",
3368
- // indigo
3369
- "#84cc16"
3370
- // lime
3371
- ];
3372
- var FONT_FAMILY = 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
3373
- var SHARED = {
3374
- font: {
3375
- family: FONT_FAMILY,
3376
- size: { tiny: 10, small: 11, base: 12, large: 14, title: 16 },
3377
- weight: { normal: 400, medium: 500, bold: 600 }
3378
- },
3379
- spacing: { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 },
3380
- radius: { sm: 4, md: 6, lg: 8 },
3381
- stroke: { thin: 1, base: 1.5, thick: 2 }
3382
- };
3383
- var lightTheme = {
3384
- name: "light",
3385
- dark: false,
3386
- color: {
3387
- background: "#ffffff",
3388
- surface: "#f8fafc",
3389
- text: "#0f172a",
3390
- textMuted: "#64748b",
3391
- axis: "#cbd5e1",
3392
- grid: "#eef2f6",
3393
- border: "#e2e8f0",
3394
- accent: "#0d9488",
3395
- palette: PALETTE,
3396
- positive: "#16a34a",
3397
- negative: "#dc2626"
3398
- },
3399
- ...SHARED
3400
- };
3401
- var darkTheme = {
3402
- name: "dark",
3403
- dark: true,
3404
- color: {
3405
- background: "#0b1220",
3406
- surface: "#111a2e",
3407
- text: "#f1f5f9",
3408
- textMuted: "#94a3b8",
3409
- axis: "#334155",
3410
- grid: "#1c2740",
3411
- border: "#1e293b",
3412
- accent: "#2dd4bf",
3413
- palette: PALETTE,
3414
- positive: "#22c55e",
3415
- negative: "#f87171"
3416
- },
3417
- ...SHARED
3418
- };
3419
- var themes = {
3420
- light: lightTheme,
3421
- dark: darkTheme
3422
- };
3423
- function isPlainObject(v) {
3424
- return typeof v === "object" && v !== null && !Array.isArray(v);
3425
- }
3426
- function deepMerge(base, override) {
3427
- if (!isPlainObject(override)) return base;
3428
- if (!isPlainObject(base)) return override;
3429
- const out = { ...base };
3430
- for (const [key, value] of Object.entries(override)) {
3431
- const baseValue = out[key];
3432
- if (isPlainObject(baseValue) && isPlainObject(value)) {
3433
- out[key] = deepMerge(baseValue, value);
3434
- } else {
3435
- out[key] = value;
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
+ }
3436
3429
  }
3430
+ return node;
3437
3431
  }
3438
- return out;
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 };
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);
3463
+ }
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;
3439
3473
  }
3440
- function resolveTheme(theme) {
3441
- if (theme == null) return lightTheme;
3442
- if (typeof theme === "string") return themes[theme] ?? lightTheme;
3443
- const baseName = theme.base ?? (theme.dark ? "dark" : "light");
3444
- const base = themes[baseName] ?? lightTheme;
3445
- const { base: _ignored, ...override } = theme;
3446
- 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();
3447
3485
  }
3448
3486
 
3449
- // src/theme/sketch.ts
3450
- var SKETCH_FONT_NAME = "Patrick Hand";
3451
- var SKETCH_FONT_FAMILY = `'Patrick Hand', 'Segoe Print', 'Bradley Hand', 'Comic Sans MS', ui-rounded, cursive`;
3452
- var injected = false;
3453
- function injectViaStyle(dataUrl) {
3454
- const style = document.createElement("style");
3455
- style.setAttribute("data-graphein-sketch-font", "");
3456
- style.textContent = `@font-face{font-family:'${SKETCH_FONT_NAME}';font-style:normal;font-weight:400;font-display:swap;src:url("${dataUrl}") format("woff2");}`;
3457
- 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);
3458
3492
  }
3459
- async function ensureSketchFont() {
3460
- if (injected) return false;
3461
- if (typeof document === "undefined") return false;
3462
- injected = true;
3463
- try {
3464
- const { PATRICK_HAND_DATA_URL } = await import('./patrickHand-ITDQJJE2.js');
3465
- const docFonts = document.fonts;
3466
- if (typeof FontFace !== "undefined" && docFonts?.add) {
3467
- const face = new FontFace(
3468
- SKETCH_FONT_NAME,
3469
- `url("${PATRICK_HAND_DATA_URL}") format("woff2")`,
3470
- { style: "normal", weight: "400" }
3471
- );
3472
- await face.load();
3473
- docFonts.add(face);
3474
- return true;
3475
- }
3476
- injectViaStyle(PATRICK_HAND_DATA_URL);
3477
- return true;
3478
- } catch {
3479
- injected = false;
3480
- return false;
3481
- }
3493
+ function toStr(v) {
3494
+ if (v == null) return "";
3495
+ if (v instanceof Date) return v.toISOString();
3496
+ return String(v);
3482
3497
  }
3483
- function withSketchFont(tokens) {
3484
- return {
3485
- ...tokens,
3486
- font: {
3487
- ...tokens.font,
3488
- family: `${SKETCH_FONT_FAMILY}`
3489
- }
3490
- };
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);
3491
3504
  }
3492
-
3493
- // src/spec/types.ts
3494
- var SLICER_TYPES = [
3495
- "dropdown",
3496
- "search",
3497
- "list",
3498
- "range",
3499
- "dateRange"
3500
- ];
3501
- var CHART_TYPES = [
3502
- "line",
3503
- "area",
3504
- "bar",
3505
- "scatter",
3506
- "combo",
3507
- "histogram",
3508
- "pie",
3509
- "heatmap",
3510
- "funnel",
3511
- "kpi",
3512
- "treemap",
3513
- "gauge",
3514
- "bullet",
3515
- "calendarHeatmap",
3516
- "waterfall",
3517
- "slope",
3518
- "dumbbell",
3519
- "table",
3520
- "matrix",
3521
- "box",
3522
- "sankey",
3523
- "choropleth",
3524
- "dropdown",
3525
- "search",
3526
- "list",
3527
- "range",
3528
- "dateRange"
3529
- ];
3530
-
3531
- // src/spec/transform/types.ts
3532
- var TRANSFORM_KINDS = ["aggregate", "bin", "filter", "fold", "timeUnit", "calculate"];
3533
- var TIME_UNITS = [
3534
- "year",
3535
- "quarter",
3536
- "month",
3537
- "week",
3538
- "day",
3539
- "hour",
3540
- "minute",
3541
- "second"
3542
- ];
3543
-
3544
- // src/spec/transform/aggregate.ts
3545
- function applyAggregate(transform2, data) {
3546
- const groupby = transform2.groupby ?? [];
3547
- const reads = groupby.map((f) => accessor(f));
3548
- const emit = (keyValues, rows) => {
3549
- const out2 = {};
3550
- groupby.forEach((field, i) => {
3551
- out2[field] = keyValues[i];
3552
- });
3553
- for (const agg of transform2.aggregate) {
3554
- const values = agg.field === void 0 ? rows : rows.map((r) => r[agg.field]);
3555
- 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);
3556
3578
  }
3557
- return out2;
3558
- };
3559
- if (groupby.length === 0) {
3560
- return [emit([], data)];
3561
- }
3562
- const groups = /* @__PURE__ */ new Map();
3563
- for (const row of data) {
3564
- const keyValues = reads.map((read) => read(row));
3565
- const key = keyValues.map((v) => toKey(v)).join("\0");
3566
- let group = groups.get(key);
3567
- if (group === void 0) {
3568
- group = { keys: keyValues, rows: [] };
3569
- 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)));
3570
3599
  }
3571
- group.rows.push(row);
3572
3600
  }
3573
- const out = [];
3574
- for (const group of groups.values()) {
3575
- 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}".`);
3576
3632
  }
3577
- return out;
3633
+ }
3634
+ function evaluate(node, row) {
3635
+ return evalNode(node, row);
3578
3636
  }
3579
3637
 
3580
- // src/spec/transform/bin.ts
3581
- function roundNice(value, step) {
3582
- if (!Number.isFinite(step) || step === 0) return value;
3583
- const places = Math.max(0, -Math.floor(Math.log10(Math.abs(step))) + 1);
3584
- const rounded = Number(value.toFixed(Math.min(places, 15)));
3585
- 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);
3586
3642
  }
3587
- function computeBins(values, opts = {}) {
3588
- let min = Infinity;
3589
- let max = -Infinity;
3590
- if (opts.extent) {
3591
- [min, max] = opts.extent;
3592
- } else {
3593
- for (const v of values) {
3594
- if (!Number.isFinite(v)) continue;
3595
- if (v < min) min = v;
3596
- if (v > max) max = v;
3597
- }
3598
- }
3599
- if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
3600
- const explicit = opts.step !== void 0 && opts.step > 0;
3601
- if (min === max) {
3602
- const step2 = explicit ? opts.step : 1;
3603
- 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;
3604
3650
  }
3605
- const nice = opts.nice !== false;
3606
- let step = explicit ? opts.step : tickStep(min, max, opts.maxbins ?? 10);
3607
- if (!Number.isFinite(step) || step <= 0) step = max - min || 1;
3608
- const start = nice ? Math.floor(min / step) * step : min;
3609
- const end = nice ? Math.ceil(max / step) * step : max;
3610
- const count = Math.max(1, Math.ceil((end - start) / step - 1e-9));
3611
- return { start: roundNice(start, step), step, count };
3612
3651
  }
3613
- function applyBin(transform2, data) {
3614
- const read = accessor(transform2.bin);
3615
- const finite4 = [];
3616
- for (const row of data) {
3617
- const n = toNumber(read(row));
3618
- if (Number.isFinite(n)) finite4.push(n);
3619
- }
3620
- const layout2 = computeBins(finite4, {
3621
- maxbins: transform2.maxbins,
3622
- step: transform2.step,
3623
- extent: transform2.extent,
3624
- nice: transform2.nice
3625
- });
3626
- const startField = Array.isArray(transform2.as) ? transform2.as[0] : transform2.as;
3627
- const endField = Array.isArray(transform2.as) ? transform2.as[1] : void 0;
3628
- return data.map((row) => {
3629
- const out = { ...row };
3630
- const v = toNumber(read(row));
3631
- const outside = transform2.extent != null && (v < transform2.extent[0] || v > transform2.extent[1]);
3632
- if (layout2 == null || !Number.isFinite(v) || outside) {
3633
- out[startField] = null;
3634
- if (endField) out[endField] = null;
3635
- return out;
3636
- }
3637
- let index = Math.floor((v - layout2.start) / layout2.step + 1e-9);
3638
- if (index < 0) index = 0;
3639
- if (index > layout2.count - 1) index = layout2.count - 1;
3640
- out[startField] = roundNice(layout2.start + index * layout2.step, layout2.step);
3641
- if (endField) out[endField] = roundNice(layout2.start + (index + 1) * layout2.step, layout2.step);
3642
- return out;
3643
- });
3644
- }
3645
-
3646
- // src/spec/transform/expr/error.ts
3647
- var ExpressionError = class extends Error {
3648
- constructor(message, pos) {
3649
- super(message);
3650
- /** Source offset where the problem was detected, when known. */
3651
- __publicField(this, "pos");
3652
- this.name = "ExpressionError";
3653
- this.pos = pos;
3654
- }
3655
- };
3656
3652
 
3657
- // src/spec/transform/expr/tokenize.ts
3658
- var MULTI_OPS = ["===", "!==", "==", "!=", "<=", ">=", "&&", "||"];
3659
- var SINGLE_OPS = new Set("+-*/%!<>?:.,()[]".split(""));
3660
- function isDigit(ch) {
3661
- return ch >= "0" && ch <= "9";
3662
- }
3663
- function isIdentStart(ch) {
3664
- return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_" || ch === "$";
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) }));
3665
3658
  }
3666
- function isIdentPart(ch) {
3667
- return isIdentStart(ch) || isDigit(ch);
3659
+
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
3668
  }
3669
- function tokenize(src) {
3670
- const tokens = [];
3671
- let i = 0;
3672
- const n = src.length;
3673
- while (i < n) {
3674
- const ch = src[i];
3675
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
3676
- i++;
3677
- continue;
3678
- }
3679
- if (ch === '"' || ch === "'") {
3680
- const start = i;
3681
- const quote = ch;
3682
- i++;
3683
- let out = "";
3684
- while (i < n && src[i] !== quote) {
3685
- if (src[i] === "\\") {
3686
- const next = src[i + 1];
3687
- if (next === void 0) throw new ExpressionError("Unterminated string literal.", start);
3688
- out += next === "n" ? "\n" : next === "t" ? " " : next === "r" ? "\r" : next;
3689
- i += 2;
3690
- } else {
3691
- out += src[i];
3692
- i++;
3693
- }
3694
- }
3695
- if (i >= n) throw new ExpressionError("Unterminated string literal.", start);
3696
- i++;
3697
- tokens.push({ type: "str", value: out, pos: start });
3698
- continue;
3699
- }
3700
- if (isDigit(ch) || ch === "." && isDigit(src[i + 1] ?? "")) {
3701
- const start = i;
3702
- while (i < n && isDigit(src[i])) i++;
3703
- if (src[i] === ".") {
3704
- i++;
3705
- while (i < n && isDigit(src[i])) i++;
3706
- }
3707
- if (src[i] === "e" || src[i] === "E") {
3708
- i++;
3709
- if (src[i] === "+" || src[i] === "-") i++;
3710
- if (!isDigit(src[i] ?? "")) throw new ExpressionError("Malformed number literal.", start);
3711
- while (i < n && isDigit(src[i])) i++;
3712
- }
3713
- tokens.push({ type: "num", value: Number(src.slice(start, i)), pos: start });
3714
- continue;
3715
- }
3716
- if (isIdentStart(ch)) {
3717
- const start = i;
3718
- i++;
3719
- while (i < n && isIdentPart(src[i])) i++;
3720
- tokens.push({ type: "ident", value: src.slice(start, i), pos: start });
3721
- continue;
3722
- }
3723
- const three = src.slice(i, i + 3);
3724
- const two = src.slice(i, i + 2);
3725
- const multi = MULTI_OPS.find((op) => op === three) ?? MULTI_OPS.find((op) => op === two);
3726
- if (multi) {
3727
- tokens.push({ type: "op", value: multi, pos: i });
3728
- i += multi.length;
3729
- continue;
3730
- }
3731
- if (SINGLE_OPS.has(ch)) {
3732
- tokens.push({ type: "op", value: ch, pos: i });
3733
- i++;
3734
- continue;
3735
- }
3736
- throw new ExpressionError(`Unexpected character ${JSON.stringify(ch)}.`, i);
3669
+ function compilePredicate(pred) {
3670
+ if ("and" in pred) {
3671
+ const parts = pred.and.map(compilePredicate);
3672
+ return (row) => parts.every((p) => p(row));
3737
3673
  }
3738
- tokens.push({ type: "eof", value: "", pos: n });
3739
- return tokens;
3740
- }
3741
-
3742
- // src/spec/transform/expr/parse.ts
3743
- var EQUALITY = /* @__PURE__ */ new Set(["==", "!=", "===", "!=="]);
3744
- var RELATIONAL = /* @__PURE__ */ new Set(["<", "<=", ">", ">="]);
3745
- var ADDITIVE = /* @__PURE__ */ new Set(["+", "-"]);
3746
- var MULTIPLICATIVE = /* @__PURE__ */ new Set(["*", "/", "%"]);
3747
- var Parser = class {
3748
- constructor(tokens) {
3749
- __publicField(this, "tokens");
3750
- __publicField(this, "pos", 0);
3751
- this.tokens = tokens;
3674
+ if ("or" in pred) {
3675
+ const parts = pred.or.map(compilePredicate);
3676
+ return (row) => parts.some((p) => p(row));
3752
3677
  }
3753
- peek() {
3754
- return this.tokens[this.pos];
3678
+ if ("not" in pred) {
3679
+ const inner = compilePredicate(pred.not);
3680
+ return (row) => !inner(row);
3755
3681
  }
3756
- next() {
3757
- return this.tokens[this.pos++];
3682
+ const read = accessor(pred.field);
3683
+ if ("equals" in pred) {
3684
+ const want = toKey(pred.equals);
3685
+ return (row) => toKey(read(row)) === want;
3758
3686
  }
3759
- isOp(value) {
3760
- const t = this.peek();
3761
- return t.type === "op" && t.value === value;
3687
+ if ("ne" in pred) {
3688
+ const want = toKey(pred.ne);
3689
+ return (row) => toKey(read(row)) !== want;
3762
3690
  }
3763
- eat(value) {
3764
- if (!this.isOp(value)) {
3765
- const t = this.peek();
3766
- throw new ExpressionError(`Expected "${value}" but found ${describe(t)}.`, t.pos);
3767
- }
3768
- this.pos++;
3691
+ if ("oneOf" in pred) {
3692
+ const want = new Set(pred.oneOf.map((x) => toKey(x)));
3693
+ return (row) => want.has(toKey(read(row)));
3769
3694
  }
3770
- parse() {
3771
- const node = this.parseTernary();
3772
- const t = this.peek();
3773
- if (t.type !== "eof") throw new ExpressionError(`Unexpected ${describe(t)}.`, t.pos);
3774
- return node;
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
+ };
3775
3701
  }
3776
- parseTernary() {
3777
- const test = this.parseBinary(0);
3778
- if (this.isOp("?")) {
3779
- this.eat("?");
3780
- const then = this.parseTernary();
3781
- this.eat(":");
3782
- const alt = this.parseTernary();
3783
- return { type: "ternary", test, then, alt };
3784
- }
3785
- return test;
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
+ };
3786
3709
  }
3787
- /** Precedence-climbing for the binary/logical operator levels. */
3788
- parseBinary(minLevel) {
3789
- let left = this.parseUnary();
3790
- for (; ; ) {
3791
- const t = this.peek();
3792
- if (t.type !== "op") break;
3793
- const level = binaryLevel(t.value);
3794
- if (level < minLevel || level < 0) break;
3795
- this.next();
3796
- const right = this.parseBinary(level + 1);
3797
- const op = t.value;
3798
- left = op === "&&" || op === "||" ? { type: "logical", op, left, right } : { type: "binary", op, left, right };
3799
- }
3800
- return left;
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
+ };
3801
3719
  }
3802
- parseUnary() {
3803
- const t = this.peek();
3804
- if (t.type === "op" && (t.value === "!" || t.value === "-" || t.value === "+")) {
3805
- this.next();
3806
- return { type: "unary", op: t.value, arg: this.parseUnary() };
3807
- }
3808
- return this.parsePostfix();
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
+ };
3809
3726
  }
3810
- parsePostfix() {
3811
- let node = this.parsePrimary();
3812
- for (; ; ) {
3813
- if (this.isOp(".")) {
3814
- this.eat(".");
3815
- const t = this.next();
3816
- if (t.type !== "ident") throw new ExpressionError(`Expected a property name after ".".`, t.pos);
3817
- node = { type: "member", obj: node, prop: { type: "str", value: t.value }, computed: false };
3818
- } else if (this.isOp("[")) {
3819
- this.eat("[");
3820
- const prop = this.parseTernary();
3821
- this.eat("]");
3822
- node = { type: "member", obj: node, prop, computed: true };
3823
- } else {
3824
- break;
3825
- }
3826
- }
3827
- return node;
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
+ };
3828
3733
  }
3829
- parsePrimary() {
3830
- const t = this.next();
3831
- if (t.type === "num") return { type: "num", value: t.value };
3832
- if (t.type === "str") return { type: "str", value: t.value };
3833
- if (t.type === "ident") {
3834
- const name = t.value;
3835
- if (name === "true") return { type: "lit", value: true };
3836
- if (name === "false") return { type: "lit", value: false };
3837
- if (name === "null") return { type: "lit", value: null };
3838
- if (this.isOp("(")) {
3839
- this.eat("(");
3840
- const args = [];
3841
- if (!this.isOp(")")) {
3842
- args.push(this.parseTernary());
3843
- while (this.isOp(",")) {
3844
- this.eat(",");
3845
- args.push(this.parseTernary());
3846
- }
3847
- }
3848
- this.eat(")");
3849
- return { type: "call", callee: name, args };
3850
- }
3851
- if (name === "datum") return { type: "datum" };
3852
- return { type: "field", name };
3853
- }
3854
- if (t.type === "op" && t.value === "(") {
3855
- const node = this.parseTernary();
3856
- this.eat(")");
3857
- return node;
3858
- }
3859
- throw new ExpressionError(`Unexpected ${describe(t)}.`, t.pos);
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
+ };
3860
3740
  }
3861
- };
3862
- function binaryLevel(op) {
3863
- if (op === "||") return 1;
3864
- if (op === "&&") return 2;
3865
- if (EQUALITY.has(op)) return 3;
3866
- if (RELATIONAL.has(op)) return 4;
3867
- if (ADDITIVE.has(op)) return 5;
3868
- if (MULTIPLICATIVE.has(op)) return 6;
3869
- return -1;
3870
- }
3871
- function describe(t) {
3872
- if (t.type === "eof") return "end of input";
3873
- if (t.type === "str") return `string ${JSON.stringify(t.value)}`;
3874
- if (t.type === "num") return `number ${t.value}`;
3875
- return `"${t.value}"`;
3876
- }
3877
- function parse(src) {
3878
- if (typeof src !== "string" || src.trim() === "") {
3879
- throw new ExpressionError("Expression must be a non-empty string.", 0);
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
+ };
3880
3747
  }
3881
- return new Parser(tokenize(src)).parse();
3882
- }
3883
-
3884
- // src/spec/transform/expr/evaluate.ts
3885
- var BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3886
- function truthy(v) {
3887
- if (typeof v === "number") return v !== 0 && !Number.isNaN(v);
3888
- return Boolean(v);
3889
- }
3890
- function toStr(v) {
3891
- if (v == null) return "";
3892
- if (v instanceof Date) return v.toISOString();
3893
- return String(v);
3894
- }
3895
- function looseEquals(a, b) {
3896
- if (a == null || b == null) return a == null && b == null;
3897
- const na = toNumber(a);
3898
- const nb = toNumber(b);
3899
- if (Number.isFinite(na) && Number.isFinite(nb)) return na === nb;
3900
- return toStr(a) === toStr(b);
3748
+ return () => false;
3901
3749
  }
3902
- function compare(a, b) {
3903
- const na = toNumber(a);
3904
- const nb = toNumber(b);
3905
- if (Number.isFinite(na) && Number.isFinite(nb)) return na < nb ? -1 : na > nb ? 1 : 0;
3906
- const sa = toStr(a);
3907
- const sb = toStr(b);
3908
- return sa < sb ? -1 : sa > sb ? 1 : 0;
3750
+ function applyFilter(transform2, data) {
3751
+ const test = compilePredicate(transform2.filter);
3752
+ return data.filter(test);
3909
3753
  }
3910
- var nums = (args) => args.map(toNumber);
3911
- var FUNCTIONS = {
3912
- abs: (a) => Math.abs(toNumber(a[0])),
3913
- round: (a) => {
3914
- const d = a.length > 1 ? Math.trunc(toNumber(a[1])) : 0;
3915
- const f = 10 ** d;
3916
- return Math.round(toNumber(a[0]) * f) / f;
3917
- },
3918
- floor: (a) => Math.floor(toNumber(a[0])),
3919
- ceil: (a) => Math.ceil(toNumber(a[0])),
3920
- trunc: (a) => Math.trunc(toNumber(a[0])),
3921
- sign: (a) => Math.sign(toNumber(a[0])),
3922
- sqrt: (a) => Math.sqrt(toNumber(a[0])),
3923
- exp: (a) => Math.exp(toNumber(a[0])),
3924
- log: (a) => Math.log(toNumber(a[0])),
3925
- log10: (a) => Math.log10(toNumber(a[0])),
3926
- log2: (a) => Math.log2(toNumber(a[0])),
3927
- pow: (a) => toNumber(a[0]) ** toNumber(a[1]),
3928
- min: (a) => Math.min(...nums(a)),
3929
- max: (a) => Math.max(...nums(a)),
3930
- number: (a) => toNumber(a[0]),
3931
- isFinite: (a) => Number.isFinite(toNumber(a[0])),
3932
- isNaN: (a) => Number.isNaN(toNumber(a[0])),
3933
- str: (a) => toStr(a[0]),
3934
- lower: (a) => toStr(a[0]).toLowerCase(),
3935
- upper: (a) => toStr(a[0]).toUpperCase(),
3936
- trim: (a) => toStr(a[0]).trim(),
3937
- length: (a) => Array.isArray(a[0]) ? a[0].length : toStr(a[0]).length,
3938
- substring: (a) => {
3939
- const s = toStr(a[0]);
3940
- const start = Math.trunc(toNumber(a[1]));
3941
- return a.length > 2 ? s.substring(start, Math.trunc(toNumber(a[2]))) : s.substring(start);
3942
- },
3943
- replace: (a) => toStr(a[0]).split(toStr(a[1])).join(toStr(a[2])),
3944
- contains: (a) => toStr(a[0]).includes(toStr(a[1])),
3945
- startsWith: (a) => toStr(a[0]).startsWith(toStr(a[1])),
3946
- endsWith: (a) => toStr(a[0]).endsWith(toStr(a[1])),
3947
- concat: (a) => a.map(toStr).join(""),
3948
- coalesce: (a) => a.find((v) => v != null),
3949
- if: (a) => truthy(a[0]) ? a[1] : a[2],
3950
- year: (a) => toDate(a[0])?.getFullYear() ?? NaN,
3951
- month: (a) => {
3952
- const d = toDate(a[0]);
3953
- return d ? d.getMonth() + 1 : NaN;
3954
- },
3955
- day: (a) => toDate(a[0])?.getDate() ?? NaN,
3956
- hours: (a) => toDate(a[0])?.getHours() ?? NaN,
3957
- minutes: (a) => toDate(a[0])?.getMinutes() ?? NaN
3958
- };
3959
- var FUNCTION_NAMES = Object.keys(FUNCTIONS);
3960
- function evalNode(node, row) {
3961
- switch (node.type) {
3962
- case "num":
3963
- case "str":
3964
- case "lit":
3965
- return node.value;
3966
- case "datum":
3967
- return row;
3968
- case "field":
3969
- return row[node.name];
3970
- case "unary": {
3971
- const v = evalNode(node.arg, row);
3972
- if (node.op === "!") return !truthy(v);
3973
- if (node.op === "-") return -toNumber(v);
3974
- return toNumber(v);
3975
- }
3976
- case "logical": {
3977
- const left = evalNode(node.left, row);
3978
- if (node.op === "&&") return truthy(left) ? evalNode(node.right, row) : left;
3979
- return truthy(left) ? left : evalNode(node.right, row);
3980
- }
3981
- case "ternary":
3982
- return truthy(evalNode(node.test, row)) ? evalNode(node.then, row) : evalNode(node.alt, row);
3983
- case "binary":
3984
- return evalBinary(node.op, evalNode(node.left, row), evalNode(node.right, row));
3985
- case "member": {
3986
- const obj = evalNode(node.obj, row);
3987
- if (obj == null || typeof obj !== "object") return void 0;
3988
- const key = node.computed ? toStr(evalNode(node.prop, row)) : node.prop.value;
3989
- if (BLOCKED_KEYS.has(key)) return void 0;
3990
- return obj[key];
3991
- }
3992
- case "call": {
3993
- const fn = Object.prototype.hasOwnProperty.call(FUNCTIONS, node.callee) ? FUNCTIONS[node.callee] : void 0;
3994
- if (!fn) throw new ExpressionError(`Unknown function "${node.callee}".`);
3995
- return fn(node.args.map((a) => evalNode(a, row)));
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] });
3996
3762
  }
3997
3763
  }
3764
+ return out;
3998
3765
  }
3999
- function evalBinary(op, a, b) {
4000
- switch (op) {
4001
- case "+":
4002
- return typeof a === "string" || typeof b === "string" ? toStr(a) + toStr(b) : toNumber(a) + toNumber(b);
4003
- case "-":
4004
- return toNumber(a) - toNumber(b);
4005
- case "*":
4006
- return toNumber(a) * toNumber(b);
4007
- case "/":
4008
- return toNumber(a) / toNumber(b);
4009
- case "%":
4010
- return toNumber(a) % toNumber(b);
4011
- case "<":
4012
- return compare(a, b) < 0;
4013
- case "<=":
4014
- return compare(a, b) <= 0;
4015
- case ">":
4016
- return compare(a, b) > 0;
4017
- case ">=":
4018
- return compare(a, b) >= 0;
4019
- case "==":
4020
- return looseEquals(a, b);
4021
- case "!=":
4022
- return !looseEquals(a, b);
4023
- case "===":
4024
- return a === b;
4025
- case "!==":
4026
- return a !== b;
3766
+
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);
4027
3795
  default:
4028
- throw new ExpressionError(`Unknown operator "${op}".`);
3796
+ return new Date(date.getTime());
4029
3797
  }
4030
3798
  }
4031
- function evaluate(node, row) {
4032
- return evalNode(node, row);
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
+ });
4033
3805
  }
4034
3806
 
4035
- // src/spec/transform/expr/index.ts
4036
- function compileExpression(src) {
4037
- const ast = parse(src);
4038
- return (row) => evaluate(ast, row);
4039
- }
4040
- function checkExpression(src) {
4041
- try {
4042
- parse(src);
4043
- return null;
4044
- } catch (e) {
4045
- if (e instanceof ExpressionError) return { message: e.message, pos: e.pos };
4046
- throw e;
4047
- }
4048
- }
4049
-
4050
- // src/spec/transform/calculate.ts
4051
- function applyCalculate(transform2, data) {
4052
- const evalRow = compileExpression(transform2.calculate);
4053
- const as = transform2.as;
4054
- return data.map((row) => ({ ...row, [as]: evalRow(row) }));
4055
- }
4056
-
4057
- // src/spec/transform/filter.ts
4058
- function toComparable(value) {
4059
- if (typeof value === "number") return value;
4060
- if (value instanceof Date) return value.getTime();
4061
- const n = toNumber(value);
4062
- if (!Number.isNaN(n)) return n;
4063
- const d = toDate(value);
4064
- return d ? d.getTime() : NaN;
4065
- }
4066
- function compilePredicate(pred) {
4067
- if ("and" in pred) {
4068
- const parts = pred.and.map(compilePredicate);
4069
- return (row) => parts.every((p) => p(row));
4070
- }
4071
- if ("or" in pred) {
4072
- const parts = pred.or.map(compilePredicate);
4073
- return (row) => parts.some((p) => p(row));
4074
- }
4075
- if ("not" in pred) {
4076
- const inner = compilePredicate(pred.not);
4077
- return (row) => !inner(row);
4078
- }
4079
- const read = accessor(pred.field);
4080
- if ("equals" in pred) {
4081
- const want = toKey(pred.equals);
4082
- return (row) => toKey(read(row)) === want;
4083
- }
4084
- if ("ne" in pred) {
4085
- const want = toKey(pred.ne);
4086
- return (row) => toKey(read(row)) !== want;
4087
- }
4088
- if ("oneOf" in pred) {
4089
- const want = new Set(pred.oneOf.map((x) => toKey(x)));
4090
- return (row) => want.has(toKey(read(row)));
4091
- }
4092
- if ("contains" in pred) {
4093
- const q = String(pred.contains).toLowerCase();
4094
- return (row) => {
4095
- const raw = read(row);
4096
- return raw != null && String(raw).toLowerCase().includes(q);
4097
- };
4098
- }
4099
- if ("valid" in pred) {
4100
- const wantValid = pred.valid !== false;
4101
- return (row) => {
4102
- const raw = read(row);
4103
- const ok = raw != null && !(typeof raw === "number" && Number.isNaN(raw));
4104
- return ok === wantValid;
4105
- };
4106
- }
4107
- if ("range" in pred) {
4108
- const a = toComparable(pred.range[0]);
4109
- const b = toComparable(pred.range[1]);
4110
- const min = Math.min(a, b);
4111
- const max = Math.max(a, b);
4112
- return (row) => {
4113
- const c = toComparable(read(row));
4114
- return !Number.isNaN(c) && c >= min && c <= max;
4115
- };
4116
- }
4117
- if ("gt" in pred) {
4118
- const t = toComparable(pred.gt);
4119
- return (row) => {
4120
- const c = toComparable(read(row));
4121
- return !Number.isNaN(c) && c > t;
4122
- };
4123
- }
4124
- if ("gte" in pred) {
4125
- const t = toComparable(pred.gte);
4126
- return (row) => {
4127
- const c = toComparable(read(row));
4128
- return !Number.isNaN(c) && c >= t;
4129
- };
4130
- }
4131
- if ("lt" in pred) {
4132
- const t = toComparable(pred.lt);
4133
- return (row) => {
4134
- const c = toComparable(read(row));
4135
- return !Number.isNaN(c) && c < t;
4136
- };
4137
- }
4138
- if ("lte" in pred) {
4139
- const t = toComparable(pred.lte);
4140
- return (row) => {
4141
- const c = toComparable(read(row));
4142
- return !Number.isNaN(c) && c <= t;
4143
- };
4144
- }
4145
- return () => false;
4146
- }
4147
- function applyFilter(transform2, data) {
4148
- const test = compilePredicate(transform2.filter);
4149
- return data.filter(test);
4150
- }
4151
-
4152
- // src/spec/transform/fold.ts
4153
- function applyFold(transform2, data) {
4154
- const [keyName, valueName] = transform2.as ?? ["key", "value"];
4155
- const out = [];
4156
- for (const row of data) {
4157
- for (const column of transform2.fold) {
4158
- out.push({ ...row, [keyName]: column, [valueName]: row[column] });
4159
- }
4160
- }
4161
- return out;
4162
- }
4163
-
4164
- // src/spec/transform/timeUnit.ts
4165
- function truncateTo(date, unit) {
4166
- const year = date.getFullYear();
4167
- const month = date.getMonth();
4168
- const day = date.getDate();
4169
- const hour = date.getHours();
4170
- const minute = date.getMinutes();
4171
- const second = date.getSeconds();
4172
- switch (unit) {
4173
- case "year":
4174
- return new Date(year, 0, 1);
4175
- case "quarter":
4176
- return new Date(year, Math.floor(month / 3) * 3, 1);
4177
- case "month":
4178
- return new Date(year, month, 1);
4179
- case "week": {
4180
- const start = new Date(year, month, day - date.getDay());
4181
- start.setHours(0, 0, 0, 0);
4182
- return start;
4183
- }
4184
- case "day":
4185
- return new Date(year, month, day);
4186
- case "hour":
4187
- return new Date(year, month, day, hour);
4188
- case "minute":
4189
- return new Date(year, month, day, hour, minute);
4190
- case "second":
4191
- return new Date(year, month, day, hour, minute, second);
4192
- default:
4193
- return new Date(date.getTime());
4194
- }
4195
- }
4196
- function applyTimeUnit(transform2, data) {
4197
- const read = accessor(transform2.field);
4198
- return data.map((row) => {
4199
- const date = toDate(read(row));
4200
- return { ...row, [transform2.as]: date ? truncateTo(date, transform2.timeUnit) : null };
4201
- });
4202
- }
4203
-
4204
- // src/spec/transform/apply.ts
4205
- function applyTransform(transform2, data) {
4206
- if ("filter" in transform2) return applyFilter(transform2, data);
4207
- if ("aggregate" in transform2) return applyAggregate(transform2, data);
4208
- if ("bin" in transform2) return applyBin(transform2, data);
4209
- if ("fold" in transform2) return applyFold(transform2, data);
4210
- if ("timeUnit" in transform2) return applyTimeUnit(transform2, data);
4211
- if ("calculate" in transform2) return applyCalculate(transform2, data);
4212
- return data;
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;
4213
3816
  }
4214
3817
  function applyTransforms(transforms, data) {
4215
3818
  if (!transforms || transforms.length === 0) return data;
@@ -4220,58 +3823,6 @@ function applyTransforms(transforms, data) {
4220
3823
  return out;
4221
3824
  }
4222
3825
 
4223
- // src/spec/infer.ts
4224
- function looksTemporal2(value) {
4225
- if (value.length < 4) return false;
4226
- if (/^\d{4}(-\d{2}(-\d{2})?)?([T\s]\d{2}:\d{2})?/.test(value)) {
4227
- const t = Date.parse(value);
4228
- return !Number.isNaN(t);
4229
- }
4230
- return false;
4231
- }
4232
- function inferValueType(value) {
4233
- if (value == null) return void 0;
4234
- if (typeof value === "number") return Number.isFinite(value) ? "quantitative" : void 0;
4235
- if (value instanceof Date) return "temporal";
4236
- if (typeof value === "boolean") return "nominal";
4237
- if (typeof value === "string") {
4238
- if (value.trim() === "") return void 0;
4239
- if (looksTemporal2(value)) return "temporal";
4240
- if (/^-?\d*\.?\d+(e[-+]?\d+)?$/i.test(value.trim())) return "quantitative";
4241
- return "nominal";
4242
- }
4243
- return "nominal";
4244
- }
4245
- function inferFieldType(data, field, sample = 50) {
4246
- let seen = 0;
4247
- const counts = {
4248
- quantitative: 0,
4249
- temporal: 0,
4250
- ordinal: 0,
4251
- nominal: 0
4252
- };
4253
- for (let i = 0; i < data.length && seen < sample; i++) {
4254
- const t = inferValueType(data[i]?.[field]);
4255
- if (!t) continue;
4256
- counts[t]++;
4257
- seen++;
4258
- }
4259
- if (seen === 0) return "nominal";
4260
- if (counts.temporal >= seen * 0.6) return "temporal";
4261
- if (counts.quantitative >= seen * 0.6) return "quantitative";
4262
- return "nominal";
4263
- }
4264
- function inferFieldTypes(data) {
4265
- const fields = /* @__PURE__ */ new Set();
4266
- for (let i = 0; i < Math.min(data.length, 20); i++) {
4267
- const row = data[i];
4268
- if (row) for (const k of Object.keys(row)) fields.add(k);
4269
- }
4270
- const out = {};
4271
- for (const f of fields) out[f] = inferFieldType(data, f);
4272
- return out;
4273
- }
4274
-
4275
3826
  // src/spec/lint.ts
4276
3827
  var MAX_PIE_SLICES = 7;
4277
3828
  var MAX_SERIES = 12;
@@ -5647,102 +5198,779 @@ function validateDashboard(spec) {
5647
5198
  });
5648
5199
  }
5649
5200
  }
5650
- if (spec.subtitle !== void 0 && typeof spec.subtitle !== "string") {
5651
- err("subtitle", '"subtitle" must be a string.');
5652
- }
5653
- const layout2 = spec.layout;
5654
- if (layout2 !== void 0) {
5655
- if (!isObject3(layout2)) {
5656
- err("layout", '"layout" must be an object { cols?, rowHeight?, gap?, breakpoints?, navigators?, sections? }.');
5657
- } else {
5658
- for (const k of ["cols", "rowHeight", "gap", "maxWidth", "padding"]) {
5659
- const n = layout2[k];
5660
- if (n !== void 0 && (typeof n !== "number" || !Number.isFinite(n) || n < 0)) {
5661
- err(`layout.${k}`, `"${k}" must be a non-negative number.`);
5662
- }
5663
- }
5664
- if (layout2.navigators !== void 0 && layout2.navigators !== "top" && layout2.navigators !== "inline") {
5665
- err("layout.navigators", 'Expected "top" or "inline".');
5666
- }
5667
- if (layout2.preset !== void 0 && !["auto", "kpi-first", "sidebar"].includes(layout2.preset)) {
5668
- err("layout.preset", 'Expected "auto", "kpi-first", or "sidebar".');
5669
- }
5670
- if (layout2.density !== void 0 && !["compact", "standard", "comfortable"].includes(layout2.density)) {
5671
- err("layout.density", 'Expected "compact", "standard", or "comfortable".');
5672
- }
5673
- if (layout2.breakpoints !== void 0) {
5674
- if (!Array.isArray(layout2.breakpoints)) {
5675
- err("layout.breakpoints", '"breakpoints" must be an array of { maxWidth, cols }.');
5676
- } else {
5677
- layout2.breakpoints.forEach((bp, i) => {
5678
- if (!isObject3(bp)) {
5679
- err(`layout.breakpoints[${i}]`, "Each breakpoint must be an object { maxWidth, cols }.");
5680
- return;
5681
- }
5682
- for (const k of ["maxWidth", "cols"]) {
5683
- const n = bp[k];
5684
- if (typeof n !== "number" || !Number.isFinite(n) || n < 1) {
5685
- err(`layout.breakpoints[${i}].${k}`, `"${k}" must be a positive number.`);
5686
- }
5687
- }
5688
- });
5689
- }
5690
- }
5691
- if (layout2.sections !== void 0) {
5692
- if (!Array.isArray(layout2.sections)) {
5693
- err("layout.sections", '"sections" must be an array of dashboard sections.');
5694
- } else {
5695
- const sectionViewIds = /* @__PURE__ */ new Set();
5696
- layout2.sections.forEach((section, i) => {
5697
- if (!isObject3(section)) {
5698
- err(`layout.sections[${i}]`, "Each section must be an object { views }.");
5699
- return;
5700
- }
5701
- for (const k of ["id", "title", "subtitle", "background"]) {
5702
- if (section[k] !== void 0 && typeof section[k] !== "string") {
5703
- err(`layout.sections[${i}].${k}`, `"${k}" must be a string.`);
5704
- }
5705
- }
5706
- for (const k of ["cols", "rowHeight"]) {
5707
- const n = section[k];
5708
- if (n !== void 0 && (typeof n !== "number" || !Number.isFinite(n) || n < 1)) {
5709
- err(`layout.sections[${i}].${k}`, `"${k}" must be a positive number.`);
5710
- }
5711
- }
5712
- if (section.collapsed !== void 0 && typeof section.collapsed !== "boolean") {
5713
- err(`layout.sections[${i}].collapsed`, '"collapsed" must be a boolean.');
5714
- }
5715
- if (!Array.isArray(section.views)) {
5716
- err(`layout.sections[${i}].views`, '"views" must be an array of view ids.');
5717
- } else {
5718
- section.views.forEach((id, j) => {
5719
- const path = `layout.sections[${i}].views[${j}]`;
5720
- if (typeof id !== "string" || id === "") {
5721
- err(path, "Section view references must be non-empty strings.");
5722
- } else if (!ids.has(id)) {
5723
- err(path, `Section view "${id}" must reference an existing view id.`);
5724
- } else if (sectionViewIds.has(id)) {
5725
- err(path, `View "${id}" is already assigned to another section.`);
5726
- } else {
5727
- sectionViewIds.add(id);
5728
- }
5729
- });
5730
- }
5731
- });
5732
- }
5733
- }
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}`
5734
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);
5735
5930
  }
5736
- return { valid: errors.length === 0, errors, warnings };
5931
+ return false;
5737
5932
  }
5738
- function assertValidSpec(spec) {
5739
- const { valid, errors } = validateSpec(spec);
5740
- if (!valid) {
5741
- const detail = errors.map((e) => ` - ${e.path || "(root)"}: ${e.message}`).join("\n");
5742
- throw new Error(`Invalid Graphein chart spec:
5743
- ${detail}`);
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";
5744
5943
  }
5745
- return spec;
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++;
5959
+ }
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";
5964
+ }
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);
5970
+ }
5971
+ const out = {};
5972
+ for (const f of fields) out[f] = inferFieldType(data, f);
5973
+ return out;
5746
5974
  }
5747
5975
 
5748
5976
  // src/spec/repair.ts
@@ -5769,10 +5997,10 @@ function repairSpec(spec) {
5769
5997
 
5770
5998
  // src/spec/sketch.ts
5771
5999
  function deriveSeed(spec) {
5772
- 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 ?? "";
5773
6001
  const dataLen = Array.isArray(spec.data) ? spec.data.length : 0;
5774
6002
  const firstRow = Array.isArray(spec.data) && spec.data[0] ? Object.keys(spec.data[0]).join(",") : "";
5775
- return hashString(`${spec.type}|${title}|${dataLen}|${firstRow}`) || 1;
6003
+ return hashString(`${spec.type}|${title2}|${dataLen}|${firstRow}`) || 1;
5776
6004
  }
5777
6005
  function resolveSketch(spec) {
5778
6006
  const sketch = spec.sketch;
@@ -6227,25 +6455,25 @@ function drawAxisTitles(surface, model) {
6227
6455
  function drawTitle(surface, frame, spec, tokens) {
6228
6456
  if (!frame.titleRect) return;
6229
6457
  const f = tokens.font;
6230
- const title = typeof spec.title === "string" ? { text: spec.title } : spec.title ?? {};
6458
+ const title2 = typeof spec.title === "string" ? { text: spec.title } : spec.title ?? {};
6231
6459
  const align = typeof spec.title === "object" && spec.title?.align || "left";
6232
6460
  const titleFont = fontString(f.size.title, f.family, f.weight.bold);
6233
6461
  addText(surface, titleFont, {
6234
6462
  left: frame.titleRect.x,
6235
6463
  top: frame.titleRect.y,
6236
6464
  width: frame.titleRect.width,
6237
- text: title.text ?? "",
6465
+ text: title2.text ?? "",
6238
6466
  color: tokens.color.text,
6239
6467
  size: f.size.title,
6240
6468
  weight: f.weight.bold,
6241
6469
  align
6242
6470
  });
6243
- if (frame.subtitleRect && title.subtitle) {
6471
+ if (frame.subtitleRect && title2.subtitle) {
6244
6472
  addText(surface, fontString(f.size.small, f.family, f.weight.normal), {
6245
6473
  left: frame.subtitleRect.x,
6246
6474
  top: frame.subtitleRect.y,
6247
6475
  width: frame.subtitleRect.width,
6248
- text: title.subtitle,
6476
+ text: title2.subtitle,
6249
6477
  color: tokens.color.textMuted,
6250
6478
  size: f.size.small,
6251
6479
  align
@@ -6403,6 +6631,10 @@ function formatNumericTick(value, userFormat) {
6403
6631
  return formatNumber2(value, ",");
6404
6632
  }
6405
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
+ }
6406
6638
  function buildCartesianModel(spec, tokens, opts) {
6407
6639
  const data = spec.data ?? [];
6408
6640
  const enc = spec.encoding;
@@ -6413,7 +6645,7 @@ function buildCartesianModel(spec, tokens, opts) {
6413
6645
  const seriesField = resolveSeriesField(spec);
6414
6646
  const rawSeries = groupBySeries(data, seriesField);
6415
6647
  const seriesKeys = rawSeries.map((s) => s.key);
6416
- const palette = tokens.color.palette;
6648
+ const palette = Array.isArray(spec.palette) ? spec.palette : typeof spec.palette === "string" ? categorical(spec.palette) : tokens.color.palette;
6417
6649
  const colorScale = ordinalColorScale({ domain: seriesKeys, palette });
6418
6650
  const colorCache = /* @__PURE__ */ new Map();
6419
6651
  const sharedColor = opts.shared?.colorOf;
@@ -6577,6 +6809,21 @@ function buildCartesianModel(spec, tokens, opts) {
6577
6809
  xAxis: { show: xShow, labels: xLabels, title: resolveAxisTitle(enc.x, xAxisCfg), edgeAnchored: !categories, labelAngle: xAxisCfg.labelAngle },
6578
6810
  yAxis: { show: yShow, labels: yLabels, title: resolveAxisTitle(enc.y, yAxisCfg) }
6579
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;
6580
6827
  const plot = frame.plot;
6581
6828
  const yRange = [plot.y + plot.height, plot.y];
6582
6829
  const yScale = yIsLog ? logScale({ domain: yDomain, range: yRange, base: yBase, clamp: yClamp }) : linearScale({ domain: yDomain, range: yRange, clamp: yClamp });
@@ -6613,6 +6860,7 @@ function buildCartesianModel(spec, tokens, opts) {
6613
6860
  xTicks,
6614
6861
  yTicks,
6615
6862
  colorOf,
6863
+ legendHits,
6616
6864
  sketch: resolveSketch(spec)
6617
6865
  };
6618
6866
  }
@@ -7122,6 +7370,8 @@ function drawBar(surface, model) {
7122
7370
  }
7123
7371
 
7124
7372
  // src/charts/scatter.ts
7373
+ var QUANTIZE_MIN_POINTS = 4e3;
7374
+ var QUANTIZE_AREA_DIVISOR = 4;
7125
7375
  function isMissing(value) {
7126
7376
  return value == null || value === "";
7127
7377
  }
@@ -7131,6 +7381,35 @@ function isInvalidPositionValue(value) {
7131
7381
  function fixedRadius() {
7132
7382
  return 3.75;
7133
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
+ }
7134
7413
  function radiusScale(rows, field, plotSize) {
7135
7414
  const rMin = 3;
7136
7415
  const rMax = clamp(plotSize * 0.05, 10, 26);
@@ -7173,18 +7452,22 @@ function drawScatter(surface, model) {
7173
7452
  y: py,
7174
7453
  r: readSize ? radius(readSize(row)) : radius(),
7175
7454
  color: series.color,
7176
- alpha: rowAlpha(model.emphasis, row)
7455
+ alpha: rowAlpha(model.emphasis, row),
7456
+ seriesKey: series.key
7177
7457
  });
7178
7458
  }
7179
7459
  }
7180
- 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);
7181
7464
  ctx.save();
7182
7465
  ctx.beginPath();
7183
7466
  ctx.rect(plot.x, plot.y, plot.width, plot.height);
7184
7467
  ctx.clip();
7185
7468
  if (model.sketch) {
7186
7469
  const pen = new RoughPen(ctx, model.sketch);
7187
- for (const point of points) {
7470
+ for (const point of drawPoints) {
7188
7471
  const prev = ctx.globalAlpha;
7189
7472
  if (point.alpha !== 1) ctx.globalAlpha = point.alpha;
7190
7473
  pen.circle(point.x, point.y, point.r, {
@@ -7200,7 +7483,7 @@ function drawScatter(surface, model) {
7200
7483
  }
7201
7484
  ctx.lineWidth = 1;
7202
7485
  ctx.strokeStyle = tokens.color.background;
7203
- for (const point of points) {
7486
+ for (const point of drawPoints) {
7204
7487
  ctx.beginPath();
7205
7488
  ctx.arc(point.x, point.y, point.r, 0, Math.PI * 2);
7206
7489
  ctx.fillStyle = point.color;
@@ -7458,10 +7741,10 @@ function buildBoxInteraction(model) {
7458
7741
 
7459
7742
  // src/charts/chrome.ts
7460
7743
  var CHROME_PAD = { top: 14, right: 16, bottom: 14, left: 16 };
7461
- function resolveTitle2(title) {
7462
- if (typeof title === "string") return { text: title, align: "left" };
7463
- if (title && typeof title === "object") {
7464
- 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;
7465
7748
  return { text: t.text, subtitle: t.subtitle, align: t.align ?? "left" };
7466
7749
  }
7467
7750
  return { align: "left" };
@@ -7516,9 +7799,9 @@ function addOverlayText(surface, tokens, o) {
7516
7799
  surface.overlay.appendChild(el);
7517
7800
  return el;
7518
7801
  }
7519
- function drawTitleBlock(surface, tokens, size, title) {
7802
+ function drawTitleBlock(surface, tokens, size, title2) {
7520
7803
  const f = tokens.font;
7521
- const t = resolveTitle2(title);
7804
+ const t = resolveTitle2(title2);
7522
7805
  let top = CHROME_PAD.top;
7523
7806
  const innerWidth = size.width - CHROME_PAD.left - CHROME_PAD.right;
7524
7807
  if (t.text) {
@@ -9897,11 +10180,11 @@ function buildHeader(opts) {
9897
10180
  thead.appendChild(tr);
9898
10181
  return thead;
9899
10182
  }
9900
- function makeHeaderCell(opts, title, colIndex, align, rowIndex = 0) {
10183
+ function makeHeaderCell(opts, title2, colIndex, align, rowIndex = 0) {
9901
10184
  const th = document.createElement("th");
9902
10185
  const metrics = densityMetrics(opts.density);
9903
10186
  th.scope = "col";
9904
- th.textContent = title;
10187
+ th.textContent = title2;
9905
10188
  const isSorted = colIndex != null && opts.sortState?.col === colIndex;
9906
10189
  if (isSorted) th.setAttribute("aria-sort", opts.sortState?.dir === "desc" ? "descending" : "ascending");
9907
10190
  setCellBaseStyles(th, opts.tokens, align ?? "left", opts.stickyHeader !== false, metrics.padding);
@@ -9933,7 +10216,7 @@ function makeHeaderCell(opts, title, colIndex, align, rowIndex = 0) {
9933
10216
  font: "inherit"
9934
10217
  });
9935
10218
  const label = document.createElement("span");
9936
- label.textContent = title;
10219
+ label.textContent = title2;
9937
10220
  label.style.overflow = "hidden";
9938
10221
  label.style.textOverflow = "ellipsis";
9939
10222
  const glyph = document.createElement("span");
@@ -10332,21 +10615,6 @@ function paintRawCellText(opts, x, y, text, font, color, size, align = "left") {
10332
10615
  baseline: "middle"
10333
10616
  });
10334
10617
  }
10335
- function ellipsize(text, maxWidth, font) {
10336
- if (maxWidth <= 0 || text.length === 0) return "";
10337
- if (measureText(text, font).width <= maxWidth) return text;
10338
- const ellipsis = "\u2026";
10339
- const ellipsisW = measureText(ellipsis, font).width;
10340
- if (ellipsisW > maxWidth) return "";
10341
- let lo = 0;
10342
- let hi = text.length;
10343
- while (lo < hi) {
10344
- const mid = Math.ceil((lo + hi) / 2);
10345
- if (measureText(text.slice(0, mid) + ellipsis, font).width <= maxWidth) lo = mid;
10346
- else hi = mid - 1;
10347
- }
10348
- return text.slice(0, lo) + ellipsis;
10349
- }
10350
10618
  function resolveCanvasConditional(opts, column, colIndex, cell, rowBg) {
10351
10619
  const cf = column.conditionalFormat;
10352
10620
  const raw = cell.raw;
@@ -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;
@@ -13107,29 +13431,36 @@ function drawRect2(ctx, pen, rect, fill, stroke) {
13107
13431
  function addTileLabels(surface, tokens, tile, valueFormat) {
13108
13432
  if (tile.rect.width < MIN_LABEL_WIDTH || tile.rect.height < MIN_LABEL_HEIGHT) return;
13109
13433
  const pad3 = 5;
13110
- const textColor = readableTextColor(tile.fill);
13111
- const textCss = rgbaToCss(textColor);
13112
- const valueCss = rgbaToCss({ ...textColor, a: 0.72 });
13434
+ const padX = 6;
13435
+ const padY = 2;
13113
13436
  const labelSize = tokens.font.size.small;
13114
13437
  const valueSize = tokens.font.size.tiny;
13115
- const maxWidth = Math.max(0, tile.rect.width - pad3 * 2);
13438
+ const labelFont = fontString(labelSize, tokens.font.family, tokens.font.weight.bold);
13439
+ const valueFont = fontString(valueSize, tokens.font.family);
13440
+ const maxTextWidth = tile.rect.width - pad3 * 2 - padX * 2 - 2;
13441
+ const labelText2 = ellipsize(tile.categoryLabel, maxTextWidth, labelFont);
13442
+ if (labelText2 === "" || labelText2 === "\u2026") return;
13443
+ const pill = { background: tokens.color.surface, border: tokens.color.border, padX, padY };
13116
13444
  addOverlayText(surface, tokens, {
13117
13445
  left: tile.rect.x + pad3,
13118
13446
  top: tile.rect.y + pad3,
13119
- width: maxWidth,
13120
- text: tile.categoryLabel,
13121
- color: textCss,
13447
+ text: labelText2,
13448
+ color: tokens.color.text,
13122
13449
  size: labelSize,
13123
- weight: tokens.font.weight.bold
13450
+ weight: tokens.font.weight.bold,
13451
+ pill
13124
13452
  });
13125
- if (tile.rect.height >= 38) {
13453
+ const labelPillH = labelSize + padY * 2 + 2;
13454
+ const valuePillH = valueSize + padY * 2 + 2;
13455
+ const valueText = formatValue(tile.value, valueFormat);
13456
+ if (tile.rect.height >= pad3 * 2 + labelPillH + 2 + valuePillH && measureText(valueText, valueFont).width <= maxTextWidth) {
13126
13457
  addOverlayText(surface, tokens, {
13127
13458
  left: tile.rect.x + pad3,
13128
- top: tile.rect.y + pad3 + labelSize + 2,
13129
- width: maxWidth,
13130
- text: formatValue(tile.value, valueFormat),
13131
- color: valueCss,
13132
- size: valueSize
13459
+ top: tile.rect.y + pad3 + labelPillH + 2,
13460
+ text: valueText,
13461
+ color: tokens.color.textMuted,
13462
+ size: valueSize,
13463
+ pill
13133
13464
  });
13134
13465
  }
13135
13466
  }
@@ -13262,8 +13593,8 @@ function resolveValue2(ref, data) {
13262
13593
  function valueField2(ref) {
13263
13594
  return typeof ref === "number" ? null : ref.field;
13264
13595
  }
13265
- function titleText(title) {
13266
- return resolveTitle2(title).text;
13596
+ function titleText(title2) {
13597
+ return resolveTitle2(title2).text;
13267
13598
  }
13268
13599
  function withAlpha3(color, alpha) {
13269
13600
  const rgba2 = parseColor(color);
@@ -13447,9 +13778,9 @@ function drawGauge(surface, spec, tokens, size, context) {
13447
13778
  function clamp9(value, min, max) {
13448
13779
  return Math.max(min, Math.min(max, value));
13449
13780
  }
13450
- function titleText2(title) {
13451
- if (typeof title === "string") return title;
13452
- 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;
13453
13784
  return void 0;
13454
13785
  }
13455
13786
  function valueField3(ref) {
@@ -15278,6 +15609,13 @@ var Tooltip = class {
15278
15609
  };
15279
15610
 
15280
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
+ }
15281
15619
  var InteractionController = class {
15282
15620
  constructor(surface, tokens) {
15283
15621
  __publicField(this, "surface", surface);
@@ -15306,9 +15644,17 @@ var InteractionController = class {
15306
15644
  __publicField(this, "onClick", (e) => {
15307
15645
  const cfg = this.selectCfg;
15308
15646
  const model = this.model;
15309
- if (!cfg || !model || !model.pick) return;
15647
+ if (!cfg || !model) return;
15310
15648
  const rect = this.surface.root.getBoundingClientRect();
15311
- 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));
15312
15658
  });
15313
15659
  __publicField(this, "flush", () => {
15314
15660
  this.raf = 0;
@@ -15336,7 +15682,7 @@ var InteractionController = class {
15336
15682
  this.tokens = tokens;
15337
15683
  this.tooltip.setTokens(tokens);
15338
15684
  this.activeKey = null;
15339
- this.pickable = Boolean(model?.pick);
15685
+ this.pickable = Boolean(model?.pick || model?.legendHits?.length);
15340
15686
  if (!model) this.tooltip.hide();
15341
15687
  }
15342
15688
  /** Wire click/tap selection for this chart (or `null` to disable it). */
@@ -15604,6 +15950,14 @@ function buildRenderReport(input) {
15604
15950
  const colorCount = model ? new Set(seriesColors).size || 1 : seriesCount;
15605
15951
  const markCount = model ? model.series.reduce((n, s) => n + s.rows.length, 0) : data.length;
15606
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
+ }
15607
15961
  const hasLiteralValue = VALUE_CHART_TYPES.has(type) && typeof spec.value === "number";
15608
15962
  if (data.length === 0 && !hasLiteralValue) {
15609
15963
  add({
@@ -15627,9 +15981,9 @@ function buildRenderReport(input) {
15627
15981
  }
15628
15982
  if (model) {
15629
15983
  checkDegenerateAxis(model, add);
15630
- checkClippedMarks(model, add);
15631
- checkAxisLabelOverlap(model, tokens, add);
15632
- checkLegendOverflow(model, add);
15984
+ checkClippedMarks(model, spec, add);
15985
+ checkAxisLabelOverlap(model, spec, tokens, add);
15986
+ checkLegendOverflow(model, spec, add);
15633
15987
  }
15634
15988
  if (bg) {
15635
15989
  const specColor = spec.color;
@@ -15668,7 +16022,8 @@ function buildRenderReport(input) {
15668
16022
  code: "too-many-colors",
15669
16023
  severity: "info",
15670
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.`,
15671
- details: { colorCount, max: MAX_DISTINGUISHABLE_SERIES }
16025
+ details: { colorCount, max: MAX_DISTINGUISHABLE_SERIES },
16026
+ ...tooManyColorsRepair(spec)
15672
16027
  });
15673
16028
  }
15674
16029
  diagnostics.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
@@ -15676,6 +16031,11 @@ function buildRenderReport(input) {
15676
16031
  const summary = summarize({ ...spec, data }) || void 0;
15677
16032
  return { type, size, plot, markCount, seriesCount, colorCount, ok, diagnostics, summary };
15678
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
+ }
15679
16039
  function seriesCountOf(spec, data) {
15680
16040
  const enc = spec.encoding;
15681
16041
  const field = enc?.series?.field ?? enc?.color?.field;
@@ -15707,7 +16067,7 @@ function checkDegenerateAxis(model, add) {
15707
16067
  });
15708
16068
  }
15709
16069
  }
15710
- function checkClippedMarks(model, add) {
16070
+ function checkClippedMarks(model, spec, add) {
15711
16071
  const field = model.y.field;
15712
16072
  if (!field) return;
15713
16073
  const read = accessor(field);
@@ -15733,11 +16093,14 @@ function checkClippedMarks(model, add) {
15733
16093
  severity: "warning",
15734
16094
  axis: "y",
15735
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.`,
15736
- 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
+ }
15737
16100
  });
15738
16101
  }
15739
16102
  }
15740
- function checkAxisLabelOverlap(model, tokens, add) {
16103
+ function checkAxisLabelOverlap(model, spec, tokens, add) {
15741
16104
  const ticks2 = model.xTicks;
15742
16105
  if (ticks2.length < 2) return;
15743
16106
  if (model.frame.xLabelAngle > 0) return;
@@ -15757,11 +16120,14 @@ function checkAxisLabelOverlap(model, tokens, add) {
15757
16120
  severity: "warning",
15758
16121
  axis: "x",
15759
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.`,
15760
- 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
+ }
15761
16127
  });
15762
16128
  }
15763
16129
  }
15764
- function checkLegendOverflow(model, add) {
16130
+ function checkLegendOverflow(model, spec, add) {
15765
16131
  const shown = model.frame.legendItems?.length;
15766
16132
  if (shown === void 0) return;
15767
16133
  const total = model.series.length;
@@ -15770,10 +16136,52 @@ function checkLegendOverflow(model, add) {
15770
16136
  code: "legend-overflow",
15771
16137
  severity: "warning",
15772
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.`,
15773
- 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
+ }
15774
16143
  });
15775
16144
  }
15776
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
+ }
15777
16185
 
15778
16186
  // src/runtime/facet.ts
15779
16187
  var FACETABLE_TYPES2 = /* @__PURE__ */ new Set(["line", "area", "bar", "scatter"]);
@@ -15809,9 +16217,9 @@ function autoColumns(n) {
15809
16217
  function legendSymbol(type) {
15810
16218
  return type === "scatter" ? "circle" : type === "line" ? "line" : "square";
15811
16219
  }
15812
- function titleInput3(title) {
15813
- if (!title) return void 0;
15814
- 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;
15815
16223
  }
15816
16224
  function panelSpec(spec, data, value) {
15817
16225
  const base = spec;
@@ -15982,6 +16390,60 @@ var CANVAS_MARK_TYPES = /* @__PURE__ */ new Set([
15982
16390
  "choropleth"
15983
16391
  ]);
15984
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
+ }
15985
16447
  function resolveContainer(target) {
15986
16448
  if (typeof target === "string") {
15987
16449
  const el = document.querySelector(target);
@@ -16077,6 +16539,7 @@ function render(target, spec, options) {
16077
16539
  let interactionModel = null;
16078
16540
  let reportModel;
16079
16541
  let facetRpt;
16542
+ let legendSelect = null;
16080
16543
  const type = currentSpec.type;
16081
16544
  const facetLayout = isFaceted(effectiveSpec) ? buildFacetModels(effectiveSpec, tokens, size) : null;
16082
16545
  if (facetLayout) {
@@ -16085,6 +16548,8 @@ function render(target, spec, options) {
16085
16548
  } else if (CARTESIAN_TYPES.has(type)) {
16086
16549
  const renderer = cartesianRenderers[type];
16087
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 };
16088
16553
  model.emphasis = emphasis;
16089
16554
  reportModel = model;
16090
16555
  if (!anim.enabled) {
@@ -16133,9 +16598,14 @@ function render(target, spec, options) {
16133
16598
  }
16134
16599
  if (!interaction) interaction = new InteractionController(surface, tokens);
16135
16600
  interaction.setModel(interactionModel, tokens);
16136
- if (ownParam && interactionModel?.pick) {
16137
- const cfg = { store, param: ownParam.name, def: ownParam.select };
16138
- 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
+ });
16139
16609
  } else {
16140
16610
  interaction.setSelect(null);
16141
16611
  }
@@ -16192,7 +16662,7 @@ function render(target, spec, options) {
16192
16662
  const unsubscribe = store.subscribe((name, value) => {
16193
16663
  for (const listener of [...hostListeners]) listener(name, value);
16194
16664
  if (destroyed) return;
16195
- if (dependentParams(currentSpec.highlight, currentSpec.filter).has(name)) {
16665
+ if (dependentParams(currentSpec.highlight, currentSpec.filter).has(name) || specLegendParam(currentSpec) === name) {
16196
16666
  draw(false);
16197
16667
  }
16198
16668
  });
@@ -16373,6 +16843,14 @@ function isSlicerType(type) {
16373
16843
  function fieldOf(channel) {
16374
16844
  return channel && typeof channel.field === "string" ? channel.field : void 0;
16375
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
+ }
16376
16854
  function specFields(spec) {
16377
16855
  const out = /* @__PURE__ */ new Set();
16378
16856
  if (isSlicerType(spec.type)) {
@@ -16461,9 +16939,14 @@ function wireViews(views, interactions) {
16461
16939
  });
16462
16940
  } else if (PICKABLE.has(type)) {
16463
16941
  const fields = keyFields(v.spec);
16464
- if (fields.length === 0) return;
16465
- const param = autoParamName(v);
16466
- 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
+ }
16467
16950
  }
16468
16951
  });
16469
16952
  const explicit = Array.isArray(interactions);
@@ -16529,6 +17012,14 @@ var DEFAULT_BREAKPOINTS = [
16529
17012
  { maxWidth: 960, cols: 6 }
16530
17013
  ];
16531
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
+ }
16532
17023
  var clamp10 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
16533
17024
  function defaultSpan(spec, cols) {
16534
17025
  const t = spec.type;
@@ -16792,14 +17283,14 @@ function renderDashboard(target, spec, options) {
16792
17283
  gap: "2px"
16793
17284
  });
16794
17285
  if (view.title) {
16795
- const title = document.createElement("div");
16796
- title.textContent = view.title;
16797
- Object.assign(title.style, {
17286
+ const title2 = document.createElement("div");
17287
+ title2.textContent = view.title;
17288
+ Object.assign(title2.style, {
16798
17289
  font: `${tokens.font.weight.bold} ${tokens.font.size.base}px ${tokens.font.family}`,
16799
17290
  color: tokens.color.text,
16800
17291
  letterSpacing: "-0.01em"
16801
17292
  });
16802
- header.appendChild(title);
17293
+ header.appendChild(title2);
16803
17294
  }
16804
17295
  if (view.subtitle) {
16805
17296
  const subtitle = document.createElement("div");
@@ -16831,6 +17322,7 @@ function renderDashboard(target, spec, options) {
16831
17322
  Object.assign(strip.style, {
16832
17323
  display: "flex",
16833
17324
  flexWrap: "wrap",
17325
+ alignItems: "flex-start",
16834
17326
  gap: `${layout2.gap}px`
16835
17327
  });
16836
17328
  for (const view of stripViews) {
@@ -16838,7 +17330,7 @@ function renderDashboard(target, spec, options) {
16838
17330
  Object.assign(cell.style, {
16839
17331
  flex: "1 1 220px",
16840
17332
  minWidth: "200px",
16841
- height: `${Math.max(96, layout2.rowHeight + 8)}px`
17333
+ height: `${stripCellHeight(view, layout2.rowHeight)}px`
16842
17334
  });
16843
17335
  strip.appendChild(cell);
16844
17336
  deferredMounts.push({ el: host, view, mounted: false });
@@ -16878,10 +17370,10 @@ function renderDashboard(target, spec, options) {
16878
17370
  const titles = document.createElement("span");
16879
17371
  Object.assign(titles.style, { display: "flex", flexDirection: "column", gap: "2px" });
16880
17372
  if (plan.section?.title) {
16881
- const title = document.createElement("span");
16882
- title.textContent = plan.section.title;
16883
- Object.assign(title.style, { fontWeight: String(tokens.font.weight.bold), fontSize: `${tokens.font.size.large}px` });
16884
- 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);
16885
17377
  }
16886
17378
  if (plan.section?.subtitle) {
16887
17379
  const subtitle = document.createElement("span");
@@ -17224,6 +17716,6 @@ function renderToContext(target, spec) {
17224
17716
  // src/index.ts
17225
17717
  var VERSION = "0.1.0";
17226
17718
 
17227
- 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, 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 };
17228
17720
  //# sourceMappingURL=index.js.map
17229
17721
  //# sourceMappingURL=index.js.map