mirror of
https://github.com/weaveworks/scope.git
synced 2026-08-18 20:07:06 +00:00
Improved rendering order of nodes/edges in Graph View (#2623)
* Rendering both nodes and edges from one component. * Organized the render layers in a more readable manner. * Rerendering optimization. * Finer edge rendering layers. * Moved the constants out of the component file. * Typo fix. * Include edge thickness in <Motion /> instead of fade-in effect.
This commit is contained in:
@@ -5,7 +5,7 @@ import { line, curveBasis } from 'd3-shape';
|
||||
import { each, omit, times, constant } from 'lodash';
|
||||
|
||||
import { NODES_SPRING_ANIMATION_CONFIG } from '../constants/animation';
|
||||
import { EDGE_WAYPOINTS_CAP } from '../constants/styles';
|
||||
import { NODE_BASE_SIZE, EDGE_WAYPOINTS_CAP } from '../constants/styles';
|
||||
import Edge from './edge';
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ const spline = line()
|
||||
.x(d => d.x)
|
||||
.y(d => d.y);
|
||||
|
||||
const transformedEdge = (props, path) => (
|
||||
<Edge {...props} path={spline(path)} />
|
||||
const transformedEdge = (props, path, thickness) => (
|
||||
<Edge {...props} path={spline(path)} thickness={thickness} />
|
||||
);
|
||||
|
||||
// Converts a waypoints map of the format { x0: 11, y0: 22, x1: 33, y1: 44 }
|
||||
@@ -45,7 +45,11 @@ const waypointsArrayToMap = (waypointsArray) => {
|
||||
export default class EdgeContainer extends React.PureComponent {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = { waypointsMap: makeMap() };
|
||||
|
||||
this.state = {
|
||||
waypointsMap: makeMap(),
|
||||
thickness: 1,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
@@ -59,21 +63,32 @@ export default class EdgeContainer extends React.PureComponent {
|
||||
if (this.props.isAnimated && nextProps.waypoints !== this.props.waypoints) {
|
||||
this.prepareWaypointsForMotion(nextProps.waypoints);
|
||||
}
|
||||
// Edge thickness will reflect the zoom scale.
|
||||
const baseScale = (nextProps.scale * 0.01) * NODE_BASE_SIZE;
|
||||
const thickness = (nextProps.focused ? 3 : 1) * baseScale;
|
||||
this.setState({ thickness });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isAnimated, waypoints } = this.props;
|
||||
const forwardedProps = omit(this.props, 'isAnimated', 'waypoints');
|
||||
const forwardedProps = omit(this.props, 'isAnimated', 'waypoints', 'scale');
|
||||
|
||||
if (!isAnimated) {
|
||||
return transformedEdge(forwardedProps, waypoints.toJS());
|
||||
return transformedEdge(forwardedProps, waypoints.toJS(), this.state.thickness);
|
||||
}
|
||||
|
||||
return (
|
||||
// For the Motion interpolation to work, the waypoints need to be in a map format like
|
||||
// { x0: 11, y0: 22, x1: 33, y1: 44 } that we convert to the array format when rendering.
|
||||
<Motion style={this.state.waypointsMap.toJS()}>
|
||||
{interpolated => transformedEdge(forwardedProps, waypointsMapToArray(interpolated))}
|
||||
<Motion
|
||||
style={{
|
||||
thickness: spring(this.state.thickness, NODES_SPRING_ANIMATION_CONFIG),
|
||||
...this.state.waypointsMap.toJS(),
|
||||
}}
|
||||
>
|
||||
{({ thickness, ...interpolatedWaypoints}) => transformedEdge(
|
||||
forwardedProps, waypointsMapToArray(interpolatedWaypoints), thickness
|
||||
)}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { enterEdge, leaveEdge } from '../actions/app-actions';
|
||||
import { NODE_BASE_SIZE } from '../constants/styles';
|
||||
|
||||
class Edge extends React.Component {
|
||||
|
||||
@@ -14,34 +13,22 @@ class Edge extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
path,
|
||||
highlighted,
|
||||
blurred,
|
||||
focused,
|
||||
scale,
|
||||
source,
|
||||
target
|
||||
} = this.props;
|
||||
const className = classNames('edge', { highlighted, blurred, focused });
|
||||
const thickness = (scale * 0.01) * NODE_BASE_SIZE;
|
||||
const strokeWidth = focused ? thickness * 3 : thickness;
|
||||
const { id, path, highlighted, blurred, focused, thickness, source, target } = this.props;
|
||||
const shouldRenderMarker = (focused || highlighted) && (source !== target);
|
||||
// Draws the edge so that its thickness reflects the zoom scale.
|
||||
// Edge shadow is always made 10x thicker than the edge itself.
|
||||
const className = classNames('edge', { highlighted, blurred });
|
||||
|
||||
return (
|
||||
<g
|
||||
id={id} className={className}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<path className="shadow" d={path} style={{ strokeWidth: 10 * strokeWidth }} />
|
||||
<path className="shadow" d={path} style={{ strokeWidth: 10 * thickness }} />
|
||||
<path
|
||||
className="link"
|
||||
d={path}
|
||||
markerEnd={shouldRenderMarker ? 'url(#end-arrow)' : null}
|
||||
style={{ strokeWidth }}
|
||||
style={{ strokeWidth: thickness }}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Map as makeMap } from 'immutable';
|
||||
|
||||
import { searchNodeMatchesSelector } from '../selectors/search';
|
||||
import { selectedNetworkNodesIdsSelector } from '../selectors/node-networks';
|
||||
import { highlightedEdgeIdsSelector } from '../selectors/graph-view/decorators';
|
||||
import { hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils';
|
||||
import EdgeContainer from './edge-container';
|
||||
|
||||
class NodesChartEdges extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
// Edge decorators
|
||||
this.edgeFocusedDecorator = this.edgeFocusedDecorator.bind(this);
|
||||
this.edgeBlurredDecorator = this.edgeBlurredDecorator.bind(this);
|
||||
this.edgeHighlightedDecorator = this.edgeHighlightedDecorator.bind(this);
|
||||
this.edgeScaleDecorator = this.edgeScaleDecorator.bind(this);
|
||||
}
|
||||
|
||||
edgeHighlightedDecorator(edge) {
|
||||
return edge.set('highlighted', this.props.highlightedEdgeIds.has(edge.get('id')));
|
||||
}
|
||||
|
||||
edgeFocusedDecorator(edge) {
|
||||
const sourceSelected = (this.props.selectedNodeId === edge.get('source'));
|
||||
const targetSelected = (this.props.selectedNodeId === edge.get('target'));
|
||||
return edge.set('focused', this.props.hasSelectedNode && (sourceSelected || targetSelected));
|
||||
}
|
||||
|
||||
edgeBlurredDecorator(edge) {
|
||||
const { selectedNodeId, searchNodeMatches, selectedNetworkNodesIds } = this.props;
|
||||
const sourceSelected = (selectedNodeId === edge.get('source'));
|
||||
const targetSelected = (selectedNodeId === edge.get('target'));
|
||||
const otherNodesSelected = this.props.hasSelectedNode && !sourceSelected && !targetSelected;
|
||||
const sourceNoMatches = searchNodeMatches.get(edge.get('source'), makeMap()).isEmpty();
|
||||
const targetNoMatches = searchNodeMatches.get(edge.get('target'), makeMap()).isEmpty();
|
||||
const notMatched = this.props.searchQuery && (sourceNoMatches || targetNoMatches);
|
||||
const sourceInNetwork = selectedNetworkNodesIds.contains(edge.get('source'));
|
||||
const targetInNetwork = selectedNetworkNodesIds.contains(edge.get('target'));
|
||||
const notInNetwork = this.props.selectedNetwork && (!sourceInNetwork || !targetInNetwork);
|
||||
return edge.set('blurred', !edge.get('highlighted') && !edge.get('focused') &&
|
||||
(otherNodesSelected || notMatched || notInNetwork));
|
||||
}
|
||||
|
||||
edgeScaleDecorator(edge) {
|
||||
return edge.set('scale', edge.get('focused') ? this.props.selectedScale : 1);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { layoutEdges, isAnimated } = this.props;
|
||||
|
||||
const edgesToRender = layoutEdges.toIndexedSeq()
|
||||
.map(this.edgeHighlightedDecorator)
|
||||
.map(this.edgeFocusedDecorator)
|
||||
.map(this.edgeBlurredDecorator)
|
||||
.map(this.edgeScaleDecorator);
|
||||
|
||||
return (
|
||||
<g className="nodes-chart-edges">
|
||||
{edgesToRender.map(edge => (
|
||||
<EdgeContainer
|
||||
key={edge.get('id')}
|
||||
id={edge.get('id')}
|
||||
source={edge.get('source')}
|
||||
target={edge.get('target')}
|
||||
waypoints={edge.get('points')}
|
||||
highlighted={edge.get('highlighted')}
|
||||
focused={edge.get('focused')}
|
||||
blurred={edge.get('blurred')}
|
||||
scale={edge.get('scale')}
|
||||
isAnimated={isAnimated}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
hasSelectedNode: hasSelectedNodeFn(state),
|
||||
searchNodeMatches: searchNodeMatchesSelector(state),
|
||||
selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state),
|
||||
highlightedEdgeIds: highlightedEdgeIdsSelector(state),
|
||||
searchQuery: state.get('searchQuery'),
|
||||
selectedNetwork: state.get('selectedNetwork'),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
})
|
||||
)(NodesChartEdges);
|
||||
@@ -1,30 +1,236 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Map as makeMap, List as makeList } from 'immutable';
|
||||
|
||||
import NodesChartEdges from './nodes-chart-edges';
|
||||
import NodesChartNodes from './nodes-chart-nodes';
|
||||
import NodeContainer from './node-container';
|
||||
import EdgeContainer from './edge-container';
|
||||
import { getAdjacentNodes, hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils';
|
||||
import { graphExceedsComplexityThreshSelector } from '../selectors/topology';
|
||||
import { nodeNetworksSelector, selectedNetworkNodesIdsSelector } from '../selectors/node-networks';
|
||||
import { searchNodeMatchesSelector } from '../selectors/search';
|
||||
import { nodeMetricSelector } from '../selectors/node-metric';
|
||||
import {
|
||||
highlightedNodeIdsSelector,
|
||||
highlightedEdgeIdsSelector
|
||||
} from '../selectors/graph-view/decorators';
|
||||
import {
|
||||
selectedScaleSelector,
|
||||
layoutNodesSelector,
|
||||
layoutEdgesSelector
|
||||
} from '../selectors/graph-view/layout';
|
||||
|
||||
import {
|
||||
BLURRED_EDGES_LAYER,
|
||||
BLURRED_NODES_LAYER,
|
||||
NORMAL_EDGES_LAYER,
|
||||
NORMAL_NODES_LAYER,
|
||||
HIGHLIGHTED_EDGES_LAYER,
|
||||
HIGHLIGHTED_NODES_LAYER,
|
||||
HOVERED_EDGES_LAYER,
|
||||
HOVERED_NODES_LAYER,
|
||||
} from '../constants/naming';
|
||||
|
||||
|
||||
class NodesChartElements extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.renderNode = this.renderNode.bind(this);
|
||||
this.renderEdge = this.renderEdge.bind(this);
|
||||
this.renderElement = this.renderElement.bind(this);
|
||||
this.nodeDisplayLayer = this.nodeDisplayLayer.bind(this);
|
||||
this.edgeDisplayLayer = this.edgeDisplayLayer.bind(this);
|
||||
|
||||
// Node decorators
|
||||
this.nodeHighlightedDecorator = this.nodeHighlightedDecorator.bind(this);
|
||||
this.nodeFocusedDecorator = this.nodeFocusedDecorator.bind(this);
|
||||
this.nodeBlurredDecorator = this.nodeBlurredDecorator.bind(this);
|
||||
this.nodeMatchesDecorator = this.nodeMatchesDecorator.bind(this);
|
||||
this.nodeNetworksDecorator = this.nodeNetworksDecorator.bind(this);
|
||||
this.nodeMetricDecorator = this.nodeMetricDecorator.bind(this);
|
||||
this.nodeScaleDecorator = this.nodeScaleDecorator.bind(this);
|
||||
|
||||
// Edge decorators
|
||||
this.edgeFocusedDecorator = this.edgeFocusedDecorator.bind(this);
|
||||
this.edgeBlurredDecorator = this.edgeBlurredDecorator.bind(this);
|
||||
this.edgeHighlightedDecorator = this.edgeHighlightedDecorator.bind(this);
|
||||
this.edgeScaleDecorator = this.edgeScaleDecorator.bind(this);
|
||||
}
|
||||
|
||||
nodeDisplayLayer(node) {
|
||||
if (node.get('id') === this.props.mouseOverNodeId) {
|
||||
return HOVERED_NODES_LAYER;
|
||||
} else if (node.get('blurred') && !node.get('focused')) {
|
||||
return BLURRED_NODES_LAYER;
|
||||
} else if (node.get('highlighted')) {
|
||||
return HIGHLIGHTED_NODES_LAYER;
|
||||
}
|
||||
return NORMAL_NODES_LAYER;
|
||||
}
|
||||
|
||||
edgeDisplayLayer(edge) {
|
||||
if (edge.get('id') === this.props.mouseOverEdgeId) {
|
||||
return HOVERED_EDGES_LAYER;
|
||||
} else if (edge.get('blurred') && !edge.get('focused')) {
|
||||
return BLURRED_EDGES_LAYER;
|
||||
} else if (edge.get('highlighted')) {
|
||||
return HIGHLIGHTED_EDGES_LAYER;
|
||||
}
|
||||
return NORMAL_EDGES_LAYER;
|
||||
}
|
||||
|
||||
nodeHighlightedDecorator(node) {
|
||||
const nodeSelected = (this.props.selectedNodeId === node.get('id'));
|
||||
const nodeHighlighted = this.props.highlightedNodeIds.has(node.get('id'));
|
||||
return node.set('highlighted', nodeHighlighted || nodeSelected);
|
||||
}
|
||||
|
||||
nodeFocusedDecorator(node) {
|
||||
const nodeSelected = (this.props.selectedNodeId === node.get('id'));
|
||||
const isNeighborOfSelected = this.props.neighborsOfSelectedNode.includes(node.get('id'));
|
||||
return node.set('focused', nodeSelected || isNeighborOfSelected);
|
||||
}
|
||||
|
||||
nodeBlurredDecorator(node) {
|
||||
const belongsToNetwork = this.props.selectedNetworkNodesIds.contains(node.get('id'));
|
||||
const noMatches = this.props.searchNodeMatches.get(node.get('id'), makeMap()).isEmpty();
|
||||
const notMatched = (this.props.searchQuery && !node.get('highlighted') && noMatches);
|
||||
const notFocused = (this.props.selectedNodeId && !node.get('focused'));
|
||||
const notInNetwork = (this.props.selectedNetwork && !belongsToNetwork);
|
||||
return node.set('blurred', notMatched || notFocused || notInNetwork);
|
||||
}
|
||||
|
||||
nodeMatchesDecorator(node) {
|
||||
return node.set('matches', this.props.searchNodeMatches.get(node.get('id')));
|
||||
}
|
||||
|
||||
nodeNetworksDecorator(node) {
|
||||
return node.set('networks', this.props.nodeNetworks.get(node.get('id')));
|
||||
}
|
||||
|
||||
nodeMetricDecorator(node) {
|
||||
return node.set('metric', this.props.nodeMetric.get(node.get('id')));
|
||||
}
|
||||
|
||||
nodeScaleDecorator(node) {
|
||||
return node.set('scale', node.get('focused') ? this.props.selectedScale : 1);
|
||||
}
|
||||
|
||||
edgeHighlightedDecorator(edge) {
|
||||
return edge.set('highlighted', this.props.highlightedEdgeIds.has(edge.get('id')));
|
||||
}
|
||||
|
||||
edgeFocusedDecorator(edge) {
|
||||
const sourceSelected = (this.props.selectedNodeId === edge.get('source'));
|
||||
const targetSelected = (this.props.selectedNodeId === edge.get('target'));
|
||||
return edge.set('focused', this.props.hasSelectedNode && (sourceSelected || targetSelected));
|
||||
}
|
||||
|
||||
edgeBlurredDecorator(edge) {
|
||||
const { selectedNodeId, searchNodeMatches, selectedNetworkNodesIds } = this.props;
|
||||
const sourceSelected = (selectedNodeId === edge.get('source'));
|
||||
const targetSelected = (selectedNodeId === edge.get('target'));
|
||||
const otherNodesSelected = this.props.hasSelectedNode && !sourceSelected && !targetSelected;
|
||||
const sourceNoMatches = searchNodeMatches.get(edge.get('source'), makeMap()).isEmpty();
|
||||
const targetNoMatches = searchNodeMatches.get(edge.get('target'), makeMap()).isEmpty();
|
||||
const notMatched = this.props.searchQuery && (sourceNoMatches || targetNoMatches);
|
||||
const sourceInNetwork = selectedNetworkNodesIds.contains(edge.get('source'));
|
||||
const targetInNetwork = selectedNetworkNodesIds.contains(edge.get('target'));
|
||||
const notInNetwork = this.props.selectedNetwork && (!sourceInNetwork || !targetInNetwork);
|
||||
return edge.set('blurred', !edge.get('highlighted') && !edge.get('focused') &&
|
||||
(otherNodesSelected || notMatched || notInNetwork));
|
||||
}
|
||||
|
||||
edgeScaleDecorator(edge) {
|
||||
return edge.set('scale', edge.get('focused') ? this.props.selectedScale : 1);
|
||||
}
|
||||
|
||||
renderNode(node) {
|
||||
const { isAnimated, contrastMode } = this.props;
|
||||
return (
|
||||
<NodeContainer
|
||||
matches={node.get('matches')}
|
||||
networks={node.get('networks')}
|
||||
metric={node.get('metric')}
|
||||
blurred={node.get('blurred')}
|
||||
focused={node.get('focused')}
|
||||
highlighted={node.get('highlighted')}
|
||||
shape={node.get('shape')}
|
||||
stack={node.get('stack')}
|
||||
key={node.get('id')}
|
||||
id={node.get('id')}
|
||||
label={node.get('label')}
|
||||
labelMinor={node.get('labelMinor')}
|
||||
pseudo={node.get('pseudo')}
|
||||
rank={node.get('rank')}
|
||||
dx={node.get('x')}
|
||||
dy={node.get('y')}
|
||||
scale={node.get('scale')}
|
||||
isAnimated={isAnimated}
|
||||
contrastMode={contrastMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderEdge(edge) {
|
||||
const { isAnimated } = this.props;
|
||||
return (
|
||||
<EdgeContainer
|
||||
key={edge.get('id')}
|
||||
id={edge.get('id')}
|
||||
source={edge.get('source')}
|
||||
target={edge.get('target')}
|
||||
waypoints={edge.get('points')}
|
||||
highlighted={edge.get('highlighted')}
|
||||
focused={edge.get('focused')}
|
||||
blurred={edge.get('blurred')}
|
||||
scale={edge.get('scale')}
|
||||
isAnimated={isAnimated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderElement(element) {
|
||||
// This heuristics is not ideal but it works.
|
||||
return element.get('points') ? this.renderEdge(element) : this.renderNode(element);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { layoutNodes, layoutEdges, selectedScale, isAnimated } = this.props;
|
||||
const nodes = this.props.layoutNodes.toIndexedSeq()
|
||||
.map(this.nodeHighlightedDecorator)
|
||||
.map(this.nodeFocusedDecorator)
|
||||
.map(this.nodeBlurredDecorator)
|
||||
.map(this.nodeMatchesDecorator)
|
||||
.map(this.nodeNetworksDecorator)
|
||||
.map(this.nodeMetricDecorator)
|
||||
.map(this.nodeScaleDecorator)
|
||||
.groupBy(this.nodeDisplayLayer);
|
||||
|
||||
const edges = this.props.layoutEdges.toIndexedSeq()
|
||||
.map(this.edgeHighlightedDecorator)
|
||||
.map(this.edgeFocusedDecorator)
|
||||
.map(this.edgeBlurredDecorator)
|
||||
.map(this.edgeScaleDecorator)
|
||||
.groupBy(this.edgeDisplayLayer);
|
||||
|
||||
// NOTE: The elements need to be arranged into a single array outside
|
||||
// of DOM structure for React rendering engine to do smart rearrangements
|
||||
// without unnecessary re-rendering of the elements themselves. So e.g.
|
||||
// rendering the element layers individually below would be significantly slower.
|
||||
const orderedElements = makeList([
|
||||
edges.get(BLURRED_EDGES_LAYER, makeList()),
|
||||
nodes.get(BLURRED_NODES_LAYER, makeList()),
|
||||
edges.get(NORMAL_EDGES_LAYER, makeList()),
|
||||
nodes.get(NORMAL_NODES_LAYER, makeList()),
|
||||
edges.get(HIGHLIGHTED_EDGES_LAYER, makeList()),
|
||||
nodes.get(HIGHLIGHTED_NODES_LAYER, makeList()),
|
||||
edges.get(HOVERED_EDGES_LAYER, makeList()),
|
||||
nodes.get(HOVERED_NODES_LAYER, makeList()),
|
||||
]).flatten(true);
|
||||
|
||||
return (
|
||||
<g className="nodes-chart-elements">
|
||||
<NodesChartEdges
|
||||
layoutEdges={layoutEdges}
|
||||
selectedScale={selectedScale}
|
||||
isAnimated={isAnimated} />
|
||||
<NodesChartNodes
|
||||
layoutNodes={layoutNodes}
|
||||
selectedScale={selectedScale}
|
||||
isAnimated={isAnimated} />
|
||||
{orderedElements.map(this.renderElement)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
@@ -33,10 +239,24 @@ class NodesChartElements extends React.Component {
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
hasSelectedNode: hasSelectedNodeFn(state),
|
||||
layoutNodes: layoutNodesSelector(state),
|
||||
layoutEdges: layoutEdgesSelector(state),
|
||||
selectedScale: selectedScaleSelector(state),
|
||||
isAnimated: !graphExceedsComplexityThreshSelector(state),
|
||||
highlightedNodeIds: highlightedNodeIdsSelector(state),
|
||||
highlightedEdgeIds: highlightedEdgeIdsSelector(state),
|
||||
selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state),
|
||||
searchNodeMatches: searchNodeMatchesSelector(state),
|
||||
neighborsOfSelectedNode: getAdjacentNodes(state),
|
||||
nodeNetworks: nodeNetworksSelector(state),
|
||||
nodeMetric: nodeMetricSelector(state),
|
||||
selectedScale: selectedScaleSelector(state),
|
||||
searchQuery: state.get('searchQuery'),
|
||||
selectedNetwork: state.get('selectedNetwork'),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
mouseOverNodeId: state.get('mouseOverNodeId'),
|
||||
mouseOverEdgeId: state.get('mouseOverEdgeId'),
|
||||
contrastMode: state.get('contrastMode'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Map as makeMap } from 'immutable';
|
||||
|
||||
import { nodeMetricSelector } from '../selectors/node-metric';
|
||||
import { searchNodeMatchesSelector } from '../selectors/search';
|
||||
import { highlightedNodeIdsSelector } from '../selectors/graph-view/decorators';
|
||||
import { nodeNetworksSelector, selectedNetworkNodesIdsSelector } from '../selectors/node-networks';
|
||||
import { getAdjacentNodes } from '../utils/topology-utils';
|
||||
import NodeContainer from './node-container';
|
||||
|
||||
class NodesChartNodes extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.nodeDisplayLayer = this.nodeDisplayLayer.bind(this);
|
||||
// Node decorators
|
||||
// TODO: Consider moving some of these one level up (or even to global selectors) so that
|
||||
// other components, like NodesChartEdges, could read more info directly from the nodes.
|
||||
this.nodeHighlightedDecorator = this.nodeHighlightedDecorator.bind(this);
|
||||
this.nodeFocusedDecorator = this.nodeFocusedDecorator.bind(this);
|
||||
this.nodeBlurredDecorator = this.nodeBlurredDecorator.bind(this);
|
||||
this.nodeMatchesDecorator = this.nodeMatchesDecorator.bind(this);
|
||||
this.nodeNetworksDecorator = this.nodeNetworksDecorator.bind(this);
|
||||
this.nodeMetricDecorator = this.nodeMetricDecorator.bind(this);
|
||||
this.nodeScaleDecorator = this.nodeScaleDecorator.bind(this);
|
||||
}
|
||||
|
||||
nodeHighlightedDecorator(node) {
|
||||
const nodeSelected = (this.props.selectedNodeId === node.get('id'));
|
||||
const nodeHighlighted = this.props.highlightedNodeIds.has(node.get('id'));
|
||||
return node.set('highlighted', nodeHighlighted || nodeSelected);
|
||||
}
|
||||
|
||||
nodeFocusedDecorator(node) {
|
||||
const nodeSelected = (this.props.selectedNodeId === node.get('id'));
|
||||
const isNeighborOfSelected = this.props.neighborsOfSelectedNode.includes(node.get('id'));
|
||||
return node.set('focused', nodeSelected || isNeighborOfSelected);
|
||||
}
|
||||
|
||||
nodeBlurredDecorator(node) {
|
||||
const belongsToNetwork = this.props.selectedNetworkNodesIds.contains(node.get('id'));
|
||||
const noMatches = this.props.searchNodeMatches.get(node.get('id'), makeMap()).isEmpty();
|
||||
const notMatched = (this.props.searchQuery && !node.get('highlighted') && noMatches);
|
||||
const notFocused = (this.props.selectedNodeId && !node.get('focused'));
|
||||
const notInNetwork = (this.props.selectedNetwork && !belongsToNetwork);
|
||||
return node.set('blurred', notMatched || notFocused || notInNetwork);
|
||||
}
|
||||
|
||||
nodeMatchesDecorator(node) {
|
||||
return node.set('matches', this.props.searchNodeMatches.get(node.get('id')));
|
||||
}
|
||||
|
||||
nodeNetworksDecorator(node) {
|
||||
return node.set('networks', this.props.nodeNetworks.get(node.get('id')));
|
||||
}
|
||||
|
||||
nodeMetricDecorator(node) {
|
||||
return node.set('metric', this.props.nodeMetric.get(node.get('id')));
|
||||
}
|
||||
|
||||
nodeScaleDecorator(node) {
|
||||
return node.set('scale', node.get('focused') ? this.props.selectedScale : 1);
|
||||
}
|
||||
|
||||
// make sure blurred nodes are in the background
|
||||
nodeDisplayLayer(node) {
|
||||
if (node.get('id') === this.props.mouseOverNodeId) {
|
||||
return 3;
|
||||
}
|
||||
if (node.get('blurred') && !node.get('focused')) {
|
||||
return 0;
|
||||
}
|
||||
if (node.get('highlighted')) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { layoutNodes, isAnimated, contrastMode } = this.props;
|
||||
|
||||
const nodesToRender = layoutNodes.toIndexedSeq()
|
||||
.map(this.nodeHighlightedDecorator)
|
||||
.map(this.nodeFocusedDecorator)
|
||||
.map(this.nodeBlurredDecorator)
|
||||
.map(this.nodeMatchesDecorator)
|
||||
.map(this.nodeNetworksDecorator)
|
||||
.map(this.nodeMetricDecorator)
|
||||
.map(this.nodeScaleDecorator)
|
||||
.sortBy(this.nodeDisplayLayer);
|
||||
|
||||
return (
|
||||
<g className="nodes-chart-nodes">
|
||||
{nodesToRender.map(node => (
|
||||
<NodeContainer
|
||||
matches={node.get('matches')}
|
||||
networks={node.get('networks')}
|
||||
metric={node.get('metric')}
|
||||
blurred={node.get('blurred')}
|
||||
focused={node.get('focused')}
|
||||
highlighted={node.get('highlighted')}
|
||||
shape={node.get('shape')}
|
||||
stack={node.get('stack')}
|
||||
key={node.get('id')}
|
||||
id={node.get('id')}
|
||||
label={node.get('label')}
|
||||
labelMinor={node.get('labelMinor')}
|
||||
pseudo={node.get('pseudo')}
|
||||
rank={node.get('rank')}
|
||||
dx={node.get('x')}
|
||||
dy={node.get('y')}
|
||||
scale={node.get('scale')}
|
||||
isAnimated={isAnimated}
|
||||
contrastMode={contrastMode}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
nodeMetric: nodeMetricSelector(state),
|
||||
nodeNetworks: nodeNetworksSelector(state),
|
||||
searchNodeMatches: searchNodeMatchesSelector(state),
|
||||
selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state),
|
||||
neighborsOfSelectedNode: getAdjacentNodes(state),
|
||||
highlightedNodeIds: highlightedNodeIdsSelector(state),
|
||||
mouseOverNodeId: state.get('mouseOverNodeId'),
|
||||
selectedNetwork: state.get('selectedNetwork'),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
searchQuery: state.get('searchQuery'),
|
||||
contrastMode: state.get('contrastMode')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
)(NodesChartNodes);
|
||||
@@ -6,3 +6,13 @@ export const EDGE_ID_SEPARATOR = '---';
|
||||
export const GRAPH_VIEW_MODE = 'topo';
|
||||
export const TABLE_VIEW_MODE = 'grid';
|
||||
export const RESOURCE_VIEW_MODE = 'resource';
|
||||
|
||||
// Named constants to avoid typos that would result in hard-to-detect bugs.
|
||||
export const BLURRED_EDGES_LAYER = 'blurred-edges';
|
||||
export const BLURRED_NODES_LAYER = 'blurred-nodes';
|
||||
export const NORMAL_EDGES_LAYER = 'normal-edges';
|
||||
export const NORMAL_NODES_LAYER = 'normal-nodes';
|
||||
export const HIGHLIGHTED_EDGES_LAYER = 'highlighted-edges';
|
||||
export const HIGHLIGHTED_NODES_LAYER = 'highlighted-nodes';
|
||||
export const HOVERED_EDGES_LAYER = 'hovered-edges';
|
||||
export const HOVERED_NODES_LAYER = 'hovered-nodes';
|
||||
|
||||
@@ -399,7 +399,7 @@
|
||||
fill: $text-secondary-color;
|
||||
}
|
||||
|
||||
.nodes-chart-nodes .node {
|
||||
.nodes-chart-elements .node {
|
||||
transition: opacity .2s $base-ease;
|
||||
text-align: center;
|
||||
|
||||
@@ -500,10 +500,6 @@
|
||||
opacity: $edge-opacity-blurred;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
animation: focusing 1.5s ease-in-out;
|
||||
}
|
||||
|
||||
.link {
|
||||
fill: none;
|
||||
stroke: $edge-color;
|
||||
@@ -1662,16 +1658,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes focusing {
|
||||
0% {
|
||||
opacity: 0;
|
||||
} 33% {
|
||||
opacity: 0.2;
|
||||
} 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blinking {
|
||||
0%, 50%, 100% {
|
||||
opacity: 1.0;
|
||||
|
||||
@@ -21,7 +21,7 @@ function clickIfVisible(list, index) {
|
||||
|
||||
function selectNode(browser) {
|
||||
debug('select node');
|
||||
return browser.elementByCssSelector('.nodes-chart-nodes .node:nth-child(1) > g', function(err, el) {
|
||||
return browser.elementByCssSelector('.nodes-chart-elements .node:nth-child(1) > g', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user