add gain
This commit is contained in:
parent
46eec852ec
commit
51ee391213
|
@ -0,0 +1,31 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Gain struct {
|
||||
Account string `json:"account"`
|
||||
OverviewTimeline []Overview `json:"overview_timeline"`
|
||||
}
|
||||
|
||||
func GetGain(db *gorm.DB) gin.H {
|
||||
var postings []posting.Posting
|
||||
result := db.Where("account like ?", "Asset:%").Order("date ASC").Find(&postings)
|
||||
if result.Error != nil {
|
||||
log.Fatal(result.Error)
|
||||
}
|
||||
|
||||
byAccount := lo.GroupBy(postings, func(p posting.Posting) string { return p.Account })
|
||||
var gains []Gain
|
||||
for account, ps := range byAccount {
|
||||
gains = append(gains, Gain{Account: account, OverviewTimeline: computeOverviewTimeline(db, ps)})
|
||||
}
|
||||
|
||||
return gin.H{"gain_timeline_breakdown": gains}
|
||||
}
|
|
@ -12,7 +12,7 @@ import (
|
|||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Networth struct {
|
||||
type Overview struct {
|
||||
Date time.Time `json:"date"`
|
||||
InvestmentAmount float64 `json:"investment_amount"`
|
||||
WithdrawalAmount float64 `json:"withdrawal_amount"`
|
||||
|
@ -25,12 +25,13 @@ func GetOverview(db *gorm.DB) gin.H {
|
|||
if result.Error != nil {
|
||||
log.Fatal(result.Error)
|
||||
}
|
||||
networthTimeline := ComputeTimeline(db, postings)
|
||||
return gin.H{"networth_timeline": networthTimeline}
|
||||
|
||||
overviewTimeline := computeOverviewTimeline(db, postings)
|
||||
return gin.H{"overview_timeline": overviewTimeline}
|
||||
}
|
||||
|
||||
func ComputeTimeline(db *gorm.DB, postings []posting.Posting) []Networth {
|
||||
var networths []Networth
|
||||
func computeOverviewTimeline(db *gorm.DB, postings []posting.Posting) []Overview {
|
||||
var networths []Overview
|
||||
|
||||
var p posting.Posting
|
||||
var pastPostings []posting.Posting
|
||||
|
@ -65,7 +66,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, InvestmentAmount: investment, WithdrawalAmount: withdrawal, GainAmount: gain})
|
||||
networths = append(networths, Overview{Date: start, InvestmentAmount: investment, WithdrawalAmount: withdrawal, GainAmount: gain})
|
||||
}
|
||||
return networths
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ func Listen(db *gorm.DB) {
|
|||
router.GET("/api/investment", func(c *gin.Context) {
|
||||
c.JSON(200, GetInvestment(db))
|
||||
})
|
||||
router.GET("/api/gain", func(c *gin.Context) {
|
||||
c.JSON(200, GetGain(db))
|
||||
})
|
||||
router.GET("/api/allocation", func(c *gin.Context) {
|
||||
c.JSON(200, GetAllocation(db))
|
||||
})
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
import * as d3 from "d3";
|
||||
import legend from "d3-svg-legend";
|
||||
import dayjs from "dayjs";
|
||||
import _ from "lodash";
|
||||
import { ajax, formatCurrencyCrude, Overview } from "./utils";
|
||||
|
||||
export default async function () {
|
||||
const { gain_timeline_breakdown: gains } = await ajax("/api/gain");
|
||||
_.each(gains, (g) =>
|
||||
_.each(g.overview_timeline, (o) => (o.timestamp = dayjs(o.date)))
|
||||
);
|
||||
|
||||
renderLegend();
|
||||
|
||||
const start = _.min(
|
||||
_.flatMap(gains, (g) => _.map(g.overview_timeline, (o) => o.timestamp))
|
||||
),
|
||||
end = dayjs();
|
||||
|
||||
const svgs = d3
|
||||
.select("#d3-gain-timeline-breakdown")
|
||||
.selectAll("svg")
|
||||
.data(_.sortBy(gains, (g) => g.account));
|
||||
|
||||
svgs.exit().remove();
|
||||
|
||||
svgs
|
||||
.enter()
|
||||
.append("svg")
|
||||
.attr("width", "100%")
|
||||
.attr("height", "150")
|
||||
.each(function (gain) {
|
||||
renderOverviewSmall(gain.overview_timeline, this, gain.account, [
|
||||
start,
|
||||
end
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
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"]);
|
||||
|
||||
function renderOverviewSmall(
|
||||
points: Overview[],
|
||||
element: Element,
|
||||
account: string,
|
||||
xDomain: [dayjs.Dayjs, dayjs.Dayjs]
|
||||
) {
|
||||
const svg = d3.select(element),
|
||||
margin = { top: 15, right: 80, bottom: 20, left: 40 },
|
||||
width = element.parentElement.clientWidth - margin.left - margin.right,
|
||||
height = +svg.attr("height") - margin.top - margin.bottom,
|
||||
g = svg
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
const x = d3.scaleTime().range([0, width]).domain(xDomain),
|
||||
y = d3
|
||||
.scaleLinear()
|
||||
.range([height, 0])
|
||||
.domain([
|
||||
0,
|
||||
d3.max<Overview, number>(
|
||||
points,
|
||||
(d) => d.gain_amount + d.investment_amount
|
||||
)
|
||||
]),
|
||||
z = d3.scaleOrdinal<string>(colors).domain(areaKeys);
|
||||
|
||||
let area = (y0, y1) =>
|
||||
d3
|
||||
.area<Overview>()
|
||||
.curve(d3.curveBasis)
|
||||
.x((d) => x(d.timestamp))
|
||||
.y0(y0)
|
||||
.y1(y1);
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis x")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(d3.axisBottom(x));
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis y")
|
||||
.attr("transform", `translate(${width},0)`)
|
||||
.call(
|
||||
d3.axisRight(y).ticks(5).tickPadding(5).tickFormat(formatCurrencyCrude)
|
||||
);
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis y")
|
||||
.call(
|
||||
d3.axisLeft(y).ticks(5).tickSize(-width).tickFormat(formatCurrencyCrude)
|
||||
);
|
||||
|
||||
const layer = g
|
||||
.selectAll(".layer")
|
||||
.data([points])
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "layer");
|
||||
|
||||
const clipAboveID = _.uniqueId("clip-above");
|
||||
layer
|
||||
.append("clipPath")
|
||||
.attr("id", clipAboveID)
|
||||
.append("path")
|
||||
.attr(
|
||||
"d",
|
||||
area(height, (d) => {
|
||||
return y(d.gain_amount + d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
const clipBelowID = _.uniqueId("clip-below");
|
||||
layer
|
||||
.append("clipPath")
|
||||
.attr("id", clipBelowID)
|
||||
.append("path")
|
||||
.attr(
|
||||
"d",
|
||||
area(0, (d) => {
|
||||
return y(d.gain_amount + d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.attr(
|
||||
"clip-path",
|
||||
`url(${new URL("#" + clipAboveID, window.location.toString())})`
|
||||
)
|
||||
.style("fill", z("gain"))
|
||||
.style("opacity", "0.8")
|
||||
.attr(
|
||||
"d",
|
||||
area(0, (d) => {
|
||||
return y(d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.attr(
|
||||
"clip-path",
|
||||
`url(${new URL("#" + clipBelowID, window.location.toString())})`
|
||||
)
|
||||
.style("fill", z("loss"))
|
||||
.style("opacity", "0.8")
|
||||
.attr(
|
||||
"d",
|
||||
area(height, (d) => {
|
||||
return y(d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.style("stroke", lineScale("investment"))
|
||||
.style("fill", "none")
|
||||
.attr(
|
||||
"d",
|
||||
d3
|
||||
.line<Overview>()
|
||||
.curve(d3.curveBasis)
|
||||
.x((d) => x(d.timestamp))
|
||||
.y((d) => y(d.investment_amount))
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.style("stroke", lineScale("withdrawal"))
|
||||
.style("fill", "none")
|
||||
.attr(
|
||||
"d",
|
||||
d3
|
||||
.line<Overview>()
|
||||
.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<Overview>()
|
||||
.curve(d3.curveBasis)
|
||||
.x((d) => x(d.timestamp))
|
||||
.y((d) => y(d.investment_amount + d.gain_amount - d.withdrawal_amount))
|
||||
);
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.append("text")
|
||||
.attr("transform", "translate(80,12)")
|
||||
.text(account);
|
||||
}
|
||||
|
||||
function renderLegend() {
|
||||
const svg = d3.select("#d3-gain-legend");
|
||||
svg
|
||||
.append("g")
|
||||
.attr("class", "legendOrdinal")
|
||||
.attr("transform", "translate(315,3)");
|
||||
|
||||
const legendOrdinal = legend
|
||||
.legendColor()
|
||||
.shape("rect")
|
||||
.orient("horizontal")
|
||||
.shapePadding(50)
|
||||
.labels(areaKeys)
|
||||
.scale(areaScale);
|
||||
|
||||
svg.select(".legendOrdinal").call(legendOrdinal as any);
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("class", "legendLine")
|
||||
.attr("transform", "translate(30,3)");
|
||||
|
||||
const legendLine = legend
|
||||
.legendColor()
|
||||
.shape("rect")
|
||||
.orient("horizontal")
|
||||
.shapePadding(70)
|
||||
.labelOffset(22)
|
||||
.shapeHeight(3)
|
||||
.shapeWidth(25)
|
||||
.labels(lineKeys)
|
||||
.scale(lineScale);
|
||||
|
||||
svg.select(".legendLine").call(legendLine as any);
|
||||
}
|
|
@ -11,12 +11,14 @@ import allocation from "./allocation";
|
|||
import investment from "./investment";
|
||||
import ledger from "./ledger";
|
||||
import overview from "./overview";
|
||||
import gain from "./gain";
|
||||
|
||||
const tabs = {
|
||||
overview: _.once(overview),
|
||||
investment: _.once(investment),
|
||||
allocation: _.once(allocation),
|
||||
ledger: _.once(ledger)
|
||||
ledger: _.once(ledger),
|
||||
gain: _.once(gain)
|
||||
};
|
||||
|
||||
let tippyInstances: Instance[] = [];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as d3 from "d3";
|
||||
window.d3 = d3;
|
||||
import { ContainerElement } from "d3";
|
||||
import legend from "d3-svg-legend";
|
||||
import dayjs from "dayjs";
|
||||
import _ from "lodash";
|
||||
|
@ -7,12 +7,12 @@ import {
|
|||
ajax,
|
||||
formatCurrency,
|
||||
formatCurrencyCrude,
|
||||
Networth,
|
||||
Overview,
|
||||
setHtml
|
||||
} from "./utils";
|
||||
|
||||
export default async function () {
|
||||
const { networth_timeline: points } = await ajax("/api/overview");
|
||||
const { overview_timeline: points } = await ajax("/api/overview");
|
||||
_.each(points, (n) => (n.timestamp = dayjs(n.date)));
|
||||
|
||||
const current = _.last(points);
|
||||
|
@ -28,16 +28,16 @@ export default async function () {
|
|||
setHtml("withdrawal", formatCurrency(current.withdrawal_amount));
|
||||
setHtml("gains", formatCurrency(current.gain_amount));
|
||||
|
||||
renderOverview(points, document.getElementById("d3-overview-timeline"));
|
||||
}
|
||||
|
||||
function renderOverview(points: Overview[], element: Element) {
|
||||
const start = _.min(_.map(points, (p) => p.timestamp)),
|
||||
end = dayjs();
|
||||
|
||||
const svg = d3.select("#d3-networth-timeline"),
|
||||
const svg = d3.select(element),
|
||||
margin = { top: 40, right: 80, bottom: 20, left: 40 },
|
||||
width =
|
||||
document.getElementById("d3-networth-timeline").parentElement
|
||||
.clientWidth -
|
||||
margin.left -
|
||||
margin.right,
|
||||
width = element.parentElement.clientWidth - margin.left - margin.right,
|
||||
height = +svg.attr("height") - margin.top - margin.bottom,
|
||||
g = svg
|
||||
.append("g")
|
||||
|
@ -52,6 +52,141 @@ export default async function () {
|
|||
.scaleOrdinal<string>()
|
||||
.domain(lineKeys)
|
||||
.range(["#1f77b4", "#17becf", "#ff7f0e"]);
|
||||
const x = d3.scaleTime().range([0, width]).domain([start, end]),
|
||||
y = d3
|
||||
.scaleLinear()
|
||||
.range([height, 0])
|
||||
.domain([
|
||||
0,
|
||||
d3.max<Overview, number>(
|
||||
points,
|
||||
(d) => d.gain_amount + d.investment_amount
|
||||
)
|
||||
]),
|
||||
z = d3.scaleOrdinal<string>(colors).domain(areaKeys);
|
||||
|
||||
let area = (y0, y1) =>
|
||||
d3
|
||||
.area<Overview>()
|
||||
.curve(d3.curveBasis)
|
||||
.x((d) => x(d.timestamp))
|
||||
.y0(y0)
|
||||
.y1(y1);
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis x")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(d3.axisBottom(x));
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis y")
|
||||
.attr("transform", `translate(${width},0)`)
|
||||
.call(d3.axisRight(y).tickPadding(5).tickFormat(formatCurrencyCrude));
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis y")
|
||||
.call(d3.axisLeft(y).tickSize(-width).tickFormat(formatCurrencyCrude));
|
||||
|
||||
const layer = g
|
||||
.selectAll(".layer")
|
||||
.data([points])
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "layer");
|
||||
|
||||
const clipAboveID = _.uniqueId("clip-above");
|
||||
layer
|
||||
.append("clipPath")
|
||||
.attr("id", clipAboveID)
|
||||
.append("path")
|
||||
.attr(
|
||||
"d",
|
||||
area(height, (d) => {
|
||||
return y(d.gain_amount + d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
const clipBelowID = _.uniqueId("clip-below");
|
||||
layer
|
||||
.append("clipPath")
|
||||
.attr("id", clipBelowID)
|
||||
.append("path")
|
||||
.attr(
|
||||
"d",
|
||||
area(0, (d) => {
|
||||
return y(d.gain_amount + d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.attr(
|
||||
"clip-path",
|
||||
`url(${new URL("#" + clipAboveID, window.location.toString())})`
|
||||
)
|
||||
.style("fill", z("gain"))
|
||||
.style("opacity", "0.8")
|
||||
.attr(
|
||||
"d",
|
||||
area(0, (d) => {
|
||||
return y(d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.attr(
|
||||
"clip-path",
|
||||
`url(${new URL("#" + clipBelowID, window.location.toString())})`
|
||||
)
|
||||
.style("fill", z("loss"))
|
||||
.style("opacity", "0.8")
|
||||
.attr(
|
||||
"d",
|
||||
area(height, (d) => {
|
||||
return y(d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.style("stroke", lineScale("investment"))
|
||||
.style("fill", "none")
|
||||
.attr(
|
||||
"d",
|
||||
d3
|
||||
.line<Overview>()
|
||||
.curve(d3.curveBasis)
|
||||
.x((d) => x(d.timestamp))
|
||||
.y((d) => y(d.investment_amount))
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.style("stroke", lineScale("withdrawal"))
|
||||
.style("fill", "none")
|
||||
.attr(
|
||||
"d",
|
||||
d3
|
||||
.line<Overview>()
|
||||
.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<Overview>()
|
||||
.curve(d3.curveBasis)
|
||||
.x((d) => x(d.timestamp))
|
||||
.y((d) => y(d.investment_amount + d.gain_amount - d.withdrawal_amount))
|
||||
);
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
|
@ -85,144 +220,4 @@ export default async function () {
|
|||
.scale(lineScale);
|
||||
|
||||
svg.select(".legendLine").call(legendLine as any);
|
||||
|
||||
const x = d3.scaleTime().range([0, width]).domain([start, end]),
|
||||
y = d3
|
||||
.scaleLinear()
|
||||
.range([height, 0])
|
||||
.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
|
||||
.area<Networth>()
|
||||
.curve(d3.curveBasis)
|
||||
.x((d) => x(d.timestamp))
|
||||
.y0(y0)
|
||||
.y1(y1);
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis x")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(d3.axisBottom(x));
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis y")
|
||||
.attr("transform", `translate(${width},0)`)
|
||||
.call(
|
||||
d3
|
||||
.axisRight(y)
|
||||
.tickPadding(5)
|
||||
.tickSize(10)
|
||||
.tickFormat(formatCurrencyCrude)
|
||||
);
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis y")
|
||||
.call(d3.axisLeft(y).tickSize(-width).tickFormat(formatCurrencyCrude));
|
||||
|
||||
const layer = g
|
||||
.selectAll(".layer")
|
||||
.data([points])
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "layer");
|
||||
|
||||
layer
|
||||
.append("clipPath")
|
||||
.attr("id", `clip-above`)
|
||||
.append("path")
|
||||
.attr(
|
||||
"d",
|
||||
area(height, (d) => {
|
||||
return y(d.gain_amount + d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
layer
|
||||
.append("clipPath")
|
||||
.attr("id", `clip-below`)
|
||||
.append("path")
|
||||
.attr(
|
||||
"d",
|
||||
area(0, (d) => {
|
||||
return y(d.gain_amount + d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.attr(
|
||||
"clip-path",
|
||||
`url(${new URL("#clip-above", window.location.toString())})`
|
||||
)
|
||||
.style("fill", z("gain"))
|
||||
.style("opacity", "0.8")
|
||||
.attr(
|
||||
"d",
|
||||
area(0, (d) => {
|
||||
return y(d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.attr(
|
||||
"clip-path",
|
||||
`url(${new URL("#clip-below", window.location.toString())})`
|
||||
)
|
||||
.style("fill", z("loss"))
|
||||
.style("opacity", "0.8")
|
||||
.attr(
|
||||
"d",
|
||||
area(height, (d) => {
|
||||
return y(d.investment_amount);
|
||||
})
|
||||
);
|
||||
|
||||
layer
|
||||
.append("path")
|
||||
.style("stroke", lineScale("investment"))
|
||||
.style("fill", "none")
|
||||
.attr(
|
||||
"d",
|
||||
d3
|
||||
.line<Networth>()
|
||||
.curve(d3.curveBasis)
|
||||
.x((d) => x(d.timestamp))
|
||||
.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))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export interface Posting {
|
|||
timestamp: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
export interface Networth {
|
||||
export interface Overview {
|
||||
date: string;
|
||||
investment_amount: number;
|
||||
withdrawal_amount: number;
|
||||
|
@ -26,6 +26,11 @@ export interface Networth {
|
|||
timestamp: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
export interface Gain {
|
||||
account: string;
|
||||
overview_timeline: Overview[];
|
||||
}
|
||||
|
||||
export interface Breakdown {
|
||||
group: string;
|
||||
investment_amount: number;
|
||||
|
@ -49,9 +54,13 @@ export function ajax(
|
|||
export function ajax(
|
||||
route: "/api/ledger"
|
||||
): Promise<{ postings: Posting[]; breakdowns: Breakdown[] }>;
|
||||
export function ajax(
|
||||
route: "/api/overview"
|
||||
): Promise<{ networth_timeline: Networth[] }>;
|
||||
export function ajax(route: "/api/overview"): Promise<{
|
||||
overview_timeline: Overview[];
|
||||
overview_timeline_breakdown: { [key: string]: Overview[] };
|
||||
}>;
|
||||
export function ajax(route: "/api/gain"): Promise<{
|
||||
gain_timeline_breakdown: Gain[];
|
||||
}>;
|
||||
export function ajax(route: "/api/allocation"): Promise<{
|
||||
aggregates: { [key: string]: Aggregate };
|
||||
aggregates_timeline: { [key: string]: Aggregate }[];
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<div class="navbar-start">
|
||||
<a id="overview" class="navbar-item is-active">Overview</a>
|
||||
<a id="investment" class="navbar-item">Investment</a>
|
||||
<a id="gain" class="navbar-item">Gain</a>
|
||||
<a id="allocation" class="navbar-item">Allocation</a>
|
||||
<a id="ledger" class="navbar-item">Ledger</a>
|
||||
</div>
|
||||
|
@ -62,7 +63,7 @@
|
|||
<div class="container is-fluid">
|
||||
<div class="columns">
|
||||
<div class="column is-12">
|
||||
<svg id="d3-networth-timeline" width="100%" height="500"></svg>
|
||||
<svg id="d3-overview-timeline" width="100%" height="500"></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
|
@ -73,6 +74,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container is-fluid">
|
||||
<div class="columns">
|
||||
<div id="d3-overview-timeline-breakdown" class="column is-12">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section tab-investment">
|
||||
<div class="container is-fluid">
|
||||
|
@ -106,6 +113,21 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section tab-gain">
|
||||
<div class="container is-fluid">
|
||||
<div class="columns">
|
||||
<div class="column is-12">
|
||||
<svg id="d3-gain-legend" width="100%" height="50"></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container is-fluid">
|
||||
<div class="columns">
|
||||
<div id="d3-gain-timeline-breakdown" class="column is-12">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section tab-allocation">
|
||||
<div class="container is-fluid">
|
||||
<div class="columns">
|
||||
|
|
Loading…
Reference in New Issue