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
317 changes: 317 additions & 0 deletions packages/demo/examples/01-xy-chart/BrushableLineChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
/* eslint react/prop-types: 0 */
import React from 'react';
import { timeParse, timeFormat } from 'd3-time-format';

import {
XYChart,
CrossHair,
XAxis,
theme,
withScreenSize,
LineSeries,
PatternLines,
LinearGradient,
Brush,
} from '@data-ui/xy-chart';

import colors from '@data-ui/theme/lib/color';

import {
timeSeriesData,
} from './data';
import PointSeries from '../../node_modules/@data-ui/xy-chart/lib/series/PointSeries';

export const parseDate = timeParse('%Y%m%d');
export const formatDate = timeFormat('%b %d');
export const formatYear = timeFormat('%Y');
export const dateFormatter = date => formatDate(parseDate(date));


class BrushableLineChart extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
pointData: [...timeSeriesData],
brushDirection: 'horizontal',
resizeTriggerAreas: ['left', 'right'],
};
this.handleBrushChange = this.handleBrushChange.bind(this);
}

handleBrushChange(domain) {
let pointData;
if (domain) {
pointData = timeSeriesData.filter(point => point.x > domain.x0 && point.x < domain.x1 && point.y > domain.y0 && point.y < domain.y1);
} else {
pointData = [...timeSeriesData];
}
this.setState(() => ({
pointData,
}));
}

renderControls() {
const { resizeTriggerAreas } = this.state;
const resizeTriggerAreaset = new Set(resizeTriggerAreas);
return (
<div className="brush-demo--form">
<h4>Brush Props</h4>
<div>
Brush Direction:
<label>
<input
type="radio"
value="horizontal"
onChange={e => this.setState({ brushDirection: e.target.value })}
checked={this.state.brushDirection === 'horizontal'}
/>{' '}
horizonal
</label>
<label>
<input
type="radio"
value="vertical"
onChange={e => this.setState({ brushDirection: e.target.value })}
checked={this.state.brushDirection === 'vertical'}
/>{' '}
vertical
</label>
<label>
<input
type="radio"
value="both"
onChange={e => this.setState({ brushDirection: e.target.value })}
checked={this.state.brushDirection === 'both'}
/>{' '}
both
</label>
</div>

<div>
Resize Trigger Border:
<label>
<input
type="checkbox"
onChange={() => {
const value = 'left';
if (resizeTriggerAreaset.has(value)) {
resizeTriggerAreaset.delete(value);
} else {
resizeTriggerAreaset.add(value);
}
this.setState(() => ({
resizeTriggerAreas: [...resizeTriggerAreaset],
}));
}}
checked={resizeTriggerAreaset.has('left')}
/>
left
</label>
<label>
<input
type="checkbox"
onChange={() => {
const value = 'right';
if (resizeTriggerAreaset.has(value)) {
resizeTriggerAreaset.delete(value);
} else {
resizeTriggerAreaset.add(value);
}
this.setState(() => ({
resizeTriggerAreas: [...resizeTriggerAreaset],
}));
}}
checked={resizeTriggerAreaset.has('right')}
/>
right
</label>
<label>
<input
type="checkbox"
onChange={() => {
const value = 'top';
if (resizeTriggerAreaset.has(value)) {
resizeTriggerAreaset.delete(value);
} else {
resizeTriggerAreaset.add(value);
}
this.setState(() => ({
resizeTriggerAreas: [...resizeTriggerAreaset],
}));
}}
checked={resizeTriggerAreaset.has('top')}
/>
top
</label>
<label>
<input
type="checkbox"
onChange={() => {
const value = 'bottom';
if (resizeTriggerAreaset.has(value)) {
resizeTriggerAreaset.delete(value);
} else {
resizeTriggerAreaset.add(value);
}
this.setState(() => ({
resizeTriggerAreas: [...resizeTriggerAreaset],
}));
}}
checked={resizeTriggerAreaset.has('bottom')}
/>
bottom
</label>
</div>
<div>
Resize Trigger Corner:
<label>
<input
type="checkbox"
onChange={() => {
const value = 'topLeft';
if (resizeTriggerAreaset.has(value)) {
resizeTriggerAreaset.delete(value);
} else {
resizeTriggerAreaset.add(value);
}
this.setState(() => ({
resizeTriggerAreas: [...resizeTriggerAreaset],
}));
}}
checked={resizeTriggerAreaset.has('topLeft')}
/>
topLeft
</label>
<label>
<input
type="checkbox"
onChange={() => {
const value = 'topRight';
if (resizeTriggerAreaset.has(value)) {
resizeTriggerAreaset.delete(value);
} else {
resizeTriggerAreaset.add(value);
}
this.setState(() => ({
resizeTriggerAreas: [...resizeTriggerAreaset],
}));
}}
checked={resizeTriggerAreaset.has('topRight')}
/>
topRight
</label>
<label>
<input
type="checkbox"
onChange={() => {
const value = 'bottomLeft';
if (resizeTriggerAreaset.has(value)) {
resizeTriggerAreaset.delete(value);
} else {
resizeTriggerAreaset.add(value);
}
this.setState(() => ({
resizeTriggerAreas: [...resizeTriggerAreaset],
}));
}}
checked={resizeTriggerAreaset.has('bottomLeft')}
/>
bottomLeft
</label>
<label>
<input
type="checkbox"
onChange={() => {
const value = 'bottomRight';
if (resizeTriggerAreaset.has(value)) {
resizeTriggerAreaset.delete(value);
} else {
resizeTriggerAreaset.add(value);
}
this.setState(() => ({
resizeTriggerAreas: [...resizeTriggerAreaset],
}));
}}
checked={resizeTriggerAreaset.has('bottomRight')}
/>
bottomRight
</label>
</div>
</div>
);
}

