从C#服务器端,有没有办法生成树状图并保存为图像?

23
我一直在使用这个JavaScript库在网页上创建树状图,效果非常好。现在的问题是,我需要将其包含在由服务器生成的PowerPoint演示文稿中(我正在使用aspose.slides for .net生成PowerPoint)。我想到的最简单的方法是尝试在服务器上构建树状图并保存为图像(因为将图像添加到PowerPoint演示文稿非常简单),但是在搜索后,我没有看到任何解决方案可以从C#服务器端生成树状图作为图像。是否存在这样的东西,可以从服务器端的C#应用程序创建树状图作为图像。

这个链接可能对你有帮助:http://pascallaurin42.blogspot.co.il/2013/12/implementing-treemap-in-c.html。我是说,看起来他正在创建一个树状图的位图。 - Eugene Krapivin
为什么不将你的JavaScript树状图导出为图片呢? - Triet Doan
嗯...在花费了很多时间寻找从C#代码创建树状图的方法后,我认为这是不可能的 :( 我认为你应该使用你的JavaScript库来创建它,然后将其转换为图像。我可以将你使用库生成的图形转换为图像。 - Triet Doan
1
一个想法是使用WinForms中的WebBrowser控件(不显示窗体),打开由JavaScript生成的图像,然后使用Control.DrawToBitmap方法获取您的图像示例:https://dev59.com/u1PTa4cB1Zd3GeqPiVdj - Fabjan
1
你尝试过phantom.js吗:http://phantomjs.org/ - Simon Mourier
显示剩余3条评论
7个回答

14

考虑到已知算法,使用树状图很容易就能绘制一个位图。目前我没有足够的时间编写代码,但我有足够的时间(几乎)毫无思考地将一些现有代码移植到C#中:) 我们可以使用这个javascript实现。它使用这篇论文中描述的算法。我在那个实现中找到了一些问题,在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类。

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

这可以有多种扩展方式,并且代码质量肯定可以得到显著提高。但是,如果您选择这种方式,它至少可以为您提供一个良好的开端。好处在于它快速且没有外部依赖关系。 如果您想使用它并发现一些问题或它不符合您的某些要求,请随时询问,我将在有更多时间时对其进行改进。


这非常酷。有一件事我想不通,就是当一个父节点有多个子节点时如何显示其标签。算法中使用父节点对项进行分组,但没有在任何地方可视化父容器名称。这种方法是否可行? - leora
@leora 是的,这是完全可能的。我已经更新了答案,包括具有子项的20像素标题。此外,我还修复了代码中的几个重要错误(这些错误也存在于那个JavaScript库中 - 我真的不确定它如何工作),所以完全丢弃旧版本并使用新版本。 - Evk
非常感谢您在这方面的努力。我会尽快查看并让您知道是否遇到任何问题。 - leora

7
使用GDI+ API可能是您唯一的选择,它具有跨平台的良好支持。但是,在服务器端执行任何与GDI+有关的操作时,您需要注意几个潜在问题。阅读此文档很值得,因为它解释了DotNet中绘制图形的当前状态,并指出了服务器端处理的重点。

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

说到这一点,有一篇文章涉及你所问的内容:

在GDI+递归绘制矩形时出现OutOfMemory异常(它特别讨论了使用GDI+生成TreeMap,如果您阅读评论和答案,将避免许多陷阱)

生成图像后,将其保存到磁盘上并在演示文稿中嵌入它是一个微不足道的过程;您还可以选择写入流,因此可能可以直接将其嵌入PowerPoint文件而无需首先将其保存到磁盘。


我认为在服务器上使用MS Office是一个坏主意,GDI+更糟糕。OP想要能够抓取树状图然后将其推入PowerPoint或PDF中。如果您同时启动10个以上用户的此解决方案,则会看到8核Xeon突然停止运行。 - Kevin B Burns

4
您可以使用WPF渲染:http://lordzoltan.blogspot.co.uk/2010/09/using-wpf-to-render-bitmaps.html,但它也有缺点。
(这是链接到我自己旧博客的链接-但如果您搜索“使用wpf生成图像”,您将得到许多其他示例-其中许多比我的更好!)
在WPF中生成树可能会很具有挑战性,因为WPF绘图基元具有分层结构。
您还可以考虑GraphViz-https://github.com/JamieDixon/GraphViz-C-Sharp-Wrapper,但我不知道在Web服务器上执行命令行会有多少运气。
还有付费库可以做到这一点,因为这是一个常见的需求。

