""" 图表样式配置模块 定义默认的颜色方案、线型、标记点等样式。 """ 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: 系列元素 () 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 规范中 的子元素顺序: 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, )