split withdrawal, investment, and networth in overview page

This commit is contained in:
Anantha Kumaran 2022-04-23 09:18:31 +05:30
parent 918dd32692
commit d6818629fb
6 changed files with 102 additions and 35 deletions

View File

@ -1,7 +1,7 @@
.PHONY: docs
serve:
./node_modules/.bin/nodemon --signal SIGTERM --watch '.' --ext go --exec 'go run . serve || exit 1'
./node_modules/.bin/nodemon --signal SIGTERM --watch '.' --ext go,js,css,html --exec 'go run . serve || exit 1'
watch:
./node_modules/.bin/esbuild web/src/index.ts --bundle --watch --sourcemap --outfile=web/static/dist.js
docs:

View File

@ -8,7 +8,7 @@ import (
"github.com/spf13/viper"
)
var cfgFile string
var configFile string
var rootCmd = &cobra.Command{
Use: "paisa",
@ -24,7 +24,7 @@ func Execute() {
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ./paisa.yaml)")
rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is ./paisa.yaml)")
}
func initConfig() {
@ -35,10 +35,11 @@ func initConfig() {
return
}
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
if envConfigFile := os.Getenv("PAISA_CONFIG"); envConfigFile != "" {
viper.SetConfigFile(envConfigFile)
} else if configFile != "" {
viper.SetConfigFile(configFile)
} else {
viper.SetConfigName("paisa.yaml")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")

View File

@ -13,9 +13,10 @@ import (
)
type Networth struct {
Date time.Time `json:"date"`
Actual float64 `json:"actual"`
Gain float64 `json:"gain"`
Date time.Time `json:"date"`
InvestmentAmount float64 `json:"investment_amount"`
WithdrawalAmount float64 `json:"withdrawal_amount"`
GainAmount float64 `json:"gain_amount"`
}
func GetOverview(db *gorm.DB) gin.H {
@ -41,14 +42,22 @@ func ComputeTimeline(db *gorm.DB, postings []posting.Posting) []Networth {
pastPostings = append(pastPostings, p)
}
actual := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
if service.IsInterest(db, p) {
investment := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
if p.Amount < 0 || service.IsInterest(db, p) {
return agg
} else {
return p.Amount + agg
}
}, 0)
withdrawal := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
if p.Amount > 0 || service.IsInterest(db, p) {
return agg
} else {
return -p.Amount + agg
}
}, 0)
gain := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
if service.IsInterest(db, p) {
return p.Amount + agg
@ -56,7 +65,7 @@ func ComputeTimeline(db *gorm.DB, postings []posting.Posting) []Networth {
return service.GetMarketPrice(db, p, start) - p.Amount + agg
}
}, 0)
networths = append(networths, Networth{Date: start, Actual: actual, Gain: gain})
networths = append(networths, Networth{Date: start, InvestmentAmount: investment, WithdrawalAmount: withdrawal, GainAmount: gain})
}
return networths
}

View File

