diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index b2c2d3ab7..59023a745 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -12,15 +12,16 @@ const Node = React.createClass({ render: function() { const props = this.props; - const scale = this.props.scale; + const nodeScale = props.focused ? props.selectedNodeScale : props.nodeScale; + const zoomScale = this.props.zoomScale; let scaleFactor = 1; if (props.focused) { - scaleFactor = 1.25; + scaleFactor = 1.25 / zoomScale; } else if (props.blurred) { scaleFactor = 0.75; } - const labelOffsetY = 18; - const subLabelOffsetY = labelOffsetY + 17; + let labelOffsetY = 18; + let subLabelOffsetY = 35; const isPseudo = !!this.props.pseudo; const color = isPseudo ? '' : this.getNodeColor(this.props.rank); const onMouseEnter = this.handleMouseEnter; @@ -28,9 +29,17 @@ const Node = React.createClass({ const onMouseClick = this.handleMouseClick; const classNames = ['node']; const animConfig = [80, 20]; // stiffness, bounce - const label = this.ellipsis(props.label, 14, scale(4 * scaleFactor)); - const subLabel = this.ellipsis(props.subLabel, 12, scale(4 * scaleFactor)); + const label = this.ellipsis(props.label, 14, nodeScale(4 * scaleFactor)); + const subLabel = this.ellipsis(props.subLabel, 12, nodeScale(4 * scaleFactor)); + let labelFontSize = 14; + let subLabelFontSize = 12; + if (props.focused) { + labelFontSize /= zoomScale; + subLabelFontSize /= zoomScale; + labelOffsetY /= zoomScale; + subLabelOffsetY /= zoomScale; + } if (this.props.highlighted) { classNames.push('highlighted'); } @@ -46,21 +55,27 @@ const Node = React.createClass({ {function(interpolated) { const transform = `translate(${interpolated.x},${interpolated.y})`; return ( - {props.highlighted && } - - - - + {props.highlighted && } + + + + {label} - + {subLabel} diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index d8647cd77..4c5507854 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -4,8 +4,6 @@ const debug = require('debug')('scope:nodes-chart'); const React = require('react'); const makeMap = require('immutable').Map; const timely = require('timely'); -const Motion = require('react-motion').Motion; -const spring = require('react-motion').spring; const AppActions = require('../actions/app-actions'); const AppStore = require('../stores/app-store'); @@ -32,12 +30,11 @@ const NodesChart = React.createClass({ return { nodes: makeMap(), edges: makeMap(), - nodeScale: d3.scale.linear(), - shiftTranslate: [0, 0], panTranslate: [0, 0], scale: 1, + nodeScale: d3.scale.linear(), + selectedNodeScale: d3.scale.linear(), hasZoomed: false, - autoShifted: false, maxNodesExceeded: false }; }, @@ -63,7 +60,6 @@ const NodesChart = React.createClass({ // wipe node states when showing different topology if (nextProps.topologyId !== this.props.topologyId) { _.assign(state, { - autoShifted: false, nodes: makeMap(), edges: makeMap() }); @@ -93,10 +89,12 @@ const NodesChart = React.createClass({ .on('touchstart.zoom', null); }, - renderGraphNodes: function(nodes, scale) { + renderGraphNodes: function(nodes, nodeScale) { const hasSelectedNode = this.props.selectedNodeId && this.props.nodes.has(this.props.selectedNodeId); const adjacency = hasSelectedNode ? AppStore.getAdjacentNodes(this.props.selectedNodeId) : null; const onNodeClick = this.props.onNodeClick; + const zoomScale = this.state.scale; + const selectedNodeScale = this.state.selectedNodeScale; // highlighter functions const setHighlighted = node => { @@ -142,7 +140,9 @@ const NodesChart = React.createClass({ pseudo={node.get('pseudo')} subLabel={node.get('subLabel')} rank={node.get('rank')} - scale={scale} + selectedNodeScale={selectedNodeScale} + nodeScale={nodeScale} + zoomScale={zoomScale} dx={node.get('x')} dy={node.get('y')} /> @@ -202,42 +202,25 @@ const NodesChart = React.createClass({ const edgeElements = this.renderGraphEdges(this.state.edges, this.state.nodeScale); const scale = this.state.scale; - // only animate shift behavior, not panning - const panTranslate = this.state.panTranslate; - const shiftTranslate = this.state.shiftTranslate; - let translate = panTranslate; - let wasShifted = false; - if (shiftTranslate[0] !== panTranslate[0] || shiftTranslate[1] !== panTranslate[1]) { - translate = shiftTranslate; - wasShifted = true; - } + const translate = this.state.panTranslate; + const transform = 'translate(' + translate + ') scale(' + scale + ')'; const svgClassNames = this.state.maxNodesExceeded || nodeElements.size === 0 ? 'hide' : ''; const errorEmpty = this.renderEmptyTopologyError(AppStore.isTopologyEmpty()); const errorMaxNodesExceeded = this.renderMaxNodesError(this.state.maxNodesExceeded); - const motionConfig = [80, 20]; return (
{errorEmpty} {errorMaxNodesExceeded} - - {function(interpolated) { - const interpolatedTranslate = wasShifted ? [interpolated.x, interpolated.y] : panTranslate; - const transform = 'translate(' + interpolatedTranslate + ')' + - ' scale(' + scale + ')'; - return ( - - - {edgeElements} - - - {nodeElements} - - - ); - }} - + + + {edgeElements} + + + {nodeElements} + +
); @@ -309,10 +292,12 @@ const NodesChart = React.createClass({ } }); - // shift center node a bit - const nodeScale = state.nodeScale; - const centerX = selectedLayoutNode.get('px') + nodeScale(1); - const centerY = selectedLayoutNode.get('py') + nodeScale(1); + // move origin node to center of viewport + const zoomScale = state.scale; + const detailsWidth = 420; + const translate = state.panTranslate; + const centerX = (-translate[0] + (props.width + MARGINS.left - detailsWidth) / 2) / zoomScale; + const centerY = (-translate[1] + (props.height + MARGINS.top) / 2) / zoomScale; stateNodes = stateNodes.mergeIn([props.selectedNodeId], { x: centerX, y: centerY @@ -321,7 +306,7 @@ const NodesChart = React.createClass({ // circle layout for adjacent nodes const adjacentCount = adjacentLayoutNodeIds.length; const density = radiusDensity(adjacentCount); - const radius = Math.min(props.width, props.height) / density; + const radius = Math.min(props.width, props.height) / density / zoomScale; const offsetAngle = Math.PI / 4; stateNodes = stateNodes.map((node) => { @@ -352,53 +337,13 @@ const NodesChart = React.createClass({ return edge; }); - // shift canvas selected node out of view if it has not been shifted already - let autoShifted = this.state.autoShifted; - const shiftTranslate = state.shiftTranslate; - - if (!autoShifted) { - const visibleWidth = Math.max(props.width - props.detailsWidth, 0); - const offsetX = shiftTranslate[0]; - // normalize graph coordinates by zoomScale - const zoomScale = state.scale; - const outerRadius = radius + this.state.nodeScale(1.5); - if (2 * outerRadius * zoomScale > props.width) { - // radius too big, centering center node on canvas - shiftTranslate[0] = -(centerX * zoomScale - (props.width + MARGINS.left) / 2); - } else if (offsetX + (centerX + outerRadius) * zoomScale > visibleWidth) { - // shift left if blocked by details - const shift = (centerX + outerRadius) * zoomScale - visibleWidth; - shiftTranslate[0] = -shift; - } else if (offsetX + (centerX - outerRadius) * zoomScale < 0) { - // shift right if off canvas - const shift = offsetX - offsetX + (centerX - outerRadius) * zoomScale; - shiftTranslate[0] = -shift; - } - const offsetY = shiftTranslate[1]; - if (2 * outerRadius * zoomScale > props.height) { - // radius too big, centering center node on canvas - shiftTranslate[1] = -(centerY * zoomScale - (props.height + MARGINS.top) / 2); - } else if (offsetY + (centerY + outerRadius) * zoomScale > props.height) { - // shift up if past bottom - const shift = (centerY + outerRadius) * zoomScale - props.height; - shiftTranslate[1] = -shift; - } else if (offsetY + (centerY - outerRadius) * zoomScale - props.topMargin < 0) { - // shift down if off canvas - const shift = offsetY - offsetY + (centerY - outerRadius) * zoomScale - props.topMargin; - shiftTranslate[1] = -shift; - } - // debug('shift', centerX, centerY, outerRadius, shiftTranslate); - - // saving translate in d3's panning cache - this.zoom.translate(shiftTranslate); - autoShifted = true; - } + // auto-scale node size for selected nodes + const selectedNodeScale = this.getNodeScale(props); return { - autoShifted: autoShifted, + selectedNodeScale, edges: stateEdges, - nodes: stateNodes, - shiftTranslate: shiftTranslate + nodes: stateNodes }; }, @@ -407,16 +352,16 @@ const NodesChart = React.createClass({ handleMouseClick: function() { if (!this.isZooming) { AppActions.clickCloseDetails(); - // allow shifts again - this.setState({ - autoShifted: false - }); } else { this.isZooming = false; } }, restoreLayout: function(state) { + // undo any pan/zooming that might have happened + this.zoom.scale(state.scale); + this.zoom.translate(state.panTranslate); + const nodes = state.nodes.map(node => { return node.merge({ x: node.get('px'), @@ -431,7 +376,7 @@ const NodesChart = React.createClass({ return edge; }); - return {edges, nodes}; + return { edges, nodes}; }, updateGraphState: function(props, state) { @@ -446,11 +391,8 @@ const NodesChart = React.createClass({ let stateNodes = this.initNodes(props.nodes, state.nodes); let stateEdges = this.initEdges(props.nodes, stateNodes); + const nodeScale = this.getNodeScale(props); - const expanse = Math.min(props.height, props.width); - const nodeSize = expanse / 3; // single node should fill a third of the screen - const normalizedNodeSize = nodeSize / Math.sqrt(n); // assuming rectangular layout - const nodeScale = this.state.nodeScale.range([0, normalizedNodeSize]); const options = { width: props.width, height: props.height, @@ -497,22 +439,31 @@ const NodesChart = React.createClass({ return { nodes: stateNodes, edges: stateEdges, - nodeScale: nodeScale, scale: zoomScale, + nodeScale: nodeScale, maxNodesExceeded: false }; }, + getNodeScale: function(props) { + const expanse = Math.min(props.height, props.width); + const nodeSize = expanse / 3; // single node should fill a third of the screen + const maxNodeSize = expanse / 10; + const normalizedNodeSize = Math.min(nodeSize / Math.sqrt(props.nodes.size), maxNodeSize); + return this.state.nodeScale.copy().range([0, normalizedNodeSize]); + }, + zoomed: function() { // debug('zoomed', d3.event.scale, d3.event.translate); this.isZooming = true; - this.setState({ - autoShifted: false, - hasZoomed: true, - panTranslate: d3.event.translate.slice(), - shiftTranslate: d3.event.translate.slice(), - scale: d3.event.scale - }); + // dont pan while node is selected + if (!this.props.selectedNodeId) { + this.setState({ + hasZoomed: true, + panTranslate: d3.event.translate.slice(), + scale: d3.event.scale + }); + } } }); diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js index 3e913aae7..60cb2f932 100644 --- a/client/app/scripts/components/nodes.js +++ b/client/app/scripts/components/nodes.js @@ -43,10 +43,10 @@ const Nodes = React.createClass({ }, setDimensions: function() { - this.setState({ - height: window.innerHeight - navbarHeight - marginTop, - width: window.innerWidth - }); + const width = window.innerWidth; + const height = window.innerHeight - navbarHeight - marginTop; + + this.setState({height, width}); } }); diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 8c41679c8..d174c4f89 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -217,7 +217,6 @@ h2 { } text { - font-size: 14px; font-family: Roboto; fill: @text-secondary-color; text-shadow: 0 2px 0 @white, 2px 0 0 @white, 0 -2px 0 @white, -2px 0 0 @white; @@ -227,7 +226,6 @@ h2 { } &.node-sublabel { - font-size: 12px; fill: @text-secondary-color; } }