#1,176 – Custom Panel, part VIII (Treemap-like Visualization)

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:
1176-001
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.

About Sean
Software developer in the Twin Cities area, passionate about software development and sailing.

One Response to #1,176 – Custom Panel, part VIII (Treemap-like Visualization)

  1. Pingback: Dew Drop – October 9, 2014 (#1873) | Morning Dew

Leave a comment