From 45d4288b812e0da559ea239a0159caaa17c88824 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Tue, 9 Jul 2019 08:09:18 -0400 Subject: [PATCH] Sequester zoom concerns to ZoomManager in plexus Signed-off-by: Joe Farro Signed-off-by: vvvprabhakar --- packages/plexus/package.json | 2 +- .../src/DirectedGraph/DirectedGraph.tsx | 91 +++------- packages/plexus/src/ZoomManager/index.tsx | 156 ++++++++++++++++++ .../utils.tsx} | 11 +- 4 files changed, 185 insertions(+), 75 deletions(-) create mode 100644 packages/plexus/src/ZoomManager/index.tsx rename packages/plexus/src/{DirectedGraph/transform-utils.tsx => ZoomManager/utils.tsx} (93%) diff --git a/packages/plexus/package.json b/packages/plexus/package.json index 450f19b50b..a4986e6703 100644 --- a/packages/plexus/package.json +++ b/packages/plexus/package.json @@ -60,7 +60,7 @@ "build": "NODE_ENV=production npm-run-all -ln --serial _tasks/clean/* _tasks/bundle-worker --parallel _tasks/build/**", "coverage": "echo 'NO TESTS YET'", "prepublishOnly": "$npm_execpath build", - "start": "NODE_ENV='development' npm-run-all -ln --serial _tasks/clean/worker --parallel '_tasks/bundle-worker --watch' _tasks/dev-server", + "start": "NODE_ENV='development' npm-run-all -ln --serial _tasks/clean/worker _tasks/bundle-worker --parallel '_tasks/bundle-worker --watch' _tasks/dev-server", "test": "echo 'NO TESTS YET'" } } diff --git a/packages/plexus/src/DirectedGraph/DirectedGraph.tsx b/packages/plexus/src/DirectedGraph/DirectedGraph.tsx index ef8feb1f81..a0b46b0ad0 100644 --- a/packages/plexus/src/DirectedGraph/DirectedGraph.tsx +++ b/packages/plexus/src/DirectedGraph/DirectedGraph.tsx @@ -13,8 +13,6 @@ // limitations under the License. import * as React from 'react'; -import { select } from 'd3-selection'; -import { zoom as d3Zoom, zoomIdentity, zoomTransform as getTransform, ZoomTransform } from 'd3-zoom'; import defaultGetNodeLabel from './builtins/defaultGetNodeLabel'; import EdgeArrowDef from './builtins/EdgeArrowDef'; @@ -25,17 +23,9 @@ import MiniMap from './MiniMap'; import classNameIsSmall from './prop-factories/classNameIsSmall'; import mergePropSetters, { mergeClassNameAndStyle } from './prop-factories/mergePropSetters'; import scaledStrokeWidth from './prop-factories/scaledStrokeWidth'; -import { - constrainZoom, - DEFAULT_SCALE_EXTENT, - fitWithinContainer, - getScaleExtent, - getZoomAttr, - getZoomStyle, -} from './transform-utils'; - import { TDirectedGraphProps, TDirectedGraphState } from './types'; import { TCancelled, TLayoutDone, TPositionsDone, TSizeVertex } from '../types'; +import ZoomManager, { zoomIdentity, ZoomTransform } from '../ZoomManager'; const PHASE_NO_DATA = 0; const PHASE_CALC_SIZES = 1; @@ -70,11 +60,9 @@ export default class DirectedGraph extends React.PureComponent< > { arrowId: string; arrowIriRef: string; - // ref API defs in flow seem to be a WIP - // https://github.com/facebook/flow/issues/6103 - rootRef: { current: HTMLDivElement | null }; + rootRef: React.RefObject; rootSelection: any; - zoom: any; + zoomManager: ZoomManager | null; static propsFactories = { classNameIsSmall, @@ -86,12 +74,10 @@ export default class DirectedGraph extends React.PureComponent< arrowScaleDampener: undefined, className: '', classNamePrefix: 'plexus', - // getEdgeLabel: defaultGetEdgeLabel, getNodeLabel: defaultGetNodeLabel, minimap: false, minimapClassName: '', zoom: false, - zoomTransform: zoomIdentity, }; state: TDirectedGraphState = { @@ -144,16 +130,18 @@ export default class DirectedGraph extends React.PureComponent< this.arrowIriRef = EdgeArrowDef.getIriRef(idBase); this.rootRef = React.createRef(); if (zoomEnabled) { - this.zoom = d3Zoom() - .scaleExtent(DEFAULT_SCALE_EXTENT) - .constrain(this._constrainZoom) - .on('zoom', this._onZoomed); + this.zoomManager = new ZoomManager(this._onZoomUpdated); + } else { + this.zoomManager = null; } } componentDidMount() { this._setSizeVertices(); - this.rootSelection = select(this.rootRef.current); + const { current } = this.rootRef; + if (current && this.zoomManager) { + this.zoomManager.setElement(current); + } } componentDidUpdate() { @@ -175,48 +163,14 @@ export default class DirectedGraph extends React.PureComponent< if (result.isCancelled || !root) { return; } - const { zoomEnabled } = this.state; const { edges: layoutEdges, graph: layoutGraph, vertices: layoutVertices } = result; - const { clientHeight: height, clientWidth: width } = root; - let zoomTransform = zoomIdentity; - if (zoomEnabled) { - const scaleExtent = getScaleExtent(layoutGraph.width, layoutGraph.height, width, height); - zoomTransform = fitWithinContainer(layoutGraph.width, layoutGraph.height, width, height); - this.zoom.scaleExtent(scaleExtent); - this.rootSelection.call(this.zoom); - // set the initial transform - this.zoom.transform(this.rootSelection, zoomTransform); - } - this.setState({ layoutEdges, layoutGraph, layoutVertices, zoomTransform, layoutPhase: PHASE_DONE }); - }; - - _onZoomed = () => { - const root = this.rootRef.current; - if (!root) { - return; - } - const zoomTransform = getTransform(root); - this.setState({ zoomTransform }); - }; - - _constrainZoom = (transform: ZoomTransform, extent: [[number, number], [number, number]]) => { - const [, [vw, vh]] = extent; - const { height: h = null, width: w = null } = this.state.layoutGraph || {}; - if (h == null || w == null) { - return transform; + this.setState({ layoutEdges, layoutGraph, layoutVertices, layoutPhase: PHASE_DONE }); + if (this.zoomManager) { + this.zoomManager.setContentSize(layoutGraph); } - return constrainZoom(transform, w, h, vw, vh); }; - _resetZoom = () => { - const root = this.rootRef.current; - const layoutGraph = this.state.layoutGraph; - if (!root || !layoutGraph) { - return; - } - const { clientHeight: height, clientWidth: width } = root; - const zoomTransform = fitWithinContainer(layoutGraph.width, layoutGraph.height, width, height); - this.zoom.transform(this.rootSelection, zoomTransform); + _onZoomUpdated = (zoomTransform: ZoomTransform) => { this.setState({ zoomTransform }); }; @@ -279,14 +233,14 @@ export default class DirectedGraph extends React.PureComponent< } = this.props; const { layoutPhase: phase, layoutGraph, zoomEnabled, zoomTransform } = this.state; const { height = 0, width = 0 } = layoutGraph || {}; - const { current: rootElm } = this.rootRef; + // const { current: rootElm } = this.rootRef; const haveEdges = phase === PHASE_DONE; const nodesContainerProps = mergeClassNameAndStyle( (setOnNodesContainer && setOnNodesContainer(this.state)) || {}, { style: { - ...(zoomEnabled ? getZoomStyle(zoomTransform) : null), + ...(zoomEnabled ? ZoomManager.getZoomStyle(zoomTransform) : null), position: 'absolute', top: 0, left: 0, @@ -315,20 +269,17 @@ export default class DirectedGraph extends React.PureComponent< scaleDampener={arrowScaleDampener} zoomScale={zoomEnabled && zoomTransform ? zoomTransform.k : null} /> - {this._renderEdges()} + + {this._renderEdges()} + )}
{this._renderVertices()}
- {zoomEnabled && minimapEnabled && layoutGraph && rootElm && ( + {zoomEnabled && minimapEnabled && this.zoomManager && ( )} diff --git a/packages/plexus/src/ZoomManager/index.tsx b/packages/plexus/src/ZoomManager/index.tsx new file mode 100644 index 0000000000..0f932e2598 --- /dev/null +++ b/packages/plexus/src/ZoomManager/index.tsx @@ -0,0 +1,156 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { select, Selection } from 'd3-selection'; +import { + zoom as d3Zoom, + zoomTransform as getTransform, + ZoomTransform as ZoomTransform_, + ZoomBehavior, + zoomIdentity, +} from 'd3-zoom'; + +import { + constrainZoom, + DEFAULT_SCALE_EXTENT, + fitWithinContainer, + getScaleExtent, + getZoomAttr, + getZoomStyle, +} from './utils'; + +export { zoomIdentity } from 'd3-zoom'; + +export type ZoomTransform = ZoomTransform_; + +type TUpdateCallback = (xform: ZoomTransform) => void; + +type TSize = { height: number; width: number }; + +type TZoomProps = { + x: number; + y: number; + k: number; + contentHeight: number; + contentWidth: number; + viewAll: () => void; + viewportHeight: number; + viewportWidth: number; +}; + +export default class ZoomManager { + static getZoomAttr(zoomTransform: ZoomTransform | void) { + return getZoomAttr(zoomTransform); + } + + static getZoomStyle(zoomTransform: ZoomTransform | void) { + return getZoomStyle(zoomTransform); + } + + private elem: Element | null = null; + private contentSize: TSize | null = null; + private selection: Selection | null = null; + private readonly updateCallback: TUpdateCallback; + private readonly zoom: ZoomBehavior; + private currentTransform: ZoomTransform = zoomIdentity; + + constructor(updateCallback: TUpdateCallback) { + this.updateCallback = updateCallback; + this.zoom = d3Zoom() + .scaleExtent(DEFAULT_SCALE_EXTENT) + .constrain(this.constrainZoom) + .on('zoom', this.onZoomed); + } + + public setElement(elem: Element) { + if (elem === this.elem) { + return; + } + this.elem = elem; + this.selection = select(elem); + this.selection.call(this.zoom); + this.setExtent(); + this.resetZoom(); + } + + public setContentSize(size: TSize) { + if ( + !this.contentSize || + this.contentSize.height !== size.height || + this.contentSize.width !== size.width + ) { + this.contentSize = size; + } + this.setExtent(); + this.resetZoom(); + } + + public resetZoom = () => { + const elem = this.elem; + const selection = this.selection; + const size = this.contentSize; + if (!elem || !selection || !size) { + this.updateCallback(zoomIdentity); + return; + } + const { clientHeight: viewHeight, clientWidth: viewWidth } = elem; + this.currentTransform = fitWithinContainer(size.width, size.height, viewWidth, viewHeight); + this.zoom.transform(selection, this.currentTransform); + this.updateCallback(this.currentTransform); + }; + + public getProps(): TZoomProps { + const { x, y, k } = this.currentTransform; + const { height: contentHeight = 1, width: contentWidth = 1 } = this.contentSize || {}; + const { clientHeight: viewportHeight = 1, clientWidth: viewportWidth = 1 } = this.elem || {}; + return { + contentHeight, + contentWidth, + k, + viewportHeight, + viewportWidth, + x, + y, + viewAll: this.resetZoom, + }; + } + + private setExtent() { + const elem = this.elem; + const size = this.contentSize; + if (!elem || !size) { + return; + } + const { clientHeight: viewHeight, clientWidth: viewWidth } = elem; + const scaleExtent = getScaleExtent(size.width, size.height, viewWidth, viewHeight); + this.zoom.scaleExtent(scaleExtent); + } + + private onZoomed = () => { + if (!this.elem) { + return; + } + this.currentTransform = getTransform(this.elem); + this.updateCallback(this.currentTransform); + }; + + private constrainZoom = (transform: ZoomTransform, extent: [[number, number], [number, number]]) => { + if (!this.contentSize) { + return transform; + } + const { height, width } = this.contentSize; + const [, [vw, vh]] = extent; + return constrainZoom(transform, width, height, vw, vh); + }; +} diff --git a/packages/plexus/src/DirectedGraph/transform-utils.tsx b/packages/plexus/src/ZoomManager/utils.tsx similarity index 93% rename from packages/plexus/src/DirectedGraph/transform-utils.tsx rename to packages/plexus/src/ZoomManager/utils.tsx index 420ff987cd..3da3506e92 100644 --- a/packages/plexus/src/DirectedGraph/transform-utils.tsx +++ b/packages/plexus/src/ZoomManager/utils.tsx @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Uber Technologies, Inc. +// Copyright (c) 2019 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,8 +14,6 @@ import { zoomIdentity, ZoomTransform } from 'd3-zoom'; -// import { TD3Transform } from './types'; - const SCALE_MAX = 1; const SCALE_MIN = 0.03; const SCALE_MARGIN = 0.05; @@ -36,7 +34,12 @@ function getFittedScale(width: number, height: number, viewWidth: number, viewHe ); } -export function getScaleExtent(width: number, height: number, viewWidth: number, viewHeight: number) { +export function getScaleExtent( + width: number, + height: number, + viewWidth: number, + viewHeight: number +): [number, number] { const scaleMin = getFittedScale(width, height, viewWidth, viewHeight); return [scaleMin, SCALE_MAX]; }