c# - 从 C# 服务器端,是否有生成 TreeMap 并另存为图像的方法?

标签 c# asp.net-mvc image treemap

我一直在使用this javascript library在网页上创建树形图,效果很好。现在的问题是我需要将其包含在我在服务器端生成的 powerpoint 演示文稿中(我正在使用 aspose.slides for .net 生成 powerpoint)

我想到的最简单的事情是尝试以某种方式在服务器上构建一个树状图并另存为图像(因为将图像添加到 powerpoint 演示文稿中非常简单)但是在谷歌搜索之后,我没有看到任何解决方案从 C# 服务器端可以生成 TreeMap 作为图像。

是否存在这样的东西,我可以从服务器端 C# 应用程序将树状图创建为图像。

最佳答案

鉴于算法是已知的,用 TreeMap 绘制位图并不难。目前我没有足够的时间自己编写代码,但我有足够的时间(几乎)不假思索地将一些现有代码移植到 C# :) 让我们来看看 this javascript 实现。它使用 this 中描述的算法纸。我在该实现中发现了一些问题,这些问题已在 C# 版本中修复。 Javascript 版本适用于纯整数数组(以及数组的数组)。我们改为定义一些类:

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; }
}

然后开始移植。第一个容器类:

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);
        }
    }
}

然后 Treemap 类构建实际的 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);
    }
}

现在让我们尝试使用看看效果如何:

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);
}

输出:Result

这可以通过多种方式进行扩展,并且代码质量当然可以显着提高。但如果你愿意走这条路,它至少可以给你一个好的开始。好处是速度快,不涉及外部依赖。 如果您想使用它并发现一些问题或它不符合您的某些要求 - 请随时提问,我会在有更多时间时改进它。

关于c# - 从 C# 服务器端,是否有生成 TreeMap 并另存为图像的方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32548949/

相关文章:

c# - 带有 IIS Express 的 VS2017 未显示最新代码更改

c# - 人脸、情感和语音识别

python - 来自字节的图像(python)

ios - 在 Xcode Swift 中从 Assets.xcassets 中的文件夹访问图像

asp.net-mvc - MVC 5 Html helper CheckBoxFor无法在Razor部分中选择

javascript - 基于变量加载图像

c# - IdenityServer 和每个租户策略实现数据库

c# - 在 SQL 2005/2008 中转义双引号

asp.net-mvc - 进程标识设置为用户的 IIS 7.5 有错误的 USERPROFILE

asp.net-mvc - ASP.NET MVC Web API 和 StructureMap : "does not have a default constructor"