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.d.ts +63 -2
- package/dist/index.js +1828 -1336
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
1866
|
+
const title2 = chartTitleText(spec) ?? (spec.type === "kpi" ? spec.label : void 0);
|
|
1845
1867
|
let label = type;
|
|
1846
|
-
if (
|
|
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/
|
|
3075
|
-
var
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
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/
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
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/
|
|
3096
|
-
function
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
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
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
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
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
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
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
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/
|
|
3151
|
-
var
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
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
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
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
|
-
|
|
3218
|
-
|
|
3219
|
-
this.marks.clear();
|
|
3220
|
-
this.interaction.clear();
|
|
3221
|
-
this.overlay.replaceChildren();
|
|
3356
|
+
peek() {
|
|
3357
|
+
return this.tokens[this.pos];
|
|
3222
3358
|
}
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
this.clear();
|
|
3226
|
-
this.root.remove();
|
|
3359
|
+
next() {
|
|
3360
|
+
return this.tokens[this.pos++];
|
|
3227
3361
|
}
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
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
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
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
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
}
|
|
3269
|
-
|
|
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
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
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
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
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
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
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
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
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
|
-
|
|
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
|
|
3441
|
-
if (
|
|
3442
|
-
if (
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
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/
|
|
3450
|
-
var
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
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
|
-
|
|
3460
|
-
if (
|
|
3461
|
-
if (
|
|
3462
|
-
|
|
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
|
|
3484
|
-
return
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
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
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
var
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
];
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
"
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
];
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
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
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
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
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
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
|
-
|
|
3633
|
+
}
|
|
3634
|
+
function evaluate(node, row) {
|
|
3635
|
+
return evalNode(node, row);
|
|
3578
3636
|
}
|
|
3579
3637
|
|
|
3580
|
-
// src/spec/transform/
|
|
3581
|
-
function
|
|
3582
|
-
|
|
3583
|
-
|
|
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
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
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/
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
return
|
|
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
|
-
|
|
3667
|
-
|
|
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
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
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
|
-
|
|
3739
|
-
|
|
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
|
-
|
|
3754
|
-
|
|
3678
|
+
if ("not" in pred) {
|
|
3679
|
+
const inner = compilePredicate(pred.not);
|
|
3680
|
+
return (row) => !inner(row);
|
|
3755
3681
|
}
|
|
3756
|
-
|
|
3757
|
-
|
|
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
|
-
|
|
3760
|
-
const
|
|
3761
|
-
return
|
|
3687
|
+
if ("ne" in pred) {
|
|
3688
|
+
const want = toKey(pred.ne);
|
|
3689
|
+
return (row) => toKey(read(row)) !== want;
|
|
3762
3690
|
}
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
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
|
-
|
|
3771
|
-
const
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
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
|
-
|
|
3777
|
-
const
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
const
|
|
3781
|
-
|
|
3782
|
-
|
|
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
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
const
|
|
3794
|
-
|
|
3795
|
-
|
|
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
|
-
|
|
3803
|
-
const t =
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
return
|
|
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
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
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
|
-
|
|
3830
|
-
const t =
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
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
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
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
|
|
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
|
|
3903
|
-
const
|
|
3904
|
-
|
|
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
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
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
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
case "
|
|
4010
|
-
return
|
|
4011
|
-
case "
|
|
4012
|
-
return
|
|
4013
|
-
case "
|
|
4014
|
-
return
|
|
4015
|
-
case "
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
return
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
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
|
-
|
|
3796
|
+
return new Date(date.getTime());
|
|
4029
3797
|
}
|
|
4030
3798
|
}
|
|
4031
|
-
function
|
|
4032
|
-
|
|
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/
|
|
4036
|
-
function
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
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
|
|
5931
|
+
return false;
|
|
5737
5932
|
}
|
|
5738
|
-
function
|
|
5739
|
-
|
|
5740
|
-
if (
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
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
|
|
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
|
|
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}|${
|
|
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
|
|
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:
|
|
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 &&
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
7462
|
-
if (typeof
|
|
7463
|
-
if (
|
|
7464
|
-
const t =
|
|
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,
|
|
7802
|
+
function drawTitleBlock(surface, tokens, size, title2) {
|
|
7520
7803
|
const f = tokens.font;
|
|
7521
|
-
const t = resolveTitle2(
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
10982
|
-
const labelText2 =
|
|
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
|
-
|
|
11264
|
+
title2.text ? tokens.font.size.large : tokens.font.size.small,
|
|
10997
11265
|
tokens.font.family,
|
|
10998
|
-
|
|
11266
|
+
title2.text ? tokens.font.weight.bold : tokens.font.weight.medium
|
|
10999
11267
|
),
|
|
11000
|
-
color:
|
|
11268
|
+
color: title2.text ? c.text : c.textMuted,
|
|
11001
11269
|
whiteSpace: "nowrap",
|
|
11002
11270
|
overflow: "hidden",
|
|
11003
11271
|
textOverflow: "ellipsis"
|
|
11004
11272
|
});
|
|
11005
|
-
if (
|
|
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: "
|
|
11108
|
-
zIndex: "
|
|
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
|
|
11593
|
+
function positionPopover(pop, anchor) {
|
|
11326
11594
|
const a = anchor.getBoundingClientRect();
|
|
11327
|
-
const
|
|
11328
|
-
|
|
11329
|
-
|
|
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
|
|
11332
|
-
pop.style.
|
|
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
|
-
|
|
11473
|
-
positionPopover(pop, trigger
|
|
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))
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
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
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
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
|
|
13111
|
-
const
|
|
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
|
|
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
|
-
|
|
13120
|
-
|
|
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
|
-
|
|
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 +
|
|
13129
|
-
|
|
13130
|
-
|
|
13131
|
-
|
|
13132
|
-
|
|
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(
|
|
13266
|
-
return resolveTitle2(
|
|
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(
|
|
13451
|
-
if (typeof
|
|
13452
|
-
if (
|
|
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
|
|
15647
|
+
if (!cfg || !model) return;
|
|
15310
15648
|
const rect = this.surface.root.getBoundingClientRect();
|
|
15311
|
-
|
|
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(
|
|
15813
|
-
if (!
|
|
15814
|
-
return typeof
|
|
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({
|
|
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
|
|
16465
|
-
|
|
16466
|
-
|
|
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
|
|
16796
|
-
|
|
16797
|
-
Object.assign(
|
|
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(
|
|
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: `${
|
|
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
|
|
16882
|
-
|
|
16883
|
-
Object.assign(
|
|
16884
|
-
titles.appendChild(
|
|
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
|