-
Notifications
You must be signed in to change notification settings - Fork 121
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: heatmap snap domain to interval (#1253)
Heatmaps with a time scale on the X-axis now adjust the rendered time range to fully cover the edges when a custom domain is used. We also took this opportunity to clean and abstract the code used for computing and handling date and time. The library is now able to abstract from the underlying implementation library (moment or luxon at the moment), allowing us to experiment and work with diverse libraries removing some tech debt. fix #1165
- Loading branch information
Showing
11 changed files
with
594 additions
and
18 deletions.
There are no files selected for viewing
Binary file added
BIN
+8.29 KB
...sual-tests-for-all-stories-heatmap-alpha-time-visually-looks-correct-1-snap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
// NOTE: to switch implementation just change the imported file (moment,luxon) | ||
import { | ||
addTimeToObj, | ||
timeObjToUnixTimestamp, | ||
startTimeOfObj, | ||
endTimeOfObj, | ||
timeObjFromAny, | ||
timeObjToUTCOffset, | ||
subtractTimeToObj, | ||
formatTimeObj, | ||
diffTimeObjs, | ||
} from './moment'; | ||
import { CalendarIntervalUnit, CalendarObj, DateTime, FixedIntervalUnit, Minutes, UnixTimestamp } from './types'; | ||
|
||
/** @internal */ | ||
export function addTime( | ||
dateTime: DateTime, | ||
timeZone: string | undefined, | ||
unit: keyof CalendarObj, | ||
count: number, | ||
): UnixTimestamp { | ||
return timeObjToUnixTimestamp(addTimeToObj(getTimeObj(dateTime, timeZone), unit, count)); | ||
} | ||
|
||
/** @internal */ | ||
export function subtractTime( | ||
dateTime: DateTime, | ||
timeZone: string | undefined, | ||
unit: keyof CalendarObj, | ||
count: number, | ||
): UnixTimestamp { | ||
return timeObjToUnixTimestamp(subtractTimeToObj(getTimeObj(dateTime, timeZone), unit, count)); | ||
} | ||
|
||
/** @internal */ | ||
export function getUnixTimestamp(dateTime: DateTime, timeZone?: string): UnixTimestamp { | ||
return timeObjToUnixTimestamp(getTimeObj(dateTime, timeZone)); | ||
} | ||
|
||
/** @internal */ | ||
export function startOf( | ||
dateTime: DateTime, | ||
timeZone: string | undefined, | ||
unit: CalendarIntervalUnit | FixedIntervalUnit, | ||
): UnixTimestamp { | ||
return timeObjToUnixTimestamp(startTimeOfObj(getTimeObj(dateTime, timeZone), unit)); | ||
} | ||
|
||
/** @internal */ | ||
export function endOf( | ||
dateTime: DateTime, | ||
timeZone: string | undefined, | ||
unit: CalendarIntervalUnit | FixedIntervalUnit, | ||
): UnixTimestamp { | ||
return timeObjToUnixTimestamp(endTimeOfObj(getTimeObj(dateTime, timeZone), unit)); | ||
} | ||
|
||
function getTimeObj(dateTime: DateTime, timeZone?: string) { | ||
return timeObjFromAny(dateTime, timeZone); | ||
} | ||
|
||
/** @internal */ | ||
export function getUTCOffset(dateTime: DateTime, timeZone?: string): Minutes { | ||
return timeObjToUTCOffset(getTimeObj(dateTime, timeZone)); | ||
} | ||
|
||
/** @internal */ | ||
export function formatTime(dateTime: DateTime, timeZone: string | undefined, format: string) { | ||
return formatTimeObj(getTimeObj(dateTime, timeZone), format); | ||
} | ||
|
||
/** @internal */ | ||
export function diff( | ||
dateTime1: DateTime, | ||
timeZone1: string | undefined, | ||
dateTime2: DateTime, | ||
timeZone2: string | undefined, | ||
unit: CalendarIntervalUnit | FixedIntervalUnit, | ||
) { | ||
return diffTimeObjs(getTimeObj(dateTime1, timeZone1), getTimeObj(dateTime2, timeZone2), unit); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { DateTime } from 'luxon'; | ||
|
||
import { snapDateToESInterval } from './elasticsearch'; | ||
|
||
describe('snap to interval', () => { | ||
it('should snap to begin of calendar interval', () => { | ||
const initialDate = DateTime.fromISO('2020-01-03T07:00:01Z'); | ||
const snappedDate = snapDateToESInterval( | ||
initialDate.toMillis(), | ||
{ type: 'calendar', unit: 'd', quantity: 1 }, | ||
'start', | ||
'UTC', | ||
); | ||
expect(DateTime.fromMillis(snappedDate, { zone: 'utc' }).toISO()).toBe('2020-01-03T00:00:00.000Z'); | ||
}); | ||
|
||
it('should snap to end of calendar interval', () => { | ||
const initialDate = DateTime.fromISO('2020-01-03T07:00:01Z'); | ||
const snappedDate = snapDateToESInterval( | ||
initialDate.toMillis(), | ||
{ type: 'calendar', unit: 'd', quantity: 1 }, | ||
'end', | ||
'UTC', | ||
); | ||
expect(DateTime.fromMillis(snappedDate, { zone: 'utc' }).toISO()).toBe('2020-01-03T23:59:59.999Z'); | ||
}); | ||
|
||
it('should snap to begin of fixed interval', () => { | ||
const initialDate = DateTime.fromISO('2020-01-03T07:00:01Z'); | ||
const snappedDate = snapDateToESInterval( | ||
initialDate.toMillis(), | ||
{ type: 'fixed', unit: 'm', quantity: 30 }, | ||
'start', | ||
'UTC', | ||
); | ||
expect(DateTime.fromMillis(snappedDate, { zone: 'utc' }).toISO()).toBe('2020-01-03T07:00:00.000Z'); | ||
}); | ||
|
||
it('should snap to end of fixed interval', () => { | ||
const initialDate = DateTime.fromISO('2020-01-03T07:00:01Z'); | ||
const snappedDate = snapDateToESInterval( | ||
initialDate.toMillis(), | ||
{ type: 'fixed', unit: 'm', quantity: 30 }, | ||
'end', | ||
'UTC', | ||
); | ||
expect(DateTime.fromMillis(snappedDate, { zone: 'utc' }).toISO()).toBe('2020-01-03T07:29:59.999Z'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { TimeMs } from '../../common/geometry'; | ||
import { endOf, getUnixTimestamp, startOf } from './chrono'; | ||
import { CalendarIntervalUnit, FixedIntervalUnit, UnixTimestamp } from './types'; | ||
|
||
/** @internal */ | ||
export type ESCalendarIntervalUnit = | ||
| 'minute' | ||
| 'm' | ||
| 'hour' | ||
| 'h' | ||
| 'day' | ||
| 'd' | ||
| 'week' | ||
| 'w' | ||
| 'month' | ||
| 'M' | ||
| 'quarter' | ||
| 'q' | ||
| 'year' | ||
| 'y'; | ||
|
||
type ESFixedIntervalUnit = 'ms' | 's' | 'm' | 'h' | 'd'; | ||
|
||
/** @internal */ | ||
export const ES_FIXED_INTERVAL_UNIT_TO_BASE: Record<ESFixedIntervalUnit, TimeMs> = { | ||
ms: 1, | ||
s: 1000, | ||
m: 1000 * 60, | ||
h: 1000 * 60 * 60, | ||
d: 1000 * 60 * 60 * 24, | ||
}; | ||
|
||
/** @internal */ | ||
export type ESCalendarInterval = { | ||
type: 'calendar'; | ||
unit: ESCalendarIntervalUnit; | ||
quantity: number; | ||
}; | ||
|
||
/** @internal */ | ||
export interface ESFixedInterval { | ||
type: 'fixed'; | ||
unit: ESFixedIntervalUnit; | ||
quantity: number; | ||
} | ||
|
||
const esCalendarIntervalToChronoInterval: Record<ESCalendarIntervalUnit, CalendarIntervalUnit | FixedIntervalUnit> = { | ||
minute: 'minute', | ||
m: 'minute', | ||
hour: 'hour', | ||
h: 'hour', | ||
day: 'day', | ||
d: 'day', | ||
week: 'week', | ||
w: 'week', | ||
month: 'month', | ||
M: 'month', | ||
quarter: 'quarter', | ||
q: 'quarter', | ||
year: 'year', | ||
y: 'year', | ||
}; | ||
|
||
/** @internal */ | ||
export function snapDateToESInterval( | ||
date: number | Date, | ||
interval: ESCalendarInterval | ESFixedInterval, | ||
snapTo: 'start' | 'end', | ||
timeZone?: string, | ||
): UnixTimestamp { | ||
return isCalendarInterval(interval) | ||
? esCalendarIntervalSnap(date, interval, snapTo, timeZone) | ||
: esFixedIntervalSnap(date, interval, snapTo, timeZone); | ||
} | ||
|
||
function isCalendarInterval(interval: ESCalendarInterval | ESFixedInterval): interval is ESCalendarInterval { | ||
return interval.type === 'calendar'; | ||
} | ||
|
||
function esCalendarIntervalSnap( | ||
date: number | Date, | ||
interval: ESCalendarInterval, | ||
snapTo: 'start' | 'end', | ||
timeZone?: string, | ||
) { | ||
return snapTo === 'start' | ||
? startOf(date, timeZone, esCalendarIntervalToChronoInterval[interval.unit]) | ||
: endOf(date, timeZone, esCalendarIntervalToChronoInterval[interval.unit]); | ||
} | ||
|
||
function esFixedIntervalSnap( | ||
date: number | Date, | ||
interval: ESFixedInterval, | ||
snapTo: 'start' | 'end', | ||
timeZone?: string, | ||
): UnixTimestamp { | ||
const unitMultiplier = interval.quantity * ES_FIXED_INTERVAL_UNIT_TO_BASE[interval.unit]; | ||
const unixTimestamp = getUnixTimestamp(date, timeZone); | ||
const roundedDate = Math.floor(unixTimestamp / unitMultiplier) * unitMultiplier; | ||
return snapTo === 'start' ? roundedDate : roundedDate + unitMultiplier - 1; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import { DateTime as LuxonDateTime } from 'luxon'; | ||
|
||
import { CalendarIntervalUnit, CalendarObj, DateTime, FixedIntervalUnit, Minutes, UnixTimestamp } from './types'; | ||
|
||
/** @internal */ | ||
export const timeObjFromCalendarObj = ( | ||
yearMonthDayHour: Partial<CalendarObj>, | ||
timeZone: string = 'local', | ||
): LuxonDateTime => LuxonDateTime.fromObject({ ...yearMonthDayHour, zone: timeZone }); | ||
/** @internal */ | ||
export const timeObjFromUnixTimestamp = (unixTimestamp: UnixTimestamp, timeZone: string = 'local'): LuxonDateTime => | ||
LuxonDateTime.fromMillis(unixTimestamp, { zone: timeZone }); | ||
|
||
/** @internal */ | ||
export const timeObjFromDate = (date: Date, timeZone: string = 'local'): LuxonDateTime => | ||
LuxonDateTime.fromJSDate(date, { zone: timeZone }); | ||
|
||
/** @internal */ | ||
export const timeObjFromAny = (time: DateTime, timeZone: string = 'local'): LuxonDateTime => { | ||
return typeof time === 'number' | ||
? timeObjFromUnixTimestamp(time, timeZone) | ||
: time instanceof Date | ||
? timeObjFromDate(time, timeZone) | ||
: timeObjFromCalendarObj(time, timeZone); | ||
}; | ||
|
||
/** @internal */ | ||
export const timeObjToSeconds = (t: LuxonDateTime) => t.toSeconds(); | ||
/** @internal */ | ||
export const timeObjToUnixTimestamp = (t: LuxonDateTime): UnixTimestamp => t.toMillis(); | ||
/** @internal */ | ||
export const timeObjToWeekday = (t: LuxonDateTime) => t.weekday; | ||
/** @internal */ | ||
export const timeObjToYear = (t: LuxonDateTime) => t.year; | ||
/** @internal */ | ||
export const addTimeToObj = (obj: LuxonDateTime, unit: CalendarIntervalUnit | FixedIntervalUnit, count: number) => | ||
obj.plus({ [unit]: count }); | ||
|
||
/** @internal */ | ||
export const subtractTimeToObj = (obj: LuxonDateTime, unit: CalendarIntervalUnit | FixedIntervalUnit, count: number) => | ||
obj.minus({ [unit]: count }); | ||
|
||
/** @internal */ | ||
export const startTimeOfObj = (obj: LuxonDateTime, unit: CalendarIntervalUnit | FixedIntervalUnit) => obj.startOf(unit); | ||
|
||
/** @internal */ | ||
export const endTimeOfObj = (obj: LuxonDateTime, unit: CalendarIntervalUnit | FixedIntervalUnit) => obj.endOf(unit); | ||
|
||
/** @internal */ | ||
export const timeObjToUTCOffset = (obj: LuxonDateTime): Minutes => obj.offset; | ||
|
||
/** @internal */ | ||
export const formatTimeObj = (obj: LuxonDateTime, format: string): string => obj.toFormat(format); | ||
|
||
/** @internal */ | ||
export const diffTimeObjs = ( | ||
obj1: LuxonDateTime, | ||
obj2: LuxonDateTime, | ||
unit: CalendarIntervalUnit | FixedIntervalUnit, | ||
): number => obj1.diff(obj2, unit).as(unit); |
Oops, something went wrong.