Fix node blurring for network hover

Fixes edge focus/blurring when selecting networks

Configurable arc size

net-view variant: Smaller arc (just the top)

net-view revision: Add white container around arcs.

To join them together in a sense. Trying to avoid the edge/arc position
association. Not sure if this really helps

net-view variations: shadows.

bg fill, align network rotation w/ rank.

Cute little dots

Stacked lines

Rounded rects

Slightly thicker "pills" to repr networks

Handle edge case by making line longer

Fix network bar offset
This commit is contained in:
David Kaltschmidt
2016-05-25 12:36:32 +02:00
committed by Simon Howe
parent 478a4a6d66
commit b1cd16d92d
7 changed files with 81 additions and 48 deletions

View File

@@ -5,41 +5,46 @@ import { getNodeColor } from '../utils/color-utils';
import { isContrastMode } from '../utils/contrast-utils';
const h = 5;
const padding = 0.05;
const offset = Math.PI;
const arc = d3.svg.arc()
.startAngle(d => d.startAngle + offset)
.endAngle(d => d.endAngle + offset);
const arcScale = d3.scale.linear()
.range([Math.PI * 0.25 + padding, Math.PI * 1.75 - padding]);
const rx = 1;
const ry = rx;
const labelOffset = 38;
function NodeNetworksOverlay({size, stack, networks = makeList()}) {
arcScale.domain([0, networks.size]);
const radius = size * 0.9;
const r = size * 0.5;
const offset = r + labelOffset;
const w = Math.max(size, (size / 4) * networks.size);
const x = d3.scale.ordinal()
.domain(networks.map((n, i) => i).toJS())
.rangeBands([w * -0.5, w * 0.5], padding, 0);
const paths = networks.map((n, i) => {
const d = arc({
padAngle: 0.05,
innerRadius: radius,
outerRadius: radius + 4,
startAngle: arcScale(i),
endAngle: arcScale(i + 1)
});
return (<path d={d} style={{fill: getNodeColor(n)}} key={n} />);
});
const bars = networks.map((n, i) => (
<rect
x={x(i)}
y={offset}
width={x.rangeBand()}
height={h}
rx={rx}
ry={ry}
className="node-network"
style={{
fill: getNodeColor(n.get('colorKey'))
}}
key={n.get('id')}
/>
));
let transform = '';
if (stack) {
const contrastMode = isContrastMode();
const [dx, dy] = contrastMode ? [0, 8] : [0, 5];
const [dx, dy] = contrastMode ? [0, 8] : [0, 0];
transform = `translate(${dx}, ${dy * -1.5})`;
}
return (
<g transform={transform}>
{paths.toJS()}
{bars.toJS()}
</g>
);
}

View File

