8

Is it possible to have double edge labels displayed by a graph?

I tried

EdgeLabels -> {Placed["Index", 0.5], Placed["Name", 0.25]}

but it didn't work.

kglr
  • 394,356
  • 18
  • 477
  • 896
Gae P
  • 637
  • 3
  • 11
  • 2
    This is how it would work in general, and this is how it works for vertices: Graph[{Labeled[1, Placed[{"foo", "bar"}, {Below, Above}]], 2}, {1 <-> 2}] or Graph[{"a", "b"}, {"a" <-> "b"}, VertexLabels -> Placed[{"Name", "Index"}, {Above, Below}]]. I can't get this to work for edges though. Placed is not accepted. – Szabolcs Feb 19 '19 at 16:15
  • @Szabolcs. Your code works fine for vertex labeling, but not for edge labeling. – m_goldberg Feb 20 '19 at 04:21
  • @m_goldberg Yes, that's what I said. Edge labels need a different (nonstandard) syntax for Placed, and it does not seem to work with multiple labels. – Szabolcs Feb 20 '19 at 08:52

3 Answers3

10

An approach using a custom EdgeShapeFunction:

The function eSF takes a Graph object (g) and a list of {label, position, offset} triples as input to label the edges of g.

The advantage of this approach is that it can handle curved edges easily.

