TreemapViewer: Building Silverlight Applications on Unix

A few months ago, to get an idea of what contributed to the size of Mono libraries I wrote a small Treemap visualizer using Moonlight:

Moonlight's assemblies; area represents the codesize.

You can browser the code or get a copy from our AnonSVN repository.

There are a couple of tools here:

My Treemap is not very ambitious, and it is nowhere as complete as the new Treemap control that is part of the open source Silverlight Toolkit. But it was a fun learning experience for me.

Reusable Engine

I did not really know where we would use this control, on the web or on the desktop when I started. So I split the actual engine that does the heavy lifting from the actual chrome for the application.

This is why I ended up with a Silverlight user interface and a Gtk# user interface. This idea in general might be useful for other developers as well.

The Custom Control

The custom control is very simple, it is called TreemapRenderer and derives from UserControl. The code overwrites two methods: MeasureOverride and ArrangeOverride. These methods are used to allow the control to participate in the Silverlight layout system (for example, the control can be embedded in a table that can auto-stretch). Silverlight invokes your MeasureOverride to find out the desired size that you control would like to consume:

public class TreemapRenderer : UserControl {
	protected override Size MeasureOverride(Size availableSize)
	[...]

	protected override Size ArrangeOverride(Size finalSize)
	[...]
}
	

Silverlight will invoke your control's ArrangeOverride to layout its contents once it has determined the size that the control will use.

This is where my TreemapRenderer lays out its contents.

This code renders an actual region on the treemap, I like the C# 3.0 initializer syntax for the text created:

public void SetRegion (Rect newRegion)
{
        region = newRegion;
        content.Children.Clear ();
        content.Width = region.Width;
        content.Height = region.Height;
        
        if (caption != ""){
                int max;
                string formatted = MakeCaption (caption, out max);
                double w = region.Width * 1.60;
                double s = w / max;

                var text = new TextBlock () {
                        FontSize = s,
                        Text = formatted,
                        Foreground = new SolidColorBrush (Color.FromArgb (255, 0x5c, 0x5c, 0x5c))
                };

                Canvas.SetTop (text, (region.Height-text.ActualHeight)/2);
                Canvas.SetLeft (text, (region.Width-text.ActualWidth)/2);
                content.Children.Add (text);
        }
 
        Rect emptyArea = region;
        Squarify (emptyArea, root.Children);
 
        Plot (root.Children);
}
	

To provide feedback to the user, I change the background color of the treemap on enter/leave. I use anonymous two anonymous methods, one for MouseEnter, one for MouseLeave.

Notice that state is shared by these two blocks of code using C# variable capturing. A key feature of the language:

	bool inside = false;
	host.MouseEnter += delegate {
	        host.Background = new SolidColorBrush (Colors.Yellow);
	        if (text != null)
	                text.Foreground = new SolidColorBrush (Colors.Black);
	        inside = true;
	};
	
	host.MouseLeave += delegate {
	        host.Background = transparentBrush;
	        if (text != null)
	                text.Foreground = borderBrush;
	        inside = false;
	};
	

One thing that proved very useful was the ability to zoom-into an area. If you click on an assembly, you will get a rendering of the code size used by a type; If you click on that, you get a method breakdown on a class:

drill-down into mscorlib's sizes.

