Skip to content

Commit 46c8375

Browse files
[ML] Data Frame Analytics map: remove cytoscape dependency (#263606)
## Summary This PR removes the cytoscape dependency for the map view and uses React Flow instead. This is consistent with the other plugins. https://github.com/user-attachments/assets/7ffe661c-3380-4124-b3f1-7de3d61da482 Fixes #260527 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [ ] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 3605c53 commit 46c8375

20 files changed

Lines changed: 1114 additions & 591 deletions

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,8 +1364,6 @@
13641364
"css-box-model": "1.2.1",
13651365
"css.escape": "1.5.1",
13661366
"cypress-data-session": "2.8.7",
1367-
"cytoscape": "3.31.2",
1368-
"cytoscape-dagre": "2.5.0",
13691367
"d3": "3.5.17",
13701368
"d3-array": "2.12.1",
13711369
"d3-brush": "3.0.0",

renovate.json

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2119,14 +2119,6 @@
21192119
"minimumReleaseAge": "14 days",
21202120
"enabled": true
21212121
},
2122-
{
2123-
"groupName": "Cytoscape",
2124-
"matchDepNames": ["cytoscape", "cytoscape-dagre"],
2125-
"reviewers": ["team:ml-ui"],
2126-
"matchBaseBranches": ["main"],
2127-
"minimumReleaseAge": "14 days",
2128-
"enabled": true
2129-
},
21302122
{
21312123
"groupName": "hdr-histogram-js",
21322124
"matchDepNames": ["hdr-histogram-js"],

typings/cytoscape_dagre.d.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
/**
9+
* Assigns (x, y) positions to React Flow nodes using the Dagre layered-graph
10+
* algorithm. React Flow requires explicit positions; without them every node
11+
* renders at (0, 0). Dagre places nodes in ranked columns (left-to-right by
12+
* default) so the job-map reads as a data-lineage pipeline. A square-grid
13+
* fallback is used if Dagre throws (e.g. the graph contains a cycle).
14+
*/
15+
16+
import Dagre from '@dagrejs/dagre';
17+
import type { Edge, Node } from '@xyflow/react';
18+
import {
19+
JOB_MAP_GRAPH_MARGIN,
20+
JOB_MAP_NODE_HEIGHT,
21+
JOB_MAP_NODE_SEPARATION,
22+
JOB_MAP_NODE_WIDTH,
23+
JOB_MAP_RANK_SEPARATION,
24+
} from './job_map_flow_constants';
25+
26+
export interface JobMapLayoutOptions {
27+
rankdir?: 'TB' | 'LR';
28+
ranksep?: number;
29+
nodesep?: number;
30+
marginx?: number;
31+
marginy?: number;
32+
nodeWidth?: number;
33+
nodeHeight?: number;
34+
}
35+
36+
const defaultOptions: Required<JobMapLayoutOptions> = {
37+
rankdir: 'LR',
38+
ranksep: JOB_MAP_RANK_SEPARATION,
39+
nodesep: JOB_MAP_NODE_SEPARATION,
40+
marginx: JOB_MAP_GRAPH_MARGIN,
41+
marginy: JOB_MAP_GRAPH_MARGIN,
42+
nodeWidth: JOB_MAP_NODE_WIDTH,
43+
nodeHeight: JOB_MAP_NODE_HEIGHT,
44+
};
45+
46+
function applyGridFallbackLayout<T extends Record<string, unknown>>(
47+
nodes: Node<T>[],
48+
opts: Required<JobMapLayoutOptions>
49+
): Node<T>[] {
50+
const cols = Math.ceil(Math.sqrt(nodes.length));
51+
return nodes.map((node, index) => {
52+
const col = index % cols;
53+
const row = Math.floor(index / cols);
54+
return {
55+
...node,
56+
position: {
57+
x: Math.round(opts.marginx + col * (opts.nodeWidth + opts.nodesep)),
58+
y: Math.round(opts.marginy + row * (opts.nodeHeight + opts.ranksep)),
59+
},
60+
};
61+
});
62+
}
63+
64+
export function applyJobMapDagreLayout<T extends Record<string, unknown>>(
65+
nodes: Node<T>[],
66+
edges: Edge[],
67+
options: JobMapLayoutOptions = {}
68+
): Node<T>[] {
69+
if (nodes.length === 0) {
70+
return nodes;
71+
}
72+
73+
const opts = { ...defaultOptions, ...options };
74+
75+
const g = new Dagre.graphlib.Graph({ directed: true, compound: false })
76+
.setGraph({
77+
rankdir: opts.rankdir,
78+
ranksep: opts.ranksep,
79+
nodesep: opts.nodesep,
80+
marginx: opts.marginx,
81+
marginy: opts.marginy,
82+
})
83+
.setDefaultEdgeLabel(() => ({}));
84+
85+
nodes.forEach((node) => {
86+
g.setNode(node.id, {
87+
width: opts.nodeWidth,
88+
height: opts.nodeHeight,
89+
});
90+
});
91+
92+
edges.forEach((edge) => {
93+
if (g.hasNode(edge.source) && g.hasNode(edge.target)) {
94+
g.setEdge(edge.source, edge.target);
95+
}
96+
});
97+
98+
try {
99+
Dagre.layout(g);
100+
} catch (err) {
101+
// eslint-disable-next-line no-console
102+
console.error('[JobMap] Dagre layout failed, falling back to grid layout:', err);
103+
return applyGridFallbackLayout(nodes, opts);
104+
}
105+
106+
return nodes.map((node) => {
107+
const dagreNode = g.node(node.id);
108+
if (!dagreNode) {
109+
return node;
110+
}
111+
return {
112+
...node,
113+
position: {
114+
x: Math.round(dagreNode.x - opts.nodeWidth / 2),
115+
y: Math.round(dagreNode.y - opts.nodeHeight / 2),
116+
},
117+
};
118+
});
119+
}

0 commit comments

Comments
 (0)