UNPKG

vega-parser

Version:

Parse Vega specifications to runtime dataflows.

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