From ff0b7ac36bd81ef4102936b4bb835b0efd3b71cd Mon Sep 17 00:00:00 2001 From: Devon Ryan Date: Fri, 4 Sep 2026 17:52:30 +0200 Subject: [PATCH 1/3] Initial addition of an include-js option, to dictate how the plotly javascript should be included (useful in restricted-access scenarios) --- nanoplot/NanoPlot.py | 2 +- nanoplot/utils.py | 6 ++++++ nanoplotter/nanoplotter_main.py | 10 +++++----- nanoplotter/spatial_heatmap.py | 2 +- nanoplotter/timeplots.py | 14 +++++++------- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/nanoplot/NanoPlot.py b/nanoplot/NanoPlot.py index 36db0a1..ad6c36a 100755 --- a/nanoplot/NanoPlot.py +++ b/nanoplot/NanoPlot.py @@ -406,4 +406,4 @@ def make_report(plots, settings): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/nanoplot/utils.py b/nanoplot/utils.py index 2eb5a1c..c2a8536 100644 --- a/nanoplot/utils.py +++ b/nanoplot/utils.py @@ -110,6 +110,12 @@ def get_args(): general.add_argument( "--info_in_report", help="Add NanoPlot run info in the report.", action="store_true" ) + general.add_argument( + "--include-js", + choices=["cdn", "True"], + help="Either cdn or True. If cdn (the default) is specified, the javascript for plotly images will be sourced from the web. If True is specified, then the javascript will be directly put in the html file.", + default="cdn", + ) filtering = parser.add_argument_group( title="Options for filtering or transforming input prior to plotting" ) diff --git a/nanoplotter/nanoplotter_main.py b/nanoplotter/nanoplotter_main.py index a336a34..6c046dd 100644 --- a/nanoplotter/nanoplotter_main.py +++ b/nanoplotter/nanoplotter_main.py @@ -146,7 +146,7 @@ def scatter( ) dot_plot.fig = fig - dot_plot.html = dot_plot.fig.to_html(full_html=False, include_plotlyjs="cdn") + dot_plot.html = dot_plot.fig.to_html(full_html=False, include_plotlyjs=settings.include_js) dot_plot.save(settings) plots_made.append(dot_plot) @@ -177,7 +177,7 @@ def scatter( ) kde_plot.fig = fig - kde_plot.html = kde_plot.fig.to_html(full_html=False, include_plotlyjs="cdn") + kde_plot.html = kde_plot.fig.to_html(full_html=False, include_plotlyjs=settings.include_js) kde_plot.save(settings) plots_made.append(kde_plot) @@ -433,7 +433,7 @@ def length_plots(array, name, path, settings, title=None, n50=None, color="#4CB3 ) histogram.fig = fig - histogram.html = histogram.fig.to_html(full_html=False, include_plotlyjs="cdn") + histogram.html = histogram.fig.to_html(full_html=False, include_plotlyjs=settings.include_js) histogram.save(settings) log_histogram = Plot( @@ -484,7 +484,7 @@ def length_plots(array, name, path, settings, title=None, n50=None, color="#4CB3 fig.update_annotations(font_size=8) log_histogram.fig = fig - log_histogram.html = log_histogram.fig.to_html(full_html=False, include_plotlyjs="cdn") + log_histogram.html = log_histogram.fig.to_html(full_html=False, include_plotlyjs=settings.include_js) log_histogram.save(settings) plots.extend([histogram, log_histogram]) @@ -560,7 +560,7 @@ def yield_by_minimal_length_plot(array, name, path, settings, title=None, color= ) yield_by_length.fig = fig - yield_by_length.html = yield_by_length.fig.to_html(full_html=False, include_plotlyjs="cdn") + yield_by_length.html = yield_by_length.fig.to_html(full_html=False, include_plotlyjs=settings.include_js) yield_by_length.save(settings) return yield_by_length diff --git a/nanoplotter/spatial_heatmap.py b/nanoplotter/spatial_heatmap.py index d7cd991..1bc4a03 100644 --- a/nanoplotter/spatial_heatmap.py +++ b/nanoplotter/spatial_heatmap.py @@ -91,6 +91,6 @@ def spatial_heatmap(array, path, colormap, settings, title=None): activity_map.fig = fig activity_map.html = activity_map.fig.to_html( - full_html=False, include_plotlyjs='cdn') + full_html=False, include_plotlyjs=settings.include_js) activity_map.save(settings) return [activity_map] diff --git a/nanoplotter/timeplots.py b/nanoplotter/timeplots.py index 50f754b..13d945f 100644 --- a/nanoplotter/timeplots.py +++ b/nanoplotter/timeplots.py @@ -136,7 +136,7 @@ def length_over_time(dfs, path, title, settings, log_length=False, color="#4CB39 time_length.fig = fig time_length.html = time_length.fig.to_html( - full_html=False, include_plotlyjs='cdn') + full_html=False, include_plotlyjs=settings.include_js) time_length.save(settings) return time_length @@ -163,7 +163,7 @@ def quality_over_time(dfs, path, settings, title=None, color="#4CB391", downsamp time_qual.fig = fig time_qual.html = time_qual.fig.to_html( - full_html=False, include_plotlyjs='cdn') + full_html=False, include_plotlyjs=settings.include_js) time_qual.save(settings) return time_qual @@ -193,7 +193,7 @@ def sequencing_speed_over_time(dfs, path, title, settings, color="#4CB391", down time_duration.fig = fig time_duration.html = time_duration.fig.to_html( - full_html=False, include_plotlyjs='cdn') + full_html=False, include_plotlyjs=settings.include_js) time_duration.save(settings) return time_duration @@ -226,7 +226,7 @@ def plot_over_time(dfs, path, title, settings, color="#4CB391"): num_reads.fig = fig num_reads.html = num_reads.fig.to_html( - full_html=False, include_plotlyjs='cdn') + full_html=False, include_plotlyjs=settings.include_js) num_reads.save(settings) plots = [num_reads] @@ -249,7 +249,7 @@ def plot_over_time(dfs, path, title, settings, color="#4CB391"): pores_over_time.fig = fig pores_over_time.html = pores_over_time.fig.to_html( - full_html=False, include_plotlyjs='cdn') + full_html=False, include_plotlyjs=settings.include_js) pores_over_time.save(settings) plots.append(pores_over_time) @@ -274,7 +274,7 @@ def cumulative_yield(dfs, path, title, color, settings): cum_yield_gb.fig = fig cum_yield_gb.html = cum_yield_gb.fig.to_html( - full_html=False, include_plotlyjs='cdn') + full_html=False, include_plotlyjs=settings.include_js) cum_yield_gb.save(settings) cum_yield_reads = Plot(path=path + "CumulativeYieldPlot_NumberOfReads.html", @@ -294,7 +294,7 @@ def cumulative_yield(dfs, path, title, color, settings): cum_yield_reads.fig = fig cum_yield_reads.html = cum_yield_reads.fig.to_html( - full_html=False, include_plotlyjs='cdn') + full_html=False, include_plotlyjs=settings.include_js) cum_yield_reads.save(settings) return [cum_yield_gb, cum_yield_reads] From 546f2e87178a117cb10463e92284f7a49308a7e5 Mon Sep 17 00:00:00 2001 From: Devon Ryan Date: Sat, 5 Sep 2026 14:54:52 +0200 Subject: [PATCH 2/3] Working version of embedded javascript option --- nanoplot/NanoPlot.py | 3 +++ nanoplot/utils.py | 6 ++++-- nanoplotter/nanoplotter_main.py | 10 +++++----- nanoplotter/spatial_heatmap.py | 2 +- nanoplotter/timeplots.py | 12 ++++++------ 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/nanoplot/NanoPlot.py b/nanoplot/NanoPlot.py index ad6c36a..639619f 100755 --- a/nanoplot/NanoPlot.py +++ b/nanoplot/NanoPlot.py @@ -18,6 +18,7 @@ import nanoplot.utils as utils from nanoplot.version import __version__ from nanoplotter.plot import Plot +from plotly.offline import get_plotlyjs def main(): @@ -401,6 +402,8 @@ def make_report(plots, settings): report.run_info(settings) if settings["info_in_report"] else "", "", ] + if settings["include_js"] == False: + html_content.insert(0, f'') with open(settings["path"] + "NanoPlot-report.html", "w") as html_file: html_file.write(report.html_head + "\n".join(html_content)) diff --git a/nanoplot/utils.py b/nanoplot/utils.py index c2a8536..b70a6f6 100644 --- a/nanoplot/utils.py +++ b/nanoplot/utils.py @@ -112,8 +112,8 @@ def get_args(): ) general.add_argument( "--include-js", - choices=["cdn", "True"], - help="Either cdn or True. If cdn (the default) is specified, the javascript for plotly images will be sourced from the web. If True is specified, then the javascript will be directly put in the html file.", + choices=["cdn", "embedded"], + help="Either cdn or embedded. If cdn (the default) is specified, the javascript for plotly images will be sourced from the web. If embedded is specified, then the javascript will be directly put into the html file, resulting in a larger but self-contained HTML file.", default="cdn", ) filtering = parser.add_argument_group( @@ -304,6 +304,8 @@ def get_args(): sys.exit("ARGUMENT ERROR: --barcoded only works with data provided as --summary!") settings = vars(args) settings["path"] = os.path.join(args.outdir, args.prefix) + if settings["include_js"] == "embedded": + settings["include_js"] = False return settings, args diff --git a/nanoplotter/nanoplotter_main.py b/nanoplotter/nanoplotter_main.py index 6c046dd..1fe517f 100644 --- a/nanoplotter/nanoplotter_main.py +++ b/nanoplotter/nanoplotter_main.py @@ -146,7 +146,7 @@ def scatter( ) dot_plot.fig = fig - dot_plot.html = dot_plot.fig.to_html(full_html=False, include_plotlyjs=settings.include_js) + dot_plot.html = dot_plot.fig.to_html(full_html=False, include_plotlyjs=settings["include_js"]) dot_plot.save(settings) plots_made.append(dot_plot) @@ -177,7 +177,7 @@ def scatter( ) kde_plot.fig = fig - kde_plot.html = kde_plot.fig.to_html(full_html=False, include_plotlyjs=settings.include_js) + kde_plot.html = kde_plot.fig.to_html(full_html=False, include_plotlyjs=settings["include_js"]) kde_plot.save(settings) plots_made.append(kde_plot) @@ -433,7 +433,7 @@ def length_plots(array, name, path, settings, title=None, n50=None, color="#4CB3 ) histogram.fig = fig - histogram.html = histogram.fig.to_html(full_html=False, include_plotlyjs=settings.include_js) + histogram.html = histogram.fig.to_html(full_html=False, include_plotlyjs=settings["include_js"]) histogram.save(settings) log_histogram = Plot( @@ -484,7 +484,7 @@ def length_plots(array, name, path, settings, title=None, n50=None, color="#4CB3 fig.update_annotations(font_size=8) log_histogram.fig = fig - log_histogram.html = log_histogram.fig.to_html(full_html=False, include_plotlyjs=settings.include_js) + log_histogram.html = log_histogram.fig.to_html(full_html=False, include_plotlyjs=settings["include_js"]) log_histogram.save(settings) plots.extend([histogram, log_histogram]) @@ -560,7 +560,7 @@ def yield_by_minimal_length_plot(array, name, path, settings, title=None, color= ) yield_by_length.fig = fig - yield_by_length.html = yield_by_length.fig.to_html(full_html=False, include_plotlyjs=settings.include_js) + yield_by_length.html = yield_by_length.fig.to_html(full_html=False, include_plotlyjs=settings["include_js"]) yield_by_length.save(settings) return yield_by_length diff --git a/nanoplotter/spatial_heatmap.py b/nanoplotter/spatial_heatmap.py index 1bc4a03..de1b0eb 100644 --- a/nanoplotter/spatial_heatmap.py +++ b/nanoplotter/spatial_heatmap.py @@ -91,6 +91,6 @@ def spatial_heatmap(array, path, colormap, settings, title=None): activity_map.fig = fig activity_map.html = activity_map.fig.to_html( - full_html=False, include_plotlyjs=settings.include_js) + full_html=False, include_plotlyjs=settings["include_js"]) activity_map.save(settings) return [activity_map] diff --git a/nanoplotter/timeplots.py b/nanoplotter/timeplots.py index 13d945f..2e963fe 100644 --- a/nanoplotter/timeplots.py +++ b/nanoplotter/timeplots.py @@ -136,7 +136,7 @@ def length_over_time(dfs, path, title, settings, log_length=False, color="#4CB39 time_length.fig = fig time_length.html = time_length.fig.to_html( - full_html=False, include_plotlyjs=settings.include_js) + full_html=False, include_plotlyjs=settings["include_js"]) time_length.save(settings) return time_length @@ -163,7 +163,7 @@ def quality_over_time(dfs, path, settings, title=None, color="#4CB391", downsamp time_qual.fig = fig time_qual.html = time_qual.fig.to_html( - full_html=False, include_plotlyjs=settings.include_js) + full_html=False, include_plotlyjs=settings["include_js"]) time_qual.save(settings) return time_qual @@ -226,7 +226,7 @@ def plot_over_time(dfs, path, title, settings, color="#4CB391"): num_reads.fig = fig num_reads.html = num_reads.fig.to_html( - full_html=False, include_plotlyjs=settings.include_js) + full_html=False, include_plotlyjs=settings["include_js"]) num_reads.save(settings) plots = [num_reads] @@ -249,7 +249,7 @@ def plot_over_time(dfs, path, title, settings, color="#4CB391"): pores_over_time.fig = fig pores_over_time.html = pores_over_time.fig.to_html( - full_html=False, include_plotlyjs=settings.include_js) + full_html=False, include_plotlyjs=settings["include_js"]) pores_over_time.save(settings) plots.append(pores_over_time) @@ -274,7 +274,7 @@ def cumulative_yield(dfs, path, title, color, settings): cum_yield_gb.fig = fig cum_yield_gb.html = cum_yield_gb.fig.to_html( - full_html=False, include_plotlyjs=settings.include_js) + full_html=False, include_plotlyjs=settings["include_js"]) cum_yield_gb.save(settings) cum_yield_reads = Plot(path=path + "CumulativeYieldPlot_NumberOfReads.html", @@ -294,7 +294,7 @@ def cumulative_yield(dfs, path, title, color, settings): cum_yield_reads.fig = fig cum_yield_reads.html = cum_yield_reads.fig.to_html( - full_html=False, include_plotlyjs=settings.include_js) + full_html=False, include_plotlyjs=settings["include_js"]) cum_yield_reads.save(settings) return [cum_yield_gb, cum_yield_reads] From af5db171a87cd9e8135770f771e77043b580fcd9 Mon Sep 17 00:00:00 2001 From: wdecoster Date: Sat, 5 Sep 2026 21:53:54 +0200 Subject: [PATCH 3/3] Fix crash with embedded JS and keep individual plot files usable sequencing_speed_over_time accessed settings.include_js while settings is a dict everywhere else, crashing every run that makes time plots. Also place the embedded plotly.js inside rather than between and , and write individual plot html files with the javascript included, since the shared html deliberately carries none. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01M1T5vKqLLmxjb93BEZ9aSn --- nanoplot/NanoPlot.py | 5 +++-- nanoplotter/plot.py | 10 ++++++++-- nanoplotter/timeplots.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/nanoplot/NanoPlot.py b/nanoplot/NanoPlot.py index 639619f..a64d83e 100755 --- a/nanoplot/NanoPlot.py +++ b/nanoplot/NanoPlot.py @@ -402,8 +402,9 @@ def make_report(plots, settings): report.run_info(settings) if settings["info_in_report"] else "", "", ] - if settings["include_js"] == False: - html_content.insert(0, f'') + if settings["include_js"] is False: + # embed plotly.js once, at the start of the body, rather than in every plot + html_content.insert(1, f'') with open(settings["path"] + "NanoPlot-report.html", "w") as html_file: html_file.write(report.html_head + "\n".join(html_content)) diff --git a/nanoplotter/plot.py b/nanoplotter/plot.py index db593f2..6b6a5cf 100644 --- a/nanoplotter/plot.py +++ b/nanoplotter/plot.py @@ -51,9 +51,15 @@ def save(self, settings): return if self.html: - # Save the interactive HTML + # Save the interactive HTML. With embedded javascript self.html contains no + # javascript at all (it is added once to the combined report), so write a + # self-contained copy here to keep individual plot files usable on their own. + if settings.get("include_js", "cdn") is False and self.fig is not None: + standalone = self.fig.to_html(full_html=False, include_plotlyjs=True) + else: + standalone = self.html with open(self.path, "w") as html_out: - html_out.write(self.html) + html_out.write(standalone) # Also save static images unless suppressed if not settings.get("no_static", False): diff --git a/nanoplotter/timeplots.py b/nanoplotter/timeplots.py index 2e963fe..d16743e 100644 --- a/nanoplotter/timeplots.py +++ b/nanoplotter/timeplots.py @@ -193,7 +193,7 @@ def sequencing_speed_over_time(dfs, path, title, settings, color="#4CB391", down time_duration.fig = fig time_duration.html = time_duration.fig.to_html( - full_html=False, include_plotlyjs=settings.include_js) + full_html=False, include_plotlyjs=settings["include_js"]) time_duration.save(settings) return time_duration