Подтвердить что ты не робот

С С# serverside, Есть ли в любом случае генерировать treemap и сохранять как изображение?

Я использую эту javascript-библиотеку для создания treemap на веб-страницах, и он отлично работает. Проблема в том, что мне нужно включить это в презентацию PowerPoint, которую я создаю на стороне сервера (я генерирую powerpoint с помощью aspose.slides для .net)

Самое легкое, о чем я думал, это попытаться каким-то образом создать treemap на сервере и сохранить как изображение (так как добавление изображения в презентацию PowerPoint довольно простое), но после googling я не вижу никакого решения, которое от С# serverside может генерировать treemap как изображение.

Есть ли что-то вроде этого, где я могу создать treemap как изображение из приложения С# на стороне сервера.

4b9b3361

Ответ 1

Я основываю следующее решение в этом проекте о treemaps в 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, treemaps включаются в виде диаграмм. Вы можете прочитать этот, чтобы увидеть, как создавать treemaps из наборов данных в 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();

Диаграмма карт в слайде:

office treemap

Вероятно, вы можете создать treemap, используя первую часть (часть Excel), и вставить диаграмму с помощью инструмента, который вы сказали, или сохранить файл Powerpoint с диаграммой, сгенерированной в VSTO, и открыть ее с помощью инструмента.

Преимущества заключаются в том, что эти объекты являются реальными диаграммами, а не только изображениями, поэтому вы можете легко изменять или добавлять цвета, стили и эффекты.

Ответ 2

Учитывая, что алгоритмы известны, нетрудно просто нарисовать растровое изображение с treemap. На данный момент у меня недостаточно времени для написания кода, но у меня достаточно времени (почти) бездумно переносить какой-то существующий код на С#:). Возьмем this javascript. Он использует алгоритм, описанный в этот документ. Я нашел некоторые проблемы в этой реализации, которые исправлены в версии С#. Версия 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 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);
}

Выход: Результат

Это можно расширить несколькими способами, и качество кода, безусловно, может быть значительно улучшено. Но если вы пойдете так, это может по крайней мере дать вам хорошее начало. Преимущество заключается в том, что он быстро и без внешних зависимостей. Если вы хотите использовать его и найти некоторые проблемы или не соответствуют вашим требованиям, не стесняйтесь спрашивать, и я улучшу его, когда у вас будет больше времени.

Ответ 3

Использование GDI + api может быть вашим единственным выбором, с хорошей поддержкой кросс-платформы. Однако есть несколько потенциальных проблем, о которых вам нужно знать, когда делаете что-либо с GDI + на стороне сервера. Стоит прочитать это, поскольку он объясняет текущее состояние рисования графики в DotNet и имеет смысл при обработке на стороне сервера:

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

Сказав это; там эта статья, которая касается того, что вы просите:

Исключение OutOfMemory при рекурсивном рисовании прямоугольников в GDI + (в нем конкретно говорится о создании TreeMap с GDI +, и если вы прочтете комментарии и ответ, вы избежите много подводные камни)

Как только вы сгенерировали свой образ, это тривиальный процесс, чтобы сохранить его на диске где-то и, надеюсь, встроить его в вашу презентацию; у вас есть варианты для записи в потоки, так что может быть возможно напрямую внедрить его в файл PowerPoint без сохранения его на диск.

Ответ 4

Вы можете использовать рендеринг WPF: http://lordzoltan.blogspot.co.uk/2010/09/using-wpf-to-render-bitmaps.html, но это не без его недостатков.

(Это ссылка на мой собственный старый блог - но если вы ищете "использование wpf для создания образов", вы получите много других примеров - многие из которых лучше моих!)

Генерация дерева в WPF будет, хотя и сложной, хотя, хотя это может быть сделано, поскольку примитивы рисования WPF носят иерархический характер.

Возможно, это не подходит, bou также может рассмотреть GraphViz - https://github.com/JamieDixon/GraphViz-C-Sharp-Wrapper, однако я не знаю, сколько удач получится выполнение командной строки на веб-сервере.

Есть - я ожидаю, что они будут оплачиваться - для библиотек, чтобы сделать это, а также, как это обычно необходимо.

Ответ 5

Эйндховенский технологический университет опубликовал статью об алгоритме квадратичных treemaps. Паскаль Лорин превратил это в С#. Существует также статья проекта кода, в которой есть раздел о treemaps.

Конечно, существуют также коммерческие решения, такие как .NET Charting, Infragistics или Telerik. Недостатком этих, вероятно, является то, что они разработаны как элементы управления, которые необходимо нарисовать, поэтому вам может понадобиться какой-то поток пользовательского интерфейса.

Существует также вопрос здесь о переполнении стека, который уже запрашивал реализацию treemap в С#. На всякий случай вы не помните.

Ответ 6

Поскольку вы уже генерируете JS и HTML-версию всего, вы можете проверить:

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

Я использую это, чтобы генерировать отчеты с высокой разрешающей способностью прямо из моих сгенерированных страниц. Он использовал WKHTML для рендеринга, и вы можете передать тонну параметров, чтобы действительно точно настроить его. Он свободен для большинства вещей, и он отлично работает. Многопоточность - это боль в прикладе, но я не сталкивался со многими проблемами. Если вы используете библиотеку NRECO PDf, вы можете даже делать партии.

С этим все, что вам нужно сделать, это отобразить страницу, как вы уже есть, пропустить ее через библиотеку и вставить в свой PPT, и все должно быть хорошо.