Skip to content

Commit 5e7494a

Browse files
authored
fix static image generation to use --dpi (#425)
* fix plot.py to work with kaleido v1 * use plotly for chrome depend * fix static image issues * cleanup NanoPlot.py
1 parent c1d01c2 commit 5e7494a

24 files changed

+679
-48
lines changed
173 Bytes
Binary file not shown.
11.4 KB
Binary file not shown.

nanoplot/NanoPlot.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313

1414
from os import path
1515
import logging
16+
import sys
1617
import nanoplot.utils as utils
1718
from nanoplot.version import __version__
18-
import sys
19+
from nanoplotter.plot import Plot
1920

2021

2122
def main():
@@ -26,11 +27,13 @@ def main():
2627
-calls plotting function
2728
"""
2829
settings, args = utils.get_args()
30+
# Thread CLI --dpi into settings so static export honors it
31+
if hasattr(args, "dpi") and args.dpi:
32+
settings["dpi"] = int(args.dpi)
33+
import nanoplot.report as report
34+
from nanoget import get_input
35+
from nanoplot.filteroptions import filter_and_transform_data
2936
try:
30-
import nanoplot.report as report
31-
from nanoget import get_input
32-
from nanoplot.filteroptions import filter_and_transform_data
33-
from nanoplotter.plot import Plot
3437
utils.make_output_dir(args.outdir)
3538
import pickle
3639
utils.init_logs(args)
@@ -74,7 +77,8 @@ def main():
7477

7578
settings["statsfile"] = [make_stats(datadf, settings, suffix="", tsv_stats=args.tsv_stats)]
7679
datadf, settings = filter_and_transform_data(datadf, settings)
77-
if settings["filtered"]: # Bool set when filter was applied in filter_and_transform_data()
80+
# Bool set when filter was applied in filter_and_transform_data()
81+
if settings["filtered"]:
7882
settings["statsfile"].append(
7983
make_stats(
8084
datadf[datadf["length_filter"]],
@@ -85,8 +89,7 @@ def main():
8589
)
8690

8791
if args.only_report:
88-
Plot.only_report = True
89-
92+
Plot.only_report = True
9093
if args.barcoded:
9194
main_path = settings["path"]
9295
for barc in list(datadf["barcode"].unique()):
@@ -382,7 +385,7 @@ def make_report(plots, settings):
382385
which is parsed to a table (rather dodgy) or nicely if it's a pandas/tsv
383386
"""
384387
logging.info("Writing html report.")
385-
from nanoplot import report
388+
import nanoplot.report as report
386389

387390
html_content = [
388391
'<body class="grid">',
@@ -397,4 +400,4 @@ def make_report(plots, settings):
397400

398401

399402
if __name__ == "__main__":
400-
main()
403+
main()

nanoplot/utils.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from nanoplot.version import __version__
77
from argparse import HelpFormatter, Action, ArgumentParser
88
import textwrap as _textwrap
9+
import plotly.io as pio
910

1011

1112
class CustomHelpFormatter(HelpFormatter):
@@ -229,7 +230,8 @@ def get_args():
229230
visual.add_argument(
230231
"--font_scale", help="Scale the font of the plots by a factor", type=float, default=1
231232
)
232-
visual.add_argument("--dpi", help="Set the dpi for saving images", type=int, default=100)
233+
# CHANGED: default dpi to 300
234+
visual.add_argument("--dpi", help="Set the dpi for saving images", type=int, default=300)
233235
visual.add_argument(
234236
"--hide_stats",
235237
help="Not adding Pearson R stats in some bivariate plots",
@@ -368,3 +370,20 @@ def subsample_datasets(df, minimal=10000):
368370
subsampled_df = df.sample(minimal)
369371

370372
return subsampled_df
373+
374+
375+
# NEW: DPI-aware Plotly static export helper
376+
def write_static_image(fig, outpath, dpi=300, default_inches=(6.4, 4.8)):
377+
width_px = int(default_inches[0] * dpi)
378+
height_px = int(default_inches[1] * dpi)
379+
380+
pio.write_image(fig, outpath, width=width_px, height=height_px, scale=1)
381+
382+
lower = outpath.lower()
383+
if lower.endswith((".png", ".jpg", ".jpeg", ".tif", ".tiff", ".webp")):
384+
try:
385+
from PIL import Image
386+
im = Image.open(outpath)
387+
im.save(outpath, dpi=(dpi, dpi))
388+
except Exception as e:
389+
logging.warning("Could not set DPI metadata for %s: %s", outpath, e)

nanoplotter/plot.py

Lines changed: 82 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,32 @@
55
import sys
66
import logging
77

8-
# Use Plotly's Chrome bootstrapper instead of kaleido
98
import plotly.io as pio
109
try:
11-
# This should find or install a compatible Chrome in a user-writable location
1210
pio.get_chrome()
1311
except Exception as e:
1412
logging.warning(
1513
"Plotly could not fetch or find Chrome automatically. "
1614
"Static exports may fail unless BROWSER_PATH is set. Details: %s", e
1715
)
16+
17+
# DPI-aware writer
18+
try:
19+
from nanoplot.utils import write_static_image
20+
except Exception as e:
21+
logging.warning("Could not import write_static_image from nanoplot.utils: %s", e)
22+
write_static_image = None
23+
1824

1925
class Plot(object):
2026
"""A Plot object is defined by a path to the output file and the title of the plot."""
2127

2228
only_report = False
23-
29+
2430
def __init__(self, path, title):
2531
self.path = path
2632
self.title = title
27-
self.fig = None
33+
self.fig = None # Plotly fig for HTML/static; Matplotlib Figure in legacy mode
2834
self.html = None
2935

3036
def encode(self):
@@ -40,60 +46,99 @@ def encode1(self):
4046
return '<img src="data:image/png;base64,{0}">'.format(data_uri)
4147

4248
def encode2(self):
49+
# Legacy Matplotlib path only
4350
buf = BytesIO()
4451
self.fig.savefig(buf, format="png", bbox_inches="tight", dpi=100)
4552
buf.seek(0)
4653
string = b64encode(buf.read())
4754
return '<img src="data:image/png;base64,{0}">'.format(urlquote(string))
4855

4956
def save(self, settings):
50-
if not self.only_report:
51-
if self.html:
52-
with open(self.path, "w") as html_out:
53-
html_out.write(self.html)
54-
if not settings["no_static"]:
55-
try:
56-
for fmt in settings["format"]:
57-
self.save_static(fmt)
58-
except (AttributeError, ValueError) as e:
59-
p = os.path.splitext(self.path)[0] + ".png"
60-
if os.path.exists(p):
61-
os.remove(p)
62-
logging.warning("No static plots are saved due to an export problem:")
63-
logging.warning(e)
64-
65-
elif self.fig:
66-
if isinstance(settings["format"], list):
67-
for fmt in settings["format"]:
68-
self.fig.savefig(
69-
fname=self.path + "." + fmt,
70-
format=fmt,
71-
bbox_inches="tight",
72-
)
73-
else:
57+
if self.only_report:
58+
return
59+
60+
if self.html:
61+
# Save the interactive HTML
62+
with open(self.path, "w") as html_out:
63+
html_out.write(self.html)
64+
65+
# Also save static images unless suppressed
66+
if not settings.get("no_static", False):
67+
try:
68+
fmts = settings.get("format", ["png"])
69+
for fmt in fmts if isinstance(fmts, list) else [fmts]:
70+
self.save_static(fmt, settings) # pass settings
71+
except (AttributeError, ValueError) as e:
72+
p = os.path.splitext(self.path)[0] + ".png"
73+
if os.path.exists(p):
74+
os.remove(p)
75+
logging.warning("No static plots are saved due to an export problem:")
76+
logging.warning(e)
77+
78+
elif self.fig:
79+
# Legacy Matplotlib path
80+
fmts = settings.get("format", ["png"])
81+
dpi = int(settings.get("dpi", 300))
82+
if isinstance(fmts, list):
83+
for fmt in fmts:
7484
self.fig.savefig(
75-
fname=self.path,
76-
format=settings["format"],
85+
fname=self.path + "." + fmt,
86+
format=fmt,
7787
bbox_inches="tight",
88+
dpi=dpi,
7889
)
7990
else:
80-
sys.exit("No method to save plot object: no html or fig defined.")
91+
self.fig.savefig(
92+
fname=self.path,
93+
format=fmts,
94+
bbox_inches="tight",
95+
dpi=dpi,
96+
)
97+
else:
98+
sys.exit("No method to save plot object: no html or fig defined.")
8199

82100
def show(self):
83101
if self.fig:
84-
return self.fig.fig
102+
return getattr(self.fig, "fig", self.fig)
85103
else:
86104
sys.stderr.write(".show not implemented for Plot instance without fig attribute!")
87105

88-
def save_static(self, figformat):
106+
def save_static(self, figformat, settings):
89107
"""
90-
Export a Plotly figure using Plotly's image writer.
108+
Export a Plotly figure as a static image with real DPI.
109+
Prefers utils.write_static_image; falls back to explicit pixel size.
91110
"""
92111
output_path = self.path.replace(".html", f".{figformat}")
112+
dpi = int(settings.get("dpi", 300))
113+
114+
if self.fig is None:
115+
logging.warning("No figure attached to Plot; skipping static export for %s", output_path)
116+
return
117+
118+
# JSON just dumps the figure spec
119+
if figformat.lower() == "json":
120+
try:
121+
pio.write_json(self.fig, output_path)
122+
logging.info("Saved %s as JSON", output_path)
123+
except Exception as e:
124+
logging.warning("Failed to write JSON for %s: %s", output_path, e)
125+
return
126+
127+
# Preferred path: DPI-aware helper
93128
try:
94-
pio.write_image(self.fig, output_path, format=figformat)
95-
logging.info(f"Saved {output_path} as {figformat}")
129+
if write_static_image is not None:
130+
write_static_image(self.fig, output_path, dpi=dpi)
131+
logging.info("Saved %s as %s (dpi=%d)", output_path, figformat, dpi)
132+
return
133+
except Exception as e:
134+
logging.warning("DPI helper failed for %s: %s; falling back to explicit px size", output_path, e)
135+
136+
# Hard fallback so we don't end up at 700x500 defaults
137+
width_px = int(6.4 * dpi)
138+
height_px = int(4.8 * dpi)
139+
try:
140+
pio.write_image(self.fig, output_path, width=width_px, height=height_px, scale=1)
141+
logging.info("Fallback saved %s at %dx%d px", output_path, width_px, height_px)
96142
except Exception as e:
97143
logging.warning("No static plots are saved due to an export problem:")
98144
logging.warning(e)
99-

scripts/agm_test.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#! /bin/bash
2+
3+
#SBATCH --time=04-00:00:00
4+
#SBATCH --partition=defq
5+
#SBATCH --mail-user=email@email.org
6+
#SBATCH --mail-type=BEGIN,END,FAIL
7+
#SBATCH --ntasks-per-node=64
8+
#SBATCH --mem=128GB
9+
#SBATCH --nodes=1
10+
#SBATCH --job-name=nplot
11+
#SBATCH --comment=nplot
12+
13+
source /home/tmhagm8/scratch/nanoplot_env/bin/activate
14+
15+
# Go to the repo root
16+
cd /home/tmhagm8/scratch/NanoPlot
17+
18+
# Make sure to use right Python imports
19+
export PYTHONPATH="$PWD:$PYTHONPATH"
20+
21+
# Double check imports
22+
python - <<'PY'
23+
import nanoplotter.plot as p
24+
import nanoplot.utils as u
25+
print("USING nanoplotter.plot:", p.__file__)
26+
print("USING nanoplot.utils :", u.__file__)
27+
PY
28+
29+
# check it
30+
python -m nanoplot.NanoPlot \
31+
--fastq /home/tmhagm8/scratch/SOMAteM_bckp/SOMAteM/examples/data/B011_2.fastq.gz \
32+
-t 14 --verbose --minqual 4 --dpi 600 \
33+
-o /home/tmhagm8/scratch/NanoPlot/scripts/agm_test -f png

scripts/agm_tests/LengthvsQualityScatterPlot_dot.html

Lines changed: 2 additions & 0 deletions
Large diffs are not rendered by default.
438 KB
Loading

scripts/agm_tests/LengthvsQualityScatterPlot_kde.html

Lines changed: 2 additions & 0 deletions
Large diffs are not rendered by default.
564 KB
Loading

0 commit comments

Comments
 (0)