ClearAll[eSF]
eSF[g_Graph, lbls_] := Module[{pts = #, bsF = BSplineFunction[#], e = #2}, 
  {If[DirectedGraphQ[g], Arrow[pts, .07], Line[pts]], 
 Text[Framed[# /. {"Name" -> e, "Index" -> EdgeIndex[g, e], 
        p_ :> (PropertyValue[g, p] /. $Failed -> p)}, 
     FrameStyle -> None, FrameMargins -> 0], bsF[#2], #3, bsF'[#2],
     Background -> If[Abs[#3[[2]]] == 0, White, None]] & @@@ lbls}] &;

Examples:

g1 = CycleGraph[5, VertexLabels -> Placed["Name", Center], 
   VertexSize -> Scaled[.07],  VertexLabelStyle -> 16, 
   BaseStyle -> {FontSize -> 14, FontColor -> Black}, ImageSize -> Medium];
g2 = DirectedGraph[g1, VertexLabels -> Placed["Name", Center], 
   VertexSize -> Scaled[.07],  VertexLabelStyle -> 16, 
   BaseStyle -> {FontSize -> 14, FontColor -> Black}, 
   ImageSize -> Medium, PerformanceGoal -> "Quality",
   GraphLayout -> {"EdgeLayout" -> {"DividedEdgeBundling", 
       "CoulombConstant" -> -15, "VelocityDamping" -> .2, 
       "NewForce" -> False, "Compatibility" -> False}}];

Row[{g1, g2}]

enter image description here

txts = {"Name", "Index"};
pos1 = {.25, .75}; off1 = {{0, 0}, {0, 0}};
pos2 = {.5, .5}; off2 = {{0, -1}, {0, 1}};

lbls1 = Thread[{txts, pos1, off1}]

{{"Name", 0.25, {0, 0}}, {"Index", 0.75, {0, 0}}}

lbls2 = Thread[{txts, pos2, off2}]

{{"Name", 0.5, {0, -1}}, {"Index", 0.5, {0, 1}}}

Grid[Table[SetProperty[#, EdgeShapeFunction -> eSF[#, l]] & /@ {g1, g2},
  {l, {lbls1, lbls2}}]]

enter image description here

If needed, we can also use it with different labeling settings for each edge:

txts2 = {"label1", "label2"};
lbls3 = Thread[{txts2, pos1, off1}]

{{"label1", 0.25, {0, 0}}, {"label2", 0.75, {0, 0}}}

Grid@Table[Graph[VertexList[#], EdgeList[#], 
     EdgeShapeFunction -> { _ :> eSF[#, lbl], #2 -> eSF[#, lbls3]}, 
     Options[#]] & @@@ 
   Transpose[{{g1, g2}, {2 \[UndirectedEdge] 3, 2 \[DirectedEdge] 3}}],
 {lbl, {lbls1, lbls2}}]

enter image description here

Using the form EdgeShapeFunction -> {e_:> func} we can use eSF to label edges individually:

SeedRandom[1]
randomlabels = Thread[{With[{col = RandomColor[]}, 
       Style[#, Opacity[1], FontSize -> Scaled[.04], col] & /@ 
        StringTake[#, UpTo[4]]], {.2, .5, .8}, {0, 0}}, List, 2] & /@ 
   Partition[RandomWord["Noun", 3 EdgeCount[g2]], 3];

labeling = AssociationThread[EdgeList[g2] -> randomlabels];

Graph[VertexList[g2], EdgeList[g2], 
 EdgeShapeFunction -> {e_ :> eSF[g2, labeling[e]]}, 
 EdgeStyle -> Directive[Arrowheads[0], AbsoluteThickness[3]], 
 ImageSize -> 500, Options[g2]]

enter image description here

Credit: I learned about the many parameters of "DividedEdgeBundling" from this answer by István Zachar.

kglr
  • 394,356
  • 18
  • 477
  • 896
5

There is a standard way to add more than one label with Placed, but unfortunately this does not work with edge labels. With vertex labels it would work like this:

Graph[{"a", "b"}, {"a" <-> "b"}, 
 VertexLabels -> Placed[{"Name", "Index"}, {Above, Below}]]

enter image description here

But EdgeLabels uses a special position specification in Placed, and it does not seem to be compatible with multiple labels.

Instead, we can add and position the labels manually.

First, we will need a function that returns an index-based edge list. I recommend the highly optimized IGIndexEdgeList from IGraph/M, but you can also do this without packages. Thus use either

<<IGraphM`
indexEdgeList = IGIndexEdgeList

or

indexEdgeList = List @@@ EdgeList@IndexGraph[#] &

Here's a test graph:

g = CycleGraph[5]

enter image description here

Get the point pair describing each edge:

pts = GraphEmbedding[g];
ptPairs = pts[[#]] & /@ indexEdgeList[g];

Create a labelling function:

Clear[position]
position[p_][label_, {a_, b_}] := Text[label, a + (b - a) p]

Add the labels through Epilog:

Graph[
 g,
 Epilog -> {
   MapThread[position[0.25], {Range@EdgeCount[g], ptPairs}],
   MapThread[position[0.75], {EdgeList[g], ptPairs}]
   }
 ]

enter image description here

We could improve on this with more advanced labelling functions:

Clear[position]
position[p_, offset_: {0, 0}][label_, {a_, b_}] := 
 With[{pt = a + (b - a) p},
  Rotate[
    Text[
      label,
      pt,
      offset,
      Background -> White
    ],
    ArcTan @@ Subtract @@ ReverseSort[{a, b}],
    pt
  ]
 ]

Graph[
 g,
 Epilog -> {
   MapThread[position[0.25], {Range@EdgeCount[g], ptPairs}],
   MapThread[position[0.75], {EdgeList[g], ptPairs}]
   }
 ]

enter image description here

Graph[
 g,
 Epilog -> {
   MapThread[position[0.5, {0, -2}], {Range@EdgeCount[g], ptPairs}],
   MapThread[position[0.5, {0, 2}], {EdgeList[g], ptPairs}]
   },
 PlotRangePadding -> Scaled[.05]
 ]

enter image description here

You could adapt this to your own use cases. Instead of setting Background -> White in Text (which was the simplest way), I recommend adding a Framed to the labels.

For example,

Framed[label,
 FrameStyle -> None, (* usually  we just want the background, not the frame *)
 Background -> LightBlue, (* make it White to mask the objects behind the label *)
 FrameMargins -> 1, (* tune the margin to your liking *)
 ContentPadding -> True (* set to False to have a tighter margin around simple text *)
]
Szabolcs
  • 234,956
  • 30
  • 623
  • 1,263
4

I've put some time in on this proble, and have decided to post my results. I'm not completely happy with them, but I offer them to the community for consideration. I hope it inspires someone to come up with something better.

First, I looked at Szbolcs suggestion. His code for vertex labeling works well.

Graph[{"a", "b"}, {"a" <-> "b"}, VertexLabels -> Placed[{"Name", "Index"}, {Above, Below}]]

g1

But when his idea is applied to edge labeling, it fails

Graph[{"a", "b"}, {"a" <-> "b"}, EdgeLabels -> Placed[{"Name", "Index"}, {Above, Below}]]

g2

It isn't that Placed can't be used with edge labels, but that, in this situation, when it is given lists as arguments, it ignores them.

Graph[{"a", "b"}, {"a" \[UndirectedEdge] "b"}, 
  EdgeLabels -> Placed["Name", .75],
  ImagePadding -> {{Automatic, Automatic}, {Automatic, 5}}]

g3

I was able to work out a way of putting two items in an edge label, but it is rather cumbersome.

With[{
       verts = {"a", "b"}, edges = {"a" \[UndirectedEdge] "b"}, 
       indents = {0}, offsets = {.75}
     },
  Module[{g, e, i},
    g = Graph[verts, edges];
    e = EdgeList[g];
    i = (EdgeIndex[g, #] & /@ e);
    Graph[e,
      EdgeLabels -> 
        MapThread[
          #1 -> Placed[Style[Row@{Spacer[#3], #2, Spacer[30], #1}, 12], #4] &, 
          {e, i, indents, offsets}]]]]

g4

My approach has enough wiggle room that it is not hard to apply it to the basic undirected graph example given in the Wolfram Documentation Center; viz.

Graph[{1 \[UndirectedEdge] 2, 2 \[UndirectedEdge] 3, 3 \[UndirectedEdge] 1}]

Like so:

With[{
       edges = {1 \[UndirectedEdge] 2, 2 \[UndirectedEdge] 3, 3 \[UndirectedEdge] 1},
       indents = {30, 20, 40},offsets = {.25, .5, .5}},
   Module[{g, e, i},
     g = Graph[edges];
     e = EdgeList[g];
     i = (EdgeIndex[g, #] & /@ e);
     Graph[e,
       EdgeLabels -> 
         MapThread[
           #1 -> Placed[Style[Row@{Spacer[#3], #2, Spacer[30], #1}, 12], #4] &, 
           {e, i, indents, offsets}]]]] 

g5

But I am not sanguine about it working well with a really complex graph.

m_goldberg
  • 107,779
  • 16
  • 103
  • 257