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,233 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared rendering utilities for palette_picker.py and showcase.py.
|
|
3
|
+
|
|
4
|
+
Why this exists
|
|
5
|
+
---------------
|
|
6
|
+
Before this file existed, palette_picker.py and showcase.py each had a copy
|
|
7
|
+
of:
|
|
8
|
+
|
|
9
|
+
_p() path join helper
|
|
10
|
+
_load_fonts() CJK font detection with the same candidate list
|
|
11
|
+
_render_chart() subprocess wrapper that calls gen_figure.py
|
|
12
|
+
compose_grid() Pillow canvas + header + cell-paste pattern
|
|
13
|
+
|
|
14
|
+
That was ~200 lines of duplicated code. This module is the single home for
|
|
15
|
+
those utilities, so both poster scripts can focus on what they actually do
|
|
16
|
+
(choosing which charts to render, arranging the grid) rather than re-
|
|
17
|
+
implementing the rendering plumbing.
|
|
18
|
+
|
|
19
|
+
Public API
|
|
20
|
+
----------
|
|
21
|
+
join(*parts) -> absolute path
|
|
22
|
+
load_fonts() -> (dict[size->Font], used_path)
|
|
23
|
+
render_chart(...) -> (out_path, error, elapsed_seconds)
|
|
24
|
+
compose_grid(...) -> canvas Image (composes thumbnails into a grid)
|
|
25
|
+
"""
|
|
26
|
+
import os
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
32
|
+
SKILL_DIR = os.path.dirname(SCRIPT_DIR)
|
|
33
|
+
GEN = os.path.join(SCRIPT_DIR, "gen_figure.py")
|
|
34
|
+
PYTHON = sys.executable
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def join(*parts):
|
|
38
|
+
"""Join path parts and return the absolute path."""
|
|
39
|
+
return os.path.abspath(os.path.join(*[str(p) for p in parts]))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_fonts(sizes=(30, 26, 20, 17, 14, 12, 10)):
|
|
43
|
+
"""Load CJK-capable fonts at several sizes.
|
|
44
|
+
|
|
45
|
+
Returns (fonts_dict, used_path) where fonts_dict maps size -> ImageFont.
|
|
46
|
+
Falls back to the system default font if no CJK-capable file is found.
|
|
47
|
+
"""
|
|
48
|
+
from PIL import ImageFont
|
|
49
|
+
candidates = []
|
|
50
|
+
try:
|
|
51
|
+
from font_detector import detect_cjk_font
|
|
52
|
+
det = detect_cjk_font()
|
|
53
|
+
if det:
|
|
54
|
+
candidates.append(det)
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
candidates += [
|
|
58
|
+
os.environ.get("FONT_PATH", ""),
|
|
59
|
+
r"C:/Windows/Fonts/msyh.ttc",
|
|
60
|
+
r"C:/Windows/Fonts/msyh.ttf",
|
|
61
|
+
r"C:/Windows/Fonts/simsun.ttc",
|
|
62
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
63
|
+
]
|
|
64
|
+
used = None
|
|
65
|
+
fonts = {}
|
|
66
|
+
for size in sizes:
|
|
67
|
+
f = None
|
|
68
|
+
for path in candidates:
|
|
69
|
+
if path and os.path.exists(path):
|
|
70
|
+
try:
|
|
71
|
+
f = ImageFont.truetype(path, size)
|
|
72
|
+
except Exception:
|
|
73
|
+
continue
|
|
74
|
+
if f is not None:
|
|
75
|
+
break
|
|
76
|
+
fonts[size] = f
|
|
77
|
+
if used is None and f is not None:
|
|
78
|
+
used = getattr(f, "path", None)
|
|
79
|
+
return fonts, used
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def render_chart(chart_type, data_path, out_path, style="nature",
|
|
83
|
+
palette=None, width=3.2, height=2.4, dpi=120,
|
|
84
|
+
timeout=120, extra_args=None):
|
|
85
|
+
"""Render one chart via gen_figure.py.
|
|
86
|
+
|
|
87
|
+
Returns (out_path_or_None, error_or_None, elapsed_seconds).
|
|
88
|
+
out_path is None (and error set) if the render failed.
|
|
89
|
+
"""
|
|
90
|
+
t0 = time.time()
|
|
91
|
+
cmd = [PYTHON, GEN,
|
|
92
|
+
"--type", chart_type,
|
|
93
|
+
"--data", data_path,
|
|
94
|
+
"--out", out_path,
|
|
95
|
+
"--style", style,
|
|
96
|
+
"--width", str(width),
|
|
97
|
+
"--height", str(height),
|
|
98
|
+
"--dpi", str(dpi)]
|
|
99
|
+
if palette:
|
|
100
|
+
cmd += ["--palette", palette]
|
|
101
|
+
if extra_args:
|
|
102
|
+
cmd += list(extra_args)
|
|
103
|
+
try:
|
|
104
|
+
proc = subprocess.run(cmd, capture_output=True, text=True,
|
|
105
|
+
timeout=timeout)
|
|
106
|
+
dt = time.time() - t0
|
|
107
|
+
if proc.returncode != 0 or not os.path.exists(out_path) \
|
|
108
|
+
or os.path.getsize(out_path) == 0:
|
|
109
|
+
return None, (proc.stderr or "").strip()[-300:], dt
|
|
110
|
+
return out_path, None, dt
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
return None, str(exc), time.time() - t0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def compose_grid(thumbnails, chart_order, header_text, subtitle_text,
|
|
116
|
+
cols, cell_w=360, cell_h=280, label_h=28,
|
|
117
|
+
gap=10, pad=16, header_h=72, footer_text=None,
|
|
118
|
+
author_tag="LIKE Sci Figures"):
|
|
119
|
+
"""Compose a grid of chart thumbnails into a single poster canvas.
|
|
120
|
+
|
|
121
|
+
thumbnails: dict[chart_type -> png_path]
|
|
122
|
+
chart_order: ordered list of chart types (determines grid layout)
|
|
123
|
+
header_text / subtitle_text: lines for the green header band
|
|
124
|
+
cols: number of columns (rows computed from len(chart_order))
|
|
125
|
+
author_tag: text displayed at the bottom-right corner of the header (e.g. "LIKE")
|
|
126
|
+
footer_text: optional line at the bottom
|
|
127
|
+
|
|
128
|
+
Returns the composed PIL Image (caller is responsible for .save()).
|
|
129
|
+
"""
|
|
130
|
+
import math
|
|
131
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
132
|
+
|
|
133
|
+
n = len(chart_order)
|
|
134
|
+
rows = max(1, int(math.ceil(n / cols)))
|
|
135
|
+
|
|
136
|
+
width = cols * cell_w + (cols - 1) * gap + 2 * pad
|
|
137
|
+
height = (header_h + rows * (cell_h + label_h)
|
|
138
|
+
+ (rows - 1) * gap + 2 * pad
|
|
139
|
+
+ (40 if footer_text else 0))
|
|
140
|
+
|
|
141
|
+
canvas = Image.new("RGB", (width, height), "white")
|
|
142
|
+
d = ImageDraw.Draw(canvas)
|
|
143
|
+
fonts, font_used = load_fonts((22, 18, 14, 12, 10))
|
|
144
|
+
f22 = fonts.get(22) or ImageFont.load_default()
|
|
145
|
+
f18 = fonts.get(18) or ImageFont.load_default()
|
|
146
|
+
f14 = fonts.get(14) or ImageFont.load_default()
|
|
147
|
+
f12 = fonts.get(12) or ImageFont.load_default()
|
|
148
|
+
|
|
149
|
+
# header
|
|
150
|
+
d.rectangle([0, 0, width, header_h], fill="#0B6E4F")
|
|
151
|
+
d.text((pad + 2, 10), header_text, fill="white", font=f22)
|
|
152
|
+
d.text((pad + 2, 42), subtitle_text, fill="#D7E1EB", font=f14)
|
|
153
|
+
|
|
154
|
+
# author tag at the bottom-right corner of the header band
|
|
155
|
+
if author_tag:
|
|
156
|
+
bbox = d.textbbox((0, 0), author_tag, font=f18)
|
|
157
|
+
tag_w = bbox[2] - bbox[0]
|
|
158
|
+
d.text((width - pad - tag_w - 2, header_h - 28),
|
|
159
|
+
author_tag, fill="#D7E1EB", font=f18)
|
|
160
|
+
|
|
161
|
+
# cells
|
|
162
|
+
for idx, chart_type in enumerate(chart_order):
|
|
163
|
+
col = idx % cols
|
|
164
|
+
row = idx // cols
|
|
165
|
+
x = pad + col * (cell_w + gap)
|
|
166
|
+
y = header_h + pad + row * (cell_h + label_h + gap)
|
|
167
|
+
|
|
168
|
+
d.rectangle([x, y, x + cell_w, y + cell_h],
|
|
169
|
+
fill="white", outline="#DDDDDD")
|
|
170
|
+
png = thumbnails.get(chart_type)
|
|
171
|
+
if png and os.path.exists(png):
|
|
172
|
+
im = Image.open(png).convert("RGB")
|
|
173
|
+
if im.width != cell_w or im.height != cell_h:
|
|
174
|
+
im = im.resize((cell_w, cell_h), Image.LANCZOS)
|
|
175
|
+
canvas.paste(im, (x, y))
|
|
176
|
+
else:
|
|
177
|
+
d.text((x + 10, y + cell_h // 2 - 10), "渲染失败",
|
|
178
|
+
fill="#999999", font=f14)
|
|
179
|
+
|
|
180
|
+
from registry import ZH_NAME
|
|
181
|
+
label = "%s (%s)" % (ZH_NAME.get(chart_type, chart_type),
|
|
182
|
+
chart_type)
|
|
183
|
+
d.text((x + 4, y + cell_h + 4), label, fill="#111111", font=f14)
|
|
184
|
+
|
|
185
|
+
if footer_text:
|
|
186
|
+
d.text((pad, height - 30), footer_text, fill="#555555", font=f12)
|
|
187
|
+
|
|
188
|
+
return canvas
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ── palette_pref integration ───────────────────────────────────────────────
|
|
192
|
+
STATE_REL = os.path.join(".workbuddy", "like-sci-figures", "palette.json")
|
|
193
|
+
AUTO_SENTINEL = "auto"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def read_palette_state(state_path=None, cwd=None):
|
|
197
|
+
"""Read the task-scoped palette preference, if any.
|
|
198
|
+
|
|
199
|
+
Returns (palette_slug_or_None, state_file_path). palette is None when no
|
|
200
|
+
record exists; the string "auto" when the user explicitly opted out.
|
|
201
|
+
"""
|
|
202
|
+
import json
|
|
203
|
+
if state_path is None:
|
|
204
|
+
state_path = os.path.join(cwd or os.getcwd(), STATE_REL)
|
|
205
|
+
state_path = os.path.abspath(state_path)
|
|
206
|
+
if not os.path.exists(state_path):
|
|
207
|
+
return None, state_path
|
|
208
|
+
try:
|
|
209
|
+
with open(state_path, "r", encoding="utf-8") as fh:
|
|
210
|
+
rec = json.load(fh)
|
|
211
|
+
value = (rec or {}).get("palette")
|
|
212
|
+
return (value if value else None), state_path
|
|
213
|
+
except Exception:
|
|
214
|
+
return None, state_path
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def write_palette_state(slug, state_path=None, cwd=None):
|
|
218
|
+
"""Persist the user's palette choice for the current task."""
|
|
219
|
+
import json
|
|
220
|
+
import time
|
|
221
|
+
if state_path is None:
|
|
222
|
+
state_path = os.path.join(cwd or os.getcwd(), STATE_REL)
|
|
223
|
+
state_path = os.path.abspath(state_path)
|
|
224
|
+
os.makedirs(os.path.dirname(state_path), exist_ok=True)
|
|
225
|
+
rec = {
|
|
226
|
+
"palette": slug,
|
|
227
|
+
"chosen_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
228
|
+
"cwd": os.getcwd(),
|
|
229
|
+
}
|
|
230
|
+
with open(state_path, "w", encoding="utf-8") as fh:
|
|
231
|
+
json.dump(rec, fh, ensure_ascii=False, indent=2)
|
|
232
|
+
fh.write("\n")
|
|
233
|
+
return state_path
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render every chart type with one palette + one style and stitch a showcase.
|
|
3
|
+
|
|
4
|
+
Why this exists
|
|
5
|
+
---------------
|
|
6
|
+
After the user picks a palette (via palette_picker.py) and a style, they want
|
|
7
|
+
to see what that combination looks like across *all* chart types — not just
|
|
8
|
+
the 5 representative ones in the picker poster. This script:
|
|
9
|
+
|
|
10
|
+
1. Renders each of the 36 chart types from examples/ with the chosen
|
|
11
|
+
palette and style (calling gen_figure.py for real).
|
|
12
|
+
2. Stitches the thumbnails into a single grid PNG.
|
|
13
|
+
3. Writes a manifest.json listing what was rendered, with per-chart
|
|
14
|
+
timings and output sizes.
|
|
15
|
+
|
|
16
|
+
The user can then present the grid to the user as a "here's what every chart
|
|
17
|
+
will look like with your choice" preview.
|
|
18
|
+
|
|
19
|
+
Usage
|
|
20
|
+
-----
|
|
21
|
+
showcase.py # defaults: --style nature
|
|
22
|
+
showcase.py --palette wx06_amber_navy --style ieee
|
|
23
|
+
showcase.py --palette p6 --style science --out outputs/my-showcase.png
|
|
24
|
+
showcase.py --dry-run # show plan, render nothing
|
|
25
|
+
|
|
26
|
+
Notes
|
|
27
|
+
-----
|
|
28
|
+
* The grid layout is auto-computed: ceil(sqrt(n)) columns for a near-square
|
|
29
|
+
grid, with per-chart Chinese labels.
|
|
30
|
+
* Requires Pillow. Thumbnails need only matplotlib (via gen_figure.py).
|
|
31
|
+
* Chart-type → example mapping lives in registry.py; shared rendering helpers
|
|
32
|
+
live in render_utils.py. This module only owns the showcase-specific layout.
|
|
33
|
+
"""
|
|
34
|
+
import argparse
|
|
35
|
+
import json
|
|
36
|
+
import math
|
|
37
|
+
import os
|
|
38
|
+
import sys
|
|
39
|
+
import tempfile
|
|
40
|
+
import time
|
|
41
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
42
|
+
|
|
43
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
44
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
45
|
+
|
|
46
|
+
from registry import CHART_EXAMPLES, example_path, ZH_NAME
|
|
47
|
+
from render_utils import render_chart, compose_grid
|
|
48
|
+
|
|
49
|
+
SKILL_DIR = os.path.dirname(SCRIPT_DIR)
|
|
50
|
+
DEFAULT_OUT = os.path.join(SKILL_DIR, "references", "showcase.png")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def generate(out_path, palette=None, style="nature", workers=4,
|
|
54
|
+
overwrite=False, dry_run=False):
|
|
55
|
+
"""Render all chart types and compose the showcase grid + manifest."""
|
|
56
|
+
chart_order = sorted(CHART_EXAMPLES)
|
|
57
|
+
|
|
58
|
+
if dry_run:
|
|
59
|
+
print("would render %d charts (%s)"
|
|
60
|
+
% (len(chart_order), ", ".join(chart_order)))
|
|
61
|
+
return 0
|
|
62
|
+
if out_path and os.path.exists(out_path) and not overwrite:
|
|
63
|
+
print("(showcase already exists, skipped — use --force to rebuild)")
|
|
64
|
+
print(out_path)
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
tmpdir = tempfile.mkdtemp(prefix="sf_showcase_")
|
|
68
|
+
thumbs = {}
|
|
69
|
+
manifest = {
|
|
70
|
+
"style": style,
|
|
71
|
+
"palette": palette,
|
|
72
|
+
"generated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
73
|
+
"charts": [],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def _do(args):
|
|
77
|
+
chart_type, data_path = args
|
|
78
|
+
png, err, dt = render_chart(
|
|
79
|
+
chart_type, data_path,
|
|
80
|
+
os.path.join(tmpdir, "%s.png" % chart_type),
|
|
81
|
+
style=style, palette=palette,
|
|
82
|
+
width=3.2, height=2.4, dpi=120)
|
|
83
|
+
return chart_type, png, err, dt
|
|
84
|
+
|
|
85
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
86
|
+
for chart_type, out_png, err, dt in pool.map(
|
|
87
|
+
_do,
|
|
88
|
+
[(ct, example_path(ct)) for ct in chart_order]
|
|
89
|
+
if all(example_path(ct) for ct in chart_order)
|
|
90
|
+
else [(ct, example_path(ct)) for ct in chart_order
|
|
91
|
+
if example_path(ct)]):
|
|
92
|
+
if out_png:
|
|
93
|
+
thumbs[chart_type] = out_png
|
|
94
|
+
manifest["charts"].append({
|
|
95
|
+
"type": chart_type,
|
|
96
|
+
"zh": ZH_NAME.get(chart_type, chart_type),
|
|
97
|
+
"example": CHART_EXAMPLES[chart_type],
|
|
98
|
+
"output": os.path.basename(out_png),
|
|
99
|
+
"bytes": os.path.getsize(out_png),
|
|
100
|
+
"seconds": round(dt, 2),
|
|
101
|
+
"status": "ok",
|
|
102
|
+
})
|
|
103
|
+
else:
|
|
104
|
+
manifest["charts"].append({
|
|
105
|
+
"type": chart_type,
|
|
106
|
+
"zh": ZH_NAME.get(chart_type, chart_type),
|
|
107
|
+
"example": CHART_EXAMPLES[chart_type],
|
|
108
|
+
"output": None,
|
|
109
|
+
"bytes": 0,
|
|
110
|
+
"seconds": round(dt, 2),
|
|
111
|
+
"status": "failed",
|
|
112
|
+
"error": err,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
# ── compose grid ─────────────────────────────────────────────────────
|
|
116
|
+
n = len(chart_order)
|
|
117
|
+
cols = max(1, int(math.ceil(math.sqrt(n))))
|
|
118
|
+
canvas = compose_grid(
|
|
119
|
+
thumbnails=thumbs,
|
|
120
|
+
chart_order=chart_order,
|
|
121
|
+
header_text="LIKE Sci Figures — Showcase · %s × %s"
|
|
122
|
+
% (style, palette or "(default)"),
|
|
123
|
+
subtitle_text="%d chart types · %s"
|
|
124
|
+
% (n, manifest["generated_at"]),
|
|
125
|
+
cols=cols,
|
|
126
|
+
cell_w=360, cell_h=280, label_h=28, gap=10, pad=16, header_h=72,
|
|
127
|
+
footer_text=None,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# ── save ─────────────────────────────────────────────────────────────
|
|
131
|
+
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
|
132
|
+
canvas.save(out_path, dpi=(160, 160))
|
|
133
|
+
|
|
134
|
+
manifest_path = os.path.splitext(out_path)[0] + ".manifest.json"
|
|
135
|
+
with open(manifest_path, "w", encoding="utf-8") as fh:
|
|
136
|
+
json.dump(manifest, fh, ensure_ascii=False, indent=2)
|
|
137
|
+
fh.write("\n")
|
|
138
|
+
|
|
139
|
+
ok_count = sum(1 for c in manifest["charts"] if c["status"] == "ok")
|
|
140
|
+
fail_count = sum(1 for c in manifest["charts"] if c["status"] == "failed")
|
|
141
|
+
if fail_count:
|
|
142
|
+
print("WARNING: %d chart(s) failed to render" % fail_count,
|
|
143
|
+
file=sys.stderr)
|
|
144
|
+
for c in manifest["charts"]:
|
|
145
|
+
if c["status"] == "failed":
|
|
146
|
+
print(" %s: %s" % (c["type"], c.get("error", "?")),
|
|
147
|
+
file=sys.stderr)
|
|
148
|
+
print("Showcase grid: %s (%d x %d px, %d bytes)"
|
|
149
|
+
% (out_path, canvas.width, canvas.height,
|
|
150
|
+
os.path.getsize(out_path)))
|
|
151
|
+
print("Manifest: %s" % manifest_path)
|
|
152
|
+
print("Charts rendered: %d ok, %d failed" % (ok_count, fail_count))
|
|
153
|
+
return 0 if fail_count == 0 else 1
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main():
|
|
157
|
+
ap = argparse.ArgumentParser(
|
|
158
|
+
prog="showcase.py",
|
|
159
|
+
description="Render every chart type with one palette + style into "
|
|
160
|
+
"a single grid poster, plus a manifest.json.")
|
|
161
|
+
ap.add_argument("--out", default=DEFAULT_OUT,
|
|
162
|
+
help="Grid PNG output (default: references/showcase.png)")
|
|
163
|
+
ap.add_argument("--palette", "-p", default=None,
|
|
164
|
+
help="Palette slug/alias to showcase (default: none)")
|
|
165
|
+
ap.add_argument("--style", "-s", default="nature",
|
|
166
|
+
help="Journal style (default: %(default)s)")
|
|
167
|
+
ap.add_argument("--workers", type=int, default=4, help="Parallel renders")
|
|
168
|
+
ap.add_argument("--force", action="store_true",
|
|
169
|
+
help="Rebuild even if the poster already exists")
|
|
170
|
+
ap.add_argument("--dry-run", action="store_true",
|
|
171
|
+
help="Print what would be rendered and exit")
|
|
172
|
+
args = ap.parse_args()
|
|
173
|
+
return generate(args.out, palette=args.palette, style=args.style,
|
|
174
|
+
workers=args.workers, overwrite=args.force,
|
|
175
|
+
dry_run=args.dry_run) or 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
sys.exit(main())
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Style manager for like-sci-figures skill.
|
|
4
|
+
Integrates scienceplots library with fallback built-in styles.
|
|
5
|
+
"""
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
# scienceplots style aliases map to their actual style names
|
|
10
|
+
SCIENCEPLOTS_STYLES = [
|
|
11
|
+
"ieee",
|
|
12
|
+
"nature",
|
|
13
|
+
"springer",
|
|
14
|
+
"aaas",
|
|
15
|
+
"science",
|
|
16
|
+
"lancet",
|
|
17
|
+
"acs",
|
|
18
|
+
"aps",
|
|
19
|
+
"cell",
|
|
20
|
+
"nejm",
|
|
21
|
+
"mdpi",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Built-in fallback styles that approximate scienceplots when it's not installed
|
|
25
|
+
# Each entry: (name, rc_params_dict)
|
|
26
|
+
BUILTIN_STYLES = {
|
|
27
|
+
"ieee": {
|
|
28
|
+
"figure.figsize": (6.5, 4.5),
|
|
29
|
+
"figure.dpi": 300,
|
|
30
|
+
"font.size": 8,
|
|
31
|
+
"font.family": "serif",
|
|
32
|
+
"axes.titlesize": 9,
|
|
33
|
+
"axes.labelsize": 8,
|
|
34
|
+
"axes.linewidth": 0.8,
|
|
35
|
+
"xtick.labelsize": 7,
|
|
36
|
+
"ytick.labelsize": 7,
|
|
37
|
+
"lines.linewidth": 1.2,
|
|
38
|
+
"grid.alpha": 0.3,
|
|
39
|
+
"grid.linestyle": "--",
|
|
40
|
+
},
|
|
41
|
+
"nature": {
|
|
42
|
+
"figure.figsize": (7, 5),
|
|
43
|
+
"figure.dpi": 300,
|
|
44
|
+
"font.size": 7,
|
|
45
|
+
"font.family": "sans-serif",
|
|
46
|
+
"axes.titlesize": 8,
|
|
47
|
+
"axes.labelsize": 7,
|
|
48
|
+
"axes.linewidth": 0.6,
|
|
49
|
+
"xtick.labelsize": 6,
|
|
50
|
+
"ytick.labelsize": 6,
|
|
51
|
+
"lines.linewidth": 1.0,
|
|
52
|
+
"grid.alpha": 0.2,
|
|
53
|
+
},
|
|
54
|
+
"springer": {
|
|
55
|
+
"figure.figsize": (7.8, 5.2),
|
|
56
|
+
"figure.dpi": 300,
|
|
57
|
+
"font.size": 8,
|
|
58
|
+
"font.family": "serif",
|
|
59
|
+
"axes.titlesize": 9,
|
|
60
|
+
"axes.labelsize": 8,
|
|
61
|
+
"axes.linewidth": 0.7,
|
|
62
|
+
"xtick.labelsize": 7,
|
|
63
|
+
"ytick.labelsize": 7,
|
|
64
|
+
"lines.linewidth": 1.1,
|
|
65
|
+
"grid.alpha": 0.3,
|
|
66
|
+
},
|
|
67
|
+
"aaas": {
|
|
68
|
+
"figure.figsize": (5.5, 4),
|
|
69
|
+
"figure.dpi": 300,
|
|
70
|
+
"font.size": 7,
|
|
71
|
+
"font.family": "sans-serif",
|
|
72
|
+
"axes.titlesize": 8,
|
|
73
|
+
"axes.labelsize": 7,
|
|
74
|
+
"axes.linewidth": 0.5,
|
|
75
|
+
"xtick.labelsize": 6,
|
|
76
|
+
"ytick.labelsize": 6,
|
|
77
|
+
"lines.linewidth": 1.0,
|
|
78
|
+
},
|
|
79
|
+
"science": {
|
|
80
|
+
"figure.figsize": (5.5, 4.5),
|
|
81
|
+
"figure.dpi": 300,
|
|
82
|
+
"font.size": 7,
|
|
83
|
+
"font.family": "sans-serif",
|
|
84
|
+
"axes.titlesize": 8,
|
|
85
|
+
"axes.labelsize": 7,
|
|
86
|
+
"axes.linewidth": 0.5,
|
|
87
|
+
"xtick.labelsize": 6,
|
|
88
|
+
"ytick.labelsize": 6,
|
|
89
|
+
"lines.linewidth": 1.0,
|
|
90
|
+
},
|
|
91
|
+
"lancet": {
|
|
92
|
+
"figure.figsize": (8, 5.5),
|
|
93
|
+
"figure.dpi": 300,
|
|
94
|
+
"font.size": 9,
|
|
95
|
+
"font.family": "serif",
|
|
96
|
+
"axes.titlesize": 10,
|
|
97
|
+
"axes.labelsize": 9,
|
|
98
|
+
"axes.linewidth": 0.8,
|
|
99
|
+
"xtick.labelsize": 8,
|
|
100
|
+
"ytick.labelsize": 8,
|
|
101
|
+
"lines.linewidth": 1.3,
|
|
102
|
+
},
|
|
103
|
+
"acs": {
|
|
104
|
+
"figure.figsize": (6, 4.5),
|
|
105
|
+
"figure.dpi": 300,
|
|
106
|
+
"font.size": 8,
|
|
107
|
+
"font.family": "sans-serif",
|
|
108
|
+
"axes.titlesize": 9,
|
|
109
|
+
"axes.labelsize": 8,
|
|
110
|
+
"axes.linewidth": 0.8,
|
|
111
|
+
"xtick.labelsize": 7,
|
|
112
|
+
"ytick.labelsize": 7,
|
|
113
|
+
"lines.linewidth": 1.2,
|
|
114
|
+
},
|
|
115
|
+
"aps": {
|
|
116
|
+
"figure.figsize": (6.5, 4.5),
|
|
117
|
+
"figure.dpi": 300,
|
|
118
|
+
"font.size": 8,
|
|
119
|
+
"font.family": "serif",
|
|
120
|
+
"axes.titlesize": 9,
|
|
121
|
+
"axes.labelsize": 8,
|
|
122
|
+
"axes.linewidth": 0.7,
|
|
123
|
+
"xtick.labelsize": 7,
|
|
124
|
+
"ytick.labelsize": 7,
|
|
125
|
+
"lines.linewidth": 1.1,
|
|
126
|
+
},
|
|
127
|
+
"cell": {
|
|
128
|
+
"figure.figsize": (7, 5),
|
|
129
|
+
"figure.dpi": 300,
|
|
130
|
+
"font.size": 8,
|
|
131
|
+
"font.family": "sans-serif",
|
|
132
|
+
"axes.titlesize": 9,
|
|
133
|
+
"axes.labelsize": 8,
|
|
134
|
+
"axes.linewidth": 0.8,
|
|
135
|
+
"xtick.labelsize": 7,
|
|
136
|
+
"ytick.labelsize": 7,
|
|
137
|
+
"lines.linewidth": 1.2,
|
|
138
|
+
"grid.alpha": 0.25,
|
|
139
|
+
},
|
|
140
|
+
"nejm": {
|
|
141
|
+
"figure.figsize": (6, 4.5),
|
|
142
|
+
"figure.dpi": 300,
|
|
143
|
+
"font.size": 8,
|
|
144
|
+
"font.family": "sans-serif",
|
|
145
|
+
"axes.titlesize": 9,
|
|
146
|
+
"axes.labelsize": 8,
|
|
147
|
+
"axes.linewidth": 0.8,
|
|
148
|
+
"xtick.labelsize": 7,
|
|
149
|
+
"ytick.labelsize": 7,
|
|
150
|
+
"lines.linewidth": 1.3,
|
|
151
|
+
"grid.alpha": 0.3,
|
|
152
|
+
},
|
|
153
|
+
"mdpi": {
|
|
154
|
+
"figure.figsize": (7.5, 5),
|
|
155
|
+
"figure.dpi": 300,
|
|
156
|
+
"font.size": 9,
|
|
157
|
+
"font.family": "sans-serif",
|
|
158
|
+
"axes.titlesize": 10,
|
|
159
|
+
"axes.labelsize": 9,
|
|
160
|
+
"axes.linewidth": 0.8,
|
|
161
|
+
"xtick.labelsize": 8,
|
|
162
|
+
"ytick.labelsize": 8,
|
|
163
|
+
"lines.linewidth": 1.3,
|
|
164
|
+
"grid.alpha": 0.3,
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Color palettes per journal style
|
|
169
|
+
COLOR_PALETTES = {
|
|
170
|
+
"ieee": ["#0072BD", "#D95319", "#EDB120", "#7E2F8E", "#77AC30", "#4DBEEE", "#A2142F"],
|
|
171
|
+
"nature": ["#E64B35", "#4DBBD5", "#00A087", "#3C5488", "#F39B7F", "#8491B4", "#91D1C2", "#DC0000"],
|
|
172
|
+
"springer": ["#00468B", "#ED0000", "#42B540", "#0099B4", "#925E9F", "#FDAF91", "#AD002A", "#1B1919"],
|
|
173
|
+
"aaas": ["#3B4992", "#EE0000", "#008B45", "#631879", "#008280", "#BB0021", "#5F559B", "#A20056"],
|
|
174
|
+
"science": ["#3B4992", "#EE0000", "#008B45", "#631879", "#008280", "#BB0021"],
|
|
175
|
+
"lancet": ["#00468B", "#ED0000", "#42B540", "#0099B4", "#525252", "#7F6F00", "#ED7D31"],
|
|
176
|
+
"acs": ["#E37222", "#07889B", "#66B9BF", "#EEAA7B", "#155265", "#E3A41B", "#7F7F7F"],
|
|
177
|
+
"aps": ["#0072B2", "#D55E00", "#009E73", "#CC79A7", "#F0E442", "#56B4E9"],
|
|
178
|
+
"cell": ["#3B4992", "#EE0000", "#008B45", "#631879", "#008280", "#BB0021", "#5F559B"],
|
|
179
|
+
"nejm": ["#1F77B4", "#D62728", "#2CA02C", "#9467BD", "#FF7F0E", "#17BECF", "#BCBD22"],
|
|
180
|
+
"mdpi": ["#E37222", "#07889B", "#66B9BF", "#EEAA7B", "#155265", "#E3A41B", "#7F7F7F"],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
DEFAULT_STYLE = "nature"
|
|
184
|
+
SCIENCEPLOTS_AVAILABLE = False
|
|
185
|
+
SCIENCEPLOTS_UNREACHABLE = False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _check_scienceplots():
|
|
189
|
+
"""Check if scienceplots is available and can be imported."""
|
|
190
|
+
global SCIENCEPLOTS_AVAILABLE, SCIENCEPLOTS_UNREACHABLE
|
|
191
|
+
if SCIENCEPLOTS_UNREACHABLE:
|
|
192
|
+
return False
|
|
193
|
+
try:
|
|
194
|
+
import scienceplots
|
|
195
|
+
SCIENCEPLOTS_AVAILABLE = True
|
|
196
|
+
return True
|
|
197
|
+
except ImportError:
|
|
198
|
+
SCIENCEPLOTS_AVAILABLE = False
|
|
199
|
+
SCIENCEPLOTS_UNREACHABLE = True
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def setup_style(style_name=DEFAULT_STYLE, custom_rc=None):
|
|
204
|
+
"""
|
|
205
|
+
Set up the matplotlib style for the given journal.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
style_name: One of the SCIENCEPLOTS_STYLES
|
|
209
|
+
custom_rc: Optional dict of rcParams overrides
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
dict with color palette and style metadata
|
|
213
|
+
"""
|
|
214
|
+
style_name = style_name.lower().strip()
|
|
215
|
+
if style_name not in SCIENCEPLOTS_STYLES and style_name not in BUILTIN_STYLES:
|
|
216
|
+
print(f"Warning: Unknown style '{style_name}', falling back to 'nature'")
|
|
217
|
+
style_name = "nature"
|
|
218
|
+
|
|
219
|
+
# scienceplots only provides 3 secondary styles: ieee, nature, science
|
|
220
|
+
_SCIENCEPLOTS_REAL_STYLES = {"ieee", "nature", "science"}
|
|
221
|
+
|
|
222
|
+
if _check_scienceplots():
|
|
223
|
+
with warnings.catch_warnings():
|
|
224
|
+
warnings.simplefilter("ignore")
|
|
225
|
+
if style_name in _SCIENCEPLOTS_REAL_STYLES:
|
|
226
|
+
plt.style.use(["science", style_name])
|
|
227
|
+
else:
|
|
228
|
+
# Use "science" base + our built-in rc params for unsupported styles
|
|
229
|
+
plt.style.use("science")
|
|
230
|
+
rc = BUILTIN_STYLES.get(style_name, BUILTIN_STYLES["nature"])
|
|
231
|
+
plt.rcParams.update(rc)
|
|
232
|
+
else:
|
|
233
|
+
rc = BUILTIN_STYLES.get(style_name, BUILTIN_STYLES["nature"])
|
|
234
|
+
plt.rcParams.update(rc)
|
|
235
|
+
|
|
236
|
+
# Always apply a few quality defaults
|
|
237
|
+
# Disable LaTeX (usetex) to avoid LaTeX dependency issues;
|
|
238
|
+
# matplotlib's built-in mathtext handles math rendering fine.
|
|
239
|
+
# Also provide sans-serif fallbacks for serif fonts (Windows lacks Times).
|
|
240
|
+
plt.rcParams.update({
|
|
241
|
+
"text.usetex": False,
|
|
242
|
+
"font.serif": ["DejaVu Serif", "Times New Roman", "Times", "serif"],
|
|
243
|
+
"savefig.dpi": 300,
|
|
244
|
+
"savefig.bbox": "tight",
|
|
245
|
+
"savefig.pad_inches": 0.05,
|
|
246
|
+
"axes.unicode_minus": False,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
# Apply any user overrides
|
|
250
|
+
if custom_rc:
|
|
251
|
+
plt.rcParams.update(custom_rc)
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"colors": COLOR_PALETTES.get(style_name, COLOR_PALETTES["nature"]),
|
|
255
|
+
"name": style_name,
|
|
256
|
+
"scienceplots_loaded": SCIENCEPLOTS_AVAILABLE,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def list_styles():
|
|
261
|
+
"""Return list of available style names."""
|
|
262
|
+
return SCIENCEPLOTS_STYLES[:]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def reset_style():
|
|
266
|
+
"""Reset matplotlib rcParams to defaults."""
|
|
267
|
+
plt.rcdefaults()
|