Added charts, env var for charts, fixed formatting and table columns

This commit is contained in:
Frederik Baerentsen
2025-10-01 20:52:29 +02:00
parent 5f43e979f9
commit 11f9e5782f
7 changed files with 422 additions and 105 deletions

View File

@@ -395,3 +395,12 @@
# - "bricktracker_wishes"."number_of_parts": set number of parts
# Default: "bricktracker_wishes"."rowid" DESC
# BK_WISHES_DEFAULT_ORDER="bricktracker_wishes"."set" DESC
# Optional: Show collection growth charts on the statistics page
# Default: true
# BK_STATISTICS_SHOW_CHARTS=false
# Optional: Default state of statistics page sections (expanded or collapsed)
# When true, all sections start expanded. When false, all sections start collapsed.
# Default: true
# BK_STATISTICS_DEFAULT_EXPANDED=false

View File

@@ -90,4 +90,6 @@ CONFIG: Final[list[dict[str, Any]]] = [
{'n': 'TIMEZONE', 'd': 'Etc/UTC'},
{'n': 'USE_REMOTE_IMAGES', 'c': bool},
{'n': 'WISHES_DEFAULT_ORDER', 'd': '"bricktracker_wishes"."rowid" DESC'},
{'n': 'STATISTICS_SHOW_CHARTS', 'd': True, 'c': bool},
{'n': 'STATISTICS_DEFAULT_EXPANDED', 'd': True, 'c': bool},
]

View File

@@ -15,7 +15,9 @@ SELECT
ROUND(AVG("bricktracker_sets"."purchase_price"), 2) AS "avg_price",
-- Problem statistics per theme
COALESCE(SUM("problem_stats"."missing_parts"), 0) AS "missing_parts",
COALESCE(SUM("problem_stats"."damaged_parts"), 0) AS "damaged_parts"
COALESCE(SUM("problem_stats"."damaged_parts"), 0) AS "damaged_parts",
-- Minifigure statistics per theme
COALESCE(SUM("minifigure_stats"."minifigure_count"), 0) AS "total_minifigures"
FROM "bricktracker_sets"
INNER JOIN "rebrickable_sets" ON "bricktracker_sets"."set" = "rebrickable_sets"."set"
LEFT JOIN (
@@ -26,5 +28,12 @@ LEFT JOIN (
FROM "bricktracker_parts"
GROUP BY "bricktracker_parts"."id"
) "problem_stats" ON "bricktracker_sets"."id" = "problem_stats"."id"
LEFT JOIN (
SELECT
"bricktracker_minifigures"."id",
SUM("bricktracker_minifigures"."quantity") AS "minifigure_count"
FROM "bricktracker_minifigures"
GROUP BY "bricktracker_minifigures"."id"
) "minifigure_stats" ON "bricktracker_sets"."id" = "minifigure_stats"."id"
GROUP BY "rebrickable_sets"."theme_id"
ORDER BY "set_count" DESC, "rebrickable_sets"."theme_id" ASC

View File

@@ -5,7 +5,7 @@ Provides statistics and analytics pages
import logging
from flask import Blueprint, render_template, request, url_for, redirect
from flask import Blueprint, render_template, request, url_for, redirect, current_app
from werkzeug.wrappers.response import Response
from .exceptions import exception_handler
@@ -34,6 +34,11 @@ def overview() -> str:
purchases_by_year_stats = stats.get_purchases_by_year_statistics()
year_summary = stats.get_year_summary()
# Prepare chart data for visualization (only if charts are enabled)
chart_data = {}
if current_app.config['STATISTICS_SHOW_CHARTS']:
chart_data = prepare_chart_data(sets_by_year_stats, purchases_by_year_stats)
# Get filter parameters for clickable statistics
filter_type = request.args.get('filter_type')
filter_value = request.args.get('filter_value')
@@ -53,6 +58,7 @@ def overview() -> str:
sets_by_year_statistics=sets_by_year_stats,
purchases_by_year_statistics=purchases_by_year_stats,
year_summary=year_summary,
chart_data=chart_data,
title="Statistics Overview"
)
@@ -124,4 +130,60 @@ def purchase_locations() -> str:
'statistics_purchase_locations.html',
purchase_location_statistics=purchase_stats,
title="Purchase Location Statistics"
)
)
def prepare_chart_data(sets_by_year_stats, purchases_by_year_stats):
"""Prepare data for Chart.js visualization"""
import json
# Get all years from both datasets
all_years = set()
# Add years from sets by year
if sets_by_year_stats:
for year_stat in sets_by_year_stats:
if 'year' in year_stat:
all_years.add(year_stat['year'])
# Add years from purchases by year
if purchases_by_year_stats:
for year_stat in purchases_by_year_stats:
if 'purchase_year' in year_stat:
all_years.add(int(year_stat['purchase_year']))
# Create sorted list of years
years = sorted(list(all_years))
# Initialize data arrays
sets_data = []
parts_data = []
minifigs_data = []
# Create lookup dictionaries for quick access
sets_by_year_lookup = {}
if sets_by_year_stats:
for year_stat in sets_by_year_stats:
if 'year' in year_stat:
sets_by_year_lookup[year_stat['year']] = year_stat
# Fill data arrays
for year in years:
# Get sets and parts data from sets_by_year
year_data = sets_by_year_lookup.get(year)
if year_data:
sets_data.append(year_data.get('total_sets', 0))
parts_data.append(year_data.get('total_parts', 0))
# Use actual minifigure count from the database
minifigs_data.append(year_data.get('total_minifigures', 0))
else:
sets_data.append(0)
parts_data.append(0)
minifigs_data.append(0)
return {
'years': json.dumps(years),
'sets_data': json.dumps(sets_data),
'parts_data': json.dumps(parts_data),
'minifigs_data': json.dumps(minifigs_data)
}

View File

@@ -0,0 +1,153 @@
/**
* Statistics page chart functionality
*/
document.addEventListener('DOMContentLoaded', function() {
// Check if charts are enabled and chart data exists
if (typeof window.chartData === 'undefined') {
return;
}
// Debug: Log chart data to console
console.log('Chart Data:', window.chartData);
// Common chart configuration
const commonConfig = {
type: 'line',
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
scales: {
x: {
title: {
display: true,
text: 'Release Year'
},
grid: {
display: false
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Count'
},
ticks: {
precision: 0
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: 'white',
bodyColor: 'white',
cornerRadius: 4
}
},
elements: {
point: {
radius: 3,
hoverRadius: 5
},
line: {
borderWidth: 2
}
}
}
};
// Sets Chart
const setsCanvas = document.getElementById('setsChart');
if (setsCanvas) {
const setsCtx = setsCanvas.getContext('2d');
new Chart(setsCtx, {
...commonConfig,
data: {
labels: window.chartData.years,
datasets: [{
label: 'Sets',
data: window.chartData.sets,
borderColor: '#0d6efd',
backgroundColor: 'rgba(13, 110, 253, 0.1)',
fill: true,
tension: 0.3
}]
}
});
}
// Parts Chart
const partsCanvas = document.getElementById('partsChart');
if (partsCanvas) {
const partsCtx = partsCanvas.getContext('2d');
new Chart(partsCtx, {
...commonConfig,
data: {
labels: window.chartData.years,
datasets: [{
label: 'Parts',
data: window.chartData.parts,
borderColor: '#198754',
backgroundColor: 'rgba(25, 135, 84, 0.1)',
fill: true,
tension: 0.3
}]
},
options: {
...commonConfig.options,
scales: {
...commonConfig.options.scales,
y: {
...commonConfig.options.scales.y,
title: {
display: true,
text: 'Parts Count'
}
}
}
}
});
}
// Minifigures Chart
const minifigsCanvas = document.getElementById('minifigsChart');
if (minifigsCanvas) {
const minifigsCtx = minifigsCanvas.getContext('2d');
new Chart(minifigsCtx, {
...commonConfig,
data: {
labels: window.chartData.years,
datasets: [{
label: 'Minifigures',
data: window.chartData.minifigs,
borderColor: '#fd7e14',
backgroundColor: 'rgba(253, 126, 20, 0.1)',
fill: true,
tension: 0.3
}]
},
options: {
...commonConfig.options,
scales: {
...commonConfig.options.scales,
y: {
...commonConfig.options.scales.y,
title: {
display: true,
text: 'Minifigures Count'
}
}
}
}
});
}
});

View File

