IFCNRUJED6H45DCCO63OOFB2TDAXH6QBB6YP6NFOCDED5QVVLO5QC
assets;Total assets including retirement
superannuation;Total assets including retirement
assets;Total assets
liabilities;Total liabilities
loan;Loan
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"name": "Wealth",
"title": {
"text": "Total wealth",
"subtitle": "Total assets and liabilities (thousand $)"
},
"width": "container",
"height": "container",
"data": { "url": "../data/wealth.csv"},
"transform": [
{ "calculate": "datum.value / 1000", "as": "thousands" }
],
"layer": [
{
"encoding": {
"x": {
"field": "end_date",
"title": null,
"type": "temporal",
"scale": { "domainMin": { "expr": "timeOffset('year', now(), -1)" } }
},
"y": {
"field": "thousands",
"title": null,
"type": "quantitative",
"aggregate": "sum",
"scale": { "zero": false }
},
"color": {
"field": "account",
"type": "nominal",
"legend": { "title": null, "orient": "bottom" }
}
},
"layer": [
{
"mark": "line",
"params": [{ "name": "grid", "select": "interval", "bind": "scales" }]
},{
"params": [{
"name": "label",
"select": { "type": "point", "encodings": ["x"], "nearest": true, "on": "mouseover" }
}],
"mark": "point",
"encoding": { "opacity": { "condition": { "param": "label", "empty": false, "value": 1 }, "value": 0 } }
}
]
},{
"transform": [{"filter": {"param": "label", "empty": false}}],
"layer": [
{
"mark": {"type": "rule", "color": "gray"},
"encoding": {
"x": {"type": "temporal", "field": "end_date", "aggregate": "min"}
}
},{
"encoding": {
"text": {"type": "quantitative", "field": "value", "aggregate": "sum", "format": "$0.3s"},
"x": {"type": "temporal", "field": "end_date"},
"y": {"type": "quantitative", "field": "thousands", "aggregate": "sum"},
"color": {"type": "nominal", "field": "account"}
},
"layer": [
{ "mark": {"type": "text", "align": "right", "dx": -5, "dy": -5, "strokeWidth": 2, "stroke": "white"} },
{ "mark": {"type": "text", "align": "right", "dx": -5, "dy": -5} }
]
}
]
}
]
}
expenses;expenses
repayments;repayments
revenue;revenue
revenuesuper;revenuesuper
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"name": "SavingsGoals",
"title": {
"text": "Savings goals",
"subtitle": "Percentage of revenues put into savings, averaged over the last half year"
},
"width": "container",
"height": "container",
"data": { "url": "../data/savings.csv"},
"transform": [
{
"window": [{"op": "sum", "field": "value", "as": "rolling_sum"}],
"groupby": ["account"],
"frame": [-181, 0]
},
{
"window": [{"op": "count", "as": "window_size"}],
"groupby": ["account"],
"frame": [-181, 0]
},
{ "filter": "datum.window_size >= 90" },
{ "pivot": "account", "groupby": ["end_date"], "value": "rolling_sum" },
{
"calculate": "datum.revenuesuper != 0 ? (datum.revenuesuper - datum.expenses) / datum.revenuesuper : 0",
"as": "Including superannuation"
},
{
"calculate": "datum.revenue != 0 ? (datum.revenue - datum.expenses) / datum.revenue : 0",
"as": "Savings"
},
{
"calculate": "datum.revenue != 0 ? (datum.revenue - datum.repayments) / datum.revenue : 0",
"as": "Liquid savings"
},
{"fold": ["Savings", "Liquid savings", "Including superannuation"], "as": ["account", "value"]},
{"calculate": "datum.value * 100", "as": "percentage"}
],
"layer": [
{
"encoding": {
"x": {
"field": "end_date",
"title": null,
"type": "temporal",
"scale": { "domainMin": { "expr": "timeOffset('year', now(), -1)" } }
},
"y": {
"field": "percentage",
"title": null,
"type": "quantitative"
},
"color": {
"field": "account",
"type": "nominal",
"legend": { "title": null, "orient": "bottom" }
}
},
"layer": [
{
"mark": "line",
"params": [{ "name": "grid", "select": "interval", "bind": "scales" }]
},{
"params": [{
"name": "label",
"select": { "type": "point", "encodings": ["x"], "nearest": true, "on": "mouseover" }
}],
"mark": "point",
"encoding": { "opacity": { "condition": { "param": "label", "empty": false, "value": 1 }, "value": 0 } }
}
]
},{
"transform": [{"filter": {"param": "label", "empty": false}}],
"layer": [
{
"mark": {"type": "rule", "color": "gray"},
"encoding": {
"x": {"type": "temporal", "field": "end_date", "aggregate": "min"}
}
},{
"encoding": {
"text": {"type": "quantitative", "field": "value", "aggregate": "sum", "format": "0.1%"},
"x": {"type": "temporal", "field": "end_date"},
"y": {"type": "quantitative", "field": "percentage", "aggregate": "sum"},
"color": {"type": "nominal", "field": "account"}
},
"layer": [
{ "mark": {"type": "text", "align": "right", "dx": -5, "dy": -5, "strokeWidth": 2, "stroke": "white"} },
{ "mark": {"type": "text", "align": "right", "dx": -5, "dy": -5} }
]
}
]
}
]
}
netposition;Net position
superannuation;Retirement
netposition;Total
superannuation;Total
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"name": "NetPosition",
"title": {
"text": "Net position",
"subtitle": "Net position (thousand $)"
},
"width": "container",
"height": "container",
"data": { "url": "../data/netposition.csv"},
"transform": [
{ "calculate": "datum.value / 1000", "as": "thousands" }
],
"layer": [
{
"encoding": {
"x": {
"field": "end_date",
"title": null,
"type": "temporal",
"scale": { "domainMin": { "expr": "timeOffset('year', now(), -1)" } }
},
"y": {
"field": "thousands",
"title": null,
"type": "quantitative",
"aggregate": "sum",
"scale": { "zero": false }
},
"color": {
"field": "account",
"type": "nominal",
"legend": { "title": null, "orient": "bottom" }
}
},
"layer": [
{
"mark": "line",
"params": [{ "name": "grid", "select": "interval", "bind": "scales" }]
},{
"params": [{
"name": "label",
"select": { "type": "point", "encodings": ["x"], "nearest": true, "on": "mouseover" }
}],
"mark": "point",
"encoding": { "opacity": { "condition": { "param": "label", "empty": false, "value": 1 }, "value": 0 } }
}
]
},{
"transform": [{"filter": {"param": "label", "empty": false}}],
"layer": [
{
"mark": {"type": "rule", "color": "gray"},
"encoding": {
"x": {"type": "temporal", "field": "end_date", "aggregate": "min"}
}
},{
"encoding": {
"text": {"type": "quantitative", "field": "value", "aggregate": "sum", "format": "$0.3s"},
"x": {"type": "temporal", "field": "end_date"},
"y": {"type": "quantitative", "field": "thousands", "aggregate": "sum"},
"color": {"type": "nominal", "field": "account"}
},
"layer": [
{ "mark": {"type": "text", "align": "right", "dx": -5, "dy": -5, "strokeWidth": 2, "stroke": "white"} },
{ "mark": {"type": "text", "align": "right", "dx": -5, "dy": -5} }
]
}
]
}
]
}
liquidassets;Liquid assets
liquidliabilities;Short-term liabilities
netliquid;Net liquid assets
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"name": "NetLiquid",
"title": {
"text": "Liquid assets",
"subtitle": "Liquid assets, net liquid assets, and short-term liabilities (thousand $)"
},
"width": "container",
"height": "container",
"data": { "url": "../data/netliquid.csv"},
"transform": [
{ "calculate": "datum.value / 1000", "as": "thousands" }
],
"layer": [
{
"encoding": {
"x": {
"field": "end_date",
"title": null,
"type": "temporal",
"scale": { "domainMin": { "expr": "timeOffset('year', now(), -1)" } }
},
"y": {
"field": "thousands",
"title": null,
"type": "quantitative",
"aggregate": "sum"
},
"color": {
"field": "account",
"type": "nominal",
"legend": { "title": null, "orient": "bottom" }
}
},
"layer": [
{
"mark": "line",
"params": [{ "name": "grid", "select": "interval", "bind": "scales" }]
},{
"params": [{
"name": "label",
"select": { "type": "point", "encodings": ["x"], "nearest": true, "on": "mouseover" }
}],
"mark": "point",
"encoding": { "opacity": { "condition": { "param": "label", "empty": false, "value": 1 }, "value": 0 } }
}
]
},{
"transform": [{"filter": {"param": "label", "empty": false}}],
"layer": [
{
"mark": {"type": "rule", "color": "gray"},
"encoding": {
"x": {"type": "temporal", "field": "end_date", "aggregate": "min"}
}
},{
"encoding": {
"text": {"type": "quantitative", "field": "value", "aggregate": "sum", "format": "$0.3s"},
"x": {"type": "temporal", "field": "end_date"},
"y": {"type": "quantitative", "field": "thousands", "aggregate": "sum"},
"color": {"type": "nominal", "field": "account"}
},
"layer": [
{ "mark": {"type": "text", "align": "right", "dx": -5, "dy": -5, "strokeWidth": 2, "stroke": "white"} },
{ "mark": {"type": "text", "align": "right", "dx": -5, "dy": -5} }
]
}
]
}
]
}
expenses;Total expenses
revenue;Total revenue
repayments;Expenses and repayments
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"name": "IncomeStatement",
"title": {
"text": "Income statement",
"subtitle": "Weekly revenues and expenses, rolling average over a two-week window ($)"
},
"width": "container",
"height": "container",
"data": { "url": "../data/incomestatement.csv"},
"transform": [
{
"window": [{"op": "mean", "field": "value", "as": "rolling_mean"}],
"groupby": ["account"],
"frame": [-13, 0]
},
{ "calculate": "datum.rolling_mean * 7", "as": "weekly" },
{ "calculate": "datum.weekly / 1000", "as": "thousands" }
],
"layer": [
{
"encoding": {
"x": {
"field": "end_date",
"title": null,
"type": "temporal",
"scale": { "domainMin": { "expr": "timeOffset('year', now(), -1)" } }
},
"y": {
"field": "thousands",
"title": null,
"type": "quantitative",
"aggregate": "sum"
},
"color": {
"field": "account",
"type": "nominal",
"legend": { "title": null, "orient": "bottom" }
}
},
"layer": [
{
"mark": "line",
"params": [{ "name": "grid", "select": "interval", "bind": "scales" }]
},{
"params": [{
"name": "label",
"select": { "type": "point", "encodings": ["x"], "nearest": true, "on": "mouseover" }
}],
"mark": "point",
"encoding": { "opacity": { "condition": { "param": "label", "empty": false, "value": 1 }, "value": 0 } }
}
]
},{
"transform": [{"filter": {"param": "label", "empty": false}}],
"layer": [
{
"mark": {"type": "rule", "color": "gray"},
"encoding": {
"x": {"type": "temporal", "field": "end_date", "aggregate": "min"}
}
},{
"encoding": {
"text": {"type": "quantitative", "field": "weekly", "aggregate": "sum", "format": "$0.3s"},
"x": {"type": "temporal", "field": "end_date"},
"y": {"type": "quantitative", "field": "thousands", "aggregate": "sum"},
"color": {"type": "nominal", "field": "account"}
},
"layer": [
{ "mark": {"type": "text", "align": "right", "dx": -5, "dy": -5, "strokeWidth": 2, "stroke": "white"} },
{ "mark": {"type": "text", "align": "right", "dx": -5, "dy": -5} }
]
}
]
}
]
}
expensecategories;
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"name": "ExpenseCategories",
"title": {
"text": "Expense breakdown",
"subtitle": "Amount per week ($)"
},
"data": { "url": "../data/expensecategories.csv"},
"transform": [
{ "calculate": "split(datum.account, ':', 2)", "as": "accountpair" },
{
"window": [{"op": "mean", "field": "value", "as": "rolling_mean"}],
"groupby": ["accountpair"],
"frame": [-13, 0]
},
{ "calculate": "datum.rolling_mean * 7", "as": "weekly" }
],
"facet": {
"field": "accountpair.0",
"type": "ordinal",
"sort": { "op": "sum", "field": "weekly", "order": "descending" },
"legend": { "title": null }
},
"columns": 2,
"resolve": { "scale": { "y": "independent", "color": "independent" } },
"params": [{
"name": "grid",
"select": "interval",
"bind": "scales"
}],
"spec": {
"width": 750,
"height": 190,
"mark": "line",
"encoding": {
"x": {
"field": "end_date",
"title": null,
"type": "temporal",
"scale": { "domainMin": { "expr": "timeOffset('year', now(), -1)" } }
},
"y": {
"field": "weekly",
"title": null,
"type": "quantitative",
"stack": true
},
"color": {
"field": "accountpair.1",
"type": "nominal",
"legend": { "title": null }
}
}
}
}
<html>
<head>
<title>Hledger charts</title>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/vega@5.21.0"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5.2.0"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6.20.2"></script>
<style>
.halfchart { float: left; width: 50%; height: 50%; }
.fullchart { float: left; width: 100%; height: 50%; }
</style>
</head>
<body>
<div class="halfchart" id="vis0"></div>
<div class="halfchart" id="vis1"></div>
<div class="halfchart" id="vis2"></div>
<div class="halfchart" id="vis3"></div>
<div class="fullchart" id="vis4"></div>
<div id="vis5"></div>
<script type="text/javascript">
vegaEmbed('#vis0', "reports/incomestatement.json");
vegaEmbed('#vis1', "reports/savings.json");
vegaEmbed('#vis2', "reports/netliquid.json");
vegaEmbed('#vis3', "reports/netposition.json");
vegaEmbed('#vis4', "reports/wealth.json");
vegaEmbed('#vis5', "reports/expensecategories.json");
</script>
</body>
</html>
#!/bin/sh
umask 077
dir="$(mktemp -d "${TMPDIR:-/tmp}/hledger-vega.XXXXXXXXXX")"
trap "rm -r $dir" EXIT
argsdir=./args
reportdir=./reports
outdir=./data
# initialize a semaphore with a given number of tokens
open_sem(){
mkfifo $dir/pipe-$$
exec 3<>$dir/pipe-$$
rm $dir/pipe-$$
local i=$1
for((;i>0;i--)); do
printf %s 000 >&3
done
}
# run the given command asynchronously and pop/push tokens
run_with_lock(){
local x
# this read waits until there is something to read
read -u 3 -n 3 x && ((0==x)) || exit $x
(
( "$@"; )
# push the return code of the command to the semaphore
printf '%.3d' $? >&3
)&
}
# Create the data directory if it doesn't exist
mkdir -p "$outdir"
# Use 4 jobs
N=4
open_sem $N
hledger_with_name() {
local namewithext="$(basename "$1")"
local name="${namewithext%.*}"
hledger balance --output-format=csv --layout=tidy --daily @"$1" \
> "$dir/$name.csv"
if [[ $(wc -l < "$dir/$name.csv") -le 1 ]]; then
>&2 echo "Warning: hledger produced no output from \"$1\""
fi
}
make_report() {
local namewithext="$(basename "$1")"
local name="${namewithext%.*}"
local IFS=";"
local first=1
while read -a line; do
local csv="$dir/${line[0]}.csv"
local label="${line[1]}"
{
if [[ $first -eq 1 ]]; then
cat "$csv"
else
tail -n "+2" "$csv"
fi
} | sed -e "s/^\"...\"/\"$label\"/"
first=0
done < "$1" \
> "$outdir/$name.csv"
}
for query in "$argsdir"/*.args; do
run_with_lock hledger_with_name "$query"
done
wait
for report in "$reportdir"/*.report; do
run_with_lock make_report "$report"
done
wait
@import/cumulative.args
^assets:.*retirement
@import/change.args
--invert
^(revenue|income)
not::taxes
@import/change.args
--invert
^(revenue|income)
not::retirement
@import/change.args
--alias=/^liabilities:(.*:loan.*)/=expenses:\1
^expenses
@import/cumulative.args
^assets
not:^assets:.*retirement
^liabilities
@import/cumulative.args
^assets
not:^assets:.*prepaids
not:^assets:.*bonds
not:^assets:.*retirement
^liabilities
not:^liabilities:.*loan
@import/cumulative.args
--invert
^liabilities:.*loan
@import/cumulative.args
--invert
^liabilities
not:^liabilities:.*loan
@import/cumulative.args
^assets
not:^assets:.*prepaids
not:^assets:.*bonds
not:^assets:.*retirement
@import/cumulative.args
--invert
^liabilities
--historical
--value=end,$
--depth=0
--value=then,$
--cost
--depth=0
@import/change.args
^expenses
@import/change.args
--alias=/^expenses:([^:]*)$/=expenses:\1:\1
--alias=/^expenses$/=expenses:uncategorised:uncategorised
--depth=3
--drop=1
^expenses
@import/cumulative.args
^assets
all: vega vega-lite vega-embed
vegaVersion = 5
vegaLiteVersion = 5
vegaEmbedVersion = 6
vega: .FORCE
curl https://cdn.jsdelivr.net/npm/vega@$(vegaVersion) > vega@$(vegaVersion)
vega-lite: .FORCE
curl https://cdn.jsdelivr.net/npm/vega-lite@$(vegaLiteVersion) > vega-lite@$(vegaLiteVersion)
vega-embed: .FORCE
curl https://cdn.jsdelivr.net/npm/vega-embed@$(vegaEmbedVersion) > vega-embed@$(vegaEmbedVersion)
.PHONY: all .FORCE
.git
.DS_Store