Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas zoom control #2513

Merged
merged 6 commits into from
May 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions client/app/scripts/charts/nodes-chart.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';

import Logo from '../components/logo';
import NodesChartElements from './nodes-chart-elements';
import ZoomWrapper from '../components/zoom-wrapper';
import ZoomableCanvas from '../components/zoomable-canvas';
import { clickBackground } from '../actions/app-actions';
import {
graphZoomLimitsSelector,
graphZoomStateSelector,
} from '../selectors/graph-view/zoom';


const EdgeMarkerDefinition = ({ selectedNodeId }) => {
const markerOffset = selectedNodeId ? '35' : '40';
const markerSize = selectedNodeId ? '10' : '30';
Expand Down Expand Up @@ -47,16 +47,14 @@ class NodesChart extends React.Component {
const { selectedNodeId } = this.props;
return (
<div className="nodes-chart">
<svg id="canvas" width="100%" height="100%" onClick={this.handleMouseClick}>
<Logo transform="translate(24,24) scale(0.25)" />
<ZoomableCanvas
onClick={this.handleMouseClick}
zoomLimitsSelector={graphZoomLimitsSelector}
zoomStateSelector={graphZoomStateSelector}
disabled={selectedNodeId}>
<EdgeMarkerDefinition selectedNodeId={selectedNodeId} />
<ZoomWrapper
svg="canvas" disabled={selectedNodeId}
zoomLimitsSelector={graphZoomLimitsSelector}
zoomStateSelector={graphZoomStateSelector}>
<NodesChartElements />
</ZoomWrapper>
</svg>
<NodesChartElements />
</ZoomableCanvas>
</div>
);
}
Expand Down
18 changes: 7 additions & 11 deletions client/app/scripts/components/nodes-resources.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';

import Logo from './logo';
import ZoomWrapper from './zoom-wrapper';
import ZoomableCanvas from './zoomable-canvas';
import NodesResourcesLayer from './nodes-resources/node-resources-layer';
import { layersTopologyIdsSelector } from '../selectors/resource-view/layout';
import {
Expand All @@ -26,15 +25,12 @@ class NodesResources extends React.Component {
render() {
return (
<div className="nodes-resources">
<svg id="canvas" width="100%" height="100%">
<Logo transform="translate(24,24) scale(0.25)" />
<ZoomWrapper
svg="canvas" bounded forwardTransform fixVertical
zoomLimitsSelector={resourcesZoomLimitsSelector}
zoomStateSelector={resourcesZoomStateSelector}>
{transform => this.renderLayers(transform)}
</ZoomWrapper>
</svg>
<ZoomableCanvas
bounded forwardTransform fixVertical
zoomLimitsSelector={resourcesZoomLimitsSelector}
zoomStateSelector={resourcesZoomStateSelector}>
{transform => this.renderLayers(transform)}
</ZoomableCanvas>
</div>
);
}
Expand Down
65 changes: 65 additions & 0 deletions client/app/scripts/components/zoom-control.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import Slider from 'rc-slider';
import { scaleLog } from 'd3-scale';


const SLIDER_STEP = 0.001;
const CLICK_STEP = 0.05;

// Returns a log-scale that maps zoom factors to slider values.
const getSliderScale = ({ minScale, maxScale }) => (
scaleLog()
// Zoom limits may vary between different views.
.domain([minScale, maxScale])
// Taking the unit range for the slider ensures consistency
// of the zoom button steps across different zoom domains.
.range([0, 1])
// This makes sure the input values are always clamped into the valid domain/range.
.clamp(true)
);

export default class ZoomControl extends React.Component {
constructor(props, context) {
super(props, context);

this.handleChange = this.handleChange.bind(this);
this.handleZoomOut = this.handleZoomOut.bind(this);
this.handleZoomIn = this.handleZoomIn.bind(this);
this.getSliderValue = this.getSliderValue.bind(this);
this.toZoomScale = this.toZoomScale.bind(this);
}

handleChange(sliderValue) {
this.props.zoomAction(this.toZoomScale(sliderValue));
}

handleZoomOut() {
this.props.zoomAction(this.toZoomScale(this.getSliderValue() - CLICK_STEP));
}

handleZoomIn() {
this.props.zoomAction(this.toZoomScale(this.getSliderValue() + CLICK_STEP));
}

getSliderValue() {
const toSliderValue = getSliderScale(this.props);
return toSliderValue(this.props.scale);
}

toZoomScale(sliderValue) {
const toSliderValue = getSliderScale(this.props);
return toSliderValue.invert(sliderValue);
}

render() {
const value = this.getSliderValue();

return (
<div className="zoom-control">
<a className="zoom-in" onClick={this.handleZoomIn}><span className="fa fa-plus" /></a>
<Slider value={value} max={1} step={SLIDER_STEP} vertical onChange={this.handleChange} />
<a className="zoom-out" onClick={this.handleZoomOut}><span className="fa fa-minus" /></a>
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { fromJS } from 'immutable';
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 { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming';
Expand All @@ -18,7 +20,7 @@ import {
import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer';


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

Expand All @@ -36,13 +38,15 @@ class ZoomWrapper 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);
}

componentDidMount() {
this.zoomRestored = false;
this.zoom = zoom().on('zoom', this.zoomed);
this.svg = select(`svg#${this.props.svg}`);
this.svg = select('svg#canvas');

this.setZoomTriggers(!this.props.disabled);
this.updateZoomLimits(this.props);
Expand Down Expand Up @@ -77,6 +81,18 @@ class ZoomWrapper 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();
}

render() {
// `forwardTransform` says whether the zoom transform is forwarded to the child
// component. The advantage of that is more control rendering control in the
Expand All @@ -86,8 +102,19 @@ class ZoomWrapper extends React.Component {
const transform = forwardTransform ? '' : transformToString(this.state);

return (
<g className="cachable-zoom-wrapper" transform={transform}>
{forwardTransform ? children(this.state) : children}
<g className="zoomable-canvas">
<svg id="canvas" width="100%" height="100%" onClick={this.props.onClick}>
<Logo transform="translate(24,24) scale(0.25)" />
<g className="zoom-content" transform={transform}>
{forwardTransform ? children(this.state) : children}
</g>
</svg>
{this.canChangeZoom() && <ZoomControl
zoomAction={this.handleZoomControlAction}
minScale={this.state.minScale}
maxScale={this.state.maxScale}
scale={this.state.scaleX}
/>}
</g>
);
}
Expand Down Expand Up @@ -157,8 +184,14 @@ class ZoomWrapper extends React.Component {
}
}

canChangeZoom() {
const { disabled, layoutZoomLimits } = this.props;
const canvasHasContent = !layoutZoomLimits.isEmpty();
return !disabled && canvasHasContent;
}

zoomed() {
if (!this.props.disabled) {
if (this.canChangeZoom()) {
const updatedState = this.cachableState({
scaleX: d3Event.transform.k,
scaleY: d3Event.transform.k,
Expand Down Expand Up @@ -189,4 +222,4 @@ function mapStateToProps(state, props) {
export default connect(
mapStateToProps,
{ cacheZoomState }
)(ZoomWrapper);
)(ZoomableCanvas);
2 changes: 1 addition & 1 deletion client/app/scripts/constants/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const EDGE_WAYPOINTS_CAP = 10;
export const CANVAS_MARGINS = {
[GRAPH_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 150 },
[TABLE_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 30 },
[RESOURCE_VIEW_MODE]: { top: 160, left: 210, right: 40, bottom: 50 },
[RESOURCE_VIEW_MODE]: { top: 140, left: 210, right: 40, bottom: 150 },
};

// Node details table constants
Expand Down
50 changes: 43 additions & 7 deletions client/app/styles/_base.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import '~xterm/dist/xterm.css';
@import '~font-awesome/scss/font-awesome.scss';
@import '~rc-slider/dist/rc-slider.css';

@font-face {
font-family: "Roboto";
Expand Down Expand Up @@ -46,6 +47,16 @@
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.19), 0 6px 10px rgba(0, 0, 0, 0.23);
}

.overlay-wrapper {
background-color: fade-out($background-average-color, 0.1);
border-radius: 4px;
color: $text-tertiary-color;
display: flex;
font-size: 0.7rem;
padding: 5px;
position: absolute;
}

.btn-opacity {
@extend .palable;
opacity: $btn-opacity-default;
Expand Down Expand Up @@ -127,18 +138,13 @@
}

.footer {
padding: 5px;
position: absolute;
@extend .overlay-wrapper;
bottom: 11px;
right: 43px;
color: $text-tertiary-color;
background-color: fade-out($background-average-color, .1);
font-size: 0.7rem;
display: flex;

a {
color: $text-secondary-color;
@extend .btn-opacity;
color: $text-secondary-color;
cursor: pointer;
}

Expand Down Expand Up @@ -1748,6 +1754,36 @@
}
}

//
// Zoom control
//

.zoom-control {
@extend .overlay-wrapper;
align-items: center;
flex-direction: column;
padding: 10px 10px 5px;
bottom: 50px;
right: 40px;

.zoom-in, .zoom-out {
@extend .btn-opacity;
color: $text-secondary-color;
cursor: pointer;
font-size: 150%;
}

.rc-slider {
margin: 10px 0;
height: 60px;

.rc-slider-step { cursor: pointer; }
.rc-slider-track { background-color: $text-tertiary-color; }
.rc-slider-rail { background-color: $border-light-color; }
.rc-slider-handle { border-color: $text-tertiary-color; }
}
}

//
// Debug panel!
//
Expand Down
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"moment": "2.18.1",
"page": "1.7.1",
"prop-types": "^15.5.8",
"rc-slider": "^7.0.2",
"react": "15.5.4",
"react-addons-perf": "15.4.2",
"react-dom": "15.5.4",
Expand Down
1 change: 1 addition & 0 deletions client/webpack.local.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ module.exports = {
includePaths: [
path.resolve(__dirname, './node_modules/xterm'),
path.resolve(__dirname, './node_modules/font-awesome'),
path.resolve(__dirname, './node_modules/rc-slider'),
]
}
}],
Expand Down
3 changes: 2 additions & 1 deletion client/webpack.production.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ module.exports = {
minimize: true,
includePaths: [
path.resolve(__dirname, './node_modules/xterm'),
path.resolve(__dirname, './node_modules/font-awesome')
path.resolve(__dirname, './node_modules/font-awesome'),
path.resolve(__dirname, './node_modules/rc-slider'),
]
}
}]
Expand Down
Loading