UNPKG

vega-scale

Version:

Scales and color schemes for visual encoding.

170 lines (149 loc) 5.46 kB
import {isLogarithmic} from './scales.js'; import {Time, UTC} from './scales/types.js'; import {timeInterval, utcInterval} from 'vega-time'; import {error, isArray, isNumber, isObject, isString, peek, span} from 'vega-util'; const defaultFormatter = value => isArray(value) ? value.map(v => String(v)) : String(value); const ascending = (a, b) => a[1] - b[1]; const descending = (a, b) => b[1] - a[1]; /** * 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. */ export function tickCount(scale, count, minStep) { let step; if (isNumber(count)) { if (scale.bins) { count = Math.max(count, scale.bins.length); } if (minStep != null) { count = Math.min( count, Math.floor((span(scale.domain()) / minStep) || 1) + 1 ); } } if (isObject(count)) { step = count.step; count = count.interval; } if (isString(count)) { count = scale.type === Time ? timeInterval(count) : scale.type == UTC ? utcInterval(count) : 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. */ export function validTicks(scale, ticks, count) { let range = scale.range(), lo = range[0], hi = peek(range), cmp = ascending; if (lo > hi) { range = hi; hi = lo; lo = range; cmp = descending; } lo = Math.floor(lo); hi = Math.ceil(hi); // filter ticks to valid values within the range // additionally sort ticks in range order (#2579) ticks = ticks.map(v => [v, scale(v)]) .filter(_ => lo <= _[1] && _[1] <= hi) .sort(cmp) .map(_ => _[0]); if (count > 0 && ticks.length > 1) { const endpoints = [ticks[0], peek(ticks)]; while (ticks.length > count && ticks.length >= 3) { ticks = ticks.filter((_, i) => !(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. */ export function tickValues(scale, count) { return scale.bins ? validTicks(scale, scale.bins, count) : 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. */ export function tickFormat(locale, scale, count, specifier, formatType, noSkip) { const type = scale.type; let format = defaultFormatter; if (type === Time || formatType === Time) { format = locale.timeFormat(specifier); } else if (type === UTC || formatType === UTC) { format = locale.utcFormat(specifier); } else if (isLogarithmic(type)) { const varfmt = locale.formatFloat(specifier); if (noSkip || scale.bins) { format = varfmt; } else { const test = tickLog(scale, count, false); format = _ => test(_) ? varfmt(_) : ''; } } else if (scale.tickFormat) { // if d3 scale has tickFormat, it must be continuous const d = scale.domain(); format = locale.formatSpan(d[0], d[d.length - 1], count, specifier); } else if (specifier) { format = locale.format(specifier); } return format; } export function tickLog(scale, count, values) { const 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 const test = d => { let i = d / Math.pow(base, Math.round(Math.log(d) / logb)); if (i * base < base - 0.5) i *= base; return i <= k; }; return values ? ticks.filter(test) : test; }