From cd15932aaddb15682a1fd71ca38fa67b87793854 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Wed, 23 Oct 2019 23:36:34 +0200 Subject: [PATCH] fix: switch to momentjs to handle timezones (#436) The current used time library, Luxon is using Intl.DateTimeFormatter as the main source for handling time zones. That library depends on browsers implementation. IE11 doesn't have any timezone and it will need a polyfill. This PR replace luxon with moment-timezone as peerDependency. I'm leaving luxon as a devDependency on the tests and on stories. --- packages/osd-charts/.playground/playgroud.tsx | 2 +- packages/osd-charts/package.json | 9 +++++-- .../osd-charts/src/utils/data/date_time.ts | 11 ++++++++ .../osd-charts/src/utils/data/formatters.ts | 25 ++++++++----------- .../src/utils/scales/scale_continuous.ts | 20 +++++++-------- packages/osd-charts/wiki/consuming.md | 5 +++- packages/osd-charts/wiki/testing.md | 8 ++++-- packages/osd-charts/yarn.lock | 19 ++++++++++++++ 8 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 packages/osd-charts/src/utils/data/date_time.ts diff --git a/packages/osd-charts/.playground/playgroud.tsx b/packages/osd-charts/.playground/playgroud.tsx index 846a259c40b2..374ccba4810a 100644 --- a/packages/osd-charts/.playground/playgroud.tsx +++ b/packages/osd-charts/.playground/playgroud.tsx @@ -37,8 +37,8 @@ export class Playground extends React.Component<{}, { dataLimit: boolean }> { yScaleType={ScaleType.Linear} xAccessor={0} yAccessors={[1]} + timeZone={'US/Pacific'} data={data} - timeZone={'utc+8'} /> diff --git a/packages/osd-charts/package.json b/packages/osd-charts/package.json index 173843173c61..bf1b4eaf3f5c 100644 --- a/packages/osd-charts/package.json +++ b/packages/osd-charts/package.json @@ -76,6 +76,8 @@ "@types/jest-environment-puppeteer": "^4.3.1", "@types/jest-image-snapshot": "^2.8.0", "@types/lodash": "^4.14.121", + "@types/luxon": "^1.11.1", + "@types/moment-timezone": "^0.5.12", "@types/puppeteer": "^1.19.1", "@types/react": "^16.8.5", "@types/react-dom": "^16.8.2", @@ -110,6 +112,8 @@ "jest-puppeteer": "^4.3.0", "jest-puppeteer-docker": "^1.2.0", "lorem-ipsum": "^2.0.3", + "luxon": "^1.11.3", + "moment-timezone": "^0.5.27", "node-sass": "^4.11.0", "postcss-cli": "^6.1.3", "prettier": "1.16.4", @@ -132,7 +136,6 @@ }, "dependencies": { "@types/d3-shape": "^1.3.1", - "@types/luxon": "^1.11.1", "classnames": "^2.2.6", "d3-array": "^1.2.4", "d3-collection": "^1.0.7", @@ -140,7 +143,6 @@ "d3-shape": "^1.3.4", "fp-ts": "^1.14.2", "konva": "^2.6.0", - "luxon": "^1.11.3", "mobx": "^4.9.2", "mobx-react": "^5.4.3", "newtype-ts": "^0.2.4", @@ -153,6 +155,9 @@ "ts-debounce": "^1.0.0", "uuid": "^3.3.2" }, + "peerDependencies": { + "moment-timezone": "^0.5.27" + }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" diff --git a/packages/osd-charts/src/utils/data/date_time.ts b/packages/osd-charts/src/utils/data/date_time.ts new file mode 100644 index 000000000000..da6f218e7415 --- /dev/null +++ b/packages/osd-charts/src/utils/data/date_time.ts @@ -0,0 +1,11 @@ +import moment from 'moment-timezone'; + +export function getMomentWithTz(date: number | Date, timeZone?: string) { + if (timeZone === 'local' || !timeZone) { + return moment(date); + } + if (timeZone.toLowerCase().startsWith('utc+') || timeZone.toLowerCase().startsWith('utc-')) { + return moment(date).utcOffset(Number(timeZone.slice(3))); + } + return moment.tz(date, timeZone); +} diff --git a/packages/osd-charts/src/utils/data/formatters.ts b/packages/osd-charts/src/utils/data/formatters.ts index 45a0d8c904e8..468ecffee1b2 100644 --- a/packages/osd-charts/src/utils/data/formatters.ts +++ b/packages/osd-charts/src/utils/data/formatters.ts @@ -1,35 +1,30 @@ -import { DateTime, Interval } from 'luxon'; import { TickFormatter, TickFormatterOptions } from '../../chart_types/xy_chart/utils/specs'; +import { getMomentWithTz } from './date_time'; +import moment from 'moment-timezone'; export function timeFormatter(format: string): TickFormatter { return (value: number, options?: TickFormatterOptions): string => { - const dateTimeOptions = options && options.timeZone ? { zone: options.timeZone } : undefined; - return DateTime.fromMillis(value, dateTimeOptions).toFormat(format); + return getMomentWithTz(value, options && options.timeZone).format(format); }; } export function niceTimeFormatter(domain: [number, number]): TickFormatter { - const minDate = DateTime.fromMillis(domain[0]); - const maxDate = DateTime.fromMillis(domain[1]); - const diff = Interval.fromDateTimes(minDate, maxDate); - const format = niceTimeFormat(diff); + const minDate = moment(domain[0]); + const maxDate = moment(domain[1]); + const diff = maxDate.diff(minDate, 'days'); + const format = niceTimeFormatByDay(diff); return timeFormatter(format); } -export function niceTimeFormat(interval: Interval) { - const days = interval.length('days'); - return niceTimeFormatByDay(days); -} - export function niceTimeFormatByDay(days: number) { if (days > 30) { - return 'yyyy-MM-dd'; + return 'YYYY-MM-DD'; } if (days > 7 && days <= 30) { - return 'MMMM dd'; + return 'MMMM DD'; } if (days > 1 && days <= 7) { - return 'MM-dd HH:mm'; + return 'MM-DD HH:mm'; } return 'HH:mm:ss'; } diff --git a/packages/osd-charts/src/utils/scales/scale_continuous.ts b/packages/osd-charts/src/utils/scales/scale_continuous.ts index fb56d2a30d3a..93e83742ec24 100644 --- a/packages/osd-charts/src/utils/scales/scale_continuous.ts +++ b/packages/osd-charts/src/utils/scales/scale_continuous.ts @@ -9,10 +9,10 @@ import { ScalePower, ScaleTime, } from 'd3-scale'; -import { DateTime } from 'luxon'; import { clamp, mergePartial } from '../commons'; import { ScaleContinuousType, ScaleType, Scale } from './scales'; +import { getMomentWithTz } from '../data/date_time'; /** * d3 scales excluding time scale @@ -186,11 +186,11 @@ export class ScaleContinuous implements Scale { this.totalBarsInCluster = totalBarsInCluster; this.isSingleValueHistogram = isSingleValueHistogram; if (type === ScaleType.Time) { - const startDomain = DateTime.fromMillis(this.domain[0], { zone: this.timeZone }); - const endDomain = DateTime.fromMillis(this.domain[1], { zone: this.timeZone }); - const offset = startDomain.offset; - const shiftedDomainMin = startDomain.plus({ minutes: offset }).toMillis(); - const shiftedDomainMax = endDomain.plus({ minutes: offset }).toMillis(); + const startDomain = getMomentWithTz(this.domain[0], this.timeZone); + const endDomain = getMomentWithTz(this.domain[1], this.timeZone); + const offset = startDomain.utcOffset(); + const shiftedDomainMin = startDomain.add(offset, 'minutes').valueOf(); + const shiftedDomainMax = endDomain.add(offset, 'minutes').valueOf(); const tzShiftedScale = scaleUtc().domain([shiftedDomainMin, shiftedDomainMax]); const rawTicks = tzShiftedScale.ticks(ticks); @@ -198,9 +198,9 @@ export class ScaleContinuous implements Scale { const hasHourTicks = timePerTick < 1000 * 60 * 60 * 12; this.tickValues = rawTicks.map((d: Date) => { - const currentDateTime = DateTime.fromJSDate(d, { zone: this.timeZone }); - const currentOffset = hasHourTicks ? offset : currentDateTime.offset; - return currentDateTime.minus({ minutes: currentOffset }).toMillis(); + const currentDateTime = getMomentWithTz(d, this.timeZone); + const currentOffset = hasHourTicks ? offset : currentDateTime.utcOffset(); + return currentDateTime.subtract(currentOffset, 'minutes').valueOf(); }); } else { /** @@ -241,7 +241,7 @@ export class ScaleContinuous implements Scale { invert(value: number): number { let invertedValue = this.d3Scale.invert(value); if (this.type === ScaleType.Time) { - invertedValue = DateTime.fromJSDate(invertedValue as Date).toMillis(); + invertedValue = getMomentWithTz(invertedValue, this.timeZone).valueOf(); } return invertedValue as number; diff --git a/packages/osd-charts/wiki/consuming.md b/packages/osd-charts/wiki/consuming.md index e81d5ee2bf60..013ec2da6e4a 100644 --- a/packages/osd-charts/wiki/consuming.md +++ b/packages/osd-charts/wiki/consuming.md @@ -14,7 +14,10 @@ To use Elastic Charts code in Kibana, check if `@elastic/charts` packages is alr ## Using Elastic Charts in a standalone project -You can consume Elastic Charts in standalone projects, such as plugins and prototypes. +You can consume Elastic Charts in standalone projects, such as plugins and prototypes. Elastic-Charts has a peer dependency on [moment-timezone](https://momentjs.com/timezone/). Add that dependency on your main project with: +``` +yarn add moment-timezone +``` ### Importing CSS diff --git a/packages/osd-charts/wiki/testing.md b/packages/osd-charts/wiki/testing.md index eb18793fb4c6..2187123607e8 100644 --- a/packages/osd-charts/wiki/testing.md +++ b/packages/osd-charts/wiki/testing.md @@ -3,17 +3,22 @@ Every jest test in this code runs in the local timezone. ### Run on every timezone + If you want to run a test suite on multiple timezone, just prepend a `tz` to the standard `.test.ts` extension like: + ``` formatters.tz.test.ts scales.tz.test.ts ``` + Your test will run in the standard local timezone and in all the configured timezones (UTC, America/New_York and Asia/Tokyo). The local timezone tests results are included in the code coverage. ### Run on a specific timezone + If you are interested into explicitly create test for a timezone (we are now testing only on UTC, America/New_York and Asia/Tokyo) you have to prepend the `tz` but add one of the following postfix before the file extension: `utc`,`ny`,`jp`, for example + ```sh your_file_name.tz.test.utc.ts your_file_name.tz.test.ny.ts @@ -22,5 +27,4 @@ your_file_name.tz.test.jp.ts Each test with the specific timezone postfix will be executed only on that timezone. -These tests are not included in the code coverage yet. - +These tests are not included in the code coverage yet. diff --git a/packages/osd-charts/yarn.lock b/packages/osd-charts/yarn.lock index 5e53d65b4faa..574ac11e7024 100644 --- a/packages/osd-charts/yarn.lock +++ b/packages/osd-charts/yarn.lock @@ -2685,6 +2685,13 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/moment-timezone@^0.5.12": + version "0.5.12" + resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.12.tgz#0fb680c03db194fe8ff4551eaeb1eec8d3d80e9f" + integrity sha512-hnHH2+Efg2vExr/dSz+IX860nSiyk9Sk4pJF2EmS11lRpMcNXeB4KBW5xcgw2QPsb9amTXdsVNEe5IoJXiT0uw== + dependencies: + moment ">=2.14.0" + "@types/node@*": version "12.0.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.2.tgz#3452a24edf9fea138b48fad4a0a028a683da1e40" @@ -10743,6 +10750,18 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +moment-timezone@^0.5.27: + version "0.5.27" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.27.tgz#73adec8139b6fe30452e78f210f27b1f346b8877" + integrity sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@>=2.14.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + monocle-ts@^1.0.0: version "1.7.2" resolved "https://registry.yarnpkg.com/monocle-ts/-/monocle-ts-1.7.2.tgz#d9825ae18846ab63f915cb6f2194a78a40025610"