-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[APM] Service maps layout enhancements #76481
Changes from 4 commits
3a8a45a
4121458
0bfe1f1
cba22a8
20daa20
39420aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ import React, { | |
useState, | ||
} from 'react'; | ||
import cytoscape from 'cytoscape'; | ||
import dagre from 'cytoscape-dagre'; | ||
import { debounce } from 'lodash'; | ||
import { useTheme } from '../../../hooks/useTheme'; | ||
import { | ||
|
@@ -22,6 +23,8 @@ import { | |
} from './cytoscapeOptions'; | ||
import { useUiTracker } from '../../../../../observability/public'; | ||
|
||
cytoscape.use(dagre); | ||
|
||
export const CytoscapeContext = createContext<cytoscape.Core | undefined>( | ||
undefined | ||
); | ||
|
@@ -30,7 +33,6 @@ interface CytoscapeProps { | |
children?: ReactNode; | ||
elements: cytoscape.ElementDefinition[]; | ||
height: number; | ||
width: number; | ||
serviceName?: string; | ||
style?: CSSProperties; | ||
} | ||
|
@@ -57,59 +59,42 @@ function useCytoscape(options: cytoscape.CytoscapeOptions) { | |
return [ref, cy] as [React.MutableRefObject<any>, cytoscape.Core | undefined]; | ||
} | ||
|
||
function rotatePoint( | ||
{ x, y }: { x: number; y: number }, | ||
degreesRotated: number | ||
) { | ||
const radiansPerDegree = Math.PI / 180; | ||
const θ = radiansPerDegree * degreesRotated; | ||
const cosθ = Math.cos(θ); | ||
const sinθ = Math.sin(θ); | ||
return { | ||
x: x * cosθ - y * sinθ, | ||
y: x * sinθ + y * cosθ, | ||
}; | ||
} | ||
|
||
function getLayoutOptions( | ||
selectedRoots: string[], | ||
height: number, | ||
width: number, | ||
nodeHeight: number | ||
): cytoscape.LayoutOptions { | ||
function getLayoutOptions(nodeHeight: number): cytoscape.LayoutOptions { | ||
return { | ||
name: 'breadthfirst', | ||
// @ts-ignore DefinitelyTyped is incorrect here. Roots can be an Array | ||
roots: selectedRoots.length ? selectedRoots : undefined, | ||
name: 'dagre', | ||
fit: true, | ||
padding: nodeHeight, | ||
spacingFactor: 1.2, | ||
// @ts-ignore | ||
// Rotate nodes counter-clockwise to transform layout from top→bottom to left→right. | ||
// The extra 5° achieves the effect of separating overlapping taxi-styled edges. | ||
transform: (node: any, pos: cytoscape.Position) => rotatePoint(pos, -95), | ||
// swap width/height of boundingBox to compensate for the rotation | ||
boundingBox: { x1: 0, y1: 0, w: height, h: width }, | ||
nodeSep: nodeHeight, | ||
edgeSep: 32, | ||
rankSep: 128, | ||
rankDir: 'LR', | ||
ranker: 'network-simplex', | ||
}; | ||
} | ||
|
||
function selectRoots(cy: cytoscape.Core): string[] { | ||
const bfs = cy.elements().bfs({ | ||
roots: cy.elements().leaves(), | ||
function applyCubicBezierStyles(edges: cytoscape.EdgeCollection) { | ||
edges.forEach((edge) => { | ||
const { x: x0, y: y0 } = edge.source().position(); | ||
const { x: x1, y: y1 } = edge.target().position(); | ||
const x = x1 - x0; | ||
const y = y1 - y0; | ||
const z = Math.sqrt(x * x + y * y); | ||
const costheta = z === 0 ? 0 : x / z; | ||
const alpha = 0.25; | ||
edge.style('control-point-distances', [ | ||
-alpha * y * costheta, | ||
alpha * y * costheta, | ||
]); | ||
edge.style('control-point-weights', [alpha, 1 - alpha]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a comment to this section what this is doing? |
||
}); | ||
const furthestNodeFromLeaves = bfs.path.last(); | ||
return cy | ||
.elements() | ||
.roots() | ||
.union(furthestNodeFromLeaves) | ||
.map((el) => el.id()); | ||
} | ||
|
||
export function Cytoscape({ | ||
children, | ||
elements, | ||
height, | ||
width, | ||
serviceName, | ||
style, | ||
}: CytoscapeProps) { | ||
|
@@ -151,13 +136,7 @@ export function Cytoscape({ | |
} else { | ||
resetConnectedEdgeStyle(); | ||
} | ||
|
||
const selectedRoots = selectRoots(event.cy); | ||
const layout = cy.layout( | ||
getLayoutOptions(selectedRoots, height, width, nodeHeight) | ||
); | ||
|
||
layout.run(); | ||
cy.layout(getLayoutOptions(nodeHeight)).run(); | ||
} | ||
}; | ||
let layoutstopDelayTimeout: NodeJS.Timeout; | ||
|
@@ -180,6 +159,7 @@ export function Cytoscape({ | |
event.cy.fit(undefined, nodeHeight); | ||
} | ||
}, 0); | ||
applyCubicBezierStyles(event.cy.edges()); | ||
}; | ||
// debounce hover tracking so it doesn't spam telemetry with redundant events | ||
const trackNodeEdgeHover = debounce( | ||
|
@@ -211,6 +191,9 @@ export function Cytoscape({ | |
console.debug('cytoscape:', event); | ||
} | ||
}; | ||
const dragHandler: cytoscape.EventHandler = (event) => { | ||
applyCubicBezierStyles(event.target.connectedEdges()); | ||
}; | ||
|
||
if (cy) { | ||
cy.on('data layoutstop select unselect', debugHandler); | ||
|
@@ -220,6 +203,7 @@ export function Cytoscape({ | |
cy.on('mouseout', 'edge, node', mouseoutHandler); | ||
cy.on('select', 'node', selectHandler); | ||
cy.on('unselect', 'node', unselectHandler); | ||
cy.on('drag', 'node', dragHandler); | ||
|
||
cy.remove(cy.elements()); | ||
cy.add(elements); | ||
|
@@ -239,19 +223,11 @@ export function Cytoscape({ | |
cy.removeListener('mouseout', 'edge, node', mouseoutHandler); | ||
cy.removeListener('select', 'node', selectHandler); | ||
cy.removeListener('unselect', 'node', unselectHandler); | ||
cy.removeListener('drag', 'node', dragHandler); | ||
} | ||
clearTimeout(layoutstopDelayTimeout); | ||
}; | ||
}, [ | ||
cy, | ||
elements, | ||
height, | ||
serviceName, | ||
trackApmEvent, | ||
width, | ||
nodeHeight, | ||
theme, | ||
]); | ||
}, [cy, elements, height, serviceName, trackApmEvent, nodeHeight, theme]); | ||
|
||
return ( | ||
<CytoscapeContext.Provider value={cy}> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,13 +71,15 @@ export function Popover({ focusedServiceName }: PopoverProps) { | |
cy.on('select', 'node', selectHandler); | ||
cy.on('unselect', 'node', deselect); | ||
cy.on('data viewport', deselect); | ||
cy.on('drag', 'node', deselect); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will dragging cause the popover to show for a brief period of time? Or will it not be displayed at all? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Node dragging doesn't trigger the |
||
} | ||
|
||
return () => { | ||
if (cy) { | ||
cy.removeListener('select', 'node', selectHandler); | ||
cy.removeListener('unselect', 'node', deselect); | ||
cy.removeListener('data viewport', undefined, deselect); | ||
cy.removeListener('drag', 'node', deselect); | ||
} | ||
}; | ||
}, [cy, deselect]); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,13 +49,11 @@ storiesOf('app/ServiceMap/Cytoscape', module) | |
}, | ||
]; | ||
const height = 300; | ||
const width = 1340; | ||
const serviceName = 'opbeans-python'; | ||
return ( | ||
<Cytoscape | ||
elements={elements} | ||
height={height} | ||
width={width} | ||
serviceName={serviceName} | ||
/> | ||
); | ||
|
@@ -330,7 +328,7 @@ storiesOf('app/ServiceMap/Cytoscape', module) | |
}, | ||
}, | ||
]; | ||
return <Cytoscape elements={elements} height={600} width={1340} />; | ||
return <Cytoscape elements={elements} height={600} />; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did we hardcode the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, this is for storybook. Then it's not a big deal 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The width was needed to define the |
||
}, | ||
{ | ||
info: { propTables: false, source: false }, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
declare module 'cytoscape-dagre'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No more custom calculations? Feels good!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah the new layout supports left->right rank direction out of the box!