Skip to content

Commit 661be73

Browse files
me-picsmoia
authored andcommitted
rm redondant lines
2 parents d796fe1 + 7477647 commit 661be73

File tree

11 files changed

+439
-2
lines changed

11 files changed

+439
-2
lines changed

phys2bids/cli/run.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,16 @@ def _get_parser():
189189
help="Only print warnings to log file. Default is False.",
190190
default=False,
191191
)
192+
optional.add_argument(
193+
"-report", "--report",
194+
dest="make_report",
195+
action="store_true",
196+
help="Generate a report with the data and generated folder structure. "
197+
"Default is False.",
198+
default=False,
199+
)
192200
optional.add_argument("-v", "--version", action="version", version=("%(prog)s " + __version__))
193-
201+
194202
parser._action_groups.append(optional)
195203

196204
return parser

phys2bids/phys2bids.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from phys2bids import _version, bids, utils, viz
3939
from phys2bids.cli.run import _get_parser
4040
from phys2bids.physio_obj import BlueprintOutput
41+
from phys2bids.reporting.html_report import generate_report
4142
from phys2bids.slice4phys import slice4phys
4243

4344
from . import __version__
@@ -145,6 +146,7 @@ def phys2bids(
145146
pad=9,
146147
ch_name=[],
147148
yml="",
149+
make_report=False,
148150
debug=False,
149151
quiet=False,
150152
):
@@ -538,6 +540,10 @@ def phys2bids(
538540
),
539541
)
540542

543+
# Only generate report if specified by the user
544+
if make_report:
545+
generate_report(conversion_path, logname, phys_out[key])
546+
541547

542548
def _main(argv=None):
543549
options = _get_parser().parse_args(argv)

