diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index e200efe1f..59ffb2839 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -3,12 +3,15 @@ import { connect } from 'react-redux'; import NodesChartElements from './nodes-chart-elements'; 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'; @@ -43,17 +46,25 @@ class NodesChart extends React.Component { } } + renderContent(transform) { + return ( + + + + + ); + } + render() { - const { selectedNodeId } = this.props; return (
- - + disabled={this.props.selectedNodeId}> + {transform => this.renderContent(transform)}
); diff --git a/client/app/scripts/components/nodes-resources.js b/client/app/scripts/components/nodes-resources.js index 50f257ab3..8912d29da 100644 --- a/client/app/scripts/components/nodes-resources.js +++ b/client/app/scripts/components/nodes-resources.js @@ -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) { @@ -40,8 +42,8 @@ class NodesResources extends React.Component {
{transform => this.renderLayers(transform)} diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 71fd32229..fc427fbb4 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -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; diff --git a/client/app/scripts/components/zoomable-canvas.js b/client/app/scripts/components/zoomable-canvas.js index 205b4f2c1..2616c966f 100644 --- a/client/app/scripts/components/zoomable-canvas.js +++ b/client/app/scripts/components/zoomable-canvas.js @@ -1,15 +1,17 @@ import React from 'react'; +import classNames from 'classnames'; import { connect } from 'react-redux'; -import { debounce, pick } from 'lodash'; +import { clamp, debounce, pick } from 'lodash'; import { fromJS } from 'immutable'; +import { drag } from 'd3-drag'; import { event as d3Event, select } from 'd3-selection'; -import { zoom, zoomIdentity } from 'd3-zoom'; import Logo from '../components/logo'; import ZoomControl from '../components/zoom-control'; import { cacheZoomState } from '../actions/app-actions'; -import { transformToString } from '../utils/transform-utils'; +import { zoomFactor } from '../utils/zoom-utils'; +import { applyTransform, inverseTransform } from '../utils/transform-utils'; import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming'; import { canvasMarginsSelector, @@ -18,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 { @@ -25,10 +28,11 @@ class ZoomableCanvas extends React.Component { super(props, context); this.state = { - minTranslateX: 0, - maxTranslateX: 0, - minTranslateY: 0, - maxTranslateY: 0, + isPanning: false, + contentMinX: 0, + contentMaxX: 0, + contentMinY: 0, + contentMaxY: 0, translateX: 0, translateY: 0, minScale: 1, @@ -40,27 +44,33 @@ class ZoomableCanvas extends React.Component { this.debouncedCacheZoom = debounce(this.cacheZoom.bind(this), ZOOM_CACHE_DEBOUNCE_INTERVAL); this.handleZoomControlAction = this.handleZoomControlAction.bind(this); this.canChangeZoom = this.canChangeZoom.bind(this); - this.zoomed = this.zoomed.bind(this); + + this.handleZoom = this.handleZoom.bind(this); + this.handlePanStart = this.handlePanStart.bind(this); + this.handlePanEnd = this.handlePanEnd.bind(this); + this.handlePan = this.handlePan.bind(this); } componentDidMount() { + this.svg = select('.zoomable-canvas svg'); + this.drag = drag() + .on('start', this.handlePanStart) + .on('end', this.handlePanEnd) + .on('drag', this.handlePan); + this.svg.call(this.drag); + this.zoomRestored = false; - this.zoom = zoom().on('zoom', this.zoomed); - this.svg = select('svg#canvas'); - this.setZoomTriggers(!this.props.disabled); this.updateZoomLimits(this.props); this.restoreZoomState(this.props); } componentWillUnmount() { - this.setZoomTriggers(false); this.debouncedCacheZoom.cancel(); } componentWillReceiveProps(nextProps) { const layoutChanged = nextProps.layoutId !== this.props.layoutId; - const disabledChanged = nextProps.disabled !== this.props.disabled; // If the layout has changed (either active topology or its options) or // relayouting has been requested, stop pending zoom caching event and @@ -70,11 +80,6 @@ class ZoomableCanvas extends React.Component { this.zoomRestored = false; } - // If the zooming has been enabled/disabled, update its triggers. - if (disabledChanged) { - this.setZoomTriggers(!nextProps.disabled); - } - this.updateZoomLimits(nextProps); if (!this.zoomRestored) { this.restoreZoomState(nextProps); @@ -82,31 +87,25 @@ class ZoomableCanvas extends React.Component { } handleZoomControlAction(scale) { - // Update the canvas scale (not touching the translation). - this.svg.call(this.zoom.scaleTo, scale); - - // Update the scale state and propagate to the global cache. - this.setState(this.cachableState({ - scaleX: scale, - scaleY: scale, - })); - this.debouncedCacheZoom(); + // 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, + }; + // Zoom factor diff is obtained by dividing the new zoom scale with the old one. + this.zoomAtPositionByFactor(centerOfCanvas, scale / this.state.scaleX); } render() { - // `forwardTransform` says whether the zoom transform is forwarded to the child - // component. The advantage of that is more control rendering control in the - // children, while the disadvantage is that it's slower, as all the children - // get updated on every zoom/pan action. - const { children, forwardTransform } = this.props; - const transform = forwardTransform ? '' : transformToString(this.state); + const className = classNames({ panning: this.state.isPanning }); return (
- + - - {forwardTransform ? children(this.state) : children} + + {this.props.children(this.state)} {this.canChangeZoom() && 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( diff --git a/client/app/scripts/selectors/resource-view/zoom.js b/client/app/scripts/selectors/resource-view/zoom.js index 39b43d55d..41c1bb8e4 100644 --- a/client/app/scripts/selectors/resource-view/zoom.js +++ b/client/app/scripts/selectors/resource-view/zoom.js @@ -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, }); } ); diff --git a/client/app/scripts/utils/transform-utils.js b/client/app/scripts/utils/transform-utils.js index 21160ae79..cc1834dd6 100644 --- a/client/app/scripts/utils/transform-utils.js +++ b/client/app/scripts/utils/transform-utils.js @@ -4,13 +4,27 @@ const applyTranslateY = ({ scaleY = 1, translateY = 0 }, y) => (y * scaleY) + tr const applyScaleX = ({ scaleX = 1 }, width) => width * scaleX; const applyScaleY = ({ scaleY = 1 }, height) => height * scaleY; -export const applyTransform = (transform, { width, height, x, y }) => ({ +export const applyTransform = (transform, { width = 0, height = 0, x, y }) => ({ x: applyTranslateX(transform, x), y: applyTranslateY(transform, y), width: applyScaleX(transform, width), height: applyScaleY(transform, height), }); + +const inverseTranslateX = ({ scaleX = 1, translateX = 0 }, x) => (x - translateX) / scaleX; +const inverseTranslateY = ({ scaleY = 1, translateY = 0 }, y) => (y - translateY) / scaleY; +const inverseScaleX = ({ scaleX = 1 }, width) => width / scaleX; +const inverseScaleY = ({ scaleY = 1 }, height) => height / scaleY; + +export const inverseTransform = (transform, { width = 0, height = 0, x, y }) => ({ + x: inverseTranslateX(transform, x), + y: inverseTranslateY(transform, y), + width: inverseScaleX(transform, width), + height: inverseScaleY(transform, height), +}); + + export const transformToString = ({ translateX = 0, translateY = 0, scaleX = 1, scaleY = 1 }) => ( `translate(${translateX},${translateY}) scale(${scaleX},${scaleY})` ); diff --git a/client/app/scripts/utils/zoom-utils.js b/client/app/scripts/utils/zoom-utils.js index 043d59a65..1b58a99ae 100644 --- a/client/app/scripts/utils/zoom-utils.js +++ b/client/app/scripts/utils/zoom-utils.js @@ -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) { diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index a859eb655..15493c682 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -63,6 +63,14 @@ a { cursor: -webkit-grabbing; } +.fully-pannable { + width: 100%; + height: 100%; + @extend .grabbable; + + &.panning { @extend .grabbing; } +} + .shadow-2 { box-shadow: 0 3px 10px rgba(0, 0, 0, 0.16), 0 3px 10px rgba(0, 0, 0, 0.23); } @@ -284,15 +292,11 @@ a { height: $timeline-height; svg { - @extend .grabbable; + @extend .fully-pannable; background-color: rgba(255, 255, 255, 0.85); box-shadow: inset 0 0 7px #aaa; pointer-events: all; margin: 0 7px; - width: 100%; - height: 100%; - - &.panning { @extend .grabbing; } .available-range { fill: #888; @@ -358,6 +362,9 @@ a { } } +.zoomable-canvas svg { + @extend .fully-pannable; +} .topologies { margin: 0 4px; diff --git a/client/package.json b/client/package.json index 00b4abf3a..27690be6a 100644 --- a/client/package.json +++ b/client/package.json @@ -18,8 +18,6 @@ "d3-selection": "1.0.5", "d3-shape": "1.0.6", "d3-time-format": "2.0.5", - "d3-transition": "1.0.4", - "d3-zoom": "1.1.4", "dagre": "0.7.4", "debug": "2.6.6", "filesize": "3.5.9",