Skip to content

Commit

Permalink
Merge pull request #127 from conglei/conglei_horizontal_bar
Browse files Browse the repository at this point in the history
Conglei horizontal bar
  • Loading branch information
conglei committed Aug 29, 2018
2 parents 9b95775 + fb8e9d4 commit 4f8b295
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 31 deletions.
127 changes: 127 additions & 0 deletions packages/demo/examples/01-xy-chart/HorizontalBarChart.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="bar-demo--form">
<div>
Direction:
<label>
<input
type="radio"
value="horizontal"
onChange={e => this.setState({ direction: e.target.value })}
checked={this.state.direction === 'horizontal'}
/>{' '}
horizonal
</label>
<label>
<input
type="radio"
value="vertical"
onChange={e => this.setState({ direction: e.target.value })}
checked={this.state.direction !== 'horizontal'}
/>{' '}
vertical
</label>
</div>
</div>
);
}

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 (
<div className="horizontal-bar-demo">
{this.renderControls()}
<XYChart
theme={theme}
width={Math.min(700, screenWidth / 1.5)}
height={Math.min(700 / 2, screenWidth / 1.5 / 2)}
ariaLabel="Required label"
xScale={horizontal ? valueScale : categoryScale}
yScale={horizontal ? categoryScale : valueScale}
margin={{ left: 100, top: 64, bottom: 64 }}
>
<BarSeries
horizontal={horizontal}
data={horizontal ? categoryHorizontalData : categoryData}
/>
<CrossHair
showHorizontalLine={false}
fullHeight
stroke={colors.darkGray}
circleFill={allColors.blue[7]}
circleStroke="white"
/>
<YAxis numTicks={5} orientation="left" />
<XAxis numTicks={5} />
</XYChart>

<style type="text/css">
{`
.horizontal-bar-demo {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.bar-demo--form > div {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
margin-right: 12px;
}
`}
</style>
</div>
);
}
}

export default withScreenSize(HorizontalBarChartExample);
6 changes: 6 additions & 0 deletions packages/demo/examples/01-xy-chart/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -102,6 +103,11 @@ export default {
</WithTooltip>
),
},
{
description: 'HorizontalBarChartExample',
components: [BarSeries, CrossHair],
example: () => <HorizontalBarChartExample />,
},
{
description: 'LineSeries',
components: [LineSeries, CrossHair],
Expand Down
6 changes: 3 additions & 3 deletions packages/xy-chart/src/chart/XYChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
}),
Expand Down
37 changes: 22 additions & 15 deletions packages/xy-chart/src/series/BarSeries.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,7 +34,6 @@ const noEventsStyles = { pointerEvents: 'none' };
export default class BarSeries extends React.PureComponent {
render() {
const {
barWidth,
data,
disableMouseEvents,
fill,
Expand All @@ -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 (
<Group style={disableMouseEvents ? noEventsStyles : null}>
{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) && (
<FocusBlurHandler
key={`bar-${barX}`}
key={`bar-${barPosition}`}
onBlur={disableMouseEvents ? null : onMouseLeave}
onFocus={
disableMouseEvents
Expand All @@ -74,10 +81,10 @@ export default class BarSeries extends React.PureComponent {
}
>
<Bar
x={barX}
y={maxHeight - barHeight}
width={barWidth}
height={barHeight}
x={horizontal ? 0 : barPosition}
y={horizontal ? barPosition : maxBarLength - barLength}
width={horizontal ? barLength : barWidth}
height={horizontal ? barWidth : barLength}
fill={color}
fillOpacity={d.fillOpacity || callOrValue(fillOpacity, d, i)}
stroke={d.stroke || callOrValue(stroke, d, i)}
Expand Down
34 changes: 21 additions & 13 deletions packages/xy-chart/src/utils/collectScalesFromProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { componentName, isBarSeries, isCirclePackSeries } from './chartUtils';

const getX = d => 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;
Expand Down Expand Up @@ -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]);
Expand Down
54 changes: 54 additions & 0 deletions packages/xy-chart/test/series/BarSeries.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,60 @@ describe('<BarSeries />', () => {
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(
<XYChart
width={maxWidth}
height={maxHeight}
margin={{
top: 0,
left: 0,
bottom: 0,
right: 0,
}}
yScale={{ type: 'time' }}
xScale={{ type: 'linear', includeZero: false }}
>
<BarSeries
data={mockData.map((d, i) => ({
x: mockData.length - i,
y: d.date,
}))}
horizontal
/>
</XYChart>,
);
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(
<XYChart
{...mockProps}
yScale={{ type: 'time' }}
xScale={{ type: 'linear', includeZero: false }}
>
<BarSeries
data={mockData.map((d, i) => ({
x: i === 0 ? null : d.num,
y: d.date,
}))}
horizontal
/>
</XYChart>,
);
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(
<XYChart {...mockProps} xScale={{ type: 'time' }}>
Expand Down

0 comments on commit 4f8b295

Please sign in to comment.