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

7

23

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.

Godolphin answered 13/9, 2015 at 10:56 Comment(8)
pascallaurin42.blogspot.co.il/2013/12/… might this information assist you? I mean it looks like he is creating a bitmap of the treemapSpurrier
Why don't you export your javascript treemap to an image?Afield
Well... after spending a lot of time searching for a way to create treemap from C# code, I think it's impossible :( I think you should create it using your javascript library, then turn it into an image. I can turn the graph generated by your using library into an image.Afield
One idea is to use WebBrowser control from WinForms (without showing the form) to open your image generated by javascript and then use Control.DrawToBitmap method to get you image example : #4674802Benisch
One option would be to use SVG to draw your images in the returned html.Quadrennial
Have you tried phantom.js: phantomjs.orgChug
@Simon Mourier - I haven't but i need the image serversideGodolphin
I got that, and it's supposed to work with phantom.js, hence my questionChug
J
2

I'm basing the following solution in this project about treemaps in WPF.

Using the data in your link, you can define your model (only with necesary data) like this:

class Data
{
    [JsonProperty("$area")]
    public float Area { get; set; }

    [JsonProperty("$color")]
    public Color Color { get; set; }
}

class Item
{
    public string Name { get; set; }
    public Data Data { get; set; }
    public IEnumerable<Item> Children { get; set; }

    internal TreeMapData TMData { get; set; }

    internal int GetDepth()
    {
        return Children.Select(c => c.GetDepth()).DefaultIfEmpty().Max() + 1;
    }
}

Adding an extra property TreeMapData with some values used in the solution:

class TreeMapData
{
    public float Area { get; set; }
    public SizeF Size { get; set; }
    public PointF Location { get; set; }
}

Now, defining a TreeMap class with the following public members:

class TreeMap
{
    public IEnumerable<Item> Items { get; private set; }

    public TreeMap(params Item[] items) : 
        this(items.AsEnumerable()) { }

    public TreeMap(IEnumerable<Item> items)
    {
        Items = items.OrderByDescending(t => t.Data.Area).ThenByDescending(t => t.Children.Count());
    }

    public Bitmap Draw(int width, int height)
    {
        var bmp = new Bitmap(width + 1, height + 1);
        using (var g = Graphics.FromImage(bmp))
        {
            DrawIn(g, 0, 0, width, height);
            g.Flush();
        }

        return bmp;
    }

    //Private members
}

So, you can use it like this:

var treeMap = new TreeMap(items);
var bmp = treeMap.Draw(1366, 768);

And the private/helper members:

private RectangleF emptyArea;

private void DrawIn(Graphics g, float x, float y, float width, float height)
{
    Measure(width, height);

    foreach (var item in Items)
    {
        var sFormat = new StringFormat
        {
            Alignment = StringAlignment.Center,
            LineAlignment = StringAlignment.Center
        };

        if (item.Children.Count() > 0)
        {
            g.FillRectangle(Brushes.DimGray, x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, 15);
            g.DrawString(item.Name, SystemFonts.DefaultFont, Brushes.LightGray, new RectangleF(x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, 15), sFormat);

            var treeMap = new TreeMap(item.Children);
            treeMap.DrawIn(g, x + item.TMData.Location.X, y + item.TMData.Location.Y + 15, item.TMData.Size.Width, item.TMData.Size.Height - 15);
        }
        else
        {                    
            g.FillRectangle(new SolidBrush(item.Data.Color), x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, item.TMData.Size.Height);
            g.DrawString(item.Name, SystemFonts.DefaultFont, Brushes.Black, new RectangleF(x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, item.TMData.Size.Height), sFormat);
        }

        var pen = new Pen(Color.Black, item.GetDepth() * 1.5f);
        g.DrawRectangle(pen, x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, item.TMData.Size.Height);
    }

    g.Flush();
}

