1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

fix as path graph layout

This commit is contained in:
checktheroads
2021-02-01 01:11:23 -07:00
parent 2db21ebd1a
commit 207b6ab9de
7 changed files with 184 additions and 114 deletions

View File

@@ -1,14 +1,12 @@
import { useMemo } from 'react';
import { Box, Flex, SkeletonText, Badge, VStack } from '@chakra-ui/react';
import ReactFlow from 'react-flow-renderer';
import { Background, ReactFlowProvider } from 'react-flow-renderer';
import { Handle, Position } from 'react-flow-renderer';
import { useConfig, useColorValue, useColorToken, useBreakpointValue } from '~/context';
import { useConfig, useColorValue, useColorToken } from '~/context';
import { useASNDetail } from '~/hooks';
import { Controls } from './controls';
import { buildElements } from './util';
import { useElements } from './useElements';
import type { ReactFlowProps } from 'react-flow-renderer';
import type { TChart, TNode, TNodeData } from './types';
export const Chart: React.FC<TChart> = (props: TChart) => {
@@ -17,19 +15,17 @@ export const Chart: React.FC<TChart> = (props: TChart) => {
const dots = useColorToken('colors', 'blackAlpha.500', 'whiteAlpha.400');
const flowProps = useBreakpointValue<Omit<ReactFlowProps, 'elements'>>({
base: { defaultPosition: [0, 300], defaultZoom: 0 },
lg: { defaultPosition: [100, 300], defaultZoom: 0.7 },
}) ?? { defaultPosition: [100, 300], defaultZoom: 0.7 };
const elements = useMemo(() => [...buildElements({ asn: primary_asn, name: org_name }, data)], [
data,
]);
const elements = useElements({ asn: primary_asn, name: org_name }, data);
return (
<ReactFlowProvider>
<Box boxSize="100%" zIndex={1}>
<ReactFlow elements={elements} nodeTypes={{ ASNode }} {...flowProps}>
<ReactFlow
snapToGrid
elements={elements}
nodeTypes={{ ASNode }}
onLoad={inst => setTimeout(() => inst.fitView(), 0)}
>
<Background color={dots} />
<Controls />
</ReactFlow>

View File

@@ -1,11 +1,11 @@
import {
Modal,
Skeleton,
ModalBody,
ModalHeader,
ModalOverlay,
ModalContent,
useDisclosure,
Skeleton,
ModalCloseButton,
} from '@chakra-ui/react';
import { useColorValue, useBreakpointValue } from '~/context';

View File

@@ -0,0 +1,115 @@
import dagre from 'dagre';
import { useMemo } from 'react';
import isEqual from 'react-fast-compare';
import type { FlowElement } from 'react-flow-renderer';
import type { BasePath } from './types';
const NODE_WIDTH = 200;
const NODE_HEIGHT = 48;
export function useElements(base: BasePath, data: TStructuredResponse): FlowElement[] {
return useMemo(() => {
return [...buildElements(base, data)];
}, [data.routes.length]);
}
/**
* Calculate the positions for each AS Path.
* @see https://github.com/MrBlenny/react-flow-chart/issues/61
*/
function* buildElements(base: BasePath, data: TStructuredResponse): Generator<FlowElement> {
const { routes } = data;
// Eliminate empty AS paths & deduplicate non-empty AS paths. Length should be same as count minus empty paths.
const asPaths = routes.filter(r => r.as_path.length !== 0).map(r => [...new Set(r.as_path)]);
const totalPaths = asPaths.length - 1;
const g = new dagre.graphlib.Graph();
g.setGraph({ marginx: 20, marginy: 20 });
g.setDefaultEdgeLabel(() => ({}));
// Set the origin (i.e., the hyperglass user) at the base.
g.setNode(base.asn, { width: NODE_WIDTH, height: NODE_HEIGHT });
for (const [groupIdx, pathGroup] of asPaths.entries()) {
// For each ROUTE's AS Path:
// Find the route after this one.
const nextGroup = groupIdx < totalPaths ? asPaths[groupIdx + 1] : [];
// Connect the first hop in the AS Path to the base (for dagre).
g.setEdge(base.asn, `${groupIdx}-${pathGroup[0]}`);
// Eliminate duplicate AS Paths.
if (!isEqual(pathGroup, nextGroup)) {
for (const [idx, asn] of pathGroup.entries()) {
// For each ASN in the ROUTE:
const node = `${groupIdx}-${asn}`;
const endIdx = pathGroup.length - 1;
// Add the AS as a node.
g.setNode(node, { width: NODE_WIDTH, height: NODE_HEIGHT });
// Connect the first hop in the AS Path to the base (for react-flow).
if (idx === 0) {
yield {
id: `e${base.asn}-${node}`,
source: base.asn,
target: node,
};
}
// Connect every intermediate hop to each other.
if (idx !== endIdx) {
const next = `${groupIdx}-${pathGroup[idx + 1]}`;
g.setEdge(node, next);
yield {
id: `e${node}-${next}`,
source: node,
target: next,
};
}
}
}
}
// Now that that nodes are added, create the layout.
dagre.layout(g, { rankdir: 'BT', align: 'UR' });
// Get the base ASN's positions.
const x = g.node(base.asn).x - NODE_WIDTH / 2;
const y = g.node(base.asn).y + NODE_HEIGHT * 6;
yield {
id: base.asn,
type: 'ASNode',
position: { x, y },
data: { asn: base.asn, name: base.name, hasChildren: true, hasParents: false },
};
for (const [groupIdx, pathGroup] of asPaths.entries()) {
const nextGroup = groupIdx < totalPaths ? asPaths[groupIdx + 1] : [];
if (!isEqual(pathGroup, nextGroup)) {
for (const [idx, asn] of pathGroup.entries()) {
const node = `${groupIdx}-${asn}`;
const endIdx = pathGroup.length - 1;
const x = g.node(node).x - NODE_WIDTH / 2;
const y = g.node(node).y - NODE_HEIGHT * (idx * 6);
// Get each ASN's positions.
yield {
id: node,
type: 'ASNode',
position: { x, y },
data: {
asn,
name: `AS${asn}`,
hasChildren: idx < endIdx,
hasParents: true,
},
};
}
}
}
}

View File

@@ -1,68 +0,0 @@
import { arrangeIntoTree } from '~/util';
import type { FlowElement, Elements } from 'react-flow-renderer';
import type { PathPart } from '~/types';
import type { BasePath } from './types';
function treeToElement(part: PathPart, len: number, index: number): FlowElement[] {
const x = index * 250;
const y = -(len * 10);
const elements = [
{
id: String(part.base),
type: 'ASNode',
position: { x, y },
data: {
asn: part.base,
name: `AS${part.base}`,
hasChildren: part.children.length !== 0,
hasParents: true,
},
},
] as Elements;
for (const child of part.children) {
let xc = index;
if (part.children.length !== 0) {
elements.push({
id: `e${part.base}-${child.base}`,
source: String(part.base),
target: String(child.base),
});
} else {
xc = x;
}
elements.push(...treeToElement(child, part.children.length * 12 + len, xc));
}
return elements;
}
export function* buildElements(base: BasePath, data: TStructuredResponse): Generator<FlowElement> {
const { routes } = data;
// Eliminate empty AS paths & deduplicate non-empty AS paths. Length should be same as count minus empty paths.
const asPaths = routes.filter(r => r.as_path.length !== 0).map(r => [...new Set(r.as_path)]);
const asTree = arrangeIntoTree(asPaths);
const numHops = asPaths.flat().length;
const childPaths = asTree.map((a, i) => {
return treeToElement(a, asTree.length, i);
});
// Add the first hop at the base.
yield {
id: base.asn,
type: 'ASNode',
position: { x: 150, y: numHops * 10 },
data: { asn: base.asn, name: base.name, hasChildren: true, hasParents: false },
};
for (const path of childPaths) {
// path = Each unique path from origin
const first = path[0];
yield { id: `e${base.asn}-${first.id}`, source: base.asn, target: first.id };
// Add link from base to each first hop.
yield { id: `e${base.asn}-${first.id}`, source: base.asn, target: first.id };
for (const hop of path) {
yield hop;
}
}
}

View File

@@ -26,6 +26,7 @@
"@hookstate/persistence": "^3.0.0",
"@meronex/icons": "^4.0.0",
"color2k": "^1.1.1",
"dagre": "^0.8.5",
"dayjs": "^1.8.25",
"framer-motion": "^3.2.2-rc.1",
"lodash": "^4.17.15",
@@ -47,6 +48,7 @@
},
"devDependencies": {
"@hookstate/devtools": "^3.0.0",
"@types/dagre": "^0.7.44",
"@types/node": "^14.14.17",
"@types/react": "^17.0.0",
"@types/react-select": "^3.0.28",

View File

@@ -1055,6 +1055,11 @@
dependencies:
tslib "^2.0.0"
"@types/dagre@^0.7.44":
version "0.7.44"
resolved "https://registry.yarnpkg.com/@types/dagre/-/dagre-0.7.44.tgz#8f4b796b118ca29c132da7068fbc0d0351ee5851"
integrity sha512-N6HD+79w77ZVAaVO7JJDW5yJ9LAxM62FpgNGO9xEde+KVYjDRyhIMzfiErXpr1g0JPon9kwlBzoBK6s4fOww9Q==
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@@ -2735,6 +2740,14 @@ d@1, d@^1.0.1:
es5-ext "^0.10.50"
type "^1.0.1"
dagre@^0.8.5:
version "0.8.5"
resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
dependencies:
graphlib "^2.1.8"
lodash "^4.17.15"
damerau-levenshtein@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
@@ -3971,6 +3984,13 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
graphlib@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
dependencies:
lodash "^4.17.15"
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"