С С# serverside, Есть ли в любом случае генерировать treemap и сохранять как изображение?
Я использую эту javascript-библиотеку для создания treemap на веб-страницах, и он отлично работает. Проблема в том, что мне нужно включить это в презентацию PowerPoint, которую я создаю на стороне сервера (я генерирую powerpoint с помощью aspose.slides для .net)
Самое легкое, о чем я думал, это попытаться каким-то образом создать treemap на сервере и сохранить как изображение (так как добавление изображения в презентацию PowerPoint довольно простое), но после googling я не вижу никакого решения, которое от С# serverside может генерировать treemap как изображение.
Есть ли что-то вроде этого, где я могу создать treemap как изображение из приложения С# на стороне сервера.
Ответы
Ответ 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, и все должно быть хорошо.
Ответ 7
Поскольку вам нужно всего лишь извлечь скриншот веб-страницы, было бы удобнее захватить веб-страницу как изображение.
Эта бесплатная библиотека способна извлекать снимок экрана с вашей веб-страницы и поддерживает Javascript/CSS.