private void Measure(float width, float height)
{
    emptyArea = new RectangleF(0, 0, width, height);

    var area = width * height;
    var sum = Items.Sum(t => t.Data.Area + 1);

    foreach (var item in Items)
    {
        item.TMData = new TreeMapData();
        item.TMData.Area = area * (item.Data.Area + 1) / sum;
    }

    Squarify(Items, new List<Item>(), ShortestSide());

    foreach (var child in Items)
        if (!IsValidSize(child.TMData.Size))
            child.TMData.Size = new Size(0, 0);
}

private void Squarify(IEnumerable<Item> items, IEnumerable<Item> row, float sideLength)
{
    if (items.Count() == 0)
    {
        ComputeTreeMaps(row);
        return;
    }

    var item = items.First();
    List<Item> row2 = new List<Item>(row);
    row2.Add(item);

    List<Item> items2 = new List<Item>(items);
    items2.RemoveAt(0);

    float worst1 = Worst(row, sideLength);
    float worst2 = Worst(row2, sideLength);

    if (row.Count() == 0 || worst1 > worst2)
        Squarify(items2, row2, sideLength);
    else
    {
        ComputeTreeMaps(row);
        Squarify(items, new List<Item>(), ShortestSide());
    }
}

private void ComputeTreeMaps(IEnumerable<Item> items)
{
    var orientation = this.GetOrientation();

    float areaSum = 0;

    foreach (var item in items)
        areaSum += item.TMData.Area;

    RectangleF currentRow;
    if (orientation == RowOrientation.Horizontal)
    {
        currentRow = new RectangleF(emptyArea.X, emptyArea.Y, areaSum / emptyArea.Height, emptyArea.Height);
        emptyArea = new RectangleF(emptyArea.X + currentRow.Width, emptyArea.Y, Math.Max(0, emptyArea.Width - currentRow.Width), emptyArea.Height);
    }
    else
    {
        currentRow = new RectangleF(emptyArea.X, emptyArea.Y, emptyArea.Width, areaSum / emptyArea.Width);
        emptyArea = new RectangleF(emptyArea.X, emptyArea.Y + currentRow.Height, emptyArea.Width, Math.Max(0, emptyArea.Height - currentRow.Height));
    }

    float prevX = currentRow.X;
    float prevY = currentRow.Y;

    foreach (var item in items)
    {
        var rect = GetRectangle(orientation, item, prevX, prevY, currentRow.Width, currentRow.Height);

        item.TMData.Size = rect.Size;
        item.TMData.Location = rect.Location;

        ComputeNextPosition(orientation, ref prevX, ref prevY, rect.Width, rect.Height);
    }
}

private RectangleF GetRectangle(RowOrientation orientation, Item item, float x, float y, float width, float height)
{
    if (orientation == RowOrientation.Horizontal)
        return new RectangleF(x, y, width, item.TMData.Area / width);
    else
        return new RectangleF(x, y, item.TMData.Area / height, height);
}

private void ComputeNextPosition(RowOrientation orientation, ref float xPos, ref float yPos, float width, float height)
{
    if (orientation == RowOrientation.Horizontal)
        yPos += height;
    else
        xPos += width;
}

private RowOrientation GetOrientation()
{
    return emptyArea.Width > emptyArea.Height ? RowOrientation.Horizontal : RowOrientation.Vertical;
}

private float Worst(IEnumerable<Item> row, float sideLength)
{
    if (row.Count() == 0) return 0;

    float maxArea = 0;
    float minArea = float.MaxValue;
    float totalArea = 0;

    foreach (var item in row)
    {
        maxArea = Math.Max(maxArea, item.TMData.Area);
        minArea = Math.Min(minArea, item.TMData.Area);
        totalArea += item.TMData.Area;
    }

    if (minArea == float.MaxValue) minArea = 0;

    float val1 = (sideLength * sideLength * maxArea) / (totalArea * totalArea);
    float val2 = (totalArea * totalArea) / (sideLength * sideLength * minArea);
    return Math.Max(val1, val2);
}

