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.
Files changed (83) hide show
  1. package/LICENSE +56 -0
  2. package/README.md +93 -2
  3. package/cli.js +79 -0
  4. package/examples/area.json +9 -0
  5. package/examples/bar.json +15 -0
  6. package/examples/bland_altman.json +128 -0
  7. package/examples/bode.json +8 -0
  8. package/examples/box.json +8 -0
  9. package/examples/bubble.json +7 -0
  10. package/examples/contour.json +10 -0
  11. package/examples/corr_matrix.json +99 -0
  12. package/examples/dendrogram.json +5 -0
  13. package/examples/density.json +8 -0
  14. package/examples/dot_plot.json +8 -0
  15. package/examples/ecdf.json +377 -0
  16. package/examples/errorbar.json +7 -0
  17. package/examples/forest.json +12 -0
  18. package/examples/heatmap.json +10 -0
  19. package/examples/hexbin.json +6009 -0
  20. package/examples/histogram.json +6 -0
  21. package/examples/kaplan_meier.json +6 -0
  22. package/examples/line.json +11 -0
  23. package/examples/manhattan.json +7 -0
  24. package/examples/panels.json +28 -0
  25. package/examples/pca.json +12 -0
  26. package/examples/periodogram.json +7 -0
  27. package/examples/pie.json +4 -0
  28. package/examples/radar.json +7 -0
  29. package/examples/ridge.json +11 -0
  30. package/examples/roc.json +6 -0
  31. package/examples/sankey.json +9 -0
  32. package/examples/scatter3d.json +6 -0
  33. package/examples/slope.json +6 -0
  34. package/examples/stacked_bar.json +12 -0
  35. package/examples/surface.json +11 -0
  36. package/examples/taylor.json +9 -0
  37. package/examples/ternary.json +18 -0
  38. package/examples/venn.json +12 -0
  39. package/examples/violin.json +9 -0
  40. package/examples/volcano.json +7 -0
  41. package/examples/waterfall.json +5 -0
  42. package/examples/windrose.json +407 -0
  43. package/package.json +33 -4
  44. package/requirements.txt +9 -0
  45. package/scripts/add_chart.py +201 -0
  46. package/scripts/build_gallery.py +360 -0
  47. package/scripts/chart_plugin.py +153 -0
  48. package/scripts/data_loader.py +260 -0
  49. package/scripts/fig_3d.py +103 -0
  50. package/scripts/fig_area.py +57 -0
  51. package/scripts/fig_bar.py +117 -0
  52. package/scripts/fig_biomed.py +289 -0
  53. package/scripts/fig_box.py +60 -0
  54. package/scripts/fig_bubble.py +65 -0
  55. package/scripts/fig_core.py +195 -0
  56. package/scripts/fig_datasci.py +276 -0
  57. package/scripts/fig_distribution.py +59 -0
  58. package/scripts/fig_engineering.py +224 -0
  59. package/scripts/fig_environ.py +239 -0
  60. package/scripts/fig_errorbar.py +49 -0
  61. package/scripts/fig_forest.py +54 -0
  62. package/scripts/fig_heatmap.py +51 -0
  63. package/scripts/fig_line.py +39 -0
  64. package/scripts/fig_pca.py +101 -0
  65. package/scripts/fig_pie.py +52 -0
  66. package/scripts/fig_radar.py +32 -0
  67. package/scripts/fig_sankey.py +196 -0
  68. package/scripts/fig_scatter.py +44 -0
  69. package/scripts/fig_subplots.py +124 -0
  70. package/scripts/fig_taylor.py +80 -0
  71. package/scripts/fig_venn.py +80 -0
  72. package/scripts/fig_windrose.py +82 -0
  73. package/scripts/font_detector.py +85 -0
  74. package/scripts/gen_figure.py +583 -0
  75. package/scripts/generate_samples.py +322 -0
  76. package/scripts/palette_picker.py +244 -0
  77. package/scripts/palette_pref.py +179 -0
  78. package/scripts/palette_preview.py +334 -0
  79. package/scripts/palettes.py +461 -0
  80. package/scripts/registry.py +138 -0
  81. package/scripts/render_utils.py +233 -0
  82. package/scripts/showcase.py +179 -0
  83. 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()