#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""集团维度 2025vs2026 增减归因图生成器（自包含 HTML → PNG，宽 1440）。
上半：各集团 Δ 双向条形（正绿负红、零线居中）
下半：Top 增集团 / Top 减集团 各一条水泡(瀑布)条，展示「集团 Δ = 各货号 Δ 之和」
零外链、内联 SVG；<title>/<br> 一律不用（用 SVG <text> 逐行摆放）。
"""
import html
import json
import os

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
PLOT = os.path.join(ROOT, '销售分析', '_group_yoy_plot.json')
OUT_HTML = os.path.join(ROOT, '销售分析', '_集团维度_增减归因.html')
OUT_PNG = os.path.join(ROOT, '销售分析', '集团维度_增减归因.png')

GREEN = '#0F9D58'
RED = '#D93025'
BLUE = '#1852B6'
GREY = '#64748B'
INK = '#1F2937'
FONT = 'font-family="-apple-system,PingFang SC,Helvetica,Arial,sans-serif"'


def esc(s):
    return html.escape(str(s), quote=True)


def fmt(v, signed=False):
    return (('%+.1f' if signed else '%.1f') % (v / 10000.0)) + '万'


def _cw(ch):
    """视觉宽度（CJK 单位=1.0，其余≈0.58）"""
    o = ord(ch)
    if 0x2E80 <= o <= 0x9FFF or 0xFF00 <= o <= 0xFF60 or 0x3000 <= o <= 0x303F:
        return 1.0
    return 0.58


def wrap(s, maxw, maxlines=2):
    """按视觉宽度折行；超行用 … 截断（禁用 <br>）。"""
    s = esc(s)
    lines, cur, w = [], '', 0.0
    for ch in s:
        c = _cw(ch)
        if w + c > maxw and cur:
            lines.append(cur)
            cur, w = '', 0.0
        cur += ch
        w += c
    if cur:
        lines.append(cur)
    if len(lines) > maxlines:
        lines = lines[:maxlines]
        lines[-1] = lines[-1][:-1] + '…'
    return lines


def main():
    P = json.load(open(PLOT, encoding='utf-8'))
    ents = P['entities']
    total = P['total_delta']
    W = 1440

    # ══ 上半：各集团 Δ 双向条形 ══════════════════════════════
    pad_x, top_y = 40, 96
    lab_w = 232
    bar_l, bar_r = pad_x + lab_w, W - pad_x
    zero = (bar_l + bar_r) / 2.0
    half = (bar_r - bar_l) / 2.0 - 6
    maxabs = max(abs(e['d']) for e in ents) or 1.0
    reserve = 76                     # 给条形末端数值标签留位，防右边缘裁切
    sc = (half - reserve) / maxabs
    rowh, gap = 30, 8
    h_top = top_y + len(ents) * (rowh + gap) + 30

    s = ['<svg width="%d" height="%d" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" %s>'
         % (W, h_top, W, h_top, FONT)]
    s.append('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1"/>'
             % (zero, top_y - 10, zero, h_top - 22, GREY))
    s.append('<text x="%.1f" y="%.1f" font-size="11" fill="%s" text-anchor="middle">0</text>'
             % (zero, top_y - 40, GREY))
    s.append('<text x="%.1f" y="%.1f" font-size="12" fill="%s" text-anchor="end">← 减少（万元）</text>'
             % (zero - 8, top_y - 40, RED))
    s.append('<text x="%.1f" y="%.1f" font-size="12" fill="%s" text-anchor="start">增加（万元）→</text>'
             % (zero + 8, top_y - 40, GREEN))
    for i, e in enumerate(ents):
        y = top_y + i * (rowh + gap)
        cy = y + rowh / 2.0
        d = e['d']
        col = GREEN if d >= 0 else RED
        blen = abs(d) * sc
        s.append('<text x="%d" y="%.1f" font-size="13" fill="%s" dominant-baseline="middle">%s</text>'
                 % (pad_x, cy, INK, esc(e['g'])))
        if d >= 0:
            x1, x2 = zero, zero + blen
            s.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%d" rx="3" fill="%s" fill-opacity="0.92"/>'
                     % (x1, y, max(blen, 0.9), rowh, col))
            s.append('<text x="%.1f" y="%.1f" font-size="12" fill="%s" dominant-baseline="middle">%s</text>'
                     % (x2 + 6, cy, col, fmt(d, True)))
        else:
            x1, x2 = zero - blen, zero
            s.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%d" rx="3" fill="%s" fill-opacity="0.92"/>'
                     % (x1, y, max(blen, 0.9), rowh, col))
            # ★ 负向标签贴左端（x1）外侧：贴零线会压在条上
            s.append('<text x="%.1f" y="%.1f" font-size="12" fill="%s" text-anchor="end" dominant-baseline="middle">%s</text>'
                     % (x1 - 6, cy, col, fmt(d, True)))
    s.append('</svg>')
    svg_top = '\n'.join(s)

    # ══ 下半：Top 增 / Top 减 集团 瀑布条 ════════════════════
    panels = []
    for key, title, tcol in ((P['top_inc'], '增量最大集团', GREEN),
                             (P['top_dec'], '减量最大集团', RED)):
        skus = sorted(P['sku_by_grp'].get(key, []), key=lambda r: -abs(r['d']))
        top5 = skus[:5]
        rest = sum(r['d'] for r in skus[5:])
        bars = [{'n': r['sku'], 'd': r['d']} for r in top5]
        if abs(rest) > 0.5:
            bars.append({'n': '其他%d个货号合计' % max(len(skus) - 5, 0), 'd': round(rest, 2)})
        bars.append({'n': '集团Δ合计', 'd': round(sum(r['d'] for r in skus), 2), 'total': True})
        panels.append((key, title, tcol, bars))

    pw = (W - pad_x * 2 - 40) / 2.0
    ph = 320
    h_bot = ph + 96
    s = ['<svg width="%d" height="%d" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" %s>'
         % (W, h_bot, W, h_bot, FONT)]
    for pi, (key, title, tcol, bars) in enumerate(panels):
        ox = pad_x + pi * (pw + 40)
        oy = 52
        s.append('<text x="%.1f" y="%.1f" font-size="15" font-weight="700" fill="%s">%s：%s</text>'
                 % (ox, oy - 22, BLUE, title, esc(key)))
        plot_h = ph - 46
        area_y0, area_y1 = oy, oy + plot_h
        maxpos = max([0] + [b['d'] for b in bars if b['d'] > 0]) or 1
        minneg = min([0] + [b['d'] for b in bars if b['d'] < 0])
        rng = (maxpos - minneg) or 1
        k = plot_h / rng * 0.84
        yzero = area_y0 + maxpos * k + plot_h * 0.08

        def yv(v, _z=yzero, _k=k):
            return _z - v * _k

        n = len(bars)
        slot = pw / n
        bw = min(slot * 0.54, 82)
        s.append('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1" stroke-dasharray="3 3"/>'
                 % (ox, yzero, ox + pw, yzero, GREY))
        for bi, b in enumerate(bars):
            cx = ox + slot * (bi + 0.5)
            x = cx - bw / 2.0
            d = b['d']
            is_total = b.get('total')
            col = BLUE if is_total else (GREEN if d >= 0 else RED)
            y_a, y_b = yv(0), yv(d)
            y_top, hh = min(y_a, y_b), max(abs(y_b - y_a), 1.0)
            s.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="3" fill="%s" fill-opacity="%s"/>'
                     % (x, y_top, bw, hh, col, '0.95' if is_total else '0.9'))
            lab_y = (y_top - 7) if d >= 0 else (y_top + hh + 16)   # 始终在条形外侧
            s.append('<text x="%.1f" y="%.1f" font-size="11.5" fill="%s" text-anchor="middle" font-weight="%s">%s</text>'
                     % (cx, lab_y, col, '700' if is_total else '500', fmt(d, True)))
            for li, ln in enumerate(wrap(b['n'], 9.8, 2)):
                s.append('<text x="%.1f" y="%.1f" font-size="10" fill="%s" text-anchor="middle" '
                         'font-weight="%s">%s</text>'
                         % (cx, area_y1 + 18 + li * 13, INK, '700' if is_total else '400', ln))
    s.append('</svg>')
    svg_bot = '\n'.join(s)

    sub = '主口径：2026-01-01~09-30 vs 2025-01-01~09-30（期末月日对齐）｜ 标准销售订单·已审核·非赠品·排除关联方'
    note = ('口径：集团归集用「集团客户#」列（无值回退客户名）；金额=价税合计；空货号记「（未填货号）」不丢。'
            '全量 Δ = %s。附口径（2025 全年 vs 2026 前 9 月）口径不可比，仅供参考，见 Excel。' % fmt(total, True))
    doc = ('<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">'
           '<style>body{margin:0;background:#fff;color:%s;'
           'font-family:-apple-system,PingFang SC,Helvetica,Arial,sans-serif}'
           '.wrap{width:%dpx;margin:0 auto;padding:26px 0 10px}'
           'h1{font-size:22px;margin:0 0 4px 40px}'
           '.sub{font-size:12.5px;color:%s;margin:0 0 16px 40px}'
           '.sec{font-size:15px;font-weight:700;margin:12px 0 2px 40px;color:%s}'
           '.note{font-size:11.5px;color:%s;margin:10px 40px 24px;line-height:1.7}'
           '</style></head><body><div class="wrap">'
           '<h1>集团维度 2025 vs 2026 同期增减归因（货号驱动）</h1>'
           '<div class="sub">%s</div>'
           '<div class="sec">一、各集团增减（Δ 双向条形）</div>%s'
           '<div class="sec">二、集团 Δ 拆解到货号（Top5 + 其他，瀑布条）</div>%s'
           '<div class="note">%s</div>'
           '</div></body></html>') % (INK, W, GREY, BLUE, GREY, esc(sub), svg_top, svg_bot, esc(note))

    with open(OUT_HTML, 'w', encoding='utf-8') as f:
        f.write(doc)
    print('✅ →', OUT_HTML)


if __name__ == '__main__':
    main()