private float ShortestSide()
{
    return Math.Min(emptyArea.Width, emptyArea.Height);
}

private bool IsValidSize(SizeF size)
{
    return (!size.IsEmpty && size.Width > 0 && size.Width != float.NaN && size.Height > 0 && size.Height != float.NaN);
}

private enum RowOrientation
{
    Horizontal,
    Vertical
}

Finally, to parse and draw the json in the example I'm doing this:

var json = File.ReadAllText(@"treemap.json");
var items = JsonConvert.DeserializeObject<Item>(json);

var treeMap = new TreeMap(items);
var bmp = treeMap.Draw(1366, 768);

bmp.Save("treemap.png", ImageFormat.Png);

And the resulting image:

treemap


Actually I don't know if the following can help you or not since you aren't using vsto, AND AS SAID IN THE COMMENTS PROBABLY IS A BAD IDEA.

Starting in Office 2016, treemaps are incorporated as charts. You can read this to see how create treemaps from datasets in Excel.

So, you can generate the chart in Excel and pass it to PowerPoint:

//Start an hidden excel application
var appExcel = new Excel.Application { Visible = false }; 
var workbook = appExcel.Workbooks.Add();
var sheet = workbook.ActiveSheet;

//Generate some random data
Random r = new Random();
for (int i = 1; i <= 10; i++)
{
    sheet.Cells[i, 1].Value2 = ((char)('A' + i - 1)).ToString();
    sheet.Cells[i, 2].Value2 = r.Next(1, 20);
}

//Select the data to use in the treemap
var range = sheet.Cells.Range["A1", "B10"];
range.Select();
range.Activate();

//Generate the chart
var shape = sheet.Shapes.AddChart2(-1, (Office.XlChartType)117, 200, 25, 300, 300, null);
shape.Chart.ChartTitle.Caption = "Generated TreeMap Chart";

//Copy the chart
shape.Copy();

appExcel.Quit();

//Start a Powerpoint application
var appPpoint = new Point.Application { Visible = Office.MsoTriState.msoTrue };            
var presentation = appPpoint.Presentations.Add();

//Add a blank slide
var master = presentation.SlideMaster;
var slide = presentation.Slides.AddSlide(1, master.CustomLayouts[7]);

//Paste the treemap
slide.Shapes.Paste();

Treemap chart in the slide:

office treemap

Probably you can generate the treemap using the first part (Excel part) and paste the chart using the tool you said, or save the Powerpoint file with the chart generated in VSTO and open it with the tool.

The benefits are that these objects are real charts not just images, so you can change or add colors, styles, effects easily.

Jacquesjacquet answered 15/5, 2016 at 21:7 Comment(8)
I have never really used anything MS Office related on the server before, when running something like this wouldn't you run into huge bottlenecks if you have to start scaling per user?Sixteenmo
@KevinBBurns: probably true, neither I used, I was just showing that is possible and how to do it, but do not need to scale explicitly by users, a single instance can be used for multiple requests.Jacquesjacquet
DONT DO SERVER SIDE AUTOMATION OF OFFICE - KB257757, otherwise good answerReisfield
@Arturo - this is very cool but i have a question. When i tested this out, i don't see the "parent" of a number of children shown anywhere (I was thinking it would show the parent label name as a label around the children. it seems to just show the lowest level nodes. Is that possible with this implementationGodolphin
@ArturoMenchaca I really the solution you offered but it doesn't really apply to the question asked. OP specifically asked for something server side. I am not trying to start anything, but I just want to make sure that other people don't stumble across this in the future and think this is an appropriate answer for a server side application.Sixteenmo
@leora: In your first link, there is an example showing a treemap, you can go this link: philogb.github.io/jit/static/v20/Jit/Examples/Treemap/… to see the javascript code, where there is a json variable describing the treemap, I just copy the json content into ajson file. About the parents, it's true, I just forgot to display parent's info, updating answer. The color come from the json file, there is property called $color with the values, if you want I can try to automatically generate the colors.Jacquesjacquet
@KevinBBurns: Agreed, I'm going to update the answer.Jacquesjacquet
@leora: updated answer with parent's info and wider borders according to depth.Jacquesjacquet
I
14

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.

