#1,176 – Custom Panel, part VIII (Treemap-like Visualization)
October 9, 2014 1 Comment
Here’s one more example of a custom panel. The code below is for a panel that arranges its children in a very simple treemap sort of structure. (This implementation isn’t really a treemap, but vaguely similar to what has been described in the literature).
The panel defines a Weight attached property that the child elements use to indicate a relative weight. The panel then sorts the children based on weight and arranges them such their final area is proportional to their weight.
public class ChildAndRect { public UIElement Element { get; set; } public Rect Rectangle { get; set; } } public class WeightedPanel : Panel { private static FrameworkPropertyMetadata weightMetadata = new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.AffectsParentArrange); public static readonly DependencyProperty WeightProperty = DependencyProperty.RegisterAttached("Weight", typeof(double), typeof(WeightedPanel), weightMetadata); public static void SetWeight(DependencyObject depObj, double value) { depObj.SetValue(WeightProperty, value); } // Measure phase protected override Size MeasureOverride(Size availableSize) { double totalWeight = totalChildWeight(); foreach (ChildAndRect child in ChildrenTreemapOrder(InternalChildren.Cast<UIElement>(), availableSize)) child.Element.Measure(child.Rectangle.Size); return availableSize; } // Arrange phase protected override Size ArrangeOverride(Size finalSize) { foreach (ChildAndRect child in ChildrenTreemapOrder(InternalChildren.Cast<UIElement>(), finalSize)) child.Element.Arrange(child.Rectangle); return finalSize; } private double totalChildWeight() { double weightSum = 0; foreach (UIElement elem in InternalChildren) weightSum += (double)elem.GetValue(WeightProperty); return weightSum; } /// <summary> /// Return child elements orderd by weight (largest to /// smallest), passing back Rect for each child /// (size and location), implementing a (crude) /// treemap. /// </summary> /// <param name="elems">Child elements to measure/arrange</param> /// <param name="containerSize">Available container size</param> /// <returns></returns> private IEnumerable<ChildAndRect> ChildrenTreemapOrder(IEnumerable<UIElement> elems, Size containerSize) { double remainingWeight = totalChildWeight(); double top = 0.0; double left = 0.0; // Alternate between left edge and top edge bool leftEdge; // Sort by weight var childrenByWeight = elems.OrderByDescending( e => (double)e.GetValue(WeightProperty)); // Allocate space for each child, one at a time. // Moving left to right, top to bottom foreach (var child in childrenByWeight) { leftEdge = (containerSize.Width - left) > (containerSize.Height - top); Size size; double childWeight = (double)child.GetValue(WeightProperty); double pctArea = childWeight / remainingWeight; remainingWeight -= childWeight; // Entire height, proportionate width if (leftEdge) size = new Size(pctArea * (containerSize.Width - left), containerSize.Height - top); // Top edge - Entire width, proportionate height else size = new Size(containerSize.Width - left, pctArea * (containerSize.Height - top)); yield return new ChildAndRect { Element = child, Rectangle = new Rect(new Point(left, top), size) }; if (leftEdge) left += size.Width; else top += size.Height; } } }
Below, we use the panel to create labels representing several states. The Weight property is used to record the states’ area. (The states are in no particular order).
<loc:WeightedPanel> <Label Content="Oregon" loc:WeightedPanel.Weight="93381" Background="Bisque" /> <Label Content="California" loc:WeightedPanel.Weight="163696" Background="Lavender" /> <Label Content="Colorado" loc:WeightedPanel.Weight="104094" Background="LightCoral" /> <Label Content="Montana" loc:WeightedPanel.Weight="147042" Background="Honeydew" /> <Label Content="Nevada" loc:WeightedPanel.Weight="110561" Background="Goldenrod" /> <Label Content="New Mexico" loc:WeightedPanel.Weight="121589" Background="Silver" /> <Label Content="Texas" loc:WeightedPanel.Weight="268581" Background="Thistle" /> <Label Content="Arizona" loc:WeightedPanel.Weight="113998" Background="GhostWhite" /> </loc:WeightedPanel>
Here’s what this looks like at run-time:
Note: One improvement that could be made to this algorithm is to adopt a true implementation of a treemap algorithm that includes “squarifying” elements to reduce the number of “long skinny” child objects.
Pingback: Dew Drop – October 9, 2014 (#1873) | Morning Dew