How to position text labels automatically to not overlap other graphics elements?

13

12

I was plotting reciprocal frame projection using Locators for a point to be expressed in terms of both the original basis (also specified with Locators and reciprocal basis.)

EDIT: Here's a stripped down example of the code (no longer matching the image)

Manipulate[DynamicModule[{f1, f2, xf, o, r, s}, o = {0, 0};
  s = {0.1, 0.1};
  r = Inverse[{e1, e2}]; f1 = Part[r, All, 1]; f2 = Part[r, All, 2];
  xf = {x.f1, x.f2};
  Graphics[{Arrow[{o, x}], Arrow[{o, xf[[1]] e1}],
    Arrow[{xf[[1]] e1, xf[[1]] e1 + xf[[2]] e2}], Text["x", x + s],
    Text["e1", e1 + s], Text["e2", e2 + s]}]], {{x, {4, 2}},
  Locator}, {{e1, {1, 1}}, Locator}, {{e2, {1, 2}}, Locator}]

I've placed labels on some of the arrows that represent the vectors, but did this by fudging things adding in a fixed offsets (variable s above).

reciprocal frame locators with problematic text labels

The manual offsets are in some cases positioned reasonably for some of the labels (because my hardcoded offsets are implicitly related to the initial geometry of the Locators).

I'd like to avoid hardcoding those offsets in the hacky way that I've done. Is there a better way to automatically position those Text labels so that they are offset slightly (e.g. the width of a text character) from nearby graphics elements?

Peeter Joot

Posted 2013-09-25T13:33:28.247

Reputation: 6 098

It is not the same but you may consider using some of similar solutions as there: labeling individual curves

– Kuba – 2013-09-25T13:47:25.640

This may help http://mathematica.stackexchange.com/a/14149/193

– Dr. belisarius – 2013-09-25T14:20:19.860

@belisarius, that answer is very cool, but I don't see how I'd use that interactive label placement to solve the problem of label placement in a Manipulate context, where the location of the labels could vary when the Locator's are moved. – Peeter Joot – 2013-09-25T15:38:42.663

@PeeterJoot That was the reason why I said It may help :) – Dr. belisarius – 2013-09-25T15:45:27.030

The part of this question that bothers me is "offset slightly from nearby graphics elements". How is the function that automates the label offsets going know what "nearby" and "slightly" means? – m_goldberg – 2013-09-26T01:07:33.313

I'd describe slightly as the width of a text character. – Peeter Joot – 2013-09-26T01:28:55.523

This is going to be very hard, partly because of the different coordinate/sizing systems text and other graphics use and partly because there isn't always a solution. I had wished for an easy solution many times before, but anything I can imagine is just too much work for me to do. So I position my labels manually. If I did decide to do it, I would base it on this answer of Heike, which is generally applicable to any shape, not just words.

– Szabolcs – 2013-09-26T02:12:30.660

A bit more detail: Create a mask out of the graphics and already positioned labels. Nothing may overlap with these. Choose an ideal centre for the label to be placed. Then use the answer I linked (the part at the end) to find a good placement for the new label. Verify if it is close enough to the desired position(s). – Szabolcs – 2013-09-26T02:16:09.717

Unfortunately this will be too slow to be manipulatable, but it'd still be quite useful for non-interactive figures. – Szabolcs – 2013-09-26T02:17:26.850

Answers

19

Here's an attempt to implement the idea suggested by Szabolcs, based on Heike's clever image processing method. The code uses MinFilter to identify regions where the label may go without overlapping anything, and Nearest to pick the closest point to the desired position. The rest is just scaling between image and graphics coordinates.

It won't be fast enough for use in dynamic graphics, and there are no doubt numerous ways to break it, but I thought it was a cool idea and worth having a go at.

Update

I've changed the original code slightly - the labels are now rasterized using Style[..., "Graphics"], and I've added an optional third argument to specify the preferred position of the label relative to the point (see second example). I've also used Charting`get2DPlotRange to get the true plot range (including any PlotRangePadding). There is still a slight alignment problem arising from the unknown ImagePadding of the original plot, this may become noticeable for plots with lots of image padding.