Inexecution answered 11/5, 2016 at 6:43 Comment(3)
This is very cool. One thing I can't figure out is how to show the label of a parent node when it has multiple children. It uses the parent in the algorithm to group the item but doesn't visualize the parent container name anywhere. Is that possible with this approach ?Godolphin
@Godolphin yes that's certainly possible. I've updated answer to include 20-pixel header for items with children. Also I fixed several important errors in code (which are also present in that javascript library - not sure how it even works really) so just throw away old version completely and use new one.Inexecution
thanks so much for the effort here. I will take a look asap and let you know if i run into any issues.Godolphin
R
7

Using the GDI+ api may be your only choice, with good support cross platform. However, there are several potential issues you need to be aware of when doing anything with GDI+ on the server side. It's worth reading this as it explains the current state of drawing Graphics in DotNet and has a point on Server-side processing:

https://github.com/imazen/Graphics-vNext

Having said that; there's this article that deals with what you're asking for:

OutOfMemory Exception when recursively drawing rectangles in GDI+ (It's specifically talking about generating a TreeMap with GDI+ and if you read the comments and answer you'll avoid a lot of the pitfalls)

Once you've generated your image, it's a trivial process to save it to disk somewhere and then, hopefully, embed it within your presentation; you have options to write to streams as well, so it may be possible to directly embed it in the powerpoint file without first saving it to disk.

Roybal answered 3/10, 2015 at 12:15 Comment(1)
I thought using MS Office on the server was a bad idea, GDI+ is much worse. OP wants to be able to grab the treemap and then shove it into a powerpoint or pdf. You start running this solution with 10+ users at the same time, you're gonna watch a 8 core Xeon come to a screeching halt.Sixteenmo
S
4

You can use WPF rendering: http://lordzoltan.blogspot.co.uk/2010/09/using-wpf-to-render-bitmaps.html but it's not without its drawbacks.

(That's a link to my own old blog - but if you search for 'using wpf to generate images' you'll get lots of other examples - many of which are better than mine!)

Generating a tree in WPF is going to be, well, challenging, though - although it can be done, since the WPF drawing primitives are hierarchical in nature.

It might not be suitable, bou could also consider GraphViz - https://github.com/JamieDixon/GraphViz-C-Sharp-Wrapper however I don't know how much luck you'll have executing the command line in a web server.

There are - I expect - bound to be paid-for libraries to do this, as well, as it's a common need.

Stabile answered 13/9, 2015 at 11:22 Comment(1)
I am using asp.net MacGodolphin
W
4

The Eindhoven University of Technology has published a paper on the algorithm of squarified treemaps. Pascal Laurin has turned this into C#. There is also a Code Project article that has a section about treemaps.

Of course there are also commercial solutions like the one from .NET Charting, Infragistics or Telerik. The downside of those is probably that they are designed as Controls which need to be painted, so you might need some sort of UI thread.

There is also a question here on Stack Overflow that already asked for treemap implementations in C#. Just in case you don't remember.

Wimsatt answered 6/10, 2015 at 19:35 Comment(0)
S
4

Since you are already generating the JS and the HTML version of everything, one thing you might want to check out is :

http://www.nrecosite.com/html_to_image_generator_net.aspx

I use this to generate high res-graphic reports straight from my generated pages. It used WKHTML to render it and you can pass a ton of parameters to it to really fine tune it. Its free to most things and it works great. Multi-threading is kind of a pain in the butt with it but I haven't run into to many issues. If you use the NRECO PDf library you can even do batches of stuff also.

