From 33a44b6318468383b0fa646f83f82f4d9c7b883c Mon Sep 17 00:00:00 2001 From: Harry Shoff Date: Mon, 7 Jan 2019 11:55:45 -0500 Subject: [PATCH 1/7] [mock data] add genPhyllotaxis --- packages/vx-mock-data/src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vx-mock-data/src/index.js b/packages/vx-mock-data/src/index.js index aeff68674..f7e951011 100644 --- a/packages/vx-mock-data/src/index.js +++ b/packages/vx-mock-data/src/index.js @@ -2,6 +2,7 @@ export { default as genDateValue } from './generators/genDateValue'; export { default as genRandomNormalPoints } from './generators/genRandomNormalPoints'; export { default as genBin } from './generators/genBin'; export { default as genBins } from './generators/genBins'; +export { default as genPhyllotaxis } from './generators/genPhyllotaxis'; export { default as genStats } from './generators/genStats'; export { default as appleStock } from './mocks/appleStock'; export { default as letterFrequency } from './mocks/letterFrequency'; From 3ecf00afb3527b14d3475ff5d0648d0ce1150e02 Mon Sep 17 00:00:00 2001 From: Harry Shoff Date: Mon, 7 Jan 2019 12:12:17 -0500 Subject: [PATCH 2/7] [mock data] add genPhyllotaxis --- .../src/generators/genPhyllotaxis.js | 11 ++++++++ .../vx-mock-data/test/genPhyllotaxis.test.js | 28 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 packages/vx-mock-data/src/generators/genPhyllotaxis.js create mode 100644 packages/vx-mock-data/test/genPhyllotaxis.test.js diff --git a/packages/vx-mock-data/src/generators/genPhyllotaxis.js b/packages/vx-mock-data/src/generators/genPhyllotaxis.js new file mode 100644 index 000000000..a6537a491 --- /dev/null +++ b/packages/vx-mock-data/src/generators/genPhyllotaxis.js @@ -0,0 +1,11 @@ +export default function genPhyllotaxis({ radius, width, height }) { + const theta = Math.PI * (3 - Math.sqrt(5)); + return i => { + const r = radius * Math.sqrt(i); + const a = theta * i; + return { + x: width / 2 + r * Math.cos(a), + y: height / 2 + r * Math.sin(a) + }; + }; +} diff --git a/packages/vx-mock-data/test/genPhyllotaxis.test.js b/packages/vx-mock-data/test/genPhyllotaxis.test.js new file mode 100644 index 000000000..0bfb32081 --- /dev/null +++ b/packages/vx-mock-data/test/genPhyllotaxis.test.js @@ -0,0 +1,28 @@ +import { genPhyllotaxis } from '../src'; + +describe('generators/genPhyllotaxis', () => { + test('it should be defined', () => { + expect(genPhyllotaxis).toBeDefined(); + }); + + test('it should return a function', () => { + const pointFn = genPhyllotaxis({ + radius: 10, + width: 200, + height: 200 + }); + expect(typeof pointFn).toEqual('function'); + }); + + test('it should return a point [x, y] when calling the returned function', () => { + const pointFn = genPhyllotaxis({ + radius: 10, + width: 200, + height: 200 + }); + const point = pointFn(3); + const expected = { x: 110, y: 113 }; + expect(Math.floor(point.x)).toEqual(expected.x); + expect(Math.floor(point.y)).toEqual(expected.y); + }); +}); From cbab20f3eecbde6d43773ae5dd8c259b69bf8ff5 Mon Sep 17 00:00:00 2001 From: Harry Shoff Date: Wed, 9 Jan 2019 16:59:36 -0500 Subject: [PATCH 3/7] [demo] zoom experiment --- packages/vx-demo/components/show.js | 8 +- packages/vx-demo/pages/zoom.js | 273 ++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 packages/vx-demo/pages/zoom.js diff --git a/packages/vx-demo/components/show.js b/packages/vx-demo/components/show.js index 6a7962c64..6163b7907 100644 --- a/packages/vx-demo/components/show.js +++ b/packages/vx-demo/components/show.js @@ -56,9 +56,11 @@ export default withScreenSize( )} -
- -
+ {false && ( +
+ +
+ )} + + ); + } +} + +export default () => { + return ( + + {`import React from 'react'; +import { Zoom } from '@vx/zoom'; +import { localPoint } from '@vx/event'; +import { RectClipPath } from '@vx/clip-path'; +import { genPhyllotaxis } from '@vx/mock-data'; +import { scaleLinear } from '@vx/scale'; +import { interpolateRainbow } from 'd3-scale-chromatic'; + +const bg = '#0a0a0a'; +const points = [...new Array(1000)]; + +const colorScale = scaleLinear({ range: [0, 1], domain: [0, 1000] }); +const sizeScale = scaleLinear({ domain: [0, 600], range: [0.5, 8] }); + +const initialTransform = { + scaleX: 1.27, + scaleY: 1.27, + translateX: -211.62, + translateY: 162.59, + skewX: 0, + skewY: 0 +}; class ZoomDemo extends React.Component { constructor(props) { super(props); + this.state = { showMiniMap: true }; + this.toggleMiniMap = this.toggleMiniMap.bind(this); + } + + toggleMiniMap() { + this.setState(prevState => { + return { + showMiniMap: !prevState.showMiniMap + }; + }); } + render() { - const { width, height, margin } = this.props; + const { width, height } = this.props; + const { showMiniMap } = this.state; + const gen = genPhyllotaxis({ radius: 10, width, height }); - const phyllotaxis = [...new Array(1000)].map((d, i) => gen(i)); + const phyllotaxis = points.map((d, i) => gen(i)); + return ( {zoom => { - console.log(zoom); return (
- - - - - - + - {phyllotaxis.map((d, i) => { - const { x, y } = d; + {phyllotaxis.map((point, i) => { return ( - + + 500 ? sizeScale(1000 - i) : sizeScale(i)} + fill={interpolateRainbow(colorScale(i))} + /> + ); })} @@ -61,66 +306,76 @@ class ZoomDemo extends React.Component { onMouseDown={zoom.dragStart} onMouseMove={zoom.dragMove} onMouseUp={zoom.dragEnd} + onMouseLeave={() => { + if (!zoom.isDragging) return; + zoom.dragEnd(); + }} + onDoubleClick={event => { + const point = localPoint(event); + zoom.scale({ scaleX: 1.1, scaleY: 1.1, point }); + }} /> + {showMiniMap && ( + + + {phyllotaxis.map((d, i) => { + const { x, y } = d; + return ( + + 500 ? sizeScale(1000 - i) : sizeScale(i)} + fill={interpolateRainbow(colorScale(i))} + /> + + ); + })} + + + )} -
+
- -
-
{`scaleX: ${zoom.transformMatrix.scaleX.toFixed(2)}`}
-
{`scaleY: ${zoom.transformMatrix.scaleY.toFixed(2)}`}
-
{`translateX: ${zoom.transformMatrix.translateX.toFixed(2)}`}
-
{`translateY: ${zoom.transformMatrix.translateY.toFixed(2)}`}
-
+ + + +
+
+
); @@ -128,12 +383,7 @@ class ZoomDemo extends React.Component { ); } -} - -export default () => { - return ( - - {``} +}`} ); }; diff --git a/packages/vx-zoom/src/Zoom.js b/packages/vx-zoom/src/Zoom.js index b2dd4ee60..fbc63dc8c 100644 --- a/packages/vx-zoom/src/Zoom.js +++ b/packages/vx-zoom/src/Zoom.js @@ -1,98 +1,15 @@ -/* eslint-disable no-trailing-spaces */ -/* eslint-disable no-case-declarations */ import React from 'react'; import PropTypes from 'prop-types'; import { localPoint } from '@vx/event'; - -export function identityMatrix() { - return { - scaleX: 1, - scaleY: 1, - translateX: 0, - translateY: 0, - skewX: 0, - skewY: 0 - }; -} - -export function createMatrix({ - scaleX = 1, - scaleY = 1, - translateX = 0, - translateY = 0, - skewX = 0, - skewY = 0 -}) { - return { - scaleX, - scaleY, - translateX, - translateY, - skewX, - skewY - }; -} - -export function inverseMatrix({ scaleX, scaleY, translateX, translateY, skewX, skewY }) { - const denominator = scaleX * scaleY - skewY * skewX; - return { - scaleX: scaleY / denominator, - scaleY: scaleX / denominator, - translateX: (scaleY * translateX - skewX * translateY) / -denominator, - translateY: (skewY * translateX - scaleX * translateY) / denominator, - skewX: skewX / -denominator, - skewY: skewY / -denominator - }; -} - -export function applyMatrixToPoint(matrix, { x, y }) { - return { - x: matrix.scaleX * x + matrix.skewX * y + matrix.translateX, - y: matrix.skewY * x + matrix.scaleY * y + matrix.translateY - }; -} - -export function applyInverseMatrixToPoint(matrix, { x, y }) { - return applyMatrixToPoint(inverseMatrix(matrix), { x, y }); -} - -export function scaleMatrix(scaleX, scaleY = undefined) { - if (!scaleY) scaleY = scaleX; - return createMatrix({ scaleX, scaleY }); -} - -export function translateMatrix(translateX, translateY = undefined) { - if (!translateY) translateY = translateX; - return createMatrix({ translateX, translateY }); -} - -function multiplyMatrices(matrix1, matrix2) { - return { - scaleX: matrix1.scaleX * matrix2.scaleX + matrix1.skewX * matrix2.skewY, - scaleY: matrix1.skewY * matrix2.skewX + matrix1.scaleY * matrix2.scaleY, - translateX: - matrix1.scaleX * matrix2.translateX + matrix1.skewX * matrix2.translateY + matrix1.translateX, - translateY: - matrix1.skewY * matrix2.translateX + matrix1.scaleY * matrix2.translateY + matrix1.translateY, - skewX: matrix1.scaleX * matrix2.skewX + matrix1.skewX * matrix2.scaleY, - skewY: matrix1.skewY * matrix2.scaleX + matrix1.scaleY * matrix2.skewY - }; -} - -function composeMatrices(...matrices) { - switch (matrices.length) { - case 0: - throw new Error('composeMatrices() requires arguments: was called with no args'); - case 1: - return matrices[0]; - case 2: - return multiplyMatrices(matrices[0], matrices[1]); - default: - const [matrix1, matrix2, ...restMatrices] = matrices; - const matrix = multiplyMatrices(matrix1, matrix2); - return composeMatrices(matrix, ...restMatrices); - } -} +import { + composeMatrices, + inverseMatrix, + applyMatrixToPoint, + applyInverseMatrixToPoint, + translateMatrix, + identityMatrix, + scaleMatrix +} from './util/matrix'; class Zoom extends React.Component { constructor(props) { @@ -105,17 +22,33 @@ class Zoom extends React.Component { }; this.toString = this.toString.bind(this); + this.clear = this.clear.bind(this); + this.center = this.center.bind(this); this.handleWheel = this.handleWheel.bind(this); this.dragStart = this.dragStart.bind(this); this.dragMove = this.dragMove.bind(this); this.dragEnd = this.dragEnd.bind(this); this.reset = this.reset.bind(this); - this.constrain = props.constrain || this.constrain.bind(this); + this.constrain = props.constrain ? props.constrain.bind(this) : this.constrain.bind(this); this.scale = this.scale.bind(this); this.translate = this.translate.bind(this); this.translateTo = this.translateTo.bind(this); this.setTranslate = this.setTranslate.bind(this); this.setTransformMatrix = this.setTransformMatrix.bind(this); + this.invert = this.invert.bind(this); + this.applyToPoint = this.applyToPoint.bind(this); + this.applyInverseToPoint = this.applyInverseToPoint.bind(this); + this.toStringInvert = this.toStringInvert.bind(this); + } + + applyToPoint({ x, y }) { + const { transformMatrix } = this.state; + return applyMatrixToPoint(transformMatrix, { x, y }); + } + + applyInverseToPoint({ x, y }) { + const { transformMatrix } = this.state; + return applyInverseMatrixToPoint(transformMatrix, { x, y }); } reset() { @@ -139,7 +72,6 @@ class Zoom extends React.Component { } translate({ translateX, translateY }) { - if (!translateY) translateY = translateX; const { transformMatrix } = this.state; const nextMatrix = composeMatrices(transformMatrix, translateMatrix(translateX, translateY)); this.setTransformMatrix(nextMatrix); @@ -167,36 +99,25 @@ class Zoom extends React.Component { }); } + invert() { + return inverseMatrix(this.state.transformMatrix); + } + + toStringInvert() { + const { translateX, translateY, scaleX, scaleY, skewX, skewY } = this.invert(); + return `matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${translateX}, ${translateY})`; + } + constrain(transformMatrix, prevTransformMatrix) { - const { - scaleXMin, - scaleXMax, - scaleYMin, - scaleYMax, - translateXMin, - translateXMax, - translateYMin, - translateYMax, - skewXMin, - skewXMax, - skewYMin, - skewYMax - } = this.props; - const { scaleX, scaleY, translateX, translateY, skewX, skewY } = transformMatrix; - const nextScaleX = Math.min(scaleXMax, Math.max(scaleXMin, scaleX)); - const nextScaleY = Math.min(scaleYMax, Math.max(scaleYMin, scaleY)); - const nextTranslateX = Math.min(translateXMax, Math.max(translateXMin, translateX)); - const nextTranslateY = Math.min(translateYMax, Math.max(translateYMin, translateY)); - const nextSkewX = Math.min(skewXMax, Math.max(skewXMin, skewX)); - const nextSkewY = Math.min(skewYMax, Math.max(skewYMin, skewY)); - return { - scaleX: nextScaleX, - scaleY: nextScaleY, - translateX: nextTranslateX, - translateY: nextTranslateY, - skewX: nextSkewX, - skewY: nextSkewY - }; + const { scaleXMin, scaleXMax, scaleYMin, scaleYMax, constrain } = this.props; + const { scaleX, scaleY } = transformMatrix; + const shouldConstrainScaleX = scaleX > scaleXMax || scaleX < scaleXMin; + const shouldConstrainScaleY = scaleY > scaleYMax || scaleY < scaleYMin; + + if (shouldConstrainScaleX || shouldConstrainScaleY) { + return prevTransformMatrix; + } + return transformMatrix; } dragStart(event) { @@ -228,7 +149,7 @@ class Zoom extends React.Component { event.preventDefault(); const { wheelDelta } = this.props; const point = localPoint(event); - const { scaleX, scaleY } = wheelDelta(event.deltaY); + const { scaleX, scaleY } = wheelDelta(event); this.scale({ scaleX, scaleY, point }); } @@ -238,12 +159,28 @@ class Zoom extends React.Component { return `matrix(${scaleX}, ${skewY}, ${skewX}, ${scaleY}, ${translateX}, ${translateY})`; } + center() { + const { width, height } = this.props; + const center = { x: width / 2, y: height / 2 }; + const inverseCentroid = this.applyInverseToPoint(center); + this.translate({ + translateX: inverseCentroid.x - center.x, + translateY: inverseCentroid.y - center.y + }); + } + + clear() { + this.setTransformMatrix(identityMatrix()); + } + render() { const { children } = this.props; const zoom = { ...this.state, center: this.center, + clear: this.clear, scale: this.scale, + scaleTo: this.scaleTo, translate: this.translate, translateTo: this.translateTo, setTranslate: this.setTranslate, @@ -253,14 +190,18 @@ class Zoom extends React.Component { dragEnd: this.dragEnd, dragMove: this.dragMove, dragStart: this.dragStart, - toString: this.toString + toString: this.toString, + invert: this.invert, + toStringInvert: this.toStringInvert, + applyToPoint: this.applyToPoint, + applyInverseToPoint: this.applyInverseToPoint }; return children(zoom); } } Zoom.propTypes = { - children: PropTypes.func, + children: PropTypes.func.isRequired, width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, /** @@ -268,21 +209,38 @@ Zoom.propTypes = { * wheelDelta(event.deltaY) * ``` * - * A function that returns {scaleX,scaleY} to scale the matrix by. + * A function that returns {scaleX,scaleY} factors to scale the matrix by. + * Scale factors greater than 1 will increase (zoom in), less than 1 will descrease (zoom out). */ wheelDelta: PropTypes.func, scaleXMin: PropTypes.number, scaleXMax: PropTypes.number, scaleYMin: PropTypes.number, scaleYMax: PropTypes.number, - translateXMin: PropTypes.number, - translateXMax: PropTypes.number, - translateYMin: PropTypes.number, - translateYMax: PropTypes.number, - skewXMin: PropTypes.number, - skewXMax: PropTypes.number, - skewYMin: PropTypes.number, - skewYMax: PropTypes.number, + /** + * By default constrain() will only constrain scale values. To change + * constraints you can pass in your own constrain function as a prop. + * + * For example, if you wanted to constrain your view to within [[0, 0], [width, height]]: + * + * ```js + * function constrain(transformMatrix, prevTransformMatrix) { + * const min = applyMatrixToPoint(transformMatrix, { x: 0, y: 0 }); + * const max = applyMatrixToPoint(transformMatrix, { x: width, y: height }); + * if (max.x < width || max.y < height) { + * return prevTransformMatrix; + * } + * if (min.x > 0 || min.y > 0) { + * return prevTransformMatrix; + * } + * return transformMatrix; + * } + * ``` + * + * @param {matrix} transformMatrix + * @param {matrix} prevTransformMatrix + * @returns {martix} + */ constrain: PropTypes.func, transformMatrix: PropTypes.shape({ scaleX: PropTypes.number, @@ -299,14 +257,6 @@ Zoom.defaultProps = { scaleXMax: Infinity, scaleYMin: 0, scaleYMax: Infinity, - translateXMin: -Infinity, - translateXMax: Infinity, - translateYMin: -Infinity, - translateYMax: Infinity, - skewXMin: -Infinity, - skewXMax: Infinity, - skewYMin: -Infinity, - skewYMax: Infinity, transformMatrix: { scaleX: 1, scaleY: 1, @@ -315,8 +265,8 @@ Zoom.defaultProps = { skewX: 0, skewY: 0 }, - wheelDelta: deltaY => { - return deltaY > 0 ? { scaleX: 1.1, scaleY: 1.1 } : { scaleX: 0.9, scaleY: 0.9 }; + wheelDelta: event => { + return -event.deltaY > 0 ? { scaleX: 1.1, scaleY: 1.1 } : { scaleX: 0.9, scaleY: 0.9 }; } }; diff --git a/packages/vx-zoom/src/index.js b/packages/vx-zoom/src/index.js index ae5b24e9c..57fad4d99 100644 --- a/packages/vx-zoom/src/index.js +++ b/packages/vx-zoom/src/index.js @@ -1 +1,12 @@ export { default as Zoom } from './Zoom'; +export { + identityMatrix, + createMatrix, + inverseMatrix, + applyMatrixToPoint, + applyInverseMatrixToPoint, + scaleMatrix, + translateMatrix, + multiplyMatrices, + composeMatrices +} from './util/matrix'; diff --git a/packages/vx-zoom/src/util/matrix.js b/packages/vx-zoom/src/util/matrix.js new file mode 100644 index 000000000..de99ba614 --- /dev/null +++ b/packages/vx-zoom/src/util/matrix.js @@ -0,0 +1,90 @@ +/* eslint-disable no-trailing-spaces */ +/* eslint-disable no-case-declarations */ +export function identityMatrix() { + return { + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0, + skewX: 0, + skewY: 0 + }; +} + +export function createMatrix({ + scaleX = 1, + scaleY = 1, + translateX = 0, + translateY = 0, + skewX = 0, + skewY = 0 +}) { + return { + scaleX, + scaleY, + translateX, + translateY, + skewX, + skewY + }; +} + +export function inverseMatrix({ scaleX, scaleY, translateX, translateY, skewX, skewY }) { + const denominator = scaleX * scaleY - skewY * skewX; + return { + scaleX: scaleY / denominator, + scaleY: scaleX / denominator, + translateX: (scaleY * translateX - skewX * translateY) / -denominator, + translateY: (skewY * translateX - scaleX * translateY) / denominator, + skewX: skewX / -denominator, + skewY: skewY / -denominator + }; +} + +export function applyMatrixToPoint(matrix, { x, y }) { + return { + x: matrix.scaleX * x + matrix.skewX * y + matrix.translateX, + y: matrix.skewY * x + matrix.scaleY * y + matrix.translateY + }; +} + +export function applyInverseMatrixToPoint(matrix, { x, y }) { + return applyMatrixToPoint(inverseMatrix(matrix), { x, y }); +} + +export function scaleMatrix(scaleX, scaleY = undefined) { + if (!scaleY) scaleY = scaleX; + return createMatrix({ scaleX, scaleY }); +} + +export function translateMatrix(translateX, translateY) { + return createMatrix({ translateX, translateY }); +} + +export function multiplyMatrices(matrix1, matrix2) { + return { + scaleX: matrix1.scaleX * matrix2.scaleX + matrix1.skewX * matrix2.skewY, + scaleY: matrix1.skewY * matrix2.skewX + matrix1.scaleY * matrix2.scaleY, + translateX: + matrix1.scaleX * matrix2.translateX + matrix1.skewX * matrix2.translateY + matrix1.translateX, + translateY: + matrix1.skewY * matrix2.translateX + matrix1.scaleY * matrix2.translateY + matrix1.translateY, + skewX: matrix1.scaleX * matrix2.skewX + matrix1.skewX * matrix2.scaleY, + skewY: matrix1.skewY * matrix2.scaleX + matrix1.scaleY * matrix2.skewY + }; +} + +export function composeMatrices(...matrices) { + switch (matrices.length) { + case 0: + throw new Error('composeMatrices() requires arguments: was called with no args'); + case 1: + return matrices[0]; + case 2: + return multiplyMatrices(matrices[0], matrices[1]); + default: + const [matrix1, matrix2, ...restMatrices] = matrices; + const matrix = multiplyMatrices(matrix1, matrix2); + return composeMatrices(matrix, ...restMatrices); + } +} diff --git a/packages/vx-zoom/test/Zoom.test.js b/packages/vx-zoom/test/Zoom.test.js index 9a968f0a6..ee6fbb521 100644 --- a/packages/vx-zoom/test/Zoom.test.js +++ b/packages/vx-zoom/test/Zoom.test.js @@ -1,7 +1,13 @@ -import { Zoom } from '../src'; +import { Zoom, inverseMatrix } from '../src'; describe('Zoom', () => { test('it should be defined', () => { expect(Zoom).toBeDefined(); }); }); + +describe('inverseMatrix', () => { + test('it should be defined', () => { + expect(inverseMatrix).toBeDefined(); + }); +}); From 90d238f878ed9df6a375c06eb8fe016adc7cca2b Mon Sep 17 00:00:00 2001 From: Harry Shoff Date: Wed, 30 Jan 2019 16:14:04 -0800 Subject: [PATCH 6/7] [demo] update zoom-i demo tile --- packages/vx-demo/components/gallery.js | 19 +- packages/vx-demo/components/show.js | 8 +- .../zoom.js => components/tiles/zoom-i.js} | 179 +----------------- packages/vx-demo/pages/zoom-i.js | 176 +++++++++++++++++ 4 files changed, 201 insertions(+), 181 deletions(-) rename packages/vx-demo/{pages/zoom.js => components/tiles/zoom-i.js} (55%) create mode 100644 packages/vx-demo/pages/zoom-i.js diff --git a/packages/vx-demo/components/gallery.js b/packages/vx-demo/components/gallery.js index 71e71aa3d..c5936085d 100644 --- a/packages/vx-demo/components/gallery.js +++ b/packages/vx-demo/components/gallery.js @@ -42,6 +42,7 @@ import LinkTypes from './tiles/linkTypes'; import Threshold from './tiles/threshold'; import Chord from './tiles/chord'; import Polygons from './tiles/polygons'; +import ZoomI from './tiles/zoom-i'; const items = [ '#242424', @@ -903,7 +904,23 @@ export default class Gallery extends React.Component {
-
+ + +
+
+ + {({ width, height }) => } + +
+
+
Zoom I
+
+
{''}
+
+
+
+ +
diff --git a/packages/vx-demo/components/show.js b/packages/vx-demo/components/show.js index 6163b7907..6a7962c64 100644 --- a/packages/vx-demo/components/show.js +++ b/packages/vx-demo/components/show.js @@ -56,11 +56,9 @@ export default withScreenSize(
)} - {false && ( -
- -
- )} +
+ +