addlabels[g_Graphics, labels_, o_: {0, 0}] := 
  Fold[Show[#1, positionlabel[##, o]] &, g, labels]

positionlabel[g_Graphics, {label_, x_}, o_] :=
 Module[{p, b, bd, xi, ls, m, ivp, nf, xx, pos, d, p1, sc},
  p = Charting`get2DPlotRange[g];
  b = ImagePad[ImagePad[Binarize@Show[g, ImagePadding -> 0], -1], 1, Black];
  bd = ImageDimensions[b];
  xi = bd MapThread[Rescale, {x, p}];
  ls = {0, 4} + Reverse[Rasterize[Style[label, "Graphics"], "RasterSize"]/2];
  m = MinFilter[b, ls];
  ivp = ImageValuePositions[m, 1];
  sc = If[ivp == {}, x,
    nf = Nearest[ivp];
    xx = Table[xi + a o Reverse[ls], {a, {1, -1, 0}}];
    pos = First[nf[#]] & /@ xx;
    d = MapThread[EuclideanDistance, {pos, xx}];
    p1 = First@Pick[pos, Negative[d - 2 Min[d]]];
    Scaled[p1/bd]];
  Graphics@Inset[label, sc, Center]]

The labels must be supplied as a list like {{"label1", {x1, y1}}, {"label2", {x2, y2}}, ...}. Here's an example:

labels = {Style[#, 20], {#, Sin[#]}} & /@ Range[0, 10];

plot = Plot[Sin[x], {x, 0, 10}, Frame -> True, 
  Epilog -> {PointSize[Large], Point@labels[[All, 2]]}];

addlabels[plot, labels]

enter image description here

The default behaviour is to position the centre of the label close to the correct location while not overlapping anything. In some cases it may be preferable to position one side of the label near to the location instead. In this example the labels are positioned to the right of the point (if possible), using {1,0} as the third argument to addlabels:

labels = Thread[{RandomSample[DictionaryLookup[], 10], RandomReal[1, {10, 2}]}];

plot = ListPlot[labels[[All, 2]], PlotStyle -> PointSize[0.02], Frame -> True];

addlabels[plot, labels, {1, 0}]

enter image description here

To position the labels to the left of the points you would use {-1,0}, for above {0,1} and for below {0,-1}

Simon Woods

Posted 2013-09-25T13:33:28.247

Reputation: 81 905

Awesome Simon. I just used this in some plots of melting points using ElementData[], and used this to label the points with the element names. – Peeter Joot – 2013-09-27T01:46:33.653

Extremely nice solution. A pity that you still have to look for an approximately appropriate spot for the label location. It would be really nice if, for some set of parametrized curves, you just specified the label for each curve and Mathematica would find a visually pleasing location automatically. But that would imply defining rules for the placement's aesthetics. Pretty hard that. – Sjoerd C. de Vries – 2013-09-27T19:33:43.040

Great tool! How can I adapt this so it positions a little closer to the original specified point? – Tom – 2016-10-01T15:22:11.250

6

The graphics display issues can be diminished by

  1. Setting the Texts to Background-> White.
  2. Offsetting the Arrow heads (but not the tails).

If there is overlap, the offset and the white background should make it less distracting.

For readability, I recommend using Offset rather than using s.

Manipulate[DynamicModule[{f1, f2, xf, o, r, s}, o = {0, 0};
r = Inverse[{e1, e2}]; f1 = Part[r, All, 1]; f2 = Part[r, All, 2];
xf = {x.f1, x.f2};
Graphics[{
  Arrow[{o, x}, {0, .15}],
  Arrow[{o, xf[[1]] e1}, {0, .15}],
  Arrow[{xf[[1]] e1, xf[[1]] e1 + xf[[2]] e2}, {0, .15}], 
  Text["x", Offset[{20, 0}, x], Background -> White],
  Text["e1", Offset[{20, 0}, e1], Background -> White], 
  Text["e2", Offset[{20, 0}, e2], Background -> White]},
Frame -> True, GridLines -> Automatic,
GridLinesStyle -> Directive[Orange, Dashed], BaseStyle -> 16]], 
{{x, {4, 2}}, Locator}, 
{{e1, {1, 1}}, 
Locator}, {{e2, {1, 2}}, Locator}]

text positioning

DavidC

Posted 2013-09-25T13:33:28.247

Reputation: 15 949