vega-lite
Version:
Vega-Lite is a concise high-level language for interactive visualization.
482 lines (390 loc) • 12 kB
text/typescript
import {isObject, isString} from 'vega-util';
import {DateTime, DateTimeExpr, dateTimeExprToExpr, dateTimeToExpr} from './datetime.js';
import {accessPathWithDatum, keys, stringify, varName} from './util.js';
import {hasOwnProperty} from 'vega';
/** Time Unit that only corresponds to only one part of Date objects. */
export const LOCAL_SINGLE_TIMEUNIT_INDEX = {
year: 1,
quarter: 1,
month: 1,
week: 1,
day: 1,
dayofyear: 1,
date: 1,
hours: 1,
minutes: 1,
seconds: 1,
milliseconds: 1,
} as const;
export type LocalSingleTimeUnit = keyof typeof LOCAL_SINGLE_TIMEUNIT_INDEX;
export const TIMEUNIT_PARTS = keys(LOCAL_SINGLE_TIMEUNIT_INDEX);
export function isLocalSingleTimeUnit(timeUnit: string): timeUnit is LocalSingleTimeUnit {
return hasOwnProperty(LOCAL_SINGLE_TIMEUNIT_INDEX, timeUnit);
}
export const UTC_SINGLE_TIMEUNIT_INDEX = {
utcyear: 1,
utcquarter: 1,
utcmonth: 1,
utcweek: 1,
utcday: 1,
utcdayofyear: 1,
utcdate: 1,
utchours: 1,
utcminutes: 1,
utcseconds: 1,
utcmilliseconds: 1,
} as const;
export type UtcSingleTimeUnit = keyof typeof UTC_SINGLE_TIMEUNIT_INDEX;
export type SingleTimeUnit = LocalSingleTimeUnit | UtcSingleTimeUnit;
export const LOCAL_MULTI_TIMEUNIT_INDEX = {
yearquarter: 1,
yearquartermonth: 1,
yearmonth: 1,
yearmonthdate: 1,
yearmonthdatehours: 1,
yearmonthdatehoursminutes: 1,
yearmonthdatehoursminutesseconds: 1,
yearweek: 1,
yearweekday: 1,
yearweekdayhours: 1,
yearweekdayhoursminutes: 1,
yearweekdayhoursminutesseconds: 1,
yeardayofyear: 1,
quartermonth: 1,
monthdate: 1,
monthdatehours: 1,
monthdatehoursminutes: 1,
monthdatehoursminutesseconds: 1,
weekday: 1,
weekdayhours: 1,
weekdayhoursminutes: 1,
weekdayhoursminutesseconds: 1,
dayhours: 1,
dayhoursminutes: 1,
dayhoursminutesseconds: 1,
hoursminutes: 1,
hoursminutesseconds: 1,
minutesseconds: 1,
secondsmilliseconds: 1,
} as const;
export type LocalMultiTimeUnit = keyof typeof LOCAL_MULTI_TIMEUNIT_INDEX;
const BINNED_LOCAL_TIMEUNIT_INDEX = {
binnedyear: 1,
binnedyearquarter: 1,
binnedyearquartermonth: 1,
binnedyearmonth: 1,
binnedyearmonthdate: 1,
binnedyearmonthdatehours: 1,
binnedyearmonthdatehoursminutes: 1,
binnedyearmonthdatehoursminutesseconds: 1,
binnedyearweek: 1,
binnedyearweekday: 1,
binnedyearweekdayhours: 1,
binnedyearweekdayhoursminutes: 1,
binnedyearweekdayhoursminutesseconds: 1,
binnedyeardayofyear: 1,
} as const;
type BinnedLocalTimeUnit = keyof typeof BINNED_LOCAL_TIMEUNIT_INDEX;
const BINNED_UTC_TIMEUNIT_INDEX = {
binnedutcyear: 1,
binnedutcyearquarter: 1,
binnedutcyearquartermonth: 1,
binnedutcyearmonth: 1,
binnedutcyearmonthdate: 1,
binnedutcyearmonthdatehours: 1,
binnedutcyearmonthdatehoursminutes: 1,
binnedutcyearmonthdatehoursminutesseconds: 1,
binnedutcyearweek: 1,
binnedutcyearweekday: 1,
binnedutcyearweekdayhours: 1,
binnedutcyearweekdayhoursminutes: 1,
binnedutcyearweekdayhoursminutesseconds: 1,
binnedutcyeardayofyear: 1,
};
export const BINNED_TIMEUNIT_INDEX = {
...BINNED_LOCAL_TIMEUNIT_INDEX,
...BINNED_UTC_TIMEUNIT_INDEX,
};
type BinnedUtcTimeUnit = keyof typeof BINNED_UTC_TIMEUNIT_INDEX;
export type BinnedTimeUnit = BinnedLocalTimeUnit | BinnedUtcTimeUnit;
export function isBinnedTimeUnit(
timeUnit: TimeUnit | BinnedTimeUnit | TimeUnitParams | undefined,
): timeUnit is BinnedTimeUnit | TimeUnitParams {
if (isObject(timeUnit)) {
return timeUnit.binned;
}
return isBinnedTimeUnitString(timeUnit);
}
export function isBinnedTimeUnitString(timeUnit: TimeUnit | BinnedTimeUnit | undefined): timeUnit is BinnedTimeUnit {
return timeUnit?.startsWith('binned');
}
export const UTC_MULTI_TIMEUNIT_INDEX = {
utcyearquarter: 1,
utcyearquartermonth: 1,
utcyearmonth: 1,
utcyearmonthdate: 1,
utcyearmonthdatehours: 1,
utcyearmonthdatehoursminutes: 1,
utcyearmonthdatehoursminutesseconds: 1,
utcyearweek: 1,
utcyearweekday: 1,
utcyearweekdayhours: 1,
utcyearweekdayhoursminutes: 1,
utcyearweekdayhoursminutesseconds: 1,
utcyeardayofyear: 1,
utcquartermonth: 1,
utcmonthdate: 1,
utcmonthdatehours: 1,
utcmonthdatehoursminutes: 1,
utcmonthdatehoursminutesseconds: 1,
utcweekday: 1,
utcweekdayhours: 1,
utcweekdayhoursminutes: 1,
utcweekdayhoursminutesseconds: 1,
utcdayhours: 1,
utcdayhoursminutes: 1,
utcdayhoursminutesseconds: 1,
utchoursminutes: 1,
utchoursminutesseconds: 1,
utcminutesseconds: 1,
utcsecondsmilliseconds: 1,
} as const;
export type UtcMultiTimeUnit = keyof typeof UTC_MULTI_TIMEUNIT_INDEX;
export type MultiTimeUnit = LocalMultiTimeUnit | UtcMultiTimeUnit;
export type LocalTimeUnit = LocalSingleTimeUnit | LocalMultiTimeUnit;
export type UtcTimeUnit = UtcSingleTimeUnit | UtcMultiTimeUnit;
export function isUTCTimeUnit(t: string): t is UtcTimeUnit {
return t.startsWith('utc');
}
export function getLocalTimeUnitFromUTCTimeUnit(t: UtcTimeUnit): LocalTimeUnit {
return t.substring(3) as LocalTimeUnit;
}
export type TimeUnit = SingleTimeUnit | MultiTimeUnit;
export type TimeUnitFormat =
| 'year'
| 'year-month'
| 'year-month-date'
| 'quarter'
| 'month'
| 'date'
| 'week'
| 'day'
| 'hours'
| 'hours-minutes'
| 'minutes'
| 'seconds'
| 'milliseconds';
export interface TimeUnitTransformParams {
/**
* Defines how date-time values should be binned.
*/
unit?: TimeUnit;
/**
* If no `unit` is specified, maxbins is used to infer time units.
*/
maxbins?: number;
/**
* The number of steps between bins, in terms of the least
* significant unit provided.
*/
step?: number;
/**
* True to use UTC timezone. Equivalent to using a `utc` prefixed `TimeUnit`.
*/
utc?: boolean;
}
/**
* Time Unit Params for encoding predicate, which can specified if the data is already "binned".
*/
export interface TimeUnitParams extends TimeUnitTransformParams {
/**
* Whether the data has already been binned to this time unit.
* If true, Vega-Lite will only format the data, marks, and guides,
* without applying the timeUnit transform to re-bin the data again.
*/
binned?: boolean;
}
// matches vega time unit format specifier
export type TimeFormatConfig = Partial<Record<TimeUnitFormat, string>>;
// In order of increasing specificity
export const VEGALITE_TIMEFORMAT: TimeFormatConfig = {
'year-month': '%b %Y ',
'year-month-date': '%b %d, %Y ',
};
export function getTimeUnitParts(timeUnit: TimeUnit): LocalSingleTimeUnit[] {
return TIMEUNIT_PARTS.filter((part) => containsTimeUnit(timeUnit, part));
}
export function getSmallestTimeUnitPart(timeUnit: TimeUnit): LocalSingleTimeUnit {
const parts = getTimeUnitParts(timeUnit);
return parts[parts.length - 1];
}
/** Returns true if fullTimeUnit contains the timeUnit, false otherwise. */
export function containsTimeUnit(fullTimeUnit: TimeUnit, timeUnit: TimeUnit) {
const index = fullTimeUnit.indexOf(timeUnit);
if (index < 0) {
return false;
}
// exclude milliseconds
if (index > 0 && timeUnit === 'seconds' && fullTimeUnit.charAt(index - 1) === 'i') {
return false;
}
// exclude dayofyear
if (fullTimeUnit.length > index + 3 && timeUnit === 'day' && fullTimeUnit.charAt(index + 3) === 'o') {
return false;
}
if (index > 0 && timeUnit === 'year' && fullTimeUnit.charAt(index - 1) === 'f') {
return false;
}
return true;
}
/**
* Returns Vega expression for a given timeUnit and fieldRef
*/
export function fieldExpr(fullTimeUnit: TimeUnit, field: string, {end}: {end: boolean} = {end: false}): string {
const fieldRef = accessPathWithDatum(field);
const utc = isUTCTimeUnit(fullTimeUnit) ? 'utc' : '';
function func(timeUnit: TimeUnit) {
if (timeUnit === 'quarter') {
// quarter starting at 0 (0,3,6,9).
return `(${utc}quarter(${fieldRef})-1)`;
} else {
return `${utc}${timeUnit}(${fieldRef})`;
}
}
let lastTimeUnit: TimeUnit;
const dateExpr: DateTimeExpr = {};
for (const part of TIMEUNIT_PARTS) {
if (containsTimeUnit(fullTimeUnit, part)) {
(dateExpr as any)[part] = func(part);
lastTimeUnit = part;
}
}
if (end) {
(dateExpr as any)[lastTimeUnit] += '+1';
}
return dateTimeExprToExpr(dateExpr);
}
export function timeUnitSpecifierExpression(timeUnit: TimeUnit) {
if (!timeUnit) {
return undefined;
}
const timeUnitParts = getTimeUnitParts(timeUnit);
return `timeUnitSpecifier(${stringify(timeUnitParts)}, ${stringify(VEGALITE_TIMEFORMAT)})`;
}
/**
* Returns the signal expression used for axis labels for a time unit.
*/
export function formatExpression(timeUnit: TimeUnit, field: string, isUTCScale: boolean): string {
if (!timeUnit) {
return undefined;
}
const expr = timeUnitSpecifierExpression(timeUnit);
// We only use utcFormat for utc scale
// For utc time units, the data is already converted as a part of timeUnit transform.
// Thus, utc time units should use timeFormat to avoid shifting the time twice.
const utc = isUTCScale || isUTCTimeUnit(timeUnit);
return `${utc ? 'utc' : 'time'}Format(${field}, ${expr})`;
}
export function normalizeTimeUnit(timeUnit: TimeUnit | BinnedTimeUnit | TimeUnitParams): TimeUnitParams {
if (!timeUnit) {
return undefined;
}
let params: TimeUnitParams;
if (isString(timeUnit)) {
if (isBinnedTimeUnitString(timeUnit)) {
params = {
unit: timeUnit.substring(6) as TimeUnit,
binned: true,
};
} else {
params = {
unit: timeUnit,
};
}
} else if (isObject(timeUnit)) {
params = {
...timeUnit,
...(timeUnit.unit ? {unit: timeUnit.unit} : {}),
};
}
if (isUTCTimeUnit(params.unit)) {
params.utc = true;
params.unit = getLocalTimeUnitFromUTCTimeUnit(params.unit);
}
return params;
}
export function timeUnitToString(tu: TimeUnit | TimeUnitTransformParams) {
const {utc, ...rest} = normalizeTimeUnit(tu);
if (rest.unit) {
return (
(utc ? 'utc' : '') +
keys(rest)
.map((p) => varName(`${p === 'unit' ? '' : `_${p}_`}${rest[p]}`))
.join('')
);
} else {
// when maxbins is specified instead of units
return `${utc ? 'utc' : ''}timeunit${keys(rest)
.map((p) => varName(`_${p}_${rest[p]}`))
.join('')}`;
}
}
export function durationExpr(
timeUnit: TimeUnit | BinnedTimeUnit | TimeUnitTransformParams,
wrap: (x: string) => string = (x) => x,
) {
const normalizedTimeUnit = normalizeTimeUnit(timeUnit);
const smallestUnitPart = getSmallestTimeUnitPart(normalizedTimeUnit.unit);
if (smallestUnitPart && smallestUnitPart !== 'day') {
const startDate: DateTime = {
year: 2001, // pick a non-leap year
month: 1,
date: 1,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
};
const {step, part} = getDateTimePartAndStep(smallestUnitPart, normalizedTimeUnit.step);
const endDate: DateTime = {
...startDate,
[part]: +startDate[part] + step,
};
// Calculate timestamp duration for the smallest unit listed
return `${wrap(dateTimeToExpr(endDate))} - ${wrap(dateTimeToExpr(startDate))}`;
}
return undefined;
}
const DATE_PARTS = {
year: 1,
month: 1,
date: 1,
hours: 1,
minutes: 1,
seconds: 1,
milliseconds: 1,
} as const;
type DatePart = keyof typeof DATE_PARTS;
export function isDatePart(timeUnit: LocalSingleTimeUnit): timeUnit is DatePart {
return hasOwnProperty(DATE_PARTS, timeUnit);
}
export function getDateTimePartAndStep(
timeUnit: LocalSingleTimeUnit,
step = 1,
): {
part: keyof DateTime;
step: number;
} {
if (isDatePart(timeUnit)) {
return {part: timeUnit, step};
}
switch (timeUnit) {
case 'day':
case 'dayofyear':
return {part: 'date', step};
case 'quarter':
return {part: 'month', step: step * 3};
case 'week':
return {part: 'date', step: step * 7};
}
}