Skip to content

Commit

Permalink
fix(tickformatter): add timeZone to tickFormatter (opensearch-project…
Browse files Browse the repository at this point in the history
…#430)

This commit add the timeZone parameter to the tickFormatter to handle correctly tick formatting on different time-zoned series. When the tickFormatter prop is an external function, the user manually configure the formatter using the specified timezone. The exposed niceTimeFormatter doesn't have that option.

fix opensearch-project#427
  • Loading branch information
markov00 authored Oct 22, 2019
1 parent 2a13e24 commit 347eacb
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 41 deletions.
40 changes: 13 additions & 27 deletions packages/osd-charts/.playground/playgroud.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Fragment } from 'react';
import { Axis, Chart, getAxisId, getSpecId, Position, ScaleType, BarSeries, Settings } from '../src';
import { Axis, Chart, getAxisId, getSpecId, Position, ScaleType, BarSeries, Settings, niceTimeFormatter } from '../src';
import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana';

export class Playground extends React.Component<{}, { dataLimit: boolean }> {
state = {
Expand All @@ -13,21 +14,7 @@ export class Playground extends React.Component<{}, { dataLimit: boolean }> {
});
};
render() {
const data = [
{
g: null,
i: 'aa',
x: 1571212800000,
y: 16,
y1: 2,
},
// {
// x: 1571290200000,
// y: 1,
// y1: 5,
// // g: 'authentication_success',
// },
];
const { data } = KIBANA_METRICS.metrics.kibana_os_load[0];
return (
<Fragment>
<div>
Expand All @@ -36,23 +23,22 @@ export class Playground extends React.Component<{}, { dataLimit: boolean }> {
<div className="chart">
<Chart>
<Settings showLegend />
<Axis id={getAxisId('top')} position={Position.Bottom} title={'Top axis'} />
<Axis
id={getAxisId('left2')}
title={'Left axis'}
position={Position.Left}
tickFormat={(d: any) => Number(d).toFixed(2)}
id={getAxisId('top')}
position={Position.Bottom}
title={'Top axis'}
tickFormat={niceTimeFormatter([data[0][0], data[data.length - 1][0]])}
/>
<Axis id={getAxisId('left2')} title={'Left axis'} position={Position.Left} />

<BarSeries
id={getSpecId('bars1')}
xScaleType={ScaleType.Linear}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
splitSeriesAccessors={['g']}
stackAccessors={['x']}
data={data.slice(0, this.state.dataLimit ? 1 : 2)}
xAccessor={0}
yAccessors={[1]}
data={data}
timeZone={'utc+8'}
/>
</Chart>
</div>
Expand Down
13 changes: 11 additions & 2 deletions packages/osd-charts/src/chart_types/xy_chart/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { TooltipValue, isFollowTooltipType, TooltipType, TooltipValueFormatter } from '../utils/interactions';
import { IndexedGeometry, isPointOnGeometry, AccessorType } from '../rendering/rendering';
import { getColorValuesAsString } from '../utils/series';
import { AxisSpec, BasicSeriesSpec, Rotation, isAreaSeriesSpec, isBandedSpec, isBarSeriesSpec } from '../utils/specs';
import {
AxisSpec,
BasicSeriesSpec,
Rotation,
isAreaSeriesSpec,
isBandedSpec,
isBarSeriesSpec,
TickFormatterOptions,
} from '../utils/specs';
import { SpecId, AxisId, GroupId } from '../../../utils/ids';
import { getAxesSpecForSpecId } from '../store/utils';
import { Scale } from '../../../utils/scales/scales';
Expand Down Expand Up @@ -65,10 +73,11 @@ export function formatTooltip(
}

const value = isXValue ? x : y;
const tickFormatOptions: TickFormatterOptions | undefined = spec.timeZone ? { timeZone: spec.timeZone } : undefined;
return {
seriesKey: seriesKeyAsString,
name: displayName,
value: axisSpec ? axisSpec.tickFormat(value) : emptyFormatter(value),
value: axisSpec ? axisSpec.tickFormat(value, tickFormatOptions) : emptyFormatter(value),
color,
isHighlighted: isXValue ? false : isHighlighted,
isXValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from './axis_utils';
import { CanvasTextBBoxCalculator } from '../../../utils/bbox/canvas_text_bbox_calculator';
import { SvgTextBBoxCalculator } from '../../../utils/bbox/svg_text_bbox_calculator';
import { niceTimeFormatter } from '../../../utils/data/formatters';

describe('Axis computational utils', () => {
const mockedRect = {
Expand Down Expand Up @@ -119,6 +120,20 @@ describe('Axis computational utils', () => {
showGridLines: true,
integersOnly: false,
};
const xAxisWithTime: AxisSpec = {
id: getAxisId('axis_1'),
groupId: getGroupId('group_1'),
title: 'v axis',
hide: false,
showOverlappingTicks: false,
showOverlappingLabels: false,
position: Position.Bottom,
tickSize: 10,
tickPadding: 10,
tickFormat: niceTimeFormatter([1551438000000, 1551441300000]),
showGridLines: true,
integersOnly: false,
};

// const horizontalAxisSpecWTitle: AxisSpec = {
// id: getAxisId('axis_2'),
Expand Down Expand Up @@ -175,6 +190,56 @@ describe('Axis computational utils', () => {
expect(axisDimensions).toBe(null);
});

test('should compute axis dimensions with timeZone', () => {
const bboxCalculator = new SvgTextBBoxCalculator();
const xDomain: XDomain = {
type: 'xDomain',
scaleType: ScaleType.Time,
domain: [1551438000000, 1551441300000],
isBandScale: false,
minInterval: 0,
timeZone: 'utc',
};
let axisDimensions = computeAxisTicksDimensions(xAxisWithTime, xDomain, [yDomain], 1, bboxCalculator, 0, axes);
expect(axisDimensions).not.toBeNull();
expect(axisDimensions!.tickLabels[0]).toBe('11:00:00');
expect(axisDimensions!.tickLabels[11]).toBe('11:55:00');

axisDimensions = computeAxisTicksDimensions(
xAxisWithTime,
{
...xDomain,
timeZone: 'utc+3',
},
[yDomain],
1,
bboxCalculator,
0,
axes,
);
expect(axisDimensions).not.toBeNull();
expect(axisDimensions!.tickLabels[0]).toBe('14:00:00');
expect(axisDimensions!.tickLabels[11]).toBe('14:55:00');

axisDimensions = computeAxisTicksDimensions(
xAxisWithTime,
{
...xDomain,
timeZone: 'utc-3',
},
[yDomain],
1,
bboxCalculator,
0,
axes,
);
expect(axisDimensions).not.toBeNull();
expect(axisDimensions!.tickLabels[0]).toBe('08:00:00');
expect(axisDimensions!.tickLabels[11]).toBe('08:55:00');

bboxCalculator.destroy();
});

test('should compute dimensions for the bounding box containing a rotated label', () => {
expect(computeRotatedLabelDimensions({ width: 1, height: 2 }, 0)).toEqual({
width: 1,
Expand Down
21 changes: 16 additions & 5 deletions packages/osd-charts/src/chart_types/xy_chart/utils/axis_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
TickFormatter,
UpperBoundedDomain,
AxisStyle,
TickFormatterOptions,
} from './specs';
import { AxisConfig, Theme } from '../../../utils/themes/theme';
import { Dimensions, Margins } from '../../../utils/dimensions';
Expand Down Expand Up @@ -91,6 +92,9 @@ export function computeAxisTicksDimensions(
axisConfig,
tickLabelPadding,
axisSpec.tickLabelRotation,
{
timeZone: xDomain.timeZone,
},
);

return {
Expand Down Expand Up @@ -211,9 +215,12 @@ function computeTickDimensions(
axisConfig: AxisConfig,
tickLabelPadding: number,
tickLabelRotation = 0,
tickFormatOptions?: TickFormatterOptions,
) {
const tickValues = scale.ticks();
const tickLabels = tickValues.map(tickFormat);
const tickLabels = tickValues.map((d) => {
return tickFormat(d, tickFormatOptions);
});

const {
tickLabelStyle: { fontFamily, fontSize },
Expand Down Expand Up @@ -404,6 +411,7 @@ export function getAvailableTicks(
scale: Scale,
totalBarsInCluster: number,
enableHistogramMode: boolean,
tickFormatOptions?: TickFormatterOptions,
): AxisTick[] {
const ticks = scale.ticks();
const isSingleValueScale = scale.domain[0] - scale.domain[1] === 0;
Expand Down Expand Up @@ -433,14 +441,14 @@ export function getAvailableTicks(
const firstTickValue = ticks[0];
const firstTick = {
value: firstTickValue,
label: axisSpec.tickFormat(firstTickValue),
label: axisSpec.tickFormat(firstTickValue, tickFormatOptions),
position: scale.scale(firstTickValue) + offset,
};

const lastTickValue = firstTickValue + scale.minInterval;
const lastTick = {
value: lastTickValue,
label: axisSpec.tickFormat(lastTickValue),
label: axisSpec.tickFormat(lastTickValue, tickFormatOptions),
position: scale.bandwidth + halfPadding * 2,
};

Expand All @@ -449,7 +457,7 @@ export function getAvailableTicks(
return ticks.map((tick) => {
return {
value: tick,
label: axisSpec.tickFormat(tick),
label: axisSpec.tickFormat(tick, tickFormatOptions),
position: scale.scale(tick) + offset,
};
});
Expand Down Expand Up @@ -605,8 +613,11 @@ export function getAxisTicksPositions(
if (!scale) {
throw new Error(`Cannot compute scale for axis spec ${axisSpec.id}`);
}
const tickFormatOptions = {
timeZone: xDomain.timeZone,
};

const allTicks = getAvailableTicks(axisSpec, scale, totalGroupsCount, enableHistogramMode);
const allTicks = getAvailableTicks(axisSpec, scale, totalGroupsCount, enableHistogramMode, tickFormatOptions);
const visibleTicks = getVisibleTicks(allTicks, axisSpec, axisDim);

if (axisSpec.showGridLines) {
Expand Down
5 changes: 4 additions & 1 deletion packages/osd-charts/src/chart_types/xy_chart/utils/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,10 @@ export interface AxisSpec {
integersOnly?: boolean;
}

export type TickFormatter = (value: any) => string;
export type TickFormatterOptions = {
timeZone?: string;
};
export type TickFormatter = (value: any, options?: TickFormatterOptions) => string;

export interface AxisStyle {
/** Specifies the amount of padding on the tick label bounding box */
Expand Down
12 changes: 6 additions & 6 deletions packages/osd-charts/src/utils/data/formatters.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { DateTime, Interval } from 'luxon';
import { TickFormatter, TickFormatterOptions } from '../../chart_types/xy_chart/utils/specs';

type TimeFormatter = (value: number) => string;

export function timeFormatter(format: string): TimeFormatter {
return (value: number): string => {
return DateTime.fromMillis(value).toFormat(format);
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);
};
}

export function niceTimeFormatter(domain: [number, number]): TimeFormatter {
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);
Expand Down

0 comments on commit 347eacb

Please sign in to comment.