I have been using this javascript library to create treemap on webpages and it works great. The issue now is that I need to include this in a powerpoint presentation that I am generating on the server side (I am generating the powerpoint using aspose.slides for .net)
The easiest thing I thought of was to try to somehow build a treemap on the server and save as an image (as adding an image into the powerpoint presentation is quite simple) but after googling, I don't see any solution that from C# serverside can generate a treemap as an image.
Does something like this exist where I can create a treemap as an image from a server side C# app.
Business transactions between different states must be pursued with a certificate, which is known as C from. It is issued by the seller of goods to the buyer of goods for the purpose of effecting a reduction on the rate of tax.
The C-Form mechanism helps the authorities locate and track foreigners in India to enhance security and safety. Failure to comply with reporting requirements could result in fines and imprisonment of up to 5 years.
C Form is designed for collecting prerequisite information of foreigner guest whose accommodation is made in hotel. This is indeed requirements for security points of view. Its one copy must be submitted at the FRRO (foreign regional registration office).
Given that algorithms are known, it's not hard to just draw a bitmap with a treemap. At the moment I don't have enough time to write code myself, but I have enough time to (almost) mindlessly port some existing code to C# :) Let's take this javascript implementation. It uses algorithm described in this paper. I found some problems in that implementation, which are fixed in C# version. Javascript version works with pure arrays (and arrays of arrays of arrays) of integers. We define some class instead:
public class TreemapItem { private TreemapItem() { FillBrush = Brushes.White; BorderBrush = Brushes.Black; TextBrush = Brushes.Black; } public TreemapItem(string label, int area, Brush fillBrush) : this() { Label = label; Area = area; FillBrush = fillBrush; Children = null; } public TreemapItem(params TreemapItem[] children) : this() { // in this implementation if there are children - all other properies are ignored // but this can be changed in future Children = children; } // Label to write on rectangle public string Label { get; set; } // color to fill rectangle with public Brush FillBrush { get; set; } // color to fill rectangle border with public Brush BorderBrush { get; set; } // color of label public Brush TextBrush { get; set; } // area public int Area { get; set; } // children public TreemapItem[] Children { get; set; } }
Then starting to port. First Container class:
class Container { public Container(int x, int y, int width, int height) { X = x; Y = y; Width = width; Height = height; } public int X { get; } public int Y { get; } public int Width { get; } public int Height { get; } public int ShortestEdge => Math.Min(Width, Height); public IDictionary<TreemapItem, Rectangle> GetCoordinates(TreemapItem[] row) { // getCoordinates - for a row of boxes which we've placed // return an array of their cartesian coordinates var coordinates = new Dictionary<TreemapItem, Rectangle>(); var subx = this.X; var suby = this.Y; var areaWidth = row.Select(c => c.Area).Sum()/(float) Height; var areaHeight = row.Select(c => c.Area).Sum()/(float) Width; if (Width >= Height) { for (int i = 0; i < row.Length; i++) { var rect = new Rectangle(subx, suby, (int) (areaWidth), (int) (row[i].Area/areaWidth)); coordinates.Add(row[i], rect); suby += (int) (row[i].Area/areaWidth); } } else { for (int i = 0; i < row.Length; i++) { var rect = new Rectangle(subx, suby, (int) (row[i].Area/areaHeight), (int) (areaHeight)); coordinates.Add(row[i], rect); subx += (int) (row[i].Area/areaHeight); } } return coordinates; } public Container CutArea(int area) { // cutArea - once we've placed some boxes into an row we then need to identify the remaining area, // this function takes the area of the boxes we've placed and calculates the location and // dimensions of the remaining space and returns a container box defined by the remaining area if (Width >= Height) { var areaWidth = area/(float) Height; var newWidth = Width - areaWidth; return new Container((int) (X + areaWidth), Y, (int) newWidth, Height); } else { var areaHeight = area/(float) Width; var newHeight = Height - areaHeight; return new Container(X, (int) (Y + areaHeight), Width, (int) newHeight); } } }
Then Treemap
class which builds actual Bitmap
public class Treemap { public Bitmap Build(TreemapItem[] items, int width, int height) { var map = BuildMultidimensional(items, width, height, 0, 0); var bmp = new Bitmap(width, height); var g = Graphics.FromImage(bmp); g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; foreach (var kv in map) { var item = kv.Key; var rect = kv.Value; // fill rectangle g.FillRectangle(item.FillBrush, rect); // draw border g.DrawRectangle(new Pen(item.BorderBrush, 1), rect); if (!String.IsNullOrWhiteSpace(item.Label)) { // draw text var format = new StringFormat(); format.Alignment = StringAlignment.Center; format.LineAlignment = StringAlignment.Center; var font = new Font("Arial", 16); g.DrawString(item.Label, font, item.TextBrush, new RectangleF(rect.X, rect.Y, rect.Width, rect.Height), format); } } return bmp; } private Dictionary<TreemapItem, Rectangle> BuildMultidimensional(TreemapItem[] items, int width, int height, int x, int y) { var results = new Dictionary<TreemapItem, Rectangle>(); var mergedData = new TreemapItem[items.Length]; for (int i = 0; i < items.Length; i++) { // calculate total area of children - current item's area is ignored mergedData[i] = SumChildren(items[i]); } // build a map for this merged items (merged because their area is sum of areas of their children) var mergedMap = BuildFlat(mergedData, width, height, x, y); for (int i = 0; i < items.Length; i++) { var mergedChild = mergedMap[mergedData[i]]; // inspect children of children in the same way if (items[i].Children != null) { var headerRect = new Rectangle(mergedChild.X, mergedChild.Y, mergedChild.Width, 20); results.Add(mergedData[i], headerRect); // reserve 20 pixels of height for header foreach (var kv in BuildMultidimensional(items[i].Children, mergedChild.Width, mergedChild.Height - 20, mergedChild.X, mergedChild.Y + 20)) { results.Add(kv.Key, kv.Value); } } else { results.Add(mergedData[i], mergedChild); } } return results; } private Dictionary<TreemapItem, Rectangle> BuildFlat(TreemapItem[] items, int width, int height, int x, int y) { // normalize all area values for given width and height Normalize(items, width*height); var result = new Dictionary<TreemapItem, Rectangle>(); Squarify(items, new TreemapItem[0], new Container(x, y, width, height), result); return result; } private void Normalize(TreemapItem[] data, int area) { var sum = data.Select(c => c.Area).Sum(); var multi = area/(float) sum; foreach (var item in data) { item.Area = (int) (item.Area*multi); } } private void Squarify(TreemapItem[] data, TreemapItem[] currentRow, Container container, Dictionary<TreemapItem, Rectangle> stack) { if (data.Length == 0) { foreach (var kv in container.GetCoordinates(currentRow)) { stack.Add(kv.Key, kv.Value); } return; } var length = container.ShortestEdge; var nextPoint = data[0]; if (ImprovesRatio(currentRow, nextPoint, length)) { currentRow = currentRow.Concat(new[] {nextPoint}).ToArray(); Squarify(data.Skip(1).ToArray(), currentRow, container, stack); } else { var newContainer = container.CutArea(currentRow.Select(c => c.Area).Sum()); foreach (var kv in container.GetCoordinates(currentRow)) { stack.Add(kv.Key, kv.Value); } Squarify(data, new TreemapItem[0], newContainer, stack); } } private bool ImprovesRatio(TreemapItem[] currentRow, TreemapItem nextNode, int length) { // if adding nextNode if (currentRow.Length == 0) return true; var newRow = currentRow.Concat(new[] {nextNode}).ToArray(); var currentRatio = CalculateRatio(currentRow, length); var newRatio = CalculateRatio(newRow, length); return currentRatio >= newRatio; } private int CalculateRatio(TreemapItem[] row, int length) { var min = row.Select(c => c.Area).Min(); var max = row.Select(c => c.Area).Max(); var sum = row.Select(c => c.Area).Sum(); return (int) Math.Max(Math.Pow(length, 2)*max/Math.Pow(sum, 2), Math.Pow(sum, 2)/(Math.Pow(length, 2)*min)); } private TreemapItem SumChildren(TreemapItem item) { int total = 0; if (item.Children?.Length > 0) { total += item.Children.Sum(c => c.Area); foreach (var child in item.Children) { total += SumChildren(child).Area; } } else { total = item.Area; } return new TreemapItem(item.Label, total, item.FillBrush); } }
Now let's try to use and see how it goes:
var map = new[] { new TreemapItem("ItemA", 0, Brushes.DarkGray) { Children = new[] { new TreemapItem("ItemA-1", 200, Brushes.White), new TreemapItem("ItemA-2", 500, Brushes.BurlyWood), new TreemapItem("ItemA-3", 600, Brushes.Purple), } }, new TreemapItem("ItemB", 1000, Brushes.Yellow) { }, new TreemapItem("ItemC", 0, Brushes.Red) { Children = new[] { new TreemapItem("ItemC-1", 200, Brushes.White), new TreemapItem("ItemC-2", 500, Brushes.BurlyWood), new TreemapItem("ItemC-3", 600, Brushes.Purple), } }, new TreemapItem("ItemD", 2400, Brushes.Blue) { }, new TreemapItem("ItemE", 0, Brushes.Cyan) { Children = new[] { new TreemapItem("ItemE-1", 200, Brushes.White), new TreemapItem("ItemE-2", 500, Brushes.BurlyWood), new TreemapItem("ItemE-3", 600, Brushes.Purple), } }, }; using (var bmp = new Treemap().Build(map, 1024, 1024)) { bmp.Save("output.bmp", ImageFormat.Bmp); }
Output:
This can be extended in multiple ways, and code quality can certainly be improved significantly. But if you would go this way, it can at least give you a good start. Benefit is that it's fast and no external dependencies involved. If you would want to use it and find some issues or it does not match some of your requirements - feel free to ask and I will improve it when will have more time.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With