Optimize the Rendering of Inline Formulas, Subscripts and Superscripts, Bubble Charts, and Horizontal Bars

This commit is contained in:
马一丁
2025-11-27 03:35:55 +08:00
parent 2a5d984aeb
commit 23356631f4
3 changed files with 225 additions and 56 deletions
+147 -24
View File
@@ -160,6 +160,19 @@ class ChartToSVGConverter:
if props.get('type'):
chart_type = props['type']
# Chart.js v4已移除horizontalBar类型,这里自动降级为bar并设置横向坐标
horizontal_bar = False
if chart_type and str(chart_type).lower() == 'horizontalbar':
chart_type = 'bar'
horizontal_bar = True
# 支持通过indexAxis: 'y' 强制横向柱状图
if isinstance(props, dict):
options = props.get('options') or {}
index_axis = (options.get('indexAxis') or props.get('indexAxis') or '').lower()
if index_axis == 'y':
horizontal_bar = True
# 提取数据
data = widget_data.get('data', {})
if not data:
@@ -172,10 +185,16 @@ class ChartToSVGConverter:
logger.debug("检测到词云图表,跳过chart_to_svg转换")
return None
render_method = getattr(self, f'_render_{chart_type}', None)
if not render_method:
logger.warning(f"不支持的图表类型: {chart_type}")
return None
# 分派渲染方法,特殊处理横向柱状图
if chart_type == 'bar':
return self._render_bar(data, props, width, height, dpi, horizontal=horizontal_bar)
elif chart_type == 'bubble':
return self._render_bubble(data, props, width, height, dpi)
else:
render_method = getattr(self, f'_render_{chart_type}', None)
if not render_method:
logger.warning(f"不支持的图表类型: {chart_type}")
return None
# 创建图表并转换为SVG
return render_method(data, props, width, height, dpi)
@@ -687,9 +706,10 @@ class ChartToSVGConverter:
props: Dict[str, Any],
width: int,
height: int,
dpi: int
dpi: int,
horizontal: bool = False
) -> Optional[str]:
"""渲染柱状图"""
"""渲染柱状图(支持横向barh"""
try:
labels = data.get('labels', [])
datasets = data.get('datasets', [])
@@ -703,44 +723,147 @@ class ChartToSVGConverter:
colors = self._get_colors(datasets)
# 计算柱子位置
x = np.arange(len(labels))
positions = np.arange(len(labels))
width_bar = 0.8 / len(datasets) if len(datasets) > 1 else 0.6
# 绘制每个数据系列
# 横向/纵向绘制
for i, dataset in enumerate(datasets):
dataset_data = dataset.get('data', [])
label = dataset.get('label', f'系列{i+1}')
color = colors[i]
offset = (i - len(datasets)/2 + 0.5) * width_bar
ax.bar(
x + offset,
dataset_data,
width_bar,
label=label,
color=color,
alpha=0.8,
edgecolor='white',
linewidth=0.5
)
# 设置x轴标签
ax.set_xticks(x)
ax.set_xticklabels(labels, rotation=45, ha='right')
if horizontal:
ax.barh(
positions + offset,
dataset_data,
height=width_bar,
label=label,
color=color,
alpha=0.8,
edgecolor='white',
linewidth=0.5
)
else:
ax.bar(
positions + offset,
dataset_data,
width_bar,
label=label,
color=color,
alpha=0.8,
edgecolor='white',
linewidth=0.5
)
# 轴标签/网格
if horizontal:
ax.set_yticks(positions)
ax.set_yticklabels(labels)
ax.invert_yaxis() # 与Chart.js横向排列保持一致
ax.grid(True, alpha=0.3, linestyle='--', axis='x')
else:
ax.set_xticks(positions)
ax.set_xticklabels(labels, rotation=45, ha='right')
ax.grid(True, alpha=0.3, linestyle='--', axis='y')
# 显示图例
if len(datasets) > 1:
ax.legend(loc='best', framealpha=0.9)
# 网格
ax.grid(True, alpha=0.3, linestyle='--', axis='y')
return self._figure_to_svg(fig)
except Exception as e:
logger.error(f"渲染柱状图失败: {e}")
return None
def _render_bubble(
self,
data: Dict[str, Any],
props: Dict[str, Any],
width: int,
height: int,
dpi: int
) -> Optional[str]:
"""渲染气泡图"""
try:
datasets = data.get('datasets', [])
if not datasets:
return None
title = props.get('title')
fig, ax = self._create_figure(width, height, dpi, title)
colors = self._get_colors(datasets)
def _safe_radius(raw) -> float:
try:
val = float(raw)
return max(val, 0.5)
except Exception:
return 1.0
all_x: list[float] = []
all_y: list[float] = []
max_r: float = 0.0
for i, dataset in enumerate(datasets):
points = dataset.get('data', [])
label = dataset.get('label', f'系列{i+1}')
color = colors[i]
if points and isinstance(points[0], dict):
xs = [p.get('x', 0) for p in points]
ys = [p.get('y', 0) for p in points]
rs = [_safe_radius(p.get('r', 1)) for p in points]
else:
xs = list(range(len(points)))
ys = points
rs = [1.0 for _ in points]
all_x.extend(xs)
all_y.extend(ys)
if rs:
max_r = max(max_r, max(rs))
# 适度放大半径,近似Chart.js像素尺寸(动态尺度,避免过大遮挡)
size_scale = 8.0 if max_r <= 20 else 6.5
sizes = [(r * size_scale) ** 2 for r in rs]
ax.scatter(
xs,
ys,
s=sizes,
label=label,
color=color,
alpha=0.45,
edgecolors='white',
linewidth=0.6
)
if len(datasets) > 1:
ax.legend(loc='best', framealpha=0.9)
# 适度留白,避免大气泡被裁切
if all_x and all_y:
x_min, x_max = min(all_x), max(all_x)
y_min, y_max = min(all_y), max(all_y)
x_span = max(x_max - x_min, 1e-6)
y_span = max(y_max - y_min, 1e-6)
pad_x = max(x_span * 0.12, max_r * 1.2)
pad_y = max(y_span * 0.12, max_r * 1.2)
ax.set_xlim(x_min - pad_x, x_max + pad_x)
ax.set_ylim(y_min - pad_y, y_max + pad_y)
# 额外安全边距
ax.margins(x=0.05, y=0.05)
ax.grid(True, alpha=0.3, linestyle='--')
return self._figure_to_svg(fig)
except Exception as e:
logger.error(f"渲染气泡图失败: {e}", exc_info=True)
return None
def _render_pie(
self,
data: Dict[str, Any],