From 97b6f2c37ee86919257c564326c574396f28d9f5 Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Mon, 13 Aug 2018 02:56:35 -0500 Subject: [PATCH 01/12] working version --- .../01-xy-chart/BrushableLineChart.jsx | 317 ++++++++++++++++ packages/demo/examples/01-xy-chart/index.jsx | 6 + packages/xy-chart/src/chart/XYChart.jsx | 9 + packages/xy-chart/src/index.js | 1 + packages/xy-chart/src/selection/Brush.jsx | 91 +++++ packages/xy-chart/src/utils/brush/Brush.js | 353 ++++++++++++++++++ .../xy-chart/src/utils/brush/BrushCorner.js | 218 +++++++++++ .../xy-chart/src/utils/brush/BrushHandle.js | 155 ++++++++ .../src/utils/brush/BrushSelection.js | 120 ++++++ .../xy-chart/src/utils/brush/RightHandle.js | 99 +++++ packages/xy-chart/src/utils/brush/index.js | 1 + packages/xy-chart/src/utils/chartUtils.js | 4 + packages/xy-chart/src/utils/drag/Drag.js | 115 ++++++ packages/xy-chart/src/utils/drag/index.js | 2 + .../xy-chart/src/utils/drag/util/raise.js | 7 + 15 files changed, 1498 insertions(+) create mode 100644 packages/demo/examples/01-xy-chart/BrushableLineChart.jsx create mode 100644 packages/xy-chart/src/selection/Brush.jsx create mode 100644 packages/xy-chart/src/utils/brush/Brush.js create mode 100644 packages/xy-chart/src/utils/brush/BrushCorner.js create mode 100644 packages/xy-chart/src/utils/brush/BrushHandle.js create mode 100644 packages/xy-chart/src/utils/brush/BrushSelection.js create mode 100644 packages/xy-chart/src/utils/brush/RightHandle.js create mode 100644 packages/xy-chart/src/utils/brush/index.js create mode 100644 packages/xy-chart/src/utils/drag/Drag.js create mode 100644 packages/xy-chart/src/utils/drag/index.js create mode 100644 packages/xy-chart/src/utils/drag/util/raise.js diff --git a/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx b/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx new file mode 100644 index 00000000..687edbbe --- /dev/null +++ b/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx @@ -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 ( +
+

Brush Props

+
+ Brush Direction: + + + +
+ +
+ Resize Trigger Border: + + + + +
+
+ Resize Trigger Corner: + + + + +
+
+ ); + } + + render() { + const { screenWidth, ...rest } = this.props; + const { pointData, brushDirection, resizeTriggerAreas } = this.state; + return ( +
+ {this.renderControls()} + + + + + + + + + + + +
+ ); + } +} + +export default withScreenSize(BrushableLineChart); diff --git a/packages/demo/examples/01-xy-chart/index.jsx b/packages/demo/examples/01-xy-chart/index.jsx index 84dacdd7..24b116af 100644 --- a/packages/demo/examples/01-xy-chart/index.jsx +++ b/packages/demo/examples/01-xy-chart/index.jsx @@ -52,6 +52,7 @@ import { import WithToggle from '../shared/WithToggle'; import computeForceBasedCirclePack from './computeForceBasedCirclePack'; +import BrushableLineChart from './BrushableLineChart'; PatternLines.displayName = 'PatternLines'; LinearGradient.displayName = 'LinearGradient'; @@ -303,6 +304,11 @@ export default { components: [XYChart, StackedBarSeries, AreaSeries, CrossHair], example: () => , }, + { + description: 'Brushable time series chart', + components: [XYChart, LineSeries], + example: () => , + }, { description: 'StackedAreaSeries', components: [XYChart], diff --git a/packages/xy-chart/src/chart/XYChart.jsx b/packages/xy-chart/src/chart/XYChart.jsx index 5705017d..929cb856 100644 --- a/packages/xy-chart/src/chart/XYChart.jsx +++ b/packages/xy-chart/src/chart/XYChart.jsx @@ -16,6 +16,7 @@ import { isCrossHair, isReferenceLine, isSeries, + isBrush, getChildWithName, numTicksForWidth, numTicksForHeight, @@ -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, + innerWidth, + margin, + }); } return Child; diff --git a/packages/xy-chart/src/index.js b/packages/xy-chart/src/index.js index d401f882..19884ccb 100644 --- a/packages/xy-chart/src/index.js +++ b/packages/xy-chart/src/index.js @@ -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'; diff --git a/packages/xy-chart/src/selection/Brush.jsx b/packages/xy-chart/src/selection/Brush.jsx new file mode 100644 index 00000000..9af8dcd1 --- /dev/null +++ b/packages/xy-chart/src/selection/Brush.jsx @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { color } from '@data-ui/theme'; + +import BaseBrush from '../utils/brush/Brush'; + +export const propTypes = { + label: PropTypes.node, + stroke: PropTypes.string, + strokeDasharray: PropTypes.string, + strokeWidth: PropTypes.number, + xScale: PropTypes.func, + yScale: PropTypes.func, + innerHeight: PropTypes.number.isRequired, + innerWidth: PropTypes.number.isRequired, + onChange: PropTypes.func, +}; + +const defaultProps = { + label: null, + stroke: color.darkGray, + strokeDasharray: null, + strokeWidth: 1, + xScale: null, + yScale: null, + onChange: () => {}, +}; + +class Brush extends React.Component { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + } + + handleChange(brush) { + const { xScale, yScale, margin, onChange } = this.props; + const { x0, x1, y0, y1 } = brush.extent; + if (x0 < 0) { + onChange(null); + return; + } + const invertedX0 = xScale.invert(x0 - margin.left); + const invertedX1 = xScale.invert(x1 - margin.left); + const invertedY0 = yScale.invert(y0 - margin.top); + const invertedY1 = yScale.invert(y1 - margin.top); + + const domainRange = { + x0: Math.min(invertedX0, invertedX1), + x1: Math.max(invertedX0, invertedX1), + y0: Math.min(invertedY0, invertedY1), + y1: Math.max(invertedY0, invertedY1), + }; + onChange(domainRange); + } + + render() { + const { + label, + stroke, + strokeDasharray, + strokeWidth, + xScale, + yScale, + innerHeight, + innerWidth, + margin, + onChange, + brushDirection, + resizeTriggerAreas, + } = this.props; + if (!xScale || !yScale) return null; + return ( + ); + } +} + +Brush.propTypes = propTypes; +Brush.defaultProps = defaultProps; +Brush.displayName = 'Brush'; + +export default Brush; diff --git a/packages/xy-chart/src/utils/brush/Brush.js b/packages/xy-chart/src/utils/brush/Brush.js new file mode 100644 index 00000000..c339ba67 --- /dev/null +++ b/packages/xy-chart/src/utils/brush/Brush.js @@ -0,0 +1,353 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Group } from '@vx/group'; +import { Bar } from '@vx/shape'; + +import BrushHandle from './BrushHandle'; +import BrushCorner from './BrushCorner'; +import BrushSelection from './BrushSelection'; +import { Drag } from '../drag'; + +export default class Brush extends React.Component { + constructor(props) { + super(props); + this.state = { + start: { x: 0, y: 0 }, + end: { x: 0, y: 0 }, + extent: { + x0: 0, + x1: 0, + y0: 0, + y1: 0, + }, + bounds: { + x0: 0, + x1: props.width, + y0: 0, + y1: props.height, + }, + }; + this.width = this.width.bind(this); + this.height = this.height.bind(this); + this.handles = this.handles.bind(this); + this.corners = this.corners.bind(this); + this.update = this.update.bind(this); + this.reset = this.reset.bind(this); + this.handleDragStart = this.handleDragStart.bind(this); + this.handleDragMove = this.handleDragMove.bind(this); + this.handleDragEnd = this.handleDragEnd.bind(this); + this.getExtent = this.getExtent.bind(this); + } + + getExtent(start, end) { + const { brushDirection, width, height } = this.props; + const x0 = brushDirection === 'vertical' + ? 0 + : Math.min(start.x, end.x); + const x1 = brushDirection === 'vertical' + ? width + : Math.max(start.x, end.x); + const y0 = brushDirection === 'horizontal' + ? 0 + : Math.min(start.y, end.y); + const y1 = brushDirection === 'horizontal' + ? height + : Math.max(start.y, end.y); + return { + x0, + x1, + y0, + y1, + }; + } + + handleDragStart(draw) { + const { onBrushStart, left, top } = this.props; + if (onBrushStart) { + onBrushStart(); + } + this.update(prevBrush => ({ + ...prevBrush, + start: { + x: draw.x - left, + y: draw.y - top, + }, + end: { + x: draw.x - left, + y: draw.y - top, + }, + extent: { + x0: -1, + x1: -1, + y0: -1, + y1: -1, + }, + isBrushing: true, + })); + } + + handleDragMove(draw) { + const { left, top } = this.props; + if (!draw.isDragging) return; + const end = { + x: draw.x + draw.dx - left, + y: draw.y + draw.dy - top, + }; + this.update((prevBrush) => { + const { start } = prevBrush; + const extent = this.getExtent(start, end); + return { + ...prevBrush, + end, + extent, + }; + }); + } + + handleDragEnd() { + this.update(((prevBrush) => { + const { extent } = prevBrush; + return { + ...prevBrush, + start: { + x: extent.x0, + y: extent.y0, + }, + end: { + x: extent.x1, + y: extent.y1, + }, + isBrushing: false, + }; + })); + } + + width() { + const { extent } = this.state; + const { x0, x1 } = extent; + return Math.max(Math.max(x0, x1) - Math.min(x0, x1), 0); + } + + height() { + const { extent } = this.state; + const { y1, y0 } = extent; + return Math.max(y1 - y0, 0); + } + + handles() { + const { handleSize } = this.props; + const { extent } = this.state; + const { x0, x1, y0, y1 } = extent; + const offset = handleSize / 2; + const width = this.width(); + const height = this.height(); + return { + top: { + x: x0 - offset, + y: y0 - offset, + height: handleSize, + width: width + handleSize, + }, + bottom: { + x: x0 - offset, + y: y1 - offset, + height: handleSize, + width: width + handleSize, + }, + right: { + x: x1 - offset, + y: y0 - offset, + height: height + handleSize, + width: handleSize, + }, + left: { + x: x0 - offset, + y: y0 - offset, + height: height + handleSize, + width: handleSize, + }, + }; + } + + corners() { + const { handleSize } = this.props; + const { extent } = this.state; + const { x0, x1, y0, y1 } = extent; + const offset = handleSize / 2; + return { + topLeft: { + x: Math.min(x0, x1) - offset, + y: Math.min(y0, y1) - offset, + }, + topRight: { + x: Math.max(x0, x1) - offset, + y: Math.min(y0, y1) - offset, + }, + bottomLeft: { + x: Math.min(x0, x1) - offset, + y: Math.max(y0, y1) - offset, + }, + bottomRight: { + x: Math.max(x0, x1) - offset, + y: Math.max(y0, y1) - offset, + }, + }; + } + + update(updater) { + this.setState(updater, () => { + this.props.onChange(this.state); + }); + } + + reset() { + this.update(prevBrush => { + return { + start: undefined, + end: undefined, + extent: { + x0: undefined, + x1: undefined, + y0: undefined, + y1: undefined, + }, + isBrushing: false, + activeHandle: undefined, + }; + }); + } + + render() { + const { + start, + end, + } = this.state; + const { + top, + left, + data, + width: stageWidth, + height: stageHeight, + handleSize, + onBrushStart, + onMouseLeave, + onMouseUp, + onMouseMove, + resizeTriggerAreas, + brushDirection, + } = this.props; + + const handles = this.handles(); + const corners = this.corners(); + const width = this.width(); + const height = this.height(); + const resizeTriggerAreaSet = new Set(resizeTriggerAreas); + + return ( + + {/* overlay */} + + {draw => { + return ( + event => this.reset(event)} + onMouseDown={data => event => draw.dragStart(event)} + onMouseLeave={data => event => { + if (onMouseLeave) onMouseLeave(event); + }} + onMouseMove={data => event => { + if (!draw.isDragging && onMouseMove) + this.props.onMouseMove(event); + if (draw.isDragging) draw.dragMove(event); + }} + onMouseUp={data => event => { + if (onMouseUp) onMouseUp(event); + draw.dragEnd(event); + }} + style={{ cursor: 'crosshair' }} + /> + ); + }} + + {/* selection */} + {start && + end && ( + + )} + {/* handles */} + {start && + end && + Object.keys(handles).filter(handleKey => resizeTriggerAreaSet.has(handleKey)).map((handleKey, i) => { + const handle = handles[handleKey]; + return ( + + ); + })} + {/* corners */} + {start && + end && + Object.keys(corners).filter(cornerKey => resizeTriggerAreaSet.has(cornerKey)).map((cornerKey, i) => { + const corner = corners[cornerKey]; + return ( + + ); + })} + )} + + ); + } +} + +Brush.propTypes = { + brushDirection: PropTypes.oneOf(['horizontal', 'vertical', 'both']), +}; + +Brush.defaultProps = { + brushDirection: 'both', +}; diff --git a/packages/xy-chart/src/utils/brush/BrushCorner.js b/packages/xy-chart/src/utils/brush/BrushCorner.js new file mode 100644 index 00000000..1c1060fe --- /dev/null +++ b/packages/xy-chart/src/utils/brush/BrushCorner.js @@ -0,0 +1,218 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Drag } from '../drag'; + +export default class BrushCorner extends React.Component { + constructor(props) { + super(props); + this.cornerDragMove = this.cornerDragMove.bind(this); + this.cornerDragEnd = this.cornerDragEnd.bind(this); + } + cornerDragMove(drag) { + const { handle, updateBrush, type } = this.props; + if (!drag.isDragging) return; + updateBrush(prevBrush => { + const { start, end, isBrushing } = prevBrush; + + const xMax = Math.max(start.x, end.x); + const xMin = Math.min(start.x, end.x); + const yMax = Math.max(start.y, end.y); + const yMin = Math.min(start.y, end.y); + + let moveX = 0; + let moveY = 0; + let nextState = {}; + + switch (type) { + case 'topRight': + moveX = xMax + drag.dx; + moveY = yMin + drag.dy; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.max( + Math.min(moveX, start.x), + prevBrush.bounds.x0, + ), + x1: Math.min( + Math.max(moveX, start.x), + prevBrush.bounds.x1, + ), + y0: Math.max( + Math.min(moveY, end.y), + prevBrush.bounds.y0, + ), + y1: Math.min( + Math.max(moveY, end.y), + prevBrush.bounds.y1, + ), + }, + }; + break; + case 'topLeft': + moveX = xMin + drag.dx; + moveY = yMin + drag.dy; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.max( + Math.min(moveX, end.x), + prevBrush.bounds.x0, + ), + x1: Math.min( + Math.max(moveX, end.x), + prevBrush.bounds.x1, + ), + y0: Math.max( + Math.min(moveY, end.y), + prevBrush.bounds.y0, + ), + y1: Math.min( + Math.max(moveY, end.y), + prevBrush.bounds.y1, + ), + }, + }; + break; + case 'bottomLeft': + moveX = xMin + drag.dx; + moveY = yMax + drag.dy; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.max( + Math.min(moveX, end.x), + prevBrush.bounds.x0, + ), + x1: Math.min( + Math.max(moveX, end.x), + prevBrush.bounds.x1, + ), + y0: Math.max( + Math.min(moveY, start.y), + prevBrush.bounds.y0, + ), + y1: Math.min( + Math.max(moveY, start.y), + prevBrush.bounds.y1, + ), + }, + }; + break; + case 'bottomRight': + moveX = xMax + drag.dx; + moveY = yMax + drag.dy; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.max( + Math.min(moveX, start.x), + prevBrush.bounds.x0, + ), + x1: Math.min( + Math.max(moveX, start.x), + prevBrush.bounds.x1, + ), + y0: Math.max( + Math.min(moveY, start.y), + prevBrush.bounds.y0, + ), + y1: Math.min( + Math.max(moveY, start.y), + prevBrush.bounds.y1, + ), + }, + }; + break; + } + return nextState; + }); + } + cornerDragEnd(drag) { + const { type, handle, updateBrush } = this.props; + updateBrush(prevBrush => { + const { start, end, extent } = prevBrush; + start.x = Math.min(extent.x0, extent.x1); + start.y = Math.min(extent.y0, extent.y0); + end.x = Math.max(extent.x0, extent.x1); + end.y = Math.max(extent.y0, extent.y1); + const nextState = { + ...prevBrush, + start, + end, + activeHandle: undefined, + domain: { + x0: Math.min(start.x, end.x), + x1: Math.max(start.x, end.x), + y0: Math.min(start.y, end.y), + y1: Math.max(start.y, end.y), + }, + }; + return nextState; + }); + } + render() { + const { + type, + brush, + updateBrush, + stageWidth, + stageHeight, + style: styleProp, + ...restProps + } = this.props; + const cursor = + type === 'topLeft' || type === 'bottomRight' + ? 'nwse-resize' + : 'nesw-resize'; + const pointerEvents = + brush.activeHandle || brush.isBrushing ? 'none' : 'all'; + const style = { + cursor, + pointerEvents, + ...styleProp, + }; + return ( + + {corner => { + return ( + + {corner.isDragging && ( + + )} + + + ); + }} + + ); + } +} diff --git a/packages/xy-chart/src/utils/brush/BrushHandle.js b/packages/xy-chart/src/utils/brush/BrushHandle.js new file mode 100644 index 00000000..c4e65e04 --- /dev/null +++ b/packages/xy-chart/src/utils/brush/BrushHandle.js @@ -0,0 +1,155 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Drag } from '../drag'; + +export default class BrushHandle extends React.Component { + constructor(props) { + super(props); + this.handleDragMove = this.handleDragMove.bind(this); + this.handleDragEnd = this.handleDragEnd.bind(this); + } + handleDragMove(drag) { + const { handle, updateBrush, type } = this.props; + if (!drag.isDragging) return; + updateBrush(prevBrush => { + const { start, end, isBrushing } = prevBrush; + let nextState = {}; + let move = 0; + const xMax = Math.max(start.x, end.x); + const xMin = Math.min(start.x, end.x); + const yMax = Math.max(start.y, end.y); + const yMin = Math.min(start.y, end.y); + switch (type) { + case 'right': + move = xMax + drag.dx; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.max( + Math.min(move, start.x), + prevBrush.bounds.x0, + ), + x1: Math.min( + Math.max(move, start.x), + prevBrush.bounds.x1, + ), + }, + }; + break; + case 'left': + move = xMin + drag.dx; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.min(move, end.x), + x1: Math.max(move, end.x), + }, + }; + break; + case 'bottom': + move = yMax + drag.dy; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + y0: Math.min(move, start.y), + y1: Math.max(move, start.y), + }, + }; + break; + case 'top': + move = yMin + drag.dy; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + y0: Math.min(move, end.y), + y1: Math.max(move, end.y), + }, + }; + break; + } + return nextState; + }); + } + handleDragEnd(drag) { + const { type, handle, updateBrush } = this.props; + updateBrush(prevBrush => { + const { start, end, extent } = prevBrush; + start.x = Math.min(extent.x0, extent.x1); + start.y = Math.min(extent.y0, extent.y0); + end.x = Math.max(extent.x0, extent.x1); + end.y = Math.max(extent.y0, extent.y1); + const nextState = { + ...prevBrush, + start, + end, + activeHandle: undefined, + isBrushing: false, + domain: { + x0: Math.min(start.x, end.x), + x1: Math.max(start.x, end.x), + y0: Math.min(start.y, end.y), + y1: Math.max(start.y, end.y), + }, + }; + return nextState; + }); + } + render() { + const { stageWidth, stageHeight, brush, type } = this.props; + const { x, y, width, height } = this.props.handle; + const cursor = + type === 'right' || type === 'left' ? 'ew-resize' : 'ns-resize'; + return ( + + {handle => { + return ( + + {handle.isDragging && ( + + )} + + + ); + }} + + ); + } +} diff --git a/packages/xy-chart/src/utils/brush/BrushSelection.js b/packages/xy-chart/src/utils/brush/BrushSelection.js new file mode 100644 index 00000000..c846fb3b --- /dev/null +++ b/packages/xy-chart/src/utils/brush/BrushSelection.js @@ -0,0 +1,120 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Drag } from '../drag'; + +export default class BrushSelection extends React.Component { + constructor(props) { + super(props); + this.selectionDragMove = this.selectionDragMove.bind(this); + this.selectionDragEnd = this.selectionDragEnd.bind(this); + } + + selectionDragMove(drag) { + const { updateBrush } = this.props; + updateBrush((prevBrush) => { + const { x: x0, y: y0 } = prevBrush.start; + const { x: x1, y: y1 } = prevBrush.end; + const validDx = drag.dx > 0 + ? Math.min(drag.dx, prevBrush.bounds.x1 - x1) + : Math.max(drag.dx, prevBrush.bounds.x0 - x0); + + const validDy = drag.dy > 0 + ? Math.min(drag.dy, prevBrush.bounds.y1 - y1) + : Math.max(drag.dy, prevBrush.bounds.y0 - y0); + return { + ...prevBrush, + isBrushing: true, + extent: { + ...prevBrush.extent, + x0: x0 + validDx, + x1: x1 + validDx, + y0: y0 + validDy, + y1: y1 + validDy, + }, + }; + }); + } + + selectionDragEnd(drag) { + const { updateBrush } = this.props; + updateBrush(prevBrush => { + return { + ...prevBrush, + isBrushing: false, + start: { + ...prevBrush.start, + x: Math.min(prevBrush.extent.x0, prevBrush.extent.x1), + y: Math.min(prevBrush.extent.y0, prevBrush.extent.y1), + }, + end: { + ...prevBrush.end, + x: Math.max(prevBrush.extent.x0, prevBrush.extent.x1), + y: Math.max(prevBrush.extent.y0, prevBrush.extent.y1), + }, + }; + }); + } + + render() { + const { + width, + height, + stageWidth, + stageHeight, + brush, + updateBrush, + ...restProps + } = this.props; + return ( + + {selection => { + return ( + + {selection.isDragging && ( + + )} + + + ); + }} + + ); + } +} + +BrushSelection.propType = { + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + stageWidth: PropTypes.number.isRequired, + stageHeight: PropTypes.number.isRequired, + brush: PropTypes.object.isRequired, + updateBrush: PropTypes.func.isRequired, +}; diff --git a/packages/xy-chart/src/utils/brush/RightHandle.js b/packages/xy-chart/src/utils/brush/RightHandle.js new file mode 100644 index 00000000..f2c9b47e --- /dev/null +++ b/packages/xy-chart/src/utils/brush/RightHandle.js @@ -0,0 +1,99 @@ +import React from 'react'; +import { Drag } from '../drag'; + +function handleResizeMove(drag, onBrushEnd) { + const { dx } = drag; + return prevBrush => { + const { start, end, domain } = prevBrush; + const nextState = { + ...prevBrush, + domain: { + ...prevBrush.domain, + x1: Math.max(start.x, end.x) + dx, + }, + }; + if (onBrushEnd) onBrushEnd(nextState); + return nextState; + }; +} + +function handleResizeEnd(drag, onBrushEnd) { + return prevBrush => { + const { start, end, domain } = prevBrush; + start.x = domain.x0; + end.x = domain.x1; + const nextState = { + ...prevBrush, + start, + end, + isBrushing: false, + domain: { + x0: Math.min(start.x, end.x), + x1: Math.max(start.x, end.x), + y0: Math.min(start.y, end.y), + y1: Math.max(start.y, end.y), + }, + }; + if (onBrushEnd) onBrushEnd(nextState); + return nextState; + }; +} + +export default class RightHandle extends React.Component { + constructor(props) { + super(props); + this.dragMove = this.dragMove.bind(this); + this.dragEnd = this.dragEnd.bind(this); + } + dragMove(drag) { + const { updateBrush, onBrushEnd } = this.props; + updateBrush(handleResizeMove(drag, onBrushEnd)); + } + dragEnd(drag) { + const { updateBrush, onBrushEnd, brush } = this.props; + updateBrush(handleResizeEnd(drag, onBrushEnd)); + } + render() { + const { width, height, top, left, brush } = this.props; + return ( + + {right => { + return ( + { + if (brush.isBrushing) { + return this.dragEnd(event); + } + right.dragEnd(event); + }} + onMouseDown={event => { + if (brush.isBrushing) return; + right.dragStart(event); + }} + onMouseMove={event => { + if (brush.isBrushing) { + return this.dragMove(event); + } + if (right.isDragging) right.dragMove(event); + }} + /> + ); + }} + + ); + } +} diff --git a/packages/xy-chart/src/utils/brush/index.js b/packages/xy-chart/src/utils/brush/index.js new file mode 100644 index 00000000..ac268b21 --- /dev/null +++ b/packages/xy-chart/src/utils/brush/index.js @@ -0,0 +1 @@ +export { default as Brush } from './Brush'; diff --git a/packages/xy-chart/src/utils/chartUtils.js b/packages/xy-chart/src/utils/chartUtils.js index b234a96a..2cf066f8 100644 --- a/packages/xy-chart/src/utils/chartUtils.js +++ b/packages/xy-chart/src/utils/chartUtils.js @@ -35,6 +35,10 @@ export function isBarSeries(name) { return /bar/gi.test(name); } +export function isBrush(name) { + return name === 'Brush'; +} + export function isCirclePackSeries(name) { return name === 'CirclePackSeries'; } diff --git a/packages/xy-chart/src/utils/drag/Drag.js b/packages/xy-chart/src/utils/drag/Drag.js new file mode 100644 index 00000000..2fac7f11 --- /dev/null +++ b/packages/xy-chart/src/utils/drag/Drag.js @@ -0,0 +1,115 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { localPoint } from '@vx/event'; + +export default class Drag extends React.Component { + constructor(props) { + super(props); + this.state = { + x: undefined, + y: undefined, + dx: 0, + dy: 0, + isDragging: false, + }; + this.dragEnd = this.dragEnd.bind(this); + this.dragMove = this.dragMove.bind(this); + this.dragStart = this.dragStart.bind(this); + } + + dragStart(event) { + const { onDragStart, resetOnStart } = this.props; + const { dx, dy } = this.state; + const point = localPoint(event); + const nextState = { + ...this.state, + isDragging: true, + dx: resetOnStart ? 0 : dx, + dy: resetOnStart ? 0 : dy, + x: resetOnStart ? point.x : -dx + point.x, + y: resetOnStart ? point.y : -dy + point.y, + }; + if (onDragStart) onDragStart({ ...nextState, event }); + this.setState(() => nextState); + } + + dragMove(event) { + const { onDragMove } = this.props; + const { x, y, isDragging } = this.state; + if (!isDragging) return; + const point = localPoint(event); + const nextState = { + ...this.state, + isDragging: true, + dx: -(x - point.x), + dy: -(y - point.y), + }; + if (onDragMove) onDragMove({ ...nextState, event }); + this.setState(() => nextState); + } + + dragEnd(event) { + const { onDragEnd } = this.props; + const nextState = { + ...this.state, + isDragging: false, + }; + if (onDragEnd) onDragEnd({ ...nextState, event }); + this.setState(() => nextState); + } + + render() { + const { + x, + y, + dx, + dy, + isDragging, + } = this.state; + const { + children, + width, + height, + captureDragArea, + } = this.props; + return ( + + {isDragging && captureDragArea && ( + + )} + {children({ + x, + y, + dx, + dy, + isDragging, + dragEnd: this.dragEnd, + dragMove: this.dragMove, + dragStart: this.dragStart, + })} + + ); + } +} + +Drag.propTypes = { + children: PropTypes.func.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + captureDragArea: PropTypes.bool, + resetOnStart: PropTypes.bool, + onDragStart: PropTypes.func.isRequired, + onDragMove: PropTypes.func.isRequired, + onDragEnd: PropTypes.func.isRequired, +}; + +Drag.defaultProps = { + captureDragArea: true, + resetOnStart: false, +}; diff --git a/packages/xy-chart/src/utils/drag/index.js b/packages/xy-chart/src/utils/drag/index.js new file mode 100644 index 00000000..9183a182 --- /dev/null +++ b/packages/xy-chart/src/utils/drag/index.js @@ -0,0 +1,2 @@ +export { default as Drag } from './Drag'; +export { default as raise } from './util/raise'; diff --git a/packages/xy-chart/src/utils/drag/util/raise.js b/packages/xy-chart/src/utils/drag/util/raise.js new file mode 100644 index 00000000..2621f80a --- /dev/null +++ b/packages/xy-chart/src/utils/drag/util/raise.js @@ -0,0 +1,7 @@ +export default function raise(items, raiseIndex) { + const array = items.slice(); + const lastIndex = array.length - 1; + const raiseItem = array.splice(raiseIndex, 1)[0]; + array.splice(lastIndex, 0, raiseItem); + return array; +} From 95108b408bbf2a36c4df09b9ed910561ea23c557 Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Mon, 20 Aug 2018 09:41:17 -0700 Subject: [PATCH 02/12] enable brush together with container level interation --- .../AreaDifferenceSeriesExampleWithBrush.jsx | 179 ++++++++++++++++++ .../01-xy-chart/BrushableLineChart.jsx | 6 +- .../01-xy-chart/LineSeriesExample.jsx | 2 +- packages/demo/examples/01-xy-chart/index.jsx | 6 + packages/xy-chart/src/chart/Voronoi.jsx | 11 +- packages/xy-chart/src/chart/XYChart.jsx | 30 ++- packages/xy-chart/src/selection/Brush.jsx | 43 ++++- packages/xy-chart/src/utils/brush/Brush.js | 75 ++++---- packages/xy-chart/src/utils/drag/Drag.js | 9 + 9 files changed, 305 insertions(+), 56 deletions(-) create mode 100644 packages/demo/examples/01-xy-chart/AreaDifferenceSeriesExampleWithBrush.jsx diff --git a/packages/demo/examples/01-xy-chart/AreaDifferenceSeriesExampleWithBrush.jsx b/packages/demo/examples/01-xy-chart/AreaDifferenceSeriesExampleWithBrush.jsx new file mode 100644 index 00000000..9e5f5dc4 --- /dev/null +++ b/packages/demo/examples/01-xy-chart/AreaDifferenceSeriesExampleWithBrush.jsx @@ -0,0 +1,179 @@ +import React from 'react'; +import { LegendOrdinal } from '@vx/legend'; +import { scaleOrdinal } from '@vx/scale'; + +import { color as colors, allColors } from '@data-ui/theme'; +import { + AreaSeries, + AreaDifferenceSeries, + CrossHair, + PatternLines, + XAxis, + YAxis, + Brush, +} from '@data-ui/xy-chart'; + +import ResponsiveXYChart, { formatYear } from './ResponsiveXYChart'; +import { timeSeriesData } from './data'; + +const COLOR_1 = allColors.grape[5]; +const COLOR_2 = allColors.pink[5]; +const PATTERN_ID = 'threshold-pattern-id'; + +const legendScale = scaleOrdinal({ + range: [`url(#${PATTERN_ID})`, COLOR_2], + domain: ['Purple', 'Pink'], +}); + +const seriesProps = [ + { + seriesKey: 'Purple', + key: 'Purple', + stroke: COLOR_1, + fill: `url(#${PATTERN_ID})`, + fillOpacity: 1, + }, + { + seriesKey: 'Pink', + key: 'Pink', + stroke: COLOR_2, + fill: COLOR_2, + fillOpacity: 0.4, + }, +]; + +const randomTimeSeries = timeSeriesData.map(d => ({ + ...d, + y: Math.random() < 0.5 ? d.y * 5 : d.y / 5, +})); + +class AreaDifferenceSeriesExampleWithBrush extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + threshold: 0.4, + brushDirection: 'horizontal', + resizeTriggerAreas: ['left', 'right'], + }; + + this.renderControls = this.renderControls.bind(this); + this.renderTooltip = this.renderTooltip.bind(this); + } + + renderControls() { + const { threshold } = this.state; + return ( +
+
+ Pink multiplier{' '} + this.setState({ threshold: Number(e.target.value) })} + />{' '} + {threshold.toFixed(2)} +
+
+ ); + } + + renderTooltip({ datum, series }) { + return ( +
+ {formatYear(datum.x)} +
+
+ {seriesProps.map( + ({ seriesKey, stroke: color }) => + series && + series[seriesKey] && ( +
+ + {`${seriesKey}`} + + {` ${series[seriesKey].y.toFixed(1)}`} +
+ ), + )} +
+ ); + } + + render() { + const { threshold, resizeTriggerAreas, brushDirection } = this.state; + const thresholdData = randomTimeSeries.map(d => ({ + ...d, + y: threshold * d.y, + })); + + return ( +
+ {this.renderControls()} +
+ ( + + + + )} + fill={({ datum }) => legendScale(datum)} + labelFormat={label => label} + /> +
+ + + + + + + + + + + +
+ ); + } +} + +export default AreaDifferenceSeriesExampleWithBrush; diff --git a/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx b/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx index 687edbbe..1f945158 100644 --- a/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx +++ b/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx @@ -41,7 +41,8 @@ class BrushableLineChart extends React.PureComponent { 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); + console.log(timeSeriesData); + pointData = timeSeriesData.filter(point => point.x > domain.x0 && point.x < domain.x1); } else { pointData = [...timeSeriesData]; } @@ -254,7 +255,7 @@ class BrushableLineChart extends React.PureComponent { ariaLabel="Required label" xScale={{ type: 'time' }} yScale={{ type: 'linear' }} - margin={{ left: 0, top: 0, bottom: 64 }} + margin={{ left: 8, top: 32, bottom: 64 }} {...rest} > @@ -289,6 +290,7 @@ class BrushableLineChart extends React.PureComponent { resizeTriggerAreas={resizeTriggerAreas} brushDirection={brushDirection} onChange={this.handleBrushChange} + brushRegion="chart" /> diff --git a/packages/demo/examples/01-xy-chart/LineSeriesExample.jsx b/packages/demo/examples/01-xy-chart/LineSeriesExample.jsx index 0aa0d15c..2730d038 100644 --- a/packages/demo/examples/01-xy-chart/LineSeriesExample.jsx +++ b/packages/demo/examples/01-xy-chart/LineSeriesExample.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { allColors } from '@data-ui/theme'; import { Button } from '@data-ui/forms'; -import { CrossHair, LineSeries, WithTooltip, XAxis, YAxis } from '@data-ui/xy-chart'; +import { CrossHair, LineSeries, WithTooltip, XAxis, YAxis, Brush } from '@data-ui/xy-chart'; import ResponsiveXYChart, { formatYear } from './ResponsiveXYChart'; import { timeSeriesData } from './data'; diff --git a/packages/demo/examples/01-xy-chart/index.jsx b/packages/demo/examples/01-xy-chart/index.jsx index 24b116af..abda859e 100644 --- a/packages/demo/examples/01-xy-chart/index.jsx +++ b/packages/demo/examples/01-xy-chart/index.jsx @@ -33,6 +33,7 @@ import StackedAreaExample from './StackedAreaExample'; import ScatterWithHistogram from './ScatterWithHistograms'; import TickLabelPlayground from './TickLabelPlayground'; import AreaDifferenceSeriesExample from './AreaDifferenceSeriesExample'; +import AreaDifferenceSeriesExampleWithBrush from './AreaDifferenceSeriesExampleWithBrush'; import { BoxPlotSeriesExample, BoxPlotViolinPlotSeriesExample } from './StatsSeriesExample'; import { @@ -244,6 +245,11 @@ export default { components: [XYChart, AreaDifferenceSeries, AreaSeries], example: () => , }, + { + description: 'AreaDifferenceSeriesWithBrush', + components: [XYChart, AreaDifferenceSeries, AreaSeries], + example: () => , + }, { description: 'AreaSeries -- confidence intervals', components: [XYChart, AreaSeries, LineSeries, PointSeries], diff --git a/packages/xy-chart/src/chart/Voronoi.jsx b/packages/xy-chart/src/chart/Voronoi.jsx index 2bcfbabe..99891faf 100644 --- a/packages/xy-chart/src/chart/Voronoi.jsx +++ b/packages/xy-chart/src/chart/Voronoi.jsx @@ -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, @@ -21,6 +22,7 @@ const defaultProps = { onClick: null, onMouseMove: null, onMouseLeave: null, + onMouseDown: null, showVoronoi: false, }; @@ -44,7 +46,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 ( @@ -62,6 +64,12 @@ class Voronoi extends React.PureComponent { onClick({ event, datum: polygon.data }); }) } + onClick={ + onClick && + (() => event => { + onClick({ event, datum: polygon.data }); + }) + } onMouseMove={ onMouseMove && (() => event => { @@ -69,6 +77,7 @@ class Voronoi extends React.PureComponent { }) } onMouseLeave={onMouseLeave && (() => onMouseLeave)} + onMouseDown={onMouseDown && (() => onMouseDown)} /> ))} diff --git a/packages/xy-chart/src/chart/XYChart.jsx b/packages/xy-chart/src/chart/XYChart.jsx index 929cb856..aeb45f0d 100644 --- a/packages/xy-chart/src/chart/XYChart.jsx +++ b/packages/xy-chart/src/chart/XYChart.jsx @@ -93,6 +93,7 @@ class XYChart extends React.PureComponent { this.handleMouseLeave = this.handleMouseLeave.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); this.handleContainerEvent = this.handleContainerEvent.bind(this); + this.registerBrushStartEvent = this.registerBrushStartEvent.bind(this); } componentDidMount() { @@ -184,6 +185,10 @@ class XYChart extends React.PureComponent { } } + registerBrushStartEvent(event) { + this.fileBrushStart = event; + } + handleMouseMove(args) { const { snapTooltipToDataX, snapTooltipToDataY, onMouseMove } = this.props; const isFocusEvent = args.event && args.event.type === 'focus'; @@ -260,6 +265,8 @@ 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; + let Brush; return ( innerWidth > 0 && @@ -324,13 +331,9 @@ class XYChart extends React.PureComponent { } else if (isReferenceLine(name)) { return React.cloneElement(Child, { xScale, yScale }); } else if (isBrush(name)) { - return React.cloneElement(Child, { - xScale, - yScale, - innerHeight, - innerWidth, - margin, - }); + hasBrush = true; + Brush = Child; + return null; } return Child; @@ -344,6 +347,7 @@ class XYChart extends React.PureComponent { width={innerWidth} height={innerHeight} onClick={this.handleClick} + onMouseDown={hasBrush ? this.fileBrushStart : null} onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave} showVoronoi={showVoronoi} @@ -358,12 +362,24 @@ class XYChart extends React.PureComponent { height={innerHeight} fill="transparent" fillOpacity={0} + onMouseDown={hasBrush ? this.fileBrushStart : null} onClick={this.handleContainerEvent} onMouseMove={this.handleContainerEvent} onMouseLeave={this.handleMouseLeave} /> )} + {hasBrush && ( + React.cloneElement(Brush, { + xScale, + yScale, + innerHeight, + innerWidth, + margin, + registerStartEvent: this.registerBrushStartEvent, + }) + )} + {tooltipData && CrossHairs.length > 0 && CrossHairs.map((CrossHair, i) => diff --git a/packages/xy-chart/src/selection/Brush.jsx b/packages/xy-chart/src/selection/Brush.jsx index 9af8dcd1..42a9d8b9 100644 --- a/packages/xy-chart/src/selection/Brush.jsx +++ b/packages/xy-chart/src/selection/Brush.jsx @@ -4,6 +4,8 @@ import { color } from '@data-ui/theme'; import BaseBrush from '../utils/brush/Brush'; +const SAFE_PIXEL = 2; + export const propTypes = { label: PropTypes.node, stroke: PropTypes.string, @@ -39,11 +41,10 @@ class Brush extends React.Component { onChange(null); return; } - const invertedX0 = xScale.invert(x0 - margin.left); - const invertedX1 = xScale.invert(x1 - margin.left); - const invertedY0 = yScale.invert(y0 - margin.top); - const invertedY1 = yScale.invert(y1 - margin.top); - + const invertedX0 = xScale.invert(x0 + (x0 < x1 ? -SAFE_PIXEL : SAFE_PIXEL)); + const invertedX1 = xScale.invert(x1 + (x1 < x0 ? -SAFE_PIXEL : SAFE_PIXEL)); + const invertedY0 = yScale.invert(y0 + (y0 < y1 ? -SAFE_PIXEL : SAFE_PIXEL)); + const invertedY1 = yScale.invert(y1 + (y1 < y0 ? -SAFE_PIXEL : SAFE_PIXEL)); const domainRange = { x0: Math.min(invertedX0, invertedX1), x1: Math.max(invertedX0, invertedX1), @@ -67,19 +68,43 @@ class Brush extends React.Component { onChange, brushDirection, resizeTriggerAreas, + brushRegion, + registerStartEvent, } = this.props; if (!xScale || !yScale) return null; + + let brushRegionWidth, brushRegionHeight, left, top; + + if (brushRegion === 'chart') { + left = 0; + top = 0; + brushRegionWidth = innerWidth; + brushRegionHeight = innerHeight; + } else if (brushRegion === 'yAxis') { + left = -margin.left; + top = 0; + brushRegionWidth = margin.left; + brushRegionHeight = innerHeight; + } else { + left = 0; + top = innerHeight; + brushRegionWidth = innerWidth; + brushRegionHeight = margin.bottom; + } + return ( ); } } diff --git a/packages/xy-chart/src/utils/brush/Brush.js b/packages/xy-chart/src/utils/brush/Brush.js index c339ba67..69ab3c69 100644 --- a/packages/xy-chart/src/utils/brush/Brush.js +++ b/packages/xy-chart/src/utils/brush/Brush.js @@ -62,19 +62,19 @@ export default class Brush extends React.Component { } handleDragStart(draw) { - const { onBrushStart, left, top } = this.props; + const { onBrushStart, left, top, inheritedMargin } = this.props; if (onBrushStart) { onBrushStart(); } this.update(prevBrush => ({ ...prevBrush, start: { - x: draw.x - left, - y: draw.y - top, + x: draw.x + draw.dx - left - inheritedMargin.left, + y: draw.y + draw.dy - top - inheritedMargin.top, }, end: { - x: draw.x - left, - y: draw.y - top, + x: draw.x + draw.dx - left - inheritedMargin.left, + y: draw.y + draw.dy - top - inheritedMargin.top, }, extent: { x0: -1, @@ -87,11 +87,11 @@ export default class Brush extends React.Component { } handleDragMove(draw) { - const { left, top } = this.props; + const { left, top, inheritedMargin } = this.props; if (!draw.isDragging) return; const end = { - x: draw.x + draw.dx - left, - y: draw.y + draw.dy - top, + x: draw.x + draw.dx - left - inheritedMargin.left, + y: draw.y + draw.dy - top - inheritedMargin.top, }; this.update((prevBrush) => { const { start } = prevBrush; @@ -235,6 +235,7 @@ export default class Brush extends React.Component { onMouseMove, resizeTriggerAreas, brushDirection, + registerStartEvent } = this.props; const handles = this.handles(); @@ -242,7 +243,6 @@ export default class Brush extends React.Component { const width = this.width(); const height = this.height(); const resizeTriggerAreaSet = new Set(resizeTriggerAreas); - return ( {/* overlay */} @@ -253,34 +253,37 @@ export default class Brush extends React.Component { onDragStart={this.handleDragStart} onDragMove={this.handleDragMove} onDragEnd={this.handleDragEnd} + registerStartEvent={registerStartEvent} > - {draw => { - return ( - event => this.reset(event)} - onMouseDown={data => event => draw.dragStart(event)} - onMouseLeave={data => event => { - if (onMouseLeave) onMouseLeave(event); - }} - onMouseMove={data => event => { - if (!draw.isDragging && onMouseMove) - this.props.onMouseMove(event); - if (draw.isDragging) draw.dragMove(event); - }} - onMouseUp={data => event => { - if (onMouseUp) onMouseUp(event); - draw.dragEnd(event); - }} - style={{ cursor: 'crosshair' }} - /> - ); + {(draw) => { + if (!registerStartEvent || (registerStartEvent && draw.isDragging)) { + return ( + event => this.reset(event)} + onMouseDown={data => event => draw.dragStart(event)} + onMouseLeave={data => event => { + if (onMouseLeave) onMouseLeave(event); + }} + onMouseMove={data => event => { + if (!draw.isDragging && onMouseMove) + this.props.onMouseMove(event); + if (draw.isDragging) draw.dragMove(event); + }} + onMouseUp={data => event => { + if (onMouseUp) onMouseUp(event); + draw.dragEnd(event); + }} + style={{ cursor: 'crosshair' }} + /> + ); + } }} {/* selection */} diff --git a/packages/xy-chart/src/utils/drag/Drag.js b/packages/xy-chart/src/utils/drag/Drag.js index 2fac7f11..31d80754 100644 --- a/packages/xy-chart/src/utils/drag/Drag.js +++ b/packages/xy-chart/src/utils/drag/Drag.js @@ -15,6 +15,15 @@ export default class Drag extends React.Component { this.dragEnd = this.dragEnd.bind(this); this.dragMove = this.dragMove.bind(this); this.dragStart = this.dragStart.bind(this); + if (props.registerStartEvent) { + props.registerStartEvent(this.dragStart); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.registerStartEvent) { + nextProps.registerStartEvent(this.dragStart); + } } dragStart(event) { From 5caf62489d775199ba67929217bc3e8fb106cf86 Mon Sep 17 00:00:00 2001 From: Conglei Shi Date: Tue, 21 Aug 2018 10:16:56 -0700 Subject: [PATCH 03/12] fixed lints --- .../AreaDifferenceSeriesExampleWithBrush.jsx | 1 + .../01-xy-chart/BrushableLineChart.jsx | 140 ++++++++++-- .../01-xy-chart/LineSeriesExample.jsx | 2 +- .../examples/01-xy-chart/LinkedXYCharts.jsx | 12 +- .../01-xy-chart/ScatterWithHistograms.jsx | 27 ++- .../01-xy-chart/TickLabelPlayground.jsx | 12 +- packages/demo/examples/01-xy-chart/data.js | 5 +- .../02-histogram/HistogramPlayground.jsx | 16 +- packages/xy-chart/src/chart/Voronoi.jsx | 13 +- packages/xy-chart/src/chart/XYChart.jsx | 38 +++- packages/xy-chart/src/selection/Brush.jsx | 84 +++++-- .../src/utils/brush/{Brush.js => Brush.jsx} | 210 ++++++++++-------- .../brush/{BrushCorner.js => BrushCorner.jsx} | 134 ++++------- .../brush/{BrushHandle.js => BrushHandle.jsx} | 81 ++++--- .../src/utils/brush/BrushSelection.js | 114 +++++----- .../brush/{RightHandle.js => RightHandle.jsx} | 67 +++--- .../src/utils/drag/{Drag.js => Drag.jsx} | 35 ++- .../xy-chart/src/utils/drag/util/raise.js | 1 + packages/xy-chart/src/utils/propShapes.js | 15 ++ 19 files changed, 603 insertions(+), 404 deletions(-) rename packages/xy-chart/src/utils/brush/{Brush.js => Brush.jsx} (64%) rename packages/xy-chart/src/utils/brush/{BrushCorner.js => BrushCorner.jsx} (53%) rename packages/xy-chart/src/utils/brush/{BrushHandle.js => BrushHandle.jsx} (69%) rename packages/xy-chart/src/utils/brush/{RightHandle.js => RightHandle.jsx} (63%) rename packages/xy-chart/src/utils/drag/{Drag.js => Drag.jsx} (85%) diff --git a/packages/demo/examples/01-xy-chart/AreaDifferenceSeriesExampleWithBrush.jsx b/packages/demo/examples/01-xy-chart/AreaDifferenceSeriesExampleWithBrush.jsx index 9e5f5dc4..85ae9b9f 100644 --- a/packages/demo/examples/01-xy-chart/AreaDifferenceSeriesExampleWithBrush.jsx +++ b/packages/demo/examples/01-xy-chart/AreaDifferenceSeriesExampleWithBrush.jsx @@ -62,6 +62,7 @@ class AreaDifferenceSeriesExampleWithBrush extends React.PureComponent { renderControls() { const { threshold } = this.state; + return (
diff --git a/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx b/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx index 1f945158..29c878e2 100644 --- a/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx +++ b/packages/demo/examples/01-xy-chart/BrushableLineChart.jsx @@ -6,6 +6,7 @@ import { XYChart, CrossHair, XAxis, + YAxis, theme, withScreenSize, LineSeries, @@ -16,9 +17,7 @@ import { import colors from '@data-ui/theme/lib/color'; -import { - timeSeriesData, -} from './data'; +import { timeSeriesData } from './data'; import PointSeries from '../../node_modules/@data-ui/xy-chart/lib/series/PointSeries'; export const parseDate = timeParse('%Y%m%d'); @@ -26,7 +25,6 @@ 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); @@ -34,15 +32,30 @@ class BrushableLineChart extends React.PureComponent { pointData: [...timeSeriesData], brushDirection: 'horizontal', resizeTriggerAreas: ['left', 'right'], + brushRegion: 'chart', + xAxisOrientation: 'bottom', + yAxisOrientation: 'left', }; this.handleBrushChange = this.handleBrushChange.bind(this); } handleBrushChange(domain) { + const { brushDirection } = this.state; let pointData; if (domain) { - console.log(timeSeriesData); - pointData = timeSeriesData.filter(point => point.x > domain.x0 && point.x < domain.x1); + if (brushDirection === 'horizontal') { + pointData = timeSeriesData.filter(point => point.x > domain.x0 && point.x < domain.x1); + } else if (brushDirection === 'vertical') { + pointData = timeSeriesData.filter(point => point.y > domain.y0 && point.y < domain.y1); + } else { + pointData = timeSeriesData.filter( + point => + point.x > domain.x0 && + point.x < domain.x1 && + point.y > domain.y0 && + point.y < domain.y1, + ); + } } else { pointData = [...timeSeriesData]; } @@ -54,6 +67,7 @@ class BrushableLineChart extends React.PureComponent { renderControls() { const { resizeTriggerAreas } = this.state; const resizeTriggerAreaset = new Set(resizeTriggerAreas); + return (

Brush Props

@@ -88,6 +102,91 @@ class BrushableLineChart extends React.PureComponent {
+
+ Brush brushRegion: + + + +
+ +
+ X Axis Orientation: + + +
+ +
+ Y Axis Orientation: + + +
+
Resize Trigger Border: