334 lines
11 KiB
Python
334 lines
11 KiB
Python
|
|
"""
|
|||
|
|
图表样式配置模块
|
|||
|
|
|
|||
|
|
定义默认的颜色方案、线型、标记点等样式。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from typing import Dict, List, Tuple
|
|||
|
|
from lxml import etree
|
|||
|
|
|
|||
|
|
from .oxml_ns import NAMESPACES
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# 颜色方案定义(RGB 格式)
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
# 深色系列(主色)
|
|||
|
|
DARK_RED = "C00000" # 深红色
|
|||
|
|
DARK_GRAY = "595959" # 深灰色
|
|||
|
|
DARK_BLUE = "0070C0" # 深蓝色
|
|||
|
|
DARK_ORANGE = "ED7D31" # 深橙色
|
|||
|
|
|
|||
|
|
# 浅色系列(辅助色)
|
|||
|
|
LIGHT_RED = "FF9999" # 浅红色
|
|||
|
|
LIGHT_GRAY = "BFBFBF" # 浅灰色
|
|||
|
|
LIGHT_BLUE = "9DC3E6" # 浅蓝色
|
|||
|
|
LIGHT_ORANGE = "F4B183" # 浅橙色
|
|||
|
|
|
|||
|
|
# ⭐ aim00 风格颜色(对比色方案:浅灰柱状图 + 深红折线)
|
|||
|
|
AIM00_LIGHT_GRAY = "C0C0C0" # 浅灰色 - 用于柱状图/面积图(第一系列)
|
|||
|
|
AIM00_DARK_RED = "C00000" # 深红色 - 用于折线图(第二系列)
|
|||
|
|
AIM00_DARK_BLUE = "305496" # 深蓝色 - 备用
|
|||
|
|
AIM00_GRAY = "808080" # 中灰色 - 备用
|
|||
|
|
|
|||
|
|
# 默认颜色序列(深色 + 浅色交替)
|
|||
|
|
DEFAULT_COLOR_SEQUENCE = [
|
|||
|
|
DARK_RED,
|
|||
|
|
DARK_BLUE,
|
|||
|
|
DARK_ORANGE,
|
|||
|
|
DARK_GRAY,
|
|||
|
|
LIGHT_RED,
|
|||
|
|
LIGHT_BLUE,
|
|||
|
|
LIGHT_ORANGE,
|
|||
|
|
LIGHT_GRAY,
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# ── 主题配套图表系列色(每个主题一套 8 色) ──
|
|||
|
|
COLOR_SCHEMES = {
|
|||
|
|
"midnight": ["CADCFC", "1E2761", "E8B931", "4A6FA5", "9BB5D6", "B08C28", "8896AB", "484E5C"],
|
|||
|
|
"charcoal": ["A8B4BE", "36454F", "E8B931", "607D8B", "B0BEC5", "C4A035", "78909C", "4A565E"],
|
|||
|
|
"jp_finance": ["A8D5DC", "1B3D6E", "C9A84C", "4A8FB8", "7FBFCF", "A07840", "8FA8C0", "505868"],
|
|||
|
|
"pension_warm": ["A8C4D4", "2E5FA3", "D4903F", "7BA7BC", "B0C9B0", "9C7B48", "8A9DB8", "606870"],
|
|||
|
|
"tech_blue": ["90CAF9", "1565C0", "00BFA5", "7E57C2", "29B6F6", "FF7043", "90A4AE", "546E7A"],
|
|||
|
|
"state_red": ["CDAC60", "8B0000", "B8860B", "4A6FA5", "C07840", "507050", "907878", "585048"],
|
|||
|
|
"esg_green": ["A5D6A7", "1A5C2A", "8BC34A", "00838F", "C8E6C9", "795548", "78909C", "607D8B"],
|
|||
|
|
"dark_pro": ["00BFFF", "FFD700", "00E676", "7B68EE", "FF6D00", "E040FB", "A0B0C0", "4A5868"],
|
|||
|
|
"morningstar": ["A4C2D8", "1D2B3A", "E67E22", "5A9BD5", "95C8D8", "B87333", "7B8794", "495057"],
|
|||
|
|
"macro_research":["AED6F1", "2C3E50", "3498DB", "95A5A6", "85C1E9", "1ABC9C", "BDC3C7", "566573"],
|
|||
|
|
# 兜底别名
|
|||
|
|
"default": ["CADCFC", "1E2761", "E8B931", "4A6FA5", "9BB5D6", "B08C28", "8896AB", "484E5C"],
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# 线型配置
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
# 线宽(EMUs: English Metric Units, 1 pt = 12700 EMUs)
|
|||
|
|
LINE_WIDTH_PT = {
|
|||
|
|
0.5: 6350, # 0.5 pt
|
|||
|
|
0.75: 9525, # 0.75 pt
|
|||
|
|
1.0: 12700, # 1 pt (默认)
|
|||
|
|
1.5: 19050, # 1.5 pt
|
|||
|
|
2.0: 25400, # 2 pt
|
|||
|
|
2.25: 28575, # 2.25 pt
|
|||
|
|
3.0: 38100, # 3 pt
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
DEFAULT_LINE_WIDTH = LINE_WIDTH_PT[1.0] # 1 pt
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# 标记点配置
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
# 标记点样式
|
|||
|
|
MARKER_STYLES = {
|
|||
|
|
"none": None, # 无标记
|
|||
|
|
"circle": "circle", # 圆形
|
|||
|
|
"square": "square", # 方形
|
|||
|
|
"diamond": "diamond", # 菱形
|
|||
|
|
"triangle": "triangle", # 三角形
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
DEFAULT_MARKER_STYLE = "none" # 默认无标记点
|
|||
|
|
DEFAULT_MARKER_SIZE = 5 # 标记点大小(pt)
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# 样式应用函数
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
def apply_series_style(
|
|||
|
|
ser_element,
|
|||
|
|
color: str = None,
|
|||
|
|
line_width: int = None,
|
|||
|
|
marker_style: str = None,
|
|||
|
|
marker_size: int = None,
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
为系列元素应用样式
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
ser_element: 系列元素 (<c:ser>)
|
|||
|
|
color: RGB 颜色(6位十六进制,如 "C00000")
|
|||
|
|
line_width: 线宽(EMUs)
|
|||
|
|
marker_style: 标记点样式("none", "circle", "square" 等)
|
|||
|
|
marker_size: 标记点大小(pt)
|
|||
|
|
"""
|
|||
|
|
# ⭐ 检测图表类型(通过父元素)
|
|||
|
|
parent = ser_element.getparent()
|
|||
|
|
parent_tag = parent.tag.split('}')[1] if '}' in parent.tag else parent.tag
|
|||
|
|
is_area_or_bar = parent_tag in ('barChart', 'areaChart')
|
|||
|
|
|
|||
|
|
# 查找或创建 spPr (shape properties) 元素
|
|||
|
|
spPr = ser_element.find('c:spPr', namespaces=NAMESPACES)
|
|||
|
|
if spPr is None:
|
|||
|
|
# 在 idx/order/tx 之后插入 spPr
|
|||
|
|
insert_index = _find_insert_position(ser_element, 'spPr')
|
|||
|
|
spPr = etree.Element(f"{{{NAMESPACES['c']}}}spPr")
|
|||
|
|
ser_element.insert(insert_index, spPr)
|
|||
|
|
|
|||
|
|
# 应用线条样式(传入图表类型信息)
|
|||
|
|
if line_width is not None or color is not None:
|
|||
|
|
_apply_line_style(spPr, color, line_width, is_area_or_bar)
|
|||
|
|
|
|||
|
|
# 应用标记点样式
|
|||
|
|
if marker_style is not None:
|
|||
|
|
_apply_marker_style(ser_element, marker_style, marker_size, color)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _apply_line_style(spPr, color: str = None, line_width: int = None, is_area_or_bar: bool = False):
|
|||
|
|
"""
|
|||
|
|
应用线条样式
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
spPr: shape properties 元素
|
|||
|
|
color: 颜色
|
|||
|
|
line_width: 线宽
|
|||
|
|
is_area_or_bar: 是否为面积图或柱状图(这些图表类型需要设置填充色而非边框色)
|
|||
|
|
"""
|
|||
|
|
# 清除现有的 ln 元素
|
|||
|
|
for ln in spPr.findall('a:ln', namespaces=NAMESPACES):
|
|||
|
|
spPr.remove(ln)
|
|||
|
|
|
|||
|
|
# 创建新的 ln (line) 元素
|
|||
|
|
ln = etree.SubElement(spPr, f"{{{NAMESPACES['a']}}}ln")
|
|||
|
|
|
|||
|
|
# ⭐ 如果是面积图或柱状图,设置为无边框
|
|||
|
|
if is_area_or_bar:
|
|||
|
|
# 设置为无边框(noFill)
|
|||
|
|
noFill = etree.SubElement(ln, f"{{{NAMESPACES['a']}}}noFill")
|
|||
|
|
|
|||
|
|
# ⭐ 核心修复:为柱状图/面积图添加填充色
|
|||
|
|
if color is not None:
|
|||
|
|
# 清除现有的 solidFill 元素
|
|||
|
|
for fill in spPr.findall('a:solidFill', namespaces=NAMESPACES):
|
|||
|
|
spPr.remove(fill)
|
|||
|
|
|
|||
|
|
# 在 ln 之后添加 solidFill
|
|||
|
|
solidFill = etree.SubElement(spPr, f"{{{NAMESPACES['a']}}}solidFill")
|
|||
|
|
srgbClr = etree.SubElement(solidFill, f"{{{NAMESPACES['a']}}}srgbClr")
|
|||
|
|
srgbClr.set('val', color)
|
|||
|
|
else:
|
|||
|
|
# 折线图/散点图:设置线宽和颜色
|
|||
|
|
if line_width is not None:
|
|||
|
|
ln.set('w', str(line_width))
|
|||
|
|
|
|||
|
|
# 设置线条颜色
|
|||
|
|
if color is not None:
|
|||
|
|
solidFill = etree.SubElement(ln, f"{{{NAMESPACES['a']}}}solidFill")
|
|||
|
|
srgbClr = etree.SubElement(solidFill, f"{{{NAMESPACES['a']}}}srgbClr")
|
|||
|
|
srgbClr.set('val', color)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _apply_marker_style(
|
|||
|
|
ser_element,
|
|||
|
|
marker_style: str,
|
|||
|
|
marker_size: int = None,
|
|||
|
|
color: str = None
|
|||
|
|
):
|
|||
|
|
"""应用标记点样式"""
|
|||
|
|
# 清除现有的 marker 元素
|
|||
|
|
for marker in ser_element.findall('c:marker', namespaces=NAMESPACES):
|
|||
|
|
ser_element.remove(marker)
|
|||
|
|
|
|||
|
|
if marker_style == "none" or marker_style is None:
|
|||
|
|
# 创建无标记的 marker 元素
|
|||
|
|
insert_index = _find_insert_position(ser_element, 'marker')
|
|||
|
|
marker = etree.Element(f"{{{NAMESPACES['c']}}}marker")
|
|||
|
|
ser_element.insert(insert_index, marker)
|
|||
|
|
|
|||
|
|
symbol = etree.SubElement(marker, f"{{{NAMESPACES['c']}}}symbol")
|
|||
|
|
symbol.set('val', 'none')
|
|||
|
|
else:
|
|||
|
|
# 创建有标记的 marker 元素
|
|||
|
|
insert_index = _find_insert_position(ser_element, 'marker')
|
|||
|
|
marker = etree.Element(f"{{{NAMESPACES['c']}}}marker")
|
|||
|
|
ser_element.insert(insert_index, marker)
|
|||
|
|
|
|||
|
|
# 标记样式
|
|||
|
|
symbol = etree.SubElement(marker, f"{{{NAMESPACES['c']}}}symbol")
|
|||
|
|
symbol.set('val', marker_style)
|
|||
|
|
|
|||
|
|
# 标记大小
|
|||
|
|
if marker_size is not None:
|
|||
|
|
size = etree.SubElement(marker, f"{{{NAMESPACES['c']}}}size")
|
|||
|
|
size.set('val', str(marker_size))
|
|||
|
|
|
|||
|
|
# 标记颜色(如果提供)
|
|||
|
|
if color is not None:
|
|||
|
|
spPr = etree.SubElement(marker, f"{{{NAMESPACES['c']}}}spPr")
|
|||
|
|
solidFill = etree.SubElement(spPr, f"{{{NAMESPACES['a']}}}solidFill")
|
|||
|
|
srgbClr = etree.SubElement(solidFill, f"{{{NAMESPACES['a']}}}srgbClr")
|
|||
|
|
srgbClr.set('val', color)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _find_insert_position(parent, target_element: str) -> int:
|
|||
|
|
"""
|
|||
|
|
查找元素的正确插入位置(按照 OOXML 规范的顺序)
|
|||
|
|
|
|||
|
|
OOXML 规范中 <c:ser> 的子元素顺序:
|
|||
|
|
1. idx
|
|||
|
|
2. order
|
|||
|
|
3. tx
|
|||
|
|
4. spPr (shape properties)
|
|||
|
|
5. marker
|
|||
|
|
6. cat / xVal
|
|||
|
|
7. val / yVal
|
|||
|
|
8. smooth
|
|||
|
|
"""
|
|||
|
|
element_order = {
|
|||
|
|
'idx': 0,
|
|||
|
|
'order': 1,
|
|||
|
|
'tx': 2,
|
|||
|
|
'spPr': 3,
|
|||
|
|
'marker': 4,
|
|||
|
|
'cat': 5,
|
|||
|
|
'xVal': 5,
|
|||
|
|
'val': 6,
|
|||
|
|
'yVal': 6,
|
|||
|
|
'smooth': 7,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
target_order = element_order.get(target_element, 99)
|
|||
|
|
|
|||
|
|
# 查找第一个顺序大于 target 的元素位置
|
|||
|
|
for i, child in enumerate(parent):
|
|||
|
|
tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
|
|||
|
|
child_order = element_order.get(tag, 99)
|
|||
|
|
if child_order > target_order:
|
|||
|
|
return i
|
|||
|
|
|
|||
|
|
# 如果没找到,返回末尾位置
|
|||
|
|
return len(parent)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_color_for_series(series_index: int, color_scheme: str = "default") -> str:
|
|||
|
|
"""
|
|||
|
|
为系列获取颜色
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
series_index: 系列索引(0-based)
|
|||
|
|
color_scheme: 颜色方案名称
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
RGB 颜色字符串(6位十六进制)
|
|||
|
|
"""
|
|||
|
|
colors = COLOR_SCHEMES.get(color_scheme, DEFAULT_COLOR_SEQUENCE)
|
|||
|
|
return colors[series_index % len(colors)]
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# 预设样式配置
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
class StyleConfig:
|
|||
|
|
"""样式配置类"""
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
color_scheme: str = "default",
|
|||
|
|
line_width_pt: float = 1.0,
|
|||
|
|
marker_style: str = "none",
|
|||
|
|
marker_size: int = 5,
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
初始化样式配置
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
color_scheme: 颜色方案("default", "dark_only", "light_only" 等)
|
|||
|
|
line_width_pt: 线宽(pt)
|
|||
|
|
marker_style: 标记点样式("none", "circle", "square" 等)
|
|||
|
|
marker_size: 标记点大小(pt)
|
|||
|
|
"""
|
|||
|
|
self.color_scheme = color_scheme
|
|||
|
|
self.line_width = LINE_WIDTH_PT.get(line_width_pt, DEFAULT_LINE_WIDTH)
|
|||
|
|
self.marker_style = marker_style
|
|||
|
|
self.marker_size = marker_size
|
|||
|
|
|
|||
|
|
def apply_to_series(self, ser_element, series_index: int):
|
|||
|
|
"""
|
|||
|
|
将样式应用到系列元素
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
ser_element: 系列元素
|
|||
|
|
series_index: 系列索引(用于选择颜色)
|
|||
|
|
"""
|
|||
|
|
color = get_color_for_series(series_index, self.color_scheme)
|
|||
|
|
|
|||
|
|
apply_series_style(
|
|||
|
|
ser_element,
|
|||
|
|
color=color,
|
|||
|
|
line_width=self.line_width,
|
|||
|
|
marker_style=self.marker_style,
|
|||
|
|
marker_size=self.marker_size,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 默认样式配置
|
|||
|
|
DEFAULT_STYLE_CONFIG = StyleConfig(
|
|||
|
|
color_scheme="default",
|
|||
|
|
line_width_pt=1.0,
|
|||
|
|
marker_style="none",
|
|||
|
|
marker_size=5,
|
|||
|
|
)
|
|||
|
|
|