Scaling edge thicknesses

UPDATE

There may be some confusion as to the specification of the problem. kguler's answer doesn't really answer the question. I'll try to elaborate a bit at the bottom of the post.

One problem with the solutions of the custom arrow shaft question is the specification of the edge thickness. In the design of the arrows, the thickness of the edge in relation to the overall size of the arrow is part of the effect one wants to achieve.

Unfortunately, the two methods that can be used to specify the edge thickness, namely Thickness and AbsoluteThickness are not sufficient for this goal. As an easy illustration of the problem, I'll draw some circles here.

Thickness

Graphics[{FaceForm[], EdgeForm[{Thickness[0.1], Blue}], Disk[{0, 0}]}, ImageSize -> 200]


Add some identical (!) copies of the same circle:

Graphics[{FaceForm[], EdgeForm[{Thickness[0.1], Blue}],
Disk[{0, 0}], Disk[{3, 0}], Disk[{6, 0}]}, ImageSize -> 600]


The number in the Thickness specification is a fraction of the horizontal image extent.

AbsoluteThickness

Graphics[{FaceForm[], EdgeForm[{AbsoluteThickness[20], Blue}], Disk[{0, 0}]}, ImageSize -> 200]


Now, let's reduce the image size:

Graphics[{FaceForm[], EdgeForm[{AbsoluteThickness[20], Blue}], Disk[{0, 0}]}, ImageSize -> 70]


In both cases, the ratio between the edge thickness and the object size changes. Sometimes that may be desirable, but here it isn't.

What is needed is either a thickness specification that is related to object size or a status variable from which the current image extent can be read, so that with known object size the edge thickness can be scaled. The latter would be something akin to CurrentValue or so.

Something self-referential like

