Skip to content

Commit

Permalink
[Series Charts] Provide custom crosshair overlay time formatting (#1429)
Browse files Browse the repository at this point in the history
* Specify a date format prop for EuiSeriesChart's crosshair.

* Add y crosshair formatting capabilities.

* Sync with latest changelog.

* Update based on PR feedback.

* Restore default crosshair formatting, write tests.

* Fixing unit tests that failed CI.

* Commit unsaved changes.
  • Loading branch information
justinkambic authored Jan 15, 2019
1 parent 9776a52 commit 5f20258
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 31 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## [`master`](https://github.com/elastic/eui/tree/master)

No public interface changes since `6.3.1`.
- Added custom date string formatting for series charts crosshair overlay ([#1429](https://github.com/elastic/eui/pull/1429))

## [`6.3.1`](https://github.com/elastic/eui/tree/v6.3.1)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import {
EuiSeriesChart,
EuiHistogramSeries,
EuiSeriesChartUtils,
} from '../../../../src/experimental';

const { SCALE } = EuiSeriesChartUtils;
let timeseriesX = Date.now();
const HOUR = 1000 * 60 * 60;

const histogramData = new Array(1000).fill(0).map(() => {
const x0 = timeseriesX;
timeseriesX += HOUR;
const x = timeseriesX;
const y = Math.floor(Math.random() * 100);
return { x0, x, y };
});

export const FormatCrosshairTimesExample = () => (
<div>
<EuiSeriesChart xCrosshairFormat="YYYY-MM-DD hh:mmZ" height={200} xType={SCALE.TIME}>
<EuiHistogramSeries yDomain={[0, 100]} name="Chart Name" data={histogramData} />
</EuiSeriesChart>
</div>
);
22 changes: 22 additions & 0 deletions src-docs/src/views/series_chart_histogram/histogram_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import StackedVerticalRectSeriesExample from './stacked_vertical_rect_series';
import HorizontalRectSeriesExample from './horizontal_rect_series';
import StackedHorizontalRectSeriesExample from './stacked_horizontal_rect_series';
import TimeHistogramSeriesExample from './time_histogram_series';
import { FormatCrosshairTimesExample } from './format_crosshair_times';

import {
EuiBadge,
Expand Down Expand Up @@ -177,6 +178,27 @@ export const XYChartHistogramExample = {
],
demo: <StackedHorizontalRectSeriesExample />,
},
{
title: 'Custom crosshair time format',
text: (
<div>
<p>
Specify a custom formatting string to change the locality or format of the time string on the x crosshair.
</p>
</div>
),
source: [
{
type: GuideSectionTypes.JS,
code: require('!!raw-loader!./format_crosshair_times'),
},
{
type: GuideSectionTypes.HTML,
code: 'This component can only be used from React',
},
],
demo: <FormatCrosshairTimesExample />
},
{
title: 'Time Series Histogram version',
text: (
Expand Down
6 changes: 4 additions & 2 deletions src/components/series_chart/crosshairs/crosshair_x.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { AbstractSeries, Crosshair } from 'react-vis';
import { SCALE } from '../utils/chart_utils';
import moment from 'moment';
/**
* The Crosshair used by the XYChart as main tooltip mechanism along X axis (vertical).
*/
Expand Down Expand Up @@ -55,9 +56,9 @@ export class EuiCrosshairX extends AbstractSeries {
}

_formatXValue = (x) => {
const { xType } = this.props;
const { xType, xCrosshairFormat } = this.props;
if (xType === SCALE.TIME || xType === SCALE.TIME_UTC) {
return new Date(x).toISOString(); // TODO add a props for time formatting
return xCrosshairFormat ? moment(x).format(xCrosshairFormat) : new Date(x).toISOString();
} else {
return x;
}
Expand Down Expand Up @@ -201,5 +202,6 @@ EuiCrosshairX.propTypes = {
* The ordered array of series names
*/
seriesNames: PropTypes.arrayOf(PropTypes.string).isRequired,
xCrosshairFormat: PropTypes.string,
};
EuiCrosshairX.defaultProps = {};
53 changes: 52 additions & 1 deletion src/components/series_chart/crosshairs/crosshair_x.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import React from 'react';
import { mount } from 'enzyme';

import moment from 'moment';
import { EuiSeriesChart } from '../series_chart';
import { EuiVerticalBarSeries } from '../series/vertical_bar_series';
import { EuiCrosshairX } from './';
import { requiredProps } from '../../../test/required_props';
import { Crosshair } from 'react-vis';
import { EuiHistogramSeries } from '../series';
import { EuiSeriesChartUtils } from '../utils';

const { SCALE } = EuiSeriesChartUtils;

describe('EuiCrosshairX', () => {
test('render the X crosshair', () => {
const data = [ { x: 0, y: 1.5 }, { x: 1, y: 2 }];
Expand Down Expand Up @@ -39,4 +44,50 @@ describe('EuiCrosshairX', () => {
expect(crosshair.find('.rv-crosshair__inner__content .rv-crosshair__title__value').text()).toBe('1');
expect(crosshair.find('.rv-crosshair__inner__content .rv-crosshair__item__value').text()).toBe('2');
});

test('x crosshair formats ISO string by default', () => {
const openRange = 1074199386000;
const closeRange = 1074210186000;
const data = [ { x0: 1074188586000, x: openRange, y: 1.5 }, { x0: 1074199386000, x: closeRange, y: 2 }];
const startRangeString = new Date(openRange).toISOString();
const endRangeString = new Date(closeRange).toISOString();
const component = mount(
<EuiSeriesChart
width={600}
height={200}
{...requiredProps}
xType={SCALE.TIME}
>
<EuiHistogramSeries name="Test Series" data={data} />
</EuiSeriesChart>
);
component.find('rect').at(0).simulate('mousemove', { nativeEvent: { clientX: 351, clientY: 100 } });
const crosshair = component.find('.rv-crosshair');
expect(crosshair).toHaveLength(1);
expect(crosshair.text()).toContain(`${startRangeString} to ${endRangeString}`);
});

test('x crosshair adheres to custom formatting', () => {
const openRange = 1074199386000;
const closeRange = 1074210186000;
const data = [ { x0: 1074188586000, x: openRange, y: 1.5 }, { x0: 1074199386000, x: closeRange, y: 2 }];
const momentFormat = 'YYYY-MM-DD hh:mmZ';
const startRangeString = moment(openRange).format(momentFormat);
const endRangeString = moment(closeRange).format(momentFormat);
const component = mount(
<EuiSeriesChart
width={600}
height={200}
{...requiredProps}
xType={SCALE.TIME}
xCrosshairFormat={momentFormat}
>
<EuiHistogramSeries name="Test Series" data={data} />
</EuiSeriesChart>
);
component.find('rect').at(0).simulate('mousemove', { nativeEvent: { clientX: 351, clientY: 100 } });
const crosshair = component.find('.rv-crosshair');
expect(crosshair).toHaveLength(1);
expect(crosshair.text()).toContain(`${startRangeString} to ${endRangeString}`);
});
});
6 changes: 4 additions & 2 deletions src/components/series_chart/crosshairs/crosshair_y.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { AbstractSeries, ScaleUtils } from 'react-vis';
import { SCALE } from '../utils/chart_utils';
import moment from 'moment';

/**
* Format title by detault.
Expand Down Expand Up @@ -237,9 +238,9 @@ export class EuiCrosshairY extends AbstractSeries {
});
}
_formatYValue = (y) => {
const { yType } = this.props;
const { yType, yCrosshairFormat } = this.props;
if (yType === SCALE.TIME || yType === SCALE.TIME_UTC) {
return new Date(y).toISOString(); // TODO add a props for time formatting
return yCrosshairFormat ? moment(y).format(yCrosshairFormat) : new Date(y).toISOString();
} else {
return y;
}
Expand Down Expand Up @@ -382,5 +383,6 @@ EuiCrosshairY.propTypes = {
* The ordered array of series names
*/
seriesNames: PropTypes.arrayOf(PropTypes.string).isRequired,
yCrosshairFormat: PropTypes.string,
};
EuiCrosshairY.defaultProps = {};
56 changes: 55 additions & 1 deletion src/components/series_chart/crosshairs/crosshair_y.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import React from 'react';
import { mount } from 'enzyme';

import moment from 'moment';
import { EuiSeriesChart } from '../series_chart';
import { EuiHorizontalBarSeries } from '../series/horizontal_bar_series';
import { EuiCrosshairY } from './';
import { CrosshairY } from './crosshair_y';
import { requiredProps } from '../../../test/required_props';
import { EuiHistogramSeries } from '../series';
import { EuiSeriesChartUtils } from '../utils';

const { SCALE } = EuiSeriesChartUtils;

describe('EuiCrosshairY', () => {
test('render the Y crosshair', () => {
Expand Down Expand Up @@ -40,4 +44,54 @@ describe('EuiCrosshairY', () => {
expect(crosshair.find('.rv-crosshair__inner__content .rv-crosshair__title__value').text()).toBe('0');
expect(crosshair.find('.rv-crosshair__inner__content .rv-crosshair__item__value').text()).toBe('1.5');
});

test('y crosshair formats ISO string by default', () => {
const openRange = 1074188586000;
const closeRange = 1074199386000;
const data = [ { y0: openRange, y: 1074199386000, x: 1.5 }, { y0: closeRange, y: 1074210186000, x: 2 }];
const startRangeString = new Date(openRange).toISOString();
const endRangeString = new Date(closeRange).toISOString();
const component = mount(
<EuiSeriesChart
width={600}
height={200}
{...requiredProps}
stackBy="x"
yType={SCALE.TIME}
orientation={EuiSeriesChartUtils.ORIENTATION.HORIZONTAL}
>
<EuiHistogramSeries name="Test Series" data={data} />
</EuiSeriesChart>
);
component.find('rect').at(0).simulate('mousemove', { nativeEvent: { clientX: 351, clientY: 100 } });
const crosshair = component.find('.rv-crosshair');
expect(crosshair).toHaveLength(1);
expect(crosshair.text()).toContain(`${startRangeString} to ${endRangeString}`);
});

test('y crosshair formats ISO string by default', () => {
const openRange = 1074188586000;
const closeRange = 1074199386000;
const data = [ { y0: openRange, y: 1074199386000, x: 1.5 }, { y0: closeRange, y: 1074210186000, x: 2 }];
const momentFormat = 'YYYY-MM-DD hh:mmZ';
const startRangeString = moment(openRange).format(momentFormat);
const endRangeString = moment(closeRange).format(momentFormat);
const component = mount(
<EuiSeriesChart
width={600}
height={200}
{...requiredProps}
stackBy="x"
yType={SCALE.TIME}
yCrosshairFormat={momentFormat}
orientation={EuiSeriesChartUtils.ORIENTATION.HORIZONTAL}
>
<EuiHistogramSeries name="Test Series" data={data} />
</EuiSeriesChart>
);
component.find('rect').at(0).simulate('mousemove', { nativeEvent: { clientX: 351, clientY: 100 } });
const crosshair = component.find('.rv-crosshair');
expect(crosshair).toHaveLength(1);
expect(crosshair.text()).toContain(`${startRangeString} to ${endRangeString}`);
});
});
53 changes: 29 additions & 24 deletions src/components/series_chart/series_chart.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { XYPlot, AbstractSeries } from 'react-vis';
import { AbstractSeries, XYPlot } from 'react-vis';
import { makeFlexible } from './utils/flexible';
import PropTypes from 'prop-types';
import { EuiEmptyPrompt } from '../empty_prompt';
Expand All @@ -17,7 +17,7 @@ const DEFAULT_MARGINS = {
left: 40,
right: 10,
top: 10,
bottom: 40
bottom: 40,
};

/**
Expand All @@ -32,18 +32,17 @@ class XYChart extends PureComponent {
colorIterator = 0;
_xyPlotRef = React.createRef();


/**
* Checks if the plot is empty, looking at existing series and data props.
*/
_isEmptyPlot(children) {
return React.Children
.toArray(children)
.filter(this._isAbstractSeries)
.filter(child => {
return child.props.data && child.props.data.length > 0;
})
.length === 0;
return (
React.Children.toArray(children)
.filter(this._isAbstractSeries)
.filter(child => {
return child.props.data && child.props.data.length > 0;
}).length === 0
);
}

/**
Expand All @@ -55,14 +54,13 @@ class XYChart extends PureComponent {
return prototype instanceof AbstractSeries;
}


/**
* Render children adding a valid EUI visualization color if the color prop is not specified.
*/
_renderChildren(children) {
let colorIterator = 0;

return React.Children.map(children, (child, i) => {
return React.Children.map(children, (child, i) => {
// Avoid applying color props to non series children
if (!this._isAbstractSeries(child)) {
return child;
Expand All @@ -80,11 +78,11 @@ class XYChart extends PureComponent {
return React.cloneElement(child, props);
});
}
_getSeriesNames = (children) => {
return React.Children.toArray(children)
_getSeriesNames = children => {
return React.Children.toArray(children)
.filter(this._isAbstractSeries)
.map(({ props: { name } }) => (name));
}
.map(({ props: { name } }) => name);
};

render() {
const {
Expand All @@ -93,6 +91,8 @@ class XYChart extends PureComponent {
height,
margins,
xType,
xCrosshairFormat,
yCrosshairFormat,
yType,
stackBy,
statusText,
Expand All @@ -119,9 +119,7 @@ class XYChart extends PureComponent {
className="euiSeriesChartContainer__emptyPrompt"
iconType="visualizeApp"
title={<span>Chart not available</span>}
body={
<p>{ statusText }</p>
}
body={<p>{statusText}</p>}
/>
);
}
Expand All @@ -130,10 +128,7 @@ class XYChart extends PureComponent {
const seriesNames = this._getSeriesNames(children);
const classes = classNames(className, 'euiSeriesChartContainer');
return (
<div
className={classes}
{...rest}
>
<div className={classes} {...rest}>
<XYPlot
ref={this._xyPlotRef}
dontCheckIfEmpty
Expand All @@ -153,7 +148,13 @@ class XYChart extends PureComponent {
{this._renderChildren(children)}
{showDefaultAxis && <EuiDefaultAxis orientation={orientation} />}
{showCrosshair && (
<Crosshair seriesNames={seriesNames} crosshairValue={crosshairValue} onCrosshairUpdate={onCrosshairUpdate} />
<Crosshair
seriesNames={seriesNames}
crosshairValue={crosshairValue}
onCrosshairUpdate={onCrosshairUpdate}
xCrosshairFormat={xCrosshairFormat}
yCrosshairFormat={yCrosshairFormat}
/>
)}

{enableSelectionBrush && (
Expand Down Expand Up @@ -182,6 +183,10 @@ XYChart.propTypes = {
stackBy: PropTypes.string,
/** The main x axis scale type. See https://github.com/uber/react-vis/blob/master/docs/scales-and-data.md */
xType: PropTypes.oneOf([LINEAR, ORDINAL, CATEGORY, TIME, TIME_UTC, LOG, LITERAL]),
/** The formatting string for the X-axis. */
xCrosshairFormat: PropTypes.string,
/** The formatting string for the Y-axis. */
yCrosshairFormat: PropTypes.string,
/** The main y axis scale type. See https://github.com/uber/react-vis/blob/master/docs/scales-and-data.md*/
yType: PropTypes.oneOf([LINEAR, ORDINAL, CATEGORY, TIME, TIME_UTC, LOG, LITERAL]),
/** Manually specify the domain of x axis. */
Expand Down

0 comments on commit 5f20258

Please sign in to comment.