I was reading a paper by Holten and van Wijk called "A User Study on Visualizing Directed Edges in Graphs". They show a number of graphical alternatives to using arrowheads:

enter image description here

I find the final one (f), that replaces the arrowheads with coneheads, particularly appealing, and would like to try using them. It appears there is no built in conehead functionality, and I was wondering if there is an easy way to do this.

For arrowheads, it is easy:

Graphics[Arrow[{{1, 0}, {2, 1}}]]

My first thought was to replace the line with a cone:

Graphics3D[Cone[{{1, 0, 0}, {2, 1, 0}}, 0.05], Boxed -> False]

This works to some extent, but is a 3D command instead of 2D, and it was not obvious how to modify it to work with Graphics instead of Graphics3D. My second though was to use a Polygon:

conehead[{{a1_, b1_}, {a2_, b2_}}, r_] := 
          Graphics[Polygon[{{a1, b1}, {a2, b2 - r}, {a2, b2 + r}}]];
conehead[{{0, 0}, {1, 0.2}}, 0.02]

enter image description here

This also works to some extent, but has several problems: the end of the cone isn't at the right angle, it doesn't work for many a's and b's, and there is no shading from transparent to dark.

So my question is: is there a straightforward way to replace arrowheads with coneheads?

  • 1
    I realize this point is tangential to the focus of the question, but I most dislike style f) because it is the only one that implies three dimensions when no such implication is appropriate. In fact, the implied depth is inconsistent, and hence very visually confusing. As such, it is the most misleading, in my opinion. – David G. Stork Aug 18 at 18:29
  • 1
    @David G. Stork -- According to Holten and van Wijk, subjects can extract information about connectedness more quickly and more accurately from (f) than from the others. They do not talk about the pseudo 3D effect of the tapered ends, but maybe this is why it is easier -- the eye has a third dimension in which to help organize the clutter. – bill s Aug 18 at 19:32
up vote 24 down vote accepted

How about:

conehead[r_][{p1_, ___, p2_}, ___] := With[
  {n = Normalize[{{0, 1}, {-1, 0}}.(p2 - p1)]},
  Polygon[{p1 - r n, p1 + r n, p2}]
  ]
Graphics[conehead[0.02][{{0, 0}, {1, 0.5}}]]

Used in a graph:

RandomGraph[
 {20, 40},
 EdgeShapeFunction -> conehead[0.02],
 EdgeStyle -> Directive[Black, Opacity@0.5]
 ]

Mathematica graphics

StreamStyle glyph "Pointer" can be made to look the same as the desired shape, so we can use

f[w_: .05][pts_] := Graphics`Glyphs`GlyphData["Pointer", GlyphWidth -> w, 
  GlyphControlFunction -> (1 - #&), PlotPoints -> 300][Line @ pts] 

to generate the desired Graphics primitive:

Graphics[{Red,f[][{{1, 0}, {3, 1}}]}]

enter image description here

The function glyph below is slightly more general than f above in that other built-in glyph functions (such as "Drop", "Dart", etc) can be used as the first argument if desired.

ClearAll[glyph]
glyph[g_: "Pointer", w_: .1] := Graphics`Glyphs`GlyphData[g, GlyphWidth -> w][
    Line[(g /. {"Drop" -> Reverse, _ :> Identity})@#]] &;  

Examples:

SeedRandom[1]
rg = RandomGraph[{20, 30}];
SetProperty[rg, {EdgeShapeFunction -> glyph[], VertexSize -> .3, 
  EdgeStyle -> Directive[Opacity[.5], Black], ImageSize -> 500}]

enter image description here

The same idea can be used to have curved versions using the above function in combination with GraphElementData["CurvedArc"]:

ClearAll[glyph2]
glyph2[g_: "Pointer", w_: .1, curv_: .5] := Module[{bf = BezierFunction @@ 
   (GraphElementData[{"CurvedArc", "Curvature" -> curv}][(g /. 
      {"Drop" -> Reverse, _ :> Identity})@#, ##2])}, 
  {Graphics`Glyphs`GlyphData[g, GlyphWidth -> w][Line[bf /@ Subdivide[10]]]}] &; 

SetProperty[rg, {EdgeShapeFunction -> glyph2[], VertexSize -> .3, 
  EdgeStyle -> Directive[Opacity[.5], Black], ImageSize -> 500}] 

enter image description here

Further examples:

SeedRandom[1] 
Graphics[{Opacity[.5], RandomColor[], glyph["Pointer", RandomReal[{1, 10}]]@#} & /@ 
  RandomInteger[100, {50, 2, 2}]] 

enter image description here

Graphics[{Opacity[.5], RandomColor[], glyph["Drop", RandomReal[{1, 10}]]@#} & /@ 
  RandomInteger[100, {50, 2, 2}]]  

enter image description here

SeedRandom[123]
Graphics[{Opacity[.5], RandomColor[], glyph2["Pointer", RandomReal[{1, 10}]]@#} & /@
   RandomInteger[100, {20, 3, 2}]]   

enter image description here

  • There is an error in one of @kglr's posted functions. I would note this in a comment, but I don't have enough reputation to comment (but I can answer!?). The function glyph2 contains the expression GraphicsGlyphsGlyphData[glyph, GlyphWidth -> w] which should instead be GraphicsGlyphsGlyphData[g, GlyphWidth -> w] – G. Shults Aug 27 at 23:33

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.