From 0a50ca207a201cfb5e06de333b5eacb4900d844e Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Thu, 24 Nov 2022 17:43:25 +0000 Subject: [PATCH 01/21] Add plot viewer module --- signac_dashboard/dashboard.py | 3 + signac_dashboard/modules/__init__.py | 2 + signac_dashboard/modules/plot_viewer.py | 78 +++++++++++++++++++ .../templates/cards/plot_viewer.html | 4 + signac_dashboard/templates/layout.html | 3 + .../templates/plot_viewer/js/plot_viewer.js | 13 ++++ 6 files changed, 103 insertions(+) create mode 100644 signac_dashboard/modules/plot_viewer.py create mode 100644 signac_dashboard/templates/cards/plot_viewer.html create mode 100644 signac_dashboard/templates/plot_viewer/js/plot_viewer.js diff --git a/signac_dashboard/dashboard.py b/signac_dashboard/dashboard.py index 4fafe162..ce15eb0a 100644 --- a/signac_dashboard/dashboard.py +++ b/signac_dashboard/dashboard.py @@ -227,11 +227,14 @@ def _create_assets(self): assets = Environment(self.app) # jQuery is served as a standalone file jquery = Bundle("js/jquery-*.min.js", output="gen/jquery.min.js") + # plotly is served as a standalone file + plotly = Bundle("js/plotly-*.min.js", output="gen/plotly.min.js") # JavaScript is combined into one file and minified js_all = Bundle("js/js_all/*.js", filters="jsmin", output="gen/app.min.js") # SCSS (Sassy CSS) is compiled to CSS scss_all = Bundle("scss/app.scss", filters="libsass", output="gen/app.css") assets.register("jquery", jquery) + assets.register("plotly", plotly) assets.register("js_all", js_all) assets.register("scss_all", scss_all) return assets diff --git a/signac_dashboard/modules/__init__.py b/signac_dashboard/modules/__init__.py index 24a76abc..8c9cfd02 100644 --- a/signac_dashboard/modules/__init__.py +++ b/signac_dashboard/modules/__init__.py @@ -4,6 +4,7 @@ from .flow_status import FlowStatus from .image_viewer import ImageViewer from .notes import Notes +from .plot_viewer import PlotViewer from .schema import Schema from .statepoint_list import StatepointList from .text_display import TextDisplay @@ -20,4 +21,5 @@ "StatepointList", "TextDisplay", "VideoViewer", + "PlotViewer", ] diff --git a/signac_dashboard/modules/plot_viewer.py b/signac_dashboard/modules/plot_viewer.py new file mode 100644 index 00000000..4cfb1e69 --- /dev/null +++ b/signac_dashboard/modules/plot_viewer.py @@ -0,0 +1,78 @@ +# Copyright (c) 2022 The Regents of the University of Michigan +# All rights reserved. +# This software is licensed under the BSD 3-Clause License. +from typing import Callable, Dict, Iterable, List, Tuple, Union + +from flask import abort, render_template +from jinja2.exceptions import TemplateNotFound +from signac import Project +from signac.contrib.job import Job + +from signac_dashboard.dashboard import Dashboard +from signac_dashboard.module import Module + + +def plot_viewer_asset(filename): + path = f"plot_viewer/{filename}" + try: + return render_template(path) + except TemplateNotFound: + abort(404, "The file requested does not exist.") + + +class PlotViewer(Module): + """Displays a plot associated with the job""" + + _supported_contexts = {"JobContext", "ProjectContext"} + + def __init__( + self, + name="Plot Viewer", + necessary_key: Callable[[Job], bool] = lambda _: True, + plotly_args: Callable[ + [Union[Job, Project]], Iterable[Tuple[str, List[Dict], Dict]] + ] = lambda _: list(), + context="JobContext", + template="cards/plot_viewer.html", + **kwargs, + ): + + super().__init__( + name=name, + context=context, + template=template, + **kwargs, + ) + self.necessary_key = necessary_key + self.plotly_args = plotly_args + + def get_cards(self, job_or_project): + if not self.necessary_key(job_or_project): + return [] + + return [ + { + "name": title if title else self.name, + "content": render_template( + self.template, + jobid=job_or_project.id, + plotlydata=data, + plotlylayout=layout, + ), + } + for title, data, layout in self.plotly_args(job_or_project) + ] + + def register(self, dashboard: Dashboard): + # Register routes + dashboard.app.route("/module/plot_viewer/")(plot_viewer_asset) + + # Register assets + assets = ["js/plot_viewer.js"] + for assetfile in assets: + dashboard.register_module_asset( + { + "file": f"templates/plot_viewer/{assetfile}", + "url": f"/module/plot_viewer/{assetfile}", + } + ) diff --git a/signac_dashboard/templates/cards/plot_viewer.html b/signac_dashboard/templates/cards/plot_viewer.html new file mode 100644 index 00000000..b978190f --- /dev/null +++ b/signac_dashboard/templates/cards/plot_viewer.html @@ -0,0 +1,4 @@ +
+
diff --git a/signac_dashboard/templates/layout.html b/signac_dashboard/templates/layout.html index d911609f..34a65f12 100644 --- a/signac_dashboard/templates/layout.html +++ b/signac_dashboard/templates/layout.html @@ -22,6 +22,9 @@ {%- assets "jquery" %} {% endassets %} + {%- assets "plotly" %} + + {% endassets %} {%- assets "js_all" %} {% endassets %} diff --git a/signac_dashboard/templates/plot_viewer/js/plot_viewer.js b/signac_dashboard/templates/plot_viewer/js/plot_viewer.js new file mode 100644 index 00000000..0b925ab3 --- /dev/null +++ b/signac_dashboard/templates/plot_viewer/js/plot_viewer.js @@ -0,0 +1,13 @@ +function draw_plot(element) { + console.log("plotting " + element.id); + data = JSON.parse(element.getAttribute("data-plotlydata")); + layout = JSON.parse(element.getAttribute("data-plotlylayout")); + + Plotly.newPlot(element, data, layout); +} + +$(document).on('turbolinks:load', function() { + $('.plot_viewer').each((index, element) => { + draw_plot(element); + }); +}) From 23bdaaa027c75214f62b4b11e6952c760633a5e5 Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Fri, 25 Nov 2022 10:18:23 +0000 Subject: [PATCH 02/21] Add documentation for PlotViewer module --- doc/api.rst | 1 + signac_dashboard/modules/plot_viewer.py | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index d203cb1d..b70e5a09 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -45,6 +45,7 @@ Dashboard Modules modules.StatepointList modules.TextDisplay modules.VideoViewer + modules.PlotViewer .. autoclass:: signac_dashboard.Module :members: diff --git a/signac_dashboard/modules/plot_viewer.py b/signac_dashboard/modules/plot_viewer.py index 4cfb1e69..57e6764c 100644 --- a/signac_dashboard/modules/plot_viewer.py +++ b/signac_dashboard/modules/plot_viewer.py @@ -21,14 +21,28 @@ def plot_viewer_asset(filename): class PlotViewer(Module): - """Displays a plot associated with the job""" + """Displays a plot associated with the job + + The PlotViewer module can display an interactive plot by using the + Plotly JavaScript library. + + :param name: Default name for the card. Ignored if the :code:`plotly_args` + callable provides one for each card. + :type name: str + :param plotly_args: A callable that accepts a job (in the :code:`'JobContext'`) + or a project (in the :code:`'ProjectContext'`) and returns an iterable. Each + element will constitute a new card and will be composed of a tuple of three + elements: the card title, the plotly data and the plotly layout specification. + :type plotly_args: callable + :param context: Supports :code:`'JobContext'` and :code:`'ProjectContext'`. + :type context: str + """ _supported_contexts = {"JobContext", "ProjectContext"} def __init__( self, name="Plot Viewer", - necessary_key: Callable[[Job], bool] = lambda _: True, plotly_args: Callable[ [Union[Job, Project]], Iterable[Tuple[str, List[Dict], Dict]] ] = lambda _: list(), @@ -43,13 +57,9 @@ def __init__( template=template, **kwargs, ) - self.necessary_key = necessary_key self.plotly_args = plotly_args def get_cards(self, job_or_project): - if not self.necessary_key(job_or_project): - return [] - return [ { "name": title if title else self.name, From 73d0ee4ab4391118f2e0198243398e4a4f852aad Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Fri, 25 Nov 2022 10:31:08 +0000 Subject: [PATCH 03/21] Move plotly to module asset --- signac_dashboard/dashboard.py | 3 --- signac_dashboard/modules/plot_viewer.py | 2 +- signac_dashboard/templates/layout.html | 3 --- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/signac_dashboard/dashboard.py b/signac_dashboard/dashboard.py index ce15eb0a..4fafe162 100644 --- a/signac_dashboard/dashboard.py +++ b/signac_dashboard/dashboard.py @@ -227,14 +227,11 @@ def _create_assets(self): assets = Environment(self.app) # jQuery is served as a standalone file jquery = Bundle("js/jquery-*.min.js", output="gen/jquery.min.js") - # plotly is served as a standalone file - plotly = Bundle("js/plotly-*.min.js", output="gen/plotly.min.js") # JavaScript is combined into one file and minified js_all = Bundle("js/js_all/*.js", filters="jsmin", output="gen/app.min.js") # SCSS (Sassy CSS) is compiled to CSS scss_all = Bundle("scss/app.scss", filters="libsass", output="gen/app.css") assets.register("jquery", jquery) - assets.register("plotly", plotly) assets.register("js_all", js_all) assets.register("scss_all", scss_all) return assets diff --git a/signac_dashboard/modules/plot_viewer.py b/signac_dashboard/modules/plot_viewer.py index 57e6764c..4cea5249 100644 --- a/signac_dashboard/modules/plot_viewer.py +++ b/signac_dashboard/modules/plot_viewer.py @@ -78,7 +78,7 @@ def register(self, dashboard: Dashboard): dashboard.app.route("/module/plot_viewer/")(plot_viewer_asset) # Register assets - assets = ["js/plot_viewer.js"] + assets = ["js/plot_viewer.js", "js/plotly-2.16.1.min.js"] for assetfile in assets: dashboard.register_module_asset( { diff --git a/signac_dashboard/templates/layout.html b/signac_dashboard/templates/layout.html index 34a65f12..d911609f 100644 --- a/signac_dashboard/templates/layout.html +++ b/signac_dashboard/templates/layout.html @@ -22,9 +22,6 @@ {%- assets "jquery" %} {% endassets %} - {%- assets "plotly" %} - - {% endassets %} {%- assets "js_all" %} {% endassets %} From 406710da2d60b4e8aab5f596c5777c5fc8b71066 Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Fri, 25 Nov 2022 10:56:07 +0000 Subject: [PATCH 04/21] Extend PlotViewer documentation --- signac_dashboard/modules/plot_viewer.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/signac_dashboard/modules/plot_viewer.py b/signac_dashboard/modules/plot_viewer.py index 4cea5249..94bb3e14 100644 --- a/signac_dashboard/modules/plot_viewer.py +++ b/signac_dashboard/modules/plot_viewer.py @@ -24,7 +24,29 @@ class PlotViewer(Module): """Displays a plot associated with the job The PlotViewer module can display an interactive plot by using the - Plotly JavaScript library. + Plotly JavaScript library. For information on the different accepted + parameters for the data and layout, refer to the `Plotly JS documentation + `_. + + Example: + + .. code-block:: python + + from signac_dashboard.modules import PlotViewer + + def plotly_args_function(project): + return [ + ("Card title", # if empty, the "name" parameter will be used + # each element on the data list is a different trace + [{ + "x": [1, 2, 3, 4, 5], # x coordinates of the trace + "y": [1, 2, 4, 8, 16] # y coordinates of the trace + }], + {"margin": { "t": 0 } } # layout specification for the whole plot + ) + ] + + plot_module = PlotViewer(plotly_args=plotly_args_function, context="ProjectContext") :param name: Default name for the card. Ignored if the :code:`plotly_args` callable provides one for each card. From ba7edd8aed7fe0c9b815737e96579b2379957edf Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Fri, 25 Nov 2022 11:33:57 +0000 Subject: [PATCH 05/21] Add PlotViewer to the changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 5fe64d87..29fbff4c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,6 +15,7 @@ Added - Improve security on multi-user systems. Dashboard now generates a login token when started. Users must login with the token to view project and job data in the dashboard. +- PlotViewer module for showing interactive plots in the dashboard. Changed +++++++ From 954a603d4f727e4686e44b3e6298b0465f1511eb Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Fri, 25 Nov 2022 12:21:40 +0000 Subject: [PATCH 06/21] Remove unnecesary id from plot viewer div --- signac_dashboard/templates/cards/plot_viewer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signac_dashboard/templates/cards/plot_viewer.html b/signac_dashboard/templates/cards/plot_viewer.html index b978190f..43d57ac6 100644 --- a/signac_dashboard/templates/cards/plot_viewer.html +++ b/signac_dashboard/templates/cards/plot_viewer.html @@ -1,4 +1,4 @@ -
From b4985b534f58a2255c3edcb8e0d927ed180a5cad Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Fri, 25 Nov 2022 12:22:56 +0000 Subject: [PATCH 07/21] Remove unnecesary console log --- signac_dashboard/templates/plot_viewer/js/plot_viewer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/signac_dashboard/templates/plot_viewer/js/plot_viewer.js b/signac_dashboard/templates/plot_viewer/js/plot_viewer.js index 0b925ab3..a87b03b8 100644 --- a/signac_dashboard/templates/plot_viewer/js/plot_viewer.js +++ b/signac_dashboard/templates/plot_viewer/js/plot_viewer.js @@ -1,5 +1,4 @@ function draw_plot(element) { - console.log("plotting " + element.id); data = JSON.parse(element.getAttribute("data-plotlydata")); layout = JSON.parse(element.getAttribute("data-plotlylayout")); From c971876e9caa32dc49f734f22e7a8e958f126c8a Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Mon, 28 Nov 2022 09:58:16 +0100 Subject: [PATCH 08/21] Minor revisions for #163 Co-authored-by: Bradley Dice --- changelog.txt | 2 +- signac_dashboard/modules/plot_viewer.py | 13 ++++++------- signac_dashboard/templates/cards/plot_viewer.html | 4 ++-- .../templates/plot_viewer/js/plot_viewer.js | 4 ++-- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index 29fbff4c..83f7c3bb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,7 +15,7 @@ Added - Improve security on multi-user systems. Dashboard now generates a login token when started. Users must login with the token to view project and job data in the dashboard. -- PlotViewer module for showing interactive plots in the dashboard. +- PlotViewer module for showing interactive plots in the dashboard (#162, #163). Changed +++++++ diff --git a/signac_dashboard/modules/plot_viewer.py b/signac_dashboard/modules/plot_viewer.py index 94bb3e14..ee593b46 100644 --- a/signac_dashboard/modules/plot_viewer.py +++ b/signac_dashboard/modules/plot_viewer.py @@ -13,15 +13,14 @@ def plot_viewer_asset(filename): - path = f"plot_viewer/{filename}" try: - return render_template(path) + return render_template(f"plot_viewer/{filename}") except TemplateNotFound: abort(404, "The file requested does not exist.") class PlotViewer(Module): - """Displays a plot associated with the job + """Displays a plot associated with the job. The PlotViewer module can display an interactive plot by using the Plotly JavaScript library. For information on the different accepted @@ -42,7 +41,7 @@ def plotly_args_function(project): "x": [1, 2, 3, 4, 5], # x coordinates of the trace "y": [1, 2, 4, 8, 16] # y coordinates of the trace }], - {"margin": { "t": 0 } } # layout specification for the whole plot + {"margin": {"t": 0}} # layout specification for the whole plot ) ] @@ -67,7 +66,7 @@ def __init__( name="Plot Viewer", plotly_args: Callable[ [Union[Job, Project]], Iterable[Tuple[str, List[Dict], Dict]] - ] = lambda _: list(), + ] = lambda _: [], context="JobContext", template="cards/plot_viewer.html", **kwargs, @@ -88,8 +87,8 @@ def get_cards(self, job_or_project): "content": render_template( self.template, jobid=job_or_project.id, - plotlydata=data, - plotlylayout=layout, + plotly_data=data, + plotly_layout=layout, ), } for title, data, layout in self.plotly_args(job_or_project) diff --git a/signac_dashboard/templates/cards/plot_viewer.html b/signac_dashboard/templates/cards/plot_viewer.html index 43d57ac6..8ac5a592 100644 --- a/signac_dashboard/templates/cards/plot_viewer.html +++ b/signac_dashboard/templates/cards/plot_viewer.html @@ -1,4 +1,4 @@
+ data-plotly-data='{{ plotly_data | tojson }}' + data-plotly-layout='{{ plotly_layout | tojson }}'>
diff --git a/signac_dashboard/templates/plot_viewer/js/plot_viewer.js b/signac_dashboard/templates/plot_viewer/js/plot_viewer.js index a87b03b8..b94f982a 100644 --- a/signac_dashboard/templates/plot_viewer/js/plot_viewer.js +++ b/signac_dashboard/templates/plot_viewer/js/plot_viewer.js @@ -1,6 +1,6 @@ function draw_plot(element) { - data = JSON.parse(element.getAttribute("data-plotlydata")); - layout = JSON.parse(element.getAttribute("data-plotlylayout")); + data = JSON.parse(element.getAttribute("data-plotly-data")); + layout = JSON.parse(element.getAttribute("data-plotly-layout")); Plotly.newPlot(element, data, layout); } From 74c3ce2d6fb3bff9081b5b8a19d06b781e721e2c Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Mon, 28 Nov 2022 10:32:37 +0100 Subject: [PATCH 09/21] Alphabetize PlotViewer module in lists --- doc/api.rst | 2 +- signac_dashboard/modules/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index b70e5a09..c3054603 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -41,11 +41,11 @@ Dashboard Modules modules.FlowStatus modules.ImageViewer modules.Notes + modules.PlotViewer modules.Schema modules.StatepointList modules.TextDisplay modules.VideoViewer - modules.PlotViewer .. autoclass:: signac_dashboard.Module :members: diff --git a/signac_dashboard/modules/__init__.py b/signac_dashboard/modules/__init__.py index 8c9cfd02..ebfb20fb 100644 --- a/signac_dashboard/modules/__init__.py +++ b/signac_dashboard/modules/__init__.py @@ -17,9 +17,9 @@ "FlowStatus", "ImageViewer", "Notes", + "PlotViewer", "Schema", "StatepointList", "TextDisplay", "VideoViewer", - "PlotViewer", ] From 2e7aea58de4d784ff79ba23b3e153c50f9f11cc7 Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Mon, 28 Nov 2022 10:34:59 +0100 Subject: [PATCH 10/21] Use Plotly from CDN instead of local copy --- signac_dashboard/modules/plot_viewer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/signac_dashboard/modules/plot_viewer.py b/signac_dashboard/modules/plot_viewer.py index ee593b46..d9e307cd 100644 --- a/signac_dashboard/modules/plot_viewer.py +++ b/signac_dashboard/modules/plot_viewer.py @@ -99,11 +99,14 @@ def register(self, dashboard: Dashboard): dashboard.app.route("/module/plot_viewer/")(plot_viewer_asset) # Register assets - assets = ["js/plot_viewer.js", "js/plotly-2.16.1.min.js"] + assets = ["js/plot_viewer.js"] for assetfile in assets: dashboard.register_module_asset( { - "file": f"templates/plot_viewer/{assetfile}", "url": f"/module/plot_viewer/{assetfile}", } ) + + cdn_assets = ["https://cdn.plot.ly/plotly-2.16.1.min.js"] + for asseturl in cdn_assets: + dashboard.register_module_asset({"url": asseturl}) From d62d08aaa58c4620e5906766f86a36bd393b198a Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Mon, 28 Nov 2022 12:49:01 +0100 Subject: [PATCH 11/21] Rename Matplotlib plot example --- examples/{plots => plots-matplotlib}/.gitignore | 0 examples/{plots => plots-matplotlib}/README.md | 2 +- examples/{plots => plots-matplotlib}/dashboard.py | 0 examples/{plots => plots-matplotlib}/init.py | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename examples/{plots => plots-matplotlib}/.gitignore (100%) rename examples/{plots => plots-matplotlib}/README.md (74%) rename examples/{plots => plots-matplotlib}/dashboard.py (100%) rename examples/{plots => plots-matplotlib}/init.py (100%) diff --git a/examples/plots/.gitignore b/examples/plots-matplotlib/.gitignore similarity index 100% rename from examples/plots/.gitignore rename to examples/plots-matplotlib/.gitignore diff --git a/examples/plots/README.md b/examples/plots-matplotlib/README.md similarity index 74% rename from examples/plots/README.md rename to examples/plots-matplotlib/README.md index 783324e6..0afe9eeb 100644 --- a/examples/plots/README.md +++ b/examples/plots-matplotlib/README.md @@ -1,3 +1,3 @@ -# Plots Example +# Matplotlib Plots Example To run this example, call `python init.py` and then `python dashboard.py run`. diff --git a/examples/plots/dashboard.py b/examples/plots-matplotlib/dashboard.py similarity index 100% rename from examples/plots/dashboard.py rename to examples/plots-matplotlib/dashboard.py diff --git a/examples/plots/init.py b/examples/plots-matplotlib/init.py similarity index 100% rename from examples/plots/init.py rename to examples/plots-matplotlib/init.py From 59b37bc6e562e6662d1d152f59124b088e47547f Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Mon, 28 Nov 2022 12:51:06 +0100 Subject: [PATCH 12/21] Add Plotly example --- examples/plots-plotly/.gitignore | 3 ++ examples/plots-plotly/README.md | 3 ++ examples/plots-plotly/dashboard.py | 78 ++++++++++++++++++++++++++++++ examples/plots-plotly/init.py | 40 +++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 examples/plots-plotly/.gitignore create mode 100644 examples/plots-plotly/README.md create mode 100755 examples/plots-plotly/dashboard.py create mode 100755 examples/plots-plotly/init.py diff --git a/examples/plots-plotly/.gitignore b/examples/plots-plotly/.gitignore new file mode 100644 index 00000000..9cb46da1 --- /dev/null +++ b/examples/plots-plotly/.gitignore @@ -0,0 +1,3 @@ +signac.rc +signac_project_document.json +workspace/ diff --git a/examples/plots-plotly/README.md b/examples/plots-plotly/README.md new file mode 100644 index 00000000..2628537e --- /dev/null +++ b/examples/plots-plotly/README.md @@ -0,0 +1,3 @@ +# Plotly Plots Example + +To run this example, call `python init.py` and then `python dashboard.py run`. diff --git a/examples/plots-plotly/dashboard.py b/examples/plots-plotly/dashboard.py new file mode 100755 index 00000000..b06ed2c8 --- /dev/null +++ b/examples/plots-plotly/dashboard.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Regents of the University of Michigan +# All rights reserved. +# This software is licensed under the BSD 3-Clause License. +from scipy.signal import coherence + +from signac_dashboard import Dashboard +from signac_dashboard.modules import PlotViewer, StatepointList, TextDisplay + + +class PlotDashboard(Dashboard): + def job_sorter(self, job): + return job.sp.get("coherence_time", -1) + + def job_title(self, job): + return f"Coherence time: {job.sp.coherence_time}" + + +def correlation_text(job): + return "Correlation coefficient: {:.5f}".format(job.doc["correlation"]) + + +def plotly_args(job): + # Visualization adapted from: + # https://matplotlib.org/gallery/lines_bars_and_markers/cohere.html + + # It's necessary to cast to list because the list elements of the job + # document are BufferedJSONAttrList, which is not serializable + signals_traces = [ + { + "x": list(job.doc["t"]), + "y": list(job.doc["s1"]), + "name": "s1", + }, + { + "x": list(job.doc["t"]), + "y": list(job.doc["s2"]), + "name": "s2", + }, + ] + signals_layout = { + "xaxis": { + "title": "time", + "range": [0, 2], + }, + "height": 200, + "margin": dict(t=30, b=40, l=40, r=0), + } + + dt = job.doc["t"][1] - job.doc["t"][0] + coherence_x, coherence_y = coherence( + job.doc["s1"], job.doc["s2"], nfft=256, fs=1.0 / dt + ) + coherence_traces = [ + { + "x": coherence_x.tolist(), + "y": coherence_y.tolist(), + } + ] + coherence_layout = { + "title": f"Coherence time = {job.sp.coherence_time}", + "xaxis": {"title": "frequency"}, + "yaxis": {"title": "coherence", "range": [0, 1]}, + "height": 200, + "margin": dict(t=30, b=40, l=40, r=0), + } + return [ + ("Signals", signals_traces, signals_layout), + ("Coherence", coherence_traces, coherence_layout), + ] + + +if __name__ == "__main__": + modules = [] + modules.append(StatepointList()) + modules.append(PlotViewer(plotly_args=plotly_args)) + modules.append(TextDisplay(name="Correlation", message=correlation_text)) + PlotDashboard(modules=modules).main() diff --git a/examples/plots-plotly/init.py b/examples/plots-plotly/init.py new file mode 100755 index 00000000..1d1495e5 --- /dev/null +++ b/examples/plots-plotly/init.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Regents of the University of Michigan +# All rights reserved. +# This software is licensed under the BSD 3-Clause License. +import numpy as np +import signac + +project = signac.init_project("plots") + + +def plot_coherence(job): + # Data generation adapted from: + # https://matplotlib.org/gallery/lines_bars_and_markers/cohere.html + + print(f"Generating signals for coherence time {job.sp.coherence_time}, job {job}") + # Fixing random state for reproducibility + np.random.seed(job.sp.seed) + + dt = 0.01 + t = np.arange(0, 30, dt) + nse1 = np.random.randn(len(t)) # white noise 1 + nse2 = np.random.randn(len(t)) # white noise 2 + + # Two signals with a coherent part and a random part + s1 = np.sin(2 * np.pi * job.sp.coherence_time * t) + nse1 + s2 = np.sin(2 * np.pi * job.sp.coherence_time * t) + nse2 + + # Save the signal data + job.doc["t"] = t.tolist() + job.doc["s1"] = s1.tolist() + job.doc["s2"] = s2.tolist() + + # Save correlation coefficient + job.doc["correlation"] = np.corrcoef(s1, s2)[0, 1] + + +for i in range(30): + job = project.open_job({"coherence_time": i, "seed": 42}) + job.init() + plot_coherence(job) From a898191678fcf600ff77be7d6960c4d89b122f59 Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Mon, 28 Nov 2022 17:37:00 +0100 Subject: [PATCH 13/21] Protect access to PlotViewer routes --- signac_dashboard/modules/plot_viewer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/signac_dashboard/modules/plot_viewer.py b/signac_dashboard/modules/plot_viewer.py index d9e307cd..cbfc17a1 100644 --- a/signac_dashboard/modules/plot_viewer.py +++ b/signac_dashboard/modules/plot_viewer.py @@ -3,6 +3,7 @@ # This software is licensed under the BSD 3-Clause License. from typing import Callable, Dict, Iterable, List, Tuple, Union +import flask_login from flask import abort, render_template from jinja2.exceptions import TemplateNotFound from signac import Project @@ -12,13 +13,6 @@ from signac_dashboard.module import Module -def plot_viewer_asset(filename): - try: - return render_template(f"plot_viewer/{filename}") - except TemplateNotFound: - abort(404, "The file requested does not exist.") - - class PlotViewer(Module): """Displays a plot associated with the job. @@ -96,7 +90,13 @@ def get_cards(self, job_or_project): def register(self, dashboard: Dashboard): # Register routes - dashboard.app.route("/module/plot_viewer/")(plot_viewer_asset) + @dashboard.app.route("/module/plot_viewer/") + @flask_login.login_required + def plot_viewer_asset(filename): + try: + return render_template(f"plot_viewer/{filename}") + except TemplateNotFound: + abort(404, "The file requested does not exist.") # Register assets assets = ["js/plot_viewer.js"] From 02df95b1d6e19b92b5d522a7795d05b7a03e4ab3 Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Mon, 28 Nov 2022 20:26:10 +0100 Subject: [PATCH 14/21] Unify Matplotlib and Plotly examples --- examples/plots-matplotlib/README.md | 3 -- examples/plots-matplotlib/dashboard.py | 26 ------------ examples/plots-plotly/.gitignore | 3 -- examples/plots-plotly/init.py | 40 ------------------- .../{plots-matplotlib => plots}/.gitignore | 0 examples/{plots-plotly => plots}/README.md | 2 +- examples/{plots-plotly => plots}/dashboard.py | 18 ++++----- examples/{plots-matplotlib => plots}/init.py | 11 +++++ 8 files changed, 21 insertions(+), 82 deletions(-) delete mode 100644 examples/plots-matplotlib/README.md delete mode 100755 examples/plots-matplotlib/dashboard.py delete mode 100644 examples/plots-plotly/.gitignore delete mode 100755 examples/plots-plotly/init.py rename examples/{plots-matplotlib => plots}/.gitignore (100%) rename examples/{plots-plotly => plots}/README.md (77%) rename examples/{plots-plotly => plots}/dashboard.py (85%) mode change 100755 => 100644 rename examples/{plots-matplotlib => plots}/init.py (79%) mode change 100755 => 100644 diff --git a/examples/plots-matplotlib/README.md b/examples/plots-matplotlib/README.md deleted file mode 100644 index 0afe9eeb..00000000 --- a/examples/plots-matplotlib/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Matplotlib Plots Example - -To run this example, call `python init.py` and then `python dashboard.py run`. diff --git a/examples/plots-matplotlib/dashboard.py b/examples/plots-matplotlib/dashboard.py deleted file mode 100755 index 4a5e5b64..00000000 --- a/examples/plots-matplotlib/dashboard.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2019 The Regents of the University of Michigan -# All rights reserved. -# This software is licensed under the BSD 3-Clause License. -from signac_dashboard import Dashboard -from signac_dashboard.modules import ImageViewer, StatepointList, TextDisplay - - -class PlotDashboard(Dashboard): - def job_sorter(self, job): - return job.sp.get("coherence_time", -1) - - def job_title(self, job): - return f"Coherence time: {job.sp.coherence_time}" - - -def correlation_text(job): - return "Correlation coefficient: {:.5f}".format(job.doc["correlation"]) - - -if __name__ == "__main__": - modules = [] - modules.append(StatepointList()) - modules.append(ImageViewer()) - modules.append(TextDisplay(name="Correlation", message=correlation_text)) - PlotDashboard(modules=modules).main() diff --git a/examples/plots-plotly/.gitignore b/examples/plots-plotly/.gitignore deleted file mode 100644 index 9cb46da1..00000000 --- a/examples/plots-plotly/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -signac.rc -signac_project_document.json -workspace/ diff --git a/examples/plots-plotly/init.py b/examples/plots-plotly/init.py deleted file mode 100755 index 1d1495e5..00000000 --- a/examples/plots-plotly/init.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2019 The Regents of the University of Michigan -# All rights reserved. -# This software is licensed under the BSD 3-Clause License. -import numpy as np -import signac - -project = signac.init_project("plots") - - -def plot_coherence(job): - # Data generation adapted from: - # https://matplotlib.org/gallery/lines_bars_and_markers/cohere.html - - print(f"Generating signals for coherence time {job.sp.coherence_time}, job {job}") - # Fixing random state for reproducibility - np.random.seed(job.sp.seed) - - dt = 0.01 - t = np.arange(0, 30, dt) - nse1 = np.random.randn(len(t)) # white noise 1 - nse2 = np.random.randn(len(t)) # white noise 2 - - # Two signals with a coherent part and a random part - s1 = np.sin(2 * np.pi * job.sp.coherence_time * t) + nse1 - s2 = np.sin(2 * np.pi * job.sp.coherence_time * t) + nse2 - - # Save the signal data - job.doc["t"] = t.tolist() - job.doc["s1"] = s1.tolist() - job.doc["s2"] = s2.tolist() - - # Save correlation coefficient - job.doc["correlation"] = np.corrcoef(s1, s2)[0, 1] - - -for i in range(30): - job = project.open_job({"coherence_time": i, "seed": 42}) - job.init() - plot_coherence(job) diff --git a/examples/plots-matplotlib/.gitignore b/examples/plots/.gitignore similarity index 100% rename from examples/plots-matplotlib/.gitignore rename to examples/plots/.gitignore diff --git a/examples/plots-plotly/README.md b/examples/plots/README.md similarity index 77% rename from examples/plots-plotly/README.md rename to examples/plots/README.md index 2628537e..783324e6 100644 --- a/examples/plots-plotly/README.md +++ b/examples/plots/README.md @@ -1,3 +1,3 @@ -# Plotly Plots Example +# Plots Example To run this example, call `python init.py` and then `python dashboard.py run`. diff --git a/examples/plots-plotly/dashboard.py b/examples/plots/dashboard.py old mode 100755 new mode 100644 similarity index 85% rename from examples/plots-plotly/dashboard.py rename to examples/plots/dashboard.py index b06ed2c8..5c651129 --- a/examples/plots-plotly/dashboard.py +++ b/examples/plots/dashboard.py @@ -2,10 +2,13 @@ # Copyright (c) 2019 The Regents of the University of Michigan # All rights reserved. # This software is licensed under the BSD 3-Clause License. -from scipy.signal import coherence - from signac_dashboard import Dashboard -from signac_dashboard.modules import PlotViewer, StatepointList, TextDisplay +from signac_dashboard.modules import ( + ImageViewer, + PlotViewer, + StatepointList, + TextDisplay, +) class PlotDashboard(Dashboard): @@ -47,14 +50,10 @@ def plotly_args(job): "margin": dict(t=30, b=40, l=40, r=0), } - dt = job.doc["t"][1] - job.doc["t"][0] - coherence_x, coherence_y = coherence( - job.doc["s1"], job.doc["s2"], nfft=256, fs=1.0 / dt - ) coherence_traces = [ { - "x": coherence_x.tolist(), - "y": coherence_y.tolist(), + "x": list(job.doc["f"]), + "y": list(job.doc["cxy"]), } ] coherence_layout = { @@ -73,6 +72,7 @@ def plotly_args(job): if __name__ == "__main__": modules = [] modules.append(StatepointList()) + modules.append(ImageViewer()) modules.append(PlotViewer(plotly_args=plotly_args)) modules.append(TextDisplay(name="Correlation", message=correlation_text)) PlotDashboard(modules=modules).main() diff --git a/examples/plots-matplotlib/init.py b/examples/plots/init.py old mode 100755 new mode 100644 similarity index 79% rename from examples/plots-matplotlib/init.py rename to examples/plots/init.py index a5030367..0256aab5 --- a/examples/plots-matplotlib/init.py +++ b/examples/plots/init.py @@ -33,6 +33,7 @@ def plot_coherence(job): # Save correlation coefficient job.doc["correlation"] = np.corrcoef(s1, s2)[0, 1] + # Image plot needs to be saved to the job folder for later visualization fig, axs = plt.subplots(2, 1) plt.title(f"Coherence time = {job.sp.coherence_time}") @@ -50,6 +51,16 @@ def plot_coherence(job): plt.savefig(job.fn("coherence.png")) plt.close() + # Save the signal and coherence data for the Plotly visualization. + # This could also be computed while the dashboard is running when + # it is requested. + job.doc["t"] = t.tolist() + job.doc["s1"] = s1.tolist() + job.doc["s2"] = s2.tolist() + job.doc["cxy"] = cxy.tolist() + job.doc["f"] = f.tolist() + job.doc["cxy"] = cxy.tolist() + for i in range(30): job = project.open_job({"coherence_time": i, "seed": 42}) From e4bd48d5fca3bd5fec80a3cf605f0a118d693089 Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Mon, 28 Nov 2022 20:36:06 +0100 Subject: [PATCH 15/21] Rename PlotViewer to PlotlyViewer --- changelog.txt | 2 +- doc/api.rst | 2 +- examples/plots/dashboard.py | 4 ++-- signac_dashboard/modules/__init__.py | 4 ++-- .../{plot_viewer.py => plotly_viewer.py} | 22 +++++++++---------- .../{plot_viewer.html => plotly_viewer.html} | 2 +- .../js/plotly_viewer.js} | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) rename signac_dashboard/modules/{plot_viewer.py => plotly_viewer.py} (84%) rename signac_dashboard/templates/cards/{plot_viewer.html => plotly_viewer.html} (80%) rename signac_dashboard/templates/{plot_viewer/js/plot_viewer.js => plotly_viewer/js/plotly_viewer.js} (85%) diff --git a/changelog.txt b/changelog.txt index 83f7c3bb..ed85e587 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,7 +15,7 @@ Added - Improve security on multi-user systems. Dashboard now generates a login token when started. Users must login with the token to view project and job data in the dashboard. -- PlotViewer module for showing interactive plots in the dashboard (#162, #163). +- PlotlyViewer module for showing interactive plots in the dashboard (#162, #163). Changed +++++++ diff --git a/doc/api.rst b/doc/api.rst index c3054603..d2314ab5 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -41,7 +41,7 @@ Dashboard Modules modules.FlowStatus modules.ImageViewer modules.Notes - modules.PlotViewer + modules.PlotlyViewer modules.Schema modules.StatepointList modules.TextDisplay diff --git a/examples/plots/dashboard.py b/examples/plots/dashboard.py index 5c651129..6c63350a 100644 --- a/examples/plots/dashboard.py +++ b/examples/plots/dashboard.py @@ -5,7 +5,7 @@ from signac_dashboard import Dashboard from signac_dashboard.modules import ( ImageViewer, - PlotViewer, + PlotlyViewer, StatepointList, TextDisplay, ) @@ -73,6 +73,6 @@ def plotly_args(job): modules = [] modules.append(StatepointList()) modules.append(ImageViewer()) - modules.append(PlotViewer(plotly_args=plotly_args)) + modules.append(PlotlyViewer(plotly_args=plotly_args)) modules.append(TextDisplay(name="Correlation", message=correlation_text)) PlotDashboard(modules=modules).main() diff --git a/signac_dashboard/modules/__init__.py b/signac_dashboard/modules/__init__.py index ebfb20fb..d63f9c86 100644 --- a/signac_dashboard/modules/__init__.py +++ b/signac_dashboard/modules/__init__.py @@ -4,7 +4,7 @@ from .flow_status import FlowStatus from .image_viewer import ImageViewer from .notes import Notes -from .plot_viewer import PlotViewer +from .plotly_viewer import PlotlyViewer from .schema import Schema from .statepoint_list import StatepointList from .text_display import TextDisplay @@ -17,7 +17,7 @@ "FlowStatus", "ImageViewer", "Notes", - "PlotViewer", + "PlotlyViewer", "Schema", "StatepointList", "TextDisplay", diff --git a/signac_dashboard/modules/plot_viewer.py b/signac_dashboard/modules/plotly_viewer.py similarity index 84% rename from signac_dashboard/modules/plot_viewer.py rename to signac_dashboard/modules/plotly_viewer.py index cbfc17a1..521b74f9 100644 --- a/signac_dashboard/modules/plot_viewer.py +++ b/signac_dashboard/modules/plotly_viewer.py @@ -13,10 +13,10 @@ from signac_dashboard.module import Module -class PlotViewer(Module): +class PlotlyViewer(Module): """Displays a plot associated with the job. - The PlotViewer module can display an interactive plot by using the + The PlotlyViewer module can display an interactive plot by using the Plotly JavaScript library. For information on the different accepted parameters for the data and layout, refer to the `Plotly JS documentation `_. @@ -25,7 +25,7 @@ class PlotViewer(Module): .. code-block:: python - from signac_dashboard.modules import PlotViewer + from signac_dashboard.modules import PlotlyViewer def plotly_args_function(project): return [ @@ -39,7 +39,7 @@ def plotly_args_function(project): ) ] - plot_module = PlotViewer(plotly_args=plotly_args_function, context="ProjectContext") + plot_module = PlotlyViewer(plotly_args=plotly_args_function, context="ProjectContext") :param name: Default name for the card. Ignored if the :code:`plotly_args` callable provides one for each card. @@ -57,12 +57,12 @@ def plotly_args_function(project): def __init__( self, - name="Plot Viewer", + name="Plotly Viewer", plotly_args: Callable[ [Union[Job, Project]], Iterable[Tuple[str, List[Dict], Dict]] ] = lambda _: [], context="JobContext", - template="cards/plot_viewer.html", + template="cards/plotly_viewer.html", **kwargs, ): @@ -90,20 +90,20 @@ def get_cards(self, job_or_project): def register(self, dashboard: Dashboard): # Register routes - @dashboard.app.route("/module/plot_viewer/") + @dashboard.app.route("/module/plotly_viewer/") @flask_login.login_required - def plot_viewer_asset(filename): + def plotly_viewer_asset(filename): try: - return render_template(f"plot_viewer/{filename}") + return render_template(f"plotly_viewer/{filename}") except TemplateNotFound: abort(404, "The file requested does not exist.") # Register assets - assets = ["js/plot_viewer.js"] + assets = ["js/plotly_viewer.js"] for assetfile in assets: dashboard.register_module_asset( { - "url": f"/module/plot_viewer/{assetfile}", + "url": f"/module/plotly_viewer/{assetfile}", } ) diff --git a/signac_dashboard/templates/cards/plot_viewer.html b/signac_dashboard/templates/cards/plotly_viewer.html similarity index 80% rename from signac_dashboard/templates/cards/plot_viewer.html rename to signac_dashboard/templates/cards/plotly_viewer.html index 8ac5a592..f52a1fc1 100644 --- a/signac_dashboard/templates/cards/plot_viewer.html +++ b/signac_dashboard/templates/cards/plotly_viewer.html @@ -1,4 +1,4 @@ -
diff --git a/signac_dashboard/templates/plot_viewer/js/plot_viewer.js b/signac_dashboard/templates/plotly_viewer/js/plotly_viewer.js similarity index 85% rename from signac_dashboard/templates/plot_viewer/js/plot_viewer.js rename to signac_dashboard/templates/plotly_viewer/js/plotly_viewer.js index b94f982a..2a26c601 100644 --- a/signac_dashboard/templates/plot_viewer/js/plot_viewer.js +++ b/signac_dashboard/templates/plotly_viewer/js/plotly_viewer.js @@ -6,7 +6,7 @@ function draw_plot(element) { } $(document).on('turbolinks:load', function() { - $('.plot_viewer').each((index, element) => { + $('.plotly_viewer').each((index, element) => { draw_plot(element); }); }) From d271eeee435d06ff87bef8201fcefbad2d2c69bc Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Mon, 28 Nov 2022 20:43:42 +0100 Subject: [PATCH 16/21] Simplify module registration for PlotlyViewer --- signac_dashboard/modules/plotly_viewer.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/signac_dashboard/modules/plotly_viewer.py b/signac_dashboard/modules/plotly_viewer.py index 521b74f9..9e2a01f7 100644 --- a/signac_dashboard/modules/plotly_viewer.py +++ b/signac_dashboard/modules/plotly_viewer.py @@ -99,14 +99,9 @@ def plotly_viewer_asset(filename): abort(404, "The file requested does not exist.") # Register assets - assets = ["js/plotly_viewer.js"] - for assetfile in assets: - dashboard.register_module_asset( - { - "url": f"/module/plotly_viewer/{assetfile}", - } - ) - - cdn_assets = ["https://cdn.plot.ly/plotly-2.16.1.min.js"] - for asseturl in cdn_assets: + assets = [ + "/module/plotly_viewer/js/plotly_viewer.js", + "https://cdn.plot.ly/plotly-2.16.1.min.js", + ] + for asseturl in assets: dashboard.register_module_asset({"url": asseturl}) From 226111ef967cd294ebace776d82d0bca2b094d58 Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Tue, 29 Nov 2022 11:42:09 +0100 Subject: [PATCH 17/21] Move PlotlyView argument passing functionality to REST endpoint Now instead of passing the data as HTML attributes a REST endpoint has been set up so that the page is rendered quickly and the client can fetch the data with a GET request --- examples/plots/dashboard.py | 23 ++--- signac_dashboard/modules/plotly_viewer.py | 85 +++++++++++++------ .../templates/cards/plotly_viewer.html | 4 +- .../plotly_viewer/js/plotly_viewer.js | 15 ++-- 4 files changed, 82 insertions(+), 45 deletions(-) diff --git a/examples/plots/dashboard.py b/examples/plots/dashboard.py index 6c63350a..5d811168 100644 --- a/examples/plots/dashboard.py +++ b/examples/plots/dashboard.py @@ -23,12 +23,14 @@ def correlation_text(job): return "Correlation coefficient: {:.5f}".format(job.doc["correlation"]) -def plotly_args(job): - # Visualization adapted from: - # https://matplotlib.org/gallery/lines_bars_and_markers/cohere.html +# Visualization adapted from: +# https://matplotlib.org/gallery/lines_bars_and_markers/cohere.html - # It's necessary to cast to list because the list elements of the job - # document are BufferedJSONAttrList, which is not serializable +# It's necessary to cast to list because the list elements of the job +# document are BufferedJSONAttrList, which is not serializable + + +def signals_plotly_args(job): signals_traces = [ { "x": list(job.doc["t"]), @@ -49,7 +51,10 @@ def plotly_args(job): "height": 200, "margin": dict(t=30, b=40, l=40, r=0), } + return (signals_traces, signals_layout) + +def coherence_plotly_args(job): coherence_traces = [ { "x": list(job.doc["f"]), @@ -63,16 +68,14 @@ def plotly_args(job): "height": 200, "margin": dict(t=30, b=40, l=40, r=0), } - return [ - ("Signals", signals_traces, signals_layout), - ("Coherence", coherence_traces, coherence_layout), - ] + return (coherence_traces, coherence_layout) if __name__ == "__main__": modules = [] modules.append(StatepointList()) modules.append(ImageViewer()) - modules.append(PlotlyViewer(plotly_args=plotly_args)) + modules.append(PlotlyViewer("Signals", plotly_args=signals_plotly_args)) + modules.append(PlotlyViewer("Coherence", plotly_args=coherence_plotly_args)) modules.append(TextDisplay(name="Correlation", message=correlation_text)) PlotDashboard(modules=modules).main() diff --git a/signac_dashboard/modules/plotly_viewer.py b/signac_dashboard/modules/plotly_viewer.py index 9e2a01f7..aec6b996 100644 --- a/signac_dashboard/modules/plotly_viewer.py +++ b/signac_dashboard/modules/plotly_viewer.py @@ -1,10 +1,12 @@ # Copyright (c) 2022 The Regents of the University of Michigan # All rights reserved. # This software is licensed under the BSD 3-Clause License. -from typing import Callable, Dict, Iterable, List, Tuple, Union +import hashlib +from typing import Callable, Dict, List, Tuple, Union import flask_login -from flask import abort, render_template +from flask import abort, jsonify, render_template, request +from flask.views import View from jinja2.exceptions import TemplateNotFound from signac import Project from signac.contrib.job import Job @@ -13,6 +15,26 @@ from signac_dashboard.module import Module +class PlotlyView(View): + decorators = [flask_login.login_required] + + def __init__(self, dashboard, args_function, context): + self.dashboard = dashboard + self.args_function = args_function + self.context = context + + def dispatch_request(self): + if self.context == "JobContext": + jobid = request.args.get("jobid") + job = self.dashboard.project.open_job(id=jobid) + traces, layout = self.args_function(job) + elif self.context == "ProjectContext": + traces, layout = self.args_function(self.dashboard.project) + else: + raise NotImplementedError() + return jsonify({"traces": traces, "layout": layout}) + + class PlotlyViewer(Module): """Displays a plot associated with the job. @@ -29,8 +51,7 @@ class PlotlyViewer(Module): def plotly_args_function(project): return [ - ("Card title", # if empty, the "name" parameter will be used - # each element on the data list is a different trace + (# each element on the data list is a different trace [{ "x": [1, 2, 3, 4, 5], # x coordinates of the trace "y": [1, 2, 4, 8, 16] # y coordinates of the trace @@ -54,13 +75,14 @@ def plotly_args_function(project): """ _supported_contexts = {"JobContext", "ProjectContext"} + _assets_url_registered = False def __init__( self, name="Plotly Viewer", plotly_args: Callable[ - [Union[Job, Project]], Iterable[Tuple[str, List[Dict], Dict]] - ] = lambda _: [], + [Union[Job, Project]], Tuple[List[Dict], Dict] + ] = lambda _: ([{}], {}), context="JobContext", template="cards/plotly_viewer.html", **kwargs, @@ -73,35 +95,48 @@ def __init__( **kwargs, ) self.plotly_args = plotly_args + self.card_id = hashlib.sha1(str(id(self)).encode("utf-8")).hexdigest() def get_cards(self, job_or_project): return [ { - "name": title if title else self.name, + "name": self.name, "content": render_template( self.template, jobid=job_or_project.id, - plotly_data=data, - plotly_layout=layout, + endpoint=self.arguments_endpoint(), ), } - for title, data, layout in self.plotly_args(job_or_project) ] def register(self, dashboard: Dashboard): # Register routes - @dashboard.app.route("/module/plotly_viewer/") - @flask_login.login_required - def plotly_viewer_asset(filename): - try: - return render_template(f"plotly_viewer/{filename}") - except TemplateNotFound: - abort(404, "The file requested does not exist.") - - # Register assets - assets = [ - "/module/plotly_viewer/js/plotly_viewer.js", - "https://cdn.plot.ly/plotly-2.16.1.min.js", - ] - for asseturl in assets: - dashboard.register_module_asset({"url": asseturl}) + if not PlotlyViewer._assets_url_registered: + + @dashboard.app.route("/module/plotly_viewer/") + @flask_login.login_required + def plotly_viewer_asset(filename): + try: + return render_template(f"plotly_viewer/{filename}") + except TemplateNotFound: + abort(404, "The file requested does not exist.") + + # Register assets + assets = [ + "/module/plotly_viewer/js/plotly_viewer.js", + "https://cdn.plot.ly/plotly-2.16.1.min.js", + ] + for asseturl in assets: + dashboard.register_module_asset({"url": asseturl}) + + PlotlyViewer._assets_url_registered = True + + dashboard.app.add_url_rule( + self.arguments_endpoint(), + view_func=PlotlyView.as_view( + f"plotly-{self.card_id}", dashboard, self.plotly_args, self.context + ), + ) + + def arguments_endpoint(self): + return f"/module/plotly_viewer/{self.card_id}/arguments" diff --git a/signac_dashboard/templates/cards/plotly_viewer.html b/signac_dashboard/templates/cards/plotly_viewer.html index f52a1fc1..95aefa48 100644 --- a/signac_dashboard/templates/cards/plotly_viewer.html +++ b/signac_dashboard/templates/cards/plotly_viewer.html @@ -1,4 +1,4 @@
+ data-endpoint="{{ endpoint }}" + data-jobid="{{ jobid }}">
diff --git a/signac_dashboard/templates/plotly_viewer/js/plotly_viewer.js b/signac_dashboard/templates/plotly_viewer/js/plotly_viewer.js index 2a26c601..a97d2d30 100644 --- a/signac_dashboard/templates/plotly_viewer/js/plotly_viewer.js +++ b/signac_dashboard/templates/plotly_viewer/js/plotly_viewer.js @@ -1,12 +1,11 @@ -function draw_plot(element) { - data = JSON.parse(element.getAttribute("data-plotly-data")); - layout = JSON.parse(element.getAttribute("data-plotly-layout")); - - Plotly.newPlot(element, data, layout); -} - $(document).on('turbolinks:load', function() { $('.plotly_viewer').each((index, element) => { - draw_plot(element); + let endpoint = element.getAttribute("data-endpoint") + let jobid = element.getAttribute("data-jobid") + jQuery.get(endpoint, {jobid: jobid}, (data, textStatus, response) => { + let traces = data["traces"] + let layout = data["layout"] + Plotly.newPlot(element, traces, layout) + }) }); }) From 24a9aa29b976c054fa4d375aaff248f34eed1e3e Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Tue, 29 Nov 2022 12:20:50 +0100 Subject: [PATCH 18/21] Update PlotlyViewer docstring for new callable type --- signac_dashboard/modules/plotly_viewer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/signac_dashboard/modules/plotly_viewer.py b/signac_dashboard/modules/plotly_viewer.py index aec6b996..0ea2172e 100644 --- a/signac_dashboard/modules/plotly_viewer.py +++ b/signac_dashboard/modules/plotly_viewer.py @@ -66,9 +66,8 @@ def plotly_args_function(project): callable provides one for each card. :type name: str :param plotly_args: A callable that accepts a job (in the :code:`'JobContext'`) - or a project (in the :code:`'ProjectContext'`) and returns an iterable. Each - element will constitute a new card and will be composed of a tuple of three - elements: the card title, the plotly data and the plotly layout specification. + or a project (in the :code:`'ProjectContext'`) and returns a tuple of two + elements: the plotly data and the plotly layout specification, respectively. :type plotly_args: callable :param context: Supports :code:`'JobContext'` and :code:`'ProjectContext'`. :type context: str From 121578f9b8d0671b9ef0b4a920ed9f5596414ee7 Mon Sep 17 00:00:00 2001 From: Javier Barbero Date: Tue, 29 Nov 2022 15:47:36 +0100 Subject: [PATCH 19/21] Fix PlotlyViewer for old signac API The Project.id and Job.id properties were not available in signac 1.0.0. The fix changes it to use the get_id() method in both cases. --- signac_dashboard/modules/plotly_viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signac_dashboard/modules/plotly_viewer.py b/signac_dashboard/modules/plotly_viewer.py index 0ea2172e..cd76719f 100644 --- a/signac_dashboard/modules/plotly_viewer.py +++ b/signac_dashboard/modules/plotly_viewer.py @@ -102,7 +102,7 @@ def get_cards(self, job_or_project): "name": self.name, "content": render_template( self.template, - jobid=job_or_project.id, + jobid=job_or_project.get_id(), endpoint=self.arguments_endpoint(), ), } From 3c32907a9c97b57dd855a262acb1059152224503 Mon Sep 17 00:00:00 2001 From: Corwin Kerr Date: Sat, 19 Aug 2023 15:25:23 -0400 Subject: [PATCH 20/21] Partial fix for getting job id --- signac_dashboard/modules/plotly_viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signac_dashboard/modules/plotly_viewer.py b/signac_dashboard/modules/plotly_viewer.py index c2cf2b17..ace01c1a 100644 --- a/signac_dashboard/modules/plotly_viewer.py +++ b/signac_dashboard/modules/plotly_viewer.py @@ -102,7 +102,7 @@ def get_cards(self, job_or_project): "name": self.name, "content": render_template( self.template, - jobid=job_or_project.get_id(), + jobid=job_or_project.id, # will not work for project anymore endpoint=self.arguments_endpoint(), ), } From 89a5c3d74871ede466214b475a2192ef03f83b79 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 18:42:00 +0000 Subject: [PATCH 21/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- signac_dashboard/modules/plotly_viewer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/signac_dashboard/modules/plotly_viewer.py b/signac_dashboard/modules/plotly_viewer.py index ace01c1a..f4dfa460 100644 --- a/signac_dashboard/modules/plotly_viewer.py +++ b/signac_dashboard/modules/plotly_viewer.py @@ -86,7 +86,6 @@ def __init__( template="cards/plotly_viewer.html", **kwargs, ): - super().__init__( name=name, context=context, @@ -102,7 +101,7 @@ def get_cards(self, job_or_project): "name": self.name, "content": render_template( self.template, - jobid=job_or_project.id, # will not work for project anymore + jobid=job_or_project.id, # will not work for project anymore endpoint=self.arguments_endpoint(), ), }