phys2bids/reporting/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Visual reporting tools for inspecting phys2bids workflow outputs."""
40.5 KB
Loading
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
html, body {
2+
margin: 0;
3+
padding: 0;
4+
font-family: 'Lato', sans-serif;
5+
overflow-x: hidden;
6+
overflow-y: scroll;
7+
}
8+
* {
9+
box-sizing: border-box;
10+
}
11+
12+
.header {
13+
background: linear-gradient(90deg, rgba(0,240,141,1) 0%, rgba(0,73,133,1) 100%);
14+
height: 70px;
15+
width: 100%;
16+
position: fixed;
17+
overflow: hidden;
18+
margin: 0;
19+
z-index: 100;
20+
}
21+
22+
.header a, span {
23+
color: white;
24+
text-decoration: none;
25+
font-weight: 700;
26+
}
27+
28+
.header_logo {
29+
display: inline-block;
30+
float: left;
31+
}
32+
33+
.header_logo img{
34+
height: 50px;
35+
top: 0;
36+
left: 0;
37+
padding-top: 15px;
38+
}
39+
40+
.header_links {
41+
top: 0;
42+
left: 0;
43+
padding-top: 25px;
44+
margin-left: 20px;
45+
margin-right: 20px;
46+
float: left;
47+
display: inline-block;
48+
}
49+
.clear {
50+
clear: both;
51+
}
52+
53+
.content {
54+
margin-top: 100px;
55+
display: flex;
56+
width: 100%;
57+
}
58+
59+
.tree {
60+
margin-left: 50px;
61+
margin-right: 50px;
62+
flex: 0.5;
63+
min-width: 300px;
64+
float: left;
65+
}
66+
67+
.tree_text {
68+
margin-top: 10px;
69+
margin-bottom: 70px;
70+
width: 100%;
71+
}
72+
73+
.bk-root {
74+
display: inline-block;
75+
margin-top: 10px;
76+
width: 100%;
77+
}
78+
79+
.bokeh_plots {
80+
margin-left: 50px;
81+
margin-right: 50px;
82+
flex: 1;
83+
min-width: 500px;
84+
float: left;
85+
}
86+
87+
@media screen and (max-width: 600px) {
88+
.content {
89+
flex-wrap: wrap;
90+
}
91+
.tree {
92+
flex-basis: 100%;
93+
}
94+
.bokeh_plots {
95+
flex-basis: 100%;
96+
}
97+
}
98+
99+
.main{
100+
margin-top: 100px;
101+
margin-left: 100px;
102+
}
38.9 KB
Loading

phys2bids/reporting/html_report.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
"""Reporting functionality for phys2bids."""
2+
import sys
3+
from distutils.dir_util import copy_tree
4+
from os.path import join
5+
from pathlib import Path
6+
from string import Template
7+
from bokeh.plotting import figure, ColumnDataSource
8+
from bokeh.embed import components
9+
from bokeh.layouts import gridplot
10+
11+
from phys2bids import _version
12+
13+
14+
def _save_as_html(log_html_path, log_content, qc_html_path):
15+
"""
16+
Save an HTML report out to a file.
17+
18+
Parameters
19+
----------
20+
log_html_path : str
21+
Body for HTML report with embedded figures
22+
log_content: str
23+
String containing the logs generated by phys2bids
24+
qc_html_path : str
25+
Path to the quality check section of the report
26+
27+
Returns
28+
-------
29+
html: HTML code of the report
30+
31+
Outcome
32+
-------
33+
Saves the html file
34+
"""
35+
resource_path = Path(__file__).resolve().parent
36+
head_template_name = 'report_log_template.html'
37+
head_template_path = resource_path.joinpath(head_template_name)
38+
with open(str(head_template_path), 'r') as head_file:
39+
head_tpl = Template(head_file.read())
40+
41+
html = head_tpl.substitute(version=_version.get_versions()['version'],
42+
log_html_path=log_html_path, log_content=log_content,
43+
qc_html_path=qc_html_path)
44+
return html
45+
46+
47+
def _update_fpage_template(tree_string, bokeh_id, bokeh_js, log_html_path, qc_html_path):
48+
"""
49+
Populate a report with content.
50+
51+
Parameters
52+
----------
53+
tree_string: str
54+
Tree of files in directory.
55+
bokeh_id : str
56+
HTML div created by bokeh.embed.components
57+
bokeh_js : str
58+
Javascript created by bokeh.embed.components
59+
log_html_path : str
60+
Path to the log section of the report
61+
qc_html_path : str
62+
Path to the quality check section of the report
63+
64+
Returns
65+
-------
66+
body : Body for HTML report with embedded figures
67+
"""
68+
resource_path = Path(__file__).resolve().parent
69+
70+
body_template_name = 'report_plots_template.html'
71+
body_template_path = resource_path.joinpath(body_template_name)
72+
with open(str(body_template_path), 'r') as body_file:
73+
body_tpl = Template(body_file.read())
74+
body = body_tpl.substitute(tree=tree_string,
75+
content=bokeh_id,
76+
javascript=bokeh_js,
77+
version=_version.get_versions()['version'],
78+
log_html_path=log_html_path,
79+
qc_html_path=qc_html_path)
80+
return body
81+
82+
83+
def _generate_file_tree(out_dir):
84+
"""
85+
Populate a report with content.
86+
87+
Parameters
88+
----------
89+
outdir : str
90+
Path to the output directory
91+
92+
Returns
93+
-------
94+
tree_string: String with the tree of files in directory
95+
"""
96+
# prefix components:
97+
space = ' '
98+
branch = '│ '
99+
# pointers:
100+
tee = '├── '
101+
last = '└── '
102+
103+
def tree(dir_path: Path, prefix: str = ''):
104+
"""Generate tree structure.
105+
106+
Given a directory Path object
107+
will yield a visual tree structure line by line
108+
with each line prefixed by the same characters
109+
110+
from https://stackoverflow.com/questions/9727673/list-directory-tree-structure-in-python
111+
"""
112+
contents = list(dir_path.iterdir())
113+
# contents each get pointers that are ├── with a final └── :
114+
pointers = [tee] * (len(contents) - 1) + [last]
115+
for pointer, path in zip(pointers, contents):
116+
yield prefix + pointer + path.name
117+
if path.is_dir(): # extend the prefix and recurse:
118+
extension = branch if pointer == tee else space
119+
# i.e. space because last, └── , above so no more |
120+
yield from tree(path, prefix=prefix + extension)
121+
122+
tree_string = ''
123+
for line in tree(Path(out_dir)):
124+
tree_string += line + '<br>'
125+
return tree_string
126+
127+
128+
def _generate_bokeh_plots(phys_in, figsize=(250, 500)):
129+
"""
130+
Plot all the channels for visualizations as linked line plots for dynamic report.
131+
132+
Parameters
133+
----------
134+
phys_in: BlueprintInput object
135+
Object returned by BlueprintInput class
136+
figsize: tuple
137+
Size of the figure expressed as (size_x, size_y),
138+
Default is 250x750px
139+
140+
Outcome
141+
-------
142+
Creates new plot with path specified in outfile.
143+
144+
See Also
145+
--------
146+
https://phys2bids.readthedocs.io/en/latest/howto.html
147+
"""
148+
colors = ['#ff7a3c', '#008eba', '#ff96d3', '#3c376b', '#ffd439']
149+
150+
time = phys_in.timeseries.T[0] # assumes first phys_in.timeseries is time
151+
ch_num = len(phys_in.ch_name)
152+
if ch_num > len(colors):
153+
colors *= 2
154+
155+
downsample = int(phys_in.freq / 100)
156+
plot_list = []
157+
for row, timeser in enumerate(phys_in.timeseries.T[1:]):
158+
# build a data source for each plot, with only the data + index (time)
159+
# for the purpose of reporting, data is downsampled 10x
160+
# doesn't make much of a difference to the naked eye, fine for reports
161+
source = ColumnDataSource(data=dict(
162+
x=time[::downsample],
163+
y=timeser[::downsample]))
164+
165+
i = row + 1
166+
167+
tools = ['wheel_zoom,pan,reset']
168+
q = figure(plot_height=figsize[0], plot_width=figsize[1],
169+
tools=tools,
170+
title=f' Channel {i}: {phys_in.ch_name[i]}',
171+
sizing_mode='stretch_both')
172+
q.line('x', 'y', color=colors[i - 1], alpha=0.9, source=source)
173+
q.xaxis.axis_label = 'Time (s)'
174+
# hovertool commented for posterity because I (KB) will be triumphant
175+
# eventually
176+
# q.add_tools(HoverTool(tooltips=[
177+
# (phys_in.ch_name[i], '@y{0.000} ' + phys_in.units[i]),
178+
# ('HELP', '100 :D')
179+
# ], mode='vline'))
180+
plot_list.append([q])
181+
p = gridplot(plot_list, toolbar_location='right',
182+
plot_height=250, plot_width=750,
183+
merge_tools=True)
184+
script, div = components(p)
185+
return script, div
186+
187+
188+
def generate_report(out_dir, log_path, phys_in):
189+
"""
190+
Plot all the channels for visualizations as linked line plots for dynamic report.
191+
192+
Parameters
193+
----------
194+
out_dir : str
195+
File path to a completed phys2bids output directory
196+
log_path: path
197+
Path to the logged output of phys2bids
198+
phys_in: BlueprintInput object
199+
Object returned by BlueprintInput class
200+
201+
Outcome
202+
-------
203+
Creates new plot with path specified in outfile.
204+
205+
See Also
206+
--------
207+
https://phys2bids.readthedocs.io/en/latest/howto.html
208+
"""
209+
# Copy assets into output folder
210+
pkgdir = sys.modules['phys2bids'].__path__[0]
211+
assets_path = join(pkgdir, 'reporting', 'assets')
212+
copy_tree(assets_path, join(out_dir, 'assets'))
213+
214+
# Read log
215+
with open(log_path, 'r') as f:
216+
log_content = f.read()
217+
218+
log_content = log_content.replace('\n', '<br>')
219+
log_html_path = join(out_dir, 'phys2bids_report_log.html')
220+
qc_html_path = join(out_dir, 'phys2bids_report.html')
221+
222+
html = _save_as_html(log_html_path, log_content, qc_html_path)
223+
224+
with open(log_html_path, 'wb') as f:
225+
f.write(html.encode('utf-8'))
226+
227+
# Read in output directory structure & create tree
228+
tree_string = _generate_file_tree(out_dir)
229+
bokeh_js, bokeh_div = _generate_bokeh_plots(phys_in, figsize=(250, 750))
230+
html = _update_fpage_template(tree_string, bokeh_div, bokeh_js, log_html_path, qc_html_path)
231+
232+
with open(qc_html_path, 'wb') as f:
233+
f.write(html.encode('utf-8'))

0 commit comments

Comments
 (0)