update to plotly
This commit is contained in:
parent
3bc937a813
commit
4ecdf2c723
2 changed files with 282 additions and 90 deletions
|
|
@ -3,15 +3,14 @@ import json
|
|||
from pathlib import Path
|
||||
from flask import Flask, render_template_string, Response
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
import matplotlib
|
||||
import plotly.graph_objects as go
|
||||
import plotly.express as px
|
||||
from plotly.subplots import make_subplots
|
||||
import plotly.io as pio
|
||||
import functools
|
||||
|
||||
matplotlib.use("agg")
|
||||
# set dark theme for plots
|
||||
sns.set_theme(style="darkgrid")
|
||||
plt.style.use("dark_background")
|
||||
# Set Plotly dark theme
|
||||
pio.templates.default = "plotly_dark"
|
||||
|
||||
app = Flask(__name__)
|
||||
DATA_DIR = Path("/var/lib/wanikani-logs")
|
||||
|
|
@ -99,102 +98,265 @@ def get_dataframe(list_of_daily_data):
|
|||
return df
|
||||
|
||||
|
||||
def get_svg_plot(df, column, title, ylabel):
|
||||
"""Generate an SVG plot for a given DataFrame column."""
|
||||
plt.figure(figsize=(10, 6), facecolor="#151519")
|
||||
plt.plot(df["date"], df[column], marker="o", label=column.capitalize())
|
||||
plt.title(title)
|
||||
plt.xlabel("Date")
|
||||
plt.ylabel(ylabel)
|
||||
# Show every 10th date label
|
||||
plt.xticks(range(0, len(df["date"]), 10), df["date"][::10], rotation=45)
|
||||
plt.grid()
|
||||
plt.legend()
|
||||
plt.gca().set_facecolor("#151519")
|
||||
plt.tight_layout()
|
||||
def get_plotly_html(df, column, title, ylabel):
|
||||
"""Generate an interactive Plotly HTML for a given DataFrame column."""
|
||||
fig = go.Figure()
|
||||
|
||||
# Save to string buffer
|
||||
import io
|
||||
fig.add_trace(go.Scatter(
|
||||
x=df["date"],
|
||||
y=df[column],
|
||||
mode='lines+markers',
|
||||
name=column.capitalize(),
|
||||
line=dict(width=2),
|
||||
marker=dict(size=6)
|
||||
))
|
||||
|
||||
buffer = io.StringIO()
|
||||
plt.savefig(buffer, format="svg", bbox_inches="tight")
|
||||
svg_content = buffer.getvalue()
|
||||
buffer.close()
|
||||
plt.close()
|
||||
|
||||
return svg_content
|
||||
|
||||
|
||||
def get_apprentice_distribution_svg(df):
|
||||
"""Generate a stacked area chart showing apprentice stage distribution over time."""
|
||||
plt.figure(figsize=(12, 8), facecolor="#151519")
|
||||
|
||||
# Create stacked area chart
|
||||
plt.stackplot(
|
||||
df["date"],
|
||||
df["apprentice_1"],
|
||||
df["apprentice_2"],
|
||||
df["apprentice_3"],
|
||||
df["apprentice_4"],
|
||||
labels=["Apprentice I", "Apprentice II", "Apprentice III", "Apprentice IV"],
|
||||
alpha=0.8,
|
||||
colors=["#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4"]
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
xaxis_title="Date",
|
||||
yaxis_title=ylabel,
|
||||
template="plotly_dark",
|
||||
plot_bgcolor='#151519',
|
||||
paper_bgcolor='#151519',
|
||||
width=1000,
|
||||
height=500,
|
||||
margin=dict(l=50, r=50, t=50, b=50)
|
||||
)
|
||||
|
||||
plt.title("Apprentice Stage Distribution Over Time", fontsize=16, pad=20)
|
||||
plt.xlabel("Date")
|
||||
plt.ylabel("Number of Items")
|
||||
# Show every 10th date label for better readability
|
||||
date_indices = list(range(0, len(df), 10))
|
||||
fig.update_xaxes(
|
||||
tickmode='array',
|
||||
tickvals=[df.iloc[i]["date"] for i in date_indices],
|
||||
ticktext=[df.iloc[i]["date"] for i in date_indices],
|
||||
tickangle=45
|
||||
)
|
||||
|
||||
# Show every 10th date label
|
||||
plt.xticks(range(0, len(df["date"]), 10), df["date"][::10], rotation=45)
|
||||
plt.grid(True, alpha=0.3)
|
||||
plt.legend(loc="upper left", bbox_to_anchor=(0, 1))
|
||||
plt.gca().set_facecolor("#151519")
|
||||
plt.tight_layout()
|
||||
return fig.to_html(include_plotlyjs=True, div_id=f"plot_{column}")
|
||||
|
||||
# Save to string buffer
|
||||
import io
|
||||
|
||||
buffer = io.StringIO()
|
||||
plt.savefig(buffer, format="svg", bbox_inches="tight")
|
||||
svg_content = buffer.getvalue()
|
||||
buffer.close()
|
||||
plt.close()
|
||||
def get_apprentice_distribution_html(df):
|
||||
"""Generate a stacked area chart showing apprentice stage distribution over time."""
|
||||
fig = go.Figure()
|
||||
|
||||
return svg_content
|
||||
# Add stacked area traces
|
||||
fig.add_trace(go.Scatter(
|
||||
x=df["date"],
|
||||
y=df["apprentice_1"],
|
||||
mode='lines',
|
||||
name='Apprentice I',
|
||||
stackgroup='one',
|
||||
fillcolor='rgba(255, 107, 107, 0.8)',
|
||||
line=dict(width=0.5, color='#ff6b6b')
|
||||
))
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
x=df["date"],
|
||||
y=df["apprentice_2"],
|
||||
mode='lines',
|
||||
name='Apprentice II',
|
||||
stackgroup='one',
|
||||
fillcolor='rgba(78, 205, 196, 0.8)',
|
||||
line=dict(width=0.5, color='#4ecdc4')
|
||||
))
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
x=df["date"],
|
||||
y=df["apprentice_3"],
|
||||
mode='lines',
|
||||
name='Apprentice III',
|
||||
stackgroup='one',
|
||||
fillcolor='rgba(69, 183, 209, 0.8)',
|
||||
line=dict(width=0.5, color='#45b7d1')
|
||||
))
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
x=df["date"],
|
||||
y=df["apprentice_4"],
|
||||
mode='lines',
|
||||
name='Apprentice IV',
|
||||
stackgroup='one',
|
||||
fillcolor='rgba(150, 206, 180, 0.8)',
|
||||
line=dict(width=0.5, color='#96ceb4')
|
||||
))
|
||||
|
||||
fig.update_layout(
|
||||
title="Apprentice Stage Distribution Over Time",
|
||||
xaxis_title="Date",
|
||||
yaxis_title="Number of Items",
|
||||
template="plotly_dark",
|
||||
plot_bgcolor='#151519',
|
||||
paper_bgcolor='#151519',
|
||||
width=1200,
|
||||
height=600,
|
||||
margin=dict(l=50, r=50, t=50, b=50),
|
||||
legend=dict(
|
||||
orientation="h",
|
||||
yanchor="bottom",
|
||||
y=1.02,
|
||||
xanchor="right",
|
||||
x=1
|
||||
)
|
||||
)
|
||||
|
||||
# Show every 10th date label for better readability
|
||||
date_indices = list(range(0, len(df), 10))
|
||||
fig.update_xaxes(
|
||||
tickmode='array',
|
||||
tickvals=[df.iloc[i]["date"] for i in date_indices],
|
||||
ticktext=[df.iloc[i]["date"] for i in date_indices],
|
||||
tickangle=45
|
||||
)
|
||||
|
||||
return fig.to_html(include_plotlyjs=True, div_id="apprentice_distribution")
|
||||
|
||||
|
||||
def generate_standalone_html(df, output_path=None):
|
||||
"""Generate a completely self-contained HTML file with all charts."""
|
||||
# Generate all chart HTML
|
||||
reviews_html = get_plotly_html(df, "num_reviews", "Daily Reviews", "Number of Reviews")
|
||||
lessons_html = get_plotly_html(df, "num_lessons", "Daily Lessons", "Number of Lessons")
|
||||
progression_html = get_plotly_html(
|
||||
df, "progression", "SRS Progression", "Progression (%)"
|
||||
)
|
||||
apprentice_distribution_html = get_apprentice_distribution_html(df)
|
||||
srs_stage_apprentice_html = get_plotly_html(
|
||||
df, "apprentice", "Apprentice Stage", "Number of Subjects"
|
||||
)
|
||||
srs_stage_guru_html = get_plotly_html(df, "guru", "Guru Stage", "Number of Subjects")
|
||||
srs_stage_master_html = get_plotly_html(
|
||||
df, "master", "Master Stage", "Number of Subjects"
|
||||
)
|
||||
srs_stage_enlightened_html = get_plotly_html(
|
||||
df, "enlightened", "Enlightened Stage", "Number of Subjects"
|
||||
)
|
||||
srs_stage_burned_html = get_plotly_html(
|
||||
df, "burned", "Burned Stage", "Number of Subjects"
|
||||
)
|
||||
|
||||
# Create complete standalone HTML
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WaniKani Statistics Dashboard</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {{
|
||||
background-color: #151519;
|
||||
color: #8b8b9c;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.chart-container {{
|
||||
margin: 20px auto;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #1e1e24;
|
||||
background-color: #1a1a1f;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}}
|
||||
h1 {{
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
margin-bottom: 40px;
|
||||
font-size: 2.5em;
|
||||
font-weight: 300;
|
||||
}}
|
||||
.dashboard-info {{
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #888;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WaniKani Statistics Dashboard</h1>
|
||||
<div class="dashboard-info">
|
||||
Interactive dashboard showing your WaniKani learning progress over time
|
||||
</div>
|
||||
|
||||
<div class="chart-container">{reviews_html}</div>
|
||||
<div class="chart-container">{lessons_html}</div>
|
||||
<div class="chart-container">{progression_html}</div>
|
||||
<div class="chart-container">{apprentice_distribution_html}</div>
|
||||
<div class="chart-container">{srs_stage_apprentice_html}</div>
|
||||
<div class="chart-container">{srs_stage_guru_html}</div>
|
||||
<div class="chart-container">{srs_stage_master_html}</div>
|
||||
<div class="chart-container">{srs_stage_enlightened_html}</div>
|
||||
<div class="chart-container">{srs_stage_burned_html}</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Save to file if output_path is provided
|
||||
if output_path:
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
print(f"Standalone HTML dashboard saved to: {output_path}")
|
||||
|
||||
return html_content
|
||||
|
||||
|
||||
@app.route("/download")
|
||||
def download_dashboard():
|
||||
"""Route to download a standalone HTML file."""
|
||||
file_names = get_zip_file_names()
|
||||
|
||||
print(f"Found {len(file_names)} zip files in {DATA_DIR}")
|
||||
list_of_daily_data = []
|
||||
for file_name in file_names:
|
||||
daily_data = load_zip(file_name)
|
||||
list_of_daily_data.append(daily_data)
|
||||
|
||||
df = get_dataframe(list_of_daily_data)
|
||||
df.sort_values(by="date", inplace=True)
|
||||
|
||||
html_content = generate_standalone_html(df)
|
||||
|
||||
response = Response(html_content, content_type="text/html")
|
||||
response.headers["Content-Disposition"] = "attachment; filename=wanikani_dashboard.html"
|
||||
return response
|
||||
|
||||
|
||||
def render_html(df):
|
||||
"""Render the DataFrame as HTML."""
|
||||
reviews_svg = get_svg_plot(df, "num_reviews", "Daily Reviews", "Number of Reviews")
|
||||
lessons_svg = get_svg_plot(df, "num_lessons", "Daily Lessons", "Number of Lessons")
|
||||
progression_svg = get_svg_plot(
|
||||
"""Render the DataFrame as HTML with interactive Plotly charts."""
|
||||
reviews_html = get_plotly_html(df, "num_reviews", "Daily Reviews", "Number of Reviews")
|
||||
lessons_html = get_plotly_html(df, "num_lessons", "Daily Lessons", "Number of Lessons")
|
||||
progression_html = get_plotly_html(
|
||||
df, "progression", "SRS Progression", "Progression (%)"
|
||||
)
|
||||
|
||||
# apprentice distribution chart
|
||||
apprentice_distribution_svg = get_apprentice_distribution_svg(df)
|
||||
apprentice_distribution_html = get_apprentice_distribution_html(df)
|
||||
|
||||
# srs stages
|
||||
srs_stage_apprentice_svg = get_svg_plot(
|
||||
srs_stage_apprentice_html = get_plotly_html(
|
||||
df, "apprentice", "Apprentice Stage", "Number of Subjects"
|
||||
)
|
||||
srs_stage_guru_svg = get_svg_plot(df, "guru", "Guru Stage", "Number of Subjects")
|
||||
srs_stage_master_svg = get_svg_plot(
|
||||
srs_stage_guru_html = get_plotly_html(df, "guru", "Guru Stage", "Number of Subjects")
|
||||
srs_stage_master_html = get_plotly_html(
|
||||
df, "master", "Master Stage", "Number of Subjects"
|
||||
)
|
||||
srs_stage_enlightened_svg = get_svg_plot(
|
||||
srs_stage_enlightened_html = get_plotly_html(
|
||||
df, "enlightened", "Enlightened Stage", "Number of Subjects"
|
||||
)
|
||||
srs_stage_burned_svg = get_svg_plot(
|
||||
srs_stage_burned_html = get_plotly_html(
|
||||
df, "burned", "Burned Stage", "Number of Subjects"
|
||||
)
|
||||
|
||||
# Render HTML with embedded SVGs
|
||||
# Render HTML with embedded Plotly charts
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WaniKani Stats</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {{
|
||||
background-color: #151519;
|
||||
|
|
@ -203,26 +365,31 @@ def render_html(df):
|
|||
margin: 0;
|
||||
padding: 20px;
|
||||
}}
|
||||
svg {{
|
||||
display: block;
|
||||
margin: 17px auto;
|
||||
background-color: transparent;
|
||||
.chart-container {{
|
||||
margin: 20px auto;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #1e1e24;
|
||||
background-color: #151519;
|
||||
}}
|
||||
h1 {{
|
||||
text-align: center;
|
||||
color: #8b8b9c;
|
||||
margin-bottom: 30px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{reviews_svg}
|
||||
{lessons_svg}
|
||||
{progression_svg}
|
||||
{apprentice_distribution_svg}
|
||||
{srs_stage_apprentice_svg}
|
||||
{srs_stage_guru_svg}
|
||||
{srs_stage_master_svg}
|
||||
{srs_stage_enlightened_svg}
|
||||
{srs_stage_burned_svg}
|
||||
<h1>WaniKani Statistics Dashboard</h1>
|
||||
<div class="chart-container">{reviews_html}</div>
|
||||
<div class="chart-container">{lessons_html}</div>
|
||||
<div class="chart-container">{progression_html}</div>
|
||||
<div class="chart-container">{apprentice_distribution_html}</div>
|
||||
<div class="chart-container">{srs_stage_apprentice_html}</div>
|
||||
<div class="chart-container">{srs_stage_guru_html}</div>
|
||||
<div class="chart-container">{srs_stage_master_html}</div>
|
||||
<div class="chart-container">{srs_stage_enlightened_html}</div>
|
||||
<div class="chart-container">{srs_stage_burned_html}</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
|
@ -259,6 +426,30 @@ def health():
|
|||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8501
|
||||
print(f"Starting WaniKani Stats Flask app on port {port}")
|
||||
app.run(host="0.0.0.0", port=port, debug=False)
|
||||
# Check if user wants to generate standalone HTML
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "generate":
|
||||
output_file = sys.argv[2] if len(sys.argv) > 2 else "wanikani_dashboard.html"
|
||||
|
||||
print("Generating standalone HTML dashboard...")
|
||||
file_names = get_zip_file_names()
|
||||
|
||||
print(f"Found {len(file_names)} zip files in {DATA_DIR}")
|
||||
list_of_daily_data = []
|
||||
for file_name in file_names:
|
||||
daily_data = load_zip(file_name)
|
||||
list_of_daily_data.append(daily_data)
|
||||
|
||||
df = get_dataframe(list_of_daily_data)
|
||||
df.sort_values(by="date", inplace=True)
|
||||
|
||||
generate_standalone_html(df, output_file)
|
||||
print(f"✅ Standalone HTML dashboard generated: {output_file}")
|
||||
print("📊 You can now open this file in any web browser to view your interactive WaniKani stats!")
|
||||
|
||||
else:
|
||||
# Start Flask server
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8501
|
||||
print(f"Starting WaniKani Stats Flask app on port {port}")
|
||||
print(f"📊 View dashboard at: http://localhost:{port}")
|
||||
print(f"💾 Download standalone HTML at: http://localhost:{port}/download")
|
||||
app.run(host="0.0.0.0", port=port, debug=False)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ let
|
|||
jinja2
|
||||
matplotlib
|
||||
seaborn
|
||||
plotly
|
||||
]
|
||||
))
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue