Skip to content

Commit 9de2d81

Browse files
authored
[APM] Service Map - Separate overlapping edges by rotating nodes (#60477)
* Adds rotation transform which does the top->bottom to left->right transformation + an extra 5 degrees which results in taxi edges separating when rendered. * PR feedback to reduce edge width on hover, and assure that connected edges are highlighted when node is selected/focused * update disabled kuery bar placeholder text for service map
1 parent e731592 commit 9de2d81

File tree

3 files changed

+55
-18
lines changed

3 files changed

+55
-18
lines changed

x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ function useCytoscape(options: cytoscape.CytoscapeOptions) {
5757
return [ref, cy] as [React.MutableRefObject<any>, cytoscape.Core | undefined];
5858
}
5959

60+
function rotatePoint(
61+
{ x, y }: { x: number; y: number },
62+
degreesRotated: number
63+
) {
64+
const radiansPerDegree = Math.PI / 180;
65+
const θ = radiansPerDegree * degreesRotated;
66+
const cosθ = Math.cos(θ);
67+
const sinθ = Math.sin(θ);
68+
return {
69+
x: x * cosθ - y * sinθ,
70+
y: x * sinθ + y * cosθ
71+
};
72+
}
73+
6074
function getLayoutOptions(
6175
selectedRoots: string[],
6276
height: number,
@@ -71,10 +85,11 @@ function getLayoutOptions(
7185
animate: true,
7286
animationEasing: animationOptions.easing,
7387
animationDuration: animationOptions.duration,
74-
// Rotate nodes from top -> bottom to display left -> right
7588
// @ts-ignore
76-
transform: (node: any, { x, y }: cytoscape.Position) => ({ x: y, y: -x }),
77-
// swap width/height of boundingBox to compensation for the rotation
89+
// Rotate nodes counter-clockwise to transform layout from top→bottom to left→right.
90+
// The extra 5° achieves the effect of separating overlapping taxi-styled edges.
91+
transform: (node: any, pos: cytoscape.Position) => rotatePoint(pos, -95),
92+
// swap width/height of boundingBox to compensate for the rotation
7893
boundingBox: { x1: 0, y1: 0, w: height, h: width }
7994
};
8095
}
@@ -109,20 +124,31 @@ export function Cytoscape({
109124
// is required and can trigger rendering when changed.
110125
const divStyle = { ...style, height };
111126

112-
const dataHandler = useCallback<cytoscape.EventHandler>(
113-
event => {
127+
const resetConnectedEdgeStyle = useCallback(
128+
(node?: cytoscape.NodeSingular) => {
114129
if (cy) {
115130
cy.edges().removeClass('highlight');
116131

117-
if (serviceName) {
118-
const focusedNode = cy.getElementById(serviceName);
119-
focusedNode.connectedEdges().addClass('highlight');
132+
if (node) {
133+
node.connectedEdges().addClass('highlight');
120134
}
135+
}
136+
},
137+
[cy]
138+
);
121139

122-
// Add the "primary" class to the node if its id matches the serviceName.
123-
if (cy.nodes().length > 0 && serviceName) {
124-
cy.nodes().removeClass('primary');
125-
cy.getElementById(serviceName).addClass('primary');
140+
const dataHandler = useCallback<cytoscape.EventHandler>(
141+
event => {
142+
if (cy) {
143+
if (serviceName) {
144+
resetConnectedEdgeStyle(cy.getElementById(serviceName));
145+
// Add the "primary" class to the node if its id matches the serviceName.
146+
if (cy.nodes().length > 0) {
147+
cy.nodes().removeClass('primary');
148+
cy.getElementById(serviceName).addClass('primary');
149+
}
150+
} else {
151+
resetConnectedEdgeStyle();
126152
}
127153
if (event.cy.elements().length > 0) {
128154
const selectedRoots = selectRoots(event.cy);
@@ -141,7 +167,7 @@ export function Cytoscape({
141167
}
142168
}
143169
},
144-
[cy, serviceName, height, width]
170+
[cy, resetConnectedEdgeStyle, serviceName, height, width]
145171
);
146172

147173
// Trigger a custom "data" event when data changes
@@ -162,12 +188,20 @@ export function Cytoscape({
162188
event.target.removeClass('hover');
163189
event.target.connectedEdges().removeClass('nodeHover');
164190
};
191+
const selectHandler: cytoscape.EventHandler = event => {
192+
resetConnectedEdgeStyle(event.target);
193+
};
194+
const unselectHandler: cytoscape.EventHandler = event => {
195+
resetConnectedEdgeStyle();
196+
};
165197

166198
if (cy) {
167199
cy.on('data', dataHandler);
168200
cy.ready(dataHandler);
169201
cy.on('mouseover', 'edge, node', mouseoverHandler);
170202
cy.on('mouseout', 'edge, node', mouseoutHandler);
203+
cy.on('select', 'node', selectHandler);
204+
cy.on('unselect', 'node', unselectHandler);
171205
}
172206

173207
return () => {
@@ -181,7 +215,7 @@ export function Cytoscape({
181215
cy.removeListener('mouseout', 'edge, node', mouseoutHandler);
182216
}
183217
};
184-
}, [cy, dataHandler, serviceName]);
218+
}, [cy, dataHandler, resetConnectedEdgeStyle, serviceName]);
185219

186220
return (
187221
<CytoscapeContext.Provider value={cy}>

x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,18 @@ const style: cytoscape.Stylesheet[] = [
121121
{
122122
selector: 'edge.nodeHover',
123123
style: {
124-
width: 4,
124+
width: 2,
125125
// @ts-ignore
126-
'z-index': zIndexEdgeHover
126+
'z-index': zIndexEdgeHover,
127+
'line-color': theme.euiColorDarkShade,
128+
'source-arrow-color': theme.euiColorDarkShade,
129+
'target-arrow-color': theme.euiColorDarkShade
127130
}
128131
},
129132
{
130133
selector: 'node.hover',
131134
style: {
132-
'border-width': 4
135+
'border-width': 2
133136
}
134137
},
135138
{

x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export function KueryBar() {
7979
const disabled = /\/service-map$/.test(location.pathname);
8080
const disabledPlaceholder = i18n.translate(
8181
'xpack.apm.kueryBar.disabledPlaceholder',
82-
{ defaultMessage: 'Search is not available for service maps' }
82+
{ defaultMessage: 'Search is not available for service map' }
8383
);
8484

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

0 commit comments

Comments
 (0)