228 lines
8.5 KiB
Python
228 lines
8.5 KiB
Python
|
|
"""
|
|||
|
|
绘图区 (Plot) XML 操作模块
|
|||
|
|
|
|||
|
|
负责创建不同类型的图表绘图区 (<c:barChart>, <c:lineChart> 等)。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from lxml import etree
|
|||
|
|
from typing import Literal
|
|||
|
|
|
|||
|
|
from ..oxml_ns import NAMESPACES
|
|||
|
|
|
|||
|
|
ChartType = Literal['bar', 'column', 'line', 'area', 'scatter']
|
|||
|
|
|
|||
|
|
|
|||
|
|
def create_plot_element(
|
|||
|
|
plotArea,
|
|||
|
|
chart_type: ChartType,
|
|||
|
|
cat_ax_id: int,
|
|||
|
|
val_ax_id: int,
|
|||
|
|
order_index: int = 0,
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
创建图表绘图区元素
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
plotArea: 父绘图区元素
|
|||
|
|
chart_type: 图表类型 ('bar', 'line', 'area', 'scatter')
|
|||
|
|
cat_ax_id: 分类轴 ID
|
|||
|
|
val_ax_id: 值轴 ID
|
|||
|
|
order_index: 绘图顺序索引(0=最底层,越大越在上层)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
创建的绘图元素 (lxml Element)
|
|||
|
|
|
|||
|
|
Raises:
|
|||
|
|
ValueError: 如果图表类型不支持
|
|||
|
|
|
|||
|
|
Notes:
|
|||
|
|
- 每个绘图元素会自动关联指定的坐标轴
|
|||
|
|
- 调用方需要自己添加系列 (<c:ser>)
|
|||
|
|
- order_index 决定图表的堆叠顺序
|
|||
|
|
"""
|
|||
|
|
chart_type = chart_type.lower()
|
|||
|
|
|
|||
|
|
if chart_type in ('bar', 'column'):
|
|||
|
|
return _create_bar_plot(plotArea, cat_ax_id, val_ax_id, order_index)
|
|||
|
|
elif chart_type == 'line':
|
|||
|
|
return _create_line_plot(plotArea, cat_ax_id, val_ax_id, order_index)
|
|||
|
|
elif chart_type == 'area':
|
|||
|
|
return _create_area_plot(plotArea, cat_ax_id, val_ax_id, order_index)
|
|||
|
|
elif chart_type == 'scatter':
|
|||
|
|
return _create_scatter_plot(plotArea, cat_ax_id, val_ax_id, order_index)
|
|||
|
|
else:
|
|||
|
|
raise ValueError(f"不支持的图表类型: {chart_type}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_bar_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int):
|
|||
|
|
"""创建柱状图元素"""
|
|||
|
|
barChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}barChart")
|
|||
|
|
|
|||
|
|
# barDir: 柱状图方向 ('col' = 垂直柱状, 'bar' = 水平条形)
|
|||
|
|
barDir = etree.SubElement(barChart, f"{{{NAMESPACES['c']}}}barDir")
|
|||
|
|
barDir.set('val', 'col')
|
|||
|
|
|
|||
|
|
# grouping: 分组方式 ('clustered' = 簇状, 'stacked' = 堆叠)
|
|||
|
|
grouping = etree.SubElement(barChart, f"{{{NAMESPACES['c']}}}grouping")
|
|||
|
|
grouping.set('val', 'clustered')
|
|||
|
|
|
|||
|
|
# varyColors: 是否每个系列使用不同颜色
|
|||
|
|
varyColors = etree.SubElement(barChart, f"{{{NAMESPACES['c']}}}varyColors")
|
|||
|
|
varyColors.set('val', '0')
|
|||
|
|
|
|||
|
|
# ⭐ 绘图顺序(决定堆叠层次,数字越小越在底层)
|
|||
|
|
# OOXML 规范建议在 varyColors 之后添加
|
|||
|
|
# 注意:这里不是 <c:ser> 的 order,而是整个 plot 的渲染顺序
|
|||
|
|
# 但 PowerPoint 实际使用 XML 元素出现的顺序来决定堆叠
|
|||
|
|
# 所以这个标签主要是语义化,真正的顺序由 XML 元素在 plotArea 中的位置决定
|
|||
|
|
|
|||
|
|
# ⚠️ 注意:不在这里添加轴引用!
|
|||
|
|
# 轴引用应该在所有系列之后添加,由调用方在添加完系列后调用 add_axis_refs()
|
|||
|
|
|
|||
|
|
return barChart
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_line_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int):
|
|||
|
|
"""创建折线图元素"""
|
|||
|
|
lineChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}lineChart")
|
|||
|
|
|
|||
|
|
# grouping: 分组方式 ('standard' = 标准)
|
|||
|
|
grouping = etree.SubElement(lineChart, f"{{{NAMESPACES['c']}}}grouping")
|
|||
|
|
grouping.set('val', 'standard')
|
|||
|
|
|
|||
|
|
# varyColors: 是否每个系列使用不同颜色
|
|||
|
|
varyColors = etree.SubElement(lineChart, f"{{{NAMESPACES['c']}}}varyColors")
|
|||
|
|
varyColors.set('val', '0')
|
|||
|
|
|
|||
|
|
# ⚠️ 注意:不在这里添加轴引用!
|
|||
|
|
# 轴引用应该在所有系列之后添加
|
|||
|
|
|
|||
|
|
return lineChart
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_area_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int):
|
|||
|
|
"""创建面积图元素"""
|
|||
|
|
areaChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}areaChart")
|
|||
|
|
|
|||
|
|
# grouping: 分组方式 ('standard' = 标准)
|
|||
|
|
grouping = etree.SubElement(areaChart, f"{{{NAMESPACES['c']}}}grouping")
|
|||
|
|
grouping.set('val', 'standard')
|
|||
|
|
|
|||
|
|
# varyColors
|
|||
|
|
varyColors = etree.SubElement(areaChart, f"{{{NAMESPACES['c']}}}varyColors")
|
|||
|
|
varyColors.set('val', '0')
|
|||
|
|
|
|||
|
|
# ⚠️ 注意:不在这里添加轴引用!
|
|||
|
|
|
|||
|
|
return areaChart
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_scatter_plot(plotArea, cat_ax_id: int, val_ax_id: int, order_index: int):
|
|||
|
|
"""创建散点图元素"""
|
|||
|
|
scatterChart = etree.SubElement(plotArea, f"{{{NAMESPACES['c']}}}scatterChart")
|
|||
|
|
|
|||
|
|
# scatterStyle: 散点样式 ('lineMarker' = 带线和标记)
|
|||
|
|
scatterStyle = etree.SubElement(scatterChart, f"{{{NAMESPACES['c']}}}scatterStyle")
|
|||
|
|
scatterStyle.set('val', 'lineMarker')
|
|||
|
|
|
|||
|
|
# varyColors
|
|||
|
|
varyColors = etree.SubElement(scatterChart, f"{{{NAMESPACES['c']}}}varyColors")
|
|||
|
|
varyColors.set('val', '0')
|
|||
|
|
|
|||
|
|
# ⚠️ 注意:不在这里添加轴引用!
|
|||
|
|
|
|||
|
|
return scatterChart
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_axis_refs(plot_element, cat_ax_id: int, val_ax_id: int):
|
|||
|
|
"""
|
|||
|
|
为绘图元素添加坐标轴引用(应该在所有系列之后调用)
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
plot_element: 绘图元素 (<c:barChart>, <c:lineChart> 等)
|
|||
|
|
cat_ax_id: 分类轴 ID
|
|||
|
|
val_ax_id: 值轴 ID
|
|||
|
|
"""
|
|||
|
|
axId1 = etree.SubElement(plot_element, f"{{{NAMESPACES['c']}}}axId")
|
|||
|
|
axId1.set('val', str(cat_ax_id))
|
|||
|
|
|
|||
|
|
axId2 = etree.SubElement(plot_element, f"{{{NAMESPACES['c']}}}axId")
|
|||
|
|
axId2.set('val', str(val_ax_id))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_plot_categories(plot_element, categories: list):
|
|||
|
|
"""
|
|||
|
|
为绘图元素添加共享的分类数据(在所有系列之前调用)
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
plot_element: 绘图元素 (<c:barChart>, <c:lineChart> 等)
|
|||
|
|
categories: 分类列表(X轴数据)
|
|||
|
|
|
|||
|
|
Notes:
|
|||
|
|
- 在 OOXML 规范中,<c:cat> 是图表级别的共享元素
|
|||
|
|
- 应该在添加任何 <c:ser> 系列之前调用
|
|||
|
|
- 所有系列共享同一组分类数据
|
|||
|
|
- ⭐ 自动检测日期类型,使用 numCache(数值缓存)而非 strCache
|
|||
|
|
"""
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
# ⭐ 检测是否为日期类型
|
|||
|
|
is_date_data = False
|
|||
|
|
if categories and isinstance(categories[0], (datetime, float)):
|
|||
|
|
# datetime 对象或浮点数(Excel 日期序列号)
|
|||
|
|
is_date_data = True
|
|||
|
|
|
|||
|
|
cat = etree.SubElement(plot_element, f"{{{NAMESPACES['c']}}}cat")
|
|||
|
|
|
|||
|
|
if is_date_data:
|
|||
|
|
# ⭐ 新方案:将日期格式化为字符串,使用 strRef + strCache
|
|||
|
|
# 这样 PowerPoint 就会将其作为文本标签显示,不会出现 1900 年问题
|
|||
|
|
strRef = etree.SubElement(cat, f"{{{NAMESPACES['c']}}}strRef")
|
|||
|
|
|
|||
|
|
# f (公式引用)
|
|||
|
|
f_elem = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}f")
|
|||
|
|
f_elem.text = f"Sheet1!$A$2:$A${len(categories) + 1}"
|
|||
|
|
|
|||
|
|
# strCache (字符串缓存)
|
|||
|
|
strCache = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}strCache")
|
|||
|
|
ptCount = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}ptCount")
|
|||
|
|
ptCount.set('val', str(len(categories)))
|
|||
|
|
|
|||
|
|
# 添加每个分类点(格式化为字符串)
|
|||
|
|
for i, cat_value in enumerate(categories):
|
|||
|
|
pt = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}pt")
|
|||
|
|
pt.set('idx', str(i))
|
|||
|
|
v = etree.SubElement(pt, f"{{{NAMESPACES['c']}}}v")
|
|||
|
|
|
|||
|
|
if isinstance(cat_value, datetime):
|
|||
|
|
# 格式化为 "yyyy/mm"(年份/月份)
|
|||
|
|
v.text = cat_value.strftime('%Y/%m')
|
|||
|
|
elif isinstance(cat_value, float):
|
|||
|
|
# 假设是 Excel 日期序列号,转换为日期字符串
|
|||
|
|
base_date = datetime(1899, 12, 30)
|
|||
|
|
from datetime import timedelta
|
|||
|
|
actual_date = base_date + timedelta(days=cat_value)
|
|||
|
|
v.text = actual_date.strftime('%Y/%m')
|
|||
|
|
else:
|
|||
|
|
v.text = str(cat_value)
|
|||
|
|
else:
|
|||
|
|
# ⭐ 使用 strRef + strCache(普通分类轴)
|
|||
|
|
strRef = etree.SubElement(cat, f"{{{NAMESPACES['c']}}}strRef")
|
|||
|
|
|
|||
|
|
# f (公式引用)
|
|||
|
|
f_elem = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}f")
|
|||
|
|
f_elem.text = f"Sheet1!$A$2:$A${len(categories) + 1}"
|
|||
|
|
|
|||
|
|
# strCache (字符串缓存)
|
|||
|
|
strCache = etree.SubElement(strRef, f"{{{NAMESPACES['c']}}}strCache")
|
|||
|
|
ptCount = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}ptCount")
|
|||
|
|
ptCount.set('val', str(len(categories)))
|
|||
|
|
|
|||
|
|
# 添加每个分类点
|
|||
|
|
for i, cat_value in enumerate(categories):
|
|||
|
|
pt = etree.SubElement(strCache, f"{{{NAMESPACES['c']}}}pt")
|
|||
|
|
pt.set('idx', str(i))
|
|||
|
|
v = etree.SubElement(pt, f"{{{NAMESPACES['c']}}}v")
|
|||
|
|
v.text = str(cat_value)
|
|||
|
|
|