Skip to content

Instantly share code, notes, and snippets.

@steveroush
Created December 19, 2023 19:05
Show Gist options
  • Save steveroush/21f1a85ea662c6bf880db26c93c1afcd to your computer and use it in GitHub Desktop.
Save steveroush/21f1a85ea662c6bf880db26c93c1afcd to your computer and use it in GitHub Desktop.
Graphviz: fixOrtho - a work-around to allow ortho edges to connect to ports
/*
neato complains about overlapping nodes, kicks our warning message (see below),
and uses splines=false.
"Warning: the bounding boxes of some nodes touch - falling back to straight
line edges"
- to do:
- multi-edge??
how to use:
dot -Gsplines=true myFile.gv |
gvpr -cf fixOrtho.gvpr |
neato -n1 -Tpng -Gsplines=ortho >myFile.png
OR
dot -Gnodesep=.8 -Gsplines=true myFile.gv |gvpr -cf fixOrtho.gvpr | neato -Tpng -n1 -Gsplines=ortho myFile.png
options: -a S << will show new node(s) and edges in red
*/
BEGIN{
graph_t Root;
int nodeNo=0, show;
int HeadsTails[], changeTail[], changeHead[], ignoreEdge[];
node_t newNode[];
string tailPt[], headPt[], nextToTail[], nextToHead[], whichPts[], xxxx;
float M, B; // slope
//////////////////////////////////////////////////////////////
void doErr(string errString) {
print("// Error: ", errString);
printf(2,"Error: %s\n", errString);
}
//////////////////////////////////////////////////////////////
void doMsg(string errString) {
print("// Note: ", errString);
//printf(2,"Note: %s\n", errString);
}
/////////////////////////////////////////////////////////////
float Max(float f1, float f2) {
float fx;
if (f1>f2)
fx=f1;
else
fx=f2;
return fx;
}
/////////////////////////////////////////////////////////////
float Min(float f1, float f2) {
float fx;
if (f1>f2)
fx=f2;
else
fx=f1;
return fx;
}
/////////////////////////////////////////////////////////////
float abs(float f1) {
float fx;
if (f1<0)
fx=-f1;
else
fx=f1;
return fx;
}
/////////////////////////////////////////////////////////////
// greater than, with some fudge for floating point values
int greater(float f1, float f2) {
int rc;
if (abs(f1)-abs(f2)>.09)
rc=1;
else
rc=0;
print("// greater : ", f1, " ", f2, " ",rc);
return rc;
}
/////////////////////////////////////////////////////////////
// less than, with some fudge for floating point values
int less(float f1, float f2) {
int rc;
if (abs(f1)-abs(f2)<-.09)
rc=1;
else
rc=0;
print("// less : ", f1, " ", f2, " ",rc);
return rc;
}
/////////////////////////////////////////////////////////////
// equal to, with some fudge for floating point values
int equal(float f1, float f2) {
int rc;
if (!(less(f1, f2)) && !(greater(f1,f2)))
rc=1;
else
rc=0;
print("// equal : ", f1, " ", f2, " ",rc);
return rc;
}
//////////////////////////////////////////////////////////////
// compute distance in points - always positive value
float distance(float x1,float y1,float x2,float y2) {
float di;
di=sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
return di;
}
//////////////////////////////////////////////////////////////
// compute distance in points - always positive value
float distancePtPt(string Pt1, string Pt2) {
float dist;
dist=distance((float)xOf(Pt1), (float)yOf(Pt1), (float)xOf(Pt2), (float)yOf(Pt2));
return dist;
}
/////////////////////////////////////////////////////////////
string whichSide() { // cannot pass an array, so we will use global array, yuck
float whichPtX[], whichPtY[], whichDX, whichDY, tmpX, tmpY;
int fi;
string rstr;
// 1 & 2 index 2 corners of node
// 3 is index of landing pt other end of edge
// 4 & 5 are two alternatives for comptued end pt of pseudo-ortho segment
for (whichPts[fi]) {
sscanf (whichPts[fi], "%lf,%lf", &tmpX, &tmpY);
whichPtX[fi]=tmpX;
whichPtY[fi]=tmpY;
print("// whichPts : ", fi, " ",whichPts[fi], " ",whichPtX[fi]," --- ",whichPtY[fi]);
}
whichDX=whichPtX[1]-whichPtX[2];
whichDY=whichPtY[1]-whichPtY[2];
if (whichDX==0)
M=9999999; // fudge - no divide by 0
else
M=whichDY/whichDX;
B=whichPtY[1]-(M*whichPtX[1]);
print("// equation M: ", M," B: ",B, " whichDX: ", whichDY);
if ((whichPtY[3] > ((M*whichPtX[3])+B) && whichPtY[4] > ((M*whichPtX[4])+B)) ||
(whichPtY[3] < ((M*whichPtX[3])+B) && whichPtY[4] < ((M*whichPtX[4])+B)) ||
(whichPtY[3] == ((M*whichPtX[3])+B) && whichPtY[4] == ((M*whichPtX[4])+B)))
rstr=whichPts[4];
else
rstr=whichPts[5];
unset (whichPtX);
print("// returning : ",rstr);
return rstr;
}
////////////////////////////////////////////////////
node_t createNode(edge_t thisEdge, string HT, string portPt, string nextToPortPt,
int hasArrowhead, string otherEdgePt, string portStr) {
node_t TN, thisNode, otherNode, thisTail, thisHead; // are all these used??????
float portPtX, portPtY, otherEndPtX, otherEndPtY, dx, dy;
float arrowSize, arrowX1, arrowX2, arrowY1, arrowY2;
float _left, _right, _top, _bottom, dAX, dAY, toGoX, toGoY;
float _centerX, _centerY, tmpPtX, tmpPtY;
string P, center;
float marg;
/////// marg, seemingly minimum of 4, look at pmargin in neatosplines.c
marg=6;
if (HT=="T") {
thisNode=thisEdge.tail;
otherNode=thisEdge.head;
} else {
thisNode=thisEdge.head;
otherNode=thisEdge.tail;
}
center=thisNode.pos;
print("// START Createnode: ", thisNode, " : ", otherNode, " : ", portPt, " : ", nextToPortPt, " : ",otherEdgePt, " : ", portStr);
////////////////////////////////////////////////
//
// should we drop this test ??? (newNode[portPt]!=NULL)
//
////////////////////////////////////////////////
if (newNode[portPt]!=NULL) {
TN=newNode[portPt];
} else {
arrowX1=(float)xOf(portPt);
arrowY1=(float)yOf(portPt);
arrowX2=(float)xOf(nextToPortPt);
arrowY2=(float)yOf(nextToPortPt);
if (hasArrowhead)
arrowSize=distance(arrowX1, arrowY1, arrowX2, arrowY2);
else
arrowSize=0;
print("// arrowsize: ", portPt, " -- ", nextToPortPt, " -- ", arrowSize);
_centerX=(float)xOf(thisNode.pos);
_centerY=(float)yOf(thisNode.pos);
_left=_centerX-((float)thisNode.width*72./2.);
_right=_centerX+((float)thisNode.width*72./2.);
_bottom=_centerY-((float)thisNode.height*72./2.);
_top=_centerY+((float)thisNode.height*72./2.);
marg+=arrowSize;
if (arrowSize==0)
arrowSize=4;
portPtX=(float)xOf(portPt);
portPtY=(float)yOf(portPt);
otherEndPtX=(float)xOf(otherEdgePt);
otherEndPtY=(float)yOf(otherEdgePt);
if (HT=="T") {
dx=otherEndPtX-portPtX;
dy=otherEndPtY-portPtY;
} else {
dx=portPtX-otherEndPtX;
dy=portPtY-otherEndPtY;
}
P=sub(portStr,"*:","");
print("// createnode (switch): ", portStr," ",P, " dx: ", dx," dy: ",dy);
whichPts[1]=portPt;
whichPts[3]=otherEdgePt;
switch(portStr) {
case "n":
xxxx=(string)(portPtX) + "," + (string)(portPtY+marg);
break;
case "s":
xxxx=(string)(portPtX) + "," + (string)(portPtY-marg);
break;
case "e":
xxxx=(string)(portPtX+marg) + "," + (string)(portPtY);
break;
case "w":
xxxx=(string)(portPtX-marg) + "," + (string)(portPtY);
break;
case "sw":
whichPts[2]=(string)_right + "," + (string)_top; // opposite corner
whichPts[4]=(string)(portPtX -marg) + "," + (string)(portPtY + 0 );
whichPts[5]=(string)(portPtX +0 ) + "," + (string)(portPtY - marg);
xxxx=whichSide();
break;
case "se":
whichPts[2]=(string)_left + "," + (string)_top; // opposite corner
whichPts[4]=(string)(portPtX +marg) + "," + (string)(portPtY + 0 );
whichPts[5]=(string)(portPtX +0 ) + "," + (string)(portPtY - marg);
xxxx=whichSide();
break;
case "nw":
whichPts[2]=(string)_right + "," + (string)_bottom; // opposite corner
whichPts[4]=(string)(portPtX -marg) + "," + (string)(portPtY + 0 );
whichPts[5]=(string)(portPtX +0 ) + "," + (string)(portPtY + marg);
xxxx=whichSide();
break;
case "ne":
whichPts[2]=(string)_left + "," + (string)_bottom; // opposite corner
whichPts[4]=(string)(portPtX +marg) + "," + (string)(portPtY + 0 );
whichPts[5]=(string)(portPtX +0 ) + "," + (string)(portPtY + marg);
xxxx=whichSide();
break;
default: { // c (center) or record/html port name
print("// corner compass point OR INSIDE node or RECORD port");
print("// _left, _right, _top, _bottom : ", _left, " : ", _right, " : ", _top, " : ", _bottom);
print("// portPtX, portPtY : ", portPtX, " : ", portPtY) ;
tmpPtX=portPtX;
tmpPtY=portPtY;
if (P=="@(ne|nw|se|sw)" || less(_left,tmpPtX) && greater(_right,tmpPtX) && less(_bottom,tmpPtY) && greater(_top,tmpPtY)) {
// inside the node
print("// INSIDE a node OR compass corner");
print("// arrowX1, arrowX2, arrowY1, arrowY2: ", arrowX1, " - ", arrowX2, " - ", arrowY1," - ", arrowY2);
dAX=abs(arrowX1-arrowX2);
dAY=abs(arrowY1-arrowY2);
dx=0;
dy=0;
if (dAY>dAX) { // go vertical
print("// vertical: ", dAX," ",dAY);
if (tmpPtY>_top || tmpPtY<_bottom)
toGoY=0;
else if (tmpPtY>_centerY)
toGoY=_top-tmpPtY;
else
toGoY=_bottom-tmpPtY;
dy=marg+Max(arrowSize, abs(toGoY));
print("// toGoY : ",toGoY, " dy : ",dy);
if (tmpPtY<_centerY)
dy=-dy;
tmpPtY+=dy;
} else {
print("// horizontal: ", dAX," ",dAY);
if (tmpPtX>_right || tmpPtX<_left)
toGoX=0;
else if (tmpPtX>_centerX)
toGoX=_right-tmpPtX;
else
toGoX=_left-tmpPtX;
dx=marg+Max(arrowSize, abs(toGoX));
print("// toGoX : ",toGoX, " dx : ",dx);
if (tmpPtX<_centerX)
dx=-dx;
tmpPtX+=dx;
}
} else {
// not compass based, but touching a side of the node
// (maybe two sides, but we will ignore that for now)
print("// Touching a Side");
if (equal(_left,tmpPtX)) {
tmpPtX-=marg;
} else if (equal(_right,tmpPtX)) {
tmpPtX+=marg;
} else if (equal(_bottom,tmpPtY)) {
tmpPtY-=marg;
} else if (equal(_top,tmpPtY)) {
tmpPtY+=marg;
}
}
xxxx=(string)(tmpPtX) + "," + (string)(tmpPtY);
break;
}
}
TN=node(Root, "__portNode__" + thisNode.name + "_"+ (string)++nodeNo);
TN.shape="point";
TN.label="";
TN.savePos=portPt;
TN.pos=xxxx;
print("// createnode portPtX & portPtY: ",portPtX," ",portPtY," pos: ", TN.pos," marg: ",marg);
if (show) {
TN.color="red";
} else {
if (hasAttr(thisEdge, "color"))
TN.color=thisEdge.color;
TN.width=0;
TN.height=0;
}
newNode[portPt]=TN;
}
return TN;
}
////////////////////////////////////////////////////
}
BEG_G{
int i, cnt, Head, Tail, new=0, hasTailArrow[], hasHeadArrow[];
node_t aN, tN, hN;
string eString, point[int];
string Dir1[], Dir2[], DIR;
edge_t oldE, newE, workE;
node_t tmpN;
int deleteIt[];
Root=$G;
if ($G.directed==1)
eString="->";
else
eString="--";
if (ARGC ==1 && ARGV[0]=="[sS]")
show=1;
else
show=0;
Dir1[""]="none";
Dir2[""]="forward";
Dir1["forward"]="none";
Dir2["forward"]="forward";
Dir1["back"]="back";
Dir2["back"]="none";
Dir1["none"]="none";
Dir2["none"]="none";
Dir1["both"]="back";
Dir2["both"]="forward";
}
E{
int needNewEdges=0;
string s;
if (ignoreEdge[$]==1)
continue;
unset(point);
print("// EDGE : ", $.name);
print("// pos: ", $.pos);
cnt=tokens($.pos, point);
/*
does not handle multiedges(?) (pos=spline ; spline ..."
*/
Tail=0;
Head=cnt-1;
for (i=0; i<cnt; i++) {
print("// point: ",i," ",point[i]);
s=point[i];
// arrowheads?
if (point[i]=="[se]*") {
print("// arrowhead : ",point[i]);
s=substr(point[i],2);
if (point[i]=="s*") {
Tail=i;
hasTailArrow[$]=1;
print("// Tail: ",Tail," Head: ", Head);
print("// POINT: ", point[i]);
point[i]=s;
print("// POINT: ", point[i]);
} else if (point[i]=="e*") {
Head=i;
hasHeadArrow[$]=1;
// if no tail (but head), set to 1
if (Tail==Head) {
Tail++;
}
print("// Head: ", Head, " Tail: ",Tail);
print("// POINT: ", point[i]);
point[i]=s;
print("// POINT: ", point[i]);
}
}
}
tailPt[$]=point[Tail];
headPt[$]=point[Head];
if (hasTailArrow[$]==1) {
if (hasHeadArrow[$]==1) { // both
nextToTail[$]=point[Head+1];
nextToHead[$]=point[cnt-1];
} else { // back
nextToTail[$]=point[Tail+1];
nextToHead[$]=point[Head-1];
}
} else {
if (hasHeadArrow[$]==1) { // forward
nextToTail[$]=point[Tail+1];
nextToHead[$]=point[cnt-1];
} else { // none
nextToTail[$]=point[Tail+1];
nextToHead[$]=point[cnt-2];
}
}
tN=$.tail;
hN=$.head;
print("// edge: ", $.name);
if (hasAttr($, "tailport") && $.tailport!="") {
print("// tailport: ", $.tailport, ", ", point[Tail], " next: ", nextToTail[$]);
changeTail[$]=1;
HeadsTails[$]++;
needNewEdges=1;
}
if (hasAttr($, "headport") && $.headport!="") {
print("// headport: ", $.headport, ", ", point[Head], " next: ", nextToHead[$]);
changeHead[$]=1;
HeadsTails[$]++;
needNewEdges=1;
}
// if (needNewEdges==0)
// $.pin="true";
workE=$;
oldE=$;
if (hasAttr($, "dir"))
DIR=$.dir;
else
DIR="";
if (changeTail[oldE]) {
print("// changeTAIL - ",oldE.name);
tmpN=createNode(workE, "T", tailPt[oldE], nextToTail[oldE], hasTailArrow[oldE], headPt[oldE], workE.tailport);
newE=edge(workE.tail, tmpN, "");
ignoreEdge[newE]=1;
copyA(workE, newE);
newE.headport="";
newE.dir=Dir1[DIR];
newE.pin="true";
if (show) {
newE.color="red";
}
////// edge #2 (reduced version of workE)
newE=edge(tmpN, workE.head, "");
ignoreEdge[newE]=1;
copyA(workE, newE);
newE.tailport="";
newE.dir=Dir2[DIR];
// fix certain attributes ??
deleteIt[workE]=1;
workE=newE; // if changeHead also, use this (new) edge
}
/********
if headport, we are creating a newnode->newnode loop edge,
in addition to correct edges
*******/
if (changeHead[oldE]) {
print("// changeHEAD - ",oldE.name);
tmpN=createNode(workE, "H", headPt[oldE], nextToHead[oldE], hasHeadArrow[oldE], tailPt[oldE], workE.headport);
newE=edge(tmpN, workE.head, "");
ignoreEdge[newE]=1;
copyA(workE, newE);
newE.tailport="";
newE.dir=Dir2[DIR];
newE.pin="true";
if (show) {
newE.color="red";
}
////// edge #2 (reduced version of workE)
newE=edge(workE.tail, tmpN, "");
ignoreEdge[newE]=1;
copyA(workE, newE);
newE.headport="";
newE.dir=Dir1[DIR];
// fix certain attributes ??
deleteIt[workE]=1;
}
}
END_G{
print("////////////////////////// END_G");
for (deleteIt[oldE]) {
delete(Root, oldE);
}
$G.splines="ortho";
}
@steveroush
Copy link
Author

The ortho edge implementation does not handle ports (see https://forum.graphviz.org/t/regarding-graphvizs-orthogonal-edge-routing/1889/2, https://gitlab.com/graphviz/graphviz/-/issues/352, or https://gitlab.com/graphviz/graphviz/-/issues/1415)

Here is a link (https://gist.github.com/steveroush/21f1a85ea662c6bf880db26c93c1afcd) to a gvpr program that will often allow ortho edges to connect to ports.
It is easy, but not trivial to use:

  1. run dot -Gsplines=true myFile.gv > myFile.dot` (This will position all the nodes and ports)
  2. run gvpr -cf fixOrtho.gvpr myFile.dot >myfileFixed.dot (This adds nodes and edges and alters existing edges)
  3. run neato -n -Tpng myFileFixed.dot >myfileFixed.png (Creates viewable image)

Note: the commands above can also be run as a single pipeline.

This is modestly tested software (surprisingly complex). Please try it and comment here. If/when it settles down, it can become part of the Graphviz package it that is desired, though I consider this a temporary "fix".

Here are three versions of the ports.gv file that is included in the Graphviz source. (The red edge components are just to show the changes)
ports montage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment