Add more statistics with graphs

This commit is contained in:
ellite 2023-11-15 12:15:30 +01:00
parent 602387df58
commit 190c052963
7 changed files with 201 additions and 28 deletions

20
scripts/libs/chart.js Normal file

File diff suppressed because one or more lines are too long

21
scripts/stats.js Normal file
View File

@ -0,0 +1,21 @@
function loadGraph(container, dataPoints, symbol, run) {
if (run) {
var ctx = document.getElementById(container).getContext('2d');
var chart = new Chart(ctx, {
type: 'pie',
data: {
datasets: [{
data: dataPoints.map(point => point.y),
}],
labels: dataPoints.map(point => `${point.label} (${point.y}${symbol})`),
},
options: {
animation: {
animateRotate: true,
animateScale: true,
},
},
});
}
}

View File

@ -540,6 +540,15 @@
</a>
</span>
</p>
<p>
Chart.js:
<span>
https://www.chartjs.org/
<a href="https://www.chartjs.org/" target="_blank" title="Visit external url">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
</a>
</span>
</p>
</div>
</section>

106
stats.php
View File

@ -51,7 +51,18 @@ while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$members[$memberId] = $row;
$memberCost[$memberId]['cost'] = 0;
$memberCost[$memberId]['name'] = $row['name'];
}
}
// Get categories
$categories = array();
$query = "SELECT * FROM categories";
$result = $db->query($query);
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$categoryId = $row['id'];
$categories[$categoryId] = $row;
$categoryCost[$categoryId]['cost'] = 0;
$categoryCost[$categoryId]['name'] = $row['name'];
}
// Get symbol of main currency to display on statistics
$query = "SELECT c.symbol
@ -76,7 +87,8 @@ $activeSubscriptions = $row['active_subscriptions'];
$mostExpensiveSubscription = 0;
$amountDueThisMonth = 0;
$totalCostPerMonth = 0;
$query = "SELECT name, price, frequency, cycle, currency_id, next_payment, payer_user_id FROM subscriptions";
$query = "SELECT name, price, frequency, cycle, currency_id, next_payment, payer_user_id, category_id FROM subscriptions";
$result = $db->query($query);
if ($result) {
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
@ -91,10 +103,12 @@ if ($result) {
$currency = $subscription['currency_id'];
$next_payment = $subscription['next_payment'];
$payerId = $subscription['payer_user_id'];
$categoryId = $subscription['category_id'];
$originalSubscriptionPrice = getPriceConverted($price, $currency, $db);
$price = getPricePerMonth($cycle, $frequency, $originalSubscriptionPrice);
$totalCostPerMonth += $price;
$memberCost[$payerId]['cost'] += $price;
$categoryCost[$categoryId]['cost'] += $price;
if ($price > $mostExpensiveSubscription) {
$mostExpensiveSubscription = $price;
}
@ -103,7 +117,13 @@ if ($result) {
if ((int)$memberCost[$payerId]['cost'] == $memberCost[$payerId]['cost']) {
$memberCost[$payerId]['cost'] = (int)$memberCost[$payerId]['cost'];
}
$categoryCost[$categoryId]['cost'] = number_format($categoryCost[$categoryId]['cost'], 2, ".", "");
if ((int)$categoryCost[$categoryId]['cost'] == $categoryCost[$categoryId]['cost']) {
$categoryCost[$categoryId]['cost'] = (int)$categoryCost[$categoryId]['cost'];
}
// Calculate ammount due this month
$nextPaymentDate = DateTime::createFromFormat('Y-m-d', trim($next_payment));
$tomorrow = new DateTime('tomorrow');
$endOfMonth = new DateTime('last day of this month');
@ -122,6 +142,7 @@ if ($result) {
}
$amountDueThisMonth += $originalSubscriptionPrice * $timesToPay;
}
}
$mostExpensiveSubscription = number_format($mostExpensiveSubscription, 2, ".", "");
@ -151,6 +172,7 @@ if ($result) {
?>
<section class="contain">
<h2>General Statistics</h2>
<div class="statistics">
<div class="statistic">
<span><?= $activeSubscriptions ?></span>
@ -178,23 +200,81 @@ if ($result) {
</div>
<?php
$numberOfElements = 6;
foreach($memberCost as $member) {
?>
<div class="statistic">
<span><?= $member['cost'] ?><?= $symbol ?></span>
<div class="title"><?= $member['name'] ?>'s monthly costs</div>
</div>
<?php
$numberOfElements++;
}
if (($numberOfElements + 1) % 3 == 0) {
?>
<div class="statistic empty"><div>
<div class="statistic empty"></div>
<?php
}
?>
?>
</div>
<h2>Split Views</h2>
<div class="graphs">
<?php
foreach ($categoryCost as $category) {
if ($category['cost'] != 0) {
$categoryDataPoints[] = [
"label" => $category['name'],
"y" => $category["cost"],
];
}
}
$showCategoryCostGraph = count($categoryCost) > 1;
foreach ($memberCost as $member) {
if ($member['cost'] != 0) {
$memberDataPoints[] = [
"label" => $member['name'],
"y" => $member["cost"],
];
}
}
$showMemberCostGraph = count($memberCost) > 1;
if ($showMemberCostGraph) {
?>
<section class="graph">
<header>
Household Split
<div class="sub-header">(Monthly cost)</div>
</header>
<canvas id="memberSplitChart"></canvas>
</section>
<?php
}
if ($showCategoryCostGraph) {
?>
<section class="graph">
<header>
Category Split
<div class="sub-header">(Monthly cost)</div>
</header>
<canvas id="categorySplitChart" style="height: 370px; width: 100%;"></canvas>
</section>
<?php
}
?>
</div>
</section>
<?php
if ($showCategoryCostGraph || $showMemberCostGraph || $showCostPerMonthGraph) {
?>
<script src="scripts/libs/chart.js"></script>
<script type="text/javascript">
window.onload = function() {
loadGraph("categorySplitChart", <?php echo json_encode($categoryDataPoints, JSON_NUMERIC_CHECK); ?>, "<?= $symbol ?>", <?= $showCategoryCostGraph ?>);
loadGraph("memberSplitChart", <?php echo json_encode($memberDataPoints, JSON_NUMERIC_CHECK); ?>, "<?= $symbol ?>", <?= $showMemberCostGraph ?>);
}
</script>
<?php
}
?>
<script src="scripts/stats.js"></script>
<?php
require_once 'includes/footer.php';
?>

View File

@ -18,7 +18,8 @@ header .logo .logo-image {
.logo-search,
.dropdown-content,
.sort-options,
.statistic {
.statistic,
.graph {
background-color: #222;
border: 1px solid #333;
box-shadow: 0 2px 5px rgba(120, 120, 120, 0.1);

View File

@ -21,7 +21,7 @@ body, html {
max-width: 100%;
padding: 20px;
background-color: #fff;
border-radius: 5px;
border-radius: 8px;
border: 1px solid #eee;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
@ -80,7 +80,7 @@ select {
padding: 15px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px;
border-radius: 8px;
outline: none;
}
@ -91,7 +91,7 @@ input[type="submit"] {
background-color: #007bff;
color: #fff;
border: none;
border-radius: 5px;
border-radius: 8px;
cursor: pointer;
}
@ -107,7 +107,7 @@ input[type="checkbox"] {
margin: 0px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
border-radius: 8px;
display: grid;
place-content: center;
}

View File

@ -165,7 +165,7 @@ main > .contain {
background-color: #007bff;
padding: 12px 30px;
font-size: 1rem;
border-radius: 5px;
border-radius: 8px;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
text-decoration: none;
}
@ -190,7 +190,7 @@ main > .contain {
background-color: #FFFFFF;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
padding: 12px 15px;
border-radius: 5px;
border-radius: 8px;
}
.subscription > span {
@ -295,7 +295,7 @@ main > .contain {
border: 1px solid #eee;
padding: 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
border-radius: 5px;
border-radius: 8px;
}
.account-section header h2 {
@ -497,6 +497,7 @@ main > .contain {
display: flex;
flex-direction: column;
gap: 16px;
line-break: anywhere;
}
.credits-list > p {
@ -586,7 +587,7 @@ select {
font-size: 16px;
background-color: #FFFFFF;
border: 1px solid #ccc;
border-radius: 5px;
border-radius: 8px;
outline: none;
color: #202020;
box-sizing: border-box;
@ -624,7 +625,7 @@ input[type="button"] {
background-color: #007bff;
color: #fff;
border: none;
border-radius: 5px;
border-radius: 8px;
cursor: pointer;
box-sizing: border-box;
border: 2px solid #007bff;
@ -674,7 +675,7 @@ input[type="checkbox"] {
margin: 0px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
border-radius: 8px;
display: grid;
place-content: center;
}
@ -697,7 +698,7 @@ input[type="checkbox"] {
overflow-x: hidden;
padding: 10px;
border: 1px solid #EEEEEE;
border-radius: 5px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
background-color: white;
box-sizing: border-box;
@ -758,7 +759,7 @@ input[type="checkbox"] {
background-color: #FFFFFF;
padding: 22px;
border: 1px solid #EEEEEE;
border-radius: 5px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
overflow: hidden;
@ -852,7 +853,7 @@ input[type="checkbox"] {
font-size: 16px;
background-color: #FFFFFF;
border: 1px solid #EEEEEE;
border-radius: 5px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
top: 48px;
@ -1040,7 +1041,7 @@ input[type="checkbox"] {
.statistic {
background-color: #FFFFFF;
border: 1px solid #EEEEEE;
border-radius: 5px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
padding: 20px 24px 30px;
display: flex;
@ -1074,4 +1075,45 @@ input[type="checkbox"] {
.statistic > .title {
text-align: center;
}
.graphs {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 20px;
justify-content: space-between;
}
.graph {
background-color: #FFFFFF;
border: 1px solid #EEEEEE;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
flex-basis: 48%;
align-items: center;
justify-content: center;
display: flex;
flex-direction: column;
padding: 20px 10px;
box-sizing: border-box;
}
.graph > header {
font-size: 18px;
font-weight: 500;
margin-bottom: 15px;
text-align: center;
}
.graph > header > .sub-header {
font-size: 13px;
font-weight: normal;
}
@media (max-width: 768px) {
.graph {
flex-basis: 100%;
max-width: 100%;
}
}