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>; // 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}; } }