Skip to content

Commit

Permalink
Sequester zoom concerns to ZoomManager in plexus (jaegertracing#409)
Browse files Browse the repository at this point in the history
Sequester zoom concerns to ZoomManager in plexus
Signed-off-by: vvvprabhakar <vvvprabhakar@gmail.com>
  • Loading branch information
tiffon committed Jul 9, 2019
2 parents c8651c1 + b585478 commit 14509fb
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 75 deletions.
2 changes: 1 addition & 1 deletion packages/plexus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
}
}
91 changes: 21 additions & 70 deletions packages/plexus/src/DirectedGraph/DirectedGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -70,11 +60,9 @@ export default class DirectedGraph<T> 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<HTMLDivElement>;
rootSelection: any;
zoom: any;
zoomManager: ZoomManager | null;

static propsFactories = {
classNameIsSmall,
Expand All @@ -86,12 +74,10 @@ export default class DirectedGraph<T> extends React.PureComponent<
arrowScaleDampener: undefined,
className: '',
classNamePrefix: 'plexus',
// getEdgeLabel: defaultGetEdgeLabel,
getNodeLabel: defaultGetNodeLabel,
minimap: false,
minimapClassName: '',
zoom: false,
zoomTransform: zoomIdentity,
};

state: TDirectedGraphState = {
Expand Down Expand Up @@ -144,16 +130,18 @@ export default class DirectedGraph<T> 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() {
Expand All @@ -175,48 +163,14 @@ export default class DirectedGraph<T> 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 });
};

Expand Down Expand Up @@ -279,14 +233,14 @@ export default class DirectedGraph<T> 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,
Expand Down Expand Up @@ -315,20 +269,17 @@ export default class DirectedGraph<T> extends React.PureComponent<
scaleDampener={arrowScaleDampener}
zoomScale={zoomEnabled && zoomTransform ? zoomTransform.k : null}
/>
<g transform={zoomEnabled ? getZoomAttr(zoomTransform) : undefined}>{this._renderEdges()}</g>
<g transform={zoomEnabled ? ZoomManager.getZoomAttr(zoomTransform) : undefined}>
{this._renderEdges()}
</g>
</EdgesContainer>
)}
<div {...nodesContainerProps}>{this._renderVertices()}</div>
{zoomEnabled && minimapEnabled && layoutGraph && rootElm && (
{zoomEnabled && minimapEnabled && this.zoomManager && (
<MiniMap
className={minimapClassName}
classNamePrefix={classNamePrefix}
contentHeight={height}
contentWidth={width}
viewAll={this._resetZoom}
viewportHeight={rootElm.clientHeight}
viewportWidth={rootElm.clientWidth}
{...zoomTransform}
{...this.zoomManager.getProps()}
/>
)}
</div>
Expand Down
156 changes: 156 additions & 0 deletions packages/plexus/src/ZoomManager/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Element, ZoomTransform, any, any> | null = null;
private readonly updateCallback: TUpdateCallback;
private readonly zoom: ZoomBehavior<Element, ZoomTransform>;
private currentTransform: ZoomTransform = zoomIdentity;

constructor(updateCallback: TUpdateCallback) {
this.updateCallback = updateCallback;
this.zoom = d3Zoom<Element, ZoomTransform>()
.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);
};
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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];
}
Expand Down

0 comments on commit 14509fb

Please sign in to comment.