diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9abc72155d..9849fbc73b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
## [`master`](https://github.com/elastic/eui/tree/master)
+- Added beta version of `EuiXYChart` and associated components ([#309](https://github.com/elastic/eui/pull/309))
+
**Bug fixes**
- Fixed some IE11 flex box bugs and documented others (modal overflowing, image shrinking, and flex group wrapping) ([#973](https://github.com/elastic/eui/pull/973))
diff --git a/package.json b/package.json
index 877eaa5eeab..bc33138dd53 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,8 @@
"react-datepicker": "v1.4.1",
"react-input-autosize": "^2.2.1",
"react-virtualized": "^9.18.5",
+ "react-vis": "^1.10.1",
+ "serve": "^6.3.1",
"tabbable": "^1.1.0",
"uuid": "^3.1.0"
},
diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js
index f32ea419b91..ecccf285268 100644
--- a/src-docs/src/routes.js
+++ b/src-docs/src/routes.js
@@ -210,6 +210,24 @@ import { ToolTipExample }
import { ToggleExample }
from './views/toggle/toggle_example';
+import { XYChartExample }
+ from './views/xy_chart/xy_chart_example';
+
+import { XYChartAxisExample }
+ from './views/xy_chart_axis/xy_axis_example';
+
+import { XYChartBarExample }
+ from './views/xy_chart_bar/bar_example';
+
+import { XYChartHistogramExample }
+ from './views/xy_chart_histogram/histogram_example';
+
+import { XYChartAreaExample }
+ from './views/xy_chart_area/area_example';
+
+import { XYChartLineExample }
+ from './views/xy_chart_line/line_example';
+
import { Changelog }
from './views/package/changelog';
@@ -340,7 +358,19 @@ const navigation = [{
FilterGroupExample,
SearchBarExample,
].map(example => createExample(example)),
-}, {
+},
+{
+ name: 'XY Charts (Beta)',
+ items: [
+ XYChartExample,
+ XYChartAxisExample,
+ XYChartLineExample,
+ XYChartAreaExample,
+ XYChartBarExample,
+ XYChartHistogramExample,
+ ].map(example => createExample(example)),
+},
+{
name: 'Utilities',
items: [
AccessibilityExample,
diff --git a/src-docs/src/views/xy_chart/complex.js b/src-docs/src/views/xy_chart/complex.js
new file mode 100644
index 00000000000..a85bafe87d2
--- /dev/null
+++ b/src-docs/src/views/xy_chart/complex.js
@@ -0,0 +1,87 @@
+import React, { Fragment, Component } from 'react';
+
+import {
+ EuiText,
+ EuiCodeBlock,
+ EuiSpacer,
+ EuiXYChart,
+ EuiBarSeries,
+ EuiAreaSeries,
+ EuiLineSeries,
+} from '../../../../src/components';
+
+const barSeries = [];
+for (let i = 0; i < 2; i++) {
+ const data = new Array(20).fill(0).map((d, i) => ({ x: i, y: Number((Math.random() * 4).toFixed(2)) }));
+ barSeries.push(data);
+}
+const lineData = new Array(20).fill(0).map((d, i) => ({ x: i, y: Number((Math.random() * 4).toFixed(2)) }));
+const areaData = new Array(20).fill(0).map((d, i) => ({ x: i, y: Number((Math.random() * 4).toFixed(2)) }));
+
+export default class ComplexDemo extends Component {
+ state = {
+ json: 'Please drag your mouse to select an area or click on an element'
+ }
+ handleSelectionBrushEnd = (area) => {
+ this.setState(() => ({
+ eventName: 'onSelectionBrushEnd',
+ json: JSON.stringify(area, null, 2),
+ }));
+ }
+ handleOnValueClick = (data) => {
+ this.setState(() => ({
+ eventName: 'onValueClick',
+ json: JSON.stringify(data, null, 2),
+ }));
+ }
+ handleOnSeriesClick = (series) => () => {
+ this.setState(() => ({
+ eventName: 'onSeriesClick',
+ json: JSON.stringify({ name: series }),
+ }));
+ }
+ render() {
+ const { eventName, json } = this.state;
+ return (
+
+
+
+ {barSeries
+ .map((data, index) => (
+
+ ))}
+
+
+
+ { eventName && (
+
+ Event: { eventName }
+
+ )}
+
+ { json }
+
+
+ );
+ }
+}
diff --git a/src-docs/src/views/xy_chart/crosshair_sync.js b/src-docs/src/views/xy_chart/crosshair_sync.js
new file mode 100644
index 00000000000..445b7bf7bf0
--- /dev/null
+++ b/src-docs/src/views/xy_chart/crosshair_sync.js
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import { EuiSpacer, EuiXYChart, EuiBarSeries } from '../../../../src/components';
+
+// eslint-disable-next-line
+export class ExampleCrosshair extends React.Component {
+ state = {
+ crosshairValue: 2,
+ };
+ _updateCrosshairLocation = crosshairValue => {
+ this.setState({ crosshairValue });
+ };
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src-docs/src/views/xy_chart/empty.js b/src-docs/src/views/xy_chart/empty.js
new file mode 100644
index 00000000000..e9bde5dc9f8
--- /dev/null
+++ b/src-docs/src/views/xy_chart/empty.js
@@ -0,0 +1,5 @@
+import React from 'react';
+
+import { EuiXYChart } from '../../../../src/components';
+
+export default () => ;
diff --git a/src-docs/src/views/xy_chart/horizontal.js b/src-docs/src/views/xy_chart/horizontal.js
new file mode 100644
index 00000000000..baca430ac0b
--- /dev/null
+++ b/src-docs/src/views/xy_chart/horizontal.js
@@ -0,0 +1,39 @@
+import React from 'react';
+
+import {
+ EuiXYChart,
+ EuiAreaSeries,
+ EuiLineSeries,
+ EuiXYChartUtils,
+} from '../../../../src/components';
+const { ORIENTATION } = EuiXYChartUtils;
+
+const data = new Array(80).fill(0).map((d, i) => {
+ const data = {
+ y: i,
+ y0: i,
+ x: Number((Math.random() * 4 + 4).toFixed(2)),
+ x0: 0,
+ };
+ return data;
+});
+
+export default function () {
+ return (
+
+
+
+
+ );
+}
diff --git a/src-docs/src/views/xy_chart/multi_axis.js b/src-docs/src/views/xy_chart/multi_axis.js
new file mode 100644
index 00000000000..e8bdb688cd8
--- /dev/null
+++ b/src-docs/src/views/xy_chart/multi_axis.js
@@ -0,0 +1,75 @@
+import React from 'react';
+
+import {
+ EuiXYChart,
+ EuiLineSeries,
+ EuiXAxis,
+ EuiYAxis,
+ EuiXYChartAxisUtils,
+} from '../../../../src/components';
+import { VISUALIZATION_COLORS } from '../../../../src/services';
+
+
+
+const DATA_A = [{ x: 'A', y: 0 }, { x: 'B', y: 1 }, { x: 'C', y: 2 }, { x: 'D', y: 1 }, { x: 'E', y: 2 }];
+const DATA_B = [{ x: 'A', y: 100 }, { x: 'B', y: 100 }, { x: 'C', y: 150 }, { x: 'D', y: 55 }, { x: 'E', y: 95 }];
+const DATA_C = [{ x: 'A', y: 30 }, { x: 'B', y: 45 }, { x: 'C', y: 67 }, { x: 'D', y: 22 }, { x: 'E', y: 44 }];
+
+const DATA_A_DOMAIN = [-0.5, 3];
+const DATA_B_DOMAIN = [0, 200];
+const DATA_C_DOMAIN = [15, 80];
+
+export default () => (
+
+
+
+
+
+
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart/xy_chart_example.js b/src-docs/src/views/xy_chart/xy_chart_example.js
new file mode 100644
index 00000000000..f6805db4932
--- /dev/null
+++ b/src-docs/src/views/xy_chart/xy_chart_example.js
@@ -0,0 +1,153 @@
+import React, { Fragment } from 'react';
+import { GuideSectionTypes } from '../../components';
+import { EuiCode, EuiXYChart, EuiCallOut, EuiSpacer } from '../../../../src/components';
+import ComplexChartExampleCode from './complex';
+import EmptyExampleCode from './empty';
+import MultiAxisChartExampleCode from './multi_axis';
+import { ExampleCrosshair } from './crosshair_sync';
+
+export const XYChartExample = {
+ title: 'General',
+ intro: (
+
+
+
+ This component is still in Beta. We consider it to be reasonably stable, and welcome you to implement it,
+ but please be aware that breaking changes can come at any time with this component as such changes on beta
+ components does not necessitate a major version bump.
+
+
+
+
+
+ ),
+ sections: [
+ {
+ title: 'Complex example',
+ text: (
+
+
+ Use EuiXYChart to display line, bar, area, and stream charts. Note
+ that charts are composed with EuiLineSeries , EuiAreaSeries ,{' '}
+ EuiBar , and EuiStream being child components.
+
+
+ ),
+ props: { EuiXYChart },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./complex'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ {
+ title: 'Empty Chart',
+ text: (
+
+
When no data is provided to EuiXYChart, an empty state is displayed
+
+ ),
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./empty'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ {
+ title: 'Keep cross-hair in sync',
+ text: (
+
+
+ When displayed side-by-side with other charts, we need to be able to keep them in sync
+
+
+ ),
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./crosshair_sync'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ {
+ title: 'Multi Axis',
+ text: (
+
+
If just displaying values is enough, then you can let the chart auto label axis
+
+ ),
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./multi_axis'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ // TODO include the following example when AreasSeries PR (create vertical areachart)
+ // will be merged into react-vis and orientation prop semantic will be solved.
+ // {
+ // title: 'Horizontal chart',
+ // text: (
+ //
+ //
If just displaying values is enough, then you can let the chart auto label axis
+ //
+ // ),
+ // source: [
+ // {
+ // type: GuideSectionTypes.JS,
+ // code: require('!!raw-loader!./horizontal'),
+ // },
+ // {
+ // type: GuideSectionTypes.HTML,
+ // code: 'This component can only be used from React',
+ // },
+ // ],
+ // demo: (
+ //
+ //
+ //
+ // ),
+ // },
+ ],
+};
diff --git a/src-docs/src/views/xy_chart_area/area.js b/src-docs/src/views/xy_chart_area/area.js
new file mode 100644
index 00000000000..2296360091d
--- /dev/null
+++ b/src-docs/src/views/xy_chart_area/area.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+import { EuiXYChart, EuiAreaSeries } from '../../../../src/components';
+
+const DATA_A = [{ x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 1 }, { x: 5, y: 2 }];
+
+export default () => (
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_area/area_example.js b/src-docs/src/views/xy_chart_area/area_example.js
new file mode 100644
index 00000000000..707484a1687
--- /dev/null
+++ b/src-docs/src/views/xy_chart_area/area_example.js
@@ -0,0 +1,138 @@
+import React, { Fragment } from 'react';
+import { GuideSectionTypes } from '../../components';
+import AreaSeriesExample from './area';
+import StackedAreaSeriesExample from './stacked_area';
+import CurvedAreaExample from './curved_area';
+import RangeAreaExample from './range_area';
+import { EuiCode, EuiAreaSeries, EuiLink, EuiCallOut, EuiSpacer } from '../../../../src/components';
+
+export const XYChartAreaExample = {
+ title: 'Area chart',
+ intro: (
+
+
+
+ This component is still in Beta. We consider it to be reasonably stable, and welcome you to implement it,
+ but please be aware that breaking changes can come at any time with this component as such changes on beta
+ components does not necessitate a major version bump.
+
+
+
+
+
+ ),
+ sections: [
+ {
+ title: 'Area Series',
+ text: (
+
+
+ Use EuiAreaSeries to display area charts.
+
+
+ ),
+ props: { EuiAreaSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./area'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+ ),
+ },
+ {
+ title: 'Stacked Area Series',
+ text: (
+
+
+ Use multiple EuiAreaSeries to display stacked area charts specifying the{' '}
+ stackBy:y prop on the EuiXYChart
+ to enable stacking.
+
+
+ ),
+ props: { EuiAreaSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./stacked_area'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ {
+ title: 'Curved Area Series',
+ text: (
+
+
+ Use the curve prop to change the curve representation. Visit{' '}
+
+ d3-shape#curves
+
+ for available values (the bundle curve does not work with area chart).
+
+
+ ),
+ props: { EuiAreaSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./curved_area'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ {
+ title: 'Range area chart',
+ text: (
+
+ Each point in the chart is specified by two y values y0 (lower value) and
+ y (upper value) to display a range area chart.
+
+ ),
+ props: { EuiAreaSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./range_area'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ ],
+};
diff --git a/src-docs/src/views/xy_chart_area/curved_area.js b/src-docs/src/views/xy_chart_area/curved_area.js
new file mode 100644
index 00000000000..dd6d439dd2d
--- /dev/null
+++ b/src-docs/src/views/xy_chart_area/curved_area.js
@@ -0,0 +1,61 @@
+import React, { Component, Fragment } from 'react';
+
+import {
+ EuiForm,
+ EuiFormRow,
+ EuiSelect,
+ EuiSpacer,
+ EuiXYChart,
+ EuiAreaSeries,
+} from '../../../../src/components';
+
+const DATA_A = [{ x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: -1 }, { x: 5, y: 2 }];
+const DATA_B = [{ x: 0, y: 3 }, { x: 1, y: 2 }, { x: 2, y: 4 }, { x: 3, y: 1 }, { x: 5, y: 3 }];
+
+export default class extends Component {
+ constructor(props) {
+ super(props);
+
+ this.options = [
+ { value: 'linear', text: 'Linear' },
+ { value: 'curveCardinal', text: 'Curve Cardinal' },
+ { value: 'curveNatural', text: 'Curve Natural' },
+ { value: 'curveMonotoneX', text: 'Curve Monotone X' },
+ { value: 'curveMonotoneY', text: 'Curve Monotone Y' },
+ { value: 'curveBasis', text: 'Curve Basis' },
+ { value: 'curveCatmullRom', text: 'Curve Catmull Rom' },
+ { value: 'curveStep', text: 'Curve Step' },
+ { value: 'curveStepAfter', text: 'Curve Step After' },
+ { value: 'curveStepBefore', text: 'Curve Step Before' },
+ ];
+
+ this.state = {
+ value: this.options[0].value,
+ };
+ }
+
+ onChange = e => {
+ this.setState({
+ value: e.target.value,
+ });
+ };
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src-docs/src/views/xy_chart_area/range_area.js b/src-docs/src/views/xy_chart_area/range_area.js
new file mode 100644
index 00000000000..04c01037014
--- /dev/null
+++ b/src-docs/src/views/xy_chart_area/range_area.js
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import { EuiXYChart, EuiAreaSeries, EuiLineSeries } from '../../../../src/components';
+
+const LINE_DATA = new Array(100).fill(0).map((d, i) => ({ x: i, y: Math.random() * 2 + 8 }));
+const AREA_DATA = LINE_DATA.map(({ x, y })=> ({ x, y0: y - Math.random() - 2, y: y + Math.random() + 2 }));
+
+export default () => (
+
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_area/stacked_area.js b/src-docs/src/views/xy_chart_area/stacked_area.js
new file mode 100644
index 00000000000..51286d6ca67
--- /dev/null
+++ b/src-docs/src/views/xy_chart_area/stacked_area.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import { EuiXYChart, EuiAreaSeries } from '../../../../src/components';
+
+const dataA = [{ x: 0, y: 3 }, { x: 1, y: 2 }, { x: 2, y: 1 }, { x: 3, y: 2 }, { x: 4, y: 3 }];
+
+const dataB = [{ x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 4 }, { x: 3, y: 1 }, { x: 4, y: 1 }];
+
+export default () => (
+
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_axis/annotations.js b/src-docs/src/views/xy_chart_axis/annotations.js
new file mode 100644
index 00000000000..7dbb66ff211
--- /dev/null
+++ b/src-docs/src/views/xy_chart_axis/annotations.js
@@ -0,0 +1,51 @@
+import React from 'react';
+
+import {
+ EuiXYChart,
+ EuiLineSeries,
+ EuiLineAnnotation,
+ EuiXYChartUtils,
+ EuiXYChartAxisUtils,
+} from '../../../../src/components';
+
+const DATA_A = [
+ { x: 0, y: 1 },
+ { x: 1, y: 1 },
+ { x: 2, y: 2 },
+ { x: 3, y: -1 },
+ { x: 4, y: null },
+ { x: 5, y: 2 },
+];
+
+export default () => (
+
+
+
+
+
+
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_axis/simple_axis.js b/src-docs/src/views/xy_chart_axis/simple_axis.js
new file mode 100644
index 00000000000..f9e688f343b
--- /dev/null
+++ b/src-docs/src/views/xy_chart_axis/simple_axis.js
@@ -0,0 +1,39 @@
+import React from 'react';
+
+import {
+ EuiLineSeries,
+ EuiXAxis,
+ EuiYAxis,
+ EuiXYChart,
+ EuiXYChartAxisUtils,
+ EuiXYChartTextUtils,
+} from '../../../../src/components';
+
+const DATA = [{ x: 0, y: 5 }, { x: 1, y: 3 }, { x: 2, y: 2 }, { x: 3, y: 3 }];
+
+function xAxisTickFormatter(value) {
+ return EuiXYChartTextUtils.labelWordWrap(`Axis value is ${value}`, 10);
+}
+
+export default () => (
+
+
+
+
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_axis/xy_axis_example.js b/src-docs/src/views/xy_chart_axis/xy_axis_example.js
new file mode 100644
index 00000000000..f26a8e251fb
--- /dev/null
+++ b/src-docs/src/views/xy_chart_axis/xy_axis_example.js
@@ -0,0 +1,82 @@
+import React, { Fragment } from 'react';
+import { GuideSectionTypes } from '../../components';
+import { EuiCode, EuiXAxis, EuiYAxis, EuiLineAnnotation, EuiCallOut, EuiSpacer } from '../../../../src/components';
+import SimpleAxisExampleCode from './simple_axis';
+import AnnotationExampleCode from './annotations';
+
+export const XYChartAxisExample = {
+ title: 'Axis',
+ intro: (
+
+
+
+ This component is still in Beta. We consider it to be reasonably stable, and welcome you to implement it,
+ but please be aware that breaking changes can come at any time with this component as such changes on beta
+ components does not necessitate a major version bump.
+
+
+
+
+
+ ),
+ sections: [
+ {
+ title: 'Complex Axis example',
+ text: (
+
+
+ EuiYAxis and EuiXAxis can be used instead of the{' '}
+ EuiDefaultAxis to allow higher axis customization. See the JS example
+ to check the available properties.
+
+
+ ),
+ props: { EuiXAxis, EuiYAxis },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./simple_axis'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ {
+ title: 'Annotations',
+ text: (
+
+
+ EuiLineAnnotation can be used to add annotation lines with optional text
+ on the chart.
+
+
+ ),
+ props: { EuiLineAnnotation },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./simple_axis'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+ ),
+ },
+ ],
+};
diff --git a/src-docs/src/views/xy_chart_bar/bar_example.js b/src-docs/src/views/xy_chart_bar/bar_example.js
new file mode 100644
index 00000000000..938f7ddd63e
--- /dev/null
+++ b/src-docs/src/views/xy_chart_bar/bar_example.js
@@ -0,0 +1,190 @@
+import React, { Fragment } from 'react';
+import { GuideSectionTypes } from '../../components';
+import VerticalBarSeriesExample from './vertical_bar_series';
+import HorizontalBarSeriesExample from './horizontal_bar_series';
+import StackedVerticalBarSeriesExample from './stacked_vertical_bar_series';
+import StackedHorizontalBarSeriesExample from './stacked_horizontal_bar_series';
+import TimeSeriesExample from './time_series';
+
+import { EuiBadge, EuiCallOut, EuiSpacer, EuiLink, EuiCode, EuiBarSeries } from '../../../../src/components';
+
+export const XYChartBarExample = {
+ title: 'Bar charts',
+ intro: (
+
+
+
+ This component is still in Beta. We consider it to be reasonably stable, and welcome you to implement it,
+ but please be aware that breaking changes can come at any time with this component as such changes on beta
+ components does not necessitate a major version bump.
+
+
+
+
+
+ You can use EuiXYChart with EuiBarSeries to
+ displaying bar charts.
+
+
+
+ The EuiXYChart component pass the orientation prop to every component child
+ to accomodate vertical and horizontal use cases.
+ The default orientation is vertical .
+
+
+
+ You should specify EuiXYChart prop xType="ordinal"
+ to specify the X Axis scale type since you are creating a Bar Chart (read the quote below).
+ You can use barchart also with other X axis scale types, but this can lead to misinterpretation
+ of your charts (basically because the bar width doesn't represent a real measure like in the histograms).
+
+
+
+ You can configure the Y-Axis scale type yType of EuiXYChart
+ with the following scales linear ,log ,
+ time , time-utc .
+
+
+
+
+
+ A bar chart or bar graph is a chart or graph that presents categorical data with rectangular
+ bars with heights or lengths proportional to the values that they represent.
+ The bars can be plotted vertically or horizontally. [...]
+
+
+ A bar graph shows comparisons among discrete categories . One axis of the chart shows the specific
+ categories being compared, and the other axis represents a measured value.
+ Some bar graphs present bars clustered in groups of more than one,
+ showing the values of more than one measured variable.
+
+ Wikipedia
+
+
+
+
+ ),
+ sections: [
+ {
+ title: 'Vertical Bar Chart',
+ text: (
+
+ You can create out-of-the-box vertical bar charts just adding a EuiBarSeries
+ component into your EuiXYChart .
+
+ ),
+ props: { EuiBarSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./vertical_bar_series'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: ( ),
+ },
+ {
+ title: 'Stacked Vertical Bar Chart',
+ text: (
+
+ To display a vertical stacked bar charts specify stackBy="y" .
+ If stackBy is not specified, bars are clustered together depending on
+ the X value.
+
+ ),
+ props: { EuiBarSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./stacked_vertical_bar_series'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: ( ),
+ },
+ {
+ title: 'Horizontal Bar Chart',
+ text: (
+
+
+ experimental
+ To display an horizontal bar chart specify orientation="horizontal" .
+ Since you are rotating the chart, you also have to invert x and y
+ values in your data. The y becomes your ordinal/categorial axis and the
+ x becomes your measure/value axis.
+
+ ),
+ props: { EuiBarSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./horizontal_bar_series'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: ( ),
+ },
+ {
+ title: 'Stacked Horizontal Bar Chart',
+ text: (
+
+
+ experimental
+ To display an horizontal stacked bar charts specify stackBy="x"
+ together with orientation="horizontal" .
+ If stackBy is not specified, bars are clustered together depending on
+ the Y value.
+
+ ),
+ props: { EuiBarSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./stacked_horizontal_bar_series'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: ( ),
+ },
+
+ {
+ title: 'Time Series',
+ text: (
+
+ Use EuiXYChart with xType='time'
+ to display a time series bar chart.
+
+ ),
+ props: { EuiBarSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./time_series'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: ( ),
+ },
+ ],
+};
diff --git a/src-docs/src/views/xy_chart_bar/horizontal_bar_series.js b/src-docs/src/views/xy_chart_bar/horizontal_bar_series.js
new file mode 100644
index 00000000000..990d5206278
--- /dev/null
+++ b/src-docs/src/views/xy_chart_bar/horizontal_bar_series.js
@@ -0,0 +1,22 @@
+import React from 'react';
+
+import { EuiXYChart, EuiBarSeries, EuiXYChartUtils } from '../../../../src/components';
+
+const { SCALE, ORIENTATION } = EuiXYChartUtils;
+const data = [
+ { x: 3, y: 'A' },
+ { x: 1, y: 'B' },
+ { x: 5, y: 'C' },
+ { x: 2, y: 'D' },
+ { x: 1, y: 'E' },
+];
+export default () => (
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_bar/stacked_horizontal_bar_series.js b/src-docs/src/views/xy_chart_bar/stacked_horizontal_bar_series.js
new file mode 100644
index 00000000000..1622eadf6e3
--- /dev/null
+++ b/src-docs/src/views/xy_chart_bar/stacked_horizontal_bar_series.js
@@ -0,0 +1,64 @@
+import React, { Component, Fragment } from 'react';
+
+import {
+ EuiSpacer,
+ EuiButton,
+ EuiXYChart,
+ EuiBarSeries,
+ EuiXYChartUtils,
+} from '../../../../src/components';
+
+const { ORIENTATION, SCALE } = EuiXYChartUtils;
+
+const dataA = [
+ { x: 1, y: 'A' },
+ { x: 2, y: 'B' },
+ { x: 3, y: 'C' },
+ { x: 4, y: 'D' },
+ { x: 5, y: 'E' },
+];
+const dataB = [
+ { x: 3, y: 'A' },
+ { x: 2, y: 'B' },
+ { x: 1, y: 'C' },
+ { x: 2, y: 'D' },
+ { x: 3, y: 'E' },
+];
+
+export default class extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ stacked: true,
+ };
+ }
+
+ onSwitchStacked = () => {
+ this.setState({
+ stacked: !this.state.stacked,
+ });
+ };
+
+ render() {
+ const { stacked } = this.state;
+ return (
+
+
+ Toggle stacked
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src-docs/src/views/xy_chart_bar/stacked_vertical_bar_series.js b/src-docs/src/views/xy_chart_bar/stacked_vertical_bar_series.js
new file mode 100644
index 00000000000..03fdf021bf5
--- /dev/null
+++ b/src-docs/src/views/xy_chart_bar/stacked_vertical_bar_series.js
@@ -0,0 +1,47 @@
+import React, { Component, Fragment } from 'react';
+
+import {
+ EuiSpacer,
+ EuiButton,
+ EuiXYChart,
+ EuiBarSeries,
+ EuiXYChartUtils,
+} from '../../../../src/components';
+
+const { SCALE } = EuiXYChartUtils;
+
+const dataA = [{ x: 0, y: 5 }, { x: 1, y: 4 }, { x: 2, y: 3 }, { x: 3, y: 2 }, { x: 4, y: 1 }];
+
+const dataB = [{ x: 0, y: 1 }, { x: 1, y: 2 }, { x: 2, y: 3 }, { x: 3, y: 4 }, { x: 4, y: 5 }];
+
+export default class extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ stacked: true,
+ };
+ }
+
+ onSwitchStacked = () => {
+ this.setState({
+ stacked: !this.state.stacked,
+ });
+ };
+
+ render() {
+ const { stacked } = this.state;
+ return (
+
+
+ Toggle stacked
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src-docs/src/views/xy_chart_bar/time_series.js b/src-docs/src/views/xy_chart_bar/time_series.js
new file mode 100644
index 00000000000..2b13f1defdc
--- /dev/null
+++ b/src-docs/src/views/xy_chart_bar/time_series.js
@@ -0,0 +1,60 @@
+import React, { Component, Fragment } from 'react';
+
+import {
+ EuiButton,
+ EuiSpacer,
+ EuiXYChart,
+ EuiLineSeries,
+ EuiBarSeries,
+ EuiXYChartUtils,
+} from '../../../../src/components';
+
+const { SCALE } = EuiXYChartUtils;
+const timestamp = Date.now();
+const ONE_HOUR = 3600000;
+
+function randomizeData(size = 24, max = 15) {
+ return new Array(size)
+ .fill(0)
+ .map((d, i) => ({
+ x0: ONE_HOUR * i,
+ x: ONE_HOUR * (i + 1),
+ y: Math.floor(Math.random() * max),
+ }))
+ .map(el => ({
+ x: el.x + timestamp,
+ y: el.y,
+ }));
+}
+function buildData(series) {
+ const max = Math.ceil(Math.random() * 100000);
+ return new Array(series).fill(0).map(() => randomizeData(10, max));
+}
+export default class Example extends Component {
+ state = {
+ series: 4,
+ data: buildData(4),
+ };
+ handleRandomize = () => {
+ this.setState({
+ data: buildData(this.state.series),
+ });
+ };
+ render() {
+ const { data } = this.state;
+ return (
+
+ Randomize data
+
+
+ {data.map((d, i) => (
+
+ ))}
+ {data.map((d, i) => (
+
+ ))}
+
+
+ );
+ }
+}
diff --git a/src-docs/src/views/xy_chart_bar/vertical_bar_series.js b/src-docs/src/views/xy_chart_bar/vertical_bar_series.js
new file mode 100644
index 00000000000..818258b1b18
--- /dev/null
+++ b/src-docs/src/views/xy_chart_bar/vertical_bar_series.js
@@ -0,0 +1,23 @@
+import React from 'react';
+
+import { EuiXYChart, EuiBarSeries, EuiXYChartUtils } from '../../../../src/components';
+const { SCALE } = EuiXYChartUtils;
+const data = [
+ { x: 'A', y: 3 },
+ { x: 'B', y: 1 },
+ { x: 'C', y: 5 },
+ { x: 'D', y: 2 },
+ { x: 'E', y: 1 },
+];
+
+export default () => (
+
+ {
+ console.log({ singleBarData });
+ }}
+ />
+
+);
diff --git a/src-docs/src/views/xy_chart_histogram/histogram_example.js b/src-docs/src/views/xy_chart_histogram/histogram_example.js
new file mode 100644
index 00000000000..950f9326ab5
--- /dev/null
+++ b/src-docs/src/views/xy_chart_histogram/histogram_example.js
@@ -0,0 +1,202 @@
+import React, { Fragment } from 'react';
+import { GuideSectionTypes } from '../../components';
+import VerticalRectSeriesExample from './vertical_rect_series';
+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 {
+ EuiBadge,
+ EuiSpacer,
+ EuiCode,
+ EuiCallOut,
+ EuiLink,
+ EuiHistogramSeries,
+} from '../../../../src/components';
+
+export const XYChartHistogramExample = {
+ title: 'Histograms',
+ intro: (
+
+
+
+ This component is still in Beta. We consider it to be reasonably stable, and welcome you to implement it,
+ but please be aware that breaking changes can come at any time with this component as such changes on beta
+ components does not necessitate a major version bump.
+
+
+
+
+
+ You can use EuiXYChart with EuiHistogramSeries to
+ displaying histogram charts.
+
+
+
+ The EuiXYChart component pass the orientation prop to every component child
+ to accomodate vertical and horizontal use cases.
+ The default orientation is vertical .
+
+
+
+ You can specify the EuiXYChart prop xType and
+ yType to one of the following scales: linear ,log ,
+ time , time-utc .
+ The use of ordinal and category is not supported.
+
+
+
+
+
+ A histogram is an accurate representation of the distribution of numerical data. [...]
+
+
+ To construct a histogram, the first step is to bin the range of values—that is,
+ divide the entire range of values into a series of intervals—and then count how many values fall into each interval.
+ The bins are usually specified as consecutive, non-overlapping intervals of a variable.
+ The bins (intervals) must be adjacent, and are often (but are not required to be) of equal size
+
+ Wikipedia
+
+
+
+
+ ),
+ sections: [
+ {
+ title: 'Vertical Histogram',
+ text: (
+
+ You can create out-of-the-box vertical histograms just adding a EuiHistogramSeries
+ component into your EuiXYChart .
+
+ ),
+ props: { EuiHistogramSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./vertical_rect_series'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: ( ),
+ },
+ {
+ title: 'Stacked Vertical Histogram',
+ text: (
+
+
+ Use EuiXYChart with EuiHistogramSeries for
+ displaying stacked vertical histograms.
+
+
+ Specify stackBy="x" to stack bars together.
+
+
+
+ ),
+ props: { EuiHistogramSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./stacked_vertical_rect_series'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: ( ),
+ },
+ {
+ title: 'Horizontal Histogram',
+ text: (
+
+
+ experimental
+ You can create horizontal histograms specifing orientation="horizontal" .
+ Since you are rotating the histogram, you also have to invert your data.
+
+ ),
+ props: { EuiHistogramSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./vertical_rect_series'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: ( ),
+ },
+ {
+ title: 'Stacked Horizontal Histogram',
+ text: (
+
+
+
+ experimental
+ To display an horizontal stacked histograms specify stackBy="x"
+ together with orientation="horizontal" .
+
+
+
+ ),
+ props: { EuiHistogramSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./stacked_horizontal_rect_series'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: ( ),
+ },
+ {
+ title: 'Time Series Histogram version',
+ text: (
+
+ Use EuiXYChart with xType='time'
+ to display a time series histogram.
+
+ ),
+ props: { EuiHistogramSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./time_histogram_series'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: ( ),
+ },
+ ],
+};
diff --git a/src-docs/src/views/xy_chart_histogram/horizontal_rect_series.js b/src-docs/src/views/xy_chart_histogram/horizontal_rect_series.js
new file mode 100644
index 00000000000..e0d81ba0f06
--- /dev/null
+++ b/src-docs/src/views/xy_chart_histogram/horizontal_rect_series.js
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { EuiXYChart, EuiHistogramSeries, EuiXYChartUtils } from '../../../../src/components';
+
+const data = [
+ { x: 3, y: 0, y0: 1 },
+ { x: 1, y: 1, y0: 2 },
+ { x: 5, y: 2, y0: 3 },
+ { x: 2, y: 3, y0: 4 },
+ { x: 1, y: 4, y0: 5 },
+];
+export default () => (
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_histogram/stacked_horizontal_rect_series.js b/src-docs/src/views/xy_chart_histogram/stacked_horizontal_rect_series.js
new file mode 100644
index 00000000000..65b14e455a5
--- /dev/null
+++ b/src-docs/src/views/xy_chart_histogram/stacked_horizontal_rect_series.js
@@ -0,0 +1,31 @@
+import React from 'react';
+
+import { EuiXYChart, EuiHistogramSeries, EuiXYChartUtils } from '../../../../src/components';
+
+const dataA = [
+ { y: 0, y0: 1, x: 1 },
+ { y: 1, y0: 2, x: 2 },
+ { y: 2, y0: 3, x: 3 },
+ { y: 3, y0: 4, x: 4 },
+ { y: 4, y0: 5, x: 5 },
+];
+
+const dataB = [
+ { y: 0, y0: 1, x: 5 },
+ { y: 1, y0: 2, x: 4 },
+ { y: 2, y0: 3, x: 3 },
+ { y: 3, y0: 4, x: 2 },
+ { y: 4, y0: 5, x: 1 },
+];
+
+export default () => (
+
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_histogram/stacked_vertical_rect_series.js b/src-docs/src/views/xy_chart_histogram/stacked_vertical_rect_series.js
new file mode 100644
index 00000000000..4e1ea5e958f
--- /dev/null
+++ b/src-docs/src/views/xy_chart_histogram/stacked_vertical_rect_series.js
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import { EuiXYChart, EuiHistogramSeries } from '../../../../src/components';
+
+const dataA = [
+ { x0: 0, x: 1, y: 1 },
+ { x0: 1, x: 2, y: 2 },
+ { x0: 2, x: 3, y: 1 },
+ { x0: 3, x: 4, y: 1 },
+ { x0: 4, x: 5, y: 1 },
+];
+
+const dataB = [
+ { x0: 0, x: 1, y: 2 },
+ { x0: 1, x: 2, y: 1 },
+ { x0: 2, x: 3, y: 2 },
+ { x0: 3, x: 4, y: 2 },
+ { x0: 4, x: 5, y: 2 },
+];
+
+export default () => (
+
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_histogram/time_histogram_series.js b/src-docs/src/views/xy_chart_histogram/time_histogram_series.js
new file mode 100644
index 00000000000..ef678a344b3
--- /dev/null
+++ b/src-docs/src/views/xy_chart_histogram/time_histogram_series.js
@@ -0,0 +1,56 @@
+import React, { Component, Fragment } from 'react';
+
+import {
+ EuiButton,
+ EuiSpacer,
+ EuiXYChart,
+ EuiHistogramSeries,
+ EuiXYChartUtils,
+} from '../../../../src/components';
+const { SCALE } = EuiXYChartUtils;
+const timestamp = Date.now();
+const ONE_HOUR = 3600000;
+
+
+function randomizeData(size = 24, max = 15) {
+ return new Array(size)
+ .fill(0)
+ .map((d, i) => ({
+ x0: ONE_HOUR * i,
+ x: ONE_HOUR * (i + 1),
+ y: Math.floor(Math.random() * max),
+ }))
+ .map(el => ({
+ x0: el.x0 + timestamp,
+ x: el.x + timestamp,
+ y: el.y,
+ }));
+}
+function buildData(series) {
+ const max = Math.ceil(Math.random() * 100000000);
+ return new Array(series).fill(0).map(() => randomizeData(100, max));
+}
+export default class Example extends Component {
+ state = {
+ series: 4,
+ data: buildData(4),
+ };
+ handleRandomize = () => {
+ this.setState({
+ data: buildData(this.state.series),
+ });
+ };
+ render() {
+ const { data } = this.state;
+ return (
+
+ Randomize data
+
+
+
+ {data.map((d, i) => )}
+
+
+ );
+ }
+}
diff --git a/src-docs/src/views/xy_chart_histogram/vertical_rect_series.js b/src-docs/src/views/xy_chart_histogram/vertical_rect_series.js
new file mode 100644
index 00000000000..ab1ca994e4e
--- /dev/null
+++ b/src-docs/src/views/xy_chart_histogram/vertical_rect_series.js
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { EuiXYChart, EuiHistogramSeries } from '../../../../src/components';
+
+const data = [
+ { x0: 0, x: 1, y: 1 },
+ { x0: 1, x: 2, y: 3 },
+ { x0: 2, x: 3, y: 2 },
+ { x0: 3, x: 4, y: 0.5 },
+ { x0: 4, x: 5, y: 5 },
+];
+
+export default () => (
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_line/curved_line.js b/src-docs/src/views/xy_chart_line/curved_line.js
new file mode 100644
index 00000000000..22c64c12e31
--- /dev/null
+++ b/src-docs/src/views/xy_chart_line/curved_line.js
@@ -0,0 +1,75 @@
+import React, { Component, Fragment } from 'react';
+
+import {
+ EuiForm,
+ EuiFormRow,
+ EuiSelect,
+ EuiSpacer,
+ EuiXYChart,
+ EuiLineSeries,
+ EuiXYChartUtils,
+} from '../../../../src/components';
+
+const {
+ LINEAR,
+ CURVE_CARDINAL,
+ CURVE_NATURAL,
+ CURVE_MONOTONE_X,
+ CURVE_MONOTONE_Y,
+ CURVE_BASIS,
+ CURVE_BUNDLE,
+ CURVE_CATMULL_ROM,
+ CURVE_STEP,
+ CURVE_STEP_AFTER,
+ CURVE_STEP_BEFORE,
+} = EuiXYChartUtils.CURVE;
+
+const DATA_A = [{ x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: -1 }, { x: 5, y: 2 }];
+
+export default class extends Component {
+ constructor(props) {
+ super(props);
+
+ this.options = [
+ { value: LINEAR, text: 'Linear' },
+ { value: CURVE_CARDINAL, text: 'Curve Cardinal' },
+ { value: CURVE_NATURAL, text: 'Curve Natural' },
+ { value: CURVE_MONOTONE_X, text: 'Curve Monotone X' },
+ { value: CURVE_MONOTONE_Y, text: 'Curve Monotone Y' },
+ { value: CURVE_BASIS, text: 'Curve Basis' },
+ { value: CURVE_BUNDLE, text: 'Curve Bundle' },
+ { value: CURVE_CATMULL_ROM, text: 'Curve Catmull Rom' },
+ { value: CURVE_STEP, text: 'Curve Step' },
+ { value: CURVE_STEP_AFTER, text: 'Curve Step After' },
+ { value: CURVE_STEP_BEFORE, text: 'Curve Step Before' },
+ ];
+
+ this.state = {
+ value: this.options[0].value,
+ };
+ }
+
+ onChange = e => {
+ this.setState({
+ value: e.target.value,
+ });
+ };
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src-docs/src/views/xy_chart_line/custom_domain_line.js b/src-docs/src/views/xy_chart_line/custom_domain_line.js
new file mode 100644
index 00000000000..157203bd138
--- /dev/null
+++ b/src-docs/src/views/xy_chart_line/custom_domain_line.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import { EuiXYChart, EuiLineSeries } from '../../../../src/components';
+
+const X_DOMAIN = [-1, 6];
+const Y_DOMAIN = [0, 3];
+
+const DATA_A = [{ x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 1 }, { x: 5, y: 2 }];
+
+export default () => (
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_line/custom_style_line.js b/src-docs/src/views/xy_chart_line/custom_style_line.js
new file mode 100644
index 00000000000..4bf72ee4d3a
--- /dev/null
+++ b/src-docs/src/views/xy_chart_line/custom_style_line.js
@@ -0,0 +1,109 @@
+import React, { Component, Fragment } from 'react';
+
+import {
+ EuiForm,
+ EuiFormRow,
+ EuiRange,
+ EuiSpacer,
+ EuiXYChart,
+ EuiLineSeries,
+ EuiCheckboxGroup,
+} from '../../../../src/components';
+
+import makeId from '../../../../src/components/form/form_row/make_id';
+
+const DATA_A = [{ x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: -1 }, { x: 5, y: 2 }];
+
+export default class extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ lineMarkSize: '4',
+ lineSize: '2',
+ lineProps: [
+ {
+ id: `showLineMarks`,
+ label: 'Show Line Marks',
+ },
+ ],
+ linePropsIdToSelectedMap: {
+ showLineMarks: true,
+ },
+ };
+ }
+
+ onLinePropsChange = optionId => {
+ const newLinePropsIdToSelectedMap = {
+ ...this.state.linePropsIdToSelectedMap,
+ ...{
+ [optionId]: !this.state.linePropsIdToSelectedMap[optionId],
+ },
+ };
+
+ this.setState({
+ linePropsIdToSelectedMap: newLinePropsIdToSelectedMap,
+ });
+ };
+
+ onChangeLineSize = e => {
+ this.setState({
+ lineSize: e.target.value,
+ });
+ };
+
+ onChangeLineMarkSize = e => {
+ this.setState({
+ lineMarkSize: e.target.value,
+ });
+ };
+
+ render() {
+ const {
+ linePropsIdToSelectedMap: { showLineMarks },
+ lineSize,
+ lineMarkSize,
+ } = this.state;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src-docs/src/views/xy_chart_line/line.js b/src-docs/src/views/xy_chart_line/line.js
new file mode 100644
index 00000000000..5b341a418a4
--- /dev/null
+++ b/src-docs/src/views/xy_chart_line/line.js
@@ -0,0 +1,21 @@
+import React from 'react';
+
+import {
+ EuiXYChart,
+ EuiLineSeries,
+} from '../../../../src/components';
+
+const DATA_A = [
+ { x: 0, y: 1 },
+ { x: 1, y: 1 },
+ { x: 2, y: 2 },
+ { x: 3, y: -1 },
+ { x: 4, y: null },
+ { x: 5, y: 2 },
+];
+
+export default () => (
+
+
+
+);
diff --git a/src-docs/src/views/xy_chart_line/line_example.js b/src-docs/src/views/xy_chart_line/line_example.js
new file mode 100644
index 00000000000..c06772487a5
--- /dev/null
+++ b/src-docs/src/views/xy_chart_line/line_example.js
@@ -0,0 +1,179 @@
+import React, { Fragment } from 'react';
+import { GuideSectionTypes } from '../../components';
+import LineChartExample from './line';
+import CustomDomainLineChartExample from './custom_domain_line';
+import MultiLineChartExample from './multi_line';
+import CurvedLineChartExample from './curved_line';
+import CustomStyleLineChartExample from './custom_style_line';
+import { EuiCode, EuiLineSeries, EuiLink, EuiCallOut, EuiSpacer } from '../../../../src/components';
+
+export const XYChartLineExample = {
+ title: 'Line chart',
+ intro: (
+
+
+
+ This component is still in Beta. We consider it to be reasonably stable, and welcome you to implement it,
+ but please be aware that breaking changes can come at any time with this component as such changes on beta
+ components does not necessitate a major version bump.
+
+
+
+
+
+ ),
+ sections: [
+ {
+ title: 'Line chart',
+ text: (
+
+
+ Use EuiLineSeries to display line charts. The chart domain will cover the
+ whole extent and doesn't add any padding.
+
+
+ ),
+ props: { EuiLineSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./line'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ {
+ title: 'Custom domain line chart',
+ text: (
+
+
+ Use EuiLineSeries to display line charts. Specify{' '}
+ xDomain and/or yDomain
+ props to use custom domains.
+
+
+ ),
+ props: { EuiLineSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./custom_domain_line'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ {
+ title: 'Multi Line chart',
+ text: (
+
+
+ Use multiple EuiLineSeries to display a milti-line chart.
+
+
+ ),
+ props: { EuiLineSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./multi_line'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ {
+ title: 'Curved Line chart',
+ text: (
+
+
+ Use the curve prop to change the curve representation. Visit{' '}
+
+ d3-shape#curves
+
+ for all possible values.
+
+
+ ),
+ props: { EuiLineSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./curved_line'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ {
+ title: 'Custom style Line chart',
+ text: (
+
+
Use the following props to change the style of the Line Chart
+
+
+ lineSize to change the size/width of the line.
+
+
+ lineMarkSize to change the size/radius of marks.
+
+
+ showLine to show/hide the line.
+
+
+ showLineMarks to show/hide the line marks.
+
+
+
+ ),
+ props: { EuiLineSeries },
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: require('!!raw-loader!./custom_style_line'),
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: 'This component can only be used from React',
+ },
+ ],
+ demo: (
+
+
+
+ ),
+ },
+ ],
+};
diff --git a/src-docs/src/views/xy_chart_line/multi_line.js b/src-docs/src/views/xy_chart_line/multi_line.js
new file mode 100644
index 00000000000..4a2bed0e284
--- /dev/null
+++ b/src-docs/src/views/xy_chart_line/multi_line.js
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import { EuiXYChart, EuiLineSeries } from '../../../../src/components';
+
+const DATA_A = [{ x: 0, y: 1 }, { x: 1.5, y: 1 }, { x: 2, y: 2 }, { x: 3, y: -1 }, { x: 5, y: 2 }];
+const DATA_B = [{ x: 0, y: 3 }, { x: 1, y: 4 }, { x: 2, y: 1 }, { x: 3, y: 2 }, { x: 5, y: 5 }];
+
+export default () => (
+
+
+
+
+);
diff --git a/src/components/index.js b/src/components/index.js
index 2c41840fd58..8a6e790e120 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -308,6 +308,27 @@ export {
EuiToolTip,
} from './tool_tip';
+export {
+ EuiXYChart,
+ EuiXYChartUtils,
+ EuiXYChartAxisUtils,
+ EuiXYChartTextUtils,
+ EuiLineSeries,
+ EuiAreaSeries,
+ EuiBarSeries,
+ EuiHistogramSeries,
+ EuiVerticalBarSeries,
+ EuiHorizontalBarSeries,
+ EuiVerticalRectSeries,
+ EuiHorizontalRectSeries,
+ EuiDefaultAxis,
+ EuiXAxis,
+ EuiYAxis,
+ EuiCrosshairX,
+ EuiCrosshairY,
+ EuiLineAnnotation,
+} from './xy_chart';
+
export {
EuiHideFor,
EuiShowFor,
diff --git a/src/components/index.scss b/src/components/index.scss
index 941b3be2524..6338c413808 100644
--- a/src/components/index.scss
+++ b/src/components/index.scss
@@ -52,3 +52,5 @@
@import 'toggle/index';
@import 'tool_tip/index';
@import 'text/index';
+@import 'xy_chart/index';
+
diff --git a/src/components/xy_chart/__snapshots__/selection_brush.test.js.snap b/src/components/xy_chart/__snapshots__/selection_brush.test.js.snap
new file mode 100644
index 00000000000..544da598cfb
--- /dev/null
+++ b/src/components/xy_chart/__snapshots__/selection_brush.test.js.snap
@@ -0,0 +1,448 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiSelectionBrush renders an horizontal selection brush 1`] = `
+
+
+
+
+
+`;
+
+exports[`EuiSelectionBrush renders an vertical selection brush 1`] = `
+
+
+
+
+
+`;
+
+exports[`EuiSelectionBrush renders free form selection brush 1`] = `
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/__snapshots__/xy_chart.test.js.snap b/src/components/xy_chart/__snapshots__/xy_chart.test.js.snap
new file mode 100644
index 00000000000..8fd924184da
--- /dev/null
+++ b/src/components/xy_chart/__snapshots__/xy_chart.test.js.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiXYChart renders an empty chart 1`] = `
+
+
+
+
+
+
+
+
+ Chart not available
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/_index.scss b/src/components/xy_chart/_index.scss
new file mode 100644
index 00000000000..1d4238e3d62
--- /dev/null
+++ b/src/components/xy_chart/_index.scss
@@ -0,0 +1,11 @@
+/* react-vis scss styles copied and pasted from react-vis lib */
+@import "styles/react_vis/plot";
+@import "styles/react_vis/legends";
+@import "styles/react_vis/radial-chart";
+@import "styles/react_vis/treemap";
+
+@import "series/index";
+@import "axis/index";
+@import "legend";
+@import "line_annotation";
+@import "xy_chart";
diff --git a/src/components/xy_chart/_legend.scss b/src/components/xy_chart/_legend.scss
new file mode 100644
index 00000000000..b95838f0152
--- /dev/null
+++ b/src/components/xy_chart/_legend.scss
@@ -0,0 +1,58 @@
+.euiLegendTitle {
+ @include euiTitle;
+ @include euiFontSizeL;
+}
+
+.euiLegendContainer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.euiLegendContent {
+ white-space: nowrap;
+ color: gray;
+ display: flex;
+}
+
+.euiLegendTruncatedLabel {
+ display: inline-block;
+}
+.euiLegendSeriesValue {
+ margin-left: 5px;
+ display: inline-block;
+ color: black;
+}
+.euiLegendMoreSeriesContainer {
+ @include euiFontSizeS;
+ color: gray;
+}
+
+.euiLegendItemContainer {
+ display: flex;
+ align-items: center;
+ color: gray;
+ cursor: pointer;
+ user-select: none;
+ margin-right: 4px;
+ opacity: 1;
+ @include euiFontSizeM;
+ &:last-of-type {
+ margin-right: 0;
+ }
+}
+
+.euiLegendItemIndicator {
+ border-radius: 100%;
+ width: 8px;
+ height: 8px;
+ margin-right: 4px;
+}
+
+.euiTitle--small {
+ @include euiFontSizeM;
+}
+
+.euiTitle--large {
+ @include euiFontSizeXL;
+}
diff --git a/src/components/xy_chart/_line_annotation.scss b/src/components/xy_chart/_line_annotation.scss
new file mode 100644
index 00000000000..85cb88d24c7
--- /dev/null
+++ b/src/components/xy_chart/_line_annotation.scss
@@ -0,0 +1,12 @@
+.euiLineAnnotations__line {
+ stroke: red;
+ stroke-width: 2px;
+ opacity: 0.3;
+}
+.euiLineAnnotations__text {
+ font-size: $euiFontSizeXS;
+ fill: red;
+ stroke-width: 0;
+ opacity: 0.3;
+ alignment-baseline: text-after-edge;
+}
diff --git a/src/components/xy_chart/_xy_chart.scss b/src/components/xy_chart/_xy_chart.scss
new file mode 100644
index 00000000000..121a7a16e9c
--- /dev/null
+++ b/src/components/xy_chart/_xy_chart.scss
@@ -0,0 +1,3 @@
+.rv-xy-plot__inner {
+ overflow: visible; // TODO fix when adding automatic margin into svg
+}
diff --git a/src/components/xy_chart/axis/__snapshots__/default_axis.test.js.snap b/src/components/xy_chart/axis/__snapshots__/default_axis.test.js.snap
new file mode 100644
index 00000000000..cb03e4c7653
--- /dev/null
+++ b/src/components/xy_chart/axis/__snapshots__/default_axis.test.js.snap
@@ -0,0 +1,950 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiDefaultAxis render default axis 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+ 1.0
+
+
+
+
+
+ 1.2
+
+
+
+
+
+ 1.4
+
+
+
+
+
+ 1.6
+
+
+
+
+
+ 1.8
+
+
+
+
+
+ 2.0
+
+
+
+
+
+
+
+
+`;
+
+exports[`EuiDefaultAxis render rotated 90deg default axis 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+ 1.0
+
+
+
+
+
+ 1.2
+
+
+
+
+
+ 1.4
+
+
+
+
+
+ 1.6
+
+
+
+
+
+ 1.8
+
+
+
+
+
+ 2.0
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/axis/__snapshots__/horizontal_grid.test.js.snap b/src/components/xy_chart/axis/__snapshots__/horizontal_grid.test.js.snap
new file mode 100644
index 00000000000..3eff90dbffe
--- /dev/null
+++ b/src/components/xy_chart/axis/__snapshots__/horizontal_grid.test.js.snap
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiHorizontalGrid render the horizontal grid 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/axis/__snapshots__/vertical_grid.test.js.snap b/src/components/xy_chart/axis/__snapshots__/vertical_grid.test.js.snap
new file mode 100644
index 00000000000..a000fa14321
--- /dev/null
+++ b/src/components/xy_chart/axis/__snapshots__/vertical_grid.test.js.snap
@@ -0,0 +1,119 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiVerticalGrid render the vertical grid 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/axis/__snapshots__/x_axis.test.js.snap b/src/components/xy_chart/axis/__snapshots__/x_axis.test.js.snap
new file mode 100644
index 00000000000..89b45cb7c12
--- /dev/null
+++ b/src/components/xy_chart/axis/__snapshots__/x_axis.test.js.snap
@@ -0,0 +1,274 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiXAxis render the x axis 1`] = `
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/axis/__snapshots__/y_axis.test.js.snap b/src/components/xy_chart/axis/__snapshots__/y_axis.test.js.snap
new file mode 100644
index 00000000000..39b1139a3e8
--- /dev/null
+++ b/src/components/xy_chart/axis/__snapshots__/y_axis.test.js.snap
@@ -0,0 +1,174 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiYAxis render the y axis 1`] = `
+
+
+
+
+
+
+
+
+
+
+ 1.0
+
+
+
+
+
+ 1.2
+
+
+
+
+
+ 1.4
+
+
+
+
+
+ 1.6
+
+
+
+
+
+ 1.8
+
+
+
+
+
+ 2.0
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/axis/_grids.scss b/src/components/xy_chart/axis/_grids.scss
new file mode 100644
index 00000000000..f924182f3c8
--- /dev/null
+++ b/src/components/xy_chart/axis/_grids.scss
@@ -0,0 +1,12 @@
+// NOTE: the current implementation of react-vis doesn't supports
+// adding classes to grid lines but only a style prop is supported.
+// We can overwrite here the original react-vis class or overwrite
+// togheter with all other scss properties.
+
+.rv-xy-plot__grid-lines__line {
+
+ stroke-dasharray: 5 5;
+ stroke-opacity: 0.3;
+ // support a clean mouse over when grids is above elements
+ pointer-events: none;
+}
diff --git a/src/components/xy_chart/axis/_index.scss b/src/components/xy_chart/axis/_index.scss
new file mode 100644
index 00000000000..fb806d52f24
--- /dev/null
+++ b/src/components/xy_chart/axis/_index.scss
@@ -0,0 +1 @@
+@import "grids";
diff --git a/src/components/xy_chart/axis/default_axis.js b/src/components/xy_chart/axis/default_axis.js
new file mode 100644
index 00000000000..77c074e807a
--- /dev/null
+++ b/src/components/xy_chart/axis/default_axis.js
@@ -0,0 +1,53 @@
+import React, { Fragment, PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { EuiXAxis } from './x_axis';
+import { EuiYAxis } from './y_axis';
+import { EuiHorizontalGrid } from './horizontal_grid';
+import { EuiVerticalGrid } from './vertical_grid';
+import { ORIENTATION } from '../utils/chart_utils';
+
+
+/**
+ * The Default Axis component, with X and Y axis on the bottom and left position respectively,
+ * and horiznontal or vertical grid depending on the orientation prop.
+ */
+export class EuiDefaultAxis extends PureComponent {
+ render() {
+ const { showGridLines, orientation, xOnZero, yOnZero, ...rest } = this.props;
+
+ return (
+
+ {showGridLines &&
+ orientation === ORIENTATION.VERTICAL && }
+
+ {showGridLines &&
+ orientation === ORIENTATION.HORIZONTAL && }
+
+
+
+
+ );
+ }
+}
+
+EuiDefaultAxis.displayName = 'EuiDefaultAxis';
+
+EuiDefaultAxis.propTypes = {
+ /** The orientation of the chart, used to determine the correct orientation of grids */
+ orientation: PropTypes.string,
+ /** Show/Hide the background grids */
+ showGridLines: PropTypes.bool,
+ /** Specify if the x axis lay on 0, otherwise lyed on min x */
+ xOnZero: PropTypes.bool,
+ /** Specify if the y axis lay on 0, otherwise layd on min y */
+ yOnZero: PropTypes.bool,
+};
+
+EuiDefaultAxis.defaultProps = {
+ orientation: ORIENTATION.VERTICAL,
+ showGridLines: true,
+ xOnZero: false,
+ yOnZero: false,
+};
+
+EuiDefaultAxis.requiresSVG = true;
diff --git a/src/components/xy_chart/axis/default_axis.test.js b/src/components/xy_chart/axis/default_axis.test.js
new file mode 100644
index 00000000000..3ecf82f1ff2
--- /dev/null
+++ b/src/components/xy_chart/axis/default_axis.test.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiLineSeries } from '../series/line_series';
+import { EuiDefaultAxis, EuiXAxis, EuiYAxis, EuiVerticalGrid, EuiHorizontalGrid } from './';
+import { requiredProps } from '../../../test/required_props';
+import { ORIENTATION } from '../utils/chart_utils';
+
+describe('EuiDefaultAxis', () => {
+ test('render default axis', () => {
+ const data = [ { x: 0, y: 1 }, { x: 1, y: 2 }];
+ const component = mount(
+
+
+
+ );
+ expect(component.find(EuiDefaultAxis)).toHaveLength(1);
+ expect(component.find(EuiXAxis)).toHaveLength(1);
+ expect(component.find(EuiYAxis)).toHaveLength(1);
+ expect(component.find(EuiHorizontalGrid)).toHaveLength(1);
+ expect(component.find(EuiVerticalGrid)).toHaveLength(0);
+ expect(component.render()).toMatchSnapshot();
+ });
+ test('render rotated 90deg default axis', () => {
+ const data = [ { x: 0, y: 1 }, { x: 1, y: 2 }];
+ const component = mount(
+
+
+
+ );
+ expect(component.find(EuiDefaultAxis)).toHaveLength(1);
+ expect(component.find(EuiVerticalGrid)).toHaveLength(1);
+ expect(component.find(EuiHorizontalGrid)).toHaveLength(0);
+ expect(component.render()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/xy_chart/axis/horizontal_grid.js b/src/components/xy_chart/axis/horizontal_grid.js
new file mode 100644
index 00000000000..29b6856b157
--- /dev/null
+++ b/src/components/xy_chart/axis/horizontal_grid.js
@@ -0,0 +1,19 @@
+import React, { PureComponent } from 'react';
+import { HorizontalGridLines } from 'react-vis';
+
+/**
+ * Horizontal grid lines aligned with y axis ticks
+ */
+export class EuiHorizontalGrid extends PureComponent {
+ render() {
+ return (
+
+ );
+ }
+}
+
+EuiHorizontalGrid.displayName = 'EuiHorizontalGrid';
+
+EuiHorizontalGrid.requiresSVG = true;
diff --git a/src/components/xy_chart/axis/horizontal_grid.test.js b/src/components/xy_chart/axis/horizontal_grid.test.js
new file mode 100644
index 00000000000..a9b3f1eee63
--- /dev/null
+++ b/src/components/xy_chart/axis/horizontal_grid.test.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiLineSeries } from '../series/line_series';
+import { EuiHorizontalGrid } from './';
+import { requiredProps } from '../../../test/required_props';
+
+describe('EuiHorizontalGrid', () => {
+ test('render the horizontal grid', () => {
+ const data = [ { x: 0, y: 1 }, { x: 1, y: 2 }];
+ const width = 600;
+ const component = mount(
+
+
+
+
+ );
+ const horizontalGridComponent = component.find(EuiHorizontalGrid);
+ expect(horizontalGridComponent).toHaveLength(1);
+ const firstLineProps = horizontalGridComponent.find('line').at(0).props();
+ expect(firstLineProps.y1).toEqual(firstLineProps.y2);
+ expect(firstLineProps.x1).toEqual(0);
+ expect(firstLineProps.x2).toEqual(width - 50); // right + left default xychart margin
+ expect(component.render()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/xy_chart/axis/index.js b/src/components/xy_chart/axis/index.js
new file mode 100644
index 00000000000..ea6034fb57d
--- /dev/null
+++ b/src/components/xy_chart/axis/index.js
@@ -0,0 +1,5 @@
+export { EuiDefaultAxis } from './default_axis';
+export { EuiXAxis } from './x_axis';
+export { EuiYAxis } from './y_axis';
+export { EuiHorizontalGrid } from './horizontal_grid';
+export { EuiVerticalGrid } from './vertical_grid';
diff --git a/src/components/xy_chart/axis/vertical_grid.js b/src/components/xy_chart/axis/vertical_grid.js
new file mode 100644
index 00000000000..657e519c9af
--- /dev/null
+++ b/src/components/xy_chart/axis/vertical_grid.js
@@ -0,0 +1,19 @@
+import React, { PureComponent } from 'react';
+import { VerticalGridLines } from 'react-vis';
+
+/**
+ * Vertical grid lines aligned with x axis ticks
+ */
+export class EuiVerticalGrid extends PureComponent {
+ render() {
+ return (
+
+ );
+ }
+}
+
+EuiVerticalGrid.displayName = 'EuiVerticalGrid';
+
+EuiVerticalGrid.requiresSVG = true;
diff --git a/src/components/xy_chart/axis/vertical_grid.test.js b/src/components/xy_chart/axis/vertical_grid.test.js
new file mode 100644
index 00000000000..a4a21e18c03
--- /dev/null
+++ b/src/components/xy_chart/axis/vertical_grid.test.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiLineSeries } from '../series/line_series';
+import { EuiVerticalGrid } from './';
+import { requiredProps } from '../../../test/required_props';
+
+describe('EuiVerticalGrid', () => {
+ test('render the vertical grid', () => {
+ const data = [ { x: 0, y: 1 }, { x: 1, y: 2 }];
+ const height = 200;
+ const component = mount(
+
+
+
+
+ );
+ const verticalGridComponent = component.find(EuiVerticalGrid);
+ expect(verticalGridComponent).toHaveLength(1);
+ const firstLineProps = verticalGridComponent.find('line').at(0).props();
+ expect(firstLineProps.x1).toEqual(firstLineProps.x2);
+ expect(firstLineProps.y1).toEqual(0);
+ expect(firstLineProps.y2).toEqual(height - 50); // top + bottom default xychart margin
+ expect(component.render()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/xy_chart/axis/x_axis.js b/src/components/xy_chart/axis/x_axis.js
new file mode 100644
index 00000000000..27220605934
--- /dev/null
+++ b/src/components/xy_chart/axis/x_axis.js
@@ -0,0 +1,67 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { XAxis } from 'react-vis';
+import { EuiXYChartAxisUtils } from '../utils/axis_utils';
+
+const { TITLE_POSITION, ORIENTATION } = EuiXYChartAxisUtils;
+
+export class EuiXAxis extends PureComponent {
+ render() {
+ const {
+ title,
+ titlePosition,
+ orientation,
+ tickSize,
+ tickLabelAngle,
+ tickFormat,
+ tickValues,
+ onZero,
+ ...rest
+ } = this.props;
+ return (
+
+ );
+ }
+}
+
+EuiXAxis.displayName = 'EuiXAxis';
+
+EuiXAxis.propTypes = {
+ /** The axis title */
+ title: PropTypes.string,
+ /** The axis title position */
+ titlePosition: PropTypes.oneOf([TITLE_POSITION.START, TITLE_POSITION.MIDDLE, TITLE_POSITION.END]),
+ /** The axis orientation */
+ orientation: PropTypes.oneOf([ORIENTATION.TOP, ORIENTATION.BOTTOM]),
+ /** Fix the axis at zero value */
+ onZero: PropTypes.bool,
+ /** An array of ticks values */
+ ticks: PropTypes.array,
+ /** The height of the ticks in pixels */
+ tickSize: PropTypes.number,
+ /** TODO */
+ tickValues: PropTypes.array,
+ /** A formatter function in the form of function(value, index, scale, tickTotal) */
+ tickFormat: PropTypes.func,
+ /** the rotation angle in degree of the tick label */
+ tickLabelAngle: PropTypes.number,
+};
+
+EuiXAxis.defaultProps = {
+ onZero: false,
+ titlePosition: TITLE_POSITION.MIDDLE,
+ orientation: ORIENTATION.BOTTOM,
+ tickSize: 0,
+};
+
+EuiXAxis.requiresSVG = true;
diff --git a/src/components/xy_chart/axis/x_axis.test.js b/src/components/xy_chart/axis/x_axis.test.js
new file mode 100644
index 00000000000..e46e74b9245
--- /dev/null
+++ b/src/components/xy_chart/axis/x_axis.test.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiLineSeries } from '../series/line_series';
+import { EuiXAxis } from './';
+import { requiredProps } from '../../../test/required_props';
+
+describe('EuiXAxis', () => {
+ test('render the x axis', () => {
+ const data = [ { x: 0, y: 1 }, { x: 1, y: 2 }];
+ const height = 200;
+ const component = mount(
+
+
+
+
+ );
+ expect(component.find(EuiXAxis)).toHaveLength(1);
+ expect(component.render()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/xy_chart/axis/y_axis.js b/src/components/xy_chart/axis/y_axis.js
new file mode 100644
index 00000000000..45761adf694
--- /dev/null
+++ b/src/components/xy_chart/axis/y_axis.js
@@ -0,0 +1,67 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { YAxis } from 'react-vis';
+import { EuiXYChartAxisUtils } from '../utils/axis_utils';
+
+const { TITLE_POSITION, ORIENTATION } = EuiXYChartAxisUtils;
+
+export class EuiYAxis extends PureComponent {
+ render() {
+ const {
+ title,
+ titlePosition,
+ orientation,
+ tickSize,
+ tickLabelAngle,
+ tickFormat,
+ tickValues,
+ onZero,
+ ...rest
+ } = this.props;
+ return (
+
+ );
+ }
+}
+
+EuiYAxis.displayName = 'EuiYAxis';
+
+EuiYAxis.propTypes = {
+ /** The axis title */
+ title: PropTypes.string,
+ /** The axis title position */
+ titlePosition: PropTypes.oneOf([TITLE_POSITION.START, TITLE_POSITION.MIDDLE, TITLE_POSITION.END]),
+ /** The axis orientation */
+ orientation: PropTypes.oneOf([ORIENTATION.LEFT, ORIENTATION.RIGHT]),
+ /** Fix the axis at zero value */
+ onZero: PropTypes.bool,
+ /** An array of ticks values */
+ ticks: PropTypes.array,
+ /** The height of the ticks in pixels */
+ tickSize: PropTypes.number,
+ /** TODO */
+ tickValues: PropTypes.array,
+ /** A formatter function in the form of function(value, index, scale, tickTotal) */
+ tickFormat: PropTypes.func,
+ /** the rotation angle in degree of the tick label */
+ tickLabelAngle: PropTypes.number,
+};
+
+EuiYAxis.defaultProps = {
+ onZero: false,
+ titlePosition: TITLE_POSITION.MIDDLE,
+ orientation: ORIENTATION.LEFT,
+ tickSize: 0,
+};
+
+EuiYAxis.requiresSVG = true;
diff --git a/src/components/xy_chart/axis/y_axis.test.js b/src/components/xy_chart/axis/y_axis.test.js
new file mode 100644
index 00000000000..7767298012d
--- /dev/null
+++ b/src/components/xy_chart/axis/y_axis.test.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiLineSeries } from '../series/line_series';
+import { EuiYAxis } from './';
+import { requiredProps } from '../../../test/required_props';
+
+describe('EuiYAxis', () => {
+ test('render the y axis', () => {
+ const data = [ { x: 0, y: 1 }, { x: 1, y: 2 }];
+ const height = 200;
+ const component = mount(
+
+
+
+
+ );
+ expect(component.find(EuiYAxis)).toHaveLength(1);
+ expect(component.render()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/xy_chart/crosshairs/__snapshots__/crosshair_x.test.js.snap b/src/components/xy_chart/crosshairs/__snapshots__/crosshair_x.test.js.snap
new file mode 100644
index 00000000000..18d0fe9b84a
--- /dev/null
+++ b/src/components/xy_chart/crosshairs/__snapshots__/crosshair_x.test.js.snap
@@ -0,0 +1,44 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiCrosshairX render the X crosshair 1`] = `
+
+`;
diff --git a/src/components/xy_chart/crosshairs/__snapshots__/crosshair_y.test.js.snap b/src/components/xy_chart/crosshairs/__snapshots__/crosshair_y.test.js.snap
new file mode 100644
index 00000000000..7757dd6fb05
--- /dev/null
+++ b/src/components/xy_chart/crosshairs/__snapshots__/crosshair_y.test.js.snap
@@ -0,0 +1,44 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiCrosshairY render the Y crosshair 1`] = `
+
+`;
diff --git a/src/components/xy_chart/crosshairs/crosshair_x.js b/src/components/xy_chart/crosshairs/crosshair_x.js
new file mode 100644
index 00000000000..6423a136d88
--- /dev/null
+++ b/src/components/xy_chart/crosshairs/crosshair_x.js
@@ -0,0 +1,205 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { AbstractSeries, Crosshair } from 'react-vis';
+import { SCALE } from '../utils/chart_utils';
+/**
+ * The Crosshair used by the XYChart as main tooltip mechanism along X axis (vertical).
+ */
+export class EuiCrosshairX extends AbstractSeries {
+ state = {
+ values: [],
+ }
+
+ static get requiresSVG() {
+ return false;
+ }
+
+ static get isCanvas() {
+ return false;
+ }
+
+ static getDerivedStateFromProps(props) {
+ const { crosshairValue, _allData } = props;
+
+ if (crosshairValue !== undefined) {
+ return {
+ values: EuiCrosshairX._computeDataFromXValue(_allData, crosshairValue),
+ };
+ }
+ return null;
+ }
+
+ static _computeDataFromXValue(dataSeries, crosshairValue) {
+ const filteredAndFlattenDataByX = dataSeries
+ .filter(series => series) // get only cleaned data series
+ .map((series, seriesIndex) => {
+ return series
+ .filter(dataPoint => dataPoint.x === crosshairValue)
+ .map(dataPoint => ({ ...dataPoint, originalValues: { ...dataPoint }, seriesIndex }));
+ })
+ .reduce((acc, val) => acc.concat(val), []);
+ return filteredAndFlattenDataByX;
+ }
+
+ onParentMouseMove(event) {
+ this._handleNearestX(event);
+ }
+
+ onParentMouseLeave() {
+ if (this.props.onCrosshairUpdate) {
+ this.props.onCrosshairUpdate(null);
+ }
+ this.setState({
+ values: []
+ });
+ }
+
+ _formatXValue = (x) => {
+ const { xType } = this.props;
+ if (xType === SCALE.TIME || xType === SCALE.TIME_UTC) {
+ return new Date(x).toISOString(); // TODO add a props for time formatting
+ } else {
+ return x;
+ }
+ }
+
+ _titleFormat = (dataPoints = []) => {
+ if (dataPoints.length > 0) {
+ const [ firstDataPoint ] = dataPoints;
+ const { originalValues } = firstDataPoint;
+ const value = (typeof originalValues.x0 === 'number')
+ ? `${this._formatXValue(originalValues.x0)} to ${this._formatXValue(originalValues.x)}`
+ : this._formatXValue(originalValues.x);
+ return {
+ title: 'X Value',
+ value,
+ };
+ }
+ }
+
+ _itemsFormat = (dataPoints) => {
+ const { seriesNames } = this.props;
+
+ return dataPoints.map(d => {
+ return {
+ title: seriesNames[d.seriesIndex],
+ value: d.y,
+ };
+ });
+ }
+
+ _handleNearestX(event) {
+ const cleanedDataSeries = this.props._allData.filter(dataSeries => dataSeries);
+ if (cleanedDataSeries.length === 0) {
+ return;
+ }
+ const containerCoordiante = super._getXYCoordinateInContainer(event);
+ this._findNearestXData(cleanedDataSeries, containerCoordiante.x);
+ }
+
+ /**
+ * _findNearestXData - Find the nearest set of data in all existing series.
+ *
+ * @param {type} dataSeries an array of dataseries
+ * @param {type} mouseXContainerCoords the x coordinate of the mouse on the chart container
+ * @protected
+ */
+ _findNearestXData(dataSeries, mouseXContainerCoords) {
+ const xScaleFn = super._getAttributeFunctor('x');
+ // keeping a global min distance to filter only elements with the same distance
+ let globalMinDistance = Number.POSITIVE_INFINITY;
+
+ const nearestXData = dataSeries
+ .map((data, seriesIndex) => {
+ let minDistance = Number.POSITIVE_INFINITY;
+ let value = null;
+ // TODO to increase the performance, it's better to use a search algorithm like bisect
+ // starting from the assumption that we will always have the same length for
+ // for each series and we can assume that the scale x index can reflect more or less
+ // the position of the mouse inside the array.
+ data.forEach((item) => {
+ let itemXCoords;
+ const xCoord = xScaleFn(item);
+ // check the right item coordinate if we use x0 and x value (e.g. on histograms)
+ if (typeof item.x0 === 'number') {
+ // we need to compute the scaled x0 using the xScale attribute functor
+ // we don't have access of the x0 attribute functor
+ const x0Coord = xScaleFn({ x: item.x0 });
+ itemXCoords = (xCoord - x0Coord) / 2 + x0Coord;
+ } else {
+ itemXCoords = xCoord;
+ }
+ const newDistance = Math.abs(mouseXContainerCoords - itemXCoords);
+ if (newDistance < minDistance) {
+ minDistance = newDistance;
+ value = item;
+ }
+ globalMinDistance = Math.min(globalMinDistance, minDistance);
+ });
+
+ if (!value) {
+ return;
+ }
+
+ return {
+ minDistance,
+ value,
+ seriesIndex,
+ };
+ })
+ .filter(d => d);
+
+ // filter and map nearest X data per dataseries to get only the nearet onces
+ const values = nearestXData
+ .filter(value => value.minDistance === globalMinDistance)
+ .map(value => {
+ // check if we are on histograms and we need to show the right x and y values
+ const d = value.value;
+ const x = typeof d.x0 === 'number'
+ ? (d.x - d.x0) / 2 + d.x0
+ : d.x;
+ const y = typeof d.y0 === 'number'
+ ? (d.y - d.y0)
+ : d.y;
+ return { x, y, originalValues: d, seriesIndex: value.seriesIndex };
+ });
+ const { onCrosshairUpdate } = this.props;
+ if (onCrosshairUpdate) {
+ onCrosshairUpdate(values[0].x);
+ }
+
+ this.setState(() => ({
+ values,
+ }));
+ }
+
+ render() {
+ const { values } = this.state;
+ return (
+
+ );
+ }
+}
+
+EuiCrosshairX.displayName = 'EuiCrosshairX';
+
+EuiCrosshairX.propTypes = {
+ /**
+ * The crosshair value used to display this crosshair (doesn't depend on mouse position)
+ */
+ crosshairValue: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ /**
+ * The ordered array of series names
+ */
+ seriesNames: PropTypes.arrayOf(PropTypes.string).isRequired,
+};
+EuiCrosshairX.defaultProps = {};
diff --git a/src/components/xy_chart/crosshairs/crosshair_x.test.js b/src/components/xy_chart/crosshairs/crosshair_x.test.js
new file mode 100644
index 00000000000..4a8246a039d
--- /dev/null
+++ b/src/components/xy_chart/crosshairs/crosshair_x.test.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiVerticalBarSeries } from '../series/vertical_bar_series';
+import { EuiCrosshairX } from './';
+import { requiredProps } from '../../../test/required_props';
+import { Crosshair } from 'react-vis';
+describe('EuiCrosshairX', () => {
+ test('render the X crosshair', () => {
+ const data = [ { x: 0, y: 1.5 }, { x: 1, y: 2 }];
+ const component = mount(
+
+
+
+
+ );
+ expect(component.find(EuiCrosshairX)).toHaveLength(1);
+ // check if the Crosshair component is empty
+ expect(component.find(Crosshair).children()).toHaveLength(0);
+ expect(component.render()).toMatchSnapshot();
+ expect(component.find('rect')).toHaveLength(2);
+
+ component.find('rect').at(0).simulate('mousemove', { nativeEvent: { clientX: 50, clientY: 100 } });
+ expect(component.find(Crosshair).children()).toHaveLength(1);
+ const crosshair = component.find('.rv-crosshair');
+ expect(crosshair).toHaveLength(1);
+ 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');
+
+ component.find('rect').at(0).simulate('mousemove', { nativeEvent: { clientX: 351, clientY: 100 } });
+ expect(crosshair).toHaveLength(1);
+ 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');
+ });
+});
diff --git a/src/components/xy_chart/crosshairs/crosshair_y.js b/src/components/xy_chart/crosshairs/crosshair_y.js
new file mode 100644
index 00000000000..108cd320751
--- /dev/null
+++ b/src/components/xy_chart/crosshairs/crosshair_y.js
@@ -0,0 +1,386 @@
+/*
+ * 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.
+ */
+
+// Copyright (c) 2016 - 2017 Uber Technologies, Inc.
+
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { AbstractSeries, ScaleUtils } from 'react-vis';
+import { SCALE } from '../utils/chart_utils';
+
+/**
+ * Format title by detault.
+ * @param {Array} values List of values.
+ * @returns {*} Formatted value or undefined.
+ */
+function defaultTitleFormat(values) {
+ const value = getFirstNonEmptyValue(values);
+ if (value) {
+ return {
+ title: 'x',
+ value: value.x
+ };
+ }
+}
+
+/**
+ * Format items by default.
+ * @param {Array} values Array of values.
+ * @returns {*} Formatted list of items.
+ */
+function defaultItemsFormat(values) {
+ return values.map((v, i) => {
+ if (v) {
+ return { value: v.y, title: i };
+ }
+ });
+}
+
+/**
+ * Get the first non-empty item from an array.
+ * @param {Array} values Array of values.
+ * @returns {*} First non-empty value or undefined.
+ */
+function getFirstNonEmptyValue(values) {
+ return (values || []).find(v => Boolean(v));
+}
+
+export class CrosshairY extends PureComponent {
+
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ values: PropTypes.array,
+ series: PropTypes.object,
+ innerWidth: PropTypes.number,
+ innerHeight: PropTypes.number,
+ marginLeft: PropTypes.number,
+ marginTop: PropTypes.number,
+ orientation: PropTypes.oneOf(['left', 'right']),
+ itemsFormat: PropTypes.func,
+ titleFormat: PropTypes.func,
+ style: PropTypes.shape({
+ line: PropTypes.object,
+ title: PropTypes.object,
+ box: PropTypes.object
+ })
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ titleFormat: defaultTitleFormat,
+ itemsFormat: defaultItemsFormat,
+ style: {
+ line: {},
+ title: {},
+ box: {}
+ }
+ };
+ }
+
+ /**
+ * Render crosshair title.
+ * @returns {*} Container with the crosshair title.
+ * @private
+ */
+ _renderCrosshairTitle() {
+ const { values, titleFormat, style } = this.props;
+ const titleItem = titleFormat(values);
+ if (!titleItem) {
+ return null;
+ }
+ return (
+
+ {titleItem.title}
+ {': '}
+ {titleItem.value}
+
+ );
+ }
+
+ /**
+ * Render crosshair items (title + value for each series).
+ * @returns {*} Array of React classes with the crosshair values.
+ * @private
+ */
+ _renderCrosshairItems() {
+ const { values, itemsFormat } = this.props;
+ const items = itemsFormat(values);
+ if (!items) {
+ return null;
+ }
+ return items.filter(i => i).map(function renderValue(item, i) {
+ return (
+
+ {item.title}
+ {': '}
+ {item.value}
+
+ );
+ });
+ }
+
+ render() {
+ const {
+ children,
+ className,
+ values,
+ marginTop,
+ marginLeft,
+ innerWidth,
+ style } = this.props;
+ const value = getFirstNonEmptyValue(values);
+ if (!value) {
+ return null;
+ }
+ const y = ScaleUtils.getAttributeFunctor(this.props, 'y');
+ const innerTop = y(value);
+
+ const left = marginLeft;
+ const top = marginTop + innerTop;
+ const innerClassName = `rv-crosshair__inner rv-crosshair__inner--left`;
+ return (
+
+
+
+
+
+ {children ?
+ children :
+
+
+ {this._renderCrosshairTitle()}
+ {this._renderCrosshairItems()}
+
+
+ }
+
+
+ );
+ }
+}
+
+CrosshairY.displayName = 'CrosshairY';
+
+/**
+ * The Crosshair used by the XYChart as main tooltip mechanism along Y axis (horizontal).
+ */
+export class EuiCrosshairY extends AbstractSeries {
+ state = {
+ values: [],
+ }
+
+ static get requiresSVG() {
+ return false;
+ }
+
+ static get isCanvas() {
+ return false;
+ }
+
+ static getDerivedStateFromProps(props) {
+ const { crosshairValue, _allData } = props;
+
+ if (crosshairValue !== undefined) {
+ return {
+ values: EuiCrosshairY._computeDataFromYValue(_allData, crosshairValue),
+ };
+ }
+ return null;
+ }
+
+ static _computeDataFromYValue(dataSeries, crosshairValue) {
+ const filteredAndFlattenDataByY = dataSeries
+ .filter(series => series) // get only cleaned data series
+ .map((series, seriesIndex) => {
+ return series
+ .filter(dataPoint => dataPoint.y === crosshairValue)
+ .map(dataPoint => ({ ...dataPoint, originalValues: { ...dataPoint }, seriesIndex }));
+ })
+ .reduce((acc, val) => acc.concat(val), []);
+ return filteredAndFlattenDataByY;
+ }
+
+ onParentMouseMove(event) {
+ this._handleNearestY(event);
+ }
+
+ onParentMouseLeave() {
+ if (this.props.onCrosshairUpdate) {
+ this.props.onCrosshairUpdate(null);
+ }
+ this.setState({
+ values: []
+ });
+ }
+ _formatYValue = (y) => {
+ const { yType } = this.props;
+ if (yType === SCALE.TIME || yType === SCALE.TIME_UTC) {
+ return new Date(y).toISOString(); // TODO add a props for time formatting
+ } else {
+ return y;
+ }
+ }
+
+ _titleFormat = (dataPoints = []) => {
+ if (dataPoints.length > 0) {
+ const [ firstDataPoint ] = dataPoints;
+ const { originalValues } = firstDataPoint;
+ const value = (typeof originalValues.y0 === 'number')
+ ? `${this._formatYValue(originalValues.y0)} to ${this._formatYValue(originalValues.y)}`
+ : this._formatYValue(originalValues.y);
+ return {
+ title: 'Y Value',
+ value,
+ };
+ }
+ }
+
+ _itemsFormat = (dataPoints) => {
+ const { seriesNames } = this.props;
+ return dataPoints.map(d => {
+ return {
+ title: seriesNames[d.seriesIndex],
+ value: d.x,
+ };
+ });
+ }
+
+ _handleNearestY(event) {
+ const cleanedDataSeries = this.props._allData.filter(dataSeries => dataSeries);
+ if (cleanedDataSeries.length === 0) {
+ return;
+ }
+ const containerCoordiante = super._getXYCoordinateInContainer(event);
+ this._findNearestYData(cleanedDataSeries, containerCoordiante.y);
+ }
+
+ /**
+ * _findNearestYData - Find the nearest set of data in all existing series.
+ *
+ * @param {type} dataSeries an array of dataseries
+ * @param {type} mouseYContainerCoords the y coordinate of the mouse on the chart container
+ * @protected
+ */
+ _findNearestYData(dataSeries, mouseYContainerCoords) {
+ const yScaleFn = super._getAttributeFunctor('y');
+ // keeping a global min distance to filter only elements with the same distance
+ let globalMinDistance = Number.POSITIVE_INFINITY;
+
+ const nearestYData = dataSeries
+ .map((data, seriesIndex) => {
+ let minDistance = Number.POSITIVE_INFINITY;
+ let value = null;
+ // TODO to increase the performance, it's better to use a search algorithm like bisect
+ // starting from the assumption that we will always have the same length for
+ // for each series and we can assume that the scale y index can reflect more or less
+ // the position of the mouse inside the array.
+ data.forEach((item) => {
+ let itemYCoords;
+ const yCoord = yScaleFn(item);
+ // check the right item coordinate if we use x0 and x value (e.g. on histograms)
+ if (typeof item.y0 === 'number') {
+ // we need to compute the scaled y0 using the xScale attribute functor
+ // we don't have access of the y0 attribute functor
+ const y0Coord = yScaleFn({ y: item.y0 });
+ itemYCoords = (yCoord - y0Coord) / 2 + y0Coord;
+ } else {
+ itemYCoords = yCoord;
+ }
+ const newDistance = Math.abs(mouseYContainerCoords - itemYCoords);
+ if (newDistance < minDistance) {
+ minDistance = newDistance;
+ value = item;
+ }
+ globalMinDistance = Math.min(globalMinDistance, minDistance);
+ });
+
+ if (!value) {
+ return;
+ }
+
+ return {
+ minDistance,
+ value,
+ seriesIndex,
+ };
+ })
+ .filter(d => d);
+
+ // filter and map nearest X data per dataseries to get only the nearet onces
+ const values = nearestYData
+ .filter(value => value.minDistance === globalMinDistance)
+ .map(value => {
+ // check if we are on histograms and we need to show the right x and y values
+ const d = value.value;
+ const y = typeof d.y0 === 'number'
+ ? (d.y - d.y0) / 2 + d.y0
+ : d.y;
+ const x = typeof d.x0 === 'number'
+ ? (d.x - d.x0)
+ : d.x;
+ return { x, y, originalValues: d, seriesIndex: value.seriesIndex };
+ });
+ const { onCrosshairUpdate } = this.props;
+ if (onCrosshairUpdate) {
+ onCrosshairUpdate(values[0].y);
+ }
+
+ this.setState(() => ({
+ values,
+ }));
+ }
+
+ render() {
+ const { values } = this.state;
+ return (
+
+ );
+ }
+}
+
+EuiCrosshairY.displayName = 'EuiCrosshairY';
+
+EuiCrosshairY.propTypes = {
+ /**
+ * The crosshair value used to display this crosshair (doesn't depend on mouse position)
+ */
+ crosshairValue: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ /**
+ * The ordered array of series names
+ */
+ seriesNames: PropTypes.arrayOf(PropTypes.string).isRequired,
+};
+EuiCrosshairY.defaultProps = {};
diff --git a/src/components/xy_chart/crosshairs/crosshair_y.test.js b/src/components/xy_chart/crosshairs/crosshair_y.test.js
new file mode 100644
index 00000000000..6a580544dd2
--- /dev/null
+++ b/src/components/xy_chart/crosshairs/crosshair_y.test.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiHorizontalBarSeries } from '../series/horizontal_bar_series';
+import { EuiCrosshairY } from './';
+import { CrosshairY } from './crosshair_y';
+import { requiredProps } from '../../../test/required_props';
+
+describe('EuiCrosshairY', () => {
+ test('render the Y crosshair', () => {
+ const data = [ { x: 1.5, y: 0 }, { x: 2, y: 1 }];
+ const component = mount(
+
+
+
+
+ );
+ expect(component.find(EuiCrosshairY)).toHaveLength(1);
+ // check if the Crosshair component is empty
+ expect(component.find(CrosshairY).children()).toHaveLength(0);
+ expect(component.render()).toMatchSnapshot();
+ expect(component.find('rect')).toHaveLength(2);
+
+ component.find('rect').at(0).simulate('mousemove', { nativeEvent: { clientX: 100, clientY: 0 } });
+ expect(component.find(CrosshairY).children()).toHaveLength(1);
+ const crosshair = component.find('.rv-crosshair');
+ expect(crosshair).toHaveLength(1);
+ 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');
+
+ component.find('rect').at(0).simulate('mousemove', { nativeEvent: { clientX: 301, clientY: 100 } });
+ expect(crosshair).toHaveLength(1);
+ 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');
+ });
+});
diff --git a/src/components/xy_chart/crosshairs/index.js b/src/components/xy_chart/crosshairs/index.js
new file mode 100644
index 00000000000..7eb5f347a4c
--- /dev/null
+++ b/src/components/xy_chart/crosshairs/index.js
@@ -0,0 +1,2 @@
+export { EuiCrosshairX } from './crosshair_x';
+export { EuiCrosshairY } from './crosshair_y';
diff --git a/src/components/xy_chart/index.js b/src/components/xy_chart/index.js
new file mode 100644
index 00000000000..7f2a702d816
--- /dev/null
+++ b/src/components/xy_chart/index.js
@@ -0,0 +1,14 @@
+export { EuiXYChart } from './xy_chart';
+export { EuiLineAnnotation } from './line_annotation';
+
+// XY chart data series
+export * from './series';
+
+// XY chart axis components
+export * from './axis';
+
+// XY chart utility classes
+export * from './utils';
+
+// XY chart crosshairs
+export * from './crosshairs';
diff --git a/src/components/xy_chart/legend.js b/src/components/xy_chart/legend.js
new file mode 100644
index 00000000000..c31ed79e95f
--- /dev/null
+++ b/src/components/xy_chart/legend.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import LegendItem from './legend_item';
+
+const Title = ({ children }) => {children}
;
+const Container = ({ children }) => {children}
;
+const LegendContent = ({ children }) => {children}
;
+const TruncatedLabel = ({ children }) => {children}
;
+const SeriesValue = ({ children }) => {children}
;
+const MoreSeriesContainer = ({ children }) => {children}
;
+
+function MoreSeries({ hiddenSeries }) {
+ if (hiddenSeries <= 0) {
+ return null;
+ }
+
+ return (+{hiddenSeries}) ;
+}
+
+export default function Legends({ chartTitle, truncateLegends, series, hiddenSeries, clickLegend, seriesVisibility }) {
+ return (
+
+
{chartTitle}
+
+ {series.filter(serie => !serie.isEmpty).map((serie, i) => {
+ const text = (
+
+ {truncateLegends ? {serie.title} : serie.title}
+ {serie.legendValue && {serie.legendValue} }
+
+ );
+ return clickLegend(i)} disabled={seriesVisibility[i]} text={text} color={serie.color} />;
+ })}
+
+
+
+ );
+}
diff --git a/src/components/xy_chart/legend_item.js b/src/components/xy_chart/legend_item.js
new file mode 100644
index 00000000000..6dd5acf0e1f
--- /dev/null
+++ b/src/components/xy_chart/legend_item.js
@@ -0,0 +1,36 @@
+import React, { PureComponent } from 'react';
+
+const Container = ({ children, disabled }) => (
+
+ {children}
+
+);
+
+const Indicator = ({ children, color }) => (
+
+ {children}
+
+);
+
+export default class Legend extends PureComponent {
+ render() {
+ const { onClick, color, text, fontSize, radius, disabled = false, className } = this.props;
+
+ return (
+
+
+ {text}
+
+ );
+ }
+}
diff --git a/src/components/xy_chart/line_annotation.js b/src/components/xy_chart/line_annotation.js
new file mode 100644
index 00000000000..1a6e0fe07cc
--- /dev/null
+++ b/src/components/xy_chart/line_annotation.js
@@ -0,0 +1,125 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { AbstractSeries, ScaleUtils } from 'react-vis';
+import { EuiXYChartUtils } from './utils/chart_utils';
+import { EuiXYChartAxisUtils } from './utils/axis_utils';
+const { HORIZONTAL, VERTICAL } = EuiXYChartUtils.ORIENTATION;
+const { START, MIDDLE, END } = EuiXYChartAxisUtils.TITLE_POSITION;
+
+/**
+ * Draw simple line annotation into the chart. Currently it's a work in progress
+ * but will be extented to add text and tooltips if required.
+ * The basic usage is for displaying the current time marker.
+ */
+export class EuiLineAnnotation extends AbstractSeries {
+ /**
+ * Get attribute functor.
+ * @param {string} attr Attribute name
+ * @returns {*} Functor.
+ * @protected
+ */
+ _getAttributeFunctor(attr) {
+ return ScaleUtils.getAttributeFunctor(this.props, attr);
+ }
+ /**
+ * Get the attribute value if it is available.
+ * @param {string} attr Attribute name.
+ * @returns {*} Attribute value if available, fallback value or undefined
+ * otherwise.
+ * @protected
+ */
+ _getAttributeValue(attr) {
+ return ScaleUtils.getAttributeValue(this.props, attr);
+ }
+ _getTextXY(textPosition, min, max) {
+ switch (textPosition) {
+ case END:
+ return min;
+ case START:
+ return max;
+ case MIDDLE:
+ return Math.abs((max - min) / 2);
+ }
+ }
+ render() {
+ const {
+ data,
+ orientation,
+ textPosition,
+ innerHeight,
+ innerWidth,
+ marginLeft,
+ marginTop,
+ } = this.props;
+ const axis = orientation === HORIZONTAL ? 'y' : 'x';
+ const scale = this._getAttributeFunctor(axis);
+
+ return (
+
+
+ {data.map((d, i) => {
+ const { value } = d;
+ const position = scale({ [axis]: value });
+ return (
+
+ );
+ })}
+
+
+ {data.filter(d => d.text).map((d, i) => {
+ const { value } = d;
+ let x = 0;
+ let y = 0;
+ let rotation = 0;
+ if (orientation === VERTICAL) {
+ x = scale({ [axis]: value });
+ y = this._getTextXY(textPosition, 0, innerHeight);
+ rotation = '-90';
+ } else {
+ x = this._getTextXY(textPosition, innerWidth, 0);
+ y = scale({ [axis]: value });
+ }
+
+ return (
+
+ {d.text}
+
+ );
+ })}
+
+
+ );
+ }
+}
+EuiLineAnnotation.displayName = 'EuiLineAnnotation';
+EuiLineAnnotation.propTypes = {
+ /** An annotation data Array<{value: string|number, text: string}> */
+ data: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ text: PropTypes.string,
+ })
+ ).isRequired,
+ /** The orientation of the annotation. */
+ orientation: PropTypes.oneOf([HORIZONTAL, VERTICAL]),
+ textPosition: PropTypes.oneOf([START, MIDDLE, END]),
+};
+
+EuiLineAnnotation.defaultProps = {
+ orientation: VERTICAL,
+ textPosition: START,
+};
diff --git a/src/components/xy_chart/selection_brush.js b/src/components/xy_chart/selection_brush.js
new file mode 100644
index 00000000000..63c1f0d0b7c
--- /dev/null
+++ b/src/components/xy_chart/selection_brush.js
@@ -0,0 +1,216 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { ScaleUtils, AbstractSeries } from 'react-vis';
+import { ORIENTATION, SCALE } from './utils/chart_utils';
+const { HORIZONTAL, VERTICAL, BOTH } = ORIENTATION;
+
+const DEFAULT_AREAS = {
+ areaSize: 0,
+ drawArea: {
+ x0: 0,
+ x1: 0,
+ y0: 0,
+ y1: 0,
+ },
+ rectArea: {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ }
+};
+
+export class EuiSelectionBrush extends AbstractSeries {
+ state = {
+ drawing: false,
+ ...DEFAULT_AREAS,
+ }
+
+ onParentMouseDown(e) {
+ this._startDrawing(e);
+ }
+
+ onParentMouseMove(e) {
+ this._brushing(e);
+ }
+
+ onParentMouseUp() {
+ this._stopDrawing();
+ }
+
+ onParentMouseLeave() {
+ this._stopDrawing();
+ }
+
+ _getDrawArea(offsetX, offsetY, isStartingPoint) {
+ const { orientation, marginTop, marginLeft, innerHeight, innerWidth } = this.props;
+ const yLocation = offsetY - marginTop;
+ const xLocation = offsetX - marginLeft;
+ let x0;
+ let y0;
+ if (isStartingPoint) {
+ x0 = orientation === VERTICAL ? 0 : xLocation;
+ y0 = orientation === HORIZONTAL ? 0 : yLocation;
+ } else {
+ x0 = this.state.drawArea.x0;
+ y0 = this.state.drawArea.y0;
+ }
+ const x1 = orientation === VERTICAL ? innerWidth : xLocation;
+ const y1 = orientation === HORIZONTAL ? innerHeight : yLocation;
+ const areaSize = Math.abs(x0 - x1) * Math.abs(y0 - y1);
+ return {
+ areaSize,
+ drawArea: {
+ x0,
+ x1,
+ y0,
+ y1,
+ },
+ rectArea: {
+ x: x0 < x1 ? x0 : x1,
+ y: y0 < y1 ? y0 : y1,
+ width: x0 < x1 ? (x1 - x0) : (x0 - x1),
+ height: y0 < y1 ? (y1 - y0) : (y0 - y1),
+ }
+ };
+ }
+
+ _getScaledValue(scale, scaleType, value0, value1) {
+ switch(scaleType) {
+ case SCALE.ORDINAL:
+ return [0, 0];
+ default:
+ return [
+ scale.invert(value0 < value1 ? value0 : value1),
+ scale.invert(value0 < value1 ? value1 : value0),
+ ];
+ break;
+
+ }
+ }
+
+ _startDrawing = (e) => {
+ const { onBrushStart } = this.props;
+ const { offsetX, offsetY } = e.nativeEvent;
+ const drawAndRectAreas = this._getDrawArea(offsetX, offsetY, true);
+ this.setState(() => ({
+ drawing: true,
+ ...drawAndRectAreas,
+ }));
+
+ if (onBrushStart) {
+ onBrushStart(drawAndRectAreas);
+ }
+ }
+
+ _brushing = (e) => {
+ const { onBrushing } = this.props;
+ const { drawing } = this.state;
+ const { offsetX, offsetY } = e.nativeEvent;
+ if (drawing) {
+ const drawAndRectAreas = this._getDrawArea(offsetX, offsetY);
+ this.setState(() => ({
+ ...drawAndRectAreas
+ }));
+
+ if (onBrushing) {
+ onBrushing(drawAndRectAreas);
+ }
+ } else {
+ this.setState(() => ({
+ drawing: false,
+ ...DEFAULT_AREAS,
+ }));
+ }
+ }
+
+ _stopDrawing = () => {
+ // Quickly short-circuit if the user isn't drawing in our component
+ const { drawing } = this.state;
+ if (!drawing) {
+ return;
+ }
+
+ // Clear the draw area
+ this.setState(() => ({
+ drawing: false,
+ ...DEFAULT_AREAS,
+ }));
+
+
+ // Don't invoke the callback if the selected area was < 25 square px.
+ // This is a click not a select
+ const { areaSize } = this.state;
+ if (areaSize < 25) {
+ return;
+ }
+ const { drawArea } = this.state;
+ const { x0, y0, x1, y1 } = drawArea;
+ const { xType, yType, onBrushEnd } = this.props;
+ const xScale = ScaleUtils.getAttributeScale(this.props, 'x');
+ const yScale = ScaleUtils.getAttributeScale(this.props, 'y');
+
+ const xValues = this._getScaledValue(xScale, xType, x0, x1);
+ const yValues = this._getScaledValue(yScale, yType, y0, y1);
+
+ // Compute the corresponding domain drawn
+ const domainArea = {
+ startX: xValues[0],
+ endX: xValues[1],
+ startY: yValues[1],
+ endY: yValues[0],
+ };
+
+ if (onBrushEnd) {
+ onBrushEnd({
+ domainArea,
+ drawArea,
+ });
+ }
+ }
+
+ render() {
+ const { marginLeft, marginTop, color, opacity } = this.props;
+ const { rectArea: { x, y, width, height } } = this.state;
+ return (
+
+
+
+ );
+ }
+}
+
+EuiSelectionBrush.displayName = 'EuiSelectionBrush';
+
+EuiSelectionBrush.propTypes = {
+ /** Specify the brush orientation */
+ orientation: PropTypes.oneOf([ HORIZONTAL, VERTICAL, BOTH ]),
+ /** Callback on brush start event. */
+ onBrushStart: PropTypes.func,
+ /** Callback on every mouse move event. */
+ onBrushing: PropTypes.func,
+ /** Callback on brush end event. */
+ onBrushEnd: PropTypes.func.isRequired,
+ /** The color of the brush rectangle */
+ color: PropTypes.string,
+ /** The opacity of the brush rectangle*/
+ opacity: PropTypes.number,
+};
+
+EuiSelectionBrush.defaultProps = {
+ orientation: HORIZONTAL,
+ color: 'black',
+ opacity: 0.2,
+};
diff --git a/src/components/xy_chart/selection_brush.test.js b/src/components/xy_chart/selection_brush.test.js
new file mode 100644
index 00000000000..dd1bd5ce4b7
--- /dev/null
+++ b/src/components/xy_chart/selection_brush.test.js
@@ -0,0 +1,235 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { EuiXYChart } from './xy_chart';
+import { EuiSelectionBrush } from './selection_brush';
+import { EuiVerticalBarSeries } from './series';
+import { ORIENTATION, SCALE } from './utils/chart_utils';
+
+const NOOP = () => {};
+const DEFAULT_MARGINS = {
+ left: 40,
+ right: 10,
+ top: 10,
+ bottom: 40
+};
+
+describe('EuiSelectionBrush', () => {
+
+ test(`renders an horizontal selection brush`, () => {
+ const data = [{ x: 0, y: 2 }, { x: 1, y: 4 }];
+ const component = mount(
+
+
+
+ );
+
+ let selectionBrush = component.find(EuiSelectionBrush);
+ expect(selectionBrush.exists()).toBe(true);
+ component.find('svg').at(0).simulate('mousemove', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 50, offsetY: DEFAULT_MARGINS.top + 50 }
+ });
+ component.find('svg').at(0).simulate('mousedown', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 50, offsetY: DEFAULT_MARGINS.top + 50 }
+ });
+ component.find('svg').at(0).simulate('mousemove', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 100, offsetY: DEFAULT_MARGINS.top + 100 }
+ });
+ selectionBrush = component.find(EuiSelectionBrush);
+
+ expect(selectionBrush).toMatchSnapshot();
+ expect(selectionBrush.find('rect').at(0).props().x).toBe(50);
+ expect(selectionBrush.find('rect').at(0).props().y).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().width).toBe(50);
+ expect(selectionBrush.find('rect').at(0).props().height).toBe(200 - DEFAULT_MARGINS.top - DEFAULT_MARGINS.bottom);
+ component.find('svg').at(0).simulate('mouseup', { nativeEvent: { offsetX: 100, offsetY: 100 } });
+ selectionBrush = component.find(EuiSelectionBrush);
+ expect(selectionBrush.find('rect').at(0).props().x).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().y).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().width).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().height).toBe(0);
+
+ });
+ test(`renders an vertical selection brush`, () => {
+ const data = [{ x: 0, y: 2 }, { x: 1, y: 4 }];
+ const component = mount(
+
+
+
+ );
+ let selectionBrush = component.find(EuiSelectionBrush);
+ expect(selectionBrush.exists()).toBe(true);
+ component.find('svg').at(0).simulate('mousemove', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 50, offsetY: DEFAULT_MARGINS.top + 50 }
+ });
+ component.find('svg').at(0).simulate('mousedown', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 50, offsetY: DEFAULT_MARGINS.top + 50 }
+ });
+ component.find('svg').at(0).simulate('mousemove', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 100, offsetY: DEFAULT_MARGINS.top + 100
+ } });
+ selectionBrush = component.find(EuiSelectionBrush);
+
+ expect(selectionBrush).toMatchSnapshot();
+ expect(selectionBrush.find('rect').at(0).props().x).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().y).toBe(50);
+ expect(selectionBrush.find('rect').at(0).props().width).toBe(600 - DEFAULT_MARGINS.left - DEFAULT_MARGINS.right);
+ expect(selectionBrush.find('rect').at(0).props().height).toBe(50);
+ component.find('svg').at(0).simulate('mouseup', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 100, offsetY: DEFAULT_MARGINS.top + 100 }
+ });
+ selectionBrush = component.find(EuiSelectionBrush);
+ expect(selectionBrush.find('rect').at(0).props().x).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().y).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().width).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().height).toBe(0);
+ });
+ test(`renders free form selection brush`, () => {
+ const data = [{ x: 0, y: 2 }, { x: 1, y: 4 }];
+ const component = mount(
+
+
+
+ );
+ let selectionBrush = component.find(EuiSelectionBrush);
+ expect(selectionBrush.exists()).toBe(true);
+ component.find('svg').at(0).simulate('mousemove', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 50, offsetY: DEFAULT_MARGINS.top + 50 }
+ });
+ component.find('svg').at(0).simulate('mousedown', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 50, offsetY: DEFAULT_MARGINS.top + 50 }
+ });
+ component.find('svg').at(0).simulate('mousemove', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 100, offsetY: DEFAULT_MARGINS.top + 100 }
+ });
+ selectionBrush = component.find(EuiSelectionBrush);
+
+ expect(selectionBrush).toMatchSnapshot();
+ expect(selectionBrush.find('rect').at(0).props().x).toBe(50);
+ expect(selectionBrush.find('rect').at(0).props().y).toBe(50);
+ expect(selectionBrush.find('rect').at(0).props().width).toBe(50);
+ expect(selectionBrush.find('rect').at(0).props().height).toBe(50);
+ component.find('svg').at(0).simulate('mouseup', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 100, offsetY: DEFAULT_MARGINS.top + 100 }
+ });
+ selectionBrush = component.find(EuiSelectionBrush);
+ expect(selectionBrush.find('rect').at(0).props().x).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().y).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().width).toBe(0);
+ expect(selectionBrush.find('rect').at(0).props().height).toBe(0);
+ });
+ test(`get onSelectionBrushEnd call on linear x scale`, () => {
+ const data = [{ x: 0, y: 2 }, { x: 1, y: 4 }];
+ const onSelectionBrushEndMock = jest.fn();
+ const component = mount(
+
+
+
+ );
+ let selectionBrush = component.find(EuiSelectionBrush);
+ expect(selectionBrush.exists()).toBe(true);
+ component.find('svg').at(0).simulate('mousemove', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 50, offsetY: DEFAULT_MARGINS.top + 50 }
+ });
+ component.find('svg').at(0).simulate('mousedown', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 50, offsetY: DEFAULT_MARGINS.top + 50 }
+ });
+ component.find('svg').at(0).simulate('mousemove', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 100, offsetY: DEFAULT_MARGINS.top + 100 }
+ });
+ component.find('svg').at(0).simulate('mouseup', {
+ nativeEvent: { offsetX: DEFAULT_MARGINS.left + 100, offsetY: DEFAULT_MARGINS.top + 100 }
+ });
+ selectionBrush = component.find(EuiSelectionBrush);
+ expect(onSelectionBrushEndMock.mock.calls.length).toBe(1);
+ const expectedBrush = {
+ domainArea: {
+ startX: -0.5,
+ endX: 1.5,
+ startY: 2,
+ endY: 3,
+ },
+ drawArea: {
+ x0: 0,
+ x1: 600,
+ y0: 50,
+ y1: 100,
+ }
+ };
+ expect(onSelectionBrushEndMock.mock.calls[0][0]).toEqual(expectedBrush);
+ });
+ test.skip(`get onSelectionBrushEnd call on ordinal x scale`, () => {
+ const data = [{ x: 0, y: 2 }, { x: 1, y: 4 }];
+ const onSelectionBrushEndMock = jest.fn();
+ const component = mount(
+
+
+
+ );
+ let selectionBrush = component.find(EuiSelectionBrush);
+ expect(selectionBrush.exists()).toBe(true);
+ component.find('svg').at(0).simulate('mousemove', { nativeEvent: { offsetX: 50, offsetY: 50 } });
+ component.find('svg').at(0).simulate('mousedown', { nativeEvent: { offsetX: 50, offsetY: 50 } });
+ component.find('svg').at(0).simulate('mousemove', { nativeEvent: { offsetX: 100, offsetY: 100 } });
+ component.find('svg').at(0).simulate('mouseup', { nativeEvent: { offsetX: 100, offsetY: 100 } });
+ selectionBrush = component.find(EuiSelectionBrush);
+ expect(onSelectionBrushEndMock.mock.calls.length).toBe(1);
+ const expectedBrush = {
+ // TODO update the domain in respect to ordinal scale
+ domainArea: {
+ startX: -0.5,
+ endX: 1.5,
+ startY: 2,
+ endY: 3,
+ },
+ drawArea: {
+ x0: 0,
+ x1: 600,
+ y0: 50,
+ y1: 100,
+ }
+ };
+ expect(onSelectionBrushEndMock.mock.calls[0][0]).toEqual(expectedBrush);
+ });
+});
diff --git a/src/components/xy_chart/series/__snapshots__/area_series.test.js.snap b/src/components/xy_chart/series/__snapshots__/area_series.test.js.snap
new file mode 100644
index 00000000000..35737f402a1
--- /dev/null
+++ b/src/components/xy_chart/series/__snapshots__/area_series.test.js.snap
@@ -0,0 +1,3621 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiAreaSeries all props are rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+ 6
+
+
+
+
+
+ 7
+
+
+
+
+
+ 8
+
+
+
+
+
+ 9
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/series/__snapshots__/horizontal_bar_series.test.js.snap b/src/components/xy_chart/series/__snapshots__/horizontal_bar_series.test.js.snap
new file mode 100644
index 00000000000..f2c56c35169
--- /dev/null
+++ b/src/components/xy_chart/series/__snapshots__/horizontal_bar_series.test.js.snap
@@ -0,0 +1,1143 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiHorizontalBarSeries all props are rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 5
+
+
+
+
+
+ 10
+
+
+
+
+
+ 15
+
+
+
+
+
+ 20
+
+
+
+
+
+
+
+
+`;
+
+exports[`EuiHorizontalBarSeries is rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 5
+
+
+
+
+
+ 10
+
+
+
+
+
+ 15
+
+
+
+
+
+ 20
+
+
+
+
+
+
+
+
+`;
+
+exports[`EuiHorizontalBarSeries renders stacked bar chart 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ 4
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/series/__snapshots__/horizontal_rect_series.test.js.snap b/src/components/xy_chart/series/__snapshots__/horizontal_rect_series.test.js.snap
new file mode 100644
index 00000000000..d01d678fa8f
--- /dev/null
+++ b/src/components/xy_chart/series/__snapshots__/horizontal_rect_series.test.js.snap
@@ -0,0 +1,1224 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiHorizontalRectSeries all props are rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+ 12
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+`;
+
+exports[`EuiHorizontalRectSeries is rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+ 12
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+`;
+
+exports[`EuiHorizontalRectSeries renders stacked bar chart 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+ 3
+
+
+
+
+
+ 4
+
+
+
+
+
+ 5
+
+
+
+
+
+ 6
+
+
+
+
+
+ 7
+
+
+
+
+
+ 8
+
+
+
+
+
+ 9
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/series/__snapshots__/line_series.test.js.snap b/src/components/xy_chart/series/__snapshots__/line_series.test.js.snap
new file mode 100644
index 00000000000..724bbed719e
--- /dev/null
+++ b/src/components/xy_chart/series/__snapshots__/line_series.test.js.snap
@@ -0,0 +1,8189 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiLineSeries all props are rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+ 12
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`EuiLineSeries is rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+ 12
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/series/__snapshots__/vertical_bar_series.test.js.snap b/src/components/xy_chart/series/__snapshots__/vertical_bar_series.test.js.snap
new file mode 100644
index 00000000000..37e8ac83ea6
--- /dev/null
+++ b/src/components/xy_chart/series/__snapshots__/vertical_bar_series.test.js.snap
@@ -0,0 +1,1292 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiVerticalBarSeries all props are rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -0.4
+
+
+
+
+
+ -0.2
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 1.0
+
+
+
+
+
+ 1.2
+
+
+
+
+
+ 1.4
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 2
+
+
+
+
+
+ 4
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+ 12
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+`;
+
+exports[`EuiVerticalBarSeries is rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -0.4
+
+
+
+
+
+ -0.2
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 1.0
+
+
+
+
+
+ 1.2
+
+
+
+
+
+ 1.4
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 2
+
+
+
+
+
+ 4
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+ 12
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+`;
+
+exports[`EuiVerticalBarSeries renders stacked bar chart 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 2
+
+
+
+
+
+ 4
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/series/__snapshots__/vertical_rect_series.test.js.snap b/src/components/xy_chart/series/__snapshots__/vertical_rect_series.test.js.snap
new file mode 100644
index 00000000000..0c59f31c9dd
--- /dev/null
+++ b/src/components/xy_chart/series/__snapshots__/vertical_rect_series.test.js.snap
@@ -0,0 +1,1332 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiVerticalRectSeries all props are rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 2
+
+
+
+
+
+ 4
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+ 12
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+`;
+
+exports[`EuiVerticalRectSeries is rendered 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 2
+
+
+
+
+
+ 4
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+ 12
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+`;
+
+exports[`EuiVerticalRectSeries renders stacked vertical histogram 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 2
+
+
+
+
+
+ 4
+
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/xy_chart/series/_area_series.scss b/src/components/xy_chart/series/_area_series.scss
new file mode 100644
index 00000000000..868f7c3a090
--- /dev/null
+++ b/src/components/xy_chart/series/_area_series.scss
@@ -0,0 +1,3 @@
+.euiAreaSeries {
+ stroke-width: 0;
+}
diff --git a/src/components/xy_chart/series/_bar_series.scss b/src/components/xy_chart/series/_bar_series.scss
new file mode 100644
index 00000000000..edebd5d9f3f
--- /dev/null
+++ b/src/components/xy_chart/series/_bar_series.scss
@@ -0,0 +1,27 @@
+// NOTE: opacity, stroke and fill are style by code inside react-vis
+// we can overwrite and add !important for force svg styled component
+// or overwrite style by code.
+
+.euiBarSeries {
+ rect {
+ stroke-width: 1;
+ stroke: white !important;
+ rx: 2;
+ ry: 2;
+ }
+}
+
+.euiBarSeries--highDataVolume {
+ rect {
+ stroke-width: 0;
+ rx: 0;
+ ry: 0;
+ }
+}
+.euiBarSeries--hoverEnabled {
+ rect{
+ &:hover {
+ cursor: pointer;
+ }
+ }
+}
diff --git a/src/components/xy_chart/series/_histogram_series.scss b/src/components/xy_chart/series/_histogram_series.scss
new file mode 100644
index 00000000000..9118a97865b
--- /dev/null
+++ b/src/components/xy_chart/series/_histogram_series.scss
@@ -0,0 +1,27 @@
+// NOTE: opacity, stroke and fill are style by code inside react-vis
+// we can overwrite and add !important for force svg styled component
+// or overwrite style by code.
+
+.euiHistogramSeries {
+ rect {
+ stroke-width: 1;
+ stroke: white !important;
+ rx: 2;
+ ry: 2;
+ }
+}
+
+.euiHistogramSeries--highDataVolume {
+ rect {
+ stroke-width: 0;
+ rx: 0;
+ ry: 0;
+ }
+}
+.euiHistogramSeries--hoverEnabled {
+ rect{
+ &:hover {
+ cursor: pointer;
+ }
+ }
+}
diff --git a/src/components/xy_chart/series/_index.scss b/src/components/xy_chart/series/_index.scss
new file mode 100644
index 00000000000..d497bbbf741
--- /dev/null
+++ b/src/components/xy_chart/series/_index.scss
@@ -0,0 +1,3 @@
+@import "area_series";
+@import "bar_series";
+@import "histogram_series";
diff --git a/src/components/xy_chart/series/area_series.js b/src/components/xy_chart/series/area_series.js
new file mode 100644
index 00000000000..d4986137b54
--- /dev/null
+++ b/src/components/xy_chart/series/area_series.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { AreaSeries, AbstractSeries } from 'react-vis';
+import { CURVE } from '../utils/chart_utils';
+
+import { VisualizationColorType } from '../utils/visualization_color_type';
+
+// TODO: needs to send a PR to react-vis for incorporate these changes into AreaSeries class for vertical
+// area chart visualizations.
+// class ExtendedAreaSeries extends AreaSeries {
+// _renderArea(data, x, y0, y, curve, getNull) {
+// const x0 = this._getAttr0Functor('x');
+// let area = d3Area();
+// if (curve !== null) {
+// if (typeof curve === 'string' && curves[curve]) {
+// area = area.curve(curves[curve]);
+// } else if (typeof curve === 'function') {
+// area = area.curve(curve);
+// }
+// }
+// console.log(Object.getPrototypeOf(this))
+// area = area.defined(getNull);
+// area = area
+// .x1(x)
+// .x0(x0) // this is required for displaying vertical area charts.
+// .y0(y0)
+// .y1(y);
+// return area(data);
+// }
+// }
+
+export class EuiAreaSeries extends AbstractSeries {
+ state = {
+ isMouseOverSeries: false,
+ }
+
+ _onSeriesMouseOver = () => {
+ this.setState(() => ({ isMouseOverSeries: true }));
+ }
+
+ _onSeriesMouseOut = () => {
+ this.setState(() => ({ isMouseOverSeries: false }));
+ }
+
+ render() {
+ const { isMouseOverSeries } = this.state;
+ const { name, data, curve, color, onSeriesClick, ...rest } = this.props;
+ return (
+
+ );
+ }
+}
+EuiAreaSeries.displayName = 'EuiAreaSeries';
+EuiAreaSeries.propTypes = {
+ /** The name used to define the data in tooltips and legends */
+ name: PropTypes.string.isRequired,
+ /** Array<{x: string|number, y: string|number}> */
+ data: PropTypes.arrayOf(
+ PropTypes.shape({
+ x: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ y: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ })
+ ).isRequired,
+ /** An EUI visualization color, the default value is enforced by EuiXYChart */
+ color: VisualizationColorType,
+ curve: PropTypes.oneOf(Object.values(CURVE)),
+ onSeriesClick: PropTypes.func,
+};
+
+EuiAreaSeries.defaultProps = {
+ curve: CURVE.LINEAR,
+};
diff --git a/src/components/xy_chart/series/area_series.test.js b/src/components/xy_chart/series/area_series.test.js
new file mode 100644
index 00000000000..4b63f09b514
--- /dev/null
+++ b/src/components/xy_chart/series/area_series.test.js
@@ -0,0 +1,123 @@
+import React from 'react';
+import { mount, render } from 'enzyme';
+import { patchRandom, unpatchRandom } from '../../../test/patch_random';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiAreaSeries } from './area_series';
+import { CURVE } from '../utils/chart_utils';
+import { benchmarkFunction } from '../../../test/time_execution';
+import { VISUALIZATION_COLORS } from '../../../services';
+
+beforeEach(patchRandom);
+afterEach(unpatchRandom);
+
+const AREA_SERIES_PROPS = {
+ name: 'name',
+ data: [{ x: 0, y: 5 }, { x: 1, y: 10 }],
+ color: VISUALIZATION_COLORS[0],
+ curve: CURVE.NATURAL,
+ onSeriesClick: jest.fn(),
+};
+
+describe('EuiAreaSeries', () => {
+ test('all props are rendered', () => {
+ const component = mount(
+
+
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('call onSeriesClick', () => {
+ const data = [{ x: 0, y: 5 }, { x: 1, y: 3 }];
+ const onSeriesClick = jest.fn();
+ const component = mount(
+
+
+
+ );
+ component.find('path').at(0).simulate('click');
+ expect(onSeriesClick.mock.calls).toHaveLength(1);
+ });
+
+ describe('performance', () => {
+ it.skip('renders 1000 items in under 1 second', () => {
+ const yTicks = [[0, 'zero'], [1, 'one']];
+ const xTicks = [
+ [0, '0'],
+ [250, '250'],
+ [500, '500'],
+ [750, '750'],
+ [1000, '1000']
+ ];
+ const data = [];
+
+ for (let i = 0; i < 1000; i++) {
+ data.push({ x: i, y: Math.random() });
+ }
+
+ function renderChart() {
+ render(
+
+
+
+ );
+ }
+
+ const runtime = benchmarkFunction(renderChart);
+ // as of 2018-05-011 / git 00cfbb94d2fcb08aeeed2bb8f4ed0b94eb08307b
+ // this is ~150ms on a MacBookPro
+ expect(runtime).toBeLessThan(1000);
+ });
+
+ it.skip('renders 30 lines of 500 items in under 3 seconds', () => {
+ const yTicks = [[0, 'zero'], [1, 'one']];
+ const xTicks = [
+ [0, '0'],
+ [125, '125'],
+ [250, '240'],
+ [375, '375'],
+ [500, '500']
+ ];
+
+ const linesData = [];
+ for (let i = 0; i < 30; i++) {
+ const data = [];
+
+ for (let i = 0; i < 500; i++) {
+ data.push({ x: i, y: Math.random() });
+ }
+
+ linesData.push(data);
+ }
+
+ function renderChart() {
+ render(
+
+ {linesData.map((data, index) => (
+
+ ))}
+
+ );
+ }
+
+ const runtime = benchmarkFunction(renderChart);
+ // as of 2018-05-011 / git 00cfbb94d2fcb08aeeed2bb8f4ed0b94eb08307b
+ // this is ~2150 on a MacBookPro
+ expect(runtime).toBeLessThan(3000);
+ });
+ });
+});
diff --git a/src/components/xy_chart/series/bar_series.js b/src/components/xy_chart/series/bar_series.js
new file mode 100644
index 00000000000..e9bfa306dcc
--- /dev/null
+++ b/src/components/xy_chart/series/bar_series.js
@@ -0,0 +1,86 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { VerticalBarSeries, HorizontalBarSeries, AbstractSeries } from 'react-vis';
+import { ORIENTATION } from '../utils/chart_utils';
+import classNames from 'classnames';
+
+import { VisualizationColorType } from '../utils/visualization_color_type';
+
+export class EuiBarSeries extends AbstractSeries {
+ state = {
+ isMouseOverValue: false,
+ }
+ static getParentConfig(attr, props) {
+ const { _orientation } = props;
+ return _orientation === ORIENTATION.HORIZONTAL
+ ? HorizontalBarSeries.getParentConfig(attr)
+ : VerticalBarSeries.getParentConfig(attr);
+ }
+ _onValueMouseOver = () => {
+ this.setState(() => ({ isMouseOverValue: true }));
+ }
+
+ _onValueMouseOut = () => {
+ this.setState(() => ({ isMouseOverValue: false }));
+ }
+ render() {
+ const { _orientation, name, data, color, onValueClick, ...rest } = this.props;
+ const { isMouseOverValue } = this.state;
+ const isHighDataVolume = data.length > 80 ? true : false;
+ const classes = classNames(
+ 'euiBarSeries',
+ isHighDataVolume && 'euiBarSeries--highDataVolume',
+ isMouseOverValue && onValueClick && 'euiBarSeries--hoverEnabled',
+ );
+ const BarSeriesComponent = _orientation === ORIENTATION.HORIZONTAL ? HorizontalBarSeries : VerticalBarSeries;
+ return (
+
+ );
+ }
+}
+
+EuiBarSeries.displayName = 'EuiBarSeries';
+
+EuiBarSeries.propTypes = {
+ /**
+ * The name used to define the data in tooltips and legends
+ */
+ name: PropTypes.string.isRequired,
+ /**
+ * Array<{x: string|number, y: string|number}> depending on XYChart xType scale and yType scale
+ */
+ data: PropTypes.arrayOf(PropTypes.shape({
+ x: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ y: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ })).isRequired,
+ /**
+ * An EUI visualization color, the default value is passed through EuiXYChart
+ */
+ color: VisualizationColorType,
+ /**
+ * @private passed via XYChart
+ */
+ // _orientation: PropTypes.string,
+
+ /**
+ * Callback when clicking on a bar. Returns { x, y } object.
+ */
+ onValueClick: PropTypes.func,
+};
+
+EuiBarSeries.defaultProps = {};
diff --git a/src/components/xy_chart/series/histogram_series.js b/src/components/xy_chart/series/histogram_series.js
new file mode 100644
index 00000000000..a18a1e4cc0a
--- /dev/null
+++ b/src/components/xy_chart/series/histogram_series.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { VerticalRectSeries, HorizontalRectSeries, AbstractSeries } from 'react-vis';
+import { ORIENTATION } from '../utils/chart_utils';
+import { VISUALIZATION_COLORS } from '../../../services';
+import classNames from 'classnames';
+
+export class EuiHistogramSeries extends AbstractSeries {
+ state = {
+ isMouseOverValue: false,
+ }
+ static getParentConfig(attr, props) {
+ const { _orientation } = props;
+ return _orientation === ORIENTATION.HORIZONTAL
+ ? HorizontalRectSeries.getParentConfig(attr)
+ : VerticalRectSeries.getParentConfig(attr);
+ }
+ _onValueMouseOver = () => {
+ this.setState(() => ({ isMouseOverValue: true }));
+ }
+
+ _onValueMouseOut = () => {
+ this.setState(() => ({ isMouseOverValue: false }));
+ }
+ render() {
+ const { _orientation, name, data, color, onValueClick, ...rest } = this.props;
+ const { isMouseOverValue } = this.state;
+ const isHighDataVolume = data.length > 80 ? true : false;
+ const classes = classNames(
+ 'euiHistogramSeries',
+ isHighDataVolume && 'euiHistogramSeries--highDataVolume',
+ isMouseOverValue && onValueClick && 'euiHistogramSeries--hoverEnabled',
+ );
+ const HistogramSeriesComponent = _orientation === ORIENTATION.HORIZONTAL ? HorizontalRectSeries : VerticalRectSeries;
+ return (
+
+ );
+ }
+}
+
+EuiHistogramSeries.displayName = 'EuiHistogramSeries';
+
+EuiHistogramSeries.propTypes = {
+ /** The name used to define the data in tooltips and legends */
+ name: PropTypes.string.isRequired,
+ /** Array<{x: number, y: string|number}> */
+ data: PropTypes.arrayOf(PropTypes.shape({
+ x: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ y: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ })).isRequired,
+ /** An EUI visualization color, the default value is enforced by EuiXYChart */
+ color: PropTypes.oneOf(VISUALIZATION_COLORS),
+
+ /**
+ * @private passed via XYChart
+ */
+ // _orientation: PropTypes.string,
+ /**
+ * Callback when clicking on a bar. Returns { x, y } object.
+ */
+ onValueClick: PropTypes.func,
+
+};
+
+EuiHistogramSeries.defaultProps = {};
diff --git a/src/components/xy_chart/series/horizontal_bar_series.js b/src/components/xy_chart/series/horizontal_bar_series.js
new file mode 100644
index 00000000000..a52702d0cb0
--- /dev/null
+++ b/src/components/xy_chart/series/horizontal_bar_series.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { HorizontalBarSeries } from 'react-vis';
+import classNames from 'classnames';
+
+import { VisualizationColorType } from '../utils/visualization_color_type';
+
+export class EuiHorizontalBarSeries extends HorizontalBarSeries {
+ state = {
+ isMouseOverValue: false,
+ }
+
+ _onValueMouseOver = () => {
+ this.setState(() => ({ isMouseOverValue: true }));
+ }
+
+ _onValueMouseOut = () => {
+ this.setState(() => ({ isMouseOverValue: false }));
+ }
+
+ render() {
+ const { isMouseOverValue } = this.state;
+ const { name, data, color, onValueClick, ...rest } = this.props;
+ const isHighDataVolume = data.length > 80 ? true : false;
+ const classes = classNames(
+ 'euiBarSeries',
+ isHighDataVolume && 'euiBarSeries--highDataVolume',
+ isMouseOverValue && onValueClick && 'euiBarSeries--hoverEnabled',
+ );
+ return (
+
+ );
+ }
+}
+
+EuiHorizontalBarSeries.displayName = 'EuiHorizontalBarSeries';
+
+EuiHorizontalBarSeries.propTypes = {
+ /** The name used to define the data in tooltips and legends */
+ name: PropTypes.string.isRequired,
+ /** Array<{x: number, y: string|number}> */
+ data: PropTypes.arrayOf(PropTypes.shape({
+ x: PropTypes.number,
+ y: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ })).isRequired,
+ /** An EUI visualization color, the default value is enforced by EuiXYChart */
+ color: VisualizationColorType,
+ /**
+ * Callback when clicking on a bar. Returns { x, y } object.
+ */
+ onValueClick: PropTypes.func
+};
+
+EuiHorizontalBarSeries.defaultProps = {};
diff --git a/src/components/xy_chart/series/horizontal_bar_series.test.js b/src/components/xy_chart/series/horizontal_bar_series.test.js
new file mode 100644
index 00000000000..a4634ec7156
--- /dev/null
+++ b/src/components/xy_chart/series/horizontal_bar_series.test.js
@@ -0,0 +1,106 @@
+import React from 'react';
+import { render, mount } from 'enzyme';
+import { patchRandom, unpatchRandom } from '../../../test/patch_random';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiHorizontalBarSeries } from './horizontal_bar_series';
+import { VISUALIZATION_COLORS } from '../../../services';
+
+beforeEach(patchRandom);
+afterEach(unpatchRandom);
+
+describe('EuiHorizontalBarSeries', () => {
+ test('is rendered', () => {
+ const component = mount(
+
+
+
+ );
+
+ expect(component.find('.rv-xy-plot__series')).toHaveLength(1);
+
+ const rects = component.find('.rv-xy-plot__series--bar rect');
+ expect(rects).toHaveLength(2);
+
+ const firstRectProps = rects.at(0).props();
+ expect(firstRectProps.x).toBeDefined();
+ expect(firstRectProps.y).toBeDefined();
+ expect(firstRectProps.width).toBeDefined();
+ expect(firstRectProps.height).toBeDefined();
+
+ const secondRectProps = rects.at(1).props();
+ expect(secondRectProps.x).toBeDefined();
+ expect(secondRectProps.y).toBeDefined();
+ expect(secondRectProps.width).toBeDefined();
+ expect(secondRectProps.height).toBeDefined();
+
+ expect(component.render()).toMatchSnapshot();
+ });
+
+ test('call onValueClick', () => {
+ const data = [{ x: 0, y: 5 }, { x: 1, y: 3 }];
+ const onValueClick = jest.fn();
+ const component = mount(
+
+
+
+ );
+ component.find('rect').at(0).simulate('click');
+ expect(onValueClick.mock.calls).toHaveLength(1);
+ expect(onValueClick.mock.calls[0][0]).toEqual(data[0]);
+ });
+
+ test('all props are rendered', () => {
+ const component = render(
+
+ {}}
+ />
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('renders stacked bar chart', () => {
+ const component = render(
+
+ {}}
+ />
+ {}}
+ />
+
+ );
+ expect(component.find('.rv-xy-plot__series')).toHaveLength(2);
+ expect(component.find('.rv-xy-plot__series--bar rect')).toHaveLength(4);
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/src/components/xy_chart/series/horizontal_rect_series.js b/src/components/xy_chart/series/horizontal_rect_series.js
new file mode 100644
index 00000000000..237a7ea27da
--- /dev/null
+++ b/src/components/xy_chart/series/horizontal_rect_series.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { HorizontalRectSeries } from 'react-vis';
+import classNames from 'classnames';
+
+import { VisualizationColorType } from '../utils/visualization_color_type';
+
+export class EuiHorizontalRectSeries extends HorizontalRectSeries {
+ state = {
+ isMouseOverValue: false,
+ }
+
+ _onValueMouseOver = () => {
+ this.setState(() => ({ isMouseOverValue: true }));
+ }
+
+ _onValueMouseOut = () => {
+ this.setState(() => ({ isMouseOverValue: false }));
+ }
+
+ render() {
+ const { isMouseOverValue } = this.state;
+ const { name, data, color, onValueClick, ...rest } = this.props;
+ const isHighDataVolume = data.length > 80 ? true : false;
+ const classes = classNames(
+ 'euiHistogramSeries',
+ isHighDataVolume && 'euiHistogramSeries--highDataVolume',
+ isMouseOverValue && onValueClick && 'euiHistogramSeries--hoverEnabled',
+ );
+ return (
+
+ );
+ }
+}
+
+EuiHorizontalRectSeries.displayName = 'EuiHorizontalRectSeries';
+
+EuiHorizontalRectSeries.propTypes = {
+ /** The name used to define the data in tooltips and legends */
+ name: PropTypes.string.isRequired,
+ /** Array<{x: number, y: number, y0: number}> */
+ data: PropTypes.arrayOf(PropTypes.shape({
+ x: PropTypes.number,
+ y: PropTypes.number,
+ y0: PropTypes.number,
+ })).isRequired,
+ /** An EUI visualization color, the default value is enforced by EuiXYChart */
+ color: VisualizationColorType,
+ /**
+ * Callback when clicking on a bar. Returns { x, y } object.
+ */
+ onValueClick: PropTypes.func
+};
+
+EuiHorizontalRectSeries.defaultProps = {};
diff --git a/src/components/xy_chart/series/horizontal_rect_series.test.js b/src/components/xy_chart/series/horizontal_rect_series.test.js
new file mode 100644
index 00000000000..fdc5df469f8
--- /dev/null
+++ b/src/components/xy_chart/series/horizontal_rect_series.test.js
@@ -0,0 +1,106 @@
+import React from 'react';
+import { render, mount } from 'enzyme';
+import { patchRandom, unpatchRandom } from '../../../test/patch_random';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiHorizontalRectSeries } from './horizontal_rect_series';
+import { VISUALIZATION_COLORS } from '../../../services';
+
+beforeEach(patchRandom);
+afterEach(unpatchRandom);
+
+describe('EuiHorizontalRectSeries', () => {
+ test('is rendered', () => {
+ const component = mount(
+
+
+
+ );
+
+ expect(component.find('.rv-xy-plot__series')).toHaveLength(1);
+
+ const rects = component.find('.rv-xy-plot__series--rect rect');
+ expect(rects).toHaveLength(2);
+
+ const firstRectProps = rects.at(0).props();
+ expect(firstRectProps.x).toBeDefined();
+ expect(firstRectProps.y).toBeDefined();
+ expect(firstRectProps.width).toBeDefined();
+ expect(firstRectProps.height).toBeDefined();
+
+ const secondRectProps = rects.at(1).props();
+ expect(secondRectProps.x).toBeDefined();
+ expect(secondRectProps.y).toBeDefined();
+ expect(secondRectProps.width).toBeDefined();
+ expect(secondRectProps.height).toBeDefined();
+
+ expect(component.render()).toMatchSnapshot();
+ });
+
+ test('all props are rendered', () => {
+ const component = render(
+
+ {}}
+ />
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('call onValueClick', () => {
+ const data = [{ x: 0, y: 5 }, { x: 1, y: 3 }];
+ const onValueClick = jest.fn();
+ const component = mount(
+
+
+
+ );
+ component.find('rect').at(0).simulate('click');
+ expect(onValueClick.mock.calls).toHaveLength(1);
+ expect(onValueClick.mock.calls[0][0]).toEqual(data[0]);
+ });
+
+ test('renders stacked bar chart', () => {
+ const component = render(
+
+ {}}
+ />
+ {}}
+ />
+
+ );
+ expect(component.find('.rv-xy-plot__series')).toHaveLength(2);
+ expect(component.find('.rv-xy-plot__series--rect rect')).toHaveLength(4);
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/src/components/xy_chart/series/index.js b/src/components/xy_chart/series/index.js
new file mode 100644
index 00000000000..4eb1e6c4f76
--- /dev/null
+++ b/src/components/xy_chart/series/index.js
@@ -0,0 +1,8 @@
+export { EuiLineSeries } from './line_series';
+export { EuiAreaSeries } from './area_series';
+export { EuiBarSeries } from './bar_series';
+export { EuiHistogramSeries } from './histogram_series';
+export { EuiVerticalBarSeries } from './vertical_bar_series';
+export { EuiHorizontalBarSeries } from './horizontal_bar_series';
+export { EuiVerticalRectSeries } from './vertical_rect_series';
+export { EuiHorizontalRectSeries } from './horizontal_rect_series';
diff --git a/src/components/xy_chart/series/line_series.js b/src/components/xy_chart/series/line_series.js
new file mode 100644
index 00000000000..047cf185d6c
--- /dev/null
+++ b/src/components/xy_chart/series/line_series.js
@@ -0,0 +1,101 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { LineSeries, MarkSeries, AbstractSeries } from 'react-vis';
+import { CURVE } from '../utils/chart_utils';
+import { VisualizationColorType } from '../utils/visualization_color_type';
+
+export class EuiLineSeries extends AbstractSeries {
+ render() {
+ const {
+ data,
+ name,
+ curve,
+ onSeriesClick,
+ onValueClick,
+ showLineMarks,
+ lineSize,
+ lineMarkColor,
+ lineMarkSize,
+ color,
+ ...rest
+ } = this.props;
+
+ return (
+
+
+
+
+ {showLineMarks && (
+
+ )}
+
+ );
+ }
+}
+
+EuiLineSeries.displayName = 'EuiLineSeries';
+
+EuiLineSeries.propTypes = {
+ /** The name used to define the data in tooltips and legends */
+ name: PropTypes.string.isRequired,
+ /** Array<{x: string|number, y: string|number}> */
+ data: PropTypes.arrayOf(PropTypes.shape({
+ x: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ y: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ })).isRequired,
+ /** An EUI visualization color, the default value is enforced by EuiXYChart */
+ color: VisualizationColorType,
+ curve: PropTypes.oneOf(Object.values(CURVE)),
+ showLineMarks: PropTypes.bool,
+ lineSize: PropTypes.number,
+ lineMarkColor: PropTypes.string,
+ lineMarkSize: PropTypes.number,
+ onSeriesClick: PropTypes.func,
+ onValueClick: PropTypes.func
+};
+
+EuiLineSeries.defaultProps = {
+ curve: CURVE.LINEAR,
+ showLineMarks: false,
+ lineSize: 1,
+ lineMarkSize: 0
+};
diff --git a/src/components/xy_chart/series/line_series.test.js b/src/components/xy_chart/series/line_series.test.js
new file mode 100644
index 00000000000..6fc92066b53
--- /dev/null
+++ b/src/components/xy_chart/series/line_series.test.js
@@ -0,0 +1,144 @@
+import React from 'react';
+import { mount, render } from 'enzyme';
+import { patchRandom, unpatchRandom } from '../../../test/patch_random';
+import { benchmarkFunction } from '../../../test/time_execution';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiLineSeries } from './line_series';
+import { VISUALIZATION_COLORS } from '../../../services';
+
+beforeEach(patchRandom);
+afterEach(unpatchRandom);
+
+describe('EuiLineSeries', () => {
+ test('is rendered', () => {
+ const component = mount(
+
+
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('all props are rendered', () => {
+ const component = mount(
+
+ {}}
+ onValueClick={() => {}}
+ />
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('call onValueClick and onSeriesClick', () => {
+ const data = [{ x: 0, y: 5 }, { x: 1, y: 3 }];
+ const onValueClick = jest.fn();
+ const onSeriesClick = jest.fn();
+ const component = mount(
+
+
+
+ );
+ component.find('path').at(0).simulate('click');
+ expect(onSeriesClick.mock.calls).toHaveLength(1);
+ component.find('circle').at(0).simulate('click');
+ expect(onValueClick.mock.calls).toHaveLength(1);
+ expect(onValueClick.mock.calls[0][0]).toEqual(data[0]);
+ // check if onSeriesClick is fired after clicking on marker
+ expect(onSeriesClick.mock.calls).toHaveLength(1);
+ });
+
+
+ describe('performance', () => {
+ it.skip('renders 1000 items in under 1 second', () => {
+ const yTicks = [[0, 'zero'], [1, 'one']];
+ const xTicks = [
+ [0, '0'],
+ [250, '250'],
+ [500, '500'],
+ [750, '750'],
+ [1000, '1000']
+ ];
+ const data = [];
+
+ for (let i = 0; i < 1000; i++) {
+ data.push({ x: i, y: Math.random() });
+ }
+
+ function renderChart() {
+ render(
+
+
+
+ );
+ }
+
+ const runtime = benchmarkFunction(renderChart);
+ // as of 2018-05-011 / git 00cfbb94d2fcb08aeeed2bb8f4ed0b94eb08307b
+ // this is ~120ms on a MacBookPro
+ expect(runtime).toBeLessThan(1000);
+ });
+
+ it.skip('renders 30 lines of 500 items in under 3 seconds', () => {
+ const yTicks = [[0, 'zero'], [1, 'one']];
+ const xTicks = [
+ [0, '0'],
+ [125, '125'],
+ [250, '240'],
+ [375, '375'],
+ [500, '500']
+ ];
+
+ const linesData = [];
+ for (let i = 0; i < 30; i++) {
+ const data = [];
+
+ for (let i = 0; i < 500; i++) {
+ data.push({ x: i, y: Math.random() });
+ }
+
+ linesData.push(data);
+ }
+
+ function renderChart() {
+ render(
+
+ {linesData.map((data, index) => (
+
+ ))}
+
+ );
+ }
+
+ const runtime = benchmarkFunction(renderChart);
+ // as of 2018-05-011 / git 00cfbb94d2fcb08aeeed2bb8f4ed0b94eb08307b
+ // this is ~1700ms on a MacBookPro
+ expect(runtime).toBeLessThan(3000);
+ });
+ });
+});
diff --git a/src/components/xy_chart/series/vertical_bar_series.js b/src/components/xy_chart/series/vertical_bar_series.js
new file mode 100644
index 00000000000..eaaf1faab39
--- /dev/null
+++ b/src/components/xy_chart/series/vertical_bar_series.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { VerticalBarSeries } from 'react-vis';
+import classNames from 'classnames';
+
+import { VisualizationColorType } from '../utils/visualization_color_type';
+
+export class EuiVerticalBarSeries extends VerticalBarSeries {
+ state = {
+ isMouseOverValue: false,
+ }
+
+ _onValueMouseOver = () => {
+ this.setState(() => ({ isMouseOverValue: true }));
+ }
+
+ _onValueMouseOut = () => {
+ this.setState(() => ({ isMouseOverValue: false }));
+ }
+
+ render() {
+ const { isMouseOverValue } = this.state;
+ const { name, data, color, onValueClick, ...rest } = this.props;
+ const isHighDataVolume = data.length > 80 ? true : false;
+ const classes = classNames(
+ 'euiBarSeries',
+ isHighDataVolume && 'euiBarSeries--highDataVolume',
+ isMouseOverValue && onValueClick && 'euiBarSeries--hoverEnabled',
+ );
+ return (
+
+ );
+ }
+}
+
+EuiVerticalBarSeries.displayName = 'EuiVerticalBarSeries';
+
+EuiVerticalBarSeries.propTypes = {
+ /** The name used to define the data in tooltips and legends */
+ name: PropTypes.string.isRequired,
+ /** Array<{x: string|number, y: number}> */
+ data: PropTypes.arrayOf(PropTypes.shape({
+ x: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number
+ ]),
+ y: PropTypes.number,
+ })).isRequired,
+ /** An EUI visualization color, the default value is enforced by EuiXYChart */
+ color: VisualizationColorType,
+ /**
+ * Callback when clicking on a bar. Returns { x, y } object.
+ */
+ onValueClick: PropTypes.func,
+};
+
+EuiVerticalBarSeries.defaultProps = {};
diff --git a/src/components/xy_chart/series/vertical_bar_series.test.js b/src/components/xy_chart/series/vertical_bar_series.test.js
new file mode 100644
index 00000000000..534741454a9
--- /dev/null
+++ b/src/components/xy_chart/series/vertical_bar_series.test.js
@@ -0,0 +1,172 @@
+import React from 'react';
+import { render, mount } from 'enzyme';
+import { patchRandom, unpatchRandom } from '../../../test/patch_random';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiVerticalBarSeries } from './vertical_bar_series';
+import { benchmarkFunction } from '../../../test/time_execution';
+import { VISUALIZATION_COLORS } from '../../../services';
+
+beforeEach(patchRandom);
+afterEach(unpatchRandom);
+
+describe('EuiVerticalBarSeries', () => {
+ test('is rendered', () => {
+ const component = mount(
+
+
+
+ );
+
+ expect(component.find('.rv-xy-plot__series')).toHaveLength(1);
+
+ const rects = component.find('.rv-xy-plot__series--bar rect');
+ expect(rects).toHaveLength(2);
+
+ const firstRectProps = rects.at(0).props();
+ expect(firstRectProps.x).toBeDefined();
+ expect(firstRectProps.y).toBeDefined();
+ expect(firstRectProps.width).toBeDefined();
+ expect(firstRectProps.height).toBeDefined();
+
+ const secondRectProps = rects.at(1).props();
+ expect(secondRectProps.x).toBeDefined();
+ expect(secondRectProps.y).toBeDefined();
+ expect(secondRectProps.width).toBeDefined();
+ expect(secondRectProps.height).toBeDefined();
+
+ expect(component.render()).toMatchSnapshot();
+ });
+
+ test('all props are rendered', () => {
+ const component = render(
+
+ {}}
+ />
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('renders stacked bar chart', () => {
+ const component = render(
+
+
+
+
+ );
+ expect(component.find('.rv-xy-plot__series')).toHaveLength(2);
+ expect(component.find('.rv-xy-plot__series--bar rect')).toHaveLength(4);
+ expect(component).toMatchSnapshot();
+ });
+ test('call onValueClick', () => {
+ const data = [{ x: 0, y: 5 }, { x: 1, y: 3 }];
+ const onValueClick = jest.fn();
+ const component = mount(
+
+
+
+ );
+ component.find('rect').at(0).simulate('click');
+ expect(onValueClick.mock.calls).toHaveLength(1);
+ expect(onValueClick.mock.calls[0][0]).toEqual(data[0]);
+ });
+
+ describe.skip('performance', () => {
+ it('renders 1000 items in under 0.5 seconds', () => {
+ const yTicks = [[0, 'zero'], [1, 'one']];
+ const xTicks = [
+ [0, '0'],
+ [250, '250'],
+ [500, '500'],
+ [750, '750'],
+ [1000, '1000']
+ ];
+ const data = [];
+
+ for (let i = 0; i < 1000; i++) {
+ data.push({ x: i, y: Math.random() });
+ }
+
+ function renderChart() {
+ render(
+
+
+
+ );
+ }
+
+ const runtime = benchmarkFunction(renderChart);
+ // as of 2018-05-011 / git 00cfbb94d2fcb08aeeed2bb8f4ed0b94eb08307b
+ // this is ~9ms on a MacBookPro
+ expect(runtime).toBeLessThan(500);
+ });
+
+ it('renders 30 lines of 500 items in under 3 seconds', () => {
+ const yTicks = [[0, 'zero'], [1, 'one']];
+ const xTicks = [
+ [0, '0'],
+ [125, '125'],
+ [250, '240'],
+ [375, '375'],
+ [500, '500']
+ ];
+
+ const linesData = [];
+ for (let i = 0; i < 30; i++) {
+ const data = [];
+
+ for (let i = 0; i < 500; i++) {
+ data.push({ x: i, y: Math.random() });
+ }
+
+ linesData.push(data);
+ }
+
+ function renderChart() {
+ render(
+
+ {linesData.map((data, index) => (
+
+ ))}
+
+ );
+ }
+
+ const runtime = benchmarkFunction(renderChart);
+ // as of 2018-05-011 / git 00cfbb94d2fcb08aeeed2bb8f4ed0b94eb08307b
+ // this is ~1750 on a MacBookPro
+ expect(runtime).toBeLessThan(3000);
+ });
+ });
+});
diff --git a/src/components/xy_chart/series/vertical_rect_series.js b/src/components/xy_chart/series/vertical_rect_series.js
new file mode 100644
index 00000000000..b1bc0e392a0
--- /dev/null
+++ b/src/components/xy_chart/series/vertical_rect_series.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { VerticalRectSeries } from 'react-vis';
+import classNames from 'classnames';
+
+import { VisualizationColorType } from '../utils/visualization_color_type';
+
+export class EuiVerticalRectSeries extends VerticalRectSeries {
+ state = {
+ isMouseOverValue: false,
+ }
+
+ _onValueMouseOver = () => {
+ this.setState(() => ({ isMouseOverValue: true }));
+ }
+
+ _onValueMouseOut = () => {
+ this.setState(() => ({ isMouseOverValue: false }));
+ }
+
+ render() {
+ const { isMouseOverValue } = this.state;
+ const { name, data, color, onValueClick, ...rest } = this.props;
+ const isHighDataVolume = data.length > 80 ? true : false;
+ const classes = classNames(
+ 'euiHistogramSeries',
+ isHighDataVolume && 'euiHistogramSeries--highDataVolume',
+ isMouseOverValue && onValueClick && 'euiHistogramSeries--hoverEnabled',
+ );
+ return (
+
+ );
+ }
+}
+
+EuiVerticalRectSeries.displayName = 'EuiVerticalRectSeries';
+
+EuiVerticalRectSeries.propTypes = {
+ /** The name used to define the data in tooltips and legends */
+ name: PropTypes.string.isRequired,
+ /** Array<{x0: number, x: number, y: number}> */
+ data: PropTypes.arrayOf(PropTypes.shape({
+ x0: PropTypes.number,
+ x: PropTypes.number,
+ y: PropTypes.number,
+ })).isRequired,
+ /** An EUI visualization color, the default value is enforced by EuiXYChart */
+ color: VisualizationColorType,
+ /**
+ * Callback when clicking on a bar. Returns { x, y } object.
+ */
+ onValueClick: PropTypes.func
+};
+
+EuiVerticalRectSeries.defaultProps = {};
diff --git a/src/components/xy_chart/series/vertical_rect_series.test.js b/src/components/xy_chart/series/vertical_rect_series.test.js
new file mode 100644
index 00000000000..29e965dd697
--- /dev/null
+++ b/src/components/xy_chart/series/vertical_rect_series.test.js
@@ -0,0 +1,176 @@
+import React from 'react';
+import { render, mount } from 'enzyme';
+import { patchRandom, unpatchRandom } from '../../../test/patch_random';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiXYChart } from '../xy_chart';
+import { EuiVerticalRectSeries } from './vertical_rect_series';
+import { benchmarkFunction } from '../../../test/time_execution';
+import { VISUALIZATION_COLORS } from '../../../services';
+
+beforeEach(patchRandom);
+afterEach(unpatchRandom);
+
+describe('EuiVerticalRectSeries', () => {
+ test('is rendered', () => {
+ const component = mount(
+
+
+
+ );
+
+ expect(component.find('.rv-xy-plot__series')).toHaveLength(1);
+
+ const rects = component.find('.rv-xy-plot__series--rect rect');
+ expect(rects).toHaveLength(2);
+
+ const firstRectProps = rects.at(0).props();
+ expect(firstRectProps.x).toBeDefined();
+ expect(firstRectProps.y).toBeDefined();
+ expect(firstRectProps.width).toBeDefined();
+ expect(firstRectProps.height).toBeDefined();
+
+ const secondRectProps = rects.at(1).props();
+ expect(secondRectProps.x).toBeDefined();
+ expect(secondRectProps.y).toBeDefined();
+ expect(secondRectProps.width).toBeDefined();
+ expect(secondRectProps.height).toBeDefined();
+
+ expect(component.render()).toMatchSnapshot();
+ });
+
+ test('all props are rendered', () => {
+ const component = render(
+
+ {}}
+ />
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('call onValueClick', () => {
+ const data = [{ x: 0, y: 5 }, { x: 1, y: 3 }];
+ const onValueClick = jest.fn();
+ const component = mount(
+
+
+
+ );
+ component.find('rect').at(0).simulate('click');
+ expect(onValueClick.mock.calls).toHaveLength(1);
+ expect(onValueClick.mock.calls[0][0]).toEqual(data[0]);
+ });
+
+ test('renders stacked vertical histogram', () => {
+ const component = render(
+
+ {}}
+ />
+ {}}
+ />
+
+ );
+ expect(component.find('.rv-xy-plot__series')).toHaveLength(2);
+ expect(component.find('.rv-xy-plot__series--rect rect')).toHaveLength(4);
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('performance', () => {
+ it.skip('renders 1000 items in under 0.5 seconds', () => {
+ const yTicks = [[0, 'zero'], [1, 'one']];
+ const xTicks = [
+ [0, '0'],
+ [250, '250'],
+ [500, '500'],
+ [750, '750'],
+ [1000, '1000']
+ ];
+ const data = [];
+
+ for (let i = 0; i < 1000; i++) {
+ data.push({ x: i, y: Math.random() });
+ }
+
+ function renderChart() {
+ render(
+
+
+
+ );
+ }
+
+ const runtime = benchmarkFunction(renderChart);
+ // as of 2018-05-011 / git 00cfbb94d2fcb08aeeed2bb8f4ed0b94eb08307b
+ // this is ~9ms on a MacBookPro
+ expect(runtime).toBeLessThan(500);
+ });
+
+ it.skip('renders 30 lines of 500 items in under 3 seconds', () => {
+ const yTicks = [[0, 'zero'], [1, 'one']];
+ const xTicks = [
+ [0, '0'],
+ [125, '125'],
+ [250, '240'],
+ [375, '375'],
+ [500, '500']
+ ];
+
+ const linesData = [];
+ for (let i = 0; i < 30; i++) {
+ const data = [];
+
+ for (let i = 0; i < 500; i++) {
+ data.push({ x: i, y: Math.random() });
+ }
+
+ linesData.push(data);
+ }
+
+ function renderChart() {
+ render(
+
+ {linesData.map((data, index) => (
+
+ ))}
+
+ );
+ }
+
+ const runtime = benchmarkFunction(renderChart);
+ // as of 2018-05-011 / git 00cfbb94d2fcb08aeeed2bb8f4ed0b94eb08307b
+ // this is ~1750 on a MacBookPro
+ expect(runtime).toBeLessThan(3000);
+ });
+ });
+});
diff --git a/src/components/xy_chart/status-text.js b/src/components/xy_chart/status-text.js
new file mode 100644
index 00000000000..45ba7da8121
--- /dev/null
+++ b/src/components/xy_chart/status-text.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { EuiText } from '../text';
+import { EuiIcon } from '../icon';
+
+function StatusText({ width, height, text }) {
+ return (
+
+
+
+
+ Graph not avaliable
+
+ {text &&
{text} }
+
+
+ );
+}
+
+StatusText.propTypes = {
+ text: PropTypes.string
+};
+
+export default StatusText;
diff --git a/src/components/xy_chart/styles/react_vis/legends.scss b/src/components/xy_chart/styles/react_vis/legends.scss
new file mode 100644
index 00000000000..45e8674dcab
--- /dev/null
+++ b/src/components/xy_chart/styles/react_vis/legends.scss
@@ -0,0 +1,134 @@
+$rv-legend-enabled-color: #3a3a48;
+$rv-legend-disabled-color: #b8b8b8;
+
+.rv-discrete-color-legend {
+ box-sizing: border-box;
+ overflow-y: auto;
+ font-size: 12px;
+
+ &.horizontal {
+ white-space: nowrap;
+ }
+}
+
+.rv-discrete-color-legend-item {
+ color: $rv-legend-enabled-color;
+ border-radius: 1px;
+ padding: 9px 10px;
+
+ &.horizontal {
+ display: inline-block;
+
+ .rv-discrete-color-legend-item__title {
+ margin-left: 0;
+ display: block;
+ }
+ }
+}
+
+.rv-discrete-color-legend-item__color {
+ background: #dcdcdc;
+ display: inline-block;
+ height: 2px;
+ vertical-align: middle;
+ width: 14px;
+}
+
+.rv-discrete-color-legend-item__title {
+ margin-left: 10px;
+}
+
+.rv-discrete-color-legend-item.disabled {
+ color: $rv-legend-disabled-color;
+}
+
+.rv-discrete-color-legend-item.clickable {
+ cursor: pointer;
+
+ &:hover {
+ background: #f9f9f9;
+ }
+}
+
+.rv-search-wrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.rv-search-wrapper__form {
+ flex: 0;
+}
+
+.rv-search-wrapper__form__input {
+ width: 100%;
+ color: #a6a6a5;
+ border: 1px solid #e5e5e4;
+ padding: 7px 10px;
+ font-size: 12px;
+ box-sizing: border-box;
+ border-radius: 2px;
+ margin: 0 0 9px;
+ outline: 0;
+}
+
+.rv-search-wrapper__contents {
+ flex: 1;
+ overflow: auto;
+}
+
+.rv-continuous-color-legend {
+ font-size: 12px;
+
+ .rv-gradient {
+ height: 4px;
+ border-radius: 2px;
+ margin-bottom: 5px;
+ }
+}
+
+.rv-continuous-size-legend {
+ font-size: 12px;
+
+ .rv-bubbles {
+ text-align: justify;
+ overflow: hidden;
+ margin-bottom: 5px;
+ width: 100%;
+ }
+
+ .rv-bubble {
+ background: #d8d9dc;
+ display: inline-block;
+ vertical-align: bottom;
+ }
+
+ .rv-spacer {
+ display: inline-block;
+ font-size: 0;
+ line-height: 0;
+ width: 100%;
+ }
+}
+
+.rv-legend-titles {
+ height: 16px;
+ position: relative;
+}
+
+.rv-legend-titles__left,
+.rv-legend-titles__right,
+.rv-legend-titles__center {
+ position: absolute;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.rv-legend-titles__center {
+ display: block;
+ text-align: center;
+ width: 100%;
+}
+
+.rv-legend-titles__right {
+ right: 0;
+}
diff --git a/src/components/xy_chart/styles/react_vis/plot.scss b/src/components/xy_chart/styles/react_vis/plot.scss
new file mode 100644
index 00000000000..8d1f75ca255
--- /dev/null
+++ b/src/components/xy_chart/styles/react_vis/plot.scss
@@ -0,0 +1,128 @@
+$rv-xy-plot-axis-font-color: #6b6b76;
+$rv-xy-plot-axis-line-color: #e6e6e9;
+$rv-xy-plot-axis-font-size: 11px;
+$rv-xy-plot-tooltip-background: #3a3a48;
+$rv-xy-plot-tooltip-color: #fff;
+$rv-xy-plot-tooltip-font-size: 12px;
+$rv-xy-plot-tooltip-border-radius: 4px;
+$rv-xy-plot-tooltip-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+$rv-xy-plot-tooltip-padding: 7px 10px;
+
+.rv-xy-plot {
+ color: #c3c3c3;
+ position: relative;
+
+ canvas {
+ pointer-events: none;
+ }
+
+ .rv-xy-canvas {
+ pointer-events: none;
+ position: absolute;
+ }
+}
+
+.rv-xy-plot__inner {
+ display: block;
+}
+
+.rv-xy-plot__axis__line {
+ fill: none;
+ stroke-width: 2px;
+ stroke: $rv-xy-plot-axis-line-color;
+}
+
+.rv-xy-plot__axis__tick__line {
+ stroke: $rv-xy-plot-axis-line-color;
+}
+
+.rv-xy-plot__axis__tick__text {
+ fill: $rv-xy-plot-axis-font-color;
+ font-size: $rv-xy-plot-axis-font-size;
+}
+
+.rv-xy-plot__axis__title {
+ text {
+ fill: $rv-xy-plot-axis-font-color;
+ font-size: $rv-xy-plot-axis-font-size;
+ }
+}
+
+.rv-xy-plot__grid-lines__line {
+ stroke: $rv-xy-plot-axis-line-color;
+}
+
+.rv-xy-plot__circular-grid-lines__line {
+ fill-opacity: 0;
+ stroke: $rv-xy-plot-axis-line-color;
+}
+
+.rv-xy-plot__series,
+.rv-xy-plot__series path {
+ pointer-events: all;
+}
+
+.rv-xy-plot__series--line {
+ fill: none;
+ stroke: #000;
+ stroke-width: 2px;
+}
+
+.rv-crosshair {
+ position: absolute;
+ font-size: 11px;
+ pointer-events: none;
+}
+
+.rv-crosshair__line {
+ background: #47d3d9;
+ width: 1px;
+}
+
+.rv-crosshair__inner {
+ position: absolute;
+ text-align: left;
+ top: 0;
+}
+
+.rv-crosshair__inner__content {
+ border-radius: $rv-xy-plot-tooltip-border-radius;
+ background: $rv-xy-plot-tooltip-background;
+ color: $rv-xy-plot-tooltip-color;
+ font-size: $rv-xy-plot-tooltip-font-size;
+ padding: $rv-xy-plot-tooltip-padding;
+ box-shadow: $rv-xy-plot-tooltip-shadow;
+}
+
+.rv-crosshair__inner--left {
+ right: 4px;
+}
+
+.rv-crosshair__inner--right {
+ left: 4px;
+}
+
+.rv-crosshair__title {
+ font-weight: bold;
+ white-space: nowrap;
+}
+
+.rv-crosshair__item {
+ white-space: nowrap;
+}
+
+.rv-hint {
+ position: absolute;
+ pointer-events: none;
+}
+
+.rv-hint__content {
+ border-radius: $rv-xy-plot-tooltip-border-radius;
+ padding: $rv-xy-plot-tooltip-padding;
+ font-size: $rv-xy-plot-tooltip-font-size;
+ background: $rv-xy-plot-tooltip-background;
+ box-shadow: $rv-xy-plot-tooltip-shadow;
+ color: $rv-xy-plot-tooltip-color;
+ text-align: left;
+ white-space: nowrap;
+}
diff --git a/src/components/xy_chart/styles/react_vis/radial-chart.scss b/src/components/xy_chart/styles/react_vis/radial-chart.scss
new file mode 100644
index 00000000000..854a57d4b67
--- /dev/null
+++ b/src/components/xy_chart/styles/react_vis/radial-chart.scss
@@ -0,0 +1,6 @@
+.rv-radial-chart {
+
+ .rv-xy-plot__series--label {
+ pointer-events: none;
+ }
+}
diff --git a/src/components/xy_chart/styles/react_vis/treemap.scss b/src/components/xy_chart/styles/react_vis/treemap.scss
new file mode 100644
index 00000000000..4626d66ea79
--- /dev/null
+++ b/src/components/xy_chart/styles/react_vis/treemap.scss
@@ -0,0 +1,22 @@
+.rv-treemap {
+ font-size: 12px;
+ position: relative;
+}
+
+.rv-treemap__leaf {
+ overflow: hidden;
+ position: absolute;
+}
+
+.rv-treemap__leaf--circle {
+ align-items: center;
+ border-radius: 100%;
+ display: flex;
+ justify-content: center;
+}
+
+.rv-treemap__leaf__content {
+ overflow: hidden;
+ padding: 10px;
+ text-overflow: ellipsis;
+}
diff --git a/src/components/xy_chart/tooltip.js b/src/components/xy_chart/tooltip.js
new file mode 100644
index 00000000000..5414c83afdd
--- /dev/null
+++ b/src/components/xy_chart/tooltip.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import _ from 'lodash';
+import { Hint } from 'react-vis';
+import PropTypes from 'prop-types';
+// import {
+// colors,
+// unit,
+// units,
+// px,
+// borderRadius,
+// fontSize,
+// fontSizes
+// } from '../../variables';
+// import Legend from '../../Legend/Legend';
+
+// const TooltipElm = styled.div`
+// margin: 0 ${px(unit)};
+// transform: translateY(-50%);
+// border: 1px solid ${colors.gray4};
+// background: ${colors.white};
+// border-radius: ${borderRadius};
+// font-size: ${fontSize};
+// color: ${colors.black};
+// `;
+
+// const Header = styled.div`
+// background: ${colors.gray5};
+// border-bottom: 1px solid ${colors.gray4};
+// border-radius: ${borderRadius} ${borderRadius} 0 0;
+// padding: ${px(units.half)};
+// color: ${colors.gray3};
+// `;
+
+// const Legends = styled.div`
+// display: flex;
+// flex-direction: column;
+// padding: ${px(units.half)};
+// padding: ${px(units.quarter)} ${px(unit)} ${px(units.quarter)}
+// ${px(units.half)};
+// font-size: ${fontSizes.small};
+// `;
+
+// const LegendContainer = styled.div`
+// display: flex;
+// align-items: center;
+// margin: ${px(units.quarter)} 0;
+// justify-content: space-between;
+// `;
+
+// const LegendGray = styled(Legend)`
+// color: ${colors.gray3};
+// `;
+
+// const Value = styled.div`
+// color: ${colors.gray2};
+// font-size: ${fontSize};
+// `;
+
+//
+// {header || moment(x).format('MMMM Do YYYY, HH:mm')}
+//
+// {tooltipPoints.map((point, i) => (
+//
+//
+// {point.value}
+//
+// ))}
+//
+// ;
+
+export default function Tooltip({ tooltipPoints, x, y, ...props }) {
+ if (_.isEmpty(tooltipPoints)) {
+ return null;
+ }
+ return ;
+}
+
+Tooltip.propTypes = {
+ header: PropTypes.string,
+ tooltipPoints: PropTypes.array.isRequired,
+ x: PropTypes.number,
+ y: PropTypes.number
+};
+
+Tooltip.defaultProps = {};
diff --git a/src/components/xy_chart/utils/axis_utils.js b/src/components/xy_chart/utils/axis_utils.js
new file mode 100644
index 00000000000..abe375135bc
--- /dev/null
+++ b/src/components/xy_chart/utils/axis_utils.js
@@ -0,0 +1,28 @@
+import { AxisUtils } from 'react-vis';
+
+/**
+ * Axis orientation. Can be top, bottom, left, right.
+ * See react-vis AxisUtils.ORIENTATION for docs.
+ */
+export const ORIENTATION = {
+ TOP: AxisUtils.ORIENTATION.TOP,
+ LEFT: AxisUtils.ORIENTATION.LEFT,
+ RIGHT: AxisUtils.ORIENTATION.RIGHT,
+ BOTTOM: AxisUtils.ORIENTATION.BOTTOM,
+ HORIZONTAL: AxisUtils.ORIENTATION.HORIZONTAL,
+ VERTICAL: AxisUtils.ORIENTATION.VERTICAL,
+};
+
+/**
+ * The title position along the axis.
+ */
+export const TITLE_POSITION = {
+ MIDDLE: 'middle',
+ START: 'start',
+ END: 'end',
+};
+
+export const EuiXYChartAxisUtils = {
+ TITLE_POSITION,
+ ORIENTATION,
+};
diff --git a/src/components/xy_chart/utils/chart_utils.js b/src/components/xy_chart/utils/chart_utils.js
new file mode 100644
index 00000000000..95669496496
--- /dev/null
+++ b/src/components/xy_chart/utils/chart_utils.js
@@ -0,0 +1,63 @@
+
+/**
+ * Used to describe orientation.
+ */
+export const ORIENTATION = {
+ /** The main measure/value is along Y axis. Standard chart orientation. */
+ VERTICAL: 'vertical',
+ /** The main measure/value is along X axis. Rotated 90 deg. */
+ HORIZONTAL: 'horizontal',
+ /** Along both axis axis */
+ BOTH: 'both',
+};
+
+
+/**
+ * Type of scales used in charts.
+ */
+export const SCALE = {
+ /** Continuous scale, that works with numbers.
+ * Similar to [d3.scaleLinear](https://github.com/d3/d3-scale/blob/master/README.md#scaleLinear). */
+ LINEAR: 'linear',
+ /** Ordinal scale, works with numbers and strings.
+ * Similar to [d3.scaleOrdinal](https://github.com/d3/d3-scale/blob/master/README.md#ordinal-scales).*/
+ ORDINAL: 'ordinal',
+ /** Categorical scale, each new value gets the next value from the range.
+ * Similar to d3.scale.category\[Number\], but works with other values besides colors. */
+ CATEGORY: 'category',
+ /** Time scale. Similar to [d3.scaleTime](https://github.com/d3/d3-scale/blob/master/README.md#time-scales). */
+ TIME: 'time',
+ /** Time UTC scale. Similar to [d3.scaleUtc](https://github.com/d3/d3-scale/blob/master/README.md#scaleUtc).*/
+ TIME_UTC: 'time-utc',
+ /** Log scale. Similar to [d3.scaleLog](https://github.com/d3/d3-scale/blob/master/README.md#log-scales). */
+ LOG: 'log',
+ /** Returns exactly the value that was given to it.
+ * Similar to [d3.scaleIdentity](https://github.com/d3/d3-scale#scaleIdentity), except that it does NOT coerce data into numbers.
+ * This is useful for precisely specifying properties in the data, eg color can be specified directly on the data. */
+ LITERAL: 'literal'
+};
+
+
+/**
+ * Differnet types of curves that can be used on lines and areas series.
+ * See [d3-shape#curves](https://github.com/d3/d3-shape#curves)
+ */
+export const CURVE = {
+ LINEAR: 'linear',
+ CURVE_CARDINAL: 'curveCardinal',
+ CURVE_NATURAL: 'curveNatural',
+ CURVE_MONOTONE_X: 'curveMonotoneX',
+ CURVE_MONOTONE_Y: 'curveMonotoneY',
+ CURVE_BASIS: 'curveBasis',
+ CURVE_BUNDLE: 'curveBundle',
+ CURVE_CATMULL_ROM: 'curveCatmullRom',
+ CURVE_STEP: 'curveStep',
+ CURVE_STEP_AFTER: 'curveStepAfter',
+ CURVE_STEP_BEFORE: 'curveStepBefore',
+};
+
+export const EuiXYChartUtils = {
+ ORIENTATION,
+ SCALE,
+ CURVE,
+};
diff --git a/src/components/xy_chart/utils/index.js b/src/components/xy_chart/utils/index.js
new file mode 100644
index 00000000000..43f4f75eb29
--- /dev/null
+++ b/src/components/xy_chart/utils/index.js
@@ -0,0 +1,3 @@
+export { EuiXYChartUtils } from './chart_utils';
+export { EuiXYChartAxisUtils } from './axis_utils';
+export { EuiXYChartTextUtils } from './text_utils';
diff --git a/src/components/xy_chart/utils/series_utils.js b/src/components/xy_chart/utils/series_utils.js
new file mode 100644
index 00000000000..5e4d5b761e7
--- /dev/null
+++ b/src/components/xy_chart/utils/series_utils.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { AbstractSeries } from 'react-vis';
+
+/**
+ * Check if the component is series or not.
+ * @param {React.Component} child Component.
+ * @returns {boolean} True if the child is series, false otherwise.
+ */
+export function isSeriesChild(child) {
+ const { prototype } = child.type;
+ return prototype instanceof AbstractSeries;
+}
+
+/**
+ * Get all series from the 'children' object of the component.
+ * @param {Object} children Children.
+ * @returns {Array} Array of children.
+ */
+export function getSeriesChildren(children) {
+ return React.Children.toArray(children).filter(child =>
+ child && isSeriesChild(child));
+}
+
+export function rotateDataSeries(data) {
+ return data.map(d => {
+ return {
+ x: d.y,
+ y: d.x,
+ x0: d.y0,
+ y0: d.x0,
+ };
+ });
+}
diff --git a/src/components/xy_chart/utils/text_utils.js b/src/components/xy_chart/utils/text_utils.js
new file mode 100644
index 00000000000..bd2de7cbc08
--- /dev/null
+++ b/src/components/xy_chart/utils/text_utils.js
@@ -0,0 +1,34 @@
+import React, { Fragment } from 'react';
+
+/**
+ * Word wrapper that takes a long text and wrap words into lines of the same length.
+ * and return a SVG component composed by tspan tags.
+ * source: https://j11y.io/snippets/wordwrap-for-javascript/
+ * @param {Array of Strings} texts - an array of splitted text, one per line
+ * @return {Object} Return a Fragment of SVG tspan elements to be used inside axis label formatter.
+ */
+function labelWordWrap(text, width) {
+ const pieces = wordWrap(text, width);
+ return (
+
+ {pieces.map((piece, i) => {
+ return (
+
+ {piece}
+
+ );
+ })}
+
+ );
+}
+
+function wordWrap(text, width = 75, cut = false) {
+ if (!text) {
+ return text;
+ }
+ const regex = `.{1,${ width }}(s|$)${ cut ? `|.{${ width }}|.+$` : '|S+?(s|$)'}`;
+ return text.match(RegExp(regex, 'g'));
+}
+export const EuiXYChartTextUtils = {
+ labelWordWrap,
+};
diff --git a/src/components/xy_chart/utils/visualization_color_type.js b/src/components/xy_chart/utils/visualization_color_type.js
new file mode 100644
index 00000000000..2ce053adc33
--- /dev/null
+++ b/src/components/xy_chart/utils/visualization_color_type.js
@@ -0,0 +1,16 @@
+import { VISUALIZATION_COLORS } from '../../../services';
+
+export function VisualizationColorType(props, propName) {
+ const color = props[propName];
+ if (color === undefined) {
+ return;
+ }
+ // TODO upgrade this to check all possible color string formats
+ // using libs like colorjs
+ if (!(typeof color === 'string' || color instanceof String) || !color.startsWith('#')) {
+ return new Error('Color must be a valid hex color string in the form #RRGGBB');
+ }
+ if (!VISUALIZATION_COLORS.includes(color.toUpperCase())) {
+ console.warn('Prefer safe EUI Visualization Colors.');
+ }
+}
diff --git a/src/components/xy_chart/xy_chart.js b/src/components/xy_chart/xy_chart.js
new file mode 100644
index 00000000000..a594f38bde2
--- /dev/null
+++ b/src/components/xy_chart/xy_chart.js
@@ -0,0 +1,322 @@
+import React, { PureComponent, Fragment } from 'react';
+import { XYPlot, AbstractSeries, makeVisFlexible } from 'react-vis';
+
+import PropTypes from 'prop-types';
+import { EuiEmptyPrompt } from '../empty_prompt';
+import { EuiSelectionBrush } from './selection_brush';
+import { EuiDefaultAxis } from './axis/default_axis';
+import { EuiCrosshairX } from './crosshairs/crosshair_x';
+import { EuiCrosshairY } from './crosshairs/crosshair_y';
+import { VISUALIZATION_COLORS } from '../../services';
+import { getSeriesChildren } from './utils/series_utils';
+import { ORIENTATION, SCALE } from './utils/chart_utils';
+const { HORIZONTAL, VERTICAL, BOTH } = ORIENTATION;
+const { LINEAR, ORDINAL, CATEGORY, TIME, TIME_UTC, LOG, LITERAL } = SCALE;
+
+const DEFAULT_MARGINS = {
+ left: 40,
+ right: 10,
+ top: 10,
+ bottom: 40
+};
+
+/**
+ * The extended version of the react-vis XYPlot with the mouseLeave and mouseUp handlers.
+ * TODO: send a PR to react-vis for incorporate these two changes directly into XYPlot class.
+ */
+class XYExtendedPlot extends XYPlot {
+ /**
+ * Trigger onMouseLeave handler if it was passed in props.
+ * @param {Event} event Native event.
+ * @private
+ */
+ _mouseLeaveHandler(event) {
+ const { onMouseLeave, children } = this.props;
+ if (onMouseLeave) {
+ super.onMouseLeave(event);
+ }
+ const seriesChildren = getSeriesChildren(children);
+ seriesChildren.forEach((child, index) => {
+ const component = this[`series${index}`];
+ if (component && component.onParentMouseLeave) {
+ component.onParentMouseLeave(event);
+ }
+ });
+ }
+
+ /**
+ * Trigger onMouseUp handler if it was passed in props.
+ * @param {Event} event Native event.
+ * @private
+ */
+ _mouseUpHandler = (event) => {
+ const { onMouseUp, children } = this.props;
+ if (onMouseUp) {
+ super.onMouseUp(event);
+ }
+ const seriesChildren = getSeriesChildren(children);
+ seriesChildren.forEach((child, index) => {
+ const component = this[`series${index}`];
+ if (component && component.onParentMouseUp) {
+ component.onParentMouseUp(event);
+ }
+ });
+ }
+
+ render() {
+ const {
+ className,
+ dontCheckIfEmpty,
+ style,
+ width,
+ height,
+ } = this.props;
+
+ if (!dontCheckIfEmpty && this._isPlotEmpty()) {
+ return (
+
+ );
+ }
+ const components = this._getClonedChildComponents();
+
+ return (
+
+
+ {components.filter(c => c && c.type.requiresSVG)}
+
+ {this.renderCanvasComponents(components, this.props)}
+ {components.filter(c => c && !c.type.requiresSVG && !c.type.isCanvas)}
+
+ );
+ }
+}
+
+
+/**
+ * The root component of any XY chart.
+ * It renders an react-vis XYPlot including default axis and a valid crosshair.
+ * You can also enable the Selection Brush.
+ */
+class XYChart extends PureComponent {
+ state = {
+ mouseOver: false,
+ };
+ 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;
+ }
+
+ /**
+ * Checks if a react child is an AbstractSeries
+ */
+ _isAbstractSeries(child) {
+ const { prototype } = child.type;
+ // Avoid applying chart props to non series children
+ 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) => {
+ // Avoid applying color props to non series children
+ if (!this._isAbstractSeries(child)) {
+ return child;
+ }
+
+ const props = {
+ id: `chart-${i}`,
+ };
+ if (!child.props.color) {
+ props.color = VISUALIZATION_COLORS[colorIterator % VISUALIZATION_COLORS.length];
+ colorIterator++;
+ }
+ props._orientation = this.props.orientation;
+
+ return React.cloneElement(child, props);
+ });
+ }
+ _getSeriesNames = (children) => {
+ return React.Children.toArray(children)
+ .filter(this._isAbstractSeries)
+ .map(({ props: { name } }) => (name));
+ }
+
+ render() {
+ const {
+ children,
+ width,
+ height,
+ xType,
+ yType,
+ stackBy,
+ statusText,
+ xDomain,
+ yDomain,
+ yPadding,
+ xPadding,
+ animateData,
+ showDefaultAxis,
+ showCrosshair,
+ enableSelectionBrush,
+ selectionBrushOrientation,
+ onSelectionBrushEnd,
+ orientation,
+ crosshairValue,
+ onCrosshairUpdate,
+ ...rest
+ } = this.props;
+
+ if (this._isEmptyPlot(children)) {
+ return (
+ Chart not available}
+ body={
+
+ { statusText }
+
+ }
+ />
+ );
+ }
+
+ const Crosshair = orientation === HORIZONTAL ? EuiCrosshairY : EuiCrosshairX;
+ const seriesNames = this._getSeriesNames(children);
+ return (
+
+
+ {this._renderChildren(children)}
+ {showDefaultAxis && }
+ {showCrosshair && (
+
+ )}
+
+ {enableSelectionBrush && (
+
+ )}
+
+
+ );
+ }
+}
+XYChart.displayName = 'EuiXYChart';
+
+XYChart.propTypes = {
+ /** The initial width of the chart. */
+ width: PropTypes.number.isRequired,
+ /** The initial height of the chart. */
+ height: PropTypes.number.isRequired,
+ /** **experimental** The orientation of the chart. */
+ orientation: PropTypes.oneOf([HORIZONTAL, VERTICAL]),
+ /** If the chart animates on data changes. */
+ animateData: PropTypes.bool,
+ /** TODO */
+ 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 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. */
+ xDomain: PropTypes.array,
+ /** Manually specify the domain of y axis. */
+ yDomain: PropTypes.array,
+ /** The horizontal padding between the chart borders and chart elements. */
+ xPadding: PropTypes.number,
+ /** The vertical padding between the chart borders and chart elements. */
+ yPadding: PropTypes.number,
+ /** Add an additional status text above the graph status message*/
+ statusText: PropTypes.string,
+ /** Shows the crosshair tooltip on mouse move.*/
+ showCrosshair: PropTypes.bool,
+ /** Specify the axis value where to display crosshair based on chart orientation value. */
+ crosshairValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ /** Callback when the crosshair position is updated. */
+ onCrosshairUpdate: PropTypes.func,
+ /** Show the default X and Y axis. */
+ showDefaultAxis: PropTypes.bool,
+ /** Enable the brush tool */
+ enableSelectionBrush: PropTypes.bool,
+ /** Specify the brush orientation */
+ selectionBrushOrientation: PropTypes.oneOf([HORIZONTAL, VERTICAL, BOTH]),
+ /** Callback on brush end event with { begin, end } object returned. */
+ onSelectionBrushEnd: PropTypes.func,
+};
+
+XYChart.defaultProps = {
+ animateData: true,
+ xType: 'linear',
+ yType: 'linear',
+ yPadding: 0,
+ xPadding: 0,
+ orientation: VERTICAL,
+ showCrosshair: true,
+ showDefaultAxis: true,
+ enableSelectionBrush: false,
+ selectionBrushOrientation: HORIZONTAL,
+};
+
+export const EuiXYChart = makeVisFlexible(XYChart);
diff --git a/src/components/xy_chart/xy_chart.test.js b/src/components/xy_chart/xy_chart.test.js
new file mode 100644
index 00000000000..2dbbfba3d75
--- /dev/null
+++ b/src/components/xy_chart/xy_chart.test.js
@@ -0,0 +1,137 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { EuiXYChart } from './xy_chart';
+import { EuiLineSeries } from './series/line_series';
+import { EuiDefaultAxis } from './axis';
+import { EuiCrosshairX, EuiCrosshairY } from './crosshairs/';
+import { requiredProps } from '../../test/required_props';
+import { VISUALIZATION_COLORS } from '../../services';
+
+const NOOP = f => f;
+
+export const XYCHART_PROPS = {
+ width: 1,
+ height: 1,
+ orientation: 'vertical',
+ animateData: true,
+ stackBy: 'y',
+ xType: 'linear',
+ yType: 'linear',
+ xDomain: [0, 1],
+ yDomain: [0, 1],
+ xPadding: 0,
+ yPadding: 0,
+ statusText: '',
+ showCrosshair: true,
+ crosshairValue: 0,
+ onCrosshairUpdate: NOOP,
+ showDefaultAxis: true,
+ enableSelectionBrush: true,
+ selectionBrushOrientation: 'vertical',
+ onSelectionBrushEnd: NOOP,
+};
+
+describe('EuiXYChart', () => {
+
+ test(`renders all props`, () => {
+ const wrapper = mount( );
+ const wrapperProps = wrapper.props();
+
+ expect(wrapper.find(EuiXYChart).length).toBe(1);
+ Object.keys(XYCHART_PROPS).forEach(propName => {
+ expect(wrapperProps[propName]).toBe(XYCHART_PROPS[propName]);
+ });
+ });
+
+ test('renders an empty chart', () => {
+ const EMPTY_CHART_MESSAGE = '~~Empty Chart~~';
+ const component = mount(
+
+ );
+
+ expect(component.render().find('.euiText').text()).toBe(EMPTY_CHART_MESSAGE);
+ expect(component.find(EuiDefaultAxis)).toHaveLength(0);
+ expect(component.find(EuiCrosshairX)).toHaveLength(0);
+ expect(component.find(EuiCrosshairY)).toHaveLength(0);
+ expect(component.render()).toMatchSnapshot();
+ });
+
+ test('renders right default colors', () => {
+ const data = [ { x: 0, y: 1 }, { x: 1, y: 2 }];
+ const series = new Array(VISUALIZATION_COLORS.length * 2)
+ .fill(0)
+ .map((color, i) => {
+ return { name: `series-${i}`, data };
+ });
+ const component = mount(
+
+ {
+ series.map((d, i) => ( ))
+ }
+
+ );
+
+ expect(component.find(EuiLineSeries)).toHaveLength(VISUALIZATION_COLORS.length * 2);
+ VISUALIZATION_COLORS.forEach((color, i) => {
+ expect(component.find(EuiLineSeries).at(i).props().color).toBe(color);
+ expect(component.find(EuiLineSeries).at(i + VISUALIZATION_COLORS.length).props().color).toBe(color);
+ });
+ });
+
+ test('renders default colors together with existing series colors', () => {
+ const data = [ { x: 0, y: 1 }, { x: 1, y: 2 }];
+ const AVAILABLE_COLORS = VISUALIZATION_COLORS.length;
+ const series = new Array(AVAILABLE_COLORS * 2)
+ .fill(0)
+ .map((color, i) => {
+ return { name: `series-${i}`, data };
+ });
+ series.splice(1, 0, { name: `series-colored`, data, color: VISUALIZATION_COLORS[5] });
+
+ const component = mount(
+
+ {
+ series.map((d, i) => ( ))
+ }
+
+ );
+ const lineComponents = component.find(EuiLineSeries);
+ expect(lineComponents).toHaveLength(AVAILABLE_COLORS * 2 + 1);
+ // check before
+ expect(lineComponents.at(0).props().color).toBe(VISUALIZATION_COLORS[0]);
+ // check if the inserted element maintain its own color
+ expect(lineComponents.at(1).props().color).toBe(VISUALIZATION_COLORS[5]);
+ // check if the skip maintain the color assignment
+ expect(lineComponents.at(2).props().color).toBe(VISUALIZATION_COLORS[1]);
+ expect(lineComponents.at(AVAILABLE_COLORS + 1).props().color).toBe(VISUALIZATION_COLORS[0]);
+ });
+
+ test(`Check wrong EUI color warning`, () => {
+ const data = [ { x: 0, y: 1 }, { x: 1, y: 2 }];
+ const original = console.warn;
+ const mock = jest.fn();
+ console.warn = mock;
+ mount(
+
+
+ );
+ expect(console.warn.mock.calls[0][0]).toBe('Prefer safe EUI Visualization Colors.');
+ console.warn.mockClear();
+ console.warn = original;
+
+ });
+});
diff --git a/src/services/color/index.js b/src/services/color/index.js
index 750249aabcb..0c4908fe99b 100644
--- a/src/services/color/index.js
+++ b/src/services/color/index.js
@@ -2,4 +2,4 @@ export { isColorDark } from './is_color_dark';
export { hexToRgb } from './hex_to_rgb';
export { rgbToHex } from './rgb_to_hex';
export { calculateContrast, calculateLuminance } from './luminance_and_contrast';
-export { VISUALIZATION_COLORS } from './visualization_colors';
+export { VISUALIZATION_COLORS, DEFAULT_VISUALIZATION_COLOR } from './visualization_colors';
diff --git a/src/services/color/visualization_colors.js b/src/services/color/visualization_colors.js
index b628f4b7c22..0ca8ab243ce 100644
--- a/src/services/color/visualization_colors.js
+++ b/src/services/color/visualization_colors.js
@@ -14,3 +14,5 @@ export const VISUALIZATION_COLORS = [
'#461A0A',
'#920000',
];
+
+export const DEFAULT_VISUALIZATION_COLOR = VISUALIZATION_COLORS[1];
diff --git a/src/services/index.js b/src/services/index.js
index eb52457c528..d44ee26f8ad 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -22,6 +22,7 @@ export {
hexToRgb,
rgbToHex,
VISUALIZATION_COLORS,
+ DEFAULT_VISUALIZATION_COLOR,
} from './color';
export {
diff --git a/src/test/patch_random.js b/src/test/patch_random.js
new file mode 100644
index 00000000000..de18b8023de
--- /dev/null
+++ b/src/test/patch_random.js
@@ -0,0 +1,10 @@
+const _mathRandom = Math.random;
+
+export function patchRandom() {
+ let x = 0;
+ Math.random = () => x += 0.00001;
+}
+
+export function unpatchRandom() {
+ Math.random = _mathRandom;
+}
diff --git a/src/test/time_execution.js b/src/test/time_execution.js
new file mode 100644
index 00000000000..582e03d3997
--- /dev/null
+++ b/src/test/time_execution.js
@@ -0,0 +1,21 @@
+export function timeExecution(fn) {
+ const start = process.hrtime();
+ fn();
+ const [seconds, nanoseconds] = process.hrtime(start);
+ const milliseconds = (seconds * 1000) + (nanoseconds / 1000000);
+ return milliseconds;
+}
+
+export function benchmarkFunction(fn, warmupRuns = 3, benchmarkRuns = 3) {
+ // warmup v8 optimizations, cache, etc
+ for (let i = 0; i < warmupRuns; i++) {
+ fn();
+ }
+
+ const runTimes = [];
+ for (let i = 0; i < benchmarkRuns; i++) {
+ runTimes.push(timeExecution(fn));
+ }
+
+ return Math.min.apply(null, runTimes);
+}
diff --git a/yarn.lock b/yarn.lock
index a071a18366f..1f432462fda 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -125,6 +125,10 @@ acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822"
+address@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
+
adm-zip@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.2.1.tgz#e801cedeb5bd9a4e98d699c5c0f4239e2731dcbf"
@@ -265,6 +269,10 @@ aproba@^1.0.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+arch@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e"
+
archiver-utils@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-1.3.0.tgz#e50b4c09c70bf3d680e32ff1b7994e9f9d895174"
@@ -296,12 +304,25 @@ are-we-there-yet@~1.1.2:
delegates "^1.0.0"
readable-stream "^2.0.6"
+arg@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arg/-/arg-2.0.0.tgz#c06e7ff69ab05b3a4a03ebe0407fac4cba657545"
+
argparse@^1.0.7:
version "1.0.9"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
dependencies:
sprintf-js "~1.0.2"
+args@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/args/-/args-4.0.0.tgz#5ca24cdba43d4b17111c56616f5f2e9d91933954"
+ dependencies:
+ camelcase "5.0.0"
+ chalk "2.3.2"
+ leven "2.1.0"
+ mri "1.1.0"
+
aria-query@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-0.7.0.tgz#4af10a1e61573ddea0cf3b99b51c52c05b424d24"
@@ -1221,6 +1242,12 @@ base@^0.11.1:
mixin-deep "^1.2.0"
pascalcase "^0.1.1"
+basic-auth@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.0.tgz#015db3f353e02e56377755f962742e8981e7bbba"
+ dependencies:
+ safe-buffer "5.1.1"
+
batch@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
@@ -1339,7 +1366,7 @@ boom@5.x.x:
dependencies:
hoek "4.x.x"
-boxen@^1.2.1:
+boxen@1.3.0, boxen@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
dependencies:
@@ -1554,6 +1581,10 @@ camelcase-keys@^2.0.0:
camelcase "^2.0.0"
map-obj "^1.0.0"
+camelcase@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
+
camelcase@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
@@ -1621,6 +1652,30 @@ chai@^4.1.2:
pathval "^1.0.0"
type-detect "^4.0.0"
+chalk@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chalk@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52"
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chalk@2.4.1, chalk@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -1639,14 +1694,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
-chalk@^2.4.1:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
- dependencies:
- ansi-styles "^3.2.1"
- escape-string-regexp "^1.0.5"
- supports-color "^5.3.0"
-
chardet@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
@@ -1802,6 +1849,13 @@ cli-width@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
+clipboardy@1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-1.2.3.tgz#0526361bf78724c1f20be248d428e365433c07ef"
+ dependencies:
+ arch "^2.1.0"
+ execa "^0.8.0"
+
cliui@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
@@ -1984,6 +2038,12 @@ compressible@~2.0.11:
dependencies:
mime-db ">= 1.30.0 < 2"
+compressible@~2.0.13:
+ version "2.0.14"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.14.tgz#326c5f507fbb055f54116782b969a81b67a29da7"
+ dependencies:
+ mime-db ">= 1.34.0 < 2"
+
compression@^1.5.2:
version "1.7.1"
resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.1.tgz#eff2603efc2e22cf86f35d2eb93589f9875373db"
@@ -1996,6 +2056,18 @@ compression@^1.5.2:
safe-buffer "5.1.1"
vary "~1.1.2"
+compression@^1.6.2:
+ version "1.7.2"
+ resolved "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz#aaffbcd6aaf854b44ebb280353d5ad1651f59a69"
+ dependencies:
+ accepts "~1.3.4"
+ bytes "3.0.0"
+ compressible "~2.0.13"
+ debug "2.6.9"
+ on-headers "~1.0.1"
+ safe-buffer "5.1.1"
+ vary "~1.1.2"
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -2069,7 +2141,7 @@ content-type-parser@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7"
-content-type@~1.0.4:
+content-type@1.0.4, content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
@@ -2367,6 +2439,88 @@ cycle@1.0.x:
version "1.0.3"
resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
+d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
+
+d3-collection@1, d3-collection@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
+
+d3-color@1, d3-color@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.0.tgz#d1ea19db5859c86854586276ec892cf93148459a"
+
+d3-contour@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.0.tgz#cfb99098c48c46edd77e15ce123162f9e333e846"
+ dependencies:
+ d3-array "^1.1.1"
+
+d3-format@1, d3-format@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.0.tgz#a3ac44269a2011cdb87c7b5693040c18cddfff11"
+
+d3-geo@^1.6.4:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.10.0.tgz#2972d18014f1e38fc1f8bb6d545377bdfb00c9ab"
+ dependencies:
+ d3-array "1"
+
+d3-hierarchy@^1.1.4:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.6.tgz#842c1372090f870b7ea013ebae5c0c8d9f56229c"
+
+d3-interpolate@1, d3-interpolate@^1.1.4:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.2.0.tgz#40d81bd8e959ff021c5ea7545bc79b8d22331c41"
+ dependencies:
+ d3-color "1"
+
+d3-path@1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
+
+d3-sankey@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/d3-sankey/-/d3-sankey-0.7.1.tgz#d229832268fc69a7fec84803e96c2256a614c521"
+ dependencies:
+ d3-array "1"
+ d3-collection "1"
+ d3-shape "^1.2.0"
+
+d3-scale@^1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-color "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
+d3-shape@^1.1.0, d3-shape@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
+ dependencies:
+ d3-path "1"
+
+d3-time-format@2:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
+ dependencies:
+ d3-time "1"
+
+d3-time@1:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
+
+d3-voronoi@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c"
+
d@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
@@ -2377,7 +2531,7 @@ damerau-levenshtein@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"
-dargs@^5.1.0:
+dargs@5.1.0, dargs@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829"
@@ -2395,7 +2549,7 @@ dateformat@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.2.tgz#9a4df4bff158ac2f34bc637abdb15471607e1659"
-debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
+debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
@@ -2515,7 +2669,7 @@ depd@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
-depd@~1.1.1:
+depd@~1.1.1, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@@ -2556,6 +2710,13 @@ detect-node@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127"
+detect-port@1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.2.3.tgz#15bf49820d02deb84bfee0a74876b32d791bf610"
+ dependencies:
+ address "^1.0.1"
+ debug "^2.6.0"
+
diff@3.5.0, diff@^3.1.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
@@ -2757,7 +2918,7 @@ emojis-list@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
-encodeurl@~1.0.1:
+encodeurl@~1.0.1, encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -2908,10 +3069,6 @@ es6-promise@^3.0.2:
version "3.3.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
-es6-promise@^4.0.3:
- version "4.2.4"
- resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
-
es6-set@~0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
@@ -3260,6 +3417,18 @@ execa@^0.7.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
+execa@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da"
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
execall@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/execall/-/execall-1.0.0.tgz#73d0904e395b3cab0658b08d09ec25307f29bb73"
@@ -3523,6 +3692,10 @@ fileset@^2.0.2:
glob "^7.0.3"
minimatch "^3.0.3"
+filesize@3.6.1:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
+
fill-range@^2.1.0:
version "2.2.3"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
@@ -3695,6 +3868,14 @@ from@~0:
version "0.1.7"
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
+fs-extra@6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-6.0.1.tgz#8abc128f7946e310135ddc93b98bddb410e7a34b"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^4.0.0"
+ universalify "^0.1.0"
+
fs-extra@^0.30.0:
version "0.30.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0"
@@ -3705,14 +3886,6 @@ fs-extra@^0.30.0:
path-is-absolute "^1.0.0"
rimraf "^2.2.8"
-fs-extra@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950"
- dependencies:
- graceful-fs "^4.1.2"
- jsonfile "^2.1.0"
- klaw "^1.0.0"
-
fs-extra@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
@@ -3945,7 +4118,7 @@ global-dirs@^0.1.0:
dependencies:
ini "^1.3.4"
-global@~4.3.0:
+global@^4.3.1, global@~4.3.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
dependencies:
@@ -4106,7 +4279,7 @@ handle-thing@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
-handlebars@^4.0.3:
+handlebars@4.0.11, handlebars@^4.0.3:
version "4.0.11"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
dependencies:
@@ -4236,13 +4409,6 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.0"
-hasha@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1"
- dependencies:
- is-stream "^1.0.1"
- pinkie-promise "^2.0.0"
-
hawk@3.1.3, hawk@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
@@ -4290,6 +4456,10 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+hoek@4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
+
hoek@4.x.x:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
@@ -4674,7 +4844,7 @@ ip-regex@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd"
-ip@^1.1.0, ip@^1.1.5:
+ip@1.1.5, ip@^1.1.0, ip@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
@@ -4979,7 +5149,7 @@ is-scoped@^1.0.0:
dependencies:
scoped-regex "^1.0.0"
-is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
+is-stream@1.1.0, is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@@ -5675,7 +5845,7 @@ left-pad@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.2.0.tgz#d30a73c6b8201d8f7d8e7956ba9616087a68e0ee"
-leven@^2.1.0:
+leven@2.1.0, leven@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
@@ -6087,6 +6257,22 @@ methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+micro-compress@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/micro-compress/-/micro-compress-1.0.0.tgz#53f5a80b4ad0320ca165a559b6e3df145d4f704f"
+ dependencies:
+ compression "^1.6.2"
+
+micro@9.3.1:
+ version "9.3.1"
+ resolved "https://registry.yarnpkg.com/micro/-/micro-9.3.1.tgz#0c37eba0171554b1beccda5215ff8ea4e7aa59d6"
+ dependencies:
+ arg "2.0.0"
+ chalk "2.4.0"
+ content-type "1.0.4"
+ is-stream "1.1.0"
+ raw-body "2.3.2"
+
micromatch@^2.1.5, micromatch@^2.3.11:
version "2.3.11"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
@@ -6134,10 +6320,24 @@ miller-rabin@^4.0.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.32.0.tgz#485b3848b01a3cda5f968b4882c0771e58e09414"
+"mime-db@>= 1.34.0 < 2":
+ version "1.34.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.34.0.tgz#452d0ecff5c30346a6dc1e64b1eaee0d3719ff9a"
+
mime-db@~1.30.0:
version "1.30.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
+mime-db@~1.33.0:
+ version "1.33.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
+
+mime-types@2.1.18:
+ version "2.1.18"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
+ dependencies:
+ mime-db "~1.33.0"
+
mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7:
version "2.1.17"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
@@ -6266,6 +6466,10 @@ moment@^2.20.1:
version "2.20.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
+mri@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.0.tgz#5c0a3f29c8ccffbbb1ec941dcec09d71fa32f36a"
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -6498,6 +6702,10 @@ node-status-codes@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f"
+node-version@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/node-version/-/node-version-1.1.3.tgz#1081c87cce6d2dbbd61d0e51e28c287782678496"
+
nodeclient-spectre@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/nodeclient-spectre/-/nodeclient-spectre-1.0.3.tgz#831ce6151cc897174843f9289db76002260aa1b3"
@@ -6797,6 +7005,16 @@ onetime@^2.0.0:
dependencies:
mimic-fn "^1.0.0"
+openssl-self-signed-certificate@1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/openssl-self-signed-certificate/-/openssl-self-signed-certificate-1.1.6.tgz#9d3a4776b1a57e9847350392114ad2f915a83dd4"
+
+opn@5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c"
+ dependencies:
+ is-wsl "^1.1.0"
+
opn@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95"
@@ -7096,7 +7314,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
-path-is-inside@^1.0.1, path-is-inside@^1.0.2:
+path-is-inside@1.0.2, path-is-inside@^1.0.1, path-is-inside@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
@@ -7122,6 +7340,12 @@ path-to-regexp@^1.7.0:
dependencies:
isarray "0.0.1"
+path-type@3.0.0, path-type@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+ dependencies:
+ pify "^3.0.0"
+
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -7136,12 +7360,6 @@ path-type@^2.0.0:
dependencies:
pify "^2.0.0"
-path-type@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
- dependencies:
- pify "^3.0.0"
-
pathval@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
@@ -7178,20 +7396,6 @@ performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
-phantomjs-prebuilt@^2.1.16:
- version "2.1.16"
- resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef"
- dependencies:
- es6-promise "^4.0.3"
- extract-zip "^1.6.5"
- fs-extra "^1.0.0"
- hasha "^2.2.0"
- kew "^0.7.0"
- progress "^1.1.8"
- request "^2.81.0"
- request-progress "^2.0.1"
- which "^1.2.10"
-
pify@^2.0.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -7657,10 +7861,6 @@ progress@2.0.0, progress@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
-progress@^1.1.8:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
-
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@@ -7797,7 +7997,7 @@ querystringify@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb"
-raf@^3.4.0:
+raf@^3.1.0, raf@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
dependencies:
@@ -7919,6 +8119,14 @@ react-input-autosize@^2.2.1:
dependencies:
prop-types "^15.5.8"
+react-motion@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
+ dependencies:
+ performance-now "^0.2.0"
+ prop-types "^15.5.8"
+ raf "^3.1.0"
+
react-onclickoutside@^6.7.1:
version "6.7.1"
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz#6a5b5b8b4eae6b776259712c89c8a2b36b17be93"
@@ -7984,6 +8192,28 @@ react-virtualized@^9.18.5:
loose-envify "^1.3.0"
prop-types "^15.6.0"
+react-vis@^1.10.1:
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/react-vis/-/react-vis-1.10.1.tgz#e9b49474001186666b4094cfa8b0395f74b22df6"
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "^1.0.3"
+ d3-color "^1.0.3"
+ d3-contour "^1.1.0"
+ d3-format "^1.2.0"
+ d3-geo "^1.6.4"
+ d3-hierarchy "^1.1.4"
+ d3-interpolate "^1.1.4"
+ d3-sankey "^0.7.1"
+ d3-scale "^1.0.5"
+ d3-shape "^1.1.0"
+ d3-voronoi "^1.1.2"
+ deep-equal "^1.0.1"
+ global "^4.3.1"
+ hoek "4.2.1"
+ prop-types "^15.5.8"
+ react-motion "^0.5.2"
+
react@^16.3.0:
version "16.3.2"
resolved "https://registry.yarnpkg.com/react/-/react-16.3.2.tgz#fdc8420398533a1e58872f59091b272ce2f91ea9"
@@ -8237,6 +8467,13 @@ regexpu-core@^2.0.0:
regjsgen "^0.2.0"
regjsparser "^0.1.4"
+registry-auth-token@3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20"
+ dependencies:
+ rc "^1.1.6"
+ safe-buffer "^5.0.1"
+
registry-auth-token@^3.0.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.1.tgz#fb0d3289ee0d9ada2cbb52af5dfe66cb070d3006"
@@ -8244,7 +8481,7 @@ registry-auth-token@^3.0.1:
rc "^1.1.6"
safe-buffer "^5.0.1"
-registry-url@^3.0.3:
+registry-url@3.1.0, registry-url@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
dependencies:
@@ -8300,12 +8537,6 @@ replace-ext@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
-request-progress@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08"
- dependencies:
- throttleit "^1.0.0"
-
request-promise-core@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6"
@@ -8774,6 +9005,24 @@ send@0.16.1:
range-parser "~1.2.0"
statuses "~1.3.1"
+send@0.16.2:
+ version "0.16.2"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
+ dependencies:
+ debug "2.6.9"
+ depd "~1.1.2"
+ destroy "~1.0.4"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "~1.6.2"
+ mime "1.4.1"
+ ms "2.0.0"
+ on-finished "~2.3.0"
+ range-parser "~1.2.0"
+ statuses "~1.4.0"
+
serializerr@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/serializerr/-/serializerr-1.0.3.tgz#12d4c5aa1c3ffb8f6d1dc5f395aa9455569c3f91"
@@ -8801,6 +9050,33 @@ serve-static@1.13.1:
parseurl "~1.3.2"
send "0.16.1"
+serve@^6.3.1:
+ version "6.5.8"
+ resolved "https://registry.yarnpkg.com/serve/-/serve-6.5.8.tgz#fd7ad6b9c10ba12084053030cc1a8b636c0a10a7"
+ dependencies:
+ args "4.0.0"
+ basic-auth "2.0.0"
+ bluebird "3.5.1"
+ boxen "1.3.0"
+ chalk "2.4.1"
+ clipboardy "1.2.3"
+ dargs "5.1.0"
+ detect-port "1.2.3"
+ filesize "3.6.1"
+ fs-extra "6.0.1"
+ handlebars "4.0.11"
+ ip "1.1.5"
+ micro "9.3.1"
+ micro-compress "1.0.0"
+ mime-types "2.1.18"
+ node-version "1.1.3"
+ openssl-self-signed-certificate "1.1.6"
+ opn "5.3.0"
+ path-is-inside "1.0.2"
+ path-type "3.0.0"
+ send "0.16.2"
+ update-check "1.5.1"
+
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@@ -9155,7 +9431,7 @@ static-extend@^0.1.1:
define-property "^0.2.5"
object-copy "^0.1.0"
-"statuses@>= 1.3.1 < 2":
+"statuses@>= 1.3.1 < 2", statuses@~1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
@@ -9500,10 +9776,6 @@ throat@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
-throttleit@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
-
through2@^2.0.0, through2@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
@@ -9792,6 +10064,13 @@ unzip-response@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
+update-check@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.5.1.tgz#24fc52266273cb8684d2f1bf9687c0e52dcf709f"
+ dependencies:
+ registry-auth-token "3.3.2"
+ registry-url "3.1.0"
+
update-notifier@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.3.0.tgz#4e8827a6bb915140ab093559d7014e3ebb837451"
@@ -10128,7 +10407,6 @@ wdio-visual-regression-service@silne30/wdio-visual-regression-service#Add_Filena
lodash "^4.13.1"
node-resemble-js "0.0.5"
nodeclient-spectre "^1.0.3"
- phantomjs-prebuilt "^2.1.16"
platform "^1.3.1"
wdio-screenshot "^0.6.0"