# Subplots with connector lines

I am looking for advice from people who have more experience in this area on what is the best (simplest, least effort) way to create a graphic like the following:

This is a rough mockup made in a drawing program. There is a central graph, surrounded by smaller ones, each of which is showing some information about a point in the main graph. Those points are connected to the subgraphs with lines.

Requirements:

• Each plot must be able to have their own axes/frame

• Proper alignments of the connector lines (red dashed lines on the mockup)---I have the coordinates of one end in the coordinate system of the central plot, while the other end must point at the smaller plots.

• Consistent font sizes and line widths (i.e. everything must be 8 pt when printed)

• Vector graphics (I'd like to avoid rasterizing to bitmaps)

Possible approaches:

• GraphicsGrid with Epilog (GraphicsGrid seems to be based on Inset.)

• Lots of Insets in a graphic (the main issue is aligning the coordinate system of the central plot with that of the whole graphic)

• Learn to use LevelScheme (I didn't use it for anything serious yet, but when I tried it last time it seemed to have issued with alignment).

Whenever I start doing something like this, and the details must be accurate, lots of small issues tend to come up. I'd like to know which approach is likely to prove the least troublesome.

The main difficulty was the correct positioning of connector lines. The usual way of including subplots is by using Inset (which is also used by GraphicsGrid). One endpoint of the lines is in the main graphics coordinate system, while the other is in the central subplot coordinate system. Converting between the two is very difficult and depends on the scaling of graphics.

Heike's solution uses FullGraphics to expand the axes/frames of subplots. Then all subplots can be directly included in the main graphic and scaled to size. There will be a single coordinate system to deal with.

Chris Degnen's solution uses image processing to align the main graphic coordinate system with the inset coordinate system. It places a red dot at the desired endpoints, rasterizes the graphic, measures the position of the dot, and then uses this information to compose a vector graphic with the connector lines going between these positions. The result is a vector graphic that looks correct only at a certain scale, but can be exported to PDF.

The other solutions recommend adding the connector lines manually.

-
Why not use one Graphicsobject? Then you would not have to fuss around with other coordinate systems. I for one find Inset cumbersome - always seem to spend too much time tweaking there. –  Yves Klett Feb 19 '12 at 17:38
@Yves Because I need separate Frames on each one. I don't know how to do this when using a single Graphics. I agree that Inset is cumbersome, that's exactly why I asked the question. –  Szabolcs Feb 19 '12 at 17:39
If you want framed text you could try Framed [see here]( mathematica.stackexchange.com/questions/1882/…) or something like that. More work if you want a tidy layout... –  Yves Klett Feb 19 '12 at 17:49
@Yves I don't mean that kind of frame, I mean a frame that has tick marks, as in Graphics[{},Frame->True]. –  Szabolcs Feb 19 '12 at 17:50
@Yves I am thinking of making the small plots Insets in the Graphics object of the central plot. It would spare me the nightmare of having to align the Inset coordinate system with the main one. –  Szabolcs Feb 19 '12 at 18:02

This solution uses FullGraphics to transform axes and ticks in a plot to lines which allows you to resize and translate the plot while keeping the ticks of the original plot. In raster, main is the main plot, list is the list of sub plots, pts is the list of points in the main plot corresponding to the begin points of the red lines, and {dx, dy} are the gaps between the sub plots and the main plot. The sub plots are placed in clockwise direction starting with the one in the upper right corner. The end plot is such that the plot range of the main plot is {{0, 0}, {1, 1}}.

raster[main_, list_, pts_, {dx_, dy_}] :=
Module[{fgmain, fglist, prm, prl, scmain, sclist, scpts, lines},
fgmain = FullGraphics[main];
fglist = FullGraphics /@ list;
prm = OptionValue[AbsoluteOptions[main, PlotRange][[1]],
PlotRange];
prl = OptionValue[Options[#, PlotRange][[1]], PlotRange] & /@ list;
scmain =
Translate[
Scale[fgmain[[1]], 1/(prm[[All, 2]] - prm[[All, 1]]),
prm[[All, 1]]], -prm[[All, 1]]];
scpts = Transpose[{Rescale[pts[[All, 1]], prm[[1]]],
Rescale[pts[[All, 2]], prm[[2]]]}];
Translate[
Scale[#, (.5 - {dx, dy}/
2)/(#2[[All, 2]] - #2[[All, 1]]), #2[[All, 1]]],
-#2[[All, 1]] + #3] &,
{fglist[[All, 1]], prl, {{-.5 - dx/2, 1 + dy},
{0, 1 + dy}, {.5 + dx/2, 1 + dy}, {1 + dx, 1 + dy},
{1 + dx, .5 + dy/2}, {1 + dx, 0}, {1 + dx, -.5 - dy/2},
{.5 + dx/2, -.5 - dy/2}, {0, -.5 - dy/2}, {-.5 - dx/2, -.5 -
dy/2},
{-.5 - dx/2, 0}, {-.5 - dx/2, .5 + dy/2}}}];
lines = Transpose[{scpts,
{{-dx, 1 + dy}, {.25 - dx/4, 1 + dy}, {.75 + dx/4,
1 + dy}, {1 + dx, 1 + dy},
{1 + dx, .75 + dy/4}, {1 + dx, .25 - dx/4}, {1 + dx, -dy},
{.75 + dx/4, -dy}, {.25 - dx/4, -dy}, {-dx, -dy},
{-dx, .25 - dy/4}, {-dx, .75 + dy/4}}}];
Graphics[{scmain, sclist, {Red, Dashed, Line[lines]}}]]


Example:

list = MapIndexed[ParametricPlot[#, {x, 0, 2 Pi},
Frame -> True, Axes -> False,
PlotStyle -> (ColorData[1] @@ #2)] &,
Table[{(n - 1) 2 Pi + x, n Sin[x]}, {n, 12}]];
main = Show[list, PlotRange -> All];
pts = N[Table[{(n - 1) 2 Pi + x, n Sin[x]}, {n, 12}] /. x -> Pi];

raster[main, list, pts, {.15, .15}]


-
I was really hoping you would come up with something. This is excellent. The only potential problem is that the frame tick lengths get scaled down too much, and I can't seem to fix this even when using an explicit tick specification. (But fortunately this is not a problem for my present application as I probably won't need ticks on the small plots, just a rough indication of the range) –  Szabolcs Feb 19 '12 at 22:52
Grrrrrrrrrrreat –  Rojo Feb 22 '12 at 6:37

Looking at your list of requirements, I think learning to use LevelScheme is the best option. It can easily handle all the requirements except the second one (which can of course be done with a hackjob). Here's a simple example reproducing a blank template for your layout:

<<LevelScheme
Figure[{
Multipanel[{{0, 1}, {0, 1}}, {4, 4},
XPanelSizes -> {1, 1, 1, 1}, XGapSizes -> 0.1,
YPanelSizes -> {1, 1, 1, 1}, YGapSizes -> 0.1,
FontSize -> 8
],

Table[If[! MemberQ[{2, 3}, i] || ! MemberQ[{2, 3}, j],
FigurePanel[{i, j}, PanelLetter -> None,
FrameTicks -> None],
## &[]
], {i, 1, 4}, {j, 1, 4}
],

FigurePanel[{2, 2}, PanelLetter -> None, FrameTicks -> None,
PanelAdjustments -> {{0, 1.1}, {1.1, 0}}
]
}, ImageSize -> 300 {1, 1}, PlotRange -> {{-0.01, 1.01}, {-0.01, 1.01}}]


### A few things to note:

1. LevelScheme will provide you with (and require you to use) more fine tuning controls than you expect. From your question, it seems like you want these controls, but do note that it takes time to get a feel for it — so if you need it to produce the example figure in a couple of hours, good luck! I don't mean to say that it is difficult or non-intuitive, but it certainly is different from your usual Mathematica way of building up graphics, which you'll need to get used to.
2. Each panel has its own handle, axis and plot range, making it easy to use each for different kinds of plots. You can also set fonts for each subpanel and style the labels and frame ticks separately for each of the four sides. You needn't worry about where to position each panel, as LS lays it out for you in a neat matrix. Here, I've created the outer ring of panels and then used a single panel ({2,2}) and expanded it to fill the rest of the gap. Note the adjustment lengths (1.1), which should take into account the XGapSizes and YGapSizesused to maintain the correct spacing.
3. The documentation for LS is fairly decent, and was sufficient for me to get started and exploring. Do give it a good read.
4. I've had issues in the past with getting some 3D graphics and some rasterized 2D plots to display properly (especially to get it to respect the plot range). However, I think that can be a separate question (on main or chat) if it arises.
5. I've used a Table to quickly plot the panels, but in your usage, you'll probably do them individually with custom settings for each (unless if they all have the same options).
6. The connector lines might be a bit tricky since LS uses a local coordinate system for each subpanel. I'll try to come up with a way to do it programmatically (someone else who knows how to can perhaps help me out here), but for now, I suggest using the drawing tools to do that. But do remember that the annotations get wiped out when you regenerate the figure. See this question for details, although I highly doubt that the answer there will work here (haven't tried it).
-
The big problem is indeed point 6: Making sure that the lines end at the correct position in the central plot. –  Szabolcs Feb 19 '12 at 18:48

Here is a solution you may be able to use. It works by rasterising the graphic. For an image with more than one line you could use more colours.

This method works by building up a composite graphic with coloured dots (in Red) at the points on the graphs to be connected. The graphic is rasterised and the location of the dots obtained, then the (vector) graphic is recreated with the connecting line included. (Temporarily setting the colours to Black was not actually necessary in this example.)

block1 :=
Graphics[{colour1, Rectangle[{0, 0}, {180, 150}]},
PlotRange -> {{0, 180}, {0, 150}}]

block2 :=
Graphics[{colour2, Rectangle[{0, 0}, {180, 150}],
Inset[Plot[Sin[x], {x, 0, 2 Pi}, PlotStyle -> Black,
Epilog -> {Red, PointSize[0.001], Point[{Pi/2, 1}]},
ImageSize -> 160]]}, PlotRange -> {{0, 180}, {0, 150}}]

mainblock :=
Graphics[{colour3, Rectangle[{0, 0}, {390, 310}],
Inset[Plot[Sin[x], {x, 0, 2 Pi}, PlotStyle -> Black,
Epilog -> {Red, PointSize[0.001], Point[{Pi/2, 1}]},
ImageSize -> 370]]}, PlotRange -> {{0, 390}, {0, 310}}]

blocks[done_] := Graphics[{colour4, Rectangle[{0, 0}, {600, 330}],
Inset[block1, {100, 150 + 85 + 10}, {Center, Center}, {180, 150}],
Inset[block2, {100, 85}, {Center, Center}, {180, 150}],
Inset[mainblock, {200 + 390/2, 165}, {Center, Center}, {390, 310}]},
PlotRange -> {{0, 600}, {0, 330}},
If[done, Epilog -> {Red, Thickness[0.001], Line[b]}, ## &[]],
ImageSize -> 600]

colour1 = colour2 = colour3 = colour4 = Black;
c = blocks[False];
a = Rasterize[c];
b = Reverse /@ Position[a[[1, 1]], {255, 0, 0}];
colour1 = Cyan;
colour2 = Orange;
colour3 = Green;
colour4 = Yellow;
blocks[True]


-
Very interesting. I almost gave up on an automated solution. Can you please explain in your post how it works, so others can understand it too (without spending too much time)? And please also point out that it does produce a vector graphic in the end (it's not just resizable, but it's exportable at the correct size). –  Szabolcs Feb 19 '12 at 21:13
I wonder why this got downvoted. It is the most practical ( = usable) answer I got so far, even if it's a hack. –  Szabolcs Feb 19 '12 at 22:20
Maybe because of the rasterisation step, although as you mentioned it's the final image that you wanted in vector format. –  Chris Degnen Feb 19 '12 at 22:23
Or maybe just a mis-tap on a touchscreen. Don't take it too seriously. –  Szabolcs Feb 19 '12 at 22:25
I have downvoted by mistake in the past by "mis-tapping", but it was not I this time! in fact I think that this is a clever (although hacky) approach, so upvoted it. –  acl Feb 19 '12 at 22:52

Edit: I made some minor adjustments to this. I know that this isn't a good solution (see above for those), but I didn't want to leave my attempt in an unfinished state:

graph1 = Plot[Sin[x], {x, 0, Pi}];
graph2 = Plot[Cos[x], {x, 0, Pi}];
graphBig = Plot[Tan[x], {x, 0, Pi}];
graph4 = Plot[ArcSin[x], {x, 0, Pi}];

group =
GraphicsGrid[
{{graph1, graph2, graph2, graph1},
{graph1, graphBig, SpanFromLeft, graph1},
{graph4, SpanFromAbove, SpanFromBoth, graph1},
{graph1, graph2, graph2, graph1}}]

arrow[tile_] := Block[{} ,
fromX = 50 + Mod[tile, 4, 1]  * 300;
fromY =  -140 +  Quotient[tile, 4, 1]   * -250;
toX = 800; toY = -550;
Arrow[{{fromX, fromY}, {toX, toY}}, {20, 260}]]

Show[{group, Graphics[{
Opacity[0.3], Blue, Thick,
Table[arrow[tile],
{tile, {1, 2, 3, 4, 5, 8, 9, 12, 13, 14, 15, 16}}]}]}]


-
The big problem is indeed making sure that coordinates are correct. I can't align with the coordinate system of an Inset` because how Mathematica mixes plot / scaled relative to different things / offset coordinates. –  Szabolcs Feb 19 '12 at 18:45