Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conglei horizontal bar #127

Merged
merged 4 commits into from
Aug 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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