From 88d1247132fdf5e1edeb73d8155448221ddc7f8e Mon Sep 17 00:00:00 2001 From: geekplux Date: Tue, 22 Oct 2019 17:23:15 +0800 Subject: [PATCH 01/16] chore(vx-demo): add brush demo --- packages/vx-demo/src/components/gallery.js | 24 +++++++++++++++++++ .../vx-demo/src/components/tiles/brush.js | 7 ++++++ packages/vx-demo/src/pages/brush.js | 11 +++++++++ 3 files changed, 42 insertions(+) create mode 100644 packages/vx-demo/src/components/tiles/brush.js create mode 100644 packages/vx-demo/src/pages/brush.js diff --git a/packages/vx-demo/src/components/gallery.js b/packages/vx-demo/src/components/gallery.js index a3ac6ae1b..bb13bd67a 100644 --- a/packages/vx-demo/src/components/gallery.js +++ b/packages/vx-demo/src/components/gallery.js @@ -42,6 +42,7 @@ import Threshold from './tiles/threshold'; import Chord from './tiles/chord'; import Polygons from './tiles/polygons'; import ZoomI from './tiles/zoom-i'; +import BrushChart from './tiles/brush'; const items = [ '#242424', @@ -63,6 +64,29 @@ export default function() { return (
+ + +
+
+ + {({ width, height }) => ( + + )} + +
+
+
Brush
+
+
{''}
+
+
+
+ +
diff --git a/packages/vx-demo/src/components/tiles/brush.js b/packages/vx-demo/src/components/tiles/brush.js new file mode 100644 index 000000000..ad6159085 --- /dev/null +++ b/packages/vx-demo/src/components/tiles/brush.js @@ -0,0 +1,7 @@ +import React from 'react'; + +function BrushChart() { + return
brush
; +} + +export default BrushChart; diff --git a/packages/vx-demo/src/pages/brush.js b/packages/vx-demo/src/pages/brush.js new file mode 100644 index 000000000..3e2ce3fbd --- /dev/null +++ b/packages/vx-demo/src/pages/brush.js @@ -0,0 +1,11 @@ +import React from 'react'; +import Show from '../components/show'; +import BrushChart from '../components/tiles/brush'; + +export default () => { + return ( + + {`import React from 'react';`} + + ); +}; From e278536fcfd9f881daf6ef79938cf45aac15ac63 Mon Sep 17 00:00:00 2001 From: geekplux Date: Tue, 22 Oct 2019 17:56:34 +0800 Subject: [PATCH 02/16] feat(vx-demo): add an aera into brush demo --- packages/vx-demo/src/components/gallery.js | 8 +- .../vx-demo/src/components/tiles/brush.js | 77 ++++++++++++++++++- packages/vx-demo/src/pages/brush.js | 11 ++- 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/packages/vx-demo/src/components/gallery.js b/packages/vx-demo/src/components/gallery.js index bb13bd67a..af551af27 100644 --- a/packages/vx-demo/src/components/gallery.js +++ b/packages/vx-demo/src/components/gallery.js @@ -69,13 +69,7 @@ export default function() {
- {({ width, height }) => ( - - )} + {({ width, height }) => }
diff --git a/packages/vx-demo/src/components/tiles/brush.js b/packages/vx-demo/src/components/tiles/brush.js index ad6159085..9b01b852a 100644 --- a/packages/vx-demo/src/components/tiles/brush.js +++ b/packages/vx-demo/src/components/tiles/brush.js @@ -1,7 +1,80 @@ import React from 'react'; +import { Group } from '@vx/group'; +import { GridRows, GridColumns } from '@vx/grid'; +import { AreaClosed, Line, Bar } from '@vx/shape'; +import { AxisLeft, AxisBottom } from '@vx/axis'; +import { curveMonotoneX } from '@vx/curve'; +import { scaleTime, scaleLinear } from '@vx/scale'; +import { appleStock } from '@vx/mock-data'; -function BrushChart() { - return
brush
; +const stock = appleStock.slice(800); +const min = (arr, fn) => Math.min(...arr.map(fn)); +const max = (arr, fn) => Math.max(...arr.map(fn)); +const extent = (arr, fn) => [min(arr, fn), max(arr, fn)]; + +// accessors +const xStock = d => new Date(d.date); +const yStock = d => d.close; + +function BrushChart({ width, height, margin = { top: 0, left: 50, bottom: 80, right: 20 } }) { + console.log(width, height, margin); + // bounds + const xMax = width - margin.left - margin.right; + const yMax = height - margin.top - margin.bottom; + + // scales + const xScale = scaleTime({ + range: [0, xMax], + domain: extent(stock, xStock), + }); + const yScale = scaleLinear({ + range: [yMax, 0], + domain: [0, max(stock, yStock) + yMax / 3], + nice: true, + }); + + return ( +
+ + + + + + + + + + + + 520 ? 10 : 5} /> + + xScale(xStock(d))} + y={d => yScale(yStock(d))} + yScale={yScale} + strokeWidth={1} + stroke="url(#gradient)" + fill="url(#gradient)" + curve={curveMonotoneX} + /> + + + +
+ ); } export default BrushChart; diff --git a/packages/vx-demo/src/pages/brush.js b/packages/vx-demo/src/pages/brush.js index 3e2ce3fbd..edf159a0f 100644 --- a/packages/vx-demo/src/pages/brush.js +++ b/packages/vx-demo/src/pages/brush.js @@ -4,7 +4,16 @@ import BrushChart from '../components/tiles/brush'; export default () => { return ( - + {`import React from 'react';`} ); From 12fe3f4a9101a841c512294d9cd6cbba5b3c8065 Mon Sep 17 00:00:00 2001 From: geekplux Date: Tue, 22 Oct 2019 18:20:40 +0800 Subject: [PATCH 03/16] feat(v-demo): add overview chart into brush demo --- .../vx-demo/src/components/tiles/brush.js | 97 ++++++++++++------- packages/vx-demo/src/pages/brush.js | 2 +- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/packages/vx-demo/src/components/tiles/brush.js b/packages/vx-demo/src/components/tiles/brush.js index 9b01b852a..f72d051b2 100644 --- a/packages/vx-demo/src/components/tiles/brush.js +++ b/packages/vx-demo/src/components/tiles/brush.js @@ -16,8 +16,15 @@ const extent = (arr, fn) => [min(arr, fn), max(arr, fn)]; const xStock = d => new Date(d.date); const yStock = d => d.close; -function BrushChart({ width, height, margin = { top: 0, left: 50, bottom: 80, right: 20 } }) { - console.log(width, height, margin); +function AreaChart({ + width, + height, + margin = { top: 50, left: 50, bottom: 0, right: 20 }, + axis = false, + grid = false, + top, + left, +}) { // bounds const xMax = width - margin.left - margin.right; const yMax = height - margin.top - margin.bottom; @@ -33,45 +40,61 @@ function BrushChart({ width, height, margin = { top: 0, left: 50, bottom: 80, ri nice: true, }); + return ( + + + + + + + + {grid && ( + + )} + {grid && ( + + )} + {axis && 520 ? 10 : 5} />} + {axis && } + xScale(xStock(d))} + y={d => yScale(yStock(d))} + yScale={yScale} + strokeWidth={1} + stroke="url(#gradient)" + fill="url(#gradient)" + curve={curveMonotoneX} + /> + + + ); +} + +function BrushChart({ width, height, margin }) { return (
- - - - - - - - - - 520 ? 10 : 5} /> - - xScale(xStock(d))} - y={d => yScale(yStock(d))} - yScale={yScale} - strokeWidth={1} - stroke="url(#gradient)" - fill="url(#gradient)" - curve={curveMonotoneX} - /> - - + +
); diff --git a/packages/vx-demo/src/pages/brush.js b/packages/vx-demo/src/pages/brush.js index edf159a0f..9bc53c5ea 100644 --- a/packages/vx-demo/src/pages/brush.js +++ b/packages/vx-demo/src/pages/brush.js @@ -8,7 +8,7 @@ export default () => { component={BrushChart} title="BrushChart" margin={{ - top: 0, + top: 50, left: 50, right: 20, bottom: 50, From 2de24b9ac9c636f108f7f6bef91a16c31ba41b7f Mon Sep 17 00:00:00 2001 From: geekplux Date: Mon, 28 Oct 2019 18:25:03 +0800 Subject: [PATCH 04/16] feat: improve demo styles --- .../vx-demo/src/components/tiles/brush.js | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/vx-demo/src/components/tiles/brush.js b/packages/vx-demo/src/components/tiles/brush.js index f72d051b2..40c4030ba 100644 --- a/packages/vx-demo/src/components/tiles/brush.js +++ b/packages/vx-demo/src/components/tiles/brush.js @@ -7,10 +7,29 @@ import { curveMonotoneX } from '@vx/curve'; import { scaleTime, scaleLinear } from '@vx/scale'; import { appleStock } from '@vx/mock-data'; +/** + * Initialize some variables + */ const stock = appleStock.slice(800); const min = (arr, fn) => Math.min(...arr.map(fn)); const max = (arr, fn) => Math.max(...arr.map(fn)); const extent = (arr, fn) => [min(arr, fn), max(arr, fn)]; +const axisColor = '#fff'; +const axisBottomTickLabelProps = { + textAnchor: 'middle', + fontFamily: 'Arial', + fontSize: 10, + fill: axisColor, +}; +const axisLeftTickLabelProps = { + dx: '-0.25em', + dy: '0.25em', + fill: 'black', + fontFamily: 'Arial', + fontSize: 10, + textAnchor: 'end', + fill: axisColor, +}; // accessors const xStock = d => new Date(d.date); @@ -66,8 +85,25 @@ function AreaChart({ stroke="rgba(255,255,255,0.3)" /> )} - {axis && 520 ? 10 : 5} />} - {axis && } + {axis && ( + 520 ? 10 : 5} + stroke={axisColor} + tickStroke={axisColor} + tickLabelProps={() => axisBottomTickLabelProps} + /> + )} + {axis && ( + axisLeftTickLabelProps} + /> + )} xScale(xStock(d))} From e4202ec4047090540aff364b22ad1c4881d2a8da Mon Sep 17 00:00:00 2001 From: geekplux Date: Mon, 28 Oct 2019 18:26:21 +0800 Subject: [PATCH 05/16] chore: copy and migrating brush from data-ui --- packages/vx-brush/package.json | 1 + packages/vx-brush/src/Brush.tsx | 455 ++++++++++++++++++ packages/vx-brush/src/BrushCorner.tsx | 194 ++++++++ packages/vx-brush/src/BrushHandle.tsx | 166 +++++++ packages/vx-brush/src/BrushSelection.tsx | 161 +++++++ packages/vx-brush/src/brushes/BoxBrush.jsx | 52 -- packages/vx-brush/src/enhancers/withBrush.js | 51 -- packages/vx-brush/src/index.js | 4 - packages/vx-brush/src/index.ts | 1 + packages/vx-brush/src/types.ts | 55 +++ .../vx-brush/src/utils/constrainToRegion.js | 7 - .../vx-brush/src/utils/getCoordsFromEvent.js | 19 - 12 files changed, 1033 insertions(+), 133 deletions(-) create mode 100644 packages/vx-brush/src/Brush.tsx create mode 100644 packages/vx-brush/src/BrushCorner.tsx create mode 100644 packages/vx-brush/src/BrushHandle.tsx create mode 100644 packages/vx-brush/src/BrushSelection.tsx delete mode 100644 packages/vx-brush/src/brushes/BoxBrush.jsx delete mode 100644 packages/vx-brush/src/enhancers/withBrush.js delete mode 100644 packages/vx-brush/src/index.js create mode 100644 packages/vx-brush/src/index.ts create mode 100644 packages/vx-brush/src/types.ts delete mode 100644 packages/vx-brush/src/utils/constrainToRegion.js delete mode 100644 packages/vx-brush/src/utils/getCoordsFromEvent.js diff --git a/packages/vx-brush/package.json b/packages/vx-brush/package.json index a37ad2290..d4528d3d9 100644 --- a/packages/vx-brush/package.json +++ b/packages/vx-brush/package.json @@ -33,6 +33,7 @@ "react": "^15.0.0-0 || ^16.0.0-0" }, "dependencies": { + "@vx/drag": "^0.0.192", "classnames": "^2.2.5", "prop-types": "^15.6.1", "recompose": "^0.23.1" diff --git a/packages/vx-brush/src/Brush.tsx b/packages/vx-brush/src/Brush.tsx new file mode 100644 index 000000000..070809f3a --- /dev/null +++ b/packages/vx-brush/src/Brush.tsx @@ -0,0 +1,455 @@ +import React from 'react'; +import { Group } from '@vx/group'; +import { Bar } from '@vx/shape'; +import { Drag } from '@vx/drag'; + +import BrushHandle from './BrushHandle'; +import BrushCorner from './BrushCorner'; +import BrushSelection from './BrushSelection'; +import { GeneralStyleShape, MarginShape } from './types'; + +export type BrushProps = { + brushDirection?: 'horizontal' | 'vertical' | 'both'; + width: number; + height: number; + left: number; + top: number; + inheritedMargin?: MarginShape; + onChange?: Function; + handleSize?: number; + resizeTriggerAreas?: [ + 'left' | 'right' | 'top' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight', + ]; + onBrushStart?: Function; + onBrushEnd?: Function; + selectedBoxStyle: GeneralStyleShape; + onMouseLeave?: Function; + onMouseUp?: Function; + onMouseMove?: Function; + onClick?: Function; + clickSensitivity?: number; + disableDraggingSelection: boolean; +}; + +export type BrushState = { + start: { + x: number; + y: number; + }; + end: { + x: number; + y: number; + }; + extent: { + x0: number; + x1: number; + y0: number; + y1: number; + }; + bounds: { + x0: number; + x1: number; + y0: number; + y1: number; + }; +}; + +export default class Brush extends React.Component { + private mouseUpTime: number; + private mouseDownTime: number; + + static defaultProps = { + brushDirection: 'both', + inheritedMargin: { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + onChange: null, + handleSize: 4, + resizeTriggerAreas: ['left', 'right'], + onBrushStart: null, + onBrushEnd: null, + onMouseLeave: null, + onMouseUp: null, + onMouseMove: null, + onClick: null, + disableDraggingSelection: false, + clickSensitivity: 200, + }; + + constructor(props: BrushProps) { + super(props); + const { width, height } = 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: width, + y0: 0, + y1: 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); + this.mouseUpTime = 0; + this.mouseDownTime = 0; + } + + componentWillReceiveProps(nextProps: BrushProps) { + if (['width', 'height'].some(prop => this.props[prop] !== nextProps[prop])) { + this.setState(() => ({ + bounds: { + x0: 0, + x1: nextProps.width, + y0: 0, + y1: nextProps.height, + }, + })); + } + } + + 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, inheritedMargin } = this.props; + const start = { + x: draw.x + draw.dx - left - inheritedMargin.left, + y: draw.y + draw.dy - top - inheritedMargin.top, + }; + const end = { ...start }; + + if (onBrushStart) { + onBrushStart(start); + } + + this.update(prevBrush => ({ + ...prevBrush, + start, + end, + extent: { + x0: -1, + x1: -1, + y0: -1, + y1: -1, + }, + isBrushing: true, + })); + } + + handleDragMove(draw) { + const { left, top, inheritedMargin } = this.props; + if (!draw.isDragging) return; + const end = { + x: draw.x + draw.dx - left - inheritedMargin.left, + y: draw.y + draw.dy - top - inheritedMargin.top, + }; + this.update(prevBrush => { + const { start } = prevBrush; + const extent = this.getExtent(start, end); + + return { + ...prevBrush, + end, + extent, + }; + }); + } + + handleDragEnd() { + const { onBrushEnd } = this.props; + this.update(prevBrush => { + const { extent } = prevBrush; + const newState = { + ...prevBrush, + start: { + x: extent.x0, + y: extent.y0, + }, + end: { + x: extent.x1, + y: extent.y1, + }, + isBrushing: false, + }; + if (onBrushEnd) { + onBrushEnd(newState); + } + + return newState; + }); + } + + 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) { + const { onChange } = this.props; + this.setState(updater, () => { + if (onChange) { + onChange(this.state); + } + }); + } + + reset() { + const { width, height } = this.props; + this.update(() => ({ + start: undefined, + end: undefined, + extent: { + x0: undefined, + x1: undefined, + y0: undefined, + y1: undefined, + }, + bounds: { + x0: 0, + x1: width, + y0: 0, + y1: height, + }, + isBrushing: false, + activeHandle: undefined, + })); + } + + render() { + const { start, end } = this.state; + const { + top, + left, + width: stageWidth, + height: stageHeight, + handleSize, + onMouseLeave, + onMouseUp, + onMouseMove, + onBrushEnd, + onClick, + resizeTriggerAreas, + selectedBoxStyle, + disableDraggingSelection, + clickSensitivity, + } = 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 => ( + event => this.reset(event)} + onClick={() => event => { + const duration = this.mouseUpTime - this.mouseDownTime; + if (onClick && duration < clickSensitivity) onClick(event); + }} + onMouseDown={() => event => { + this.mouseDownTime = new Date(); + draw.dragStart(event); + }} + onMouseLeave={() => event => { + if (onMouseLeave) onMouseLeave(event); + }} + onMouseMove={() => event => { + if (!draw.isDragging && onMouseMove) onMouseMove(event); + if (draw.isDragging) draw.dragMove(event); + }} + onMouseUp={() => event => { + this.mouseUpTime = new Date(); + 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 => { + const handle = handles[handleKey]; + + return ( + + ); + })} + {/* corners */} + {start && + end && + Object.keys(corners) + .filter(cornerKey => resizeTriggerAreaSet.has(cornerKey)) + .map(cornerKey => { + const corner = corners[cornerKey]; + + return ( + + ); + })} + + ); + } +} diff --git a/packages/vx-brush/src/BrushCorner.tsx b/packages/vx-brush/src/BrushCorner.tsx new file mode 100644 index 000000000..bc4f6b062 --- /dev/null +++ b/packages/vx-brush/src/BrushCorner.tsx @@ -0,0 +1,194 @@ +/* eslint react/jsx-handler-names: 0 */ +import React from 'react'; +import { Drag } from '@vx/drag'; +import { GeneralStyleShape, Brush } from './types'; + +export type BrushCornerProps = { + stageWidth: number; + stageHeight: number; + brush: Brush; + updateBrush: Function; + onBrushEnd?: Function; + type: string; + style?: GeneralStyleShape; +}; + +export type BrushCornerState = {}; + +export default class BrushCorner extends React.Component { + static defaultProps = { + style: {}, + }; + + constructor(props: BrushCornerProps) { + super(props); + this.cornerDragMove = this.cornerDragMove.bind(this); + this.cornerDragEnd = this.cornerDragEnd.bind(this); + } + + cornerDragMove(drag) { + const { updateBrush, type } = this.props; + if (!drag.isDragging) return; + updateBrush(prevBrush => { + const { start, end } = 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; + default: + break; + } + + return nextState; + }); + } + + cornerDragEnd() { + const { updateBrush, onBrushEnd } = 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 nextBrush = { + ...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), + }, + }; + if (onBrushEnd) { + onBrushEnd(nextBrush); + } + + return nextBrush; + }); + } + + render() { + const { + type, + brush, + updateBrush, + stageWidth, + stageHeight, + style: styleProp, + onBrushEnd, + ...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 ( + + {handle => ( + + {handle.isDragging && ( + + )} + + + )} + + ); + } +} diff --git a/packages/vx-brush/src/BrushHandle.tsx b/packages/vx-brush/src/BrushHandle.tsx new file mode 100644 index 000000000..62b72840b --- /dev/null +++ b/packages/vx-brush/src/BrushHandle.tsx @@ -0,0 +1,166 @@ +/* eslint react/jsx-handler-names: 0 */ +import React from 'react'; +import { Drag } from '@vx/drag'; +import { Brush, DragShape } from './types'; + +export type BrushHandleProps = { + stageWidth: number; + stageHeight: number; + brush: Brush; + updateBrush: Function; + onBrushEnd?: Function; + handle: DragShape; + type: string; +}; + +export default class BrushHandle extends React.Component { + constructor(props: BrushHandleProps) { + super(props); + this.handleDragMove = this.handleDragMove.bind(this); + this.handleDragEnd = this.handleDragEnd.bind(this); + } + + handleDragMove(drag) { + const { updateBrush, type } = this.props; + if (!drag.isDragging) return; + updateBrush(prevBrush => { + const { start, end } = 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; + default: + break; + } + + return nextState; + }); + } + + handleDragEnd() { + const { updateBrush, onBrushEnd } = 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 nextBrush = { + ...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), + }, + }; + if (onBrushEnd) { + onBrushEnd(nextBrush); + } + + return nextBrush; + }); + } + + render() { + const { stageWidth, stageHeight, brush, type, handle } = this.props; + const { x, y, width, height } = handle; + const cursor = type === 'right' || type === 'left' ? 'ew-resize' : 'ns-resize'; + + return ( + + {drag => ( + + {handle.isDragging && ( + + )} + + + )} + + ); + } +} diff --git a/packages/vx-brush/src/BrushSelection.tsx b/packages/vx-brush/src/BrushSelection.tsx new file mode 100644 index 000000000..eeb0e1aaf --- /dev/null +++ b/packages/vx-brush/src/BrushSelection.tsx @@ -0,0 +1,161 @@ +/* eslint react/jsx-handler-names: 0 */ +import React from 'react'; +import { Drag } from '@vx/drag'; +import { Brush } from './types'; + +export type BrushSelectionProps = { + width: number; + height: number; + stageWidth: number; + stageHeight: number; + brush: Brush; + updateBrush: Function; + onBrushEnd?: Function; + disableDraggingSelection: boolean; + onMouseLeave: Function; + onMouseMove: Function; + onMouseUp: Function; + onClick: Function; +}; + +export default class BrushSelection extends React.Component { + static defaultProps = { + onMouseLeave: null, + onMouseUp: null, + onMouseMove: null, + onClick: null, + }; + + constructor(props: BrushSelectionProps) { + 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() { + const { updateBrush, onBrushEnd } = this.props; + updateBrush(prevBrush => { + const nextBrush = { + ...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), + }, + }; + if (onBrushEnd) { + onBrushEnd(nextBrush); + } + + return nextBrush; + }); + } + + render() { + const { + width, + height, + stageWidth, + stageHeight, + brush, + updateBrush, + disableDraggingSelection, + onBrushEnd, + onMouseLeave, + onMouseMove, + onMouseUp, + onClick, + ...restProps + } = this.props; + + return ( + + {selection => ( + + {selection.isDragging && ( + + )} + { + if (onMouseLeave) onMouseLeave(event); + }} + onMouseMove={event => { + selection.dragMove(event); + if (onMouseMove) onMouseMove(event); + }} + onMouseUp={event => { + selection.dragEnd(event); + if (onMouseUp) onMouseUp(event); + }} + onClick={event => { + if (onClick) onClick(event); + }} + style={{ + pointerEvents: brush.isBrushing || brush.activeHandle ? 'none' : 'all', + cursor: disableDraggingSelection ? null : 'move', + }} + {...restProps} + /> + + )} + + ); + } +} diff --git a/packages/vx-brush/src/brushes/BoxBrush.jsx b/packages/vx-brush/src/brushes/BoxBrush.jsx deleted file mode 100644 index 9ae80fcb0..000000000 --- a/packages/vx-brush/src/brushes/BoxBrush.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; - -const propTypes = { - brush: PropTypes.shape({ - start: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), - end: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), - isBrushing: PropTypes.bool, - }), - className: PropTypes.string, - fill: PropTypes.string, - stroke: PropTypes.string, - strokeWidth: PropTypes.oneOf([PropTypes.string, PropTypes.number]), -}; - -function BoxBrush({ - brush, - className, - fill = 'rgba(102, 181, 245, 0.1)', - stroke = 'rgba(102, 181, 245, 1)', - strokeWidth = 1, - ...otherProps -}) { - const { start, end, isBrushing } = brush; - if (!start) return null; - if (!end) return null; - const x = end.x > start.x ? start.x : end.x; - const y = end.y > start.y ? start.y : end.y; - const width = Math.abs(start.x - end.x); - const height = Math.abs(start.y - end.y); - return ( - - {isBrushing && ( - - )} - - ); -} - -BoxBrush.propTypes = propTypes; - -export default BoxBrush; diff --git a/packages/vx-brush/src/enhancers/withBrush.js b/packages/vx-brush/src/enhancers/withBrush.js deleted file mode 100644 index 63e6d0603..000000000 --- a/packages/vx-brush/src/enhancers/withBrush.js +++ /dev/null @@ -1,51 +0,0 @@ -import { compose, withState, withHandlers } from 'recompose'; - -export default compose( - withState('brush', 'updateBrush', { - start: undefined, - end: undefined, - domain: undefined, - isBrushing: false, - }), - withHandlers({ - onBrushStart: ({ updateBrush }) => ({ x, y }) => { - updateBrush(prevState => ({ - ...prevState, - start: { x, y }, - isBrushing: true, - end: undefined, - domain: undefined, - })); - }, - onBrushDrag: ({ updateBrush }) => ({ x, y }) => { - updateBrush(prevState => ({ - ...prevState, - end: { x, y }, - domain: undefined, - })); - }, - onBrushEnd: ({ updateBrush }) => ({ x, y }) => { - updateBrush(prevState => { - const { start } = prevState; - return { - ...prevState, - isBrushing: false, - domain: { - x0: Math.min(start.x, x), - x1: Math.max(start.x, x), - y0: Math.min(start.y, y), - y1: Math.max(start.y, y), - }, - }; - }); - }, - onBrushReset: ({ updateBrush }) => (/** event */) => { - updateBrush((/** prevState */) => ({ - start: undefined, - end: undefined, - domain: undefined, - isBrushing: false, - })); - }, - }), -); diff --git a/packages/vx-brush/src/index.js b/packages/vx-brush/src/index.js deleted file mode 100644 index 666908daf..000000000 --- a/packages/vx-brush/src/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { default as withBrush } from './enhancers/withBrush'; -export { default as BoxBrush } from './brushes/BoxBrush'; -export { default as constrainToRegion } from './utils/constrainToRegion'; -export { default as getCoordsFromEvent } from './utils/getCoordsFromEvent'; diff --git a/packages/vx-brush/src/index.ts b/packages/vx-brush/src/index.ts new file mode 100644 index 000000000..ac268b21e --- /dev/null +++ b/packages/vx-brush/src/index.ts @@ -0,0 +1 @@ +export { default as Brush } from './Brush'; diff --git a/packages/vx-brush/src/types.ts b/packages/vx-brush/src/types.ts new file mode 100644 index 000000000..26f4929aa --- /dev/null +++ b/packages/vx-brush/src/types.ts @@ -0,0 +1,55 @@ +export type GeneralStyleShape = { + stroke: string; + strokeWidth: number; + strokeOpacity: number; + fill: string; + fillOpacity: number; +}; + +export type MarginShape = { + top?: number; + left?: number; + right?: number; + bottom?: number; +}; + +export type BrushShape = { + start: { + x: number; + y: number; + }; + end: { + x: number; + y: number; + }; + extent: { + x0: number; + y0: number; + x1: number; + y1: number; + }; + bounds: { + x0: number; + y0: number; + x1: number; + y1: number; + }; +}; + +export type Brush = BrushShape & { + activeHandle: any; + isBrushing: boolean; +}; + +export type DragShape = { + x?: number; + y?: number; + dx?: number; + dy?: number; + isDragging?: boolean; + dragEnd?: Function; + dragMove?: Function; + dragStart?: Function; + width: number; + height: number; +}; diff --git a/packages/vx-brush/src/utils/constrainToRegion.js b/packages/vx-brush/src/utils/constrainToRegion.js deleted file mode 100644 index 80107d754..000000000 --- a/packages/vx-brush/src/utils/constrainToRegion.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function constrainToRegion({ region, x, y }) { - const { x0, x1, y0, y1 } = region; - return { - x: x < x0 ? x0 : x > x1 ? x1 : x, - y: y < y0 ? y0 : y > y1 ? y1 : y, - }; -} diff --git a/packages/vx-brush/src/utils/getCoordsFromEvent.js b/packages/vx-brush/src/utils/getCoordsFromEvent.js deleted file mode 100644 index 875019bea..000000000 --- a/packages/vx-brush/src/utils/getCoordsFromEvent.js +++ /dev/null @@ -1,19 +0,0 @@ -export default function getCoordsFromEvent(node, event) { - if (!node) return; - const svg = node.ownerSVGElement || node; - if (svg.createSVGPoint) { - let point = svg.createSVGPoint(); - point.x = event.clientX; - point.y = event.clientY; - point = point.matrixTransform(node.getScreenCTM().inverse()); - return { - x: point.x, - y: point.y, - }; - } - const rect = node.getBoundingClientRect(); - return { - x: event.clientX - rect.left - node.clientLeft, - y: event.clientY - rect.top - node.clientTop, - }; -} From b5f90fca095b72753a8fbd847c0f1eae2c66630e Mon Sep 17 00:00:00 2001 From: geekplux Date: Thu, 31 Oct 2019 11:19:03 +0800 Subject: [PATCH 06/16] fix: typing errors in vx-brush --- packages/vx-brush/src/Brush.tsx | 93 +++++++++++++++++------- packages/vx-brush/src/BrushCorner.tsx | 19 +++-- packages/vx-brush/src/BrushHandle.tsx | 14 ++-- packages/vx-brush/src/BrushSelection.tsx | 14 ++-- packages/vx-brush/src/types.ts | 5 -- 5 files changed, 92 insertions(+), 53 deletions(-) diff --git a/packages/vx-brush/src/Brush.tsx b/packages/vx-brush/src/Brush.tsx index 070809f3a..23023865e 100644 --- a/packages/vx-brush/src/Brush.tsx +++ b/packages/vx-brush/src/Brush.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Group } from '@vx/group'; import { Bar } from '@vx/shape'; +//@ts-ignore import { Drag } from '@vx/drag'; import BrushHandle from './BrushHandle'; @@ -16,10 +17,16 @@ export type BrushProps = { top: number; inheritedMargin?: MarginShape; onChange?: Function; - handleSize?: number; - resizeTriggerAreas?: [ - 'left' | 'right' | 'top' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight', - ]; + handleSize: number; + resizeTriggerAreas?: + | 'left' + | 'right' + | 'top' + | 'bottom' + | 'topLeft' + | 'topRight' + | 'bottomLeft' + | 'bottomRight'; onBrushStart?: Function; onBrushEnd?: Function; selectedBoxStyle: GeneralStyleShape; @@ -27,7 +34,7 @@ export type BrushProps = { onMouseUp?: Function; onMouseMove?: Function; onClick?: Function; - clickSensitivity?: number; + clickSensitivity: number; disableDraggingSelection: boolean; }; @@ -52,11 +59,13 @@ export type BrushState = { y0: number; y1: number; }; + activeHandle: any; + isBrushing: boolean; }; -export default class Brush extends React.Component { - private mouseUpTime: number; - private mouseDownTime: number; +export default class Brush extends React.Component { + private mouseUpTime: any; + private mouseDownTime: any; static defaultProps = { brushDirection: 'both', @@ -97,6 +106,8 @@ export default class Brush extends React.Component { y0: 0, y1: height, }, + isBrushing: false, + activeHandle: null, }; this.width = this.width.bind(this); this.height = this.height.bind(this); @@ -113,6 +124,7 @@ export default class Brush extends React.Component { } componentWillReceiveProps(nextProps: BrushProps) { + //@ts-ignore if (['width', 'height'].some(prop => this.props[prop] !== nextProps[prop])) { this.setState(() => ({ bounds: { @@ -125,7 +137,16 @@ export default class Brush extends React.Component { } } - getExtent(start, end) { + getExtent( + start: { + x: number; + y: number; + }, + end: { + x: number; + y: number; + }, + ) { 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); @@ -140,11 +161,13 @@ export default class Brush extends React.Component { }; } - handleDragStart(draw) { + handleDragStart(draw: any) { const { onBrushStart, left, top, inheritedMargin } = this.props; + const marginLeft = inheritedMargin && inheritedMargin.left ? inheritedMargin.left : 0; + const marginTop = inheritedMargin && inheritedMargin.top ? inheritedMargin.top : 0; const start = { - x: draw.x + draw.dx - left - inheritedMargin.left, - y: draw.y + draw.dy - top - inheritedMargin.top, + x: draw.x + draw.dx - left - marginLeft, + y: draw.y + draw.dy - top - marginTop, }; const end = { ...start }; @@ -152,7 +175,7 @@ export default class Brush extends React.Component { onBrushStart(start); } - this.update(prevBrush => ({ + this.update((prevBrush: BrushState) => ({ ...prevBrush, start, end, @@ -166,14 +189,16 @@ export default class Brush extends React.Component { })); } - handleDragMove(draw) { + handleDragMove(draw: any) { const { left, top, inheritedMargin } = this.props; if (!draw.isDragging) return; + const marginLeft = inheritedMargin && inheritedMargin.left ? inheritedMargin.left : 0; + const marginTop = inheritedMargin && inheritedMargin.top ? inheritedMargin.top : 0; const end = { - x: draw.x + draw.dx - left - inheritedMargin.left, - y: draw.y + draw.dy - top - inheritedMargin.top, + x: draw.x + draw.dx - left - marginLeft, + y: draw.y + draw.dy - top - marginTop, }; - this.update(prevBrush => { + this.update((prevBrush: BrushState) => { const { start } = prevBrush; const extent = this.getExtent(start, end); @@ -187,7 +212,7 @@ export default class Brush extends React.Component { handleDragEnd() { const { onBrushEnd } = this.props; - this.update(prevBrush => { + this.update((prevBrush: BrushState) => { const { extent } = prevBrush; const newState = { ...prevBrush, @@ -223,7 +248,14 @@ export default class Brush extends React.Component { return Math.max(y1 - y0, 0); } - handles() { + handles(): { + [index: string]: { + x: number; + y: number; + height: number; + width: number; + }; + } { const { handleSize } = this.props; const { extent } = this.state; const { x0, x1, y0, y1 } = extent; @@ -259,7 +291,12 @@ export default class Brush extends React.Component { }; } - corners() { + corners(): { + [index: string]: { + x: number; + y: number; + }; + } { const { handleSize } = this.props; const { extent } = this.state; const { x0, x1, y0, y1 } = extent; @@ -285,7 +322,7 @@ export default class Brush extends React.Component { }; } - update(updater) { + update(updater: any) { const { onChange } = this.props; this.setState(updater, () => { if (onChange) { @@ -352,7 +389,7 @@ export default class Brush extends React.Component { onDragMove={this.handleDragMove} onDragEnd={this.handleDragEnd} > - {draw => ( + {(draw: any) => ( { y={0} width={stageWidth} height={stageHeight} - onDoubleClick={() => event => this.reset(event)} - onClick={() => event => { + onDoubleClick={() => this.reset()} + onClick={() => (event: MouseEvent) => { const duration = this.mouseUpTime - this.mouseDownTime; if (onClick && duration < clickSensitivity) onClick(event); }} - onMouseDown={() => event => { + onMouseDown={() => (event: MouseEvent) => { this.mouseDownTime = new Date(); draw.dragStart(event); }} - onMouseLeave={() => event => { + onMouseLeave={() => (event: MouseEvent) => { if (onMouseLeave) onMouseLeave(event); }} - onMouseMove={() => event => { + onMouseMove={() => (event: MouseEvent) => { if (!draw.isDragging && onMouseMove) onMouseMove(event); if (draw.isDragging) draw.dragMove(event); }} - onMouseUp={() => event => { + onMouseUp={() => (event: MouseEvent) => { this.mouseUpTime = new Date(); if (onMouseUp) onMouseUp(event); draw.dragEnd(event); diff --git a/packages/vx-brush/src/BrushCorner.tsx b/packages/vx-brush/src/BrushCorner.tsx index bc4f6b062..9f4c2b057 100644 --- a/packages/vx-brush/src/BrushCorner.tsx +++ b/packages/vx-brush/src/BrushCorner.tsx @@ -1,12 +1,14 @@ /* eslint react/jsx-handler-names: 0 */ -import React from 'react'; +import React, { SVGProps } from 'react'; +//@ts-ignore import { Drag } from '@vx/drag'; -import { GeneralStyleShape, Brush } from './types'; +import { GeneralStyleShape } from './types'; +import { BrushState } from './Brush'; -export type BrushCornerProps = { +export type BrushCornerProps = SVGProps & { stageWidth: number; stageHeight: number; - brush: Brush; + brush: BrushState; updateBrush: Function; onBrushEnd?: Function; type: string; @@ -26,10 +28,10 @@ export default class BrushCorner extends React.Component { + updateBrush((prevBrush: BrushState) => { const { start, end } = prevBrush; const xMax = Math.max(start.x, end.x); @@ -112,7 +114,7 @@ export default class BrushCorner extends React.Component { + updateBrush((prevBrush: BrushState) => { const { start, end, extent } = prevBrush; start.x = Math.min(extent.x0, extent.x1); start.y = Math.min(extent.y0, extent.y0); @@ -165,7 +167,7 @@ export default class BrushCorner extends React.Component - {handle => ( + {(handle: any) => ( {handle.isDragging && ( diff --git a/packages/vx-brush/src/BrushHandle.tsx b/packages/vx-brush/src/BrushHandle.tsx index 62b72840b..b42a8adc0 100644 --- a/packages/vx-brush/src/BrushHandle.tsx +++ b/packages/vx-brush/src/BrushHandle.tsx @@ -1,12 +1,14 @@ /* eslint react/jsx-handler-names: 0 */ import React from 'react'; +//@ts-ignore import { Drag } from '@vx/drag'; -import { Brush, DragShape } from './types'; +import { DragShape } from './types'; +import { BrushState } from './Brush'; export type BrushHandleProps = { stageWidth: number; stageHeight: number; - brush: Brush; + brush: BrushState; updateBrush: Function; onBrushEnd?: Function; handle: DragShape; @@ -20,10 +22,10 @@ export default class BrushHandle extends React.Component { this.handleDragEnd = this.handleDragEnd.bind(this); } - handleDragMove(drag) { + handleDragMove(drag: any) { const { updateBrush, type } = this.props; if (!drag.isDragging) return; - updateBrush(prevBrush => { + updateBrush((prevBrush: BrushState) => { const { start, end } = prevBrush; let nextState = {}; let move = 0; @@ -90,7 +92,7 @@ export default class BrushHandle extends React.Component { handleDragEnd() { const { updateBrush, onBrushEnd } = this.props; - updateBrush(prevBrush => { + updateBrush((prevBrush: BrushState) => { const { start, end, extent } = prevBrush; start.x = Math.min(extent.x0, extent.x1); start.y = Math.min(extent.y0, extent.y0); @@ -130,7 +132,7 @@ export default class BrushHandle extends React.Component { onDragEnd={this.handleDragEnd} resetOnStart > - {drag => ( + {(drag: any) => ( {handle.isDragging && ( this.selectionDragEnd = this.selectionDragEnd.bind(this); } - selectionDragMove(drag) { + selectionDragMove(drag: any) { const { updateBrush } = this.props; - updateBrush(prevBrush => { + updateBrush((prevBrush: BrushState) => { const { x: x0, y: y0 } = prevBrush.start; const { x: x1, y: y1 } = prevBrush.end; const validDx = @@ -63,7 +64,7 @@ export default class BrushSelection extends React.Component selectionDragEnd() { const { updateBrush, onBrushEnd } = this.props; - updateBrush(prevBrush => { + updateBrush((prevBrush: BrushState) => { const nextBrush = { ...prevBrush, isBrushing: false, @@ -111,7 +112,7 @@ export default class BrushSelection extends React.Component onDragMove={this.selectionDragMove} onDragEnd={this.selectionDragEnd} > - {selection => ( + {(selection: any) => ( {selection.isDragging && ( onClick={event => { if (onClick) onClick(event); }} + //@ts-ignore style={{ pointerEvents: brush.isBrushing || brush.activeHandle ? 'none' : 'all', cursor: disableDraggingSelection ? null : 'move', diff --git a/packages/vx-brush/src/types.ts b/packages/vx-brush/src/types.ts index 26f4929aa..7acaefac4 100644 --- a/packages/vx-brush/src/types.ts +++ b/packages/vx-brush/src/types.ts @@ -36,11 +36,6 @@ export type BrushShape = { }; }; -export type Brush = BrushShape & { - activeHandle: any; - isBrushing: boolean; -}; - export type DragShape = { x?: number; y?: number; From 0c70194d8b56a27267641b915a8053aa8cb37f47 Mon Sep 17 00:00:00 2001 From: geekplux Date: Thu, 31 Oct 2019 14:46:49 +0800 Subject: [PATCH 07/16] feat: Brush component --- packages/vx-brush/src/BaseBrush.tsx | 463 ++++++++++++++ packages/vx-brush/src/Brush.tsx | 602 +++++------------- packages/vx-brush/src/BrushCorner.tsx | 2 +- packages/vx-brush/src/BrushHandle.tsx | 2 +- packages/vx-brush/src/BrushSelection.tsx | 2 +- packages/vx-brush/src/types.ts | 36 +- packages/vx-brush/src/utils.ts | 47 ++ .../vx-demo/src/components/tiles/brush.js | 32 + 8 files changed, 732 insertions(+), 454 deletions(-) create mode 100644 packages/vx-brush/src/BaseBrush.tsx create mode 100644 packages/vx-brush/src/utils.ts diff --git a/packages/vx-brush/src/BaseBrush.tsx b/packages/vx-brush/src/BaseBrush.tsx new file mode 100644 index 000000000..358679b1e --- /dev/null +++ b/packages/vx-brush/src/BaseBrush.tsx @@ -0,0 +1,463 @@ +import React from 'react'; +import { Group } from '@vx/group'; +import { Bar } from '@vx/shape'; +//@ts-ignore +import { Drag } from '@vx/drag'; + +import BrushHandle from './BrushHandle'; +import BrushCorner from './BrushCorner'; +import BrushSelection from './BrushSelection'; +import { GeneralStyleShape, MarginShape, Point, BrushShape } from './types'; + +export type BaseBrushProps = { + brushDirection?: 'horizontal' | 'vertical' | 'both'; + width: number; + height: number; + left: number; + top: number; + inheritedMargin?: MarginShape; + onChange?: Function; + handleSize: number; + resizeTriggerAreas?: + | 'left' + | 'right' + | 'top' + | 'bottom' + | 'topLeft' + | 'topRight' + | 'bottomLeft' + | 'bottomRight'; + onBrushStart?: Function; + onBrushEnd?: Function; + selectedBoxStyle: GeneralStyleShape; + onMouseLeave?: Function; + onMouseUp?: Function; + onMouseMove?: Function; + onClick?: Function; + clickSensitivity: number; + disableDraggingSelection: boolean; +}; + +export type BaseBrushState = BrushShape & { + activeHandle: any; + isBrushing: boolean; +}; + +export default class BaseBrush extends React.Component { + private mouseUpTime: any; + private mouseDownTime: any; + + static defaultProps = { + brushDirection: 'both', + inheritedMargin: { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + onChange: null, + handleSize: 4, + resizeTriggerAreas: ['left', 'right'], + onBrushStart: null, + onBrushEnd: null, + onMouseLeave: null, + onMouseUp: null, + onMouseMove: null, + onClick: null, + disableDraggingSelection: false, + clickSensitivity: 200, + }; + + constructor(props: BaseBrushProps) { + super(props); + const { width, height } = 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: width, + y0: 0, + y1: height, + }, + isBrushing: false, + activeHandle: null, + }; + 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); + this.mouseUpTime = 0; + this.mouseDownTime = 0; + } + + componentWillReceiveProps(nextProps: BaseBrushProps) { + //@ts-ignore + if (['width', 'height'].some(prop => this.props[prop] !== nextProps[prop])) { + this.setState(() => ({ + bounds: { + x0: 0, + x1: nextProps.width, + y0: 0, + y1: nextProps.height, + }, + })); + } + } + + getExtent(start: Point, end: Point) { + 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: any) { + const { onBrushStart, left, top, inheritedMargin } = this.props; + const marginLeft = inheritedMargin && inheritedMargin.left ? inheritedMargin.left : 0; + const marginTop = inheritedMargin && inheritedMargin.top ? inheritedMargin.top : 0; + const start = { + x: draw.x + draw.dx - left - marginLeft, + y: draw.y + draw.dy - top - marginTop, + }; + const end = { ...start }; + + if (onBrushStart) { + onBrushStart(start); + } + + this.update((prevBrush: BaseBrushState) => ({ + ...prevBrush, + start, + end, + extent: { + x0: -1, + x1: -1, + y0: -1, + y1: -1, + }, + isBrushing: true, + })); + } + + handleDragMove(draw: any) { + const { left, top, inheritedMargin } = this.props; + if (!draw.isDragging) return; + const marginLeft = inheritedMargin && inheritedMargin.left ? inheritedMargin.left : 0; + const marginTop = inheritedMargin && inheritedMargin.top ? inheritedMargin.top : 0; + const end = { + x: draw.x + draw.dx - left - marginLeft, + y: draw.y + draw.dy - top - marginTop, + }; + this.update((prevBrush: BaseBrushState) => { + const { start } = prevBrush; + const extent = this.getExtent(start, end); + + return { + ...prevBrush, + end, + extent, + }; + }); + } + + handleDragEnd() { + const { onBrushEnd } = this.props; + this.update((prevBrush: BaseBrushState) => { + const { extent } = prevBrush; + const newState = { + ...prevBrush, + start: { + x: extent.x0, + y: extent.y0, + }, + end: { + x: extent.x1, + y: extent.y1, + }, + isBrushing: false, + }; + if (onBrushEnd) { + onBrushEnd(newState); + } + + return newState; + }); + } + + 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(): { + [index: string]: { + x: number; + y: number; + height: number; + width: number; + }; + } { + 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(): { + [index: string]: { + x: number; + y: number; + }; + } { + 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: any) { + const { onChange } = this.props; + this.setState(updater, () => { + if (onChange) { + onChange(this.state); + } + }); + } + + reset() { + const { width, height } = this.props; + this.update(() => ({ + start: undefined, + end: undefined, + extent: { + x0: undefined, + x1: undefined, + y0: undefined, + y1: undefined, + }, + bounds: { + x0: 0, + x1: width, + y0: 0, + y1: height, + }, + isBrushing: false, + activeHandle: undefined, + })); + } + + render() { + const { start, end } = this.state; + const { + top, + left, + width: stageWidth, + height: stageHeight, + handleSize, + onMouseLeave, + onMouseUp, + onMouseMove, + onBrushEnd, + onClick, + resizeTriggerAreas, + selectedBoxStyle, + disableDraggingSelection, + clickSensitivity, + } = 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: any) => ( + this.reset()} + onClick={() => (event: MouseEvent) => { + const duration = this.mouseUpTime - this.mouseDownTime; + if (onClick && duration < clickSensitivity) onClick(event); + }} + onMouseDown={() => (event: MouseEvent) => { + this.mouseDownTime = new Date(); + draw.dragStart(event); + }} + onMouseLeave={() => (event: MouseEvent) => { + if (onMouseLeave) onMouseLeave(event); + }} + onMouseMove={() => (event: MouseEvent) => { + if (!draw.isDragging && onMouseMove) onMouseMove(event); + if (draw.isDragging) draw.dragMove(event); + }} + onMouseUp={() => (event: MouseEvent) => { + this.mouseUpTime = new Date(); + 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 => { + const handle = handles[handleKey]; + + return ( + + ); + })} + {/* corners */} + {start && + end && + Object.keys(corners) + .filter(cornerKey => resizeTriggerAreaSet.has(cornerKey)) + .map(cornerKey => { + const corner = corners[cornerKey]; + + return ( + + ); + })} + + ); + } +} diff --git a/packages/vx-brush/src/Brush.tsx b/packages/vx-brush/src/Brush.tsx index 23023865e..fffc46797 100644 --- a/packages/vx-brush/src/Brush.tsx +++ b/packages/vx-brush/src/Brush.tsx @@ -1,24 +1,26 @@ import React from 'react'; -import { Group } from '@vx/group'; -import { Bar } from '@vx/shape'; -//@ts-ignore -import { Drag } from '@vx/drag'; +import BaseBrush, { BaseBrushState } from './BaseBrush'; +import { GeneralStyleShape, MarginShape, Point } from './types'; +import { scaleInvert, getDomainFromExtent } from './utils'; -import BrushHandle from './BrushHandle'; -import BrushCorner from './BrushCorner'; -import BrushSelection from './BrushSelection'; -import { GeneralStyleShape, MarginShape } from './types'; +const SAFE_PIXEL = 2; +const DEFAULT_COLOR = 'steelblue'; export type BrushProps = { - brushDirection?: 'horizontal' | 'vertical' | 'both'; - width: number; - height: number; - left: number; - top: number; - inheritedMargin?: MarginShape; - onChange?: Function; - handleSize: number; - resizeTriggerAreas?: + selectedBoxStyle: GeneralStyleShape; + xScale: Function; + yScale: Function; + innerHeight: number; + innerWidth: number; + onChange: Function; + onBrushStart: Function; + onBrushEnd: Function; + onMouseMove: Function; + onMouseLeave: Function; + onClick: Function; + margin: MarginShape; + brushDirection: 'vertical' | 'horizontal' | 'both'; + resizeTriggerAreas: | 'left' | 'right' | 'top' @@ -27,466 +29,204 @@ export type BrushProps = { | 'topRight' | 'bottomLeft' | 'bottomRight'; - onBrushStart?: Function; - onBrushEnd?: Function; - selectedBoxStyle: GeneralStyleShape; - onMouseLeave?: Function; - onMouseUp?: Function; - onMouseMove?: Function; - onClick?: Function; - clickSensitivity: number; + brushRegion: 'xAxis' | 'yAxis' | 'chart'; + yAxisOrientation: 'left' | 'right'; + xAxisOrientation: 'top' | 'bottom'; disableDraggingSelection: boolean; + handleSize: number; }; -export type BrushState = { - start: { - x: number; - y: number; - }; - end: { - x: number; - y: number; - }; - extent: { - x0: number; - x1: number; - y0: number; - y1: number; - }; - bounds: { - x0: number; - x1: number; - y0: number; - y1: number; - }; - activeHandle: any; - isBrushing: boolean; -}; - -export default class Brush extends React.Component { - private mouseUpTime: any; - private mouseDownTime: any; - +class Brush extends React.Component { static defaultProps = { - brushDirection: 'both', - inheritedMargin: { - left: 0, + xScale: null, + yScale: null, + onChange: null, + innerHeight: 0, + innerWidth: 0, + selectedBoxStyle: { + fill: DEFAULT_COLOR, + fillOpacity: 0.2, + stroke: DEFAULT_COLOR, + strokeWidth: 1, + strokeOpacity: 0.8, + }, + margin: { top: 0, + left: 0, right: 0, bottom: 0, }, - onChange: null, handleSize: 4, + brushDirection: 'horizontal', resizeTriggerAreas: ['left', 'right'], + brushRegion: 'chart', + yAxisOrientation: 'right', + xAxisOrientation: 'bottom', onBrushStart: null, onBrushEnd: null, - onMouseLeave: null, - onMouseUp: null, + disableDraggingSelection: false, onMouseMove: null, + onMouseLeave: null, onClick: null, - disableDraggingSelection: false, - clickSensitivity: 200, }; + private BaseBrush: BaseBrush | null; + constructor(props: BrushProps) { super(props); - const { width, height } = 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: width, - y0: 0, - y1: height, - }, - isBrushing: false, - activeHandle: null, - }; - 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); - this.mouseUpTime = 0; - this.mouseDownTime = 0; - } - componentWillReceiveProps(nextProps: BrushProps) { - //@ts-ignore - if (['width', 'height'].some(prop => this.props[prop] !== nextProps[prop])) { - this.setState(() => ({ - bounds: { - x0: 0, - x1: nextProps.width, - y0: 0, - y1: nextProps.height, - }, - })); - } + this.BaseBrush = null; + this.handleChange = this.handleChange.bind(this); + this.handleBrushStart = this.handleBrushStart.bind(this); + this.handleBrushEnd = this.handleBrushEnd.bind(this); + this.reset = this.reset.bind(this); } - getExtent( - start: { - x: number; - y: number; - }, - end: { - x: number; - y: number; - }, - ) { - 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, - }; + reset() { + this.BaseBrush && this.BaseBrush.reset(); } - handleDragStart(draw: any) { - const { onBrushStart, left, top, inheritedMargin } = this.props; - const marginLeft = inheritedMargin && inheritedMargin.left ? inheritedMargin.left : 0; - const marginTop = inheritedMargin && inheritedMargin.top ? inheritedMargin.top : 0; - const start = { - x: draw.x + draw.dx - left - marginLeft, - y: draw.y + draw.dy - top - marginTop, - }; - const end = { ...start }; + handleChange(brush: BaseBrushState) { + const { onChange } = this.props; + if (!onChange) return; + const { x0 } = brush.extent; + if (x0 < 0 || typeof x0 === 'undefined') { + onChange(null); - if (onBrushStart) { - onBrushStart(start); + return; } - - this.update((prevBrush: BrushState) => ({ - ...prevBrush, - start, - end, - extent: { - x0: -1, - x1: -1, - y0: -1, - y1: -1, - }, - isBrushing: true, - })); + const domain = this.convertRangeToDomain(brush); + onChange(domain); } - handleDragMove(draw: any) { - const { left, top, inheritedMargin } = this.props; - if (!draw.isDragging) return; - const marginLeft = inheritedMargin && inheritedMargin.left ? inheritedMargin.left : 0; - const marginTop = inheritedMargin && inheritedMargin.top ? inheritedMargin.top : 0; - const end = { - x: draw.x + draw.dx - left - marginLeft, - y: draw.y + draw.dy - top - marginTop, - }; - this.update((prevBrush: BrushState) => { - const { start } = prevBrush; - const extent = this.getExtent(start, end); + convertRangeToDomain(brush: BaseBrushState) { + const { xScale, yScale } = this.props; + const { x0, x1, y0, y1 } = brush.extent; - return { - ...prevBrush, - end, - extent, - }; - }); - } + const xDomain = getDomainFromExtent(xScale, x0, x1, SAFE_PIXEL); + const yDomain = getDomainFromExtent(yScale, y0, y1, SAFE_PIXEL); - handleDragEnd() { - const { onBrushEnd } = this.props; - this.update((prevBrush: BrushState) => { - const { extent } = prevBrush; - const newState = { - ...prevBrush, - start: { - x: extent.x0, - y: extent.y0, - }, - end: { - x: extent.x1, - y: extent.y1, - }, - isBrushing: false, - }; - if (onBrushEnd) { - onBrushEnd(newState); - } - - return newState; - }); - } - - 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(): { - [index: string]: { - x: number; - y: number; - height: number; - width: number; + const domain = { + x0: xDomain.start, + x1: xDomain.end, + xValues: xDomain.values, + y0: yDomain.start, + y1: yDomain.end, + yValues: yDomain.values, }; - } { - 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, - }, - }; + return domain; } - corners(): { - [index: string]: { - x: number; - y: number; - }; - } { - 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, - }, - }; + handleBrushStart(point: Point) { + const { x, y } = point; + const { onBrushStart, xScale, yScale } = this.props; + const invertedX = scaleInvert(xScale, x); + const invertedY = scaleInvert(yScale, y); + if (onBrushStart) { + onBrushStart({ + //@ts-ignore + x: xScale.invert ? invertedX : xScale.domain()[invertedX], + //@ts-ignore + y: yScale.invert ? invertedY : yScale.domain()[invertedY], + }); + } } - update(updater: any) { - const { onChange } = this.props; - this.setState(updater, () => { - if (onChange) { - onChange(this.state); - } - }); - } + handleBrushEnd(brush: BaseBrushState) { + const { onBrushEnd } = this.props; + if (!onBrushEnd) return; + const { x0 } = brush.extent; + if (x0 < 0) { + onBrushEnd(null); - reset() { - const { width, height } = this.props; - this.update(() => ({ - start: undefined, - end: undefined, - extent: { - x0: undefined, - x1: undefined, - y0: undefined, - y1: undefined, - }, - bounds: { - x0: 0, - x1: width, - y0: 0, - y1: height, - }, - isBrushing: false, - activeHandle: undefined, - })); + return; + } + const domain = this.convertRangeToDomain(brush); + onBrushEnd(domain); } render() { - const { start, end } = this.state; const { - top, - left, - width: stageWidth, - height: stageHeight, - handleSize, - onMouseLeave, - onMouseUp, - onMouseMove, - onBrushEnd, - onClick, + xScale, + yScale, + innerHeight, + innerWidth, + margin, + brushDirection, resizeTriggerAreas, + brushRegion, + yAxisOrientation, + xAxisOrientation, selectedBoxStyle, disableDraggingSelection, - clickSensitivity, + onMouseLeave, + onMouseMove, + onClick, + handleSize, } = this.props; - - const handles = this.handles(); - const corners = this.corners(); - const width = this.width(); - const height = this.height(); - const resizeTriggerAreaSet = new Set(resizeTriggerAreas); + if (!xScale || !yScale) return null; + + let brushRegionWidth; + let brushRegionHeight; + let left; + let top; + const marginLeft = margin && margin.left ? margin.left : 0; + const marginTop = margin && margin.top ? margin.top : 0; + const marginRight = margin && margin.right ? margin.right : 0; + const marginBottom = margin && margin.bottom ? margin.bottom : 0; + + if (brushRegion === 'chart') { + left = 0; + top = 0; + brushRegionWidth = innerWidth; + brushRegionHeight = innerHeight; + } else if (brushRegion === 'yAxis') { + top = 0; + brushRegionHeight = innerHeight; + if (yAxisOrientation === 'right') { + left = innerWidth; + brushRegionWidth = marginRight; + } else { + left = -marginLeft; + brushRegionWidth = marginLeft; + } + } else { + left = 0; + brushRegionWidth = innerWidth; + if (xAxisOrientation === 'bottom') { + top = innerHeight; + brushRegionHeight = marginBottom; + } else { + top = -marginTop; + brushRegionHeight = marginTop; + } + } return ( - - {/* overlay */} - - {(draw: any) => ( - this.reset()} - onClick={() => (event: MouseEvent) => { - const duration = this.mouseUpTime - this.mouseDownTime; - if (onClick && duration < clickSensitivity) onClick(event); - }} - onMouseDown={() => (event: MouseEvent) => { - this.mouseDownTime = new Date(); - draw.dragStart(event); - }} - onMouseLeave={() => (event: MouseEvent) => { - if (onMouseLeave) onMouseLeave(event); - }} - onMouseMove={() => (event: MouseEvent) => { - if (!draw.isDragging && onMouseMove) onMouseMove(event); - if (draw.isDragging) draw.dragMove(event); - }} - onMouseUp={() => (event: MouseEvent) => { - this.mouseUpTime = new Date(); - 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 => { - const handle = handles[handleKey]; - - return ( - - ); - })} - {/* corners */} - {start && - end && - Object.keys(corners) - .filter(cornerKey => resizeTriggerAreaSet.has(cornerKey)) - .map(cornerKey => { - const corner = corners[cornerKey]; - - return ( - - ); - })} - + { + this.BaseBrush = el; + }} + /> ); } } + +export default Brush; diff --git a/packages/vx-brush/src/BrushCorner.tsx b/packages/vx-brush/src/BrushCorner.tsx index 9f4c2b057..db01bb726 100644 --- a/packages/vx-brush/src/BrushCorner.tsx +++ b/packages/vx-brush/src/BrushCorner.tsx @@ -3,7 +3,7 @@ import React, { SVGProps } from 'react'; //@ts-ignore import { Drag } from '@vx/drag'; import { GeneralStyleShape } from './types'; -import { BrushState } from './Brush'; +import { BaseBrushState as BrushState } from './BaseBrush'; export type BrushCornerProps = SVGProps & { stageWidth: number; diff --git a/packages/vx-brush/src/BrushHandle.tsx b/packages/vx-brush/src/BrushHandle.tsx index b42a8adc0..259f99709 100644 --- a/packages/vx-brush/src/BrushHandle.tsx +++ b/packages/vx-brush/src/BrushHandle.tsx @@ -3,7 +3,7 @@ import React from 'react'; //@ts-ignore import { Drag } from '@vx/drag'; import { DragShape } from './types'; -import { BrushState } from './Brush'; +import { BaseBrushState as BrushState } from './BaseBrush'; export type BrushHandleProps = { stageWidth: number; diff --git a/packages/vx-brush/src/BrushSelection.tsx b/packages/vx-brush/src/BrushSelection.tsx index 2eca649a8..98cf69e64 100644 --- a/packages/vx-brush/src/BrushSelection.tsx +++ b/packages/vx-brush/src/BrushSelection.tsx @@ -2,7 +2,7 @@ import React from 'react'; //@ts-ignore import { Drag } from '@vx/drag'; -import { BrushState } from './Brush'; +import { BaseBrushState as BrushState } from './BaseBrush'; export type BrushSelectionProps = { width: number; diff --git a/packages/vx-brush/src/types.ts b/packages/vx-brush/src/types.ts index 7acaefac4..af8f36e72 100644 --- a/packages/vx-brush/src/types.ts +++ b/packages/vx-brush/src/types.ts @@ -1,3 +1,15 @@ +export type Point = { + x: number; + y: number; +}; + +export type Bound = { + x0: number; + x1: number; + y0: number; + y1: number; +}; + export type GeneralStyleShape = { stroke: string; strokeWidth: number; @@ -14,26 +26,10 @@ export type MarginShape = { }; export type BrushShape = { - start: { - x: number; - y: number; - }; - end: { - x: number; - y: number; - }; - extent: { - x0: number; - y0: number; - x1: number; - y1: number; - }; - bounds: { - x0: number; - y0: number; - x1: number; - y1: number; - }; + start: Point; + end: Point; + extent: Bound; + bounds: Bound; }; export type DragShape = { diff --git a/packages/vx-brush/src/utils.ts b/packages/vx-brush/src/utils.ts new file mode 100644 index 000000000..b0512901b --- /dev/null +++ b/packages/vx-brush/src/utils.ts @@ -0,0 +1,47 @@ +export function scaleInvert(scale: any, value: number) { + // Test if the scale is an ordinalScale or not, + // Since an ordinalScale doesn't support invert function. + if (!scale.invert) { + const [start, end] = scale.range(); + let i = 0; + const width = (scale.step() * (end - start)) / Math.abs(end - start); + if (width > 0) { + while (value > start + width * (i + 1)) { + i += 1; + } + } else { + while (value < start + width * (i + 1)) { + i += 1; + } + } + + return i; + } + + return scale.invert(value); +} + +export function getDomainFromExtent(scale: any, start: number, end: number, tolerentDelta: number) { + let domain; + const invertedStart = scaleInvert(scale, start + (start < end ? -tolerentDelta : tolerentDelta)); + const invertedEnd = scaleInvert(scale, end + (end < start ? -tolerentDelta : tolerentDelta)); + const minValue = Math.min(invertedStart, invertedEnd); + const maxValue = Math.max(invertedStart, invertedEnd); + if (scale.invert) { + domain = { + start: minValue, + end: maxValue, + }; + } else { + const values = []; + const scaleDomain = scale.domain(); + for (let i = minValue; i <= maxValue; i += 1) { + values.push(scaleDomain[i]); + } + domain = { + values, + }; + } + + return domain; +} diff --git a/packages/vx-demo/src/components/tiles/brush.js b/packages/vx-demo/src/components/tiles/brush.js index 40c4030ba..2b5bf97ef 100644 --- a/packages/vx-demo/src/components/tiles/brush.js +++ b/packages/vx-demo/src/components/tiles/brush.js @@ -6,6 +6,8 @@ import { AxisLeft, AxisBottom } from '@vx/axis'; import { curveMonotoneX } from '@vx/curve'; import { scaleTime, scaleLinear } from '@vx/scale'; import { appleStock } from '@vx/mock-data'; +import { Brush } from '@vx/brush'; +import { PatternLines } from '@vx/pattern'; /** * Initialize some variables @@ -43,6 +45,8 @@ function AreaChart({ grid = false, top, left, + brush, + onBrushChange, }) { // bounds const xMax = width - margin.left - margin.right; @@ -115,11 +119,37 @@ function AreaChart({ curve={curveMonotoneX} /> + {brush && ( + + )} + {brush && ( + + )} ); } function BrushChart({ width, height, margin }) { + function onBrushChange(domain) { + console.log(domain); + } + return (
@@ -130,6 +160,8 @@ function BrushChart({ width, height, margin }) { height={120} margin={{ top: 0, bottom: 20, left: 50, right: 20 }} top={height * 0.6 + 50} + brush + onBrushChange={onBrushChange} />
From 1539228b5e7dfc62cb122c5ec15719d4c7531ec4 Mon Sep 17 00:00:00 2001 From: geekplux Date: Thu, 31 Oct 2019 16:33:34 +0800 Subject: [PATCH 08/16] feat: complete brush demo --- packages/vx-brush/src/BaseBrush.tsx | 14 +- packages/vx-brush/src/Brush.tsx | 36 ++-- packages/vx-brush/src/types.ts | 4 + .../vx-demo/src/components/tiles/brush.js | 159 ++++++++++-------- 4 files changed, 115 insertions(+), 98 deletions(-) diff --git a/packages/vx-brush/src/BaseBrush.tsx b/packages/vx-brush/src/BaseBrush.tsx index 358679b1e..137e8736f 100644 --- a/packages/vx-brush/src/BaseBrush.tsx +++ b/packages/vx-brush/src/BaseBrush.tsx @@ -7,7 +7,7 @@ import { Drag } from '@vx/drag'; import BrushHandle from './BrushHandle'; import BrushCorner from './BrushCorner'; import BrushSelection from './BrushSelection'; -import { GeneralStyleShape, MarginShape, Point, BrushShape } from './types'; +import { GeneralStyleShape, MarginShape, Point, BrushShape, ResizeTriggerAreas } from './types'; export type BaseBrushProps = { brushDirection?: 'horizontal' | 'vertical' | 'both'; @@ -18,15 +18,7 @@ export type BaseBrushProps = { inheritedMargin?: MarginShape; onChange?: Function; handleSize: number; - resizeTriggerAreas?: - | 'left' - | 'right' - | 'top' - | 'bottom' - | 'topLeft' - | 'topRight' - | 'bottomLeft' - | 'bottomRight'; + resizeTriggerAreas?: ResizeTriggerAreas; onBrushStart?: Function; onBrushEnd?: Function; selectedBoxStyle: GeneralStyleShape; @@ -415,6 +407,7 @@ export default class BaseBrush extends React.Component resizeTriggerAreaSet.has(handleKey)) .map(handleKey => { const handle = handles[handleKey]; @@ -436,6 +429,7 @@ export default class BaseBrush extends React.Component resizeTriggerAreaSet.has(cornerKey)) .map(cornerKey => { const corner = corners[cornerKey]; diff --git a/packages/vx-brush/src/Brush.tsx b/packages/vx-brush/src/Brush.tsx index fffc46797..068e146eb 100644 --- a/packages/vx-brush/src/Brush.tsx +++ b/packages/vx-brush/src/Brush.tsx @@ -1,6 +1,6 @@ import React from 'react'; import BaseBrush, { BaseBrushState } from './BaseBrush'; -import { GeneralStyleShape, MarginShape, Point } from './types'; +import { GeneralStyleShape, MarginShape, Point, ResizeTriggerAreas } from './types'; import { scaleInvert, getDomainFromExtent } from './utils'; const SAFE_PIXEL = 2; @@ -10,8 +10,8 @@ export type BrushProps = { selectedBoxStyle: GeneralStyleShape; xScale: Function; yScale: Function; - innerHeight: number; - innerWidth: number; + height: number; + width: number; onChange: Function; onBrushStart: Function; onBrushEnd: Function; @@ -20,15 +20,7 @@ export type BrushProps = { onClick: Function; margin: MarginShape; brushDirection: 'vertical' | 'horizontal' | 'both'; - resizeTriggerAreas: - | 'left' - | 'right' - | 'top' - | 'bottom' - | 'topLeft' - | 'topRight' - | 'bottomLeft' - | 'bottomRight'; + resizeTriggerAreas: ResizeTriggerAreas; brushRegion: 'xAxis' | 'yAxis' | 'chart'; yAxisOrientation: 'left' | 'right'; xAxisOrientation: 'top' | 'bottom'; @@ -41,8 +33,8 @@ class Brush extends React.Component { xScale: null, yScale: null, onChange: null, - innerHeight: 0, - innerWidth: 0, + height: 0, + width: 0, selectedBoxStyle: { fill: DEFAULT_COLOR, fillOpacity: 0.2, @@ -150,8 +142,8 @@ class Brush extends React.Component { const { xScale, yScale, - innerHeight, - innerWidth, + height, + width, margin, brushDirection, resizeTriggerAreas, @@ -179,13 +171,13 @@ class Brush extends React.Component { if (brushRegion === 'chart') { left = 0; top = 0; - brushRegionWidth = innerWidth; - brushRegionHeight = innerHeight; + brushRegionWidth = width; + brushRegionHeight = height; } else if (brushRegion === 'yAxis') { top = 0; - brushRegionHeight = innerHeight; + brushRegionHeight = height; if (yAxisOrientation === 'right') { - left = innerWidth; + left = width; brushRegionWidth = marginRight; } else { left = -marginLeft; @@ -193,9 +185,9 @@ class Brush extends React.Component { } } else { left = 0; - brushRegionWidth = innerWidth; + brushRegionWidth = width; if (xAxisOrientation === 'bottom') { - top = innerHeight; + top = height; brushRegionHeight = marginBottom; } else { top = -marginTop; diff --git a/packages/vx-brush/src/types.ts b/packages/vx-brush/src/types.ts index af8f36e72..f58e766af 100644 --- a/packages/vx-brush/src/types.ts +++ b/packages/vx-brush/src/types.ts @@ -44,3 +44,7 @@ export type DragShape = { width: number; height: number; }; + +export type ResizeTriggerAreas = [ + 'left' | 'right' | 'top' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight', +]; diff --git a/packages/vx-demo/src/components/tiles/brush.js b/packages/vx-demo/src/components/tiles/brush.js index 2b5bf97ef..c570844b1 100644 --- a/packages/vx-demo/src/components/tiles/brush.js +++ b/packages/vx-demo/src/components/tiles/brush.js @@ -1,6 +1,5 @@ import React from 'react'; import { Group } from '@vx/group'; -import { GridRows, GridColumns } from '@vx/grid'; import { AreaClosed, Line, Bar } from '@vx/shape'; import { AxisLeft, AxisBottom } from '@vx/axis'; import { curveMonotoneX } from '@vx/curve'; @@ -40,29 +39,16 @@ const yStock = d => d.close; function AreaChart({ width, height, - margin = { top: 50, left: 50, bottom: 0, right: 20 }, + xMax, + yMax, + margin, + xScale, + yScale, axis = false, - grid = false, top, left, - brush, - onBrushChange, + children, }) { - // bounds - const xMax = width - margin.left - margin.right; - const yMax = height - margin.top - margin.bottom; - - // scales - const xScale = scaleTime({ - range: [0, xMax], - domain: extent(stock, xStock), - }); - const yScale = scaleLinear({ - range: [yMax, 0], - domain: [0, max(stock, yStock) + yMax / 3], - nice: true, - }); - return ( @@ -71,24 +57,6 @@ function AreaChart({ - {grid && ( - - )} - {grid && ( - - )} {axis && ( - {brush && ( - - )} - {brush && ( - - )} + {children} ); } -function BrushChart({ width, height, margin }) { +function BrushChart({ + width, + height, + margin = { + top: 50, + left: 50, + bottom: 0, + right: 20, + }, +}) { function onBrushChange(domain) { console.log(domain); } + function onBrushStart(domain) { + console.log('start', domain); + } + function onBrushEnd(domain) { + console.log('end', domain); + } + + const brushMargin = { top: 0, bottom: 20, left: 50, right: 20 }; + + // bounds + const xMax = width - margin.left - margin.right; + const yMax = height * 0.6 - margin.top - margin.bottom; + const xBrushMax = width - brushMargin.left - brushMargin.right; + const yBrushMax = 120 - brushMargin.top - brushMargin.bottom; + console.log(xBrushMax, yBrushMax); + + // scales + const xScale = scaleTime({ + range: [0, xMax], + domain: extent(stock, xStock), + }); + const yScale = scaleLinear({ + range: [yMax, 0], + domain: [0, max(stock, yStock) + yMax / 3], + nice: true, + }); + const xBrushScale = scaleTime({ + range: [0, xBrushMax], + domain: extent(stock, xStock), + }); + const yBrushScale = scaleLinear({ + range: [yBrushMax, 0], + domain: [0, max(stock, yStock) + yBrushMax / 3], + nice: true, + }); return (
- + + > + + +
); From f48e8be1db5180fad842bb53c264ab8b4d183935 Mon Sep 17 00:00:00 2001 From: geekplux Date: Fri, 1 Nov 2019 10:23:29 +0800 Subject: [PATCH 09/16] fix: brush event handler --- packages/vx-brush/src/BaseBrush.tsx | 10 +++++----- packages/vx-demo/src/components/tiles/brush.js | 11 +++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/vx-brush/src/BaseBrush.tsx b/packages/vx-brush/src/BaseBrush.tsx index 137e8736f..b8a1b4a8b 100644 --- a/packages/vx-brush/src/BaseBrush.tsx +++ b/packages/vx-brush/src/BaseBrush.tsx @@ -361,22 +361,22 @@ export default class BaseBrush extends React.Component this.reset()} - onClick={() => (event: MouseEvent) => { + onClick={(event: React.MouseEvent) => { const duration = this.mouseUpTime - this.mouseDownTime; if (onClick && duration < clickSensitivity) onClick(event); }} - onMouseDown={() => (event: MouseEvent) => { + onMouseDown={(event: React.MouseEvent) => { this.mouseDownTime = new Date(); draw.dragStart(event); }} - onMouseLeave={() => (event: MouseEvent) => { + onMouseLeave={(event: React.MouseEvent) => { if (onMouseLeave) onMouseLeave(event); }} - onMouseMove={() => (event: MouseEvent) => { + onMouseMove={(event: React.MouseEvent) => { if (!draw.isDragging && onMouseMove) onMouseMove(event); if (draw.isDragging) draw.dragMove(event); }} - onMouseUp={() => (event: MouseEvent) => { + onMouseUp={(event: React.MouseEvent) => { this.mouseUpTime = new Date(); if (onMouseUp) onMouseUp(event); draw.dragEnd(event); diff --git a/packages/vx-demo/src/components/tiles/brush.js b/packages/vx-demo/src/components/tiles/brush.js index c570844b1..eb83e1192 100644 --- a/packages/vx-demo/src/components/tiles/brush.js +++ b/packages/vx-demo/src/components/tiles/brush.js @@ -103,7 +103,7 @@ function BrushChart({ }, }) { function onBrushChange(domain) { - console.log(domain); + console.log('change', domain); } function onBrushStart(domain) { console.log('start', domain); @@ -115,11 +115,10 @@ function BrushChart({ const brushMargin = { top: 0, bottom: 20, left: 50, right: 20 }; // bounds - const xMax = width - margin.left - margin.right; - const yMax = height * 0.6 - margin.top - margin.bottom; - const xBrushMax = width - brushMargin.left - brushMargin.right; - const yBrushMax = 120 - brushMargin.top - brushMargin.bottom; - console.log(xBrushMax, yBrushMax); + const xMax = Math.max(width - margin.left - margin.right, 0); + const yMax = Math.max(height * 0.6 - margin.top - margin.bottom, 0); + const xBrushMax = Math.max(width - brushMargin.left - brushMargin.right, 0); + const yBrushMax = Math.max(120 - brushMargin.top - brushMargin.bottom, 0); // scales const xScale = scaleTime({ From eb7894bebc22f39f948e1b7488acb7c67d8b8cac Mon Sep 17 00:00:00 2001 From: geekplux Date: Fri, 1 Nov 2019 11:41:29 +0800 Subject: [PATCH 10/16] feat: finish brush demo --- .../vx-demo/src/components/tiles/brush.js | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/vx-demo/src/components/tiles/brush.js b/packages/vx-demo/src/components/tiles/brush.js index eb83e1192..cb7a6e72b 100644 --- a/packages/vx-demo/src/components/tiles/brush.js +++ b/packages/vx-demo/src/components/tiles/brush.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Group } from '@vx/group'; import { AreaClosed, Line, Bar } from '@vx/shape'; import { AxisLeft, AxisBottom } from '@vx/axis'; @@ -11,7 +11,7 @@ import { PatternLines } from '@vx/pattern'; /** * Initialize some variables */ -const stock = appleStock.slice(800); +const stock = appleStock.slice(1200); const min = (arr, fn) => Math.min(...arr.map(fn)); const max = (arr, fn) => Math.max(...arr.map(fn)); const extent = (arr, fn) => [min(arr, fn), max(arr, fn)]; @@ -37,6 +37,7 @@ const xStock = d => new Date(d.date); const yStock = d => d.close; function AreaChart({ + data, width, height, xMax, @@ -77,7 +78,7 @@ function AreaChart({ /> )} xScale(xStock(d))} y={d => yScale(yStock(d))} yScale={yScale} @@ -102,14 +103,17 @@ function BrushChart({ right: 20, }, }) { + const [filteredStock, setFilteredStock] = useState(stock); + function onBrushChange(domain) { - console.log('change', domain); - } - function onBrushStart(domain) { - console.log('start', domain); - } - function onBrushEnd(domain) { - console.log('end', domain); + if (!domain) return; + const { x0, x1, y0, y1 } = domain; + const stockCopy = stock.filter(s => { + const x = xStock(s).getTime(); + const y = yStock(s); + return x > x0 && x < x1 && y > y0 && y < y1; + }); + setFilteredStock(stockCopy); } const brushMargin = { top: 0, bottom: 20, left: 50, right: 20 }; @@ -123,11 +127,11 @@ function BrushChart({ // scales const xScale = scaleTime({ range: [0, xMax], - domain: extent(stock, xStock), + domain: extent(filteredStock, xStock), }); const yScale = scaleLinear({ range: [yMax, 0], - domain: [0, max(stock, yStock) + yMax / 3], + domain: [0, max(filteredStock, yStock) + yMax / 3], nice: true, }); const xBrushScale = scaleTime({ @@ -145,6 +149,7 @@ function BrushChart({ @@ -180,12 +186,10 @@ function BrushChart({ handleSize={4} resizeTriggerAreas={['left', 'right']} brushDirection="horizontal" - onBrushStart={onBrushStart} - onBrushEnd={onBrushEnd} onChange={onBrushChange} selectedBoxStyle={{ fill: 'url(#brush_pattern)', - stroke: '#000', + stroke: 'white', }} /> From 9b4c838b0bf6d2c9c5f268d2985fbdef980b30e7 Mon Sep 17 00:00:00 2001 From: geekplux Date: Tue, 19 Nov 2019 10:44:37 +0800 Subject: [PATCH 11/16] chore: lint fix --- packages/vx-brush/package.json | 2 ++ packages/vx-brush/src/BaseBrush.tsx | 8 ++++---- packages/vx-brush/src/Brush.tsx | 8 +++++--- packages/vx-brush/src/BrushCorner.tsx | 4 ++-- packages/vx-brush/src/BrushHandle.tsx | 2 +- packages/vx-brush/src/BrushSelection.tsx | 4 ++-- packages/vx-demo/src/components/tiles/brush.js | 4 +--- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/vx-brush/package.json b/packages/vx-brush/package.json index d4528d3d9..cb67513c5 100644 --- a/packages/vx-brush/package.json +++ b/packages/vx-brush/package.json @@ -34,6 +34,8 @@ }, "dependencies": { "@vx/drag": "^0.0.192", + "@vx/group": "^0.0.192", + "@vx/shape": "^0.0.192", "classnames": "^2.2.5", "prop-types": "^15.6.1", "recompose": "^0.23.1" diff --git a/packages/vx-brush/src/BaseBrush.tsx b/packages/vx-brush/src/BaseBrush.tsx index b8a1b4a8b..75055dfa8 100644 --- a/packages/vx-brush/src/BaseBrush.tsx +++ b/packages/vx-brush/src/BaseBrush.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Group } from '@vx/group'; import { Bar } from '@vx/shape'; -//@ts-ignore +// @ts-ignore import { Drag } from '@vx/drag'; import BrushHandle from './BrushHandle'; @@ -96,7 +96,7 @@ export default class BaseBrush extends React.Component this.props[prop] !== nextProps[prop])) { this.setState(() => ({ bounds: { @@ -407,7 +407,7 @@ export default class BaseBrush extends React.Component resizeTriggerAreaSet.has(handleKey)) .map(handleKey => { const handle = handles[handleKey]; @@ -429,7 +429,7 @@ export default class BaseBrush extends React.Component resizeTriggerAreaSet.has(cornerKey)) .map(cornerKey => { const corner = corners[cornerKey]; diff --git a/packages/vx-brush/src/Brush.tsx b/packages/vx-brush/src/Brush.tsx index 068e146eb..a903b4733 100644 --- a/packages/vx-brush/src/Brush.tsx +++ b/packages/vx-brush/src/Brush.tsx @@ -75,7 +75,9 @@ class Brush extends React.Component { } reset() { - this.BaseBrush && this.BaseBrush.reset(); + if (this.BaseBrush) { + this.BaseBrush.reset(); + } } handleChange(brush: BaseBrushState) { @@ -117,9 +119,9 @@ class Brush extends React.Component { const invertedY = scaleInvert(yScale, y); if (onBrushStart) { onBrushStart({ - //@ts-ignore + // @ts-ignore x: xScale.invert ? invertedX : xScale.domain()[invertedX], - //@ts-ignore + // @ts-ignore y: yScale.invert ? invertedY : yScale.domain()[invertedY], }); } diff --git a/packages/vx-brush/src/BrushCorner.tsx b/packages/vx-brush/src/BrushCorner.tsx index db01bb726..61aed7f9a 100644 --- a/packages/vx-brush/src/BrushCorner.tsx +++ b/packages/vx-brush/src/BrushCorner.tsx @@ -1,6 +1,6 @@ /* eslint react/jsx-handler-names: 0 */ import React, { SVGProps } from 'react'; -//@ts-ignore +// @ts-ignore import { Drag } from '@vx/drag'; import { GeneralStyleShape } from './types'; import { BaseBrushState as BrushState } from './BaseBrush'; @@ -185,7 +185,7 @@ export default class BrushCorner extends React.Component diff --git a/packages/vx-brush/src/BrushHandle.tsx b/packages/vx-brush/src/BrushHandle.tsx index 259f99709..81e600a96 100644 --- a/packages/vx-brush/src/BrushHandle.tsx +++ b/packages/vx-brush/src/BrushHandle.tsx @@ -1,6 +1,6 @@ /* eslint react/jsx-handler-names: 0 */ import React from 'react'; -//@ts-ignore +// @ts-ignore import { Drag } from '@vx/drag'; import { DragShape } from './types'; import { BaseBrushState as BrushState } from './BaseBrush'; diff --git a/packages/vx-brush/src/BrushSelection.tsx b/packages/vx-brush/src/BrushSelection.tsx index 98cf69e64..a1cc1da39 100644 --- a/packages/vx-brush/src/BrushSelection.tsx +++ b/packages/vx-brush/src/BrushSelection.tsx @@ -1,6 +1,6 @@ /* eslint react/jsx-handler-names: 0 */ import React from 'react'; -//@ts-ignore +// @ts-ignore import { Drag } from '@vx/drag'; import { BaseBrushState as BrushState } from './BaseBrush'; @@ -148,7 +148,7 @@ export default class BrushSelection extends React.Component onClick={event => { if (onClick) onClick(event); }} - //@ts-ignore + // @ts-ignore style={{ pointerEvents: brush.isBrushing || brush.activeHandle ? 'none' : 'all', cursor: disableDraggingSelection ? null : 'move', diff --git a/packages/vx-demo/src/components/tiles/brush.js b/packages/vx-demo/src/components/tiles/brush.js index cb7a6e72b..9e75485ab 100644 --- a/packages/vx-demo/src/components/tiles/brush.js +++ b/packages/vx-demo/src/components/tiles/brush.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Group } from '@vx/group'; -import { AreaClosed, Line, Bar } from '@vx/shape'; +import { AreaClosed, Bar } from '@vx/shape'; import { AxisLeft, AxisBottom } from '@vx/axis'; import { curveMonotoneX } from '@vx/curve'; import { scaleTime, scaleLinear } from '@vx/scale'; @@ -25,7 +25,6 @@ const axisBottomTickLabelProps = { const axisLeftTickLabelProps = { dx: '-0.25em', dy: '0.25em', - fill: 'black', fontFamily: 'Arial', fontSize: 10, textAnchor: 'end', @@ -40,7 +39,6 @@ function AreaChart({ data, width, height, - xMax, yMax, margin, xScale, From d166c1697dead7b2a62078308816efa3f9699cf4 Mon Sep 17 00:00:00 2001 From: geekplux Date: Tue, 19 Nov 2019 10:56:35 +0800 Subject: [PATCH 12/16] refactor: drop ts ignore for vx/drag --- packages/vx-brush/src/BaseBrush.tsx | 1 - packages/vx-brush/src/BrushCorner.tsx | 2 -- packages/vx-brush/src/BrushHandle.tsx | 1 - packages/vx-brush/src/BrushSelection.tsx | 1 - 4 files changed, 5 deletions(-) diff --git a/packages/vx-brush/src/BaseBrush.tsx b/packages/vx-brush/src/BaseBrush.tsx index 75055dfa8..4324b8213 100644 --- a/packages/vx-brush/src/BaseBrush.tsx +++ b/packages/vx-brush/src/BaseBrush.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Group } from '@vx/group'; import { Bar } from '@vx/shape'; -// @ts-ignore import { Drag } from '@vx/drag'; import BrushHandle from './BrushHandle'; diff --git a/packages/vx-brush/src/BrushCorner.tsx b/packages/vx-brush/src/BrushCorner.tsx index 61aed7f9a..9be45cf15 100644 --- a/packages/vx-brush/src/BrushCorner.tsx +++ b/packages/vx-brush/src/BrushCorner.tsx @@ -1,6 +1,4 @@ -/* eslint react/jsx-handler-names: 0 */ import React, { SVGProps } from 'react'; -// @ts-ignore import { Drag } from '@vx/drag'; import { GeneralStyleShape } from './types'; import { BaseBrushState as BrushState } from './BaseBrush'; diff --git a/packages/vx-brush/src/BrushHandle.tsx b/packages/vx-brush/src/BrushHandle.tsx index 81e600a96..151225d06 100644 --- a/packages/vx-brush/src/BrushHandle.tsx +++ b/packages/vx-brush/src/BrushHandle.tsx @@ -1,6 +1,5 @@ /* eslint react/jsx-handler-names: 0 */ import React from 'react'; -// @ts-ignore import { Drag } from '@vx/drag'; import { DragShape } from './types'; import { BaseBrushState as BrushState } from './BaseBrush'; diff --git a/packages/vx-brush/src/BrushSelection.tsx b/packages/vx-brush/src/BrushSelection.tsx index a1cc1da39..010c32d1d 100644 --- a/packages/vx-brush/src/BrushSelection.tsx +++ b/packages/vx-brush/src/BrushSelection.tsx @@ -1,6 +1,5 @@ /* eslint react/jsx-handler-names: 0 */ import React from 'react'; -// @ts-ignore import { Drag } from '@vx/drag'; import { BaseBrushState as BrushState } from './BaseBrush'; From cf3e85355338a39b772a2a0400b73088278c5816 Mon Sep 17 00:00:00 2001 From: geekplux Date: Tue, 19 Nov 2019 11:49:53 +0800 Subject: [PATCH 13/16] refactor: follow typescript style --- packages/vx-brush/src/BaseBrush.tsx | 84 ++++++++++-------------- packages/vx-brush/src/Brush.tsx | 30 +++------ packages/vx-brush/src/BrushCorner.tsx | 28 ++++---- packages/vx-brush/src/BrushHandle.tsx | 28 ++++---- packages/vx-brush/src/BrushSelection.tsx | 31 ++++----- 5 files changed, 78 insertions(+), 123 deletions(-) diff --git a/packages/vx-brush/src/BaseBrush.tsx b/packages/vx-brush/src/BaseBrush.tsx index 4324b8213..bff7637aa 100644 --- a/packages/vx-brush/src/BaseBrush.tsx +++ b/packages/vx-brush/src/BaseBrush.tsx @@ -35,8 +35,27 @@ export type BaseBrushState = BrushShape & { }; export default class BaseBrush extends React.Component { - private mouseUpTime: any; - private mouseDownTime: any; + private mouseUpTime: any = 0; + private mouseDownTime: any = 0; + + state = { + start: { x: 0, y: 0 }, + end: { x: 0, y: 0 }, + extent: { + x0: 0, + x1: 0, + y0: 0, + y1: 0, + }, + bounds: { + x0: 0, + x1: this.props.width, + y0: 0, + y1: this.props.height, + }, + isBrushing: false, + activeHandle: null, + }; static defaultProps = { brushDirection: 'both', @@ -59,41 +78,6 @@ export default class BaseBrush extends React.Component this.props[prop] !== nextProps[prop])) { @@ -123,7 +107,7 @@ export default class BaseBrush extends React.Component { const { onBrushStart, left, top, inheritedMargin } = this.props; const marginLeft = inheritedMargin && inheritedMargin.left ? inheritedMargin.left : 0; const marginTop = inheritedMargin && inheritedMargin.top ? inheritedMargin.top : 0; @@ -149,9 +133,9 @@ export default class BaseBrush extends React.Component { const { left, top, inheritedMargin } = this.props; if (!draw.isDragging) return; const marginLeft = inheritedMargin && inheritedMargin.left ? inheritedMargin.left : 0; @@ -170,9 +154,9 @@ export default class BaseBrush extends React.Component { const { onBrushEnd } = this.props; this.update((prevBrush: BaseBrushState) => { const { extent } = prevBrush; @@ -194,7 +178,7 @@ export default class BaseBrush extends React.Component { const { onChange } = this.props; this.setState(updater, () => { if (onChange) { onChange(this.state); } }); - } + }; reset() { const { width, height } = this.props; @@ -351,7 +335,7 @@ export default class BaseBrush extends React.Component - {(draw: any) => ( + {({ dragStart, isDragging, dragMove, dragEnd }) => ( ) => { this.mouseDownTime = new Date(); - draw.dragStart(event); + dragStart(event); }} onMouseLeave={(event: React.MouseEvent) => { if (onMouseLeave) onMouseLeave(event); }} onMouseMove={(event: React.MouseEvent) => { - if (!draw.isDragging && onMouseMove) onMouseMove(event); - if (draw.isDragging) draw.dragMove(event); + if (!isDragging && onMouseMove) onMouseMove(event); + if (isDragging) dragMove(event); }} onMouseUp={(event: React.MouseEvent) => { this.mouseUpTime = new Date(); if (onMouseUp) onMouseUp(event); - draw.dragEnd(event); + dragEnd(event); }} style={{ cursor: 'crosshair' }} /> diff --git a/packages/vx-brush/src/Brush.tsx b/packages/vx-brush/src/Brush.tsx index a903b4733..fdf8aa556 100644 --- a/packages/vx-brush/src/Brush.tsx +++ b/packages/vx-brush/src/Brush.tsx @@ -29,6 +29,8 @@ export type BrushProps = { }; class Brush extends React.Component { + private BaseBrush: any = React.createRef(); + static defaultProps = { xScale: null, yScale: null, @@ -62,25 +64,13 @@ class Brush extends React.Component { onClick: null, }; - private BaseBrush: BaseBrush | null; - - constructor(props: BrushProps) { - super(props); - - this.BaseBrush = null; - this.handleChange = this.handleChange.bind(this); - this.handleBrushStart = this.handleBrushStart.bind(this); - this.handleBrushEnd = this.handleBrushEnd.bind(this); - this.reset = this.reset.bind(this); - } - reset() { if (this.BaseBrush) { this.BaseBrush.reset(); } } - handleChange(brush: BaseBrushState) { + handleChange = (brush: BaseBrushState) => { const { onChange } = this.props; if (!onChange) return; const { x0 } = brush.extent; @@ -91,7 +81,7 @@ class Brush extends React.Component { } const domain = this.convertRangeToDomain(brush); onChange(domain); - } + }; convertRangeToDomain(brush: BaseBrushState) { const { xScale, yScale } = this.props; @@ -112,7 +102,7 @@ class Brush extends React.Component { return domain; } - handleBrushStart(point: Point) { + handleBrushStart = (point: Point) => { const { x, y } = point; const { onBrushStart, xScale, yScale } = this.props; const invertedX = scaleInvert(xScale, x); @@ -125,9 +115,9 @@ class Brush extends React.Component { y: yScale.invert ? invertedY : yScale.domain()[invertedY], }); } - } + }; - handleBrushEnd(brush: BaseBrushState) { + handleBrushEnd = (brush: BaseBrushState) => { const { onBrushEnd } = this.props; if (!onBrushEnd) return; const { x0 } = brush.extent; @@ -138,7 +128,7 @@ class Brush extends React.Component { } const domain = this.convertRangeToDomain(brush); onBrushEnd(domain); - } + }; render() { const { @@ -215,9 +205,7 @@ class Brush extends React.Component { onMouseLeave={onMouseLeave} onMouseMove={onMouseMove} onClick={onClick} - ref={el => { - this.BaseBrush = el; - }} + ref={this.BaseBrush} /> ); } diff --git a/packages/vx-brush/src/BrushCorner.tsx b/packages/vx-brush/src/BrushCorner.tsx index 9be45cf15..745b53a4d 100644 --- a/packages/vx-brush/src/BrushCorner.tsx +++ b/packages/vx-brush/src/BrushCorner.tsx @@ -20,13 +20,7 @@ export default class BrushCorner extends React.Component { const { updateBrush, type } = this.props; if (!drag.isDragging) return; updateBrush((prevBrush: BrushState) => { @@ -108,9 +102,9 @@ export default class BrushCorner extends React.Component { const { updateBrush, onBrushEnd } = this.props; updateBrush((prevBrush: BrushState) => { const { start, end, extent } = prevBrush; @@ -136,7 +130,7 @@ export default class BrushCorner extends React.Component - {(handle: any) => ( + {({ dragMove, dragEnd, dragStart, isDragging }) => ( - {handle.isDragging && ( + {isDragging && ( )} { - constructor(props: BrushHandleProps) { - super(props); - this.handleDragMove = this.handleDragMove.bind(this); - this.handleDragEnd = this.handleDragEnd.bind(this); - } - - handleDragMove(drag: any) { + handleDragMove = (drag: any) => { const { updateBrush, type } = this.props; if (!drag.isDragging) return; updateBrush((prevBrush: BrushState) => { @@ -87,9 +81,9 @@ export default class BrushHandle extends React.Component { return nextState; }); - } + }; - handleDragEnd() { + handleDragEnd = () => { const { updateBrush, onBrushEnd } = this.props; updateBrush((prevBrush: BrushState) => { const { start, end, extent } = prevBrush; @@ -116,7 +110,7 @@ export default class BrushHandle extends React.Component { return nextBrush; }); - } + }; render() { const { stageWidth, stageHeight, brush, type, handle } = this.props; @@ -131,7 +125,7 @@ export default class BrushHandle extends React.Component { onDragEnd={this.handleDragEnd} resetOnStart > - {(drag: any) => ( + {({ isDragging, dragStart, dragEnd, dragMove }) => ( {handle.isDragging && ( { width={stageWidth} height={stageHeight} style={{ cursor }} - onMouseMove={drag.dragMove} - onMouseUp={drag.dragEnd} - onMouseLeave={drag.dragEnd} + onMouseMove={dragMove} + onMouseUp={dragEnd} + onMouseLeave={dragEnd} /> )} { height={height} fill="transparent" className={`vx-brush-handle-${type}`} - onMouseDown={drag.dragStart} - onMouseMove={drag.dragMove} - onMouseUp={drag.dragEnd} + onMouseDown={dragStart} + onMouseMove={dragMove} + onMouseUp={dragEnd} style={{ cursor, pointerEvents: !!brush.activeHandle || !!brush.isBrushing ? 'none' : 'all', diff --git a/packages/vx-brush/src/BrushSelection.tsx b/packages/vx-brush/src/BrushSelection.tsx index 010c32d1d..246137702 100644 --- a/packages/vx-brush/src/BrushSelection.tsx +++ b/packages/vx-brush/src/BrushSelection.tsx @@ -26,13 +26,7 @@ export default class BrushSelection extends React.Component onClick: null, }; - constructor(props: BrushSelectionProps) { - super(props); - this.selectionDragMove = this.selectionDragMove.bind(this); - this.selectionDragEnd = this.selectionDragEnd.bind(this); - } - - selectionDragMove(drag: any) { + selectionDragMove = (drag: any) => { const { updateBrush } = this.props; updateBrush((prevBrush: BrushState) => { const { x: x0, y: y0 } = prevBrush.start; @@ -59,9 +53,9 @@ export default class BrushSelection extends React.Component }, }; }); - } + }; - selectionDragEnd() { + selectionDragEnd = () => { const { updateBrush, onBrushEnd } = this.props; updateBrush((prevBrush: BrushState) => { const nextBrush = { @@ -84,7 +78,7 @@ export default class BrushSelection extends React.Component return nextBrush; }); - } + }; render() { const { @@ -111,16 +105,16 @@ export default class BrushSelection extends React.Component onDragMove={this.selectionDragMove} onDragEnd={this.selectionDragEnd} > - {(selection: any) => ( + {({ isDragging, dragStart, dragEnd, dragMove }) => ( - {selection.isDragging && ( + {isDragging && ( width={width} height={height} className="vx-brush-selection" - onMouseDown={disableDraggingSelection ? null : selection.dragStart} + // @ts-ignore + onMouseDown={disableDraggingSelection ? null : dragStart} onMouseLeave={event => { if (onMouseLeave) onMouseLeave(event); }} onMouseMove={event => { - selection.dragMove(event); + dragMove(event); if (onMouseMove) onMouseMove(event); }} onMouseUp={event => { - selection.dragEnd(event); + dragEnd(event); if (onMouseUp) onMouseUp(event); }} onClick={event => { From 91ae0aa77df9cbcfdcf79b6756193a89dbd994de Mon Sep 17 00:00:00 2001 From: geekplux Date: Tue, 19 Nov 2019 12:11:33 +0800 Subject: [PATCH 14/16] fix: lint --- packages/vx-brush/src/BaseBrush.tsx | 5 ++++- packages/vx-brush/src/BrushCorner.tsx | 1 + packages/vx-brush/test/BoxBrush.test.js | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/vx-brush/src/BaseBrush.tsx b/packages/vx-brush/src/BaseBrush.tsx index bff7637aa..d1697025d 100644 --- a/packages/vx-brush/src/BaseBrush.tsx +++ b/packages/vx-brush/src/BaseBrush.tsx @@ -78,7 +78,7 @@ export default class BaseBrush extends React.Component this.props[prop] !== nextProps[prop])) { this.setState(() => ({ @@ -89,7 +89,10 @@ export default class BaseBrush extends React.Component', () => { test('it should be defined', () => { - expect(BoxBrush).toBeDefined(); + expect(Brush).toBeDefined(); }); }); From 6cfbb2b7b940b0d66f13915c7fc3908eeefcafdb Mon Sep 17 00:00:00 2001 From: geekplux Date: Tue, 19 Nov 2019 12:39:03 +0800 Subject: [PATCH 15/16] fix: test of brush --- packages/vx-vx/test/index.test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/vx-vx/test/index.test.js b/packages/vx-vx/test/index.test.js index 59e62eaad..a381e636c 100644 --- a/packages/vx-vx/test/index.test.js +++ b/packages/vx-vx/test/index.test.js @@ -22,10 +22,6 @@ describe('vx', () => { expect(vx.BoxPlot).toBeDefined(); }); - test('it should export @vx/brush', () => { - expect(vx.withBrush).toBeDefined(); - }); - test('it should export @vx/clip-path', () => { expect(vx.ClipPath).toBeDefined(); }); From 3110fd2a2e64609ebd0ae695e0beb4da9aff8f69 Mon Sep 17 00:00:00 2001 From: geekplux Date: Mon, 25 Nov 2019 14:06:11 +0800 Subject: [PATCH 16/16] fix: lint and test --- packages/vx-brush/test/{BoxBrush.test.js => Brush.test.tsx} | 2 +- packages/vx-responsive/test/ScaleSVG.test.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename packages/vx-brush/test/{BoxBrush.test.js => Brush.test.tsx} (77%) diff --git a/packages/vx-brush/test/BoxBrush.test.js b/packages/vx-brush/test/Brush.test.tsx similarity index 77% rename from packages/vx-brush/test/BoxBrush.test.js rename to packages/vx-brush/test/Brush.test.tsx index c725cefb1..4d395a37f 100644 --- a/packages/vx-brush/test/BoxBrush.test.js +++ b/packages/vx-brush/test/Brush.test.tsx @@ -1,6 +1,6 @@ import { Brush } from '../src'; -describe('', () => { +describe('', () => { test('it should be defined', () => { expect(Brush).toBeDefined(); }); diff --git a/packages/vx-responsive/test/ScaleSVG.test.tsx b/packages/vx-responsive/test/ScaleSVG.test.tsx index 3d36ce1bf..4c9782bcf 100644 --- a/packages/vx-responsive/test/ScaleSVG.test.tsx +++ b/packages/vx-responsive/test/ScaleSVG.test.tsx @@ -9,6 +9,7 @@ describe('', () => { }); test('it should expose its ref via an innerRef prop', () => { + // eslint-disable-next-line require-await return new Promise(done => { const refCallback = (n: SVGSVGElement) => { expect(n.tagName).toEqual('svg');