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. */