StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics
https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3856 lines
107 KiB
3856 lines
107 KiB
4 years ago
|
(function (global, factory) {
|
||
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vega-util'), require('vega-expression'), require('vega-functions'), require('vega-event-selector'), require('vega-scale'), require('vega-dataflow')) :
|
||
|
typeof define === 'function' && define.amd ? define(['exports', 'vega-util', 'vega-expression', 'vega-functions', 'vega-event-selector', 'vega-scale', 'vega-dataflow'], factory) :
|
||
|
(global = global || self, factory(global.vega = {}, global.vega, global.vega, global.vega, global.vega, global.vega, global.vega));
|
||
|
}(this, (function (exports, vegaUtil, vegaExpression, vegaFunctions, vegaEventSelector, vegaScale, vegaDataflow) { 'use strict';
|
||
|
|
||
|
function parseAutosize(spec, config) {
|
||
|
spec = spec || config.autosize;
|
||
|
return vegaUtil.isObject(spec)
|
||
|
? spec
|
||
|
: {type: spec || 'pad'};
|
||
|
}
|
||
|
|
||
|
function parsePadding(spec, config) {
|
||
|
spec = spec || config.padding;
|
||
|
return vegaUtil.isObject(spec)
|
||
|
? {
|
||
|
top: number(spec.top),
|
||
|
bottom: number(spec.bottom),
|
||
|
left: number(spec.left),
|
||
|
right: number(spec.right)
|
||
|
}
|
||
|
: paddingObject(number(spec));
|
||
|
}
|
||
|
|
||
|
function number(_) {
|
||
|
return +_ || 0;
|
||
|
}
|
||
|
|
||
|
function paddingObject(_) {
|
||
|
return {top: _, bottom: _, left: _, right: _};
|
||
|
}
|
||
|
|
||
|
var OUTER = 'outer',
|
||
|
OUTER_INVALID = ['value', 'update', 'init', 'react', 'bind'];
|
||
|
|
||
|
function outerError(prefix, name) {
|
||
|
vegaUtil.error(prefix + ' for "outer" push: ' + vegaUtil.stringValue(name));
|
||
|
}
|
||
|
|
||
|
function parseSignal(signal, scope) {
|
||
|
var 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(function(prop) {
|
||
|
if (signal[prop] !== undefined) outerError('Invalid property ', prop);
|
||
|
});
|
||
|
} else {
|
||
|
// define a new signal in the current scope
|
||
|
var op = scope.addSignal(name, signal.value);
|
||
|
if (signal.react === false) op.react = false;
|
||
|
if (signal.bind) scope.addBinding(name, signal.bind);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function parseExpression(expr, scope, preamble) {
|
||
|
var params = {}, ast, gen;
|
||
|
|
||
|
// parse the expression to an abstract syntax tree (ast)
|
||
|
try {
|
||
|
expr = vegaUtil.isString(expr) ? expr : (vegaUtil.stringValue(expr) + '');
|
||
|
ast = vegaExpression.parse(expr);
|
||
|
} catch (err) {
|
||
|
vegaUtil.error('Expression parse error: ' + expr);
|
||
|
}
|
||
|
|
||
|
// analyze ast function calls for dependencies
|
||
|
ast.visit(function visitor(node) {
|
||
|
if (node.type !== vegaExpression.CallExpression) return;
|
||
|
var name = node.callee.name,
|
||
|
visit = vegaFunctions.codegenParams.visitors[name];
|
||
|
if (visit) visit(name, node.arguments, scope, params);
|
||
|
});
|
||
|
|
||
|
// perform code generation
|
||
|
gen = vegaFunctions.codeGenerator(ast);
|
||
|
|
||
|
// collect signal dependencies
|
||
|
gen.globals.forEach(function(name) {
|
||
|
var signalName = vegaFunctions.SignalPrefix + name;
|
||
|
if (!vegaUtil.hasOwnProperty(params, signalName) && scope.getSignal(name)) {
|
||
|
params[signalName] = scope.signalRef(name);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// return generated expression code and dependencies
|
||
|
return {
|
||
|
$expr: preamble ? preamble + 'return(' + gen.code + ');' : gen.code,
|
||
|
$fields: gen.fields,
|
||
|
$params: params
|
||
|
};
|
||
|
}
|
||
|
|
||
|
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) {
|
||
|
var 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(field, name) {
|
||
|
return name ? {$field: field, $name: name} : {$field: field};
|
||
|
}
|
||
|
|
||
|
var keyFieldRef = fieldRef('key');
|
||
|
|
||
|
function compareRef(fields, orders) {
|
||
|
return {$compare: fields, $order: orders};
|
||
|
}
|
||
|
|
||
|
function keyRef(fields, flat) {
|
||
|
var ref = {$key: fields};
|
||
|
if (flat) ref.$flat = true;
|
||
|
return ref;
|
||
|
}
|
||
|
|
||
|
// -----
|
||
|
|
||
|
var Ascending = 'ascending';
|
||
|
|
||
|
var Descending = 'descending';
|
||
|
|
||
|
function sortKey(sort) {
|
||
|
return !vegaUtil.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 || '');
|
||
|
}
|
||
|
|
||
|
// -----
|
||
|
|
||
|
var Scope = 'scope';
|
||
|
|
||
|
var View = 'view';
|
||
|
|
||
|
function isSignal(_) {
|
||
|
return _ && _.signal;
|
||
|
}
|
||
|
|
||
|
function isExpr(_) {
|
||
|
return _ && _.expr;
|
||
|
}
|
||
|
|
||
|
function hasSignal(_) {
|
||
|
if (isSignal(_)) return true;
|
||
|
if (vegaUtil.isObject(_)) for (var 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;
|
||
|
}
|
||
|
|
||
|
var Timer = 'timer';
|
||
|
|
||
|
function parseStream(stream, scope) {
|
||
|
var method = stream.merge ? mergeStream
|
||
|
: stream.stream ? nestedStream
|
||
|
: stream.type ? eventStream
|
||
|
: vegaUtil.error('Invalid stream specification: ' + vegaUtil.stringValue(stream));
|
||
|
|
||
|
return method(stream, scope);
|
||
|
}
|
||
|
|
||
|
function eventSource(source) {
|
||
|
return source === Scope ? View : (source || View);
|
||
|
}
|
||
|
|
||
|
function mergeStream(stream, scope) {
|
||
|
var list = stream.merge.map(s => parseStream(s, scope)),
|
||
|
entry = streamParameters({merge: list}, stream, scope);
|
||
|
return scope.addStream(entry).id;
|
||
|
}
|
||
|
|
||
|
function nestedStream(stream, scope) {
|
||
|
var id = parseStream(stream.stream, scope),
|
||
|
entry = streamParameters({stream: id}, stream, scope);
|
||
|
return scope.addStream(entry).id;
|
||
|
}
|
||
|
|
||
|
function eventStream(stream, scope) {
|
||
|
var id, entry;
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
entry = streamParameters({stream: id}, stream, scope);
|
||
|
return Object.keys(entry).length === 1
|
||
|
? id
|
||
|
: scope.addStream(entry).id;
|
||
|
}
|
||
|
|
||
|
function streamParameters(entry, stream, scope) {
|
||
|
var param = stream.between;
|
||
|
|
||
|
if (param) {
|
||
|
if (param.length !== 2) {
|
||
|
vegaUtil.error('Stream "between" parameter must have 2 entries: ' + vegaUtil.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) {
|
||
|
// add filter to limit events from sub-scope only
|
||
|
param.push('inScope(event.item)');
|
||
|
}
|
||
|
if (param.length) {
|
||
|
entry.filter = parseExpression('(' + param.join(')&&(') + ')').$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) {
|
||
|
var item = 'event.item';
|
||
|
return item
|
||
|
+ (type && type !== '*' ? '&&' + item + '.mark.marktype===\'' + type + '\'' : '')
|
||
|
+ (role ? '&&' + item + '.mark.role===\'' + role + '\'' : '')
|
||
|
+ (name ? '&&' + item + '.mark.name===\'' + name + '\'' : '');
|
||
|
}
|
||
|
|
||
|
var preamble = 'var datum=event.item&&event.item.datum;';
|
||
|
|
||
|
function parseUpdate(spec, scope, target) {
|
||
|
var events = spec.events,
|
||
|
update = spec.update,
|
||
|
encode = spec.encode,
|
||
|
sources = [],
|
||
|
entry = {target: target};
|
||
|
|
||
|
if (!events) {
|
||
|
vegaUtil.error('Signal update missing events specification.');
|
||
|
}
|
||
|
|
||
|
// interpret as an event selector string
|
||
|
if (vegaUtil.isString(events)) {
|
||
|
events = vegaEventSelector.selector(events, scope.isSubscope() ? Scope : View);
|
||
|
}
|
||
|
|
||
|
// separate event streams from signal updates
|
||
|
events = vegaUtil.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) vegaUtil.error('Signal encode and update are mutually exclusive.');
|
||
|
update = 'encode(item(),' + vegaUtil.stringValue(encode) + ')';
|
||
|
}
|
||
|
|
||
|
// resolve update value
|
||
|
entry.update = vegaUtil.isString(update) ? parseExpression(update, scope, preamble)
|
||
|
: update.expr != null ? parseExpression(update.expr, scope, preamble)
|
||
|
: update.value != null ? update.value
|
||
|
: update.signal != null ? {
|
||
|
$expr: '_.value',
|
||
|
$params: {value: scope.signalRef(update.signal)}
|
||
|
}
|
||
|
: vegaUtil.error('Invalid signal update specification.');
|
||
|
|
||
|
if (spec.force) {
|
||
|
entry.options = {force: true};
|
||
|
}
|
||
|
|
||
|
sources.forEach(function(source) {
|
||
|
scope.addUpdate(vegaUtil.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) {
|
||
|
var op = scope.getSignal(signal.name),
|
||
|
expr = signal.update;
|
||
|
|
||
|
if (signal.init) {
|
||
|
if (expr) {
|
||
|
vegaUtil.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(function(_) {
|
||
|
parseUpdate(_, scope, op.id);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function transform(name) {
|
||
|
return function(params, value, parent) {
|
||
|
return entry(name, value, params || undefined, parent);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
var Aggregate = transform('aggregate');
|
||
|
var AxisTicks = transform('axisticks');
|
||
|
var Bound = transform('bound');
|
||
|
var Collect = transform('collect');
|
||
|
var Compare = transform('compare');
|
||
|
var DataJoin = transform('datajoin');
|
||
|
var Encode = transform('encode');
|
||
|
var Expression = transform('expression');
|
||
|
var Facet = transform('facet');
|
||
|
var Field = transform('field');
|
||
|
var Key = transform('key');
|
||
|
var LegendEntries = transform('legendentries');
|
||
|
var Load = transform('load');
|
||
|
var Mark = transform('mark');
|
||
|
var MultiExtent = transform('multiextent');
|
||
|
var MultiValues = transform('multivalues');
|
||
|
var Overlap = transform('overlap');
|
||
|
var Params = transform('params');
|
||
|
var PreFacet = transform('prefacet');
|
||
|
var Projection = transform('projection');
|
||
|
var Proxy = transform('proxy');
|
||
|
var Relay = transform('relay');
|
||
|
var Render = transform('render');
|
||
|
var Scale = transform('scale');
|
||
|
var Sieve = transform('sieve');
|
||
|
var SortItems = transform('sortitems');
|
||
|
var ViewLayout = transform('viewlayout');
|
||
|
var Values = transform('values');
|
||
|
|
||
|
var FIELD_REF_ID = 0;
|
||
|
|
||
|
var MULTIDOMAIN_SORT_OPS = {min: 'min', max: 'max', count: 'sum'};
|
||
|
|
||
|
function initScale(spec, scope) {
|
||
|
var type = spec.type || 'linear';
|
||
|
|
||
|
if (!vegaScale.isValidScaleType(type)) {
|
||
|
vegaUtil.error('Unrecognized scale type: ' + vegaUtil.stringValue(type));
|
||
|
}
|
||
|
|
||
|
scope.addScale(spec.name, {
|
||
|
type: type,
|
||
|
domain: undefined
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function parseScale(spec, scope) {
|
||
|
var params = scope.getScale(spec.name).params,
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
if (spec.bins != null) {
|
||
|
params.bins = parseScaleBins(spec.bins, scope);
|
||
|
}
|
||
|
|
||
|
for (key in spec) {
|
||
|
if (vegaUtil.hasOwnProperty(params, key) || key === 'name') continue;
|
||
|
params[key] = parseLiteral(spec[key], scope);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function parseLiteral(v, scope) {
|
||
|
return !vegaUtil.isObject(v) ? v
|
||
|
: v.signal ? scope.signalRef(v.signal)
|
||
|
: vegaUtil.error('Unsupported object: ' + vegaUtil.stringValue(v));
|
||
|
}
|
||
|
|
||
|
function parseArray(v, scope) {
|
||
|
return v.signal
|
||
|
? scope.signalRef(v.signal)
|
||
|
: v.map(v => parseLiteral(v, scope));
|
||
|
}
|
||
|
|
||
|
function dataLookupError(name) {
|
||
|
vegaUtil.error('Can not find data set: ' + vegaUtil.stringValue(name));
|
||
|
}
|
||
|
|
||
|
// -- SCALE DOMAIN ----
|
||
|
|
||
|
function parseScaleDomain(domain, spec, scope) {
|
||
|
if (!domain) {
|
||
|
if (spec.domainMin != null || spec.domainMax != null) {
|
||
|
vegaUtil.error('No scale domain defined for domainMin/domainMax to override.');
|
||
|
}
|
||
|
return; // default domain
|
||
|
}
|
||
|
|
||
|
return domain.signal ? scope.signalRef(domain.signal)
|
||
|
: (vegaUtil.isArray(domain) ? explicitDomain
|
||
|
: domain.fields ? multipleDomain
|
||
|
: singularDomain)(domain, spec, scope);
|
||
|
}
|
||
|
|
||
|
function explicitDomain(domain, spec, scope) {
|
||
|
return domain.map(function(v) {
|
||
|
return parseLiteral(v, scope);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function singularDomain(domain, spec, scope) {
|
||
|
var data = scope.getData(domain.data);
|
||
|
if (!data) dataLookupError(domain.data);
|
||
|
|
||
|
return vegaScale.isDiscrete(spec.type)
|
||
|
? data.valuesRef(scope, domain.field, parseSort(domain.sort, false))
|
||
|
: vegaScale.isQuantile(spec.type) ? data.domainRef(scope, domain.field)
|
||
|
: data.extentRef(scope, domain.field);
|
||
|
}
|
||
|
|
||
|
function multipleDomain(domain, spec, scope) {
|
||
|
var data = domain.data,
|
||
|
fields = domain.fields.reduce(function(dom, d) {
|
||
|
d = vegaUtil.isString(d) ? {data: data, field: d}
|
||
|
: (vegaUtil.isArray(d) || d.signal) ? fieldRef$1(d, scope)
|
||
|
: d;
|
||
|
dom.push(d);
|
||
|
return dom;
|
||
|
}, []);
|
||
|
|
||
|
return (vegaScale.isDiscrete(spec.type) ? ordinalMultipleDomain
|
||
|
: vegaScale.isQuantile(spec.type) ? quantileMultipleDomain
|
||
|
: numericMultipleDomain)(domain, scope, fields);
|
||
|
}
|
||
|
|
||
|
function fieldRef$1(data, scope) {
|
||
|
var name = '_:vega:_' + (FIELD_REF_ID++),
|
||
|
coll = Collect({});
|
||
|
|
||
|
if (vegaUtil.isArray(data)) {
|
||
|
coll.value = {$ingest: data};
|
||
|
} else if (data.signal) {
|
||
|
var code = 'setdata(' + vegaUtil.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) {
|
||
|
var sort = parseSort(domain.sort, true),
|
||
|
counts, p, a, c, v;
|
||
|
|
||
|
// get value counts for each domain field
|
||
|
counts = fields.map(function(f) {
|
||
|
var data = scope.getData(f.data);
|
||
|
if (!data) dataLookupError(f.data);
|
||
|
return data.countsRef(scope, f.field, sort);
|
||
|
});
|
||
|
|
||
|
// aggregate the results from each domain field
|
||
|
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
|
||
|
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 (vegaUtil.isObject(sort)) sort.field = 'key';
|
||
|
else sort = {field: 'key'};
|
||
|
} else if (!sort.field && sort.op !== 'count') {
|
||
|
vegaUtil.error('No field provided for sort aggregate op: ' + sort.op);
|
||
|
} else if (multidomain && sort.field) {
|
||
|
if (sort.op && !MULTIDOMAIN_SORT_OPS[sort.op]) {
|
||
|
vegaUtil.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
|
||
|
var values = fields.map(function(f) {
|
||
|
var 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
|
||
|
var extents = fields.map(function(f) {
|
||
|
var 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 || vegaUtil.isArray(v)
|
||
|
? parseArray(v, scope)
|
||
|
: scope.objectProperty(v);
|
||
|
}
|
||
|
|
||
|
// -- SCALE NICE -----
|
||
|
|
||
|
function parseScaleNice(nice) {
|
||
|
return vegaUtil.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) {
|
||
|
var range = spec.range,
|
||
|
config = scope.config.range;
|
||
|
|
||
|
if (range.signal) {
|
||
|
return scope.signalRef(range.signal);
|
||
|
} else if (vegaUtil.isString(range)) {
|
||
|
if (config && vegaUtil.hasOwnProperty(config, range)) {
|
||
|
spec = vegaUtil.extend({}, spec, {range: config[range]});
|
||
|
return parseScaleRange(spec, scope, params);
|
||
|
} else if (range === 'width') {
|
||
|
range = [0, {signal: 'width'}];
|
||
|
} else if (range === 'height') {
|
||
|
range = vegaScale.isDiscrete(spec.type)
|
||
|
? [0, {signal: 'height'}]
|
||
|
: [{signal: 'height'}, 0];
|
||
|
} else {
|
||
|
vegaUtil.error('Unrecognized scale range value: ' + vegaUtil.stringValue(range));
|
||
|
}
|
||
|
} else if (range.scheme) {
|
||
|
params.scheme = vegaUtil.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 (vegaScale.isDiscrete(spec.type) && !vegaUtil.isArray(range)) {
|
||
|
return parseScaleDomain(range, spec, scope);
|
||
|
} else if (!vegaUtil.isArray(range)) {
|
||
|
vegaUtil.error('Unsupported range type: ' + vegaUtil.stringValue(range));
|
||
|
}
|
||
|
|
||
|
return range.map(v => (vegaUtil.isArray(v) ? parseArray : parseLiteral)(v, scope));
|
||
|
}
|
||
|
|
||
|
function parseProjection(proj, scope) {
|
||
|
var config = scope.config.projection || {},
|
||
|
params = {};
|
||
|
|
||
|
for (var name in proj) {
|
||
|
if (name === 'name') continue;
|
||
|
params[name] = parseParameter(proj[name], name, scope);
|
||
|
}
|
||
|
|
||
|
// apply projection defaults from config
|
||
|
for (name in config) {
|
||
|
if (params[name] == null) {
|
||
|
params[name] = parseParameter(config[name], name, scope);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
scope.addProjection(proj.name, params);
|
||
|
}
|
||
|
|
||
|
function parseParameter(_, name, scope) {
|
||
|
return vegaUtil.isArray(_) ? _.map(function(_) { return parseParameter(_, name, scope); })
|
||
|
: !vegaUtil.isObject(_) ? _
|
||
|
: _.signal ? scope.signalRef(_.signal)
|
||
|
: name === 'fit' ? _
|
||
|
: vegaUtil.error('Unsupported parameter object: ' + vegaUtil.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 Size = 'size';
|
||
|
const Value = 'value';
|
||
|
|
||
|
const GuideLabelStyle = 'guide-label';
|
||
|
const GuideTitleStyle = 'guide-title';
|
||
|
const GroupTitleStyle = 'group-title';
|
||
|
const GroupSubtitleStyle = 'group-subtitle';
|
||
|
|
||
|
const Symbols = 'symbol';
|
||
|
const Gradient = 'gradient';
|
||
|
const Discrete = 'discrete';
|
||
|
|
||
|
// 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};
|
||
|
|
||
|
var Skip$1 = vegaUtil.toSet(['rule']),
|
||
|
Swap = vegaUtil.toSet(['group', 'image', 'rect']);
|
||
|
|
||
|
function adjustSpatial(encode, marktype) {
|
||
|
var code = '';
|
||
|
|
||
|
if (Skip$1[marktype]) return code;
|
||
|
|
||
|
if (encode.x2) {
|
||
|
if (encode.x) {
|
||
|
if (Swap[marktype]) {
|
||
|
code += 'if(o.x>o.x2)$=o.x,o.x=o.x2,o.x2=$;';
|
||
|
}
|
||
|
code += 'o.width=o.x2-o.x;';
|
||
|
} else {
|
||
|
code += 'o.x=o.x2-(o.width||0);';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (encode.xc) {
|
||
|
code += 'o.x=o.xc-(o.width||0)/2;';
|
||
|
}
|
||
|
|
||
|
if (encode.y2) {
|
||
|
if (encode.y) {
|
||
|
if (Swap[marktype]) {
|
||
|
code += 'if(o.y>o.y2)$=o.y,o.y=o.y2,o.y2=$;';
|
||
|
}
|
||
|
code += 'o.height=o.y2-o.y;';
|
||
|
} else {
|
||
|
code += 'o.y=o.y2-(o.height||0);';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (encode.yc) {
|
||
|
code += 'o.y=o.yc-(o.height||0)/2;';
|
||
|
}
|
||
|
|
||
|
return code;
|
||
|
}
|
||
|
|
||
|
function color(enc, scope, params, fields) {
|
||
|
function color(type, x, y, z) {
|
||
|
var a = entry$1(null, x, scope, params, fields),
|
||
|
b = entry$1(null, y, scope, params, fields),
|
||
|
c = entry$1(null, z, scope, params, fields);
|
||
|
return 'this.' + type + '(' + [a, b, c].join(',') + ').toString()';
|
||
|
}
|
||
|
|
||
|
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 expression(code, scope, params, fields) {
|
||
|
var expr = parseExpression(code, scope);
|
||
|
expr.$fields.forEach(function(name) { fields[name] = 1; });
|
||
|
vegaUtil.extend(params, expr.$params);
|
||
|
return expr.$expr;
|
||
|
}
|
||
|
|
||
|
function field(ref, scope, params, fields) {
|
||
|
return resolve(vegaUtil.isObject(ref) ? ref : {datum: ref}, scope, params, fields);
|
||
|
}
|
||
|
|
||
|
function resolve(ref, scope, params, fields) {
|
||
|
var object, level, field;
|
||
|
|
||
|
if (ref.signal) {
|
||
|
object = 'datum';
|
||
|
field = expression(ref.signal, scope, params, fields);
|
||
|
} 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 {
|
||
|
vegaUtil.error('Invalid field reference: ' + vegaUtil.stringValue(ref));
|
||
|
}
|
||
|
|
||
|
if (!ref.signal) {
|
||
|
if (vegaUtil.isString(field)) {
|
||
|
fields[field] = 1; // TODO review field tracking?
|
||
|
field = vegaUtil.splitAccessPath(field).map(vegaUtil.stringValue).join('][');
|
||
|
} else {
|
||
|
field = resolve(field, scope, params, fields);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return object + '[' + field + ']';
|
||
|
}
|
||
|
|
||
|
function property(property, scope, params, fields) {
|
||
|
return vegaUtil.isObject(property)
|
||
|
? '(' + entry$1(null, property, scope, params, fields) + ')'
|
||
|
: property;
|
||
|
}
|
||
|
|
||
|
function scale(enc, value, scope, params, fields) {
|
||
|
var scale = getScale(enc.scale, scope, params, fields),
|
||
|
interp, func, flag;
|
||
|
|
||
|
if (enc.range != null) {
|
||
|
// pull value from scale range
|
||
|
interp = +enc.range;
|
||
|
func = scale + '.range()';
|
||
|
value = (interp === 0) ? (func + '[0]')
|
||
|
: '($=' + func + ',' + ((interp === 1) ? '$[$.length-1]'
|
||
|
: '$[0]+' + interp + '*($[$.length-1]-$[0])') + ')';
|
||
|
} else {
|
||
|
// run value through scale and/or pull scale bandwidth
|
||
|
if (value !== undefined) value = scale + '(' + value + ')';
|
||
|
|
||
|
if (enc.band && (flag = hasBandwidth(enc.scale, scope))) {
|
||
|
func = scale + '.bandwidth';
|
||
|
|
||
|
if (enc.band.signal) {
|
||
|
interp = func + '()*' + property(enc.band, scope, params, fields);
|
||
|
} else {
|
||
|
interp = +enc.band;
|
||
|
interp = func + '()' + (interp===1 ? '' : '*' + interp);
|
||
|
}
|
||
|
|
||
|
// if we don't know the scale type, check for bandwidth
|
||
|
if (flag < 0) interp = '(' + func + '?' + interp + ':0)';
|
||
|
|
||
|
value = (value ? value + '+' : '') + interp;
|
||
|
|
||
|
if (enc.extra) {
|
||
|
// include logic to handle extraneous elements
|
||
|
value = '(datum.extra?' + scale + '(datum.extra.value):' + value + ')';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (value == null) value = '0';
|
||
|
}
|
||
|
|
||
|
return value;
|
||
|
}
|
||
|
|
||
|
function hasBandwidth(name, scope) {
|
||
|
if (!vegaUtil.isString(name)) return -1;
|
||
|
var type = scope.scaleType(name);
|
||
|
return type === 'band' || type === 'point' ? 1 : 0;
|
||
|
}
|
||
|
|
||
|
function getScale(name, scope, params, fields) {
|
||
|
var scaleName;
|
||
|
|
||
|
if (vegaUtil.isString(name)) {
|
||
|
// direct scale lookup; add scale as parameter
|
||
|
scaleName = vegaFunctions.ScalePrefix + name;
|
||
|
if (!vegaUtil.hasOwnProperty(params, scaleName)) {
|
||
|
params[scaleName] = scope.scaleRef(name);
|
||
|
}
|
||
|
scaleName = vegaUtil.stringValue(scaleName);
|
||
|
} else {
|
||
|
// indirect scale lookup; add all scales as parameters
|
||
|
for (scaleName in scope.scales) {
|
||
|
params[vegaFunctions.ScalePrefix + scaleName] = scope.scaleRef(scaleName);
|
||
|
}
|
||
|
scaleName = vegaUtil.stringValue(vegaFunctions.ScalePrefix) + '+'
|
||
|
+ (name.signal
|
||
|
? '(' + expression(name.signal, scope, params, fields) + ')'
|
||
|
: field(name, scope, params, fields));
|
||
|
}
|
||
|
|
||
|
return '_[' + scaleName + ']';
|
||
|
}
|
||
|
|
||
|
function gradient(enc, scope, params, fields) {
|
||
|
return 'this.gradient('
|
||
|
+ getScale(enc.gradient, scope, params, fields)
|
||
|
+ ',' + vegaUtil.stringValue(enc.start)
|
||
|
+ ',' + vegaUtil.stringValue(enc.stop)
|
||
|
+ ',' + vegaUtil.stringValue(enc.count)
|
||
|
+ ')';
|
||
|
}
|
||
|
|
||
|
function entry$1(channel, enc, scope, params, fields) {
|
||
|
if (enc.gradient != null) {
|
||
|
return gradient(enc, scope, params, fields);
|
||
|
}
|
||
|
|
||
|
var value = enc.signal ? expression(enc.signal, scope, params, fields)
|
||
|
: enc.color ? color(enc.color, scope, params, fields)
|
||
|
: enc.field != null ? field(enc.field, scope, params, fields)
|
||
|
: enc.value !== undefined ? vegaUtil.stringValue(enc.value)
|
||
|
: undefined;
|
||
|
|
||
|
if (enc.scale != null) {
|
||
|
value = scale(enc, value, scope, params, fields);
|
||
|
}
|
||
|
|
||
|
if (value === undefined) {
|
||
|
value = null;
|
||
|
}
|
||
|
|
||
|
if (enc.exponent != null) {
|
||
|
value = 'Math.pow(' + value + ','
|
||
|
+ property(enc.exponent, scope, params, fields) + ')';
|
||
|
}
|
||
|
|
||
|
if (enc.mult != null) {
|
||
|
value += '*' + property(enc.mult, scope, params, fields);
|
||
|
}
|
||
|
|
||
|
if (enc.offset != null) {
|
||
|
value += '+' + property(enc.offset, scope, params, fields);
|
||
|
}
|
||
|
|
||
|
if (enc.round) {
|
||
|
value = 'Math.round(' + value + ')';
|
||
|
}
|
||
|
|
||
|
return value;
|
||
|
}
|
||
|
|
||
|
function set(obj, key, value) {
|
||
|
const o = obj + '[' + vegaUtil.stringValue(key) + ']';
|
||
|
return `$=${value};if(${o}!==$)${o}=$,m=1;`;
|
||
|
}
|
||
|
|
||
|
function rule(channel, rules, scope, params, fields) {
|
||
|
var code = '';
|
||
|
|
||
|
rules.forEach(function(rule) {
|
||
|
var value = entry$1(channel, rule, scope, params, fields);
|
||
|
code += rule.test
|
||
|
? expression(rule.test, scope, params, fields) + '?' + value + ':'
|
||
|
: value;
|
||
|
});
|
||
|
|
||
|
// if no else clause, terminate with null (vega/vega#1366)
|
||
|
if (vegaUtil.peek(code) === ':') {
|
||
|
code += 'null';
|
||
|
}
|
||
|
|
||
|
return set('o', channel, code);
|
||
|
}
|
||
|
|
||
|
function parseEncode(encode, marktype, params, scope) {
|
||
|
var fields = {},
|
||
|
code = 'var o=item,datum=o.datum,m=0,$;',
|
||
|
channel, enc, value;
|
||
|
|
||
|
for (channel in encode) {
|
||
|
enc = encode[channel];
|
||
|
if (vegaUtil.isArray(enc)) { // rule
|
||
|
code += rule(channel, enc, scope, params, fields);
|
||
|
} else {
|
||
|
value = entry$1(channel, enc, scope, params, fields);
|
||
|
code += set('o', channel, value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
code += adjustSpatial(encode, marktype);
|
||
|
code += 'return m;';
|
||
|
|
||
|
return {
|
||
|
$expr: code,
|
||
|
$fields: Object.keys(fields),
|
||
|
$output: Object.keys(encode)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
var MarkRole = 'mark';
|
||
|
var FrameRole = 'frame';
|
||
|
var ScopeRole = 'scope';
|
||
|
|
||
|
var AxisRole = 'axis';
|
||
|
var AxisDomainRole = 'axis-domain';
|
||
|
var AxisGridRole = 'axis-grid';
|
||
|
var AxisLabelRole = 'axis-label';
|
||
|
var AxisTickRole = 'axis-tick';
|
||
|
var AxisTitleRole = 'axis-title';
|
||
|
|
||
|
var LegendRole = 'legend';
|
||
|
var LegendBandRole = 'legend-band';
|
||
|
var LegendEntryRole = 'legend-entry';
|
||
|
var LegendGradientRole = 'legend-gradient';
|
||
|
var LegendLabelRole = 'legend-label';
|
||
|
var LegendSymbolRole = 'legend-symbol';
|
||
|
var LegendTitleRole = 'legend-title';
|
||
|
|
||
|
var TitleRole = 'title';
|
||
|
var TitleTextRole = 'title-text';
|
||
|
var TitleSubtitleRole = 'title-subtitle';
|
||
|
|
||
|
function encoder(_) {
|
||
|
return vegaUtil.isObject(_) && !vegaUtil.isArray(_) ? vegaUtil.extend({}, _) : {value: _};
|
||
|
}
|
||
|
|
||
|
function addEncode(object, name, value, set) {
|
||
|
if (value != null) {
|
||
|
if (vegaUtil.isObject(value) && !vegaUtil.isArray(value)) {
|
||
|
object.update[name] = value;
|
||
|
} else {
|
||
|
object[set || 'enter'][name] = {value: value};
|
||
|
}
|
||
|
return 1;
|
||
|
} else {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function addEncoders(object, enter, update) {
|
||
|
for (let name in enter) {
|
||
|
addEncode(object, name, enter[name]);
|
||
|
}
|
||
|
for (let name in update) {
|
||
|
addEncode(object, name, update[name], 'update');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function extendEncode(encode, extra, skip) {
|
||
|
for (var name in extra) {
|
||
|
if (skip && vegaUtil.hasOwnProperty(skip, name)) continue;
|
||
|
encode[name] = vegaUtil.extend(encode[name] || {}, extra[name]);
|
||
|
}
|
||
|
return encode;
|
||
|
}
|
||
|
|
||
|
function encoders(encode, type, role, style, scope, params) {
|
||
|
var enc, key;
|
||
|
params = params || {};
|
||
|
params.encoders = {$encode: (enc = {})};
|
||
|
|
||
|
encode = applyDefaults(encode, type, role, style, scope.config);
|
||
|
|
||
|
for (key in encode) {
|
||
|
enc[key] = parseEncode(encode[key], type, params, scope);
|
||
|
}
|
||
|
|
||
|
return params;
|
||
|
}
|
||
|
|
||
|
function applyDefaults(encode, type, role, style, config) {
|
||
|
var defaults = {}, enter = {}, update, key, skip, props;
|
||
|
|
||
|
// ignore legend and axis
|
||
|
if (role == 'legend' || String(role).indexOf('axis') === 0) {
|
||
|
role = null;
|
||
|
}
|
||
|
|
||
|
// resolve mark config
|
||
|
props = role === FrameRole ? config.group
|
||
|
: (role === MarkRole) ? vegaUtil.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
|
||
|
vegaUtil.array(style).forEach(function(name) {
|
||
|
var props = config.style && config.style[name];
|
||
|
for (var key in props) {
|
||
|
if (!has(key, encode)) {
|
||
|
applyDefault(defaults, key, props[key]);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
encode = vegaUtil.extend({}, encode); // defensive copy
|
||
|
for (key in defaults) {
|
||
|
props = defaults[key];
|
||
|
if (props.signal) {
|
||
|
(update = update || {})[key] = props;
|
||
|
} else {
|
||
|
enter[key] = props;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
encode.enter = vegaUtil.extend(enter, encode.enter);
|
||
|
if (update) encode.update = vegaUtil.extend(update, encode.update);
|
||
|
|
||
|
return encode;
|
||
|
}
|
||
|
|
||
|
function applyDefault(defaults, key, value) {
|
||
|
defaults[key] = value && value.signal
|
||
|
? {signal: value.signal}
|
||
|
: {value: value};
|
||
|
}
|
||
|
|
||
|
function has(key, encode) {
|
||
|
return encode && (
|
||
|
(encode.enter && encode.enter[key]) ||
|
||
|
(encode.update && encode.update[key])
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function guideMark(type, role, style, key, dataRef, encode, extras) {
|
||
|
return {
|
||
|
type: type,
|
||
|
name: extras ? extras.name : undefined,
|
||
|
role: role,
|
||
|
style: (extras && extras.style) || style,
|
||
|
key: key,
|
||
|
from: dataRef,
|
||
|
interactive: !!(extras && extras.interactive),
|
||
|
encode: extendEncode(encode, extras, Skip)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
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) {
|
||
|
var 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) {
|
||
|
var 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 = anchorExpr(
|
||
|
vegaUtil.stringValue(Left),
|
||
|
vegaUtil.stringValue(Right),
|
||
|
vegaUtil.stringValue(Center)
|
||
|
);
|
||
|
|
||
|
function tickBand(_) {
|
||
|
let v = _('tickBand'),
|
||
|
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'?true:false`};
|
||
|
if (!vegaUtil.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};
|
||
|
}
|
||
|
|
||
|
var GroupMark = 'group';
|
||
|
var RectMark = 'rect';
|
||
|
var RuleMark = 'rule';
|
||
|
var SymbolMark = 'symbol';
|
||
|
var TextMark = 'text';
|
||
|
|
||
|
function legendGradient(spec, scale, config, userEncode) {
|
||
|
var _ = lookup(spec, config),
|
||
|
vertical = _.isVertical(),
|
||
|
thickness = _.gradientThickness(),
|
||
|
length = _.gradientLength(),
|
||
|
encode, 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;
|
||
|
}
|
||
|
|
||
|
encode = {
|
||
|
enter: enter = {
|
||
|
opacity: zero,
|
||
|
x: zero,
|
||
|
y: zero,
|
||
|
width: encoder(width),
|
||
|
height: encoder(height)
|
||
|
},
|
||
|
update: vegaUtil.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(RectMark, LegendGradientRole, null, undefined, undefined, encode, userEncode);
|
||
|
}
|
||
|
|
||
|
function legendGradientDiscrete(spec, scale, config, userEncode, dataRef) {
|
||
|
var _ = lookup(spec, config),
|
||
|
vertical = _.isVertical(),
|
||
|
thickness = _.gradientThickness(),
|
||
|
length = _.gradientLength(),
|
||
|
encode, enter, u, v, uu, vv, adjust = '';
|
||
|
|
||
|
vertical
|
||
|
? (u = 'y', uu = 'y2', v = 'x', vv = 'width', adjust = '1-')
|
||
|
: (u = 'x', uu = 'x2', v = 'y', vv = 'height');
|
||
|
|
||
|
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);
|
||
|
|
||
|
encode = {
|
||
|
enter: enter,
|
||
|
update: vegaUtil.extend({}, enter, {opacity: one}),
|
||
|
exit: {opacity: zero}
|
||
|
};
|
||
|
|
||
|
addEncoders(encode, {
|
||
|
stroke: _('gradientStrokeColor'),
|
||
|
strokeWidth: _('gradientStrokeWidth')
|
||
|
}, { // update
|
||
|
opacity: _('gradientOpacity')
|
||
|
});
|
||
|
|
||
|
return guideMark(RectMark, LegendBandRole, null, Value, dataRef, encode, userEncode);
|
||
|
}
|
||
|
|
||
|
const alignExpr$1 = `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) {
|
||
|
var _ = lookup(spec, config),
|
||
|
vertical = _.isVertical(),
|
||
|
thickness = encoder(_.gradientThickness()),
|
||
|
length = _.gradientLength(),
|
||
|
overlap = _('labelOverlap'),
|
||
|
separation = _('labelSeparation'),
|
||
|
encode, enter, update, u, v, adjust = '';
|
||
|
|
||
|
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$1};
|
||
|
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;
|
||
|
|
||
|
spec = guideMark(TextMark, LegendLabelRole, GuideLabelStyle, Value, dataRef, encode, userEncode);
|
||
|
if (overlap) {
|
||
|
spec.overlap = {
|
||
|
separation: separation,
|
||
|
method: overlap,
|
||
|
order: 'datum.' + Index
|
||
|
};
|
||
|
}
|
||
|
return spec;
|
||
|
}
|
||
|
|
||
|
function guideGroup(role, style, name, dataRef, interactive, encode, marks, layout) {
|
||
|
return {
|
||
|
type: GroupMark,
|
||
|
name: name,
|
||
|
role: role,
|
||
|
style: style,
|
||
|
from: dataRef,
|
||
|
interactive: interactive || false,
|
||
|
encode: encode,
|
||
|
marks: marks,
|
||
|
layout: layout
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// userEncode is top-level, includes entries, symbols, labels
|
||
|
function legendSymbolGroups(spec, config, userEncode, dataRef, columns) {
|
||
|
var _ = lookup(spec, config),
|
||
|
entries = userEncode.entries,
|
||
|
interactive = !!(entries && entries.interactive),
|
||
|
name = entries ? entries.name : undefined,
|
||
|
height = _('clipHeight'),
|
||
|
symbolOffset = _('symbolOffset'),
|
||
|
valueRef = {data: 'value'},
|
||
|
encode = {},
|
||
|
xSignal = `${columns} ? datum.${Offset} : datum.${Size}`,
|
||
|
yEncode = height ? encoder(height) : {field: Size},
|
||
|
index = `datum.${Index}`,
|
||
|
ncols = `max(1, ${columns})`,
|
||
|
enter, update, labelOffset, symbols, labels, 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
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var 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(function(scale) {
|
||
|
if (spec[scale]) {
|
||
|
update[scale] = enter[scale] = {scale: spec[scale], field: Value};
|
||
|
}
|
||
|
});
|
||
|
|
||
|
symbols = guideMark(
|
||
|
SymbolMark, LegendSymbolRole, null,
|
||
|
Value, valueRef, encode, userEncode.symbols
|
||
|
);
|
||
|
if (height) symbols.clip = true;
|
||
|
|
||
|
// -- LEGEND LABELS --
|
||
|
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')
|
||
|
});
|
||
|
|
||
|
labels = guideMark(
|
||
|
TextMark, LegendLabelRole, GuideLabelStyle,
|
||
|
Value, 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}};
|
||
|
|
||
|
spec = guideGroup(
|
||
|
ScopeRole, null, name, dataRef, interactive,
|
||
|
extendEncode(encode, entries, Skip), [symbols, labels]
|
||
|
);
|
||
|
spec.sort = sort;
|
||
|
return spec;
|
||
|
}
|
||
|
|
||
|
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}`,
|
||
|
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) {
|
||
|
var _ = lookup(spec, config), encode;
|
||
|
|
||
|
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(TextMark, LegendTitleRole, GuideTitleStyle, null, dataRef, encode, userEncode);
|
||
|
}
|
||
|
|
||
|
function clip(clip, scope) {
|
||
|
var expr;
|
||
|
|
||
|
if (vegaUtil.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 vegaUtil.isObject(value) && value.signal
|
||
|
? value.signal
|
||
|
: vegaUtil.stringValue(value);
|
||
|
}
|
||
|
|
||
|
function getRole(spec) {
|
||
|
var role = spec.role || '';
|
||
|
return (!role.indexOf('axis') || !role.indexOf('legend') || !role.indexOf('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
|
||
|
};
|
||
|
}
|
||
|
|
||
|
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) {
|
||
|
var def = vegaDataflow.definition(spec.type);
|
||
|
if (!def) vegaUtil.error('Unrecognized transform type: ' + vegaUtil.stringValue(spec.type));
|
||
|
|
||
|
var 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) {
|
||
|
var params = {}, pdef, i, n;
|
||
|
for (i=0, n=def.params.length; i<n; ++i) {
|
||
|
pdef = def.params[i];
|
||
|
params[pdef.name] = parseParameter$1(pdef, spec, scope);
|
||
|
}
|
||
|
return params;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse a data transform parameter.
|
||
|
*/
|
||
|
function parseParameter$1(def, spec, scope) {
|
||
|
var type = def.type,
|
||
|
value = spec[def.name];
|
||
|
|
||
|
if (type === 'index') {
|
||
|
return parseIndexParameter(def, spec, scope);
|
||
|
} else if (value === undefined) {
|
||
|
if (def.required) {
|
||
|
vegaUtil.error('Missing required ' + vegaUtil.stringValue(spec.type)
|
||
|
+ ' parameter: ' + vegaUtil.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(function(v) { return parameterValue(def, v, scope); })
|
||
|
: parameterValue(def, value, scope);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse a single parameter value.
|
||
|
*/
|
||
|
function parameterValue(def, value, scope) {
|
||
|
var type = def.type;
|
||
|
|
||
|
if (isSignal(value)) {
|
||
|
return isExpr$1(type) ? vegaUtil.error('Expression references can not be signals.')
|
||
|
: isField(type) ? scope.fieldRef(value)
|
||
|
: isCompare(type) ? scope.compareRef(value)
|
||
|
: scope.signalRef(value.signal);
|
||
|
} else {
|
||
|
var expr = def.expr || isField(type);
|
||
|
return expr && outerExpr(value) ? scope.exprRef(value.expr, value.as)
|
||
|
: expr && outerField(value) ? fieldRef(value.field, value.as)
|
||
|
: isExpr$1(type) ? parseExpression(value, scope)
|
||
|
: isData(type) ? ref(scope.getData(value).values)
|
||
|
: isField(type) ? fieldRef(value)
|
||
|
: isCompare(type) ? scope.compareRef(value)
|
||
|
: value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse parameter for accessing an index of another data set.
|
||
|
*/
|
||
|
function parseIndexParameter(def, spec, scope) {
|
||
|
if (!vegaUtil.isString(spec.from)) {
|
||
|
vegaUtil.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) {
|
||
|
var value = spec[def.name];
|
||
|
|
||
|
if (def.array) {
|
||
|
if (!vegaUtil.isArray(value)) { // signals not allowed!
|
||
|
vegaUtil.error('Expected an array of sub-parameters. Instead: ' + vegaUtil.stringValue(value));
|
||
|
}
|
||
|
return value.map(function(v) {
|
||
|
return parseSubParameter(def, v, scope);
|
||
|
});
|
||
|
} else {
|
||
|
return parseSubParameter(def, value, scope);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse a sub-parameter object.
|
||
|
*/
|
||
|
function parseSubParameter(def, value, scope) {
|
||
|
var params, pdef, k, i, n;
|
||
|
|
||
|
// loop over defs to find matching key
|
||
|
for (i=0, n=def.params.length; i<n; ++i) {
|
||
|
pdef = def.params[i];
|
||
|
for (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) vegaUtil.error('Unsupported parameter: ' + vegaUtil.stringValue(value));
|
||
|
|
||
|
// parse params, create Params transform, return ref
|
||
|
params = vegaUtil.extend(parseParameters(pdef, value, scope), pdef.key);
|
||
|
return ref(scope.add(Params(params)));
|
||
|
}
|
||
|
|
||
|
// -- Utilities -----
|
||
|
|
||
|
function outerExpr(_) {
|
||
|
return _ && _.expr;
|
||
|
}
|
||
|
|
||
|
function outerField(_) {
|
||
|
return _ && _.field;
|
||
|
}
|
||
|
|
||
|
function isData(_) {
|
||
|
return _ === 'data';
|
||
|
}
|
||
|
|
||
|
function isExpr$1(_) {
|
||
|
return _ === 'expr';
|
||
|
}
|
||
|
|
||
|
function isField(_) {
|
||
|
return _ === 'field';
|
||
|
}
|
||
|
|
||
|
function isCompare(_) {
|
||
|
return _ === 'compare'
|
||
|
}
|
||
|
|
||
|
function parseData(from, group, scope) {
|
||
|
var 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) vegaUtil.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(vegaUtil.extend({
|
||
|
type: 'aggregate',
|
||
|
groupby: vegaUtil.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) {
|
||
|
var n = entries.length,
|
||
|
i = 1,
|
||
|
input = entries[0],
|
||
|
values = entries[n-1],
|
||
|
output = entries[n-2],
|
||
|
aggr = null;
|
||
|
|
||
|
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);
|
||
|
};
|
||
|
|
||
|
var prototype = DataScope.prototype;
|
||
|
|
||
|
prototype.countsRef = function(scope, field, sort) {
|
||
|
var ds = this,
|
||
|
cache = ds.counts || (ds.counts = {}),
|
||
|
k = fieldKey(field), 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;
|
||
|
};
|
||
|
|
||
|
function fieldKey(field) {
|
||
|
return vegaUtil.isString(field) ? field : null;
|
||
|
}
|
||
|
|
||
|
function addSortField(scope, p, sort) {
|
||
|
var as = aggrField(sort.op, sort.field), s;
|
||
|
|
||
|
if (p.ops) {
|
||
|
for (var 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) {
|
||
|
var cache = ds[name] || (ds[name] = {}),
|
||
|
sort = sortKey(counts),
|
||
|
k = fieldKey(field), v, op;
|
||
|
|
||
|
if (k != null) {
|
||
|
scope = ds.scope;
|
||
|
k = k + (sort ? '|' + sort : '');
|
||
|
v = cache[k];
|
||
|
}
|
||
|
|
||
|
if (!v) {
|
||
|
var 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;
|
||
|
}
|
||
|
|
||
|
prototype.tuplesRef = function() {
|
||
|
return ref(this.values);
|
||
|
};
|
||
|
|
||
|
prototype.extentRef = function(scope, field) {
|
||
|
return cache(scope, this, 'extent', 'extent', field, false);
|
||
|
};
|
||
|
|
||
|
prototype.domainRef = function(scope, field) {
|
||
|
return cache(scope, this, 'domain', 'values', field, false);
|
||
|
};
|
||
|
|
||
|
prototype.valuesRef = function(scope, field, sort) {
|
||
|
return cache(scope, this, 'vals', 'values', field, sort || true);
|
||
|
};
|
||
|
|
||
|
prototype.lookupRef = function(scope, field) {
|
||
|
return cache(scope, this, 'lookup', 'tupleindex', field, false);
|
||
|
};
|
||
|
|
||
|
prototype.indataRef = function(scope, field) {
|
||
|
return cache(scope, this, 'indata', 'tupleindex', field, true, true);
|
||
|
};
|
||
|
|
||
|
function parseFacet(spec, scope, group) {
|
||
|
var facet = spec.from.facet,
|
||
|
name = facet.name,
|
||
|
data = getDataRef(facet, scope),
|
||
|
subscope, source, values, op;
|
||
|
|
||
|
if (!facet.name) {
|
||
|
vegaUtil.error('Facet must have a name: ' + vegaUtil.stringValue(facet));
|
||
|
}
|
||
|
if (!facet.data) {
|
||
|
vegaUtil.error('Facet must reference a data set: ' + vegaUtil.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 {
|
||
|
vegaUtil.error('Facet must specify groupby or field: ' + vegaUtil.stringValue(facet));
|
||
|
}
|
||
|
|
||
|
// initialize facet subscope
|
||
|
subscope = scope.fork();
|
||
|
source = subscope.add(Collect());
|
||
|
values = subscope.add(Sieve({pulse: ref(source)}));
|
||
|
subscope.addData(name, new DataScope(subscope, source, source, values));
|
||
|
subscope.addSignal('parent', null);
|
||
|
|
||
|
// parse faceted subflow
|
||
|
op.params.subflow = {
|
||
|
$subflow: parseSpec(spec, subscope).toRuntime()
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function parseSubflow(spec, scope, input) {
|
||
|
var op = scope.add(PreFacet({pulse: input.pulse})),
|
||
|
subscope = scope.fork();
|
||
|
|
||
|
subscope.add(Sieve());
|
||
|
subscope.addSignal('parent', null);
|
||
|
|
||
|
// parse group mark subflow
|
||
|
op.params.subflow = {
|
||
|
$subflow: parseSpec(spec, subscope).toRuntime()
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function parseTrigger(spec, scope, name) {
|
||
|
var remove = spec.remove,
|
||
|
insert = spec.insert,
|
||
|
toggle = spec.toggle,
|
||
|
modify = spec.modify,
|
||
|
values = spec.values,
|
||
|
op = scope.add(operator()),
|
||
|
update, expr;
|
||
|
|
||
|
update = 'if(' + spec.trigger + ',modify("'
|
||
|
+ name + '",'
|
||
|
+ [insert, remove, toggle, modify, values]
|
||
|
.map(function(_) { return _ == null ? 'null' : _; })
|
||
|
.join(',')
|
||
|
+ '),0)';
|
||
|
|
||
|
expr = parseExpression(update, scope);
|
||
|
op.update = expr.$expr;
|
||
|
op.params = expr.$params;
|
||
|
}
|
||
|
|
||
|
function parseMark(spec, scope) {
|
||
|
var role = getRole(spec),
|
||
|
group = spec.type === GroupMark,
|
||
|
facet = spec.from && spec.from.facet,
|
||
|
layout = spec.layout || role === ScopeRole || role === FrameRole,
|
||
|
nested = role === MarkRole || layout || facet,
|
||
|
overlap = spec.overlap,
|
||
|
ops, op, input, store, enc, bound, render, sieve, name,
|
||
|
joinRef, markRef, encodeRef, layoutRef, boundRef;
|
||
|
|
||
|
// resolve input data
|
||
|
input = parseData(spec.from, group, scope);
|
||
|
|
||
|
// data join to map tuples to visual items
|
||
|
op = scope.add(DataJoin({
|
||
|
key: input.key || (spec.key ? fieldRef(spec.key) : undefined),
|
||
|
pulse: input.pulse,
|
||
|
clean: !group
|
||
|
}));
|
||
|
joinRef = ref(op);
|
||
|
|
||
|
// collect visual items
|
||
|
op = store = scope.add(Collect({pulse: joinRef}));
|
||
|
|
||
|
// connect visual items to scenegraph
|
||
|
op = scope.add(Mark({
|
||
|
markdef: definition(spec),
|
||
|
interactive: interactive(spec.interactive, scope),
|
||
|
clip: clip(spec.clip, scope),
|
||
|
context: {$context: true},
|
||
|
groups: scope.lookup(),
|
||
|
parent: scope.signals.parent ? scope.signalRef('parent') : null,
|
||
|
index: scope.markpath(),
|
||
|
pulse: ref(op)
|
||
|
}));
|
||
|
markRef = ref(op);
|
||
|
|
||
|
// add visual encoders
|
||
|
op = enc = scope.add(Encode(encoders(
|
||
|
spec.encode, spec.type, role, spec.style, scope,
|
||
|
{mod: false, pulse: markRef}
|
||
|
)));
|
||
|
|
||
|
// monitor parent marks to propagate changes
|
||
|
op.params.parent = scope.encode();
|
||
|
|
||
|
// add post-encoding transforms, if defined
|
||
|
if (spec.transform) {
|
||
|
spec.transform.forEach(function(_) {
|
||
|
const tx = parseTransform(_, scope),
|
||
|
md = tx.metadata;
|
||
|
if (md.generates || md.changes) {
|
||
|
vegaUtil.error('Mark transforms should not generate new data.');
|
||
|
}
|
||
|
if (!md.nomod) enc.params.mod = true; // update encode mod handling
|
||
|
tx.params.pulse = ref(op);
|
||
|
scope.add(op = tx);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// if item sort specified, perform post-encoding
|
||
|
if (spec.sort) {
|
||
|
op = scope.add(SortItems({
|
||
|
sort: scope.compareRef(spec.sort),
|
||
|
pulse: ref(op)
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
encodeRef = ref(op);
|
||
|
|
||
|
// add view layout operator if needed
|
||
|
if (facet || layout) {
|
||
|
layout = scope.add(ViewLayout({
|
||
|
layout: scope.objectProperty(spec.layout),
|
||
|
legends: scope.legends,
|
||
|
mark: markRef,
|
||
|
pulse: encodeRef
|
||
|
}));
|
||
|
layoutRef = ref(layout);
|
||
|
}
|
||
|
|
||
|
// compute bounding boxes
|
||
|
bound = scope.add(Bound({mark: markRef, pulse: layoutRef || encodeRef}));
|
||
|
boundRef = ref(bound);
|
||
|
|
||
|
// if group mark, recurse to parse nested content
|
||
|
if (group) {
|
||
|
// juggle layout & bounds to ensure they run *after* any faceting transforms
|
||
|
if (nested) { ops = scope.operators; ops.pop(); if (layout) ops.pop(); }
|
||
|
|
||
|
scope.pushState(encodeRef, layoutRef || boundRef, joinRef);
|
||
|
facet ? parseFacet(spec, scope, input) // explicit facet
|
||
|
: nested ? parseSubflow(spec, scope, input) // standard mark group
|
||
|
: parseSpec(spec, scope); // guide group, we can avoid nested scopes
|
||
|
scope.popState();
|
||
|
|
||
|
if (nested) { if (layout) ops.push(layout); ops.push(bound); }
|
||
|
}
|
||
|
|
||
|
// if requested, add overlap removal transform
|
||
|
if (overlap) {
|
||
|
boundRef = parseOverlap(overlap, boundRef, scope);
|
||
|
}
|
||
|
|
||
|
// render / sieve items
|
||
|
render = scope.add(Render({pulse: boundRef}));
|
||
|
sieve = scope.add(Sieve({pulse: ref(render)}, undefined, scope.parent()));
|
||
|
|
||
|
// if mark is named, make accessible as reactive geometry
|
||
|
// add trigger updates if defined
|
||
|
if (spec.name != null) {
|
||
|
name = spec.name;
|
||
|
scope.addData(name, new DataScope(scope, store, render, sieve));
|
||
|
if (spec.on) spec.on.forEach(function(on) {
|
||
|
if (on.insert || on.remove || on.toggle) {
|
||
|
vegaUtil.error('Marks only support modify triggers.');
|
||
|
}
|
||
|
parseTrigger(on, scope, name);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function parseOverlap(overlap, source, scope) {
|
||
|
var method = overlap.method,
|
||
|
bound = overlap.bound,
|
||
|
sep = overlap.separation, tol;
|
||
|
|
||
|
var params = {
|
||
|
separation: isSignal(sep) ? scope.signalRef(sep.signal) : sep,
|
||
|
method: isSignal(method) ? scope.signalRef(method.signal) : method,
|
||
|
pulse: source
|
||
|
};
|
||
|
|
||
|
if (overlap.order) {
|
||
|
params.sort = scope.compareRef({field: overlap.order});
|
||
|
}
|
||
|
|
||
|
if (bound) {
|
||
|
tol = bound.tolerance;
|
||
|
params.boundTolerance = isSignal(tol) ? scope.signalRef(tol.signal) : +tol;
|
||
|
params.boundScale = scope.scaleRef(bound.scale);
|
||
|
params.boundOrient = bound.orient;
|
||
|
}
|
||
|
|
||
|
return ref(scope.add(Overlap(params)));
|
||
|
}
|
||
|
|
||
|
function parseLegend(spec, scope) {
|
||
|
var config = scope.config.legend,
|
||
|
encode = spec.encode || {},
|
||
|
legendEncode = encode.legend || {},
|
||
|
name = legendEncode.name || undefined,
|
||
|
interactive = legendEncode.interactive,
|
||
|
style = legendEncode.style,
|
||
|
_ = lookup(spec, config),
|
||
|
entryEncode, entryLayout, params, children,
|
||
|
type, datum, dataRef, entryRef, group;
|
||
|
|
||
|
// resolve 'canonical' scale name
|
||
|
var scale = LegendScales.reduce(function(a, b) { return a || spec[b]; }, 0);
|
||
|
if (!scale) vegaUtil.error('Missing valid scale for legend.');
|
||
|
|
||
|
// resolve legend type (symbol, gradient, or discrete gradient)
|
||
|
type = legendType(spec, scope.scaleType(scale));
|
||
|
|
||
|
// single-element data source for legend group
|
||
|
datum = {
|
||
|
title: spec.title != null,
|
||
|
type: type,
|
||
|
vgrad: type !== 'symbol' && _.isVertical()
|
||
|
};
|
||
|
dataRef = ref(scope.add(Collect(null, [datum])));
|
||
|
|
||
|
// encoding properties for legend group
|
||
|
legendEncode = extendEncode(
|
||
|
buildLegendEncode(_, config), legendEncode, Skip
|
||
|
);
|
||
|
|
||
|
// encoding properties for legend entry sub-group
|
||
|
entryEncode = {enter: {x: {value: 0}, y: {value: 0}}};
|
||
|
|
||
|
// data source for legend values
|
||
|
entryRef = ref(scope.add(LegendEntries(params = {
|
||
|
type: type,
|
||
|
scale: scope.scaleRef(scale),
|
||
|
count: scope.objectProperty(_('tickCount')),
|
||
|
limit: scope.property(_('symbolLimit')),
|
||
|
values: scope.objectProperty(spec.values),
|
||
|
minstep: scope.property(spec.tickMinStep),
|
||
|
formatType: scope.property(spec.formatType),
|
||
|
formatSpecifier: scope.property(spec.format)
|
||
|
})));
|
||
|
|
||
|
// continuous gradient legend
|
||
|
if (type === Gradient) {
|
||
|
children = [
|
||
|
legendGradient(spec, scale, config, encode.gradient),
|
||
|
legendGradientLabels(spec, config, encode.labels, entryRef)
|
||
|
];
|
||
|
// adjust default tick count based on the gradient length
|
||
|
params.count = params.count || scope.signalRef(
|
||
|
`max(2,2*floor((${deref(_.gradientLength())})/100))`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// discrete gradient legend
|
||
|
else if (type === Discrete) {
|
||
|
children = [
|
||
|
legendGradientDiscrete(spec, scale, config, encode.gradient, entryRef),
|
||
|
legendGradientLabels(spec, config, encode.labels, entryRef)
|
||
|
];
|
||
|
}
|
||
|
|
||
|
// symbol legend
|
||
|
else {
|
||
|
// determine legend symbol group layout
|
||
|
entryLayout = legendSymbolLayout(spec, config);
|
||
|
children = [
|
||
|
legendSymbolGroups(spec, config, encode, entryRef, deref(entryLayout.columns))
|
||
|
];
|
||
|
// pass symbol size information to legend entry generator
|
||
|
params.size = sizeExpression(spec, scope, children[0].marks);
|
||
|
}
|
||
|
|
||
|
// generate legend marks
|
||
|
children = [
|
||
|
guideGroup(LegendEntryRole, null, null, dataRef, interactive,
|
||
|
entryEncode, children, entryLayout)
|
||
|
];
|
||
|
|
||
|
// include legend title if defined
|
||
|
if (datum.title) {
|
||
|
children.push(legendTitle(spec, config, encode.title, dataRef));
|
||
|
}
|
||
|
|
||
|
// build legend specification
|
||
|
group = guideGroup(LegendRole, style, name, dataRef, interactive, legendEncode, children);
|
||
|
if (spec.zindex) group.zindex = spec.zindex;
|
||
|
|
||
|
// parse legend specification
|
||
|
return parseMark(group, scope);
|
||
|
}
|
||
|
|
||
|
function legendType(spec, scaleType) {
|
||
|
var type = spec.type || Symbols;
|
||
|
|
||
|
if (!spec.type && scaleCount(spec) === 1 && (spec.fill || spec.stroke)) {
|
||
|
type = vegaScale.isContinuous(scaleType) ? Gradient
|
||
|
: vegaScale.isDiscretizing(scaleType) ? Discrete
|
||
|
: Symbols;
|
||
|
}
|
||
|
|
||
|
return type !== Gradient ? type
|
||
|
: vegaScale.isDiscretizing(scaleType) ? Discrete
|
||
|
: Gradient;
|
||
|
}
|
||
|
|
||
|
function scaleCount(spec) {
|
||
|
return LegendScales.reduce(function(count, type) {
|
||
|
return count + (spec[type] ? 1 : 0);
|
||
|
}, 0);
|
||
|
}
|
||
|
|
||
|
function buildLegendEncode(_, config) {
|
||
|
var encode = {enter: {}, update: {}};
|
||
|
|
||
|
addEncoders(encode, {
|
||
|
orient: _('orient'),
|
||
|
offset: _('offset'),
|
||
|
padding: _('padding'),
|
||
|
titlePadding: _('titlePadding'),
|
||
|
cornerRadius: _('cornerRadius'),
|
||
|
fill: _('fillColor'),
|
||
|
stroke: _('strokeColor'),
|
||
|
strokeWidth: config.strokeWidth,
|
||
|
strokeDash: config.strokeDash,
|
||
|
x: _('legendX'),
|
||
|
y: _('legendY'),
|
||
|
});
|
||
|
|
||
|
return encode;
|
||
|
}
|
||
|
|
||
|
function sizeExpression(spec, scope, marks) {
|
||
|
var size = deref(getChannel('size', spec, marks)),
|
||
|
strokeWidth = deref(getChannel('strokeWidth', spec, marks)),
|
||
|
fontSize = deref(getFontSize(marks[1].encode, scope, GuideLabelStyle));
|
||
|
|
||
|
return parseExpression(
|
||
|
`max(ceil(sqrt(${size})+${strokeWidth}),${fontSize})`,
|
||
|
scope
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function getChannel(name, spec, marks) {
|
||
|
return spec[name]
|
||
|
? `scale("${spec[name]}",datum)`
|
||
|
: getEncoding(name, marks[0].encode);
|
||
|
}
|
||
|
|
||
|
function getFontSize(encode, scope, style) {
|
||
|
return getEncoding('fontSize', encode) || getStyle('fontSize', scope, style);
|
||
|
}
|
||
|
|
||
|
const angleExpr = `item.orient==="${Left}"?-90:item.orient==="${Right}"?90:0`;
|
||
|
|
||
|
function parseTitle(spec, scope) {
|
||
|
spec = vegaUtil.isString(spec) ? {text: spec} : spec;
|
||
|
|
||
|
var _ = lookup(spec, scope.config.title),
|
||
|
encode = spec.encode || {},
|
||
|
userEncode = encode.group || {},
|
||
|
name = userEncode.name || undefined,
|
||
|
interactive = userEncode.interactive,
|
||
|
style = userEncode.style,
|
||
|
children = [],
|
||
|
dataRef, group;
|
||
|
|
||
|
// single-element data source for group title
|
||
|
dataRef = ref(scope.add(Collect(null, [{}])));
|
||
|
|
||
|
// include title text
|
||
|
children.push(buildTitle(spec, _, titleEncode(spec), dataRef));
|
||
|
|
||
|
// include subtitle text
|
||
|
if (spec.subtitle) {
|
||
|
children.push(buildSubTitle(spec, _, encode.subtitle, dataRef));
|
||
|
}
|
||
|
|
||
|
// build title specification
|
||
|
group = guideGroup(TitleRole, style, name, dataRef, interactive,
|
||
|
groupEncode(_, userEncode), children);
|
||
|
if (spec.zindex) group.zindex = spec.zindex;
|
||
|
|
||
|
// parse title specification
|
||
|
return parseMark(group, scope);
|
||
|
}
|
||
|
|
||
|
// provide backwards-compatibility for title custom encode;
|
||
|
// the top-level encode block has been *deprecated*.
|
||
|
function titleEncode(spec) {
|
||
|
const encode = spec.encode;
|
||
|
return (encode && encode.title) || vegaUtil.extend({
|
||
|
name: spec.name,
|
||
|
interactive: spec.interactive,
|
||
|
style: spec.style
|
||
|
}, encode);
|
||
|
}
|
||
|
|
||
|
function groupEncode(_, userEncode) {
|
||
|
var encode = {enter: {}, update: {}};
|
||
|
|
||
|
addEncoders(encode, {
|
||
|
orient: _('orient'),
|
||
|
anchor: _('anchor'),
|
||
|
align: {signal: alignExpr},
|
||
|
angle: {signal: angleExpr},
|
||
|
limit: _('limit'),
|
||
|
frame: _('frame'),
|
||
|
offset: _('offset') || 0,
|
||
|
padding: _('subtitlePadding')
|
||
|
});
|
||
|
|
||
|
return extendEncode(encode, userEncode, Skip);
|
||
|
}
|
||
|
|
||
|
function buildTitle(spec, _, userEncode, dataRef) {
|
||
|
var zero = {value: 0},
|
||
|
text = spec.text,
|
||
|
encode = {
|
||
|
enter: {opacity: zero},
|
||
|
update: {opacity: {value: 1}},
|
||
|
exit: {opacity: zero}
|
||
|
};
|
||
|
|
||
|
addEncoders(encode, {
|
||
|
text: text,
|
||
|
align: {signal: 'item.mark.group.align'},
|
||
|
angle: {signal: 'item.mark.group.angle'},
|
||
|
limit: {signal: 'item.mark.group.limit'},
|
||
|
baseline: 'top',
|
||
|
dx: _('dx'),
|
||
|
dy: _('dy'),
|
||
|
fill: _('color'),
|
||
|
font: _('font'),
|
||
|
fontSize: _('fontSize'),
|
||
|
fontStyle: _('fontStyle'),
|
||
|
fontWeight: _('fontWeight'),
|
||
|
lineHeight: _('lineHeight')
|
||
|
}, { // update
|
||
|
align: _('align'),
|
||
|
angle: _('angle'),
|
||
|
baseline: _('baseline')
|
||
|
});
|
||
|
|
||
|
return guideMark(TextMark, TitleTextRole, GroupTitleStyle,
|
||
|
null, dataRef, encode, userEncode);
|
||
|
}
|
||
|
|
||
|
function buildSubTitle(spec, _, userEncode, dataRef) {
|
||
|
var zero = {value: 0},
|
||
|
text = spec.subtitle,
|
||
|
encode = {
|
||
|
enter: {opacity: zero},
|
||
|
update: {opacity: {value: 1}},
|
||
|
exit: {opacity: zero}
|
||
|
};
|
||
|
|
||
|
addEncoders(encode, {
|
||
|
text: text,
|
||
|
align: {signal: 'item.mark.group.align'},
|
||
|
angle: {signal: 'item.mark.group.angle'},
|
||
|
limit: {signal: 'item.mark.group.limit'},
|
||
|
baseline: 'top',
|
||
|
dx: _('dx'),
|
||
|
dy: _('dy'),
|
||
|
fill: _('subtitleColor'),
|
||
|
font: _('subtitleFont'),
|
||
|
fontSize: _('subtitleFontSize'),
|
||
|
fontStyle: _('subtitleFontStyle'),
|
||
|
fontWeight: _('subtitleFontWeight'),
|
||
|
lineHeight: _('subtitleLineHeight')
|
||
|
}, { // update
|
||
|
align: _('align'),
|
||
|
angle: _('angle'),
|
||
|
baseline: _('baseline')
|
||
|
});
|
||
|
|
||
|
return guideMark(TextMark, TitleSubtitleRole, GroupSubtitleStyle,
|
||
|
null, dataRef, encode, userEncode);
|
||
|
}
|
||
|
|
||
|
function parseData$1(data, scope) {
|
||
|
var transforms = [];
|
||
|
|
||
|
if (data.transform) {
|
||
|
data.transform.forEach(function(tx) {
|
||
|
transforms.push(parseTransform(tx, scope));
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (data.on) {
|
||
|
data.on.forEach(function(on) {
|
||
|
parseTrigger(on, scope, data.name);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
scope.addDataPipeline(data.name, analyze(data, scope, transforms));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Analyze a data pipeline, add needed operators.
|
||
|
*/
|
||
|
function analyze(data, scope, ops) {
|
||
|
var output = [],
|
||
|
source = null,
|
||
|
modify = false,
|
||
|
generate = false,
|
||
|
upstream, i, n, t, m;
|
||
|
|
||
|
if (data.values) {
|
||
|
// hard-wired input data set
|
||
|
if (hasSignal(data.values) || hasSignal(data.format)) {
|
||
|
// if either values or format has signal, use dynamic loader
|
||
|
output.push(load(scope, data));
|
||
|
output.push(source = collect());
|
||
|
} else {
|
||
|
// otherwise, ingest upon dataflow init
|
||
|
output.push(source = collect({
|
||
|
$ingest: data.values,
|
||
|
$format: data.format
|
||
|
}));
|
||
|
}
|
||
|
} else if (data.url) {
|
||
|
// load data from external source
|
||
|
if (hasSignal(data.url) || hasSignal(data.format)) {
|
||
|
// if either url or format has signal, use dynamic loader
|
||
|
output.push(load(scope, data));
|
||
|
output.push(source = collect());
|
||
|
} else {
|
||
|
// otherwise, request load upon dataflow init
|
||
|
output.push(source = collect({
|
||
|
$request: data.url,
|
||
|
$format: data.format
|
||
|
}));
|
||
|
}
|
||
|
} else if (data.source) {
|
||
|
// derives from one or more other data sets
|
||
|
source = upstream = vegaUtil.array(data.source).map(function(d) {
|
||
|
return ref(scope.getData(d).output);
|
||
|
});
|
||
|
output.push(null); // populate later
|
||
|
}
|
||
|
|
||
|
// scan data transforms, add collectors as needed
|
||
|
for (i=0, n=ops.length; i<n; ++i) {
|
||
|
t = ops[i];
|
||
|
m = t.metadata;
|
||
|
|
||
|
if (!source && !m.source) {
|
||
|
output.push(source = collect());
|
||
|
}
|
||
|
output.push(t);
|
||
|
|
||
|
if (m.generates) generate = true;
|
||
|
if (m.modifies && !generate) modify = true;
|
||
|
|
||
|
if (m.source) source = t;
|
||
|
else if (m.changes) source = null;
|
||
|
}
|
||
|
|
||
|
if (upstream) {
|
||
|
n = upstream.length - 1;
|
||
|
output[0] = Relay({
|
||
|
derive: modify,
|
||
|
pulse: n ? upstream : upstream[0]
|
||
|
});
|
||
|
if (modify || n) {
|
||
|
// collect derived and multi-pulse tuples
|
||
|
output.splice(1, 0, collect());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!source) output.push(collect());
|
||
|
output.push(Sieve({}));
|
||
|
return output;
|
||
|
}
|
||
|
|
||
|
function collect(values) {
|
||
|
var s = Collect({}, values);
|
||
|
s.metadata = {source: true};
|
||
|
return s;
|
||
|
}
|
||
|
|
||
|
function load(scope, data) {
|
||
|
return Load({
|
||
|
url: data.url ? scope.property(data.url) : undefined,
|
||
|
async: data.async ? scope.property(data.async) : undefined,
|
||
|
values: data.values ? scope.property(data.values) : undefined,
|
||
|
format: scope.objectProperty(data.format)
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function axisConfig(spec, scope) {
|
||
|
var config = scope.config,
|
||
|
orient = spec.orient,
|
||
|
xy = (orient === Top || orient === Bottom) ? config.axisX : config.axisY,
|
||
|
or = config['axis' + orient[0].toUpperCase() + orient.slice(1)],
|
||
|
band = scope.scaleType(spec.scale) === 'band' && config.axisBand;
|
||
|
|
||
|
return (xy || or || band)
|
||
|
? vegaUtil.extend({}, config.axis, xy, or, band)
|
||
|
: config.axis;
|
||
|
}
|
||
|
|
||
|
function axisDomain(spec, config, userEncode, dataRef) {
|
||
|
var _ = lookup(spec, config),
|
||
|
orient = spec.orient,
|
||
|
encode, enter, update, u, u2, v;
|
||
|
|
||
|
encode = {
|
||
|
enter: enter = {opacity: zero},
|
||
|
update: update = {opacity: one},
|
||
|
exit: {opacity: zero}
|
||
|
};
|
||
|
|
||
|
addEncoders(encode, {
|
||
|
stroke: _('domainColor'),
|
||
|
strokeDash: _('domainDash'),
|
||
|
strokeDashOffset: _('domainDashOffset'),
|
||
|
strokeWidth: _('domainWidth'),
|
||
|
strokeOpacity: _('domainOpacity')
|
||
|
});
|
||
|
|
||
|
if (orient === Top || orient === Bottom) {
|
||
|
u = 'x';
|
||
|
v = 'y';
|
||
|
} else {
|
||
|
u = 'y';
|
||
|
v = 'x';
|
||
|
}
|
||
|
u2 = u + '2';
|
||
|
|
||
|
enter[v] = zero;
|
||
|
update[u] = enter[u] = position(spec, 0);
|
||
|
update[u2] = enter[u2] = position(spec, 1);
|
||
|
|
||
|
return guideMark(RuleMark, AxisDomainRole, null, null, dataRef, encode, userEncode);
|
||
|
}
|
||
|
|
||
|
function position(spec, pos) {
|
||
|
return {scale: spec.scale, range: pos};
|
||
|
}
|
||
|
|
||
|
function axisGrid(spec, config, userEncode, dataRef, band) {
|
||
|
var _ = lookup(spec, config),
|
||
|
orient = spec.orient,
|
||
|
vscale = spec.gridScale,
|
||
|
sign = (orient === Left || orient === Top) ? 1 : -1,
|
||
|
offset = offsetValue(spec.offset, sign),
|
||
|
encode, enter, exit, update, tickPos, u, v, v2, s;
|
||
|
|
||
|
encode = {
|
||
|
enter: enter = {opacity: zero},
|
||
|
update: update = {opacity: one},
|
||
|
exit: exit = {opacity: zero}
|
||
|
};
|
||
|
|
||
|
addEncoders(encode, {
|
||
|
stroke: _('gridColor'),
|
||
|
strokeDash: _('gridDash'),
|
||
|
strokeDashOffset: _('gridDashOffset'),
|
||
|
strokeOpacity: _('gridOpacity'),
|
||
|
strokeWidth: _('gridWidth')
|
||
|
});
|
||
|
|
||
|
tickPos = {
|
||
|
scale: spec.scale,
|
||
|
field: Value,
|
||
|
band: band.band,
|
||
|
extra: band.extra,
|
||
|
offset: band.offset,
|
||
|
round: _('tickRound')
|
||
|
};
|
||
|
|
||
|
if (orient === Top || orient === Bottom) {
|
||
|
u = 'x';
|
||
|
v = 'y';
|
||
|
s = 'height';
|
||
|
} else {
|
||
|
u = 'y';
|
||
|
v = 'x';
|
||
|
s = 'width';
|
||
|
}
|
||
|
v2 = v + '2';
|
||
|
|
||
|
update[u] = enter[u] = exit[u] = tickPos;
|
||
|
|
||
|
if (vscale) {
|
||
|
update[v] = enter[v] = {scale: vscale, range: 0, mult: sign, offset: offset};
|
||
|
update[v2] = enter[v2] = {scale: vscale, range: 1, mult: sign, offset: offset};
|
||
|
} else {
|
||
|
update[v] = enter[v] = {value: 0, offset: offset};
|
||
|
update[v2] = enter[v2] = {signal: s, mult: sign, offset: offset};
|
||
|
}
|
||
|
|
||
|
return guideMark(RuleMark, AxisGridRole, null, Value, dataRef, encode, userEncode);
|
||
|
}
|
||
|
|
||
|
function offsetValue(offset, sign) {
|
||
|
if (sign === 1) ; else if (!vegaUtil.isObject(offset)) {
|
||
|
offset = sign * (offset || 0);
|
||
|
} else {
|
||
|
var entry = offset = vegaUtil.extend({}, offset);
|
||
|
|
||
|
while (entry.mult != null) {
|
||
|
if (!vegaUtil.isObject(entry.mult)) {
|
||
|
entry.mult *= sign;
|
||
|
return offset;
|
||
|
} else {
|
||
|
entry = entry.mult = vegaUtil.extend({}, entry.mult);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
entry.mult = sign;
|
||
|
}
|
||
|
|
||
|
return offset;
|
||
|
}
|
||
|
|
||
|
function axisTicks(spec, config, userEncode, dataRef, size, band) {
|
||
|
var _ = lookup(spec, config),
|
||
|
orient = spec.orient,
|
||
|
sign = (orient === Left || orient === Top) ? -1 : 1,
|
||
|
encode, enter, exit, update, tickSize, tickPos;
|
||
|
|
||
|
encode = {
|
||
|
enter: enter = {opacity: zero},
|
||
|
update: update = {opacity: one},
|
||
|
exit: exit = {opacity: zero}
|
||
|
};
|
||
|
|
||
|
addEncoders(encode, {
|
||
|
stroke: _('tickColor'),
|
||
|
strokeDash: _('tickDash'),
|
||
|
strokeDashOffset: _('tickDashOffset'),
|
||
|
strokeOpacity: _('tickOpacity'),
|
||
|
strokeWidth: _('tickWidth')
|
||
|
});
|
||
|
|
||
|
tickSize = encoder(size);
|
||
|
tickSize.mult = sign;
|
||
|
|
||
|
tickPos = {
|
||
|
scale: spec.scale,
|
||
|
field: Value,
|
||
|
band: band.band,
|
||
|
extra: band.extra,
|
||
|
offset: band.offset,
|
||
|
round: _('tickRound')
|
||
|
};
|
||
|
|
||
|
if (orient === Top || orient === Bottom) {
|
||
|
update.y = enter.y = zero;
|
||
|
update.y2 = enter.y2 = tickSize;
|
||
|
update.x = enter.x = exit.x = tickPos;
|
||
|
} else {
|
||
|
update.x = enter.x = zero;
|
||
|
update.x2 = enter.x2 = tickSize;
|
||
|
update.y = enter.y = exit.y = tickPos;
|
||
|
}
|
||
|
|
||
|
return guideMark(RuleMark, AxisTickRole, null, Value, dataRef, encode, userEncode);
|
||
|
}
|
||
|
|
||
|
function flushExpr(scale, threshold, a, b, c) {
|
||
|
return {
|
||
|
signal: 'flush(range("' + scale + '"), '
|
||
|
+ 'scale("' + scale + '", datum.value), '
|
||
|
+ threshold + ',' + a + ',' + b + ',' + c + ')'
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function axisLabels(spec, config, userEncode, dataRef, size, band) {
|
||
|
var _ = lookup(spec, config),
|
||
|
orient = spec.orient,
|
||
|
sign = (orient === Left || orient === Top) ? -1 : 1,
|
||
|
isXAxis = (orient === Top || orient === Bottom),
|
||
|
scale = spec.scale,
|
||
|
flush = deref(_('labelFlush')),
|
||
|
flushOffset = deref(_('labelFlushOffset')),
|
||
|
flushOn = flush === 0 || !!flush,
|
||
|
labelAlign = _('labelAlign'),
|
||
|
labelBaseline = _('labelBaseline'),
|
||
|
encode, enter, tickSize, tickPos, align, baseline, offset,
|
||
|
bound, overlap, separation;
|
||
|
|
||
|
tickSize = encoder(size);
|
||
|
tickSize.mult = sign;
|
||
|
tickSize.offset = encoder(_('labelPadding') || 0);
|
||
|
tickSize.offset.mult = sign;
|
||
|
|
||
|
tickPos = {
|
||
|
scale: scale,
|
||
|
field: Value,
|
||
|
band: 0.5,
|
||
|
offset: band.offset
|
||
|
};
|
||
|
|
||
|
if (isXAxis) {
|
||
|
align = labelAlign || (flushOn
|
||
|
? flushExpr(scale, flush, '"left"', '"right"', '"center"')
|
||
|
: 'center');
|
||
|
baseline = labelBaseline || (orient === Top ? 'bottom' : 'top');
|
||
|
offset = !labelAlign;
|
||
|
} else {
|
||
|
align = labelAlign || (orient === Right ? 'left' : 'right');
|
||
|
baseline = labelBaseline || (flushOn
|
||
|
? flushExpr(scale, flush, '"top"', '"bottom"', '"middle"')
|
||
|
: 'middle');
|
||
|
offset = !labelBaseline;
|
||
|
}
|
||
|
|
||
|
offset = offset && flushOn && flushOffset
|
||
|
? flushExpr(scale, flush, '-(' + flushOffset + ')', flushOffset, 0)
|
||
|
: null;
|
||
|
|
||
|
encode = {
|
||
|
enter: enter = {
|
||
|
opacity: zero,
|
||
|
x: isXAxis ? tickPos : tickSize,
|
||
|
y: isXAxis ? tickSize : tickPos
|
||
|
},
|
||
|
update: {
|
||
|
opacity: one,
|
||
|
text: {field: Label},
|
||
|
x: enter.x,
|
||
|
y: enter.y
|
||
|
},
|
||
|
exit: {
|
||
|
opacity: zero,
|
||
|
x: enter.x,
|
||
|
y: enter.y
|
||
|
}
|
||
|
};
|
||
|
|
||
|
addEncoders(encode, {
|
||
|
[isXAxis ? 'dx' : 'dy']: offset,
|
||
|
align: align,
|
||
|
baseline: baseline,
|
||
|
angle: _('labelAngle'),
|
||
|
fill: _('labelColor'),
|
||
|
fillOpacity: _('labelOpacity'),
|
||
|
font: _('labelFont'),
|
||
|
fontSize: _('labelFontSize'),
|
||
|
fontWeight: _('labelFontWeight'),
|
||
|
fontStyle: _('labelFontStyle'),
|
||
|
limit: _('labelLimit')
|
||
|
});
|
||
|
|
||
|
bound = _('labelBound');
|
||
|
overlap = _('labelOverlap');
|
||
|
separation = _('labelSeparation');
|
||
|
|
||
|
spec = guideMark(TextMark, AxisLabelRole, GuideLabelStyle, Value, dataRef, encode, userEncode);
|
||
|
|
||
|
// if overlap method or bound defined, request label overlap removal
|
||
|
if (overlap || bound) {
|
||
|
spec.overlap = {
|
||
|
separation: separation,
|
||
|
method: overlap,
|
||
|
order: 'datum.index',
|
||
|
bound: bound ? {scale: scale, orient: orient, tolerance: bound} : null
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return spec;
|
||
|
}
|
||
|
|
||
|
function axisTitle(spec, config, userEncode, dataRef) {
|
||
|
var _ = lookup(spec, config),
|
||
|
orient = spec.orient,
|
||
|
sign = (orient === Left || orient === Top) ? -1 : 1,
|
||
|
horizontal = (orient === Top || orient === Bottom),
|
||
|
encode, enter, update, titlePos;
|
||
|
|
||
|
encode = {
|
||
|
enter: enter = {
|
||
|
opacity: zero,
|
||
|
anchor: encoder(_('titleAnchor')),
|
||
|
align: {signal: alignExpr}
|
||
|
},
|
||
|
update: update = vegaUtil.extend({}, enter, {
|
||
|
opacity: one,
|
||
|
text: encoder(spec.title)
|
||
|
}),
|
||
|
exit: {
|
||
|
opacity: zero
|
||
|
}
|
||
|
};
|
||
|
|
||
|
titlePos = {
|
||
|
signal: `lerp(range("${spec.scale}"), ${anchorExpr(0, 1, 0.5)})`
|
||
|
};
|
||
|
|
||
|
if (horizontal) {
|
||
|
update.x = titlePos;
|
||
|
enter.angle = {value: 0};
|
||
|
enter.baseline = {value: orient === Top ? 'bottom' : 'top'};
|
||
|
} else {
|
||
|
update.y = titlePos;
|
||
|
enter.angle = {value: sign * 90};
|
||
|
enter.baseline = {value: 'bottom'};
|
||
|
}
|
||
|
|
||
|
addEncoders(encode, {
|
||
|
angle: _('titleAngle'),
|
||
|
baseline: _('titleBaseline'),
|
||
|
fill: _('titleColor'),
|
||
|
fillOpacity: _('titleOpacity'),
|
||
|
font: _('titleFont'),
|
||
|
fontSize: _('titleFontSize'),
|
||
|
fontStyle: _('titleFontStyle'),
|
||
|
fontWeight: _('titleFontWeight'),
|
||
|
limit: _('titleLimit'),
|
||
|
lineHeight: _('titleLineHeight')
|
||
|
}, { // require update
|
||
|
align: _('titleAlign')
|
||
|
});
|
||
|
|
||
|
!addEncode(encode, 'x', _('titleX'), 'update')
|
||
|
&& !horizontal && !has('x', userEncode)
|
||
|
&& (encode.enter.auto = {value: true});
|
||
|
|
||
|
!addEncode(encode, 'y', _('titleY'), 'update')
|
||
|
&& horizontal && !has('y', userEncode)
|
||
|
&& (encode.enter.auto = {value: true});
|
||
|
|
||
|
return guideMark(TextMark, AxisTitleRole, GuideTitleStyle, null, dataRef, encode, userEncode);
|
||
|
}
|
||
|
|
||
|
function parseAxis(spec, scope) {
|
||
|
var config = axisConfig(spec, scope),
|
||
|
encode = spec.encode || {},
|
||
|
axisEncode = encode.axis || {},
|
||
|
name = axisEncode.name || undefined,
|
||
|
interactive = axisEncode.interactive,
|
||
|
style = axisEncode.style,
|
||
|
_ = lookup(spec, config),
|
||
|
band = tickBand(_),
|
||
|
datum, dataRef, ticksRef, size, group, children;
|
||
|
|
||
|
// single-element data source for axis group
|
||
|
datum = {
|
||
|
orient: spec.orient,
|
||
|
ticks: !!_('ticks'),
|
||
|
labels: !!_('labels'),
|
||
|
grid: !!_('grid'),
|
||
|
domain: !!_('domain'),
|
||
|
title: spec.title != null,
|
||
|
translate: _('translate')
|
||
|
};
|
||
|
dataRef = ref(scope.add(Collect({}, [datum])));
|
||
|
|
||
|
// encoding properties for axis group item
|
||
|
axisEncode = extendEncode({
|
||
|
update: {
|
||
|
offset: encoder(_('offset') || 0),
|
||
|
position: encoder(value(spec.position, 0)),
|
||
|
titlePadding: encoder(_('titlePadding')),
|
||
|
minExtent: encoder(_('minExtent')),
|
||
|
maxExtent: encoder(_('maxExtent')),
|
||
|
range: {signal: `abs(span(range("${spec.scale}")))`}
|
||
|
}
|
||
|
}, encode.axis, Skip);
|
||
|
|
||
|
// data source for axis ticks
|
||
|
ticksRef = ref(scope.add(AxisTicks({
|
||
|
scale: scope.scaleRef(spec.scale),
|
||
|
extra: scope.property(band.extra),
|
||
|
count: scope.objectProperty(spec.tickCount),
|
||
|
values: scope.objectProperty(spec.values),
|
||
|
minstep: scope.property(spec.tickMinStep),
|
||
|
formatType: scope.property(spec.formatType),
|
||
|
formatSpecifier: scope.property(spec.format)
|
||
|
})));
|
||
|
|
||
|
// generate axis marks
|
||
|
children = [];
|
||
|
|
||
|
// include axis gridlines if requested
|
||
|
if (datum.grid) {
|
||
|
children.push(axisGrid(spec, config, encode.grid, ticksRef, band));
|
||
|
}
|
||
|
|
||
|
// include axis ticks if requested
|
||
|
if (datum.ticks) {
|
||
|
size = _('tickSize');
|
||
|
children.push(axisTicks(spec, config, encode.ticks, ticksRef, size, band));
|
||
|
}
|
||
|
|
||
|
// include axis labels if requested
|
||
|
if (datum.labels) {
|
||
|
size = datum.ticks ? size : 0;
|
||
|
children.push(axisLabels(spec, config, encode.labels, ticksRef, size, band));
|
||
|
}
|
||
|
|
||
|
// include axis domain path if requested
|
||
|
if (datum.domain) {
|
||
|
children.push(axisDomain(spec, config, encode.domain, dataRef));
|
||
|
}
|
||
|
|
||
|
// include axis title if defined
|
||
|
if (datum.title) {
|
||
|
children.push(axisTitle(spec, config, encode.title, dataRef));
|
||
|
}
|
||
|
|
||
|
// build axis specification
|
||
|
group = guideGroup(AxisRole, style, name, dataRef, interactive, axisEncode, children);
|
||
|
if (spec.zindex) group.zindex = spec.zindex;
|
||
|
|
||
|
// parse axis specification
|
||
|
return parseMark(group, scope);
|
||
|
}
|
||
|
|
||
|
function parseSpec(spec, scope, preprocessed) {
|
||
|
var signals = vegaUtil.array(spec.signals),
|
||
|
scales = vegaUtil.array(spec.scales);
|
||
|
|
||
|
// parse signal definitions, if not already preprocessed
|
||
|
if (!preprocessed) signals.forEach(_ => parseSignal(_, scope));
|
||
|
|
||
|
// parse cartographic projection definitions
|
||
|
vegaUtil.array(spec.projections).forEach(_ => parseProjection(_, scope));
|
||
|
|
||
|
// initialize scale references
|
||
|
scales.forEach(_ => initScale(_, scope));
|
||
|
|
||
|
// parse data sources
|
||
|
vegaUtil.array(spec.data).forEach(_ => parseData$1(_, scope));
|
||
|
|
||
|
// parse scale definitions
|
||
|
scales.forEach(_ => parseScale(_, scope));
|
||
|
|
||
|
// parse signal updates
|
||
|
(preprocessed || signals).forEach(_ => parseSignalUpdates(_, scope));
|
||
|
|
||
|
// parse axis definitions
|
||
|
vegaUtil.array(spec.axes).forEach(_ => parseAxis(_, scope));
|
||
|
|
||
|
// parse mark definitions
|
||
|
vegaUtil.array(spec.marks).forEach(_ => parseMark(_, scope));
|
||
|
|
||
|
// parse legend definitions
|
||
|
vegaUtil.array(spec.legends).forEach(_ => parseLegend(_, scope));
|
||
|
|
||
|
// parse title, if defined
|
||
|
if (spec.title) parseTitle(spec.title, scope);
|
||
|
|
||
|
// parse collected lambda (anonymous) expressions
|
||
|
scope.parseLambdas();
|
||
|
|
||
|
return scope;
|
||
|
}
|
||
|
|
||
|
var defined = vegaUtil.toSet(['width', 'height', 'padding', 'autosize']);
|
||
|
|
||
|
function parseView(spec, scope) {
|
||
|
var config = scope.config,
|
||
|
op, input, encode, parent, root, signals;
|
||
|
|
||
|
scope.background = spec.background || config.background;
|
||
|
scope.eventConfig = config.events;
|
||
|
root = ref(scope.root = scope.add(operator()));
|
||
|
scope.addSignal('width', spec.width || 0);
|
||
|
scope.addSignal('height', spec.height || 0);
|
||
|
scope.addSignal('padding', parsePadding(spec.padding, config));
|
||
|
scope.addSignal('autosize', parseAutosize(spec.autosize, config));
|
||
|
scope.legends = scope.objectProperty(config.legend && config.legend.layout);
|
||
|
|
||
|
// parse signal definitions, including config entries
|
||
|
signals = addSignals(scope, spec.signals, config.signals);
|
||
|
|
||
|
// Store root group item
|
||
|
input = scope.add(Collect());
|
||
|
|
||
|
// Encode root group item
|
||
|
encode = extendEncode({
|
||
|
enter: { x: {value: 0}, y: {value: 0} },
|
||
|
update: { width: {signal: 'width'}, height: {signal: 'height'} }
|
||
|
}, spec.encode);
|
||
|
|
||
|
encode = scope.add(Encode(
|
||
|
encoders(encode, GroupMark, FrameRole, spec.style, scope, {pulse: ref(input)}))
|
||
|
);
|
||
|
|
||
|
// Perform view layout
|
||
|
parent = scope.add(ViewLayout({
|
||
|
layout: scope.objectProperty(spec.layout),
|
||
|
legends: scope.legends,
|
||
|
autosize: scope.signalRef('autosize'),
|
||
|
mark: root,
|
||
|
pulse: ref(encode)
|
||
|
}));
|
||
|
scope.operators.pop();
|
||
|
|
||
|
// Parse remainder of specification
|
||
|
scope.pushState(ref(encode), ref(parent), null);
|
||
|
parseSpec(spec, scope, signals);
|
||
|
scope.operators.push(parent);
|
||
|
|
||
|
// Bound / render / sieve root item
|
||
|
op = scope.add(Bound({mark: root, pulse: ref(parent)}));
|
||
|
op = scope.add(Render({pulse: ref(op)}));
|
||
|
op = scope.add(Sieve({pulse: ref(op)}));
|
||
|
|
||
|
// Track metadata for root item
|
||
|
scope.addData('root', new DataScope(scope, input, input, op));
|
||
|
|
||
|
return scope;
|
||
|
}
|
||
|
|
||
|
function addSignals(scope, signals, config) {
|
||
|
// signals defined in the spec take priority
|
||
|
vegaUtil.array(signals).forEach(_ => {
|
||
|
if (!defined[_.name]) parseSignal(_, scope);
|
||
|
});
|
||
|
|
||
|
if (!config) return signals;
|
||
|
const out = vegaUtil.array(signals).slice();
|
||
|
|
||
|
// add config signals if not already defined
|
||
|
vegaUtil.array(config).forEach(_ => {
|
||
|
if (!scope.hasOwnSignal(_.name)) {
|
||
|
parseSignal(_, scope);
|
||
|
out.push(_);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return out;
|
||
|
}
|
||
|
|
||
|
function Scope$1(config) {
|
||
|
this.config = config;
|
||
|
|
||
|
this.bindings = [];
|
||
|
this.field = {};
|
||
|
this.signals = {};
|
||
|
this.lambdas = {};
|
||
|
this.scales = {};
|
||
|
this.events = {};
|
||
|
this.data = {};
|
||
|
|
||
|
this.streams = [];
|
||
|
this.updates = [];
|
||
|
this.operators = [];
|
||
|
this.background = null;
|
||
|
this.eventConfig = null;
|
||
|
|
||
|
this._id = 0;
|
||
|
this._subid = 0;
|
||
|
this._nextsub = [0];
|
||
|
|
||
|
this._parent = [];
|
||
|
this._encode = [];
|
||
|
this._lookup = [];
|
||
|
this._markpath = [];
|
||
|
}
|
||
|
|
||
|
function Subscope(scope) {
|
||
|
this.config = scope.config;
|
||
|
this.legends = scope.legends;
|
||
|
|
||
|
this.field = Object.create(scope.field);
|
||
|
this.signals = Object.create(scope.signals);
|
||
|
this.lambdas = Object.create(scope.lambdas);
|
||
|
this.scales = Object.create(scope.scales);
|
||
|
this.events = Object.create(scope.events);
|
||
|
this.data = Object.create(scope.data);
|
||
|
|
||
|
this.streams = [];
|
||
|
this.updates = [];
|
||
|
this.operators = [];
|
||
|
|
||
|
this._id = 0;
|
||
|
this._subid = ++scope._nextsub[0];
|
||
|
this._nextsub = scope._nextsub;
|
||
|
|
||
|
this._parent = scope._parent.slice();
|
||
|
this._encode = scope._encode.slice();
|
||
|
this._lookup = scope._lookup.slice();
|
||
|
this._markpath = scope._markpath;
|
||
|
}
|
||
|
|
||
|
var prototype$1 = Scope$1.prototype = Subscope.prototype;
|
||
|
|
||
|
// ----
|
||
|
|
||
|
prototype$1.fork = function() {
|
||
|
return new Subscope(this);
|
||
|
};
|
||
|
|
||
|
prototype$1.isSubscope = function() {
|
||
|
return this._subid > 0;
|
||
|
};
|
||
|
|
||
|
prototype$1.toRuntime = function() {
|
||
|
this.finish();
|
||
|
return {
|
||
|
background: this.background,
|
||
|
operators: this.operators,
|
||
|
streams: this.streams,
|
||
|
updates: this.updates,
|
||
|
bindings: this.bindings,
|
||
|
eventConfig: this.eventConfig
|
||
|
};
|
||
|
};
|
||
|
|
||
|
prototype$1.id = function() {
|
||
|
return (this._subid ? this._subid + ':' : 0) + this._id++;
|
||
|
};
|
||
|
|
||
|
prototype$1.add = function(op) {
|
||
|
this.operators.push(op);
|
||
|
op.id = this.id();
|
||
|
// if pre-registration references exist, resolve them now
|
||
|
if (op.refs) {
|
||
|
op.refs.forEach(function(ref) { ref.$ref = op.id; });
|
||
|
op.refs = null;
|
||
|
}
|
||
|
return op;
|
||
|
};
|
||
|
|
||
|
prototype$1.proxy = function(op) {
|
||
|
var vref = op instanceof Entry ? ref(op) : op;
|
||
|
return this.add(Proxy({value: vref}));
|
||
|
};
|
||
|
|
||
|
prototype$1.addStream = function(stream) {
|
||
|
this.streams.push(stream);
|
||
|
stream.id = this.id();
|
||
|
return stream;
|
||
|
};
|
||
|
|
||
|
prototype$1.addUpdate = function(update) {
|
||
|
this.updates.push(update);
|
||
|
return update;
|
||
|
};
|
||
|
|
||
|
// Apply metadata
|
||
|
prototype$1.finish = function() {
|
||
|
var name, ds;
|
||
|
|
||
|
// annotate root
|
||
|
if (this.root) this.root.root = true;
|
||
|
|
||
|
// annotate signals
|
||
|
for (name in this.signals) {
|
||
|
this.signals[name].signal = name;
|
||
|
}
|
||
|
|
||
|
// annotate scales
|
||
|
for (name in this.scales) {
|
||
|
this.scales[name].scale = name;
|
||
|
}
|
||
|
|
||
|
// annotate data sets
|
||
|
function annotate(op, name, type) {
|
||
|
var data, list;
|
||
|
if (op) {
|
||
|
data = op.data || (op.data = {});
|
||
|
list = data[name] || (data[name] = []);
|
||
|
list.push(type);
|
||
|
}
|
||
|
}
|
||
|
for (name in this.data) {
|
||
|
ds = this.data[name];
|
||
|
annotate(ds.input, name, 'input');
|
||
|
annotate(ds.output, name, 'output');
|
||
|
annotate(ds.values, name, 'values');
|
||
|
for (var field in ds.index) {
|
||
|
annotate(ds.index[field], name, 'index:' + field);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
// ----
|
||
|
|
||
|
prototype$1.pushState = function(encode, parent, lookup) {
|
||
|
this._encode.push(ref(this.add(Sieve({pulse: encode}))));
|
||
|
this._parent.push(parent);
|
||
|
this._lookup.push(lookup ? ref(this.proxy(lookup)) : null);
|
||
|
this._markpath.push(-1);
|
||
|
};
|
||
|
|
||
|
prototype$1.popState = function() {
|
||
|
this._encode.pop();
|
||
|
this._parent.pop();
|
||
|
this._lookup.pop();
|
||
|
this._markpath.pop();
|
||
|
};
|
||
|
|
||
|
prototype$1.parent = function() {
|
||
|
return vegaUtil.peek(this._parent);
|
||
|
};
|
||
|
|
||
|
prototype$1.encode = function() {
|
||
|
return vegaUtil.peek(this._encode);
|
||
|
};
|
||
|
|
||
|
prototype$1.lookup = function() {
|
||
|
return vegaUtil.peek(this._lookup);
|
||
|
};
|
||
|
|
||
|
prototype$1.markpath = function() {
|
||
|
var p = this._markpath;
|
||
|
return ++p[p.length-1];
|
||
|
};
|
||
|
|
||
|
// ----
|
||
|
|
||
|
prototype$1.fieldRef = function(field, name) {
|
||
|
if (vegaUtil.isString(field)) return fieldRef(field, name);
|
||
|
if (!field.signal) {
|
||
|
vegaUtil.error('Unsupported field reference: ' + vegaUtil.stringValue(field));
|
||
|
}
|
||
|
|
||
|
var s = field.signal,
|
||
|
f = this.field[s],
|
||
|
params;
|
||
|
|
||
|
if (!f) {
|
||
|
params = {name: this.signalRef(s)};
|
||
|
if (name) params.as = name;
|
||
|
this.field[s] = f = ref(this.add(Field(params)));
|
||
|
}
|
||
|
return f;
|
||
|
};
|
||
|
|
||
|
prototype$1.compareRef = function(cmp) {
|
||
|
function check(_) {
|
||
|
if (isSignal(_)) {
|
||
|
signal = true;
|
||
|
return scope.signalRef(_.signal);
|
||
|
} else if (isExpr(_)) {
|
||
|
signal = true;
|
||
|
return scope.exprRef(_.expr);
|
||
|
} else {
|
||
|
return _;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var scope = this,
|
||
|
signal = false,
|
||
|
fields = vegaUtil.array(cmp.field).map(check),
|
||
|
orders = vegaUtil.array(cmp.order).map(check);
|
||
|
|
||
|
return signal
|
||
|
? ref(this.add(Compare({fields: fields, orders: orders})))
|
||
|
: compareRef(fields, orders);
|
||
|
};
|
||
|
|
||
|
prototype$1.keyRef = function(fields, flat) {
|
||
|
function check(_) {
|
||
|
if (isSignal(_)) {
|
||
|
signal = true;
|
||
|
return ref(sig[_.signal]);
|
||
|
} else {
|
||
|
return _;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var sig = this.signals,
|
||
|
signal = false;
|
||
|
fields = vegaUtil.array(fields).map(check);
|
||
|
|
||
|
return signal
|
||
|
? ref(this.add(Key({fields: fields, flat: flat})))
|
||
|
: keyRef(fields, flat);
|
||
|
};
|
||
|
|
||
|
prototype$1.sortRef = function(sort) {
|
||
|
if (!sort) return sort;
|
||
|
|
||
|
// including id ensures stable sorting
|
||
|
var a = aggrField(sort.op, sort.field),
|
||
|
o = sort.order || Ascending;
|
||
|
|
||
|
return o.signal
|
||
|
? ref(this.add(Compare({
|
||
|
fields: a,
|
||
|
orders: this.signalRef(o.signal)
|
||
|
})))
|
||
|
: compareRef(a, o);
|
||
|
};
|
||
|
|
||
|
// ----
|
||
|
|
||
|
prototype$1.event = function(source, type) {
|
||
|
var key = source + ':' + type;
|
||
|
if (!this.events[key]) {
|
||
|
var id = this.id();
|
||
|
this.streams.push({
|
||
|
id: id,
|
||
|
source: source,
|
||
|
type: type
|
||
|
});
|
||
|
this.events[key] = id;
|
||
|
}
|
||
|
return this.events[key];
|
||
|
};
|
||
|
|
||
|
// ----
|
||
|
|
||
|
prototype$1.hasOwnSignal = function(name) {
|
||
|
return vegaUtil.hasOwnProperty(this.signals, name);
|
||
|
};
|
||
|
|
||
|
prototype$1.addSignal = function(name, value) {
|
||
|
if (this.hasOwnSignal(name)) {
|
||
|
vegaUtil.error('Duplicate signal name: ' + vegaUtil.stringValue(name));
|
||
|
}
|
||
|
var op = value instanceof Entry ? value : this.add(operator(value));
|
||
|
return this.signals[name] = op;
|
||
|
};
|
||
|
|
||
|
prototype$1.getSignal = function(name) {
|
||
|
if (!this.signals[name]) {
|
||
|
vegaUtil.error('Unrecognized signal name: ' + vegaUtil.stringValue(name));
|
||
|
}
|
||
|
return this.signals[name];
|
||
|
};
|
||
|
|
||
|
prototype$1.signalRef = function(s) {
|
||
|
if (this.signals[s]) {
|
||
|
return ref(this.signals[s]);
|
||
|
} else if (!vegaUtil.hasOwnProperty(this.lambdas, s)) {
|
||
|
this.lambdas[s] = this.add(operator(null));
|
||
|
}
|
||
|
return ref(this.lambdas[s]);
|
||
|
};
|
||
|
|
||
|
prototype$1.parseLambdas = function() {
|
||
|
var code = Object.keys(this.lambdas);
|
||
|
for (var i=0, n=code.length; i<n; ++i) {
|
||
|
var s = code[i],
|
||
|
e = parseExpression(s, this),
|
||
|
op = this.lambdas[s];
|
||
|
op.params = e.$params;
|
||
|
op.update = e.$expr;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
prototype$1.property = function(spec) {
|
||
|
return spec && spec.signal ? this.signalRef(spec.signal) : spec;
|
||
|
};
|
||
|
|
||
|
prototype$1.objectProperty = function(spec) {
|
||
|
return (!spec || !vegaUtil.isObject(spec)) ? spec
|
||
|
: this.signalRef(spec.signal || propertyLambda(spec));
|
||
|
};
|
||
|
|
||
|
function propertyLambda(spec) {
|
||
|
return (vegaUtil.isArray(spec) ? arrayLambda : objectLambda)(spec);
|
||
|
}
|
||
|
|
||
|
function arrayLambda(array) {
|
||
|
var code = '[',
|
||
|
i = 0,
|
||
|
n = array.length,
|
||
|
value;
|
||
|
|
||
|
for (; i<n; ++i) {
|
||
|
value = array[i];
|
||
|
code += (i > 0 ? ',' : '')
|
||
|
+ (vegaUtil.isObject(value)
|
||
|
? (value.signal || propertyLambda(value))
|
||
|
: vegaUtil.stringValue(value));
|
||
|
}
|
||
|
return code + ']';
|
||
|
}
|
||
|
|
||
|
function objectLambda(obj) {
|
||
|
var code = '{',
|
||
|
i = 0,
|
||
|
key, value;
|
||
|
|
||
|
for (key in obj) {
|
||
|
value = obj[key];
|
||
|
code += (++i > 1 ? ',' : '')
|
||
|
+ vegaUtil.stringValue(key) + ':'
|
||
|
+ (vegaUtil.isObject(value)
|
||
|
? (value.signal || propertyLambda(value))
|
||
|
: vegaUtil.stringValue(value));
|
||
|
}
|
||
|
return code + '}';
|
||
|
}
|
||
|
|
||
|
prototype$1.exprRef = function(code, name) {
|
||
|
var params = {expr: parseExpression(code, this)};
|
||
|
if (name) params.expr.$name = name;
|
||
|
return ref(this.add(Expression(params)));
|
||
|
};
|
||
|
|
||
|
prototype$1.addBinding = function(name, bind) {
|
||
|
if (!this.bindings) {
|
||
|
vegaUtil.error('Nested signals do not support binding: ' + vegaUtil.stringValue(name));
|
||
|
}
|
||
|
this.bindings.push(vegaUtil.extend({signal: name}, bind));
|
||
|
};
|
||
|
|
||
|
// ----
|
||
|
|
||
|
prototype$1.addScaleProj = function(name, transform) {
|
||
|
if (vegaUtil.hasOwnProperty(this.scales, name)) {
|
||
|
vegaUtil.error('Duplicate scale or projection name: ' + vegaUtil.stringValue(name));
|
||
|
}
|
||
|
this.scales[name] = this.add(transform);
|
||
|
};
|
||
|
|
||
|
prototype$1.addScale = function(name, params) {
|
||
|
this.addScaleProj(name, Scale(params));
|
||
|
};
|
||
|
|
||
|
prototype$1.addProjection = function(name, params) {
|
||
|
this.addScaleProj(name, Projection(params));
|
||
|
};
|
||
|
|
||
|
prototype$1.getScale = function(name) {
|
||
|
if (!this.scales[name]) {
|
||
|
vegaUtil.error('Unrecognized scale name: ' + vegaUtil.stringValue(name));
|
||
|
}
|
||
|
return this.scales[name];
|
||
|
};
|
||
|
|
||
|
prototype$1.projectionRef =
|
||
|
prototype$1.scaleRef = function(name) {
|
||
|
return ref(this.getScale(name));
|
||
|
};
|
||
|
|
||
|
prototype$1.projectionType =
|
||
|
prototype$1.scaleType = function(name) {
|
||
|
return this.getScale(name).params.type;
|
||
|
};
|
||
|
|
||
|
// ----
|
||
|
|
||
|
prototype$1.addData = function(name, dataScope) {
|
||
|
if (vegaUtil.hasOwnProperty(this.data, name)) {
|
||
|
vegaUtil.error('Duplicate data set name: ' + vegaUtil.stringValue(name));
|
||
|
}
|
||
|
return (this.data[name] = dataScope);
|
||
|
};
|
||
|
|
||
|
prototype$1.getData = function(name) {
|
||
|
if (!this.data[name]) {
|
||
|
vegaUtil.error('Undefined data set name: ' + vegaUtil.stringValue(name));
|
||
|
}
|
||
|
return this.data[name];
|
||
|
};
|
||
|
|
||
|
prototype$1.addDataPipeline = function(name, entries) {
|
||
|
if (vegaUtil.hasOwnProperty(this.data, name)) {
|
||
|
vegaUtil.error('Duplicate data set name: ' + vegaUtil.stringValue(name));
|
||
|
}
|
||
|
return this.addData(name, DataScope.fromEntries(this, entries));
|
||
|
};
|
||
|
|
||
|
var defaultFont = 'sans-serif',
|
||
|
defaultSymbolSize = 30,
|
||
|
defaultStrokeWidth = 2,
|
||
|
defaultColor = '#4c78a8',
|
||
|
black = '#000',
|
||
|
gray = '#888',
|
||
|
lightGray = '#ddd';
|
||
|
|
||
|
/**
|
||
|
* Standard configuration defaults for Vega specification parsing.
|
||
|
* Users can provide their own (sub-)set of these default values
|
||
|
* by passing in a config object to the top-level parse method.
|
||
|
*/
|
||
|
function defaults() {
|
||
|
return {
|
||
|
// default padding around visualization
|
||
|
padding: 0,
|
||
|
|
||
|
// default for automatic sizing; options: 'none', 'pad', 'fit'
|
||
|
// or provide an object (e.g., {'type': 'pad', 'resize': true})
|
||
|
autosize: 'pad',
|
||
|
|
||
|
// default view background color
|
||
|
// covers the entire view component
|
||
|
background: null,
|
||
|
|
||
|
// default event handling configuration
|
||
|
// preventDefault for view-sourced event types except 'wheel'
|
||
|
events: {
|
||
|
defaults: {allow: ['wheel']}
|
||
|
},
|
||
|
|
||
|
// defaults for top-level group marks
|
||
|
// accepts mark properties (fill, stroke, etc)
|
||
|
// covers the data rectangle within group width/height
|
||
|
group: null,
|
||
|
|
||
|
// defaults for basic mark types
|
||
|
// each subset accepts mark properties (fill, stroke, etc)
|
||
|
mark: null,
|
||
|
arc: { fill: defaultColor },
|
||
|
area: { fill: defaultColor },
|
||
|
image: null,
|
||
|
line: {
|
||
|
stroke: defaultColor,
|
||
|
strokeWidth: defaultStrokeWidth
|
||
|
},
|
||
|
path: { stroke: defaultColor },
|
||
|
rect: { fill: defaultColor },
|
||
|
rule: { stroke: black },
|
||
|
shape: { stroke: defaultColor },
|
||
|
symbol: {
|
||
|
fill: defaultColor,
|
||
|
size: 64
|
||
|
},
|
||
|
text: {
|
||
|
fill: black,
|
||
|
font: defaultFont,
|
||
|
fontSize: 11
|
||
|
},
|
||
|
|
||
|
// style definitions
|
||
|
style: {
|
||
|
// axis & legend labels
|
||
|
'guide-label': {
|
||
|
fill: black,
|
||
|
font: defaultFont,
|
||
|
fontSize: 10
|
||
|
},
|
||
|
// axis & legend titles
|
||
|
'guide-title': {
|
||
|
fill: black,
|
||
|
font: defaultFont,
|
||
|
fontSize: 11,
|
||
|
fontWeight: 'bold'
|
||
|
},
|
||
|
// headers, including chart title
|
||
|
'group-title': {
|
||
|
fill: black,
|
||
|
font: defaultFont,
|
||
|
fontSize: 13,
|
||
|
fontWeight: 'bold'
|
||
|
},
|
||
|
// chart subtitle
|
||
|
'group-subtitle': {
|
||
|
fill: black,
|
||
|
font: defaultFont,
|
||
|
fontSize: 12
|
||
|
},
|
||
|
// defaults for styled point marks in Vega-Lite
|
||
|
point: {
|
||
|
size: defaultSymbolSize,
|
||
|
strokeWidth: defaultStrokeWidth,
|
||
|
shape: 'circle'
|
||
|
},
|
||
|
circle: {
|
||
|
size: defaultSymbolSize,
|
||
|
strokeWidth: defaultStrokeWidth
|
||
|
},
|
||
|
square: {
|
||
|
size: defaultSymbolSize,
|
||
|
strokeWidth: defaultStrokeWidth,
|
||
|
shape: 'square'
|
||
|
},
|
||
|
// defaults for styled group marks in Vega-Lite
|
||
|
cell: {
|
||
|
fill: 'transparent',
|
||
|
stroke: lightGray
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// defaults for title
|
||
|
title: {
|
||
|
orient: 'top',
|
||
|
anchor: 'middle',
|
||
|
offset: 4,
|
||
|
subtitlePadding: 3
|
||
|
},
|
||
|
|
||
|
// defaults for axes
|
||
|
axis: {
|
||
|
minExtent: 0,
|
||
|
maxExtent: 200,
|
||
|
bandPosition: 0.5,
|
||
|
domain: true,
|
||
|
domainWidth: 1,
|
||
|
domainColor: gray,
|
||
|
grid: false,
|
||
|
gridWidth: 1,
|
||
|
gridColor: lightGray,
|
||
|
labels: true,
|
||
|
labelAngle: 0,
|
||
|
labelLimit: 180,
|
||
|
labelPadding: 2,
|
||
|
ticks: true,
|
||
|
tickColor: gray,
|
||
|
tickOffset: 0,
|
||
|
tickRound: true,
|
||
|
tickSize: 5,
|
||
|
tickWidth: 1,
|
||
|
titlePadding: 4
|
||
|
},
|
||
|
|
||
|
// correction for centering bias
|
||
|
axisBand: {
|
||
|
tickOffset: -0.5
|
||
|
},
|
||
|
|
||
|
// defaults for cartographic projection
|
||
|
projection: {
|
||
|
type: 'mercator'
|
||
|
},
|
||
|
|
||
|
// defaults for legends
|
||
|
legend: {
|
||
|
orient: 'right',
|
||
|
padding: 0,
|
||
|
gridAlign: 'each',
|
||
|
columnPadding: 10,
|
||
|
rowPadding: 2,
|
||
|
symbolDirection: 'vertical',
|
||
|
gradientDirection: 'vertical',
|
||
|
gradientLength: 200,
|
||
|
gradientThickness: 16,
|
||
|
gradientStrokeColor: lightGray,
|
||
|
gradientStrokeWidth: 0,
|
||
|
gradientLabelOffset: 2,
|
||
|
labelAlign: 'left',
|
||
|
labelBaseline: 'middle',
|
||
|
labelLimit: 160,
|
||
|
labelOffset: 4,
|
||
|
labelOverlap: true,
|
||
|
symbolLimit: 30,
|
||
|
symbolType: 'circle',
|
||
|
symbolSize: 100,
|
||
|
symbolOffset: 0,
|
||
|
symbolStrokeWidth: 1.5,
|
||
|
symbolBaseFillColor: 'transparent',
|
||
|
symbolBaseStrokeColor: gray,
|
||
|
titleLimit: 180,
|
||
|
titleOrient: 'top',
|
||
|
titlePadding: 5,
|
||
|
layout: {
|
||
|
offset: 18,
|
||
|
direction: 'horizontal',
|
||
|
left: { direction: 'vertical' },
|
||
|
right: { direction: 'vertical' }
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// defaults for scale ranges
|
||
|
range: {
|
||
|
category: {
|
||
|
scheme: 'tableau10'
|
||
|
},
|
||
|
ordinal: {
|
||
|
scheme: 'blues'
|
||
|
},
|
||
|
heatmap: {
|
||
|
scheme: 'yellowgreenblue'
|
||
|
},
|
||
|
ramp: {
|
||
|
scheme: 'blues'
|
||
|
},
|
||
|
diverging: {
|
||
|
scheme: 'blueorange',
|
||
|
extent: [1, 0]
|
||
|
},
|
||
|
symbol: [
|
||
|
'circle',
|
||
|
'square',
|
||
|
'triangle-up',
|
||
|
'cross',
|
||
|
'diamond',
|
||
|
'triangle-right',
|
||
|
'triangle-down',
|
||
|
'triangle-left'
|
||
|
]
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function parse(spec, config) {
|
||
|
if (!vegaUtil.isObject(spec)) {
|
||
|
vegaUtil.error('Input Vega specification must be an object.');
|
||
|
}
|
||
|
|
||
|
config = vegaUtil.mergeConfig(defaults(), config, spec.config);
|
||
|
return parseView(spec, new Scope$1(config)).toRuntime();
|
||
|
}
|
||
|
|
||
|
exports.AxisDomainRole = AxisDomainRole;
|
||
|
exports.AxisGridRole = AxisGridRole;
|
||
|
exports.AxisLabelRole = AxisLabelRole;
|
||
|
exports.AxisRole = AxisRole;
|
||
|
exports.AxisTickRole = AxisTickRole;
|
||
|
exports.AxisTitleRole = AxisTitleRole;
|
||
|
exports.DataScope = DataScope;
|
||
|
exports.FrameRole = FrameRole;
|
||
|
exports.LegendEntryRole = LegendEntryRole;
|
||
|
exports.LegendLabelRole = LegendLabelRole;
|
||
|
exports.LegendRole = LegendRole;
|
||
|
exports.LegendSymbolRole = LegendSymbolRole;
|
||
|
exports.LegendTitleRole = LegendTitleRole;
|
||
|
exports.MarkRole = MarkRole;
|
||
|
exports.Scope = Scope$1;
|
||
|
exports.ScopeRole = ScopeRole;
|
||
|
exports.config = defaults;
|
||
|
exports.parse = parse;
|
||
|
exports.signal = parseSignal;
|
||
|
exports.signalUpdates = parseSignalUpdates;
|
||
|
exports.stream = parseStream;
|
||
|
|
||
|
Object.defineProperty(exports, '__esModule', { value: true });
|
||
|
|
||
|
})));
|