@@ -12,6 +12,7 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.3.4/dist/css/datepicker-bs5.min.css">
<link href="{{ url_for('static', filename='styles.css') }}" rel="stylesheet">
<link rel="icon" type="image/png" sizes="48x48" href="{{ url_for('static', filename='brick.png') }}">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.js"></script>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
@@ -109,6 +110,9 @@
<script src="{{ url_for('static', filename='scripts/parts-bulk-operations.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/set-details.js') }}"></script>
{% endif %}
{% if request.endpoint == 'statistics.overview' %}
<script src="{{ url_for('static', filename='scripts/statistics.js') }}"></script>
{% endif %}
{% if request.endpoint == 'instructions.download' or request.endpoint == 'instructions.do_download' %}
<script src="{{ url_for('static', filename='scripts/socket/peeron.js') }}"></script>
{% endif %}

View File

@@ -21,42 +21,42 @@
<div class="text-center">
<a href="{{ url_for('set.list') }}" class="text-decoration-none">
<div class="h4 text-primary mb-0">{{ collection_summary.total_sets }}</div>
<small class="text-muted">Total Sets</small>
<small class="text-dark">Total Sets</small>
</a>
</div>
</div>
<div class="col-6">
<div class="text-center">
<div class="h4 text-info mb-0">{{ collection_summary.unique_sets }}</div>
<small class="text-muted">Unique Sets</small>
<div class="h4 text-dark mb-0">{{ collection_summary.unique_sets }}</div>
<small class="text-dark">Unique Sets</small>
</div>
</div>
<div class="col-6">
<div class="text-center">
<a href="{{ url_for('part.list') }}" class="text-decoration-none">
<div class="h4 text-success mb-0">{{ "{:,}".format(collection_summary.total_parts_count) }}</div>
<small class="text-muted">Total Parts</small>
<div class="h4 text-primary mb-0">{{ "{:,}".format(collection_summary.total_parts_count) }}</div>
<small class="text-dark">Total Parts</small>
</a>
</div>
</div>
<div class="col-6">
<div class="text-center">
<div class="h4 text-secondary mb-0">{{ collection_summary.unique_parts }}</div>
<small class="text-muted">Unique Parts</small>
<div class="h4 text-dark mb-0">{{ collection_summary.unique_parts }}</div>
<small class="text-dark">Unique Parts</small>
</div>
</div>
<div class="col-6">
<div class="text-center">
<a href="{{ url_for('minifigure.list') }}" class="text-decoration-none">
<div class="h4 text-warning mb-0">{{ collection_summary.total_minifigures_count }}</div>
<small class="text-muted">Minifigures</small>
<div class="h4 text-primary mb-0">{{ collection_summary.total_minifigures_count }}</div>
<small class="text-dark">Minifigures</small>
</a>
</div>
</div>
<div class="col-6">
<div class="text-center">
<div class="h4 text-info mb-0">{{ collection_summary.unique_minifigures }}</div>
<small class="text-muted">Unique Figs</small>
<div class="h4 text-dark mb-0">{{ collection_summary.unique_minifigures }}</div>
<small class="text-dark">Unique Figs</small>
</div>
</div>
</div>
@@ -75,30 +75,30 @@
<div class="card-body">
<div class="text-center mb-3">
<div class="h3 text-success mb-0">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(financial_summary.total_cost) }}</div>
<small class="text-muted">Total Investment</small>
<small class="text-dark">Total Investment</small>
</div>
<div class="row g-2">
<div class="col-6">
<div class="text-center">
<div class="h5 text-primary mb-0">{{ financial_summary.sets_with_price }}</div>
<small class="text-muted">Sets with Price</small>
<div class="h5 text-dark mb-0">{{ financial_summary.sets_with_price }}</div>
<small class="text-dark">Sets with Price</small>
</div>
</div>
<div class="col-6">
<div class="text-center">
<div class="h5 text-info mb-0">{{ financial_summary.percentage_with_price }}%</div>
<small class="text-muted">Price Coverage</small>
<div class="h5 text-dark mb-0">{{ financial_summary.percentage_with_price }}%</div>
<small class="text-dark">Price Coverage</small>
</div>
</div>
<div class="col-6">
<div class="text-center">
<div class="text-muted small">Average</div>
<div class="text-dark small">Average</div>
<div class="fw-bold">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(financial_summary.average_cost) }}</div>
</div>
</div>
<div class="col-6">
<div class="text-center">
<div class="text-muted small">Range</div>
<div class="text-dark small">Range</div>
<div class="fw-bold">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(financial_summary.minimum_cost) }} - {{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(financial_summary.maximum_cost) }}</div>
</div>
</div>
@@ -121,34 +121,34 @@
<div class="text-center">
<a href="{{ url_for('part.problem') }}" class="text-decoration-none">
<div class="h4 text-danger mb-0">{{ collection_summary.total_missing_parts }}</div>
<small class="text-muted">Missing Parts</small>
<small class="text-dark">Missing Parts</small>
</a>
</div>
</div>
<div class="col-6">
<div class="text-center">
<a href="{{ url_for('part.problem') }}" class="text-decoration-none">
<div class="h4 text-warning mb-0">{{ collection_summary.total_damaged_parts }}</div>
<small class="text-muted">Damaged Parts</small>
<div class="h4 text-danger mb-0">{{ collection_summary.total_damaged_parts }}</div>
<small class="text-dark">Damaged Parts</small>
</a>
</div>
</div>
<div class="col-6">
<div class="text-center">
{% if config['HIDE_ALL_STORAGES'] %}
<div class="h4 text-info mb-0">{{ collection_summary.storage_locations_used }}</div>
<div class="h4 text-dark mb-0">{{ collection_summary.storage_locations_used }}</div>
{% else %}
<a href="{{ url_for('storage.list') }}" class="text-decoration-none">
<div class="h4 text-info mb-0">{{ collection_summary.storage_locations_used }}</div>
<div class="h4 text-primary mb-0">{{ collection_summary.storage_locations_used }}</div>
</a>
{% endif %}
<small class="text-muted">Storage Locations</small>
<small class="text-dark">Storage Locations</small>
</div>
</div>
<div class="col-6">
<div class="text-center">
<div class="h4 text-secondary mb-0">{{ collection_summary.purchase_locations_used }}</div>
<small class="text-muted">Purchase Locations</small>
<div class="h4 text-dark mb-0">{{ collection_summary.purchase_locations_used }}</div>
<small class="text-dark">Purchase Locations</small>
</div>
</div>
</div>
@@ -157,6 +157,48 @@
</div>
</div>
<!-- Collection Growth Charts -->
{% if config['STATISTICS_SHOW_CHARTS'] %}
<div class="row g-3 mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapseCharts" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapseCharts">
<i class="ri-line-chart-line"></i> Collection Growth Over Time
<i class="ri-arrow-down-s-line float-end"></i>
</a>
</h5>
</div>
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapseCharts">
<div class="card-body">
<div class="row">
<div class="col-lg-4 mb-3">
<h6 class="text-center text-dark">Sets by Release Year</h6>
<div style="position: relative; height: 250px;">
<canvas id="setsChart"></canvas>
</div>
</div>
<div class="col-lg-4 mb-3">
<h6 class="text-center text-dark">Parts by Release Year</h6>
<div style="position: relative; height: 250px;">
<canvas id="partsChart"></canvas>
</div>
</div>
<div class="col-lg-4 mb-3">
<h6 class="text-center text-dark">Minifigures by Release Year</h6>
<div style="position: relative; height: 250px;">
<canvas id="minifigsChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Detailed Statistics Tables -->
<div class="row g-3">
<!-- Theme Statistics -->
@@ -165,10 +207,14 @@
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="ri-palette-line"></i> Sets by Theme
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapseThemes" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapseThemes">
<i class="ri-palette-line"></i> Sets by Theme
<i class="ri-arrow-down-s-line float-end"></i>
</a>
</h5>
</div>
<div class="card-body p-0">
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapseThemes">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
@@ -176,6 +222,7 @@
<th>Theme</th>
<th class="text-center">Sets</th>
<th class="text-center">Parts</th>
<th class="text-center">Minifigures</th>
<th class="text-center">Spent</th>
</tr>
</thead>
@@ -188,18 +235,19 @@
</a>
</td>
<td class="text-center">
<a href="{{ url_for('set.list', theme=theme.theme_id) }}" class="text-decoration-none">
<span class="badge bg-primary">{{ theme.set_count }}</span>
</a>
<small class="text-dark">{{ theme.set_count }}</small>
</td>
<td class="text-center">
<small class="text-muted">{{ "{:,}".format(theme.total_parts) }}</small>
<small class="text-dark">{{ "{:,}".format(theme.total_parts) }}</small>
</td>
<td class="text-center">
<small class="text-dark">{{ theme.total_minifigures }}</small>
</td>
<td class="text-center">
{% if theme.total_spent %}
<small class="text-success">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(theme.total_spent) }}</small>
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(theme.total_spent) }}</small>
{% else %}
<small class="text-muted">-</small>
<small class="text-dark">-</small>
{% endif %}
</td>
</tr>
@@ -209,9 +257,10 @@
</div>
{% if theme_statistics|length > 10 %}
<div class="card-footer text-center">
<small class="text-muted">Showing top 10 themes</small>
<small class="text-dark">Showing top 10 themes</small>
</div>
{% endif %}
</div>
</div>
</div>
</div>
@@ -223,10 +272,14 @@
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="ri-archive-2-line"></i> Sets by Storage
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapseStorage" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapseStorage">
<i class="ri-archive-2-line"></i> Sets by Storage
<i class="ri-arrow-down-s-line float-end"></i>
</a>
</h5>
</div>
<div class="card-body p-0">
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapseStorage">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
@@ -234,6 +287,7 @@
<th>Storage Location</th>
<th class="text-center">Sets</th>
<th class="text-center">Parts</th>
<th class="text-center">Minifigures</th>
<th class="text-center">Value</th>
</tr>
</thead>
@@ -246,18 +300,19 @@
</a>
</td>
<td class="text-center">
<a href="{{ url_for('set.list', storage=storage.storage_id) }}" class="text-decoration-none">
<span class="badge bg-info">{{ storage.set_count }}</span>
</a>
<small class="text-dark">{{ storage.set_count }}</small>
</td>
<td class="text-center">
<small class="text-muted">{{ "{:,}".format(storage.total_parts) }}</small>
<small class="text-dark">{{ "{:,}".format(storage.total_parts) }}</small>
</td>
<td class="text-center">
<small class="text-dark">{{ storage.total_minifigures }}</small>
</td>
<td class="text-center">
{% if storage.total_value %}
<small class="text-success">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(storage.total_value) }}</small>
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(storage.total_value) }}</small>
{% else %}
<small class="text-muted">-</small>
<small class="text-dark">-</small>
{% endif %}
</td>
</tr>
@@ -265,6 +320,7 @@
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@@ -278,21 +334,25 @@
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="ri-shopping-cart-line"></i> Sets by Purchase Location
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapsePurchase" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapsePurchase">
<i class="ri-shopping-cart-line"></i> Sets by Purchase Location
<i class="ri-arrow-down-s-line float-end"></i>
</a>
</h5>
</div>
<div class="card-body p-0">
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapsePurchase">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Purchase Location</th>
<th class="text-center">Sets</th>
<th class="text-center">Total Parts</th>
<th class="text-center">Parts</th>
<th class="text-center">Minifigures</th>
<th class="text-center">Total Spent</th>
<th class="text-center">Avg. Price</th>
<th class="text-center">Price Range</th>
<th class="text-center">Issues</th>
</tr>
</thead>
<tbody>
@@ -304,39 +364,33 @@
</a>
</td>
<td class="text-center">
<a href="{{ url_for('set.list', purchase_location=location.location_id) }}" class="text-decoration-none">
<span class="badge bg-secondary">{{ location.set_count }}</span>
</a>
<small class="text-dark">{{ location.set_count }}</small>
</td>
<td class="text-center">
<small class="text-muted">{{ "{:,}".format(location.total_parts) }}</small>
<small class="text-dark">{{ "{:,}".format(location.total_parts) }}</small>
</td>
<td class="text-center">
<small class="text-dark">{{ location.total_minifigures }}</small>
</td>
<td class="text-center">
{% if location.total_spent %}
<span class="text-success fw-bold">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(location.total_spent) }}</span>
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(location.total_spent) }}</small>
{% else %}
<small class="text-muted">-</small>
<small class="text-dark">-</small>
{% endif %}
</td>
<td class="text-center">
{% if location.avg_price %}
<small class="text-info">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(location.avg_price) }}</small>
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(location.avg_price) }}</small>
{% else %}
<small class="text-muted">-</small>
<small class="text-dark">-</small>
{% endif %}
</td>
<td class="text-center">
{% if location.min_price and location.max_price %}
<small class="text-muted">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(location.min_price) }}-{{ "%.0f"|format(location.max_price) }}</small>
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(location.min_price) }}-{{ "%.0f"|format(location.max_price) }}</small>
{% else %}
<small class="text-muted">-</small>
{% endif %}
</td>
<td class="text-center">
{% if location.missing_parts or location.damaged_parts %}
<small class="text-warning">{{ location.missing_parts + location.damaged_parts }}</small>
{% else %}
<small class="text-success"></small>
<small class="text-dark">-</small>
{% endif %}
</td>
</tr>
@@ -344,6 +398,7 @@
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@@ -358,11 +413,15 @@
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="ri-calendar-line"></i> Sets by Release Year
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapseReleaseYear" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapseReleaseYear">
<i class="ri-calendar-line"></i> Sets by Release Year
<i class="ri-arrow-down-s-line float-end"></i>
</a>
</h5>
<small class="text-muted">Statistics grouped by when LEGO released the sets</small>
<small class="text-dark">Statistics grouped by when LEGO released the sets</small>
</div>
<div class="card-body p-0">
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapseReleaseYear">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 table-sm">
<thead class="table-light">
@@ -371,6 +430,7 @@
<th class="text-center">Unique</th>
<th class="text-center">Total</th>
<th class="text-center">Parts</th>
<th class="text-center">Minifigures</th>
<th class="text-center">Spent</th>
<th class="text-center">Avg/Set</th>
</tr>
@@ -383,34 +443,33 @@
<strong>{{ year.year }}</strong>
</a>
{% if year.unique_themes > 1 %}
<small class="text-muted d-block">{{ year.unique_themes }} themes</small>
<small class="text-dark d-block">{{ year.unique_themes }} themes</small>
{% endif %}
</td>
<td class="text-center">
<a href="{{ url_for('set.list', year=year.year) }}" class="year-filter-link text-decoration-none" data-year="{{ year.year }}">
<span class="badge bg-secondary">{{ year.unique_sets }}</span>
</a>
<small class="text-dark">{{ year.unique_sets }}</small>
</td>
<td class="text-center">
<a href="{{ url_for('set.list', year=year.year) }}" class="year-filter-link text-decoration-none" data-year="{{ year.year }}">
<span class="badge bg-primary">{{ year.total_sets }}</span>
</a>
<small class="text-dark">{{ year.total_sets }}</small>
</td>
<td class="text-center">
<small class="text-muted">{{ "{:,}".format(year.total_parts) }}</small>
<small class="text-dark">{{ "{:,}".format(year.total_parts) }}</small>
</td>
<td class="text-center">
<small class="text-dark">{{ year.total_minifigures }}</small>
</td>
<td class="text-center">
{% if year.total_spent %}
<small class="text-success">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.total_spent) }}</small>
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.total_spent) }}</small>
{% else %}
<small class="text-muted">-</small>
<small class="text-dark">-</small>
{% endif %}
</td>
<td class="text-center">
{% if year.avg_price_per_set %}
<small class="text-info">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.avg_price_per_set) }}</small>
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.avg_price_per_set) }}</small>
{% else %}
<small class="text-muted">-</small>
<small class="text-dark">-</small>
{% endif %}
</td>
</tr>
@@ -420,9 +479,10 @@
</div>
{% if sets_by_year_statistics|length > 15 %}
<div class="card-footer text-center">
<small class="text-muted">Showing last 15 years</small>
<small class="text-dark">Showing last 15 years</small>
</div>
{% endif %}
</div>
</div>
</div>
</div>
@@ -434,11 +494,15 @@
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="ri-shopping-cart-line"></i> Purchases by Year
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapsePurchaseYear" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapsePurchaseYear">
<i class="ri-shopping-cart-line"></i> Purchases by Year
<i class="ri-arrow-down-s-line float-end"></i>
</a>
</h5>
<small class="text-muted">Statistics grouped by when you purchased the sets</small>
<small class="text-dark">Statistics grouped by when you purchased the sets</small>
</div>
<div class="card-body p-0">
<div class="collapse {{ 'show' if config['STATISTICS_DEFAULT_EXPANDED'] else '' }}" id="collapsePurchaseYear">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 table-sm">
<thead class="table-light">
@@ -447,6 +511,7 @@
<th class="text-center">Unique</th>
<th class="text-center">Total</th>
<th class="text-center">Parts</th>
<th class="text-center">Minifigures</th>
<th class="text-center">Spent</th>
<th class="text-center">Avg/Set</th>
</tr>
@@ -456,32 +521,31 @@
<tr>
<td>
<strong>{{ year.purchase_year }}</strong>
<small class="text-muted d-block">
{% if year.months_with_purchases > 1 %}{{ year.months_with_purchases }} months{% endif %}
<span class="text-info">Purchase year</span>
</small>
</td>
<td class="text-center">
<span class="badge bg-secondary">{{ year.unique_sets }}</span>
<small class="text-dark">{{ year.unique_sets }}</small>
</td>
<td class="text-center">
<span class="badge bg-primary">{{ year.total_sets }}</span>
<small class="text-dark">{{ year.total_sets }}</small>
</td>
<td class="text-center">
<small class="text-muted">{{ "{:,}".format(year.total_parts) }}</small>
<small class="text-dark">{{ "{:,}".format(year.total_parts) }}</small>
</td>
<td class="text-center">
<small class="text-dark">{{ year.total_minifigures }}</small>
</td>
<td class="text-center">
{% if year.total_spent %}
<strong class="text-success">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(year.total_spent) }}</strong>
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(year.total_spent) }}</small>
{% else %}
<small class="text-muted">-</small>
<small class="text-dark">-</small>
{% endif %}
</td>
<td class="text-center">
{% if year.avg_price_per_set %}
<small class="text-info">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.avg_price_per_set) }}</small>
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.avg_price_per_set) }}</small>
{% else %}
<small class="text-muted">-</small>
<small class="text-dark">-</small>
{% endif %}
</td>
</tr>
@@ -493,22 +557,23 @@
<div class="card-footer">
<div class="row text-center">
<div class="col-6">
<small class="text-muted">Peak Year</small><br>
<small class="text-dark">Peak Year</small><br>
{% if year_summary.peak_spending_year %}
<strong>{{ year_summary.peak_spending_year }}</strong>
<small class="text-success d-block">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year_summary.max_spending) }}</small>
{% else %}
<small class="text-muted">N/A</small>
<small class="text-dark">N/A</small>
{% endif %}
</div>
<div class="col-6">
<small class="text-muted">Active Years</small><br>
<small class="text-dark">Active Years</small><br>
<strong>{{ year_summary.years_with_purchases }}</strong>
<small class="text-muted d-block">years</small>
<small class="text-dark d-block">years</small>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
@@ -524,25 +589,25 @@
{% if year_summary.oldest_set_year and year_summary.newest_set_year %}
<div class="col-md-3">
<strong>{{ year_summary.oldest_set_year }} - {{ year_summary.newest_set_year }}</strong><br>
<small class="text-muted">Set Year Range</small>
<small class="text-dark">Set Year Range</small>
</div>
{% endif %}
{% if year_summary.peak_collection_year %}
<div class="col-md-3">
<strong>{{ year_summary.peak_collection_year }}</strong><br>
<small class="text-muted">Most Sets from Year ({{ year_summary.max_sets_in_year }} sets)</small>
<small class="text-dark">Most Sets from Year ({{ year_summary.max_sets_in_year }} sets)</small>
</div>
{% endif %}
{% if year_summary.years_represented %}
<div class="col-md-3">
<strong>{{ year_summary.years_represented }}</strong><br>
<small class="text-muted">Years Represented</small>
<small class="text-dark">Years Represented</small>
</div>
{% endif %}
{% if year_summary.years_with_purchases %}
<div class="col-md-3">
<strong>{{ year_summary.years_with_purchases }}</strong><br>
<small class="text-muted">Years with Purchases</small>
<small class="text-dark">Years with Purchases</small>
</div>
{% endif %}
</div>
@@ -551,5 +616,18 @@
</div>
{% endif %}
</div>
{% if config['STATISTICS_SHOW_CHARTS'] %}
<script type="text/javascript">
// Initialize chart data for statistics.js
window.chartData = {
years: {{ chart_data.years|safe }},
sets: {{ chart_data.sets_data|safe }},
parts: {{ chart_data.parts_data|safe }},
minifigs: {{ chart_data.minifigs_data|safe }}
};
</script>
{% endif %}
{% endblock %}