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.
1360 lines
42 KiB
1360 lines
42 KiB
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vega-scale'), require('vega-time'), require('vega-util'), require('d3-format'), require('vega-dataflow'), require('d3-array'), require('d3-interpolate')) :
|
|
typeof define === 'function' && define.amd ? define(['exports', 'vega-scale', 'vega-time', 'vega-util', 'd3-format', 'vega-dataflow', 'd3-array', 'd3-interpolate'], factory) :
|
|
(global = global || self, factory((global.vega = global.vega || {}, global.vega.transforms = {}), global.vega, global.vega, global.vega, global.d3, global.vega, global.d3, global.d3));
|
|
}(this, (function (exports, vegaScale, vegaTime, vegaUtil, d3Format, vegaDataflow, d3Array, d3Interpolate) { 'use strict';
|
|
|
|
/**
|
|
* Determine the tick count or interval function.
|
|
* @param {Scale} scale - The scale for which to generate tick values.
|
|
* @param {*} count - The desired tick count or interval specifier.
|
|
* @param {number} minStep - The desired minimum step between tick values.
|
|
* @return {*} - The tick count or interval function.
|
|
*/
|
|
function tickCount(scale, count, minStep) {
|
|
var step;
|
|
|
|
if (vegaUtil.isNumber(count)) {
|
|
if (scale.bins) {
|
|
count = Math.max(count, scale.bins.length);
|
|
}
|
|
if (minStep != null) {
|
|
count = Math.min(count, ~~(vegaUtil.span(scale.domain()) / minStep) || 1);
|
|
}
|
|
}
|
|
|
|
if (vegaUtil.isObject(count)) {
|
|
step = count.step;
|
|
count = count.interval;
|
|
}
|
|
|
|
if (vegaUtil.isString(count)) {
|
|
count = scale.type === vegaScale.Time ? vegaTime.timeInterval(count)
|
|
: scale.type == vegaScale.UTC ? vegaTime.utcInterval(count)
|
|
: vegaUtil.error('Only time and utc scales accept interval strings.');
|
|
if (step) count = count.every(step);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Filter a set of candidate tick values, ensuring that only tick values
|
|
* that lie within the scale range are included.
|
|
* @param {Scale} scale - The scale for which to generate tick values.
|
|
* @param {Array<*>} ticks - The candidate tick values.
|
|
* @param {*} count - The tick count or interval function.
|
|
* @return {Array<*>} - The filtered tick values.
|
|
*/
|
|
function validTicks(scale, ticks, count) {
|
|
var range = scale.range(),
|
|
lo = Math.floor(range[0]),
|
|
hi = Math.ceil(vegaUtil.peek(range));
|
|
|
|
if (lo > hi) {
|
|
range = hi;
|
|
hi = lo;
|
|
lo = range;
|
|
}
|
|
|
|
ticks = ticks.filter(function(v) {
|
|
v = scale(v);
|
|
return lo <= v && v <= hi;
|
|
});
|
|
|
|
if (count > 0 && ticks.length > 1) {
|
|
var endpoints = [ticks[0], vegaUtil.peek(ticks)];
|
|
while (ticks.length > count && ticks.length >= 3) {
|
|
ticks = ticks.filter(function(_, i) { return !(i % 2); });
|
|
}
|
|
if (ticks.length < 3) {
|
|
ticks = endpoints;
|
|
}
|
|
}
|
|
|
|
return ticks;
|
|
}
|
|
|
|
/**
|
|
* Generate tick values for the given scale and approximate tick count or
|
|
* interval value. If the scale has a 'ticks' method, it will be used to
|
|
* generate the ticks, with the count argument passed as a parameter. If the
|
|
* scale lacks a 'ticks' method, the full scale domain will be returned.
|
|
* @param {Scale} scale - The scale for which to generate tick values.
|
|
* @param {*} [count] - The approximate number of desired ticks.
|
|
* @return {Array<*>} - The generated tick values.
|
|
*/
|
|
function tickValues(scale, count) {
|
|
return scale.bins ? validTicks(scale, scale.bins)
|
|
: scale.ticks ? scale.ticks(count)
|
|
: scale.domain();
|
|
}
|
|
|
|
/**
|
|
* Generate a label format function for a scale. If the scale has a
|
|
* 'tickFormat' method, it will be used to generate the formatter, with the
|
|
* count and specifier arguments passed as parameters. If the scale lacks a
|
|
* 'tickFormat' method, the returned formatter performs simple string coercion.
|
|
* If the input scale is a logarithmic scale and the format specifier does not
|
|
* indicate a desired decimal precision, a special variable precision formatter
|
|
* that automatically trims trailing zeroes will be generated.
|
|
* @param {Scale} scale - The scale for which to generate the label formatter.
|
|
* @param {*} [count] - The approximate number of desired ticks.
|
|
* @param {string} [specifier] - The format specifier. Must be a legal d3
|
|
* specifier string (see https://github.com/d3/d3-format#formatSpecifier) or
|
|
* time multi-format specifier object.
|
|
* @return {function(*):string} - The generated label formatter.
|
|
*/
|
|
function tickFormat(scale, count, specifier, formatType, noSkip) {
|
|
var type = scale.type,
|
|
format = (type === vegaScale.Time || formatType === vegaScale.Time) ? vegaTime.timeFormat(specifier)
|
|
: (type === vegaScale.UTC || formatType === vegaScale.UTC) ? vegaTime.utcFormat(specifier)
|
|
: scale.tickFormat ? scale.tickFormat(count, specifier)
|
|
: specifier ? d3Format.format(specifier)
|
|
: String;
|
|
|
|
if (vegaScale.isLogarithmic(type)) {
|
|
var logfmt = variablePrecision(specifier);
|
|
format = noSkip || scale.bins ? logfmt : filter(format, logfmt);
|
|
}
|
|
|
|
return format;
|
|
}
|
|
|
|
function filter(sourceFormat, targetFormat) {
|
|
return _ => sourceFormat(_) ? targetFormat(_) : '';
|
|
}
|
|
|
|
function variablePrecision(specifier) {
|
|
var s = d3Format.formatSpecifier(specifier || ',');
|
|
|
|
if (s.precision == null) {
|
|
s.precision = 12;
|
|
switch (s.type) {
|
|
case '%': s.precision -= 2; break;
|
|
case 'e': s.precision -= 1; break;
|
|
}
|
|
return trimZeroes(
|
|
d3Format.format(s), // number format
|
|
d3Format.format('.1f')(1)[1] // decimal point character
|
|
);
|
|
} else {
|
|
return d3Format.format(s);
|
|
}
|
|
}
|
|
|
|
function trimZeroes(format, decimalChar) {
|
|
return function(x) {
|
|
var str = format(x),
|
|
dec = str.indexOf(decimalChar),
|
|
idx, end;
|
|
|
|
if (dec < 0) return str;
|
|
|
|
idx = rightmostDigit(str, dec);
|
|
end = idx < str.length ? str.slice(idx) : '';
|
|
while (--idx > dec) if (str[idx] !== '0') { ++idx; break; }
|
|
|
|
return str.slice(0, idx) + end;
|
|
};
|
|
}
|
|
|
|
function rightmostDigit(str, dec) {
|
|
var i = str.lastIndexOf('e'), c;
|
|
if (i > 0) return i;
|
|
for (i=str.length; --i > dec;) {
|
|
c = str.charCodeAt(i);
|
|
if (c >= 48 && c <= 57) return i + 1; // is digit
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates axis ticks for visualizing a spatial scale.
|
|
* @constructor
|
|
* @param {object} params - The parameters for this operator.
|
|
* @param {Scale} params.scale - The scale to generate ticks for.
|
|
* @param {*} [params.count=10] - The approximate number of ticks, or
|
|
* desired tick interval, to use.
|
|
* @param {Array<*>} [params.values] - The exact tick values to use.
|
|
* These must be legal domain values for the provided scale.
|
|
* If provided, the count argument is ignored.
|
|
* @param {function(*):string} [params.formatSpecifier] - A format specifier
|
|
* to use in conjunction with scale.tickFormat. Legal values are
|
|
* any valid d3 4.0 format specifier.
|
|
* @param {function(*):string} [params.format] - The format function to use.
|
|
* If provided, the formatSpecifier argument is ignored.
|
|
*/
|
|
function AxisTicks(params) {
|
|
vegaDataflow.Transform.call(this, null, params);
|
|
}
|
|
|
|
var prototype = vegaUtil.inherits(AxisTicks, vegaDataflow.Transform);
|
|
|
|
prototype.transform = function(_, pulse) {
|
|
if (this.value && !_.modified()) {
|
|
return pulse.StopPropagation;
|
|
}
|
|
|
|
var out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
|
|
ticks = this.value,
|
|
scale = _.scale,
|
|
tally = _.count == null ? (_.values ? _.values.length : 10) : _.count,
|
|
count = tickCount(scale, tally, _.minstep),
|
|
format = _.format || tickFormat(scale, count, _.formatSpecifier, _.formatType, !!_.values),
|
|
values = _.values ? validTicks(scale, _.values, count) : tickValues(scale, count);
|
|
|
|
if (ticks) out.rem = ticks;
|
|
|
|
ticks = values.map(function(value, i) {
|
|
return vegaDataflow.ingest({
|
|
index: i / (values.length - 1 || 1),
|
|
value: value,
|
|
label: format(value)
|
|
});
|
|
});
|
|
|
|
if (_.extra && ticks.length) {
|
|
// add an extra tick pegged to the initial domain value
|
|
// this is used to generate axes with 'binned' domains
|
|
ticks.push(vegaDataflow.ingest({
|
|
index: -1,
|
|
extra: {value: ticks[0].value},
|
|
label: ''
|
|
}));
|
|
}
|
|
|
|
out.source = ticks;
|
|
out.add = ticks;
|
|
this.value = ticks;
|
|
|
|
return out;
|
|
};
|
|
|
|
/**
|
|
* Joins a set of data elements against a set of visual items.
|
|
* @constructor
|
|
* @param {object} params - The parameters for this operator.
|
|
* @param {function(object): object} [params.item] - An item generator function.
|
|
* @param {function(object): *} [params.key] - The key field associating data and visual items.
|
|
*/
|
|
function DataJoin(params) {
|
|
vegaDataflow.Transform.call(this, null, params);
|
|
}
|
|
|
|
var prototype$1 = vegaUtil.inherits(DataJoin, vegaDataflow.Transform);
|
|
|
|
function defaultItemCreate() {
|
|
return vegaDataflow.ingest({});
|
|
}
|
|
|
|
function isExit(t) {
|
|
return t.exit;
|
|
}
|
|
|
|
prototype$1.transform = function(_, pulse) {
|
|
var df = pulse.dataflow,
|
|
out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
|
|
item = _.item || defaultItemCreate,
|
|
key = _.key || vegaDataflow.tupleid,
|
|
map = this.value;
|
|
|
|
// prevent transient (e.g., hover) requests from
|
|
// cascading across marks derived from marks
|
|
if (vegaUtil.isArray(out.encode)) {
|
|
out.encode = null;
|
|
}
|
|
|
|
if (map && (_.modified('key') || pulse.modified(key))) {
|
|
vegaUtil.error('DataJoin does not support modified key function or fields.');
|
|
}
|
|
|
|
if (!map) {
|
|
pulse = pulse.addAll();
|
|
this.value = map = vegaUtil.fastmap().test(isExit);
|
|
map.lookup = function(t) { return map.get(key(t)); };
|
|
}
|
|
|
|
pulse.visit(pulse.ADD, function(t) {
|
|
var k = key(t),
|
|
x = map.get(k);
|
|
|
|
if (x) {
|
|
if (x.exit) {
|
|
map.empty--;
|
|
out.add.push(x);
|
|
} else {
|
|
out.mod.push(x);
|
|
}
|
|
} else {
|
|
map.set(k, (x = item(t)));
|
|
out.add.push(x);
|
|
}
|
|
|
|
x.datum = t;
|
|
x.exit = false;
|
|
});
|
|
|
|
pulse.visit(pulse.MOD, function(t) {
|
|
var k = key(t),
|
|
x = map.get(k);
|
|
|
|
if (x) {
|
|
x.datum = t;
|
|
out.mod.push(x);
|
|
}
|
|
});
|
|
|
|
pulse.visit(pulse.REM, function(t) {
|
|
var k = key(t),
|
|
x = map.get(k);
|
|
|
|
if (t === x.datum && !x.exit) {
|
|
out.rem.push(x);
|
|
x.exit = true;
|
|
++map.empty;
|
|
}
|
|
});
|
|
|
|
if (pulse.changed(pulse.ADD_MOD)) out.modifies('datum');
|
|
|
|
if (_.clean && map.empty > df.cleanThreshold) df.runAfter(map.clean);
|
|
|
|
return out;
|
|
};
|
|
|
|
/**
|
|
* Invokes encoding functions for visual items.
|
|
* @constructor
|
|
* @param {object} params - The parameters to the encoding functions. This
|
|
* parameter object will be passed through to all invoked encoding functions.
|
|
* @param {object} [params.mod=false] - Flag indicating if tuples in the input
|
|
* mod set that are unmodified by encoders should be included in the output.
|
|
* @param {object} param.encoders - The encoding functions
|
|
* @param {function(object, object): boolean} [param.encoders.update] - Update encoding set
|
|
* @param {function(object, object): boolean} [param.encoders.enter] - Enter encoding set
|
|
* @param {function(object, object): boolean} [param.encoders.exit] - Exit encoding set
|
|
*/
|
|
function Encode(params) {
|
|
vegaDataflow.Transform.call(this, null, params);
|
|
}
|
|
|
|
var prototype$2 = vegaUtil.inherits(Encode, vegaDataflow.Transform);
|
|
|
|
prototype$2.transform = function(_, pulse) {
|
|
var out = pulse.fork(pulse.ADD_REM),
|
|
fmod = _.mod || false,
|
|
encoders = _.encoders,
|
|
encode = pulse.encode;
|
|
|
|
// if an array, the encode directive includes additional sets
|
|
// that must be defined in order for the primary set to be invoked
|
|
// e.g., only run the update set if the hover set is defined
|
|
if (vegaUtil.isArray(encode)) {
|
|
if (out.changed() || encode.every(function(e) { return encoders[e]; })) {
|
|
encode = encode[0];
|
|
out.encode = null; // consume targeted encode directive
|
|
} else {
|
|
return pulse.StopPropagation;
|
|
}
|
|
}
|
|
|
|
// marshall encoder functions
|
|
var reenter = encode === 'enter',
|
|
update = encoders.update || vegaUtil.falsy,
|
|
enter = encoders.enter || vegaUtil.falsy,
|
|
exit = encoders.exit || vegaUtil.falsy,
|
|
set = (encode && !reenter ? encoders[encode] : update) || vegaUtil.falsy;
|
|
|
|
if (pulse.changed(pulse.ADD)) {
|
|
pulse.visit(pulse.ADD, function(t) { enter(t, _); update(t, _); });
|
|
out.modifies(enter.output);
|
|
out.modifies(update.output);
|
|
if (set !== vegaUtil.falsy && set !== update) {
|
|
pulse.visit(pulse.ADD, function(t) { set(t, _); });
|
|
out.modifies(set.output);
|
|
}
|
|
}
|
|
|
|
if (pulse.changed(pulse.REM) && exit !== vegaUtil.falsy) {
|
|
pulse.visit(pulse.REM, function(t) { exit(t, _); });
|
|
out.modifies(exit.output);
|
|
}
|
|
|
|
if (reenter || set !== vegaUtil.falsy) {
|
|
var flag = pulse.MOD | (_.modified() ? pulse.REFLOW : 0);
|
|
if (reenter) {
|
|
pulse.visit(flag, function(t) {
|
|
var mod = enter(t, _) || fmod;
|
|
if (set(t, _) || mod) out.mod.push(t);
|
|
});
|
|
if (out.mod.length) out.modifies(enter.output);
|
|
} else {
|
|
pulse.visit(flag, function(t) {
|
|
if (set(t, _) || fmod) out.mod.push(t);
|
|
});
|
|
}
|
|
if (out.mod.length) out.modifies(set.output);
|
|
}
|
|
|
|
return out.changed() ? out : pulse.StopPropagation;
|
|
};
|
|
|
|
var Symbols = 'symbol';
|
|
var Discrete = 'discrete';
|
|
var Gradient = 'gradient';
|
|
|
|
const symbols = {
|
|
[vegaScale.Quantile]: 'quantiles',
|
|
[vegaScale.Quantize]: 'thresholds',
|
|
[vegaScale.Threshold]: 'domain'
|
|
};
|
|
|
|
const formats = {
|
|
[vegaScale.Quantile]: 'quantiles',
|
|
[vegaScale.Quantize]: 'domain'
|
|
};
|
|
|
|
function labelValues(scale, count) {
|
|
return scale.bins ? binValues(scale.bins)
|
|
: scale.type === vegaScale.Log ? logValues(scale, count)
|
|
: symbols[scale.type] ? thresholdValues(scale[symbols[scale.type]]())
|
|
: tickValues(scale, count);
|
|
}
|
|
|
|
function logValues(scale, count) {
|
|
var ticks = tickValues(scale, count),
|
|
base = scale.base(),
|
|
logb = Math.log(base),
|
|
k = Math.max(1, base * count / ticks.length);
|
|
|
|
// apply d3-scale's log format filter criteria
|
|
return ticks.filter(d => {
|
|
var i = d / Math.pow(base, Math.round(Math.log(d) / logb));
|
|
if (i * base < base - 0.5) i *= base;
|
|
return i <= k;
|
|
});
|
|
}
|
|
|
|
function thresholdFormat(scale, specifier) {
|
|
var _ = scale[formats[scale.type]](),
|
|
n = _.length,
|
|
d = n > 1 ? _[1] - _[0] : _[0], i;
|
|
|
|
for (i=1; i<n; ++i) {
|
|
d = Math.min(d, _[i] - _[i-1]);
|
|
}
|
|
|
|
// 3 ticks times 10 for increased resolution
|
|
return vegaScale.tickFormat(0, d, 3 * 10, specifier);
|
|
}
|
|
|
|
function thresholdValues(thresholds) {
|
|
const values = [-Infinity].concat(thresholds);
|
|
values.max = +Infinity;
|
|
|
|
return values;
|
|
}
|
|
|
|
function binValues(bins) {
|
|
const values = bins.slice(0, -1);
|
|
values.max = vegaUtil.peek(bins);
|
|
|
|
return values;
|
|
}
|
|
|
|
function isDiscreteRange(scale) {
|
|
return symbols[scale.type] || scale.bins;
|
|
}
|
|
|
|
function labelFormat(scale, count, type, specifier, formatType, noSkip) {
|
|
const format = formats[scale.type] && formatType !== vegaScale.Time && formatType !== vegaScale.UTC
|
|
? thresholdFormat(scale, specifier)
|
|
: tickFormat(scale, count, specifier, formatType, noSkip);
|
|
|
|
return type === Symbols && isDiscreteRange(scale) ? formatRange(format)
|
|
: type === Discrete ? formatDiscrete(format)
|
|
: formatPoint(format);
|
|
}
|
|
|
|
function formatRange(format) {
|
|
return function(value, index, array) {
|
|
var limit = get(array[index + 1], get(array.max, +Infinity)),
|
|
lo = formatValue(value, format),
|
|
hi = formatValue(limit, format);
|
|
return lo && hi ? lo + ' \u2013 ' + hi : hi ? '< ' + hi : '\u2265 ' + lo;
|
|
};
|
|
}
|
|
|
|
function get(value, dflt) {
|
|
return value != null ? value : dflt;
|
|
}
|
|
|
|
function formatDiscrete(format) {
|
|
return function(value, index) {
|
|
return index ? format(value) : null;
|
|
}
|
|
}
|
|
|
|
function formatPoint(format) {
|
|
return function(value) {
|
|
return format(value);
|
|
};
|
|
}
|
|
|
|
function formatValue(value, format) {
|
|
return Number.isFinite(value) ? format(value) : null;
|
|
}
|
|
|
|
function labelFraction(scale) {
|
|
var domain = scale.domain(),
|
|
count = domain.length - 1,
|
|
lo = +domain[0],
|
|
hi = +vegaUtil.peek(domain),
|
|
span = hi - lo;
|
|
|
|
if (scale.type === vegaScale.Threshold) {
|
|
var adjust = count ? span / count : 0.1;
|
|
lo -= adjust;
|
|
hi += adjust;
|
|
span = hi - lo;
|
|
}
|
|
|
|
return function(value) {
|
|
return (value - lo) / span;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generates legend entries for visualizing a scale.
|
|
* @constructor
|
|
* @param {object} params - The parameters for this operator.
|
|
* @param {Scale} params.scale - The scale to generate items for.
|
|
* @param {*} [params.count=5] - The approximate number of items, or
|
|
* desired tick interval, to use.
|
|
* @param {*} [params.limit] - The maximum number of entries to
|
|
* include in a symbol legend.
|
|
* @param {Array<*>} [params.values] - The exact tick values to use.
|
|
* These must be legal domain values for the provided scale.
|
|
* If provided, the count argument is ignored.
|
|
* @param {string} [params.formatSpecifier] - A format specifier
|
|
* to use in conjunction with scale.tickFormat. Legal values are
|
|
* any valid D3 format specifier string.
|
|
* @param {function(*):string} [params.format] - The format function to use.
|
|
* If provided, the formatSpecifier argument is ignored.
|
|
*/
|
|
function LegendEntries(params) {
|
|
vegaDataflow.Transform.call(this, [], params);
|
|
}
|
|
|
|
var prototype$3 = vegaUtil.inherits(LegendEntries, vegaDataflow.Transform);
|
|
|
|
prototype$3.transform = function(_, pulse) {
|
|
if (this.value != null && !_.modified()) {
|
|
return pulse.StopPropagation;
|
|
}
|
|
|
|
var out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
|
|
items = this.value,
|
|
type = _.type || Symbols,
|
|
scale = _.scale,
|
|
limit = +_.limit,
|
|
count = tickCount(scale, _.count == null ? 5 : _.count, _.minstep),
|
|
lskip = !!_.values || type === Symbols,
|
|
format = _.format || labelFormat(scale, count, type, _.formatSpecifier, _.formatType, lskip),
|
|
values = _.values || labelValues(scale, count),
|
|
domain, fraction, size, offset, ellipsis;
|
|
|
|
if (items) out.rem = items;
|
|
|
|
if (type === Symbols) {
|
|
if (limit && values.length > limit) {
|
|
pulse.dataflow.warn('Symbol legend count exceeds limit, filtering items.');
|
|
items = values.slice(0, limit - 1);
|
|
ellipsis = true;
|
|
} else {
|
|
items = values;
|
|
}
|
|
|
|
if (vegaUtil.isFunction(size = _.size)) {
|
|
// if first value maps to size zero, remove from list (vega#717)
|
|
if (!_.values && scale(items[0]) === 0) {
|
|
items = items.slice(1);
|
|
}
|
|
// compute size offset for legend entries
|
|
offset = items.reduce(function(max, value) {
|
|
return Math.max(max, size(value, _));
|
|
}, 0);
|
|
} else {
|
|
size = vegaUtil.constant(offset = size || 8);
|
|
}
|
|
|
|
items = items.map(function(value, index) {
|
|
return vegaDataflow.ingest({
|
|
index: index,
|
|
label: format(value, index, items),
|
|
value: value,
|
|
offset: offset,
|
|
size: size(value, _)
|
|
});
|
|
});
|
|
|
|
if (ellipsis) {
|
|
ellipsis = values[items.length];
|
|
items.push(vegaDataflow.ingest({
|
|
index: items.length,
|
|
label: `\u2026${values.length-items.length} entries`,
|
|
value: ellipsis,
|
|
offset: offset,
|
|
size: size(ellipsis, _)
|
|
}));
|
|
}
|
|
}
|
|
|
|
else if (type === Gradient) {
|
|
domain = scale.domain(),
|
|
fraction = vegaScale.scaleFraction(scale, domain[0], vegaUtil.peek(domain));
|
|
|
|
// if automatic label generation produces 2 or fewer values,
|
|
// use the domain end points instead (fixes vega/vega#1364)
|
|
if (values.length < 3 && !_.values && domain[0] !== vegaUtil.peek(domain)) {
|
|
values = [domain[0], vegaUtil.peek(domain)];
|
|
}
|
|
|
|
items = values.map(function(value, index) {
|
|
return vegaDataflow.ingest({
|
|
index: index,
|
|
label: format(value, index, values),
|
|
value: value,
|
|
perc: fraction(value)
|
|
});
|
|
});
|
|
}
|
|
|
|
else {
|
|
size = values.length - 1;
|
|
fraction = labelFraction(scale);
|
|
|
|
items = values.map(function(value, index) {
|
|
return vegaDataflow.ingest({
|
|
index: index,
|
|
label: format(value, index, values),
|
|
value: value,
|
|
perc: index ? fraction(value) : 0,
|
|
perc2: index === size ? 1 : fraction(values[index+1])
|
|
});
|
|
});
|
|
}
|
|
|
|
out.source = items;
|
|
out.add = items;
|
|
this.value = items;
|
|
|
|
return out;
|
|
};
|
|
|
|
var Paths = vegaUtil.fastmap({
|
|
'line': line,
|
|
'line-radial': lineR,
|
|
'arc': arc,
|
|
'arc-radial': arcR,
|
|
'curve': curve,
|
|
'curve-radial': curveR,
|
|
'orthogonal-horizontal': orthoX,
|
|
'orthogonal-vertical': orthoY,
|
|
'orthogonal-radial': orthoR,
|
|
'diagonal-horizontal': diagonalX,
|
|
'diagonal-vertical': diagonalY,
|
|
'diagonal-radial': diagonalR
|
|
});
|
|
|
|
function sourceX(t) { return t.source.x; }
|
|
function sourceY(t) { return t.source.y; }
|
|
function targetX(t) { return t.target.x; }
|
|
function targetY(t) { return t.target.y; }
|
|
|
|
/**
|
|
* Layout paths linking source and target elements.
|
|
* @constructor
|
|
* @param {object} params - The parameters for this operator.
|
|
*/
|
|
function LinkPath(params) {
|
|
vegaDataflow.Transform.call(this, {}, params);
|
|
}
|
|
|
|
LinkPath.Definition = {
|
|
"type": "LinkPath",
|
|
"metadata": {"modifies": true},
|
|
"params": [
|
|
{ "name": "sourceX", "type": "field", "default": "source.x" },
|
|
{ "name": "sourceY", "type": "field", "default": "source.y" },
|
|
{ "name": "targetX", "type": "field", "default": "target.x" },
|
|
{ "name": "targetY", "type": "field", "default": "target.y" },
|
|
{ "name": "orient", "type": "enum", "default": "vertical",
|
|
"values": ["horizontal", "vertical", "radial"] },
|
|
{ "name": "shape", "type": "enum", "default": "line",
|
|
"values": ["line", "arc", "curve", "diagonal", "orthogonal"] },
|
|
{ "name": "require", "type": "signal" },
|
|
{ "name": "as", "type": "string", "default": "path" }
|
|
]
|
|
};
|
|
|
|
var prototype$4 = vegaUtil.inherits(LinkPath, vegaDataflow.Transform);
|
|
|
|
prototype$4.transform = function(_, pulse) {
|
|
var sx = _.sourceX || sourceX,
|
|
sy = _.sourceY || sourceY,
|
|
tx = _.targetX || targetX,
|
|
ty = _.targetY || targetY,
|
|
as = _.as || 'path',
|
|
orient = _.orient || 'vertical',
|
|
shape = _.shape || 'line',
|
|
path = Paths.get(shape + '-' + orient) || Paths.get(shape);
|
|
|
|
if (!path) {
|
|
vegaUtil.error('LinkPath unsupported type: ' + _.shape
|
|
+ (_.orient ? '-' + _.orient : ''));
|
|
}
|
|
|
|
pulse.visit(pulse.SOURCE, function(t) {
|
|
t[as] = path(sx(t), sy(t), tx(t), ty(t));
|
|
});
|
|
|
|
return pulse.reflow(_.modified()).modifies(as);
|
|
};
|
|
|
|
// -- Link Path Generation Methods -----
|
|
|
|
function line(sx, sy, tx, ty) {
|
|
return 'M' + sx + ',' + sy +
|
|
'L' + tx + ',' + ty;
|
|
}
|
|
|
|
function lineR(sa, sr, ta, tr) {
|
|
return line(
|
|
sr * Math.cos(sa), sr * Math.sin(sa),
|
|
tr * Math.cos(ta), tr * Math.sin(ta)
|
|
);
|
|
}
|
|
|
|
function arc(sx, sy, tx, ty) {
|
|
var dx = tx - sx,
|
|
dy = ty - sy,
|
|
rr = Math.sqrt(dx * dx + dy * dy) / 2,
|
|
ra = 180 * Math.atan2(dy, dx) / Math.PI;
|
|
return 'M' + sx + ',' + sy +
|
|
'A' + rr + ',' + rr +
|
|
' ' + ra + ' 0 1' +
|
|
' ' + tx + ',' + ty;
|
|
}
|
|
|
|
function arcR(sa, sr, ta, tr) {
|
|
return arc(
|
|
sr * Math.cos(sa), sr * Math.sin(sa),
|
|
tr * Math.cos(ta), tr * Math.sin(ta)
|
|
);
|
|
}
|
|
|
|
function curve(sx, sy, tx, ty) {
|
|
var dx = tx - sx,
|
|
dy = ty - sy,
|
|
ix = 0.2 * (dx + dy),
|
|
iy = 0.2 * (dy - dx);
|
|
return 'M' + sx + ',' + sy +
|
|
'C' + (sx+ix) + ',' + (sy+iy) +
|
|
' ' + (tx+iy) + ',' + (ty-ix) +
|
|
' ' + tx + ',' + ty;
|
|
}
|
|
|
|
function curveR(sa, sr, ta, tr) {
|
|
return curve(
|
|
sr * Math.cos(sa), sr * Math.sin(sa),
|
|
tr * Math.cos(ta), tr * Math.sin(ta)
|
|
);
|
|
}
|
|
|
|
function orthoX(sx, sy, tx, ty) {
|
|
return 'M' + sx + ',' + sy +
|
|
'V' + ty + 'H' + tx;
|
|
}
|
|
|
|
function orthoY(sx, sy, tx, ty) {
|
|
return 'M' + sx + ',' + sy +
|
|
'H' + tx + 'V' + ty;
|
|
}
|
|
|
|
function orthoR(sa, sr, ta, tr) {
|
|
var sc = Math.cos(sa),
|
|
ss = Math.sin(sa),
|
|
tc = Math.cos(ta),
|
|
ts = Math.sin(ta),
|
|
sf = Math.abs(ta - sa) > Math.PI ? ta <= sa : ta > sa;
|
|
return 'M' + (sr*sc) + ',' + (sr*ss) +
|
|
'A' + sr + ',' + sr + ' 0 0,' + (sf?1:0) +
|
|
' ' + (sr*tc) + ',' + (sr*ts) +
|
|
'L' + (tr*tc) + ',' + (tr*ts);
|
|
}
|
|
|
|
function diagonalX(sx, sy, tx, ty) {
|
|
var m = (sx + tx) / 2;
|
|
return 'M' + sx + ',' + sy +
|
|
'C' + m + ',' + sy +
|
|
' ' + m + ',' + ty +
|
|
' ' + tx + ',' + ty;
|
|
}
|
|
|
|
function diagonalY(sx, sy, tx, ty) {
|
|
var m = (sy + ty) / 2;
|
|
return 'M' + sx + ',' + sy +
|
|
'C' + sx + ',' + m +
|
|
' ' + tx + ',' + m +
|
|
' ' + tx + ',' + ty;
|
|
}
|
|
|
|
function diagonalR(sa, sr, ta, tr) {
|
|
var sc = Math.cos(sa),
|
|
ss = Math.sin(sa),
|
|
tc = Math.cos(ta),
|
|
ts = Math.sin(ta),
|
|
mr = (sr + tr) / 2;
|
|
return 'M' + (sr*sc) + ',' + (sr*ss) +
|
|
'C' + (mr*sc) + ',' + (mr*ss) +
|
|
' ' + (mr*tc) + ',' + (mr*ts) +
|
|
' ' + (tr*tc) + ',' + (tr*ts);
|
|
}
|
|
|
|
/**
|
|
* Pie and donut chart layout.
|
|
* @constructor
|
|
* @param {object} params - The parameters for this operator.
|
|
* @param {function(object): *} params.field - The value field to size pie segments.
|
|
* @param {number} [params.startAngle=0] - The start angle (in radians) of the layout.
|
|
* @param {number} [params.endAngle=2π] - The end angle (in radians) of the layout.
|
|
* @param {boolean} [params.sort] - Boolean flag for sorting sectors by value.
|
|
*/
|
|
function Pie(params) {
|
|
vegaDataflow.Transform.call(this, null, params);
|
|
}
|
|
|
|
Pie.Definition = {
|
|
"type": "Pie",
|
|
"metadata": {"modifies": true},
|
|
"params": [
|
|
{ "name": "field", "type": "field" },
|
|
{ "name": "startAngle", "type": "number", "default": 0 },
|
|
{ "name": "endAngle", "type": "number", "default": 6.283185307179586 },
|
|
{ "name": "sort", "type": "boolean", "default": false },
|
|
{ "name": "as", "type": "string", "array": true, "length": 2, "default": ["startAngle", "endAngle"] }
|
|
]
|
|
};
|
|
|
|
var prototype$5 = vegaUtil.inherits(Pie, vegaDataflow.Transform);
|
|
|
|
prototype$5.transform = function(_, pulse) {
|
|
var as = _.as || ['startAngle', 'endAngle'],
|
|
startAngle = as[0],
|
|
endAngle = as[1],
|
|
field = _.field || vegaUtil.one,
|
|
start = _.startAngle || 0,
|
|
stop = _.endAngle != null ? _.endAngle : 2 * Math.PI,
|
|
data = pulse.source,
|
|
values = data.map(field),
|
|
n = values.length,
|
|
a = start,
|
|
k = (stop - start) / d3Array.sum(values),
|
|
index = d3Array.range(n),
|
|
i, t, v;
|
|
|
|
if (_.sort) {
|
|
index.sort(function(a, b) {
|
|
return values[a] - values[b];
|
|
});
|
|
}
|
|
|
|
for (i=0; i<n; ++i) {
|
|
v = values[index[i]];
|
|
t = data[index[i]];
|
|
t[startAngle] = a;
|
|
t[endAngle] = (a += v * k);
|
|
}
|
|
|
|
this.value = values;
|
|
return pulse.reflow(_.modified()).modifies(as);
|
|
};
|
|
|
|
var DEFAULT_COUNT = 5;
|
|
|
|
function includeZero(scale) {
|
|
const type = scale.type;
|
|
return !scale.bins && (
|
|
type === vegaScale.Linear || type === vegaScale.Pow || type === vegaScale.Sqrt
|
|
);
|
|
}
|
|
|
|
function includePad(type) {
|
|
return vegaScale.isContinuous(type) && type !== vegaScale.Sequential;
|
|
}
|
|
|
|
var SKIP = vegaUtil.toSet([
|
|
'set', 'modified', 'clear', 'type', 'scheme', 'schemeExtent', 'schemeCount',
|
|
'domain', 'domainMin', 'domainMid', 'domainMax',
|
|
'domainRaw', 'domainImplicit', 'nice', 'zero', 'bins',
|
|
'range', 'rangeStep', 'round', 'reverse', 'interpolate', 'interpolateGamma'
|
|
]);
|
|
|
|
/**
|
|
* Maintains a scale function mapping data values to visual channels.
|
|
* @constructor
|
|
* @param {object} params - The parameters for this operator.
|
|
*/
|
|
function Scale(params) {
|
|
vegaDataflow.Transform.call(this, null, params);
|
|
this.modified(true); // always treat as modified
|
|
}
|
|
|
|
var prototype$6 = vegaUtil.inherits(Scale, vegaDataflow.Transform);
|
|
|
|
prototype$6.transform = function(_, pulse) {
|
|
var df = pulse.dataflow,
|
|
scale = this.value,
|
|
key = scaleKey(_);
|
|
|
|
if (!scale || key !== scale.type) {
|
|
this.value = scale = vegaScale.scale(key)();
|
|
}
|
|
|
|
for (key in _) if (!SKIP[key]) {
|
|
// padding is a scale property for band/point but not others
|
|
if (key === 'padding' && includePad(scale.type)) continue;
|
|
// invoke scale property setter, raise warning if not found
|
|
vegaUtil.isFunction(scale[key])
|
|
? scale[key](_[key])
|
|
: df.warn('Unsupported scale property: ' + key);
|
|
}
|
|
|
|
configureRange(scale, _,
|
|
configureBins(scale, _, configureDomain(scale, _, df))
|
|
);
|
|
|
|
return pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS);
|
|
};
|
|
|
|
function scaleKey(_) {
|
|
var t = _.type, d = '', n;
|
|
|
|
// backwards compatibility pre Vega 5.
|
|
if (t === vegaScale.Sequential) return vegaScale.Sequential + '-' + vegaScale.Linear;
|
|
|
|
if (isContinuousColor(_)) {
|
|
n = _.rawDomain ? _.rawDomain.length
|
|
: _.domain ? _.domain.length + +(_.domainMid != null)
|
|
: 0;
|
|
d = n === 2 ? vegaScale.Sequential + '-'
|
|
: n === 3 ? vegaScale.Diverging + '-'
|
|
: '';
|
|
}
|
|
|
|
return ((d + t) || vegaScale.Linear).toLowerCase();
|
|
}
|
|
|
|
function isContinuousColor(_) {
|
|
const t = _.type;
|
|
return vegaScale.isContinuous(t) && t !== vegaScale.Time && t !== vegaScale.UTC && (
|
|
_.scheme || _.range && _.range.length && _.range.every(vegaUtil.isString)
|
|
);
|
|
}
|
|
|
|
function configureDomain(scale, _, df) {
|
|
// check raw domain, if provided use that and exit early
|
|
var raw = rawDomain(scale, _.domainRaw, df);
|
|
if (raw > -1) return raw;
|
|
|
|
var domain = _.domain,
|
|
type = scale.type,
|
|
zero = _.zero || (_.zero === undefined && includeZero(scale)),
|
|
n, mid;
|
|
|
|
if (!domain) return 0;
|
|
|
|
// adjust continuous domain for minimum pixel padding
|
|
if (includePad(type) && _.padding && domain[0] !== vegaUtil.peek(domain)) {
|
|
domain = padDomain(type, domain, _.range, _.padding, _.exponent, _.constant);
|
|
}
|
|
|
|
// adjust domain based on zero, min, max settings
|
|
if (zero || _.domainMin != null || _.domainMax != null || _.domainMid != null) {
|
|
n = ((domain = domain.slice()).length - 1) || 1;
|
|
if (zero) {
|
|
if (domain[0] > 0) domain[0] = 0;
|
|
if (domain[n] < 0) domain[n] = 0;
|
|
}
|
|
if (_.domainMin != null) domain[0] = _.domainMin;
|
|
if (_.domainMax != null) domain[n] = _.domainMax;
|
|
|
|
if (_.domainMid != null) {
|
|
mid = _.domainMid;
|
|
if (mid < domain[0] || mid > domain[n]) {
|
|
df.warn('Scale domainMid exceeds domain min or max.', mid);
|
|
}
|
|
domain.splice(n, 0, mid);
|
|
}
|
|
}
|
|
|
|
// set the scale domain
|
|
scale.domain(domainCheck(type, domain, df));
|
|
|
|
// if ordinal scale domain is defined, prevent implicit
|
|
// domain construction as side-effect of scale lookup
|
|
if (type === vegaScale.Ordinal) {
|
|
scale.unknown(_.domainImplicit ? vegaScale.scaleImplicit : undefined);
|
|
}
|
|
|
|
// perform 'nice' adjustment as requested
|
|
if (_.nice && scale.nice) {
|
|
scale.nice((_.nice !== true && tickCount(scale, _.nice)) || null);
|
|
}
|
|
|
|
// return the cardinality of the domain
|
|
return domain.length;
|
|
}
|
|
|
|
function rawDomain(scale, raw, df) {
|
|
if (raw) {
|
|
scale.domain(domainCheck(scale.type, raw, df));
|
|
return raw.length;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
function padDomain(type, domain, range, pad, exponent, constant) {
|
|
var span = Math.abs(vegaUtil.peek(range) - range[0]),
|
|
frac = span / (span - 2 * pad),
|
|
d = type === vegaScale.Log ? vegaUtil.zoomLog(domain, null, frac)
|
|
: type === vegaScale.Sqrt ? vegaUtil.zoomPow(domain, null, frac, 0.5)
|
|
: type === vegaScale.Pow ? vegaUtil.zoomPow(domain, null, frac, exponent || 1)
|
|
: type === vegaScale.Symlog ? vegaUtil.zoomSymlog(domain, null, frac, constant || 1)
|
|
: vegaUtil.zoomLinear(domain, null, frac);
|
|
|
|
domain = domain.slice();
|
|
domain[0] = d[0];
|
|
domain[domain.length-1] = d[1];
|
|
return domain;
|
|
}
|
|
|
|
function domainCheck(type, domain, df) {
|
|
if (vegaScale.isLogarithmic(type)) {
|
|
// sum signs of domain values
|
|
// if all pos or all neg, abs(sum) === domain.length
|
|
var s = Math.abs(domain.reduce(function(s, v) {
|
|
return s + (v < 0 ? -1 : v > 0 ? 1 : 0);
|
|
}, 0));
|
|
|
|
if (s !== domain.length) {
|
|
df.warn('Log scale domain includes zero: ' + vegaUtil.stringValue(domain));
|
|
}
|
|
}
|
|
return domain;
|
|
}
|
|
|
|
function configureBins(scale, _, count) {
|
|
let bins = _.bins;
|
|
|
|
if (bins && !vegaUtil.isArray(bins)) {
|
|
// generate bin boundary array
|
|
let domain = scale.domain(),
|
|
lo = domain[0],
|
|
hi = vegaUtil.peek(domain),
|
|
start = bins.start == null ? lo : bins.start,
|
|
stop = bins.stop == null ? hi : bins.stop,
|
|
step = bins.step;
|
|
|
|
if (!step) vegaUtil.error('Scale bins parameter missing step property.');
|
|
if (start < lo) start = step * Math.ceil(lo / step);
|
|
if (stop > hi) stop = step * Math.floor(hi / step);
|
|
bins = d3Array.range(start, stop + step / 2, step);
|
|
}
|
|
|
|
if (bins) {
|
|
// assign bin boundaries to scale instance
|
|
scale.bins = bins;
|
|
} else if (scale.bins) {
|
|
// no current bins, remove bins if previously set
|
|
delete scale.bins;
|
|
}
|
|
|
|
// special handling for bin-ordinal scales
|
|
if (scale.type === vegaScale.BinOrdinal) {
|
|
if (!bins) {
|
|
// the domain specifies the bins
|
|
scale.bins = scale.domain();
|
|
} else if (!_.domain && !_.domainRaw) {
|
|
// the bins specify the domain
|
|
scale.domain(bins);
|
|
count = bins.length;
|
|
}
|
|
}
|
|
|
|
// return domain cardinality
|
|
return count;
|
|
}
|
|
|
|
function configureRange(scale, _, count) {
|
|
var type = scale.type,
|
|
round = _.round || false,
|
|
range = _.range;
|
|
|
|
// if range step specified, calculate full range extent
|
|
if (_.rangeStep != null) {
|
|
range = configureRangeStep(type, _, count);
|
|
}
|
|
|
|
// else if a range scheme is defined, use that
|
|
else if (_.scheme) {
|
|
range = configureScheme(type, _, count);
|
|
if (vegaUtil.isFunction(range)) {
|
|
if (scale.interpolator) {
|
|
return scale.interpolator(range);
|
|
} else {
|
|
vegaUtil.error(`Scale type ${type} does not support interpolating color schemes.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// given a range array for an interpolating scale, convert to interpolator
|
|
if (range && vegaScale.isInterpolating(type)) {
|
|
return scale.interpolator(
|
|
vegaScale.interpolateColors(flip(range, _.reverse), _.interpolate, _.interpolateGamma)
|
|
);
|
|
}
|
|
|
|
// configure rounding / interpolation
|
|
if (range && _.interpolate && scale.interpolate) {
|
|
scale.interpolate(vegaScale.interpolate(_.interpolate, _.interpolateGamma));
|
|
} else if (vegaUtil.isFunction(scale.round)) {
|
|
scale.round(round);
|
|
} else if (vegaUtil.isFunction(scale.rangeRound)) {
|
|
scale.interpolate(round ? d3Interpolate.interpolateRound : d3Interpolate.interpolate);
|
|
}
|
|
|
|
if (range) scale.range(flip(range, _.reverse));
|
|
}
|
|
|
|
function configureRangeStep(type, _, count) {
|
|
if (type !== vegaScale.Band && type !== vegaScale.Point) {
|
|
vegaUtil.error('Only band and point scales support rangeStep.');
|
|
}
|
|
|
|
// calculate full range based on requested step size and padding
|
|
var outer = (_.paddingOuter != null ? _.paddingOuter : _.padding) || 0,
|
|
inner = type === vegaScale.Point ? 1
|
|
: ((_.paddingInner != null ? _.paddingInner : _.padding) || 0);
|
|
return [0, _.rangeStep * vegaScale.bandSpace(count, inner, outer)];
|
|
}
|
|
|
|
function configureScheme(type, _, count) {
|
|
var extent = _.schemeExtent,
|
|
name, scheme;
|
|
|
|
if (vegaUtil.isArray(_.scheme)) {
|
|
scheme = vegaScale.interpolateColors(_.scheme, _.interpolate, _.interpolateGamma);
|
|
} else {
|
|
name = _.scheme.toLowerCase();
|
|
scheme = vegaScale.scheme(name);
|
|
if (!scheme) vegaUtil.error(`Unrecognized scheme name: ${_.scheme}`);
|
|
}
|
|
|
|
// determine size for potential discrete range
|
|
count = (type === vegaScale.Threshold) ? count + 1
|
|
: (type === vegaScale.BinOrdinal) ? count - 1
|
|
: (type === vegaScale.Quantile || type === vegaScale.Quantize) ? (+_.schemeCount || DEFAULT_COUNT)
|
|
: count;
|
|
|
|
// adjust and/or quantize scheme as appropriate
|
|
return vegaScale.isInterpolating(type) ? adjustScheme(scheme, extent, _.reverse)
|
|
: vegaUtil.isFunction(scheme) ? vegaScale.quantizeInterpolator(adjustScheme(scheme, extent), count)
|
|
: type === vegaScale.Ordinal ? scheme : scheme.slice(0, count);
|
|
}
|
|
|
|
function adjustScheme(scheme, extent, reverse) {
|
|
return (vegaUtil.isFunction(scheme) && (extent || reverse))
|
|
? vegaScale.interpolateRange(scheme, flip(extent || [0, 1], reverse))
|
|
: scheme;
|
|
}
|
|
|
|
function flip(array, reverse) {
|
|
return reverse ? array.slice().reverse() : array;
|
|
}
|
|
|
|
/**
|
|
* Sorts scenegraph items in the pulse source array.
|
|
* @constructor
|
|
* @param {object} params - The parameters for this operator.
|
|
* @param {function(*,*): number} [params.sort] - A comparator
|
|
* function for sorting tuples.
|
|
*/
|
|
function SortItems(params) {
|
|
vegaDataflow.Transform.call(this, null, params);
|
|
}
|
|
|
|
var prototype$7 = vegaUtil.inherits(SortItems, vegaDataflow.Transform);
|
|
|
|
prototype$7.transform = function(_, pulse) {
|
|
var mod = _.modified('sort')
|
|
|| pulse.changed(pulse.ADD)
|
|
|| pulse.modified(_.sort.fields)
|
|
|| pulse.modified('datum');
|
|
|
|
if (mod) pulse.source.sort(vegaDataflow.stableCompare(_.sort));
|
|
|
|
this.modified(mod);
|
|
return pulse;
|
|
};
|
|
|
|
var Zero = 'zero',
|
|
Center = 'center',
|
|
Normalize = 'normalize',
|
|
DefOutput = ['y0', 'y1'];
|
|
|
|
/**
|
|
* Stack layout for visualization elements.
|
|
* @constructor
|
|
* @param {object} params - The parameters for this operator.
|
|
* @param {function(object): *} params.field - The value field to stack.
|
|
* @param {Array<function(object): *>} [params.groupby] - An array of accessors to groupby.
|
|
* @param {function(object,object): number} [params.sort] - A comparator for stack sorting.
|
|
* @param {string} [offset='zero'] - Stack baseline offset. One of 'zero', 'center', 'normalize'.
|
|
*/
|
|
function Stack(params) {
|
|
vegaDataflow.Transform.call(this, null, params);
|
|
}
|
|
|
|
Stack.Definition = {
|
|
"type": "Stack",
|
|
"metadata": {"modifies": true},
|
|
"params": [
|
|
{ "name": "field", "type": "field" },
|
|
{ "name": "groupby", "type": "field", "array": true },
|
|
{ "name": "sort", "type": "compare" },
|
|
{ "name": "offset", "type": "enum", "default": Zero, "values": [Zero, Center, Normalize] },
|
|
{ "name": "as", "type": "string", "array": true, "length": 2, "default": DefOutput }
|
|
]
|
|
};
|
|
|
|
var prototype$8 = vegaUtil.inherits(Stack, vegaDataflow.Transform);
|
|
|
|
prototype$8.transform = function(_, pulse) {
|
|
var as = _.as || DefOutput,
|
|
y0 = as[0],
|
|
y1 = as[1],
|
|
sort = vegaDataflow.stableCompare(_.sort),
|
|
field = _.field || vegaUtil.one,
|
|
stack = _.offset === Center ? stackCenter
|
|
: _.offset === Normalize ? stackNormalize
|
|
: stackZero,
|
|
groups, i, n, max;
|
|
|
|
// partition, sum, and sort the stack groups
|
|
groups = partition(pulse.source, _.groupby, sort, field);
|
|
|
|
// compute stack layouts per group
|
|
for (i=0, n=groups.length, max=groups.max; i<n; ++i) {
|
|
stack(groups[i], max, field, y0, y1);
|
|
}
|
|
|
|
return pulse.reflow(_.modified()).modifies(as);
|
|
};
|
|
|
|
function stackCenter(group, max, field, y0, y1) {
|
|
var last = (max - group.sum) / 2,
|
|
m = group.length,
|
|
j = 0, t;
|
|
|
|
for (; j<m; ++j) {
|
|
t = group[j];
|
|
t[y0] = last;
|
|
t[y1] = (last += Math.abs(field(t)));
|
|
}
|
|
}
|
|
|
|
function stackNormalize(group, max, field, y0, y1) {
|
|
var scale = 1 / group.sum,
|
|
last = 0,
|
|
m = group.length,
|
|
j = 0, v = 0, t;
|
|
|
|
for (; j<m; ++j) {
|
|
t = group[j];
|
|
t[y0] = last;
|
|
t[y1] = last = scale * (v += Math.abs(field(t)));
|
|
}
|
|
}
|
|
|
|
function stackZero(group, max, field, y0, y1) {
|
|
var lastPos = 0,
|
|
lastNeg = 0,
|
|
m = group.length,
|
|
j = 0, v, t;
|
|
|
|
for (; j<m; ++j) {
|
|
t = group[j];
|
|
v = +field(t);
|
|
if (v < 0) {
|
|
t[y0] = lastNeg;
|
|
t[y1] = (lastNeg += v);
|
|
} else {
|
|
t[y0] = lastPos;
|
|
t[y1] = (lastPos += v);
|
|
}
|
|
}
|
|
}
|
|
|
|
function partition(data, groupby, sort, field) {
|
|
var groups = [],
|
|
get = function(f) { return f(t); },
|
|
map, i, n, m, t, k, g, s, max;
|
|
|
|
// partition data points into stack groups
|
|
if (groupby == null) {
|
|
groups.push(data.slice());
|
|
} else {
|
|
for (map={}, i=0, n=data.length; i<n; ++i) {
|
|
t = data[i];
|
|
k = groupby.map(get);
|
|
g = map[k];
|
|
if (!g) {
|
|
map[k] = (g = []);
|
|
groups.push(g);
|
|
}
|
|
g.push(t);
|
|
}
|
|
}
|
|
|
|
// compute sums of groups, sort groups as needed
|
|
for (k=0, max=0, m=groups.length; k<m; ++k) {
|
|
g = groups[k];
|
|
for (i=0, s=0, n=g.length; i<n; ++i) {
|
|
s += Math.abs(field(g[i]));
|
|
}
|
|
g.sum = s;
|
|
if (s > max) max = s;
|
|
if (sort) g.sort(sort);
|
|
}
|
|
groups.max = max;
|
|
|
|
return groups;
|
|
}
|
|
|
|
exports.axisticks = AxisTicks;
|
|
exports.datajoin = DataJoin;
|
|
exports.encode = Encode;
|
|
exports.legendentries = LegendEntries;
|
|
exports.linkpath = LinkPath;
|
|
exports.pie = Pie;
|
|
exports.scale = Scale;
|
|
exports.sortitems = SortItems;
|
|
exports.stack = Stack;
|
|
exports.validTicks = validTicks;
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
})));
|
|
|