In the previous post I looked at a method of dividing up a group shape using a grid system layout, to allow you to position child shapes in a simple and flexible way. One of my motivations for considering this is to try and create a flexible system for laying out icons around a shape when Data Graphics are not an option.
Dynamic icons
I want to use the grid concept to layout a set of icons sequentially, given a starting position, and to choose where the sub-shapes sit around the group, either vertically or horizontally. I also want the shapes to concertina up, so that when one shape is not visible the remaining shapes will shuffle up next to each other leaving no gaps. Beyond this, I would like it to be relatively easy to add new sub-shapes to the group and have them take part in the dynamic positioning – you can read this as limiting the number of cell changes when a new shape is introduced.
So here’s a screenshot of the shape, and remember that the visible grid is just a separate shape I’ve added underneath so that you can visualise the ‘virtual’ one defined in the shape:
The group defines a grid of 20 rows and 20 columns and although the origin is set at the bottom left hand corner of the shape (as per Visio’s coordinate system) the grid is actually infinite and so you’re free to set the position outside of this space.
Each icon shape (A-E) uses a row and column span of three, but you might be wondering why they appear to only take up a little over 2 units. The reason for this is that, although the sub-shape does indeed have a width and height of three column and row units, I’ve set the local pin (LocPinX/Y) to a negative value to allow padding to be set for the visible geometry inside the 3x3 grid that each one has been allotted. This is means by which spacing is achieved between the sub-shapes.
Visibility and Position
Each icons’ visibility is controlled by a User cell list in the group, where 1 = visible and 0 = hidden, and this string can be mapped to Shape Data or changed via code.
As each icon also knows its item index, it’s able to lookup its own visibility value from this group shape cell.
If an icon in a given position is not visible, then that grid position becomes available for the following icon and so on to the end of the sequence. It would be great to have some kind of iterator style functions in the ShapeSheet, but in their absence I’ve added a series of cells, with each one looking back at the previous one to determine a grid placement position.
A final cell, named User.ItemsPositionList, wraps these position cells back up into a list so that the icon shapes can calculate their actual positions. Here’s the (long) formula that concatenates the list:
=User.Item1Position&";"&User.Item2Position
&";"&User.Item3Position&";"&User.Item4Position
&";"&User.Item5Position
So, for example, if the ItemsVisibilityList cell was “1;0;0;1;1” then the result of ItemsPositionList would be “1;1;1;2;3”. The two zeros will ensure that the second and third place icons do not display and so the fourth and fifth icons are allocated grid positions ‘2’ and ‘3’ respectively.
Group shape cells
So now you’ve seen the core group level cells, let’s have a look at the whole User section for the group shape. It has a few extra cells but, by and large, they are just convenience values for nudging the individual sub-shapes basic layout around plus layout logic for the entire icons set, which I’m going to move onto next.
[Just in case you’re wondering, I’m also going to look at the User.fx… cell in a minute when we cover the sub-shape logic.]
Icon set layout
Since I’m trying to collapse non-visible icon positions, the layout of the icons set is really determined by the position of a primary icon. All other icons arrange themselves horizontally or vertically relative to that initial position. There are a couple of key cells that set the position of the primary icon (item) – User.GridOriginPnt and User.PrimaryItemOffset.
As you can guess GridOriginPnt allows you to set the grid’s origin anywhere inside or out of the group shape and its coordinates are relative to the bottom left corner of the shape. This is followed by the PrimaryItemOffset that specifies an offset from the origin. So, for example, as it stands the origin (orange) is set to the top right and the offset (green) shifts it 3 columns to the left. Since the icon positions vary horizontally, the grid row is set by User.FixedGridVector. (If User.ItemsLayoutHorizontal were to equal false or 0, then FixedGridVector would refer to the column position instead.)
Changing the values of these cells allows you to position the icons set anywhere around the shape and, most importantly, you can do this all from the group shape.
Individual icon logic
So it’s now time to have a look at what’s happening in the individual icon sub-shapes themselves.
I’ve pushed some of the longer formulae onto the following lines in this screenshot, so I hope you don't get too much eye strain. Aside from the Padding cell pair, which are here just in case you want to override the group defaults on a per sub-shape basis, the key User cells are:
- ItemIdx – This defines the icon’s grid column or row index position and is the only (position related) cell that needs to be written to when adding more sub-shapes. This index value is zero based.
- IsHidden – Is the cell that looks up the 1 or 0 value in the group’s ItemsVisibilityList cell to find out whether it should be visible or not.
- VariableVectorPosition – This cell heads off to the function cell in the group shape that you saw earlier: User.fxGetItemGridPosition, and passes in its index. We’ll look at this function next.
User.fxGetItemGridPosition is in charge of calculating the final grid position for a given index. The formula, which I’ve broken over a few lines for clarity, reads as follows:
User.PrimaryItemOffset
+(IF(User.ReverseOrder,-1,1)*
((INDEX(ARG("ItemIdx"),User.ItemsPositionList)-1)
*User.DefaultGridSpan))
The most important parts of this formula are on the third and fourth lines, which looks up the icon position from the ItemsPositionList and multiplies it by the default grid span. (You remember that the list is the concatenated Item Position cells and looks like this: “1;1;1;2;3”.)
By default the icons will flow to the right if horizontally aligned, or up if vertical. User.ReverseOrder, which is a boolean value, is used to multiply the resulting position by a negative or positive number and so reversing the grid placement.
Icon size and position
The final piece of the individual sub-shape puzzle is the width/height and PinX/Y cells. For the size, the sub-shape just needs to know whether it’s visible or not and reduces to a size of zero if it’s not.
This size reduction is used together with an equivalent in Pin position cells so that it doesn’t interfere with other connector routing and layout if the sub-shape is not visible. The remaining elements of the PinX formula calculate the actual position based on the grid Column and ColumnWidth, with an offset for the grid origin. Here’s the entire PinX formula:
IF(User.IsHidden,0,
PNTX(Sheet.1!User.GridOriginPnt)
+Sheet.1!User.ColumnWidth*User.Column)
The PinY is a mirror of this, with the only differences being references to rows instead of columns and the Y component of the origin point rather than the X.
Adding new icons
You might remember that at the start of this post I said that one of the goals of this shape was to limit the number of cell changes required when adding new sub-shapes, so let’s look at the steps you need to take to add, say, a sixth icon.
- First of all you open the group shape and copy and paste an existing icon shape.
- Open the ShapeSheet for the new shape and set its User.ItemIdx cell to 5 (the sixth shape). Note that this is the only cell change needed in the sub-shape as the size and position cells are guarded and so aren’t overwritten when pasting the new copy, and the existing logic in the shape handles the rest.
- In the group shape you just need to accommodate a sixth sub-shape, so select User.Spare1, rename it to User.Item6Position and set its formula to:
User.Item5Position+INDEX(5,User.ItemsVisibilityList) - Move up to User.ItemsPositionList and append its formula with:
&";"&User.Item5Position - Finally add another ‘1’ (or other logic that determines the new icons visibility) to the end of User.ItemsVisibilityList.
I think that’s pretty reasonable, just four cell changes to have the new icon take part in the grid system. Of course, if you already have an icon shape that you want to add to the group then you’ll need to populate it with all of the correct cells and formulae. An alternative might be to modify an existing icon, make it a group shape and then add your new icon to this as a child shape. I don’t really like that option as it’s usually best to avoid groups within groups due to the extra transform processing that this requires. So, to make things a little easier, I’m going to write some quick builder code to add a new icon and I’ll cover this in a following post.
In the meantime, if you want to examine that shape as it stands you can download the file here:
Note that I’ve added a few Shape Data rows to make it a bit quicker to change some of the values, so if some of the group shape User cells look a little different they’re really just pointing to the Shape Data for convenience.