UNPKG

9.68 kBJavaScriptView Raw
1import {Transform} from 'vega-dataflow';
2import {
3 error, inherits, isArray, isFunction, isString, peek, stringValue,
4 toSet, zoomLinear, zoomLog, zoomPow, zoomSymlog
5} from 'vega-util';
6
7import {
8 Band,
9 BinOrdinal,
10 Diverging,
11 Linear,
12 Log,
13 Ordinal,
14 Point,
15 Pow,
16 Quantile,
17 Quantize,
18 Sequential,
19 Sqrt,
20 Symlog,
21 Threshold,
22 Time,
23 UTC,
24 bandSpace,
25 interpolate as getInterpolate,
26 scale as getScale,
27 scheme as getScheme,
28 interpolateColors,
29 interpolateRange,
30 isContinuous,
31 isInterpolating,
32 isLogarithmic,
33 quantizeInterpolator,
34 scaleImplicit,
35 tickCount
36} from 'vega-scale';
37
38import {range as sequence} from 'd3-array';
39
40import {
41 interpolate,
42 interpolateRound
43} from 'd3-interpolate';
44
45var DEFAULT_COUNT = 5;
46
47function includeZero(scale) {
48 const type = scale.type;
49 return !scale.bins && (
50 type === Linear || type === Pow || type === Sqrt
51 );
52}
53
54function includePad(type) {
55 return isContinuous(type) && type !== Sequential;
56}
57
58var SKIP = toSet([
59 'set', 'modified', 'clear', 'type', 'scheme', 'schemeExtent', 'schemeCount',
60 'domain', 'domainMin', 'domainMid', 'domainMax',
61 'domainRaw', 'domainImplicit', 'nice', 'zero', 'bins',
62 'range', 'rangeStep', 'round', 'reverse', 'interpolate', 'interpolateGamma'
63]);
64
65/**
66 * Maintains a scale function mapping data values to visual channels.
67 * @constructor
68 * @param {object} params - The parameters for this operator.
69 */
70export default function Scale(params) {
71 Transform.call(this, null, params);
72 this.modified(true); // always treat as modified
73}
74
75var prototype = inherits(Scale, Transform);
76
77prototype.transform = function(_, pulse) {
78 var df = pulse.dataflow,
79 scale = this.value,
80 key = scaleKey(_);
81
82 if (!scale || key !== scale.type) {
83 this.value = scale = getScale(key)();
84 }
85
86 for (key in _) if (!SKIP[key]) {
87 // padding is a scale property for band/point but not others
88 if (key === 'padding' && includePad(scale.type)) continue;
89 // invoke scale property setter, raise warning if not found
90 isFunction(scale[key])
91 ? scale[key](_[key])
92 : df.warn('Unsupported scale property: ' + key);
93 }
94
95 configureRange(scale, _,
96 configureBins(scale, _, configureDomain(scale, _, df))
97 );
98
99 return pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS);
100};
101
102function scaleKey(_) {
103 var t = _.type, d = '', n;
104
105 // backwards compatibility pre Vega 5.
106 if (t === Sequential) return Sequential + '-' + Linear;
107
108 if (isContinuousColor(_)) {
109 n = _.rawDomain ? _.rawDomain.length
110 : _.domain ? _.domain.length + +(_.domainMid != null)
111 : 0;
112 d = n === 2 ? Sequential + '-'
113 : n === 3 ? Diverging + '-'
114 : '';
115 }
116
117 return ((d + t) || Linear).toLowerCase();
118}
119
120function isContinuousColor(_) {
121 const t = _.type;
122 return isContinuous(t) && t !== Time && t !== UTC && (
123 _.scheme || _.range && _.range.length && _.range.every(isString)
124 );
125}
126
127function configureDomain(scale, _, df) {
128 // check raw domain, if provided use that and exit early
129 var raw = rawDomain(scale, _.domainRaw, df);
130 if (raw > -1) return raw;
131
132 var domain = _.domain,
133 type = scale.type,
134 zero = _.zero || (_.zero === undefined && includeZero(scale)),
135 n, mid;
136
137 if (!domain) return 0;
138
139 // adjust continuous domain for minimum pixel padding
140 if (includePad(type) && _.padding && domain[0] !== peek(domain)) {
141 domain = padDomain(type, domain, _.range, _.padding, _.exponent, _.constant);
142 }
143
144 // adjust domain based on zero, min, max settings
145 if (zero || _.domainMin != null || _.domainMax != null || _.domainMid != null) {
146 n = ((domain = domain.slice()).length - 1) || 1;
147 if (zero) {
148 if (domain[0] > 0) domain[0] = 0;
149 if (domain[n] < 0) domain[n] = 0;
150 }
151 if (_.domainMin != null) domain[0] = _.domainMin;
152 if (_.domainMax != null) domain[n] = _.domainMax;
153
154 if (_.domainMid != null) {
155 mid = _.domainMid;
156 if (mid < domain[0] || mid > domain[n]) {
157 df.warn('Scale domainMid exceeds domain min or max.', mid);
158 }
159 domain.splice(n, 0, mid);
160 }
161 }
162
163 // set the scale domain
164 scale.domain(domainCheck(type, domain, df));
165
166 // if ordinal scale domain is defined, prevent implicit
167 // domain construction as side-effect of scale lookup
168 if (type === Ordinal) {
169 scale.unknown(_.domainImplicit ? scaleImplicit : undefined);
170 }
171
172 // perform 'nice' adjustment as requested
173 if (_.nice && scale.nice) {
174 scale.nice((_.nice !== true && tickCount(scale, _.nice)) || null);
175 }
176
177 // return the cardinality of the domain
178 return domain.length;
179}
180
181function rawDomain(scale, raw, df) {
182 if (raw) {
183 scale.domain(domainCheck(scale.type, raw, df));
184 return raw.length;
185 } else {
186 return -1;
187 }
188}
189
190function padDomain(type, domain, range, pad, exponent, constant) {
191 var span = Math.abs(peek(range) - range[0]),
192 frac = span / (span - 2 * pad),
193 d = type === Log ? zoomLog(domain, null, frac)
194 : type === Sqrt ? zoomPow(domain, null, frac, 0.5)
195 : type === Pow ? zoomPow(domain, null, frac, exponent || 1)
196 : type === Symlog ? zoomSymlog(domain, null, frac, constant || 1)
197 : zoomLinear(domain, null, frac);
198
199 domain = domain.slice();
200 domain[0] = d[0];
201 domain[domain.length-1] = d[1];
202 return domain;
203}
204
205function domainCheck(type, domain, df) {
206 if (isLogarithmic(type)) {
207 // sum signs of domain values
208 // if all pos or all neg, abs(sum) === domain.length
209 var s = Math.abs(domain.reduce(function(s, v) {
210 return s + (v < 0 ? -1 : v > 0 ? 1 : 0);
211 }, 0));
212
213 if (s !== domain.length) {
214 df.warn('Log scale domain includes zero: ' + stringValue(domain));
215 }
216 }
217 return domain;
218}
219
220function configureBins(scale, _, count) {
221 let bins = _.bins;
222
223 if (bins && !isArray(bins)) {
224 // generate bin boundary array
225 let domain = scale.domain(),
226 lo = domain[0],
227 hi = peek(domain),
228 start = bins.start == null ? lo : bins.start,
229 stop = bins.stop == null ? hi : bins.stop,
230 step = bins.step;
231
232 if (!step) error('Scale bins parameter missing step property.');
233 if (start < lo) start = step * Math.ceil(lo / step);
234 if (stop > hi) stop = step * Math.floor(hi / step);
235 bins = sequence(start, stop + step / 2, step);
236 }
237
238 if (bins) {
239 // assign bin boundaries to scale instance
240 scale.bins = bins;
241 } else if (scale.bins) {
242 // no current bins, remove bins if previously set
243 delete scale.bins;
244 }
245
246 // special handling for bin-ordinal scales
247 if (scale.type === BinOrdinal) {
248 if (!bins) {
249 // the domain specifies the bins
250 scale.bins = scale.domain();
251 } else if (!_.domain && !_.domainRaw) {
252 // the bins specify the domain
253 scale.domain(bins);
254 count = bins.length;
255 }
256 }
257
258 // return domain cardinality
259 return count;
260}
261
262function configureRange(scale, _, count) {
263 var type = scale.type,
264 round = _.round || false,
265 range = _.range;
266
267 // if range step specified, calculate full range extent
268 if (_.rangeStep != null) {
269 range = configureRangeStep(type, _, count);
270 }
271
272 // else if a range scheme is defined, use that
273 else if (_.scheme) {
274 range = configureScheme(type, _, count);
275 if (isFunction(range)) {
276 if (scale.interpolator) {
277 return scale.interpolator(range);
278 } else {
279 error(`Scale type ${type} does not support interpolating color schemes.`);
280 }
281 }
282 }
283
284 // given a range array for an interpolating scale, convert to interpolator
285 if (range && isInterpolating(type)) {
286 return scale.interpolator(
287 interpolateColors(flip(range, _.reverse), _.interpolate, _.interpolateGamma)
288 );
289 }
290
291 // configure rounding / interpolation
292 if (range && _.interpolate && scale.interpolate) {
293 scale.interpolate(getInterpolate(_.interpolate, _.interpolateGamma));
294 } else if (isFunction(scale.round)) {
295 scale.round(round);
296 } else if (isFunction(scale.rangeRound)) {
297 scale.interpolate(round ? interpolateRound : interpolate);
298 }
299
300 if (range) scale.range(flip(range, _.reverse));
301}
302
303function configureRangeStep(type, _, count) {
304 if (type !== Band && type !== Point) {
305 error('Only band and point scales support rangeStep.');
306 }
307
308 // calculate full range based on requested step size and padding
309 var outer = (_.paddingOuter != null ? _.paddingOuter : _.padding) || 0,
310 inner = type === Point ? 1
311 : ((_.paddingInner != null ? _.paddingInner : _.padding) || 0);
312 return [0, _.rangeStep * bandSpace(count, inner, outer)];
313}
314
315function configureScheme(type, _, count) {
316 var extent = _.schemeExtent,
317 name, scheme;
318
319 if (isArray(_.scheme)) {
320 scheme = interpolateColors(_.scheme, _.interpolate, _.interpolateGamma);
321 } else {
322 name = _.scheme.toLowerCase();
323 scheme = getScheme(name);
324 if (!scheme) error(`Unrecognized scheme name: ${_.scheme}`);
325 }
326
327 // determine size for potential discrete range
328 count = (type === Threshold) ? count + 1
329 : (type === BinOrdinal) ? count - 1
330 : (type === Quantile || type === Quantize) ? (+_.schemeCount || DEFAULT_COUNT)
331 : count;
332
333 // adjust and/or quantize scheme as appropriate
334 return isInterpolating(type) ? adjustScheme(scheme, extent, _.reverse)
335 : isFunction(scheme) ? quantizeInterpolator(adjustScheme(scheme, extent), count)
336 : type === Ordinal ? scheme : scheme.slice(0, count);
337}
338
339function adjustScheme(scheme, extent, reverse) {
340 return (isFunction(scheme) && (extent || reverse))
341 ? interpolateRange(scheme, flip(extent || [0, 1], reverse))
342 : scheme;
343}
344
345function flip(array, reverse) {
346 return reverse ? array.slice().reverse() : array;
347}
348