SHow dragged node on top and refactor

This commit is contained in:
JUDIT GRESKOVITS 2017-04-27 12:49:52 +01:00 committed by Sérgio Ramos
parent f82fee8a7f
commit 858a0a2c24
12 changed files with 343 additions and 272 deletions

View File

@ -126,17 +126,8 @@ const metricByInterval = (data = [], {
const q1 = statistics.quantile(data, 0.25);
const median = statistics.median(data);
const q3 = statistics.quantile(data, 0.75);
const iqr = q3-q1;
const outlierMultiplier = 1.5;
let max = statistics.max(data);
if(max < q3 + iqr*outlierMultiplier) {
max = q3;
}
let min = statistics.min(data);
if(min > q1 - iqr*outlierMultiplier){
min = q3;
}
const max = statistics.max(data);
const min = statistics.min(data);
return {
start: sample.start.valueOf(),

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,53 @@
import { Baseline } from '../../../shared/composers';
import React from 'react';
import {
GraphLinkCircle,
GraphLinkArrowLine
} from './shapes';
const GraphLinkArrow = ({
data,
index
}) => {
const {
targetPosition,
arrowAngle
} = data;
return (
<g
transform={
// eslint-disable-next-line max-len
`translate(${targetPosition.x}, ${targetPosition.y}) rotate(${arrowAngle})`
}
>
<GraphLinkCircle
cx={0}
cy={0}
r={9}
/>
<GraphLinkArrowLine
x1={-1}
x2={2}
y1={-3}
y2={0}
/>
<GraphLinkArrowLine
x1={-1}
x2={2}
y1={3}
y2={0}
/>
</g>
);
};
GraphLinkArrow.propTypes = {
data: React.PropTypes.object.isRequired,
index: React.PropTypes.number
};
export default Baseline(
GraphLinkArrow
);

View File

@ -0,0 +1,100 @@
import Constants from '../constants';
const getAngleFromPoints = (source, target) => {
const lineAngle = Math.atan2(target.y-source.y, target.x - source.x);
const lineAngleDeg = lineAngle*180/Math.PI;
const zeroToThreeSixty = lineAngleDeg < 0 ? 360 + lineAngleDeg : lineAngleDeg;
return zeroToThreeSixty;
};
const getPosition = (angle, positions, position, noCorners=false) => {
const positionIndex = noCorners ?
Math.round(angle/90)*2 : Math.round(angle/45);
const offsetPosition = positions[positionIndex];
return {
id: offsetPosition.id,
x: position.x + offsetPosition.x,
y: position.y + offsetPosition.y
};
};
const getPositions = (rect, halfCorner=0) => ([{
id: 'r',
x: rect.right,
y: 0
}, {
id: 'br',
x: rect.right - halfCorner,
y: rect.bottom - halfCorner
}, {
id: 'b',
x: 0,
y: rect.bottom
}, {
id: 'bl',
x: rect.left + halfCorner,
y: rect.bottom - halfCorner
}, {
id: 'l',
x: rect.left,
y: 0
}, {
id: 'tl',
x: rect.left + halfCorner,
y: rect.top + halfCorner
}, {
id: 't',
x: 0,
y: rect.top
}, {
id: 'tr',
x: rect.right- halfCorner,
y: rect.top + halfCorner
},{
id: 'r',
x: rect.right,
y: 0
}]);
const getRect = (data) => {
return data.children ?
Constants.nodeRectWithChildren :
Constants.nodeRect;
};
const calculateLineLayout = ({
source,
target
}) => {
// actually, this will need to be got dynamically, in case them things are different sizes
// yeah right, now you'll get to do exactly that
const sourceRect = getRect(source);
const targetRect= getRect(target);
const halfCorner = 2;
const sourcePositions = getPositions(sourceRect, halfCorner);
const sourceAngle = getAngleFromPoints(source, target);
const sourcePosition = getPosition(sourceAngle, sourcePositions, source);
const targetPositions = getPositions(targetRect, halfCorner);
const targetAngle = getAngleFromPoints(target, sourcePosition);
const targetPosition = getPosition(targetAngle, targetPositions, target); //, true);
const arrowAngle = getAngleFromPoints(sourcePosition, targetPosition);
return {
source,
target,
sourcePosition,
targetPosition,
arrowAngle
};
};
export {
calculateLineLayout
};

View File

@ -1,143 +1,32 @@
import { Baseline } from '../../../shared/composers';
import Constants from '../constants';
import React from 'react';
import {
GraphLinkLine,
GraphLinkCircle,
GraphLinkArrow
GraphLinkLine
} from './shapes';
const getAngleFromPoints = (source, target) => {
const lineAngle = Math.atan2(target.y-source.y, target.x - source.x);
const lineAngleDeg = lineAngle*180/Math.PI;
const zeroToThreeSixty = lineAngleDeg < 0 ? 360 + lineAngleDeg : lineAngleDeg;
return zeroToThreeSixty;
};
const getPosition = (angle, positions, position, noCorners=false) => {
const positionIndex = noCorners ?
Math.round(angle/90)*2 : Math.round(angle/45);
const offsetPosition = positions[positionIndex];
return {
id: offsetPosition.id,
x: position.x + offsetPosition.x,
y: position.y + offsetPosition.y
};
};
const getPositions = (rect, halfCorner=0) => ([{
id: 'r',
x: rect.right,
y: 0
}, {
id: 'br',
x: rect.right - halfCorner,
y: rect.bottom - halfCorner
}, {
id: 'b',
x: 0,
y: rect.bottom
}, {
id: 'bl',
x: rect.left + halfCorner,
y: rect.bottom - halfCorner
}, {
id: 'l',
x: rect.left,
y: 0
}, {
id: 'tl',
x: rect.left + halfCorner,
y: rect.top + halfCorner
}, {
id: 't',
x: 0,
y: rect.top
}, {
id: 'tr',
x: rect.right- halfCorner,
y: rect.top + halfCorner
},{
id: 'r',
x: rect.right,
y: 0
}]);
const getRect = (data) => {
return data.children ?
Constants.nodeRectWithChildren :
Constants.nodeRect;
};
const GraphLink = ({
data,
index
}) => {
const {
source,
target
sourcePosition,
targetPosition
} = data;
// actually, this will need to be got dynamically, in case them things are different sizes
// yeah right, now you'll get to do exactly that
const sourceRect = getRect(source);
const targetRect= getRect(target);
const halfCorner = 2;
const sourcePositions = getPositions(sourceRect, halfCorner);
const sourceAngle = getAngleFromPoints(source, target);
const sourcePosition = getPosition(sourceAngle, sourcePositions, source);
const targetPositions = getPositions(targetRect, halfCorner);
const targetAngle = getAngleFromPoints(target, sourcePosition);
const targetPosition = getPosition(targetAngle, targetPositions, target); //, true);
const arrowAngle = getAngleFromPoints(sourcePosition, targetPosition);
return (
<g>
<GraphLinkLine
x1={sourcePosition.x}
x2={targetPosition.x}
y1={sourcePosition.y}
y2={targetPosition.y}
/>
<g
transform={
// eslint-disable-next-line max-len
`translate(${targetPosition.x}, ${targetPosition.y}) rotate(${arrowAngle})`
}
>
<GraphLinkCircle
cx={0}
cy={0}
r={9}
/>
<GraphLinkArrow
x1={-1}
x2={2}
y1={-3}
y2={0}
/>
<GraphLinkArrow
x1={-1}
x2={2}
y1={3}
y2={0}
/>
</g>
</g>
<GraphLinkLine
x1={sourcePosition.x}
x2={targetPosition.x}
y1={sourcePosition.y}
y2={targetPosition.y}
/>
);
};
GraphLink.propTypes = {
data: React.PropTypes.object.isRequired,
index: React.PropTypes.number.isRequired
index: React.PropTypes.number
};
export default Baseline(

View File

@ -12,7 +12,7 @@ export const GraphLinkCircle = styled.circle`
stroke-width: 1.5;
`;
export const GraphLinkArrow = styled.line`
export const GraphLinkArrowLine = styled.line`
stroke: ${colors.base.white};
stroke-width: 2;
stroke-linecap: round;

View File

@ -67,7 +67,7 @@ const GraphNode = ({
const onStart = (evt) => {
evt.preventDefault();
onDragStart(evt, data.id);
onDragStart(evt, data.uuid);
};
const nodeRectEvents = connected ? {

View File

@ -14,6 +14,16 @@ const rectRadius = (size) => {
return Math.round(hypotenuse(width, height)/2);
};
const forcePlayAnimation = (simulation, animationTicks) => {
const n = Math.ceil(
Math.log(
simulation.alphaMin()) / Math.log(
1 - simulation.alphaDecay())) - animationTicks;
for (var i = 0; i < n; ++i) {
simulation.tick();
}
};
const createLinks = (services) =>
services.reduce((acc, service, index) =>
service.connections ?
@ -28,13 +38,12 @@ const createLinks = (services) =>
const createSimulation = (
services,
svgSize,
onTick,
onEnd
animationTicks = 0
) => {
// This is not going to work given that as well as the d3 layout stuff, other things might be at play too
// We should pass two objects to the components - one for positioning and one for data
const nodes = services.map((service, index) => ({
id: service.uuid,
uuid: service.uuid,
index: index
}));
@ -47,15 +56,17 @@ const createSimulation = (
const nodeRadius = rectRadius(Constants.nodeSizeWithChildren);
const simulation = forceSimulation(nodes)
.force('link', forceLink(links).id(d => d.uuid))
.force('collide', forceCollide(nodeRadius))
.force('center', forceCenter(width/2, height/2));
forcePlayAnimation(simulation, animationTicks);
return ({
simulation: forceSimulation(nodes)
.force('link', forceLink(links).id(d => d.id))
.force('collide', forceCollide(nodeRadius))
.force('center', forceCenter(width/2, height/2))
.on('tick', onTick)
.on('end', onEnd),
nodes: nodes,
links: links
nodes,
links,
simulation
});
};

View File

@ -4,7 +4,7 @@ import { Baseline } from '../../shared/composers';
import Input from '../form/input';
import Select from '../form/select';
import { TopologyGraph } from './';
import data from './data';
import data from './wp-data';
import React from 'react';
const StyledForm = styled.form`

View File

@ -6,11 +6,16 @@ import README from './readme.md';
import StoryHelper from './story-helper';
import GraphNode from './graph-node';
import TopologyGraph from './topology-graph';
import data from './big-data';
storiesOf('Topology', module)
.add('5 services', withReadme(README, () => (
.add('Wordpress example', withReadme(README, () => (
<StoryHelper />
)))
.add('Many services example', withReadme(README, () => (
<TopologyGraph services={data} />
)))
.add('Consul', withReadme(README, () => (
<svg width={180} height={159}>
<GraphNode

View File

@ -1,105 +1,56 @@
import React from 'react';
import styled from 'styled-components';
import { Baseline } from '../../shared/composers';
import {
createSimulation//,
//updateSimulation
} from './graph-simulation';
import Constants from './constants';
import {
createSimulation
} from './graph-simulation';
import {
calculateLineLayout
} from './graph-link/functions';
import GraphNode from './graph-node';
import GraphLink from './graph-link';
import React from 'react';
import GraphLinkArrow from './graph-link/arrow';
const StyledSvg = styled.svg`
width: 100%;
height: 1400px;
`;
let dragInfo = {
dragging: false,
nodeId: null,
position: null
};
class TopologyGraph extends React.Component {
componentWillMount() {
const services = this.props.services.reduce((acc, service, index) => {
if(service.id !== 'consul') acc.push(service);
return acc;
}, []);
const services = this.getServicesWithoutConsul();
const svgSize = this.getSvgSize();
const simulationData = createSimulation(
services,
svgSize//,
//() => this.forceUpdate(),
//() => this.forceUpdate()
);
const simulation = simulationData.simulation;
const n = Math.ceil(
Math.log(
simulation.alphaMin()) / Math.log(
1 - simulation.alphaDecay()));
for (var i = 0; i < n; ++i) {
simulation.tick();
}
this.setState(simulationData);
}
/*componentWillReceiveProps(nextProps) {
// either, we'll have more services
// or, we'll have less services
// or, data of services had changed =>
// do shallow check on objects and links, if no change, don't do rerender
// otherwise, redo them bitches = by what I mean to update the simulation
// try freezing exisiting ones... then adding another
const {
nodes,
links
} = this.state;
links,
simulation
} = createSimulation(
services,
svgSize
);
const services = nextProps.services.reduce((acc, service, index) => {
this.setState({
nodes,
links,
simulation
});
}
getServicesWithoutConsul() {
return this.props.services.reduce((acc, service, index) => {
if(service.id !== 'consul') acc.push(service);
return acc;
}, []);
// TODO this here means we'll need to evaluate whether to we have more links!
// this is tmp for the compare above
if(services !== nodes.length) {
const simulation = this.state.simulation;
const nextSimulationData = updateSimulation(
simulation,
services,
nodes,
links,
svgSize,
() => this.forceUpdate(),
() => this.forceUpdate()
);
const nextSimulation = nextSimulationData.simulation;
// console.log('nextSimulationData.nodes = ', nextSimulationData.nodes);
const n = Math.ceil(
Math.log(
nextSimulation.alphaMin()) / Math.log(
1 - nextSimulation.alphaDecay()));
for (var i = 0; i < n; ++i) {
nextSimulation.tick();
}
//this.state.simulation.nodes().forEach((node, index) => {
// delete node.fx;
// delete node.fy;
//});
this.setState(nextSimulationData);
}
}*/
}
getSvgSize() {
if(document.getElementById('topology-svg')) {
@ -116,7 +67,7 @@ class TopologyGraph extends React.Component {
};
}
constrain(x, y, children=false) {
constrainNodePosition(x, y, children=false) {
const svgSize = this.getSvgSize();
const nodeRect = children ?
@ -142,6 +93,40 @@ class TopologyGraph extends React.Component {
};
}
findNode(nodeUuid) {
return this.state.nodes.reduce((acc, simNode, index) =>
simNode.uuid === nodeUuid ? simNode : acc, {});
}
getConsulNodePosition() {
const svgSize = this.getSvgSize();
const x = svgSize.width - Constants.nodeSize.width;
return {
x,
y: 0
};
}
getConstrainedNodePosition(nodeUuid, children=false) {
const node = this.findNode(nodeUuid);
return this.constrainNodePosition(node.x, node.y, children);
}
findNodeData(nodesData, nodeUuid) {
return nodesData.reduce((acc, nodeData, index) =>
nodeData.uuid === nodeUuid ? nodeData : acc, {});
}
setDragInfo(dragging, nodeUuid=null, position={}) {
this.dragInfo = {
dragging,
nodeUuid,
position
};
}
render() {
const {
@ -154,55 +139,42 @@ class TopologyGraph extends React.Component {
links
} = this.state;
const simNode = (nodeId) =>
nodes.reduce((acc, simNode, index) =>
simNode.id === nodeId ? simNode : acc, {});
const svgSize = this.getSvgSize();
const nodesData = services.map((service, index) => {
const sNode = service.id === 'consul' ? {
x: svgSize.width - Constants.nodeSize.width,
y: 0
} : simNode(service.uuid);
const constrained = {
...sNode,
...this.constrain(sNode.x, sNode.y, service.children)
};
const nodePosition = service.id === 'consul' ?
this.getConsulNodePosition() :
this.getConstrainedNodePosition(service.uuid, service.children);
return ({
...service,
...constrained
...nodePosition
});
});
const nodeData = (nodeId) =>
nodesData.reduce((acc, nodeData, index) =>
nodeData.id === nodeId ? nodeData : acc, {});
// TODO links will need to know whether a service has children
// if it does, the height of it will be different
const linksData = links.map((link, index) => ({
source: nodeData(link.source.id),
target: nodeData(link.target.id)
}));
source: this.findNodeData(nodesData, link.source.uuid),
target: this.findNodeData(nodesData, link.target.uuid)
})).map((linkData, index) => calculateLineLayout(linkData, index ));
const onDragStart = (evt, nodeId) => {
// it's this node's position that we'll need to update
dragInfo.dragging = true;
dragInfo.nodeId = nodeId;
const x = evt.changedTouches ? evt.changedTouches[0].pageX : evt.clientX;
const y = evt.changedTouches ? evt.changedTouches[0].pageY : evt.clientY;
dragInfo.position = {
x,
y
};
this.setDragInfo(
true,
nodeId,
{
x,
y
}
);
};
const onDragMove = (evt) => {
if ( dragInfo.dragging ) {
if ( this.dragInfo && this.dragInfo.dragging ) {
const x = evt.changedTouches
? evt.changedTouches[0].pageX
@ -212,12 +184,12 @@ class TopologyGraph extends React.Component {
: evt.clientY;
const offset = {
x: x - dragInfo.position.x,
y: y - dragInfo.position.y
x: x - this.dragInfo.position.x,
y: y - this.dragInfo.position.y
};
const dragNodes = nodes.map((simNode, index) => {
if ( simNode.id === dragInfo.nodeId ) {
if ( simNode.uuid === this.dragInfo.nodeUuid ) {
return ({
...simNode,
x: simNode.x + offset.x,
@ -233,25 +205,25 @@ class TopologyGraph extends React.Component {
nodes: dragNodes
});
dragInfo.position = {
x,
y
};
this.setDragInfo(
true,
this.dragInfo.nodeUuid,
{
x,
y
}
);
}
};
const onDragEnd = (evt) => {
dragInfo = {
dragging: false,
nodeId: null,
position: {}
};
this.setDragInfo(false);
};
const onTitleClick = (serviceUUID) =>
this.props.onNodeTitleClick(serviceUUID);
const renderedNodes = nodesData.map((n, index) => (
const renderedNode = (n, index) => (
<GraphNode
key={index}
data={n}
@ -261,15 +233,54 @@ class TopologyGraph extends React.Component {
onQuickActions={onQuickActions}
connected={n.id !== 'consul'}
/>
));
);
const renderedLinks = linksData.map((l, index) => (
const renderedLink = (l, index) => (
<GraphLink
key={index}
data={l}
index={index}
/>
));
);
const renderedLinkArrow = (l, index) => (
<GraphLinkArrow
key={index}
data={l}
index={index}
/>
);
const renderedNodes = this.dragInfo && this.dragInfo.dragging ?
nodesData.filter((n, index) => n.uuid !== this.dragInfo.nodeUuid)
.map((n, index) => renderedNode(n, index)) :
nodesData.map((n, index) => renderedNode(n, index));
const renderedLinks = linksData.map((l, index) => renderedLink(l, index));
const renderedLinkArrows = this.dragInfo && this.dragInfo.dragging ?
linksData.filter((l, index) => l.target.uuid !== this.dragInfo.nodeUuid)
.map((l, index) => renderedLinkArrow(l, index)) :
linksData.map((l, index) => renderedLinkArrow(l, index));
const dragNode = !this.dragInfo || !this.dragInfo.dragging ? null :
renderedNode(
nodesData.reduce((dragNode, n, index) => {
if(n.uuid === this.dragInfo.nodeUuid) {
return n;
}
return dragNode;
}, {}));
const dragLinkArrow = !this.dragInfo || !this.dragInfo.dragging ||
renderedLinkArrows.length === renderedLinks.length ? null :
renderedLinkArrow(
linksData.reduce((dragLinkArrow, l, index) => {
if(l.target.uuid === this.dragInfo.nodeUuid) {
return l;
}
return dragLinkArrow;
}, {}));
return (
<StyledSvg
@ -286,6 +297,15 @@ class TopologyGraph extends React.Component {
<g>
{renderedLinks}
</g>
<g>
{renderedLinkArrows}
</g>
<g>
{dragNode}
</g>
<g>
{dragLinkArrow}
</g>
</StyledSvg>
);
}