+
-
+
-
+
diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js
index 1be2a0f3b..5e5eb1e07 100644
--- a/client/app/scripts/charts/node.js
+++ b/client/app/scripts/charts/node.js
@@ -15,13 +15,8 @@ import NodeShapeHexagon from './node-shape-hexagon';
import NodeShapeHeptagon from './node-shape-heptagon';
import NodeShapeCloud from './node-shape-cloud';
import NodeNetworksOverlay from './node-networks-overlay';
-import { MIN_NODE_LABEL_SIZE, BASE_NODE_LABEL_SIZE, BASE_NODE_SIZE } from '../constants/styles';
-function labelFontSize(nodeSize) {
- return Math.max(MIN_NODE_LABEL_SIZE, (BASE_NODE_LABEL_SIZE / BASE_NODE_SIZE) * nodeSize);
-}
-
function stackedShape(Shape) {
const factory = React.createFactory(NodeShapeStack);
return props => factory(Object.assign({}, props, {shape: Shape}));
@@ -43,58 +38,72 @@ function getNodeShape({ shape, stack }) {
return stack ? stackedShape(nodeShape) : nodeShape;
}
-function svgLabels(label, subLabel, labelClassName, subLabelClassName, labelOffsetY) {
- return (
-
- {label}
-
- {subLabel}
-
-
- );
-}
class Node extends React.Component {
-
constructor(props, context) {
super(props, context);
- this.handleMouseClick = this.handleMouseClick.bind(this);
- this.handleMouseEnter = this.handleMouseEnter.bind(this);
- this.handleMouseLeave = this.handleMouseLeave.bind(this);
- this.saveShapeRef = this.saveShapeRef.bind(this);
this.state = {
hovered: false,
matched: false
};
+
+ this.handleMouseClick = this.handleMouseClick.bind(this);
+ this.handleMouseEnter = this.handleMouseEnter.bind(this);
+ this.handleMouseLeave = this.handleMouseLeave.bind(this);
+ this.saveShapeRef = this.saveShapeRef.bind(this);
}
componentWillReceiveProps(nextProps) {
// marks as matched only when search query changes
if (nextProps.searchQuery !== this.props.searchQuery) {
- this.setState({
- matched: nextProps.matched
- });
+ this.setState({ matched: nextProps.matched });
} else {
- this.setState({
- matched: false
- });
+ this.setState({ matched: false });
}
}
+ renderSvgLabels(labelClassName, subLabelClassName, labelOffsetY) {
+ const { label, subLabel } = this.props;
+ return (
+
+ {label}
+
+ {subLabel}
+
+
+ );
+ }
+
+ renderStandardLabels(labelClassName, subLabelClassName, labelOffsetY, mouseEvents) {
+ const { label, subLabel, blurred, matches = makeMap() } = this.props;
+ const matchedMetadata = matches.get('metadata', makeList());
+ const matchedParents = matches.get('parents', makeList());
+ const matchedNodeDetails = matchedMetadata.concat(matchedParents);
+
+ return (
+
+
+
+
+
+
+
+
+ {!blurred &&
}
+
+
+ );
+ }
+
render() {
- const { blurred, focused, highlighted, label, matches = makeMap(), networks,
- pseudo, rank, subLabel, scaleFactor, transform, exportingGraph,
- showingNetworks, stack } = this.props;
+ const { blurred, focused, highlighted, networks, pseudo, rank, label,
+ transform, exportingGraph, showingNetworks, stack } = this.props;
const { hovered, matched } = this.state;
- const nodeScale = focused ? this.props.selectedNodeScale : this.props.nodeScale;
const color = getNodeColor(rank, label, pseudo);
const truncate = !focused && !hovered;
- const labelWidth = nodeScale(scaleFactor * 3);
- const labelOffsetX = -labelWidth / 2;
- const labelDy = (showingNetworks && networks) ? 0.70 : 0.55;
- const labelOffsetY = nodeScale(labelDy * scaleFactor);
- const networkOffset = nodeScale(scaleFactor * 0.67);
+ const labelOffsetY = (showingNetworks && networks) ? 40 : 30;
+ const networkOffset = 0.67;
const nodeClassName = classnames('node', {
highlighted,
@@ -109,51 +118,25 @@ class Node extends React.Component {
const NodeShapeType = getNodeShape(this.props);
const useSvgLabels = exportingGraph;
- const size = nodeScale(scaleFactor);
- const fontSize = labelFontSize(size);
const mouseEvents = {
onClick: this.handleMouseClick,
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave,
};
- const matchedNodeDetails = matches.get('metadata', makeList())
- .concat(matches.get('parents', makeList()));
return (
-
- {useSvgLabels ?
-
- svgLabels(label, subLabel, labelClassName, subLabelClassName, labelOffsetY) :
-
-
-
-
-
-
-
-
-
- {!blurred &&
}
-
- }
+ {useSvgLabels || false ?
+ this.renderSvgLabels(labelClassName, subLabelClassName, labelOffsetY) :
+ this.renderStandardLabels(labelClassName, subLabelClassName, labelOffsetY, mouseEvents)}
-
+
{showingNetworks && }
diff --git a/client/app/scripts/charts/nodes-chart-edges.js b/client/app/scripts/charts/nodes-chart-edges.js
index 3e0d3ce90..350e5e02d 100644
--- a/client/app/scripts/charts/nodes-chart-edges.js
+++ b/client/app/scripts/charts/nodes-chart-edges.js
@@ -8,7 +8,7 @@ import EdgeContainer from './edge-container';
class NodesChartEdges extends React.Component {
render() {
const { hasSelectedNode, highlightedEdgeIds, layoutEdges,
- layoutPrecision, searchNodeMatches = makeMap(), searchQuery,
+ searchNodeMatches = makeMap(), searchQuery, isAnimated,
selectedNodeId, selectedNetwork, selectedNetworkNodes } = this.props;
return (
@@ -35,10 +35,10 @@ class NodesChartEdges extends React.Component {
id={edge.get('id')}
source={edge.get('source')}
target={edge.get('target')}
- points={edge.get('points')}
+ waypoints={edge.get('points')}
+ isAnimated={isAnimated}
blurred={blurred}
focused={focused}
- layoutPrecision={layoutPrecision}
highlighted={highlighted}
/>
);
diff --git a/client/app/scripts/charts/nodes-chart-elements.js b/client/app/scripts/charts/nodes-chart-elements.js
index 19b1aa996..df193d9fe 100644
--- a/client/app/scripts/charts/nodes-chart-elements.js
+++ b/client/app/scripts/charts/nodes-chart-elements.js
@@ -12,13 +12,12 @@ class NodesChartElements extends React.Component {
+ isAnimated={props.isAnimated} />
+ selectedScale={props.selectedScale}
+ isAnimated={props.isAnimated} />
);
}
diff --git a/client/app/scripts/charts/nodes-chart-nodes.js b/client/app/scripts/charts/nodes-chart-nodes.js
index 4bacb2d23..0477d0ae4 100644
--- a/client/app/scripts/charts/nodes-chart-nodes.js
+++ b/client/app/scripts/charts/nodes-chart-nodes.js
@@ -7,12 +7,9 @@ import NodeContainer from './node-container';
class NodesChartNodes extends React.Component {
render() {
- const { adjacentNodes, highlightedNodeIds, layoutNodes, layoutPrecision,
- mouseOverNodeId, nodeScale, scale, searchNodeMatches = makeMap(),
- searchQuery, selectedMetric, selectedNetwork, selectedNodeScale, selectedNodeId,
- topCardNode } = this.props;
-
- const zoomScale = scale;
+ const { adjacentNodes, highlightedNodeIds, layoutNodes, isAnimated, mouseOverNodeId, scale,
+ selectedScale, searchQuery, selectedMetric, selectedNetwork, selectedNodeId, topCardNode,
+ searchNodeMatches = makeMap() } = this.props;
// highlighter functions
const setHighlighted = node => node.set('highlighted',
@@ -73,12 +70,11 @@ class NodesChartNodes extends React.Component {
subLabel={node.get('subLabel')}
metric={metric(node)}
rank={node.get('rank')}
- layoutPrecision={layoutPrecision}
- selectedNodeScale={selectedNodeScale}
- nodeScale={nodeScale}
- zoomScale={zoomScale}
+ isAnimated={isAnimated}
+ magnified={node.get('focused') ? selectedScale / scale : 1}
dx={node.get('x')}
- dy={node.get('y')} />)}
+ dy={node.get('y')}
+ />)}
);
}
diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js
index 72869150f..1d8f48a74 100644
--- a/client/app/scripts/charts/nodes-chart.js
+++ b/client/app/scripts/charts/nodes-chart.js
@@ -5,14 +5,14 @@ import { assign, pick, includes } from 'lodash';
import { Map as makeMap, fromJS } from 'immutable';
import timely from 'timely';
-import { scaleThreshold, scaleLinear } from 'd3-scale';
+import { scaleThreshold } from 'd3-scale';
import { event as d3Event, select } from 'd3-selection';
import { zoom, zoomIdentity } from 'd3-zoom';
import { nodeAdjacenciesSelector, adjacentNodesSelector } from '../selectors/chartSelectors';
import { clickBackground } from '../actions/app-actions';
import { EDGE_ID_SEPARATOR } from '../constants/naming';
-import { MIN_NODE_SIZE, DETAILS_PANEL_WIDTH, MAX_NODE_SIZE } from '../constants/styles';
+import { DETAILS_PANEL_WIDTH, NODE_BASE_SIZE } from '../constants/styles';
import Logo from '../components/logo';
import { doLayout } from './nodes-layout';
import NodesChartElements from './nodes-chart-elements';
@@ -20,32 +20,20 @@ import { getActiveTopologyOptions } from '../utils/topology-utils';
const log = debug('scope:nodes-chart');
-const ZOOM_CACHE_FIELDS = ['scale', 'panTranslateX', 'panTranslateY'];
+const ZOOM_CACHE_FIELDS = ['scale', 'panTranslateX', 'panTranslateY', 'minScale', 'maxScale'];
// make sure circular layouts a bit denser with 3-6 nodes
const radiusDensity = scaleThreshold()
.domain([3, 6])
.range([2.5, 3.5, 3]);
-/**
- * dynamic coords precision based on topology size
- */
-function getLayoutPrecision(nodesCount) {
- let precision;
- if (nodesCount >= 50) {
- precision = 0;
- } else if (nodesCount > 20) {
- precision = 1;
- } else if (nodesCount > 10) {
- precision = 2;
- } else {
- precision = 3;
- }
-
- return precision;
-}
+const emptyLayoutState = {
+ nodes: makeMap(),
+ edges: makeMap(),
+};
+// EDGES
function initEdges(nodes) {
let edges = makeMap();
@@ -76,24 +64,45 @@ function initEdges(nodes) {
}
-function getNodeScale(nodesCount, width, height) {
- const expanse = Math.min(height, width);
- const nodeSize = expanse / 3; // single node should fill a third of the screen
- const maxNodeSize = Math.min(MAX_NODE_SIZE, expanse / 10);
- const normalizedNodeSize = Math.max(MIN_NODE_SIZE,
- Math.min(nodeSize / Math.sqrt(nodesCount), maxNodeSize));
+// ZOOM STATE
+function getLayoutDefaultZoom(layoutNodes, width, height) {
+ const xMin = layoutNodes.minBy(n => n.get('x')).get('x');
+ const xMax = layoutNodes.maxBy(n => n.get('x')).get('x');
+ const yMin = layoutNodes.minBy(n => n.get('y')).get('y');
+ const yMax = layoutNodes.maxBy(n => n.get('y')).get('y');
- return scaleLinear().range([0, normalizedNodeSize]);
+ const xFactor = width / (xMax - xMin);
+ const yFactor = height / (yMax - yMin);
+ const scale = Math.min(xFactor, yFactor);
+
+ return {
+ translateX: (width - ((xMax + xMin) * scale)) / 2,
+ translateY: (height - ((yMax + yMin) * scale)) / 2,
+ scale,
+ };
+}
+
+function defaultZoomState(props, state) {
+ // adjust layout based on viewport
+ const width = state.width - props.margins.left - props.margins.right;
+ const height = state.height - props.margins.top - props.margins.bottom;
+
+ const { translateX, translateY, scale } = getLayoutDefaultZoom(state.nodes, width, height);
+
+ return {
+ scale,
+ minScale: scale / 5,
+ maxScale: Math.min(width, height) / NODE_BASE_SIZE / 3,
+ panTranslateX: translateX + props.margins.left,
+ panTranslateY: translateY + props.margins.top,
+ };
}
+// LAYOUT STATE
function updateLayout(width, height, nodes, baseOptions) {
- const nodeScale = getNodeScale(nodes.size, width, height);
const edges = initEdges(nodes);
-
- const options = Object.assign({}, baseOptions, {
- scale: nodeScale,
- });
+ const options = Object.assign({}, baseOptions);
const timedLayouter = timely(doLayout);
const graph = timedLayouter(nodes, edges, options);
@@ -108,13 +117,52 @@ function updateLayout(width, height, nodes, baseOptions) {
py: node.get('y')
}));
- const layoutEdges = graph.edges
- .map(edge => edge.set('ppoints', edge.get('points')));
+ const layoutEdges = graph.edges.map(edge => edge.set('ppoints', edge.get('points')));
- return { layoutNodes, layoutEdges, layoutWidth: graph.width, layoutHeight: graph.height };
+ return { layoutNodes, layoutEdges };
+}
+
+function updatedGraphState(props, state) {
+ if (props.nodes.size === 0) {
+ return emptyLayoutState;
+ }
+
+ const options = {
+ width: state.width,
+ height: state.height,
+ margins: props.margins,
+ forceRelayout: props.forceRelayout,
+ topologyId: props.topologyId,
+ topologyOptions: props.topologyOptions,
+ };
+
+ const { layoutNodes, layoutEdges } =
+ updateLayout(state.width, state.height, props.nodes, options);
+
+ return {
+ nodes: layoutNodes,
+ edges: layoutEdges,
+ };
+}
+
+function restoredLayout(state) {
+ const restoredNodes = state.nodes.map(node => node.merge({
+ x: node.get('px'),
+ y: node.get('py')
+ }));
+
+ const restoredEdges = state.edges.map(edge => (
+ edge.has('ppoints') ? edge.set('points', edge.get('ppoints')) : edge
+ ));
+
+ return {
+ nodes: restoredNodes,
+ edges: restoredEdges,
+ };
}
+// SELECTED NODE
function centerSelectedNode(props, state) {
let stateNodes = state.nodes;
let stateEdges = state.edges;
@@ -179,10 +227,10 @@ function centerSelectedNode(props, state) {
});
// auto-scale node size for selected nodes
- const selectedNodeScale = getNodeScale(adjacentNodes.size, state.width, state.height);
+ // const selectedNodeScale = getNodeScale(adjacentNodes.size, state.width, state.height);
return {
- selectedNodeScale,
+ selectedScale: 1,
edges: stateEdges,
nodes: stateNodes
};
@@ -193,27 +241,26 @@ class NodesChart extends React.Component {
constructor(props, context) {
super(props, context);
-
- this.handleMouseClick = this.handleMouseClick.bind(this);
- this.zoomed = this.zoomed.bind(this);
-
- this.state = {
- edges: makeMap(),
- nodes: makeMap(),
- nodeScale: scaleLinear(),
+ this.state = Object.assign({
+ scale: 1,
+ minScale: 1,
+ maxScale: 1,
panTranslateX: 0,
panTranslateY: 0,
- scale: 1,
- selectedNodeScale: scaleLinear(),
- hasZoomed: false,
+ selectedScale: 1,
height: props.height || 0,
width: props.width || 0,
zoomCache: {},
- };
+ }, emptyLayoutState);
+
+ this.handleMouseClick = this.handleMouseClick.bind(this);
+ this.zoomed = this.zoomed.bind(this);
}
componentWillMount() {
- const state = this.updateGraphState(this.props, this.state);
+ const state = updatedGraphState(this.props, this.state);
+ // debugger;
+ // assign(state, this.restoreZoomState(this.props, Object.assign(this.state, state)));
this.setState(state);
}
@@ -221,25 +268,11 @@ class NodesChart extends React.Component {
// gather state, setState should be called only once here
const state = assign({}, this.state);
+ const topologyChanged = nextProps.topologyId !== this.props.topologyId;
+
// wipe node states when showing different topology
- if (nextProps.topologyId !== this.props.topologyId) {
- // re-apply cached canvas zoom/pan to d3 behavior (or set the default values)
- const defaultZoom = { scale: 1, panTranslateX: 0, panTranslateY: 0, hasZoomed: false };
- const nextZoom = this.state.zoomCache[nextProps.topologyId] || defaultZoom;
- if (nextZoom) {
- this.setZoom(nextZoom);
- }
-
- // saving previous zoom state
- const prevZoom = pick(this.state, ZOOM_CACHE_FIELDS);
- const zoomCache = assign({}, this.state.zoomCache);
- zoomCache[this.props.topologyId] = prevZoom;
-
- // clear canvas and apply zoom state
- assign(state, nextZoom, { zoomCache }, {
- nodes: makeMap(),
- edges: makeMap()
- });
+ if (topologyChanged) {
+ assign(state, emptyLayoutState);
}
// reset layout dimensions only when forced
@@ -247,29 +280,50 @@ class NodesChart extends React.Component {
state.width = nextProps.forceRelayout ? nextProps.width : (state.width || nextProps.width);
if (nextProps.forceRelayout || nextProps.nodes !== this.props.nodes) {
- assign(state, this.updateGraphState(nextProps, state));
+ assign(state, updatedGraphState(nextProps, state));
}
- if (this.props.selectedNodeId !== nextProps.selectedNodeId) {
- assign(state, this.restoreLayout(state));
- }
- if (nextProps.selectedNodeId) {
- assign(state, centerSelectedNode(nextProps, state));
+ console.log(`Prepare ${nextProps.nodes.size}`);
+ if (nextProps.nodes.size > 0) {
+ console.log(state.zoomCache);
+ assign(state, this.restoreZoomState(nextProps, state));
}
+ // if (this.props.selectedNodeId !== nextProps.selectedNodeId) {
+ // // undo any pan/zooming that might have happened
+ // this.setZoom(state);
+ // assign(state, restoredLayout(state));
+ // }
+ //
+ // if (nextProps.selectedNodeId) {
+ // assign(state, centerSelectedNode(nextProps, state));
+ // }
+
+ if (topologyChanged) {
+ // saving previous zoom state
+ const prevZoom = pick(this.state, ZOOM_CACHE_FIELDS);
+ const zoomCache = assign({}, this.state.zoomCache);
+ zoomCache[this.props.topologyId] = prevZoom;
+ assign(state, { zoomCache });
+ }
+
+ // console.log(topologyChanged);
+ // console.log(state);
this.setState(state);
}
componentDidMount() {
// distinguish pan/zoom from click
this.isZooming = false;
+ // debugger;
this.zoom = zoom()
- .scaleExtent([0.1, 2])
+ .scaleExtent([this.state.minScale, this.state.maxScale])
.on('zoom', this.zoomed);
this.svg = select('.nodes-chart svg');
this.svg.call(this.zoom);
+ // this.setZoom(this.state);
}
componentWillUnmount() {
@@ -282,15 +336,19 @@ class NodesChart extends React.Component {
.on('touchstart.zoom', null);
}
+ isSmallTopology() {
+ return this.state.nodes.size < 100;
+ }
+
render() {
const { edges, nodes, panTranslateX, panTranslateY, scale } = this.state;
+ console.log(`Render ${nodes.size}`);
// not passing translates into child components for perf reasons, use getTranslate instead
const translate = [panTranslateX, panTranslateY];
const transform = `translate(${translate}) scale(${scale})`;
const svgClassNames = this.props.isEmpty ? 'hide' : '';
- const layoutPrecision = getLayoutPrecision(nodes.size);
return (
+ isAnimated={this.isSmallTopology()} />
);
@@ -320,74 +377,28 @@ class NodesChart extends React.Component {
}
}
- restoreLayout(state) {
- // undo any pan/zooming that might have happened
- this.setZoom(state);
-
- const nodes = state.nodes.map(node => node.merge({
- x: node.get('px'),
- y: node.get('py')
- }));
-
- const edges = state.edges.map((edge) => {
- if (edge.has('ppoints')) {
- return edge.set('points', edge.get('ppoints'));
- }
- return edge;
- });
-
- return { edges, nodes };
- }
-
- updateGraphState(props, state) {
- if (props.nodes.size === 0) {
- return {
- nodes: makeMap(),
- edges: makeMap()
- };
+ restoreZoomState(props, state) {
+ // re-apply cached canvas zoom/pan to d3 behavior (or set the default values)
+ const nextZoom = state.zoomCache[props.topologyId] || defaultZoomState(props, state);
+ if (this.zoom) {
+ this.zoom = this.zoom.scaleExtent([nextZoom.minScale, nextZoom.maxScale]);
+ this.setZoom(nextZoom);
}
- const options = {
- width: state.width,
- height: state.height,
- margins: props.margins,
- forceRelayout: props.forceRelayout,
- topologyId: props.topologyId,
- topologyOptions: props.topologyOptions,
- };
-
- const { layoutNodes, layoutEdges, layoutWidth, layoutHeight } = updateLayout(
- state.width, state.height, props.nodes, options);
- //
- // adjust layout based on viewport
- const xFactor = (state.width - props.margins.left - props.margins.right) / layoutWidth;
- const yFactor = state.height / layoutHeight;
- const zoomFactor = Math.min(xFactor, yFactor);
- let zoomScale = state.scale;
-
- if (this.svg && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
- zoomScale = zoomFactor;
- }
-
- return {
- scale: zoomScale,
- nodes: layoutNodes,
- edges: layoutEdges,
- nodeScale: getNodeScale(props.nodes.size, state.width, state.height),
- };
+ return nextZoom;
}
zoomed() {
this.isZooming = true;
- // dont pan while node is selected
+ // don't pan while node is selected
if (!this.props.selectedNodeId) {
this.setState({
- hasZoomed: true,
panTranslateX: d3Event.transform.x,
panTranslateY: d3Event.transform.y,
scale: d3Event.transform.k
});
}
+ // console.log(d3Event.transform);
}
setZoom(newZoom) {
diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js
index efd356541..635fb8092 100644
--- a/client/app/scripts/charts/nodes-layout.js
+++ b/client/app/scripts/charts/nodes-layout.js
@@ -2,6 +2,7 @@ import dagre from 'dagre';
import debug from 'debug';
import { fromJS, Map as makeMap, Set as ImmSet } from 'immutable';
+import { NODE_BASE_SIZE } from '../constants/styles';
import { EDGE_ID_SEPARATOR } from '../constants/naming';
import { featureIsEnabledAny } from '../utils/feature-utils';
import { buildTopologyCacheId, updateNodeDegrees } from '../utils/topology-utils';
@@ -12,10 +13,9 @@ const topologyCaches = {};
export const DEFAULT_WIDTH = 800;
export const DEFAULT_HEIGHT = DEFAULT_WIDTH / 2;
export const DEFAULT_MARGINS = {top: 0, left: 0};
-const DEFAULT_SCALE = val => val * 2;
-const NODE_SIZE_FACTOR = 1;
-const NODE_SEPARATION_FACTOR = 2.0;
-const RANK_SEPARATION_FACTOR = 3.0;
+const NODE_SIZE_FACTOR = NODE_BASE_SIZE;
+const NODE_SEPARATION_FACTOR = 2 * NODE_BASE_SIZE;
+const RANK_SEPARATION_FACTOR = 3 * NODE_BASE_SIZE;
let layoutRuns = 0;
let layoutRunsTrivial = 0;
@@ -34,19 +34,16 @@ function fromGraphNodeId(encodedId) {
* @param {Object} graph dagre graph instance
* @param {Map} imNodes new node set
* @param {Map} imEdges new edge set
- * @param {Object} opts dimensions, scales, etc.
* @return {Object} Layout with nodes, edges, dimensions
*/
-function runLayoutEngine(graph, imNodes, imEdges, opts) {
+function runLayoutEngine(graph, imNodes, imEdges) {
let nodes = imNodes;
let edges = imEdges;
- const options = opts || {};
- const scale = options.scale || DEFAULT_SCALE;
- const ranksep = scale(RANK_SEPARATION_FACTOR);
- const nodesep = scale(NODE_SEPARATION_FACTOR);
- const nodeWidth = scale(NODE_SIZE_FACTOR);
- const nodeHeight = scale(NODE_SIZE_FACTOR);
+ const ranksep = RANK_SEPARATION_FACTOR;
+ const nodesep = NODE_SEPARATION_FACTOR;
+ const nodeWidth = NODE_SIZE_FACTOR;
+ const nodeHeight = NODE_SIZE_FACTOR;
// configure node margins
graph.setGraph({
@@ -154,12 +151,10 @@ function setSimpleEdgePoints(edge, nodeCache) {
* @param {object} opts Options
* @return {object} new layout object
*/
-export function doLayoutNewNodesOfExistingRank(layout, nodeCache, opts) {
+export function doLayoutNewNodesOfExistingRank(layout, nodeCache) {
const result = Object.assign({}, layout);
- const options = opts || {};
- const scale = options.scale || DEFAULT_SCALE;
- const nodesep = scale(NODE_SEPARATION_FACTOR);
- const nodeWidth = scale(NODE_SIZE_FACTOR);
+ const nodesep = NODE_SEPARATION_FACTOR;
+ const nodeWidth = NODE_SIZE_FACTOR;
// determine new nodes
const oldNodes = ImmSet.fromKeys(nodeCache);
@@ -200,11 +195,10 @@ function layoutSingleNodes(layout, opts) {
const result = Object.assign({}, layout);
const options = opts || {};
const margins = options.margins || DEFAULT_MARGINS;
- const scale = options.scale || DEFAULT_SCALE;
- const ranksep = scale(RANK_SEPARATION_FACTOR) / 2; // dagre splits it in half
- const nodesep = scale(NODE_SEPARATION_FACTOR);
- const nodeWidth = scale(NODE_SIZE_FACTOR);
- const nodeHeight = scale(NODE_SIZE_FACTOR);
+ const ranksep = RANK_SEPARATION_FACTOR / 2; // dagre splits it in half
+ const nodesep = NODE_SEPARATION_FACTOR;
+ const nodeWidth = NODE_SIZE_FACTOR;
+ const nodeHeight = NODE_SIZE_FACTOR;
const graphHeight = layout.graphHeight || layout.height;
const graphWidth = layout.graphWidth || layout.width;
const aspectRatio = graphHeight ? graphWidth / graphHeight : 1;
@@ -271,50 +265,6 @@ function layoutSingleNodes(layout, opts) {
return result;
}
-/**
- * Shifts all coordinates of node and edge points to make the layout more centered
- * @param {Object} layout Layout
- * @param {Object} opts Options with width and margins
- * @return {Object} modified layout
- */
-export function shiftLayoutToCenter(layout, opts) {
- const result = Object.assign({}, layout);
- const options = opts || {};
- const margins = options.margins || DEFAULT_MARGINS;
- const width = options.width || DEFAULT_WIDTH;
- const height = options.height || DEFAULT_HEIGHT;
-
- let offsetX = 0 + margins.left;
- let offsetY = 0 + margins.top;
-
- if (layout.width < width) {
- const xMin = layout.nodes.minBy(n => n.get('x'));
- const xMax = layout.nodes.maxBy(n => n.get('x'));
- offsetX = ((width - (xMin.get('x') + xMax.get('x'))) / 2) + margins.left;
- }
- if (layout.height < height) {
- const yMin = layout.nodes.minBy(n => n.get('y'));
- const yMax = layout.nodes.maxBy(n => n.get('y'));
- offsetY = ((height - (yMin.get('y') + yMax.get('y'))) / 2) + margins.top;
- }
-
- if (offsetX || offsetY) {
- result.nodes = layout.nodes.map(node => node.merge({
- x: node.get('x') + offsetX,
- y: node.get('y') + offsetY
- }));
-
- result.edges = layout.edges.map(edge => edge.update('points',
- points => points.map(point => point.merge({
- x: point.get('x') + offsetX,
- y: point.get('y') + offsetY
- }))
- ));
- }
-
- return result;
-}
-
/**
* Determine if nodes were added between node sets
* @param {Map} nodes new Map of nodes
@@ -478,17 +428,16 @@ export function doLayout(immNodes, immEdges, opts) {
log('skip layout, used rank-based insertion');
layout = cloneLayout(cachedLayout, nodesWithDegrees, immEdges);
layout = copyLayoutProperties(layout, nodeCache, edgeCache);
- layout = doLayoutNewNodesOfExistingRank(layout, nodeCache, opts);
+ layout = doLayoutNewNodesOfExistingRank(layout, nodeCache);
} else {
const graph = cache.graph;
- layout = runLayoutEngine(graph, nodesWithDegrees, immEdges, opts);
+ layout = runLayoutEngine(graph, nodesWithDegrees, immEdges);
if (!layout) {
return layout;
}
}
layout = layoutSingleNodes(layout, opts);
- layout = shiftLayoutToCenter(layout, opts);
}
// cache results
diff --git a/client/app/scripts/components/sparkline.js b/client/app/scripts/components/sparkline.js
index 49b22688d..9a47dc7df 100644
--- a/client/app/scripts/components/sparkline.js
+++ b/client/app/scripts/components/sparkline.js
@@ -6,7 +6,6 @@ import { line, curveLinear } from 'd3-shape';
import { scaleLinear } from 'd3-scale';
import { formatMetricSvg } from '../utils/string-utils';
-import { round } from '../utils/math-utils';
export default class Sparkline extends React.Component {
@@ -64,7 +63,7 @@ export default class Sparkline extends React.Component {
const min = formatMetricSvg(d3Min(data, d => d.value), this.props);
const max = formatMetricSvg(d3Max(data, d => d.value), this.props);
const mean = formatMetricSvg(d3Mean(data, d => d.value), this.props);
- const title = `Last ${round((lastDate - firstDate) / 1000)} seconds, ` +
+ const title = `Last ${Math.round((lastDate - firstDate) / 1000)} seconds, ` +
`${data.length} samples, min: ${min}, max: ${max}, mean: ${mean}`;
return {title, lastX, lastY, data};
diff --git a/client/app/scripts/constants/animation.js b/client/app/scripts/constants/animation.js
new file mode 100644
index 000000000..e24d70770
--- /dev/null
+++ b/client/app/scripts/constants/animation.js
@@ -0,0 +1,2 @@
+
+export const NODES_SPRING_ANIMATION_CONFIG = { stiffness: 80, damping: 20, precision: 0.1 };
diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js
index 909b9a311..c81285f49 100644
--- a/client/app/scripts/constants/styles.js
+++ b/client/app/scripts/constants/styles.js
@@ -18,14 +18,13 @@ export const CANVAS_MARGINS = {
bottom: 100,
};
-//
-// The base size the shapes were defined at matches nicely w/ a 14px font.
-//
-export const BASE_NODE_SIZE = 64;
-export const MIN_NODE_SIZE = 24;
-export const MAX_NODE_SIZE = 96;
-export const BASE_NODE_LABEL_SIZE = 14;
-export const MIN_NODE_LABEL_SIZE = 12;
+// Node shapes
+export const NODE_SHAPE_HIGHLIGHT_RADIUS = 0.7;
+export const NODE_SHAPE_BORDER_RADIUS = 0.5;
+export const NODE_SHAPE_SHADOW_RADIUS = 0.45;
+export const NODE_SHAPE_DOT_RADIUS = 0.125;
+export const NODE_BLUR_OPACITY = 0.2;
+export const NODE_BASE_SIZE = 50;
// Node details table constants
export const NODE_DETAILS_TABLE_CW = {
diff --git a/client/app/scripts/hoc/metric-feeder.js b/client/app/scripts/hoc/metric-feeder.js
index 1180cf9d4..c2167fd66 100644
--- a/client/app/scripts/hoc/metric-feeder.js
+++ b/client/app/scripts/hoc/metric-feeder.js
@@ -2,8 +2,6 @@ import React from 'react';
import { isoParse as parseDate } from 'd3-time-format';
import { OrderedMap } from 'immutable';
-import { round } from '../utils/math-utils';
-
const makeOrderedMap = OrderedMap;
const sortDate = (v, d) => d;
const DEFAULT_TICK_INTERVAL = 1000; // DEFAULT_TICK_INTERVAL + renderTime < 1000ms
@@ -104,7 +102,7 @@ export default ComposedComponent => class extends React.Component {
let lastIndex = bufferKeys.indexOf(movingLast);
// speed up the window if it falls behind
- const step = lastIndex > 0 ? round(buffer.size / lastIndex) : 1;
+ const step = lastIndex > 0 ? Math.round(buffer.size / lastIndex) : 1;
// only move first if we have enough values in window
const windowLength = lastIndex - firstIndex;
diff --git a/client/app/scripts/utils/__tests__/math-utils-test.js b/client/app/scripts/utils/__tests__/math-utils-test.js
index 6d5f95398..ab46b1a96 100644
--- a/client/app/scripts/utils/__tests__/math-utils-test.js
+++ b/client/app/scripts/utils/__tests__/math-utils-test.js
@@ -19,21 +19,4 @@ describe('MathUtils', () => {
expect(f(-5, 5)).toBe(0);
});
});
-
- describe('round', () => {
- const f = MathUtils.round;
-
- it('it should round the decimal number to given precision', () => {
- expect(f(-173.6499023, -2)).toBe(-200);
- expect(f(-173.6499023, -1)).toBe(-170);
- expect(f(-173.6499023, 0)).toBe(-174);
- expect(f(-173.6499023)).toBe(-174);
- expect(f(-173.6499023, 1)).toBe(-173.6);
- expect(f(-173.6499023, 2)).toBe(-173.65);
- expect(f(0.0013, 2)).toBe(0);
- expect(f(0.0013, 3)).toBe(0.001);
- expect(f(0.0013, 4)).toBe(0.0013);
- expect(f(0.0013, 5)).toBe(0.0013);
- });
- });
});
diff --git a/client/app/scripts/utils/math-utils.js b/client/app/scripts/utils/math-utils.js
index 401bf76b8..578d83760 100644
--- a/client/app/scripts/utils/math-utils.js
+++ b/client/app/scripts/utils/math-utils.js
@@ -18,10 +18,3 @@
export function modulo(i, n) {
return ((i % n) + n) % n;
}
-
-// Does the same that the deprecated d3.round was doing.
-// Possibly imprecise: This https://github.com/d3/d3/issues/210
-export function round(value, decimals = 0) {
- const p = Math.pow(10, decimals);
- return Math.round(value * p) / p;
-}
diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js
index 5acf724ae..f27a56760 100644
--- a/client/app/scripts/utils/metric-utils.js
+++ b/client/app/scripts/utils/metric-utils.js
@@ -2,32 +2,30 @@ import { includes } from 'lodash';
import { scaleLog } from 'd3-scale';
import React from 'react';
+import { NODE_SHAPE_DOT_RADIUS } from '../constants/styles';
import { formatMetricSvg } from './string-utils';
import { colors } from './color-utils';
-export function getClipPathDefinition(clipId, size, height,
- x = -size * 0.5, y = (size * 0.5) - height) {
+export function getClipPathDefinition(clipId, height) {
return (
-
+
);
}
+export function renderMetricValue(value, condition) {
+ return condition ? {value} : ;
+}
//
// loadScale(1) == 0.5; E.g. a nicely balanced system :).
const loadScale = scaleLog().domain([0.01, 100]).range([0, 1]);
-export function getMetricValue(metric, size) {
+export function getMetricValue(metric) {
if (!metric) {
return {height: 0, value: null, formattedValue: 'n/a'};
}
@@ -48,10 +46,9 @@ export function getMetricValue(metric, size) {
} else if (displayedValue >= m.max && displayedValue > 0) {
displayedValue = 1;
}
- const height = size * displayedValue;
return {
- height,
+ height: displayedValue,
hasMetric: value !== null,
formattedValue: formatMetricSvg(value, m)
};
diff --git a/client/app/scripts/utils/node-shape-utils.js b/client/app/scripts/utils/node-shape-utils.js
new file mode 100644
index 000000000..45dbf6b2b
--- /dev/null
+++ b/client/app/scripts/utils/node-shape-utils.js
@@ -0,0 +1,12 @@
+import { line, curveCardinalClosed } from 'd3-shape';
+import range from 'lodash/range';
+
+const shapeSpline = line().curve(curveCardinalClosed.tension(0.65));
+
+export function nodeShapePolygon(radius, n) {
+ const innerAngle = (2 * Math.PI) / n;
+ return shapeSpline(range(0, n).map(k => [
+ radius * Math.sin(k * innerAngle),
+ -radius * Math.cos(k * innerAngle)
+ ]));
+}
diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss
index 22601a7d8..8456f8974 100644
--- a/client/app/styles/_base.scss
+++ b/client/app/styles/_base.scss
@@ -299,7 +299,7 @@
fill: $text-secondary-color;
}
- .nodes-chart-nodes > .node {
+ .nodes-chart-nodes .node {
transition: opacity .5s $base-ease;
text-align: center;
@@ -316,6 +316,14 @@
color: $text-color;
}
+ .node-labels-container {
+ transform: scale($node-text-scale);
+ pointer-events: none;
+ height: 5em;
+ x: -0.5 * $node-labels-max-width;
+ width: $node-labels-max-width;
+ }
+
.node-label-wrapper {
//
// Base line height doesn't hop across foreignObject =/
@@ -336,6 +344,9 @@
vertical-align: top;
cursor: pointer;
+ pointer-events: all;
+ font-size: 12px;
+ width: 100%;
}
.node-sublabel {
@@ -344,7 +355,6 @@
}
.node-label, .node-sublabel {
-
span {
border-radius: 2px;
}
@@ -411,15 +421,13 @@
}
.link {
- stroke: $text-secondary-color;
- stroke-width: $edge-link-stroke-width;
fill: none;
+ stroke-width: $edge-link-stroke-width;
stroke-opacity: $edge-opacity;
}
.shadow {
- stroke: $weave-blue;
- stroke-width: 10px;
fill: none;
+ stroke: $weave-blue;
stroke-opacity: 0;
}
&.highlighted {
@@ -433,7 +441,7 @@
display: none;
}
- .stack .onlyHighlight .shape {
+ .stack .highlight .shape {
.border { display: none; }
.shadow { display: none; }
.node { display: none; }
@@ -448,8 +456,7 @@
transform: scale(1);
cursor: pointer;
- /* cloud paths have stroke-width set dynamically */
- &:not(.shape-cloud) .border {
+ .border {
stroke-width: $node-border-stroke-width;
fill: $background-color;
transition: stroke-opacity 0.333s $base-ease, fill 0.333s $base-ease;
@@ -475,10 +482,11 @@
.node {
fill: $text-color;
stroke: $background-lighter-color;
- stroke-width: 2px;
+ stroke-width: 0.05;
}
text {
+ transform: scale($node-text-scale);
font-size: 12px;
dominant-baseline: middle;
text-anchor: middle;
@@ -494,7 +502,7 @@
}
.stack .shape .border {
- stroke-width: $node-border-stroke-width - 0.5;
+ stroke-width: $node-border-stroke-width * 0.8;
}
}
diff --git a/client/app/styles/_contrast-overrides.scss b/client/app/styles/_contrast-overrides.scss
index adcc59256..a62c5de5f 100644
--- a/client/app/styles/_contrast-overrides.scss
+++ b/client/app/styles/_contrast-overrides.scss
@@ -14,13 +14,12 @@ $white: white;
$node-opacity-blurred: 0.6;
$node-highlight-fill-opacity: 0.3;
$node-highlight-stroke-opacity: 0.5;
-$node-highlight-stroke-width: 3px;
-$node-border-stroke-width: 5px;
+$node-highlight-stroke-width: 0.06;
+$node-border-stroke-width: 0.1;
$node-pseudo-opacity: 1;
$edge-highlight-opacity: 0.3;
$edge-opacity-blurred: 0;
$edge-opacity: 0.5;
-$edge-link-stroke-width: 3px;
$btn-opacity-default: 1;
$btn-opacity-hover: 1;
diff --git a/client/app/styles/_variables.scss b/client/app/styles/_variables.scss
index 8e28a72e2..14c434d06 100644
--- a/client/app/styles/_variables.scss
+++ b/client/app/styles/_variables.scss
@@ -33,13 +33,14 @@ $terminal-header-height: 44px;
$node-opacity-blurred: 0.25;
$node-highlight-fill-opacity: 0.1;
$node-highlight-stroke-opacity: 0.4;
-$node-highlight-stroke-width: 1px;
-$node-border-stroke-width: 2.5px;
+$node-highlight-stroke-width: 0.02;
+$node-border-stroke-width: 0.06;
$node-pseudo-opacity: 0.8;
+$node-text-scale: 0.02;
+$node-labels-max-width: 120px;
$edge-highlight-opacity: 0.1;
$edge-opacity-blurred: 0.2;
$edge-opacity: 0.5;
-$edge-link-stroke-width: 1px;
$btn-opacity-default: 0.7;
$btn-opacity-hover: 1;