blob: 81ba457522b542c84a25a79fb99bbe6bb4ffce42 [file] [log] [blame]
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
global d3, localStorage, dagreD3, dagNodes, edges, arrange, document,
*/
const highlightColor = '#000000';
const upstreamColor = '#2020A0';
const downstreamColor = '#0000FF';
const initialStrokeWidth = '3px';
const highlightStrokeWidth = '5px';
const duration = 500;
let nodes = dagNodes;
const fullNodes = nodes;
const filteredNodes = nodes.filter((n) => edges.some((e) => e.u === n.id || e.v === n.id));
// Preparation of DagreD3 data structures
let g = new dagreD3.graphlib.Graph()
.setGraph({
nodesep: 15,
ranksep: 15,
rankdir: arrange,
})
.setDefaultEdgeLabel(() => ({ lineInterpolate: 'basis' }));
const render = dagreD3.render();
const svg = d3.select('#graph-svg');
const innerSvg = d3.select('#graph-svg g');
// Returns true if a node's id or its children's id matches search_text
function nodeMatches(nodeId, searchText) {
if (nodeId.indexOf(searchText) > -1) return true;
return false;
}
function highlightNodes(nodesToHighlight, color, strokeWidth) {
nodesToHighlight.forEach((nodeid) => {
const myNode = g.node(nodeid).elem;
d3.select(myNode)
.selectAll('rect,circle')
.style('stroke', color)
.style('stroke-width', strokeWidth);
});
}
let zoom = null;
function setUpZoomSupport() {
// Set up zoom support for Graph
zoom = d3.behavior.zoom().on('zoom', () => {
innerSvg.attr('transform', `translate(${d3.event.translate})scale(${d3.event.scale})`);
});
svg.call(zoom);
// Centering the DAG on load
// Get Dagre Graph dimensions
const graphWidth = g.graph().width;
const graphHeight = g.graph().height;
// Get SVG dimensions
const padding = 20;
const svgBb = svg.node().getBoundingClientRect();
const width = svgBb.width - padding * 2;
const height = svgBb.height - padding; // we are not centering the dag vertically
// Calculate applicable scale for zoom
const zoomScale = Math.min(
Math.min(width / graphWidth, height / graphHeight),
1.5, // cap zoom level to 1.5 so nodes are not too large
);
zoom.translate([(width / 2) - ((graphWidth * zoomScale) / 2) + padding, padding]);
zoom.scale(zoomScale);
zoom.event(innerSvg);
}
function setUpNodeHighlighting(focusItem = null) {
d3.selectAll('g.node').on('mouseover', function (d) {
d3.select(this).selectAll('rect').style('stroke', highlightColor);
highlightNodes(g.predecessors(d), upstreamColor, highlightStrokeWidth);
highlightNodes(g.successors(d), downstreamColor, highlightStrokeWidth);
const adjacentNodeNames = [d, ...g.predecessors(d), ...g.successors(d)];
d3.selectAll('g.nodes g.node')
.filter((x) => !adjacentNodeNames.includes(x))
.style('opacity', 0.2);
const adjacentEdges = g.nodeEdges(d);
d3.selectAll('g.edgePath')[0]
// eslint-disable-next-line no-underscore-dangle
.filter((x) => !adjacentEdges.includes(x.__data__))
.forEach((x) => {
d3.select(x).style('opacity', 0.2);
});
});
d3.selectAll('g.node').on('mouseout', function (d) {
d3.select(this).selectAll('rect,circle').style('stroke', null);
highlightNodes(g.predecessors(d), null, initialStrokeWidth);
highlightNodes(g.successors(d), null, initialStrokeWidth);
d3.selectAll('g.node')
.style('opacity', 1);
d3.selectAll('g.node rect')
.style('stroke-width', initialStrokeWidth);
d3.selectAll('g.edgePath')
.style('opacity', 1);
if (focusItem) {
localStorage.removeItem(focusItem);
}
});
}
function searchboxHighlighting(s) {
let match = null;
d3.selectAll('g.nodes g.node').filter(function forEach(d) {
if (s === '') {
d3.select('g.edgePaths')
.transition().duration(duration)
.style('opacity', 1);
d3.select(this)
.transition().duration(duration)
.style('opacity', 1)
.selectAll('rect')
.style('stroke-width', initialStrokeWidth);
} else {
d3.select('g.edgePaths')
.transition().duration(duration)
.style('opacity', 0.2);
if (nodeMatches(d, s)) {
if (!match) match = this;
d3.select(this)
.transition().duration(duration)
.style('opacity', 1)
.selectAll('rect')
.style('stroke-width', highlightStrokeWidth);
} else {
d3.select(this)
.transition()
.style('opacity', 0.2).duration(duration)
.selectAll('rect')
.style('stroke-width', initialStrokeWidth);
}
}
return null;
});
// This moves the matched node to the center of the graph area
if (match) {
const transform = d3.transform(d3.select(match).attr('transform'));
const svgBb = svg.node().getBoundingClientRect();
transform.translate = [
svgBb.width / 2 - transform.translate[0],
svgBb.height / 2 - transform.translate[1],
];
transform.scale = [1, 1];
if (zoom !== null) {
zoom.translate(transform.translate);
zoom.scale(1);
zoom.event(innerSvg);
}
}
}
d3.select('#searchbox').on('keyup', () => {
const s = document.getElementById('searchbox').value;
searchboxHighlighting(s);
});
const renderGraph = () => {
g = new dagreD3.graphlib.Graph()
.setGraph({
nodesep: 15,
ranksep: 15,
rankdir: arrange,
})
.setDefaultEdgeLabel(() => ({ lineInterpolate: 'basis' }));
// set nodes
nodes.forEach((node) => {
g.setNode(node.id, node.value);
});
// filter out edges that point to non-existent nodes
const realEdges = edges.filter((e) => {
const edgeNodes = nodes.filter((n) => n.id === e.u || n.id === e.v);
return edgeNodes.length === 2;
});
// Set edges
realEdges.forEach((edge) => {
g.setEdge(edge.u, edge.v, {
curve: d3.curveBasis,
arrowheadClass: 'arrowhead',
});
});
innerSvg.call(render, g);
setUpNodeHighlighting();
setUpZoomSupport();
};
// rerender graph when filtering dags with dependencies or not
document.getElementById('deps-filter').addEventListener('change', function onChange() {
// reset searchbox
document.getElementById('searchbox').value = '';
if (this.checked) {
nodes = filteredNodes;
} else {
nodes = fullNodes;
}
renderGraph();
});
// initial filter check and render
if (document.getElementById('deps-filter').checked) {
nodes = filteredNodes;
}
renderGraph();