-
Notifications
You must be signed in to change notification settings - Fork 6
Add interactive plots with Plotly #163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 13 commits
0a50ca2
23bdaaa
73d0ee4
406710d
ba7edd8
954a603
b4985b5
c971876
74c3ce2
2e7aea5
d62d08a
59b37bc
a898191
02df95b
e4bd48d
d271eee
226111e
24a9aa2
121578f
152d94a
3c32907
89a5c3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Matplotlib Plots Example | ||
|
|
||
| To run this example, call `python init.py` and then `python dashboard.py run`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| signac.rc | ||
| signac_project_document.json | ||
| workspace/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| # Plots Example | ||
| # Plotly Plots Example | ||
|
|
||
| To run this example, call `python init.py` and then `python dashboard.py run`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| # 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 flask_login | ||
| 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 | ||
|
|
||
|
|
||
| class PlotViewer(Module): | ||
|
javierbg marked this conversation as resolved.
Outdated
|
||
| """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 | ||
| parameters for the data and layout, refer to the `Plotly JS documentation | ||
| <https://plotly.com/javascript/>`_. | ||
|
|
||
| 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. | ||
| :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", | ||
| plotly_args: Callable[ | ||
| [Union[Job, Project]], Iterable[Tuple[str, List[Dict], Dict]] | ||
| ] = lambda _: [], | ||
| context="JobContext", | ||
| template="cards/plot_viewer.html", | ||
| **kwargs, | ||
| ): | ||
|
|
||
| super().__init__( | ||
| name=name, | ||
| context=context, | ||
| template=template, | ||
| **kwargs, | ||
| ) | ||
| self.plotly_args = plotly_args | ||
|
|
||
| def get_cards(self, job_or_project): | ||
| return [ | ||
| { | ||
| "name": title if title else self.name, | ||
| "content": render_template( | ||
| self.template, | ||
| jobid=job_or_project.id, | ||
| plotly_data=data, | ||
| plotly_layout=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/<path:filename>") | ||
| @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"] | ||
| for assetfile in assets: | ||
| dashboard.register_module_asset( | ||
| { | ||
| "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}) | ||
|
javierbg marked this conversation as resolved.
Outdated
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| <div class="plot_viewer" | ||
| data-plotly-data='{{ plotly_data | tojson }}' | ||
| data-plotly-layout='{{ plotly_layout | tojson }}'> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| function draw_plot(element) { | ||
| data = JSON.parse(element.getAttribute("data-plotly-data")); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe the card HTML can contain a REST endpoint, and fetch the data with a second call? That way we don't have to generate and include all the raw data in the HTML page. This should help a lot with page responsiveness. You can see examples of REST calls in other modules:
If you need help with this, let me know.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have implemented this in 226111e. The page loading is way more responsive now. Still, I'm a total newbie with Flask, so a thorough review is needed. My main concerns are:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| layout = JSON.parse(element.getAttribute("data-plotly-layout")); | ||
|
|
||
| Plotly.newPlot(element, data, layout); | ||
| } | ||
|
|
||
| $(document).on('turbolinks:load', function() { | ||
| $('.plot_viewer').each((index, element) => { | ||
| draw_plot(element); | ||
| }); | ||
| }) | ||
Uh oh!
There was an error while loading. Please reload this page.