-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
439 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Visual reporting tools for inspecting phys2bids workflow outputs.""" |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
html, body { | ||
margin: 0; | ||
padding: 0; | ||
font-family: 'Lato', sans-serif; | ||
overflow-x: hidden; | ||
overflow-y: scroll; | ||
} | ||
* { | ||
box-sizing: border-box; | ||
} | ||
|
||
.header { | ||
background: linear-gradient(90deg, rgba(0,240,141,1) 0%, rgba(0,73,133,1) 100%); | ||
height: 70px; | ||
width: 100%; | ||
position: fixed; | ||
overflow: hidden; | ||
margin: 0; | ||
z-index: 100; | ||
} | ||
|
||
.header a, span { | ||
color: white; | ||
text-decoration: none; | ||
font-weight: 700; | ||
} | ||
|
||
.header_logo { | ||
display: inline-block; | ||
float: left; | ||
} | ||
|
||
.header_logo img{ | ||
height: 50px; | ||
top: 0; | ||
left: 0; | ||
padding-top: 15px; | ||
} | ||
|
||
.header_links { | ||
top: 0; | ||
left: 0; | ||
padding-top: 25px; | ||
margin-left: 20px; | ||
margin-right: 20px; | ||
float: left; | ||
display: inline-block; | ||
} | ||
.clear { | ||
clear: both; | ||
} | ||
|
||
.content { | ||
margin-top: 100px; | ||
display: flex; | ||
width: 100%; | ||
} | ||
|
||
.tree { | ||
margin-left: 50px; | ||
margin-right: 50px; | ||
flex: 0.5; | ||
min-width: 300px; | ||
float: left; | ||
} | ||
|
||
.tree_text { | ||
margin-top: 10px; | ||
margin-bottom: 70px; | ||
width: 100%; | ||
} | ||
|
||
.bk-root { | ||
display: inline-block; | ||
margin-top: 10px; | ||
width: 100%; | ||
} | ||
|
||
.bokeh_plots { | ||
margin-left: 50px; | ||
margin-right: 50px; | ||
flex: 1; | ||
min-width: 500px; | ||
float: left; | ||
} | ||
|
||
@media screen and (max-width: 600px) { | ||
.content { | ||
flex-wrap: wrap; | ||
} | ||
.tree { | ||
flex-basis: 100%; | ||
} | ||
.bokeh_plots { | ||
flex-basis: 100%; | ||
} | ||
} | ||
|
||
.main{ | ||
margin-top: 100px; | ||
margin-left: 100px; | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
"""Reporting functionality for phys2bids.""" | ||
import sys | ||
from distutils.dir_util import copy_tree | ||
from os.path import join | ||
from pathlib import Path | ||
from string import Template | ||
from bokeh.plotting import figure, ColumnDataSource | ||
from bokeh.embed import components | ||
from bokeh.layouts import gridplot | ||
|
||
from phys2bids import _version | ||
|
||
|
||
def _save_as_html(log_html_path, log_content, qc_html_path): | ||
""" | ||
Save an HTML report out to a file. | ||
Parameters | ||
---------- | ||
log_html_path : str | ||
Body for HTML report with embedded figures | ||
log_content: str | ||
String containing the logs generated by phys2bids | ||
qc_html_path : str | ||
Path to the quality check section of the report | ||
Returns | ||
------- | ||
html: HTML code of the report | ||
Outcome | ||
------- | ||
Saves the html file | ||
""" | ||
resource_path = Path(__file__).resolve().parent | ||
head_template_name = 'report_log_template.html' | ||
head_template_path = resource_path.joinpath(head_template_name) | ||
with open(str(head_template_path), 'r') as head_file: | ||
head_tpl = Template(head_file.read()) | ||
|
||
html = head_tpl.substitute(version=_version.get_versions()['version'], | ||
log_html_path=log_html_path, log_content=log_content, | ||
qc_html_path=qc_html_path) | ||
return html | ||
|
||
|
||
def _update_fpage_template(tree_string, bokeh_id, bokeh_js, log_html_path, qc_html_path): | ||
""" | ||
Populate a report with content. | ||
Parameters | ||
---------- | ||
tree_string: str | ||
Tree of files in directory. | ||
bokeh_id : str | ||
HTML div created by bokeh.embed.components | ||
bokeh_js : str | ||
Javascript created by bokeh.embed.components | ||
log_html_path : str | ||
Path to the log section of the report | ||
qc_html_path : str | ||
Path to the quality check section of the report | ||
Returns | ||
------- | ||
body : Body for HTML report with embedded figures | ||
""" | ||
resource_path = Path(__file__).resolve().parent | ||
|
||
body_template_name = 'report_plots_template.html' | ||
body_template_path = resource_path.joinpath(body_template_name) | ||
with open(str(body_template_path), 'r') as body_file: | ||
body_tpl = Template(body_file.read()) | ||
body = body_tpl.substitute(tree=tree_string, | ||
content=bokeh_id, | ||
javascript=bokeh_js, | ||
version=_version.get_versions()['version'], | ||
log_html_path=log_html_path, | ||
qc_html_path=qc_html_path) | ||
return body | ||
|
||
|
||
def _generate_file_tree(out_dir): | ||
""" | ||
Populate a report with content. | ||
Parameters | ||
---------- | ||
outdir : str | ||
Path to the output directory | ||
Returns | ||
------- | ||
tree_string: String with the tree of files in directory | ||
""" | ||
# prefix components: | ||
space = ' ' | ||
branch = '│ ' | ||
# pointers: | ||
tee = '├── ' | ||
last = '└── ' | ||
|
||
def tree(dir_path: Path, prefix: str = ''): | ||
"""Generate tree structure. | ||
Given a directory Path object | ||
will yield a visual tree structure line by line | ||
with each line prefixed by the same characters | ||
from https://stackoverflow.com/questions/9727673/list-directory-tree-structure-in-python | ||
""" | ||
contents = list(dir_path.iterdir()) | ||
# contents each get pointers that are ├── with a final └── : | ||
pointers = [tee] * (len(contents) - 1) + [last] | ||
for pointer, path in zip(pointers, contents): | ||
yield prefix + pointer + path.name | ||
if path.is_dir(): # extend the prefix and recurse: | ||
extension = branch if pointer == tee else space | ||
# i.e. space because last, └── , above so no more | | ||
yield from tree(path, prefix=prefix + extension) | ||
|
||
tree_string = '' | ||
for line in tree(Path(out_dir)): | ||
tree_string += line + '<br>' | ||
return tree_string | ||
|
||
|
||
def _generate_bokeh_plots(phys_in, figsize=(250, 500)): | ||
""" | ||
Plot all the channels for visualizations as linked line plots for dynamic report. | ||
Parameters | ||
---------- | ||
phys_in: BlueprintInput object | ||
Object returned by BlueprintInput class | ||
figsize: tuple | ||
Size of the figure expressed as (size_x, size_y), | ||
Default is 250x750px | ||
Outcome | ||
------- | ||
Creates new plot with path specified in outfile. | ||
See Also | ||
-------- | ||
https://phys2bids.readthedocs.io/en/latest/howto.html | ||
""" | ||
colors = ['#ff7a3c', '#008eba', '#ff96d3', '#3c376b', '#ffd439'] | ||
|
||
time = phys_in.timeseries.T[0] # assumes first phys_in.timeseries is time | ||
ch_num = len(phys_in.ch_name) | ||
if ch_num > len(colors): | ||
colors *= 2 | ||
|
||
downsample = int(phys_in.freq / 100) | ||
plot_list = [] | ||
for row, timeser in enumerate(phys_in.timeseries.T[1:]): | ||
# build a data source for each plot, with only the data + index (time) | ||
# for the purpose of reporting, data is downsampled 10x | ||
# doesn't make much of a difference to the naked eye, fine for reports | ||
source = ColumnDataSource(data=dict( | ||
x=time[::downsample], | ||
y=timeser[::downsample])) | ||
|
||
i = row + 1 | ||
|
||
tools = ['wheel_zoom,pan,reset'] | ||
q = figure(plot_height=figsize[0], plot_width=figsize[1], | ||
tools=tools, | ||
title=f' Channel {i}: {phys_in.ch_name[i]}', | ||
sizing_mode='stretch_both') | ||
q.line('x', 'y', color=colors[i - 1], alpha=0.9, source=source) | ||
q.xaxis.axis_label = 'Time (s)' | ||
# hovertool commented for posterity because I (KB) will be triumphant | ||
# eventually | ||
# q.add_tools(HoverTool(tooltips=[ | ||
# (phys_in.ch_name[i], '@y{0.000} ' + phys_in.units[i]), | ||
# ('HELP', '100 :D') | ||
# ], mode='vline')) | ||
plot_list.append([q]) | ||
p = gridplot(plot_list, toolbar_location='right', | ||
plot_height=250, plot_width=750, | ||
merge_tools=True) | ||
script, div = components(p) | ||
return script, div | ||
|
||
|
||
def generate_report(out_dir, log_path, phys_in): | ||
""" | ||
Plot all the channels for visualizations as linked line plots for dynamic report. | ||
Parameters | ||
---------- | ||
out_dir : str | ||
File path to a completed phys2bids output directory | ||
log_path: path | ||
Path to the logged output of phys2bids | ||
phys_in: BlueprintInput object | ||
Object returned by BlueprintInput class | ||
Outcome | ||
------- | ||
Creates new plot with path specified in outfile. | ||
See Also | ||
-------- | ||
https://phys2bids.readthedocs.io/en/latest/howto.html | ||
""" | ||
# Copy assets into output folder | ||
pkgdir = sys.modules['phys2bids'].__path__[0] | ||
assets_path = join(pkgdir, 'reporting', 'assets') | ||
copy_tree(assets_path, join(out_dir, 'assets')) | ||
|
||
# Read log | ||
with open(log_path, 'r') as f: | ||
log_content = f.read() | ||
|
||
log_content = log_content.replace('\n', '<br>') | ||
log_html_path = join(out_dir, 'phys2bids_report_log.html') | ||
qc_html_path = join(out_dir, 'phys2bids_report.html') | ||
|
||
html = _save_as_html(log_html_path, log_content, qc_html_path) | ||
|
||
with open(log_html_path, 'wb') as f: | ||
f.write(html.encode('utf-8')) | ||
|
||
# Read in output directory structure & create tree | ||
tree_string = _generate_file_tree(out_dir) | ||
bokeh_js, bokeh_div = _generate_bokeh_plots(phys_in, figsize=(250, 750)) | ||
html = _update_fpage_template(tree_string, bokeh_div, bokeh_js, log_html_path, qc_html_path) | ||
|
||
with open(qc_html_path, 'wb') as f: | ||
f.write(html.encode('utf-8')) |
Oops, something went wrong.