With this all you would have to do is render the page like you already are, pipe it through the library and insert into your PPT and all should be good.

Sixteenmo answered 13/5, 2016 at 14:35 Comment(0)
M
4

Since all you need to do is to extract a screenshot of the web page, it would be more convenient to capture the web page as an Image.

This free library is capable of extracting screenshot from your web page, and it supports Javascript / CSS.

Mediant answered 17/5, 2016 at 7:25 Comment(1)
I had already added the same link, OP is already doing the heavy lifting why not just take the screenshot and move on.Sixteenmo
J
2

I'm basing the following solution in this project about treemaps in WPF.

Using the data in your link, you can define your model (only with necesary data) like this:

class Data
{
    [JsonProperty("$area")]
    public float Area { get; set; }

    [JsonProperty("$color")]
    public Color Color { get; set; }
}

class Item
{
    public string Name { get; set; }
    public Data Data { get; set; }
    public IEnumerable<Item> Children { get; set; }

    internal TreeMapData TMData { get; set; }

    internal int GetDepth()
    {
        return Children.Select(c => c.GetDepth()).DefaultIfEmpty().Max() + 1;
    }
}

Adding an extra property TreeMapData with some values used in the solution:

class TreeMapData
{
    public float Area { get; set; }
    public SizeF Size { get; set; }
    public PointF Location { get; set; }
}

Now, defining a TreeMap class with the following public members:

class TreeMap
{
    public IEnumerable<Item> Items { get; private set; }

    public TreeMap(params Item[] items) : 
        this(items.AsEnumerable()) { }

    public TreeMap(IEnumerable<Item> items)
    {
        Items = items.OrderByDescending(t => t.Data.Area).ThenByDescending(t => t.Children.Count());
    }

    public Bitmap Draw(int width, int height)
    {
        var bmp = new Bitmap(width + 1, height + 1);
        using (var g = Graphics.FromImage(bmp))
        {
            DrawIn(g, 0, 0, width, height);
            g.Flush();
        }

        return bmp;
    }

    //Private members
}

So, you can use it like this:

var treeMap = new TreeMap(items);
var bmp = treeMap.Draw(1366, 768);

And the private/helper members:

private RectangleF emptyArea;

private void DrawIn(Graphics g, float x, float y, float width, float height)
{
    Measure(width, height);

    foreach (var item in Items)
    {
        var sFormat = new StringFormat
        {
            Alignment = StringAlignment.Center,
            LineAlignment = StringAlignment.Center
        };

        if (item.Children.Count() > 0)
        {
            g.FillRectangle(Brushes.DimGray, x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, 15);
            g.DrawString(item.Name, SystemFonts.DefaultFont, Brushes.LightGray, new RectangleF(x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, 15), sFormat);

            var treeMap = new TreeMap(item.Children);
            treeMap.DrawIn(g, x + item.TMData.Location.X, y + item.TMData.Location.Y + 15, item.TMData.Size.Width, item.TMData.Size.Height - 15);
        }
        else
        {                    
            g.FillRectangle(new SolidBrush(item.Data.Color), x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, item.TMData.Size.Height);
            g.DrawString(item.Name, SystemFonts.DefaultFont, Brushes.Black, new RectangleF(x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, item.TMData.Size.Height), sFormat);
        }

        var pen = new Pen(Color.Black, item.GetDepth() * 1.5f);
        g.DrawRectangle(pen, x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, item.TMData.Size.Height);
    }

    g.Flush();
}

private void Measure(float width, float height)
{
    emptyArea = new RectangleF(0, 0, width, height);

    var area = width * height;
    var sum = Items.Sum(t => t.Data.Area + 1);

    foreach (var item in Items)
    {
        item.TMData = new TreeMapData();
        item.TMData.Area = area * (item.Data.Area + 1) / sum;
    }

    Squarify(Items, new List<Item>(), ShortestSide());

    foreach (var child in Items)
        if (!IsValidSize(child.TMData.Size))
            child.TMData.Size = new Size(0, 0);
}

