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]

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}]

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