Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

From C# serverside, Is there anyway to generate a treemap and save as an image?

Tags:

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.

like image 593
leora Avatar asked Sep 13 '15 10:09

leora


People also ask

What is Form C?

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.

What is Form C in India?

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.

What is C Form hotel?

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).


1 Answers

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: Result

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.

like image 197
Evk Avatar answered Sep 20 '22 14:09

Evk