diff --git a/nanomath/__pycache__/__init__.cpython-310.pyc b/nanomath/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..13a8629
Binary files /dev/null and b/nanomath/__pycache__/__init__.cpython-310.pyc differ
diff --git a/nanomath/__pycache__/nanomath.cpython-310.pyc b/nanomath/__pycache__/nanomath.cpython-310.pyc
new file mode 100644
index 0000000..f172544
Binary files /dev/null and b/nanomath/__pycache__/nanomath.cpython-310.pyc differ
diff --git a/nanoplot/NanoPlot.py b/nanoplot/NanoPlot.py
index e7fb6c4..61acaae 100755
--- a/nanoplot/NanoPlot.py
+++ b/nanoplot/NanoPlot.py
@@ -13,9 +13,10 @@
from os import path
import logging
+import sys
import nanoplot.utils as utils
from nanoplot.version import __version__
-import sys
+from nanoplotter.plot import Plot
def main():
@@ -26,11 +27,13 @@ def main():
-calls plotting function
"""
settings, args = utils.get_args()
+ # Thread CLI --dpi into settings so static export honors it
+ if hasattr(args, "dpi") and args.dpi:
+ settings["dpi"] = int(args.dpi)
+ import nanoplot.report as report
+ from nanoget import get_input
+ from nanoplot.filteroptions import filter_and_transform_data
try:
- import nanoplot.report as report
- from nanoget import get_input
- from nanoplot.filteroptions import filter_and_transform_data
- from nanoplotter.plot import Plot
utils.make_output_dir(args.outdir)
import pickle
utils.init_logs(args)
@@ -74,7 +77,8 @@ def main():
settings["statsfile"] = [make_stats(datadf, settings, suffix="", tsv_stats=args.tsv_stats)]
datadf, settings = filter_and_transform_data(datadf, settings)
- if settings["filtered"]: # Bool set when filter was applied in filter_and_transform_data()
+ # Bool set when filter was applied in filter_and_transform_data()
+ if settings["filtered"]:
settings["statsfile"].append(
make_stats(
datadf[datadf["length_filter"]],
@@ -85,8 +89,7 @@ def main():
)
if args.only_report:
- Plot.only_report = True
-
+ Plot.only_report = True
if args.barcoded:
main_path = settings["path"]
for barc in list(datadf["barcode"].unique()):
@@ -382,7 +385,7 @@ def make_report(plots, settings):
which is parsed to a table (rather dodgy) or nicely if it's a pandas/tsv
"""
logging.info("Writing html report.")
- from nanoplot import report
+ import nanoplot.report as report
html_content = [
'
',
@@ -397,4 +400,4 @@ def make_report(plots, settings):
if __name__ == "__main__":
- main()
+ main()
\ No newline at end of file
diff --git a/nanoplot/utils.py b/nanoplot/utils.py
index 035e8f5..2eb5a1c 100644
--- a/nanoplot/utils.py
+++ b/nanoplot/utils.py
@@ -6,6 +6,7 @@
from nanoplot.version import __version__
from argparse import HelpFormatter, Action, ArgumentParser
import textwrap as _textwrap
+import plotly.io as pio
class CustomHelpFormatter(HelpFormatter):
@@ -229,7 +230,8 @@ def get_args():
visual.add_argument(
"--font_scale", help="Scale the font of the plots by a factor", type=float, default=1
)
- visual.add_argument("--dpi", help="Set the dpi for saving images", type=int, default=100)
+ # CHANGED: default dpi to 300
+ visual.add_argument("--dpi", help="Set the dpi for saving images", type=int, default=300)
visual.add_argument(
"--hide_stats",
help="Not adding Pearson R stats in some bivariate plots",
@@ -368,3 +370,20 @@ def subsample_datasets(df, minimal=10000):
subsampled_df = df.sample(minimal)
return subsampled_df
+
+
+# NEW: DPI-aware Plotly static export helper
+def write_static_image(fig, outpath, dpi=300, default_inches=(6.4, 4.8)):
+ width_px = int(default_inches[0] * dpi)
+ height_px = int(default_inches[1] * dpi)
+
+ pio.write_image(fig, outpath, width=width_px, height=height_px, scale=1)
+
+ lower = outpath.lower()
+ if lower.endswith((".png", ".jpg", ".jpeg", ".tif", ".tiff", ".webp")):
+ try:
+ from PIL import Image
+ im = Image.open(outpath)
+ im.save(outpath, dpi=(dpi, dpi))
+ except Exception as e:
+ logging.warning("Could not set DPI metadata for %s: %s", outpath, e)
diff --git a/nanoplotter/plot.py b/nanoplotter/plot.py
index 188ccaf..1f80a3b 100644
--- a/nanoplotter/plot.py
+++ b/nanoplotter/plot.py
@@ -5,26 +5,32 @@
import sys
import logging
-# Use Plotly's Chrome bootstrapper instead of kaleido
import plotly.io as pio
try:
- # This should find or install a compatible Chrome in a user-writable location
pio.get_chrome()
except Exception as e:
logging.warning(
"Plotly could not fetch or find Chrome automatically. "
"Static exports may fail unless BROWSER_PATH is set. Details: %s", e
)
+
+# DPI-aware writer
+try:
+ from nanoplot.utils import write_static_image
+except Exception as e:
+ logging.warning("Could not import write_static_image from nanoplot.utils: %s", e)
+ write_static_image = None
+
class Plot(object):
"""A Plot object is defined by a path to the output file and the title of the plot."""
only_report = False
-
+
def __init__(self, path, title):
self.path = path
self.title = title
- self.fig = None
+ self.fig = None # Plotly fig for HTML/static; Matplotlib Figure in legacy mode
self.html = None
def encode(self):
@@ -40,6 +46,7 @@ def encode1(self):
return ''.format(data_uri)
def encode2(self):
+ # Legacy Matplotlib path only
buf = BytesIO()
self.fig.savefig(buf, format="png", bbox_inches="tight", dpi=100)
buf.seek(0)
@@ -47,53 +54,91 @@ def encode2(self):
return ''.format(urlquote(string))
def save(self, settings):
- if not self.only_report:
- if self.html:
- with open(self.path, "w") as html_out:
- html_out.write(self.html)
- if not settings["no_static"]:
- try:
- for fmt in settings["format"]:
- self.save_static(fmt)
- except (AttributeError, ValueError) as e:
- p = os.path.splitext(self.path)[0] + ".png"
- if os.path.exists(p):
- os.remove(p)
- logging.warning("No static plots are saved due to an export problem:")
- logging.warning(e)
-
- elif self.fig:
- if isinstance(settings["format"], list):
- for fmt in settings["format"]:
- self.fig.savefig(
- fname=self.path + "." + fmt,
- format=fmt,
- bbox_inches="tight",
- )
- else:
+ if self.only_report:
+ return
+
+ if self.html:
+ # Save the interactive HTML
+ with open(self.path, "w") as html_out:
+ html_out.write(self.html)
+
+ # Also save static images unless suppressed
+ if not settings.get("no_static", False):
+ try:
+ fmts = settings.get("format", ["png"])
+ for fmt in fmts if isinstance(fmts, list) else [fmts]:
+ self.save_static(fmt, settings) # pass settings
+ except (AttributeError, ValueError) as e:
+ p = os.path.splitext(self.path)[0] + ".png"
+ if os.path.exists(p):
+ os.remove(p)
+ logging.warning("No static plots are saved due to an export problem:")
+ logging.warning(e)
+
+ elif self.fig:
+ # Legacy Matplotlib path
+ fmts = settings.get("format", ["png"])
+ dpi = int(settings.get("dpi", 300))
+ if isinstance(fmts, list):
+ for fmt in fmts:
self.fig.savefig(
- fname=self.path,
- format=settings["format"],
+ fname=self.path + "." + fmt,
+ format=fmt,
bbox_inches="tight",
+ dpi=dpi,
)
else:
- sys.exit("No method to save plot object: no html or fig defined.")
+ self.fig.savefig(
+ fname=self.path,
+ format=fmts,
+ bbox_inches="tight",
+ dpi=dpi,
+ )
+ else:
+ sys.exit("No method to save plot object: no html or fig defined.")
def show(self):
if self.fig:
- return self.fig.fig
+ return getattr(self.fig, "fig", self.fig)
else:
sys.stderr.write(".show not implemented for Plot instance without fig attribute!")
- def save_static(self, figformat):
+ def save_static(self, figformat, settings):
"""
- Export a Plotly figure using Plotly's image writer.
+ Export a Plotly figure as a static image with real DPI.
+ Prefers utils.write_static_image; falls back to explicit pixel size.
"""
output_path = self.path.replace(".html", f".{figformat}")
+ dpi = int(settings.get("dpi", 300))
+
+ if self.fig is None:
+ logging.warning("No figure attached to Plot; skipping static export for %s", output_path)
+ return
+
+ # JSON just dumps the figure spec
+ if figformat.lower() == "json":
+ try:
+ pio.write_json(self.fig, output_path)
+ logging.info("Saved %s as JSON", output_path)
+ except Exception as e:
+ logging.warning("Failed to write JSON for %s: %s", output_path, e)
+ return
+
+ # Preferred path: DPI-aware helper
try:
- pio.write_image(self.fig, output_path, format=figformat)
- logging.info(f"Saved {output_path} as {figformat}")
+ if write_static_image is not None:
+ write_static_image(self.fig, output_path, dpi=dpi)
+ logging.info("Saved %s as %s (dpi=%d)", output_path, figformat, dpi)
+ return
+ except Exception as e:
+ logging.warning("DPI helper failed for %s: %s; falling back to explicit px size", output_path, e)
+
+ # Hard fallback so we don't end up at 700x500 defaults
+ width_px = int(6.4 * dpi)
+ height_px = int(4.8 * dpi)
+ try:
+ pio.write_image(self.fig, output_path, width=width_px, height=height_px, scale=1)
+ logging.info("Fallback saved %s at %dx%d px", output_path, width_px, height_px)
except Exception as e:
logging.warning("No static plots are saved due to an export problem:")
logging.warning(e)
-
diff --git a/scripts/agm_test.sh b/scripts/agm_test.sh
new file mode 100644
index 0000000..a241295
--- /dev/null
+++ b/scripts/agm_test.sh
@@ -0,0 +1,33 @@
+#! /bin/bash
+
+#SBATCH --time=04-00:00:00
+#SBATCH --partition=defq
+#SBATCH --mail-user=email@email.org
+#SBATCH --mail-type=BEGIN,END,FAIL
+#SBATCH --ntasks-per-node=64
+#SBATCH --mem=128GB
+#SBATCH --nodes=1
+#SBATCH --job-name=nplot
+#SBATCH --comment=nplot
+
+source /home/tmhagm8/scratch/nanoplot_env/bin/activate
+
+# Go to the repo root
+cd /home/tmhagm8/scratch/NanoPlot
+
+# Make sure to use right Python imports
+export PYTHONPATH="$PWD:$PYTHONPATH"
+
+# Double check imports
+python - <<'PY'
+import nanoplotter.plot as p
+import nanoplot.utils as u
+print("USING nanoplotter.plot:", p.__file__)
+print("USING nanoplot.utils :", u.__file__)
+PY
+
+# check it
+python -m nanoplot.NanoPlot \
+ --fastq /home/tmhagm8/scratch/SOMAteM_bckp/SOMAteM/examples/data/B011_2.fastq.gz \
+ -t 14 --verbose --minqual 4 --dpi 600 \
+ -o /home/tmhagm8/scratch/NanoPlot/scripts/agm_test -f png
diff --git a/scripts/agm_tests/LengthvsQualityScatterPlot_dot.html b/scripts/agm_tests/LengthvsQualityScatterPlot_dot.html
new file mode 100644
index 0000000..03efae1
--- /dev/null
+++ b/scripts/agm_tests/LengthvsQualityScatterPlot_dot.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/scripts/agm_tests/LengthvsQualityScatterPlot_dot.png b/scripts/agm_tests/LengthvsQualityScatterPlot_dot.png
new file mode 100644
index 0000000..f43d3a2
Binary files /dev/null and b/scripts/agm_tests/LengthvsQualityScatterPlot_dot.png differ
diff --git a/scripts/agm_tests/LengthvsQualityScatterPlot_kde.html b/scripts/agm_tests/LengthvsQualityScatterPlot_kde.html
new file mode 100644
index 0000000..b40b563
--- /dev/null
+++ b/scripts/agm_tests/LengthvsQualityScatterPlot_kde.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/scripts/agm_tests/LengthvsQualityScatterPlot_kde.png b/scripts/agm_tests/LengthvsQualityScatterPlot_kde.png
new file mode 100644
index 0000000..2e0bee3
Binary files /dev/null and b/scripts/agm_tests/LengthvsQualityScatterPlot_kde.png differ
diff --git a/scripts/agm_tests/NanoPlot-report.html b/scripts/agm_tests/NanoPlot-report.html
new file mode 100644
index 0000000..6839338
--- /dev/null
+++ b/scripts/agm_tests/NanoPlot-report.html
@@ -0,0 +1,463 @@
+
+
+
+
+
+
+NanoPlot Report
+
+
+
NanoPlot statistics report
+
+
NanoPlot reports
+
Summary statistics prior to filtering
+
+
+
+
General summary
+
+
+
+
Mean read length
+
1,604.9
+
+
+
Mean read quality
+
16.1
+
+
+
Median read length
+
1,016.0
+
+
+
Median read quality
+
17.4
+
+
+
Number of reads
+
45,338.0
+
+
+
Read length N50
+
2,645.0
+
+
+
STDEV read length
+
1,624.0
+
+
+
Total bases
+
72,762,618.0
+
+
+
Number, percentage and megabases of reads above quality cutoffs
+
+
+
+
>Q10
+
45338 (100.0%) 72.8Mb
+
+
+
>Q15
+
33736 (74.4%) 58.1Mb
+
+
+
>Q20
+
9761 (21.5%) 18.2Mb
+
+
+
>Q25
+
825 (1.8%) 0.9Mb
+
+
+
>Q30
+
65 (0.1%) 0.0Mb
+
+
+
Top 5 highest mean basecall quality scores and their read lengths
+
+
+
+
1
+
37.2 (276)
+
+
+
2
+
37.1 (714)
+
+
+
3
+
36.2 (333)
+
+
+
4
+
35.9 (720)
+
+
+
5
+
35.8 (521)
+
+
+
Top 5 longest reads and their mean basecall quality score
+
+
+
+
1
+
35318 (18.4)
+
+
+
2
+
32010 (21.7)
+
+
+
3
+
25780 (20.0)
+
+
+
4
+
24951 (10.3)
+
+
+
5
+
21731 (17.8)
+
+
+
+
Summary statistics after filtering
+
+
+
+
General summary
+
+
+
+
Mean read length
+
1,604.9
+
+
+
Mean read quality
+
16.1
+
+
+
Median read length
+
1,016.0
+
+
+
Median read quality
+
17.4
+
+
+
Number of reads
+
45,338.0
+
+
+
Read length N50
+
2,645.0
+
+
+
STDEV read length
+
1,624.0
+
+
+
Total bases
+
72,762,618.0
+
+
+
Number, percentage and megabases of reads above quality cutoffs
+
+
+
+
>Q10
+
45338 (100.0%) 72.8Mb
+
+
+
>Q15
+
33736 (74.4%) 58.1Mb
+
+
+
>Q20
+
9761 (21.5%) 18.2Mb
+
+
+
>Q25
+
825 (1.8%) 0.9Mb
+
+
+
>Q30
+
65 (0.1%) 0.0Mb
+
+
+
Top 5 highest mean basecall quality scores and their read lengths
+
+
+
+
1
+
37.2 (276)
+
+
+
2
+
37.1 (714)
+
+
+
3
+
36.2 (333)
+
+
+
4
+
35.9 (720)
+
+
+
5
+
35.8 (521)
+
+
+
Top 5 longest reads and their mean basecall quality score
+
+
+
+
1
+
35318 (18.4)
+
+
+
2
+
32010 (21.7)
+
+
+
3
+
25780 (20.0)
+
+
+
4
+
24951 (10.3)
+
+
+
5
+
21731 (17.8)
+
+
+
+
Plots
+
+
Weighted histogram of read lengths
+
+
+
+
+
Weighted histogram of read lengths after log transformation
+
+
+
+
+
Non weighted histogram of read lengths
+
+
+
+
+
Non weighted histogram of read lengths after log transformation
+
+
+
+
+
Yield by length
+
+
+
+
+
Read lengths vs Average read quality plot using dots
\ No newline at end of file
diff --git a/scripts/agm_tests/Non_weightedHistogramReadlength.png b/scripts/agm_tests/Non_weightedHistogramReadlength.png
new file mode 100644
index 0000000..ffa1e69
Binary files /dev/null and b/scripts/agm_tests/Non_weightedHistogramReadlength.png differ
diff --git a/scripts/agm_tests/Non_weightedLogTransformed_HistogramReadlength.html b/scripts/agm_tests/Non_weightedLogTransformed_HistogramReadlength.html
new file mode 100644
index 0000000..0008bdb
--- /dev/null
+++ b/scripts/agm_tests/Non_weightedLogTransformed_HistogramReadlength.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/scripts/agm_tests/Non_weightedLogTransformed_HistogramReadlength.png b/scripts/agm_tests/Non_weightedLogTransformed_HistogramReadlength.png
new file mode 100644
index 0000000..1cdef65
Binary files /dev/null and b/scripts/agm_tests/Non_weightedLogTransformed_HistogramReadlength.png differ
diff --git a/scripts/agm_tests/WeightedHistogramReadlength.html b/scripts/agm_tests/WeightedHistogramReadlength.html
new file mode 100644
index 0000000..3ba9a64
--- /dev/null
+++ b/scripts/agm_tests/WeightedHistogramReadlength.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/scripts/agm_tests/WeightedHistogramReadlength.png b/scripts/agm_tests/WeightedHistogramReadlength.png
new file mode 100644
index 0000000..1214a0c
Binary files /dev/null and b/scripts/agm_tests/WeightedHistogramReadlength.png differ
diff --git a/scripts/agm_tests/WeightedLogTransformed_HistogramReadlength.html b/scripts/agm_tests/WeightedLogTransformed_HistogramReadlength.html
new file mode 100644
index 0000000..7719c76
--- /dev/null
+++ b/scripts/agm_tests/WeightedLogTransformed_HistogramReadlength.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/scripts/agm_tests/WeightedLogTransformed_HistogramReadlength.png b/scripts/agm_tests/WeightedLogTransformed_HistogramReadlength.png
new file mode 100644
index 0000000..2adb134
Binary files /dev/null and b/scripts/agm_tests/WeightedLogTransformed_HistogramReadlength.png differ
diff --git a/scripts/agm_tests/Yield_By_Length.html b/scripts/agm_tests/Yield_By_Length.html
new file mode 100644
index 0000000..ebf0519
--- /dev/null
+++ b/scripts/agm_tests/Yield_By_Length.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/scripts/agm_tests/Yield_By_Length.png b/scripts/agm_tests/Yield_By_Length.png
new file mode 100644
index 0000000..ce67199
Binary files /dev/null and b/scripts/agm_tests/Yield_By_Length.png differ