diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-timeslip-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-timeslip-visually-looks-correct-1-snap.png index 17457c2110..3a5b9b752e 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-timeslip-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-timeslip-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-can-show-a-finer-raster-than-the-data-bin-width-if-min-interval-is-expressly-specified-to-be-low-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-can-show-a-finer-raster-than-the-data-bin-width-if-min-interval-is-expressly-specified-to-be-low-1-snap.png index fe5dde7812..60bf178fca 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-can-show-a-finer-raster-than-the-data-bin-width-if-min-interval-is-expressly-specified-to-be-low-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-can-show-a-finer-raster-than-the-data-bin-width-if-min-interval-is-expressly-specified-to-be-low-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-renders-multilayer-time-axis-with-a-single-point-and-a-degenerate-zero-width-domain-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-renders-multilayer-time-axis-with-a-single-point-and-a-degenerate-zero-width-domain-1-snap.png new file mode 100644 index 0000000000..02ccae8cf2 Binary files /dev/null and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-renders-multilayer-time-axis-with-a-single-point-and-a-degenerate-zero-width-domain-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-renders-multilayer-time-axis-with-a-single-point-and-an-arbitrary-non-degenerate-domain-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-renders-multilayer-time-axis-with-a-single-point-and-an-arbitrary-non-degenerate-domain-1-snap.png new file mode 100644 index 0000000000..3a1d96cfee Binary files /dev/null and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-renders-multilayer-time-axis-with-a-single-point-and-an-arbitrary-non-degenerate-domain-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-extend-the-domain-on-the-right-with-one-bin-width-with-custom-bin-width-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-extend-the-domain-on-the-right-with-one-bin-width-with-custom-bin-width-1-snap.png index c160466212..34b007127e 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-extend-the-domain-on-the-right-with-one-bin-width-with-custom-bin-width-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-extend-the-domain-on-the-right-with-one-bin-width-with-custom-bin-width-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-extend-the-domain-on-the-right-with-one-bin-width-without-custom-bin-width-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-extend-the-domain-on-the-right-with-one-bin-width-without-custom-bin-width-1-snap.png index fc6778281e..5ba021bd81 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-extend-the-domain-on-the-right-with-one-bin-width-without-custom-bin-width-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-extend-the-domain-on-the-right-with-one-bin-width-without-custom-bin-width-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-not-show-a-raster-that-is-finer-than-the-bin-width-min-interval-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-not-show-a-raster-that-is-finer-than-the-bin-width-min-interval-1-snap.png index cec36a28b9..b06a4bec21 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-not-show-a-raster-that-is-finer-than-the-bin-width-min-interval-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-not-show-a-raster-that-is-finer-than-the-bin-width-min-interval-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-dark-should-switch-to-a-30-minute-raster-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-dark-should-switch-to-a-30-minute-raster-1-snap.png index b7e48ebb41..2e43887c02 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-dark-should-switch-to-a-30-minute-raster-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-dark-should-switch-to-a-30-minute-raster-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-eui-dark-should-switch-to-a-30-minute-raster-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-eui-dark-should-switch-to-a-30-minute-raster-1-snap.png index 9eb8bbb5a1..85014eefa7 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-eui-dark-should-switch-to-a-30-minute-raster-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-eui-dark-should-switch-to-a-30-minute-raster-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-eui-light-should-switch-to-a-30-minute-raster-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-eui-light-should-switch-to-a-30-minute-raster-1-snap.png index 1fafe0ae65..b96a90c76e 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-eui-light-should-switch-to-a-30-minute-raster-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-eui-light-should-switch-to-a-30-minute-raster-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-light-should-switch-to-a-30-minute-raster-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-light-should-switch-to-a-30-minute-raster-1-snap.png index 883d3525c7..f6fa2491e1 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-light-should-switch-to-a-30-minute-raster-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-theme-light-should-switch-to-a-30-minute-raster-1-snap.png differ diff --git a/integration/tests/axis_stories.test.ts b/integration/tests/axis_stories.test.ts index 1c95cec67a..45bc8d28bd 100644 --- a/integration/tests/axis_stories.test.ts +++ b/integration/tests/axis_stories.test.ts @@ -58,6 +58,16 @@ describe('Axis stories', () => { 'http://localhost:9001/?path=/story/area-chart--timeslip&globals=theme:light&knob-Bin width in ms (0: none specifed)=0&knob-Minor grid lines=true&knob-Shift time=8.5&knob-Shorter X axis minor whiskers=true&knob-Stretch time=6.8&knob-Time zoom=120&knob-X axis minor whiskers=true&knob-fallback placement=left-start&knob-layerCount=3&knob-placement=left&knob-placement offset=5&knob-showOverlappingLabels time axis=true&knob-showOverlappingTicks time axis=true&knob-stickTo=MousePosition&knob-Horizontal axis title=&knob-Top X axis=true', ); }); + it('renders multilayer time axis with a single point and an arbitrary non-degenerate domain', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/bar-chart--test-single-histogram-bar-chart&globals=theme:light&knob-non-round time domain start=true&knob-use multilayer time axis=true&knob-use lines instead of bars=true', + ); + }); + it('renders multilayer time axis with a single point and a degenerate, zero width domain', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/bar-chart--test-single-histogram-bar-chart&globals=theme:light&knob-non-round time domain start=&knob-use multilayer time axis=true&knob-use lines instead of bars=true', + ); + }); it('should render proper tick count', async () => { await common.expectChartAtUrlToMatchScreenshot( 'http://localhost:9001/?path=/story/axes--basic&knob-Tick Label Padding=0&knob-debug=&knob-Bottom overlap labels=&knob-Bottom overlap ticks=true&knob-Number of ticks on bottom=20&knob-Left overlap labels=&knob-Left overlap ticks=true&knob-Number of ticks on left=10', diff --git a/packages/charts/src/chart_types/xy_chart/axes/timeslip/rasters.ts b/packages/charts/src/chart_types/xy_chart/axes/timeslip/rasters.ts index ac2b37ca6e..84b9d59bbc 100644 --- a/packages/charts/src/chart_types/xy_chart/axes/timeslip/rasters.ts +++ b/packages/charts/src/chart_types/xy_chart/axes/timeslip/rasters.ts @@ -80,6 +80,12 @@ interface YearToHour extends YearToDay { hour: number; } +// todo DRY up the config for the other time units too, where sensible +const hourFormat: Partial<ConstructorParameters<typeof Intl.DateTimeFormat>[1]> = { + hour: '2-digit', + hour12: false, +}; + /** @internal */ export const rasters = ({ minimumTickPixelDistance, locale }: RasterConfig, timeZone: string) => { const minorDayFormat = new Intl.DateTimeFormat(locale, { day: 'numeric', timeZone }).format; @@ -89,6 +95,14 @@ export const rasters = ({ minimumTickPixelDistance, locale }: RasterConfig, time day: 'numeric', timeZone, }).format; + const detailedHourFormatBase = new Intl.DateTimeFormat(locale, { + year: 'numeric', + month: 'short', + day: 'numeric', + ...hourFormat, + timeZone, + }).format; + const detailedHourFormat = (d: number) => `${detailedHourFormatBase(d)}h`; const years: TimeRaster<TimeBin & { year: number }> = { unit: 'year', @@ -280,15 +294,9 @@ export const rasters = ({ minimumTickPixelDistance, locale }: RasterConfig, time labeled: true, minimumTickPixelDistance: 2 * minimumTickPixelDistance, binStarts: millisecondBinStarts(60 * 60 * 1000), - detailedLabelFormat: new Intl.DateTimeFormat(locale, { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: 'numeric', - timeZone, - }).format, + detailedLabelFormat: detailedHourFormat, minorTickLabelFormat: new Intl.DateTimeFormat(locale, { - hour: 'numeric', + ...hourFormat, timeZone, }).format, minimumPixelsPerSecond: NaN, @@ -333,13 +341,10 @@ export const rasters = ({ minimumTickPixelDistance, locale }: RasterConfig, time Object.assign(b, { nextTimePointSec: i === a.length - 1 ? b.nextTimePointSec : a[i + 1].timePointSec }), ), minorTickLabelFormat: new Intl.DateTimeFormat(locale, { - hour: 'numeric', - timeZone, - }).format, - detailedLabelFormat: new Intl.DateTimeFormat(locale, { - hour: 'numeric', + ...hourFormat, timeZone, }).format, + detailedLabelFormat: detailedHourFormat, minimumPixelsPerSecond: NaN, approxWidthInMs: NaN, }; @@ -358,7 +363,7 @@ export const rasters = ({ minimumTickPixelDistance, locale }: RasterConfig, time year: 'numeric', month: 'short', day: 'numeric', - hour: 'numeric', + ...hourFormat, minute: 'numeric', timeZone, }).format, @@ -409,7 +414,7 @@ export const rasters = ({ minimumTickPixelDistance, locale }: RasterConfig, time year: 'numeric', month: 'short', day: 'numeric', - hour: 'numeric', + ...hourFormat, minute: 'numeric', second: 'numeric', timeZone, diff --git a/packages/charts/src/chart_types/xy_chart/domains/types.ts b/packages/charts/src/chart_types/xy_chart/domains/types.ts index 5bcad27d05..3726b2f0e2 100644 --- a/packages/charts/src/chart_types/xy_chart/domains/types.ts +++ b/packages/charts/src/chart_types/xy_chart/domains/types.ts @@ -17,7 +17,7 @@ export type XDomain = Pick<LogScaleOptions, 'logBase'> & { nice: boolean; /* if the scale needs to be a band scale: used when displaying bars */ isBandScale: boolean; - /* the minimum interval of the scale if not-ordinal band-scale */ + /* the minimum interval of the scale (for time, in milliseconds) if not-ordinal band-scale */ minInterval: number; /** the configured timezone in the specs or the fallback to the browser local timezone */ timeZone: string; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts index a335a3741c..2c203c8504 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts @@ -392,11 +392,12 @@ function getVisibleTickSets( (combinedEntry: { ticks: AxisTick[] }, l: TimeRaster<TimeBin>, detailedLayerIndex) => { if (l.labeled) layerIndex++; // we want three (or however many) _labeled_ axis layers; others are useful for minor ticks/gridlines, and for giving coarser structure eg. stronger gridline for every 6th hour of the day if (layerIndex >= timeAxisLayerCount) return combinedEntry; + const binWidthS = binWidth / 1000; const { entry } = fillLayerTimeslip( layerIndex, detailedLayerIndex, - [...l.binStarts(domainFromS, domainToS)] - .filter((b) => b.nextTimePointSec > domainFromS && b.timePointSec < domainToS) + [...l.binStarts(domainFromS - binWidthS, domainToS + binWidthS)] + .filter((b) => b.nextTimePointSec > domainFromS && b.timePointSec <= domainToS) .map((b) => 1000 * b.timePointSec), !l.labeled ? () => '' diff --git a/storybook/stories/bar/44_test_single_histogram.story.tsx b/storybook/stories/bar/44_test_single_histogram.story.tsx index 8d7142764e..26f976c26f 100644 --- a/storybook/stories/bar/44_test_single_histogram.story.tsx +++ b/storybook/stories/bar/44_test_single_histogram.story.tsx @@ -6,12 +6,14 @@ * Side Public License, v 1. */ +import { boolean } from '@storybook/addon-knobs'; import React from 'react'; import { Axis, Chart, HistogramBarSeries, + LineSeries, niceTimeFormatByDay, Position, ScaleType, @@ -24,14 +26,22 @@ import { useBaseTheme } from '../../use_base_theme'; // for testing purposes only export const Example = () => { + const useLine = boolean('use lines instead of bars', false); + const multiLayerAxis = boolean('use multilayer time axis', false); + const oddDomain = boolean('non-round time domain start', false); + const formatter = timeFormatter(niceTimeFormatByDay(1)); + const binWidth = 60000; + const xDomain = { - min: NaN, - max: NaN, - minInterval: 60000, + min: KIBANA_METRICS.metrics.kibana_os_load[0].data[0][0] - (oddDomain ? 217839 : 0), + max: KIBANA_METRICS.metrics.kibana_os_load[0].data[0][0] + (oddDomain ? 200000 : 0 ?? binWidth - 1), + minInterval: binWidth, }; + const Series = useLine ? LineSeries : HistogramBarSeries; + return ( <Chart> <Settings xDomain={xDomain} baseTheme={useBaseTheme()} /> @@ -39,13 +49,23 @@ export const Example = () => { id="bottom" title="timestamp per 1 minute" position={Position.Bottom} - showOverlappingTicks + showOverlappingTicks={!multiLayerAxis} tickFormat={formatter} + timeAxisLayerCount={multiLayerAxis ? 3 : 0} + style={ + multiLayerAxis + ? { + tickLine: { size: 0.0001, padding: 4 }, + tickLabel: { alignment: { horizontal: Position.Left, vertical: Position.Bottom } }, + } + : {} + } /> <Axis id="left" title={KIBANA_METRICS.metrics.kibana_os_load[0].metric.title} position={Position.Left} /> - <HistogramBarSeries - id="bars" - xScaleType={ScaleType.Linear} + <Series + id="series" + timeZone="local" + xScaleType={multiLayerAxis ? ScaleType.Time : ScaleType.Linear} yScaleType={ScaleType.Linear} xAccessor={0} yAccessors={[1]}