154 lines
4.6 KiB
JavaScript
154 lines
4.6 KiB
JavaScript
import * as d3 from 'd3';
|
|
import { LINK_SELECTOR, NODE_SELECTOR, IS_HIGHLIGHTED } from './constants';
|
|
|
|
export const highlightIn = 1;
|
|
export const highlightOut = 0.2;
|
|
|
|
const getCurrent = (idx, collection) => d3.select(collection[idx]);
|
|
const getLiveLinks = () => d3.selectAll(`.${LINK_SELECTOR}.${IS_HIGHLIGHTED}`);
|
|
const getOtherLinks = () => d3.selectAll(`.${LINK_SELECTOR}:not(.${IS_HIGHLIGHTED})`);
|
|
const getNodesNotLive = () => d3.selectAll(`.${NODE_SELECTOR}:not(.${IS_HIGHLIGHTED})`);
|
|
|
|
export const getLiveLinksAsDict = () => {
|
|
return Object.fromEntries(
|
|
getLiveLinks()
|
|
.data()
|
|
.map((d) => [d.uid, d]),
|
|
);
|
|
};
|
|
export const currentIsLive = (idx, collection) =>
|
|
getCurrent(idx, collection).classed(IS_HIGHLIGHTED);
|
|
|
|
const backgroundLinks = (selection) => selection.style('stroke-opacity', highlightOut);
|
|
const backgroundNodes = (selection) => selection.attr('stroke', '#f2f2f2');
|
|
const foregroundLinks = (selection) => selection.style('stroke-opacity', highlightIn);
|
|
const foregroundNodes = (selection) => selection.attr('stroke', (d) => d.color);
|
|
const renewLinks = (selection, baseOpacity) => selection.style('stroke-opacity', baseOpacity);
|
|
const renewNodes = (selection) => selection.attr('stroke', (d) => d.color);
|
|
|
|
export const getAllLinkAncestors = (node) => {
|
|
if (node.targetLinks) {
|
|
return node.targetLinks.flatMap((n) => {
|
|
return [n, ...getAllLinkAncestors(n.source)];
|
|
});
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
const getAllNodeAncestors = (node) => {
|
|
let allNodes = [];
|
|
|
|
if (node.targetLinks) {
|
|
allNodes = node.targetLinks.flatMap((n) => {
|
|
return getAllNodeAncestors(n.source);
|
|
});
|
|
}
|
|
|
|
return [...allNodes, node.uid];
|
|
};
|
|
|
|
export const highlightLinks = (d, idx, collection) => {
|
|
const currentLink = getCurrent(idx, collection);
|
|
const currentSourceNode = d3.select(`#${d.source.uid}`);
|
|
const currentTargetNode = d3.select(`#${d.target.uid}`);
|
|
|
|
/* Higlight selected link, de-emphasize others */
|
|
backgroundLinks(getOtherLinks());
|
|
foregroundLinks(currentLink);
|
|
|
|
/* Do the same to related nodes */
|
|
backgroundNodes(getNodesNotLive());
|
|
foregroundNodes(currentSourceNode);
|
|
foregroundNodes(currentTargetNode);
|
|
};
|
|
|
|
const highlightPath = (parentLinks, parentNodes) => {
|
|
/* de-emphasize everything else */
|
|
backgroundLinks(getOtherLinks());
|
|
backgroundNodes(getNodesNotLive());
|
|
|
|
/* highlight correct links */
|
|
parentLinks.forEach(({ uid }) => {
|
|
foregroundLinks(d3.select(`#${uid}`)).classed(IS_HIGHLIGHTED, true);
|
|
});
|
|
|
|
/* highlight correct nodes */
|
|
parentNodes.forEach((id) => {
|
|
foregroundNodes(d3.select(`#${id}`)).classed(IS_HIGHLIGHTED, true);
|
|
});
|
|
};
|
|
|
|
const restoreNodes = () => {
|
|
/*
|
|
When paths are unclicked, they can take down nodes that
|
|
are still in use for other paths. This checks the live paths and
|
|
rehighlights their nodes.
|
|
*/
|
|
|
|
getLiveLinks().each((d) => {
|
|
foregroundNodes(d3.select(`#${d.source.uid}`)).classed(IS_HIGHLIGHTED, true);
|
|
foregroundNodes(d3.select(`#${d.target.uid}`)).classed(IS_HIGHLIGHTED, true);
|
|
});
|
|
};
|
|
|
|
const restorePath = (parentLinks, parentNodes, baseOpacity) => {
|
|
parentLinks.forEach(({ uid }) => {
|
|
renewLinks(d3.select(`#${uid}`), baseOpacity).classed(IS_HIGHLIGHTED, false);
|
|
});
|
|
|
|
parentNodes.forEach((id) => {
|
|
d3.select(`#${id}`).classed(IS_HIGHLIGHTED, false);
|
|
});
|
|
|
|
if (d3.selectAll(`.${IS_HIGHLIGHTED}`).empty()) {
|
|
renewLinks(getOtherLinks(), baseOpacity);
|
|
renewNodes(getNodesNotLive());
|
|
return;
|
|
}
|
|
|
|
backgroundLinks(getOtherLinks());
|
|
backgroundNodes(getNodesNotLive());
|
|
restoreNodes();
|
|
};
|
|
|
|
export const restoreLinks = (baseOpacity) => {
|
|
/*
|
|
if there exist live links, reset to highlight out / pale
|
|
otherwise, reset to base
|
|
*/
|
|
|
|
if (d3.selectAll(`.${IS_HIGHLIGHTED}`).empty()) {
|
|
renewLinks(d3.selectAll(`.${LINK_SELECTOR}`), baseOpacity);
|
|
renewNodes(d3.selectAll(`.${NODE_SELECTOR}`));
|
|
return;
|
|
}
|
|
|
|
backgroundLinks(getOtherLinks());
|
|
backgroundNodes(getNodesNotLive());
|
|
};
|
|
|
|
export const toggleLinkHighlight = (baseOpacity, d, idx, collection) => {
|
|
if (currentIsLive(idx, collection)) {
|
|
restorePath([d], [d.source.uid, d.target.uid], baseOpacity);
|
|
restoreNodes();
|
|
return;
|
|
}
|
|
|
|
highlightPath([d], [d.source.uid, d.target.uid]);
|
|
};
|
|
|
|
export const togglePathHighlights = (baseOpacity, d, idx, collection) => {
|
|
const parentLinks = getAllLinkAncestors(d);
|
|
const parentNodes = getAllNodeAncestors(d);
|
|
const currentNode = getCurrent(idx, collection);
|
|
|
|
/* if this node is already live, make it unlive and reset its path */
|
|
if (currentIsLive(idx, collection)) {
|
|
currentNode.classed(IS_HIGHLIGHTED, false);
|
|
restorePath(parentLinks, parentNodes, baseOpacity);
|
|
return;
|
|
}
|
|
|
|
highlightPath(parentLinks, parentNodes);
|
|
};
|