Add income and expenses chart in SVG
This commit is contained in:
parent
d1b978054b
commit
39b0b801b2
110
pkg/dashboard.go
110
pkg/dashboard.go
|
@ -1,9 +1,13 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"html/template"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -22,6 +26,7 @@ type DashboardPage struct {
|
|||
VAT string
|
||||
IRPF string
|
||||
NetIncome string
|
||||
Chart template.HTML
|
||||
Filters *dashboardFilterForm
|
||||
}
|
||||
|
||||
|
@ -53,6 +58,10 @@ func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
|
|||
filters.Period.Selected = MonthPeriod
|
||||
}
|
||||
conn := getConn(r)
|
||||
dashboard := &DashboardPage{
|
||||
Filters: filters,
|
||||
Chart: buildDashboardChart(r.Context(), conn, company, periodStart, periodEnd, filters.Period.Selected),
|
||||
}
|
||||
rows := conn.MustQuery(r.Context(), fmt.Sprintf(`
|
||||
select to_price(0, decimal_digits) as sales
|
||||
, to_price(coalesce(invoice.total, 0), decimal_digits) as income
|
||||
|
@ -101,9 +110,6 @@ func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
|
|||
`, periodStart, periodEnd), company.Id)
|
||||
defer rows.Close()
|
||||
|
||||
dashboard := &DashboardPage{
|
||||
Filters: filters,
|
||||
}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&dashboard.Sales, &dashboard.Income, &dashboard.Expenses, &dashboard.VAT, &dashboard.IRPF, &dashboard.NetIncome); err != nil {
|
||||
panic(err)
|
||||
|
@ -166,3 +172,101 @@ func (form *dashboardFilterForm) Parse(r *http.Request) error {
|
|||
form.Period.FillValue(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildDashboardChart(ctx context.Context, conn *Conn, company *Company, periodStart string, periodEnd string, selectedPeriod string) template.HTML {
|
||||
group := "yyyymmdd"
|
||||
switch selectedPeriod {
|
||||
case YearPeriod, YesteryearPeriod:
|
||||
group = "yyyymm"
|
||||
}
|
||||
rows := conn.MustQuery(ctx, fmt.Sprintf(`
|
||||
select date
|
||||
, 0 as sales
|
||||
, coalesce(invoice.total, 0) as income
|
||||
, coalesce(expense.total, 0) as expenses
|
||||
from (
|
||||
select to_char(date.invoice_date, '%[3]s')::integer as date
|
||||
, sum(total)::integer as total
|
||||
from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date)
|
||||
left join invoice on invoice.invoice_date = date.invoice_date and company_id = $1
|
||||
left join invoice_amount using (invoice_id)
|
||||
group by date
|
||||
) as invoice
|
||||
left join (
|
||||
select to_char(date.invoice_date, '%[3]s')::integer as date
|
||||
, sum(amount)::integer as total
|
||||
from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date)
|
||||
left join expense on expense.invoice_date = date.invoice_date and company_id = 1
|
||||
group by date
|
||||
) as expense using (date)
|
||||
order by date
|
||||
`, periodStart, periodEnd, group), company.Id)
|
||||
defer rows.Close()
|
||||
|
||||
type value struct {
|
||||
date int
|
||||
sales float64
|
||||
income float64
|
||||
expenses float64
|
||||
}
|
||||
var values []value
|
||||
var max = 0.
|
||||
for rows.Next() {
|
||||
var v value
|
||||
var sales int
|
||||
var income int
|
||||
var expenses int
|
||||
if err := rows.Scan(&v.date, &sales, &income, &expenses); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
v.sales = float64(sales)
|
||||
v.income = float64(income)
|
||||
v.expenses = float64(expenses)
|
||||
values = append(values, v)
|
||||
max = math.Max(v.sales, max)
|
||||
max = math.Max(v.income, max)
|
||||
max = math.Max(v.expenses, max)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
panic(rows.Err())
|
||||
}
|
||||
|
||||
width := 1024.
|
||||
height := 300.
|
||||
dataPoints := float64(len(values))
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("<svg id='income-chart' viewBox='-10 -10 %d %d'>", int(width)+20, int(height)+20))
|
||||
sb.WriteString("<polyline points='")
|
||||
for i, v := range values {
|
||||
sb.WriteString(fmt.Sprintf(" %f,%f", float64(i)/dataPoints*width, height-v.sales/max*height))
|
||||
}
|
||||
sb.WriteString("'/>")
|
||||
sb.WriteString("<polyline points='")
|
||||
for i, v := range values {
|
||||
sb.WriteString(fmt.Sprintf(" %f,%f", float64(i)/dataPoints*width, height-v.income/max*height))
|
||||
}
|
||||
sb.WriteString("'/>")
|
||||
sb.WriteString("<polyline points='")
|
||||
for i, v := range values {
|
||||
sb.WriteString(fmt.Sprintf(" %f,%f", float64(i)/dataPoints*width, height-v.expenses/max*height))
|
||||
}
|
||||
sb.WriteString("'/>")
|
||||
for i, v := range values {
|
||||
sb.WriteString(fmt.Sprintf("<circle cx='%f' cy='%f' r='4'/>", float64(i)/dataPoints*width, height-v.sales/max*height))
|
||||
sb.WriteString(fmt.Sprintf("<circle cx='%f' cy='%f' r='4'/>", float64(i)/dataPoints*width, height-v.income/max*height))
|
||||
sb.WriteString(fmt.Sprintf("<circle cx='%f' cy='%f' r='4'/>", float64(i)/dataPoints*width, height-v.expenses/max*height))
|
||||
}
|
||||
sb.WriteString("</svg>")
|
||||
return template.HTML(sb.String())
|
||||
return template.HTML(`
|
||||
<svg id="income-chart" viewBox="0 0 500 200">
|
||||
<polyline points="0,120 20,60 40,80 60,20"/>
|
||||
<polyline points="10,130 30,70 50,90 70,30"/>
|
||||
<polyline points="20,140 40,80 60,100 80,40"/>
|
||||
<circle cx="0" cy="120" r="2"/>
|
||||
<circle cx="20" cy="60" r="2"/>
|
||||
<circle cx="40" cy="80" r="2"/>
|
||||
<circle cx="60" cy="20" r="2"/>
|
||||
</svg>
|
||||
`)
|
||||
}
|
||||
|
|
|
@ -900,6 +900,29 @@ div[x-data="snackbar"] div[role="alert"].enter.end, div[x-data="snackbar"] div[r
|
|||
font-size: .66666em;
|
||||
}
|
||||
|
||||
#income-chart polyline {
|
||||
fill: none;
|
||||
stroke-width: 5;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
#income-chart polyline:nth-of-type(1) {
|
||||
stroke: var(--numerus--color--yellow);
|
||||
}
|
||||
|
||||
#income-chart polyline:nth-of-type(2) {
|
||||
stroke: var(--numerus--color--green);
|
||||
}
|
||||
|
||||
#income-chart polyline:nth-of-type(3) {
|
||||
stroke: var(--numerus--color--red);
|
||||
}
|
||||
|
||||
#income-chart circle {
|
||||
fill: var(--numerus--color--dark-gray);
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
/* Remix Icon */
|
||||
|
||||
@font-face {
|
||||
|
|
|
@ -48,4 +48,5 @@
|
|||
<dd>{{ formatPriceSpan .NetIncome }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{{ .Chart }}
|
||||
{{- end }}
|
||||
|
|
Loading…
Reference in New Issue