diff --git a/packages/demo/examples/01-xy-chart/HorizontalBarChart.jsx b/packages/demo/examples/01-xy-chart/HorizontalBarChart.jsx new file mode 100644 index 00000000..1f839428 --- /dev/null +++ b/packages/demo/examples/01-xy-chart/HorizontalBarChart.jsx @@ -0,0 +1,127 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; +import { timeParse, timeFormat } from 'd3-time-format'; + +import { + XYChart, + CrossHair, + XAxis, + YAxis, + theme, + withScreenSize, + BarSeries, + PatternLines, +} from '@data-ui/xy-chart'; + +import colors, { allColors } from '@data-ui/theme/lib/color'; + +import { timeSeriesData } from './data'; + +export const parseDate = timeParse('%Y%m%d'); +export const formatDate = timeFormat('%b %d'); +export const formatYear = timeFormat('%Y'); +export const dateFormatter = date => formatYear(parseDate(date)); + +const categoryHorizontalData = timeSeriesData.map((d, i) => ({ + x: d.y, + y: i + 1, +})); + +const categoryData = timeSeriesData.map((d, i) => ({ + x: i + 1, + y: d.y, +})); + +class HorizontalBarChartExample extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + direction: 'horizontal', + }; + } + + renderControls() { + return ( +
+
+ Direction: + + +
+
+ ); + } + + render() { + const { screenWidth } = this.props; + const { direction } = this.state; + const categoryScale = { type: 'band', paddingInner: 0.15 }; + const valueScale = { type: 'linear' }; + const horizontal = direction === 'horizontal'; + + return ( +
+ {this.renderControls()} + + + + + + + + +
+ ); + } +} + +export default withScreenSize(HorizontalBarChartExample); diff --git a/packages/demo/examples/01-xy-chart/index.jsx b/packages/demo/examples/01-xy-chart/index.jsx index 3dfebf86..7637c8ad 100644 --- a/packages/demo/examples/01-xy-chart/index.jsx +++ b/packages/demo/examples/01-xy-chart/index.jsx @@ -37,6 +37,7 @@ import AreaDifferenceSeriesExample from './AreaDifferenceSeriesExample'; import { BoxPlotSeriesExample, BoxPlotViolinPlotSeriesExample } from './StatsSeriesExample'; import BrushableLineChart from './BrushableLineChart'; import BrushableLinkedLineCharts from './BrushableLinkedLineCharts'; +import HorizontalBarChartExample from './HorizontalBarChart'; import { circlePackData, @@ -102,6 +103,11 @@ export default { ), }, + { + description: 'HorizontalBarChartExample', + components: [BarSeries, CrossHair], + example: () => , + }, { description: 'LineSeries', components: [LineSeries, CrossHair], diff --git a/packages/xy-chart/src/chart/XYChart.jsx b/packages/xy-chart/src/chart/XYChart.jsx index ad523d08..57dfd4fd 100644 --- a/packages/xy-chart/src/chart/XYChart.jsx +++ b/packages/xy-chart/src/chart/XYChart.jsx @@ -268,7 +268,6 @@ class XYChart extends React.PureComponent { } = this.state; const { numXTicks, numYTicks } = this.getNumTicks(innerWidth, innerHeight); - const barWidth = xScale.barWidth || (xScale.bandwidth && xScale.bandwidth()) || 0; const CrossHairs = []; // ensure these are the top-most layer let Brush = null; let xAxisOrientation; @@ -329,7 +328,6 @@ class XYChart extends React.PureComponent { return React.cloneElement(Child, { xScale, yScale, - barWidth, onClick: Child.props.onClick || (Child.props.disableMouseEvents ? undefined : this.handleClick), @@ -407,7 +405,9 @@ class XYChart extends React.PureComponent { left: xScale(getX(tooltipData.datum) || 0) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0), - top: yScale(getY(tooltipData.datum) || 0), + top: + yScale(getY(tooltipData.datum) || 0) + + (yScale.bandwidth ? yScale.bandwidth() / 2 : 0), xScale, yScale, }), diff --git a/packages/xy-chart/src/series/BarSeries.jsx b/packages/xy-chart/src/series/BarSeries.jsx index 38c83d61..880dd721 100644 --- a/packages/xy-chart/src/series/BarSeries.jsx +++ b/packages/xy-chart/src/series/BarSeries.jsx @@ -11,20 +11,20 @@ import sharedSeriesProps from '../utils/sharedSeriesProps'; const propTypes = { ...sharedSeriesProps, - barWidth: PropTypes.number, data: barSeriesDataShape.isRequired, fill: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), fillOpacity: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), stroke: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), strokeWidth: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), + horizontal: PropTypes.bool, }; const defaultProps = { - barWidth: null, fill: themeColors.default, fillOpacity: null, stroke: '#FFFFFF', strokeWidth: 1, + horizontal: false, }; const x = d => d.x; @@ -34,7 +34,6 @@ const noEventsStyles = { pointerEvents: 'none' }; export default class BarSeries extends React.PureComponent { render() { const { - barWidth, data, disableMouseEvents, fill, @@ -46,24 +45,32 @@ export default class BarSeries extends React.PureComponent { onClick, onMouseMove, onMouseLeave, + horizontal, } = this.props; + if (!xScale || !yScale) return null; + const valueScale = horizontal ? xScale : yScale; + const categoryScale = horizontal ? yScale : xScale; + const barWidth = + categoryScale.barWidth || (categoryScale.bandwidth && categoryScale.bandwidth()) || 0; + const valueField = horizontal ? x : y; + const categoryField = horizontal ? y : x; - if (!xScale || !yScale || !barWidth) return null; - - const maxHeight = (yScale.range() || [0])[0]; - const offset = xScale.offset || 0; + const maxBarLength = Math.max(...valueScale.range()); + const offset = categoryScale.offset || 0; return ( {data.map((d, i) => { - const barHeight = maxHeight - yScale(y(d)); + const barLength = horizontal + ? valueScale(valueField(d)) + : maxBarLength - valueScale(valueField(d)); const color = d.fill || callOrValue(fill, d, i); - const barX = xScale(x(d)) - offset; + const barPosition = categoryScale(categoryField(d)) - offset; return ( - isDefined(d.y) && ( + isDefined(horizontal ? d.x : d.y) && ( d && d.x; const xString = d => getX(d).toString(); +const getY = d => d && d.y; +const yString = d => getY(d).toString(); export default function collectScalesFromProps(props) { const { xScale: xScaleObject, yScale: yScaleObject, children } = props; @@ -42,20 +44,26 @@ export default function collectScalesFromProps(props) { Children.forEach(children, Child => { // Child-specific scales or adjustments here const name = componentName(Child); - if (isBarSeries(name) && xScaleObject.type !== 'band') { - const dummyBand = getScaleForAccessor({ - allData, - minAccessor: xString, - maxAccessor: xString, - type: 'band', - rangeRound: [0, innerWidth], - paddingOuter: 1, - }); + if (isBarSeries(name)) { + const { horizontal } = Child.props; + const categoryScaleObject = horizontal ? yScaleObject : xScaleObject; + if (categoryScaleObject.type !== 'band') { + const categoryScale = horizontal ? yScale : xScale; + const range = horizontal ? innerHeight : innerWidth; + const dummyBand = getScaleForAccessor({ + allData, + minAccessor: horizontal ? yString : xString, + maxAccessor: horizontal ? yString : xString, + type: 'band', + rangeRound: [0, range], + paddingOuter: 1, + }); - const offset = dummyBand.bandwidth() / 2; - xScale.range([offset, innerWidth - offset]); - xScale.barWidth = dummyBand.bandwidth(); - xScale.offset = offset; + const offset = dummyBand.bandwidth() / 2; + categoryScale.range([offset, range - offset]); + categoryScale.barWidth = dummyBand.bandwidth(); + categoryScale.offset = offset; + } } if (isCirclePackSeries(name)) { yScale.domain([-innerHeight / 2, innerHeight / 2]); diff --git a/packages/xy-chart/test/series/BarSeries.test.js b/packages/xy-chart/test/series/BarSeries.test.js index e2296910..c9be42f5 100644 --- a/packages/xy-chart/test/series/BarSeries.test.js +++ b/packages/xy-chart/test/series/BarSeries.test.js @@ -62,6 +62,60 @@ describe('', () => { expect(barSeries.find(Bar)).toHaveLength(mockData.length - 1); }); + it('should render bar width correctly for horizontal barchart', () => { + const maxWidth = 500; + const maxHeight = 10; + const wrapper = shallow( + + ({ + x: mockData.length - i, + y: d.date, + }))} + horizontal + /> + , + ); + const barSeries = wrapper.find(BarSeries).dive(); + expect( + barSeries + .find(Bar) + .first() + .props().width, + ).toBe(maxWidth); + }); + + it('should not render bars for null data for horizontal barchart', () => { + const wrapper = shallow( + + ({ + x: i === 0 ? null : d.num, + y: d.date, + }))} + horizontal + /> + , + ); + const barSeries = wrapper.find(BarSeries).dive(); + expect(barSeries.find(Bar)).toHaveLength(mockData.length - 1); + }); + it('should work with time or band scales', () => { const timeWrapper = shallow(