render() {
const { screenWidth, ...rest } = this.props;
const { pointData, brushDirection, resizeTriggerAreas } = this.state;
return (
<div className="brush-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={{ type: 'time' }}
yScale={{ type: 'linear' }}
margin={{ left: 0, top: 0, bottom: 64 }}
{...rest}
Copy link
Owner

Choose a reason for hiding this comment

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

I think you can get rid of this right?

>
<LinearGradient id="area_gradient" from={colors.categories[2]} to="#fff" />
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 need the gradient + pattern? (I'd just remove anything unneeded in case people look at the example usage)

<PatternLines
id="area_pattern"
height={12}
width={12}
stroke={colors.categories[2]}
strokeWidth={1}
orientation={['diagonal']}
/>
<LineSeries
seriesKey="one"
data={timeSeriesData}
strokeWidth={1}
/>
<PointSeries
seriesKey="one"
data={pointData}
strokeWidth={1}
/>
<CrossHair
showHorizontalLine={false}
fullHeight
stroke={colors.darkGray}
circleFill={colors.categories[2]}
circleStroke="white"
/>
<XAxis label="Time" numTicks={5} />
<Brush
handleSize={4}
resizeTriggerAreas={resizeTriggerAreas}
brushDirection={brushDirection}
onChange={this.handleBrushChange}
/>
</XYChart>

<style type="text/css">
{`
.brush-demo {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}

.brush-demo--form > div {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
margin-right: 12px;
}
`}
</style>
</div>
);
}
}

export default withScreenSize(BrushableLineChart);
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
9 changes: 9 additions & 0 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 @@ -322,6 +323,14 @@ class XYChart extends React.PureComponent {
return null;
} else if (isReferenceLine(name)) {
return React.cloneElement(Child, { xScale, yScale });
} else if (isBrush(name)) {
return React.cloneElement(Child, {
xScale,
yScale,
innerHeight,
Copy link
Owner

Choose a reason for hiding this comment

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

curious if innerWidth/Height can be computed from the passed scales?

innerWidth,
margin,
});
}

return Child;
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