diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bc209203a9..9a97b3c67ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/src-docs/src/views/series_chart_histogram/format_crosshair_times.js b/src-docs/src/views/series_chart_histogram/format_crosshair_times.js
new file mode 100644
index 00000000000..454cb1ef6f3
--- /dev/null
+++ b/src-docs/src/views/series_chart_histogram/format_crosshair_times.js
@@ -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 = () => (
+
+
+
+
+
+);
diff --git a/src-docs/src/views/series_chart_histogram/histogram_example.js b/src-docs/src/views/series_chart_histogram/histogram_example.js
index eec51f283bb..212f7fd2b54 100644
--- a/src-docs/src/views/series_chart_histogram/histogram_example.js
+++ b/src-docs/src/views/series_chart_histogram/histogram_example.js
@@ -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,
@@ -177,6 +178,27 @@ export const XYChartHistogramExample = {
],
demo: ,
},
+ {
+ title: 'Custom crosshair time format',
+ text: (
+
+
+ Specify a custom formatting string to change the locality or format of the time string on the x crosshair.
+
+
+ ),
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./format_crosshair_times'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo:
+ },
{
title: 'Time Series Histogram version',
text: (
diff --git a/src/components/series_chart/crosshairs/crosshair_x.js b/src/components/series_chart/crosshairs/crosshair_x.js
index 6423a136d88..35eec890f9a 100644
--- a/src/components/series_chart/crosshairs/crosshair_x.js
+++ b/src/components/series_chart/crosshairs/crosshair_x.js
@@ -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).
*/
@@ -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;
}
@@ -201,5 +202,6 @@ EuiCrosshairX.propTypes = {
* The ordered array of series names
*/
seriesNames: PropTypes.arrayOf(PropTypes.string).isRequired,
+ xCrosshairFormat: PropTypes.string,
};
EuiCrosshairX.defaultProps = {};
diff --git a/src/components/series_chart/crosshairs/crosshair_x.test.js b/src/components/series_chart/crosshairs/crosshair_x.test.js
index 7929853c489..998b05ee065 100644
--- a/src/components/series_chart/crosshairs/crosshair_x.test.js
+++ b/src/components/series_chart/crosshairs/crosshair_x.test.js
@@ -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 }];
@@ -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(
+
+
+
+ );
+ 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(
+
+
+
+ );
+ 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}`);
+ });
});
diff --git a/src/components/series_chart/crosshairs/crosshair_y.js b/src/components/series_chart/crosshairs/crosshair_y.js
index 108cd320751..aa4050cd94a 100644
--- a/src/components/series_chart/crosshairs/crosshair_y.js
+++ b/src/components/series_chart/crosshairs/crosshair_y.js
@@ -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.
@@ -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;
}
@@ -382,5 +383,6 @@ EuiCrosshairY.propTypes = {
* The ordered array of series names
*/
seriesNames: PropTypes.arrayOf(PropTypes.string).isRequired,
+ yCrosshairFormat: PropTypes.string,
};
EuiCrosshairY.defaultProps = {};
diff --git a/src/components/series_chart/crosshairs/crosshair_y.test.js b/src/components/series_chart/crosshairs/crosshair_y.test.js
index 1ea8ecdb57e..38384eca27b 100644
--- a/src/components/series_chart/crosshairs/crosshair_y.test.js
+++ b/src/components/series_chart/crosshairs/crosshair_y.test.js
@@ -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', () => {
@@ -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(
+
+
+
+ );
+ 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(
+
+
+
+ );
+ 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}`);
+ });
});
diff --git a/src/components/series_chart/series_chart.js b/src/components/series_chart/series_chart.js
index fa44089c317..c34b6b8bdc8 100644
--- a/src/components/series_chart/series_chart.js
+++ b/src/components/series_chart/series_chart.js
@@ -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';
@@ -17,7 +17,7 @@ const DEFAULT_MARGINS = {
left: 40,
right: 10,
top: 10,
- bottom: 40
+ bottom: 40,
};
/**
@@ -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
+ );
}
/**
@@ -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;
@@ -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 {
@@ -93,6 +91,8 @@ class XYChart extends PureComponent {
height,
margins,
xType,
+ xCrosshairFormat,
+ yCrosshairFormat,
yType,
stackBy,
statusText,
@@ -119,9 +119,7 @@ class XYChart extends PureComponent {
className="euiSeriesChartContainer__emptyPrompt"
iconType="visualizeApp"
title={Chart not available}
- body={
- { statusText }
- }
+ body={{statusText}
}
/>
);
}
@@ -130,10 +128,7 @@ class XYChart extends PureComponent {
const seriesNames = this._getSeriesNames(children);
const classes = classNames(className, 'euiSeriesChartContainer');
return (
-
+
}
{showCrosshair && (
-
+
)}
{enableSelectionBrush && (
@@ -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. */