diff --git a/app.py b/app.py new file mode 100644 index 0000000..3325597 --- /dev/null +++ b/app.py @@ -0,0 +1,214 @@ +import pandas as pd +import vobject +import requests +from datetime import datetime, date +import matplotlib.pyplot as plt +import calmap +import streamlit as st +import plotly.express as px +import plotly.graph_objects as go +import json +import os + + + +# Function to load data from a JSON file +def load_data(file_path): + try: + with open(file_path, 'r') as file: + if os.path.getsize(file_path) == 0: return {} + return json.load(file) + except FileNotFoundError: + print("settings file not found") + return {} + except : + print("settings file invalid") + return {} + + +# Function to save data to a JSON file +def save_data(file_path, data): + with open(file_path, 'w') as file: + json.dump(data, file) + + +# Function to create a calendar heatmap for a specific year +def create_calendar_heatmap(event_series, year): + # Filter the series to include only the specific year + event_series_year = event_series[event_series.index.year == year] + + # Create a complete date range for the year + all_dates_year = pd.date_range(start=f'{year}-01-01', end=f'{year}-12-31') + + # Reindex the Series to include all dates in the year and fill missing dates with zero + event_series_year = event_series_year.reindex(all_dates_year, fill_value=0) + + # Create a DataFrame for Plotly + df_year = pd.DataFrame({'Date': event_series_year.index, 'Events': event_series_year.values}) + + # Generate day of the week and week of the year + df_year['DayOfWeek'] = df_year['Date'].dt.dayofweek + df_year['WeekOfYear'] = df_year['Date'].dt.isocalendar().week + df_year['Month'] = df_year['Date'].dt.strftime('%b') + + # Create the Plotly figure + fig = go.Figure(data=go.Heatmap( + z=df_year['Events'], + x=df_year['WeekOfYear'], + y=df_year['DayOfWeek'], + colorscale='YlGn', + showscale=True, + colorbar=dict( + title='# of Events', + orientation='h', # Set the color scale to horizontal + len=0.3, # Adjust the length of the color scale + thickness=10, + x=0.98, + y=1, + xanchor="right", + yanchor="bottom", # Position the color scale at the bottom + ), + hoverinfo='z+x+y' + )) + + # Set the aspect ratio to ensure square boxes + fig.update_yaxes(scaleanchor="x", scaleratio=1, constrain="domain") + + # Update the layout with the axes + fig.update_layout( + title=f'Calendar Heatmap for {year}', + yaxis=dict( + tickmode='array', + tickvals=[0, 1, 2, 3, 4, 5, 6], + ticktext=['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], + range=[-1.2, 6.5], # Adjust the range to eliminate space + automargin=True, # Adjust margins automatically + domain=[0.1,0.95] # Use available space effectively + ), + xaxis=dict( + tickmode='array', + tickvals=list(range(1, 54)), + ticktext=list(range(1, 54)), + title=dict( + text='Weeks of the Year', + standoff=0 + ), + title_standoff=0, # Adjust the distance of the title from the axis + tickangle=-90, # Adjust the angle of the week numbers + ticklen=2, + side='top', # Display x-axis ticks above the heatmap + automargin=True, # Adjust margins automatically + ), + autosize=True, + margin=dict( + l=50, + r=50, + b=30, # Increase bottom margin to fit month names + t=30, # Increase top margin to fit week numbers + pad=0 + ), + height=450, # Fixed height to avoid excessive space + ) + + # Add month annotations with arrows at the bottom + month_starts = df_year.iloc[:-10].groupby('Month')['WeekOfYear'].min().to_dict() + for month, week in month_starts.items(): + fig.add_annotation( + x=week, + y=-0.5, # Position below the x-axis + text=month, + showarrow=False, + arrowhead=2, + ax=week, + ay=0, # Position the arrow closer + xref="x", + yref="y", + #font=dict(size=12, color="black"), + xanchor='left', + yanchor='top' + ) + + return fig + + +def save_load(): + st.session_state['options'].pop("New") + save_data(settings_file, st.session_state['options']) + st.session_state['options'] = load_data(settings_file) + +def update_params(): + st.session_state['cal_url'] = st.session_state['options'][st.session_state['setup']]['cal_url'] + st.session_state['number_of_years'] = st.session_state['options'][st.session_state['setup']]['number_of_years'] + + +# set webpage title +st.title("Yearly Activity Heatmaps") + +# File path for data storage +settings_file = 'settings.json' +# Load persisted data if it exists; set default values otherwise +st.session_state['options'] = load_data(settings_file) +st.session_state['options']["New"] = { + 'cal_url':"https://canada-holidays.ca/ics?cd=true", + 'number_of_years': 3 + } + +# Get the Calendar Export link and number of years from sidebar menu +st.sidebar.header("Settings") +st.session_state['setup'] = st.sidebar.selectbox("Choose a Setup:", st.session_state['options'].keys()) +st.session_state['cal_url'] = st.sidebar.text_input("Enter The Calendar Export Link:", value=st.session_state['options'][st.session_state['setup']]['cal_url']) +st.session_state['number_of_years'] = st.sidebar.number_input("Enter the number of years:", min_value=1, max_value=10, value=st.session_state['options'][st.session_state['setup']]['number_of_years']) +update_params() +if(st.session_state['setup'] == "New"): + setup_save_name = st.sidebar.text_input("Setup Save Name:", value='yahoo') + st.session_state['options'][setup_save_name] = { + 'cal_url': st.session_state['cal_url'], + 'number_of_years': st.session_state['number_of_years'] + } + st.sidebar.button("Save",on_click=save_load) + + +response = requests.get(st.session_state['cal_url']) +if response.status_code == 200: + # Parse the .ics file + cal = vobject.readOne(response.text) +else: + print("Failed to download the file") + + + + +# Create a dictionary to store event counts by date +event_counts = {} +for component in cal.components(): + if component.name == "VEVENT": + event_date = component.dtstart.value + if isinstance(event_date, datetime): + event_date = event_date.date() + if event_date in event_counts: + event_counts[event_date] += 1 + else: + event_counts[event_date] = 1 + + + +# Convert the dictionary to a pandas Series +event_series = pd.Series(event_counts) +# Ensure the index is in datetime format +event_series.index = pd.to_datetime(event_series.index) + + +# Get the current year +current_year = datetime.now().year +# Create a list of years to plot +years_to_plot = [current_year - i for i in range(st.session_state['number_of_years'])] + + + +# Plot each year's data +for year in years_to_plot: + fig = create_calendar_heatmap(event_series, year) + st.plotly_chart(fig) + + +