我正在使用 asp.net Mac。 - leora

4

荷兰埃因霍温理工大学发表了一篇关于方形树状图算法的论文a paperPascal Laurin将其转换为C#语言。还有一篇Code Project article文章,其中包含关于树状图的部分。

当然还有商业解决方案,比如来自.NET Charting, InfragisticsTelerik的解决方案。它们的缺点可能是它们被设计为需要绘制的控件,因此您可能需要某种UI线程。
这里还有一个关于在C#中实现树状图的问题Stack Overflow上已经提出。以防您不记得。

4
既然您已经生成了JS和HTML版本的网页,您可能想要尝试的一件事是:http://www.nrecosite.com/html_to_image_generator_net.aspx。我使用这个工具可以直接从我的生成页面中生成高分辨率图形报告。它使用WKHTML进行渲染,并且您可以传递大量参数来对其进行微调。它对大多数东西免费,并且运行良好。使用它时多线程有点麻烦,但我还没有遇到太多问题。如果您使用NRECO PDf库,您甚至可以批处理一些东西。使用这个工具,您只需要像现在一样渲染页面,通过库将其传输并插入到您的PPT中,所有的问题都会解决。

4

由于你只需要提取网页的截图,因此将网页捕获为图像会更方便。

这个免费库能够从您的网页中提取截图,并支持Javascript / CSS。


我已经添加了相同的链接,OP已经在做大部分工作,为什么不只是截图然后继续呢? - Kevin B Burns

2
我将基于这个关于WPF中树状图的项目提供以下解决方案。
使用您链接中的数据,您可以定义模型(仅包含必要数据),如下所示:
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;
    }
}

在解决方案中添加一个额外的属性TreeMapData,其中包含一些值:

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

现在,定义一个具有以下公共成员的TreeMap类:
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
}

所以,你可以像这样使用它:
var treeMap = new TreeMap(items);
var bmp = treeMap.Draw(1366, 768);

还有私有/助手成员:

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
}

最后,为了解析和绘制我正在处理的json示例,我要这样做:

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

以下是生成的图像:

treemap


实际上,我不知道以下内容是否对您有所帮助,因为您没有使用 vsto,而且正如评论中所说,这可能是一个坏主意。

Office 2016 开始,树状图被作为图表纳入其中。您可以阅读此文档以了解如何从 Excel 数据集创建树状图。

因此,您可以在 Excel 中生成图表,然后将其传递给 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图表:

office treemap

您可以使用第一部分(Excel部分)生成treemap,并使用您所说的工具粘贴图表,或者使用在VSTO中生成的图表保存Powerpoint文件并使用该工具打开。

好处是这些对象是真实的图表而不仅仅是图像,因此您可以轻松更改或添加颜色、样式和效果。


不要在服务器上自动化处理Office文件 - KB257757,除此之外是一个好答案。 - Jeremy Thompson
@Arturo - 这真的很酷,但我有一个问题。当我测试它时,我没有看到任何显示“父节点”下的所有子节点的地方(我本以为它会将父级标签名称显示为子节点周围的标签)。它似乎只显示最低级别的节点。这个实现能做到吗? - leora
@ArturoMenchaca,我真的很喜欢你提供的解决方案,但它并不适用于所问的问题。 OP明确要求服务器端的解决方案。我不想引发任何争端,但我只是想确保其他人在未来不会偶然发现这个答案,并认为这是服务器端应用程序的合适答案。 - Kevin B Burns
@leora:在您提供的第一个链接中,有一个示例展示了树状图,您可以访问此链接:http://philogb.github.io/jit/static/v20/Jit/Examples/Treemap/example1.js 查看 JavaScript 代码,在其中有一个描述树状图的 JSON 变量,我将其复制到一个json文件中。关于父项,是的,我忘记显示父项信息了,正在更新答案。颜色来自json文件,有一个名为$color的属性及其值,如果您想要,我可以尝试自动生成颜色。 - Arturo Menchaca
@KevinBBurns:同意,我会更新答案。 - Arturo Menchaca
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接