vega-parser
Version:
Parse Vega specifications to runtime dataflows.
1,807 lines (1,711 loc) • 104 kB
JavaScript
import { isObject, extend, hasOwnProperty, isArray, array, stringValue, peek, isString, error, splitAccessPath, mergeConfig } from 'vega-util';
import { parseExpression } from 'vega-functions';
import { parseSelector } from 'vega-event-selector';
import { isValidScaleType, isDiscrete, isQuantile, isContinuous, isDiscretizing } from 'vega-scale';
import { definition as definition$1 } from 'vega-dataflow';
function parseAutosize (spec) {
return isObject(spec) ? spec : {
type: spec || 'pad'
};
}
const number = _ => +_ || 0;
const paddingObject = _ => ({
top: _,
bottom: _,
left: _,
right: _
});
function parsePadding (spec) {
return !isObject(spec) ? paddingObject(number(spec)) : spec.signal ? spec : {
top: number(spec.top),
bottom: number(spec.bottom),
left: number(spec.left),
right: number(spec.right)
};
}
const encoder = _ => isObject(_) && !isArray(_) ? extend({}, _) : {
value: _
};
function addEncode(object, name, value, set) {
if (value != null) {
const isEncoder = isObject(value) && !isArray(value) || isArray(value) && value.length && isObject(value[0]);
// Always assign signal to update, even if the signal is from the enter block
if (isEncoder) {
object.update[name] = value;
} else {
object[set || 'enter'][name] = {
value: value
};
}
return 1;
} else {
return 0;
}
}
function addEncoders(object, enter, update) {
for (const name in enter) {
addEncode(object, name, enter[name]);
}
for (const name in update) {
addEncode(object, name, update[name], 'update');
}
}
function extendEncode(encode, extra, skip) {
for (const name in extra) {
if (skip && hasOwnProperty(skip, name)) continue;
encode[name] = extend(encode[name] || {}, extra[name]);
}
return encode;
}
function has(key, encode) {
return encode && (encode.enter && encode.enter[key] || encode.update && encode.update[key]);
}
const MarkRole = 'mark';
const FrameRole = 'frame';
const ScopeRole = 'scope';
const AxisRole = 'axis';
const AxisDomainRole = 'axis-domain';
const AxisGridRole = 'axis-grid';
const AxisLabelRole = 'axis-label';
const AxisTickRole = 'axis-tick';
const AxisTitleRole = 'axis-title';
const LegendRole = 'legend';
const LegendBandRole = 'legend-band';
const LegendEntryRole = 'legend-entry';
const LegendGradientRole = 'legend-gradient';
const LegendLabelRole = 'legend-label';
const LegendSymbolRole = 'legend-symbol';
const LegendTitleRole = 'legend-title';
const TitleRole = 'title';
const TitleTextRole = 'title-text';
const TitleSubtitleRole = 'title-subtitle';
function applyDefaults (encode, type, role, style, config) {
const defaults = {},
enter = {};
let update, key, skip, props;
// if text mark, apply global lineBreak settings (#2370)
key = 'lineBreak';
if (type === 'text' && config[key] != null && !has(key, encode)) {
applyDefault(defaults, key, config[key]);
}
// ignore legend and axis roles
if (role == 'legend' || String(role).startsWith('axis')) {
role = null;
}
// resolve mark config
props = role === FrameRole ? config.group : role === MarkRole ? extend({}, config.mark, config[type]) : null;
for (key in props) {
// do not apply defaults if relevant fields are defined
skip = has(key, encode) || (key === 'fill' || key === 'stroke') && (has('fill', encode) || has('stroke', encode));
if (!skip) applyDefault(defaults, key, props[key]);
}
// resolve styles, apply with increasing precedence
array(style).forEach(name => {
const props = config.style && config.style[name];
for (const key in props) {
if (!has(key, encode)) {
applyDefault(defaults, key, props[key]);
}
}
});
encode = extend({}, encode); // defensive copy
for (key in defaults) {
props = defaults[key];
if (props.signal) {
(update = update || {})[key] = props;
} else {
enter[key] = props;
}
}
encode.enter = extend(enter, encode.enter);
if (update) encode.update = extend(update, encode.update);
return encode;
}
function applyDefault(defaults, key, value) {
defaults[key] = value && value.signal ? {
signal: value.signal
} : {
value: value
};
}
const scaleRef = scale => isString(scale) ? stringValue(scale) : scale.signal ? `(${scale.signal})` : field(scale);
function entry$1(enc) {
if (enc.gradient != null) {
return gradient(enc);
}
let value = enc.signal ? `(${enc.signal})` : enc.color ? color(enc.color) : enc.field != null ? field(enc.field) : enc.value !== undefined ? stringValue(enc.value) : undefined;
if (enc.scale != null) {
value = scale(enc, value);
}
if (value === undefined) {
value = null;
}
if (enc.exponent != null) {
value = `pow(${value},${property(enc.exponent)})`;
}
if (enc.mult != null) {
value += `*${property(enc.mult)}`;
}
if (enc.offset != null) {
value += `+${property(enc.offset)}`;
}
if (enc.round) {
value = `round(${value})`;
}
return value;
}
const _color = (type, x, y, z) => `(${type}(${[x, y, z].map(entry$1).join(',')})+'')`;
function color(enc) {
return enc.c ? _color('hcl', enc.h, enc.c, enc.l) : enc.h || enc.s ? _color('hsl', enc.h, enc.s, enc.l) : enc.l || enc.a ? _color('lab', enc.l, enc.a, enc.b) : enc.r || enc.g || enc.b ? _color('rgb', enc.r, enc.g, enc.b) : null;
}
function gradient(enc) {
// map undefined to null; expression lang does not allow undefined
const args = [enc.start, enc.stop, enc.count].map(_ => _ == null ? null : stringValue(_));
// trim null inputs from the end
while (args.length && peek(args) == null) args.pop();
args.unshift(scaleRef(enc.gradient));
return `gradient(${args.join(',')})`;
}
function property(property) {
return isObject(property) ? '(' + entry$1(property) + ')' : property;
}
function field(ref) {
return resolveField(isObject(ref) ? ref : {
datum: ref
});
}
function resolveField(ref) {
let object, level, field;
if (ref.signal) {
object = 'datum';
field = ref.signal;
} else if (ref.group || ref.parent) {
level = Math.max(1, ref.level || 1);
object = 'item';
while (level-- > 0) {
object += '.mark.group';
}
if (ref.parent) {
field = ref.parent;
object += '.datum';
} else {
field = ref.group;
}
} else if (ref.datum) {
object = 'datum';
field = ref.datum;
} else {
error('Invalid field reference: ' + stringValue(ref));
}
if (!ref.signal) {
field = isString(field) ? splitAccessPath(field).map(stringValue).join('][') : resolveField(field);
}
return object + '[' + field + ']';
}
function scale(enc, value) {
const scale = scaleRef(enc.scale);
if (enc.range != null) {
// pull value from scale range
value = `lerp(_range(${scale}), ${+enc.range})`;
} else {
// run value through scale and/or pull scale bandwidth
if (value !== undefined) value = `_scale(${scale}, ${value})`;
if (enc.band) {
value = (value ? value + '+' : '') + `_bandwidth(${scale})` + (+enc.band === 1 ? '' : '*' + property(enc.band));
if (enc.extra) {
// include logic to handle extraneous elements
value = `(datum.extra ? _scale(${scale}, datum.extra.value) : ${value})`;
}
}
if (value == null) value = '0';
}
return value;
}
function rule (enc) {
let code = '';
enc.forEach(rule => {
const value = entry$1(rule);
code += rule.test ? `(${rule.test})?${value}:` : value;
});
// if no else clause, terminate with null (#1366)
if (peek(code) === ':') {
code += 'null';
}
return code;
}
function parseEncode (encode, type, role, style, scope, params) {
const enc = {};
params = params || {};
params.encoders = {
$encode: enc
};
encode = applyDefaults(encode, type, role, style, scope.config);
for (const key in encode) {
enc[key] = parseBlock(encode[key], type, params, scope);
}
return params;
}
function parseBlock(block, marktype, params, scope) {
const channels = {},
fields = {};
for (const name in block) {
if (block[name] != null) {
// skip any null entries
channels[name] = parse$1(expr(block[name]), scope, params, fields);
}
}
return {
$expr: {
marktype,
channels
},
$fields: Object.keys(fields),
$output: Object.keys(block)
};
}
function expr(enc) {
return isArray(enc) ? rule(enc) : entry$1(enc);
}
function parse$1(code, scope, params, fields) {
const expr = parseExpression(code, scope);
expr.$fields.forEach(name => fields[name] = 1);
extend(params, expr.$params);
return expr.$expr;
}
const OUTER = 'outer',
OUTER_INVALID = ['value', 'update', 'init', 'react', 'bind'];
function outerError(prefix, name) {
error(prefix + ' for "outer" push: ' + stringValue(name));
}
function parseSignal (signal, scope) {
const name = signal.name;
if (signal.push === OUTER) {
// signal must already be defined, raise error if not
if (!scope.signals[name]) outerError('No prior signal definition', name);
// signal push must not use properties reserved for standard definition
OUTER_INVALID.forEach(prop => {
if (signal[prop] !== undefined) outerError('Invalid property ', prop);
});
} else {
// define a new signal in the current scope
const op = scope.addSignal(name, signal.value);
if (signal.react === false) op.react = false;
if (signal.bind) scope.addBinding(name, signal.bind);
}
}
function Entry(type, value, params, parent) {
this.id = -1;
this.type = type;
this.value = value;
this.params = params;
if (parent) this.parent = parent;
}
function entry(type, value, params, parent) {
return new Entry(type, value, params, parent);
}
function operator(value, params) {
return entry('operator', value, params);
}
// -----
function ref(op) {
const ref = {
$ref: op.id
};
// if operator not yet registered, cache ref to resolve later
if (op.id < 0) (op.refs = op.refs || []).push(ref);
return ref;
}
function fieldRef$1(field, name) {
return name ? {
$field: field,
$name: name
} : {
$field: field
};
}
const keyFieldRef = fieldRef$1('key');
function compareRef(fields, orders) {
return {
$compare: fields,
$order: orders
};
}
function keyRef(fields, flat) {
const ref = {
$key: fields
};
if (flat) ref.$flat = true;
return ref;
}
// -----
const Ascending = 'ascending';
const Descending = 'descending';
function sortKey(sort) {
return !isObject(sort) ? '' : (sort.order === Descending ? '-' : '+') + aggrField(sort.op, sort.field);
}
function aggrField(op, field) {
return (op && op.signal ? '$' + op.signal : op || '') + (op && field ? '_' : '') + (field && field.signal ? '$' + field.signal : field || '');
}
// -----
const Scope$1 = 'scope';
const View = 'view';
function isSignal(_) {
return _ && _.signal;
}
function isExpr$1(_) {
return _ && _.expr;
}
function hasSignal(_) {
if (isSignal(_)) return true;
if (isObject(_)) for (const key in _) {
if (hasSignal(_[key])) return true;
}
return false;
}
function value(specValue, defaultValue) {
return specValue != null ? specValue : defaultValue;
}
function deref(v) {
return v && v.signal || v;
}
const Timer = 'timer';
function parseStream(stream, scope) {
const method = stream.merge ? mergeStream : stream.stream ? nestedStream : stream.type ? eventStream : error('Invalid stream specification: ' + stringValue(stream));
return method(stream, scope);
}
function eventSource(source) {
return source === Scope$1 ? View : source || View;
}
function mergeStream(stream, scope) {
const list = stream.merge.map(s => parseStream(s, scope)),
entry = streamParameters({
merge: list
}, stream, scope);
return scope.addStream(entry).id;
}
function nestedStream(stream, scope) {
const id = parseStream(stream.stream, scope),
entry = streamParameters({
stream: id
}, stream, scope);
return scope.addStream(entry).id;
}
function eventStream(stream, scope) {
let id;
if (stream.type === Timer) {
id = scope.event(Timer, stream.throttle);
stream = {
between: stream.between,
filter: stream.filter
};
} else {
id = scope.event(eventSource(stream.source), stream.type);
}
const entry = streamParameters({
stream: id
}, stream, scope);
return Object.keys(entry).length === 1 ? id : scope.addStream(entry).id;
}
function streamParameters(entry, stream, scope) {
let param = stream.between;
if (param) {
if (param.length !== 2) {
error('Stream "between" parameter must have 2 entries: ' + stringValue(stream));
}
entry.between = [parseStream(param[0], scope), parseStream(param[1], scope)];
}
param = stream.filter ? [].concat(stream.filter) : [];
if (stream.marktype || stream.markname || stream.markrole) {
// add filter for mark type, name and/or role
param.push(filterMark(stream.marktype, stream.markname, stream.markrole));
}
if (stream.source === Scope$1) {
// add filter to limit events from sub-scope only
param.push('inScope(event.item)');
}
if (param.length) {
entry.filter = parseExpression('(' + param.join(')&&(') + ')', scope).$expr;
}
if ((param = stream.throttle) != null) {
entry.throttle = +param;
}
if ((param = stream.debounce) != null) {
entry.debounce = +param;
}
if (stream.consume) {
entry.consume = true;
}
return entry;
}
function filterMark(type, name, role) {
const item = 'event.item';
return item + (type && type !== '*' ? '&&' + item + '.mark.marktype===\'' + type + '\'' : '') + (role ? '&&' + item + '.mark.role===\'' + role + '\'' : '') + (name ? '&&' + item + '.mark.name===\'' + name + '\'' : '');
}
// bypass expression parser for internal operator references
const OP_VALUE_EXPR = {
code: '_.$value',
ast: {
type: 'Identifier',
value: 'value'
}
};
function parseUpdate (spec, scope, target) {
const encode = spec.encode,
entry = {
target: target
};
let events = spec.events,
update = spec.update,
sources = [];
if (!events) {
error('Signal update missing events specification.');
}
// interpret as an event selector string
if (isString(events)) {
events = parseSelector(events, scope.isSubscope() ? Scope$1 : View);
}
// separate event streams from signal updates
events = array(events).filter(s => s.signal || s.scale ? (sources.push(s), 0) : 1);
// merge internal operator listeners
if (sources.length > 1) {
sources = [mergeSources(sources)];
}
// merge event streams, include as source
if (events.length) {
sources.push(events.length > 1 ? {
merge: events
} : events[0]);
}
if (encode != null) {
if (update) error('Signal encode and update are mutually exclusive.');
update = 'encode(item(),' + stringValue(encode) + ')';
}
// resolve update value
entry.update = isString(update) ? parseExpression(update, scope) : update.expr != null ? parseExpression(update.expr, scope) : update.value != null ? update.value : update.signal != null ? {
$expr: OP_VALUE_EXPR,
$params: {
$value: scope.signalRef(update.signal)
}
} : error('Invalid signal update specification.');
if (spec.force) {
entry.options = {
force: true
};
}
sources.forEach(source => scope.addUpdate(extend(streamSource(source, scope), entry)));
}
function streamSource(stream, scope) {
return {
source: stream.signal ? scope.signalRef(stream.signal) : stream.scale ? scope.scaleRef(stream.scale) : parseStream(stream, scope)
};
}
function mergeSources(sources) {
return {
signal: '[' + sources.map(s => s.scale ? 'scale("' + s.scale + '")' : s.signal) + ']'
};
}
function parseSignalUpdates (signal, scope) {
const op = scope.getSignal(signal.name);
let expr = signal.update;
if (signal.init) {
if (expr) {
error('Signals can not include both init and update expressions.');
} else {
expr = signal.init;
op.initonly = true;
}
}
if (expr) {
expr = parseExpression(expr, scope);
op.update = expr.$expr;
op.params = expr.$params;
}
if (signal.on) {
signal.on.forEach(_ => parseUpdate(_, scope, op.id));
}
}
const transform = name => (params, value, parent) => entry(name, value, params || undefined, parent);
const Aggregate = transform('aggregate');
const AxisTicks = transform('axisticks');
const Bound = transform('bound');
const Collect = transform('collect');
const Compare = transform('compare');
const DataJoin = transform('datajoin');
const Encode = transform('encode');
const Expression = transform('expression');
const Facet = transform('facet');
const Field = transform('field');
const Key = transform('key');
const LegendEntries = transform('legendentries');
const Load = transform('load');
const Mark = transform('mark');
const MultiExtent = transform('multiextent');
const MultiValues = transform('multivalues');
const Overlap = transform('overlap');
const Params = transform('params');
const PreFacet = transform('prefacet');
const Projection = transform('projection');
const Proxy = transform('proxy');
const Relay = transform('relay');
const Render = transform('render');
const Scale = transform('scale');
const Sieve = transform('sieve');
const SortItems = transform('sortitems');
const ViewLayout = transform('viewlayout');
const Values = transform('values');
let FIELD_REF_ID = 0;
const MULTIDOMAIN_SORT_OPS = {
min: 'min',
max: 'max',
count: 'sum'
};
function initScale(spec, scope) {
const type = spec.type || 'linear';
if (!isValidScaleType(type)) {
error('Unrecognized scale type: ' + stringValue(type));
}
scope.addScale(spec.name, {
type,
domain: undefined
});
}
function parseScale(spec, scope) {
const params = scope.getScale(spec.name).params;
let key;
params.domain = parseScaleDomain(spec.domain, spec, scope);
if (spec.range != null) {
params.range = parseScaleRange(spec, scope, params);
}
if (spec.interpolate != null) {
parseScaleInterpolate(spec.interpolate, params);
}
if (spec.nice != null) {
params.nice = parseScaleNice(spec.nice, scope);
}
if (spec.bins != null) {
params.bins = parseScaleBins(spec.bins, scope);
}
for (key in spec) {
if (hasOwnProperty(params, key) || key === 'name') continue;
params[key] = parseLiteral(spec[key], scope);
}
}
function parseLiteral(v, scope) {
return !isObject(v) ? v : v.signal ? scope.signalRef(v.signal) : error('Unsupported object: ' + stringValue(v));
}
function parseArray(v, scope) {
return v.signal ? scope.signalRef(v.signal) : v.map(v => parseLiteral(v, scope));
}
function dataLookupError(name) {
error('Can not find data set: ' + stringValue(name));
}
// -- SCALE DOMAIN ----
function parseScaleDomain(domain, spec, scope) {
if (!domain) {
if (spec.domainMin != null || spec.domainMax != null) {
error('No scale domain defined for domainMin/domainMax to override.');
}
return; // default domain
}
return domain.signal ? scope.signalRef(domain.signal) : (isArray(domain) ? explicitDomain : domain.fields ? multipleDomain : singularDomain)(domain, spec, scope);
}
function explicitDomain(domain, spec, scope) {
return domain.map(v => parseLiteral(v, scope));
}
function singularDomain(domain, spec, scope) {
const data = scope.getData(domain.data);
if (!data) dataLookupError(domain.data);
return isDiscrete(spec.type) ? data.valuesRef(scope, domain.field, parseSort(domain.sort, false)) : isQuantile(spec.type) ? data.domainRef(scope, domain.field) : data.extentRef(scope, domain.field);
}
function multipleDomain(domain, spec, scope) {
const data = domain.data,
fields = domain.fields.reduce((dom, d) => {
d = isString(d) ? {
data: data,
field: d
} : isArray(d) || d.signal ? fieldRef(d, scope) : d;
dom.push(d);
return dom;
}, []);
return (isDiscrete(spec.type) ? ordinalMultipleDomain : isQuantile(spec.type) ? quantileMultipleDomain : numericMultipleDomain)(domain, scope, fields);
}
function fieldRef(data, scope) {
const name = '_:vega:_' + FIELD_REF_ID++,
coll = Collect({});
if (isArray(data)) {
coll.value = {
$ingest: data
};
} else if (data.signal) {
const code = 'setdata(' + stringValue(name) + ',' + data.signal + ')';
coll.params.input = scope.signalRef(code);
}
scope.addDataPipeline(name, [coll, Sieve({})]);
return {
data: name,
field: 'data'
};
}
function ordinalMultipleDomain(domain, scope, fields) {
const sort = parseSort(domain.sort, true);
let a, v;
// get value counts for each domain field
const counts = fields.map(f => {
const data = scope.getData(f.data);
if (!data) dataLookupError(f.data);
return data.countsRef(scope, f.field, sort);
});
// aggregate the results from each domain field
const p = {
groupby: keyFieldRef,
pulse: counts
};
if (sort) {
a = sort.op || 'count';
v = sort.field ? aggrField(a, sort.field) : 'count';
p.ops = [MULTIDOMAIN_SORT_OPS[a]];
p.fields = [scope.fieldRef(v)];
p.as = [v];
}
a = scope.add(Aggregate(p));
// collect aggregate output
const c = scope.add(Collect({
pulse: ref(a)
}));
// extract values for combined domain
v = scope.add(Values({
field: keyFieldRef,
sort: scope.sortRef(sort),
pulse: ref(c)
}));
return ref(v);
}
function parseSort(sort, multidomain) {
if (sort) {
if (!sort.field && !sort.op) {
if (isObject(sort)) sort.field = 'key';else sort = {
field: 'key'
};
} else if (!sort.field && sort.op !== 'count') {
error('No field provided for sort aggregate op: ' + sort.op);
} else if (multidomain && sort.field) {
if (sort.op && !MULTIDOMAIN_SORT_OPS[sort.op]) {
error('Multiple domain scales can not be sorted using ' + sort.op);
}
}
}
return sort;
}
function quantileMultipleDomain(domain, scope, fields) {
// get value arrays for each domain field
const values = fields.map(f => {
const data = scope.getData(f.data);
if (!data) dataLookupError(f.data);
return data.domainRef(scope, f.field);
});
// combine value arrays
return ref(scope.add(MultiValues({
values: values
})));
}
function numericMultipleDomain(domain, scope, fields) {
// get extents for each domain field
const extents = fields.map(f => {
const data = scope.getData(f.data);
if (!data) dataLookupError(f.data);
return data.extentRef(scope, f.field);
});
// combine extents
return ref(scope.add(MultiExtent({
extents: extents
})));
}
// -- SCALE BINS -----
function parseScaleBins(v, scope) {
return v.signal || isArray(v) ? parseArray(v, scope) : scope.objectProperty(v);
}
// -- SCALE NICE -----
function parseScaleNice(nice, scope) {
return nice.signal ? scope.signalRef(nice.signal) : isObject(nice) ? {
interval: parseLiteral(nice.interval),
step: parseLiteral(nice.step)
} : parseLiteral(nice);
}
// -- SCALE INTERPOLATION -----
function parseScaleInterpolate(interpolate, params) {
params.interpolate = parseLiteral(interpolate.type || interpolate);
if (interpolate.gamma != null) {
params.interpolateGamma = parseLiteral(interpolate.gamma);
}
}
// -- SCALE RANGE -----
function parseScaleRange(spec, scope, params) {
const config = scope.config.range;
let range = spec.range;
if (range.signal) {
return scope.signalRef(range.signal);
} else if (isString(range)) {
if (config && hasOwnProperty(config, range)) {
spec = extend({}, spec, {
range: config[range]
});
return parseScaleRange(spec, scope, params);
} else if (range === 'width') {
range = [0, {
signal: 'width'
}];
} else if (range === 'height') {
range = isDiscrete(spec.type) ? [0, {
signal: 'height'
}] : [{
signal: 'height'
}, 0];
} else {
error('Unrecognized scale range value: ' + stringValue(range));
}
} else if (range.scheme) {
params.scheme = isArray(range.scheme) ? parseArray(range.scheme, scope) : parseLiteral(range.scheme, scope);
if (range.extent) params.schemeExtent = parseArray(range.extent, scope);
if (range.count) params.schemeCount = parseLiteral(range.count, scope);
return;
} else if (range.step) {
params.rangeStep = parseLiteral(range.step, scope);
return;
} else if (isDiscrete(spec.type) && !isArray(range)) {
return parseScaleDomain(range, spec, scope);
} else if (!isArray(range)) {
error('Unsupported range type: ' + stringValue(range));
}
return range.map(v => (isArray(v) ? parseArray : parseLiteral)(v, scope));
}
function parseProjection (proj, scope) {
const config = scope.config.projection || {},
params = {};
for (const name in proj) {
if (name === 'name') continue;
params[name] = parseParameter$1(proj[name], name, scope);
}
// apply projection defaults from config
for (const name in config) {
if (params[name] == null) {
params[name] = parseParameter$1(config[name], name, scope);
}
}
scope.addProjection(proj.name, params);
}
function parseParameter$1(_, name, scope) {
return isArray(_) ? _.map(_ => parseParameter$1(_, name, scope)) : !isObject(_) ? _ : _.signal ? scope.signalRef(_.signal) : name === 'fit' ? _ : error('Unsupported parameter object: ' + stringValue(_));
}
const Top = 'top';
const Left = 'left';
const Right = 'right';
const Bottom = 'bottom';
const Center = 'center';
const Vertical = 'vertical';
const Start = 'start';
const Middle = 'middle';
const End = 'end';
const Index = 'index';
const Label = 'label';
const Offset = 'offset';
const Perc = 'perc';
const Perc2 = 'perc2';
const Value = 'value';
const GuideLabelStyle = 'guide-label';
const GuideTitleStyle = 'guide-title';
const GroupTitleStyle = 'group-title';
const GroupSubtitleStyle = 'group-subtitle';
/** All values of LegendType */
const Symbols = 'symbol';
const Gradient = 'gradient';
const Discrete = 'discrete';
const Size = 'size';
const Shape = 'shape';
const Fill = 'fill';
const Stroke = 'stroke';
const StrokeWidth = 'strokeWidth';
const StrokeDash = 'strokeDash';
const Opacity = 'opacity';
// Encoding channels supported by legends
// In priority order of 'canonical' scale
const LegendScales = [Size, Shape, Fill, Stroke, StrokeWidth, StrokeDash, Opacity];
const Skip = {
name: 1,
style: 1,
interactive: 1
};
const zero = {
value: 0
};
const one = {
value: 1
};
const GroupMark = 'group';
const RectMark = 'rect';
const RuleMark = 'rule';
const SymbolMark = 'symbol';
const TextMark = 'text';
function guideGroup (mark) {
mark.type = GroupMark;
mark.interactive = mark.interactive || false;
return mark;
}
function lookup(spec, config) {
const _ = (name, dflt) => value(spec[name], value(config[name], dflt));
_.isVertical = s => Vertical === value(spec.direction, config.direction || (s ? config.symbolDirection : config.gradientDirection));
_.gradientLength = () => value(spec.gradientLength, config.gradientLength || config.gradientWidth);
_.gradientThickness = () => value(spec.gradientThickness, config.gradientThickness || config.gradientHeight);
_.entryColumns = () => value(spec.columns, value(config.columns, +_.isVertical(true)));
return _;
}
function getEncoding(name, encode) {
const v = encode && (encode.update && encode.update[name] || encode.enter && encode.enter[name]);
return v && v.signal ? v : v ? v.value : null;
}
function getStyle(name, scope, style) {
const s = scope.config.style[style];
return s && s[name];
}
function anchorExpr(s, e, m) {
return `item.anchor === '${Start}' ? ${s} : item.anchor === '${End}' ? ${e} : ${m}`;
}
const alignExpr$1 = anchorExpr(stringValue(Left), stringValue(Right), stringValue(Center));
function tickBand(_) {
const v = _('tickBand');
let offset = _('tickOffset'),
band,
extra;
if (!v) {
// if no tick band entry, fall back on other properties
band = _('bandPosition');
extra = _('tickExtra');
} else if (v.signal) {
// if signal, augment code to interpret values
band = {
signal: `(${v.signal}) === 'extent' ? 1 : 0.5`
};
extra = {
signal: `(${v.signal}) === 'extent'`
};
if (!isObject(offset)) {
offset = {
signal: `(${v.signal}) === 'extent' ? 0 : ${offset}`
};
}
} else if (v === 'extent') {
// if constant, simply set values
band = 1;
extra = true;
offset = 0;
} else {
band = 0.5;
extra = false;
}
return {
extra,
band,
offset
};
}
function extendOffset(value, offset) {
return !offset ? value : !value ? offset : !isObject(value) ? {
value,
offset
} : Object.assign({}, value, {
offset: extendOffset(value.offset, offset)
});
}
function guideMark (mark, extras) {
if (extras) {
mark.name = extras.name;
mark.style = extras.style || mark.style;
mark.interactive = !!extras.interactive;
mark.encode = extendEncode(mark.encode, extras, Skip);
} else {
mark.interactive = false;
}
return mark;
}
function legendGradient (spec, scale, config, userEncode) {
const _ = lookup(spec, config),
vertical = _.isVertical(),
thickness = _.gradientThickness(),
length = _.gradientLength();
let enter, start, stop, width, height;
if (vertical) {
start = [0, 1];
stop = [0, 0];
width = thickness;
height = length;
} else {
start = [0, 0];
stop = [1, 0];
width = length;
height = thickness;
}
const encode = {
enter: enter = {
opacity: zero,
x: zero,
y: zero,
width: encoder(width),
height: encoder(height)
},
update: extend({}, enter, {
opacity: one,
fill: {
gradient: scale,
start: start,
stop: stop
}
}),
exit: {
opacity: zero
}
};
addEncoders(encode, {
stroke: _('gradientStrokeColor'),
strokeWidth: _('gradientStrokeWidth')
}, {
// update
opacity: _('gradientOpacity')
});
return guideMark({
type: RectMark,
role: LegendGradientRole,
encode
}, userEncode);
}
function legendGradientDiscrete (spec, scale, config, userEncode, dataRef) {
const _ = lookup(spec, config),
vertical = _.isVertical(),
thickness = _.gradientThickness(),
length = _.gradientLength();
let u,
v,
uu,
vv,
adjust = '';
vertical ? (u = 'y', uu = 'y2', v = 'x', vv = 'width', adjust = '1-') : (u = 'x', uu = 'x2', v = 'y', vv = 'height');
const enter = {
opacity: zero,
fill: {
scale: scale,
field: Value
}
};
enter[u] = {
signal: adjust + 'datum.' + Perc,
mult: length
};
enter[v] = zero;
enter[uu] = {
signal: adjust + 'datum.' + Perc2,
mult: length
};
enter[vv] = encoder(thickness);
const encode = {
enter: enter,
update: extend({}, enter, {
opacity: one
}),
exit: {
opacity: zero
}
};
addEncoders(encode, {
stroke: _('gradientStrokeColor'),
strokeWidth: _('gradientStrokeWidth')
}, {
// update
opacity: _('gradientOpacity')
});
return guideMark({
type: RectMark,
role: LegendBandRole,
key: Value,
from: dataRef,
encode
}, userEncode);
}
const alignExpr = `datum.${Perc}<=0?"${Left}":datum.${Perc}>=1?"${Right}":"${Center}"`,
baselineExpr = `datum.${Perc}<=0?"${Bottom}":datum.${Perc}>=1?"${Top}":"${Middle}"`;
function legendGradientLabels (spec, config, userEncode, dataRef) {
const _ = lookup(spec, config),
vertical = _.isVertical(),
thickness = encoder(_.gradientThickness()),
length = _.gradientLength();
let overlap = _('labelOverlap'),
enter,
update,
u,
v,
adjust = '';
const encode = {
enter: enter = {
opacity: zero
},
update: update = {
opacity: one,
text: {
field: Label
}
},
exit: {
opacity: zero
}
};
addEncoders(encode, {
fill: _('labelColor'),
fillOpacity: _('labelOpacity'),
font: _('labelFont'),
fontSize: _('labelFontSize'),
fontStyle: _('labelFontStyle'),
fontWeight: _('labelFontWeight'),
limit: value(spec.labelLimit, config.gradientLabelLimit)
});
if (vertical) {
enter.align = {
value: 'left'
};
enter.baseline = update.baseline = {
signal: baselineExpr
};
u = 'y';
v = 'x';
adjust = '1-';
} else {
enter.align = update.align = {
signal: alignExpr
};
enter.baseline = {
value: 'top'
};
u = 'x';
v = 'y';
}
enter[u] = update[u] = {
signal: adjust + 'datum.' + Perc,
mult: length
};
enter[v] = update[v] = thickness;
thickness.offset = value(spec.labelOffset, config.gradientLabelOffset) || 0;
overlap = overlap ? {
separation: _('labelSeparation'),
method: overlap,
order: 'datum.' + Index
} : undefined;
// type, role, style, key, dataRef, encode, extras
return guideMark({
type: TextMark,
role: LegendLabelRole,
style: GuideLabelStyle,
key: Value,
from: dataRef,
encode,
overlap
}, userEncode);
}
// userEncode is top-level, includes entries, symbols, labels
function legendSymbolGroups (spec, config, userEncode, dataRef, columns) {
const _ = lookup(spec, config),
entries = userEncode.entries,
interactive = !!(entries && entries.interactive),
name = entries ? entries.name : undefined,
height = _('clipHeight'),
symbolOffset = _('symbolOffset'),
valueRef = {
data: 'value'
},
xSignal = `(${columns}) ? datum.${Offset} : datum.${Size}`,
yEncode = height ? encoder(height) : {
field: Size
},
index = `datum.${Index}`,
ncols = `max(1, ${columns})`;
let encode, enter, update, nrows, sort;
yEncode.mult = 0.5;
// -- LEGEND SYMBOLS --
encode = {
enter: enter = {
opacity: zero,
x: {
signal: xSignal,
mult: 0.5,
offset: symbolOffset
},
y: yEncode
},
update: update = {
opacity: one,
x: enter.x,
y: enter.y
},
exit: {
opacity: zero
}
};
let baseFill = null,
baseStroke = null;
if (!spec.fill) {
baseFill = config.symbolBaseFillColor;
baseStroke = config.symbolBaseStrokeColor;
}
addEncoders(encode, {
fill: _('symbolFillColor', baseFill),
shape: _('symbolType'),
size: _('symbolSize'),
stroke: _('symbolStrokeColor', baseStroke),
strokeDash: _('symbolDash'),
strokeDashOffset: _('symbolDashOffset'),
strokeWidth: _('symbolStrokeWidth')
}, {
// update
opacity: _('symbolOpacity')
});
LegendScales.forEach(scale => {
if (spec[scale]) {
update[scale] = enter[scale] = {
scale: spec[scale],
field: Value
};
}
});
const symbols = guideMark({
type: SymbolMark,
role: LegendSymbolRole,
key: Value,
from: valueRef,
clip: height ? true : undefined,
encode
}, userEncode.symbols);
// -- LEGEND LABELS --
const labelOffset = encoder(symbolOffset);
labelOffset.offset = _('labelOffset');
encode = {
enter: enter = {
opacity: zero,
x: {
signal: xSignal,
offset: labelOffset
},
y: yEncode
},
update: update = {
opacity: one,
text: {
field: Label
},
x: enter.x,
y: enter.y
},
exit: {
opacity: zero
}
};
addEncoders(encode, {
align: _('labelAlign'),
baseline: _('labelBaseline'),
fill: _('labelColor'),
fillOpacity: _('labelOpacity'),
font: _('labelFont'),
fontSize: _('labelFontSize'),
fontStyle: _('labelFontStyle'),
fontWeight: _('labelFontWeight'),
limit: _('labelLimit')
});
const labels = guideMark({
type: TextMark,
role: LegendLabelRole,
style: GuideLabelStyle,
key: Value,
from: valueRef,
encode
}, userEncode.labels);
// -- LEGEND ENTRY GROUPS --
encode = {
enter: {
noBound: {
value: !height
},
// ignore width/height in bounds calc
width: zero,
height: height ? encoder(height) : zero,
opacity: zero
},
exit: {
opacity: zero
},
update: update = {
opacity: one,
row: {
signal: null
},
column: {
signal: null
}
}
};
// annotate and sort groups to ensure correct ordering
if (_.isVertical(true)) {
nrows = `ceil(item.mark.items.length / ${ncols})`;
update.row.signal = `${index}%${nrows}`;
update.column.signal = `floor(${index} / ${nrows})`;
sort = {
field: ['row', index]
};
} else {
update.row.signal = `floor(${index} / ${ncols})`;
update.column.signal = `${index} % ${ncols}`;
sort = {
field: index
};
}
// handle zero column case (implies infinite columns)
update.column.signal = `(${columns})?${update.column.signal}:${index}`;
// facet legend entries into sub-groups
dataRef = {
facet: {
data: dataRef,
name: 'value',
groupby: Index
}
};
return guideGroup({
role: ScopeRole,
from: dataRef,
encode: extendEncode(encode, entries, Skip),
marks: [symbols, labels],
name,
interactive,
sort
});
}
function legendSymbolLayout(spec, config) {
const _ = lookup(spec, config);
// layout parameters for legend entries
return {
align: _('gridAlign'),
columns: _.entryColumns(),
center: {
row: true,
column: false
},
padding: {
row: _('rowPadding'),
column: _('columnPadding')
}
};
}
// expression logic for align, anchor, angle, and baseline calculation
const isL = 'item.orient === "left"',
isR = 'item.orient === "right"',
isLR = `(${isL} || ${isR})`,
isVG = `datum.vgrad && ${isLR}`,
baseline = anchorExpr('"top"', '"bottom"', '"middle"'),
alignFlip = anchorExpr('"right"', '"left"', '"center"'),
exprAlign = `datum.vgrad && ${isR} ? (${alignFlip}) : (${isLR} && !(datum.vgrad && ${isL})) ? "left" : ${alignExpr$1}`,
exprAnchor = `item._anchor || (${isLR} ? "middle" : "start")`,
exprAngle = `${isVG} ? (${isL} ? -90 : 90) : 0`,
exprBaseline = `${isLR} ? (datum.vgrad ? (${isR} ? "bottom" : "top") : ${baseline}) : "top"`;
function legendTitle (spec, config, userEncode, dataRef) {
const _ = lookup(spec, config);
const encode = {
enter: {
opacity: zero
},
update: {
opacity: one,
x: {
field: {
group: 'padding'
}
},
y: {
field: {
group: 'padding'
}
}
},
exit: {
opacity: zero
}
};
addEncoders(encode, {
orient: _('titleOrient'),
_anchor: _('titleAnchor'),
anchor: {
signal: exprAnchor
},
angle: {
signal: exprAngle
},
align: {
signal: exprAlign
},
baseline: {
signal: exprBaseline
},
text: spec.title,
fill: _('titleColor'),
fillOpacity: _('titleOpacity'),
font: _('titleFont'),
fontSize: _('titleFontSize'),
fontStyle: _('titleFontStyle'),
fontWeight: _('titleFontWeight'),
limit: _('titleLimit'),
lineHeight: _('titleLineHeight')
}, {
// require update
align: _('titleAlign'),
baseline: _('titleBaseline')
});
return guideMark({
type: TextMark,
role: LegendTitleRole,
style: GuideTitleStyle,
from: dataRef,
encode
}, userEncode);
}
function clip (clip, scope) {
let expr;
if (isObject(clip)) {
if (clip.signal) {
expr = clip.signal;
} else if (clip.path) {
expr = 'pathShape(' + param(clip.path) + ')';
} else if (clip.sphere) {
expr = 'geoShape(' + param(clip.sphere) + ', {type: "Sphere"})';
}
}
return expr ? scope.signalRef(expr) : !!clip;
}
function param(value) {
return isObject(value) && value.signal ? value.signal : stringValue(value);
}
function getRole (spec) {
const role = spec.role || '';
return role.startsWith('axis') || role.startsWith('legend') || role.startsWith('title') ? role : spec.type === GroupMark ? ScopeRole : role || MarkRole;
}
function definition (spec) {
return {
marktype: spec.type,
name: spec.name || undefined,
role: spec.role || getRole(spec),
zindex: +spec.zindex || undefined,
aria: spec.aria,
description: spec.description
};
}
function interactive (spec, scope) {
return spec && spec.signal ? scope.signalRef(spec.signal) : spec === false ? false : true;
}
/**
* Parse a data transform specification.
*/
function parseTransform (spec, scope) {
const def = definition$1(spec.type);
if (!def) error('Unrecognized transform type: ' + stringValue(spec.type));
const t = entry(def.type.toLowerCase(), null, parseParameters(def, spec, scope));
if (spec.signal) scope.addSignal(spec.signal, scope.proxy(t));
t.metadata = def.metadata || {};
return t;
}
/**
* Parse all parameters of a data transform.
*/
function parseParameters(def, spec, scope) {
const params = {},
n = def.params.length;
for (let i = 0; i < n; ++i) {
const pdef = def.params[i];
params[pdef.name] = parseParameter(pdef, spec, scope);
}
return params;
}
/**
* Parse a data transform parameter.
*/
function parseParameter(def, spec, scope) {
const type = def.type,
value = spec[def.name];
if (type === 'index') {
return parseIndexParameter(def, spec, scope);
} else if (value === undefined) {
if (def.required) {
error('Missing required ' + stringValue(spec.type) + ' parameter: ' + stringValue(def.name));
}
return;
} else if (type === 'param') {
return parseSubParameters(def, spec, scope);
} else if (type === 'projection') {
return scope.projectionRef(spec[def.name]);
}
return def.array && !isSignal(value) ? value.map(v => parameterValue(def, v, scope)) : parameterValue(def, value, scope);
}
/**
* Parse a single parameter value.
*/
function parameterValue(def, value, scope) {
const type = def.type;
if (isSignal(value)) {
return isExpr(type) ? error('Expression references can not be signals.') : isField(type) ? scope.fieldRef(value) : isCompare(type) ? scope.compareRef(value) : scope.signalRef(value.signal);
} else {
const expr = def.expr || isField(type);
return expr && outerExpr(value) ? scope.exprRef(value.expr, value.as) : expr && outerField(value) ? fieldRef$1(value.field, value.as) : isExpr(type) ? parseExpression(value, scope) : isData(type) ? ref(scope.getData(value).values) : isField(type) ? fieldRef$1(value) : isCompare(type) ? scope.compareRef(value) : value;
}
}
/**
* Parse parameter for accessing an index of another data set.
*/
function parseIndexParameter(def, spec, scope) {
if (!isString(spec.from)) {
error('Lookup "from" parameter must be a string literal.');
}
return scope.getData(spec.from).lookupRef(scope, spec.key);
}
/**
* Parse a parameter that contains one or more sub-parameter objects.
*/
function parseSubParameters(def, spec, scope) {
const value = spec[def.name];
if (def.array) {
if (!isArray(value)) {
// signals not allowed!
error('Expected an array of sub-parameters. Instead: ' + stringValue(value));
}
return value.map(v => parseSubParameter(def, v, scope));
} else {
return parseSubParameter(def, value, scope);
}
}
/**
* Parse a sub-parameter object.
*/
function parseSubParameter(def, value, scope) {
const n = def.params.length;
let pdef;
// loop over defs to find matching key
for (let i = 0; i < n; ++i) {
pdef = def.params[i];
for (const k in pdef.key) {
if (pdef.key[k] !== value[k]) {
pdef = null;
break;
}
}
if (pdef) break;
}
// raise error if matching key not found
if (!pdef) error('Unsupported parameter: ' + stringValue(value));
// parse params, create Params transform, return ref
const params = extend(parseParameters(pdef, value, scope), pdef.key);
return ref(scope.add(Params(params)));
}
// -- Utilities -----
const outerExpr = _ => _ && _.expr;
const outerField = _ => _ && _.field;
const isData = _ => _ === 'data';
const isExpr = _ => _ === 'expr';
const isField = _ => _ === 'field';
const isCompare = _ => _ === 'compare';
function parseData$1 (from, group, scope) {
let facet, key, op, dataRef, parent;
// if no source data, generate singleton datum
if (!from) {
dataRef = ref(scope.add(Collect(null, [{}])));
}
// if faceted, process facet specification
else if (facet = from.facet) {
if (!group) error('Only group marks can be faceted.');
// use pre-faceted source data, if available
if (facet.field != null) {
dataRef = parent = getDataRef(facet, scope);
} else {
// generate facet aggregates if no direct data specification
if (!from.data) {
op = parseTransform(extend({
type: 'aggregate',
groupby: array(facet.groupby)
}, facet.aggregate), scope);
op.params.key = scope.keyRef(facet.groupby);
op.params.pulse = getDataRef(facet, scope);
dataRef = parent = ref(scope.add(op));
} else {
parent = ref(scope.getData(from.data).aggregate);
}
key = scope.keyRef(facet.groupby, true);
}
}
// if not yet defined, get source data reference
if (!dataRef) {
dataRef = getDataRef(from, scope);
}
return {
key: key,
pulse: dataRef,
parent: parent
};
}
function getDataRef(from, scope) {
return from.$ref ? from : from.data && from.data.$ref ? from.data : ref(scope.getData(from.data).output);
}
function DataScope(scope, input, output, values, aggr) {
this.scope = scope; // parent scope object
this.input = input; // first operator in pipeline (tuple input)
this.output = output; // last operator in pipeline (tuple output)
this.values = values; // operator for accessing tuples (but not tuple flow)
// last aggregate in transform pipeline
this.aggregate = aggr;
// lookup table of field indices
this.index = {};
}
DataScope.fromEntries = function (scope, entries) {
const n = entries.length,
values = entries[n - 1],
output = entries[n - 2];
let input = entries[0],
aggr = null,
i = 1;
if (input && input.type === 'load') {
input = entries[1];
}
// add operator entries to this scope, wire up pulse chain
scope.add(entries[0]);
for (; i < n; ++i) {
entries[i].params.pulse = ref(entries[i - 1]);
scope.add(entries[i]);
if (entries[i].type === 'aggregate') aggr = entries[i];
}
return new DataScope(scope, input, output, values, aggr);
};
function fieldKey(field) {
return isString(field) ? field : null;
}
function addSortField(scope, p, sort) {
const as = aggrField(sort.op, sort.field);
let s;
if (p.ops) {
for (let i = 0, n = p.as.length; i < n; ++i) {
if (p.as[i] === as) return;
}
} else {
p.ops = ['count'];
p.fields = [null];
p.as = ['count'];
}
if (sort.op) {
p.ops.push((s = sort.op.signal) ? scope.signalRef(s) : sort.op);
p.fields.push(scope.fieldRef(sort.field));
p.as.push(as);
}
}
function cache(scope, ds, name, optype, field, counts, index) {
const cache = ds[name] || (ds[name] = {}),
sort = sortKey(counts);
let k = fieldKey(field),
v,
op;
if (k != null) {
scope = ds.scope;
k = k + (sort ? '|' + sort : '');
v = cache[k];
}
if (!v) {
const params = counts ? {
field: keyFieldRef,
pulse: ds.countsRef(scope, field, counts)
} : {
field: scope.fieldRef(field),
pulse: ref(ds.output)
};
if (sort) params.sort = scope.sortRef(counts);
op = scope.add(entry(optype, undefined, params));
if (index) ds.index[field] = op;
v = ref(op);
if (k != null) cache[k] = v;
}
return v;
}
DataScope.prototype = {
countsRef(scope, field, sort) {
const ds = this,
cache = ds.counts || (ds.counts = {}),
k = fieldKey(field);
let v, a, p;
if (k != null) {
scope = ds.scope;
v = cache[k];
}
if (!v) {
p = {
groupby: scope.fieldRef(field, 'key'),
pulse: ref(ds.output)
};
if (sort && sort.field) addSortField(scope, p, sort);
a = scope.add(Aggregate(p));
v = scope.add(Collect({
pulse: ref(a)
}));
v = {
agg: a,
ref: ref(v)
};
if (k != null) cache[k] = v;
} else if (sort && sort.field) {
addSortField(scope, v.agg.params, sort);
}
return v.ref;
},
tuplesRef() {
return ref(this.values);
},
extentRef(scope, field) {
return cache(scope, this, 'extent', 'extent', field, false);
},
domainRef(scope, field) {
return cache(scope, this, 'domain', 'values', field, false);
},
valuesRef(scope, field, sort) {
return cache(scope, this, 'vals', 'values', field, sort || true);
},
lookupRef(scope, field) {
return cache(scope, this, 'lookup', 'tupleindex', field, false);
},
indataRef(scope, field) {
return cache(scope, this, 'indata', 'tupleindex', field, true, true);
}
};
function parseFacet (spec, scope, group) {
const facet = spec.from.facet,
name = facet.name,
data = getDataRef(facet, scope);
let op;
if (!facet.name) {
error('Facet must have a name: ' + stringValue(facet));
}
if (!facet.data) {
error('Facet must reference a data set: ' + stringValue(facet));
}
if (facet.field) {
op = scope.add(PreFacet({
field: scope.fieldRef(facet.field),
pulse: data
}));
} else if (facet.groupby) {
op = scope.add(Facet({
key: scope.keyRef(facet.groupby),
group: ref(scope.proxy(group.parent)),
pulse: data
}));
} else {
error('Facet must specify groupby or field: ' + stringValue(facet));
}
// initialize facet subscope
const subscope = scope.fork(),
source = subscope.add(Collect()),
values = subscope.add(Sieve({
pulse: ref(source)
}));
subscope.