@@ -107,9 +107,6 @@ class Node extends React.Component {
<g className={nodeClassName} transform={transform}
onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
{showingNetworks && <NodeNetworksOverlay size={size} networks={networks}
stack={stack} />}
{useSvgLabels ?
svgLabels(label, subLabel, labelClassName, subLabelClassName, labelOffsetY) :
@@ -133,6 +130,9 @@ class Node extends React.Component {
color={color}
{...this.props} />
</g>
{showingNetworks && <NodeNetworksOverlay id={this.props.id} size={size}
networks={networks} stack={stack} />}
</g>
);
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { Map as makeMap } from 'immutable';
import { Map as makeMap, List as makeList } from 'immutable';
import { hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils';
import EdgeContainer from './edge-container';
@@ -9,7 +9,7 @@ class NodesChartEdges extends React.Component {
render() {
const { hasSelectedNode, highlightedEdgeIds, layoutEdges,
layoutPrecision, searchNodeMatches = makeMap(), searchQuery,
selectedNodeId } = this.props;
selectedNodeId, selectedNetwork, selectedNetworkNodes } = this.props;
return (
<g className="nodes-chart-edges">
@@ -18,10 +18,16 @@ class NodesChartEdges extends React.Component {
const targetSelected = selectedNodeId === edge.get('target');
const highlighted = highlightedEdgeIds.has(edge.get('id'));
const focused = hasSelectedNode && (sourceSelected || targetSelected);
const blurred = !(highlightedEdgeIds.size > 0 && highlighted)
&& ((hasSelectedNode && !sourceSelected && !targetSelected)
|| !focused && searchQuery && !(searchNodeMatches.has(edge.get('source'))
&& searchNodeMatches.has(edge.get('target'))));
const otherNodesSelected = hasSelectedNode && !sourceSelected && !targetSelected;
const noMatches = searchQuery &&
!(searchNodeMatches.has(edge.get('source')) &&
searchNodeMatches.has(edge.get('target')));
const noSelectedNetworks = selectedNetwork &&
!(selectedNetworkNodes.contains(edge.get('source')) &&
selectedNetworkNodes.contains(edge.get('target')));
const blurred = !highlighted && (otherNodesSelected ||
!focused && noMatches ||
!focused && noSelectedNetworks);
return (
<EdgeContainer
@@ -49,7 +55,9 @@ function mapStateToProps(state) {
highlightedEdgeIds: state.get('highlightedEdgeIds'),
searchNodeMatches: state.getIn(['searchNodeMatches', currentTopologyId]),
searchQuery: state.get('searchQuery'),
selectedNodeId: state.get('selectedNodeId')
selectedNetwork: state.get('selectedNetwork'),
selectedNetworkNodes: state.get('networkNodes').get(state.get('selectedNetwork'), makeList()),
selectedNodeId: state.get('selectedNodeId'),
};
}

View File

@@ -24,7 +24,8 @@ class NodesChartNodes extends React.Component {
selectedNodeId && !node.get('focused')
|| searchQuery && !searchNodeMatches.has(node.get('id'))
&& !node.get('highlighted')
|| selectedNetwork && !(node.get('networks') || makeList()).contains(selectedNetwork));
|| selectedNetwork
&& !(node.get('networks') || makeList()).find(n => n.get('id') === selectedNetwork));
// make sure blurred nodes are in the background
const sortNodes = node => {

View File

@@ -17,7 +17,9 @@ const SHAPES = ['square', 'hexagon', 'heptagon', 'circle'];
const NODE_COUNTS = [1, 2, 3];
const STACK_VARIANTS = [false, true];
const METRIC_FILLS = [0, 0.1, 50, 99.9, 100];
const NETWORKS = ['be', 'fe', 'lb', 'db'];
const NETWORKS = [
'be', 'fe', 'zb', 'db', 're', 'gh', 'jk', 'lol', 'nw'
].map(n => ({id: n, label: n, colorKey: n}));
const LOREM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
@@ -45,7 +47,7 @@ const LABEL_PREFIXES = _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1)
const deltaAdd = (
name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1,
networks = ['fe', 'be']
networks = NETWORKS
) => ({
adjacency,
controls: {},
@@ -176,7 +178,7 @@ class DebugToolbar extends React.Component {
_.sample(SHAPES),
_.sample(STACK_VARIANTS),
_.sample(NODE_COUNTS),
sample(NETWORKS, 3)
sample(NETWORKS, 10)
))
}));

View File

@@ -33,6 +33,7 @@ export const initialState = makeMap({
hostname: '...',
mouseOverEdgeId: null,
mouseOverNodeId: null,
networkNodes: makeMap(),
nodeDetails: makeOrderedMap(), // nodeId -> details
nodes: makeOrderedMap(), // nodeId -> node
// nodes cache, infrequently updated, used for search
@@ -81,6 +82,24 @@ function processTopologies(state, nextTopologies) {
return state.mergeDeepIn(['topologies'], immNextTopologies);
}
function getNetworkNodes(nodes) {
const networks = {};
nodes.forEach(node => (node.get('networks') || makeList()).forEach(n => {
const networkId = n.get('id');
networks[networkId] = (networks[networkId] || []).concat([node.get('id')]);
}));
return fromJS(networks);
}
function getAvailableNetworks(nodes) {
return nodes
.valueSeq()
.flatMap(node => node.get('networks') || makeList())
.toSet()
.toList()
.sortBy(m => m.get('label'));
}
function setTopology(state, topologyId) {
state = state.set('currentTopology', findTopologyById(
state.get('topologies'), topologyId));
@@ -526,19 +545,15 @@ export function rootReducer(state = initialState, action) {
const networks = node.get('metadata')
.find(field => field.get('id') === 'docker_container_networks');
if (networks) {
return node.set('networks', fromJS(networks.get('value').split(', ')));
return node.set('networks', fromJS(
networks.get('value').split(', ').map(n => ({id: n, label: n, colorKey: n}))));
}
}
return node;
}));
state = state.set('availableNetworks', state.get('nodes')
.valueSeq()
.flatMap(node => node.get('networks') || makeList())
.toSet()
.toList()
.sort()
.map(n => makeMap({id: n, label: n})));
state = state.set('networkNodes', getNetworkNodes(state.get('nodes')));
state = state.set('availableNetworks', getAvailableNetworks(state.get('nodes')));
// optimize color coding for networks
const networkPrefix = longestCommonPrefix(state.get('availableNetworks')
@@ -547,7 +562,8 @@ export function rootReducer(state = initialState, action) {
if (networkPrefix) {
state = state.update('nodes',
nodes => nodes.map(node => node.update('networks',
networks => networks.map(n => n.substr(networkPrefix.length)))));
networks => networks.map(n => n.set('colorKey',
n.get('colorKey').substr(networkPrefix.length))))));
state = state.update('availableNetworks',
networks => networks.map(network => network
@@ -642,6 +658,7 @@ export function rootReducer(state = initialState, action) {
}
if (action.state.pinnedNetwork) {
state = state.set('pinnedNetwork', action.state.pinnedNetwork);
state = state.set('selectedNetwork', action.state.pinnedNetwork);
}
if (action.state.controlPipe) {
state = state.set('controlPipes', makeOrderedMap({

View File

@@ -341,9 +341,9 @@ h2 {
transition: opacity .5s @base-ease;
text-align: center;
.node-networks-overlay {
fill: none;
stroke: @background-darker-secondary-color;
.node-network {
// stroke: @background-lighter-color;
// stroke-width: 4px;
}
.node-label,