Skip to content
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 Map - Separate overlapping edges by rotating nodes #60477

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -57,6 +57,20 @@ 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1,000,000,000 points for using θs in variable names.

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,
Expand All @@ -71,10 +85,11 @@ function getLayoutOptions(
animate: true,
animationEasing: animationOptions.easing,
animationDuration: animationOptions.duration,
// Rotate nodes from top -> bottom to display left -> right
// @ts-ignore
transform: (node: any, { x, y }: cytoscape.Position) => ({ x: y, y: -x }),
// swap width/height of boundingBox to compensation for the rotation
// 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 }
};
}
Expand Down Expand Up @@ -109,20 +124,31 @@ export function Cytoscape({
// is required and can trigger rendering when changed.
const divStyle = { ...style, height };

const dataHandler = useCallback<cytoscape.EventHandler>(
event => {
const resetConnectedEdgeStyle = useCallback(
(node?: cytoscape.NodeSingular) => {
if (cy) {
cy.edges().removeClass('highlight');

if (serviceName) {
const focusedNode = cy.getElementById(serviceName);
focusedNode.connectedEdges().addClass('highlight');
if (node) {
node.connectedEdges().addClass('highlight');
}
}
},
[cy]
);

// Add the "primary" class to the node if its id matches the serviceName.
if (cy.nodes().length > 0 && serviceName) {
cy.nodes().removeClass('primary');
cy.getElementById(serviceName).addClass('primary');
const dataHandler = useCallback<cytoscape.EventHandler>(
event => {
if (cy) {
if (serviceName) {
resetConnectedEdgeStyle(cy.getElementById(serviceName));
// Add the "primary" class to the node if its id matches the serviceName.
if (cy.nodes().length > 0) {
cy.nodes().removeClass('primary');
cy.getElementById(serviceName).addClass('primary');
}
} else {
resetConnectedEdgeStyle();
}
if (event.cy.elements().length > 0) {
const selectedRoots = selectRoots(event.cy);
Expand All @@ -141,7 +167,7 @@ export function Cytoscape({
}
}
},
[cy, serviceName, height, width]
[cy, resetConnectedEdgeStyle, serviceName, height, width]
);

// Trigger a custom "data" event when data changes
Expand All @@ -162,12 +188,20 @@ export function Cytoscape({
event.target.removeClass('hover');
event.target.connectedEdges().removeClass('nodeHover');
};
const selectHandler: cytoscape.EventHandler = event => {
resetConnectedEdgeStyle(event.target);
};
const unselectHandler: cytoscape.EventHandler = event => {
resetConnectedEdgeStyle();
};

if (cy) {
cy.on('data', dataHandler);
cy.ready(dataHandler);
cy.on('mouseover', 'edge, node', mouseoverHandler);
cy.on('mouseout', 'edge, node', mouseoutHandler);
cy.on('select', 'node', selectHandler);
cy.on('unselect', 'node', unselectHandler);
}

return () => {
Expand All @@ -181,7 +215,7 @@ export function Cytoscape({
cy.removeListener('mouseout', 'edge, node', mouseoutHandler);
}
};
}, [cy, dataHandler, serviceName]);
}, [cy, dataHandler, resetConnectedEdgeStyle, serviceName]);

return (
<CytoscapeContext.Provider value={cy}>
Expand Down
Expand Up @@ -121,15 +121,18 @@ const style: cytoscape.Stylesheet[] = [
{
selector: 'edge.nodeHover',
style: {
width: 4,
width: 2,
// @ts-ignore
'z-index': zIndexEdgeHover
'z-index': zIndexEdgeHover,
'line-color': theme.euiColorDarkShade,
'source-arrow-color': theme.euiColorDarkShade,
'target-arrow-color': theme.euiColorDarkShade
}
},
{
selector: 'node.hover',
style: {
'border-width': 4
'border-width': 2
}
},
{
Expand Down
Expand Up @@ -79,7 +79,7 @@ export function KueryBar() {
const disabled = /\/service-map$/.test(location.pathname);
const disabledPlaceholder = i18n.translate(
'xpack.apm.kueryBar.disabledPlaceholder',
{ defaultMessage: 'Search is not available for service maps' }
{ defaultMessage: 'Search is not available for service map' }
);

async function onChange(inputValue: string, selectionStart: number) {
Expand Down