Skip to content

Commit

Permalink
Move PlotlyView argument passing functionality to REST endpoint
Browse files Browse the repository at this point in the history
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
  • Loading branch information
javierbg committed Nov 29, 2022
1 parent d271eee commit 226111e
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 45 deletions.
23 changes: 13 additions & 10 deletions examples/plots/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand All @@ -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"]),
Expand All @@ -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()
85 changes: 60 additions & 25 deletions signac_dashboard/modules/plotly_viewer.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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/<path:filename>")
@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/<path:filename>")
@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"
4 changes: 2 additions & 2 deletions signac_dashboard/templates/cards/plotly_viewer.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="plotly_viewer"
data-plotly-data='{{ plotly_data | tojson }}'
data-plotly-layout='{{ plotly_layout | tojson }}'>
data-endpoint="{{ endpoint }}"
data-jobid="{{ jobid }}">
</div>
15 changes: 7 additions & 8 deletions signac_dashboard/templates/plotly_viewer/js/plotly_viewer.js
Original file line number Diff line number Diff line change
@@ -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)
})
});
})

0 comments on commit 226111e

Please sign in to comment.