diff --git a/README.md b/README.md
index e35a3b6..c9b3420 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-📊 **一键生成专业级科研图表** — 30+ 期刊风格,500+ 参考代码,开箱即用
+📊 **一键生成专业级科研图表** — 4 种期刊风格,37 种图表,500+ 参考代码,开箱即用
[](https://python.org)
[](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=` 时四者按同比例统一缩放。场数据图默认 `cmap='viridis'`(可 `cmap=` 覆盖);2D 折线/柱类默认去顶右轴线(`despine`)。完整美化规定见 `SKILL.md` 的「样式与美化规定」。完整样式列表见 `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..32a47dd 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -4,121 +4,191 @@
## 前置条件
-- 环境: `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`。
+
+## 样式与美化规定
+
+技能的绘图美化分两层管:**集中默认**(在 `plot_chart` 里统一缩放/配色/布局/轴线)+ **图表模块就近处理**(结构性参数)。下列规定对齐 SCI 期刊图面习惯,默认值即期刊合理态;同时暴露 kwarg,让 LLM 按具体图情自主微调。
+
+### 1. 样式选择
+
+scienceplots 的 4 个样式,每个自带一套配套 rcParams(figsize / font.size / 线宽 / 刻度),恒定加 `'no-latex'`(不依赖本地 LaTeX):
+
+| 样式 | 自带 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 | 带网格 |
+
+> 默认 `style='nature'`。图按原尺寸放进论文单栏(~3.3–3.5in)时字号/线宽比例正合适。
+
+### 2. 统一缩放(figsize ↔ 字号 ↔ 线宽 ↔ 刻度)
+
+`_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 自主性
+
+以上是**默认 + 原则**,不是逐元素硬规。LLM 应:① 默认值通常够用,先按默认出图;② 仅在数据/版式确有需要时改对应 kwarg(如双向数据换 `coolwarm`、多子图加大 `figsize`、标签补单位);③ 避免无意义堆砌修饰。给具体图情留判断空间,不必每张图都把 kwarg 调满。
## 工作流程
### 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': [...]}
+col_info = auto_detect_columns(df) # {'numeric': [...], 'categorical': [...], 'other': [...]}
+recs = recommend_charts(df) # [(chart_type, 说明), ...]
```
-**推荐图表:**
-```python
-from src.sci_plot_api import recommend_charts
-recs = recommend_charts(df)
-# 返回: [(chart_type, description), ...]
-```
-
-**与用户确认列映射:**
-> 检测到以下列:
-> 数值列: 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 +197,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/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/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/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/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..ddef4ce 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,33 @@ 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']
+
+# 默认 colormap:感知均匀 + 色盲友好(SCI 期刊场数据图通用)
+DEFAULT_CMAP = 'viridis'
+
+# 这些图表默认保留四边框(数据填满画布,去 spine 会缺角),其余 2D 图默认去顶/右 spine
+DESPINE_OFF = {'contour', 'contourf', 'pcolormesh', 'imshow',
+ 'quiver', 'streamplot', 'barbs',
+ 'tricontour', 'tricontourf', 'tripcolor'}
# ==================== 核心 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 +255,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 +297,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 +307,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 +318,56 @@ 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 样式是「figsize + font.size + 线宽」配套设计的(如 nature:
+ 3.3in + 7pt + linewidth 1.0)。单独放大 figsize 而不动字号/线宽,会「大图小字
+ 细线」失衡——本函数让字号、线宽、轴宽、刻度按同一比例同步缩放。
+
+ - 都为 None:完全沿用样式自带值(推荐,符合期刊比例)。
+ - figsize= 自定义:按 新宽/基准宽 比例同步放大字号/线宽/刻度。
+ - fontsize= 自定义:按 新字号/基准字号 反推比例,同步放大线宽/刻度。
+ """
+ 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:
+ 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):
"""
统一绘图接口。
@@ -237,14 +377,21 @@ 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=用样式自带;给定则作主控钮(线宽/刻度同比例缩放)
+ 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=默认)
+ legend_frame : bool, 是否显示图例边框 (默认 False)
返回
----
@@ -252,36 +399,67 @@ 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)
+ 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. 设置样式 + 中文字体
+ # 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. 去顶/右轴线(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)
+
+ # 7. 保存
+ 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