Toggle navigation
Toggle navigation
This project
Loading...
Sign in
万朱浩
/
Venue-Ops
Go to a project
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
马一丁
2025-11-20 02:18:52 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
994477fd601f44053891332dcf99de196d4cc1ca
994477fd
1 parent
f98d3606
Fixed Chart Handling Issues in HTML and PDF and Improved Chart Readability
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
126 additions
and
70 deletions
ReportEngine/renderers/chart_to_svg.py
ReportEngine/renderers/chart_to_svg.py
View file @
994477f
...
...
@@ -24,7 +24,7 @@ try:
matplotlib
.
use
(
'Agg'
)
# 使用非GUI后端
import
matplotlib.pyplot
as
plt
import
matplotlib.font_manager
as
fm
from
matplotlib.patches
import
Wedge
from
matplotlib.patches
import
Wedge
,
Rectangle
import
numpy
as
np
MATPLOTLIB_AVAILABLE
=
True
except
ImportError
:
...
...
@@ -45,24 +45,29 @@ class ChartToSVGConverter:
将Chart.js图表数据转换为SVG矢量图形
"""
# 默认颜色调色板(
与Chart.js默认颜色接近
)
# 默认颜色调色板(
优化版:明亮且易区分
)
DEFAULT_COLORS
=
[
'#FF6384'
,
'#36A2EB'
,
'#FFCE56'
,
'#4BC0C0'
,
'#9966FF'
,
'#FF9F40'
,
'#FF6384'
,
'#C9CBCF'
'#4A90E2'
,
'#E85D75'
,
'#50C878'
,
'#FFB347'
,
# 明亮蓝、珊瑚红、翠绿、橙黄
'#9B59B6'
,
'#3498DB'
,
'#E67E22'
,
'#16A085'
,
# 紫色、天蓝、橙色、青色
'#F39C12'
,
'#D35400'
,
'#27AE60'
,
'#8E44AD'
# 金色、深橙、绿色、紫罗兰
]
# CSS变量到颜色的映射表(
支持常见的Chart.js主题变量
)
# CSS变量到颜色的映射表(
优化版:使用更明亮、更浅的颜色
)
CSS_VAR_COLOR_MAP
=
{
'var(--color-accent)'
:
'#007AFF'
,
# 蓝色(强调色)
'var(--re-accent-color)'
:
'#007AFF'
,
# 蓝色
'var(--color-kpi-down)'
:
'#DC3545'
,
# 红色(下降/危险)
'var(--re-danger-color)'
:
'#DC3545'
,
# 红色(危险)
'var(--color-warning)'
:
'#FFC107'
,
# 黄色(警告)
'var(--re-warning-color)'
:
'#FFC107'
,
# 黄色
'var(--color-success)'
:
'#28A745'
,
# 绿色(成功)
'var(--re-success-color)'
:
'#28A745'
,
# 绿色
'var(--color-primary)'
:
'#007BFF'
,
# 主色
'var(--color-secondary)'
:
'#6C757D'
,
# 次要色
'var(--color-accent)'
:
'#4A90E2'
,
# 明亮蓝色(从#007AFF改为更浅)
'var(--re-accent-color)'
:
'#4A90E2'
,
# 明亮蓝色
'var(--re-accent-color-translucent)'
:
(
0.29
,
0.565
,
0.886
,
0.08
),
# 蓝色极浅透明 rgba(74, 144, 226, 0.08)
'var(--color-kpi-down)'
:
'#E85D75'
,
# 珊瑚红色(从#DC3545改为更柔和)
'var(--re-danger-color)'
:
'#E85D75'
,
# 珊瑚红色
'var(--re-danger-color-translucent)'
:
(
0.91
,
0.365
,
0.459
,
0.08
),
# 红色极浅透明 rgba(232, 93, 117, 0.08)
'var(--color-warning)'
:
'#FFB347'
,
# 柔和橙黄色(从#FFC107改为更浅)
'var(--re-warning-color)'
:
'#FFB347'
,
# 柔和橙黄色
'var(--re-warning-color-translucent)'
:
(
1.0
,
0.702
,
0.278
,
0.08
),
# 黄色极浅透明 rgba(255, 179, 71, 0.08)
'var(--color-success)'
:
'#50C878'
,
# 翠绿色(从#28A745改为更明亮)
'var(--re-success-color)'
:
'#50C878'
,
# 翠绿色
'var(--re-success-color-translucent)'
:
(
0.314
,
0.784
,
0.471
,
0.08
),
# 绿色极浅透明 rgba(80, 200, 120, 0.08)
'var(--color-primary)'
:
'#3498DB'
,
# 天蓝色
'var(--color-secondary)'
:
'#95A5A6'
,
# 浅灰色
}
def
__init__
(
self
,
font_path
:
Optional
[
str
]
=
None
):
...
...
@@ -277,7 +282,7 @@ class ChartToSVGConverter:
渲染折线图(增强版)
支持特性:
-
双y轴(yAxisID: 'y' 和 'y1'
)
-
多y轴(yAxisID: 'y', 'y1', 'y2', 'y3'...
)
- 填充区域(fill: true)
- 透明度(backgroundColor中的alpha通道)
- 线条样式(tension曲线平滑)
...
...
@@ -289,30 +294,71 @@ class ChartToSVGConverter:
if
not
labels
or
not
datasets
:
return
None
# 检查是否有双y轴
has_dual_axis
=
any
(
dataset
.
get
(
'yAxisID'
)
==
'y1'
for
dataset
in
datasets
)
# 收集所有唯一的yAxisID
y_axis_ids
=
[]
for
dataset
in
datasets
:
y_axis_id
=
dataset
.
get
(
'yAxisID'
,
'y'
)
if
y_axis_id
not
in
y_axis_ids
:
y_axis_ids
.
append
(
y_axis_id
)
# 确保'y'是第一个轴
if
'y'
in
y_axis_ids
:
y_axis_ids
.
remove
(
'y'
)
y_axis_ids
.
insert
(
0
,
'y'
)
# 检查是否有多个y轴
has_multiple_axes
=
len
(
y_axis_ids
)
>
1
title
=
props
.
get
(
'title'
)
options
=
props
.
get
(
'options'
,
{})
scales
=
options
.
get
(
'scales'
,
{})
# 创建图表,如果有双y轴则创建双y轴布局
if
has_dual_axis
:
fig
,
ax1
=
plt
.
subplots
(
figsize
=
(
width
/
dpi
,
height
/
dpi
),
dpi
=
dpi
)
ax2
=
ax1
.
twinx
()
# 创建共享x轴的第二个y轴
else
:
fig
,
ax1
=
self
.
_create_figure
(
width
,
height
,
dpi
,
title
)
ax2
=
None
# 创建图表和多个y轴
fig
,
ax1
=
plt
.
subplots
(
figsize
=
(
width
/
dpi
,
height
/
dpi
),
dpi
=
dpi
)
if
title
and
has_dual_axis
:
if
title
:
ax1
.
set_title
(
title
,
fontsize
=
14
,
fontweight
=
'bold'
,
pad
=
20
)
# 创建y轴映射字典
axes
=
{
'y'
:
ax1
}
if
has_multiple_axes
:
# 统计每个位置(left/right)的轴数量,用于计算偏移
left_axes_count
=
0
right_axes_count
=
0
# 为每个额外的yAxisID创建新的y轴
for
y_axis_id
in
y_axis_ids
[
1
:]:
if
y_axis_id
==
'y'
:
continue
# 创建新的y轴
new_ax
=
ax1
.
twinx
()
axes
[
y_axis_id
]
=
new_ax
# 从scales配置中获取轴的位置
y_config
=
scales
.
get
(
y_axis_id
,
{})
position
=
y_config
.
get
(
'position'
,
'right'
)
if
position
==
'left'
:
# 左侧额外轴,向左偏移
if
left_axes_count
>
0
:
new_ax
.
spines
[
'left'
]
.
set_position
((
'outward'
,
60
*
left_axes_count
))
new_ax
.
yaxis
.
set_label_position
(
'left'
)
new_ax
.
yaxis
.
set_ticks_position
(
'left'
)
left_axes_count
+=
1
else
:
# 右侧额外轴,向右偏移
if
right_axes_count
>
0
:
new_ax
.
spines
[
'right'
]
.
set_position
((
'outward'
,
60
*
right_axes_count
))
right_axes_count
+=
1
colors
=
self
.
_get_colors
(
datasets
)
# 分别收集两个y轴的数据系列
y1_lines
=
[]
y2_lines
=
[]
# 收集每个y轴的线条和填充信息用于图例
axis_lines
=
{
axis_id
:
[]
for
axis_id
in
y_axis_ids
}
legend_handles
=
[]
# 图例句柄
legend_labels
=
[]
# 图例标签
# 绘制每个数据系列
for
i
,
dataset
in
enumerate
(
datasets
):
...
...
@@ -328,7 +374,7 @@ class ChartToSVGConverter:
background_color
=
self
.
_parse_color
(
dataset
.
get
(
'backgroundColor'
,
color
))
# 选择对应的坐标轴
ax
=
ax
2
if
(
y_axis_id
==
'y1'
and
ax2
is
not
None
)
else
ax1
ax
=
ax
es
.
get
(
y_axis_id
,
ax1
)
# 绘制折线
x_data
=
range
(
len
(
labels
))
...
...
@@ -343,69 +389,79 @@ class ChartToSVGConverter:
y_smooth
=
spl
(
x_smooth
)
line
,
=
ax
.
plot
(
x_smooth
,
y_smooth
,
label
=
label
,
color
=
border_color
,
linewidth
=
2
)
# 如果需要填充
# 如果需要填充
(使用极低透明度避免遮挡)
if
fill
:
ax
.
fill_between
(
x_smooth
,
y_smooth
,
alpha
=
0.
3
,
color
=
background_color
)
ax
.
fill_between
(
x_smooth
,
y_smooth
,
alpha
=
0.
08
,
color
=
background_color
)
except
:
# 如果平滑失败,使用普通折线
line
,
=
ax
.
plot
(
x_data
,
dataset_data
,
marker
=
'o'
,
label
=
label
,
color
=
border_color
,
linewidth
=
2
,
markersize
=
6
)
if
fill
:
ax
.
fill_between
(
x_data
,
dataset_data
,
alpha
=
0.
3
,
color
=
background_color
)
ax
.
fill_between
(
x_data
,
dataset_data
,
alpha
=
0.
08
,
color
=
background_color
)
else
:
line
,
=
ax
.
plot
(
x_data
,
dataset_data
,
marker
=
'o'
,
label
=
label
,
color
=
border_color
,
linewidth
=
2
,
markersize
=
6
)
if
fill
:
ax
.
fill_between
(
x_data
,
dataset_data
,
alpha
=
0.
3
,
color
=
background_color
)
ax
.
fill_between
(
x_data
,
dataset_data
,
alpha
=
0.
08
,
color
=
background_color
)
else
:
# 直线连接(tension=0或scipy不可用)
line
,
=
ax
.
plot
(
x_data
,
dataset_data
,
marker
=
'o'
,
label
=
label
,
color
=
border_color
,
linewidth
=
2
,
markersize
=
6
)
# 如果需要填充
# 如果需要填充
(使用极低透明度避免遮挡)
if
fill
:
ax
.
fill_between
(
x_data
,
dataset_data
,
alpha
=
0.3
,
color
=
background_color
)
# 记录哪个轴有哪些线
if
ax
==
ax2
:
y2_lines
.
append
(
line
)
ax
.
fill_between
(
x_data
,
dataset_data
,
alpha
=
0.08
,
color
=
background_color
)
# 记录这条线属于哪个轴
axis_lines
[
y_axis_id
]
.
append
(
line
)
# 创建图例项:如果有填充,创建带填充背景的图例
if
fill
:
# 创建一个矩形patch作为填充背景(使用稍高透明度以便在图例中可见)
fill_patch
=
Rectangle
((
0
,
0
),
1
,
1
,
facecolor
=
background_color
,
edgecolor
=
'none'
,
alpha
=
0.15
)
# 组合线条和填充patch
legend_handles
.
append
((
line
,
fill_patch
))
legend_labels
.
append
(
label
)
else
:
y1_lines
.
append
(
line
)
legend_handles
.
append
(
line
)
legend_labels
.
append
(
label
)
# 设置x轴标签
ax1
.
set_xticks
(
range
(
len
(
labels
)))
ax1
.
set_xticklabels
(
labels
,
rotation
=
45
,
ha
=
'right'
)
# 设置y轴标签和标题
if
has_dual_axis
and
ax2
:
# 从options中获取y轴配置
scales
=
options
.
get
(
'scales'
,
{})
y_config
=
scales
.
get
(
'y'
,
{})
y1_config
=
scales
.
get
(
'y1'
,
{})
# 设置左侧y轴
for
y_axis_id
,
ax
in
axes
.
items
():
y_config
=
scales
.
get
(
y_axis_id
,
{})
y_title
=
y_config
.
get
(
'title'
,
{})
.
get
(
'text'
,
''
)
if
y_title
:
ax1
.
set_ylabel
(
y_title
,
fontsize
=
11
)
# 设置右侧y轴
y1_title
=
y1_config
.
get
(
'title'
,
{})
.
get
(
'text'
,
''
)
if
y1_title
:
ax2
.
set_ylabel
(
y1_title
,
fontsize
=
11
)
# 设置网格(只在主轴显示)
ax1
.
grid
(
True
,
alpha
=
0.3
,
linestyle
=
'--'
)
ax2
.
grid
(
False
)
# 右侧y轴不显示网格
# 合并图例(显示所有数据系列)
lines
=
y1_lines
+
y2_lines
labels_list
=
[
line
.
get_label
()
for
line
in
lines
]
ax1
.
legend
(
lines
,
labels_list
,
loc
=
'best'
,
framealpha
=
0.9
)
else
:
# 单y轴的情况
if
len
(
datasets
)
>
1
:
ax1
.
legend
(
loc
=
'best'
,
framealpha
=
0.9
)
ax1
.
grid
(
True
,
alpha
=
0.3
,
linestyle
=
'--'
)
ax
.
set_ylabel
(
y_title
,
fontsize
=
11
)
# 设置y轴标签颜色(如果该轴只有一条线,使用该线的颜色)
if
len
(
axis_lines
[
y_axis_id
])
==
1
:
line_color
=
axis_lines
[
y_axis_id
][
0
]
.
get_color
()
ax
.
tick_params
(
axis
=
'y'
,
labelcolor
=
line_color
)
ax
.
yaxis
.
label
.
set_color
(
line_color
)
# 设置网格(只在主轴显示)
ax1
.
grid
(
True
,
alpha
=
0.3
,
linestyle
=
'--'
)
for
y_axis_id
in
y_axis_ids
[
1
:]:
if
y_axis_id
in
axes
:
axes
[
y_axis_id
]
.
grid
(
False
)
# 创建图例
if
has_multiple_axes
or
len
(
datasets
)
>
1
:
# 使用自定义的legend_handles和legend_labels
from
matplotlib.legend_handler
import
HandlerTuple
ax1
.
legend
(
legend_handles
,
legend_labels
,
loc
=
'best'
,
framealpha
=
0.9
,
handler_map
=
{
tuple
:
HandlerTuple
(
ndivide
=
None
)})
return
self
.
_figure_to_svg
(
fig
)
...
...
Please
register
or
login
to post a comment