@ -1,4 +1,5 @@
import * as d3 from "d3";
window.d3 = d3;
import legend from "d3-svg-legend";
import dayjs from "dayjs";
import _ from "lodash";
@ -15,9 +16,17 @@ export default async function () {
_.each(points, (n) => (n.timestamp = dayjs(n.date)));
const current = _.last(points);
setHtml("networth", formatCurrency(current.actual + current.gain));
setHtml("investment", formatCurrency(current.actual));
setHtml("gains", formatCurrency(current.gain));
setHtml(
"networth",
formatCurrency(
current.investment_amount +
current.gain_amount -
current.withdrawal_amount
)
);
setHtml("investment", formatCurrency(current.investment_amount));
setHtml("withdrawal", formatCurrency(current.withdrawal_amount));
setHtml("gains", formatCurrency(current.gain_amount));
const start = _.min(_.map(points, (p) => p.timestamp)),
end = dayjs();
@ -34,22 +43,28 @@ export default async function () {
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const keys = ["gain", "loss"];
const colors = d3.schemeSet2;
const ordinal = d3.scaleOrdinal().domain(keys).range(colors);
const areaKeys = ["gain", "loss"];
const colors = ["#b2df8a", "#fb9a99"];
const areaScale = d3.scaleOrdinal().domain(areaKeys).range(colors);
const lineKeys = ["networth", "investment", "withdrawal"];
const lineScale = d3
.scaleOrdinal<string>()
.domain(lineKeys)
.range(["#1f77b4", "#17becf", "#ff7f0e"]);
svg
.append("g")
.attr("class", "legendOrdinal")
.attr("transform", "translate(165,3)");
.attr("transform", "translate(365,3)");
const legendOrdinal = legend
.legendColor()
.shape("rect")
.orient("horizontal")
.shapePadding(50)
.labels(keys)
.scale(ordinal);
.labels(areaKeys)
.scale(areaScale);
svg.select(".legendOrdinal").call(legendOrdinal as any);
@ -62,12 +77,12 @@ export default async function () {
.legendColor()
.shape("rect")
.orient("horizontal")
.shapePadding(0)
.shapePadding(70)
.labelOffset(22)
.shapeHeight(3)
.shapeWidth(25)
.labels(["investment"])
.scale(d3.scaleOrdinal().domain(["investment"]).range(["#333"]));
.labels(lineKeys)
.scale(lineScale);
svg.select(".legendLine").call(legendLine as any);
@ -75,8 +90,14 @@ export default async function () {
y = d3
.scaleLinear()
.range([height, 0])
.domain([0, d3.max<Networth, number>(points, (d) => d.gain + d.actual)]),
z = d3.scaleOrdinal<string>(colors).domain(keys);
.domain([
0,
d3.max<Networth, number>(
points,
(d) => d.gain_amount + d.investment_amount
)
]),
z = d3.scaleOrdinal<string>(colors).domain(areaKeys);
let area = (y0, y1) =>
d3
@ -120,7 +141,7 @@ export default async function () {
.attr(
"d",
area(height, (d) => {
return y(d.gain + d.actual);
return y(d.gain_amount + d.investment_amount);
})
);
@ -131,7 +152,7 @@ export default async function () {
.attr(
"d",
area(0, (d) => {
return y(d.gain + d.actual);
return y(d.gain_amount + d.investment_amount);
})
);
@ -142,10 +163,11 @@ export default async function () {
`url(${new URL("#clip-above", window.location.toString())})`
)
.style("fill", z("gain"))
.style("opacity", "0.8")
.attr(
"d",
area(0, (d) => {
return y(d.actual);
return y(d.investment_amount);
})
);
@ -156,16 +178,17 @@ export default async function () {
`url(${new URL("#clip-below", window.location.toString())})`
)
.style("fill", z("loss"))
.style("opacity", "0.8")
.attr(
"d",
area(height, (d) => {
return y(d.actual);
return y(d.investment_amount);
})
);
layer
.append("path")
.style("stroke", "#333")
.style("stroke", lineScale("investment"))
.style("fill", "none")
.attr(
"d",
@ -173,6 +196,33 @@ export default async function () {
.line<Networth>()
.curve(d3.curveBasis)
.x((d) => x(d.timestamp))
.y((d) => y(d.actual))
.y((d) => y(d.investment_amount))
);
layer
.append("path")
.style("stroke", lineScale("withdrawal"))
.style("fill", "none")
.attr(
"d",
d3
.line<Networth>()
.curve(d3.curveBasis)
.defined((d) => d.withdrawal_amount > 0)
.x((d) => x(d.timestamp))
.y((d) => y(d.withdrawal_amount))
);
layer
.append("path")
.style("stroke", lineScale("networth"))
.style("fill", "none")
.attr(
"d",
d3
.line<Networth>()
.curve(d3.curveBasis)
.x((d) => x(d.timestamp))
.y((d) => y(d.investment_amount + d.gain_amount - d.withdrawal_amount))
);
}

View File

@ -19,8 +19,9 @@ export interface Posting {
export interface Networth {
date: string;
actual: number;
gain: number;
investment_amount: number;
withdrawal_amount: number;
gain_amount: number;
timestamp: dayjs.Dayjs;
}

View File

@ -43,6 +43,12 @@
<p class="d3-investment title"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Withdrawal</p>
<p class="d3-withdrawal title"></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Gain</p>
@ -56,7 +62,7 @@
<div class="container is-fluid">
<div class="columns">
<div class="column is-12">
<svg id="d3-networth-timeline" width="100%" height="300"></svg>
<svg id="d3-networth-timeline" width="100%" height="500"></svg>
</div>
</div>
<div class="columns">