highlight edges and connected nodes on edge hover

This commit is contained in:
David Kaltschmidt
2015-05-29 18:00:35 +02:00
parent e6f4a8ce71
commit 9c3db34d49
9 changed files with 134 additions and 23 deletions

View File

@@ -39,6 +39,13 @@ module.exports = {
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
},
enterEdge: function(edgeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_EDGE,
edgeId: edgeId
});
},
enterNode: function(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_NODE,
@@ -53,6 +60,13 @@ module.exports = {
RouterUtils.updateRoute();
},
leaveEdge: function(edgeId) {
AppDispatcher.dispatch({
type: ActionTypes.LEAVE_EDGE,
edgeId: edgeId
});
},
leaveNode: function(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.LEAVE_NODE,

View File

@@ -0,0 +1,34 @@
const d3 = require('d3');
const React = require('react');
const AppActions = require('../actions/app-actions');
const line = d3.svg.line()
.interpolate('basis')
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
const Edge = React.createClass({
render: function() {
const className = this.props.highlighted ? 'edge highlighted' : 'edge';
return (
<g className={className} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} id={this.props.id}>
<path d={line(this.props.points)} className="shadow" />
<path d={line(this.props.points)} className="link" />
</g>
);
},
handleMouseEnter: function(ev) {
AppActions.enterEdge(ev.currentTarget.id);
},
handleMouseLeave: function(ev) {
AppActions.leaveEdge(ev.currentTarget.id);
}
});
module.exports = Edge;

View File

@@ -2,6 +2,8 @@ const _ = require('lodash');
const d3 = require('d3');
const React = require('react');
const Edge = require('./edge');
const Naming = require('../constants/naming');
const NodesLayout = require('./nodes-layout');
const Node = require('./node');
@@ -12,11 +14,6 @@ const MARGINS = {
bottom: 0
};
const line = d3.svg.line()
.interpolate('basis')
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
const NodesChart = React.createClass({
getInitialState: function() {
@@ -77,9 +74,9 @@ const NodesChart = React.createClass({
return fingerprint.join(';');
},
getGraphNodes: function(nodes, scale) {
renderGraphNodes: function(nodes, scale) {
return _.map(nodes, function(node) {
const highlighted = node.id === this.props.mouseOverNodeId || _.includes(node.adjacency, this.props.mouseOverNodeId);
const highlighted = _.includes(this.props.highlightedNodeIds, node.id);
return (
<Node
highlighted={highlighted}
@@ -96,17 +93,18 @@ const NodesChart = React.createClass({
}, this);
},
getGraphEdges: function(edges) {
renderGraphEdges: function(edges) {
return _.map(edges, function(edge) {
const highlighted = _.includes(this.props.highlightedEdgeIds, edge.id);
return (
<path className="link" d={line(edge.points)} key={edge.id} />
<Edge key={edge.id} id={edge.id} points={edge.points} highlighted={highlighted} />
);
});
}, this);
},
render: function() {
const nodeElements = this.getGraphNodes(this.state.nodes, this.state.nodeScale);
const edgeElements = this.getGraphEdges(this.state.edges, this.state.nodeScale);
const nodeElements = this.renderGraphNodes(this.state.nodes, this.state.nodeScale);
const edgeElements = this.renderGraphEdges(this.state.edges, this.state.nodeScale);
const transform = 'translate(' + this.state.translate + ')' +
' scale(' + this.state.scale + ')';
@@ -158,7 +156,7 @@ const NodesChart = React.createClass({
_.each(topology, function(node) {
_.each(node.adjacency, function(adjacent) {
const edge = [node.id, adjacent];
const edgeId = edge.join('-');
const edgeId = edge.join(Naming.EDGE_ID_SEPARATOR);
if (!edges[edgeId]) {
const source = nodes[edge[0]];

View File

@@ -19,7 +19,8 @@ function getStateFromStores() {
currentTopology: AppStore.getCurrentTopology(),
connectionState: AppStore.getConnectionState(),
currentGrouping: AppStore.getCurrentGrouping(),
mouseOverNodeId: AppStore.getMouseOverNodeId(),
highlightedEdgeIds: AppStore.getHighlightedEdgeIds(),
highlightedNodeIds: AppStore.getHighlightedNodeIds(),
selectedNodeId: AppStore.getSelectedNodeId(),
nodeDetails: AppStore.getNodeDetails(),
nodes: AppStore.getNodes(),
@@ -68,7 +69,8 @@ const App = React.createClass({
<Status connectionState={this.state.connectionState} />
</div>
<Nodes nodes={this.state.nodes} mouseOverNodeId={this.state.mouseOverNodeId} />
<Nodes nodes={this.state.nodes} highlightedNodeIds={this.state.highlightedNodeIds}
highlightedEdgeIds={this.state.highlightedEdgeIds} />
</div>
);
}

View File

@@ -31,7 +31,8 @@ const Nodes = React.createClass({
return (
<div id="nodes">
<NodesChart
mouseOverNodeId={this.props.mouseOverNodeId}
highlightedEdgeIds={this.props.highlightedEdgeIds}
highlightedNodeIds={this.props.highlightedNodeIds}
nodes={this.props.nodes}
onNodeClick={this.onNodeClick}
width={this.state.width}

View File

@@ -5,8 +5,10 @@ module.exports = keymirror({
CLICK_GROUPING: null,
CLICK_NODE: null,
CLICK_TOPOLOGY: null,
ENTER_EDGE: null,
ENTER_NODE: null,
HIT_ESC_KEY: null,
LEAVE_EDGE: null,
LEAVE_NODE: null,
RECEIVE_NODE_DETAILS: null,
RECEIVE_NODES: null,

View File

@@ -0,0 +1,4 @@
module.exports = {
EDGE_ID_SEPARATOR: '-'
};

View File

@@ -4,6 +4,7 @@ const assign = require('object-assign');
const AppDispatcher = require('../dispatcher/app-dispatcher');
const ActionTypes = require('../constants/action-types');
const Naming = require('../constants/naming');
// Helpers
@@ -16,6 +17,7 @@ function isUrlForTopologyId(url, topologyId) {
let connectionState = 'disconnected';
let currentGrouping = 'none';
let currentTopologyId = 'applications';
let mouseOverEdgeId = null;
let mouseOverNodeId = null;
let nodes = {};
let nodeDetails = null;
@@ -58,8 +60,34 @@ const AppStore = assign({}, EventEmitter.prototype, {
return currentGrouping;
},
getMouseOverNodeId: function() {
return mouseOverNodeId;
getHighlightedEdgeIds: function() {
if (mouseOverNodeId) {
// all neighbour combinations because we dont know which direction exists
const node = nodes[mouseOverNodeId];
return _.flatten(
_.map(node.adjacency, function(nodeId) {
return [
[nodeId, mouseOverNodeId].join(Naming.EDGE_ID_SEPARATOR),
[mouseOverNodeId, nodeId].join(Naming.EDGE_ID_SEPARATOR)
];
})
);
}
if (mouseOverEdgeId) {
return mouseOverEdgeId;
}
return null;
},
getHighlightedNodeIds: function() {
if (mouseOverNodeId) {
const node = nodes[mouseOverNodeId];
return _.union(node.adjacency, [mouseOverNodeId]);
}
if (mouseOverEdgeId) {
return mouseOverEdgeId.split(Naming.EDGE_ID_SEPARATOR);
}
return null;
},
getNodeDetails: function() {
@@ -114,6 +142,11 @@ AppStore.registeredCallback = function(payload) {
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.ENTER_EDGE:
mouseOverEdgeId = payload.edgeId;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.ENTER_NODE:
mouseOverNodeId = payload.nodeId;
AppStore.emit(AppStore.CHANGE_EVENT);
@@ -125,6 +158,12 @@ AppStore.registeredCallback = function(payload) {
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.LEAVE_EDGE:
mouseOverEdgeId = null;
console.log('leave');
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.LEAVE_NODE:
mouseOverNodeId = null;
AppStore.emit(AppStore.CHANGE_EVENT);
@@ -149,6 +188,9 @@ AppStore.registeredCallback = function(payload) {
if (mouseOverNodeId === nodeId) {
mouseOverNodeId = null;
}
if (nodes[nodeId] && _.contains(mouseOverEdgeId, nodeId)) {
mouseOverEdgeId = null;
}
delete nodes[nodeId];
});

View File

@@ -162,13 +162,27 @@ body {
cursor: pointer;
}
.link {
stroke: @text-secondary-color;
stroke-width: 1.5px;
fill: none;
opacity: 0.5;
.edge {
.link {
stroke: @text-secondary-color;
stroke-width: 1.5px;
fill: none;
stroke-opacity: 0.5;
}
.shadow {
stroke: @weave-blue;
stroke-width: 10px;
fill: none;
stroke-opacity: 0;
}
&.highlighted {
.shadow {
stroke-opacity: 0.1;
}
}
}
circle.border {
stroke-width: 3px;
fill: none;