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 q1 = statistics.quantile(data, 0.25);
const median = statistics.median(data); const median = statistics.median(data);
const q3 = statistics.quantile(data, 0.75); const q3 = statistics.quantile(data, 0.75);
const max = statistics.max(data);
const iqr = q3-q1; const min = statistics.min(data);
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;
}
return { return {
start: sample.start.valueOf(), 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 { Baseline } from '../../../shared/composers';
import Constants from '../constants';
import React from 'react'; import React from 'react';
import { import {
GraphLinkLine, GraphLinkLine
GraphLinkCircle,
GraphLinkArrow
} from './shapes'; } 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 = ({ const GraphLink = ({
data, data,
index index
}) => { }) => {
const { const {
source, sourcePosition,
target targetPosition
} = data; } = 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 ( return (
<g> <GraphLinkLine
<GraphLinkLine x1={sourcePosition.x}
x1={sourcePosition.x} x2={targetPosition.x}
x2={targetPosition.x} y1={sourcePosition.y}
y1={sourcePosition.y} y2={targetPosition.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>
); );
}; };
GraphLink.propTypes = { GraphLink.propTypes = {
data: React.PropTypes.object.isRequired, data: React.PropTypes.object.isRequired,
index: React.PropTypes.number.isRequired index: React.PropTypes.number
}; };
export default Baseline( export default Baseline(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,105 +1,56 @@
import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { Baseline } from '../../shared/composers'; import { Baseline } from '../../shared/composers';
import {
createSimulation//,
//updateSimulation
} from './graph-simulation';
import Constants from './constants'; import Constants from './constants';
import {
createSimulation
} from './graph-simulation';
import {
calculateLineLayout
} from './graph-link/functions';
import GraphNode from './graph-node'; import GraphNode from './graph-node';
import GraphLink from './graph-link'; import GraphLink from './graph-link';
import React from 'react'; import GraphLinkArrow from './graph-link/arrow';
const StyledSvg = styled.svg` const StyledSvg = styled.svg`
width: 100%; width: 100%;
height: 1400px; height: 1400px;
`; `;
let dragInfo = {
dragging: false,
nodeId: null,
position: null
};
class TopologyGraph extends React.Component { class TopologyGraph extends React.Component {
componentWillMount() { 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 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 { const {
nodes, nodes,
links links,
} = this.state; 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); if(service.id !== 'consul') acc.push(service);
return acc; 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() { getSvgSize() {
if(document.getElementById('topology-svg')) { 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 svgSize = this.getSvgSize();
const nodeRect = children ? 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() { render() {
const { const {
@ -154,55 +139,42 @@ class TopologyGraph extends React.Component {
links links
} = this.state; } = 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 nodesData = services.map((service, index) => {
const sNode = service.id === 'consul' ? { const nodePosition = service.id === 'consul' ?
x: svgSize.width - Constants.nodeSize.width, this.getConsulNodePosition() :
y: 0 this.getConstrainedNodePosition(service.uuid, service.children);
} : simNode(service.uuid);
const constrained = {
...sNode,
...this.constrain(sNode.x, sNode.y, service.children)
};
return ({ return ({
...service, ...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 // TODO links will need to know whether a service has children
// if it does, the height of it will be different // if it does, the height of it will be different
const linksData = links.map((link, index) => ({ const linksData = links.map((link, index) => ({
source: nodeData(link.source.id), source: this.findNodeData(nodesData, link.source.uuid),
target: nodeData(link.target.id) target: this.findNodeData(nodesData, link.target.uuid)
})); })).map((linkData, index) => calculateLineLayout(linkData, index ));
const onDragStart = (evt, nodeId) => { const onDragStart = (evt, nodeId) => {
// it's this node's position that we'll need to update // 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 x = evt.changedTouches ? evt.changedTouches[0].pageX : evt.clientX;
const y = evt.changedTouches ? evt.changedTouches[0].pageY : evt.clientY; const y = evt.changedTouches ? evt.changedTouches[0].pageY : evt.clientY;
dragInfo.position = { this.setDragInfo(
x, true,
y nodeId,
}; {
x,
y
}
);
}; };
const onDragMove = (evt) => { const onDragMove = (evt) => {
if ( this.dragInfo && this.dragInfo.dragging ) {
if ( dragInfo.dragging ) {
const x = evt.changedTouches const x = evt.changedTouches
? evt.changedTouches[0].pageX ? evt.changedTouches[0].pageX
@ -212,12 +184,12 @@ class TopologyGraph extends React.Component {
: evt.clientY; : evt.clientY;
const offset = { const offset = {
x: x - dragInfo.position.x, x: x - this.dragInfo.position.x,
y: y - dragInfo.position.y y: y - this.dragInfo.position.y
}; };
const dragNodes = nodes.map((simNode, index) => { const dragNodes = nodes.map((simNode, index) => {
if ( simNode.id === dragInfo.nodeId ) { if ( simNode.uuid === this.dragInfo.nodeUuid ) {
return ({ return ({
...simNode, ...simNode,
x: simNode.x + offset.x, x: simNode.x + offset.x,
@ -233,25 +205,25 @@ class TopologyGraph extends React.Component {
nodes: dragNodes nodes: dragNodes
}); });
dragInfo.position = { this.setDragInfo(
x, true,
y this.dragInfo.nodeUuid,
}; {
x,
y
}
);
} }
}; };
const onDragEnd = (evt) => { const onDragEnd = (evt) => {
dragInfo = { this.setDragInfo(false);
dragging: false,
nodeId: null,
position: {}
};
}; };
const onTitleClick = (serviceUUID) => const onTitleClick = (serviceUUID) =>
this.props.onNodeTitleClick(serviceUUID); this.props.onNodeTitleClick(serviceUUID);
const renderedNodes = nodesData.map((n, index) => ( const renderedNode = (n, index) => (
<GraphNode <GraphNode
key={index} key={index}
data={n} data={n}
@ -261,15 +233,54 @@ class TopologyGraph extends React.Component {
onQuickActions={onQuickActions} onQuickActions={onQuickActions}
connected={n.id !== 'consul'} connected={n.id !== 'consul'}
/> />
)); );
const renderedLinks = linksData.map((l, index) => ( const renderedLink = (l, index) => (
<GraphLink <GraphLink
key={index} key={index}
data={l} data={l}
index={index} 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 ( return (
<StyledSvg <StyledSvg
@ -286,6 +297,15 @@ class TopologyGraph extends React.Component {
<g> <g>
{renderedLinks} {renderedLinks}
</g> </g>
<g>
{renderedLinkArrows}
</g>
<g>
{dragNode}
</g>
<g>
{dragLinkArrow}
</g>
</StyledSvg> </StyledSvg>
); );
} }