From 4bf4d55a7e9142f11348ae05e27b1916ccfe8ca1 Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Tue, 14 Nov 2017 08:29:40 -0800 Subject: [PATCH 1/9] added stats charts in xy-chart --- packages/demo/examples/01-xy-chart/data.js | 4 +- packages/demo/examples/01-xy-chart/index.jsx | 211 ++++++++++++++++++ packages/demo/package.json | 2 +- packages/xy-chart/package.json | 1 + packages/xy-chart/src/index.js | 2 + .../xy-chart/src/series/BoxPlotSeries.jsx | 104 +++++++++ .../xy-chart/src/series/ViolinPlotSeries.jsx | 85 +++++++ packages/xy-chart/src/utils/propShapes.js | 17 ++ 8 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 packages/xy-chart/src/series/BoxPlotSeries.jsx create mode 100644 packages/xy-chart/src/series/ViolinPlotSeries.jsx diff --git a/packages/demo/examples/01-xy-chart/data.js b/packages/demo/examples/01-xy-chart/data.js index 23cf11a9..bffc6919 100644 --- a/packages/demo/examples/01-xy-chart/data.js +++ b/packages/demo/examples/01-xy-chart/data.js @@ -1,4 +1,4 @@ -import { cityTemperature, appleStock, genRandomNormalPoints, letterFrequency } from '@vx/mock-data'; +import { cityTemperature, appleStock, genRandomNormalPoints, letterFrequency, genStats } from '@vx/mock-data'; import { theme } from '@data-ui/xy-chart'; export const timeSeriesData = appleStock.filter((d, i) => i % 120 === 0).map(d => ({ @@ -123,3 +123,5 @@ export const circlePackData = Array(400).fill(null).map((_, i) => ({ fillOpacity: Math.max(0.4, Math.random()), fill: theme.colors.categories[i % 2 === 0 ? 1 : 3], })); + +export const statsData = genStats(5); diff --git a/packages/demo/examples/01-xy-chart/index.jsx b/packages/demo/examples/01-xy-chart/index.jsx index 44dc7f8e..cca2c467 100644 --- a/packages/demo/examples/01-xy-chart/index.jsx +++ b/packages/demo/examples/01-xy-chart/index.jsx @@ -15,6 +15,8 @@ import { LineSeries, PointSeries, StackedBarSeries, + BoxPlotSeries, + ViolinPlotSeries, HorizontalReferenceLine, PatternLines, @@ -43,6 +45,7 @@ import { intervalData, temperatureBands, priceBandData, + statsData, } from './data'; import WithToggle from '../shared/WithToggle'; @@ -488,6 +491,214 @@ export default { ), }, + { + description: 'Box Plot Example', + example: () => { + const boxPlotData = statsData.map(s => s.boxPlot); + const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); + const minYValue = Math.min(...values); + const maxYValue = Math.max(...values); + const yDomain = [minYValue - (0.1 * Math.abs(minYValue)), + maxYValue + (0.1 * Math.abs(minYValue))]; + return ( + + + + + + + ); + }, + }, + { + description: 'Single Horizontal Box Plot Example', + example: () => { + const singleStats = [statsData[0]]; + const boxPlotData = singleStats.map((s) => { + const { boxPlot } = s; + const { x, ...rest } = boxPlot; + return { + y: x, + ...rest, + }; + }); + const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); + const minXValue = Math.min(...values); + const maxXValue = Math.max(...values); + const xDomain = [minXValue - (0.1 * Math.abs(minXValue)), + maxXValue + (0.1 * Math.abs(maxXValue))]; + return ( + + + + + + ); + }, + }, + { + description: 'Horizontal BoxPlot With ViolinPlot Example', + example: () => { + const boxPlotData = statsData.map((s) => { + const { boxPlot } = s; + const { x, ...rest } = boxPlot; + return { + y: x, + ...rest, + }; + }); + const violinData = statsData.map(s => ({ y: s.boxPlot.x, binData: s.binData })); + const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); + const minXValue = Math.min(...values); + const maxXValue = Math.max(...values); + const xDomain = [minXValue - (0.1 * Math.abs(minXValue)), + maxXValue + (0.1 * Math.abs(maxXValue))]; + return ( + + + + + + + + ); + }, + }, + { + description: 'BoxPlot With ViolinPlot Example', + example: () => { + const boxPlotData = statsData.map(s => s.boxPlot); + const violinData = statsData.map(s => ({ x: s.boxPlot.x, binData: s.binData })); + const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); + const minYValue = Math.min(...values); + const maxYValue = Math.max(...values); + const yDomain = [minYValue - (0.1 * Math.abs(minYValue)), + maxYValue + (0.1 * Math.abs(minYValue))]; + return ( + + + + + + + + ); + }, + }, { description: 'XAxis, YAxis -- orientation', components: [XAxis, YAxis], diff --git a/packages/demo/package.json b/packages/demo/package.json index d1836ac0..903aea80 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -32,7 +32,7 @@ "@storybook/addon-options": "^3.1.6", "@storybook/react": "3.2.12", "@vx/legend": "0.0.140", - "@vx/mock-data": "0.0.136", + "@vx/mock-data": "0.0.147", "@vx/responsive": "0.0.140", "@vx/scale": "0.0.140", "aphrodite": "^1.2.0", diff --git a/packages/xy-chart/package.json b/packages/xy-chart/package.json index 56bde4da..e0ca74b0 100644 --- a/packages/xy-chart/package.json +++ b/packages/xy-chart/package.json @@ -35,6 +35,7 @@ "@vx/responsive": "0.0.140", "@vx/scale": "0.0.140", "@vx/shape": "0.0.145", + "@vx/stats": "0.0.147", "@vx/tooltip": "0.0.140", "@vx/voronoi": "0.0.140", "d3-array": "^1.2.0", diff --git a/packages/xy-chart/src/index.js b/packages/xy-chart/src/index.js index c9f3cf99..1857bbee 100644 --- a/packages/xy-chart/src/index.js +++ b/packages/xy-chart/src/index.js @@ -11,6 +11,8 @@ export { default as LineSeries } from './series/LineSeries'; export { default as PointSeries, pointComponentPropTypes } from './series/PointSeries'; export { default as StackedAreaSeries } from './series/StackedAreaSeries'; export { default as StackedBarSeries } from './series/StackedBarSeries'; +export { default as BoxPlotSeries } from './series/BoxPlotSeries'; +export { default as ViolinPlotSeries } from './series/ViolinPlotSeries'; export { default as HorizontalReferenceLine } from './annotation/HorizontalReferenceLine'; export { default as CrossHair } from './chart/CrossHair'; diff --git a/packages/xy-chart/src/series/BoxPlotSeries.jsx b/packages/xy-chart/src/series/BoxPlotSeries.jsx new file mode 100644 index 00000000..bf71e9d1 --- /dev/null +++ b/packages/xy-chart/src/series/BoxPlotSeries.jsx @@ -0,0 +1,104 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Group } from '@vx/group'; +import { BoxPlot } from '@vx/stats'; +import themeColors from '@data-ui/theme/build/color'; + +import { callOrValue, isDefined } from '../utils/chartUtils'; + +import { boxPlotSeriesDataShape } from '../utils/propShapes'; + +const propTypes = { + data: boxPlotSeriesDataShape.isRequired, + label: PropTypes.string.isRequired, + + // attributes on data points will override these + fill: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + stroke: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + strokeWidth: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), + fillOpacity: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), + + // likely be injected by the parent chart + xScale: PropTypes.func, + yScale: PropTypes.func, + horizontal: PropTypes.bool, + widthRatio: PropTypes.number, +}; + +const defaultProps = { + boxWidth: null, + stroke: '#000000', + strokeWidth: 2, + fill: themeColors.default, + fillOpacity: 1, + xScale: null, + yScale: null, + horizontal: false, + widthRatio: 1, +}; + +const MAX_BOX_WIDTH = 50; +const x = d => d.x; +const y = d => d.y; +const min = d => d.min; +const max = d => d.max; +const median = d => d.median; +const firstQuartile = d => d.firstQuartile; +const thirdQuartile = d => d.thirdQuartile; +const outliers = d => d.outliers || []; + +export default function BoxPlotSeries({ + data, + label, + fill, + stroke, + strokeWidth, + xScale, + yScale, + horizontal, + widthRatio, + fillOpacity, +}) { + if (!xScale || !yScale) return null; + const offsetScale = horizontal ? yScale : xScale; + const offsetValue = horizontal ? y : x; + const valueScale = horizontal ? xScale : yScale; + const boxWidth = offsetScale.bandwidth(); + const actualyWidth = Math.min(MAX_BOX_WIDTH, boxWidth); + const offset = (offsetScale.offset || 0) - ((boxWidth - actualyWidth) / 2); + const offsetPropName = horizontal ? 'top' : 'left'; + const offsetProp = d => ({ + [offsetPropName]: (offsetScale(offsetValue(d)) - offset) + + (((1 - widthRatio) / 2) * actualyWidth), + }); + return ( + + {data.map((d, i) => ( + isDefined(min(d)) && ( + + ) + )) + } + + ); +} + +BoxPlotSeries.propTypes = propTypes; +BoxPlotSeries.defaultProps = defaultProps; +BoxPlotSeries.displayName = 'BoxPlotSeries'; diff --git a/packages/xy-chart/src/series/ViolinPlotSeries.jsx b/packages/xy-chart/src/series/ViolinPlotSeries.jsx new file mode 100644 index 00000000..8f4a0b82 --- /dev/null +++ b/packages/xy-chart/src/series/ViolinPlotSeries.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Group } from '@vx/group'; +import { ViolinPlot } from '@vx/stats'; +import themeColors from '@data-ui/theme/build/color'; + +import { callOrValue } from '../utils/chartUtils'; + +const propTypes = { + data: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + + // attributes on data points will override these + fill: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + stroke: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + strokeWidth: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), + + // likely be injected by the parent chart + xScale: PropTypes.func, + yScale: PropTypes.func, + horizontal: PropTypes.bool, + widthRatio: PropTypes.number, +}; + +const defaultProps = { + boxWidth: null, + stroke: '#000000', + strokeWidth: 2, + fill: themeColors.default, + xScale: null, + yScale: null, + horizontal: false, + widthRatio: 1, +}; + +const MAX_BOX_WIDTH = 50; +const x = d => d.x; +const y = d => d.y; + +export default function ViolinPlotSeries({ + data, + label, + fill, + stroke, + strokeWidth, + xScale, + yScale, + horizontal, + widthRatio, +}) { + if (!xScale || !yScale) return null; + const offsetScale = horizontal ? yScale : xScale; + const offsetValue = horizontal ? y : x; + const valueScale = horizontal ? xScale : yScale; + const boxWidth = offsetScale.bandwidth(); + const actualyWidth = Math.min(MAX_BOX_WIDTH, boxWidth); + const offset = (offsetScale.offset || 0) - ((boxWidth - actualyWidth) / 2); + const offsetPropName = horizontal ? 'top' : 'left'; + const offsetProp = d => ({ + [offsetPropName]: (offsetScale(offsetValue(d)) - offset) + + (((1 - widthRatio) / 2) * actualyWidth), + }); + return ( + + {data.map((d, i) => ( + + )) + } + + ); +} + +ViolinPlotSeries.propTypes = propTypes; +ViolinPlotSeries.defaultProps = defaultProps; +ViolinPlotSeries.displayName = 'ViolinPlotSeries'; diff --git a/packages/xy-chart/src/utils/propShapes.js b/packages/xy-chart/src/utils/propShapes.js index 3a370b07..9de86d9a 100644 --- a/packages/xy-chart/src/utils/propShapes.js +++ b/packages/xy-chart/src/utils/propShapes.js @@ -18,6 +18,23 @@ export const scaleShape = PropTypes.shape({ domain: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])), }); +export const boxPlotSeriesDataShape = PropTypes.arrayOf( + PropTypes.shape({ + x: PropTypes.oneOfType([ // data with null x/y are not rendered + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Date), + PropTypes.object, // eg a moment() instance + ]), + median: PropTypes.number.isRequired, + min: PropTypes.number.isRequired, + max: PropTypes.number.isRequired, + firstQuartile: PropTypes.number.isRequired, + thirdQuartile: PropTypes.number.isRequired, + outliers: PropTypes.array.isRequired, + }), +); + export const lineSeriesDataShape = PropTypes.arrayOf( PropTypes.shape({ x: PropTypes.oneOfType([ // data with null x/y are not rendered From 1f0321c183fdf4382bae599fad6f0c4a8c8c0391 Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Tue, 14 Nov 2017 18:51:40 -0800 Subject: [PATCH 2/9] added test files and exposed computeStats --- packages/xy-chart/src/index.js | 1 + .../xy-chart/src/series/BoxPlotSeries.jsx | 3 +- .../xy-chart/src/series/ViolinPlotSeries.jsx | 7 +++- packages/xy-chart/src/utils/propShapes.js | 13 +++++++ packages/xy-chart/test/BoxPlotSeries.test.js | 38 +++++++++++++++++++ .../xy-chart/test/ViolinPlotSeries.test.js | 38 +++++++++++++++++++ 6 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 packages/xy-chart/test/BoxPlotSeries.test.js create mode 100644 packages/xy-chart/test/ViolinPlotSeries.test.js diff --git a/packages/xy-chart/src/index.js b/packages/xy-chart/src/index.js index 1857bbee..a21aaebb 100644 --- a/packages/xy-chart/src/index.js +++ b/packages/xy-chart/src/index.js @@ -13,6 +13,7 @@ export { default as StackedAreaSeries } from './series/StackedAreaSeries'; export { default as StackedBarSeries } from './series/StackedBarSeries'; export { default as BoxPlotSeries } from './series/BoxPlotSeries'; export { default as ViolinPlotSeries } from './series/ViolinPlotSeries'; +export { computeStats } from '@vx/stats'; export { default as HorizontalReferenceLine } from './annotation/HorizontalReferenceLine'; export { default as CrossHair } from './chart/CrossHair'; diff --git a/packages/xy-chart/src/series/BoxPlotSeries.jsx b/packages/xy-chart/src/series/BoxPlotSeries.jsx index bf71e9d1..7ea34fde 100644 --- a/packages/xy-chart/src/series/BoxPlotSeries.jsx +++ b/packages/xy-chart/src/series/BoxPlotSeries.jsx @@ -93,8 +93,7 @@ export default function BoxPlotSeries({ horizontal={horizontal} /> ) - )) - } + ))} ); } diff --git a/packages/xy-chart/src/series/ViolinPlotSeries.jsx b/packages/xy-chart/src/series/ViolinPlotSeries.jsx index 8f4a0b82..579bc832 100644 --- a/packages/xy-chart/src/series/ViolinPlotSeries.jsx +++ b/packages/xy-chart/src/series/ViolinPlotSeries.jsx @@ -6,8 +6,11 @@ import themeColors from '@data-ui/theme/build/color'; import { callOrValue } from '../utils/chartUtils'; +import { violinPlotSeriesDataShape } from '../utils/propShapes'; + + const propTypes = { - data: PropTypes.object.isRequired, + data: violinPlotSeriesDataShape.isRequired, label: PropTypes.string.isRequired, // attributes on data points will override these @@ -64,7 +67,7 @@ export default function ViolinPlotSeries({ {data.map((d, i) => ( ', () => { + const mockProps = { + xScale: { type: 'band', paddingInner: 0.15, paddingOuter: 0.3 }, + yScale: { type: 'linear', includeZero: false }, + width: 100, + height: 100, + margin: { top: 10, right: 10, bottom: 10, left: 10 }, + ariaLabel: 'label', + }; + + const mockData = [1, 2, 3, 4, 5, 5, 5, 5, 5, 6, 9, 5, 1]; + const mockStats = computeStats(mockData); + + + test('it should be defined', () => { + expect(BoxPlotSeries).toBeDefined(); + }); + + test('it should not render without x- and y-scales', () => { + expect(shallow().type()).toBeNull(); + }); + + test('it should render only one boxplot', () => { + const wrapper = shallow( + + + , + ); + expect(wrapper.find(BoxPlotSeries).length).toBe(1); + expect(wrapper.find(BoxPlotSeries).first().dive().find(BoxPlot).length).toBe(1); + }); +}); diff --git a/packages/xy-chart/test/ViolinPlotSeries.test.js b/packages/xy-chart/test/ViolinPlotSeries.test.js new file mode 100644 index 00000000..1723f5e3 --- /dev/null +++ b/packages/xy-chart/test/ViolinPlotSeries.test.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { ViolinPlot } from '@vx/stats'; + +import { XYChart, ViolinPlotSeries, computeStats } from '../src/'; + +describe('', () => { + const mockProps = { + xScale: { type: 'band', paddingInner: 0.15, paddingOuter: 0.3 }, + yScale: { type: 'linear', includeZero: false }, + width: 100, + height: 100, + margin: { top: 10, right: 10, bottom: 10, left: 10 }, + ariaLabel: 'label', + }; + + const mockData = [1, 2, 3, 4, 5, 5, 5, 5, 5, 6, 9, 5, 1]; + const mockStats = computeStats(mockData); + + + test('it should be defined', () => { + expect(ViolinPlotSeries).toBeDefined(); + }); + + test('it should not render without x- and y-scales', () => { + expect(shallow().type()).toBeNull(); + }); + + test('it should render only one boxplot', () => { + const wrapper = shallow( + + + , + ); + expect(wrapper.find(ViolinPlotSeries).length).toBe(1); + expect(wrapper.find(ViolinPlotSeries).first().dive().find(ViolinPlot).length).toBe(1); + }); +}); From 83f810abfd147cdec500d87c0cdc3c3fde4255a6 Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Tue, 14 Nov 2017 18:55:11 -0800 Subject: [PATCH 3/9] fixed lint in demo --- packages/demo/examples/01-xy-chart/index.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/demo/examples/01-xy-chart/index.jsx b/packages/demo/examples/01-xy-chart/index.jsx index cca2c467..03734b5a 100644 --- a/packages/demo/examples/01-xy-chart/index.jsx +++ b/packages/demo/examples/01-xy-chart/index.jsx @@ -669,12 +669,12 @@ export default { showYGrid > From ff4097e817942219336e6bb003dc33e6ae1b012e Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Wed, 15 Nov 2017 08:31:52 -0800 Subject: [PATCH 4/9] added renderTooltip support for boxplot --- packages/demo/examples/01-xy-chart/index.jsx | 4 ++++ packages/xy-chart/src/series/BoxPlotSeries.jsx | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/demo/examples/01-xy-chart/index.jsx b/packages/demo/examples/01-xy-chart/index.jsx index 03734b5a..45b899b2 100644 --- a/packages/demo/examples/01-xy-chart/index.jsx +++ b/packages/demo/examples/01-xy-chart/index.jsx @@ -493,6 +493,7 @@ export default { }, { description: 'Box Plot Example', + components: [BoxPlotSeries], example: () => { const boxPlotData = statsData.map(s => s.boxPlot); const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); @@ -534,6 +535,7 @@ export default { }, { description: 'Single Horizontal Box Plot Example', + components: [BoxPlotSeries], example: () => { const singleStats = [statsData[0]]; const boxPlotData = singleStats.map((s) => { @@ -583,6 +585,7 @@ export default { }, { description: 'Horizontal BoxPlot With ViolinPlot Example', + components: [BoxPlotSeries, ViolinPlotSeries], example: () => { const boxPlotData = statsData.map((s) => { const { boxPlot } = s; @@ -646,6 +649,7 @@ export default { }, { description: 'BoxPlot With ViolinPlot Example', + components: [BoxPlotSeries, ViolinPlotSeries], example: () => { const boxPlotData = statsData.map(s => s.boxPlot); const violinData = statsData.map(s => ({ x: s.boxPlot.x, binData: s.binData })); diff --git a/packages/xy-chart/src/series/BoxPlotSeries.jsx b/packages/xy-chart/src/series/BoxPlotSeries.jsx index 7ea34fde..fb4cb07f 100644 --- a/packages/xy-chart/src/series/BoxPlotSeries.jsx +++ b/packages/xy-chart/src/series/BoxPlotSeries.jsx @@ -23,6 +23,8 @@ const propTypes = { yScale: PropTypes.func, horizontal: PropTypes.bool, widthRatio: PropTypes.number, + onMouseMove: PropTypes.func, + onMouseLeave: PropTypes.func, }; const defaultProps = { @@ -35,6 +37,8 @@ const defaultProps = { yScale: null, horizontal: false, widthRatio: 1, + onMouseMove: undefined, + onMouseLeave: undefined, }; const MAX_BOX_WIDTH = 50; @@ -58,6 +62,8 @@ export default function BoxPlotSeries({ horizontal, widthRatio, fillOpacity, + onMouseMove, + onMouseLeave, }) { if (!xScale || !yScale) return null; const offsetScale = horizontal ? yScale : xScale; @@ -91,6 +97,13 @@ export default function BoxPlotSeries({ fillOpacity={d.fillOpacity || callOrValue(fillOpacity, d, i)} valueScale={valueScale} horizontal={horizontal} + boxProps={{ + onMouseMove: onMouseMove && (() => (event) => { + onMouseMove({ event, data, datum: d }); + }), + onMouseLeave: onMouseLeave && (() => onMouseLeave), + }} + /> ) ))} From 6fbc44249832bc0e02ebdfddfbb03a4809746ca3 Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Wed, 15 Nov 2017 10:35:41 -0800 Subject: [PATCH 5/9] move box plot example to a separate file --- .../01-xy-chart/StatsSeriesExample.jsx | 268 ++++++++++++++++++ packages/demo/examples/01-xy-chart/index.jsx | 215 ++------------ 2 files changed, 286 insertions(+), 197 deletions(-) create mode 100644 packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx diff --git a/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx b/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx new file mode 100644 index 00000000..123004c6 --- /dev/null +++ b/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx @@ -0,0 +1,268 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import { + XAxis, + YAxis, + BoxPlotSeries, + ViolinPlotSeries, + PatternLines, + LinearGradient, + theme, +} from '@data-ui/xy-chart'; + +import ResponsiveXYChart from './ResponsiveXYChart'; + +import { statsData } from './data'; + +const { colors } = theme; + +function renderBoxPlotTooltip({ datum, color }) { + const { + x, + y, + min, + max, + median, + firstQuartile, + thirdQuartile, + outliers, + } = datum; + + const label = x || y; + return ( +
+
+ {label} +
+
+ Min + {min && min.toFixed ? min.toFixed(2) : min} +
+
+ Max + {max && max.toFixed ? max.toFixed(2) : max} +
+
+ Median + {median && median.toFixed ? median.toFixed(2) : median} +
+
+ First Quartile + {firstQuartile && firstQuartile.toFixed ? firstQuartile.toFixed(2) : firstQuartile} +
+
+ Third Quartile + {thirdQuartile && thirdQuartile.toFixed ? thirdQuartile.toFixed(2) : thirdQuartile} +
+
+ Outliers Number + {outliers.length} +
+
+ ); +} + +export function SimpleBoxPlotSeriesExample() { + const boxPlotData = statsData.map(s => s.boxPlot); + const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); + const minYValue = Math.min(...values); + const maxYValue = Math.max(...values); + const yDomain = [minYValue - (0.1 * Math.abs(minYValue)), + maxYValue + (0.1 * Math.abs(minYValue))]; + return ( + + + + + + + ); +} + +export function SingleBoxPlotSeriesExample() { + const singleStats = [statsData[0]]; + const boxPlotData = singleStats.map((s) => { + const { boxPlot } = s; + const { x, ...rest } = boxPlot; + return { + y: x, + ...rest, + }; + }); + const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); + const minXValue = Math.min(...values); + const maxXValue = Math.max(...values); + const xDomain = [minXValue - (0.1 * Math.abs(minXValue)), + maxXValue + (0.1 * Math.abs(maxXValue))]; + return ( + + + + + + ); +} + +export function HorizontalBoxPlotViolinPlotSeriesExample() { + const boxPlotData = statsData.map((s) => { + const { boxPlot } = s; + const { x, ...rest } = boxPlot; + return { + y: x, + ...rest, + }; + }); + const violinData = statsData.map(s => ({ y: s.boxPlot.x, binData: s.binData })); + const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); + const minXValue = Math.min(...values); + const maxXValue = Math.max(...values); + const xDomain = [minXValue - (0.1 * Math.abs(minXValue)), + maxXValue + (0.1 * Math.abs(maxXValue))]; + return ( + + + + + + + + ); +} + +export function BoxPlotViolinPlotSeriesExample() { + const boxPlotData = statsData.map(s => s.boxPlot); + const violinData = statsData.map(s => ({ x: s.boxPlot.x, binData: s.binData })); + const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); + const minYValue = Math.min(...values); + const maxYValue = Math.max(...values); + const yDomain = [minYValue - (0.1 * Math.abs(minYValue)), + maxYValue + (0.1 * Math.abs(minYValue))]; + return ( + + + + + + + + ); +} diff --git a/packages/demo/examples/01-xy-chart/index.jsx b/packages/demo/examples/01-xy-chart/index.jsx index 45b899b2..c911ae1b 100644 --- a/packages/demo/examples/01-xy-chart/index.jsx +++ b/packages/demo/examples/01-xy-chart/index.jsx @@ -32,6 +32,12 @@ import RectPointComponent from './RectPointComponent'; import ResponsiveXYChart, { dateFormatter } from './ResponsiveXYChart'; import StackedAreaExample from './StackedAreaExample'; import ScatterWithHistogram from './ScatterWithHistograms'; +import { + SimpleBoxPlotSeriesExample, + SingleBoxPlotSeriesExample, + HorizontalBoxPlotViolinPlotSeriesExample, + BoxPlotViolinPlotSeriesExample, +} from './StatsSeriesExample'; import { circlePackData, @@ -45,7 +51,6 @@ import { intervalData, temperatureBands, priceBandData, - statsData, } from './data'; import WithToggle from '../shared/WithToggle'; @@ -494,214 +499,30 @@ export default { { description: 'Box Plot Example', components: [BoxPlotSeries], - example: () => { - const boxPlotData = statsData.map(s => s.boxPlot); - const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); - const minYValue = Math.min(...values); - const maxYValue = Math.max(...values); - const yDomain = [minYValue - (0.1 * Math.abs(minYValue)), - maxYValue + (0.1 * Math.abs(minYValue))]; - return ( - - - - - - - ); - }, + example: () => ( + + ), }, { description: 'Single Horizontal Box Plot Example', components: [BoxPlotSeries], - example: () => { - const singleStats = [statsData[0]]; - const boxPlotData = singleStats.map((s) => { - const { boxPlot } = s; - const { x, ...rest } = boxPlot; - return { - y: x, - ...rest, - }; - }); - const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); - const minXValue = Math.min(...values); - const maxXValue = Math.max(...values); - const xDomain = [minXValue - (0.1 * Math.abs(minXValue)), - maxXValue + (0.1 * Math.abs(maxXValue))]; - return ( - - - - - - ); - }, + example: () => ( + + ), }, { description: 'Horizontal BoxPlot With ViolinPlot Example', components: [BoxPlotSeries, ViolinPlotSeries], - example: () => { - const boxPlotData = statsData.map((s) => { - const { boxPlot } = s; - const { x, ...rest } = boxPlot; - return { - y: x, - ...rest, - }; - }); - const violinData = statsData.map(s => ({ y: s.boxPlot.x, binData: s.binData })); - const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); - const minXValue = Math.min(...values); - const maxXValue = Math.max(...values); - const xDomain = [minXValue - (0.1 * Math.abs(minXValue)), - maxXValue + (0.1 * Math.abs(maxXValue))]; - return ( - - - - - - - - ); - }, + example: () => ( + + ), }, { description: 'BoxPlot With ViolinPlot Example', components: [BoxPlotSeries, ViolinPlotSeries], - example: () => { - const boxPlotData = statsData.map(s => s.boxPlot); - const violinData = statsData.map(s => ({ x: s.boxPlot.x, binData: s.binData })); - const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); - const minYValue = Math.min(...values); - const maxYValue = Math.max(...values); - const yDomain = [minYValue - (0.1 * Math.abs(minYValue)), - maxYValue + (0.1 * Math.abs(minYValue))]; - return ( - - - - - - - - ); - }, + example: () => ( + + ), }, { description: 'XAxis, YAxis -- orientation', From beaa221ada526a3147c68d5afac98ee6dabdc8fc Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Wed, 15 Nov 2017 12:31:26 -0800 Subject: [PATCH 6/9] removed label props and used absolute import --- packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx | 6 ------ packages/xy-chart/src/series/BoxPlotSeries.jsx | 8 +++----- packages/xy-chart/src/series/ViolinPlotSeries.jsx | 8 +++----- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx b/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx index 123004c6..d1814dd3 100644 --- a/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx +++ b/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx @@ -93,7 +93,6 @@ export function SimpleBoxPlotSeriesExample() { d.outliers || []; export default function BoxPlotSeries({ data, - label, fill, stroke, strokeWidth, @@ -78,7 +76,7 @@ export default function BoxPlotSeries({ (((1 - widthRatio) / 2) * actualyWidth), }); return ( - + {data.map((d, i) => ( isDefined(min(d)) && ( d.y; export default function ViolinPlotSeries({ data, - label, fill, stroke, strokeWidth, @@ -64,7 +62,7 @@ export default function ViolinPlotSeries({ (((1 - widthRatio) / 2) * actualyWidth), }); return ( - + {data.map((d, i) => ( Date: Wed, 15 Nov 2017 12:37:03 -0800 Subject: [PATCH 7/9] fixed typos --- packages/xy-chart/src/series/BoxPlotSeries.jsx | 11 +++++------ packages/xy-chart/src/series/ViolinPlotSeries.jsx | 10 +++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/xy-chart/src/series/BoxPlotSeries.jsx b/packages/xy-chart/src/series/BoxPlotSeries.jsx index 35f07e41..a90292bc 100644 --- a/packages/xy-chart/src/series/BoxPlotSeries.jsx +++ b/packages/xy-chart/src/series/BoxPlotSeries.jsx @@ -28,7 +28,7 @@ const propTypes = { const defaultProps = { boxWidth: null, - stroke: '#000000', + stroke: themeColors.darkGray, strokeWidth: 2, fill: themeColors.default, fillOpacity: 1, @@ -68,12 +68,12 @@ export default function BoxPlotSeries({ const offsetValue = horizontal ? y : x; const valueScale = horizontal ? xScale : yScale; const boxWidth = offsetScale.bandwidth(); - const actualyWidth = Math.min(MAX_BOX_WIDTH, boxWidth); - const offset = (offsetScale.offset || 0) - ((boxWidth - actualyWidth) / 2); + const actualWidth = Math.min(MAX_BOX_WIDTH, boxWidth); + const offset = (offsetScale.offset || 0) - ((boxWidth - actualWidth) / 2); const offsetPropName = horizontal ? 'top' : 'left'; const offsetProp = d => ({ [offsetPropName]: (offsetScale(offsetValue(d)) - offset) + - (((1 - widthRatio) / 2) * actualyWidth), + (((1 - widthRatio) / 2) * actualWidth), }); return ( @@ -87,7 +87,7 @@ export default function BoxPlotSeries({ firstQuartile={firstQuartile(d)} thirdQuartile={thirdQuartile(d)} median={median(d)} - boxWidth={actualyWidth * widthRatio} + boxWidth={actualWidth * widthRatio} outliers={outliers(d)} fill={d.fill || callOrValue(fill, d, i)} stroke={d.stroke || callOrValue(stroke, d, i)} @@ -101,7 +101,6 @@ export default function BoxPlotSeries({ }), onMouseLeave: onMouseLeave && (() => onMouseLeave), }} - /> ) ))} diff --git a/packages/xy-chart/src/series/ViolinPlotSeries.jsx b/packages/xy-chart/src/series/ViolinPlotSeries.jsx index 27cb5f01..e700f176 100644 --- a/packages/xy-chart/src/series/ViolinPlotSeries.jsx +++ b/packages/xy-chart/src/series/ViolinPlotSeries.jsx @@ -26,7 +26,7 @@ const propTypes = { const defaultProps = { boxWidth: null, - stroke: '#000000', + stroke: themeColors.darkGray, strokeWidth: 2, fill: themeColors.default, xScale: null, @@ -54,12 +54,12 @@ export default function ViolinPlotSeries({ const offsetValue = horizontal ? y : x; const valueScale = horizontal ? xScale : yScale; const boxWidth = offsetScale.bandwidth(); - const actualyWidth = Math.min(MAX_BOX_WIDTH, boxWidth); - const offset = (offsetScale.offset || 0) - ((boxWidth - actualyWidth) / 2); + const actualWidth = Math.min(MAX_BOX_WIDTH, boxWidth); + const offset = (offsetScale.offset || 0) - ((boxWidth - actualWidth) / 2); const offsetPropName = horizontal ? 'top' : 'left'; const offsetProp = d => ({ [offsetPropName]: (offsetScale(offsetValue(d)) - offset) + - (((1 - widthRatio) / 2) * actualyWidth), + (((1 - widthRatio) / 2) * actualWidth), }); return ( @@ -68,7 +68,7 @@ export default function ViolinPlotSeries({ key={offsetValue(d)} {...offsetProp(d)} binData={d.binData} - width={actualyWidth * widthRatio} + width={actualWidth * widthRatio} fill={d.fill || callOrValue(fill, d, i)} stroke={d.stroke || callOrValue(stroke, d, i)} strokeWidth={d.strokeWidth || callOrValue(strokeWidth, d, i)} From bdb21a6a4b59d50aa50dee09024534fa02a5251e Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Wed, 15 Nov 2017 12:57:35 -0800 Subject: [PATCH 8/9] added rendertooltip for violinplot and added disableMouseEvent prop --- .../01-xy-chart/StatsSeriesExample.jsx | 28 ++++++++++++------- packages/demo/examples/01-xy-chart/index.jsx | 8 +++--- .../xy-chart/src/series/BoxPlotSeries.jsx | 13 +++++++-- .../xy-chart/src/series/ViolinPlotSeries.jsx | 19 +++++++++++++ 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx b/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx index d1814dd3..06758f93 100644 --- a/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx +++ b/packages/demo/examples/01-xy-chart/StatsSeriesExample.jsx @@ -17,6 +17,21 @@ import { statsData } from './data'; const { colors } = theme; +function renderViolinPlotTooltip({ datum, color }) { + const { x, y, binData } = datum; + const label = x || y; + return ( +
+
+ {label} +
+
+ Bin Number + {binData.length} +
+
+ ); +} function renderBoxPlotTooltip({ datum, color }) { const { x, @@ -194,6 +209,7 @@ export function HorizontalBoxPlotViolinPlotSeriesExample() { stroke="#22b8cf" strokeWidth={0.5} horizontal + disableMouseEvents /> s.boxPlot); const violinData = statsData.map(s => ({ x: s.boxPlot.x, binData: s.binData })); const values = boxPlotData.reduce((r, e) => r.push(e.min, e.max, ...e.outliers) && r, []); @@ -229,7 +245,7 @@ export function BoxPlotViolinPlotSeriesExample() { yScale={{ type: 'linear', domain: yDomain, }} - renderTooltip={renderBoxPlotTooltip} + renderTooltip={renderViolinPlotTooltip} showYGrid > - ); diff --git a/packages/demo/examples/01-xy-chart/index.jsx b/packages/demo/examples/01-xy-chart/index.jsx index c911ae1b..dd14dad7 100644 --- a/packages/demo/examples/01-xy-chart/index.jsx +++ b/packages/demo/examples/01-xy-chart/index.jsx @@ -36,7 +36,7 @@ import { SimpleBoxPlotSeriesExample, SingleBoxPlotSeriesExample, HorizontalBoxPlotViolinPlotSeriesExample, - BoxPlotViolinPlotSeriesExample, + ViolinPlotSeriesExample, } from './StatsSeriesExample'; import { @@ -518,10 +518,10 @@ export default { ), }, { - description: 'BoxPlot With ViolinPlot Example', - components: [BoxPlotSeries, ViolinPlotSeries], + description: 'ViolinPlot Example', + components: [ViolinPlotSeries], example: () => ( - + ), }, { diff --git a/packages/xy-chart/src/series/BoxPlotSeries.jsx b/packages/xy-chart/src/series/BoxPlotSeries.jsx index a90292bc..b30255ba 100644 --- a/packages/xy-chart/src/series/BoxPlotSeries.jsx +++ b/packages/xy-chart/src/series/BoxPlotSeries.jsx @@ -12,6 +12,7 @@ const propTypes = { data: boxPlotSeriesDataShape.isRequired, // attributes on data points will override these + disableMouseEvents: PropTypes.bool, fill: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), stroke: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), strokeWidth: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), @@ -22,6 +23,7 @@ const propTypes = { yScale: PropTypes.func, horizontal: PropTypes.bool, widthRatio: PropTypes.number, + onClick: PropTypes.func, onMouseMove: PropTypes.func, onMouseLeave: PropTypes.func, }; @@ -36,8 +38,10 @@ const defaultProps = { yScale: null, horizontal: false, widthRatio: 1, + disableMouseEvents: false, onMouseMove: undefined, onMouseLeave: undefined, + onClick: undefined, }; const MAX_BOX_WIDTH = 50; @@ -62,6 +66,8 @@ export default function BoxPlotSeries({ fillOpacity, onMouseMove, onMouseLeave, + disableMouseEvents, + onClick, }) { if (!xScale || !yScale) return null; const offsetScale = horizontal ? yScale : xScale; @@ -96,10 +102,13 @@ export default function BoxPlotSeries({ valueScale={valueScale} horizontal={horizontal} boxProps={{ - onMouseMove: onMouseMove && (() => (event) => { + onMouseMove: disableMouseEvents ? null : onMouseMove && (() => (event) => { onMouseMove({ event, data, datum: d }); }), - onMouseLeave: onMouseLeave && (() => onMouseLeave), + onMouseLeave: disableMouseEvents ? null : onMouseLeave && (() => onMouseLeave), + onClick: disableMouseEvents ? null : onClick && (() => (event) => { + onClick({ event, data, datum: d, index: i }); + }), }} /> ) diff --git a/packages/xy-chart/src/series/ViolinPlotSeries.jsx b/packages/xy-chart/src/series/ViolinPlotSeries.jsx index e700f176..4f3a219e 100644 --- a/packages/xy-chart/src/series/ViolinPlotSeries.jsx +++ b/packages/xy-chart/src/series/ViolinPlotSeries.jsx @@ -22,6 +22,10 @@ const propTypes = { yScale: PropTypes.func, horizontal: PropTypes.bool, widthRatio: PropTypes.number, + disableMouseEvents: PropTypes.bool, + onMouseMove: PropTypes.func, + onMouseLeave: PropTypes.func, + onClick: PropTypes.func, }; const defaultProps = { @@ -33,6 +37,10 @@ const defaultProps = { yScale: null, horizontal: false, widthRatio: 1, + disableMouseEvents: false, + onMouseMove: undefined, + onMouseLeave: undefined, + onClick: undefined, }; const MAX_BOX_WIDTH = 50; @@ -48,6 +56,10 @@ export default function ViolinPlotSeries({ yScale, horizontal, widthRatio, + disableMouseEvents, + onMouseMove, + onMouseLeave, + onClick, }) { if (!xScale || !yScale) return null; const offsetScale = horizontal ? yScale : xScale; @@ -74,6 +86,13 @@ export default function ViolinPlotSeries({ strokeWidth={d.strokeWidth || callOrValue(strokeWidth, d, i)} valueScale={valueScale} horizontal={horizontal} + onMouseMove={disableMouseEvents ? null : onMouseMove && (() => (event) => { + onMouseMove({ event, data, datum: d }); + })} + onMouseLeave={disableMouseEvents ? null : onMouseLeave && (() => onMouseLeave)} + onClick={disableMouseEvents ? null : onClick && (() => (event) => { + onClick({ event, data, datum: d, index: i }); + })} /> )) } From eaf230d008215c6f1ba1d60d7e74f590673edfc5 Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Wed, 15 Nov 2017 13:15:04 -0800 Subject: [PATCH 9/9] updated readme --- packages/xy-chart/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/xy-chart/README.md b/packages/xy-chart/README.md index dd06da6d..ab461a33 100644 --- a/packages/xy-chart/README.md +++ b/packages/xy-chart/README.md @@ -171,6 +171,9 @@ Series | supported x scale type | supported y scale types | data shape | voronoi `` | band | linear | `{ x, y }` (colors controlled with groupFills & groupKeys) | no `` | time, linear | y is computed | `{ x [, size] }` | no `` | time, linear | linear | `{ x0, x1 [, fill, stroke] }` | no +`` | linear, band | band, linear | `{ x (or y), min, max, median, firstQuartile, thirdQuartile, outliers [, fill, stroke] }` | no +`` | linear, band | band, linear | `{ x (or y), binData [, fill, stroke] }` | no + \* The y boundaries of the `` may be specified by either - defined `y0` and `y1` values or