psyclaw 0.28.1 → 0.28.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/agents/recommended/catalog.json +1 -1
- package/dist/src/adapters/pi/extension.js +79 -25
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/ars/keybindings.d.ts +13 -0
- package/dist/src/ars/keybindings.js +50 -0
- package/dist/src/ars/keybindings.js.map +1 -0
- package/dist/src/ars/mode-editor.d.ts +25 -0
- package/dist/src/ars/mode-editor.js +78 -0
- package/dist/src/ars/mode-editor.js.map +1 -0
- package/dist/src/ars/profile.d.ts +4 -1
- package/dist/src/ars/profile.js +23 -6
- package/dist/src/ars/profile.js.map +1 -1
- package/dist/src/chat.js +3 -0
- package/dist/src/chat.js.map +1 -1
- package/dist/src/panel/server.js +1 -1
- package/dist/src/panel/server.js.map +1 -1
- package/package.json +10 -3
- package/vendor/academic-paper-skills/LICENSE +21 -0
- package/vendor/academic-paper-skills/NOTICE.md +7 -0
- package/vendor/academic-paper-skills/PSYCLAW_SOURCE.json +14 -0
- package/vendor/academic-paper-skills/academic-paper-composer/SKILL.md +840 -0
- package/vendor/academic-paper-skills/academic-paper-composer/references/section_guides.md +675 -0
- package/vendor/academic-paper-skills/academic-paper-composer/references/writing_standards.md +629 -0
- package/vendor/academic-paper-skills/academic-paper-composer/scripts/chapter_quality_check.py +470 -0
- package/vendor/academic-paper-skills/academic-paper-composer/scripts/final_evaluation.py +550 -0
- package/vendor/academic-paper-skills/academic-paper-strategist/SKILL.md +670 -0
- package/vendor/academic-paper-skills/academic-paper-strategist/references/quality_standards.md +336 -0
- package/vendor/academic-paper-skills/academic-paper-strategist/references/search_strategy.md +459 -0
- package/vendor/academic-paper-skills/academic-paper-strategist/scripts/evaluate_samples.py +300 -0
- package/vendor/academic-paper-skills/academic-paper-strategist/scripts/gap_analysis.py +399 -0
- package/vendor/ars/pi/wrapper.js +9 -0
- package/vendor/nature-skills/LICENSE +201 -0
- package/vendor/nature-skills/NOTICE.md +7 -0
- package/vendor/nature-skills/PSYCLAW_SOURCE.json +19 -0
- package/vendor/nature-skills/skills/nature-figure/README.md +107 -0
- package/vendor/nature-skills/skills/nature-figure/README_EN.md +107 -0
- package/vendor/nature-skills/skills/nature-figure/SKILL.md +154 -0
- package/vendor/nature-skills/skills/nature-figure/agents/openai.yaml +4 -0
- package/vendor/nature-skills/skills/nature-figure/evals/evals.json +325 -0
- package/vendor/nature-skills/skills/nature-figure/manifest.yaml +103 -0
- package/vendor/nature-skills/skills/nature-figure/references/ai-graphical-abstract-workflow.md +129 -0
- package/vendor/nature-skills/skills/nature-figure/references/api.md +576 -0
- package/vendor/nature-skills/skills/nature-figure/references/asset-adaptation.md +72 -0
- package/vendor/nature-skills/skills/nature-figure/references/backend-selection.md +118 -0
- package/vendor/nature-skills/skills/nature-figure/references/chart-types.md +348 -0
- package/vendor/nature-skills/skills/nature-figure/references/common-patterns.md +371 -0
- package/vendor/nature-skills/skills/nature-figure/references/demos.md +58 -0
- package/vendor/nature-skills/skills/nature-figure/references/design-theory.md +467 -0
- package/vendor/nature-skills/skills/nature-figure/references/figure-contract.md +122 -0
- package/vendor/nature-skills/skills/nature-figure/references/figure-legend-conventions.md +90 -0
- package/vendor/nature-skills/skills/nature-figure/references/multipanel-evidence-architecture.md +268 -0
- package/vendor/nature-skills/skills/nature-figure/references/nature-2026-observations.md +124 -0
- package/vendor/nature-skills/skills/nature-figure/references/nature-article-requirements.md +149 -0
- package/vendor/nature-skills/skills/nature-figure/references/openrouter-image-generation.md +141 -0
- package/vendor/nature-skills/skills/nature-figure/references/qa-contract.md +344 -0
- package/vendor/nature-skills/skills/nature-figure/references/r-template-index.md +66 -0
- package/vendor/nature-skills/skills/nature-figure/references/r-workflow.md +234 -0
- package/vendor/nature-skills/skills/nature-figure/references/template-catalog.md +33 -0
- package/vendor/nature-skills/skills/nature-figure/references/tutorials.md +260 -0
- package/vendor/nature-skills/skills/nature-figure/requirements.txt +1 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/audit_figure_collisions.py +742 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/audit_panel_alignment.py +933 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/audit_pdf_text.py +152 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/figure_safety.py +50 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/generate_openrouter_schematic.py +260 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/nature_figure_backend.py +93 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/panel_alignment.R +188 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/plot_templates.py +604 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/validate_figure.py +835 -0
- package/vendor/nature-skills/skills/nature-figure/static/core/contract.md +55 -0
- package/vendor/nature-skills/skills/nature-figure/static/core/stance.md +41 -0
- package/vendor/nature-skills/skills/nature-figure/static/fragments/backend/python.md +56 -0
- package/vendor/nature-skills/skills/nature-figure/static/fragments/backend/r.md +63 -0
- package/vendor/nature-skills/skills/nature-figure/tests/test_figure_safety.py +209 -0
- package/vendor/nature-skills/skills/nature-polishing/README.md +58 -0
- package/vendor/nature-skills/skills/nature-polishing/README_EN.md +58 -0
- package/vendor/nature-skills/skills/nature-polishing/SKILL.md +111 -0
- package/vendor/nature-skills/skills/nature-polishing/agents/openai.yaml +4 -0
- package/vendor/nature-skills/skills/nature-polishing/manifest.yaml +101 -0
- package/vendor/nature-skills/skills/nature-polishing/references/latex-layout.md +211 -0
- package/vendor/nature-skills/skills/nature-polishing/references/nat-comms-2025-diction.md +73 -0
- package/vendor/nature-skills/skills/nature-polishing/references/phrasebank-playbook.md +175 -0
- package/vendor/nature-skills/skills/nature-polishing/references/published-article-patterns.md +126 -0
- package/vendor/nature-skills/skills/nature-polishing/references/section-moves.md +252 -0
- package/vendor/nature-skills/skills/nature-polishing/references/style-guardrails.md +94 -0
- package/vendor/nature-skills/skills/nature-polishing/references/writing-strategy.md +160 -0
- package/vendor/nature-skills/skills/nature-polishing/static/core/failure-modes.md +29 -0
- package/vendor/nature-skills/skills/nature-polishing/static/core/output-format.md +21 -0
- package/vendor/nature-skills/skills/nature-polishing/static/core/stance.md +26 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/journal/generic.md +17 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/journal/nat-comms.md +38 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/journal/nat-mach-intell.md +102 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/journal/nature.md +19 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/language/en.md +19 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/language/zh-to-en.md +19 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/paper_type/algorithmic.md +18 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/paper_type/hypothesis.md +17 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/paper_type/methods.md +35 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/paper_type/research.md +29 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/paper_type/review.md +17 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/abstract.md +26 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/conclusion.md +15 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/discussion.md +48 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/intro.md +33 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/methods.md +26 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/results.md +61 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/title.md +17 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/README.md +44 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/README_EN.md +44 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/SKILL.md +196 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/agents/openai.yaml +4 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/manifest.yaml +19 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/references/common-patterns.md +96 -0
- package/vendor/nature-skills/skills/nature-shared/README.md +43 -0
- package/vendor/nature-skills/skills/nature-shared/README_EN.md +43 -0
- package/vendor/nature-skills/skills/nature-shared/SKILL.md +33 -0
- package/vendor/nature-skills/skills/nature-shared/agents/openai.yaml +6 -0
- package/vendor/nature-skills/skills/nature-shared/core/consistency-sweep.md +122 -0
- package/vendor/nature-skills/skills/nature-shared/core/discussion-argument-language.md +192 -0
- package/vendor/nature-skills/skills/nature-shared/core/ethics.md +88 -0
- package/vendor/nature-skills/skills/nature-shared/core/main-text-discipline.md +179 -0
- package/vendor/nature-skills/skills/nature-shared/core/nature-abstract.md +171 -0
- package/vendor/nature-skills/skills/nature-shared/core/nature-introduction.md +164 -0
- package/vendor/nature-skills/skills/nature-shared/core/nature-results-discussion.md +215 -0
- package/vendor/nature-skills/skills/nature-shared/core/paper-type-taxonomy.md +41 -0
- package/vendor/nature-skills/skills/nature-shared/core/reader-workflow.md +21 -0
- package/vendor/nature-skills/skills/nature-shared/core/research-compliance.md +192 -0
- package/vendor/nature-skills/skills/nature-shared/core/terminology-ledger.md +58 -0
- package/vendor/nature-skills/skills/nature-shared/journal-formats/nat-comms.md +139 -0
- package/vendor/nature-skills/skills/nature-shared/journal-formats/nature-machine-intelligence.md +431 -0
- package/vendor/nature-skills/skills/nature-shared/journal-formats/nature.md +313 -0
- package/vendor/nature-skills/skills/nature-shared/manifest.yaml +52 -0
- package/vendor/nature-skills/skills/nature-shared/scripts/check_consistency.py +273 -0
- package/vendor/nature-skills/skills/nature-shared/tests/test_check_consistency.py +63 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
# API Reference — Nature Figure Making
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- [Constants](#constants)
|
|
6
|
+
- [MANDATORY font + SVG rules (always first, no exceptions)](#mandatory-font-svg-rules-always-first-no-exceptions)
|
|
7
|
+
- [apply_publication_style()](#apply_publication_style)
|
|
8
|
+
- [is_dark(hex_color, threshold=128)](#is_darkhex_color-threshold128)
|
|
9
|
+
- [add_panel_label(ax, label, ...)](#add_panel_labelax-label)
|
|
10
|
+
- [style_dark_image_ax(ax, ...)](#style_dark_image_axax)
|
|
11
|
+
- [make_grouped_bar(ax, categories, series, labels, ...)](#make_grouped_barax-categories-series-labels)
|
|
12
|
+
- [make_trend(ax, x, y_series, labels, ...)](#make_trendax-x-y_series-labels)
|
|
13
|
+
- [make_forest_plot(ax, labels, estimates, ci_low, ci_high, ...)](#make_forest_plotax-labels-estimates-ci_low-ci_high)
|
|
14
|
+
- [make_heatmap(ax, matrix, ...)](#make_heatmapax-matrix)
|
|
15
|
+
- [Numerical and annotation safety helpers](#numerical-and-annotation-safety-helpers)
|
|
16
|
+
- [require_matplotlib_panel_alignment(fig, ...)](#require_matplotlib_panel_alignmentfig-)
|
|
17
|
+
- [finalize_figure(fig, out_path, ...)](#finalize_figurefig-out_path)
|
|
18
|
+
- [Validation Rules](#validation-rules)
|
|
19
|
+
- [Conventions](#conventions)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Conventions, constants, and reusable code blocks. Implement in your script or adapt as needed.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Constants
|
|
27
|
+
|
|
28
|
+
### PALETTE
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
PALETTE = {
|
|
32
|
+
"blue_main": "#0F4D92",
|
|
33
|
+
"blue_secondary": "#3775BA",
|
|
34
|
+
"green_1": "#DDF3DE",
|
|
35
|
+
"green_2": "#AADCA9",
|
|
36
|
+
"green_3": "#8BCF8B",
|
|
37
|
+
"red_1": "#F6CFCB",
|
|
38
|
+
"red_2": "#E9A6A1",
|
|
39
|
+
"red_strong": "#B64342",
|
|
40
|
+
"neutral_light": "#CFCECE",
|
|
41
|
+
"neutral_mid": "#767676",
|
|
42
|
+
"neutral_dark": "#4D4D4D",
|
|
43
|
+
"neutral_black": "#272727",
|
|
44
|
+
"gold": "#FFD700",
|
|
45
|
+
"teal": "#42949E",
|
|
46
|
+
"violet": "#9A4D8E",
|
|
47
|
+
"magenta":"#EA84DD",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
DEFAULT_COLORS = [
|
|
51
|
+
PALETTE["blue_main"],
|
|
52
|
+
PALETTE["green_3"],
|
|
53
|
+
PALETTE["red_strong"],
|
|
54
|
+
PALETTE["teal"],
|
|
55
|
+
PALETTE["violet"],
|
|
56
|
+
PALETTE["neutral_light"],
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
PALETTE_NMI_PASTEL = {
|
|
60
|
+
"baseline_dark": "#484878",
|
|
61
|
+
"baseline_mid": "#7884B4",
|
|
62
|
+
"baseline_soft": "#B4C0E4",
|
|
63
|
+
"ours_tiny": "#E4E4F0",
|
|
64
|
+
"ours_base": "#E4CCD8",
|
|
65
|
+
"ours_large": "#F0C0CC",
|
|
66
|
+
"bg_lilac": "#E0E0F0",
|
|
67
|
+
"bg_aqua": "#E0F0F0",
|
|
68
|
+
"bg_peach": "#F0E0D0",
|
|
69
|
+
"neutral_light": "#D8D8D8",
|
|
70
|
+
"neutral_mid": "#A8A8A8",
|
|
71
|
+
"neutral_dark": "#606060",
|
|
72
|
+
"delta_up": "#2E9E44",
|
|
73
|
+
"delta_down": "#E53935",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
DEFAULT_COLORS_NMI_PASTEL = [
|
|
77
|
+
PALETTE_NMI_PASTEL["baseline_dark"],
|
|
78
|
+
PALETTE_NMI_PASTEL["baseline_mid"],
|
|
79
|
+
PALETTE_NMI_PASTEL["baseline_soft"],
|
|
80
|
+
PALETTE_NMI_PASTEL["ours_tiny"],
|
|
81
|
+
PALETTE_NMI_PASTEL["ours_base"],
|
|
82
|
+
PALETTE_NMI_PASTEL["ours_large"],
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
PALETTE_NATURE_IMAGING = {
|
|
86
|
+
"bg": "#000000",
|
|
87
|
+
"context": "#B8B8B8",
|
|
88
|
+
"cyan": "#22D7E6",
|
|
89
|
+
"magenta": "#FF2AD4",
|
|
90
|
+
"white": "#FFFFFF",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
PALETTE_NATURE_MATERIAL = {
|
|
94
|
+
"aqua": "#77D7D1",
|
|
95
|
+
"teal": "#33B5A5",
|
|
96
|
+
"lilac": "#B9A7E8",
|
|
97
|
+
"violet": "#7C6CCF",
|
|
98
|
+
"callout_red": "#E53935",
|
|
99
|
+
"neutral": "#D9D9D9",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
PALETTE_NATURE_CLINICAL = {
|
|
103
|
+
"baseline": "#272727",
|
|
104
|
+
"week6": "#E28E2C",
|
|
105
|
+
"week13": "#D24B40",
|
|
106
|
+
"week26": "#5B8FD6",
|
|
107
|
+
"year1": "#7BAA5B",
|
|
108
|
+
"year2": "#C45AD6",
|
|
109
|
+
"group_band": "#F2E6D9",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
PALETTE_NATURE_GENOMICS = {
|
|
113
|
+
"neutral_light": "#D8D8D8",
|
|
114
|
+
"neutral_mid": "#8F8F8F",
|
|
115
|
+
"wave1": "#D9544D",
|
|
116
|
+
"wave2": "#5B7FCA",
|
|
117
|
+
"wave3": "#B89BD9",
|
|
118
|
+
"outline": "#4D4D4D",
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use `DEFAULT_COLORS` when color itself carries explicit semantic meaning (`hero`, `baseline`, `positive variant`).
|
|
123
|
+
Use `DEFAULT_COLORS_NMI_PASTEL` when several compared methods belong to one or two related families and the page
|
|
124
|
+
should feel visually unified.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## MANDATORY font + SVG rules (always first, no exceptions)
|
|
129
|
+
|
|
130
|
+
These three lines are **non-negotiable** and must appear at the top of every script,
|
|
131
|
+
before any figure is created. They guarantee editable text in SVG output:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
plt.rcParams['font.family'] = 'sans-serif'
|
|
135
|
+
plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans', 'Liberation Sans']
|
|
136
|
+
plt.rcParams['svg.fonttype'] = 'none' # keeps text as <text> nodes, not paths
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Why `svg.fonttype = 'none'`**: matplotlib's default (`'path'`) converts every
|
|
140
|
+
glyph to a bezier path, making text unselectable, unsearchable, and impossible to
|
|
141
|
+
re-align in Illustrator / Inkscape. With `'none'`, text stays as SVG `<text>` elements
|
|
142
|
+
and font substitution happens at render time.
|
|
143
|
+
|
|
144
|
+
**Output format**: always save as `.svg` (primary). PNG/PDF are optional secondary
|
|
145
|
+
exports. Never use `.png` alone when the figure contains text that may need adjustment.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## apply_publication_style()
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
def apply_publication_style(font_size=16, axes_linewidth=2.5, use_tex=False):
|
|
153
|
+
"""Apply Nature-style rcParams. Call once before creating any figures."""
|
|
154
|
+
# ── MANDATORY: editable SVG text ──────────────────────────────────────────
|
|
155
|
+
plt.rcParams['font.family'] = 'sans-serif'
|
|
156
|
+
plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans', 'Liberation Sans']
|
|
157
|
+
plt.rcParams['svg.fonttype'] = 'none'
|
|
158
|
+
# ── Layout & style ────────────────────────────────────────────────────────
|
|
159
|
+
plt.rcParams['font.size'] = font_size
|
|
160
|
+
plt.rcParams['axes.spines.right'] = False
|
|
161
|
+
plt.rcParams['axes.spines.top'] = False
|
|
162
|
+
plt.rcParams['axes.linewidth'] = axes_linewidth
|
|
163
|
+
plt.rcParams['legend.frameon'] = False
|
|
164
|
+
if use_tex:
|
|
165
|
+
plt.rcParams['text.usetex'] = True
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Presets:**
|
|
169
|
+
- Large bar panels: `apply_publication_style(font_size=24, axes_linewidth=3)`
|
|
170
|
+
- Compact figures: `apply_publication_style(font_size=15, axes_linewidth=2)`
|
|
171
|
+
- Dense journal-width multi-panels: `apply_publication_style(font_size=8, axes_linewidth=1)`
|
|
172
|
+
- LaTeX labels: `apply_publication_style(use_tex=True)`
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## is_dark(hex_color, threshold=128)
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
def is_dark(hex_color, threshold=128):
|
|
180
|
+
"""Return True if hex color is dark (use white text on it)."""
|
|
181
|
+
c = hex_color.lstrip('#')
|
|
182
|
+
r, g, b = int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)
|
|
183
|
+
return (0.299*r + 0.587*g + 0.114*b) < threshold
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## add_panel_label(ax, label, ...)
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
def add_panel_label(ax, label, x=0, y=1, x_offset_pt=-4, y_offset_pt=3,
|
|
192
|
+
fontsize=8, color='black', fontweight='bold', va='bottom'):
|
|
193
|
+
"""Place a panel label with a fixed physical offset from an axes anchor."""
|
|
194
|
+
from matplotlib.transforms import ScaledTranslation
|
|
195
|
+
offset = ScaledTranslation(
|
|
196
|
+
x_offset_pt / 72,
|
|
197
|
+
y_offset_pt / 72,
|
|
198
|
+
ax.figure.dpi_scale_trans,
|
|
199
|
+
)
|
|
200
|
+
ax.text(
|
|
201
|
+
x, y, label,
|
|
202
|
+
transform=ax.transAxes + offset,
|
|
203
|
+
fontsize=fontsize,
|
|
204
|
+
fontweight=fontweight,
|
|
205
|
+
color=color,
|
|
206
|
+
ha='left',
|
|
207
|
+
va=va,
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The point offset keeps labels aligned when one panel spans two rows and its
|
|
212
|
+
neighbouring axes are shorter; a shared axes-fraction offset such as `y=1.02`
|
|
213
|
+
would produce different physical displacements. For dark image plates, move
|
|
214
|
+
the label inside the panel and switch to white:
|
|
215
|
+
`add_panel_label(ax, 'a', x=0.01, y=0.98, x_offset_pt=0, y_offset_pt=0, color='white', va='top')`
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## style_dark_image_ax(ax, ...)
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
def style_dark_image_ax(ax, facecolor='black'):
|
|
223
|
+
"""Prepare an axes for microscopy / rendering plates."""
|
|
224
|
+
ax.set_facecolor(facecolor)
|
|
225
|
+
ax.set_xticks([])
|
|
226
|
+
ax.set_yticks([])
|
|
227
|
+
for spine in ax.spines.values():
|
|
228
|
+
spine.set_visible(False)
|
|
229
|
+
return ax
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## make_grouped_bar(ax, categories, series, labels, ...)
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
def make_grouped_bar(ax, categories, series, labels,
|
|
238
|
+
ylabel='Value', colors=None,
|
|
239
|
+
annotate=False, bar_width=0.8,
|
|
240
|
+
series_spread=None, error_kw=None):
|
|
241
|
+
"""
|
|
242
|
+
Grouped bar chart.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
ax : matplotlib Axes
|
|
247
|
+
categories : list[str] — x-axis category names (length K)
|
|
248
|
+
series : list[array] — one array per group (each length K)
|
|
249
|
+
labels : list[str] — legend label per group
|
|
250
|
+
ylabel : str
|
|
251
|
+
colors : list[str] | None — defaults to DEFAULT_COLORS; override with
|
|
252
|
+
DEFAULT_COLORS_NMI_PASTEL for unified-family figures
|
|
253
|
+
annotate : bool — print value above each bar
|
|
254
|
+
bar_width : float — total width for all bars in one category
|
|
255
|
+
error_kw : dict — passed to ax.bar as error_kw
|
|
256
|
+
series_spread : list[array] | None — one uncertainty array per series
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
list[BarContainer]
|
|
261
|
+
"""
|
|
262
|
+
import numpy as np
|
|
263
|
+
if colors is None:
|
|
264
|
+
colors = DEFAULT_COLORS
|
|
265
|
+
if error_kw is None:
|
|
266
|
+
error_kw = {'elinewidth': 2, 'capthick': 2, 'capsize': 10}
|
|
267
|
+
n_groups = len(series)
|
|
268
|
+
n_cats = len(categories)
|
|
269
|
+
w = bar_width / n_groups
|
|
270
|
+
x = np.arange(n_cats)
|
|
271
|
+
flat_values = np.concatenate([np.asarray(values, dtype=float) for values in series])
|
|
272
|
+
label_pad = 0.02 * max(float(np.ptp(flat_values)), float(np.max(np.abs(flat_values))), 1.0)
|
|
273
|
+
containers = []
|
|
274
|
+
for i, (vals, label, color) in enumerate(zip(series, labels, colors)):
|
|
275
|
+
spread = None if series_spread is None else np.asarray(series_spread[i], dtype=float)
|
|
276
|
+
offset = (i - (n_groups - 1) / 2) * w
|
|
277
|
+
bars = ax.bar(x + offset, vals, width=w, label=label,
|
|
278
|
+
color=color, edgecolor='black', linewidth=1.5,
|
|
279
|
+
yerr=spread, error_kw=error_kw)
|
|
280
|
+
containers.append(bars)
|
|
281
|
+
if annotate:
|
|
282
|
+
for j, (bar, val) in enumerate(zip(bars, vals)):
|
|
283
|
+
upper = bar.get_height() + (0 if spread is None else spread[j])
|
|
284
|
+
ax.text(bar.get_x() + bar.get_width() / 2,
|
|
285
|
+
upper + label_pad,
|
|
286
|
+
f'{val:.2f}', ha='center', va='bottom', fontsize=10)
|
|
287
|
+
ax.set_xticks(x)
|
|
288
|
+
ax.set_xticklabels(categories)
|
|
289
|
+
ax.set_ylabel(ylabel)
|
|
290
|
+
ax.legend()
|
|
291
|
+
return containers
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## make_trend(ax, x, y_series, labels, ...)
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
def make_trend(ax, x, y_series, labels,
|
|
300
|
+
colors=None, ylabel=None, xlabel=None,
|
|
301
|
+
show_shadow=True, shadow_alpha=0.15,
|
|
302
|
+
lw=2.5, marker='o', markersize=8):
|
|
303
|
+
"""
|
|
304
|
+
Multi-line trend plot.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
x : array-like — shared x values
|
|
309
|
+
y_series : list[array] — one 1D array per line
|
|
310
|
+
labels : list[str]
|
|
311
|
+
show_shadow : bool — fill_between ± std if y_series contains 2D arrays (rows=runs)
|
|
312
|
+
"""
|
|
313
|
+
import numpy as np
|
|
314
|
+
if colors is None:
|
|
315
|
+
colors = DEFAULT_COLORS
|
|
316
|
+
for y, label, color in zip(y_series, labels, colors):
|
|
317
|
+
y = np.asarray(y)
|
|
318
|
+
if y.ndim == 2:
|
|
319
|
+
mean, std = y.mean(0), y.std(0)
|
|
320
|
+
else:
|
|
321
|
+
mean, std = y, None
|
|
322
|
+
ax.plot(x, mean, color=color, lw=lw, marker=marker,
|
|
323
|
+
markersize=markersize, label=label)
|
|
324
|
+
if show_shadow and std is not None:
|
|
325
|
+
ax.fill_between(x, mean - std, mean + std,
|
|
326
|
+
color=color, alpha=shadow_alpha)
|
|
327
|
+
if ylabel:
|
|
328
|
+
ax.set_ylabel(ylabel)
|
|
329
|
+
if xlabel:
|
|
330
|
+
ax.set_xlabel(xlabel)
|
|
331
|
+
ax.legend()
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## make_forest_plot(ax, labels, estimates, ci_low, ci_high, ...)
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
def make_forest_plot(ax, labels, estimates, ci_low, ci_high,
|
|
340
|
+
colors=None, ref=0.0, xlabel=None, xlim=None,
|
|
341
|
+
marker='o', markersize=5, lw=1.5):
|
|
342
|
+
"""
|
|
343
|
+
Minimal forest plot helper for Nature-style clinical/statistical panels.
|
|
344
|
+
"""
|
|
345
|
+
import numpy as np
|
|
346
|
+
y = np.arange(len(labels))[::-1]
|
|
347
|
+
if colors is None:
|
|
348
|
+
colors = ['#B64342'] * len(labels)
|
|
349
|
+
for yi, est, lo, hi, color in zip(y, estimates, ci_low, ci_high, colors):
|
|
350
|
+
ax.plot([lo, hi], [yi, yi], color=color, lw=lw)
|
|
351
|
+
ax.plot(est, yi, marker=marker, ms=markersize, color=color)
|
|
352
|
+
ax.axvline(ref, color='#767676', linestyle='--', linewidth=1.2, alpha=0.8)
|
|
353
|
+
ax.set_yticks(y)
|
|
354
|
+
ax.set_yticklabels(labels)
|
|
355
|
+
if xlabel:
|
|
356
|
+
ax.set_xlabel(xlabel)
|
|
357
|
+
if xlim is not None:
|
|
358
|
+
ax.set_xlim(xlim)
|
|
359
|
+
ax.spines['right'].set_visible(False)
|
|
360
|
+
ax.spines['top'].set_visible(False)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Use pale `ax.axhspan(...)` bands behind contiguous label groups when you need the
|
|
364
|
+
clinical-triptych look from `Nature`.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## make_heatmap(ax, matrix, ...)
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
def make_heatmap(ax, matrix, x_labels=None, y_labels=None,
|
|
372
|
+
cmap='magma', cbar_label=None, annotate=False,
|
|
373
|
+
fmt='{:.2f}', fontsize=12):
|
|
374
|
+
"""
|
|
375
|
+
2D heatmap with optional colorbar and cell annotations.
|
|
376
|
+
"""
|
|
377
|
+
import numpy as np
|
|
378
|
+
import matplotlib as mpl
|
|
379
|
+
im = ax.imshow(matrix, cmap=cmap, aspect='auto')
|
|
380
|
+
if cbar_label:
|
|
381
|
+
cbar = ax.figure.colorbar(im, ax=ax)
|
|
382
|
+
cbar.set_label(cbar_label)
|
|
383
|
+
if x_labels:
|
|
384
|
+
ax.set_xticks(range(len(x_labels)))
|
|
385
|
+
ax.set_xticklabels(x_labels, rotation=30, ha='right', rotation_mode='anchor')
|
|
386
|
+
if y_labels:
|
|
387
|
+
ax.set_yticks(range(len(y_labels)))
|
|
388
|
+
ax.set_yticklabels(y_labels)
|
|
389
|
+
if annotate:
|
|
390
|
+
norm = mpl.colors.Normalize(vmin=matrix.min(), vmax=matrix.max())
|
|
391
|
+
cm_obj = plt.get_cmap(cmap)
|
|
392
|
+
for (i, j), val in np.ndenumerate(matrix):
|
|
393
|
+
r, g, b, _ = cm_obj(norm(val))
|
|
394
|
+
lum = 0.299*r + 0.587*g + 0.114*b
|
|
395
|
+
color = 'white' if lum < 0.5 else 'black'
|
|
396
|
+
ax.text(j, i, fmt.format(val), ha='center', va='center',
|
|
397
|
+
fontsize=fontsize, color=color)
|
|
398
|
+
ax.set_frame_on(False)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Numerical and annotation safety helpers
|
|
404
|
+
|
|
405
|
+
Copy `scripts/figure_safety.py` beside the plotting script, or add that scripts directory to `PYTHONPATH`. Then import the tested helpers rather than calling `np.interp` directly on a curve whose direction has not been asserted:
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
from figure_safety import interp_monotone, label_y_above
|
|
409
|
+
|
|
410
|
+
n_equivalent = interp_monotone(target_error, error_curve, example_counts)
|
|
411
|
+
label_y = label_y_above(metric_center, metric_spread)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
`interp_monotone` accepts strictly increasing or decreasing `xp`, reverses a decreasing grid and its paired values together, and rejects duplicates/direction changes. `label_y_above` places a shared annotation above the upper uncertainty extent rather than relying on a fixed `LABEL_Y`.
|
|
415
|
+
|
|
416
|
+
For compact mathematical labels, prefer supported Unicode glyphs such as `R²` over `$R^2$` when the meaning is unchanged, then audit the exported PDF:
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
python skills/nature-figure/scripts/audit_pdf_text.py figure.pdf --min-pt 5
|
|
420
|
+
python skills/nature-figure/scripts/audit_figure_collisions.py figure.pdf \
|
|
421
|
+
--json-out figure.collision-audit.json \
|
|
422
|
+
--overlay-pdf figure.collision-audit.pdf
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## require_matplotlib_panel_alignment(fig, ...)
|
|
428
|
+
|
|
429
|
+
For every multi-panel figure, measure the final Matplotlib axes positions after
|
|
430
|
+
the last `tight_layout`, constrained-layout draw, legend, colorbar or manual
|
|
431
|
+
position change and before saving. Copy `scripts/audit_panel_alignment.py`
|
|
432
|
+
beside the plotting source or add the skill's scripts directory to
|
|
433
|
+
`PYTHONPATH`, then use the blocking helper:
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
from audit_panel_alignment import require_matplotlib_panel_alignment
|
|
437
|
+
|
|
438
|
+
alignment = require_matplotlib_panel_alignment(
|
|
439
|
+
fig,
|
|
440
|
+
json_out="figures/figure.alignment.json",
|
|
441
|
+
overlay_svg="figures/figure.alignment.svg",
|
|
442
|
+
tolerance_pt=1.5,
|
|
443
|
+
gutter_tolerance_pt=1.5,
|
|
444
|
+
require_panel_labels=True,
|
|
445
|
+
strict=True,
|
|
446
|
+
)
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The helper calls `fig.canvas.draw()` and converts final axes positions to
|
|
450
|
+
physical points. Ordinary `plt.subplots()` and `GridSpec` layouts infer row and
|
|
451
|
+
column groups from `SubplotSpec`, then check:
|
|
452
|
+
|
|
453
|
+
- common top/bottom edges and equal heights within each row;
|
|
454
|
+
- equal final widths for three or four same-row panels with equal grid spans;
|
|
455
|
+
- common left/right edges and equal widths within each column;
|
|
456
|
+
- shared top/bottom or left/right grid boundaries when adjacent panels have
|
|
457
|
+
unequal spans, including `left two + right one` and `left one + right two`;
|
|
458
|
+
- repeated horizontal/vertical gutters;
|
|
459
|
+
- bold lowercase top-left panel-label anchors when detectable;
|
|
460
|
+
- plot-area rectangle overlap.
|
|
461
|
+
|
|
462
|
+
Ordinary unequal-span `GridSpec` layouts are inferred automatically: a two-row
|
|
463
|
+
spanning panel is compared with the appropriate upper and lower small panels at
|
|
464
|
+
the shared outer edges, without imposing an invalid equal-height comparison.
|
|
465
|
+
For ordinary horizontal `1 × 3` and `1 × 4` grids, equal column spans must also
|
|
466
|
+
produce equal final plot-area widths. An intentional unequal `width_ratios`
|
|
467
|
+
layout needs a panel-specific `panel-width` exemption with a reason.
|
|
468
|
+
For nested or separately created grids, declare intended comparisons instead
|
|
469
|
+
of asking the auditor to guess them:
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
require_matplotlib_panel_alignment(
|
|
473
|
+
fig,
|
|
474
|
+
axes=[ax_a, ax_b, ax_c, ax_d],
|
|
475
|
+
panel_ids=["a", "b", "c", "d"],
|
|
476
|
+
row_groups=[["a", "b"], ["c", "d"]],
|
|
477
|
+
column_groups=[["a", "c"], ["b", "d"]],
|
|
478
|
+
exclude_axes=[colorbar.ax, inset_ax],
|
|
479
|
+
exemptions=[
|
|
480
|
+
{
|
|
481
|
+
"panels": ["a"],
|
|
482
|
+
"checks": ["column", "panel-width"],
|
|
483
|
+
"reason": "hero panel intentionally spans two grid columns",
|
|
484
|
+
}
|
|
485
|
+
],
|
|
486
|
+
json_out="figures/figure.alignment.json",
|
|
487
|
+
strict=True,
|
|
488
|
+
)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Every exemption needs panel ids, exact checks and a reason. Prefer omitting a
|
|
492
|
+
hero panel, inset, legend-only axis or colorbar from unrelated comparison groups
|
|
493
|
+
over globally increasing the `1.5 pt` tolerance. Exit-equivalent failures raise
|
|
494
|
+
`PanelAlignmentError`; do not catch and ignore it in a delivery script. A
|
|
495
|
+
single-panel figure is outside this gate, while a multi-panel figure without
|
|
496
|
+
valid comparable groups is `NOT AUDITABLE`, not a pass.
|
|
497
|
+
|
|
498
|
+
The optional SVG contains measured rectangles only and is QA-only. Preserve
|
|
499
|
+
the JSON with the figure bundle, then run PDF glyph and collision QA on the
|
|
500
|
+
exported figure.
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## finalize_figure(fig, out_path, ...)
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
def finalize_figure(fig, out_path, formats=None, dpi=300,
|
|
508
|
+
pad=2, bbox_inches=None, close=True,
|
|
509
|
+
alignment_options=None):
|
|
510
|
+
"""
|
|
511
|
+
Apply tight_layout and save figure. For multi-panel figures, run
|
|
512
|
+
require_matplotlib_panel_alignment() after this layout operation and before
|
|
513
|
+
the save loop; do not close the figure before alignment QA.
|
|
514
|
+
|
|
515
|
+
Parameters
|
|
516
|
+
----------
|
|
517
|
+
out_path : str — path without extension, or with extension
|
|
518
|
+
formats : list — e.g. ['png', 'pdf']. If None, uses extension of out_path.
|
|
519
|
+
dpi : int — 300 standard, 600 for dense bar panels
|
|
520
|
+
pad : float — tight_layout pad (2 default, 1 for compact multi-panel)
|
|
521
|
+
alignment_options : dict | None — explicit groups/exemptions for unusual layouts
|
|
522
|
+
"""
|
|
523
|
+
import os
|
|
524
|
+
from pathlib import Path
|
|
525
|
+
base = Path(out_path)
|
|
526
|
+
if formats is None:
|
|
527
|
+
formats = [base.suffix.lstrip('.') or 'png']
|
|
528
|
+
base = base.with_suffix('')
|
|
529
|
+
os.makedirs(base.parent, exist_ok=True)
|
|
530
|
+
fig.tight_layout(pad=pad)
|
|
531
|
+
options = {} if alignment_options is None else dict(alignment_options)
|
|
532
|
+
require_matplotlib_panel_alignment(
|
|
533
|
+
fig,
|
|
534
|
+
json_out=str(base) + ".alignment.json",
|
|
535
|
+
overlay_svg=str(base) + ".alignment.svg",
|
|
536
|
+
strict=True,
|
|
537
|
+
**options,
|
|
538
|
+
)
|
|
539
|
+
saved = []
|
|
540
|
+
for fmt in formats:
|
|
541
|
+
p = str(base) + f'.{fmt}'
|
|
542
|
+
kw = {}
|
|
543
|
+
if bbox_inches is not None:
|
|
544
|
+
kw['bbox_inches'] = bbox_inches
|
|
545
|
+
fig.savefig(p, dpi=dpi, **kw)
|
|
546
|
+
saved.append(p)
|
|
547
|
+
if close:
|
|
548
|
+
plt.close(fig)
|
|
549
|
+
return saved
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Validation Rules
|
|
555
|
+
|
|
556
|
+
- `make_grouped_bar`: `len(categories)` must equal length of each array in `series`.
|
|
557
|
+
- `make_grouped_bar`: if `series_spread` is supplied, it must mirror `series`; annotations clear `value + spread`.
|
|
558
|
+
- `make_trend`: each array in `y_series` must have same length as `x`.
|
|
559
|
+
- `make_trend`: 2D run/seed arrays show a standard-deviation band by default; use a different definition only when explicitly justified and documented.
|
|
560
|
+
- `make_heatmap`: `matrix` must be 2D; `x_labels` length = `matrix.shape[1]`; `y_labels` length = `matrix.shape[0]`.
|
|
561
|
+
- `finalize_figure`: supported formats — `png`, `pdf`, `svg`, `eps`, `jpg`, `tif`.
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Conventions
|
|
566
|
+
|
|
567
|
+
- Save outputs under `./figures/` (or path given by user); `finalize_figure` creates parent dirs.
|
|
568
|
+
- In headless / batch runs, set non-interactive backend before importing pyplot:
|
|
569
|
+
```python
|
|
570
|
+
import matplotlib
|
|
571
|
+
matplotlib.use('Agg')
|
|
572
|
+
import matplotlib.pyplot as plt
|
|
573
|
+
```
|
|
574
|
+
- Always `plt.close(fig)` after saving to free memory.
|
|
575
|
+
- For multi-panel figures, prefer one baseline family plus one hero family; reserve green/red for delta cues.
|
|
576
|
+
- When color roles, resolution, or layout are underspecified and would change the figure, confirm with user before finalizing.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Plotting Asset Adaptation
|
|
2
|
+
|
|
3
|
+
Use this reference when reusing a bundled example, a preview image, or a user-provided plotting script. Treat examples as visual and structural starting points, not as evidence that a script is compatible with new data.
|
|
4
|
+
|
|
5
|
+
## Choose the reuse level
|
|
6
|
+
|
|
7
|
+
Assign every candidate to one of four levels before editing it:
|
|
8
|
+
|
|
9
|
+
| Level | Use when | Allowed changes |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Exact reuse | Scientific meaning, data shape, transformations, and backend all match | Input path, labels, and output prefix only |
|
|
12
|
+
| Structural adaptation | Scientific meaning and dimensionality match, but field names or group labels differ | Explicit field mapping plus documented transform guards |
|
|
13
|
+
| Style-only inheritance | The plot family is useful but the data structure or statistic differs | Palette, typography, spacing, marker, legend, and annotation conventions only |
|
|
14
|
+
| Build anew | The candidate answers a different question or would require replacing its statistical logic | Do not force the template; implement the confirmed figure contract directly |
|
|
15
|
+
|
|
16
|
+
Do not call a script production-ready merely because it renders its bundled example.
|
|
17
|
+
|
|
18
|
+
## Inspect before mapping
|
|
19
|
+
|
|
20
|
+
1. Open the companion preview when one exists.
|
|
21
|
+
2. State what the candidate actually displays: dimensionality, mark type, grouping, statistic, uncertainty, transforms, and annotations.
|
|
22
|
+
3. State what the requested panel must answer.
|
|
23
|
+
4. Reject structural reuse when those meanings differ. A 2D joint-density plot is not a reusable implementation of several 1D marginal densities, and a benchmark bar chart is not automatically a valid small-sample biological comparison.
|
|
24
|
+
|
|
25
|
+
## Map the data contract
|
|
26
|
+
|
|
27
|
+
Write an explicit mapping before changing code:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
template field -> user field -> role -> units -> allowed values
|
|
31
|
+
group field -> user field -> category order
|
|
32
|
+
replicate unit -> source rows/images -> biological or technical
|
|
33
|
+
uncertainty -> source field or calculation -> definition
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Confirm ambiguous mappings with the user. Never choose convenient columns silently. Keep identifiers separate from measurements and preserve the requested category order unless a scientifically justified ordering is declared.
|
|
37
|
+
|
|
38
|
+
## Guard transformations
|
|
39
|
+
|
|
40
|
+
Check every inherited transformation against the new data:
|
|
41
|
+
|
|
42
|
+
- Log axes and logarithms require strictly positive values unless a declared signed-log or pseudocount method is scientifically justified.
|
|
43
|
+
- Ratios and normalized values require finite denominators and a defined zero-denominator policy.
|
|
44
|
+
- Square-root transforms require non-negative inputs.
|
|
45
|
+
- Min-max scaling requires non-constant finite ranges.
|
|
46
|
+
- Binning and density estimation require enough distinct observations; record bin or bandwidth choices.
|
|
47
|
+
- Correlation, PCA, clustering, and statistical annotations require explicit missing-value handling and an appropriate replicate unit.
|
|
48
|
+
|
|
49
|
+
If a guard fails, change the transformation only when the scientific meaning remains valid and record the change. Otherwise use style-only inheritance or build anew.
|
|
50
|
+
|
|
51
|
+
## Preserve data integrity
|
|
52
|
+
|
|
53
|
+
- Use all supplied observations and requested variables by default.
|
|
54
|
+
- Do not downsample for aesthetics or rendering speed. Use rasterization, hexbin/density marks, transparent points, aggregation with a stated rule, or backend-native large-data rendering.
|
|
55
|
+
- If the analysis requires filtering, record the exact predicate and before/after row, column, replicate, or image counts.
|
|
56
|
+
- When the user explicitly requests sampling, record the method, sample size, seed, and whether sampling changes any inferential claim.
|
|
57
|
+
- Never leave simulated values in a production deliverable. Isolate demos behind an explicit demo flag or a separate example file.
|
|
58
|
+
|
|
59
|
+
## Adapt without erasing provenance
|
|
60
|
+
|
|
61
|
+
Copy the candidate into the task workspace before editing. Keep source assets unchanged. Preserve license and attribution notices, but do not expose private local paths or private template identifiers in generated figures, legends, manuscript text, or user-facing reports.
|
|
62
|
+
|
|
63
|
+
Record the reuse level and source category in internal QA notes. The adaptation method in this reference incorporates portable ideas from the Apache-2.0 `academic-figure-skill` workflow while replacing its path-bound runners and project-specific assumptions.
|
|
64
|
+
|
|
65
|
+
## Validate and deliver
|
|
66
|
+
|
|
67
|
+
1. Run the adapted script with representative real input using the selected backend.
|
|
68
|
+
2. Run `python scripts/validate_figure.py path/to/script.py` or the corresponding `.R` file.
|
|
69
|
+
3. Run `python scripts/audit_pdf_text.py path/to/figure.pdf --min-pt 5` and `python scripts/audit_figure_collisions.py path/to/figure.pdf --json-out path/to/figure.collision-audit.json` on the final export.
|
|
70
|
+
4. Treat static and geometry validation as preflight only; fix collision FAIL findings and review every WARN, but do not infer statistical correctness or complete visual quality from a pass.
|
|
71
|
+
5. Inspect SVG/PDF text editability, raster resolution, clipping, ambiguous overlays, color accessibility, and readability at final physical size.
|
|
72
|
+
6. Include the field mapping, exclusions, transform changes, collision report, and remaining caveats in the QA notes.
|