Skip to content

Commit

Permalink
feat: remove duplicate tick labels from axis (opensearch-project#577)
Browse files Browse the repository at this point in the history
  • Loading branch information
rshen91 authored Mar 18, 2020
1 parent 7240823 commit 52c6921
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 13 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions packages/osd-charts/src/chart_types/xy_chart/state/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,14 @@ export function computeSeriesGeometries(

// compute how many series are clustered
const { stackedBarsInCluster, totalBarsInCluster } = countBarsInCluster(stacked, nonStacked);

// compute scales
const xScale = computeXScale({ xDomain, totalBarsInCluster, range: [0, width], barsPadding, enableHistogramMode });
const xScale = computeXScale({
xDomain,
totalBarsInCluster,
range: [0, width],
barsPadding,
enableHistogramMode,
});
const yScales = computeYScales({ yDomains: yDomain, range: [height, 0] });

// compute colors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { AxisSpec, DomainRange, AxisStyle } from './specs';
import { Position } from '../../../utils/commons';
import { LIGHT_THEME } from '../../../utils/themes/light_theme';
import { AxisId, GroupId } from '../../../utils/ids';
import { ScaleType } from '../../../scales';
import { ScaleType, Scale } from '../../../scales';
import {
AxisTick,
AxisTicksDimensions,
Expand All @@ -48,13 +48,17 @@ import {
getAxisTickLabelPadding,
isVerticalGrid,
isHorizontalGrid,
enableDuplicatedTicks,
} 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';
import { mergeYCustomDomainsByGroupId } from '../state/selectors/merge_y_custom_domains';
import { ChartTypes } from '../..';
import { SpecTypes } from '../../../specs/settings';
import { DateTime } from 'luxon';
import { computeXScale } from './scales';
import moment from 'moment-timezone';

describe('Axis computational utils', () => {
const mockedRect = {
Expand Down Expand Up @@ -202,7 +206,7 @@ describe('Axis computational utils', () => {
expect(axisDimensions).toEqual(axis1Dims);

const computeScalelessSpec = () => {
computeAxisTicksDimensions(ungroupedAxisSpec, xDomain, [yDomain], 1, bboxCalculator, 0, axes);
computeAxisTicksDimensions(ungroupedAxisSpec, xDomain, [yDomain], 1, bboxCalculator, 0, axes, undefined, false);
};

const ungroupedAxisSpec = { ...verticalAxisSpec, groupId: 'foo' };
Expand Down Expand Up @@ -1436,4 +1440,88 @@ describe('Axis computational utils', () => {

expect(getAxisTickLabelPadding(axisConfigTickLabelPadding, axisSpecStyle)).toEqual(2);
});
test('should show unique tick labels if duplicateTicks is set to false', () => {
const now = DateTime.fromISO('2019-01-11T00:00:00.000')
.setZone('utc+1')
.toMillis();
const oneDay = moment.duration(1, 'day');
const formatter = niceTimeFormatter([now, oneDay.add(now).asMilliseconds() * 31]);
const axisSpec: AxisSpec = {
id: 'bottom',
position: 'bottom',
showDuplicatedTicks: false,
chartType: 'xy_axis',
specType: 'axis',
groupId: '__global__',
hide: false,
showOverlappingLabels: false,
showOverlappingTicks: false,
tickSize: 10,
tickPadding: 10,
tickLabelRotation: 0,
tickFormat: formatter,
};
const xDomainTime: XDomain = {
type: 'xDomain',
isBandScale: false,
domain: [1547190000000, 1547622000000],
minInterval: 86400000,
scaleType: ScaleType.Time,
};
const scale: Scale = computeXScale({ xDomain: xDomainTime, totalBarsInCluster: 0, range: [0, 603.5] });
const offset = 0;
const tickFormatOption = { timeZone: 'utc+1' };
expect(enableDuplicatedTicks(axisSpec, scale, offset, tickFormatOption)).toEqual([
{ value: 1547208000000, label: '2019-01-11', position: 25.145833333333332 },
{ value: 1547251200000, label: '2019-01-12', position: 85.49583333333334 },
{ value: 1547337600000, label: '2019-01-13', position: 206.19583333333333 },
{ value: 1547424000000, label: '2019-01-14', position: 326.8958333333333 },
{ value: 1547510400000, label: '2019-01-15', position: 447.59583333333336 },
{ value: 1547596800000, label: '2019-01-16', position: 568.2958333333333 },
]);
});
test('should show duplicate tick labels if duplicateTicks is set to true', () => {
const now = DateTime.fromISO('2019-01-11T00:00:00.000')
.setZone('utc+1')
.toMillis();
const oneDay = moment.duration(1, 'day');
const formatter = niceTimeFormatter([now, oneDay.add(now).asMilliseconds() * 31]);
const axisSpec: AxisSpec = {
id: 'bottom',
position: 'bottom',
showDuplicatedTicks: true,
chartType: 'xy_axis',
specType: 'axis',
groupId: '__global__',
hide: false,
showOverlappingLabels: false,
showOverlappingTicks: false,
tickSize: 10,
tickPadding: 10,
tickLabelRotation: 0,
tickFormat: formatter,
};
const xDomainTime: XDomain = {
type: 'xDomain',
isBandScale: false,
domain: [1547190000000, 1547622000000],
minInterval: 86400000,
scaleType: ScaleType.Time,
};
const scale: Scale = computeXScale({ xDomain: xDomainTime, totalBarsInCluster: 0, range: [0, 603.5] });
const offset = 0;
const tickFormatOption = { timeZone: 'utc+1' };
expect(enableDuplicatedTicks(axisSpec, scale, offset, tickFormatOption)).toEqual([
{ value: 1547208000000, label: '2019-01-11', position: 25.145833333333332 },
{ value: 1547251200000, label: '2019-01-12', position: 85.49583333333334 },
{ value: 1547294400000, label: '2019-01-12', position: 145.84583333333333 },
{ value: 1547337600000, label: '2019-01-13', position: 206.19583333333333 },
{ value: 1547380800000, label: '2019-01-13', position: 266.54583333333335 },
{ value: 1547424000000, label: '2019-01-14', position: 326.8958333333333 },
{ value: 1547467200000, label: '2019-01-14', position: 387.24583333333334 },
{ value: 1547510400000, label: '2019-01-15', position: 447.59583333333336 },
{ value: 1547553600000, label: '2019-01-15', position: 507.9458333333333 },
{ value: 1547596800000, label: '2019-01-16', position: 568.2958333333333 },
]);
});
});
25 changes: 19 additions & 6 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 @@ -29,7 +29,7 @@ import {
AxisStyle,
TickFormatterOptions,
} from './specs';
import { Position, Rotation } from '../../../utils/commons';
import { Position, Rotation, getUniqueValues } from '../../../utils/commons';
import { AxisConfig, Theme } from '../../../utils/themes/theme';
import { Dimensions, Margins } from '../../../utils/dimensions';
import { AxisId } from '../../../utils/ids';
Expand Down Expand Up @@ -87,7 +87,6 @@ export function computeAxisTicksDimensions(
if (axisSpec.hide) {
return null;
}

const scale = getScaleForAxisSpec(
axisSpec,
xDomain,
Expand Down Expand Up @@ -238,11 +237,9 @@ function computeTickDimensions(
const tickLabels = tickValues.map((d) => {
return tickFormat(d, tickFormatOptions);
});

const {
tickLabelStyle: { fontFamily, fontSize },
} = axisConfig;

const {
maxLabelBboxWidth,
maxLabelBboxHeight,
Expand All @@ -252,7 +249,6 @@ function computeTickDimensions(
getMaxBboxDimensions(bboxCalculator, fontSize, fontFamily, tickLabelRotation, tickLabelPadding),
{ maxLabelBboxWidth: 0, maxLabelBboxHeight: 0, maxLabelTextWidth: 0, maxLabelTextHeight: 0 },
);

return {
tickValues,
tickLabels,
Expand Down Expand Up @@ -444,14 +440,31 @@ export function getAvailableTicks(

return [firstTick, lastTick];
}
return ticks.map((tick) => {
return enableDuplicatedTicks(axisSpec, scale, offset, tickFormatOptions);
}

/** @internal */
export function enableDuplicatedTicks(
axisSpec: AxisSpec,
scale: Scale,
offset: number,
tickFormatOptions?: TickFormatterOptions,
) {
const ticks = scale.ticks();
const allTicks: AxisTick[] = ticks.map((tick) => {
return {
value: tick,
label: axisSpec.tickFormat(tick, tickFormatOptions),
position: scale.scale(tick) + offset,
};
});

if (axisSpec.showDuplicatedTicks === true) {
return allTicks;
}
return getUniqueValues(allTicks, 'label');
}

export function getVisibleTicks(allTicks: AxisTick[], axisSpec: AxisSpec, axisDim: AxisTicksDimensions): AxisTick[] {
// We sort the ticks by position so that we can incrementally compute previousOccupiedSpace
allTicks.sort((a: AxisTick, b: AxisTick) => a.position - b.position);
Expand Down
3 changes: 3 additions & 0 deletions packages/osd-charts/src/chart_types/xy_chart/utils/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,11 +525,14 @@ export interface AxisSpec extends Spec {
style?: AxisStyle;
/** Show only integar values **/
integersOnly?: boolean;
/** Remove duplicate ticks, default is false*/
showDuplicatedTicks?: boolean;
}

export type TickFormatterOptions = {
timeZone?: string;
};

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

export interface AxisStyle {
Expand Down
22 changes: 22 additions & 0 deletions packages/osd-charts/src/utils/commons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,28 @@ export function isNumberArray(value: unknown): value is number[] {
return Array.isArray(value) && value.every((element) => typeof element === 'number');
}

/** @internal */
export function getUniqueValues<T>(fullArray: T[], uniqueProperty: keyof T): T[] {
return fullArray.reduce<{
filtered: T[];
uniqueValues: Set<T[keyof T]>;
}>(
(acc, currentValue) => {
const uniqueValue = currentValue[uniqueProperty];
if (acc.uniqueValues.has(uniqueValue)) {
return acc;
}
acc.uniqueValues.add(uniqueValue);
acc.filtered.push(currentValue);
return acc;
},
{
filtered: [],
uniqueValues: new Set(),
},
).filtered;
}

export type ValueFormatter = (value: number) => string;
export type ValueAccessor = (d: Datum) => number;
export type LabelAccessor = (value: PrimitiveValue) => string;
Expand Down
65 changes: 65 additions & 0 deletions packages/osd-charts/stories/axes/12_duplicate_ticks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License. */

import React from 'react';
import { Axis, Chart, LineSeries, Position, ScaleType, niceTimeFormatter } from '../../src';
import { KIBANA_METRICS } from '../../src/utils/data_samples/test_dataset_kibana';
import { boolean } from '@storybook/addon-knobs';
import { DateTime } from 'luxon';
import moment from 'moment-timezone';

export const example = () => {
const now = DateTime.fromISO('2019-01-11T00:00:00.000')
.setZone('utc+1')
.toMillis();
const oneDay = moment.duration(1, 'd');
const twoDays = moment.duration(2, 'd');
const oneMonth = moment.duration(31, 'd');
const threeDays = moment.duration(3, 'd');
const fourDays = moment.duration(4, 'd');
const fiveDays = moment.duration(5, 'd');
const formatter = niceTimeFormatter([now, oneMonth.add(now).asMilliseconds()]);
const duplicateTicksInAxis = boolean('Show duplicate ticks in x axis', false);
return (
<Chart className="story-chart">
<Axis id="bottom" position={Position.Bottom} tickFormat={formatter} showDuplicatedTicks={duplicateTicksInAxis} />
<Axis
id="left"
title={KIBANA_METRICS.metrics.kibana_os_load[0].metric.title}
position={Position.Left}
tickFormat={(d) => `${Number(d).toFixed(1)}`}
/>
<LineSeries
id="lines"
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
data={[
{ x: now, y: 2 },
{ x: oneDay.add(now).asMilliseconds(), y: 3 },
{ x: twoDays.add(now).asMilliseconds(), y: 3 },
{ x: threeDays.add(now).asMilliseconds(), y: 4 },
{ x: fourDays.add(now).asMilliseconds(), y: 8 },
{ x: fiveDays.add(now).asMilliseconds(), y: 6 },
]}
timeZone="local"
/>
</Chart>
);
};
1 change: 1 addition & 0 deletions packages/osd-charts/stories/axes/axes.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ export { example as customDomain } from './8_custom_domain';
export { example as customMixed } from './9_custom_mixed_domain';
export { example as oneDomainBound } from './10_one_domain_bound';
export { example as fitDomain } from './11_fit_domain_extent';
export { example as duplicateTicks } from './12_duplicate_ticks';
10 changes: 7 additions & 3 deletions packages/osd-charts/stories/interactions/11_brush_time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ import { Axis, BarSeries, Chart, LineSeries, niceTimeFormatter, Position, ScaleT
import { boolean } from '@storybook/addon-knobs';
import { DateTime } from 'luxon';
import { getChartRotationKnob } from '../utils/knobs';
import moment from 'moment-timezone';

export const example = () => {
const now = DateTime.fromISO('2019-01-11T00:00:00.000')
.setZone('utc+1')
.toMillis();
const oneDay = 1000 * 60 * 60 * 24;
const formatter = niceTimeFormatter([now, now + oneDay * 5]);
const oneDays = moment.duration(1, 'd');
const twoDays = moment.duration(2, 'd');
const fiveDays = moment.duration(5, 'd');
const formatter = niceTimeFormatter([now, fiveDays.add(now).asMilliseconds()]);
return (
<Chart className="story-chart">
<Settings
Expand All @@ -52,8 +56,8 @@ export const example = () => {
timeZone="Europe/Rome"
data={[
{ x: now, y: 2 },
{ x: now + oneDay, y: 7 },
{ x: now + oneDay * 2, y: 3 },
{ x: oneDays.add(now).asMilliseconds(), y: 7 },
{ x: twoDays.add(now).asMilliseconds(), y: 3 },
{ x: now + oneDay * 5, y: 6 },
]}
/>
Expand Down

0 comments on commit 52c6921

Please sign in to comment.