UNPKG

vega-lite

Version:

Vega-Lite is a concise high-level language for interactive visualization.

1,663 lines (1,652 loc) 742 kB
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