like-sci-figures 0.0.0-stage → 1.0.0
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/LICENSE +56 -0
- package/README.md +93 -2
- package/cli.js +79 -0
- package/examples/area.json +9 -0
- package/examples/bar.json +15 -0
- package/examples/bland_altman.json +128 -0
- package/examples/bode.json +8 -0
- package/examples/box.json +8 -0
- package/examples/bubble.json +7 -0
- package/examples/contour.json +10 -0
- package/examples/corr_matrix.json +99 -0
- package/examples/dendrogram.json +5 -0
- package/examples/density.json +8 -0
- package/examples/dot_plot.json +8 -0
- package/examples/ecdf.json +377 -0
- package/examples/errorbar.json +7 -0
- package/examples/forest.json +12 -0
- package/examples/heatmap.json +10 -0
- package/examples/hexbin.json +6009 -0
- package/examples/histogram.json +6 -0
- package/examples/kaplan_meier.json +6 -0
- package/examples/line.json +11 -0
- package/examples/manhattan.json +7 -0
- package/examples/panels.json +28 -0
- package/examples/pca.json +12 -0
- package/examples/periodogram.json +7 -0
- package/examples/pie.json +4 -0
- package/examples/radar.json +7 -0
- package/examples/ridge.json +11 -0
- package/examples/roc.json +6 -0
- package/examples/sankey.json +9 -0
- package/examples/scatter3d.json +6 -0
- package/examples/slope.json +6 -0
- package/examples/stacked_bar.json +12 -0
- package/examples/surface.json +11 -0
- package/examples/taylor.json +9 -0
- package/examples/ternary.json +18 -0
- package/examples/venn.json +12 -0
- package/examples/violin.json +9 -0
- package/examples/volcano.json +7 -0
- package/examples/waterfall.json +5 -0
- package/examples/windrose.json +407 -0
- package/package.json +33 -4
- package/requirements.txt +9 -0
- package/scripts/add_chart.py +201 -0
- package/scripts/build_gallery.py +360 -0
- package/scripts/chart_plugin.py +153 -0
- package/scripts/data_loader.py +260 -0
- package/scripts/fig_3d.py +103 -0
- package/scripts/fig_area.py +57 -0
- package/scripts/fig_bar.py +117 -0
- package/scripts/fig_biomed.py +289 -0
- package/scripts/fig_box.py +60 -0
- package/scripts/fig_bubble.py +65 -0
- package/scripts/fig_core.py +195 -0
- package/scripts/fig_datasci.py +276 -0
- package/scripts/fig_distribution.py +59 -0
- package/scripts/fig_engineering.py +224 -0
- package/scripts/fig_environ.py +239 -0
- package/scripts/fig_errorbar.py +49 -0
- package/scripts/fig_forest.py +54 -0
- package/scripts/fig_heatmap.py +51 -0
- package/scripts/fig_line.py +39 -0
- package/scripts/fig_pca.py +101 -0
- package/scripts/fig_pie.py +52 -0
- package/scripts/fig_radar.py +32 -0
- package/scripts/fig_sankey.py +196 -0
- package/scripts/fig_scatter.py +44 -0
- package/scripts/fig_subplots.py +124 -0
- package/scripts/fig_taylor.py +80 -0
- package/scripts/fig_venn.py +80 -0
- package/scripts/fig_windrose.py +82 -0
- package/scripts/font_detector.py +85 -0
- package/scripts/gen_figure.py +583 -0
- package/scripts/generate_samples.py +322 -0
- package/scripts/palette_picker.py +244 -0
- package/scripts/palette_pref.py +179 -0
- package/scripts/palette_preview.py +334 -0
- package/scripts/palettes.py +461 -0
- package/scripts/registry.py +138 -0
- package/scripts/render_utils.py +233 -0
- package/scripts/showcase.py +179 -0
- package/scripts/style_manager.py +267 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
like-sci-figures: Publication-quality academic figure generator.
|
|
4
|
+
Powered by scienceplots + matplotlib.
|
|
5
|
+
|
|
6
|
+
Supports 36 chart types with 11 journal styles and 10 curated color palettes,
|
|
7
|
+
CJK auto-detection, multi-panel subplots, and high-DPI output.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python gen_figure.py --type bar --data data.json --out figure.png --style nature
|
|
11
|
+
python gen_figure.py --type heatmap --data data.csv --out figure.svg --style ieee
|
|
12
|
+
python gen_figure.py --type surface --data data.json --out figure.png --style science
|
|
13
|
+
python gen_figure.py --type bar --data data.json --out fig.png --palette wx06_amber_navy
|
|
14
|
+
python gen_figure.py --type heatmap --data data.json --out fig.png --palette p1
|
|
15
|
+
python gen_figure.py --list-palettes
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# Fix Windows OpenBLAS threading conflict (must be set before numpy/matplotlib imports)
|
|
19
|
+
import os as _os
|
|
20
|
+
if _os.name == 'nt': # Windows
|
|
21
|
+
_os.environ.setdefault('OPENBLAS_NUM_THREADS', '1')
|
|
22
|
+
_os.environ.setdefault('OMP_NUM_THREADS', '1')
|
|
23
|
+
|
|
24
|
+
import argparse, json, os, sys, subprocess, tempfile, time
|
|
25
|
+
|
|
26
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
28
|
+
|
|
29
|
+
# ── Dependency check: friendly error instead of raw ImportError ───────────
|
|
30
|
+
def _check_deps():
|
|
31
|
+
import importlib.util
|
|
32
|
+
required = [("numpy", "numpy"), ("matplotlib", "matplotlib"),
|
|
33
|
+
("scipy", "scipy"), ("PIL", "pillow")]
|
|
34
|
+
missing = [pip for mod, pip in required
|
|
35
|
+
if importlib.util.find_spec(mod) is None]
|
|
36
|
+
if missing:
|
|
37
|
+
sys.stderr.write(
|
|
38
|
+
"\n[like-sci-figures] 缺少运行依赖: %s\n" % ", ".join(missing) +
|
|
39
|
+
"请先安装: pip install %s\n" % " ".join(missing) +
|
|
40
|
+
"(scienceplots 为可选增强: pip install scienceplots)\n")
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
_check_deps()
|
|
43
|
+
|
|
44
|
+
import matplotlib.pyplot as plt
|
|
45
|
+
|
|
46
|
+
from data_loader import load_data
|
|
47
|
+
from style_manager import setup_style, list_styles
|
|
48
|
+
from palettes import list_palettes, get_palette, palette_colormap, palette_info, PALETTES
|
|
49
|
+
from fig_core import (
|
|
50
|
+
load_cjk_font, needs_cjk, apply_base_style,
|
|
51
|
+
set_axis_labels, set_title, finalize_and_save
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# ── Import all generators ────────────────────────────────────────────────
|
|
55
|
+
from fig_bar import gen_bar, gen_stacked_bar
|
|
56
|
+
from fig_heatmap import gen_heatmap
|
|
57
|
+
from fig_scatter import gen_scatter
|
|
58
|
+
from fig_line import gen_line
|
|
59
|
+
from fig_box import gen_box, gen_violin
|
|
60
|
+
from fig_forest import gen_forest
|
|
61
|
+
from fig_distribution import gen_histogram, gen_density
|
|
62
|
+
from fig_pie import gen_pie
|
|
63
|
+
from fig_radar import gen_radar
|
|
64
|
+
from fig_3d import gen_surface, gen_contour, gen_scatter3d
|
|
65
|
+
from fig_bubble import gen_bubble
|
|
66
|
+
from fig_area import gen_area
|
|
67
|
+
from fig_errorbar import gen_errorbar
|
|
68
|
+
from fig_venn import gen_venn
|
|
69
|
+
from fig_pca import gen_pca
|
|
70
|
+
from fig_windrose import gen_windrose
|
|
71
|
+
from fig_taylor import gen_taylor
|
|
72
|
+
from fig_sankey import gen_sankey
|
|
73
|
+
from fig_subplots import gen_subplots
|
|
74
|
+
|
|
75
|
+
# Biomedical chart types (ChartPlugin subclasses).
|
|
76
|
+
from fig_biomed import VolcanoPlot, ManhattanPlot, KaplanMeierCurve, ROCCurve
|
|
77
|
+
_volcano = VolcanoPlot()
|
|
78
|
+
_manhattan = ManhattanPlot()
|
|
79
|
+
_km = KaplanMeierCurve()
|
|
80
|
+
_roc = ROCCurve()
|
|
81
|
+
|
|
82
|
+
# Data science chart types (ChartPlugin subclasses).
|
|
83
|
+
from fig_datasci import RidgePlot, TernaryPlot, DotPlot, DendrogramPlot
|
|
84
|
+
_ridge = RidgePlot()
|
|
85
|
+
_ternary = TernaryPlot()
|
|
86
|
+
_dot_plot = DotPlot()
|
|
87
|
+
_dendrogram = DendrogramPlot()
|
|
88
|
+
|
|
89
|
+
# Engineering/finance chart types (ChartPlugin subclasses).
|
|
90
|
+
from fig_engineering import SlopeChart, WaterfallChart, BodePlot, PeriodogramPlot
|
|
91
|
+
_slope = SlopeChart()
|
|
92
|
+
_waterfall = WaterfallChart()
|
|
93
|
+
_bode = BodePlot()
|
|
94
|
+
_periodogram = PeriodogramPlot()
|
|
95
|
+
|
|
96
|
+
# Environmental statistics suite (ChartPlugin subclasses).
|
|
97
|
+
from fig_environ import CorrMatrixPlot, HexbinPlot, ECDFPlot, BlandAltmanPlot
|
|
98
|
+
_corr_matrix = CorrMatrixPlot()
|
|
99
|
+
_hexbin = HexbinPlot()
|
|
100
|
+
_ecdf = ECDFPlot()
|
|
101
|
+
_bland_altman = BlandAltmanPlot()
|
|
102
|
+
|
|
103
|
+
# ── Generator registry ────────────────────────────────────────────────────
|
|
104
|
+
GENERATORS = {
|
|
105
|
+
"bar": gen_bar,
|
|
106
|
+
"grouped_bar": gen_bar,
|
|
107
|
+
"stacked_bar": gen_stacked_bar,
|
|
108
|
+
"stacked": gen_stacked_bar,
|
|
109
|
+
"heatmap": gen_heatmap,
|
|
110
|
+
"scatter": gen_scatter,
|
|
111
|
+
"line": gen_line,
|
|
112
|
+
"box": gen_box,
|
|
113
|
+
"boxplot": gen_box,
|
|
114
|
+
"violin": gen_violin,
|
|
115
|
+
"forest": gen_forest,
|
|
116
|
+
"histogram": gen_histogram,
|
|
117
|
+
"density": gen_density,
|
|
118
|
+
"pie": gen_pie,
|
|
119
|
+
"donut": gen_pie,
|
|
120
|
+
"radar": gen_radar,
|
|
121
|
+
"surface": gen_surface,
|
|
122
|
+
"contour": gen_contour,
|
|
123
|
+
"scatter3d": gen_scatter3d,
|
|
124
|
+
"bubble": gen_bubble,
|
|
125
|
+
"area": gen_area,
|
|
126
|
+
"errorbar": gen_errorbar,
|
|
127
|
+
"venn": gen_venn,
|
|
128
|
+
"pca": gen_pca,
|
|
129
|
+
"windrose": gen_windrose,
|
|
130
|
+
"taylor": gen_taylor,
|
|
131
|
+
"sankey": gen_sankey,
|
|
132
|
+
# Biomedical charts (ChartPlugin instances)
|
|
133
|
+
"volcano": _volcano,
|
|
134
|
+
"manhattan": _manhattan,
|
|
135
|
+
"kaplan_meier": _km,
|
|
136
|
+
"roc": _roc,
|
|
137
|
+
# Data science charts (ChartPlugin instances)
|
|
138
|
+
"ridge": _ridge,
|
|
139
|
+
"ternary": _ternary,
|
|
140
|
+
"dot_plot": _dot_plot,
|
|
141
|
+
"dendrogram": _dendrogram,
|
|
142
|
+
# Engineering/finance charts (ChartPlugin instances)
|
|
143
|
+
"slope": _slope,
|
|
144
|
+
"waterfall": _waterfall,
|
|
145
|
+
"bode": _bode,
|
|
146
|
+
"periodogram": _periodogram,
|
|
147
|
+
# Environmental statistics suite (ChartPlugin instances)
|
|
148
|
+
"corr_matrix": _corr_matrix,
|
|
149
|
+
"hexbin": _hexbin,
|
|
150
|
+
"ecdf": _ecdf,
|
|
151
|
+
"bland_altman": _bland_altman,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Chart types that need 3D axes
|
|
155
|
+
_3D_TYPES = {"surface", "scatter3d"}
|
|
156
|
+
|
|
157
|
+
# Chart types that need polar axes
|
|
158
|
+
_POLAR_TYPES = {"radar", "windrose", "taylor"}
|
|
159
|
+
|
|
160
|
+
# Chart types that need specific figure size
|
|
161
|
+
_SPECIAL_TYPES = {"subplots"}
|
|
162
|
+
|
|
163
|
+
# Chart types that should NOT receive a y-axis grid overlay
|
|
164
|
+
# (grid lines would clutter heatmaps, pies, forest CI plots, venn & sankey)
|
|
165
|
+
_NO_GRID = {"heatmap", "pie", "forest", "venn", "sankey", "corr_matrix"}
|
|
166
|
+
|
|
167
|
+
# Chart types that use a continuous colormap (palette -> cmap)
|
|
168
|
+
_CMAP_TYPES = {"heatmap", "contour", "surface", "corr_matrix", "hexbin"}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def infer_n_series(data, chart_type):
|
|
172
|
+
"""Best-effort count of the number of series/groups in the data."""
|
|
173
|
+
if not isinstance(data, dict):
|
|
174
|
+
return None
|
|
175
|
+
for key in ("series", "groups", "datasets"):
|
|
176
|
+
v = data.get(key)
|
|
177
|
+
if isinstance(v, dict):
|
|
178
|
+
return len(v)
|
|
179
|
+
if isinstance(v, list):
|
|
180
|
+
return len(v)
|
|
181
|
+
if chart_type in ("pie", "donut"):
|
|
182
|
+
for key in ("values", "sizes"):
|
|
183
|
+
v = data.get(key)
|
|
184
|
+
if isinstance(v, (list, dict)):
|
|
185
|
+
return len(v)
|
|
186
|
+
if isinstance(data.get("labels"), list) and len(data["labels"]) > 0:
|
|
187
|
+
# labels may enumerate categories (box/violin) — count only if values are nested
|
|
188
|
+
vals = data.get("values")
|
|
189
|
+
if isinstance(vals, dict):
|
|
190
|
+
return len(vals)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ── Batch verification of every example ────────────────────────────────────
|
|
195
|
+
# Chart-type → example-file mapping lives in registry.py (the single source of
|
|
196
|
+
# truth). _example_type() inverts it so verify_all can map a file path back to
|
|
197
|
+
# its chart type without re-declaring the table here.
|
|
198
|
+
from registry import CHART_EXAMPLES, resolve
|
|
199
|
+
from render_utils import read_palette_state, AUTO_SENTINEL
|
|
200
|
+
|
|
201
|
+
_EXAMPLE_TO_TYPE = {
|
|
202
|
+
os.path.splitext(v)[0]: k
|
|
203
|
+
for k, v in CHART_EXAMPLES.items()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _example_type(path):
|
|
208
|
+
"""Infer the chart type for one example file (or None if not recognized)."""
|
|
209
|
+
stem = os.path.splitext(os.path.basename(path))[0]
|
|
210
|
+
return _EXAMPLE_TO_TYPE.get(stem)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def verify_all(style="nature", palette=None, quiet=False):
|
|
214
|
+
"""Render every example file and report pass/fail per type.
|
|
215
|
+
|
|
216
|
+
Returns a truthy value (0 = all passed, 1 = some failed) suitable for use
|
|
217
|
+
as a process exit code via `return verify_all(...) or 0`.
|
|
218
|
+
"""
|
|
219
|
+
examples_dir = os.path.join(SCRIPT_DIR, "..", "examples")
|
|
220
|
+
if not os.path.isdir(examples_dir):
|
|
221
|
+
print("ERROR: examples/ directory not found at %s" % examples_dir,
|
|
222
|
+
file=sys.stderr)
|
|
223
|
+
return 1
|
|
224
|
+
|
|
225
|
+
todo = []
|
|
226
|
+
for name in sorted(os.listdir(examples_dir)):
|
|
227
|
+
path = os.path.join(examples_dir, name)
|
|
228
|
+
if not os.path.isfile(path):
|
|
229
|
+
continue
|
|
230
|
+
chart_type = _example_type(path)
|
|
231
|
+
if chart_type is None:
|
|
232
|
+
continue # not a chart example (e.g. README)
|
|
233
|
+
todo.append((chart_type, path))
|
|
234
|
+
|
|
235
|
+
if not todo:
|
|
236
|
+
print("No example files recognised in %s" % examples_dir,
|
|
237
|
+
file=sys.stderr)
|
|
238
|
+
return 1
|
|
239
|
+
|
|
240
|
+
tmpdir = tempfile.mkdtemp(prefix="sf_verify_")
|
|
241
|
+
passed, failed = [], []
|
|
242
|
+
t0 = time.time()
|
|
243
|
+
|
|
244
|
+
for chart_type, path in todo:
|
|
245
|
+
out = os.path.join(tmpdir, "%s.png" % chart_type)
|
|
246
|
+
cmd = [sys.executable, os.path.abspath(__file__),
|
|
247
|
+
"--type", chart_type, "--data", path, "--out", out,
|
|
248
|
+
"--style", style,
|
|
249
|
+
"--width", "4", "--height", "3", "--dpi", "100"]
|
|
250
|
+
if palette:
|
|
251
|
+
cmd += ["--palette", palette]
|
|
252
|
+
try:
|
|
253
|
+
proc = subprocess.run(cmd, capture_output=True, text=True,
|
|
254
|
+
timeout=120)
|
|
255
|
+
ok = proc.returncode == 0 and os.path.exists(out) and \
|
|
256
|
+
os.path.getsize(out) > 0
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
proc = None
|
|
259
|
+
ok = False
|
|
260
|
+
label = "%-12s %s" % (chart_type, os.path.basename(path))
|
|
261
|
+
if ok:
|
|
262
|
+
passed.append(label)
|
|
263
|
+
if not quiet:
|
|
264
|
+
print(" OK %s" % label)
|
|
265
|
+
else:
|
|
266
|
+
failed.append((label, proc))
|
|
267
|
+
if not quiet:
|
|
268
|
+
tail = ""
|
|
269
|
+
if proc is not None:
|
|
270
|
+
tail = (proc.stderr or proc.stdout or "").strip().splitlines()
|
|
271
|
+
tail = " | ".join(tail[-2:])
|
|
272
|
+
print(" FAIL %s%s" % (label, (" — " + tail) if tail else ""))
|
|
273
|
+
|
|
274
|
+
total = time.time() - t0
|
|
275
|
+
print("")
|
|
276
|
+
print("Verify summary: %d passed, %d failed (%.1fs, style=%s%s)"
|
|
277
|
+
% (len(passed), len(failed), total, style,
|
|
278
|
+
", palette=" + palette if palette else ""))
|
|
279
|
+
if failed:
|
|
280
|
+
for label, proc in failed:
|
|
281
|
+
if proc is not None:
|
|
282
|
+
print(" FAIL %s" % label)
|
|
283
|
+
for line in (proc.stderr or "").strip().splitlines()[-3:]:
|
|
284
|
+
print(" " + line)
|
|
285
|
+
return 1
|
|
286
|
+
return 0
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def main():
|
|
290
|
+
parser = argparse.ArgumentParser(
|
|
291
|
+
description="like-sci-figures: Publication-quality academic figure generator (scienceplots-based)"
|
|
292
|
+
)
|
|
293
|
+
parser.add_argument("--type", "-t", choices=sorted(list(GENERATORS.keys()) + ["subplots"]),
|
|
294
|
+
help="Figure type")
|
|
295
|
+
parser.add_argument("--data", "-d", help="Input data file (JSON or CSV)")
|
|
296
|
+
parser.add_argument("--out", "-o", help="Output file path (.png/.svg/.pdf)")
|
|
297
|
+
parser.add_argument("--title", default="", help="Figure title (use \\n for newline)")
|
|
298
|
+
parser.add_argument("--xlabel", default="", help="X-axis label")
|
|
299
|
+
parser.add_argument("--ylabel", default="", help="Y-axis label")
|
|
300
|
+
parser.add_argument("--style", "-s", default="nature", choices=list_styles(),
|
|
301
|
+
help="Journal style (from scienceplots)")
|
|
302
|
+
parser.add_argument("--palette", "-p", default=None,
|
|
303
|
+
help="Color palette override: slug (wx01_blue_red), "
|
|
304
|
+
"alias (p1/wx1/1), or raw hex list \"#F26A1D,#1E5FA8\". "
|
|
305
|
+
"If omitted, reads from .workbuddy/like-sci-figures/palette.json")
|
|
306
|
+
parser.add_argument("--no-pref", action="store_true",
|
|
307
|
+
help="Do not read palette preference from disk")
|
|
308
|
+
parser.add_argument("--palette-n", type=int, default=None,
|
|
309
|
+
help="Number of colors to evenly sample from --palette")
|
|
310
|
+
parser.add_argument("--palette-reverse", action="store_true",
|
|
311
|
+
help="Reverse the palette order (e.g. make heatmap high=red)")
|
|
312
|
+
parser.add_argument("--list-palettes", action="store_true",
|
|
313
|
+
help="List available palettes and exit")
|
|
314
|
+
parser.add_argument("--cjk", action="store_true", help="Force enable CJK font")
|
|
315
|
+
parser.add_argument("--cjk-font", default=None, help="Specific CJK font path")
|
|
316
|
+
parser.add_argument("--width", type=float, default=None, help="Figure width (inches)")
|
|
317
|
+
parser.add_argument("--height", type=float, default=None, help="Figure height (inches)")
|
|
318
|
+
parser.add_argument("--legend", action="store_true", default=True, help="Show legend")
|
|
319
|
+
parser.add_argument("--no-legend", action="store_true", help="Hide legend")
|
|
320
|
+
|
|
321
|
+
# Bar-specific
|
|
322
|
+
parser.add_argument("--show-values", action="store_true", help="Show value labels on bars")
|
|
323
|
+
|
|
324
|
+
# Scatter-specific
|
|
325
|
+
parser.add_argument("--trend", action="store_true", default=True, help="Show trend line (scatter)")
|
|
326
|
+
parser.add_argument("--no-trend", action="store_true", help="Hide trend line")
|
|
327
|
+
|
|
328
|
+
# Heatmap-specific
|
|
329
|
+
parser.add_argument("--cmap", default=None, help="Colormap for heatmap/contour/surface")
|
|
330
|
+
parser.add_argument("--vmin", type=float, default=None, help="Heatmap vmin")
|
|
331
|
+
parser.add_argument("--vmax", type=float, default=None, help="Heatmap vmax")
|
|
332
|
+
parser.add_argument("--center", type=float, default=None, help="Center value for divergence colormaps")
|
|
333
|
+
|
|
334
|
+
# Distribution-specific
|
|
335
|
+
parser.add_argument("--kde", action="store_true", default=True, help="Show KDE overlay (histogram)")
|
|
336
|
+
parser.add_argument("--no-kde", action="store_true", help="Hide KDE overlay")
|
|
337
|
+
parser.add_argument("--bins", default="auto", help="Histogram bins (int or 'auto')")
|
|
338
|
+
parser.add_argument("--density", action="store_true", help="Normalize histogram to density")
|
|
339
|
+
|
|
340
|
+
# Pie-specific
|
|
341
|
+
parser.add_argument("--donut", action="store_true", help="Draw as donut chart (when --type pie)")
|
|
342
|
+
parser.add_argument("--show-pct", action="store_true", default=True, help="Show percentages on pie")
|
|
343
|
+
parser.add_argument("--no-pct", action="store_true", help="Hide percentages")
|
|
344
|
+
|
|
345
|
+
# 3D-specific
|
|
346
|
+
parser.add_argument("--contour-floor", action="store_true", help="Show contour project floor (surface)")
|
|
347
|
+
parser.add_argument("--filled", action="store_true", default=True, help="Filled contour (contour)")
|
|
348
|
+
parser.add_argument("--no-filled", action="store_true", help="Line-only contour")
|
|
349
|
+
|
|
350
|
+
# Area-specific
|
|
351
|
+
parser.add_argument("--stacked", dest="stacked", action="store_true", default=True,
|
|
352
|
+
help="Stack area chart (area)")
|
|
353
|
+
parser.add_argument("--no-stacked", dest="stacked", action="store_false",
|
|
354
|
+
help="Do not stack area chart (area)")
|
|
355
|
+
|
|
356
|
+
# Wind rose / PCA
|
|
357
|
+
parser.add_argument("--sectors", type=int, default=None, help="Wind rose sector count")
|
|
358
|
+
parser.add_argument("--components", type=int, default=2, help="PCA components to plot (pca)")
|
|
359
|
+
|
|
360
|
+
# Subplots
|
|
361
|
+
parser.add_argument("--layout", default="auto", help="Subplot layout: auto, RxC, or rows,cols (e.g. 2x3)")
|
|
362
|
+
|
|
363
|
+
parser.add_argument("--dpi", type=int, default=300, help="Output DPI (default: 300)")
|
|
364
|
+
parser.add_argument("--list-styles", action="store_true", help="List available styles and exit")
|
|
365
|
+
parser.add_argument("--verify-all", action="store_true",
|
|
366
|
+
help="Render every example file in examples/ with its inferred "
|
|
367
|
+
"chart type and report per-example pass/fail. Exits 0 only "
|
|
368
|
+
"if every example renders cleanly.")
|
|
369
|
+
parser.add_argument("--verify-style", default="nature",
|
|
370
|
+
help="Journal style used by --verify-all (default: %(default)s)")
|
|
371
|
+
parser.add_argument("--verify-palette", default=None,
|
|
372
|
+
help="Optional palette slug to exercise under --verify-all")
|
|
373
|
+
|
|
374
|
+
args = parser.parse_args()
|
|
375
|
+
|
|
376
|
+
if args.list_styles:
|
|
377
|
+
print("Available styles: " + ", ".join(list_styles()))
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
if args.list_palettes:
|
|
381
|
+
print("Available palettes:\n")
|
|
382
|
+
for slug, meta in PALETTES.items():
|
|
383
|
+
print(f" {slug} [{meta['kind']}] {meta['zh']}")
|
|
384
|
+
print(" " + " ".join(meta["colors"]))
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
if args.verify_all:
|
|
388
|
+
# Auto-read palette preference unless --verify-palette or --no-pref
|
|
389
|
+
v_palette = args.verify_palette
|
|
390
|
+
if not v_palette and not args.no_pref:
|
|
391
|
+
stored, _ = read_palette_state()
|
|
392
|
+
if stored and stored != AUTO_SENTINEL:
|
|
393
|
+
v_palette = stored
|
|
394
|
+
print("Verify palette (from preference): %s" % stored,
|
|
395
|
+
file=sys.stderr)
|
|
396
|
+
return verify_all(style=args.verify_style,
|
|
397
|
+
palette=v_palette) or 0
|
|
398
|
+
|
|
399
|
+
# ── Style setup ──────────────────────────────────────────────────────
|
|
400
|
+
custom_rc = {}
|
|
401
|
+
if args.width:
|
|
402
|
+
custom_rc["figure.figsize"] = (args.width, args.height or args.width * 0.65)
|
|
403
|
+
elif args.height:
|
|
404
|
+
custom_rc["figure.figsize"] = (args.height * 1.5, args.height)
|
|
405
|
+
if args.dpi:
|
|
406
|
+
custom_rc["savefig.dpi"] = args.dpi
|
|
407
|
+
|
|
408
|
+
style_info = setup_style(args.style, custom_rc)
|
|
409
|
+
colors = style_info["colors"]
|
|
410
|
+
palette_cmap = None
|
|
411
|
+
|
|
412
|
+
# ── Load data ─────────────────────────────────────────────────────────
|
|
413
|
+
if args.type == "subplots":
|
|
414
|
+
with open(args.data, 'r', encoding='utf-8') as f:
|
|
415
|
+
data = json.load(f)
|
|
416
|
+
else:
|
|
417
|
+
data = load_data(args.data, chart_type=args.type)
|
|
418
|
+
|
|
419
|
+
if args.type == "subplots":
|
|
420
|
+
if not data or not isinstance(data, list):
|
|
421
|
+
print("ERROR: Subplots data must be a non-empty JSON array.", file=sys.stderr)
|
|
422
|
+
sys.exit(1)
|
|
423
|
+
else:
|
|
424
|
+
if not data or not isinstance(data, dict):
|
|
425
|
+
print("ERROR: Data file is empty or invalid.", file=sys.stderr)
|
|
426
|
+
sys.exit(1)
|
|
427
|
+
|
|
428
|
+
has_data = any(data.get(k) for k in
|
|
429
|
+
["series", "matrix", "x", "y", "estimates", "values",
|
|
430
|
+
"direction", "speed", "regions", "nodes", "links",
|
|
431
|
+
"models", "obs_std", "samples", "variables",
|
|
432
|
+
"sizes", "xerr", "yerr",
|
|
433
|
+
# Biomedical charts
|
|
434
|
+
"log2fc", "pvalue", "chr", "pos", "time", "event",
|
|
435
|
+
"group", "fpr", "tpr",
|
|
436
|
+
# Data science charts
|
|
437
|
+
"points", "leaves", "merges",
|
|
438
|
+
# Engineering/finance charts
|
|
439
|
+
"before", "after", "freq", "mag", "phase", "psd",
|
|
440
|
+
# Environmental statistics suite
|
|
441
|
+
"method1", "method2", "gridsize"])
|
|
442
|
+
if not has_data:
|
|
443
|
+
print("ERROR: No data fields found.", file=sys.stderr)
|
|
444
|
+
sys.exit(1)
|
|
445
|
+
|
|
446
|
+
# ── Palette override (from palettes.py) ───────────────────────────────
|
|
447
|
+
# If --palette was not given, auto-read the task-scoped preference stored
|
|
448
|
+
# by palette_pref.py. The "auto" sentinel means the user explicitly opted
|
|
449
|
+
# out: skip the palette override and let the journal style supply its
|
|
450
|
+
# own colours.
|
|
451
|
+
effective_palette = args.palette
|
|
452
|
+
if not effective_palette and not args.no_pref:
|
|
453
|
+
stored, state_file = read_palette_state()
|
|
454
|
+
if stored and stored != AUTO_SENTINEL:
|
|
455
|
+
effective_palette = stored
|
|
456
|
+
print("Palette (from preference): %s (%s)" % (stored, state_file),
|
|
457
|
+
file=sys.stderr)
|
|
458
|
+
elif stored == AUTO_SENTINEL:
|
|
459
|
+
print("Palette preference: auto (skipped, using style colours)",
|
|
460
|
+
file=sys.stderr)
|
|
461
|
+
|
|
462
|
+
if effective_palette:
|
|
463
|
+
try:
|
|
464
|
+
n = args.palette_n
|
|
465
|
+
if n is None and args.type not in _CMAP_TYPES:
|
|
466
|
+
# For categorical charts, sample one color per series so a long
|
|
467
|
+
# palette spreads across the groups (matching the poster style).
|
|
468
|
+
n = infer_n_series(data if args.type != "subplots" else None, args.type)
|
|
469
|
+
colors = get_palette(effective_palette, n)
|
|
470
|
+
except ValueError as e:
|
|
471
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
472
|
+
sys.exit(2)
|
|
473
|
+
print(f"Palette: {effective_palette} -> {' '.join(colors)}", file=sys.stderr)
|
|
474
|
+
if args.palette_reverse:
|
|
475
|
+
colors = list(reversed(colors))
|
|
476
|
+
if args.type in _CMAP_TYPES and not args.cmap:
|
|
477
|
+
palette_cmap = palette_colormap(effective_palette)
|
|
478
|
+
if palette_cmap is not None and args.palette_reverse:
|
|
479
|
+
palette_cmap = palette_cmap.reversed()
|
|
480
|
+
|
|
481
|
+
# ── CJK font ──────────────────────────────────────────────────────────
|
|
482
|
+
cjk_fp = None
|
|
483
|
+
if args.cjk or args.cjk_font or needs_cjk(data, args.title, args.xlabel, args.ylabel):
|
|
484
|
+
cjk_fp, cjk_name = load_cjk_font(args.cjk_font)
|
|
485
|
+
if cjk_name:
|
|
486
|
+
plt.rcParams['font.sans-serif'] = [cjk_name, 'DejaVu Sans'] + \
|
|
487
|
+
plt.rcParams.get('font.sans-serif', [])
|
|
488
|
+
plt.rcParams['axes.unicode_minus'] = False
|
|
489
|
+
if cjk_fp:
|
|
490
|
+
print(f"CJK font loaded: {cjk_name}", file=sys.stderr)
|
|
491
|
+
else:
|
|
492
|
+
print("WARNING: No CJK font found, CJK characters may not render", file=sys.stderr)
|
|
493
|
+
|
|
494
|
+
# ── Create figure ─────────────────────────────────────────────────────
|
|
495
|
+
kw_width = args.width if args.width else plt.rcParams.get("figure.figsize", (8, 5))[0]
|
|
496
|
+
kw_height = args.height if args.height else plt.rcParams.get("figure.figsize", (8, 5))[1]
|
|
497
|
+
|
|
498
|
+
# Special axes types
|
|
499
|
+
if args.type in _3D_TYPES:
|
|
500
|
+
fig = plt.figure(figsize=(kw_width, kw_height))
|
|
501
|
+
ax = fig.add_subplot(111, projection='3d')
|
|
502
|
+
elif args.type in _POLAR_TYPES:
|
|
503
|
+
fig = plt.figure(figsize=(kw_width, kw_height))
|
|
504
|
+
ax = fig.add_subplot(111, polar=True)
|
|
505
|
+
elif args.type == "subplots":
|
|
506
|
+
fig, ax = plt.subplots(figsize=(kw_width, kw_height))
|
|
507
|
+
new_fig = gen_subplots(data, fig, ax, colors, cjk_fp,
|
|
508
|
+
layout=args.layout)
|
|
509
|
+
if new_fig is not None:
|
|
510
|
+
fig = new_fig
|
|
511
|
+
# For subplots, save directly
|
|
512
|
+
out_ext = os.path.splitext(args.out)[1].lower()
|
|
513
|
+
save_kwargs = {"bbox_inches": "tight", "facecolor": "white", "edgecolor": "none"}
|
|
514
|
+
if out_ext == ".svg":
|
|
515
|
+
save_kwargs["format"] = "svg"
|
|
516
|
+
elif out_ext == ".pdf":
|
|
517
|
+
save_kwargs["format"] = "pdf"
|
|
518
|
+
fig.savefig(args.out, **save_kwargs)
|
|
519
|
+
plt.close(fig)
|
|
520
|
+
sz = os.path.getsize(args.out)
|
|
521
|
+
print(f"Saved: {args.out} ({sz:,} bytes)", file=sys.stderr)
|
|
522
|
+
return
|
|
523
|
+
else:
|
|
524
|
+
fig, ax = plt.subplots(figsize=(kw_width, kw_height))
|
|
525
|
+
|
|
526
|
+
# ── Generate ──────────────────────────────────────────────────────────
|
|
527
|
+
gen_func = GENERATORS[args.type]
|
|
528
|
+
# Parse bins: convert to int if numeric string, otherwise keep as "auto"
|
|
529
|
+
bins_val = args.bins
|
|
530
|
+
if isinstance(bins_val, str) and bins_val.isdigit():
|
|
531
|
+
bins_val = int(bins_val)
|
|
532
|
+
|
|
533
|
+
gen_kwargs = {
|
|
534
|
+
"show_values": args.show_values,
|
|
535
|
+
"trend": args.trend and not args.no_trend,
|
|
536
|
+
"cmap": args.cmap or palette_cmap,
|
|
537
|
+
"vmin": args.vmin,
|
|
538
|
+
"vmax": args.vmax,
|
|
539
|
+
"center": args.center,
|
|
540
|
+
"kde": args.kde and not args.no_kde,
|
|
541
|
+
"bins": bins_val,
|
|
542
|
+
"density": args.density,
|
|
543
|
+
"show_pct": args.show_pct and not args.no_pct,
|
|
544
|
+
# `--type donut` must behave like `--type pie --donut` (docs advertise
|
|
545
|
+
# donut as its own type); otherwise a plain pie is silently drawn.
|
|
546
|
+
"donut": args.donut or args.type == "donut",
|
|
547
|
+
"contour_floor": args.contour_floor,
|
|
548
|
+
"filled": args.filled and not args.no_filled,
|
|
549
|
+
"stacked": args.stacked,
|
|
550
|
+
"sectors": args.sectors,
|
|
551
|
+
"components": args.components,
|
|
552
|
+
}
|
|
553
|
+
extra = gen_func(data, ax, colors, cjk_fp, **gen_kwargs)
|
|
554
|
+
|
|
555
|
+
# ── Style & labels ────────────────────────────────────────────────────
|
|
556
|
+
if args.type not in _3D_TYPES and args.type not in _POLAR_TYPES:
|
|
557
|
+
apply_base_style(ax, grid=args.type not in _NO_GRID)
|
|
558
|
+
elif args.type in _3D_TYPES:
|
|
559
|
+
# 3D axes: make panes transparent
|
|
560
|
+
ax.xaxis.pane.fill = False
|
|
561
|
+
ax.yaxis.pane.fill = False
|
|
562
|
+
ax.zaxis.pane.fill = False
|
|
563
|
+
ax.xaxis.pane.set_edgecolor('white')
|
|
564
|
+
ax.yaxis.pane.set_edgecolor('white')
|
|
565
|
+
ax.zaxis.pane.set_edgecolor('white')
|
|
566
|
+
|
|
567
|
+
set_title(ax, args.title, cjk_fp)
|
|
568
|
+
if args.type not in _3D_TYPES:
|
|
569
|
+
set_axis_labels(ax, args.xlabel, args.ylabel, cjk_fp)
|
|
570
|
+
else:
|
|
571
|
+
if args.xlabel:
|
|
572
|
+
ax.set_xlabel(args.xlabel)
|
|
573
|
+
if args.ylabel:
|
|
574
|
+
ax.set_ylabel(args.ylabel)
|
|
575
|
+
|
|
576
|
+
# ── Finalize ──────────────────────────────────────────────────────────
|
|
577
|
+
legend = args.legend and not args.no_legend
|
|
578
|
+
finalize_and_save(fig, ax, args.out, legend=legend,
|
|
579
|
+
colorbar_artist=extra, cbar_label=None, cjk_fp=cjk_fp)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
if __name__ == "__main__":
|
|
583
|
+
main()
|