From 4a126d6dd9625d95fbcdd8974de0c27d5982aa38 Mon Sep 17 00:00:00 2001 From: chendengji Date: Tue, 1 Sep 2026 20:53:12 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=B6=E6=96=AD=E9=93=BE=20+=20=E5=AD=97=E5=8F=B7/=E9=97=B4?= =?UTF-8?q?=E8=B7=9D=E8=81=94=E5=8A=A8=E9=87=8D=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 运行时断链(实测修复): - boxplot/eventplot: labels= 被 mpl 3.9+ 移除 → tick_labels / 手动设刻度 - hexbin/hist2d: 从 paired 重分到 xy_series(原预处理把 x 转 str 致崩) - triplot: 不再把 z 当 triangles 传入 ax.triplot - scatter3d/gridded/irregular: 预处理改 dropna(subset=) 避免 x/z 同列名返回重名列 DataFrame - ieeetran: 从 AVAILABLE_STYLES 移除(scienceplots 无此样式, 调用抛 OSError) - 导入: 改双模式 + 新增 src/__init__.py, 修 CLI sci_plot.py 导入顺序与 --columns 列名映射 字号/间距(核心): - plot_chart 不再覆盖样式自带 figsize(nature 原为 3.3x2.5+7pt 被强制改成 5x4 致大图小字) - 新增 _apply_typography: figsize 自定义时字号按比例联动, fontsize 作主控钮 - 新增 kwargs: figsize/fontsize/constrained_layout(2D 默认 True)/legend_loc/legend_frame(默认 False) - 3D 因 constrained 支持有限自动回退 tight_layout 文档修正: - SKILL.md/README/environment.yml: env 名 geo(非 geo3.13), 图表名 line/scatter(非 xy_line) - README 删除不存在的 cell/plos/elsevier/springer/aps/jgr 等样式, 修正 plot_chart 签名与图表计数(37) Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 78 ++++------ SKILL.md | 172 ++++++++++++--------- environment.yml | 2 +- src/__init__.py | 1 + src/charts/stats/boxplot.py | 2 +- src/charts/stats/eventplot.py | 5 +- src/charts/unstructured/triplot.py | 5 +- src/process_data.py | 16 +- src/sci_plot.py | 14 +- src/sci_plot_api.py | 238 +++++++++++++++++++++++------ 10 files changed, 350 insertions(+), 183 deletions(-) create mode 100644 src/__init__.py diff --git a/README.md b/README.md index e35a3b6..27e5a84 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-📊 **一键生成专业级科研图表** — 30+ 期刊风格,500+ 参考代码,开箱即用 +📊 **一键生成专业级科研图表** — 4 种期刊风格,37 种图表,500+ 参考代码,开箱即用 [![Python](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://python.org) [![matplotlib](https://img.shields.io/badge/matplotlib-3.5%2B-red.svg)](https://matplotlib.org) @@ -20,7 +20,7 @@ Sci-Plot 是一个面向科研场景的 **matplotlib 风格化绘图框架**, |----------|------| | **零样板代码** | 预置 510+ 官方 gallery 参考实现,用户无需从零编写绘图代码,直接调用即可生成符合期刊规范的图表 | | **Agent 友好** | 标准化 API 接口支持自动化调用,可无缝集成至 AI Agent 工作流,实现"数据输入 → 图表生成"的全自动 pipeline | -| **期刊级规范** | 内置 30+ 主流期刊风格(Nature、Science、IEEE、Cell 等),确保输出图表直接满足投稿要求 | +| **期刊级规范** | 内置 4 种期刊风格(Nature / Science / IEEE / grid),确保输出图表满足投稿要求 | | **效率与稳定性** | 统一的图表接口 + 完整测试覆盖,消除样式调试的不确定性,将绘图时间从数小时缩短至分钟级 | ### 适用场景 @@ -32,7 +32,7 @@ Sci-Plot 是一个面向科研场景的 **matplotlib 风格化绘图框架**, ### 技术特点 -- **模块化设计**:基础图表、统计图表、网格/场数据、三维可视化、非规则网格五大模块,覆盖 50+ 图表类型 +- **模块化设计**:基础图表、统计图表、网格/场数据、三维可视化、非规则网格五大模块,覆盖 37 种图表类型 - **多数据格式支持**:原生支持 CSV、Excel,自动处理分组、网格化、插值等数据预处理 - **双入口架构**:CLI 命令行适合交互式使用,Python API 适合程序化调用 @@ -43,7 +43,7 @@ Sci-Plot 是一个面向科研场景的 **matplotlib 风格化绘图框架**, ```bash # 方式一:conda(推荐) conda env create -f environment.yml -conda activate geo3.13 +conda activate geo # 方式二:pip pip install scienceplots pandas numpy matplotlib @@ -69,24 +69,21 @@ python src/sci_plot.py \ ```python import sys -sys.path.insert(0, 'src') -from sci_plot_api import plot_chart +sys.path.insert(0, '/home/chen/claude/.claude/skills/sci-plot') # 技能根目录 +from src.sci_plot_api import plot_chart, read_data # 加载数据 -df = plot_chart.read_data('data.csv') -columns = plot_chart.auto_detect_columns(df, 'paired') - -# 生成图表 -fig, ax = plot_chart( - df, - data_type='paired', - chart_type='violin', - style='science', - columns=columns -) -fig.savefig('figs/violin_science.png', dpi=300, bbox_inches='tight') +df = read_data('data.csv') + +# 生成图表(返回保存路径字符串,不是 (fig, ax)) +path = plot_chart(df, chart_type='violin', + x_col='group', y_col='value', + style='science') +print(path) ``` +`plot_chart` 完整 kwargs:`x_col / y_col / z_col / group_col`(列映射)、`style`(默认 `nature`)、`title`、`figsize`(None=样式自带,字号按比例联动)、`fontsize`(主控钮)、`dpi`、`constrained_layout`(默认 True)、`legend_loc`、`legend_frame`(默认 False)。 + ## 支持的图表类型 ### 基础图表 (basic) @@ -146,23 +143,14 @@ fig.savefig('figs/violin_science.png', dpi=300, bbox_inches='tight') ## 支持的期刊风格 -| 风格 | 对应期刊 | -|------|----------| -| `nature` | Nature | -| `science` | Science | -| `ieee` | IEEE Transactions | -| `ieee tran` | IEEEtran | -| `cell` | Cell | -| `plos` | PLOS ONE | -| `elsevier` | Elsevier 期刊 | -| `springer` | Springer 期刊 | -| `aps` | APS 物理期刊 | -| `jgr` | JGR (地球物理) | -| `glt` | 地学线性图 | -| `grid` | 基础网格风格 | -| `scienceplain` | 简洁科学风 | - -> 完整列表见 `src/config.py` 中的 `STYLES` 字典。 +| 风格 | 对应期刊 | 自带 figsize | font.size | +|------|----------|-------------|-----------| +| `nature` | Nature | (3.3, 2.5) | 7.0 | +| `science` | Science | (3.3, 2.5) | 7.0 | +| `ieee` | IEEE Transactions | (3.5, 2.6) | 8.0 | +| `grid` | 带网格科学风 | (3.3, 2.5) | 7.0 | + +> figsize 与 font.size 配套设计;`plot_chart` 默认不覆盖,传 `figsize=` 时字号按比例联动,传 `fontsize=` 则作为主控钮。完整列表见 `src/sci_plot_api.py` 的 `AVAILABLE_STYLES`。 ## 项目结构 @@ -173,12 +161,12 @@ sci-plot/ │ ├── sci_plot_api.py # Python API 入口 │ ├── config.py # 配置(风格、路径、数据) │ ├── process_data.py # 数据处理(6 种数据类型) -│ ├── charts/ # 43 个图表脚本 -│ │ ├── basic/ # 基础图表 (8) -│ │ ├── stats/ # 统计图表 (10) -│ │ ├── arrays/ # 网格/场数据 (8) -│ │ ├── plot3d/ # 三维图表 (11) -│ │ └── unstructured/ # 非规则网格 (5) +│ ├── charts/ # 37 个图表脚本 +│ │ ├── basic/ # 基础图表 (7) +│ │ ├── stats/ # 统计图表 (9) +│ │ ├── arrays/ # 网格/场数据 (7) +│ │ ├── plot3d/ # 三维图表 (10) +│ │ └── unstructured/ # 非规则网格 (4) │ └── gallery_python/ # 510+ 官方参考代码 │ ├── lines_bars_and_markers/ │ ├── statistics/ @@ -200,7 +188,7 @@ sci-plot/ | 数据类型 | 说明 | 推荐图表 | |----------|------|----------| | `paired` | 成对分组数据 | bar, boxplot, violin, hist | -| `xy_series` | 连续 XY 序列 | line, scatter | +| `xy_series` | 连续 XY 序列 | line, scatter, hexbin, hist2d | | `distribution` | 统计分布 | violin, hist, ecdf, pie | | `gridded` | 规则网格 (X,Y,Z) | pcolormesh, contour, quiver | | `irregular` | 非规则网格 (x,y,z) | tricontour, tripcolor | @@ -244,10 +232,10 @@ python src/sci_plot.py --interactive ### 批量生成多风格对比 ```python -from sci_plot_api import plot_chart +from src.sci_plot_api import plot_chart for style in ['nature', 'science', 'ieee', 'grid']: - fig, ax = plot_chart(df, 'paired', 'bar', style, columns) - fig.savefig(f'figs/bar_{style}.png', dpi=300, bbox_inches='tight') + path = plot_chart(df, 'bar', x_col='group', y_col='value', style=style) + print(style, path) ``` ## 依赖 diff --git a/SKILL.md b/SKILL.md index 1e2821f..7d028b2 100644 --- a/SKILL.md +++ b/SKILL.md @@ -4,121 +4,140 @@ ## 前置条件 -- 环境: `conda geo3.13` (或安装了 scienceplots 的 Python 环境) -- 工作目录: `D:\CODE\sci-plot` -- 脚本路径: `src/sci_plot_api.py` (统一API入口) +- **环境**: `conda geo`(已装 scienceplots;不是 geo3.13) +- **技能根目录**: `/home/chen/claude/.claude/skills/sci-plot`(本 SKILL.md 所在目录) +- **统一 API 入口**: `src/sci_plot_api.py` 中的 `plot_chart()` + +### 导入方式(任选其一) + +```python +# 方式 A(推荐):把技能根目录加入 sys.path,输出写到用户当前工作目录/plot/ +import sys +sys.path.insert(0, '/home/chen/claude/.claude/skills/sci-plot') +from src.sci_plot_api import plot_chart, read_data, auto_detect_columns, recommend_charts, list_charts, list_styles +``` + +```bash +# 方式 B:cd 到技能根目录后用 -m 运行(输出会写到技能目录/plot/,不建议) +cd /home/chen/claude/.claude/skills/sci-plot && python -m src.sci_plot_api -i data.csv -ch line --xcol x --ycol y +``` + +> 旧写法 `from src.sci_plot_api import ...` 若不先把技能根目录加入 `sys.path`,会报 `ModuleNotFoundError: No module named 'config'`。务必先 `sys.path.insert`。 + +## 字号与间距约定(重要) + +scienceplots 的期刊样式是「**figsize + font.size 配套**」设计的,不可单独放大其一: + +| 样式 | 自带 figsize | 自带 font.size | 适用 | +|------|-------------|---------------|------| +| `nature` | (3.3, 2.5) | 7.0 | 期刊单栏(默认) | +| `science` | (3.3, 2.5) | 7.0 | 通用科学风 | +| `ieee` | (3.5, 2.6) | 8.0 | IEEE | +| `grid` | (3.3, 2.5) | 7.0 | 带网格 | + +**默认行为**:`plot_chart` 不再覆盖样式自带 figsize,字号也保持样式基准——图按原尺寸放进论文版面时比例正好。 + +**调整字号/尺寸的 kwarg**: + +```python +plot_chart(df, 'line', x_col='x', y_col='y', + style='nature', + figsize=(6, 4), # 自定义尺寸:字号会按 新宽/3.3 比例自动放大,保持比例 + fontsize=10, # 或直接指定基准字号(pt),作为主控钮覆盖一切 + constrained_layout=True, # 约束布局:多子图/colorbar/legend 不打架(2D 默认开) + legend_loc='upper left', # 图例位置 + legend_frame=False) # 图例边框,默认关 +``` + +- **只放大图不改字号 → 字会偏小**:已被 `_apply_typography` 修复,传 `figsize=` 时字号按比例同步放大。 +- **多子图/带 colorbar 的图**:保持 `constrained_layout=True`(默认),不要退回 `tight_layout`,否则易裁切/重叠。 +- **3D 图**:`constrained_layout` 对 3D 支持有限,自动回退 `tight_layout`。 +- **中文**:自动注册 WSL2 挂载的 SimHei/SimSun,无需额外配置;`axes.unicode_minus` 已置 False。 ## 工作流程 ### Step 1: 询问数据位置 -- 问用户数据文件在哪里 -- 支持 CSV, Excel, TXT, DAT -- 读取后展示: 前 5 行、列名、数据类型 +- 问用户数据文件在哪里(CSV / Excel / TXT / DAT) +- 读取后展示:前 5 行、列名、数据类型 ```python -from src.sci_plot_api import read_data df = read_data("用户提供的路径") -print(df.head()) -print(df.dtypes) +print(df.head()); print(df.dtypes) ``` ### Step 2: 分析数据 + 列映射 -**自动检测列类型:** -```python -from src.sci_plot_api import auto_detect_columns -col_info = auto_detect_columns(df) -# 返回: {'numeric': [...], 'categorical': [...], 'other': [...]} -``` - -**推荐图表:** ```python -from src.sci_plot_api import recommend_charts -recs = recommend_charts(df) -# 返回: [(chart_type, description), ...] +col_info = auto_detect_columns(df) # {'numeric': [...], 'categorical': [...], 'other': [...]} +recs = recommend_charts(df) # [(chart_type, 说明), ...] ``` -**与用户确认列映射:** -> 检测到以下列: -> 数值列: axial_strain, axial_stress_mpa -> 分类列: sample_code -> 推荐图表: xy_line (连续x-y折线图) -> 哪列是 x(自变量)? [axial_strain] -> 哪列是 y(因变量)? [axial_stress_mpa] -> 是否需要分组列? [sample_code] +与用户确认列映射:哪列是 x、哪列是 y、是否需要 group_col。 ### Step 3: 选择图表类型 -根据数据类型选择: - | 数据类型 | 列需求 | 可用图表 | |---------|--------|---------| -| **成对数据** | 1 分类 + 1 数值 | bar, boxplot, violin, scatter, line, hist, ecdf, pie, stem, point, swarm, errorbar, stairs, stackplot, fill_between, eventplot | -| **连续XY数据** | 2 数值 + 可选分组 | **xy_line**, **xy_scatter** ← 应力应变曲线、时间序列 | -| **网格数据** | x, y, z 三列 | contour, contourf, pcolormesh, imshow, quiver, streamplot, barbs | -| **不规则数据** | x, y, z 三列 | tricontour, tricontourf, tripcolor, triplot | -| **3D数据** | x, y, z 三列+ | plot3d, scatter3d, surface3d, wire3d, bar3d, voxels, trisurf3d | +| **成对数据** | 1 分类 + 1 数值 | `bar boxplot violin hist ecdf pie stem errorbar eventplot stairs stackplot fill_between` | +| **连续 XY 数据** | 2 数值 + 可选分组 | `line` `scatter` `hexbin` `hist2d` ← 应力应变、时间序列、二维密度 | +| **网格数据** | x, y, z | `contour contourf pcolormesh imshow quiver streamplot barbs` | +| **不规则数据** | x, y, z | `tricontour tricontourf tripcolor triplot` | +| **3D 数据** | x, y, z (+可选 c/v) | `plot3d scatter3d surface3d wire3d bar3d stem3d voxels trisurf3d` | + +> ⚠️ 图表类型名用 `line` / `scatter`,**不是** `xy_line` / `xy_scatter`(旧文档写法已废弃,注册表无此类型会报 `Unknown chart type`)。 +> `hexbin` / `hist2d` 需要两个**数值**列(属 xy_series),不要当成分组柱状图用。 -完整列表: ```python -from src.sci_plot_api import list_charts -all_charts = list_charts() # 41 种图表 +all_charts = list_charts() # 37 种 ``` ### Step 4: 选择样式 ```python -from src.sci_plot_api import list_styles -styles = list_styles() # ['science', 'nature', 'ieee', 'ieeetran', 'grid'] +styles = list_styles() # ['science', 'nature', 'ieee', 'grid'] ``` -| 样式 | 适用场景 | -|------|---------| +| 样式 | 适用 | +|------|------| | `science` | 通用科学风格 | | `nature` | **Nature 期刊风格(默认)** | | `ieee` | IEEE 期刊风格 | -| `ieeetran` | IEEE 双栏排版 | | `grid` | 带网格线的科学风格 | +> 旧文档列的 `ieeetran / cell / plos / elsevier / springer / aps / jgr` 等样式 scienceplots **未提供**,调用会抛 `OSError`。仅上述 4 种可用。 + ### Step 5: 生成图表 -**成对数据 (分组统计图):** +**成对数据(分组统计图)**: ```python -from src.sci_plot_api import plot_chart - -path = plot_chart(df, 'bar', # 图表类型 - x_col='group', # 分组列 - y_col='value', # 数值列 - style='science', # 样式 - title='Bar Plot') # 可选标题 +path = plot_chart(df, 'bar', + x_col='group', y_col='value', + style='science', title='Bar Plot') print(f"Saved: {path}") ``` -**连续XY数据 (应力应变曲线、时间序列):** +**连续 XY 数据(应力应变曲线、时间序列)**: ```python -path = plot_chart(df, 'xy_line', # 连续x-y折线图 - x_col='axial_strain', # x轴列 - y_col='axial_stress_mpa', # y轴列 - group_col='sample_code', # 分组列(可选) - style='nature', # 样式 - title='Stress-Strain Curve') # 标题 -print(f"Saved: {path}") +path = plot_chart(df, 'line', # 不是 'xy_line' + x_col='axial_strain', + y_col='axial_stress_mpa', + group_col='sample_code', # 可选分组 + style='nature', + title='Stress-Strain Curve') ``` -**数据会自动标准化处理** (去空值、排序、保存到 data/ 目录)。 +数据会自动标准化处理(去空值、排序、保存到 `plot/data/`)。 ### Step 6: 展示结果 -- 告诉用户图片保存路径 (`D:\CODE\sci-plot\figs\`) -- 展示图片给用户预览 -- 询问是否要调整参数或换一种图表类型 +- 告诉用户图片路径(`<当前工作目录>/plot/fig_{type}_{timestamp}.png`) +- 展示图片预览,询问是否调整参数或换图表类型 ## 批量生成多图 ```python -charts = ['bar', 'boxplot', 'violin', 'scatter'] -for chart in charts: +for chart in ['bar', 'boxplot', 'violin', 'scatter']: path = plot_chart(df, chart, x_col='group', y_col='value', style='nature') print(f" {chart}: {path}") ``` @@ -127,21 +146,22 @@ for chart in charts: ```python from src.sci_plot_api import quick_plot -path = quick_plot("data.csv", "xy_line", x_col='x', y_col='y', group_col='group') +path = quick_plot("data.csv", "line", x_col='x', y_col='y', group_col='group') ``` ## 输出文件 -- 图片: `D:\CODE\sci-plot\figs\fig_{type}_{timestamp}.png` -- 300 DPI, bbox_inches='tight' -- 时间戳命名,不会覆盖 -- 处理后的标准化数据自动保存到 `data/` 目录 +- 图片: `<当前工作目录>/plot/fig_{type}_{timestamp}.png`,300 DPI +- 时间戳命名,不覆盖 +- 标准化数据自动存到 `<当前工作目录>/plot/data/` +- 输出根目录可用环境变量 `SCI_PLOT_OUTPUT_DIR` 覆盖(如不想污染当前目录) ## 注意事项 -1. 原始数据文件**不存储在项目中**,用户自行管理 -2. `plot_chart` 会自动调用 `process_data.py` 进行数据标准化预处理 -3. 所有图表使用 `scienceplots` 库的样式,符合期刊美观要求 -4. 3D 图表会自动创建 `projection='3d'` 坐标轴 -5. 网格类图表 (contour, pcolormesh, surface3d等) 需要 x, y, z 三列 -6. 连续XY图表 (xy_line, xy_scatter) 的 `group_col` 是可选参数 \ No newline at end of file +1. 原始数据文件**不存进项目**,用户自行管理 +2. `plot_chart` 会自动调 `process_data` 做标准化预处理 +3. 所有图表用 `scienceplots` 样式,已包含 `no-latex`(无需本地 LaTeX) +4. 3D 图表自动创建 `projection='3d'` 坐标轴 +5. 网格/不规则/3D 类图表需要 x, y, z 三列 +6. `line`/`scatter` 的 `group_col` 是可选参数 +7. matplotlib ≥ 3.9 已移除 `boxplot`/`eventplot` 的 `labels=` 参数,本技能已改用 `tick_labels` / 手动设刻度 diff --git a/environment.yml b/environment.yml index 3eef39a..b09d97d 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,4 @@ -name: geo3.13 +name: geo channels: - conda-forge - defaults diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..4164907 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +"""sci-plot 源码包。统一 API 入口见 sci_plot_api.plot_chart。""" diff --git a/src/charts/stats/boxplot.py b/src/charts/stats/boxplot.py index 832961e..372ca6c 100644 --- a/src/charts/stats/boxplot.py +++ b/src/charts/stats/boxplot.py @@ -11,7 +11,7 @@ def plot(fig, ax, df, **kwargs): groups = sorted(df[x_col].unique()) data = [df[df[x_col] == g][y_col].values for g in groups] - bp = ax.boxplot(data, labels=groups, patch_artist=True, + bp = ax.boxplot(data, tick_labels=groups, patch_artist=True, boxprops=dict(linewidth=0.8), whiskerprops=dict(linewidth=0.8), capprops=dict(linewidth=0.8), diff --git a/src/charts/stats/eventplot.py b/src/charts/stats/eventplot.py index 8043d14..66fb0a2 100644 --- a/src/charts/stats/eventplot.py +++ b/src/charts/stats/eventplot.py @@ -13,7 +13,10 @@ def plot(fig, ax, df, **kwargs): # eventplot 不会自动使用颜色循环,需要手动设置 colors = [ax._get_lines.get_next_color() for _ in range(len(groups))] - ax.eventplot(data, labels=groups, colors=colors) + ax.eventplot(data, colors=colors) + # eventplot 无 tick_labels 参数,手动设置分组刻度(默认水平方向:y 轴为分组) + ax.set_yticks(range(1, len(groups) + 1)) + ax.set_yticklabels(groups) ax.set_title(kwargs.get('title', 'Event Plot')) ax.set_ylabel('Group') ax.set_xlabel('Position') diff --git a/src/charts/unstructured/triplot.py b/src/charts/unstructured/triplot.py index f191ac4..4a5bcfe 100644 --- a/src/charts/unstructured/triplot.py +++ b/src/charts/unstructured/triplot.py @@ -7,9 +7,12 @@ def plot(fig, ax, df, **kwargs): y_col = kwargs.get('y_col') z_col = kwargs.get('z_col') - ax.triplot(df[x_col], df[y_col], df[z_col], linewidth=0.5) + # ax.triplot 第三位置参数是 triangles(三角网索引),不是 z; + # 误传 df[z_col] 会触发 "truth value of a Series is ambiguous"。triplot 仅需 x, y。 + ax.triplot(df[x_col], df[y_col], linewidth=0.5) ax.set_title(kwargs.get('title', 'Triplot')) ax.set_xlabel(x_col) ax.set_ylabel(y_col) + _ = z_col # 预处理后保留 z 列但本图不用 return ax \ No newline at end of file diff --git a/src/process_data.py b/src/process_data.py index 4b00462..ab4cc86 100644 --- a/src/process_data.py +++ b/src/process_data.py @@ -8,7 +8,10 @@ import os import numpy as np import pandas as pd -from config import DATA_DIR +try: + from .config import DATA_DIR # 包模式 +except ImportError: + from config import DATA_DIR # 平铺模式 # ==================== 数据类型定义 ==================== @@ -19,15 +22,15 @@ 'columns': ['group', 'value'], 'charts': ['bar', 'boxplot', 'violin', 'hist', 'ecdf', 'pie', 'stem', 'errorbar', 'stairs', 'stackplot', 'fill_between', - 'eventplot', 'hexbin', 'hist2d'], + 'eventplot'], 'example': 'group: Control/Treated, value: 测量值' }, 'xy_series': { 'name': '连续XY数据 (XY Series)', 'description': '两个连续数值列,x-y 关系,可选分组', 'columns': ['x', 'y', 'group'], - 'charts': ['line', 'scatter'], - 'example': '应力应变曲线、时间序列' + 'charts': ['line', 'scatter', 'hexbin', 'hist2d'], + 'example': '应力应变曲线、时间序列、二维密度' }, 'distribution': { 'name': '统计分布 (Distribution)', @@ -187,7 +190,10 @@ def process_data(df, *args, timestamp=None, data_type='paired', v_col=None, grou xy_series 分组列 """ if timestamp is None: - from config import TIMESTAMP + try: + from .config import TIMESTAMP + except ImportError: + from config import TIMESTAMP timestamp = TIMESTAMP if data_type == 'paired': diff --git a/src/sci_plot.py b/src/sci_plot.py index fa055db..36e032e 100644 --- a/src/sci_plot.py +++ b/src/sci_plot.py @@ -14,10 +14,10 @@ import pandas as pd -from config import DATA_DIR, FIGS_DIR, TIMESTAMP +# 先把 src/ 加入路径,使平铺导入(from config import ...)在脚本模式下可用 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -# 使用新的 API 结构 -sys.path.insert(0, os.path.dirname(__file__)) +from config import DATA_DIR, FIGS_DIR, TIMESTAMP from sci_plot_api import plot_chart, list_charts, read_data, auto_detect_columns, recommend_charts from process_data import DATA_TYPES @@ -90,12 +90,16 @@ def main(): y = input(f"Y column (value) [{default_y}]: ").strip() or default_y args.columns = f"x={x},y={y}" - # 2. 解析列映射 + # 2. 解析列映射(CLI 短名 x/y/z/group → API 的 x_col/y_col/... ) + _COL_ALIASES = {'x': 'x_col', 'y': 'y_col', 'z': 'z_col', + 'group': 'group_col', 'u': 'u_col', 'v': 'v_col', + 'w': 'w_col', 'c': 'c_col', 'value': 'y_col'} columns = {} if args.columns: for pair in args.columns.split(','): key, val = pair.split('=') - columns[key.strip()] = val.strip() + key = _COL_ALIASES.get(key.strip(), key.strip()) + columns[key] = val.strip() print(f"\n[Chart] {args.chart}") print(f"[Style] {args.style}") diff --git a/src/sci_plot_api.py b/src/sci_plot_api.py index 0cde1e8..b2b0259 100644 --- a/src/sci_plot_api.py +++ b/src/sci_plot_api.py @@ -22,32 +22,109 @@ import matplotlib.font_manager as fm import scienceplots -from config import FIGS_DIR +try: + from .config import FIGS_DIR # 包模式: from src.sci_plot_api import ... +except ImportError: + from config import FIGS_DIR # 平铺模式: sys.path.insert(0,'src') # ==================== 中文字体配置 ==================== +# +# 优先使用系统自带的中文字体(黑体 SimHei、宋体 SimSun、微软雅黑 等)。 +# +# 已知问题:matplotlib 默认只扫描 Linux 字体目录,不扫描 WSL2 挂载的 +# /mnt/c/Windows/Fonts,因此 Windows 自带的 SimHei/SimSun 即便存在, +# findfont 也找不到(旧实现只按名字 findfont,所以在 WSL2 下中文乱码)。 +# 修复:主动用 addfont() 注册候选字体文件,再按优先级解析字体名。 +# 跨平台:Windows 原生 / WSL2 挂载点 / Linux / macOS 字体目录都尝试。 + +import glob as _glob + +# 候选 CJK 字体目录(跨平台) +_CJK_FONT_DIRS = [ + "/mnt/c/Windows/Fonts", # WSL2 → Windows C 盘 + "/mnt/d/Windows/Fonts", # WSL2 → Windows D 盘 + "C:/Windows/Fonts", # Windows 原生 + "/usr/share/fonts", # Linux + "/usr/local/share/fonts", + os.path.expanduser("~/.fonts"), + os.path.expanduser("~/.local/share/fonts"), + "/System/Library/Fonts", # macOS + "/Library/Fonts", +] -# 尝试注册中文字体,避免 scienceplots 的 STIX 字体无法显示中文 -_CJK_FONTS = [ - 'Microsoft YaHei', # Windows - 'SimHei', # Windows - 'WenQuanYi Micro Hei', # Linux - 'PingFang SC', # macOS - 'Noto Sans CJK SC', # Linux +# 候选 CJK 字体文件名(黑体/宋体/雅黑 优先) +_CJK_FONT_FILES = [ + "simhei.ttf", # SimHei 黑体 + "simsun.ttc", # SimSun 宋体 + "nsimsun.ttf", # NSimSun 新宋体 + "msyh.ttc", # Microsoft YaHei 微软雅黑 + "msyhbd.ttc", # Microsoft YaHei Bold + "simfang.ttf", # FangSong 仿宋 + "simkai.ttf", # KaiTi 楷体 + "STSONG.TTF", # STSong 华文宋体 + "wqy-zenhei.ttc", # WenQuanYi Zen Hei + "wqy-microhei.ttc", # WenQuanYi Micro Hei + "NotoSansCJKsc-Regular.otf", + "NotoSansSC-Regular.otf", + "SourceHanSansSC-Regular.otf", + "PingFang.ttc", +] + +# 字体名解析优先级:黑体 > 宋体 > 新宋体 > 雅黑 > 仿宋/楷体 > 文泉驿 > Noto > 思源 > 苹方 +_CJK_FONT_PRIORITY = [ + "SimHei", "SimSun", "NSimSun", "Microsoft YaHei", + "FangSong", "KaiTi", "STSong", + "WenQuanYi Zen Hei", "WenQuanYi Micro Hei", + "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", "PingFang SC", ] -for _fname in _CJK_FONTS: - try: - _fp = fm.findfont(_fname, fallback_to_default=False) - _CJK_FONT = _fname - break - except Exception: - _CJK_FONT = None -else: - _CJK_FONT = None + + +def _register_cjk_fonts(): + """扫描候选目录并用 addfont() 注册 CJK 字体文件(WSL2 下必需,仅内存生效)。""" + for d in _CJK_FONT_DIRS: + if not os.path.isdir(d): + continue + names = list(_CJK_FONT_FILES) + # glob 兜底:再扫该目录下其他 sim*/msyh*/wqy*/Noto*/SourceHan*/ST* 等 + for pat in ("sim*.tt[fc]", "msyh*.tt[fc]", "wqy*.tt[fc]", + "Noto*CJK*", "NotoSansSC*", "SourceHan*", + "PingFang*", "ST*.TTF", "*.otc"): + names += _glob.glob(os.path.join(d, pat)) + seen = set() + for fname in names: + p = fname if os.path.isabs(fname) else os.path.join(d, fname) + if p in seen or not os.path.isfile(p): + continue + try: + fm.fontManager.addfont(p) + seen.add(p) + except Exception: + pass + + +def _resolve_cjk_font(): + """按优先级返回第一个 matplotlib 可识别的 CJK 字体名。""" + for name in _CJK_FONT_PRIORITY: + try: + fm.findfont(name, fallback_to_default=False) + return name + except Exception: + continue + return None + + +# 模块导入时注册一次(仅作用于当前进程,不改动系统字体缓存) +_register_cjk_fonts() +_CJK_FONT = _resolve_cjk_font() def _setup_cjk_font(): - """配置中文字体回退,解决 scienceplots 下中文乱码。""" + """配置中文字体回退,解决 scienceplots(STIX) 下中文乱码。 + + 优先级:黑体(SimHei) > 宋体(SimSun) > 微软雅黑 > 文泉驿 > Noto... + 找不到任何 CJK 字体时不改 rcParams,避免破坏当前样式。 + """ if _CJK_FONT: plt.rcParams['font.family'] = 'sans-serif' # 将 CJK 字体加入 font.sans-serif 列表首位 @@ -117,9 +194,9 @@ def _setup_cjk_font(): 'boxplot': 'paired', 'violin': 'paired', 'hist': 'paired', 'ecdf': 'paired', 'pie': 'paired', 'errorbar': 'paired', 'eventplot': 'paired', - 'hexbin': 'paired', 'hist2d': 'paired', - # xy_series (x_col, y_col, group_col) + # xy_series (两个数值列 x_col, y_col, 可选 group_col) 'line': 'xy_series', 'scatter': 'xy_series', + 'hexbin': 'xy_series', 'hist2d': 'xy_series', # arrays → gridded 'contour': 'gridded', 'contourf': 'gridded', 'pcolormesh': 'gridded', 'imshow': 'gridded', 'quiver': 'gridded', 'streamplot': 'gridded', @@ -138,19 +215,25 @@ def _setup_cjk_font(): CHARTS_3D = {'plot3d', 'scatter3d', 'surface3d', 'wire3d', 'bar3d', 'stem3d', 'voxels', 'trisurf3d', 'quiver3d'} -# 可用样式 -AVAILABLE_STYLES = ['science', 'nature', 'ieee', 'ieeetran', 'grid'] +# 可用样式(scienceplots 实测可用;'ieeetran' 已移除,scienceplots 无此样式会抛 OSError) +AVAILABLE_STYLES = ['science', 'nature', 'ieee', 'grid'] # ==================== 核心 API ==================== def _import_chart_module(chart_type): - """动态导入图表模块。""" + """动态导入图表模块(兼容包模式与平铺模式两种调用方式)。 + + 包模式(from src.sci_plot_api import): __package__='src' → src.charts.basic.line + 平铺模式(sys.path.insert(0,'src'); from sci_plot_api import): __package__='' → charts.basic.line + """ if chart_type not in CHART_REGISTRY: raise ValueError(f"Unknown chart type: {chart_type}. " f"Available: {list(CHART_REGISTRY.keys())}") module_path, _ = CHART_REGISTRY[chart_type] - return importlib.import_module(module_path, package='src') + pkg = __package__ or '' + full = f'{pkg}.{module_path}' if pkg else module_path + return importlib.import_module(full) def _preprocess_data(df, chart_type, timestamp, **kwargs): @@ -164,11 +247,18 @@ def _preprocess_data(df, chart_type, timestamp, **kwargs): - 保存到 data/ 目录 """ # 延迟导入,避免循环依赖 - from process_data import ( - process_paired_data, process_xy_series_data, - process_distribution_data, process_gridded_data, - process_irregular_data, process_3d_data - ) + try: + from .process_data import ( + process_paired_data, process_xy_series_data, + process_distribution_data, process_gridded_data, + process_irregular_data, process_3d_data + ) + except ImportError: + from process_data import ( + process_paired_data, process_xy_series_data, + process_distribution_data, process_gridded_data, + process_irregular_data, process_3d_data + ) data_type = CHART_DATA_TYPE.get(chart_type, 'paired') @@ -199,7 +289,7 @@ def _preprocess_data(df, chart_type, timestamp, **kwargs): y_col = kwargs.get('y_col') z_col = kwargs.get('z_col') if all(c in df.columns for c in [x_col, y_col, z_col]): - df_proc = df[[x_col, y_col, z_col]].dropna() + df_proc = df.dropna(subset=[x_col, y_col, z_col]) else: df_proc = df return df_proc @@ -209,7 +299,8 @@ def _preprocess_data(df, chart_type, timestamp, **kwargs): y_col = kwargs.get('y_col') z_col = kwargs.get('z_col') if all(c in df.columns for c in [x_col, y_col, z_col]): - df_proc = df[[x_col, y_col, z_col]].dropna() + # dropna(subset=) 而非 df[[...]]:当 y_col 与 z_col 同名时避免重名列 + df_proc = df.dropna(subset=[x_col, y_col, z_col]) else: df_proc = df return df_proc @@ -219,15 +310,46 @@ def _preprocess_data(df, chart_type, timestamp, **kwargs): y_col = kwargs.get('y_col') z_col = kwargs.get('z_col') cols = [c for c in [x_col, y_col, z_col] if c and c in df.columns] - if cols: - df_proc = df[cols].dropna() - else: - df_proc = df - return df_proc + # 用 dropna(subset=...) 而非 df[cols]:当 x_col 与 z_col 同名时 + # df[cols] 会产生重名列、返回 DataFrame,下游 df[x_col] 取到多列 + # 报 shape mismatch。subset 只按列名去空值,保留原结构。 + return df.dropna(subset=cols) if cols else df return df +def _apply_typography(figsize=None, fontsize=None): + """按期刊样式基准同步缩放字号,避免“大图小字”失衡。 + + scienceplots 的 nature/science 样式是“figsize + font.size 配套”设计的 + (如 nature: 3.3in 宽 + 7pt 字)。若单独放大 figsize 而不动字号,字会 + 相对图极小、留白失衡——本函数负责让两者联动。 + + - 都为 None:完全沿用样式自带值(推荐,符合期刊比例)。 + - figsize= 自定义:按 新宽/基准宽 比例同步放大 font.size 等字号。 + - fontsize= 自定义:作为主控钮,覆盖 font.size 并令各元素字号跟随。 + """ + base_w = plt.rcParams['figure.figsize'][0] + base_font = plt.rcParams['font.size'] + + if figsize is not None: + plt.rcParams['figure.figsize'] = figsize + scale = figsize[0] / base_w if base_w else 1.0 + new_font = base_font * scale + else: + new_font = base_font + + if fontsize is not None: + new_font = fontsize + + if figsize is not None or fontsize is not None: + plt.rcParams['font.size'] = new_font + for k in ('axes.labelsize', 'xtick.labelsize', 'ytick.labelsize', + 'legend.fontsize'): + plt.rcParams[k] = new_font + plt.rcParams['axes.titlesize'] = new_font + 1 + + def plot_chart(df, chart_type, **kwargs): """ 统一绘图接口。 @@ -237,14 +359,18 @@ def plot_chart(df, chart_type, **kwargs): df : pd.DataFrame 原始数据(会自动进行标准化预处理) chart_type : str - 图表类型名(如 'bar', 'violin', 'surface3d', 'xy_line') + 图表类型名(如 'bar', 'violin', 'line', 'scatter', 'surface3d') **kwargs : x_col, y_col, z_col, u_col, v_col, w_col, c_col : 列映射 group_col : str, xy_series 类型的分组列 - style : str, 样式名 (默认 'science') + style : str, 样式名 (默认 'nature') title : str, 图表标题 - figsize : tuple, 图片尺寸 (默认 (5, 4)) + figsize : tuple or None, 图片尺寸;None=用样式自带基准(推荐,字号配套) + fontsize : float or None, 基准字号(pt);None=用样式自带;给定则作为主控钮 dpi : int, 输出 DPI (默认 300) + constrained_layout : bool, 是否启用约束布局 (默认 True;多子图/colorbar/legend 不打架) + legend_loc : str or None, 图例位置 (如 'best','upper left';None=默认) + legend_frame : bool, 是否显示图例边框 (默认 False) 返回 ---- @@ -252,36 +378,52 @@ def plot_chart(df, chart_type, **kwargs): """ style = kwargs.pop('style', 'nature') title = kwargs.pop('title', None) - figsize = kwargs.pop('figsize', (5, 4)) + figsize = kwargs.pop('figsize', None) + fontsize = kwargs.pop('fontsize', None) dpi = kwargs.pop('dpi', 300) + legend_loc = kwargs.pop('legend_loc', None) + legend_frame = kwargs.pop('legend_frame', False) + use_constrained = kwargs.pop('constrained_layout', True) # 1. 数据预处理(标准化),每次调用生成独立时间戳 _timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') df_proc = _preprocess_data(df, chart_type, _timestamp, **kwargs) - # 2. 设置样式 + 中文字体 + # 2. 设置样式 + 中文字体 + 字号(与 figsize 联动) plt.style.use([style, 'no-latex']) plt.rcParams['figure.dpi'] = dpi _setup_cjk_font() + _apply_typography(figsize=figsize, fontsize=fontsize) - # 3. 创建 Figure (3D 图表需要特殊处理) - if chart_type in CHARTS_3D: + # 3. 创建 Figure(3D 对 constrained_layout 支持有限,走 tight_layout 回退) + is_3d = chart_type in CHARTS_3D + if is_3d: fig, ax = plt.subplots(figsize=figsize, subplot_kw={'projection': '3d'}) else: - fig, ax = plt.subplots(figsize=figsize) + layout = 'constrained' if use_constrained else None + fig, ax = plt.subplots(figsize=figsize, layout=layout) # 4. 导入并调用图表模块 - module = _import_chart_module(chart_type) if title: kwargs['title'] = title + module = _import_chart_module(chart_type) module.plot(fig, ax, df_proc, **kwargs) - # 5. 保存 - plt.tight_layout() + # 5. 统一图例:默认无边框、位置可配(模块内 ax.legend() 的标签会保留) + handles, labels = ax.get_legend_handles_labels() + if labels: + ax.legend(handles, labels, loc=legend_loc, frameon=legend_frame) + + # 6. 保存 + if not is_3d and use_constrained: + save_kw = dict(dpi=dpi) # constrained_layout 已处理布局,不再 tight + else: + plt.tight_layout() + save_kw = dict(dpi=dpi, bbox_inches='tight', pad_inches=0.1) os.makedirs(FIGS_DIR, exist_ok=True) fig_filename = f'fig_{chart_type}_{_timestamp}.png' fig_path = os.path.join(FIGS_DIR, fig_filename) - fig.savefig(fig_path, dpi=dpi, bbox_inches='tight', pad_inches=0.1) + fig.savefig(fig_path, **save_kw) plt.close(fig) return fig_path From 279cee7fe9c9901de66e87626a347d5a8cd13675 Mon Sep 17 00:00:00 2001 From: chendengji Date: Tue, 1 Sep 2026 21:23:09 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E4=B8=8E=E7=BE=8E=E5=8C=96=E8=A7=84=E5=AE=9A:=20=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E7=BC=A9=E6=94=BE/=E9=85=8D=E8=89=B2/despine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 代码层: - _apply_typography 扩展为统一缩放: 字号/数据线宽/轴宽/刻度按同比例同步放大 (figsize 或 fontsize 给定时), 避免大图小字细线失衡 - 新增 DEFAULT_CMAP='viridis' (感知均匀+色盲友好), 场数据图全部改读 cmap kwarg (contourf/imshow/pcolormesh/hexbin/hist2d/scatter3d/surface3d/trisurf3d/tricontourf/tripcolor) - 新增 despine: 2D 折线/柱/散点/统计类默认去顶右轴线(SCI 干净风格), 场数据图/3D 默认保留四框; despine= kwarg 可覆盖 - 新增 linewidth/cmap/despine kwargs; line.py 线宽改跟随样式+kwarg - contour/tricontour 等高线标号去掉写死 fontsize=8, 改跟随 font.size 缩放 文档层: - SKILL.md 「字号与间距约定」扩为「样式与美化规定」9 小节: 样式选择/统一缩放/配色/线宽刻度/图例/布局轴线/SCI 美化原则/kwargs 总表/LLM 自主性 - README 样式表补统一缩放/cmap/despine 说明, 指向 SKILL.md 回归: 全 37 图表 × 4 样式 + cmap/despine/统一缩放专项通过 Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- SKILL.md | 85 ++++++++++++++++++++------ src/charts/arrays/contour.py | 2 +- src/charts/arrays/contourf.py | 2 +- src/charts/arrays/imshow.py | 2 +- src/charts/arrays/pcolormesh.py | 2 +- src/charts/basic/line.py | 5 +- src/charts/plot3d/scatter3d.py | 2 +- src/charts/plot3d/surface3d.py | 2 +- src/charts/plot3d/trisurf3d.py | 2 +- src/charts/stats/hexbin.py | 2 +- src/charts/stats/hist2d.py | 2 +- src/charts/unstructured/tricontour.py | 2 +- src/charts/unstructured/tricontourf.py | 2 +- src/charts/unstructured/tripcolor.py | 2 +- src/sci_plot_api.py | 58 ++++++++++++++---- 16 files changed, 131 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 27e5a84..c9b3420 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ print(path) | `ieee` | IEEE Transactions | (3.5, 2.6) | 8.0 | | `grid` | 带网格科学风 | (3.3, 2.5) | 7.0 | -> figsize 与 font.size 配套设计;`plot_chart` 默认不覆盖,传 `figsize=` 时字号按比例联动,传 `fontsize=` 则作为主控钮。完整列表见 `src/sci_plot_api.py` 的 `AVAILABLE_STYLES`。 +> figsize / font.size / 线宽 / 刻度配套设计;`plot_chart` 默认不覆盖,传 `figsize=` 或 `fontsize=` 时四者按同比例统一缩放。场数据图默认 `cmap='viridis'`(可 `cmap=` 覆盖);2D 折线/柱类默认去顶右轴线(`despine`)。完整美化规定见 `SKILL.md` 的「样式与美化规定」。完整样式列表见 `src/sci_plot_api.py` 的 `AVAILABLE_STYLES`。 ## 项目结构 diff --git a/SKILL.md b/SKILL.md index 7d028b2..32a47dd 100644 --- a/SKILL.md +++ b/SKILL.md @@ -24,9 +24,13 @@ cd /home/chen/claude/.claude/skills/sci-plot && python -m src.sci_plot_api -i da > 旧写法 `from src.sci_plot_api import ...` 若不先把技能根目录加入 `sys.path`,会报 `ModuleNotFoundError: No module named 'config'`。务必先 `sys.path.insert`。 -## 字号与间距约定(重要) +## 样式与美化规定 -scienceplots 的期刊样式是「**figsize + font.size 配套**」设计的,不可单独放大其一: +技能的绘图美化分两层管:**集中默认**(在 `plot_chart` 里统一缩放/配色/布局/轴线)+ **图表模块就近处理**(结构性参数)。下列规定对齐 SCI 期刊图面习惯,默认值即期刊合理态;同时暴露 kwarg,让 LLM 按具体图情自主微调。 + +### 1. 样式选择 + +scienceplots 的 4 个样式,每个自带一套配套 rcParams(figsize / font.size / 线宽 / 刻度),恒定加 `'no-latex'`(不依赖本地 LaTeX): | 样式 | 自带 figsize | 自带 font.size | 适用 | |------|-------------|---------------|------| @@ -35,24 +39,71 @@ scienceplots 的期刊样式是「**figsize + font.size 配套**」设计的, | `ieee` | (3.5, 2.6) | 8.0 | IEEE | | `grid` | (3.3, 2.5) | 7.0 | 带网格 | -**默认行为**:`plot_chart` 不再覆盖样式自带 figsize,字号也保持样式基准——图按原尺寸放进论文版面时比例正好。 +> 默认 `style='nature'`。图按原尺寸放进论文单栏(~3.3–3.5in)时字号/线宽比例正合适。 -**调整字号/尺寸的 kwarg**: +### 2. 统一缩放(figsize ↔ 字号 ↔ 线宽 ↔ 刻度) -```python -plot_chart(df, 'line', x_col='x', y_col='y', - style='nature', - figsize=(6, 4), # 自定义尺寸:字号会按 新宽/3.3 比例自动放大,保持比例 - fontsize=10, # 或直接指定基准字号(pt),作为主控钮覆盖一切 - constrained_layout=True, # 约束布局:多子图/colorbar/legend 不打架(2D 默认开) - legend_loc='upper left', # 图例位置 - legend_frame=False) # 图例边框,默认关 -``` +`_apply_typography(figsize, fontsize)` 保证「字号、数据线宽、轴宽、刻度尺寸」按同一比例同步缩放,避免大图小字细线。规则: + +- 都不传 → 完全沿用样式自带值(推荐)。 +- 传 `figsize=(w,h)` → 按比例同步放大字号/线宽/刻度。 +- 传 `fontsize=N` → 按比例同步放大线宽/刻度。 +- 3.3in→6in 时 font 7→12.7pt、lines.linewidth 1.0→1.82、axes.linewidth 0.5→0.91(实测)。 + +### 3. 配色 + +- **场数据图**(contour/imshow/pcolormesh/hexbin/hist2d/surface3d/trisurf3d/scatter3d/…)默认 `cmap='viridis'`(感知均匀 + 色盲友好,SCI 通用),用 `cmap=` kwarg 覆盖。备选:`plasma`(暖向)、`cividis`(纯色盲友好)、`coolwarm`(双向发散数据)、`gray`(印刷黑白)。 +- **分类数据**(line/bar/violin/scatter 分组)颜色跟随样式色环(`ax._get_lines.get_next_color()`),不硬编码;色环为 scienceplots 默认(蓝/橙/绿/红/紫/棕),≤6 组区分清晰。 +- 需自定义配色时,LLM 可在调 `plot_chart` 前设 `plt.rcParams['axes.prop_cycle']`,或对场数据传 `cmap=`。 + +### 4. 线宽与刻度 + +- 数据线宽默认跟随样式(随 figsize 统一缩放);`line`/`scatter`/`fill_between` 等接受 `linewidth=` kwarg 覆盖。 +- 等高线标号字号(`clabel`)跟随 `font.size` 缩放(不再写死 8pt)。 +- 轴线/刻度宽度由样式 + `_apply_typography` 统一缩放,不单独暴露。 + +### 5. 图例 + +- 默认**无边框**(`legend_frame=False`)、位置可配(`legend_loc=`,如 `'best'`/`'upper left'`)。 +- 模块内 `ax.legend()` 的标签会被保留,`plot_chart` 统一重建以套用边框/位置。 +- 无分组标签的图(如单线 scatter、bar)自动不加图例。 + +### 6. 布局与轴线 + +- 2D 默认 `constrained_layout=True`(多子图 / colorbar / legend 不打架,不裁切);3D 因支持有限自动回退 `tight_layout` + `bbox_inches='tight'`。 +- **去轴线**:2D 折线/柱/散点/统计类默认去顶/右两条 spine(`despine`,干净风格,SCI 习惯);场数据图(contour/imshow/…)与 3D 默认保留四框(数据填满画布)。`despine=` kwarg 可强制 True/False。 +- 中文:自动注册 WSL2 挂载的 SimHei/SimSun,`axes.unicode_minus=False`。 + +### 7. SCI 美化原则(LLM 判断依据) + +下列为期刊图面共识,**默认已套**,LLM 按图情判断是否偏离: + +- **最小 chartjunk**:默认无网格(除 `grid` 样式)、无图例边框、去多余 spine;除非数据确实需要参考线。 +- **不要为 2D 数据用 3D**:柱状/饼图别加 3D 投影,降读图精度。 +- **误差棒带 cap**:`errorbar`/`bar` 默认 `capsize=5`。 +- **标签含单位**:`set_xlabel`/`set_ylabel` 用列名时,LLM 应在标题/标签补单位(如 `Axial strain (%)`、`Stress (MPa)`)。 +- **单栏宽度优先**:多图拼版时优先单栏宽(~3.3in)+ 多子图,而非一张超大图。 +- **分组≤6**:分类色环超 6 组区分度下降,多于此改用 `tab10`/`Set2` 或换图表类型。 +- **不过度修饰**:不加阴影/渐变/3D 斜面/背景色;线宽/字号按比例即可,别逐元素手调。 + +### 8. 美化 kwargs 总表 + +| kwarg | 默认 | 作用 | +|-------|------|------| +| `style` | `'nature'` | 期刊样式 | +| `figsize` | `None`(用样式) | 图片尺寸;字号/线宽/刻度同比例缩放 | +| `fontsize` | `None`(用样式) | 基准字号主控钮 | +| `linewidth` | `None`(用样式) | 数据线宽,line/scatter/fill_between 生效 | +| `cmap` | `'viridis'` | 场数据图 colormap | +| `despine` | 自动 | 去顶/右轴线(2D 折线类 True,场/3D False) | +| `constrained_layout` | `True`(2D) | 约束布局 | +| `legend_loc` | `None`(默认) | 图例位置 | +| `legend_frame` | `False` | 图例边框 | +| `dpi` | `300` | 输出 DPI | + +### 9. LLM 自主性 -- **只放大图不改字号 → 字会偏小**:已被 `_apply_typography` 修复,传 `figsize=` 时字号按比例同步放大。 -- **多子图/带 colorbar 的图**:保持 `constrained_layout=True`(默认),不要退回 `tight_layout`,否则易裁切/重叠。 -- **3D 图**:`constrained_layout` 对 3D 支持有限,自动回退 `tight_layout`。 -- **中文**:自动注册 WSL2 挂载的 SimHei/SimSun,无需额外配置;`axes.unicode_minus` 已置 False。 +以上是**默认 + 原则**,不是逐元素硬规。LLM 应:① 默认值通常够用,先按默认出图;② 仅在数据/版式确有需要时改对应 kwarg(如双向数据换 `coolwarm`、多子图加大 `figsize`、标签补单位);③ 避免无意义堆砌修饰。给具体图情留判断空间,不必每张图都把 kwarg 调满。 ## 工作流程 diff --git a/src/charts/arrays/contour.py b/src/charts/arrays/contour.py index 20087eb..5e6d4ca 100644 --- a/src/charts/arrays/contour.py +++ b/src/charts/arrays/contour.py @@ -19,7 +19,7 @@ def plot(fig, ax, df, **kwargs): Z[y_to_idx[row[y_col]], x_to_idx[row[x_col]]] = row[z_col] cntr = ax.contour(X, Y, Z, levels=10, colors='black') - ax.clabel(cntr, inline=True, fontsize=8) + ax.clabel(cntr, inline=True) ax.set_title(kwargs.get('title', 'Contour Plot')) ax.set_xlabel(x_col) ax.set_ylabel(y_col) diff --git a/src/charts/arrays/contourf.py b/src/charts/arrays/contourf.py index 287ef69..5460107 100644 --- a/src/charts/arrays/contourf.py +++ b/src/charts/arrays/contourf.py @@ -18,7 +18,7 @@ def plot(fig, ax, df, **kwargs): for _, row in df.iterrows(): Z[y_to_idx[row[y_col]], x_to_idx[row[x_col]]] = row[z_col] - cf = ax.contourf(X, Y, Z, levels=10, cmap='viridis') + cf = ax.contourf(X, Y, Z, levels=10, cmap=kwargs.get('cmap', 'viridis')) fig.colorbar(cf, ax=ax) ax.set_title(kwargs.get('title', 'Filled Contour Plot')) ax.set_xlabel(x_col) diff --git a/src/charts/arrays/imshow.py b/src/charts/arrays/imshow.py index 6b01b3a..cf4707c 100644 --- a/src/charts/arrays/imshow.py +++ b/src/charts/arrays/imshow.py @@ -18,7 +18,7 @@ def plot(fig, ax, df, **kwargs): for _, row in df.iterrows(): Z[y_to_idx[row[y_col]], x_to_idx[row[x_col]]] = row[z_col] - im = ax.imshow(Z, cmap='viridis', aspect='auto', + im = ax.imshow(Z, cmap=kwargs.get('cmap', 'viridis'), aspect='auto', extent=[x_unique[0], x_unique[-1], y_unique[0], y_unique[-1]]) fig.colorbar(im, ax=ax) ax.set_title(kwargs.get('title', 'Imshow')) diff --git a/src/charts/arrays/pcolormesh.py b/src/charts/arrays/pcolormesh.py index 50ec28b..afbea58 100644 --- a/src/charts/arrays/pcolormesh.py +++ b/src/charts/arrays/pcolormesh.py @@ -18,7 +18,7 @@ def plot(fig, ax, df, **kwargs): for _, row in df.iterrows(): Z[y_to_idx[row[y_col]], x_to_idx[row[x_col]]] = row[z_col] - m = ax.pcolormesh(X, Y, Z, shading='auto', cmap='viridis') + m = ax.pcolormesh(X, Y, Z, shading='auto', cmap=kwargs.get('cmap', 'viridis')) fig.colorbar(m, ax=ax) ax.set_title(kwargs.get('title', 'Pcolormesh')) ax.set_xlabel(x_col) diff --git a/src/charts/basic/line.py b/src/charts/basic/line.py index 6c5e77f..9ec9c51 100644 --- a/src/charts/basic/line.py +++ b/src/charts/basic/line.py @@ -10,15 +10,16 @@ def plot(fig, ax, df, **kwargs): x_col = kwargs.get('x_col') y_col = kwargs.get('y_col') group_col = kwargs.get('group_col', None) + lw = kwargs.get('linewidth') # None → 跟随 rcParams(随 figsize 统一缩放) if group_col and group_col in df.columns: groups = sorted(df[group_col].unique()) for g in groups: subset = df[df[group_col] == g].sort_values(x_col) - ax.plot(subset[x_col], subset[y_col], label=g, linewidth=1.2) + ax.plot(subset[x_col], subset[y_col], label=g, linewidth=lw) ax.legend() else: - ax.plot(df[x_col], df[y_col], linewidth=1.2) + ax.plot(df[x_col], df[y_col], linewidth=lw) ax.set_title(kwargs.get('title', 'Line Plot')) ax.set_xlabel(x_col) diff --git a/src/charts/plot3d/scatter3d.py b/src/charts/plot3d/scatter3d.py index df7663d..2a0f0a8 100644 --- a/src/charts/plot3d/scatter3d.py +++ b/src/charts/plot3d/scatter3d.py @@ -10,7 +10,7 @@ def plot(fig, ax, df, **kwargs): sc = ax.scatter(df[x_col], df[y_col], df[z_col], c=df[c_col] if c_col else None, - cmap='viridis', s=30, alpha=0.7) + cmap=kwargs.get('cmap', 'viridis'), s=30, alpha=0.7) if c_col: fig.colorbar(sc, ax=ax, shrink=0.5) ax.set_title(kwargs.get('title', '3D Scatter')) diff --git a/src/charts/plot3d/surface3d.py b/src/charts/plot3d/surface3d.py index 0e532c0..7220cdd 100644 --- a/src/charts/plot3d/surface3d.py +++ b/src/charts/plot3d/surface3d.py @@ -18,7 +18,7 @@ def plot(fig, ax, df, **kwargs): for _, row in df.iterrows(): Z[y_to_idx[row[y_col]], x_to_idx[row[x_col]]] = row[z_col] - surf = ax.plot_surface(X, Y, Z, cmap='viridis', + surf = ax.plot_surface(X, Y, Z, cmap=kwargs.get('cmap', 'viridis'), linewidth=0, antialiased=True, alpha=0.8) fig.colorbar(surf, ax=ax, shrink=0.5) ax.set_title(kwargs.get('title', '3D Surface')) diff --git a/src/charts/plot3d/trisurf3d.py b/src/charts/plot3d/trisurf3d.py index 9eade26..76ee537 100644 --- a/src/charts/plot3d/trisurf3d.py +++ b/src/charts/plot3d/trisurf3d.py @@ -8,7 +8,7 @@ def plot(fig, ax, df, **kwargs): z_col = kwargs.get('z_col') surf = ax.plot_trisurf(df[x_col], df[y_col], df[z_col], - cmap='viridis', linewidth=0, alpha=0.8) + cmap=kwargs.get('cmap', 'viridis'), linewidth=0, alpha=0.8) fig.colorbar(surf, ax=ax, shrink=0.5) ax.set_title(kwargs.get('title', '3D Triangulated Surface')) ax.set_xlabel(x_col) diff --git a/src/charts/stats/hexbin.py b/src/charts/stats/hexbin.py index bdd448a..e466357 100644 --- a/src/charts/stats/hexbin.py +++ b/src/charts/stats/hexbin.py @@ -10,7 +10,7 @@ def plot(fig, ax, df, **kwargs): cols = df.select_dtypes(include='number').columns[:2] x_col, y_col = cols[0], cols[1] - hb = ax.hexbin(df[x_col], df[y_col], gridsize=20, cmap='viridis') + hb = ax.hexbin(df[x_col], df[y_col], gridsize=20, cmap=kwargs.get('cmap', 'viridis')) fig.colorbar(hb, ax=ax) ax.set_title(kwargs.get('title', 'Hexbin Plot')) ax.set_xlabel(x_col) diff --git a/src/charts/stats/hist2d.py b/src/charts/stats/hist2d.py index f00ecd9..7eaff4d 100644 --- a/src/charts/stats/hist2d.py +++ b/src/charts/stats/hist2d.py @@ -10,7 +10,7 @@ def plot(fig, ax, df, **kwargs): cols = df.select_dtypes(include='number').columns[:2] x_col, y_col = cols[0], cols[1] - h = ax.hist2d(df[x_col], df[y_col], bins=20, cmap='viridis') + h = ax.hist2d(df[x_col], df[y_col], bins=20, cmap=kwargs.get('cmap', 'viridis')) fig.colorbar(h[3], ax=ax) ax.set_title(kwargs.get('title', '2D Histogram')) ax.set_xlabel(x_col) diff --git a/src/charts/unstructured/tricontour.py b/src/charts/unstructured/tricontour.py index 59cd84e..6e65fb3 100644 --- a/src/charts/unstructured/tricontour.py +++ b/src/charts/unstructured/tricontour.py @@ -9,7 +9,7 @@ def plot(fig, ax, df, **kwargs): cntr = ax.tricontour(df[x_col], df[y_col], df[z_col], levels=10, colors='black') - ax.clabel(cntr, inline=True, fontsize=8) + ax.clabel(cntr, inline=True) ax.set_title(kwargs.get('title', 'Triangular Contour')) ax.set_xlabel(x_col) ax.set_ylabel(y_col) diff --git a/src/charts/unstructured/tricontourf.py b/src/charts/unstructured/tricontourf.py index 416c84e..ee94835 100644 --- a/src/charts/unstructured/tricontourf.py +++ b/src/charts/unstructured/tricontourf.py @@ -8,7 +8,7 @@ def plot(fig, ax, df, **kwargs): z_col = kwargs.get('z_col') cf = ax.tricontourf(df[x_col], df[y_col], df[z_col], - levels=10, cmap='viridis') + levels=10, cmap=kwargs.get('cmap', 'viridis')) fig.colorbar(cf, ax=ax) ax.set_title(kwargs.get('title', 'Filled Triangular Contour')) ax.set_xlabel(x_col) diff --git a/src/charts/unstructured/tripcolor.py b/src/charts/unstructured/tripcolor.py index d377401..defb613 100644 --- a/src/charts/unstructured/tripcolor.py +++ b/src/charts/unstructured/tripcolor.py @@ -8,7 +8,7 @@ def plot(fig, ax, df, **kwargs): z_col = kwargs.get('z_col') tc = ax.tripcolor(df[x_col], df[y_col], df[z_col], - cmap='viridis', shading='gouraud') + cmap=kwargs.get('cmap', 'viridis'), shading='gouraud') fig.colorbar(tc, ax=ax) ax.set_title(kwargs.get('title', 'Tripcolor')) ax.set_xlabel(x_col) diff --git a/src/sci_plot_api.py b/src/sci_plot_api.py index b2b0259..ddef4ce 100644 --- a/src/sci_plot_api.py +++ b/src/sci_plot_api.py @@ -218,6 +218,14 @@ def _setup_cjk_font(): # 可用样式(scienceplots 实测可用;'ieeetran' 已移除,scienceplots 无此样式会抛 OSError) AVAILABLE_STYLES = ['science', 'nature', 'ieee', 'grid'] +# 默认 colormap:感知均匀 + 色盲友好(SCI 期刊场数据图通用) +DEFAULT_CMAP = 'viridis' + +# 这些图表默认保留四边框(数据填满画布,去 spine 会缺角),其余 2D 图默认去顶/右 spine +DESPINE_OFF = {'contour', 'contourf', 'pcolormesh', 'imshow', + 'quiver', 'streamplot', 'barbs', + 'tricontour', 'tricontourf', 'tripcolor'} + # ==================== 核心 API ==================== @@ -319,15 +327,15 @@ def _preprocess_data(df, chart_type, timestamp, **kwargs): def _apply_typography(figsize=None, fontsize=None): - """按期刊样式基准同步缩放字号,避免“大图小字”失衡。 + """按期刊样式基准统一缩放字号 / 线宽 / 刻度,保持视觉比例。 - scienceplots 的 nature/science 样式是“figsize + font.size 配套”设计的 - (如 nature: 3.3in 宽 + 7pt 字)。若单独放大 figsize 而不动字号,字会 - 相对图极小、留白失衡——本函数负责让两者联动。 + scienceplots 样式是「figsize + font.size + 线宽」配套设计的(如 nature: + 3.3in + 7pt + linewidth 1.0)。单独放大 figsize 而不动字号/线宽,会「大图小字 + 细线」失衡——本函数让字号、线宽、轴宽、刻度按同一比例同步缩放。 - 都为 None:完全沿用样式自带值(推荐,符合期刊比例)。 - - figsize= 自定义:按 新宽/基准宽 比例同步放大 font.size 等字号。 - - fontsize= 自定义:作为主控钮,覆盖 font.size 并令各元素字号跟随。 + - figsize= 自定义:按 新宽/基准宽 比例同步放大字号/线宽/刻度。 + - fontsize= 自定义:按 新字号/基准字号 反推比例,同步放大线宽/刻度。 """ base_w = plt.rcParams['figure.figsize'][0] base_font = plt.rcParams['font.size'] @@ -337,17 +345,27 @@ def _apply_typography(figsize=None, fontsize=None): scale = figsize[0] / base_w if base_w else 1.0 new_font = base_font * scale else: + scale = 1.0 new_font = base_font if fontsize is not None: + scale = fontsize / base_font if base_font else 1.0 new_font = fontsize if figsize is not None or fontsize is not None: + # —— 字号族 —— plt.rcParams['font.size'] = new_font for k in ('axes.labelsize', 'xtick.labelsize', 'ytick.labelsize', 'legend.fontsize'): plt.rcParams[k] = new_font plt.rcParams['axes.titlesize'] = new_font + 1 + # —— 线宽/刻度族:同比例缩放,与字号保持平衡 —— + for k in ('lines.linewidth', 'lines.markersize', 'axes.linewidth', + 'xtick.major.width', 'ytick.major.width', + 'xtick.major.size', 'ytick.major.size', + 'xtick.minor.width', 'ytick.minor.width', + 'grid.linewidth'): + plt.rcParams[k] = plt.rcParams[k] * scale def plot_chart(df, chart_type, **kwargs): @@ -365,8 +383,11 @@ def plot_chart(df, chart_type, **kwargs): group_col : str, xy_series 类型的分组列 style : str, 样式名 (默认 'nature') title : str, 图表标题 - figsize : tuple or None, 图片尺寸;None=用样式自带基准(推荐,字号配套) - fontsize : float or None, 基准字号(pt);None=用样式自带;给定则作为主控钮 + figsize : tuple or None, 图片尺寸;None=用样式自带基准(推荐,字号/线宽配套) + fontsize : float or None, 基准字号(pt);None=用样式自带;给定则作主控钮(线宽/刻度同比例缩放) + linewidth : float or None, 数据线宽;None=跟随样式(随 figsize 缩放);line/scatter/fill_between 等生效 + cmap : str, 场数据图 colormap (默认 'viridis',感知均匀+色盲友好) + despine : bool or None, 是否去顶/右轴线;None=自动(2D 折线/柱类 True,场数据/3D False) dpi : int, 输出 DPI (默认 300) constrained_layout : bool, 是否启用约束布局 (默认 True;多子图/colorbar/legend 不打架) legend_loc : str or None, 图例位置 (如 'best','upper left';None=默认) @@ -380,16 +401,24 @@ def plot_chart(df, chart_type, **kwargs): title = kwargs.pop('title', None) figsize = kwargs.pop('figsize', None) fontsize = kwargs.pop('fontsize', None) + linewidth = kwargs.pop('linewidth', None) + cmap = kwargs.pop('cmap', DEFAULT_CMAP) + despine = kwargs.pop('despine', None) dpi = kwargs.pop('dpi', 300) legend_loc = kwargs.pop('legend_loc', None) legend_frame = kwargs.pop('legend_frame', False) use_constrained = kwargs.pop('constrained_layout', True) + # 把 colormap / 线宽透传给图表模块(场数据图读 cmap;line 类读 linewidth) + kwargs.setdefault('cmap', cmap) + if linewidth is not None: + kwargs['linewidth'] = linewidth + # 1. 数据预处理(标准化),每次调用生成独立时间戳 _timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') df_proc = _preprocess_data(df, chart_type, _timestamp, **kwargs) - # 2. 设置样式 + 中文字体 + 字号(与 figsize 联动) + # 2. 设置样式 + 中文字体 + 字号/线宽(与 figsize 统一缩放) plt.style.use([style, 'no-latex']) plt.rcParams['figure.dpi'] = dpi _setup_cjk_font() @@ -409,12 +438,19 @@ def plot_chart(df, chart_type, **kwargs): module = _import_chart_module(chart_type) module.plot(fig, ax, df_proc, **kwargs) - # 5. 统一图例:默认无边框、位置可配(模块内 ax.legend() 的标签会保留) + # 5. 去顶/右轴线(SCI 干净风格;场数据/3D 默认保留四框) + if despine is None: + despine = (chart_type not in DESPINE_OFF) and (not is_3d) + if despine and not is_3d: + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + + # 6. 统一图例:默认无边框、位置可配(模块内 ax.legend() 的标签会保留) handles, labels = ax.get_legend_handles_labels() if labels: ax.legend(handles, labels, loc=legend_loc, frameon=legend_frame) - # 6. 保存 + # 7. 保存 if not is_3d and use_constrained: save_kw = dict(dpi=dpi) # constrained_layout 已处理布局,不再 tight else: