From 6e3883d07e47ea7c382ad39393fc932e9fe246ff Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 16 Sep 2015 15:26:02 +0200 Subject: [PATCH 1/3] Dont shift canvas after it has been autoshifted --- client/app/scripts/charts/nodes-chart.js | 82 ++++++++++++------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 2d7115819..08eb4dba2 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -34,6 +34,7 @@ const NodesChart = React.createClass({ panTranslate: [0, 0], scale: 1, hasZoomed: false, + autoShifted: false, maxNodesExceeded: false }; }, @@ -72,7 +73,7 @@ const NodesChart = React.createClass({ _.assign(state, this.restoreLayout(state)); } if (nextProps.selectedNodeId) { - this.centerSelectedNode(nextProps, state); + _.assign(state, this.centerSelectedNode(nextProps, state)); } this.setState(state); @@ -90,18 +91,6 @@ const NodesChart = React.createClass({ .on('touchstart.zoom', null); }, - getTopologyFingerprint: function(topology) { - const fingerprint = []; - - _.each(topology, function(node) { - fingerprint.push(node.id); - if (node.adjacency) { - fingerprint.push(node.adjacency.join(',')); - } - }); - return fingerprint.join(';'); - }, - renderGraphNodes: function(nodes, scale) { const hasSelectedNode = this.props.selectedNodeId && this.props.nodes.has(this.props.selectedNodeId); const adjacency = hasSelectedNode ? AppStore.getAdjacentNodes(this.props.selectedNodeId) : null; @@ -316,38 +305,44 @@ const NodesChart = React.createClass({ } }); - // shift canvas selected node out of view - const visibleWidth = Math.max(props.width - props.detailsWidth, 0); + // shift canvas selected node out of view if it has not been shifted already + let autoShifted = this.state.autoShifted; const translate = state.translate; - const offsetX = translate[0]; - // normalize graph coordinates by zoomScale - const zoomScale = state.scale; - const outerRadius = radius + this.state.nodeScale(2); - if (offsetX + (centerX + outerRadius) * zoomScale > visibleWidth) { - // shift left if blocked by details - const shift = (centerX + outerRadius) * zoomScale - visibleWidth; - translate[0] = -shift; - } else if (offsetX + (centerX - outerRadius) * zoomScale < 0) { - // shift right if off canvas - const shift = offsetX - offsetX + (centerX - outerRadius) * zoomScale; - translate[0] = -shift; - } - const offsetY = translate[1]; - if (offsetY + (centerY + outerRadius) * zoomScale > props.height) { - // shift up if past bottom - const shift = (centerY + outerRadius) * zoomScale - props.height; - translate[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; - translate[1] = -shift; - } - // debug('shift', centerX, centerY, outerRadius, translate); - // saving translate in d3's panning cache - this.zoom.translate(translate); + if (!autoShifted) { + const visibleWidth = Math.max(props.width - props.detailsWidth, 0); + const offsetX = translate[0]; + // normalize graph coordinates by zoomScale + const zoomScale = state.scale; + const outerRadius = radius + this.state.nodeScale(2); + if (offsetX + (centerX + outerRadius) * zoomScale > visibleWidth) { + // shift left if blocked by details + const shift = (centerX + outerRadius) * zoomScale - visibleWidth; + translate[0] = -shift; + } else if (offsetX + (centerX - outerRadius) * zoomScale < 0) { + // shift right if off canvas + const shift = offsetX - offsetX + (centerX - outerRadius) * zoomScale; + translate[0] = -shift; + } + const offsetY = translate[1]; + if (offsetY + (centerY + outerRadius) * zoomScale > props.height) { + // shift up if past bottom + const shift = (centerY + outerRadius) * zoomScale - props.height; + translate[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; + translate[1] = -shift; + } + // debug('shift', centerX, centerY, outerRadius, translate); + + // saving translate in d3's panning cache + this.zoom.translate(translate); + autoShifted = true; + } return { + autoShifted: autoShifted, edges: layoutEdges, nodes: layoutNodes, translate: translate @@ -356,6 +351,10 @@ const NodesChart = React.createClass({ handleBackgroundClick: function() { AppActions.clickCloseDetails(); + // allow shifts again + this.setState({ + autoShifted: false + }); }, restoreLayout: function(state) { @@ -441,6 +440,7 @@ const NodesChart = React.createClass({ zoomed: function() { // debug('zoomed', d3.event.scale, d3.event.translate); this.setState({ + autoShifted: false, hasZoomed: true, panTranslate: d3.event.translate.slice(), translate: d3.event.translate.slice(), From 808fc558323bbbd4e603fda352fcbf73bcbea485 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 18 Sep 2015 17:18:32 +0200 Subject: [PATCH 2/3] filter loopback adjacents on radial layout needed to adjust size of nodes, too --- client/app/scripts/charts/nodes-chart.js | 18 +++++++++++------- client/app/scripts/charts/nodes-layout.js | 4 ++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 08eb4dba2..f876206b2 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -19,9 +19,9 @@ const MARGINS = { bottom: 0 }; -// make sure circular layouts lots of nodes spread out -const radiusDensity = d3.scale.sqrt() - .domain([12, 2]).range([3, 4]).clamp(true); +// make sure circular layouts a bit denser with 3-6 nodes +const radiusDensity = d3.scale.threshold() + .domain([3, 6]).range([3, 4, 3]); const NodesChart = React.createClass({ @@ -270,7 +270,10 @@ const NodesChart = React.createClass({ const adjacentLayoutNodes = []; adjacency.forEach(function(adjacentId) { - adjacentLayoutNodes.push(layoutNodes[adjacentId]); + // filter loopback + if (adjacentId !== props.selectedNodeId) { + adjacentLayoutNodes.push(layoutNodes[adjacentId]); + } }); // shift center node a bit @@ -314,7 +317,7 @@ const NodesChart = React.createClass({ const offsetX = translate[0]; // normalize graph coordinates by zoomScale const zoomScale = state.scale; - const outerRadius = radius + this.state.nodeScale(2); + const outerRadius = radius + this.state.nodeScale(1.5); if (offsetX + (centerX + outerRadius) * zoomScale > visibleWidth) { // shift left if blocked by details const shift = (centerX + outerRadius) * zoomScale - visibleWidth; @@ -386,8 +389,9 @@ const NodesChart = React.createClass({ const edges = this.initEdges(props.nodes, nodes); const expanse = Math.min(props.height, props.width); - const nodeSize = expanse / 2; - const nodeScale = this.state.nodeScale.range([0, nodeSize / Math.pow(n, 0.7)]); + 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 timedLayouter = timely(NodesLayout.doLayout); const graph = timedLayouter( diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index 5a6011789..5ee720f5b 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -33,8 +33,8 @@ const doLayout = function(nodes, edges, width, height, scale, margins, topologyI if (!graph.hasNode(node.id)) { graph.setNode(node.id, { id: node.id, - width: scale(0.75), - height: scale(0.75) + width: scale(1), + height: scale(1) }); } }); From a3597c9639bc32c06b05e149c7e09ab3ec958f42 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 21 Sep 2015 11:44:26 +0200 Subject: [PATCH 3/3] Fix zooming and autoshift * track panning to not trigger mouse up event * offset angle for circle * fixed panning/zooming * center the center node if radius bigger than screen --- client/app/scripts/charts/nodes-chart.js | 69 ++++++++++++++---------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index f876206b2..0a4714c76 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -21,7 +21,7 @@ const MARGINS = { // make sure circular layouts a bit denser with 3-6 nodes const radiusDensity = d3.scale.threshold() - .domain([3, 6]).range([3, 4, 3]); + .domain([3, 6]).range([2.5, 3.5, 3]); const NodesChart = React.createClass({ @@ -30,7 +30,7 @@ const NodesChart = React.createClass({ nodes: {}, edges: {}, nodeScale: d3.scale.linear(), - translate: [0, 0], + shiftTranslate: [0, 0], panTranslate: [0, 0], scale: 1, hasZoomed: false, @@ -49,8 +49,7 @@ const NodesChart = React.createClass({ .scaleExtent([0.1, 2]) .on('zoom', this.zoomed); - d3.select('.canvas') - .on('click', this.handleBackgroundClick) + d3.select('.nodes-chart svg') .call(this.zoom); }, @@ -61,6 +60,7 @@ const NodesChart = React.createClass({ // wipe node states when showing different topology if (nextProps.topologyId !== this.props.topologyId) { _.assign(state, { + autoShifted: false, nodes: {}, edges: {} }); @@ -82,8 +82,7 @@ const NodesChart = React.createClass({ componentWillUnmount: function() { // undoing .call(zoom) - d3.select('.canvas') - .on('click', null) + d3.select('.nodes-chart svg') .on('mousedown.zoom', null) .on('onwheel', null) .on('onmousewheel', null) @@ -159,7 +158,7 @@ const NodesChart = React.createClass({ // only animate shift behavior, not panning const panTranslate = this.state.panTranslate; - const shiftTranslate = this.state.translate; + const shiftTranslate = this.state.shiftTranslate; let translate = panTranslate; let wasShifted = false; if (shiftTranslate[0] !== panTranslate[0] || shiftTranslate[1] !== panTranslate[1]) { @@ -175,7 +174,7 @@ const NodesChart = React.createClass({
Too many nodes to show in the browser.
We're working on it, but for now, try a different view?
- + {function(interpolated) { let interpolatedTranslate = wasShifted ? interpolated.val : panTranslate; @@ -287,9 +286,10 @@ const NodesChart = React.createClass({ const adjacentCount = adjacentLayoutNodes.length; const density = radiusDensity(adjacentCount); const radius = Math.min(props.width, props.height) / density; + const offsetAngle = Math.PI / 4; _.each(adjacentLayoutNodes, function(node, i) { - const angle = Math.PI * 2 * i / adjacentCount; + const angle = offsetAngle + Math.PI * 2 * i / adjacentCount; node.x = centerX + radius * Math.sin(angle); node.y = centerY + radius * Math.cos(angle); }); @@ -310,37 +310,43 @@ const NodesChart = React.createClass({ // shift canvas selected node out of view if it has not been shifted already let autoShifted = this.state.autoShifted; - const translate = state.translate; + const shiftTranslate = state.shiftTranslate; if (!autoShifted) { const visibleWidth = Math.max(props.width - props.detailsWidth, 0); - const offsetX = translate[0]; + const offsetX = shiftTranslate[0]; // normalize graph coordinates by zoomScale const zoomScale = state.scale; const outerRadius = radius + this.state.nodeScale(1.5); - if (offsetX + (centerX + outerRadius) * zoomScale > visibleWidth) { + 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; - translate[0] = -shift; + shiftTranslate[0] = -shift; } else if (offsetX + (centerX - outerRadius) * zoomScale < 0) { // shift right if off canvas const shift = offsetX - offsetX + (centerX - outerRadius) * zoomScale; - translate[0] = -shift; + shiftTranslate[0] = -shift; } - const offsetY = translate[1]; - if (offsetY + (centerY + outerRadius) * zoomScale > props.height) { + 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; - translate[1] = -shift; + 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; - translate[1] = -shift; + shiftTranslate[1] = -shift; } - // debug('shift', centerX, centerY, outerRadius, translate); + // debug('shift', centerX, centerY, outerRadius, shiftTranslate); // saving translate in d3's panning cache - this.zoom.translate(translate); + this.zoom.translate(shiftTranslate); autoShifted = true; } @@ -348,16 +354,22 @@ const NodesChart = React.createClass({ autoShifted: autoShifted, edges: layoutEdges, nodes: layoutNodes, - translate: translate + shiftTranslate: shiftTranslate }; }, - handleBackgroundClick: function() { - AppActions.clickCloseDetails(); - // allow shifts again - this.setState({ - autoShifted: false - }); + isZooming: false, // distinguish pan/zoom from click + + handleMouseUp: function() { + if (!this.isZooming) { + AppActions.clickCloseDetails(); + // allow shifts again + this.setState({ + autoShifted: false + }); + } else { + this.isZooming = false; + } }, restoreLayout: function(state) { @@ -443,11 +455,12 @@ const NodesChart = React.createClass({ zoomed: function() { // debug('zoomed', d3.event.scale, d3.event.translate); + this.isZooming = true; this.setState({ autoShifted: false, hasZoomed: true, panTranslate: d3.event.translate.slice(), - translate: d3.event.translate.slice(), + shiftTranslate: d3.event.translate.slice(), scale: d3.event.scale }); }