588 lines
22 KiB
Python
588 lines
22 KiB
Python
|
|
"""
|
|||
|
|
日期轴 XML 直接操作模块
|
|||
|
|
|
|||
|
|
专门用于设置日期轴的底层 XML 属性,绕过 python-pptx 的限制。
|
|||
|
|
|
|||
|
|
使用场景:
|
|||
|
|
- 横轴确定为日期类型
|
|||
|
|
- 需要精确控制刻度密度
|
|||
|
|
- 需要设置日期范围
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from lxml import etree
|
|||
|
|
from typing import Optional, Literal
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
from .oxml_ns import NAMESPACES
|
|||
|
|
|
|||
|
|
|
|||
|
|
def excel_date(date: datetime) -> float:
|
|||
|
|
"""
|
|||
|
|
将 Python datetime 转换为 Excel 日期序号
|
|||
|
|
|
|||
|
|
Excel 的日期基准:1900-01-01 = 1
|
|||
|
|
"""
|
|||
|
|
base_date = datetime(1899, 12, 30) # Excel 的实际基准日期
|
|||
|
|
delta = date - base_date
|
|||
|
|
return float(delta.days)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DateAxisConfig:
|
|||
|
|
"""
|
|||
|
|
日期轴 XML 配置类
|
|||
|
|
|
|||
|
|
直接操作 XML 以实现完整的日期轴控制
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
base_unit: Literal["days", "months", "years"] = "days",
|
|||
|
|
major_unit: float = 7.0,
|
|||
|
|
major_unit_scale: Literal["days", "months", "years"] = "days",
|
|||
|
|
minor_unit: Optional[float] = None,
|
|||
|
|
minor_unit_scale: Literal["days", "months", "years"] = "days",
|
|||
|
|
min_date: Optional[datetime] = None,
|
|||
|
|
max_date: Optional[datetime] = None,
|
|||
|
|
number_format: str = "yyyy-mm-dd",
|
|||
|
|
auto_adjust: bool = False,
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
初始化日期轴配置
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
base_unit: 基础时间单位(days/months/years)
|
|||
|
|
major_unit: 主刻度单位数值(如 7 表示每 7 个单位一个刻度)
|
|||
|
|
major_unit_scale: 主刻度单位类型
|
|||
|
|
minor_unit: 次刻度单位数值
|
|||
|
|
minor_unit_scale: 次刻度单位类型
|
|||
|
|
min_date: 最小日期(用于固定范围)
|
|||
|
|
max_date: 最大日期(用于固定范围)
|
|||
|
|
number_format: 日期显示格式
|
|||
|
|
auto_adjust: 是否允许 PowerPoint 自动调整
|
|||
|
|
|
|||
|
|
Examples:
|
|||
|
|
>>> # 每周显示一个刻度
|
|||
|
|
>>> config = DateAxisConfig(
|
|||
|
|
... base_unit="days",
|
|||
|
|
... major_unit=7.0,
|
|||
|
|
... major_unit_scale="days",
|
|||
|
|
... )
|
|||
|
|
|
|||
|
|
>>> # 每月显示一个刻度
|
|||
|
|
>>> config = DateAxisConfig(
|
|||
|
|
... base_unit="months",
|
|||
|
|
... major_unit=1.0,
|
|||
|
|
... major_unit_scale="months",
|
|||
|
|
... )
|
|||
|
|
|
|||
|
|
>>> # 固定日期范围
|
|||
|
|
>>> config = DateAxisConfig(
|
|||
|
|
... base_unit="days",
|
|||
|
|
... major_unit=14.0,
|
|||
|
|
... min_date=datetime(2024, 1, 1),
|
|||
|
|
... max_date=datetime(2024, 12, 31),
|
|||
|
|
... )
|
|||
|
|
"""
|
|||
|
|
self.base_unit = base_unit
|
|||
|
|
self.major_unit = major_unit
|
|||
|
|
self.major_unit_scale = major_unit_scale
|
|||
|
|
self.minor_unit = minor_unit or (major_unit / 7) # 默认为主刻度的 1/7
|
|||
|
|
self.minor_unit_scale = minor_unit_scale
|
|||
|
|
self.min_date = min_date
|
|||
|
|
self.max_date = max_date
|
|||
|
|
self.number_format = number_format
|
|||
|
|
self.auto_adjust = auto_adjust
|
|||
|
|
|
|||
|
|
def apply_to_chart(self, chart):
|
|||
|
|
"""
|
|||
|
|
应用日期轴配置到图表
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
chart: python-pptx 的 Chart 对象
|
|||
|
|
"""
|
|||
|
|
print(f"\n📅 应用日期轴配置(XML 直接操作):")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 1. 获取 XML 元素
|
|||
|
|
chart_element = chart._element
|
|||
|
|
|
|||
|
|
# ⭐ 新方案:数据已在 plots.py 中格式化为字符串并使用 strCache
|
|||
|
|
print(f" → 数据已格式化为字符串标签(如 '2024/01'),使用 strCache")
|
|||
|
|
|
|||
|
|
# ⭐ 关键修复:查找catAx并转换为dateAx
|
|||
|
|
# PowerPoint 不允许在 catAx 中使用时间单位元素,必须使用 dateAx
|
|||
|
|
cat_ax_elements = chart_element.findall('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}catAx')
|
|||
|
|
|
|||
|
|
if not cat_ax_elements:
|
|||
|
|
print(" ⚠️ 未找到分类轴元素")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# ⭐ 新方案:不转换为 dateAx,保持 catAx
|
|||
|
|
# PowerPoint 对 dateAx 的时间单位元素支持不稳定
|
|||
|
|
# 改用 catAx + numRef/numCache + scaling(min/max) + numFmt(日期格式)
|
|||
|
|
# 这样更简单且兼容性更好
|
|||
|
|
|
|||
|
|
cat_ax = cat_ax_elements[0]
|
|||
|
|
print(f" ✅ 使用 catAx(分类轴)+ 格式化字符串标签")
|
|||
|
|
|
|||
|
|
# ⭐ 设置标签位置为"低"(在坐标轴下方)
|
|||
|
|
self._set_tick_label_position(cat_ax, 'low')
|
|||
|
|
print(f" - 标签位置: 低(坐标轴下方)")
|
|||
|
|
|
|||
|
|
# ⭐ 设置标签间隔(控制显示数量不超过 7 个)
|
|||
|
|
# 使用 major_unit 作为间隔值(向下取整)
|
|||
|
|
label_interval = int(self.major_unit)
|
|||
|
|
self._set_label_interval(cat_ax, label_interval)
|
|||
|
|
print(f" - 标签间隔: 每 {label_interval} 个点显示一个标签")
|
|||
|
|
|
|||
|
|
# ⭐ 设置字体大小为 9pt
|
|||
|
|
try:
|
|||
|
|
category_axis = chart.category_axis
|
|||
|
|
from pptx.util import Pt
|
|||
|
|
category_axis.tick_labels.font.size = Pt(9)
|
|||
|
|
category_axis.tick_labels.font.name = "黑体"
|
|||
|
|
print(f" - 横轴字体: 黑体 9pt")
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" ⚠️ 字体设置失败: {e}")
|
|||
|
|
|
|||
|
|
print(f" ✅ 日期轴配置完成")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" ❌ 日期轴配置失败: {e}")
|
|||
|
|
import traceback
|
|||
|
|
traceback.print_exc()
|
|||
|
|
|
|||
|
|
def _convert_cat_to_numcache(self, chart_element):
|
|||
|
|
"""
|
|||
|
|
⚠️ 备用方法:将分类数据从 strCache 转换为 numCache
|
|||
|
|
|
|||
|
|
【重要】此方法已不再默认调用(自修复后)
|
|||
|
|
|
|||
|
|
修复说明:
|
|||
|
|
- 在 api.py 修复后,python-pptx 会直接生成 numCache
|
|||
|
|
- 此方法仅在需要手动处理旧文件或特殊场景时使用
|
|||
|
|
- 如果未来遇到 strCache,此方法会将字符串日期转换为 Excel 序列号
|
|||
|
|
|
|||
|
|
转换逻辑:
|
|||
|
|
- 从 strCache 读取 "YYYY-MM-DD" 格式的字符串
|
|||
|
|
- 调用 excel_date() 转换为 Excel 日期序列号(如 45567)
|
|||
|
|
- 写入 numCache,让 PowerPoint 正确识别为日期
|
|||
|
|
"""
|
|||
|
|
from lxml import etree
|
|||
|
|
|
|||
|
|
# 查找所有 c:cat 元素
|
|||
|
|
cat_elements = chart_element.findall('.//{http://schemas.openxmlformats.org/drawingml/2006/chart}cat')
|
|||
|
|
|
|||
|
|
converted_count = 0
|
|||
|
|
for cat_elem in cat_elements:
|
|||
|
|
# 查找 strCache(直接子元素)
|
|||
|
|
str_cache = cat_elem.find('{http://schemas.openxmlformats.org/drawingml/2006/chart}strCache')
|
|||
|
|
if str_cache is None:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 提取数据点
|
|||
|
|
pt_count_elem = str_cache.find('{http://schemas.openxmlformats.org/drawingml/2006/chart}ptCount')
|
|||
|
|
pt_count = int(pt_count_elem.get('val')) if pt_count_elem is not None else 0
|
|||
|
|
|
|||
|
|
pts = str_cache.findall('{http://schemas.openxmlformats.org/drawingml/2006/chart}pt')
|
|||
|
|
|
|||
|
|
if not pts:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 创建新的 numCache 元素
|
|||
|
|
num_cache = etree.Element(f"{{{NAMESPACES['c']}}}numCache")
|
|||
|
|
|
|||
|
|
# 添加格式代码
|
|||
|
|
format_code = etree.SubElement(num_cache, f"{{{NAMESPACES['c']}}}formatCode")
|
|||
|
|
format_code.text = self.number_format if self.number_format else "yyyy/mm"
|
|||
|
|
|
|||
|
|
# 添加点计数
|
|||
|
|
new_pt_count = etree.SubElement(num_cache, f"{{{NAMESPACES['c']}}}ptCount")
|
|||
|
|
new_pt_count.set('val', str(pt_count))
|
|||
|
|
|
|||
|
|
# 复制所有数据点
|
|||
|
|
for pt in pts:
|
|||
|
|
idx = pt.get('idx')
|
|||
|
|
v_elem = pt.find('{http://schemas.openxmlformats.org/drawingml/2006/chart}v')
|
|||
|
|
if v_elem is not None and v_elem.text:
|
|||
|
|
# 创建新的 pt 元素
|
|||
|
|
new_pt = etree.SubElement(num_cache, f"{{{NAMESPACES['c']}}}pt")
|
|||
|
|
new_pt.set('idx', idx)
|
|||
|
|
|
|||
|
|
# 添加值(保持数字格式)
|
|||
|
|
new_v = etree.SubElement(new_pt, f"{{{NAMESPACES['c']}}}v")
|
|||
|
|
new_v.text = v_elem.text
|
|||
|
|
|
|||
|
|
# 获取 strCache 在父元素中的位置
|
|||
|
|
str_cache_index = list(cat_elem).index(str_cache)
|
|||
|
|
|
|||
|
|
# 删除旧的 strCache
|
|||
|
|
cat_elem.remove(str_cache)
|
|||
|
|
|
|||
|
|
# 在相同位置插入新的 numCache
|
|||
|
|
cat_elem.insert(str_cache_index, num_cache)
|
|||
|
|
converted_count += 1
|
|||
|
|
|
|||
|
|
if converted_count > 0:
|
|||
|
|
print(f" → 已将 {converted_count} 个分类数据转换为 numCache(数值格式)")
|
|||
|
|
|
|||
|
|
def _convert_catax_to_dateax(self, chart_element, cat_ax):
|
|||
|
|
"""
|
|||
|
|
将 catAx (分类轴) 转换为 dateAx (日期轴)
|
|||
|
|
|
|||
|
|
这是关键修复!PowerPoint 不允许在 catAx 中使用时间单位元素(baseTimeUnit, majorTimeUnit 等)。
|
|||
|
|
必须将整个 catAx 元素替换为 dateAx。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
chart_element: 图表的根 XML 元素
|
|||
|
|
cat_ax: 要转换的 catAx 元素
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
转换后的 dateAx 元素
|
|||
|
|
"""
|
|||
|
|
from lxml import etree
|
|||
|
|
|
|||
|
|
# 创建新的 dateAx 元素
|
|||
|
|
date_ax = etree.Element(f"{{{NAMESPACES['c']}}}dateAx")
|
|||
|
|
|
|||
|
|
# 复制 catAx 的所有子元素到 dateAx
|
|||
|
|
# 移除不兼容的元素(如 lblAlgn, lblOffset, noMultiLvlLbl)
|
|||
|
|
# 保留重要元素(如 numFmt, txPr 等)
|
|||
|
|
skip_elements = ['lblAlgn', 'lblOffset', 'noMultiLvlLbl']
|
|||
|
|
|
|||
|
|
for child in cat_ax:
|
|||
|
|
tag_name = child.tag.split('}')[-1] if '}' in child.tag else child.tag
|
|||
|
|
if tag_name not in skip_elements:
|
|||
|
|
# 深度复制元素(包括所有子元素)
|
|||
|
|
new_child = etree.Element(child.tag, child.attrib)
|
|||
|
|
new_child.text = child.text
|
|||
|
|
new_child.tail = child.tail
|
|||
|
|
|
|||
|
|
# 复制所有子元素
|
|||
|
|
for subchild in child:
|
|||
|
|
new_child.append(subchild)
|
|||
|
|
|
|||
|
|
date_ax.append(new_child)
|
|||
|
|
|
|||
|
|
# 在父元素中替换 catAx 为 dateAx
|
|||
|
|
parent = cat_ax.getparent()
|
|||
|
|
if parent is not None:
|
|||
|
|
parent_list = list(parent)
|
|||
|
|
cat_ax_index = parent_list.index(cat_ax)
|
|||
|
|
parent.remove(cat_ax)
|
|||
|
|
parent.insert(cat_ax_index, date_ax)
|
|||
|
|
|
|||
|
|
return date_ax
|
|||
|
|
|
|||
|
|
def _set_or_update_element(self, parent, tag_name: str, value: str):
|
|||
|
|
"""
|
|||
|
|
设置或更新 XML 元素
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
parent: 父元素
|
|||
|
|
tag_name: 标签名(不含命名空间)
|
|||
|
|
value: 属性值
|
|||
|
|
"""
|
|||
|
|
full_tag = f"{{{NAMESPACES['c']}}}{tag_name}"
|
|||
|
|
|
|||
|
|
# 查找现有元素
|
|||
|
|
existing = parent.find(f'.//c:{tag_name}', namespaces=NAMESPACES)
|
|||
|
|
|
|||
|
|
if existing is not None:
|
|||
|
|
# 更新现有元素
|
|||
|
|
existing.set('val', value)
|
|||
|
|
else:
|
|||
|
|
# 创建新元素
|
|||
|
|
new_element = etree.Element(full_tag)
|
|||
|
|
new_element.set('val', value)
|
|||
|
|
|
|||
|
|
# ⭐ 关键修复:时间单位元素必须在最后(auto 之后)
|
|||
|
|
# OOXML 规范的 dateAx 元素顺序:
|
|||
|
|
# axId → scaling → delete → axPos → majorTickMark → minorTickMark → tickLblPos → numFmt → crossAx → crosses → auto → [时间单位]
|
|||
|
|
|
|||
|
|
# 策略:在 auto 之后插入,如果没有 auto 则追加到最后
|
|||
|
|
auto_elements = parent.findall('.//c:auto', namespaces=NAMESPACES)
|
|||
|
|
if auto_elements and len(auto_elements) > 0:
|
|||
|
|
# 在 auto 之后插入
|
|||
|
|
auto_elem = auto_elements[0]
|
|||
|
|
parent_list = list(parent)
|
|||
|
|
auto_index = parent_list.index(auto_elem)
|
|||
|
|
parent.insert(auto_index + 1, new_element)
|
|||
|
|
else:
|
|||
|
|
# 如果没有 auto,追加到最后
|
|||
|
|
parent.append(new_element)
|
|||
|
|
|
|||
|
|
def _set_date_range(self, cat_ax):
|
|||
|
|
"""设置日期范围(最小值/最大值)"""
|
|||
|
|
# 查找或创建 scaling 元素
|
|||
|
|
scaling = cat_ax.find('.//c:scaling', namespaces=NAMESPACES)
|
|||
|
|
|
|||
|
|
if scaling is None:
|
|||
|
|
scaling = etree.Element(f"{{{NAMESPACES['c']}}}scaling")
|
|||
|
|
# 插入到合适位置(通常在 axId 之前)
|
|||
|
|
ax_id_elements = cat_ax.findall('.//c:axId', namespaces=NAMESPACES)
|
|||
|
|
if ax_id_elements:
|
|||
|
|
ax_id_elements[0].addprevious(scaling)
|
|||
|
|
else:
|
|||
|
|
cat_ax.insert(0, scaling)
|
|||
|
|
|
|||
|
|
# 设置最小值
|
|||
|
|
if self.min_date:
|
|||
|
|
min_val = excel_date(self.min_date)
|
|||
|
|
self._set_scaling_value(scaling, 'min', str(min_val))
|
|||
|
|
print(f" - 最小日期: {self.min_date.strftime('%Y-%m-%d')} (Excel: {min_val})")
|
|||
|
|
|
|||
|
|
# 设置最大值
|
|||
|
|
if self.max_date:
|
|||
|
|
max_val = excel_date(self.max_date)
|
|||
|
|
self._set_scaling_value(scaling, 'max', str(max_val))
|
|||
|
|
print(f" - 最大日期: {self.max_date.strftime('%Y-%m-%d')} (Excel: {max_val})")
|
|||
|
|
|
|||
|
|
def _set_scaling_value(self, scaling_element, tag_name: str, value: str):
|
|||
|
|
"""在 scaling 元素中设置 min/max"""
|
|||
|
|
full_tag = f"{{{NAMESPACES['c']}}}{tag_name}"
|
|||
|
|
|
|||
|
|
existing = scaling_element.find(f'.//c:{tag_name}', namespaces=NAMESPACES)
|
|||
|
|
|
|||
|
|
if existing is not None:
|
|||
|
|
existing.set('val', value)
|
|||
|
|
else:
|
|||
|
|
new_element = etree.Element(full_tag)
|
|||
|
|
new_element.set('val', value)
|
|||
|
|
scaling_element.append(new_element)
|
|||
|
|
|
|||
|
|
def _set_date_format(self, axis_element, format_code: str):
|
|||
|
|
"""
|
|||
|
|
设置日期轴的数字格式(numFmt)
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
axis_element: 轴元素(dateAx 或 catAx)
|
|||
|
|
format_code: Excel 格式代码(如 "yyyy/mm", "mm-dd" 等)
|
|||
|
|
"""
|
|||
|
|
from lxml import etree
|
|||
|
|
|
|||
|
|
# 查找或创建 numFmt 元素
|
|||
|
|
numfmt = axis_element.find('.//c:numFmt', namespaces=NAMESPACES)
|
|||
|
|
|
|||
|
|
if numfmt is None:
|
|||
|
|
# 创建新的 numFmt 元素
|
|||
|
|
numfmt = etree.Element(f"{{{NAMESPACES['c']}}}numFmt")
|
|||
|
|
|
|||
|
|
# ⭐ 关键:numFmt 应该在 tickLblPos 之后、crossAx 之前插入
|
|||
|
|
# 正确顺序:axId → scaling → delete → axPos → majorTickMark → minorTickMark → tickLblPos → numFmt → crossAx → crosses → auto → [时间单位]
|
|||
|
|
tick_lbl_pos_elements = axis_element.findall('.//c:tickLblPos', namespaces=NAMESPACES)
|
|||
|
|
if tick_lbl_pos_elements:
|
|||
|
|
tick_lbl_pos = tick_lbl_pos_elements[0]
|
|||
|
|
parent_list = list(axis_element)
|
|||
|
|
pos_index = parent_list.index(tick_lbl_pos)
|
|||
|
|
axis_element.insert(pos_index + 1, numfmt)
|
|||
|
|
else:
|
|||
|
|
# Fallback:在 crossAx 之前
|
|||
|
|
cross_ax_elements = axis_element.findall('.//c:crossAx', namespaces=NAMESPACES)
|
|||
|
|
if cross_ax_elements:
|
|||
|
|
cross_ax = cross_ax_elements[0]
|
|||
|
|
parent_list = list(axis_element)
|
|||
|
|
cross_ax_index = parent_list.index(cross_ax)
|
|||
|
|
axis_element.insert(cross_ax_index, numfmt)
|
|||
|
|
else:
|
|||
|
|
# 最后的fallback:追加到最后
|
|||
|
|
axis_element.append(numfmt)
|
|||
|
|
|
|||
|
|
# 设置格式代码和 sourceLinked
|
|||
|
|
numfmt.set('formatCode', format_code)
|
|||
|
|
numfmt.set('sourceLinked', '0') # 不链接到数据源
|
|||
|
|
|
|||
|
|
def _set_date_range_from_data(self, date_ax, chart_element):
|
|||
|
|
"""
|
|||
|
|
从数据中提取日期范围并设置到 dateAx 的 scaling 中
|
|||
|
|
|
|||
|
|
这是关键修复!PowerPoint 的 dateAx 默认从 0(1900-01-01)开始计算。
|
|||
|
|
必须明确设置 min 和 max 值来指定实际的日期范围。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
date_ax: dateAx 元素
|
|||
|
|
chart_element: 图表根元素
|
|||
|
|
"""
|
|||
|
|
from lxml import etree
|
|||
|
|
|
|||
|
|
# 查找第一个 cat 元素中的 numCache 或 numRef
|
|||
|
|
cat_elements = chart_element.findall('.//c:cat', namespaces=NAMESPACES)
|
|||
|
|
|
|||
|
|
if not cat_elements:
|
|||
|
|
print(" ⚠️ 未找到分类数据,跳过日期范围设置")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
cat_elem = cat_elements[0]
|
|||
|
|
|
|||
|
|
# 尝试从 numCache 中提取日期值
|
|||
|
|
num_cache = cat_elem.find('.//c:numCache', namespaces=NAMESPACES)
|
|||
|
|
if num_cache is not None:
|
|||
|
|
pt_elements = num_cache.findall('.//c:pt', namespaces=NAMESPACES)
|
|||
|
|
if pt_elements:
|
|||
|
|
# 获取第一个和最后一个日期值
|
|||
|
|
first_pt = pt_elements[0]
|
|||
|
|
last_pt = pt_elements[-1]
|
|||
|
|
|
|||
|
|
first_v = first_pt.find('c:v', namespaces=NAMESPACES)
|
|||
|
|
last_v = last_pt.find('c:v', namespaces=NAMESPACES)
|
|||
|
|
|
|||
|
|
if first_v is not None and last_v is not None:
|
|||
|
|
try:
|
|||
|
|
min_val = float(first_v.text)
|
|||
|
|
max_val = float(last_v.text)
|
|||
|
|
|
|||
|
|
# 转换为日期显示
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
min_date = datetime(1899, 12, 30) + timedelta(days=min_val)
|
|||
|
|
max_date = datetime(1899, 12, 30) + timedelta(days=max_val)
|
|||
|
|
|
|||
|
|
# 设置 scaling 的 min 和 max
|
|||
|
|
self._set_scaling_min_max(date_ax, min_val, max_val)
|
|||
|
|
|
|||
|
|
print(f" ✅ 已设置日期范围: {min_date.strftime('%Y-%m-%d')} ~ {max_date.strftime('%Y-%m-%d')}")
|
|||
|
|
print(f" (Excel 序列号: {min_val} ~ {max_val})")
|
|||
|
|
return
|
|||
|
|
except (ValueError, AttributeError) as e:
|
|||
|
|
print(f" ⚠️ 提取日期范围失败: {e}")
|
|||
|
|
|
|||
|
|
print(" ⚠️ 未能从数据中提取日期范围")
|
|||
|
|
|
|||
|
|
def _set_scaling_min_max(self, axis_element, min_val, max_val):
|
|||
|
|
"""
|
|||
|
|
设置 scaling 元素的 min 和 max 值
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
axis_element: 轴元素(dateAx 或 catAx)
|
|||
|
|
min_val: 最小值(Excel 日期序列号)
|
|||
|
|
max_val: 最大值(Excel 日期序列号)
|
|||
|
|
"""
|
|||
|
|
from lxml import etree
|
|||
|
|
|
|||
|
|
# 查找或创建 scaling 元素
|
|||
|
|
scaling = axis_element.find('.//c:scaling', namespaces=NAMESPACES)
|
|||
|
|
|
|||
|
|
if scaling is None:
|
|||
|
|
# 创建 scaling 元素
|
|||
|
|
scaling = etree.Element(f"{{{NAMESPACES['c']}}}scaling")
|
|||
|
|
|
|||
|
|
# scaling 应该在 axId 之后、delete 之前插入
|
|||
|
|
ax_id_elements = axis_element.findall('.//c:axId', namespaces=NAMESPACES)
|
|||
|
|
if ax_id_elements:
|
|||
|
|
ax_id = ax_id_elements[0]
|
|||
|
|
parent_list = list(axis_element)
|
|||
|
|
ax_id_index = parent_list.index(ax_id)
|
|||
|
|
axis_element.insert(ax_id_index + 1, scaling)
|
|||
|
|
else:
|
|||
|
|
axis_element.insert(0, scaling)
|
|||
|
|
|
|||
|
|
# 设置 min 值
|
|||
|
|
self._set_scaling_value(scaling, 'min', str(min_val))
|
|||
|
|
|
|||
|
|
# 设置 max 值
|
|||
|
|
self._set_scaling_value(scaling, 'max', str(max_val))
|
|||
|
|
|
|||
|
|
def _set_tick_label_position(self, axis_element, position: str):
|
|||
|
|
"""
|
|||
|
|
设置刻度标签位置
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
axis_element: 轴元素(catAx 或 dateAx)
|
|||
|
|
position: 位置值('low', 'high', 'nextTo')
|
|||
|
|
"""
|
|||
|
|
# 查找 tickLblPos 元素
|
|||
|
|
tick_lbl_pos = axis_element.find('.//c:tickLblPos', namespaces=NAMESPACES)
|
|||
|
|
|
|||
|
|
if tick_lbl_pos is not None:
|
|||
|
|
# 更新现有元素
|
|||
|
|
tick_lbl_pos.set('val', position)
|
|||
|
|
else:
|
|||
|
|
# 如果不存在,不创建(使用默认值)
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _set_label_interval(self, axis_element, interval: int):
|
|||
|
|
"""
|
|||
|
|
设置标签显示间隔
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
axis_element: 轴元素(catAx 或 dateAx)
|
|||
|
|
interval: 间隔值(每隔多少个数据点显示一个标签)
|
|||
|
|
"""
|
|||
|
|
from lxml import etree
|
|||
|
|
|
|||
|
|
# 查找或创建 tickLblSkip 元素
|
|||
|
|
tick_lbl_skip = axis_element.find('.//c:tickLblSkip', namespaces=NAMESPACES)
|
|||
|
|
|
|||
|
|
if tick_lbl_skip is None:
|
|||
|
|
# 创建新的 tickLblSkip 元素
|
|||
|
|
tick_lbl_skip = etree.Element(f"{{{NAMESPACES['c']}}}tickLblSkip")
|
|||
|
|
|
|||
|
|
# tickLblSkip 应该在 auto 之后插入
|
|||
|
|
auto_elements = axis_element.findall('.//c:auto', namespaces=NAMESPACES)
|
|||
|
|
if auto_elements:
|
|||
|
|
auto_elem = auto_elements[0]
|
|||
|
|
parent_list = list(axis_element)
|
|||
|
|
auto_index = parent_list.index(auto_elem)
|
|||
|
|
axis_element.insert(auto_index + 1, tick_lbl_skip)
|
|||
|
|
else:
|
|||
|
|
# Fallback:追加到最后
|
|||
|
|
axis_element.append(tick_lbl_skip)
|
|||
|
|
|
|||
|
|
# 设置间隔值
|
|||
|
|
tick_lbl_skip.set('val', str(interval))
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# 便捷预设
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
# 每日刻度(适用于短期数据:1-30天)
|
|||
|
|
DAILY_TICKS = DateAxisConfig(
|
|||
|
|
base_unit="days",
|
|||
|
|
major_unit=1.0,
|
|||
|
|
major_unit_scale="days",
|
|||
|
|
number_format="mm-dd",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 每周刻度(适用于中期数据:1-6个月)
|
|||
|
|
WEEKLY_TICKS = DateAxisConfig(
|
|||
|
|
base_unit="days",
|
|||
|
|
major_unit=7.0,
|
|||
|
|
major_unit_scale="days",
|
|||
|
|
number_format="yyyy-mm-dd",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 每双周刻度(适用于季度数据)
|
|||
|
|
BIWEEKLY_TICKS = DateAxisConfig(
|
|||
|
|
base_unit="days",
|
|||
|
|
major_unit=14.0,
|
|||
|
|
major_unit_scale="days",
|
|||
|
|
number_format="yyyy-mm-dd",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 每月刻度(适用于年度数据)
|
|||
|
|
MONTHLY_TICKS = DateAxisConfig(
|
|||
|
|
base_unit="months",
|
|||
|
|
major_unit=1.0,
|
|||
|
|
major_unit_scale="months",
|
|||
|
|
number_format="yyyy-mm",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 每季度刻度(适用于多年数据)
|
|||
|
|
QUARTERLY_TICKS = DateAxisConfig(
|
|||
|
|
base_unit="months",
|
|||
|
|
major_unit=3.0,
|
|||
|
|
major_unit_scale="months",
|
|||
|
|
number_format="yyyy-mm",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 每年刻度(适用于长期数据)
|
|||
|
|
YEARLY_TICKS = DateAxisConfig(
|
|||
|
|
base_unit="years",
|
|||
|
|
major_unit=1.0,
|
|||
|
|
major_unit_scale="years",
|
|||
|
|
number_format="yyyy",
|
|||
|
|
)
|
|||
|
|
|