vega-lite
Version:
Vega-Lite is a concise high-level language for interactive visualization.
1,663 lines (1,652 loc) • 742 kB
JavaScript
import { isObject, hasOwnProperty, isNumber, isString, splitAccessPath, stringValue, writeConfig, isBoolean as isBoolean$1, isArray, array, logger, Warn, isFunction, mergeConfig, identity } from 'vega-util';
import { stringValue as stringValue$1, hasOwnProperty as hasOwnProperty$1, isObject as isObject$1, isString as isString$1 } from 'vega';
import { parseSelector } from 'vega-event-selector';
import { parseExpression as parseExpression$1 } from 'vega-expression';
var version$1 = "6.4.1";
var pkg = {
version: version$1};
function isLogicalOr(op) {
return hasProperty(op, 'or');
}
function isLogicalAnd(op) {
return hasProperty(op, 'and');
}
function isLogicalNot(op) {
return hasProperty(op, 'not');
}
function forEachLeaf(op, fn) {
if (isLogicalNot(op)) {
forEachLeaf(op.not, fn);
}
else if (isLogicalAnd(op)) {
for (const subop of op.and) {
forEachLeaf(subop, fn);
}
}
else if (isLogicalOr(op)) {
for (const subop of op.or) {
forEachLeaf(subop, fn);
}
}
else {
fn(op);
}
}
function normalizeLogicalComposition(op, normalizer) {
if (isLogicalNot(op)) {
return { not: normalizeLogicalComposition(op.not, normalizer) };
}
else if (isLogicalAnd(op)) {
return { and: op.and.map((o) => normalizeLogicalComposition(o, normalizer)) };
}
else if (isLogicalOr(op)) {
return { or: op.or.map((o) => normalizeLogicalComposition(o, normalizer)) };
}
else {
return normalizer(op);
}
}
const duplicate = structuredClone;
function never(message) {
throw new Error(message);
}
/**
* Creates an object composed of the picked object properties.
*
* var object = {'a': 1, 'b': '2', 'c': 3};
* pick(object, ['a', 'c']);
* // → {'a': 1, 'c': 3}
*/
function pick(obj, props) {
const copy = {};
for (const prop of props) {
if (hasOwnProperty(obj, prop)) {
copy[prop] = obj[prop];
}
}
return copy;
}
/**
* The opposite of _.pick; this method creates an object composed of the own
* and inherited enumerable string keyed properties of object that are not omitted.
*/
function omit(obj, props) {
const copy = { ...obj };
for (const prop of props) {
delete copy[prop];
}
return copy;
}
/**
* Monkey patch Set so that `stringify` produces a string representation of sets.
*/
Set.prototype['toJSON'] = function () {
return `Set(${[...this].map((x) => stringify(x)).join(',')})`;
};
/**
* Converts any object to a string of limited size, or a number.
*/
function hash(a) {
if (isNumber(a)) {
return a;
}
const str = isString(a) ? a : stringify(a);
// short strings can be used as hash directly, longer strings are hashed to reduce memory usage
if (str.length < 250) {
return str;
}
// from http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
let h = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
h = (h << 5) - h + char;
h = h & h; // Convert to 32bit integer
}
return h;
}
function isNullOrFalse(x) {
return x === false || x === null;
}
function contains(array, item) {
return array.includes(item);
}
/**
* Returns true if any item returns true.
*/
function some(arr, f) {
let i = 0;
for (const [k, a] of arr.entries()) {
if (f(a, k, i++)) {
return true;
}
}
return false;
}
/**
* Returns true if all items return true.
*/
function every(arr, f) {
let i = 0;
for (const [k, a] of arr.entries()) {
if (!f(a, k, i++)) {
return false;
}
}
return true;
}
/**
* recursively merges src into dest
*/
function mergeDeep(dest, ...src) {
for (const s of src) {
deepMerge_(dest, s ?? {});
}
return dest;
}
function deepMerge_(dest, src) {
for (const property of keys(src)) {
writeConfig(dest, property, src[property], true);
}
}
function unique(values, f) {
const results = [];
const u = {};
let v;
for (const val of values) {
v = f(val);
if (v in u) {
continue;
}
u[v] = 1;
results.push(val);
}
return results;
}
/**
* Returns true if the two dictionaries agree. Applies only to defined values.
*/
function isEqual(dict, other) {
const dictKeys = keys(dict);
const otherKeys = keys(other);
if (dictKeys.length !== otherKeys.length) {
return false;
}
for (const key of dictKeys) {
if (dict[key] !== other[key]) {
return false;
}
}
return true;
}
function setEqual(a, b) {
if (a.size !== b.size) {
return false;
}
for (const e of a) {
if (!b.has(e)) {
return false;
}
}
return true;
}
function hasIntersection(a, b) {
for (const key of a) {
if (b.has(key)) {
return true;
}
}
return false;
}
function prefixGenerator(a) {
const prefixes = new Set();
for (const x of a) {
const splitField = splitAccessPath(x);
// Wrap every element other than the first in `[]`
const wrappedWithAccessors = splitField.map((y, i) => (i === 0 ? y : `[${y}]`));
const computedPrefixes = wrappedWithAccessors.map((_, i) => wrappedWithAccessors.slice(0, i + 1).join(''));
for (const y of computedPrefixes) {
prefixes.add(y);
}
}
return prefixes;
}
/**
* Returns true if a and b have an intersection. Also return true if a or b are undefined
* since this means we don't know what fields a node produces or depends on.
*/
function fieldIntersection(a, b) {
if (a === undefined || b === undefined) {
return true;
}
return hasIntersection(prefixGenerator(a), prefixGenerator(b));
}
function isEmpty(obj) {
return keys(obj).length === 0;
}
// This is a stricter version of Object.keys but with better types. See https://github.com/Microsoft/TypeScript/pull/12253#issuecomment-263132208
const keys = Object.keys;
// Stricter version from https://github.com/microsoft/TypeScript/issues/51572#issuecomment-1319153323
const vals = Object.values;
// Stricter version from https://github.com/microsoft/TypeScript/issues/51572#issuecomment-1319153323
const entries$1 = Object.entries;
function isBoolean(b) {
return b === true || b === false;
}
/**
* Convert a string into a valid variable name
*/
function varName(s) {
// Replace non-alphanumeric characters (anything besides a-zA-Z0-9_) with _
const alphanumericS = s.replace(/\W/g, '_');
// Add _ if the string has leading numbers.
return (s.match(/^\d+/) ? '_' : '') + alphanumericS;
}
function logicalExpr(op, cb) {
if (isLogicalNot(op)) {
return `!(${logicalExpr(op.not, cb)})`;
}
else if (isLogicalAnd(op)) {
return `(${op.and.map((and) => logicalExpr(and, cb)).join(') && (')})`;
}
else if (isLogicalOr(op)) {
return `(${op.or.map((or) => logicalExpr(or, cb)).join(') || (')})`;
}
else {
return cb(op);
}
}
/**
* Delete nested property of an object, and delete the ancestors of the property if they become empty.
*/
function deleteNestedProperty(obj, orderedProps) {
if (orderedProps.length === 0) {
return true;
}
const prop = orderedProps.shift();
if (prop in obj && deleteNestedProperty(obj[prop], orderedProps)) {
delete obj[prop];
}
return isEmpty(obj);
}
function titleCase(s) {
return s.charAt(0).toUpperCase() + s.substr(1);
}
/**
* Converts a path to an access path with datum.
* @param path The field name.
* @param datum The string to use for `datum`.
*/
function accessPathWithDatum(path, datum = 'datum') {
const pieces = splitAccessPath(path);
const prefixes = [];
for (let i = 1; i <= pieces.length; i++) {
const prefix = `[${pieces.slice(0, i).map(stringValue).join('][')}]`;
prefixes.push(`${datum}${prefix}`);
}
return prefixes.join(' && ');
}
/**
* Return access with datum to the flattened field.
*
* @param path The field name.
* @param datum The string to use for `datum`.
*/
function flatAccessWithDatum(path, datum = 'datum') {
return `${datum}[${stringValue(splitAccessPath(path).join('.'))}]`;
}
/**
* Return access with datum to **an unescaped path**.
*
* ```ts
* console.log(accessWithDatumToUnescapedPath("vega's favorite"))
* // "datum['vega\\'s favorite']"
* ```
*
* @param path The unescaped path name. E.g., `"a.b"`, `"vega's favorite"`. (Note
* that the field defs take escaped strings like `"a\\.b"`, `"vega\\'s favorite"`,
* but this function is for the unescaped field/path)
*/
function accessWithDatumToUnescapedPath(unescapedPath) {
const singleQuoteEscapedPath = unescapedPath.replaceAll("'", "\\'");
return `datum['${singleQuoteEscapedPath}']`;
}
function unescapeSingleQuoteAndPathDot(escapedPath) {
return escapedPath.replaceAll("\\'", "'").replaceAll('\\.', '.');
}
function escapePathAccess(string) {
return string.replace(/(\[|\]|\.|'|")/g, '\\$1');
}
/**
* Replaces path accesses with access to non-nested field.
* For example, `foo["bar"].baz` becomes `foo\\.bar\\.baz`.
*/
function replacePathInField(path) {
return `${splitAccessPath(path).map(escapePathAccess).join('\\.')}`;
}
/**
* Replace all occurrences of a string with another string.
*
* @param string the string to replace in
* @param find the string to replace
* @param replacement the replacement
*/
function replaceAll(string, find, replacement) {
return string.replace(new RegExp(find.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'), replacement);
}
/**
* Remove path accesses with access from field.
* For example, `foo["bar"].baz` becomes `foo.bar.baz`.
*/
function removePathFromField(path) {
return `${splitAccessPath(path).join('.')}`;
}
/**
* Count the depth of the path. Returns 1 for fields that are not nested.
*/
function accessPathDepth(path) {
if (!path) {
return 0;
}
return splitAccessPath(path).length;
}
/**
* This is a replacement for chained || for numeric properties or properties that respect null so that 0 will be included.
*/
function getFirstDefined(...args) {
return args.find((a) => a !== undefined);
}
// variable used to generate id
let idCounter = 42;
/**
* Returns a new random id every time it gets called.
*
* Has side effect!
*/
function uniqueId(prefix) {
const id = ++idCounter;
return prefix ? String(prefix) + id : id;
}
/**
* Resets the id counter used in uniqueId. This can be useful for testing.
*/
function resetIdCounter() {
idCounter = 42;
}
function internalField(name) {
return isInternalField(name) ? name : `__${name}`;
}
function isInternalField(name) {
return name.startsWith('__');
}
/**
* Normalize angle to be within [0,360).
*/
function normalizeAngle(angle) {
if (angle === undefined) {
return undefined;
}
return ((angle % 360) + 360) % 360;
}
/**
* Returns whether the passed in value is a valid number.
*/
function isNumeric(value) {
if (isNumber(value)) {
return true;
}
return !isNaN(value) && !isNaN(parseFloat(value));
}
const clonedProto = Object.getPrototypeOf(structuredClone({}));
/**
* Compares two values for equality, including arrays and objects.
*
* Adapted from https://github.com/epoberezkin/fast-deep-equal.
*/
function deepEqual(a, b) {
if (a === b)
return true;
if (a && b && typeof a == 'object' && typeof b == 'object') {
// compare names to avoid issues with structured clone
if (a.constructor.name !== b.constructor.name)
return false;
let length;
let i;
if (Array.isArray(a)) {
length = a.length;
if (length != b.length)
return false;
for (i = length; i-- !== 0;)
if (!deepEqual(a[i], b[i]))
return false;
return true;
}
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size)
return false;
for (const e of a.entries())
if (!b.has(e[0]))
return false;
for (const e of a.entries())
if (!deepEqual(e[1], b.get(e[0])))
return false;
return true;
}
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size)
return false;
for (const e of a.entries())
if (!b.has(e[0]))
return false;
return true;
}
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
length = a.length;
if (length != b.length)
return false;
for (i = length; i-- !== 0;)
if (a[i] !== b[i])
return false;
return true;
}
if (a.constructor === RegExp)
return a.source === b.source && a.flags === b.flags;
// also compare to structured clone prototype
if (a.valueOf !== Object.prototype.valueOf && a.valueOf !== clonedProto.valueOf)
return a.valueOf() === b.valueOf();
if (a.toString !== Object.prototype.toString && a.toString !== clonedProto.toString)
return a.toString() === b.toString();
const ks = Object.keys(a);
length = ks.length;
if (length !== Object.keys(b).length)
return false;
for (i = length; i-- !== 0;)
if (!Object.prototype.hasOwnProperty.call(b, ks[i]))
return false;
for (i = length; i-- !== 0;) {
const key = ks[i];
if (!deepEqual(a[key], b[key]))
return false;
}
return true;
}
// true if both NaN, false otherwise
return a !== a && b !== b;
}
/**
* Converts any object to a string representation that can be consumed by humans.
*
* Adapted from https://github.com/epoberezkin/fast-json-stable-stringify
*/
function stringify(data) {
const seen = [];
return (function _stringify(node) {
if (node?.toJSON && typeof node.toJSON === 'function') {
node = node.toJSON();
}
if (node === undefined)
return undefined;
if (typeof node == 'number')
return isFinite(node) ? `${node}` : 'null';
if (typeof node !== 'object')
return JSON.stringify(node);
let i;
let out;
if (Array.isArray(node)) {
out = '[';
for (i = 0; i < node.length; i++) {
if (i)
out += ',';
out += _stringify(node[i]) || 'null';
}
return `${out}]`;
}
if (node === null)
return 'null';
if (seen.includes(node)) {
throw new TypeError('Converting circular structure to JSON');
}
const seenIndex = seen.push(node) - 1;
const ks = Object.keys(node).sort();
out = '';
for (i = 0; i < ks.length; i++) {
const key = ks[i];
const value = _stringify(node[key]);
if (!value)
continue;
if (out)
out += ',';
out += `${JSON.stringify(key)}:${value}`;
}
seen.splice(seenIndex, 1);
return `{${out}}`;
})(data);
}
/**
* Check if the input object has the property and it's not undefined.
*
* @param object the object
* @param property the property to search
* @returns if the object has the property and it's not undefined.
*/
function hasProperty(obj, key) {
return isObject(obj) && hasOwnProperty(obj, key) && obj[key] !== undefined;
}
/*
* Constants and utilities for encoding channels (Visual variables)
* such as 'x', 'y', 'color'.
*/
// Facet
const ROW = 'row';
const COLUMN = 'column';
const FACET = 'facet';
// Position
const X = 'x';
const Y = 'y';
const X2 = 'x2';
const Y2 = 'y2';
// Position Offset
const XOFFSET = 'xOffset';
const YOFFSET = 'yOffset';
// Arc-Position
const RADIUS = 'radius';
const RADIUS2 = 'radius2';
const THETA = 'theta';
const THETA2 = 'theta2';
// Geo Position
const LATITUDE = 'latitude';
const LONGITUDE = 'longitude';
const LATITUDE2 = 'latitude2';
const LONGITUDE2 = 'longitude2';
// Time
const TIME = 'time';
// Mark property with scale
const COLOR = 'color';
const FILL = 'fill';
const STROKE = 'stroke';
const SHAPE = 'shape';
const SIZE = 'size';
const ANGLE = 'angle';
const OPACITY = 'opacity';
const FILLOPACITY = 'fillOpacity';
const STROKEOPACITY = 'strokeOpacity';
const STROKEWIDTH = 'strokeWidth';
const STROKEDASH = 'strokeDash';
// Non-scale channel
const TEXT$1 = 'text';
const ORDER = 'order';
const DETAIL = 'detail';
const KEY = 'key';
const TOOLTIP = 'tooltip';
const HREF = 'href';
const URL = 'url';
const DESCRIPTION = 'description';
const POSITION_CHANNEL_INDEX = {
x: 1,
y: 1,
x2: 1,
y2: 1,
};
const POLAR_POSITION_CHANNEL_INDEX = {
theta: 1,
theta2: 1,
radius: 1,
radius2: 1,
};
function isPolarPositionChannel(c) {
return hasOwnProperty(POLAR_POSITION_CHANNEL_INDEX, c);
}
const GEO_POSIITON_CHANNEL_INDEX = {
longitude: 1,
longitude2: 1,
latitude: 1,
latitude2: 1,
};
function getPositionChannelFromLatLong(channel) {
switch (channel) {
case LATITUDE:
return 'y';
case LATITUDE2:
return 'y2';
case LONGITUDE:
return 'x';
case LONGITUDE2:
return 'x2';
}
}
function isGeoPositionChannel(c) {
return hasOwnProperty(GEO_POSIITON_CHANNEL_INDEX, c);
}
const GEOPOSITION_CHANNELS = keys(GEO_POSIITON_CHANNEL_INDEX);
const UNIT_CHANNEL_INDEX = {
...POSITION_CHANNEL_INDEX,
...POLAR_POSITION_CHANNEL_INDEX,
...GEO_POSIITON_CHANNEL_INDEX,
xOffset: 1,
yOffset: 1,
// color
color: 1,
fill: 1,
stroke: 1,
// time
time: 1,
// other non-position with scale
opacity: 1,
fillOpacity: 1,
strokeOpacity: 1,
strokeWidth: 1,
strokeDash: 1,
size: 1,
angle: 1,
shape: 1,
// channels without scales
order: 1,
text: 1,
detail: 1,
key: 1,
tooltip: 1,
href: 1,
url: 1,
description: 1,
};
function isColorChannel(channel) {
return channel === COLOR || channel === FILL || channel === STROKE;
}
const FACET_CHANNEL_INDEX = {
row: 1,
column: 1,
facet: 1,
};
const FACET_CHANNELS = keys(FACET_CHANNEL_INDEX);
const CHANNEL_INDEX = {
...UNIT_CHANNEL_INDEX,
...FACET_CHANNEL_INDEX,
};
const CHANNELS = keys(CHANNEL_INDEX);
const { order: _o, detail: _d, tooltip: _tt1, ...SINGLE_DEF_CHANNEL_INDEX } = CHANNEL_INDEX;
const { row: _r, column: _c, facet: _f, ...SINGLE_DEF_UNIT_CHANNEL_INDEX } = SINGLE_DEF_CHANNEL_INDEX;
function isSingleDefUnitChannel(str) {
return hasOwnProperty(SINGLE_DEF_UNIT_CHANNEL_INDEX, str);
}
function isChannel(str) {
return hasOwnProperty(CHANNEL_INDEX, str);
}
const SECONDARY_RANGE_CHANNEL = [X2, Y2, LATITUDE2, LONGITUDE2, THETA2, RADIUS2];
function isSecondaryRangeChannel(c) {
const main = getMainRangeChannel(c);
return main !== c;
}
/**
* Get the main channel for a range channel. E.g. `x` for `x2`.
*/
function getMainRangeChannel(channel) {
switch (channel) {
case X2:
return X;
case Y2:
return Y;
case LATITUDE2:
return LATITUDE;
case LONGITUDE2:
return LONGITUDE;
case THETA2:
return THETA;
case RADIUS2:
return RADIUS;
}
return channel;
}
function getVgPositionChannel(channel) {
if (isPolarPositionChannel(channel)) {
switch (channel) {
case THETA:
return 'startAngle';
case THETA2:
return 'endAngle';
case RADIUS:
return 'outerRadius';
case RADIUS2:
return 'innerRadius';
}
}
return channel;
}
/**
* Get the main channel for a range channel. E.g. `x` for `x2`.
*/
function getSecondaryRangeChannel(channel) {
switch (channel) {
case X:
return X2;
case Y:
return Y2;
case LATITUDE:
return LATITUDE2;
case LONGITUDE:
return LONGITUDE2;
case THETA:
return THETA2;
case RADIUS:
return RADIUS2;
}
return undefined;
}
function getSizeChannel(channel) {
switch (channel) {
case X:
case X2:
return 'width';
case Y:
case Y2:
return 'height';
}
return undefined;
}
/**
* Get the main channel for a range channel. E.g. `x` for `x2`.
*/
function getOffsetChannel(channel) {
switch (channel) {
case X:
return 'xOffset';
case Y:
return 'yOffset';
case X2:
return 'x2Offset';
case Y2:
return 'y2Offset';
case THETA:
return 'thetaOffset';
case RADIUS:
return 'radiusOffset';
case THETA2:
return 'theta2Offset';
case RADIUS2:
return 'radius2Offset';
}
return undefined;
}
/**
* Get the main channel for a range channel. E.g. `x` for `x2`.
*/
function getOffsetScaleChannel(channel) {
switch (channel) {
case X:
return 'xOffset';
case Y:
return 'yOffset';
}
return undefined;
}
function getMainChannelFromOffsetChannel(channel) {
switch (channel) {
case 'xOffset':
return 'x';
case 'yOffset':
return 'y';
}
}
// CHANNELS without COLUMN, ROW
const UNIT_CHANNELS = keys(UNIT_CHANNEL_INDEX);
// NONPOSITION_CHANNELS = UNIT_CHANNELS without X, Y, X2, Y2;
const { x: _x, y: _y,
// x2 and y2 share the same scale as x and y
x2: _x2, y2: _y2,
//
xOffset: _xo, yOffset: _yo, latitude: _latitude, longitude: _longitude, latitude2: _latitude2, longitude2: _longitude2, theta: _theta, theta2: _theta2, radius: _radius, radius2: _radius2,
// The rest of unit channels then have scale
...NONPOSITION_CHANNEL_INDEX } = UNIT_CHANNEL_INDEX;
const NONPOSITION_CHANNELS = keys(NONPOSITION_CHANNEL_INDEX);
const POSITION_SCALE_CHANNEL_INDEX = {
x: 1,
y: 1,
};
const POSITION_SCALE_CHANNELS = keys(POSITION_SCALE_CHANNEL_INDEX);
function isXorY(channel) {
return hasOwnProperty(POSITION_SCALE_CHANNEL_INDEX, channel);
}
const POLAR_POSITION_SCALE_CHANNEL_INDEX = {
theta: 1,
radius: 1,
};
const POLAR_POSITION_SCALE_CHANNELS = keys(POLAR_POSITION_SCALE_CHANNEL_INDEX);
function getPositionScaleChannel(sizeType) {
return sizeType === 'width' ? X : Y;
}
const OFFSET_SCALE_CHANNEL_INDEX = { xOffset: 1, yOffset: 1 };
function isXorYOffset(channel) {
return hasOwnProperty(OFFSET_SCALE_CHANNEL_INDEX, channel);
}
const TIME_SCALE_CHANNEL_INDEX = {
time: 1,
};
function isTime(channel) {
return channel in TIME_SCALE_CHANNEL_INDEX;
}
// NON_POSITION_SCALE_CHANNEL = SCALE_CHANNELS without position / offset
const {
// x2 and y2 share the same scale as x and y
// text and tooltip have format instead of scale,
// href has neither format, nor scale
text: _t, tooltip: _tt, href: _hr, url: _u, description: _al,
// detail and order have no scale
detail: _dd, key: _k, order: _oo, ...NONPOSITION_SCALE_CHANNEL_INDEX } = NONPOSITION_CHANNEL_INDEX;
const NONPOSITION_SCALE_CHANNELS = keys(NONPOSITION_SCALE_CHANNEL_INDEX);
function isNonPositionScaleChannel(channel) {
return hasOwnProperty(NONPOSITION_CHANNEL_INDEX, channel);
}
/**
* @returns whether Vega supports legends for a particular channel
*/
function supportLegend(channel) {
switch (channel) {
case COLOR:
case FILL:
case STROKE:
case SIZE:
case SHAPE:
case OPACITY:
case STROKEWIDTH:
case STROKEDASH:
return true;
case FILLOPACITY:
case STROKEOPACITY:
case ANGLE:
case TIME:
return false;
}
}
// Declare SCALE_CHANNEL_INDEX
const SCALE_CHANNEL_INDEX = {
...POSITION_SCALE_CHANNEL_INDEX,
...POLAR_POSITION_SCALE_CHANNEL_INDEX,
...OFFSET_SCALE_CHANNEL_INDEX,
...NONPOSITION_SCALE_CHANNEL_INDEX,
};
/** List of channels with scales */
const SCALE_CHANNELS = keys(SCALE_CHANNEL_INDEX);
function isScaleChannel(channel) {
return hasOwnProperty(SCALE_CHANNEL_INDEX, channel);
}
/**
* Return whether a channel supports a particular mark type.
* @param channel channel name
* @param mark the mark type
* @return whether the mark supports the channel
*/
function supportMark(channel, mark) {
return getSupportedMark(channel)[mark];
}
const ALL_MARKS = {
// all marks
arc: 'always',
area: 'always',
bar: 'always',
circle: 'always',
geoshape: 'always',
image: 'always',
line: 'always',
rule: 'always',
point: 'always',
rect: 'always',
square: 'always',
trail: 'always',
text: 'always',
tick: 'always',
};
const { geoshape: _g, ...ALL_MARKS_EXCEPT_GEOSHAPE } = ALL_MARKS;
/**
* Return a dictionary showing whether a channel supports mark type.
* @param channel
* @return A dictionary mapping mark types to 'always', 'binned', or undefined
*/
function getSupportedMark(channel) {
switch (channel) {
case COLOR:
case FILL:
case STROKE:
// falls through
case DESCRIPTION:
case DETAIL:
case KEY:
case TOOLTIP:
case HREF:
case ORDER: // TODO: revise (order might not support rect, which is not stackable?)
case OPACITY:
case FILLOPACITY:
case STROKEOPACITY:
case STROKEWIDTH:
// falls through
case FACET:
case ROW: // falls through
case COLUMN:
return ALL_MARKS;
case X:
case Y:
case XOFFSET:
case YOFFSET:
case LATITUDE:
case LONGITUDE:
case TIME:
// all marks except geoshape. geoshape does not use X, Y -- it uses a projection
return ALL_MARKS_EXCEPT_GEOSHAPE;
case X2:
case Y2:
case LATITUDE2:
case LONGITUDE2:
return {
area: 'always',
bar: 'always',
image: 'always',
rect: 'always',
rule: 'always',
circle: 'binned',
point: 'binned',
square: 'binned',
tick: 'binned',
line: 'binned',
trail: 'binned',
};
case SIZE:
return {
point: 'always',
tick: 'always',
rule: 'always',
circle: 'always',
square: 'always',
bar: 'always',
text: 'always',
line: 'always',
trail: 'always',
};
case STROKEDASH:
return {
line: 'always',
point: 'always',
tick: 'always',
rule: 'always',
circle: 'always',
square: 'always',
bar: 'always',
geoshape: 'always',
};
case SHAPE:
return { point: 'always', geoshape: 'always' };
case TEXT$1:
return { text: 'always' };
case ANGLE:
return { point: 'always', square: 'always', text: 'always' };
case URL:
return { image: 'always' };
case THETA:
return { text: 'always', arc: 'always' };
case RADIUS:
return { text: 'always', arc: 'always' };
case THETA2:
case RADIUS2:
return { arc: 'always' };
}
}
function rangeType(channel) {
switch (channel) {
case X:
case Y:
case THETA:
case RADIUS:
case XOFFSET:
case YOFFSET:
case SIZE:
case ANGLE:
case STROKEWIDTH:
case OPACITY:
case FILLOPACITY:
case STROKEOPACITY:
case TIME:
// X2 and Y2 use X and Y scales, so they similarly have continuous range. [falls through]
case X2:
case Y2:
case THETA2:
case RADIUS2:
return undefined;
case FACET:
case ROW:
case COLUMN:
case SHAPE:
case STROKEDASH:
// TEXT, TOOLTIP, URL, and HREF have no scale but have discrete output [falls through]
case TEXT$1:
case TOOLTIP:
case HREF:
case URL:
case DESCRIPTION:
return 'discrete';
// Color can be either continuous or discrete, depending on scale type.
case COLOR:
case FILL:
case STROKE:
return 'flexible';
// No scale, no range type.
case LATITUDE:
case LONGITUDE:
case LATITUDE2:
case LONGITUDE2:
case DETAIL:
case KEY:
case ORDER:
return undefined;
}
}
const AGGREGATE_OP_INDEX = {
argmax: 1,
argmin: 1,
average: 1,
count: 1,
distinct: 1,
exponential: 1,
exponentialb: 1,
product: 1,
max: 1,
mean: 1,
median: 1,
min: 1,
missing: 1,
q1: 1,
q3: 1,
ci0: 1,
ci1: 1,
stderr: 1,
stdev: 1,
stdevp: 1,
sum: 1,
valid: 1,
values: 1,
variance: 1,
variancep: 1,
};
const MULTIDOMAIN_SORT_OP_INDEX = {
count: 1,
min: 1,
max: 1,
};
function isArgminDef(a) {
return hasProperty(a, 'argmin');
}
function isArgmaxDef(a) {
return hasProperty(a, 'argmax');
}
function isAggregateOp(a) {
return isString(a) && hasOwnProperty(AGGREGATE_OP_INDEX, a);
}
const COUNTING_OPS = new Set([
'count',
'valid',
'missing',
'distinct',
]);
function isCountingAggregateOp(aggregate) {
return isString(aggregate) && COUNTING_OPS.has(aggregate);
}
function isMinMaxOp(aggregate) {
return isString(aggregate) && contains(['min', 'max'], aggregate);
}
/** Additive-based aggregation operations. These can be applied to stack. */
const SUM_OPS = new Set([
'count',
'sum',
'distinct',
'valid',
'missing',
]);
/**
* Aggregation operators that always produce values within the range [domainMin, domainMax].
*/
const SHARED_DOMAIN_OPS = new Set([
'mean',
'average',
'median',
'q1',
'q3',
'min',
'max',
]);
/**
* Create a key for the bin configuration. Not for prebinned bin.
*/
function binToString(bin) {
if (isBoolean$1(bin)) {
bin = normalizeBin(bin, undefined);
}
return `bin${keys(bin)
.map((p) => (isParameterExtent(bin[p]) ? varName(`_${p}_${entries$1(bin[p])}`) : varName(`_${p}_${bin[p]}`)))
.join('')}`;
}
/**
* Vega-Lite should bin the data.
*/
function isBinning(bin) {
return bin === true || (isBinParams(bin) && !bin.binned);
}
/**
* The data is already binned and so Vega-Lite should not bin it again.
*/
function isBinned(bin) {
return bin === 'binned' || (isBinParams(bin) && bin.binned === true);
}
function isBinParams(bin) {
return isObject(bin);
}
function isParameterExtent(extent) {
return hasProperty(extent, 'param');
}
function autoMaxBins(channel) {
switch (channel) {
case ROW:
case COLUMN:
case SIZE:
case COLOR:
case FILL:
case STROKE:
case STROKEWIDTH:
case OPACITY:
case FILLOPACITY:
case STROKEOPACITY:
// Facets and Size shouldn't have too many bins
// We choose 6 like shape to simplify the rule [falls through]
case SHAPE:
return 6; // Vega's "shape" has 6 distinct values
case STROKEDASH:
return 4; // We only provide 5 different stroke dash values (but 4 is more effective)
default:
return 10;
}
}
function isExprRef(o) {
return hasProperty(o, 'expr');
}
function replaceExprRef(index, { level } = { level: 0 }) {
const props = keys(index || {});
const newIndex = {};
for (const prop of props) {
newIndex[prop] = level === 0 ? signalRefOrValue(index[prop]) : replaceExprRef(index[prop], { level: level - 1 });
}
return newIndex;
}
function extractTitleConfig(titleConfig) {
const {
// These are non-mark title config that need to be hardcoded
anchor, frame, offset, orient, angle, limit,
// color needs to be redirect to fill
color,
// subtitle properties
subtitleColor, subtitleFont, subtitleFontSize, subtitleFontStyle, subtitleFontWeight, subtitleLineHeight, subtitlePadding,
// The rest are mark config.
...rest } = titleConfig;
const titleMarkConfig = {
...rest,
...(color ? { fill: color } : {}),
};
// These are non-mark title config that need to be hardcoded
const nonMarkTitleProperties = {
...(anchor ? { anchor } : {}),
...(frame ? { frame } : {}),
...(offset ? { offset } : {}),
...(orient ? { orient } : {}),
...(angle !== undefined ? { angle } : {}),
...(limit !== undefined ? { limit } : {}),
};
// subtitle part can stay in config.title since header titles do not use subtitle
const subtitle = {
...(subtitleColor ? { subtitleColor } : {}),
...(subtitleFont ? { subtitleFont } : {}),
...(subtitleFontSize ? { subtitleFontSize } : {}),
...(subtitleFontStyle ? { subtitleFontStyle } : {}),
...(subtitleFontWeight ? { subtitleFontWeight } : {}),
...(subtitleLineHeight ? { subtitleLineHeight } : {}),
...(subtitlePadding ? { subtitlePadding } : {}),
};
const subtitleMarkConfig = pick(titleConfig, ['align', 'baseline', 'dx', 'dy', 'limit']);
return { titleMarkConfig, subtitleMarkConfig, nonMarkTitleProperties, subtitle };
}
function isText(v) {
return isString(v) || (isArray(v) && isString(v[0]));
}
function isSignalRef(o) {
return hasProperty(o, 'signal');
}
function isVgRangeStep(range) {
return hasProperty(range, 'step');
}
function isDataRefUnionedDomain(domain) {
if (!isArray(domain)) {
return hasProperty(domain, 'fields') && !hasProperty(domain, 'data');
}
return false;
}
function isFieldRefUnionDomain(domain) {
if (!isArray(domain)) {
return hasProperty(domain, 'fields') && hasProperty(domain, 'data');
}
return false;
}
function isDataRefDomain(domain) {
if (!isArray(domain)) {
return hasProperty(domain, 'field') && hasProperty(domain, 'data');
}
return false;
}
const VG_MARK_CONFIG_INDEX = {
aria: 1,
description: 1,
ariaRole: 1,
ariaRoleDescription: 1,
blend: 1,
opacity: 1,
fill: 1,
fillOpacity: 1,
stroke: 1,
strokeCap: 1,
strokeWidth: 1,
strokeOpacity: 1,
strokeDash: 1,
strokeDashOffset: 1,
strokeJoin: 1,
strokeOffset: 1,
strokeMiterLimit: 1,
startAngle: 1,
endAngle: 1,
padAngle: 1,
innerRadius: 1,
outerRadius: 1,
size: 1,
shape: 1,
interpolate: 1,
tension: 1,
orient: 1,
align: 1,
baseline: 1,
text: 1,
dir: 1,
dx: 1,
dy: 1,
ellipsis: 1,
limit: 1,
radius: 1,
theta: 1,
angle: 1,
font: 1,
fontSize: 1,
fontWeight: 1,
fontStyle: 1,
lineBreak: 1,
lineHeight: 1,
cursor: 1,
href: 1,
tooltip: 1,
cornerRadius: 1,
cornerRadiusTopLeft: 1,
cornerRadiusTopRight: 1,
cornerRadiusBottomLeft: 1,
cornerRadiusBottomRight: 1,
aspect: 1,
width: 1,
height: 1,
url: 1,
smooth: 1,
// commented below are vg channel that do not have mark config.
// x: 1,
// y: 1,
// x2: 1,
// y2: 1,
// xc'|'yc'
// clip: 1,
// path: 1,
// url: 1,
};
const VG_MARK_CONFIGS = keys(VG_MARK_CONFIG_INDEX);
const VG_MARK_INDEX = {
arc: 1,
area: 1,
group: 1,
image: 1,
line: 1,
path: 1,
rect: 1,
rule: 1,
shape: 1,
symbol: 1,
text: 1,
trail: 1,
};
// Vega's cornerRadius channels.
const VG_CORNERRADIUS_CHANNELS = [
'cornerRadius',
'cornerRadiusTopLeft',
'cornerRadiusTopRight',
'cornerRadiusBottomLeft',
'cornerRadiusBottomRight',
];
const BIN_RANGE_DELIMITER = ' \u2013 ';
function signalOrValueRefWithCondition(val) {
const condition = isArray(val.condition)
? val.condition.map(conditionalSignalRefOrValue)
: conditionalSignalRefOrValue(val.condition);
return {
...signalRefOrValue(val),
condition,
};
}
function signalRefOrValue(value) {
if (isExprRef(value)) {
const { expr, ...rest } = value;
return { signal: expr, ...rest };
}
return value;
}
function conditionalSignalRefOrValue(value) {
if (isExprRef(value)) {
const { expr, ...rest } = value;
return { signal: expr, ...rest };
}
return value;
}
function signalOrValueRef(value) {
if (isExprRef(value)) {
const { expr, ...rest } = value;
return { signal: expr, ...rest };
}
if (isSignalRef(value)) {
return value;
}
return value !== undefined ? { value } : undefined;
}
function exprFromSignalRefOrValue(ref) {
if (isSignalRef(ref)) {
return ref.signal;
}
return stringValue(ref);
}
function exprFromValueRefOrSignalRef(ref) {
if (isSignalRef(ref)) {
return ref.signal;
}
return stringValue(ref.value);
}
function signalOrStringValue(v) {
if (isSignalRef(v)) {
return v.signal;
}
return v == null ? null : stringValue(v);
}
function applyMarkConfig(e, model, propsList) {
for (const property of propsList) {
const value = getMarkConfig(property, model.markDef, model.config);
if (value !== undefined) {
e[property] = signalOrValueRef(value);
}
}
return e;
}
function getStyles(mark) {
return [].concat(mark.type, mark.style ?? []);
}
function getMarkPropOrConfig(channel, mark, config, opt = {}) {
const { vgChannel, ignoreVgConfig } = opt;
if (vgChannel && hasProperty(mark, vgChannel)) {
return mark[vgChannel];
}
else if (mark[channel] !== undefined) {
return mark[channel];
}
else if (ignoreVgConfig && (!vgChannel || vgChannel === channel)) {
return undefined;
}
return getMarkConfig(channel, mark, config, opt);
}
/**
* Return property value from style or mark specific config property if exists.
* Otherwise, return general mark specific config.
*/
function getMarkConfig(channel, mark, config, { vgChannel } = {}) {
const cfg = getMarkStyleConfig(channel, mark, config.style);
return getFirstDefined(
// style config has highest precedence
vgChannel ? cfg : undefined, cfg,
// then mark-specific config
vgChannel ? config[mark.type][vgChannel] : undefined, config[mark.type][channel], // Need to cast because MarkDef doesn't perfectly match with AnyMarkConfig, but if the type isn't available, we'll get nothing here, which is fine
// If there is vgChannel, skip vl channel.
// For example, vl size for text is vg fontSize, but config.mark.size is only for point size.
vgChannel ? config.mark[vgChannel] : config.mark[channel]);
}
function getMarkStyleConfig(prop, mark, styleConfigIndex) {
return getStyleConfig(prop, getStyles(mark), styleConfigIndex);
}
function getStyleConfig(p, styles, styleConfigIndex) {
styles = array(styles);
let value;
for (const style of styles) {
const styleConfig = styleConfigIndex[style];
if (hasProperty(styleConfig, p)) {
value = styleConfig[p];
}
}
return value;
}
/**
* Return Vega sort parameters (tuple of field and order).
*/
function sortParams(orderDef, fieldRefOption) {
return array(orderDef).reduce((s, orderChannelDef) => {
s.field.push(vgField(orderChannelDef, fieldRefOption));
s.order.push(orderChannelDef.sort ?? 'ascending');
return s;
}, { field: [], order: [] });
}
function mergeTitleFieldDefs(f1, f2) {
const merged = [...f1];
f2.forEach((fdToMerge) => {
for (const fieldDef1 of merged) {
// If already exists, no need to append to merged array
if (deepEqual(fieldDef1, fdToMerge)) {
return;
}
}
merged.push(fdToMerge);
});
return merged;
}
function mergeTitle(title1, title2) {
if (deepEqual(title1, title2) || !title2) {
// if titles are the same or title2 is falsy
return title1;
}
else if (!title1) {
// if title1 is falsy
return title2;
}
else {
return [...array(title1), ...array(title2)].join(', ');
}
}
function mergeTitleComponent(v1, v2) {
const v1Val = v1.value;
const v2Val = v2.value;
if (v1Val == null || v2Val === null) {
return {
explicit: v1.explicit,
value: null,
};
}
else if ((isText(v1Val) || isSignalRef(v1Val)) && (isText(v2Val) || isSignalRef(v2Val))) {
return {
explicit: v1.explicit,
value: mergeTitle(v1Val, v2Val),
};
}
else if (isText(v1Val) || isSignalRef(v1Val)) {
return {
explicit: v1.explicit,
value: v1Val,
};
}
else if (isText(v2Val) || isSignalRef(v2Val)) {
return {
explicit: v1.explicit,
value: v2Val,
};
}
else if (!isText(v1Val) && !isSignalRef(v1Val) && !isText(v2Val) && !isSignalRef(v2Val)) {
return {
explicit: v1.explicit,
value: mergeTitleFieldDefs(v1Val, v2Val),
};
}
/* istanbul ignore next: Condition should not happen -- only for warning in development. */
throw new Error('It should never reach here');
}
/**
* Collection of all Vega-Lite Error Messages
*/
function invalidSpec(spec) {
return `Invalid specification ${stringify(spec)}. Make sure the specification includes at least one of the following properties: "mark", "layer", "facet", "hconcat", "vconcat", "concat", or "repeat".`;
}
// FIT
const FIT_NON_SINGLE = 'Autosize "fit" only works for single views and layered views.';
function containerSizeNonSingle(name) {
const uName = name == 'width' ? 'Width' : 'Height';
return `${uName} "container" only works for single views and layered views.`;
}
function containerSizeNotCompatibleWithAutosize(name) {
const uName = name == 'width' ? 'Width' : 'Height';
const fitDirection = name == 'width' ? 'x' : 'y';
return `${uName} "container" only works well with autosize "fit" or "fit-${fitDirection}".`;
}
function droppingFit(channel) {
return channel
? `Dropping "fit-${channel}" because spec has discrete ${getSizeChannel(channel)}.`
: `Dropping "fit" because spec has discrete size.`;
}
// VIEW SIZE
function unknownField(channel) {
return `Unknown field for ${channel}. Cannot calculate view size.`;
}
// SELECTION
function cannotProjectOnChannelWithoutField(channel) {
return `Cannot project a selection on encoding channel "${channel}", which has no field.`;
}
function cannotProjectAggregate(channel, aggregate) {
return `Cannot project a selection on encoding channel "${channel}" as it uses an aggregate function ("${aggregate}").`;
}
function nearestNotSupportForContinuous(mark) {
return `The "nearest" transform is not supported for ${mark} marks.`;
}
function selectionNotSupported(mark) {
return `Selection not supported for ${mark} yet.`;
}
function selectionNotFound(name) {
return `Cannot find a selection named "${name}".`;
}
const SCALE_BINDINGS_CONTINUOUS = 'Scale bindings are currently only supported for scales with unbinned, continuous domains.';
const SEQUENTIAL_SCALE_DEPRECATED = 'Sequntial scales are deprecated. The available quantitative scale type values are linear, log, pow, sqrt, symlog, time and utc';
const LEGEND_BINDINGS_MUST_HAVE_PROJECTION = 'Legend bindings are only supported for selections over an individual field or encoding channel.';
function cannotLookupVariableParameter(name) {
return `Lookups can only be performed on selection parameters. "${name}" is a variable parameter.`;
}
function noSameUnitLookup(name) {
return (`Cannot define and lookup the "${name}" selection in the same view. ` +
`Try moving the lookup into a second, layered view?`);
}
const NEEDS_SAME_SELECTION = 'The same selection must be used to override scale domains in a layered view.';
const INTERVAL_INITIALIZED_WITH_POS = 'Interval selections should be initialized using "x", "y", "longitude", or "latitude" keys.';
// REPEAT
function noSuchRepeatedValue(field) {
return `Unknown repeated value "${field}".`;
}
function columnsNotSupportByRowCol(type) {
return `The "columns" property cannot be used when "${type}" has nested row/column.`;
}
const MULTIPLE_TIMER_ANIMATION_SELECTION = 'Multiple timer selections in one unit spec are not supported. Ignoring all but the first.';
const MULTI_VIEW_ANIMATION_UNSUPPORTED = 'Animation involving facet, layer, or concat is currently unsupported.';
function selectionAsScaleDomainWithoutField(field) {
return ('A "field" or "encoding" must be specified when using a selection as a scale domain. ' +
`Using "field": ${stringValue$1(field)}.`);
}
function selectionAsScaleDomainWrongEncodings(encodings, encoding, extent, field) {
return (`${!encodings.length ? 'No ' : 'Multiple '}matching ${stringValue$1(encoding)} encoding found for selection ${stringValue$1(extent.param)}. ` +
`Using "field": ${stringValue$1(field)}.`);
}
// CONCAT / REPEAT
const CONCAT_CANNOT_SHARE_AXIS = 'Axes cannot be shared in concatenated or repeated views yet (https://github.com/vega/vega-lite/issues/2415).';
// DATA
function unrecognizedParse(p) {
return `Unrecognized parse "${p}".`;
}
function differentParse(field, local, ancestor) {
return `An ancestor parsed field "${field}" as ${ancestor} but a child wants to parse the field as ${local}.`;
}
const ADD_SAME_CHILD_TWICE = 'Attempt to add the same child twice.';
// TRANSFORMS
function invalidTransformIgnored(transform) {
return `Ignoring an invalid transform: ${stringify(transform)}.`;
}
const NO_FIELDS_NEEDS_AS = 'If "from.fields" is not specified, "as" has to be a string that specifies the key to be used for the data from the secondary source.';
// ENCODING & FACET
function customFormatTypeNotAllowed(channel) {
return `Config.customFormatTypes is not true, thus custom format type and format for channel ${channel} are dropped.`;
}
function projectionOverridden(opt) {
const { parentProjection, projection } = opt;
return `Layer's shared projection ${stringify(parentProjection)} is overridden by a child projection ${stringify(projection)}.`;
}
const REPLACE_ANGLE_WITH_THETA = 'Arc marks uses theta channel rather than angle, replacing angle with theta.';
function offsetNestedInsideContinuousPositionScaleDropped(mainChannel) {
return `${mainChannel}Offset dropped because ${mainChannel} is continuous`;
}
function primitiveChannelDef(channel, type, value) {
return `Channel ${channel} is a ${type}. Converted to {value: ${stringify(value)}}.`;
}
function invalidFieldType(type) {
return `Invalid field type "${type}".`;
}
function invalidFieldTypeForCountAggregate(type, aggregate) {
return `Invalid field type "${type}" for aggregate: "${aggregate}", using "quantitative" instead.`;
}
function invalidAggregate(aggregate) {
return `Invalid aggregation operator "${aggregate}".`;
}
function droppingColor(type, opt) {
const { fill, stroke } = opt;
return `Dropping color ${type} as the plot also has ${fill && stroke ? 'fill and stroke' : fill ? 'fill' : 'stroke'}.`;
}
function relativeBandSizeNotSupported(sizeChannel) {
return `Position range does not support relative band size for ${sizeChannel}.`;
}
function emptyFieldDef(fieldDef, channel) {
return `Dropping ${stringify(fieldDef)} from channel "${channel}" since it does not contain any data field, datum, value, or signal.`;
}
const LINE_WITH_VARYING_SIZE = 'Line marks cannot encode size with a non-groupby field. You may want to use trail marks instead.';
function incompatibleChannel(channel, markOrFacet, when) {
return `${channel} dropped as it is incompatible with "${markOrFacet}"${''}.`;
}
function invalidEncodingChannel(channel) {
return `${channel}-encoding is dropped as ${channel} is not a valid encoding channel.`;
}
function channelShouldBeDiscrete(channel) {
return `${channel} encoding should be discrete (ordinal / nominal / binned).`;
}
function channelShouldBeDiscreteOrDiscretizing(channel) {
return `${channel} encoding should be discrete (ordinal / nominal / binned) or use a discretizing scale (e.g. threshold).`;
}
function facetChannelDropped(channels) {
return `Facet encoding dropped as ${channels.join(' and ')} ${channels.length > 1 ? 'are' : 'is'} also specified.`;
}
function discreteChannelCannotEncode(channel, type) {
return `Using discrete channel "${channel}" to encode "${type}" field can be misleading as it does not encode ${type === 'ordinal' ? 'order' : 'magnitude'}.`;
}
// MARK
function rangeMarkAlignmentCannotBeExpression(align) {
return `The ${align} for range marks cannot be an expression`;
}
function lineWithRange(hasX2, hasY2) {
const channels = hasX2 && hasY2 ? 'x2 and y2' : hasX2 ? 'x2' : 'y2';
return `Line mark is for continuous lines and thus cannot be used with ${channels}. We will use the rule mark (line segments) instead.`;
}
function orientOverridden(original, actual