To transition, I added a cute animation where I render the new image and animate it from the area occupied in the larger view into the final size. This is the code that queues the animation:

        // Render a child
        void Clicked (Node n)
        {
		// This is the child rendered, configure it to its final size
                TreemapRenderer c = new TreemapRenderer (n, n.Name);

                Size ns = new Size(region.Width, region.Height);
                c.Measure (ns);
                c.Arrange(region);

		//
		// Scale it and position on the spot that it currently
		// occupies on the screen
		//
                var xlate = new TranslateTransform () {
                        X = n.Rect.X,
                        Y = n.Rect.Y };

                var scale = new ScaleTransform () {
                        ScaleX = n.Rect.Width / region.Width,
                        ScaleY = n.Rect.Height / region.Height };

                c.RenderTransform = new TransformGroup { Children = { scale, xlate } };
                c.Opacity = 0.5;
                content.Children.Add (c);
                activeChild = c;

                //
		// Animate this to its final location
		//
                TimeSpan time = TimeSpan.FromSeconds (0.3);

                var s = new Storyboard () {
                        Children = {
                                Animate (time, xlate, "X", 0),
                                Animate (time, xlate, "Y", 0),
                                Animate (time, scale, "ScaleX", 1.0),
                                Animate (time, scale, "ScaleY", 1.0),
                                Animate (time, c, "Opacity", 1.0),
                        }};

                s.Begin ();
	}

	// Helper method;   
        static Timeline Animate (TimeSpan time, DependencyObject target, string path, double to)
        {
                var animation = new DoubleAnimation () {
                        Duration = time,
                        To = to
                };

                Storyboard.SetTarget (animation, target);
                Storyboard.SetTargetProperty (animation, new PropertyPath (path));

                return animation;
        }
	

The Silverlight Application

Using the control is fairly simple, but since it loads a large XML files (feel free to change this to use a web service) I use the downloader and a callback to render the control.

        private void Application_Startup(object sender, StartupEventArgs e)
        {
	    [...]
            var webclient = new WebClient();
            webclient.DownloadStringCompleted += delegate (object sender2, DownloadStringCompletedEventArgs ee){
                try {
                    RootVisual.Dispatcher.BeginInvoke(() => LoadNodesFromString(ee.Result));
                }
                catch (Exception ex) {
                    main.BackButton.Content = ex.ToString();
                }
            };

            webclient.DownloadStringAsync(new Uri("../mscorlib.xml", UriKind.Relative));
        }
	

The actual creation of the control happens in LoadNodesFromString:

        void LoadNodesFromString(String s)
        {
	    // Use Size for the area.
            Node n = LoadNodes(s, "Size", "Extra");
            while (n.Children.Count == 1)
                n = n.Children[0];
            
            treemap = new TreemapRenderer(n, "");
            Grid.SetColumn(treemap, 0);
            Grid.SetRow(treemap, 1);
            main.grid.Children.Add(treemap);
        }

Building a Gtk# Out-of-Browser Client

The above works great for Silverlight, but I like my application on the desktop, so I created a Gtk# version of it.

Moonlight provides a Gtk# widget that can be embedded into C# desktop applications. This is what it looks like when running on the desktop. I know this screenshot is not too exciting as I did not do much with the Gtk+ side of things:

Gtk# is rendering, seriously.

The core of embedding Moonlight as a Gtk# widget is very simple, here it is:

using Moonlight.Gtk;
using Moonlight;

        public static void Main(string[] args)
        {
                n = LoadNodes (args [0], "Size", "Foo");

                Gtk.Application.Init ();
                MoonlightRuntime.Init ();

		// My container window.
                Gtk.Window w = new Gtk.Window ("Foo");
                w.DeleteEvent += delegate {
                        Gtk.Application.Quit ();
                };
                w.SetSizeRequest (width, height);

		// The Moonlight widget that will host my UI.
                MoonlightHost h = new MoonlightHost ();

		// Add Moonlight widget, show window.
                w.Add (h);
                w.ShowAll ();

                // Make it pretty, skip all levels that are just 1 element
                while (n.Children.Count == 1)
                        n = n.Children [0];

                // Render
                TreemapRenderer r = new TreemapRenderer (n, "");
                Size available = new Size (width, height);
                r.Measure (available);
                r.Arrange (new Rect (0, 0, width, height));

		// This informs the widget which widget is the root
                h.Application.RootVisual = r;
                Gtk.Application.Run ();
        }

There are Visual Studio and MonoDevelop solutions on the SVN for folks to try out.

You can also try a sample live on the web.

Posted on 20 Jul 2009 by Miguel de Icaza
This is a personal web page. Things said here do not represent the position of my employer.