├── cli_resolve ├── __init__.py ├── utils │ ├── __init__.py │ ├── menu_utils.py │ ├── resolution_utils.py │ ├── export_utils.py │ ├── input_utils.py │ └── graph_utils.py ├── __main__.py ├── menu.py ├── export.py └── resolution.py ├── data ├── resolutions.json ├── trips.json └── legacy │ └── nyr22.csv ├── images ├── minimaps.png ├── demo_logres.gif └── heatmap_with_trips.png ├── Pipfile ├── LICENSE ├── scripts ├── convert_legacy_resolutions.py └── backpopulate_data.py ├── README.md └── Pipfile.lock /cli_resolve/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resolutions.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cli_resolve/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/minimaps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snickerdoodle-codes/cli-resolve/HEAD/images/minimaps.png -------------------------------------------------------------------------------- /images/demo_logres.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snickerdoodle-codes/cli-resolve/HEAD/images/demo_logres.gif -------------------------------------------------------------------------------- /images/heatmap_with_trips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snickerdoodle-codes/cli-resolve/HEAD/images/heatmap_with_trips.png -------------------------------------------------------------------------------- /cli_resolve/__main__.py: -------------------------------------------------------------------------------- 1 | from menu import Menu 2 | 3 | 4 | def main(): 5 | while True: 6 | menu = Menu() 7 | menu.print_menu() 8 | 9 | 10 | if __name__ == "__main__": 11 | main() 12 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | pandas = "*" 8 | seaborn = "*" 9 | matplotlib = "*" 10 | 11 | [dev-packages] 12 | 13 | [requires] 14 | python_version = "3.10" 15 | -------------------------------------------------------------------------------- /cli_resolve/utils/menu_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | 4 | 5 | def go_home_message(delay=2): 6 | """ 7 | Takes the user back to the main menu after a small delay (for UX purposes) 8 | """ 9 | if delay > 0: 10 | print("*** Taking you back to main menu in 3, 2, 1...") 11 | sleep(delay) 12 | os.system('clear') 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 vickiwyang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli_resolve/menu.py: -------------------------------------------------------------------------------- 1 | from resolution import * 2 | from export import * 3 | 4 | 5 | class Menu: 6 | def __init__(self): 7 | self.options = { 8 | "1": { 9 | "text": "log resolutions", 10 | "function": log_resolutions, 11 | }, 12 | "2": { 13 | "text": "add resolution", 14 | "function": add_resolution, 15 | }, 16 | "3": { 17 | "text": "toggle active resolutions", 18 | "function": toggle_active_resolutions, 19 | }, 20 | "4": { 21 | "text": "export csv", 22 | "function": export_csv, 23 | }, 24 | "5": { 25 | "text": "export graph", 26 | "function": export_graph, 27 | } 28 | } 29 | 30 | def print_menu(self): 31 | os.system('clear') 32 | print(f"Welcome to ✨ Resolve ✨\n") 33 | for key, val in self.options.items(): 34 | print(f"{key} - {val['text']}") 35 | print() 36 | print("Enter 'q' at anytime to quit\n") 37 | command = handle_input(prompt="What would you like to do? (enter #): ") 38 | print() 39 | try: 40 | self.options[command]["function"]() 41 | except KeyError as e: 42 | print(f"{command} is not a valid command") 43 | sleep(1) 44 | 45 | -------------------------------------------------------------------------------- /data/trips.json: -------------------------------------------------------------------------------- 1 | { 2 | "1/27/2022": "King's Landing <-> Old Town", 3 | "1/28/2022": "", 4 | "1/29/2022": "", 5 | "1/30/2022": "", 6 | "1/31/2022": "", 7 | "2/1/2022": "", 8 | "2/2/2022": "", 9 | "2/3/2022": "", 10 | "2/4/2022": "", 11 | "2/5/2022": "", 12 | "2/6/2022": "", 13 | "2/13/2022": "King's Landing <-> Old Town", 14 | "2/14/2022": "", 15 | "2/15/2022": "", 16 | "2/16/2022": "", 17 | "2/17/2022": "", 18 | "2/18/2022": "", 19 | "2/19/2022": "", 20 | "2/20/2022": "", 21 | "2/21/2022": "", 22 | "2/22/2022": "", 23 | "2/23/2022": "", 24 | "2/24/2022": "", 25 | "2/25/2022": "", 26 | "2/26/2022": "", 27 | "2/27/2022": "", 28 | "2/28/2022": "", 29 | "3/1/2022": "", 30 | "3/2/2022": "", 31 | "3/3/2022": "", 32 | "3/4/2022": "", 33 | "3/5/2022": "", 34 | "3/6/2022": "", 35 | "3/13/2022": "Trip to the Wall", 36 | "3/14/2022": "", 37 | "3/15/2022": "", 38 | "3/16/2022": "", 39 | "3/17/2022": "", 40 | "3/18/2022": "", 41 | "3/19/2022": "", 42 | "3/20/2022": "", 43 | "3/26/2022": "King's Landing <-> Old Town", 44 | "3/27/2022": "", 45 | "3/28/2022": "", 46 | "3/29/2022": "", 47 | "6/4/2022": "Riverrun Roadtrip", 48 | "6/5/2022": "", 49 | "6/6/2022": "", 50 | "6/7/2022": "", 51 | "6/8/2022": "", 52 | "6/9/2022": "", 53 | "6/10/2022": "", 54 | "6/11/2022": "", 55 | "6/12/2022": "", 56 | "10/11/2022": "Highgarden Trip", 57 | "10/12/2022": "", 58 | "10/13/2022": "", 59 | "10/14/2022": "", 60 | "10/15/2022": "", 61 | "10/16/2022": "" 62 | } -------------------------------------------------------------------------------- /cli_resolve/utils/resolution_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | 4 | 5 | def add_detail_code(char): 6 | """ 7 | Return dict for a new detail code where the key is the code and the value is that code's description. 8 | """ 9 | print(f"`{char}` is not defined yet, but we can add it now") 10 | new_code_descript = input(f"What activity does `{char}` stand for?: ") 11 | return {char: new_code_descript} 12 | 13 | 14 | def get_all_resolutions(): 15 | """ 16 | Returns dict of all resolutions. 17 | """ 18 | with open("data/resolutions.json", "r") as f: 19 | all_res_dict = json.load(f) 20 | return all_res_dict 21 | 22 | 23 | def get_active_resolutions(): 24 | """ 25 | Returns a dict with all currently active resolutions. 26 | Automatically inactivates resolutions past their expiration date. 27 | """ 28 | with open("data/resolutions.json", "r") as f: 29 | all_res_dict = json.load(f) 30 | active_res_dict = {} 31 | for key, val in all_res_dict.items(): 32 | if val["is_active"]: 33 | # Check against expiration date first and update active status if expired 34 | expiry = val["res_expiration_date"] 35 | if not expiry or datetime.strptime(expiry, "%m/%d/%Y") > datetime.today(): 36 | active_res_dict[key] = val 37 | else: 38 | val["is_active"] = False 39 | with open("data/resolutions.json", "w") as f: 40 | json.dump(all_res_dict, f, indent=4) 41 | return active_res_dict 42 | 43 | 44 | def print_detail_codes(detail_codes): 45 | """ 46 | Print a list of detail codes and descriptions. 47 | """ 48 | codes = "" 49 | if detail_codes: 50 | for key, val in detail_codes.items(): 51 | codes += f"{key} - {val}\n" 52 | return print(f"Existing codes:\n{codes}") 53 | 54 | 55 | def print_resolutions_and_status(resolutions): 56 | """ 57 | Prints all resolutions and whether currently active. 58 | """ 59 | print("Printing resolutions and is_active status:") 60 | for key, val in resolutions.items(): 61 | print(f"* {key}: {val['is_active']}") 62 | print() 63 | -------------------------------------------------------------------------------- /scripts/convert_legacy_resolutions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pandas as pd 4 | 5 | # INPUTS 6 | filename = sys.argv[1] 7 | 8 | 9 | def get_index_response(prompt, num_cols): 10 | while True: 11 | try: 12 | value = int(input(prompt)) 13 | if value < 0 or value > num_cols - 1: 14 | raise ValueError(f"Index {value} out of range for current dataset") 15 | else: 16 | return value 17 | except ValueError as value_e: 18 | print(f"Invalid input: {value_e}") 19 | continue 20 | 21 | 22 | df = pd.read_csv(f"../data/legacy/{filename}") 23 | col_list = list(df.columns) 24 | cols = "" 25 | for idx, col_name in enumerate(col_list): 26 | cols += "{0:20} {1}".format(col_name, idx) 27 | cols += "\n" 28 | print("{0:20} {1}".format("COLUMN", "INDEX")) 29 | print(cols) 30 | 31 | data_start = get_index_response("Index of first column containing resolution data: ", len(col_list)) 32 | data_end = get_index_response("Index of last column containing resolution data: ", len(col_list)) 33 | 34 | # Make new df for saving just the data we want 35 | df_cleaned = pd.DataFrame() 36 | 37 | # Split a column called "Date" or "date" 38 | try: 39 | df_cleaned[['Month', 'Day', 'Year']] = df['Date'].str.split('/', expand=True) 40 | except KeyError as e: 41 | print(f"No column called={e}: trying 'date' instead") 42 | df_cleaned[['Month', 'Day', 'Year']] = df['date'].str.split('/', expand=True) 43 | # Convert 2-digit year to 4-digit year 44 | df_cleaned['Year'] = ["20" + year if len(year) == 2 else year for year in df_cleaned['Year']] 45 | df_cleaned['date'] = df_cleaned[['Month', 'Day', 'Year']].apply(lambda row: '/'.join(row.values), axis=1) 46 | df_cleaned['Month'] = df_cleaned['Month'].astype(int) 47 | df_cleaned['Day'] = df_cleaned['Day'].astype(int) 48 | df_cleaned['Year'] = df_cleaned['Year'].astype(int) 49 | 50 | # Replace falsy values with 0's, otherwise the boolean convqersion will return True for "0" and "0.0" 51 | df.fillna(0, inplace=True) 52 | clean_binary_dict = { 53 | '0': 0, 54 | '0.0': 0, 55 | 'False': 0, 56 | False: 0, 57 | 'True': 1, 58 | True: 1, 59 | } 60 | df.replace(clean_binary_dict, inplace=True) 61 | 62 | # Create new column that sums up total resolutions met per day 63 | resolution_bools = df.iloc[:, data_start:data_end + 1].astype(bool) 64 | df_cleaned['Resolutions Met'] = resolution_bools.sum(axis=1) 65 | 66 | # Copy over resolution columns as well as create new boolean columns from categorical resolutions 67 | start = data_start 68 | while start < data_end + 1: 69 | end = start + 1 70 | col_name = df.iloc[:, start:end].columns.values[0].lower() 71 | df_cleaned[col_name] = df.iloc[:, start:end] 72 | df_cleaned[f"{col_name}_bool"] = df.iloc[:, start:end].astype(bool).astype(int) 73 | start += 1 74 | 75 | # Save cleaned df as CSV 76 | print("*** Saving cleaned csv to data/cleaned/") 77 | cleaned_path = "../data/cleaned" 78 | if not os.path.exists(cleaned_path): 79 | os.makedirs(cleaned_path) 80 | df_cleaned.to_csv(f"{cleaned_path}/{filename}", index=False) 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resolve CLI 2 | 3 | Resolve is an open-source CLI app for people who make New Year's resolutions[^1] and intend to keep them. I decided to develop this tool after six years of using spreadsheets to track habits that I wanted to adopt. 4 | 5 | You're welcome to fork and clone this repo to your heart's content. If you've found value in my work, please consider [buying me a coffee](https://ko-fi.com/snickerdoodle_studio). 6 | 7 | ![Demo of how to log resolutions](images/demo_logres.gif) 8 | 9 | ## Getting Started 10 | 1. Install `pipenv` for package management if you don't already have it ([instructions](https://pipenv.pypa.io/en/latest/)) 11 | 2. Clone this repo to your local machine and navigate into the `cli-resolve` directory 12 | 3. Install all dependencies: `pipenv install` 13 | 4. Run the program: `pipenv run python cli_resolve` 14 | 15 | ## Features 16 | ### Core Features 17 | #### Add and Log Resolutions 18 | Keep track of your progress on existing resolutions and add new resolutions. 19 | 20 | #### Graph Resolutions 21 | Generate heatmaps from your data across multiple years and resolutions. Heatmaps are temporarily stored as PDFs in `data/exports` and saved over the next time you graph. 22 | 23 | You can overlay event labels on your graph, for example, to mark days when you were traveling (see [how it's stored](data/trips.json)). 24 | ![Resolutions heatmap overlaid with trip information](images/heatmap_with_trips.png) 25 | 26 | You can also generate minimaps of individual resolutions. 27 | ![Mini heatmaps of individual resolutions](images/minimaps.png) 28 | 29 | #### Export Data 30 | Export your data (stored [locally](data/resolutions.json) as JSON) as a CSV file. Exported CSVs reside in `data/exports`). 31 | 32 | ### Standalone Scripts 33 | #### Convert Legacy Data for Graphing 34 | If you used to keep track of your resolutions in a spreadsheet (or you want to keep using a spreadsheet) but now want to import and visualize them in Resolve, you can use `convert_legacy_resolutions.py` ([location](scripts/convert_legacy_resolutions.py)). 35 | ##### Instructions 36 | 1. Save legacy spreadsheet as a CSV file under `data/legacy` (here's my [actual data from 2022](data/legacy/nyr22.csv)). In order for the conversion script to work, your data must meet the following assumptions: 37 | - Must have a date field under the column header of "Date" or "date" 38 | - Date field must be in MM/DD/YYYY format 39 | - Must contain columns with headers that refer to resolutions (such as "exercise") 40 | - Each row must correspond to a unique date, as well as contain data pertaining to whether and how a resolution was met on that date 41 | 2. Run `python convert_legacy_resolutions.py {filename}.csv` from the `scripts` folder. The output is a cleaned CSV saved to `data/cleaned` which can then be used for graphing. 42 | 3. Run the CLI tool and export graph from file. 43 | 44 | #### Backpopulate App with Legacy Data 45 | You may want to import some or all of your legacy data into the app if you are ditching spreadsheets altogether or wish to generate graphs spanning multiple years. 46 | ##### Instructions 47 | 1. Follow the steps above to Convert Legacy Data for Graphing. This will generate a clean and standardized CSV in the `data/cleaned` folder, which can then be used to backpopulate the application data stored locally in `data/resolutions.json`. 48 | 2. Run `python backpopulate_data.py {filename}.csv` in the `scripts` folder and follow the instructions. 49 | 50 | ## Imaginary FAQs 51 | ### Is it really that hard to use a spreadsheet? 52 | No, but sometimes you just gotta let a gal overengineer. 53 | 54 | Also, you can schedule a cron job to run this program or host it in the cloud to automatically run once a day. 55 | 56 | ### Shouldn't this really be a web/desktop/mobile app? 57 | ### It would be useful to have features X, Y, and Z. 58 | ### Thing A, B, and C could do with a bit of refactoring. / Here be bugs. 59 | Probably, possibly, and most definitely! 60 | 61 | I do have it somewhere on my backlog to turn this into a more accessible GUI app (with additional features), but I'm just one person and my backlog never shrinks. You're welcome to contribute, though! 62 | 63 | ### How do I get in touch with you? 64 | 65 | Feel free to shoot me an [email](mailto:snickerdoodle.studio@gmail.com) if you have any questions or are interested in collaborating. 66 | 67 | [^1]: Or, indeed, at any time of the year. -------------------------------------------------------------------------------- /cli_resolve/utils/export_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import csv 3 | import pandas as pd 4 | import os 5 | 6 | 7 | def get_filename(date_str): 8 | """ 9 | Takes MM/DD/YYYY date string and returns MM-DD-YYYY string to be used in filenames. 10 | """ 11 | return "-".join(date_str.split("/")) 12 | 13 | 14 | def get_res_fieldnames(res_dict, start_date_str, end_date): 15 | """ 16 | Gets a list of res_id's that contains data that falls within the provided date range. 17 | """ 18 | res_fields = [] 19 | for res_id, entry in res_dict.items(): 20 | log_data = entry["data"] 21 | has_data = check_data_against_range(log_data, start_date_str, end_date) 22 | if has_data: 23 | res_fields.append(res_id) 24 | return res_fields 25 | 26 | 27 | def check_data_against_range(data, start_date_str, end_date): 28 | """ 29 | Checks the data attached to a resolution to see whether any of it falls within the provided date range. 30 | """ 31 | curr_date = datetime.strptime(start_date_str, "%m/%d/%Y") 32 | while curr_date <= end_date: 33 | curr_date_str = datetime.strftime(curr_date, "%-m/%-d/%Y") 34 | if curr_date_str in data: 35 | # We're happy as long as there's a single date with data 36 | return True 37 | curr_date += timedelta(days=1) 38 | return False 39 | 40 | 41 | def write_new_csv_with_header(fieldnames, fname_start, fname_end): 42 | """ 43 | Write header for the CSV to be exported. 44 | """ 45 | path = "data/exports" 46 | if not os.path.exists(path): 47 | os.makedirs(path) 48 | with open(f"{path}/res_{fname_start}_{fname_end}.csv", "w", newline="") as f: 49 | w = csv.DictWriter(f, fieldnames=fieldnames) 50 | w.writeheader() 51 | 52 | 53 | def write_res_row(fieldnames, curr_data, fname_start, fname_end): 54 | """ 55 | Write a row for the CSV to be exported using curr_data. 56 | 57 | curr_data is a dict in the following format: 58 | e.g. { "date": "MM/DD/YYYY", "res_A": 0, "res_B": 1, "res_C": "R" } 59 | 60 | Data values can be 0, 1, or a detail code. 61 | """ 62 | with open(f"data/exports/res_{fname_start}_{fname_end}.csv", "a", newline="") as f: 63 | w = csv.DictWriter(f, fieldnames=fieldnames) 64 | w.writerow(curr_data) 65 | 66 | 67 | def get_years_list(start_date_str, end_date_str): 68 | """ 69 | Returns a list of years included in a given date range. 70 | This list is used to determine display settings for graphs. 71 | """ 72 | start_yr = int(start_date_str.split("/")[2]) 73 | end_yr = int(end_date_str.split("/")[2]) 74 | 75 | years_list = [] 76 | for yr in range(start_yr, end_yr + 1): 77 | years_list.append(yr) 78 | return years_list 79 | 80 | 81 | def clean_for_graphing(filepath): 82 | """ 83 | Wrangle CSV data in data/exports and save the cleaned CSV to data/cleaned. 84 | """ 85 | print("*** Cleaning data") 86 | filename = filepath.split("data/exports/")[1] 87 | df = pd.read_csv(filepath) 88 | data_start = 1 89 | data_end = len(df.columns) - 1 90 | 91 | df[['Month', 'Day', 'Year']] = df['date'].str.split('/', expand=True) 92 | 93 | df['Month'] = df['Month'].astype(int) 94 | df['Day'] = df['Day'].astype(int) 95 | df['Year'] = df['Year'].astype(int) 96 | 97 | # Replace falsy values with 0's, otherwise the boolean conversion will return True for "0" and "0.0" 98 | df.fillna(0, inplace=True) 99 | clean_binary_dict = { 100 | '0': 0, 101 | '0.0': 0, 102 | 'False': 0, 103 | False: 0, 104 | 'True': 1, 105 | True: 1, 106 | } 107 | df.replace(clean_binary_dict, inplace=True) 108 | 109 | # Create new column that sums up total resolutions met per day 110 | resolution_bools = df.iloc[:, data_start:data_end + 1].astype(bool) 111 | df['Resolutions Met'] = resolution_bools.sum(axis=1) 112 | 113 | # Create new boolean columns from categorical resolutions 114 | start = data_start 115 | while start < data_end + 1: 116 | end = start + 1 117 | col_name = df.iloc[:, start:end].columns.values[0] 118 | df[f"{col_name}_bool"] = df.iloc[:, start:end].astype(bool) 119 | df[f"{col_name}_bool"] = df[f"{col_name}_bool"].astype(int) 120 | start += 1 121 | 122 | # Save cleaned df as CSV 123 | path = "data/cleaned" 124 | if not os.path.exists(path): 125 | os.makedirs(path) 126 | df.to_csv(f"{path}/{filename}", index=False) 127 | -------------------------------------------------------------------------------- /cli_resolve/utils/input_utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import date, datetime 3 | 4 | from .resolution_utils import print_detail_codes, add_detail_code 5 | 6 | 7 | def get_index_response(prompt, num_cols): 8 | """ 9 | Gets a valid index. 10 | """ 11 | while True: 12 | command = input(prompt) 13 | try: 14 | value = int(command) 15 | if value < 0 or value > num_cols - 1: 16 | raise ValueError(f"Index {value} out of range for current dataset") 17 | else: 18 | return value 19 | except ValueError as value_e: 20 | print(f"Invalid input: {value_e}") 21 | continue 22 | 23 | 24 | def get_date_string_response(prompt, year_start=False, year_end=False): 25 | """ 26 | Gets a date string in the form of "MM/DD/YYYY". 27 | """ 28 | while True: 29 | command = input(prompt) 30 | try: 31 | if command.lower() == "today": 32 | return date.today().strftime("%-m/%-d/%Y") 33 | if command.lower() == "never": 34 | return None 35 | if command.lower() == "file": 36 | return "file" 37 | if year_start: 38 | command = f"1/1/{command}" 39 | if year_end: 40 | command = f"12/31/{command}" 41 | validate_date_string(command) 42 | return command 43 | except ValueError as e: 44 | print(f"Invalid input: {e}") 45 | continue 46 | 47 | 48 | def validate_date_string(date_string): 49 | """ 50 | Validates date input strings to ensure that they are in 'MM/DD/YYYY' format. 51 | """ 52 | try: 53 | datetime.strptime(date_string, "%m/%d/%Y") 54 | except ValueError: 55 | raise ValueError(f"Invalid date={date_string}; date input should be in the form of 'MM/DD/YYYY'") 56 | 57 | 58 | def get_boolean_response(prompt): 59 | """ 60 | Takes the response to a Y/N question and returns the corresponding boolean. 61 | """ 62 | while True: 63 | command = input(prompt) 64 | try: 65 | if command.upper() == "Y": 66 | return True 67 | elif command.upper() == "N": 68 | return False 69 | else: 70 | raise ValueError("Input should be 'Y' or 'N'") 71 | except ValueError as e: 72 | print(f"Invalid input: {e}") 73 | continue 74 | 75 | 76 | def get_detail_code_response(prompt, existing_codes): 77 | """ 78 | Gets a valid detail code response. 79 | """ 80 | while True: 81 | print_detail_codes(existing_codes) 82 | command = input(prompt) 83 | try: 84 | if command.upper() == "N": 85 | return False 86 | 87 | code_list = command.split(",") 88 | # Check that each code is already a defined code, or add it now 89 | for char in code_list: 90 | code = char.upper() 91 | if len(char) > 1: 92 | raise ValueError(f"Code should be of len=1; found len={len(char)} for code `{char}`") 93 | if code not in existing_codes: 94 | existing_codes.update(add_detail_code(code)) 95 | return command 96 | except ValueError as e: 97 | print(f"Invalid input: {e}") 98 | continue 99 | 100 | 101 | def handle_input(prompt, response_type=None, **kwargs): 102 | """ 103 | Generic function for handling user input. 104 | Returns the appropriate response getter (which performs input validation) depending on response_type. 105 | """ 106 | # Print instructions first if provided 107 | if kwargs.get("instructions"): 108 | print(f"\nINSTRUCTIONS: {kwargs['instructions']}\n") 109 | 110 | # Without a specified response type, we just want the input string, no validation required 111 | if not response_type: 112 | command = input(prompt) 113 | if command == "q": 114 | sys.exit("Goodbye!") 115 | return command 116 | elif response_type == "index": 117 | return get_index_response(prompt, num_cols=kwargs["num_cols"]) 118 | elif response_type == "datestring": 119 | return get_date_string_response( 120 | prompt, 121 | year_start=kwargs.get("year_start", False), 122 | year_end=kwargs.get("year_end", False) 123 | ) 124 | elif response_type == "boolean": 125 | return get_boolean_response(prompt) 126 | elif response_type == "code": 127 | return get_detail_code_response( 128 | prompt, 129 | existing_codes=kwargs.get("codes") 130 | ) 131 | else: 132 | return print(f"Something went wrong: we don't know how to handle command={command} " 133 | f"with response_type={response_type}") 134 | -------------------------------------------------------------------------------- /cli_resolve/export.py: -------------------------------------------------------------------------------- 1 | from utils.resolution_utils import get_all_resolutions 2 | from utils.export_utils import * 3 | from utils.graph_utils import * 4 | from utils.menu_utils import * 5 | 6 | INSTRUCTIONS = "Enter the start and end dates ('MM/DD/YYYY') for which you would like to make an export.\n" \ 7 | "--if you specify only the year, all data from that year will be included\n" \ 8 | "e.g. start_date='2020' and end_date='2021' exports all data from 1/1/2020 to 12/31/2021\n" 9 | GRAPH_INSTRUCTIONS = "--enter 'file' to load a dataset from file" 10 | 11 | 12 | def export_csv(start_date_str=None, end_date_str=None): 13 | """ 14 | Export CSV from app data. 15 | """ 16 | print("*** Exporting CSV") 17 | # User input is requested if this function is not called from export_graph() 18 | if not start_date_str or not end_date_str: 19 | print(f"INSTRUCTIONS: {INSTRUCTIONS}") 20 | start_date_str = handle_input(prompt="start date: ", response_type="datestring", year_start=True) 21 | end_date_str = handle_input(prompt="end date: ", response_type="datestring", year_end=True) 22 | 23 | fname_start = get_filename(start_date_str) 24 | fname_end = get_filename(end_date_str) 25 | 26 | # Check whether there's already a CSV for the time range of interest 27 | if os.path.exists(f"data/exports/res_{fname_start}_{fname_end}.csv"): 28 | print(f"There is already a CSV file for start={start_date_str} end={end_date_str}.") 29 | override = handle_input(prompt="Save over existing file? (Y/N): ", response_type="boolean") 30 | if not override: 31 | return go_home_message() 32 | 33 | print(f"*** Exporting data: {start_date_str} - {end_date_str}") 34 | 35 | # Get a list of the resolutions that have any data within the time range of interest 36 | curr_date_str = start_date_str 37 | curr_date = datetime.strptime(curr_date_str, "%m/%d/%Y") 38 | end_date = datetime.strptime(end_date_str, "%m/%d/%Y") 39 | 40 | all_res_dict = get_all_resolutions() 41 | 42 | fieldnames = ["date"] 43 | res_fields = get_res_fieldnames(all_res_dict, start_date_str, end_date) 44 | if len(res_fields) < 1: 45 | print(f"Found no data from {start_date_str} to {end_date_str}") 46 | return go_home_message() 47 | fieldnames = fieldnames + res_fields 48 | 49 | write_new_csv_with_header(fieldnames, fname_start, fname_end) 50 | # Go through all the resolutions that have some data for the date range and write the CSV row-by-row 51 | while curr_date <= end_date: 52 | curr_data = {"date": curr_date_str} 53 | for res in res_fields: 54 | log_data = all_res_dict[res]["data"] 55 | try: 56 | data_on_date = log_data[curr_date_str] 57 | if data_on_date is False: 58 | datapoint = {res: 0} 59 | elif data_on_date is True: 60 | datapoint = {res: 1} 61 | else: # non-boolean value 62 | datapoint = {res: data_on_date} 63 | print(f"*** Data found for resolution={res} on date={curr_date_str}!") 64 | except KeyError: 65 | print(f"resolution={res} does not have data for date={curr_date_str}, recording as 0") 66 | datapoint = {res: 0} 67 | curr_data.update(datapoint) 68 | write_res_row(fieldnames, curr_data, fname_start, fname_end) 69 | curr_date += timedelta(days=1) 70 | curr_date_str = datetime.strftime(curr_date, "%-m/%-d/%Y") 71 | 72 | 73 | def export_graph(): 74 | """ 75 | Generate and export heatmaps from app data. 76 | """ 77 | print(f"INSTRUCTIONS: {INSTRUCTIONS}{GRAPH_INSTRUCTIONS}") 78 | start_date_str = handle_input(prompt="start date: ", response_type="datestring", year_start=True) 79 | if start_date_str == "file": 80 | return export_graph_from_file() 81 | end_date_str = handle_input(prompt="end date: ", response_type="datestring", year_end=True) 82 | years_list = get_years_list(start_date_str, end_date_str) 83 | 84 | display_events = handle_input(prompt="Do you want event data overlaid on this graph? (Y/N): ", 85 | response_type="boolean") 86 | if display_events: 87 | event_type = handle_input(prompt="What type of events? ") 88 | else: 89 | event_type = None 90 | export_minimaps = handle_input(prompt="Do you want minimaps for select resolutions? (Y/N): ", 91 | response_type="boolean") 92 | 93 | fname_start = get_filename(start_date_str) 94 | fname_end = get_filename(end_date_str) 95 | 96 | # Generate graph from existing cleaned CSV, or export a CSV first and then generate graph 97 | cleaned_filepath = f"data/cleaned/res_{fname_start}_{fname_end}.csv" 98 | exports_filepath = f"data/exports/res_{fname_start}_{fname_end}.csv" 99 | 100 | already_cleaned = os.path.exists(cleaned_filepath) 101 | 102 | if already_cleaned: 103 | last_generated_ts = datetime.fromtimestamp(os.path.getmtime(cleaned_filepath)) 104 | regenerate = handle_input( 105 | prompt=f"Clean data for this graph last generated {last_generated_ts}. Do you want to rerun? (Y/N): ", 106 | response_type="boolean" 107 | ) 108 | if not regenerate: 109 | generate_heatmap(cleaned_filepath, years_list=years_list, notable_days=event_type) 110 | if export_minimaps: 111 | generate_minimaps(cleaned_filepath, years_list=years_list) 112 | plt.show() 113 | if not already_cleaned or regenerate: 114 | try: 115 | export_csv(start_date_str, end_date_str) 116 | clean_for_graphing(exports_filepath) 117 | generate_heatmap(cleaned_filepath, years_list=years_list, notable_days=event_type) 118 | if export_minimaps: 119 | generate_minimaps(cleaned_filepath, years_list=years_list) 120 | plt.show() 121 | except FileNotFoundError as e: 122 | print(f"File not found: {e}") 123 | return go_home_message() 124 | -------------------------------------------------------------------------------- /cli_resolve/resolution.py: -------------------------------------------------------------------------------- 1 | from utils.input_utils import * 2 | from utils.resolution_utils import * 3 | from utils.menu_utils import * 4 | 5 | 6 | def log_resolutions(): 7 | """ 8 | Log entry for all active resolutions. 9 | """ 10 | active_res = get_active_resolutions() 11 | if not active_res: 12 | print("You don't have any active resolutions!") 13 | return go_home_message() 14 | 15 | log_date = handle_input(prompt="What is the date for this entry? ('today' or 'MM/DD/YYYY'): ", 16 | response_type="datestring") 17 | 18 | for res, val in active_res.items(): 19 | # Get Y/N response for binary resolution 20 | if val['is_binary']: 21 | prompt = f"- Did you `{val['res_descript']}`? (Y/N): " 22 | response = handle_input(prompt=prompt, response_type="boolean") 23 | val['data'][log_date] = response 24 | # Get N or detail codes for non-binary resolution 25 | else: 26 | codes = val["res_detail_codes"] 27 | prompt = f"- Did you `{val['res_descript']}`? " 28 | instructions = "Enter an existing or new detail code, a comma-separated list of existing detail codes, " \ 29 | "or 'N' for no." 30 | response = handle_input(prompt=prompt, response_type="code", instructions=instructions, codes=codes) 31 | val['data'][log_date] = response 32 | 33 | # Persist to file 34 | print("*** Saving new logs") 35 | with open("data/resolutions.json", "r") as f: 36 | all_res_dict = json.load(f) 37 | all_res_dict.update(active_res) 38 | with open("data/resolutions.json", "w") as f: 39 | json.dump(all_res_dict, f, indent=4) 40 | print(f"*** Saved logs for {log_date}!") 41 | return go_home_message() 42 | 43 | 44 | def add_resolution(): 45 | """ 46 | Add a new resolution. 47 | """ 48 | all_res_dict = get_all_resolutions() 49 | while True: 50 | try: 51 | res_id = handle_input(prompt="Enter a short ID for this resolution in snake_case: ") 52 | if res_id in all_res_dict: 53 | print(f"`{res_id}` already exists") 54 | continue 55 | res_descript = handle_input(prompt="Describe this resolution: ") 56 | res_creation_date = date.today() 57 | is_active = True 58 | res_expiration_date = handle_input( 59 | prompt="When does this resolution expire? ('MM/DD/YYYY' or 'never' for no " 60 | "expiration): ", 61 | response_type="datestring" 62 | ) 63 | is_binary = handle_input( 64 | prompt="Is this resolution's outcome binary? (Y/N): ", 65 | response_type="boolean", 66 | instructions="Binary outcomes tell us whether or not you did something, while categorical outcomes " 67 | "tell us about the kind of thing you did.\n" 68 | "\nFor example, for the resolution to exercise, " 69 | "a binary outcome is exercising, or not exercising. " 70 | "In contrast, a categorical outcome names the kind of exercise you did (e.g. " 71 | "run/bike/swim). " 72 | ) 73 | if not is_binary: 74 | print( 75 | "You will be able to tell me what kind of activity you did when you log an entry for this " 76 | "resolution." 77 | ) 78 | res_detail_codes = {} 79 | except ValueError as e: 80 | print(f"Invalid input: {e}") 81 | continue 82 | 83 | res_dict = { 84 | res_id: { 85 | "res_descript": res_descript, 86 | "res_creation_date": res_creation_date.strftime("%-m/%-d/%Y"), 87 | "is_active": is_active, 88 | "res_expiration_date": res_expiration_date, 89 | "is_binary": is_binary, 90 | "res_detail_codes": res_detail_codes, 91 | "data": {}, 92 | } 93 | } 94 | 95 | print(f"Preview: {res_dict}") 96 | confirm = handle_input(prompt="Does everything look right? (Y/N): ", response_type="boolean") 97 | if confirm: 98 | print(f"*** Adding new resolution={res_id}") 99 | all_res_dict.update(res_dict) 100 | with open("data/resolutions.json", "w+") as f: 101 | json.dump(all_res_dict, f, indent=4) 102 | print("*** Added new resolution!") 103 | return go_home_message() 104 | else: 105 | print("Okay, let's try this again.") 106 | continue 107 | 108 | 109 | def toggle_active_resolutions(): 110 | """ 111 | Toggle the is_active status of resolutions. 112 | Resolutions are considered active if they are currently being tracked. 113 | """ 114 | all_res_dict = get_all_resolutions() 115 | 116 | if len(all_res_dict) == 0: 117 | print("No resolutions found") 118 | return go_home_message() 119 | 120 | while True: 121 | print_resolutions_and_status(all_res_dict) 122 | res_key = handle_input(prompt="Which resolution would you like to toggle?: ", 123 | instructions="Enter 'menu' to go back") 124 | if res_key == "menu": 125 | break 126 | try: 127 | # Flip the status and save back to disk 128 | all_res_dict[res_key]["is_active"] = not all_res_dict[res_key]["is_active"] 129 | # Set expiration date to today if toggling to inactive 130 | if not all_res_dict[res_key]["is_active"]: 131 | all_res_dict[res_key]["res_expiration_date"] = date.today().strftime("%-m/%-d/%Y") 132 | # Ask for expiration date if toggling to active 133 | else: 134 | res_expiration_date = handle_input( 135 | prompt="When does this resolution expire? ('MM/DD/YYYY' or 'never' for no " 136 | "expiration): ", 137 | response_type="datestring" 138 | ) 139 | all_res_dict[res_key]["res_expiration_date"] = res_expiration_date 140 | with open("data/resolutions.json", "w") as f: 141 | json.dump(all_res_dict, f, indent=4) 142 | print(f"*** Toggled active status of `{res_key}`!\n") 143 | except KeyError as e: 144 | print(f"No such resolution key={e}\n") 145 | -------------------------------------------------------------------------------- /data/legacy/nyr22.csv: -------------------------------------------------------------------------------- 1 | Date,Exercise,Floss,Writing,Coding,Skincare 2 | 1/1/22,NA,NA,NA,NA,NA 3 | 1/2/22,NA,NA,NA,NA,NA 4 | 1/3/22,NA,NA,NA,NA,NA 5 | 1/4/22,R,1,0,0,NA 6 | 1/5/22,R,1,0,0,NA 7 | 1/6/22,0,0,0,0,NA 8 | 1/7/22,0,0,0,0,NA 9 | 1/8/22,R,0,0,0,NA 10 | 1/9/22,R,0,0,0,NA 11 | 1/10/22,R,0,0,0,NA 12 | 1/11/22,R,0,0,0,NA 13 | 1/12/22,0,0,0,0,NA 14 | 1/13/22,0,0,0,0,NA 15 | 1/14/22,0,0,0,0,NA 16 | 1/15/22,0,0,0,0,NA 17 | 1/16/22,0,0,0,0,NA 18 | 1/17/22,0,0,0,0,NA 19 | 1/18/22,R,0,0,0,NA 20 | 1/19/22,R,1,0,0,NA 21 | 1/20/22,R,0,0,0,NA 22 | 1/21/22,0,0,0,0,NA 23 | 1/22/22,R,0,0,0,NA 24 | 1/23/22,0,0,0,0,NA 25 | 1/24/22,0,0,0,0,NA 26 | 1/25/22,0,1,0,0,NA 27 | 1/26/22,0,0,0,0,NA 28 | 1/27/22,0,0,0,0,NA 29 | 1/28/22,0,0,0,0,NA 30 | 1/29/22,0,0,0,0,NA 31 | 1/30/22,0,0,0,0,NA 32 | 1/31/22,0,0,0,0,NA 33 | 2/1/22,0,0,0,0,NA 34 | 2/2/22,0,0,0,0,NA 35 | 2/3/22,0,0,0,0,NA 36 | 2/4/22,0,0,0,0,NA 37 | 2/5/22,0,0,0,0,NA 38 | 2/6/22,0,1,0,0,NA 39 | 2/7/22,0,0,0,0,NA 40 | 2/8/22,0,0,0,0,NA 41 | 2/9/22,0,0,0,0,NA 42 | 2/10/22,R,0,0,0,NA 43 | 2/11/22,0,0,0,0,NA 44 | 2/12/22,R,1,0,0,1 45 | 2/13/22,0,0,0,0,0 46 | 2/14/22,0,0,0,0,0 47 | 2/15/22,0,0,0,0,0 48 | 2/16/22,0,0,0,0,0 49 | 2/17/22,0,0,0,0,0 50 | 2/18/22,0,0,0,0,0 51 | 2/19/22,0,0,0,0,0 52 | 2/20/22,0,0,0,0,0 53 | 2/21/22,0,0,0,0,0 54 | 2/22/22,0,0,0,0,0 55 | 2/23/22,0,0,0,0,0 56 | 2/24/22,0,0,0,0,0 57 | 2/25/22,0,0,0,0,0 58 | 2/26/22,0,0,0,0,0 59 | 2/27/22,0,0,0,0,0 60 | 2/28/22,0,0,0,0,0 61 | 3/1/22,0,0,0,0,0 62 | 3/2/22,0,0,0,0,0 63 | 3/3/22,0,0,0,0,0 64 | 3/4/22,0,0,0,0,0 65 | 3/5/22,0,0,0,0,0 66 | 3/6/22,0,1,0,0,1 67 | 3/7/22,R,1,J,0,1 68 | 3/8/22,0,0,0,0,0 69 | 3/9/22,0,0,0,0,0 70 | 3/10/22,0,0,0,0,0 71 | 3/11/22,R,0,0,0,1 72 | 3/12/22,0,0,0,0,0 73 | 3/13/22,0,0,0,0,0 74 | 3/14/22,C,0,0,0,0 75 | 3/15/22,0,0,0,0,0 76 | 3/16/22,0,0,0,0,0 77 | 3/17/22,0,0,0,0,1 78 | 3/18/22,0,0,0,0,0 79 | 3/19/22,0,0,0,0,0 80 | 3/20/22,0,1,0,0,1 81 | 3/21/22,0,0,0,0,0 82 | 3/22/22,0,0,0,0,0 83 | 3/23/22,0,0,J,0,0 84 | 3/24/22,0,0,0,0,0 85 | 3/25/22,0,0,0,0,0 86 | 3/26/22,0,0,0,0,1 87 | 3/27/22,0,0,0,0,1 88 | 3/28/22,0,0,0,0,1 89 | 3/29/22,0,0,0,0,0 90 | 3/30/22,0,0,0,0,0 91 | 3/31/22,0,0,0,0,0 92 | 4/1/22,0,0,0,0,0 93 | 4/2/22,0,0,0,0,0 94 | 4/3/22,0,0,0,0,0 95 | 4/4/22,0,0,0,0,0 96 | 4/5/22,0,0,0,0,0 97 | 4/6/22,0,0,0,0,0 98 | 4/7/22,R,1,0,0,1 99 | 4/8/22,C,1,0,0,1 100 | 4/9/22,0,0,0,0,0 101 | 4/10/22,0,0,0,0,0 102 | 4/11/22,0,0,0,0,0 103 | 4/12/22,0,0,0,0,0 104 | 4/13/22,0,0,0,0,0 105 | 4/14/22,0,0,0,0,0 106 | 4/15/22,0,0,0,0,1 107 | 4/16/22,0,0,0,0,0 108 | 4/17/22,0,0,0,0,0 109 | 4/18/22,R,1,0,0,1 110 | 4/19/22,0,0,0,0,0 111 | 4/20/22,0,0,0,0,0 112 | 4/21/22,0,1,0,0,1 113 | 4/22/22,0,0,0,0,0 114 | 4/23/22,0,0,0,0,0 115 | 4/24/22,0,0,0,0,0 116 | 4/25/22,0,0,0,0,1 117 | 4/26/22,0,1,0,0,1 118 | 4/27/22,0,0,0,0,0 119 | 4/28/22,0,0,0,1,0 120 | 4/29/22,0,0,0,1,0 121 | 4/30/22,0,0,0,0,0 122 | 5/1/22,0,0,0,1,0 123 | 5/2/22,0,0,0,1,0 124 | 5/3/22,R,0,0,1,0 125 | 5/4/22,0,1,J,1,1 126 | 5/5/22,0,0,0,1,0 127 | 5/6/22,0,0,0,1,0 128 | 5/7/22,0,,J,1,0 129 | 5/8/22,0,0,0,0,0 130 | 5/9/22,0,0,0,0,0 131 | 5/10/22,0,0,0,1,0 132 | 5/11/22,0,0,0,1,0 133 | 5/12/22,R,0,0,1,0 134 | 5/13/22,0,0,0,1,0 135 | 5/14/22,0,0,0,0,0 136 | 5/15/22,0,0,0,0,1 137 | 5/16/22,0,0,0,1,0 138 | 5/17/22,0,0,0,0,0 139 | 5/18/22,0,0,0,0,0 140 | 5/19/22,0,0,0,1,0 141 | 5/20/22,0,0,0,1,0 142 | 5/21/22,R,1,0,0,1 143 | 5/22/22,0,0,0,0,0 144 | 5/23/22,R,1,0,1,1 145 | 5/24/22,0,0,0,1,0 146 | 5/25/22,0,0,0,0,0 147 | 5/26/22,R,0,0,1,1 148 | 5/27/22,0,0,0,1,0 149 | 5/28/22,0,0,0,0,0 150 | 5/29/22,0,0,0,0,0 151 | 5/30/22,0,0,0,0,0 152 | 5/31/22,0,0,0,1,0 153 | 6/1/22,0,0,0,0,0 154 | 6/2/22,R,1,0,0,1 155 | 6/3/22,0,0,0,0,0 156 | 6/4/22,0,0,0,0,0 157 | 6/5/22,0,0,0,0,0 158 | 6/6/22,0,0,0,0,0 159 | 6/7/22,0,0,0,0,0 160 | 6/8/22,0,0,0,0,0 161 | 6/9/22,0,0,0,0,0 162 | 6/10/22,0,0,0,0,0 163 | 6/11/22,0,0,0,0,0 164 | 6/12/22,0,0,0,0,0 165 | 6/13/22,0,1,0,0,1 166 | 6/14/22,0,0,0,1,0 167 | 6/15/22,0,0,0,0,0 168 | 6/16/22,0,0,0,0,0 169 | 6/17/22,R,0,0,1,0 170 | 6/18/22,R,0,0,0,1 171 | 6/19/22,0,0,0,1,0 172 | 6/20/22,R,0,0,0,0 173 | 6/21/22,0,0,0,1,0 174 | 6/22/22,0,0,0,0,0 175 | 6/23/22,0,0,J,1,0 176 | 6/24/22,0,0,0,0,1 177 | 6/25/22,0,0,0,0,0 178 | 6/26/22,R,1,0,0,1 179 | 6/27/22,0,0,0,1,0 180 | 6/28/22,0,0,0,0,0 181 | 6/29/22,0,0,0,0,0 182 | 6/30/22,0,0,0,0,1 183 | 7/1/22,0,0,0,0,0 184 | 7/2/22,0,0,0,0,0 185 | 7/3/22,0,0,0,0,0 186 | 7/4/22,0,0,0,0,1 187 | 7/5/22,0,0,0,0,0 188 | 7/6/22,R,1,0,1,1 189 | 7/7/22,0,1,0,1,0 190 | 7/8/22,0,0,0,0,0 191 | 7/9/22,R,0,0,0,0 192 | 7/10/22,0,0,0,0,0 193 | 7/11/22,0,0,0,0,1 194 | 7/12/22,0,0,0,0,0 195 | 7/13/22,0,0,0,1,0 196 | 7/14/22,0,0,0,1,0 197 | 7/15/22,0,0,0,0,0 198 | 7/16/22,0,0,0,0,0 199 | 7/17/22,0,0,0,1,0 200 | 7/18/22,0,0,0,1,0 201 | 7/19/22,0,0,0,1,0 202 | 7/20/22,0,0,0,0,0 203 | 7/21/22,R,1,0,1,1 204 | 7/22/22,0,0,0,1,0 205 | 7/23/22,0,0,0,1,0 206 | 7/24/22,0,0,0,0,0 207 | 7/25/22,0,0,0,1,1 208 | 7/26/22,0,0,J,0,0 209 | 7/27/22,0,0,0,0,0 210 | 7/28/22,0,0,0,0,1 211 | 7/29/22,0,0,0,0,0 212 | 7/30/22,0,0,0,0,0 213 | 7/31/22,0,0,0,0,0 214 | 8/1/22,R,1,0,0,1 215 | 8/2/22,0,0,0,0,0 216 | 8/3/22,0,0,0,0,0 217 | 8/4/22,0,0,0,0,0 218 | 8/5/22,0,0,0,0,0 219 | 8/6/22,0,0,0,0,0 220 | 8/7/22,R,0,0,0,0 221 | 8/8/22,0,0,0,0,0 222 | 8/9/22,0,0,0,0,0 223 | 8/10/22,0,0,J,0,0 224 | 8/11/22,0,0,0,0,0 225 | 8/12/22,0,0,0,0,0 226 | 8/13/22,0,0,0,0,0 227 | 8/14/22,0,0,0,0,0 228 | 8/15/22,0,0,0,0,0 229 | 8/16/22,0,0,0,0,0 230 | 8/17/22,0,0,0,0,0 231 | 8/18/22,R,0,0,0,1 232 | 8/19/22,0,0,0,0,0 233 | 8/20/22,0,0,0,0,0 234 | 8/21/22,0,0,0,0,0 235 | 8/22/22,0,0,0,0,1 236 | 8/23/22,0,0,0,0,0 237 | 8/24/22,0,0,0,0,0 238 | 8/25/22,0,0,0,0,0 239 | 8/26/22,R,0,0,0,1 240 | 8/27/22,0,0,0,0,0 241 | 8/28/22,0,0,0,0,0 242 | 8/29/22,R,0,0,0,1 243 | 8/30/22,0,0,0,0,0 244 | 8/31/22,R,0,0,0,1 245 | 9/1/22,0,0,0,0,0 246 | 9/2/22,0,0,0,0,0 247 | 9/3/22,0,0,0,0,0 248 | 9/4/22,0,0,0,0,0 249 | 9/5/22,R,0,0,0,1 250 | 9/6/22,R,0,0,0,1 251 | 9/7/22,0,0,0,0,0 252 | 9/8/22,0,0,0,0,0 253 | 9/9/22,0,1,0,0,0 254 | 9/10/22,B,0,0,0,1 255 | 9/11/22,R,0,0,0,1 256 | 9/12/22,0,0,0,0,0 257 | 9/13/22,0,0,0,0,0 258 | 9/14/22,0,0,0,0,0 259 | 9/15/22,0,0,0,0,0 260 | 9/16/22,0,0,0,0,0 261 | 9/17/22,0,0,0,1,0 262 | 9/18/22,0,0,0,1,1 263 | 9/19/22,0,0,0,0,0 264 | 9/20/22,0,0,0,1,0 265 | 9/21/22,0,0,0,0,0 266 | 9/22/22,0,0,0,1,1 267 | 9/23/22,0,0,0,1,0 268 | 9/24/22,0,0,0,0,0 269 | 9/25/22,0,0,0,1,0 270 | 9/26/22,0,0,0,0,0 271 | 9/27/22,R,0,0,1,1 272 | 9/28/22,0,0,0,1,0 273 | 9/29/22,R,0,0,1,0 274 | 9/30/22,0,0,0,1,0 275 | 10/1/22,R,0,0,1,1 276 | 10/2/22,0,0,0,0,0 277 | 10/3/22,0,0,0,0,0 278 | 10/4/22,0,0,0,0,0 279 | 10/5/22,0,1,0,1,1 280 | 10/6/22,0,0,0,0,0 281 | 10/7/22,0,0,0,0,0 282 | 10/8/22,0,0,0,0,0 283 | 10/9/22,0,0,0,0,0 284 | 10/10/22,0,1,0,0,1 285 | 10/11/22,0,0,0,0,0 286 | 10/12/22,0,0,0,0,0 287 | 10/13/22,0,0,0,0,0 288 | 10/14/22,0,0,0,0,0 289 | 10/15/22,0,0,0,0,0 290 | 10/16/22,0,0,0,0,0 291 | 10/17/22,0,0,0,0,0 292 | 10/18/22,0,0,0,0,0 293 | 10/19/22,0,1,0,0,1 294 | 10/20/22,0,0,0,0,0 295 | 10/21/22,0,0,0,0,0 296 | 10/22/22,0,0,0,0,0 297 | 10/23/22,0,0,0,0,0 298 | 10/24/22,0,0,0,0,1 299 | 10/25/22,0,0,0,0,0 300 | 10/26/22,0,0,0,0,0 301 | 10/27/22,0,0,0,0,0 302 | 10/28/22,0,0,0,0,1 303 | 10/29/22,0,0,0,1,0 304 | 10/30/22,0,0,0,1,0 305 | 10/31/22,0,0,0,1,0 306 | 11/1/22,0,1,0,1,0 307 | 11/2/22,0,0,0,1,0 308 | 11/3/22,0,0,0,1,0 309 | 11/4/22,0,1,0,1,1 310 | 11/5/22,0,0,0,0,0 311 | 11/6/22,0,0,0,1,0 312 | 11/7/22,0,1,0,1,1 313 | 11/8/22,0,0,0,1,0 314 | 11/9/22,0,0,0,1,0 315 | 11/10/22,0,1,0,1,1 316 | 11/11/22,0,0,0,1,0 317 | 11/12/22,0,0,0,1,1 318 | 11/13/22,0,0,0,1,0 319 | 11/14/22,0,0,0,1,0 320 | 11/15/22,R,1,0,1,1 321 | 11/16/22,0,0,0,0,0 322 | 11/17/22,0,0,0,0,0 323 | 11/18/22,0,0,0,1,0 324 | 11/19/22,0,1,0,0,1 325 | 11/20/22,0,0,0,0,0 326 | 11/21/22,0,1,0,1,0 327 | 11/22/22,0,0,0,1,0 328 | 11/23/22,0,0,0,1,0 329 | 11/24/22,0,0,0,0,1 330 | 11/25/22,0,0,0,1,0 331 | 11/26/22,R,0,0,1,0 332 | 11/27/22,0,1,0,1,0 333 | 11/28/22,R,1,0,1,1 334 | 11/29/22,0,0,0,1,0 335 | 11/30/22,0,0,0,1,0 336 | 12/1/22,R,0,0,1,1 337 | 12/2/22,0,0,0,1,0 338 | 12/3/22,0,0,0,1,0 339 | 12/4/22,0,0,0,0,0 340 | 12/5/22,0,0,0,1,0 341 | 12/6/22,R,1,0,1,1 342 | 12/7/22,0,0,0,1,0 343 | 12/8/22,0,0,0,0,0 344 | 12/9/22,0,1,0,0,1 345 | 12/10/22,0,0,0,0,0 346 | 12/11/22,0,0,0,0,1 347 | 12/12/22,0,0,0,1,0 348 | 12/13/22,0,0,0,1,0 349 | 12/14/22,0,1,0,1,0 350 | 12/15/22,0,0,0,1,0 351 | 12/16/22,0,0,0,1,1 352 | 12/17/22,0,0,0,1,0 353 | 12/18/22,0,1,0,1,0 354 | 12/19/22,0,0,0,1,0 355 | 12/20/22,R,1,0,1,1 356 | 12/21/22,0,0,0,1,0 357 | 12/22/22,0,1,0,1,1 358 | 12/23/22,0,0,0,0,0 359 | 12/24/22,0,0,0,0,0 360 | 12/25/22,0,0,0,0,1 361 | 12/26/22,0,0,0,1,0 362 | 12/27/22,R,1,0,1,1 363 | 12/28/22,0,0,0,1,0 364 | 12/29/22,0,0,0,1,0 365 | 12/30/22,0,0,0,0,0 366 | 12/31/22,0,1,0,0,1 -------------------------------------------------------------------------------- /scripts/backpopulate_data.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import os 3 | import sys 4 | import json 5 | from datetime import datetime 6 | import shutil 7 | 8 | # Set parent directory 9 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 10 | from cli_resolve.utils.input_utils import * 11 | 12 | # INPUTS 13 | filename = sys.argv[1] 14 | 15 | 16 | def get_resolution_choice_set(df): 17 | non_options = ["date", "Month", "Day", "Year", "Resolutions Met"] 18 | return [x for x in df if x not in non_options and "bool" not in x] 19 | 20 | 21 | def get_resolution_columns_and_values(df): 22 | choice_set = get_resolution_choice_set(df) 23 | options = "" 24 | for col in choice_set: 25 | values = df[col].unique() 26 | options += "{0:20} {1}".format(col, values) 27 | options += "\n" 28 | return options 29 | 30 | 31 | def get_columns(prompt, df): 32 | cols = input(prompt) 33 | cols = "".join(cols.split()) # remove whitespace 34 | all_options = get_resolution_choice_set(df) 35 | 36 | if cols.lower() == "all": 37 | return all_options 38 | 39 | col_list = cols.split(",") 40 | col_list = [x for x in col_list if x] # remove empty elements 41 | validated_list = [] 42 | for col in col_list: 43 | if col not in all_options: 44 | print(f"`{col}` is not a valid column -- removing from list to generate minimaps from") 45 | else: 46 | validated_list.append(col) 47 | return validated_list 48 | 49 | 50 | # Read the CSV 51 | filepath = f"../data/cleaned/{filename}" 52 | if not os.path.exists(filepath): 53 | sys.exit(f"filepath={filepath} does not exist") 54 | 55 | df = pd.read_csv(filepath) 56 | options = get_resolution_columns_and_values(df) 57 | print(f"Preview of data from uploaded dataset:\n{options}\n") 58 | 59 | instructions = "Enter a comma-separated list of columns containing resolution data (e.g. exercise,skincare).\n" \ 60 | "Enter 'all' to use all columns.\n" 61 | print(f"INSTRUCTIONS: {instructions}") 62 | col_list = [] 63 | while len(col_list) == 0: 64 | col_list = get_columns( 65 | "Which resolutions do you want to backpopulate?: ", 66 | df 67 | ) 68 | 69 | # Load app data 70 | with open("../data/resolutions.json", "r") as f: 71 | app_data = json.load(f) 72 | 73 | # Loop through each resolution column 74 | days = len(df) 75 | curr_day = 0 76 | curr_col = 0 77 | data_end = len(col_list) - 1 78 | 79 | while curr_col <= data_end: 80 | col_name = col_list[curr_col] 81 | print(f"*** Adding data from resolution={col_name}") 82 | 83 | # Check whether this resolution already exists in app data 84 | exists = col_name.lower() in app_data 85 | first_date = df["date"][0] 86 | last_date = df["date"][days - 1] 87 | 88 | if exists: 89 | merge_data = handle_input(prompt=f"res={col_name} already exists. Merge with existing data? (Y/N): ", 90 | response_type="boolean") 91 | if merge_data: 92 | print(f"*** Merging res={col_name}") 93 | res_id = col_name.lower() 94 | is_binary = app_data[res_id]["is_binary"] 95 | 96 | # Calculate res_creation_date (first date entry from current data OR res_creation_date of existing 97 | # resolution, whichever is earlier) 98 | first_date_dt = datetime.strptime(first_date, "%m/%d/%Y") 99 | exist_creation_date = app_data[res_id]["res_creation_date"] 100 | exist_creation_date_dt = datetime.strptime(exist_creation_date, "%m/%d/%Y") 101 | res_creation_date = first_date if first_date_dt < exist_creation_date_dt else exist_creation_date 102 | 103 | # Expiration is last date entry from current data OR res_expiration_date of existing resolution, 104 | # whichever is later 105 | last_date_dt = datetime.strptime(last_date, "%m/%d/%Y") 106 | exist_expiration_date = app_data[res_id]["res_expiration_date"] 107 | if exist_expiration_date is not None: 108 | exist_expiration_date_dt = datetime.strptime(exist_expiration_date, "%m/%d/%Y") 109 | res_expiration_date = last_date if last_date_dt > exist_expiration_date_dt else exist_expiration_date 110 | else: 111 | res_expiration_date = exist_expiration_date 112 | 113 | app_data[res_id].update( 114 | { 115 | "res_creation_date": res_creation_date, 116 | "res_expiration_date": res_expiration_date, 117 | } 118 | ) 119 | 120 | # New resolution without precedent 121 | if (not exists) or (not merge_data): 122 | if exists: 123 | res_id = handle_input(f"Let's give this resolution a new res_id to differentiate it from {col_name}: ") 124 | else: 125 | keep_name = handle_input(prompt=f"Want to keep the res_id `{col_name}`? (Y/N): ", response_type="boolean") 126 | if keep_name: 127 | res_id = col_name 128 | else: 129 | res_id = handle_input("Let's give this resolution a new res_id: ") 130 | print(f"*** Creating a new resolution res={res_id}") 131 | res_descript = handle_input("Provide a short description for this resolution: ") 132 | res_creation_date = df.loc[[0], "date"].values[0] # first date entry from current data 133 | is_active = handle_input(prompt="Is this an active resolution? (Y/N): ", response_type="boolean") 134 | is_expired = handle_input(prompt=f"Did this resolution expire on {last_date}? (Y/N): ", response_type="boolean") 135 | if is_expired: 136 | res_expiration_date = last_date 137 | else: 138 | res_expiration_date = handle_input( 139 | prompt="When does this resolution expire? ('MM/DD/YYYY' or 'never' for no expiration): ", 140 | response_type="datestring" 141 | ) 142 | is_binary = handle_input( 143 | prompt="Is this resolution's outcome binary? (Y/N): ", 144 | response_type="boolean", 145 | instructions="Binary outcomes tell us whether or not you did something, while categorical outcomes " 146 | "tell us about the kind of thing you did.\n" 147 | "For example, for the resolution to exercise, " 148 | "a binary outcome is exercising, or not exercising. " 149 | "In contrast, a categorical outcome names the kind of exercise you did (e.g. " 150 | "run/bike/swim). " 151 | ) 152 | 153 | res = { 154 | "res_descript": res_descript, 155 | "res_creation_date": res_creation_date, 156 | "is_active": is_active, 157 | "res_expiration_date": res_expiration_date, 158 | "is_binary": is_binary, 159 | "res_detail_codes": {}, 160 | "data": {}, 161 | } 162 | app_data[res_id] = res 163 | 164 | # Populate the data dict for both merged and new resolutions 165 | print("*** Backpopulating resolutions data") 166 | detail_codes = app_data[res_id]["res_detail_codes"] 167 | code_translator = {} # keep track of recoded datapoints 168 | 169 | while curr_day < days: 170 | datapoint = df.loc[[curr_day], col_name].values[0] 171 | if not datapoint or datapoint == "0" or datapoint == "0.0": # binary and non-binary falsy value 172 | datapoint = False 173 | if is_binary: 174 | if datapoint: # binary truthy value 175 | datapoint = True 176 | else: 177 | if datapoint: # non-binary truthy value 178 | # Check the translator first to see if previously encountered 179 | datapoint = code_translator.get(datapoint, datapoint) 180 | # Add to detail codes if seeing for first time 181 | if datapoint not in detail_codes: 182 | keep_code = handle_input(prompt=f"Keep the code `{datapoint}`? (Y/N): ", response_type="boolean") 183 | if keep_code: # add new code to res_detail_codes without changing datapoint 184 | descript = handle_input(f"What activity does `{datapoint}` stand for?: ") 185 | detail_codes[datapoint] = descript 186 | else: # add new code to res_detail_codes and change datapoint 187 | code = handle_input(f"Enter a 1-char code to replace `{datapoint}`: ").upper() 188 | if code not in detail_codes: 189 | descript = handle_input(f"What activity does `{code}` stand for?: ") 190 | detail_codes[code] = descript 191 | code_translator[datapoint] = code 192 | datapoint = code 193 | date = df["date"][curr_day] 194 | app_data[res_id]["data"][date] = datapoint 195 | curr_day += 1 196 | curr_day = 0 197 | curr_col += 1 198 | 199 | print("*** Making backup copy of current resolutions data before save") 200 | ts = int(datetime.now().timestamp()) 201 | backups_path = "../data/backups" 202 | if not os.path.exists(backups_path): 203 | os.makedirs(backups_path) 204 | shutil.copy("../data/resolutions.json", f"{backups_path}/resolutions_{ts}.json") 205 | print("*** Saving backpopulated resolutions data") 206 | with open("../data/resolutions.json", "w+") as f: 207 | json.dump(app_data, f, indent=4) 208 | print("*** Saved!") 209 | -------------------------------------------------------------------------------- /cli_resolve/utils/graph_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | import os 4 | import pandas as pd 5 | import seaborn as sns 6 | from matplotlib import pyplot as plt 7 | from .export_utils import get_years_list 8 | from .input_utils import * 9 | 10 | 11 | def print_files(list_of_files): 12 | """ 13 | Print a formatted list of files along with their index. 14 | """ 15 | files = "" 16 | for idx, f in enumerate(list_of_files): 17 | files += "{0:30} {1}".format(f, idx) 18 | files += "\n" 19 | print("\n{0:30} {1}".format("FILE", "INDEX")) 20 | print(files) 21 | 22 | 23 | def export_graph_from_file(): 24 | """ 25 | Generate and export heatmaps from existing file. 26 | """ 27 | path = "data/cleaned" 28 | dir_list = os.listdir(path) 29 | dir_list.sort() 30 | print_files(dir_list) 31 | index = handle_input(prompt="Enter the index of the file from which you wish to generate graphs: ", 32 | response_type="index", 33 | num_cols=len(dir_list)) 34 | file = dir_list[index] 35 | filepath = f"data/cleaned/{file}" 36 | 37 | export_minimaps = handle_input(prompt="Do you want minimaps for select resolutions? (Y/N): ", 38 | response_type="boolean") 39 | generate_heatmap(filepath) 40 | if export_minimaps: 41 | generate_minimaps(filepath) 42 | plt.show() 43 | 44 | 45 | def generate_heatmap(filepath, years_list=None, notable_days=None): 46 | """ 47 | Generate a heatmap from data in which values denote # of resolutions met that day. 48 | Graph is displayed on-screen as well as temporarily saved to data/exports as temp_graph.pdf. 49 | 50 | For aesthetic reasons, maps with multi-year data are displayed differently from maps containing data from a single 51 | year. If the name of a valid JSON file is passed into notable_days, annotations will be overlaid onto the map. 52 | """ 53 | df = pd.read_csv(filepath) 54 | if not years_list: 55 | start_date_str = df["date"][0] 56 | end_date_str = df["date"][len(df) - 1] 57 | years_list = get_years_list(start_date_str, end_date_str) 58 | num_years = len(years_list) 59 | 60 | if num_years == 1: 61 | # Date range within single year 62 | print("*** Generating heatmap") 63 | plt.rcParams["figure.figsize"] = (12, 8) 64 | 65 | df_map = df.pivot(index="Month", columns="Day", values="Resolutions Met") 66 | nyr_map = sns.heatmap( 67 | df_map, 68 | cmap="inferno", 69 | square=True, 70 | vmin=0, 71 | cbar_kws={'orientation': 'horizontal'}, 72 | xticklabels=True, 73 | yticklabels=True, 74 | ) 75 | nyr_map.set( 76 | title=f"{df['date'][0]} - {df['date'][len(df['date']) - 1]}" 77 | ) 78 | plt.xticks(rotation=0) 79 | plt.yticks(rotation=0) 80 | else: 81 | # Date range spanning multiple years 82 | print("*** Generating multi-year heatmap") 83 | inches_per_year = 5 84 | graph_height = inches_per_year * num_years 85 | plt.rcParams["figure.figsize"] = (12, graph_height) 86 | 87 | df_map = df.pivot(index=["Year", "Month"], columns="Day", values="Resolutions Met") 88 | nyr_map = sns.heatmap( 89 | df_map, 90 | cmap="inferno", 91 | square=True, 92 | cbar_kws={ 93 | 'orientation': 'horizontal', 94 | 'fraction': 0.04, 95 | 'pad': 0.08 96 | }, 97 | xticklabels=True, 98 | yticklabels=True, 99 | ) 100 | nyr_map.set( 101 | title=f"{df['date'][0]} - {df['date'][len(df['date']) - 1]}" 102 | ) 103 | 104 | # Generate special tick labels that display the year for only the first month of that year 105 | start_year = df["Year"][0] 106 | end_year = df["Year"][len(df["Year"]) - 1] 107 | start_month = 1 108 | end_month = 12 109 | year_month_labs = [] 110 | for year in range(start_year, end_year + 1): 111 | for month in range(start_month, end_month + 1): 112 | if month == 1: 113 | year_month_labs.append(f"{year} - {month}") 114 | else: 115 | year_month_labs.append(month) 116 | nyr_map.set_xticklabels(nyr_map.get_xticklabels(), rotation=0) 117 | nyr_map.set_yticklabels(year_month_labs, rotation=0) 118 | 119 | if notable_days: 120 | # Load from JSON file passed into parameter 121 | try: 122 | with open(f"data/{notable_days}.json", "r") as f: 123 | notable_days = json.load(f) 124 | 125 | for date, descript in notable_days.items(): 126 | date_list = date.split("/") 127 | month = int(date_list[0]) 128 | day = int(date_list[1]) 129 | year = int(date_list[2]) 130 | if num_years == 1: 131 | date_coords = (day - 1, month - 1) 132 | else: 133 | months_in_year = 12 134 | year_idx = years_list.index(year) 135 | year_month = (year_idx * months_in_year) + month 136 | date_coords = (day - 1, year_month - 1) 137 | rect = plt.Rectangle(date_coords, 138 | width=1, 139 | height=1, 140 | color="white", 141 | linewidth=0, 142 | fill=False, 143 | hatch='..', 144 | alpha=0.6) 145 | nyr_map.add_patch(rect) 146 | nyr_map.text(day - 0.5, 147 | month - 0.5 if num_years == 1 else year_month - 0.5, 148 | descript, 149 | horizontalalignment='left', 150 | verticalalignment='center', 151 | size='small', 152 | color='white', 153 | bbox=dict(boxstyle='round', fc='black')) 154 | except Exception as e: 155 | print(f"No such file: {e}") 156 | 157 | print("*** Saving to data/exports folder") 158 | plt.savefig("data/exports/temp_graph.pdf", orientation='portrait') 159 | 160 | 161 | def get_resolution_choice_set(df): 162 | """ 163 | Returns a list of resolution columns from the given dataframe (excluding columns that are not resolutions, such as 164 | date columns). 165 | """ 166 | non_options = ["date", "Month", "Day", "Year", "Resolutions Met"] 167 | return [x for x in df if x not in non_options] 168 | 169 | 170 | def get_resolution_columns_and_values(df): 171 | """ 172 | Returns a formatted string containing all options for generating minimaps. 173 | First column contains the resolution column name. 174 | Second column contains a preview of the values in that column. 175 | """ 176 | choice_set = get_resolution_choice_set(df) 177 | options = "" 178 | for col in choice_set: 179 | values = df[col].unique() 180 | options += "{0:20} {1}".format(col, values) 181 | options += "\n" 182 | return options 183 | 184 | 185 | def get_columns(prompt, df): 186 | """ 187 | Returns a list of validated columns from which to generate minimaps. 188 | """ 189 | cols = input(prompt) 190 | cols = "".join(cols.split()) # remove whitespace 191 | all_options = get_resolution_choice_set(df) 192 | 193 | if cols.lower() == "all": 194 | return all_options 195 | if cols.lower() == "binary": 196 | return [x for x in all_options if "bool" in x] 197 | if cols.lower() == "nonbinary": 198 | return [x for x in all_options if "bool" not in x] 199 | 200 | col_list = cols.split(",") 201 | col_list = [x for x in col_list if x] # remove empty elements 202 | validated_list = [] 203 | for col in col_list: 204 | if col not in all_options: 205 | print(f"`{col}` is not a valid column -- removing from list to generate minimaps from") 206 | else: 207 | validated_list.append(col) 208 | return validated_list 209 | 210 | 211 | def optimize_display_size(num_cols, num_rows, num_years): 212 | """ 213 | Calculates an aesthetically pleasing (or at least, aesthetically acceptable) height and width for the display based 214 | on both the dimensions of the grid containing the minimaps and the number of years covered by the data. 215 | """ 216 | # Determine height and width of final figure 217 | months_in_year = 12 218 | days_in_month = 31 219 | h = months_in_year * num_years * num_rows 220 | w = days_in_month * num_cols 221 | # Set inches per horizontal and vertical unit 222 | inches_per_col = 2 223 | inches_per_year = 4.5 224 | # Set minimum display sizes for long and short sides of canvas 225 | min_long = 12 226 | min_short = 8 227 | if w > h: # wide display 228 | graph_width = min_long if inches_per_col * num_cols < min_long else inches_per_col * num_cols 229 | graph_height = min_short if inches_per_year * num_years < min_short else inches_per_year * num_years 230 | else: # long display 231 | graph_width = min_short if inches_per_col * num_cols < min_short else inches_per_col * num_cols 232 | graph_height = min_long if inches_per_year * num_years < min_long else inches_per_year * num_years 233 | return graph_height, graph_width 234 | 235 | 236 | def calculate_grid_dimensions(num_maps): 237 | """ 238 | Calculates the number of columns and rows for the grid containing the minimaps based on the number of minimaps to be 239 | displayed, based on the "smallest squarish grid" heuristic. 240 | 241 | The smallest squarish grid is the grid in which num_cols * num_rows >= num_maps and num_cols <= num_rows + 1. 242 | """ 243 | num_cols = math.ceil(math.sqrt(num_maps)) 244 | num_rows = num_cols 245 | while (num_rows * num_cols) >= num_maps: 246 | if ((num_rows - 1) * num_cols) >= num_maps: 247 | num_rows -= 1 248 | else: 249 | break 250 | return num_cols, num_rows 251 | 252 | 253 | def generate_minimaps(filename, years_list=None): 254 | """ 255 | Generate mini heatmaps from data. 256 | Each minimap corresponds to a resolution, and values indicate whether the resolution was met that day. 257 | Minimaps can display both binary and non-binary data. 258 | Graph is displayed on-screen as well as temporarily saved to data/exports as temp_minimaps.pdf. 259 | """ 260 | df = pd.read_csv(filename) 261 | if not years_list: 262 | start_date_str = df["date"][0] 263 | end_date_str = df["date"][len(df) - 1] 264 | years_list = get_years_list(start_date_str, end_date_str) 265 | num_years = len(years_list) 266 | 267 | options = get_resolution_columns_and_values(df) 268 | print(f"\nPreview of data from uploaded dataset:\n{options}") 269 | 270 | instructions = "Enter a comma-separated list of columns to create minimaps from (e.g. exercise,skincare).\n" \ 271 | "You may also enter:\n" \ 272 | "--'all' to use all columns, \n" \ 273 | "--'binary' to use all boolean columns, or\n" \ 274 | "--'nonbinary' to use all non-boolean columns\n" 275 | print(f"INSTRUCTIONS: {instructions}") 276 | col_list = [] 277 | while len(col_list) == 0: 278 | col_list = get_columns( 279 | "Which resolutions do you want to create minimaps from?: ", 280 | df 281 | ) 282 | 283 | # Calculate the smallest squarish grid that will hold all plots 284 | num_maps = len(col_list) 285 | num_cols, num_rows = calculate_grid_dimensions(num_maps) 286 | 287 | graph_height, graph_width = optimize_display_size(num_cols, num_rows, num_years) 288 | plt.rcParams["figure.figsize"] = (graph_width, graph_height) 289 | 290 | fig, axes = plt.subplots( 291 | nrows=num_rows, 292 | ncols=num_cols, 293 | sharex=True, 294 | sharey=True 295 | ) 296 | i = 0 297 | j = 0 298 | for res in col_list: 299 | try: 300 | if num_years == 1: 301 | df_map = df.pivot(index="Month", columns="Day", values=res) 302 | else: 303 | df_map = df.pivot(index=["Year", "Month"], columns="Day", values=res) 304 | 305 | # Use qualitative colormap to display non-binary resolutions and show cbar 306 | # TODO: display differently days in which multiple non-binary resolutions were fulfilled 307 | if "bool" not in res: 308 | # Get categories from categorical data 309 | categories = pd.unique(df_map.values.ravel()) 310 | categories = [x for x in categories if not pd.isnull(x)] 311 | cat_to_int = {j: i for i, j in enumerate(categories)} 312 | n = len(cat_to_int) 313 | df_map = df_map.replace(cat_to_int) 314 | # Generate colors based on a cmap, with 0's as white 315 | if n > 2: 316 | palette = sns.color_palette("turbo", n - 1) 317 | colors = ["white"] + palette 318 | else: # if there's only 2 categories (including 0) it might as well be binary 319 | colors = ["white", "black"] 320 | display_cbar = True 321 | else: 322 | colors = "binary" 323 | display_cbar = False 324 | 325 | nyr_map = sns.heatmap( 326 | df_map, 327 | cmap=colors, 328 | square=True, 329 | cbar=display_cbar, 330 | cbar_kws={ 331 | "orientation": "horizontal", 332 | "fraction": 0.03, 333 | "pad": 0.08 334 | }, 335 | ax=axes[i][j] if num_maps > 2 else axes[j] if num_maps == 2 else axes 336 | ) 337 | 338 | # Modify cbar for categorical resolutions 339 | if display_cbar: 340 | colorbar = nyr_map.collections[0].colorbar 341 | r = colorbar.vmax - colorbar.vmin 342 | colorbar.set_ticks([colorbar.vmin + r / n * (0.5 + i) for i in range(n)]) 343 | colorbar.set_ticklabels(list(cat_to_int.keys())) 344 | 345 | # Turn off labels on minimaps 346 | nyr_map.set( 347 | title=f"{res}", 348 | xlabel="", 349 | ylabel="", 350 | ) 351 | # Set tick bins and rotation 352 | xbins = 7 # weekly ticks 353 | if num_years == 1: 354 | ybins = 2 # semiannual ticks 355 | else: 356 | ybins = num_years 357 | nyr_map.locator_params(axis='x', nbins=xbins) 358 | nyr_map.tick_params(axis='x', labelrotation=0) 359 | nyr_map.locator_params(axis='y', nbins=ybins) 360 | nyr_map.tick_params(axis='y', labelrotation=90) 361 | 362 | # Keep border around each subplot 363 | sns.despine( 364 | top=False, 365 | right=False, 366 | left=False, 367 | bottom=False, 368 | ) 369 | 370 | # Fill grid across columns first, then move down to next row 371 | if num_maps > 2: 372 | if j < num_cols - 1: 373 | j += 1 374 | else: 375 | j = 0 376 | i += 1 377 | else: 378 | j += 1 379 | except Exception as e: 380 | print(f"Something went wrong: {e}") 381 | 382 | # Remove empty plots from grid by continuing to read through cols, then rows, until out of rows 383 | if num_maps > 2: 384 | while i < num_rows: 385 | fig.delaxes(axes[i][j]) 386 | if j < num_cols - 1: 387 | j += 1 388 | else: 389 | j = 0 390 | i += 1 391 | 392 | fig.subplots_adjust(hspace=0.2, wspace=0) 393 | 394 | # Add background axis with hidden frame for common xlabel and ylabel 395 | fig.add_subplot(111, frameon=False) 396 | plt.tick_params(labelcolor='none', which='both', top=False, bottom=False, left=False, right=False) 397 | plt.xlabel("Day") 398 | if num_years == 1: 399 | plt.ylabel("Month") 400 | else: 401 | plt.ylabel("Year-Month") 402 | 403 | print("*** Saving to data/exports folder") 404 | plt.savefig("data/exports/temp_minimaps.pdf", dpi=300) 405 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "81ff246840b1c8350bf3998daf2eba226831e6697346aa368c53313bbc50c9a0" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "contourpy": { 20 | "hashes": [ 21 | "sha256:0236875c5a0784215b49d00ebbe80c5b6b5d5244b3655a36dda88105334dea17", 22 | "sha256:03d1b9c6b44a9e30d554654c72be89af94fab7510b4b9f62356c64c81cec8b7d", 23 | "sha256:0537cc1195245bbe24f2913d1f9211b8f04eb203de9044630abd3664c6cc339c", 24 | "sha256:06ca79e1efbbe2df795822df2fa173d1a2b38b6e0f047a0ec7903fbca1d1847e", 25 | "sha256:08e8d09d96219ace6cb596506fb9b64ea5f270b2fb9121158b976d88871fcfd1", 26 | "sha256:0b1e66346acfb17694d46175a0cea7d9036f12ed0c31dfe86f0f405eedde2bdd", 27 | "sha256:0b97454ed5b1368b66ed414c754cba15b9750ce69938fc6153679787402e4cdf", 28 | "sha256:0e4854cc02006ad6684ce092bdadab6f0912d131f91c2450ce6dbdea78ee3c0b", 29 | "sha256:12a7dc8439544ed05c6553bf026d5e8fa7fad48d63958a95d61698df0e00092b", 30 | "sha256:1b1ee48a130da4dd0eb8055bbab34abf3f6262957832fd575e0cab4979a15a41", 31 | "sha256:1c0e1308307a75e07d1f1b5f0f56b5af84538a5e9027109a7bcf6cb47c434e72", 32 | "sha256:1dedf4c64185a216c35eb488e6f433297c660321275734401760dafaeb0ad5c2", 33 | "sha256:208bc904889c910d95aafcf7be9e677726df9ef71e216780170dbb7e37d118fa", 34 | "sha256:211dfe2bd43bf5791d23afbe23a7952e8ac8b67591d24be3638cabb648b3a6eb", 35 | "sha256:341330ed19074f956cb20877ad8d2ae50e458884bfa6a6df3ae28487cc76c768", 36 | "sha256:344cb3badf6fc7316ad51835f56ac387bdf86c8e1b670904f18f437d70da4183", 37 | "sha256:358f6364e4873f4d73360b35da30066f40387dd3c427a3e5432c6b28dd24a8fa", 38 | "sha256:371f6570a81dfdddbb837ba432293a63b4babb942a9eb7aaa699997adfb53278", 39 | "sha256:375d81366afd547b8558c4720337218345148bc2fcffa3a9870cab82b29667f2", 40 | "sha256:3a1917d3941dd58732c449c810fa7ce46cc305ce9325a11261d740118b85e6f3", 41 | "sha256:4081918147fc4c29fad328d5066cfc751da100a1098398742f9f364be63803fc", 42 | "sha256:444fb776f58f4906d8d354eb6f6ce59d0a60f7b6a720da6c1ccb839db7c80eb9", 43 | "sha256:46deb310a276cc5c1fd27958e358cce68b1e8a515fa5a574c670a504c3a3fe30", 44 | "sha256:494efed2c761f0f37262815f9e3c4bb9917c5c69806abdee1d1cb6611a7174a0", 45 | "sha256:50627bf76abb6ba291ad08db583161939c2c5fab38c38181b7833423ab9c7de3", 46 | "sha256:5641927cc5ae66155d0c80195dc35726eae060e7defc18b7ab27600f39dd1fe7", 47 | "sha256:5b117d29433fc8393b18a696d794961464e37afb34a6eeb8b2c37b5f4128a83e", 48 | "sha256:613c665529899b5d9fade7e5d1760111a0b011231277a0d36c49f0d3d6914bd6", 49 | "sha256:6e459ebb8bb5ee4c22c19cc000174f8059981971a33ce11e17dddf6aca97a142", 50 | "sha256:6f56515e7c6fae4529b731f6c117752247bef9cdad2b12fc5ddf8ca6a50965a5", 51 | "sha256:730c27978a0003b47b359935478b7d63fd8386dbb2dcd36c1e8de88cbfc1e9de", 52 | "sha256:75a2e638042118118ab39d337da4c7908c1af74a8464cad59f19fbc5bbafec9b", 53 | "sha256:78ced51807ccb2f45d4ea73aca339756d75d021069604c2fccd05390dc3c28eb", 54 | "sha256:7ee394502026d68652c2824348a40bf50f31351a668977b51437131a90d777ea", 55 | "sha256:8468b40528fa1e15181cccec4198623b55dcd58306f8815a793803f51f6c474a", 56 | "sha256:84c593aeff7a0171f639da92cb86d24954bbb61f8a1b530f74eb750a14685832", 57 | "sha256:913bac9d064cff033cf3719e855d4f1db9f1c179e0ecf3ba9fdef21c21c6a16a", 58 | "sha256:9447c45df407d3ecb717d837af3b70cfef432138530712263730783b3d016512", 59 | "sha256:9b0e7fe7f949fb719b206548e5cde2518ffb29936afa4303d8a1c4db43dcb675", 60 | "sha256:9bc407a6af672da20da74823443707e38ece8b93a04009dca25856c2d9adadb1", 61 | "sha256:9e8e686a6db92a46111a1ee0ee6f7fbfae4048f0019de207149f43ac1812cf95", 62 | "sha256:9fc4e7973ed0e1fe689435842a6e6b330eb7ccc696080dda9a97b1a1b78e41db", 63 | "sha256:a457ee72d9032e86730f62c5eeddf402e732fdf5ca8b13b41772aa8ae13a4563", 64 | "sha256:a628bba09ba72e472bf7b31018b6281fd4cc903f0888049a3724afba13b6e0b8", 65 | "sha256:a79d239fc22c3b8d9d3de492aa0c245533f4f4c7608e5749af866949c0f1b1b9", 66 | "sha256:aa4674cf3fa2bd9c322982644967f01eed0c91bb890f624e0e0daf7a5c3383e9", 67 | "sha256:acd2bd02f1a7adff3a1f33e431eb96ab6d7987b039d2946a9b39fe6fb16a1036", 68 | "sha256:b3b1bd7577c530eaf9d2bc52d1a93fef50ac516a8b1062c3d1b9bcec9ebe329b", 69 | "sha256:b48d94386f1994db7c70c76b5808c12e23ed7a4ee13693c2fc5ab109d60243c0", 70 | "sha256:b64f747e92af7da3b85631a55d68c45a2d728b4036b03cdaba4bd94bcc85bd6f", 71 | "sha256:b98c820608e2dca6442e786817f646d11057c09a23b68d2b3737e6dcb6e4a49b", 72 | "sha256:c1baa49ab9fedbf19d40d93163b7d3e735d9cd8d5efe4cce9907902a6dad391f", 73 | "sha256:c38c6536c2d71ca2f7e418acaf5bca30a3af7f2a2fa106083c7d738337848dbe", 74 | "sha256:c78bfbc1a7bff053baf7e508449d2765964d67735c909b583204e3240a2aca45", 75 | "sha256:cd2bc0c8f2e8de7dd89a7f1c10b8844e291bca17d359373203ef2e6100819edd", 76 | "sha256:d2eff2af97ea0b61381828b1ad6cd249bbd41d280e53aea5cccd7b2b31b8225c", 77 | "sha256:d8834c14b8c3dd849005e06703469db9bf96ba2d66a3f88ecc539c9a8982e0ee", 78 | "sha256:d912f0154a20a80ea449daada904a7eb6941c83281a9fab95de50529bfc3a1da", 79 | "sha256:da1ef35fd79be2926ba80fbb36327463e3656c02526e9b5b4c2b366588b74d9a", 80 | "sha256:dbe6fe7a1166b1ddd7b6d887ea6fa8389d3f28b5ed3f73a8f40ece1fc5a3d340", 81 | "sha256:dcd556c8fc37a342dd636d7eef150b1399f823a4462f8c968e11e1ebeabee769", 82 | "sha256:e13b31d1b4b68db60b3b29f8e337908f328c7f05b9add4b1b5c74e0691180109", 83 | "sha256:e1739496c2f0108013629aa095cc32a8c6363444361960c07493818d0dea2da4", 84 | "sha256:e43255a83835a129ef98f75d13d643844d8c646b258bebd11e4a0975203e018f", 85 | "sha256:e626cefff8491bce356221c22af5a3ea528b0b41fbabc719c00ae233819ea0bf", 86 | "sha256:eadad75bf91897f922e0fb3dca1b322a58b1726a953f98c2e5f0606bd8408621", 87 | "sha256:f33da6b5d19ad1bb5e7ad38bb8ba5c426d2178928bc2b2c44e8823ea0ecb6ff3", 88 | "sha256:f4052a8a4926d4468416fc7d4b2a7b2a3e35f25b39f4061a7e2a3a2748c4fc48", 89 | "sha256:f6ca38dd8d988eca8f07305125dec6f54ac1c518f1aaddcc14d08c01aebb6efc" 90 | ], 91 | "markers": "python_version >= '3.7'", 92 | "version": "==1.0.6" 93 | }, 94 | "cycler": { 95 | "hashes": [ 96 | "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3", 97 | "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f" 98 | ], 99 | "markers": "python_version >= '3.6'", 100 | "version": "==0.11.0" 101 | }, 102 | "fonttools": { 103 | "hashes": [ 104 | "sha256:2bb244009f9bf3fa100fc3ead6aeb99febe5985fa20afbfbaa2f8946c2fbdaf1", 105 | "sha256:820466f43c8be8c3009aef8b87e785014133508f0de64ec469e4efb643ae54fb" 106 | ], 107 | "markers": "python_version >= '3.7'", 108 | "version": "==4.38.0" 109 | }, 110 | "kiwisolver": { 111 | "hashes": [ 112 | "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b", 113 | "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166", 114 | "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c", 115 | "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c", 116 | "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0", 117 | "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4", 118 | "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9", 119 | "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286", 120 | "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767", 121 | "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c", 122 | "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6", 123 | "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b", 124 | "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004", 125 | "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf", 126 | "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494", 127 | "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac", 128 | "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626", 129 | "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766", 130 | "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514", 131 | "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6", 132 | "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f", 133 | "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d", 134 | "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191", 135 | "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d", 136 | "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51", 137 | "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f", 138 | "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8", 139 | "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454", 140 | "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb", 141 | "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da", 142 | "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8", 143 | "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de", 144 | "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a", 145 | "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9", 146 | "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008", 147 | "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3", 148 | "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32", 149 | "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938", 150 | "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1", 151 | "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9", 152 | "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d", 153 | "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824", 154 | "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b", 155 | "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd", 156 | "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2", 157 | "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5", 158 | "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69", 159 | "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3", 160 | "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae", 161 | "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597", 162 | "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e", 163 | "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955", 164 | "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca", 165 | "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a", 166 | "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea", 167 | "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede", 168 | "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4", 169 | "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6", 170 | "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686", 171 | "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408", 172 | "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871", 173 | "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29", 174 | "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750", 175 | "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897", 176 | "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0", 177 | "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2", 178 | "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09", 179 | "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c" 180 | ], 181 | "markers": "python_version >= '3.7'", 182 | "version": "==1.4.4" 183 | }, 184 | "matplotlib": { 185 | "hashes": [ 186 | "sha256:0844523dfaaff566e39dbfa74e6f6dc42e92f7a365ce80929c5030b84caa563a", 187 | "sha256:0eda9d1b43f265da91fb9ae10d6922b5a986e2234470a524e6b18f14095b20d2", 188 | "sha256:168093410b99f647ba61361b208f7b0d64dde1172b5b1796d765cd243cadb501", 189 | "sha256:1836f366272b1557a613f8265db220eb8dd883202bbbabe01bad5a4eadfd0c95", 190 | "sha256:19d61ee6414c44a04addbe33005ab1f87539d9f395e25afcbe9a3c50ce77c65c", 191 | "sha256:252957e208c23db72ca9918cb33e160c7833faebf295aaedb43f5b083832a267", 192 | "sha256:32d29c8c26362169c80c5718ce367e8c64f4dd068a424e7110df1dd2ed7bd428", 193 | "sha256:380d48c15ec41102a2b70858ab1dedfa33eb77b2c0982cb65a200ae67a48e9cb", 194 | "sha256:3964934731fd7a289a91d315919cf757f293969a4244941ab10513d2351b4e83", 195 | "sha256:3cef89888a466228fc4e4b2954e740ce8e9afde7c4315fdd18caa1b8de58ca17", 196 | "sha256:4426c74761790bff46e3d906c14c7aab727543293eed5a924300a952e1a3a3c1", 197 | "sha256:5024b8ed83d7f8809982d095d8ab0b179bebc07616a9713f86d30cf4944acb73", 198 | "sha256:52c2bdd7cd0bf9d5ccdf9c1816568fd4ccd51a4d82419cc5480f548981b47dd0", 199 | "sha256:54fa9fe27f5466b86126ff38123261188bed568c1019e4716af01f97a12fe812", 200 | "sha256:5ba73aa3aca35d2981e0b31230d58abb7b5d7ca104e543ae49709208d8ce706a", 201 | "sha256:5e16dcaecffd55b955aa5e2b8a804379789c15987e8ebd2f32f01398a81e975b", 202 | "sha256:5ecfc6559132116dedfc482d0ad9df8a89dc5909eebffd22f3deb684132d002f", 203 | "sha256:74153008bd24366cf099d1f1e83808d179d618c4e32edb0d489d526523a94d9f", 204 | "sha256:78ec3c3412cf277e6252764ee4acbdbec6920cc87ad65862272aaa0e24381eee", 205 | "sha256:795ad83940732b45d39b82571f87af0081c120feff2b12e748d96bb191169e33", 206 | "sha256:7f716b6af94dc1b6b97c46401774472f0867e44595990fe80a8ba390f7a0a028", 207 | "sha256:83dc89c5fd728fdb03b76f122f43b4dcee8c61f1489e232d9ad0f58020523e1c", 208 | "sha256:8a0ae37576ed444fe853709bdceb2be4c7df6f7acae17b8378765bd28e61b3ae", 209 | "sha256:8a8dbe2cb7f33ff54b16bb5c500673502a35f18ac1ed48625e997d40c922f9cc", 210 | "sha256:8a9d899953c722b9afd7e88dbefd8fb276c686c3116a43c577cfabf636180558", 211 | "sha256:8d0068e40837c1d0df6e3abf1cdc9a34a6d2611d90e29610fa1d2455aeb4e2e5", 212 | "sha256:9347cc6822f38db2b1d1ce992f375289670e595a2d1c15961aacbe0977407dfc", 213 | "sha256:9f335e5625feb90e323d7e3868ec337f7b9ad88b5d633f876e3b778813021dab", 214 | "sha256:b03fd10a1709d0101c054883b550f7c4c5e974f751e2680318759af005964990", 215 | "sha256:b0ca2c60d3966dfd6608f5f8c49b8a0fcf76de6654f2eda55fc6ef038d5a6f27", 216 | "sha256:b2604c6450f9dd2c42e223b1f5dca9643a23cfecc9fde4a94bb38e0d2693b136", 217 | "sha256:ca0e7a658fbafcddcaefaa07ba8dae9384be2343468a8e011061791588d839fa", 218 | "sha256:d0e9ac04065a814d4cf2c6791a2ad563f739ae3ae830d716d54245c2b96fead6", 219 | "sha256:d50e8c1e571ee39b5dfbc295c11ad65988879f68009dd281a6e1edbc2ff6c18c", 220 | "sha256:d840adcad7354be6f2ec28d0706528b0026e4c3934cc6566b84eac18633eab1b", 221 | "sha256:e0bbee6c2a5bf2a0017a9b5e397babb88f230e6f07c3cdff4a4c4bc75ed7c617", 222 | "sha256:e5afe0a7ea0e3a7a257907060bee6724a6002b7eec55d0db16fd32409795f3e1", 223 | "sha256:e68be81cd8c22b029924b6d0ee814c337c0e706b8d88495a617319e5dd5441c3", 224 | "sha256:ec9be0f4826cdb3a3a517509dcc5f87f370251b76362051ab59e42b6b765f8c4", 225 | "sha256:f04f97797df35e442ed09f529ad1235d1f1c0f30878e2fe09a2676b71a8801e0", 226 | "sha256:f41e57ad63d336fe50d3a67bb8eaa26c09f6dda6a59f76777a99b8ccd8e26aec" 227 | ], 228 | "index": "pypi", 229 | "version": "==3.6.2" 230 | }, 231 | "numpy": { 232 | "hashes": [ 233 | "sha256:01dd17cbb340bf0fc23981e52e1d18a9d4050792e8fb8363cecbf066a84b827d", 234 | "sha256:06005a2ef6014e9956c09ba07654f9837d9e26696a0470e42beedadb78c11b07", 235 | "sha256:09b7847f7e83ca37c6e627682f145856de331049013853f344f37b0c9690e3df", 236 | "sha256:0aaee12d8883552fadfc41e96b4c82ee7d794949e2a7c3b3a7201e968c7ecab9", 237 | "sha256:0cbe9848fad08baf71de1a39e12d1b6310f1d5b2d0ea4de051058e6e1076852d", 238 | "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a", 239 | "sha256:33161613d2269025873025b33e879825ec7b1d831317e68f4f2f0f84ed14c719", 240 | "sha256:5039f55555e1eab31124a5768898c9e22c25a65c1e0037f4d7c495a45778c9f2", 241 | "sha256:522e26bbf6377e4d76403826ed689c295b0b238f46c28a7251ab94716da0b280", 242 | "sha256:56e454c7833e94ec9769fa0f86e6ff8e42ee38ce0ce1fa4cbb747ea7e06d56aa", 243 | "sha256:58f545efd1108e647604a1b5aa809591ccd2540f468a880bedb97247e72db387", 244 | "sha256:5e05b1c973a9f858c74367553e236f287e749465f773328c8ef31abe18f691e1", 245 | "sha256:7903ba8ab592b82014713c491f6c5d3a1cde5b4a3bf116404e08f5b52f6daf43", 246 | "sha256:8969bfd28e85c81f3f94eb4a66bc2cf1dbdc5c18efc320af34bffc54d6b1e38f", 247 | "sha256:92c8c1e89a1f5028a4c6d9e3ccbe311b6ba53694811269b992c0b224269e2398", 248 | "sha256:9c88793f78fca17da0145455f0d7826bcb9f37da4764af27ac945488116efe63", 249 | "sha256:a7ac231a08bb37f852849bbb387a20a57574a97cfc7b6cabb488a4fc8be176de", 250 | "sha256:abdde9f795cf292fb9651ed48185503a2ff29be87770c3b8e2a14b0cd7aa16f8", 251 | "sha256:af1da88f6bc3d2338ebbf0e22fe487821ea4d8e89053e25fa59d1d79786e7481", 252 | "sha256:b2a9ab7c279c91974f756c84c365a669a887efa287365a8e2c418f8b3ba73fb0", 253 | "sha256:bf837dc63ba5c06dc8797c398db1e223a466c7ece27a1f7b5232ba3466aafe3d", 254 | "sha256:ca51fcfcc5f9354c45f400059e88bc09215fb71a48d3768fb80e357f3b457e1e", 255 | "sha256:ce571367b6dfe60af04e04a1834ca2dc5f46004ac1cc756fb95319f64c095a96", 256 | "sha256:d208a0f8729f3fb790ed18a003f3a57895b989b40ea4dce4717e9cf4af62c6bb", 257 | "sha256:dbee87b469018961d1ad79b1a5d50c0ae850000b639bcb1b694e9981083243b6", 258 | "sha256:e9f4c4e51567b616be64e05d517c79a8a22f3606499941d97bb76f2ca59f982d", 259 | "sha256:f063b69b090c9d918f9df0a12116029e274daf0181df392839661c4c7ec9018a", 260 | "sha256:f9a909a8bae284d46bbfdefbdd4a262ba19d3bc9921b1e76126b1d21c3c34135" 261 | ], 262 | "markers": "python_version >= '3.8'", 263 | "version": "==1.23.5" 264 | }, 265 | "packaging": { 266 | "hashes": [ 267 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", 268 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" 269 | ], 270 | "markers": "python_version >= '3.6'", 271 | "version": "==21.3" 272 | }, 273 | "pandas": { 274 | "hashes": [ 275 | "sha256:0183cb04a057cc38fde5244909fca9826d5d57c4a5b7390c0cc3fa7acd9fa883", 276 | "sha256:1fc87eac0541a7d24648a001d553406f4256e744d92df1df8ebe41829a915028", 277 | "sha256:220b98d15cee0b2cd839a6358bd1f273d0356bf964c1a1aeb32d47db0215488b", 278 | "sha256:2552bffc808641c6eb471e55aa6899fa002ac94e4eebfa9ec058649122db5824", 279 | "sha256:315e19a3e5c2ab47a67467fc0362cb36c7c60a93b6457f675d7d9615edad2ebe", 280 | "sha256:344021ed3e639e017b452aa8f5f6bf38a8806f5852e217a7594417fb9bbfa00e", 281 | "sha256:375262829c8c700c3e7cbb336810b94367b9c4889818bbd910d0ecb4e45dc261", 282 | "sha256:457d8c3d42314ff47cc2d6c54f8fc0d23954b47977b2caed09cd9635cb75388b", 283 | "sha256:4aed257c7484d01c9a194d9a94758b37d3d751849c05a0050c087a358c41ad1f", 284 | "sha256:530948945e7b6c95e6fa7aa4be2be25764af53fba93fe76d912e35d1c9ee46f5", 285 | "sha256:5ae7e989f12628f41e804847a8cc2943d362440132919a69429d4dea1f164da0", 286 | "sha256:71f510b0efe1629bf2f7c0eadb1ff0b9cf611e87b73cd017e6b7d6adb40e2b3a", 287 | "sha256:73f219fdc1777cf3c45fde7f0708732ec6950dfc598afc50588d0d285fddaefc", 288 | "sha256:8092a368d3eb7116e270525329a3e5c15ae796ccdf7ccb17839a73b4f5084a39", 289 | "sha256:82ae615826da838a8e5d4d630eb70c993ab8636f0eff13cb28aafc4291b632b5", 290 | "sha256:9608000a5a45f663be6af5c70c3cbe634fa19243e720eb380c0d378666bc7702", 291 | "sha256:a40dd1e9f22e01e66ed534d6a965eb99546b41d4d52dbdb66565608fde48203f", 292 | "sha256:b4f5a82afa4f1ff482ab8ded2ae8a453a2cdfde2001567b3ca24a4c5c5ca0db3", 293 | "sha256:c009a92e81ce836212ce7aa98b219db7961a8b95999b97af566b8dc8c33e9519", 294 | "sha256:c218796d59d5abd8780170c937b812c9637e84c32f8271bbf9845970f8c1351f", 295 | "sha256:cc3cd122bea268998b79adebbb8343b735a5511ec14efb70a39e7acbc11ccbdc", 296 | "sha256:d0d8fd58df5d17ddb8c72a5075d87cd80d71b542571b5f78178fb067fa4e9c72", 297 | "sha256:e18bc3764cbb5e118be139b3b611bc3fbc5d3be42a7e827d1096f46087b395eb", 298 | "sha256:e2b83abd292194f350bb04e188f9379d36b8dfac24dd445d5c87575f3beaf789", 299 | "sha256:e7469271497960b6a781eaa930cba8af400dd59b62ec9ca2f4d31a19f2f91090", 300 | "sha256:e9dbacd22555c2d47f262ef96bb4e30880e5956169741400af8b306bbb24a273", 301 | "sha256:f6257b314fc14958f8122779e5a1557517b0f8e500cfb2bd53fa1f75a8ad0af2" 302 | ], 303 | "index": "pypi", 304 | "version": "==1.5.2" 305 | }, 306 | "pillow": { 307 | "hashes": [ 308 | "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040", 309 | "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8", 310 | "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65", 311 | "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2", 312 | "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627", 313 | "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07", 314 | "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef", 315 | "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535", 316 | "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c", 317 | "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc", 318 | "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3", 319 | "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1", 320 | "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c", 321 | "sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa", 322 | "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32", 323 | "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502", 324 | "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4", 325 | "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f", 326 | "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812", 327 | "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636", 328 | "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20", 329 | "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c", 330 | "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91", 331 | "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe", 332 | "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b", 333 | "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad", 334 | "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9", 335 | "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72", 336 | "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4", 337 | "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de", 338 | "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29", 339 | "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee", 340 | "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c", 341 | "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7", 342 | "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11", 343 | "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c", 344 | "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c", 345 | "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448", 346 | "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b", 347 | "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20", 348 | "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228", 349 | "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd", 350 | "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699", 351 | "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b", 352 | "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2", 353 | "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4", 354 | "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c", 355 | "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f", 356 | "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2", 357 | "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c", 358 | "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3", 359 | "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193", 360 | "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48", 361 | "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02", 362 | "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8", 363 | "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e", 364 | "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f", 365 | "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b", 366 | "sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74", 367 | "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb", 368 | "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0" 369 | ], 370 | "markers": "python_version >= '3.7'", 371 | "version": "==9.3.0" 372 | }, 373 | "pyparsing": { 374 | "hashes": [ 375 | "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", 376 | "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" 377 | ], 378 | "markers": "python_full_version >= '3.6.8'", 379 | "version": "==3.0.9" 380 | }, 381 | "python-dateutil": { 382 | "hashes": [ 383 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 384 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 385 | ], 386 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 387 | "version": "==2.8.2" 388 | }, 389 | "pytz": { 390 | "hashes": [ 391 | "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427", 392 | "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2" 393 | ], 394 | "version": "==2022.6" 395 | }, 396 | "seaborn": { 397 | "hashes": [ 398 | "sha256:a9eb39cba095fcb1e4c89a7fab1c57137d70a715a7f2eefcd41c9913c4d4ed65", 399 | "sha256:bb1eb1d51d3097368c187c3ef089c0288ec1fe8aa1c69fb324c68aa1d02df4c1" 400 | ], 401 | "index": "pypi", 402 | "version": "==0.12.1" 403 | }, 404 | "six": { 405 | "hashes": [ 406 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 407 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 408 | ], 409 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 410 | "version": "==1.16.0" 411 | } 412 | }, 413 | "develop": {} 414 | } 415 | --------------------------------------------------------------------------------