Skip to content

Commit

Permalink
Separated zoom limits from zoom state.
Browse files Browse the repository at this point in the history
  • Loading branch information
fbarl committed Mar 21, 2017
1 parent 09dfa10 commit b26f5fe
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 94 deletions.
6 changes: 3 additions & 3 deletions client/app/scripts/charts/nodes-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { connect } from 'react-redux';

import Logo from '../components/logo';
import NodesChartElements from './nodes-chart-elements';
import CachableZoomWrapper from '../components/cachable-zoom-wrapper';
import ZoomWrapper from '../components/zoom-wrapper';
import { clickBackground } from '../actions/app-actions';


Expand Down Expand Up @@ -47,9 +47,9 @@ class NodesChart extends React.Component {
<svg id="canvas" width="100%" height="100%" onClick={this.handleMouseClick}>
<Logo transform="translate(24,24) scale(0.25)" />
<EdgeMarkerDefinition selectedNodeId={selectedNodeId} />
<CachableZoomWrapper svg="canvas" disabled={selectedNodeId}>
<ZoomWrapper svg="canvas" disabled={selectedNodeId}>
<NodesChartElements />
</CachableZoomWrapper>
</ZoomWrapper>
</svg>
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions client/app/scripts/components/nodes-resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { connect } from 'react-redux';

import Logo from './logo';
import { layersTopologyIdsSelector } from '../selectors/resource-view/layers';
import CachableZoomWrapper from './cachable-zoom-wrapper';
import ZoomWrapper from './zoom-wrapper';
import NodesResourcesLayer from './nodes-resources/node-resources-layer';


Expand All @@ -24,9 +24,9 @@ class NodesResources extends React.Component {
<div className="nodes-resources">
<svg id="canvas" width="100%" height="100%">
<Logo transform="translate(24,24) scale(0.25)" />
<CachableZoomWrapper svg="canvas" bounded forwardTransform fixVertical>
<ZoomWrapper svg="canvas" bounded forwardTransform fixVertical>
{transform => this.renderLayers(transform)}
</CachableZoomWrapper>
</ZoomWrapper>
</svg>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import { zoom, zoomIdentity } from 'd3-zoom';

import { cacheZoomState } from '../actions/app-actions';
import { transformToString } from '../utils/transform-utils';
import { activeLayoutZoomSelector } from '../selectors/zooming';
import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/topology';
import {
activeLayoutZoomStateSelector,
activeLayoutZoomLimitsSelector,
} from '../selectors/zooming';
import {
canvasMarginsSelector,
canvasWidthSelector,
Expand All @@ -19,7 +22,7 @@ import {
import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer';


class CachableZoomWrapper extends React.Component {
class ZoomWrapper extends React.Component {
constructor(props, context) {
super(props, context);

Expand All @@ -46,7 +49,8 @@ class CachableZoomWrapper extends React.Component {
this.svg = select(`svg#${this.props.svg}`);

this.setZoomTriggers(!this.props.disabled);
this.restoreCachedZoom(this.props);
this.updateZoomLimits(this.props);
this.restoreZoomState(this.props);
}

componentWillUnmount() {
Expand All @@ -71,8 +75,9 @@ class CachableZoomWrapper extends React.Component {
this.setZoomTriggers(!nextProps.disabled);
}

this.updateZoomLimits(nextProps);
if (!this.zoomRestored) {
this.restoreCachedZoom(nextProps);
this.restoreZoomState(nextProps);
}
}

Expand Down Expand Up @@ -102,52 +107,46 @@ class CachableZoomWrapper extends React.Component {
// Decides which part of the zoom state is cachable depending
// on the horizontal/vertical degrees of freedom.
cachableState(state = this.state) {
// TODO: Probably shouldn't cache the limits if the layout can
// change a lot. However, before removing them from here, we have
// to make sure we can always get them from the default zooms.
let cachableFields = [
'minTranslateX', 'maxTranslateX',
'minTranslateY', 'maxTranslateY',
'minScale', 'maxScale'
];
if (!this.props.fixHorizontal) {
cachableFields = cachableFields.concat(['scaleX', 'translateX']);
}
if (!this.props.fixVertical) {
cachableFields = cachableFields.concat(['scaleY', 'translateY']);
}
const cachableFields = []
.concat(this.props.fixHorizontal ? [] : ['scaleX', 'translateX'])
.concat(this.props.fixVertical ? [] : ['scaleY', 'translateY']);

return pick(state, cachableFields);
}

cacheZoom() {
this.props.cacheZoomState(fromJS(this.cachableState()));
}

updateZoomLimits(props) {
const zoomLimits = props.layoutZoomLimits.toJS();

this.zoom = this.zoom.scaleExtent([zoomLimits.minScale, zoomLimits.maxScale]);

if (props.bounded) {
this.zoom = this.zoom
// Translation limits are only set if explicitly demanded (currently we are using them
// in the resource view, but not in the graph view, although I think the idea would be
// to use them everywhere).
.translateExtent([
[zoomLimits.minTranslateX, zoomLimits.minTranslateY],
[zoomLimits.maxTranslateX, zoomLimits.maxTranslateY],
])
// This is to ensure that the translation limits are properly
// centered, so that the canvas margins are respected.
.extent([
[props.canvasMargins.left, props.canvasMargins.top],
[props.canvasMargins.left + props.width, props.canvasMargins.top + props.height]
]);
}

this.setState(zoomLimits);
}

// Restore the zooming settings
restoreCachedZoom(props) {
if (!props.layoutZoom.isEmpty()) {
const zoomState = props.layoutZoom.toJS();

// Scaling limits are always set.
this.zoom = this.zoom.scaleExtent([zoomState.minScale, zoomState.maxScale]);

// Translation limits are optional.
if (props.bounded) {
this.zoom = this.zoom
// Translation limits are only set if explicitly demanded (currently we are using them
// in the resource view, but not in the graph view, although I think the idea would be
// to use them everywhere).
.translateExtent([
[zoomState.minTranslateX, zoomState.minTranslateY],
[zoomState.maxTranslateX, zoomState.maxTranslateY],
])
// This is to ensure that the translation limits are properly
// centered, so that the canvas margins are respected.
.extent([
[props.canvasMargins.left, props.canvasMargins.top],
[props.canvasMargins.left + props.width, props.canvasMargins.top + props.height]
]);
}
restoreZoomState(props) {
if (!props.layoutZoomState.isEmpty()) {
const zoomState = props.layoutZoomState.toJS();

// After the limits have been set, update the zoom.
this.svg.call(this.zoom.transform, zoomIdentity
Expand Down Expand Up @@ -181,7 +180,8 @@ function mapStateToProps(state) {
width: canvasWidthSelector(state),
height: canvasHeightSelector(state),
canvasMargins: canvasMarginsSelector(state),
layoutZoom: activeLayoutZoomSelector(state),
layoutZoomState: activeLayoutZoomStateSelector(state),
layoutZoomLimits: activeLayoutZoomLimitsSelector(state),
layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)),
forceRelayout: state.get('forceRelayout'),
};
Expand All @@ -191,4 +191,4 @@ function mapStateToProps(state) {
export default connect(
mapStateToProps,
{ cacheZoomState }
)(CachableZoomWrapper);
)(ZoomWrapper);
61 changes: 44 additions & 17 deletions client/app/scripts/selectors/graph-view/default-zoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,75 @@ import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from
import { graphNodesSelector } from './graph';


// Compute the default zoom settings for the given graph.
export const graphDefaultZoomSelector = createSelector(
const graphBoundingRectangleSelector = createSelector(
[
graphNodesSelector,
canvasMarginsSelector,
canvasWidthSelector,
canvasHeightSelector,
],
(graphNodes, canvasMargins, width, height) => {
if (graphNodes.size === 0) {
return makeMap();
}
(graphNodes) => {
if (graphNodes.size === 0) return null;

const xMin = graphNodes.map(n => n.get('x') - NODE_BASE_SIZE).min();
const yMin = graphNodes.map(n => n.get('y') - NODE_BASE_SIZE).min();
const xMax = graphNodes.map(n => n.get('x') + NODE_BASE_SIZE).max();
const yMax = graphNodes.map(n => n.get('y') + NODE_BASE_SIZE).max();

return makeMap({ xMin, yMin, xMax, yMax });
}
);

// Max scale limit will always be such that a node covers 1/5 of the viewport.
const maxScaleSelector = createSelector(
[
canvasWidthSelector,
canvasHeightSelector,
],
(width, height) => Math.min(width, height) / NODE_BASE_SIZE / 5
);

// Compute the default zoom settings for the given graph.
export const graphDefaultZoomSelector = createSelector(
[
graphBoundingRectangleSelector,
canvasMarginsSelector,
canvasWidthSelector,
canvasHeightSelector,
maxScaleSelector,
],
(boundingRectangle, canvasMargins, width, height, maxScale) => {
if (!boundingRectangle) return makeMap();

const { xMin, xMax, yMin, yMax } = boundingRectangle.toJS();
const xFactor = width / (xMax - xMin);
const yFactor = height / (yMax - yMin);

// Maximal allowed zoom will always be such that a node covers 1/5 of the viewport.
const maxScale = Math.min(width, height) / NODE_BASE_SIZE / 5;

// Initial zoom is such that the graph covers 90% of either the viewport,
// or one half of maximal zoom constraint, whichever is smaller.
const scale = Math.min(xFactor, yFactor, maxScale / 2) * 0.9;

// Finally, we always allow zooming out exactly 5x compared to the initial zoom.
const minScale = scale / 5;

// This translation puts the graph in the center of the viewport, respecting the margins.
const translateX = ((width - ((xMax + xMin) * scale)) / 2) + canvasMargins.left;
const translateY = ((height - ((yMax + yMin) * scale)) / 2) + canvasMargins.top;

return makeMap({
translateX,
translateY,
minScale,
maxScale,
scaleX: scale,
scaleY: scale,
});
}
);

export const graphZoomLimitsSelector = createSelector(
[
graphDefaultZoomSelector,
maxScaleSelector,
],
(defaultZoom, maxScale) => {
if (defaultZoom.isEmpty()) return makeMap({ minScale: 1, maxScale: 1 });

// We always allow zooming out exactly 5x compared to the initial zoom.
const minScale = defaultZoom.get('scaleX') / 5;

return makeMap({ minScale, maxScale });
}
);
6 changes: 3 additions & 3 deletions client/app/scripts/selectors/graph-view/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fromJS, Set as makeSet, List as makeList } from 'immutable';

import { NODE_BASE_SIZE } from '../../constants/styles';
import { graphNodesSelector, graphEdgesSelector } from './graph';
import { activeLayoutZoomSelector } from '../zooming';
import { activeLayoutZoomStateSelector } from '../zooming';
import {
canvasCircularExpanseSelector,
canvasDetailsHorizontalCenterSelector,
Expand All @@ -25,7 +25,7 @@ const translationToViewportCenterSelector = createSelector(
[
canvasDetailsHorizontalCenterSelector,
canvasDetailsVerticalCenterSelector,
activeLayoutZoomSelector,
activeLayoutZoomStateSelector,
],
(centerX, centerY, zoomState) => {
const { scaleX, scaleY, translateX, translateY } = zoomState.toJS();
Expand Down Expand Up @@ -76,7 +76,7 @@ const focusedNodesIdsSelector = createSelector(
const circularLayoutScalarsSelector = createSelector(
[
// TODO: Fix this.
state => activeLayoutZoomSelector(state).get('scaleX'),
state => activeLayoutZoomStateSelector(state).get('scaleX'),
state => focusedNodesIdsSelector(state).length - 1,
canvasCircularExpanseSelector,
],
Expand Down
Loading

0 comments on commit b26f5fe

Please sign in to comment.