pl = Graphics[{Text[Norm@PlotRange[pl][[2]] // Dynamic, {10, 10}]}]


could work, but is rather ugly and, frankly, scary. When I changed PlotRange to ImageSize I got my frontend hanging.

Any ideas?

UPDATE

Trying to further clarify the question.

First, some definitions:

We have one or more objects with a given object size. In this case, circles with a radius of one. This size is given in Mathematica's internal, dimensionless canvas coordinate system.

We want to specify the object edge thickness in terms of this object size. However, MMA doesn't have that capability. You have to either specify the EdgeForm as Thickness (which measures edge thickness as a fraction of the horizontal PlotRange) or as AbsoluteThickness (which measures edge thickness in terms of printer's points, 1/72 of an inch).

The PlotRange is the dimensionless size of MMA's canvas as displayed in the figure. It is fully independent of the ImageSize, which determines the graphics's physical size in printer's points.

This leads to the following problem :

Graphics[{FaceForm[], EdgeForm[{Thickness[0.1], Blue}], Disk[{0, 0}]}]


Graphics[{FaceForm[], EdgeForm[{Thickness[0.1], Blue}], Disk[{0, 0}], Disk[{3, 0}], Disk[{6, 0}]}]


Both figures have the same ImageSize, but the PlotRange has increased (more of MMA's internal canvas is revealed), which means that edge thickness related to object size increases too, since the thickness in canvas terms increases whereas the object remains at the same canvas size. The circle's edge now takes up most of the circle's area, which wasn't the case before.

If we specify thickness with AbsoluteThickness we get:

Graphics[{FaceForm[], EdgeForm[{AbsoluteThickness[10], Blue}], Disk[{0, 0}]}]


Graphics[{FaceForm[], EdgeForm[{AbsoluteThickness[10], Blue}],
Disk[{0, 0}], Disk[{3, 0}], Disk[{6, 0}]}]


In this case, the edge thickness in points remains the same, but the object size in points decreases because the ImageSize in points is the same, but more objects have to be fitted in that same space, so they get less points allotted per object. Again, the edge thickness related to the object size changes.

Introducing ThicknessC as the desired edge thickness and ObjectSizeC the object size, both in canvas terms, we have

ThicknessC = C*ObjectSizeC


with C the desired constant ratio of these two variables. C is responsible for the appearance of the object. For instance, we might want to have a circle where the edge thickness is 1/10 of its diameter, so C = 1/10.

Since Thickness is a fraction of the horizontal PlotRange we have:

ThicknessC = Thickness*PlotRange (hor.)


and hence, to get the desired edge thickness ThicknessC, we have to specify a Thickness given by:

Thickness =  C * ObjectSizeC / PlotRange (hor.)


Similarly, we have:

Thickness = AbsoluteThickness / ImageSize


and hence,

AbsoluteThickness = C * ObjectSizeC * ImageSize / PlotRange (hor.)


It seems that any object related edge thickness specification minimally needs to know the final PlotRange of the figure, which, on its turn, is determined by all objects in the figure.

If we're going to use AbsoluteThickness (but why would we bother?) we'd also need awareness of the ImageSize in addition to knowledge about PlotRange.

So, there you have it. The problem can be solved if we can obtain the final PlotRange. The question to be asked is: How can we determine the PlotRange of a figure while we are drawing it?

Of course, PlotRange can be set manually, or be obtained after the fact, but that would mean something ugly like:

pr1 = -Subtract @@PlotRange[
Graphics[{FaceForm[], EdgeForm[{Thickness[0.3], Blue}],
Disk[{0, 0}], Disk[{3, 0}], Disk[{6, 0}]}]][[1]];
pr2 = -Subtract @@ PlotRange[
Graphics[{FaceForm[], EdgeForm[{Thickness[0.3], Blue}], Disk[{0, 0}]}]][[1]];
Graphics[{FaceForm[], EdgeForm[{Thickness[0.3 pr2/pr1], Blue}],
Disk[{0, 0}], Disk[{3, 0}], Disk[{6, 0}]}]
Graphics[{FaceForm[], EdgeForm[{Thickness[0.3], Blue}], Disk[{0, 0}]}]


This works, but isn't elegant.

-

Second try.

Perhaps you are looking for Inset?

disk = Graphics[{EdgeForm[{Black, Thickness[0.1]}], LightBlue, Disk[]}];

Graphics[{
Inset[disk, {0, 0}, {0, 0}, 1],
Inset[disk, {2, 0}, {0, 0}, 2],
Inset[disk, {4, 0}, {0, 0}, 1.5]
}]

Show[%, ImageSize -> 200]


-
The problem with scaling the circle itself is that I can't put three on a row anymore. I have to go now, so can't try, but could you check this? –  Sjoerd C. de Vries Oct 9 '12 at 22:09
+1 Inset seems to solve the problem of the thickness/size ratio. Aligning and scaling of complex objects will need some attention, but that will be doable. Are you aware of any disadvantages of building up graphics from many Inset-s? I have yet to test this in combination with Graph, but will do that tomorrow. –  Sjoerd C. de Vries Oct 11 '12 at 21:47
@Sjoerd I have created a Graphics object with lots of Insets without problem (and Text is implemented as Inset IIRC), but I cannot recall creating one with many levels of Insets, meaning one inside another; that remains to be tested. –  Mr.Wizard Oct 11 '12 at 21:54
I used this in an update to my designer's arrow answer. Works very well! You got my accept. A minor complaint I have is about the whitespace it generates. See note over there. Try the code without the "ShrinkWrap" set to True. –  Sjoerd C. de Vries Oct 14 '12 at 22:16
BTW Did you notice that using Inset allowed me to strip GeometricTransform and FindGeometricTransform from the code? Inset is powerful, although there are details you have to take care of, like the PlotRangePadding, which will prevent precise scaling and positioning when forgotten. –  Sjoerd C. de Vries Oct 14 '12 at 22:36

Update: Could not get CurrentValue[..., "ImageSize"] to pick up the correct image size.

Based on Sjoerd's update, an alternative approach would be to post process a graphics object to transform the thickness primitives using the aspect ratio, plot range and image size of the input graphics. This can be done in a number of ways using AbsoluteOptions (getting PlotRange or AspectRatio) to calculate a new thickness.

Sjoerd's example (slightly modified)

 grpSjrd1 = Graphics[{FaceForm[], EdgeForm[{Thickness[0.1], Blue}], Disk[{0, 0}],
Disk[{3, 0}], Disk[{6, 0}]}, ImageSize -> 300]
grpSjrd2 = Graphics[{FaceForm[], EdgeForm[{Thickness[0.1], Blue}], Disk[{0, 0}]},
ImageSize -> 300]


ClearAll[thicknessUnits];
thicknessUnits[gr_Graphics, sc_] :=
With[{units =
AbsoluteOptions[gr, AspectRatio][[1, 2]]},
gr /. EdgeForm[{beg___, Thickness[thcknss_], end___}] :>
EdgeForm[{beg, Thickness[sc units ], end}]]


Usage examples:

Grid[Transpose[
{thicknessUnits[grpSjrd1, #],thicknessUnits[grpSjrd2, #]} & /@
{.03, .05, .1, .2}], Spacings -> {3, 2}]


Alternatively, one can use the PlotRange instead of AspectRatio:

ClearAll[thicknessUnitsB];
thicknessUnitsB[gr_Graphics, sc_] :=
With[{units =-Subtract @@ AbsoluteOptions[gr, PlotRange][[1, 2, #]] & /@
{1, 2} // (#[[2]]/#[[1]]) & },
gr /. EdgeForm[{beg___, Thickness[thcknss_], end___}] :>
EdgeForm[{beg, Thickness[sc units ], end}]]


Leftover from original post ... (holding for now until I give up struggling with CurrentValue[...])

Using CurrentValue["Magnification"] seems more promising:

Column[Style[Graphics[{FaceForm[],
Dynamic@EdgeForm[{Thickness[.02 CurrentValue["Magnification"]], Blue}],
Disk[{0, 0}], Disk[{3, 0}], Disk[{6, 0}]}, ImageSize -> 600],
Magnification -> #] & /@ {.2, .5, 1., 1.5}]


-
Thanks! "ImageSize" was a nice find. It wasn't on my CurrentValue list. I'm not sure about "Magnification"'s utility. I have to check it but it looks like the thickness/object size doesn't remain constant in the last plot. –  Sjoerd C. de Vries Oct 9 '12 at 5:32
I'm afraid this doesn't work. Graphics[{Text[CurrentValue["ImageSize"]]}, ImageSize -> 100] returns {350,350} not 100. Addition of Dynamic doesn't help either. –  Sjoerd C. de Vries Oct 9 '12 at 18:28
The magnification part doesn't work either. The ratio of the circle radius to line thickness for the largest two sets of circles differs by a factor of about 1.5, so it isn't an invariant as was asked for. –  Sjoerd C. de Vries Oct 9 '12 at 18:32
Please, see the update to my question. –  Sjoerd C. de Vries Oct 9 '12 at 21:30
SjoerdC, sorry took a while to respond. I could not get CurrentValue[... "ImageSize"] to pick up the image size of the object it resides in. In the light of the update to your question, I will put together an updated post. –  kguler Oct 10 '12 at 22:53