考虑到已知算法,使用树状图很容易就能绘制一个位图。目前我没有足够的时间编写代码,但我有足够的时间(几乎)毫无思考地将一些现有代码移植到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);
}
这可以有多种扩展方式,并且代码质量肯定可以得到显著提高。但是,如果您选择这种方式,它至少可以为您提供一个良好的开端。好处在于它快速且没有外部依赖关系。 如果您想使用它并发现一些问题或它不符合您的某些要求,请随时询问,我将在有更多时间时对其进行改进。
https://github.com/imazen/Graphics-vNext
说到这一点,有一篇文章涉及你所问的内容:
在GDI+递归绘制矩形时出现OutOfMemory异常(它特别讨论了使用GDI+生成TreeMap,如果您阅读评论和答案,将避免许多陷阱)
生成图像后,将其保存到磁盘上并在演示文稿中嵌入它是一个微不足道的过程;您还可以选择写入流,因此可能可以直接将其嵌入PowerPoint文件而无需首先将其保存到磁盘。
荷兰埃因霍温理工大学发表了一篇关于方形树状图算法的论文a paper。Pascal Laurin将其转换为C#语言。还有一篇Code Project article文章,其中包含关于树状图的部分。
当然还有商业解决方案,比如来自.NET Charting, Infragistics或Telerik的解决方案。它们的缺点可能是它们被设计为需要绘制的控件,因此您可能需要某种UI线程。由于你只需要提取网页的截图,因此将网页捕获为图像会更方便。
这个免费库能够从您的网页中提取截图,并支持Javascript / CSS。
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; }
}
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);
以下是生成的图像:
实际上,我不知道以下内容是否对您有所帮助,因为您没有使用 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图表:
您可以使用第一部分(Excel部分)生成treemap,并使用您所说的工具粘贴图表,或者使用在VSTO中生成的图表保存Powerpoint文件并使用该工具打开。
好处是这些对象是真实的图表而不仅仅是图像,因此您可以轻松更改或添加颜色、样式和效果。
WinForms
中的WebBrowser
控件(不显示窗体),打开由JavaScript生成的图像,然后使用Control.DrawToBitmap
方法获取您的图像示例:https://dev59.com/u1PTa4cB1Zd3GeqPiVdj - Fabjan