diff --git a/.playground/playgroud.tsx b/.playground/playgroud.tsx index 846a259c40..374ccba481 100644 --- a/.playground/playgroud.tsx +++ b/.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/package.json b/package.json index 173843173c..bf1b4eaf3f 100644 --- a/package.json +++ b/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/src/utils/data/date_time.ts b/src/utils/data/date_time.ts new file mode 100644 index 0000000000..da6f218e74 --- /dev/null +++ b/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/src/utils/data/formatters.ts b/src/utils/data/formatters.ts index 45a0d8c904..468ecffee1 100644 --- a/src/utils/data/formatters.ts +++ b/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/src/utils/scales/scale_continuous.ts b/src/utils/scales/scale_continuous.ts index fb56d2a30d..93e83742ec 100644 --- a/src/utils/scales/scale_continuous.ts +++ b/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/wiki/consuming.md b/wiki/consuming.md index e81d5ee2bf..013ec2da6e 100644 --- a/wiki/consuming.md +++ b/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/wiki/testing.md b/wiki/testing.md index eb18793fb4..2187123607 100644 --- a/wiki/testing.md +++ b/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/yarn.lock b/yarn.lock index 5e53d65b4f..574ac11e70 100644 --- a/yarn.lock +++ b/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"