private void Squarify(IEnumerable<Item> items, IEnumerable<Item> row, float sideLength)
{
    if (items.Count() == 0)
    {
        ComputeTreeMaps(row);
        return;
    }

    var item = items.First();
    List<Item> row2 = new List<Item>(row);
    row2.Add(item);

    List<Item> items2 = new List<Item>(items);
    items2.RemoveAt(0);

    float worst1 = Worst(row, sideLength);
    float worst2 = Worst(row2, sideLength);

    if (row.Count() == 0 || worst1 > worst2)
        Squarify(items2, row2, sideLength);
    else
    {
        ComputeTreeMaps(row);
        Squarify(items, new List<Item>(), ShortestSide());
    }
}

private void ComputeTreeMaps(IEnumerable<Item> items)
{
    var orientation = this.GetOrientation();

    float areaSum = 0;

    foreach (var item in items)
        areaSum += item.TMData.Area;

    RectangleF currentRow;
    if (orientation == RowOrientation.Horizontal)
    {
        currentRow = new RectangleF(emptyArea.X, emptyArea.Y, areaSum / emptyArea.Height, emptyArea.Height);
        emptyArea = new RectangleF(emptyArea.X + currentRow.Width, emptyArea.Y, Math.Max(0, emptyArea.Width - currentRow.Width), emptyArea.Height);
    }
    else
    {
        currentRow = new RectangleF(emptyArea.X, emptyArea.Y, emptyArea.Width, areaSum / emptyArea.Width);
        emptyArea = new RectangleF(emptyArea.X, emptyArea.Y + currentRow.Height, emptyArea.Width, Math.Max(0, emptyArea.Height - currentRow.Height));
    }

    float prevX = currentRow.X;
    float prevY = currentRow.Y;

    foreach (var item in items)
    {
        var rect = GetRectangle(orientation, item, prevX, prevY, currentRow.Width, currentRow.Height);

        item.TMData.Size = rect.Size;
        item.TMData.Location = rect.Location;

        ComputeNextPosition(orientation, ref prevX, ref prevY, rect.Width, rect.Height);
    }
}

private RectangleF GetRectangle(RowOrientation orientation, Item item, float x, float y, float width, float height)
{
    if (orientation == RowOrientation.Horizontal)
        return new RectangleF(x, y, width, item.TMData.Area / width);
    else
        return new RectangleF(x, y, item.TMData.Area / height, height);
}

private void ComputeNextPosition(RowOrientation orientation, ref float xPos, ref float yPos, float width, float height)
{
    if (orientation == RowOrientation.Horizontal)
        yPos += height;
    else
        xPos += width;
}

private RowOrientation GetOrientation()
{
    return emptyArea.Width > emptyArea.Height ? RowOrientation.Horizontal : RowOrientation.Vertical;
}

private float Worst(IEnumerable<Item> row, float sideLength)
{
    if (row.Count() == 0) return 0;

    float maxArea = 0;
    float minArea = float.MaxValue;
    float totalArea = 0;

    foreach (var item in row)
    {
        maxArea = Math.Max(maxArea, item.TMData.Area);
        minArea = Math.Min(minArea, item.TMData.Area);
        totalArea += item.TMData.Area;
    }

    if (minArea == float.MaxValue) minArea = 0;

    float val1 = (sideLength * sideLength * maxArea) / (totalArea * totalArea);
    float val2 = (totalArea * totalArea) / (sideLength * sideLength * minArea);
    return Math.Max(val1, val2);
}

private float ShortestSide()
{
    return Math.Min(emptyArea.Width, emptyArea.Height);
}

private bool IsValidSize(SizeF size)
{
    return (!size.IsEmpty && size.Width > 0 && size.Width != float.NaN && size.Height > 0 && size.Height != float.NaN);
}

private enum RowOrientation
{
    Horizontal,
    Vertical
}

