added sparkline at-a-glaces
This commit is contained in:
parent
6b2f5b577b
commit
eb339feb6b
9 changed files with 200 additions and 73 deletions
|
@ -28,9 +28,10 @@ type HumanLegibleTransaction struct {
|
||||||
|
|
||||||
type ChartjsData struct {
|
type ChartjsData struct {
|
||||||
Labels []string `json:"labels"`
|
Labels []string `json:"labels"`
|
||||||
DataSets []DataSet `json:"datasets"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DataSet struct {
|
|
||||||
Data []int `json:"data"`
|
Data []int `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TwoIntsItem struct {
|
||||||
|
Item1 int
|
||||||
|
Item2 int
|
||||||
|
}
|
||||||
|
|
|
@ -8,18 +8,23 @@ templ dashboard() {
|
||||||
<div class="c-s0 d-flex w-100 cr-top">
|
<div class="c-s0 d-flex w-100 cr-top">
|
||||||
<i class="my-auto c-text py-3 ps-3 ms-1" data-feather="arrow-left"></i>
|
<i class="my-auto c-text py-3 ps-3 ms-1" data-feather="arrow-left"></i>
|
||||||
<span class="mx-auto my-auto c-text">Income/Expenses</span>
|
<span class="mx-auto my-auto c-text">Income/Expenses</span>
|
||||||
<i class="my-auto c-text py-3 pe-3 me-1 ms-auto" data-feather="arrow-right"></i>
|
<i class="my-auto c-text py-3 pe-3 me-1" data-feather="arrow-right"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-100">
|
<div class="d-flex" style="height: 88%">
|
||||||
<div class="w-50 h-100">
|
<div class="w-50">
|
||||||
<canvas
|
<canvas
|
||||||
class="chartjs-chart"
|
class="chartjs-chart"
|
||||||
data-chart-endpoint="/web/dashboard/expenditure_chart"
|
data-chart-endpoint="/web/dashboard/expenditure_chart"
|
||||||
data-chart-type="bar"
|
data-chart-type="historical_vs_current"
|
||||||
id="IncomeVsExpenditureChart"
|
id="IncomeVsExpenditureChart"
|
||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-mantle">
|
<div class="w-50 c-s1" style="overflow-y: scroll">
|
||||||
|
<div class="m-4 my-5"
|
||||||
|
hx-trigger="load delay:0.25s"
|
||||||
|
hx-get="web/account_summaries"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"nickiel.net/recount_server/types"
|
"nickiel.net/recount_server/types"
|
||||||
)
|
)
|
||||||
|
@ -56,3 +57,28 @@ func getExpenditureChart(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
w.Write(json_data);
|
w.Write(json_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAccountSummaries(w http.ResponseWriter, req *http.Request) {
|
||||||
|
accounts := make([]types.TwoIntsItem, 20)
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
accounts[i] = types.TwoIntsItem {Item1: i*100, Item2: i+5}
|
||||||
|
}
|
||||||
|
component := account_summary_rows(&accounts)
|
||||||
|
component.Render(context.Background(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAccountSummaryChart(w http.ResponseWriter, req *http.Request) {
|
||||||
|
accountID := chi.URLParam(req, "accountID")
|
||||||
|
|
||||||
|
data_package := types.ChartjsData {
|
||||||
|
Labels: []string {accountID, "1/10", "1/17", "1/24"},
|
||||||
|
Data: []int {100, 0, -50, 25},
|
||||||
|
}
|
||||||
|
|
||||||
|
json_data, err := json.Marshal(data_package);
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Could not jsonify data_package");
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(json_data);
|
||||||
|
}
|
||||||
|
|
|
@ -26,3 +26,33 @@ templ transaction_rows(transactions *[]types.HumanLegibleTransaction) {
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ account_summary_rows(accounts *[]types.TwoIntsItem){
|
||||||
|
for _, value := range *accounts {
|
||||||
|
<div class="c-crust m-2" style="height: 90px">
|
||||||
|
<div class="w-100 d-flex" style="height:15px">
|
||||||
|
<span class="mx-auto">Account: {strconv.Itoa(value.Item2)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex w-100">
|
||||||
|
<div class="w-75" style="height:75px">
|
||||||
|
<canvas
|
||||||
|
class="chartjs-chart sparkline-chart"
|
||||||
|
data-chart-endpoint={"/web/dashboard/account_summary/" + strconv.Itoa(value.Item2)}
|
||||||
|
data-chart-type="sparkline_summary"
|
||||||
|
></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="w-25 d-flex">
|
||||||
|
if value.Item1 > 0 {
|
||||||
|
<span class="my-auto w-100 t-m cr-all c-mantle" style="color: var(--pf-green)">
|
||||||
|
-${strconv.Itoa(value.Item1)}
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
<span class="my-auto w-100 t-m cr-all c-mantle" style="color: var(--pf-red)">
|
||||||
|
+${strconv.Itoa(value.Item1)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -28,7 +28,9 @@ func WebRouter() http.Handler {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Get("/", getIndex)
|
r.Get("/", getIndex)
|
||||||
r.Get("/web/transaction_table_rows", getTransactions)
|
r.Get("/web/transaction_table_rows", getTransactions)
|
||||||
|
r.Get("/web/account_summaries", getAccountSummaries)
|
||||||
r.Get("/web/dashboard/expenditure_chart", getExpenditureChart)
|
r.Get("/web/dashboard/expenditure_chart", getExpenditureChart)
|
||||||
|
r.Get("/web/dashboard/account_summary/{accountID}", getAccountSummaryChart)
|
||||||
r.Get("/hello", getHello)
|
r.Get("/hello", getHello)
|
||||||
r.Handle("/chart.js/*", http.StripPrefix("/chart.js/", http.FileServer(http.Dir("web/node_modules/chart.js/"))))
|
r.Handle("/chart.js/*", http.StripPrefix("/chart.js/", http.FileServer(http.Dir("web/node_modules/chart.js/"))))
|
||||||
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static/"))))
|
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static/"))))
|
||||||
|
|
|
@ -20,6 +20,9 @@ $directions: (
|
||||||
.m-#{$size} {
|
.m-#{$size} {
|
||||||
margin: $val;
|
margin: $val;
|
||||||
}
|
}
|
||||||
|
.p-#{$size} {
|
||||||
|
padding: $val;
|
||||||
|
}
|
||||||
@each $dir, $dir-val in $directions {
|
@each $dir, $dir-val in $directions {
|
||||||
.m#{$dir}-#{$size} {
|
.m#{$dir}-#{$size} {
|
||||||
margin-#{$dir-val}: $val;
|
margin-#{$dir-val}: $val;
|
||||||
|
@ -35,7 +38,7 @@ $directions: (
|
||||||
}
|
}
|
||||||
.my-#{$size} {
|
.my-#{$size} {
|
||||||
margin-top: $val;
|
margin-top: $val;
|
||||||
margin-right: $val;
|
margin-bottom: $val;
|
||||||
}
|
}
|
||||||
.px-#{$size} {
|
.px-#{$size} {
|
||||||
padding-left: $val;
|
padding-left: $val;
|
||||||
|
@ -46,6 +49,9 @@ $directions: (
|
||||||
padding-bottom: $val;
|
padding-bottom: $val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.m-auto {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
.my-auto {
|
.my-auto {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
|
@ -107,6 +113,9 @@ $w_h_sizes: (
|
||||||
.t-e {
|
.t-e {
|
||||||
text-align: end;
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
.t-m {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
table.table {
|
table.table {
|
||||||
color: var(--#{$prefix}-text);
|
color: var(--#{$prefix}-text);
|
||||||
|
|
|
@ -27,10 +27,117 @@ function createDiagonalPattern(color = '#ffffff') {
|
||||||
return c.createPattern(shape, 'repeat')
|
return c.createPattern(shape, 'repeat')
|
||||||
}
|
}
|
||||||
|
|
||||||
function fill_charts() {
|
function historical_vs_current_chart(jsonData, element) {
|
||||||
const style = getComputedStyle(document.body);
|
const style = getComputedStyle(document.body);
|
||||||
const red_color = style.getPropertyValue("--pf-red");
|
const red_color = style.getPropertyValue("--pf-red");
|
||||||
const green_color = style.getPropertyValue("--pf-green");
|
const green_color = style.getPropertyValue("--pf-green");
|
||||||
|
const legend_bg = style.getPropertyValue("--pf-overlay2");
|
||||||
|
const config = {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: jsonData.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: "Historical",
|
||||||
|
data: [
|
||||||
|
jsonData.income_data[0],
|
||||||
|
jsonData.expenses_data[0]
|
||||||
|
],
|
||||||
|
backgroundColor: [
|
||||||
|
createDiagonalPattern(green_color),
|
||||||
|
createDiagonalPattern(red_color)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Last 30 Days",
|
||||||
|
data: [
|
||||||
|
jsonData.income_data[1],
|
||||||
|
jsonData.expenses_data[1]
|
||||||
|
],
|
||||||
|
backgroundColor: [
|
||||||
|
green_color,
|
||||||
|
red_color
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
labels: {
|
||||||
|
boxWidth: 20,
|
||||||
|
position: "bottom",
|
||||||
|
generateLabels: function(chart) {
|
||||||
|
var labels = Chart.defaults.plugins.legend.labels.generateLabels(chart);
|
||||||
|
for (var key in labels) {
|
||||||
|
if (labels[key].text == "Historical") {
|
||||||
|
labels[key].fillStyle = createDiagonalPattern(legend_bg);
|
||||||
|
} else {
|
||||||
|
labels[key].fillStyle = legend_bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
labels[key].strokeStyle = "rgba(33, 44, 22, 0.7)";
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
new Chart(element, config);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function sparkline_summary_chart(jsonData, element) {
|
||||||
|
const style = getComputedStyle(document.body);
|
||||||
|
const red_color = style.getPropertyValue("--pf-red");
|
||||||
|
const green_color = style.getPropertyValue("--pf-green");
|
||||||
|
const legend_bg = style.getPropertyValue("--pf-overlay2");
|
||||||
|
const config = {
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: jsonData.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: "Historical",
|
||||||
|
data: jsonData.data
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
display: true,
|
||||||
|
callback: function(value, index, values) {
|
||||||
|
if (value === 0) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
suggestedMin: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
new Chart(element, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fill_charts() {
|
||||||
document.querySelectorAll(".chartjs-chart").forEach(function (el) {
|
document.querySelectorAll(".chartjs-chart").forEach(function (el) {
|
||||||
var url = el.dataset.chartEndpoint;
|
var url = el.dataset.chartEndpoint;
|
||||||
var type = el.dataset.chartType;
|
var type = el.dataset.chartType;
|
||||||
|
@ -43,66 +150,11 @@ function fill_charts() {
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
}).then(jsonData => {
|
}).then(jsonData => {
|
||||||
const config = {
|
if (type == "historical_vs_current") {
|
||||||
type: type,
|
historical_vs_current_chart(jsonData, el);
|
||||||
data: {
|
} else if (type == "sparkline_summary") {
|
||||||
labels: jsonData.labels,
|
sparkline_summary_chart(jsonData, el);
|
||||||
datasets: [{
|
|
||||||
label: "Historical",
|
|
||||||
data: [
|
|
||||||
jsonData.income_data[0],
|
|
||||||
jsonData.expenses_data[0]
|
|
||||||
],
|
|
||||||
backgroundColor: [
|
|
||||||
createDiagonalPattern(green_color),
|
|
||||||
createDiagonalPattern(red_color)
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Last 30 Days",
|
|
||||||
data: [
|
|
||||||
jsonData.income_data[1],
|
|
||||||
jsonData.expenses_data[1]
|
|
||||||
],
|
|
||||||
backgroundColor: [
|
|
||||||
green_color,
|
|
||||||
red_color
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
responsive: true,
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: true,
|
|
||||||
labels: {
|
|
||||||
boxWidth: 20,
|
|
||||||
position: "bottom",
|
|
||||||
generateLabels: function(chart) {
|
|
||||||
var labels = Chart.defaults.plugins.legend.labels.generateLabels(chart);
|
|
||||||
for (var key in labels) {
|
|
||||||
if (labels[key].text == "Historical") {
|
|
||||||
labels[key].fillStyle = createDiagonalPattern("#888888");
|
|
||||||
} else {
|
|
||||||
labels[key].fillStyle = "#88888840"
|
|
||||||
}
|
|
||||||
|
|
||||||
labels[key].strokeStyle = "rgba(33, 44, 22, 0.7)";
|
|
||||||
}
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
new Chart(el, config);
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error("Unable to set up chart: ", error)
|
console.error("Unable to set up chart: ", error)
|
||||||
});
|
});
|
||||||
|
|
|
@ -50,4 +50,6 @@ function load_in_table() {
|
||||||
|
|
||||||
const trigger_table_animation = debounce(load_in_table, 100);
|
const trigger_table_animation = debounce(load_in_table, 100);
|
||||||
|
|
||||||
export {register_handlers, trigger_table_animation, fill_charts};
|
const fill_all_charts = debounce(fill_charts, 500);
|
||||||
|
|
||||||
|
export {register_handlers, trigger_table_animation, fill_all_charts};
|
||||||
|
|
|
@ -61,14 +61,14 @@
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js" integrity="sha512-CQBWl4fJHWbryGE+Pc7UAxWMUMNMWzWxF4SQo9CgkJIN1kx6djDQZjh3Y8SZ1d+6I+1zze6Z7kHXO7q3UyZAWw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js" integrity="sha512-CQBWl4fJHWbryGE+Pc7UAxWMUMNMWzWxF4SQo9CgkJIN1kx6djDQZjh3Y8SZ1d+6I+1zze6Z7kHXO7q3UyZAWw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
<script type="module" src="/static/index.js"></script>
|
<script type="module" src="/static/index.js"></script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import {register_handlers, fill_charts, trigger_table_animation} from "/static/index.js";
|
import {register_handlers, fill_all_charts, trigger_table_animation} from "/static/index.js";
|
||||||
register_handlers();
|
register_handlers();
|
||||||
feather.replace();
|
feather.replace();
|
||||||
htmx.onLoad(function (element) {
|
htmx.onLoad(function (element) {
|
||||||
if (element.localName === "tr") {
|
if (element.localName === "tr") {
|
||||||
trigger_table_animation();
|
trigger_table_animation();
|
||||||
} else {
|
} else {
|
||||||
fill_charts();
|
fill_all_charts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue