Skip to content

Commit d1b1678

Browse files
first commit, WIP
1 parent 5016786 commit d1b1678

File tree

7 files changed

+325
-30
lines changed

7 files changed

+325
-30
lines changed

.gitignore

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#data files
2+
*.csv
3+
*.xlsx
4+
*.xls
5+
*.json
6+
7+
#log files
8+
*.log
9+
tests/*.json
10+
11+
# pycharm project
12+
.idea/
13+
# vscode settings
14+
.vscode/
15+
# pbs log files
16+
*.pbs.*
17+
# local env folder
18+
env/
19+
20+
# python runtime generated
21+
__pycache__/
22+
*.py[cod]
23+
*$py.class
24+
25+
# files generated by setup.py
26+
build/
27+
dist/
28+
*.egg*/
29+
30+
# file generated by pytest
31+
*.coverage

cookiecutter_replay.json

Lines changed: 0 additions & 30 deletions
This file was deleted.

plotme/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import os
2+
3+
__version__ = '0.1.0'
4+
5+
os.environ['PLOTME_VERSION'] = __version__

plotme/helper.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
""" Copyright (c) 3M, 2020. All rights reserved.
2+
helper.py contains data related helper functions including import,
3+
verification, manipulation, logging, and upload used by roadrunner.py
4+
"""
5+
import collections.abc
6+
import datetime as dt
7+
import importlib
8+
import logging
9+
import os
10+
from pathlib import Path
11+
12+
13+
def start_logging(log_folder='', log_level=logging.INFO, file_name='',
14+
date_time_stamp=''):
15+
"""
16+
Sets up the log file using python's built in logging
17+
Parameters
18+
----------
19+
log_folder: str
20+
relative path to experiment folder
21+
log_level: int
22+
integer representing logging severity level
23+
file_name: str
24+
experiment name
25+
date_time_stamp: str
26+
date + time stamp
27+
"""
28+
29+
# in case it is already running shutdown the logging library and reload it
30+
# basicConfig only works the 1st time it's called unless you do this
31+
logging.shutdown()
32+
importlib.reload(logging)
33+
34+
if len(date_time_stamp) == 0:
35+
date_time_stamp = dt.datetime.now().strftime('%Y%m%d_%H.%M.%S')
36+
if len(log_folder) > 1:
37+
file_name = os.path.join(log_folder, f"{date_time_stamp}_{file_name}.log")
38+
Path(file_name).parent.mkdir(parents=True, exist_ok=True)
39+
else:
40+
file_name = 'log.log'
41+
logging_format = '%(asctime)s %(levelname)s %(message)s'
42+
logging.basicConfig(filename=file_name, level=log_level, format=logging_format)
43+
44+
# Get the top-level logger object
45+
log = logging.getLogger()
46+
# make the log print to the console.
47+
console = logging.StreamHandler()
48+
log.addHandler(console)
49+
50+
logging.debug('debug logging active')
51+
logging.info('info logging active')
52+
logging.warning('warning logging active')
53+
logging.error('error logging active')
54+
55+
56+
def deep_update(to_update, update):
57+
"""
58+
Update a nested dictionary or similar mapping.
59+
Modifies ``source`` in place.
60+
Parameters
61+
----------
62+
to_update: dict
63+
dictionary to update in place
64+
update: dict
65+
new items for dict
66+
Returns
67+
-------
68+
to_update: dict
69+
dictionary with updated values, needed for recursion?
70+
"""
71+
for k, v in update.items():
72+
if isinstance(v, collections.abc.Mapping):
73+
to_update[k] = deep_update(to_update.get(k, {}), v)
74+
else:
75+
to_update[k] = v
76+
return to_update
77+
78+
79+
def try_for(func, args=[], iterations=3):
80+
"""
81+
for loop with try and error handling around any method
82+
Parameters
83+
----------
84+
func: method
85+
function
86+
iterations: int
87+
number of iterations to run
88+
args: tuple
89+
arguments
90+
Returns
91+
-------
92+
output: list
93+
output objects or variables
94+
"""
95+
96+
success = False
97+
for x in range(1, iterations + 1):
98+
try:
99+
output = func(*args)
100+
success = True
101+
break
102+
except Exception as e:
103+
logging.error(e, exc_info=True)
104+
logging.warning(f'try_for: exception during attempt {x}, trying '
105+
f'{func.__name__}() again')
106+
107+
if success is False:
108+
raise Exception(f"try_for: final attempt to execute {func.__name__}()"
109+
" failed, exiting")
110+
111+
return output
112+

plotme/plotme.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import argparse
2+
import glob
3+
import json
4+
import logging
5+
from pathlib import Path
6+
import sys
7+
8+
import plotly.graph_objects as go
9+
import plotly.io as pio
10+
11+
from dirhash import dirhash
12+
from plotly.subplots import make_subplots
13+
14+
15+
import from_xlsx
16+
import helper
17+
18+
19+
def main(kwargs={}):
20+
"""
21+
generate multiple plots, globs through data_root to find plot_info files, checks previous hash against current hash,
22+
runs single_plot
23+
24+
Parameters
25+
----------
26+
kwargs: dictionary
27+
key word arguments
28+
29+
"""
30+
31+
plot_info_file = kwargs.get('plot_info', 'plot_info.json')
32+
33+
data_root = Path(kwargs.get('data_root', Path.home()))
34+
# TODO: add setting for depth of glob
35+
plot_info_files = data_root.glob(f"**/*{plot_info_file}")
36+
for file in plot_info_files:
37+
dir_path = file.parent
38+
39+
# hashing folder recursively
40+
current_hash = dirhash(dir_path, "md5", ignore=["previous_hash", "*.html", "*.png"])
41+
hash_file_path = Path(dir_path, "previous_hash")
42+
if hash_file_path.exists():
43+
with open(hash_file_path) as txt_file:
44+
previous_hash = txt_file.read()
45+
else:
46+
previous_hash = ''
47+
48+
if current_hash == previous_hash:
49+
logging.info(f"no changes detected, skipping {dir_path}")
50+
else:
51+
logging.info(f'loading plot settings from {file}')
52+
with open(file) as json_file:
53+
plot_info = json.load(json_file)
54+
plot_info['plot_dir'] = dir_path
55+
single_plot(plot_info)
56+
with open(hash_file_path, "w+") as txt_file:
57+
txt_file.write(current_hash)
58+
59+
return 0
60+
61+
62+
def single_plot(kwargs={}):
63+
64+
plot_dir = kwargs.get('plot_dir', Path.home())
65+
title = kwargs.get('title', 'plotme plot')
66+
x_id = kwargs.get('x_id', 'x_id not set')
67+
x_title = kwargs.get('x_title', x_id) # use x_id if no label is given
68+
y_id = kwargs.get('y_id', 'y_id not set')
69+
y_title = kwargs.get('y_title', y_id)
70+
71+
split_on = kwargs.get('split_on', '_') # used to split x_id out of file name
72+
exclude_from_trace_label = kwargs.get('exclude_from_trace_label') # remove this
73+
constant_lines = kwargs.get('constant_lines')
74+
constant_lines_x = constant_lines.get('x') # list
75+
constant_lines_y = constant_lines.get('y') # list
76+
add_line = True
77+
error_bar_percent_of_y = 5
78+
enable_error_bars = False
79+
80+
folders = glob.glob(f"{plot_dir}/*/")
81+
x_dict = {}
82+
if add_line:
83+
y_dict = {'y=1': [1, 1]}
84+
else:
85+
y_dict = {}
86+
x_max = 0
87+
for folder in folders:
88+
directory = Path(folder)
89+
if directory.name == 'ignore':
90+
continue
91+
if exclude_from_trace_label not in directory.name:
92+
continue
93+
else:
94+
if exclude_from_trace_label:
95+
d_name_part = directory.name.strip(exclude_from_trace_label)
96+
else:
97+
d_name_part = directory.name
98+
x, y = from_xlsx.collect_1_x_per_file(directory, x_id, y_id, split_on)
99+
# if not x:
100+
# x, y = collect_from_pkl(directory, x_id, y_id)
101+
x_max = max(max(x), x_max)
102+
x_dict.update({d_name_part: x})
103+
y_dict.update({d_name_part: y})
104+
105+
if add_line:
106+
x_dict['y=1'] = [0, x_max]
107+
108+
pio.templates.default = "plotly_white"
109+
fig = make_subplots(rows=1, cols=1, shared_yaxes=True,
110+
x_title=x_title, y_title=y_title)
111+
112+
for i, folder in enumerate(x_dict, start=1):
113+
if folder == 'y=1':
114+
mode = 'lines'
115+
else:
116+
mode = 'markers'
117+
fig.add_trace(go.Scatter(name=folder, mode=mode, x=x_dict[folder], y=y_dict[folder], marker_symbol=i - 1,
118+
error_y=dict(
119+
type='percent', # value of error bar given as percentage of y value
120+
value=error_bar_percent_of_y,
121+
visible=enable_error_bars)
122+
), row=1, col=1)
123+
124+
fig.update_layout(height=600, width=1000, title_text=title)
125+
126+
fig.write_html(str(Path(plot_dir, f"{y_title} vs {x_title}.html")))
127+
fig.write_image(str(Path(plot_dir, f"{y_title} vs {x_title}.png")))
128+
fig.show()
129+
130+
131+
if __name__ == "__main__":
132+
133+
# parse the arguments
134+
parser = argparse.ArgumentParser(description='automates plotting of tabular data')
135+
136+
parser.add_argument('-s', action="store", default="", type=str,
137+
help="Specify data directory")
138+
parser.add_argument('-gt', action="store_true",
139+
help="generate a template")
140+
141+
args = parser.parse_args()
142+
143+
helper.start_logging(log_level=logging.INFO)
144+
try:
145+
main(args)
146+
# single_plot()
147+
except Exception as e:
148+
logging.exception("Fatal error in main")
149+
logging.error(e, exc_info=True)
150+
sys.exit(1)

setup.cfg

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[metadata]
2+
platforms =
3+
Linux
4+
Windows
5+
classifiers =
6+
Intended Audience :: Developers
7+
Operating System :: POSIX :: Linux
8+
Operating System :: Microsoft :: Windows
9+
Programming Language :: Python
10+
Programming Language :: Python :: 3
11+
Programming Language :: Python :: 3.7
12+
Programming Language :: Python :: 3.8
13+
Programming Language :: Python :: 3.9
14+
Programming Language :: Python :: 3.10
15+
Programming Language :: Python :: Implementation :: CPython
16+
Programming Language :: Python :: Implementation :: PyPy
17+
18+
[options.entry_points]
19+
console_scripts =
20+
plotme = plotme.plotme:main
21+
22+
[bdist_wheel]
23+
universal = 1

tests/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import sys
2+
from pathlib import Path
3+
4+
sys.path.append(str(Path('model/hatchme')))

0 commit comments

Comments
 (0)