diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js
index 6263d220b6b4..986c99fcf3fc 100644
--- a/src-docs/src/routes.js
+++ b/src-docs/src/routes.js
@@ -207,8 +207,20 @@ import { ToolTipExample }
import { XYChartExample }
from './views/xy_chart/xy_chart_example';
-import { XYChartSeriesExample }
- from './views/xy_chart_series/series_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';
@@ -326,12 +338,6 @@ const navigation = [{
ToastExample,
ToolTipExample,
].map(example => createExample(example)),
-}, {
- name: 'Charts',
- items: [
- XYChartExample,
- XYChartSeriesExample,
- ].map(example => createExample(example)),
}, {
name: 'Forms',
items: [
@@ -346,7 +352,19 @@ const navigation = [{
FilterGroupExample,
SearchBarExample,
].map(example => createExample(example)),
-}, {
+},
+{
+ name: 'XY Charts',
+ 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 000000000000..6bac4133a62d
--- /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 000000000000..445b7bf7bf0c
--- /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 000000000000..e9bde5dc9f8f
--- /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/example-auto-axis.js b/src-docs/src/views/xy_chart/example-auto-axis.js
deleted file mode 100644
index db95d1d216bc..000000000000
--- a/src-docs/src/views/xy_chart/example-auto-axis.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-import { EuiXYChart, EuiBar } from '../../../../src/components';
-
-export default () => (
-
-
-
-);
diff --git a/src-docs/src/views/xy_chart/example-crosshair.js b/src-docs/src/views/xy_chart/example-crosshair.js
deleted file mode 100644
index c73729f8d45a..000000000000
--- a/src-docs/src/views/xy_chart/example-crosshair.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-
-import { EuiXYChart, EuiBar } from '../../../../src/components';
-
-// eslint-disable-next-line
-export class ExampleCrosshair extends React.Component {
- state = {
- crosshairX: 0
- }
- _updateCrosshairLocation = (crosshairX) => {
- this.setState({ crosshairX })
- }
- render() {
- return (
-
-
-
-
-
-
-
-
-
- );
- }
-}
\ No newline at end of file
diff --git a/src-docs/src/views/xy_chart/example-empty.js b/src-docs/src/views/xy_chart/example-empty.js
deleted file mode 100644
index d27394be5936..000000000000
--- a/src-docs/src/views/xy_chart/example-empty.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-
-import { EuiXYChart } from '../../../../src/components';
-
-export default () => (
-
-);
diff --git a/src-docs/src/views/xy_chart/examples.js b/src-docs/src/views/xy_chart/examples.js
deleted file mode 100644
index 137475163009..000000000000
--- a/src-docs/src/views/xy_chart/examples.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-
-import { EuiXYChart, EuiBar, EuiArea, EuiLine } from '../../../../src/components';
-
-export default () => {
- const yTicks = [[0, 'zero'], [1, 'one']];
- const xTicks = [
- [0, '0'],
- [5, '5'],
- [10, '10'],
- [15, '15'],
- [20, '20']
- ];
-
- const barData = [];
- for (let i = 0; i < 10; i++) {
- const data = [];
-
- for (let i = 0; i < 20; i++) {
- data.push({ x: i, y: Math.random() });
- }
-
- barData.push(data);
- }
-
- return (
- {
- alert('selection ended with an area :) Check console to see it');
- console.log(area);
- }}
- width={600}
- height={200}
- xTicks={xTicks}
- yTicks={yTicks}
- >
-
- {
- alert('clicked!');
- }}
- data={[{ x: 0, y: 0 }, { x: 1, y: 2 }, { x: 2, y: 1 }, { x: 3, y: 2 },{ x: 4, y: 1 }, { x: 10, y: 1 }, { x: 20, y: 2 } ]}
- />
- {barData.map((data, index) => (
-
- ))}
-
- )
-}
\ No newline at end of file
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 000000000000..27e7dcdf87a3
--- /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 000000000000..e8bdb688cd8b
--- /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
index 05ac54d72178..850432ebba3e 100644
--- a/src-docs/src/views/xy_chart/xy_chart_example.js
+++ b/src-docs/src/views/xy_chart/xy_chart_example.js
@@ -1,65 +1,64 @@
import React from 'react';
import { GuideSectionTypes } from '../../components';
-import { EuiCode } from '../../../../src/components';
-import ChartExampleCode from './examples';
-import EmptyExampleCode from './example-empty';
-import AutoAxisChartExampleCode from './example-auto-axis';
-import { ExampleCrosshair } from './example-crosshair';
+import { EuiCode, EuiXYChart } 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: 'XYChart',
+ title: 'General',
sections: [
{
title: 'Complex example',
text: (
- Use EuiXYChart to display line, bar, area, and stream charts. Note that charts are composed with{' '}
- EuiLine , EuiArea , EuiBar , and EuiStream being child
- components.
+ 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!./examples')
+ code: require('!!raw-loader!./complex'),
},
{
type: GuideSectionTypes.HTML,
- code: 'This component can only be used from React'
- }
+ 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
-
+
When no data is provided to EuiXYChart, an empty state is displayed
),
source: [
{
type: GuideSectionTypes.JS,
- code: require('!!raw-loader!./example-empty')
+ code: require('!!raw-loader!./empty'),
},
{
type: GuideSectionTypes.HTML,
- code: 'This component can only be used from React'
- }
+ code: 'This component can only be used from React',
+ },
],
demo: (
- )
+ ),
},
{
title: 'Keep cross-hair in sync',
@@ -73,43 +72,66 @@ export const XYChartExample = {
source: [
{
type: GuideSectionTypes.JS,
- code: require('!!raw-loader!./example-empty')
+ code: require('!!raw-loader!./crosshair_sync'),
},
{
type: GuideSectionTypes.HTML,
- code: 'This component can only be used from React'
- }
+ code: 'This component can only be used from React',
+ },
],
demo: (
- )
+ ),
},
{
- title: 'Auto Axis',
+ title: 'Multi Axis',
text: (
-
- If just displaying values is enough, then you can let the chart auto label axis
-
+
If just displaying values is enough, then you can let the chart auto label axis
),
source: [
{
type: GuideSectionTypes.JS,
- code: require('!!raw-loader!./example-auto-axis')
+ code: require('!!raw-loader!./multi_axis'),
},
{
type: GuideSectionTypes.HTML,
- code: 'This component can only be used from React'
- }
+ 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 000000000000..2296360091d5
--- /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 000000000000..2d75bbd58e9f
--- /dev/null
+++ b/src-docs/src/views/xy_chart_area/area_example.js
@@ -0,0 +1,123 @@
+import React 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 } from '../../../../src/components';
+
+export const XYChartAreaExample = {
+ title: 'Area chart',
+ 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 000000000000..dd6d439dd2d6
--- /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 000000000000..8de9a4df2ed9
--- /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 000000000000..51286d6ca676
--- /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 000000000000..7dbb66ff2118
--- /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 000000000000..f9e688f343b8
--- /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 000000000000..04d24bb06154
--- /dev/null
+++ b/src-docs/src/views/xy_chart_axis/xy_axis_example.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import { GuideSectionTypes } from '../../components';
+import { EuiCode, EuiXAxis, EuiYAxis, EuiLineAnnotation } from '../../../../src/components';
+import SimpleAxisExampleCode from './simple_axis';
+import AnnotationExampleCode from './annotations';
+
+export const XYChartAxisExample = {
+ title: 'Axis',
+ 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 000000000000..0e37c929d60a
--- /dev/null
+++ b/src-docs/src/views/xy_chart_bar/bar_example.js
@@ -0,0 +1,178 @@
+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: (
+
+
+ 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 000000000000..990d52062787
--- /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 000000000000..1622eadf6e37
--- /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 000000000000..03fdf021bf55
--- /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 000000000000..2b13f1defdcb
--- /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 000000000000..818258b1b187
--- /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 000000000000..f151e40d0921
--- /dev/null
+++ b/src-docs/src/views/xy_chart_histogram/histogram_example.js
@@ -0,0 +1,190 @@
+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: (
+
+
+ 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 000000000000..e0d81ba0f06b
--- /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 000000000000..65b14e455a52
--- /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 000000000000..4e1ea5e958f4
--- /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 000000000000..ef678a344b39
--- /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 000000000000..ab1ca994e4e1
--- /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 000000000000..22c64c12e31f
--- /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 000000000000..157203bd138e
--- /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 000000000000..4bf72ee4d3ab
--- /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 000000000000..5b341a418a4a
--- /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 000000000000..2eb461624412
--- /dev/null
+++ b/src-docs/src/views/xy_chart_line/line_example.js
@@ -0,0 +1,163 @@
+import React 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 } from '../../../../src/components';
+
+export const XYChartLineExample = {
+ title: 'Line chart',
+ 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 000000000000..ba918494afb5
--- /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, 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-docs/src/views/xy_chart_series/area_series.js b/src-docs/src/views/xy_chart_series/area_series.js
deleted file mode 100644
index bf849ee40d00..000000000000
--- a/src-docs/src/views/xy_chart_series/area_series.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-import { EuiXYChart, EuiArea } from '../../../../src/components';
-
-export default () => (
-
-
-
-);
diff --git a/src-docs/src/views/xy_chart_series/bar_series.js b/src-docs/src/views/xy_chart_series/bar_series.js
deleted file mode 100644
index d2b31cd1d096..000000000000
--- a/src-docs/src/views/xy_chart_series/bar_series.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-import { EuiXYChart, EuiBar } from '../../../../src/components';
-
-export default () => (
-
-
-
-);
\ No newline at end of file
diff --git a/src-docs/src/views/xy_chart_series/line_series.js b/src-docs/src/views/xy_chart_series/line_series.js
deleted file mode 100644
index 1a2464b4b193..000000000000
--- a/src-docs/src/views/xy_chart_series/line_series.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-import { EuiXYChart, EuiLine } from '../../../../src/components';
-
-export default () => (
-
-
-
-);
diff --git a/src-docs/src/views/xy_chart_series/series_example.js b/src-docs/src/views/xy_chart_series/series_example.js
deleted file mode 100644
index ac26a419e457..000000000000
--- a/src-docs/src/views/xy_chart_series/series_example.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import React from 'react';
-import { GuideSectionTypes } from '../../components';
-import LineSeriesExample from './line_series';
-import BarSeriesExample from './bar_series';
-import AreaSeriesExample from './area_series';
-import { EuiCode, EuiBar, EuiLine, EuiArea } from '../../../../src/components';
-
-export const XYChartSeriesExample = {
- title: 'XYChart Series',
- sections: [
- {
- title: 'Line Series',
- text: (
-
-
- Use EuiXYChart to display line, bar, area, and stream charts. Note that charts are composed with{' '}
- EuiLine , EuiArea , EuiBar , and EuiStream being child
- components.
-
-
- ),
- props: { EuiLine },
- source: [
- {
- type: GuideSectionTypes.JS,
- code: require('!!raw-loader!./line_series')
- },
- {
- type: GuideSectionTypes.HTML,
- code: 'This component can only be used from React'
- }
- ],
- demo: (
-
-
-
- )
- },
- {
- title: 'Area Series',
- text: (
-
-
- Use EuiXYChart to display line, bar, area, and stream charts. Note that charts are composed with{' '}
- EuiLine , EuiArea , EuiBar , and EuiStream being child
- components.
-
-
- ),
- props: { EuiArea },
- source: [
- {
- type: GuideSectionTypes.JS,
- code: require('!!raw-loader!./area_series')
- },
- {
- type: GuideSectionTypes.HTML,
- code: 'This component can only be used from React'
- }
- ],
- demo: (
-
- )
- },
- {
- title: 'Bar Series',
- text: (
-
-
- When no data is provided to EuiXYChart, an empty state is displayed
-
-
- ),
- props: { EuiBar },
- source: [
- {
- type: GuideSectionTypes.JS,
- code: require('!!raw-loader!./bar_series')
- },
- {
- type: GuideSectionTypes.HTML,
- code: 'This component can only be used from React'
- }
- ],
- demo: (
-
-
-
- )
- }
- ]
-};
diff --git a/src/components/index.js b/src/components/index.js
index 4183ccd24d78..c0268112ce1c 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -304,7 +304,21 @@ export {
export {
EuiXYChart,
- EuiLine,
- EuiArea,
- EuiBar
-} from './xy_chart';
\ No newline at end of file
+ EuiXYChartUtils,
+ EuiXYChartAxisUtils,
+ EuiXYChartTextUtils,
+ EuiLineSeries,
+ EuiAreaSeries,
+ EuiBarSeries,
+ EuiHistogramSeries,
+ EuiVerticalBarSeries,
+ EuiHorizontalBarSeries,
+ EuiVerticalRectSeries,
+ EuiHorizontalRectSeries,
+ EuiDefaultAxis,
+ EuiXAxis,
+ EuiYAxis,
+ EuiCrosshairX,
+ EuiCrosshairY,
+ EuiLineAnnotation,
+} from './xy_chart';
diff --git a/src/components/xy_chart/__snapshots__/area.test.js.snap b/src/components/xy_chart/__snapshots__/area.test.js.snap
deleted file mode 100644
index 99e1f750b518..000000000000
--- a/src/components/xy_chart/__snapshots__/area.test.js.snap
+++ /dev/null
@@ -1,9086 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`EuiArea 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[`EuiArea 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/__snapshots__/bar.test.js.snap b/src/components/xy_chart/__snapshots__/bar.test.js.snap
deleted file mode 100644
index 033dbf3bbf62..000000000000
--- a/src/components/xy_chart/__snapshots__/bar.test.js.snap
+++ /dev/null
@@ -1,6772 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`EuiBar 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[`EuiBar 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/components/xy_chart/__snapshots__/chart.test.js.snap b/src/components/xy_chart/__snapshots__/chart.test.js.snap
deleted file mode 100644
index 2ec3454ac13d..000000000000
--- a/src/components/xy_chart/__snapshots__/chart.test.js.snap
+++ /dev/null
@@ -1,201 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`EuiXYChart is rendered (empty) 1`] = `
-
-`;
-
-exports[`EuiXYChart passes handler functions 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Graph not avaliable
-
-
-
-
-
-
-
-
-`;
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 000000000000..544da598cfb1
--- /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__/title.test.js.snap b/src/components/xy_chart/__snapshots__/title.test.js.snap
deleted file mode 100644
index 5d502f32bce9..000000000000
--- a/src/components/xy_chart/__snapshots__/title.test.js.snap
+++ /dev/null
@@ -1,11 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`EuiTitle is rendered 1`] = `
-
- Title
-
-`;
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 000000000000..fe9850c650ae
--- /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/_chart.scss b/src/components/xy_chart/_chart.scss
deleted file mode 100644
index cd75098eddda..000000000000
--- a/src/components/xy_chart/_chart.scss
+++ /dev/null
@@ -1,19 +0,0 @@
-.rv-xy-plot__inner {
- overflow: visible;
-}
-
-.euixychart-error-nodata {
- position: relative;
- border: 2px solid #e2e2e2;
- border-radius: 8px;
- text-align: center;
-}
-
-.euixychart-error-nodata .euiToastHeader__title {
- margin-left: 5px;
- font-weight: bold;
-}
-
-.euixychart-error-nodata .euiToastHeader__icon {
- margin-top: -5px;
-}
\ No newline at end of file
diff --git a/src/components/xy_chart/_index.scss b/src/components/xy_chart/_index.scss
index abd633274265..1d4238e3d628 100644
--- a/src/components/xy_chart/_index.scss
+++ b/src/components/xy_chart/_index.scss
@@ -1,4 +1,11 @@
-@import "reactvis";
+/* 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 "chart";
\ No newline at end of file
+@import "line_annotation";
+@import "xy_chart";
diff --git a/src/components/xy_chart/_line_annotation.scss b/src/components/xy_chart/_line_annotation.scss
new file mode 100644
index 000000000000..85cb88d24c7f
--- /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/_reactvis.css b/src/components/xy_chart/_reactvis.css
deleted file mode 100644
index b2a184bbbb6c..000000000000
--- a/src/components/xy_chart/_reactvis.css
+++ /dev/null
@@ -1 +0,0 @@
-.react-vis-magic-css-import-rule{display:inherit}.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}.rv-xy-plot{color:#c3c3c3;position:relative}.rv-xy-plot canvas{pointer-events:none}.rv-xy-plot .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:#e6e6e9}.rv-xy-plot__axis__tick__line{stroke:#e6e6e9}.rv-xy-plot__axis__tick__text{fill:#6b6b76;font-size:11px}.rv-xy-plot__axis__title text{fill:#6b6b76;font-size:11px}.rv-xy-plot__grid-lines__line{stroke:#e6e6e9}.rv-xy-plot__circular-grid-lines__line{fill-opacity:0;stroke:#e6e6e9}.rv-xy-plot__series,.rv-xy-plot__series path{pointer-events:all}.rv-xy-plot__circular-grid-lines__line{fill-opacity:0;stroke:#e6e6e9}.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:4px;background:#3a3a48;color:#fff;font-size:12px;padding:7px 10px;box-shadow:0 2px 4px rgba(0,0,0,0.5)}.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:4px;padding:7px 10px;font-size:12px;background:#3a3a48;box-shadow:0 2px 4px rgba(0,0,0,0.5);color:#fff;text-align:left;white-space:nowrap}.rv-discrete-color-legend{box-sizing:border-box;overflow-y:auto;font-size:12px}.rv-discrete-color-legend.horizontal{white-space:nowrap}.rv-discrete-color-legend-item{color:#3a3a48;border-radius:1px;padding:9px 10px}.rv-discrete-color-legend-item.horizontal{display:inline-block}.rv-discrete-color-legend-item.horizontal .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:#b8b8b8}.rv-discrete-color-legend-item.clickable{cursor:pointer}.rv-discrete-color-legend-item.clickable: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-continuous-color-legend .rv-gradient{height:4px;border-radius:2px;margin-bottom:5px}.rv-continuous-size-legend{font-size:12px}.rv-continuous-size-legend .rv-bubbles{text-align:justify;overflow:hidden;margin-bottom:5px;width:100%}.rv-continuous-size-legend .rv-bubble{background:#d8d9dc;display:inline-block;vertical-align:bottom}.rv-continuous-size-legend .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}.rv-radial-chart .rv-xy-plot__series--label{pointer-events:none}
diff --git a/src/components/xy_chart/_xy_chart.scss b/src/components/xy_chart/_xy_chart.scss
new file mode 100644
index 000000000000..121a7a16e9cb
--- /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/area.js b/src/components/xy_chart/area.js
deleted file mode 100644
index 98cc05d34b55..000000000000
--- a/src/components/xy_chart/area.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { EuiLine } from './line';
-import { AreaSeries, AbstractSeries } from 'react-vis';
-
-export class EuiArea extends AbstractSeries {
- render() {
- const { name, data, curve, color, ...rest } = this.props;
- return (
-
-
-
-
- );
- }
-}
-
-EuiArea.propTypes = {
- /** The name used to define the data in tooltips and ledgends */
- 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,
- /** Without a color set, a random EUI color palette color will be chosen */
- color: PropTypes.string,
- curve: PropTypes.string,
- hasLineMarks: PropTypes.bool,
- lineMarkColor: PropTypes.string,
- lineMarkSize: PropTypes.number,
- onClick: PropTypes.func,
- onMarkClick: PropTypes.func
-}
-
-EuiArea.defaultProps = {
- curve: 'linear',
- hasLineMarks: true,
- lineMarkSize: 5
-};
\ No newline at end of file
diff --git a/src/components/xy_chart/as_series.js b/src/components/xy_chart/as_series.js
deleted file mode 100644
index 304e78da241c..000000000000
--- a/src/components/xy_chart/as_series.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-import { AbstractSeries } from 'react-vis';
-
-export function asSeries(Component) {
- return class AsSeries extends AbstractSeries {
- static displayName = `${Component.displayName}AsSeries` || 'asSeriesHOC';
- static propTypes = {
- ...Component.propTypes
- };
- static defaultProps = {
- ...Component.defaultProps
- };
- render() {
- return (
-
- );
- }
- }
-}
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 000000000000..cb03e4c76536
--- /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 000000000000..3eff90dbffe1
--- /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 000000000000..a000fa143219
--- /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 000000000000..89b45cb7c122
--- /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 000000000000..39b1139a3e87
--- /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 000000000000..f924182f3c8f
--- /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 000000000000..fb806d52f247
--- /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 000000000000..77c074e807ae
--- /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 000000000000..14e4b04e930f
--- /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 000000000000..467ad65a290e
--- /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 000000000000..8e85335355d4
--- /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 000000000000..ea6034fb57d7
--- /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 000000000000..d4759ab1597e
--- /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 000000000000..7cad5fc76bc3
--- /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 000000000000..27220605934d
--- /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 000000000000..1e7e48677aa3
--- /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 000000000000..45761adf6946
--- /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 000000000000..e35968a75f6b
--- /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/bar.js b/src/components/xy_chart/bar.js
deleted file mode 100644
index 9d54695ef776..000000000000
--- a/src/components/xy_chart/bar.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { VerticalBarSeries } from 'react-vis';
-
-class EUIBarSeries extends VerticalBarSeries {
- render() {
- const { name, data, color, onClick, ...rest } = this.props;
-
- return (
-
-
-
- );
- }
-}
-export default EUIBarSeries;
-
-EUIBarSeries.propTypes = {
- /** The name used to define the data in tooltips and ledgends */
- 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,
- /** Without a color set, a random EUI color palette color will be chosen */
- color: PropTypes.string,
- onClick: PropTypes.func
-};
-
-EUIBarSeries.defaultProps = {};
diff --git a/src/components/xy_chart/bar.test.js b/src/components/xy_chart/bar.test.js
deleted file mode 100644
index a086d0caca8e..000000000000
--- a/src/components/xy_chart/bar.test.js
+++ /dev/null
@@ -1,109 +0,0 @@
-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 './chart';
-import EuiBar from './bar';
-import { benchmarkFunction } from '../../test/time_execution';
-
-beforeEach(patchRandom);
-afterEach(unpatchRandom);
-
-describe('EuiBar', () => {
- test('is rendered', () => {
- const component = mount(
-
-
-
- );
-
- expect(component).toMatchSnapshot();
- });
-
- test('all props are rendered', () => {
- const component = mount(
-
- {}}
- />
-
- );
-
- 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/chart.js b/src/components/xy_chart/chart.js
deleted file mode 100644
index 8b05fc58ad2d..000000000000
--- a/src/components/xy_chart/chart.js
+++ /dev/null
@@ -1,239 +0,0 @@
-import React, { PureComponent } from 'react';
-import { XYPlot, makeWidthFlexible, XAxis, YAxis, HorizontalGridLines, Crosshair } from 'react-vis';
-import PropTypes from 'prop-types';
-import { getPlotValues } from './utils';
-import Highlight from './highlight';
-import { VISUALIZATION_COLORS } from '../../services';
-import StatusText from './status-text';
-
-const NO_DATA_VALUE = '~~NODATATODISPLAY~~';
-
-export class XYChart extends PureComponent {
- state = {
- crosshairValues: [],
- };
- seriesItems = [];
- colorIterator = 0;
- lastCrosshairX = 0;
- _xyPlotRef = React.createRef();;
-
- _onMouseLeave = () => {
- this.setState({ crosshairValues: [], lastCrosshairIndex: null });
- }
-
- _onMouseMove = (e) => {
- e.persist();
- this._updateCrosshairValues({
- boundingClientRect: e.currentTarget.getBoundingClientRect(),
- clientX: e.clientX
- });
- }
-
- _updateCrosshairValues = ({ boundingClientRect, clientX }) => {
- // Calculate the range of the X axis
- const chartData = this._xyPlotRef.current.state.data.filter(d => d !== undefined)
- const plotValues = getPlotValues(chartData, this.props.width);
- const xDomain = plotValues.x.domain();
- const maxChartXValue = (xDomain[1] - xDomain[0]) + 1;
-
- const innerChartWidth = this._xyPlotRef.current._getDefaultScaleProps(this._xyPlotRef.current.props).xRange[1]
-
- const mouseX = clientX - boundingClientRect.left;
- const xAxisesBucketWidth = innerChartWidth / maxChartXValue;
- const bucketX = Math.floor(mouseX / xAxisesBucketWidth)
-
- if (bucketX !== this.lastCrosshairX) {
- if(this.props.onCrosshairUpdate) this.props.onCrosshairUpdate(bucketX)
- if(!this.props.crosshairX) {
- this.lastCrosshairX = bucketX;
-
- const crosshairValues = this._getAllSeriesFromDataAtIndex(chartData, bucketX)
-
- this.setState({
- crosshairValues
- });
- }
- }
- }
-
- _getAllSeriesFromDataAtIndex = (chartData, xBucket) => {
- const chartDataForXValue = chartData.map(series => series.filter(seriesData => {
- return seriesData.x === xBucket
- })[0])
-
- if(chartDataForXValue.length === 0) {
- chartDataForXValue.push({ x: xBucket, y: NO_DATA_VALUE })
- }
-
- return chartDataForXValue;
- };
-
- _itemsFormat = (values) => {
- return values.map((v, i) => {
- if (v) {
- if(v.y === NO_DATA_VALUE) {
- return {
- title: 'No Data',
- };
- }
- return {
- value: v.y,
- title: this.seriesItems[i] || 'Other',
- };
- }
- });
- }
-
- _getTickLabels(ticks) {
- if (!ticks) return;
-
- return ticks.map(v => {
- return v[1];
- });
- }
-
- _getTicks(ticks) {
- if (!ticks) return;
-
- {
- return ticks.map(v => {
- return v[0];
- });
- }
- }
-
- _renderChildren = (child, i) => {
- const props = {
- id: `chart-${i}`,
- };
-
- this.seriesItems.push(child.props.name);
-
- if (!child.props.color) {
- props.color = VISUALIZATION_COLORS[this.colorIterator];
-
- this.colorIterator++;
- if (this.colorIterator > VISUALIZATION_COLORS.length - 1) this.colorIterator = 0;
- }
-
- return React.cloneElement(child, props);
- }
-
- _getCrosshairValues = (crosshairX) => {
- if(!crosshairX) return this.state.crosshairValues
-
- const chartData = this._xyPlotRef.current.state.data.filter(d => d !== undefined)
- return this._getAllSeriesFromDataAtIndex(chartData, crosshairX)
- }
-
-
- render() {
- const {
- width,
- height,
- mode,
- errorText,
- xAxisLocation,
- yAxisLocation,
- showAxis,
- yTicks,
- xTicks,
- crosshairX,
- showTooltips,
- onSelectEnd,
- children,
- animation, // eslint-disable-line no-unused-vars
- onCrosshairUpdate, // eslint-disable-line no-unused-vars
- truncateLegends, // eslint-disable-line no-unused-vars
- ...rest
- } = this.props;
-
-
- if (!children || errorText) {
- return ;
- }
-
- this.colorIterator = 0;
-
- return (
-
-
-
- {showAxis && [
- ,
- this._getTickLabels(xTicks)[v] || v : undefined}
- />,
- this._getTickLabels(yTicks)[v] || v : undefined}
- />
- ]}
-
- {React.Children.map(children, this._renderChildren)}
-
- {showTooltips && (
- null}
- itemsFormat={this._itemsFormat}
- />
- )}
-
- {onSelectEnd && }
-
-
- );
- }
-}
-
-XYChart.propTypes = {
- width: PropTypes.number.isRequired,
- height: PropTypes.number.isRequired,
- onHover: PropTypes.func,
- onMouseLeave: PropTypes.func,
- onSelectEnd: PropTypes.func,
- hoverIndex: PropTypes.number,
- xTicks: PropTypes.array,
- yTicks: PropTypes.array, // [[0, "zero"], [1.2, "one mark"], [2.4, "two marks"]]
- truncateLegends: PropTypes.bool,
- showAxis: PropTypes.bool,
- xAxisLocation: PropTypes.string,
- yAxisLocation: PropTypes.string,
- mode: PropTypes.string,
- showTooltips: PropTypes.bool,
- errorText: PropTypes.string,
- crosshairX: PropTypes.number,
- onCrosshairUpdate: PropTypes.func
-};
-
-XYChart.defaultProps = {
- truncateLegends: false,
- showAxis: true,
- showTooltips: true,
- mode: 'linear',
-};
-
-export default makeWidthFlexible(XYChart);
diff --git a/src/components/xy_chart/chart.test.js b/src/components/xy_chart/chart.test.js
deleted file mode 100644
index d9466c3927b3..000000000000
--- a/src/components/xy_chart/chart.test.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react';
-import { mount, render } from 'enzyme';
-
-import EuiXYChart from './chart';
-import { requiredProps } from '../../test/required_props';
-
-describe('EuiXYChart', () => {
- test('is rendered (empty)', () => {
- const component = render(
-
- );
-
- expect(component).toMatchSnapshot();
- });
-
- test('passes handler functions', () => {
- const component = mount(
- {}}
- onMouseLeave={() => {}}
- onSelectEnd={() => {}}
- yTicks={[[0, 'zero'], [100, 'one hundred']]}
- xTicks={[[0, 'zero', 5, 'five'], [10, '10']]}
- />
- );
-
- expect(component).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 000000000000..18d0fe9b84ac
--- /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 000000000000..7757dd6fb052
--- /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 000000000000..3a8671dc7a08
--- /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 000000000000..6be14ccd6213
--- /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 000000000000..ab7354d3686c
--- /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 000000000000..0b74298bdb33
--- /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 000000000000..7eb5f347a4c1
--- /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/highlight.js b/src/components/xy_chart/highlight.js
deleted file mode 100644
index 3d29900f7920..000000000000
--- a/src/components/xy_chart/highlight.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import React from 'react';
-import { ScaleUtils, AbstractSeries } from 'react-vis';
-
-export default class Highlight extends AbstractSeries {
- static displayName = 'HighlightOverlay';
- static defaultProps = {
- allow: 'x',
- color: 'rgb(0,0, 0)',
- opacity: 0.2
- };
- state = {
- drawing: false,
- drawArea: { top: 0, right: 0, bottom: 0, left: 0 },
- startLoc: 0
- };
-
- _getDrawArea(loc) {
- const { innerWidth } = this.props;
- const { drawArea, startLoc } = this.state;
-
- if (loc < startLoc) {
- return {
- ...drawArea,
- left: Math.max(loc, 0),
- right: startLoc
- };
- }
-
- return {
- ...drawArea,
- right: Math.min(loc, innerWidth),
- left: startLoc
- };
- }
-
- onParentMouseDown(e) {
- const { marginLeft, innerHeight, onSelectStart } = this.props;
- const location = e.nativeEvent.offsetX - marginLeft;
-
- this.setState({
- drawing: true,
- drawArea: {
- top: 0,
- right: location,
- bottom: innerHeight,
- left: location
- },
- startLoc: location
- });
-
- if (onSelectStart) {
- onSelectStart(e);
- }
- }
-
- stopDrawing() {
- // Quickly short-circuit if the user isn't drawing in our component
- if (!this.state.drawing) {
- return;
- }
-
- const { onSelectEnd } = this.props;
- const { drawArea } = this.state;
- const xScale = ScaleUtils.getAttributeScale(this.props, 'x');
-
- // Clear the draw area
- this.setState({
- drawing: false,
- drawArea: { top: 0, right: 0, bottom: 0, left: 0 },
- startLoc: 0
- });
-
- // Don't invoke the callback if the selected area was < 5px.
- // This is a click not a select
- if (Math.abs(drawArea.right - drawArea.left) < 5) {
- return;
- }
-
- // Compute the corresponding domain drawn
- const domainArea = {
- end: xScale.invert(drawArea.right),
- begin: xScale.invert(drawArea.left)
- };
-
- if (onSelectEnd) {
- onSelectEnd(domainArea);
- }
- }
-
- onParentMouseMove(e) {
- const { marginLeft, onSelect } = this.props;
- const { drawing } = this.state;
- const loc = e.nativeEvent.offsetX - marginLeft;
-
- if (drawing) {
- const newDrawArea = this._getDrawArea(loc);
- this.setState({ drawArea: newDrawArea });
-
- if (onSelect) {
- onSelect(e);
- }
- }
- }
-
- render() {
- const { marginLeft, marginTop, innerWidth, innerHeight, color, opacity } = this.props;
- const { drawArea: { left, right, top, bottom } } = this.state;
-
- return (
- this.stopDrawing()}
- onMouseLeave={() => this.stopDrawing()}
- >
-
-
-
- );
- }
-}
diff --git a/src/components/xy_chart/index.js b/src/components/xy_chart/index.js
index b375c1a81f23..7f2a702d8167 100644
--- a/src/components/xy_chart/index.js
+++ b/src/components/xy_chart/index.js
@@ -1,15 +1,14 @@
-import { asSeries } from './as_series';
-import EuiXYChart from './chart';
-import * as utils from './utils';
-import { EuiLine } from './line';
-import EuiBar from './bar';
-import{ EuiArea } from './area';
-
-export {
- EuiXYChart,
- EuiLine,
- EuiArea,
- EuiBar,
- utils,
- asSeries
-};
\ No newline at end of file
+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/line.js b/src/components/xy_chart/line.js
deleted file mode 100644
index 3c669c1bd2c8..000000000000
--- a/src/components/xy_chart/line.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { LineSeries, MarkSeries, AbstractSeries } from 'react-vis';
-
-export class EuiLine extends AbstractSeries {
- render() {
- const {
- data,
- name,
- curve,
- onClick,
- onMarkClick,
- hasLineMarks,
- lineMarkColor,
- lineMarkSize,
- color,
- ...rest
- } = this.props;
-
- return (
-
-
-
-
- {hasLineMarks && (
-
- )}
-
- )
- }
-}
-
-EuiLine.propTypes = {
- /** The name used to define the data in tooltips and ledgends */
- 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,
- /** Without a color set, a random EUI color palette color will be chosen */
- color: PropTypes.string,
- curve: PropTypes.string,
- hasLineMarks: PropTypes.bool,
- lineMarkColor: PropTypes.string,
- lineMarkSize: PropTypes.number,
- onClick: PropTypes.func,
- onMarkClick: PropTypes.func
-};
-
-EuiLine.defaultProps = {
- curve: 'linear',
- hasLineMarks: true,
- lineMarkSize: 5
-};
diff --git a/src/components/xy_chart/line_annotation.js b/src/components/xy_chart/line_annotation.js
new file mode 100644
index 000000000000..1a6e0fe07cc0
--- /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 000000000000..6b86fa7b353a
--- /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 000000000000..786b7a515ac6
--- /dev/null
+++ b/src/components/xy_chart/selection_brush.test.js
@@ -0,0 +1,205 @@
+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 000000000000..35737f402a1f
--- /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 000000000000..f2c56c35169a
--- /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 000000000000..d01d678fa8fb
--- /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/__snapshots__/line.test.js.snap b/src/components/xy_chart/series/__snapshots__/line_series.test.js.snap
similarity index 50%
rename from src/components/xy_chart/__snapshots__/line.test.js.snap
rename to src/components/xy_chart/series/__snapshots__/line_series.test.js.snap
index e92e5260609b..724bbed719ec 100644
--- a/src/components/xy_chart/__snapshots__/line.test.js.snap
+++ b/src/components/xy_chart/series/__snapshots__/line_series.test.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`EuiLine all props are rendered 1`] = `
-
@@ -13,26 +13,42 @@ exports[`EuiLine all props are rendered 1`] = `
}
}
>
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+ }
+ transform="translate(40,10)"
+ />
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
- 0.0
-
-
-
-
-
- 0.1
-
-
-
-
-
- 0.2
-
-
-
-
-
- 0.3
-
-
-
-
-
- 0.4
-
-
-
-
-
- 0.5
-
-
-
-
-
- 0.6
-
-
-
-
-
- 0.7
-
-
-
-
-
- 0.8
-
-
-
-
-
- 0.9
-
-
-
-
-
- 1.0
-
-
-
-
-
-
+ }
+ transform="translate(40,10)"
+ />
+
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- 6
-
-
-
+
-
-
- 8
-
-
-
+
-
-
- 10
-
-
-
+
-
-
- 12
-
-
-
+
-
-
- 14
-
-
+ x1={0}
+ x2={550}
+ y1={15}
+ y2={15}
+ />
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EuiLine 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[`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
-
-
-
-
-
-
+ }
+ transform="translate(40,10)"
+ />
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
- 6
-
-
-
-
-
- 8
-
-
-
-
-
- 10
-
-
-
-
-
- 12
-
-
-
-
-
- 14
-
-
-
-
-
-
+ }
+ transform="translate(40,10)"
+ />
+
-
-
-
+
+
-
-
+
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ 0.0
+
+
+
+
+
+ 0.1
+
+
+
+
+
+ 0.2
+
+
+
+
+
+ 0.3
+
+
+
+
+
+ 0.4
+
+
+
+
+
+ 0.5
+
+
+
+
+
+ 0.6
+
+
+
+
+
+ 0.7
+
+
+
+
+
+ 0.8
+
+
+
+
+
+ 0.9
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+ xType="linear"
+ yDomain={
+ Array [
+ 5,
+ 15,
+ ]
+ }
+ yPadding={0}
+ yRange={
+ Array [
+ 150,
+ 0,
+ ]
+ }
+ yType="linear"
+ >
+
+
+
+
+ 6
+
+
+
+
+
+ 8
+
+
+
+
+
+ 10
+
+
+
+
+
+ 12
+
+
+
+
+
+ 14
+
+
+
+
+
+
+
+
+
+
+
+
-
+ yType="linear"
+ >
+
+
-
+
-
+
`;
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 000000000000..37e8ac83ea63
--- /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 000000000000..0c59f31c9ddd
--- /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 000000000000..868f7c3a090d
--- /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 000000000000..edebd5d9f3f1
--- /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 000000000000..9118a97865b6
--- /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 000000000000..d497bbbf7411
--- /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 000000000000..d4986137b548
--- /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/area.test.js b/src/components/xy_chart/series/area_series.test.js
similarity index 63%
rename from src/components/xy_chart/area.test.js
rename to src/components/xy_chart/series/area_series.test.js
index 0184d8f76b2c..98fe6d1b1480 100644
--- a/src/components/xy_chart/area.test.js
+++ b/src/components/xy_chart/series/area_series.test.js
@@ -1,22 +1,31 @@
import React from 'react';
import { mount, render } from 'enzyme';
-import { patchRandom, unpatchRandom } from '../../test/patch_random';
-import { requiredProps } from '../../test/required_props';
+import { patchRandom, unpatchRandom } from '../../../test/patch_random';
+import { requiredProps } from '../../../test/required_props';
-import EuiXYChart from './chart';
-import { EuiArea } from './area';
-import { benchmarkFunction } from '../../test/time_execution';
+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);
-describe('EuiArea', () => {
- test('is rendered', () => {
+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(
-
);
@@ -24,24 +33,24 @@ describe('EuiArea', () => {
expect(component).toMatchSnapshot();
});
- test('all props are rendered', () => {
+ test('call onSeriesClick', () => {
+ const data = [{ x: 0, y: 5 }, { x: 1, y: 3 }];
+ const onSeriesClick = jest.fn();
const component = mount(
-
- {}}
- onMarkClick={() => {}}
+
+
);
-
- expect(component).toMatchSnapshot();
+ component.find('path').at(0).simulate('click');
+ expect(onSeriesClick.mock.calls).toHaveLength(1);
});
describe('performance', () => {
@@ -63,7 +72,7 @@ describe('EuiArea', () => {
function renderChart() {
render(
-
+
)
}
@@ -99,7 +108,7 @@ describe('EuiArea', () => {
render(
{linesData.map((data, index) => (
-
+
))}
)
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 000000000000..c33a5c24c2b1
--- /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 000000000000..a39f594403c9
--- /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 000000000000..a52702d0cb0f
--- /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 000000000000..c19b6e9e65c4
--- /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 000000000000..237a7ea27da7
--- /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 000000000000..2e8389748d4b
--- /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 000000000000..4eb1e6c4f76e
--- /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 000000000000..fff016674beb
--- /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/line.test.js b/src/components/xy_chart/series/line_series.test.js
similarity index 61%
rename from src/components/xy_chart/line.test.js
rename to src/components/xy_chart/series/line_series.test.js
index e0fe5275736e..104725cf33d5 100644
--- a/src/components/xy_chart/line.test.js
+++ b/src/components/xy_chart/series/line_series.test.js
@@ -1,20 +1,21 @@
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 { patchRandom, unpatchRandom } from '../../../test/patch_random';
+import { benchmarkFunction } from '../../../test/time_execution';
+import { requiredProps } from '../../../test/required_props';
-import EuiXYChart from './chart';
-import { EuiLine } from './line';
+import { EuiXYChart } from '../xy_chart';
+import { EuiLineSeries } from './line_series';
+import { VISUALIZATION_COLORS } from '../../../services';
beforeEach(patchRandom);
afterEach(unpatchRandom);
-describe('EuiLine', () => {
+describe('EuiLineSeries', () => {
test('is rendered', () => {
const component = mount(
-
@@ -27,16 +28,16 @@ describe('EuiLine', () => {
test('all props are rendered', () => {
const component = mount(
- {}}
- onMarkClick={() => {}}
+ onSeriesClick={() => {}}
+ onValueClick={() => {}}
/>
);
@@ -44,6 +45,34 @@ describe('EuiLine', () => {
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', () => {
@@ -64,7 +93,7 @@ describe('EuiLine', () => {
function renderChart() {
render(
-
+
)
}
@@ -100,7 +129,7 @@ describe('EuiLine', () => {
render(
{linesData.map((data, index) => (
-
+
))}
)
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 000000000000..1bfefae7d773
--- /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 000000000000..1d20aa954b55
--- /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 000000000000..b1bc0e392a0b
--- /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 000000000000..466043a1ca68
--- /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/styles/react_vis/legends.scss b/src/components/xy_chart/styles/react_vis/legends.scss
new file mode 100644
index 000000000000..45e8674dcabd
--- /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 000000000000..8d1f75ca2559
--- /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 000000000000..854a57d4b67b
--- /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 000000000000..4626d66ea798
--- /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/title.js b/src/components/xy_chart/title.js
deleted file mode 100644
index e2278a89ce6e..000000000000
--- a/src/components/xy_chart/title.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import {
- cloneElement,
-} from 'react';
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-
-const titleSizeToClassNameMap = {
- s: 'euiTitle--small',
- l: 'euiTitle--large',
-};
-
-export const TITLE_SIZES = Object.keys(titleSizeToClassNameMap);
-
-export const EuiTitle = ({ size, children, className, ...rest }) => {
-
- const classes = classNames(
- 'euiTitle',
- titleSizeToClassNameMap[size],
- className
- );
-
- const props = {
- className: classes,
- ...rest
- };
-
- return cloneElement(children, props);
-};
-
-EuiTitle.propTypes = {
- children: PropTypes.element.isRequired,
- className: PropTypes.string,
- size: PropTypes.oneOf(TITLE_SIZES),
-};
diff --git a/src/components/xy_chart/title.test.js b/src/components/xy_chart/title.test.js
deleted file mode 100644
index 16b907516f2d..000000000000
--- a/src/components/xy_chart/title.test.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-import { render } from 'enzyme';
-import { requiredProps } from '../../test/required_props';
-
-import { EuiTitle } from './title';
-
-describe('EuiTitle', () => {
- test('is rendered', () => {
- const component = render(
-
- Title
-
- );
-
- expect(component)
- .toMatchSnapshot();
- });
-});
diff --git a/src/components/xy_chart/utils.js b/src/components/xy_chart/utils.js
deleted file mode 100644
index e39b95121988..000000000000
--- a/src/components/xy_chart/utils.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { scaleLinear } from 'd3-scale';
-import _ from 'lodash';
-import * as d3 from 'd3-array';
-
-const unit = 16;
-const XY_HEIGHT = unit * 16;
-const XY_MARGIN = {
- top: unit,
- left: unit * 5,
- right: unit,
- bottom: unit * 2
-};
-
-const getXScale = _.memoize(
- (xMin, xMax, width) => {
- return scaleLinear()
- .domain([xMin, xMax])
- .range([XY_MARGIN.left, width - XY_MARGIN.right]);
- },
- (...args) => args.join('_')
-);
-
-const getYScale = _.memoize(
- (yMin, yMax) => {
- return scaleLinear()
- .domain([yMin, yMax])
- .range([XY_HEIGHT, 0])
- .nice();
- },
- (...args) => args.join('_')
-);
-
-const getYTickValues = _.memoize(yMaxNice => [0, yMaxNice / 2, yMaxNice]);
-
-export function getPlotValues(series, width) {
- if (series.length === 0) return;
-
- const allCoordinates = _.flatten(series);
-
- const xMin = d3.min(allCoordinates, d => d.x);
- const xMax = d3.max(allCoordinates, d => d.x);
- const yMin = 0;
- const yMax = d3.max(allCoordinates, d => d.y);
-
- const x = getXScale(xMin, xMax, width);
- const y = getYScale(yMin, yMax);
- const yTickValues = getYTickValues(y.domain()[1]);
- const xTickValues = getYTickValues(x.domain()[1]);
-
- return { x, y, yTickValues, xTickValues };
-}
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 000000000000..abe375135bc7
--- /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 000000000000..060ab5e10717
--- /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 000000000000..43f4f75eb290
--- /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 000000000000..aacc1438bf8f
--- /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 000000000000..5e10936206fb
--- /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 000000000000..a93b62e19623
--- /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 000000000000..a01288e400f4
--- /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.refs[`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.refs[`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 000000000000..0dee8a01626f
--- /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 750249aabcb1..0c4908fe99b4 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 b628f4b7c224..0ca8ab243ced 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 0a71e340409d..ade40feeeac0 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/yarn.lock b/yarn.lock
index 73797c3f2ce0..33c855e5f1f0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2438,8 +2438,8 @@ d3-color@1, d3-color@^1.0.3:
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.0.tgz#d1ea19db5859c86854586276ec892cf93148459a"
d3-contour@^1.1.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.2.0.tgz#de3ea7991bbb652155ee2a803aeafd084be03b63"
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.0.tgz#cfb99098c48c46edd77e15ce123162f9e333e846"
dependencies:
d3-array "^1.1.1"
@@ -3606,7 +3606,19 @@ fb-watchman@^2.0.0:
dependencies:
bser "^2.0.0"
-fbjs@^0.8.16, fbjs@^0.8.9:
+fbjs@^0.8.16:
+ version "0.8.17"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+ dependencies:
+ core-js "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.18"
+
+fbjs@^0.8.9:
version "0.8.16"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
dependencies:
@@ -4426,6 +4438,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"
@@ -4603,10 +4619,16 @@ humanize-string@^1.0.0:
dependencies:
decamelize "^1.0.0"
-iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
+iconv-lite@0.4.19, iconv-lite@^0.4.17:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+iconv-lite@~0.4.13:
+ version "0.4.23"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
icss-replace-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@@ -7822,7 +7844,7 @@ promisify-node@~0.3.0:
dependencies:
nodegit-promise "~4.0.0"
-prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0:
+prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.6.0:
version "15.6.0"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
dependencies:
@@ -7830,6 +7852,13 @@ prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0:
loose-envify "^1.3.1"
object-assign "^4.1.1"
+prop-types@^15.5.8:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
+ dependencies:
+ loose-envify "^1.3.1"
+ object-assign "^4.1.1"
+
prop-types@^15.6.1:
version "15.6.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
@@ -8131,8 +8160,8 @@ react-virtualized@^9.18.5:
prop-types "^15.6.0"
react-vis@^1.9.3:
- version "1.9.3"
- resolved "https://registry.yarnpkg.com/react-vis/-/react-vis-1.9.3.tgz#2e5ea8e6bfa0f03bf6e7954d4559b4fd0bc3987c"
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/react-vis/-/react-vis-1.10.0.tgz#e6e268d14745d7207bfe4533a0e94451dbb6e4b1"
dependencies:
d3-array "^1.2.0"
d3-collection "^1.0.3"
@@ -8148,6 +8177,7 @@ react-vis@^1.9.3:
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"
@@ -8738,6 +8768,10 @@ safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
samsam@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
@@ -9886,9 +9920,9 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-ua-parser-js@^0.7.9:
- version "0.7.17"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
+ua-parser-js@^0.7.18, ua-parser-js@^0.7.9:
+ version "0.7.18"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.5"
@@ -10330,7 +10364,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"
@@ -10462,8 +10495,8 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
iconv-lite "0.4.19"
whatwg-fetch@>=0.10.0:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
whatwg-url@^6.4.0:
version "6.4.0"