#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""科研经费 × 宝锐产品线：TAB 式单页（合并 总览/矩阵/单位/课题组PI/战略/mRNA/口径）
输出：销售分析/科研经费与宝锐产品线关联_统一口径_2026-10-17.html
"""
import io
import json
import sys

sys.path.insert(0, '/Users/liuxinyuan/Desktop/Hermes输出-工作类/脚本')
import brand_kit as BK  # noqa: E402

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
J1 = W + '/数据/科研经费_统一口径_产品线.json'
J2 = W + '/数据/科研经费_课题组PI级.json'
J3 = W + '/数据/战略突破_项目PI清单.json'
J4 = W + '/数据/mrna_pubmed_pi.json'
OUT = W + '/销售分析/科研经费与宝锐产品线关联_统一口径_2026-10-17.html'
L = ['分子·RT-qPCR', '分子·PCR/克隆', '分子·核酸提取', '分子·LAMP/恒温', '分子·其他', 'WB']

d1 = json.load(io.open(J1, encoding='utf-8'))
d2 = json.load(io.open(J2, encoding='utf-8'))
d3 = json.load(io.open(J3, encoding='utf-8'))
d4 = json.load(io.open(J4, encoding='utf-8'))
P = d1['provinces']; S = d1['struct']

NON = {
    '蛋白/信号/修饰': {'mRNA': 2, '细胞': 35, '抗体/免疫': 40, '测序/组学': 15, '动物/表型': 8},
    '肿瘤': {'mRNA': 3, '细胞': 30, '抗体/免疫': 35, '测序/组学': 20, '动物/表型': 12},
    '免疫/感染/炎症': {'mRNA': 8, '细胞': 30, '抗体/免疫': 35, '测序/组学': 15, '动物/表型': 12},
    '神经科学': {'mRNA': 3, '细胞': 30, '抗体/免疫': 30, '测序/组学': 20, '动物/表型': 17},
    '代谢/内分泌': {'mRNA': 3, '细胞': 30, '抗体/免疫': 35, '测序/组学': 20, '动物/表型': 12},
    '遗传/表观/基因组': {'mRNA': 5, '细胞': 20, '抗体/免疫': 20, '测序/组学': 45, '动物/表型': 10},
    '生物材料/传感器': {'mRNA': 2, '细胞': 45, '抗体/免疫': 20, '测序/组学': 10, '动物/表型': 23},
    '细胞生物学': {'mRNA': 3, '细胞': 40, '抗体/免疫': 30, '测序/组学': 15, '动物/表型': 12},
    '药理/中药': {'mRNA': 2, '细胞': 30, '抗体/免疫': 30, '测序/组学': 15, '动物/表型': 23},
    '临床/流行病': {'mRNA': 3, '细胞': 25, '抗体/免疫': 35, '测序/组学': 20, '动物/表型': 17},
    '农学/动植': {'mRNA': 3, '细胞': 25, '抗体/免疫': 20, '测序/组学': 25, '动物/表型': 27},
    '其他生医': {'mRNA': 3, '细胞': 30, '抗体/免疫': 30, '测序/组学': 22, '动物/表型': 15},
}


def full_share(tp):
    rate, mix = S[tp]['rate'], S[tp]['mix']
    o = {k: rate * mix[k] / 100 for k in L}
    rest = 1 - rate
    for k, v in NON[tp].items():
        o[k] = rest * v / 100
    return o


def matrix(prov=None):
    rows = []
    tp_source = P['浙江']['by_topic'] if prov == '浙江' else P['广东']['by_topic'] if prov == '广东' else None
    topics = sorted(set(P['浙江']['by_topic']) | set(P['广东']['by_topic']))
    for t in topics:
        if tp_source is not None:
            amt = tp_source.get(t, {'amt': 0})['amt']
        else:
            amt = P['浙江']['by_topic'].get(t, {'amt': 0})['amt'] + P['广东']['by_topic'].get(t, {'amt': 0})['amt']
        if amt <= 0:
            continue
        fs = full_share(t)
        cols = [round(amt * fs[k]) for k in ['分子·RT-qPCR', '分子·PCR/克隆', '分子·核酸提取', '分子·LAMP/恒温',
                                              '分子·其他', 'WB', 'mRNA', '细胞', '抗体/免疫', '测序/组学', '动物/表型']]
        rate = S[t]['rate']
        rows.append([t] + [round(amt)] + cols + [round(amt * rate), round(rate * 100)])
    rows.sort(key=lambda r: -r[1])
    tot = ['合计', sum(r[1] for r in rows)] + [sum(r[2 + i] for r in rows) for i in range(11)] + \
          [sum(r[13] for r in rows), round(sum(r[13] for r in rows) / max(1, sum(r[1] for r in rows)) * 100)]
    rows.append([tot[0]] + tot[1:])
    return rows


pis = [[x['prov'], x['unit'], x['pi'], x['n'], round(x['amt']), round(x['addr'], 1), round(x['rate'] * 100),
        x['main'], x['topic']] for x in d2['rows'] if x['addr'] > 0]

import re as _re


def unit_type(u):
    if _re.search(r'(医院|卫生院|疾控|防治中心)', u):
        return '医院'
    if _re.search(r'(大学|学院|学校)', u):
        return '高校'
    if _re.search(r'(研究院|研究所|实验室|科学院|中心)', u):
        return '院所'
    if _re.search(r'(公司|集团|企业)', u):
        return '企业'
    return '其他'


units = []
for prov in ('广东', '浙江'):
    for u in P[prov]['units']:
        units.append([prov, u['unit'], unit_type(u['unit']), u['n'], round(u['amt']), round(u['bio_amt']), round(u['addr']),
                      round(u['addr'] / u['amt'] * 100 if u['amt'] else 0)] + [round(u['lines'][l]) for l in L])

strat_om = [[u['prov'], u['unit'], '—', u['n'], round(u['amt']), '单位汇总', '', ''] for u in d3['omics']['units'][:12]] + \
           [[p['prov'], p['unit'], p['pi'], p['n'], round(p['amt']), p['cat'] if len(p['proj']) == 1 else '/'.join(p['hit']),
             (p['proj'][0] if p['proj'] else '')[:70], ''] for p in d3['omics']['pis']]
strat_mr = [[u['prov'], u['unit'], '—', u['n'], round(u['amt']), '单位汇总', '', ''] for u in d3['mrna']['units'][:12]] + \
           [[p['prov'], p['unit'], p['pi'], p['n'], round(p['amt']), p['cat'] if len(p['proj']) == 1 else '/'.join(p['hit']),
             (p['proj'][0] if p['proj'] else '')[:70], ''] for p in d3['mrna']['pis']]

pub = [[x['inst'], x['pi'], x['n'], '/'.join(x['nsfc']) or '—', '/'.join(x['years']),
        (x['titles'][0] if x['titles'] else '')[:80], '/'.join(x['pmids'][:2])] for x in
       (d4['gd']['pis'] + d4['zj']['pis'])]
pub = [['广东' if i < len(d4['gd']['pis']) else '浙江'] + r for i, r in enumerate(pub)]

TECHS = [
    ['Western blot', '6.8%', '85–90%', '95%', 'WB 一线全线（RIPA/抑制剂/BCA/上样/电泳/转膜/TBST/膜/ECL/marker）', '高'],
    ['qPCR / RT-qPCR', '5.2%', '60–70%', '80%', 'RT-qPCR 预混 + 逆转录 + 核酸提取', '高'],
    ['免疫沉淀 IP/Co-IP', '4.5%', '20–30%', '65%', 'WB 线（裂解液、蛋白酶抑制剂）+ 蛋白纯化缺口', '中高'],
    ['免疫荧光 IF', '4.5%', '50–60%', '20%', '仅覆盖封闭/抗体稀释；抗体非我司', '低'],
    ['免疫组化 IHC', '3.1%', '20–30%', '15%', '同上，石蜡切片耗材非我司', '低'],
    ['流式细胞术', '3.5%', '20–25%', '5%', '几乎无对应货号', '极低'],
    ['ELISA', '2.7%', '15–20%', '15%', '仅缓冲液/封闭', '低'],
    ['RNA-seq', '7.9%', '30–40%', '25%', '仅核酸提取', '低'],
    ['质谱/蛋白组', '7.5%', '15–20%', '10%', '仅裂解/提取', '极低'],
    ['CRISPR/基因编辑', '1.7%', '10–15%', '75%', '无缝克隆/连接/感受态/质粒提取', '高'],
]
DATA = {
    'lines': L,
    'techs': TECHS,
    'matrixAll': matrix(), 'matrixZJ': matrix('浙江'), 'matrixGD': matrix('广东'),
    'units': units, 'pis': pis,
    'stratOm': strat_om, 'stratMr': strat_mr, 'pubmed': pub,
    'kpi': {
        'total': 92020, 'bio': 51910, 'addr': 28278,
        'zjTotal': 49910, 'zjAddr': 15224, 'gdTotal': 42110, 'gdAddr': 13054,
        'groups': len(d2['rows']), 'piRows': len(pis),
        'pubGd': d4['gd']['n'], 'pubZj': d4['zj']['n'],
        'omN': d3['omics']['n'], 'omAmt': d3['omics']['amt'], 'mrN': d3['mrna']['n'], 'mrAmt': d3['mrna']['amt'],
    },
}

CSS = """
:root{--pri:#1852B6;--pri2:#1F4E79;--bg:#f4f6fa;--line:#e3e8f1;--sub:#5f6b7a}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:#1b2430;font-size:13px;line-height:1.6}
.wrap{max-width:1360px;margin:0 auto;padding:0 18px 40px}
h1{font-size:21px;margin:14px 0 4px}
.meta{color:var(--sub);font-size:12.2px;margin-bottom:12px}
.tabs{display:flex;gap:4px;flex-wrap:wrap;border-bottom:2px solid var(--line);margin:10px 0 14px}
.tab{padding:9px 15px;cursor:pointer;border:none;background:transparent;font-size:13.2px;color:#43505f;border-radius:8px 8px 0 0;font-weight:600}
.tab:hover{background:#eaf0fa;color:var(--pri)}
.tab.on{background:var(--pri);color:#fff}
.panel{display:none}.panel.on{display:block}
.card{background:#fff;border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin-bottom:14px}
.card h2{font-size:15px;margin:0 0 10px;padding-left:9px;border-left:3px solid var(--pri2)}
.card h2 span{font-size:10.8px;color:#8a94a3;font-weight:600;margin-left:6px}
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;margin:10px 0 14px}
.kpi{background:#fff;border:1px solid var(--line);border-top:3px solid var(--pri);border-radius:9px;padding:9px 11px}
.kl{font-size:11px;color:#6b7684}.kv{font-size:19px;font-weight:800;color:var(--pri);margin:2px 0}.ks{font-size:10.5px;color:#8a94a3}
.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:0 0 10px}
input,select{padding:7px 10px;border:1px solid #cfd8e6;border-radius:7px;font-size:12.4px;background:#fff}
input{width:260px}select{min-width:110px}
.btn{padding:7px 12px;border:1px solid var(--pri);background:var(--pri);color:#fff;border-radius:7px;cursor:pointer;font-size:12.4px}
.btn.ghost{background:#fff;color:var(--pri)}
.tbl-wrap{overflow-x:auto;max-height:640px;overflow-y:auto;border:1px solid var(--line);border-radius:8px}
table{width:100%;border-collapse:collapse;font-size:11.7px}
th{background:var(--pri2);color:#fff;padding:7px 7px;text-align:left;position:sticky;top:0;cursor:pointer;white-space:nowrap;z-index:2}
th.n{text-align:right}th:hover{background:#173d61}
td{padding:6px 7px;border-bottom:1px solid #eef1f6;vertical-align:top}
td.n{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
td.pj{color:#6b7684;font-size:11.2px}
tr:hover td{background:#f7fafd}
.acc{background:#f2f7fd}
.tag{display:inline-block;font-size:10px;font-weight:800;padding:1px 6px;border-radius:8px}
.ok{background:#e8f6ee;color:#1c7c54}.est{background:#fff4e0;color:#8a5a12}
.cnt{color:var(--sub);font-size:12px;margin:6px 0}
.fn{color:#6b7684;font-size:11.4px;margin-top:8px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.box{background:#f7f9fd;border:1px solid #dfe6f2;border-radius:8px;padding:10px 12px}
.box.warn{background:#fffaf0;border-color:#f3e2bf}
ul{margin:4px 0;padding-left:18px}
.subtabs{display:flex;gap:6px;margin-bottom:10px}
.subtab{padding:6px 12px;border:1px solid #cfd8e6;background:#fff;border-radius:16px;cursor:pointer;font-size:12.2px}
.subtab.on{background:var(--pri);color:#fff;border-color:var(--pri)}
.chartbox{width:100%;max-width:100%}
.chartbox svg{width:100%;max-width:760px;height:auto;display:block;margin:0 auto}
.legend{display:flex;flex-wrap:wrap;gap:10px;margin-top:8px;font-size:11.5px;color:#43505f}
.legend span{display:inline-flex;align-items:center;gap:5px}
.legend i{width:10px;height:10px;border-radius:2px;display:inline-block}
.chartgrid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.chartgrid .card{margin-bottom:0}
@media(max-width:900px){.chartgrid{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,1fr)}.grid2{grid-template-columns:1fr}.bior-tagwrap{display:none!important}.tabs{overflow-x:auto;flex-wrap:nowrap}.tab{white-space:nowrap}}
"""

HTML = """<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>科研经费 × 宝锐产品线（统一口径看板）</title><style>__CSS__</style></head><body>
<div class="wrap">
<h1>科研经费 × 宝锐产品线 · 统一口径看板</h1>
<div class="meta">广东 / 浙江 2026 省自然科学基金 5,838 项逐项 + 课题组 PI 台账 + 战略方向 + PubMed 反推。<b>蓝色列＝宝锐可及</b>。结构与可及率为经验值，技术路径占比用 PubMed 提及率做排序锚点。截至 2026-10-17。</div>

<div class="tabs" id="tabs">
  <button class="tab on" data-t="ov">总览</button>
  <button class="tab" data-t="mat">研究方向 × 产品线</button>
  <button class="tab" data-t="unit">单位层级</button>
  <button class="tab" data-t="pi">课题组 / PI</button>
  <button class="tab" data-t="tech">技术路径</button>
  <button class="tab" data-t="strat">战略突破</button>
  <button class="tab" data-t="pub">mRNA 活跃 PI</button>
  <button class="tab" data-t="doc">口径与来源</button>
</div>

<!-- 总览 -->
<div class="panel on" id="p-ov">
  <div class="kpis">
    <div class="kpi"><div class="kl">两省省基金总盘</div><div class="kv">9.20 亿</div><div class="ks">5,838 项</div></div>
    <div class="kpi"><div class="kl">生医相关</div><div class="kv">5.19 亿</div><div class="ks">3,383 项</div></div>
    <div class="kpi"><div class="kl">宝锐可及</div><div class="kv">2.83 亿</div><div class="ks">占生医 54.5%</div></div>
    <div class="kpi"><div class="kl">课题组 / PI</div><div class="kv">5,830</div><div class="ks">有可及 3,378 个</div></div>
  </div>
  <div class="chartgrid" style="margin-bottom:14px">
    <div class="card"><h2>六条产品线结构 <span>占宝锐可及经费</span></h2>
      <div class="chartbox" id="c-donut" style="max-width:340px;margin-left:0"></div><div class="legend" id="l-donut"></div></div>
    <div class="card"><h2>研究方向可及经费 Top 8 <span>万元</span></h2>
      <div class="chartbox" id="c-topic"></div></div>
  </div>
  <div class="card"><h2>两省对比 <span>六条产品线，万元</span></h2>
    <div class="chartbox" id="c-prov"></div><div class="legend" id="l-prov"></div></div>
  <div class="card"><h2>六条产品线 <span>两省合计，万元</span></h2>
    <div class="tbl-wrap" style="max-height:none"><table id="ov-lines"><thead></thead><tbody></tbody></table></div>
    <div class="fn">WB 是最大单一品类（9,878 万，34.9%），RT-qPCR 第二（6,123 万，21.6%）；两者合计 56.5%。</div>
  </div>
  <div class="grid2">
    <div class="card"><h2>战略方向 <span>当期盘子</span></h2>
      <div class="tbl-wrap" style="max-height:none"><table id="ov-strat"><thead></thead><tbody></tbody></table></div>
    </div>
    <div class="card"><h2>mRNA 活跃 PI <span>PubMed 2024–25</span></h2>
      <div class="tbl-wrap" style="max-height:none"><table id="ov-pub"><thead></thead><tbody></tbody></table></div>
    </div>
  </div>
</div>

<!-- 矩阵 -->
<div class="panel" id="p-mat">
  <div class="card"><h2>研究方向 × 产品大类 <span>万元；蓝底＝宝锐可及</span></h2>
    <div class="bar">
      <select id="m-prov"><option value="all">两省合计</option><option value="GD">广东</option><option value="ZJ">浙江</option></select>
      <input id="m-q" placeholder="搜研究方向">
      <span class="cnt" id="m-cnt"></span>
    </div>
    <div class="chartbox" id="c-mat" style="margin-bottom:10px"></div>
    <div class="tbl-wrap"><table id="t-mat"><thead></thead><tbody></tbody></table></div>
    <div class="fn">读法：每行一个研究方向，蓝色列相加＝该方向宝锐可做的钱；可及率＝可及／经费合计。</div>
  </div>
</div>

<!-- 单位 -->
<div class="panel" id="p-unit">
  <div class="card"><h2>单位层级 <span>按宝锐可及经费排序</span></h2>
    <div class="bar">
      <select id="u-prov"><option value="all">全部省份</option><option>广东</option><option>浙江</option></select>
      <select id="u-type"><option value="all">全部类型</option><option>高校</option><option>医院</option><option>院所</option><option>企业</option><option>其他</option></select>
      <input id="u-q" placeholder="搜单位名">
      <span class="cnt" id="u-cnt"></span>
    </div>
    <div class="chartbox" id="c-unit" style="margin-bottom:10px"></div>
    <div class="tbl-wrap"><table id="t-unit"><thead></thead><tbody></tbody></table></div>
  </div>
</div>

<!-- PI -->
<div class="panel" id="p-pi">
  <div class="card"><h2>课题组 / PI <span>清单无课题组字段，按 1 位负责人 = 1 个课题组</span></h2>
    <div class="bar">
      <select id="p-prov"><option value="all">全部省份</option><option>广东</option><option>浙江</option></select>
      <select id="p-line"><option value="all">全部主产品线</option>__LINEOPTS__</select>
      <input id="p-q" placeholder="搜单位 / 负责人 / 方向">
      <span class="cnt" id="p-cnt"></span>
    </div>
    <div class="tbl-wrap"><table id="t-pi"><thead></thead><tbody></tbody></table></div>
    <div class="fn" id="p-more"></div>
  </div>
</div>

<!-- 技术路径 -->
<div class="panel" id="p-tech">
  <div class="card"><h2>蛋白 / 信号 / 通路：主流技术路径与宝锐关联度 <span>PubMed 提及率可核，使用率为经验值</span></h2>
    <div class="chartbox" id="c-tech" style="margin-bottom:10px"></div>
    <div class="tbl-wrap" style="max-height:none"><table id="t-tech"><thead></thead><tbody></tbody></table></div>
    <div class="fn">PubMed 6.8% 是“摘要里提到 western blot”的比例，不是实验使用率；实验使用率（85–90%）为经验值。两者一起看：排序可信，绝对值不可当份额。</div>
  </div>
</div>

<!-- 战略 -->
<div class="panel" id="p-strat">
  <div class="card"><h2>战略突破方向 <span>单细胞/组学/测序 + mRNA</span></h2>
    <div class="subtabs">
      <button class="subtab on" data-s="om">单细胞 / 组学 / 测序 <b id="s-om-n"></b></button>
      <button class="subtab" data-s="mr">mRNA <b id="s-mr-n"></b></button>
    </div>
    <div class="bar">
      <select id="s-prov"><option value="all">全部省份</option><option>广东</option><option>浙江</option></select>
      <input id="s-q" placeholder="搜单位 / 负责人 / 项目">
      <span class="cnt" id="s-cnt"></span>
    </div>
    <div class="chartbox" id="c-strat" style="margin-bottom:10px"></div>
    <div class="tbl-wrap"><table id="t-strat"><thead></thead><tbody></tbody></table></div>
  </div></div>

<!-- PubMed -->
<div class="panel" id="p-pub">
  <div class="card"><h2>mRNA 活跃 PI <span>PubMed 反推，不是立项名单</span></h2>
    <div class="bar">
      <select id="b-prov"><option value="all">全部省份</option><option>广东</option><option>浙江</option></select>
      <input id="b-q" placeholder="搜单位 / 负责人 / 国自然号">
      <span class="cnt" id="b-cnt"></span>
    </div>
    <div class="chartbox" id="c-pub" style="margin-bottom:10px"></div>
    <div class="tbl-wrap"><table id="t-pub"><thead></thead><tbody></tbody></table></div>
    <div class="fn">通讯作者取末位作者；国自然号取 8 位数字，可与官方导出对撞。姓名为拼音，中文名需核实。</div>
  </div></div>

<!-- 口径 -->
<div class="panel" id="p-doc">
  <div class="card"><h2>口径与来源</h2>
    <div class="grid2">
      <div class="box"><h4 style="margin:0 0 4px">数据源（可核）</h4><ul>
        <li>浙江：2026 年度省自然科学基金拟资助项目清单（92 页 / 2,667 项，官方 PDF）</li>
        <li>广东：2026 年度省自然科学基金拟立项及资金安排表（137 页 / 3,171 项 / 42,110 万，官方附件转载件，总量一致）</li>
        <li>PubMed：E-utilities，2024–2025，mRNA/LNP/circRNA/IVT + China[ad]</li>
      </ul></div>
      <div class="box warn"><h4 style="margin:0 0 4px">不确定的 3 个点</h4><ul>
        <li>可及率（55%/54%）与产品线结构是经验值，需用客户实际支出校准。</li>
        <li>“实验使用率”（WB 85–90%）无权威统计；PubMed 数字是摘要提及率，只可排序。</li>
        <li>浙江金额按类别标准额测算，非财政逐项下达数。</li>
      </ul></div>
    </div>
  </div></div>
__FOOTER__
</div>
<script>
var D = __DATA__;
var state = {tab:'ov', sort:{}, limit:600};

function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function num(v){return (typeof v==='number')?v.toLocaleString():esc(v);}
function fmt0(v){return Math.round(v).toLocaleString();}

/* ---------- 通用表格 ---------- */
function buildTable(tid, cols, rows, opt){
  opt = opt || {};
  var t = document.getElementById(tid);
  var th = '<tr>' + cols.map(function(c,i){
    return '<th class="'+(c.n?'n':'')+'" data-c="'+i+'">'+c.t+(c.n?'':'')+'</th>';
  }).join('') + '</tr>';
  t.querySelector('thead').innerHTML = th;
  var body = rows.slice(0, opt.limit || 10000).map(function(r){
    return '<tr>' + cols.map(function(c,i){
      var v = r[i];
      var cls = (c.n?'n ':'') + (c.pj?'pj ':'') + (c.acc?'acc':'');
      var txt = c.n ? (typeof v === 'number' ? v.toLocaleString() : esc(v))
                    : (c.raw ? (v||'') : esc(v));
      return '<td class="'+cls.trim()+'">'+txt+'</td>';
    }).join('') + '</tr>';
  }).join('');
  t.querySelector('tbody').innerHTML = body;
  /* 排序 */
  t.querySelectorAll('thead th').forEach(function(h){
    h.onclick = function(){
      var i = +h.dataset.c, key = tid+':'+i;
      var dir = (state.sort[key] === 1) ? -1 : 1;
      state.sort[key] = dir;
      var numeric = cols[i].n;
      rows.sort(function(a,b){
        var x=a[i], y=b[i];
        if(numeric) return (Number(x)-Number(y))*dir;
        return String(x).localeCompare(String(y),'zh')*dir;
      });
      buildTable(tid, cols, rows, opt);
    };
  });
  if(opt.count){
    var el = document.getElementById(opt.count);
    if(el) el.textContent = '共 ' + rows.length + ' 条' + (rows.length>opt.limit ? '（显示前 '+opt.limit+'）' : '');
  }
}

/* ---------- 图表（内联 SVG，无外部依赖） ---------- */
var PAL=['#1852B6','#2E7DD1','#4FA3E3','#7FC0EF','#0F9D8F','#E3A008','#C0392B','#7B61A8','#5F6B7A','#A9D6F5'];

function legend(elId, items, total){
  var el=document.getElementById(elId); if(!el) return;
  total = total || items.reduce(function(a,b){return a+b.v;},0) || 1;
  el.innerHTML=items.map(function(it,i){
    return '<span><i style="background:'+PAL[i%PAL.length]+'"></i>'+esc(it.n)+' <b>'+fmt0(it.v)+'</b>（'+Math.round(it.v/total*100)+'%）</span>';
  }).join('');
}

function donut(id, legendId, items){
  var el=document.getElementById(id); if(!el) return;
  var total=items.reduce(function(a,b){return a+b.v;},0)||1;
  var W=240,H=230,R=76,SW=34,C=2*Math.PI*R,cx=118,cy=112,off=0,s='';
  items.forEach(function(it,i){
    var frac=it.v/total, len=frac*C;
    s+='<circle cx="'+cx+'" cy="'+cy+'" r="'+R+'" fill="none" stroke="'+PAL[i%PAL.length]+'" stroke-width="'+SW+'" stroke-dasharray="'+len.toFixed(2)+' '+(C-len).toFixed(2)+'" stroke-dashoffset="'+(-off).toFixed(2)+'" transform="rotate(-90 '+cx+' '+cy+')" aria-label="'+esc(it.n)+' '+Math.round(frac*100)+'%"></circle>';
    off+=len;
  });
  s+='<text x="'+cx+'" y="'+(cy-2)+'" text-anchor="middle" font-size="20" font-weight="800" fill="#1F4E79">'+fmt0(total)+'</text>';
  s+='<text x="'+cx+'" y="'+(cy+18)+'" text-anchor="middle" font-size="11" fill="#6b7684">万元可及</text>';
  el.innerHTML='<svg viewBox="0 0 240 230">'+s+'</svg>';
  if(legendId) legend(legendId, items, total);
}

function barH(id, items, opt){
  var el=document.getElementById(id); if(!el) return;
  opt=opt||{};
  var n=items.length; if(!n){el.innerHTML='';return;}
  var rowH=opt.rowH||26, labelW=opt.labelW||150, W=opt.W||620, H=n*rowH+16;
  var max=Math.max.apply(null, items.map(function(x){return x.v;}))||1;
  var barMax=W-labelW-78, s='';
  items.forEach(function(it,i){
    var w=Math.max(2, it.v/max*barMax), y=i*rowH+8;
    var lab=it.n.length>18?it.n.slice(0,17)+'…':it.n;
    s+='<text x="'+(labelW-8)+'" y="'+(y+13)+'" font-size="11.5" text-anchor="end" fill="#1b2430">'+esc(lab)+'</text>';
    s+='<rect x="'+labelW+'" y="'+y+'" width="'+w.toFixed(1)+'" height="16" rx="3" fill="'+(it.c||PAL[i%PAL.length])+'" aria-label="'+esc(it.n)+' '+fmt0(it.v)+'"></rect>';
    var inside = w>Math.max(46, String(fmt0(it.v)).length*8+12);
    s+='<text x="'+(labelW+w+(inside?-6:6))+'" y="'+(y+12)+'" font-size="11" text-anchor="'+(inside?'end':'start')+'" fill="'+(inside?'#fff':'#43505f')+'">'+fmt0(it.v)+'</text>';
  });
  el.innerHTML='<svg viewBox="0 0 '+W+' '+H+'">'+s+'</svg>';
}

function barGroup(id, legendId, cats, series){
  var el=document.getElementById(id); if(!el) return;
  var W=680,H=262,padL=52,padB=54,padT=26;
  var max=0; series.forEach(function(sr){sr.v.forEach(function(v){if(v>max)max=v;});});
  max=(max||1)*1.04;
  var plotW=W-padL-16, plotH=H-padT-padB;
  var gw=plotW/cats.length, bw=Math.min(34,(gw-14)/series.length), s='';
  for(var g=0;g<=4;g++){
    var y=padT+plotH-plotH*g/4;
    s+='<line x1="'+padL+'" y1="'+y+'" x2="'+(W-16)+'" y2="'+y+'" stroke="#eef1f6"></line>';
    s+='<text x="'+(padL-6)+'" y="'+(y+4)+'" font-size="10" text-anchor="end" fill="#8a94a3">'+fmt0(max*g/4)+'</text>';
  }
  cats.forEach(function(c,i){
    s+='<text x="'+(padL+gw*i+gw/2)+'" y="'+(H-padB+18)+'" font-size="11" text-anchor="middle" fill="#43505f">'+esc(c)+'</text>';
    series.forEach(function(sr,j){
      var v=sr.v[i], h=Math.max(1,v/max*plotH);
      var x=padL+gw*i+(gw-bw*series.length-6)/2+j*(bw+6);
      var y=padT+plotH-h;
      s+='<rect x="'+x.toFixed(1)+'" y="'+y.toFixed(1)+'" width="'+bw.toFixed(1)+'" height="'+h.toFixed(1)+'" rx="2" fill="'+sr.color+'" aria-label="'+esc(c)+' '+esc(sr.name)+' '+fmt0(v)+'"></rect>';
      if(h>18) s+='<text x="'+(x+bw/2).toFixed(1)+'" y="'+(y+12).toFixed(1)+'" font-size="9.5" text-anchor="middle" fill="#fff">'+fmt0(v)+'</text>';
    });
  });
  el.innerHTML='<svg viewBox="0 0 '+W+' '+H+'">'+s+'</svg>';
  if(legendId){
    document.getElementById(legendId).innerHTML=series.map(function(sr){
      return '<span><i style="background:'+sr.color+'"></i>'+esc(sr.name)+'</span>';
    }).join('');
  }
}

/* ---------- Tab ---------- */
document.getElementById('tabs').addEventListener('click', function(e){
  var b = e.target.closest('.tab'); if(!b) return;
  state.tab = b.dataset.t;
  document.querySelectorAll('.tab').forEach(function(x){x.classList.toggle('on', x===b);});
  document.querySelectorAll('.panel').forEach(function(p){p.classList.toggle('on', p.id==='p-'+state.tab);});
  renderTab(state.tab);
});

function renderTab(t){
  if(t==='ov') renderOv();
  if(t==='mat') renderMat();
  if(t==='unit') renderUnit();
  if(t==='pi') renderPi();
  if(t==='tech') renderTech();
  if(t==='strat') renderStrat();
  if(t==='pub') renderPub();
}

/* ---------- 总览 ---------- */
function renderOv(){
  var zl = D.lines.map(function(l,i){
    var z = D.matrixZJ, g = D.matrixGD;
    var zi = 2+i, gi = 2+i;
    var zv = z[z.length-1][zi], gv = g[g.length-1][gi];
    return [l, zv, gv, zv+gv, (zv+gv)/(D.kpi.addr)*100];
  });
  var cols = [{t:'产品线'},{t:'浙江',n:1},{t:'广东',n:1},{t:'合计',n:1},{t:'占可及%',n:1}];
  buildTable('ov-lines', cols, zl.map(function(r){return [r[0],r[1],r[2],r[3],Math.round(r[4])+'%'];}));
  buildTable('ov-strat', [{t:'方向'},{t:'项数',n:1},{t:'经费(万)',n:1}],
    [['单细胞/组学/测序', D.kpi.omN, D.kpi.omAmt], ['mRNA', D.kpi.mrN, D.kpi.mrAmt]]);
  buildTable('ov-pub', [{t:'省'},{t:'论文',n:1},{t:'PI',n:1}],
    [['广东', D.kpi.pubGd, D.pubmed.filter(function(x){return x[0]==='广东';}).length],
     ['浙江', D.kpi.pubZj, D.pubmed.filter(function(x){return x[0]==='浙江';}).length]]);
  /* 图：产品线结构 + 研究方向 Top8 + 两省对比 */
  var tot = D.matrixAll[D.matrixAll.length-1];
  donut('c-donut','l-donut', D.lines.map(function(l,i){return {n:l, v:tot[2+i]};}));
  var tp = D.matrixAll.slice(0,-1).map(function(r){return {n:r[0], v:r[13]};}).sort(function(a,b){return b.v-a.v;}).slice(0,8);
  barH('c-topic', tp, {labelW:150});
  barGroup('c-prov','l-prov', D.lines,
    [{name:'浙江', v:D.lines.map(function(l,i){return D.matrixZJ[D.matrixZJ.length-1][2+i];}), color:'#1852B6'},
     {name:'广东', v:D.lines.map(function(l,i){return D.matrixGD[D.matrixGD.length-1][2+i];}), color:'#4FA3E3'}].map(function(o){o.v=o.v.map(function(v){return Math.round(v);});return o;}));
}

/* ---------- 矩阵 ---------- */
function renderMat(){
  var prov = document.getElementById('m-prov').value;
  var q = (document.getElementById('m-q').value||'').trim();
  var rows = prov==='GD'?D.matrixGD: prov==='ZJ'?D.matrixZJ: D.matrixAll;
  if(q) rows = rows.filter(function(r){return r[0].indexOf(q)>=0;});
  var cols = [{t:'研究方向'},{t:'经费合计',n:1},{t:'RT-qPCR',n:1,acc:1},{t:'PCR/克隆',n:1,acc:1},
              {t:'提取',n:1,acc:1},{t:'LAMP',n:1,acc:1},{t:'其他分子',n:1,acc:1},{t:'WB',n:1,acc:1},
              {t:'mRNA',n:1,acc:1},{t:'细胞培养',n:1},{t:'抗体/免疫',n:1},{t:'测序/组学',n:1},{t:'动物/表型',n:1},
              {t:'可及小计',n:1},{t:'可及率%',n:1}];
  buildTable('t-mat', cols, rows, {count:'m-cnt'});
  barH('c-mat', rows.filter(function(r){return r[0]!=='合计';}).map(function(r){return {n:r[0],v:r[13]};})
       .sort(function(a,b){return b.v-a.v;}).slice(0,8), {labelW:150});
}

/* ---------- 单位 ---------- */
function renderUnit(){
  var prov = document.getElementById('u-prov').value;
  var typ = document.getElementById('u-type').value;
  var q = (document.getElementById('u-q').value||'').trim();
  var rows = D.units.filter(function(r){
    if(prov!=='all' && r[0]!==prov) return false;
    if(typ!=='all' && r[2]!==typ) return false;
    if(q && r[1].indexOf(q)<0) return false;
    return true;
  });
  var cols = [{t:'省'},{t:'单位'},{t:'类型'},{t:'项数',n:1},{t:'经费(万)',n:1},{t:'生医经费',n:1},
              {t:'宝锐可及',n:1},{t:'可及率%',n:1},
              {t:'RT-qPCR',n:1},{t:'PCR/克隆',n:1},{t:'提取',n:1},{t:'LAMP',n:1},{t:'其他分子',n:1},{t:'WB',n:1}];
  buildTable('t-unit', cols, rows, {count:'u-cnt', limit:state.limit});
  var top = rows.slice().sort(function(a,b){return b[6]-a[6];}).slice(0,10)
    .map(function(r){return {n:r[1], v:r[6]};});
  barH('c-unit', top, {labelW:190});
}

/* ---------- PI ---------- */
function renderPi(){
  var prov = document.getElementById('p-prov').value;
  var line = document.getElementById('p-line').value;
  var q = (document.getElementById('p-q').value||'').trim();
  var rows = D.pis.filter(function(r){
    if(prov!=='all' && r[0]!==prov) return false;
    if(line!=='all' && r[7]!==line) return false;
    if(q && (r[1]+r[2]+r[8]).indexOf(q)<0) return false;
    return true;
  });
  var cols = [{t:'省'},{t:'单位'},{t:'负责人'},{t:'项数',n:1},{t:'经费(万)',n:1},{t:'可及(万)',n:1},
              {t:'可及率%',n:1},{t:'主产品线'},{t:'主要方向'}];
  buildTable('t-pi', cols, rows, {count:'p-cnt', limit:state.limit});
  var more = document.getElementById('p-more');
  more.textContent = rows.length > state.limit ? ('当前显示前 '+state.limit+' 条，共 '+rows.length+' 条；用筛选缩小范围可看全。') : '';
}

/* ---------- 技术路径 ---------- */
function renderTech(){
  var cols = [{t:'技术路径'},{t:'PubMed 提及率',n:1},{t:'实验使用率',n:1},{t:'宝锐覆盖度',n:1},{t:'对应产品线 / 缺口',pj:1},{t:'关联强度'}];
  buildTable('t-tech', cols, D.techs);
  barH('c-tech', D.techs.map(function(t){return {n:t[0], v:parseFloat(String(t[3]).replace('%',''))};}), {labelW:170});
}

/* ---------- 战略 ---------- */
var stratSub = 'om';
function renderStrat(){
  var prov = document.getElementById('s-prov').value;
  var q = (document.getElementById('s-q').value||'').trim();
  var src = stratSub==='om'?D.stratOm:D.stratMr;
  var rows = src.filter(function(r){
    if(prov!=='all' && r[0]!==prov) return false;
    if(q && (r[1]+r[2]+r[6]).indexOf(q)<0) return false;
    return true;
  });
  var cols = [{t:'省'},{t:'单位'},{t:'负责人'},{t:'项数',n:1},{t:'经费(万)',n:1},{t:'类别/关键词'},{t:'代表项目',pj:1},{t:''}];
  buildTable('t-strat', cols, rows, {count:'s-cnt'});
  var byU={}; rows.forEach(function(r){ if(r[2]!=='—'){ byU[r[1]]=(byU[r[1]]||0)+r[4]; } });
  barH('c-strat', Object.keys(byU).map(function(k){return {n:k,v:byU[k]};})
       .sort(function(a,b){return b.v-a.v;}).slice(0,8), {labelW:210});
  document.getElementById('s-om-n').textContent = '（'+D.kpi.omN+' 项 / '+fmt0(D.kpi.omAmt)+' 万）';
  document.getElementById('s-mr-n').textContent = '（'+D.kpi.mrN+' 项 / '+fmt0(D.kpi.mrAmt)+' 万）';
}

/* ---------- PubMed ---------- */
function renderPub(){
  var prov = document.getElementById('b-prov').value;
  var q = (document.getElementById('b-q').value||'').trim();
  var rows = D.pubmed.filter(function(r){
    if(prov!=='all' && r[0]!==prov) return false;
    if(q && (r[1]+r[2]+r[4]).indexOf(q)<0) return false;
    return true;
  });
  var cols = [{t:'省'},{t:'单位'},{t:'通讯作者'},{t:'论文数',n:1},{t:'国自然批准号'},{t:'年份'},{t:'代表论文',pj:1},{t:'PMID'}];
  buildTable('t-pub', cols, rows, {count:'b-cnt'});
  var byInst={}; rows.forEach(function(r){byInst[r[1]]=(byInst[r[1]]||0)+r[2];});
  var topPub=Object.keys(byInst).map(function(k){return {n:k,v:byInst[k]};}).sort(function(a,b){return b.v-a.v;}).slice(0,10);
  barH('c-pub', topPub, {labelW:200});
}

/* ---------- 筛选绑定 ---------- */
['m-prov','m-q','u-prov','u-type','u-q','p-prov','p-line','p-q','s-prov','s-q','b-prov','b-q'].forEach(function(id){
  var el = document.getElementById(id);
  if(!el) return;
  var ev = (el.tagName==='INPUT')?'input':'change';
  el.addEventListener(ev, function(){
    var t = state.tab;
    if(id[0]==='m') renderMat();
    if(id[0]==='u') renderUnit();
    if(id[0]==='p') renderPi();
    if(id[0]==='s') renderStrat();
    if(id[0]==='b') renderPub();
  });
});
document.querySelectorAll('.subtab').forEach(function(b){
  b.addEventListener('click', function(){
    stratSub = b.dataset.s;
    document.querySelectorAll('.subtab').forEach(function(x){x.classList.toggle('on', x===b);});
    renderStrat();
  });
});
renderOv();
</script></body></html>"""

HTML = HTML.replace('__CSS__', CSS).replace('__LINEOPTS__', ''.join('<option>%s</option>' % l for l in L))
HTML = HTML.replace('__DATA__', json.dumps(DATA, ensure_ascii=False, separators=(',', ':')))
HTML = BK.inject(HTML, theme='dark', tag='科研经费 × 产品线 · 统一口径看板',
                 footer_extra='内部资料 · 请勿外发 ｜ 源：广东/浙江 2026 省基金立项清单、PubMed E-utilities')
HTML = HTML.replace('__FOOTER__', '')
io.open(OUT, 'w', encoding='utf-8').write(HTML)
print('✅ %.0f KB' % (len(HTML.encode()) / 1024))
