Skip to content

Commit c0a20e1

Browse files
authored
Merge pull request #273 from xieguigang/refactor-data-viz
Refactor data viz
2 parents 1c739d3 + 611f083 commit c0a20e1

73 files changed

Lines changed: 4624 additions & 1320 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
---
2+
name: 新增9种GDI+图表Class模块
3+
overview: 在现有 DataPlot 框架基础上,遵循 PlotEngine 基类约定,新增 9 个图表绘制类:基础面积图(带AUC)、堆叠面积图、堆叠柱状图、南丁格尔玫瑰图、jitter散点图、气泡图、雷达图、矩形树图、和弦图。每个类继承 PlotEngine,复用坐标变换/坐标轴/图例/主题系统,仅新增各自的几何布局与绘制逻辑。
4+
todos:
5+
- id: area-charts
6+
content: 实现 AreaPlot(基础面积图,Catmull-Rom 平滑 + FillPolygon)和 StackedAreaPlot(堆叠面积图,逐列累加填充)于 Basic/ 目录
7+
status: completed
8+
- id: stacked-bar
9+
content: 实现 StackedBarPlot(堆叠柱状图)于 Basic/ 目录,复用 BarPlot 数据结构改为累加堆叠
10+
status: completed
11+
- id: polar-charts
12+
content: 实现 RosePlot(南丁格尔玫瑰图,等角度扇区变半径 FillPie)和 RadarPlot(雷达图,辐射轴 + FillPolygon 多系列叠加)于 Advanced/ 目录
13+
status: completed
14+
- id: scatter-variants
15+
content: 实现 JitterPlot(分组随机抖动散点图)和 BubblePlot(气泡图,标记半径映射第三维)于 Advanced/ 目录
16+
status: completed
17+
- id: treemap
18+
content: 实现 TreemapPlot(矩形树图,Squarified 布局算法递归切分)于 Advanced/ 目录,含 TreemapNode 辅助类
19+
status: completed
20+
- id: chord
21+
content: 实现 ChordPlot(和弦图,圆周节点弧段 + GraphicsPath 贝塞尔和弦)于 Advanced/ 目录,含 ChordLink 辅助类
22+
status: completed
23+
---
24+
25+
## 产品概述
26+
27+
在现有 VB.NET GDI+ 数据绘图框架(DataPlot)基础上,新增 9 种图表绘制 Class 模块,扩展框架的图表类型覆盖范围。所有新模块复用现有 PlotEngine 基类、PlotTheme 主题系统、Series 数据系列及 Extensions 工具方法。
28+
29+
## 核心功能
30+
31+
- **基础面积图(AreaPlot)**:在折线下方填充半透明颜色,支持多系列重叠,可选平滑曲线
32+
- **堆叠面积图(StackedAreaPlot)**:多系列沿 Y 轴累加堆叠,各层颜色区分,展示各部分对整体贡献
33+
- **堆叠柱状图(StackedBarPlot)**:多系列柱体沿 Y 轴堆叠(而非分组),支持横向/纵向、数值标签
34+
- **南丁格尔玫瑰图(RosePlot)**:极坐标图,每个类别等角度扇区,半径与数值成比例
35+
- **Jitter 散点图(JitterPlot)**:分组散点图,同组数据点在 X 轴方向添加随机抖动以避免遮挡
36+
- **气泡图(BubblePlot)**:三维散点图,X/Y 定位,气泡大小映射第三维数据
37+
- **雷达图(RadarPlot)**:多维度数据以蛛网图展示,多系列叠加填充对比
38+
- **矩形树图(TreemapPlot)**:层次数据以嵌套矩形展示,使用 Squarified 布局算法优化宽高比
39+
- **和弦图(ChordPlot)**:邻接矩阵数据以圆形布局展示,节点间用贝塞尔曲线连接,粗细映射关系强度
40+
41+
## 技术栈
42+
43+
- 语言:VB.NET(.NET 10.0)
44+
- 绘图:GDI+(通过 `Microsoft.VisualBasic.Imaging``IGraphics` 接口)
45+
- 基类:`PlotEngine`(提供画布管理、坐标变换 `ToPixelX/ToPixelY`、轴/网格绘制 `DrawAxisAndGrid`、图例 `DrawLegend`、标记 `DrawMarker`、刻度生成 `GenerateTicks`
46+
- 主题:`PlotTheme`(Palette 10 色调色板、字体、线宽、边距等)
47+
- 项目结构:SDK 风格 `<Project Sdk="Microsoft.NET.Sdk">`,glob 自动包含 .vb 文件,无需手动注册
48+
49+
## 实现方案
50+
51+
### 整体策略
52+
53+
所有 9 个图表 Class 均 `Inherits PlotEngine`,遵循现有代码模式:构造函数 `New(width, height, Optional theme)``Plot()` 方法内依次调用 `DrawBackground``ComputePlotArea`/`DrawPlotArea``DrawTitle``DrawAxisAndGrid`(如需)→ 绘制数据 → `DrawLegend`(如需)。GDI 资源(Brush/Pen/GraphicsPath)均使用 `Using` 块确保释放。
54+
55+
### 各图表关键技术决策
56+
57+
**1. AreaPlot & StackedAreaPlot(面积图系列)**
58+
59+
- AreaPlot 复用 `IList(Of Series)` 模式(同 ScatterPlot),对每条线计算多边形顶点 = 线上点 + 基线点(Y=0 处),用 `FillPolygon` 半透明填充 + `DrawLines` 描边
60+
- 平滑曲线采用 Catmull-Rom 样条插值:将原始控制点采样为密集折线点(每段 ~20 点),再 `FillPolygon`/`DrawLines`,仅依赖已确认可用的 API
61+
- StackedAreaPlot 使用 `Categories` + `MultiValues[系列, 类别]` + `SeriesNames`(同 BarPlot 的多维数组模式),逐列累加得到各层上界,用 `FillPolygon` 填充相邻层之间的区域
62+
63+
**2. StackedBarPlot(堆叠柱状图)**
64+
65+
- 基于 BarPlot 的数据结构(`Categories``MultiValues[系列,类别]``SeriesNames``Horizontal``ShowValueLabels`),核心差异:同一类别内各系列柱体沿 Y 轴累加堆叠而非并排
66+
- Y 轴最大值 = 各类别跨系列累加和的最大值 × 1.1
67+
68+
**3. RosePlot & RadarPlot(极坐标系列)**
69+
70+
- RosePlot 复用 PiePlot 的极坐标模式:每个类别等角度扇区(`sweep = 360/n`),用 `FillPie` 绘制,椭圆尺寸的半径 = `value/maxValue × maxRadius`,扇区半径不同
71+
- RadarPlot 自定义布局:圆心 + n 条辐射轴,每轴上数据点距圆心 = `value/maxValue × radius`;多系列用 `FillPolygon`(半透明)+ `DrawPolygon`(描边)叠加;同心多边形网格 + 轴线 + 轴标签
72+
73+
**4. JitterPlot & BubblePlot(散点变体系列)**
74+
75+
- JitterPlot 复用 BoxPlot 的分组模式(`Groups: List(Of {Name, Data()})`),每个数据点 X = 组中心 + 随机偏移(`jitter = groupWidth × 0.25`),Y = 数据值,用 `DrawMarker` 绘制
76+
- BubblePlot 复用 ScatterPlot 的 `IList(Of Series)` 模式,但 Series 扩展第三维 `Size`(通过自定义 `BubbleSeries` 或在 Plot 方法接收 `Sizes` 数组),标记半径 = `sizeNorm × Theme.MarkerSize × scale`
77+
78+
**5. TreemapPlot(矩形树图)**
79+
80+
- 输入:扁平节点列表 `TreemapNode {Label, Value, Color?}`(同 SankeyNode 模式,辅助类同文件定义)
81+
- Squarified 算法:递归将矩形按面积比例切分,优化子矩形宽高比接近 1:1
82+
- 无标准坐标轴,自定义 `_plotArea` 后全画布填充
83+
84+
**6. ChordPlot(和弦图)**
85+
86+
- 输入:`NodeLabels: String()` + `Matrix: Double(,)`(n×n 邻接矩阵)
87+
- 节点排列在圆周上,各节点占弧段比例 = 该节点总流量 / 全局总流量
88+
- 弧段用 `FillPie` 绘制节点色带;和弦用 `GraphicsPath` + `AddBezier`(同 SankeyPlot 已验证模式)穿过圆心附近连接两端
89+
- 节点标签沿径向外侧排列
90+
91+
### 性能考量
92+
93+
- Catmull-Rom 插值每段采样 ~20 点,典型 100 点数据线产生 ~2000 顶点的多边形,GDI+ FillPolygon 可轻松处理
94+
- TreemapPlot Squarified 算法复杂度 O(n log n)(排序)+ O(n)(递归切分),对典型几百节点无压力
95+
- ChordPlot 和弦数量 = n²(矩阵元素数),n>50 时考虑仅绘制非零元素;贝塞尔路径填充约 O(n²) 次绘制调用
96+
- 所有图表的 GDI 资源均 `Using` 包裹,避免泄漏
97+
98+
## 实现注意事项
99+
100+
- **导入约定**:参照 SankeyPlot/BoxPlot 使用别名导入(`Imports SolidBrush = Microsoft.VisualBasic.Imaging.SolidBrush` 等),确保 `GraphicsPath``DashStyle` 等类型解析正确
101+
- **坐标变换**:极坐标图表(Rose/Radar/Chord)不调用 `DrawAxisAndGrid`,自定义布局后直接绘制;笛卡尔图表复用基类坐标轴
102+
- **图例**:多系列图表(AreaPlot/StackedAreaPlot/StackedBarPlot/RadarPlot/BubblePlot)调用 `DrawLegend(seriesList)`,单类别图表(Rose/Treemap/Jitter)按需绘制
103+
- **颜色透明度**:面积/雷达填充使用 `Color.FromArgb(120, color)` 半透明,避免多系列遮挡
104+
- **向后兼容**:不修改任何现有文件,仅新增 .vb 文件;SDK 项目 glob 自动包含,无需编辑 .vbproj
105+
- **随机抖动**:JitterPlot 使用固定随机种子(`New Random(42)`)确保可复现,或暴露 `RandomSeed` 属性
106+
107+
## 架构设计
108+
109+
```mermaid
110+
graph TD
111+
PlotEngine["PlotEngine (基类)"]
112+
113+
subgraph Basic["Basic/"]
114+
AreaPlot["AreaPlot"]
115+
StackedAreaPlot["StackedAreaPlot"]
116+
StackedBarPlot["StackedBarPlot"]
117+
end
118+
119+
subgraph Advanced["Advanced/"]
120+
RosePlot["RosePlot"]
121+
JitterPlot["JitterPlot"]
122+
BubblePlot["BubblePlot"]
123+
RadarPlot["RadarPlot"]
124+
TreemapPlot["TreemapPlot"]
125+
ChordPlot["ChordPlot"]
126+
end
127+
128+
PlotEngine --> AreaPlot
129+
PlotEngine --> StackedAreaPlot
130+
PlotEngine --> StackedBarPlot
131+
PlotEngine --> RosePlot
132+
PlotEngine --> JitterPlot
133+
PlotEngine --> BubblePlot
134+
PlotEngine --> RadarPlot
135+
PlotEngine --> TreemapPlot
136+
PlotEngine --> ChordPlot
137+
138+
PlotTheme["PlotTheme (主题)"]
139+
Series["Series (数据系列)"]
140+
PlotEngine -.-> PlotTheme
141+
PlotEngine -.-> Series
142+
```
143+
144+
## 目录结构
145+
146+
```
147+
DataPlot/
148+
├── Basic/
149+
│ ├── BarPlot.vb # [现有] 无修改
150+
│ ├── LinePlot.vb # [现有] 无修改
151+
│ ├── ScatterPlot.vb # [现有] 无修改
152+
│ ├── HistogramPlot.vb # [现有] 无修改
153+
│ ├── AreaPlot.vb # [新增] 基础面积图。接收 IList(Of Series),Catmull-Rom 平滑插值,FillPolygon 半透明填充 + DrawLines 描边。支持多系列、可选平滑开关。
154+
│ ├── StackedAreaPlot.vb # [新增] 堆叠面积图。Categories + MultiValues[系列,类别] + SeriesNames,逐列累加,FillPolygon 填充相邻累积层间区域。
155+
│ └── StackedBarPlot.vb # [新增] 堆叠柱状图。复用 BarPlot 数据结构,同类别各系列沿 Y 轴累加堆叠,支持 Horizontal/ShowValueLabels。
156+
├── Advanced/
157+
│ ├── PiePlot.vb # [现有] 无修改
158+
│ ├── HeatmapPlot.vb # [现有] 无修改
159+
│ ├── BoxPlot.vb # [现有] 无修改
160+
│ ├── ViolinPlot.vb # [现有] 无修改
161+
│ ├── BoxGroup.vb # [现有] 无修改
162+
│ ├── SankeyPlot/ # [现有] 无修改
163+
│ ├── RosePlot.vb # [新增] 南丁格尔玫瑰图。Labels + Values,等角度扇区 FillPie,半径与值成比例,支持 Donut 模式、标签百分比。
164+
│ ├── JitterPlot.vb # [新增] Jitter 散点图。Groups(List of {Name, Data()}),组内点 X 加随机抖动,DrawMarker 绘制,可复现随机种子。
165+
│ ├── BubblePlot.vb # [新增] 气泡图。IList(Of BubbleSeries)(含 X/Y/Sizes),标记半径映射第三维,可选大小图例。
166+
│ ├── RadarPlot.vb # [新增] 雷达图。Categories + MultiValues[系列,类别],辐射轴 + 同心网格,FillPolygon 半透明叠加多系列。
167+
│ ├── TreemapPlot.vb # [新增] 矩形树图。List(Of TreemapNode),Squarified 布局算法递归切分,同文件定义 TreemapNode 辅助类。
168+
│ └── ChordPlot.vb # [新增] 和弦图。NodeLabels + Matrix(n,n),圆周节点弧段 + GraphicsPath 贝塞尔和弦,同文件定义 ChordLink 辅助类。
169+
├── Engine/ # [现有] 无修改
170+
├── Extensions.vb # [现有] 无修改
171+
└── DataPlot.vbproj # [现有] 无修改(SDK glob 自动包含新文件)
172+
```
173+
174+
## 关键代码结构
175+
176+
### BubbleSeries(气泡图数据系列,继承 Series 扩展第三维)
177+
178+
```
179+
''' <summary>气泡图数据系列</summary>
180+
Public Class BubbleSeries
181+
Inherits Series
182+
''' <summary>气泡大小(第三维数据)</summary>
183+
Public Property Sizes As Double() = {}
184+
End Class
185+
```
186+
187+
### TreemapNode(矩形树图节点)
188+
189+
```
190+
''' <summary>矩形树图节点</summary>
191+
Public Class TreemapNode
192+
Public Property Label As String
193+
Public Property Value As Double
194+
Public Property Color As Color? = Nothing
195+
Public Property Group As String = ""
196+
End Class
197+
```
198+
199+
### ChordLink(和弦图连接,可选辅助结构)
200+
201+
```
202+
''' <summary>和弦图连接</summary>
203+
Public Class ChordLink
204+
Public Property Source As Integer
205+
Public Property Target As Integer
206+
Public Property Value As Double
207+
End Class
208+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
Imports System.Drawing
3+
4+
''' <summary>盒须图分组</summary>
5+
Public Class BoxGroup
6+
Public Property Name As String = ""
7+
Public Property Data As Double() = {}
8+
Public Property Color As Color? = Nothing
9+
End Class
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
2+
Imports Microsoft.VisualBasic.Imaging
3+
4+
''' <summary>盒须图(Box Plot / Box-and-Whisker)</summary>
5+
Public Class BoxPlot
6+
Inherits PlotEngine
7+
8+
Public Property Groups As New List(Of BoxGroup)()
9+
Public Property Horizontal As Boolean = False
10+
Public Property ShowOutliers As Boolean = True
11+
Public Property ShowMean As Boolean = True
12+
13+
Public Sub New(width As Integer, height As Integer, Optional theme As PlotTheme = Nothing)
14+
MyBase.New(width, height, theme)
15+
End Sub
16+
17+
Public Sub Plot()
18+
DrawBackground()
19+
ComputePlotArea()
20+
DrawPlotArea()
21+
DrawTitle()
22+
23+
Dim nGrp = Groups.Count
24+
' 计算全局 Y 范围
25+
Dim allData = Groups.SelectMany(Function(g) g.Data).ToList()
26+
Dim ymin = If(Me.YMin, allData.Min())
27+
Dim ymax = If(Me.YMax, allData.Max())
28+
Dim pad = (ymax - ymin) * 0.05
29+
ymin -= pad : ymax += pad
30+
31+
Dim xTicks = Enumerable.Range(0, nGrp).Select(Function(i) CDbl(i)).ToArray()
32+
Dim yTicks = GenerateTicks(ymin, ymax)
33+
If Horizontal Then
34+
DrawAxisAndGrid(ymin, ymax, -0.5, nGrp - 0.5, yTicks, xTicks, Nothing, Groups.Select(Function(g) g.Name).ToArray())
35+
Else
36+
DrawAxisAndGrid(-0.5, nGrp - 0.5, ymin, ymax, xTicks, yTicks, Groups.Select(Function(g) g.Name).ToArray(), Nothing)
37+
End If
38+
39+
Dim groupWidth = If(Horizontal, _plotArea.Height / nGrp, _plotArea.Width / nGrp)
40+
Dim boxWidth = groupWidth * 0.5
41+
42+
For i = 0 To nGrp - 1
43+
Dim g = Groups(i)
44+
Dim color = If(g.Color, Theme.Palette(i Mod Theme.Palette.Length))
45+
Dim sorted = g.Data.OrderBy(Function(v) v).ToArray()
46+
Dim q1 = Quantile(sorted, 0.25)
47+
Dim q2 = Quantile(sorted, 0.5)
48+
Dim q3 = Quantile(sorted, 0.75)
49+
Dim iqr = q3 - q1
50+
Dim wLo = q1 - 1.5 * iqr
51+
Dim wHi = q3 + 1.5 * iqr
52+
Dim outliers = sorted.Where(Function(v) v < wLo OrElse v > wHi).ToList()
53+
Dim whiskerLo = sorted.Where(Function(v) v >= wLo).Min()
54+
Dim whiskerHi = sorted.Where(Function(v) v <= wHi).Max()
55+
56+
Dim center As Single = If(Horizontal,
57+
_plotArea.Top + (i + 0.5) * groupWidth,
58+
_plotArea.Left + (i + 0.5) * groupWidth)
59+
60+
If Horizontal Then
61+
' 横向:X 是数值
62+
Dim pxQ1 = ToPixelX(q1, ymin, ymax)
63+
Dim pxQ2 = ToPixelX(q2, ymin, ymax)
64+
Dim pxQ3 = ToPixelX(q3, ymin, ymax)
65+
Dim pxWLo = ToPixelX(whiskerLo, ymin, ymax)
66+
Dim pxWHi = ToPixelX(whiskerHi, ymin, ymax)
67+
Dim by0 As Single = center - boxWidth / 2
68+
Dim by1 As Single = center + boxWidth / 2
69+
' 盒子
70+
Using br As New SolidBrush(color.FromArgb(180, color)),
71+
pen As New Pen(color, Theme.LineWidth)
72+
_g.FillRectangle(br, pxQ1, by0, pxQ3 - pxQ1, by1 - by0)
73+
_g.DrawRectangle(pen, pxQ1, by0, pxQ3 - pxQ1, by1 - by0)
74+
' 中位数线
75+
_g.DrawLine(pen, pxQ2, by0, pxQ2, by1)
76+
' 须
77+
_g.DrawLine(pen, pxQ1, center, pxWLo, center)
78+
_g.DrawLine(pen, pxQ3, center, pxWHi, center)
79+
_g.DrawLine(pen, pxWLo, by0 + 4, pxWLo, by1 - 4)
80+
_g.DrawLine(pen, pxWHi, by0 + 4, pxWHi, by1 - 4)
81+
' 均值
82+
If ShowMean Then
83+
Dim mean = g.Data.Average()
84+
Dim pxM = ToPixelX(mean, ymin, ymax)
85+
Using dsh As New Pen(color, Theme.LineWidth)
86+
dsh.DashStyle = DashStyle.Dot
87+
_g.DrawLine(dsh, pxM, CSng(by0), pxM, CSng(by1))
88+
End Using
89+
End If
90+
End Using
91+
' 异常值
92+
If ShowOutliers Then
93+
For Each o In outliers
94+
Dim px = ToPixelX(o, ymin, ymax)
95+
DrawMarker(px, center, MarkerShape.Circle, 4, color)
96+
Next
97+
End If
98+
Else
99+
' 纵向:Y 是数值
100+
Dim pyQ1 = ToPixelY(q1, ymin, ymax)
101+
Dim pyQ2 = ToPixelY(q2, ymin, ymax)
102+
Dim pyQ3 = ToPixelY(q3, ymin, ymax)
103+
Dim pyWLo = ToPixelY(whiskerLo, ymin, ymax)
104+
Dim pyWHi = ToPixelY(whiskerHi, ymin, ymax)
105+
Dim bx0 = center - boxWidth / 2
106+
Dim bx1 = center + boxWidth / 2
107+
Using br As New SolidBrush(color.FromArgb(180, color)),
108+
pen As New Pen(color, Theme.LineWidth)
109+
_g.FillRectangle(br, CSng(bx0), pyQ3, CSng(bx1 - bx0), pyQ1 - pyQ3)
110+
_g.DrawRectangle(pen, CSng(bx0), pyQ3, CSng(bx1 - bx0), pyQ1 - pyQ3)
111+
_g.DrawLine(pen, CSng(bx0), pyQ2, CSng(bx1), pyQ2)
112+
_g.DrawLine(pen, CSng(center), pyQ1, CSng(center), pyWLo)
113+
_g.DrawLine(pen, CSng(center), pyQ3, CSng(center), pyWHi)
114+
_g.DrawLine(pen, CSng(bx0 + 4), pyWLo, CSng(bx1 - 4), pyWLo)
115+
_g.DrawLine(pen, CSng(bx0 + 4), pyWHi, CSng(bx1 - 4), pyWHi)
116+
If ShowMean Then
117+
Dim mean = g.Data.Average()
118+
Dim pyM = ToPixelY(mean, ymin, ymax)
119+
Using dsh As New Pen(color, Theme.LineWidth)
120+
dsh.DashStyle = DashStyle.Dot
121+
_g.DrawLine(dsh, CSng(bx0), pyM, CSng(bx1), pyM)
122+
End Using
123+
End If
124+
End Using
125+
If ShowOutliers Then
126+
For Each o In outliers
127+
Dim py = ToPixelY(o, ymin, ymax)
128+
DrawMarker(center, py, MarkerShape.Circle, 4, color)
129+
Next
130+
End If
131+
End If
132+
Next
133+
End Sub
134+
135+
Private Shared Function Quantile(sorted As Double(), q As Double) As Double
136+
If sorted.Length = 0 Then Return 0
137+
Dim pos = (sorted.Length - 1) * q
138+
Dim lo = CInt(Math.Floor(pos))
139+
Dim hi = CInt(Math.Ceiling(pos))
140+
If lo = hi Then Return sorted(lo)
141+
Return sorted(lo) + (sorted(hi) - sorted(lo)) * (pos - lo)
142+
End Function
143+
End Class

0 commit comments

Comments
 (0)