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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Brush Interaction for XY-chart #117

Merged
merged 12 commits into from
Aug 22, 2018
442 changes: 442 additions & 0 deletions packages/demo/examples/01-xy-chart/BrushableLineChart.jsx

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions packages/demo/examples/01-xy-chart/LinkedXYCharts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ class LinkedXYCharts extends React.Component {

handleMouseMove({ datum, seriesKey }) {
if (this.state.mousedOverDatum !== datum || this.state.mousedOverKey !== seriesKey) {
this.setState(() => ({ mousedOverDatum: datum, mousedOverKey: seriesKey }));
this.setState(() => ({
mousedOverDatum: datum,
mousedOverKey: seriesKey,
}));
}
}

Expand Down Expand Up @@ -117,7 +120,12 @@ class LinkedXYCharts extends React.Component {
: null;

const stackCrossHairData = mousedOverDatum
? { datum: { ...mousedOverDatum, y: getYForKey(mousedOverDatum, mousedOverKey) } }
? {
datum: {
...mousedOverDatum,
y: getYForKey(mousedOverDatum, mousedOverKey),
},
}
: null;

return (
Expand Down
27 changes: 23 additions & 4 deletions packages/demo/examples/01-xy-chart/ScatterWithHistograms.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,18 @@ export const pointData = genRandomNormalPoints(n).forEach(([x, y], i) => {
});

const marginScatter = { top: 10, right: 10, bottom: 64, left: 64 };
const marginTopHist = { top: 10, right: marginScatter.right, bottom: 5, left: marginScatter.left };
const marginSideHist = { top: 10, right: marginScatter.bottom, bottom: 5, left: marginScatter.top };
const marginTopHist = {
top: 10,
right: marginScatter.right,
bottom: 5,
left: marginScatter.left,
};
const marginSideHist = {
top: 10,
right: marginScatter.bottom,
bottom: 5,
left: marginScatter.top,
};

// eslint-disable-next-line react/prop-types
function renderTooltip({ datum }) {
Expand Down Expand Up @@ -104,7 +114,13 @@ class ScatterWithHistogram extends React.PureComponent {

renderRightHistogram({ width, height }) {
return (
<div style={{ transform: 'rotate(90deg)', display: 'flex', alignItems: 'flex-end' }}>
<div
style={{
transform: 'rotate(90deg)',
display: 'flex',
alignItems: 'flex-end',
}}
>
<Histogram
width={width}
height={height}
Expand Down Expand Up @@ -178,7 +194,10 @@ class ScatterWithHistogram extends React.PureComponent {
{this.renderTopHistogram({ width: scatterSize, height: histSize })}
<div style={{ display: 'flex', flexDirection: 'row' }}>
{this.renderScatter({ width: scatterSize, height: scatterSize })}
{this.renderRightHistogram({ width: scatterSize, height: histSize })}
{this.renderRightHistogram({
width: scatterSize,
height: histSize,
})}
</div>
</div>
<Checkbox
Expand Down
12 changes: 10 additions & 2 deletions packages/demo/examples/01-xy-chart/TickLabelPlayground.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,11 @@ class TickLabelPlayground extends React.PureComponent {
type="radio"
value="left"
onChange={e =>
this.setState({ axisOrientation: e.target.value, textAnchor: 'end', dx: -4 })
this.setState({
axisOrientation: e.target.value,
textAnchor: 'end',
dx: -4,
})
}
checked={this.state.axisOrientation === 'left'}
/>{' '}
Expand All @@ -251,7 +255,11 @@ class TickLabelPlayground extends React.PureComponent {
type="radio"
value="right"
onChange={e =>
this.setState({ axisOrientation: e.target.value, textAnchor: 'start', dx: 4 })
this.setState({
axisOrientation: e.target.value,
textAnchor: 'start',
dx: 4,
})
}
checked={this.state.axisOrientation === 'right'}
/>{' '}
Expand Down
5 changes: 4 additions & 1 deletion packages/demo/examples/01-xy-chart/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export const timeSeriesData = appleStock.filter((d, i) => i % 120 === 0).map(d =
y: d.close,
}));

export const categoricalData = letterFrequency.map(d => ({ x: d.letter, y: d.frequency }));
export const categoricalData = letterFrequency.map(d => ({
x: d.letter,
y: d.frequency,
}));

// stacked data
export const groupKeys = Object.keys(cityTemperature[0]).filter(attr => attr !== 'date');
Expand Down
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 @@ -52,6 +52,7 @@ import {
import WithToggle from '../shared/WithToggle';

import computeForceBasedCirclePack from './computeForceBasedCirclePack';
import BrushableLineChart from './BrushableLineChart';

PatternLines.displayName = 'PatternLines';
LinearGradient.displayName = 'LinearGradient';
Expand Down Expand Up @@ -303,6 +304,11 @@ export default {
components: [XYChart, StackedBarSeries, AreaSeries, CrossHair],
example: () => <LinkedXYCharts />,
},
{
description: 'Brushable time series chart',
components: [XYChart, LineSeries],
example: () => <BrushableLineChart />,
},
{
description: 'StackedAreaSeries',
components: [XYChart],
Expand Down
16 changes: 14 additions & 2 deletions packages/demo/examples/02-histogram/HistogramPlayground.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,20 @@ class HistogramPlayground extends React.PureComponent {

this.state = {
datasets: [0, 1],
0: getDataset({ n: NUM_POINTS, mu: 0, sigma: 1, distribution: 'normal', color: colors[0] }),
1: getDataset({ n: NUM_POINTS, mu: 2, sigma: 1, distribution: 'normal', color: colors[1] }),
0: getDataset({
n: NUM_POINTS,
mu: 0,
sigma: 1,
distribution: 'normal',
color: colors[0],
}),
1: getDataset({
n: NUM_POINTS,
mu: 2,
sigma: 1,
distribution: 'normal',
color: colors[1],
}),

cumulative: false,
normalized: false,
Expand Down
12 changes: 9 additions & 3 deletions packages/xy-chart/src/chart/Voronoi.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const propTypes = {
onClick: PropTypes.func,
onMouseMove: PropTypes.func,
onMouseLeave: PropTypes.func,
onMouseDown: PropTypes.func,
showVoronoi: PropTypes.bool,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
Expand All @@ -21,6 +22,7 @@ const defaultProps = {
onClick: null,
onMouseMove: null,
onMouseLeave: null,
onMouseDown: null,
showVoronoi: false,
};

Expand All @@ -31,8 +33,11 @@ class Voronoi extends React.PureComponent {
}

componentWillReceiveProps(nextProps) {
// eslint-disable-next-line react/destructuring-assignment
if (['data', 'x', 'y', 'width', 'height'].some(prop => this.props[prop] !== nextProps[prop])) {
if (
['data', 'x', 'y', 'width', 'height'].some(
prop => this.props[prop] !== nextProps[prop], // eslint-disable-line react/destructuring-assignment
)
) {
this.setState({ voronoi: Voronoi.getVoronoi(nextProps) });
}
}
Expand All @@ -44,7 +49,7 @@ class Voronoi extends React.PureComponent {
}

render() {
const { onMouseLeave, onMouseMove, onClick, showVoronoi } = this.props;
const { onMouseLeave, onMouseMove, onClick, showVoronoi, onMouseDown } = this.props;
const { voronoi } = this.state;

return (
Expand All @@ -69,6 +74,7 @@ class Voronoi extends React.PureComponent {
})
}
onMouseLeave={onMouseLeave && (() => onMouseLeave)}
onMouseDown={onMouseDown && (() => onMouseDown)}
/>
))}
</Group>
Expand Down
49 changes: 47 additions & 2 deletions packages/xy-chart/src/chart/XYChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
isCrossHair,
isReferenceLine,
isSeries,
isBrush,
getChildWithName,
numTicksForWidth,
numTicksForHeight,
Expand Down Expand Up @@ -91,7 +92,9 @@ class XYChart extends React.PureComponent {
this.handleClick = this.handleClick.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleContainerEvent = this.handleContainerEvent.bind(this);
this.registerBrushStartEvent = this.registerBrushStartEvent.bind(this);
}

componentDidMount() {
Expand All @@ -107,8 +110,11 @@ class XYChart extends React.PureComponent {

componentWillReceiveProps(nextProps) {
let shouldComputeScales = false;
// eslint-disable-next-line react/destructuring-assignment
if (['width', 'height', 'children'].some(prop => this.props[prop] !== nextProps[prop])) {
if (
['width', 'height', 'children'].some(
prop => this.props[prop] !== nextProps[prop], // eslint-disable-line react/destructuring-assignment
)
) {
shouldComputeScales = true;
}
if (
Expand Down Expand Up @@ -183,6 +189,16 @@ class XYChart extends React.PureComponent {
}
}

registerBrushStartEvent(event) {
this.fireBrushStart = event;
}

handleMouseDown(event) {
if (this.fireBrushStart) {
this.fireBrushStart(event);
}
}

handleMouseMove(args) {
const { snapTooltipToDataX, snapTooltipToDataY, onMouseMove } = this.props;
const isFocusEvent = args.event && args.event.type === 'focus';
Expand Down Expand Up @@ -259,6 +275,10 @@ class XYChart extends React.PureComponent {
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 hasBrush = false;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you initially set let Brush = null and then just call Brush && React.cloneElement(Brush ...) below to get rid of the extra hasBrush var?

let Brush;
let xAxisOrientation;
let yAxisOrientation;

return (
innerWidth > 0 &&
Expand All @@ -283,6 +303,11 @@ class XYChart extends React.PureComponent {
const name = componentName(Child);
if (isAxis(name)) {
const styleKey = name[0].toLowerCase();
if (name === 'XAxis') {
xAxisOrientation = Child.props.orientation;
} else {
yAxisOrientation = Child.props.orientation;
}

return React.cloneElement(Child, {
innerHeight,
Expand Down Expand Up @@ -322,6 +347,11 @@ class XYChart extends React.PureComponent {
return null;
} else if (isReferenceLine(name)) {
return React.cloneElement(Child, { xScale, yScale });
} else if (isBrush(name)) {
hasBrush = true;
Brush = Child;

return null;
}

return Child;
Expand All @@ -335,6 +365,7 @@ class XYChart extends React.PureComponent {
width={innerWidth}
height={innerHeight}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
onMouseLeave={this.handleMouseLeave}
showVoronoi={showVoronoi}
Expand All @@ -349,12 +380,26 @@ class XYChart extends React.PureComponent {
height={innerHeight}
fill="transparent"
fillOpacity={0}
onMouseDown={this.handleMouseDown}
onClick={this.handleContainerEvent}
onMouseMove={this.handleContainerEvent}
onMouseLeave={this.handleMouseLeave}
/>
)}

{hasBrush &&
React.cloneElement(Brush, {
xScale,
yScale,
innerHeight,
innerWidth,
margin,
registerStartEvent:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add a comment about why this isn't needed for a series event trigger?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! In deed for area chart and bar chart, we do need to do like this. Will revise the code logic.

eventTrigger === SERIES_TRIGGER ? null : this.registerBrushStartEvent,
xAxisOrientation,
yAxisOrientation,
})}

{tooltipData &&
CrossHairs.length > 0 &&
CrossHairs.map((CrossHair, i) =>
Expand Down
1 change: 1 addition & 0 deletions packages/xy-chart/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export { PatternLines, PatternCircles, PatternWaves, PatternHexagons } from '@vx
export { withScreenSize, withParentSize, ParentSize } from '@vx/responsive';
export { default as withTheme } from './enhancer/withTheme';
export { chartTheme as theme } from '@data-ui/theme';
export { default as Brush } from './selection/Brush';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think all of the brush components should go under src/components/brush/... and the utils could go under src/util/brush?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here, I was thinking for a long term, will we keep the code here or we should move it to vx? The reason I put it in the utils is that I think now it is the temporary solution.

Loading