Finally, to parse and draw the json in the example I'm doing this:

var json = File.ReadAllText(@"treemap.json");
var items = JsonConvert.DeserializeObject<Item>(json);

var treeMap = new TreeMap(items);
var bmp = treeMap.Draw(1366, 768);

bmp.Save("treemap.png", ImageFormat.Png);

And the resulting image:

treemap


Actually I don't know if the following can help you or not since you aren't using vsto, AND AS SAID IN THE COMMENTS PROBABLY IS A BAD IDEA.

Starting in Office 2016, treemaps are incorporated as charts. You can read this to see how create treemaps from datasets in Excel.

So, you can generate the chart in Excel and pass it to PowerPoint:

//Start an hidden excel application
var appExcel = new Excel.Application { Visible = false }; 
var workbook = appExcel.Workbooks.Add();
var sheet = workbook.ActiveSheet;

//Generate some random data
Random r = new Random();
for (int i = 1; i <= 10; i++)
{
    sheet.Cells[i, 1].Value2 = ((char)('A' + i - 1)).ToString();
    sheet.Cells[i, 2].Value2 = r.Next(1, 20);
}

//Select the data to use in the treemap
var range = sheet.Cells.Range["A1", "B10"];
range.Select();
range.Activate();

//Generate the chart
var shape = sheet.Shapes.AddChart2(-1, (Office.XlChartType)117, 200, 25, 300, 300, null);
shape.Chart.ChartTitle.Caption = "Generated TreeMap Chart";

//Copy the chart
shape.Copy();

appExcel.Quit();

//Start a Powerpoint application
var appPpoint = new Point.Application { Visible = Office.MsoTriState.msoTrue };            
var presentation = appPpoint.Presentations.Add();

//Add a blank slide
var master = presentation.SlideMaster;
var slide = presentation.Slides.AddSlide(1, master.CustomLayouts[7]);

//Paste the treemap
slide.Shapes.Paste();

Treemap chart in the slide:

office treemap

Probably you can generate the treemap using the first part (Excel part) and paste the chart using the tool you said, or save the Powerpoint file with the chart generated in VSTO and open it with the tool.

The benefits are that these objects are real charts not just images, so you can change or add colors, styles, effects easily.

Jacquesjacquet answered 15/5, 2016 at 21:7 Comment(8)
I have never really used anything MS Office related on the server before, when running something like this wouldn't you run into huge bottlenecks if you have to start scaling per user?Sixteenmo
@KevinBBurns: probably true, neither I used, I was just showing that is possible and how to do it, but do not need to scale explicitly by users, a single instance can be used for multiple requests.Jacquesjacquet
DONT DO SERVER SIDE AUTOMATION OF OFFICE - KB257757, otherwise good answerReisfield
@Arturo - this is very cool but i have a question. When i tested this out, i don't see the "parent" of a number of children shown anywhere (I was thinking it would show the parent label name as a label around the children. it seems to just show the lowest level nodes. Is that possible with this implementationGodolphin
@ArturoMenchaca I really the solution you offered but it doesn't really apply to the question asked. OP specifically asked for something server side. I am not trying to start anything, but I just want to make sure that other people don't stumble across this in the future and think this is an appropriate answer for a server side application.Sixteenmo
@leora: In your first link, there is an example showing a treemap, you can go this link: philogb.github.io/jit/static/v20/Jit/Examples/Treemap/… to see the javascript code, where there is a json variable describing the treemap, I just copy the json content into ajson file. About the parents, it's true, I just forgot to display parent's info, updating answer. The color come from the json file, there is property called $color with the values, if you want I can try to automatically generate the colors.Jacquesjacquet
@KevinBBurns: Agreed, I'm going to update the answer.Jacquesjacquet
@leora: updated answer with parent's info and wider borders according to depth.Jacquesjacquet

© 2022 - 2024 — McMap. All rights reserved.