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 {
|
||||
Labels []string `json:"labels"`
|
||||
DataSets []DataSet `json:"datasets"`
|
||||
}
|
||||
|
||||
type DataSet struct {
|
||||
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">
|
||||
<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>
|
||||
<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 class="h-100">
|
||||
<div class="w-50 h-100">
|
||||
<div class="d-flex" style="height: 88%">
|
||||
<div class="w-50">
|
||||
<canvas
|
||||
class="chartjs-chart"
|
||||
data-chart-endpoint="/web/dashboard/expenditure_chart"
|
||||
data-chart-type="bar"
|
||||
data-chart-type="historical_vs_current"
|
||||
id="IncomeVsExpenditureChart"
|
||||
></canvas>
|
||||
</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>
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
"nickiel.net/recount_server/types"
|
||||
)
|
||||
|
@ -56,3 +57,28 @@ func getExpenditureChart(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
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.Get("/", getIndex)
|
||||
r.Get("/web/transaction_table_rows", getTransactions)
|
||||
r.Get("/web/account_summaries", getAccountSummaries)
|
||||
r.Get("/web/dashboard/expenditure_chart", getExpenditureChart)
|
||||
r.Get("/web/dashboard/account_summary/{accountID}", getAccountSummaryChart)
|
||||
r.Get("/hello", getHello)
|
||||
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/"))))
|
||||
|
|
|
@ -20,6 +20,9 @@ $directions: (
|
|||
.m-#{$size} {
|
||||
margin: $val;
|
||||
}
|
||||
.p-#{$size} {
|
||||
padding: $val;
|
||||
}
|
||||
@each $dir, $dir-val in $directions {
|
||||
.m#{$dir}-#{$size} {
|
||||
margin-#{$dir-val}: $val;
|
||||
|
@ -35,7 +38,7 @@ $directions: (
|
|||
}
|
||||
.my-#{$size} {
|
||||
margin-top: $val;
|
||||
margin-right: $val;
|
||||
margin-bottom: $val;
|
||||
}
|
||||
.px-#{$size} {
|
||||
padding-left: $val;
|
||||
|
@ -46,6 +49,9 @@ $directions: (
|
|||
padding-bottom: $val;
|
||||
}
|
||||
}
|
||||
.m-auto {
|
||||
margin: auto;
|
||||
}
|
||||
.my-auto {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
|
@ -107,6 +113,9 @@ $w_h_sizes: (
|
|||
.t-e {
|
||||
text-align: end;
|
||||
}
|
||||
.t-m {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table.table {
|
||||
color: var(--#{$prefix}-text);
|
||||
|
|
|
@ -27,10 +27,117 @@ function createDiagonalPattern(color = '#ffffff') {
|
|||
return c.createPattern(shape, 'repeat')
|
||||
}
|
||||
|
||||
function fill_charts() {
|
||||
function historical_vs_current_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: "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) {
|
||||
var url = el.dataset.chartEndpoint;
|
||||
var type = el.dataset.chartType;
|
||||
|
@ -43,66 +150,11 @@ function fill_charts() {
|
|||
}
|
||||
return response.json();
|
||||
}).then(jsonData => {
|
||||
const config = {
|
||||
type: type,
|
||||
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("#888888");
|
||||
} else {
|
||||
labels[key].fillStyle = "#88888840"
|
||||
}
|
||||
|
||||
labels[key].strokeStyle = "rgba(33, 44, 22, 0.7)";
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type == "historical_vs_current") {
|
||||
historical_vs_current_chart(jsonData, el);
|
||||
} else if (type == "sparkline_summary") {
|
||||
sparkline_summary_chart(jsonData, el);
|
||||
}
|
||||
};
|
||||
new Chart(el, config);
|
||||
}).catch(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);
|
||||
|
||||
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 type="module" src="/static/index.js"></script>
|
||||
<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();
|
||||
feather.replace();
|
||||
htmx.onLoad(function (element) {
|
||||
if (element.localName === "tr") {
|
||||
trigger_table_animation();
|
||||
} else {
|
||||
fill_charts();
|
||||
fill_all_charts();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
Loading…
Reference in a new issue