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`] = ` -
-
-
-
- - - Graph not avaliable - -
-
-
-
-`; - -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 +

+
+
+

+ ~~Empty Chart~~ +

+
+
+ +
+
+`; 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"