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 length1,604.9
Mean read quality16.1
Median read length1,016.0
Median read quality17.4
Number of reads45,338.0
Read length N502,645.0
STDEV read length1,624.0
Total bases72,762,618.0
Number, percentage and megabases of reads above quality cutoffs
>Q1045338 (100.0%) 72.8Mb
>Q1533736 (74.4%) 58.1Mb
>Q209761 (21.5%) 18.2Mb
>Q25825 (1.8%) 0.9Mb
>Q3065 (0.1%) 0.0Mb
Top 5 highest mean basecall quality scores and their read lengths
137.2 (276)
237.1 (714)
336.2 (333)
435.9 (720)
535.8 (521)
Top 5 longest reads and their mean basecall quality score
135318 (18.4)
232010 (21.7)
325780 (20.0)
424951 (10.3)
521731 (17.8)
+

Summary statistics after filtering

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
General summary
Mean read length1,604.9
Mean read quality16.1
Median read length1,016.0
Median read quality17.4
Number of reads45,338.0
Read length N502,645.0
STDEV read length1,624.0
Total bases72,762,618.0
Number, percentage and megabases of reads above quality cutoffs
>Q1045338 (100.0%) 72.8Mb
>Q1533736 (74.4%) 58.1Mb
>Q209761 (21.5%) 18.2Mb
>Q25825 (1.8%) 0.9Mb
>Q3065 (0.1%) 0.0Mb
Top 5 highest mean basecall quality scores and their read lengths
137.2 (276)
237.1 (714)
336.2 (333)
435.9 (720)
535.8 (521)
Top 5 longest reads and their mean basecall quality score
135318 (18.4)
232010 (21.7)
325780 (20.0)
424951 (10.3)
521731 (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

+
+
+
+ +

Read lengths vs Average read quality kde plot

+
+
+
+ + +
\ No newline at end of file diff --git a/scripts/agm_tests/NanoPlot_20250825_1425.log b/scripts/agm_tests/NanoPlot_20250825_1425.log new file mode 100644 index 0000000..e69de29 diff --git a/scripts/agm_tests/NanoStats.txt b/scripts/agm_tests/NanoStats.txt new file mode 100644 index 0000000..c804f6d --- /dev/null +++ b/scripts/agm_tests/NanoStats.txt @@ -0,0 +1,27 @@ +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) diff --git a/scripts/agm_tests/NanoStats_post_filtering.txt b/scripts/agm_tests/NanoStats_post_filtering.txt new file mode 100644 index 0000000..c804f6d --- /dev/null +++ b/scripts/agm_tests/NanoStats_post_filtering.txt @@ -0,0 +1,27 @@ +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) diff --git a/scripts/agm_tests/Non_weightedHistogramReadlength.html b/scripts/agm_tests/Non_weightedHistogramReadlength.html new file mode 100644 index 0000000..0b229ba --- /dev/null +++ b/scripts/agm_tests/Non_weightedHistogramReadlength.html @@ -0,0 +1,2 @@ +
+
\ 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