Team Ai
Datasetpublic

ZZoutian/figure2data-database-v2

figure2data databasev2 — 40K v6 合成科研图表数据集(最终交付版) 生成日期:2026-09-17 生成器:generator 1.6.0 / dataset_generation_revision v6(含两次 hotfix) 规模:40,000 样本(10 图族 / 33 亚型;area、matrix 冻结不生成,set_relation 已删除) 目录结构 databasev2/ ├── figure2data.sqlite3 # 主数据库(2.0 GB:40,000 samples / 280,000 documents) ├── schema/ # sqlite schema ├── shards/ # 数据资产(shard = (样本序号-1)//1000) │ └── shard_000 .. shard_039/ │ ├── images/ # PNG… See the full description on the dataset page: https://huggingface.co/datasets/ZZoutian/figure2data-database-v2.

sourceHugging Faceupdated 24d agoView on Hugging Face
0likes6.3kdownloads
databasev6.md3502 linesDownload Raw Back to code
1# databasev6.md2 3## Figure2Data Database v6:提取对象收敛 + Area 冻结 + 统计图专项重构方案4 5> 基于本轮实际审查的 `src(3).zip` 与 `database100single.zip`。  6> 本文不是概念建议,而是给 Agent/Codex 的**代码级执行方案**。  7> 优先级:**先冻结 Area 相关生产链路并从当前生成范围排除,再修统计图/图例/横向柱图;随后跑 100 single regression → 1K → 5K。未通过前不要进入 40K。**8>9> 本版新增原则:**Area 不是删除,而是冻结(source retained, generation excluded)。`set_relation/UpSet` 才是彻底删除。**10 11---12 13# 0. 本轮明确目标14 15当前项目开始对**实际提取对象进行收敛**。因此 v6 不再把所有既有 family 都当作同优先级目标。16 17本轮先做两类范围控制:18 19```text20Area / area21    → FROZEN22    → 保留源码、schema、adapter 兼容23    → 当前 100 / 1K / 5K / 40K GenerationPlan 不再生成24    → 本轮 Agent 不修改 area recipe / renderer / GT 语义25 26Set relation / UpSet27    → REMOVED28    → 从 recipe / registry / quota / taxonomy / renderer / adapter / QA 中删除29```30 31本轮真正需要修复的对象只包括:32 331. **Box plot 横坐标与箱体位置不一致**342. **Legend 位置混乱,尤其 scatter legend 与 point 重叠**353. **Horizontal bar 排布混乱**364. **Box plot 太简单;violin 视觉与语义质量差**375. **彻底删除 set intersection / UpSet (`set_relation`) family**38 39本轮同时修复两个会污染 GT 的隐藏问题:40 416. **Box / violin 每个 series 的 geometry 当前错误地重复记录了全部 boxes / violins**427. **Violin 的 GT KDE 与 Matplotlib 实际重新计算的 violin KDE 不是同一条密度曲线**43 44---45 46## 0.1 v6 提取对象状态表47 48本轮必须显式区分“修复”“保留”“冻结”“删除”:49 50|对象|v6 状态|源码|当前生成计划|本轮是否修改|51|---|---|---|---|---|52|`area/area`|**FROZEN**|保留|**排除**|**禁止**|53|`set_relation/upset`|**REMOVED**|删除|排除|删除相关代码|54|`statistical/box`|ACTIVE|保留|保留|重点重构|55|`statistical/violin`|ACTIVE|保留|保留|重点重构|56|`scatter` / jitter / strip|ACTIVE|保留|保留|legend 专项修复|57|`bar`(含 horizontal)|ACTIVE|保留|保留|布局专项修复|58|其余现有 family|RETAINED|保留|按现有 scope|除兼容性修复外不扩展|59 60**注意:**“Area 冻结”不等于从 schema 删除 `kind=area`。历史数据仍需可读,adapter/scene/validation 仍需能够解析旧 Area GT。61 62---63 64## 0.2 Area 冻结的工程含义65 66本轮 Agent 必须遵守以下四条:67 68```text69A. 不修改 src/databasev1/recipes/area.py70B. 不修改 generator/charts.py::draw_area()71C. 不修改 area 的 truth / geometry / adapter 语义72D. 当前新 GenerationPlan 不得出现 chart_family == "area"73```74 75允许修改与 Area 同文件的其它函数,例如:76 77```text78generator/charts.py::draw_box79generator/charts.py::draw_violin80generator/charts.py::draw_bars81```82 83但不得顺手重构 `draw_area()`。84 85---86 87## 0.3 为什么 Area 当前选择冻结而不是删除88 89当前目标是缩小 benchmark 的核心提取对象,而不是破坏已有兼容性。90 91因此:92 93```text94Area:95保留实现 + 保留历史读取能力 + 停止继续扩数据96```97 98未来若重新纳入,只需要恢复 generation scope/quota,不需要重新实现旧 schema。99 100这与 UpSet 不同:UpSet 当前明确决定不再作为目标,所以直接删除。101 102---103 104# 1. 当前源码中已确认的根因105 106## 1.1 Box / violin 类别锚点偏移一位107 108当前 `src/databasev1/generator/charts.py`:109 110```python111pos = np.arange(1, len(data) + 1)112```113 114但统一的 categorical axis 使用:115 116```python117positions = list(range(len(cats)))118```119 120即:121 122```text123box / violin:1241, 2, 3, 4125 126categorical ticks:1270, 1, 2, 3128```129 130这就是当前箱体和 x tick 错位的直接根因。131 132**修复原则:所有 categorical geometry 均使用 `CategoryMapper` 的 0-based anchor。**133 134---135 136## 1.2 Box / violin geometry GT 也有重复映射问题137 138当前 `render.py::_panel_gt()` 对每一个 box series 都:139 140```python141for box_patch in bp["boxes"]:142    ...143```144 145结果:146 147```text148series_1 -> 全部 box149series_2 -> 全部 box150series_3 -> 全部 box151...152```153 154violin 同理。155 156正确结果必须是:157 158```text159series_1 -> body/box 0160series_2 -> body/box 1161...162```163 164否则后续 CV/eval target 无法做 series-level 对齐。165 166---167 168## 1.3 Scatter legend 当前天然有概率落在 point cloud 内169 170当前 `legend_spec()` 随机权重:171 172```text173inside_boxed = 30174inside_plain = 35175outside      = 10176none         = 5177```178 179也就是说绝大多数 legend 天然优先放在图内。180 181当前 `_place_legend()` 允许:182 183```text184legend 覆盖 <= 2% data points185```186 187仍然认为可接受。188 189`qa_render.py` 甚至允许:190 191```text192<= 6% point overlap193```194 195对于 scatter / jitter / strip:196 197> **legend 与 marker 重叠应视为硬错误,不应作为“少量可容忍”。**198 199---200 201## 1.4 Horizontal bar 的 rotated label 策略直接用于 y tick202 203当前 `_finalize_bar_panel()`:204 205```python206if longest * len(cats) > 28:207    rotation = rng.choice([30, 45, 90])208```209 210然后 `draw_bars()` 的 horizontal 分支:211 212```python213ax.set_yticklabels(cats, rotation=config.get("rotation", 0))214```215 216这会把 horizontal bar 的类别 y-label 旋转 30/45/90 度。217 218例如:219 220```text221Bulk222Film223Nanowire224Nanosheet225```226 227会出现你当前样本中那种挤压、交叠、视觉层级混乱。228 229**横向 bar 的 y-axis categorical labels 必须保持 0°。**230 231---232 233## 1.5 Box 数据分布过于单一234 235当前 `_grouped_samples()` 基本全部是:236 237```text238normal distribution239+ 不同 mean240+ 相近 sigma241+ 25/40 个点242```243 244因此 box 只是在:245 246```text247低 → 中 → 高248```249 250做机械变化。251 252缺少真实科研统计图常见的:253 254```text255skewed256bimodal257heavy-tail258heteroscedastic259unequal-n260outlier-rich261```262 263---264 265## 1.6 Violin GT 与实际 renderer 不一致266 267recipe 已经计算:268 269```text270support271density272```273 274并保存到 GT。275 276但 renderer 又调用:277 278```python279ax.violinplot(raw)280```281 282Matplotlib 会重新做 KDE。283 284于是:285 286```text287GT density != rendered density288```289 290这对于 benchmark 是不允许的。291 292v6 必须:293 294> **由 recipe 计算一次 KDE,renderer 直接使用该 KDE polygon。**295 296---297 298## 1.7 `split_pairs` violin 当前没有真正 split semantics299 300当前 variant 名为:301 302```text303split_pairs304```305 306但仍然调用普通:307 308```python309ax.violinplot(...)310```311 312并没有左右 split 两组。313 314v6 不保留名不副实的 variant。315 316---317 318## 1.8 Area 当前实现已存在且本轮不触碰319 320源码审查确认 Area 相关路径包括:321 322```text323src/databasev1/recipes/area.py324src/databasev1/generator/charts.py::draw_area325src/databasev1/generator/render.py   # dk == "area" 分派326src/databasev1/generator/labels.py   # area polygon geometry327src/databasev1/adapter/common.py     # area baseline normalize328src/databasev1/adapter/scene.py      # area scene mapping329src/databasev1/adapter/quality.py    # area quality compatibility330src/databasev1/validation/__init__.py # valid kind includes area331src/databasev1/review_batch.py       # historical review mapping332```333 334这些路径中:335 336- `recipes/area.py` 与 `draw_area()`:**源码冻结**;337- adapter / scene / validation:**兼容性冻结**,不能删除 Area 支持;338- `review_batch.py`:当前新 review 计划不再主动抽 Area,但历史文件读取能力保留。339 340---341 342# 2. Agent 修改原则343 344Agent 必须遵守:345 346```text3471. 先建立 Area freeze baseline,再改其它代码3482. categorical anchor 全部统一为 0-based3493. renderer geometry 必须与 truth 使用同一组数值3504. point-heavy chart legend 不允许覆盖 data3515. horizontal category label 不旋转3526. 不通过改 GT 标签迁就错误 renderer3537. 不允许只靠 human QC 发现可程序化错误3548. 删除 UpSet 后必须重建 plan,旧 plan 不再复用3559. Area 只从当前 generation scope 排除,不从 schema/adapter 删除35610. 通用 helper 修改不得造成 Area frozen fixture 回归357```358 359本轮尤其禁止两种“顺手重构”:360 361```text362禁止:为了统一 chart API 改 draw_area()363禁止:为了删除当前不生成的对象,把 area 从 valid_kinds / scene mapping 删除364```365 366---367 368# 3. 修改文件总览369 370主要修改:371 372```text373src/databasev1/generator/charts.py       # 只改 box / violin / bar,draw_area 冻结374src/databasev1/generator/render.py       # box/violin geometry + targeted legend dispatcher375src/databasev1/generator/qa_render.py376 377src/databasev1/recipes/registry.py       # 仅增加 explicit legend policy,不改 legacy auto 行为378src/databasev1/recipes/scatter.py379src/databasev1/recipes/bar.py380src/databasev1/recipes/statistical.py381src/databasev1/recipes/__init__.py382 383src/databasev1/generation_plan.py        # 新增 v6 scope filter:冻结 area,移除 set_relation384src/databasev1/constants.py              # 只删除 set_relation;area mapping 保留385src/databasev1/axis_policy.py386src/databasev1/adapter/scene.py387src/databasev1/audit_yh.py388src/databasev1/review_batch.py389```390 391需要删除:392 393```text394src/databasev1/recipes/set_relation.py395```396 397### Area 冻结文件/代码块398 399以下内容**不允许修改语义**:400 401```text402src/databasev1/recipes/area.py403src/databasev1/generator/charts.py::draw_area404src/databasev1/generator/render.py::elif dk == "area"405src/databasev1/generator/labels.py::draw_kind == "area"406src/databasev1/adapter/common.py 的 area baseline 逻辑407src/databasev1/adapter/scene.py 的 area mapping408src/databasev1/adapter/quality.py 的 area compatibility409src/databasev1/validation/__init__.py 中 kind="area" 合法性410```411 412---413 414## 3.1 先增加 Area freeze guard415 416在修改任何生产代码前新增:417 418```text419tools/check_area_freeze.py420```421 422Agent 可直接放入以下代码:423 424```python425from __future__ import annotations426 427import ast428import hashlib429import json430from pathlib import Path431 432ROOT = Path(__file__).resolve().parents[1]433SRC = ROOT / "src" / "databasev1"434BASELINE = ROOT / "tests" / "baselines" / "area_freeze_v6.json"435 436 437def sha256_bytes(data: bytes) -> str:438    return hashlib.sha256(data).hexdigest()439 440 441def file_sha(path: Path) -> str:442    return sha256_bytes(path.read_bytes())443 444 445def function_ast_sha(path: Path, function_name: str) -> str:446    tree = ast.parse(path.read_text(encoding="utf-8"))447    for node in ast.walk(tree):448        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == function_name:449            payload = ast.dump(node, include_attributes=False).encode("utf-8")450            return sha256_bytes(payload)451    raise RuntimeError(f"function not found: {path}:{function_name}")452 453 454def current() -> dict:455    return {456        "area_recipe_file": file_sha(SRC / "recipes" / "area.py"),457        "draw_area_ast": function_ast_sha(458            SRC / "generator" / "charts.py",459            "draw_area",460        ),461    }462 463 464def main() -> int:465    import argparse466 467    ap = argparse.ArgumentParser()468    ap.add_argument("--capture", action="store_true")469    args = ap.parse_args()470 471    now = current()472 473    if args.capture:474        BASELINE.parent.mkdir(parents=True, exist_ok=True)475        BASELINE.write_text(476            json.dumps(now, indent=2, sort_keys=True) + "\n",477            encoding="utf-8",478        )479        print(f"captured: {BASELINE}")480        return 0481 482    if not BASELINE.exists():483        raise SystemExit("missing area freeze baseline; run --capture before editing")484 485    old = json.loads(BASELINE.read_text(encoding="utf-8"))486 487    if old != now:488        print("AREA FREEZE VIOLATION")489        print("baseline:", old)490        print("current :", now)491        return 1492 493    print("AREA FREEZE PASS")494    return 0495 496 497if __name__ == "__main__":498    raise SystemExit(main())499```500 501在任何修改前执行:502 503```bash504python tools/check_area_freeze.py --capture505```506 507修改后每次测试执行:508 509```bash510python tools/check_area_freeze.py511```512 513这保证:514 515```text516recipes/area.py 未改变517draw_area() AST 未改变518```519 520---521 522## 3.2 Area 功能冻结回归523 524仅源码 hash 不够,因为通用 renderer / legend / axis helper 可能间接改变 Area。525 526因此还必须保留一个 fixed-seed Area legacy fixture:527 528```text529tests/fixtures/area_freeze_v6/530```531 532至少覆盖:533 534```text535single_fill536two_overlapping537stacked_areas538baseline_offset539outline_only540```541 542比较以下**结构信息**,不要比较完整 PNG hash:543 544```text545series count546axis scale/type547truth x/y/baseline548area polygon vertex count549area bbox550legend variant551```552 553允许字体/backend 引起极小像素差,但 Area truth/geometry 不能语义变化。554 555---556 557# 4. P0:替换 `draw_box()`558 559文件:560 561```text562src/databasev1/generator/charts.py563```564 565将当前 `draw_box()` **完整替换**为:566 567```python568def draw_box(ax, series: list[dict], config: dict, fontsize: float) -> tuple[dict, list]:569    """高质量 box plot。570 571    v6 contract:572    - category anchor 使用 0..n-1,与 CategoryMapper/ensure_categorical_ticks 完全一致;573    - horizontal/vertical 均保持同一 anchor 语义;574    - raw points 使用 scatter,不生成伪折线;575    - 每个 group 可独立颜色;576    - categorical plot area 保留固定边距,避免第一/最后一组贴边。577    """578    data = [np.asarray(s["raw"], float) for s in series]579    colors = [s["style"].get("color", "#1f77b4") for s in series]580    categories = [str(c) for c in config["categories"]]581 582    n = len(data)583    pos = np.arange(n, dtype=float)   # 关键:0-based,不再 1..n584    horizontal = bool(config.get("horizontal", False))585 586    bp = ax.boxplot(587        data,588        positions=pos,589        orientation="horizontal" if horizontal else "vertical",590        notch=bool(config.get("notch", False)),591        patch_artist=True,592        showmeans=bool(config.get("means", False)),593        showfliers=True,594        manage_ticks=False,595        widths=float(config.get("width", 0.58)),596        whis=1.5,597        medianprops={598            "color": "black",599            "linewidth": 1.4,600        },601        whiskerprops={602            "color": "black",603            "linewidth": 1.0,604        },605        capprops={606            "color": "black",607            "linewidth": 1.0,608        },609        meanprops={610            "marker": "D",611            "markerfacecolor": "white",612            "markeredgecolor": "black",613            "markersize": 4.0,614        },615        flierprops={616            "marker": "o",617            "markerfacecolor": "none",618            "markeredgecolor": "black",619            "markersize": 3.0,620            "alpha": 0.75,621        },622    )623 624    for patch, color in zip(bp["boxes"], colors):625        patch.set_facecolor(color)626        patch.set_edgecolor("black")627        patch.set_linewidth(1.0)628        patch.set_alpha(float(config.get("alpha", 0.72)))629 630    extra_pts = []631 632    if bool(config.get("points", False)):633        g = np.random.default_rng(int(config.get("jitter_seed", 0)))634        max_points = int(config.get("max_points", 80))635 636        for i, (d, color) in enumerate(zip(data, colors)):637            if len(d) > max_points:638                choose = np.sort(g.choice(len(d), size=max_points, replace=False))639                d_show = d[choose]640            else:641                d_show = d642 643            jitter = g.uniform(-0.12, 0.12, len(d_show))644            cat = np.full(len(d_show), pos[i], dtype=float) + jitter645 646            if horizontal:647                art = ax.scatter(648                    d_show,649                    cat,650                    s=float(config.get("point_size", 14.0)),651                    facecolors="none",652                    edgecolors=color,653                    linewidths=0.8,654                    alpha=0.55,655                    zorder=4,656                )657            else:658                art = ax.scatter(659                    cat,660                    d_show,661                    s=float(config.get("point_size", 14.0)),662                    facecolors="none",663                    edgecolors=color,664                    linewidths=0.8,665                    alpha=0.55,666                    zorder=4,667                )668            extra_pts.append(art)669 670    if horizontal:671        ax.set_yticks(pos)672        ax.set_yticklabels(categories, rotation=0)673        ax.set_ylim(n - 0.45, -0.55) if config.get("first_category_top", False) \674            else ax.set_ylim(-0.55, n - 0.45)675    else:676        ax.set_xticks(pos)677        ax.set_xticklabels(678            categories,679            rotation=int(config.get("rotation", 0)),680            ha="right" if int(config.get("rotation", 0)) in (30, 45) else "center",681        )682        ax.set_xlim(-0.55, n - 0.45)683 684    return bp, extra_pts685```686 687---688 689# 5. P0:Box recipe 改为更丰富的真实统计分布690 691文件:692 693```text694src/databasev1/recipes/statistical.py695```696 697## 5.1 替换 `_grouped_samples()`698 699将当前函数替换为:700 701```python702def _grouped_samples(rng, ctx, n_groups: int, yq, g):703    """生成更接近真实科研统计图的 group distributions。704 705    分布模式:706    - normal707    - skewed708    - bimodal709    - heavy_tail710 711    仍保证总体落在 quantity 合理范围内。712    """713    groups = []714    span = float(yq.hi - yq.lo)715 716    modes = ["normal", "skewed", "bimodal", "heavy_tail"]717 718    for k in range(n_groups):719        n = int(rng.choice([28, 36, 48, 64]))720 721        frac = 0.20 + 0.60 * k / max(n_groups - 1, 1)722        center = yq.lo + frac * span723        center += float(g.normal(0.0, 0.035 * span))724 725        scale = float(rng.uniform(0.045, 0.105) * span)726        mode = rng.choice(modes)727 728        if mode == "normal":729            z = g.normal(0.0, 1.0, n)730 731        elif mode == "skewed":732            z = g.lognormal(mean=0.0, sigma=rng.uniform(0.35, 0.65), size=n)733            z = z - np.median(z)734 735        elif mode == "bimodal":736            n1 = n // 2737            n2 = n - n1738            sep = rng.uniform(1.0, 2.0)739            z = np.concatenate([740                g.normal(-sep / 2.0, 0.55, n1),741                g.normal(+sep / 2.0, 0.55, n2),742            ])743            g.shuffle(z)744 745        else:  # heavy_tail746            z = g.standard_t(df=rng.choice([3, 4, 5]), size=n)747 748        # robust normalization:保留分布形态,同时控制整体 scale749        med = float(np.median(z))750        mad = float(np.median(np.abs(z - med)))751        robust_sigma = max(1.4826 * mad, float(np.std(z)), 1e-9)752        z = (z - med) / robust_sigma753 754        # heteroscedastic:不同组 sigma 不完全一致755        local_scale = scale * rng.uniform(0.75, 1.35)756        grp = center + local_scale * z757 758        margin = 0.025 * span759        grp = np.clip(grp, yq.lo + margin, yq.hi - margin)760 761        groups.append(np.asarray(grp, float))762 763    return groups764```765 766---767 768## 5.2 替换 `box()` recipe769 770```python771@R.register(772    "statistical",773    "box",774    ["single", "grouped", "notched", "horizontal", "with_points", "heterogeneous"],775)776def box(rng, ctx) -> dict:777    p = R.new_panel(ctx, chart_type="box")778 779    variants = R.get_recipe("statistical", "box")["param_variants"]780    variant = R.choose_variant(ctx, variants)781    p["param_variant"] = variant782 783    if variant == "single":784        n_groups = 1785    else:786        n_groups = rng.randint(3, 5)787 788    yq = rng.choice([q for q in Q.Y.values() if not q.log])789    g = R.sub_rng(ctx, "box")790    cats = _exact_cats(rng, n_groups)791 792    # 关键:每组独立颜色,而不是所有 box 同一个颜色793    pal, colors = R.pick_palette(ctx, n_groups)794    groups = _grouped_samples(rng, ctx, n_groups, yq, g)795 796    for k, grp in enumerate(groups):797        st = R.series_style(rng, colors[k], kind="box", n=n_groups, k=k)798        st.update(linestyle="solid", marker="none")799 800        p["series"].append({801            "series_id": f"{ctx['panel_id']}_series_{k+1}",802            "kind": "distribution",803            "draw_kind": "box",804            "label": cats[k],805            "role": "data",806            "axis": "y",807            "style": st,808            "raw": grp.tolist(),809            "yerr": None,810            "truth": None,811        })812 813    show_points = (814        variant == "with_points"815        or variant == "heterogeneous"816        or rng.random() < 0.45817    )818 819    p["extras"]["box"] = {820        "categories": cats,821        "notch": variant == "notched",822        "horizontal": variant == "horizontal",823        "points": show_points,824        "means": rng.random() < 0.30,825        "width": rng.uniform(0.48, 0.66),826        "alpha": rng.uniform(0.62, 0.82),827        "jitter_seed": R.stable_derive_int(ctx, "jitter"),828        "point_size": rng.uniform(10.0, 18.0),829        "max_points": 80,830        "rotation": 0,831        "first_category_top": False,832    }833 834    p["extras"]["panel_truth"] = _box_truth(cats, groups)835 836    if variant == "horizontal":837        p["axes"]["x"] = R.axis_spec_from_quantity(rng, yq, "x")838        p["axes"]["y"] = R.categorical_axis(rng, cats)839    else:840        p["axes"]["x"] = R.categorical_axis(rng, cats)841        p["axes"]["y"] = R.axis_spec_from_quantity(rng, yq, "y")842 843    p["features"] = ["box", variant]844 845    if show_points:846        p["features"].append("raw_points")847 848    return p849```850 851---852 853# 6. P0:重写 violin renderer,不再调用 `ax.violinplot()`854 855## 6.1 原则856 857v6 的 violin 必须:858 859```text860recipe KDE861      ↓862support + density863      ↓864renderer polygon865      ↓866GT density867```868 869必须是同一份数据。870 871---872 873## 6.2 替换 `draw_violin()`874 875文件:876 877```text878src/databasev1/generator/charts.py879```880 881完整替换:882 883```python884def draw_violin(ax, series: list[dict], config: dict, fontsize: float) -> tuple[dict, list]:885    """Custom violin renderer。886 887    不调用 matplotlib.violinplot():888    renderer 直接使用 recipe 已计算的 support/density,889    保证 rendered geometry == GT KDE。890    """891    categories = [str(c) for c in config["categories"]]892    supports = config["support"]893    densities = config["density"]894 895    medians = np.asarray(config["median"], float)896    q1 = np.asarray(config["q1"], float)897    q3 = np.asarray(config["q3"], float)898 899    horizontal = bool(config.get("horizontal", False))900    width = float(config.get("width", 0.78))901    show_points = bool(config.get("points", False))902 903    pos = np.arange(len(series), dtype=float)  # 0-based904 905    bodies = []906    median_artists = []907    iqr_artists = []908    point_artists = []909 910    g = np.random.default_rng(int(config.get("jitter_seed", 0)))911 912    for i, s in enumerate(series):913        color = s["style"].get("color", "#1f77b4")914 915        support = np.asarray(supports[i], float)916        density = np.asarray(densities[i], float)917 918        if support.ndim != 1 or density.ndim != 1 or len(support) != len(density):919            raise ValueError("invalid violin support/density")920 921        peak = max(float(np.max(density)), 1e-12)922        half = 0.5 * width * density / peak923        center = pos[i]924 925        if horizontal:926            body = ax.fill_between(927                support,928                center - half,929                center + half,930                facecolor=color,931                edgecolor="black",932                linewidth=0.9,933                alpha=float(config.get("alpha", 0.68)),934                zorder=2.5,935            )936 937            iqr = ax.plot(938                [q1[i], q3[i]],939                [center, center],940                color="black",941                linewidth=3.0,942                solid_capstyle="butt",943                zorder=4,944            )[0]945 946            med = ax.scatter(947                [medians[i]],948                [center],949                s=18,950                c="white",951                edgecolors="black",952                linewidths=0.8,953                zorder=5,954            )955 956        else:957            body = ax.fill_betweenx(958                support,959                center - half,960                center + half,961                facecolor=color,962                edgecolor="black",963                linewidth=0.9,964                alpha=float(config.get("alpha", 0.68)),965                zorder=2.5,966            )967 968            iqr = ax.plot(969                [center, center],970                [q1[i], q3[i]],971                color="black",972                linewidth=3.0,973                solid_capstyle="butt",974                zorder=4,975            )[0]976 977            med = ax.scatter(978                [center],979                [medians[i]],980                s=18,981                c="white",982                edgecolors="black",983                linewidths=0.8,984                zorder=5,985            )986 987        bodies.append(body)988        iqr_artists.append(iqr)989        median_artists.append(med)990 991        if show_points:992            raw = np.asarray(s["raw"], float)993 994            max_points = int(config.get("max_points", 60))995            if len(raw) > max_points:996                choose = np.sort(g.choice(len(raw), size=max_points, replace=False))997                raw = raw[choose]998 999            jitter = g.uniform(-0.09, 0.09, len(raw))1000 1001            if horizontal:1002                art = ax.scatter(1003                    raw,1004                    center + jitter,1005                    s=9,1006                    c=color,1007                    alpha=0.28,1008                    edgecolors="none",1009                    zorder=3.5,1010                )1011            else:1012                art = ax.scatter(1013                    center + jitter,1014                    raw,1015                    s=9,1016                    c=color,1017                    alpha=0.28,1018                    edgecolors="none",1019                    zorder=3.5,1020                )1021 1022            point_artists.append(art)1023 1024    if horizontal:1025        ax.set_yticks(pos)1026        ax.set_yticklabels(categories, rotation=0)1027        ax.set_ylim(-0.60, len(series) - 0.40)1028    else:1029        ax.set_xticks(pos)1030        ax.set_xticklabels(1031            categories,1032            rotation=int(config.get("rotation", 0)),1033            ha="right" if int(config.get("rotation", 0)) in (30, 45) else "center",1034        )1035        ax.set_xlim(-0.60, len(series) - 0.40)1036 1037    return {1038        "bodies": bodies,1039        "medians": median_artists,1040        "iqr": iqr_artists,1041        "points": point_artists,1042    }, point_artists1043```1044 1045---1046 1047# 7. P0:重写 violin recipe1048 1049将当前:1050 1051```python1052@R.register("statistical", "violin", ["single", "grouped", "split_pairs", "horizontal", "narrow"])1053```1054 1055替换为:1056 1057```python1058@R.register(1059    "statistical",1060    "violin",1061    ["single", "grouped", "horizontal", "with_points", "bimodal"],1062)1063def violin(rng, ctx) -> dict:1064    p = R.new_panel(ctx, chart_type="box")1065 1066    variants = R.get_recipe("statistical", "violin")["param_variants"]1067    variant = R.choose_variant(ctx, variants)1068    p["param_variant"] = variant1069 1070    n_groups = 1 if variant == "single" else rng.randint(2, 4)1071 1072    yq = rng.choice([q for q in Q.Y.values() if not q.log])1073    g = R.sub_rng(ctx, "violin")1074    cats = _exact_cats(rng, n_groups)1075 1076    pal, colors = R.pick_palette(ctx, n_groups)1077 1078    groups = _grouped_samples(rng, ctx, n_groups, yq, g)1079 1080    # bimodal variant 强制至少让部分 group 呈明显双峰1081    if variant == "bimodal":1082        span = float(yq.hi - yq.lo)1083        groups2 = []1084        for k, grp in enumerate(groups):1085            n = len(grp)1086            center = float(np.median(grp))1087            shift = rng.uniform(0.055, 0.10) * span1088            n1 = n // 21089            n2 = n - n11090            sigma = rng.uniform(0.018, 0.035) * span1091 1092            arr = np.concatenate([1093                g.normal(center - shift, sigma, n1),1094                g.normal(center + shift, sigma, n2),1095            ])1096 1097            arr = np.clip(1098                arr,1099                yq.lo + 0.02 * span,1100                yq.hi - 0.02 * span,1101            )1102            groups2.append(arr)1103 1104        groups = groups21105 1106    support = []1107    density = []1108 1109    for k, grp in enumerate(groups):1110        sd = max(float(np.std(grp, ddof=1)), 1e-9)1111        bw = rng.uniform(0.22, 0.40) * sd1112 1113        sup = _peaked_support(grp, bw)1114        dens = _kde_profile(sup, grp, bw)1115 1116        support.append(sup.tolist())1117        density.append(dens.tolist())1118 1119        st = R.series_style(1120            rng,1121            colors[k],1122            kind="violin",1123            n=n_groups,1124            k=k,1125        )1126 1127        p["series"].append({1128            "series_id": f"{ctx['panel_id']}_series_{k+1}",1129            "kind": "distribution",1130            "draw_kind": "violin",1131            "label": cats[k],1132            "role": "data",1133            "axis": "y",1134            "style": st,1135            "raw": grp.tolist(),1136            "yerr": None,1137            "truth": None,1138        })1139 1140    med = [float(np.median(grp)) for grp in groups]1141    q1 = [float(np.percentile(grp, 25)) for grp in groups]1142    q3 = [float(np.percentile(grp, 75)) for grp in groups]1143 1144    show_points = variant == "with_points" or rng.random() < 0.251145 1146    p["extras"]["violin"] = {1147        "categories": cats,1148        "horizontal": variant == "horizontal",1149        "width": rng.uniform(0.70, 0.88),1150        "alpha": rng.uniform(0.58, 0.74),1151        "support": support,1152        "density": density,1153        "median": med,1154        "q1": q1,1155        "q3": q3,1156        "points": show_points,1157        "jitter_seed": R.stable_derive_int(ctx, "violin_points"),1158        "max_points": 60,1159        "rotation": 0,1160    }1161 1162    p["extras"]["panel_truth"] = {1163        "kind": "distribution",1164        "representation": "violin",1165        "category": [str(c) for c in cats],1166        "support_x": support,1167        "density": density,1168        "median": med,1169        "q1": q1,1170        "q3": q3,1171        "whisker_low": [float(np.min(grp)) for grp in groups],1172        "whisker_high": [float(np.max(grp)) for grp in groups],1173        "outliers": [[] for _ in groups],1174    }1175 1176    horizontal = variant == "horizontal"1177 1178    if horizontal:1179        p["axes"]["x"] = R.axis_spec_from_quantity(rng, yq, "x")1180        p["axes"]["y"] = R.categorical_axis(rng, cats)1181    else:1182        p["axes"]["x"] = R.categorical_axis(rng, cats)1183        p["axes"]["y"] = R.axis_spec_from_quantity(rng, yq, "y")1184 1185    p["features"] = ["violin", variant]1186 1187    if show_points:1188        p["features"].append("raw_points")1189 1190    return p1191```1192 1193---1194 1195# 8. P0:修复 box / violin series-level geometry1196 1197文件:1198 1199```text1200src/databasev1/generator/render.py

Showing the first 1,200 of 3502 lines. Download the file for the rest.

ZZoutian/figure2data-database-v2 · Team Ai