Open iainhallam opened 6 months ago
Sorry for derailing this a bit, but for anyone looking for a quick workaround, you can style edges with filter: drop-shadow(...)
to symbolize very basic line crossings, like so:
.flowchart-link {
filter:
drop-shadow(0px 2px 0px black)
drop-shadow(2px 0px 0px black)
drop-shadow(0px -2px 0px black)
drop-shadow(-2px 0px 0px black)
}
The color of the shadow of course needs to be the same as the background, which in this case is black.
But note that is a very bad workaround, adding css filters to a graph with already like 15 edges (at least for me) hurts performance when the graph gets animated for example. It's just not a good idea in general but the best "easy" fix I could come up for now. And if you need more space between the lines at the crossings, you have to add even more shadows, like so for 4 pixels for example:
.flowchart-link {
filter:
drop-shadow(0px 2px 0px black)
drop-shadow(2px 0px 0px black)
drop-shadow(0px -2px 0px black)
drop-shadow(-2px 0px 0px black)
drop-shadow(0px 4px 0px black)
drop-shadow(4px 0px 0px black)
drop-shadow(0px -4px 0px black)
drop-shadow(-4px 0px 0px black)
}
But making it thicker obviously leads to problems like these:
And obviously this approach is utterly useless for graphs on backgrounds that are anything but a solid color.
Because this is not a long-term solution, I'm currently working on a way to detect line crossings from the output SVG and then changing those paths to get "actual" crossings - should be possible, technically...
Sorry again but ahh, I did it! Obviously still a hack, but a way better one.
Assume the following graph and look at all these crosssings:
%%{init:{"flowchart": { "defaultRenderer": "elk" }}}%%
graph TD
Amy --- Bob
Amy --- Christine
Amy --- Daniel
Amy --- Elizabeth
Amy --- Frank
Amy --- Gloria
Bob --- Christine
Bob --- Daniel
Bob --- Elizabeth
Bob --- Frank
Bob --- Gloria
Christine --- Daniel
Christine --- Elizabeth
Christine --- Frank
Christine --- Gloria
Daniel --- Elizabeth
Daniel --- Frank
Daniel --- Gloria
Elizabeth --- Frank
Elizabeth --- Gloria
Frank --- Gloria
Then render the graph definition and run the SVG string through my very badly written function addCrossings
before adding it to the DOM, like so:
import mermaid from "mermaid";
import SVGPathCommander from "svg-path-commander";
import { DOMParser, XMLSerializer } from "xmldom";
const getIntersectionOfSegments = (
[{ x: xA, y: yA }, { x: xB, y: yB }],
[{ x: xC, y: yC }, { x: xD, y: yD }]
) => {
const divisor = (((xA - xB) * (yC - yD)) - ((yA - yB) * (xC - xD)));
const firstNormalizedIntersection = (
(((xA - xC) * (yC - yD)) - ((yA - yC) * (xC - xD))) /
divisor
);
const secondNormalizedIntersecion = -(
(((xA - xB) * (yA - yC)) - ((yA - yB) * (xA - xC))) /
divisor
);
if (
firstNormalizedIntersection >= 0 &&
firstNormalizedIntersection <= 1 &&
secondNormalizedIntersecion >= 0 &&
secondNormalizedIntersecion <= 1
) {
const intersectionX = (xA + (firstNormalizedIntersection * (xB - xA)));
const intersectionY = (yA + (firstNormalizedIntersection * (yB - yA)));
return {
x: intersectionX,
y: intersectionY
};
}
return null;
};
const parser = new DOMParser();
const serializer = new XMLSerializer();
const padding = 5;
const addCrossings = (svgString) => {
const svgDocument = parser.parseFromString(svgString, "text/xml");
const edgeNodes = [
...[...svgDocument.childNodes[0].childNodes]
.find((childNode) => childNode.tagName === "g" && childNode.getAttribute("class") === "edges edgePath").childNodes
]
.filter(({ tagName }) => tagName === "path");
for (const [index, edgeNode1] of edgeNodes.entries()) {
const pathString1 = edgeNode1.getAttribute("d");
const path1 = new SVGPathCommander(pathString1);
for (const edgeNode2 of edgeNodes.slice(index + 1)) {
const pathString2 = edgeNode2.getAttribute("d");
const path2 = new SVGPathCommander(pathString2);
const {
segments: points1
} = path1;
const {
segments: points2
} = path2;
const paddedPoints1 = [];
for (const [point1Index, point1] of points1.slice(0, -1).entries()) {
const [
command1,
x1,
y1
] = point1;
const [
command2,
x2,
y2
] = points1[point1Index + 1];
const orientation1 = x1 === x2 ? "vertical" : "horizontal";
const direction1 = orientation1 === "vertical"
? (
y1 <= y2
? "down"
: "up"
)
: (
x1 <= x2
? "right"
: "left"
);
paddedPoints1.push(point1);
for (const [point2Index, point2] of points2.slice(0, -1).entries()) {
const [
command3,
x3,
y3
] = point2;
const [
command4,
x4,
y4
] = points2[point2Index + 1];
const intersection = getIntersectionOfSegments(
[
{
x: x1,
y: y1
},
{
x: x2,
y: y2
}
],
[
{
x: x3,
y: y3
},
{
x: x4,
y: y4
}
]
);
if (intersection) {
paddedPoints1.push(
[
"L",
intersection.x -
(
orientation1 === "horizontal"
? padding * (direction1 === "left" ? -1 : 1)
: 0
),
intersection.y -
(
orientation1 === "vertical"
? padding * (direction1 === "up" ? -1 : 1)
: 0
)
],
[
"M",
intersection.x +
(
orientation1 === "horizontal"
? padding * (direction1 === "left" ? -1 : 1)
: 0
),
intersection.y +
(
orientation1 === "vertical"
? padding * (direction1 === "up" ? -1 : 1)
: 0
)
]
);
}
}
}
paddedPoints1.push(points1.at(-1));
path1.segments = paddedPoints1;
edgeNode1.setAttribute("d", path1.toString());
}
}
return serializer.serializeToString(svgDocument);
};
const graphDefinition = `...`;
const { svg: renderedSvgString } = await mermaid.render("graph", graphDefinition);
const svgStringWithCrossings = addCrossings(renderedSvgString);
This was done with the elk renderer in mind because I use it, so it probably needs some tinkering to make it more generic, but feel free to yank this code and do something better with it.
Description
334 was closed by a bot, but it's a key feature for serious diagramming work. Lines that cross should be able to be visually indicated, ideally in a user-selected choice of things like little arcs, or a break in the rear-most line.
Steps to reproduce
Create any diagram with crossing lines. See the slightly misleading crosses.
Screenshots
No response
Code Sample
No response
Setup
Suggested Solutions
A config option for the diagram on how to show crossing lines.
Additional Context
No response