recharts
Version:
React charts
279 lines (237 loc) • 6.87 kB
JavaScript
import React, { PropTypes } from 'react';
import D3Scale from 'd3-scale';
import Layer from '../container/Layer';
import LodashUtils from '../util/LodashUtils';
class Brush extends React.Component {
static displayName = 'Brush';
static propTypes = {
className: PropTypes.string,
fill: PropTypes.string,
stroke: PropTypes.string,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
data: PropTypes.array,
burshWidth: PropTypes.number,
start: PropTypes.number,
end: PropTypes.number,
onBrushChange: PropTypes.func,
};
static defaultProps = {
x: 0,
y: 0,
width: 0,
height: 40,
burshWidth: 5,
fill: '#fff',
stroke: '#8884d8',
};
constructor(props) {
super(props);
if (props.data && props.data.length) {
const len = props.data.length;
const startIndex = props.start === +props.start ? props.start : 0;
const endIndex = props.end === +props.end ? props.end : len - 1;
this.scale = D3Scale.point().domain(LodashUtils.range(0, len))
.range([props.x, props.x + props.width - props.burshWidth]);
this.scaleValues = this.scale.domain().map(entry => this.scale(entry));
this.state = {
isSlideMoving: false,
isBrushMoving: false,
startIndex, endIndex,
startX: this.scale(startIndex),
endX: this.scale(endIndex),
};
} else {
this.state = {};
}
}
getIndexInRange(range, x) {
const len = range.length;
let start = 0;
let end = len - 1;
while (end - start > 1) {
const middle = Math.floor((start + end) / 2);
if (range[middle] > x) {
end = middle;
} else {
start = middle;
}
}
return x >= range[end] ? end : start;
}
getIndex({ startX, endX }) {
const min = Math.min(startX, endX);
const max = Math.max(startX, endX);
const minIndex = this.getIndexInRange(this.scaleValues, min);
const maxIndex = this.getIndexInRange(this.scaleValues, max);
return {
startIndex: minIndex,
endIndex: maxIndex,
};
}
handleMove(e) {
if (this.state.isBrushMoving) {
this.handleBrushMove(e);
} else if (this.state.isSlideMoving) {
this.handleSlideMove(e);
}
}
handleUp() {
this.setState({
isBrushMoving: false,
isSlideMoving: false,
});
}
handleSlideDown(e) {
this.setState({
isBrushMoving: false,
isSlideMoving: true,
slideMoveStartX: e.pageX,
});
}
handleSlideMove(e) {
const { slideMoveStartX, startX, endX } = this.state;
const { x, width, burshWidth, onBrushChange } = this.props;
let delta = e.pageX - slideMoveStartX;
if (delta > 0) {
delta = Math.min(delta, x + width - burshWidth - endX, x + width - burshWidth - startX);
} else if (delta < 0) {
delta = Math.max(delta, x - startX, x - endX);
}
const newIndex = this.getIndex({
startX: startX + delta,
endX: endX + delta,
});
this.setState({
startX: startX + delta,
endX: endX + delta,
slideMoveStartX: e.pageX,
...newIndex,
}, () => {
if (onBrushChange) {
onBrushChange(newIndex);
}
});
}
handleBrushDown(id, e) {
this.setState({
isSlideMoving: false,
isBrushMoving: true,
movingBrushId: id,
brushMoveStartX: e.pageX,
});
}
handleBrushMove(e) {
const { brushMoveStartX, movingBrushId } = this.state;
const prevValue = this.state[movingBrushId];
const { x, width, burshWidth, onBrushChange } = this.props;
const params = { startX: this.state.startX, endX: this.state.endX };
let delta = e.pageX - brushMoveStartX;
if (delta > 0) {
delta = Math.min(delta, x + width - burshWidth - prevValue);
} else if (delta < 0) {
delta = Math.max(delta, x - prevValue);
}
params[movingBrushId] = prevValue + delta;
const newIndex = this.getIndex(params);
this.setState({
[movingBrushId]: prevValue + delta,
brushMoveStartX: e.pageX,
...newIndex,
}, () => {
if (onBrushChange) {
onBrushChange(newIndex);
}
});
}
renderBackground() {
const { x, y, width, height, fill, stroke } = this.props;
return (
<rect
stroke={stroke}
fill={fill}
x={x}
y={y}
width={width}
height={height}
/>
);
}
renderBrush(startX, id) {
const { y, burshWidth, height, stroke } = this.props;
const lineY = Math.floor(y + height / 2) - 1;
const x = Math.max(startX, this.props.x);
return (
<Layer
className="layer-brush"
onMouseDown={this.handleBrushDown.bind(this, id)}
style={{ cursor: 'col-resize' }}
>
<rect
x={x}
y={y}
width={burshWidth}
height={height}
fill={stroke}
stroke="none"
/>
<line x1={x + 1} y1={lineY} x2={x + burshWidth - 1} y2={lineY} fill="none" stroke="#fff"/>
<line x1={x + 1} y1={lineY + 2} x2={x + burshWidth - 1} y2={lineY + 2} fill="none" stroke="#fff"/>
</Layer>
);
}
renderSlide(startX, endX) {
const { y, height } = this.props;
return (
<rect
onMouseDown={::this.handleSlideDown}
style={{ cursor: 'move' }}
stroke="none"
fill="#e5e5f7"
x={Math.min(startX, endX)}
y={y}
width={Math.abs(endX - startX)}
height={height}
/>
);
}
renderText() {
const { data, y, height, burshWidth, stroke } = this.props;
const { startIndex, endIndex, startX, endX } = this.state;
const offset = 5;
const style = {
pointerEvents: 'none',
fill: stroke,
};
return (
<Layer className="recharts-brush-texts">
<text textAnchor="end" style={style} dy={offset} x={Math.min(startX, endX) - offset} y={y + height / 2}>
{data[startIndex]}
</text>
<text textAnchor="start" style={style} dy={offset} x={Math.max(startX, endX) + burshWidth + offset} y={y + height / 2}>
{data[endIndex]}
</text>
</Layer>
);
}
render() {
const { x, width, burshWidth, data, className } = this.props;
const { startX, endX } = this.state;
if (!data || !data.length) {return null;}
return (
<Layer className={'layer-recharts-bursh ' + (className || '')}
onMouseUp={::this.handleUp}
onMouseMove={::this.handleMove}
>
{this.renderBackground()}
{this.renderSlide(startX, endX)}
{this.renderBrush(startX, 'startX')}
{this.renderBrush(endX, 'endX')}
{this.renderText()}
</Layer>
);
}
}
export default Brush;