Introduced translation limits to the graph view.

This commit is contained in:
Filip Barl
2017-08-15 18:02:16 +02:00
parent 297e21d42c
commit a51d34f1e5
9 changed files with 96 additions and 41 deletions

View File

@@ -6,10 +6,12 @@ import ZoomableCanvas from '../components/zoomable-canvas';
import { transformToString } from '../utils/transform-utils';
import { clickBackground } from '../actions/app-actions';
import {
graphZoomLimitsSelector,
graphLimitsSelector,
graphZoomStateSelector,
} from '../selectors/graph-view/zoom';
import { CONTENT_INCLUDED } from '../constants/naming';
const EdgeMarkerDefinition = ({ selectedNodeId }) => {
const markerOffset = selectedNodeId ? '35' : '40';
@@ -58,7 +60,8 @@ class NodesChart extends React.Component {
<div className="nodes-chart">
<ZoomableCanvas
onClick={this.handleMouseClick}
zoomLimitsSelector={graphZoomLimitsSelector}
boundContent={CONTENT_INCLUDED}
limitsSelector={graphLimitsSelector}
zoomStateSelector={graphZoomStateSelector}
disabled={this.props.selectedNodeId}>
{transform => this.renderContent(transform)}

View File

@@ -5,11 +5,13 @@ import ZoomableCanvas from './zoomable-canvas';
import NodesResourcesLayer from './nodes-resources/node-resources-layer';
import { layersTopologyIdsSelector } from '../selectors/resource-view/layout';
import {
resourcesZoomLimitsSelector,
resourcesLimitsSelector,
resourcesZoomStateSelector,
} from '../selectors/resource-view/zoom';
import { clickBackground } from '../actions/app-actions';
import { CONTENT_COVERING } from '../constants/naming';
class NodesResources extends React.Component {
constructor(props, context) {
@@ -39,9 +41,9 @@ class NodesResources extends React.Component {
return (
<div className="nodes-resources">
<ZoomableCanvas
bounded fixVertical
onClick={this.handleMouseClick}
zoomLimitsSelector={resourcesZoomLimitsSelector}
fixVertical boundContent={CONTENT_COVERING}
limitsSelector={resourcesLimitsSelector}
zoomStateSelector={resourcesZoomStateSelector}>
{transform => this.renderLayers(transform)}
</ZoomableCanvas>

View File

@@ -168,7 +168,7 @@ class TimeTravelTimeline extends React.Component {
}
handleZoom(ev) {
let durationPerPixel = scaleDuration(this.state.durationPerPixel, zoomFactor(ev));
let durationPerPixel = scaleDuration(this.state.durationPerPixel, 1 / zoomFactor(ev));
if (durationPerPixel > MAX_DURATION_PER_PX) durationPerPixel = MAX_DURATION_PER_PX;
if (durationPerPixel < MIN_DURATION_PER_PX) durationPerPixel = MIN_DURATION_PER_PX;

View File

@@ -20,6 +20,7 @@ import {
} from '../selectors/canvas';
import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer';
import { CONTENT_INCLUDED, CONTENT_COVERING } from '../constants/naming';
class ZoomableCanvas extends React.Component {
@@ -28,10 +29,10 @@ class ZoomableCanvas extends React.Component {
this.state = {
isPanning: false,
minTranslateX: 0,
maxTranslateX: 0,
minTranslateY: 0,
maxTranslateY: 0,
contentMinX: 0,
contentMaxX: 0,
contentMinY: 0,
contentMaxY: 0,
translateX: 0,
translateY: 0,
minScale: 1,
@@ -86,13 +87,14 @@ class ZoomableCanvas extends React.Component {
}
handleZoomControlAction(scale) {
// Update the canvas scale (not touching the translation).
// Get the center of the SVG and zoom around it.
const { top, bottom, left, right } = this.svg.node().getBoundingClientRect();
const centerOfCanvas = {
x: (left + right) / 2,
y: (top + bottom) / 2,
};
this.zoomAtPosition(centerOfCanvas, scale / this.state.scaleX);
// Zoom factor diff is obtained by dividing the new zoom scale with the old one.
this.zoomAtPositionByFactor(centerOfCanvas, scale / this.state.scaleX);
}
render() {
@@ -131,7 +133,7 @@ class ZoomableCanvas extends React.Component {
}
updateZoomLimits(props) {
this.setState(props.layoutZoomLimits.toJS());
this.setState(props.layoutLimits.toJS());
}
// Restore the zooming settings
@@ -146,8 +148,8 @@ class ZoomableCanvas extends React.Component {
}
canChangeZoom() {
const { disabled, layoutZoomLimits } = this.props;
const canvasHasContent = !layoutZoomLimits.isEmpty();
const { disabled, layoutLimits } = this.props;
const canvasHasContent = !layoutLimits.isEmpty();
return !disabled && canvasHasContent;
}
@@ -161,6 +163,7 @@ class ZoomableCanvas extends React.Component {
handlePan() {
let state = { ...this.state };
// Apply the translation respecting the boundaries.
state = this.clampedTranslation({ ...state,
translateX: this.state.translateX + d3Event.dx,
translateY: this.state.translateY + d3Event.dy,
@@ -170,40 +173,70 @@ class ZoomableCanvas extends React.Component {
handleZoom(ev) {
if (this.canChangeZoom()) {
// Get the exact mouse cursor position in the SVG and zoom around it.
const { top, left } = this.svg.node().getBoundingClientRect();
const mousePosition = {
x: ev.clientX - left,
y: ev.clientY - top,
};
this.zoomAtPosition(mousePosition, 1 / zoomFactor(ev));
this.zoomAtPositionByFactor(mousePosition, zoomFactor(ev));
}
}
clampedTranslation(state) {
const { width, height, canvasMargins, bounded, layoutZoomLimits } = this.props;
const { maxTranslateX, minTranslateX, maxTranslateY, minTranslateY } = layoutZoomLimits.toJS();
const { width, height, canvasMargins, boundContent, layoutLimits } = this.props;
const { contentMinX, contentMaxX, contentMinY, contentMaxY } = layoutLimits.toJS();
if (bounded) {
const contentMinPoint = applyTransform(state, { x: minTranslateX, y: minTranslateY });
const contentMaxPoint = applyTransform(state, { x: maxTranslateX, y: maxTranslateY });
const viewportMinPoint = { x: canvasMargins.left, y: canvasMargins.top };
const viewportMaxPoint = { x: canvasMargins.left + width, y: canvasMargins.top + height };
if (boundContent) {
// If the content is required to be bounded in any way, the translation will
// be adjusted so that certain constraints between the viewport and displayed
// content bounding box are met.
const viewportMin = { x: canvasMargins.left, y: canvasMargins.top };
const viewportMax = { x: canvasMargins.left + width, y: canvasMargins.top + height };
const contentMin = applyTransform(state, { x: contentMinX, y: contentMinY });
const contentMax = applyTransform(state, { x: contentMaxX, y: contentMaxY });
state.translateX += Math.max(0, viewportMaxPoint.x - contentMaxPoint.x);
state.translateX += Math.min(0, viewportMinPoint.x - contentMinPoint.x);
state.translateY += Math.max(0, viewportMaxPoint.y - contentMaxPoint.y);
state.translateY += Math.min(0, viewportMinPoint.y - contentMinPoint.y);
switch (boundContent) {
case CONTENT_COVERING:
// These lines will adjust the translation by 'minimal effort' in
// such a way that the content always FULLY covers the viewport,
// i.e. that the viewport rectangle is always fully contained in
// the content bounding box rectangle - the assumption made here
// is that that can always be done.
state.translateX += Math.max(0, viewportMax.x - contentMax.x);
state.translateX -= Math.max(0, contentMin.x - viewportMin.x);
state.translateY += Math.max(0, viewportMax.y - contentMax.y);
state.translateY -= Math.max(0, contentMin.y - viewportMin.y);
break;
case CONTENT_INCLUDED:
// These lines will adjust the translation by 'minimal effort' in
// such a way that the content is always at least PARTLY contained
// within the viewport, i.e. that the intersection between the
// viewport and the content bounding box always exists.
state.translateX -= Math.max(0, contentMin.x - viewportMax.x);
state.translateX += Math.max(0, viewportMin.x - contentMax.x);
state.translateY -= Math.max(0, contentMin.y - viewportMax.y);
state.translateY += Math.max(0, viewportMin.y - contentMax.y);
break;
default:
break;
}
}
return state;
}
zoomAtPosition(position, factor) {
zoomAtPositionByFactor(position, factor) {
// Update the scales by the given factor, respecting the zoom limits.
const { minScale, maxScale } = this.state;
const scaleX = clamp(this.state.scaleX * factor, minScale, maxScale);
const scaleY = clamp(this.state.scaleY * factor, minScale, maxScale);
let state = { ...this.state, scaleX, scaleY };
// Get the position in the coordinates before the transition and use it
// to adjust the translation part of the new transition (respecting the
// translation limits). Adapted from:
// https://github.com/d3/d3-zoom/blob/807f02c7a5fe496fbd08cc3417b62905a8ce95fa/src/zoom.js#L251
const inversePosition = inverseTransform(this.state, position);
state = this.clampedTranslation({ ...state,
translateX: position.x - (inversePosition.x * scaleX),
@@ -226,7 +259,7 @@ function mapStateToProps(state, props) {
height: canvasHeightSelector(state),
canvasMargins: canvasMarginsSelector(state),
layoutZoomState: props.zoomStateSelector(state),
layoutZoomLimits: props.zoomLimitsSelector(state),
layoutLimits: props.limitsSelector(state),
layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)),
forceRelayout: state.get('forceRelayout'),
};

View File

@@ -16,3 +16,6 @@ export const HIGHLIGHTED_EDGES_LAYER = 'highlighted-edges';
export const HIGHLIGHTED_NODES_LAYER = 'highlighted-nodes';
export const HOVERED_EDGES_LAYER = 'hovered-edges';
export const HOVERED_NODES_LAYER = 'hovered-nodes';
export const CONTENT_INCLUDED = 'content-included';
export const CONTENT_COVERING = 'content-covering';

View File

@@ -38,7 +38,7 @@ export const NODE_BASE_SIZE = 100;
export const EDGE_WAYPOINTS_CAP = 10;
export const CANVAS_MARGINS = {
[GRAPH_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 150 },
[GRAPH_VIEW_MODE]: { top: 160, left: 80, right: 80, bottom: 150 },
[TABLE_VIEW_MODE]: { top: 220, left: 40, right: 40, bottom: 30 },
[RESOURCE_VIEW_MODE]: { top: 140, left: 210, right: 40, bottom: 150 },
};

View File

@@ -6,7 +6,7 @@ import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from
import { activeLayoutCachedZoomSelector } from '../zooming';
import { graphNodesSelector } from './graph';
// Nodes in the layout are always kept between 1px and 200px big.
// Nodes in the layout are always kept between 3px and 200px big.
const MAX_SCALE = 200 / NODE_BASE_SIZE;
const MIN_SCALE = 3 / NODE_BASE_SIZE;
@@ -58,10 +58,24 @@ export const graphDefaultZoomSelector = createSelector(
}
);
// NOTE: This constant is made into a selector to fit
// props requirements for <ZoomableCanvas /> component.
export const graphZoomLimitsSelector = createSelector(
[], () => makeMap({ minScale: MIN_SCALE, maxScale: MAX_SCALE })
export const graphLimitsSelector = createSelector(
[
graphBoundingRectangleSelector,
],
(boundingRectangle) => {
if (!boundingRectangle) return makeMap();
const { xMin, xMax, yMin, yMax } = boundingRectangle.toJS();
return makeMap({
minScale: MIN_SCALE,
maxScale: MAX_SCALE,
contentMinX: xMin,
contentMaxX: xMax,
contentMinY: yMin,
contentMaxY: yMax,
});
}
);
export const graphZoomStateSelector = createSelector(

View File

@@ -66,7 +66,7 @@ export const resourcesDefaultZoomSelector = createSelector(
}
);
export const resourcesZoomLimitsSelector = createSelector(
export const resourcesLimitsSelector = createSelector(
[
resourcesDefaultZoomSelector,
resourceNodesBoundingRectangleSelector,
@@ -83,10 +83,10 @@ export const resourcesZoomLimitsSelector = createSelector(
maxScale: width / minNodeWidth,
// Minimal zoom is equivalent to the initial one, where the whole layout matches the canvas.
minScale: defaultZoom.get('scaleX'),
minTranslateX: xMin,
maxTranslateX: xMax,
minTranslateY: yMin,
maxTranslateY: yMax,
contentMinX: xMin,
contentMaxX: xMax,
contentMinY: yMin,
contentMaxY: yMax,
});
}
);

View File

@@ -7,7 +7,7 @@ function wheelDelta(ev) {
// Only Firefox seems to use the line unit (which we assume to
// be 25px), otherwise the delta is already measured in pixels.
const unitInPixels = (ev.deltaMode === DOM_DELTA_LINE ? 25 : 1);
return ev.deltaY * unitInPixels * ZOOM_SENSITIVITY;
return -ev.deltaY * unitInPixels * ZOOM_SENSITIVITY;
}
export function zoomFactor(ev) {